[
  {
    "path": ".agents/skills/task-think/PROMPTS.md",
    "content": "# Phase Prompts\n\nUse these templates as Codex subagent messages. Use them as same-session checklists only for Phase 0, intentional main-session build work, Phase 7, or when delegation is unavailable from the start. Replace `<TASK>`, `<PROJECT>`, `<LETTER>`, and `<REPO_ROOT>`.\n\n## Orchestration Rules\n\n- Phase 0 runs in the main session.\n- When delegation is available, use a fresh subagent for Phase 1, Phase 2, Phase 3, each Phase 4 implementation unit, and each Phase 6 pass. Do not switch those phases to same-session midstream because of a timeout or missing artifact.\n- Phase 7 runs in the main session on Windows because it depends on the final local diff and touched-file set.\n- Write each phase prompt to `.ai/<PROJECT>/<LETTER>/logs/phase-<name>.prompt.md` before execution.\n- If you delegate a phase, send the prompt file contents as the initial `spawn_agent` message.\n- When writing the phase prompt file, append the standard progress file contract and the standard compact reply block below so the subagent knows how to surface progress before the final artifact.\n- After each phase completes, write `.ai/<PROJECT>/<LETTER>/logs/phase-<name>.result.md` summarizing the status, files touched, and any follow-up notes.\n- Use `fork_context: false` by default. If the phase depends on thread-only context or UI attachments, pass that context explicitly or enable `fork_context` only for that phase.\n- Prefer `worker` for phases that write files. Use `default` for plan or review passes if that fits the host better. Use `explorer` only for narrow read-only questions.\n- When supported, request `model: gpt-5.4` and `reasoning_effort: xhigh` for delegated phases.\n- Default wait budget for delegated phases is 5 minutes while the phase is clearly still in progress. Successful completion may wake earlier, so this does not delay finished work.\n- When a phase appears close to landing, use 1-2 minute waits until it finishes.\n- A `wait_agent` timeout is not failure. On timeout, inspect both the expected artifact and the matching progress file before deciding anything.\n- If the expected artifact exists and shows progress, wait again.\n- If the expected artifact is not ready but the progress file mtime moved or its heartbeat counter increased since the previous check, wait again. Prefer mtime checks first and avoid rereading the file unless you need detail. Do not count that as a failed wait.\n- If neither the expected artifact nor the progress file moved since the previous blocked check, send one short follow-up asking the same agent to refresh the progress file, finish the required artifact, and return the standard compact reply block, then wait again.\n- If the same agent still produces no usable artifact and no meaningful progress-file movement after two full default waits and one follow-up, close it and retry the phase in a fresh subagent.\n- For Phase 1, Phase 2, Phase 3, Phase 4, and Phase 6, if delegated retries still fail, stop and ask the user rather than rerunning the phase locally.\n- Never use `codex exec`, background shell child processes, or JSONL child-session logging from this skill.\n\n## Standard Progress File Contract\n\nAppend this verbatim to every delegated phase prompt:\n\n```text\nBefore deep work, create or update the matching progress file in `.ai/<PROJECT>/<LETTER>/logs/`.\n\nUse `<phase-name>.progress.md` as a concise heartbeat with:\n- `Heartbeat: <N>` on the first line, incremented on each meaningful update\n- Current step\n- Files being read or edited\n- Concrete findings or decisions so far\n- Blocker or next checkpoint\n\nUpdate it sparingly: preferably at natural milestones, and otherwise only after a longer quiet stretch such as roughly 5-10 minutes.\nKeep it tiny so the parent can usually rely on file mtime or the heartbeat counter instead of rereading the whole file.\nDo not wait until the final artifact to write progress.\n```\n\n## Standard Compact Reply Block\n\nAppend this verbatim to every delegated phase prompt:\n\n```text\nBefore replying in chat, write the required artifact(s) to disk.\n\nReply in 8 lines or fewer using exactly these keys:\nSTATUS: <DONE|BLOCKED|APPROVED|NEEDS_CHANGES>\nARTIFACTS: <paths>\nTOUCHED: <repo paths or none>\nBLOCKER: <none or one short line>\n\nDo not restate the full context, plan, diff, or long reasoning in the chat reply.\n```\n\n## Artifact-Based Completion Checks\n\n- Phase 1 is complete only when `about.md` and `context.md` both exist and are non-empty.\n- Phase 2 is complete only when `plan.md` exists, contains a `## Status` section, and no unintended source edits were made.\n- Phase 3 is complete only when `plan.md` contains both `Phases:` in the Status section and `Assessed: yes`.\n- Phase 4 is complete only when the target phase checkbox changed to checked and the touched-file list matches the owned write set, or the blocker explains any mismatch.\n- Phase 5 is complete only when the build outcome is known and the build checkbox is updated on success.\n- Phase 6a is complete only when `review<R>.md` exists and contains a verdict line.\n- Phase 6b is complete only when the requested fixes were applied and the post-fix build outcome is known.\n\n## Phase 0: Setup\n\nRecord the current time now and store it as `$START_TIME`. You will use this at the end to display total elapsed time.\n\nBefore running any phase prompts, determine whether this is a new project or a follow-up task.\n\nFollow-up detection:\n1. Extract the first word or token from the task description. Call it `FIRST_TOKEN`.\n2. Check `.ai/` to see existing project names.\n3. Check whether `.ai/<FIRST_TOKEN>/about.md` exists.\n4. If the file exists, this is a follow-up task. The project name is `FIRST_TOKEN`. The task description is everything after `FIRST_TOKEN`.\n5. If the file does not exist, this is a new project. The full input is the task description.\n\nDo not proceed until you have determined follow-up vs new.\n\nFor new projects:\n- Using the list of existing projects, pick a unique short name (1-2 lowercase words, hyphen-separated) that does not collide.\n- Create `.ai/<PROJECT>/`, `.ai/<PROJECT>/a/`, and `.ai/<PROJECT>/a/logs/`.\n- Set `<LETTER>` = `a`.\n\nFor follow-up tasks:\n- Scan `.ai/<PROJECT>/` for existing task folders (`a/`, `b/`, ...). Find the latest one (highest letter).\n- The previous task letter = that highest letter.\n- The new task letter = next letter in sequence.\n- Create `.ai/<PROJECT>/<LETTER>/` and `.ai/<PROJECT>/<LETTER>/logs/`.\n\nThen proceed to Phase 1. Follow-up tasks do not skip context gathering. They use a modified Phase 1F prompt.\n\n## Phase 1: Context (New Project, letter = `a`)\n\n```text\nYou are a context-gathering agent for a large C++ codebase (Telegram Desktop).\n\nTASK: <TASK>\n\nYOUR JOB: Read AGENTS.md, inspect the codebase, find all files and code relevant to this task, and write two documents.\n\nSteps:\n1. Read AGENTS.md for project conventions and build instructions.\n2. Search the codebase for files, classes, functions, and patterns related to the task.\n3. Read all potentially relevant files. Be thorough and prefer reading more rather than less.\n4. For each relevant file, note:\n   - file path\n   - relevant line ranges\n   - what the code does and how it relates to the task\n   - key data structures, function signatures, and patterns used\n5. Look for similar existing features that could serve as a reference implementation.\n6. Check api.tl if the task involves Telegram API.\n7. Check .style files if the task involves UI.\n8. Check lang.strings if the task involves user-visible text.\n\nWrite two files.\n\nFile 1: .ai/<PROJECT>/about.md\n\nThis 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 phase should rely on it during the current task.\n\nWrite it as if the project is already fully implemented and working. It should contain:\n- Project: What this project does (feature description, goals, scope)\n- Architecture: High-level architectural decisions, which modules are involved, how they interact\n- Key Design Decisions: Important choices made about the approach\n- Relevant Codebase Areas: Which parts of the codebase this project touches, key types and APIs involved\n\nDo not include temporal state like \"Current State\", \"Pending Changes\", \"Not yet implemented\", or \"TODO\". Describe the project as a complete, coherent whole.\n\nFile 2: .ai/<PROJECT>/a/context.md\n\nThis is the primary task-specific implementation context. All downstream phases should be able to work from this file plus the referenced source files. It must be self-contained. Include:\n- Task Description: The full task restated clearly\n- Relevant Files: Every file path with line ranges and descriptions\n- Key Code Patterns: How similar things are done in the codebase, with snippets when useful\n- Data Structures: Relevant types, structs, classes\n- API Methods: Any TL schema methods involved, copied from api.tl when useful\n- UI Styles: Any relevant style definitions\n- Localization: Any relevant string keys\n- Build Info: Build command and any special notes\n- Reference Implementations: Similar features that can serve as templates\n\nBe extremely thorough. Another agent with no prior context will rely on this file.\n\nDo not implement code in this phase.\n```\n\n## Phase 1F: Context (Follow-up Task, letter = `b`, `c`, ...)\n\n```text\nYou are a context-gathering agent for a follow-up task on an existing project in a large C++ codebase (Telegram Desktop).\n\nNEW TASK: <TASK>\n\nYOUR JOB: Read the existing project state, gather any additional context needed, and produce fresh documents for the new task.\n\nSteps:\n1. Read AGENTS.md for project conventions and build instructions.\n2. Read .ai/<PROJECT>/about.md. This is the project-level blueprint describing everything done so far.\n3. Read .ai/<PROJECT>/<PREV_LETTER>/context.md. This is the previous task's gathered context.\n4. Understand what has already been implemented by reading the actual source files referenced in about.md and the previous context.\n5. 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.\n6. Read all newly relevant files thoroughly.\n\nWrite two files.\n\nFile 1: .ai/<PROJECT>/about.md (rewrite)\n\nRewrite this file instead of appending to it. 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.\n\nIt should incorporate:\n- everything from the old about.md that is still accurate and relevant\n- the new task's functionality described as part of the project, not as a pending change\n- any changed design decisions or architectural updates from the new task requirements\n\nIt should not contain:\n- temporal state such as \"Current State\", \"Pending Changes\", or \"TODO\"\n- history of how requirements changed between tasks\n- references to \"the old approach\" versus \"the new approach\"\n- task-by-task changelog or timeline\n- information that contradicts the new task requirements\n\nFile 2: .ai/<PROJECT>/<LETTER>/context.md\n\nThis is the primary document for the new task. It must be self-contained and should include:\n- Task Description: The new task restated clearly, with enough project background that an implementation agent can understand it without reading any other .ai files\n- Relevant Files: Every file path with line ranges relevant to this task\n- Key Code Patterns: How similar things are done in the codebase\n- Data Structures: Relevant types, structs, classes\n- API Methods: Any TL schema methods involved\n- UI Styles: Any relevant style definitions\n- Localization: Any relevant string keys\n- Build Info: Build command and any special notes\n- Reference Implementations: Similar features that can serve as templates\n\nBe extremely thorough. Another agent with no prior context should be able to work from this file alone.\n\nDo not implement code in this phase.\n```\n\n## Phase 2: Plan\n\n```text\nYou are a planning agent. You must create a detailed implementation plan.\n\nRead these files:\n- .ai/<PROJECT>/<LETTER>/context.md\n- Then read the specific source files referenced in context.md to understand the code deeply.\n\nCreate a detailed plan in: .ai/<PROJECT>/<LETTER>/plan.md\n\nThe plan.md should contain:\n\n## Task\n<one-line summary>\n\n## Approach\n<high-level description of the implementation approach>\n\n## Files to Modify\n<list of files that will be created or modified>\n\n## Files to Create\n<list of new files, if any>\n\n## Implementation Steps\n\nEach step must be specific enough that an agent can execute it without ambiguity:\n- exact file paths\n- exact function names\n- what code to add, modify, or remove\n- where exactly in the file (after which function, in which class, and so on)\n\nNumber every step. Group steps into phases if there are more than about eight steps.\n\n### Phase 1: <name>\n1. <specific step>\n2. <specific step>\n\n### Phase 2: <name> (if needed)\n1. <specific step>\n\n## Build Verification\n- build command to run\n- expected outcome\n\n## Status\n- [ ] Phase 1: <name>\n- [ ] Phase 2: <name> (if applicable)\n- [ ] Build verification\n- [ ] Code review\n\nDo not implement code in this phase.\n```\n\n## Phase 3: Plan Assessment\n\n```text\nYou are a plan assessment agent. Review and refine an implementation plan.\n\nRead these files:\n- .ai/<PROJECT>/<LETTER>/context.md\n- .ai/<PROJECT>/<LETTER>/plan.md\n- Then read the actual source files referenced to verify the plan makes sense.\n\nAssess the plan:\n\n1. Correctness: Are the file paths and line references accurate? Does the plan reference real functions and types?\n2. Completeness: Are there missing steps? Edge cases not handled?\n3. Code quality: Will the plan minimize code duplication? Does it follow existing codebase patterns from AGENTS.md?\n4. Design: Could the approach be improved? Are there better patterns already used in the codebase?\n5. Phase sizing: Each phase should be implementable by a single agent in one session. If a phase has more than about 8-10 substantive code changes, split it further.\n\nUpdate plan.md with your refinements. Keep the same structure but:\n- fix any inaccuracies\n- add missing steps\n- improve the approach if you found better patterns\n- ensure phases are properly sized for single-agent execution\n- add a line at the top of the Status section: `Phases: <N>`\n- add `Assessed: yes` at the bottom of the file\n\nIf the plan is small enough for a single agent (roughly 8 steps or fewer), mark it as a single phase.\n\nDo not implement code in this phase.\n```\n\n## Phase 4: Implementation\n\nRun one implementation unit per plan phase. Keep implementation phases sequential by default. Parallelize only if their write sets are disjoint and the plan makes that safe.\n\nFor each phase in the plan that is not yet marked as done, use this prompt:\n\n```text\nYou are an implementation agent working on phase <N> of an implementation plan.\n\nRead these files first:\n- .ai/<PROJECT>/<LETTER>/context.md\n- .ai/<PROJECT>/<LETTER>/plan.md\n\nThen read the source files you will be modifying.\n\nYour owned write set for this phase:\n<OWNED_WRITE_SET>\n\nYOUR TASK: Implement only Phase <N> from the plan:\n<paste the specific phase steps here>\n\nRules:\n- Follow the plan precisely.\n- Follow AGENTS.md coding conventions.\n- You are not alone in the codebase. Respect existing changes and do not revert unrelated work.\n- Do not modify .ai/ files except to update the Status section in plan.md.\n- When done, update plan.md Status section: change `- [ ] Phase <N>: ...` to `- [x] Phase <N>: ...`\n- Do not work on other phases.\n\nWhen finished, report what you did, which files you changed, and any issues encountered.\n```\n\nAfter each implementation phase:\n1. Use a narrow read or search to confirm the status line was updated.\n2. Verify the owned write set and touched files with a small diff summary such as `git diff --name-only`.\n3. If more phases remain, run the next implementation phase.\n4. If all phases are done, proceed to build verification.\n\n## Phase 5: Build Verification\n\nOnly run this phase if the task modified project source code.\n\nPrefer running the build in the main session because it is critical-path work. If you delegate it, use a worker subagent and wait immediately for the result.\n\n```text\nYou are a build verification agent.\n\nRead these files:\n- .ai/<PROJECT>/<LETTER>/context.md\n- .ai/<PROJECT>/<LETTER>/plan.md\n\nThe implementation is complete. Your job is to build the project and fix any build errors that block the planned work.\n\nSteps:\n1. Run (from repository root): cmake --build ./out --config Debug --target Telegram\n2. If the build succeeds, update plan.md: change `- [ ] Build verification` to `- [x] Build verification`\n3. If the build fails:\n   a. Read the error messages carefully\n   b. Read the relevant source files\n   c. Fix the errors in accordance with the plan and AGENTS.md conventions\n   d. Rebuild and repeat until the build passes\n   e. Update plan.md status when done\n\nRules:\n- Only fix build errors. Do not refactor or improve code beyond what is needed for a passing build.\n- Follow AGENTS.md conventions.\n- If build fails with file-locked errors (C1041, LNK1104, \"cannot open output file\", or similar access-denied lock issues), stop and report the lock. Do not retry.\n- You are not alone in the codebase. Respect existing changes and do not revert unrelated work.\n\nWhen finished, report the build result and which files, if any, you changed.\n```\n\n## Phase 6: Code Review Loop\n\nAfter build verification passes, run up to 3 review-fix iterations. Set iteration counter `R = 1`.\n\nReview loop:\n\n```text\nLOOP:\n  1. Run review phase 6a with iteration R.\n  2. Read review<R>.md verdict:\n     - \"APPROVED\" -> go to FINISH\n     - \"NEEDS_CHANGES\" -> run fix phase 6b\n  3. After fix work completes and build passes:\n     R = R + 1\n     If R > 3 -> go to FINISH\n     Otherwise -> go to step 1\n\nFINISH:\n  - Update plan.md: change `- [ ] Code review` to `- [x] Code review`\n  - Proceed to Phase 7 on Windows, otherwise proceed to Completion\n```\n\n### Step 6a: Code Review\n\n```text\nYou are a code review agent for Telegram Desktop (C++ / Qt).\n\nRead these files:\n- .ai/<PROJECT>/<LETTER>/context.md\n- .ai/<PROJECT>/<LETTER>/plan.md\n- REVIEW.md\n- If R > 1, also read .ai/<PROJECT>/<LETTER>/review<R-1>.md\n\nThen run `git diff` to see the current uncommitted changes for this task.\n\nRead the modified source files in full to understand the changes in context.\n\nPerform a focused code review using these criteria, in order:\n\n1. Correctness and safety: Obvious logic errors, missing null checks at API boundaries, potential crashes, use-after-free, dangling references, race conditions.\n2. Dead code: Added or left-behind code that is never used within the scope of the changes.\n3. Redundant changes: Diff hunks that have no functional effect.\n4. Code duplication: Repeated logic that should be shared.\n5. Wrong placement: Code added to a module where it does not logically belong.\n6. Function decomposition: Whether an extracted helper would clearly improve readability.\n7. Module structure: Only in exceptional cases where a large new chunk of code clearly belongs elsewhere.\n8. Style compliance: REVIEW.md rules and AGENTS.md conventions.\n\nImportant guidelines:\n- Review only the changes made, not pre-existing code outside the scope of the task.\n- Be pragmatic. Each suggestion should have a clear, concrete benefit.\n- Do not suggest comments, docstrings, or over-engineering.\n\nWrite your review to: .ai/<PROJECT>/<LETTER>/review<R>.md\n\nThe review document should contain:\n\n## Code Review - Iteration <R>\n\n## Summary\n<1-2 sentence overall assessment>\n\n## Verdict: <APPROVED or NEEDS_CHANGES>\n\nIf the verdict is NEEDS_CHANGES, continue with:\n\n## Changes Required\n\n### <Issue 1 title>\n- Category: <dead code | duplication | wrong placement | function decomposition | module structure | style | correctness>\n- File(s): <file paths>\n- Problem: <clear description>\n- Fix: <specific description of what to change>\n\nKeep the list focused. Prioritize the most impactful issues.\n\nWhen finished, report your verdict clearly as: APPROVED or NEEDS_CHANGES.\n```\n\n### Step 6b: Review Fix\n\n```text\nYou are a review fix agent. You implement improvements identified during code review.\n\nRead these files:\n- .ai/<PROJECT>/<LETTER>/context.md\n- .ai/<PROJECT>/<LETTER>/plan.md\n- .ai/<PROJECT>/<LETTER>/review<R>.md\n\nThen read the source files mentioned in the review.\n\nYOUR TASK: Implement all changes listed in review<R>.md.\n\nRules:\n- Implement exactly the review changes, nothing more.\n- Follow AGENTS.md coding conventions.\n- You are not alone in the codebase. Respect existing changes and do not revert unrelated work.\n- Do not modify .ai/ files except where the review process explicitly requires it.\n\nAfter all changes are made:\n1. Build (from repository root): cmake --build ./out --config Debug --target Telegram\n2. If the build fails, fix build errors and rebuild until it passes.\n3. If build fails with file-locked errors (C1041, LNK1104, \"cannot open output file\", or similar access-denied lock issues), stop and report the lock. Do not retry.\n\nWhen finished, report what changes were made and which files you touched.\n```\n\n## Phase 7: Windows Text Normalization\n\nRun this phase only on Windows hosts and only after the review loop has finished.\n\nUse the current task's result logs as the source of truth for what Codex touched. Do not sweep the whole repo and do not rewrite unrelated files from a dirty worktree.\n\n```text\nYou are performing the final Windows-only text normalization phase for task-think.\n\nRead these files:\n- .ai/<PROJECT>/<LETTER>/plan.md\n- .ai/<PROJECT>/<LETTER>/logs/phase-4*.result.md\n- .ai/<PROJECT>/<LETTER>/logs/phase-5*.result.md\n- .ai/<PROJECT>/<LETTER>/logs/phase-6*.result.md\n\nYour job:\n- Collect the union of repo file paths listed under \"Touched files\" in those result logs.\n- Keep only files inside the repository that currently exist and are textual project files: source, headers, build/config files, localization files, style files, and similar text assets.\n- Exclude `.ai/`, `out/`, binary files, and unrelated user files that were not touched by Codex in this task.\n- Rewrite each kept file so all line endings are CRLF.\n- If a kept file is UTF-8 or ASCII text, write it back as UTF-8 without BOM. Never add a UTF-8 BOM to source/config/project text files.\n- Preserve file content otherwise. Preserve whether the file ended with a trailing newline.\n\nRules:\n- Run this phase in the main session on Windows.\n- Do not modify files outside the touched-file set for the current task.\n- Do not rewrite binary files.\n- When scripting this phase, do not use writer APIs or defaults that emit UTF-8 with BOM.\n- If a file cannot be normalized safely, record it as a failure instead of silently skipping it.\n\nWhen finished:\n1. Write `.ai/<PROJECT>/<LETTER>/logs/phase-7-line-endings.result.md`\n2. Include:\n   - whether the phase completed\n   - which files were normalized\n   - which files were skipped and why\n   - whether any UTF-8 BOMs were removed or verified absent\n   - any failures that need to be mentioned in the final summary\n```\n\n## Completion\n\nWhen all phases, including build verification, code review, and Windows line ending normalization when applicable, are done:\n1. Read the final `plan.md` and report the summary to the user.\n2. Show which files were modified or created.\n3. Note any issues encountered during implementation.\n4. Summarize the code review iterations: how many rounds, what was found and fixed, or whether it was approved on the first pass.\n5. On Windows, mention the text-normalization result briefly: which project files were normalized, whether any BOMs were removed, or whether nothing needed changes.\n6. Calculate and display the total elapsed time since `$START_TIME` (format as `Xh Ym Zs`, omitting zero components).\n7. Remind the user of the project name so they can request follow-up tasks within the same project.\n\n## Error Handling\n\n- If any phase fails or gets stuck, follow the timeout and retry rules above. Do not close an agent solely because the final artifact is missing while its progress file is still advancing. For Phase 1, Phase 2, Phase 3, Phase 4, and Phase 6, do not rerun locally after delegated retries fail; ask the user instead.\n- If `context.md` or `plan.md` is not written properly by a phase, rerun that phase in a fresh subagent with more specific instructions.\n- If build errors persist after the build phase's attempts, report the remaining errors to the user.\n- If a review-fix phase introduces new build errors that it cannot resolve, report to the user.\n\n## Prompt Delivery And Logs\n\nFor each phase:\n1. Write the full prompt to `.ai/<PROJECT>/<LETTER>/logs/phase-<name>.prompt.md`\n2. Delegate by sending that prompt text to a fresh subagent, or use it as a same-session checklist only for the designated main-session phases or when delegation was unavailable from the start\n3. For delegated phases, expect a matching `.ai/<PROJECT>/<LETTER>/logs/phase-<name>.progress.md` heartbeat while work is in flight\n4. Save a concise completion note to `.ai/<PROJECT>/<LETTER>/logs/phase-<name>.result.md`\n\nFor review iterations, include the iteration in the file name, for example:\n- `phase-6a-review-1.prompt.md`\n- `phase-6a-review-1.result.md`\n- `phase-6b-fix-1.prompt.md`\n- `phase-6b-fix-1.result.md`\n\n## Subagent Pattern\n\nUse this pattern conceptually for delegated phases:\n\n1. Write the phase prompt file.\n2. Spawn a fresh subagent with the phase prompt, usually with `fork_context: false`.\n3. Require the agent to create the matching progress file early and refresh it sparingly: at natural milestones when possible, otherwise only after a longer quiet stretch such as roughly 5-10 minutes.\n4. Wait in 5-minute intervals when the next step is blocked on that phase, checking both the final artifact and the progress file on timeout.\n5. When the phase looks close to finishing, switch to 1-2 minute waits.\n6. Prefer filesystem mtime checks on the progress file first. If its mtime moved or the heartbeat counter increased, keep waiting; do not treat that as a stall.\n7. If neither the artifact nor the progress file moves, send one short follow-up to the same agent, then retry once with a fresh subagent before involving the user.\n8. Validate the expected artifact or code changes with small shell summaries and the completion checks above.\n9. Write the result log from the validated outcome and the compact reply block.\n\nDo not replace this pattern with shell-launched `codex exec`.\n"
  },
  {
    "path": ".agents/skills/task-think/SKILL.md",
    "content": "---\nname: task-think\ndescription: Orchestrate a multi-phase implementation workflow for this repository with artifact files under .ai/<project-name>/<letter>/ using Codex subagents instead of shell-spawned child processes. Use when the user wants one prompt to drive context gathering, planning, plan assessment, implementation, build verification, and review with persistent artifacts, clear phase handoffs, and a thin parent thread. Prefer spawn_agent/send_input/wait_agent, keep heavy pre-build work delegated when possible, and avoid pulling timed-out phases back into the main session.\n---\n\n# Task Pipeline\n\nRun a full implementation workflow with repository artifacts and clear phase boundaries.\n\n## Inputs\n\nCollect:\n- task description\n- optional project name (if missing, derive a short kebab-case name)\n- optional constraints (files, architecture, risk tolerance)\n- optional screenshot paths\n\nIf screenshots are attached in UI but not present as files, write a brief textual summary into the task artifacts before spawning fresh subagents so later phases can read the requirements without inheriting the whole parent thread.\n\n## Overview\n\nThe workflow is organized around projects. Each project lives in `.ai/<project-name>/` and can contain multiple sequential tasks (labeled `a`, `b`, `c`, ... `z`).\n\nProject structure:\n```text\n.ai/<project-name>/\n  about.md              # Single source of truth for the entire project\n  a/                    # First task\n    context.md          # Gathered codebase context for this task\n    plan.md             # Implementation plan\n    review1.md          # Code review documents (up to 3 iterations)\n    review2.md\n    review3.md\n    logs/\n      phase-*.prompt.md\n      phase-*.progress.md\n      phase-*.result.md\n  b/                    # Follow-up task\n    context.md\n    plan.md\n    review1.md\n    logs/\n      ...\n  c/                    # Another follow-up task\n    ...\n```\n\n- `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.\n- 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 phases should rely on the current task folder.\n\n## Artifacts\n\nCreate and maintain:\n- `.ai/<project-name>/about.md`\n- `.ai/<project-name>/<letter>/context.md`\n- `.ai/<project-name>/<letter>/plan.md`\n- `.ai/<project-name>/<letter>/review<R>.md` (up to 3 review iterations)\n- `.ai/<project-name>/<letter>/logs/phase-<name>.prompt.md`\n- `.ai/<project-name>/<letter>/logs/phase-<name>.progress.md` for delegated phases\n- `.ai/<project-name>/<letter>/logs/phase-<name>.result.md`\n\nEach `phase-<name>.result.md` should capture a concise outcome summary: whether the phase completed, which files it touched, and any follow-up notes or blockers.\nEach delegated `phase-<name>.progress.md` should act as a heartbeat: a tiny monotonic counter plus current step, files being read or edited, concrete findings so far, and the next checkpoint. It is not a final artifact; it exists so the parent can distinguish active research from a truly stuck subagent without rereading large context.\n\n## Phases\n\nRun these phases sequentially:\n\n1. Phase 0: Setup - Record start time, detect follow-up vs new project, create directories.\n2. Phase 1: Context Gathering - Read codebase, write `about.md` and `context.md`. Use Phase 1F for follow-up tasks.\n3. Phase 2: Planning - Read context, write detailed `plan.md` with numbered steps grouped into phases.\n4. Phase 3: Plan Assessment - Review and refine the plan for correctness, completeness, code quality, and phase sizing.\n5. Phase 4: Implementation - Execute one implementation unit per plan phase.\n6. Phase 5: Build Verification - Build the project, fix any build errors. Skip if no source code was modified.\n7. Phase 6: Code Review Loop - Run review and fix iterations until approved or the iteration limit is reached.\n8. Phase 7: Windows Text Normalization - On Windows only, after review passes and before the final summary, normalize LF to CRLF for the text source/config files Codex edited in this task and ensure rewritten UTF-8 project files are saved without BOM.\n\nUse the phase prompt templates in `PROMPTS.md`.\n\n## Execution Mode\n\nUse Codex subagents as the primary orchestration mechanism.\n\n- When delegation is available, Phase 1, Phase 2, Phase 3, each Phase 4 implementation unit, and each Phase 6 review or review-fix pass must run in fresh subagents. Do not rerun those phases in the main session midstream just because a wait timed out or an artifact is missing.\n- Run Phase 7 in the main session on Windows because it depends on the final local file state and the exact touched-file set for the current task.\n- When any same-session helper rewrites Windows project text files, preserve CRLF and write UTF-8 without BOM. Avoid writer APIs or defaults that silently inject a UTF-8 BOM.\n- The main session may read `context.md` once after Phase 1 and `plan.md` once after Phase 3. After that, prefer narrow shell checks, file existence checks, and status-line reads instead of rereading full documents or diffs.\n- Prefer `worker` for phases that write files. Use `explorer` only for narrow read-only questions that unblock your next local step.\n- Keep `fork_context` off by default. Pass the phase prompt and explicit file paths instead of the whole thread unless the phase truly needs prior conversational context or thread-only attachments.\n- When the platform supports it, request `model: gpt-5.4` and `reasoning_effort: xhigh` for spawned phase agents. If overrides are unavailable, inherit the current session settings.\n- Write the exact phase prompt to the matching `logs/phase-<name>.prompt.md` file before you delegate. Use the same prompt file as a checklist if you later need to fall back to same-session execution.\n- For delegated phases, require an early `logs/phase-<name>.progress.md` heartbeat before deep work. The subagent should create or update it early, keep it tiny, and refresh it sparingly: preferably at natural milestones, and otherwise only after a longer quiet stretch such as roughly 5-10 minutes.\n- In every delegated prompt, require a compact final reply with only status, artifact paths, touched files, and blocker or `none`. Detailed reasoning belongs in `.ai/` artifacts, not in the chat reply.\n- After a subagent finishes, verify that the expected artifacts or code changes exist, then write a short result log in `logs/phase-<name>.result.md`.\n- For delegated phases, use `wait_agent` with a 5-minute timeout by default while a phase is still clearly in progress. Successful completion may wake earlier, so this does not add latency to finished phases.\n- When a phase looks close to completion — for example the final artifact has appeared, a build is in its final pass, or the agent said it is wrapping up — switch to 1-2 minute waits until it lands.\n- A timeout is not a failure; it only means no final status arrived yet. Do not treat short waits as stall detection for research-heavy phases.\n- On timeout, inspect the expected artifact, the phase progress file mtime, and the worktree for movement. Prefer mtime checks first; only reread the progress file when you need detail.\n- If the progress file mtime moved or its heartbeat counter increased since the previous check, treat that as active progress and wait again.\n- If no usable final artifact exists yet but the progress file is appearing or advancing, keep the same subagent alive. Progress-file movement does not count toward the retry limit.\n- If no usable final artifact exists yet and neither the expected artifact nor the progress file has moved since the previous blocked check, send one short follow-up asking the same subagent to refresh the progress file, finish the artifact, and return the compact status block, then wait again.\n- Only if the same subagent still shows no meaningful movement in either the expected artifact or the progress file after two full default waits and one follow-up should you close it and rerun that phase in a fresh subagent.\n- Use `wait_agent` only when the next step is blocked on the result. While the delegated phase runs, do small non-overlapping local tasks such as validating directory structure or preparing the next prompt file.\n- Build verification is critical-path work. Prefer running the build in the main session, and only delegate a bounded build-fix phase when there is a concrete reason.\n- If subagents are unavailable in the current environment, or current policy does not allow delegation from the start, run the phase in the main session using the same prompt files. Otherwise, do not switch a pre-build phase to same-session midstream. Never fall back to shell-spawned `codex exec` child processes from this skill.\n\n## Verification Rules\n\n- If build or test commands fail due to file locks or access-denied outputs (C1041, LNK1104), stop and ask the user to close locking processes before retrying.\n- Treat a delegated phase as complete only when the required artifact or status update exists on disk and matches the phase goals; do not rely on the chat reply alone.\n- Never claim completion without:\n  - implemented code changes present\n  - build attempt results recorded\n  - review pass documented with any follow-up fixes\n  - on Windows, if the task edited project source/config text files, a CRLF / no-BOM normalization pass recorded after review\n\n## Completion Criteria\n\nMark complete only when:\n- All plan phases are done\n- Build verification is recorded\n- Review issues are addressed or explicitly deferred with rationale\n- On Windows, Codex-edited project source/config text files have been normalized to CRLF, any UTF-8 rewrites were saved without BOM, and the result is logged\n- Display total elapsed time since start (format: `Xh Ym Zs`, omitting zero components)\n- Remind the user of the project name so they can request follow-up tasks within the same project\n\n## Error Handling\n\n- If any phase fails, times out, or gets stuck, follow the retry ladder from Execution Mode. Do not close an agent solely because the final artifact is missing while its progress file is still moving. After two delegated attempts remain blocked with no meaningful progress, report the issue to the user. Do not absorb the phase into the main session before build unless delegation was unavailable from the start.\n- If `context.md` or `plan.md` is not written properly by a phase, rerun that phase in a fresh subagent with more specific instructions. Do not repair it locally before build unless delegation was unavailable from the start.\n- If build errors persist after the build phase's attempts, report the remaining errors to the user.\n- If a review-fix phase introduces new build errors that it cannot resolve, report to the user.\n- If Phase 7 cannot safely normalize a touched file on Windows or remove an introduced UTF-8 BOM from a touched project text file, record the failure in the result log and report it in the final summary instead of silently skipping it.\n\n## User Invocation\n\nUse plain language with the skill name in the request, for example:\n\n`Use local task-think skill with subagents: make sure FileLoadTask::process does not create or read QPixmap on background threads; use QImage with ARGB32_Premultiplied instead.`\n\nFor follow-up tasks on an existing project:\n\n`Use local task-think skill with subagents: my-project also handle the case where the file is already cached`\n\nIf screenshots are relevant, include file paths in the same prompt when possible.\n"
  },
  {
    "path": ".claude/commands/icon.md",
    "content": "---\ndescription: Generate an SVG icon from a design mockup using vectosolve vectorization\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash, Agent, AskUserQuestion, TodoWrite, mcp__vectosolve__vectorize\n---\n\n# Icon - SVG Icon Generation from Design Mockup\n\nYou generate production-quality SVG icons for Telegram Desktop by vectorizing design mockup screenshots using the vectosolve MCP service, then post-processing the result to match the Telegram icon format.\n\n**Arguments:** `$ARGUMENTS` = \"$ARGUMENTS\"\n\nIf `$ARGUMENTS` is empty, ask the user to describe the icon they want and paste a cropped screenshot of it.\n\n## Overview\n\nThe workflow takes a cropped screenshot of an icon from a design mockup (grabbed from the clipboard), vectorizes it via the vectosolve MCP, then post-processes the SVG (recolor to white-on-transparent, restructure to minimal format, set 24x24 output size).\n\nWorking directory: `.ai/icon_{name}/` with iterations labeled by letter (`a/`, `b/`, ...), each containing `source.png`. Output SVGs are in the icon root: `a.svg`, `b.svg`, etc.\n\nFollow-ups are supported: `/icon {icon_name} <description>` continues from where the previous run left off.\n\n## Phase 0: Setup\n\n**Record the current time** (using `date` or equivalent) as `$START_TIME`.\n\n### Step 0a: Clipboard grab (MUST be the VERY FIRST action)\n\nIf there is an image attached to the user's message:\n\n1. Generate a random 8-character hex string for `HASH` (use `openssl rand -hex 4` or similar).\n2. **IMMEDIATELY** — before any other processing — run this Bash command to save the clipboard image:\n   ```bash\n   HASH=$(openssl rand -hex 4) && if [[ \"$OSTYPE\" == darwin* ]]; then bash .claude/grab_clipboard.sh \".ai/icon_${HASH}.png\"; else powershell -ExecutionPolicy Bypass -File .claude/grab_clipboard.ps1 \".ai/icon_${HASH}.png\"; fi\n   ```\n   On macOS `.claude/grab_clipboard.sh` is used; on Windows `.claude/grab_clipboard.ps1`. Both grab the current clipboard image and save it to the specified path.\n\n3. If the command fails (exit 1 / no image on clipboard):\n   - Tell the user: **\"Clipboard doesn't contain an image. Please copy the icon area first, then retry.\"** (On macOS: Cmd+Ctrl+Shift+4 to snip to clipboard; on Windows: Win+Shift+S.)\n   - **STOP IMMEDIATELY. Do NOT continue.** You cannot use the image pasted in the conversation — it exists only as pixels in the chat, not as a file you can send to vectosolve. The clipboard grab is the ONLY way to get the image to disk. Do not attempt any workaround.\n\n4. Read back the saved `.ai/icon_HASH.png` using the Read tool.\n5. Compare it visually with the image pasted in the conversation. They should depict the same thing.\n   - If they look **completely different**: delete `.ai/icon_HASH.png` and fail:\n     > \"The clipboard image doesn't match what you pasted. Please re-copy and retry.\"\n   - If they look the same (or close enough): proceed. Store the temp path.\n\nIf NO image is attached to the message, skip this step entirely.\n\n### Step 0b: Fail-fast — verify vectosolve MCP\n\nCheck that the `mcp__vectosolve__vectorize` tool is available by looking at your available tools list. If it is NOT available, fail immediately with:\n\n> vectosolve MCP is not configured. Set it up with:\n> ```\n> claude mcp add vectosolve --scope user -e VECTOSOLVE_API_KEY=vs_xxx -- npx @vectosolve/mcp\n> ```\n> Then restart Claude Code.\n\n### Step 0c: Follow-up detection\n\nExtract the first word/token from `$ARGUMENTS` (everything before the first space or newline). Call it `FIRST_TOKEN`.\n\nRun these TWO commands using the Bash tool, **IN PARALLEL**:\n1. `ls .ai/` — to see all existing icon project names\n2. `ls .ai/icon_{FIRST_TOKEN}/context.md` — to check if this specific icon project exists\n\n**Evaluate the results:**\n- If command 2 **succeeds** (context.md exists): this is a **follow-up**. The icon name is `FIRST_TOKEN`. The follow-up description is everything in `$ARGUMENTS` after `FIRST_TOKEN`.\n- If command 2 **fails** (not found): this is a **new icon**. The full `$ARGUMENTS` is the icon description.\n\n### Step 0d: New icon setup\n\n1. Parse `$ARGUMENTS` to determine:\n   - **Icon description**: what the icon should depict\n   - **Icon type**: default is `menu` (24x24 menu/button icon). User may specify otherwise.\n   - **Target subfolder**: `menu/` by default, or another subfolder if specified.\n\n2. Choose an icon file name:\n   - Lowercase letters and underscores only — **NO hyphens**\n   - Match existing naming conventions (check `Telegram/Resources/icons/{subfolder}/`)\n   - Must NOT conflict with existing icons\n   - Must NOT collide with existing `.ai/icon_{name}/` directories\n\n3. Create `.ai/icon_{name}/` and `.ai/icon_{name}/a/`.\n\n4. Write `.ai/icon_{name}/context.md` with:\n   ```\n   ## Icon: {icon_name}\n   Type: {menu/other}\n   Target: Telegram/Resources/icons/{subfolder}/{icon_name}.svg\n\n   ## Original Request\n   {full $ARGUMENTS text}\n\n   ## Follow-ups\n   (none yet)\n   ```\n\n5. Set `LETTER` to `a`.\n\n### Step 0e: Follow-up setup\n\n1. Read `.ai/icon_{name}/context.md` to get the icon type, subfolder, and full history.\n2. Find the latest existing letter folder in `.ai/icon_{name}/` (highest letter).\n3. Set `LETTER` to the next letter after the latest.\n4. Create `.ai/icon_{name}/{LETTER}/`.\n5. Update `.ai/icon_{name}/context.md` — append the follow-up description to the `## Follow-ups` section:\n   ```\n   ### Follow-up (starting at letter {LETTER})\n   {follow-up description}\n   ```\n\n### Step 0f: Place source image\n\nIf a clipboard image was grabbed in Step 0a:\n1. Copy (or move) `.ai/icon_HASH.png` → `.ai/icon_{name}/source.png` (overwrite if exists — this is always the latest source).\n2. Copy it to `.ai/icon_{name}/{LETTER}/source.png` (archive per-iteration source).\n3. Delete the temp `.ai/icon_HASH.png` if it was copied (not moved).\n\nIf NO image was grabbed:\n- **New icon with no image**: Ask the user to provide a screenshot. STOP.\n- **Follow-up with no image**: The existing `source.png` in the icon root carries forward. Copy it to `.ai/icon_{name}/{LETTER}/source.png`. If no source.png exists at all, ask the user for an image.\n\n### Step 0g: Verify renderer\n\nLocate the render tool (`codegen_style` with `--render-svg` mode):\n\n```bash\nif [[ \"$OSTYPE\" == darwin* ]]; then\n    ls out/Debug/codegen_style\nelse\n    ls out/Telegram/codegen/codegen/style/Debug/codegen_style.exe\nfi\n```\n\nIf missing, build it: `cmake --build out --config Debug --target codegen_style`\n\nTest on a known good SVG (use the appropriate binary path for the OS):\n```bash\nCODEGEN=$(if [[ \"$OSTYPE\" == darwin* ]]; then echo out/Debug/codegen_style; else echo out/Telegram/codegen/codegen/style/Debug/codegen_style.exe; fi)\n$CODEGEN --render-svg Telegram/Resources/icons/menu/tag_add.svg .ai/icon_{name}/test_render.png 512\n```\n\nIf works → delete test render, set `RENDER_AVAILABLE = true`. If fails → `RENDER_AVAILABLE = false`.\n\n## Phase 1: Vectorize & Post-process\n\n### Step 1a: Call vectosolve\n\nUse the `mcp__vectosolve__vectorize` tool with `file_path` set to the **absolute path** of `.ai/icon_{name}/{LETTER}/source.png`.\n\n**If this fails, STOP IMMEDIATELY.** Do NOT try to generate the SVG manually or by any other means. Report the error to the user and let them fix the issue (bad API key, no credits, network error, etc.).\n\nSave the returned SVG content to `.ai/icon_{name}/{LETTER}/raw_vectosolve.svg`.\n\nThe MCP tool calls the vectosolve API ($0.20/call). The API key is stored in `~/.claude.json` MCP config (never in the repository).\n\n### Step 1b: Post-process the SVG\n\nThe vectosolve SVG will have colors from the mockup, arbitrary dimensions, and possibly a non-square aspect ratio from a non-square screenshot crop. Post-processing fixes this by adjusting the **viewBox** — leave path coordinates untouched.\n\n**Do NOT transform path coordinates.** Vectosolve's paths are correct — the only thing wrong is the framing. All geometry adjustments are done by manipulating the `viewBox` and the `width`/`height` attributes.\n\n#### Sub-step 1: Read the request and determine parameters\n\nBefore touching the SVG, determine these from the user's request and context.md:\n\n1. **Output size** (`OUT_W × OUT_H`): default is `24px × 24px` for menu icons. The user may request different dimensions (e.g., 36×36, 48×48, or non-square). Always check the request.\n2. **Content padding**: default is ~2px equivalent on each side at the output scale (so content fills roughly (OUT_W-4) × (OUT_H-4)). The user may request different padding or edge-to-edge.\n3. **Centering**: default is centered both horizontally and vertically. The user may request specific alignment (e.g., \"align to bottom\").\n\n#### Sub-step 2: Parse the raw SVG\n\n1. Extract the `viewBox`: `viewBox=\"VB_X VB_Y VB_W VB_H\"` (typically `0 0 W H`).\n2. Identify ALL paths. Classify each:\n   - **Background**: a rect or path spanning the full viewBox (first path that's a simple rectangle matching the viewBox bounds). **Remove it entirely.**\n   - **Content**: the actual icon shapes. **Keep these, paths unchanged.**\n3. If paths have `transform=\"translate(TX,TY)\"` attributes, that's fine — keep them as-is. The viewBox framing will work regardless.\n\n#### Sub-step 3: Compute the content bounding box\n\nEstimate the bounding box of the content paths (after removing the background). You can either:\n- Eyeball it from the path coordinates (look at first/last M commands and extremes of curves)\n- Or for precision, write a quick script to parse the paths and find min/max X/Y\n\nCall the result: `CX_MIN, CY_MIN, CX_MAX, CY_MAX`. Content dimensions: `CW = CX_MAX - CX_MIN`, `CH = CY_MAX - CY_MIN`.\n\n#### Sub-step 4: Compute the new viewBox\n\nThe viewBox determines what part of the SVG coordinate space maps to the output rectangle. By expanding the viewBox beyond the content bounds, we add padding. By making the viewBox aspect ratio match the output aspect ratio, we prevent stretching.\n\n1. **Output aspect ratio**: `OUT_AR = OUT_W / OUT_H` (for 24×24 this is 1.0).\n2. **Padding in SVG coordinates**: we want ~2px padding at output scale. The scale factor is `OUT_W / VB_CONTENT_W` approximately, so padding in SVG coords = `2 * (CW / (OUT_W - 4))` (or similar — the exact formula depends on which dimension is dominant). Simpler approach: aim for content to occupy ~83% of the viewBox (≈ 20/24), so:\n   - `PADDED_W = CW / 0.83`\n   - `PADDED_H = CH / 0.83`\n3. **Match output aspect ratio**: the viewBox aspect ratio must equal `OUT_AR` to avoid stretching.\n   - If `PADDED_W / PADDED_H > OUT_AR`: width is dominant → `VB_W = PADDED_W`, `VB_H = VB_W / OUT_AR`\n   - If `PADDED_W / PADDED_H < OUT_AR`: height is dominant → `VB_H = PADDED_H`, `VB_W = VB_H * OUT_AR`\n   - If equal: `VB_W = PADDED_W`, `VB_H = PADDED_H`\n4. **Center the content** in the new viewBox:\n   - `VB_X = CX_MIN - (VB_W - CW) / 2`\n   - `VB_Y = CY_MIN - (VB_H - CH) / 2`\n   - (Adjust if the user requested non-centered alignment)\n\nThe new viewBox is: `viewBox=\"VB_X VB_Y VB_W VB_H\"`.\n\n#### Sub-step 5: Recolor to white-on-transparent\n\n- Replace ALL `fill` color values (anything that isn't `none`) with `#FFFFFF`.\n- Remove ALL `stroke` and `stroke-width` attributes entirely.\n- Remove `opacity` attributes if present.\n\n#### Sub-step 6: Determine path composition\n\nLook at the icon's visual structure and decide how paths should combine:\n- **Outlined shape** (e.g., circle outline with something inside): combine outer + inner cutout into one `<path>` with `fill-rule=\"evenodd\"`.\n- **Separate distinct parts** (e.g., magnifying glass + checkmark): keep as separate `<path>` elements.\n- **Filled shape with cutout** (e.g., filled circle with checkmark punched out): combine into one path with `fill-rule=\"evenodd\"`.\n\n#### Sub-step 7: Assemble final SVG\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg width=\"{OUT_W}px\" height=\"{OUT_H}px\" viewBox=\"{VB_X} {VB_Y} {VB_W} {VB_H}\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g stroke=\"none\" fill=\"none\" fill-rule=\"evenodd\">\n        <path d=\"...\" fill=\"#FFFFFF\"></path>\n    </g>\n</svg>\n```\n\n- `width`/`height` = the output size from the request (default `24px`/`24px`).\n- `viewBox` = the computed viewBox from Sub-step 4. The SVG renderer maps this coordinate region to the output size.\n- Path `d` attributes are **unchanged** from vectosolve output (just background removed, colors replaced).\n- No `<title>`, `id`, `xmlns:xlink`, `version`, `class`, `style`, XML comments, `<metadata>`, or `preserveAspectRatio`.\n- No `<circle>`, `<rect>`, `<line>` — only `<path>`.\n\nWrite the final SVG to `.ai/icon_{name}/{LETTER}.svg`.\n\n### Step 1c: Render\n\nIf `RENDER_AVAILABLE`:\n```bash\n$CODEGEN --render-svg \".ai/icon_{name}/{LETTER}.svg\" \".ai/icon_{name}/render_{LETTER}.png\" 512\n```\n\nRead the render to visually verify the result.\n\n## Phase 2: Review\n\nAfter rendering, assess the result:\n\n1. **Recognizable?** The icon should be clearly identifiable as the intended symbol.\n2. **Scale reasonable?** Should fill the space appropriately with ~2-3px padding.\n3. **Clean lines?** No broken paths, artifacts, or unwanted elements.\n4. **Correct colors?** All white on transparent (no leftover colors from the mockup).\n\nIf the result looks good → proceed to Phase 3 (Output).\n\nIf there are fixable issues (stray element, missed color, etc.) → fix the SVG directly, re-render, and re-check.\n\nIf the result is poor (vectosolve couldn't handle the input well) → report to the user and suggest:\n- Trying a cleaner/larger crop of the icon\n- Providing a different screenshot\n- Following up: `/icon {icon_name} <description of what to change>`\n\n## Phase 3: Output\n\n1. Read the `Target:` line from `.ai/icon_{name}/context.md` to get the output path.\n\n2. Copy the final SVG to that target path (e.g., `Telegram/Resources/icons/menu/{icon_name}.svg`).\n\n3. Update `.ai/icon_{name}/context.md` — append to the end:\n   ```\n   ## Latest Output\n   Letter: {LETTER}\n   Written to: {target_path}\n   ```\n\n4. Report to the user:\n   - Final icon file path\n   - Number of vectosolve calls made (cost at $0.20/call)\n   - Suggest verifying visually\n   - Working directory `.ai/icon_{name}/` has all iterations\n   - Elapsed time since `$START_TIME` (format `Xm Ys`)\n   - Follow-up: `/icon {icon_name} <description of what to change>`\n\n## Text-only Follow-ups (no new image)\n\nWhen a follow-up has no attached image, the user wants to refine the existing SVG based on text feedback. In this case:\n\n1. Skip Phase 1 (no vectosolve call needed).\n2. Read the latest SVG (`.ai/icon_{name}/{prev_letter}.svg`).\n3. Read the latest render if available.\n4. Apply the user's requested changes by editing the SVG directly.\n5. Save as `.ai/icon_{name}/{LETTER}.svg`.\n6. Render, review, and output as normal (Phases 1c → 3).\n\nIf the changes are too complex for manual SVG editing, suggest the user provide a new screenshot instead.\n\n## Error Handling\n\n- If clipboard grab fails → tell user to re-copy and retry.\n- If vectosolve returns an error → report it and suggest a different/cleaner screenshot.\n- If vectosolve returns SVG that can't be parsed → save raw output for debugging, report to user.\n- If the render helper fails → set `RENDER_AVAILABLE = false`, continue with SVG-only review.\n- If post-processing produces a broken SVG → fall back to the raw vectosolve output and do lighter cleanup.\n"
  },
  {
    "path": ".claude/commands/planner.md",
    "content": "---\ndescription: Plan and create a repetitive task automation (prompt.md + tasks.json pair)\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash(mkdir:*), Bash(ls:*), AskUserQuestion\n---\n\n# Task Planner - Create Automated Task Workflows\n\nYou are setting up a new **repetitive task automation** for Claude Code. The goal is to create a folder in `.ai/<featurename>/` containing:\n- `prompt.md` - Detailed instructions for the autonomous agent\n- `tasks.json` - List of tasks with completion tracking\n\nThis pair can then be executed via `.claude/iterate.ps1 <featurename>`.\n\n## Your Workflow\n\n### 1. Understand the Goal\n\nFirst, understand what the user wants to automate. Ask clarifying questions using AskUserQuestion if needed:\n- What is the overall goal/feature being implemented?\n- What are the individual tasks involved?\n- Are there dependencies between tasks?\n- What files/areas of the codebase are involved?\n- Are there any reference examples or patterns to follow?\n\n### 2. Choose a Feature Name\n\nThe `<featurename>` should be:\n- Short (1-2 words, lowercase, hyphen-separated)\n- Easy to type on command line\n- Descriptive of the work being done\n- Not already used in `.ai/`\n\nCheck existing folders:\n```bash\nls .ai/\n```\n\nSuggest a name to the user or let them specify one directly via $ARGUMENTS.\n\n\n### 3. Create the Folder and Files\n\nCreate `.ai/<featurename>/`:\n\n**prompt.md** should include:\n- Overview of what we're doing\n- Architecture/context needed\n- Step-by-step instructions for each task type\n- Code patterns and examples\n- Build/test commands\n- Commit message format (see below)\n\n### Commit Message Guidelines\n\nAll prompts should specify commit message length requirements:\n- **Soft limit**: ~50 characters (ideal length for first line)\n- **Hard limit**: 76 characters (must not exceed)\n\nExample instruction for prompt.md:\n```\n## Commit Format\n\nFirst line: Short summary ending with a dot (aim for ~50 chars, max 76 chars)\n\n<Optional body with details, also ending with a dot.>\n\nIMPORTANT: Never try to commit files in .ai/\n```\n\n**tasks.json** format:\n```json\n{\n  \"tasks\": [\n    {\n      \"id\": \"task-id\",\n      \"title\": \"Short task title\",\n      \"description\": \"Detailed description of what to do\",\n      \"started\": false,\n      \"completed\": false,\n      \"dependencies\": [\"other-task-id\"]\n    }\n  ]\n}\n```\n\n### 4. Iterate with the User\n\nAfter creating initial files, the user may want to:\n- Add more tasks to tasks.json\n- Refine the prompt with more details\n- Add examples or patterns\n- Clarify instructions\n\nKeep refining until the user is satisfied.\n\n## Arguments\n\nIf `$ARGUMENTS` is provided, it's the feature name to use:\n- `$ARGUMENTS` = \"$ARGUMENTS\"\n\nIf empty, you'll need to determine/suggest a name based on the discussion.\n\n## Examples\n\n### Example 1: Settings Migration\n```\n/taskplanner settings-upgrade\n```\nCreates `.ai/settings-upgrade/` with prompt and tasks for migrating settings sections.\n\n### Example 2: Open-ended\n```\n/taskplanner\n```\nStarts a conversation to understand what needs to be automated, then creates the appropriate folder.\n\n## Starting Point\n\nLet's begin! Please describe:\n1. What repetitive coding task do you want to automate?\n2. What is the end goal?\n3. Do you have initial tasks in mind, or should we discover them together?\n"
  },
  {
    "path": ".claude/commands/reflect.md",
    "content": "---\ndescription: Learn from corrections — examine staged vs unstaged diffs and optionally distill insights into AGENTS.md or REVIEW.md\nallowed-tools: Read, Edit, Bash(git diff:*), Bash(git status:*), Bash(git log:*), Bash(ls:*), AskUserQuestion\n---\n\n# Reflect — Learn from Corrections\n\nYou are a reflection agent. Your job is to examine the difference between what an AI agent produced (staged changes) and what the user corrected (unstaged changes), and determine whether any **general, reusable insight** can be extracted and added to the project's coding guidelines.\n\n**CRITICAL: Use extended thinking ultrathink for your analysis. This requires deep, careful reasoning.**\n\n## Arguments\n\n`$ARGUMENTS` = \"$ARGUMENTS\"\n\nIf `$ARGUMENTS` is provided, it is a task name (project name from the `/task` workflow). This means the agent was working within `.ai/<task-name>/` and you should read the task context for deeper understanding of what the agent was trying to do.\n\nIf `$ARGUMENTS` is empty, skip the task context step — just work from the diffs alone.\n\n## Context\n\nThe workflow is:\n1. An AI agent implemented something and its changes were staged (`git add`).\n2. The user reviewed and corrected the agent's work. These corrections are unstaged.\n3. You are now invoked to reflect on what went wrong and whether it reveals a pattern.\n\n## Step 1: Gather the Diffs and Task Context\n\nRun these commands in parallel:\n\n```bash\ngit diff --cached    # What the agent wrote (staged)\ngit diff             # What the user corrected (unstaged, on top of staged)\ngit status           # Which files are involved\n```\n\nIf either diff is empty, tell the user and stop. Both diffs must be non-empty for reflection to be meaningful.\n\n### Task context (only if `$ARGUMENTS` is non-empty)\n\nThe task name is `$ARGUMENTS`. Read the task's project context:\n\n1. Read `.ai/$ARGUMENTS/about.md` — the project-level description of what this feature does.\n2. Find the latest task iteration folder: list `.ai/$ARGUMENTS/` and pick the folder with the highest letter (`a`, `b`, `c`, ...).\n3. Read `.ai/$ARGUMENTS/<latest-letter>/context.md` — the detailed implementation context the agent was working from.\n\nThis helps you distinguish between:\n- **Task-specific mistakes** — the agent misunderstood this particular feature's requirements or made a wrong choice within the specific problem. These are NOT documentation-worthy.\n- **General convention mistakes** — the agent did something that violates a pattern the codebase follows broadly, regardless of which feature is being implemented. These ARE potentially documentation-worthy.\n\nHaving the task context makes this distinction much sharper. Without it, you might mistake a task-specific correction for a general pattern or vice versa.\n\n## Step 2: Read the Current Guidelines\n\nRead both files:\n- `AGENTS.md` — development guidelines: build system, coding style, API usage patterns, UI styling, localization, rpl, architectural conventions, \"how to do things\"\n- `REVIEW.md` — mechanical style and formatting rules: brace placement, operator position, type checks, variable initialization, call formatting\n\nRead them carefully. You need to know exactly what's already documented to avoid duplicates and detect contradictions.\n\n## Step 3: Analyze the Corrections\n\nNow think deeply. For each correction the user made, ask yourself:\n\n1. **What did the agent do wrong?** Understand the specific mistake.\n2. **Why was it wrong?** Identify the underlying principle.\n3. **Is this already covered by AGENTS.md or REVIEW.md?** Check carefully:\n   - If the existing rule's scope, title, and examples **clearly cover** this exact scenario and the agent just ignored it — that's not a documentation problem. Skip it.\n   - If the existing rule **technically applies** but its scope is too narrow, its examples don't illustrate this usage pattern, or its wording would reasonably lead an agent to think it doesn't apply here — **the rule needs improvement**. Treat this as a potential insight (broaden the scope, add examples, adjust wording). A rule that agents repeatedly violate is an ineffective rule.\n4. **Is this specific to this particular task, or is it general?** Most corrections are task-specific (\"wrong variable here\", \"this should call that function instead\"). These are NOT documentation-worthy. Only patterns that would apply across many different tasks are worth capturing.\n5. **Would documenting this actually help a future agent?** Some things are too context-dependent or too obvious to be useful as a written rule. Be honest about this.\n\n## Step 4: Decision\n\nAfter analysis, you MUST reach one of these conclusions:\n\n### Conclusion A: No actionable insight\n\nThe corrections are purely task-specific, or the existing documentation clearly and specifically covers the exact scenario and the agent simply ignored it. Say what the corrections were and why no doc changes are needed. Then **stop**.\n\n### Conclusion B: New insight found\n\nYou can articulate a **concise, general rule** that:\n- Applies broadly (not just to this one task)\n- Is not already documented\n- Would genuinely help a future agent avoid the same class of mistake\n- Can be expressed in a few sentences with a clear code example\n\nIf you have a new insight, proceed to Step 5.\n\n### Conclusion C: Existing rule needs improvement\n\nA rule already exists in AGENTS.md or REVIEW.md, but its **scope is too narrow**, its **examples don't cover** the pattern the agent encountered, or its **wording** would reasonably lead an agent to think the rule doesn't apply. The agent's mistake is evidence the rule isn't effective.\n\nThis is NOT the same as Conclusion A. The test: would a careful agent, reading the existing rule, clearly know it applies to this specific situation? If no — the rule needs to be broadened, its examples expanded, or its title/scope adjusted. Proceed to Step 5.\n\n**Common signs of an ineffective rule:**\n- The rule's title or scope restricts it to a context narrower than the actual principle (e.g., \"in localization calls\" when the pattern applies generally)\n- The examples only show one usage pattern, and the agent encountered a different one\n- The wording describes *what* to use but not *when* — so agents only apply it in situations that look like the examples\n\n## Step 5: Categorize and Check for Contradictions\n\n### Where does it belong?\n\n- **REVIEW.md** — if it's a mechanical/style rule: formatting, naming, syntax preferences, call structure, brace/operator placement, type usage patterns. Rules that can be checked by looking at code locally without understanding the broader feature.\n- **AGENTS.md** — if it's an architectural/behavioral guideline: how to use APIs, where to place code, design patterns, build conventions, module organization, reactive patterns (rpl), localization usage, style system usage. Rules that require understanding the broader context.\n\n### Does it contradict existing content?\n\nRead the target file again carefully. Check if:\n1. The new insight **contradicts** an existing rule — if so, do NOT just append or just remove. Instead, use AskUserQuestion to present both the existing rule and the new insight to the user, explain the contradiction, and ask how to reconcile them.\n2. The new insight **overlaps** with an existing rule — if so, consider whether the existing rule should be extended/refined rather than adding a separate entry.\n3. The new insight is **complementary** — it adds something new without conflicting. This is the simplest case.\n\n## Step 6: Propose the Change\n\n**Do NOT silently edit the files.** First, present your proposed change to the user:\n\n- Quote the exact text you want to add or modify\n- Explain which file and where in the file\n- Explain why this is general enough to document\n- If modifying existing text, show the before and after\n\nUse AskUserQuestion to get the user's approval before making any edit.\n\nOnly after the user approves, apply the edit using the Edit tool.\n\n## Rules\n\n- **Keep docs lean and high-signal.** Don't add vague or overly specific rules. But don't default to inaction either — if the user had to manually fix something that a better-worded rule would have prevented, improving that rule is high-signal work.\n- **Never dump corrections verbatim.** The goal is distilled principles, not a changelog of mistakes.\n- **One insight per reflection, maximum.** If you think you see multiple insights, pick the strongest one. You can always run `/reflect` again next time.\n- **Keep the same style.** Match the formatting, tone, and level of detail of the target file. REVIEW.md uses specific before/after code examples. AGENTS.md uses explanatory sections with code snippets.\n- **Don't add \"don't do X\" rules.** Frame rules positively: \"do Y\" is better than \"don't do X.\" Show the right way, not just the wrong way.\n- **No meta-commentary.** Don't add notes like \"Added after reflection on...\" — the rule should read as if it was always there.\n"
  },
  {
    "path": ".claude/commands/release.md",
    "content": "---\ndescription: Prepare changelog, set version, and commit a new release\nallowed-tools: Read, Bash, Edit, Grep, AskUserQuestion\n---\n\n# Release — Changelog, Set Version, Commit\n\nFull release flow: generate changelog entry, run `set_version`, and commit.\n\n**Arguments:** `$ARGUMENTS` = \"$ARGUMENTS\"\n\nParse `$ARGUMENTS` for two optional parts (in any order):\n- A **version number** like `6.7` or `6.7.0` — if provided, use it as the new version exactly.\n- The word **\"beta\"** — if present, mark the release as beta.\n\nIf no version number is given, auto-increment the patch component (see step 2).\n\n## Steps\n\n### 1. Check git status is clean\n\nRun `git status --porcelain`. If there are any uncommitted changes, **stop** and ask the user to commit or discard them before proceeding. Do not continue until status is clean.\n\n### 2. Read the current changelog\n\nRead `changelog.txt` from the repository root. Note the **latest version number** on the first line (e.g. `6.6.3 beta (12.03.26)`). Parse its major.minor.patch components.\n\n### 3. Determine the new version number\n\n- **If a version was provided in arguments**, use it directly (append `.0` if only major.minor was given).\n- **If no version was provided**, auto-increment from the latest changelog version:\n  - If it was a beta, and the new release is **not** beta, reuse the same version number but drop \"beta\".\n  - If the new release is beta and the latest was also beta with the same major.minor, bump patch.\n  - Otherwise bump the patch component by 1.\n- Present the chosen version to the user and ask for confirmation before proceeding. If the user suggests a different version, use that instead.\n\n### 4. Fetch tags and determine the last release tag\n\nRun `git fetch origin --tags` first to ensure all tags from the public repository are available locally. Then run `git tag --sort=-v:refname` and find the most recent `v*` tag. This is the baseline for the diff.\n\n### 5. Collect commits\n\nRun `git log <last-tag>..HEAD --oneline` to get all commits since the last release.\n\n### 6. Write the changelog entry\n\nAnalyze every commit message. Group them mentally into features, improvements, and bug fixes. Then produce **brief, user-facing bullet points** following these rules:\n\n- **Style:** Match the existing changelog tone exactly — short, imperative sentences starting with a verb (Fix, Add, Allow, Show, Improve, Support…). Keep the trailing periods (the existing changelog uses them).\n- **Brevity:** Each bullet should be one short sentence, around 80 characters when possible. No implementation details. No commit hashes.\n- **Selection:** Only include changes that matter to end users. Skip CI, build infra, submodule bumps, code style, refactors, and intermediate WIP commits. Collapse many related commits (e.g. a dozen image-editor commits) into one or two bullets.\n- **Ordering:** Features first, then improvements, then bug fixes.\n- **Quantity:** Aim for 4-12 bullets total depending on the amount of changes.\n\n### 7. Format and insert into changelog.txt\n\nUse this exact format (date is today in DD.MM.YY):\n\n```\n<version> [beta ](DD.MM.YY)\n\n- Bullet one.\n- Bullet two.\n```\n\nPrepend the new entry at the very top of `changelog.txt`, separated by a blank line from the previous first entry. Use the Edit tool.\n\n### 8. Wait for approval\n\nAfter writing the entry to `changelog.txt` (step 7), tell the user the changelog has been updated and ask them to review it in the IDE. They can edit it directly and tell you to continue, or tell you what to change in chat. Do **not** print the full entry in chat — the file itself is the review surface.\n\n**Do NOT proceed until the user explicitly approves.**\n\n### 9. Run set_version\n\nOnce approved, run the `set_version` script from the repository root. On Windows:\n\n```\n.\\Telegram\\build\\set_version.bat <version_arg>\n```\n\nWhere `<version_arg>` is formatted as the `set_version` script expects:\n- Stable: `6.7.0` or `6.7`\n- Beta: `6.7.0.beta`\n\nVerify the script exits successfully (exit code 0). If it fails, show the error and stop.\n\n### 10. Commit\n\nStage all changes and create a commit. The commit message format:\n\n**First line:**\n- For stable: `Version <major>.<minor>.` if patch is 0, otherwise `Version <major>.<minor>.<patch>.`\n- For beta: `Beta version <major>.<minor>.<patch>.`\n\n**Then an empty line, then the changelog bullets.** Copy bullet lines from the changelog as-is. Only wrap lines that exceed 72 characters; shorter lines must stay on a single line. When wrapping is needed, break at logically correct places (between words/phrases) and indent continuation lines with two spaces.\n\nExample commit message:\n```\nBeta version 6.6.3.\n\n- Drawing tools in image editor\n  (brush, marker, eraser, arrow).\n- Draw-to-reply button in media viewer.\n- Trim recorded voice messages.\n- Fix reorder freeze in chats list.\n```\n\nUse a HEREDOC to pass the message to `git commit -a`.\n\n### 11. Done\n\nRun `git log -1` to show the resulting commit and confirm success.\n"
  },
  {
    "path": ".claude/commands/task.md",
    "content": "---\ndescription: Implement a feature or fix using multi-agent workflow with fresh context at each phase\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash, Task, AskUserQuestion, TodoWrite\n---\n\n# Task - Multi-Agent Implementation Workflow\n\nYou orchestrate a multi-phase implementation workflow that uses fresh agent spawns to work within context window limits on a large codebase.\n\n**Arguments:** `$ARGUMENTS` = \"$ARGUMENTS\"\n\nIf `$ARGUMENTS` is provided, it's the task description. If empty, ask the user what they want implemented.\n\n## Overview\n\nThe workflow is organized around **projects**. Each project lives in `.ai/<project-name>/` and can contain multiple sequential **tasks** (labeled `a`, `b`, `c`, ... `z`).\n\nProject structure:\n```\n.ai/<project-name>/\n  about.md              # Single source of truth for the entire project\n  a/                    # First task\n    context.md          # Gathered codebase context for this task\n    plan.md             # Implementation plan\n    review1.md          # Code review documents (up to 3)\n    review2.md\n    review3.md\n  b/                    # Follow-up task\n    context.md\n    plan.md\n    review1.md\n  c/                    # Another follow-up task\n    ...\n```\n\n- `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.\n- 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.\n\n## Phase 0: Setup\n\n**Record the current time now** (using `Get-Date` in PowerShell or equivalent) and store it as `$START_TIME`. You will use this at the end to display total elapsed time.\n\n⚠️ **CRITICAL: Follow-up detection MUST happen FIRST, before anything else.**\n\n### Step 0a: Follow-up detection (MANDATORY — do this BEFORE understanding the task)\n\nExtract the first word/token from `$ARGUMENTS` (everything before the first space or newline). Call it `FIRST_TOKEN`.\n\nThen run these TWO commands using the Bash tool, IN PARALLEL, right now:\n1. `ls .ai/` — to see all existing project names\n2. `ls .ai/<FIRST_TOKEN>/about.md` — to check if this specific project exists\n\n**Evaluate the results:**\n- 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).\n- If command 2 **fails** (file not found): this is a **new project**. The full `$ARGUMENTS` is the task description.\n\n**Do NOT proceed to step 0b until you have run these commands and determined follow-up vs new.**\n\n### Step 0b: Project setup\n\n**For new projects:**\n- Using the list from command 1, pick a unique short name (1-2 lowercase words, hyphen-separated) that doesn't collide with existing projects.\n- Create `.ai/<project-name>/` and `.ai/<project-name>/a/`.\n- Set current task letter = `a`.\n\n**For follow-up tasks:**\n- Scan `.ai/<project-name>/` for existing task folders (`a/`, `b/`, ...). Find the latest one (highest letter).\n- The previous task letter = that highest letter.\n- The new task letter = next letter in sequence.\n- Create `.ai/<project-name>/<new-letter>/`.\n\nThen 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.\n\n## Phase 1: Context Gathering\n\n### For New Projects (task letter = `a`)\n\nSpawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt structure:\n\n```\nYou are a context-gathering agent for a large C++ codebase (Telegram Desktop).\n\nTASK: <paste the user's task description here>\n\nYOUR JOB: Read AGENTS.md, inspect the codebase, find ALL files and code relevant to this task, and write two documents.\n\nSteps:\n1. Read AGENTS.md for project conventions and build instructions.\n2. Search the codebase for files, classes, functions, and patterns related to the task.\n3. Read all potentially relevant files. Be thorough - read more rather than less.\n4. For each relevant file, note:\n   - File path\n   - Relevant line ranges\n   - What the code does and how it relates to the task\n   - Key data structures, function signatures, patterns used\n5. Look for similar existing features that could serve as a reference implementation.\n6. Check api.tl if the task involves Telegram API.\n7. Check .style files if the task involves UI.\n8. Check lang.strings if the task involves user-visible text.\n\nWrite TWO files:\n\n### File 1: .ai/<project-name>/about.md\n\nNOTE: 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.\n\nWrite it as if the project is already fully implemented and working. It should contain:\n- **Project**: What this project does (feature description, goals, scope)\n- **Architecture**: High-level architectural decisions, which modules are involved, how they interact\n- **Key Design Decisions**: Important choices made about the approach\n- **Relevant Codebase Areas**: Which parts of the codebase this project touches, key types and APIs involved\n\nDo 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.\n\n### File 2: .ai/<project-name>/a/context.md\n\nThis 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:\n- **Task Description**: The full task restated clearly\n- **Relevant Files**: Every file path with line ranges and descriptions of what's there\n- **Key Code Patterns**: How similar things are done in the codebase (with code snippets)\n- **Data Structures**: Relevant types, structs, classes\n- **API Methods**: Any TL schema methods involved (copied from api.tl)\n- **UI Styles**: Any relevant style definitions\n- **Localization**: Any relevant string keys\n- **Build Info**: Build command and any special notes\n- **Reference Implementations**: Similar features that can serve as templates\n\nBe extremely thorough. Another agent with NO prior context will read this file and must be able to understand everything needed to implement the task.\n```\n\nAfter this agent completes, read both `about.md` and `a/context.md` to verify they were written properly.\n\n### For Follow-up Tasks (task letter = `b`, `c`, ...)\n\nSpawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt structure:\n\n```\nYou are a context-gathering agent for a follow-up task on an existing project in a large C++ codebase (Telegram Desktop).\n\nNEW TASK: <paste the follow-up task description here>\n\nYOUR JOB: Read the existing project state, gather any additional context needed, and produce fresh documents for the new task.\n\nSteps:\n1. Read AGENTS.md for project conventions and build instructions.\n2. Read .ai/<project-name>/about.md — this is the project-level blueprint describing everything done so far.\n3. Read .ai/<project-name>/<previous-letter>/context.md — this is the previous task's gathered context.\n4. Understand what has already been implemented by reading the actual source files referenced in about.md and the previous context.\n5. 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.\n6. Read all newly relevant files thoroughly.\n\nWrite TWO files:\n\n### File 1: .ai/<project-name>/about.md (REWRITE)\n\nNOTE: 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.\n\nREWRITE 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.\n\nIt should incorporate:\n- Everything from the old about.md that is still accurate and relevant\n- The new task's functionality described as part of the project (not as \"changes to make\")\n- Any changed design decisions or architectural updates from the new task requirements\n\nIt should NOT contain:\n- Any temporal state: \"Current State\", \"Pending Changes\", \"TODO\", \"Not yet implemented\"\n- History of how requirements changed between tasks\n- References to \"the old approach\" vs \"the new approach\"\n- Task-by-task changelog or timeline\n- Any distinction between \"what was done before\" and \"what this task adds\"\n- 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)\n\nThink 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.\n\n### File 2: .ai/<project-name>/<new-letter>/context.md\n\nThis 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.\n\nIt should contain:\n- **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\n- **Relevant Files**: Every file path with line ranges relevant to THIS task (including files modified by previous tasks and any newly relevant files)\n- **Key Code Patterns**: How similar things are done in the codebase\n- **Data Structures**: Relevant types, structs, classes\n- **API Methods**: Any TL schema methods involved\n- **UI Styles**: Any relevant style definitions\n- **Localization**: Any relevant string keys\n- **Build Info**: Build command and any special notes\n- **Reference Implementations**: Similar features that can serve as templates\n\nBe 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.\n```\n\nAfter this agent completes, read both `about.md` and `<new-letter>/context.md` to verify they were written properly.\n\n## Phase 2: Planning\n\nSpawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt structure:\n\n```\nYou are a planning agent. You must create a detailed implementation plan.\n\nRead these files:\n- .ai/<project-name>/<letter>/context.md - Contains all gathered context for this task\n- Then read the specific source files referenced in context.md to understand the code deeply.\n\nThink carefully about the implementation approach.\n\nCreate a detailed plan in: .ai/<project-name>/<letter>/plan.md\n\nThe plan.md should contain:\n\n## Task\n<one-line summary>\n\n## Approach\n<high-level description of the implementation approach>\n\n## Files to Modify\n<list of files that will be created or modified>\n\n## Files to Create\n<list of new files, if any>\n\n## Implementation Steps\n\nEach step must be specific enough that an agent can execute it without ambiguity:\n- Exact file paths\n- Exact function names\n- What code to add/modify/remove\n- Where exactly in the file (after which function, in which class, etc.)\n\nNumber every step. Group steps into phases if there are more than ~8 steps.\n\n### Phase 1: <name>\n1. <specific step>\n2. <specific step>\n...\n\n### Phase 2: <name> (if needed)\n...\n\n## Build Verification\n- Build command to run\n- Expected outcome\n\n## Status\n- [ ] Phase 1: <name>\n- [ ] Phase 2: <name> (if applicable)\n- [ ] Build verification\n- [ ] Code review\n```\n\nAfter this agent completes, read `plan.md` to verify it was written properly.\n\n## Phase 3: Plan Assessment\n\nSpawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt structure:\n\n```\nYou are a plan assessment agent. Review and refine an implementation plan.\n\nRead these files:\n- .ai/<project-name>/<letter>/context.md\n- .ai/<project-name>/<letter>/plan.md\n- Then read the actual source files referenced to verify the plan makes sense.\n\nCarefully assess the plan:\n\n1. **Correctness**: Are the file paths and line references accurate? Does the plan reference real functions and types?\n2. **Completeness**: Are there missing steps? Edge cases not handled?\n3. **Code quality**: Will the plan minimize code duplication? Does it follow existing codebase patterns from AGENTS.md?\n4. **Design**: Could the approach be improved? Are there better patterns already used in the codebase?\n5. **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.\n\nUpdate plan.md with your refinements. Keep the same structure but:\n- Fix any inaccuracies\n- Add missing steps\n- Improve the approach if you found better patterns\n- Ensure phases are properly sized for single-agent execution\n- Add a line at the top of the Status section: `Phases: <N>` indicating how many implementation phases there are\n- Add `Assessed: yes` at the bottom of the file\n\nIf the plan is small enough for a single agent (roughly <=8 steps), mark it as a single phase.\n```\n\nAfter this agent completes, read `plan.md` to verify it was assessed.\n\n## Phase 4: Implementation\n\nNow read `plan.md` yourself to understand the phases.\n\nFor each phase in the plan that is not yet marked as done, spawn an implementation agent (Task tool, subagent_type=`general-purpose`):\n\n```\nYou are an implementation agent working on phase <N> of an implementation plan.\n\nRead these files first:\n- .ai/<project-name>/<letter>/context.md - Full codebase context\n- .ai/<project-name>/<letter>/plan.md - Implementation plan\n\nThen read the source files you'll be modifying.\n\nYOUR TASK: Implement ONLY Phase <N> from the plan:\n<paste the specific phase steps here>\n\nRules:\n- Follow the plan precisely\n- Follow AGENTS.md coding conventions (no comments except complex algorithms, use auto, empty line before closing brace, etc.)\n- Do NOT modify .ai/ files except to update the Status section in plan.md\n- When done, update plan.md Status section: change `- [ ] Phase <N>: ...` to `- [x] Phase <N>: ...`\n- Do NOT work on other phases\n\nWhen finished, report what you did and any issues encountered.\n```\n\nAfter each implementation agent returns:\n1. Read `plan.md` to check the status was updated.\n2. If more phases remain, spawn the next implementation agent.\n3. If all phases are done, proceed to build verification.\n\n## Phase 5: Build Verification\n\nOnly run this phase if the task involved modifying project source code (not just docs or config).\n\nSpawn a build verification agent (Task tool, subagent_type=`general-purpose`):\n\n```\nYou are a build verification agent.\n\nRead these files:\n- .ai/<project-name>/<letter>/context.md\n- .ai/<project-name>/<letter>/plan.md\n\nThe implementation is complete. Your job is to build the project and fix any build errors.\n\nSteps:\n1. Run (from repository root): cmake --build ./out --config Debug --target Telegram\n2. If the build succeeds, update plan.md: change `- [ ] Build verification` to `- [x] Build verification`\n3. If the build fails:\n   a. Read the error messages carefully\n   b. Read the relevant source files\n   c. Fix the errors in accordance with the plan and AGENTS.md conventions\n   d. Rebuild and repeat until the build passes\n   e. Update plan.md status when done\n\nRules:\n- Only fix build errors, do not refactor or improve code\n- Follow AGENTS.md conventions\n- If build fails with file-locked errors (C1041, LNK1104), STOP and report - do not retry\n\nWhen finished, report the build result.\n```\n\nAfter the build agent returns, read `plan.md` to confirm the final status. Then proceed to Phase 6.\n\n## Phase 6: Code Review Loop\n\nAfter build verification passes, run up to 3 review-fix iterations to improve code quality. Set iteration counter `R = 1`.\n\n### Review Loop\n\n```\nLOOP:\n  1. Spawn review agent (Step 6a) with iteration R\n  2. Read review<R>.md verdict:\n     - \"APPROVED\" → go to FINISH\n     - Has improvement suggestions → spawn fix agent (Step 6b)\n  3. After fix agent completes and build passes:\n     R = R + 1\n     If R > 3 → go to FINISH (stop iterating, accept current state)\n     Otherwise → go to step 1\n\nFINISH:\n  - Update plan.md: change `- [ ] Code review` to `- [x] Code review`\n  - Proceed to Completion\n```\n\n### Step 6a: Code Review Agent\n\nSpawn an agent (Task tool, subagent_type=`general-purpose`):\n\n```\nYou are a code review agent for Telegram Desktop (C++ / Qt).\n\nRead these files:\n- .ai/<project-name>/<letter>/context.md - Codebase context\n- .ai/<project-name>/<letter>/plan.md - Implementation plan\n- REVIEW.md - Style and formatting rules to enforce\n<if R > 1, also read:>\n- .ai/<project-name>/<letter>/review<R-1>.md - Previous review (to see what was already addressed)\n\nThen 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.\n\nThen read the modified source files in full to understand changes in context.\n\nPerform a thorough code review.\n\nREVIEW CRITERIA (in order of importance):\n\n1. **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.\n\n2. **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.\n\n3. **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.\n\n4. **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.\n\n5. **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.\n\n6. **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.\n\n7. **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.\n\n8. **Style compliance**: Verify adherence to REVIEW.md rules (empty line before closing brace, operators at start of continuation lines, minimize type checks with direct cast instead of is+as, no if-with-initializer when simpler alternatives exist) and AGENTS.md conventions (no unnecessary comments, `auto` usage, no hardcoded sizes — must use .style definitions), etc.\n\nIMPORTANT GUIDELINES:\n- Review ONLY the changes made, not pre-existing code in the repository.\n- Be pragmatic. Don't suggest changes for the sake of it. Each suggestion should have a clear, concrete benefit.\n- Don't suggest adding comments, docstrings, or type annotations — the codebase style avoids these.\n- Don't suggest error handling for impossible scenarios or over-engineering.\n\nWrite your review to: .ai/<project-name>/<letter>/review<R>.md\n\nThe review document should contain:\n\n## Code Review - Iteration <R>\n\n## Summary\n<1-2 sentence overall assessment>\n\n## Verdict: <APPROVED or NEEDS_CHANGES>\n\n<If APPROVED, stop here. Everything looks good.>\n\n<If NEEDS_CHANGES, continue with:>\n\n## Changes Required\n\n### <Issue 1 title>\n- **Category**: <dead code | duplication | wrong placement | function decomposition | module structure | style | correctness>\n- **File(s)**: <file paths>\n- **Problem**: <clear description of what's wrong>\n- **Fix**: <specific description of what to change>\n\n### <Issue 2 title>\n...\n\nKeep 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.\n\nWhen finished, report your verdict clearly as: APPROVED or NEEDS_CHANGES.\n```\n\nAfter the review agent returns, read `review<R>.md`. If the verdict is APPROVED, proceed to Completion. If NEEDS_CHANGES, spawn the fix agent.\n\n### Step 6b: Review Fix Agent\n\nSpawn an agent (Task tool, subagent_type=`general-purpose`):\n\n```\nYou are a review fix agent. You implement improvements identified during code review.\n\nRead these files:\n- .ai/<project-name>/<letter>/context.md - Codebase context\n- .ai/<project-name>/<letter>/plan.md - Original implementation plan\n- .ai/<project-name>/<letter>/review<R>.md - Code review with required changes\n\nThen read the source files mentioned in the review.\n\nYOUR TASK: Implement ALL changes listed in review<R>.md.\n\nFor each issue in the review:\n1. Read the relevant source file(s).\n2. Make the specified change.\n3. Verify the change makes sense in context.\n\nAfter all changes are made:\n1. Build (from repository root): cmake --build ./out --config Debug --target Telegram\n2. If the build fails, fix build errors and rebuild until it passes.\n3. If build fails with file-locked errors (C1041, LNK1104), STOP and report - do not retry.\n\nRules:\n- Implement exactly the changes from the review, nothing more.\n- Follow AGENTS.md coding conventions.\n- Do NOT modify .ai/ files.\n\nWhen finished, report what changes were made.\n```\n\nAfter the fix agent returns, increment R and loop back to Step 6a (unless R > 3, in which case proceed to Completion).\n\n## Completion\n\nWhen all phases including build verification and code review are done:\n1. Read the final `plan.md` and report the summary to the user.\n2. Show which files were modified/created.\n3. Note any issues encountered during implementation.\n4. Summarize code review iterations: how many rounds, what was found and fixed, or if it was approved on first pass.\n5. Calculate and display the total elapsed time since `$START_TIME` (format as `Xh Ym Zs`, omitting zero components — e.g. `12m 34s` or `1h 5m 12s`).\n6. Remind the user of the project name so they can use `/task <project-name> <follow-up description>` for follow-up changes.\n\n## Error Handling\n\n- If any agent fails or gets stuck, report the issue to the user and ask how to proceed.\n- If context.md or plan.md is not written properly by an agent, re-spawn that agent with more specific instructions.\n- If build errors persist after the build agent's attempts, report the remaining errors to the user.\n- If a review fix agent introduces new build errors that it cannot resolve, report to the user.\n"
  },
  {
    "path": ".claude/commands/withtest.md",
    "content": "---\ndescription: Implement a feature using multi-agent workflow, then iteratively test and fix it in-app\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash, Task, AskUserQuestion, TodoWrite\n---\n\n# WithTest - Multi-Agent Implementation + Testing Workflow\n\nYou 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.\n\n**Arguments:** `$ARGUMENTS` = \"$ARGUMENTS\"\n\nIf `$ARGUMENTS` is provided, it's the task description. If empty, ask the user what they want implemented.\n\n## Overview\n\nThe workflow produces `.ai/<feature-name>/` containing:\n- `context.md` - Gathered codebase context relevant to the task\n- `plan.md` - Detailed implementation plan with phases and status\n- `testN.md` - Test plan for iteration N\n- `resultN.md` - Test result report for iteration N\n- `planN.md` - Fix plan for iteration N (if implementation bugs found)\n- `screenshots/` - Screenshots captured during test runs\n\nTwo major stages:\n1. **Implementation** (Phases 0-5) - same as `/task`\n2. **Testing Loop** (Phase 6) - iterative test-plan → test-do → test-run → test-check cycle\n\n---\n\n## STAGE 1: IMPLEMENTATION (Phases 0-5)\n\nThese phases are identical to the `/task` workflow.\n\n### Phase 0: Setup\n\n1. Understand the task from `$ARGUMENTS` or ask the user.\n2. **Follow-up detection:** Check if `$ARGUMENTS` starts with a task name (the first word/token before any whitespace or newline). Look for `.ai/<that-name>/` directory:\n   - If `.ai/<that-name>/` 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.\n   - If no matching directory exists, this is a **new task** - proceed normally.\n3. For new tasks: check existing folders in `.ai/` to pick a unique short name (1-2 lowercase words, hyphen-separated) and create `.ai/<feature-name>/`.\n4. For follow-up tasks: the folder already exists, skip creation.\n\n### Follow-up Task Flow\n\nWhen a follow-up task is detected (existing `.ai/<name>/` with `context.md` and `plan.md`):\n\n1. Skip Phase 1 (Context Gathering) - context already exists.\n2. Skip Phase 2 (Planning) - original plan already exists.\n3. Go directly to **Phase 2F (Follow-up Planning)** instead of Phase 3.\n\n**Phase 2F: Follow-up Planning**\n\nSpawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt:\n\n```\nYou are a planning agent for a follow-up task on an existing implementation.\n\nRead these files:\n- .ai/<feature-name>/context.md - Previously gathered codebase context\n- .ai/<feature-name>/plan.md - Previous implementation plan (already completed)\n\nThen read the source files referenced in context.md and plan.md to understand what was already implemented.\n\nFOLLOW-UP TASK: <paste the follow-up task description here>\n\nThe previous plan was already implemented and tested. Now there are follow-up changes needed.\n\nYOUR JOB:\n1. Understand what was already done from plan.md (look at the completed phases).\n2. Read the actual source files to see the current state of the code.\n3. 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.\n4. Create a NEW follow-up plan. Update plan.md by:\n   - Keep the existing content as history (do NOT delete it)\n   - Add a new section at the end:\n\n   ---\n   ## Follow-up Task\n   <description>\n\n   ## Follow-up Approach\n   <high-level description>\n\n   ## Follow-up Files to Modify\n   <list>\n\n   ## Follow-up Implementation Steps\n\n   ### Phase F1: <name>\n   1. <specific step>\n   2. ...\n\n   ### Phase F2: <name> (if needed)\n   ...\n\n   ## Follow-up Status\n   Phases: <N>\n   - [ ] Phase F1: <name>\n   - [ ] Phase F2: <name> (if applicable)\n   - [ ] Build verification\n   - [ ] Testing\n   Assessed: yes\n\nReason 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.\n```\n\nAfter 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.\n\n### New Task Flow\n\nWhen this is a new task (no existing folder), proceed with Phases 1-5 as described below.\n\n### Phase 1: Context Gathering\n\nSpawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt structure:\n\n```\nYou are a context-gathering agent for a large C++ codebase (Telegram Desktop).\n\nTASK: <paste the user's task description here>\n\nYOUR JOB: Read CLAUDE.md, inspect the codebase, find ALL files and code relevant to this task, and write a comprehensive context document.\n\nSteps:\n1. Read CLAUDE.md for project conventions and build instructions.\n2. Search the codebase for files, classes, functions, and patterns related to the task.\n3. Read all potentially relevant files. Be thorough - read more rather than less.\n4. For each relevant file, note:\n   - File path\n   - Relevant line ranges\n   - What the code does and how it relates to the task\n   - Key data structures, function signatures, patterns used\n5. Look for similar existing features that could serve as a reference implementation.\n6. Check api.tl if the task involves Telegram API.\n7. Check .style files if the task involves UI.\n8. Check lang.strings if the task involves user-visible text.\n\nWrite your findings to: .ai/<feature-name>/context.md\n\nThe context.md should contain:\n- **Task Description**: The full task restated clearly\n- **Relevant Files**: Every file path with line ranges and descriptions of what's there\n- **Key Code Patterns**: How similar things are done in the codebase (with code snippets)\n- **Data Structures**: Relevant types, structs, classes\n- **API Methods**: Any TL schema methods involved (copied from api.tl)\n- **UI Styles**: Any relevant style definitions\n- **Localization**: Any relevant string keys\n- **Build Info**: Build command and any special notes\n- **Reference Implementations**: Similar features that can serve as templates\n\nBe extremely thorough. Another agent with NO prior context will read this file and must be able to understand everything needed to implement the task.\n```\n\nAfter this agent completes, read `context.md` to verify it was written properly.\n\n### Phase 2: Planning\n\nSpawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt structure:\n\n```\nYou are a planning agent. You must create a detailed implementation plan.\n\nRead these files:\n- .ai/<feature-name>/context.md - Contains all gathered context\n- Then read the specific source files referenced in context.md to understand the code deeply.\n\nThink carefully about the implementation approach.\n\nCreate a detailed plan in: .ai/<feature-name>/plan.md\n\nThe plan.md should contain:\n\n## Task\n<one-line summary>\n\n## Approach\n<high-level description of the implementation approach>\n\n## Files to Modify\n<list of files that will be created or modified>\n\n## Files to Create\n<list of new files, if any>\n\n## Implementation Steps\n\nEach step must be specific enough that an agent can execute it without ambiguity:\n- Exact file paths\n- Exact function names\n- What code to add/modify/remove\n- Where exactly in the file (after which function, in which class, etc.)\n\nNumber every step. Group steps into phases if there are more than ~8 steps.\n\n### Phase 1: <name>\n1. <specific step>\n2. <specific step>\n...\n\n### Phase 2: <name> (if needed)\n...\n\n## Build Verification\n- Build command to run\n- Expected outcome\n\n## Status\n- [ ] Phase 1: <name>\n- [ ] Phase 2: <name> (if applicable)\n- [ ] Build verification\n- [ ] Testing\n```\n\nAfter this agent completes, read `plan.md` to verify it was written properly.\n\n### Phase 3: Plan Assessment\n\nSpawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt structure:\n\n```\nYou are a plan assessment agent. Review and refine an implementation plan.\n\nRead these files:\n- .ai/<feature-name>/context.md\n- .ai/<feature-name>/plan.md\n- Then read the actual source files referenced to verify the plan makes sense.\n\nCarefully assess the plan:\n\n1. **Correctness**: Are the file paths and line references accurate? Does the plan reference real functions and types?\n2. **Completeness**: Are there missing steps? Edge cases not handled?\n3. **Code quality**: Will the plan minimize code duplication? Does it follow existing codebase patterns from CLAUDE.md?\n4. **Design**: Could the approach be improved? Are there better patterns already used in the codebase?\n5. **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.\n\nUpdate plan.md with your refinements. Keep the same structure but:\n- Fix any inaccuracies\n- Add missing steps\n- Improve the approach if you found better patterns\n- Ensure phases are properly sized for single-agent execution\n- Add a line at the top of the Status section: `Phases: <N>` indicating how many implementation phases there are\n- Add `Assessed: yes` at the bottom of the file\n\nIf the plan is small enough for a single agent (roughly <=8 steps), mark it as a single phase.\n```\n\nAfter this agent completes, read `plan.md` to verify it was assessed.\n\n### Phase 4: Implementation\n\nNow read `plan.md` yourself to understand the phases.\n\nFor each phase in the plan that is not yet marked as done, spawn an implementation agent (Task tool, subagent_type=`general-purpose`):\n\n```\nYou are an implementation agent working on phase <N> of an implementation plan.\n\nRead these files first:\n- .ai/<feature-name>/context.md - Full codebase context\n- .ai/<feature-name>/plan.md - Implementation plan\n\nThen read the source files you'll be modifying.\n\nYOUR TASK: Implement ONLY Phase <N> from the plan:\n<paste the specific phase steps here>\n\nRules:\n- Follow the plan precisely\n- Follow CLAUDE.md coding conventions (no comments except complex algorithms, use auto, empty line before closing brace, etc.)\n- Do NOT modify .ai/ files except to update the Status section in plan.md\n- When done, update plan.md Status section: change `- [ ] Phase <N>: ...` to `- [x] Phase <N>: ...`\n- Do NOT work on other phases\n\nWhen finished, report what you did and any issues encountered.\n```\n\nAfter each implementation agent returns:\n1. Read `plan.md` to check the status was updated.\n2. If more phases remain, spawn the next implementation agent.\n3. If all phases are done, proceed to build verification.\n\n### Phase 5: Build Verification\n\nSpawn a build verification agent (Task tool, subagent_type=`general-purpose`):\n\n```\nYou are a build verification agent.\n\nRead these files:\n- .ai/<feature-name>/context.md\n- .ai/<feature-name>/plan.md\n\nThe implementation is complete. Your job is to build the project and fix any build errors.\n\nSteps:\n1. Run: cmake --build \"c:\\Telegram\\tdesktop\\out\" --config Debug --target Telegram\n2. If the build succeeds, update plan.md: change `- [ ] Build verification` to `- [x] Build verification`\n3. If the build fails:\n   a. Read the error messages carefully\n   b. Read the relevant source files\n   c. Fix the errors in accordance with the plan and CLAUDE.md conventions\n   d. Rebuild and repeat until the build passes\n   e. Update plan.md status when done\n\nRules:\n- Only fix build errors, do not refactor or improve code\n- Follow CLAUDE.md conventions\n- If build fails with file-locked errors (C1041, LNK1104), STOP and report - do not retry\n\nWhen finished, report the build result.\n```\n\nAfter the build agent returns, read `plan.md` to confirm build verification passed. If it did, proceed to Stage 2.\n\n---\n\n## STAGE 2: TESTING LOOP (Phase 6)\n\nThis stage iteratively tests the implementation in-app and fixes issues. It maintains an iteration counter `N` starting at 1.\n\n**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.\n\n### Git Submodule Awareness\n\nBefore ANY git operation (commit, stash, stash pop), the agent must:\n1. Run `git submodule status` to check for modified submodules.\n2. If submodules have changes, commit/stash those submodules FIRST, individually:\n   ```\n   cd <submodule-path> && git add -A && git commit -m \"[wip-N] test changes\" && cd <repo-root>\n   ```\n   or for stash:\n   ```\n   cd <submodule-path> && git stash && cd <repo-root>\n   ```\n3. Then operate on the main repo.\n\n### Step 6a: Test Plan (test-plan agent)\n\nSpawn an agent (Task tool, subagent_type=`general-purpose`):\n\n```\nYou are a test-planning agent for Telegram Desktop (C++ / Qt).\n\nRead these files:\n- .ai/<feature-name>/context.md\n- .ai/<feature-name>/plan.md\n<if N > 1, also include:>\n- .ai/<feature-name>/result<N-1>.md - Previous test result\n<if a planN.md triggered this iteration:>\n- .ai/<feature-name>/plan<trigger>.md - Fix plan that was just implemented\n\nCURRENT ITERATION: <N>\n\nYOUR TASKS:\n\n1. **Commit current implementation changes.**\n   - Run `git submodule status` to check for modified submodules.\n   - If any submodules are dirty, go into each one and commit:\n     `cd <submodule> && git add -A && git commit -m \"[wip-<N>]\" && cd <repo-root>`\n   - Then in main repo: `git add -A && git commit -m \"[wip-<N>]\"`\n   - Do NOT add files in .ai/ to the commit.\n\n2. <If N > 1> **Restore previous test code.**\n   - Run `git submodule status` and `git stash list` in any dirty submodules to check for stashed test code.\n   - Pop submodule stashes first: `cd <submodule> && git stash pop && cd <repo-root>`\n   - Then pop main repo stash: `git stash pop`\n   - Read the previous test<N-1>.md to understand what was tested before.\n   - Decide: reuse/modify existing test code or start fresh.\n\n3. **Plan the test code.**\n   Carefully design test code that will verify the implementation works correctly.\n\n   The test code must:\n   - Be wrapped in `#ifdef _DEBUG` blocks so it only runs in Debug builds\n   - Be injected at appropriate points in the app lifecycle (e.g., after main window shows, after chats load, etc.)\n   - Write progress and results to a log file. Use a dedicated path like:\n     `QFile logFile(\"c:/Telegram/tdesktop/.ai/<feature-name>/test_log.txt\");`\n     Open with `QIODevice::Append | QIODevice::Text`, write with QTextStream, and flush after every write.\n   - Save screenshots where visual verification is needed:\n     `widget->grab().save(\"c:/Telegram/tdesktop/.ai/<feature-name>/screenshots/<name>.png\");`\n     Log each screenshot save: `\"SCREENSHOT: <full-path>\"`\n   - Use `QTimer::singleShot(...)` or deferred calls to schedule test steps after UI events settle\n   - Call `Core::Quit()` when all test steps complete, so the app exits cleanly\n   - Log `\"TEST_COMPLETE\"` right before `Core::Quit()` so the test-run agent knows testing finished\n   - Log `\"TEST_STEP: <description>\"` before each major step for progress tracking\n   - Log `\"TEST_RESULT: PASS: <what>\"` or `\"TEST_RESULT: FAIL: <what> - <details>\"` for each check\n\n   Consider what needs testing:\n   - Does the new UI appear correctly?\n   - Do interactions work (clicks, navigation)?\n   - Does data flow correctly?\n   - Are there edge cases to verify?\n\n4. **Write the test plan** to `.ai/<feature-name>/test<N>.md` containing:\n\n   ## Test Iteration <N>\n   ## What We're Testing\n   <description of what this test verifies>\n\n   ## Test Steps\n   1. <step>: what we do, what we expect, how we verify\n   2. ...\n\n   ## Code Injection Points\n   - File: <path>, Location: <where in file>, Purpose: <what this block does>\n   - ...\n\n   ## Expected Log Output\n   <example of what test_log.txt should contain if everything works>\n\n   ## Expected Screenshots\n   - <name>.png: should show <description>\n   - ...\n\n   ## Success Criteria\n   - <criterion 1>\n   - <criterion 2>\n   - ...\n\nWhen finished, report what test plan was created.\n```\n\n### Step 6b: Test Implementation (test-do agent)\n\nSpawn an agent (Task tool, subagent_type=`general-purpose`):\n\n```\nYou are a test implementation agent for Telegram Desktop (C++ / Qt).\n\nRead these files:\n- .ai/<feature-name>/context.md\n- .ai/<feature-name>/plan.md\n- .ai/<feature-name>/test<N>.md - The test plan to implement\n\nYOUR TASK: Implement the test code described in test<N>.md.\n\nRules:\n- ALL test code MUST be inside `#ifdef _DEBUG` blocks\n- Place test code at the injection points specified in the test plan\n- Make sure the screenshots folder exists: create `.ai/<feature-name>/screenshots/` directory\n- Delete any old test_log.txt before the test starts (in code, at the first test step)\n- Use QTimer::singleShot for delayed operations to let the UI settle\n- Flush log writes immediately (don't buffer)\n- End with logging \"TEST_COMPLETE\" and calling Core::Quit()\n- Follow CLAUDE.md coding conventions\n- Make sure the code compiles: run `cmake --build \"c:\\Telegram\\tdesktop\\out\" --config Debug --target Telegram`\n- If build fails, fix errors and rebuild until it passes\n- If build fails with file-locked errors (C1041, LNK1104), STOP and report\n\nWhen finished, report what test code was added and where.\n```\n\n### Step 6c: Test Run (test-run agent)\n\nSpawn an agent (Task tool, subagent_type=`general-purpose`):\n\n```\nYou are a test execution agent. You run the Telegram Desktop app and observe test output.\n\nRead these files:\n- .ai/<feature-name>/test<N>.md - The test plan (so you know what to expect)\n\nYOUR TASK: Run the built app and monitor test execution.\n\nSteps:\n\n1. **Prepare.**\n   - Delete old test_log.txt if it exists: `del \"c:\\Telegram\\tdesktop\\docs\\ai\\work\\<feature-name>\\test_log.txt\" 2>nul`\n   - Ensure screenshots folder exists: `mkdir \"c:\\Telegram\\tdesktop\\docs\\ai\\work\\<feature-name>\\screenshots\" 2>nul`\n\n2. **Launch the app.**\n   - Run in background: `start \"\" \"c:\\Telegram\\tdesktop\\out\\Debug\\Telegram.exe\"`\n   - Note the time of launch.\n\n3. **Monitor test_log.txt in a polling loop.**\n   - Every 5 seconds, read the log file to check for new output.\n   - When you see `\"SCREENSHOT: <path>\"`, read the screenshot image file to visually verify it.\n   - Track which TEST_STEP entries appear.\n   - Track TEST_RESULT entries (PASS/FAIL).\n\n4. **Detect completion or failure.**\n   - **Success**: Log contains `\"TEST_COMPLETE\"` - the app should exit on its own shortly after.\n   - **Crash**: The process disappears before `\"TEST_COMPLETE\"`. Check for crash dumps or error dialogs.\n   - **Hang/Timeout**: If no new log output for 120 seconds and no `\"TEST_COMPLETE\"`, kill the process:\n     `taskkill /IM Telegram.exe /F`\n   - **No log at all**: If no test_log.txt appears within 60 seconds of launch, kill the process.\n\n5. **After the process exits (or is killed), wait 5 seconds, then:**\n   - Read the full final test_log.txt\n   - Read all screenshot files saved during the test\n   - Check for any leftover Telegram.exe processes: `tasklist /FI \"IMAGENAME eq Telegram.exe\"` and kill if needed\n\n6. **Write the result report** to `.ai/<feature-name>/result<N>.md`:\n\n   ## Test Result - Iteration <N>\n   ## Outcome: <PASS / FAIL / CRASH / TIMEOUT>\n\n   ## Log Output\n   <full contents of test_log.txt, or note that it was empty/missing>\n\n   ## Screenshot Analysis\n   - <name>.png: <description of what you see, whether it matches expectations from test<N>.md>\n   - ...\n\n   ## Test Results Summary\n   - PASS: <list>\n   - FAIL: <list>\n\n   ## Issues Found\n   <any problems observed, unexpected behavior, etc.>\n\n   ## Raw Details\n   <process exit code if available, timing information, any stderr output>\n\nWhen finished, report the test outcome.\n```\n\nAfter the test-run agent returns, read `result<N>.md`.\n\n### Step 6d: Test Assessment (test-check agent)\n\nSpawn an agent (Task tool, subagent_type=`general-purpose`):\n\n```\nYou are a test assessment agent. You analyze test results and decide next steps.\n\nRead these files:\n- .ai/<feature-name>/context.md\n- .ai/<feature-name>/plan.md\n- .ai/<feature-name>/test<N>.md\n- .ai/<feature-name>/result<N>.md\n<if N > 1, also read previous test/result pairs for history>\n\nCarefully analyze the test results.\n\nDECIDE one of three outcomes:\n\n### Outcome A: ALL TESTS PASS\nIf all test results are PASS and screenshots look correct:\n1. Write to result<N>.md (append): `\\n## Verdict: PASS`\n2. Report \"ALL_TESTS_PASS\" so the orchestrator knows to finish.\n\n### Outcome B: TEST CODE NEEDS CHANGES\nIf the test itself was flawed (wrong assertions, bad timing, insufficient waits, screenshot taken too early, wrong injection point, etc.) but the implementation seems correct:\n1. Describe what's wrong with the test and what to change.\n2. Make the changes directly to the test code in the source files.\n3. Rebuild: `cmake --build \"c:\\Telegram\\tdesktop\\out\" --config Debug --target Telegram`\n4. If build fails with file-locked errors (C1041, LNK1104), STOP and report.\n5. Write the updated test description to `.ai/<feature-name>/test<N+1>.md` explaining what changed and why.\n6. Report \"TEST_NEEDS_RERUN\" so the orchestrator goes back to step 6c.\n\n### Outcome C: IMPLEMENTATION HAS BUGS\nIf the test results indicate actual bugs in the implementation (not test issues):\n1. Analyze what's wrong with the implementation.\n2. Write a fix plan to `.ai/<feature-name>/plan<N>.md`:\n\n   ## Fix Plan - Iteration <N>\n   ## Problem\n   <what the test revealed>\n\n   ## Root Cause\n   <analysis of why the implementation is wrong>\n\n   ## Fix Steps\n   1. <specific fix with file path, location, what to change>\n   2. ...\n\n3. Stash the test code (it will be restored later):\n   - Run `git submodule status` and stash dirty submodules first:\n     `cd <submodule> && git stash && cd <repo-root>`\n   - Then: `git stash`\n4. Report \"IMPLEMENTATION_NEEDS_FIX\" so the orchestrator goes to re-implementation.\n\nWhen finished, report your verdict clearly as one of: ALL_TESTS_PASS, TEST_NEEDS_RERUN, IMPLEMENTATION_NEEDS_FIX.\n```\n\n### Orchestrator Loop Logic\n\nAfter Phase 5 (build verification) succeeds, you (the orchestrator) run the testing loop:\n\n```\nSet N = 1\n\nLOOP:\n  1. Spawn test-plan agent (Step 6a) with iteration N\n  2. Spawn test-do agent (Step 6b) with iteration N\n  3. Spawn test-run agent (Step 6c) with iteration N\n  4. Spawn test-check agent (Step 6d) with iteration N\n  5. Read the verdict:\n     - \"ALL_TESTS_PASS\" → go to FINISH\n     - \"TEST_NEEDS_RERUN\" →\n         N = N + 1\n         go to step 3 (skip 6a and 6b, test code was already updated by test-check)\n     - \"IMPLEMENTATION_NEEDS_FIX\" →\n         Spawn implementation fix agent (see below)\n         N = N + 1\n         go to step 1 (full restart: new commit, stash pop test code, etc.)\n  6. Safety: if N > 5, stop and report to user - too many iterations.\n\nFINISH:\n  - Stash or revert all test code (#ifdef _DEBUG blocks):\n    - git submodule status, stash submodules if dirty\n    - git stash (to save test code separately, user may want it later)\n  - Update plan.md: change `- [ ] Testing` to `- [x] Testing`\n  - Report to user\n```\n\n### Implementation Fix Agent\n\nWhen test-check reports IMPLEMENTATION_NEEDS_FIX, spawn this agent:\n\n```\nYou are an implementation fix agent.\n\nRead these files:\n- .ai/<feature-name>/context.md\n- .ai/<feature-name>/plan.md\n- .ai/<feature-name>/plan<N>.md - The fix plan from test assessment\n\nThen read the source files mentioned in the fix plan.\n\nYOUR TASK: Implement the fixes described in plan<N>.md.\n\nSteps:\n1. Read and understand the fix plan.\n2. Make the specified code changes.\n3. Build: `cmake --build \"c:\\Telegram\\tdesktop\\out\" --config Debug --target Telegram`\n4. Fix any build errors.\n5. If build fails with file-locked errors (C1041, LNK1104), STOP and report.\n\nRules:\n- Only make changes specified in the fix plan\n- Follow CLAUDE.md conventions\n- Do NOT touch test code or .ai/ files (except plan.md status if relevant)\n\nWhen finished, report what was fixed.\n```\n\n---\n\n## Completion\n\nWhen the testing loop finishes (ALL_TESTS_PASS or user stops it):\n1. Read the final `plan.md` and report full summary to the user.\n2. List all files modified/created by the implementation.\n3. Summarize test iterations: how many rounds, what was found and fixed.\n4. Note that test code is stashed (available via `git stash pop` if needed).\n5. Note any remaining concerns.\n\n## Error Handling\n\n- If any agent fails or gets stuck, report the issue to the user and ask how to proceed.\n- If context.md or plan.md is not written properly by an agent, re-spawn that agent with more specific instructions.\n- If build errors persist after agent attempts, report remaining errors to the user.\n- If the testing loop exceeds 5 iterations, stop and report - something fundamental may be wrong.\n- If the app crashes repeatedly, report to user - may need manual investigation.\n- If file-locked build errors occur at ANY point, stop immediately and ask user to close Telegram.exe.\n"
  },
  {
    "path": ".claude/grab_clipboard.ps1",
    "content": "param([string]$outPath)\nAdd-Type -AssemblyName System.Windows.Forms\n$img = [System.Windows.Forms.Clipboard]::GetImage()\nif ($img) {\n    $img.Save($outPath, [System.Drawing.Imaging.ImageFormat]::Png)\n    Write-Host \"Saved to $outPath\"\n    exit 0\n} else {\n    Write-Host \"No image on clipboard\"\n    exit 1\n}\n"
  },
  {
    "path": ".claude/grab_clipboard.sh",
    "content": "#!/bin/bash\n# Grab clipboard image on macOS and save as PNG.\noutPath=\"$1\"\nif [ -z \"$outPath\" ]; then\n    echo \"Usage: grab_clipboard.sh <output.png>\"\n    exit 1\nfi\n\nosascript -e '\nset theFile to POSIX file \"'\"$outPath\"'\"\ntry\n    set theImage to the clipboard as «class PNGf»\non error\n    return \"no image\"\nend try\nset fh to open for access theFile with write permission\nwrite theImage to fh\nclose access fh\nreturn \"ok\"\n' 2>/dev/null | grep -q \"ok\"\n\nif [ $? -eq 0 ]; then\n    echo \"Saved to $outPath\"\n    exit 0\nelse\n    echo \"No image on clipboard\"\n    exit 1\nfi\n"
  },
  {
    "path": ".claude/iterate.ps1",
    "content": "#!/usr/bin/env pwsh\n# Iterative Task Runner\n# Runs Claude Code in a loop to complete tasks from a taskplanner-created folder\n#\n# Usage: .\\docs\\ai\\iterate.ps1 <featurename> [-MaxIterations N] [-Interactive] [-DryRun] [-SingleCommit] [-NoCommit]\n#\n# Arguments:\n#   featurename     Name of the folder in .ai/ containing prompt.md and tasks.json\n#   -MaxIterations  Maximum iterations before stopping (default: 50)\n#   -Interactive    Pause between iterations for user confirmation (default: auto/no pause)\n#   -DryRun         Show what would be executed without running\n#   -SingleCommit   Don't commit after each task, commit all changes at the end\n#   -NoCommit       Don't commit at all (no per-task commits, no final commit)\n\nparam(\n    [Parameter(Position=0, Mandatory=$true)]\n    [string]$FeatureName,\n\n    [int]$MaxIterations = 50,\n    [switch]$Interactive,\n    [switch]$DryRun,\n    [switch]$SingleCommit,\n    [switch]$NoCommit\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path\n$RepoRoot = Resolve-Path (Join-Path $ScriptDir \"..\\..\")\n$WorkDir = Join-Path $ScriptDir \"work\\$FeatureName\"\n$PromptMd = Join-Path $WorkDir \"prompt.md\"\n$TasksJson = Join-Path $WorkDir \"tasks.json\"\n\n$BuildOutputDir = Join-Path $RepoRoot \"out\\Debug\"\n$TelegramExe = Join-Path $BuildOutputDir \"Telegram.exe\"\n$TelegramPdb = Join-Path $BuildOutputDir \"Telegram.pdb\"\n\nfunction Format-Duration {\n    param([int]$Seconds)\n\n    if ($Seconds -lt 60) {\n        return \"${Seconds}s\"\n    } elseif ($Seconds -lt 3600) {\n        $min = [math]::Floor($Seconds / 60)\n        $sec = $Seconds % 60\n        return \"${min}m ${sec}s\"\n    } else {\n        $hr = [math]::Floor($Seconds / 3600)\n        $min = [math]::Floor(($Seconds % 3600) / 60)\n        $sec = $Seconds % 60\n        return \"${hr}h ${min}m ${sec}s\"\n    }\n}\n\nfunction Test-BuildFilesUnlocked {\n    $filesToCheck = @($TelegramExe, $TelegramPdb)\n\n    foreach ($file in $filesToCheck) {\n        if (Test-Path $file) {\n            try {\n                Remove-Item $file -Force -ErrorAction Stop\n                Write-Host \"Removed: $file\" -ForegroundColor DarkGray\n            }\n            catch {\n                Write-Host \"\"\n                Write-Host \"========================================\" -ForegroundColor Red\n                Write-Host \"  ERROR: Cannot delete build output\" -ForegroundColor Red\n                Write-Host \"  File is locked: $file\" -ForegroundColor Red\n                Write-Host \"\" -ForegroundColor Red\n                Write-Host \"  Please close Telegram.exe and any\" -ForegroundColor Red\n                Write-Host \"  debugger, then try again.\" -ForegroundColor Red\n                Write-Host \"========================================\" -ForegroundColor Red\n                Write-Host \"\"\n                return $false\n            }\n        }\n    }\n    return $true\n}\n\nfunction Show-ClaudeStream {\n    param([string]$Line)\n\n    try {\n        $obj = $Line | ConvertFrom-Json -ErrorAction Stop\n\n        switch ($obj.type) {\n            \"assistant\" {\n                if ($obj.message.content) {\n                    foreach ($block in $obj.message.content) {\n                        if ($block.type -eq \"text\") {\n                            Write-Host $block.text -ForegroundColor White\n                        }\n                        elseif ($block.type -eq \"tool_use\") {\n                            $summary = \"\"\n                            if ($block.input) {\n                                if ($block.input.file_path) {\n                                    $summary = $block.input.file_path\n                                } elseif ($block.input.pattern) {\n                                    $summary = $block.input.pattern\n                                } elseif ($block.input.command) {\n                                    $cmd = $block.input.command\n                                    if ($cmd.Length -gt 60) { $cmd = $cmd.Substring(0, 60) + \"...\" }\n                                    $summary = $cmd\n                                } else {\n                                    $inputStr = $block.input | ConvertTo-Json -Compress -Depth 1\n                                    if ($inputStr.Length -gt 60) { $inputStr = $inputStr.Substring(0, 60) + \"...\" }\n                                    $summary = $inputStr\n                                }\n                            }\n                            Write-Host \"[Tool: $($block.name)] $summary\" -ForegroundColor Yellow\n                        }\n                    }\n                }\n            }\n            \"user\" {\n                # Tool results - skip verbose output\n            }\n            \"result\" {\n                Write-Host \"`n--- Session Complete ---\" -ForegroundColor Cyan\n                if ($obj.cost_usd) {\n                    Write-Host \"Cost: `$$($obj.cost_usd)\" -ForegroundColor DarkCyan\n                }\n            }\n            \"system\" {\n                # System messages - skip\n            }\n        }\n    }\n    catch {\n        # Not valid JSON, skip\n    }\n}\n\n# Verify feature folder exists\nif (-not (Test-Path $WorkDir)) {\n    Write-Error \"Feature folder not found: $WorkDir`nRun '/taskplanner $FeatureName' first to create it.\"\n    exit 1\n}\n\n# Verify required files exist\nforeach ($file in @($PromptMd, $TasksJson)) {\n    if (-not (Test-Path $file)) {\n        Write-Error \"Required file not found: $file\"\n        exit 1\n    }\n}\n\nif ($SingleCommit -or $NoCommit) {\n    $AfterImplementation = @\"\n   - Mark the task completed in tasks.json (\"completed\": true)\n   - If new tasks emerged, add them to tasks.json\n\"@\n    $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.\"\n} else {\n    $AfterImplementation = @\"\n   - Mark the task completed in tasks.json (\"completed\": true)\n   - Commit your changes\n   - If new tasks emerged, add them to tasks.json\n\"@\n    $CommitRule = \"\"\n}\n\n$Prompt = @\"\nYou are an autonomous coding agent working on: $FeatureName\n\nRead these files for context:\n- .ai/$FeatureName/prompt.md - Detailed instructions and architecture\n- .ai/$FeatureName/tasks.json - Task list with completion status\n\nDo exactly ONE task per iteration.\n\n## Steps\n\n1. 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)\n2. Plan the implementation carefully\n3. Implement that ONE task only\n4. After successful implementation:\n$AfterImplementation\n\n## Critical Rules\n\n- Only mark a task complete if you verified the work is done (build passes, etc.)\n- If stuck, document the issue in the task's notes field and move on\n- Do ONE task per iteration, then stop\n- NEVER try to commit files in .ai/\n$CommitRule\n\n## Completion Signal\n\nIf ALL tasks in tasks.json have \"completed\": true, output exactly:\n===ALL_TASKS_COMPLETE===\n\"@\n\n$CommitPrompt = @\"\nYou are an autonomous coding agent. All tasks for \"$FeatureName\" are now complete.\n\nYour job: Create a single commit with all the changes.\n\n## Steps\n\n1. Run git status to see all modified files\n2. Run git diff to review the changes\n3. Create a commit with a short summary (aim for ~50 chars, max 76 chars) describing what was implemented\n4. The commit message should describe the overall feature/fix, not list individual changes\n\n## Critical Rules\n\n- NEVER try to commit files in .ai/\n- Use a concise commit message that captures the essence of the work done\n\"@\n\nWrite-Host \"\"\nWrite-Host \"========================================\" -ForegroundColor Cyan\nWrite-Host \"  Iterative Task Runner\" -ForegroundColor Cyan\nWrite-Host \"  Feature: $FeatureName\" -ForegroundColor Cyan\nWrite-Host \"  Max iterations: $MaxIterations\" -ForegroundColor Cyan\nWrite-Host \"  Mode: $(if ($Interactive) { 'Interactive' } else { 'Auto' })\" -ForegroundColor Cyan\nWrite-Host \"  Commit: $(if ($NoCommit) { 'None' } elseif ($SingleCommit) { 'Single (at end)' } else { 'Per task' })\" -ForegroundColor Cyan\nWrite-Host \"  Working directory: $RepoRoot\" -ForegroundColor Cyan\nWrite-Host \"========================================\" -ForegroundColor Cyan\nWrite-Host \"\"\n\nif ($DryRun) {\n    Write-Host \"[DRY RUN] Would execute with prompt:\" -ForegroundColor Yellow\n    Write-Host $Prompt\n    Write-Host \"\"\n    Write-Host \"Feature folder: $WorkDir\" -ForegroundColor Yellow\n    Write-Host \"Prompt file: $PromptMd\" -ForegroundColor Yellow\n    Write-Host \"Tasks file: $TasksJson\" -ForegroundColor Yellow\n    exit 0\n}\n\nPush-Location $RepoRoot\n\n$ScriptStartTime = Get-Date\n$IterationTimes = @()\n\ntry {\n    for ($i = 1; $i -le $MaxIterations; $i++) {\n        Write-Host \"\"\n        Write-Host \"========================================\" -ForegroundColor Yellow\n        Write-Host \"  Iteration $i of $MaxIterations\" -ForegroundColor Yellow\n        Write-Host \"========================================\" -ForegroundColor Yellow\n        Write-Host \"\"\n\n        if (-not (Test-BuildFilesUnlocked)) {\n            exit 1\n        }\n\n        $IterationStartTime = Get-Date\n\n        claude --dangerously-skip-permissions --verbose -p $Prompt --output-format stream-json 2>&1 | ForEach-Object {\n            Show-ClaudeStream $_\n        }\n\n        $IterationEndTime = Get-Date\n        $IterationDuration = [int]($IterationEndTime - $IterationStartTime).TotalSeconds\n        $IterationTimes += $IterationDuration\n        Write-Host \"Iteration time: $(Format-Duration $IterationDuration)\" -ForegroundColor DarkCyan\n\n        # Check task status after each run\n        $tasks = Get-Content $TasksJson | ConvertFrom-Json\n        $incomplete = @($tasks.tasks | Where-Object { -not $_.completed })\n        $inProgress = @($tasks.tasks | Where-Object { $_.started -and -not $_.completed })\n\n        if ($incomplete.Count -eq 0) {\n            if ($SingleCommit -and -not $NoCommit) {\n                $i++\n                if ($i -le $MaxIterations) {\n                    Write-Host \"\"\n                    Write-Host \"========================================\" -ForegroundColor Yellow\n                    Write-Host \"  Final commit iteration\" -ForegroundColor Yellow\n                    Write-Host \"========================================\" -ForegroundColor Yellow\n                    Write-Host \"\"\n\n                    $CommitStartTime = Get-Date\n\n                    claude --dangerously-skip-permissions --verbose -p $CommitPrompt --output-format stream-json 2>&1 | ForEach-Object {\n                        Show-ClaudeStream $_\n                    }\n\n                    $CommitEndTime = Get-Date\n                    $CommitDuration = [int]($CommitEndTime - $CommitStartTime).TotalSeconds\n                    $IterationTimes += $CommitDuration\n                    Write-Host \"Commit time: $(Format-Duration $CommitDuration)\" -ForegroundColor DarkCyan\n                } else {\n                    Write-Host \"\"\n                    Write-Host \"========================================\" -ForegroundColor Red\n                    Write-Host \"  Max iterations reached before commit\" -ForegroundColor Red\n                    Write-Host \"  Run manually: git add . && git commit\" -ForegroundColor Red\n                    Write-Host \"========================================\" -ForegroundColor Red\n                    Write-Host \"\"\n                    exit 1\n                }\n            }\n\n            $TotalTime = [int]((Get-Date) - $ScriptStartTime).TotalSeconds\n            $AvgTime = if ($IterationTimes.Count -gt 0) { [int](($IterationTimes | Measure-Object -Sum).Sum / $IterationTimes.Count) } else { 0 }\n\n            Write-Host \"\"\n            Write-Host \"========================================\" -ForegroundColor Green\n            Write-Host \"  ALL TASKS COMPLETE!\" -ForegroundColor Green\n            Write-Host \"  Feature: $FeatureName\" -ForegroundColor Green\n            Write-Host \"  Iterations: $($IterationTimes.Count)\" -ForegroundColor Green\n            Write-Host \"  Total time: $(Format-Duration $TotalTime)\" -ForegroundColor Green\n            Write-Host \"  Avg per iteration: $(Format-Duration $AvgTime)\" -ForegroundColor Green\n            Write-Host \"========================================\" -ForegroundColor Green\n            Write-Host \"\"\n\n            exit 0\n        }\n\n        Write-Host \"\"\n        Write-Host \"Remaining tasks: $($incomplete.Count)\" -ForegroundColor Cyan\n        if ($inProgress.Count -gt 0) {\n            Write-Host \"In progress: $($inProgress[0].title)\" -ForegroundColor Yellow\n        }\n\n        if ($Interactive) {\n            Write-Host \"Press Enter to continue, Ctrl+C to stop...\" -ForegroundColor Cyan\n            Read-Host\n        } else {\n            Start-Sleep -Seconds 2\n        }\n    }\n\n    $TotalTime = [int]((Get-Date) - $ScriptStartTime).TotalSeconds\n    $AvgTime = if ($IterationTimes.Count -gt 0) { [int](($IterationTimes | Measure-Object -Sum).Sum / $IterationTimes.Count) } else { 0 }\n\n    Write-Host \"\"\n    Write-Host \"========================================\" -ForegroundColor Red\n    Write-Host \"  Max iterations ($MaxIterations) reached\" -ForegroundColor Red\n    Write-Host \"  Check tasks.json for remaining tasks\" -ForegroundColor Red\n    Write-Host \"  Total time: $(Format-Duration $TotalTime)\" -ForegroundColor Red\n    Write-Host \"  Avg per iteration: $(Format-Duration $AvgTime)\" -ForegroundColor Red\n    Write-Host \"========================================\" -ForegroundColor Red\n    Write-Host \"\"\n    exit 1\n}\nfinally {\n    Pop-Location\n}\n"
  },
  {
    "path": ".cursorignore",
    "content": "# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)\nTelegram/ThirdParty/\n"
  },
  {
    "path": ".devcontainer.json",
    "content": "{\n    \"name\": \"CentOS\",\n    \"image\": \"tdesktop:centos_env\",\n    \"customizations\": {\n        \"vscode\": {\n            \"settings\": {\n                \"C_Cpp.intelliSenseEngine\": \"disabled\",\n                \"cmake.generator\": \"Ninja Multi-Config\",\n                \"cmake.buildDirectory\": \"${workspaceFolder}/out\",\n                \"cmake.copyCompileCommands\": \"${workspaceFolder}/compile_commands.json\"\n            },\n            \"extensions\": [\n                \"ms-vscode.cpptools-extension-pack\",\n                \"llvm-vs-code-extensions.vscode-clangd\",\n                \"TheQtCompany.qt\",\n                \"ms-python.python\",\n                \"ms-azuretools.vscode-docker\",\n                \"eamodio.gitlens\"\n            ]\n        }\n    },\n    \"capAdd\": [\n        \"SYS_PTRACE\"\n    ],\n    \"securityOpt\": [\n        \"seccomp=unconfined\"\n    ],\n    \"workspaceMount\": \"source=${localWorkspaceFolder},target=/usr/src/tdesktop,type=bind,consistency=cached\",\n    \"workspaceFolder\": \"/usr/src/tdesktop\"\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Set the default behavior, in case people don't have core.autocrlf set.\n* text=auto\n\n# Ensure diffs have LF endings\n*.diff text eol=lf\n*.bat text eol=crlf\n\n# Ensure lottie animations are treated as binary files\n*.lottie binary\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis document describes how you can contribute to Telegram Desktop. Please read it carefully.\n\n**Table of Contents**\n\n* [What contributions are accepted](#what-contributions-are-accepted)\n* [Build instructions](#build-instructions)\n* [Pull upstream changes into your fork regularly](#pull-upstream-changes-into-your-fork-regularly)\n* [How to get your pull request accepted](#how-to-get-your-pull-request-accepted)\n  * [Keep your pull requests limited to a single issue](#keep-your-pull-requests-limited-to-a-single-issue)\n    * [Squash your commits to a single commit](#squash-your-commits-to-a-single-commit)\n  * [Don't mix code changes with whitespace cleanup](#dont-mix-code-changes-with-whitespace-cleanup)\n  * [Keep your code simple!](#keep-your-code-simple)\n  * [Test your changes!](#test-your-changes)\n  * [Write a good commit message](#write-a-good-commit-message)\n\n## What contributions are accepted\n\nWe highly appreciate your contributions in the matter of fixing bugs and optimizing the Telegram Desktop source code and its documentation. In case of fixing the existing user experience please push to your fork and [submit a pull request][pr].\n\nWait for us. We try to review your pull requests as fast as possible.\nIf we find issues with your pull request, we may suggest some changes and improvements.\n\nUnfortunately we **do not merge** any pull requests that have new feature implementations, translations to new languages and those which introduce any new user interface elements.\n\nIf you have a translations-related contribution, check out [Translations platform][translate].\n\nTelegram Desktop is not a standalone application but a part of [Telegram project][telegram], so all the decisions about the features, languages, user experience, user interface and the design are made inside Telegram team, often according to some roadmap which is not public.\n\n## Build instructions\n\nSee the [README.md][build_instructions] for details on the various build\nenvironments.\n\n## Pull upstream changes into your fork regularly\n\nTelegram Desktop is advancing quickly. It is therefore critical that you pull upstream changes into your fork on a regular basis. Nothing is worse than putting in a days of hard work into a pull request only to have it rejected because it has diverged too far from upstream.\n\nTo pull in upstream changes:\n\n    git remote add upstream https://github.com/telegramdesktop/tdesktop.git\n    git fetch upstream master\n\nCheck the log to be sure that you actually want the changes, before merging:\n\n    git log upstream/master\n\nThen rebase your changes on the latest commits in the `master` branch:\n\n    git rebase upstream/master\n\nAfter that, you have to force push your commits:\n\n    git push --force\n\nFor more info, see [GitHub Help][help_fork_repo].\n\n## How to get your pull request accepted\n\nWe want to improve Telegram Desktop with your contributions. But we also want to provide a stable experience for our users and the community. Follow these rules and you should succeed without a problem!\n\n### Keep your pull requests limited to a single issue\n\nPull requests should be as small/atomic as possible. Large, wide-sweeping changes in a pull request will be **rejected**, with comments to isolate the specific code in your pull request. Some examples:\n\n* If you are making spelling corrections in the docs, don't modify other files.\n* If you are adding new functions don't '*cleanup*' unrelated functions. That cleanup belongs in another pull request.\n\n#### Squash your commits to a single commit\n\nTo keep the history of the project clean, you should make one commit per pull request.\nIf you already have multiple commits, you can add the commits together (squash them) with the following commands in Git Bash:\n\n1. Open `Git Bash` (or `Git Shell`)\n2. Enter following command to squash the recent {N} commits: `git reset --soft HEAD~{N} && git commit` (replace `{N}` with the number of commits you want to squash)\n3. Press <kbd>i</kbd> to get into Insert-mode\n4. Enter the commit message of the new commit\n5. After adding the message, press <kbd>ESC</kbd> to get out of the Insert-mode\n6. Write `:wq` and press <kbd>Enter</kbd> to save the new message or write `:q!` to discard your changes\n7. Enter `git push --force` to push the new commit to the remote repository\n\nFor example, if you want to squash the last 5 commits, use `git reset --soft HEAD~5 && git commit`\n\n### Don't mix code changes with whitespace cleanup\n\nIf you change two lines of code and correct 200 lines of whitespace issues in a file the diff on that pull request is functionally unreadable and will be **rejected**. Whitespace cleanups need to be in their own pull request.\n\n### Keep your code simple!\n\nPlease keep your code as clean and straightforward as possible.\nFurthermore, the pixel shortage is over. We want to see:\n\n* `opacity` instead of `o`\n* `placeholder` instead of `ph`\n* `myFunctionThatDoesThings()` instead of `mftdt()`\n\n### Test your changes!\n\nBefore you submit a pull request, please test your changes. Verify that Telegram Desktop still works and your changes don't cause other issue or crashes.\n\n### Write a good commit message\n\n* Explain why you make the changes. [More infos about a good commit message.][commit_message]\n\n* If you fix an issue with your commit, please close the issue by [adding one of the keywords and the issue number][closing-issues-via-commit-messages] to your commit message.\n\n  For example: `Fix #545`\n\n[//]: # (LINKS)\n[telegram]: https://telegram.org/\n[help_fork_repo]: https://help.github.com/articles/fork-a-repo/\n[help_change_commit_message]: https://help.github.com/articles/changing-a-commit-message/\n[commit_message]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html\n[pr]: https://github.com/telegramdesktop/tdesktop/compare\n[build_instructions]: https://github.com/telegramdesktop/tdesktop/blob/master/README.md#build-instructions\n[closing-issues-via-commit-messages]: https://help.github.com/articles/closing-issues-via-commit-messages/\n[translate]: https://translations.telegram.org\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/BUG_REPORT.yml",
    "content": "name: Bug report\ndescription: Report errors or unexpected behavior.\nlabels: [bug]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for reporting issues of Telegram Desktop!\n\n        To make it easier for us to help you please enter detailed information below.\n  - type: textarea\n    attributes:\n      label: Steps to reproduce\n      placeholder: |\n        1. \n        2. \n        3. \n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Expected behaviour\n      placeholder: Tell us what should happen\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Actual behaviour\n      placeholder: Tell us what happens instead\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: Operating system\n      description: >\n        Your operating system name, version and desktop environment.\n        **Don't use kernel version (uname), it's useless.**\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: Version of Telegram Desktop\n      description: >\n        Please note we don't support versions from Linux distro repositories.\n        If you need support for these versions, **please contact your distro maintainer**\n        or your distro bugtracker.\n        **Don't use 'latest'**, specify actual version, **that's a reason to close your issue**.\n    validations:\n      required: true\n  - type: dropdown\n    attributes:\n      label: Installation source\n      multiple: false\n      options:\n        - Static binary from official website\n        - Microsoft Store\n        - Mac App Store\n        - Flatpak\n        - Snap\n        - Other (unofficial) source\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: Crash ID\n      description: >\n        If you're reporting a crash, please enter the crash ID from the crash reporter\n        opening on the next launch after crash. **You have to enable beta versions\n        installation in Settings -> Advanced for the reporter to appear.**\n        You don't have to wait for a beta version to arrive.\n  - type: textarea\n    attributes:\n      label: Logs\n      description: >\n        You can find log.txt using the `viewlogs`\n        [cheat code](https://github.com/telegramdesktop/tdesktop/wiki/Cheat-Codes).\n      placeholder: Insert log.txt here (if necessary)\n      render: shell\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml",
    "content": "---\nname: Feature request\ndescription: Suggest an idea.\nlabels: [enhancement]\nbody:\n  - type: textarea\n    attributes:\n      label: Is your feature request related to a problem?\n      placeholder: A clear and concise description of what the problem is.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Describe the solution you'd like\n      placeholder: A clear and concise description of what you want to happen.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Describe alternatives you've considered\n      placeholder: A clear and concise description of any alternative solutions or features you've considered.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Additional context\n      placeholder: Add any other context or screenshots about the feature request here.\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Platform-wide issue\n    url: https://bugs.telegram.org\n    about: Any bug report or feature request affecting more than only Telegram Desktop.\n  - name: Issue of other client\n    url: https://bugs.telegram.org\n    about: Any bug report or feature request not about Telegram Desktop.\n  - name: Question\n    url: https://t.me/TelegramDesktopTalk\n    about: Ask a question.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/scripts/generate_changelog.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Convert changelog.txt to a static HTML page for GitHub Pages.\"\"\"\n\nimport re\nimport shutil\nimport sys\nimport html\nfrom pathlib import Path\n\nMONTHS = [\n    \"\", \"January\", \"February\", \"March\", \"April\", \"May\", \"June\",\n    \"July\", \"August\", \"September\", \"October\", \"November\", \"December\",\n]\n\nVERSION_RE = re.compile(\n    r\"^(\\d+\\.\\d+(?:\\.\\d+)?)\\s*\"        # version number\n    r\"(?:(alpha|beta|dev|stable)\\s*)?\"   # optional tag\n    r\"\\((\\d{2})\\.(\\d{2})\\.(\\d{2,4})\\)$\" # date (DD.MM.YY or DD.MM.YYYY)\n)\n\n\ndef parse_date(day: str, month: str, year: str) -> tuple[str, str, str]:\n    \"\"\"Return (sort_key, raw_display, full_display) from DD, MM, YY strings.\"\"\"\n    y = int(year)\n    if y < 100:\n        y += 2000\n    m = int(month)\n    d = int(day)\n    sort_key = f\"{y:04d}-{m:02d}-{d:02d}\"\n    raw_display = f\"{day}.{month}.{year}\"\n    full_display = f\"{d} {MONTHS[m]} {y}\"\n    return sort_key, raw_display, full_display\n\n\ndef parse_changelog(text: str) -> list[dict]:\n    entries = []\n    current = None\n\n    for raw_line in text.splitlines():\n        line = raw_line.rstrip()\n        m = VERSION_RE.match(line)\n        if m:\n            if current:\n                entries.append(current)\n            version, tag, day, month, year = m.groups()\n            sort_key, raw_date, full_date = parse_date(day, month, year)\n            current = {\n                \"version\": version,\n                \"tag\": tag or \"\",\n                \"date\": raw_date,\n                \"full_date\": full_date,\n                \"sort_key\": sort_key,\n                \"lines\": [],\n            }\n        elif current is not None:\n            # Skip blank lines at the start\n            if not line and not current[\"lines\"]:\n                continue\n            # Skip stray artifact lines\n            if line.strip() in (\"),\", \"),\"):\n                continue\n            current[\"lines\"].append(line)\n\n    if current:\n        entries.append(current)\n\n    # Trim trailing blank lines from each entry\n    for entry in entries:\n        while entry[\"lines\"] and not entry[\"lines\"][-1]:\n            entry[\"lines\"].pop()\n\n    return entries\n\n\ndef render_entry(entry: dict) -> str:\n    version = html.escape(entry[\"version\"])\n    tag = entry[\"tag\"]\n    date = html.escape(entry[\"date\"])\n    anchor = f\"v{version}\"\n\n    tag_html = \"\"\n    if tag and tag not in (\"stable\",):\n        tag_html = f' {html.escape(tag)}'\n\n    parts = [\n        f'<article class=\"entry\" id=\"{anchor}\">',\n        f'  <h2><a class=\"anchor\" href=\"#{anchor}\"></a>'\n        f'{version}{tag_html}'\n        f' <time>{date}</time></h2>',\n    ]\n\n    in_list = False\n    for line in entry[\"lines\"]:\n        stripped = line.lstrip()\n        if stripped.startswith(\"- \") or stripped.startswith(\"\\u2014 \"):\n            # Bullet point (- or em dash)\n            if not in_list:\n                parts.append(\"  <ul>\")\n                in_list = True\n            bullet_text = stripped[2:]\n            parts.append(f\"    <li>{html.escape(bullet_text)}</li>\")\n        else:\n            if in_list:\n                parts.append(\"  </ul>\")\n                in_list = False\n            if stripped:\n                parts.append(f\"  <p>{html.escape(stripped)}</p>\")\n\n    if in_list:\n        parts.append(\"  </ul>\")\n\n    parts.append(\"</article>\")\n    return \"\\n\".join(parts)\n\n\ndef build_html(entries: list[dict]) -> str:\n    count = len(entries)\n    first_date = entries[-1][\"full_date\"] if entries else \"\"\n    latest_version = entries[0][\"version\"] if entries else \"\"\n\n    entries_html = \"\\n\\n\".join(render_entry(e) for e in entries)\n\n    return f\"\"\"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Version history</title>\n<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"icon32.png\">\n<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"icon16.png\">\n<style>\n* {{ margin: 0; padding: 0; box-sizing: border-box; }}\nbody {{\n  font: 12px / 18px \"Lucida Grande\", \"Lucida Sans Unicode\", Arial,\n    Helvetica, Verdana, sans-serif;\n  background: #fff;\n  color: #000;\n}}\nheader {{\n  background: #1d98dc;\n  color: #fff;\n  padding: 2rem 1.5rem;\n  text-align: center;\n}}\nheader h1 {{ font-size: 18px; font-weight: 700; }}\nheader p {{ opacity: .85; margin-top: 4px; font-size: 12px; }}\n.container {{\n  max-width: 600px;\n  margin: 0 auto;\n  padding: 20px 15px;\n}}\n.search-box {{\n  position: sticky;\n  top: 0;\n  z-index: 10;\n  background: #fff;\n  padding: 8px 0 12px;\n}}\n.search-box input {{\n  width: 100%;\n  padding: 6px 10px;\n  font: 12px / 18px \"Lucida Grande\", \"Lucida Sans Unicode\", Arial,\n    Helvetica, Verdana, sans-serif;\n  border: 1px solid #ccc;\n  border-radius: 4px;\n  background: #fff;\n  color: #000;\n  outline: none;\n}}\n.search-box input:focus {{ border-color: #1d98dc; }}\n.entry {{\n  padding: 14px 0 4px;\n  scroll-margin-top: 48px;\n}}\n.entry h2 {{\n  font-size: 16px;\n  font-weight: 700;\n  line-height: 22px;\n  margin-bottom: 6px;\n  position: relative;\n}}\n.entry h2 .anchor {{\n  position: absolute;\n  left: -24px;\n  top: 0;\n  width: 24px;\n  height: 22px;\n  display: block;\n  opacity: 0;\n  transition: opacity .15s;\n  background: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 16 16'%3E%3Cpath fill='%23168acd' d='M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 0 1 0-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 0 1-2.83 0z'/%3E%3C/svg%3E\") 0 center / 18px no-repeat;\n  cursor: pointer;\n}}\n.entry h2:hover .anchor {{ opacity: .6; }}\n.entry h2 .anchor:hover {{ opacity: 1; }}\n.entry h2 time {{\n  font-size: 12px;\n  font-weight: 400;\n  color: #999;\n  margin-left: 6px;\n}}\n.entry ul {{\n  margin: 0 0 4px 8px;\n  padding: 0;\n  list-style: none;\n}}\n.entry li {{\n  padding: 2px 0 2px 16px;\n  position: relative;\n  color: #333;\n}}\n.entry li::before {{\n  content: \"\";\n  position: absolute;\n  left: 0;\n  top: 9px;\n  width: 6px;\n  height: 6px;\n  border-radius: 50%;\n  background: #009be1;\n}}\n.entry p {{\n  margin: 4px 0;\n  color: #555;\n  font-style: italic;\n}}\n.hidden {{ display: none; }}\nfooter {{\n  text-align: center;\n  padding: 24px 15px;\n  font-size: 11px;\n  color: #999;\n}}\nfooter a {{ color: #168acd; text-decoration: none; }}\nfooter a:hover {{ text-decoration: underline; }}\n</style>\n</head>\n<body>\n\n<header>\n  <h1>Version history</h1>\n  <p>{count} releases since {first_date} &middot; latest: {latest_version}</p>\n</header>\n\n<div class=\"container\">\n  <div class=\"search-box\">\n    <input type=\"text\" id=\"search\" placeholder=\"Search versions and changes\\u2026\"\n           autocomplete=\"off\" spellcheck=\"false\">\n  </div>\n\n  <div id=\"entries\">\n{entries_html}\n  </div>\n</div>\n\n<footer>\n  Auto-generated from\n  <a href=\"https://github.com/telegramdesktop/tdesktop/blob/dev/changelog.txt\">changelog.txt</a>.\n  Source code is published under\n  <a href=\"https://github.com/telegramdesktop/tdesktop\">GPL v3</a>.\n</footer>\n\n<script>\n(function() {{\n  var input = document.getElementById('search');\n  var entries = document.querySelectorAll('.entry');\n  var timer;\n  input.addEventListener('input', function() {{\n    clearTimeout(timer);\n    timer = setTimeout(function() {{\n      var q = input.value.toLowerCase().trim();\n      entries.forEach(function(el) {{\n        if (!q) {{\n          el.classList.remove('hidden');\n        }} else {{\n          el.classList.toggle('hidden', el.textContent.toLowerCase().indexOf(q) === -1);\n        }}\n      }});\n    }}, 150);\n  }});\n\n  // Anchor links: copy URL on click\n  document.addEventListener('click', function(e) {{\n    var anchor = e.target.closest('.anchor');\n    if (!anchor) return;\n    e.preventDefault();\n    var url = location.origin + location.pathname + anchor.getAttribute('href');\n    history.replaceState(null, '', anchor.getAttribute('href'));\n    if (navigator.clipboard) {{\n      navigator.clipboard.writeText(url);\n    }}\n  }});\n}})();\n</script>\n\n</body>\n</html>\"\"\"\n\n\ndef main():\n    repo = Path(__file__).resolve().parent.parent.parent\n    src = repo / \"changelog.txt\"\n    if len(sys.argv) > 1:\n        src = Path(sys.argv[1])\n\n    out = repo / \"docs\" / \"changelog\" / \"index.html\"\n    if len(sys.argv) > 2:\n        out = Path(sys.argv[2])\n\n    text = src.read_text(encoding=\"utf-8\")\n    entries = parse_changelog(text)\n    html_content = build_html(entries)\n\n    out.parent.mkdir(parents=True, exist_ok=True)\n    out.write_text(html_content, encoding=\"utf-8\")\n\n    # Copy favicon files from resources\n    icons_src = repo / \"Telegram\" / \"Resources\" / \"art\"\n    for name in (\"icon16.png\", \"icon32.png\"):\n        icon = icons_src / name\n        if icon.exists():\n            shutil.copy2(icon, out.parent / name)\n\n    print(f\"Generated {out} ({len(entries)} entries, {out.stat().st_size:,} bytes)\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": ".github/workflows/changelog.yml",
    "content": "name: Changelog\n\non:\n  push:\n    branches: [dev]\n    paths: [changelog.txt]\n  release:\n    types: [published]\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\nconcurrency:\n  group: pages\n  cancel-in-progress: true\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: actions/setup-python@v6\n        with:\n          python-version: \"3.12\"\n\n      - name: Generate HTML\n        run: python .github/scripts/generate_changelog.py\n\n      - name: Upload pages artifact\n        uses: actions/upload-pages-artifact@v4\n        with:\n          path: docs\n\n  deploy:\n    needs: build\n    runs-on: ubuntu-latest\n    environment:\n      name: github-pages\n      url: ${{ steps.deploy.outputs.page_url }}\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deploy\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "\nname: Docker.\n\non:\n  push:\n    paths:\n      - '.github/workflows/docker.yml'\n      - 'Telegram/build/docker/centos_env/**'\n\njobs:\n  docker:\n    name: Ubuntu\n    runs-on: ubuntu-latest\n    if: github.ref_name == github.event.repository.default_branch\n\n    env:\n      IMAGE_TAG: ghcr.io/${{ github.repository }}/centos_env:latest\n\n    steps:\n      - name: Clone.\n        uses: actions/checkout@v6\n        with:\n          submodules: recursive\n\n      - name: First set up.\n        run: |\n          sudo apt update\n          curl -sSL https://install.python-poetry.org | python3 -\n          echo \"${{ secrets.GITHUB_TOKEN }}\" | docker login ghcr.io -u $ --password-stdin\n\n      - name: Free up some disk space.\n        uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be\n        with:\n          tool-cache: true\n\n      - name: Docker image build.\n        run: |\n          cd Telegram/build/docker/centos_env\n          poetry install\n          DEBUG= LTO= poetry run gen_dockerfile | DOCKER_BUILDKIT=1 docker build -t $IMAGE_TAG -\n\n      - name: Push the Docker image.\n        run: docker push $IMAGE_TAG\n"
  },
  {
    "path": ".github/workflows/full_source.yml",
    "content": "name: Upload full source to release.\n\non:\n  release:\n    types: released\n  workflow_dispatch:\n    inputs:\n      release_version:\n        description: 'The version of the release to upload'\n        required: true\n        default: 'v1.0.0'\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Clone.\n      uses: actions/checkout@v2\n      with:\n        submodules: recursive\n    - name: Install prerequisites.\n      run: |\n        pip install git-archive-all\n\n        if [ \"${{ github.event_name }}\" == \"workflow_dispatch\" ]; then\n          # Use the release version from the input if available.\n          p=\"frk-${{ github.event.inputs.release_version }}-full.tar.gz\"\n        else\n          # Otherwise fallback to the release event.\n          p=\"frk-v${GITHUB_REF##*/v}-full.tar.gz\"\n        fi\n        echo \"TAG=$p\" >> $GITHUB_ENV\n    - name: Build assets.\n      run: |\n        mkdir -p dist/\n        echo \"$TAG\"\n        ~/.local/bin/git-archive-all ./dist/${TAG}\n    - name: Get release ID by tag.\n      id: get_release\n      if: github.event_name == 'workflow_dispatch'\n      uses: cardinalby/git-get-release-action@master\n      env:\n        GITHUB_TOKEN: ${{ github.token }}\n      with:\n        tag: ${{ github.event.inputs.release_version }}\n    - name: Set upload URL for release event.\n      id: set_upload_url\n      if: github.event_name == 'release'\n      run: |\n        echo \"upload_url=${{ github.event.release.upload_url }}\" >> $GITHUB_OUTPUT\n    - name: Upload.\n      uses: lovasoa/upload-release-asset@master\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      with:\n        upload_url: ${{ steps.get_release.outputs.upload_url || steps.set_upload_url.outputs.upload_url }}\n        asset_path: ./dist/${{ env.TAG }}\n        asset_label: Source code (tar.gz, full)\n        asset_name: ${{ env.TAG }}\n        asset_content_type: application/x-gzip\n"
  },
  {
    "path": ".github/workflows/inno.yml",
    "content": "name: Create Inno Setup Installers\n\non:\n  release:\n    types: [released]\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version number (e.g., 1.2.3)'\n        required: true\n\njobs:\n  create-installers:\n    runs-on: windows-latest\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v4\n\n    - name: Get version\n      id: version\n      run: |\n        # For workflow_dispatch\n        if (-not [string]::IsNullOrEmpty(\"${{ github.event.inputs.version }}\")) {\n          echo \"version=${{ github.event.inputs.version }}\" >> $env:GITHUB_OUTPUT\n          echo \"version_full=${{ github.event.inputs.version }}\" >> $env:GITHUB_OUTPUT\n        } else {\n          # For release event\n          echo \"version=$($env:GITHUB_REF -replace 'refs/tags/', '')\" >> $env:GITHUB_OUTPUT\n          echo \"version_full=$($env:GITHUB_REF -replace 'refs/tags/', '')\" >> $env:GITHUB_OUTPUT\n        }\n      shell: pwsh\n\n    - name: Download x64 zip\n      uses: robinraju/release-downloader@v1.12\n      with:\n        repository: ${{ github.repository }}\n        tag: ${{ steps.version.outputs.version_full }}\n        fileName: Telegram.zip\n        out-file-path: ./artifacts/x64/\n\n    - name: Download x86 zip\n      uses: robinraju/release-downloader@v1.12\n      with:\n        repository: ${{ github.repository }}\n        tag: ${{ steps.version.outputs.version_full }}\n        fileName: Telegram_x86.zip\n        out-file-path: ./artifacts/x86/\n\n    - name: Extract x64 zip\n      run: |\n        Expand-Archive -Path ./artifacts/x64/Telegram.zip -DestinationPath ./artifacts/x64/extracted\n\n    - name: Extract x86 zip\n      run: |\n        Expand-Archive -Path ./artifacts/x86/Telegram_x86.zip -DestinationPath ./artifacts/x86/extracted\n\n    - name: Install Inno Setup\n      run: |\n        choco install innosetup -y\n\n    - name: Prepare directories\n      run: |\n        New-Item -ItemType Directory -Path ./Telegram/build/release_x64 -Force\n        New-Item -ItemType Directory -Path ./Telegram/build/release_x86 -Force\n        Copy-Item -Path ./artifacts/x64/extracted/* -Destination ./Telegram/build/release_x64 -Recurse -Force\n        Copy-Item -Path ./artifacts/x86/extracted/* -Destination ./Telegram/build/release_x86 -Recurse -Force\n\n    - name: Get release ID by tag.\n      id: get_release\n      if: github.event_name == 'workflow_dispatch'\n      uses: cardinalby/git-get-release-action@master\n      env:\n        GITHUB_TOKEN: ${{ github.token }}\n      with:\n        tag: ${{ github.event.inputs.version }}\n    - name: Set upload URL for release event.\n      id: set_upload_url\n      if: github.event_name == 'release'\n      run: |\n        echo \"upload_url=${{ github.event.release.upload_url }}\" >> $env:GITHUB_OUTPUT\n\n    - name: Create x64 installer\n      id: create_x64\n      run: |\n        $cleanVersion = \"${{ steps.version.outputs.version }}\".TrimStart('v')\n        echo \"Version: $cleanVersion.\"\n        iscc ./Telegram/build/setup.iss `\n          /dMyAppVersion=$cleanVersion `\n          /dMyAppVersionZero=$cleanVersion `\n          /dMyAppVersionFull=$cleanVersion `\n          /dMyBuildTarget=\"win64\" `\n          /dReleasePath=\"../../artifacts/x64/extracted\" `\n          /dSourcePath=\"../SourceFiles/\"\n        \n        # Find the output exe file\n        $exePath = Get-ChildItem -Path \".\" -Filter \"tsetup-x64.$cleanVersion.exe\" -Recurse | Select-Object -First 1 -ExpandProperty FullName\n        \n        if ($exePath) {\n          echo \"$exePath\"\n          echo \"output_file=$exePath\" >> $env:GITHUB_OUTPUT\n        } else {\n          Write-Error \"Output exe file not found\"\n          exit 1\n        }\n\n    - name: Create x86 installer\n      id: create_x86\n      run: |\n        $cleanVersion = \"${{ steps.version.outputs.version }}\".TrimStart('v')\n        echo \"Version: $cleanVersion.\"\n        iscc ./Telegram/build/setup.iss `\n          /dMyAppVersion=$cleanVersion `\n          /dMyAppVersionZero=$cleanVersion `\n          /dMyAppVersionFull=$cleanVersion `\n          /dMyBuildTarget=\"win32\" `\n          /dReleasePath=\"../../artifacts/x86/extracted\" `\n          /dSourcePath=\"../SourceFiles/\"\n\n        # Find the output exe file\n        $exePath = Get-ChildItem -Path \".\" -Filter \"tsetup.$cleanVersion.exe\" -Recurse | Select-Object -First 1 -ExpandProperty FullName\n        \n        if ($exePath) {\n          echo \"$exePath\"\n          echo \"output_file=$exePath\" >> $env:GITHUB_OUTPUT\n        } else {\n          Write-Error \"Output exe file not found\"\n          exit 1\n        }\n\n    - name: Upload x64 installer to release\n      uses: lovasoa/upload-release-asset@master\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      with:\n        upload_url: ${{ steps.get_release.outputs.upload_url || steps.set_upload_url.outputs.upload_url }}\n        asset_path: ${{ steps.create_x64.outputs.output_file }}\n        asset_name: Telegram Windows x64 Installer.exe\n        asset_content_type: application/octet-stream\n\n    - name: Upload x86 installer to release\n      uses: lovasoa/upload-release-asset@master\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      with:\n        upload_url: ${{ steps.get_release.outputs.upload_url || steps.set_upload_url.outputs.upload_url }}\n        asset_path: ${{ steps.create_x86.outputs.output_file }}\n        asset_name: Telegram Windows x32 Installer.exe\n        asset_content_type: application/octet-stream\n"
  },
  {
    "path": ".github/workflows/jekyll-gh-pages.yml",
    "content": "# Sample workflow for building and deploying a Jekyll site to GitHub Pages\nname: Deploy Jekyll with GitHub Pages dependencies preinstalled\n\non:\n  # Runs on pushes targeting the default branch\n  push:\n    branches: [\"dev\"]\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\n# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\n# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.\n# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.\nconcurrency:\n  group: \"pages\"\n  cancel-in-progress: false\n\njobs:\n  # Build job\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: Setup Pages\n        uses: actions/configure-pages@v5\n      - name: Build with Jekyll\n        uses: actions/jekyll-build-pages@v1\n        with:\n          source: ./\n          destination: ./_site\n          jekyll_config: _config_github_pages.yml\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v3\n\n  # Deployment job\n  deploy:\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".github/workflows/linux.yml",
    "content": "name: Linux.\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version label (e.g., v5.15.3)'\n        required: false\n        type: string\n  release:\n    types: [published]\n  repository_dispatch:\n    types: [\"Restart linux workflow.\"]\n  push:\n    paths-ignore:\n      - 'docs/**'\n      - '**.md'\n      - 'changelog.txt'\n      - 'LEGAL'\n      - 'LICENSE'\n      - '.github/**'\n      - '!.github/workflows/linux.yml'\n      - 'snap/**'\n      - 'Telegram/build/**'\n      - '!Telegram/build/docker/centos_env/**'\n      - 'Telegram/Resources/uwp/**'\n      - 'Telegram/Resources/winrc/**'\n      - 'Telegram/SourceFiles/platform/win/**'\n      - 'Telegram/SourceFiles/platform/mac/**'\n      - 'Telegram/Telegram/**'\n      - 'Telegram/configure.bat'\n      - 'Telegram/Telegram.plist'\n  pull_request:\n    paths-ignore:\n      - 'docs/**'\n      - '**.md'\n      - 'changelog.txt'\n      - 'LEGAL'\n      - 'LICENSE'\n      - '.github/**'\n      - '!.github/workflows/linux.yml'\n      - 'snap/**'\n      - 'Telegram/build/**'\n      - '!Telegram/build/docker/centos_env/**'\n      - 'Telegram/Resources/uwp/**'\n      - 'Telegram/Resources/winrc/**'\n      - 'Telegram/SourceFiles/platform/win/**'\n      - 'Telegram/SourceFiles/platform/mac/**'\n      - 'Telegram/Telegram/**'\n      - 'Telegram/configure.bat'\n      - 'Telegram/Telegram.plist'\n\njobs:\n\n  linux:\n    name: Rocky Linux 8\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        defines:\n          - \"\"\n\n    env:\n      UPLOAD_ARTIFACT: \"true\"\n      ONLY_CACHE: \"false\"\n      IMAGE_TAG: tdesktop:centos_env\n\n    steps:\n      - name: Validate upload secret parts.\n        env:\n          UPDATER_UPLOAD_BASE64: ${{ secrets.UPDATER_UPLOAD_BASE64 }}\n          UPDATER_UPLOAD_BASE64_PART1: ${{ secrets.UPDATER_UPLOAD_BASE64_PART1 }}\n          UPDATER_UPLOAD_BASE64_PART2: ${{ secrets.UPDATER_UPLOAD_BASE64_PART2 }}\n          UPDATER_UPLOAD_BASE64_PART3: ${{ secrets.UPDATER_UPLOAD_BASE64_PART3 }}\n        run: |\n          echo \"UPDATER_UPLOAD_BASE64 length: ${#UPDATER_UPLOAD_BASE64}\"\n          echo \"UPDATER_UPLOAD_BASE64_PART1 length: ${#UPDATER_UPLOAD_BASE64_PART1}\"\n          echo \"UPDATER_UPLOAD_BASE64_PART2 length: ${#UPDATER_UPLOAD_BASE64_PART2}\"\n          echo \"UPDATER_UPLOAD_BASE64_PART3 length: ${#UPDATER_UPLOAD_BASE64_PART3}\"\n          MERGED_UPLOAD_BASE64=\"${UPDATER_UPLOAD_BASE64_PART1}${UPDATER_UPLOAD_BASE64_PART2}${UPDATER_UPLOAD_BASE64_PART3}\"\n          if [ -z \"${MERGED_UPLOAD_BASE64}\" ]; then\n            MERGED_UPLOAD_BASE64=\"${UPDATER_UPLOAD_BASE64}\"\n          fi\n          echo \"Merged parts length: ${#MERGED_UPLOAD_BASE64}\"\n          if [ -z \"${MERGED_UPLOAD_BASE64}\" ]; then\n            echo \"No upload base64 payload provided.\"\n            exit 0\n          fi\n\n          if ! printf \"%s\" \"${MERGED_UPLOAD_BASE64}\" | base64 --decode > /tmp/updater_upload_check.zip 2>/tmp/updater_upload_check.err; then\n            echo \"Base64 decode failed.\"\n            cat /tmp/updater_upload_check.err\n            exit 1\n          fi\n\n          if [ ! -s /tmp/updater_upload_check.zip ]; then\n            echo \"Decoded payload is empty.\"\n            exit 1\n          fi\n\n          echo \"Decoded file info: $(file -b /tmp/updater_upload_check.zip)\"\n\n      - name: Clone.\n        uses: actions/checkout@v6\n        with:\n          submodules: recursive\n\n      - name: First set up.\n        run: |\n          sudo apt update\n          curl -sSL https://install.python-poetry.org | python3 -\n          cd Telegram/build/docker/centos_env\n          poetry install\n          DOCKERFILE=$(DEBUG= LTO= poetry run gen_dockerfile)\n          echo \"$DOCKERFILE\" > Dockerfile\n          rm -rf __pycache__\n\n          sudo apt -y install wget 7zip unzip\n\n          cd $GITHUB_WORKSPACE/..\n\n          rm -rf DesktopPrivate\n          mkdir -p DesktopPrivate\n          if [ -n \"${{ secrets.DESKTOP_PRIVATE_BASE64 }}\" ]; then\n            echo \"${{ secrets.DESKTOP_PRIVATE_BASE64 }}\" | base64 --decode > desktop_private.zip\n            unzip -oq desktop_private.zip -d DesktopPrivate\n          else\n            cd DesktopPrivate\n            echo \"#pragma once\" > alpha_private.h\n            echo \"#pragma once\" > packer_private.h\n            echo '${{ secrets.ALPHA_PRIVATE }}' >> alpha_private.h\n            echo '${{ secrets.PACKER_PRIVATE }}' >> packer_private.h\n            cd ..\n          fi\n\n          wget ${{ secrets.PACK_FILE }} -O p.7z\n          7za x p.7z -p${{ secrets.PACK_FILE_PASSWORD }}\n\n      - name: Free up some disk space.\n        uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be\n        with:\n          tool-cache: true\n\n      - name: Set up Docker Buildx.\n        uses: docker/setup-buildx-action@v4\n\n      - name: Libraries cache.\n        uses: actions/cache@v5\n        with:\n          path: ${{ runner.temp }}/.buildx-cache\n          key: ${{ runner.OS }}-libs-${{ hashFiles('Telegram/build/docker/centos_env/**') }}\n          restore-keys: ${{ runner.OS }}-libs-\n\n      - name: Libraries.\n        uses: docker/build-push-action@v7\n        with:\n          context: Telegram/build/docker/centos_env\n          load: ${{ env.ONLY_CACHE == 'false' }}\n          tags: ${{ env.IMAGE_TAG }}\n          cache-from: type=local,src=${{ runner.temp }}/.buildx-cache\n          cache-to: type=local,dest=${{ runner.temp }}/.buildx-cache-new,mode=max\n\n      - name: Move cache.\n        run: |\n          rm -rf ${{ runner.temp }}/.buildx-cache\n          mv ${{ runner.temp }}/.buildx-cache{-new,}\n\n      - name: Telegram Desktop build.\n        if: env.ONLY_CACHE == 'false'\n        run: |\n          DEFINE=\"\"\n          if [ -n \"${{ matrix.defines }}\" ]; then\n            DEFINE=\"-D ${{ matrix.defines }}=ON\"\n            echo Define from matrix: $DEFINE\n            echo \"ARTIFACT_NAME=Telegram ${{ matrix.defines }}\" >> $GITHUB_ENV\n          else\n            echo \"ARTIFACT_NAME=Telegram\" >> $GITHUB_ENV\n          fi\n\n          docker run --rm \\\n          -u $(id -u) \\\n          -v $PWD:/usr/src/tdesktop \\\n          -v $PWD/../DesktopPrivate:/usr/src/DesktopPrivate \\\n          $IMAGE_TAG \\\n          /usr/src/tdesktop/Telegram/build/docker/centos_env/build.sh \\\n          -D TDESKTOP_API_ID=${{ secrets.API_ID }} \\\n          -D TDESKTOP_API_HASH=${{ secrets.API_HASH }} \\\n          -D DESKTOP_APP_SPECIAL_TARGET=linux \\\n          $DEFINE\n\n          strip -s out/MinSizeRel/Forkgram\n\n      - name: Check.\n        if: env.ONLY_CACHE == 'false'\n        run: |\n          filePath=\"out/MinSizeRel/Forkgram\"\n          if test -f \"$filePath\"; then\n            echo \"Build successfully done! :)\"\n\n            size=$(stat -c %s \"$filePath\")\n            echo \"File size of ${filePath}: ${size} Bytes.\"\n          else\n            echo \"Build error, output file does not exist.\"\n            exit 1\n          fi\n\n      - name: Compress.\n        if: (github.event_name == 'release')\n        run: tar -cJvf Forkgram.tar.xz -C out/MinSizeRel Forkgram Updater\n\n      - name: Pack updater files.\n        if: (github.event_name == 'release') || (github.event_name == 'workflow_dispatch') || (github.event_name == 'repository_dispatch')\n        run: |\n          verPacker=$(awk '$1 == \"AppVersion\" { print $2 }' Telegram/build/version)\n          cd out/MinSizeRel\n          BEFORE=$(mktemp)\n          AFTER=$(mktemp)\n          ls -1 tlinuxupd* 2>/dev/null | sort > \"$BEFORE\" || true\n          ./Packer -path Forkgram -path Updater -version \"$verPacker\"\n          ls -1 tlinuxupd* 2>/dev/null | sort > \"$AFTER\" || true\n          UPDATE_FILE=$(comm -13 \"$BEFORE\" \"$AFTER\" | tail -n 1)\n          if [ -z \"$UPDATE_FILE\" ]; then\n            UPDATE_FILE=$(ls -1t tlinuxupd* | head -n 1)\n          fi\n          if [ -z \"$UPDATE_FILE\" ] || [ ! -f \"$UPDATE_FILE\" ]; then\n            echo \"Packed update file was not generated.\"\n            exit 1\n          fi\n          echo \"PACKED_UPDATE_FILE=$GITHUB_WORKSPACE/out/MinSizeRel/$UPDATE_FILE\" >> $GITHUB_ENV\n          echo \"PACKED_UPDATE_BASENAME=$UPDATE_FILE\" >> $GITHUB_ENV\n          echo \"VER_PACKER=$verPacker\" >> $GITHUB_ENV\n\n      - name: Upload packed updater to Telegram channel.\n        if: (github.event_name == 'release') || (github.event_name == 'workflow_dispatch') || (github.event_name == 'repository_dispatch')\n        env:\n          UPDATER_UPLOAD_BASE64: ${{ secrets.UPDATER_UPLOAD_BASE64 }}\n          UPDATER_UPLOAD_BASE64_PART1: ${{ secrets.UPDATER_UPLOAD_BASE64_PART1 }}\n          UPDATER_UPLOAD_BASE64_PART2: ${{ secrets.UPDATER_UPLOAD_BASE64_PART2 }}\n          UPDATER_UPLOAD_BASE64_PART3: ${{ secrets.UPDATER_UPLOAD_BASE64_PART3 }}\n        run: |\n          MERGED_UPLOAD_BASE64=\"${UPDATER_UPLOAD_BASE64_PART1}${UPDATER_UPLOAD_BASE64_PART2}${UPDATER_UPLOAD_BASE64_PART3}\"\n          if [ -z \"${MERGED_UPLOAD_BASE64}\" ]; then\n            MERGED_UPLOAD_BASE64=\"${UPDATER_UPLOAD_BASE64}\"\n          fi\n\n          if [ -z \"${MERGED_UPLOAD_BASE64}\" ]; then\n            echo \"UPDATER_UPLOAD_BASE64(_PART1/_PART2/_PART3) is empty, skip channel upload.\"\n            exit 0\n          fi\n\n          python3 -m pip install --upgrade pip\n          python3 -m pip install requests tgcrypto-pyrofork Kurigram\n\n          rm -rf updater_channel_upload\n          mkdir -p updater_channel_upload\n\n          printf \"%s\" \"${MERGED_UPLOAD_BASE64}\" | base64 --decode > updater_upload.raw\n          if unzip -tqq updater_upload.raw >/dev/null 2>&1; then\n            unzip -oq updater_upload.raw -d updater_channel_upload\n          elif tar -tzf updater_upload.raw >/dev/null 2>&1; then\n            tar -xzf updater_upload.raw -C updater_channel_upload\n          elif tar -tf updater_upload.raw >/dev/null 2>&1; then\n            tar -xf updater_upload.raw -C updater_channel_upload\n          else\n            echo \"Unsupported upload bundle format: $(file -b updater_upload.raw)\"\n            exit 1\n          fi\n\n          (\n            cd updater_channel_upload\n            7za a -tzip -mx=0 ../updater_upload.zip . >/dev/null\n          )\n\n          SCRIPT_PATH=$(find updater_channel_upload -type f -name linux_pack_upl.py | head -n 1)\n          if [ -z \"$SCRIPT_PATH\" ]; then\n            echo \"linux_pack_upl.py not found in UPDATER_UPLOAD_BASE64 bundle.\"\n            exit 1\n          fi\n\n          SCRIPT_DIR=$(dirname \"$SCRIPT_PATH\")\n          cp \"$PACKED_UPDATE_FILE\" \"$SCRIPT_DIR/\"\n          cp updater_upload.zip \"$SCRIPT_DIR/\"\n\n          cd \"$SCRIPT_DIR\"\n          if [ -f requirements.txt ]; then\n            python3 -m pip install -r requirements.txt\n          fi\n          python3 linux_pack_upl.py \"tlinuxupd${VER_PACKER}\"\n\n      - name: Upload.\n        if: (github.event_name == 'release')\n        uses: svenstaro/upload-release-action@2.11.4\n        with:\n          repo_token: ${{ secrets.GITHUB_TOKEN }}\n          file: ./Forkgram.tar.xz\n          tag: ${{ github.event.release.tag_name }}\n          asset_name: Forkgram.tar.xz\n"
  },
  {
    "path": ".github/workflows/mac.yml",
    "content": "name: MacOS.\n\non:\n  push:\n    paths-ignore:\n      - 'docs/**'\n      - '**.md'\n      - 'changelog.txt'\n      - 'LEGAL'\n      - 'LICENSE'\n      - '.github/**'\n      - '!.github/workflows/mac.yml'\n      - 'lib/xdg/**'\n      - 'snap/**'\n      - 'Telegram/build/docker/**'\n      - 'Telegram/Resources/uwp/**'\n      - 'Telegram/Resources/winrc/**'\n      - 'Telegram/SourceFiles/platform/win/**'\n      - 'Telegram/SourceFiles/platform/linux/**'\n      - 'Telegram/configure.bat'\n  pull_request:\n    paths-ignore:\n      - 'docs/**'\n      - '**.md'\n      - 'changelog.txt'\n      - 'LEGAL'\n      - 'LICENSE'\n      - '.github/**'\n      - '!.github/workflows/mac.yml'\n      - 'lib/xdg/**'\n      - 'snap/**'\n      - 'Telegram/build/docker/**'\n      - 'Telegram/Resources/uwp/**'\n      - 'Telegram/Resources/winrc/**'\n      - 'Telegram/SourceFiles/platform/win/**'\n      - 'Telegram/SourceFiles/platform/linux/**'\n      - 'Telegram/configure.bat'\n\njobs:\n\n  macos:\n    name: MacOS\n    runs-on: macos-latest\n\n    strategy:\n      matrix:\n        defines:\n          - \"\"\n    env:\n      UPLOAD_ARTIFACT: \"true\"\n      ONLY_CACHE: \"false\"\n      PREPARE_PATH: \"Telegram/build/prepare/prepare.py\"\n\n    steps:\n      - name: Get repository name.\n        run: echo \"REPO_NAME=${GITHUB_REPOSITORY##*/}\" >> $GITHUB_ENV\n\n      - name: Clone.\n        uses: actions/checkout@v6\n        with:\n          submodules: recursive\n          path: ${{ env.REPO_NAME }}\n\n      - name: First set up.\n        run: |\n          sudo chown -R `whoami`:admin /usr/local/share\n\n          brew update\n          brew upgrade || true\n          brew install automake meson nasm ninja pkg-config\n\n          # Disable spotlight.\n          sudo mdutil -a -i off\n\n          sudo xcode-select -s /Applications/Xcode.app/Contents/Developer\n\n          sudo sed -i '' '/CMAKE_${lang}_FLAGS_DEBUG_INIT/s/ -g//' /opt/homebrew/share/cmake/Modules/Compiler/GNU.cmake\n\n      - name: Libraries cache.\n        id: cache-libs\n        uses: actions/cache@v5\n        with:\n          path: |\n            Libraries\n            ThirdParty\n          key: ${{ runner.OS }}-libs-${{ hashFiles(format('{0}/{1}', env.REPO_NAME, env.PREPARE_PATH)) }}\n          restore-keys: ${{ runner.OS }}-libs-\n\n      - name: Libraries.\n        run: |\n          ./$REPO_NAME/Telegram/build/prepare/mac.sh skip-release silent\n\n      - name: Free up some disk space.\n        run: find Libraries '(' '(' ! '(' -name '*.a' -o -name '*.h' -o -name '*.hpp' -o -name '*.inc' -o -name '*.cmake' -o -path '*/include/*' -o -path '*/objects-*' -o -path '*/cache_keys/*' -o -path '*/patches/*' -o -perm +111 ')' -type f ')' -o -empty ')' -delete\n\n      - name: Telegram Desktop build.\n        if: env.ONLY_CACHE == 'false'\n        run: |\n          cd $REPO_NAME/Telegram\n\n          DEFINE=\"\"\n          if [ -n \"${{ matrix.defines }}\" ]; then\n            DEFINE=\"-D ${{ matrix.defines }}=ON\"\n            echo Define from matrix: $DEFINE\n            echo \"ARTIFACT_NAME=Telegram ${{ matrix.defines }}\" >> $GITHUB_ENV\n          else\n            echo \"ARTIFACT_NAME=Telegram\" >> $GITHUB_ENV\n          fi\n\n          ./configure.sh \\\n          -D CMAKE_CONFIGURATION_TYPES=Debug \\\n          -D CMAKE_COMPILE_WARNING_AS_ERROR=ON \\\n          -D CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=NO \\\n          -D TDESKTOP_API_TEST=ON \\\n          -D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF \\\n          -D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF \\\n          $DEFINE\n\n          cmake --build ../out --config Debug --parallel\n\n      - name: Move artifact.\n        if: env.UPLOAD_ARTIFACT == 'true'\n        run: |\n          cd $REPO_NAME/out/Debug\n          mkdir artifact\n          mv Telegram.app artifact/\n          mv Updater artifact/\n      - uses: actions/upload-artifact@v7\n        if: env.UPLOAD_ARTIFACT == 'true'\n        name: Upload artifact.\n        with:\n          name: ${{ env.ARTIFACT_NAME }}\n          path: ${{ env.REPO_NAME }}/out/Debug/artifact/\n"
  },
  {
    "path": ".github/workflows/mac_packaged.yml",
    "content": "name: MacOS Packaged.\n\non:\n  push:\n    paths-ignore:\n      - 'docs/**'\n      - '**.md'\n      - 'changelog.txt'\n      - 'LEGAL'\n      - 'LICENSE'\n      - '.github/**'\n      - '!.github/workflows/mac_packaged.yml'\n      - 'lib/xdg/**'\n      - 'snap/**'\n      - 'Telegram/build/**'\n      - 'Telegram/Resources/uwp/**'\n      - 'Telegram/Resources/winrc/**'\n      - 'Telegram/SourceFiles/platform/win/**'\n      - 'Telegram/SourceFiles/platform/linux/**'\n      - 'Telegram/configure.bat'\n  pull_request:\n    paths-ignore:\n      - 'docs/**'\n      - '**.md'\n      - 'changelog.txt'\n      - 'LEGAL'\n      - 'LICENSE'\n      - '.github/**'\n      - '!.github/workflows/mac_packaged.yml'\n      - 'lib/xdg/**'\n      - 'snap/**'\n      - 'Telegram/build/**'\n      - 'Telegram/Resources/uwp/**'\n      - 'Telegram/Resources/winrc/**'\n      - 'Telegram/SourceFiles/platform/win/**'\n      - 'Telegram/SourceFiles/platform/linux/**'\n      - 'Telegram/configure.bat'\n\njobs:\n\n  macos:\n    name: MacOS\n    runs-on: macos-latest\n\n    strategy:\n      matrix:\n        defines:\n          - \"\"\n\n    env:\n      GIT: \"https://github.com\"\n      CMAKE_GENERATOR: \"Ninja\"\n      CMAKE_BUILD_TYPE: \"Debug\"\n      CMAKE_BUILD_PARALLEL_LEVEL: \"\"\n      CMAKE_PREFIX_PATH: \"/opt/homebrew/opt/ffmpeg@6:/opt/homebrew/opt/openal-soft\"\n      TDE2E: \"51743dfd01dff6179e2d8f7095729caa4e2222e9\"\n      UPLOAD_ARTIFACT: \"true\"\n      ONLY_CACHE: \"false\"\n      MANUAL_CACHING: \"1\"\n      AUTO_CACHING: \"1\"\n\n    steps:\n      - name: Get repository name.\n        run: echo \"REPO_NAME=${GITHUB_REPOSITORY##*/}\" >> $GITHUB_ENV\n\n      - name: Clone.\n        uses: actions/checkout@v6\n        with:\n          submodules: recursive\n          path: ${{ env.REPO_NAME }}\n\n      - name: First set up.\n        run: |\n          brew update\n          brew upgrade || true\n          brew install ada-url autoconf automake boost cmake ffmpeg@6 jpeg-xl libavif libheif libtool openal-soft openh264 openssl opus ninja pkg-config python qtbase qtimageformats qtsvg xz || true\n          sudo xcode-select -s /Applications/Xcode.app/Contents/Developer\n          sudo sed -i '' '/CMAKE_${lang}_FLAGS_DEBUG_INIT/s/ -g//' /opt/homebrew/share/cmake/Modules/Compiler/GNU.cmake\n\n          xcodebuild -version > CACHE_KEY.txt\n          brew list --versions >> CACHE_KEY.txt\n          echo $MANUAL_CACHING >> CACHE_KEY.txt\n          echo \"$GITHUB_WORKSPACE\" >> CACHE_KEY.txt\n          if [ \"$AUTO_CACHING\" = \"1\" ]; then\n            thisFile=$REPO_NAME/.github/workflows/mac_packaged.yml\n            echo `md5 -q $thisFile` >> CACHE_KEY.txt\n          fi\n          echo \"CACHE_KEY=`md5 -q CACHE_KEY.txt`\" >> $GITHUB_ENV\n\n          echo \"MACOSX_DEPLOYMENT_TARGET=$(grep 'set(QT_SUPPORTED_MIN_MACOS_VERSION' /opt/homebrew/Cellar/qtbase/*/lib/cmake/Qt6/Qt6ConfigExtras.cmake | sed -E 's/^.*\"(.*)\"\\)$/\\1/')\" >> $GITHUB_ENV\n          echo \"LibrariesPath=`pwd`\" >> $GITHUB_ENV\n\n          echo \"RNNOISE=`curl -sSL --header 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' https://api.github.com/repos/xiph/rnnoise/git/refs/heads/master | jq -r .object.sha`\" >> $GITHUB_ENV\n          echo \"WEBRTC=`curl -sSL --header 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' https://api.github.com/repos/desktop-app/tg_owt/git/refs/heads/master | jq -r .object.sha`\" >> $GITHUB_ENV\n\n      - name: RNNoise cache.\n        id: cache-rnnoise\n        uses: actions/cache@v5\n        with:\n          path: ${{ env.LibrariesPath }}/local/rnnoise\n          key: ${{ runner.OS }}-rnnoise-${{ env.RNNOISE }}-${{ env.CACHE_KEY }}\n      - name: RNNoise.\n        if: steps.cache-rnnoise.outputs.cache-hit != 'true'\n        run: |\n          cd $LibrariesPath\n\n          git clone --depth=1 $GIT/xiph/rnnoise.git\n          cd rnnoise\n          ./autogen.sh\n          ./configure --prefix=$LibrariesPath/local/rnnoise --disable-examples --disable-doc\n          make -j$(sysctl -n hw.logicalcpu)\n          make install\n\n      - name: WebRTC cache.\n        id: cache-webrtc\n        uses: actions/cache@v5\n        with:\n          path: ${{ env.LibrariesPath }}/local/tg_owt\n          key: ${{ runner.OS }}-webrtc-${{ env.WEBRTC }}-${{ env.CACHE_KEY }}\n      - name: WebRTC.\n        if: steps.cache-webrtc.outputs.cache-hit != 'true'\n        run: |\n          cd $LibrariesPath\n\n          git clone --depth=1 --recursive --shallow-submodules $GIT/desktop-app/tg_owt.git\n          cd tg_owt\n\n          cmake -Bbuild . -DCMAKE_INSTALL_PREFIX=$LibrariesPath/local/tg_owt\n          cmake --build build\n          cmake --install build\n\n      - name: TDE2E cache.\n        id: cache-tde2e\n        uses: actions/cache@v5\n        with:\n          path: ${{ env.LibrariesPath }}/local/tde2e\n          key: ${{ runner.OS }}-tde2e-${{ env.TDE2E }}-${{ env.CACHE_KEY }}\n      - name: TDE2E.\n        if: steps.cache-tde2e.outputs.cache-hit != 'true'\n        run: |\n          cd $LibrariesPath\n\n          git init tde2e\n          cd tde2e\n          git remote add origin $GIT/tdlib/td.git\n          git fetch --depth=1 origin $TDE2E\n          git reset --hard FETCH_HEAD\n\n          cmake -Bbuild . -DCMAKE_INSTALL_PREFIX=$LibrariesPath/local/tde2e -DTD_E2E_ONLY=ON\n          cmake --build build\n          cmake --install build\n\n      - name: Telegram Desktop build.\n        if: env.ONLY_CACHE == 'false'\n        run: |\n          cd $REPO_NAME\n          export CMAKE_PREFIX_PATH=\"$CMAKE_PREFIX_PATH$(find $LibrariesPath/local -mindepth 1 -maxdepth 1 -type d -exec printf ':%s' {} +)\"\n\n          DEFINE=\"\"\n          if [ -n \"${{ matrix.defines }}\" ]; then\n            DEFINE=\"-D ${{ matrix.defines }}=ON\"\n            echo Define from matrix: $DEFINE\n            echo \"ARTIFACT_NAME=Telegram ${{ matrix.defines }}\" >> $GITHUB_ENV\n          else\n            echo \"ARTIFACT_NAME=Telegram\" >> $GITHUB_ENV\n          fi\n\n          cmake -Bbuild . -DTDESKTOP_API_TEST=ON $DEFINE\n          cmake --build build\n          macdeployqt build/Telegram.app\n          codesign --remove-signature build/Telegram.app\n\n      - name: Move artifact.\n        if: env.UPLOAD_ARTIFACT == 'true'\n        run: |\n          cd $REPO_NAME/build\n          mkdir artifact\n          mv Telegram.app artifact/\n      - uses: actions/upload-artifact@v7\n        if: env.UPLOAD_ARTIFACT == 'true'\n        name: Upload artifact.\n        with:\n          name: ${{ env.ARTIFACT_NAME }}\n          path: ${{ env.REPO_NAME }}/build/artifact/\n"
  },
  {
    "path": ".github/workflows/needs-user-action.yml",
    "content": "name: Needs user action.\n\non:\n  issue_comment:\n    types: [created]\n  schedule:\n    - cron: '0 2 * * *'\n\njobs:\n  needs-user-action:\n    runs-on: ubuntu-slim\n    steps:\n      - uses: lee-dohm/no-response@v0.5.0\n        with:\n          token: ${{ github.token }}\n          responseRequiredLabel: needs user action\n"
  },
  {
    "path": ".github/workflows/snap.yml",
    "content": "name: Snap\n\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n\njobs:\n  snap:\n    name: Build and publish snap\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          submodules: recursive\n\n      - name: Setup LXD\n        run: |\n          sudo iptables -P FORWARD ACCEPT\n          sudo snap install --classic snapcraft\n          sudo usermod -aG lxd $USER\n          sudo lxd init --auto\n          sudo lxd waitready\n\n      - name: Free up disk space\n        uses: samueldr/more-space-action@97048bd0df83fb05b5257887bdbaffc848887673\n        with:\n          enable-remove-default-apt-patterns: false\n          enable-lvm-span: true\n          lvm-span-mountpoint: /var/snap/lxd/common/lxd/storage-pools/default/containers\n\n      - name: Build snap\n        run: sudo -u $USER snap run snapcraft --verbosity=debug\n\n      - name: Find snap artifact\n        run: |\n          SNAP_FILE=$(echo forkgram_*.snap)\n          echo \"SNAP_FILE=$SNAP_FILE\" >> \"$GITHUB_ENV\"\n\n      - name: Upload artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: ${{ env.SNAP_FILE }}\n          path: ${{ env.SNAP_FILE }}\n\n      - name: Publish to Snap Store\n        env:\n          SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}\n        run: snapcraft upload \"$SNAP_FILE\" --release=stable\n"
  },
  {
    "path": ".github/workflows/waiting-for-answer.yml",
    "content": "name: Waiting for answer.\n\non:\n  issue_comment:\n    types: [created]\n  schedule:\n    - cron: '30 0 * * *'\n\njobs:\n  waiting-for-answer:\n    runs-on: ubuntu-slim\n    steps:\n      - uses: lee-dohm/no-response@v0.5.0\n        with:\n          token: ${{ github.token }}\n          responseRequiredLabel: waiting for answer\n"
  },
  {
    "path": ".github/workflows/win.yml",
    "content": "name: Windows.\n\non:\n  push:\n    paths-ignore:\n      - 'docs/**'\n      - '**.md'\n      - '!docs/building-win*.md'\n      - 'changelog.txt'\n      - 'LEGAL'\n      - 'LICENSE'\n      - '.github/**'\n      - '!.github/workflows/win.yml'\n      - 'lib/xdg/**'\n      - 'snap/**'\n      - 'Telegram/build/docker/**'\n      - 'Telegram/Resources/uwp/**'\n      - 'Telegram/SourceFiles/platform/linux/**'\n      - 'Telegram/SourceFiles/platform/mac/**'\n      - 'Telegram/Telegram/**'\n      - 'Telegram/configure.sh'\n      - 'Telegram/Telegram.plist'\n  pull_request:\n    paths-ignore:\n      - 'docs/**'\n      - '**.md'\n      - '!docs/building-win*.md'\n      - 'changelog.txt'\n      - 'LEGAL'\n      - 'LICENSE'\n      - '.github/**'\n      - '!.github/workflows/win.yml'\n      - 'lib/xdg/**'\n      - 'snap/**'\n      - 'Telegram/build/docker/**'\n      - 'Telegram/Resources/uwp/**'\n      - 'Telegram/SourceFiles/platform/linux/**'\n      - 'Telegram/SourceFiles/platform/mac/**'\n      - 'Telegram/Telegram/**'\n      - 'Telegram/configure.sh'\n      - 'Telegram/Telegram.plist'\n\njobs:\n\n  windows:\n    name: Windows\n    runs-on: ${{ matrix.arch == 'arm64' && 'windows-11-arm' || 'windows-latest' }}\n\n    strategy:\n      matrix:\n        arch: [x64_x86, x64, arm64]\n        qt: [\"\", qt6]\n        generator: [\"\", \"Ninja Multi-Config\"]\n        exclude:\n          - arch: arm64\n            qt: \"\"\n          - arch: x64_x86\n            qt: qt6\n\n    env:\n      UPLOAD_ARTIFACT: \"true\"\n      ONLY_CACHE: \"false\"\n      PREPARE_PATH: \"Telegram/build/prepare/prepare.py\"\n\n    defaults:\n      run:\n        shell: cmd\n\n    steps:\n      - name: Prepare directories.\n        run: |\n          mkdir %userprofile%\\TBuild\\Libraries\n          mklink /d %GITHUB_WORKSPACE%\\TBuild %userprofile%\\TBuild\n          echo TBUILD=%GITHUB_WORKSPACE%\\TBuild>>%GITHUB_ENV%\n          echo LibrariesPath=%GITHUB_WORKSPACE%\\TBuild\\Libraries${{ matrix.arch == 'x64' && '\\win64' || '' }}>>%GITHUB_ENV%\n\n      - name: Get repository name.\n        shell: bash\n        run: echo \"REPO_NAME=${GITHUB_REPOSITORY##*/}\" >> $GITHUB_ENV\n\n      - name: Clone.\n        uses: actions/checkout@v6\n        with:\n          submodules: recursive\n          path: ${{ env.TBUILD }}\\${{ env.REPO_NAME }}\n\n      - name: First set up.\n        shell: bash\n        run: |\n          DOCPATH=$TBUILD/$REPO_NAME/docs/building-win.md\n          SDK=\"$(grep \"SDK version\" $DOCPATH | sed -r 's/.*\\*\\*(.*)\\*\\* SDK version.*/\\1/')\"\n          echo \"SDK=$SDK\" >> $GITHUB_ENV\n\n          sed -i '/CMAKE_${lang}_FLAGS_DEBUG_INIT/s/${_Zi}//' \"$PROGRAMFILES\"/CMake/share/cmake*/Modules/Platform/Windows-MSVC.cmake\n\n          echo \"$(sha256sum $TBUILD/$REPO_NAME/$PREPARE_PATH | awk '{ print $1 }')\" >> CACHE_KEY.txt\n          echo \"$SDK\" >> CACHE_KEY.txt\n          echo \"CACHE_KEY=$(sha256sum CACHE_KEY.txt | awk '{ print $1 }')\" >> $GITHUB_ENV\n\n          echo \"Configurate git for cherry-picks.\"\n          git config --global user.email \"you@example.com\"\n          git config --global user.name \"Sample\"\n\n      - uses: ilammy/msvc-dev-cmd@v1.13.0\n        name: Native Tools Command Prompt.\n        with:\n          arch: ${{ matrix.arch }}\n          sdk: ${{ env.SDK }}\n\n      - name: NuGet sources.\n        run: |\n          nuget sources Disable -Name \"Microsoft Visual Studio Offline Packages\"\n          nuget sources Add -Source https://api.nuget.org/v3/index.json & exit 0\n\n      - name: ThirdParty cache.\n        id: cache-third-party\n        uses: actions/cache@v5\n        with:\n          path: ${{ env.TBUILD }}\\ThirdParty\n          key: ${{ runner.OS }}-${{ runner.arch }}-third-party-${{ env.CACHE_KEY }}\n          restore-keys: ${{ runner.OS }}-${{ runner.arch }}-third-party-\n\n      - name: Libraries cache.\n        id: cache-libs\n        uses: actions/cache@v5\n        with:\n          path: |\n            ${{ env.LibrariesPath }}\\*\n            !${{ env.LibrariesPath }}\\cache_keys\n            !${{ env.LibrariesPath }}\\[qQ]t[_-]*\n            ${{ env.LibrariesPath }}\\cache_keys\\*\n            !${{ env.LibrariesPath }}\\cache_keys\\[qQ]t[_-]*\n          key: ${{ runner.OS }}-${{ matrix.arch }}-libs-${{ env.CACHE_KEY }}\n          restore-keys: ${{ runner.OS }}-${{ matrix.arch }}-libs-\n\n      - name: Qt cache.\n        id: cache-qt\n        uses: actions/cache@v5\n        with:\n          path: |\n            ${{ env.LibrariesPath }}\\[qQ]t[_-]*\n            ${{ env.LibrariesPath }}\\cache_keys\\[qQ]t[_-]*\n          key: ${{ runner.OS }}-${{ matrix.arch }}-${{ matrix.qt || 'qt' }}-${{ env.CACHE_KEY }}\n          restore-keys: ${{ runner.OS }}-${{ matrix.arch }}-${{ matrix.qt || 'qt' }}-\n\n      - name: Libraries.\n        env:\n          GYP_MSVS_OVERRIDE_PATH: 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\'\n          GYP_MSVS_VERSION: 2022\n        run: |\n          %TBUILD%\\%REPO_NAME%\\Telegram\\build\\prepare\\win.bat skip-release silent ${{ matrix.qt }}\n\n      - name: Read configuration matrix.\n        shell: bash\n        run: |\n          ARTIFACT_NAME=\"Telegram\"\n\n          ARCH=\"\"\n          if [ -n \"${{ matrix.arch }}\" ]; then\n            case \"${{ matrix.arch }}\" in\n              x64_x86) ARCH=\"x86\";;\n              arm64) ARCH=\"arm\";;\n              *) ARCH=\"${{ matrix.arch }}\";;\n            esac\n            echo \"Architecture from matrix: $ARCH\"\n            ARTIFACT_NAME=\"${ARTIFACT_NAME} ${{ matrix.arch }}\"\n          fi\n\n          if [ -n \"${{ matrix.qt }}\" ]; then\n            ARTIFACT_NAME=\"${ARTIFACT_NAME} ${{ matrix.qt }}\"\n          fi\n\n          GENERATOR=\"\"\n          if [ -n \"${{ matrix.generator }}\" ]; then\n            GENERATOR=\"-G \\\"${{ matrix.generator }}\\\"\"\n            echo \"Generator from matrix: $GENERATOR\"\n            ARTIFACT_NAME=\"${ARTIFACT_NAME} ${{ matrix.generator }}\"\n          fi\n          echo \"TDESKTOP_BUILD_GENERATOR=$GENERATOR\" >> $GITHUB_ENV\n\n          [ -n \"$GENERATOR\" ] && ARCH=\"\"\n          echo \"TDESKTOP_BUILD_ARCH=$ARCH\" >> $GITHUB_ENV\n\n          DEFINE=\"\"\n          if [ -n \"${{ matrix.defines }}\" ]; then\n            DEFINE=\"-D ${{ matrix.defines }}=ON\"\n            echo \"Define from matrix: $DEFINE\"\n            ARTIFACT_NAME=\"${ARTIFACT_NAME} ${{ matrix.defines }}\"\n          fi\n          echo \"TDESKTOP_BUILD_DEFINE=$DEFINE\" >> $GITHUB_ENV\n\n          echo \"ARTIFACT_NAME=$ARTIFACT_NAME\" >> $GITHUB_ENV\n\n          API=\"-D TDESKTOP_API_TEST=ON\"\n          if [ $GITHUB_REF == 'refs/heads/nightly' ]; then\n            echo \"Use the open credentials.\"\n            API=\"-D TDESKTOP_API_ID=611335 -D TDESKTOP_API_HASH=d524b414d21f4d37f08684c1df41ac9c\"\n          fi\n          echo \"TDESKTOP_BUILD_API=$API\" >> $GITHUB_ENV\n\n      - name: Free up some disk space.\n        shell: bash\n        run: find $LibrariesPath '(' '(' ! '(' -name '*.lib' -o -name '*.a' -o -name '*.exe' -o -name '*.h' -o -name '*.hpp' -o -name '*.inc' -o -name '*.cmake' -o -path '*/include/*' -o -path '*/objects-*' -o -path '*/cache_keys/*' -o -path '*/patches/*' ')' -type f ')' -o -empty ')' -delete\n\n      - name: Telegram Desktop build.\n        if: env.ONLY_CACHE == 'false'\n        run: |\n          cd %TBUILD%\\%REPO_NAME%\\Telegram\n\n          call configure.bat ^\n          %TDESKTOP_BUILD_GENERATOR% ^\n          %TDESKTOP_BUILD_ARCH% ^\n          ${{ matrix.qt }} ^\n          %TDESKTOP_BUILD_API% ^\n          -D CMAKE_CONFIGURATION_TYPES=Debug ^\n          -D CMAKE_COMPILE_WARNING_AS_ERROR=ON ^\n          -D CMAKE_MSVC_DEBUG_INFORMATION_FORMAT= ^\n          -D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF ^\n          -D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF ^\n          %TDESKTOP_BUILD_DEFINE%\n\n          cmake --build ..\\out --config Debug --parallel\n\n      - name: Move artifact.\n        if: (env.UPLOAD_ARTIFACT == 'true') || (github.ref == 'refs/heads/nightly')\n        run: |\n          set OUT=%TBUILD%\\%REPO_NAME%\\out\\Debug\n          mkdir artifact\n          move %OUT%\\Telegram.exe artifact/\n          move %OUT%\\Updater.exe artifact/\n      - uses: actions/upload-artifact@v7\n        name: Upload artifact.\n        if: (env.UPLOAD_ARTIFACT == 'true') || (github.ref == 'refs/heads/nightly')\n        with:\n          name: ${{ env.ARTIFACT_NAME }}\n          path: artifact\\\n"
  },
  {
    "path": ".gitignore",
    "content": "/out/\nDebug/\nRelease/\n/ThirdParty/\n/Telegram/build/target\n/Telegram/tests/\n/Telegram/gyp/tests/*.test\n/Telegram/out/\n/Telegram/*.user\n*.vcxproj*\n*.sln\n*.suo\n*.sdf\n*.opensdf\n*.opendb\n*.VC.db\n*.aps\n*.xcodeproj\nipch/\n.vs/\n.vscode/\n.cache/\ncompile_commands.json\n\n/Telegram/log.txt\n/Telegram/data\n/Telegram/data_config\n/Telegram/DebugLogs/\n/Telegram/tdata/\n/Telegram/tdumps/\n\n.DS_Store\n._*\n.qmake.stash\n/Mac/\nproject.xcworkspace\nxcuserdata\n\nparts\nprime\nstage\n*.snap\n.snapcraft\n/snap/gui/*.png\n/snap/gui/*.desktop\n/snap/plugins/__pycache__\n\n/Telegram/*.user.*\n*.txt.user\n*.pro.user\n/Linux/\n/Telegram/Makefile\n*.*~\n.idea/\ncmake-build-debug/\n*.qsb\n\n# Local configuration files\nsettings.local.json\n*.local.json\n.env\n.env.local\n.env.*.local\n\n# Cursor IDE local settings (but keep .cursor/rules/)\n.cursor/*\n!.cursor/rules/\n\n# AI work folder (session-specific, not for version control)\n.ai\n\n# Generated changelog page (built by CI, deployed to GitHub Pages)\n/docs/changelog/\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"Telegram/ThirdParty/GSL\"]\n\tpath = Telegram/ThirdParty/GSL\n\turl = https://github.com/Microsoft/GSL.git\n[submodule \"Telegram/ThirdParty/xxHash\"]\n\tpath = Telegram/ThirdParty/xxHash\n\turl = https://github.com/Cyan4973/xxHash.git\n[submodule \"Telegram/ThirdParty/rlottie\"]\n\tpath = Telegram/ThirdParty/rlottie\n\turl = https://github.com/desktop-app/rlottie.git\n[submodule \"Telegram/ThirdParty/lz4\"]\n\tpath = Telegram/ThirdParty/lz4\n\turl = https://github.com/lz4/lz4.git\n[submodule \"Telegram/lib_crl\"]\n\tpath = Telegram/lib_crl\n\turl = https://github.com/desktop-app/lib_crl.git\n[submodule \"Telegram/lib_rpl\"]\n\tpath = Telegram/lib_rpl\n\turl = https://github.com/desktop-app/lib_rpl.git\n[submodule \"Telegram/lib_base\"]\n\tpath = Telegram/lib_base\n\turl = https://github.com/desktop-app/lib_base.git\n[submodule \"Telegram/codegen\"]\n\tpath = Telegram/codegen\n\turl = https://github.com/forkgram/codegen.git\n[submodule \"Telegram/lib_ui\"]\n\tpath = Telegram/lib_ui\n\turl = https://github.com/forkgram/lib_ui.git\n[submodule \"Telegram/lib_lottie\"]\n\tpath = Telegram/lib_lottie\n\turl = https://github.com/desktop-app/lib_lottie.git\n[submodule \"Telegram/lib_tl\"]\n\tpath = Telegram/lib_tl\n\turl = https://github.com/desktop-app/lib_tl.git\n[submodule \"Telegram/lib_spellcheck\"]\n\tpath = Telegram/lib_spellcheck\n\turl = https://github.com/desktop-app/lib_spellcheck\n[submodule \"Telegram/lib_storage\"]\n\tpath = Telegram/lib_storage\n\turl = https://github.com/desktop-app/lib_storage.git\n[submodule \"cmake\"]\n\tpath = cmake\n\turl = https://github.com/desktop-app/cmake_helpers.git\n[submodule \"Telegram/ThirdParty/expected\"]\n\tpath = Telegram/ThirdParty/expected\n\turl = https://github.com/TartanLlama/expected\n[submodule \"Telegram/ThirdParty/QR\"]\n\tpath = Telegram/ThirdParty/QR\n\turl = https://github.com/nayuki/QR-Code-generator\n[submodule \"Telegram/lib_qr\"]\n\tpath = Telegram/lib_qr\n\turl = https://github.com/desktop-app/lib_qr.git\n[submodule \"Telegram/ThirdParty/hunspell\"]\n\tpath = Telegram/ThirdParty/hunspell\n\turl = https://github.com/hunspell/hunspell\n[submodule \"Telegram/ThirdParty/range-v3\"]\n\tpath = Telegram/ThirdParty/range-v3\n\turl = https://github.com/ericniebler/range-v3.git\n[submodule \"Telegram/ThirdParty/nimf\"]\n\tpath = Telegram/ThirdParty/nimf\n\turl = https://github.com/hamonikr/nimf.git\n[submodule \"Telegram/ThirdParty/hime\"]\n\tpath = Telegram/ThirdParty/hime\n\turl = https://github.com/hime-ime/hime.git\n[submodule \"Telegram/ThirdParty/fcitx5-qt\"]\n\tpath = Telegram/ThirdParty/fcitx5-qt\n\turl = https://github.com/fcitx/fcitx5-qt.git\n[submodule \"Telegram/lib_webrtc\"]\n\tpath = Telegram/lib_webrtc\n\turl = https://github.com/desktop-app/lib_webrtc.git\n[submodule \"Telegram/ThirdParty/tgcalls\"]\n\tpath = Telegram/ThirdParty/tgcalls\n\turl = https://github.com/TelegramMessenger/tgcalls.git\n[submodule \"Telegram/lib_webview\"]\n\tpath = Telegram/lib_webview\n\turl = https://github.com/desktop-app/lib_webview.git\n[submodule \"Telegram/ThirdParty/dispatch\"]\n\tpath = Telegram/ThirdParty/dispatch\n\turl = https://github.com/apple/swift-corelibs-libdispatch\n[submodule \"Telegram/ThirdParty/kimageformats\"]\n\tpath = Telegram/ThirdParty/kimageformats\n\turl = https://github.com/KDE/kimageformats.git\n[submodule \"Telegram/ThirdParty/kcoreaddons\"]\n\tpath = Telegram/ThirdParty/kcoreaddons\n\turl = https://github.com/KDE/kcoreaddons.git\n[submodule \"Telegram/ThirdParty/cld3\"]\n\tpath = Telegram/ThirdParty/cld3\n\turl = https://github.com/google/cld3.git\n[submodule \"Telegram/ThirdParty/libprisma\"]\n\tpath = Telegram/ThirdParty/libprisma\n\turl = https://github.com/desktop-app/libprisma.git\n[submodule \"Telegram/ThirdParty/xdg-desktop-portal\"]\n\tpath = Telegram/ThirdParty/xdg-desktop-portal\n\turl = https://github.com/flatpak/xdg-desktop-portal.git\n[submodule \"Telegram/lib_translate\"]\n\tpath = Telegram/lib_translate\n\turl = https://github.com/desktop-app/lib_translate\n[submodule \"Telegram/ThirdParty/zbar\"]\n\tpath = Telegram/ThirdParty/zbar\n\turl = https://github.com/mchehab/zbar.git\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# Agent Guide for Telegram Desktop\n\nThis guide defines repository-wide instructions for coding agents working with the Telegram Desktop codebase.\n\n## Build System Structure\n\nThe build system expects this directory layout:\n\n```text\nL:\\Telegram\\                    # BuildPath\nL:\\Telegram\\tdesktop\\           # Repository (you work here)\nL:\\Telegram\\Libraries\\          # 32-bit dependencies (Linux/macOS)\nL:\\Telegram\\win64\\Libraries\\    # 64-bit dependencies (Windows)\nL:\\Telegram\\ThirdParty\\         # Build tools (NuGet, Python, etc.)\n```\n\nDependencies are located relative to the repository: `../Libraries`, `../win64/Libraries`, or `../ThirdParty`.\n\n## Build Configuration\n\n### Build Commands\n\n**From repository root, run:**\n\n```bash\ncmake --build out --config Debug --target Telegram\n```\n\nThat's it. The `out/` directory is already configured. The executable will be at `out/Debug/Telegram.exe`.\n\n**Important:** When running cmake from a shell that doesn't support `cd`, use quoted absolute paths:\n```bash\ncmake --build \"l:\\Telegram\\tx64\\out\" --config Debug --target Telegram\n```\n\n**Never build Release** - it's extremely heavy and not needed for testing changes.\n\n## Platform-Specific Requirements\n\n### Windows\n- Requires Visual Studio 2022\n- Must run from appropriate Native Tools Command Prompt:\n  - \"x64 Native Tools Command Prompt\" for `win64`\n  - \"x86 Native Tools Command Prompt\" for `win`\n  - \"ARM64 Native Tools Command Prompt\" for `winarm`\n- Dependencies: `../win64/Libraries` (64-bit) or `../Libraries` (32-bit)\n\n### macOS\n- Requires Xcode\n- Dependencies: `../Libraries/local/Qt-*`\n- Set `QT` environment variable: `export QT=6.8`\n\n### Linux\n- Build dependencies in `../Libraries`\n- Set `QT` environment variable if needed\n\n## Key Files\n\n- **`Telegram/build/version`** - Version information\n- **`out/`** - Build output directory\n\n## Troubleshooting\n\n### \"Libraries not found\"\nEnsure the repository is in `L:\\Telegram\\tdesktop`. The build system requires `../win64/Libraries` to exist.\n\n### Build fails with \"wrong command prompt\"\nOn Windows, use the correct Visual Studio Native Tools Command Prompt matching your target (x64/x86/ARM64).\n\n### Build fails with PDB or EXE access errors\n\n**âš ï¸ CRITICAL: DO NOT RETRY THE BUILD. STOP AND WAIT FOR USER.**\n\nIf the build fails with ANY of these errors:\n- `fatal error C1041: cannot open program database`\n- `cannot open output file 'Telegram.exe'`\n- `LNK1104: cannot open file`\n- Any \"access denied\" or \"file in use\" error\n\n**STOP IMMEDIATELY.** These errors mean files are locked by a running process (Telegram.exe or debugger).\n\n**What to do:**\n1. Do NOT attempt another build - it will fail the same way\n2. Do NOT try to delete files - they are locked\n3. Do NOT try any workarounds or fixes\n4. IMMEDIATELY inform the user:\n\n> \"Build failed - files are locked. Please close Telegram.exe (and any debugger) so I can rebuild.\"\n\n**Then WAIT for user confirmation before attempting any build.**\n\nRetrying builds wastes time and context. The ONLY fix is for the user to close the running process.\n\n## Best Practices\n\n1. **Always use Debug builds** - Release builds are extremely heavy\n2. **Don't build Release configuration** - it's too heavy for testing\n\n## Text File Format\n\n- On Windows, keep project text files with CRLF line endings.\n- Do not save source, header, build/config, style, or localization files as UTF-8 with BOM. Use UTF-8 without BOM.\n- When rewriting project text files for normalization, preserve file content otherwise and do not introduce a BOM.\n\n## Local Storage Serialization\n\nBoth app-level (`Core::Settings`) and session-level (`Main::SessionSettings`) use sequential binary serialization via `QDataStream`. Key rules:\n\n- New fields must ALWAYS be appended at the **end** of the stream, never inserted in the middle\n- Reading new fields must be guarded with `!stream.atEnd()` and provide a meaningful default/fallback\n- Inserting in the middle breaks reading of data saved by older versions (the new read code consumes bytes that belong to subsequent fields)\n- For simple flags and values, prefer using the generic KV prefs facility (`writePref<Type>` / `readPref<Type>`) instead of adding to the binary stream -- this avoids serialization ordering issues entirely\n\n---\n\n# Development Guidelines\n\n## Coding Style\n\n**Do NOT write comments in code:**\n\nThis 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.\n\n```cpp\n// BAD - don't do this:\n// Get the user's name\nauto name = user->name();\n// Check if premium\nif (user->isPremium()) {\n\n// GOOD - no comments needed, code is self-explanatory:\nauto name = user->name();\nif (user->isPremium()) {\n\n// ACCEPTABLE - complex algorithm explanation (4+ lines):\n// The algorithm works by first collecting all visible messages\n// in the viewport, then calculating their intersection with\n// the clip rectangle. Messages are grouped by date headers,\n// and we need to account for sticky headers that may overlap\n// with the first message in each group.\n```\n\n**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.\n\n**Use `auto` for type deduction:**\n\nPrefer `auto` (or `const auto`, `const auto &`) instead of explicit types:\n\n```cpp\n// Prefer this:\nauto currentTitle = tr::lng_settings_title(tr::now);\nauto nameProducer = GetNameProducer();\n\n// Instead of this:\nQString currentTitle = tr::lng_settings_title(tr::now);\nrpl::producer<QString> nameProducer = GetNameProducer();\n```\n\n## API Usage\n\n### API Schema Files\n\nAPI definitions use [TL Language](https://core.telegram.org/mtproto/TL):\n\n1. **`Telegram/SourceFiles/mtproto/scheme/mtproto.tl`** - MTProto protocol (encryption, auth, etc.)\n2. **`Telegram/SourceFiles/mtproto/scheme/api.tl`** - Telegram API (messages, users, chats, etc.)\n\n### Making API Requests\n\nStandard pattern using `api()`, generated `MTP...` types, and callbacks:\n\n```cpp\napi().request(MTPnamespace_MethodName(\n    MTP_flags(flags_value),\n    MTP_inputPeer(peer),\n    MTP_string(messageText),\n    MTP_long(randomId),\n    MTP_vector<MTPMessageEntity>()\n)).done([=](const MTPResponseType &result) {\n    // Handle successful response\n\n    // Multiple constructors - use .match() or check type:\n    result.match([&](const MTPDuser &data) {\n        // use data.vfirst_name().v\n    }, [&](const MTPDuserEmpty &data) {\n        // handle empty user\n    });\n\n    // Single constructor - use .data() shortcut:\n    const auto &data = result.data();\n    // use data.vmessages().v\n\n}).fail([=](const MTP::Error &error) {\n    // Handle API error\n    if (error.type() == u\"FLOOD_WAIT_X\"_q) {\n        // Handle flood wait\n    }\n}).handleFloodErrors().send();\n```\n\n**Key points:**\n- Always refer to `api.tl` for method signatures and return types\n- Use generated `MTP...` types for parameters (`MTP_int`, `MTP_string`, etc.)\n- For multiple constructors, use `.match()` or check `.type()` against `mtpc_` constants then call `.c_constructorName()`:\n  ```cpp\n  // Using match:\n  result.match([&](const MTPDuser &data) { ... }, [&](const MTPDuserEmpty &data) { ... });\n  // Or explicit type check:\n  if (result.type() == mtpc_user) {\n      const auto &data = result.c_user(); // asserts on type mismatch\n  }\n  ```\n- For single constructors, use `.data()` shortcut\n- Include `.handleFloodErrors()` before `.send()` in rare cases where you want special case flood error handling\n\n## UI Styling\n\n### Style Files\n\nUI styles are defined in `.style` files using custom syntax:\n\n```style\nusing \"ui/basic.style\";\nusing \"ui/widgets/widgets.style\";\n\nMyButtonStyle {\n    textPadding: margins;\n    icon: icon;\n    height: pixels;\n}\n\ndefaultButton: MyButtonStyle {\n    textPadding: margins(10px, 15px, 10px, 15px);\n    icon: icon{{ \"gui/icons/search\", iconColor }};\n    height: 30px;\n}\n\nprimaryButton: MyButtonStyle(defaultButton) {\n    icon: icon{{ \"gui/icons/check\", iconColor }};\n}\n```\n\n**Built-in types:**\n- `int` - Integer numbers (e.g., `maxLines: 3;`)\n- `bool` - Boolean values (e.g., `useShadow: true;`)\n- `pixels` - Pixel values with `px` suffix (e.g., `10px`)\n- `color` - Named colors from `ui/colors.palette`\n- `icon` - Inline icon definition: `icon{{ \"path/stem\", color }}`\n- `margins` - Four values: `margins(top, right, bottom, left)`\n- `size` - Two values: `size(width, height)`\n- `point` - Two values: `point(x, y)`\n- `align` - Alignment: `align(center)`, `align(left)`\n- `font` - Font: `font(14px semibold)`\n- `double` - Floating point\n\n**Multi-part icons** (layers drawn bottom-up):\n```style\nmyComplexIcon: icon{\n  { \"gui/icons/background\", iconBgColor },\n  { \"gui/icons/foreground\", iconFgColor }\n};\n```\n\n**Borders** are typically separate fields, not a single property:\n```style\nchatInput {\n  border: 1px;                       // width\n  borderFg: defaultInputFieldBorder; // color\n}\n```\n\n**Never hardcode sizes in code:**\n\nThe 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::`.\n\n```cpp\n// BAD - breaks at non-100% interface scale:\np.drawText(10, 20, text);\nwidget->setFixedHeight(48);\nauto margin = 8;\nauto iconSize = QSize(24, 24);\n\n// GOOD - define in .style file and reference:\np.drawText(st::myWidgetTextLeft, st::myWidgetTextTop, text);\nwidget->setFixedHeight(st::myWidgetHeight);\nauto margin = st::myWidgetMargin;\nauto iconSize = st::myWidgetIconSize;\n```\n\n**Duration constants**: Animation durations should NOT go in `.style` files, this is a legacy approach. Prefer `constexpr auto kName = crl::time(N)` in an anonymous namespace in the relevant `.cpp` file.\n\n### Usage in Code\n\n```cpp\n#include \"styles/style_widgets.h\"\n\n// Access style members\nint height = st::primaryButton.height;\nconst style::icon &icon = st::primaryButton.icon;\nstyle::margins padding = st::primaryButton.textPadding;\n\n// Use in painting\nvoid MyWidget::paintEvent(QPaintEvent *e) {\n    Painter p(this);\n    p.fillRect(rect(), st::chatInput.backgroundColor);\n}\n```\n\n## Localization\n\n### String Definitions\n\nStrings are defined in `Telegram/Resources/langs/lang.strings`:\n\n```\n\"lng_settings_title\" = \"Settings\";\n\"lng_confirm_delete_item\" = \"Are you sure you want to delete {item_name}?\";\n\"lng_files_selected#one\" = \"{count} file selected\";\n\"lng_files_selected#other\" = \"{count} files selected\";\n```\n\n### Usage in Code\n\n**Immediate (current value):**\n\n```cpp\nauto currentTitle = tr::lng_settings_title(tr::now);\n\nauto currentConfirmation = tr::lng_confirm_delete_item(\n    tr::now,\n    lt_item_name, currentItemName);\n\nauto filesText = tr::lng_files_selected(tr::now, lt_count, count);\n```\n\n**Reactive (rpl::producer):**\n\n```cpp\nauto titleProducer = tr::lng_settings_title();\n\nauto confirmationProducer = tr::lng_confirm_delete_item(\n    lt_item_name,\n    std::move(itemNameProducer));\n\nauto filesTextProducer = tr::lng_files_selected(\n    lt_count,\n    countProducer | tr::to_count());\n```\n\n**Key points:**\n- Pass `tr::now` as first argument for immediate `QString`\n- Omit `tr::now` for reactive `rpl::producer<QString>`\n- Placeholders use `lt_tag_name, value` pattern\n- For `{count}`: immediate uses `int`, reactive uses `rpl::producer<float64>` with `| tr::to_count()`\n- Move producers with `std::move` when passing to placeholders\n- Rich text projectors — these `tr::` helpers serve double duty: as the **last argument** (projector) they set the return type to `TextWithEntities`, and as **placeholder values** they wrap individual substitutions in formatting. Always prefer them over `Ui::Text::Bold()`, `Ui::Text::RichLangValue`, etc. — see REVIEW.md for the full mapping.\n  - `tr::marked` — basic projection, converts `QString` to `TextWithEntities`\n  - `tr::rich` — interprets `**bold**`/`__italic__` markup in the string\n  - `tr::bold`, `tr::italic`, `tr::underline` — wrap text in that formatting\n  - `tr::link` — wrap as a clickable link\n  - `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\n  ```cpp\n  // As last argument (projector):\n  auto title = tr::lng_export_progress_title(tr::now, tr::bold);\n  auto text = tr::lng_proxy_incorrect_secret(tr::now, tr::rich);\n  // As placeholder value wrapper + projector:\n  auto desc = tr::lng_some_key(\n      tr::now,\n      lt_name,\n      tr::bold(userName),\n      lt_group,\n      tr::bold(groupName),\n      tr::rich);\n  // Nested tr::lng as placeholder:\n  auto linked = tr::lng_settings_birthday_contacts(\n      lt_link,\n      tr::lng_settings_birthday_contacts_link(tr::url(link)),\n      tr::marked);\n  ```\n\n## RPL (Reactive Programming Library)\n\n### Core Concepts\n\n**Producers** represent streams of values over time:\n\n```cpp\nauto intProducer = rpl::single(123);  // Emits single value\nauto lifetime = rpl::lifetime();       // Manages subscription lifetime\n```\n\n### Starting Pipelines\n\n```cpp\nstd::move(counter) | rpl::on_next([=](int value) {\n    qDebug() << \"Received: \" << value;\n}, lifetime);\n\n// Without lifetime parameter - MUST store returned lifetime:\nauto subscriptionLifetime = std::move(counter) | rpl::on_next([=](int value) {\n    // process value\n});\n```\n\n### Transforming Producers\n\n```cpp\nauto strings = std::move(ints) | rpl::map([](int value) {\n    return QString::number(value * 2);\n});\n\nauto evenInts = std::move(ints) | rpl::filter([](int value) {\n    return (value % 2 == 0);\n});\n```\n\n### Combining Producers\n\n**`rpl::combine`** - combines latest values (lambdas receive unpacked arguments):\n\n```cpp\nauto combined = rpl::combine(countProducer, textProducer);\n\nstd::move(combined) | rpl::on_next([=](int count, const QString &text) {\n    qDebug() << \"Count=\" << count << \", Text=\" << text;\n}, lifetime);\n```\n\n**`rpl::merge`** - merges producers of same type:\n\n```cpp\nauto merged = rpl::merge(sourceA, sourceB);\n\nstd::move(merged) | rpl::on_next([=](QString &&value) {\n    qDebug() << \"Merged value: \" << value;\n}, lifetime);\n```\n\n**Other pipeline starters** — besides `rpl::on_next`, there are:\n- `rpl::on_error([=](Error &&e) { ... }, lifetime)` — handle errors\n- `rpl::on_done([=] { ... }, lifetime)` — handle stream completion\n- `rpl::on_next_error_done(nextCb, errorCb, doneCb, lifetime)` — handle all three\n\nThe `Error` template parameter defaults to `rpl::no_error`: `rpl::producer<Type, Error = no_error>`.\n\n**Key points:**\n- Explicitly `std::move` producers when starting pipelines\n- Pass `rpl::lifetime` to `on_...` methods or store returned lifetime\n- Use `rpl::duplicate(producer)` to reuse a producer multiple times\n- Combined producers automatically unpack tuples in lambdas (works with `rpl::map`, `rpl::filter`, and `rpl::on_next`)\n\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# Claude Code Pointer\n\nRead `AGENTS.md` and treat it as the canonical repository-wide instructions.\n"
  },
  {
    "path": "CMakeLists.txt",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\ncmake_minimum_required(VERSION 3.25...3.31)\n\nset_property(GLOBAL PROPERTY USE_FOLDERS ON)\n\n# Apply qt6.11 patches to submodules at configure time.\nfile(GLOB _submodule_patches \"${CMAKE_SOURCE_DIR}/patches/*.patch\")\nforeach(_patch ${_submodule_patches})\n    get_filename_component(_patch_name ${_patch} NAME)\n    # Extract target submodule directory from patch filename (prefix before first '_').\n    string(REGEX MATCH \"^([a-z]+)\" _submodule ${_patch_name})\n    if (IS_DIRECTORY \"${CMAKE_SOURCE_DIR}/${_submodule}\")\n        execute_process(\n            COMMAND git apply --check ${_patch}\n            WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}/${_submodule}\"\n            RESULT_VARIABLE _check_result\n            OUTPUT_QUIET ERROR_QUIET\n        )\n        if (_check_result EQUAL 0)\n            execute_process(\n                COMMAND git apply ${_patch}\n                WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}/${_submodule}\"\n            )\n            message(STATUS \"Applied patch: ${_patch_name}\")\n        endif()\n    endif()\nendforeach()\n\ninclude(cmake/validate_special_target.cmake)\ninclude(cmake/version.cmake)\ndesktop_app_parse_version(Telegram/build/version)\n\nif (NOT DEFINED CMAKE_CONFIGURATION_TYPES)\n    set(configuration_types_init 1)\nendif()\n\nproject(Telegram\n    LANGUAGES C CXX\n    VERSION ${desktop_app_version_cmake}\n    DESCRIPTION \"Official Telegram Desktop messenger\"\n    HOMEPAGE_URL \"https://desktop.telegram.org\"\n)\n\nif (APPLE)\n    enable_language(OBJC OBJCXX)\nendif()\n\nif (configuration_types_init\n    AND CMAKE_CONFIGURATION_TYPES\n    AND NOT MinSizeRel IN_LIST CMAKE_CONFIGURATION_TYPES)\n    set(CMAKE_CONFIGURATION_TYPES \"${CMAKE_CONFIGURATION_TYPES};MinSizeRel\" CACHE STRING \"\" FORCE)\nendif()\n\nset_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT Telegram)\n\nget_filename_component(third_party_loc \"Telegram/ThirdParty\" REALPATH)\nget_filename_component(submodules_loc \"Telegram\" REALPATH)\nget_filename_component(cmake_helpers_loc \"cmake\" REALPATH)\n\nif (NOT DESKTOP_APP_USE_PACKAGED AND WIN32)\n    set(Python3_EXECUTABLE ${CMAKE_CURRENT_SOURCE_DIR}/../ThirdParty/python/Scripts/python)\nendif()\n\ninclude(cmake/variables.cmake)\ninclude(cmake/nice_target_sources.cmake)\ninclude(cmake/target_compile_options_if_exists.cmake)\ninclude(cmake/target_link_frameworks.cmake)\ninclude(cmake/target_link_options_if_exists.cmake)\ninclude(cmake/init_target.cmake)\ninclude(cmake/generate_target.cmake)\ninclude(cmake/nuget.cmake)\ninclude(cmake/validate_d3d_compiler.cmake)\ninclude(cmake/target_prepare_qrc.cmake)\n\ninclude(cmake/options.cmake)\ninclude(cmake/external/qt/package.cmake)\n\nset(desktop_app_skip_libs\n    glibmm\n    variant\n)\n\nadd_subdirectory(cmake)\nadd_subdirectory(Telegram)\n"
  },
  {
    "path": "LEGAL",
    "content": "This file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nCopyright (c) 2014-2026 The Telegram Desktop Authors.\n\nTelegram Desktop is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nIt is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nIn addition, as a special exception, the copyright holders give permission\nto link the code of portions of this program with the OpenSSL library.\n\nMore information about the Telegram project: https://telegram.org\n\nFull license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE\n"
  },
  {
    "path": "LICENSE",
    "content": "Telegram Desktop is licensed under the GNU General Public License\nversion 3 with the addition of the following special exception:\n\nIn addition, as a special exception, the copyright holders give\npermission to link the code of portions of this program with the OpenSSL\nlibrary.\n\nYou must obey the GNU General Public License in all respects for all of\nthe code used other than OpenSSL. If you modify file(s) with this\nexception, you may extend this exception to your version of the file(s),\nbut you are not obligated to do so. If you do not wish to do so, delete\nthis exception statement from your version. If you delete this exception\nstatement from all source files in the program, then also delete it here.\n\n                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    {one line to give the program's name and a brief idea of what it does.}\n    Copyright (C) {year}  {name of author}\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    {project}  Copyright (C) {year}  {fullname}\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<http://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "# Forkgram — Unofficial Telegram Messenger\n![image](https://user-images.githubusercontent.com/4051126/43634235-402a8b74-9714-11e8-85c0-8ceb0844a3b0.png)  \nForkgram is the cutting-edge Telegram fork with small Quality-of-Life enhancements.  \n\n[![Github All Releases](https://img.shields.io/github/downloads/Forkgram/tdesktop/total.svg?style=for-the-badge)](https://github.com/Forkgram/tdesktop/releases)  \n[![Snap Store](https://img.shields.io/snapcraft/v/forkgram/latest/stable?style=for-the-badge)](https://snapcraft.io/forkgram)  \n[![AUR](https://img.shields.io/aur/version/forkgram.svg?style=for-the-badge)](https://aur.archlinux.org/packages/forkgram)  \n[![Chocolatey](https://img.shields.io/chocolatey/v/forkgram.svg?style=for-the-badge)](https://community.chocolatey.org/packages/forkgram)  \n[![Scoop](https://img.shields.io/scoop/v/forkgram?bucket=extras&style=for-the-badge)](https://scoop.sh/#/apps?q=forkgram)\n[![Windows](https://img.shields.io/badge/Windows-x64%20%7C%20x86-blue?style=for-the-badge&logo=windows)](https://github.com/Forkgram/tdesktop/releases/latest)  \n[![Homebrew Cask](https://img.shields.io/homebrew/cask/v/forkgram.svg?style=for-the-badge)](https://formulae.brew.sh/cask/forkgram)  \n[![macOS](https://img.shields.io/badge/macOS-arm64%20%7C%20x86-black?style=for-the-badge&logo=apple)](https://github.com/Forkgram/tdesktop/releases/latest)  \n\n## Features:\n- Square avatars. *(Added XX.04.2017.)*  \nSquare tray icon. *(Updated 18.08.2018.)*\n\n- Opening any links with a custom URI Scheme. *(Added XX.04.2017 and updated 02.08.2019.)*  \n\n- Checkmark \"Delete for everyone\" by default. *(Added XX.04.2017.)*  \n![image](https://user-images.githubusercontent.com/4051126/43633718-bee9dd4a-9712-11e8-8b24-57822bd20c04.png)\n\n- The fourth button in the title to \"Always on top\" window. (Windows only.) *(Added 16.05.2017.)*  \n![image](https://user-images.githubusercontent.com/4051126/43633763-d441eaa2-9712-11e8-8fb1-1e9e37af1d5e.png)\n\n- Showing the full timestamp if the user was online more than 1 hour ago. *(Added 14.06.2017.)*  \n![image](https://user-images.githubusercontent.com/4051126/27127613-5ead16c2-5104-11e7-8be0-f231b4bc3f6b.png)\n\n- Hotkeys for the mediaview was usability improved. [**Cheatsheet**](#cheatsheet). *(Added 29.06.2017.)*  \n\n- Experimental auto-submitting of the local passcode. *(Added 29.06.2017.)*  \n![2018-08-10_01-50-45](https://user-images.githubusercontent.com/4051126/43930001-e5d1c1e8-9c3f-11e8-9ddd-ff1a60518d91.gif)\n\n- Showing date of forwarded messages in the title. *(Added 25.08.2017.)*  \n![image](https://user-images.githubusercontent.com/4051126/30046488-c94deb14-9213-11e7-8b2b-397ad6dbe6f7.png)\n\n- Button to go to the first message. *(Added 16.05.2018.)* ([Telegrate's](https://github.com/Sea-n/tdesktop) idea.)  \n![image](https://user-images.githubusercontent.com/4051126/40266489-7a31a1c8-5b54-11e8-868e-3c9877e0d6ff.png)\n\n- Option to remove fading of audio when it's paused/played. *(Added 10.08.2018.)*  \n\n- Ability to see thumbs of the executable files as well as for other files. *(Added 14.08.2018.)*  \n\n- Ability to search a selected text via the context menu. *(Added 22.01.2019.)*  \n\n- Ability to show the last seen time of the user in list of the dialogs. *(Added 31.01.2019.)*  \n\n- Option to enable a display of all recently used stickers. *(Added 09.02.2019.)*  \n\n- Auto-replacing of \"...\" with \"…\". *(Added 17.02.2019.)*  \n\n- Ability to bind Ctrl+6-9 dialogs with Ctrl+Shift+6-9 shortcuts. *(Added 11.06.2019.)*  \n\n- Ability to send files to Saved Messages without the forward header. *(Added 23.06.2019.)*  \n\n- Forward messages without quoting the original sender via `Share` box. *(Added 25.06.2019.)*  \n\n\n\n## Cheatsheet\n\n| Zoom in           | Zoom out           | Zoom reset         | Old zoom in           | Old zoom out           | Old zoom reset         |\n|----------------|-----------------|------------------|----------------|-----------------|------------------|\n| +     | -             | 0                | Ctrl`+`+     | Ctrl`+`-             | Ctrl`+`0                |\n| Up arrow     | Down arrow               |  *               | Ctrl`+`]     | Ctrl`+`_               |  Middle button              |\n| Scroll up     | Scroll down              | Middle button                 | Ctrl`+`=     | Ctrl`+`Scroll down              |                    |\n| ]     | _             |                  | Ctrl`+`*     |                 |                  |\n| =     |              |                  | Ctrl`+`Scroll up         |              |                  |\n\nYou can scroll images by the wheel with pressed Ctrl.\n\n## Downloads:\nYou can download binaries from the Releases or from my [Telegram channel Forkgram](https://t.me/forkgram).\n"
  },
  {
    "path": "REVIEW.md",
    "content": "# Code Review Style Guide\n\nThis 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.\n\n## Empty line before closing brace\n\nAlways add an empty line before the closing brace of a **class** (which has one or more sections like `public:` / `private:`). Plain **structs** with just data members do NOT get a trailing empty line — they are compact: `struct Foo { data lines; };`.\n\n```cpp\n// BAD:\nclass MyClass {\npublic:\n    void foo();\n\nprivate:\n    int _value = 0;\n};\n\n// GOOD:\nclass MyClass {\npublic:\n    void foo();\n\nprivate:\n    int _value = 0;\n\n};\n```\n\n## Multi-line expressions — operators at the start of continuation lines\n\nWhen 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.\n\n```cpp\n// BAD - continuation looks like scope code:\nif (const auto &lottie = animation->lottie;\n\tlottie && lottie->valid() && lottie->framesCount() > 1) {\n\tlottie->animate([=] {\n\n// GOOD - semicolon at start signals continuation:\nif (const auto &lottie = animation->lottie\n\t; lottie && lottie->valid() && lottie->framesCount() > 1) {\n\tlottie->animate([=] {\n\n// BAD - trailing && makes next line look like independent code:\nif (veryLongExpression() &&\n\tanotherLongExpression() &&\n\tanotherOne()) {\n\tdoSomething();\n\n// GOOD - leading && clearly marks continuation:\nif (veryLongExpression()\n\t&& anotherLongExpression()\n\t&& anotherOne()) {\n\tdoSomething();\n```\n\n## Minimize type checks — prefer direct cast over is + as\n\nDon'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.\n\n```cpp\n// BAD - redundant isUser() check, then asUser():\nif (peer && peer->isUser()) {\n\tpeer->asUser()->setNoForwardFlags(\n\n// GOOD - just cast and null-check:\nif (const auto user = peer->asUser()) {\n\tuser->setNoForwardFlags(\n```\n\nWhen you need a specific subtype, look up the specific subtype directly instead of loading a generic type and then casting:\n\n```cpp\n// BAD - loads generic peer, then casts:\nif (const auto peer = session().data().peerLoaded(peerId)\n\t; peer && peer->isUser()) {\n\tpeer->asUser()->setNoForwardFlags(\n\n// GOOD - look up the specific subtype directly:\nconst auto userId = peerToUser(peerId);\nif (const auto user = session().data().userLoaded(userId)) {\n\tuser->setNoForwardFlags(\n```\n\nAvoid 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:\n\n```cpp\n// BAD - complex if-with-initializer:\nif (const auto peer = session().data().peerLoaded(peerId)\n\t; peer && peer->isUser()) {\n\n// GOOD - simple nested ifs when direct lookup isn't available:\nif (const auto peer = session().data().peerLoaded(peerId)) {\n\tif (const auto user = peer->asUser()) {\n\n## Always initialize variables of basic types\n\nNever leave variables of basic types (`int`, `float`, `bool`, pointers, etc.) uninitialized. Custom types with constructors are fine — they initialize themselves. But for any basic type, always provide a default value (`= 0`, `= false`, `= nullptr`, etc.). This applies especially to class fields, where uninitialized members are a persistent source of bugs.\n\nThe only exception is performance-critical hot paths where you can prove no read-from-uninitialized-memory occurs. For class fields there is no such exception — always initialize.\n\n```cpp\n// BAD:\nint _bulletLeft;\nint _bulletTop;\nbool _expanded;\nSomeType *_pointer;\n\n// GOOD:\nint _bulletLeft = 0;\nint _bulletTop = 0;\nbool _expanded = false;\nSomeType *_pointer = nullptr;\n```\n\n## Use tr:: projections for TextWithEntities\n\nInside `tr::lng_...()` calls, always use the `tr::` projection helpers instead of their `Ui::Text::` equivalents. The `tr::` helpers are shorter and work uniformly as both placeholder wrappers and final projectors.\n\n| Instead of | Use |\n|---|---|\n| `Ui::Text::Bold(x)` | `tr::bold(x)` |\n| `Ui::Text::Italic(x)` | `tr::italic(x)` |\n| `Ui::Text::RichLangValue` | `tr::rich` |\n| `Ui::Text::WithEntities` | `tr::marked` |\n\n```cpp\n// BAD - verbose Ui::Text:: functions:\ntr::lng_some_key(\n    tr::now,\n    lt_name,\n    Ui::Text::Bold(name),\n    lt_group,\n    Ui::Text::Bold(group),\n    Ui::Text::RichLangValue)\n\n// GOOD - concise tr:: helpers:\ntr::lng_some_key(\n    tr::now,\n    lt_name,\n    tr::bold(name),\n    lt_group,\n    tr::bold(group),\n    tr::rich)\n```\n\nAlso use `tr::marked()` as the standard way to create `TextWithEntities` — not just as a projector:\n\n```cpp\n// BAD - verbose constructor:\nauto text = TextWithEntities();\nauto text = TextWithEntities{ u\"hello\"_q };\nauto text = TextWithEntities().append(u\"hello\"_q);\n\n// GOOD - concise:\nauto text = tr::marked();\nauto text = tr::marked(u\"hello\"_q);\n```\n\n## Multi-line calls — one argument per line\n\nWhen a function call doesn't fit on one line, put each argument on its own line. Don't group \"logical pairs\" on the same line — it creates inconsistent line lengths and makes diffs noisier.\n\n```cpp\n// BAD - pairs of arguments sharing lines:\ntr::lng_some_key(\n    tr::now,\n    lt_name, tr::bold(name),\n    lt_group, tr::bold(group),\n    tr::rich)\n\n// GOOD - one argument per line:\ntr::lng_some_key(\n    tr::now,\n    lt_name,\n    tr::bold(name),\n    lt_group,\n    tr::bold(group),\n    tr::rich)\n\n// Single-line is fine when everything fits:\nauto text = tr::lng_settings_title(tr::now);\n```\n\n## std::optional access — avoid value()\n\nDo 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.\n\n## Sort includes alphabetically, nested folders first\n\nAfter the file's own header, sort `#include` directives alphabetically with two special rules:\n\n1. **Nested folders before files** in the same directory — like Finder / File Explorer (folders first, then files). E.g. `ui/controls/button.h` sorts before `ui/abstract_button.h`.\n2. **Style includes (`styles/style_*.h`) always go last**, separated from the rest.\n\n```cpp\n// BAD - arbitrary order, style mixed in:\n#include \"media/audio/media_audio.h\"\n#include \"styles/style_media_player.h\"\n#include \"data/data_document.h\"\n#include \"apiwrap.h\"\n\n// GOOD - alphabetical, folders first, styles last:\n#include \"apiwrap.h\"\n#include \"data/data_document.h\"\n#include \"media/audio/media_audio.h\"\n\n#include \"styles/style_media_player.h\"\n```\n\n## Use C++17 nested namespace syntax\n\nUse `namespace A::B {` instead of nesting `namespace A { namespace B {`. The closing comment mirrors the opening: `} // namespace A::B`.\n\n```cpp\n// BAD - old-style nesting:\nnamespace Media {\nnamespace Player {\n...\n} // namespace Player\n} // namespace Media\n\n// GOOD - C++17 nested:\nnamespace Media::Player {\n...\n} // namespace Media::Player\n```\n\n## Merge consecutive branches with identical bodies\n\nWhen two or more consecutive `if` / `else if` branches execute the same code, combine their conditions into a single branch.\n\n```cpp\n// BAD - duplicated body:\nif (!document) {\n    finalize();\n    return;\n}\nif (!document->isSong()) {\n    finalize();\n    return;\n}\n\n// GOOD - combined:\nif (!document || !document->isSong()) {\n    finalize();\n    return;\n}\n```\n\n## Use base::take for read-and-reset\n\nWhen you need to read a variable's current value and reset it in one step, use `base::take(var)` instead of manually copying and clearing. `base::take` returns the old value and resets the variable to its default-constructed state.\n\n```cpp\n// BAD - manual read + reset:\nif (_playing) {\n    _listenedMs += crl::now() - _playStartedAt;\n    _playing = false;\n}\n\n// GOOD:\nif (base::take(_playing)) {\n    _listenedMs += crl::now() - _playStartedAt;\n}\n\n// BAD - copy fields then clear them one by one:\nconst auto document = _document;\nconst auto contextId = _contextId;\n_document = nullptr;\n_listenedMs = 0;\nif (!document) {\n    return;\n}\n\n// GOOD - take everything upfront, then validate:\nconst auto document = base::take(_document);\nconst auto contextId = base::take(_contextId);\nconst auto duration = static_cast<int>(base::take(_listenedMs) / 1000);\nif (!document || duration <= 0) {\n    return;\n}\n```\n\n## Don't wrap tr:: lang keys in rpl::single\n\n`tr::lng_*()` (without `tr::now`) already returns an `rpl::producer`. Wrapping a snapshot in `rpl::single()` defeats live language switching — the value is captured once and never updates. Just call the lang key without `tr::now`.\n\n```cpp\n// BAD - frozen snapshot, won't update on language change:\nrpl::single(tr::lng_ai_compose_title(tr::now))\n\n// GOOD - live producer that updates automatically:\ntr::lng_ai_compose_title()\n```\n\n## Extract method definitions from local classes\n\nWhen defining local classes (e.g. in anonymous namespaces), keep the class body compact — only declarations. Put all method definitions **after** all class definitions. This avoids unnecessary nesting inside the class body and keeps methods at the same indentation level as free functions.\n\n```cpp\n// BAD - methods defined inline, adding a nesting level:\nclass MyWidget final : public Ui::RpWidget {\npublic:\n    MyWidget(QWidget *parent)\n    : RpWidget(parent) {\n        // ... 20 lines of setup\n    }\n\n    void setActive(bool active) {\n        _active = active;\n        update();\n    }\n\nprotected:\n    void paintEvent(QPaintEvent *e) override {\n        // ... 30 lines of painting\n    }\n\nprivate:\n    bool _active = false;\n\n};\n\n// GOOD - class is a compact declaration, methods defined after:\nclass MyWidget final : public Ui::RpWidget {\npublic:\n    MyWidget(QWidget *parent, QString label);\n\n    void setActive(bool active);\n\nprotected:\n    void paintEvent(QPaintEvent *e) override;\n\nprivate:\n    bool _active = false;\n\n};\n\nMyWidget::MyWidget(QWidget *parent, QString label)\n: RpWidget(parent) {\n    // ... 20 lines of setup\n}\n\nvoid MyWidget::setActive(bool active) {\n    _active = active;\n    update();\n}\n\nvoid MyWidget::paintEvent(QPaintEvent *e) {\n    // ... 30 lines of painting\n}\n```\n\nWhen there are multiple local classes, put **all class definitions first**, then **all method definitions** after. This keeps the declarations readable as an overview.\n\n## Use RAII for resource cleanup\n\nWhen working with raw resources (Win32 HANDLEs, file descriptors, COM objects), use `gsl::finally` or a dedicated RAII wrapper for cleanup instead of calling release functions manually. Manual cleanup breaks when early returns are added later.\n\n```cpp\n// BAD - manual cleanup, fragile with early returns:\nconst auto snapshot = CreateToolhelp32Snapshot(...);\nif (snapshot != INVALID_HANDLE_VALUE) {\n\t// ... logic that might grow early returns ...\n\tCloseHandle(snapshot);\n}\n\n// GOOD - RAII guard, cleanup runs on any exit path:\nconst auto snapshot = CreateToolhelp32Snapshot(...);\nif (snapshot == INVALID_HANDLE_VALUE) {\n\treturn;\n}\nconst auto guard = gsl::finally([&] {\n\tCloseHandle(snapshot);\n});\n// ... logic, early returns are safe ...\n```\n\n## Extract substantial logic from lambdas\n\nWhen a lambda grows beyond a few lines of self-contained logic, extract it into a named function (free function in anonymous namespace, or a private method). Lambdas should primarily be glue — captures, dispatch, short transforms. This applies when the lambda's captures are minimal and can easily become function parameters. When a lambda captures many variables from its surrounding context, it may be cleaner to keep it inline.\n\n```cpp\n// BAD - substantial logic buried in a lambda:\ncrl::async([=] {\n\tauto found = false;\n\tauto pe = PROCESSENTRY32();\n\tpe.dwSize = sizeof(PROCESSENTRY32);\n\tconst auto snapshot = CreateToolhelp32Snapshot(...);\n\tif (snapshot != INVALID_HANDLE_VALUE) {\n\t\tfor (...) {\n\t\t\tif (/* match */) {\n\t\t\t\tfound = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tCloseHandle(snapshot);\n\t}\n\tcrl::on_main(weak, [=] { handle(found); });\n});\n\n// GOOD - logic extracted, lambda is just glue:\ncrl::async([=] {\n\tconst auto found = FindRunningReader();\n\tcrl::on_main(weak, [=] { handle(found); });\n});\n```\n\n## Data-driven matching over chained conditions\n\nWhen comparing a value against multiple known constants, store them in a collection and loop instead of chaining `||` conditions. Easier to extend, less repetition, and reads as data rather than logic.\n\n```cpp\n// BAD - repetitive chain, hard to extend:\nif (_wcsicmp(name, L\"Narrator.exe\") == 0\n\t|| _wcsicmp(name, L\"nvda.exe\") == 0\n\t|| _wcsicmp(name, L\"jfw.exe\") == 0\n\t|| _wcsicmp(name, L\"Zt.exe\") == 0) {\n\n// GOOD - data-driven, easy to extend:\nconst auto list = std::array{\n\tL\"Narrator.exe\",\n\tL\"nvda.exe\",\n\tL\"jfw.exe\",\n\tL\"Zt.exe\",\n};\nfor (const auto &entry : list) {\n\tif (_wcsicmp(name, entry) == 0) {\n\t\treturn true;\n\t}\n}\n```\n\n## Use !isHidden() for logic checks, not isVisible()\n\nWhen you call `show()` / `hide()` / `setVisible()` on a widget and later branch on that state, always check `!isHidden()` (the widget's own flag) — never `isVisible()`. `isVisible()` returns `true` only when the widget **and every ancestor** are visible, so it silently returns `false` during parent show-animations, before the parent is laid out, etc. `isHidden()` reflects exactly the flag you set.\n\n```cpp\n// BAD — breaks when parent is still animating / not yet shown:\nchild->setVisible(true);\n// ... later, in resizeGetHeight or similar:\nif (child->isVisible()) {   // false if parent isn't visible yet!\n    child->moveToRight(x, y, w);\n}\n\n// GOOD — checks the widget's own state:\nif (!child->isHidden()) {\n    child->moveToRight(x, y, w);\n}\n```\n\nThe same applies to any logic that depends on a previous `show()`/`hide()` call: skip blocks, layout branches, opacity decisions, etc.\n\n## Consolidate make_state calls into a single State struct\n\nEvery `make_state` is a separate heap allocation. When a function needs multiple pieces of lambda-captured mutable state, define a local `struct State` with all fields and call `make_state<State>()` once, then capture the resulting pointer everywhere.\n\n```cpp\n// BAD - two allocations:\nconst auto shown = lifetime.make_state<bool>(false);\nconst auto count = lifetime.make_state<int>(0);\n\n// GOOD - one allocation:\nstruct State {\n\tbool shown = false;\n\tint count = 0;\n};\nconst auto state = lifetime.make_state<State>();\n```\n\n## Use trailing return type when the return type doesn't fit on one line\n\nWhen a function's return type is long enough that the declaration would need a line break between the return type and the function name, use trailing return type syntax (`auto ... -> Type`) to keep the function name on the opening line.\n\n```cpp\n// BAD - return type orphaned on its own line:\nnot_null<HistoryView::Controls::ComposeAiButton*>\nSetupCaptionAiButton(SetupCaptionAiButtonArgs &&args);\n\n// GOOD - trailing return type keeps name visible:\nauto SetupCaptionAiButton(SetupCaptionAiButtonArgs &&args)\n-> not_null<HistoryView::Controls::ComposeAiButton*>;\n```\n\n## Mind data structure sizes and alignment\n\nWhen adding fields to a class or struct, consider the memory layout. A standalone `bool` between two pointer-sized fields wastes 7 bytes to alignment padding. Review new fields for packing opportunities:\n\n- If the struct already has bitfields, pack new boolean flags as `: 1` members rather than standalone `bool`.\n- If alignment leaves a gap (e.g., an `int` followed by a pointer), consider whether a new small field can fill it.\n- For classes instantiated in large quantities (per-message, per-element, per-row), every wasted byte is multiplied thousands of times.\n\n```cpp\n// BAD - standalone bool adds 8 bytes (1 + 7 padding) before the next pointer:\nmutable bool _myFlag = false;\nmutable std::unique_ptr<Foo> _foo;\n\n// GOOD - packed into existing bitfield group, no extra bytes:\nmutable uint32 _myFlag : 1 = 0;\n```\n\n## Static member functions use PascalCase\n\nNon-static member functions use camelCase (`startBatch`, `finalize`). Static member functions use PascalCase (`ShouldTrack`, `Parse`, `Create`), matching the convention for free functions.\n\n```cpp\n// BAD - camelCase for static method:\n[[nodiscard]] static bool shouldTrack(not_null<HistoryItem*> item);\n\n// GOOD - PascalCase for static method:\n[[nodiscard]] static bool ShouldTrack(not_null<HistoryItem*> item);\n```\n"
  },
  {
    "path": "Telegram/CMakeLists.txt",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nadd_executable(Telegram WIN32 MACOSX_BUNDLE)\ninit_non_host_target(Telegram)\n\nadd_subdirectory(lib_rpl)\nadd_subdirectory(lib_crl)\nadd_subdirectory(lib_base)\nadd_subdirectory(lib_ui)\nadd_subdirectory(lib_tl)\nadd_subdirectory(lib_spellcheck)\nadd_subdirectory(lib_storage)\nadd_subdirectory(lib_lottie)\nadd_subdirectory(lib_qr)\nadd_subdirectory(lib_translate)\nadd_subdirectory(lib_webrtc)\nadd_subdirectory(lib_webview)\nadd_subdirectory(codegen)\n\nget_filename_component(src_loc SourceFiles REALPATH)\nget_filename_component(res_loc Resources REALPATH)\n\ninclude(cmake/telegram_options.cmake)\ninclude(cmake/lib_ffmpeg.cmake)\ninclude(cmake/lib_stripe.cmake)\ninclude(cmake/lib_tgcalls.cmake)\ninclude(cmake/lib_prisma.cmake)\ninclude(cmake/td_export.cmake)\ninclude(cmake/td_iv.cmake)\ninclude(cmake/td_lang.cmake)\ninclude(cmake/td_mtproto.cmake)\ninclude(cmake/td_scheme.cmake)\ninclude(cmake/td_tde2e.cmake)\ninclude(cmake/td_ui.cmake)\ninclude(cmake/telegram_apple_swift_runtime.cmake)\ninclude(cmake/generate_appstream_changelog.cmake)\ninclude(cmake/td_forkgram.cmake)\n\nif (DESKTOP_APP_TEST_APPS)\n    include(cmake/tests.cmake)\nendif()\n\nif (WIN32)\n    include(cmake/generate_midl.cmake)\n    generate_midl(Telegram ${src_loc}\n        platform/win/windows_quiethours.idl\n        platform/win/windows_toastactivator.idl\n    )\nendif()\n\nset_target_properties(Telegram PROPERTIES AUTOMOC ON)\n\ntarget_link_libraries(Telegram\nPRIVATE\n    tdesktop::lib_tgcalls\n\n    # Order in this list defines the order of include paths in command line.\n    # We need to place desktop-app::external_minizip this early to have its\n    # include paths (usually ${PREFIX}/include/minizip) before any depend that\n    # would add ${PREFIX}/include. This path may have a different <zip.h>,\n    # for example installed by libzip (https://libzip.org).\n    desktop-app::external_minizip\n\n    tdesktop::td_export\n    tdesktop::td_iv\n    tdesktop::td_lang\n    tdesktop::td_mtproto\n    tdesktop::td_scheme\n    tdesktop::td_tde2e\n    tdesktop::td_ui\n    tdesktop::td_forkgram\n    desktop-app::lib_webrtc\n    desktop-app::lib_base\n    desktop-app::lib_crl\n    desktop-app::lib_ui\n    desktop-app::lib_tl\n    desktop-app::lib_spellcheck\n    desktop-app::lib_storage\n    desktop-app::lib_lottie\n    desktop-app::lib_qr\n    desktop-app::lib_translate\n    desktop-app::lib_webview\n    desktop-app::lib_ffmpeg\n    desktop-app::lib_stripe\n    desktop-app::external_rlottie\n    desktop-app::external_zlib\n    desktop-app::external_kcoreaddons\n    desktop-app::external_qt_static_plugins\n    desktop-app::external_qt\n    desktop-app::external_qr_code_generator\n    desktop-app::external_zbar\n    desktop-app::external_crash_reports\n    desktop-app::external_auto_updates\n    desktop-app::external_openssl\n    desktop-app::external_openal\n    desktop-app::external_xxhash\n)\n\nif (NOT DESKTOP_APP_DISABLE_SWIFT6)\n    telegram_add_apple_swift_runtime(Telegram)\nendif()\n\ntarget_precompile_headers(Telegram PRIVATE $<$<COMPILE_LANGUAGE:CXX,OBJCXX>:${src_loc}/stdafx.h>)\nnice_target_sources(Telegram ${src_loc}\nPRIVATE\n    ${style_files}\n\n    api/api_as_copy.cpp\n    api/api_as_copy.h\n    api/api_attached_stickers.cpp\n    api/api_attached_stickers.h\n    api/api_authorizations.cpp\n    api/api_authorizations.h\n    api/api_blocked_peers.cpp\n    api/api_blocked_peers.h\n    api/api_bot.cpp\n    api/api_bot.h\n    api/api_chat_filters.cpp\n    api/api_chat_filters.h\n    api/api_chat_filters_remove_manager.cpp\n    api/api_chat_filters_remove_manager.h\n    api/api_chat_invite.cpp\n    api/api_chat_invite.h\n    api/api_chat_links.cpp\n    api/api_chat_links.h\n    api/api_chat_participants.cpp\n    api/api_chat_participants.h\n    api/api_cloud_password.cpp\n    api/api_cloud_password.h\n    data/data_search_calendar.h\n    data/data_search_calendar.cpp\n    api/api_common.cpp\n    api/api_common.h\n    api/api_confirm_phone.cpp\n    api/api_confirm_phone.h\n    api/api_compose_with_ai.cpp\n    api/api_compose_with_ai.h\n    api/api_credits.cpp\n    api/api_credits.h\n    api/api_credits_history_entry.cpp\n    api/api_credits_history_entry.h\n    api/api_earn.cpp\n    api/api_earn.h\n    api/api_editing.cpp\n    api/api_editing.h\n    api/api_filter_updates.h\n    api/api_global_privacy.cpp\n    api/api_global_privacy.h\n    api/api_hash.cpp\n    api/api_hash.h\n    api/api_invite_links.cpp\n    api/api_invite_links.h\n    api/api_media.cpp\n    api/api_media.h\n    api/api_messages_search.cpp\n    api/api_messages_search.h\n    api/api_messages_search_merged.cpp\n    api/api_messages_search_merged.h\n    api/api_peer_colors.cpp\n    api/api_peer_colors.h\n    api/api_peer_photo.cpp\n    api/api_peer_photo.h\n    api/api_peer_search.cpp\n    api/api_peer_search.h\n    api/api_polls.cpp\n    api/api_polls.h\n    api/api_premium.cpp\n    api/api_premium.h\n    api/api_premium_option.cpp\n    api/api_premium_option.h\n    api/api_reactions_notify_settings.cpp\n    api/api_reactions_notify_settings.h\n    api/api_read_metrics.cpp\n    api/api_read_metrics.h\n    api/api_report.cpp\n    api/api_report.h\n    api/api_ringtones.cpp\n    api/api_ringtones.h\n    api/api_self_destruct.cpp\n    api/api_self_destruct.h\n    api/api_send_progress.cpp\n    api/api_send_progress.h\n    api/api_sending.cpp\n    api/api_sending.h\n    api/api_sensitive_content.cpp\n    api/api_sensitive_content.h\n    api/api_single_message_search.cpp\n    api/api_single_message_search.h\n    api/api_statistics.cpp\n    api/api_statistics.h\n    api/api_statistics_data_deserialize.cpp\n    api/api_statistics_data_deserialize.h\n    api/api_statistics_sender.cpp\n    api/api_statistics_sender.h\n    api/api_suggest_post.cpp\n    api/api_suggest_post.h\n    api/api_text_entities.cpp\n    api/api_text_entities.h\n    api/api_todo_lists.cpp\n    api/api_todo_lists.h\n    api/api_toggling_media.cpp\n    api/api_toggling_media.h\n    api/api_transcribes.cpp\n    api/api_transcribes.h\n    api/api_unread_things.cpp\n    api/api_unread_things.h\n    api/api_updates.cpp\n    api/api_updates.h\n    api/api_user_names.cpp\n    api/api_user_names.h\n    api/api_user_privacy.cpp\n    api/api_user_privacy.h\n    api/api_views.cpp\n    api/api_views.h\n    api/api_websites.cpp\n    api/api_websites.h\n    api/api_who_reacted.cpp\n    api/api_who_reacted.h\n    boxes/filters/edit_filter_box.cpp\n    boxes/filters/edit_filter_box.h\n    boxes/filters/edit_filter_chats_list.cpp\n    boxes/filters/edit_filter_chats_list.h\n    boxes/filters/edit_filter_chats_preview.cpp\n    boxes/filters/edit_filter_chats_preview.h\n    boxes/filters/edit_filter_links.cpp\n    boxes/filters/edit_filter_links.h\n    boxes/peers/add_bot_to_chat_box.cpp\n    boxes/peers/add_bot_to_chat_box.h\n    boxes/peers/add_participants_box.cpp\n    boxes/peers/add_participants_box.h\n    boxes/peers/channel_ownership_transfer.cpp\n    boxes/peers/channel_ownership_transfer.h\n    boxes/peers/choose_peer_box.cpp\n    boxes/peers/choose_peer_box.h\n    boxes/peers/create_managed_bot_box.cpp\n    boxes/peers/create_managed_bot_box.h\n    boxes/peers/edit_contact_box.cpp\n    boxes/peers/edit_contact_box.h\n    boxes/peers/edit_forum_topic_box.cpp\n    boxes/peers/edit_forum_topic_box.h\n    boxes/peers/edit_discussion_link_box.cpp\n    boxes/peers/edit_discussion_link_box.h\n    boxes/peers/edit_members_visible.cpp\n    boxes/peers/edit_members_visible.h\n    boxes/peers/edit_participant_box.cpp\n    boxes/peers/edit_participant_box.h\n    boxes/peers/edit_tag_control.cpp\n    boxes/peers/edit_tag_control.h\n    boxes/peers/edit_participants_box.cpp\n    boxes/peers/edit_participants_box.h\n    boxes/peers/edit_peer_color_box.cpp\n    boxes/peers/edit_peer_color_box.h\n    boxes/peers/edit_peer_common.h\n    boxes/peers/edit_peer_info_box.cpp\n    boxes/peers/edit_peer_info_box.h\n    boxes/peers/edit_peer_invite_link.cpp\n    boxes/peers/edit_peer_invite_link.h\n    boxes/peers/edit_peer_invite_links.cpp\n    boxes/peers/edit_peer_invite_links.h\n    boxes/peers/edit_peer_permissions_box.cpp\n    boxes/peers/edit_peer_permissions_box.h\n    boxes/peers/edit_peer_reactions.cpp\n    boxes/peers/edit_peer_reactions.h\n    boxes/peers/edit_peer_requests_box.cpp\n    boxes/peers/edit_peer_requests_box.h\n    boxes/peers/edit_peer_type_box.cpp\n    boxes/peers/edit_peer_type_box.h\n    boxes/peers/edit_peer_usernames_list.cpp\n    boxes/peers/edit_peer_usernames_list.h\n    boxes/peers/peer_short_info_box.cpp\n    boxes/peers/peer_short_info_box.h\n    boxes/peers/prepare_short_info_box.cpp\n    boxes/peers/prepare_short_info_box.h\n    boxes/peers/replace_boost_box.cpp\n    boxes/peers/replace_boost_box.h\n    boxes/peers/tag_info_box.cpp\n    boxes/peers/tag_info_box.h\n    boxes/peers/verify_peers_box.cpp\n    boxes/peers/verify_peers_box.h\n    boxes/about_box.cpp\n    boxes/about_box.h\n    boxes/about_sponsored_box.cpp\n    boxes/about_sponsored_box.h\n    boxes/abstract_box.cpp\n    boxes/abstract_box.h\n    boxes/add_contact_box.cpp\n    boxes/add_contact_box.h\n    boxes/auto_lock_box.cpp\n    boxes/auto_lock_box.h\n    boxes/auto_download_box.cpp\n    boxes/auto_download_box.h\n    boxes/background_box.cpp\n    boxes/background_box.h\n    boxes/background_preview_box.cpp\n    boxes/background_preview_box.h\n    boxes/choose_filter_box.cpp\n    boxes/choose_filter_box.h\n    boxes/connection_box.cpp\n    boxes/connection_box.h\n    boxes/create_poll_box.cpp\n    boxes/create_poll_box.h\n    boxes/delete_messages_box.cpp\n    boxes/delete_messages_box.h\n    boxes/dictionaries_manager.cpp\n    boxes/dictionaries_manager.h\n    boxes/download_path_box.cpp\n    boxes/download_path_box.h\n    boxes/edit_caption_box.cpp\n    boxes/edit_caption_box.h\n    boxes/edit_privacy_box.cpp\n    boxes/edit_privacy_box.h\n    boxes/edit_todo_list_box.cpp\n    boxes/edit_todo_list_box.h\n    boxes/gift_credits_box.cpp\n    boxes/gift_credits_box.h\n    boxes/gift_premium_box.cpp\n    boxes/gift_premium_box.h\n    boxes/language_box.cpp\n    boxes/language_box.h\n    boxes/local_storage_box.cpp\n    boxes/local_storage_box.h\n    boxes/max_invite_box.cpp\n    boxes/max_invite_box.h\n    boxes/moderate_messages_box.cpp\n    boxes/moderate_messages_box.h\n    boxes/peer_list_box.cpp\n    boxes/peer_list_box.h\n    boxes/peer_list_controllers.cpp\n    boxes/peer_list_controllers.h\n    boxes/peer_list_widgets.cpp\n    boxes/peer_list_widgets.h\n    boxes/peer_lists_box.cpp\n    boxes/peer_lists_box.h\n    boxes/passcode_box.cpp\n    boxes/passcode_box.h\n    boxes/phone_banned_box.cpp\n    boxes/phone_banned_box.h\n    boxes/pin_messages_box.cpp\n    boxes/pin_messages_box.h\n    boxes/premium_limits_box.cpp\n    boxes/premium_limits_box.h\n    boxes/premium_preview_box.cpp\n    boxes/premium_preview_box.h\n    boxes/reactions_settings_box.cpp\n    boxes/reactions_settings_box.h\n    boxes/report_messages_box.cpp\n    boxes/report_messages_box.h\n    boxes/ringtones_box.cpp\n    boxes/ringtones_box.h\n    boxes/select_future_owner_box.cpp\n    boxes/select_future_owner_box.h\n    boxes/self_destruction_box.cpp\n    boxes/self_destruction_box.h\n    boxes/send_credits_box.cpp\n    boxes/send_credits_box.h\n    boxes/send_gif_with_caption_box.cpp\n    boxes/send_gif_with_caption_box.h\n    boxes/send_files_box.cpp\n    boxes/send_files_box.h\n    boxes/share_box.cpp\n    boxes/share_box.h\n    boxes/star_gift_auction_box.cpp\n    boxes/star_gift_auction_box.h\n    boxes/star_gift_box.cpp\n    boxes/star_gift_box.h\n    boxes/star_gift_cover_box.cpp\n    boxes/star_gift_cover_box.h\n    boxes/star_gift_craft_animation.cpp\n    boxes/star_gift_craft_animation.h\n    boxes/star_gift_craft_box.cpp\n    boxes/star_gift_craft_box.h\n    boxes/star_gift_preview_box.cpp\n    boxes/star_gift_preview_box.h\n    boxes/star_gift_resale_box.cpp\n    boxes/star_gift_resale_box.h\n    boxes/sticker_set_box.cpp\n    boxes/sticker_set_box.h\n    boxes/stickers_box.cpp\n    boxes/stickers_box.h\n    boxes/transfer_gift_box.cpp\n    boxes/transfer_gift_box.h\n    boxes/compose_ai_box.cpp\n    boxes/compose_ai_box.h\n    boxes/translate_box.cpp\n    boxes/translate_box.h\n    boxes/url_auth_box.cpp\n    boxes/url_auth_box.h\n    boxes/username_box.cpp\n    boxes/username_box.h\n    calls/group/calls_choose_join_as.cpp\n    calls/group/calls_choose_join_as.h\n    calls/group/calls_cover_item.cpp\n    calls/group/calls_cover_item.h\n    calls/group/calls_group_call.cpp\n    calls/group/calls_group_call.h\n    calls/group/calls_group_common.cpp\n    calls/group/calls_group_common.h\n    calls/group/calls_group_invite_controller.cpp\n    calls/group/calls_group_invite_controller.h\n    calls/group/calls_group_members.cpp\n    calls/group/calls_group_members.h\n    calls/group/calls_group_members_row.cpp\n    calls/group/calls_group_members_row.h\n    calls/group/calls_group_menu.cpp\n    calls/group/calls_group_menu.h\n    calls/group/calls_group_message_encryption.cpp\n    calls/group/calls_group_message_encryption.h\n    calls/group/calls_group_message_field.cpp\n    calls/group/calls_group_message_field.h\n    calls/group/calls_group_messages.cpp\n    calls/group/calls_group_messages.h\n    calls/group/calls_group_messages_ui.cpp\n    calls/group/calls_group_messages_ui.h\n    calls/group/calls_group_panel.cpp\n    calls/group/calls_group_panel.h\n    calls/group/calls_group_rtmp.cpp\n    calls/group/calls_group_rtmp.h\n    calls/group/calls_group_settings.cpp\n    calls/group/calls_group_settings.h\n    calls/group/calls_group_stars_box.cpp\n    calls/group/calls_group_stars_box.h\n    calls/group/calls_group_toasts.cpp\n    calls/group/calls_group_toasts.h\n    calls/group/calls_group_viewport.cpp\n    calls/group/calls_group_viewport.h\n    calls/group/calls_group_viewport_opengl.cpp\n    calls/group/calls_group_viewport_opengl.h\n    calls/group/calls_group_viewport_raster.cpp\n    calls/group/calls_group_viewport_raster.h\n    calls/group/calls_group_viewport_rhi.cpp\n    calls/group/calls_group_viewport_rhi.h\n    calls/group/calls_group_viewport_tile.cpp\n    calls/group/calls_group_viewport_tile.h\n    calls/group/calls_volume_item.cpp\n    calls/group/calls_volume_item.h\n    calls/calls_box_controller.cpp\n    calls/calls_box_controller.h\n    calls/calls_call.cpp\n    calls/calls_call.h\n    calls/calls_emoji_fingerprint.cpp\n    calls/calls_emoji_fingerprint.h\n    calls/calls_instance.cpp\n    calls/calls_instance.h\n    calls/calls_panel.cpp\n    calls/calls_panel.h\n    calls/calls_panel_background.cpp\n    calls/calls_panel_background.h\n    calls/calls_signal_bars.cpp\n    calls/calls_signal_bars.h\n    calls/calls_top_bar.cpp\n    calls/calls_top_bar.h\n    calls/calls_userpic.cpp\n    calls/calls_userpic.h\n    calls/calls_video_bubble.cpp\n    calls/calls_video_bubble.h\n    calls/calls_video_incoming.cpp\n    calls/calls_video_incoming.h\n    calls/calls_window.cpp\n    calls/calls_window.h\n    chat_helpers/compose/compose_features.h\n    chat_helpers/compose/compose_show.cpp\n    chat_helpers/compose/compose_show.h\n    chat_helpers/bot_command.cpp\n    chat_helpers/bot_command.h\n    chat_helpers/bot_keyboard.cpp\n    chat_helpers/bot_keyboard.h\n    chat_helpers/emoji_interactions.cpp\n    chat_helpers/emoji_interactions.h\n    chat_helpers/emoji_keywords.cpp\n    chat_helpers/emoji_keywords.h\n    chat_helpers/emoji_list_widget.cpp\n    chat_helpers/emoji_list_widget.h\n    chat_helpers/emoji_sets_manager.cpp\n    chat_helpers/emoji_sets_manager.h\n    chat_helpers/emoji_suggestions_widget.cpp\n    chat_helpers/emoji_suggestions_widget.h\n    chat_helpers/field_autocomplete.cpp\n    chat_helpers/field_autocomplete.h\n    chat_helpers/gifs_list_widget.cpp\n    chat_helpers/gifs_list_widget.h\n    chat_helpers/message_field.cpp\n    chat_helpers/message_field.h\n    chat_helpers/share_message_phrase_factory.cpp\n    chat_helpers/share_message_phrase_factory.h\n    chat_helpers/spellchecker_common.cpp\n    chat_helpers/spellchecker_common.h\n    chat_helpers/stickers_dice_pack.cpp\n    chat_helpers/stickers_dice_pack.h\n    chat_helpers/stickers_emoji_pack.cpp\n    chat_helpers/stickers_emoji_pack.h\n    chat_helpers/stickers_gift_box_pack.cpp\n    chat_helpers/stickers_gift_box_pack.h\n    chat_helpers/stickers_list_footer.cpp\n    chat_helpers/stickers_list_footer.h\n    chat_helpers/stickers_list_widget.cpp\n    chat_helpers/stickers_list_widget.h\n    chat_helpers/stickers_lottie.cpp\n    chat_helpers/stickers_lottie.h\n    chat_helpers/tabbed_panel.cpp\n    chat_helpers/tabbed_panel.h\n    chat_helpers/tabbed_section.cpp\n    chat_helpers/tabbed_section.h\n    chat_helpers/tabbed_selector.cpp\n    chat_helpers/tabbed_selector.h\n    chat_helpers/ttl_media_layer_widget.cpp\n    chat_helpers/ttl_media_layer_widget.h\n    core/application.cpp\n    core/application.h\n    core/cached_webview_availability.h\n    core/bank_card_click_handler.cpp\n    core/bank_card_click_handler.h\n    core/base_integration.cpp\n    core/base_integration.h\n    core/changelogs.cpp\n    core/changelogs.h\n    core/click_handler_types.cpp\n    core/click_handler_types.h\n    core/core_cloud_password.cpp\n    core/core_cloud_password.h\n    core/core_settings.cpp\n    core/core_settings.h\n    core/fork_settings.cpp\n    core/fork_settings.h\n    core/core_settings_proxy.cpp\n    core/core_settings_proxy.h\n    core/crash_report_window.cpp\n    core/crash_report_window.h\n    core/crash_reports.cpp\n    core/crash_reports.h\n    core/credits_amount.h\n    core/deadlock_detector.h\n    core/deep_links/deep_links_chats.cpp\n    core/deep_links/deep_links_chats.h\n    core/deep_links/deep_links_contacts.cpp\n    core/deep_links/deep_links_contacts.h\n    core/deep_links/deep_links_new.cpp\n    core/deep_links/deep_links_new.h\n    core/deep_links/deep_links_router.cpp\n    core/deep_links/deep_links_router.h\n    core/deep_links/deep_links_settings.cpp\n    core/deep_links/deep_links_settings.h\n    core/deep_links/deep_links_types.h\n    core/file_utilities.cpp\n    core/file_utilities.h\n    core/launcher.cpp\n    core/launcher.h\n    core/local_url_handlers.cpp\n    core/local_url_handlers.h\n    core/phone_click_handler.cpp\n    core/phone_click_handler.h\n    core/sandbox.cpp\n    core/sandbox.h\n    core/shortcuts.cpp\n    core/shortcuts.h\n    core/ui_integration.cpp\n    core/ui_integration.h\n    core/update_checker.cpp\n    core/update_checker.h\n    core/utils.cpp\n    core/utils.h\n    core/version.h\n    countries/countries_manager.cpp\n    countries/countries_manager.h\n    data/business/data_business_chatbots.cpp\n    data/business/data_business_chatbots.h\n    data/business/data_business_common.cpp\n    data/business/data_business_common.h\n    data/business/data_business_info.cpp\n    data/business/data_business_info.h\n    data/business/data_shortcut_messages.cpp\n    data/business/data_shortcut_messages.h\n    data/components/credits.cpp\n    data/components/credits.h\n    data/components/factchecks.cpp\n    data/components/factchecks.h\n    data/components/gift_auctions.cpp\n    data/components/gift_auctions.h\n    data/components/location_pickers.cpp\n    data/components/location_pickers.h\n    data/components/passkeys.cpp\n    data/components/passkeys.h\n    data/components/promo_suggestions.cpp\n    data/components/promo_suggestions.h\n    data/components/recent_peers.cpp\n    data/components/recent_peers.h\n    data/components/recent_shared_media_gifts.cpp\n    data/components/recent_shared_media_gifts.h\n    data/components/scheduled_messages.cpp\n    data/components/scheduled_messages.h\n    data/components/sponsored_messages.cpp\n    data/components/sponsored_messages.h\n    data/components/top_peers.cpp\n    data/components/top_peers.h\n    data/notify/data_notify_settings.cpp\n    data/notify/data_notify_settings.h\n    data/notify/data_peer_notify_settings.cpp\n    data/notify/data_peer_notify_settings.h\n    data/notify/data_peer_notify_volume.cpp\n    data/notify/data_peer_notify_volume.h\n    data/stickers/data_custom_emoji.cpp\n    data/stickers/data_custom_emoji.h\n    data/stickers/data_stickers_set.cpp\n    data/stickers/data_stickers_set.h\n    data/stickers/data_stickers.cpp\n    data/stickers/data_stickers.h\n    data/data_abstract_sparse_ids.h\n    data/data_abstract_structure.cpp\n    data/data_abstract_structure.h\n    data/data_audio_msg_id.cpp\n    data/data_audio_msg_id.h\n    data/data_auto_download.cpp\n    data/data_auto_download.h\n    data/data_boosts.h\n    data/data_bot_app.cpp\n    data/data_bot_app.h\n    data/data_chat.cpp\n    data/data_chat.h\n    data/data_chat_filters.cpp\n    data/data_chat_filters.h\n    data/data_chat_participant_status.cpp\n    data/data_chat_participant_status.h\n    data/data_changes.cpp\n    data/data_changes.h\n    data/data_channel.cpp\n    data/data_channel.h\n    data/data_channel_admins.cpp\n    data/data_channel_admins.h\n    data/data_cloud_file.cpp\n    data/data_cloud_file.h\n    data/data_cloud_themes.cpp\n    data/data_cloud_themes.h\n    data/data_document.cpp\n    data/data_document.h\n    data/data_document_media.cpp\n    data/data_document_media.h\n    data/data_document_resolver.cpp\n    data/data_document_resolver.h\n    data/data_download_manager.cpp\n    data/data_download_manager.h\n    data/data_drafts.cpp\n    data/data_drafts.h\n    data/data_emoji_statuses.cpp\n    data/data_emoji_statuses.h\n    data/data_folder.cpp\n    data/data_folder.h\n    data/data_forum.cpp\n    data/data_forum.h\n    data/data_forum_icons.cpp\n    data/data_forum_icons.h\n    data/data_forum_topic.cpp\n    data/data_forum_topic.h\n    data/data_file_click_handler.cpp\n    data/data_file_click_handler.h\n    data/data_file_origin.cpp\n    data/data_file_origin.h\n    data/data_flags.h\n    data/data_game.cpp\n    data/data_game.h\n    data/data_group_call.cpp\n    data/data_group_call.h\n    data/data_groups.cpp\n    data/data_groups.h\n    data/data_histories.cpp\n    data/data_histories.h\n    data/data_history_messages.cpp\n    data/data_history_messages.h\n    data/data_lastseen_status.h\n    data/data_location.cpp\n    data/data_location.h\n    data/data_media_preload.cpp\n    data/data_media_preload.h\n    data/data_media_rotation.cpp\n    data/data_media_rotation.h\n    data/data_media_types.cpp\n    data/data_media_types.h\n    # data/data_messages.cpp\n    # data/data_messages.h\n    data/data_message_reaction_id.cpp\n    data/data_message_reaction_id.h\n    data/data_message_reactions.cpp\n    data/data_message_reactions.h\n    data/data_msg_id.h\n    data/data_peer.cpp\n    data/data_peer.h\n    data/data_peer_bot_command.cpp\n    data/data_peer_bot_command.h\n    data/data_peer_bot_commands.cpp\n    data/data_peer_bot_commands.h\n    data/data_peer_common.h\n    data/data_peer_id.cpp\n    data/data_peer_id.h\n    data/data_peer_values.cpp\n    data/data_peer_values.h\n    data/data_photo.cpp\n    data/data_photo.h\n    data/data_photo_media.cpp\n    data/data_photo_media.h\n    data/data_poll.cpp\n    data/data_poll.h\n    data/data_poll_messages.cpp\n    data/data_poll_messages.h\n    data/data_premium_limits.cpp\n    data/data_premium_limits.h\n    data/data_pts_waiter.cpp\n    data/data_pts_waiter.h\n    data/data_replies_list.cpp\n    data/data_replies_list.h\n    data/data_reply_preview.cpp\n    data/data_reply_preview.h\n    data/data_report.h\n    data/data_saved_messages.cpp\n    data/data_saved_messages.h\n    data/data_saved_music.cpp\n    data/data_saved_music.h\n    data/data_saved_sublist.cpp\n    data/data_saved_sublist.h\n    data/data_search_controller.cpp\n    data/data_search_controller.h\n    data/data_send_action.cpp\n    data/data_send_action.h\n    data/data_session.cpp\n    data/data_session.h\n    data/data_shared_media.cpp\n    data/data_shared_media.h\n    data/data_sparse_ids.cpp\n    data/data_sparse_ids.h\n    data/data_star_gift.cpp\n    data/data_star_gift.h\n    data/data_statistics.h\n    data/data_stories.cpp\n    data/data_stories.h\n    data/data_stories_ids.cpp\n    data/data_stories_ids.h\n    data/data_story.cpp\n    data/data_story.h\n    data/data_streaming.cpp\n    data/data_streaming.h\n    data/data_thread.cpp\n    data/data_thread.h\n    data/data_todo_list.cpp\n    data/data_todo_list.h\n    data/data_types.cpp\n    data/data_types.h\n    data/data_unread_value.cpp\n    data/data_unread_value.h\n    data/data_user.cpp\n    data/data_user.h\n    data/data_user_photos.cpp\n    data/data_user_photos.h\n    data/data_user_names.cpp\n    data/data_user_names.h\n    data/data_wall_paper.cpp\n    data/data_wall_paper.h\n    data/data_web_page.cpp\n    data/data_web_page.h\n    dialogs/ui/dialogs_layout.cpp\n    dialogs/ui/dialogs_layout.h\n    dialogs/ui/dialogs_message_view.cpp\n    dialogs/ui/dialogs_message_view.h\n    dialogs/ui/dialogs_stories_content.cpp\n    dialogs/ui/dialogs_stories_content.h\n    dialogs/ui/dialogs_suggestions.cpp\n    dialogs/ui/dialogs_suggestions.h\n    dialogs/ui/dialogs_topics_view.cpp\n    dialogs/ui/dialogs_topics_view.h\n    dialogs/ui/dialogs_video_userpic.cpp\n    dialogs/ui/dialogs_video_userpic.h\n    dialogs/dialogs_entry.cpp\n    dialogs/dialogs_entry.h\n    dialogs/dialogs_indexed_list.cpp\n    dialogs/dialogs_indexed_list.h\n    dialogs/dialogs_inner_widget.cpp\n    dialogs/dialogs_inner_widget.h\n    dialogs/dialogs_key.cpp\n    dialogs/dialogs_key.h\n    dialogs/dialogs_list.cpp\n    dialogs/dialogs_list.h\n    dialogs/dialogs_main_list.cpp\n    dialogs/dialogs_main_list.h\n    dialogs/dialogs_pinned_list.cpp\n    dialogs/dialogs_pinned_list.h\n    dialogs/dialogs_quick_action.cpp\n    dialogs/dialogs_quick_action.h\n    dialogs/dialogs_row.cpp\n    dialogs/dialogs_row.h\n    dialogs/dialogs_search_from_controllers.cpp\n    dialogs/dialogs_search_from_controllers.h\n    dialogs/dialogs_search_tags.cpp\n    dialogs/dialogs_search_tags.h\n    dialogs/dialogs_search_posts.cpp\n    dialogs/dialogs_search_posts.h\n    dialogs/dialogs_top_bar_suggestion.cpp\n    dialogs/dialogs_top_bar_suggestion.h\n    dialogs/dialogs_widget.cpp\n    dialogs/dialogs_widget.h\n    editor/color_picker.cpp\n    editor/color_picker.h\n    editor/controllers/controllers.h\n    editor/controllers/stickers_panel_controller.cpp\n    editor/controllers/stickers_panel_controller.h\n    editor/editor_paint.cpp\n    editor/editor_paint.h\n    editor/photo_editor.cpp\n    editor/photo_editor.h\n    editor/photo_editor_content.cpp\n    editor/photo_editor_content.h\n    editor/photo_editor_controls.cpp\n    editor/photo_editor_controls.h\n    editor/photo_editor_layer_widget.cpp\n    editor/photo_editor_layer_widget.h\n    editor/scene/scene_item_sticker.cpp\n    editor/scene/scene_item_sticker.h\n    export/export_manager.cpp\n    export/export_manager.h\n    export/view/export_view_content.cpp\n    export/view/export_view_content.h\n    export/view/export_view_panel_controller.cpp\n    export/view/export_view_panel_controller.h\n    export/view/export_view_progress.cpp\n    export/view/export_view_progress.h\n    export/view/export_view_settings.cpp\n    export/view/export_view_settings.h\n    export/view/export_view_top_bar.cpp\n    export/view/export_view_top_bar.h\n    forkgram/gpu_demo_renderer.cpp\n    forkgram/gpu_demo_renderer.h\n    forkgram/uri_menu.cpp\n    forkgram/uri_menu.h\n    history/admin_log/history_admin_log_filter.cpp\n    history/admin_log/history_admin_log_filter.h\n    history/admin_log/history_admin_log_inner.cpp\n    history/admin_log/history_admin_log_inner.h\n    history/admin_log/history_admin_log_item.cpp\n    history/admin_log/history_admin_log_item.h\n    history/admin_log/history_admin_log_section.cpp\n    history/admin_log/history_admin_log_section.h\n    history/view/controls/compose_controls_common.h\n    history/view/controls/history_view_compose_ai_button.cpp\n    history/view/controls/history_view_compose_ai_button.h\n    history/view/controls/history_view_compose_ai_tooltip.cpp\n    history/view/controls/history_view_compose_ai_tooltip.h\n    history/view/controls/history_view_compose_controls.cpp\n    history/view/controls/history_view_compose_controls.h\n    history/view/controls/history_view_compose_media_edit_manager.cpp\n    history/view/controls/history_view_compose_media_edit_manager.h\n    history/view/controls/history_view_compose_search.cpp\n    history/view/controls/history_view_compose_search.h\n    history/view/controls/history_view_draft_options.cpp\n    history/view/controls/history_view_draft_options.h\n    history/view/controls/history_view_forward_panel.cpp\n    history/view/controls/history_view_forward_panel.h\n    history/view/controls/history_view_suggest_options.cpp\n    history/view/controls/history_view_suggest_options.h\n    history/view/controls/history_view_ttl_button.cpp\n    history/view/controls/history_view_ttl_button.h\n    history/view/controls/history_view_voice_record_bar.cpp\n    history/view/controls/history_view_voice_record_bar.h\n    history/view/controls/history_view_webpage_processor.cpp\n    history/view/controls/history_view_webpage_processor.h\n    history/view/media/history_view_birthday_suggestion.cpp\n    history/view/media/history_view_birthday_suggestion.h\n    history/view/media/history_view_call.cpp\n    history/view/media/history_view_call.h\n    history/view/media/history_view_contact.cpp\n    history/view/media/history_view_contact.h\n    history/view/media/history_view_custom_emoji.cpp\n    history/view/media/history_view_custom_emoji.h\n    history/view/media/history_view_dice.cpp\n    history/view/media/history_view_dice.h\n    history/view/media/history_view_document.cpp\n    history/view/media/history_view_document.h\n    history/view/media/history_view_file.cpp\n    history/view/media/history_view_file.h\n    history/view/media/history_view_game.cpp\n    history/view/media/history_view_game.h\n    history/view/media/history_view_gif.cpp\n    history/view/media/history_view_gif.h\n    history/view/media/history_view_giveaway.cpp\n    history/view/media/history_view_giveaway.h\n    history/view/media/history_view_invoice.cpp\n    history/view/media/history_view_invoice.h\n    history/view/media/history_view_large_emoji.cpp\n    history/view/media/history_view_large_emoji.h\n    history/view/media/history_view_location.cpp\n    history/view/media/history_view_location.h\n    history/view/media/history_view_media.cpp\n    history/view/media/history_view_media.h\n    history/view/media/history_view_media_common.cpp\n    history/view/media/history_view_media_common.h\n    history/view/media/history_view_media_generic.cpp\n    history/view/media/history_view_media_generic.h\n    history/view/media/history_view_media_grouped.cpp\n    history/view/media/history_view_media_grouped.h\n    history/view/media/history_view_media_spoiler.cpp\n    history/view/media/history_view_media_spoiler.h\n    history/view/media/history_view_media_unwrapped.cpp\n    history/view/media/history_view_media_unwrapped.h\n    history/view/media/history_view_no_forwards_request.cpp\n    history/view/media/history_view_no_forwards_request.h\n    history/view/media/history_view_photo.cpp\n    history/view/media/history_view_photo.h\n    history/view/media/history_view_poll.cpp\n    history/view/media/history_view_poll.h\n    history/view/media/menu/history_view_poll_menu.cpp\n    history/view/media/menu/history_view_poll_menu.h\n    history/view/media/history_view_premium_gift.cpp\n    history/view/media/history_view_premium_gift.h\n    history/view/media/history_view_save_document_action.cpp\n    history/view/media/history_view_save_document_action.h\n    history/view/media/history_view_service_box.cpp\n    history/view/media/history_view_service_box.h\n    history/view/media/history_view_similar_channels.cpp\n    history/view/media/history_view_similar_channels.h\n    history/view/media/history_view_slot_machine.cpp\n    history/view/media/history_view_slot_machine.h\n    history/view/media/history_view_sticker.cpp\n    history/view/media/history_view_sticker.h\n    history/view/media/history_view_sticker_player.cpp\n    history/view/media/history_view_sticker_player.h\n    history/view/media/history_view_sticker_player_abstract.h\n    history/view/media/history_view_story_mention.cpp\n    history/view/media/history_view_story_mention.h\n    history/view/media/history_view_suggest_decision.cpp\n    history/view/media/history_view_suggest_decision.h\n    history/view/media/history_view_theme_document.cpp\n    history/view/media/history_view_theme_document.h\n    history/view/media/history_view_todo_list.cpp\n    history/view/media/history_view_todo_list.h\n    history/view/media/history_view_unique_gift.cpp\n    history/view/media/history_view_unique_gift.h\n    history/view/media/history_view_userpic_suggestion.cpp\n    history/view/media/history_view_userpic_suggestion.h\n    history/view/media/history_view_web_page.cpp\n    history/view/media/history_view_web_page.h\n    history/view/reactions/history_view_reactions.cpp\n    history/view/reactions/history_view_reactions.h\n    history/view/reactions/history_view_reactions_button.cpp\n    history/view/reactions/history_view_reactions_button.h\n    history/view/reactions/history_view_reactions_list.cpp\n    history/view/reactions/history_view_reactions_list.h\n    history/view/reactions/history_view_reactions_selector.cpp\n    history/view/reactions/history_view_reactions_selector.h\n    history/view/reactions/history_view_reactions_strip.cpp\n    history/view/reactions/history_view_reactions_strip.h\n    history/view/reactions/history_view_reactions_tabs.cpp\n    history/view/reactions/history_view_reactions_tabs.h\n    history/view/history_view_top_peers_selector.cpp\n    history/view/history_view_top_peers_selector.h\n    history/view/history_view_about_view.cpp\n    history/view/history_view_about_view.h\n    history/view/history_view_bottom_info.cpp\n    history/view/history_view_bottom_info.h\n    history/view/history_view_chat_preview.cpp\n    history/view/history_view_chat_preview.h\n    history/view/history_view_chat_section.cpp\n    history/view/history_view_chat_section.h\n    history/view/history_view_contact_status.cpp\n    history/view/history_view_contact_status.h\n    history/view/history_view_context_menu.cpp\n    history/view/history_view_context_menu.h\n    history/view/history_view_context_menu_fork.cpp\n    history/view/history_view_context_menu_fork.h\n    history/view/history_view_corner_buttons.cpp\n    history/view/history_view_corner_buttons.h\n    history/view/history_view_cursor_state.cpp\n    history/view/history_view_cursor_state.h\n    history/view/history_view_draw_to_reply.cpp\n    history/view/history_view_draw_to_reply.h\n    history/view/history_view_add_poll_option.cpp\n    history/view/history_view_add_poll_option.h\n    history/view/history_view_element_overlay.cpp\n    history/view/history_view_element_overlay.h\n    history/view/history_view_element.cpp\n    history/view/history_view_element.h\n    history/view/history_view_emoji_interactions.cpp\n    history/view/history_view_emoji_interactions.h\n    history/view/history_view_empty_list_bubble.cpp\n    history/view/history_view_empty_list_bubble.h\n    history/view/history_view_fake_items.cpp\n    history/view/history_view_fake_items.h\n    history/view/history_view_group_call_bar.cpp\n    history/view/history_view_group_call_bar.h\n    history/view/history_view_group_members_widget.cpp\n    history/view/history_view_group_members_widget.h\n    history/view/history_view_item_preview.h\n    history/view/history_view_list_widget.cpp\n    history/view/history_view_list_widget.h\n    history/view/history_view_message.cpp\n    history/view/history_view_message.h\n    history/view/history_view_object.h\n    history/view/history_view_paid_reaction_toast.cpp\n    history/view/history_view_paid_reaction_toast.h\n    history/view/history_view_pinned_bar.cpp\n    history/view/history_view_pinned_bar.h\n    history/view/history_view_pinned_section.cpp\n    history/view/history_view_pinned_section.h\n    history/view/history_view_pinned_tracker.cpp\n    history/view/history_view_pinned_tracker.h\n    history/view/history_view_quick_action.cpp\n    history/view/history_view_quick_action.h\n    history/view/history_view_reaction_preview.cpp\n    history/view/history_view_reaction_preview.h\n    history/view/history_view_read_metrics_tracker.cpp\n    history/view/history_view_read_metrics_tracker.h\n    history/view/history_view_reply.cpp\n    history/view/history_view_reply.h\n    history/view/history_view_reply_button.cpp\n    history/view/history_view_reply_button.h\n    history/view/history_view_requests_bar.cpp\n    history/view/history_view_requests_bar.h\n    history/view/history_view_schedule_box.cpp\n    history/view/history_view_schedule_box.h\n    history/view/history_view_scheduled_section.cpp\n    history/view/history_view_scheduled_section.h\n    history/view/history_view_self_forwards_tagger.cpp\n    history/view/history_view_self_forwards_tagger.h\n    history/view/history_view_send_action.cpp\n    history/view/history_view_send_action.h\n    history/view/history_view_service_message.cpp\n    history/view/history_view_service_message.h\n    history/view/history_view_sponsored_click_handler.cpp\n    history/view/history_view_sponsored_click_handler.h\n    history/view/history_view_sticker_toast.cpp\n    history/view/history_view_sticker_toast.h\n    history/view/history_view_subsection_tabs.cpp\n    history/view/history_view_subsection_tabs.h\n    history/view/history_view_summary_header.cpp\n    history/view/history_view_summary_header.h\n    history/view/history_view_text_helper.cpp\n    history/view/history_view_text_helper.h\n    history/view/history_view_transcribe_button.cpp\n    history/view/history_view_transcribe_button.h\n    history/view/history_view_translate_bar.cpp\n    history/view/history_view_translate_bar.h\n    history/view/history_view_translate_tracker.cpp\n    history/view/history_view_translate_tracker.h\n    history/view/history_view_top_bar_widget.cpp\n    history/view/history_view_top_bar_widget.h\n    history/view/history_view_view_button.cpp\n    history/view/history_view_view_button.h\n    history/view/history_view_webpage_preview.cpp\n    history/view/history_view_webpage_preview.h\n    history/history.cpp\n    history/history.h\n    history/history_drag_area.cpp\n    history/history_drag_area.h\n    history/history_item.cpp\n    history/history_item.h\n    history/history_item_components.cpp\n    history/history_item_components.h\n    history/history_item_edition.cpp\n    history/history_item_edition.h\n    history/history_item_helpers.cpp\n    history/history_item_helpers.h\n    history/history_item_reply_markup.cpp\n    history/history_item_reply_markup.h\n    history/history_item_text.cpp\n    history/history_item_text.h\n    history/history_inner_widget.cpp\n    history/history_inner_widget.h\n    history/history_location_manager.cpp\n    history/history_location_manager.h\n    history/history_streamed_drafts.cpp\n    history/history_streamed_drafts.h\n    history/history_translation.cpp\n    history/history_translation.h\n    history/history_unread_things.cpp\n    history/history_unread_things.h\n    history/history_view_highlight_manager.cpp\n    history/history_view_highlight_manager.h\n    history/history_view_swipe_back_session.cpp\n    history/history_view_swipe_back_session.h\n    history/history_widget.cpp\n    history/history_widget.h\n    info/bot/earn/info_bot_earn_list.cpp\n    info/bot/earn/info_bot_earn_list.h\n    info/bot/earn/info_bot_earn_widget.cpp\n    info/bot/earn/info_bot_earn_widget.h\n    info/bot/starref/info_bot_starref_common.cpp\n    info/bot/starref/info_bot_starref_common.h\n    info/bot/starref/info_bot_starref_join_widget.cpp\n    info/bot/starref/info_bot_starref_join_widget.h\n    info/bot/starref/info_bot_starref_setup_widget.cpp\n    info/bot/starref/info_bot_starref_setup_widget.h\n    info/channel_statistics/boosts/create_giveaway_box.cpp\n    info/channel_statistics/boosts/create_giveaway_box.h\n    info/channel_statistics/boosts/giveaway/giveaway_list_controllers.cpp\n    info/channel_statistics/boosts/giveaway/giveaway_list_controllers.h\n    info/channel_statistics/boosts/info_boosts_inner_widget.cpp\n    info/channel_statistics/boosts/info_boosts_inner_widget.h\n    info/channel_statistics/boosts/info_boosts_widget.cpp\n    info/channel_statistics/boosts/info_boosts_widget.h\n    info/channel_statistics/earn/info_channel_earn_list.cpp\n    info/channel_statistics/earn/info_channel_earn_list.h\n    info/channel_statistics/earn/info_channel_earn_widget.cpp\n    info/channel_statistics/earn/info_channel_earn_widget.h\n    info/common_groups/info_common_groups_inner_widget.cpp\n    info/common_groups/info_common_groups_inner_widget.h\n    info/common_groups/info_common_groups_widget.cpp\n    info/common_groups/info_common_groups_widget.h\n    info/downloads/info_downloads_inner_widget.cpp\n    info/downloads/info_downloads_inner_widget.h\n    info/downloads/info_downloads_provider.cpp\n    info/downloads/info_downloads_provider.h\n    info/downloads/info_downloads_widget.cpp\n    info/downloads/info_downloads_widget.h\n    info/global_media/info_global_media_widget.cpp\n    info/global_media/info_global_media_widget.h\n    info/global_media/info_global_media_inner_widget.cpp\n    info/global_media/info_global_media_inner_widget.h\n    info/global_media/info_global_media_provider.cpp\n    info/global_media/info_global_media_provider.h\n    info/media/info_media_buttons.cpp\n    info/media/info_media_buttons.h\n    info/media/info_media_common.cpp\n    info/media/info_media_common.h\n    info/media/info_media_empty_widget.cpp\n    info/media/info_media_empty_widget.h\n    info/media/info_media_inner_widget.cpp\n    info/media/info_media_inner_widget.h\n    info/media/info_media_list_section.cpp\n    info/media/info_media_list_section.h\n    info/media/info_media_list_widget.cpp\n    info/media/info_media_list_widget.h\n    info/media/info_media_provider.cpp\n    info/media/info_media_provider.h\n    info/media/info_media_widget.cpp\n    info/media/info_media_widget.h\n    info/members/info_members_widget.cpp\n    info/members/info_members_widget.h\n    info/peer_gifts/info_peer_gifts_collections.cpp\n    info/peer_gifts/info_peer_gifts_collections.h\n    info/peer_gifts/info_peer_gifts_common.cpp\n    info/peer_gifts/info_peer_gifts_common.h\n    info/peer_gifts/info_peer_gifts_widget.cpp\n    info/peer_gifts/info_peer_gifts_widget.h\n    info/polls/info_polls_list_widget.cpp\n    info/polls/info_polls_list_widget.h\n    info/polls/info_polls_results_inner_widget.cpp\n    info/polls/info_polls_results_inner_widget.h\n    info/polls/info_polls_results_widget.cpp\n    info/polls/info_polls_results_widget.h\n    info/profile/info_profile_actions.cpp\n    info/profile/info_profile_actions.h\n    info/profile/info_profile_badge_tooltip.cpp\n    info/profile/info_profile_badge_tooltip.h\n    info/profile/info_profile_badge.cpp\n    info/profile/info_profile_badge.h\n    info/profile/info_profile_cover.cpp\n    info/profile/info_profile_cover.h\n    info/profile/info_profile_emoji_status_panel.cpp\n    info/profile/info_profile_emoji_status_panel.h\n    info/profile/info_profile_inner_widget.cpp\n    info/profile/info_profile_inner_widget.h\n    info/profile/info_profile_members.cpp\n    info/profile/info_profile_members.h\n    info/profile/info_profile_members_controllers.cpp\n    info/profile/info_profile_members_controllers.h\n    info/profile/info_profile_phone_menu.cpp\n    info/profile/info_profile_phone_menu.h\n    info/profile/info_profile_status_label.cpp\n    info/profile/info_profile_status_label.h\n    info/profile/info_profile_top_bar.cpp\n    info/profile/info_profile_top_bar.h\n    info/profile/info_profile_values.cpp\n    info/profile/info_profile_values.h\n    info/profile/info_profile_widget.cpp\n    info/profile/info_profile_widget.h\n    info/reactions_list/info_reactions_list_widget.cpp\n    info/reactions_list/info_reactions_list_widget.h\n    info/requests_list/info_requests_list_widget.cpp\n    info/requests_list/info_requests_list_widget.h\n    info/saved/info_saved_music_common.cpp\n    info/saved/info_saved_music_common.h\n    info/saved/info_saved_music_provider.cpp\n    info/saved/info_saved_music_provider.h\n    info/saved/info_saved_music_widget.cpp\n    info/saved/info_saved_music_widget.h\n    info/saved/info_saved_sublists_widget.cpp\n    info/saved/info_saved_sublists_widget.h\n    info/settings/info_settings_widget.cpp\n    info/settings/info_settings_widget.h\n    info/similar_peers/info_similar_peers_widget.cpp\n    info/similar_peers/info_similar_peers_widget.h\n    info/statistics/info_statistics_common.h\n    info/statistics/info_statistics_inner_widget.cpp\n    info/statistics/info_statistics_inner_widget.h\n    info/statistics/info_statistics_list_controllers.cpp\n    info/statistics/info_statistics_list_controllers.h\n    info/statistics/info_statistics_recent_message.cpp\n    info/statistics/info_statistics_recent_message.h\n    info/statistics/info_statistics_tag.h\n    info/statistics/info_statistics_widget.cpp\n    info/statistics/info_statistics_widget.h\n    info/stories/info_stories_albums.cpp\n    info/stories/info_stories_albums.h\n    info/stories/info_stories_common.h\n    info/stories/info_stories_inner_widget.cpp\n    info/stories/info_stories_inner_widget.h\n    info/stories/info_stories_provider.cpp\n    info/stories/info_stories_provider.h\n    info/stories/info_stories_widget.cpp\n    info/stories/info_stories_widget.h\n    info/userpic/info_userpic_colors_editor.cpp\n    info/userpic/info_userpic_colors_editor.h\n    info/userpic/info_userpic_emoji_builder.cpp\n    info/userpic/info_userpic_emoji_builder.h\n    info/userpic/info_userpic_emoji_builder_common.cpp\n    info/userpic/info_userpic_emoji_builder_common.h\n    info/userpic/info_userpic_emoji_builder_menu_item.cpp\n    info/userpic/info_userpic_emoji_builder_menu_item.h\n    info/userpic/info_userpic_emoji_builder_preview.cpp\n    info/userpic/info_userpic_emoji_builder_preview.h\n    info/userpic/info_userpic_emoji_builder_widget.cpp\n    info/userpic/info_userpic_emoji_builder_widget.h\n    info/info_content_widget.cpp\n    info/info_content_widget.h\n    info/info_controller.cpp\n    info/info_controller.h\n    info/info_layer_widget.cpp\n    info/info_layer_widget.h\n    info/info_memento.cpp\n    info/info_memento.h\n    info/info_section_widget.cpp\n    info/info_section_widget.h\n    info/info_top_bar.cpp\n    info/info_top_bar.h\n    info/info_wrap_widget.cpp\n    info/info_wrap_widget.h\n    inline_bots/bot_attach_web_view.cpp\n    inline_bots/bot_attach_web_view.h\n    inline_bots/inline_bot_confirm_prepared.cpp\n    inline_bots/inline_bot_confirm_prepared.h\n    inline_bots/inline_bot_downloads.cpp\n    inline_bots/inline_bot_downloads.h\n    inline_bots/inline_bot_layout_internal.cpp\n    inline_bots/inline_bot_layout_internal.h\n    inline_bots/inline_bot_layout_item.cpp\n    inline_bots/inline_bot_layout_item.h\n    inline_bots/inline_bot_result.cpp\n    inline_bots/inline_bot_result.h\n    inline_bots/inline_bot_send_data.cpp\n    inline_bots/inline_bot_send_data.h\n    inline_bots/inline_bot_storage.cpp\n    inline_bots/inline_bot_storage.h\n    inline_bots/inline_results_inner.cpp\n    inline_bots/inline_results_inner.h\n    inline_bots/inline_results_widget.cpp\n    inline_bots/inline_results_widget.h\n    intro/intro_code.cpp\n    intro/intro_code.h\n    intro/intro_email.cpp\n    intro/intro_email.h\n    intro/intro_password_check.cpp\n    intro/intro_password_check.h\n    intro/intro_phone.cpp\n    intro/intro_phone.h\n    intro/intro_qr.cpp\n    intro/intro_qr.h\n    intro/intro_signup.cpp\n    intro/intro_signup.h\n    intro/intro_start.cpp\n    intro/intro_start.h\n    intro/intro_step.cpp\n    intro/intro_step.h\n    intro/intro_widget.cpp\n    intro/intro_widget.h\n    iv/iv_delegate_impl.cpp\n    iv/iv_delegate_impl.h\n    iv/iv_instance.cpp\n    iv/iv_instance.h\n    lang/lang_cloud_manager.cpp\n    lang/lang_cloud_manager.h\n    lang/lang_instance.cpp\n    lang/lang_instance.h\n    lang/lang_numbers_animation.cpp\n    lang/lang_numbers_animation.h\n    lang/lang_translator.cpp\n    lang/lang_translator.h\n    lang/translate_mtproto_provider.cpp\n    lang/translate_mtproto_provider.h\n    lang/translate_provider.cpp\n    lang/translate_provider.h\n    lang/translate_url_provider.cpp\n    lang/translate_url_provider.h\n    layout/layout_document_generic_preview.cpp\n    layout/layout_document_generic_preview.h\n    layout/layout_item_base.cpp\n    layout/layout_item_base.h\n    main/main_account.cpp\n    main/main_account.h\n    main/main_app_config.cpp\n    main/main_app_config.h\n    main/main_app_config_values.cpp\n    main/main_app_config_values.h\n    main/main_domain.cpp\n    main/main_domain.h\n    main/main_session.cpp\n    main/main_session.h\n    main/main_session_settings.cpp\n    main/main_session_settings.h\n    main/session/send_as_peers.cpp\n    main/session/send_as_peers.h\n    main/session/session_show.cpp\n    main/session/session_show.h\n    media/audio/media_audio.cpp\n    media/audio/media_audio.h\n    media/audio/media_audio_edit.cpp\n    media/audio/media_audio_edit.h\n    media/audio/media_audio_capture.cpp\n    media/audio/media_audio_capture.h\n    media/audio/media_audio_capture_common.h\n    media/audio/media_audio_ffmpeg_loader.cpp\n    media/audio/media_audio_ffmpeg_loader.h\n    media/audio/media_audio_loader.cpp\n    media/audio/media_audio_loader.h\n    media/audio/media_audio_loaders.cpp\n    media/audio/media_audio_loaders.h\n    media/audio/media_audio_local_cache.cpp\n    media/audio/media_audio_local_cache.h\n    media/audio/media_audio_track.cpp\n    media/audio/media_audio_track.h\n    media/audio/media_child_ffmpeg_loader.cpp\n    media/audio/media_child_ffmpeg_loader.h\n    media/player/media_player_float.cpp\n    media/player/media_player_float.h\n    media/player/media_player_instance.cpp\n    media/player/media_player_instance.h\n    media/player/media_player_listen_tracker.cpp\n    media/player/media_player_listen_tracker.h\n    media/player/media_player_panel.cpp\n    media/player/media_player_panel.h\n    media/player/media_player_volume_controller.cpp\n    media/player/media_player_volume_controller.h\n    media/player/media_player_widget.cpp\n    media/player/media_player_widget.h\n    media/stories/media_stories_caption_full_view.cpp\n    media/stories/media_stories_caption_full_view.h\n    media/stories/media_stories_controller.cpp\n    media/stories/media_stories_controller.h\n    media/stories/media_stories_delegate.cpp\n    media/stories/media_stories_delegate.h\n    media/stories/media_stories_header.cpp\n    media/stories/media_stories_header.h\n    media/stories/media_stories_reactions.cpp\n    media/stories/media_stories_reactions.h\n    media/stories/media_stories_recent_views.cpp\n    media/stories/media_stories_recent_views.h\n    media/stories/media_stories_reply.cpp\n    media/stories/media_stories_reply.h\n    media/stories/media_stories_repost_view.cpp\n    media/stories/media_stories_repost_view.h\n    media/stories/media_stories_share.cpp\n    media/stories/media_stories_share.h\n    media/stories/media_stories_sibling.cpp\n    media/stories/media_stories_sibling.h\n    media/stories/media_stories_slider.cpp\n    media/stories/media_stories_slider.h\n    media/stories/media_stories_stealth.cpp\n    media/stories/media_stories_stealth.h\n    media/stories/media_stories_view.cpp\n    media/stories/media_stories_view.h\n    media/streaming/media_streaming_audio_track.cpp\n    media/streaming/media_streaming_audio_track.h\n    media/streaming/media_streaming_common.h\n    media/streaming/media_streaming_document.cpp\n    media/streaming/media_streaming_document.h\n    media/streaming/media_streaming_file.cpp\n    media/streaming/media_streaming_file.h\n    media/streaming/media_streaming_file_delegate.h\n    media/streaming/media_streaming_instance.cpp\n    media/streaming/media_streaming_instance.h\n    media/streaming/media_streaming_loader.cpp\n    media/streaming/media_streaming_loader.h\n    media/streaming/media_streaming_loader_local.cpp\n    media/streaming/media_streaming_loader_local.h\n    media/streaming/media_streaming_loader_mtproto.cpp\n    media/streaming/media_streaming_loader_mtproto.h\n    media/streaming/media_streaming_player.cpp\n    media/streaming/media_streaming_player.h\n    media/streaming/media_streaming_reader.cpp\n    media/streaming/media_streaming_reader.h\n    media/streaming/media_streaming_round_preview.cpp\n    media/streaming/media_streaming_round_preview.h\n    media/streaming/media_streaming_utility.cpp\n    media/streaming/media_streaming_utility.h\n    media/streaming/media_streaming_video_track.cpp\n    media/streaming/media_streaming_video_track.h\n    media/view/media_view_group_thumbs.cpp\n    media/view/media_view_group_thumbs.h\n    media/view/media_view_open_common.cpp\n    media/view/media_view_open_common.h\n    media/view/media_view_overlay_opengl.cpp\n    media/view/media_view_overlay_opengl.h\n    media/view/media_view_overlay_raster.cpp\n    media/view/media_view_overlay_raster.h\n    media/view/media_view_overlay_rhi.cpp\n    media/view/media_view_overlay_rhi.h\n    media/view/media_view_metal_texture.h\n    media/view/media_view_overlay_renderer.h\n    media/view/media_view_overlay_widget.cpp\n    media/view/media_view_overlay_widget.h\n    media/view/media_view_pip.cpp\n    media/view/media_view_pip.h\n    media/view/media_view_pip_opengl.cpp\n    media/view/media_view_pip_opengl.h\n    media/view/media_view_pip_raster.cpp\n    media/view/media_view_pip_raster.h\n    media/view/media_view_pip_rhi.cpp\n    media/view/media_view_pip_rhi.h\n    media/view/media_view_pip_renderer.h\n    media/view/media_view_playback_controls.cpp\n    media/view/media_view_playback_controls.h\n    media/view/media_view_playback_progress.cpp\n    media/view/media_view_playback_progress.h\n    media/view/media_view_playback_sponsored.cpp\n    media/view/media_view_playback_sponsored.h\n    media/view/media_view_video_stream.cpp\n    media/view/media_view_video_stream.h\n    media/system_media_controls_manager.h\n    media/system_media_controls_manager.cpp\n    menu/menu_antispam_validator.cpp\n    menu/menu_antispam_validator.h\n    menu/menu_dock.cpp\n    menu/menu_dock.h\n    menu/menu_item_download_files.cpp\n    menu/menu_item_download_files.h\n    menu/menu_item_rate_transcribe_session.cpp\n    menu/menu_item_rate_transcribe_session.h\n    menu/menu_mute.cpp\n    menu/menu_mute.h\n    menu/menu_send.cpp\n    menu/menu_send.h\n    menu/menu_sponsored.cpp\n    menu/menu_sponsored.h\n    menu/menu_ttl_validator.cpp\n    menu/menu_ttl_validator.h\n    mtproto/config_loader.cpp\n    mtproto/config_loader.h\n    mtproto/connection_abstract.cpp\n    mtproto/connection_abstract.h\n    mtproto/connection_http.cpp\n    mtproto/connection_http.h\n    mtproto/connection_resolving.cpp\n    mtproto/connection_resolving.h\n    mtproto/connection_tcp.cpp\n    mtproto/connection_tcp.h\n    mtproto/core_types.h\n    mtproto/dedicated_file_loader.cpp\n    mtproto/dedicated_file_loader.h\n    mtproto/facade.cpp\n    mtproto/facade.h\n    mtproto/mtp_instance.cpp\n    mtproto/mtp_instance.h\n    mtproto/sender.h\n    mtproto/session.cpp\n    mtproto/session.h\n    mtproto/session_private.cpp\n    mtproto/session_private.h\n    mtproto/special_config_request.cpp\n    mtproto/special_config_request.h\n    mtproto/type_utils.h\n    overview/overview_checkbox.cpp\n    overview/overview_checkbox.h\n    overview/overview_layout.cpp\n    overview/overview_layout.h\n    overview/overview_layout_delegate.h\n    poll/poll_media_upload.cpp\n    poll/poll_media_upload.h\n    passport/passport_encryption.cpp\n    passport/passport_encryption.h\n    passport/passport_form_controller.cpp\n    passport/passport_form_controller.h\n    passport/passport_form_view_controller.cpp\n    passport/passport_form_view_controller.h\n    passport/passport_panel.cpp\n    passport/passport_panel.h\n    passport/passport_panel_controller.cpp\n    passport/passport_panel_controller.h\n    passport/passport_panel_edit_contact.cpp\n    passport/passport_panel_edit_contact.h\n    passport/passport_panel_edit_document.cpp\n    passport/passport_panel_edit_document.h\n    passport/passport_panel_edit_scans.cpp\n    passport/passport_panel_edit_scans.h\n    passport/passport_panel_form.cpp\n    passport/passport_panel_form.h\n    passport/passport_panel_password.cpp\n    passport/passport_panel_password.h\n    payments/payments_checkout_process.cpp\n    payments/payments_checkout_process.h\n    payments/payments_form.cpp\n    payments/payments_form.h\n    payments/payments_non_panel_process.cpp\n    payments/payments_non_panel_process.h\n    payments/payments_reaction_process.cpp\n    payments/payments_reaction_process.h\n    platform/linux/file_utilities_linux.cpp\n    platform/linux/file_utilities_linux.h\n    platform/linux/launcher_linux.cpp\n    platform/linux/launcher_linux.h\n    platform/linux/integration_linux.cpp\n    platform/linux/integration_linux.h\n    platform/linux/main_window_linux.cpp\n    platform/linux/main_window_linux.h\n    platform/linux/notifications_manager_linux.cpp\n    platform/linux/notifications_manager_linux.h\n    platform/linux/overlay_widget_linux.h\n    platform/linux/specific_linux.cpp\n    platform/linux/specific_linux.h\n    platform/linux/translate_provider_linux.cpp\n    platform/linux/translate_provider_linux.h\n    platform/linux/tray_linux.cpp\n    platform/linux/tray_linux.h\n    platform/linux/webauthn_linux.cpp\n    platform/mac/file_utilities_mac.mm\n    platform/mac/file_utilities_mac.h\n    platform/mac/launcher_mac.mm\n    platform/mac/launcher_mac.h\n    platform/mac/integration_mac.mm\n    platform/mac/integration_mac.h\n    platform/mac/mac_iconv_helper.c\n    platform/mac/main_window_mac.mm\n    platform/mac/main_window_mac.h\n    platform/mac/notifications_manager_mac.mm\n    platform/mac/notifications_manager_mac.h\n    platform/mac/overlay_widget_mac.h\n    platform/mac/overlay_widget_mac.mm\n    platform/mac/specific_mac.mm\n    platform/mac/specific_mac.h\n    platform/mac/specific_mac_p.mm\n    platform/mac/specific_mac_p.h\n    platform/mac/translate_provider_mac.h\n    platform/mac/translate_provider_mac.mm\n    platform/mac/tray_mac.h\n    platform/mac/tray_mac.mm\n    platform/mac/webauthn_mac.mm\n    platform/mac/window_title_mac.mm\n    platform/mac/touchbar/items/mac_formatter_item.h\n    platform/mac/touchbar/items/mac_formatter_item.mm\n    platform/mac/touchbar/items/mac_pinned_chats_item.h\n    platform/mac/touchbar/items/mac_pinned_chats_item.mm\n    platform/mac/touchbar/items/mac_scrubber_item.h\n    platform/mac/touchbar/items/mac_scrubber_item.mm\n    platform/mac/touchbar/mac_touchbar_audio.h\n    platform/mac/touchbar/mac_touchbar_audio.mm\n    platform/mac/touchbar/mac_touchbar_common.h\n    platform/mac/touchbar/mac_touchbar_common.mm\n    platform/mac/touchbar/mac_touchbar_controls.h\n    platform/mac/touchbar/mac_touchbar_controls.mm\n    platform/mac/touchbar/mac_touchbar_main.h\n    platform/mac/touchbar/mac_touchbar_main.mm\n    platform/mac/touchbar/mac_touchbar_manager.h\n    platform/mac/touchbar/mac_touchbar_manager.mm\n    platform/mac/touchbar/mac_touchbar_media_view.h\n    platform/mac/touchbar/mac_touchbar_media_view.mm\n    platform/win/file_utilities_win.cpp\n    platform/win/file_utilities_win.h\n    platform/win/launcher_win.cpp\n    platform/win/launcher_win.h\n    platform/win/integration_win.cpp\n    platform/win/integration_win.h\n    platform/win/main_window_win.cpp\n    platform/win/main_window_win.h\n    platform/win/notifications_manager_win.cpp\n    platform/win/notifications_manager_win.h\n    platform/win/overlay_widget_win.h\n    platform/win/specific_win.cpp\n    platform/win/specific_win.h\n    platform/win/translate_provider_win.h\n    platform/win/tray_win.cpp\n    platform/win/tray_win.h\n    platform/win/webauthn_win.cpp\n    platform/win/windows_app_user_model_id.cpp\n    platform/win/windows_app_user_model_id.h\n    platform/win/windows_dlls.cpp\n    platform/win/windows_dlls.h\n    platform/win/windows_autostart_task.cpp\n    platform/win/windows_autostart_task.h\n    platform/win/windows_toast_activator.cpp\n    platform/win/windows_toast_activator.h\n    platform/platform_file_utilities.h\n    platform/platform_launcher.h\n    platform/platform_integration.cpp\n    platform/platform_integration.h\n    platform/platform_main_window.h\n    platform/platform_notifications_manager.h\n    platform/platform_overlay_widget.cpp\n    platform/platform_overlay_widget.h\n    platform/platform_specific.h\n    platform/platform_translate_provider.h\n    platform/platform_tray.h\n    platform/platform_webauthn.h\n    platform/platform_window_title.h\n    profile/profile_block_widget.cpp\n    profile/profile_block_widget.h\n    profile/profile_cover_drop_area.cpp\n    profile/profile_cover_drop_area.h\n    settings/business/settings_away_message.cpp\n    settings/business/settings_away_message.h\n    settings/business/settings_shortcut_messages.cpp\n    settings/business/settings_shortcut_messages.h\n    settings/business/settings_chat_intro.cpp\n    settings/business/settings_chat_intro.h\n    settings/business/settings_chat_links.cpp\n    settings/business/settings_chat_links.h\n    settings/business/settings_chatbots.cpp\n    settings/business/settings_chatbots.h\n    settings/business/settings_greeting.cpp\n    settings/business/settings_greeting.h\n    settings/business/settings_location.cpp\n    settings/business/settings_location.h\n    settings/business/settings_quick_replies.cpp\n    settings/business/settings_quick_replies.h\n    settings/business/settings_recipients_helper.cpp\n    settings/business/settings_recipients_helper.h\n    settings/business/settings_working_hours.cpp\n    settings/business/settings_working_hours.h\n    settings/cloud_password/settings_cloud_password_email.cpp\n    settings/cloud_password/settings_cloud_password_email.h\n    settings/cloud_password/settings_cloud_password_email_confirm.cpp\n    settings/cloud_password/settings_cloud_password_email_confirm.h\n    settings/cloud_password/settings_cloud_password_hint.cpp\n    settings/cloud_password/settings_cloud_password_hint.h\n    settings/cloud_password/settings_cloud_password_input.cpp\n    settings/cloud_password/settings_cloud_password_input.h\n    settings/cloud_password/settings_cloud_password_login_email.cpp\n    settings/cloud_password/settings_cloud_password_login_email.h\n    settings/cloud_password/settings_cloud_password_login_email_confirm.cpp\n    settings/cloud_password/settings_cloud_password_login_email_confirm.h\n    settings/cloud_password/settings_cloud_password_manage.cpp\n    settings/cloud_password/settings_cloud_password_manage.h\n    settings/cloud_password/settings_cloud_password_start.cpp\n    settings/cloud_password/settings_cloud_password_start.h\n    settings/cloud_password/settings_cloud_password_step.cpp\n    settings/cloud_password/settings_cloud_password_step.h\n    settings/cloud_password/settings_cloud_password_validate_icon.cpp\n    settings/cloud_password/settings_cloud_password_validate_icon.h\n    settings/sections/settings_active_sessions.cpp\n    settings/sections/settings_active_sessions.h\n    settings/sections/settings_advanced.cpp\n    settings/sections/settings_advanced.h\n    settings/sections/settings_chat.cpp\n    settings/sections/settings_chat.h\n    settings/sections/settings_blocked_peers.cpp\n    settings/sections/settings_blocked_peers.h\n    settings/sections/settings_business.cpp\n    settings/sections/settings_business.h\n    settings/sections/settings_calls.cpp\n    settings/sections/settings_calls.h\n    settings/sections/settings_fork.cpp\n    settings/sections/settings_fork.h\n    settings/sections/settings_link_device.cpp\n    settings/sections/settings_link_device.h\n    settings/settings_codes.cpp\n    settings/settings_codes.h\n    settings/settings_common_session.cpp\n    settings/settings_common_session.h\n    settings/detailed_settings_button.cpp\n    settings/detailed_settings_button.h\n    settings/sections/settings_credits.cpp\n    settings/sections/settings_credits.h\n    settings/settings_credits_graphics.cpp\n    settings/settings_credits_graphics.h\n    settings/settings_experimental.cpp\n    settings/settings_experimental.h\n    settings/sections/settings_folders.cpp\n    settings/sections/settings_folders.h\n    settings/sections/settings_fork.cpp\n    settings/sections/settings_fork.h\n    settings/sections/settings_global_ttl.cpp\n    settings/sections/settings_global_ttl.h\n    settings/sections/settings_information.cpp\n    settings/sections/settings_information.h\n    settings/settings_intro.cpp\n    settings/settings_intro.h\n    settings/sections/settings_local_passcode.cpp\n    settings/sections/settings_local_passcode.h\n    settings/sections/settings_main.cpp\n    settings/sections/settings_main.h\n    settings/settings_recent_searches.cpp\n    settings/settings_recent_searches.h\n    settings/settings_search.cpp\n    settings/settings_search.h\n    settings/settings_faq_suggestions.cpp\n    settings/settings_faq_suggestions.h\n    settings/settings_builder.cpp\n    settings/settings_builder.h\n    settings/sections/settings_notifications.cpp\n    settings/sections/settings_notifications.h\n    settings/sections/settings_privacy_security.cpp\n    settings/sections/settings_privacy_security.h\n    settings/sections/settings_notifications_reactions.cpp\n    settings/sections/settings_notifications_reactions.h\n    settings/sections/settings_notifications_type.cpp\n    settings/sections/settings_notifications_type.h\n    settings/sections/settings_passkeys.cpp\n    settings/sections/settings_passkeys.h\n    settings/settings_power_saving.cpp\n    settings/settings_power_saving.h\n    settings/sections/settings_premium.cpp\n    settings/sections/settings_premium.h\n    settings/settings_privacy_controllers.cpp\n    settings/settings_privacy_controllers.h\n    settings/settings_scale_preview.cpp\n    settings/settings_scale_preview.h\n    settings/settings_type.h\n    settings/sections/settings_shortcuts.cpp\n    settings/sections/settings_shortcuts.h\n    settings/sections/settings_websites.cpp\n    settings/sections/settings_websites.h\n    storage/details/storage_file_utilities.cpp\n    storage/details/storage_file_utilities.h\n    storage/details/storage_settings_scheme.cpp\n    storage/details/storage_settings_scheme.h\n    storage/download_manager_mtproto.cpp\n    storage/download_manager_mtproto.h\n    storage/file_download.cpp\n    storage/file_download.h\n    storage/file_download_mtproto.cpp\n    storage/file_download_mtproto.h\n    storage/file_download_web.cpp\n    storage/file_download_web.h\n    storage/file_upload.cpp\n    storage/file_upload.h\n    storage/localimageloader.cpp\n    storage/localimageloader.h\n    storage/localstorage.cpp\n    storage/localstorage.h\n    storage/serialize_common.cpp\n    storage/serialize_common.h\n    storage/serialize_document.cpp\n    storage/serialize_document.h\n    storage/serialize_peer.cpp\n    storage/serialize_peer.h\n    storage/storage_account.cpp\n    storage/storage_account.h\n    storage/storage_cloud_blob.cpp\n    storage/storage_cloud_blob.h\n    storage/storage_domain.cpp\n    storage/storage_domain.h\n    storage/storage_facade.cpp\n    storage/storage_facade.h\n    storage/storage_media_prepare.cpp\n    storage/storage_media_prepare.h\n    storage/storage_shared_media.cpp\n    storage/storage_shared_media.h\n    storage/storage_sparse_ids_list.cpp\n    storage/storage_sparse_ids_list.h\n    storage/storage_user_photos.cpp\n    storage/storage_user_photos.h\n    storage/streamed_file_downloader.cpp\n    storage/streamed_file_downloader.h\n    support/support_autocomplete.cpp\n    support/support_autocomplete.h\n    support/support_common.cpp\n    support/support_common.h\n    support/support_helper.cpp\n    support/support_helper.h\n    support/support_preload.cpp\n    support/support_preload.h\n    support/support_templates.cpp\n    support/support_templates.h\n    tde2e/tde2e_integration.cpp\n    tde2e/tde2e_integration.h\n    ui/boxes/edit_invite_link_session.cpp\n    ui/boxes/edit_invite_link_session.h\n    ui/boxes/emoji_stake_box.cpp\n    ui/boxes/emoji_stake_box.h\n    ui/boxes/peer_qr_box.cpp\n    ui/boxes/peer_qr_box.h\n    ui/chat/attach/attach_item_single_file_preview.cpp\n    ui/chat/attach/attach_item_single_file_preview.h\n    ui/chat/attach/attach_item_single_media_preview.cpp\n    ui/chat/attach/attach_item_single_media_preview.h\n    ui/chat/choose_send_as.cpp\n    ui/chat/choose_send_as.h\n    ui/chat/choose_theme_controller.cpp\n    ui/chat/choose_theme_controller.h\n    ui/chat/sponsored_message_bar.cpp\n    ui/chat/sponsored_message_bar.h\n    ui/controls/compose_ai_button_factory.cpp\n    ui/controls/compose_ai_button_factory.h\n    ui/controls/emoji_button_factory.cpp\n    ui/controls/emoji_button_factory.h\n    ui/controls/location_picker.cpp\n    ui/controls/location_picker.h\n    ui/controls/silent_toggle.cpp\n    ui/controls/silent_toggle.h\n    ui/controls/table_rows.cpp\n    ui/controls/table_rows.h\n    ui/controls/userpic_button.cpp\n    ui/controls/userpic_button.h\n    ui/effects/credits_graphics.cpp\n    ui/effects/credits_graphics.h\n    ui/effects/emoji_fly_animation.cpp\n    ui/effects/emoji_fly_animation.h\n    ui/effects/message_sending_animation_common.h\n    ui/effects/message_sending_animation_controller.cpp\n    ui/effects/message_sending_animation_controller.h\n    ui/effects/reaction_fly_animation.cpp\n    ui/effects/reaction_fly_animation.h\n    ui/effects/thanos_effect.cpp\n    ui/effects/thanos_effect.h\n    ui/effects/thanos_effect_controller.cpp\n    ui/effects/thanos_effect_controller.h\n    ui/effects/thanos_effect_renderer.cpp\n    ui/effects/thanos_effect_renderer.h\n    ui/effects/thanos_effect_session.cpp\n    ui/effects/thanos_effect_session.h\n    ui/effects/send_action_animations.cpp\n    ui/effects/send_action_animations.h\n    ui/image/image.cpp\n    ui/image/image.h\n    ui/image/image_location.cpp\n    ui/image/image_location.h\n    ui/image/image_location_factory.cpp\n    ui/image/image_location_factory.h\n    ui/text/format_song_document_name.cpp\n    ui/text/format_song_document_name.h\n    ui/widgets/expandable_peer_list.cpp\n    ui/widgets/expandable_peer_list.h\n    ui/widgets/chat_filters_tabs_strip.cpp\n    ui/widgets/chat_filters_tabs_strip.h\n    ui/widgets/peer_bubble.cpp\n    ui/widgets/peer_bubble.h\n    ui/countryinput.cpp\n    ui/countryinput.h\n    ui/dynamic_thumbnails.cpp\n    ui/dynamic_thumbnails.h\n    ui/filter_icons.cpp\n    ui/filter_icons.h\n    ui/filter_icon_panel.cpp\n    ui/filter_icon_panel.h\n    ui/item_text_options.cpp\n    ui/item_text_options.h\n    ui/resize_area.h\n    ui/top_background_gradient.cpp\n    ui/top_background_gradient.h\n    ui/unread_badge.cpp\n    ui/unread_badge.h\n    ui/peer/video_userpic_player.cpp\n    ui/peer/video_userpic_player.h\n    window/main_window.cpp\n    window/main_window.h\n    window/notifications_manager.cpp\n    window/notifications_manager.h\n    window/notifications_manager_default.cpp\n    window/notifications_manager_default.h\n    window/notifications_utilities.cpp\n    window/notifications_utilities.h\n    window/section_memento.h\n    window/section_widget.cpp\n    window/section_widget.h\n    window/window_adaptive.cpp\n    window/window_adaptive.h\n    window/window_chat_preview.cpp\n    window/window_chat_preview.h\n    window/window_chat_switch_process.cpp\n    window/window_chat_switch_process.h\n    window/window_connecting_widget.cpp\n    window/window_connecting_widget.h\n    window/window_controller.cpp\n    window/window_controller.h\n    window/window_filters_menu.cpp\n    window/window_filters_menu.h\n    window/window_history_hider.cpp\n    window/window_history_hider.h\n    window/window_lock_widgets.cpp\n    window/window_lock_widgets.h\n    window/window_main_menu.cpp\n    window/window_main_menu.h\n    window/window_main_menu_helpers.cpp\n    window/window_main_menu_helpers.h\n    window/window_media_preview.cpp\n    window/window_media_preview.h\n    window/window_peer_menu.cpp\n    window/window_peer_menu.h\n    window/window_section_common.h\n    window/window_separate_id.cpp\n    window/window_separate_id.h\n    window/session/window_session_media.cpp\n    window/window_session_controller.cpp\n    window/window_session_controller.h\n    window/window_session_controller_link_info.h\n    window/window_setup_email.cpp\n    window/window_setup_email.h\n    window/window_top_bar_wrap.h\n    window/themes/window_theme.cpp\n    window/themes/window_theme.h\n    window/themes/window_theme_editor.cpp\n    window/themes/window_theme_editor.h\n    window/themes/window_theme_editor_block.cpp\n    window/themes/window_theme_editor_block.h\n    window/themes/window_theme_editor_box.cpp\n    window/themes/window_theme_editor_box.h\n    window/themes/window_theme_preview.cpp\n    window/themes/window_theme_preview.h\n    window/themes/window_theme_warning.cpp\n    window/themes/window_theme_warning.h\n    window/themes/window_themes_cloud_list.cpp\n    window/themes/window_themes_cloud_list.h\n    window/themes/window_themes_embedded.cpp\n    window/themes/window_themes_embedded.h\n    window/themes/window_themes_generate_name.cpp\n    window/themes/window_themes_generate_name.h\n    apiwrap.cpp\n    apiwrap.h\n    config.h\n    logs.cpp\n    logs.h\n    main.cpp\n    mainwidget.cpp\n    mainwidget.h\n    mainwindow.cpp\n    mainwindow.h\n    settings.cpp\n    settings.h\n    stdafx.h\n    tray_accounts_menu.h\n    tray.cpp\n    tray.h\n)\n\nif (APPLE)\n    nice_target_sources(Telegram ${src_loc}\n    PRIVATE\n        tray_accounts_menu.cpp\n        media/view/media_view_metal_texture.mm\n    )\nelse()\n    nice_target_sources(Telegram ${src_loc}\n    PRIVATE\n        tray_accounts_menu_dummy.cpp\n    )\nendif()\n\nif (NOT build_winstore)\n    remove_target_sources(Telegram ${src_loc}\n        platform/win/windows_start_task.cpp\n        platform/win/windows_start_task.h\n    )\nendif()\n\nif (DESKTOP_APP_USE_PACKAGED)\n    remove_target_sources(Telegram ${src_loc}\n        platform/mac/mac_iconv_helper.c\n    )\nendif()\n\nnice_target_sources(Telegram ${res_loc}\nPRIVATE\n    qrc/emoji_1.qrc\n    qrc/emoji_2.qrc\n    qrc/emoji_3.qrc\n    qrc/emoji_4.qrc\n    qrc/emoji_5.qrc\n    qrc/emoji_6.qrc\n    qrc/emoji_7.qrc\n    qrc/emoji_8.qrc\n    qrc/emoji_preview.qrc\n    qrc/telegram/animations.qrc\n    qrc/telegram/export.qrc\n    qrc/telegram/iv.qrc\n    qrc/telegram/picker.qrc\n    qrc/telegram/telegram.qrc\n    qrc/telegram/sounds.qrc\n    winrc/Telegram.rc\n    winrc/Telegram.manifest\n    langs/lang.strings\n    langs/cloud_lang.strings\n    numbers.txt\n)\n\ninclude(cmake/qrhi_shaders.cmake)\n\nif (APPLE AND NOT build_macstore)\n    nice_target_sources(Telegram ${res_loc}\n    PRIVATE\n        qrc/telegram/mac_icons.qrc\n    )\nendif()\n\nif (WIN32)\n    # message(${CMAKE_GENERATOR})\n    # mt.exe -manifest \"${res_loc}/winrc/Telegram.manifest\" \"-inputresource:\\\"$<TARGET_FILE:Telegram>\\\";#1\" \"-outputresource:\\\"$<TARGET_FILE:Telegram>\\\";#1\" >NUL\n    # set(hash_symbol \"#\")\n    # set(release $<CONFIG:Release>)\n    # add_custom_command(\n    # TARGET\n    #     Telegram\n    # POST_BUILD COMMAND\n    #     $<IF:${release},mt.exe,echo.> $<${release}:-manifest> $<${release}:\"${res_loc}/winrc/Telegram.manifest\"> $<${release}:-inputresource:\"$<TARGET_FILE:Telegram>\"$<SEMICOLON>${hash_symbol}1> $<${release}:-outputresource:\"$<TARGET_FILE:Telegram>\"$<SEMICOLON>${hash_symbol}1> $<${release}:$<ANGLE-R>NUL>\n    # COMMENT\n    #     $<IF:${release},\"Appending compatibility manifest.\",\"Finalizing build.\">\n    # )\n\n    if (QT_VERSION LESS 6)\n        target_link_libraries(Telegram PRIVATE desktop-app::win_directx_helper)\n    endif()\n\n    target_link_options(Telegram PRIVATE /PDBPAGESIZE:8192)\nelseif (APPLE)\n    if (NOT DESKTOP_APP_USE_PACKAGED)\n        target_link_libraries(Telegram PRIVATE desktop-app::external_iconv)\n    endif()\n\n    set(icons_path ${CMAKE_CURRENT_SOURCE_DIR}/Telegram/Images.xcassets)\n    if (CMAKE_GENERATOR STREQUAL Xcode)\n        target_add_resource(Telegram ${icons_path})\n    else()\n        set(icon_path ${icons_path}/Icon.iconset)\n        find_program(ICONUTIL iconutil)\n        find_program(PNG2ICNS png2icns)\n        if (ICONUTIL)\n            add_custom_command(\n                OUTPUT Icon.icns\n                COMMAND ${ICONUTIL}\n                ARGS\n                    --convert icns\n                    --output Icon.icns\n                    ${icon_path}\n            )\n        elseif (PNG2ICNS)\n            add_custom_command(\n                OUTPUT Icon.icns\n                COMMAND ${PNG2ICNS}\n                ARGS\n                    Icon.icns\n                    ${icon_path}/icon_16x16.png\n                    ${icon_path}/icon_32x32.png\n                    ${icon_path}/icon_128x128.png\n                    ${icon_path}/icon_256x256.png\n                    ${icon_path}/icon_512x512.png\n            )\n        endif()\n        if (ICONUTIL OR PNG2ICNS)\n            set_source_files_properties(Icon.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources)\n            target_add_resource(Telegram Icon.icns)\n        endif()\n    endif()\n\n    set(lang_packs\n        en\n        de\n        es\n        it\n        nl\n        ko\n        pt-BR\n    )\n    foreach (lang ${lang_packs})\n        set(strings_path ${res_loc}/langs/${lang}.lproj/Localizable.strings)\n        set_source_files_properties(${strings_path} PROPERTIES\n        MACOSX_PACKAGE_LOCATION\n            Resources/${lang}.lproj\n        )\n        target_sources(Telegram PRIVATE ${strings_path})\n        source_group(TREE ${res_loc} PREFIX Resources FILES ${strings_path})\n    endforeach()\n\n    add_custom_command(TARGET Telegram\n    PRE_LINK\n        COMMAND mkdir -p $<TARGET_FILE_DIR:Telegram>/../Resources\n        COMMAND cp ${CMAKE_BINARY_DIR}/lib_ui.rcc $<TARGET_FILE_DIR:Telegram>/../Resources\n        COMMAND cp ${CMAKE_BINARY_DIR}/lib_spellcheck.rcc $<TARGET_FILE_DIR:Telegram>/../Resources\n    )\n    if (NOT build_macstore AND NOT DESKTOP_APP_DISABLE_CRASH_REPORTS)\n        if (DESKTOP_APP_USE_PACKAGED)\n            find_program(CRASHPAD_HANDLER crashpad_handler REQUIRED)\n        elseif (DESKTOP_APP_MAC_ARCH STREQUAL \"x86_64\" OR DESKTOP_APP_MAC_ARCH STREQUAL \"arm64\")\n            set(CRASHPAD_HANDLER \"${libs_loc}/crashpad/out/$<IF:$<CONFIG:Debug>,Debug,Release>.${DESKTOP_APP_MAC_ARCH}/crashpad_handler\")\n        else()\n            set(CRASHPAD_HANDLER \"${libs_loc}/crashpad/out/$<IF:$<CONFIG:Debug>,Debug,Release>/crashpad_handler\")\n        endif()\n        add_custom_command(TARGET Telegram\n        PRE_LINK\n            COMMAND mkdir -p $<TARGET_FILE_DIR:Telegram>/../Helpers\n            COMMAND cp ${CRASHPAD_HANDLER} $<TARGET_FILE_DIR:Telegram>/../Helpers/\n        )\n    endif()\nelse()\n    include(${cmake_helpers_loc}/external/glib/generate_dbus.cmake)\n    generate_dbus(Telegram org.freedesktop.portal. XdpBackground ${third_party_loc}/xdg-desktop-portal/data/org.freedesktop.portal.Background.xml)\n    generate_dbus(Telegram org.freedesktop. XdgNotifications ${src_loc}/platform/linux/org.freedesktop.Notifications.xml)\n\n    if (NOT DESKTOP_APP_DISABLE_X11_INTEGRATION)\n        target_link_libraries(Telegram\n        PRIVATE\n            desktop-app::external_xcb\n        )\n    endif()\nendif()\n\nif (build_macstore)\n    set(bundle_identifier \"org.telegram.desktop\")\n    set(bundle_entitlements \"Telegram Lite.entitlements\")\n    set(output_name \"Telegram Lite\")\n    target_link_options(Telegram\n    PRIVATE\n        -F${libs_loc}/breakpad/src/client/mac/build/Release\n    )\n    target_link_frameworks(Telegram PRIVATE Breakpad)\n    add_custom_command(TARGET Telegram\n    PRE_LINK\n        COMMAND rm -rf $<TARGET_FILE_DIR:Telegram>/../Frameworks\n        COMMAND mkdir -p $<TARGET_FILE_DIR:Telegram>/../Frameworks\n        COMMAND cp -a ${libs_loc}/breakpad/src/client/mac/build/Release/Breakpad.framework $<TARGET_FILE_DIR:Telegram>/../Frameworks/Breakpad.framework\n        COMMAND rm -rf $<TARGET_FILE_DIR:Telegram>/../Frameworks/Breakpad.framework/Resources/crash_report_sender.app\n        COMMAND rm -rf $<TARGET_FILE_DIR:Telegram>/../Frameworks/Breakpad.framework/Resources/Inspector\n    )\nelse()\n    if (CMAKE_GENERATOR STREQUAL Xcode)\n        set(bundle_identifier \"com.tdesktop.Telegram$<$<CONFIG:Debug>:Debug>\")\n    else()\n        set(bundle_identifier \"com.tdesktop.Telegram\")\n    endif()\n    set(bundle_entitlements \"Telegram.entitlements\")\n    set(output_name \"Telegram\")\n    if (LINUX OR APPLE)\n        set(output_name \"Forkgram\")\n    endif()\nendif()\n\nif (CMAKE_GENERATOR STREQUAL Xcode)\n    set(bundle_identifier_plist \"$(PRODUCT_BUNDLE_IDENTIFIER)\")\nelse()\n    set(bundle_identifier_plist ${bundle_identifier})\nendif()\n\nset_target_properties(Telegram PROPERTIES\n    OUTPUT_NAME ${output_name}\n    MACOSX_BUNDLE_GUI_IDENTIFIER ${bundle_identifier}\n    MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Telegram.plist\n    XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS \"${CMAKE_CURRENT_SOURCE_DIR}/Telegram/${bundle_entitlements}\"\n    XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER ${bundle_identifier}\n    XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION ${desktop_app_version_string}\n    XCODE_ATTRIBUTE_PRODUCT_NAME ${output_name}\n    XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME AppIcon\n    XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES\n    XCODE_ATTRIBUTE_COMBINE_HIDPI_IMAGES YES\n    XCODE_ATTRIBUTE_COPY_PHASE_STRIP NO\n    XCODE_ATTRIBUTE_ALWAYS_SEARCH_USER_PATHS NO\n    XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY libc++\n    XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS --deep\n    XCODE_ATTRIBUTE_CLANG_DEBUG_INFORMATION_LEVEL $<IF:$<CONFIG:Debug>,default,line-tables-only>\n)\nset(entitlement_sources\n    \"${CMAKE_CURRENT_SOURCE_DIR}/Telegram/Telegram.entitlements\"\n    \"${CMAKE_CURRENT_SOURCE_DIR}/Telegram/Telegram Lite.entitlements\"\n)\ntarget_sources(Telegram PRIVATE ${entitlement_sources})\nsource_group(TREE ${CMAKE_CURRENT_SOURCE_DIR}/Telegram PREFIX Resources FILES ${entitlement_sources})\n\ntarget_include_directories(Telegram PRIVATE ${src_loc})\n\ntarget_compile_definitions(Telegram\nPRIVATE\n    TDESKTOP_API_ID=${TDESKTOP_API_ID}\n    TDESKTOP_API_HASH=${TDESKTOP_API_HASH}\n    G_LOG_DOMAIN=\"Telegram\"\n)\n\nget_property(is_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)\nif (APPLE\n    OR is_multi_config\n    OR NOT CMAKE_EXECUTABLE_SUFFIX STREQUAL \"\"\n    OR NOT \"${output_name}\" STREQUAL \"Telegram\")\n    set(output_folder ${CMAKE_BINARY_DIR})\nelse()\n    set(output_folder ${CMAKE_BINARY_DIR}/bin)\nendif()\n\nset_target_properties(Telegram PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${output_folder})\n\nif (MSVC)\n    target_link_libraries(Telegram\n    PRIVATE\n        delayimp\n    )\n    target_link_options(Telegram\n    PRIVATE\n        /DELAYLOAD:secur32.dll\n        /DELAYLOAD:winmm.dll\n        /DELAYLOAD:ws2_32.dll\n        /DELAYLOAD:user32.dll\n        /DELAYLOAD:gdi32.dll\n        /DELAYLOAD:advapi32.dll\n        /DELAYLOAD:avrt.dll\n        /DELAYLOAD:shell32.dll\n        /DELAYLOAD:ole32.dll\n        /DELAYLOAD:oleaut32.dll\n        /DELAYLOAD:shlwapi.dll\n        /DELAYLOAD:iphlpapi.dll\n        /DELAYLOAD:gdiplus.dll\n        /DELAYLOAD:version.dll\n        /DELAYLOAD:dwmapi.dll\n        /DELAYLOAD:uxtheme.dll\n        /DELAYLOAD:crypt32.dll\n        /DELAYLOAD:bcrypt.dll\n        /DELAYLOAD:netapi32.dll\n        /DELAYLOAD:imm32.dll\n        /DELAYLOAD:userenv.dll\n        /DELAYLOAD:wtsapi32.dll\n        /DELAYLOAD:propsys.dll\n    )\n    if (QT_VERSION GREATER 6)\n        if (NOT build_winarm)\n            target_link_options(Telegram PRIVATE\n                /DELAYLOAD:API-MS-Win-EventLog-Legacy-l1-1-0.dll\n            )\n        endif()\n\n        target_link_options(Telegram\n        PRIVATE\n            /DELAYLOAD:API-MS-Win-Core-Console-l1-1-0.dll\n            /DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-0.dll\n            /DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-1.dll\n            /DELAYLOAD:API-MS-Win-Core-File-l1-1-0.dll\n            /DELAYLOAD:API-MS-Win-Core-LibraryLoader-l1-2-0.dll\n            /DELAYLOAD:API-MS-Win-Core-Localization-l1-2-0.dll\n            /DELAYLOAD:API-MS-Win-Core-Memory-l1-1-0.dll\n            /DELAYLOAD:API-MS-Win-Core-Memory-l1-1-1.dll\n            /DELAYLOAD:API-MS-Win-Core-ProcessThreads-l1-1-0.dll\n            /DELAYLOAD:API-MS-Win-Core-Synch-l1-2-0.dll # Synchronization.lib\n            /DELAYLOAD:API-MS-Win-Core-SysInfo-l1-1-0.dll\n            # /DELAYLOAD:API-MS-Win-Core-Timezone-l1-1-0.dll\n            /DELAYLOAD:API-MS-Win-Core-WinRT-l1-1-0.dll\n            /DELAYLOAD:API-MS-Win-Core-WinRT-Error-l1-1-0.dll\n            /DELAYLOAD:API-MS-Win-Core-WinRT-String-l1-1-0.dll\n            /DELAYLOAD:API-MS-Win-Security-CryptoAPI-l1-1-0.dll\n            # /DELAYLOAD:API-MS-Win-Shcore-Scaling-l1-1-1.dll # We shadowed GetDpiForMonitor\n            /DELAYLOAD:authz.dll # Authz.lib\n            /DELAYLOAD:comdlg32.dll\n            /DELAYLOAD:dwrite.dll # DWrite.lib\n            /DELAYLOAD:dxgi.dll # DXGI.lib\n            /DELAYLOAD:d3d9.dll # D3D9.lib\n            /DELAYLOAD:d3d11.dll # D3D11.lib\n            /DELAYLOAD:d3d12.dll # D3D12.lib\n            /DELAYLOAD:setupapi.dll # SetupAPI.lib\n            /DELAYLOAD:winhttp.dll\n        )\n    endif()\nendif()\n\ntarget_prepare_qrc(Telegram)\n\nif (NOT DESKTOP_APP_DISABLE_AUTOUPDATE AND NOT build_macstore AND NOT build_winstore)\n    add_executable(Updater WIN32)\n    init_non_host_target(Updater)\n\n    add_dependencies(Telegram Updater)\n\n    nice_target_sources(Updater ${src_loc}\n    PRIVATE\n        _other/updater_win.cpp\n        _other/updater_linux.cpp\n        _other/updater_osx.m\n        _other/updater.h\n    )\n\n    set_target_properties(Updater PROPERTIES\n        RUNTIME_OUTPUT_DIRECTORY ${output_folder}\n    )\n\n    if (WIN32)\n        get_filename_component(lib_base_loc lib_base REALPATH)\n        nice_target_sources(Updater ${lib_base_loc}\n        PRIVATE\n            base/platform/win/base_windows_safe_library.cpp\n            base/platform/win/base_windows_safe_library.h\n        )\n        target_include_directories(Updater PRIVATE ${lib_base_loc})\n        if (MSVC)\n            target_link_libraries(Updater\n            PRIVATE\n                delayimp\n            )\n            target_link_options(Updater\n            PRIVATE\n                /DELAYLOAD:user32.dll\n                /DELAYLOAD:advapi32.dll\n                /DELAYLOAD:shell32.dll\n                /DELAYLOAD:ole32.dll\n                /DELAYLOAD:shlwapi.dll\n            )\n        else()\n            target_link_options(Updater PRIVATE -municode)\n        endif()\n    elseif (APPLE)\n        add_custom_command(TARGET Updater\n        POST_BUILD\n            COMMAND mkdir -p $<TARGET_FILE_DIR:Telegram>/../Frameworks\n            COMMAND cp $<TARGET_FILE:Updater> $<TARGET_FILE_DIR:Telegram>/../Frameworks/\n        )\n    endif()\n\n    if (DESKTOP_APP_SPECIAL_TARGET)\n        add_executable(Packer)\n        init_target(Packer)\n\n        add_dependencies(Telegram Packer)\n\n        nice_target_sources(Packer ${src_loc}\n        PRIVATE\n            _other/packer.cpp\n            _other/packer.h\n        )\n\n        target_link_libraries(Packer\n        PRIVATE\n            desktop-app::external_qt\n            desktop-app::external_zlib\n            desktop-app::external_auto_updates\n            desktop-app::external_openssl\n        )\n\n        if (DESKTOP_APP_USE_PACKAGED)\n            target_compile_definitions(Packer PRIVATE PACKER_USE_PACKAGED)\n        endif()\n\n        set_target_properties(Packer PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${output_folder})\n    endif()\nelseif (build_winstore)\n    add_executable(StartupTask WIN32)\n    init_non_host_target(StartupTask)\n\n    add_dependencies(Telegram StartupTask)\n\n    nice_target_sources(StartupTask ${src_loc}\n    PRIVATE\n        _other/startup_task_win.cpp\n    )\n\n    set_target_properties(StartupTask PROPERTIES\n        RUNTIME_OUTPUT_DIRECTORY ${output_folder}\n    )\nendif()\n\nif (LINUX AND DESKTOP_APP_USE_PACKAGED)\n    include(GNUInstallDirs)\n    configure_file(\"../lib/xdg/org.telegram.desktop.service\" \"${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.service\" @ONLY)\n    configure_file(\"../lib/xdg/org.telegram.desktop.metainfo.xml\" \"${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.metainfo.xml\" @ONLY)\n    generate_appstream_changelog(Telegram \"${CMAKE_SOURCE_DIR}/changelog.txt\" \"${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.metainfo.xml\")\n    install(TARGETS Telegram RUNTIME DESTINATION \"${CMAKE_INSTALL_BINDIR}\" BUNDLE DESTINATION \"${CMAKE_INSTALL_BINDIR}\")\n    install(FILES \"Resources/art/icon16.png\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps\" RENAME \"io.github.forkgram.tdesktop.png\")\n    install(FILES \"Resources/art/icon16@2x.png\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16@2/apps\" RENAME \"io.github.forkgram.tdesktop.png\")\n    install(FILES \"Resources/art/icon32.png\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps\" RENAME \"io.github.forkgram.tdesktop.png\")\n    install(FILES \"Resources/art/icon32@2x.png\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32@2/apps\" RENAME \"io.github.forkgram.tdesktop.png\")\n    install(FILES \"Resources/art/icon48.png\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps\" RENAME \"io.github.forkgram.tdesktop.png\")\n    install(FILES \"Resources/art/icon48@2x.png\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48@2/apps\" RENAME \"io.github.forkgram.tdesktop.png\")\n    install(FILES \"Resources/art/icon64.png\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/apps\" RENAME \"io.github.forkgram.tdesktop.png\")\n    install(FILES \"Resources/art/icon64@2x.png\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64@2/apps\" RENAME \"io.github.forkgram.tdesktop.png\")\n    install(FILES \"Resources/art/icon128.png\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128/apps\" RENAME \"io.github.forkgram.tdesktop.png\")\n    install(FILES \"Resources/art/icon128@2x.png\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128@2/apps\" RENAME \"io.github.forkgram.tdesktop.png\")\n    install(FILES \"Resources/art/icon256.png\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps\" RENAME \"io.github.forkgram.tdesktop.png\")\n    install(FILES \"Resources/art/icon256@2x.png\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256@2/apps\" RENAME \"io.github.forkgram.tdesktop.png\")\n    install(FILES \"Resources/art/icon512.png\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512/apps\" RENAME \"io.github.forkgram.tdesktop.png\")\n    install(FILES \"Resources/art/icon512@2x.png\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512@2/apps\" RENAME \"io.github.forkgram.tdesktop.png\")\n    install(FILES \"Resources/icons/tray_monochrome.svg\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps\" RENAME \"io.github.forkgram.tdesktop-symbolic.svg\")\n    install(FILES \"Resources/icons/tray_monochrome_attention.svg\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps\" RENAME \"io.github.forkgram.tdesktop-attention-symbolic.svg\")\n    install(FILES \"Resources/icons/tray_monochrome_mute.svg\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps\" RENAME \"io.github.forkgram.tdesktop-mute-symbolic.svg\")\n    install(FILES \"../lib/xdg/org.telegram.desktop.desktop\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/applications\" RENAME \"io.github.forkgram.tdesktop.desktop\")\n    install(FILES \"${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.service\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/dbus-1/services\" RENAME \"io.github.forkgram.tdesktop.service\")\n    install(FILES \"${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.metainfo.xml\" DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/metainfo\" RENAME \"io.github.forkgram.tdesktop.metainfo.xml\")\nendif()\n"
  },
  {
    "path": "Telegram/Resources/default_shortcuts-custom.json",
    "content": "// This is a list of your own shortcuts for Telegram Desktop\n// You can see full list of commands in the 'shortcuts-default.json' file\n// Place a null value instead of a command string to switch the shortcut off\n// You can also edit them in Settings > Chat Settings > Keyboard Shortcuts.\n\n[\n    // {\n    //     \"command\": \"close_telegram\",\n    //     \"keys\": \"ctrl+f4\"\n    // },\n    // {\n    //     \"command\": \"quit_telegram\",\n    //     \"keys\": \"ctrl+q\"\n    // }\n]\n"
  },
  {
    "path": "Telegram/Resources/export_html/css/style.css",
    "content": "body {\n    margin: 0;\n    font: 12px/18px 'Open Sans',\"Lucida Grande\",\"Lucida Sans Unicode\",Arial,Helvetica,Verdana,sans-serif;\n}\nstrong {\n    font-weight: 700;\n}\ncode, kbd, pre, samp {\n    font-family: Menlo,Monaco,Consolas,\"Courier New\",monospace;\n}\ncode {\n    padding: 2px 4px;\n    font-size: 90%;\n    color: #c7254e;\n    background-color: #f9f2f4;\n    border-radius: 4px;\n}\npre {\n    display: block;\n    margin: 0;\n    line-height: 1.42857143;\n    word-break: break-all;\n    word-wrap: break-word;\n    color: #333;\n    background-color: #f5f5f5;\n    border-radius: 4px;\n    overflow: auto;\n    padding: 3px;\n    border: 1px solid #eee;\n    max-height: none;\n    font-size: inherit;\n}\n.clearfix:after {\n    content: \" \";\n    visibility: hidden;\n    display: block;\n    height: 0;\n    clear: both;\n}\n.pull_left {\n    float: left;\n}\n.pull_right {\n    float: right;\n}\n.page_wrap {\n    background-color: #ffffff;\n    color: #000000;\n}\n.page_wrap a {\n    color: #168acd;\n    text-decoration: none;\n}\n.page_wrap a:hover {\n    text-decoration: underline;\n}\n.page_header {\n    position: fixed;\n    z-index: 10;\n    background-color: #ffffff;\n    width: 100%;\n    border-bottom: 1px solid #e3e6e8;\n}\n.page_header .content {\n    width: 480px;\n    margin: 0 auto;\n    border-radius: 0 !important;\n}\n.page_header a.content {\n    background-repeat: no-repeat;\n    background-position: 24px 21px;\n    background-size: 24px 24px;\n}\n.bold {\n    color: #212121;\n    font-weight: 700;\n}\n.details {\n    color: #70777b;\n}\n.page_header .content .text {\n    padding: 24px 24px 22px 24px;\n    font-size: 22px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n.page_header a.content .text {\n    padding: 24px 24px 22px 82px;\n}\n.page_body {\n    padding-top: 64px;\n    width: 480px;\n    margin: 0 auto;\n}\n.page_about {\n    padding: 24px 24px;\n}\n.with_divider {\n    border-top: 1px solid #e3e6e8;\n}\n.userpic_link {\n    display: block;\n    text-decoration: none;\n}\n.userpic_link:hover {\n    text-decoration: none;\n}\n.userpic {\n    display: block;\n    border-radius: 50%;\n    overflow: hidden;\n}\n.story {\n    display: block;\n    border-radius: 4px;\n    overflow: hidden;\n}\n.userpic .initials {\n    display: block;\n    color: #fff;\n    text-align: center;\n    text-transform: uppercase;\n    user-select: none;\n}\n.color_red,\n.userpic1,\n.media_call .fill,\n.media_file .fill,\n.media_live_location .fill {\n    background-color: #ff5555;\n}\n.color_green,\n.userpic2,\n.media_call.success .fill,\n.media_photo .fill {\n    background-color: #64bf47;\n}\n.color_yellow,\n.userpic3,\n.media_venue .fill {\n    background-color: #ffab00;\n}\n.color_blue,\n.userpic4,\n.media_audio_file .fill,\n.media_voice_message .fill {\n    background-color: #4f9cd9;\n}\n.color_purple,\n.userpic5,\n.media_game .fill {\n    background-color: #9884e8;\n}\n.color_pink,\n.userpic6,\n.media_invoice .fill {\n    background-color: #e671a5;\n}\n.color_sea,\n.userpic7,\n.media_location .fill,\n.media_video .fill {\n    background-color: #47bcd1;\n}\n.color_orange,\n.userpic8,\n.media_contact .fill {\n    background-color: #ff8c44;\n}\n.personal_info {\n    padding: 24px;\n}\n.personal_info .userpic .initials {\n    font-size: 30px;\n}\n.personal_info .rows {\n    float: left;\n    padding-right: 24px;\n}\n.personal_info .names {\n    width: 164px;\n}\n.personal_info .info {\n    width: 124px;\n}\n.personal_info .bio {\n    width: 400px;\n}\n.personal_info .row {\n    padding-bottom: 16px;\n}\na.block_link {\n    display: block;\n    text-decoration: none !important;\n    border-radius: 4px;\n}\na.block_link:hover {\n    text-decoration: none !important;\n    background-color: #f5f7f8;\n}\na.expanded {\n    padding: 2px 8px;\n    margin: -2px -8px;\n}\n.sections {\n    padding: 11px 0;\n}\n.section {\n    height: 48px;\n    background-position: 24px 12px;\n    background-repeat: no-repeat;\n    background-size: 24px 24px;\n}\n.section .counter {\n    float: right;\n    padding: 14px 24px 0;\n    font-size: 15px;\n}\n.section .label {\n    padding: 15px 0 0 82px;\n    font-size: 15px;\n}\n.list_page .page_about {\n    padding: 16px 24px 0;\n    font-size: 11px;\n}\n.list_page .entry_list {\n    padding: 16px 0;\n}\n.list_page .entry {\n    padding: 10px 16px;\n}\n.list_page .entry .userpic .initials {\n    font-size: 18px;\n}\n.list_page .entry .body {\n    margin-left: 66px;\n}\n.list_page .entry .name {\n    padding: 4px 0 2px;\n    font-size: 14px;\n}\n.list_page .entry .subname {\n    padding-top: 4px;\n}\n.list_page .entry .details_entry {\n    padding-top: 4px;\n}\n.list_page .entry .info {\n    font-size: 11px;\n    padding-top: 5px;\n}\n.history {\n    padding: 16px 0;\n}\n.message {\n    margin: 0 -10px;\n    transition: background-color 2.0s ease;\n}\ndiv.selected {\n    background-color: rgba(242,246,250,255);\n    transition: background-color 0.5s ease;\n}\n.service {\n    padding: 10px 24px;\n}\n.service .body {\n    text-align: center;\n}\n.service .userpic_wrap {\n    padding-top: 10px;\n}\n.service .userpic {\n    margin: 0 auto;\n}\n.service .userpic .initials {\n    font-size: 24px;\n}\n.message .userpic .initials {\n    font-size: 16px;\n}\n.default {\n    padding: 10px;\n}\n.default.joined {\n    margin-top: -10px;\n}\n.default .from_name {\n    color: #3892db;\n    font-weight: 700;\n    padding-bottom: 5px;\n}\n.default .from_name .details {\n    font-weight: normal;\n}\n.default .body {\n    margin-left: 60px;\n}\n.default .text {\n    word-wrap: break-word;\n    line-height: 150%;\n    unicode-bidi: plaintext;\n    text-align: start;\n}\n.default .reply_to,\n.default .media_wrap {\n    padding-bottom: 5px;\n}\n.default .media {\n    margin: 0 -10px;\n    padding: 5px 10px;\n}\n.default .media .fill,\n.default .media .thumb {\n    width: 48px;\n    height: 48px;\n    border-radius: 50%;\n}\n.default .media .fill {\n    background-repeat: no-repeat;\n    background-position: 12px 12px;\n    background-size: 24px 24px;\n}\n.default .media .title,\n.default .media_poll .question {\n    padding-top: 4px;\n    font-size: 14px;\n}\n.default .media .description {\n    color: #000000;\n    padding-top: 4px;\n    font-size: 13px;\n}\n.default .media .status {\n    padding-top: 4px;\n    font-size: 13px;\n}\n.default .video_file_wrap,\n.default .animated_wrap {\n    position: relative;\n}\n.default .video_file,\n.default .animated,\n.default .photo,\n.default .sticker {\n    display: block;\n}\n.video_duration {\n    background: rgba(0, 0, 0, .4);\n    padding: 0px 5px;\n    position: absolute;\n    z-index: 2;\n    border-radius: 2px;\n    right: 3px;\n    bottom: 3px;\n    color: #ffffff;\n    font-size: 11px;\n}\n.video_play_bg {\n    background: rgba(0, 0, 0, .4);\n    width: 40px;\n    height: 40px;\n    line-height: 0;\n    position: absolute;\n    z-index: 2;\n    border-radius: 50%;\n    overflow: hidden;\n    margin: -20px auto 0 -20px;\n    top: 50%;\n    left: 50%;\n    pointer-events: none;\n}\n.video_play {\n    position: absolute;\n    display: inline-block;\n    top: 50%;\n    left: 50%;\n    margin-left: -5px;\n    margin-top: -9px;\n    z-index: 1;\n    width: 0;\n    height: 0;\n    border-style: solid;\n    border-width: 9px 0 9px 14px;\n    border-color: transparent transparent transparent #fff;\n}\n.gif_play {\n    font-weight: 700;\n    color: #FFF;\n    display: block;\n    line-height: 40px;\n    font-size: 13px;\n    text-align: center;\n}\n.pagination {\n    text-align: center;\n    padding: 20px;\n    font-size: 16px;\n}\n\n.toast_container {\n    position: fixed;\n    left: 50%;\n    top: 50%;\n    opacity: 0;\n    transition: opacity 3.0s ease;\n}\n.toast_body {\n    margin: 0 -50%;\n    float: left;\n    border-radius: 15px;\n    padding: 10px 20px;\n    background: rgba(0, 0, 0, 0.7);\n    color: #ffffff;\n}\ndiv.toast_shown {\n    opacity: 1;\n    transition: opacity 0.4s ease;\n}\n\n.section.calls {\n    background-image: url(../images/section_calls.png);\n}\n.section.chats {\n    background-image: url(../images/section_chats.png);\n}\n.section.contacts {\n    background-image: url(../images/section_contacts.png);\n}\n.section.frequent {\n    background-image: url(../images/section_frequent.png);\n}\n.section.photos {\n    background-image: url(../images/section_photos.png);\n}\n.section.sessions {\n    background-image: url(../images/section_sessions.png);\n}\n.section.stories {\n    background-image: url(../images/section_stories.png);\n}\n.section.music {\n    background-image: url(../images/section_music.png);\n}\n.section.web {\n    background-image: url(../images/section_web.png);\n}\n.section.other {\n    background-image: url(../images/section_other.png)\n}\n.page_header a.content {\n    background-image: url(../images/back.png);\n}\n.media_call .fill {\n    background-image: url(../images/media_call.png)\n}\n.media_contact .fill {\n    background-image: url(../images/media_contact.png)\n}\n.media_file .fill {\n    background-image: url(../images/media_file.png)\n}\n.media_game .fill {\n    background-image: url(../images/media_game.png)\n}\n.media_live_location .fill,\n.media_location .fill,\n.media_venue .fill {\n    background-image: url(../images/media_location.png)\n}\n.media_audio_file .fill {\n    background-image: url(../images/media_music.png)\n}\n.media_invoice .fill {\n    background-image: url(../images/media_shop.png)\n}\n.media_voice_message .fill {\n    background-image: url(../images/media_voice.png)\n}\n.media_photo .fill {\n    background-image: url(../images/media_photo.png)\n}\n.media_video .fill {\n    background-image: url(../images/media_video.png)\n}\n.audio_icon {\n    width: 48px;\n    height: 48px;\n    border-radius: 50%;\n    background-color: #4f9cd9;\n    background-image: url(../images/media_music.png);\n    background-repeat: no-repeat;\n    background-position: 12px 12px;\n    background-size: 24px 24px;\n}\n\n@media only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) {\n.section.calls {\n    background-image: url(../images/section_calls@2x.png);\n}\n.section.chats {\n    background-image: url(../images/section_chats@2x.png);\n}\n.section.contacts {\n    background-image: url(../images/section_contacts@2x.png);\n}\n.section.frequent {\n    background-image: url(../images/section_frequent@2x.png);\n}\n.section.photos {\n    background-image: url(../images/section_photos@2x.png);\n}\n.section.sessions {\n    background-image: url(../images/section_sessions@2x.png);\n}\n.section.stories {\n    background-image: url(../images/section_stories@2x.png);\n}\n.section.music {\n    background-image: url(../images/section_music@2x.png);\n}\n.section.web {\n    background-image: url(../images/section_web@2x.png);\n}\n.section.other {\n    background-image: url(../images/section_other@2x.png);\n}\n.page_header a.content {\n    background-image: url(../images/back@2x.png);\n}\n.media_call .fill {\n    background-image: url(../images/media_call@2x.png)\n}\n.media_contact .fill {\n    background-image: url(../images/media_contact@2x.png)\n}\n.media_file .fill {\n    background-image: url(../images/media_file@2x.png)\n}\n.media_game .fill {\n    background-image: url(../images/media_game@2x.png)\n}\n.media_live_location .fill,\n.media_location .fill,\n.media_venue .fill {\n    background-image: url(../images/media_location@2x.png)\n}\n.media_audio_file .fill {\n    background-image: url(../images/media_music@2x.png)\n}\n.media_invoice .fill {\n    background-image: url(../images/media_shop@2x.png)\n}\n.media_voice_message .fill {\n    background-image: url(../images/media_voice@2x.png)\n}\n.media_photo .fill {\n    background-image: url(../images/media_photo@2x.png)\n}\n.media_video .fill {\n    background-image: url(../images/media_video@2x.png)\n}\n.audio_icon {\n    background-image: url(../images/media_music@2x.png);\n}\n}\n\n.spoiler {\n    background: #e8e8e8;\n}\n.spoiler.hidden {\n    background: #a9a9a9;\n    cursor: pointer;\n    border-radius: 3px;\n}\n.spoiler.hidden span {\n    opacity: 0;\n    user-select: none;\n}\n\n.bot_buttons_table {\n    border-spacing: 0px 2px;\n    width: 100%;\n}\n.bot_button {\n    border-radius: 8px;\n    text-align: center;\n    vertical-align: middle;\n    background-color: #168acd40;\n}\n.bot_button_row {\n    display: table;\n    table-layout: fixed;\n    padding: 0px;\n    width:100%;\n}\n.bot_button_row div {\n    display: table-cell;\n}\n.bot_button_column_separator {\n    width: 2px\n}\n\n.reactions {\n    margin: 5px 0;\n}\n\n.reactions .reaction {\n    display: inline-flex;\n    height: 20px;\n    border-radius: 15px;\n    background-color: #e8f5fc;\n    color: #168acd;\n    font-weight: bold;\n    margin-bottom: 5px;\n}\n\n.reactions .reaction.active {\n    background-color: #40a6e2;\n    color: #fff;\n}\n\n.reactions .reaction.paid {\n    background-color: #fdf6e1;\n    color: #c58523;\n}\n\n.reactions .reaction.active.paid {\n    background-color: #ecae0a;\n    color: #fdf6e1;\n}\n\n.reactions .reaction .emoji {\n    line-height: 20px;\n    margin: 0 5px;\n    font-size: 15px;\n}\n\n.reactions .reaction .userpic:not(:first-child) {\n    margin-left: -8px;\n}\n\n.reactions .reaction .userpic {\n    display: inline-block;\n}\n\n.reactions .reaction .userpic .initials {\n    font-size: 8px;\n}\n\n.reactions .reaction .count {\n    margin-right: 8px;\n    line-height: 20px;\n}\n\n@media (prefers-color-scheme: dark) {\nhtml, body {\n    background-color: #1a2026; /* groupCallBg */\n    margin: 0;\n    padding: 0;\n}\n.page_wrap {\n    background-color: #1a2026; /* groupCallBg */\n    color: #ffffff; /* groupCallMembersFg */\n    min-height: 100vh;\n}\n.page_wrap a {\n    color: #4db8ff; /* groupCallActiveFg */\n}\n.page_header {\n    background-color: #1a2026; /* groupCallBg */\n    border-bottom: 1px solid #2c333d; /* groupCallMembersBg */\n}\n.bold {\n    color: #ffffff; /* groupCallMembersFg */\n}\n.details {\n    color: #91979e; /* groupCallMemberNotJoinedStatus */\n}\n.page_body {\n    background-color: #1a2026; /* groupCallBg */\n}\ncode {\n    color: #ff8aac; /* historyPeer6UserpicBg */\n    background-color: #2c333d; /* groupCallMembersBg */\n}\npre {\n    color: #ffffff; /* groupCallMembersFg */\n    background-color: #2c333d; /* groupCallMembersBg */\n    border: 1px solid #323a45; /* groupCallMembersBgOver */\n}\n.with_divider {\n    border-top: 1px solid #2c333d; /* groupCallMembersBg */\n}\na.block_link:hover {\n    background-color: #323a45; /* groupCallMembersBgOver */\n}\n.list_page .entry {\n    color: #ffffff; /* groupCallMembersFg */\n}\n.message {\n    color: #ffffff; /* groupCallMembersFg */\n}\ndiv.selected {\n    background-color: #323a45; /* groupCallMembersBgOver */\n}\n.default .from_name {\n    color: #4db8ff; /* groupCallActiveFg */\n}\n.default .media .description {\n    color: #ffffff; /* groupCallMembersFg */\n}\nmsgInBg,\n.historyComposeAreaBg {\n    background-color: #2c333d; /* groupCallMembersBg */\n}\nmsgOutBg {\n    background-color: #323a45; /* groupCallMembersBgOver */\n}\nmsgInBgSelected {\n    background-color: #39424f; /* groupCallMembersBgRipple */\n}\nmsgOutBgSelected {\n    background-color: #39424f; /* groupCallMembersBgRipple */\n}\n.spoiler {\n    background: #323a45; /* groupCallMembersBgOver */\n}\n.spoiler.hidden {\n    background: #61c0ff; /* groupCallMemberInactiveStatus */\n}\n.bot_button {\n    background-color: #4db8ff40; /* groupCallActiveFg with opacity */\n}\n.reactions .reaction {\n    background-color: #2c333d; /* groupCallMembersBg */\n    color: #4db8ff; /* groupCallActiveFg */\n}\n.reactions .reaction.active {\n    background-color: #4db8ff; /* groupCallActiveFg */\n    color: #1a2026; /* groupCallBg */\n}\n.reactions .reaction.paid {\n    background-color: #323a45; /* groupCallMembersBgOver */\n    color: #febb5b; /* historyPeer8UserpicBg */\n}\n.reactions .reaction.active.paid {\n    background-color: #febb5b; /* historyPeer8UserpicBg */\n    color: #1a2026; /* groupCallBg */\n}\n}"
  },
  {
    "path": "Telegram/Resources/export_html/js/script.js",
    "content": "\"use strict\";\n\nwindow.AllowBackFromHistory = false;\nfunction CheckLocation() {\n    var start = \"#go_to_message\";\n    var hash = location.hash;\n    if (hash.substr(0, start.length) == start) {\n        var messageId = parseInt(hash.substr(start.length));\n        if (messageId) {\n            GoToMessage(messageId);\n        }\n    } else if (hash == \"#allow_back\") {\n        window.AllowBackFromHistory = true;\n    }\n}\n\nfunction ShowToast(text) {\n    var container = document.createElement(\"div\");\n    container.className = \"toast_container\";\n    var inner = container.appendChild(document.createElement(\"div\"));\n    inner.className = \"toast_body\";\n    inner.appendChild(document.createTextNode(text));\n    var appended = document.body.appendChild(container);\n    setTimeout(function () {\n        AddClass(appended, \"toast_shown\");\n        setTimeout(function () {\n            RemoveClass(appended, \"toast_shown\");\n            setTimeout(function () {\n                document.body.removeChild(appended);\n            }, 3000);\n        }, 3000);\n    }, 0);\n}\n\nfunction ShowHashtag(tag) {\n    ShowToast(\"This is a hashtag '#\" + tag + \"' link.\");\n    return false;\n}\n\nfunction ShowCashtag(tag) {\n    ShowToast(\"This is a cashtag '$\" + tag + \"' link.\");\n    return false;\n}\n\nfunction ShowBotCommand(command) {\n    ShowToast(\"This is a bot command '/\" + command + \"' link.\");\n    return false;\n}\n\nfunction ShowMentionName() {\n    ShowToast(\"This is a link to a user mentioned by name.\");\n    return false;\n}\n\nfunction ShowNotLoadedEmoji() {\n    ShowToast(\"This custom emoji is not included, change data exporting settings to download.\");\n    return false;\n}\n\nfunction ShowNotAvailableEmoji() {\n    ShowToast(\"This custom emoji is not available.\");\n    return false;\n}\n\nfunction ShowTextCopied(content) {\n    navigator.clipboard.writeText(content);\n    ShowToast(\"Text copied to clipboard.\");\n    return false;\n}\n\nfunction ShowSpoiler(target) {\n    if (target.classList.contains(\"hidden\")) {\n        target.classList.toggle(\"hidden\");\n    }\n}\n\nfunction AddClass(element, name) {\n    var current = element.className;\n    var expression = new RegExp('(^|\\\\s)' + name + '(\\\\s|$)', 'g');\n    if (expression.test(current)) {\n        return;\n    }\n    element.className = current + ' ' + name;\n}\n\nfunction RemoveClass(element, name) {\n    var current = element.className;\n    var expression = new RegExp('(^|\\\\s)' + name + '(\\\\s|$)', '');\n    var match = expression.exec(current);\n    while ((match = expression.exec(current)) != null) {\n        if (match[1].length > 0 && match[2].length > 0) {\n            current = current.substr(0, match.index + match[1].length)\n                + current.substr(match.index + match[0].length);\n        } else {\n            current = current.substr(0, match.index)\n                + current.substr(match.index + match[0].length);\n        }\n    }\n    element.className = current;\n}\n\nfunction EaseOutQuad(t) {\n    return t * t;\n}\n\nfunction EaseInOutQuad(t) {\n    return (t < 0.5) ? (2 * t * t) : ((4 - 2 * t) * t - 1);\n}\n\nfunction ScrollHeight() {\n    if (\"innerHeight\" in window) {\n        return window.innerHeight;\n    } else if (document.documentElement) {\n        return document.documentElement.clientHeight;\n    }\n    return document.body.clientHeight;\n}\n\nfunction ScrollTo(top, callback) {\n    var html = document.documentElement;\n    var current = html.scrollTop;\n    var delta = top - current;\n    var finish = function () {\n        html.scrollTop = top;\n        if (callback) {\n            callback();\n        }\n    };\n    if (!window.performance.now || delta == 0) {\n        finish();\n        return;\n    }\n    var transition = EaseOutQuad;\n    var max = 300;\n    if (delta < -max) {\n        current = top + max;\n        delta = -max;\n    } else if (delta > max) {\n        current = top - max;\n        delta = max;\n    } else {\n        transition = EaseInOutQuad;\n    }\n    var duration = 150;\n    var interval = 7;\n    var time = window.performance.now();\n    var animate = function () {\n        var now = window.performance.now();\n        if (now >= time + duration) {\n            finish();\n            return;\n        }\n        var dt = (now - time) / duration;\n        html.scrollTop = Math.round(current + delta * transition(dt));\n        setTimeout(animate, interval);\n    };\n    setTimeout(animate, interval);\n}\n\nfunction ScrollToElement(element, callback) {\n    var header = document.getElementsByClassName(\"page_header\")[0];\n    var headerHeight = header.offsetHeight;\n    var html = document.documentElement;\n    var scrollHeight = ScrollHeight();\n    var available = scrollHeight - headerHeight;\n    var padding = 10;\n    var top = element.offsetTop;\n    var height = element.offsetHeight;\n    var desired = top\n        - Math.max((available - height) / 2, padding)\n        - headerHeight;\n    var scrollTopMax = html.offsetHeight - scrollHeight;\n    ScrollTo(Math.min(desired, scrollTopMax), callback);\n}\n\nfunction GoToMessage(messageId) {\n    var element = document.getElementById(\"message\" + messageId);\n    if (element) {\n        var hash = \"#go_to_message\" + messageId;\n        if (location.hash != hash) {\n            location.hash = hash;\n        }\n        ScrollToElement(element, function () {\n            AddClass(element, \"selected\");\n            setTimeout(function () {\n                RemoveClass(element, \"selected\");\n            }, 1000);\n        });\n    } else {\n        ShowToast(\"This message was not exported. Maybe it was deleted.\");\n    }\n    return false;\n}\n\nfunction GoBack(anchor) {\n    if (!window.AllowBackFromHistory) {\n        return true;\n    }\n    history.back();\n    if (!anchor || !anchor.getAttribute) {\n        return true;\n    }\n    var destination = anchor.getAttribute(\"href\");\n    if (!destination) {\n        return true;\n    }\n    setTimeout(function () {\n        location.href = destination;\n    }, 100);\n    return false;\n}\n"
  },
  {
    "path": "Telegram/Resources/icons/calls/hands.lottie",
    "content": "{\"v\":\"5.5.7\",\"meta\":{\"g\":\"LottieFiles AE 0.1.20\",\"a\":\"\",\"k\":\"\",\"d\":\"\",\"tc\":\"\"},\"fr\":60,\"ip\":0,\"op\":540,\"w\":60,\"h\":60,\"nm\":\"Raise Hand Test\",\"ddd\":0,\"assets\":[{\"id\":\"comp_0\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":3,\"nm\":\"NULL CONTROL\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":0,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[256.989,275.127,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[115,115,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[500,500,100],\"ix\":6}},\"ao\":0,\"ip\":0,\"op\":119,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"head\",\"parent\":6,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.833],\"y\":[1]},\"o\":{\"x\":[0.6],\"y\":[0]},\"t\":1,\"s\":[0]},{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0]},\"t\":11,\"s\":[0]},{\"i\":{\"x\":[0.833],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0]},\"t\":23,\"s\":[-13]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0]},\"t\":66,\"s\":[-13]},{\"i\":{\"x\":[0.09],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"t\":75,\"s\":[7]},{\"t\":92,\"s\":[0]}],\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":1,\"s\":[0,-37.327,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":11,\"s\":[0,-26.827,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":23,\"s\":[-4.4,-39.427,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":36,\"s\":[-2.401,-30.424,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":50,\"s\":[-4.4,-39.427,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.09,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":66,\"s\":[-2.401,-30.424,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":92,\"s\":[0,-37.327,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":1,\"s\":[100,100,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":5,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":11,\"s\":[105,95,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":16,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":23,\"s\":[105,95,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":28,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0,0,0]},\"t\":36,\"s\":[100,100,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":44,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":50,\"s\":[105,95,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":58,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0,0,0]},\"t\":66,\"s\":[100,100,100]},{\"i\":{\"x\":[0.09,0.09,0.09],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":75,\"s\":[95,105,100]},{\"t\":92,\"s\":[100,100,100]}],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],\"o\":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],\"v\":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Oval\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":3,\"nm\":\"hands\",\"parent\":6,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":0,\"ix\":11},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.5],\"y\":[1]},\"o\":{\"x\":[0.5],\"y\":[0]},\"t\":0,\"s\":[0]},{\"i\":{\"x\":[0.5],\"y\":[1]},\"o\":{\"x\":[0.5],\"y\":[0]},\"t\":21,\"s\":[-10]},{\"i\":{\"x\":[0.17],\"y\":[1]},\"o\":{\"x\":[0.5],\"y\":[0]},\"t\":65,\"s\":[-10]},{\"t\":93,\"s\":[0]}],\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[-2.341,-17.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":10,\"s\":[-2.341,-9.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":21,\"s\":[-3.741,-17.977,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":35,\"s\":[-1.541,-11.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":49,\"s\":[-1.541,-17.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.17,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":65,\"s\":[-1.541,-11.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":93,\"s\":[-2.341,-17.777,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[115,115,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[20,20,100],\"ix\":6}},\"ao\":0,\"ip\":0,\"op\":120,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"hands 3\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[115,115,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[500,500,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],\"o\":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],\"v\":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.6,\"y\":0},\"t\":10,\"s\":[{\"i\":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.659,-0.445]],\"o\":[[-3.232,5.731],[0.566,4.637],[3.994,-0.505],[1.183,0.079]],\"v\":[[-18.219,-21.464],[-27.079,-2.224],[-7.644,3.484],[11.49,2.929]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":16,\"s\":[{\"i\":[[0,0],[-1.902,-5.873],[-9.833,-0.14],[-7.372,0.373]],\"o\":[[-4.328,1.758],[1.817,5.11],[4.766,0.217],[1.182,0.009]],\"v\":[[-17.041,-20.099],[-26.446,-5.406],[-7.078,3.541],[9.615,2.763]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.5,\"y\":0},\"t\":23,\"s\":[{\"i\":[[0,0],[-4.205,-5.986],[-9.957,-1.926],[-4.458,0.474]],\"o\":[[5.753,4.944],[4.232,6.025],[3.773,0.73],[1.179,-0.125]],\"v\":[[-37.604,-27.175],[-25.224,-11.553],[-5.844,2.761],[7.055,3.069]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":27,\"s\":[{\"i\":[[0,0],[-1.728,-7.979],[-9.811,-4.073],[-4.458,0.474]],\"o\":[[3.018,5.449],[1.024,4.728],[4.574,1.385],[1.179,-0.125]],\"v\":[[-30.389,-31.532],[-24.556,-12.392],[-6.174,2.474],[7.055,3.069]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.5,\"y\":0},\"t\":32,\"s\":[{\"i\":[[0,0],[-0.133,-5.267],[-9.077,-3.752],[-4.458,0.474]],\"o\":[[-4.804,4.722],[0.193,7.66],[5.825,2.408],[1.179,-0.125]],\"v\":[[-10.804,-33.837],[-18.834,-15.906],[-6.69,2.025],[7.055,3.069]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":37,\"s\":[{\"i\":[[0,0],[-0.271,-4.621],[-9.517,-2.839],[-4.458,0.474]],\"o\":[[-3.394,4.297],[0.242,4.13],[4.799,1.569],[1.179,-0.125]],\"v\":[[-15.907,-30.468],[-22.029,-13.729],[-6.267,2.393],[7.055,3.069]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.5,\"y\":0},\"t\":42,\"s\":[{\"i\":[[0,0],[-3.928,-6.171],[-9.883,-2.273],[-3.875,0.81]],\"o\":[[5.522,5.2],[3.954,6.211],[4.837,1.112],[1.161,-0.243]],\"v\":[[-36.207,-29.267],[-24.551,-13.099],[-5.745,2.464],[7.055,3.069]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":48,\"s\":[{\"i\":[[0,0],[-1.728,-7.979],[-9.811,-4.073],[-4.458,0.474]],\"o\":[[3.018,5.449],[1.024,4.728],[4.574,1.385],[1.179,-0.125]],\"v\":[[-30.389,-31.532],[-24.556,-12.392],[-6.174,2.474],[7.055,3.069]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.5,\"y\":0},\"t\":54,\"s\":[{\"i\":[[0,0],[-0.133,-5.267],[-9.319,-3.103],[-4.568,0.674]],\"o\":[[-4.804,4.722],[0.193,7.66],[5.735,1.909],[1.173,-0.173]],\"v\":[[-10.804,-33.837],[-18.834,-15.906],[-6.656,1.832],[7.072,2.972]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":61,\"s\":[{\"i\":[[0,0],[-0.271,-4.621],[-9.517,-2.839],[-4.458,0.474]],\"o\":[[-3.394,4.297],[0.242,4.13],[4.799,1.569],[1.179,-0.125]],\"v\":[[-15.907,-30.468],[-22.029,-13.729],[-6.267,2.393],[7.055,3.069]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.5,\"y\":0},\"t\":68,\"s\":[{\"i\":[[0,0],[-4.205,-5.986],[-9.957,-1.926],[-4.458,0.474]],\"o\":[[5.753,4.944],[4.232,6.025],[3.773,0.73],[1.179,-0.125]],\"v\":[[-37.604,-27.175],[-25.224,-11.553],[-5.844,2.761],[7.055,3.069]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":78,\"s\":[{\"i\":[[0,0],[-3.924,-6.477],[-10.111,-0.781],[-3.392,0.105]],\"o\":[[4.365,3.67],[3.808,6.286],[4.491,0.347],[1.183,-0.028]],\"v\":[[-37.672,-26.337],[-25.271,-10.97],[-5.844,2.761],[6.463,3.033]],\"c\":false}]},{\"t\":95,\"s\":[{\"i\":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],\"o\":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],\"v\":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":4.8,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path-8\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"hands 2\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[115,115,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[500,500,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],\"o\":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],\"v\":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":10,\"s\":[{\"i\":[[-2.831,0.099],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],\"o\":[[3.206,-0.113],[11.894,0.339],[2.109,3.377],[0,0]],\"v\":[[2.767,2.88],[11.324,2.869],[28.065,7.836],[30.818,22.809]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":22,\"s\":[{\"i\":[[-2.833,0.019],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],\"o\":[[4.963,-0.034],[5.323,2.097],[1.94,4.605],[0,0]],\"v\":[[2.632,3.384],[12.827,2.873],[22.174,19.291],[30.152,32.241]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.7,\"y\":0},\"t\":36,\"s\":[{\"i\":[[-2.83,-0.135],[-3.536,-1.048],[-1.592,-3.476],[-1.09,-6.235]],\"o\":[[7.035,0.337],[5.485,1.626],[1.67,3.647],[0,0]],\"v\":[[1.498,3.488],[12.683,2.542],[23.022,15.874],[26.398,31.323]],\"c\":false}]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":50,\"s\":[{\"i\":[[-2.829,0.15],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],\"o\":[[4.712,-0.25],[5.323,2.097],[1.94,4.605],[0,0]],\"v\":[[2.417,3.447],[12.827,2.873],[22.174,19.291],[30.152,32.241]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.4,\"y\":0},\"t\":66,\"s\":[{\"i\":[[-2.833,0.023],[-3.532,-0.206],[-2.149,-3.438],[-1.064,-6.535]],\"o\":[[4.317,-0.036],[4.623,0.27],[2.442,3.906],[0,0]],\"v\":[[1.983,3.325],[12.861,3.014],[29.432,13.325],[34.61,28.122]],\"c\":false}]},{\"t\":90,\"s\":[{\"i\":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],\"o\":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],\"v\":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":4.8,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path-8\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"body\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[116.171,146.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":10,\"s\":[116.171,151.524,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":21,\"s\":[116.171,140.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":35,\"s\":[116.171,146.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":49,\"s\":[116.171,143.924,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.17,\"y\":0.17},\"o\":{\"x\":0.6,\"y\":0.6},\"t\":65,\"s\":[116.171,146.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":93,\"s\":[116.171,146.724,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,14.211,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.5,0.5,0.5],\"y\":[0,0,0]},\"t\":0,\"s\":[100,100,100]},{\"i\":{\"x\":[0.5,0.5,0.5],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":5,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.484,0.484,0.484],\"y\":[0,0,0]},\"t\":10,\"s\":[100,100,100]},{\"i\":{\"x\":[0.486,0.486,0.486],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":15,\"s\":[103,97,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.5,0.5,0.5],\"y\":[0,0,0]},\"t\":21,\"s\":[100,100,100]},{\"i\":{\"x\":[0.5,0.5,0.5],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":27,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.5,0.5,0.5],\"y\":[0,0,0]},\"t\":35,\"s\":[100,100,100]},{\"i\":{\"x\":[0.5,0.5,0.5],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":42,\"s\":[103,97,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.5,0.5,0.5],\"y\":[0,0,0]},\"t\":49,\"s\":[100,100,100]},{\"i\":{\"x\":[0.4,0.4,0.4],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":56,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":65,\"s\":[100,100,100]},{\"i\":{\"x\":[0.17,0.17,0.17],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":77,\"s\":[97,103,100]},{\"t\":93,\"s\":[100,100,100]}],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0]],\"o\":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0]],\"v\":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[-1.232,-13.968]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":10,\"s\":[{\"i\":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0],[7.2,-1.2]],\"o\":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0],[-7.2,1.2]],\"v\":[[-14.032,-4.05],[-13.127,7.863],[-9.825,12.562],[0.35,14.211],[10.398,12.616],[13.744,7.749],[14.232,-7.571],[-0.968,-7.539]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":21,\"s\":[{\"i\":[[-0.869,-2.458],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.3,6.472],[11.956,-3.973]],\"o\":[[1.863,5.271],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.119,-0.593],[-5.848,1.943]],\"v\":[[-14.632,-13.411],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[11.232,-16.011],[-3.825,-14.466]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0},\"t\":35,\"s\":[{\"i\":[[-0.896,-2.536],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[2.094,6.249],[8.562,0.638]],\"o\":[[1.922,5.439],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.172],[-6.543,-0.487]],\"v\":[[-15.346,-10.627],[-12.527,7.863],[-9.225,12.562],[0.35,14.211],[9.798,12.616],[13.144,7.749],[11.974,-10.116],[-6.43,-8.177]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":42,\"s\":[{\"i\":[[-0.878,-2.484],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.559,6.399],[11.133,-0.108]],\"o\":[[1.882,5.326],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.105,-0.49],[-6.188,0.06]],\"v\":[[-14.481,-12.569],[-12.098,7.863],[-8.796,12.562],[0.35,14.211],[9.369,12.616],[12.715,7.749],[11.484,-13.946],[-3.724,-11.3]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.5,\"y\":0},\"t\":49,\"s\":[{\"i\":[[-0.869,-2.458],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.3,6.472],[11.176,-3.111]],\"o\":[[1.863,5.271],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.119,-0.593],[-6.043,1.682]],\"v\":[[-14.732,-16.111],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[11.532,-15.511],[-3.745,-13.528]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":54,\"s\":[{\"i\":[[-0.873,-2.469],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.413,6.44],[7.166,0.651]],\"o\":[[1.872,5.295],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.102,-0.533],[-6.261,-0.569]],\"v\":[[-14.525,-15.487],[-12.348,7.863],[-9.046,12.562],[0.35,14.211],[9.619,12.616],[12.964,7.749],[11.758,-14.741],[-3.318,-12.791]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":57,\"s\":[{\"i\":[[-0.878,-2.484],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.559,6.399],[7.228,0.668]],\"o\":[[1.882,5.326],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0.146],[-6.315,-0.584]],\"v\":[[-14.816,-12.663],[-12.153,7.863],[-8.85,12.562],[0.35,14.211],[9.424,12.616],[12.769,7.749],[11.588,-12.58],[-3.901,-10.75]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0},\"t\":65,\"s\":[{\"i\":[[-0.896,-2.536],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[2.094,6.249],[11.969,-4.333]],\"o\":[[1.922,5.439],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.172],[-6.23,2.255]],\"v\":[[-15.524,-9.227],[-12.727,7.863],[-9.425,12.562],[0.35,14.211],[9.998,12.616],[13.344,7.749],[12.174,-10.116],[-3.838,-7.806]],\"c\":false}]},{\"i\":{\"x\":0.17,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":73,\"s\":[{\"i\":[[-0.39,-1.104],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0.912,2.72],[13.551,-2.732]],\"o\":[[0.837,2.368],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.075],[-6.34,1.278]],\"v\":[[-14.493,-10.166],[-12.749,7.863],[-9.446,12.562],[0.35,14.211],[10.02,12.616],[13.365,7.749],[12.988,-9.89],[-1.256,-9.949]],\"c\":false}]},{\"t\":93,\"s\":[{\"i\":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0]],\"o\":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0]],\"v\":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[-1.232,-13.968]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":0,\"bm\":0}]},{\"id\":\"comp_1\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":3,\"nm\":\"NULL CONTROL\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":0,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[256.989,275.127,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[115,115,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[500,500,100],\"ix\":6}},\"ao\":0,\"ip\":0,\"op\":183,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"head\",\"parent\":6,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":0,\"s\":[0,-37.327,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":11,\"s\":[0,-27.027,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":23,\"s\":[0.383,-39.627,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":35,\"s\":[0.383,-33.606,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":49,\"s\":[0.383,-39.627,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":63,\"s\":[0.383,-33.606,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":79,\"s\":[3.183,-39.627,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.15,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":93,\"s\":[3.583,-33.606,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":124,\"s\":[0,-37.327,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":0,\"s\":[100,100,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":5,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":11,\"s\":[105,95,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":16,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":23,\"s\":[100,100,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":29,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":35,\"s\":[105,95,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":41,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":49,\"s\":[100,100,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":55,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":63,\"s\":[105,95,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":70,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":79,\"s\":[100,100,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":85,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":93,\"s\":[105,95,100]},{\"i\":{\"x\":[0.15,0.15,0.15],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":105,\"s\":[97,103,100]},{\"t\":124,\"s\":[100,100,100]}],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],\"o\":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],\"v\":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Oval\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":184,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":3,\"nm\":\"hands\",\"parent\":6,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":0,\"ix\":11},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.5],\"y\":[1]},\"o\":{\"x\":[0.5],\"y\":[0]},\"t\":0,\"s\":[0]},{\"i\":{\"x\":[0.16],\"y\":[1]},\"o\":{\"x\":[0.5],\"y\":[0]},\"t\":21,\"s\":[0]},{\"t\":118,\"s\":[0]}],\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[-2.341,-17.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":10,\"s\":[-2.341,-9.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":21,\"s\":[-1.541,-17.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":33,\"s\":[-1.541,-15.377,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":47,\"s\":[-1.541,-17.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":61,\"s\":[-1.541,-15.377,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":78,\"s\":[-1.541,-17.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.16,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":102,\"s\":[-1.541,-15.377,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":118,\"s\":[-2.341,-17.777,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[115,115,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[20,20,100],\"ix\":6}},\"ao\":0,\"ip\":0,\"op\":184,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"hands 3\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[115,115,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[500,500,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],\"o\":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],\"v\":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":10,\"s\":[{\"i\":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.667,-0.441]],\"o\":[[-3.232,5.731],[0.566,4.637],[3.994,-0.505],[1.183,0.078]],\"v\":[[-18.219,-21.464],[-27.079,-2.224],[-7.644,3.584],[11.49,2.929]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.7,\"y\":0},\"t\":23,\"s\":[{\"i\":[[0,0],[-1.268,-5.758],[-4.491,-2.782],[-9.089,1.417]],\"o\":[[-0.238,6.778],[1.347,6.12],[4.565,2.828],[1.172,-0.183]],\"v\":[[-19.257,-37.475],[-19.531,-14.871],[-9.165,2.078],[11.49,3.527]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.7,\"y\":0},\"t\":35,\"s\":[{\"i\":[[0,0],[-0.961,-3.853],[-6.666,-1.601],[-9.089,1.417]],\"o\":[[0.03,7.313],[1.517,6.08],[5.221,1.254],[1.172,-0.183]],\"v\":[[-23.457,-27.675],[-24.527,-6.059],[-7.961,2.838],[11.49,2.929]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.7,\"y\":0},\"t\":49,\"s\":[{\"i\":[[0,0],[-1.268,-5.758],[-4.491,-2.782],[-9.089,1.417]],\"o\":[[-0.238,6.778],[1.347,6.12],[4.565,2.828],[1.172,-0.183]],\"v\":[[-19.257,-37.475],[-19.531,-14.871],[-9.165,2.078],[11.49,3.527]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.7,\"y\":0},\"t\":59,\"s\":[{\"i\":[[0,0],[-0.961,-3.853],[-6.666,-1.601],[-9.116,1.293]],\"o\":[[0.03,7.313],[1.517,6.08],[5.221,1.254],[1.174,-0.167]],\"v\":[[-23.457,-27.675],[-24.527,-6.059],[-7.961,2.838],[11.49,2.929]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":65,\"s\":[{\"i\":[[0,0],[-0.651,-3.886],[-6.545,-1.827],[-9.086,1.105]],\"o\":[[4.595,6.728],[1.028,6.132],[5.027,1.403],[1.177,-0.143]],\"v\":[[-25.036,-29.511],[-22.232,-9.273],[-7.789,2.781],[11.545,2.978]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0},\"t\":73,\"s\":[{\"i\":[[0,0],[0.125,-3.969],[-6.039,-3.086],[-8.715,1.918]],\"o\":[[-2.798,7.352],[-0.197,6.263],[4.981,2.545],[1.158,-0.255]],\"v\":[[-9.536,-38.346],[-16.487,-17.314],[-7.359,2.636],[11.683,3.1]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":79,\"s\":[{\"i\":[[0,0],[-0.447,-3.908],[-4.757,-3.608],[-9.817,2.3]],\"o\":[[-4.119,5.691],[0.705,6.167],[4.297,3.185],[1.155,-0.271]],\"v\":[[-10.167,-36.731],[-16.027,-16.023],[-7.359,2.636],[12.29,3.029]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0},\"t\":85,\"s\":[{\"i\":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-7.117,1.703]],\"o\":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.153,-0.276]],\"v\":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[11.59,3.229]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":90,\"s\":[{\"i\":[[0,0],[-0.511,-3.901],[-4.724,-3.504],[-8.017,1.106]],\"o\":[[2.238,7.884],[0.807,6.156],[4.331,3.14],[1.175,-0.162]],\"v\":[[-18.139,-37.479],[-16.265,-15.877],[-7.667,2.047],[11.49,2.929]],\"c\":false}]},{\"i\":{\"x\":0.1,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":96,\"s\":[{\"i\":[[0,0],[0.125,-3.969],[-6.039,-3.086],[-9.123,0.867]],\"o\":[[-2.798,7.352],[-0.197,6.263],[4.981,2.545],[1.181,-0.112]],\"v\":[[-9.536,-38.346],[-16.487,-17.314],[-7.559,2.136],[11.683,3.1]],\"c\":false}]},{\"t\":137,\"s\":[{\"i\":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],\"o\":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],\"v\":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":4.8,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path-8\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":184,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"hands 2\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[115,115,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[500,500,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],\"o\":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],\"v\":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":10,\"s\":[{\"i\":[[-2.832,-0.073],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],\"o\":[[2.794,0.072],[11.894,0.339],[2.109,3.377],[0,0]],\"v\":[[2.763,2.93],[11.216,2.961],[28.065,7.836],[30.818,22.809]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.7,\"y\":0},\"t\":23,\"s\":[{\"i\":[[-2.832,0.111],[-2.264,0.431],[-1.371,7.044],[-0.143,3.51]],\"o\":[[2.794,-0.11],[8.983,-1.711],[1.168,-6],[0,0]],\"v\":[[2.891,4.486],[10.849,3.71],[25.852,-14.597],[24.804,-37.956]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.7,\"y\":0},\"t\":35,\"s\":[{\"i\":[[-2.832,0.111],[-2.272,0.384],[-1.192,6.245],[-0.009,3.513]],\"o\":[[2.794,-0.11],[9.419,-1.594],[1.146,-6.005],[0,0]],\"v\":[[3.091,3.837],[10.849,3.012],[29.365,-6.408],[27.224,-29.702]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.7,\"y\":0},\"t\":49,\"s\":[{\"i\":[[-2.832,0.111],[-2.264,0.431],[-1.371,7.044],[-0.143,3.51]],\"o\":[[2.794,-0.11],[8.983,-1.711],[1.168,-6],[0,0]],\"v\":[[2.891,4.486],[10.849,3.71],[25.852,-14.597],[24.804,-37.956]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.7,\"y\":0},\"t\":63,\"s\":[{\"i\":[[-2.832,0.111],[-2.272,0.384],[-1.192,6.245],[-0.009,3.513]],\"o\":[[2.794,-0.11],[9.419,-1.594],[1.146,-6.005],[0,0]],\"v\":[[3.115,3.787],[10.849,3.012],[29.365,-6.408],[27.224,-29.702]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":68,\"s\":[{\"i\":[[-2.832,0.111],[-2.329,0.428],[-1.207,6.31],[-0.019,3.512]],\"o\":[[2.794,-0.11],[9.405,-1.697],[1.148,-6.004],[0,0]],\"v\":[[3.081,4.077],[10.905,3.036],[29.081,-7.069],[27.028,-30.369]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.7,\"y\":0},\"t\":81,\"s\":[{\"i\":[[-2.832,0.111],[-2.976,0.923],[-1.371,7.044],[-0.143,3.51]],\"o\":[[2.794,-0.11],[9.238,-2.866],[1.168,-6],[0,0]],\"v\":[[2.091,4.836],[11.549,3.31],[25.852,-14.597],[24.804,-37.956]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.7,\"y\":0},\"t\":95,\"s\":[{\"i\":[[-2.832,0.111],[-2.272,0.384],[-1.192,6.245],[-0.009,3.513]],\"o\":[[2.794,-0.11],[9.419,-1.594],[1.146,-6.005],[0,0]],\"v\":[[3.091,3.837],[10.849,3.012],[29.365,-6.408],[27.224,-29.702]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":104,\"s\":[{\"i\":[[-2.831,0.08],[-2.289,0.181],[-7.091,2.705],[-0.08,0.435]],\"o\":[[2.753,-0.078],[9.55,-0.749],[7.091,-2.705],[0,0]],\"v\":[[2.94,3.427],[10.784,3.015],[28.399,6.452],[28.599,-12.123]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":108,\"s\":[{\"i\":[[-2.826,0.181],[-2.296,0.098],[-2.884,-0.516],[0.064,0.955]],\"o\":[[2.87,-0.184],[9.603,-0.406],[2.056,-0.027],[0,0]],\"v\":[[2.852,3.381],[10.755,2.875],[28.007,11.666],[29.663,9.28]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":109,\"s\":[{\"i\":[[-2.829,0.086],[-2.298,0.077],[-0.947,-0.492],[-0.646,-0.097]],\"o\":[[2.923,-0.087],[9.617,-0.321],[0.947,0.492],[0,0]],\"v\":[[2.841,3.319],[10.792,2.896],[27.909,12.969],[29.9,13.784]],\"c\":false}]},{\"i\":{\"x\":0.1,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":110,\"s\":[{\"i\":[[-2.832,-0.009],[-2.3,0.057],[-0.78,-1.215],[-0.945,-1.801]],\"o\":[[2.976,0.009],[9.63,-0.235],[0.78,1.215],[0,0]],\"v\":[[2.83,3.257],[10.73,2.954],[27.812,14.272],[30.136,18.289]],\"c\":false}]},{\"t\":134,\"s\":[{\"i\":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],\"o\":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],\"v\":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":4.8,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path-8\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":184,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"body\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[116.171,146.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.31,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":10,\"s\":[116.171,151.524,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.31,\"y\":1},\"o\":{\"x\":0.69,\"y\":0},\"t\":21,\"s\":[116.171,140.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.31,\"y\":1},\"o\":{\"x\":0.69,\"y\":0},\"t\":33,\"s\":[116.171,150.324,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.31,\"y\":1},\"o\":{\"x\":0.69,\"y\":0},\"t\":47,\"s\":[116.171,146.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.31,\"y\":1},\"o\":{\"x\":0.69,\"y\":0},\"t\":61,\"s\":[116.171,150.324,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.31,\"y\":1},\"o\":{\"x\":0.69,\"y\":0},\"t\":78,\"s\":[116.171,146.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.1,\"y\":1},\"o\":{\"x\":0.69,\"y\":0},\"t\":102,\"s\":[116.171,150.324,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":133,\"s\":[116.171,146.724,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,14.211,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.5,0.5,0.5],\"y\":[0,0,0]},\"t\":0,\"s\":[100,100,100]},{\"i\":{\"x\":[0.5,0.5,0.5],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":5,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.484,0.484,0.484],\"y\":[0,0,0]},\"t\":10,\"s\":[100,100,100]},{\"i\":{\"x\":[0.31,0.31,0.31],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":15,\"s\":[103,97,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.563,0.563,0.563],\"y\":[0,0,0]},\"t\":21,\"s\":[100,100,100]},{\"i\":{\"x\":[0.219,0.219,0.219],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":27,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.406,0.406,0.406],\"y\":[0,0,0]},\"t\":33,\"s\":[100,100,100]},{\"i\":{\"x\":[0.233,0.233,0.233],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":40,\"s\":[103,97,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.69,0.69,0.69],\"y\":[0,0,0]},\"t\":47,\"s\":[100,100,100]},{\"i\":{\"x\":[0.31,0.31,0.31],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":54,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.495,0.495,0.495],\"y\":[0,0,0]},\"t\":61,\"s\":[100,100,100]},{\"i\":{\"x\":[0.203,0.203,0.203],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":67,\"s\":[103,97,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.69,0.69,0.69],\"y\":[0,0,0]},\"t\":78,\"s\":[100,100,100]},{\"i\":{\"x\":[0.31,0.31,0.31],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":89,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.495,0.495,0.495],\"y\":[0,0,0]},\"t\":102,\"s\":[100,100,100]},{\"i\":{\"x\":[0.1,0.1,0.1],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":117,\"s\":[103,97,100]},{\"t\":133,\"s\":[100,100,100]}],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0]],\"o\":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0]],\"v\":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[0.089,-13.897]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":10,\"s\":[{\"i\":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0],[0,0]],\"o\":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0],[0,0]],\"v\":[[-13.432,-6.25],[-13.127,7.863],[-9.825,12.562],[0.35,14.211],[10.398,12.616],[13.744,7.749],[13.432,-6.171],[0.069,-6.21]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":21,\"s\":[{\"i\":[[0,0],[0,0],[-1.772,-0.83],[-2.763,0],[-2.314,1.063],[-0.081,2.131],[0,0],[4.6,0.1]],\"o\":[[0,0],[0.132,2.059],[2.348,1.099],[2.717,0],[1.837,-0.844],[0,0],[0,0],[-4.6,-0.1]],\"v\":[[-12.732,-16.711],[-11.415,7.863],[-8.318,12.562],[0.348,14.211],[8.894,12.616],[12.032,7.749],[12.732,-14.111],[0.032,-12.939]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":33,\"s\":[{\"i\":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0],[0,0]],\"o\":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0],[0,0]],\"v\":[[-13.832,-12.011],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[13.832,-12.011],[0.071,-12.011]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":47,\"s\":[{\"i\":[[0,0],[0,0],[-1.772,-0.83],[-2.763,0],[-2.314,1.063],[-0.081,2.131],[0,0],[0,0]],\"o\":[[0,0],[0.132,2.059],[2.348,1.099],[2.717,0],[1.837,-0.844],[0,0],[0,0],[0,0]],\"v\":[[-12.732,-16.711],[-11.415,7.863],[-8.318,12.562],[0.348,14.211],[8.894,12.616],[12.032,7.749],[12.732,-14.111],[0.065,-13.504]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0},\"t\":61,\"s\":[{\"i\":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0],[0,0]],\"o\":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0],[0,0]],\"v\":[[-13.832,-12.011],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[13.832,-12.011],[0.071,-12.011]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":68,\"s\":[{\"i\":[[0.583,-2.853],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-1.681,3.721],[9.547,0.72]],\"o\":[[-1.162,5.686],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.172,-0.387],[-6.293,-0.475]],\"v\":[[-11.685,-13.446],[-12.103,7.863],[-8.8,12.562],[0.35,14.211],[9.374,12.616],[12.719,7.749],[13.832,-14.267],[-0.931,-12.07]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0},\"t\":78,\"s\":[{\"i\":[[0.583,-2.853],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-1.202,3.902],[10.545,0.088]],\"o\":[[-1.162,5.686],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.108,-0.363],[-6.244,-0.052]],\"v\":[[-11.402,-16.846],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[13.815,-16.611],[-0.813,-13.327]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":90,\"s\":[{\"i\":[[0.279,-1.426],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-0.575,1.951],[6.396,0.322]],\"o\":[[-0.556,2.843],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.052,-0.181],[-8.294,-0.418]],\"v\":[[-11.944,-17.745],[-12.154,7.863],[-8.852,12.562],[0.35,14.211],[9.425,12.616],[12.771,7.749],[13.386,-13.461],[-1.432,-12.505]],\"c\":false}]},{\"i\":{\"x\":0.1,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":102,\"s\":[{\"i\":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0],[0,0]],\"o\":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0],[0,0]],\"v\":[[-12.832,-15.411],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[12.932,-11.911],[-1.184,-11.652]],\"c\":false}]},{\"t\":133,\"s\":[{\"i\":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0]],\"o\":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0]],\"v\":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[0.089,-13.897]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":184,\"st\":0,\"bm\":0}]},{\"id\":\"comp_2\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":3,\"nm\":\"NULL CONTROL\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":0,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[256.989,275.127,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[115,115,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[500,500,100],\"ix\":6}},\"ao\":0,\"ip\":0,\"op\":119,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"head\",\"parent\":6,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.833],\"y\":[1]},\"o\":{\"x\":[0.6],\"y\":[0]},\"t\":1,\"s\":[0]},{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0]},\"t\":11,\"s\":[0]},{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0]},\"t\":23,\"s\":[21]},{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0]},\"t\":36,\"s\":[11]},{\"i\":{\"x\":[0.4],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0]},\"t\":50,\"s\":[21]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.243],\"y\":[0]},\"t\":68,\"s\":[21]},{\"i\":{\"x\":[0.566],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"t\":78,\"s\":[-15.198]},{\"t\":88,\"s\":[0]}],\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":1,\"s\":[0,-37.327,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":11,\"s\":[0,-26.927,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":23,\"s\":[5.6,-39.527,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":36,\"s\":[4,-29.127,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":50,\"s\":[5.6,-39.527,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.2,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":66,\"s\":[4,-30.327,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":88,\"s\":[0,-37.327,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":1,\"s\":[100,100,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":5,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":11,\"s\":[105,95,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":16,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":23,\"s\":[105,95,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":29,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":36,\"s\":[105,95,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":43,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":50,\"s\":[105,95,100]},{\"i\":{\"x\":[0.4,0.4,0.4],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":57,\"s\":[95,105,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":66,\"s\":[102,98,100]},{\"i\":{\"x\":[0.2,0.2,0.2],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":78,\"s\":[95,105,100]},{\"t\":88,\"s\":[100,100,100]}],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],\"o\":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],\"v\":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Oval\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":3,\"nm\":\"hands\",\"parent\":6,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":0,\"ix\":11},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.5],\"y\":[1]},\"o\":{\"x\":[0.5],\"y\":[0]},\"t\":0,\"s\":[0]},{\"i\":{\"x\":[0.5],\"y\":[1]},\"o\":{\"x\":[0.5],\"y\":[0]},\"t\":21,\"s\":[8]},{\"i\":{\"x\":[0.4],\"y\":[1]},\"o\":{\"x\":[0.5],\"y\":[0]},\"t\":49,\"s\":[8]},{\"i\":{\"x\":[0.2],\"y\":[1]},\"o\":{\"x\":[0.6],\"y\":[0]},\"t\":65,\"s\":[8]},{\"t\":89,\"s\":[0]}],\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[-2.341,-17.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":10,\"s\":[-2.341,-9.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":21,\"s\":[-1.541,-17.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":35,\"s\":[-1.541,-11.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":49,\"s\":[-1.541,-17.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.2,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":65,\"s\":[-1.541,-11.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":89,\"s\":[-2.341,-17.777,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[115,115,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[20,20,100],\"ix\":6}},\"ao\":0,\"ip\":0,\"op\":120,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"hands 3\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[115,115,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[500,500,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],\"o\":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],\"v\":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":10,\"s\":[{\"i\":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.659,-0.445]],\"o\":[[-3.232,5.731],[0.566,4.637],[3.994,-0.505],[1.183,0.079]],\"v\":[[-18.219,-21.464],[-27.079,-2.224],[-7.644,3.484],[10.985,2.86]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":23,\"s\":[{\"i\":[[0,0],[-0.961,-3.853],[-4.984,-1.753],[-8.926,0.862]],\"o\":[[1.597,6.857],[1.517,6.08],[5.484,1.929],[1.18,-0.114]],\"v\":[[-20.247,-37.336],[-17.926,-14.859],[-7.906,2.713],[8.267,2.627]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.7,\"y\":0},\"t\":37,\"s\":[{\"i\":[[0,0],[-1.803,-4.807],[-6.68,-0.3],[-7.645,-0.484]],\"o\":[[-1.052,7.028],[2.083,5.552],[5.685,0.256],[1.184,0.075]],\"v\":[[-20.218,-23.988],[-22.536,-2.921],[-7.359,2.636],[10.433,2.572]],\"c\":false}]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.7,\"y\":0},\"t\":51,\"s\":[{\"i\":[[0,0],[-0.961,-3.853],[-4.984,-1.753],[-8.926,0.862]],\"o\":[[1.597,6.857],[1.517,6.08],[5.484,1.929],[1.18,-0.114]],\"v\":[[-20.247,-37.336],[-17.926,-14.859],[-7.906,2.713],[8.267,2.627]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.4,\"y\":0},\"t\":67,\"s\":[{\"i\":[[0,0],[-1.803,-4.807],[-6.68,-0.3],[-7.645,-0.484]],\"o\":[[-1.052,7.028],[2.083,5.552],[5.685,0.256],[1.184,0.075]],\"v\":[[-20.218,-23.988],[-22.536,-2.921],[-7.359,2.636],[10.433,2.572]],\"c\":false}]},{\"i\":{\"x\":0.1,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":78,\"s\":[{\"i\":[[0,0],[-1.746,-7.006],[-9.167,-1.369],[-3.018,-0.266]],\"o\":[[-3.628,5.112],[1.694,6.8],[4.106,0.209],[1.182,0.076]],\"v\":[[-20.614,-25.461],[-25.11,-7.259],[-6.208,2.731],[7.297,3.206]],\"c\":false}]},{\"t\":105,\"s\":[{\"i\":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],\"o\":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],\"v\":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":4.8,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path-8\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"hands 2\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[115,115,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[500,500,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],\"o\":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],\"v\":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":10,\"s\":[{\"i\":[[-2.832,-0.073],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],\"o\":[[2.794,0.072],[11.894,0.339],[2.109,3.377],[0,0]],\"v\":[[2.747,2.681],[11.207,2.811],[28.065,7.836],[30.818,22.809]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":22,\"s\":[{\"i\":[[-2.805,0.395],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],\"o\":[[4.23,-0.595],[5.323,2.097],[1.94,4.605],[0,0]],\"v\":[[2.696,3.339],[12.911,2.962],[22.174,19.291],[30.152,32.241]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.7,\"y\":0},\"t\":36,\"s\":[{\"i\":[[-2.83,0.127],[-2.144,-0.845],[-1.985,-3.535],[-1.3,-6.195]],\"o\":[[5.352,-0.241],[5.323,2.097],[1.964,3.497],[0,0]],\"v\":[[1.908,2.538],[13.562,3.072],[22.983,13.903],[27.16,28.662]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.6,\"y\":0},\"t\":50,\"s\":[{\"i\":[[-2.805,0.395],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],\"o\":[[4.23,-0.595],[5.323,2.097],[1.94,4.605],[0,0]],\"v\":[[2.696,3.339],[12.911,2.962],[22.174,19.291],[30.152,32.241]],\"c\":false}]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":58,\"s\":[{\"i\":[[-2.818,0.261],[-2.144,-0.845],[-1.78,-3.636],[-2.457,-5.823]],\"o\":[[4.791,-0.418],[5.323,2.097],[1.952,4.051],[0,0]],\"v\":[[2.321,3.134],[13.236,3.017],[22.578,16.597],[28.656,30.451]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.4,\"y\":0},\"t\":66,\"s\":[{\"i\":[[-2.83,0.127],[-2.144,-0.845],[-1.985,-3.535],[-1.3,-6.195]],\"o\":[[5.352,-0.241],[5.323,2.097],[1.964,3.497],[0,0]],\"v\":[[1.908,2.538],[13.562,3.072],[22.983,13.903],[27.16,28.662]],\"c\":false}]},{\"i\":{\"x\":0.1,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":76,\"s\":[{\"i\":[[-2.803,-0.138],[-2.518,-0.399],[-2.207,-4.957],[2.376,-4.348]],\"o\":[[3.802,0.187],[4.102,0.65],[1.627,3.656],[0,0]],\"v\":[[1.941,2.94],[11.88,3.616],[26.524,13.453],[23.943,27.653]],\"c\":false}]},{\"t\":97,\"s\":[{\"i\":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],\"o\":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],\"v\":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":4.8,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path-8\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"body\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[116.171,146.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":10,\"s\":[116.171,151.524,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":21,\"s\":[116.171,140.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":35,\"s\":[116.171,146.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":49,\"s\":[116.171,143.924,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.2,\"y\":0.2},\"o\":{\"x\":0.6,\"y\":0.6},\"t\":65,\"s\":[116.171,146.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":89,\"s\":[116.171,146.724,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,14.211,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.5,0.5,0.5],\"y\":[0,0,0]},\"t\":0,\"s\":[100,100,100]},{\"i\":{\"x\":[0.5,0.5,0.5],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":5,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.484,0.484,0.484],\"y\":[0,0,0]},\"t\":10,\"s\":[100,100,100]},{\"i\":{\"x\":[0.486,0.486,0.486],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":15,\"s\":[103,97,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.5,0.5,0.5],\"y\":[0,0,0]},\"t\":21,\"s\":[100,100,100]},{\"i\":{\"x\":[0.5,0.5,0.5],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":27,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.5,0.5,0.5],\"y\":[0,0,0]},\"t\":35,\"s\":[100,100,100]},{\"i\":{\"x\":[0.5,0.5,0.5],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":42,\"s\":[103,97,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.5,0.5,0.5],\"y\":[0,0,0]},\"t\":49,\"s\":[100,100,100]},{\"i\":{\"x\":[0.4,0.4,0.4],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":56,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":65,\"s\":[100,100,100]},{\"i\":{\"x\":[0.2,0.2,0.2],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":76,\"s\":[97,103,100]},{\"t\":89,\"s\":[100,100,100]}],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0]],\"o\":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0]],\"v\":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[-2.127,-14.017]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":10,\"s\":[{\"i\":[[0,0],[0,0],[-2.036,-0.83],[-3.175,0],[-2.659,1.063],[-0.093,2.131],[0,0],[0,0]],\"o\":[[0,0],[0.446,2.497],[2.698,1.099],[3.123,0],[2.111,-0.844],[0,0],[0,0],[0,0]],\"v\":[[-14.932,-5.55],[-13.915,7.864],[-10.056,12.562],[0.354,14.211],[10.626,12.616],[14.232,7.749],[14.732,-4.571],[-1.668,-6.839]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":21,\"s\":[{\"i\":[[0.416,-4.877],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43],[7.773,-0.606]],\"o\":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.023,-0.095],[-6.044,0.472]],\"v\":[[-11.695,-16.726],[-11.827,7.863],[-8.772,12.562],[0.392,14.211],[9.438,12.616],[12.534,7.749],[14.054,-9.469],[3.058,-14.133]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":35,\"s\":[{\"i\":[[0.168,-4.901],[0,0],[-1.759,-0.83],[-2.743,0],[-2.297,1.063],[-0.08,2.131],[-1.325,4.892],[7.682,-1.08]],\"o\":[[-0.283,8.264],[0.131,2.059],[2.331,1.099],[2.698,0],[1.823,-0.844],[0,0],[0.15,-0.385],[-6.216,0.874]],\"v\":[[-12.874,-10.003],[-11.908,7.863],[-8.834,12.562],[0.377,14.211],[9.469,12.616],[12.584,7.749],[14.152,-3.848],[-1.25,-8.759]],\"c\":false}]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":49,\"s\":[{\"i\":[[0.416,-4.877],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43],[9.673,-2.072]],\"o\":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.023,-0.095],[-5.963,1.277]],\"v\":[[-11.695,-17.126],[-11.827,7.863],[-8.772,12.562],[0.392,14.211],[9.438,12.616],[12.534,7.749],[14.054,-9.469],[-0.642,-13.867]],\"c\":false}]},{\"i\":{\"x\":0.2,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":65,\"s\":[{\"i\":[[0.168,-4.901],[0,0],[-1.759,-0.83],[-2.743,0],[-2.297,1.063],[-0.08,2.131],[-1.325,4.892],[9.682,-1.898]],\"o\":[[-0.283,8.264],[0.131,2.059],[2.331,1.099],[2.698,0],[1.823,-0.844],[0,0],[0.15,-0.385],[-6.146,1.205]],\"v\":[[-12.874,-10.003],[-11.908,7.863],[-8.834,12.562],[0.377,14.211],[9.469,12.616],[12.584,7.749],[14.152,-4.048],[-1.55,-8.541]],\"c\":false}]},{\"t\":89,\"s\":[{\"i\":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0]],\"o\":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0]],\"v\":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[-2.127,-14.017]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":0,\"bm\":0}]},{\"id\":\"comp_3\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":3,\"nm\":\"NULL CONTROL\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":0,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[256.989,275.127,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[115,115,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[500,500,100],\"ix\":6}},\"ao\":0,\"ip\":0,\"op\":119,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"head\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.833],\"y\":[1]},\"o\":{\"x\":[0.6],\"y\":[0]},\"t\":1,\"s\":[0]},{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0]},\"t\":11,\"s\":[0]},{\"i\":{\"x\":[0.1],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0]},\"t\":23,\"s\":[21]},{\"t\":107,\"s\":[0]}],\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":1,\"s\":[122.706,17.15,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":11,\"s\":[122.706,68.999,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.589,\"y\":1},\"o\":{\"x\":0.275,\"y\":0},\"t\":23,\"s\":[150.706,5.999,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.206,\"y\":0},\"t\":32,\"s\":[150.706,32.05,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.589,\"y\":1},\"o\":{\"x\":0.275,\"y\":0},\"t\":40,\"s\":[150.706,27.67,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.206,\"y\":0},\"t\":47,\"s\":[150.706,32.05,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.589,\"y\":1},\"o\":{\"x\":0.275,\"y\":0},\"t\":54,\"s\":[150.706,27.67,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.2,\"y\":1},\"o\":{\"x\":0.206,\"y\":0},\"t\":61,\"s\":[150.706,32.05,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.1,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":70,\"s\":[150.706,27.67,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":107,\"s\":[122.706,17.15,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":1,\"s\":[500,500,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":5,\"s\":[485,515,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":11,\"s\":[525,475,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":16,\"s\":[485,515,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":23,\"s\":[525,475,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":32,\"s\":[500,500,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":40,\"s\":[500,500,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":47,\"s\":[500,500,100]},{\"i\":{\"x\":[0.3,0.3,0.3],\"y\":[1,1,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":54,\"s\":[500,500,100]},{\"i\":{\"x\":[0.2,0.2,0.2],\"y\":[1,1,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":61,\"s\":[500,500,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":70,\"s\":[500,500,100]},{\"i\":{\"x\":[0.1,0.1,0.1],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":77,\"s\":[480,520,100]},{\"t\":107,\"s\":[500,500,100]}],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],\"o\":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],\"v\":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Oval\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":3,\"nm\":\"NULL CONTROL\",\"parent\":7,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":0,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.2,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":21,\"s\":[-1.541,-17.777,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.13,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":66,\"s\":[-1.541,-20.177,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":94,\"s\":[-1.541,-17.777,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[115,115,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[20,20,100],\"ix\":6}},\"ao\":0,\"ip\":0,\"op\":120,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":3,\"nm\":\"hands\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":0,\"ix\":11},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.5],\"y\":[1]},\"o\":{\"x\":[0.5],\"y\":[0]},\"t\":0,\"s\":[0]},{\"i\":{\"x\":[0.5],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0]},\"t\":23,\"s\":[8]},{\"i\":{\"x\":[0.13],\"y\":[1]},\"o\":{\"x\":[0.6],\"y\":[0]},\"t\":65,\"s\":[8]},{\"t\":94,\"s\":[0]}],\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[111,115,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":10,\"s\":[111,155,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":23,\"s\":[115,115,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":30,\"s\":[115,121,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":38,\"s\":[115,115,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":45,\"s\":[115,121,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":53,\"s\":[115,115,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.2,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":57,\"s\":[115,121,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.13,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":65,\"s\":[115,115,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":94,\"s\":[111,115,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[115,115,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"ip\":0,\"op\":120,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"hands 3\",\"parent\":4,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[115,115,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[500,500,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],\"o\":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],\"v\":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":10,\"s\":[{\"i\":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.666,-0.138]],\"o\":[[-3.232,5.731],[0.566,4.637],[3.994,-0.505],[1.186,0.024]],\"v\":[[-18.219,-21.464],[-27.079,-2.224],[-7.644,3.484],[11.485,2.829]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0},\"t\":23,\"s\":[{\"i\":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-8.28,0.661]],\"o\":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.182,-0.094]],\"v\":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[10.724,2.534]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":26,\"s\":[{\"i\":[[0,0],[-0.511,-3.901],[-4.724,-3.504],[-8.291,0.526]],\"o\":[[2.238,7.884],[0.807,6.156],[4.331,3.14],[1.184,-0.075]],\"v\":[[-18.139,-37.479],[-16.265,-15.877],[-7.359,2.636],[10.941,2.657]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0},\"t\":30,\"s\":[{\"i\":[[0,0],[0.125,-3.969],[-5.052,-4.524],[-8.435,1.023]],\"o\":[[-2.798,7.352],[-0.197,6.263],[4,3.582],[1.177,-0.143]],\"v\":[[-6.969,-38.346],[-13.919,-17.314],[-7.359,2.636],[9.48,3.009]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":34,\"s\":[{\"i\":[[0,0],[-0.447,-3.908],[-4.757,-3.608],[-8.213,0.625]],\"o\":[[-4.119,5.691],[0.705,6.167],[4.297,3.185],[1.182,-0.09]],\"v\":[[-10.167,-36.731],[-16.027,-16.023],[-7.359,2.636],[11.49,2.929]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0},\"t\":37.666,\"s\":[{\"i\":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-8.28,0.661]],\"o\":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.182,-0.094]],\"v\":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[10.724,2.534]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":41,\"s\":[{\"i\":[[0,0],[-0.511,-3.901],[-4.724,-3.504],[-8.291,0.526]],\"o\":[[2.238,7.884],[0.807,6.156],[4.331,3.14],[1.184,-0.075]],\"v\":[[-18.139,-37.479],[-16.265,-15.877],[-7.359,2.636],[10.941,2.657]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0},\"t\":45,\"s\":[{\"i\":[[0,0],[0.125,-3.969],[-5.052,-4.524],[-8.435,1.023]],\"o\":[[-2.798,7.352],[-0.197,6.263],[4,3.582],[1.177,-0.143]],\"v\":[[-6.969,-38.346],[-13.919,-17.314],[-7.359,2.636],[9.48,3.009]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":48,\"s\":[{\"i\":[[0,0],[-0.447,-3.908],[-4.757,-3.608],[-8.213,0.625]],\"o\":[[-4.119,5.691],[0.705,6.167],[4.297,3.185],[1.182,-0.09]],\"v\":[[-10.167,-36.731],[-16.027,-16.023],[-7.359,2.636],[11.49,2.929]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0},\"t\":52.334,\"s\":[{\"i\":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-8.28,0.661]],\"o\":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.182,-0.094]],\"v\":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[10.724,2.534]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":56,\"s\":[{\"i\":[[0,0],[-0.511,-3.901],[-4.724,-3.504],[-8.291,0.526]],\"o\":[[2.238,7.884],[0.807,6.156],[4.331,3.14],[1.184,-0.075]],\"v\":[[-18.139,-37.479],[-16.265,-15.877],[-7.359,2.636],[10.941,2.657]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0},\"t\":60,\"s\":[{\"i\":[[0,0],[0.125,-3.969],[-5.052,-4.524],[-8.435,1.023]],\"o\":[[-2.798,7.352],[-0.197,6.263],[4,3.582],[1.177,-0.143]],\"v\":[[-6.969,-38.346],[-13.919,-17.314],[-7.359,2.636],[9.48,3.009]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":64,\"s\":[{\"i\":[[0,0],[-0.447,-3.908],[-4.757,-3.608],[-8.213,0.625]],\"o\":[[-4.119,5.691],[0.705,6.167],[4.297,3.185],[1.182,-0.09]],\"v\":[[-10.167,-36.731],[-16.027,-16.023],[-7.359,2.636],[11.49,2.929]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0},\"t\":68,\"s\":[{\"i\":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-8.28,0.661]],\"o\":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.182,-0.094]],\"v\":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[10.724,2.534]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":71.666,\"s\":[{\"i\":[[0,0],[-0.511,-3.901],[-4.724,-3.504],[-8.522,-0.294]],\"o\":[[2.238,7.884],[0.807,6.156],[4.331,3.14],[1.185,0.041]],\"v\":[[-18.139,-37.479],[-16.265,-15.877],[-7.359,2.636],[11.49,2.929]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0},\"t\":76,\"s\":[{\"i\":[[0,0],[0.125,-3.969],[-5.052,-4.524],[-9.764,-0.338]],\"o\":[[-2.798,7.352],[-0.197,6.263],[4,3.582],[1.185,0.041]],\"v\":[[-6.969,-38.346],[-13.919,-17.314],[-7.359,2.636],[11.487,2.829]],\"c\":false}]},{\"i\":{\"x\":0.09,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":84,\"s\":[{\"i\":[[0,0],[-1.151,-9.822],[-6.924,-2.401],[-5.542,0.051]],\"o\":[[-3.654,5.5],[0.733,6.255],[4.389,1.538],[1.186,-0.011]],\"v\":[[-12.896,-35.078],[-19.976,-13.433],[-7.442,2.05],[9.027,3.064]],\"c\":false}]},{\"t\":104,\"s\":[{\"i\":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],\"o\":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],\"v\":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":4.8,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path-8\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"hands 2\",\"parent\":4,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[115,115,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[500,500,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],\"o\":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],\"v\":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":10,\"s\":[{\"i\":[[-2.832,-0.073],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],\"o\":[[2.794,0.072],[11.894,0.339],[2.109,3.377],[0,0]],\"v\":[[2.758,2.83],[11.206,2.761],[28.065,7.836],[30.818,22.809]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":23,\"s\":[{\"i\":[[-2.816,0.314],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],\"o\":[[4.14,-0.462],[5.323,2.097],[1.94,4.605],[0,0]],\"v\":[[2.456,3.576],[12.827,2.873],[22.174,19.291],[30.152,32.241]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":31,\"s\":[{\"i\":[[-2.805,0.396],[-2.168,-0.779],[-1.2,-3.73],[-2.259,-5.223]],\"o\":[[3.592,-0.507],[6.085,2.162],[1.511,4.741],[0,0]],\"v\":[[2.725,4.04],[12.869,3.17],[22.316,18.867],[27.587,32.67]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":38,\"s\":[{\"i\":[[-2.816,0.314],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],\"o\":[[4.14,-0.462],[5.323,2.097],[1.94,4.605],[0,0]],\"v\":[[2.456,3.576],[12.827,2.873],[22.174,19.291],[30.152,32.241]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":46,\"s\":[{\"i\":[[-2.805,0.396],[-2.168,-0.779],[-1.2,-3.73],[-2.259,-5.223]],\"o\":[[3.592,-0.507],[6.085,2.162],[1.511,4.741],[0,0]],\"v\":[[2.725,4.04],[12.869,3.17],[22.316,18.867],[27.587,32.67]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":53,\"s\":[{\"i\":[[-2.816,0.314],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],\"o\":[[4.14,-0.462],[5.323,2.097],[1.94,4.605],[0,0]],\"v\":[[2.456,3.576],[12.827,2.873],[22.174,19.291],[30.152,32.241]],\"c\":false}]},{\"i\":{\"x\":0.2,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":61,\"s\":[{\"i\":[[-2.805,0.396],[-2.168,-0.779],[-1.2,-3.73],[-2.259,-5.223]],\"o\":[[3.592,-0.507],[6.085,2.162],[1.511,4.741],[0,0]],\"v\":[[2.725,4.04],[12.869,3.17],[22.316,18.867],[27.587,32.67]],\"c\":false}]},{\"i\":{\"x\":0.13,\"y\":1},\"o\":{\"x\":0.6,\"y\":0},\"t\":69,\"s\":[{\"i\":[[-2.816,0.314],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],\"o\":[[4.14,-0.462],[5.323,2.097],[1.94,4.605],[0,0]],\"v\":[[2.456,3.576],[12.827,2.873],[22.174,19.291],[30.152,32.241]],\"c\":false}]},{\"t\":100,\"s\":[{\"i\":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],\"o\":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],\"v\":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":4.8,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path-8\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":7,\"ty\":4,\"nm\":\"body\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[116.171,146.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":10,\"s\":[116.171,151.524,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.723,\"y\":1},\"o\":{\"x\":0.395,\"y\":0},\"t\":21,\"s\":[116.171,146.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.361,\"y\":0},\"t\":30,\"s\":[116.171,147.924,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.723,\"y\":1},\"o\":{\"x\":0.395,\"y\":0},\"t\":38,\"s\":[116.171,146.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.361,\"y\":0},\"t\":45,\"s\":[116.171,147.924,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.723,\"y\":1},\"o\":{\"x\":0.395,\"y\":0},\"t\":52,\"s\":[116.171,146.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.361,\"y\":0},\"t\":59,\"s\":[116.171,147.924,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.104,\"y\":0.104},\"o\":{\"x\":0.395,\"y\":0.395},\"t\":66,\"s\":[116.171,146.724,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":94,\"s\":[116.171,146.724,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,14.211,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.5,0.5,0.5],\"y\":[0,0,0]},\"t\":0,\"s\":[100,100,100]},{\"i\":{\"x\":[0.5,0.5,0.5],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":5,\"s\":[97,103,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.484,0.484,0.484],\"y\":[0,0,0]},\"t\":10,\"s\":[100,100,100]},{\"i\":{\"x\":[0.486,0.486,0.486],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":15,\"s\":[103,97,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.5,0.5,0.5],\"y\":[0,0,0]},\"t\":21,\"s\":[100,100,100]},{\"i\":{\"x\":[0.486,0.486,0.486],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":25,\"s\":[98,102,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.5,0.5,0.5],\"y\":[0,0,0]},\"t\":30,\"s\":[100,100,100]},{\"i\":{\"x\":[0.486,0.486,0.486],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":34,\"s\":[103,97,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.48,0.48,0.48],\"y\":[0,0,0]},\"t\":38,\"s\":[100,100,100]},{\"i\":{\"x\":[0.471,0.471,0.471],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":41,\"s\":[98,102,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.537,0.537,0.537],\"y\":[0,0,0]},\"t\":45,\"s\":[100,100,100]},{\"i\":{\"x\":[0.582,0.582,0.582],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":49,\"s\":[103,97,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.48,0.48,0.48],\"y\":[0,0,0]},\"t\":52,\"s\":[100,100,100]},{\"i\":{\"x\":[0.471,0.471,0.471],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":55,\"s\":[98,102,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[0.468,0.468,0.468],\"y\":[0,0,0]},\"t\":59,\"s\":[100,100,100]},{\"i\":{\"x\":[0.2,0.2,0.2],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":62,\"s\":[103,97,100]},{\"i\":{\"x\":[0.13,0.13,0.13],\"y\":[1,1,1]},\"o\":{\"x\":[0.6,0.6,0.6],\"y\":[0,0,0]},\"t\":66,\"s\":[100,100,100]},{\"t\":94,\"s\":[100,100,100]}],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0],[0,0]],\"v\":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[8.179,-13.458],[-3.728,-14.103]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":10,\"s\":[{\"i\":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0],[0,0],[0,0]],\"v\":[[-14.032,-5.05],[-13.127,7.863],[-9.825,12.562],[0.35,14.211],[10.398,12.616],[13.744,7.749],[14.232,-5.571],[9.351,-5.481],[-4.158,-6.032]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.5,\"y\":0},\"t\":23,\"s\":[{\"i\":[[0.662,-3.59],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-2.532,4.395],[2.35,1.639],[4.409,1.158]],\"o\":[[-1.585,8.587],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.006,-0.084],[-3.61,-2.518],[-3.578,-0.94]],\"v\":[[-11.286,-17.808],[-12.133,7.863],[-8.83,12.562],[0.35,14.211],[9.404,12.616],[12.749,7.749],[13.232,-10.927],[9.318,-13.932],[-4.455,-14.008]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":30,\"s\":[{\"i\":[[1.056,-3.495],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-3.19,3.944],[2.35,1.814],[4.6,1.543]],\"o\":[[-2.702,8.946],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.021,-0.037],[-3.61,-2.786],[-3.733,-1.252]],\"v\":[[-11.366,-17.585],[-11.927,7.863],[-8.625,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[14.152,-8.699],[10.273,-11.929],[-3.437,-12.668]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.5,\"y\":0},\"t\":38,\"s\":[{\"i\":[[0.662,-3.59],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-2.532,4.395],[2.35,1.804],[4.409,1.445]],\"o\":[[-1.585,8.587],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.006,-0.084],[-3.61,-2.771],[-3.578,-1.173]],\"v\":[[-11.286,-19.408],[-12.133,7.863],[-8.83,12.562],[0.35,14.211],[9.404,12.616],[12.749,7.749],[13.232,-10.927],[9.318,-14.209],[-5.653,-15.762]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":45,\"s\":[{\"i\":[[3.203,-10.603],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-3.19,3.944],[3,-0.7],[3.7,2]],\"o\":[[-2.702,8.946],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.08,-0.14],[-3,0.7],[-3.7,-2]],\"v\":[[-11.166,-19.585],[-11.927,7.863],[-8.625,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[14.152,-8.699],[8.332,-14.539],[-4.768,-13.439]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.5,\"y\":0},\"t\":52,\"s\":[{\"i\":[[0.662,-3.59],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-2.532,4.395],[2.352,1.824],[4.412,1.481]],\"o\":[[-1.585,8.587],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.006,-0.084],[-3.613,-2.802],[-3.581,-1.202]],\"v\":[[-11.303,-19.608],[-12.133,7.863],[-8.83,12.562],[0.35,14.211],[9.404,12.616],[12.749,7.749],[13.232,-10.927],[9.315,-14.243],[-5.266,-16.412]],\"c\":false}]},{\"i\":{\"x\":0.2,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":59,\"s\":[{\"i\":[[1.056,-3.495],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-3.19,3.944],[2.33,2.05],[4.563,1.956]],\"o\":[[-2.702,8.946],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.021,-0.037],[-3.578,-3.149],[-3.704,-1.587]],\"v\":[[-11.164,-19.885],[-11.927,7.863],[-8.625,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[14.152,-8.699],[10.308,-12.326],[-4.786,-14.983]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.6,\"y\":0},\"t\":66,\"s\":[{\"i\":[[0.662,-3.59],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-2.532,4.395],[2.352,1.886],[4.412,1.589]],\"o\":[[-1.585,8.587],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.006,-0.084],[-3.613,-2.897],[-3.581,-1.29]],\"v\":[[-11.303,-20.208],[-12.133,7.863],[-8.83,12.562],[0.35,14.211],[9.404,12.616],[12.749,7.749],[13.232,-10.927],[9.315,-14.347],[-4.166,-15.759]],\"c\":false}]},{\"i\":{\"x\":0.13,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":79,\"s\":[{\"i\":[[0.229,-1.092],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-0.774,1.343],[2.54,0.846],[4.54,0.959]],\"o\":[[-0.964,4.6],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.002,-0.026],[-3.902,-1.3],[-3.684,-0.778]],\"v\":[[-12.005,-18.139],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[13.275,-12.666],[9.02,-14.164],[-5.367,-14.041]],\"c\":false}]},{\"t\":94,\"s\":[{\"i\":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0],[0,0]],\"v\":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[8.179,-13.458],[-3.728,-14.103]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":0,\"bm\":0}]}],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":0,\"nm\":\"hand_4\",\"refId\":\"comp_0\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[29.062,27.773,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[11.426,11.426,100],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":420,\"op\":540,\"st\":420,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":0,\"nm\":\"hand_3\",\"refId\":\"comp_1\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[29.062,27.773,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[11.426,11.426,100],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":240,\"op\":420,\"st\":240,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":0,\"nm\":\"hand_2\",\"refId\":\"comp_2\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[29.062,27.773,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[11.426,11.426,100],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":120,\"op\":240,\"st\":120,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":0,\"nm\":\"hand_1\",\"refId\":\"comp_3\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[29.062,27.773,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[11.426,11.426,100],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":0,\"op\":120,\"st\":0,\"bm\":0}],\"markers\":[]}"
  },
  {
    "path": "Telegram/Resources/icons/calls/voice.lottie",
    "content": "{\"v\":\"5.5.7\",\"meta\":{\"g\":\"LottieFiles AE 0.1.20\",\"a\":\"\",\"k\":\"\",\"d\":\"\",\"tc\":\"\"},\"fr\":60,\"ip\":0,\"op\":404,\"w\":60,\"h\":60,\"nm\":\"ALL\",\"ddd\":0,\"assets\":[{\"id\":\"comp_0\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":0,\"nm\":\"Pre-comp 1\",\"refId\":\"comp_1\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[13.6,13.6,100],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":0,\"op\":29,\"st\":0,\"bm\":0}]},{\"id\":\"comp_1\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"START\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[282.33,256.042,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":7,\"s\":[254.33,256.042,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[12.33,0.042,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.27,\"y\":0},\"t\":0,\"s\":[{\"i\":[[2.8,-1.7],[0,0],[5.7,9.5],[0,3.6],[0,0],[-11.1,0],[-3.1,-1.9],[0,0],[5.7,-9.4]],\"o\":[[0,0],[-9.4,5.7],[-1.9,-3.1],[0,0],[0,-11],[3.6,0],[0,0],[9.4,5.7],[-1.7,2.7]],\"v\":[[103.4,17.1],[-58.1,114.7],[-85.5,107.9],[-88.4,97.6],[-88.4,-97.6],[-68.4,-117.5],[-58.1,-114.6],[103.4,-17],[110.2,10.4]],\"c\":true}]},{\"t\":10,\"s\":[{\"i\":[[5.225,-7.477],[6.6,-2.2],[5.7,9.5],[0,3.6],[-0.127,11.332],[-7.35,7.25],[-5.65,0.1],[-14.675,-17.878],[0.55,-11.25]],\"o\":[[-6.15,8.8],[-7.202,2.401],[-1.9,-3.1],[0,0],[0.15,-13.4],[2.563,-2.528],[0,0],[7.1,8.65],[-0.2,7.75]],\"v\":[[36.4,28.7],[15.4,39.95],[-11,30.15],[-16.4,18.1],[-18.65,0.9],[-8.65,-24],[7.4,-31.85],[36.15,-20.4],[43.7,7.5]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Triangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":5,\"st\":-6,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Leg\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[252.45,380.6,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.55,124.6,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":5,\"s\":[{\"i\":[[-6.3,0],[0,0],[0,-1.435],[0,0],[6.3,0],[0,0],[0,1.435],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,1.435],[0,0],[-6.3,0],[0,0],[0,-1.435]],\"v\":[[-5.2,78.5],[-1.9,78.5],[9.5,81.097],[9.5,86.403],[-1.9,89],[-5.2,89],[-16.6,86.403],[-16.6,81.097]],\"c\":true}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":13,\"s\":[{\"i\":[[-6.3,0],[0,0],[0,-6.3],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-6.3]],\"v\":[[-5.15,70.5],[-1.85,70.5],[9.55,81.9],[9.5,113.2],[-1.9,124.6],[-5.2,124.6],[-16.6,113.2],[-16.55,81.9]],\"c\":true}]},{\"i\":{\"x\":0.7,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":22,\"s\":[{\"i\":[[-6.3,0],[0,0],[0,-6.3],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-6.3]],\"v\":[[-5.15,86.75],[-1.85,86.75],[9.55,98.15],[9.5,113.2],[-1.9,124.6],[-5.2,124.6],[-16.6,113.2],[-16.55,98.15]],\"c\":true}]},{\"t\":29,\"s\":[{\"i\":[[-6.3,0],[0,0],[0,-6.3],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-6.3]],\"v\":[[-5.2,78.5],[-1.9,78.5],[9.5,89.9],[9.5,113.2],[-1.9,124.6],[-5.2,124.6],[-16.6,113.2],[-16.6,89.9]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":7,\"op\":30,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Arc R\",\"parent\":4,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.31,\"y\":0},\"t\":8,\"s\":[{\"i\":[[0,0],[35.3,-21.739]],\"o\":[[0,15.6],[0,0]],\"v\":[[78.4,8.4],[36.7,75]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":10,\"s\":[{\"i\":[[0,0],[14.8,-11.381]],\"o\":[[0.03,17.185],[0,0]],\"v\":[[78.7,5.25],[62.7,54.175]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":12,\"s\":[{\"i\":[[0,0],[15.195,-13.612]],\"o\":[[0.075,19.589],[0,0]],\"v\":[[79.155,2.974],[63.805,52.795]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":15,\"s\":[{\"i\":[[0,0],[9.715,-14.875]],\"o\":[[0.094,20.585],[0,0]],\"v\":[[79.372,1.136],[64.285,50.377]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":16,\"s\":[{\"i\":[[0,0],[9.715,-14.875]],\"o\":[[0.094,20.585],[0,0]],\"v\":[[79.372,1.136],[64.285,50.377]],\"c\":false}]},{\"t\":23,\"s\":[{\"i\":[[0,0],[8.7,-12]],\"o\":[[0,15.6],[0,0]],\"v\":[[78.4,8.4],[64.7,50.5]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.711],\"y\":[6.356]},\"o\":{\"x\":[0.392],\"y\":[0]},\"t\":8,\"s\":[100]},{\"i\":{\"x\":[0.66],\"y\":[0.822]},\"o\":{\"x\":[0.353],\"y\":[0.153]},\"t\":9,\"s\":[100]},{\"i\":{\"x\":[0.544],\"y\":[0.863]},\"o\":{\"x\":[0.254],\"y\":[0.3]},\"t\":10,\"s\":[57.213]},{\"i\":{\"x\":[0.564],\"y\":[1]},\"o\":{\"x\":[0.246],\"y\":[0.217]},\"t\":12,\"s\":[19.305]},{\"t\":15,\"s\":[0]}],\"ix\":1},\"e\":{\"a\":0,\"k\":100,\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":5,\"op\":30,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Arc L\",\"parent\":2,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":3,\"s\":[-3.75,84.5,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":13,\"s\":[-3.75,77.5,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.7,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":22,\"s\":[-3.75,87.25,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":29,\"s\":[-3.75,84.5,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.75,84.5,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":5,\"s\":[{\"i\":[[0,0],[13.1,0],[0,42]],\"o\":[[-11,5.1],[-45.3,0],[0,0]],\"v\":[[33,76.5],[-3.5,84.5],[-85.5,6.5]],\"c\":false}]},{\"i\":{\"x\":0.7,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":15,\"s\":[{\"i\":[[0,0],[13.1,0],[0,42]],\"o\":[[-11,5.1],[-45.3,0],[0,0]],\"v\":[[33,76.5],[-3.5,84.5],[-85,2]],\"c\":false}]},{\"t\":23,\"s\":[{\"i\":[[0,0],[13.1,0],[0,42]],\"o\":[[-11,5.1],[-45.3,0],[0,0]],\"v\":[[33,76.5],[-3.5,84.5],[-85.5,6.5]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.31],\"y\":[0]},\"t\":5,\"s\":[15]},{\"t\":15,\"s\":[100]}],\"ix\":1},\"e\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.723],\"y\":[5.239]},\"o\":{\"x\":[0.4],\"y\":[0]},\"t\":5,\"s\":[15]},{\"i\":{\"x\":[0.695],\"y\":[0.969]},\"o\":{\"x\":[0.368],\"y\":[0.188]},\"t\":7,\"s\":[15]},{\"t\":8,\"s\":[0]}],\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":5,\"op\":30,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"Line\",\"parent\":6,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[3.687,-6.898,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[3.6,-7,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":5,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-101.613,-97],[108.787,112]],\"c\":false}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":15,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-108.213,-118.069],[115.413,104.069]],\"c\":false}]},{\"t\":23,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-101.6,-111.5],[108.8,97.5]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.4],\"y\":[1]},\"o\":{\"x\":[0.31],\"y\":[0]},\"t\":5,\"s\":[34]},{\"t\":15,\"s\":[0]}],\"ix\":1},\"e\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.4],\"y\":[1]},\"o\":{\"x\":[0.31],\"y\":[0]},\"t\":5,\"s\":[59]},{\"t\":15,\"s\":[100]}],\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path-10\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":5,\"op\":30,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"Head\",\"parent\":4,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-3.6,-33.55,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.6,-33.55,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.31,\"y\":0},\"t\":3,\"s\":[{\"i\":[[-25.4,0],[0,-14.432],[0,0],[12.95,-6.477],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,6.477],[0,0],[0,0],[0,0],[0,-14.432]],\"v\":[[-3.6,-45],[42.4,-18.863],[42.4,24.32],[23.55,44.576],[-27.58,17.939],[-49.7,3.439],[-49.6,-18.863]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[-25.217,0],[0,-18.82],[0,0],[12.857,-8.447],[0,0],[0,0],[0,0]],\"o\":[[25.217,0],[0,0],[0,8.447],[0,0],[0,0],[0,0],[0,-18.82]],\"v\":[[-3.6,-74.003],[42.068,-39.92],[42.068,16.391],[23.354,42.805],[-22.609,2.243],[-49.345,-21.035],[-49.269,-39.92]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[-25.133,0],[0,-20.833],[0,0],[7.486,-6.386],[0,0],[0,0],[0,0]],\"o\":[[25.133,0],[0,0],[0,9.35],[0,0],[0,0],[0,0],[0,-20.833]],\"v\":[[-3.601,-87.309],[41.916,-49.58],[41.916,12.754],[28.014,36.493],[-16.079,-7.707],[-49.113,-39.513],[-49.117,-49.58]],\"c\":true}]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[-24.994,0],[0,-24.173],[0,0],[9.026,-7.804],[0,0],[0,0],[0,0]],\"o\":[[24.994,0],[0,0],[0,10.849],[0,0],[0,0],[0,0],[0,-24.173]],\"v\":[[-3.601,-109.388],[41.663,-65.611],[41.663,6.718],[27.287,33.135],[-21.692,-14.203],[-48.939,-44.406],[-48.865,-65.611]],\"c\":true}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":13,\"s\":[{\"i\":[[-24.882,0],[0,-26.136],[0,0],[6.004,-6.398],[0,0],[0,0],[0,0]],\"o\":[[24.882,0],[0,0],[0,11.73],[0,0],[0,0],[0,0],[0,-26.136]],\"v\":[[-3.601,-123],[41.462,-75.667],[41.462,2.534],[30.496,26.07],[-20.343,-23.873],[-48.773,-48.671],[-48.664,-75.667]],\"c\":true}]},{\"t\":22,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[6.9,-8],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.6,-117.5],[42.4,-71.5],[42.4,4.5],[31.3,34.4],[-20.33,-17.23],[-49.6,-46.5],[-49.6,-71.5]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.31,\"y\":0},\"t\":3,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.9,0],[0,14.432],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-11.85,5.796],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.7,1.365],[-24.952,12.671],[-23,13.78],[26.35,43.241],[-3.6,50.4],[-49.6,24.263],[-49.7,1.365]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.857,0],[0,18.82],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-11.765,7.558],[-25.217,0],[0,0],[0,0]],\"v\":[[-49.345,-23.74],[-20,-4.627],[-18.062,-3.18],[26.134,41.064],[-3.6,50.4],[-49.269,16.317],[-49.345,-23.74]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.838,0],[0,20.833],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-10.035,6.292],[-25.133,0],[0,0],[0,0]],\"v\":[[-49.113,-23.507],[-25.728,0.688],[-23.796,2.289],[19.285,43.816],[-3.601,50.4],[-49.117,12.672],[-49.113,-23.257]],\"c\":true}]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.806,0],[0,24.173],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-6.163,2.551],[-24.994,0],[0,0],[0,0]],\"v\":[[-49.382,-11.516],[-31.116,7.197],[-29.195,9.055],[8.663,46.861],[-3.601,50.4],[-48.865,6.622],[-49.243,-11.446]],\"c\":true}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":13,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.78,0],[0,26.837],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-4.239,1.413],[-24.882,0],[0,0],[0,0]],\"v\":[[-48.796,-13.783],[-30.018,6.241],[-28.105,8.304],[9.739,46.087],[-3.601,50.4],[-48.664,1.797],[-48.546,-13.658]],\"c\":true}]},{\"t\":22,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.9,0],[0,25.4],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-5.2,2],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.6,-15.4],[-40.702,-6.502],[-38.75,-4.55],[13.1,47.3],[-3.6,50.4],[-49.6,4.4],[-49.6,-15.4]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":4,\"op\":30,\"st\":0,\"bm\":0}]},{\"id\":\"comp_2\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":0,\"nm\":\"Pre-comp 1\",\"refId\":\"comp_3\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[13.6,13.6,100],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":0,\"op\":33,\"st\":0,\"bm\":0}]},{\"id\":\"comp_3\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Line\",\"parent\":2,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[3.687,-6.898,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[3.6,-7,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.2,\"y\":0},\"t\":14,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-101.6,-111.5],[108.8,97.5]],\"c\":false}]},{\"t\":26,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-101.6,-111.5],[108.8,97.5]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.2],\"y\":[0]},\"t\":14,\"s\":[14]},{\"t\":26,\"s\":[0]}],\"ix\":1},\"e\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.2],\"y\":[0]},\"t\":14,\"s\":[86]},{\"t\":26,\"s\":[100]}],\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path-10\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":14,\"op\":34,\"st\":-1,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Head\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-3.6,-33.55,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.6,-33.55,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-25.4,0],[0,-25.4],[0,0],[6.9,-8],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.6,-117.5],[42.4,-71.5],[42.4,4.5],[31.3,34.4],[-20.33,-17.23],[-49.6,-46.5],[-49.6,-71.5]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.9,0],[0,25.4],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-5.2,2],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.6,-15.4],[-40.702,-6.502],[-38.75,-4.55],[13.1,47.3],[-3.6,50.4],[-49.6,4.4],[-49.6,-15.4]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":12,\"op\":34,\"st\":-1,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Arc L\",\"parent\":5,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":13,\"s\":[-3.75,69.094,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":25,\"s\":[-3.75,89.094,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":33,\"s\":[-3.75,84.5,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.75,84.5,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.29,\"y\":0},\"t\":17,\"s\":[{\"i\":[[0,0],[13.1,0],[0,42]],\"o\":[[-11,5.1],[-45.3,0],[0,0]],\"v\":[[33,76.5],[-3.5,84.5],[-85.5,6.5]],\"c\":false}]},{\"t\":27,\"s\":[{\"i\":[[0,0],[13.1,0],[0,42]],\"o\":[[-11,5.1],[-45.3,0],[0,0]],\"v\":[[33,76.5],[-3.5,84.5],[-85.5,6.5]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.723],\"y\":[0.572]},\"o\":{\"x\":[0.4],\"y\":[0]},\"t\":4,\"s\":[15]},{\"i\":{\"x\":[0.695],\"y\":[0.627]},\"o\":{\"x\":[0.368],\"y\":[0.45]},\"t\":6,\"s\":[29]},{\"i\":{\"x\":[0.323],\"y\":[1]},\"o\":{\"x\":[0.222],\"y\":[0.388]},\"t\":7,\"s\":[37.868]},{\"t\":17,\"s\":[100]}],\"ix\":1},\"e\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.723],\"y\":[1.14]},\"o\":{\"x\":[0.4],\"y\":[0]},\"t\":4,\"s\":[12]},{\"i\":{\"x\":[0.695],\"y\":[0.945]},\"o\":{\"x\":[0.368],\"y\":[0.061]},\"t\":6,\"s\":[17.559]},{\"i\":{\"x\":[0.652],\"y\":[0.857]},\"o\":{\"x\":[0.357],\"y\":[0.122]},\"t\":7,\"s\":[9]},{\"i\":{\"x\":[0.426],\"y\":[1]},\"o\":{\"x\":[0.183],\"y\":[-2.717]},\"t\":9,\"s\":[0]},{\"t\":17,\"s\":[0]}],\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":6,\"op\":34,\"st\":-1,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Arc R\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.29,\"y\":0},\"t\":17,\"s\":[{\"i\":[[0,0],[8.7,-12]],\"o\":[[0,15.6],[0,0]],\"v\":[[78.4,8.4],[64.7,50.5]],\"c\":false}]},{\"t\":27,\"s\":[{\"i\":[[0,0],[8.7,-12]],\"o\":[[0,15.6],[0,0]],\"v\":[[78.4,8.4],[64.7,50.5]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.4],\"y\":[0]},\"t\":7,\"s\":[99]},{\"t\":17,\"s\":[0]}],\"ix\":1},\"e\":{\"a\":0,\"k\":100,\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":10,\"op\":34,\"st\":-1,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"Leg\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[252.45,380.6,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.55,124.6,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":13,\"s\":[{\"i\":[[-6.3,0],[0,0],[0,-6.3],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-6.3]],\"v\":[[-5.15,64],[-1.85,64],[9.55,75.4],[9.5,113.2],[-1.9,124.6],[-5.2,124.6],[-16.6,113.2],[-16.55,75.4]],\"c\":true}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":25,\"s\":[{\"i\":[[-6.3,0],[0,0],[0,-2.846],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-2.846]],\"v\":[[-5.2,84.75],[-1.9,84.75],[9.5,89.9],[9.5,113.2],[-1.9,124.6],[-5.2,124.6],[-16.6,113.2],[-16.6,89.9]],\"c\":true}]},{\"t\":33,\"s\":[{\"i\":[[-6.3,0],[0,0],[0,-6.3],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-6.3]],\"v\":[[-5.2,78.5],[-1.9,78.5],[9.5,89.9],[9.5,113.2],[-1.9,124.6],[-5.2,124.6],[-16.6,113.2],[-16.6,89.9]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":9,\"op\":34,\"st\":-1,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"Line Bell\",\"parent\":7,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.28,\"y\":0},\"t\":-1,\"s\":[5.03,-9.859,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":14,\"s\":[4.03,-8.359,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.05,-2.95,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-98.152,-101.129],[98.052,95.229]],\"c\":false},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.28],\"y\":[0]},\"t\":-1,\"s\":[0]},{\"t\":14,\"s\":[12]}],\"ix\":1},\"e\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.28],\"y\":[0]},\"t\":-1,\"s\":[100]},{\"t\":14,\"s\":[88]}],\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":14,\"st\":-1,\"bm\":0},{\"ddd\":0,\"ind\":7,\"ty\":4,\"nm\":\"Bell\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":-1,\"s\":[258.564,256.028,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":13,\"s\":[253.564,241.028,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-1.88,-0.927,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":-1,\"s\":[{\"i\":[[-4.2,9.7],[0,0],[0,0],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-74.4,-46.7],[32.121,59.821],[34.778,62.478],[51.9,79.6],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"t\":13,\"s\":[{\"i\":[[-0.557,1.753],[0,0],[0,0],[0,0],[12.242,3.875],[4.463,6.051],[0,0],[0,0],[0,1.225],[0.586,4.254],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0],[-8.598,-2.722],[0,0],[0,0],[-0.69,-2.075],[0,0],[-1.67,-12.113],[0.086,-1.777]],\"v\":[[-48.888,-16.333],[2.602,35.024],[4.48,36.832],[14.055,46.983],[-15.561,47.733],[-39.407,31.182],[-39.266,30.978],[-44.13,22.682],[-46.406,17.666],[-48.156,11.291],[-49.281,-8.366]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":-1,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[10.111,-11.873],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[10.58,-1.555]],\"v\":[[88.55,58.6],[73.795,40.25],[71.995,35.45],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-62.4,-65.9],[-19.568,-22.503],[-18.761,-21.685],[79.699,78.074],[80.6,78.975]],\"c\":true}]},{\"t\":13,\"s\":[{\"i\":[[-2.182,4.07],[-1.1,3.295],[0,1.8],[0,0],[25.933,14.308],[0,0],[11.842,-0.367],[0,0],[3.626,-2.33],[0,0],[-2.875,-21.055],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0.815,-2.442],[0,0],[0.221,-54.605],[0,0],[-4.442,-2.455],[0,0],[-13.97,-0.305],[0,0],[-19.874,13.29],[0,0],[0,0],[0,0],[0,0],[2.995,-3.055]],\"v\":[[36.363,27.6],[40.655,19.625],[42.779,11.2],[43.21,-19.85],[21.372,-111.763],[20.684,-112.312],[-3.286,-118.838],[-3.6,-118.9],[-29.821,-110.375],[-31.571,-108.995],[-48.57,-47.15],[-22.056,-18.753],[-21.467,-17.935],[30.402,32.574],[31.31,33.225]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":12,\"st\":-1,\"bm\":0},{\"ddd\":0,\"ind\":8,\"ty\":4,\"nm\":\"Bottom Bell\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[255.445,394.655,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,137.7,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.4,\"y\":0},\"t\":-1,\"s\":[{\"i\":[[-13.1,0],[-2,12],[1.2,0],[20.1,0],[-0.2,-1.3]],\"o\":[[12.8,0],[0.2,-1.4],[-10.3,0],[-0.9,0],[1.9,12.1]],\"v\":[[0.5,119.605],[26.1,97.805],[22.8,93.605],[-20.9,93.605],[-25.2,97.405]],\"c\":true}]},{\"t\":9,\"s\":[{\"i\":[[-8.162,0],[0.625,15.53],[0.51,0.546],[4.907,-5.5],[0.14,-2.002]],\"o\":[[7.975,0],[0.125,-2.157],[-6.069,-6.5],[-0.373,0.418],[-1.164,16.646]],\"v\":[[-3.269,123.85],[10.181,105.515],[9.694,64.545],[-15.782,65.045],[-16.531,104.149]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":9,\"st\":-1,\"bm\":0}]},{\"id\":\"comp_4\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":0,\"nm\":\"Pre-comp 1\",\"refId\":\"comp_5\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[13.6,13.6,100],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":0,\"op\":33,\"st\":0,\"bm\":0}]},{\"id\":\"comp_5\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"EXAMPLE ON 2\",\"parent\":4,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-3.981,2.44,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.038,1.5,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-9.1,0],[0,0],[0,-9.1],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[9,0],[0,0],[0,0],[0,0],[0,0],[0,-9.1]],\"v\":[[-1.6,-115.5],[-0.4,-115.5],[16,-99.1],[16,-89.5],[16,-89.5],[-18,-89.5],[-18,-99.1]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-13.1,0],[-2,12],[1.2,0],[20.1,0],[-0.2,-1.3]],\"o\":[[12.8,0],[0.2,-1.4],[-10.3,0],[-0.9,0],[1.9,12.2]],\"v\":[[0,118.5],[25.6,96.7],[22.3,92.5],[-21.4,92.5],[-25.7,96.3]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,1.8],[0,0],[0,0],[36.4,8.6],[3.4,0],[5.9,-0.1],[0,-38.8],[0,0],[1.2,-1.3],[0,0],[0,0],[-11.3,0],[0,0],[8.1,7.9],[0,0]],\"o\":[[0,0],[0,0],[0,-38.6],[-6.7,-0.2],[-3,0],[-36.5,8.6],[0,0],[0,1.8],[0,0],[0,0],[-8,7.9],[0,0],[11.3,0],[0,0],[-1.1,-1.3]],\"v\":[[76.4,35.6],[76.4,-15.7],[76.4,-15.7],[14.3,-93.3],[-0.8,-93.5],[-14.2,-93.3],[-76.3,-15.7],[-76.3,35.6],[-78.1,40.4],[-92.8,57],[-92.8,57],[-81.5,78.5],[81.3,78.5],[92.7,57],[78.1,40.4]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Combined-Shape\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":3,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":15,\"op\":33,\"st\":-1,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Line Bell\",\"parent\":4,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.4,\"y\":0},\"t\":2,\"s\":[5.03,-9.859,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":23,\"s\":[12.954,-0.45,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.05,-2.95,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-98.152,-101.129],[98.052,95.229]],\"c\":false},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.4],\"y\":[0]},\"t\":2,\"s\":[0]},{\"t\":23,\"s\":[100]}],\"ix\":1},\"e\":{\"a\":0,\"k\":100,\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":21,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Bottom Bell\",\"parent\":4,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[0,137.7,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,137.7,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-13.1,0],[-2,12],[1.2,0],[20.1,0],[-0.2,-1.3]],\"o\":[[12.8,0],[0.2,-1.4],[-10.3,0],[-0.9,0],[1.9,12.1]],\"v\":[[-4.5,119.6],[21.1,97.8],[17.8,93.6],[-25.9,93.6],[-30.2,97.4]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":15,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Bell\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[260.562,232.132,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0.12,-24.927,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.7,0.7,0.7],\"y\":[1,1,1]},\"o\":{\"x\":[0.3,0.3,0.3],\"y\":[0,0,0]},\"t\":0,\"s\":[100,100,100]},{\"i\":{\"x\":[0.7,0.7,0.7],\"y\":[1,1,1]},\"o\":{\"x\":[0.3,0.3,0.3],\"y\":[0,0,0]},\"t\":12,\"s\":[93,93,100]},{\"i\":{\"x\":[0.7,0.7,0.7],\"y\":[1,1,1]},\"o\":{\"x\":[0.3,0.3,0.3],\"y\":[0,0,0]},\"t\":22,\"s\":[101,101,100]},{\"t\":32,\"s\":[100,100,100]}],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[-4.2,9.7],[0,0],[0,0],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-74.4,-46.7],[32.121,59.821],[34.778,62.478],[51.9,79.6],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":8,\"s\":[{\"i\":[[-14.003,15.695],[-5.659,-3.648],[-10.756,-14.742],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[-6.594,-3.203],[4.9,6.715],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-63.408,-63.817],[-47.235,-56.249],[-60.858,-34.829],[50.693,79.614],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[-14.003,15.695],[-5.659,-3.648],[-10.756,-14.742],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[-6.594,-3.203],[4.9,6.715],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-63.408,-63.817],[-24.266,-35.672],[-39.752,-12.387],[50.693,79.614],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":10,\"s\":[{\"i\":[[-14.003,15.695],[-5.659,-3.648],[-10.756,-14.742],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[-6.594,-3.203],[4.9,6.715],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-63.408,-63.817],[-3.42,-13.502],[-18.646,10.055],[50.693,79.614],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":12,\"s\":[{\"i\":[[-14.003,15.695],[-5.659,-3.648],[-10.756,-14.742],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[-6.594,-3.203],[4.9,6.715],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-63.408,-63.817],[39.053,27.896],[23.827,51.453],[50.693,79.614],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":13,\"s\":[{\"i\":[[-14.003,15.695],[-5.659,-3.648],[-10.756,-14.742],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[-6.594,-3.203],[4.9,6.715],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-63.408,-63.817],[52.725,43.176],[39.376,66.197],[50.693,79.614],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":14,\"s\":[{\"i\":[[-14.003,15.695],[-5.659,-3.648],[-7.11,-19.386],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[-6.594,-3.203],[1.271,3.465],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-63.408,-63.817],[67.899,50.896],[48.96,73.385],[50.693,79.614],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"t\":15,\"s\":[{\"i\":[[-7.545,13.495],[0,0],[0,0],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-68.9,-57.2],[46.687,58.223],[49.57,61.102],[68.15,79.655],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[10.111,-11.873],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[10.58,-1.555]],\"v\":[[88.55,58.6],[73.795,40.25],[71.995,35.45],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-62.4,-65.9],[-19.568,-22.503],[-18.761,-21.685],[79.699,78.074],[80.6,78.975]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":8,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[9.92,-13.055],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[9.935,-1.566]],\"v\":[[88.675,58.537],[73.795,40.125],[71.995,35.325],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-64.104,-63.629],[-25.68,-45.311],[-14.342,-27.377],[81.753,76.333],[80.85,79.006]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":10,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[9.92,-13.055],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[9.935,-1.566]],\"v\":[[88.675,58.537],[73.795,40.125],[71.995,35.325],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-64.104,-63.629],[7.983,-0.159],[27.87,17.507],[81.753,76.333],[80.85,79.006]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":12,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[9.92,-13.055],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[9.935,-1.566]],\"v\":[[88.675,58.537],[73.795,40.125],[71.995,35.325],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-64.104,-63.629],[50.456,41.238],[70.343,58.905],[81.753,76.333],[80.85,79.006]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":13,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[9.92,-13.055],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[9.935,-1.566]],\"v\":[[88.675,58.537],[73.795,40.125],[71.995,35.325],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-64.104,-63.629],[51.394,43.785],[70.343,58.905],[81.753,76.333],[80.85,79.006]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":14,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[9.92,-13.055],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[9.935,-1.566]],\"v\":[[88.675,58.537],[73.795,40.125],[71.995,35.325],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-64.104,-63.629],[66.569,51.505],[70.343,58.905],[81.753,76.333],[80.85,79.006]],\"c\":true}]},{\"t\":15,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[9.346,-16.6],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[8,-1.6]],\"v\":[[89.05,58.35],[73.795,39.75],[71.995,34.95],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-69.215,-56.816],[-28.207,-15.716],[-27.434,-14.941],[66.835,79.54],[81.6,79.1]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":15,\"st\":0,\"bm\":0}]},{\"id\":\"comp_6\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":0,\"nm\":\"Pre-comp 1\",\"refId\":\"comp_7\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.7,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[30,30,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":10,\"s\":[28.282,29.857,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.7,0.7,0.7],\"y\":[1,1,1]},\"o\":{\"x\":[0.3,0.3,0.3],\"y\":[0,0,0]},\"t\":0,\"s\":[13.6,13.6,100]},{\"t\":10,\"s\":[13.5,13.5,100]}],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":0,\"op\":37,\"st\":0,\"bm\":0}]},{\"id\":\"comp_7\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Head 2\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[0.613,-101.537,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":12,\"s\":[11.613,-50.537,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":26,\"s\":[11.613,-89.537,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":36,\"s\":[11.113,-83.037,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[11.613,-84.537,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,-8.774],[8.774,0],[0,8.774],[-8.774,0]],\"o\":[[0,8.774],[-8.774,0],[0,-8.774],[8.774,0]],\"v\":[[25.5,-84.537],[9.613,-68.65],[-6.274,-84.537],[9.613,-100.424]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[0,-11.72],[13.247,0],[0,11.72],[-13.247,0]],\"o\":[[0,11.72],[-13.247,0],[0,-11.72],[13.247,0]],\"v\":[[33.09,-69.693],[9.103,-48.473],[-14.883,-69.693],[9.103,-90.913]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":14,\"s\":[{\"i\":[[0,-24.735],[24.735,0],[0,24.735],[-24.735,0]],\"o\":[[0,24.735],[-24.735,0],[0,-24.735],[24.735,0]],\"v\":[[56.4,-84.537],[11.613,-39.75],[-33.174,-84.537],[11.613,-129.324]],\"c\":true}]},{\"t\":25,\"s\":[{\"i\":[[0,-26.924],[26.924,0],[0,26.924],[-26.924,0]],\"o\":[[0,26.924],[-26.924,0],[0,-26.924],[26.924,0]],\"v\":[[60.363,-84.537],[11.613,-35.787],[-37.137,-84.537],[11.613,-133.287]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":37,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Hands 2\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0,\"y\":1},\"o\":{\"x\":0.05,\"y\":0},\"t\":9,\"s\":[11.336,13.774,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":12,\"s\":[11.336,18.274,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":25,\"s\":[11.336,11.774,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[11.336,10.774,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":7,\"s\":[{\"i\":[[0,0],[0.875,44.36],[0.012,11.61]],\"o\":[[0,0],[-0.348,-17.658],[-0.016,-14.955]],\"v\":[[-33.583,109.659],[-40.875,50.64],[-45.012,-17.11]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0,0],[-7.48,30.25],[-7.964,9.648]],\"o\":[[0,0],[2.73,-11.373],[7.228,-11.196]],\"v\":[[-57.163,97.688],[-57.996,36.651],[-42.036,0.102]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":10,\"s\":[{\"i\":[[0,0],[-14.611,30.379],[-23.925,1.795]],\"o\":[[0,0],[7.389,-12.121],[13.171,-8.112]],\"v\":[[-77.526,99.758],[-81.889,29.906],[-41.075,4.99]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":11,\"s\":[{\"i\":[[0,0],[-27.718,19.589],[-10.136,-1.028]],\"o\":[[0,0],[21.782,-16.411],[19.114,-5.028]],\"v\":[[-107.888,77.829],[-93.782,16.161],[-42.114,8.877]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":13,\"s\":[{\"i\":[[0,0],[-27.325,4.127],[-14.975,-3.949]],\"o\":[[0,0],[20.334,-3.734],[35.444,0.066]],\"v\":[[-136.225,32.858],[-90.834,7.893],[-38.525,10.472]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":14,\"s\":[{\"i\":[[0,0],[-27.129,-3.603],[-14.269,-4.157]],\"o\":[[0,0],[19.61,2.605],[43.609,2.613]],\"v\":[[-147.393,-5.627],[-89.61,-2.992],[-38.231,9.77]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":18,\"s\":[{\"i\":[[0,0],[-38.825,-22.336],[-19.04,-2.789]],\"o\":[[0,0],[14.675,9.164],[62.76,8.586]],\"v\":[[-144.801,-73.381],[-91.175,-7.197],[-37.96,10.757]],\"c\":false}]},{\"t\":28,\"s\":[{\"i\":[[0,0],[-46.458,-35.583],[-19.738,-3.567]],\"o\":[[0,0],[8.352,6.397],[79.039,14.286]],\"v\":[[-142.333,-130.341],[-88.062,-16.235],[-40.512,7.89]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":7,\"s\":[{\"i\":[[0,0],[0.583,25.777],[4.5,19.11]],\"o\":[[0,0],[-0.238,-10.518],[-18.41,-78.181]],\"v\":[[63,101.64],[63.167,54.973],[56,-16.61]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0,0],[2.455,25.149],[6.868,13.265]],\"o\":[[0,0],[-0.66,-9.745],[-29.265,-65.813]],\"v\":[[68.548,94.42],[70.368,48.723],[56.521,2.29]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":11,\"s\":[{\"i\":[[-11.495,10.995],[15.537,39.692],[11.504,1.535]],\"o\":[[14.034,-13.424],[-3.378,-8.551],[-47.075,-45.52]],\"v\":[[79.466,115.674],[91.463,38.058],[62.496,12.174]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":13,\"s\":[{\"i\":[[-14.54,10.097],[25.244,37.544],[12.466,1.965]],\"o\":[[15.872,-11.198],[-4.741,-7.952],[-56.007,-35.342]],\"v\":[[88.294,117.357],[93.089,33.781],[63.701,13.504]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":14,\"s\":[{\"i\":[[-16.063,9.648],[30.097,36.471],[11.946,0.457]],\"o\":[[16.791,-10.086],[-5.422,-7.653],[-60.473,-30.253]],\"v\":[[92.709,118.199],[93.903,31.642],[61.554,13.67]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":18,\"s\":[{\"i\":[[-5.509,3.309],[41.86,36.38],[11.653,0.923]],\"o\":[[5.759,-3.459],[-7.371,-6.796],[-73.243,-15.702]],\"v\":[[124.602,127.851],[94.405,26.111],[62.332,13.5]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":25,\"s\":[{\"i\":[[0,0],[48,36.333],[11.5,1.167]],\"o\":[[0,0],[-8.388,-6.35],[-79.91,-8.107]],\"v\":[[141.25,132.89],[94.667,23.223],[63.5,11.89]],\"c\":false}]},{\"t\":28,\"s\":[{\"i\":[[0,0],[48,36.333],[11.5,1.167]],\"o\":[[0,0],[-8.388,-6.35],[-79.91,-8.107]],\"v\":[[140,128.89],[94.667,23.848],[63.5,11.89]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":9,\"op\":37,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Body 2\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":12,\"s\":[266.676,398.378,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":23,\"s\":[266.676,387.378,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":34,\"s\":[266.676,392.378,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[10.676,136.378,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.69,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":12,\"s\":[{\"i\":[[-12.288,0.098],[-12.498,-0.987],[0,0],[3.339,-1.222],[4.897,-9.213],[22.369,0],[7.456,13.82],[-1.892,54.808]],\"o\":[[8.847,-0.071],[35.724,2.82],[24.706,0],[0,0],[-7.456,13.82],[-22.369,0],[-4.897,-9.213],[-12.353,-3.008]],\"v\":[[-46.407,7.071],[-18.334,9.585],[61.794,9.679],[70.141,28.952],[68.805,121.176],[13.161,136.5],[-42.484,121.176],[-47.047,25.191]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":23,\"s\":[{\"i\":[[-10.8,-2.3],[-8.2,-0.4],[0,0],[3,-1.3],[4.4,-9.8],[20.1,0],[6.7,14.7],[-1.7,58.3]],\"o\":[[7.9,1.7],[0,0],[22.2,0],[0,0],[-6.7,14.7],[-20.1,0],[-4.4,-9.8],[-11.1,-3.2]],\"v\":[[-40.2,-2.3],[-15.1,1.5],[56.9,1.6],[64.4,22.1],[63.2,120.2],[13.2,136.5],[-36.8,120.2],[-40.9,18.1]],\"c\":true}]},{\"t\":34,\"s\":[{\"i\":[[-10.8,-2.3],[-8.2,-0.4],[0,0],[3,-1.3],[4.4,-9.8],[24.175,0.094],[6.7,14.7],[-1.7,58.3]],\"o\":[[7.9,1.7],[0,0],[22.2,0],[0,0],[-6.669,14.144],[-20.1,-0.078],[-4.4,-9.8],[-11.1,-3.2]],\"v\":[[-40.2,-2.3],[-15.1,1.5],[56.9,1.6],[64.4,22.1],[63.044,119.606],[13.2,136.156],[-36.8,120.2],[-40.9,18.1]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":12,\"op\":37,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Line Bell\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[264.975,247.096,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":12,\"s\":[260.475,307.096,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.05,-2.95,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-98.152,-101.129],[98.052,95.229]],\"c\":false},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.3],\"y\":[0]},\"t\":0,\"s\":[0]},{\"t\":12,\"s\":[45]}],\"ix\":1},\"e\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.3],\"y\":[0]},\"t\":0,\"s\":[100]},{\"t\":12,\"s\":[69]}],\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":13,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"Bell\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[260.467,231.7,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":12,\"s\":[255.967,270.7,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.033,-25.3,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-4.2,9.7],[0,0],[0,0],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-74.4,-46.7],[32.121,59.821],[34.778,62.478],[51.9,79.6],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[-8.362,11.23],[0,0],[0,0],[3.762,-1.859],[8.142,-0.264],[0.777,8.157],[0.156,0.224],[1.314,3.134],[0.224,1.943],[0,0],[-0.582,7.711]],\"o\":[[0,0],[0,0],[0,0],[-19.321,9.547],[-28.463,-1.013],[-0.005,-0.156],[-0.156,-3.776],[0.062,-2.373],[0,0],[-0.045,-4.946],[0,-7.124]],\"v\":[[-60.638,-49.681],[29.345,35.402],[55.841,63.676],[71.571,79.252],[-31.443,86.874],[-68.777,71.642],[-68.75,71.544],[-67.843,58.677],[-67.812,51.954],[-68.27,48.401],[-68.748,-3.168]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[-1.323,3.262],[0,0],[0,0],[6.275,-3.565],[14.005,-0.455],[0.542,0.356],[1.713,1.084],[2.26,5.391],[0.385,2.046],[0,0],[-1.001,13.264]],\"o\":[[0,0],[0,0],[0,0],[-4.624,2.627],[-40.891,-1.743],[0.542,-0.578],[-4.048,-2.564],[-0.758,-3.145],[0,0],[-0.077,-8.507],[0,-3.901]],\"v\":[[-57.609,-43.228],[22.617,23.841],[64.126,70.129],[61.225,80.722],[7.766,92.111],[-48.042,81.338],[-48.213,81.277],[-57.218,71.044],[-58.46,62.937],[-59.249,56.826],[-60.071,5.063]],\"c\":true}]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[-0.309,0.762],[0,0],[0,0],[4.568,-5.804],[17.516,-1.439],[9.846,0.175],[5.202,1.836],[3.057,6.91],[0.521,1.988],[0,0],[-1.354,17]],\"o\":[[0,0],[0,0],[0,0],[-4.183,5.585],[-15.234,1.811],[-9.592,-0.227],[-5.947,-2.247],[-1.448,-3.573],[0,0],[-0.104,-10.903],[0,-0.911]],\"v\":[[-52.53,-28.79],[11.994,25.222],[67.477,74.382],[64.512,86.679],[25.234,99.127],[-10.469,99.989],[-34.862,95.528],[-48.272,82.948],[-50.586,74.249],[-51.653,66.417],[-52.765,18.153]],\"c\":true}]},{\"t\":12,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[4.244,-6.392],[19.61,-0.621],[11.618,0.122],[6.498,1.941],[3.306,7.372],[0.563,1.97],[0,0],[-1.464,18.138]],\"o\":[[0,0],[0,0],[0,0],[-4.244,6.392],[-5.683,0.18],[-11.618,-0.122],[-6.595,-1.969],[-1.661,-3.704],[0,0],[-0.113,-11.633],[0,0]],\"v\":[[-46.474,-23.891],[12.383,25.893],[71.24,75.677],[67.655,88.306],[34.655,101.868],[5.941,102.334],[-23.997,99.619],[-42.777,86.574],[-45.423,77.695],[-46.577,69.338],[-47.779,22.141]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[10.111,-11.873],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[10.58,-1.555]],\"v\":[[88.55,58.6],[73.795,40.25],[71.995,35.45],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-62.4,-65.9],[-19.568,-22.503],[-18.761,-21.685],[79.699,78.074],[80.6,78.975]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[0.157,8.024],[0,0],[0,1.922],[0,0],[24.177,-0.77],[0,0],[9,0],[0,0],[5.709,0.272],[0,0],[-0.26,-17.504],[0,0],[0,0],[0,0],[-7.459,5.266]],\"o\":[[0,0],[-0.323,-1.991],[0,0],[1.096,-24.22],[0,0],[-9.573,2.118],[0,0],[-9.1,0],[0,0],[-6.6,1.142],[0,0],[0,0],[0,0],[0,0],[3.993,-2.819]],\"v\":[[71.843,56.59],[71.106,36.657],[70.583,30.009],[69.845,-21.987],[38.823,-52.309],[28.073,-50.661],[3.062,-46.678],[1.048,-46.86],[-26.209,-52.017],[-31.319,-53.002],[-58.24,-38.393],[-25.737,-8.451],[-22.131,-4.064],[59.51,80.628],[65.709,78.296]],\"c\":true}]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0.094,4.576],[0,0],[0,1.828],[0,0],[20.355,-2.421],[0,0],[9,0],[0,0],[6.507,0.351],[0,0],[2.835,-13.717],[0,0],[0,0],[0,0],[-1.742,1.23]],\"o\":[[0,0],[-0.014,-2.094],[0,0],[-0.871,-19.045],[0,0],[-6.547,0.878],[0,0],[-9.1,0],[0,0],[-3.266,-0.12],[0,0],[0,0],[0,0],[0,0],[1.239,-8.208]],\"v\":[[69.031,57.556],[68.76,39.315],[68.408,33.605],[67.871,-20.767],[25.645,-36.641],[23.135,-36.246],[-0.875,-35.181],[-1.998,-35.415],[-22.891,-36.675],[-31.934,-38.285],[-52.647,-28.157],[-24.554,-1.976],[-21.081,1.4],[63.583,76.467],[67.733,76.517]],\"c\":true}]},{\"t\":12,\"s\":[{\"i\":[[0.075,3.525],[0,0],[0,1.8],[0,0],[24.798,0.638],[0,0],[9.018,0],[0,0],[6.763,0.375],[0,0],[-1.858,-4.475],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0.08,-2.125],[0,0],[1.607,-13.525],[0,0],[-5.636,0.5],[0,0],[-9.118,0],[0,0],[-2.254,-0.505],[0,0],[0,0],[0,0],[0,0],[0.401,-9.85]],\"v\":[[71.166,57.85],[71.036,40.125],[70.735,34.7],[70.643,-14.975],[22.897,-23.388],[22.897,-23.375],[0.453,-23.525],[0.253,-23.775],[-17.431,-24.5],[-24.068,-24.87],[-46.892,-24.4],[-21.382,-0.003],[-17.943,3.065],[67.559,75.199],[71.091,75.975]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":13,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"Bottom Bell\",\"parent\":5,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[0,136.7,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":12,\"s\":[12,91.7,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,137.7,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-13.1,0],[-2,12],[1.2,0],[20.1,0],[-0.2,-1.3]],\"o\":[[12.8,0],[0.2,-1.4],[-10.3,0],[-0.9,0],[1.9,12.1]],\"v\":[[-5.125,120.225],[20.6,99.05],[17.3,94.85],[-26.4,94.85],[-30.7,98.65]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[-19.183,0.659],[-2.93,11.67],[1.758,0],[29.45,0],[-0.296,-1.263]],\"o\":[[23.128,-0.795],[2.186,-3.703],[-15.091,0],[-1.319,0],[2.784,11.767]],\"v\":[[-2.487,124.469],[35.205,101.877],[29.62,99.793],[-34.409,99.793],[-40.709,103.488]],\"c\":true}]},{\"t\":12,\"s\":[{\"i\":[[-28.151,0],[-4.298,11.184],[2.579,0],[43.194,0],[-0.438,-1.209]],\"o\":[[27.506,0],[0.43,-1.305],[-22.134,0],[-1.934,0],[4.083,11.277]],\"v\":[[-0.461,137.5],[54.82,117.764],[47.729,113.85],[-46.18,113.85],[-55.42,117.392]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":13,\"st\":0,\"bm\":0}]},{\"id\":\"comp_8\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":0,\"nm\":\"Pre-comp 2\",\"refId\":\"comp_9\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.7,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[30,30,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":10,\"s\":[28.282,29.857,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.7,0.7,0.7],\"y\":[1,1,1]},\"o\":{\"x\":[0.3,0.3,0.3],\"y\":[0,0,0]},\"t\":0,\"s\":[13.6,13.6,100]},{\"t\":10,\"s\":[13.5,13.5,100]}],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":0,\"op\":37,\"st\":0,\"bm\":0}]},{\"id\":\"comp_9\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Head 2\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[0.613,-101.537,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":12,\"s\":[11.613,-50.537,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":26,\"s\":[11.613,-87.537,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":36,\"s\":[11.113,-83.037,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[11.613,-84.537,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,-8.774],[8.774,0],[0,8.774],[-8.774,0]],\"o\":[[0,8.774],[-8.774,0],[0,-8.774],[8.774,0]],\"v\":[[25.5,-84.537],[9.613,-68.65],[-6.274,-84.537],[9.613,-100.424]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[0,-11.72],[13.247,0],[0,11.72],[-13.247,0]],\"o\":[[0,11.72],[-13.247,0],[0,-11.72],[13.247,0]],\"v\":[[33.09,-69.693],[9.103,-48.473],[-14.883,-69.693],[9.103,-90.913]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":14,\"s\":[{\"i\":[[0,-24.735],[24.735,0],[0,24.735],[-24.735,0]],\"o\":[[0,24.735],[-24.735,0],[0,-24.735],[24.735,0]],\"v\":[[56.4,-84.537],[11.613,-39.75],[-33.174,-84.537],[11.613,-129.324]],\"c\":true}]},{\"t\":25,\"s\":[{\"i\":[[0,-26.924],[26.924,0],[0,26.924],[-26.924,0]],\"o\":[[0,26.924],[-26.924,0],[0,-26.924],[26.924,0]],\"v\":[[60.363,-84.537],[11.613,-35.787],[-37.137,-84.537],[11.613,-133.287]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":37,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Hands 2\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0,\"y\":1},\"o\":{\"x\":0.05,\"y\":0},\"t\":9,\"s\":[11.336,13.774,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":12,\"s\":[11.336,18.274,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":25,\"s\":[11.336,11.774,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[11.336,10.774,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":7,\"s\":[{\"i\":[[0,0],[0.875,44.36],[0.012,11.61]],\"o\":[[0,0],[-0.348,-17.658],[-0.016,-14.955]],\"v\":[[-33.583,109.659],[-40.875,50.64],[-45.012,-17.11]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0,0],[-7.48,30.25],[-7.964,9.648]],\"o\":[[0,0],[2.73,-11.373],[7.228,-11.196]],\"v\":[[-57.163,97.688],[-57.996,36.651],[-42.036,0.102]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":10,\"s\":[{\"i\":[[0,0],[-14.611,30.379],[-23.925,1.795]],\"o\":[[0,0],[7.389,-12.121],[13.171,-8.112]],\"v\":[[-77.526,99.758],[-81.889,29.906],[-41.075,4.99]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":11,\"s\":[{\"i\":[[0,0],[-27.718,19.589],[-10.136,-1.028]],\"o\":[[0,0],[21.782,-16.411],[19.114,-5.028]],\"v\":[[-107.888,77.829],[-93.782,16.161],[-42.114,8.877]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":13,\"s\":[{\"i\":[[0,0],[-27.325,4.127],[-14.975,-3.949]],\"o\":[[0,0],[20.334,-3.734],[35.444,0.066]],\"v\":[[-136.225,32.858],[-90.834,7.893],[-38.525,10.472]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":14,\"s\":[{\"i\":[[0,0],[-27.129,-3.603],[-14.269,-4.157]],\"o\":[[0,0],[19.61,2.605],[43.609,2.613]],\"v\":[[-147.393,-5.627],[-89.61,-2.992],[-38.231,9.77]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":18,\"s\":[{\"i\":[[0,0],[-38.825,-22.336],[-19.04,-2.789]],\"o\":[[0,0],[14.675,9.164],[62.76,8.586]],\"v\":[[-144.801,-73.381],[-91.175,-7.197],[-37.96,10.757]],\"c\":false}]},{\"t\":28,\"s\":[{\"i\":[[0,0],[-46.458,-35.583],[-19.738,-3.567]],\"o\":[[0,0],[8.352,6.397],[79.039,14.286]],\"v\":[[-142.333,-130.341],[-88.062,-16.235],[-40.512,7.89]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":7,\"s\":[{\"i\":[[0,0],[0.583,25.777],[4.5,19.11]],\"o\":[[0,0],[-0.238,-10.518],[-18.41,-78.181]],\"v\":[[63,101.64],[63.167,54.973],[56,-16.61]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0,0],[2.455,25.149],[6.868,13.265]],\"o\":[[0,0],[-0.66,-9.745],[-29.265,-65.813]],\"v\":[[68.548,94.42],[70.368,48.723],[56.521,2.29]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":11,\"s\":[{\"i\":[[-11.495,10.995],[15.537,39.692],[11.504,1.535]],\"o\":[[14.034,-13.424],[-3.378,-8.551],[-47.075,-45.52]],\"v\":[[79.466,115.674],[91.463,38.058],[62.496,12.174]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":13,\"s\":[{\"i\":[[-14.54,10.097],[25.244,37.544],[12.466,1.965]],\"o\":[[15.872,-11.198],[-4.741,-7.952],[-56.007,-35.342]],\"v\":[[88.294,117.357],[93.089,33.781],[63.701,13.504]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":14,\"s\":[{\"i\":[[-16.063,9.648],[30.097,36.471],[11.946,0.457]],\"o\":[[16.791,-10.086],[-5.422,-7.653],[-60.473,-30.253]],\"v\":[[92.709,118.199],[93.903,31.642],[61.554,13.67]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":18,\"s\":[{\"i\":[[-5.509,3.309],[41.86,36.38],[11.653,0.923]],\"o\":[[5.759,-3.459],[-7.371,-6.796],[-73.243,-15.702]],\"v\":[[124.602,127.851],[94.405,26.111],[62.332,13.5]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":25,\"s\":[{\"i\":[[0,0],[48,36.333],[11.5,1.167]],\"o\":[[0,0],[-8.388,-6.35],[-79.91,-8.107]],\"v\":[[141.25,132.89],[94.667,23.223],[63.5,11.89]],\"c\":false}]},{\"t\":28,\"s\":[{\"i\":[[0,0],[48,36.333],[11.5,1.167]],\"o\":[[0,0],[-8.388,-6.35],[-79.91,-8.107]],\"v\":[[140,128.89],[94.667,23.848],[63.5,11.89]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":9,\"op\":37,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Body 2\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":12,\"s\":[266.676,398.378,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":23,\"s\":[266.676,387.378,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":34,\"s\":[266.676,392.378,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[10.676,136.378,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.69,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":12,\"s\":[{\"i\":[[-12.288,0.098],[-12.498,-0.987],[0,0],[3.339,-1.222],[4.897,-9.213],[22.369,0],[7.456,13.82],[-1.892,54.808]],\"o\":[[8.847,-0.071],[35.724,2.82],[24.706,0],[0,0],[-7.456,13.82],[-22.369,0],[-4.897,-9.213],[-12.353,-3.008]],\"v\":[[-46.407,7.071],[-18.334,9.585],[61.794,9.679],[70.141,28.952],[68.805,121.176],[13.161,136.5],[-42.484,121.176],[-47.047,25.191]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":23,\"s\":[{\"i\":[[-10.8,-2.3],[-8.2,-0.4],[0,0],[3,-1.3],[4.4,-9.8],[20.1,0],[6.7,14.7],[-1.7,58.3]],\"o\":[[7.9,1.7],[0,0],[22.2,0],[0,0],[-6.7,14.7],[-20.1,0],[-4.4,-9.8],[-11.1,-3.2]],\"v\":[[-40.2,-2.3],[-15.1,1.5],[56.9,1.6],[64.4,22.1],[63.2,120.2],[13.2,136.5],[-36.8,120.2],[-40.9,18.1]],\"c\":true}]},{\"t\":34,\"s\":[{\"i\":[[-10.8,-2.3],[-8.2,-0.4],[0,0],[3,-1.3],[4.4,-9.8],[24.175,0.094],[6.7,14.7],[-1.7,58.3]],\"o\":[[7.9,1.7],[0,0],[22.2,0],[0,0],[-6.669,14.144],[-20.1,-0.078],[-4.4,-9.8],[-11.1,-3.2]],\"v\":[[-40.2,-2.3],[-15.1,1.5],[56.9,1.6],[64.4,22.1],[63.044,119.606],[13.2,136.156],[-36.8,120.2],[-40.9,18.1]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":12,\"op\":37,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"EXAMPLE ON\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[256.462,259.5,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.038,1.5,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-9.1,0],[0,0],[0,-9.1],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[9,0],[0,0],[0,0],[0,0],[0,0],[0,-9.1]],\"v\":[[-1.6,-115.5],[-0.4,-115.5],[16,-99.1],[16,-89.5],[16,-89.5],[-18,-89.5],[-18,-99.1]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-13.1,0],[-2,12],[1.2,0],[20.1,0],[-0.2,-1.3]],\"o\":[[12.8,0],[0.2,-1.4],[-10.3,0],[-0.9,0],[1.9,12.2]],\"v\":[[0,118.5],[25.6,96.7],[22.3,92.5],[-21.4,92.5],[-25.7,96.3]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,1.8],[0,0],[0,0],[36.4,8.6],[3.4,0],[5.9,-0.1],[0,-38.8],[0,0],[1.2,-1.3],[0,0],[0,0],[-11.3,0],[0,0],[8.1,7.9],[0,0]],\"o\":[[0,0],[0,0],[0,-38.6],[-6.7,-0.2],[-3,0],[-36.5,8.6],[0,0],[0,1.8],[0,0],[0,0],[-8,7.9],[0,0],[11.3,0],[0,0],[-1.1,-1.3]],\"v\":[[76.4,35.6],[76.4,-15.7],[76.4,-15.7],[14.3,-93.3],[-0.8,-93.5],[-14.2,-93.3],[-76.3,-15.7],[-76.3,35.6],[-78.1,40.4],[-92.8,57],[-92.8,57],[-81.5,78.5],[81.3,78.5],[92.7,57],[78.1,40.4]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Combined-Shape\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":3,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":1,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"Bell\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[261.217,231.2,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":12,\"s\":[255.967,270.7,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.033,-25.3,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-11.475,14.325],[0,0],[0,0],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-66.9,-60.7],[52.121,57.296],[54.778,59.953],[70.15,79.575],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[-8.362,11.23],[0,0],[0,0],[3.762,-1.859],[8.142,-0.264],[0.62,12.856],[0.156,0.224],[1.314,3.134],[0.224,1.943],[0,0],[-0.582,7.711]],\"o\":[[0,0],[0,0],[0,0],[-19.321,9.547],[-26.964,1.125],[-0.005,-0.156],[-1.406,-14.295],[0.062,-2.373],[0,0],[-0.045,-4.946],[0,-7.124]],\"v\":[[-60.638,-49.681],[29.345,35.402],[55.841,63.676],[71.571,79.252],[-31.443,86.874],[-70.777,71.892],[-71,70.044],[-67.748,45.427],[-67.717,38.704],[-68.176,35.151],[-68.748,-3.168]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[-1.323,3.262],[0,0],[0,0],[6.275,-3.565],[14.005,-0.455],[0.542,0.356],[1.713,1.084],[2.26,5.391],[0.385,2.046],[0,0],[-1.001,13.264]],\"o\":[[0,0],[0,0],[0,0],[-4.624,2.627],[-40.891,-1.743],[0.542,-0.578],[-4.048,-2.564],[-0.758,-3.145],[0,0],[-0.077,-8.507],[0,-3.901]],\"v\":[[-57.609,-43.228],[22.617,23.841],[64.126,70.129],[61.225,80.722],[7.766,92.111],[-48.042,81.338],[-48.213,81.277],[-57.218,71.044],[-58.46,62.937],[-59.249,56.826],[-60.071,5.063]],\"c\":true}]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[-0.309,0.762],[0,0],[0,0],[4.568,-5.804],[17.516,-1.439],[9.846,0.175],[5.202,1.836],[3.057,6.91],[0.521,1.988],[0,0],[-1.354,17]],\"o\":[[0,0],[0,0],[0,0],[-4.183,5.585],[-15.234,1.811],[-9.592,-0.227],[-5.947,-2.247],[-1.448,-3.573],[0,0],[-0.104,-10.903],[0,-0.911]],\"v\":[[-52.53,-28.79],[11.994,25.222],[67.477,74.382],[64.512,86.679],[25.234,99.127],[-10.469,99.989],[-34.862,95.528],[-48.272,82.948],[-50.586,74.249],[-51.653,66.417],[-52.765,18.153]],\"c\":true}]},{\"t\":12,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[4.095,-6.22],[20.07,-1.559],[12.041,0.281],[8.085,2.941],[3.171,6.676],[0.562,1.97],[0,0],[-1.461,18.138]],\"o\":[[0,0],[0,0],[0,0],[-4.095,6.221],[-5.657,0.44],[-12.041,-0.281],[-6.457,-2.348],[-1.741,-3.665],[0,0],[-0.112,-11.633],[0,0]],\"v\":[[-46.983,-24.891],[12.039,25.393],[71.061,75.677],[67.888,87.728],[34.61,102.118],[5.478,102.841],[-27.296,98.494],[-42.796,86.574],[-45.437,77.695],[-46.589,69.338],[-47.789,22.141]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[13.525,-18.6],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[10.58,-1.555]],\"v\":[[88.55,58.6],[73.795,40.25],[71.995,35.45],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-68.65,-58.275],[-25.568,-8.753],[-24.761,-7.935],[67.699,79.699],[80.6,78.975]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[0.157,8.024],[0,0],[0,1.922],[0,0],[24.177,-0.77],[0,0],[9,0],[0,0],[5.709,0.272],[0,0],[-0.26,-17.504],[0,0],[0,0],[0,0],[-7.459,5.266]],\"o\":[[0,0],[-0.323,-1.991],[0,0],[1.096,-24.22],[0,0],[-9.573,2.118],[0,0],[-9.1,0],[0,0],[-6.6,1.142],[0,0],[0,0],[0,0],[0,0],[3.993,-2.819]],\"v\":[[71.843,56.59],[71.106,36.657],[70.583,30.009],[69.845,-21.987],[38.823,-52.309],[28.073,-50.661],[3.062,-46.678],[1.048,-46.86],[-26.209,-52.017],[-31.319,-53.002],[-58.24,-38.393],[-25.737,-8.451],[-22.131,-4.064],[59.51,80.628],[65.709,78.296]],\"c\":true}]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0.094,4.576],[0,0],[0,1.828],[0,0],[20.355,-2.421],[0,0],[9,0],[0,0],[6.507,0.351],[0,0],[2.835,-13.717],[0,0],[0,0],[0,0],[-1.742,1.23]],\"o\":[[0,0],[-0.014,-2.094],[0,0],[-0.871,-19.045],[0,0],[-6.547,0.878],[0,0],[-9.1,0],[0,0],[-3.266,-0.12],[0,0],[0,0],[0,0],[0,0],[1.239,-8.208]],\"v\":[[69.031,57.556],[68.76,39.315],[68.408,33.605],[67.871,-20.767],[25.645,-36.641],[23.135,-36.246],[-0.875,-35.181],[-1.998,-35.415],[-22.891,-36.675],[-31.934,-38.285],[-52.647,-28.157],[-24.554,-1.976],[-21.081,1.4],[63.583,76.467],[67.733,76.517]],\"c\":true}]},{\"t\":12,\"s\":[{\"i\":[[0.075,3.525],[0,0],[0,1.8],[0,0],[24.75,0.638],[0,0],[9,0],[0,0],[6.75,0.375],[0,0],[-2.6,-5.6],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0.08,-2.125],[0,0],[1.85,-14.4],[0,0],[-5.625,0.5],[0,0],[-9.1,0],[0,0],[-2.25,-0.505],[0,0],[0,0],[0,0],[0,0],[0.4,-9.85]],\"v\":[[70.925,57.85],[70.795,40.125],[70.495,34.7],[71.4,-15.1],[22.75,-23.388],[22.75,-23.375],[0.35,-23.525],[0.15,-23.775],[-17.5,-24.5],[-23.5,-24.995],[-47.4,-25.4],[-21.443,-0.003],[-18.011,3.065],[67.387,75.199],[70.912,75.975]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":1,\"op\":13,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"Bottom Bell\",\"parent\":5,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[0,137.7,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":12,\"s\":[12,91.7,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,137.7,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-13.1,0],[-2,12],[1.2,0],[20.1,0],[-0.2,-1.3]],\"o\":[[12.8,0],[0.2,-1.4],[-10.3,0],[-0.9,0],[1.9,12.1]],\"v\":[[-5.125,120.225],[20.6,99.05],[17.3,94.85],[-26.4,94.85],[-30.7,98.65]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[-19.183,0.659],[-2.93,11.67],[1.758,0],[29.45,0],[-0.296,-1.263]],\"o\":[[23.128,-0.795],[2.186,-3.703],[-15.091,0],[-1.319,0],[2.784,11.767]],\"v\":[[-2.487,124.469],[35.205,101.877],[29.62,99.793],[-34.409,99.793],[-40.709,103.488]],\"c\":true}]},{\"t\":12,\"s\":[{\"i\":[[-28.151,0],[-4.298,11.184],[2.579,0],[43.194,0],[-0.438,-1.209]],\"o\":[[27.506,0],[0.43,-1.305],[-22.134,0],[-1.934,0],[4.083,11.277]],\"v\":[[-0.461,137.5],[54.82,117.764],[47.729,113.85],[-46.18,113.85],[-55.42,117.392]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":1,\"op\":10,\"st\":0,\"bm\":0}]},{\"id\":\"comp_10\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":0,\"nm\":\"Pre-comp 1\",\"refId\":\"comp_11\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[13.6,13.6,100],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":0,\"op\":35,\"st\":0,\"bm\":0}]},{\"id\":\"comp_11\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Line\",\"parent\":2,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.67,\"y\":1},\"o\":{\"x\":0.33,\"y\":0},\"t\":6,\"s\":[1.687,-2.898,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":14,\"s\":[3.687,-6.898,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[3.6,-7,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.4,\"y\":0},\"t\":4,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-101.6,-111.5],[108.8,97.5]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":14,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-109.713,-119.559],[116.913,105.559]],\"c\":false}]},{\"t\":27,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-101.6,-111.5],[108.8,97.5]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.2],\"y\":[0]},\"t\":6,\"s\":[28]},{\"t\":14,\"s\":[0]}],\"ix\":1},\"e\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.2],\"y\":[0]},\"t\":6,\"s\":[71]},{\"t\":14,\"s\":[100]}],\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path-10\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":6,\"op\":35,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Head\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-3.6,-33.55,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.6,-33.55,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-25.4,0],[0,-25.4],[0,0],[6.9,-8],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.6,-117.5],[42.4,-71.5],[42.4,4.5],[31.3,34.4],[-20.33,-17.23],[-49.6,-46.5],[-49.6,-71.5]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.9,0],[0,25.4],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-5.2,2],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.6,-15.4],[-40.702,-6.502],[-38.75,-4.55],[13.1,47.3],[-3.6,50.4],[-49.6,4.4],[-49.6,-15.4]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":13,\"op\":35,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Arc L\",\"parent\":5,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":14,\"s\":[-3.75,69.094,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":26,\"s\":[-3.75,88.094,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":34,\"s\":[-3.75,84.5,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.75,84.5,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.29,\"y\":0},\"t\":18,\"s\":[{\"i\":[[0,0],[13.1,0],[0,42]],\"o\":[[-11,5.1],[-45.3,0],[0,0]],\"v\":[[33,76.5],[-3.5,84.5],[-85.5,6.5]],\"c\":false}]},{\"t\":28,\"s\":[{\"i\":[[0,0],[13.1,0],[0,42]],\"o\":[[-11,5.1],[-45.3,0],[0,0]],\"v\":[[33,76.5],[-3.5,84.5],[-85.5,6.5]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.723],\"y\":[0.572]},\"o\":{\"x\":[0.4],\"y\":[0]},\"t\":5,\"s\":[15]},{\"i\":{\"x\":[0.695],\"y\":[0.627]},\"o\":{\"x\":[0.368],\"y\":[0.45]},\"t\":7,\"s\":[29]},{\"i\":{\"x\":[0.323],\"y\":[1]},\"o\":{\"x\":[0.222],\"y\":[0.388]},\"t\":8,\"s\":[37.868]},{\"t\":18,\"s\":[100]}],\"ix\":1},\"e\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.723],\"y\":[1.14]},\"o\":{\"x\":[0.4],\"y\":[0]},\"t\":5,\"s\":[12]},{\"i\":{\"x\":[0.695],\"y\":[0.945]},\"o\":{\"x\":[0.368],\"y\":[0.061]},\"t\":7,\"s\":[17.559]},{\"i\":{\"x\":[0.652],\"y\":[0.857]},\"o\":{\"x\":[0.357],\"y\":[0.122]},\"t\":8,\"s\":[9]},{\"i\":{\"x\":[0.426],\"y\":[1]},\"o\":{\"x\":[0.183],\"y\":[-2.717]},\"t\":10,\"s\":[0]},{\"t\":18,\"s\":[0]}],\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":7,\"op\":35,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Arc R\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.29,\"y\":0},\"t\":18,\"s\":[{\"i\":[[0,0],[8.7,-12]],\"o\":[[0,15.6],[0,0]],\"v\":[[78.4,8.4],[64.7,50.5]],\"c\":false}]},{\"t\":28,\"s\":[{\"i\":[[0,0],[8.7,-12]],\"o\":[[0,15.6],[0,0]],\"v\":[[78.4,8.4],[64.7,50.5]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.4],\"y\":[0]},\"t\":8,\"s\":[99]},{\"t\":18,\"s\":[0]}],\"ix\":1},\"e\":{\"a\":0,\"k\":100,\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":11,\"op\":35,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"Leg\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[252.45,380.6,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.55,124.6,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":14,\"s\":[{\"i\":[[-6.3,0],[0,0],[0,-6.3],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-6.3]],\"v\":[[-5.15,64],[-1.85,64],[9.55,75.4],[9.5,113.2],[-1.9,124.6],[-5.2,124.6],[-16.6,113.2],[-16.55,75.4]],\"c\":true}]},{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":26,\"s\":[{\"i\":[[-6.3,0],[0,0],[0,-2.846],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-2.846]],\"v\":[[-5.2,84.75],[-1.9,84.75],[9.5,89.9],[9.5,113.2],[-1.9,124.6],[-5.2,124.6],[-16.6,113.2],[-16.6,89.9]],\"c\":true}]},{\"t\":34,\"s\":[{\"i\":[[-6.3,0],[0,0],[0,-6.3],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-6.3]],\"v\":[[-5.2,78.5],[-1.9,78.5],[9.5,89.9],[9.5,113.2],[-1.9,124.6],[-5.2,124.6],[-16.6,113.2],[-16.6,89.9]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":10,\"op\":35,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"Bell\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[259.064,256.028,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":14,\"s\":[253.564,241.028,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-1.88,-0.927,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-16.045,13.245],[0,0],[0,0],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-61.4,-66.7],[44.371,41.071],[47.028,43.728],[73.9,79.64],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[-4.254,9.376],[0,0],[0,0],[0,0],[6.23,1.972],[-1.412,7.008],[0,0],[0,0],[0,1.507],[0.298,2.165],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0],[-9.876,-1.385],[0,0],[0,0],[0.238,-1.694],[0,0],[-0.85,-6.164],[0.044,-6.601]],\"v\":[[-57.532,-45.068],[21.114,38.993],[23.375,41.218],[43.195,62.771],[-50.105,63.383],[-67.187,44.549],[-67.717,44.298],[-64.273,32.174],[-64.047,26.764],[-64.187,23.769],[-63.385,-11.302]],\"c\":true}]},{\"t\":14,\"s\":[{\"i\":[[-0.557,1.753],[0,0],[0,0],[0,0],[12.242,3.875],[4.463,6.051],[0,0],[0,0],[0,1.225],[0.586,4.254],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0],[-8.598,-2.722],[0,0],[0,0],[-0.69,-2.075],[0,0],[-1.67,-12.113],[0.086,-1.777]],\"v\":[[-48.888,-16.333],[2.602,35.024],[4.48,36.832],[14.055,46.983],[-15.561,47.733],[-39.407,31.182],[-39.266,30.978],[-44.13,22.682],[-46.406,17.666],[-48.156,11.291],[-49.281,-8.366]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[12.143,-14.492],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[10.58,-1.555]],\"v\":[[88.55,58.6],[73.795,40.25],[71.995,35.45],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-63.338,-64.963],[-22.443,-19.878],[-21.636,-19.06],[71.262,79.637],[80.6,78.975]],\"c\":true}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[5.809,9.358],[-0.56,1.677],[0,1.8],[0,0],[30.628,11.768],[0,0],[10.446,-0.187],[0,0],[1.845,-5.655],[0,0],[2.161,-13.67],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-0.175,-1.881],[0,0],[0.112,-46.447],[0,0],[-2.26,-5.718],[0,0],[-11.578,-0.155],[0,0],[-27.545,10.149],[0,0],[0,0],[0,0],[0,0],[6.72,-2.318]],\"v\":[[62.53,35.324],[56.968,24.754],[57.164,18.109],[57.299,-17.272],[16.524,-101.941],[16.174,-105.284],[-4.079,-116.658],[-4.828,-116.69],[-26.225,-104.298],[-27.116,-100.217],[-56.572,-52.898],[-23.746,-18.806],[-23.05,-17.988],[50.468,61.936],[58.016,59.693]],\"c\":true}]},{\"t\":14,\"s\":[{\"i\":[[-2.182,4.07],[-1.1,3.295],[0,1.8],[0,0],[25.933,14.308],[0,0],[11.842,-0.367],[0,0],[3.626,-2.33],[0,0],[-2.875,-21.055],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0.815,-2.442],[0,0],[0.221,-54.605],[0,0],[-4.442,-2.455],[0,0],[-13.97,-0.305],[0,0],[-19.874,13.29],[0,0],[0,0],[0,0],[0,0],[2.995,-3.055]],\"v\":[[36.363,27.6],[40.655,19.625],[42.779,11.2],[43.21,-19.85],[21.372,-111.763],[20.684,-112.312],[-3.286,-118.838],[-3.6,-118.9],[-29.821,-110.375],[-31.571,-108.995],[-48.57,-47.15],[-22.056,-18.753],[-21.467,-17.935],[30.402,32.574],[31.31,33.225]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":13,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":7,\"ty\":4,\"nm\":\"Bottom Bell\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[255.445,394.655,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,137.7,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.4,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-13.1,0],[-2,12],[1.2,0],[20.1,0],[-0.2,-1.3]],\"o\":[[12.8,0],[0.2,-1.4],[-10.3,0],[-0.9,0],[1.9,12.1]],\"v\":[[1,119.605],[26.6,97.805],[23.3,93.605],[-20.4,93.605],[-24.7,97.405]],\"c\":true}]},{\"t\":10,\"s\":[{\"i\":[[-8.162,0],[0.625,15.53],[0.51,0.546],[4.907,-5.5],[0.14,-2.002]],\"o\":[[7.975,0],[0.125,-2.157],[-6.069,-6.5],[-0.373,0.418],[-1.164,16.646]],\"v\":[[-3.269,123.85],[10.181,105.515],[9.694,64.545],[-15.782,65.045],[-16.531,104.149]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":10,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":8,\"ty\":4,\"nm\":\"EXAMPLE ON\",\"parent\":6,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-4.483,2.545,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.038,1.5,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-9.1,0],[0,0],[0,-9.1],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[9,0],[0,0],[0,0],[0,0],[0,0],[0,-9.1]],\"v\":[[-1.6,-115.5],[-0.4,-115.5],[16,-99.1],[16,-89.5],[16,-89.5],[-18,-89.5],[-18,-99.1]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-13.1,0],[-2,12],[1.2,0],[20.1,0],[-0.2,-1.3]],\"o\":[[12.8,0],[0.2,-1.4],[-10.3,0],[-0.9,0],[1.9,12.2]],\"v\":[[0,118.5],[25.6,96.7],[22.3,92.5],[-21.4,92.5],[-25.7,96.3]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,1.8],[0,0],[0,0],[36.4,8.6],[3.4,0],[5.9,-0.1],[0,-38.8],[0,0],[1.2,-1.3],[0,0],[0,0],[-11.3,0],[0,0],[8.1,7.9],[0,0]],\"o\":[[0,0],[0,0],[0,-38.6],[-6.7,-0.2],[-3,0],[-36.5,8.6],[0,0],[0,1.8],[0,0],[0,0],[-8,7.9],[0,0],[11.3,0],[0,0],[-1.1,-1.3]],\"v\":[[76.4,35.6],[76.4,-15.7],[76.4,-15.7],[14.3,-93.3],[-0.8,-93.5],[-14.2,-93.3],[-76.3,-15.7],[-76.3,35.6],[-78.1,40.4],[-92.8,57],[-92.8,57],[-81.5,78.5],[81.3,78.5],[92.7,57],[78.1,40.4]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Combined-Shape\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":3,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":2,\"st\":0,\"bm\":0}]},{\"id\":\"comp_12\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":0,\"nm\":\"Pre-comp 1\",\"refId\":\"comp_13\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[13.6,13.6,100],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":0,\"op\":29,\"st\":0,\"bm\":0}]},{\"id\":\"comp_13\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"EXAMPLE ON\",\"parent\":4,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-3.983,2.545,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.038,1.5,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-9.1,0],[0,0],[0,-9.1],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[9,0],[0,0],[0,0],[0,0],[0,0],[0,-9.1]],\"v\":[[-1.6,-115.5],[-0.4,-115.5],[16,-99.1],[16,-89.5],[16,-89.5],[-18,-89.5],[-18,-99.1]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-13.1,0],[-2,12],[1.2,0],[20.1,0],[-0.2,-1.3]],\"o\":[[12.8,0],[0.2,-1.4],[-10.3,0],[-0.9,0],[1.9,12.2]],\"v\":[[0,118.5],[25.6,96.7],[22.3,92.5],[-21.4,92.5],[-25.7,96.3]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,1.8],[0,0],[0,0],[36.4,8.6],[3.4,0],[5.9,-0.1],[0,-38.8],[0,0],[1.2,-1.3],[0,0],[0,0],[-11.3,0],[0,0],[8.1,7.9],[0,0]],\"o\":[[0,0],[0,0],[0,-38.6],[-6.7,-0.2],[-3,0],[-36.5,8.6],[0,0],[0,1.8],[0,0],[0,0],[-8,7.9],[0,0],[11.3,0],[0,0],[-1.1,-1.3]],\"v\":[[76.4,35.6],[76.4,-15.7],[76.4,-15.7],[14.3,-93.3],[-0.8,-93.5],[-14.2,-93.3],[-76.3,-15.7],[-76.3,35.6],[-78.1,40.4],[-92.8,57],[-92.8,57],[-81.5,78.5],[81.3,78.5],[92.7,57],[78.1,40.4]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Combined-Shape\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":3,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":3,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Line Bell\",\"parent\":4,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.4,\"y\":0},\"t\":0,\"s\":[-11.47,-25.859,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":17,\"s\":[5.03,-9.859,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.05,-2.95,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-98.152,-101.129],[98.052,95.229]],\"c\":false},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":0,\"k\":0,\"ix\":1},\"e\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.4],\"y\":[0]},\"t\":0,\"s\":[0]},{\"t\":17,\"s\":[100]}],\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":29,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Bottom Bell\",\"parent\":4,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[0,137.7,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,137.7,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-13.1,0],[-2,12],[1.2,0],[20.1,0],[-0.2,-1.3]],\"o\":[[12.8,0],[0.2,-1.4],[-10.3,0],[-0.9,0],[1.9,12.1]],\"v\":[[-4.5,119.6],[21.1,97.8],[17.8,93.6],[-25.9,93.6],[-30.2,97.4]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":2,\"op\":29,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Bell\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[260.562,232.132,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0.12,-24.927,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.72,0.72,0.72],\"y\":[1,1,1]},\"o\":{\"x\":[0.28,0.28,0.28],\"y\":[0,0,0]},\"t\":1,\"s\":[100,100,100]},{\"i\":{\"x\":[0.72,0.72,0.72],\"y\":[1,1,1]},\"o\":{\"x\":[0.28,0.28,0.28],\"y\":[0,0,0]},\"t\":12,\"s\":[93,93,100]},{\"i\":{\"x\":[0.72,0.72,0.72],\"y\":[1,1,1]},\"o\":{\"x\":[0.28,0.28,0.28],\"y\":[0,0,0]},\"t\":21,\"s\":[101,101,100]},{\"t\":28,\"s\":[100,100,100]}],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[-7.545,13.495],[0,0],[0,0],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-68.9,-57.2],[46.687,58.223],[49.57,61.102],[68.15,79.655],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":5,\"s\":[{\"i\":[[-5.872,11.598],[-1.72,-1.503],[-1.818,10.555],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[9.034,7.892],[6.962,6.261],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-73.235,-48.204],[-65.308,-41.244],[-37.924,-49.058],[74.965,79.428],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[-5.872,11.598],[-1.681,-1.546],[-1.818,10.555],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[11.181,10.285],[6.962,6.261],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-73.235,-48.204],[-35.586,-11.586],[-5.934,-19.545],[74.965,79.428],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[-5.036,10.649],[-2.391,-2.446],[-2.124,14.747],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[6.637,6.293],[10.443,9.392],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-73.817,-47.452],[-6.583,20.071],[22.942,7.689],[78.372,79.315],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":8,\"s\":[{\"i\":[[-4.2,9.7],[-3.062,-3.39],[-2.43,18.938],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[4.241,4.695],[13.924,12.522],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-74.4,-46.7],[19.467,48.272],[48.098,35.753],[81.779,79.202],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[-4.2,9.7],[-2.2,-2.106],[-1.62,12.626],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[9.752,9.336],[9.282,8.348],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-74.4,-46.7],[34.992,63.228],[69.333,53.948],[81.641,79.138],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]},{\"t\":10,\"s\":[{\"i\":[[-4.2,9.7],[0,0],[0,0],[0,0],[0,0],[-7.5,8],[0,0],[0,0],[0,1.8],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0],[-11.2,0],[0,0],[0,0],[1.2,-1.3],[0,0],[0,0],[0,-11.6]],\"v\":[[-74.4,-46.7],[32.121,59.821],[34.778,62.478],[51.9,79.6],[-85.9,79.6],[-97.5,58.4],[-97.2,58.1],[-82.6,41.5],[-80.8,36.7],[-80.8,36.7],[-80.8,-14.6]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[9.346,-16.6],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[8,-1.6]],\"v\":[[89.05,58.35],[73.795,39.75],[71.995,34.95],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-69.215,-56.816],[-28.207,-15.716],[-27.434,-14.941],[66.835,79.54],[81.6,79.1]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":5,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[2.875,-7.688],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[8,-1.6]],\"v\":[[89.05,58.35],[73.795,39.75],[71.995,34.95],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-55.392,-66.381],[-37.754,-49.074],[-39.423,-40.52],[73.962,79.404],[81.6,79.1]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[2.875,-7.688],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[8,-1.6]],\"v\":[[89.05,58.35],[73.795,39.75],[71.995,34.95],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-55.392,-66.381],[-4.288,-18.099],[-3.284,-9.024],[73.962,79.404],[81.6,79.1]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[2.047,-7.688],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[8,-1.6]],\"v\":[[89.05,58.35],[73.795,39.75],[71.995,34.95],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-56.464,-65.251],[-10.415,-18.43],[4.841,-14.706],[77.525,79.337],[81.6,79.1]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":8,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[1.218,-7.689],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[8,-1.6]],\"v\":[[89.05,58.35],[73.795,39.75],[71.995,34.95],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-57.536,-64.121],[-15.752,-20.901],[36.667,21.906],[81.088,79.269],[81.6,79.1]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[4.182,-9.084],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[8.86,-1.585]],\"v\":[[88.883,58.433],[73.795,39.917],[71.995,35.117],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-59.157,-64.714],[-11.573,-25.425],[-12.451,-25.128],[71.59,57.475],[81.267,79.058]],\"c\":true}]},{\"t\":10,\"s\":[{\"i\":[[6.6,6.5],[0,0],[0,1.8],[0,0],[35.493,9.136],[0,0],[9,0],[0,0],[0,-9.1],[0,0],[10.111,-11.873],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[-1.2,-1.3],[0,0],[0,-37.993],[0,0],[0,-9.1],[0,0],[-9.1,0],[0,0],[-16.061,4.493],[0,0],[0,0],[0,0],[0,0],[10.58,-1.555]],\"v\":[[88.55,58.6],[73.795,40.25],[71.995,35.45],[71.9,-14.6],[11.5,-91.763],[11.5,-98],[-4.9,-114.4],[-6.1,-114.4],[-22.5,-98],[-22.5,-91.12],[-62.4,-65.9],[-19.568,-22.503],[-18.761,-21.685],[79.699,78.074],[80.6,78.975]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":2,\"op\":29,\"st\":0,\"bm\":0}]},{\"id\":\"comp_14\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":0,\"nm\":\"Pre-comp 2\",\"refId\":\"comp_15\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.7,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[30,30,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":10,\"s\":[28.282,29.857,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.7,0.7,0.7],\"y\":[1,1,1]},\"o\":{\"x\":[0.3,0.3,0.3],\"y\":[0,0,0]},\"t\":0,\"s\":[13.6,13.6,100]},{\"t\":10,\"s\":[13.5,13.5,100]}],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":0,\"op\":37,\"st\":0,\"bm\":0}]},{\"id\":\"comp_15\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Head 2\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[0.613,-101.537,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":12,\"s\":[11.613,-50.537,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":26,\"s\":[11.613,-86.537,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":36,\"s\":[11.113,-83.037,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[11.613,-84.537,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":3,\"s\":[{\"i\":[[0,-17.328],[25.612,0],[0,17.328],[-25.612,0]],\"o\":[[0,17.328],[-25.612,0],[0,-17.328],[25.612,0]],\"v\":[[54.039,-62.942],[7.664,-31.567],[-38.711,-62.942],[7.664,-94.317]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[0,-23.543],[25.081,0],[0,23.543],[-25.081,0]],\"o\":[[0,23.543],[-25.081,0],[0,-23.543],[25.081,0]],\"v\":[[55.47,-76.028],[10.057,-33.4],[-35.356,-76.028],[10.057,-118.656]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":14,\"s\":[{\"i\":[[0,-24.735],[24.735,0],[0,24.735],[-24.735,0]],\"o\":[[0,24.735],[-24.735,0],[0,-24.735],[24.735,0]],\"v\":[[56.4,-84.537],[11.613,-39.75],[-33.174,-84.537],[11.613,-129.324]],\"c\":true}]},{\"t\":25,\"s\":[{\"i\":[[0,-26.924],[26.924,0],[0,26.924],[-26.924,0]],\"o\":[[0,26.924],[-26.924,0],[0,-26.924],[26.924,0]],\"v\":[[60.363,-84.537],[11.613,-35.787],[-37.137,-84.537],[11.613,-133.287]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":2,\"op\":37,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Hands 2\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0,\"y\":1},\"o\":{\"x\":0.05,\"y\":0},\"t\":9,\"s\":[11.336,13.774,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":12,\"s\":[11.336,18.274,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":25,\"s\":[11.336,11.774,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[11.336,10.774,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":7,\"s\":[{\"i\":[[0,0],[0.875,44.36],[0.012,11.61]],\"o\":[[0,0],[-0.348,-17.658],[-0.016,-14.955]],\"v\":[[-33.583,109.659],[-40.875,50.64],[-45.012,-17.11]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0,0],[-7.48,30.25],[-7.964,9.648]],\"o\":[[0,0],[2.73,-11.373],[7.228,-11.196]],\"v\":[[-57.163,97.688],[-57.996,36.651],[-42.036,0.102]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":10,\"s\":[{\"i\":[[0,0],[-14.611,30.379],[-23.925,1.795]],\"o\":[[0,0],[7.389,-12.121],[13.171,-8.112]],\"v\":[[-77.526,99.758],[-81.889,29.906],[-41.075,4.99]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":11,\"s\":[{\"i\":[[0,0],[-27.718,19.589],[-10.136,-1.028]],\"o\":[[0,0],[21.782,-16.411],[19.114,-5.028]],\"v\":[[-107.888,77.829],[-93.782,16.161],[-42.114,8.877]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":13,\"s\":[{\"i\":[[0,0],[-27.325,4.127],[-14.975,-3.949]],\"o\":[[0,0],[20.334,-3.734],[35.444,0.066]],\"v\":[[-136.225,32.858],[-90.834,7.893],[-38.525,10.472]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":14,\"s\":[{\"i\":[[0,0],[-27.129,-3.603],[-14.269,-4.157]],\"o\":[[0,0],[19.61,2.605],[43.609,2.613]],\"v\":[[-147.393,-5.627],[-89.61,-2.992],[-38.231,9.77]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":18,\"s\":[{\"i\":[[0,0],[-38.825,-22.336],[-19.04,-2.789]],\"o\":[[0,0],[14.675,9.164],[62.76,8.586]],\"v\":[[-144.801,-73.381],[-91.175,-7.197],[-37.96,10.757]],\"c\":false}]},{\"t\":28,\"s\":[{\"i\":[[0,0],[-46.458,-35.583],[-19.738,-3.567]],\"o\":[[0,0],[8.352,6.397],[79.039,14.286]],\"v\":[[-142.333,-130.341],[-88.062,-16.235],[-40.512,7.89]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":7,\"s\":[{\"i\":[[0,0],[0.583,25.777],[4.5,19.11]],\"o\":[[0,0],[-0.238,-10.518],[-18.41,-78.181]],\"v\":[[63,101.64],[63.167,54.973],[56,-16.61]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0,0],[2.455,25.149],[6.868,13.265]],\"o\":[[0,0],[-0.66,-9.745],[-29.265,-65.813]],\"v\":[[68.548,94.42],[70.368,48.723],[56.521,2.29]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":11,\"s\":[{\"i\":[[-11.495,10.995],[15.537,39.692],[11.504,1.535]],\"o\":[[14.034,-13.424],[-3.378,-8.551],[-47.075,-45.52]],\"v\":[[79.466,115.674],[91.463,38.058],[62.496,12.174]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":13,\"s\":[{\"i\":[[-14.54,10.097],[25.244,37.544],[12.466,1.965]],\"o\":[[15.872,-11.198],[-4.741,-7.952],[-56.007,-35.342]],\"v\":[[88.294,117.357],[93.089,33.781],[63.701,13.504]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":14,\"s\":[{\"i\":[[-16.063,9.648],[30.097,36.471],[11.946,0.457]],\"o\":[[16.791,-10.086],[-5.422,-7.653],[-60.473,-30.253]],\"v\":[[92.709,118.199],[93.903,31.642],[61.554,13.67]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":18,\"s\":[{\"i\":[[-5.509,3.309],[41.86,36.38],[11.653,0.923]],\"o\":[[5.759,-3.459],[-7.371,-6.796],[-73.243,-15.702]],\"v\":[[124.602,127.851],[94.405,26.111],[62.332,13.5]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":25,\"s\":[{\"i\":[[0,0],[48,36.333],[11.5,1.167]],\"o\":[[0,0],[-8.388,-6.35],[-79.91,-8.107]],\"v\":[[141.25,132.89],[94.667,23.223],[63.5,11.89]],\"c\":false}]},{\"t\":28,\"s\":[{\"i\":[[0,0],[48,36.333],[11.5,1.167]],\"o\":[[0,0],[-8.388,-6.35],[-79.91,-8.107]],\"v\":[[140,128.89],[94.667,23.848],[63.5,11.89]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":12,\"op\":37,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Body 2\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":12,\"s\":[266.676,398.378,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":23,\"s\":[266.676,387.378,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":34,\"s\":[266.676,392.378,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[10.676,136.378,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.69,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":12,\"s\":[{\"i\":[[-12.288,0.098],[-12.498,-0.987],[0,0],[3.339,-1.222],[4.897,-9.213],[22.369,0],[7.456,13.82],[-1.892,54.808]],\"o\":[[8.847,-0.071],[35.724,2.82],[24.706,0],[0,0],[-7.456,13.82],[-22.369,0],[-4.897,-9.213],[-12.353,-3.008]],\"v\":[[-46.407,7.071],[-18.334,9.585],[61.794,9.679],[70.141,28.952],[68.805,121.176],[13.161,136.5],[-42.484,121.176],[-47.047,25.191]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":23,\"s\":[{\"i\":[[-10.8,-2.3],[-8.2,-0.4],[0,0],[3,-1.3],[4.4,-9.8],[20.1,0],[6.7,14.7],[-1.7,58.3]],\"o\":[[7.9,1.7],[0,0],[22.2,0],[0,0],[-6.7,14.7],[-20.1,0],[-4.4,-9.8],[-11.1,-3.2]],\"v\":[[-40.2,-2.3],[-15.1,1.5],[56.9,1.6],[64.4,22.1],[63.2,120.2],[13.2,136.5],[-36.8,120.2],[-40.9,18.1]],\"c\":true}]},{\"t\":34,\"s\":[{\"i\":[[-10.8,-2.3],[-8.2,-0.4],[0,0],[3,-1.3],[4.4,-9.8],[24.175,0.094],[6.7,14.7],[-1.7,58.3]],\"o\":[[7.9,1.7],[0,0],[22.2,0],[0,0],[-6.669,14.144],[-20.1,-0.078],[-4.4,-9.8],[-11.1,-3.2]],\"v\":[[-40.2,-2.3],[-15.1,1.5],[56.9,1.6],[64.4,22.1],[63.044,119.606],[13.2,136.156],[-36.8,120.2],[-40.9,18.1]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":12,\"op\":37,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"EXAMPLE ON\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[252.75,259.55,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,3.55,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[45.3,0],[0,42]],\"o\":[[0,42],[-45.3,0],[0,0]],\"v\":[[82,8.4],[0,84.5],[-82,6.5]],\"c\":false},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-25.4,0],[0,0],[0,-25.4],[0,0],[25.4,0],[0,0],[0,25.4],[0,0]],\"o\":[[0,0],[25.4,0],[0,0],[0,25.4],[0,0],[-25.4,0],[0,0],[0,-25.4]],\"v\":[[0,-117.5],[0,-117.5],[46,-71.5],[46,4.5],[0,50.5],[0,50.5],[-46,4.5],[-46,-71.5]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-6.3,0],[0,0],[0,-6.3],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-6.3]],\"v\":[[-1.6,78.5],[1.7,78.5],[13.1,89.9],[13.1,113.2],[1.7,124.6],[-1.6,124.6],[-13,113.2],[-13,89.9]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":3,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":1,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"Head Mic\",\"parent\":6,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[-3.6,-33.55,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":12,\"s\":[9.4,60.45,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.6,-33.55,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[11.45,-8.15],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.6,-117.5],[42.4,-71.5],[42.4,4.5],[25.8,40.525],[-28.33,-5.73],[-49.7,-35.75],[-49.6,-71.5]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[-25.393,-0.391],[0.18,-19.791],[-0.243,-7.895],[14.056,-6.795],[0,0],[0,0],[0,0]],\"o\":[[28.416,0.506],[0,0],[0.243,14.68],[0,0],[0,0],[0,0],[0.052,-15.482]],\"v\":[[-6.434,-101.102],[47.056,-73.018],[47.664,4.298],[29.68,40.735],[-22.557,-15.51],[-55.36,-50.751],[-55.066,-73.828]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[-25.388,-0.636],[-1.714,-14.258],[-0.395,-12.85],[18.974,-7.989],[0,0],[0,0],[0,0]],\"o\":[[30.309,0.824],[0,0],[0.395,16.737],[0,0],[0,0],[0,0],[2.421,-11.434]],\"v\":[[-4.713,-90.812],[49.978,-73.971],[50.966,4.171],[29.789,44.635],[-23.955,-14.43],[-58.974,-53.419],[-58.496,-75.289]],\"c\":true}]},{\"t\":12,\"s\":[{\"i\":[[-25.357,-0.966],[8.591,-7],[-0.599,-19.5],[30.67,-1.65],[0,0],[0,0],[0,0]],\"o\":[[32.818,1.25],[0,0],[0.599,19.5],[0,0],[0,0],[0,0],[7.976,-0.75]],\"v\":[[-7.528,-77],[56.909,-75.25],[58.407,4],[30.335,46.65],[-22.743,-12.98],[-60.7,-57],[-59.976,-81.5]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.9,0],[0,25.4],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-11.6,9.325],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.7,-36.15],[-27.202,-14.502],[-25.25,-12.55],[26.975,39.425],[-3.6,50.4],[-49.6,4.4],[-49.7,-31.65]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[6.435,0.16],[0,28.821],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-13.044,10.194],[-29.138,-0.769],[0,0],[0,0]],\"v\":[[-55.579,-50.718],[-28.188,-26.309],[-26.057,-24.356],[32.281,39.246],[-10.39,49.691],[-55.329,4.4],[-55.38,-31.494]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[6.77,0.261],[0,30.968],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-13.708,7.528],[-31.483,-1.252],[0,0],[0,0]],\"v\":[[-59.067,-54.691],[-31.67,-27.138],[-29.427,-25.186],[31.261,43.864],[-14.65,49.247],[-58.924,4.4],[-59.007,-41.593]],\"c\":true}]},{\"t\":12,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[7.214,0.396],[0,33.85],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-14.586,3.95],[-34.597,-1.9],[0,0],[0,0]],\"v\":[[-60.625,-60.025],[-33.246,-28.252],[-30.856,-26.3],[32.633,46.55],[-17.287,48.65],[-60.625,4.4],[-60.75,-55.15]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":1,\"op\":13,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"Arc L\",\"parent\":8,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-3.75,84.5,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.75,84.5,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[13.1,0],[0,42]],\"o\":[[-11,5.1],[-45.3,0],[0,0]],\"v\":[[33,76.5],[-3.5,84.5],[-85.5,6.5]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":2,\"s\":[{\"i\":[[0,0],[17.605,8.684],[-3.033,28.67]],\"o\":[[-19.95,7.814],[-28.895,-17.316],[0,0]],\"v\":[[24.45,79.436],[-39.105,77.316],[-76.362,0.644]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":3,\"s\":[{\"i\":[[0,0],[11.789,14.347],[-5.217,19.073]],\"o\":[[-50.375,15.159],[-15.78,-19.203],[0,0]],\"v\":[[26.125,87.591],[-58.72,68.703],[-69.783,-3.573]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[0,0],[3.96,23.737],[-17.216,17.641]],\"o\":[[-21.241,-5.148],[-3.54,-27.263],[0,0]],\"v\":[[-26.759,91.648],[-66.96,42.763],[-43.784,-26.641]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":5,\"s\":[{\"i\":[[0,0],[0.701,27.677],[-23.215,14.71]],\"o\":[[-17.607,-9.204],[-0.405,-15.984],[0,0]],\"v\":[[-32.893,81.704],[-69.201,29.323],[-41.785,-18.71]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[0,0],[-6.372,14.388],[-16.86,4.436]],\"o\":[[-9.639,-11.784],[6.608,-14.922],[0,0]],\"v\":[[-74.929,75.894],[-84.108,31.922],[-41.89,3.314]],\"c\":false}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0,0],[-14.453,13.82],[-10.072,-1.911]],\"o\":[[-3.391,-12.713],[16.367,-15.651],[0,0]],\"v\":[[-102.109,72.713],[-93.047,27.18],[-41.962,18.507]],\"c\":false}]},{\"t\":12,\"s\":[{\"i\":[[0,0],[-10.081,5.804],[-6.5,-5.25]],\"o\":[[-1.25,-14.5],[24.75,-14.25],[0,0]],\"v\":[[-121.75,68.75],[-97.75,28.5],[-42,26.5]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":1,\"op\":13,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":7,\"ty\":4,\"nm\":\"Arc R\",\"parent\":6,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[42.3,-15.5]],\"o\":[[0,15.6],[0,0]],\"v\":[[78.4,8.4],[28.45,78.125]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":2,\"s\":[{\"i\":[[0,0],[45.259,-17.574]],\"o\":[[5.569,19.742],[0,0]],\"v\":[[71.436,-1.265],[27.374,78.492]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":3,\"s\":[{\"i\":[[0,0],[47.39,-19.067]],\"o\":[[9.578,22.724],[0,0]],\"v\":[[66.422,-8.224],[29.61,86.067]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[0,0],[43.341,-23.274]],\"o\":[[42.63,43.092],[0,0]],\"v\":[[43.37,-29.592],[33.159,90.274]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":5,\"s\":[{\"i\":[[0,0],[32.542,-17.732]],\"o\":[[46.682,32.959],[0,0]],\"v\":[[44.318,-21.959],[47.958,77.232]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[0,0],[29.55,-17.51]],\"o\":[[48.487,23.807],[0,0]],\"v\":[[50.036,1.981],[67.878,99.974]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":8,\"s\":[{\"i\":[[0,0],[28.054,-17.399]],\"o\":[[49.39,19.231],[0,0]],\"v\":[[56.395,12.201],[71.838,111.344]],\"c\":false}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0,0],[27.199,-17.335]],\"o\":[[49.907,16.614],[0,0]],\"v\":[[57.245,18.726],[77.588,118.775]],\"c\":false}]},{\"t\":12,\"s\":[{\"i\":[[0,0],[26.05,-17.25]],\"o\":[[47.35,10.35],[0,0]],\"v\":[[62.15,25.9],[83.95,128.75]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":13,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":8,\"ty\":4,\"nm\":\"Leg\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[252.45,380.6,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.55,124.6,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-6.3,0],[0,0],[0,-6.3],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-6.3]],\"v\":[[-5.2,78.5],[-1.9,78.5],[9.5,89.9],[9.5,113.2],[-1.9,124.6],[-5.2,124.6],[-16.6,113.2],[-16.6,89.9]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":3,\"s\":[{\"i\":[[-10.717,-0.585],[0,0],[-5.941,-0.906],[0.206,-8.157],[11.865,-1.977],[0,0],[1.529,7.318],[0,0]],\"o\":[[0,0],[12.64,0.23],[0,0],[-0.717,13.324],[0,0],[-9.832,-0.809],[-1.086,-1.566],[7.288,1.01]],\"v\":[[-6.991,83.123],[-1.449,83.426],[26.156,78.756],[29.671,97.051],[10.094,121.234],[-4.602,122.184],[-26.436,108.079],[-29.519,79.278]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":5,\"s\":[{\"i\":[[-15.856,0],[0,0],[-3.721,-3.379],[3.175,-9.561],[10.154,-0.123],[0,0],[3.074,9.411],[0,0]],\"o\":[[0,0],[17.039,-0.353],[0,0],[-4.368,11.421],[0,0],[-13.978,-0.72],[-2.523,-3.997],[4.562,-2.465]],\"v\":[[-1.547,83.311],[6.603,83.311],[48.503,77.629],[40.118,101.079],[14.346,125.123],[-0.309,126.215],[-28.324,109.339],[-40.364,80.189]],\"c\":true}]},{\"t\":12,\"s\":[{\"i\":[[-24.292,0],[0,0],[0,-6.3],[8.795,-7.101],[15.905,-0.35],[0,0],[7.818,10.05],[0,0]],\"o\":[[0,0],[24.292,0],[0,0],[-9.351,7.55],[0,0],[-20.755,-1.1],[-4.75,-6.106],[0,-6.3]],\"v\":[[1.971,106.75],[14.403,106.75],[66.25,108.9],[57.149,125.2],[14.987,135.85],[2.263,135.85],[-43.318,122.45],[-49,110.15]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":1,\"op\":10,\"st\":0,\"bm\":0}]},{\"id\":\"comp_16\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":0,\"nm\":\"Pre-comp 1\",\"refId\":\"comp_17\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.7,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[30,30,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":10,\"s\":[28.282,29.857,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.7,0.7,0.7],\"y\":[1,1,1]},\"o\":{\"x\":[0.3,0.3,0.3],\"y\":[0,0,0]},\"t\":0,\"s\":[13.6,13.6,100]},{\"t\":10,\"s\":[13.5,13.5,100]}],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":0,\"op\":37,\"st\":0,\"bm\":0}]},{\"id\":\"comp_17\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Line\",\"parent\":2,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[3.687,-6.898,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":8,\"s\":[-3.313,3.102,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[3.6,-7,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-101.6,-111.5],[108.8,97.5]],\"c\":false},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.3],\"y\":[0]},\"t\":0,\"s\":[0]},{\"t\":7,\"s\":[31]}],\"ix\":1},\"e\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.3],\"y\":[0]},\"t\":0,\"s\":[100]},{\"t\":7,\"s\":[63]}],\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path-10\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":9,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Head Mic\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[-3.6,-33.55,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":12,\"s\":[9.4,60.45,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.6,-33.55,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[6.9,-8],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.6,-117.5],[42.4,-71.5],[42.4,4.5],[31.3,34.4],[-20.33,-17.23],[-49.6,-46.5],[-49.6,-71.5]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[-25.393,-0.391],[0.18,-19.791],[-0.243,-7.895],[14.056,-6.795],[0,0],[0,0],[0,0]],\"o\":[[28.416,0.506],[0,0],[0.243,14.68],[0,0],[0,0],[0,0],[0.052,-15.482]],\"v\":[[-6.434,-101.102],[47.056,-73.018],[47.664,4.298],[29.68,40.735],[-22.557,-15.51],[-55.36,-50.751],[-55.066,-73.828]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[-25.388,-0.636],[-1.714,-14.258],[-0.395,-12.85],[18.974,-7.989],[0,0],[0,0],[0,0]],\"o\":[[30.309,0.824],[0,0],[0.395,16.737],[0,0],[0,0],[0,0],[2.421,-11.434]],\"v\":[[-4.713,-90.812],[49.978,-73.971],[50.966,4.171],[29.789,44.635],[-23.955,-14.43],[-58.974,-53.419],[-58.496,-75.289]],\"c\":true}]},{\"t\":12,\"s\":[{\"i\":[[-25.306,-0.966],[8.574,-7],[-0.598,-19.5],[30.609,-1.65],[0,0],[0,0],[0,0]],\"o\":[[32.753,1.25],[0,0],[0.598,19.5],[0,0],[0,0],[0,0],[5.227,-2.25]],\"v\":[[-7.633,-77],[56.676,-75.25],[58.171,4],[29.904,46.4],[-22.818,-12.98],[-60.7,-57],[-59.227,-81]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.9,0],[0,25.4],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-5.2,2],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.6,-15.4],[-40.702,-6.502],[-38.75,-4.55],[13.1,47.3],[-3.6,50.4],[-49.6,4.4],[-49.6,-15.4]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[6.435,0.16],[0,28.821],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-13.044,10.194],[-29.138,-0.769],[0,0],[0,0]],\"v\":[[-55.579,-50.718],[-28.188,-26.309],[-26.057,-24.356],[32.281,39.246],[-10.39,49.691],[-55.329,4.4],[-55.38,-31.494]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[6.77,0.261],[0,30.968],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-13.708,7.528],[-31.483,-1.252],[0,0],[0,0]],\"v\":[[-59.067,-54.691],[-31.67,-27.138],[-29.427,-25.186],[31.261,43.864],[-14.65,49.247],[-58.924,4.4],[-59.007,-41.593]],\"c\":true}]},{\"t\":12,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[7.199,0.396],[0,33.85],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-14.557,3.95],[-34.528,-1.9],[0,0],[0,0]],\"v\":[[-60.625,-60.025],[-33.301,-28.252],[-30.915,-26.3],[32.198,46.3],[-17.623,48.4],[-60.625,4.4],[-60.75,-55.15]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":13,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Arc L\",\"parent\":5,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-3.75,84.5,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.75,84.5,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[13.1,0],[0,42]],\"o\":[[-11,5.1],[-45.3,0],[0,0]],\"v\":[[33,76.5],[-3.5,84.5],[-85.5,6.5]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":2,\"s\":[{\"i\":[[0,0],[17.605,8.684],[-3.033,28.67]],\"o\":[[-19.95,7.814],[-28.895,-17.316],[0,0]],\"v\":[[24.45,79.436],[-39.105,77.316],[-76.362,0.644]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":3,\"s\":[{\"i\":[[0,0],[11.789,14.347],[-5.217,19.073]],\"o\":[[-50.375,15.159],[-15.78,-19.203],[0,0]],\"v\":[[26.125,87.591],[-58.72,68.703],[-69.783,-3.573]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[0,0],[3.96,23.737],[-17.216,17.641]],\"o\":[[-21.241,-5.148],[-3.54,-27.263],[0,0]],\"v\":[[-26.759,91.648],[-66.96,42.763],[-43.784,-26.641]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":5,\"s\":[{\"i\":[[0,0],[0.701,27.677],[-23.215,14.71]],\"o\":[[-17.607,-9.204],[-0.405,-15.984],[0,0]],\"v\":[[-32.893,81.704],[-69.201,29.323],[-41.785,-18.71]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[0,0],[-6.372,14.388],[-16.86,4.436]],\"o\":[[-9.639,-11.784],[6.608,-14.922],[0,0]],\"v\":[[-74.929,75.894],[-84.108,31.922],[-41.89,3.314]],\"c\":false}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0,0],[-14.453,13.82],[-10.072,-1.911]],\"o\":[[-3.391,-12.713],[16.367,-15.651],[0,0]],\"v\":[[-102.109,72.713],[-93.047,27.18],[-41.962,18.507]],\"c\":false}]},{\"t\":12,\"s\":[{\"i\":[[0,0],[-10.081,5.804],[-6.5,-5.25]],\"o\":[[-1.25,-14.5],[24.75,-14.25],[0,0]],\"v\":[[-121.75,68.75],[-97.75,28.5],[-42,26.5]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":13,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Arc R\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[8.7,-12]],\"o\":[[0,15.6],[0,0]],\"v\":[[78.4,8.4],[64.7,50.5]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":3,\"s\":[{\"i\":[[0,0],[16.64,-12.817]],\"o\":[[15.078,25.724],[0,0]],\"v\":[[66.422,-8.224],[58.36,66.317]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4,\"s\":[{\"i\":[[0,0],[38.841,-19.274]],\"o\":[[42.63,43.092],[0,0]],\"v\":[[43.37,-29.592],[35.659,89.274]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":5,\"s\":[{\"i\":[[0,0],[32.542,-17.732]],\"o\":[[46.682,32.959],[0,0]],\"v\":[[44.318,-21.959],[47.958,77.232]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[0,0],[29.55,-17.51]],\"o\":[[48.487,23.807],[0,0]],\"v\":[[50.036,1.981],[67.878,99.974]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":8,\"s\":[{\"i\":[[0,0],[28.054,-17.399]],\"o\":[[49.39,19.231],[0,0]],\"v\":[[56.395,12.201],[71.838,111.344]],\"c\":false}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0,0],[27.199,-17.335]],\"o\":[[49.907,16.614],[0,0]],\"v\":[[57.245,18.726],[77.588,118.775]],\"c\":false}]},{\"t\":12,\"s\":[{\"i\":[[0,0],[26.05,-17.25]],\"o\":[[47.35,10.35],[0,0]],\"v\":[[62.15,25.9],[83.95,128.75]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":13,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"Leg\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[252.45,380.6,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.55,124.6,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-6.3,0],[0,0],[0,-6.3],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-6.3]],\"v\":[[-5.2,78.5],[-1.9,78.5],[9.5,89.9],[9.5,113.2],[-1.9,124.6],[-5.2,124.6],[-16.6,113.2],[-16.6,89.9]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":3,\"s\":[{\"i\":[[-10.717,-0.585],[0,0],[-5.941,-0.906],[0.206,-8.157],[11.865,-1.977],[0,0],[1.529,7.318],[0,0]],\"o\":[[0,0],[12.64,0.23],[0,0],[-0.717,13.324],[0,0],[-9.832,-0.809],[-1.086,-1.566],[7.288,1.01]],\"v\":[[-6.991,83.123],[-1.449,83.426],[26.156,78.756],[29.671,97.051],[10.094,121.234],[-4.602,122.184],[-26.436,108.079],[-29.519,79.278]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":5,\"s\":[{\"i\":[[-15.856,0],[0,0],[-3.721,-3.379],[3.175,-9.561],[10.154,-0.123],[0,0],[3.074,9.411],[0,0]],\"o\":[[0,0],[17.039,-0.353],[0,0],[-4.368,11.421],[0,0],[-13.978,-0.72],[-2.523,-3.997],[4.562,-2.465]],\"v\":[[-1.547,83.311],[6.603,83.311],[48.503,77.629],[40.118,101.079],[14.346,125.123],[-0.309,126.215],[-28.324,109.339],[-40.364,80.189]],\"c\":true}]},{\"t\":12,\"s\":[{\"i\":[[-24.292,0],[0,0],[0,-6.3],[8.795,-7.101],[15.905,-0.35],[0,0],[7.818,10.05],[0,0]],\"o\":[[0,0],[24.292,0],[0,0],[-9.351,7.55],[0,0],[-20.755,-1.1],[-4.75,-6.106],[0,-6.3]],\"v\":[[1.971,106.75],[14.403,106.75],[66.25,108.9],[57.149,125.2],[14.987,135.85],[2.263,135.85],[-43.318,122.45],[-49,110.15]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":10,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"Head 2\",\"parent\":8,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[0.613,-101.537,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":12,\"s\":[11.613,-50.537,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":26,\"s\":[11.613,-86.537,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":36,\"s\":[11.113,-83.037,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[11.613,-84.537,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":3,\"s\":[{\"i\":[[0,-17.328],[25.612,0],[0,17.328],[-25.612,0]],\"o\":[[0,17.328],[-25.612,0],[0,-17.328],[25.612,0]],\"v\":[[54.039,-62.942],[7.664,-31.567],[-38.711,-62.942],[7.664,-94.317]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[0,-23.543],[25.081,0],[0,23.543],[-25.081,0]],\"o\":[[0,23.543],[-25.081,0],[0,-23.543],[25.081,0]],\"v\":[[55.47,-76.028],[10.057,-33.4],[-35.356,-76.028],[10.057,-118.656]],\"c\":true}]},{\"i\":{\"x\":0.39,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":14,\"s\":[{\"i\":[[0,-24.735],[24.735,0],[0,24.735],[-24.735,0]],\"o\":[[0,24.735],[-24.735,0],[0,-24.735],[24.735,0]],\"v\":[[56.4,-84.537],[11.613,-39.75],[-33.174,-84.537],[11.613,-129.324]],\"c\":true}]},{\"t\":25,\"s\":[{\"i\":[[0,-26.924],[26.924,0],[0,26.924],[-26.924,0]],\"o\":[[0,26.924],[-26.924,0],[0,-26.924],[26.924,0]],\"v\":[[60.363,-84.537],[11.613,-35.787],[-37.137,-84.537],[11.613,-133.287]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":2,\"op\":37,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":7,\"ty\":4,\"nm\":\"Hands 2\",\"parent\":8,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0,\"y\":1},\"o\":{\"x\":0.05,\"y\":0},\"t\":9,\"s\":[11.336,13.774,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":12,\"s\":[11.336,18.274,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":25,\"s\":[11.336,11.774,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[11.336,10.774,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":7,\"s\":[{\"i\":[[0,0],[0.875,44.36],[0.012,11.61]],\"o\":[[0,0],[-0.348,-17.658],[-0.016,-14.955]],\"v\":[[-33.583,109.659],[-40.875,50.64],[-45.012,-17.11]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0,0],[-7.48,30.25],[-7.964,9.648]],\"o\":[[0,0],[2.73,-11.373],[7.228,-11.196]],\"v\":[[-57.163,97.688],[-57.996,36.651],[-42.036,0.102]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":10,\"s\":[{\"i\":[[0,0],[-14.611,30.379],[-23.925,1.795]],\"o\":[[0,0],[7.389,-12.121],[13.171,-8.112]],\"v\":[[-77.526,99.758],[-81.889,29.906],[-41.075,4.99]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":11,\"s\":[{\"i\":[[0,0],[-27.718,19.589],[-10.136,-1.028]],\"o\":[[0,0],[21.782,-16.411],[19.114,-5.028]],\"v\":[[-107.888,77.829],[-93.782,16.161],[-42.114,8.877]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":13,\"s\":[{\"i\":[[0,0],[-27.325,4.127],[-14.975,-3.949]],\"o\":[[0,0],[20.334,-3.734],[35.444,0.066]],\"v\":[[-136.225,32.858],[-90.834,7.893],[-38.525,10.472]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":14,\"s\":[{\"i\":[[0,0],[-27.129,-3.603],[-14.269,-4.157]],\"o\":[[0,0],[19.61,2.605],[43.609,2.613]],\"v\":[[-147.393,-5.627],[-89.61,-2.992],[-38.231,9.77]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":18,\"s\":[{\"i\":[[0,0],[-38.825,-22.336],[-19.04,-2.789]],\"o\":[[0,0],[14.675,9.164],[62.76,8.586]],\"v\":[[-144.801,-73.381],[-91.175,-7.197],[-37.96,10.757]],\"c\":false}]},{\"t\":28,\"s\":[{\"i\":[[0,0],[-46.458,-35.583],[-19.738,-3.567]],\"o\":[[0,0],[8.352,6.397],[79.039,14.286]],\"v\":[[-142.333,-130.341],[-88.062,-16.235],[-40.512,7.89]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.3,\"y\":0},\"t\":7,\"s\":[{\"i\":[[0,0],[0.583,25.777],[4.5,19.11]],\"o\":[[0,0],[-0.238,-10.518],[-18.41,-78.181]],\"v\":[[63,101.64],[63.167,54.973],[56,-16.61]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0,0],[2.455,25.149],[6.868,13.265]],\"o\":[[0,0],[-0.66,-9.745],[-29.265,-65.813]],\"v\":[[68.548,94.42],[70.368,48.723],[56.521,2.29]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":11,\"s\":[{\"i\":[[-11.495,10.995],[15.537,39.692],[11.504,1.535]],\"o\":[[14.034,-13.424],[-3.378,-8.551],[-47.075,-45.52]],\"v\":[[79.466,115.674],[91.463,38.058],[62.496,12.174]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":13,\"s\":[{\"i\":[[-14.54,10.097],[25.244,37.544],[12.466,1.965]],\"o\":[[15.872,-11.198],[-4.741,-7.952],[-56.007,-35.342]],\"v\":[[88.294,117.357],[93.089,33.781],[63.701,13.504]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":14,\"s\":[{\"i\":[[-16.063,9.648],[30.097,36.471],[11.946,0.457]],\"o\":[[16.791,-10.086],[-5.422,-7.653],[-60.473,-30.253]],\"v\":[[92.709,118.199],[93.903,31.642],[61.554,13.67]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":18,\"s\":[{\"i\":[[-5.509,3.309],[41.86,36.38],[11.653,0.923]],\"o\":[[5.759,-3.459],[-7.371,-6.796],[-73.243,-15.702]],\"v\":[[124.602,127.851],[94.405,26.111],[62.332,13.5]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"t\":25,\"s\":[{\"i\":[[0,0],[48,36.333],[11.5,1.167]],\"o\":[[0,0],[-8.388,-6.35],[-79.91,-8.107]],\"v\":[[141.25,132.89],[94.667,23.223],[63.5,11.89]],\"c\":false}]},{\"t\":28,\"s\":[{\"i\":[[0,0],[48,36.333],[11.5,1.167]],\"o\":[[0,0],[-8.388,-6.35],[-79.91,-8.107]],\"v\":[[140,128.89],[94.667,23.848],[63.5,11.89]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":12,\"op\":37,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":8,\"ty\":4,\"nm\":\"Body 2\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":12,\"s\":[266.676,398.378,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":23,\"s\":[266.676,387.378,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":34,\"s\":[266.676,392.378,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[10.676,136.378,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.69,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":12,\"s\":[{\"i\":[[-12.288,0.098],[-12.498,-0.987],[0,0],[3.339,-1.222],[4.897,-9.213],[22.369,0],[7.456,13.82],[-1.892,54.808]],\"o\":[[8.847,-0.071],[35.724,2.82],[24.706,0],[0,0],[-7.456,13.82],[-22.369,0],[-4.897,-9.213],[-12.353,-3.008]],\"v\":[[-46.407,7.071],[-18.334,9.585],[61.794,9.679],[70.141,28.952],[68.805,121.176],[13.161,136.5],[-42.484,121.176],[-47.047,25.191]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":23,\"s\":[{\"i\":[[-10.8,-2.3],[-8.2,-0.4],[0,0],[3,-1.3],[4.4,-9.8],[20.1,0],[6.7,14.7],[-1.7,58.3]],\"o\":[[7.9,1.7],[0,0],[22.2,0],[0,0],[-6.7,14.7],[-20.1,0],[-4.4,-9.8],[-11.1,-3.2]],\"v\":[[-40.2,-2.3],[-15.1,1.5],[56.9,1.6],[64.4,22.1],[63.2,120.2],[13.2,136.5],[-36.8,120.2],[-40.9,18.1]],\"c\":true}]},{\"t\":34,\"s\":[{\"i\":[[-10.8,-2.3],[-8.2,-0.4],[0,0],[3,-1.3],[4.4,-9.8],[24.175,0.094],[6.7,14.7],[-1.7,58.3]],\"o\":[[7.9,1.7],[0,0],[22.2,0],[0,0],[-6.669,14.144],[-20.1,-0.078],[-4.4,-9.8],[-11.1,-3.2]],\"v\":[[-40.2,-2.3],[-15.1,1.5],[56.9,1.6],[64.4,22.1],[63.044,119.606],[13.2,136.156],[-36.8,120.2],[-40.9,18.1]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":12,\"op\":37,\"st\":0,\"bm\":0}]},{\"id\":\"comp_18\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":0,\"nm\":\"Pre-comp 1\",\"refId\":\"comp_19\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[13.6,13.6,100],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":0,\"op\":30,\"st\":0,\"bm\":0}]},{\"id\":\"comp_19\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":3,\"nm\":\"Null 1\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":0,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[254.042,238.57,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[50,50,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.72,0.72,0.72],\"y\":[1,1,1]},\"o\":{\"x\":[0.28,0.28,0.28],\"y\":[0,0,0]},\"t\":0,\"s\":[100,100,100]},{\"i\":{\"x\":[0.72,0.72,0.72],\"y\":[1,1,1]},\"o\":{\"x\":[0.28,0.28,0.28],\"y\":[0,0,0]},\"t\":11,\"s\":[93,93,100]},{\"i\":{\"x\":[0.72,0.72,0.72],\"y\":[1,1,1]},\"o\":{\"x\":[0.28,0.28,0.28],\"y\":[0,0,0]},\"t\":21,\"s\":[101,101,100]},{\"t\":29,\"s\":[100,100,100]}],\"ix\":6}},\"ao\":0,\"ip\":0,\"op\":30,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Line\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.4,\"y\":0},\"t\":0,\"s\":[-13.538,-23.613,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":17,\"s\":[3.687,-6.898,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[3.6,-7,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-101.6,-111.5],[108.8,97.5]],\"c\":false},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":0,\"k\":0,\"ix\":1},\"e\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.4],\"y\":[0]},\"t\":0,\"s\":[0]},{\"t\":17,\"s\":[100]}],\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path-10\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":2,\"op\":30,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Head\",\"parent\":4,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-3.6,-33.55,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.6,-33.55,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":5,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[15.481,-9.962],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.6,-117.5],[42.4,-71.5],[42.4,4.5],[23.519,42.712],[-23.209,-2.257],[-49.7,-27.75],[-49.6,-71.5]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[6.9,-8],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.6,-117.5],[42.4,-71.5],[42.4,4.5],[31.3,34.4],[-10.034,-21.084],[-49.6,-46.5],[-49.6,-71.5]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[6.9,-8],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.6,-117.5],[42.4,-71.5],[42.4,4.5],[31.3,34.4],[19.623,9.37],[-49.6,-46.5],[-49.6,-71.5]],\"c\":true}]},{\"t\":8,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[6.9,-8],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.6,-117.5],[42.4,-71.5],[42.4,4.5],[31.3,34.4],[-20.33,-17.23],[-49.6,-46.5],[-49.6,-71.5]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":5,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.9,0],[0,25.4],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-12.35,7.7],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.7,-29.15],[-39.262,-18.975],[-36.972,-16.742],[23.85,42.55],[-3.6,50.4],[-49.6,4.4],[-49.7,-29.15]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":6,\"s\":[{\"i\":[[0,0],[-3.331,-2.789],[4.414,24.475],[0,0],[12.582,-0.112],[0,25.4],[0,0]],\"o\":[[0,0],[4.215,3.528],[21.749,15.62],[-9.968,12.883],[-25.399,0.226],[0,0],[0,0]],\"v\":[[-49.6,-15.4],[-38.553,-4.468],[-12.248,-24.48],[31.491,33.902],[-3.6,50.4],[-49.6,4.4],[-49.6,-15.4]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[0,0],[-1.647,-1.416],[3.578,20.948],[0,0],[13.582,-0.261],[0,25.4],[0,0]],\"o\":[[0,0],[7.932,6.822],[10.875,7.81],[-10.683,10.613],[-25.395,0.487],[0,0],[0,0]],\"v\":[[-49.6,-15.4],[-6.153,27.653],[21.197,9.349],[31.475,35.139],[-3.6,50.4],[-49.6,4.4],[-49.6,-15.4]],\"c\":true}]},{\"t\":8,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.9,0],[0,25.4],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-5.2,2],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.6,-15.4],[-40.702,-6.502],[-38.75,-4.55],[13.1,47.3],[-3.6,50.4],[-49.6,4.4],[-49.6,-15.4]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":30,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Arc L\",\"parent\":6,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-3.75,84.5,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.75,84.5,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[13.1,0],[0,42]],\"o\":[[-11,5.1],[-45.3,0],[0,0]],\"v\":[[33,76.5],[-3.5,84.5],[-85.5,6.5]],\"c\":false},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":30,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"Arc R\",\"parent\":4,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":8,\"s\":[{\"i\":[[0,0],[34.576,-19.595]],\"o\":[[0,15.6],[0,0]],\"v\":[[78.4,8.4],[38.027,74.567]],\"c\":false}]},{\"t\":9,\"s\":[{\"i\":[[0,0],[8.7,-12]],\"o\":[[0,15.6],[0,0]],\"v\":[[78.4,8.4],[64.7,50.5]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":30,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"Leg\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[48.408,192.03,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.55,124.6,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-6.3,0],[0,0],[0,-6.3],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-6.3]],\"v\":[[-5.2,78.5],[-1.9,78.5],[9.5,89.9],[9.5,113.2],[-1.9,124.6],[-5.2,124.6],[-16.6,113.2],[-16.6,89.9]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":30,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":7,\"ty\":4,\"nm\":\"EXAMPLE ON\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[48.708,70.98,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,3.55,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[45.3,0],[0,42]],\"o\":[[0,42],[-45.3,0],[0,0]],\"v\":[[82,8.4],[0,84.5],[-82,6.5]],\"c\":false},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":20.888,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-25.4,0],[0,0],[0,-25.4],[0,0],[25.4,0],[0,0],[0,25.4],[0,0]],\"o\":[[0,0],[25.4,0],[0,0],[0,25.4],[0,0],[-25.4,0],[0,0],[0,-25.4]],\"v\":[[0,-117.5],[0,-117.5],[46,-71.5],[46,4.5],[0,50.5],[0,50.5],[-46,4.5],[-46,-71.5]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-6.3,0],[0,0],[0,-6.3],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-6.3]],\"v\":[[-1.6,78.5],[1.7,78.5],[13.1,89.9],[13.1,113.2],[1.7,124.6],[-1.6,124.6],[-13,113.2],[-13,89.9]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":3,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":5,\"st\":0,\"bm\":0}]},{\"id\":\"comp_20\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":0,\"nm\":\"Pre-comp 1\",\"refId\":\"comp_21\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[13.6,13.6,100],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":0,\"op\":33,\"st\":0,\"bm\":0}]},{\"id\":\"comp_21\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":3,\"nm\":\"Null 1\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":0,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[254.062,238.57,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[50,50,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.7,0.7,0.7],\"y\":[1,1,1]},\"o\":{\"x\":[0.3,0.3,0.3],\"y\":[0,0,0]},\"t\":0,\"s\":[100,100,100]},{\"i\":{\"x\":[0.7,0.7,0.7],\"y\":[1,1,1]},\"o\":{\"x\":[0.3,0.3,0.3],\"y\":[0,0,0]},\"t\":12,\"s\":[93,93,100]},{\"i\":{\"x\":[0.7,0.7,0.7],\"y\":[1,1,1]},\"o\":{\"x\":[0.3,0.3,0.3],\"y\":[0,0,0]},\"t\":22,\"s\":[101,101,100]},{\"t\":32,\"s\":[100,100,100]}],\"ix\":6}},\"ao\":0,\"ip\":0,\"op\":33,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Line\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.4,\"y\":0},\"t\":2,\"s\":[3.687,-6.898,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":23,\"s\":[12.601,2.016,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[3.6,-7,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-101.6,-111.5],[108.8,97.5]],\"c\":false},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.3],\"y\":[1]},\"o\":{\"x\":[0.4],\"y\":[0]},\"t\":2,\"s\":[0]},{\"t\":23,\"s\":[100]}],\"ix\":1},\"e\":{\"a\":0,\"k\":100,\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path-10\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":21,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Head\",\"parent\":4,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-3.6,-33.55,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.6,-33.55,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":8,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[6.9,-8],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.6,-117.5],[42.4,-71.5],[42.4,4.5],[31.3,34.4],[-20.33,-17.23],[-49.6,-46.5],[-49.6,-71.5]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[5.665,-6.576],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.6,-117.5],[42.4,-71.5],[42.4,4.5],[32.133,31.549],[-27.809,-30.43],[-49.62,-42.75],[-49.6,-71.5]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":10,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[5.665,-6.576],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.6,-117.5],[42.4,-71.5],[42.4,4.5],[32.133,31.549],[-4.565,-6.652],[-49.62,-42.75],[-49.6,-71.5]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":11,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[5.665,-6.576],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.6,-117.5],[42.4,-71.5],[42.4,4.5],[32.133,31.549],[17.977,14.816],[-49.62,-42.75],[-49.6,-71.5]],\"c\":true}]},{\"t\":12,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[15.481,-9.962],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.6,-117.5],[42.4,-71.5],[42.4,4.5],[23.519,42.712],[-23.209,-2.257],[-49.7,-27.75],[-49.6,-71.5]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":8,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.9,0],[0,25.4],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-5.2,2],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.6,-15.4],[-40.702,-6.502],[-38.75,-4.55],[13.1,47.3],[-3.6,50.4],[-49.6,4.4],[-49.6,-15.4]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":9,\"s\":[{\"i\":[[0,0],[0,0],[-11.683,-16.357],[0,0],[5.9,0],[0,25.4],[0,0]],\"o\":[[0,0],[0,0],[4.734,6.628],[-6.63,3.14],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.575,-44.964],[-29.795,-33.421],[-38.394,-6.988],[15.25,46.35],[-3.6,50.4],[-49.6,4.4],[-49.575,-44.167]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":10,\"s\":[{\"i\":[[0,0],[0,0],[-11.683,-16.357],[0,0],[5.9,0],[0,25.4],[0,0]],\"o\":[[0,0],[0,0],[4.734,6.628],[-6.63,3.14],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.575,-44.964],[-6.551,-9.643],[-15.151,16.79],[15.25,46.35],[-3.6,50.4],[-49.6,4.4],[-49.575,-44.167]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":11,\"s\":[{\"i\":[[0,0],[0,0],[-11.683,-16.357],[0,0],[5.9,0],[0,25.4],[0,0]],\"o\":[[0,0],[0,0],[4.734,6.628],[-6.63,3.14],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.575,-44.964],[15.99,11.825],[7.391,38.258],[15.25,46.35],[-3.6,50.4],[-49.6,4.4],[-49.575,-44.167]],\"c\":true}]},{\"t\":12,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.9,0],[0,25.4],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-12.35,7.7],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.7,-29.15],[-39.262,-18.975],[-36.972,-16.742],[23.85,42.55],[-3.6,50.4],[-49.6,4.4],[-49.7,-29.15]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":33,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Arc L\",\"parent\":6,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-3.75,84.5,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.75,84.5,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[13.1,0],[0,42]],\"o\":[[-11,5.1],[-45.3,0],[0,0]],\"v\":[[33,76.5],[-3.5,84.5],[-85.5,6.5]],\"c\":false},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":33,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"Arc R\",\"parent\":4,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":13,\"s\":[{\"i\":[[0,0],[8.7,-12]],\"o\":[[0,15.6],[0,0]],\"v\":[[78.4,8.4],[64.7,50.5]],\"c\":false}]},{\"t\":14,\"s\":[{\"i\":[[0,0],[36.503,-16.928]],\"o\":[[0,15.6],[0,0]],\"v\":[[78.4,8.4],[34.73,75.676]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":33,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"Leg\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[48.408,192.03,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.55,124.6,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-6.3,0],[0,0],[0,-6.3],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-6.3]],\"v\":[[-5.2,78.5],[-1.9,78.5],[9.5,89.9],[9.5,113.2],[-1.9,124.6],[-5.2,124.6],[-16.6,113.2],[-16.6,89.9]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":33,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":7,\"ty\":4,\"nm\":\"EXAMPLE ON\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[48.708,70.98,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,3.55,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[45.3,0],[0,42]],\"o\":[[0,42],[-45.3,0],[0,0]],\"v\":[[82,8.4],[0,84.5],[-82,6.5]],\"c\":false},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":20.888,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-25.4,0],[0,0],[0,-25.4],[0,0],[25.4,0],[0,0],[0,25.4],[0,0]],\"o\":[[0,0],[25.4,0],[0,0],[0,25.4],[0,0],[-25.4,0],[0,0],[0,-25.4]],\"v\":[[0,-117.5],[0,-117.5],[46,-71.5],[46,4.5],[0,50.5],[0,50.5],[-46,4.5],[-46,-71.5]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-6.3,0],[0,0],[0,-6.3],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-6.3]],\"v\":[[-1.6,78.5],[1.7,78.5],[13.1,89.9],[13.1,113.2],[1.7,124.6],[-1.6,124.6],[-13,113.2],[-13,89.9]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":3,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":14,\"op\":33,\"st\":0,\"bm\":0}]},{\"id\":\"comp_22\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":0,\"nm\":\"Pre-comp 2\",\"refId\":\"comp_23\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.7,\"y\":1},\"o\":{\"x\":0.3,\"y\":0},\"t\":0,\"s\":[28.282,29.857,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":10,\"s\":[30,30,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[256,256,0],\"ix\":1},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.7,0.7,0.7],\"y\":[1,1,1]},\"o\":{\"x\":[0.3,0.3,0.3],\"y\":[0,0,0]},\"t\":0,\"s\":[13.5,13.5,100]},{\"t\":10,\"s\":[13.6,13.6,100]}],\"ix\":6}},\"ao\":0,\"w\":512,\"h\":512,\"ip\":0,\"op\":36,\"st\":0,\"bm\":0}]},{\"id\":\"comp_23\",\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Head 2\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[11.613,-83.537,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":14,\"s\":[13.613,-4.037,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[11.613,-84.537,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,-26.924],[26.924,0],[0,26.924],[-26.924,0]],\"o\":[[0,26.924],[-26.924,0],[0,-26.924],[26.924,0]],\"v\":[[60.363,-84.537],[11.613,-35.787],[-37.137,-84.537],[11.613,-133.287]],\"c\":true}]},{\"t\":14,\"s\":[{\"i\":[[0,-25.609],[26.234,0],[0,25.609],[-26.234,0]],\"o\":[[0,25.609],[-26.234,0],[0,-25.609],[26.234,0]],\"v\":[[55.5,-92.894],[8,-46.525],[-39.5,-92.894],[8,-139.262]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":13,\"st\":-2,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Hands 2\",\"parent\":3,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[11.336,11.274,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[11.336,10.774,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.818},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[-46.458,-35.583],[-24.488,-5.64]],\"o\":[[0,0],[8.352,6.397],[78.271,18.027]],\"v\":[[-141.771,-129.716],[-87.875,-16.36],[-40.512,7.89]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.182},\"t\":2.742,\"s\":[{\"i\":[[0,0],[-36.01,-10.9],[-24.155,1.341]],\"o\":[[0,0],[17.49,5.6],[69.436,6.533]],\"v\":[[-154.359,-38.55],[-101.24,-1.379],[-38.096,3.379]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.818},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":4.572,\"s\":[{\"i\":[[0,0],[-22.495,10.354],[-22.973,-2.409]],\"o\":[[0,0],[18.597,-8.552],[52.89,-5.679]],\"v\":[[-130.223,30.529],[-97.344,4.391],[-37.533,-6.68]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.818},\"o\":{\"x\":0.167,\"y\":0.182},\"t\":5.486,\"s\":[{\"i\":[[0,0],[-15.738,20.981],[-22.382,-4.284]],\"o\":[[0,0],[16.262,-20.519],[44.618,-11.784]],\"v\":[[-110.655,59.818],[-92.396,11.026],[-37.251,-11.709]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.182},\"t\":9,\"s\":[{\"i\":[[0,0],[15.091,34.278],[-16.072,17.731]],\"o\":[[0,0],[-7.208,-22.817],[16.812,-10.179]],\"v\":[[-13.011,129.282],[-62.232,89.104],[-50.644,19.509]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.818},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":10,\"s\":[{\"i\":[[0,0],[19.6,34.344],[-11.896,20.735]],\"o\":[[0,0],[-10.53,-23.142],[12.875,-9.952]],\"v\":[[0.934,129.625],[-59.674,93.522],[-55.966,25.06]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.182},\"t\":11,\"s\":[{\"i\":[[0,0],[24.108,34.41],[-7.719,23.739]],\"o\":[[0,0],[-13.853,-23.467],[8.939,-9.725]],\"v\":[[1.88,129.968],[-57.117,97.94],[-61.289,30.611]],\"c\":false}]},{\"t\":14,\"s\":[{\"i\":[[0,0],[25.209,29.401],[-7.488,20.11]],\"o\":[[0,0],[-15.125,-17.64],[3.974,-10.674]],\"v\":[[6.417,130.659],[-53.875,103.64],[-63.512,36.89]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.818},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[48,36.333],[11.5,1.167]],\"o\":[[0,0],[-8.388,-6.35],[-79.91,-8.107]],\"v\":[[140.75,128.89],[94.542,23.473],[63.5,11.89]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.818},\"o\":{\"x\":0.167,\"y\":0.182},\"t\":2.742,\"s\":[{\"i\":[[-0.107,1.301],[43.705,35.181],[12.499,4.424]],\"o\":[[1.452,-17.63],[-7.425,-6.513],[-75.279,-8.976]],\"v\":[[133.797,130.851],[92.055,22.64],[62.256,6.25]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.818},\"o\":{\"x\":0.167,\"y\":0.182},\"t\":5.486,\"s\":[{\"i\":[[-6.276,1.09],[19.852,26.915],[21.224,10.921]],\"o\":[[6.157,-12.218],[-8.148,-12.585],[-54.801,-12.818]],\"v\":[[109.709,105.725],[104.015,23.092],[59.642,-11.414]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.818},\"o\":{\"x\":0.167,\"y\":0.182},\"t\":9,\"s\":[{\"i\":[[-14.16,0.82],[-3.078,22.988],[16.441,25.833]],\"o\":[[22.052,-4.736],[2.47,-14.595],[-28.631,-17.728]],\"v\":[[32.732,128.976],[84.858,79.012],[68.64,15.393]],\"c\":false}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.182},\"t\":11,\"s\":[{\"i\":[[-18.622,0.668],[-13.293,19.932],[12.19,32.742]],\"o\":[[37.47,-1.344],[5.361,-8.677],[-13.817,-20.508]],\"v\":[[7.53,130.546],[77.418,98.03],[80.302,30.109]],\"c\":false}]},{\"t\":14,\"s\":[{\"i\":[[-19.233,1.202],[-15.167,22.777],[7,26.11]],\"o\":[[30.25,-1.89],[5.831,-8.757],[-6.192,-23.095]],\"v\":[[11.75,130.39],[78.167,98.723],[84,36.89]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":12,\"st\":-2,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Body 2\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[266.426,392.378,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":14,\"s\":[253.676,337.378,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[10.676,136.378,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.5,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-10.795,-2.32],[-8.2,-0.399],[0,0],[3,-1.298],[6.05,-16.428],[20.1,0],[6.7,14.68],[-1.7,58.223]],\"o\":[[7.9,1.698],[0,0],[22.2,0],[0,0],[-7.825,16.029],[-20.1,0],[-4.4,-9.787],[-11.1,-3.196]],\"v\":[[-40.2,-2.303],[-15.1,1.492],[56.9,1.592],[64.619,22.065],[63.2,120.034],[13.2,136.312],[-36.8,120.034],[-40.9,18.07]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":5,\"s\":[{\"i\":[[-7.729,4.405],[-14.608,0.316],[-11.697,-7.483],[-0.238,-10.621],[7.036,-14.499],[18.581,0],[6.908,15.026],[-1.237,40.257]],\"o\":[[7.771,-9.845],[25.241,-0.704],[10.053,2.017],[0,0],[-6.792,14.491],[-18.581,0],[-5.829,-11.041],[-0.133,-10.892]],\"v\":[[-35.485,-12.023],[0.894,-25.934],[52.233,-18.385],[61.774,26.253],[58.326,106.281],[12.028,125.952],[-35.105,105.616],[-40.582,23.524]],\"c\":true}]},{\"i\":{\"x\":0.3,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[-4.018,5.73],[-11.874,0.331],[-0.482,-11.186],[1.315,-6.931],[7.788,-13.027],[17.423,0],[7.066,15.289],[-0.885,26.553]],\"o\":[[3.929,-18.735],[44.497,-1.242],[7.668,7.255],[0,0],[-6.004,13.318],[-17.423,0],[-6.92,-11.998],[0.031,-13.41]],\"v\":[[-38.5,-13.268],[1.196,-38.483],[57.046,-13.793],[59.603,29.448],[54.608,95.788],[11.134,118.049],[-33.812,94.617],[-40.339,27.685]],\"c\":true}]},{\"t\":14,\"s\":[{\"i\":[[-0.661,9.718],[-13.678,-0.71],[-0.721,-16.729],[0.48,-9.722],[8.649,-11.342],[16.096,0],[7.247,15.59],[-0.48,10.861]],\"o\":[[1.962,-28.86],[25.305,1.314],[0.467,10.849],[0,0],[-5.101,11.975],[-16.096,0],[-8.168,-13.094],[0.521,-13.007]],\"v\":[[-37.657,-18.701],[9.27,-58.29],[57.118,-21.416],[57.118,33.106],[50.351,83.775],[10.111,109],[-32.332,82.023],[-37.817,32.45]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":13,\"st\":-2,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Line\",\"parent\":5,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[3.687,-6.898,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[3.6,-7,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.1,\"y\":0},\"t\":8,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-87.744,-97.736],[94.944,83.736]],\"c\":false}]},{\"t\":24,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-101.6,-111.5],[108.8,97.5]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"tm\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.4],\"y\":[1]},\"o\":{\"x\":[0.2],\"y\":[0]},\"t\":8.143,\"s\":[65]},{\"t\":23.857421875,\"s\":[0]}],\"ix\":1},\"e\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.4],\"y\":[1]},\"o\":{\"x\":[0.2],\"y\":[0]},\"t\":8,\"s\":[22]},{\"t\":23.857421875,\"s\":[100]}],\"ix\":2},\"o\":{\"a\":0,\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\",\"mn\":\"ADBE Vector Filter - Trim\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Path-10\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":15,\"op\":36,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"Head Mic\",\"parent\":6,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-3.6,-33.55,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.6,-33.55,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.32,\"y\":0},\"t\":12,\"s\":[{\"i\":[[-26.211,0],[-0.75,-24.287],[0,0],[8.887,-9.825],[0,0],[0,0],[0,0]],\"o\":[[26.211,0],[0,0],[0,11.606],[0,0],[0,0],[0,0],[-1,-21.796]],\"v\":[[-3.735,-109.75],[45.281,-65.463],[43.991,8.35],[34.343,45.154],[-28.222,-7.666],[-50.958,-26.52],[-50.969,-65.204]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":14,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[3.254,-6.84],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.715,-109.619],[43.599,-65.808],[42.504,7.127],[37.527,30.34],[-24.609,-26.91],[-49.585,-52.154],[-50.153,-65.37]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":15,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[2.173,-4.255],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.699,-110.688],[43.436,-66.58],[42.49,6.771],[35.692,29.42],[-20.918,-24.458],[-49.597,-54.517],[-50.078,-66.202]],\"c\":true}]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":17,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[2.511,-3.584],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.668,-112.826],[43.111,-68.124],[42.462,6.058],[35.521,27.954],[-13.534,-19.556],[-49.621,-59.243],[-49.928,-67.865]],\"c\":true}]},{\"t\":25,\"s\":[{\"i\":[[-25.4,0],[0,-25.4],[0,0],[6.9,-8],[0,0],[0,0],[0,0]],\"o\":[[25.4,0],[0,0],[0,11.4],[0,0],[0,0],[0,0],[0,-25.4]],\"v\":[[-3.6,-117.5],[42.4,-71.5],[42.4,4.5],[31.3,34.4],[-20.33,-17.23],[-49.6,-46.5],[-49.6,-71.5]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false},{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.32,\"y\":0},\"t\":12,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[6.088,0],[-0.503,31.164],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-10.319,13.723],[-26.211,0],[0,0],[0,0]],\"v\":[[-50.984,-30.364],[-31.444,-15.166],[-26.333,-11.224],[33.943,44.851],[-3.477,59],[-50.945,10.019],[-50.958,-21.446]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":13,\"s\":[{\"i\":[[0,0],[-0.064,2.378],[0,0],[0,0],[6.019,0],[-0.47,30.673],[0,0]],\"o\":[[0,0],[0.089,-3.285],[0,0],[-10.163,17.808],[-25.911,0],[0,0],[0,0]],\"v\":[[-50.431,-27.196],[-50.235,-36.056],[-34.964,-26.612],[35.415,38.984],[-3.485,57.977],[-50.411,9.433],[-50.391,-18.196]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":14,\"s\":[{\"i\":[[0,0],[-0.174,6.439],[0,0],[0,0],[5.9,0],[-0.415,29.833],[0,0]],\"o\":[[0,0],[0.241,-8.895],[0,0],[-9.897,24.787],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.489,-21.787],[-49.835,-35.855],[-49.705,-52.894],[37.928,28.963],[-3.499,56.229],[-49.499,8.433],[-49.422,-12.646]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":15,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.9,0],[-0.361,29.258],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-5.945,8.024],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.614,-15.443],[-16.569,15.355],[-4.062,27.479],[16.726,47.226],[-3.512,55.472],[-49.512,7.909],[-49.493,-12.332]],\"c\":true}]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":17,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.9,0],[-0.253,28.106],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-4.54,4.12],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.647,-16.255],[-37.036,-2.726],[-33.526,0.224],[15.322,49.25],[-3.538,53.958],[-49.538,6.862],[-49.634,-11.706]],\"c\":true}]},{\"t\":25,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0],[5.9,0],[0,25.4],[0,0]],\"o\":[[0,0],[0,0],[0,0],[-5.2,2],[-25.4,0],[0,0],[0,0]],\"v\":[[-49.6,-15.4],[-40.702,-6.502],[-38.75,-4.55],[13.1,47.3],[-3.6,50.4],[-49.6,4.4],[-49.6,-15.4]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":2,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":12,\"op\":36,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"Arc L\",\"parent\":8,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.5,\"y\":1},\"o\":{\"x\":0.05,\"y\":0},\"t\":15,\"s\":[-3.781,80.5,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.69,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":25,\"s\":[-3.781,89.75,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":33,\"s\":[-3.75,84.5,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.75,84.5,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.1,\"y\":0},\"t\":12,\"s\":[{\"i\":[[0,0],[13.1,0],[-24.469,59.5]],\"o\":[[-11,5.1],[-45.3,0],[0,0]],\"v\":[[32,72.5],[-6,80],[-74,-20]],\"c\":false}]},{\"t\":26,\"s\":[{\"i\":[[0,0],[13.1,0],[0,42]],\"o\":[[-11,5.1],[-45.3,0],[0,0]],\"v\":[[33,76.5],[-3.5,84.5],[-85.5,6.5]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":12,\"op\":36,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":7,\"ty\":4,\"nm\":\"Arc R\",\"parent\":6,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[66.05,51.45,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.1,\"y\":0},\"t\":12,\"s\":[{\"i\":[[0,0],[39.331,-15]],\"o\":[[9.631,29.1],[0,0]],\"v\":[[68.4,-20.1],[39.2,68.5]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":15,\"s\":[{\"i\":[[0,0],[38.51,-16.677]],\"o\":[[5.914,24.45],[0,0]],\"v\":[[72.925,-10.242],[39.451,70.103]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":16,\"s\":[{\"i\":[[0,0],[10.82,-9.511]],\"o\":[[4.75,22.994],[0,0]],\"v\":[[74.342,-7.154],[65.462,49.587]],\"c\":false}]},{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":19,\"s\":[{\"i\":[[0,0],[13.536,-11.424]],\"o\":[[1.257,18.625],[0,0]],\"v\":[[78.594,2.109],[61.495,50.04]],\"c\":false}]},{\"t\":26,\"s\":[{\"i\":[[0,0],[8.7,-12]],\"o\":[[0,15.6],[0,0]],\"v\":[[78.4,8.4],[64.7,50.5]],\"c\":false}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":22,\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":12,\"op\":36,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":8,\"ty\":4,\"nm\":\"Leg\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[252.45,380.6,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-3.55,124.6,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.4,\"y\":1},\"o\":{\"x\":0.1,\"y\":0},\"t\":11,\"s\":[{\"i\":[[-11.972,0],[0,0],[0,-2.631],[0,0],[11.972,0],[0,0],[0,2.631],[0,0]],\"o\":[[0,0],[11.972,0],[0,0],[0,2.631],[0,0],[-11.972,0],[0,0],[0,-2.631]],\"v\":[[-6.686,71.5],[-0.414,71.5],[21.25,76.26],[21.25,85.99],[-0.414,90.75],[-6.686,90.75],[-28.35,85.99],[-28.35,76.26]],\"c\":true}]},{\"i\":{\"x\":0.69,\"y\":1},\"o\":{\"x\":0.31,\"y\":0},\"t\":20,\"s\":[{\"i\":[[-6.3,0],[0,0],[0,-4.763],[0,0],[6.3,0],[0,0],[0,4.763],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,4.763],[0,0],[-6.3,0],[0,0],[0,-4.763]],\"v\":[[-5.2,89.75],[-1.9,89.75],[9.5,98.368],[9.5,115.982],[-1.9,124.6],[-5.2,124.6],[-16.6,115.982],[-16.6,98.368]],\"c\":true}]},{\"t\":33,\"s\":[{\"i\":[[-6.3,0],[0,0],[0,-6.3],[0,0],[6.3,0],[0,0],[0,6.3],[0,0]],\"o\":[[0,0],[6.3,0],[0,0],[0,6.3],[0,0],[-6.3,0],[0,0],[0,-6.3]],\"v\":[[-5.2,78.5],[-1.9,78.5],[9.5,89.9],[9.5,113.2],[-1.9,124.6],[-5.2,124.6],[-16.6,113.2],[-16.6,89.9]],\"c\":true}]}],\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":11,\"op\":36,\"st\":0,\"bm\":0}]}],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":0,\"nm\":\"Start Chat to Mute Android 3\",\"refId\":\"comp_0\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[30,30,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"w\":60,\"h\":60,\"ip\":376,\"op\":405,\"st\":376,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":0,\"nm\":\"Cancel Reminder to Mute Android\",\"refId\":\"comp_2\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[30,30,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"w\":60,\"h\":60,\"ip\":344,\"op\":376,\"st\":344,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":0,\"nm\":\"Set  Reminder Android\",\"refId\":\"comp_4\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[30,30,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"w\":60,\"h\":60,\"ip\":311,\"op\":344,\"st\":311,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":0,\"nm\":\"Cancel Reminder to Rise Hand\",\"refId\":\"comp_6\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[30,30,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"w\":60,\"h\":60,\"ip\":274,\"op\":311,\"st\":274,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":0,\"nm\":\"Set Reminder to Rise Hand\",\"refId\":\"comp_8\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[30,30,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"w\":60,\"h\":60,\"ip\":237,\"op\":274,\"st\":237,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":0,\"nm\":\"Set Reminder to Mute\",\"refId\":\"comp_10\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[30,30,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"w\":60,\"h\":60,\"ip\":202,\"op\":237,\"st\":202,\"bm\":0},{\"ddd\":0,\"ind\":7,\"ty\":0,\"nm\":\"Cancel Reminder\",\"refId\":\"comp_12\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[30,30,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"w\":60,\"h\":60,\"ip\":173,\"op\":202,\"st\":173,\"bm\":0},{\"ddd\":0,\"ind\":8,\"ty\":0,\"nm\":\"Unmute to Rise Hand\",\"refId\":\"comp_14\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[30,30,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"w\":60,\"h\":60,\"ip\":136,\"op\":173,\"st\":136,\"bm\":0},{\"ddd\":0,\"ind\":9,\"ty\":0,\"nm\":\"Mute to Rise Hand\",\"refId\":\"comp_16\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[30,30,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"w\":60,\"h\":60,\"ip\":99,\"op\":136,\"st\":99,\"bm\":0},{\"ddd\":0,\"ind\":10,\"ty\":0,\"nm\":\"Mute\",\"refId\":\"comp_18\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[30,30,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"w\":60,\"h\":60,\"ip\":69,\"op\":99,\"st\":69,\"bm\":0},{\"ddd\":0,\"ind\":11,\"ty\":0,\"nm\":\"Unmute Android\",\"refId\":\"comp_20\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[30,30,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"w\":60,\"h\":60,\"ip\":36,\"op\":69,\"st\":36,\"bm\":0},{\"ddd\":0,\"ind\":12,\"ty\":0,\"nm\":\"Raise Hand to Mute\",\"refId\":\"comp_22\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,30,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[30,30,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"w\":60,\"h\":60,\"ip\":0,\"op\":36,\"st\":0,\"bm\":0}],\"markers\":[]}"
  },
  {
    "path": "Telegram/Resources/icons/settings/devices/device_desktop_mac.lottie",
    "content": "{\"v\":\"5.7.4\",\"fr\":60,\"ip\":0,\"op\":120,\"w\":30,\"h\":30,\"nm\":\"mac_30\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Rectangle 21\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[15,20.5,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[],\"ip\":0,\"op\":120,\"st\":-120,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Rectangle 23\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[15,22,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,-0.276],[-0.828,0],[0,0],[0,0.828],[0.276,0]],\"o\":[[-0.276,0],[0,0.828],[0,0],[0.828,0],[0,-0.276],[0,0]],\"v\":[[-12.5,-1],[-13,-0.5],[-11.5,1],[11.5,1],[13,-0.5],[12.5,-1]],\"c\":true},\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Rectangle 22\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-120,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Rectangle 22\",\"td\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[15,22,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,-0.276],[-0.828,0],[0,0],[0,0.828],[0.276,0]],\"o\":[[-0.276,0],[0,0.828],[0,0],[0.828,0],[0,-0.276],[0,0]],\"v\":[[-12.5,-17.667],[-13,-0.5],[-11.5,1],[11.5,1],[13,-0.5],[12.5,-17.667]],\"c\":true},\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Rectangle 22\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-120,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Rectangle 21\",\"tt\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":0,\"s\":[15,15,0],\"to\":[0,1.083,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":1,\"y\":0},\"t\":20,\"s\":[15,21.5,0],\"to\":[0,0,0],\"ti\":[0,1.083,0]},{\"t\":40,\"s\":[15,15,0]}],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0.192,-0.376],[0,-1.12],[0,0],[0,0],[0,0],[0.218,0.428],[0.376,0.192],[1.12,0],[0,0],[0.428,-0.218]],\"o\":[[-0.218,0.428],[0,0],[0,0],[0,0],[0,-1.12],[-0.192,-0.376],[-0.428,-0.218],[0,0],[-1.12,0],[-0.376,0.192]],\"v\":[[-8.782,-5.908],[-9,-3.8],[-9,7],[9,7],[9,-3.8],[8.782,-5.908],[7.908,-6.782],[5.8,-7],[-5.8,-7],[-7.908,-6.782]],\"c\":true}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":1,\"y\":0},\"t\":20,\"s\":[{\"i\":[[0.256,-0.027],[0,-0.08],[0,0],[0,0],[0,0],[0.291,0.031],[0.502,0.014],[1.493,0],[0,0],[0.57,-0.016]],\"o\":[[-0.291,0.031],[0,0],[0,0],[0,0],[0,-0.08],[-0.256,-0.027],[-0.57,-0.016],[0,0],[-1.493,0],[-0.502,0.014]],\"v\":[[-11.709,-0.422],[-12,-0.271],[-12,0.5],[12,0.5],[12,-0.271],[11.709,-0.422],[10.544,-0.484],[7.733,-0.5],[-7.733,-0.5],[-10.544,-0.484]],\"c\":true}]},{\"t\":40,\"s\":[{\"i\":[[0.192,-0.376],[0,-1.12],[0,0],[0,0],[0,0],[0.218,0.428],[0.376,0.192],[1.12,0],[0,0],[0.428,-0.218]],\"o\":[[-0.218,0.428],[0,0],[0,0],[0,0],[0,-1.12],[-0.192,-0.376],[-0.428,-0.218],[0,0],[-1.12,0],[-0.376,0.192]],\"v\":[[-8.782,-5.908],[-9,-3.8],[-9,7],[9,7],[9,-3.8],[8.782,-5.908],[7.908,-6.782],[5.8,-7],[-5.8,-7],[-7.908,-6.782]],\"c\":true}]}],\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":2,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Обводка 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Rectangle 21\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-120,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"apple 2\",\"parent\":4,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-0.955,-1.5,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0,0,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":0,\"s\":[80,80,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[1,1,0.333],\"y\":[0,0,0]},\"t\":20,\"s\":[40,0,100]},{\"t\":40,\"s\":[80,80,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-0.31,0.445],[-0.161,0.375],[-0.056,0.176],[0.222,0.21],[0.009,0.704],[-0.829,0.5],[0.927,0.074],[0.494,-0.176],[0.093,0],[0.431,0.162],[0.264,0],[0.409,-0.246],[0.242,-0.418],[0,-0.737],[-0.236,-0.686],[-0.359,-0.515],[-0.213,-0.185],[-0.334,0.014],[-0.357,0.144],[-0.344,0.006],[-0.307,-0.133],[-0.25,0],[-0.306,0.278]],\"o\":[[0.234,-0.334],[0.07,-0.162],[-0.28,-0.122],[-0.5,-0.463],[-0.009,-0.899],[-0.463,-0.658],[-0.34,-0.028],[-0.524,0.19],[-0.12,0],[-0.431,-0.162],[-0.477,0.005],[-0.414,0.248],[-0.32,0.533],[0,0.644],[0.201,0.594],[0.32,0.449],[0.334,0.31],[0.222,-0.009],[0.317,-0.135],[0.335,0.007],[0.352,0.148],[0.347,-0.009],[0.195,-0.171]],\"v\":[[3.848,4.137],[4.441,3.071],[4.631,2.566],[3.871,2.066],[3.107,0.315],[4.334,-1.789],[2.249,-2.887],[0.998,-2.665],[0.076,-2.377],[-0.753,-2.623],[-1.8,-2.868],[-3.153,-2.484],[-4.154,-1.469],[-4.631,0.435],[-4.274,2.427],[-3.431,4.1],[-2.634,5.054],[-1.633,5.499],[-0.767,5.272],[0.234,5.059],[1.207,5.272],[2.11,5.49],[3.088,5.059]],\"c\":true},\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ind\":1,\"ty\":\"sh\",\"ix\":2,\"ks\":{\"a\":0,\"k\":{\"i\":[[-0.482,0.565],[0,0.551],[0.005,0.074],[0.277,-0.142],[0.217,-0.246],[0,-0.528],[-0.01,-0.069]],\"o\":[[0.403,-0.473],[0,-0.074],[-0.31,0.023],[-0.297,0.139],[-0.408,0.463],[0,0.07],[0.63,0.051]],\"v\":[[1.717,-3.739],[2.319,-5.278],[2.31,-5.5],[1.42,-5.25],[0.642,-4.666],[0.002,-3.109],[0.016,-2.901]],\"c\":true},\"ix\":2},\"nm\":\"Контур 2\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"mm\",\"mm\":1,\"nm\":\"Объединить контуры 1\",\"mn\":\"ADBE Vector Filter - Merge\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"apple\",\"np\":4,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-120,\"bm\":0}],\"markers\":[]}"
  },
  {
    "path": "Telegram/Resources/icons/settings/devices/device_desktop_win.lottie",
    "content": "{\"v\":\"5.7.4\",\"fr\":60,\"ip\":0,\"op\":120,\"w\":30,\"h\":30,\"nm\":\"windows_30\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Union\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"s\":true,\"x\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":0,\"s\":[15]},{\"i\":{\"x\":[0.833],\"y\":[0.889]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":25,\"s\":[12.5]},{\"i\":{\"x\":[0],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.222]},\"t\":35,\"s\":[15]},{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":55,\"s\":[17.5]},{\"t\":70,\"s\":[15]}],\"ix\":3},\"y\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":0,\"s\":[15]},{\"i\":{\"x\":[0.833],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":25,\"s\":[15]},{\"i\":{\"x\":[0],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0]},\"t\":35,\"s\":[15]},{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":55,\"s\":[15]},{\"t\":70,\"s\":[15]}],\"ix\":4}},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],\"v\":[[-1,11],[-1,1],[-12.667,1],[-12.667,-1],[-1,-1],[-1,-11],[1,-11],[1,-1],[12.667,-1],[12.667,1],[1,1],[1,11]],\"c\":true},\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0,0,0,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Union\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-60,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Rectangle 23\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[15,15,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0]],\"v\":[[9,-9],[9,9],[-9,9],[-9,-9]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.333,\"y\":0},\"t\":25,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0]],\"v\":[[9,-10.667],[9,10.667],[-10.667,5.667],[-10.667,-5.667]],\"c\":true}]},{\"i\":{\"x\":0,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":35,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0]],\"v\":[[9,-9],[9,9],[-9,9],[-9,-9]],\"c\":true}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":55,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0]],\"v\":[[10.667,-5.667],[10.667,5.667],[-9,10.667],[-9,-10.667]],\"c\":true}]},{\"t\":70,\"s\":[{\"i\":[[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0]],\"v\":[[9,-9],[9,9],[-9,9],[-9,-9]],\"c\":true}]}],\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Rectangle 23\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-60,\"bm\":0}],\"markers\":[]}"
  },
  {
    "path": "Telegram/Resources/icons/settings/devices/device_linux.lottie",
    "content": "{\"v\":\"5.7.4\",\"fr\":60,\"ip\":0,\"op\":120,\"w\":30,\"h\":30,\"nm\":\"linux_30\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Vector 25\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":0,\"s\":[15,21.686,0],\"to\":[0,0.069,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":10,\"s\":[15,22.103,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":20,\"s\":[15,21.686,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":30,\"s\":[15,22.103,0],\"to\":[0,0,0],\"ti\":[0,0.069,0]},{\"t\":40,\"s\":[15,21.686,0]}],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-0.452,0.127],[-1.798,0],[-1.471,-0.414],[0.428,-0.195],[1.333,-1.348],[0.372,0.376],[1.412,0.643]],\"o\":[[1.471,-0.414],[1.798,0],[0.452,0.127],[-1.412,0.643],[-0.372,0.376],[-1.333,-1.348],[-0.428,-0.195]],\"v\":[[-5.66,-1.69],[0,-2.686],[5.66,-1.69],[5.712,-0.738],[0.726,2.404],[-0.726,2.404],[-5.712,-0.738]],\"c\":true}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":10,\"s\":[{\"i\":[[-0.452,0.127],[-1.798,0],[-1.471,-0.414],[0.428,-0.195],[1.333,-1.348],[0.372,0.376],[1.412,0.643]],\"o\":[[1.471,-0.414],[1.798,0],[0.452,0.127],[-1.412,0.643],[-0.372,0.376],[-1.333,-1.348],[-0.428,-0.195]],\"v\":[[-5.66,-1.69],[0,-2.686],[5.66,-1.69],[5.712,-0.738],[0.726,2.821],[-0.726,2.821],[-5.712,-0.738]],\"c\":true}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":20,\"s\":[{\"i\":[[-0.452,0.127],[-1.798,0],[-1.471,-0.414],[0.428,-0.195],[1.333,-1.348],[0.372,0.376],[1.412,0.643]],\"o\":[[1.471,-0.414],[1.798,0],[0.452,0.127],[-1.412,0.643],[-0.372,0.376],[-1.333,-1.348],[-0.428,-0.195]],\"v\":[[-5.66,-1.69],[0,-2.686],[5.66,-1.69],[5.712,-0.738],[0.726,2.404],[-0.726,2.404],[-5.712,-0.738]],\"c\":true}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":30,\"s\":[{\"i\":[[-0.452,0.127],[-1.798,0],[-1.471,-0.414],[0.428,-0.195],[1.333,-1.348],[0.372,0.376],[1.412,0.643]],\"o\":[[1.471,-0.414],[1.798,0],[0.452,0.127],[-1.412,0.643],[-0.372,0.376],[-1.333,-1.348],[-0.428,-0.195]],\"v\":[[-5.66,-1.69],[0,-2.686],[5.66,-1.69],[5.712,-0.738],[0.726,2.821],[-0.726,2.821],[-5.712,-0.738]],\"c\":true}]},{\"t\":40,\"s\":[{\"i\":[[-0.452,0.127],[-1.798,0],[-1.471,-0.414],[0.428,-0.195],[1.333,-1.348],[0.372,0.376],[1.412,0.643]],\"o\":[[1.471,-0.414],[1.798,0],[0.452,0.127],[-1.412,0.643],[-0.372,0.376],[-1.333,-1.348],[-0.428,-0.195]],\"v\":[[-5.66,-1.69],[0,-2.686],[5.66,-1.69],[5.712,-0.738],[0.726,2.404],[-0.726,2.404],[-5.712,-0.738]],\"c\":true}]}],\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Vector 25\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-60,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Ellipse 28\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[19,16.5,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":0,\"s\":[16.667,16.667,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":10,\"s\":[20,5,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":20,\"s\":[16.667,16.667,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":30,\"s\":[20,5,100]},{\"t\":40,\"s\":[16.667,16.667,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[2,2],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Контур эллипса 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Ellipse 28\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-60,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Ellipse 27\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[11,16.5,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":0,\"s\":[16.667,16.667,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":10,\"s\":[20,5,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":20,\"s\":[16.667,16.667,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":30,\"s\":[20,5,100]},{\"t\":40,\"s\":[16.667,16.667,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[2,2],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Контур эллипса 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Ellipse 27\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-60,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Subtract\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[15,12,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,1.422],[-5.523,0],[0,-5.523],[0.535,-1.225],[0,0],[2.761,0],[0.81,-1.668],[1.979,0],[0,-2.761],[0,0]],\"o\":[[0,-5.523],[5.523,0],[0,1.422],[0,0],[0,-2.761],[-1.979,0],[-0.81,-1.668],[-2.761,0],[0,0],[-0.535,-1.225]],\"v\":[[-10,3],[0,-7],[10,3],[9.168,7],[9.5,5],[4.5,0],[0,2.818],[-4.5,0],[-9.5,5],[-9.168,7]],\"c\":true}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":10,\"s\":[{\"i\":[[0,1.422],[-5.523,0],[0,-5.523],[0.535,-1.225],[0,0],[2.761,0],[0.81,-1.668],[1.979,0],[0,-2.761],[0,0]],\"o\":[[0,-5.523],[5.523,0],[0,1.422],[0,0],[0,-2.761],[-1.979,0],[-0.81,-1.668],[-2.761,0],[0,0],[-0.535,-1.225]],\"v\":[[-10,3],[0,-7],[10,3],[9.168,7],[9.5,5],[4.5,1.25],[0,4.068],[-4.5,1.25],[-9.5,5],[-9.168,7]],\"c\":true}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":20,\"s\":[{\"i\":[[0,1.422],[-5.523,0],[0,-5.523],[0.535,-1.225],[0,0],[2.761,0],[0.81,-1.668],[1.979,0],[0,-2.761],[0,0]],\"o\":[[0,-5.523],[5.523,0],[0,1.422],[0,0],[0,-2.761],[-1.979,0],[-0.81,-1.668],[-2.761,0],[0,0],[-0.535,-1.225]],\"v\":[[-10,3],[0,-7],[10,3],[9.168,7],[9.5,5],[4.5,0],[0,2.818],[-4.5,0],[-9.5,5],[-9.168,7]],\"c\":true}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":30,\"s\":[{\"i\":[[0,1.422],[-5.523,0],[0,-5.523],[0.535,-1.225],[0,0],[2.761,0],[0.81,-1.668],[1.979,0],[0,-2.761],[0,0]],\"o\":[[0,-5.523],[5.523,0],[0,1.422],[0,0],[0,-2.761],[-1.979,0],[-0.81,-1.668],[-2.761,0],[0,0],[-0.535,-1.225]],\"v\":[[-10,3],[0,-7],[10,3],[9.168,7],[9.5,5],[4.5,1.25],[0,4.068],[-4.5,1.25],[-9.5,5],[-9.168,7]],\"c\":true}]},{\"t\":40,\"s\":[{\"i\":[[0,1.422],[-5.523,0],[0,-5.523],[0.535,-1.225],[0,0],[2.761,0],[0.81,-1.668],[1.979,0],[0,-2.761],[0,0]],\"o\":[[0,-5.523],[5.523,0],[0,1.422],[0,0],[0,-2.761],[-1.979,0],[-0.81,-1.668],[-2.761,0],[0,0],[-0.535,-1.225]],\"v\":[[-10,3],[0,-7],[10,3],[9.168,7],[9.5,5],[4.5,0],[0,2.818],[-4.5,0],[-9.5,5],[-9.168,7]],\"c\":true}]}],\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Subtract\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-60,\"bm\":0}],\"markers\":[]}"
  },
  {
    "path": "Telegram/Resources/icons/settings/devices/device_linux_ubuntu.lottie",
    "content": "{\"v\":\"5.7.4\",\"fr\":60,\"ip\":0,\"op\":120,\"w\":30,\"h\":30,\"nm\":\"ubuntu_30\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Union\",\"parent\":5,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[13.594,0,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0]],\"v\":[[-4.297,-5.178],[-6.797,-9.508],[-8.234,-8.678],[-5.734,-4.348]],\"c\":true},\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ind\":1,\"ty\":\"sh\",\"ix\":2,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0]],\"v\":[[-8.234,8.678],[-5.734,4.348],[-4.297,5.178],[-6.797,9.508]],\"c\":true},\"ix\":2},\"nm\":\"Контур 2\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ind\":2,\"ty\":\"sh\",\"ix\":3,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,0],[0,0],[0,0]],\"o\":[[0,0],[0,0],[0,0],[0,0]],\"v\":[[3.234,0.83],[8.234,0.83],[8.234,-0.83],[3.234,-0.83]],\"c\":true},\"ix\":2},\"nm\":\"Контур 3\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"mm\",\"mm\":1,\"nm\":\"Объединить контуры 1\",\"mn\":\"ADBE Vector Filter - Merge\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0,0,0,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Union\",\"np\":5,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-60,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Ellipse 24\",\"parent\":5,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,51.962,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":0,\"s\":[-100,-100,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":15,\"s\":[-70,-70,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":20,\"s\":[-130,-130,100]},{\"t\":30,\"s\":[-100,-100,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[4,4],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Контур эллипса 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0,0,0,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":2.66,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Обводка 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":60,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Ellipse 24\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-60,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Ellipse 24\",\"parent\":5,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-60,0,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":0,\"s\":[-100,-100,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":15,\"s\":[-70,-70,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":20,\"s\":[-130,-130,100]},{\"t\":30,\"s\":[-100,-100,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[4,4],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Контур эллипса 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0,0,0,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":2.66,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Обводка 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Ellipse 24\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-60,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Ellipse 25\",\"parent\":5,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,-51.961,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":0,\"s\":[-100,-100,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":15,\"s\":[-70,-70,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":20,\"s\":[-130,-130,100]},{\"t\":30,\"s\":[-100,-100,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[4,4],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Контур эллипса 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0,0,0,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":2.66,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Обводка 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":-60,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Ellipse 24\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-60,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"Ellipse 3\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":0,\"s\":[0]},{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":20,\"s\":[-245]},{\"t\":30,\"s\":[-240]}],\"ix\":10},\"p\":{\"a\":0,\"k\":[15,15,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[16,16],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Контур эллипса 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":4,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Обводка 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Ellipse 3\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-60,\"bm\":0}],\"markers\":[]}"
  },
  {
    "path": "Telegram/Resources/icons/settings/devices/device_phone_android.lottie",
    "content": "{\"v\":\"5.7.4\",\"fr\":60,\"ip\":0,\"op\":120,\"w\":30,\"h\":30,\"nm\":\"android_30\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Eye L\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[10.5,16,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":0,\"s\":[16.667,16.667,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":10,\"s\":[20,5,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":20,\"s\":[16.667,16.667,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":30,\"s\":[20,5,100]},{\"t\":40,\"s\":[16.667,16.667,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[3,3],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Контур эллипса 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0,0,0,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Eye L\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-10,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Eye R\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[19.5,16,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":0,\"s\":[16.667,16.667,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":10,\"s\":[20,5,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":20,\"s\":[16.667,16.667,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":30,\"s\":[20,5,100]},{\"t\":40,\"s\":[16.667,16.667,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[3,3],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Контур эллипса 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0,0,0,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Eye R\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-10,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Face\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[15,15.5,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-0.335,1.25],[-4.973,0],[-1.361,-5.079],[0.382,-0.702],[0.129,-0.123],[1.573,0],[0,0],[0.579,0.551],[0.085,0.157]],\"o\":[[1.361,-5.079],[4.973,0],[0.335,1.25],[-0.085,0.157],[-0.579,0.551],[0,0],[-1.573,0],[-0.129,-0.123],[-0.382,-0.702]],\"v\":[[-10.536,2.379],[0,-6],[10.536,2.379],[10.657,4.957],[10.279,5.449],[7.34,6],[-7.34,6],[-10.279,5.449],[-10.657,4.957]],\"c\":true},\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Face\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-10,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Ri\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[20.75,9.75,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":1,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[1.25,-2.25],[-1.25,2.25]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":10,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[2.5,-1],[-1.25,2.25]],\"c\":false}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":1,\"y\":0},\"t\":20,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[1.25,-2.25],[-1.25,2.25]],\"c\":false}]},{\"i\":{\"x\":0,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":30,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[2.5,-1],[-1.25,2.25]],\"c\":false}]},{\"t\":40,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[1.25,-2.25],[-1.25,2.25]],\"c\":false}]}],\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":1.66,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Обводка 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Ri\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-10,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"Le\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[9.25,9.75,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":1,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-1.25,-2.25],[1.25,2.25]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":10,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-2.5,-1],[1.25,2.25]],\"c\":false}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":1,\"y\":0},\"t\":20,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-1.25,-2.25],[1.25,2.25]],\"c\":false}]},{\"i\":{\"x\":0,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":30,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-2.5,-1],[1.25,2.25]],\"c\":false}]},{\"t\":40,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-1.25,-2.25],[1.25,2.25]],\"c\":false}]}],\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":1.66,\"ix\":5},\"lc\":2,\"lj\":2,\"bm\":0,\"nm\":\"Обводка 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Le\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-10,\"bm\":0}],\"markers\":[]}"
  },
  {
    "path": "Telegram/Resources/icons/settings/devices/device_phone_ios.lottie",
    "content": "{\"v\":\"5.7.4\",\"fr\":60,\"ip\":0,\"op\":120,\"w\":30,\"h\":30,\"nm\":\"iphone_30\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"apple\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[14.841,14.333,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0,0,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":10,\"s\":[0,13.333,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[1,1,0.333],\"y\":[0,0,0]},\"t\":20,\"s\":[15,15,100]},{\"t\":30,\"s\":[0,13.333,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-0.31,0.445],[-0.161,0.375],[-0.056,0.176],[0.222,0.21],[0.009,0.704],[-0.829,0.5],[0.927,0.074],[0.494,-0.176],[0.093,0],[0.431,0.162],[0.264,0],[0.409,-0.246],[0.242,-0.418],[0,-0.737],[-0.236,-0.686],[-0.359,-0.515],[-0.213,-0.185],[-0.334,0.014],[-0.357,0.144],[-0.344,0.006],[-0.307,-0.133],[-0.25,0],[-0.306,0.278]],\"o\":[[0.234,-0.334],[0.07,-0.162],[-0.28,-0.122],[-0.5,-0.463],[-0.009,-0.899],[-0.463,-0.658],[-0.34,-0.028],[-0.524,0.19],[-0.12,0],[-0.431,-0.162],[-0.477,0.005],[-0.414,0.248],[-0.32,0.533],[0,0.644],[0.201,0.594],[0.32,0.449],[0.334,0.31],[0.222,-0.009],[0.317,-0.135],[0.335,0.007],[0.352,0.148],[0.347,-0.009],[0.195,-0.171]],\"v\":[[3.848,4.137],[4.441,3.071],[4.631,2.566],[3.871,2.066],[3.107,0.315],[4.334,-1.789],[2.249,-2.887],[0.998,-2.665],[0.076,-2.377],[-0.753,-2.623],[-1.8,-2.868],[-3.153,-2.484],[-4.154,-1.469],[-4.631,0.435],[-4.274,2.427],[-3.431,4.1],[-2.634,5.054],[-1.633,5.499],[-0.767,5.272],[0.234,5.059],[1.207,5.272],[2.11,5.49],[3.088,5.059]],\"c\":true},\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ind\":1,\"ty\":\"sh\",\"ix\":2,\"ks\":{\"a\":0,\"k\":{\"i\":[[-0.482,0.565],[0,0.551],[0.005,0.074],[0.277,-0.142],[0.217,-0.246],[0,-0.528],[-0.01,-0.069]],\"o\":[[0.403,-0.473],[0,-0.074],[-0.31,0.023],[-0.297,0.139],[-0.408,0.463],[0,0.07],[0.63,0.051]],\"v\":[[1.717,-3.739],[2.319,-5.278],[2.31,-5.5],[1.42,-5.25],[0.642,-4.666],[0.002,-3.109],[0.016,-2.901]],\"c\":true},\"ix\":2},\"nm\":\"Контур 2\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"mm\",\"mm\":1,\"nm\":\"Объединить контуры 1\",\"mn\":\"ADBE Vector Filter - Merge\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0,0,0,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"apple\",\"np\":4,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-120,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Vector 39\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[15,6,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.333,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[0,0],[0,-0.884],[0.221,0],[0,0],[0,0.221],[0.884,0],[0,0]],\"o\":[[0,0],[-0.884,0],[0,0.221],[0,0],[-0.221,0],[0,-0.884],[0,0],[0,0]],\"v\":[[4,-1],[3.6,-1],[2,0.6],[1.6,1],[-1.6,1],[-2,0.6],[-3.6,-1],[-4,-1]],\"c\":false}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":7,\"s\":[{\"i\":[[0,0],[0,0],[0,-0.884],[0.028,0],[0,0],[0,0.221],[0.11,0],[0,0]],\"o\":[[0,0],[-0.11,0],[0,0.221],[0,0],[-0.028,0],[0,-0.884],[0,0],[0,0]],\"v\":[[0.5,-1],[0.45,-1],[0.25,0.6],[0.2,1],[-0.2,1],[-0.25,0.6],[-0.45,-1],[-0.5,-1]],\"c\":false}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":33,\"s\":[{\"i\":[[0,0],[0,0],[0,-0.884],[0.028,0],[0,0],[0,0.221],[0.11,0],[0,0]],\"o\":[[0,0],[-0.11,0],[0,0.221],[0,0],[-0.028,0],[0,-0.884],[0,0],[0,0]],\"v\":[[0.5,-1],[0.45,-1],[0.25,0.6],[0.2,1],[-0.2,1],[-0.25,0.6],[-0.45,-1],[-0.5,-1]],\"c\":false}]},{\"t\":40,\"s\":[{\"i\":[[0,0],[0,0],[0,-0.884],[0.221,0],[0,0],[0,0.221],[0.884,0],[0,0]],\"o\":[[0,0],[-0.884,0],[0,0.221],[0,0],[-0.221,0],[0,-0.884],[0,0],[0,0]],\"v\":[[4,-1],[3.6,-1],[2,0.6],[1.6,1],[-1.6,1],[-2,0.6],[-3.6,-1],[-4,-1]],\"c\":false}]}],\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":2,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Обводка 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Vector 39\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-120,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Rectangle 21\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[15,15,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.333,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0.192,-0.376],[0,-1.12],[0,0],[-0.218,-0.428],[-0.376,-0.192],[-1.12,0],[0,0],[-0.428,0.218],[-0.192,0.376],[0,1.12],[0,0],[0.218,0.428],[0.376,0.192],[1.12,0],[0,0],[0.428,-0.218]],\"o\":[[-0.218,0.428],[0,0],[0,1.12],[0.192,0.376],[0.428,0.218],[0,0],[1.12,0],[0.376,-0.192],[0.218,-0.428],[0,0],[0,-1.12],[-0.192,-0.376],[-0.428,-0.218],[0,0],[-1.12,0],[-0.376,0.192]],\"v\":[[-5.782,-8.908],[-6,-6.8],[-6,6.8],[-5.782,8.908],[-4.908,9.782],[-2.8,10],[2.8,10],[4.908,9.782],[5.782,8.908],[6,6.8],[6,-6.8],[5.782,-8.908],[4.908,-9.782],[2.8,-10],[-2.8,-10],[-4.908,-9.782]],\"c\":true}]},{\"i\":{\"x\":0,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":10,\"s\":[{\"i\":[[0.016,-0.376],[0,-1.12],[0,0],[-0.018,-0.428],[-0.031,-0.192],[-0.093,0],[0,0],[-0.036,0.218],[-0.016,0.376],[0,1.12],[0,0],[0.018,0.428],[0.031,0.192],[0.093,0],[0,0],[0.036,-0.218]],\"o\":[[-0.018,0.428],[0,0],[0,1.12],[0.016,0.376],[0.036,0.218],[0,0],[0.093,0],[0.031,-0.192],[0.018,-0.428],[0,0],[0,-1.12],[-0.016,-0.376],[-0.036,-0.218],[0,0],[-0.093,0],[-0.031,0.192]],\"v\":[[-0.482,-8.908],[-0.5,-6.8],[-0.5,6.8],[-0.482,8.908],[-0.409,9.782],[-0.233,10],[0.233,10],[0.409,9.782],[0.482,8.908],[0.5,6.8],[0.5,-6.8],[0.482,-8.908],[0.409,-9.782],[0.233,-10],[-0.233,-10],[-0.409,-9.782]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":1,\"y\":0},\"t\":20,\"s\":[{\"i\":[[0.192,-0.376],[0,-1.12],[0,0],[-0.218,-0.428],[-0.376,-0.192],[-1.12,0],[0,0],[-0.428,0.218],[-0.192,0.376],[0,1.12],[0,0],[0.218,0.428],[0.376,0.192],[1.12,0],[0,0],[0.428,-0.218]],\"o\":[[-0.218,0.428],[0,0],[0,1.12],[0.192,0.376],[0.428,0.218],[0,0],[1.12,0],[0.376,-0.192],[0.218,-0.428],[0,0],[0,-1.12],[-0.192,-0.376],[-0.428,-0.218],[0,0],[-1.12,0],[-0.376,0.192]],\"v\":[[-5.782,-8.908],[-6,-6.8],[-6,6.8],[-5.782,8.908],[-4.908,9.782],[-2.8,10],[2.8,10],[4.908,9.782],[5.782,8.908],[6,6.8],[6,-6.8],[5.782,-8.908],[4.908,-9.782],[2.8,-10],[-2.8,-10],[-4.908,-9.782]],\"c\":true}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":30,\"s\":[{\"i\":[[0.016,-0.376],[0,-1.12],[0,0],[-0.018,-0.428],[-0.031,-0.192],[-0.093,0],[0,0],[-0.036,0.218],[-0.016,0.376],[0,1.12],[0,0],[0.018,0.428],[0.031,0.192],[0.093,0],[0,0],[0.036,-0.218]],\"o\":[[-0.018,0.428],[0,0],[0,1.12],[0.016,0.376],[0.036,0.218],[0,0],[0.093,0],[0.031,-0.192],[0.018,-0.428],[0,0],[0,-1.12],[-0.016,-0.376],[-0.036,-0.218],[0,0],[-0.093,0],[-0.031,0.192]],\"v\":[[-0.482,-8.908],[-0.5,-6.8],[-0.5,6.8],[-0.482,8.908],[-0.409,9.782],[-0.233,10],[0.233,10],[0.409,9.782],[0.482,8.908],[0.5,6.8],[0.5,-6.8],[0.482,-8.908],[0.409,-9.782],[0.233,-10],[-0.233,-10],[-0.409,-9.782]],\"c\":true}]},{\"t\":40,\"s\":[{\"i\":[[0.192,-0.376],[0,-1.12],[0,0],[-0.218,-0.428],[-0.376,-0.192],[-1.12,0],[0,0],[-0.428,0.218],[-0.192,0.376],[0,1.12],[0,0],[0.218,0.428],[0.376,0.192],[1.12,0],[0,0],[0.428,-0.218]],\"o\":[[-0.218,0.428],[0,0],[0,1.12],[0.192,0.376],[0.428,0.218],[0,0],[1.12,0],[0.376,-0.192],[0.218,-0.428],[0,0],[0,-1.12],[-0.192,-0.376],[-0.428,-0.218],[0,0],[-1.12,0],[-0.376,0.192]],\"v\":[[-5.782,-8.908],[-6,-6.8],[-6,6.8],[-5.782,8.908],[-4.908,9.782],[-2.8,10],[2.8,10],[4.908,9.782],[5.782,8.908],[6,6.8],[6,-6.8],[5.782,-8.908],[4.908,-9.782],[2.8,-10],[-2.8,-10],[-4.908,-9.782]],\"c\":true}]}],\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"rd\",\"nm\":\"Скругленные углы 1\",\"r\":{\"a\":0,\"k\":2,\"ix\":1},\"ix\":2,\"mn\":\"ADBE Vector Filter - RC\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":2,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Обводка 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"t\":10,\"s\":[0]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"t\":11,\"s\":[100]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"t\":29,\"s\":[100]},{\"t\":30,\"s\":[0]}],\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Rectangle 21\",\"np\":4,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-120,\"bm\":0}],\"markers\":[]}"
  },
  {
    "path": "Telegram/Resources/icons/settings/devices/device_tablet_ios.lottie",
    "content": "{\"v\":\"5.7.4\",\"fr\":60,\"ip\":0,\"op\":120,\"w\":30,\"h\":30,\"nm\":\"ipad_30\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"apple\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[14.841,14.333,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0,0,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.167,0.167,0.167],\"y\":[0.167,0.167,0]},\"t\":10,\"s\":[0,13.333,100]},{\"i\":{\"x\":[0.833,0.833,0.833],\"y\":[0.833,0.833,1]},\"o\":{\"x\":[1,1,0.333],\"y\":[0,0,0]},\"t\":20,\"s\":[15,15,100]},{\"t\":30,\"s\":[0,13.333,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-0.31,0.445],[-0.161,0.375],[-0.056,0.176],[0.222,0.21],[0.009,0.704],[-0.829,0.5],[0.927,0.074],[0.494,-0.176],[0.093,0],[0.431,0.162],[0.264,0],[0.409,-0.246],[0.242,-0.418],[0,-0.737],[-0.236,-0.686],[-0.359,-0.515],[-0.213,-0.185],[-0.334,0.014],[-0.357,0.144],[-0.344,0.006],[-0.307,-0.133],[-0.25,0],[-0.306,0.278]],\"o\":[[0.234,-0.334],[0.07,-0.162],[-0.28,-0.122],[-0.5,-0.463],[-0.009,-0.899],[-0.463,-0.658],[-0.34,-0.028],[-0.524,0.19],[-0.12,0],[-0.431,-0.162],[-0.477,0.005],[-0.414,0.248],[-0.32,0.533],[0,0.644],[0.201,0.594],[0.32,0.449],[0.334,0.31],[0.222,-0.009],[0.317,-0.135],[0.335,0.007],[0.352,0.148],[0.347,-0.009],[0.195,-0.171]],\"v\":[[3.848,4.137],[4.441,3.071],[4.631,2.566],[3.871,2.066],[3.107,0.315],[4.334,-1.789],[2.249,-2.887],[0.998,-2.665],[0.076,-2.377],[-0.753,-2.623],[-1.8,-2.868],[-3.153,-2.484],[-4.154,-1.469],[-4.631,0.435],[-4.274,2.427],[-3.431,4.1],[-2.634,5.054],[-1.633,5.499],[-0.767,5.272],[0.234,5.059],[1.207,5.272],[2.11,5.49],[3.088,5.059]],\"c\":true},\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ind\":1,\"ty\":\"sh\",\"ix\":2,\"ks\":{\"a\":0,\"k\":{\"i\":[[-0.482,0.565],[0,0.551],[0.005,0.074],[0.277,-0.142],[0.217,-0.246],[0,-0.528],[-0.01,-0.069]],\"o\":[[0.403,-0.473],[0,-0.074],[-0.31,0.023],[-0.297,0.139],[-0.408,0.463],[0,0.07],[0.63,0.051]],\"v\":[[1.717,-3.739],[2.319,-5.278],[2.31,-5.5],[1.42,-5.25],[0.642,-4.666],[0.002,-3.109],[0.016,-2.901]],\"c\":true},\"ix\":2},\"nm\":\"Контур 2\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"mm\",\"mm\":1,\"nm\":\"Объединить контуры 1\",\"mn\":\"ADBE Vector Filter - Merge\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0,0,0,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"apple\",\"np\":4,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-120,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Rectangle 21\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[15,15,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.333,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0.192,-0.376],[0,-1.12],[0,0],[-0.218,-0.428],[-0.376,-0.192],[-1.12,0],[0,0],[-0.428,0.218],[-0.192,0.376],[0,1.12],[0,0],[0.218,0.428],[0.376,0.192],[1.12,0],[0,0],[0.428,-0.218]],\"o\":[[-0.218,0.428],[0,0],[0,1.12],[0.192,0.376],[0.428,0.218],[0,0],[1.12,0],[0.376,-0.192],[0.218,-0.428],[0,0],[0,-1.12],[-0.192,-0.376],[-0.428,-0.218],[0,0],[-1.12,0],[-0.376,0.192]],\"v\":[[-7.782,-8.908],[-8,-6.8],[-8,6.8],[-7.782,8.908],[-6.908,9.782],[-4.8,10],[4.8,10],[6.908,9.782],[7.782,8.908],[8,6.8],[8,-6.8],[7.782,-8.908],[6.908,-9.782],[4.8,-10],[-4.8,-10],[-6.908,-9.782]],\"c\":true}]},{\"i\":{\"x\":0,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":10,\"s\":[{\"i\":[[0.012,-0.376],[0,-1.12],[0,0],[-0.014,-0.428],[-0.024,-0.192],[-0.07,0],[0,0],[-0.027,0.218],[-0.012,0.376],[0,1.12],[0,0],[0.014,0.428],[0.024,0.192],[0.07,0],[0,0],[0.027,-0.218]],\"o\":[[-0.014,0.428],[0,0],[0,1.12],[0.012,0.376],[0.027,0.218],[0,0],[0.07,0],[0.024,-0.192],[0.014,-0.428],[0,0],[0,-1.12],[-0.012,-0.376],[-0.027,-0.218],[0,0],[-0.07,0],[-0.024,0.192]],\"v\":[[-0.486,-8.908],[-0.5,-6.8],[-0.5,6.8],[-0.486,8.908],[-0.432,9.782],[-0.3,10],[0.3,10],[0.432,9.782],[0.486,8.908],[0.5,6.8],[0.5,-6.8],[0.486,-8.908],[0.432,-9.782],[0.3,-10],[-0.3,-10],[-0.432,-9.782]],\"c\":true}]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":1,\"y\":0},\"t\":20,\"s\":[{\"i\":[[0.192,-0.376],[0,-1.12],[0,0],[-0.218,-0.428],[-0.376,-0.192],[-1.12,0],[0,0],[-0.428,0.218],[-0.192,0.376],[0,1.12],[0,0],[0.218,0.428],[0.376,0.192],[1.12,0],[0,0],[0.428,-0.218]],\"o\":[[-0.218,0.428],[0,0],[0,1.12],[0.192,0.376],[0.428,0.218],[0,0],[1.12,0],[0.376,-0.192],[0.218,-0.428],[0,0],[0,-1.12],[-0.192,-0.376],[-0.428,-0.218],[0,0],[-1.12,0],[-0.376,0.192]],\"v\":[[-7.782,-8.908],[-8,-6.8],[-8,6.8],[-7.782,8.908],[-6.908,9.782],[-4.8,10],[4.8,10],[6.908,9.782],[7.782,8.908],[8,6.8],[8,-6.8],[7.782,-8.908],[6.908,-9.782],[4.8,-10],[-4.8,-10],[-6.908,-9.782]],\"c\":true}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":30,\"s\":[{\"i\":[[0.012,-0.376],[0,-1.12],[0,0],[-0.014,-0.428],[-0.024,-0.192],[-0.07,0],[0,0],[-0.027,0.218],[-0.012,0.376],[0,1.12],[0,0],[0.014,0.428],[0.024,0.192],[0.07,0],[0,0],[0.027,-0.218]],\"o\":[[-0.014,0.428],[0,0],[0,1.12],[0.012,0.376],[0.027,0.218],[0,0],[0.07,0],[0.024,-0.192],[0.014,-0.428],[0,0],[0,-1.12],[-0.012,-0.376],[-0.027,-0.218],[0,0],[-0.07,0],[-0.024,0.192]],\"v\":[[-0.486,-8.908],[-0.5,-6.8],[-0.5,6.8],[-0.486,8.908],[-0.432,9.782],[-0.3,10],[0.3,10],[0.432,9.782],[0.486,8.908],[0.5,6.8],[0.5,-6.8],[0.486,-8.908],[0.432,-9.782],[0.3,-10],[-0.3,-10],[-0.432,-9.782]],\"c\":true}]},{\"t\":40,\"s\":[{\"i\":[[0.192,-0.376],[0,-1.12],[0,0],[-0.218,-0.428],[-0.376,-0.192],[-1.12,0],[0,0],[-0.428,0.218],[-0.192,0.376],[0,1.12],[0,0],[0.218,0.428],[0.376,0.192],[1.12,0],[0,0],[0.428,-0.218]],\"o\":[[-0.218,0.428],[0,0],[0,1.12],[0.192,0.376],[0.428,0.218],[0,0],[1.12,0],[0.376,-0.192],[0.218,-0.428],[0,0],[0,-1.12],[-0.192,-0.376],[-0.428,-0.218],[0,0],[-1.12,0],[-0.376,0.192]],\"v\":[[-7.782,-8.908],[-8,-6.8],[-8,6.8],[-7.782,8.908],[-6.908,9.782],[-4.8,10],[4.8,10],[6.908,9.782],[7.782,8.908],[8,6.8],[8,-6.8],[7.782,-8.908],[6.908,-9.782],[4.8,-10],[-4.8,-10],[-6.908,-9.782]],\"c\":true}]}],\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":2,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Обводка 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"t\":10,\"s\":[0]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"t\":11,\"s\":[100]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"t\":29,\"s\":[100]},{\"t\":30,\"s\":[0]}],\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Rectangle 21\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-120,\"bm\":0}],\"markers\":[]}"
  },
  {
    "path": "Telegram/Resources/icons/settings/devices/device_web_chrome.lottie",
    "content": "{\"v\":\"5.7.4\",\"fr\":60,\"ip\":0,\"op\":120,\"w\":30,\"h\":30,\"nm\":\"chrome_30\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Vector 20\",\"parent\":5,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-38.383,-12.48,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[-100,-100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.333,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[5,0],[-5,0]],\"c\":false}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":10,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[5,0],[-5.252,2.02]],\"c\":false}]},{\"t\":20,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[5,0],[-5,0]],\"c\":false}]}],\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0,0,0,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":1.66,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Обводка 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":60,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Vector 20\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-10,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Vector 20\",\"parent\":5,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[8.383,39.481,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[-100,-100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.333,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[5,0],[-5,0]],\"c\":false}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":10,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[5,0],[-5.879,2.144]],\"c\":false}]},{\"t\":20,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[5,0],[-5,0]],\"c\":false}]}],\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0,0,0,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":1.66,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Обводка 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":-60,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Vector 20\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-10,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Vector 20\",\"parent\":5,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[30,-27,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.333,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[5,0],[-5,0]],\"c\":false}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"t\":10,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[5,0],[-5.333,2]],\"c\":false}]},{\"t\":20,\"s\":[{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[5,0],[-5,0]],\"c\":false}]}],\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0,0,0,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":1.66,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Обводка 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Vector 20\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-10,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Ellipse 18\",\"parent\":5,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[0,0,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.833,0.833],\"y\":[0.833,0.833]},\"o\":{\"x\":[0.333,0.333],\"y\":[0,0]},\"t\":0,\"s\":[9,9]},{\"i\":{\"x\":[0.667,0.667],\"y\":[1,1]},\"o\":{\"x\":[0.167,0.167],\"y\":[0.167,0.167]},\"t\":10,\"s\":[5,5]},{\"t\":20,\"s\":[9,9]}],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Контур эллипса 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0,0,0,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":1.66,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Обводка 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Ellipse 18\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-10,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"Ellipse 3\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":0,\"s\":[0]},{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":20,\"s\":[-245]},{\"t\":30,\"s\":[-240]}],\"ix\":10},\"p\":{\"a\":0,\"k\":[15,15,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[20,20],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Контур эллипса 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Ellipse 3\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-10,\"bm\":0}],\"markers\":[]}"
  },
  {
    "path": "Telegram/Resources/icons/settings/devices/device_web_edge.lottie",
    "content": "{\"v\":\"5.7.4\",\"fr\":60,\"ip\":0,\"op\":120,\"w\":30,\"h\":30,\"nm\":\"edge_30\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Union\",\"parent\":2,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[0,0.003,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":0,\"s\":[{\"i\":[[-0.082,0.078],[-0.014,0.019],[0,0.42],[0.001,0.035],[0,0],[0,0],[0,0.009],[0.002,0.03],[0.293,0.391],[0.348,0.176],[0.39,0.003],[0.247,-0.117],[0,0],[0,0],[0.009,-0.004],[0.226,-0.375],[0.005,-0.438],[-3.697,0],[-0.897,0.336],[-0.267,0.139],[-0.069,-0.011],[-0.048,-0.051],[-0.007,-0.069],[0.037,-0.059],[2.152,-0.744],[0,0],[0.253,-0.054],[-0.228,0.072],[2.252,0.966],[1.04,2.219],[-0.014,1.51],[-0.03,0.216],[0.003,-0.209],[-1.868,1.842],[-2.628,0],[-1.655,-3.238],[-0.012,-0.024],[-0.003,-0.006],[0.019,-1.66],[0.417,-0.725],[0.722,-0.422],[0.832,-0.002],[0.001,0],[1.105,0.768],[0,0.204]],\"o\":[[0.024,-0.023],[0.395,-0.514],[0,-0.029],[0,0],[0,0],[0,-0.009],[-0.001,-0.027],[-0.029,-0.486],[-0.233,-0.313],[-0.348,-0.176],[-0.533,-0.01],[0,0],[0,0],[-0.009,0.004],[-0.384,0.209],[-0.226,0.375],[0,3.263],[0.958,0.002],[0.282,-0.106],[0.061,-0.034],[0.069,0.011],[0.048,0.051],[0.007,0.069],[-1.215,1.926],[0,0],[-0.193,0.061],[0.234,-0.044],[-2.325,0.775],[-2.252,-0.966],[-0.636,-1.37],[0,-0.213],[-0.033,0.203],[0.041,-2.622],[1.871,-1.845],[3.886,0],[0.011,0.022],[0.003,0.006],[0.283,0.552],[-0.002,0.836],[-0.417,0.725],[-0.716,0.425],[0,0],[-0.073,0.003],[-0.236,-0.165],[0,-0.191]],\"v\":[[1.84,1.691],[1.9,1.628],[2.509,0.073],[2.508,-0.023],[2.508,-0.022],[2.508,-0.021],[2.507,-0.048],[2.504,-0.134],[2.011,-1.479],[1.127,-2.223],[0.004,-2.495],[-1.181,-2.196],[-1.181,-2.196],[-1.183,-2.195],[-1.21,-2.182],[-2.142,-1.29],[-2.493,-0.049],[4.423,5.757],[7.228,5.252],[8.051,4.885],[8.251,4.849],[8.43,4.943],[8.514,5.128],[8.467,5.325],[3.264,9.45],[3.162,9.484],[2.478,9.66],[3.171,9.487],[-3.935,9.19],[-9.045,4.245],[-9.991,-0.13],[-9.945,-0.774],[-10,-0.156],[-7.022,-7.122],[-0.001,-10.001],[9.114,-4.9],[9.149,-4.832],[9.157,-4.815],[10,-1.47],[9.361,0.912],[7.622,2.661],[5.258,3.314],[5.256,3.314],[2.011,2.635],[1.642,2.071]],\"c\":true}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":40,\"s\":[{\"i\":[[-0.082,0.078],[-0.014,0.019],[0,0.42],[0.001,0.035],[0,0],[0,0],[0,0.009],[0.002,0.03],[0.293,0.391],[0.348,0.176],[0.39,0.003],[0.247,-0.117],[0,0],[0,0],[0.009,-0.004],[0.226,-0.375],[0.005,-0.438],[-3.279,0.493],[-0.897,0.336],[-0.267,0.139],[-0.069,-0.011],[-0.048,-0.051],[-0.007,-0.069],[0.037,-0.059],[2.152,-0.744],[0,0],[0.253,-0.054],[-0.228,0.072],[2.252,0.966],[1.04,2.219],[-0.014,1.51],[-0.03,0.216],[0.003,-0.209],[-1.868,1.842],[-2.628,0],[-1.655,-3.238],[-0.012,-0.024],[-0.003,-0.006],[0.019,-1.66],[0.417,-0.725],[0.722,-0.422],[0.832,-0.002],[0.001,0],[1.105,0.768],[0,0.204]],\"o\":[[0.024,-0.023],[0.395,-0.514],[0,-0.029],[0,0],[0,0],[0,-0.009],[-0.001,-0.027],[-0.029,-0.486],[-0.233,-0.313],[-0.348,-0.176],[-0.533,-0.01],[0,0],[0,0],[-0.009,0.004],[-0.384,0.209],[-0.226,0.375],[0,3.263],[1.152,-0.173],[0.282,-0.106],[0.061,-0.034],[0.069,0.011],[0.048,0.051],[0.007,0.069],[-1.215,1.926],[0,0],[-0.193,0.061],[0.234,-0.044],[-2.325,0.775],[-2.252,-0.966],[-0.636,-1.37],[0,-0.213],[-0.033,0.203],[0.041,-2.622],[1.871,-1.845],[3.886,0],[0.011,0.022],[0.003,0.006],[0.283,0.552],[-0.002,0.836],[-0.417,0.725],[-0.716,0.425],[0,0],[-0.073,0.003],[-0.236,-0.165],[0,-0.191]],\"v\":[[1.319,2.836],[1.379,2.774],[2.509,0.073],[2.508,-0.023],[2.508,-0.022],[2.508,-0.021],[2.507,-0.048],[2.504,-0.134],[2.011,-1.479],[1.127,-2.223],[0.004,-2.495],[-1.181,-2.196],[-1.181,-2.196],[-1.183,-2.195],[-1.21,-2.182],[-2.142,-1.29],[-2.493,-0.049],[4.423,5.757],[6.189,5.293],[7.114,4.78],[7.313,4.745],[7.493,4.839],[7.577,5.024],[7.529,5.221],[3.264,9.45],[3.162,9.484],[2.478,9.66],[3.171,9.487],[-3.935,9.19],[-9.045,4.245],[-9.991,-0.13],[-9.945,-0.774],[-10,-0.156],[-7.022,-7.122],[-0.001,-10.001],[9.114,-4.9],[9.149,-4.832],[9.157,-4.815],[10,-1.47],[8.84,2.058],[7.102,3.807],[4.737,4.459],[4.735,4.459],[1.49,3.781],[1.121,3.216]],\"c\":true}]},{\"t\":50,\"s\":[{\"i\":[[-0.082,0.078],[-0.014,0.019],[0,0.42],[0.001,0.035],[0,0],[0,0],[0,0.009],[0.002,0.03],[0.293,0.391],[0.348,0.176],[0.39,0.003],[0.247,-0.117],[0,0],[0,0],[0.009,-0.004],[0.226,-0.375],[0.005,-0.438],[-3.697,0],[-0.897,0.336],[-0.267,0.139],[-0.069,-0.011],[-0.048,-0.051],[-0.007,-0.069],[0.037,-0.059],[2.152,-0.744],[0,0],[0.253,-0.054],[-0.228,0.072],[2.252,0.966],[1.04,2.219],[-0.014,1.51],[-0.03,0.216],[0.003,-0.209],[-1.868,1.842],[-2.628,0],[-1.655,-3.238],[-0.012,-0.024],[-0.003,-0.006],[0.019,-1.66],[0.417,-0.725],[0.722,-0.422],[0.832,-0.002],[0.001,0],[1.105,0.768],[0,0.204]],\"o\":[[0.024,-0.023],[0.395,-0.514],[0,-0.029],[0,0],[0,0],[0,-0.009],[-0.001,-0.027],[-0.029,-0.486],[-0.233,-0.313],[-0.348,-0.176],[-0.533,-0.01],[0,0],[0,0],[-0.009,0.004],[-0.384,0.209],[-0.226,0.375],[0,3.263],[0.958,0.002],[0.282,-0.106],[0.061,-0.034],[0.069,0.011],[0.048,0.051],[0.007,0.069],[-1.215,1.926],[0,0],[-0.193,0.061],[0.234,-0.044],[-2.325,0.775],[-2.252,-0.966],[-0.636,-1.37],[0,-0.213],[-0.033,0.203],[0.041,-2.622],[1.871,-1.845],[3.886,0],[0.011,0.022],[0.003,0.006],[0.283,0.552],[-0.002,0.836],[-0.417,0.725],[-0.716,0.425],[0,0],[-0.073,0.003],[-0.236,-0.165],[0,-0.191]],\"v\":[[1.84,1.691],[1.9,1.628],[2.509,0.073],[2.508,-0.023],[2.508,-0.022],[2.508,-0.021],[2.507,-0.048],[2.504,-0.134],[2.011,-1.479],[1.127,-2.223],[0.004,-2.495],[-1.181,-2.196],[-1.181,-2.196],[-1.183,-2.195],[-1.21,-2.182],[-2.142,-1.29],[-2.493,-0.049],[4.423,5.757],[7.228,5.252],[8.051,4.885],[8.251,4.849],[8.43,4.943],[8.514,5.128],[8.467,5.325],[3.264,9.45],[3.162,9.484],[2.478,9.66],[3.171,9.487],[-3.935,9.19],[-9.045,4.245],[-9.991,-0.13],[-9.945,-0.774],[-10,-0.156],[-7.022,-7.122],[-0.001,-10.001],[9.114,-4.9],[9.149,-4.832],[9.157,-4.815],[10,-1.47],[9.361,0.912],[7.622,2.661],[5.258,3.314],[5.256,3.314],[2.011,2.635],[1.642,2.071]],\"c\":true}]}],\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ind\":1,\"ty\":\"sh\",\"ix\":2,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[0,0.005]],\"o\":[[0,-0.005],[0,0]],\"v\":[[-10,-0.142],[-10,-0.156]],\"c\":false},\"ix\":2},\"nm\":\"Контур 2\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"mm\",\"mm\":1,\"nm\":\"Объединить контуры 1\",\"mn\":\"ADBE Vector Filter - Merge\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Union\",\"np\":4,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-60,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":3,\"nm\":\"Ellipse 3\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":0,\"s\":[0]},{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":40,\"s\":[1090]},{\"t\":50,\"s\":[1080]}],\"ix\":10},\"p\":{\"a\":0,\"k\":[15,15,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":0,\"s\":[16.667,16.667,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":20,\"s\":[11.667,11.667,100]},{\"t\":40,\"s\":[16.667,16.667,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"ip\":0,\"op\":120,\"st\":-60,\"bm\":0}],\"markers\":[]}"
  },
  {
    "path": "Telegram/Resources/icons/settings/devices/device_web_firefox.lottie",
    "content": "{\"v\":\"5.7.4\",\"fr\":60,\"ip\":0,\"op\":120,\"w\":30,\"h\":30,\"nm\":\"firefox_30\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Vector\",\"parent\":2,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[-0.974,-1.629,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":0,\"s\":[{\"i\":[[0,0],[-0.03,-0.196],[0.001,-0.315],[0,-0.044],[0.784,-1.26],[0.037,-0.059],[0.129,-0.308],[0.711,-0.509],[0.55,-0.259],[0,0],[0,0],[0.07,-0.029],[0.18,-0.163],[3.652,0],[0.988,0.296],[-0.036,-0.014],[0.11,0.04],[0.723,0.489],[0.845,1.898],[-0.075,1.505],[0.08,-0.373],[-0.467,1.186],[0.175,-0.384],[-0.528,0.892],[-0.21,0.229],[0,0],[-0.018,0.121],[-0.293,0.65],[0,0],[0,-0.031],[-0.004,0.014],[-0.041,0.083],[-0.067,0.076],[-0.005,0.007],[0.006,-0.047],[0,0.004],[-0.041,0.093],[-0.224,0.154],[0,0],[-0.14,-0.364],[0,0],[-0.016,0],[0,-0.005],[-0.039,-0.068],[-0.052,-0.077],[0,0],[0,0],[-0.157,-0.134],[0,0],[-0.764,-0.289],[-0.118,0.088],[0.005,-0.039],[-0.257,0.109],[-0.001,-0.033],[-0.368,0.071],[0.068,-0.058],[-0.176,0.009],[0.062,-0.026],[0.117,-0.057],[0.035,-0.028],[0.261,-0.684],[0,0],[0,0],[-0.485,-0.077],[-0.041,-0.152],[0,0],[0.006,-0.04],[0.233,-0.198],[0.07,-0.045],[0.28,-0.123],[0.316,-0.196],[0.045,-0.044],[0,-0.005],[-0.011,-0.056],[-0.036,-0.068],[0.009,-0.115],[0.083,-0.192],[0.101,0.051],[0,0],[-0.046,-0.118],[0,0],[0.09,0.05],[0.113,0.049],[0.012,-0.004],[0,0],[0.129,0.064],[0.093,-0.166],[-0.179,-0.324],[-0.259,-0.177],[-0.015,-0.012],[-0.218,-0.109],[-0.068,-0.039],[-0.064,-0.034],[-0.763,0.111],[-0.451,-0.592],[0.366,0.116],[0,0],[0.762,-0.423],[0.818,0.173],[0.199,0.069],[0,0],[0,0],[-0.562,-0.34],[-0.416,-0.078],[-1.148,0.914],[-0.001,1.467],[0.272,0.642],[-0.488,-0.931],[1.067,0.88],[0.381,1.005],[0,0],[-1.363,-1.273],[-0.526,-0.531],[-0.141,0.387],[-0.334,-0.559],[-0.386,-0.63],[-0.079,-0.119],[-0.165,-0.347],[-0.16,-0.563],[-0.039,-0.466]],\"o\":[[0,0],[0.055,0.31],[0,0.04],[-0.062,1.483],[-0.04,0.067],[0.117,0.313],[-0.218,0.847],[-0.474,0.382],[0,0],[0,0],[-0.072,0.033],[-0.104,0.22],[-0.111,0.139],[-1.032,-0.001],[0.036,0.016],[-0.114,-0.038],[-0.834,-0.257],[-1.778,-1.076],[-0.63,-1.369],[-0.123,0.361],[0,-1.275],[-0.241,0.346],[0.247,-1.007],[0.158,-0.267],[0,0],[-0.004,-0.122],[0.065,-0.71],[0,0],[0.006,-0.012],[0,0.031],[0.026,-0.089],[0.045,-0.091],[0.005,-0.007],[0.005,-0.007],[0,0.017],[0.027,-0.051],[0.09,-0.257],[0,0],[-0.013,0.39],[0,0],[0.014,0.02],[0.003,0.001],[0.04,0.082],[0.055,0.097],[0,0],[0,0],[0.115,0.18],[0,0],[0.797,-0.161],[0.095,-0.112],[0,0.039],[0.171,-0.221],[-0.014,0.03],[0.323,-0.19],[0.048,0],[0.162,-0.07],[0.288,-0.03],[-0.122,0.044],[-0.035,0.029],[-0.613,0.401],[0,0],[0,0],[0.108,0.479],[1.373,0.129],[0,0],[-0.002,0.04],[-0.056,0.301],[-0.062,0.056],[-0.047,0.027],[-0.344,0.141],[-0.053,0.035],[-0.016,0.017],[0.025,0.051],[-0.063,-0.071],[0.04,0.108],[0.007,0.209],[-0.091,-0.068],[0,0],[0.106,0.069],[0.034,0.095],[-0.059,-0.084],[-0.132,-0.078],[-0.012,0.007],[0.032,0.057],[0,0],[-0.171,0.083],[-0.151,0.338],[0.174,0.261],[0.016,0.012],[0.2,0.14],[0.066,0.044],[0.061,0.04],[1,0.525],[0.733,-0.129],[0.261,0.371],[0,0],[-0.374,-0.124],[-0.755,0.359],[-0.207,-0.04],[0,0],[0,0],[0.383,0.533],[0.377,0.192],[1.431,0.327],[1.148,-0.914],[0.004,-0.698],[0.904,0.537],[-0.606,-1.77],[-0.858,-0.646],[-1.103,-2.96],[0,0],[0.276,0.257],[0.074,-0.405],[0.11,0.642],[0.512,0.728],[0.083,0.112],[0.221,0.314],[0.262,0.523],[0.128,0.45],[0.127,-0.167]],\"v\":[[10.333,-1.238],[10.393,-0.927],[10.475,0.011],[10.475,0.135],[9.184,4.32],[9.068,4.509],[9.048,5.476],[7.611,7.575],[6.069,8.54],[5.987,8.579],[5.942,8.599],[5.729,8.692],[5.298,9.271],[0.329,10.801],[-2.718,10.353],[-2.614,10.398],[-2.948,10.281],[-5.296,9.156],[-9.325,4.587],[-10.17,0.211],[-10.475,1.314],[-9.767,-2.41],[-10.394,-1.312],[-9.224,-4.176],[-8.671,-4.921],[-8.671,-4.932],[-8.65,-5.296],[-8.109,-7.351],[-8.094,-7.38],[-8.094,-7.305],[-8.094,-7.261],[-7.992,-7.52],[-7.822,-7.772],[-7.809,-7.791],[-7.831,-7.656],[-7.831,-7.632],[-7.736,-7.827],[-7.253,-8.459],[-7.242,-8.464],[-7.05,-7.322],[-7.05,-7.318],[-7.009,-7.374],[-7,-7.366],[-6.881,-7.137],[-6.721,-6.875],[-6.708,-6.86],[-6.697,-6.863],[-6.282,-6.389],[-6.275,-6.383],[-3.888,-6.184],[-3.568,-6.485],[-3.575,-6.367],[-2.922,-6.871],[-2.942,-6.776],[-1.898,-7.171],[-2.121,-7.035],[-1.609,-7.156],[-1.058,-7.036],[-1.416,-6.884],[-1.311,-6.863],[-2.655,-5.193],[-2.655,-5.182],[-2.655,-5.188],[-1.674,-4.268],[0.003,-3.872],[0.003,-3.801],[-0.009,-3.681],[-0.456,-2.909],[-0.654,-2.756],[-1.214,-2.513],[-2.206,-2.008],[-2.352,-1.889],[-2.404,-1.82],[-2.351,-1.658],[-2.434,-1.635],[-2.387,-1.296],[-2.502,-0.687],[-2.791,-0.866],[-2.802,-0.866],[-2.567,-0.579],[-2.579,-0.462],[-2.806,-0.665],[-3.301,-0.919],[-3.334,-0.912],[-3.244,-0.746],[-3.453,-0.858],[-3.859,-0.476],[-3.814,0.576],[-3.159,1.239],[-3.294,1.203],[-2.666,1.578],[-2.815,1.564],[-2.628,1.674],[-0.165,1.546],[1.763,2.299],[1.37,2.908],[1.363,2.908],[-0.235,3.521],[-2.661,3.807],[-3.27,3.644],[-3.355,3.613],[-3.349,3.624],[-1.917,4.948],[-0.722,5.354],[3.358,4.425],[5.176,0.656],[4.77,-1.375],[6.898,0.87],[4.076,-2.472],[2.181,-4.999],[3.623,-10.801],[4.828,-8.868],[6.088,-7.703],[6.41,-8.892],[7.081,-7.074],[8.422,-5.33],[8.665,-4.985],[9.245,-3.991],[9.879,-2.359],[10.13,-0.982]],\"c\":true}]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":20,\"s\":[{\"i\":[[0,0],[-0.03,-0.196],[0.001,-0.315],[0,-0.044],[0.784,-1.26],[0.037,-0.059],[0.129,-0.308],[0.711,-0.509],[0.55,-0.259],[0,0],[0,0],[0.07,-0.029],[0.18,-0.163],[3.652,0],[0.988,0.296],[-0.036,-0.014],[0.11,0.04],[0.723,0.489],[0.845,1.898],[-0.075,1.505],[0.08,-0.373],[-0.467,1.186],[0.175,-0.384],[-0.528,0.892],[-0.21,0.229],[0,0],[-0.018,0.121],[-0.293,0.65],[0,0],[0,-0.031],[-0.004,0.014],[-0.041,0.083],[-0.067,0.076],[-0.005,0.007],[0.006,-0.047],[0,0.004],[-0.041,0.093],[-0.224,0.154],[0,0],[-0.14,-0.364],[0,0],[-0.016,0],[0,-0.005],[-0.039,-0.068],[-0.052,-0.077],[0,0],[0,0],[-0.157,-0.134],[0,0],[-0.764,-0.289],[-0.118,0.088],[0.005,-0.039],[-0.257,0.109],[-0.001,-0.033],[-0.368,0.071],[0.068,-0.058],[-0.176,0.009],[0.062,-0.026],[0.117,-0.057],[0.035,-0.028],[0.261,-0.684],[0,0],[0,0],[-0.485,-0.077],[-0.041,-0.152],[0,0],[0.006,-0.04],[0.233,-0.198],[0.07,-0.045],[0.28,-0.123],[0.316,-0.196],[0.045,-0.044],[0,-0.005],[-0.011,-0.056],[-0.036,-0.068],[0.009,-0.115],[0.083,-0.192],[0.101,0.051],[0,0],[-0.046,-0.118],[0,0],[0.09,0.05],[0.113,0.049],[0.012,-0.004],[0,0],[0.129,0.064],[0.093,-0.166],[-0.179,-0.324],[-0.259,-0.177],[-0.015,-0.012],[-0.218,-0.109],[-0.068,-0.039],[-0.017,-0.071],[-0.427,-0.474],[-0.451,-0.592],[0.366,0.116],[0,0],[0.762,-0.423],[0.818,0.173],[0.199,0.069],[0,0],[0,0],[-0.562,-0.34],[-0.416,-0.078],[-1.148,0.914],[-0.001,1.467],[0.272,0.642],[-0.488,-0.931],[1.067,0.88],[0.381,1.005],[0,0],[0.043,-1.277],[-0.03,-0.651],[-0.141,0.387],[-0.334,-0.559],[-0.386,-0.63],[-0.079,-0.119],[-0.165,-0.347],[-0.16,-0.563],[-0.039,-0.466]],\"o\":[[0,0],[0.055,0.31],[0,0.04],[-0.062,1.483],[-0.04,0.067],[0.117,0.313],[-0.218,0.847],[-0.474,0.382],[0,0],[0,0],[-0.072,0.033],[-0.104,0.22],[-0.111,0.139],[-1.032,-0.001],[0.036,0.016],[-0.114,-0.038],[-0.834,-0.257],[-1.778,-1.076],[-0.63,-1.369],[-0.123,0.361],[0,-1.275],[-0.241,0.346],[0.247,-1.007],[0.158,-0.267],[0,0],[-0.004,-0.122],[0.065,-0.71],[0,0],[0.006,-0.012],[0,0.031],[0.026,-0.089],[0.045,-0.091],[0.005,-0.007],[0.005,-0.007],[0,0.017],[0.027,-0.051],[0.09,-0.257],[0,0],[-0.013,0.39],[0,0],[0.014,0.02],[0.003,0.001],[0.04,0.082],[0.055,0.097],[0,0],[0,0],[0.115,0.18],[0,0],[0.797,-0.161],[0.095,-0.112],[0,0.039],[0.171,-0.221],[-0.014,0.03],[0.323,-0.19],[0.048,0],[0.162,-0.07],[0.288,-0.03],[-0.122,0.044],[-0.035,0.029],[-0.613,0.401],[0,0],[0,0],[0.108,0.479],[1.373,0.129],[0,0],[-0.002,0.04],[-0.056,0.301],[-0.062,0.056],[-0.047,0.027],[-0.344,0.141],[-0.053,0.035],[-0.016,0.017],[0.025,0.051],[-0.063,-0.071],[0.04,0.108],[0.007,0.209],[-0.091,-0.068],[0,0],[0.106,0.069],[0.034,0.095],[-0.059,-0.084],[-0.132,-0.078],[-0.012,0.007],[0.032,0.057],[0,0],[-0.171,0.083],[-0.151,0.338],[0.174,0.261],[0.016,0.012],[0.2,0.14],[0.066,0.044],[0.061,0.04],[0.332,1.348],[0.786,0.559],[0.261,0.371],[0,0],[-0.374,-0.124],[-0.603,-0.499],[-0.344,-0.557],[0,0],[0,0],[0.383,0.533],[0.377,0.192],[1.431,0.327],[1.148,-0.914],[0.004,-0.698],[0.904,0.537],[-0.606,-1.77],[-0.858,-0.646],[-1.103,-2.96],[0,0],[0.293,0.889],[0.074,-0.405],[0.11,0.642],[0.512,0.728],[0.083,0.112],[0.221,0.314],[0.262,0.523],[0.128,0.45],[0.127,-0.167]],\"v\":[[10.333,-1.238],[10.393,-0.927],[10.475,0.011],[10.475,0.135],[9.184,4.32],[9.068,4.509],[9.048,5.476],[7.611,7.575],[6.069,8.54],[5.987,8.579],[5.942,8.599],[5.729,8.692],[5.298,9.271],[0.329,10.801],[-2.718,10.353],[-2.614,10.398],[-2.948,10.281],[-5.296,9.156],[-9.325,4.587],[-10.17,0.211],[-10.162,1.314],[-9.767,-2.41],[-10.185,-1.312],[-9.224,-4.176],[-8.671,-4.921],[-8.671,-4.932],[-8.65,-5.296],[-8.109,-6.934],[-8.094,-6.963],[-8.094,-6.888],[-8.094,-6.845],[-7.992,-7.103],[-7.822,-7.355],[-7.809,-7.375],[-7.831,-7.239],[-7.831,-7.216],[-7.736,-7.41],[-7.253,-8.042],[-7.242,-8.048],[-7.05,-6.906],[-7.05,-6.901],[-7.009,-6.957],[-7,-6.949],[-6.881,-6.72],[-6.721,-6.458],[-6.708,-6.443],[-6.697,-6.446],[-6.282,-6.389],[-6.275,-6.383],[-3.888,-6.184],[-3.568,-6.485],[-3.575,-6.367],[-2.922,-6.662],[-2.942,-6.568],[-1.898,-6.676],[-2.121,-6.54],[-1.609,-6.661],[-1.058,-6.541],[-1.416,-6.389],[-1.311,-6.368],[-2.655,-5.193],[-2.655,-5.182],[-2.655,-5.188],[-1.674,-4.268],[-0.31,-3.872],[-0.31,-3.801],[-0.321,-3.681],[-0.768,-2.909],[-0.967,-2.756],[-1.526,-2.513],[-2.206,-2.008],[-2.352,-1.889],[-2.404,-1.82],[-2.351,-1.658],[-2.434,-1.635],[-2.387,-1.296],[-1.761,-0.752],[-2.049,-0.931],[-2.06,-0.931],[-1.826,-0.644],[-1.838,-0.527],[-2.064,-0.73],[-1.967,-1.036],[-2,-1.028],[-1.909,-0.863],[-2.119,-0.975],[-2.525,-0.593],[-2.48,0.459],[-2.346,1.122],[-2.481,1.087],[-2.135,1.462],[-2.284,1.447],[-2.097,1.557],[-1.721,2.102],[-0.49,2.763],[-0.883,3.372],[-0.889,3.372],[-1.895,2.891],[-2.101,2.604],[-2.066,2.045],[-2.151,2.014],[-2.144,2.025],[-1.357,2.808],[-0.162,3.214],[2.435,2.414],[3.385,0.876],[2.979,-1.155],[4.378,1.091],[2.001,-2.291],[2.181,-4.999],[4.248,-10.801],[5.453,-8.868],[5.359,-7.703],[5.264,-8.892],[7.081,-7.074],[8.422,-5.33],[8.665,-4.985],[9.245,-3.991],[9.879,-2.359],[10.13,-0.982]],\"c\":true}]},{\"i\":{\"x\":0,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":37,\"s\":[{\"i\":[[0,0],[-0.03,-0.196],[0.001,-0.315],[0,-0.044],[0.784,-1.26],[0.037,-0.059],[0.129,-0.308],[0.711,-0.509],[0.55,-0.259],[0,0],[0,0],[0.07,-0.029],[0.18,-0.163],[3.652,0],[0.988,0.296],[-0.036,-0.014],[0.11,0.04],[0.723,0.489],[0.845,1.898],[-0.075,1.505],[0.08,-0.373],[-0.467,1.186],[0.175,-0.384],[-0.528,0.892],[-0.21,0.229],[0,0],[-0.018,0.121],[-0.293,0.65],[0,0],[0,-0.031],[-0.004,0.014],[-0.041,0.083],[-0.067,0.076],[-0.005,0.007],[0.006,-0.047],[0,0.004],[-0.041,0.093],[-0.224,0.154],[0,0],[-0.14,-0.364],[0,0],[-0.016,0],[0,-0.005],[-0.039,-0.068],[-0.052,-0.077],[0,0],[0,0],[-0.157,-0.134],[0,0],[-0.764,-0.289],[-0.118,0.088],[0.005,-0.039],[-0.257,0.109],[-0.001,-0.033],[-0.368,0.071],[0.068,-0.058],[-0.176,0.009],[0.062,-0.026],[0.117,-0.057],[0.035,-0.028],[0.261,-0.684],[0,0],[0,0],[-0.485,-0.077],[-0.041,-0.152],[0,0],[0.006,-0.04],[0.233,-0.198],[0.07,-0.045],[0.28,-0.123],[0.316,-0.196],[0.045,-0.044],[0,-0.005],[-0.011,-0.056],[-0.036,-0.068],[0.009,-0.115],[0.083,-0.192],[0.101,0.051],[0,0],[-0.046,-0.118],[0,0],[0.09,0.05],[0.113,0.049],[0.012,-0.004],[0,0],[0.129,0.064],[0.093,-0.166],[-0.179,-0.324],[-0.259,-0.177],[-0.015,-0.012],[-0.218,-0.109],[-0.068,-0.039],[-0.017,-0.071],[-0.427,-0.474],[-0.451,-0.592],[0.366,0.116],[0,0],[0.762,-0.423],[0.818,0.173],[0.199,0.069],[0,0],[0,0],[-0.562,-0.34],[-0.416,-0.078],[-1.148,0.914],[-0.001,1.467],[0.272,0.642],[-0.488,-0.931],[1.067,0.88],[0.381,1.005],[0,0],[0.043,-1.277],[-0.03,-0.651],[-0.141,0.387],[-0.334,-0.559],[-0.386,-0.63],[-0.079,-0.119],[-0.165,-0.347],[-0.16,-0.563],[-0.039,-0.466]],\"o\":[[0,0],[0.055,0.31],[0,0.04],[-0.062,1.483],[-0.04,0.067],[0.117,0.313],[-0.218,0.847],[-0.474,0.382],[0,0],[0,0],[-0.072,0.033],[-0.104,0.22],[-0.111,0.139],[-1.032,-0.001],[0.036,0.016],[-0.114,-0.038],[-0.834,-0.257],[-1.778,-1.076],[-0.63,-1.369],[-0.123,0.361],[0,-1.275],[-0.241,0.346],[0.247,-1.007],[0.158,-0.267],[0,0],[-0.004,-0.122],[0.065,-0.71],[0,0],[0.006,-0.012],[0,0.031],[0.026,-0.089],[0.045,-0.091],[0.005,-0.007],[0.005,-0.007],[0,0.017],[0.027,-0.051],[0.09,-0.257],[0,0],[-0.013,0.39],[0,0],[0.014,0.02],[0.003,0.001],[0.04,0.082],[0.055,0.097],[0,0],[0,0],[0.115,0.18],[0,0],[0.797,-0.161],[0.095,-0.112],[0,0.039],[0.171,-0.221],[-0.014,0.03],[0.323,-0.19],[0.048,0],[0.162,-0.07],[0.288,-0.03],[-0.122,0.044],[-0.035,0.029],[-0.613,0.401],[0,0],[0,0],[0.108,0.479],[1.373,0.129],[0,0],[-0.002,0.04],[-0.056,0.301],[-0.062,0.056],[-0.047,0.027],[-0.344,0.141],[-0.053,0.035],[-0.016,0.017],[0.025,0.051],[-0.063,-0.071],[0.04,0.108],[0.007,0.209],[-0.091,-0.068],[0,0],[0.106,0.069],[0.034,0.095],[-0.059,-0.084],[-0.132,-0.078],[-0.012,0.007],[0.032,0.057],[0,0],[-0.171,0.083],[-0.151,0.338],[0.174,0.261],[0.016,0.012],[0.2,0.14],[0.066,0.044],[0.061,0.04],[0.332,1.348],[0.786,0.559],[0.261,0.371],[0,0],[-0.374,-0.124],[-0.603,-0.499],[-0.344,-0.557],[0,0],[0,0],[0.383,0.533],[0.377,0.192],[1.431,0.327],[1.148,-0.914],[0.004,-0.698],[0.904,0.537],[-0.606,-1.77],[-0.858,-0.646],[-1.103,-2.96],[0,0],[0.293,0.889],[0.074,-0.405],[0.11,0.642],[0.512,0.728],[0.083,0.112],[0.221,0.314],[0.262,0.523],[0.128,0.45],[0.127,-0.167]],\"v\":[[10.333,-1.238],[10.393,-0.927],[10.475,0.011],[10.475,0.135],[9.184,4.32],[9.068,4.509],[9.048,5.476],[7.611,7.575],[6.069,8.54],[5.987,8.579],[5.942,8.599],[5.729,8.692],[5.298,9.271],[0.329,10.801],[-2.718,10.353],[-2.614,10.398],[-2.948,10.281],[-5.296,9.156],[-9.325,4.587],[-10.17,0.211],[-10.162,1.314],[-9.767,-2.41],[-10.185,-1.312],[-9.224,-4.176],[-8.671,-4.921],[-8.671,-4.932],[-8.65,-5.296],[-8.109,-6.934],[-8.094,-6.963],[-8.094,-6.888],[-8.094,-6.845],[-7.992,-7.103],[-7.822,-7.355],[-7.809,-7.375],[-7.831,-7.239],[-7.831,-7.216],[-7.736,-7.41],[-7.253,-8.042],[-7.242,-8.048],[-7.05,-6.906],[-7.05,-6.901],[-7.009,-6.957],[-7,-6.949],[-6.881,-6.72],[-6.721,-6.458],[-6.708,-6.443],[-6.697,-6.446],[-6.282,-6.389],[-6.275,-6.383],[-3.888,-6.184],[-3.568,-6.485],[-3.575,-6.367],[-2.922,-6.662],[-2.942,-6.568],[-1.898,-6.676],[-2.121,-6.54],[-1.609,-6.661],[-1.058,-6.541],[-1.416,-6.389],[-1.311,-6.368],[-2.655,-5.193],[-2.655,-5.182],[-2.655,-5.188],[-1.674,-4.268],[-0.31,-3.872],[-0.31,-3.801],[-0.321,-3.681],[-0.768,-2.909],[-0.967,-2.756],[-1.526,-2.513],[-2.206,-2.008],[-2.352,-1.889],[-2.404,-1.82],[-2.351,-1.658],[-2.434,-1.635],[-2.387,-1.296],[-1.761,-0.752],[-2.049,-0.931],[-2.06,-0.931],[-1.826,-0.644],[-1.838,-0.527],[-2.064,-0.73],[-1.967,-1.036],[-2,-1.028],[-1.909,-0.863],[-2.119,-0.975],[-2.525,-0.593],[-2.48,0.459],[-2.346,1.122],[-2.481,1.087],[-2.135,1.462],[-2.284,1.447],[-2.097,1.557],[-1.721,2.102],[-0.49,2.763],[-0.883,3.372],[-0.889,3.372],[-1.895,2.891],[-2.101,2.604],[-2.066,2.045],[-2.151,2.014],[-2.144,2.025],[-1.357,2.808],[-0.162,3.214],[2.435,2.414],[3.385,0.876],[2.979,-1.155],[4.378,1.091],[2.001,-2.291],[2.181,-4.999],[4.248,-10.801],[5.453,-8.868],[5.359,-7.703],[5.264,-8.892],[7.081,-7.074],[8.422,-5.33],[8.665,-4.985],[9.245,-3.991],[9.879,-2.359],[10.13,-0.982]],\"c\":true}]},{\"t\":60,\"s\":[{\"i\":[[0,0],[-0.03,-0.196],[0.001,-0.315],[0,-0.044],[0.784,-1.26],[0.037,-0.059],[0.129,-0.308],[0.711,-0.509],[0.55,-0.259],[0,0],[0,0],[0.07,-0.029],[0.18,-0.163],[3.652,0],[0.988,0.296],[-0.036,-0.014],[0.11,0.04],[0.723,0.489],[0.845,1.898],[-0.075,1.505],[0.08,-0.373],[-0.467,1.186],[0.175,-0.384],[-0.528,0.892],[-0.21,0.229],[0,0],[-0.018,0.121],[-0.293,0.65],[0,0],[0,-0.031],[-0.004,0.014],[-0.041,0.083],[-0.067,0.076],[-0.005,0.007],[0.006,-0.047],[0,0.004],[-0.041,0.093],[-0.224,0.154],[0,0],[-0.14,-0.364],[0,0],[-0.016,0],[0,-0.005],[-0.039,-0.068],[-0.052,-0.077],[0,0],[0,0],[-0.157,-0.134],[0,0],[-0.764,-0.289],[-0.118,0.088],[0.005,-0.039],[-0.257,0.109],[-0.001,-0.033],[-0.368,0.071],[0.068,-0.058],[-0.176,0.009],[0.062,-0.026],[0.117,-0.057],[0.035,-0.028],[0.261,-0.684],[0,0],[0,0],[-0.485,-0.077],[-0.041,-0.152],[0,0],[0.006,-0.04],[0.233,-0.198],[0.07,-0.045],[0.28,-0.123],[0.316,-0.196],[0.045,-0.044],[0,-0.005],[-0.011,-0.056],[-0.036,-0.068],[0.009,-0.115],[0.083,-0.192],[0.101,0.051],[0,0],[-0.046,-0.118],[0,0],[0.09,0.05],[0.113,0.049],[0.012,-0.004],[0,0],[0.129,0.064],[0.093,-0.166],[-0.179,-0.324],[-0.259,-0.177],[-0.015,-0.012],[-0.218,-0.109],[-0.068,-0.039],[-0.064,-0.034],[-0.763,0.111],[-0.451,-0.592],[0.366,0.116],[0,0],[0.762,-0.423],[0.818,0.173],[0.199,0.069],[0,0],[0,0],[-0.562,-0.34],[-0.416,-0.078],[-1.148,0.914],[-0.001,1.467],[0.272,0.642],[-0.488,-0.931],[1.067,0.88],[0.381,1.005],[0,0],[-1.363,-1.273],[-0.526,-0.531],[-0.141,0.387],[-0.334,-0.559],[-0.386,-0.63],[-0.079,-0.119],[-0.165,-0.347],[-0.16,-0.563],[-0.039,-0.466]],\"o\":[[0,0],[0.055,0.31],[0,0.04],[-0.062,1.483],[-0.04,0.067],[0.117,0.313],[-0.218,0.847],[-0.474,0.382],[0,0],[0,0],[-0.072,0.033],[-0.104,0.22],[-0.111,0.139],[-1.032,-0.001],[0.036,0.016],[-0.114,-0.038],[-0.834,-0.257],[-1.778,-1.076],[-0.63,-1.369],[-0.123,0.361],[0,-1.275],[-0.241,0.346],[0.247,-1.007],[0.158,-0.267],[0,0],[-0.004,-0.122],[0.065,-0.71],[0,0],[0.006,-0.012],[0,0.031],[0.026,-0.089],[0.045,-0.091],[0.005,-0.007],[0.005,-0.007],[0,0.017],[0.027,-0.051],[0.09,-0.257],[0,0],[-0.013,0.39],[0,0],[0.014,0.02],[0.003,0.001],[0.04,0.082],[0.055,0.097],[0,0],[0,0],[0.115,0.18],[0,0],[0.797,-0.161],[0.095,-0.112],[0,0.039],[0.171,-0.221],[-0.014,0.03],[0.323,-0.19],[0.048,0],[0.162,-0.07],[0.288,-0.03],[-0.122,0.044],[-0.035,0.029],[-0.613,0.401],[0,0],[0,0],[0.108,0.479],[1.373,0.129],[0,0],[-0.002,0.04],[-0.056,0.301],[-0.062,0.056],[-0.047,0.027],[-0.344,0.141],[-0.053,0.035],[-0.016,0.017],[0.025,0.051],[-0.063,-0.071],[0.04,0.108],[0.007,0.209],[-0.091,-0.068],[0,0],[0.106,0.069],[0.034,0.095],[-0.059,-0.084],[-0.132,-0.078],[-0.012,0.007],[0.032,0.057],[0,0],[-0.171,0.083],[-0.151,0.338],[0.174,0.261],[0.016,0.012],[0.2,0.14],[0.066,0.044],[0.061,0.04],[1,0.525],[0.733,-0.129],[0.261,0.371],[0,0],[-0.374,-0.124],[-0.755,0.359],[-0.207,-0.04],[0,0],[0,0],[0.383,0.533],[0.377,0.192],[1.431,0.327],[1.148,-0.914],[0.004,-0.698],[0.904,0.537],[-0.606,-1.77],[-0.858,-0.646],[-1.103,-2.96],[0,0],[0.276,0.257],[0.074,-0.405],[0.11,0.642],[0.512,0.728],[0.083,0.112],[0.221,0.314],[0.262,0.523],[0.128,0.45],[0.127,-0.167]],\"v\":[[10.333,-1.238],[10.393,-0.927],[10.475,0.011],[10.475,0.135],[9.184,4.32],[9.068,4.509],[9.048,5.476],[7.611,7.575],[6.069,8.54],[5.987,8.579],[5.942,8.599],[5.729,8.692],[5.298,9.271],[0.329,10.801],[-2.718,10.353],[-2.614,10.398],[-2.948,10.281],[-5.296,9.156],[-9.325,4.587],[-10.17,0.211],[-10.475,1.314],[-9.767,-2.41],[-10.394,-1.312],[-9.224,-4.176],[-8.671,-4.921],[-8.671,-4.932],[-8.65,-5.296],[-8.109,-7.351],[-8.094,-7.38],[-8.094,-7.305],[-8.094,-7.261],[-7.992,-7.52],[-7.822,-7.772],[-7.809,-7.791],[-7.831,-7.656],[-7.831,-7.632],[-7.736,-7.827],[-7.253,-8.459],[-7.242,-8.464],[-7.05,-7.322],[-7.05,-7.318],[-7.009,-7.374],[-7,-7.366],[-6.881,-7.137],[-6.721,-6.875],[-6.708,-6.86],[-6.697,-6.863],[-6.282,-6.389],[-6.275,-6.383],[-3.888,-6.184],[-3.568,-6.485],[-3.575,-6.367],[-2.922,-6.871],[-2.942,-6.776],[-1.898,-7.171],[-2.121,-7.035],[-1.609,-7.156],[-1.058,-7.036],[-1.416,-6.884],[-1.311,-6.863],[-2.655,-5.193],[-2.655,-5.182],[-2.655,-5.188],[-1.674,-4.268],[0.003,-3.872],[0.003,-3.801],[-0.009,-3.681],[-0.456,-2.909],[-0.654,-2.756],[-1.214,-2.513],[-2.206,-2.008],[-2.352,-1.889],[-2.404,-1.82],[-2.351,-1.658],[-2.434,-1.635],[-2.387,-1.296],[-2.502,-0.687],[-2.791,-0.866],[-2.802,-0.866],[-2.567,-0.579],[-2.579,-0.462],[-2.806,-0.665],[-3.301,-0.919],[-3.334,-0.912],[-3.244,-0.746],[-3.453,-0.858],[-3.859,-0.476],[-3.814,0.576],[-3.159,1.239],[-3.294,1.203],[-2.666,1.578],[-2.815,1.564],[-2.628,1.674],[-0.165,1.546],[1.763,2.299],[1.37,2.908],[1.363,2.908],[-0.235,3.521],[-2.661,3.807],[-3.27,3.644],[-3.355,3.613],[-3.349,3.624],[-1.917,4.948],[-0.722,5.354],[3.358,4.425],[5.176,0.656],[4.77,-1.375],[6.898,0.87],[4.076,-2.472],[2.181,-4.999],[3.623,-10.801],[4.828,-8.868],[6.088,-7.703],[6.41,-8.892],[7.081,-7.074],[8.422,-5.33],[8.665,-4.985],[9.245,-3.991],[9.879,-2.359],[10.13,-0.982]],\"c\":true}]}],\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ind\":1,\"ty\":\"sh\",\"ix\":2,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[-0.02,0.05]],\"o\":[[0.018,-0.051],[0,0]],\"v\":[[-2.71,-5.03],[-2.655,-5.182]],\"c\":false},\"ix\":2},\"nm\":\"Контур 2\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"mm\",\"mm\":1,\"nm\":\"Объединить контуры 1\",\"mn\":\"ADBE Vector Filter - Merge\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Vector\",\"np\":4,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-60,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":3,\"nm\":\"Ellipse 3\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":0,\"s\":[0]},{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":40,\"s\":[1090]},{\"t\":50,\"s\":[1080]}],\"ix\":10},\"p\":{\"a\":0,\"k\":[15,15,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":0,\"s\":[16.667,16.667,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":20,\"s\":[11.667,11.667,100]},{\"t\":40,\"s\":[16.667,16.667,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"ip\":0,\"op\":120,\"st\":-60,\"bm\":0}],\"markers\":[]}"
  },
  {
    "path": "Telegram/Resources/icons/settings/devices/device_web_safari.lottie",
    "content": "{\"v\":\"5.7.4\",\"fr\":60,\"ip\":0,\"op\":120,\"w\":30,\"h\":30,\"nm\":\"safari_30\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Com 2\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":0,\"s\":[0]},{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":20,\"s\":[185]},{\"t\":30,\"s\":[180]}],\"ix\":10},\"p\":{\"a\":0,\"k\":[15,15,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[-0.15,0.205],[-0.233,0.543],[0,0],[0.142,0.444],[0.385,0.123],[1.435,-0.615],[0,0],[0.231,-0.17],[0.15,-0.205],[0.233,-0.543],[0,0],[-0.142,-0.444],[-0.385,-0.123],[-1.435,0.615],[0,0],[-0.231,0.17]],\"o\":[[0.17,-0.231],[0,0],[0.615,-1.435],[-0.123,-0.385],[-0.444,-0.142],[0,0],[-0.543,0.233],[-0.205,0.15],[-0.17,0.231],[0,0],[-0.615,1.435],[0.123,0.385],[0.444,0.142],[0,0],[0.543,-0.233],[0.205,-0.15]],\"v\":[[2.611,2.075],[3.13,1.029],[4.28,-1.654],[5.061,-4.251],[4.251,-5.061],[1.654,-4.28],[-1.029,-3.13],[-2.075,-2.611],[-2.611,-2.075],[-3.13,-1.029],[-4.28,1.654],[-5.061,4.251],[-4.251,5.061],[-1.654,4.28],[1.029,3.13],[2.075,2.611]],\"c\":true},\"ix\":2},\"nm\":\"Контур 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[2.5,2.5],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Контур эллипса 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"mm\",\"mm\":3,\"nm\":\"Объединить контуры 1\",\"mn\":\"ADBE Vector Filter - Merge\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0,0,0,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Com 2\",\"np\":4,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-10,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Com 1\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[15,15,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[16.667,16.667,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[20,20],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Контур эллипса 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Заливка 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[600,600],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Преобразовать\"}],\"nm\":\"Com 1\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":120,\"st\":-10,\"bm\":0}],\"markers\":[]}"
  },
  {
    "path": "Telegram/Resources/iv_html/highlight.9.12.0.css",
    "content": ".hljs{display:block;overflow-x:auto;padding:0.5em;background:#F0F0F0}.hljs,.hljs-subst{color:#444}.hljs-comment{color:#888888}.hljs-keyword,.hljs-attribute,.hljs-selector-tag,.hljs-meta-keyword,.hljs-doctag,.hljs-name{font-weight:bold}.hljs-type,.hljs-string,.hljs-number,.hljs-selector-id,.hljs-selector-class,.hljs-quote,.hljs-template-tag,.hljs-deletion{color:#880000}.hljs-title,.hljs-section{color:#880000;font-weight:bold}.hljs-regexp,.hljs-symbol,.hljs-variable,.hljs-template-variable,.hljs-link,.hljs-selector-attr,.hljs-selector-pseudo{color:#BC6060}.hljs-literal{color:#78A960}.hljs-built_in,.hljs-bullet,.hljs-code,.hljs-addition{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta-string{color:#4d99bf}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold}"
  },
  {
    "path": "Telegram/Resources/iv_html/highlight.9.12.0.js",
    "content": "/*! highlight.js v9.12.0 | BSD3 License | git.io/hljslicense */\n!function(e){var t=\"object\"==typeof window&&window||\"object\"==typeof self&&self;\"undefined\"!=typeof exports?e(exports):t&&(t.hljs=e({}),\"function\"==typeof define&&define.amd&&define([],function(){return t.hljs}))}(function(e){function t(e){return e.replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\")}function r(e){return e.nodeName.toLowerCase()}function a(e,t){var r=e&&e.exec(t);return r&&0===r.index}function n(e){return E.test(e)}function i(e){var t,r,a,i,s=e.className+\" \";if(s+=e.parentNode?e.parentNode.className:\"\",r=M.exec(s))return w(r[1])?r[1]:\"no-highlight\";for(s=s.split(/\\s+/),t=0,a=s.length;a>t;t++)if(i=s[t],n(i)||w(i))return i}function s(e){var t,r={},a=Array.prototype.slice.call(arguments,1);for(t in e)r[t]=e[t];return a.forEach(function(e){for(t in e)r[t]=e[t]}),r}function c(e){var t=[];return function a(e,n){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?n+=i.nodeValue.length:1===i.nodeType&&(t.push({event:\"start\",offset:n,node:i}),n=a(i,n),r(i).match(/br|hr|img|input/)||t.push({event:\"stop\",offset:n,node:i}));return n}(e,0),t}function o(e,a,n){function i(){return e.length&&a.length?e[0].offset!==a[0].offset?e[0].offset<a[0].offset?e:a:\"start\"===a[0].event?e:a:e.length?e:a}function s(e){function a(e){return\" \"+e.nodeName+'=\"'+t(e.value).replace('\"',\"&quot;\")+'\"'}u+=\"<\"+r(e)+N.map.call(e.attributes,a).join(\"\")+\">\"}function c(e){u+=\"</\"+r(e)+\">\"}function o(e){(\"start\"===e.event?s:c)(e.node)}for(var l=0,u=\"\",d=[];e.length||a.length;){var b=i();if(u+=t(n.substring(l,b[0].offset)),l=b[0].offset,b===e){d.reverse().forEach(c);do o(b.splice(0,1)[0]),b=i();while(b===e&&b.length&&b[0].offset===l);d.reverse().forEach(s)}else\"start\"===b[0].event?d.push(b[0].node):d.pop(),o(b.splice(0,1)[0])}return u+t(n.substr(l))}function l(e){return e.v&&!e.cached_variants&&(e.cached_variants=e.v.map(function(t){return s(e,{v:null},t)})),e.cached_variants||e.eW&&[s(e)]||[e]}function u(e){function t(e){return e&&e.source||e}function r(r,a){return new RegExp(t(r),\"m\"+(e.cI?\"i\":\"\")+(a?\"g\":\"\"))}function a(n,i){if(!n.compiled){if(n.compiled=!0,n.k=n.k||n.bK,n.k){var s={},c=function(t,r){e.cI&&(r=r.toLowerCase()),r.split(\" \").forEach(function(e){var r=e.split(\"|\");s[r[0]]=[t,r[1]?Number(r[1]):1]})};\"string\"==typeof n.k?c(\"keyword\",n.k):k(n.k).forEach(function(e){c(e,n.k[e])}),n.k=s}n.lR=r(n.l||/\\w+/,!0),i&&(n.bK&&(n.b=\"\\\\b(\"+n.bK.split(\" \").join(\"|\")+\")\\\\b\"),n.b||(n.b=/\\B|\\b/),n.bR=r(n.b),n.e||n.eW||(n.e=/\\B|\\b/),n.e&&(n.eR=r(n.e)),n.tE=t(n.e)||\"\",n.eW&&i.tE&&(n.tE+=(n.e?\"|\":\"\")+i.tE)),n.i&&(n.iR=r(n.i)),null==n.r&&(n.r=1),n.c||(n.c=[]),n.c=Array.prototype.concat.apply([],n.c.map(function(e){return l(\"self\"===e?n:e)})),n.c.forEach(function(e){a(e,n)}),n.starts&&a(n.starts,i);var o=n.c.map(function(e){return e.bK?\"\\\\.?(\"+e.b+\")\\\\.?\":e.b}).concat([n.tE,n.i]).map(t).filter(Boolean);n.t=o.length?r(o.join(\"|\"),!0):{exec:function(){return null}}}}a(e)}function d(e,r,n,i){function s(e,t){var r,n;for(r=0,n=t.c.length;n>r;r++)if(a(t.c[r].bR,e))return t.c[r]}function c(e,t){if(a(e.eR,t)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?c(e.parent,t):void 0}function o(e,t){return!n&&a(t.iR,e)}function l(e,t){var r=v.cI?t[0].toLowerCase():t[0];return e.k.hasOwnProperty(r)&&e.k[r]}function p(e,t,r,a){var n=a?\"\":L.classPrefix,i='<span class=\"'+n,s=r?\"\":R;return i+=e+'\">',i+t+s}function m(){var e,r,a,n;if(!N.k)return t(E);for(n=\"\",r=0,N.lR.lastIndex=0,a=N.lR.exec(E);a;)n+=t(E.substring(r,a.index)),e=l(N,a),e?(M+=e[1],n+=p(e[0],t(a[0]))):n+=t(a[0]),r=N.lR.lastIndex,a=N.lR.exec(E);return n+t(E.substr(r))}function f(){var e=\"string\"==typeof N.sL;if(e&&!x[N.sL])return t(E);var r=e?d(N.sL,E,!0,k[N.sL]):b(E,N.sL.length?N.sL:void 0);return N.r>0&&(M+=r.r),e&&(k[N.sL]=r.top),p(r.language,r.value,!1,!0)}function g(){C+=null!=N.sL?f():m(),E=\"\"}function _(e){C+=e.cN?p(e.cN,\"\",!0):\"\",N=Object.create(e,{parent:{value:N}})}function h(e,t){if(E+=e,null==t)return g(),0;var r=s(t,N);if(r)return r.skip?E+=t:(r.eB&&(E+=t),g(),r.rB||r.eB||(E=t)),_(r,t),r.rB?0:t.length;var a=c(N,t);if(a){var n=N;n.skip?E+=t:(n.rE||n.eE||(E+=t),g(),n.eE&&(E=t));do N.cN&&(C+=R),N.skip||(M+=N.r),N=N.parent;while(N!==a.parent);return a.starts&&_(a.starts,\"\"),n.rE?0:t.length}if(o(t,N))throw new Error('Illegal lexeme \"'+t+'\" for mode \"'+(N.cN||\"<unnamed>\")+'\"');return E+=t,t.length||1}var v=w(e);if(!v)throw new Error('Unknown language: \"'+e+'\"');u(v);var y,N=i||v,k={},C=\"\";for(y=N;y!==v;y=y.parent)y.cN&&(C=p(y.cN,\"\",!0)+C);var E=\"\",M=0;try{for(var B,S,$=0;;){if(N.t.lastIndex=$,B=N.t.exec(r),!B)break;S=h(r.substring($,B.index),B[0]),$=B.index+S}for(h(r.substr($)),y=N;y.parent;y=y.parent)y.cN&&(C+=R);return{r:M,value:C,language:e,top:N}}catch(A){if(A.message&&-1!==A.message.indexOf(\"Illegal\"))return{r:0,value:t(r)};throw A}}function b(e,r){r=r||L.languages||k(x);var a={r:0,value:t(e)},n=a;return r.filter(w).forEach(function(t){var r=d(t,e,!1);r.language=t,r.r>n.r&&(n=r),r.r>a.r&&(n=a,a=r)}),n.language&&(a.second_best=n),a}function p(e){return L.tabReplace||L.useBR?e.replace(B,function(e,t){return L.useBR&&\"\\n\"===e?\"<br>\":L.tabReplace?t.replace(/\\t/g,L.tabReplace):\"\"}):e}function m(e,t,r){var a=t?C[t]:r,n=[e.trim()];return e.match(/\\bhljs\\b/)||n.push(\"hljs\"),-1===e.indexOf(a)&&n.push(a),n.join(\" \").trim()}function f(e){var t,r,a,s,l,u=i(e);n(u)||(L.useBR?(t=document.createElementNS(\"http://www.w3.org/1999/xhtml\",\"div\"),t.innerHTML=e.innerHTML.replace(/\\n/g,\"\").replace(/<br[ \\/]*>/g,\"\\n\")):t=e,l=t.textContent,a=u?d(u,l,!0):b(l),r=c(t),r.length&&(s=document.createElementNS(\"http://www.w3.org/1999/xhtml\",\"div\"),s.innerHTML=a.value,a.value=o(r,c(s),l)),a.value=p(a.value),e.innerHTML=a.value,e.className=m(e.className,u,a.language),e.result={language:a.language,re:a.r},a.second_best&&(e.second_best={language:a.second_best.language,re:a.second_best.r}))}function g(e){L=s(L,e)}function _(){if(!_.called){_.called=!0;var e=document.querySelectorAll(\"pre code\");N.forEach.call(e,f)}}function h(){addEventListener(\"DOMContentLoaded\",_,!1),addEventListener(\"load\",_,!1)}function v(t,r){var a=x[t]=r(e);a.aliases&&a.aliases.forEach(function(e){C[e]=t})}function y(){return k(x)}function w(e){return e=(e||\"\").toLowerCase(),x[e]||x[C[e]]}var N=[],k=Object.keys,x={},C={},E=/^(no-?highlight|plain|text)$/i,M=/\\blang(?:uage)?-([\\w-]+)\\b/i,B=/((^(<[^>]+>|\\t|)+|(?:\\n)))/gm,R=\"</span>\",L={classPrefix:\"hljs-\",tabReplace:null,useBR:!1,languages:void 0};return e.highlight=d,e.highlightAuto=b,e.fixMarkup=p,e.highlightBlock=f,e.configure=g,e.initHighlighting=_,e.initHighlightingOnLoad=h,e.registerLanguage=v,e.listLanguages=y,e.getLanguage=w,e.inherit=s,e.IR=\"[a-zA-Z]\\\\w*\",e.UIR=\"[a-zA-Z_]\\\\w*\",e.NR=\"\\\\b\\\\d+(\\\\.\\\\d+)?\",e.CNR=\"(-?)(\\\\b0[xX][a-fA-F0-9]+|(\\\\b\\\\d+(\\\\.\\\\d*)?|\\\\.\\\\d+)([eE][-+]?\\\\d+)?)\",e.BNR=\"\\\\b(0b[01]+)\",e.RSR=\"!|!=|!==|%|%=|&|&&|&=|\\\\*|\\\\*=|\\\\+|\\\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\\\?|\\\\[|\\\\{|\\\\(|\\\\^|\\\\^=|\\\\||\\\\|=|\\\\|\\\\||~\",e.BE={b:\"\\\\\\\\[\\\\s\\\\S]\",r:0},e.ASM={cN:\"string\",b:\"'\",e:\"'\",i:\"\\\\n\",c:[e.BE]},e.QSM={cN:\"string\",b:'\"',e:'\"',i:\"\\\\n\",c:[e.BE]},e.PWM={b:/\\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\\b/},e.C=function(t,r,a){var n=e.inherit({cN:\"comment\",b:t,e:r,c:[]},a||{});return n.c.push(e.PWM),n.c.push({cN:\"doctag\",b:\"(?:TODO|FIXME|NOTE|BUG|XXX):\",r:0}),n},e.CLCM=e.C(\"//\",\"$\"),e.CBCM=e.C(\"/\\\\*\",\"\\\\*/\"),e.HCM=e.C(\"#\",\"$\"),e.NM={cN:\"number\",b:e.NR,r:0},e.CNM={cN:\"number\",b:e.CNR,r:0},e.BNM={cN:\"number\",b:e.BNR,r:0},e.CSSNM={cN:\"number\",b:e.NR+\"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?\",r:0},e.RM={cN:\"regexp\",b:/\\//,e:/\\/[gimuy]*/,i:/\\n/,c:[e.BE,{b:/\\[/,e:/\\]/,r:0,c:[e.BE]}]},e.TM={cN:\"title\",b:e.IR,r:0},e.UTM={cN:\"title\",b:e.UIR,r:0},e.METHOD_GUARD={b:\"\\\\.\\\\s*\"+e.UIR,r:0},e.registerLanguage(\"apache\",function(e){var t={cN:\"number\",b:\"[\\\\$%]\\\\d+\"};return{aliases:[\"apacheconf\"],cI:!0,c:[e.HCM,{cN:\"section\",b:\"</?\",e:\">\"},{cN:\"attribute\",b:/\\w+/,r:0,k:{nomarkup:\"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername\"},starts:{e:/$/,r:0,k:{literal:\"on off all\"},c:[{cN:\"meta\",b:\"\\\\s\\\\[\",e:\"\\\\]$\"},{cN:\"variable\",b:\"[\\\\$%]\\\\{\",e:\"\\\\}\",c:[\"self\",t]},t,e.QSM]}}],i:/\\S/}}),e.registerLanguage(\"bash\",function(e){var t={cN:\"variable\",v:[{b:/\\$[\\w\\d#@][\\w\\d_]*/},{b:/\\$\\{(.*?)}/}]},r={cN:\"string\",b:/\"/,e:/\"/,c:[e.BE,t,{cN:\"variable\",b:/\\$\\(/,e:/\\)/,c:[e.BE]}]},a={cN:\"string\",b:/'/,e:/'/};return{aliases:[\"sh\",\"zsh\"],l:/\\b-?[a-z\\._]+\\b/,k:{keyword:\"if then else elif fi for while in do done case esac function\",literal:\"true false\",built_in:\"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp\",_:\"-ne -eq -lt -gt -f -d -e -s -l -a\"},c:[{cN:\"meta\",b:/^#![^\\n]+sh\\s*$/,r:10},{cN:\"function\",b:/\\w[\\w\\d_]*\\s*\\(\\s*\\)\\s*\\{/,rB:!0,c:[e.inherit(e.TM,{b:/\\w[\\w\\d_]*/})],r:0},e.HCM,r,a,t]}}),e.registerLanguage(\"coffeescript\",function(e){var t={keyword:\"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super yield import export from as default await then unless until loop of by when and or is isnt not\",literal:\"true false null undefined yes no on off\",built_in:\"npm require console print module global window document\"},r=\"[A-Za-z$_][0-9A-Za-z$_]*\",a={cN:\"subst\",b:/#\\{/,e:/}/,k:t},n=[e.BNM,e.inherit(e.CNM,{starts:{e:\"(\\\\s*/)?\",r:0}}),{cN:\"string\",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/\"\"\"/,e:/\"\"\"/,c:[e.BE,a]},{b:/\"/,e:/\"/,c:[e.BE,a]}]},{cN:\"regexp\",v:[{b:\"///\",e:\"///\",c:[a,e.HCM]},{b:\"//[gim]*\",r:0},{b:/\\/(?![ *])(\\\\\\/|.)*?\\/[gim]*(?=\\W|$)/}]},{b:\"@\"+r},{sL:\"javascript\",eB:!0,eE:!0,v:[{b:\"```\",e:\"```\"},{b:\"`\",e:\"`\"}]}];a.c=n;var i=e.inherit(e.TM,{b:r}),s=\"(\\\\(.*\\\\))?\\\\s*\\\\B[-=]>\",c={cN:\"params\",b:\"\\\\([^\\\\(]\",rB:!0,c:[{b:/\\(/,e:/\\)/,k:t,c:[\"self\"].concat(n)}]};return{aliases:[\"coffee\",\"cson\",\"iced\"],k:t,i:/\\/\\*/,c:n.concat([e.C(\"###\",\"###\"),e.HCM,{cN:\"function\",b:\"^\\\\s*\"+r+\"\\\\s*=\\\\s*\"+s,e:\"[-=]>\",rB:!0,c:[i,c]},{b:/[:\\(,=]\\s*/,r:0,c:[{cN:\"function\",b:s,e:\"[-=]>\",rB:!0,c:[c]}]},{cN:\"class\",bK:\"class\",e:\"$\",i:/[:=\"\\[\\]]/,c:[{bK:\"extends\",eW:!0,i:/[:=\"\\[\\]]/,c:[i]},i]},{b:r+\":\",e:\":\",rB:!0,rE:!0,r:0}])}}),e.registerLanguage(\"cpp\",function(e){var t={cN:\"keyword\",b:\"\\\\b[a-z\\\\d_]*_t\\\\b\"},r={cN:\"string\",v:[{b:'(u8?|U)?L?\"',e:'\"',i:\"\\\\n\",c:[e.BE]},{b:'(u8?|U)?R\"',e:'\"',c:[e.BE]},{b:\"'\\\\\\\\?.\",e:\"'\",i:\".\"}]},a={cN:\"number\",v:[{b:\"\\\\b(0b[01']+)\"},{b:\"(-?)\\\\b([\\\\d']+(\\\\.[\\\\d']*)?|\\\\.[\\\\d']+)(u|U|l|L|ul|UL|f|F|b|B)\"},{b:\"(-?)(\\\\b0[xX][a-fA-F0-9']+|(\\\\b[\\\\d']+(\\\\.[\\\\d']*)?|\\\\.[\\\\d']+)([eE][-+]?[\\\\d']+)?)\"}],r:0},n={cN:\"meta\",b:/#\\s*[a-z]+\\b/,e:/$/,k:{\"meta-keyword\":\"if else elif endif define undef warning error line pragma ifdef ifndef include\"},c:[{b:/\\\\\\n/,r:0},e.inherit(r,{cN:\"meta-string\"}),{cN:\"meta-string\",b:/<[^\\n>]*>/,e:/$/,i:\"\\\\n\"},e.CLCM,e.CBCM]},i=e.IR+\"\\\\s*\\\\(\",s={keyword:\"int float while private char catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and or not\",built_in:\"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr\",literal:\"true false nullptr NULL\"},c=[t,e.CLCM,e.CBCM,a,r];return{aliases:[\"c\",\"cc\",\"h\",\"c++\",\"h++\",\"hpp\"],k:s,i:\"</\",c:c.concat([n,{b:\"\\\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\\\s*<\",e:\">\",k:s,c:[\"self\",t]},{b:e.IR+\"::\",k:s},{v:[{b:/=/,e:/;/},{b:/\\(/,e:/\\)/},{bK:\"new throw return else\",e:/;/}],k:s,c:c.concat([{b:/\\(/,e:/\\)/,k:s,c:c.concat([\"self\"]),r:0}]),r:0},{cN:\"function\",b:\"(\"+e.IR+\"[\\\\*&\\\\s]+)+\"+i,rB:!0,e:/[{;=]/,eE:!0,k:s,i:/[^\\w\\s\\*&]/,c:[{b:i,rB:!0,c:[e.TM],r:0},{cN:\"params\",b:/\\(/,e:/\\)/,k:s,r:0,c:[e.CLCM,e.CBCM,r,a,t]},e.CLCM,e.CBCM,n]},{cN:\"class\",bK:\"class struct\",e:/[{;:]/,c:[{b:/</,e:/>/,c:[\"self\"]},e.TM]}]),exports:{preprocessor:n,strings:r,k:s}}}),e.registerLanguage(\"cs\",function(e){var t={keyword:\"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long nameof object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let on orderby partial remove select set value var where yield\",literal:\"null false true\"},r={cN:\"string\",b:'@\"',e:'\"',c:[{b:'\"\"'}]},a=e.inherit(r,{i:/\\n/}),n={cN:\"subst\",b:\"{\",e:\"}\",k:t},i=e.inherit(n,{i:/\\n/}),s={cN:\"string\",b:/\\$\"/,e:'\"',i:/\\n/,c:[{b:\"{{\"},{b:\"}}\"},e.BE,i]},c={cN:\"string\",b:/\\$@\"/,e:'\"',c:[{b:\"{{\"},{b:\"}}\"},{b:'\"\"'},n]},o=e.inherit(c,{i:/\\n/,c:[{b:\"{{\"},{b:\"}}\"},{b:'\"\"'},i]});n.c=[c,s,r,e.ASM,e.QSM,e.CNM,e.CBCM],i.c=[o,s,a,e.ASM,e.QSM,e.CNM,e.inherit(e.CBCM,{i:/\\n/})];var l={v:[c,s,r,e.ASM,e.QSM]},u=e.IR+\"(<\"+e.IR+\"(\\\\s*,\\\\s*\"+e.IR+\")*>)?(\\\\[\\\\])?\";return{aliases:[\"csharp\"],k:t,i:/::/,c:[e.C(\"///\",\"$\",{rB:!0,c:[{cN:\"doctag\",v:[{b:\"///\",r:0},{b:\"<!--|-->\"},{b:\"</?\",e:\">\"}]}]}),e.CLCM,e.CBCM,{cN:\"meta\",b:\"#\",e:\"$\",k:{\"meta-keyword\":\"if else elif endif define undef warning error line region endregion pragma checksum\"}},l,e.CNM,{bK:\"class interface\",e:/[{;=]/,i:/[^\\s:]/,c:[e.TM,e.CLCM,e.CBCM]},{bK:\"namespace\",e:/[{;=]/,i:/[^\\s:]/,c:[e.inherit(e.TM,{b:\"[a-zA-Z](\\\\.?\\\\w)*\"}),e.CLCM,e.CBCM]},{cN:\"meta\",b:\"^\\\\s*\\\\[\",eB:!0,e:\"\\\\]\",eE:!0,c:[{cN:\"meta-string\",b:/\"/,e:/\"/}]},{bK:\"new return throw await else\",r:0},{cN:\"function\",b:\"(\"+u+\"\\\\s+)+\"+e.IR+\"\\\\s*\\\\(\",rB:!0,e:/[{;=]/,eE:!0,k:t,c:[{b:e.IR+\"\\\\s*\\\\(\",rB:!0,c:[e.TM],r:0},{cN:\"params\",b:/\\(/,e:/\\)/,eB:!0,eE:!0,k:t,r:0,c:[l,e.CNM,e.CBCM]},e.CLCM,e.CBCM]}]}}),e.registerLanguage(\"css\",function(e){var t=\"[a-zA-Z-][a-zA-Z0-9_-]*\",r={b:/[A-Z\\_\\.\\-]+\\s*:/,rB:!0,e:\";\",eW:!0,c:[{cN:\"attribute\",b:/\\S/,e:\":\",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\\w-]+\\(/,rB:!0,c:[{cN:\"built_in\",b:/[\\w-]+/},{b:/\\(/,e:/\\)/,c:[e.ASM,e.QSM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:\"number\",b:\"#[0-9A-Fa-f]+\"},{cN:\"meta\",b:\"!important\"}]}}]};return{cI:!0,i:/[=\\/|'\\$]/,c:[e.CBCM,{cN:\"selector-id\",b:/#[A-Za-z0-9_-]+/},{cN:\"selector-class\",b:/\\.[A-Za-z0-9_-]+/},{cN:\"selector-attr\",b:/\\[/,e:/\\]/,i:\"$\"},{cN:\"selector-pseudo\",b:/:(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\"'.]+/},{b:\"@(font-face|page)\",l:\"[a-z-]+\",k:\"font-face page\"},{b:\"@\",e:\"[{;]\",i:/:/,c:[{cN:\"keyword\",b:/\\w+/},{b:/\\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:\"selector-tag\",b:t,r:0},{b:\"{\",e:\"}\",i:/\\S/,c:[e.CBCM,r]}]}}),e.registerLanguage(\"diff\",function(e){return{aliases:[\"patch\"],c:[{cN:\"meta\",r:10,v:[{b:/^@@ +\\-\\d+,\\d+ +\\+\\d+,\\d+ +@@$/},{b:/^\\*\\*\\* +\\d+,\\d+ +\\*\\*\\*\\*$/},{b:/^\\-\\-\\- +\\d+,\\d+ +\\-\\-\\-\\-$/}]},{cN:\"comment\",v:[{b:/Index: /,e:/$/},{b:/={3,}/,e:/$/},{b:/^\\-{3}/,e:/$/},{b:/^\\*{3} /,e:/$/},{b:/^\\+{3}/,e:/$/},{b:/\\*{5}/,e:/\\*{5}$/}]},{cN:\"addition\",b:\"^\\\\+\",e:\"$\"},{cN:\"deletion\",b:\"^\\\\-\",e:\"$\"},{cN:\"addition\",b:\"^\\\\!\",e:\"$\"}]}}),e.registerLanguage(\"http\",function(e){var t=\"HTTP/[0-9\\\\.]+\";return{aliases:[\"https\"],i:\"\\\\S\",c:[{b:\"^\"+t,e:\"$\",c:[{cN:\"number\",b:\"\\\\b\\\\d{3}\\\\b\"}]},{b:\"^[A-Z]+ (.*?) \"+t+\"$\",rB:!0,e:\"$\",c:[{cN:\"string\",b:\" \",e:\" \",eB:!0,eE:!0},{b:t},{cN:\"keyword\",b:\"[A-Z]+\"}]},{cN:\"attribute\",b:\"^\\\\w\",e:\": \",eE:!0,i:\"\\\\n|\\\\s|=\",starts:{e:\"$\",r:0}},{b:\"\\\\n\\\\n\",starts:{sL:[],eW:!0}}]}}),e.registerLanguage(\"ini\",function(e){var t={cN:\"string\",c:[e.BE],v:[{b:\"'''\",e:\"'''\",r:10},{b:'\"\"\"',e:'\"\"\"',r:10},{b:'\"',e:'\"'},{b:\"'\",e:\"'\"}]};return{aliases:[\"toml\"],cI:!0,i:/\\S/,c:[e.C(\";\",\"$\"),e.HCM,{cN:\"section\",b:/^\\s*\\[+/,e:/\\]+/},{b:/^[a-z0-9\\[\\]_-]+\\s*=\\s*/,e:\"$\",rB:!0,c:[{cN:\"attr\",b:/[a-z0-9\\[\\]_-]+/},{b:/=/,eW:!0,r:0,c:[{cN:\"literal\",b:/\\bon|off|true|false|yes|no\\b/},{cN:\"variable\",v:[{b:/\\$[\\w\\d\"][\\w\\d_]*/},{b:/\\$\\{(.*?)}/}]},t,{cN:\"number\",b:/([\\+\\-]+)?[\\d]+_[\\d_]+/},e.NM]}]}]}}),e.registerLanguage(\"java\",function(e){var t=\"[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*\",r=t+\"(<\"+t+\"(\\\\s*,\\\\s*\"+t+\")*>)?\",a=\"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do\",n=\"\\\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\\\d]+[\\\\d_]+[\\\\d]+|[\\\\d]+)(\\\\.([\\\\d]+[\\\\d_]+[\\\\d]+|[\\\\d]+))?|\\\\.([\\\\d]+[\\\\d_]+[\\\\d]+|[\\\\d]+))([eE][-+]?\\\\d+)?)[lLfF]?\",i={cN:\"number\",b:n,r:0};return{aliases:[\"jsp\"],k:a,i:/<\\/|#/,c:[e.C(\"/\\\\*\\\\*\",\"\\\\*/\",{r:0,c:[{b:/\\w+@/,r:0},{cN:\"doctag\",b:\"@[A-Za-z]+\"}]}),e.CLCM,e.CBCM,e.ASM,e.QSM,{cN:\"class\",bK:\"class interface\",e:/[{;=]/,eE:!0,k:\"class interface\",i:/[:\"\\[\\]]/,c:[{bK:\"extends implements\"},e.UTM]},{bK:\"new throw return else\",r:0},{cN:\"function\",b:\"(\"+r+\"\\\\s+)+\"+e.UIR+\"\\\\s*\\\\(\",rB:!0,e:/[{;=]/,eE:!0,k:a,c:[{b:e.UIR+\"\\\\s*\\\\(\",rB:!0,r:0,c:[e.UTM]},{cN:\"params\",b:/\\(/,e:/\\)/,k:a,r:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]},i,{cN:\"meta\",b:\"@[A-Za-z]+\"}]}}),e.registerLanguage(\"javascript\",function(e){var t=\"[A-Za-z$_][0-9A-Za-z$_]*\",r={keyword:\"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as\",literal:\"true false null undefined NaN Infinity\",built_in:\"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise\"},a={cN:\"number\",v:[{b:\"\\\\b(0[bB][01]+)\"},{b:\"\\\\b(0[oO][0-7]+)\"},{b:e.CNR}],r:0},n={cN:\"subst\",b:\"\\\\$\\\\{\",e:\"\\\\}\",k:r,c:[]},i={cN:\"string\",b:\"`\",e:\"`\",c:[e.BE,n]};n.c=[e.ASM,e.QSM,i,a,e.RM];var s=n.c.concat([e.CBCM,e.CLCM]);return{aliases:[\"js\",\"jsx\"],k:r,c:[{cN:\"meta\",r:10,b:/^\\s*['\"]use (strict|asm)['\"]/},{cN:\"meta\",b:/^#!/,e:/$/},e.ASM,e.QSM,i,e.CLCM,e.CBCM,a,{b:/[{,]\\s*/,r:0,c:[{b:t+\"\\\\s*:\",rB:!0,r:0,c:[{cN:\"attr\",b:t,r:0}]}]},{b:\"(\"+e.RSR+\"|\\\\b(case|return|throw)\\\\b)\\\\s*\",k:\"return throw case\",c:[e.CLCM,e.CBCM,e.RM,{cN:\"function\",b:\"(\\\\(.*?\\\\)|\"+t+\")\\\\s*=>\",rB:!0,e:\"\\\\s*=>\",c:[{cN:\"params\",v:[{b:t},{b:/\\(\\s*\\)/},{b:/\\(/,e:/\\)/,eB:!0,eE:!0,k:r,c:s}]}]},{b:/</,e:/(\\/\\w+|\\w+\\/)>/,sL:\"xml\",c:[{b:/<\\w+\\s*\\/>/,skip:!0},{b:/<\\w+/,e:/(\\/\\w+|\\w+\\/)>/,skip:!0,c:[{b:/<\\w+\\s*\\/>/,skip:!0},\"self\"]}]}],r:0},{cN:\"function\",bK:\"function\",e:/\\{/,eE:!0,c:[e.inherit(e.TM,{b:t}),{cN:\"params\",b:/\\(/,e:/\\)/,eB:!0,eE:!0,c:s}],i:/\\[|%/},{b:/\\$[(.]/},e.METHOD_GUARD,{cN:\"class\",bK:\"class\",e:/[{;=]/,eE:!0,i:/[:\"\\[\\]]/,c:[{bK:\"extends\"},e.UTM]},{bK:\"constructor\",e:/\\{/,eE:!0}],i:/#(?!!)/}}),e.registerLanguage(\"json\",function(e){var t={literal:\"true false null\"},r=[e.QSM,e.CNM],a={e:\",\",eW:!0,eE:!0,c:r,k:t},n={b:\"{\",e:\"}\",c:[{cN:\"attr\",b:/\"/,e:/\"/,c:[e.BE],i:\"\\\\n\"},e.inherit(a,{b:/:/})],i:\"\\\\S\"},i={b:\"\\\\[\",e:\"\\\\]\",c:[e.inherit(a)],i:\"\\\\S\"};return r.splice(r.length,0,n,i),{c:r,k:t,i:\"\\\\S\"}}),e.registerLanguage(\"makefile\",function(e){var t={cN:\"variable\",v:[{b:\"\\\\$\\\\(\"+e.UIR+\"\\\\)\",c:[e.BE]},{b:/\\$[@%<?\\^\\+\\*]/}]},r={cN:\"string\",b:/\"/,e:/\"/,c:[e.BE,t]},a={cN:\"variable\",b:/\\$\\([\\w-]+\\s/,e:/\\)/,k:{built_in:\"subst patsubst strip findstring filter filter-out sort word wordlist firstword lastword dir notdir suffix basename addsuffix addprefix join wildcard realpath abspath error warning shell origin flavor foreach if or and call eval file value\"},c:[t]},n={b:\"^\"+e.UIR+\"\\\\s*[:+?]?=\",i:\"\\\\n\",rB:!0,c:[{b:\"^\"+e.UIR,e:\"[:+?]?=\",eE:!0}]},i={cN:\"meta\",b:/^\\.PHONY:/,e:/$/,k:{\"meta-keyword\":\".PHONY\"},l:/[\\.\\w]+/},s={cN:\"section\",b:/^[^\\s]+:/,e:/$/,c:[t]};return{aliases:[\"mk\",\"mak\"],k:\"define endef undefine ifdef ifndef ifeq ifneq else endif include -include sinclude override export unexport private vpath\",l:/[\\w-]+/,c:[e.HCM,t,r,a,n,i,s]}}),e.registerLanguage(\"xml\",function(e){var t=\"[A-Za-z0-9\\\\._:-]+\",r={eW:!0,i:/</,r:0,c:[{cN:\"attr\",b:t,r:0},{b:/=\\s*/,r:0,c:[{cN:\"string\",endsParent:!0,v:[{b:/\"/,e:/\"/},{b:/'/,e:/'/},{b:/[^\\s\"'=<>`]+/}]}]}]};return{aliases:[\"html\",\"xhtml\",\"rss\",\"atom\",\"xjb\",\"xsd\",\"xsl\",\"plist\"],cI:!0,c:[{cN:\"meta\",b:\"<!DOCTYPE\",e:\">\",r:10,c:[{b:\"\\\\[\",e:\"\\\\]\"}]},e.C(\"<!--\",\"-->\",{r:10}),{b:\"<\\\\!\\\\[CDATA\\\\[\",e:\"\\\\]\\\\]>\",r:10},{b:/<\\?(php)?/,e:/\\?>/,sL:\"php\",c:[{b:\"/\\\\*\",e:\"\\\\*/\",skip:!0}]},{cN:\"tag\",b:\"<style(?=\\\\s|>|$)\",e:\">\",k:{name:\"style\"},c:[r],starts:{e:\"</style>\",rE:!0,sL:[\"css\",\"xml\"]}},{cN:\"tag\",b:\"<script(?=\\\\s|>|$)\",e:\">\",k:{name:\"script\"},c:[r],starts:{e:\"</script>\",rE:!0,sL:[\"actionscript\",\"javascript\",\"handlebars\",\"xml\"]}},{cN:\"meta\",v:[{b:/<\\?xml/,e:/\\?>/,r:10},{b:/<\\?\\w+/,e:/\\?>/}]},{cN:\"tag\",b:\"</?\",e:\"/?>\",c:[{cN:\"name\",b:/[^\\/><\\s]+/,r:0},r]}]}}),e.registerLanguage(\"markdown\",function(e){return{aliases:[\"md\",\"mkdown\",\"mkd\"],c:[{cN:\"section\",v:[{b:\"^#{1,6}\",e:\"$\"},{b:\"^.+?\\\\n[=-]{2,}$\"}]},{b:\"<\",e:\">\",sL:\"xml\",r:0},{cN:\"bullet\",b:\"^([*+-]|(\\\\d+\\\\.))\\\\s+\"},{cN:\"strong\",b:\"[*_]{2}.+?[*_]{2}\"},{cN:\"emphasis\",v:[{b:\"\\\\*.+?\\\\*\"},{b:\"_.+?_\",r:0}]},{cN:\"quote\",b:\"^>\\\\s+\",e:\"$\"},{cN:\"code\",v:[{b:\"^```w*s*$\",e:\"^```s*$\"},{b:\"`.+?`\"},{b:\"^( {4}|\t)\",e:\"$\",r:0}]},{b:\"^[-\\\\*]{3,}\",e:\"$\"},{b:\"\\\\[.+?\\\\][\\\\(\\\\[].*?[\\\\)\\\\]]\",rB:!0,c:[{cN:\"string\",b:\"\\\\[\",e:\"\\\\]\",eB:!0,rE:!0,r:0},{cN:\"link\",b:\"\\\\]\\\\(\",e:\"\\\\)\",eB:!0,eE:!0},{cN:\"symbol\",b:\"\\\\]\\\\[\",e:\"\\\\]\",eB:!0,eE:!0}],r:10},{b:/^\\[[^\\n]+\\]:/,rB:!0,c:[{cN:\"symbol\",b:/\\[/,e:/\\]/,eB:!0,eE:!0},{cN:\"link\",b:/:\\s*/,e:/$/,eB:!0}]}]}}),e.registerLanguage(\"nginx\",function(e){var t={cN:\"variable\",v:[{b:/\\$\\d+/},{b:/\\$\\{/,e:/}/},{b:\"[\\\\$\\\\@]\"+e.UIR}]},r={eW:!0,l:\"[a-z/_]+\",k:{literal:\"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll\"},r:0,i:\"=>\",c:[e.HCM,{cN:\"string\",c:[e.BE,t],v:[{b:/\"/,e:/\"/},{b:/'/,e:/'/}]},{b:\"([a-z]+):/\",e:\"\\\\s\",eW:!0,eE:!0,c:[t]},{cN:\"regexp\",c:[e.BE,t],v:[{b:\"\\\\s\\\\^\",e:\"\\\\s|{|;\",rE:!0},{b:\"~\\\\*?\\\\s+\",e:\"\\\\s|{|;\",rE:!0},{b:\"\\\\*(\\\\.[a-z\\\\-]+)+\"},{b:\"([a-z\\\\-]+\\\\.)+\\\\*\"}]},{cN:\"number\",b:\"\\\\b\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}(:\\\\d{1,5})?\\\\b\"},{cN:\"number\",b:\"\\\\b\\\\d+[kKmMgGdshdwy]*\\\\b\",r:0},t]};return{aliases:[\"nginxconf\"],c:[e.HCM,{b:e.UIR+\"\\\\s+{\",rB:!0,e:\"{\",c:[{cN:\"section\",b:e.UIR}],r:0},{b:e.UIR+\"\\\\s\",e:\";|{\",rB:!0,c:[{cN:\"attribute\",b:e.UIR,starts:r}],r:0}],i:\"[^\\\\s\\\\}]\"}}),e.registerLanguage(\"objectivec\",function(e){var t={cN:\"built_in\",b:\"\\\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\\\w+\"},r={keyword:\"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN\",literal:\"false true FALSE TRUE nil YES NO NULL\",built_in:\"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once\"},a=/[a-zA-Z@][a-zA-Z0-9_]*/,n=\"@interface @class @protocol @implementation\";return{aliases:[\"mm\",\"objc\",\"obj-c\"],k:r,l:a,i:\"</\",c:[t,e.CLCM,e.CBCM,e.CNM,e.QSM,{cN:\"string\",v:[{b:'@\"',e:'\"',i:\"\\\\n\",c:[e.BE]},{b:\"'\",e:\"[^\\\\\\\\]'\",i:\"[^\\\\\\\\][^']\"}]},{cN:\"meta\",b:\"#\",e:\"$\",c:[{cN:\"meta-string\",v:[{b:'\"',e:'\"'},{b:\"<\",e:\">\"}]}]},{cN:\"class\",b:\"(\"+n.split(\" \").join(\"|\")+\")\\\\b\",e:\"({|$)\",eE:!0,k:n,l:a,c:[e.UTM]},{b:\"\\\\.\"+e.UIR,r:0}]}}),e.registerLanguage(\"perl\",function(e){var t=\"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when\",r={cN:\"subst\",b:\"[$@]\\\\{\",e:\"\\\\}\",k:t},a={b:\"->{\",e:\"}\"},n={v:[{b:/\\$\\d/},{b:/[\\$%@](\\^\\w\\b|#\\w+(::\\w+)*|{\\w+}|\\w+(::\\w*)*)/},{b:/[\\$%@][^\\s\\w{]/,r:0}]},i=[e.BE,r,n],s=[n,e.HCM,e.C(\"^\\\\=\\\\w\",\"\\\\=cut\",{eW:!0}),a,{cN:\"string\",c:i,v:[{b:\"q[qwxr]?\\\\s*\\\\(\",e:\"\\\\)\",r:5},{b:\"q[qwxr]?\\\\s*\\\\[\",e:\"\\\\]\",r:5},{b:\"q[qwxr]?\\\\s*\\\\{\",e:\"\\\\}\",r:5},{b:\"q[qwxr]?\\\\s*\\\\|\",e:\"\\\\|\",r:5},{b:\"q[qwxr]?\\\\s*\\\\<\",e:\"\\\\>\",r:5},{b:\"qw\\\\s+q\",e:\"q\",r:5},{b:\"'\",e:\"'\",c:[e.BE]},{b:'\"',e:'\"'},{b:\"`\",e:\"`\",c:[e.BE]},{b:\"{\\\\w+}\",c:[],r:0},{b:\"-?\\\\w+\\\\s*\\\\=\\\\>\",c:[],r:0}]},{cN:\"number\",b:\"(\\\\b0[0-7_]+)|(\\\\b0x[0-9a-fA-F_]+)|(\\\\b[1-9][0-9_]*(\\\\.[0-9_]+)?)|[0_]\\\\b\",r:0},{b:\"(\\\\/\\\\/|\"+e.RSR+\"|\\\\b(split|return|print|reverse|grep)\\\\b)\\\\s*\",k:\"split return print reverse grep\",r:0,c:[e.HCM,{cN:\"regexp\",b:\"(s|tr|y)/(\\\\\\\\.|[^/])*/(\\\\\\\\.|[^/])*/[a-z]*\",r:10},{cN:\"regexp\",b:\"(m|qr)?/\",e:\"/[a-z]*\",c:[e.BE],r:0}]},{cN:\"function\",bK:\"sub\",e:\"(\\\\s*\\\\(.*?\\\\))?[;{]\",eE:!0,r:5,c:[e.TM]},{b:\"-\\\\w\\\\b\",r:0},{b:\"^__DATA__$\",e:\"^__END__$\",sL:\"mojolicious\",c:[{b:\"^@@.*\",e:\"$\",cN:\"comment\"}]}];return r.c=s,a.c=s,{aliases:[\"pl\",\"pm\"],l:/[\\w\\.]+/,k:t,c:s}}),e.registerLanguage(\"php\",function(e){var t={b:\"\\\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*\"},r={cN:\"meta\",b:/<\\?(php)?|\\?>/},a={cN:\"string\",c:[e.BE,r],v:[{b:'b\"',e:'\"'},{b:\"b'\",e:\"'\"},e.inherit(e.ASM,{i:null}),e.inherit(e.QSM,{i:null})]},n={v:[e.BNM,e.CNM]};return{aliases:[\"php3\",\"php4\",\"php5\",\"php6\"],cI:!0,k:\"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally\",c:[e.HCM,e.C(\"//\",\"$\",{c:[r]}),e.C(\"/\\\\*\",\"\\\\*/\",{c:[{cN:\"doctag\",b:\"@[A-Za-z]+\"}]}),e.C(\"__halt_compiler.+?;\",!1,{eW:!0,k:\"__halt_compiler\",l:e.UIR}),{cN:\"string\",b:/<<<['\"]?\\w+['\"]?$/,e:/^\\w+;?$/,c:[e.BE,{cN:\"subst\",v:[{b:/\\$\\w+/},{b:/\\{\\$/,e:/\\}/}]}]},r,{cN:\"keyword\",b:/\\$this\\b/},t,{b:/(::|->)+[a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*/},{cN:\"function\",bK:\"function\",e:/[;{]/,eE:!0,i:\"\\\\$|\\\\[|%\",c:[e.UTM,{cN:\"params\",b:\"\\\\(\",e:\"\\\\)\",c:[\"self\",t,e.CBCM,a,n]}]},{cN:\"class\",bK:\"class interface\",e:\"{\",eE:!0,i:/[:\\(\\$\"]/,c:[{bK:\"extends implements\"},e.UTM]},{bK:\"namespace\",e:\";\",i:/[\\.']/,c:[e.UTM]},{bK:\"use\",e:\";\",c:[e.UTM]},{b:\"=>\"},a,n]}}),e.registerLanguage(\"python\",function(e){var t={keyword:\"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False\",built_in:\"Ellipsis NotImplemented\"},r={cN:\"meta\",b:/^(>>>|\\.\\.\\.) /},a={cN:\"subst\",b:/\\{/,e:/\\}/,k:t,i:/#/},n={cN:\"string\",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[r],r:10},{b:/(u|b)?r?\"\"\"/,e:/\"\"\"/,c:[r],r:10},{b:/(fr|rf|f)'''/,e:/'''/,c:[r,a]},{b:/(fr|rf|f)\"\"\"/,e:/\"\"\"/,c:[r,a]},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)\"/,e:/\"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)\"/,e:/\"/},{b:/(fr|rf|f)'/,e:/'/,c:[a]},{b:/(fr|rf|f)\"/,e:/\"/,c:[a]},e.ASM,e.QSM]},i={cN:\"number\",r:0,v:[{b:e.BNR+\"[lLjJ]?\"},{b:\"\\\\b(0o[0-7]+)[lLjJ]?\"},{b:e.CNR+\"[lLjJ]?\"}]},s={cN:\"params\",b:/\\(/,e:/\\)/,c:[\"self\",r,i,n]};return a.c=[n,i,r],{aliases:[\"py\",\"gyp\"],k:t,i:/(<\\/|->|\\?)|=>/,c:[r,i,n,e.HCM,{v:[{cN:\"function\",bK:\"def\"},{cN:\"class\",bK:\"class\"}],e:/:/,i:/[${=;\\n,]/,c:[e.UTM,s,{b:/->/,eW:!0,k:\"None\"}]},{cN:\"meta\",b:/^[\\t ]*@/,e:/$/},{b:/\\b(print|exec)\\(/}]}}),e.registerLanguage(\"ruby\",function(e){\nvar t=\"[a-zA-Z_]\\\\w*[!?=]?|[-+~]\\\\@|<<|>>|=~|===?|<=>|[<>]=?|\\\\*\\\\*|[-/+%^&*~`|]|\\\\[\\\\]=?\",r={keyword:\"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor\",literal:\"true false nil\"},a={cN:\"doctag\",b:\"@[A-Za-z]+\"},n={b:\"#<\",e:\">\"},i=[e.C(\"#\",\"$\",{c:[a]}),e.C(\"^\\\\=begin\",\"^\\\\=end\",{c:[a],r:10}),e.C(\"^__END__\",\"\\\\n$\")],s={cN:\"subst\",b:\"#\\\\{\",e:\"}\",k:r},c={cN:\"string\",c:[e.BE,s],v:[{b:/'/,e:/'/},{b:/\"/,e:/\"/},{b:/`/,e:/`/},{b:\"%[qQwWx]?\\\\(\",e:\"\\\\)\"},{b:\"%[qQwWx]?\\\\[\",e:\"\\\\]\"},{b:\"%[qQwWx]?{\",e:\"}\"},{b:\"%[qQwWx]?<\",e:\">\"},{b:\"%[qQwWx]?/\",e:\"/\"},{b:\"%[qQwWx]?%\",e:\"%\"},{b:\"%[qQwWx]?-\",e:\"-\"},{b:\"%[qQwWx]?\\\\|\",e:\"\\\\|\"},{b:/\\B\\?(\\\\\\d{1,3}|\\\\x[A-Fa-f0-9]{1,2}|\\\\u[A-Fa-f0-9]{4}|\\\\?\\S)\\b/},{b:/<<(-?)\\w+$/,e:/^\\s*\\w+$/}]},o={cN:\"params\",b:\"\\\\(\",e:\"\\\\)\",endsParent:!0,k:r},l=[c,n,{cN:\"class\",bK:\"class module\",e:\"$|;\",i:/=/,c:[e.inherit(e.TM,{b:\"[A-Za-z_]\\\\w*(::\\\\w+)*(\\\\?|\\\\!)?\"}),{b:\"<\\\\s*\",c:[{b:\"(\"+e.IR+\"::)?\"+e.IR}]}].concat(i)},{cN:\"function\",bK:\"def\",e:\"$|;\",c:[e.inherit(e.TM,{b:t}),o].concat(i)},{b:e.IR+\"::\"},{cN:\"symbol\",b:e.UIR+\"(\\\\!|\\\\?)?:\",r:0},{cN:\"symbol\",b:\":(?!\\\\s)\",c:[c,{b:t}],r:0},{cN:\"number\",b:\"(\\\\b0[0-7_]+)|(\\\\b0x[0-9a-fA-F_]+)|(\\\\b[1-9][0-9_]*(\\\\.[0-9_]+)?)|[0_]\\\\b\",r:0},{b:\"(\\\\$\\\\W)|((\\\\$|\\\\@\\\\@?)(\\\\w+))\"},{cN:\"params\",b:/\\|/,e:/\\|/,k:r},{b:\"(\"+e.RSR+\"|unless)\\\\s*\",k:\"unless\",c:[n,{cN:\"regexp\",c:[e.BE,s],i:/\\n/,v:[{b:\"/\",e:\"/[a-z]*\"},{b:\"%r{\",e:\"}[a-z]*\"},{b:\"%r\\\\(\",e:\"\\\\)[a-z]*\"},{b:\"%r!\",e:\"![a-z]*\"},{b:\"%r\\\\[\",e:\"\\\\][a-z]*\"}]}].concat(i),r:0}].concat(i);s.c=l,o.c=l;var u=\"[>?]>\",d=\"[\\\\w#]+\\\\(\\\\w+\\\\):\\\\d+:\\\\d+>\",b=\"(\\\\w+-)?\\\\d+\\\\.\\\\d+\\\\.\\\\d(p\\\\d+)?[^>]+>\",p=[{b:/^\\s*=>/,starts:{e:\"$\",c:l}},{cN:\"meta\",b:\"^(\"+u+\"|\"+d+\"|\"+b+\")\",starts:{e:\"$\",c:l}}];return{aliases:[\"rb\",\"gemspec\",\"podspec\",\"thor\",\"irb\"],k:r,i:/\\/\\*/,c:i.concat(p).concat(l)}}),e.registerLanguage(\"shell\",function(e){return{aliases:[\"console\"],c:[{cN:\"meta\",b:\"^\\\\s{0,3}[\\\\w\\\\d\\\\[\\\\]()@-]*[>%$#]\",starts:{e:\"$\",sL:\"bash\"}}]}}),e.registerLanguage(\"sql\",function(e){var t=e.C(\"--\",\"$\");return{cI:!0,i:/[<>{}*#]/,c:[{bK:\"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment\",e:/;/,eW:!0,l:/[\\w\\.]+/,k:{keyword:\"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek\",literal:\"true false null\",built_in:\"array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text varchar varying void\"},c:[{cN:\"string\",b:\"'\",e:\"'\",c:[e.BE,{b:\"''\"}]},{cN:\"string\",b:'\"',e:'\"',c:[e.BE,{b:'\"\"'}]},{cN:\"string\",b:\"`\",e:\"`\",c:[e.BE]},e.CNM,e.CBCM,t]},e.CBCM,t]}}),e});"
  },
  {
    "path": "Telegram/Resources/iv_html/morphdom-umd.min.2.7.2.js",
    "content": "(function(global,factory){typeof exports===\"object\"&&typeof module!==\"undefined\"?module.exports=factory():typeof define===\"function\"&&define.amd?define(factory):(global=global||self,global.morphdom=factory())})(this,function(){\"use strict\";var DOCUMENT_FRAGMENT_NODE=11;function morphAttrs(fromNode,toNode){var toNodeAttrs=toNode.attributes;var attr;var attrName;var attrNamespaceURI;var attrValue;var fromValue;if(toNode.nodeType===DOCUMENT_FRAGMENT_NODE||fromNode.nodeType===DOCUMENT_FRAGMENT_NODE){return}for(var i=toNodeAttrs.length-1;i>=0;i--){attr=toNodeAttrs[i];attrName=attr.name;attrNamespaceURI=attr.namespaceURI;attrValue=attr.value;if(attrNamespaceURI){attrName=attr.localName||attrName;fromValue=fromNode.getAttributeNS(attrNamespaceURI,attrName);if(fromValue!==attrValue){if(attr.prefix===\"xmlns\"){attrName=attr.name}fromNode.setAttributeNS(attrNamespaceURI,attrName,attrValue)}}else{fromValue=fromNode.getAttribute(attrName);if(fromValue!==attrValue){fromNode.setAttribute(attrName,attrValue)}}}var fromNodeAttrs=fromNode.attributes;for(var d=fromNodeAttrs.length-1;d>=0;d--){attr=fromNodeAttrs[d];attrName=attr.name;attrNamespaceURI=attr.namespaceURI;if(attrNamespaceURI){attrName=attr.localName||attrName;if(!toNode.hasAttributeNS(attrNamespaceURI,attrName)){fromNode.removeAttributeNS(attrNamespaceURI,attrName)}}else{if(!toNode.hasAttribute(attrName)){fromNode.removeAttribute(attrName)}}}}var range;var NS_XHTML=\"http://www.w3.org/1999/xhtml\";var doc=typeof document===\"undefined\"?undefined:document;var HAS_TEMPLATE_SUPPORT=!!doc&&\"content\"in doc.createElement(\"template\");var HAS_RANGE_SUPPORT=!!doc&&doc.createRange&&\"createContextualFragment\"in doc.createRange();function createFragmentFromTemplate(str){var template=doc.createElement(\"template\");template.innerHTML=str;return template.content.childNodes[0]}function createFragmentFromRange(str){if(!range){range=doc.createRange();range.selectNode(doc.body)}var fragment=range.createContextualFragment(str);return fragment.childNodes[0]}function createFragmentFromWrap(str){var fragment=doc.createElement(\"body\");fragment.innerHTML=str;return fragment.childNodes[0]}function toElement(str){str=str.trim();if(HAS_TEMPLATE_SUPPORT){return createFragmentFromTemplate(str)}else if(HAS_RANGE_SUPPORT){return createFragmentFromRange(str)}return createFragmentFromWrap(str)}function compareNodeNames(fromEl,toEl){var fromNodeName=fromEl.nodeName;var toNodeName=toEl.nodeName;var fromCodeStart,toCodeStart;if(fromNodeName===toNodeName){return true}fromCodeStart=fromNodeName.charCodeAt(0);toCodeStart=toNodeName.charCodeAt(0);if(fromCodeStart<=90&&toCodeStart>=97){return fromNodeName===toNodeName.toUpperCase()}else if(toCodeStart<=90&&fromCodeStart>=97){return toNodeName===fromNodeName.toUpperCase()}else{return false}}function createElementNS(name,namespaceURI){return!namespaceURI||namespaceURI===NS_XHTML?doc.createElement(name):doc.createElementNS(namespaceURI,name)}function moveChildren(fromEl,toEl){var curChild=fromEl.firstChild;while(curChild){var nextChild=curChild.nextSibling;toEl.appendChild(curChild);curChild=nextChild}return toEl}function syncBooleanAttrProp(fromEl,toEl,name){if(fromEl[name]!==toEl[name]){fromEl[name]=toEl[name];if(fromEl[name]){fromEl.setAttribute(name,\"\")}else{fromEl.removeAttribute(name)}}}var specialElHandlers={OPTION:function(fromEl,toEl){var parentNode=fromEl.parentNode;if(parentNode){var parentName=parentNode.nodeName.toUpperCase();if(parentName===\"OPTGROUP\"){parentNode=parentNode.parentNode;parentName=parentNode&&parentNode.nodeName.toUpperCase()}if(parentName===\"SELECT\"&&!parentNode.hasAttribute(\"multiple\")){if(fromEl.hasAttribute(\"selected\")&&!toEl.selected){fromEl.setAttribute(\"selected\",\"selected\");fromEl.removeAttribute(\"selected\")}parentNode.selectedIndex=-1}}syncBooleanAttrProp(fromEl,toEl,\"selected\")},INPUT:function(fromEl,toEl){syncBooleanAttrProp(fromEl,toEl,\"checked\");syncBooleanAttrProp(fromEl,toEl,\"disabled\");if(fromEl.value!==toEl.value){fromEl.value=toEl.value}if(!toEl.hasAttribute(\"value\")){fromEl.removeAttribute(\"value\")}},TEXTAREA:function(fromEl,toEl){var newValue=toEl.value;if(fromEl.value!==newValue){fromEl.value=newValue}var firstChild=fromEl.firstChild;if(firstChild){var oldValue=firstChild.nodeValue;if(oldValue==newValue||!newValue&&oldValue==fromEl.placeholder){return}firstChild.nodeValue=newValue}},SELECT:function(fromEl,toEl){if(!toEl.hasAttribute(\"multiple\")){var selectedIndex=-1;var i=0;var curChild=fromEl.firstChild;var optgroup;var nodeName;while(curChild){nodeName=curChild.nodeName&&curChild.nodeName.toUpperCase();if(nodeName===\"OPTGROUP\"){optgroup=curChild;curChild=optgroup.firstChild}else{if(nodeName===\"OPTION\"){if(curChild.hasAttribute(\"selected\")){selectedIndex=i;break}i++}curChild=curChild.nextSibling;if(!curChild&&optgroup){curChild=optgroup.nextSibling;optgroup=null}}}fromEl.selectedIndex=selectedIndex}}};var ELEMENT_NODE=1;var DOCUMENT_FRAGMENT_NODE$1=11;var TEXT_NODE=3;var COMMENT_NODE=8;function noop(){}function defaultGetNodeKey(node){if(node){return node.getAttribute&&node.getAttribute(\"id\")||node.id}}function morphdomFactory(morphAttrs){return function morphdom(fromNode,toNode,options){if(!options){options={}}if(typeof toNode===\"string\"){if(fromNode.nodeName===\"#document\"||fromNode.nodeName===\"HTML\"||fromNode.nodeName===\"BODY\"){var toNodeHtml=toNode;toNode=doc.createElement(\"html\");toNode.innerHTML=toNodeHtml}else{toNode=toElement(toNode)}}else if(toNode.nodeType===DOCUMENT_FRAGMENT_NODE$1){toNode=toNode.firstElementChild}var getNodeKey=options.getNodeKey||defaultGetNodeKey;var onBeforeNodeAdded=options.onBeforeNodeAdded||noop;var onNodeAdded=options.onNodeAdded||noop;var onBeforeElUpdated=options.onBeforeElUpdated||noop;var onElUpdated=options.onElUpdated||noop;var onBeforeNodeDiscarded=options.onBeforeNodeDiscarded||noop;var onNodeDiscarded=options.onNodeDiscarded||noop;var onBeforeElChildrenUpdated=options.onBeforeElChildrenUpdated||noop;var skipFromChildren=options.skipFromChildren||noop;var addChild=options.addChild||function(parent,child){return parent.appendChild(child)};var childrenOnly=options.childrenOnly===true;var fromNodesLookup=Object.create(null);var keyedRemovalList=[];function addKeyedRemoval(key){keyedRemovalList.push(key)}function walkDiscardedChildNodes(node,skipKeyedNodes){if(node.nodeType===ELEMENT_NODE){var curChild=node.firstChild;while(curChild){var key=undefined;if(skipKeyedNodes&&(key=getNodeKey(curChild))){addKeyedRemoval(key)}else{onNodeDiscarded(curChild);if(curChild.firstChild){walkDiscardedChildNodes(curChild,skipKeyedNodes)}}curChild=curChild.nextSibling}}}function removeNode(node,parentNode,skipKeyedNodes){if(onBeforeNodeDiscarded(node)===false){return}if(parentNode){parentNode.removeChild(node)}onNodeDiscarded(node);walkDiscardedChildNodes(node,skipKeyedNodes)}function indexTree(node){if(node.nodeType===ELEMENT_NODE||node.nodeType===DOCUMENT_FRAGMENT_NODE$1){var curChild=node.firstChild;while(curChild){var key=getNodeKey(curChild);if(key){fromNodesLookup[key]=curChild}indexTree(curChild);curChild=curChild.nextSibling}}}indexTree(fromNode);function handleNodeAdded(el){onNodeAdded(el);var curChild=el.firstChild;while(curChild){var nextSibling=curChild.nextSibling;var key=getNodeKey(curChild);if(key){var unmatchedFromEl=fromNodesLookup[key];if(unmatchedFromEl&&compareNodeNames(curChild,unmatchedFromEl)){curChild.parentNode.replaceChild(unmatchedFromEl,curChild);morphEl(unmatchedFromEl,curChild)}else{handleNodeAdded(curChild)}}else{handleNodeAdded(curChild)}curChild=nextSibling}}function cleanupFromEl(fromEl,curFromNodeChild,curFromNodeKey){while(curFromNodeChild){var fromNextSibling=curFromNodeChild.nextSibling;if(curFromNodeKey=getNodeKey(curFromNodeChild)){addKeyedRemoval(curFromNodeKey)}else{removeNode(curFromNodeChild,fromEl,true)}curFromNodeChild=fromNextSibling}}function morphEl(fromEl,toEl,childrenOnly){var toElKey=getNodeKey(toEl);if(toElKey){delete fromNodesLookup[toElKey]}if(!childrenOnly){if(onBeforeElUpdated(fromEl,toEl)===false){return}morphAttrs(fromEl,toEl);onElUpdated(fromEl);if(onBeforeElChildrenUpdated(fromEl,toEl)===false){return}}if(fromEl.nodeName!==\"TEXTAREA\"){morphChildren(fromEl,toEl)}else{specialElHandlers.TEXTAREA(fromEl,toEl)}}function morphChildren(fromEl,toEl){var skipFrom=skipFromChildren(fromEl,toEl);var curToNodeChild=toEl.firstChild;var curFromNodeChild=fromEl.firstChild;var curToNodeKey;var curFromNodeKey;var fromNextSibling;var toNextSibling;var matchingFromEl;outer:while(curToNodeChild){toNextSibling=curToNodeChild.nextSibling;curToNodeKey=getNodeKey(curToNodeChild);while(!skipFrom&&curFromNodeChild){fromNextSibling=curFromNodeChild.nextSibling;if(curToNodeChild.isSameNode&&curToNodeChild.isSameNode(curFromNodeChild)){curToNodeChild=toNextSibling;curFromNodeChild=fromNextSibling;continue outer}curFromNodeKey=getNodeKey(curFromNodeChild);var curFromNodeType=curFromNodeChild.nodeType;var isCompatible=undefined;if(curFromNodeType===curToNodeChild.nodeType){if(curFromNodeType===ELEMENT_NODE){if(curToNodeKey){if(curToNodeKey!==curFromNodeKey){if(matchingFromEl=fromNodesLookup[curToNodeKey]){if(fromNextSibling===matchingFromEl){isCompatible=false}else{fromEl.insertBefore(matchingFromEl,curFromNodeChild);if(curFromNodeKey){addKeyedRemoval(curFromNodeKey)}else{removeNode(curFromNodeChild,fromEl,true)}curFromNodeChild=matchingFromEl;curFromNodeKey=getNodeKey(curFromNodeChild)}}else{isCompatible=false}}}else if(curFromNodeKey){isCompatible=false}isCompatible=isCompatible!==false&&compareNodeNames(curFromNodeChild,curToNodeChild);if(isCompatible){morphEl(curFromNodeChild,curToNodeChild)}}else if(curFromNodeType===TEXT_NODE||curFromNodeType==COMMENT_NODE){isCompatible=true;if(curFromNodeChild.nodeValue!==curToNodeChild.nodeValue){curFromNodeChild.nodeValue=curToNodeChild.nodeValue}}}if(isCompatible){curToNodeChild=toNextSibling;curFromNodeChild=fromNextSibling;continue outer}if(curFromNodeKey){addKeyedRemoval(curFromNodeKey)}else{removeNode(curFromNodeChild,fromEl,true)}curFromNodeChild=fromNextSibling}if(curToNodeKey&&(matchingFromEl=fromNodesLookup[curToNodeKey])&&compareNodeNames(matchingFromEl,curToNodeChild)){if(!skipFrom){addChild(fromEl,matchingFromEl)}morphEl(matchingFromEl,curToNodeChild)}else{var onBeforeNodeAddedResult=onBeforeNodeAdded(curToNodeChild);if(onBeforeNodeAddedResult!==false){if(onBeforeNodeAddedResult){curToNodeChild=onBeforeNodeAddedResult}if(curToNodeChild.actualize){curToNodeChild=curToNodeChild.actualize(fromEl.ownerDocument||doc)}addChild(fromEl,curToNodeChild);handleNodeAdded(curToNodeChild)}}curToNodeChild=toNextSibling;curFromNodeChild=fromNextSibling}cleanupFromEl(fromEl,curFromNodeChild,curFromNodeKey);var specialElHandler=specialElHandlers[fromEl.nodeName];if(specialElHandler){specialElHandler(fromEl,toEl)}}var morphedNode=fromNode;var morphedNodeType=morphedNode.nodeType;var toNodeType=toNode.nodeType;if(!childrenOnly){if(morphedNodeType===ELEMENT_NODE){if(toNodeType===ELEMENT_NODE){if(!compareNodeNames(fromNode,toNode)){onNodeDiscarded(fromNode);morphedNode=moveChildren(fromNode,createElementNS(toNode.nodeName,toNode.namespaceURI))}}else{morphedNode=toNode}}else if(morphedNodeType===TEXT_NODE||morphedNodeType===COMMENT_NODE){if(toNodeType===morphedNodeType){if(morphedNode.nodeValue!==toNode.nodeValue){morphedNode.nodeValue=toNode.nodeValue}return morphedNode}else{morphedNode=toNode}}}if(morphedNode===toNode){onNodeDiscarded(fromNode)}else{if(toNode.isSameNode&&toNode.isSameNode(morphedNode)){return}morphEl(morphedNode,toNode,childrenOnly);if(keyedRemovalList){for(var i=0,len=keyedRemovalList.length;i<len;i++){var elToRemove=fromNodesLookup[keyedRemovalList[i]];if(elToRemove){removeNode(elToRemove,elToRemove.parentNode,false)}}}}if(!childrenOnly&&morphedNode!==fromNode&&fromNode.parentNode){if(morphedNode.actualize){morphedNode=morphedNode.actualize(fromNode.ownerDocument||doc)}fromNode.parentNode.replaceChild(morphedNode,fromNode)}return morphedNode}}var morphdom=morphdomFactory(morphAttrs);return morphdom});"
  },
  {
    "path": "Telegram/Resources/iv_html/page.css",
    "content": ":root {\n\t--font-sans: -apple-system, BlinkMacSystemFont, avenir next, avenir, Segoe UI Variable Text, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, tahoma, arial, sans-serif;\n\t--font-serif: Iowan Old Style, Apple Garamond, Baskerville, Georgia, Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;\n\t--font-mono: Menlo, Cascadia Code, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;\n}\n\nbody {\n\tfont-family: var(--font-sans);\n\tfont-size: 17px;\n\tline-height: 25px;\n\tpadding: 0;\n\tmargin: 0;\n\tbackground-color: var(--td-window-bg);\n\tcolor: var(--td-window-fg);\n\tzoom: var(--td-zoom-percentage);\n}\n\nhtml.custom_scroll ::-webkit-scrollbar {\n\tborder-radius: 5px !important;\n\tborder: 3px solid transparent !important;\n\tbackground-color: var(--td-scroll-bg) !important;\n\tbackground-clip: content-box !important;\n\twidth: 10px !important;\n}\nhtml.custom_scroll ::-webkit-scrollbar:hover {\n\tbackground-color: var(--td-scroll-bg-over) !important;\n}\nhtml.custom_scroll ::-webkit-scrollbar-thumb {\n\tborder-radius: 5px !important;\n\tborder: 3px solid transparent !important;\n\tbackground-color: var(--td-scroll-bar-bg) !important;\n\tbackground-clip: content-box !important;\n}\nhtml.custom_scroll ::-webkit-scrollbar-thumb:hover {\n\tbackground-color: var(--td-scroll-bar-bg-over) !important;\n}\n\n.fixed_button {\n\tposition: fixed;\n\tbackground-color: var(--td-history-to-down-bg);\n\tborder: none;\n\tborder-radius: 50%;\n\twidth: 32px;\n\theight: 32px;\n\tbox-shadow: 0 0 3px 0px var(--td-history-to-down-shadow);\n\tcursor: pointer;\n\toutline: none;\n\tz-index: 1000;\n\toverflow: hidden;\n\tuser-select: none;\n\tdisplay: flex;\n\tjustify-content: center;\n\talign-items: center;\n\tpadding: 0px;\n}\n.fixed_button:hover {\n\tbackground-color: var(--td-history-to-down-bg-over);\n}\n.fixed_button svg {\n\tfill: none;\n\tposition: relative;\n\tz-index: 1;\n\twidth: 24px;\n\theight: 24px;\n}\n.fixed_button .ripple .inner {\n\tposition: absolute;\n\tborder-radius: 50%;\n\ttransform: scale(0);\n\topacity: 1;\n\tanimation: ripple 650ms cubic-bezier(0.22, 1, 0.36, 1) forwards;\n\tbackground-color: var(--td-history-to-down-bg-ripple);\n}\n.fixed_button .ripple.hiding {\n\tanimation: fadeOut 200ms linear forwards;\n}\n@keyframes ripple {\n\tto {\n\t\ttransform: scale(2);\n\t}\n}\n@keyframes fadeOut {\n\tto {\n\t\topacity: 0;\n\t}\n}\n@keyframes fadeIn {\n\tto {\n\t\topacity: 1;\n\t}\n}\n#menu_page_blocker {\n\tz-index: 999;\n\tposition: fixed;\n\twidth: 100%;\n\theight: 100%;\n}\n#top_shadow {\n\tz-index: 999;\n\tposition: fixed;\n\ttop: 0;\n\theight: 1px;\n\twidth: 100%;\n\tleft: 0;\n\tbackground-color: var(--td-shadow-fg)\n}\n#bottom_up path {\n\tstroke: var(--td-history-to-down-fg);\n}\n#bottom_up path {\n\tstroke-width: 1.4;\n}\n#bottom_up:hover path {\n\tstroke: var(--td-history-to-down-fg-over);\n}\n#bottom_up {\n\tbottom: 10px;\n\tright: 10px;\n\ttransition: bottom 200ms linear;\n}\n#bottom_up.hidden {\n\tbottom: -36px;\n}\n\n.page-scroll {\n\tposition: absolute;\n\twidth: 100%;\n\theight: 100%;\n\toverflow-x: hidden;\n\toverflow-y: auto;\n}\n.page-scroll:focus {\n\toutline: none;\n}\n.page-slide {\n\tposition: relative;\n\twidth: 100%;\n\tmin-height: 100%;\n\tmargin-left: 0%;\n\ttransition: margin 240ms ease-in-out;\n}\n.page-footer {\n\theight: 32px;\n\tmargin-top: -32px;\n\tbackground: var(--td-window-bg-over);\n}\n.page-footer .content {\n\tpadding: 3px 18px;\n\tfont-size: 15px;\n\tcolor: var(--td-window-sub-text-fg);\n\ttext-align: center;\n}\n.page-footer .wrong {\n\tposition: relative;\n\tpadding: 5px;\n\tmargin: -5px;\n\tcolor: var(--td-window-sub-text-fg);\n\ttext-decoration: none;\n\tcursor: pointer;\n}\n.page-footer .wrong:hover {\n\ttext-decoration: underline;\n}\n.hidden-left,\n.hidden-right {\n\tpointer-events: none;\n}\n.hidden-left .page-slide {\n\tmargin-left: -100%;\n}\n.hidden-right .page-slide {\n\tmargin-left: 100%;\n}\narticle {\n\tpadding-bottom: 40px;\n\toverflow-y: hidden;\n\toverflow-x: auto;\n\twhite-space: pre-wrap;\n\tmax-width: 732px;\n\tmargin: 0 auto;\n}\narticle h1,\narticle h2 {\n\tfont-family: var(--font-serif);\n\tfont-size: 28px;\n\tline-height: 31px;\n\tmargin: 21px 18px 12px;\n\tfont-weight: normal;\n\tmin-height: 31px;\n}\narticle h2 {\n\tfont-size: 24px;\n\tline-height: 30px;\n\tmargin: -6px 18px 12px;\n\tcolor: var(--td-window-sub-text-fg);\n}\narticle h5 {\n\tmargin: 21px 18px 12px;\n}\narticle address {\n\tfont-size: 15px;\n\tcolor: var(--td-window-sub-text-fg);\n\tmargin: 12px 18px 21px;\n\tfont-style: normal;\n}\narticle.rtl address {\n\tdirection: ltr;\n\ttext-align: right;\n}\narticle address figure {\n\twidth: 25px;\n\theight: 25px;\n\tfloat: right;\n\tmargin: 0 0 0 12px;\n\tbackground: no-repeat center;\n\tbackground-size: cover;\n\tborder-radius: 50%;\n}\narticle address a,\narticle address a[href] {\n\tcolor: var(--td-window-sub-text-fg);\n}\narticle address a[href] {\n\ttext-decoration: underline;\n}\narticle a[href] {\n\tcolor: var(--td-window-active-text-fg);\n\ttext-decoration: none;\n}\narticle a.internal-iv-link {\n\tborder-radius: 3px;\n\tmargin: 0px -3px;\n\tpadding: 0px 3px;\n\tposition: relative;\n\tbackground: var(--td-light-button-bg-over);\n\tcolor: var(--td-light-button-fg);\n}\narticle span.reference {\n\tborder: dotted var(--td-window-sub-text-fg);\n\tborder-width: 1px 1px 1px 2px;\n\tbackground: rgba(255, 255, 255, 0.7);\n\tmargin: 0 1px;\n\tpadding: 2px;\n\tposition: relative;\n}\narticle.rtl span.reference {\n\tborder-width: 1px 0 1px 1px;\n}\narticle code {\n\tfont-size: 0.94em;\n\tbackground: var(--td-box-divider-bg);\n\tborder-radius: 2px;\n\tpadding: 0 3px 1px;\n}\narticle mark {\n\tbackground: var(--td-window-bg-over);\n\tcolor: var(--td-window-fg);\n\tborder-radius: 2px;\n\tpadding: 0 3px 1px;\n}\narticle sup,\narticle sub {\n\tfont-size: 0.75em;\n\tline-height: 1;\n}\narticle p {\n\tmargin: 0 18px 12px;\n\tword-wrap: break-word;\n}\narticle ul p,\narticle ol p {\n\tmargin: 0 0 6px;\n}\narticle pre,\narticle pre.hljs {\n\tfont-family: var(--font-mono);\n\tmargin: 14px 0;\n\tpadding: 7px 18px;\n\tbackground: var(--td-box-divider-bg);\n\tfont-size: 16px;\n\twhite-space: pre-wrap;\n\tword-wrap: break-word;\n\toverflow-x: visible;\n\tposition: relative;\n}\narticle ul pre,\narticle ol pre,\narticle ul pre.hljs,\narticle ol pre.hljs {\n\tmargin: 6px 0 6px -18px;\n}\narticle.rtl ul pre,\narticle.rtl ol pre,\narticle.rtl ul pre.hljs,\narticle.rtl ol pre.hljs {\n\tmargin-right: -18px;\n\tmargin-left: 0;\n}\narticle pre + pre {\n\tmargin-top: -14px;\n}\narticle h3,\narticle h4 {\n\tfont-family: var(--font-serif);\n\tfont-size: 24px;\n\tline-height: 30px;\n\tmargin: 18px 18px 9px;\n\tfont-weight: normal;\n}\narticle h4 {\n\tfont-size: 19px;\n\tmargin: 18px 18px 7px;\n}\narticle ul h3,\narticle ol h3 {\n\tmargin: 12px 0 7px;\n}\narticle ul h4,\narticle ol h4 {\n\tmargin: 10px 0 5px;\n}\narticle blockquote {\n\tfont-family: var(--font-serif);\n\tmargin: 18px 18px 16px;\n\tpadding-left: 22px;\n\tposition: relative;\n\tfont-style: italic;\n\tword-wrap: break-word;\n}\narticle blockquote:before {\n\tcontent: '';\n\tposition: absolute;\n\tleft: 1px;\n\ttop: 3px;\n\tbottom: 3px;\n\twidth: 3px;\n\tbackground-color: var(--td-window-bg-active);\n\tborder-radius: 3px;\n}\narticle.rtl blockquote {\n\tpadding-right: 22px;\n\tpadding-left: 0;\n}\narticle.rtl blockquote:before {\n\tright: 1px;\n\tleft: auto;\n}\narticle aside {\n\tfont-family: var(--font-serif);\n\tmargin: 18px 18px 16px;\n\tpadding: 0 18px;\n\ttext-align: center;\n\tfont-style: italic;\n}\narticle ul blockquote,\narticle ol blockquote,\narticle ul aside,\narticle ol aside {\n\tmargin: 12px 0 10px;\n}\narticle blockquote cite,\narticle aside cite,\narticle footer cite,\narticle .pullquote cite {\n\tfont-family: var(--font-sans);\n\tfont-size: 15px;\n\tdisplay: block;\n\tcolor: var(--td-window-sub-text-fg);\n\tline-height: 19px;\n\tpadding: 5px 0 2px;\n\tfont-style: normal;\n}\narticle hr {\n\twidth: 50%;\n\tmargin: 36px auto 26px;\n\tpadding: 2px 0 10px;\n\tborder: none;\n}\narticle ul hr,\narticle ol hr {\n\tmargin: 18px auto;\n}\narticle hr:before {\n\tcontent: '';\n\tdisplay: block;\n\tborder-top: 1px solid var(--td-window-sub-text-fg);\n\tpadding: 0 0 2px;\n}\narticle footer hr {\n\tmargin: 18px auto 6px;\n}\narticle ul,\narticle ol {\n\tmargin: 0 18px 12px;\n\tpadding-left: 18px;\n}\narticle.rtl ul,\narticle.rtl ol {\n\tpadding-right: 18px;\n\tpadding-left: 0;\n}\n/*article ul {\n\tlist-style: none;\n}*/\narticle ul > li,\narticle ol > li {\n\tpadding-left: 4px;\n}\narticle.rtl ul > li,\narticle.rtl ol > li {\n\tpadding-right: 4px;\n\tpadding-left: 0;\n}\n/*article ul > li {\n\tposition: relative;\n}\narticle ul > li:before {\n\tcontent: '\\2022';\n\tposition: absolute;\n\tdisplay: block;\n\tfont-size: 163%;\n\tleft: -19px;\n\ttop: 1px;\n}\narticle.rtl ul > li:before {\n\tleft: auto;\n\tright: -19px;\n}*/\narticle ul ul,\narticle ul ol,\narticle ol ul,\narticle ol ol {\n\tmargin: 0 0 12px;\n}\n\narticle table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n}\narticle table.bordered,\narticle table.bordered td,\narticle table.bordered th {\n\tborder: 1px solid var(--td-history-to-down-shadow);\n}\narticle table.striped tr:nth-child(odd) td {\n\tbackground-color: var(--td-box-divider-bg);\n}\narticle table caption {\n\tfont-size: 15px;\n\tline-height: 18px;\n\tmargin: 4px 0 7px;\n\ttext-align: left;\n\tcolor: var(--td-window-sub-text-fg);\n}\narticle.rtl table caption {\n\ttext-align: right;\n}\narticle td,\narticle th {\n\tfont-size: 15px;\n\tline-height: 21px;\n\tpadding: 6px 5px 5px;\n\tbackground-color: var(--td-window-bg);\n\tvertical-align: middle;\n\tfont-weight: normal;\n\ttext-align: left;\n}\narticle th {\n\tbackground-color: var(--td-box-divider-bg);\n}\narticle.rtl table td,\narticle.rtl table th {\n\ttext-align: right;\n}\narticle details {\n\tposition: relative;\n\tmargin: 0 0 12px;\n\tpadding: 0 0 1px;\n}\narticle details:before {\n\tcontent: '';\n\tdisplay: block;\n\tborder-bottom: 1px solid var(--td-history-to-down-shadow);\n\tposition: absolute;\n\tleft: 18px;\n\tright: 0;\n\tbottom: 0;\n}\narticle.rtl details:before {\n\tright: 18px;\n\tleft: 0;\n}\narticle details + details {\n\tmargin-top: -12px;\n}\narticle details > details:last-child {\n\tmargin-bottom: -1px;\n}\narticle summary {\n\tpadding: 10px 18px 10px 42px;\n\tline-height: 25px;\n\tmin-height: 25px;\n}\narticle.rtl summary {\n\tpadding-left: 18px;\n\tpadding-right: 42px;\n}\narticle summary:hover {\n\tcursor: pointer;\n}\narticle summary:focus {\n\toutline: none;\n}\narticle summary::-webkit-details-marker {\n\tdisplay: none;\n}\narticle summary::marker {\n\tcontent: '';\n}\narticle summary:before {\n\tcontent: '';\n\tbackground: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAICAYAAADN5B7xAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAH1JREFUeNqEjUEKgCAQRSfrNi1bdZFadJjsMC46SSAIHqjB5mcFqdFfhD3eUyKZtb6ln92O2janmXdvrRu+ZTfAgasu1jAHU4qiHAwc/Ff4oCQKsxxZ0NT33XrxUTjkWvgiXFf3TWkU6Vt+XihH515yFiQRpfLnEMUw3yHAABZNTT9emBrvAAAAAElFTkSuQmCC');\n\ttransition: all .2s ease;\n\tdisplay: inline-block;\n\tposition: absolute;\n\twidth: 12px;\n\theight: 8px;\n\tleft: 18px;\n\ttop: 18px;\n}\narticle.rtl summary:before {\n\tright: 18px;\n\tleft: auto;\n}\n@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {\n\tarticle summary:before {\n\tbackground: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAQCAYAAAAMJL+VAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAPxJREFUeNq8lEESgiAUhgFbZ0epSW28gB2pZbrrSukBHDWto1TrwHih45AiaDOxesLP9w1PBlzXNfrLSNPqkGWV8ysHGMBqv4mAlyFC7MRPk+T51Z0Lh73AAJZgIoRFUR/bEMb4TggJPG9TTIUzxmIuWHWzOCLfQQgwRiedRMBpIsObFvn+NgSTLEE2bCiKm6eDQ0bAkS2v4AjYuPvJcqtEu9DDshaB665zFZzSV6yCfyr5JplLTOA9wZiEg/a+72Qic9nxubMOPijQSZraCK4UjEiezSVYmsBHBSrJAEIJ1wr0knG4kUAt0cONBX2JGXzGi1uG7SNmOt4CDADc4r+K4txg+wAAAABJRU5ErkJggg==');\n\tbackground-size: 12px 8px;\n\t}\n}\narticle details[open] > summary:before {\n\t/*transform: rotateZ(-180deg);*/\n\ttransform: scaleY(-1);\n}\narticle li summary {\n\tpadding-left: 24px;\n}\narticle li details:before,\narticle li summary:before {\n\tleft: 0;\n}\n\nimg,\nvideo,\niframe {\n\tmax-width: 100%;\n\tmax-height: 480px;\n\tvertical-align: top;\n}\nvideo {\n\twidth: 100%;\n}\naudio {\n\twidth: 100%;\n\twidth: calc(100% - 36px);\n\tmargin: 0 18px;\n\tvertical-align: top;\n}\nimg {\n\tfont-size: 12px;\n\tline-height: 14px;\n\tcolor: var(--td-window-sub-text-fg);\n}\nimg.pic {\n\tmax-height: none;\n\tfont-size: inherit;\n\tvertical-align: middle;\n\tposition: relative;\n\ttop: -0.1em;\n}\nimg.pic.optional {\n\topacity: 0.4;\n}\nbody:hover img.pic.optional {\n\topacity: 1;\n}\niframe.autosize {\n\tmax-height: none;\n}\n.iframe-wrap {\n\tmax-width: 100%;\n\tvertical-align: top;\n\tdisplay: inline-block;\n\tposition: relative;\n}\n.iframe-wrap iframe {\n\tposition: absolute;\n\twidth: 100%;\n\theight: 100%;\n\ttop: 0;\n\tleft: 0;\n}\nfigure {\n\tmargin: 0 0 16px;\n\tpadding: 0;\n\ttext-align: center;\n\tposition: relative;\n}\nfigure.nowide {\n\tmargin-left: 18px;\n\tmargin-right: 18px;\n}\nfigure.nowide figcaption {\n\tpadding-left: 0;\n\tpadding-right: 0;\n}\nul figure.nowide,\nol figure.nowide {\n\tmargin: 0 0 12px;\n}\nfigure > figure {\n\tmargin: 0;\n}\nfigcaption {\n\tfont-size: 15px;\n\tcolor: var(--td-window-sub-text-fg);\n\tpadding: 6px 18px 0;\n\tline-height: 19px;\n\ttext-align: left;\n}\narticle.rtl figcaption {\n\ttext-align: right;\n}\nul figcaption,\nol figcaption {\n\tpadding-left: 0;\n\tpadding-right: 0;\n}\nfigcaption > cite {\n\tfont-family: var(--font-sans);\n\tfont-size: 12px;\n\tdisplay: block;\n\tline-height: 15px;\n\tpadding: 2px 0 0;\n\tfont-style: normal;\n}\nfooter {\n\tmargin: 12px 18px;\n\tcolor: var(--td-window-sub-text-fg);\n}\n\nfigure.slideshow-wrap {\n\tposition: relative;\n}\nfigure.slideshow {\n\tposition: absolute;\n\ttop: 0px;\n\twhite-space: nowrap;\n\twidth: 100%;\n\tbackground: #000;\n\toverflow: hidden;\n}\nfigure.slideshow a {\n\ttransition: margin 200ms ease-in-out;\n}\nfigure.slideshow .photo-wrap,\nfigure.slideshow .video-wrap {\n\tposition: static !important;\n\tdisplay: inline-block;\n\tmargin: 0;\n\tvertical-align: middle;\n}\n.slideshow-buttons {\n\tposition: absolute;\n\twidth: 100%;\n\tbottom: 10px;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\tz-index: 5;\n}\n.slideshow-buttons > fieldset {\n\tpadding: 0;\n\tmargin: 0;\n\tborder: none;\n\tline-height: 0;\n\toverflow: hidden;\n\toverflow-x: auto;\n\tmin-width: auto;\n}\n.slideshow-buttons label {\n\tdisplay: inline-block;\n\tpadding: 7px;\n\tcursor: pointer;\n}\n.slideshow-buttons input {\n\tposition: absolute;\n\tleft: -10000px;\n}\n.slideshow-buttons label i {\n\tdisplay: inline-block;\n\tbackground: #fff;\n\tbox-shadow: 0 0 3px rgba(0, 0, 0, .4);\n\tborder-radius: 3.5px;\n\twidth: 7px;\n\theight: 7px;\n\topacity: .6;\n\ttransition: opacity .3s;\n}\n.slideshow-buttons input:checked ~ i {\n\topacity: 1;\n}\n.slideshow-next,\n.slideshow-prev {\n\tposition: absolute;\n\tz-index: 4;\n\ttop: 0;\n\twidth: 25%;\n\tmax-width: 128px;\n\theight: 100%;\n\tcursor: pointer;\n\ttransition: opacity 200ms ease-in-out;\n\tuser-select: none;\n\topacity: 0.6;\n}\n.slideshow-next {\n\tright: 0;\n\tbackground: linear-gradient(to right, rgba(0,0,0,0) 0%, rgba(0,0,0,0.6) 100%);\n}\n.slideshow-prev {\n\tleft: 0;\n\tbackground: linear-gradient(to left, rgba(0,0,0,0) 0%, rgba(0,0,0,0.6) 100%);\n}\n.slideshow-next:hover {\n\topacity: 1;\n}\n.slideshow-prev:hover {\n\topacity: 1;\n}\n.slideshow-prev svg,\n.slideshow-next svg {\n\tfill: none;\n\ttop: calc(50% - 12px);\n\tposition: absolute;\n\tz-index: 5;\n\twidth: 24px;\n\theight: 24px;\n\tpointer-events: none;\n}\n.slideshow-prev svg {\n\tleft: calc(min(50% - 12px, 20px));\n}\n.slideshow-next svg {\n\tright: calc(min(50% - 12px, 20px));\n}\n.slideshow-prev path,\n.slideshow-next path {\n\tstroke-width: 1.4;\n\tstroke: #fff;\n}\n\nfigure.collage-wrap {\n\tmargin: 0px 12px;\n}\nfigure.collage-wrap figcaption {\n\tpadding: 6px 6px 0px;\n}\nfigure.collage {\n\toverflow: hidden;\n\tborder-radius: 6px;\n}\nfigure.collage .photo-wrap,\nfigure.collage .video-wrap {\n\tposition: absolute;\n}\nfigure.collage .photo-wrap .photo {\n\tbackground-size: cover;\n}\nfigure.collage .video-wrap video {\n\tobject-fit: cover;\n\tposition: absolute;\n\ttop: 50%;\n\tleft: 50%;\n\twidth: auto;\n\theight: auto;\n\tmin-width: 100%;\n\tmin-height: 100%;\n\ttransform: translate(-50%, -50%);\n}\nfigure.collage .video-wrap .video-small,\nvideo[autoplay] {\n\tpointer-events: none;\n}\n\nfigure.table-wrap {\n\toverflow: auto;\n\t-webkit-overflow-scrolling: touch;\n}\nfigure.table {\n\tdisplay: table-cell;\n\tpadding: 0 18px;\n}\narticle ol figure.table-wrap,\narticle ul figure.table-wrap {\n\tmargin-top: 7px;\n}\narticle ol figure.table,\narticle ul figure.table {\n\tpadding: 0;\n}\n\nfigure blockquote.embed-post {\n\ttext-align: left;\n\tmargin-bottom: 0;\n}\narticle.rtl figure blockquote.embed-post {\n\ttext-align: right;\n}\nblockquote.embed-post address {\n\tmargin: 0;\n\tpadding: 5px 0 9px;\n\toverflow: hidden;\n}\nblockquote.embed-post address figure {\n\twidth: 50px;\n\theight: 50px;\n\tfloat: left;\n\tmargin: 0 12px 0 0;\n\tbackground: no-repeat center;\n\tbackground-size: cover;\n\tborder-radius: 50%;\n}\narticle.rtl blockquote.embed-post address figure {\n\tfloat: right;\n\tmargin-left: 12px;\n\tmargin-right: 0;\n}\nblockquote.embed-post address a {\n\tdisplay: inline-block;\n\tpadding-top: 2px;\n\tfont-size: 17px;\n\tfont-weight: 600;\n\tcolor: var(--td-window-fg);\n}\nblockquote.embed-post address time {\n\tdisplay: block;\n\tline-height: 19px;\n}\nblockquote.embed-post p,\nblockquote.embed-post blockquote {\n\tmargin: 0 0 7px;\n\tclear: left;\n}\nblockquote.embed-post figcaption {\n\tpadding-left: 0;\n\tpadding-right: 0;\n}\nblockquote.embed-post figure.collage {\n\tmargin-left: -2px;\n\tmargin-right: -2px;\n}\nblockquote.embed-post footer {\n\tmargin: 12px 0 0;\n\tfont-style: normal;\n}\nblockquote.embed-post footer hr {\n\tdisplay: none;\n}\n\nsection.embed-post {\n\tdisplay: block;\n\twidth: auto;\n\theight: auto;\n\tbackground: var(--td-box-divider-bg);\n\tmargin: 0 18px 12px;\n\tpadding: 24px 18px;\n\ttext-align: center;\n}\nsection.embed-post strong {\n\tfont-size: 21px;\n\tfont-weight: normal;\n\tdisplay: block;\n\tcolor: var(--td-window-sub-text-fg);\n}\nsection.embed-post small {\n\tdisplay: block;\n\tcolor: var(--td-window-sub-text-fg);\n}\n\n\nsection.related {\n\tmargin: 7px 0 12px;\n}\nsection.related h4 {\n\tfont-family: var(--font-sans);\n\tfont-size: 17px;\n\tline-height: 26px;\n\tfont-weight: 500;\n\tdisplay: block;\n\tpadding: 7px 18px;\n\tbackground: var(--td-box-divider-bg);\n\tmargin: 0;\n\tcolor: var(--td-window-fg);\n}\nsection.related a.related-link {\n\tdisplay: block;\n\tpadding: 15px 18px 16px;\n\tbackground: var(--td-window-bg);\n\tposition: relative;\n\toverflow: hidden;\n}\nsection.related a.related-link:after {\n\tcontent: '';\n\tdisplay: block;\n\tborder-bottom: 1px solid var(--td-history-to-down-shadow);\n\tposition: absolute;\n\tleft: 18px;\n\tright: 0;\n\tbottom: 0;\n}\nsection.related a.related-link:last-child:after {\n\tborder-bottom: 0px;\n}\nsection.related .related-link-url {\n\tdisplay: block;\n\tfont-size: 15px;\n\tline-height: 18px;\n\tpadding: 7px 0;\n\tcolor: var(--td-window-sub-text-fg);\n\tword-break: break-word;\n}\nsection.related .related-link-thumb {\n\tdisplay: inline-block;\n\tfloat: right;\n\twidth: 87px;\n\theight: 87px;\n\tborder-radius: 4px;\n\tbackground: no-repeat center;\n\tbackground-size: cover;\n\tmargin-left: 15px;\n}\nsection.related .related-link-content {\n\tdisplay: block;\n\tmargin: -3px 0;\n}\nsection.related .related-link-title {\n\tfont-size: 15px;\n\tfont-weight: 500;\n\tline-height: 18px;\n\tdisplay: block;\n\tdisplay: -webkit-box;\n\tmargin-bottom: 4px;\n\tmax-height: 36px;\n\t-webkit-line-clamp: 2;\n\t-webkit-box-orient: vertical;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: pre-wrap;\n\tcolor: var(--td-window-fg);\n}\nsection.related .related-link-desc {\n\tfont-size: 14px;\n\tline-height: 17px;\n\tdisplay: block;\n\tdisplay: -webkit-box;\n\tmax-height: 51px;\n\t-webkit-line-clamp: 3;\n\t-webkit-box-orient: vertical;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: pre-wrap;\n\tcolor: var(--td-window-fg);\n}\nsection.related .related-link-source {\n\tfont-size: 13px;\n\tline-height: 17px;\n\tdisplay: block;\n\toverflow: hidden;\n\tmargin-top: 4px;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n\tcolor: var(--td-window-sub-text-fg);\n}\n\nsection.message {\n\tposition: absolute;\n\tdisplay: table;\n\twidth: 100%;\n\theight: 100%;\n}\nsection.message.static {\n\tposition: static;\n\tmin-height: 200px;\n\theight: 100vh;\n}\nsection.message > aside {\n\tdisplay: table-cell;\n\tvertical-align: middle;\n\ttext-align: center;\n\tcolor: var(--td-window-sub-text-fg);\n\tfont-size: 24px;\n\tpointer-events: none;\n}\nsection.message > aside > cite {\n\tdisplay: block;\n\tfont-size: 14px;\n\tpadding: 10px 0 0;\n\tfont-style: normal;\n\tcolor: var(--td-window-sub-text-fg);\n}\n\nsection.channel {\n\tmargin-top: -16px;\n\tmargin-bottom: -9px;\n}\nsection.channel:first-child {\n\tmargin-top: 0;\n}\nsection.channel > a {\n\tdisplay: block;\n\tbackground: var(--td-box-divider-bg);\n}\nsection.channel > a > div.join {\n\tcolor: var(--td-window-active-text-fg);\n\tfont-weight: 500;\n\tpadding: 7px 18px;\n\tfloat: right;\n}\nsection.channel.joined > a > div.join {\n\tdisplay: none;\n}\nsection.channel > a > div.join:hover {\n\ttext-decoration: underline;\n}\nsection.channel > a > div.join span:before {\n\tcontent: var(--td-lng-iv-join-channel);\n}\nsection.channel > a > h4 {\n\tfont-family: var(--font-sans);\n\tfont-size: 17px;\n\tline-height: 26px;\n\tfont-weight: 500;\n\tmargin: 0;\n\tcolor: var(--td-window-fg);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\tpadding: 7px 18px;\n}\n\n.pullquote {\n\ttext-align: center;\n\tmax-width: 420px;\n\tfont-size: 19px;\n\tdisplay: block;\n\tmargin: 0 auto;\n}\n.media-outer {\n\tmargin-bottom: 16px;\n}\n.photo-wrap,\n.video-wrap {\n\twidth: 100%;\n\tmargin: 0 auto;\n\tposition: relative;\n\toverflow: hidden;\n}\n.photo-bg,\n.video-bg {\n\tbackground-size: cover;\n\tbackground-position: center;\n\tbackground-repeat: no-repeat;\n\tposition: absolute;\n\tfilter: blur(16px);\n\twidth: 100%;\n\theight: 100%;\n}\n.video-bg,\nvideo {\n\tposition: absolute;\n\ttop: 0px;\n}\n.photo {\n\tposition: relative;\n\tbackground-size: contain;\n\tbackground-position: center;\n\tbackground-repeat: no-repeat;\n}\n.photo,\nvideo {\n\topacity: 0;\n\ttransition: opacity 300ms ease-in-out;\n}\n.photo.loaded,\nvideo.loaded {\n\topacity: 1;\n}\n.video-play-outer {\n\tposition: relative;\n\twidth: 100%;\n\theight: 100%;\n\tdisplay: flex;\n\tjustify-content: center;\n\talign-items: center;\n}\n.video-play {\n\tposition: relative;\n\twidth: 48px;\n\theight: 0;\n\tpadding-top: 48px;\n\tmax-width: 48px;\n\tmax-height: 48px;\n\tbackground-color: rgba(0, 0, 0, 0.34);\n\tborder-radius: 50%;\n\tdisplay: flex;\n\tjustify-content: center;\n\talign-items: center;\n\toverflow: hidden;\n}\n.video-play::before {\n\tcontent: '';\n\tposition: absolute;\n\tmargin: -48px -4px 0px 0px;\n\twidth: 0;\n\theight: 0;\n\tborder-style: solid;\n\tborder-width: 10px 0 10px 16px;\n\tborder-color: transparent transparent transparent white;\n}\n\n.toast {\n\tposition: fixed;\n\ttop: 50%;\n\tleft: 50%;\n\ttransform: translate(-50%, -50%);\n\tbackground-color: var(--td-toast-bg);\n\tcolor: var(--td-toast-fg);\n\tpadding: 10px 20px;\n\tborder-radius: 6px;\n\tz-index: 9999;\n\topacity: 0;\n\tanimation: fadeIn 200ms linear forwards;\n}\n.toast.hiding {\n\topacity: 1;\n\tanimation: fadeOut 1000ms linear forwards;\n}\n"
  },
  {
    "path": "Telegram/Resources/iv_html/page.js",
    "content": "var IV = {\n\tnotify: function(message) {\n\t\tif (window.external && window.external.invoke) {\n\t\t\twindow.external.invoke(JSON.stringify(message));\n\t\t}\n\t},\n\tframeClickHandler: function(e) {\n\t\tvar target = e.target;\n\t\tvar context = '';\n\t\twhile (target) {\n\t\t\tif (target.id == 'menu_page_blocker') {\n\t\t\t\tIV.notify({ event: 'menu_page_blocker_click' });\n\t\t\t\tIV.menuShown(false);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (target.tagName == 'AUDIO' || target.tagName == 'VIDEO') {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (context === ''\n\t\t\t\t&& target.hasAttribute\n\t\t\t\t&& target.hasAttribute('data-context')) {\n\t\t\t\tcontext = String(target.getAttribute('data-context'));\n\t\t\t}\n\t\t\tif (target.tagName == 'A') {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ttarget = target.parentNode;\n\t\t}\n\t\tif (!target || (context === '' && !target.hasAttribute('href'))) {\n\t\t\treturn;\n\t\t}\n\t\tvar base = document.createElement('A');\n\t\tbase.href = window.location.href;\n\t\tif (base.origin != target.origin\n\t\t\t|| base.pathname != target.pathname\n\t\t\t|| base.search != target.search) {\n\t\t\tIV.notify({\n\t\t\t\tevent: 'link_click',\n\t\t\t\turl: target.href,\n\t\t\t\tcontext: context,\n\t\t\t});\n\t\t} else if (target.hash.length < 2) {\n\t\t\tIV.jumpToHash('');\n\t\t} else {\n\t\t\tIV.jumpToHash(decodeURIComponent(target.hash.substr(1)));\n\t\t}\n\t\te.preventDefault();\n\t},\n\tgetElementTop: function (element) {\n\t\tvar top = 0;\n\t\twhile (element && !element.classList.contains('page-scroll')) {\n\t\t\ttop += element.offsetTop;\n\t\t\telement = element.offsetParent;\n\t\t}\n\t\treturn top;\n\t},\n\tjumpToHash: function (hash, instant) {\n\t\tvar current = IV.computeCurrentState();\n\t\tcurrent.hash = hash;\n\t\twindow.history.replaceState(\n\t\t\tcurrent,\n\t\t\t'',\n\t\t\t'page' + IV.index + '.html');\n\t\tif (hash == '') {\n\t\t\tIV.scrollTo(0, instant);\n\t\t\treturn;\n\t\t}\n\n\t\tvar element = document.getElementsByName(hash)[0];\n\t\tif (element) {\n\t\t\tIV.scrollTo(IV.getElementTop(element), instant);\n\t\t}\n\t},\n\tframeKeyDown: function (e) {\n\t\tconst key0 = (e.key === '0')\n\t\t\t|| (e.code === 'Key0')\n\t\t\t|| (e.keyCode === 48);\n\t\tconst keyW = (e.key === 'w')\n\t\t\t|| (e.code === 'KeyW')\n\t\t\t|| (e.keyCode === 87);\n\t\tconst keyQ = (e.key === 'q')\n\t\t\t|| (e.code === 'KeyQ')\n\t\t\t|| (e.keyCode === 81);\n\t\tconst keyM = (e.key === 'm')\n\t\t\t|| (e.code === 'KeyM')\n\t\t\t|| (e.keyCode === 77);\n\t\tif ((e.metaKey || e.ctrlKey) && (keyW || keyQ || keyM || key0)) {\n\t\t\te.preventDefault();\n\t\t\tIV.notify({\n\t\t\t\tevent: 'keydown',\n\t\t\t\tmodifier: e.ctrlKey ? 'ctrl' : 'cmd',\n\t\t\t\tkey: key0 ? '0' : keyW ? 'w' : keyQ ? 'q' : 'm',\n\t\t\t});\n\t\t} else if (e.key === 'Escape' || e.keyCode === 27) {\n\t\t\te.preventDefault();\n\t\t\tif (IV.position) {\n\t\t\t\twindow.history.back();\n\t\t\t} else {\n\t\t\t\tIV.notify({\n\t\t\t\t\tevent: 'keydown',\n\t\t\t\t\tkey: 'escape',\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t},\n\tframeMouseEnter: function (e) {\n\t\tIV.notify({ event: 'mouseenter' });\n\t},\n\tframeMouseUp: function (e) {\n\t\tIV.notify({ event: 'mouseup' });\n\t},\n\tlastScrollTop: 0,\n\tframeScrolled: function (e) {\n\t\tconst was = IV.lastScrollTop;\n\t\tIV.lastScrollTop = IV.findPageScroll().scrollTop;\n\t\tIV.updateJumpToTop(was < IV.lastScrollTop);\n\t\tIV.checkVideos();\n\t},\n\tupdateJumpToTop: function (scrolledDown) {\n\t\tif (IV.lastScrollTop < 100) {\n\t\t\tdocument.getElementById('bottom_up').classList.add('hidden');\n\t\t} else if (scrolledDown && IV.lastScrollTop > 200) {\n\t\t\tdocument.getElementById('bottom_up').classList.remove('hidden');\n\t\t}\n\t},\n\tupdateStyles: function (styles) {\n\t\tif (IV.styles !== styles) {\n\t\t\tIV.styles = styles;\n\t\t\tdocument.getElementsByTagName('html')[0].style = styles;\n\t\t}\n\t},\n\ttoggleChannelJoined: function (id, joined) {\n\t\tIV.channelsJoined['channel' + id] = joined;\n\t\tIV.checkChannelButtons();\n\t},\n\tcheckChannelButtons: function() {\n\t\tconst channels = document.getElementsByClassName('channel');\n\t\tfor (var i = 0; i < channels.length; ++i) {\n\t\t\tconst channel = channels[i];\n\t\t\tconst full = String(channel.getAttribute('data-context'));\n\t\t\tconst value = IV.channelsJoined[full];\n\t\t\tif (value !== undefined) {\n\t\t\t\tchannel.classList.toggle('joined', value);\n\t\t\t}\n\t\t}\n\t},\n\tslideshowSlide: function(el, delta) {\n\t\tvar dir = window.getComputedStyle(el, null).direction || 'ltr';\n\t\tvar marginProp = dir == 'rtl' ? 'marginRight' : 'marginLeft';\n\t\tif (delta) {\n\t\t\tvar form = el.parentNode.firstChild;\n\t\t\tvar s = form.s;\n\t\t\tconst next = +s.value + delta;\n\t\t\ts.value = (next == s.length) ? 0 : (next == -1) ? (s.length - 1) : next;\n\t\t\tform.nextSibling.firstChild.style[marginProp] = (-100 * s.value) + '%';\n\t\t} else {\n\t\t\tel.form.nextSibling.firstChild.style[marginProp] = (-100 * el.value) + '%';\n\t\t}\n\t\treturn false;\n\t},\n\tinitPreBlocks: function() {\n\t\tif (!window.hljs) {\n\t\t\treturn;\n\t\t}\n\t\tvar pres = document.getElementsByTagName('pre');\n\t\tfor (var i = 0; i < pres.length; i++) {\n\t\t\tif (pres[i].hasAttribute('data-language')) {\n\t\t\t\twindow.hljs.highlightBlock(pres[i]);\n\t\t\t}\n\t\t}\n\t},\n\tinitEmbedBlocks: function() {\n\t\tvar iframes = document.getElementsByTagName('iframe');\n\t\tfor (var i = 0; i < iframes.length; i++) {\n\t\t\t(function(iframe) {\n\t\t\t\twindow.addEventListener('message', function(event) {\n\t\t\t\t\tif (event.source !== iframe.contentWindow ||\n\t\t\t\t\t\t\tevent.origin != window.origin) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\tvar data = JSON.parse(event.data);\n\t\t\t\t\t} catch(e) {\n\t\t\t\t\t\tvar data = {};\n\t\t\t\t\t}\n\t\t\t\t\tif (data.eventType == 'resize_frame') {\n\t\t\t\t\t\tif (data.eventData.height) {\n\t\t\t\t\t\t\tiframe.style.height = data.eventData.height + 'px';\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}, false);\n\t\t\t})(iframes[i]);\n\t\t}\n\t},\n\taddRipple: function (button, x, y) {\n\t\tconst ripple = document.createElement('span');\n\t\tripple.classList.add('ripple');\n\n\t\tconst inner = document.createElement('span');\n\t\tinner.classList.add('inner');\n\t\tx -= button.offsetLeft;\n\t\ty -= button.offsetTop;\n\n\t\tconst mx = button.clientWidth - x;\n\t\tconst my = button.clientHeight - y;\n\t\tconst sq1 = x * x + y * y;\n\t\tconst sq2 = mx * mx + y * y;\n\t\tconst sq3 = x * x + my * my;\n\t\tconst sq4 = mx * mx + my * my;\n\t\tconst radius = Math.sqrt(Math.max(sq1, sq2, sq3, sq4));\n\n\t\tinner.style.width = inner.style.height = `${2 * radius}px`;\n\t\tinner.style.left = `${x - radius}px`;\n\t\tinner.style.top = `${y - radius}px`;\n\t\tinner.classList.add('inner');\n\n\t\tripple.addEventListener('animationend', function (e) {\n\t\t\tif (e.animationName === 'fadeOut') {\n\t\t\t\tripple.remove();\n\t\t\t}\n\t\t});\n\n\t\tripple.appendChild(inner);\n\t\tbutton.appendChild(ripple);\n\t},\n\tstopRipples: function (button) {\n\t\tconst id = button.id ? button.id : button;\n\t\tbutton = document.getElementById(id);\n\t\tconst ripples = button.getElementsByClassName('ripple');\n\t\tfor (var i = 0; i < ripples.length; ++i) {\n\t\t\tconst ripple = ripples[i];\n\t\t\tif (!ripple.classList.contains('hiding')) {\n\t\t\t\tripple.classList.add('hiding');\n\t\t\t}\n\t\t}\n\t},\n\tinit: function () {\n\t\tvar current = IV.computeCurrentState();\n\t\twindow.history.replaceState(current, '', IV.pageUrl(0));\n\t\tIV.jumpToHash(current.hash, true);\n\n\t\tIV.lastScrollTop = window.history.state.scroll;\n\t\tIV.findPageScroll().onscroll = IV.frameScrolled;\n\n\t\tconst buttons = document.getElementsByClassName('fixed_button');\n\t\tfor (let i = 0; i < buttons.length; ++i) {\n\t\t\tconst button = buttons[i];\n\t\t\tbutton.addEventListener('mousedown', function (e) {\n\t\t\t\tIV.addRipple(e.currentTarget, e.clientX, e.clientY);\n\t\t\t});\n\t\t\tbutton.addEventListener('mouseup', function (e) {\n\t\t\t\tconst id = e.currentTarget.id;\n\t\t\t\tsetTimeout(function () {\n\t\t\t\t\tIV.stopRipples(id);\n\t\t\t\t}, 0);\n\t\t\t});\n\t\t\tbutton.addEventListener('mouseleave', function (e) {\n\t\t\t\tIV.stopRipples(e.currentTarget);\n\t\t\t});\n\t\t}\n\t\tIV.initMedia();\n\t\tIV.notify({ event: 'ready' });\n\n\t\tIV.forceScrollFocus();\n\t\tIV.frameScrolled();\n\t},\n\tinitMedia: function () {\n\t\tvar scroll = IV.findPageScroll();\n\t\tconst photos = scroll.getElementsByClassName('photo');\n\t\tfor (let i = 0; i < photos.length; ++i) {\n\t\t\tconst photo = photos[i];\n\t\t\tif (photo.classList.contains('loaded')) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst url = photo.style.backgroundImage;\n\t\t\tif (!url || url.length < 7) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvar img = new Image();\n\t\t\timg.onload = function () {\n\t\t\t\tphoto.classList.add('loaded');\n\t\t\t}\n\t\t\timg.src = url.substr(5, url.length - 7);\n\t\t\tif (img.complete) {\n\t\t\t\tphoto.classList.add('loaded');\n\t\t\t\tIV.stopAnimations(photo);\n\t\t\t}\n\t\t}\n\t\tIV.videos = [];\n\t\tconst videos = scroll.getElementsByClassName('video');\n\t\tfor (let i = 0; i < videos.length; ++i) {\n\t\t\tconst element = videos[i];\n\t\t\tIV.videos.push({\n\t\t\t\telement: element,\n\t\t\t\tsrc: String(element.getAttribute('data-src')),\n\t\t\t\tautoplay: (element.getAttribute('data-autoplay') == '1'),\n\t\t\t\tloop: (element.getAttribute('data-loop') == '1'),\n\t\t\t\tsmall: (element.getAttribute('data-small') == '1'),\n\t\t\t\tfilled: (element.firstChild\n\t\t\t\t\t&& element.firstChild.tagName == 'VIDEO'),\n\t\t\t});\n\t\t}\n\t},\n\tcheckVideos: function () {\n\t\tconst visibleTop = IV.lastScrollTop;\n\t\tconst visibleBottom = visibleTop + IV.findPageScroll().offsetHeight;\n\t\tconst videos = IV.videos;\n\t\tfor (let i = 0; i < videos.length; ++i) {\n\t\t\tconst video = videos[i];\n\t\t\tconst element = video.element;\n\t\t\tconst wrap = element.offsetParent; // video-wrap\n\t\t\tconst top = IV.getElementTop(wrap);\n\t\t\tconst bottom = top + wrap.offsetHeight;\n\t\t\tif (top < visibleBottom && bottom > visibleTop) {\n\t\t\t\tif (!video.created) {\n\t\t\t\t\tvideo.created = new Date();\n\t\t\t\t\tvideo.loaded = false;\n\t\t\t\t\telement.innerHTML = '<video muted class=\"'\n\t\t\t\t\t\t+ (video.small ? 'video-small' : '')\n\t\t\t\t\t\t+ '\"'\n\t\t\t\t\t\t+ (video.autoplay\n\t\t\t\t\t\t\t? ' preload=\"auto\" autoplay'\n\t\t\t\t\t\t\t: (video.small\n\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t: ' controls'))\n\t\t\t\t\t\t+ (video.loop ? ' loop' : '')\n\t\t\t\t\t\t+ ' oncanplay=\"IV.checkVideos();\"'\n\t\t\t\t\t\t+ ' onloadeddata=\"IV.checkVideos();\">'\n\t\t\t\t\t\t\t+ '<source src=\"'\n\t\t\t\t\t\t\t+ video.src\n\t\t\t\t\t\t\t+ '\" type=\"video/mp4\" />'\n\t\t\t\t\t\t+ '</video>';\n\t\t\t\t\tvar media = element.firstChild;\n\t\t\t\t\tmedia.oncontextmenu = function () { return false; };\n\t\t\t\t\tmedia.oncanplay = IV.checkVideos;\n\t\t\t\t\tmedia.onloadeddata = IV.checkVideos;\n\t\t\t\t}\n\t\t\t} else if (video.created && video.autoplay) {\n\t\t\t\tvideo.created = false;\n\t\t\t\telement.innerHTML = '';\n\t\t\t}\n\t\t\tif (video.created && !video.loaded) {\n\t\t\t\tvar media = element.firstChild;\n\t\t\t\tconst HAVE_CURRENT_DATA = 2;\n\t\t\t\tif (media && media.readyState >= HAVE_CURRENT_DATA) {\n\t\t\t\t\tvideo.loaded = true;\n\t\t\t\t\tmedia.classList.add('loaded');\n\t\t\t\t\tif ((new Date() - video.created) < 100) {\n\t\t\t\t\t\tIV.stopAnimations(media);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\tshowTooltip: function (text) {\n\t\tvar toast = document.createElement('div');\n\t\ttoast.classList.add('toast');\n\t\ttoast.textContent = text;\n\t\tdocument.body.appendChild(toast);\n\t\tsetTimeout(function () {\n\t\t\ttoast.classList.add('hiding');\n\t\t}, 2000);\n\t\tsetTimeout(function () {\n\t\t\tdocument.body.removeChild(toast);\n\t\t}, 3000);\n\t},\n\tscrollTo: function (y, instant) {\n\t\tif (y < 200) {\n\t\t\tdocument.getElementById('bottom_up').classList.add('hidden');\n\t\t}\n\t\tIV.findPageScroll().scrollTo({\n\t\t\ttop: y || 0,\n\t\t\tbehavior: instant ? 'instant' : 'smooth'\n\t\t});\n\t},\n\tcomputeCurrentState: function () {\n\t\tvar now = IV.findPageScroll();\n\t\treturn {\n\t\t\tposition: IV.position,\n\t\t\tindex: IV.index,\n\t\t\thash: ((!window.history.state\n\t\t\t\t|| window.history.state.hash === undefined)\n\t\t\t\t? window.location.hash.substr(1)\n\t\t\t\t: window.history.state.hash),\n\t\t\tscroll: now ? now.scrollTop : 0\n\t\t};\n\t},\n\tpageUrl: function (index, hash) {\n\t\tvar result = 'page' + index + '.html';\n\t\tif (hash) {\n\t\t\tresult += '#' + hash;\n\t\t}\n\t\treturn result;\n\t},\n\tnavigateTo: function (index, hash) {\n\t\tif (!index && !IV.index) {\n\t\t\tIV.navigateToDOM(IV.index, hash);\n\t\t\treturn;\n\t\t}\n\t\tIV.pending = [index, hash];\n\t\tif (!IV.cache[index]) {\n\t\t\tIV.loadPage(index);\n\t\t} else if (IV.cache[index].dom) {\n\t\t\tIV.navigateToDOM(index, hash);\n\t\t} else if (IV.cache[index].content) {\n\t\t\tIV.navigateToLoaded(index, hash);\n\t\t}\n\t},\n\tapplyUpdatedContent: function (index) {\n\t\tif (IV.index != index) {\n\t\t\tIV.cache[index].contentUpdated = (IV.cache[index].dom !== undefined);\n\t\t\treturn;\n\t\t}\n\t\tvar data = JSON.parse(IV.cache[index].content);\n\t\tvar article = function (el) {\n\t\t\treturn el.getElementsByTagName('article')[0];\n\t\t};\n\t\tvar footer = function (el) {\n\t\t\treturn el.getElementsByClassName('page-footer')[0];\n\t\t};\n\t\tvar from = IV.findPageScroll();\n\t\tvar to = IV.makeScrolledContent(data.html);\n\t\tmorphdom(article(from), article(to), {\n\t\t\tonBeforeElUpdated: function (fromEl, toEl) {\n\t\t\t\tif (fromEl.classList.contains('video')\n\t\t\t\t\t&& toEl.classList.contains('video')\n\t\t\t\t\t&& fromEl.hasAttribute('data-src')\n\t\t\t\t\t&& toEl.hasAttribute('data-src')\n\t\t\t\t\t&& (fromEl.getAttribute('data-src')\n\t\t\t\t\t\t== toEl.getAttribute('data-src'))) {\n\t\t\t\t\treturn false;\n\t\t\t\t} else if (fromEl.tagName == 'SECTION'\n\t\t\t\t\t&& fromEl.classList.contains('channel')\n\t\t\t\t\t&& fromEl.hasAttribute('data-context')\n\t\t\t\t\t&& toEl.tagName == 'SECTION'\n\t\t\t\t\t&& toEl.classList.contains('channel')\n\t\t\t\t\t&& toEl.hasAttribute('data-context')\n\t\t\t\t\t&& (String(fromEl.getAttribute('data-context'))\n\t\t\t\t\t\t== String(toEl.getAttribute('data-context')))) {\n\t\t\t\t\treturn false;\n\t\t\t\t} else if (fromEl.classList.contains('loaded')) {\n\t\t\t\t\ttoEl.classList.add('loaded');\n\t\t\t\t}\n\t\t\t\treturn !fromEl.isEqualNode(toEl);\n\t\t\t}\n\t\t});\n\t\tmorphdom(footer(from), footer(to));\n\t\tIV.initMedia();\n\t\teval(data.js);\n\t},\n\tloadPage: function (index) {\n\t\tif (!IV.cache[index]) {\n\t\t\tIV.cache[index] = {};\n\t\t}\n\t\tIV.cache[index].loading = true;\n\n\t\tlet xhr = new XMLHttpRequest();\n\t\txhr.onload = function () {\n\t\t\tIV.cache[index].loading = false;\n\t\t\tIV.cache[index].content = xhr.responseText;\n\t\t\tIV.applyUpdatedContent(index);\n\t\t\tif (IV.pending && IV.pending[0] == index) {\n\t\t\t\tIV.navigateToLoaded(index, IV.pending[1]);\n\t\t\t}\n\t\t\tif (IV.cache[index].reloadPending) {\n\t\t\t\tIV.cache[index].reloadPending = false;\n\t\t\t\tIV.reloadPage(index);\n\t\t\t}\n\t\t}\n\n\t\txhr.open('GET', 'page' + index + '.json');\n\t\txhr.send();\n\t},\n\treloadPage: function (index) {\n\t\tif (IV.cache[index] && IV.cache[index].loading) {\n\t\t\tIV.cache[index].reloadPending = true;\n\t\t\treturn;\n\t\t}\n\t\tIV.loadPage(index);\n\t},\n\n\tmakeScrolledContent: function (html) {\n\t\tvar result = document.createElement('div');\n\t\tresult.className = 'page-scroll';\n\t\tresult.tabIndex = '-1';\n\t\tresult.innerHTML = html.trim();\n\t\tresult.onscroll = IV.frameScrolled;\n\t\treturn result;\n\t},\n\tnavigateToLoaded: function (index, hash) {\n\t\tif (IV.cache[index].dom) {\n\t\t\tIV.navigateToDOM(index, hash);\n\t\t} else {\n\t\t\tvar data = JSON.parse(IV.cache[index].content);\n\t\t\tIV.cache[index].dom = IV.makeScrolledContent(data.html);\n\n\t\t\tIV.navigateToDOM(index, hash);\n\t\t\teval(data.js);\n\t\t}\n\t},\n\tnavigateToDOM: function (index, hash) {\n\t\tIV.pending = null;\n\t\tif (IV.index == index) {\n\t\t\tIV.jumpToHash(hash);\n\t\t\tIV.forceScrollFocus();\n\t\t\treturn;\n\t\t}\n\t\twindow.history.replaceState(\n\t\t\tIV.computeCurrentState(),\n\t\t\t'',\n\t\t\tIV.pageUrl(IV.index));\n\n\t\tIV.position = IV.position + 1;\n\t\twindow.history.pushState(\n\t\t\t{ position: IV.position, index: index, hash: hash },\n\t\t\t'',\n\t\t\tIV.pageUrl(index));\n\t\tIV.showDOM(index, hash);\n\t},\n\tfindPageScroll: function () {\n\t\tvar all = document.getElementsByClassName('page-scroll');\n\t\tfor (i = 0; i < all.length; ++i) {\n\t\t\tif (!all[i].classList.contains('hidden-left')\n\t\t\t\t&& !all[i].classList.contains('hidden-right')) {\n\t\t\t\treturn all[i];\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t},\n\tshowDOM: function (index, hash, scroll) {\n\t\tIV.pending = null;\n\t\tif (IV.index != index) {\n\t\t\tvar initial = !window.history.state\n\t\t\t\t|| window.history.state.position === undefined;\n\t\t\tvar back = initial\n\t\t\t\t|| IV.position > window.history.state.position;\n\t\t\tIV.position = initial ? 0 : window.history.state.position;\n\n\t\t\tvar now = IV.cache[index].dom;\n\t\t\tvar was = IV.findPageScroll();\n\t\t\tif (!IV.cache[IV.index]) {\n\t\t\t\tIV.cache[IV.index] = {};\n\t\t\t}\n\t\t\tIV.cache[IV.index].dom = was;\n\t\t\twas.parentNode.appendChild(now);\n\t\t\tif (scroll !== undefined) {\n\t\t\t\tnow.scrollTop = scroll;\n\t\t\t\tsetTimeout(function () {\n\t\t\t\t\t// When returning by history.back to an URL with a hash\n\t\t\t\t\t// for the first time browser forces the scroll to the\n\t\t\t\t\t// hash instead of the saved scroll position.\n\t\t\t\t\t//\n\t\t\t\t\t// This workaround prevents incorrect scroll position.\n\t\t\t\t\tnow.scrollTop = scroll;\n\t\t\t\t}, 0);\n\t\t\t}\n\n\t\t\tnow.classList.add(back ? 'hidden-left' : 'hidden-right');\n\t\t\tnow.classList.remove(back ? 'hidden-right' : 'hidden-left');\n\t\t\tIV.stopAnimations(now.firstChild);\n\n\t\t\tif (!was.listening) {\n\t\t\t\twas.listening = true;\n\t\t\t\twas.firstChild.addEventListener('transitionend', function (e) {\n\t\t\t\t\tif (was.classList.contains('hidden-left')\n\t\t\t\t\t\t|| was.classList.contains('hidden-right')) {\n\t\t\t\t\t\tif (was.parentNode) {\n\t\t\t\t\t\t\twas.parentNode.removeChild(was);\n\t\t\t\t\t\t\tvar videos = was.getElementsByClassName('video');\n\t\t\t\t\t\t\tfor (var i = 0; i < videos.length; ++i) {\n\t\t\t\t\t\t\t\tvideos[i].innerHTML = '';\n                            }\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\n\t\t\twas.classList.add(back ? 'hidden-right' : 'hidden-left');\n\t\t\tnow.classList.remove(back ? 'hidden-left' : 'hidden-right');\n\n\t\t\tIV.index = index;\n\t\t\tIV.notify({\n\t\t\t\tevent: 'location_change',\n\t\t\t\tindex: IV.index,\n\t\t\t\tposition: IV.position,\n\t\t\t\thash: IV.computeCurrentState().hash,\n\t\t\t});\n\t\t\tif (IV.cache[index].contentUpdated) {\n\t\t\t\tIV.cache[index].contentUpdated = false;\n\t\t\t\tIV.applyUpdatedContent(index);\n\t\t\t} else {\n\t\t\t\tIV.initMedia();\n\t\t\t}\n\t\t\tIV.checkChannelButtons();\n\t\t\tif (scroll === undefined) {\n\t\t\t\tIV.jumpToHash(hash, true);\n\t\t\t} else {\n\t\t\t\tIV.lastScrollTop = scroll;\n\t\t\t\tIV.updateJumpToTop(true);\n\t\t\t}\n\t\t} else if (scroll !== undefined) {\n\t\t\tIV.scrollTo(scroll);\n\t\t\tIV.lastScrollTop = scroll;\n\t\t\tIV.updateJumpToTop(true);\n\t\t} else {\n\t\t\tIV.jumpToHash(hash);\n\t\t}\n\n\t\tIV.forceScrollFocus();\n\t\tIV.frameScrolled();\n\t},\n\tforceScrollFocus: function () {\n\t\tIV.findPageScroll().focus();\n\t\tsetTimeout(function () {\n\t\t\t// Doesn't work on #hash-ed pages in Windows WebView2 otherwise.\n\t\t\tIV.findPageScroll().focus();\n\t\t}, 100);\n\t},\n\tstopAnimations: function (element) {\n\t\telement.getAnimations().forEach(\n\t\t\t(animation) => animation.finish());\n\t},\n\tmenuShown: function (shown) {\n\t\tvar already = document.getElementById('menu_page_blocker');\n\t\tif (already && shown) {\n\t\t\treturn;\n\t\t} else if (already) {\n\t\t\tdocument.body.removeChild(already);\n\t\t\treturn;\n\t\t} else if (!shown) {\n\t\t\treturn;\n\t\t}\n\t\tvar blocker = document.createElement('div');\n\t\tblocker.id = 'menu_page_blocker';\n\t\tdocument.body.appendChild(blocker);\n\t},\n\n\tvideos: {},\n\tvideosPlaying: {},\n\n\tcache: {},\n\tchannelsJoined: {},\n\tindex: 0,\n\tposition: 0\n};\n\ndocument.onclick = IV.frameClickHandler;\ndocument.onkeydown = IV.frameKeyDown;\ndocument.onmouseenter = IV.frameMouseEnter;\ndocument.onmouseup = IV.frameMouseUp;\ndocument.onresize = IV.checkVideos;\nwindow.onmessage = IV.postMessageHandler;\nwindow.addEventListener('popstate', function (e) {\n\tif (e.state) {\n\t\tIV.showDOM(e.state.index, e.state.hash, e.state.scroll);\n\t}\n});\ndocument.addEventListener(\"DOMContentLoaded\", IV.forceScrollFocus);\n"
  },
  {
    "path": "Telegram/Resources/langs/cloud_lang.strings",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\n\"cloud_lng_badge_psa_covid\" = \"COVID-19\";\n\"cloud_lng_about_psa_covid\" = \"This message provides you with a public service announcement in relation to the ongoing COVID-19 pandemic. To remove it from your chats list, right click it and select **Hide**.\";\n\"cloud_lng_forwarded_psa_covid\" = \"COVID-19 Notification from {channel}\";\n\"cloud_lng_tooltip_psa_covid\" = \"This message provides you with a public service announcement in relation to the ongoing COVID-19 pandemic. Learn more about this initiative at https://telegram.org/blog/coronavirus\";\n\n\"cloud_lng_topup_purpose_subs\" = \"Buy **Stars** to keep your channel subscriptions.\";\n\n\"cloud_lng_age_verify_about_gb#one\" = \"To access such content, you must confirm that you are at least **{count}** year old as required by UK law.\";\n\"cloud_lng_age_verify_about_gb#other\" = \"To access such content, you must confirm that you are at least **{count}** years old as required by UK law.\";\n\n\"cloud_lng_passport_in_ar\" = \"Arabic\";\n\"cloud_lng_passport_in_az\" = \"Azerbaijani\";\n\"cloud_lng_passport_in_bg\" = \"Bulgarian\";\n\"cloud_lng_passport_in_bn\" = \"Bengali\";\n\"cloud_lng_passport_in_cs\" = \"Czech\";\n\"cloud_lng_passport_in_da\" = \"Danish\";\n\"cloud_lng_passport_in_de\" = \"German\";\n\"cloud_lng_passport_in_dv\" = \"Divehi\";\n\"cloud_lng_passport_in_dz\" = \"Dzongkha\";\n\"cloud_lng_passport_in_el\" = \"Greek\";\n\"cloud_lng_passport_in_en\" = \"English\";\n\"cloud_lng_passport_in_es\" = \"Spanish\";\n\"cloud_lng_passport_in_et\" = \"Estonian\";\n\"cloud_lng_passport_in_fa\" = \"Persian\";\n\"cloud_lng_passport_in_fr\" = \"French\";\n\"cloud_lng_passport_in_he\" = \"Hebrew\";\n\"cloud_lng_passport_in_hr\" = \"Croatian\";\n\"cloud_lng_passport_in_hu\" = \"Hungarian\";\n\"cloud_lng_passport_in_hy\" = \"Armenian\";\n\"cloud_lng_passport_in_id\" = \"Indonesian\";\n\"cloud_lng_passport_in_is\" = \"Icelandic\";\n\"cloud_lng_passport_in_it\" = \"Italian\";\n\"cloud_lng_passport_in_ja\" = \"Japanese\";\n\"cloud_lng_passport_in_ka\" = \"Georgian\";\n// \"cloud_lng_passport_in_km\" = \"Khmer\";\n\"cloud_lng_passport_in_ko\" = \"Korean\";\n\"cloud_lng_passport_in_lo\" = \"Lao\";\n\"cloud_lng_passport_in_lt\" = \"Lithuanian\";\n\"cloud_lng_passport_in_lv\" = \"Latvian\";\n\"cloud_lng_passport_in_mk\" = \"Macedonian\";\n\"cloud_lng_passport_in_mn\" = \"Mongolian\";\n\"cloud_lng_passport_in_ms\" = \"Malay\";\n\"cloud_lng_passport_in_my\" = \"Burmese\";\n\"cloud_lng_passport_in_ne\" = \"Nepali\";\n\"cloud_lng_passport_in_nl\" = \"Dutch\";\n\"cloud_lng_passport_in_pl\" = \"Polish\";\n\"cloud_lng_passport_in_pt\" = \"Portuguese\";\n\"cloud_lng_passport_in_ro\" = \"Romanian\";\n\"cloud_lng_passport_in_ru\" = \"Russian\";\n\"cloud_lng_passport_in_sk\" = \"Slovak\";\n\"cloud_lng_passport_in_sl\" = \"Slovenian\";\n\"cloud_lng_passport_in_th\" = \"Thai\";\n\"cloud_lng_passport_in_tk\" = \"Turkmen\";\n\"cloud_lng_passport_in_tr\" = \"Turkish\";\n\"cloud_lng_passport_in_uk\" = \"Ukrainian\";\n\"cloud_lng_passport_in_uz\" = \"Uzbek\";\n\"cloud_lng_passport_in_vi\" = \"Vietnamese\";\n\n\"cloud_lng_translate_to_ar\" = \"Arabic\";\n\"cloud_lng_translate_to_az\" = \"Azerbaijani\";\n\"cloud_lng_translate_to_bg\" = \"Bulgarian\";\n// \"cloud_lng_translate_to_bn\" = \"Bengali\";\n\"cloud_lng_translate_to_cs\" = \"Czech\";\n\"cloud_lng_translate_to_da\" = \"Danish\";\n\"cloud_lng_translate_to_de\" = \"German\";\n// \"cloud_lng_translate_to_dv\" = \"Divehi\";\n// \"cloud_lng_translate_to_dz\" = \"Dzongkha\";\n\"cloud_lng_translate_to_el\" = \"Greek\";\n\"cloud_lng_translate_to_en\" = \"English\";\n\"cloud_lng_translate_to_es\" = \"Spanish\";\n\"cloud_lng_translate_to_et\" = \"Estonian\";\n\"cloud_lng_translate_to_fa\" = \"Persian\";\n\"cloud_lng_translate_to_fr\" = \"French\";\n\"cloud_lng_translate_to_he\" = \"Hebrew\";\n\"cloud_lng_translate_to_hr\" = \"Croatian\";\n\"cloud_lng_translate_to_hu\" = \"Hungarian\";\n\"cloud_lng_translate_to_hy\" = \"Armenian\";\n\"cloud_lng_translate_to_id\" = \"Indonesian\";\n\"cloud_lng_translate_to_is\" = \"Icelandic\";\n\"cloud_lng_translate_to_it\" = \"Italian\";\n\"cloud_lng_translate_to_ja\" = \"Japanese\";\n\"cloud_lng_translate_to_ka\" = \"Georgian\";\n// \"cloud_lng_translate_to_km\" = \"Khmer\";\n\"cloud_lng_translate_to_ko\" = \"Korean\";\n\"cloud_lng_translate_to_lo\" = \"Lao\";\n\"cloud_lng_translate_to_lt\" = \"Lithuanian\";\n\"cloud_lng_translate_to_lv\" = \"Latvian\";\n\"cloud_lng_translate_to_mk\" = \"Macedonian\";\n\"cloud_lng_translate_to_mn\" = \"Mongolian\";\n\"cloud_lng_translate_to_ms\" = \"Malay\";\n\"cloud_lng_translate_to_my\" = \"Burmese\";\n\"cloud_lng_translate_to_ne\" = \"Nepali\";\n\"cloud_lng_translate_to_nl\" = \"Dutch\";\n\"cloud_lng_translate_to_pl\" = \"Polish\";\n\"cloud_lng_translate_to_pt\" = \"Portuguese\";\n\"cloud_lng_translate_to_ro\" = \"Romanian\";\n\"cloud_lng_translate_to_ru\" = \"Russian\";\n\"cloud_lng_translate_to_sk\" = \"Slovak\";\n\"cloud_lng_translate_to_sl\" = \"Slovenian\";\n\"cloud_lng_translate_to_th\" = \"Thai\";\n\"cloud_lng_translate_to_tk\" = \"Turkmen\";\n\"cloud_lng_translate_to_tr\" = \"Turkish\";\n\"cloud_lng_translate_to_uk\" = \"Ukrainian\";\n\"cloud_lng_translate_to_uz\" = \"Uzbek\";\n\"cloud_lng_translate_to_vi\" = \"Vietnamese\";\n\n\"cloud_lng_language_af\" = \"Afrikaans\";\n\"cloud_lng_language_am\" = \"Amharic\";\n\"cloud_lng_language_ar\" = \"Arabic\";\n\"cloud_lng_language_az\" = \"Azerbaijani\";\n\"cloud_lng_language_be\" = \"Belarusian\";\n\"cloud_lng_language_bg\" = \"Bulgarian\";\n\"cloud_lng_language_bn\" = \"Bengali\";\n\"cloud_lng_language_bs\" = \"Bosnian\";\n\"cloud_lng_language_ca\" = \"Catalan\";\n// \"cloud_lng_language_ceb\" = \"Cebuano\";\n\"cloud_lng_language_co\" = \"Corsican\";\n\"cloud_lng_language_cs\" = \"Czech\";\n\"cloud_lng_language_cy\" = \"Welsh\";\n\"cloud_lng_language_da\" = \"Danish\";\n\"cloud_lng_language_de\" = \"German\";\n\"cloud_lng_language_dv\" = \"Divehi\";\n\"cloud_lng_language_dz\" = \"Dzongkha\";\n\"cloud_lng_language_el\" = \"Greek\";\n\"cloud_lng_language_en\" = \"English\";\n\"cloud_lng_language_eo\" = \"Esperanto\";\n\"cloud_lng_language_es\" = \"Spanish\";\n\"cloud_lng_language_et\" = \"Estonian\";\n\"cloud_lng_language_eu\" = \"Basque\";\n\"cloud_lng_language_fa\" = \"Persian\";\n\"cloud_lng_language_fi\" = \"Finnish\";\n\"cloud_lng_language_fr\" = \"French\";\n\"cloud_lng_language_fy\" = \"Frisian\";\n\"cloud_lng_language_ga\" = \"Irish\";\n\"cloud_lng_language_gd\" = \"Scots Gaelic\";\n\"cloud_lng_language_gl\" = \"Galician\";\n\"cloud_lng_language_gu\" = \"Gujarati\";\n\"cloud_lng_language_ha\" = \"Hausa\";\n\"cloud_lng_language_haw\" = \"Hawaiian\";\n\"cloud_lng_language_he\" = \"Hebrew\";\n\"cloud_lng_language_hi\" = \"Hindi\";\n// \"cloud_lng_language_hmn\" = \"Hmong\";\n\"cloud_lng_language_hr\" = \"Croatian\";\n\"cloud_lng_language_ht\" = \"Haitian Creole\";\n\"cloud_lng_language_hu\" = \"Hungarian\";\n\"cloud_lng_language_hy\" = \"Armenian\";\n\"cloud_lng_language_id\" = \"Indonesian\";\n\"cloud_lng_language_ig\" = \"Igbo\";\n\"cloud_lng_language_is\" = \"Icelandic\";\n\"cloud_lng_language_it\" = \"Italian\";\n\"cloud_lng_language_iw\" = \"Hebrew (Obsolete code)\";\n\"cloud_lng_language_ja\" = \"Japanese\";\n\"cloud_lng_language_jv\" = \"Javanese\";\n\"cloud_lng_language_ka\" = \"Georgian\";\n\"cloud_lng_language_kk\" = \"Kazakh\";\n\"cloud_lng_language_km\" = \"Khmer\";\n\"cloud_lng_language_kn\" = \"Kannada\";\n\"cloud_lng_language_ko\" = \"Korean\";\n\"cloud_lng_language_ku\" = \"Kurdish\";\n\"cloud_lng_language_ky\" = \"Kyrgyz\";\n\"cloud_lng_language_la\" = \"Latin\";\n\"cloud_lng_language_lb\" = \"Luxembourgish\";\n\"cloud_lng_language_lo\" = \"Lao\";\n\"cloud_lng_language_lt\" = \"Lithuanian\";\n\"cloud_lng_language_lv\" = \"Latvian\";\n\"cloud_lng_language_mg\" = \"Malagasy\";\n\"cloud_lng_language_mi\" = \"Maori\";\n\"cloud_lng_language_mk\" = \"Macedonian\";\n\"cloud_lng_language_ml\" = \"Malayalam\";\n\"cloud_lng_language_mn\" = \"Mongolian\";\n\"cloud_lng_language_mr\" = \"Marathi\";\n\"cloud_lng_language_ms\" = \"Malay\";\n\"cloud_lng_language_mt\" = \"Maltese\";\n\"cloud_lng_language_my\" = \"Burmese\";\n\"cloud_lng_language_ne\" = \"Nepali\";\n\"cloud_lng_language_nl\" = \"Dutch\";\n\"cloud_lng_language_no\" = \"Norwegian\";\n\"cloud_lng_language_ny\" = \"Nyanja\";\n\"cloud_lng_language_or\" = \"Odia (Oriya)\";\n\"cloud_lng_language_pa\" = \"Punjabi\";\n\"cloud_lng_language_pl\" = \"Polish\";\n\"cloud_lng_language_ps\" = \"Pashto\";\n\"cloud_lng_language_pt\" = \"Portuguese\";\n\"cloud_lng_language_ro\" = \"Romanian\";\n\"cloud_lng_language_ru\" = \"Russian\";\n\"cloud_lng_language_rw\" = \"Kinyarwanda\";\n\"cloud_lng_language_sd\" = \"Sindhi\";\n\"cloud_lng_language_si\" = \"Sinhala\";\n\"cloud_lng_language_sk\" = \"Slovak\";\n\"cloud_lng_language_sl\" = \"Slovenian\";\n\"cloud_lng_language_sm\" = \"Samoan\";\n\"cloud_lng_language_sn\" = \"Shona\";\n\"cloud_lng_language_so\" = \"Somali\";\n\"cloud_lng_language_sq\" = \"Albanian\";\n\"cloud_lng_language_sr\" = \"Serbian\";\n\"cloud_lng_language_st\" = \"Sesotho\";\n\"cloud_lng_language_su\" = \"Sundanese\";\n\"cloud_lng_language_sv\" = \"Swedish\";\n\"cloud_lng_language_sw\" = \"Swahili\";\n\"cloud_lng_language_ta\" = \"Tamil\";\n\"cloud_lng_language_te\" = \"Telugu\";\n\"cloud_lng_language_tg\" = \"Tajik\";\n\"cloud_lng_language_th\" = \"Thai\";\n\"cloud_lng_language_tk\" = \"Turkmen\";\n\"cloud_lng_language_tl\" = \"Tagalog\";\n\"cloud_lng_language_tr\" = \"Turkish\";\n\"cloud_lng_language_tt\" = \"Tatar\";\n\"cloud_lng_language_ug\" = \"Uyghur\";\n\"cloud_lng_language_uk\" = \"Ukrainian\";\n\"cloud_lng_language_ur\" = \"Urdu\";\n\"cloud_lng_language_uz\" = \"Uzbek\";\n\"cloud_lng_language_vi\" = \"Vietnamese\";\n\"cloud_lng_language_xh\" = \"Xhosa\";\n\"cloud_lng_language_yi\" = \"Yiddish\";\n\"cloud_lng_language_yo\" = \"Yoruba\";\n\"cloud_lng_language_zh\" = \"Chinese\";\n// \"cloud_lng_language_zh-CN\" = \"Chinese (Simplified)\";\n// \"cloud_lng_language_zh-TW\" = \"Chinese (Traditional)\";\n\"cloud_lng_language_zu\" = \"Zulu\";\n"
  },
  {
    "path": "Telegram/Resources/langs/de.lproj/Localizable.strings",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n"
  },
  {
    "path": "Telegram/Resources/langs/en.lproj/Localizable.strings",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n"
  },
  {
    "path": "Telegram/Resources/langs/es.lproj/Localizable.strings",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n"
  },
  {
    "path": "Telegram/Resources/langs/it.lproj/Localizable.strings",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n"
  },
  {
    "path": "Telegram/Resources/langs/ko.lproj/Localizable.strings",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n"
  },
  {
    "path": "Telegram/Resources/langs/lang.strings",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\"lng_language_name\" = \"English\";\n\"lng_switch_to_this\" = \"Continue in English\";\n\n\"lng_menu_contacts\" = \"Contacts\";\n\"lng_menu_calls\" = \"Calls\";\n\"lng_menu_settings\" = \"Settings\";\n\"lng_menu_about\" = \"About\";\n\"lng_menu_update\" = \"Update\";\n\"lng_menu_night_mode\" = \"Night Mode\";\n\"lng_menu_add_account\" = \"Add Account\";\n\"lng_menu_activate\" = \"Use this account\";\n\"lng_menu_set_status\" = \"Set Emoji Status\";\n\"lng_menu_change_status\" = \"Change Emoji Status\";\n\"lng_menu_my_profile\" = \"My Profile\";\n\"lng_menu_my_stories\" = \"My Stories\";\n\"lng_menu_my_groups\" = \"My Groups\";\n\"lng_menu_my_channels\" = \"My Channels\";\n\"lng_main_menu\" = \"Main menu\";\n\"lng_filter_unread_chats#one\" = \"{text} ({count} unread chat)\";\n\"lng_filter_unread_chats#other\" = \"{text} ({count} unread chats)\";\n\n\"lng_disable_notifications_from_tray\" = \"Disable notifications\";\n\"lng_enable_notifications_from_tray\" = \"Enable notifications\";\n\"lng_open_from_tray\" = \"Open Telegram\";\n\"lng_minimize_to_tray\" = \"Minimize to Tray\";\n\"lng_quit_from_tray\" = \"Quit Telegram\";\n\"lng_tray_icon_text\" = \"Telegram is still running here,\\nyou can change this in Settings.\\nIf it disappears from the tray,\\nyou can drag it back from the hidden icons.\";\n\n// For lng_month_year or plain month name.\n\"lng_month1\" = \"January\";\n\"lng_month2\" = \"February\";\n\"lng_month3\" = \"March\";\n\"lng_month4\" = \"April\";\n\"lng_month5\" = \"May\";\n\"lng_month6\" = \"June\";\n\"lng_month7\" = \"July\";\n\"lng_month8\" = \"August\";\n\"lng_month9\" = \"September\";\n\"lng_month10\" = \"October\";\n\"lng_month11\" = \"November\";\n\"lng_month12\" = \"December\";\n\n// For lng_month_day and lng_month_day_year.\n\"lng_month_day1\" = \"January\";\n\"lng_month_day2\" = \"February\";\n\"lng_month_day3\" = \"March\";\n\"lng_month_day4\" = \"April\";\n\"lng_month_day5\" = \"May\";\n\"lng_month_day6\" = \"June\";\n\"lng_month_day7\" = \"July\";\n\"lng_month_day8\" = \"August\";\n\"lng_month_day9\" = \"September\";\n\"lng_month_day10\" = \"October\";\n\"lng_month_day11\" = \"November\";\n\"lng_month_day12\" = \"December\";\n\n\"lng_month1_small\" = \"Jan\";\n\"lng_month2_small\" = \"Feb\";\n\"lng_month3_small\" = \"Mar\";\n\"lng_month4_small\" = \"Apr\";\n\"lng_month5_small\" = \"May\";\n\"lng_month6_small\" = \"Jun\";\n\"lng_month7_small\" = \"Jul\";\n\"lng_month8_small\" = \"Aug\";\n\"lng_month9_small\" = \"Sep\";\n\"lng_month10_small\" = \"Oct\";\n\"lng_month11_small\" = \"Nov\";\n\"lng_month12_small\" = \"Dec\";\n\n\"lng_weekday1\" = \"Mon\";\n\"lng_weekday2\" = \"Tue\";\n\"lng_weekday3\" = \"Wed\";\n\"lng_weekday4\" = \"Thu\";\n\"lng_weekday5\" = \"Fri\";\n\"lng_weekday6\" = \"Sat\";\n\"lng_weekday7\" = \"Sun\";\n\n\"lng_month_day\" = \"{month} {day}\";\n\"lng_month_day_year\" = \"{month} {day}, {year}\";\n\"lng_month_year\" = \"{month} {year}\";\n\n\"lng_calendar_select_days\" = \"Select days\";\n\"lng_calendar_start_tip\" = \"Press and hold to jump to the start.\";\n\"lng_calendar_end_tip\" = \"Press and hold to jump to the end.\";\n\"lng_calendar_days#one\" = \"{count} day\";\n\"lng_calendar_days#other\" = \"{count} days\";\n\n\"lng_seconds#one\" = \"{count} second\";\n\"lng_seconds#other\" = \"{count} seconds\";\n\"lng_minutes#one\" = \"{count} minute\";\n\"lng_minutes#other\" = \"{count} minutes\";\n\"lng_hours#one\" = \"{count} hour\";\n\"lng_hours#other\" = \"{count} hours\";\n\"lng_days#one\" = \"{count} day\";\n\"lng_days#other\" = \"{count} days\";\n\"lng_weeks#one\" = \"{count} week\";\n\"lng_weeks#other\" = \"{count} weeks\";\n\"lng_months#one\" = \"{count} month\";\n\"lng_months#other\" = \"{count} months\";\n\"lng_years#one\" = \"{count} year\";\n\"lng_years#other\" = \"{count} years\";\n\n\"lng_seconds_tiny#one\" = \"{count}s\";\n\"lng_seconds_tiny#other\" = \"{count}s\";\n\"lng_minutes_tiny#one\" = \"{count}m\";\n\"lng_minutes_tiny#other\" = \"{count}m\";\n\"lng_hours_tiny#one\" = \"{count}h\";\n\"lng_hours_tiny#other\" = \"{count}h\";\n\"lng_days_tiny#one\" = \"{count}d\";\n\"lng_days_tiny#other\" = \"{count}d\";\n\"lng_weeks_tiny#one\" = \"{count}w\";\n\"lng_weeks_tiny#other\" = \"{count}w\";\n\"lng_months_tiny#one\" = \"{count}m\";\n\"lng_months_tiny#other\" = \"{count}m\";\n\"lng_years_tiny#one\" = \"{count}y\";\n\"lng_years_tiny#other\" = \"{count}y\";\n\n\"lng_box_ok\" = \"OK\";\n\"lng_box_done\" = \"Done\";\n\"lng_box_yes\" = \"Yes\";\n\"lng_box_no\" = \"No\";\n\n\"lng_cancel\" = \"Cancel\";\n\"lng_continue\" = \"Continue\";\n\"lng_close\" = \"Close\";\n\"lng_minimize_window\" = \"Minimize\";\n\"lng_maximize_window\" = \"Maximize\";\n\"lng_restore_window\" = \"Restore\";\n\"lng_go_back\" = \"Go back\";\n\"lng_connecting\" = \"Connecting...\";\n\"lng_reconnecting#one\" = \"Reconnect in {count} s...\";\n\"lng_reconnecting#other\" = \"Reconnect in {count} s...\";\n\"lng_reconnecting_try_now\" = \"Try now\";\n\n\"lng_code_block_header_copy\" = \"copy\";\n\n\"lng_status_service_notifications\" = \"service notifications\";\n\"lng_status_support\" = \"support\";\n\"lng_status_bot\" = \"bot\";\n\"lng_status_bot_reads_all\" = \"has access to messages\";\n\"lng_status_bot_not_reads_all\" = \"has no access to messages\";\n\"lng_status_offline\" = \"last seen a long time ago\";\n\"lng_status_recently\" = \"last seen recently\";\n\"lng_status_last_week\" = \"last seen within a week\";\n\"lng_status_last_month\" = \"last seen within a month\";\n\"lng_status_lastseen_now\" = \"last seen just now\";\n\"lng_status_lastseen_when\" = \"when?\";\n\"lng_status_lastseen_minutes#one\" = \"last seen {count} minute ago\";\n\"lng_status_lastseen_minutes#other\" = \"last seen {count} minutes ago\";\n\"lng_status_lastseen_hours#one\" = \"last seen {count} hour ago\";\n\"lng_status_lastseen_hours#other\" = \"last seen {count} hours ago\";\n\"lng_status_lastseen_today\" = \"last seen today at {time}\";\n\"lng_status_lastseen_yesterday\" = \"last seen yesterday at {time}\";\n\"lng_status_lastseen_date\" = \"last seen {date}\";\n\"lng_status_lastseen_date_time\" = \"last seen {date} at {time}\";\n\"lng_status_online\" = \"online\";\n\"lng_status_connecting\" = \"connecting...\";\n\n\"lng_chat_status_unaccessible\" = \"group is inaccessible\";\n\"lng_chat_status_members#one\" = \"{count} member\";\n\"lng_chat_status_members#other\" = \"{count} members\";\n\"lng_chat_status_online#one\" = \"{count} online\";\n\"lng_chat_status_online#other\" = \"{count} online\";\n\"lng_chat_status_members_online\" = \"{members_count}, {online_count}\";\n\"lng_chat_status_subscribers#one\" = \"{count} subscriber\";\n\"lng_chat_status_subscribers#other\" = \"{count} subscribers\";\n\"lng_chat_status_direct\" = \"Direct messages\";\n\n\"lng_channel_status\" = \"channel\";\n\"lng_group_status\" = \"group\";\n\"lng_scam_badge\" = \"SCAM\";\n\"lng_fake_badge\" = \"FAKE\";\n\"lng_direct_badge\" = \"DIRECT\";\n\n\"lng_remember\" = \"Remember this choice\";\n\n\"lng_lastseen_show_title\" = \"Show Your Last Seen\";\n\"lng_lastseen_show_about\" = \"To see **{user}'s** Last Seen time, either start\\nshowing your own Last Seen time...\";\n\"lng_lastseen_show_button\" = \"Show My Last Seen\";\n\"lng_lastseen_or\" = \"or\";\n\"lng_lastseen_premium_title\" = \"Upgrade to Premium\";\n\"lng_lastseen_premium_about\" = \"Subscribing will let you see **{user}'s** Last Seen\\nstatus without showing yours.\";\n\"lng_lastseen_premium_button\" = \"Subscribe to Telegram Premium\";\n\"lng_lastseen_shown_toast\" = \"Your last seen time is now visible.\";\n\n\"lng_readtime_show_title\" = \"Show Your Read Date\";\n\"lng_readtime_show_about\" = \"To see when **{user}** read the message,\\neither start showing your own read time...\";\n\"lng_readtime_show_button\" = \"Show My Read Time\";\n\"lng_readtime_or\" = \"or\";\n\"lng_readtime_premium_title\" = \"Upgrade to Premium\";\n\"lng_readtime_premium_about\" = \"Subscription will let you see **{user}'s** read time\\nwithout showing yours.\";\n\"lng_readtime_premium_button\" = \"Subscribe to Telegram Premium\";\n\"lng_readtime_shown_toast\" = \"Your read times are now visible.\";\n\n\"lng_channels_limit_title\" = \"Too Many Communities\";\n\"lng_channels_limit1#one\" = \"You are a member of **{count}** group or channel.\";\n\"lng_channels_limit1#other\" = \"You are a member of **{count}** groups and channels.\";\n\"lng_channels_limit2#one\" = \"Please leave some before joining a new one — or upgrade to **Telegram Premium** to double the limit to **{count}** group or channel.\";\n\"lng_channels_limit2#other\" = \"Please leave some before joining a new one — or upgrade to **Telegram Premium** to double the limit to **{count}** groups and channels.\";\n\"lng_channels_limit2_final\" = \"Please leave some before joining a new one.\";\n\"lng_channels_leave_title\" = \"Least active communities\";\n\"lng_channels_leave_status\" = \"{type}, inactive {time}\";\n\"lng_channels_leave#one\" = \"Leave {count} community\";\n\"lng_channels_leave#other\" = \"Leave {count} communities\";\n\"lng_channels_leave_done\" = \"You've left the selected communities.\";\n\n\"lng_links_limit_title\" = \"Too Many Public Links\";\n\"lng_links_limit1#one\" = \"You have reserved **{count}** public link.\";\n\"lng_links_limit1#other\" = \"You have reserved **{count}** public links.\";\n\"lng_links_limit2#one\" = \"Try revoking the link from an older group or channel, or subscribe to **Telegram Premium** to double the limit to **{count}** public link.\";\n\"lng_links_limit2#other\" = \"Try revoking the link from an older group or channel, or subscribe to **Telegram Premium** to double the limit to **{count}** public links.\";\n\"lng_links_limit2_final\" = \"Try revoking the link from an older group or channel\";\n\"lng_links_revoke_title\" = \"Your public communities\";\n\n\"lng_filter_chats_limit_title\" = \"Limit Reached\";\n\"lng_filter_chats_limit1#one\" = \"Sorry, you can't add more than **{count}** chat to a folder.\";\n\"lng_filter_chats_limit1#other\" = \"Sorry, you can't add more than **{count}** chats to a folder.\";\n\"lng_filter_chats_exlude_limit1#one\" = \"Sorry, you can't exclude more than **{count}** chat from a folder.\";\n\"lng_filter_chats_exlude_limit1#other\" = \"Sorry, you can't exclude more than **{count}** chats from a folder.\";\n\"lng_filter_chats_limit2#one\" = \"You can increase this limit to **{count}** by subscribing to **Telegram Premium**.\";\n\"lng_filter_chats_limit2#other\" = \"You can increase this limit to **{count}** by subscribing to **Telegram Premium**.\";\n\n\"lng_filters_limit_title\" = \"Limit Reached\";\n\"lng_filters_limit1#one\" = \"You have reached the limit of **{count}** folder.\";\n\"lng_filters_limit1#other\" = \"You have reached the limit of **{count}** folders.\";\n\"lng_filters_limit2#one\" = \"You can double the limit to **{count}** folder by subscribing to **Telegram Premium**.\";\n\"lng_filters_limit2#other\" = \"You can double the limit to **{count}** folders by subscribing to **Telegram Premium**.\";\n\n\"lng_filter_pin_limit_title\" = \"Limit Reached\";\n\"lng_filter_pin_limit1#one\" = \"Sorry, you can't pin more than **{count}** chat to the top.\";\n\"lng_filter_pin_limit1#other\" = \"Sorry, you can't pin more than **{count}** chats to the top.\";\n\"lng_filter_pin_limit2#one\" = \"Unpin some of the currently pinned ones or subscribe to **Telegram Premium** to double the limit to **{count}** chat.\";\n\"lng_filter_pin_limit2#other\" = \"Unpin some of the currently pinned ones or subscribe to **Telegram Premium** to double the limit to **{count}** chats.\";\n\n\"lng_forum_pin_limit#one\" = \"Sorry, you can't pin more than **{count}** topic to the top.\";\n\"lng_forum_pin_limit#other\" = \"Sorry, you can't pin more than **{count}** topics to the top.\";\n\n\"lng_fave_sticker_limit_title#one\" = \"Limit of {count} Sticker Reached\";\n\"lng_fave_sticker_limit_title#other\" = \"Limit of {count} Stickers Reached\";\n\"lng_fave_sticker_limit_more#one\" = \"An older sticker was replaced with this one.\\nYou can {link} to {count} sticker.\";\n\"lng_fave_sticker_limit_more#other\" = \"An older sticker was replaced with this one.\\nYou can {link} to {count} stickers.\";\n\"lng_fave_sticker_limit_link\" = \"increase the limit\";\n\n\"lng_saved_gif_limit_title#one\" = \"Limit of {count} GIF Reached\";\n\"lng_saved_gif_limit_title#other\" = \"Limit of {count} GIFs Reached\";\n\"lng_saved_gif_limit_more#one\" = \"An older GIF was replaced with this one.\\nYou can {link} to {count} GIF.\";\n\"lng_saved_gif_limit_more#other\" = \"An older GIF was replaced with this one.\\nYou can {link} to {count} GIFs.\";\n\"lng_saved_gif_limit_link\" = \"increase the limit\";\n\n\"lng_caption_limit_title\" = \"Limit Reached\";\n\"lng_caption_limit1#one\" = \"Sorry, you can't use more than **{count}** character in media captions.\";\n\"lng_caption_limit1#other\" = \"Sorry, you can't use more than **{count}** characters in media captions.\";\n\"lng_caption_limit2#one\" = \"Make the caption shorter or subscribe to **Telegram Premium** to double the limit to **{count}** character.\";\n\"lng_caption_limit2#other\" = \"Make the caption shorter or subscribe to **Telegram Premium** to double the limit to **{count}** characters.\";\n\"lng_caption_limit_reached#one\" = \"You've reached the media caption limit. Please make the caption shorter by {count} character.\";\n\"lng_caption_limit_reached#other\" = \"You've reached the media caption limit. Please make the caption shorter by {count} characters.\";\n\"lng_caption_move_up\" = \"Move Caption Up\";\n\"lng_caption_move_down\" = \"Move Caption Down\";\n\n\"lng_file_size_limit_title\" = \"File Too Large\";\n\"lng_file_size_limit#one\" = \"{count} GB\";\n\"lng_file_size_limit#other\" = \"{count} GB\";\n\"lng_file_size_limit1\" = \"The document can't be sent, because it is larger than {size}.\";\n\"lng_file_size_limit2\" = \"You can double this limit to {size} per document by subscribing to **Telegram Premium**.\";\n\n\"lng_filter_links_limit_title\" = \"Limit Reached\";\n\"lng_filter_links_limit1#one\" = \"Sorry, you can't create more than **{count}** invite link.\";\n\"lng_filter_links_limit1#other\" = \"Sorry, you can't create more than **{count}** invite links.\";\n\"lng_filter_links_limit2#one\" = \"You can increase the limit to **{count}** link by subscribing to **Telegram Premium**.\";\n\"lng_filter_links_limit2#other\" = \"You can increase the limit to **{count}** links by subscribing to **Telegram Premium**.\";\n\n\"lng_filter_shared_limit_title\" = \"Limit Reached\";\n\"lng_filter_shared_limit1#one\" = \"Sorry, you can't add more than **{count}** shareable folders.\";\n\"lng_filter_shared_limit1#other\" = \"Sorry, you can't add more than **{count}** shareable folders.\";\n\"lng_filter_shared_limit2#one\" = \"You can increase the limit to **{count}** folder by subscribing to **Telegram Premium**.\";\n\"lng_filter_shared_limit2#other\" = \"You can increase the limit to **{count}** folders by subscribing to **Telegram Premium**.\";\n\n\"lng_limits_increase\" = \"Increase Limit\";\n\n\"lng_sticker_premium_text\" = \"This set contains premium stickers like this one.\";\n\"lng_sticker_premium_view\" = \"View\";\n\"lng_animated_emoji_text\" = \"Subscribe to **Telegram Premium** to unlock this emoji.\";\n\"lng_animated_emoji_saved\" = \"Try sending these emoji in **Saved Messages** for free to test.\";\n\"lng_animated_emoji_saved_open\" = \"Open\";\n\n\"lng_flood_error\" = \"Too many tries. Please try again later.\";\n\"lng_edit_error\" = \"You can't edit this message.\";\n\"lng_error_phone_flood\" = \"Sorry, you have deleted and re-created your account too many times recently. Please wait for a few days before signing up again.\";\n\"lng_error_start_minimized_passcoded\" = \"You have set a local passcode, so Telegram Desktop can't be launched minimized; it will ask you to enter your passcode before it can start working.\";\n\"lng_error_public_groups_denied\" = \"Unfortunately, you are banned from participating in public groups.\\n{more_info}\";\n\"lng_error_cant_add_member\" = \"Sorry, you can't add the bot to this group. Ask a group admin to do it.\";\n\"lng_error_cant_add_bot\" = \"Sorry, this bot can't be added to groups.\";\n\"lng_error_cant_add_admin_invite\" = \"You can't add this user as an admin because they are not a member of this group and you are not allowed to add them.\";\n\"lng_error_you_blocked_user\" = \"Sorry, you can't add this user or bot to groups because you've blocked them. Please unblock to proceed.\";\n\"lng_error_add_admin_not_member\" = \"You can't add this user as an admin because they are not a member of this group and you are not allowed to add them.\";\n\"lng_error_user_admin_invalid\" = \"You can't ban this user because they are an admin in this group and you are not allowed to demote them.\";\n\"lng_error_channel_bots_too_much\" = \"Sorry, this channel has too many bots.\";\n\"lng_error_group_bots_too_much\" = \"There are too many bots in this group. Please remove some of the bots you're not using first.\";\n\"lng_error_cant_add_admin_unban\" = \"Sorry, you can't add this user as an admin because they are in the Removed Users list and you can't unban them.\";\n\"lng_error_cant_ban_admin\" = \"You can't ban this user because they are an admin in this group and you are not allowed to demote them.\";\n\"lng_error_cant_reply_other\" = \"This message can't be replied in another chat.\";\n\"lng_error_admin_limit\" = \"Sorry, you've reached the maximum number of admins for this group.\";\n\"lng_error_admin_limit_channel\" = \"Sorry, you've reached the maximum number of admins for this channel.\";\n\"lng_error_post_link_invalid\" = \"Unfortunately, you can't access this message. You aren't a member of the chat where it was posted.\";\n\"lng_error_noforwards_group\" = \"Sorry, forwarding from this group is disabled by admins.\";\n\"lng_error_noforwards_channel\" = \"Sorry, forwarding from this channel is disabled by admins.\";\n\"lng_error_nocopy_group\" = \"Sorry, copying from this group is disabled by admins.\";\n\"lng_error_nocopy_channel\" = \"Sorry, copying from this channel is disabled by admins.\";\n\"lng_error_noforwards_user\" = \"Sorry, forwarding from this chat is restricted.\";\n\"lng_error_nocopy_user\" = \"Sorry, copying from this chat is restricted.\";\n\"lng_error_nocopy_story\" = \"Sorry, the creator of this story disabled copying.\";\n\"lng_error_schedule_limit\" = \"Sorry, you can't schedule more than 100 messages.\";\n\"lng_sure_add_admin_invite\" = \"This user is not a member of this group. Add them to the group and promote them to admin?\";\n\"lng_sure_add_admin_invite_channel\" = \"This user is not a subscriber of this channel. Add them to the channel and promote them to admin?\";\n\"lng_sure_add_admin_unremove\" = \"This user is currently restricted or removed. Are you sure you want to promote them?\";\n\"lng_sure_ban_admin\" = \"This user is an admin. Are you sure you want to go ahead and restrict them?\";\n\"lng_sure_enable_socks\" = \"Are you sure you want to enable this proxy?\\n\\nServer: {server}\\nPort: {port}\\n\\nYou can change your proxy server later in Settings > Advanced > Connection Type.\";\n\"lng_sure_enable\" = \"Enable\";\n\"lng_proxy_box_title\" = \"Enable proxy\";\n\"lng_proxy_box_table_title\" = \"Proxy Server\";\n\"lng_proxy_box_table_button\" = \"Connect Proxy\";\n\"lng_proxy_box_table_checking\" = \"Checking…\";\n\"lng_proxy_box_table_available\" = \"Available (ping: {ping} ms)\";\n\"lng_proxy_box_table_unavailable\" = \"Not Available\";\n\"lng_proxy_box_server\" = \"Server\";\n\"lng_proxy_box_port\" = \"Port\";\n\"lng_proxy_box_secret\" = \"Secret\";\n\"lng_proxy_box_status\" = \"Status\";\n\"lng_proxy_box_check_status\" = \"Check Status\";\n\"lng_proxy_box_username\" = \"Username\";\n\"lng_proxy_box_password\" = \"Password\";\n\"lng_proxy_invalid\" = \"The proxy link is invalid.\";\n\"lng_proxy_unsupported\" = \"Your Telegram Desktop version doesn't support this proxy type or the proxy link is invalid. Please update Telegram Desktop to the latest version.\";\n\"lng_proxy_incorrect_secret\" = \"This proxy link uses invalid **secret** parameter. Please contact the proxy provider and ask him to update MTProxy source code and configure it with a correct **secret** value. Then let him provide a new link.\";\n\"lng_proxy_check_ip_warning_title\" = \"Warning\";\n\"lng_proxy_check_ip_warning\" = \"This will expose your IP address to the admin of the proxy server.\";\n\"lng_proxy_check_ip_proceed\" = \"Proceed\";\n\n\"lng_edit_deleted\" = \"This message was deleted\";\n\"lng_edit_limit_reached#one\" = \"You've reached the message text limit. Please make the text shorter by {count} character.\";\n\"lng_edit_limit_reached#other\" = \"You've reached the message text limit. Please make the text shorter by {count} characters.\";\n\"lng_edit_message\" = \"Edit message\";\n\"lng_edit_message_text\" = \"New message...\";\n\"lng_deleted\" = \"Deleted Account\";\n\"lng_deleted_message\" = \"Deleted message\";\n\"lng_deleted_story\" = \"Deleted story\";\n\"lng_pinned_message\" = \"Pinned message\";\n\"lng_pinned_previous\" = \"Previous message\";\n\"lng_pinned_unpin_sure\" = \"Would you like to unpin this message?\";\n\"lng_pinned_pin_sure\" = \"Would you like to pin this message?\";\n\"lng_pinned_pin_sure_group\" = \"Pin this message in the group?\";\n\"lng_pinned_pin_old_sure\" = \"Do you want to pin an older message while leaving a more recent one pinned?\";\n\"lng_pinned_pin\" = \"Pin\";\n\"lng_pinned_unpin\" = \"Unpin\";\n\"lng_pinned_notify\" = \"Notify all members\";\n\"lng_pinned_also_for_other\" = \"Also pin for {user}\";\n\"lng_pinned_messages_title#one\" = \"{count} pinned message\";\n\"lng_pinned_messages_title#other\" = \"{count} pinned messages\";\n\"lng_pinned_hide_all\" = \"Don't show pinned messages\";\n\"lng_pinned_unpin_all#one\" = \"Unpin {count} message\";\n\"lng_pinned_unpin_all#other\" = \"Unpin all {count} messages\";\n\"lng_pinned_unpin_all_sure\" = \"Do you want to unpin all messages?\";\n\"lng_pinned_hide_all_sure\" = \"Do you want to hide the pinned message bar? It will stay hidden until a new message is pinned.\";\n\"lng_pinned_hide_all_hide\" = \"Hide\";\n\n\"lng_binded\" = \"Dialog was binded.\";\n\"lng_unbinded\" = \"Dialog was unbinded.\";\n\n\"lng_edit_media_album_error\" = \"This file cannot be saved as a part of an album.\";\n\"lng_edit_media_invalid_file\" = \"Sorry, no way to use this file.\";\n\"lng_edit_photo_editor_hint\" = \"Left-click on the photo to edit.\";\n\"lng_edit_caption_attach\" = \"Sorry, you can't attach new media while editing a message.\";\n\"lng_edit_caption_voice\" = \"Sorry, you can't edit messages when you have an unsent voice message.\";\n\n\"lng_intro_about\" = \"Welcome to the official Telegram Desktop app.\\nIt's fast and secure.\";\n\"lng_start_msgs\" = \"Start Messaging\";\n\n\"lng_intro_next\" = \"Next\";\n\"lng_intro_finish\" = \"Sign Up\";\n\"lng_intro_submit\" = \"Submit\";\n\n\"lng_photo_caption\" = \"Caption\";\n\"lng_photos_comment\" = \"Comment\";\n\n\"lng_intro_qr_title\" = \"Scan From Mobile Telegram\";\n\"lng_intro_qr_step1\" = \"Open Telegram on your phone\";\n\"lng_intro_qr_step2\" = \"Go to Settings > Devices > Link Desktop Device\";\n\"lng_intro_qr_step3\" = \"Scan this image to Log In\";\n\"lng_intro_qr_skip\" = \"Or log in using your phone number\";\n\"lng_intro_qr_phone\" = \"Log in using phone number\";\n\"lng_intro_qr_passkey\" = \"Log in using passkey\";\n\n\"lng_intro_fragment_title\" = \"Enter code\";\n\"lng_intro_fragment_about\" = \"Get the code for {phone_number} in the Anonymous Numbers section on Fragment.\";\n\"lng_intro_fragment_button\" = \"Open Fragment\";\n\n\"lng_intro_email_setup_title\" = \"Choose a login email\";\n\"lng_intro_email_confirm_subtitle\" = \"Please check your email {email} (don't forget the spam folder) and enter the code we just sent you.\";\n\n\"lng_phone_title\" = \"Your Phone Number\";\n\"lng_phone_desc\" = \"Please confirm your country code\\nand enter your phone number.\";\n\"lng_phone_to_qr\" = \"Quick log in using QR code\";\n\"lng_country_code\" = \"Country Code\";\n\"lng_bad_country_code\" = \"Invalid Country Code\";\n\"lng_country_ph\" = \"Search\";\n\"lng_country_none\" = \"Country not found\";\n\"lng_country_select\" = \"Select Country\";\n\"lng_phone_number\" = \"Phone number\";\n\n\"lng_code_ph\" = \"Code\";\n\"lng_code_desc\" = \"We've sent an activation code to your phone.\\nPlease enter it below.\";\n\"lng_code_from_telegram\" = \"A code was sent **via Telegram** to your other\\ndevices, if you have any connected.\";\n\"lng_code_no_telegram\" = \"Send code via SMS\";\n\"lng_code_call\" = \"Telegram will call you in {minutes}:{seconds}\";\n\"lng_code_calling\" = \"Requesting a call from Telegram...\";\n\"lng_code_called\" = \"Telegram dialed your number\";\n\n\"lng_bad_phone\" = \"Invalid phone number. Please try again.\";\n\"lng_bad_code\" = \"You have entered an invalid code.\";\n\"lng_bad_name\" = \"Enter your first and last name.\";\n\"lng_bad_photo\" = \"Sorry, Telegram can't process that type of image.\";\n\n\"lng_signin_title\" = \"Cloud password check\";\n\"lng_signin_desc\" = \"Please enter your cloud password.\";\n\"lng_signin_recover_desc\" = \"Please enter the code from the email\\n{email}\";\n\"lng_signin_password\" = \"Your cloud password\";\n\"lng_signin_code\" = \"Code from the email\";\n\"lng_signin_recover\" = \"Forgot password?\";\n\"lng_signin_recover_title\" = \"Password reset\";\n\"lng_signin_hint\" = \"Hint: {password_hint}\";\n\"lng_signin_recover_hint\" = \"We sent a code to {recover_email}\";\n\"lng_signin_bad_password\" = \"You have entered a wrong password.\";\n\"lng_signin_wrong_code\" = \"You have entered an invalid code.\";\n\"lng_signin_try_password\" = \"Unable to access your email?\";\n\"lng_signin_no_email_forgot\" = \"Since you didn't provide a recovery email when setting up your password, your remaining options are either to remember your password or to reset your account.\";\n\"lng_signin_cant_email_forgot\" = \"If you can't restore access to the email, your remaining options are either to remember your password or to reset your account.\";\n\"lng_signin_reset_account\" = \"Reset your account\";\n\"lng_signin_sure_reset\" = \"You will lose all your Telegram chats, messages, media and files if you proceed.\\n\\nDo you want to reset your account?\";\n\"lng_signin_reset\" = \"Reset\";\n\"lng_signin_reset_wait\" = \"Since the account {phone_number} is active and protected by a password, it will be deleted in 1 week. This delay is required for security purposes. You can cancel this process anytime.\\n\\nYou'll be able to reset your account in:\\n{when}\";\n\"lng_signin_reset_in_days\" = \"{days_count} {hours_count} {minutes_count}\";\n\"lng_signin_reset_in_hours\" = \"{hours_count} {minutes_count}\";\n\"lng_signin_reset_cancelled\" = \"Your recent attempts to reset this account have been canceled by its active user. Please try again in 7 days.\";\n\"lng_signin_banned_text\" = \"This phone number is banned.\";\n\"lng_signin_banned_help\" = \"Help\";\n\n\"lng_signup_title\" = \"Your Info\";\n\"lng_signup_desc\" = \"Please enter your name and\\nupload a photo.\";\n\n\"lng_signup_firstname\" = \"First name\";\n\"lng_signup_lastname\" = \"Last name\";\n\n\"lng_dlg_filter\" = \"Search\";\n\"lng_dlg_new_group_name\" = \"Group name\";\n\"lng_dlg_new_channel_name\" = \"Channel name\";\n\"lng_dlg_new_bot_name\" = \"Bot name\";\n\"lng_no_chats\" = \"Your chats will be here\";\n\"lng_no_conversations\" = \"You have no\\nconversations yet.\";\n\"lng_no_conversations_button\" = \"New Message\";\n\"lng_no_conversations_subtitle\" = \"Your contacts on Telegram\";\n\"lng_no_chats_filter\" = \"No chats currently belong to this folder.\";\n\"lng_no_saved_sublists\" = \"You can save messages from other chats here.\";\n\"lng_contacts_loading\" = \"Loading...\";\n\"lng_contacts_not_found\" = \"No contacts found\";\n\"lng_topics_not_found\" = \"No topics found.\";\n\"lng_forum_all_messages\" = \"All Messages\";\n\"lng_forum_create_new_topic\" = \"Create New Thread\";\n\"lng_dlg_search_for_messages\" = \"Search for messages\";\n\"lng_update_telegram\" = \"Update Telegram\";\n\"lng_dlg_search_in\" = \"Search messages in\";\n\"lng_dlg_search_from\" = \"From: {user}\";\n\"lng_chat_menu\" = \"Chat menu\";\n\n\"lng_settings_save\" = \"Save\";\n\"lng_settings_apply\" = \"Apply\";\n\n\"lng_username_title\" = \"Username\";\n\"lng_username_description1\" = \"You can choose a username on Telegram. If you do, other people will be able to find you by this username and contact you without knowing your phone number.\";\n\"lng_username_description2\" = \"You can use **a-z**, **0-9** and **underscores**.\\nMinimum length is **5 characters**.\";\n\"lng_username_choose\" = \"Choose your username.\";\n\"lng_username_invalid\" = \"This username is invalid.\";\n\"lng_username_occupied\" = \"This username is already occupied.\";\n\"lng_username_too_short\" = \"This username is too short.\";\n\"lng_username_purchase_available\" = \"Sorry, this link is taken. But it's available for purchase. {link}\";\n\"lng_username_purchase_available_link\" = \"Learn more...\";\n\"lng_username_bad_symbols\" = \"Only a-z, 0-9, and underscores allowed.\";\n\"lng_username_available\" = \"This username is available.\";\n\"lng_username_not_found\" = \"Username @{user} not found.\";\n\"lng_username_by_phone_not_found\" = \"User {phone} not found.\";\n\"lng_username_app_not_found\" = \"Bot application not found.\";\n\"lng_username_link\" = \"This link opens a chat with you:\";\n\"lng_username_copied\" = \"Link copied to clipboard.\";\n\"lng_username_text_copied\" = \"Username copied to clipboard.\";\n\n\"lng_usernames_edit\" = \"click to edit\";\n\"lng_usernames_active\" = \"active\";\n\"lng_usernames_non_active\" = \"inactive\";\n\"lng_usernames_subtitle\" = \"Username order\";\n\"lng_usernames_activate_error#one\" = \"Sorry, you can't activate more than **{count}** usernames.\";\n\"lng_usernames_activate_error#other\" = \"Sorry, you can't activate more than **{count}** usernames.\";\n\"lng_usernames_activate_description\" = \"Do you want to show this username on your info page?\";\n\"lng_usernames_activate_confirm\" = \"Show\";\n\"lng_channel_usernames_subtitle\" = \"Link order\";\n\"lng_usernames_deactivate_description\" = \"Do you want to hide this username from your info page?\";\n\"lng_usernames_deactivate_confirm\" = \"Hide\";\n\"lng_usernames_description\" = \"Drag and drop links to change the order in which they will be displayed on your info page.\";\n\n\"lng_channel_usernames_activate_description\" = \"Do you want to show this link on the channel info page?\";\n\"lng_channel_usernames_deactivate_description\" = \"Do you want to hide this link from the channel info page?\";\n\"lng_channel_usernames_description\" = \"Drag and drop links to change the order in which they will be displayed on the channel info page.\";\n\n\"lng_bot_username_title\" = \"Username\";\n\"lng_bot_username_description1\" = \"This link cannot be edited. You can acquire additional usernames on {link}.\";\n\"lng_bot_username_description1_link\" = \"Fragment\";\n\"lng_bot_usernames_activate_description\" = \"Do you want to show this link on the bot info page?\";\n\"lng_bot_usernames_deactivate_description\" = \"Do you want to hide this link from the bot info page?\";\n\"lng_bot_usernames_description\" = \"Drag and drop links to change the order in which they will be displayed on the bot info page.\";\n\n\"lng_bio_placeholder\" = \"Bio\";\n\n\"lng_collectible_username_title\" = \"{username} is a collectible username that belongs to\";\n\"lng_collectible_username_info\" = \"This username was bought on **Fragment** on {date} for {price}\";\n\"lng_collectible_username_copy\" = \"Copy Link\";\n\"lng_collectible_phone_title\" = \"{phone} is a collectible phone number that belongs to\";\n\"lng_collectible_phone_info\" = \"This phone number was bought on **Fragment** on {date} for {price}\";\n\"lng_collectible_phone_copy\" = \"Copy Phone Number\";\n\"lng_collectible_learn_more\" = \"Learn More\";\n\"lng_collectible_phone_copied\" = \"Phone number copied to clipboard.\";\n\n\"lng_settings_section_info\" = \"Info\";\n\n// Fork settings.\n\"lng_settings_section_fork\" = \"Fork Settings\";\n\"lng_settings_square_avatats\" = \"Square avatars\";\n\"lng_settings_audio_fade\" = \"Audio fade\";\n\"lng_settings_uri_scheme\" = \"Open links with custom URI Scheme\";\n\"lng_settings_uri_scheme_box_title\" = \"Custom URI Scheme\";\n\"lng_settings_uri_scheme_field_label\" = \"Example: potplayer://.\";\n\"lng_settings_last_seen_in_dialogs\" = \"Show last seen time in list of dialogs\";\n\"lng_settings_search_engine\" = \"Add a 'Search Selected Text' to Context Menu\";\n\"lng_settings_search_engine_box_title\" = \"URL of Search Engine\";\n\"lng_settings_search_engine_field_label\" = \"URL must contain '%q'\";\n\"lng_settings_show_all_recent_stickers\" = \"Show all Recently used Stickers\";\n\"lng_settings_use_black_tray_icon\" = \"Use black tray icon\";\n\"lng_settings_use_original_tray_icon\" = \"Use original tray icon\";\n\"lng_settings_auto_submit_passcode\" = \"Auto-submit local passcode\";\n\"lng_settings_emoji_on_click\" = \"Disable emoji popup display on hovering\";\n\"lng_settings_mention_by_name\" = \"Disable mention by name\";\n\"lng_settings_primary_unmuted\" = \"Unmuted Messages are Primary\";\n\n\"lng_settings_custom_sticker_size\" = \"Custom Sticker Size\";\n\"lng_settings_sticker_size_label\" = \"Allowed values are between 50 and 256.\";\n\n// Link Device (QR login).\n\"lng_settings_link_device\" = \"Link Device\";\n\"lng_settings_link_device_privacy\" = \"Your camera feed stays on your device. No video data is sent over the network — QR codes are scanned locally.\";\n\"lng_settings_link_device_start\" = \"Open Camera\";\n\"lng_settings_link_device_scanning\" = \"Point camera at the QR code shown on the device you want to log in.\";\n\"lng_settings_link_device_success\" = \"Device linked successfully!\";\n\"lng_settings_link_device_error\" = \"Could not link device: {error}\";\n\"lng_settings_link_device_confirm\" = \"Do you want to link this device to your account?\";\n\"lng_settings_link_device_stop\" = \"Stop Camera\";\n\"lng_settings_link_device_clipboard\" = \"Scan QR from Clipboard\";\n\n\"lng_context_open_uri_link\" = \"Open Link with URI Scheme\";\n\n\"lng_context_search_selected\" = \"Search Selected Text\";\n\n\"lng_info_id_label\" = \"ID\";\n\"lng_profile_copy_id\" = \"Copy ID\";\n\"lng_info_id_topic_label\" = \"Topic ID\";\n\"lng_info_id_topic_value_label\" = \"{id}. Created at: {date}\";\n// \n\n\"lng_settings_section_notify\" = \"Notifications and Sounds\";\n\"lng_settings_show_from\" = \"Show notifications from\";\n\"lng_settings_notify_all\" = \"All accounts\";\n\"lng_settings_notify_all_about\" = \"Turn this off if you want to receive notifications only from the account you are currently using.\";\n\"lng_settings_notify_global\" = \"Global settings\";\n\"lng_settings_notify_title\" = \"Notifications for chats\";\n\"lng_settings_desktop_notify\" = \"Desktop notifications\";\n\"lng_settings_master_volume_notifications\" = \"Volume\";\n\"lng_settings_native_title\" = \"System integration\";\n\"lng_settings_use_windows\" = \"Use Windows notifications\";\n\"lng_settings_skip_in_focus\" = \"Respect system Focus mode\";\n\"lng_settings_use_native_notifications\" = \"Use native notifications\";\n\"lng_settings_notifications_position\" = \"Location on the screen\";\n\"lng_settings_notifications_count\" = \"Notifications count\";\n\"lng_settings_notifications_display\" = \"Display for notifications\";\n\"lng_settings_notifications_display_default\" = \"Default\";\n\"lng_settings_sound_allowed\" = \"Allow sound\";\n\"lng_settings_alert_windows\" = \"Flash the taskbar icon\";\n\"lng_settings_alert_mac\" = \"Bounce the Dock icon\";\n\"lng_settings_alert_linux\" = \"Draw attention to the window\";\n\"lng_settings_badge_title\" = \"Badge counter\";\n\"lng_settings_include_muted\" = \"Include muted chats in unread count\";\n\"lng_settings_include_muted_folders\" = \"Include muted chats in folder counters\";\n\"lng_settings_count_unread\" = \"Count unread messages\";\n\"lng_settings_events_title\" = \"Events\";\n\"lng_settings_events_joined\" = \"Contact joined Telegram\";\n\"lng_settings_events_pinned\" = \"Pinned messages\";\n\"lng_settings_notifications_calls_title\" = \"Calls\";\n\n\"lng_notification_preview_title\" = \"Dino Rex\";\n\"lng_notification_preview_text\" = \"It's morning in Tokyo 😎\";\n\"lng_notification_show_name\" = \"Name\";\n\"lng_notification_show_text\" = \"Text\";\n\"lng_notification_preview\" = \"You have a new message\";\n\"lng_notification_reply\" = \"Reply\";\n\"lng_notification_hide_all\" = \"Hide all\";\n\"lng_notification_sample\" = \"This is a sample notification\";\n\"lng_notification_reminder\" = \"Reminder\";\n\"lng_notification_private_chats\" = \"Private chats\";\n\"lng_notification_groups\" = \"Groups\";\n\"lng_notification_channels\" = \"Channels\";\n\"lng_notification_reactions\" = \"Reactions\";\n\"lng_notification_reactions_title\" = \"Notifications for reactions\";\n\"lng_notification_reactions_notify_about\" = \"Notify me about\";\n\"lng_notification_reactions_messages\" = \"Messages\";\n\"lng_notification_reactions_messages_full\" = \"Reactions to my messages\";\n\"lng_notification_reactions_poll_votes\" = \"Poll votes\";\n\"lng_notification_reactions_poll_votes_full\" = \"Votes in my polls\";\n\"lng_notification_reactions_from\" = \"Notify about reactions from\";\n\"lng_notification_reactions_from_nobody\" = \"Off\";\n\"lng_notification_reactions_from_contacts\" = \"From my contacts\";\n\"lng_notification_reactions_from_all\" = \"From everyone\";\n\"lng_notification_reactions_settings\" = \"Settings\";\n\"lng_notification_reactions_show_sender\" = \"Show sender's name\";\n\"lng_notification_click_to_change\" = \"Click here to change\";\n\"lng_notification_on\" = \"On, {exceptions}\";\n\"lng_notification_off\" = \"Off, {exceptions}\";\n\"lng_notification_exceptions#one\" = \"{count} exception\";\n\"lng_notification_exceptions#other\" = \"{count} exceptions\";\n\"lng_notification_exceptions_title\" = \"Exceptions\";\n\"lng_notification_title_private_chats\" = \"Notifications for private chats\";\n\"lng_notification_about_private_chats#one\" = \"Please note that **{count} chat** is listed as an exception and won't be affected by this change.\";\n\"lng_notification_about_private_chats#other\" = \"Please note that **{count} chats** are listed as exceptions and won't be affected by this change.\";\n\"lng_notification_volume_private_chats\" = \"Notifications volume for private chats\";\n\"lng_notification_title_groups\" = \"Notifications for groups\";\n\"lng_notification_about_groups#one\" = \"Please note that **{count} group** is listed as an exception and won't be affected by this change.\";\n\"lng_notification_about_groups#other\" = \"Please note that **{count} groups** are listed as exceptions and won't be affected by this change.\";\n\"lng_notification_volume_groups\" = \"Notifications volume for groups\";\n\"lng_notification_title_channels\" = \"Notifications for channels\";\n\"lng_notification_about_channels#one\" = \"Please note that **{count} channel** is listed as an exception and won't be affected by this change.\";\n\"lng_notification_about_channels#other\" = \"Please note that **{count} channels** are listed as exceptions and won't be affected by this change.\";\n\"lng_notification_volume_channel\" = \"Notifications volume for channels\";\n\"lng_notification_exceptions_view\" = \"View exceptions\";\n\"lng_notification_enable\" = \"Enable notifications\";\n\"lng_notification_sound\" = \"Sound\";\n\"lng_notification_tone\" = \"Notification tone\";\n\"lng_notification_exceptions_muted\" = \"Muted\";\n\"lng_notification_exceptions_unmuted\" = \"Unmuted\";\n\"lng_notification_exceptions_add\" = \"Add an exception\";\n\"lng_notification_exceptions_clear\" = \"Delete all exceptions\";\n\"lng_notification_exceptions_clear_sure\" = \"Are you sure you want to delete all exceptions?\";\n\"lng_notification_exceptions_clear_button\" = \"Delete\";\n\"lng_notification_exceptions_remove\" = \"Remove\";\n\"lng_notification_context_remove\" = \"Remove exception\";\n\n\"lng_reaction_text\" = \"{reaction} to your \\\"{text}\\\"\";\n\"lng_reaction_notext\" = \"{reaction} to your message\";\n\"lng_reaction_photo\" = \"{reaction} to your photo\";\n\"lng_reaction_video\" = \"{reaction} to your video\";\n\"lng_reaction_video_message\" = \"{reaction} to your video message\";\n\"lng_reaction_document\" = \"{reaction} to your file\";\n\"lng_reaction_sticker\" = \"{reaction} to your {emoji} sticker\";\n\"lng_reaction_voice_message\" = \"{reaction} to your voice message\";\n\"lng_reaction_contact\" = \"{reaction} to your contact {name}\";\n\"lng_reaction_location\" = \"{reaction} to your map\";\n\"lng_reaction_poll\" = \"{reaction} to your poll \\\"{title}\\\"\";\n\"lng_reaction_quiz\" = \"{reaction} to your quiz \\\"{title}\\\"\";\n\"lng_reaction_game\" = \"{reaction} to your game\";\n\"lng_reaction_invoice\" = \"{reaction} to your invoice\";\n\"lng_reaction_gif\" = \"{reaction} to your GIF\";\n\n\"lng_poll_vote_option\" = \"voted for \\\"{option}\\\" in your poll\";\n\"lng_poll_vote\" = \"voted in your poll \\\"{title}\\\"\";\n\"lng_poll_vote_notext\" = \"voted in your poll\";\n\n\"lng_effect_add_title\" = \"Add an animated effect\";\n\"lng_effect_stickers_title\" = \"Effects from stickers\";\n\"lng_effect_send\" = \"Send with Effect\";\n\"lng_effect_none\" = \"No effects found.\";\n\"lng_effect_premium\" = \"Subscribe to {link} to add this animated effect.\";\n\"lng_effect_premium_link\" = \"Telegram Premium\";\n\n\"lng_languages\" = \"Language\";\n\"lng_languages_none\" = \"No languages found.\";\n\"lng_languages_count#one\" = \"{count} language\";\n\"lng_languages_count#other\" = \"{count} languages\";\n\"lng_sure_save_language\" = \"Telegram will restart in order to change the language\";\n\"lng_settings_update_automatically\" = \"Update automatically\";\n\"lng_settings_install_beta\" = \"Install beta versions\";\n\"lng_settings_current_version\" = \"Version {version}\";\n\"lng_settings_check_now\" = \"Check for updates\";\n\"lng_settings_update_checking\" = \"Checking for updates...\";\n\"lng_settings_latest_installed\" = \"Latest version is installed\";\n\"lng_settings_update_ready\" = \"New version is ready\";\n\"lng_settings_update_fail\" = \"Update check failed :(\";\n\"lng_settings_workmode_tray\" = \"Show tray icon\";\n\"lng_settings_workmode_window\" = \"Show taskbar icon\";\n\"lng_settings_window_close\" = \"When window closed\";\n\"lng_settings_run_in_background\" = \"Run in the background\";\n\"lng_settings_quit_on_close\" = \"Quit the application\";\n\"lng_settings_close_to_taskbar\" = \"Close to taskbar\";\n\"lng_settings_monochrome_icon\" = \"Use monochrome icon\";\n\"lng_settings_window_system\" = \"Window title bar\";\n\"lng_settings_title_chat_name\" = \"Show chat name\";\n\"lng_settings_title_account_name\" = \"Show active account\";\n\"lng_settings_title_total_count\" = \"Total unread count\";\n\"lng_settings_native_frame\" = \"Use system window frame\";\n\"lng_settings_qt_frame\" = \"Use Qt window frame\";\n\"lng_settings_auto_start\" = \"Launch Telegram when system starts\";\n\"lng_settings_start_min\" = \"Launch minimized\";\n\"lng_settings_auto_start_disabled_uwp\" = \"Starting with the system was disabled in Windows Settings.\\n\\nPlease enable Telegram Desktop in the Startup Apps Settings.\";\n\"lng_settings_open_system_settings\" = \"Open Settings\";\n\"lng_settings_add_sendto\" = \"Place Telegram in \\\"Send to\\\" menu\";\n\"lng_settings_mac_warn_before_quit\" = \"Show warning before quitting with {text}\";\n\"lng_settings_mac_round_icon\" = \"Round application icon\";\n\n\"lng_settings_experimental\" = \"Experimental settings\";\n\"lng_settings_experimental_about\" = \"Warning! These are experimental settings. Some may not work. Others may break the app. Any of them may disappear in the next version without a trace. Use at your own risk.\";\n\"lng_settings_experimental_restore\" = \"Restore default values\";\n\"lng_settings_experimental_irrelevant\" = \"This option isn't relevant for your system.\";\n\n\"lng_settings_section_chat_settings\" = \"Chat Settings\";\n\"lng_settings_replace_emojis\" = \"Replace emoji automatically\";\n\"lng_settings_system_text_replace\" = \"System text replacements\";\n\"lng_settings_suggest_emoji\" = \"Suggest emoji replacements\";\n\"lng_settings_suggest_animated_emoji\" = \"Suggest animated emoji\";\n\"lng_settings_suggest_by_emoji\" = \"Suggest popular stickers by emoji\";\n\"lng_settings_loop_stickers\" = \"Loop animated stickers\";\n\"lng_settings_large_emoji\" = \"Large emoji\";\n\"lng_settings_send_enter\" = \"Send with Enter\";\n\"lng_settings_send_ctrlenter\" = \"Send with Ctrl+Enter\";\n\"lng_settings_send_cmdenter\" = \"Send with Cmd+Enter\";\n\"lng_settings_chat_quick_action_reply\" = \"Reply with double click\";\n\"lng_settings_chat_quick_action_react\" = \"Send reaction with double click\";\n\"lng_settings_chat_corner_reply\" = \"Reply button on messages\";\n\"lng_settings_chat_corner_reaction\" = \"Reaction button on messages\";\n\n\"lng_settings_shortcuts\" = \"Keyboard shortcuts\";\n\n\"lng_shortcuts_reset\" = \"Reset to default\";\n\"lng_shortcuts_recording\" = \"Recording...\";\n\"lng_shortcuts_add_another\" = \"Add another\";\n\n\"lng_shortcuts_close\" = \"Close the window\";\n\"lng_shortcuts_lock\" = \"Lock the application\";\n\"lng_shortcuts_minimize\" = \"Minimize the window\";\n\"lng_shortcuts_quit\" = \"Quit the application\";\n\"lng_shortcuts_media_play\" = \"Play the media\";\n\"lng_shortcuts_media_pause\" = \"Pause the media\";\n\"lng_shortcuts_media_play_pause\" = \"Toggle media playback\";\n\"lng_shortcuts_media_stop\" = \"Stop media playback\";\n\"lng_shortcuts_media_previous\" = \"Previous track\";\n\"lng_shortcuts_media_next\" = \"Next track\";\n\"lng_shortcuts_search\" = \"Search messages\";\n\"lng_shortcuts_chat_previous\" = \"Previous chat\";\n\"lng_shortcuts_chat_next\" = \"Next chat\";\n\"lng_shortcuts_chat_first\" = \"First chat\";\n\"lng_shortcuts_chat_last\" = \"Last chat\";\n\"lng_shortcuts_chat_self\" = \"Saved Messages\";\n\"lng_shortcuts_chat_pinned_n\" = \"Pinned chat #{index}\";\n\"lng_shortcuts_show_account_n\" = \"Account #{index}\";\n\"lng_shortcuts_show_all_chats\" = \"All Chats folder\";\n\"lng_shortcuts_show_folder_n\" = \"Folder #{index}\";\n\"lng_shortcuts_show_folder_last\" = \"Last folder\";\n\"lng_shortcuts_folder_next\" = \"Next folder\";\n\"lng_shortcuts_folder_previous\" = \"Previous folder\";\n\"lng_shortcuts_scheduled\" = \"Scheduled messages\";\n\"lng_shortcuts_archive\" = \"Archived chats\";\n\"lng_shortcuts_contacts\" = \"Contacts list\";\n\"lng_shortcuts_just_send\" = \"Just send\";\n\"lng_shortcuts_silent_send\" = \"Silent send\";\n\"lng_shortcuts_schedule\" = \"Schedule\";\n\"lng_shortcuts_read_chat\" = \"Mark chat as read\";\n\"lng_shortcuts_archive_chat\" = \"Archive chat\";\n\"lng_shortcuts_media_fullscreen\" = \"Toggle video fullscreen\";\n\"lng_shortcuts_show_chat_menu\" = \"Show chat menu\";\n\"lng_shortcuts_show_chat_preview\" = \"Show chat preview\";\n\"lng_shortcuts_record_voice_message\" = \"Record Voice Message\";\n\"lng_shortcuts_record_round_message\" = \"Record Round Message\";\n\"lng_shortcuts_admin_log\" = \"Group/Channel Recent Actions\";\n\n\"lng_attach\" = \"Add attachment\";\n\"lng_attach_replace\" = \"Replace attachment\";\n\"lng_emoji_sticker_gif\" = \"Choose emoji, sticker or gif\";\n\"lng_bot_keyboard_show\" = \"Show bot keyboard\";\n\"lng_bot_keyboard_hide\" = \"Hide bot keyboard\";\n\"lng_bot_commands_start\" = \"Bot commands start\";\n\"lng_broadcast_silent\" = \"Silent broadcast\";\n\n\"lng_settings_chat_reactions_title\" = \"Quick Reaction\";\n\"lng_settings_chat_reactions_subtitle\" = \"Choose your favorite reaction\";\n\"lng_settings_chat_message_reply_from\" = \"Bob Harris\";\n\"lng_settings_chat_message_reply\" = \"Good morning\";\n\"lng_settings_chat_message\" = \"Do you know what time it is?\";\n\n\"lng_settings_section_filters\" = \"Folders\";\n\n\"lng_settings_section_background\" = \"Chat wallpaper\";\n\"lng_settings_bg_from_gallery\" = \"Choose from gallery\";\n\"lng_settings_bg_from_file\" = \"Choose from file\";\n\"lng_settings_bg_remove\" = \"Remove wallpaper\";\n\"lng_settings_bg_theme_edit\" = \"Edit theme\";\n\"lng_settings_bg_theme_create\" = \"Create new theme\";\n\"lng_settings_bg_cloud_themes\" = \"Custom themes\";\n\"lng_settings_bg_show_all\" = \"Show all themes\";\n\"lng_settings_bg_tile\" = \"Tile background\";\n\"lng_settings_adaptive_wide\" = \"Adaptive layout for wide screens\";\n\n\"lng_settings_section_call_settings\" = \"Call Settings\";\n\"lng_settings_call_camera\" = \"Camera\";\n\"lng_settings_call_section_output\" = \"Speakers and headphones\";\n\"lng_settings_call_section_input\" = \"Microphone\";\n\"lng_settings_call_input_device\" = \"Input device\";\n\"lng_settings_call_output_device\" = \"Output device\";\n\"lng_settings_call_section_other\" = \"Other settings\";\n\"lng_settings_call_open_system_prefs\" = \"Open system sound preferences\";\n\"lng_settings_call_accept_calls\" = \"Accept calls on this device\";\n\"lng_settings_call_device_default\" = \"Default\";\n\n\"lng_settings_section_devices\" = \"Speakers and Camera\";\n\"lng_settings_devices_calls\" = \"Calls and video chats\";\n\"lng_settings_devices_calls_same\" = \"Use the same devices for calls\";\n\"lng_settings_devices_inactive\" = \"Unavailable\";\n\n\"lng_settings_language\" = \"Language\";\n\"lng_settings_default_scale\" = \"Default interface scale\";\n\"lng_settings_scale\" = \"Interface scale\";\n\"lng_settings_connection_type\" = \"Connection type\";\n\"lng_settings_downloading_update\" = \"Downloading update {progress}...\";\n\"lng_settings_privacy_title\" = \"Privacy\";\n\"lng_settings_last_seen\" = \"Last seen & online\";\n\"lng_settings_calls\" = \"Calls\";\n\"lng_settings_calls_peer_to_peer_title\" = \"Peer-to-peer\";\n\"lng_settings_calls_peer_to_peer_button\" = \"Use peer-to-peer with\";\n\"lng_settings_groups_invite\" = \"Groups & channels\";\n\"lng_settings_phone_number_privacy\" = \"Phone number\";\n\"lng_settings_forwards_privacy\" = \"Forwarded messages\";\n\"lng_settings_profile_photo_privacy\" = \"Profile photos\";\n\"lng_settings_messages_privacy\" = \"Messages\";\n\"lng_settings_voices_privacy\" = \"Voice messages\";\n\"lng_settings_bio_privacy\" = \"Bio\";\n\"lng_settings_gifts_privacy\" = \"Gifts\";\n\"lng_settings_birthday_privacy\" = \"Date of Birth\";\n\"lng_settings_saved_music_privacy\" = \"Saved Music\";\n\"lng_settings_privacy_premium\" = \"Only subscribers of {link} can restrict receiving voice messages.\";\n\"lng_settings_privacy_premium_link\" = \"Telegram Premium\";\n\"lng_settings_passcode_disable\" = \"Disable passcode\";\n\"lng_settings_passcode_disable_sure\" = \"Are you sure you want to disable passcode?\";\n\"lng_settings_use_winhello\" = \"Unlock with Windows Hello\";\n\"lng_settings_use_winhello_about\" = \"You need to enter your passcode once before unlocking Telegram with Windows Hello.\";\n\"lng_settings_use_touchid\" = \"Unlock with Touch ID\";\n\"lng_settings_use_touchid_about\" = \"You need to enter your passcode once before unlocking Telegram with Touch ID.\";\n\"lng_settings_use_applewatch\" = \"Unlock with Apple Watch\";\n\"lng_settings_use_applewatch_about\" = \"You need to enter your passcode once before unlocking Telegram with Apple Watch.\";\n\"lng_settings_use_systempwd\" = \"Unlock with System Password\";\n\"lng_settings_use_systempwd_about\" = \"You need to enter your passcode once before unlocking Telegram with System Password.\";\n\"lng_settings_password_disable\" = \"Disable cloud password\";\n\"lng_settings_password_abort\" = \"Abort two-step verification setup\";\n\"lng_settings_about_bio\" = \"Any details such as age, occupation or city.\\nExample: 23 y.o. designer from San Francisco\";\n\"lng_settings_name_label\" = \"Name\";\n\"lng_settings_username_label\" = \"Username\";\n\"lng_settings_phone_label\" = \"Phone number\";\n\"lng_settings_username_add\" = \"Add username\";\n\"lng_settings_username_about\" = \"Username lets people contact you on Telegram without needing your phone number.\";\n\"lng_settings_birthday_label\" = \"Date of Birth\";\n\"lng_settings_birthday_title\" = \"Set your Birthday\";\n\"lng_settings_birthday_add\" = \"Add\";\n\"lng_settings_birthday_about\" = \"Choose who can see your birthday in {link}.\";\n\"lng_settings_birthday_about_link\" = \"Settings\";\n\"lng_settings_birthday_contacts\" = \"Only your contacts can see your birthday. {link}\";\n\"lng_settings_birthday_contacts_link\" = \"Change >\";\n\"lng_settings_birthday_saved\" = \"Your date of birth was updated.\";\n\"lng_settings_birthday_suggested\" = \"Date of birth was suggested to {user}\";\n\"lng_settings_birthday_reset\" = \"Remove\";\n\"lng_settings_channel_label\" = \"Personal channel\";\n\"lng_settings_channel_add\" = \"Add\";\n\"lng_settings_channel_remove\" = \"Remove\";\n\"lng_settings_channel_menu_remove\" = \"Remove Personal Channel\";\n\"lng_settings_channel_no_yet\" = \"You don't have any public channels yet.\";\n\"lng_settings_channel_start\" = \"Start a Channel\";\n\"lng_settings_channel_saved\" = \"Your personal channel was updated.\";\n\"lng_settings_channel_removed\" = \"Personal channel removed.\";\n\"lng_settings_add_account_about\" = \"You can add up to four accounts with different phone numbers.\";\n\"lng_settings_peer_to_peer_about\" = \"Disabling peer-to-peer will relay all calls through Telegram servers to avoid revealing your IP address, but may decrease audio and video quality.\";\n\"lng_settings_advanced\" = \"Advanced\";\n\"lng_settings_stickers_emoji\" = \"Stickers and emoji\";\n\"lng_settings_messages\" = \"Messages\";\n\"lng_settings_themes\" = \"Themes\";\n\"lng_settings_theme_day\" = \"Day\";\n\"lng_settings_theme_classic\" = \"Classic\";\n\"lng_settings_theme_tinted\" = \"Tinted\";\n\"lng_settings_theme_night\" = \"Night\";\n\"lng_settings_theme_accent_title\" = \"Choose accent color\";\n\"lng_settings_theme_system_accent_color\" = \"System accent color\";\n\"lng_settings_data_storage\" = \"Data and storage\";\n\"lng_settings_information\" = \"Edit profile\";\n\"lng_settings_my_account\" = \"My Account\";\n\"lng_settings_security\" = \"Security\";\n\"lng_settings_passcode_title\" = \"Local passcode\";\n\"lng_settings_sessions_title\" = \"Active sessions\";\n\"lng_settings_sessions_about\" = \"Manage your sessions on all your devices.\";\n\"lng_settings_archive_title\" = \"Archive Settings\";\n\"lng_settings_new_unknown\" = \"New chats from unknown users\";\n\"lng_settings_auto_archive\" = \"Archive and Mute\";\n\"lng_settings_auto_archive_about\" = \"Automatically archive and mute new chats, groups and channels from non-contacts.\";\n\"lng_settings_unmuted_chats\" = \"Unmuted chats\";\n\"lng_settings_always_in_archive\" = \"Always keep archived\";\n\"lng_settings_unmuted_chats_about\" = \"Keep archived chats in the Archive even if they are unmuted and get a new message.\";\n\"lng_settings_chats_from_folders\" = \"Chats from folders\";\n\"lng_settings_chats_from_folders_about\" = \"Keep archived chats from folders in the Archive even if they are unmuted and get a new message.\";\n\"lng_settings_destroy_title\" = \"Delete my account\";\n\"lng_settings_version_info\" = \"Version and updates\";\n\"lng_settings_system_integration\" = \"System integration\";\n\"lng_settings_performance\" = \"Performance\";\n\"lng_settings_enable_hwaccel\" = \"Hardware accelerated video decoding\";\n\"lng_settings_enable_opengl\" = \"Enable OpenGL rendering for media\";\n\"lng_settings_angle_backend\" = \"ANGLE graphics backend\";\n\"lng_settings_angle_backend_auto\" = \"Auto\";\n\"lng_settings_angle_backend_d3d9\" = \"Direct3D 9\";\n\"lng_settings_angle_backend_d3d11\" = \"Direct3D 11\";\n\"lng_settings_angle_backend_d3d11on12\" = \"D3D11on12\";\n\"lng_settings_angle_backend_opengl\" = \"OpenGL\";\n\"lng_settings_angle_backend_disabled\" = \"Disabled\";\n\"lng_settings_top_peers_title\" = \"Frequent contacts\";\n\"lng_settings_top_peers_suggest\" = \"Suggest frequent contacts\";\n\"lng_settings_top_peers_about\" = \"Display people you message frequently at the top of the search section for quick access.\";\n\"lng_settings_sensitive_title\" = \"Sensitive content\";\n\"lng_settings_sensitive_disable_filtering\" = \"Show 18+ Content\";\n\"lng_settings_sensitive_about\" = \"Do not hide media that contains content suitable only for adults.\";\n\"lng_settings_security_bots\" = \"Bots and websites\";\n\"lng_settings_file_confirmations\" = \"File open confirmations\";\n\"lng_settings_edit_extensions\" = \"Extensions whitelist\";\n\"lng_settings_edit_extensions_about\" = \"Open files with the following extensions without additional confirmation.\";\n\"lng_settings_edit_ip_confirm\" = \"IP reveal warning\";\n\"lng_settings_edit_ip_confirm_about\" = \"Show confirmation when opening files that may reveal your IP address.\";\n\"lng_settings_clear_payment_info\" = \"Clear Payment and Shipping Info\";\n\"lng_settings_logged_in\" = \"Connected websites\";\n\"lng_settings_logged_in_title\" = \"Logged In with Telegram\";\n\"lng_settings_logged_in_description\" = \"You can log in on websites that support signing in with Telegram.\";\n\"lng_settings_disconnect_all\" = \"Disconnect all websites\";\n\"lng_settings_disconnect_title\" = \"Disconnect website\";\n\"lng_settings_disconnect_sure\" = \"Are you sure you want to disconnect {domain}?\";\n\"lng_settings_disconnect_block\" = \"Block {name}\";\n\"lng_settings_disconnect_all_title\" = \"Disconnect websites\";\n\"lng_settings_disconnect_all_sure\" = \"Are you sure you want to disconnect all websites where you logged in with Telegram?\";\n\"lng_settings_disconnect\" = \"Disconnect\";\n\"lng_settings_connected_title\" = \"Connected websites\";\n\n\"lng_settings_suggestion_phone_number_title\" = \"Is {phone} still your number?\";\n\"lng_settings_suggestion_phone_number_about\" = \"Keep your number up to date to ensure you can always log into Telegram. {link}\";\n\"lng_settings_suggestion_phone_number_about_link\" = \"https://telegram.org/faq#q-i-have-a-new-phone-number-what-do-i-do\";\n\"lng_settings_suggestion_phone_number_change\" = \"Please change your phone number in the official Telegram app on your phone as soon as possible. {emoji}\";\n\"lng_settings_suggestion_password_title\" = \"Your password\";\n\"lng_settings_suggestion_password_about\" = \"Your account is protected by 2-Step Veritifaction. Do you still remember your password?\";\n\"lng_settings_suggestion_password_yes\" = \"Yes, definitely\";\n\"lng_settings_suggestion_password_no\" = \"Not sure\";\n\"lng_settings_suggestion_password_step_input_title\" = \"Enter your password\";\n\"lng_settings_suggestion_password_step_input_about\" = \"Do you still remember your password?\";\n\"lng_settings_suggestion_password_step_finish_title\" = \"Perfect!\";\n\"lng_settings_suggestion_password_step_finish_about\" = \"You still remember your password.\";\n\n\"lng_settings_power_menu\" = \"Battery and Animations\";\n\"lng_settings_power_title\" = \"Power Usage\";\n\"lng_settings_power_subtitle\" = \"Power saving options\";\n\"lng_settings_power_stickers\" = \"Animated Stickers\";\n\"lng_settings_power_stickers_panel\" = \"Autoplay in panel\";\n\"lng_settings_power_stickers_chat\" = \"Autoplay in chat\";\n\"lng_settings_power_emoji\" = \"Animated Emoji\";\n\"lng_settings_power_emoji_panel\" = \"Autoplay in panel\";\n\"lng_settings_power_emoji_reactions\" = \"Autoplay in reactions menu\";\n\"lng_settings_power_emoji_chat\" = \"Autoplay in messages\";\n\"lng_settings_power_emoji_status\" = \"Autoplay in premium status\";\n\"lng_settings_power_chat\" = \"Animations in Chats\";\n\"lng_settings_power_chat_background\" = \"Wallpaper rotation\";\n\"lng_settings_power_chat_spoiler\" = \"Animated spoiler effect\";\n\"lng_settings_power_chat_effects\" = \"Effects in messages\";\n\"lng_settings_power_calls\" = \"Animations in Calls\";\n\"lng_settings_power_ui\" = \"Interface animations\";\n\"lng_settings_power_auto\" = \"Save Power on Low Battery\";\n\"lng_settings_power_auto_about\" = \"Automatically disable all animations when your laptop is in a battery saving mode.\";\n\"lng_settings_power_turn_off\" = \"Please turn off Save Power on Low Battery to change these settings.\";\n\n\"lng_settings_cloud_password_on\" = \"On\";\n\"lng_settings_cloud_password_off\" = \"Off\";\n\"lng_settings_cloud_password_start_title\" = \"Two-Step Verification\";\n\"lng_settings_cloud_password_password_title\" = \"Password\";\n\"lng_settings_cloud_password_hint_title\" = \"Password Hint\";\n\"lng_settings_cloud_password_email_title\" = \"Recovery Email\";\n\"lng_settings_cloud_password_start_about\" = \"Protect your Telegram account with an additional password.\";\n\"lng_settings_cloud_password_hint_about\" = \"You can create a hint for your password.\";\n\"lng_settings_cloud_password_email_about\" = \"Please enter your new recovery email. It is the only way to recover a forgotten password.\";\n\"lng_settings_cloud_password_password_subtitle\" = \"Create Password\";\n\"lng_settings_cloud_password_check_subtitle\" = \"Your Password\";\n\"lng_settings_cloud_password_hint_subtitle\" = \"Add Password Hint\";\n\"lng_settings_cloud_password_email_subtitle\" = \"Add Recovery Email\";\n\"lng_settings_cloud_password_email_recovery_subtitle\" = \"Password Recovery\";\n\"lng_settings_cloud_password_manage_about1\" = \"You have Two-Step Verification enabled, so your account is protected with an additional password.\";\n\"lng_settings_cloud_password_manage_about2\" = \"This email is the only way to recover a forgotten password.\";\n\"lng_settings_cloud_password_manage_disable_sure\" = \"Are you sure you want to disable your password?\";\n\"lng_settings_cloud_password_manage_email_new\" = \"Set Recovery Email\";\n\"lng_settings_cloud_password_manage_email_change\" = \"Change Recovery Email\";\n\"lng_settings_cloud_password_manage_password_change\" = \"Change Password\";\n\"lng_settings_cloud_password_skip_hint\" = \"Skip setting hint\";\n\"lng_settings_cloud_password_save\" = \"Save and Finish\";\n\"lng_settings_cloud_password_email_confirm\" = \"Confirm and Finish\";\n\"lng_settings_cloud_password_reset_in\" = \"You can reset your password in {duration}.\";\n\n\"lng_settings_cloud_login_email_section_title\" = \"Login Email\";\n\"lng_settings_cloud_login_email_box_about\" = \"This email address will be used every time you log in to your Telegram account from a new device.\";\n\"lng_settings_cloud_login_email_box_ok\" = \"Change email\";\n\"lng_settings_cloud_login_email_title\" = \"Enter New Email\";\n\"lng_settings_cloud_login_email_placeholder\" = \"Enter Login Email\";\n\"lng_settings_cloud_login_email_about\" = \"You will receive Telegram login codes via email and not SMS. Please enter an email address to which you have access.\";\n\"lng_settings_cloud_login_email_confirm\" = \"Confirm\";\n\"lng_settings_cloud_login_email_code_title\" = \"Check Your New Email\";\n\"lng_settings_cloud_login_email_code_about\" = \"Please enter the code we have sent to your new email {email}\";\n\"lng_settings_cloud_login_email_success\" = \"Your email has been changed.\";\n\"lng_settings_cloud_login_email_set_success\" = \"Your login email has been set successfully.\";\n\"lng_settings_cloud_login_email_busy\" = \"Please set up login email in another window.\";\n\"lng_settings_error_email_not_alowed\" = \"Sorry, this email is not allowed\";\n\n\"lng_settings_ttl_title\" = \"Auto-Delete Messages\";\n\"lng_settings_ttl_about\" = \"Automatically delete messages for everyone after a period of time in all new chats you start.\";\n\"lng_settings_ttl_after\" = \"After {after_duration}\";\n\"lng_settings_ttl_after_hours#one\" = \"{count} hour\";\n\"lng_settings_ttl_after_hours#other\" = \"{count} hours\";\n\"lng_settings_ttl_after_days#one\" = \"{count} day\";\n\"lng_settings_ttl_after_days#other\" = \"{count} days\";\n\"lng_settings_ttl_after_weeks#one\" = \"{count} week\";\n\"lng_settings_ttl_after_weeks#other\" = \"{count} weeks\";\n\"lng_settings_ttl_after_months#one\" = \"{count} month\";\n\"lng_settings_ttl_after_months#other\" = \"{count} months\";\n\"lng_settings_ttl_after_years#one\" = \"{count} year\";\n\"lng_settings_ttl_after_years#other\" = \"{count} years\";\n\"lng_settings_ttl_after_off\" = \"Off\";\n\"lng_settings_ttl_after_custom\" = \"Set Custom Time\";\n\"lng_settings_ttl_after_about\" = \"If enabled, all new messages in chats you start will be automatically deleted for everyone at some point after they are sent. You can also {link}.\";\n\"lng_settings_ttl_after_about_link\" = \"apply this setting for your existing chats\";\n\"lng_settings_ttl_after_subtitle\" = \"Self-destruct timer\";\n\"lng_settings_ttl_after_sure\" = \"Are you sure you want all messages in your new private chats and in new groups you create to be automatically deleted for everyone {after_duration} after they are sent?\";\n\"lng_settings_ttl_after_toast\" = \"Messages in all new chats you start will be automatically deleted after {after_duration}.\";\n\n\"lng_settings_ttl_select_chats_sorry\" = \"Sorry, you can't set a self-destruct timer in this chat.\";\n\"lng_settings_ttl_select_chats_status\" = \"auto-delete after {after_duration}\";\n\"lng_settings_ttl_select_chats_status_disabled\" = \"auto-delete disabled\";\n\"lng_settings_ttl_select_chats_toast#one\" = \"Self-destruct timer for {duration} has been enabled in {count} selected chat.\";\n\"lng_settings_ttl_select_chats_toast#other\" = \"Self-destruct timer for {duration} has been enabled in {count} selected chats.\";\n\"lng_settings_ttl_select_chats_disabled_toast#one\" = \"Auto-delete timer has been disabled in {count} selected chat.\";\n\"lng_settings_ttl_select_chats_disabled_toast#other\" = \"Auto-delete timer has been disabled in {count} selected chats.\";\n\n\"lng_clear_payment_info_title\" = \"Clear payment info\";\n\"lng_clear_payment_info_sure\" = \"Are you sure you want to clear your payment and shipping info?\";\n\"lng_clear_payment_info_shipping\" = \"Shipping info\";\n\"lng_clear_payment_info_payment\" = \"Payment info\";\n\"lng_clear_payment_info_clear\" = \"Clear\";\n\"lng_clear_payment_info_confirm\" = \"Delete your shipping info and instruct all payment providers to remove your saved credit cards? Note that Telegram never stores your credit card data.\";\n\n\"lng_settings_theme_settings\" = \"Theme settings\";\n\"lng_settings_theme_name_color\" = \"Your name color\";\n\"lng_settings_auto_night_mode\" = \"Auto-night mode\";\n\"lng_settings_auto_night_mode_off\" = \"Off\";\n\"lng_settings_auto_night_mode_on\" = \"System\";\n\"lng_settings_auto_night_warning\" = \"You have enabled auto-night mode. If you want to change the dark mode settings, you'll need to disable it first.\";\n\"lng_settings_auto_night_disable\" = \"Disable\";\n\"lng_settings_font_family\" = \"Font family\";\n\n\"lng_settings_color_title\" = \"Color preview\";\n\"lng_settings_color_tab_profile\" = \"Profile\";\n\"lng_settings_color_tab_name\" = \"Name\";\n\"lng_settings_color_reply\" = \"Reply to your message\";\n\"lng_settings_color_reply_channel\" = \"Reply to your channel message\";\n\"lng_settings_color_text\" = \"Your name and replies to your messages will be shown in the selected color.\";\n\"lng_settings_color_text_channel\" = \"The name of the channel and replies to its messages will be shown in the selected color.\";\n\"lng_settings_color_link_name\" = \"Telegram\";\n\"lng_settings_color_link_title\" = \"Link Preview\";\n\"lng_settings_color_link_description\" = \"Previews of links you send will also use this color.\";\n\"lng_settings_color_about\" = \"This color will be used for your name, the links you send, and replies to your messages.\";\n\"lng_settings_color_about_channel\" = \"This color will be used for your channel's name, the links it sends, and replies to its posts.\";\n\"lng_settings_color_emoji\" = \"Add icons to replies\";\n\"lng_settings_color_emoji_remove\" = \"Remove icon\";\n\"lng_settings_color_emoji_off\" = \"Off\";\n\"lng_settings_color_emoji_about\" = \"Make replies to your messages stand out by adding custom patterns to them.\";\n\"lng_settings_color_emoji_about_channel\" = \"Select an icon to create a custom pattern for replies to your messages.\";\n\"lng_settings_color_changed\" = \"Your name color has been updated!\";\n\"lng_settings_color_changed_channel\" = \"Your channel color has been updated!\";\n\"lng_settings_color_changed_profile\" = \"Your profile style has been updated!\";\n\"lng_settings_color_changed_profile_channel\" = \"Your channel profile style has been updated!\";\n\"lng_settings_color_apply\" = \"Apply Style\";\n\"lng_settings_color_wear\" = \"Wear Collectible\";\n\"lng_settings_color_profile_emoji\" = \"Add icons to Profile\";\n\"lng_settings_color_profile_emoji_channel\" = \"Profile Logo\";\n\"lng_settings_color_reset\" = \"Reset Profile Color\";\n\"lng_settings_color_profile_about\" = \"You can change the color of your name and customize replies to you. {link}\";\n\"lng_settings_color_profile_about_link\" = \"Change {emoji}\";\n\"lng_settings_color_choose_channel\" = \"Choose a color and a logo for your channel's profile\";\n\"lng_settings_color_choose_group\" = \"Choose a color and a logo for the group's profile\";\n\"lng_settings_color_group_boost_footer#one\" = \"The group has **{count}** boost. {link}\";\n\"lng_settings_color_group_boost_footer#other\" = \"The group has **{count}** boosts. {link}\";\n\"lng_settings_color_group_boost_footer_link\" = \"What are boosts?\";\n\n\"lng_suggest_hide_new_title\" = \"Hide new chats?\";\n\"lng_suggest_hide_new_about\" = \"You are receiving lots of new chats from users who are not in your Contact List.\\n\\nDo you want to have such chats **automatically muted** and **archived**?\";\n\"lng_suggest_hide_new_to_settings\" = \"Go to Settings\";\n\n\"lng_settings_spellchecker\" = \"Spell checker\";\n\"lng_settings_system_spellchecker\" = \"Use system spell checker\";\n\"lng_settings_custom_spellchecker\" = \"Use spell checker\";\n\"lng_settings_auto_download_dictionaries\" = \"Download dictionaries automatically\";\n\"lng_settings_manage_dictionaries\" = \"Manage dictionaries\";\n\"lng_settings_manage_enabled_dictionary\" = \"Dictionary is enabled\";\n\"lng_settings_manage_remove_dictionary\" = \"Remove Dictionary\";\n\n\"lng_settings_gift_premium\" = \"Premium Gifting\";\n\"lng_settings_gift_premium_users_confirm\" = \"Proceed\";\n\"lng_settings_gift_premium_users_error#one\" = \"You can select maximum {count} user.\";\n\"lng_settings_gift_premium_users_error#other\" = \"You can select maximum {count} users.\";\n\"lng_settings_gift_premium_choose\" = \"Please choose at least one recipient.\";\n\n\"lng_backgrounds_header\" = \"Choose a Wallpaper\";\n\"lng_theme_sure_keep\" = \"Keep this theme?\";\n\"lng_theme_reverting#one\" = \"Reverting to the old theme in {count} second.\";\n\"lng_theme_reverting#other\" = \"Reverting to the old theme in {count} seconds.\";\n\"lng_theme_keep_changes\" = \"Keep changes\";\n\"lng_theme_revert\" = \"Revert\";\n\"lng_theme_no_desktop\" = \"Sorry, this theme doesn't include a version for Telegram Desktop.\";\n\"lng_theme_share\" = \"Share\";\n\"lng_theme_edit\" = \"Edit\";\n\"lng_theme_delete\" = \"Delete\";\n\"lng_theme_delete_sure\" = \"Are you sure you want to delete this theme?\";\n\"lng_background_header\" = \"Wallpaper preview\";\n\"lng_background_text1\" = \"Ah, you kids today with techno music! You should enjoy the classics, like Hasselhoff!\";\n\"lng_background_text2\" = \"I can't even take you seriously right now.\";\n\"lng_background_bad_link\" = \"Invalid wallpaper link.\";\n\"lng_background_share\" = \"Share\";\n\"lng_background_link_copied\" = \"Link copied to clipboard.\";\n\"lng_background_blur\" = \"Blurred\";\n\"lng_background_sure_delete\" = \"Are you sure you want to delete this wallpaper?\";\n\"lng_background_other_info\" = \"{user} will be able to apply this wallpaper\";\n\"lng_background_other_channel\" = \"All subscribers will see this wallpaper\";\n\"lng_background_other_group\" = \"All members will see this wallpaper\";\n\"lng_background_apply1\" = \"Apply the wallpaper in this chat.\";\n\"lng_background_apply2\" = \"Looks good.\";\n\"lng_background_apply_button\" = \"Apply For This Chat\";\n\"lng_background_dimming\" = \"Wallpaper dimming\";\n\"lng_background_sure_reset_default\" = \"Are you sure you want to reset the wallpaper?\";\n\"lng_background_reset_default\" = \"Reset\";\n\"lng_background_apply_me\" = \"Apply for me\";\n\"lng_background_apply_both\" = \"Apply for me and {user}\";\n\"lng_background_apply_channel\" = \"Apply For Channel\";\n\"lng_background_apply_group\" = \"Apply For Group\";\n\n\"lng_download_path_ask\" = \"Ask download path for each file\";\n\"lng_download_path\" = \"Download path\";\n\"lng_download_path_temp\" = \"Temp folder\";\n\"lng_download_path_default\" = \"Default folder\";\n\"lng_download_path_header\" = \"Choose download path\";\n\"lng_download_path_default_radio\" = \"Telegram folder in system «Downloads»\";\n\"lng_download_path_temp_radio\" = \"Temp folder, cleared on logout or uninstall\";\n\"lng_download_path_dir_radio\" = \"Custom folder, cleared only manually\";\n\"lng_download_path_choose\" = \"Choose download path\";\n\"lng_download_path_failed\" = \"File download could not be started.\\n\\nThis might be because your selected download location is invalid. Try changing it in Settings > Advanced > Download Path.\";\n\"lng_download_finish_failed\" = \"File download could not be completed.\\n\\nWould you like to try again?\";\n\n\"lng_settings_section_privacy\" = \"Privacy and Security\";\n\n\"lng_local_storage_title\" = \"Local storage\";\n\"lng_local_storage_empty\" = \"No cached files\";\n\"lng_local_storage_image#one\" = \"{count} image\";\n\"lng_local_storage_image#other\" = \"{count} images\";\n\"lng_local_storage_sticker#one\" = \"{count} sticker\";\n\"lng_local_storage_sticker#other\" = \"{count} stickers\";\n\"lng_local_storage_voice#one\" = \"{count} voice message\";\n\"lng_local_storage_voice#other\" = \"{count} voice messages\";\n\"lng_local_storage_round#one\" = \"{count} video message\";\n\"lng_local_storage_round#other\" = \"{count} video messages\";\n\"lng_local_storage_animation#one\" = \"{count} GIF animation\";\n\"lng_local_storage_animation#other\" = \"{count} GIF animations\";\n\"lng_local_storage_media\" = \"Media cache\";\n\"lng_local_storage_size_limit\" = \"Total size limit: {size}\";\n\"lng_local_storage_media_limit\" = \"Media cache limit: {size}\";\n\"lng_local_storage_time_limit\" = \"Clear files older than: {limit}\";\n\"lng_local_storage_limit_never\" = \"Never\";\n\"lng_local_storage_summary\" = \"Summary\";\n\"lng_local_storage_clear_some\" = \"Clear\";\n\"lng_local_storage_clear\" = \"Clear all\";\n\"lng_local_storage_clearing\" = \"Clearing...\";\n\n\"lng_passcode_remove_button\" = \"Remove\";\n\n\"lng_passcode_change\" = \"Change passcode\";\n\"lng_passcode_create\" = \"Local passcode\";\n\"lng_passcode_remove\" = \"Remove local passcode\";\n\"lng_passcode_autolock\" = \"Auto-Lock\";\n\"lng_passcode_autolock_away\" = \"Auto-Lock if away for...\";\n\"lng_passcode_autolock_inactive\" = \"Auto-Lock if inactive for...\";\n\"lng_passcode_autolock_hours_minutes\" = \"{hours_count}h {minutes_count}m\";\n\"lng_passcode_enter_old\" = \"Enter current passcode\";\n\"lng_passcode_enter_first\" = \"Enter a passcode\";\n\"lng_passcode_enter_new\" = \"Enter new passcode\";\n\"lng_passcode_confirm_new\" = \"Re-enter new passcode\";\n\"lng_passcode_about\" = \"When a local passcode is set, a lock icon appears at the top of your chat list. Click it to lock Telegram Desktop.\\n\\nNote: if you forget your local passcode, you'll need to log out of Telegram Desktop and log in again.\";\n\"lng_passcode_about1\" = \"When a local passcode is set, a lock icon appears at the top of your chat list.\";\n\"lng_passcode_about2\" = \"Click it to lock Telegram Desktop.\";\n\"lng_passcode_about3\" = \"Note: if you forget your passcode, you'll need to log out of Telegram Desktop and log in again.\";\n\"lng_passcode_differ\" = \"Passcodes are different\";\n\"lng_passcode_wrong\" = \"Wrong passcode\";\n\"lng_passcode_is_same\" = \"Passcode was not changed\";\n\"lng_passcode_enter\" = \"Enter your local passcode\";\n\"lng_passcode_ph\" = \"Your passcode\";\n\"lng_passcode_submit\" = \"Submit\";\n\"lng_passcode_logout\" = \"Log out\";\n\"lng_passcode_winhello\" = \"You need to enter your passcode\\nbefore you can use Windows Hello.\";\n\"lng_passcode_touchid\" = \"You need to enter your passcode\\nbefore you can use Touch ID.\";\n\"lng_passcode_applewatch\" = \"You need to enter your passcode\\nbefore you can use Watch to unlock.\";\n\"lng_passcode_systempwd\" = \"You need to enter your passcode\\nbefore you can use system password.\";\n\"lng_passcode_winhello_unlock\" = \"Telegram wants to unlock with Windows Hello.\";\n\"lng_passcode_touchid_unlock\" = \"unlock\";\n\"lng_passcode_create_button\" = \"Save Passcode\";\n\"lng_passcode_check_button\" = \"Submit\";\n\"lng_passcode_change_button\" = \"Save Passcode\";\n\"lng_passcode_create_title\" = \"Create Local Passcode\";\n\"lng_passcode_check_title\" = \"Enter Passcode\";\n\"lng_passcode_change_title\" = \"Enter Passcode\";\n\n\"lng_cloud_password_waiting_code\" = \"Confirmation code sent to {email}...\";\n\"lng_cloud_password_confirm\" = \"Confirm recovery email\";\n\"lng_cloud_password_change\" = \"Change cloud password\";\n\"lng_cloud_password_create\" = \"Telegram password\";\n\"lng_cloud_password_remove\" = \"Remove cloud password\";\n\"lng_cloud_password_reset_ready\" = \"Reset password\";\n\"lng_cloud_password_enter_old\" = \"Enter current password\";\n\"lng_cloud_password_enter_first\" = \"Enter a password\";\n\"lng_cloud_password_enter_new\" = \"Enter new password\";\n\"lng_cloud_password_confirm_new\" = \"Re-enter new password\";\n\"lng_cloud_password_hint\" = \"Add a password hint\";\n\"lng_cloud_password_change_hint\" = \"Enter new password hint\";\n\"lng_cloud_password_bad\" = \"Password and hint cannot be the same.\";\n\"lng_cloud_password_email\" = \"Enter recovery email\";\n\"lng_cloud_password_bad_email\" = \"Invalid email\";\n\"lng_cloud_password_about\" = \"This password will be asked when you log in on a new device in addition to the code you get via SMS.\";\n\"lng_cloud_password_about_recover\" = \"Warning! Are you sure you don't want to\\nadd a password recovery email?\\n\\nIf you forget your password, you will\\nlose access to your Telegram account.\";\n\"lng_cloud_password_skip_email\" = \"Skip email\";\n\"lng_cloud_password_was_set\" = \"Two-step verification enabled.\";\n\"lng_cloud_password_updated\" = \"Your cloud password was updated.\";\n\"lng_cloud_password_removed\" = \"Two-step verification was disabled.\";\n\"lng_cloud_password_differ\" = \"Passwords do not match\";\n\"lng_cloud_password_wrong\" = \"Wrong cloud password\";\n\"lng_cloud_password_is_same\" = \"Password was not changed\";\n\"lng_cloud_password_passport_losing\" = \"Warning! All data saved in your Telegram Passport will be lost.\";\n\"lng_cloud_password_resend\" = \"Resend code\";\n\"lng_cloud_password_resent\" = \"Code was resent.\";\n\"lng_cloud_password_reset_no_email\" = \"Since you didn't provide a recovery email when setting up your password, your remaining options are either to remember your password or wait 7 days until your password is reset.\";\n\"lng_cloud_password_reset_with_email\" = \"If you don't have access to your recovery email, your remaining options are either to remember your password or wait 7 days until your password resets.\";\n\"lng_cloud_password_reset_ok\" = \"Reset\";\n\"lng_cloud_password_reset_cancel_title\" = \"Cancel reset\";\n\"lng_cloud_password_reset_cancel_sure\" = \"Cancel the password reset process? If you request a new reset later, it will take another 7 days.\";\n\"lng_cloud_password_reset_later\" = \"You recently requested a password reset that was canceled. Please wait {duration} before making a new request.\";\n\"lng_cloud_password_expired\" = \"Please re-enter your password.\";\n\n\"lng_connection_auto_connecting\" = \"Default (connecting...)\";\n\"lng_connection_auto\" = \"Default ({transport} used)\";\n\"lng_connection_proxy_connecting\" = \"Connecting through proxy...\";\n\"lng_connection_proxy\" = \"{transport} with proxy\";\n\"lng_connection_try_ipv6\" = \"Try connecting through IPv6\";\n\"lng_connection_host_ph\" = \"Hostname\";\n\"lng_connection_port_ph\" = \"Port\";\n\"lng_connection_user_ph\" = \"Username\";\n\"lng_connection_password_ph\" = \"Password\";\n\"lng_connection_proxy_secret_ph\" = \"Secret\";\n\"lng_connection_save\" = \"Save\";\n\n\"lng_proxy_settings\" = \"Proxy settings\";\n\"lng_proxy_disable\" = \"Disable proxy\";\n\"lng_proxy_use_system_settings\" = \"Use system proxy settings\";\n\"lng_proxy_use_custom\" = \"Use custom proxy\";\n\"lng_proxy_use_for_calls\" = \"Use proxy for calls\";\n\"lng_proxy_about\" = \"Proxy servers may be helpful in accessing Telegram if there is no connection in a specific region.\";\n\"lng_proxy_add\" = \"Add proxy\";\n\"lng_proxy_share\" = \"Share\";\n\"lng_proxy_online\" = \"online\";\n\"lng_proxy_checking\" = \"checking…\";\n\"lng_proxy_connecting\" = \"connecting…\";\n\"lng_proxy_available\" = \"available (ping: {ping} ms)\";\n\"lng_proxy_unavailable\" = \"not available\";\n\"lng_proxy_edit\" = \"Edit proxy\";\n\"lng_proxy_menu_edit\" = \"Edit\";\n\"lng_proxy_menu_delete\" = \"Delete\";\n\"lng_proxy_menu_restore\" = \"Restore\";\n\"lng_proxy_edit_share\" = \"Share\";\n\"lng_proxy_edit_share_qr_box_title\" = \"Share proxy with QR code\";\n\"lng_proxy_edit_share_list_button\" = \"Share Proxy List\";\n\"lng_proxy_edit_share_list_toast\" = \"Proxy List copied to clipboard.\";\n\"lng_proxy_address_label\" = \"Socket address\";\n\"lng_proxy_credentials_optional\" = \"Credentials (optional)\";\n\"lng_proxy_credentials\" = \"Credentials\";\n\"lng_proxy_description\" = \"Your saved proxy list will be here.\";\n\"lng_proxy_sponsor\" = \"Proxy sponsor\";\n\"lng_proxy_sponsor_about\" = \"This channel is shown by your proxy server.\\nTo remove this channel from your chat list,\\ndisable the proxy in Telegram Settings.\";\n\"lng_proxy_sponsor_warning\" = \"This proxy may display a sponsored channel in your chat list. This doesn't reveal any of your Telegram traffic.\";\n\"lng_proxy_add_from_clipboard\" = \"Add proxy from clipboard\";\n\"lng_proxy_add_from_clipboard_good_toast\" = \"Proxy was added from clipboard.\";\n\"lng_proxy_add_from_clipboard_failed_toast\" = \"This is not a proxy link.\";\n\"lng_proxy_add_from_clipboard_existing_toast\" = \"This proxy is already in the list.\";\n\"lng_badge_psa_default\" = \"PSA\";\n\"lng_about_psa_default\" = \"This message provides you with a public service announcement. To remove it from your chat list, right click it and select **Hide**.\";\n\"lng_tooltip_psa_default\" = \"This message provides you with a public service announcement.\";\n\n\"lng_settings_blocked_users\" = \"Blocked users\";\n\"lng_settings_no_blocked_users\" = \"None\";\n\"lng_settings_show_sessions\" = \"Active sessions\";\n\"lng_settings_export_data\" = \"Export Telegram data\";\n\"lng_settings_destroy_if\" = \"If away for...\";\n\n\"lng_settings_terminate_title\" = \"Terminate old sessions\";\n\"lng_settings_terminate_if\" = \"If inactive for...\";\n\"lng_settings_reset_sure\" = \"Are you sure you want to terminate\\nall other sessions?\";\n\"lng_settings_reset_one_sure\" = \"Do you want to terminate this session?\";\n\"lng_settings_reset_button\" = \"Terminate\";\n\"lng_settings_rename_device\" = \"Rename\";\n\"lng_settings_device_name\" = \"Device name\";\n\"lng_settings_rename_device_title\" = \"Rename current device\";\n\"lng_settings_manage_local_storage\" = \"Manage local storage\";\n\"lng_settings_ask_question\" = \"Ask a Question\";\n\"lng_settings_ask_sure\" = \"Please note that Telegram Support is done by volunteers. We try to respond as quickly as possible, but it may take a while.\\n\\nPlease take a look at the Telegram FAQ: it has important troubleshooting tips and answers to most questions.\";\n\"lng_settings_faq_button\" = \"Go to FAQ\";\n\"lng_settings_ask_ok\" = \"Ask a Volunteer\";\n\"lng_settings_faq\" = \"Telegram FAQ\";\n\"lng_settings_faq_subtitle\" = \"FAQ\";\n\"lng_settings_faq_link\" = \"https://telegram.org/faq#general-questions\";\n\"lng_settings_features\" = \"Telegram Features\";\n\"lng_settings_credits\" = \"My Stars\";\n\"lng_settings_currency\" = \"My TON\";\n\"lng_settings_logout\" = \"Log out\";\n\"lng_sure_logout\" = \"Are you sure you want to log out?\";\n\n\"lng_settings_need_restart\" = \"You need to restart for applying some of the new settings. Restart now?\";\n\"lng_settings_restart_now\" = \"Restart\";\n\"lng_settings_restart_later\" = \"Later\";\n\n\"lng_settings_passkeys_title\" = \"Passkeys\";\n\"lng_settings_passkeys_about\" = \"Manage your passkey, stored safely in the cloud service you choose.\";\n\"lng_settings_passkeys_button\" = \"Add Passkey\";\n\"lng_settings_passkeys_button_about\" = \"Your passkey is stored securely in your password manager. {link}\";\n\"lng_settings_passkeys_delete_sure_title\" = \"Delete Passkey\";\n\"lng_settings_passkeys_delete_sure_about\" = \"Once deleted, this passkey can't be used to log in.\";\n\"lng_settings_passkeys_delete_sure_about2\" = \"Don't forget to remove it from your password manager too.\";\n\"lng_settings_passkeys_none_title\" = \"Protect your account\";\n\"lng_settings_passkeys_none_about\" = \"Log in safely and keep your account secure.\";\n\"lng_settings_passkeys_none_info1_title\" = \"Create a Passkey\";\n\"lng_settings_passkeys_none_info1_about\" = \"Make a passkey to sign in easily and safely.\";\n\"lng_settings_passkeys_none_info2_title\" = \"Log in with face recognition\";\n\"lng_settings_passkeys_none_info2_about\" = \"Use your face, fingerprint, or screen lock to sign in.\";\n\"lng_settings_passkeys_none_info3_title\" = \"Store Passkey securely\";\n\"lng_settings_passkeys_none_info3_about\" = \"Your passkey is stored safely in the cloud service you choose.\";\n\"lng_settings_passkeys_none_button\" = \"Create Passkey\";\n\"lng_settings_passkeys_none_button_unsupported\" = \"Unsupported\";\n\"lng_settings_passkeys_created\" = \"Created {date}\";\n\"lng_settings_passkeys_last_used\" = \"Last used {date}\";\n\"lng_settings_passkeys_unsigned_error\" = \"Passkeys are not available in unsigned builds. Please use an official signed version of Telegram Desktop.\";\n\"lng_settings_passkey_unknown\" = \"Passkey\";\n\n\"lng_settings_quick_dialog_action_title\" = \"Chat list quick action\";\n\"lng_settings_quick_dialog_action_about\" = \"Choose the action you want to perform when you middle-click or swipe left in the chat list.\";\n\"lng_settings_quick_dialog_action_both\" = \"Swipe left and Middle-click\";\n\"lng_settings_quick_dialog_action_swipe\" = \"Swipe left\";\n\"lng_settings_quick_dialog_action_mute\" = \"Mute\";\n\"lng_settings_quick_dialog_action_unmute\" = \"Unmute\";\n\"lng_settings_quick_dialog_action_pin\" = \"Pin\";\n\"lng_settings_quick_dialog_action_unpin\" = \"Unpin\";\n\"lng_settings_quick_dialog_action_read\" = \"Read\";\n\"lng_settings_quick_dialog_action_unread\" = \"Unread\";\n\"lng_settings_quick_dialog_action_archive\" = \"Archive\";\n\"lng_settings_quick_dialog_action_unarchive\" = \"Unarchive\";\n\"lng_settings_quick_dialog_action_delete\" = \"Delete\";\n\"lng_settings_quick_dialog_action_disabled\" = \"Change folder\";\n\n\"lng_quick_dialog_action_toast_mute_success\" = \"Notifications for this chat have been muted.\";\n\"lng_quick_dialog_action_toast_unmute_success\" = \"Notifications enabled for this chat.\";\n\"lng_quick_dialog_action_toast_pin_success\" = \"The chat has been pinned.\";\n\"lng_quick_dialog_action_toast_unpin_success\" = \"The chat has been unpinned.\";\n\"lng_quick_dialog_action_toast_read_success\" = \"The chat has been marked as read.\";\n\"lng_quick_dialog_action_toast_unread_success\" = \"The chat has been marked as unread.\";\n\"lng_quick_dialog_action_toast_archive_success\" = \"The chat has been archived.\";\n\"lng_quick_dialog_action_toast_unarchive_success\" = \"The chat has been unarchived.\";\n\n\"lng_archive_hint_title\" = \"This is your Archive\";\n\"lng_archive_hint_about\" = \"Archived chats will remain in the Archive when you receive a new message. {link}\";\n\"lng_archive_hint_about_unmuted\" = \"When you receive a new message, muted chats will remain in the Archive, while unmuted chats will be moved to Chats. {link}\";\n\"lng_archive_hint_about_link\" = \"Tap to change {emoji}\";\n\"lng_archive_hint_section_1\" = \"Archived Chats\";\n\"lng_archive_hint_section_1_info\" = \"Move any chat into your Archive and back by swiping on it.\";\n\"lng_archive_hint_section_2\" = \"Hiding Archive\";\n\"lng_archive_hint_section_2_info\" = \"Hide the Archive from your Main screen by swiping on it.\";\n\"lng_archive_hint_section_3\" = \"Stories\";\n\"lng_archive_hint_section_3_info\" = \"Archive Stories from your contacts separately from chats with them.\";\n\"lng_archive_hint_button\" = \"Got it\";\n\n\"lng_settings_generic_subscribe\" = \"Subscribe to {link} to use this setting.\";\n\"lng_settings_generic_subscribe_link\" = \"Telegram Premium\";\n\n\"lng_sessions_header\" = \"This device\";\n\"lng_sessions_other_header\" = \"Active Devices\";\n\"lng_sessions_other_desc\" = \"You can log in to Telegram from other mobile, tablet and desktop devices, using the same phone number. All your data will be instantly synchronized.\";\n\"lng_sessions_terminate_all\" = \"Terminate all other sessions\";\n\"lng_sessions_terminate_all_about\" = \"Logs out all devices except for this one.\";\n\"lng_sessions_incomplete\" = \"Incomplete login attempts\";\n\"lng_sessions_incomplete_about\" = \"The devices above have no access to your messages. The code was entered correctly, but no correct password was given.\";\n\"lng_sessions_info\" = \"Info\";\n\"lng_sessions_terminate\" = \"Terminate Session\";\n\"lng_sessions_application\" = \"Application\";\n\"lng_sessions_system\" = \"System version\";\n\"lng_sessions_browser\" = \"Browser\";\n\"lng_sessions_ip\" = \"IP address\";\n\"lng_sessions_location\" = \"Location\";\n\"lng_sessions_location_about\" = \"This location estimate is based on the IP address and may not always be accurate.\";\n\"lng_sessions_about_apps\" = \"The official Telegram app is available for Android, iPhone, iPad, Windows, macOS and Linux.\";\n\n\"lng_blocked_list_title\" = \"Blocked users\";\n\"lng_blocked_list_unknown_phone\" = \"unknown phone number\";\n\"lng_blocked_list_unblock\" = \"Unblock\";\n\"lng_blocked_list_add\" = \"Block user\";\n\"lng_blocked_list_add_title\" = \"Select user to block\";\n\"lng_blocked_list_already_blocked\" = \"already blocked\";\n\"lng_blocked_list_about\" = \"Blocked users can't send you messages or add you to groups. They will not see your profile photos, stories, online and last seen status.\";\n\"lng_blocked_list_not_found\" = \"No users found.\";\n\"lng_blocked_list_confirm_title\" = \"Block {name}\";\n\"lng_blocked_list_confirm_text\" = \"Do you want to block {name} from messaging and calling you on Telegram?\";\n\"lng_blocked_list_confirm_clear\" = \"Delete this chat\";\n\"lng_blocked_list_confirm_ok\" = \"Block\";\n\"lng_blocked_list_empty_title\" = \"No blocked users\";\n\"lng_blocked_list_empty_description\" = \"You haven't blocked anyone yet.\";\n\"lng_blocked_list_subtitle#one\" = \"{count} blocked user\";\n\"lng_blocked_list_subtitle#other\" = \"{count} blocked users\";\n\n\"lng_edit_privacy_everyone\" = \"Everybody\";\n\"lng_edit_privacy_no_miniapps\" = \"Not Mini Apps\";\n\"lng_edit_privacy_contacts\" = \"My contacts\";\n\"lng_edit_privacy_close_friends\" = \"Close friends\";\n\"lng_edit_privacy_contacts_and_premium\" = \"Contacts & Premium\";\n\"lng_edit_privacy_paid\" = \"Paid\";\n\"lng_edit_privacy_contacts_and_miniapps\" = \"Contacts & Mini Apps\";\n\"lng_edit_privacy_nobody\" = \"Nobody\";\n\"lng_edit_privacy_premium\" = \"Premium users\";\n\"lng_edit_privacy_miniapps\" = \"Mini Apps\";\n\"lng_edit_privacy_exceptions\" = \"Add exceptions\";\n\"lng_edit_privacy_user_types\" = \"User types\";\n\"lng_edit_privacy_users_and_groups\" = \"Users and groups\";\n\"lng_edit_privacy_premium_status\" = \"all Telegram Premium subscribers\";\n\"lng_edit_privacy_miniapps_status\" = \"web mini apps that you use\";\n\n\"lng_edit_privacy_exceptions_count#one\" = \"{count} user\";\n\"lng_edit_privacy_exceptions_count#other\" = \"{count} users\";\n\"lng_edit_privacy_exceptions_premium_and\" = \"Premium & {users}\";\n\"lng_edit_privacy_exceptions_miniapps_and\" = \"Mini Apps & {users}\";\n\"lng_edit_privacy_exceptions_add\" = \"Add users\";\n\n\"lng_edit_privacy_phone_number_title\" = \"Phone number privacy\";\n\"lng_edit_privacy_phone_number_header\" = \"Who can see my phone number\";\n\"lng_edit_privacy_phone_number_warning\" = \"Users who have your number saved in their contacts will also see it on Telegram.\";\n\"lng_edit_privacy_phone_number_always_empty\" = \"Always share with\";\n\"lng_edit_privacy_phone_number_never_empty\" = \"Never share with\";\n\"lng_edit_privacy_phone_number_exceptions\" = \"Add users or groups to override the settings above.\";\n\"lng_edit_privacy_phone_number_always_title\" = \"Always share with\";\n\"lng_edit_privacy_phone_number_never_title\" = \"Never share with\";\n\n\"lng_edit_privacy_phone_number_find\" = \"Who can find me by my number\";\n\"lng_edit_privacy_phone_number_contacts\" = \"Users who add your number to their contacts will see it on Telegram only if they are your contacts.\";\n\n\"lng_edit_privacy_lastseen_title\" = \"Last Seen & Online\";\n\"lng_edit_privacy_lastseen_header\" = \"Who can see my last seen time\";\n\"lng_edit_privacy_lastseen_warning\" = \"Unless you are a Premium user, you won't see Last Seen or Online statuses for people with whom you don't share yours. Approximate times will be shown instead (recently, within a week, within a month).\";\n\"lng_edit_privacy_lastseen_always_empty\" = \"Always share with\";\n\"lng_edit_privacy_lastseen_never_empty\" = \"Never share with\";\n\"lng_edit_privacy_lastseen_exceptions\" = \"You can add users or entire groups as exceptions that will override the settings above.\";\n\"lng_edit_privacy_lastseen_always_title\" = \"Always share with\";\n\"lng_edit_privacy_lastseen_never_title\" = \"Never share with\";\n\"lng_edit_lastseen_hide_read_time\" = \"Hide read time\";\n\"lng_edit_lastseen_hide_read_time_about\" = \"Hide the time when you read messages from people who can't see your last seen. If you turn this on, their read time will also be hidden from you (unless you are a Premium user).\\n\\nThis setting does not affect group chats.\";\n\"lng_edit_lastseen_subscribe\" = \"Subscribe to Telegram Premium\";\n\"lng_edit_lastseen_subscribe_about\" = \"If you subscribe to Premium, you will see other users' last seen and read time even if you hid yours from them (unless they specifically restricted it).\";\n\n\"lng_edit_privacy_groups_title\" = \"Groups & Channels\";\n\"lng_edit_privacy_groups_header\" = \"Who can add me to groups and channels\";\n\"lng_edit_privacy_groups_always_empty\" = \"Always allow\";\n\"lng_edit_privacy_groups_never_empty\" = \"Never allow\";\n\"lng_edit_privacy_groups_exceptions\" = \"These users will or will not be able to add you to groups and channels regardless of the settings above.\";\n\"lng_edit_privacy_groups_always_title\" = \"Always allow\";\n\"lng_edit_privacy_groups_never_title\" = \"Never allow\";\n\n\"lng_edit_privacy_about_title\" = \"Bio\";\n\"lng_edit_privacy_about_header\" = \"Who can see my bio\";\n\"lng_edit_privacy_about_always_empty\" = \"Always share with\";\n\"lng_edit_privacy_about_never_empty\" = \"Never share with\";\n\"lng_edit_privacy_about_exceptions\" = \"These users will or will not be able to see your profile bio regardless of the settings above.\";\n\"lng_edit_privacy_about_always_title\" = \"Always Share With\";\n\"lng_edit_privacy_about_never_title\" = \"Never Share With\";\n\n\"lng_edit_privacy_birthday_title\" = \"Date of birth privacy\";\n\"lng_edit_privacy_birthday_header\" = \"Who can see my date of birth\";\n\"lng_edit_privacy_birthday_always_empty\" = \"Always allow\";\n\"lng_edit_privacy_birthday_never_empty\" = \"Never allow\";\n\"lng_edit_privacy_birthday_exceptions\" = \"These users will or will not be able to see your date of birth regardless of the settings above.\";\n\"lng_edit_privacy_birthday_always_title\" = \"Always allow\";\n\"lng_edit_privacy_birthday_never_title\" = \"Never allow\";\n\"lng_edit_privacy_birthday_yet\" = \"You haven't entered your date of birth yet.\\n{link}\";\n\"lng_edit_privacy_birthday_yet_link\" = \"Add my birthday >\";\n\n\"lng_edit_privacy_gifts_title\" = \"Gifts\";\n\"lng_edit_privacy_gifts_header\" = \"Who can display gifts on my profile\";\n\"lng_edit_privacy_gifts_always_empty\" = \"Always allow\";\n\"lng_edit_privacy_gifts_never_empty\" = \"Never allow\";\n\"lng_edit_privacy_gifts_exceptions\" = \"Choose whether gifts from specific senders need your approval before they're visible to others on your profile.\";\n\"lng_edit_privacy_gifts_always_title\" = \"Always allow\";\n\"lng_edit_privacy_gifts_never_title\" = \"Never allow\";\n\n\"lng_edit_privacy_gifts_types\" = \"Accepted Gift Types\";\n\"lng_edit_privacy_gifts_premium\" = \"Premium Subscriptions\";\n\"lng_edit_privacy_gifts_unlimited\" = \"Unlimited\";\n\"lng_edit_privacy_gifts_limited\" = \"Limited-Edition\";\n\"lng_edit_privacy_gifts_unique\" = \"Unique\";\n\"lng_edit_privacy_gifts_channels\" = \"From Channels\";\n\"lng_edit_privacy_gifts_types_about\" = \"Choose the types of gifts that you accept.\";\n\"lng_edit_privacy_gifts_show_icon\" = \"Show Gift Icon in Chats\";\n\"lng_edit_privacy_gifts_show_icon_about\" = \"Display the {emoji}Gift icon in the message input field for both participants in all chats.\";\n\"lng_edit_privacy_gifts_restricted\" = \"This user doesn't accept gifts.\";\n\n\"lng_edit_privacy_calls_title\" = \"Calls\";\n\"lng_edit_privacy_calls_header\" = \"Who can call me\";\n\"lng_edit_privacy_calls_always_empty\" = \"Always allow\";\n\"lng_edit_privacy_calls_never_empty\" = \"Never allow\";\n\"lng_edit_privacy_calls_exceptions\" = \"These users will or will not be able to call you regardless of the settings above.\";\n\"lng_edit_privacy_calls_always_title\" = \"Always allow\";\n\"lng_edit_privacy_calls_never_title\" = \"Never allow\";\n\n\"lng_edit_privacy_calls_p2p_title\" = \"Peer-to-peer in calls\";\n\"lng_edit_privacy_calls_p2p_header\" = \"Use peer-to-peer with\";\n\"lng_edit_privacy_calls_p2p_always_empty\" = \"Always allow\";\n\"lng_edit_privacy_calls_p2p_never_empty\" = \"Never allow\";\n\"lng_edit_privacy_calls_p2p_exceptions\" = \"Peer-to-peer in calls will or will not be used with these users regardless of the settings above.\";\n\"lng_edit_privacy_calls_p2p_always_title\" = \"Always allow\";\n\"lng_edit_privacy_calls_p2p_never_title\" = \"Never allow\";\n\n\"lng_edit_privacy_calls_p2p_everyone\" = \"Everybody\";\n\"lng_edit_privacy_calls_p2p_contacts\" = \"My contacts\";\n\"lng_edit_privacy_calls_p2p_nobody\" = \"Nobody\";\n\n\"lng_edit_privacy_forwards_title\" = \"Forwarded Messages\";\n\"lng_edit_privacy_forwards_header\" = \"Who can add a link to my account when forwarding my messages\";\n\"lng_edit_privacy_forwards_warning\" = \"Messages you send will not link back to your account when forwarded by other users.\";\n\"lng_edit_privacy_forwards_always_empty\" = \"Always allow\";\n\"lng_edit_privacy_forwards_never_empty\" = \"Never allow\";\n\"lng_edit_privacy_forwards_exceptions\" = \"These settings will override the values above.\";\n\"lng_edit_privacy_forwards_exceptions_everyone\" = \"You can add users or entire groups that will not see your profile photo.\";\n\"lng_edit_privacy_forwards_exceptions_nobody\" = \"Add users or entire groups which will still see your profile photo.\";\n\"lng_edit_privacy_forwards_always_title\" = \"Always Allow\";\n\"lng_edit_privacy_forwards_never_title\" = \"Never Allow\";\n\"lng_edit_privacy_forwards_sample_message\" = \"Reinhardt, we need to find you some new tunes 🎶\";\n\"lng_edit_privacy_forwards_sample_everyone\" = \"Link to your account.\";\n\"lng_edit_privacy_forwards_sample_contacts\" = \"Link if allowed by settings below.\";\n\"lng_edit_privacy_forwards_sample_nobody\" = \"Not a link to your account.\";\n\n\"lng_edit_privacy_profile_photo_title\" = \"Profile Photos\";\n\"lng_edit_privacy_profile_photo_header\" = \"Who can see my profile photos\";\n\"lng_edit_privacy_profile_photo_always_empty\" = \"Always share with\";\n\"lng_edit_privacy_profile_photo_never_empty\" = \"Never share with\";\n\"lng_edit_privacy_profile_photo_always_title\" = \"Always Share With\";\n\"lng_edit_privacy_profile_photo_never_title\" = \"Never Share With\";\n\"lng_edit_privacy_profile_photo_public_set\" = \"Set Public Photo\";\n\"lng_edit_privacy_profile_photo_public_update\" = \"Update Public Photo\";\n\"lng_edit_privacy_profile_photo_public_remove\" = \"Remove Public Photo\";\n\"lng_edit_privacy_profile_photo_public_about\" = \"You can upload a public photo for users who are restricted from viewing your real profile photos.\";\n\n\"lng_edit_privacy_voices_title\" = \"Voice Messages\";\n\"lng_edit_privacy_voices_header\" = \"Who can send me voice messages\";\n\"lng_edit_privacy_voices_always_empty\" = \"Always allow\";\n\"lng_edit_privacy_voices_never_empty\" = \"Never allow\";\n\"lng_edit_privacy_voices_exceptions\" = \"These users will or will not be able to send voice and video messages to you regardless of the settings above.\";\n\"lng_edit_privacy_voices_always_title\" = \"Always allow\";\n\"lng_edit_privacy_voices_never_title\" = \"Never Allow\";\n\n\"lng_edit_privacy_saved_music_title\" = \"Saved Music\";\n\"lng_edit_privacy_saved_music_header\" = \"Who can see my saved music in profile\";\n\"lng_edit_privacy_saved_music_always_empty\" = \"Always allow\";\n\"lng_edit_privacy_saved_music_never_empty\" = \"Never allow\";\n\"lng_edit_privacy_saved_music_exceptions\" = \"These users will or will not be able to see your saved music regardless of the settings above.\";\n\"lng_edit_privacy_saved_music_always_title\" = \"Always allow\";\n\"lng_edit_privacy_saved_music_never_title\" = \"Never allow\";\n\n\"lng_messages_privacy_title\" = \"Messages\";\n\"lng_messages_privacy_subtitle\" = \"Who can send me messages\";\n\"lng_messages_privacy_everyone\" = \"Everybody\";\n\"lng_messages_privacy_restricted\" = \"My Contacts and Premium Users\";\n\"lng_messages_privacy_about\" = \"You can restrict messages from users who are not in your contacts and don't have Premium.\";\n\"lng_messages_privacy_premium_button\" = \"Subscribe to Telegram Premium\";\n\"lng_messages_privacy_premium_about\" = \"Subscribe now to change this setting and get access to other exclusive features of Telegram Premium.\";\n\"lng_messages_privacy_premium\" = \"Only subscribers of {link} can select this option.\";\n\"lng_messages_privacy_premium_link\" = \"Telegram Premium\";\n\"lng_messages_privacy_charge\" = \"Charge for messages\";\n\"lng_messages_privacy_charge_about\" = \"Charge a fee for messages from people outside your contacts or those you haven't messaged first.\";\n\"lng_messages_privacy_price\" = \"Set your price per message\";\n\"lng_messages_privacy_price_about\" = \"You will receive {percent} of the selected fee ({amount}) for each incoming message.\";\n\"lng_messages_privacy_exceptions\" = \"Exceptions\";\n\"lng_messages_privacy_remove_fee\" = \"Remove Fee\";\n\"lng_messages_privacy_remove_about\" = \"Add users or entire groups who won't be charged for sending messages to you.\";\n\n\"lng_self_destruct_title\" = \"Account self-destruction\";\n\"lng_self_destruct_description\" = \"If you don't come online at least once within this period, your account will be deleted along with all groups, messages and contacts.\";\n\"lng_self_destruct_sessions_title\" = \"Session termination\";\n\"lng_self_destruct_sessions_description\" = \"If you don't come online from a specific session at least once within this period, it will be terminated.\";\n\n\"lng_change_phone_new_submit\" = \"Submit\";\n\"lng_change_phone_code_title\" = \"Enter code\";\n\"lng_change_phone_error\" = \"You can only change your phone number using mobile Telegram apps. Please use an official Telegram app on your phone to update your number.\";\n\n\"lng_mute_menu_mute\" = \"Mute\";\n\"lng_mute_menu_unmute\" = \"Unmute\";\n\"lng_mute_menu_duration_any\" = \"Mute for {duration}\";\n\"lng_mute_menu_duration\" = \"Mute for...\";\n\"lng_mute_menu_duration_forever\" = \"Mute forever\";\n\"lng_mute_menu_duration_unmute\" = \"Unmute\";\n\"lng_mute_menu_sound_on\" = \"Enable sound\";\n\"lng_mute_menu_sound_off\" = \"Disable sound\";\n\"lng_mute_menu_sound_select\" = \"Select tone\";\n\"lng_mute_box_title\" = \"Mute notifications for...\";\n\n\"lng_preview_loading\" = \"Getting Link Info...\";\n\"lng_preview_cant\" = \"Could not generate preview for this link.\";\n\n\"lng_profile_settings_section\" = \"Settings\";\n\"lng_profile_bot_settings\" = \"Bot Settings\";\n\"lng_profile_bot_help\" = \"Bot Help\";\n\"lng_profile_bot_privacy\" = \"Bot Privacy Policy\";\n\"lng_profile_bot_privacy_url\" = \"https://telegram.org/privacy-tpa\";\n\"lng_profile_common_groups#one\" = \"{count} group in common\";\n\"lng_profile_common_groups#other\" = \"{count} groups in common\";\n\"lng_profile_similar_channels#one\" = \"{count} similar channel\";\n\"lng_profile_similar_channels#other\" = \"{count} similar channels\";\n\"lng_profile_similar_bots#one\" = \"{count} similar bot\";\n\"lng_profile_similar_bots#other\" = \"{count} similar bots\";\n\"lng_profile_saved_messages#one\" = \"{count} saved message\";\n\"lng_profile_saved_messages#other\" = \"{count} saved messages\";\n\"lng_profile_peer_gifts#one\" = \"{count} gift\";\n\"lng_profile_peer_gifts#other\" = \"{count} gifts\";\n\"lng_profile_unofficial_warning\" = \"{icon} {name} uses an unofficial Telegram client — messages to this user may be less secure.\";\n\"lng_profile_participants_section\" = \"Members\";\n\"lng_profile_subscribers_section\" = \"Subscribers\";\n\"lng_profile_add_contact\" = \"Add Contact\";\n\"lng_profile_clear_and_exit\" = \"Delete and leave\";\n\"lng_profile_leave_channel\" = \"Leave channel\";\n\"lng_profile_delete_channel\" = \"Delete channel\";\n\"lng_profile_leave_group\" = \"Leave group\";\n\"lng_profile_delete_group\" = \"Delete group\";\n\"lng_profile_report\" = \"Report\";\n\"lng_profile_block_bot\" = \"Stop and block bot\";\n\"lng_profile_restart_bot\" = \"Restart bot\";\n\"lng_profile_invite_to_group\" = \"Add to Group\";\n\"lng_profile_add_bot_as_admin\" = \"Add to Group or Channel\";\n\"lng_profile_invite_to_channel\" = \"Add to Channel\";\n\"lng_profile_invite_to_group_about\" = \"This bot is able to manage a group.\";\n\"lng_profile_invite_to_channel_about\" = \"This bot is able to manage a channel.\";\n\"lng_profile_add_bot_as_admin_about\" = \"This bot is able to manage a group or channel.\";\n\"lng_profile_add_participant\" = \"Add Members\";\n\"lng_profile_add_via_link\" = \"Invite via Link\";\n\"lng_profile_hide_participants\" = \"Hide Members\";\n\"lng_profile_hide_participants_about\" = \"Switch this on to hide the list of members in this group. Admins will remain visible.\";\n\"lng_profile_view_channel\" = \"View Channel\";\n\"lng_profile_view_discussion\" = \"View discussion\";\n\"lng_profile_direct_messages\" = \"Direct messages\";\n\"lng_profile_join_channel\" = \"Join Channel\";\n\"lng_profile_join_group\" = \"Join Group\";\n\"lng_profile_apply_to_join_group\" = \"Apply to Join Group\";\n\"lng_profile_kick\" = \"Remove\";\n\"lng_profile_delete_removed\" = \"Delete\";\n\"lng_profile_sure_kick\" = \"Remove {user} from the group?\";\n\"lng_profile_sure_kick_channel\" = \"Remove {user} from the channel?\";\n\"lng_profile_sure_remove_admin\" = \"Remove {user} from admins?\";\n\"lng_profile_loading\" = \"Loading...\";\n\"lng_profile_saved_stories#one\" = \"{count} post\";\n\"lng_profile_saved_stories#other\" = \"{count} posts\";\n\"lng_profile_posts#one\" = \"{count} post\";\n\"lng_profile_posts#other\" = \"{count} posts\";\n\"lng_profile_photos#one\" = \"{count} photo\";\n\"lng_profile_photos#other\" = \"{count} photos\";\n\"lng_profile_gifs#one\" = \"{count} GIF\";\n\"lng_profile_gifs#other\" = \"{count} GIFs\";\n\"lng_profile_polls#one\" = \"{count} poll\";\n\"lng_profile_polls#other\" = \"{count} polls\";\n\"lng_profile_videos#one\" = \"{count} video\";\n\"lng_profile_videos#other\" = \"{count} videos\";\n\"lng_profile_songs#one\" = \"{count} audio file\";\n\"lng_profile_songs#other\" = \"{count} audio files\";\n\"lng_profile_files#one\" = \"{count} file\";\n\"lng_profile_files#other\" = \"{count} files\";\n\"lng_profile_audios#one\" = \"{count} voice message\";\n\"lng_profile_audios#other\" = \"{count} voice messages\";\n\"lng_profile_shared_links#one\" = \"{count} shared link\";\n\"lng_profile_shared_links#other\" = \"{count} shared links\";\n\"lng_profile_copy_phone\" = \"Copy Phone Number\";\n\"lng_profile_copy_fullname\" = \"Copy Name\";\n\"lng_profile_photo_by_you\" = \"photo set by you\";\n\"lng_profile_public_photo\" = \"public photo\";\n\"lng_profile_administrators#one\" = \"{count} administrator\";\n\"lng_profile_administrators#other\" = \"{count} administrators\";\n\"lng_profile_manage\" = \"Channel settings\";\n\"lng_profile_topic_toast\" = \"This topic contains {name}\";\n\n\"lng_invite_upgrade_title\" = \"Upgrade to Premium\";\n\"lng_invite_upgrade_group_invite#one\" = \"{users} only accepts invitations to groups from Contacts and **Premium** users.\";\n\"lng_invite_upgrade_group_invite#other\" = \"{users} only accept invitations to groups from Contacts and **Premium** users.\";\n\"lng_invite_upgrade_group_write#one\" = \"{users} only accepts messages and invitations to groups from Contacts and **Premium** users.\";\n\"lng_invite_upgrade_group_write#other\" = \"{users} only accept messages and invitations to groups from Contacts and **Premium** users.\";\n\"lng_invite_upgrade_channel_invite#one\" = \"{users} only accepts invitations to channels from Contacts and **Premium** users.\";\n\"lng_invite_upgrade_channel_invite#other\" = \"{users} only accept invitations to channels from Contacts and **Premium** users.\";\n\"lng_invite_upgrade_channel_write#one\" = \"{users} only accepts messages and invitations to channels from Contacts and **Premium** users.\";\n\"lng_invite_upgrade_channel_write#other\" = \"{users} only accept messages and invitations to channels from Contacts and **Premium** users.\";\n\"lng_invite_upgrade_users_few\" = \"{users} and {last}\";\n\"lng_invite_upgrade_users_many#one\" = \"{users} and **{count}** more person\";\n\"lng_invite_upgrade_users_many#other\" = \"{users} and **{count}** more people\";\n\"lng_invite_upgrade_or\" = \"or\";\n\"lng_invite_upgrade_via_title\" = \"Invite via Link\";\n\"lng_invite_upgrade_via_group_about\" = \"You can send an invite link to the group in a private message instead.\";\n\"lng_invite_upgrade_via_channel_about\" = \"You can send an invite link to the channel in a private message instead.\";\n\"lng_invite_status_disabled\" = \"available only to Premium users\";\n\n\"lng_leave_next_owner_box_title\" = \"Leave Channel?\";\n\"lng_leave_next_owner_box_title_group\" = \"Leave Group?\";\n\"lng_leave_next_owner_box_about\" = \"If you leave, **{user}** will become the new owner of **{chat}** in **1 week**.\";\n\"lng_leave_next_owner_box_about_admin\" = \"If you leave, **{user}** will become the new admin of **{chat}** in **1 week**.\";\n\"lng_leave_next_owner_box_about_legacy\" = \"If you leave, **{user}** will immediately become the new owner of **{chat}**.\";\n\"lng_leave_next_owner_box_about_admin_legacy\" = \"If you leave, **{user}** will immediately become the new admin of **{chat}**.\";\n\"lng_select_next_owner_box\" = \"Appoint Another Owner\";\n\"lng_select_next_owner_box_admin\" = \"Appoint Another Admin\";\n\"lng_select_next_owner_box_title\" = \"Appoint New Owner\";\n\"lng_select_next_owner_box_title_admin\" = \"Appoint New Admin\";\n\"lng_select_next_owner_box_confirm\" = \"Appoint and Leave Group\";\n\"lng_select_next_owner_box_sub_admins\" = \"Channel admins\";\n\"lng_select_next_owner_box_sub_admins_group\" = \"Group admins\";\n\"lng_select_next_owner_box_sub_members\" = \"Channel members\";\n\"lng_select_next_owner_box_sub_members_group\" = \"Group members\";\n\"lng_select_next_owner_box_status_joined\" = \"joined {date}\";\n\"lng_select_next_owner_box_status_promoted\" = \"promoted {date}\";\n\"lng_select_next_owner_box_empty_list\" = \"There are no eligible participants to appoint as owner.\";\n\"lng_select_next_owner_box_empty_list_admin\" = \"There are no eligible participants to appoint as admin.\";\n\n\"lng_via_link_group_one\" = \"**{user}** restricts adding them to groups.\\nYou can send them an invite link as message instead.\";\n\"lng_via_link_group_many#one\" = \"**{count} user** restricts adding them to groups.\\nYou can send them an invite link as message instead.\";\n\"lng_via_link_group_many#other\" = \"**{count} users** restrict adding them to groups.\\nYou can send them an invite link as message instead.\";\n\"lng_via_link_channel_one\" = \"**{user}** restricts adding them to channels.\\nYou can send them an invite link as message instead.\";\n\"lng_via_link_channel_many#one\" = \"**{count} user** restricts adding them to channels.\\nYou can send them an invite link as message instead.\";\n\"lng_via_link_channel_many#other\" = \"**{count} users** restrict adding them to channels.\\nYou can send them an invite link as message instead.\";\n\"lng_via_link_send\" = \"Send Invite Link\";\n\"lng_via_link_cant\" = \"You can't create a link\";\n\"lng_via_link_cant_one\" = \"**{user}** can only be invited via link, but you don't have permission to share invite links to this group.\";\n\"lng_via_link_cant_many#one\" = \"**{count} user** can only be invited via link, but you don't have permission to share invite links to this group.\";\n\"lng_via_link_cant_many#other\" = \"**{count} users** can only be invited via link, but you don't have permission to share invite links to this group.\";\n\"lng_via_link_shared_one\" = \"Link shared with **{user}**.\";\n\"lng_via_link_shared_many#one\" = \"Link shared with **{count} user**.\";\n\"lng_via_link_shared_many#other\" = \"Link shared with **{count} users**.\";\n\n\"lng_info_personal_channel_label\" = \"Channel\";\n\"lng_info_mobile_label\" = \"Mobile\";\n\"lng_info_mobile_context_menu_fragment_about\" = \"This number is not tied to a SIM card and was acquired on {link}.\";\n\"lng_info_mobile_context_menu_fragment_about_link\" = \"Fragment\";\n\"lng_info_mobile_hidden\" = \"Hidden\";\n\"lng_info_username_label\" = \"Username\";\n\"lng_info_usernames_label\" = \"also\";\n\"lng_info_birthday_label\" = \"Date of birth\";\n\"lng_info_birthday_years#one\" = \"{date} ({count} year old)\";\n\"lng_info_birthday_years#other\" = \"{date} ({count} years old)\";\n\"lng_info_birthday_today_years#one\" = \"{date}\\n({count} year old)\";\n\"lng_info_birthday_today_years#other\" = \"{date}\\n({count} years old)\";\n\"lng_info_birthday_today_label\" = \"Birthday today\";\n\"lng_info_birthday_today\" = \"{emoji} {date}\";\n\"lng_info_notes_label\" = \"Notes\";\n\"lng_info_notes_private\" = \"only visible to you\";\n\"lng_edit_note\" = \"Edit Note\";\n\"lng_delete_note\" = \"Delete Note\";\n\"lng_info_bio_label\" = \"Bio\";\n\"lng_info_link_label\" = \"Link\";\n\"lng_info_link_topic_label\" = \"This topic link will only work for group members\";\n\"lng_info_location_label\" = \"Location\";\n\"lng_info_about_label\" = \"Description\";\n\"lng_info_work_open\" = \"Open\";\n\"lng_info_work_closed\" = \"Closed\";\n\"lng_info_hours_label\" = \"Business hours\";\n\"lng_info_hours_closed\" = \"closed\";\n\"lng_info_hours_opens_in_minutes#one\" = \"opens in {count} minute\";\n\"lng_info_hours_opens_in_minutes#other\" = \"opens in {count} minutes\";\n\"lng_info_hours_opens_in_hours#one\" = \"opens in {count} hour\";\n\"lng_info_hours_opens_in_hours#other\" = \"opens in {count} hours\";\n\"lng_info_hours_opens_in_days#one\" = \"opens in {count} day\";\n\"lng_info_hours_opens_in_days#other\" = \"opens in {count} days\";\n\"lng_info_hours_open_full\" = \"open 24 hours\";\n\"lng_info_hours_next_day\" = \"{time} (next day)\";\n\"lng_info_hours_local_time\" = \"local time\";\n\"lng_info_hours_my_time\" = \"my time\";\n\"lng_info_user_title\" = \"User Info\";\n\"lng_info_bot_title\" = \"Bot Info\";\n\"lng_info_group_title\" = \"Group Info\";\n\"lng_info_channel_title\" = \"Channel Info\";\n\"lng_info_topic_title\" = \"Topic Info\";\n\"lng_info_thread_title\" = \"Thread Info\";\n\"lng_profile_enable_notifications\" = \"Notifications\";\n\"lng_profile_send_message\" = \"Send Message\";\n\"lng_profile_open_app\" = \"Open App\";\n\"lng_profile_open_app_short\" = \"Open\";\n\"lng_profile_open_app_about\" = \"By launching this mini app, you agree to the {terms}.\";\n\"lng_profile_open_app_terms\" = \"Terms of Service for Mini Apps\";\n\"lng_profile_open_photo\" = \"Open Photo\";\n\"lng_profile_bot_permissions_title\" = \"Allow access to\";\n\"lng_profile_bot_emoji_status_access\" = \"Emoji Status\";\n\"lng_info_add_as_contact\" = \"Add to contacts\";\n\"lng_profile_shared_media\" = \"Shared media\";\n\"lng_profile_suggest_photo\" = \"Suggest Profile Photo\";\n\"lng_profile_suggest_photo_from_clipboard\" = \"Suggest From Clipboard\";\n\"lng_profile_set_photo_for\" = \"Set Profile Photo\";\n\"lng_profile_set_photo_for_group\" = \"Set Group Photo\";\n\"lng_profile_set_photo_for_channel\" = \"Set Channel Photo\";\n\"lng_profile_set_photo_for_from_clipboard\" = \"Set From Clipboard\";\n\"lng_profile_set_photo_for_about\" = \"You can replace {user}'s photo with another photo that only you will see.\";\n\"lng_profile_photo_reset\" = \"Reset to Original\";\n\"lng_profile_photo_reset_button\" = \"Reset\";\n\"lng_profile_photo_reset_sure\" = \"Are you sure you want to reset {user}'s photo to the original?\";\n\"lng_profile_photo_from_clipboard\" = \"From clipboard\";\n\"lng_profile_suggest_sure\" = \"You can suggest {user} to set this photo for their Telegram profile.\";\n\"lng_profile_suggest_button\" = \"Suggest\";\n\"lng_profile_set_personal_sure\" = \"Only you will see this photo and it will replace any photo {user} sets for themselves.\";\n\"lng_profile_accept_photo_sure\" = \"{user} suggests this photo for your Telegram profile.\";\n\"lng_profile_set_photo_button\" = \"Set Photo\";\n\"lng_profile_accept_video_sure\" = \"{user} suggests this photo for your Telegram profile.\";\n\"lng_profile_set_video_button\" = \"Set Photo\";\n\"lng_profile_changed_photo_title\" = \"Photo updated\";\n\"lng_profile_changed_photo_about\" = \"You can change it in {link}.\";\n\"lng_profile_changed_photo_link\" = \"Settings\";\n\n\"lng_profile_action_short_message\" = \"Message\";\n\"lng_profile_action_short_channel\" = \"Channel\";\n\"lng_profile_action_short_mute\" = \"Mute\";\n\"lng_profile_action_short_unmute\" = \"Unmute\";\n\"lng_profile_action_short_call\" = \"Call\";\n\"lng_profile_action_short_discuss\" = \"Discuss\";\n\"lng_profile_action_short_gift\" = \"Gift\";\n\"lng_profile_action_short_join\" = \"Join\";\n\"lng_profile_action_short_report\" = \"Report\";\n\"lng_profile_action_short_leave\" = \"Leave\";\n\"lng_profile_action_short_more\" = \"More\";\n\"lng_profile_action_short_manage\" = \"Manage\";\n\n\"lng_media_type_photos\" = \"Photos\";\n\"lng_media_type_gifs\" = \"GIFs\";\n\"lng_media_type_videos\" = \"Videos\";\n\"lng_media_type_songs\" = \"Audio files\";\n\"lng_media_type_files\" = \"Files\";\n\"lng_media_type_audios\" = \"Voice messages\";\n\"lng_media_type_links\" = \"Shared links\";\n\"lng_media_type_polls\" = \"Polls\";\n\"lng_polls_search_none\" = \"No polls found\";\n\"lng_media_type_rounds\" = \"Video messages\";\n\"lng_media_saved_music_your\" = \"Your playlist\";\n\"lng_media_saved_music_title\" = \"Playlist\";\n\"lng_profile_common_groups_section\" = \"Groups in common\";\n\"lng_info_edit_contact\" = \"Edit contact\";\n\"lng_info_delete_contact\" = \"Delete contact\";\n\"lng_info_share_contact\" = \"Share this contact\";\n\"lng_profile_clear_history\" = \"Clear history\";\n\"lng_profile_delete_conversation\" = \"Delete chat\";\n\"lng_profile_block_user\" = \"Block user\";\n\"lng_profile_unblock_user\" = \"Unblock user\";\n\"lng_profile_export_chat\" = \"Export chat history\";\n\"lng_profile_export_topic\" = \"Export topic history\";\n\"lng_profile_gift_premium\" = \"Gift Premium\";\n\"lng_media_selected_photo#one\" = \"{count} Photo\";\n\"lng_media_selected_photo#other\" = \"{count} Photos\";\n\"lng_media_selected_gif#one\" = \"{count} GIF\";\n\"lng_media_selected_gif#other\" = \"{count} GIFs\";\n\"lng_media_selected_video#one\" = \"{count} Video\";\n\"lng_media_selected_video#other\" = \"{count} Videos\";\n\"lng_media_selected_song#one\" = \"{count} Audio file\";\n\"lng_media_selected_song#other\" = \"{count} Audio files\";\n\"lng_media_selected_file#one\" = \"{count} File\";\n\"lng_media_selected_file#other\" = \"{count} Files\";\n\"lng_media_selected_audio#one\" = \"{count} Voice message\";\n\"lng_media_selected_audio#other\" = \"{count} Voice messages\";\n\"lng_media_selected_link#one\" = \"{count} shared link\";\n\"lng_media_selected_link#other\" = \"{count} shared links\";\n\"lng_media_selected_poll#one\" = \"{count} Poll\";\n\"lng_media_selected_poll#other\" = \"{count} Polls\";\n\"lng_media_photo_empty\" = \"No photos here yet\";\n\"lng_media_gif_empty\" = \"No GIFs here yet\";\n\"lng_media_video_empty\" = \"No videos here yet\";\n\"lng_media_song_empty\" = \"No music files here yet\";\n\"lng_media_file_empty\" = \"No files here yet\";\n\"lng_media_audio_empty\" = \"No voice messages here yet\";\n\"lng_media_link_empty\" = \"No shared links here yet\";\n\"lng_media_song_empty_search\" = \"No music files found\";\n\"lng_media_file_empty_search\" = \"No files found\";\n\"lng_media_link_empty_search\" = \"No shared links found\";\n\n\"lng_manage_group_title\" = \"Manage group\";\n\"lng_manage_channel_title\" = \"Manage Channel\";\n\"lng_manage_bot_title\" = \"Manage Bot\";\n\"lng_manage_peer_recent_actions\" = \"Recent actions\";\n\"lng_manage_peer_star_ref\" = \"Affiliate programs\";\n\"lng_manage_peer_members\" = \"Members\";\n\"lng_manage_peer_subscribers\" = \"Subscribers\";\n\"lng_manage_peer_administrators\" = \"Administrators\";\n\"lng_manage_peer_exceptions\" = \"Exceptions\";\n\"lng_manage_peer_removed_users\" = \"Removed users\";\n\"lng_manage_peer_permissions\" = \"Permissions\";\n\"lng_manage_peer_invite_links\" = \"Invite links\";\n\"lng_manage_peer_reactions\" = \"Reactions\";\n\"lng_manage_peer_reactions_on\" = \"All\";\n\"lng_manage_peer_reactions_off\" = \"Off\";\n\"lng_manage_peer_requests\" = \"Join Requests\";\n\"lng_manage_peer_requests_channel\" = \"Join Requests\";\n\n\"lng_manage_peer_reactions_enable\" = \"Enable Reactions\";\n\"lng_manage_peer_reactions_about_channel\" = \"Allow subscribers to react to channel posts.\";\n\"lng_manage_peer_reactions_all\" = \"All reactions\";\n\"lng_manage_peer_reactions_all_about\" = \"Members of the group can use any emoji as reactions to messages.\";\n\"lng_manage_peer_reactions_some\" = \"Some reactions\";\n\"lng_manage_peer_reactions_some_about\" = \"Members of the group can use only certain approved emoji as reactions to messages.\";\n\"lng_manage_peer_reactions_none\" = \"No reactions\";\n\"lng_manage_peer_reactions_none_about\" = \"Members of the group can't add any reactions to messages.\";\n\"lng_manage_peer_reactions_some_title\" = \"Only allow these reactions\";\n\"lng_manage_peer_reactions_available\" = \"Available reactions\";\n\"lng_manage_peer_reactions_available_ph\" = \"Add reactions...\";\n\"lng_manage_peer_reactions_own\" = \"You can also {link} emoji packs and use them as reactions.\";\n\"lng_manage_peer_reactions_own_link\" = \"create your own\";\n\"lng_manage_peer_reactions_level#one\" = \"Your channel needs to reach level **{count}** to use **{same_count}** custom reaction.\";\n\"lng_manage_peer_reactions_level#other\" = \"Your channel needs to reach level **{count}** to use **{same_count}** custom reactions.\";\n\"lng_manage_peer_reactions_boost\" = \"Boost your channel {link}.\";\n\"lng_manage_peer_reactions_boost_link\" = \"here\";\n\"lng_manage_peer_reactions_limit\" = \"Channels can't have more custom reactions.\";\n\n\"lng_manage_peer_reactions_max_title\" = \"Maximum number of reactions\";\n\"lng_manage_peer_reactions_max_slider#one\" = \"{count} reaction per post\";\n\"lng_manage_peer_reactions_max_slider#other\" = \"{count} reactions per post\";\n\"lng_manage_peer_reactions_max_about\" = \"Limit the number of different reactions that can be added to a post, including already published ones.\";\n\n\"lng_manage_peer_reactions_paid\" = \"Enable Paid Reactions\";\n\"lng_manage_peer_reactions_paid_about\" = \"Switch this on to let your subscribers react to posts with Telegram Stars, which you will be able to withdraw as TON. {link}\";\n\"lng_manage_peer_reactions_paid_link\" = \"Learn more >\";\n\n\"lng_manage_peer_antispam\" = \"Aggressive Anti-Spam\";\n\"lng_manage_peer_antispam_about\" = \"Telegram will filter more spam but may occasionally affect ordinary messages. You can report False Positives in Recent Actions.\";\n\"lng_manage_peer_antispam_not_enough#one\" = \"Aggressive filtering can be enabled only in groups with more than **{count} member**.\";\n\"lng_manage_peer_antispam_not_enough#other\" = \"Aggressive filtering can be enabled only in groups with more than **{count} members**.\";\n\n\"lng_manage_peer_group_type\" = \"Group type\";\n\"lng_manage_peer_channel_type\" = \"Channel type\";\n\"lng_manage_peer_link_type\" = \"Link type\";\n\"lng_manage_peer_link_permanent\" = \"Permanent link\";\n\"lng_manage_peer_link_invite\" = \"Invite link\";\n\"lng_manage_peer_link_expired\" = \"Expired link\";\n\"lng_manage_private_group_title\" = \"Private\";\n\"lng_manage_private_group_noforwards_title\" = \"Private restricted\";\n\"lng_manage_public_group_title\" = \"Public\";\n\"lng_manage_private_peer_title\" = \"Private\";\n\"lng_manage_private_peer_noforwards_title\" = \"Private restricted\";\n\"lng_manage_public_peer_title\" = \"Public\";\n\"lng_manage_peer_send_title\" = \"Who can send messages?\";\n\"lng_manage_peer_send_only_members\" = \"Only members\";\n\"lng_manage_peer_send_only_members_about\" = \"Turn this on if you expect users to join your group before being able to send messages.\";\n\"lng_manage_peer_send_approve_members\" = \"Approve new members\";\n\"lng_manage_peer_send_approve_members_about\" = \"Turn this on if you want users to join the group only after they are approved by an admin.\";\n\"lng_manage_peer_no_forwards_title\" = \"Content protection\";\n\"lng_manage_peer_no_forwards\" = \"Restrict saving content\";\n\"lng_manage_peer_no_forwards_about\" = \"Members won't be able to copy, save or forward content from this group.\";\n\"lng_manage_peer_no_forwards_about_channel\" = \"Subscribers won't be able to copy, save or forward content from this channel.\";\n\n\"lng_disable_sharing\" = \"Disable Sharing\";\n\"lng_enable_sharing\" = \"Enable Sharing\";\n\"lng_disable_sharing_title\" = \"Disable Sharing\";\n\"lng_disable_sharing_no_forwarding\" = \"No Forwarding\";\n\"lng_disable_sharing_no_forwarding_about\" = \"Disable message forwarding to other chats.\";\n\"lng_disable_sharing_no_saving\" = \"No Saving\";\n\"lng_disable_sharing_no_saving_about\" = \"Disable copying texts and saving photos and videos to gallery.\";\n\"lng_disable_sharing_unlock\" = \"Unlock with Telegram Premium\";\n\"lng_disable_sharing_button\" = \"Disable Sharing\";\n\"lng_disable_sharing_toast\" = \"Sharing disabled for this chat.\";\n\"lng_enable_sharing_toast\" = \"Sharing enabled for this chat.\";\n\"lng_enable_sharing_request_title\" = \"Enable Sharing\";\n\"lng_enable_sharing_request_text\" = \"You need **{name}'s** approval to enable sharing. Send a request?\";\n\"lng_enable_sharing_request_button\" = \"Send Request\";\n\n\"lng_action_no_forwards_you_disabled\" = \"You disabled sharing in this chat\";\n\"lng_action_no_forwards_you_enabled\" = \"You enabled sharing in this chat\";\n\"lng_action_no_forwards_disabled\" = \"{from} disabled sharing in this chat\";\n\"lng_action_no_forwards_enabled\" = \"{from} enabled sharing in this chat\";\n\"lng_action_no_forwards_still_disabled\" = \"Sharing in this chat is still disabled\";\n\"lng_action_no_forwards_request\" = \"{from} would like to enable sharing in this chat, which includes:\";\n\"lng_action_no_forwards_request_you\" = \"You requested to enable sharing in this chat, which includes:\";\n\"lng_action_no_forwards_feature_forwarding\" = \"Forwarding messages\";\n\"lng_action_no_forwards_feature_saving\" = \"Saving photos and videos\";\n\"lng_action_no_forwards_feature_copying\" = \"Copying messages\";\n\"lng_action_no_forwards_accept\" = \"Accept\";\n\"lng_action_no_forwards_reject\" = \"Reject\";\n\"lng_action_no_forwards_request_expired\" = \"Sharing enable request has expired\";\n\n\"lng_manage_peer_bot_public_link\" = \"Public Link\";\n\"lng_manage_peer_bot_public_links\" = \"Public Links\";\n\"lng_manage_peer_bot_balance\" = \"Balance\";\n\"lng_manage_peer_bot_balance_currency\" = \"Toncoin\";\n\"lng_manage_peer_bot_balance_credits\" = \"Stars\";\n\"lng_manage_peer_bot_star_ref\" = \"Affiliate Program\";\n\"lng_manage_peer_bot_star_ref_off\" = \"Off\";\n\"lng_manage_peer_bot_star_ref_about\" = \"Share a link to {bot} with your friends and earn {amount} of their spending there.\";\n\"lng_manage_peer_bot_verify\" = \"Verify Accounts\";\n\"lng_manage_peer_bot_edit_intro\" = \"Edit Intro\";\n\"lng_manage_peer_bot_edit_commands\" = \"Edit Commands\";\n\"lng_manage_peer_bot_edit_settings\" = \"Change Bot Settings\";\n\"lng_manage_peer_bot_about\" = \"Use {bot} to manage this bot.\";\n\n\"lng_bot_verify_title\" = \"Choose Chat to Verify\";\n\"lng_bot_verify_bot_title\" = \"Verify Bot\";\n\"lng_bot_verify_bot_text\" = \"Do you want to verify {name} with your verification mark and description?\";\n\"lng_bot_verify_bot_about\" = \"You can customize your description for each bot.\";\n\"lng_bot_verify_bot_submit\" = \"Verify Bot\";\n\"lng_bot_verify_bot_sent\" = \"{name} has been notified and will receive your verification mark and description upon accepting.\";\n\"lng_bot_verify_bot_remove\" = \"This bot is already verified by you. Do you want to remove verification?\";\n\"lng_bot_verify_user_title\" = \"Verify User\";\n\"lng_bot_verify_user_text\" = \"Do you want to verify {name} with your verification mark and description?\";\n\"lng_bot_verify_user_about\" = \"You can customize your description for each account.\";\n\"lng_bot_verify_user_submit\" = \"Verify User\";\n\"lng_bot_verify_user_sent\" = \"{name} has been notified and will receive your verification mark and description upon accepting.\";\n\"lng_bot_verify_user_remove\" = \"This account is already verified by you. Do you want to remove verification?\";\n\"lng_bot_verify_channel_title\" = \"Verify Channel\";\n\"lng_bot_verify_channel_text\" = \"Do you want to verify {name} with your verification mark and description?\";\n\"lng_bot_verify_channel_about\" = \"You can customize your description for each channel.\";\n\"lng_bot_verify_channel_submit\" = \"Verify Channel\";\n\"lng_bot_verify_channel_sent\" = \"{name} has been notified and will receive your verification mark and description upon accepting.\";\n\"lng_bot_verify_channel_remove\" = \"This channel is already verified by you. Do you want to remove verification?\";\n\"lng_bot_verify_group_title\" = \"Verify Group\";\n\"lng_bot_verify_group_text\" = \"Do you want to verify {name} with your verification mark and description?\";\n\"lng_bot_verify_group_about\" = \"You can customize your description for each group.\";\n\"lng_bot_verify_group_submit\" = \"Verify Group\";\n\"lng_bot_verify_group_sent\" = \"{name} has been notified and will receive your verification mark and description upon accepting.\";\n\"lng_bot_verify_group_remove\" = \"This group is already verified by you. Do you want to remove verification?\";\n\"lng_bot_verify_description_label\" = \"Description\";\n\"lng_bot_verify_remove_title\" = \"Remove verification\";\n\"lng_bot_verify_remove_submit\" = \"Remove\";\n\"lng_bot_verify_remove_done\" = \"You've removed this verification.\";\n\n\"lng_star_ref_title\" = \"Affiliate Program\";\n\"lng_star_ref_about\" = \"Reward those who help grow your user base.\";\n\"lng_star_ref_share_title\" = \"Share revenue with affiliates\";\n\"lng_star_ref_share_about\" = \"Set the commission for revenue generated by users referred to you.\";\n\"lng_star_ref_launch_title\" = \"Launch your affiliate program\";\n\"lng_star_ref_launch_about\" = \"Telegram will feature your program for millions of potential affiliates.\";\n\"lng_star_ref_let_title\" = \"Let affiliate promote you\";\n\"lng_star_ref_let_about\" = \"Affiliates will share your referral link with their audience.\";\n\"lng_star_ref_commission_title\" = \"Commission\";\n\"lng_star_ref_commission_about\" = \"Define the percentage of star revenue your affiliates earn for referring users to your bot.\";\n\"lng_star_ref_duration_title\" = \"Duration\";\n\"lng_star_ref_duration_about\" = \"Set the duration for which affiliates will earn commissions from referred users.\";\n\"lng_star_ref_existing_title\" = \"View existing programs\";\n\"lng_star_ref_existing_about\" = \"Explore what other mini apps offer.\";\n\"lng_star_ref_add_bot\" = \"Add {bot}\";\n\"lng_star_ref_end\" = \"End Affiliate Program\";\n\"lng_star_ref_start\" = \"Start Affiliate Program\";\n\"lng_star_ref_start_disabled\" = \"Available in {time}\";\n\"lng_star_ref_start_info\" = \"By creating an affiliate program, you agree to the {terms} of Affiliate Programs.\";\n\"lng_star_ref_update\" = \"Update Affiliate Program\";\n\"lng_star_ref_update_info\" = \"By updating an affiliate program, you agree to the {terms} of Affiliate Programs.\";\n\"lng_star_ref_button_link\" = \"terms and conditions\";\n\"lng_star_ref_tos_url\" = \"https://telegram.org/tos/mini-apps\";\n\"lng_star_ref_warning_title\" = \"Warning\";\n\"lng_star_ref_warning_text\" = \"Once you start the affiliate program, you won't be able to decrease its commission or duration. You can only increase these parameters or end the program, which will disable all previously distributed referral links.\";\n\"lng_star_ref_warning_change\" = \"This change is irreversible. You won't be able to reduce commission or duration. You can only increase these parameters or end the program, which will disable all previously shared referral links.\";\n\"lng_star_ref_warning_start\" = \"Start\";\n\"lng_star_ref_warning_update\" = \"Update\";\n\"lng_star_ref_warning_if_end\" = \"If you end your affiliate program:\";\n\"lng_star_ref_warning_if_end1\" = \"Any referral links already shared will be disabled in **24** hours.\";\n\"lng_star_ref_warning_if_end2\" = \"All participating affiliates will be notified.\";\n\"lng_star_ref_warning_if_end3\" = \"You will be able to start a new affiliate program only in **24** hours.\";\n\"lng_star_ref_warning_end\" = \"End Anyway\";\n\"lng_star_ref_created_title\" = \"Affiliate program started\";\n\"lng_star_ref_created_text\" = \"Any Telegram user, channel owner or mini app developer can now join your program.\";\n\"lng_star_ref_updated_title\" = \"Affiliate program updated\";\n\"lng_star_ref_updated_text\" = \"Any Telegram user, channel owner or mini app developer can join your program.\";\n\"lng_star_ref_ended_title\" = \"Affiliate program ended\";\n\"lng_star_ref_ended_text\" = \"Participating affiliates have been notified. All referral links will be disabled in **24** hours.\";\n\"lng_star_ref_list_title\" = \"Affiliate Programs\";\n\"lng_star_ref_list_about_channel\" = \"Promote mini apps to your subscribers and earn a share of their revenue in Stars.\";\n\"lng_star_ref_list_text\" = \"Earn a commission each time a user who first accessed a mini app through your referral link spends **Stars** within it.\";\n\"lng_star_ref_list_my\" = \"My Programs\";\n\"lng_star_ref_list_my_open\" = \"Open App\";\n\"lng_star_ref_list_my_copy\" = \"Copy Link\";\n\"lng_star_ref_list_my_leave\" = \"Leave\";\n\"lng_star_ref_list_subtitle\" = \"Programs\";\n\"lng_star_ref_sort_text\" = \"Sort by {sort}\";\n\"lng_star_ref_sort_profitability\" = \"Profitability\";\n\"lng_star_ref_sort_date\" = \"Date\";\n\"lng_star_ref_sort_revenue\" = \"Revenue\";\n\"lng_star_ref_reliable_title\" = \"Reliable\";\n\"lng_star_ref_reliable_about\" = \"Receive guaranteed commissions for spending by users you refer.\";\n\"lng_star_ref_transparent_title\" = \"Transparent\";\n\"lng_star_ref_transparent_about\" = \"Track your commissions from referred users in real time.\";\n\"lng_star_ref_simple_title\" = \"Simple\";\n\"lng_star_ref_simple_about\" = \"Choose a mini app below, get your referral link, and start earning Stars.\";\n\"lng_star_ref_duration_forever\" = \"Forever\";\n\"lng_star_ref_one_about\" = \"{app} will share {amount} of the revenue from each user you refer to it {duration}.\";\n\"lng_star_ref_one_about_for_forever\" = \"for **lifetime**\";\n\"lng_star_ref_one_about_for_months#one\" = \"for **{count} month**\";\n\"lng_star_ref_one_about_for_months#other\" = \"for **{count} months**\";\n\"lng_star_ref_one_about_for_years#one\" = \"for **{count} year**\";\n\"lng_star_ref_one_about_for_years#other\" = \"for **{count} years**\";\n\"lng_star_ref_one_daily_revenue\" = \"Daily revenue per user: {amount}\";\n\"lng_star_ref_one_join\" = \"Join Program\";\n\"lng_star_ref_one_join_text\" = \"By joining this program, you agree to the {terms} of Affiliate Programs.\";\n\"lng_star_ref_joined_title\" = \"Program joined\";\n\"lng_star_ref_joined_text\" = \"You can now copy the referral link.\";\n\"lng_star_ref_link_title\" = \"Referral Link\";\n\"lng_star_ref_link_about_channel\" = \"Share this link with your subscribers to earn a {amount} commission on their spending in {app} {duration}.\";\n\"lng_star_ref_link_about_user\" = \"Share this link with your friends to earn a {amount} commission on their spending in {app} {duration}.\";\n\"lng_star_ref_link_about_bot\" = \"Share this link with your users to earn a {amount} commission on their spending in {app} {duration}.\";\n\"lng_star_ref_link_recipient\" = \"Commissions will be sent to:\";\n\"lng_star_ref_link_copy\" = \"Copy Link\";\n\"lng_star_ref_link_copy_none\" = \"No one have opened {app} through this link.\";\n\"lng_star_ref_link_copy_users#one\" = \"{count} user have opened {app} through this link.\";\n\"lng_star_ref_link_copy_users#other\" = \"{count} users have opened {app} through this link.\";\n\"lng_star_ref_link_copied_title\" = \"Link copied to clipboard\";\n\"lng_star_ref_link_copied_text\" = \"Share this link and earn {amount} of what people who use it spend in {app}!\";\n\"lng_star_ref_stopped\" = \"This affiliate link is no longer active.\";\n\"lng_star_ref_revoke_title\" = \"Revoke Link\";\n\"lng_star_ref_revoke_text\" = \"Are you sure you want to revoke the link of {bot}?\";\n\"lng_star_ref_revoked_title\" = \"Link removed\";\n\"lng_star_ref_revoked_text\" = \"It will no longer work.\";\n\n\"lng_stars_rating_title\" = \"Rating\";\n\"lng_stars_rating_future\" = \"Future Rating\";\n\"lng_stars_rating_updates#one\" = \"in {count} day\";\n\"lng_stars_rating_updates#other\" = \"in {count} days\";\n\"lng_stars_rating_pending#one\" = \"The rating will update {when}.\\n{count} point is pending. {link}\";\n\"lng_stars_rating_pending#other\" = \"The rating will update {when}.\\n{count} points are pending. {link}\";\n\"lng_stars_rating_pending_preview\" = \"Preview {arrow}\";\n\"lng_stars_rating_pending_back\" = \"Back {arrow}\";\n\"lng_stars_rating_negative_label\" = \"Negative Rating\";\n\"lng_stars_rating_negative\" = \"A negative rating indicates that **{name}'s** payments are unreliable.\";\n\"lng_stars_rating_negative_your#one\" = \"A negative rating indicates that your payments are unreliable. Spend **{count} Star** to fix this issue.\";\n\"lng_stars_rating_negative_your#other\" = \"A negative rating indicates that your payments are unreliable. Spend **{count} Stars** to fix this issue.\";\n\"lng_stars_rating_about\" = \"This rating reflects **{name}'s** activity on Telegram. What affects it:\";\n\"lng_stars_rating_about_your\" = \"This rating reflects your activity on Telegram. What affects it:\";\n\"lng_stars_title_gifts_telegram\" = \"Gifts from Telegram\";\n\"lng_stars_about_gifts_telegram\" = \"{emoji} 100% of the Stars spent on gifts purchased from Telegram.\";\n\"lng_stars_title_gifts_users\" = \"Gifts and Posts from Users\";\n\"lng_stars_about_gifts_users\" = \"{emoji} 20% of the Stars spent on resold gifts, paid messages and channel posts.\";\n\"lng_stars_title_refunds\" = \"Refunds and Conversions\";\n\"lng_stars_about_refunds\" = \"{emoji} 10x of refunded Stars and 85% of bought gifts converted to Stars.\";\n\"lng_stars_rating_added\" = \"Added\";\n\"lng_stars_rating_deducted\" = \"Deducted\";\n\"lng_stars_rating_understood\" = \"Understood\";\n\n\"lng_manage_discussion_group\" = \"Discussion\";\n\"lng_manage_discussion_group_add\" = \"Add a group\";\n\"lng_manage_linked_channel\" = \"Linked channel\";\n\"lng_manage_linked_channel_restore\" = \"Restore linked channel\";\n\"lng_manage_discussion_group_about\" = \"Select a group chat for discussion that will be displayed in your channel.\";\n\"lng_manage_discussion_group_about_chosen\" = \"A link to {group} is shown to all subscribers in the bottom panel.\";\n\"lng_manage_discussion_group_create\" = \"Create a new group\";\n\"lng_manage_discussion_group_unlink\" = \"Unlink group\";\n\"lng_manage_discussion_group_posted\" = \"Everything you post in the channel is forwarded to this group.\";\n\"lng_manage_discussion_group_sure\" = \"Do you want to make {group} the discussion board for {channel}?\";\n\"lng_manage_linked_channel_private\" = \"Any member of this group will be able to see messages in the channel.\";\n\"lng_manage_discussion_group_private\" = \"Anyone from the channel will be able to see messages in this group.\";\n\"lng_manage_discussion_group_link\" = \"Link group\";\n\"lng_manage_linked_channel_private_status\" = \"private channel\";\n\"lng_manage_discussion_group_private_status\" = \"private group\";\n\"lng_manage_linked_channel_about\" = \"This group is linked as the discussion board for {channel}.\";\n\"lng_manage_linked_channel_unlink\" = \"Unlink channel\";\n\"lng_manage_linked_channel_posted\" = \"All new posts from this channel are forwarded to the group.\";\n\"lng_manage_discussion_group_warning\" = \"\\\"Chat history for new members\\\" will be switched to **Visible**.\";\n\n\"lng_manage_monoforum\" = \"Direct Messages\";\n\"lng_manage_monoforum_off\" = \"Off\";\n\"lng_manage_monoforum_free\" = \"Free\";\n\"lng_manage_monoforum_allow\" = \"Allow Channel Messages\";\n\"lng_manage_monoforum_price\" = \"Price for each message\";\n\"lng_manage_monoforum_about\" = \"Allow users to send messages to your channel, with the option to charge a fee for each message.\";\n\"lng_manage_monoforum_price_about\" = \"Your channel will receive {percent} of the selected fee ({amount}) for each incoming message.\";\n\"lng_manage_monoforum_link_subtitle\" = \"Link to direct messages\";\n\n\"lng_manage_history_visibility_title\" = \"Chat history for new members\";\n\"lng_manage_history_visibility_shown\" = \"Visible\";\n\"lng_manage_history_visibility_shown_about\" = \"New members will see messages that were sent before they joined.\";\n\"lng_manage_history_visibility_hidden\" = \"Hidden\";\n\"lng_manage_history_visibility_hidden_about\" = \"New members won't see earlier messages.\";\n\"lng_manage_history_visibility_hidden_legacy\" = \"New members won't see more than 100 previous messages.\";\n\n\"lng_manage_messages_ttl_title\" = \"Auto-delete messages\";\n\"lng_manage_messages_ttl_disable\" = \"Disable\";\n\"lng_manage_messages_ttl_after1\" = \"1 day\";\n\"lng_manage_messages_ttl_after2\" = \"1 week\";\n\"lng_manage_messages_ttl_after3\" = \"1 month\";\n\"lng_manage_messages_ttl_after4\" = \"1 month\";\n\"lng_manage_messages_ttl_after_custom\" = \"Custom\";\n\"lng_manage_messages_ttl_menu\" = \"Auto-Delete\";\n\n\"lng_ttl_edit_about\" = \"Automatically delete new messages for you and {user} after a certain period of time.\";\n\"lng_ttl_edit_about_group\" = \"Automatically delete new messages sent in this chat after a certain period of time.\";\n\"lng_ttl_edit_about_channel\" = \"Automatically delete new messages sent in this channel after a certain period of time.\";\n\"lng_ttl_edit_about2\" = \"You can also set a default {link} for all your chats in Settings.\";\n\"lng_ttl_edit_about2_link\" = \"self-destruct timer\";\n\"lng_ttl_about_tooltip\" = \"New messages in this chat will be automatically deleted after {duration}.\";\n\"lng_ttl_about_tooltip_channel\" = \"New messages in this chat will be automatically deleted after {duration}.\";\n\"lng_ttl_about_tooltip_off\" = \"Auto-delete is now disabled.\";\n\n\"lng_report_title\" = \"Report channel\";\n\"lng_report_group_title\" = \"Report Group\";\n\"lng_report_bot_title\" = \"Report bot\";\n\"lng_report_message_title\" = \"Report message\";\n\"lng_report_profile_photo_title\" = \"Report profile photo\";\n\"lng_report_profile_video_title\" = \"Report profile photo\";\n\"lng_report_group_photo_title\" = \"Report group photo\";\n\"lng_report_group_video_title\" = \"Report group photo\";\n\"lng_report_channel_photo_title\" = \"Report channel photo\";\n\"lng_report_channel_video_title\" = \"Report channel photo\";\n\"lng_report_story\" = \"Report story\";\n\"lng_report_please_select_messages\" = \"Please select messages to report.\";\n\"lng_report_select_messages\" = \"Select messages\";\n\"lng_report_messages_none\" = \"Select Messages\";\n\"lng_report_messages_count#one\" = \"Report {count} Message\";\n\"lng_report_messages_count#other\" = \"Report {count} Messages\";\n\"lng_report_reaction\" = \"Report reaction\";\n\"lng_report_and_ban\" = \"Ban and report\";\n\"lng_report_reaction_title\" = \"Report reaction\";\n\"lng_report_reaction_about\" = \"Are you sure you want to report reactions from this user?\";\n\"lng_report_and_ban_button\" = \"Ban user\";\n\"lng_report_details_about\" = \"Please enter any additional details relevant to your report.\";\n\"lng_report_details\" = \"Additional Details\";\n\"lng_report_details_optional\" = \"Add Comment (Optional)\";\n\"lng_report_details_non_optional\" = \"Add Comment\";\n\"lng_report_details_message_about\" = \"Please help us by telling what is wrong with the message you have selected\";\n\"lng_report_reason_spam\" = \"Spam\";\n\"lng_report_reason_fake\" = \"Fake Account\";\n\"lng_report_reason_violence\" = \"Violence\";\n\"lng_report_reason_child_abuse\" = \"Child Abuse\";\n\"lng_report_reason_pornography\" = \"Pornography\";\n\"lng_report_reason_copyright\" = \"Copyright\";\n\"lng_report_reason_illegal_drugs\" = \"Illegal Drugs\";\n\"lng_report_reason_personal_details\" = \"Personal Details\";\n\"lng_report_reason_other\" = \"Other\";\n\"lng_report_button\" = \"Report\";\n\"lng_report_thanks\" = \"Thank you! Your report will be reviewed by our team.\";\n\n\"lng_report_sponsored_hidden\" = \"Sponsored messages will be hidden.\";\n\"lng_report_sponsored_reported\" = \"We will review this ad to ensure it matches our {link}.\";\n\"lng_report_sponsored_reported_link\" = \"Ad Policies and Guidelines\";\n\"lng_report_sponsored_reported_learn\" = \"Learn more about our {link}.\";\n\n\"lng_channel_add_members\" = \"Add members\";\n\"lng_channel_add_users\" = \"Add users\";\n\"lng_channel_add_removed\" = \"Remove user\";\n\"lng_channel_add_exception\" = \"Add exception\";\n\"lng_channel_admins\" = \"Administrators\";\n\"lng_channel_add_admin\" = \"Add Administrator\";\n\"lng_channel_admin_status_creator\" = \"Owner\";\n\"lng_channel_admin_status_promoted_by\" = \"Promoted by {user}\";\n\"lng_channel_admin_status_not_admin\" = \"not admin\";\n\"lng_channel_banned_status_restricted_by\" = \"Restricted by {user}\";\n\"lng_channel_banned_status_removed_by\" = \"Removed by {user}\";\n\n\"lng_channel_removed_list_about\" = \"Users removed from the channel by admins can't rejoin via invite links.\";\n\"lng_group_removed_list_about\" = \"Users removed from the group by admins can't rejoin via invite links.\";\n\n\"lng_participant_filter\" = \"Search\";\n\"lng_participant_invite\" = \"Add\";\n\"lng_participant_invite_history\" = \"Show the last 100 messages\";\n\"lng_participant_invite_sure\" = \"Are you sure you want to add **{user}** to **{group}**?\";\n\"lng_participant_invite_sure_many#one\" = \"Are you sure you want to add {count} member to **{group}**?\";\n\"lng_participant_invite_sure_many#other\" = \"Are you sure you want to add {count} members to **{group}**?\";\n\"lng_participant_invite_sorry#one\" = \"Sorry, you can only add the first {count} subscriber to a channel personally.\\n\\nFrom now on, people will need to join via your invite link.\";\n\"lng_participant_invite_sorry#other\" = \"Sorry, you can only add the first {count} subscribers to a channel personally.\\n\\nFrom now on, people will need to join via your invite link.\";\n\n\"lng_create_group_back\" = \"Back\";\n\"lng_create_group_next\" = \"Next\";\n\"lng_create_group_create\" = \"Create\";\n\"lng_create_group_title\" = \"New Group\";\n\"lng_create_channel_title\" = \"New Channel\";\n\"lng_create_public_channel_title\" = \"Public Channel\";\n\"lng_create_public_channel_about\" = \"Anyone can find the channel in search and join\";\n\"lng_create_private_channel_title\" = \"Private Channel\";\n\"lng_create_private_channel_about\" = \"Only people with an invite link can join.\";\n\"lng_create_public_group_title\" = \"Public Group\";\n\"lng_create_public_group_about\" = \"Anyone can find the group in search and join, chat history is available to everybody\";\n\"lng_create_private_group_title\" = \"Private Group\";\n\"lng_create_private_group_about\" = \"People can only join if they are added or have an invite link\";\n\"lng_create_permanent_link_title\" = \"Primary link\";\n\"lng_create_invite_link_title\" = \"Invite link\";\n\"lng_create_invite_link_about\" = \"People can be added, or join via invite link or \\\"Groups nearby\\\"\";\n\n\"lng_create_group_skip\" = \"Skip\";\n\n\"lng_create_channel_link_about\" = \"You can use a-z, 0-9 and underscores.\\nMinimum length is 5 characters.\";\n\n\"lng_create_channel_link_invalid\" = \"This link is invalid\";\n\"lng_create_channel_link_occupied\" = \"Sorry, this link is already occupied\";\n\"lng_create_channel_link_too_short\" = \"Sorry, this link is too short\";\n\"lng_create_channel_link_bad_symbols\" = \"Only 0-9, a-z, and underscores allowed.\";\n\"lng_create_channel_link_available\" = \"This link is available\";\n\"lng_create_channel_link_pending\" = \"Checking name...\";\n\"lng_create_channel_link_copied\" = \"Link copied to clipboard.\";\n\n\"lng_failed_add_participant\" = \"Could not add user. Please try again later.\";\n\"lng_failed_add_not_mutual\" = \"Sorry, if a person is no longer part of a group, you need to add each other to your respective contact lists to be able to add them back.\\n\\nNote that they could still join via the group's invite link as long as they are not in the Removed users list.\";\n\n\"lng_sure_delete_contact\" = \"Are you sure you want to delete {contact} from your contact list?\";\n\"lng_sure_delete_history\" = \"Are you sure you want to delete all message history with {contact}?\\n\\nThis action cannot be undone.\";\n\"lng_sure_delete_group_history\" = \"Are you sure you want to delete all messages in \\\"{group}\\\"?\\n\\nThis action cannot be undone.\";\n\"lng_sure_delete_channel_history\" = \"Are you sure you want to delete all messages in \\\"{channel}\\\"?\\n\\n**This action cannot be undone.**\";\n\"lng_sure_delete_and_exit\" = \"Are you sure you want to delete all message history and leave «{group}»?\\n\\nThis action cannot be undone.\";\n\"lng_sure_leave_channel\" = \"Are you sure you want to leave\\nthis channel?\";\n\"lng_sure_delete_channel\" = \"Are you sure you want to delete this channel? All subscribers will be removed and all messages will be lost.\";\n\"lng_sure_leave_group\" = \"Are you sure you want to leave this group?\";\n\"lng_sure_delete_group\" = \"Are you sure you want to delete this group? All members will be removed, and all messages will be lost.\";\n\"lng_sure_delete_saved_messages\" = \"Are you sure you want to delete all your saved messages?\\n\\nThis action cannot be undone.\";\n\"lng_no_clear_history_channel\" = \"In channels you can enable auto-delete for messages.\";\n\"lng_no_clear_history_group\" = \"In public groups you can enable auto-delete for messages.\";\n\"lng_sure_delete_by_date_one\" = \"Are you sure you want to delete all messages for **{date}**?\\n\\nThis action cannot be undone.\";\n\"lng_sure_delete_by_date_many\" = \"Are you sure you want to delete all messages for the **{days}**?\\n\\nThis action cannot be undone.\";\n\"lng_sure_delete_selected_days#one\" = \"{count} selected day\";\n\"lng_sure_delete_selected_days#other\" = \"{count} selected days\";\n\n\"lng_message_empty\" = \"Empty Message\";\n\"lng_message_unsupported\" = \"This message is not supported by your version of Telegram. Please update to the latest version in Settings > Advanced, or install it from {link}. If you are already using the latest version, this message might depend on a feature that is not yet implemented.\";\n\"lng_message_not_found\" = \"Message doesn't exist.\";\n\n\"lng_duration_minsec_minutes#one\" = \"{count} min\";\n\"lng_duration_minsec_minutes#other\" = \"{count} min\";\n\"lng_duration_minsec_seconds#one\" = \"{count} s\";\n\"lng_duration_minsec_seconds#other\" = \"{count} s\";\n\"lng_duration_minutes_seconds\" = \"{minutes_count} {seconds_count}\";\n\n\"lng_action_invite_user\" = \"{from} invited {user} to {chat}\";\n\"lng_action_invite_users_many\" = \"{from} invited {users} to {chat}\";\n\"lng_action_invite_user_chat\" = \"the video chat\";\n\"lng_action_invite_users_and_one\" = \"{accumulated}, {user}\";\n\"lng_action_invite_users_and_last\" = \"{accumulated} and {user}\";\n\"lng_action_group_call_started_group\" = \"{from} started a video chat\";\n\"lng_action_group_call_started_channel\" = \"Live stream started\";\n\"lng_action_group_call_scheduled_group\" = \"{from} scheduled a video chat for {date}\";\n\"lng_action_group_call_scheduled_channel\" = \"Live stream scheduled for {date}\";\n\"lng_action_group_call_finished\" = \"Live stream finished ({duration})\";\n\"lng_action_group_call_finished_group\" = \"{from} ended the video chat ({duration})\";\n\"lng_action_add_user\" = \"{from} added {user}\";\n\"lng_action_add_users_many\" = \"{from} added {users}\";\n\"lng_action_add_users_and_one\" = \"{accumulated}, {user}\";\n\"lng_action_add_users_and_last\" = \"{accumulated} and {user}\";\n\"lng_action_add_you\" = \"{from} added you to this channel\";\n\"lng_action_you_joined\" = \"You joined this channel\";\n\"lng_action_add_you_group\" = \"{from} added you to this group\";\n\"lng_action_kick_user\" = \"{from} removed {user}\";\n\"lng_action_user_left\" = \"{from} left the group\";\n\"lng_action_user_joined\" = \"{from} joined the group\";\n\"lng_action_user_joined_by_link\" = \"{from} joined the group via invite link\";\n\"lng_action_user_joined_by_request\" = \"{from} was accepted to the group\";\n\"lng_action_you_joined_by_request\" = \"Your request to join the group was approved\";\n\"lng_action_you_joined_by_request_channel\" = \"Your request to join the channel was approved\";\n\"lng_action_user_registered\" = \"{from} joined Telegram\";\n\"lng_action_removed_photo\" = \"{from} removed group photo\";\n\"lng_action_removed_photo_channel\" = \"Channel photo removed\";\n\"lng_action_changed_photo\" = \"{from} updated group photo\";\n\"lng_action_changed_photo_channel\" = \"Channel photo updated\";\n\"lng_action_changed_title\" = \"{from} changed group name to «{title}»\";\n\"lng_action_changed_title_channel\" = \"Channel name was changed to «{title}»\";\n\"lng_action_created_chat\" = \"{from} created the group «{title}»\";\n\"lng_action_created_monoforum\" = \"Direct messages were enabled in this channel.\";\n\"lng_action_ttl_changed\" = \"{from} set messages to auto-delete in {duration}\";\n\"lng_action_ttl_changed_you\" = \"You set messages to auto-delete in {duration}\";\n\"lng_action_ttl_changed_channel\" = \"Messages in this channel will be automatically deleted after {duration}\";\n\"lng_action_ttl_global\" = \"{from} uses a self-destruct timer for all chats. All new messages in this chat will be automatically deleted after {duration} they are sent.\";\n\"lng_action_ttl_global_me\" = \"You set a self-destruct timer for all chats. All new messages in this chat will be automatically deleted after {duration} they’ve been sent.\";\n\"lng_action_ttl_removed\" = \"{from} disabled the auto-delete timer\";\n\"lng_action_ttl_removed_you\" = \"You disabled the auto-delete timer\";\n\"lng_action_ttl_removed_channel\" = \"Messages in this channel will no longer be automatically deleted\";\n\"lng_action_created_channel\" = \"Channel created\";\n\"lng_action_pinned_message\" = \"{from} pinned \\\"{text}\\\"\";\n\"lng_action_pinned_media\" = \"{from} pinned {media}\";\n\"lng_action_pinned_media_photo\" = \"a photo\";\n\"lng_action_pinned_media_video\" = \"a video\";\n\"lng_action_pinned_media_audio\" = \"an audio file\";\n\"lng_action_pinned_media_voice\" = \"a voice message\";\n\"lng_action_pinned_media_video_message\" = \"a video message\";\n\"lng_action_pinned_media_file\" = \"a file\";\n\"lng_action_pinned_media_gif\" = \"a GIF\";\n\"lng_action_pinned_media_contact\" = \"a contact information\";\n\"lng_action_pinned_media_location\" = \"a location mark\";\n\"lng_action_pinned_media_sticker\" = \"a sticker\";\n\"lng_action_pinned_media_emoji_sticker\" = \"a {emoji} sticker\";\n\"lng_action_pinned_media_game\" = \"the game «{game}»\";\n\"lng_action_pinned_media_story\" = \"a story\";\n\"lng_action_game_score#one\" = \"{from} scored {count} in {game}\";\n\"lng_action_game_score#other\" = \"{from} scored {count} in {game}\";\n\"lng_action_game_you_scored#one\" = \"You scored {count} in {game}\";\n\"lng_action_game_you_scored#other\" = \"You scored {count} in {game}\";\n\"lng_action_game_score_no_game#one\" = \"{from} scored {count}\";\n\"lng_action_game_score_no_game#other\" = \"{from} scored {count}\";\n\"lng_action_game_you_scored_no_game#one\" = \"You scored {count}\";\n\"lng_action_game_you_scored_no_game#other\" = \"You scored {count}\";\n\"lng_action_payment_done\" = \"You successfully transferred {amount} to {user}\";\n\"lng_action_payment_done_for\" = \"You successfully transferred {amount} to {user} for {invoice}\";\n\"lng_action_payment_init_recurring_for\" = \"You successfully transferred {amount} to {user} for {invoice} and allowed future recurring payments\";\n\"lng_action_payment_init_recurring\" = \"You successfully transferred {amount} to {user} and allowed future recurring payments\";\n\"lng_action_payment_used_recurring\" = \"You were charged {amount} via recurring payment\";\n\"lng_action_payment_bot_done\" = \"Bot connected to this account received {amount}\";\n\"lng_action_payment_bot_recurring\" = \"Bot connected to this account received {amount} via recurring payment\";\n\"lng_action_took_screenshot\" = \"{from} took a screenshot!\";\n\"lng_action_you_took_screenshot\" = \"You took a screenshot!\";\n\"lng_action_bot_allowed_from_domain\" = \"You allowed this bot to message you when you logged in on {domain}.\";\n\"lng_action_bot_allowed_from_app\" = \"You allowed this bot to message you when you opened {app}.\";\n\"lng_action_secure_values_sent\" = \"{user} received the following documents: {documents}\";\n\"lng_action_secure_personal_details\" = \"personal details\";\n\"lng_action_secure_proof_of_identity\" = \"proof of identity\";\n\"lng_action_secure_address\" = \"address\";\n\"lng_action_secure_proof_of_address\" = \"proof of address\";\n\"lng_action_secure_phone\" = \"phone number\";\n\"lng_action_secure_email\" = \"email address\";\n\"lng_action_proximity_reached\" = \"{from} is now within {distance} from {user}\";\n\"lng_action_proximity_reached_you\" = \"{from} is now within {distance} from you\";\n\"lng_action_you_proximity_reached\" = \"You are now within {distance} from {user}\";\n\"lng_action_you_theme_changed\" = \"You changed the chat theme to {emoji}\";\n\"lng_action_theme_changed\" = \"{from} changed the chat theme to {emoji}\";\n\"lng_action_you_gift_theme_changed\" = \"You set {name} as a new theme for this chat.\";\n\"lng_action_gift_theme_changed\" = \"{from} set {name} as a new theme for this chat.\";\n\"lng_action_you_theme_disabled\" = \"You disabled the chat theme\";\n\"lng_action_theme_disabled\" = \"{from} disabled the chat theme\";\n\"lng_action_proximity_distance_m#one\" = \"{count} meter\";\n\"lng_action_proximity_distance_m#other\" = \"{count} meters\";\n\"lng_action_proximity_distance_km#one\" = \"{count} km\";\n\"lng_action_proximity_distance_km#other\" = \"{count} km\";\n\"lng_action_webview_data_done\" = \"Data from the \\\"{text}\\\" button was transferred to the bot.\";\n\"lng_action_gift_received\" = \"{user} sent you a gift for {cost}\";\n\"lng_action_gift_received_sold\" = \"{user} sold you a gift for {cost}\";\n\"lng_action_gift_unique_received\" = \"{user} sent you a unique collectible item\";\n\"lng_action_gift_sent\" = \"You sent a gift for {cost}\";\n\"lng_action_gift_sent_sold\" = \"You sold a gift for {cost}\";\n\"lng_action_gift_unique_sent\" = \"You sent a unique collectible item\";\n\"lng_action_gift_upgraded\" = \"{user} turned the gift from you into a unique collectible\";\n\"lng_action_gift_upgraded_channel\" = \"{user} turned this gift to {channel} into a unique collectible\";\n\"lng_action_gift_upgraded_self_channel\" = \"You turned this gift to {channel} into a unique collectible\";\n\"lng_action_gift_upgraded_mine\" = \"You turned the gift from {user} into a unique collectible\";\n\"lng_action_gift_upgraded_self\" = \"You turned this gift into a unique collectible\";\n\"lng_action_gift_sent_upgrade_other\" = \"{from} sent an upgrade worth {cost} for the gift you received from {user}.\";\n\"lng_action_gift_sent_upgrade_self_other\" = \"You sent an upgrade worth {cost} for the gift {name} received from {user}.\";\n\"lng_action_gift_sent_upgrade\" = \"{from} sent an upgrade worth {cost} for your gift.\";\n\"lng_action_gift_sent_upgrade_self\" = \"You sent an upgrade worth {cost} for this gift.\";\n\"lng_action_gift_sent_upgrade_self_channel\" = \"You sent an upgrade worth {cost} for your gift to {name}.\";\n\"lng_action_gift_upgraded_helped\" = \"{user} unpacked the gift that you helped to upgrade.\";\n\"lng_action_gift_upgraded_helped_self\" = \"You unpacked the gift that {user} helped to upgrade.\";\n\"lng_action_gift_transferred\" = \"{user} transferred you a gift\";\n\"lng_action_gift_transferred_channel\" = \"{user} transferred a gift to {channel}\";\n\"lng_action_gift_transferred_unknown\" = \"Someone transferred you a gift\";\n\"lng_action_gift_transferred_unknown_channel\" = \"Someone transferred a gift to {channel}\";\n\"lng_action_gift_transferred_self\" = \"You transferred a unique collectible\";\n\"lng_action_gift_displayed_self\" = \"You've started displaying {name} on your Telegram profile page.\";\n\"lng_action_gift_transferred_self_channel\" = \"You transferred a gift to {channel}\";\n\"lng_action_gift_transferred_mine\" = \"You transferred a gift to {user}\";\n\"lng_action_gift_crafted\" = \"You crafted a new gift\";\n\"lng_action_gift_received_anonymous\" = \"Unknown user sent you a gift for {cost}\";\n\"lng_action_gift_sent_channel\" = \"{user} sent a gift to {name} for {cost}\";\n\"lng_action_gift_sent_self_channel\" = \"You sent a gift to {name} for {cost}\";\n\"lng_action_gift_self_bought\" = \"You bought a gift for {cost}\";\n\"lng_action_gift_self_auction\" = \"You've successfully bought a gift in the auction for {cost}.\";\n\"lng_action_gift_auction_won\" = \"You won the auction with a bid of {cost}.\";\n\"lng_action_gift_self_subtitle\" = \"Saved Gift\";\n\"lng_action_gift_crafted_subtitle\" = \"Crafted Gift\";\n\"lng_action_gift_self_about#one\" = \"Display this gift on your page or convert it to **{count}** Star.\";\n\"lng_action_gift_self_about#other\" = \"Display this gift on your page or convert it to **{count}** Stars.\";\n\"lng_action_gift_self_about_unique\" = \"You can display this gift on your page or turn it into unique collectible and send to others.\";\n\"lng_action_gift_channel_about#one\" = \"Display this gift in channel's Gifts or convert it to **{count}** Star.\";\n\"lng_action_gift_channel_about#other\" = \"Display this gift in channel's Gifts or convert it to **{count}** Stars.\";\n\"lng_action_gift_channel_about_unique\" = \"You can display this gift in channel's Gifts or turn it into unique collectible.\";\n\"lng_action_gift_for_stars#one\" = \"{count} Star\";\n\"lng_action_gift_for_stars#other\" = \"{count} Stars\";\n\"lng_action_gift_for_ton#one\" = \"{count} TON\";\n\"lng_action_gift_for_ton#other\" = \"{count} TON\";\n\"lng_action_gift_got_subtitle\" = \"Gift from {user}\";\n\"lng_action_gift_got_stars_text#one\" = \"Display this gift on your page or convert it to **{count}** Star.\";\n\"lng_action_gift_got_stars_text#other\" = \"Display this gift on your page or convert it to **{count}** Stars.\";\n\"lng_action_gift_got_upgradable_text\" = \"Upgrade this gift to a unique collectible.\";\n\"lng_action_gift_got_gift_text\" = \"You can keep this gift on your page.\";\n\"lng_action_gift_can_remove_text\" = \"You can remove this gift from your page.\";\n\"lng_action_gift_got_gift_channel\" = \"You can keep this gift in channel's Gifts.\";\n\"lng_action_gift_can_remove_channel\" = \"You can remove this gift from channel's Gifts.\";\n\"lng_action_gift_sent_subtitle\" = \"Gift for {user}\";\n\"lng_action_gift_sent_text#one\" = \"{user} can display this gift on their page or convert it to {count} Star.\";\n\"lng_action_gift_sent_text#other\" = \"{user} can display this gift on their page or convert it to {count} Stars.\";\n\"lng_action_gift_sent_upgradable\" = \"{user} can upgrade this gift to a unique collectible.\";\n\"lng_action_gift_premium_months#one\" = \"{count} Month Premium\";\n\"lng_action_gift_premium_months#other\" = \"{count} Months Premium\";\n\"lng_action_gift_premium_about\" = \"Subscription for exclusive Telegram features.\";\n\"lng_action_gift_refunded\" = \"This gift was downgraded because a request to refund the payment related to this gift was made, and the money was returned.\";\n\"lng_action_gift_got_ton\" = \"Use TON to suggest posts to channels.\";\n\"lng_action_gift_offer_incoming\" = \"An offer to buy this gift for {amount}.\";\n\"lng_action_gift_offer_you\" = \"You offered {cost} for {name}.\";\n\"lng_action_gift_offer_state_expires\" = \"This offer expires in {time}.\";\n\"lng_action_gift_offer_time_large\" = \"{hours} h\";\n\"lng_action_gift_offer_time_medium\" = \"{hours} h {minutes} m\";\n\"lng_action_gift_offer_time_small\" = \"{minutes} m\";\n\"lng_action_gift_offer_state_accepted\" = \"This offer was accepted.\";\n\"lng_action_gift_offer_state_rejected\" = \"This offer was rejected.\";\n\"lng_action_gift_offer_state_expired\" = \"This offer has expired.\";\n\"lng_action_gift_offer_sold\" = \"{user} sold {name} for {cost}.\";\n\"lng_action_gift_offer_sold_you\" = \"You sold {name} for {cost}.\";\n\"lng_action_gift_offer_decline\" = \"Reject\";\n\"lng_action_gift_offer_accept\" = \"Accept\";\n\"lng_action_gift_offer_expired\" = \"The offer from {user} to buy your {name} for {cost} has expired.\";\n\"lng_action_gift_offer_expired_your\" = \"Your offer to buy {name} for {cost} has expired.\";\n\"lng_action_gift_offer_declined\" = \"{user} rejected your offer to buy {name} for {cost}.\";\n\"lng_action_gift_offer_declined_you\" = \"You rejected {user}'s offer to buy your {name} for {cost}.\";\n\"lng_action_suggested_photo_me\" = \"You suggested this photo for {user}'s Telegram profile.\";\n\"lng_action_suggested_photo\" = \"{user} suggests this photo for your Telegram profile.\";\n\"lng_action_suggested_photo_button\" = \"View Photo\";\n\"lng_action_suggested_video_me\" = \"You suggested this photo for {user}'s Telegram profile.\";\n\"lng_action_suggested_video\" = \"{user} suggests this photo for your Telegram profile.\";\n\"lng_action_suggested_video_button\" = \"View Photo\";\n\"lng_action_suggested_birthday_me\" = \"You suggest {user} add a date of birth:\";\n\"lng_action_suggested_birthday\" = \"{user} suggests you add your date of birth:\";\n\"lng_action_suggested_birtday_button\" = \"View\";\n\"lng_action_attach_menu_bot_allowed\" = \"You allowed this bot to message you when you added it to your attachment menu.\";\n\"lng_action_webapp_bot_allowed\" = \"You allowed this bot to message you in its web-app.\";\n\"lng_action_set_wallpaper_me\" = \"You set a new wallpaper for this chat\";\n\"lng_action_set_wallpaper\" = \"{user} set a new wallpaper for this chat\";\n\"lng_action_set_wallpaper_both_me\" = \"You set a new wallpaper for {user} and you.\";\n\"lng_action_set_wallpaper_button\" = \"View Wallpaper\";\n\"lng_action_set_wallpaper_remove\" = \"Remove\";\n\"lng_action_set_same_wallpaper_me\" = \"You set the same wallpaper as your chat partner\";\n\"lng_action_set_same_wallpaper\" = \"{user} set the same wallpaper for this chat\";\n\"lng_action_topic_created_inside\" = \"Topic created\";\n\"lng_action_topic_closed_inside\" = \"Topic closed\";\n\"lng_action_topic_reopened_inside\" = \"Topic reopened\";\n\"lng_action_topic_closed_inside_by\" = \"{from} closed the topic\";\n\"lng_action_topic_reopened_inside_by\" = \"{from} reopened the topic\";\n\"lng_action_topic_hidden_inside\" = \"Topic hidden\";\n\"lng_action_topic_unhidden_inside\" = \"Topic unhidden\";\n\"lng_action_topic_created\" = \"The topic \\\"{topic}\\\" was created\";\n\"lng_action_topic_closed\" = \"\\\"{topic}\\\" was closed\";\n\"lng_action_topic_reopened\" = \"\\\"{topic}\\\" was reopened\";\n\"lng_action_topic_closed_by\" = \"{from} closed \\\"{topic}\\\"\";\n\"lng_action_topic_reopened_by\" = \"{from} reopened \\\"{topic}\\\"\";\n\"lng_action_topic_hidden\" = \"\\\"{topic}\\\" was hidden\";\n\"lng_action_topic_unhidden\" = \"\\\"{topic}\\\" was unhidden\";\n\"lng_action_topic_placeholder\" = \"topic\";\n\"lng_action_topic_bot_thread\" = \"thread\";\n\"lng_action_topic_renamed\" = \"{from} renamed the {link} to \\\"{title}\\\"\";\n\"lng_action_topic_icon_changed\" = \"{from} changed the {link} icon to {emoji}\";\n\"lng_action_topic_icon_removed\" = \"{from} removed the {link} icon\";\n\"lng_action_shared_chat_with_bot\" = \"You shared {chat} with {bot}\";\n\"lng_action_story_mention_me\" = \"You mentioned {user} in a story\";\n\"lng_action_story_mention\" = \"{user} mentioned you in a story\";\n\"lng_action_story_mention_button\" = \"View Story\";\n\"lng_action_story_mention_me_unavailable\" = \"The story where you mentioned {user} is no longer available.\";\n\"lng_action_story_mention_unavailable\" = \"The story where {user} mentioned you is no longer available.\";\n\"lng_action_giveaway_started_group\" = \"{from} just started a giveaway of Telegram Premium subscriptions to its members.\";\n\"lng_action_giveaway_started\" = \"{from} just started a giveaway of Telegram Premium subscriptions for its followers.\";\n\"lng_action_giveaway_credits_started_amount#one\" = \"{count} Star\";\n\"lng_action_giveaway_credits_started_amount#other\" = \"{count} Stars\";\n\"lng_action_giveaway_credits_started_group\" = \"{from} just started a giveaway of {amount} to its members.\";\n\"lng_action_giveaway_credits_started\" = \"{from} just started a giveaway of {amount} to its followers.\";\n\"lng_action_giveaway_results#one\" = \"{count} winner of the giveaway was randomly selected by Telegram and received private messages with giftcodes.\";\n\"lng_action_giveaway_results#other\" = \"{count} winners of the giveaway were randomly selected by Telegram and received private messages with giftcodes.\";\n\"lng_action_giveaway_results_some\" = \"Some winners of the giveaway were randomly selected by Telegram and received private messages with giftcodes.\";\n\"lng_action_giveaway_results_credits#one\" = \"{count} winner of the giveaway was randomly selected by Telegram and received their prize.\";\n\"lng_action_giveaway_results_credits#other\" = \"{count} winners of the giveaway were randomly selected by Telegram and received their prize.\";\n\"lng_action_giveaway_results_credits_some\" = \"Some winners of the giveaway were randomly selected by Telegram and received their prize.\";\n\"lng_action_giveaway_results_none\" = \"No winners of the giveaway could be selected.\";\n\"lng_action_boost_apply_me\" = \"You boosted the group\";\n\"lng_action_boost_apply#one\" = \"{from} boosted the group\";\n\"lng_action_boost_apply#other\" = \"{from} boosted the group {count} times\";\n\"lng_action_set_chat_intro\" = \"{from} added the message below for all empty chats. How?\";\n\"lng_action_payment_refunded\" = \"{peer} refunded {amount}\";\n\"lng_action_paid_message_sent#one\" = \"You paid {count} Star to {action}\";\n\"lng_action_paid_message_sent#other\" = \"You paid {count} Stars to {action}\";\n\"lng_action_paid_message_one\" = \"send a message\";\n\"lng_action_paid_message_some#one\" = \"send {count} message\";\n\"lng_action_paid_message_some#other\" = \"send {count} messages\";\n\"lng_action_paid_message_got#one\" = \"You received {count} Star from {name}\";\n\"lng_action_paid_message_got#other\" = \"You received {count} Stars from {name}\";\n\"lng_action_paid_message_refund#one\" = \"{from} refunded {count} Star to you\";\n\"lng_action_paid_message_refund#other\" = \"{from} refunded {count} Stars to you\";\n\"lng_action_paid_message_refund_self#one\" = \"You refunded {count} Star to {name}\";\n\"lng_action_paid_message_refund_self#other\" = \"You refunded {count} Stars to {name}\";\n\"lng_action_message_price_free\" = \"Messages are now free in this group.\";\n\"lng_action_message_price_paid#one\" = \"Messages now cost {count} Star each in this group.\";\n\"lng_action_message_price_paid#other\" = \"Messages now cost {count} Stars each in this group.\";\n\"lng_action_direct_messages_enabled\" = \"Channel enabled Direct Messages.\";\n\"lng_action_direct_messages_paid#one\" = \"Channel allows Direct Messages for {count} Star each.\";\n\"lng_action_direct_messages_paid#other\" = \"Channel allows Direct Messages for {count} Stars each.\";\n\"lng_action_direct_messages_disabled\" = \"Channel disabled Direct Messages.\";\n\"lng_action_todo_marked_done\" = \"{from} marked {tasks} as done.\";\n\"lng_action_todo_marked_done_self\" = \"You marked {tasks} as done.\";\n\"lng_action_todo_marked_not_done\" = \"{from} marked {tasks} as not done.\";\n\"lng_action_todo_marked_not_done_self\" = \"You marked {tasks} as not done.\";\n\"lng_action_todo_added\" = \"{from} added {tasks} to the list.\";\n\"lng_action_todo_added_self\" = \"You added {tasks} to the list.\";\n\"lng_action_poll_added_answer\" = \"{from} added \\\"{option}\\\" to the poll.\";\n\"lng_action_poll_added_answer_self\" = \"You added \\\"{option}\\\" to the poll.\";\n\"lng_action_poll_deleted_answer\" = \"{from} removed \\\"{option}\\\" from the poll.\";\n\"lng_action_poll_deleted_answer_self\" = \"You removed \\\"{option}\\\" from the poll.\";\n\"lng_action_todo_tasks_fallback#one\" = \"task\";\n\"lng_action_todo_tasks_fallback#other\" = \"{count} tasks\";\n\"lng_action_todo_tasks_and_one\" = \"{tasks}, {task}\";\n\"lng_action_todo_tasks_and_last\" = \"{tasks} and {task}\";\n\"lng_action_suggest_success_stars#one\" = \"{from} has received {count} Star for publishing post.\";\n\"lng_action_suggest_success_stars#other\" = \"{from} has received {count} Stars for publishing post.\";\n\"lng_action_suggest_success_ton#one\" = \"{from} has received {count} TON for publishing post.\";\n\"lng_action_suggest_success_ton#other\" = \"{from} has received {count} TON for publishing post.\";\n\"lng_action_suggest_refund_user\" = \"User refunded the Stars so that post was deleted.\";\n\"lng_action_suggest_refund_admin\" = \"Admin deleted the post early so that the price was refunded to the user.\";\n\"lng_action_post_rejected\" = \"The post was rejected.\";\n\"lng_action_not_enough_funds\" = \"Transaction failed.\";\n\"lng_you_paid_stars#one\" = \"You paid {count} Star.\";\n\"lng_you_paid_stars#other\" = \"You paid {count} Stars.\";\n\"lng_action_stake_game_nothing\" = \"{from} didn't win anything\";\n\"lng_action_stake_game_nothing_you\" = \"You didn't win anything\";\n\"lng_action_stake_game_won\" = \"{from} won {amount}\";\n\"lng_action_stake_game_won_you\" = \"You won {amount}\";\n\"lng_action_stake_game_lost\" = \"{from} lost {amount}\";\n\"lng_action_stake_game_lost_you\" = \"You lost {amount}\";\n\"lng_action_change_creator\" = \"{from} made {user} the new main admin of the group.\";\n\"lng_action_new_creator_pending\" = \"{user} will become the new main admin in 7 days if {from} does not return.\";\n\"lng_action_managed_bot_created\" = \"{from} created a bot {bot}.\";\n\n\"lng_create_bot_title\" = \"Create Bot\";\n\"lng_create_bot_subtitle\" = \"{bot} would like to create and manage a chatbot on your behalf.\";\n\"lng_create_bot_name_placeholder\" = \"Bot Name\";\n\"lng_create_bot_username_placeholder\" = \"Bot Username\";\n\"lng_create_bot_username_available\" = \"{username} is available.\";\n\"lng_create_bot_username_link\" = \"Link: {link}\";\n\"lng_create_bot_username_taken\" = \"This username is already taken.\";\n\"lng_create_bot_username_bad_symbols\" = \"Username can only contain a-z, 0-9, and underscores.\";\n\"lng_create_bot_username_too_short\" = \"Username must be at least 5 characters.\";\n\"lng_create_bot_button\" = \"Create\";\n\"lng_managed_bot_label\" = \"{icon} Created and managed by {bot}.\";\n\"lng_managed_bot_ready\" = \"**{name}** is ready!\\n\\nClick **Start** below to test your new chatbot. Its behavior is defined by **{parent}**.\";\n\"lng_managed_bot_created_title\" = \"{name} created!\";\n\"lng_managed_bot_created_text\" = \"{parent_name} will manage this bot for you.\";\n\"lng_managed_bot_edit_photo\" = \"You can edit your bot's profile picture {link}\";\n\"lng_managed_bot_edit_photo_link\" = \"here {arrow}\";\n\"lng_managed_bot_set_photo\" = \"Set Profile Photo\";\n\n\"lng_create_bot_no_manage\" = \"{bot} doesn't have Bot Management Mode enabled.\";\n\n\"lng_bots_create_limit#one\" = \"Subscribe to {link} to create up to {premium_count} bots, or delete one of your **{count}** bot via {bot}.\";\n\"lng_bots_create_limit#other\" = \"Subscribe to {link} to create up to {premium_count} bots, or delete one of your **{count}** bots via {bot}.\";\n\"lng_bots_create_limit_link\" = \"Premium\";\n\"lng_bots_create_limit_final#one\" = \"You can create up to **{count}** bot. Delete your current ones via {bot}.\";\n\"lng_bots_create_limit_final#other\" = \"You can create up to **{count}** bots. Delete your current ones via {bot}.\";\n\n\"lng_stake_game_title\" = \"Emoji Stake\";\n\"lng_stake_game_beta\" = \"Beta\";\n\"lng_stake_game_about\" = \"A limited play-test of the upcoming emoji mini-game platform for a small group of users.\";\n\"lng_stake_game_results\" = \"Results and Returns\";\n\"lng_stake_game_resets\" = \"A streak resets after 3 {emoji} or a stake change.\";\n\"lng_stake_game_your\" = \"Your stake\";\n\"lng_stake_game_save_and_roll\" = \"Save and Roll\";\n\n\"lng_you_joined_group\" = \"You joined this group\";\n\n\"lng_similar_channels_title\" = \"Similar channels\";\n\"lng_similar_channels_view_all\" = \"View all\";\n\"lng_similar_channels_more\" = \"More Channels\";\n\"lng_similar_channels_premium_all#one\" = \"Subscribe to {link} to unlock up to **{count}** similar channel.\";\n\"lng_similar_channels_premium_all#other\" = \"Subscribe to {link} to unlock up to **{count}** similar channels.\";\n\"lng_similar_channels_premium_all_link\" = \"Telegram Premium\";\n\"lng_similar_channels_show_more\" = \"Show more channels\";\n\n\"lng_similar_bots_title\" = \"Similar bots\";\n\"lng_similar_bots_premium_all#one\" = \"Subscribe to {link} to unlock up to **{count}** similar bot.\";\n\"lng_similar_bots_premium_all#other\" = \"Subscribe to {link} to unlock up to **{count}** similar bots.\";\n\"lng_similar_bots_show_more\" = \"Show more bots\";\n\n\"lng_peer_gifts_title\" = \"Gifts\";\n\"lng_peer_gifts_about\" = \"These gifts were sent to {user} by other users.\";\n\"lng_peer_gifts_about_mine\" = \"These gifts were sent to you by other users. Click on a gift to convert it to Stars or change its privacy settings.\";\n\"lng_peer_gifts_empty_search\" = \"No matching gifts\";\n\"lng_peer_gifts_view_all\" = \"View All Gifts\";\n\"lng_peer_gifts_notify\" = \"Notify About New Gifts\";\n\"lng_peer_gifts_notify_enabled\" = \"You will receive a message from Telegram when your channel receives a gift.\";\n\"lng_peer_gifts_filter_by_value\" = \"Sort by Value\";\n\"lng_peer_gifts_filter_by_date\" = \"Sort by Date\";\n\"lng_peer_gifts_filter_unlimited\" = \"Unlimited\";\n\"lng_peer_gifts_filter_upgradable\" = \"Upgradeable\";\n\"lng_peer_gifts_filter_limited\" = \"Limited\";\n\"lng_peer_gifts_filter_unique\" = \"Unique\";\n\"lng_peer_gifts_filter_saved\" = \"Displayed\";\n\"lng_peer_gifts_filter_unsaved\" = \"Hidden\";\n\n\"lng_premium_gift_duration_days#one\" = \"for {count} day\";\n\"lng_premium_gift_duration_days#other\" = \"for {count} days\";\n\"lng_premium_gift_duration_months#one\" = \"for {count} month\";\n\"lng_premium_gift_duration_months#other\" = \"for {count} months\";\n\"lng_premium_gift_duration_years#one\" = \"for {count} year\";\n\"lng_premium_gift_duration_years#other\" = \"for {count} years\";\n\n\"lng_ttl_photo_received\" = \"{from} sent you a self-destructing photo. Please view it on your mobile.\";\n\"lng_ttl_photo_sent\" = \"You sent a self-destructing photo.\";\n\"lng_ttl_photo_expired\" = \"Expired photo\";\n\"lng_ttl_video_received\" = \"{from} sent you a self-destructing video. Please view it on your mobile.\";\n\"lng_ttl_video_sent\" = \"You sent a self-destructing video.\";\n\"lng_ttl_video_expired\" = \"Expired video\";\n\"lng_ttl_voice_sent\" = \"You sent a self-destructing voice message.\";\n\"lng_ttl_voice_expired\" = \"Voice message expired\";\n\"lng_ttl_round_sent\" = \"You sent a self-destructing video message.\";\n\"lng_ttl_round_expired\" = \"Round message expired\";\n\"lng_ttl_voice_tooltip_in\" = \"This voice message can only be played once.\";\n\"lng_ttl_voice_tooltip_out\" = \"This message will disappear after **{user}** plays it once.\";\n\"lng_ttl_voice_close_in\" = \"Delete and close\";\n\"lng_ttl_round_tooltip_in\" = \"This video message can only be played once.\";\n\"lng_ttl_round_tooltip_out\" = \"This message will disappear after **{user}** plays it once.\";\n\n\"lng_profile_add_more_after_create\" = \"You will be able to add more members after you create the group.\";\n\"lng_profile_camera_title\" = \"Capture yourself\";\n\n\"lng_channel_not_accessible\" = \"Sorry, this channel is not accessible.\";\n\"lng_group_not_accessible\" = \"Sorry, this group is not accessible.\";\n\"lng_group_full\" = \"Sorry, this group is full.\";\n\n\"lng_channels_too_much_public_revoke_confirm_group\" = \"Are you sure you want to revoke the link {link}?\\n\\nThe group «{group}» will become private.\";\n\"lng_channels_too_much_public_revoke_confirm_channel\" = \"Are you sure you want to revoke the link {link}?\\n\\nThe channel «{group}» will become private.\";\n\"lng_channels_too_much_public_revoke\" = \"Revoke\";\n\"lng_channels_too_much_public_other\" = \"Sorry, the target user has too many public groups or channels already. Please ask them to make one of their existing groups or channels private first.\";\n\"lng_channels_too_much_located_other\" = \"Sorry, the target user has too many location-based groups already. Please ask them to delete or transfer one of their existing ones first.\";\n\n\"lng_group_invite_bad_link\" = \"This invite link is invalid or has expired.\";\n\n\"lng_group_invite_members#one\" = \"{count} member, among them:\";\n\"lng_group_invite_members#other\" = \"{count} members, among them:\";\n\"lng_channel_invite_private\" = \"This channel is private.\\nPlease join it to continue viewing its content.\";\n\n\"lng_channel_invite_subscription_button\" = \"Subscribe\";\n\"lng_channel_invite_subscription_title\" = \"Subscribe to the Channel\";\n\"lng_channel_invite_subscription_about\" = \"Do you want to subscribe for {channel} for {price} per month?\";\n\"lng_channel_invite_subscription_terms\" = \"By subscribing you agree to the {link}.\";\n\n\"lng_group_invite_create\" = \"Create an invite link\";\n\"lng_group_invite_about_new\" = \"Your previous link will be deactivated and we'll generate a new invite link for you.\";\n\"lng_group_invite_copied\" = \"Invite link copied to clipboard.\";\n\"lng_group_invite_no_room\" = \"Unable to join this group because it has too many members.\";\n\n\"lng_group_invite_copy\" = \"Copy Link\";\n\"lng_group_invite_share\" = \"Share Link\";\n\"lng_group_invite_reactivate\" = \"Reactivate Link\";\n\"lng_group_invite_delete\" = \"Delete Link\";\n\"lng_group_invite_no_joined\" = \"No one joined yet\";\n\"lng_group_invite_joined#one\" = \"{count} joined\";\n\"lng_group_invite_joined#other\" = \"{count} joined\";\n\"lng_group_invite_joined_via_filter\" = \"joined via a folder invite link\";\n\"lng_group_invite_remaining#one\" = \"{count} remaining\";\n\"lng_group_invite_remaining#other\" = \"{count} remaining\";\n\"lng_group_invite_requested#one\" = \"{count} request\";\n\"lng_group_invite_requested#other\" = \"{count} requests\";\n\"lng_group_invite_requested_full#one\" = \"{count} join request\";\n\"lng_group_invite_requested_full#other\" = \"{count} join requests\";\n\"lng_group_invite_can_join#one\" = \"{count} can join\";\n\"lng_group_invite_can_join#other\" = \"{count} can join\";\n\"lng_group_invite_days_left#one\" = \"{count} day left\";\n\"lng_group_invite_days_left#other\" = \"{count} days left\";\n\"lng_group_invite_about_permanent_group\" = \"Anyone who has Telegram installed will be able to join your group by following this link.\";\n\"lng_group_invite_about_permanent_channel\" = \"Anyone who has Telegram installed will be able to join your channel by following this link.\";\n\"lng_group_invite_title\" = \"Invite links\";\n\"lng_group_invite_add\" = \"Create a New Link\";\n\"lng_group_invite_add_about\" = \"You can generate invite links that expire after they are used.\";\n\"lng_group_invite_expires_at\" = \"This link expires {when}.\";\n\"lng_group_invite_created_by\" = \"Link created by\";\n\"lng_group_invite_links_count#one\" = \"{count} link\";\n\"lng_group_invite_links_count#other\" = \"{count} links\";\n\"lng_group_invite_context_copy\" = \"Copy\";\n\"lng_group_invite_context_share\" = \"Share\";\n\"lng_group_invite_context_edit\" = \"Edit\";\n\"lng_group_invite_context_qr\" = \"Get QR Code\";\n\"lng_group_invite_context_revoke\" = \"Revoke\";\n\"lng_group_invite_context_delete\" = \"Delete\";\n\"lng_group_invite_context_delete_all\" = \"Delete all\";\n\"lng_group_invite_delete_sure\" = \"Are you sure you want to delete this revoked link?\";\n\"lng_group_invite_delete_all_sure\" = \"Are you sure you want to delete all revoked links? This action cannot be undone.\";\n\"lng_group_invite_revoked_title\" = \"Revoked links\";\n\"lng_group_invite_revoke_about\" = \"Are you sure you want to revoke this invite link?\";\n\"lng_group_invite_link_expired\" = \"Expired\";\n\"lng_group_invite_edit_title\" = \"Edit Link\";\n\"lng_group_invite_new_title\" = \"New Link\";\n\"lng_group_invite_label_header\" = \"Link Name (optional)\";\n\"lng_group_invite_label_about\" = \"Only admins will see this name.\";\n\"lng_group_invite_expire_title\" = \"Limit by time period\";\n\"lng_group_invite_expire_about\" = \"You can make the link expire after a certain time.\";\n\"lng_group_invite_expire_never\" = \"No limit\";\n\"lng_group_invite_expire_custom\" = \"Custom\";\n\"lng_group_invite_usage_title\" = \"Limit by number of users\";\n\"lng_group_invite_usage_about\" = \"You can make the link work only for a certain number of users.\";\n\"lng_group_invite_expire_after\" = \"Expire after\";\n\"lng_group_invite_custom_limit\" = \"Enter custom limit\";\n\"lng_group_invite_usage_any\" = \"No limit\";\n\"lng_group_invite_usage_custom\" = \"Custom\";\n\"lng_group_invite_other_title\" = \"Links created by other admins\";\n\"lng_group_invite_other_count#one\" = \"{count} invite link\";\n\"lng_group_invite_other_count#other\" = \"{count} invite links\";\n\"lng_group_invite_other_list\" = \"Links created by this admin\";\n\"lng_group_invite_expired_about\" = \"The time limit for this link has expired.\";\n\"lng_group_invite_used_about\" = \"This link reached its usage limit.\";\n\"lng_group_invite_can_join_via_link#one\" = \"{count} person can join via this link.\";\n\"lng_group_invite_can_join_via_link#other\" = \"{count} people can join via this link.\";\n\"lng_group_invite_qr_title\" = \"Invite by QR Code\";\n\"lng_group_invite_qr_about\" = \"Everyone on Telegram can scan this code to join your group.\";\n\"lng_group_invite_qr_copied\" = \"QR Code copied to clipboard.\";\n\"lng_group_invite_request_approve\" = \"Request admin approval\";\n\"lng_group_invite_about_approve\" = \"Turn this on if you want users to join only after they are approved by an admin.\";\n\"lng_group_invite_about_no_approve\" = \"Turn this on if you want users to join only after they are approved by an admin.\";\n\"lng_group_invite_about_approve_channel\" = \"Turn this on if you want users to join only after they are approved by an admin.\";\n\"lng_group_invite_about_no_approve_channel\" = \"Turn this on if you want users to join only after they are approved by an admin.\";\n\"lng_group_invite_subscription\" = \"Require Monthly Fee\";\n\"lng_group_invite_subscription_ph\" = \"Stars Amount per month\";\n\"lng_group_invite_subscription_price\" = \"~{cost} / month\";\n\"lng_group_invite_subscription_toast\" = \"To change the subscription fee, create a new invite link with a different price.\";\n\"lng_group_invite_subscription_about\" = \"Charge a subscription fee from people who join your channel via this link. {link}\";\n\"lng_group_invite_subscription_about_link\" = \"Learn more {emoji}\";\n\"lng_group_invite_subscription_about_url\" = \"https://telegram.org/tos/stars\";\n\"lng_group_invite_subscription_info_subtitle\" = \"Subscription fee\";\n\"lng_group_invite_subscription_info_title\" = \"{emoji} {price} / month {multiplier} {total}\";\n\"lng_group_invite_subscription_info_title_none\" = \"{emoji} {price} / month\";\n\"lng_group_invite_subscription_info_about\" = \"you get approximately {total} monthly\";\n\"lng_group_invite_joined_right\" = \"per month\";\n\"lng_group_invite_joined_status\" = \"joined {date}\";\n\"lng_group_invite_joined_row_subscriber\" = \"Subscriber\";\n\"lng_group_invite_joined_row_date\" = \"Subscribed\";\n\n\"lng_group_request_to_join\" = \"Request to Join\";\n\"lng_group_request_about\" = \"This group accepts new members only after they are approved by its admins.\";\n\"lng_group_request_about_channel\" = \"This channel accepts new subscribers only after they are approved by its admins.\";\n\"lng_group_request_sent\" = \"You will be added to the group once an admin approves your request.\";\n\"lng_group_request_sent_channel\" = \"You will be added to the channel once its admins approve your request.\";\n\"lng_group_requests_pending#one\" = \"{count} join request\";\n\"lng_group_requests_pending#other\" = \"{count} join requests\";\n\"lng_group_requests_pending_user\" = \"{user} requested to join\";\n\"lng_group_requests_status_today\" = \"requested to join today at {time}\";\n\"lng_group_requests_status_yesterday\" = \"requested to join yesterday at {time}\";\n\"lng_group_requests_status_date_time\" = \"requested to join {date} at {time}\";\n\"lng_group_requests_add\" = \"Add to Group\";\n\"lng_group_requests_add_channel\" = \"Add to Channel\";\n\"lng_group_requests_dismiss\" = \"Dismiss\";\n\"lng_group_requests_was_added\" = \"{user} has been added to the group.\";\n\"lng_group_requests_was_added_channel\" = \"{user} has been added to the channel.\";\n\"lng_group_requests_none\" = \"There are no pending join requests.\";\n\"lng_group_requests_none_channel\" = \"There are no pending join requests.\";\n\n\"lng_channel_public_link_copied\" = \"Link copied to clipboard.\";\n\"lng_context_about_private_link\" = \"This link will only work for members of this chat.\";\n\"lng_public_post_private_hint_ctrl\" = \"Use Ctrl+Click to copy a non-public link.\";\n\"lng_public_post_private_hint_cmd\" = \"Use Cmd+Click to copy a non-public link.\";\n\n\"lng_forwarded\" = \"Forwarded from {user}\";\n\"lng_forwarded_story\" = \"Story from {user}\";\n\"lng_forwarded_story_expired\" = \"This story has expired.\";\n\"lng_forwarded_date\" = \"Original: {date}\";\n\"lng_forwarded_forwarded_date\" = \"Forwarded date: {date}\";\n\"lng_forwarded_channel\" = \"Forwarded from {channel}\";\n\"lng_forwarded_psa_default\" = \"Forwarded from {channel}\";\n\"lng_forwarded_via\" = \"Forwarded from {user} via {inline_bot}\";\n\"lng_forwarded_channel_via\" = \"Forwarded from {channel} via {inline_bot}\";\n\"lng_forwarded_signed\" = \"{channel} ({user})\";\n\"lng_forwarded_hidden\" = \"The account was hidden by the user.\";\n\"lng_forwarded_imported\" = \"This message was imported from another app. It may not be real.\";\n\"lng_signed_author\" = \"Author: {user}\";\n\"lng_sponsored_message_title\" = \"Ad\";\n\"lng_sponsored_message_revenue_button\" = \"what's this?\";\n\"lng_recommended_message_title\" = \"Recommended\";\n\"lng_edited\" = \"edited\";\n\"lng_commented\" = \"commented\";\n\"lng_approximate\" = \"appx.\";\n\"lng_repeated_daily\" = \"daily\";\n\"lng_repeated_weekly\" = \"weekly\";\n\"lng_repeated_biweekly\" = \"biweekly\";\n\"lng_repeated_monthly\" = \"monthly\";\n\"lng_repeated_every_month#one\" = \"{count}-monthly\";\n\"lng_repeated_every_month#other\" = \"{count}-monthly\";\n\"lng_repeated_yearly\" = \"yearly\";\n\"lng_edited_date\" = \"Edited: {date}\";\n\"lng_sent_date\" = \"Sent: {date}\";\n\"lng_approximate_about\" = \"Estimated date of video publishing.\";\n\"lng_views_tooltip#one\" = \"Views: {count}\";\n\"lng_views_tooltip#other\" = \"Views: {count}\";\n\"lng_forwards_tooltip#one\" = \"Shares: {count}\";\n\"lng_forwards_tooltip#other\" = \"Shares: {count}\";\n\"lng_imported\" = \"imported\";\n\"lng_admin_badge\" = \"admin\";\n\"lng_owner_badge\" = \"owner\";\n\"lng_channel_badge\" = \"channel\";\n\"lng_topic_author_badge\" = \"Topic Creator\";\n\"lng_fast_reply\" = \"Reply\";\n\"lng_fast_share_tooltip\" = \"Right-click to select a Recent Contact.\";\n\"lng_cancel_edit_post_sure\" = \"Cancel editing?\";\n\"lng_cancel_edit_post_yes\" = \"Yes\";\n\"lng_cancel_edit_post_no\" = \"No\";\n\n\"lng_bot_share_location_unavailable\" = \"Sorry, location sharing is currently unavailable in Telegram Desktop.\";\n\"lng_bot_share_phone\" = \"Do you want to share your phone number with this bot?\";\n\"lng_bot_share_phone_confirm\" = \"Share\";\n\"lng_bot_allow_write_title\" = \"Allow messaging\";\n\"lng_bot_allow_write\" = \"Do you want to allow this bot to send you messages?\";\n\"lng_bot_allow_write_confirm\" = \"Allow\";\n\"lng_bot_new_chat\" = \"New Chat\";\n\"lng_bot_new_thread_title\" = \"New Thread\";\n\"lng_bot_new_thread_about\" = \"Type any message to create a new thread.\";\n\"lng_bot_show_threads_list\" = \"Show Threads List\";\n\"lng_bot_off_thread_ph\" = \"Off-thread message\";\n\n\"lng_attach_failed\" = \"Failed\";\n\"lng_attach_file\" = \"File\";\n\"lng_attach_photo\" = \"Photo\";\n\"lng_attach_camera\" = \"Camera\";\n\"lng_attach_document\" = \"Document\";\n\"lng_attach_photo_or_video\" = \"Photo or video\";\n\"lng_attach_profile_emoji\" = \"Use an Emoji\";\n\n\"lng_media_open_with\" = \"Open With\";\n\"lng_media_download\" = \"Download\";\n\"lng_media_cancel\" = \"Cancel\";\n\"lng_media_video\" = \"Video\";\n\"lng_media_audio\" = \"Voice message\";\n\"lng_media_round\" = \"Video message\";\n\n\"lng_media_auto_settings\" = \"Automatic media download\";\n\"lng_media_auto_in_private\" = \"In private chats\";\n\"lng_media_auto_in_groups\" = \"In groups\";\n\"lng_media_auto_in_channels\" = \"In channels\";\n\"lng_media_auto_title\" = \"Automatically download\";\n\"lng_media_auto_play\" = \"Autoplay\";\n\"lng_media_photo_title\" = \"Photos\";\n\"lng_media_video_title\" = \"Videos\";\n\"lng_media_video_messages_title\" = \"Round video messages\";\n\"lng_media_file_title\" = \"Files\";\n\"lng_media_animation_title\" = \"GIFs\";\n\"lng_media_size_limit\" = \"Limit by size\";\n\"lng_media_size_up_to\" = \"up to {size}\";\n\"lng_media_chat_background\" = \"Chat Wallpaper\";\n\"lng_media_color_theme\" = \"Color theme\";\n\n\"lng_emoji_category1\" = \"Emoji & People\";\n\"lng_emoji_category2\" = \"Nature\";\n\"lng_emoji_category3\" = \"Food & Drink\";\n\"lng_emoji_category4\" = \"Activity\";\n\"lng_emoji_category5\" = \"Travel & Places\";\n\"lng_emoji_category6\" = \"Objects\";\n\"lng_emoji_category7\" = \"Symbols & Flags\";\n\"lng_emoji_manage_sets\" = \"Choose emoji set\";\n\"lng_emoji_set_ready\" = \"Downloaded\";\n\"lng_emoji_set_active\" = \"Current set\";\n\"lng_emoji_set_download\" = \"Download {size}\";\n\"lng_emoji_set_loading\" = \"{percent}, {progress}\";\n\"lng_emoji_color_all\" = \"Choose color for all emoji\";\n\"lng_emoji_copy\" = \"Copy emoji\";\n\"lng_emoji_view_pack\" = \"View pack\";\n\"lng_emoji_remove_recent\" = \"Remove from Recent\";\n\"lng_emoji_reset_recent\" = \"Reset recents\";\n\"lng_emoji_reset_recent_sure\" = \"Do you want to reset recent emoji?\";\n\"lng_emoji_reset_recent_button\" = \"Reset\";\n\n\"lng_recent_stickers\" = \"Recently used\";\n\"lng_faved_stickers_add\" = \"Add to Favorites\";\n\"lng_faved_stickers_remove\" = \"Remove from Favorites\";\n\"lng_recent_stickers_remove\" = \"Remove from Recent\";\n\"lng_group_stickers\" = \"Group stickers\";\n\"lng_group_stickers_description\" = \"You can choose a sticker set which will be available for every member while in the group chat.\";\n\"lng_group_stickers_add\" = \"Choose sticker set\";\n\"lng_group_emoji\" = \"Select Emoji Pack\";\n\"lng_group_emoji_description\" = \"Choose an emoji pack that will be available to all members within the group.\";\n\"lng_collectible_emoji\" = \"Collectibles\";\n\n\"lng_premium\" = \"Premium\";\n\"lng_premium_free\" = \"Free\";\n\"lng_premium_more_about\" = \"About Telegram Premium\";\n\"lng_premium_unlock_reactions\" = \"Unlock Premium Reactions\";\n\"lng_premium_unlock_stickers\" = \"Unlock Premium Stickers\";\n\"lng_premium_unlock_emoji\" = \"Unlock Animated Emoji\";\n\"lng_premium_unlock_status\" = \"Unlock Emoji Status\";\n\n\"lng_premium_subscribe_months_24\" = \"2-Year\";\n\"lng_premium_subscribe_months_12\" = \"Annual\";\n\"lng_premium_subscribe_months_6\" = \"Semiannual\";\n\"lng_premium_subscribe_months_1\" = \"Monthly\";\n\"lng_premium_subscribe_total\" = \"{cost} per year\";\n\"lng_premium_subscribe_button\" = \"Subscribe for {cost} per month\";\n\n\"lng_premium_emoji_status_title\" = \"{user} set this emoji from {link} as their current status.\";\n\"lng_premium_emoji_status_title_colored\" = \"{user} set this emoji as their current status.\";\n\"lng_premium_emoji_status_about\" = \"Emoji status is a premium feature. Other features included in **Telegram Premium**:\";\n\"lng_premium_emoji_status_button\" = \"Unlock Emoji Status\";\n\n\"lng_premium_summary_user_title\" = \"{user} is a subscriber of Telegram Premium.\";\n\"lng_premium_summary_user_about\" = \"Owners of Telegram Premium accounts have exclusive access to multiple additional features.\";\n\n\"lng_premium_summary_title\" = \"Telegram Premium\";\n\"lng_premium_summary_top_about\" = \"Go **beyond the limits** and **unlock dozens of exclusive** features by subscribing to **Telegram Premium**.\";\n\"lng_premium_summary_title_subscribed\" = \"You are all set!\";\n\"lng_premium_summary_subtitle_gift#one\" = \"{user} has gifted you a {count}-month subscription to Telegram Premium.\";\n\"lng_premium_summary_subtitle_gift#other\" = \"{user} has gifted you a {count}-months subscription to Telegram Premium.\";\n\"lng_premium_summary_subtitle_gift_days#one\" = \"{user} has gifted you a {count}-day subscription to Telegram Premium.\";\n\"lng_premium_summary_subtitle_gift_days#other\" = \"{user} has gifted you a {count}-days subscription to Telegram Premium.\";\n\"lng_premium_summary_subtitle_gift_me#one\" = \"You gifted {user} a {count}-month subscription to Telegram Premium.\";\n\"lng_premium_summary_subtitle_gift_me#other\" = \"You gifted {user} a {count}-months subscription to Telegram Premium.\";\n\"lng_premium_summary_subtitle_gift_days_me#one\" = \"You gifted {user} a {count}-month subscription to Telegram Premium.\";\n\"lng_premium_summary_subtitle_gift_days_me#other\" = \"You gifted {user} a {count}-months subscription to Telegram Premium.\";\n\"lng_premium_summary_subtitle_wallpapers\" = \"Wallpapers for Both Sides\";\n\"lng_premium_summary_about_wallpapers\" = \"Set custom wallpapers for you and your chat partner.\";\n\"lng_premium_summary_subtitle_stories\" = \"Stories\";\n\"lng_premium_summary_about_stories\" = \"Unlimited posting, priority order, stealth mode, permanent view history and more.\";\n\"lng_premium_summary_subtitle_double_limits\" = \"Doubled Limits\";\n\"lng_premium_summary_about_double_limits\" = \"Up to 1000 channels, 30 folders, 10 pins, 20 public links, 4 accounts and more.\";\n\"lng_premium_summary_subtitle_more_upload\" = \"Unlimited Cloud Storage\";\n\"lng_premium_summary_about_more_upload\" = \"4 GB per each document, unlimited storage for your chats and media overall.\";\n\"lng_premium_summary_subtitle_faster_download\" = \"Faster Download Speed\";\n\"lng_premium_summary_about_faster_download\" = \"No more limits on the speed with which media and documents are downloaded.\";\n\"lng_premium_summary_subtitle_voice_to_text\" = \"Voice-to-Text Conversion\";\n\"lng_premium_summary_about_voice_to_text\" = \"Ability to read the transcript of any incoming voice message.\";\n\"lng_premium_summary_subtitle_no_ads\" = \"No Ads\";\n\"lng_premium_summary_about_no_ads\" = \"No more ads in public channels where Telegram sometimes shows ads.\";\n\"lng_premium_summary_subtitle_emoji_status\" = \"Emoji Statuses\";\n\"lng_premium_summary_about_emoji_status\" = \"Choose from thousands of emoji to display current activity next to your name.\";\n\"lng_premium_summary_subtitle_infinite_reactions\" = \"Infinite Reactions\";\n\"lng_premium_summary_about_infinite_reactions\" = \"React with thousands of emoji — with multiple reactions per message.\";\n\"lng_premium_summary_subtitle_tags_for_messages\" = \"Tags for Messages\";\n\"lng_premium_summary_about_tags_for_messages\" = \"Organize your Saved Messages with tags for quicker access.\";\n\"lng_premium_summary_subtitle_last_seen\" = \"Last Seen Times\";\n\"lng_premium_summary_about_last_seen\" = \"View the last seen and read times of others even if you hide yours.\";\n\"lng_premium_summary_subtitle_message_privacy\" = \"Message Privacy\";\n\"lng_premium_summary_about_message_privacy\" = \"Restrict people you don't know from sending you messages.\";\n\"lng_premium_summary_subtitle_premium_stickers\" = \"Premium Stickers\";\n\"lng_premium_summary_about_premium_stickers\" = \"Exclusive enlarged stickers featuring additional effects, updated monthly.\";\n\"lng_premium_summary_subtitle_animated_emoji\" = \"Animated Emoji\";\n\"lng_premium_summary_about_animated_emoji\" = \"Include animated emoji from different packs in any message you send.\";\n\"lng_premium_summary_subtitle_advanced_chat_management\" = \"Advanced Chat Management\";\n\"lng_premium_summary_about_advanced_chat_management\" = \"Tools to set the default folder, auto-archive and hide new chats from non-contacts.\";\n\"lng_premium_summary_subtitle_profile_badge\" = \"Profile Badge\";\n\"lng_premium_summary_about_profile_badge\" = \"An exclusive badge next to your name showing that you subscribe to Telegram Premium.\";\n\"lng_premium_summary_subtitle_animated_userpics\" = \"Animated Profile Pictures\";\n\"lng_premium_summary_about_animated_userpics\" = \"Video avatars animated in chat lists and chats to allow for additional self-expression.\";\n\"lng_premium_summary_subtitle_translation\" = \"Real-Time Translation\";\n\"lng_premium_summary_about_translation\" = \"Real-time translation of channels and chats into other languages.\";\n\"lng_premium_summary_subtitle_business\" = \"Telegram Business\";\n\"lng_premium_summary_about_business\" = \"Upgrade your account with business features such as location, opening hours and quick replies.\";\n\"lng_premium_summary_subtitle_effects\" = \"Message Effects\";\n\"lng_premium_summary_about_effects\" = \"Add over 500 animated effects to private messages.\";\n\"lng_premium_summary_subtitle_filter_tags\" = \"Tag Your Chats\";\n\"lng_premium_summary_about_filter_tags\" = \"Display folder names for each chat in the chat list.\";\n\"lng_premium_summary_subtitle_todo_lists\" = \"Checklists\";\n\"lng_premium_summary_about_todo_lists\" = \"Plan, assign, and complete tasks - seamlessly and efficiently.\";\n\"lng_premium_summary_subtitle_peer_colors\" = \"Name and Profile Colors\";\n\"lng_premium_summary_about_peer_colors\" = \"Choose a color and logo for your profile and replies to your messages.\";\n\"lng_premium_summary_subtitle_gifts\" = \"Telegram Gifts\";\n\"lng_premium_summary_about_gifts\" = \"Gifts are collectible items you can trade or showcase on your profile.\";\n\"lng_premium_summary_subtitle_no_forwards\" = \"Disable Sharing\";\n\"lng_premium_summary_about_no_forwards\" = \"Restrict forwarding, copying, and saving content from your private chats.\";\n\"lng_premium_summary_subtitle_ai_compose\" = \"AI Tools\";\n\"lng_premium_summary_about_ai_compose\" = \"Transform your messages and entire chats in your preferred style and language.\";\n\"lng_premium_summary_bottom_subtitle\" = \"About Telegram Premium\";\n\"lng_premium_summary_bottom_about\" = \"While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\\n\\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone.\";\n\"lng_premium_summary_button\" = \"Subscribe for {cost} per month\";\n\n\"lng_premium_summary_new_badge\" = \"NEW\";\n\"lng_soon_badge\" = \"Soon\";\n\n\"lng_premium_success\" = \"You've successfully subscribed to Telegram Premium!\";\n\"lng_premium_unavailable\" = \"This feature requires subscription to **Telegram Premium**.\\n\\nUnfortunately, **Telegram Premium** is not available in your region.\";\n\n// Upgraded Stories.\n\"lng_premium_stories_subtitle_order\" = \"Priority Order\";\n\"lng_premium_stories_about_order\" = \"Get more views as your stories are always displayed first.\";\n\n\"lng_premium_stories_subtitle_stealth\" = \"Stealth Mode\";\n\"lng_premium_stories_about_stealth\" = \"Hide the fact that you viewed other people's stories.\";\n\n\"lng_premium_stories_subtitle_views\" = \"Permanent View History\";\n\"lng_premium_stories_about_views\" = \"Check who opens your stories — even after they expire.\";\n\n\"lng_premium_stories_subtitle_expiration\" = \"Expiration Options*\";\n\"lng_premium_stories_about_expiration\" = \"Set custom durations like 6 or 48 hours for your stories.\";\n\n\"lng_premium_stories_subtitle_download\" = \"Download Stories\";\n\"lng_premium_stories_about_download\" = \"Save other people's stories that are not protected.\";\n\n\"lng_premium_stories_subtitle_caption\" = \"Longer Captions*\";\n\"lng_premium_stories_about_caption\" = \"Add 10x longer captions to your stories – up to 2048 characters.\";\n\n\"lng_premium_stories_subtitle_links\" = \"Links and Formatting*\";\n\"lng_premium_stories_about_links\" = \"Add links and formatting to your story captions.\";\n\n\"lng_premium_stories_about_mobile\" = \"* Available when posting stories from Telegram apps for iOS and Android.\";\n\n// Doubled Limits.\n\"lng_premium_double_limits_subtitle_channels\" = \"Groups and Channels\";\n\"lng_premium_double_limits_about_channels#one\" = \"Join up to {count} channel or large group\";\n\"lng_premium_double_limits_about_channels#other\" = \"Join up to {count} channels and large groups\";\n\n\"lng_premium_double_limits_subtitle_pins\" = \"Pinned Chats\";\n\"lng_premium_double_limits_about_pins#one\" = \"Pin up to {count} chat in your main chat list\";\n\"lng_premium_double_limits_about_pins#other\" = \"Pin up to {count} chats in your main chat list\";\n\n\"lng_premium_double_limits_subtitle_links\" = \"Public Links\";\n\"lng_premium_double_limits_about_links#one\" = \"Reserve up to {count} t.me/username link\";\n\"lng_premium_double_limits_about_links#other\" = \"Reserve up to {count} t.me/username links\";\n\n\"lng_premium_double_limits_subtitle_gifs\" = \"Saved GIFs\";\n\"lng_premium_double_limits_about_gifs#one\" = \"Save up to {count} GIF in your Favorite GIFs\";\n\"lng_premium_double_limits_about_gifs#other\" = \"Save up to {count} GIFs in your Favorite GIFs\";\n\n\"lng_premium_double_limits_subtitle_stickers\" = \"Favorite Stickers\";\n\"lng_premium_double_limits_about_stickers#one\" = \"Save up to {count} sticker in your Favorite stickers\";\n\"lng_premium_double_limits_about_stickers#other\" = \"Save up to {count} stickers in your Favorite stickers\";\n\n\"lng_premium_double_limits_subtitle_bio\" = \"Bio\";\n\"lng_premium_double_limits_about_bio\" = \"Use more characters and add links in your bio\";\n\n\"lng_premium_double_limits_subtitle_captions\" = \"Captions\";\n\"lng_premium_double_limits_about_captions\" = \"Use longer descriptions for your photos and videos\";\n\n\"lng_premium_double_limits_subtitle_folders\" = \"Folders\";\n\"lng_premium_double_limits_about_folders#one\" = \"Organize your chats into {count} folder\";\n\"lng_premium_double_limits_about_folders#other\" = \"Organize your chats into {count} folders\";\n\n\"lng_premium_double_limits_subtitle_folder_chats\" = \"Chats per Folder\";\n\"lng_premium_double_limits_about_folder_chats#one\" = \"Add up to {count} chat into each of your folders\";\n\"lng_premium_double_limits_about_folder_chats#other\" = \"Add up to {count} chats into each of your folders\";\n\n\"lng_premium_double_limits_subtitle_accounts\" = \"Connected Accounts\";\n\"lng_premium_double_limits_about_accounts#one\" = \"Connect {count} account with different mobile numbers\";\n\"lng_premium_double_limits_about_accounts#other\" = \"Connect {count} accounts with different mobile numbers\";\n\n\"lng_premium_double_limits_subtitle_similar_channels\" = \"Similar Channel\";\n\"lng_premium_double_limits_about_similar_channels#one\" = \"View up to {count} similar channel\";\n\"lng_premium_double_limits_about_similar_channels#other\" = \"View up to {count} similar channels\";\n//\n\n\"lng_premium_gift_title\" = \"Gift Telegram Premium\";\n\"lng_premium_gift_about\" = \"Give **{user}** access to exclusive features with **Telegram Premium**.\";\n\"lng_premium_gift_button\" = \"Gift Subscription for {cost}\";\n\"lng_premium_gift_per\" = \"{cost} / month\";\n\"lng_premium_gift_terms\" = \"You can review the list of features and more details about Telegram Premium {link}.\";\n\"lng_premium_gift_terms_link\" = \"here\";\n\"lng_premium_gifts_about_user1\" = \"Give **{user}** access to exclusive features.\";\n\"lng_premium_gifts_about_user2\" = \"Give **{user}** and **{second_user}** access to exclusive features.\";\n\"lng_premium_gifts_about_user3\" = \"Give **{user}**, **{second_user}** and **{name}** access to exclusive features.\";\n\"lng_premium_gifts_about_user_more#one\" = \"Give **{user}**, **{second_user}**, **{name}** and **{count}** more friend access to exclusive features.\";\n\"lng_premium_gifts_about_user_more#other\" = \"Give **{user}**, **{second_user}**, **{name}** and **{count}** more friends access to exclusive features.\";\n\"lng_premium_gifts_about_reward#one\" = \"You will receive {emoji}**{count}** boost.\";\n\"lng_premium_gifts_about_reward#other\" = \"You will receive {emoji}**{count}** boosts.\";\n\"lng_premium_gifts_about_paid_title\" = \"Gifts Sent!\";\n\"lng_premium_gifts_about_paid1\" = \"**{user}** has been notified about the gifts you purchased.\";\n\"lng_premium_gifts_about_paid2\" = \"**{user}** and **{second_user}** have been notified about the gifts you purchased.\";\n\"lng_premium_gifts_about_paid3\" = \"**{user}**, **{second_user}** and **{name}** have been notified about the gifts you purchased.\";\n\"lng_premium_gifts_about_paid_more#one\" = \"**{user}**, **{second_user}**, **{name}** and **{count}** other have been notified about the gifts you purchased.\";\n\"lng_premium_gifts_about_paid_more#other\" = \"**{user}**, **{second_user}**, **{name}** and **{count}** others have been notified about the gifts you purchased.\";\n\"lng_premium_gifts_about_paid_below#one\" = \"They now have access to additional features.\";\n\"lng_premium_gifts_about_paid_below#other\" = \"They now have access to additional features.\";\n\"lng_premium_gifts_summary_subtitle\" = \"What's Included\";\n\"lng_premium_gifts_terms\" = \"By gifting Telegram Premium, you agree to the Telegram {link} and {policy}.\";\n\"lng_premium_gifts_terms_policy\" = \"Privacy Policy\";\n\n\"lng_business_title\" = \"Telegram Business\";\n\"lng_business_about\" = \"Turn your account to a business page with these additional features.\";\n\"lng_business_unlocked\" = \"You have now unlocked these additional business features.\";\n\"lng_business_subtitle_location\" = \"Location\";\n\"lng_business_about_location\" = \"Display the location of your business on your account.\";\n\"lng_business_subtitle_opening_hours\" = \"Opening Hours\";\n\"lng_business_about_opening_hours\" = \"Show to your customers when you are open for business.\";\n\"lng_business_subtitle_quick_replies\" = \"Quick Replies\";\n\"lng_business_about_quick_replies\" = \"Set up shortcuts with rich text and media to respond to messages faster.\";\n\"lng_business_subtitle_greeting_messages\" = \"Greeting Messages\";\n\"lng_business_about_greeting_messages\" = \"Create greetings that will be automatically sent to new customers.\";\n\"lng_business_subtitle_away_messages\" = \"Away Messages\";\n\"lng_business_about_away_messages\" = \"Define messages that are automatically sent when you are off.\";\n\"lng_business_subtitle_chatbots\" = \"Chatbots\";\n\"lng_business_about_chatbots\" = \"Add any third party chatbots that will process customer interactions.\";\n\"lng_business_subtitle_chat_intro\" = \"Custom Intro\";\n\"lng_business_about_chat_intro\" = \"Customize the message people see before they start a chat with you.\";\n\"lng_business_subtitle_chat_links\" = \"Links to Chat\";\n\"lng_business_about_chat_links\" = \"Create links that start a chat with you, suggesting the first message.\";\n\"lng_business_subtitle_sponsored\" = \"Ads in Channels\";\n\"lng_business_button_sponsored\" = \"Do Not Hide Ads\";\n\"lng_business_about_sponsored\" = \"As a Premium subscriber, you don’t see any ads on Telegram, but you can turn them on, for example, to view your own ads that you launched on the {link}\";\n\"lng_business_about_sponsored_link\" = \"Telegram Ad Platform {emoji}\";\n\"lng_business_about_sponsored_url\" = \"https://ads.telegram.org\";\n\n\"lng_credits_summary_title\" = \"Telegram Stars\";\n\"lng_credits_summary_about\" = \"Buy Stars to unlock content and services in miniapps on Telegram.\";\n\"lng_credits_currency_summary_title\" = \"TON Balance\";\n\"lng_credits_currency_summary_about\" = \"Offer TON to submit post suggestions to channels on Telegram.\";\n\"lng_credits_currency_summary_subtitle\" = \"You can withdraw your TON using Fragment.\";\n\"lng_credits_currency_summary_in_button\" = \"Top-up via Fragment\";\n\"lng_credits_currency_summary_in_subtitle\" = \"You can top-up your TON balance via Fragment.\";\n\"lng_credits_summary_options_subtitle\" = \"Choose package\";\n\"lng_credits_summary_options_credits#one\" = \"{count} Star\";\n\"lng_credits_summary_options_credits#other\" = \"{count} Stars\";\n\"lng_credits_summary_options_more\" = \"More Options\";\n\"lng_credits_summary_options_about\" = \"By proceeding and purchasing Stars, you agree with the {link}.\";\n\"lng_credits_summary_options_about_link\" = \"Terms and Conditions\";\n\"lng_credits_summary_options_about_url\" = \"https://telegram.org/tos/stars\";\n\"lng_credits_summary_earn_title\" = \"Earn Stars\";\n\"lng_credits_summary_earn_about\" = \"Distribute links to mini apps and earn a share of their revenue in Stars.\";\n\"lng_credits_summary_history_tab_full\" = \"All Transactions\";\n\"lng_credits_summary_history_tab_in\" = \"Incoming\";\n\"lng_credits_summary_history_tab_out\" = \"Outgoing\";\n\"lng_credits_summary_history_entry_inner_in\" = \"In-App Purchase\";\n\"lng_credits_summary_balance\" = \"Balance\";\n\"lng_credits_commission\" = \"{amount} commission\";\n\"lng_credits_paid_messages_fee_live_reaction\" = \"Fee for Live Story Reaction\";\n\"lng_credits_paid_messages_fee#one\" = \"Fee for {count} Message\";\n\"lng_credits_paid_messages_fee#other\" = \"Fee for {count} Messages\";\n\"lng_credits_paid_messages_fee_about\" = \"You receive {percent} of the price that you charge for each incoming message. {link}\";\n\"lng_credits_paid_messages_fee_about_link\" = \"Change Fee {emoji}\";\n\"lng_credits_paid_messages_full\" = \"Full Price\";\n\"lng_credits_premium_gift_duration\" = \"Duration\";\n\"lng_credits_more_options\" = \"More Options\";\n\"lng_credits_balance_me\" = \"your balance\";\n\"lng_credits_balance_me_count\" = \"Your balance: {emoji} {amount}\";\n\"lng_credits_buy_button\" = \"Top Up Balance\";\n\"lng_credits_topup_button\" = \"{emoji} Top Up Balance\";\n\"lng_credits_buy_button_short\" = \"Top Up\";\n\"lng_credits_stats_button_short\" = \"Stats\";\n\"lng_credits_stats_button\" = \"View Statistics\";\n\"lng_credits_gift_button\" = \"Gift Stars to Friends\";\n\"lng_credits_earn_button\" = \"Earn Stars from Mini Apps\";\n\"lng_credits_box_out_title\" = \"Confirm Your Purchase\";\n\"lng_credits_box_out_sure#one\" = \"Do you want to buy **\\\"{text}\\\"** in **{bot}** for **{count} Star**?\";\n\"lng_credits_box_out_sure#other\" = \"Do you want to buy **\\\"{text}\\\"** in **{bot}** for **{count} Stars**?\";\n\"lng_credits_box_out_media#one\" = \"Do you want to unlock {media} in {chat} for **{count} Star**?\";\n\"lng_credits_box_out_media#other\" = \"Do you want to unlock {media} in {chat} for **{count} Stars**?\";\n\"lng_credits_box_out_media_user#one\" = \"Do you want to unlock {media} from {user} for **{count} Star**?\";\n\"lng_credits_box_out_media_user#other\" = \"Do you want to unlock {media} from {user} for **{count} Stars**?\";\n\"lng_credits_box_out_subscription_bot#one\" = \"Do you want to subscribe to **{title}** in **{recipient}** for **{count}** star per month?\";\n\"lng_credits_box_out_subscription_bot#other\" = \"Do you want to subscribe to **{title}** in **{recipient}** for **{count}** stars per month?\";\n\"lng_credits_box_out_subscription_business#one\" = \"Do you want to subscribe to **{title}** from **{recipient}** for **{count}** star per month?\";\n\"lng_credits_box_out_subscription_business#other\" = \"Do you want to subscribe to **{title}** from **{recipient}** for **{count}** stars per month?\";\n\"lng_credits_box_out_subscription_confirm#one\" = \"Subscribe for {emoji} {count} / month\";\n\"lng_credits_box_out_subscription_confirm#other\" = \"Subscribe for {emoji} {count} / month\";\n\"lng_credits_box_out_photo\" = \"a photo\";\n\"lng_credits_box_out_photos#one\" = \"{count} photo\";\n\"lng_credits_box_out_photos#other\" = \"{count} photos\";\n\"lng_credits_box_out_video\" = \"a video\";\n\"lng_credits_box_out_videos#one\" = \"{count} video\";\n\"lng_credits_box_out_videos#other\" = \"{count} videos\";\n\"lng_credits_box_out_both\" = \"{photo} and {video}\";\n\"lng_credits_box_out_confirm#one\" = \"Confirm and Pay {emoji} {count} Star\";\n\"lng_credits_box_out_confirm#other\" = \"Confirm and Pay {emoji} {count} Stars\";\n\"lng_credits_box_out_about\" = \"Review the {link} for Stars.\";\n\"lng_credits_box_out_about_link\" = \"https://telegram.org/tos/stars\";\n\"lng_credits_media_done_title\" = \"Media Unlocked\";\n\"lng_credits_media_done_text#one\" = \"**{count} Star** transferred to {chat}.\";\n\"lng_credits_media_done_text#other\" = \"**{count} Stars** transferred to {chat}.\";\n\"lng_credits_media_done_text_user#one\" = \"**{count} Star** transferred to {user}.\";\n\"lng_credits_media_done_text_user#other\" = \"**{count} Stars** transferred to {user}.\";\n\"lng_credits_summary_in_toast_title\" = \"Stars Acquired\";\n\"lng_credits_summary_in_toast_about#one\" = \"**{count}** Star added to your balance.\";\n\"lng_credits_summary_in_toast_about#other\" = \"**{count}** Stars added to your balance.\";\n\"lng_credits_box_history_entry_peer\" = \"Recipient\";\n\"lng_credits_box_history_entry_peer_in\" = \"From\";\n\"lng_credits_box_history_entry_gift_from\" = \"Gift From\";\n\"lng_credits_box_history_entry_via\" = \"Via\";\n\"lng_credits_box_history_entry_play_market\" = \"Play Store\";\n\"lng_credits_box_history_entry_app_store\" = \"App Store\";\n\"lng_credits_box_history_entry_fragment\" = \"Fragment\";\n\"lng_credits_box_history_entry_anonymous\" = \"Unknown User\";\n\"lng_credits_box_history_entry_gift_name\" = \"Received Gift\";\n\"lng_credits_box_history_entry_giveaway_name\" = \"Received Prize\";\n\"lng_credits_box_history_entry_gift_sent\" = \"Sent Gift\";\n\"lng_credits_box_history_entry_gift_converted\" = \"Converted Gift\";\n\"lng_credits_box_history_entry_gift_transfer\" = \"Gift Transfer\";\n\"lng_credits_box_history_entry_gift_unavailable\" = \"Unavailable\";\n\"lng_credits_box_history_entry_gift_released\" = \"released by {name}\";\n\"lng_credits_box_history_entry_gift_sold_out\" = \"This gift has sold out\";\n\"lng_credits_box_history_entry_gift_out_about\" = \"With Stars, **{user}** will be able to unlock content and services on Telegram.\\n{link}\";\n\"lng_credits_box_history_entry_gift_in_about\" = \"Use Stars to unlock content and services on Telegram. {link}\";\n\"lng_credits_box_history_entry_gift_about_link\" = \"See Examples {emoji}\";\n\"lng_credits_box_history_entry_gift_examples\" = \"Examples\";\n\"lng_credits_box_history_entry_ads\" = \"Ads Platform\";\n\"lng_credits_box_history_entry_premium_bot\" = \"Stars Top-Up\";\n\"lng_credits_box_history_entry_currency_in\" = \"TON Top-Up\";\n\"lng_credits_box_history_entry_posts_search\" = \"Posts Search\";\n\"lng_credits_box_history_entry_api\" = \"Paid Broadcast\";\n\"lng_credits_box_history_entry_floodskip_about#one\" = \"{count} Message\";\n\"lng_credits_box_history_entry_floodskip_about#other\" = \"{count} Messages\";\n\"lng_credits_box_history_entry_floodskip_row\" = \"Messages\";\n\"lng_credits_box_history_entry_via_premium_bot\" = \"Premium Bot\";\n\"lng_credits_box_history_entry_id\" = \"Transaction ID\";\n\"lng_credits_box_history_entry_id_copied\" = \"Transaction ID copied to clipboard.\";\n\"lng_credits_box_history_entry_reason_star_ref\" = \"Affiliate Program\";\n\"lng_credits_box_history_entry_affiliate\" = \"Affiliate\";\n\"lng_credits_box_history_entry_miniapp\" = \"Mini App\";\n\"lng_credits_box_history_entry_referred\" = \"Referred User\";\n\"lng_credits_box_history_entry_success_date\" = \"Transaction date\";\n\"lng_credits_box_history_entry_success_url\" = \"Transaction link\";\n\"lng_credits_box_history_entry_media\" = \"Media\";\n\"lng_credits_box_history_entry_message\" = \"Message\";\n\"lng_credits_box_history_entry_about\" = \"You can dispute this transaction {link}.\";\n\"lng_credits_box_history_entry_about_link\" = \"here\";\n\"lng_credits_box_history_entry_reaction_name\" = \"Star Reaction\";\n\"lng_credits_box_history_entry_subscription\" = \"Monthly subscription fee\";\n\"lng_credits_box_history_entry_gift_upgrade\" = \"Collectible Upgrade\";\n\"lng_credits_box_history_entry_gift_sold\" = \"Gift Sale\";\n\"lng_credits_box_history_entry_gift_bought\" = \"Gift Purchase\";\n\"lng_credits_box_history_entry_gift_sold_to\" = \"To\";\n\"lng_credits_box_history_entry_gift_full_price\" = \"Full Price\";\n\"lng_credits_box_history_entry_gift_bought_from\" = \"From\";\n\n\"lng_credits_subscription_section\" = \"My subscriptions\";\n\"lng_credits_box_subscription_title\" = \"Subscription\";\n\"lng_credits_subscription_subtitle\" = \"{emoji} {cost} / month\";\n\"lng_credits_subscriber_subtitle\" = \"appx. {total} per month\";\n\n\"lng_credits_subscription_row_to\" = \"Subscription\";\n\"lng_credits_subscription_row_to_bot\" = \"Bot\";\n\"lng_credits_subscription_row_to_business\" = \"Business\";\n\"lng_credits_subscription_row_from\" = \"Subscribed\";\n\n\"lng_credits_subscription_row_next_on\" = \"Renews\";\n\"lng_credits_subscription_row_next_off\" = \"Expires\";\n\"lng_credits_subscription_row_next_none\" = \"Expired\";\n\n\"lng_credits_subscription_on_button\" = \"Cancel Subscription\";\n\"lng_credits_subscription_on_about\" = \"If you cancel now, you will still be able to access your subscription until {date}.\";\n\n\"lng_credits_subscription_off_button\" = \"Renew Subscription\";\n\"lng_credits_subscription_off_rejoin_button\" = \"Subscribe again\";\n\"lng_credits_subscription_off_about\" = \"You have canceled your subscription.\";\n\"lng_credits_subscription_off_by_bot_about\" = \"{bot} has canceled your subscription.\";\n\n\"lng_credits_subscription_status_on\" = \"renews on {date}\";\n\"lng_credits_subscription_status_off\" = \"expires on {date}\";\n\"lng_credits_subscription_status_none\" = \"expired on {date}\";\n\"lng_credits_subscription_status_off_right\" = \"canceled\";\n\"lng_credits_subscription_status_none_right\" = \"expired\";\n\"lng_credits_subscription_status_off_by_bot_right\" = \"canceled\\nby bot\";\n\n\"lng_credits_small_balance_title#one\" = \"{count} Star Needed\";\n\"lng_credits_small_balance_title#other\" = \"{count} Stars Needed\";\n\"lng_credits_small_balance_about\" = \"Buy **Stars** and use them on **{bot}** and other miniapps.\";\n\"lng_credits_small_balance_reaction\" = \"Buy **Stars** and send them to {channel} to support their posts.\";\n\"lng_credits_small_balance_video_stream\" = \"Buy **Stars** to send them to {name} to support their stream.\";\n\"lng_credits_small_balance_subscribe\" = \"Buy **Stars** and subscribe to **{channel}** and other channels.\";\n\"lng_credits_small_balance_star_gift\" = \"Buy **Stars** to send gifts to {user} and other contacts.\";\n\"lng_credits_small_balance_for_message\" = \"Buy **Stars** to send messages to {user}.\";\n\"lng_credits_small_balance_for_messages\" = \"Buy **Stars** to send messages.\";\n\"lng_credits_small_balance_for_suggest\" = \"Buy **Stars** to suggest post to {channel}.\";\n\"lng_credits_small_balance_for_offer\" = \"Buy **Stars** to offer for this gift.\";\n\"lng_credits_small_balance_for_search\" = \"Buy **Stars** to search through public posts.\";\n\"lng_credits_small_balance_fallback\" = \"Buy **Stars** to unlock content and services on Telegram.\";\n\"lng_credits_purchase_blocked\" = \"Sorry, you can't purchase this item with Telegram Stars.\";\n\"lng_credits_enough\" = \"You have enough stars at the moment. {link}\";\n\"lng_credits_enough_link\" = \"Buy anyway\";\n\n\"lng_credits_gift_title\" = \"Gift Telegram Stars\";\n\n\"lng_location_title\" = \"Location\";\n\"lng_location_about\" = \"Display the location of your business on your account.\";\n\"lng_location_address\" = \"Enter Address\";\n\"lng_location_set_map\" = \"Set Location on Map\";\n\"lng_location_fallback\" = \"You can set your location on the map from your mobile device.\";\n\n\"lng_hours_title\" = \"Business Hours\";\n\"lng_hours_about\" = \"Turn this on to show your opening hours schedule to your customers.\";\n\"lng_hours_show\" = \"Show Business Hours\";\n\"lng_hours_time_zone\" = \"Time Zone\";\n\"lng_hours_monday\" = \"Monday\";\n\"lng_hours_tuesday\" = \"Tuesday\";\n\"lng_hours_wednesday\" = \"Wednesday\";\n\"lng_hours_thursday\" = \"Thursday\";\n\"lng_hours_friday\" = \"Friday\";\n\"lng_hours_saturday\" = \"Saturday\";\n\"lng_hours_sunday\" = \"Sunday\";\n\"lng_hours_closed\" = \"Closed\";\n\"lng_hours_open_full\" = \"Open 24 hours\";\n\"lng_hours_next_day\" = \"{time} (Next day)\";\n\"lng_hours_on_next_day\" = \"Next day {time}\";\n\"lng_hours_time_zone_title\" = \"Choose Time Zone\";\n\"lng_hours_add_button\" = \"Add a Set of Hours\";\n\"lng_hours_opening\" = \"Opening Time\";\n\"lng_hours_closing\" = \"Closing Time\";\n\"lng_hours_remove\" = \"Remove\";\n\"lng_hours_about_day\" = \"Specify your working hours during the day.\";\n\n\"lng_replies_title\" = \"Quick Replies\";\n\"lng_replies_about\" = \"Set up shortcuts with rich text and media to respond to messages faster.\";\n\"lng_replies_add\" = \"Add Quick Reply\";\n\"lng_replies_add_title\" = \"New Quick Reply\";\n\"lng_replies_add_shortcut\" = \"Add a shortcut for your reply.\";\n\"lng_replies_add_placeholder\" = \"Shortcut\";\n\"lng_replies_add_exists\" = \"This shortcut already exists.\";\n\"lng_replies_empty_title\" = \"New Quick Reply\";\n\"lng_replies_empty_about\" = \"Enter a message below that will be sent in chat when you type {shortcut}.\\n\\nYou can access Quick Replies in any chat by typing /.\";\n\"lng_replies_remove_title\" = \"Remove Shortcut\";\n\"lng_replies_remove_text\" = \"You didn't create a quick reply message. Do you want to remove the shortcut?\";\n\"lng_replies_edit_title\" = \"Edit Shortcut\";\n\"lng_replies_edit_about\" = \"Change the name of your shortcut.\";\n\"lng_replies_message_placeholder\" = \"Add a Quick Reply\";\n\"lng_replies_delete_sure\" = \"Are you sure you want to delete this quick reply with all its messages?\";\n\"lng_replies_error_occupied\" = \"This shortcut is already used.\";\n\"lng_replies_edit_button\" = \"Edit Quick Replies\";\n\n\"lng_greeting_title\" = \"Greeting Message\";\n\"lng_greeting_about\" = \"Greet customers when they message you the first time or after a period of no activity.\";\n\"lng_greeting_enable\" = \"Send Greeting Message\";\n\"lng_greeting_create\" = \"Create a Greeting Message\";\n\"lng_greeting_recipients\" = \"Recipients\";\n\"lng_greeting_select\" = \"Select chats or entire chat categories for sending greeting messages.\";\n\"lng_greeting_period_title\" = \"Period of no activity\";\n\"lng_greeting_period_about\" = \"Choose how many days should pass after your last interaction with a recipient to send them a greeting in response to their message.\";\n\"lng_greeting_empty_title\" = \"New Greeting Message\";\n\"lng_greeting_empty_about\" = \"Create greetings that will be automatically sent to new customers.\";\n\"lng_greeting_message_placeholder\" = \"Add a Greeting\";\n\"lng_greeting_limit_reached\" = \"You have too many quick replies. Remove one to add a greeting message.\";\n\"lng_greeting_recipients_empty\" = \"Please choose at least one recipient.\";\n\n\"lng_away_title\" = \"Away Message\";\n\"lng_away_about\" = \"Automatically reply with a message when you are away.\";\n\"lng_away_enable\" = \"Send Away Message\";\n\"lng_away_create\" = \"Create an Away Message\";\n\"lng_away_schedule\" = \"Schedule\";\n\"lng_away_schedule_always\" = \"Send Always\";\n\"lng_away_schedule_outside\" = \"Outside of Business Hours\";\n\"lng_away_schedule_custom\" = \"Custom Schedule\";\n\"lng_away_custom_start\" = \"Start Time\";\n\"lng_away_custom_end\" = \"End Time\";\n\"lng_away_offline_only\" = \"Only if Offline\";\n\"lng_away_offline_only_about\" = \"Don't send the away message if you've recently been online.\";\n\"lng_away_recipients\" = \"Recipients\";\n\"lng_away_select\" = \"Select chats or entire chat categories for sending an away message.\";\n\"lng_away_empty_title\" = \"New Away Message\";\n\"lng_away_empty_about\" = \"Add messages that will be automatically sent when you are off.\";\n\"lng_away_message_placeholder\" = \"Add an Away Message\";\n\"lng_away_limit_reached\" = \"You have too many quick replies. Remove one to add an away message.\";\n\n\"lng_business_edit_messages\" = \"Edit messages\";\n\"lng_business_limit_reached#one\" = \"Limit of {count} message reached.\";\n\"lng_business_limit_reached#other\" = \"Limit of {count} messages reached.\";\n\n\"lng_chatbots_title\" = \"Chatbots\";\n\"lng_chatbots_about\" = \"Add a bot to your account to help you automatically process and respond to the messages you receive. {link}\";\n\"lng_chatbots_about_link\" = \"Learn more...\";\n\"lng_chatbots_placeholder\" = \"Enter bot URL or username\";\n\"lng_chatbots_add_about\" = \"Enter the link to the Telegram bot that you want to automatically process your chats.\";\n\"lng_chatbots_access_title\" = \"Chats accessible for the bot\";\n\"lng_chatbots_all_except\" = \"All 1-to-1 Chats Except...\";\n\"lng_chatbots_selected\" = \"Only Selected Chats\";\n\"lng_chatbots_excluded_title\" = \"Excluded chats\";\n\"lng_chatbots_exclude_button\" = \"Exclude Chats\";\n\"lng_chatbots_included_title\" = \"Included chats\";\n\"lng_chatbots_include_button\" = \"Select Chats\";\n\"lng_chatbots_exclude_about\" = \"Select chats or entire chat categories which the bot will not have access to.\";\n\"lng_chatbots_permissions_title\" = \"Bot permissions\";\n\"lng_chatbots_warning_title\" = \"Warning\";\n\"lng_chatbots_warning_both_text\" = \"The bot {bot} will be able to **manage your gifts and stars**, including giving them away to other users.\";\n\"lng_chatbots_warning_gifts_text\" = \"The bot {bot} will be able to **manage your gifts**, including giving them away to other users.\";\n\"lng_chatbots_warning_stars_text\" = \"The bot {bot} will be able to **transfer your stars**.\";\n\"lng_chatbots_warning_username_text\" = \"The bot {bot} will be able to **set and remove usernames** for your account, which may result in the loss of your current username.\";\n\n\"lng_chatbots_manage_messages\" = \"Manage Messages\";\n\"lng_chatbots_read\" = \"Read Messages\";\n\"lng_chatbots_reply\" = \"Reply to Messages\";\n\"lng_chatbots_mark_as_read\" = \"Mark Messages as Read\";\n\"lng_chatbots_delete_sent\" = \"Delete Sent Messages\";\n\"lng_chatbots_delete_received\" = \"Delete Received Messages\";\n\n\"lng_chatbots_manage_profile\" = \"Manage Profile\";\n\"lng_chatbots_edit_name\" = \"Edit Name\";\n\"lng_chatbots_edit_bio\" = \"Edit Bio\";\n\"lng_chatbots_edit_userpic\" = \"Edit Profile Picture\";\n\"lng_chatbots_edit_username\" = \"Edit Username\";\n\n\"lng_chatbots_manage_gifts\" = \"Manage Gifts and Stars\";\n\"lng_chatbots_view_gifts\" = \"View Gifts\";\n\"lng_chatbots_sell_gifts\" = \"Sell Gifts\";\n\"lng_chatbots_gift_settings\" = \"Change Gift Settings\";\n\"lng_chatbots_transfer_gifts\" = \"Transfer and Upgrade Gifts\";\n\"lng_chatbots_transfer_stars\" = \"Transfer Stars\";\n\n\"lng_chatbots_manage_stories\" = \"Manage Stories\";\n\n\"lng_chatbots_remove\" = \"Remove Bot\";\n\"lng_chatbots_not_found\" = \"Chatbot not found.\";\n\"lng_chatbots_not_supported\" = \"This bot doesn't support Telegram Business yet.\";\n\"lng_chatbots_add\" = \"Add\";\n\"lng_chatbots_info_url\" = \"https://telegram.org/blog/telegram-business#chatbots-for-business\";\n\"lng_chatbot_status_can_reply\" = \"bot manages this chat\";\n\"lng_chatbot_status_paused\" = \"bot paused\";\n\"lng_chatbot_status_views\" = \"bot has access to this chat\";\n\"lng_chatbot_button_pause\" = \"Stop\";\n\"lng_chatbot_button_resume\" = \"Start\";\n\"lng_chatbot_menu_manage\" = \"Manage bot\";\n\"lng_chatbot_menu_remove\" = \"Remove bot from this chat\";\n\"lng_chatbot_menu_revoke\" = \"Revoke access to this chat\";\n\n\"lng_chat_intro_title\" = \"Start Page\";\n\"lng_chat_intro_subtitle\" = \"Customize your start page\";\n\"lng_chat_intro_default_title\" = \"No messages here yet...\";\n\"lng_chat_intro_default_message\" = \"Send a message or click on the greeting below\";\n\"lng_chat_intro_enter_title\" = \"Enter Title\";\n\"lng_chat_intro_enter_message\" = \"Enter Message\";\n\"lng_chat_intro_choose_sticker\" = \"Choose Sticker\";\n\"lng_chat_intro_random_sticker\" = \"Random\";\n\"lng_chat_intro_about\" = \"You can customize the message people see before they start a chat with you.\";\n\"lng_chat_intro_reset\" = \"Reset to Default\";\n\n\"lng_chat_links_title\" = \"Links to Chat\";\n\"lng_chat_links_about\" = \"Give your customers short links that start a chat with you – and suggest the first message from them to you.\";\n\"lng_chat_links_create_link\" = \"Create a Link to Chat\";\n\"lng_chat_links_footer\" = \"You can also use a simple link for a chat with you – {links}\";\n\"lng_chat_links_footer_both\" = \"{username} or {link}\";\n\"lng_chat_links_no_clicks\" = \"no clicks\";\n\"lng_chat_links_clicks#one\" = \"{count} click\";\n\"lng_chat_links_clicks#other\" = \"{count} clicks\";\n\"lng_chat_link_new_title\" = \"New Link\";\n\"lng_chat_link_edit_title\" = \"Edit Link\";\n\"lng_chat_link_description\" = \"Add a message that will be entered in the message field for anyone who starts a chat with you using this link.\";\n\"lng_chat_link_placeholder\" = \"Add Preset Message\";\n\"lng_chat_link_saved\" = \"Chat link saved.\";\n\"lng_chat_link_copy\" = \"Copy\";\n\"lng_chat_link_share\" = \"Share\";\n\"lng_chat_link_rename\" = \"Rename\";\n\"lng_chat_link_delete\" = \"Delete\";\n\"lng_chat_link_name\" = \"Link Name (optional)\";\n\"lng_chat_link_name_about\" = \"Add a name for this link that only you will see.\";\n\"lng_chat_link_delete_sure\" = \"Are you sure you want to delete this chat link?\";\n\"lng_chat_link_qr_title\" = \"Chat Link QR Code\";\n\"lng_chat_link_qr_about\" = \"Everyone on Telegram can scan this code to contact you.\";\n\"lng_chat_link_copied\" = \"Chat link copied to clipboard.\";\n\n\"lng_boost_channel_button\" = \"Boost Channel\";\n\"lng_boost_group_button\" = \"Boost Group\";\n\"lng_boost_again_button\" = \"Boost Again\";\n\"lng_boost_group_about\" = \"Boost your group to unlock additional\\nappearance settings.\";\n\"lng_boost_level#one\" = \"Level {count}\";\n\"lng_boost_level#other\" = \"Level {count}\";\n\"lng_boost_level_unlocks#one\" = \"Level {count} Unlocks:\";\n\"lng_boost_level_unlocks#other\" = \"Level {count} Unlocks:\";\n\"lng_boost_channel_title_first\" = \"Enable stories for this channel\";\n\"lng_boost_channel_title_first_group\" = \"Enable stories for group\";\n\"lng_boost_channel_needs_unlock#one\" = \"{channel} needs **{count}** more boost to unlock new features.\";\n\"lng_boost_channel_needs_unlock#other\" = \"{channel} needs **{count}** more boosts to unlock new features.\";\n//\"lng_boost_channel_needs_first#one\" = \"{channel} needs **{count}** more boost to enable posting stories. Help make it possible!\";\n//\"lng_boost_channel_needs_first#other\" = \"{channel} needs **{count}** more boosts to enable posting stories. Help make it possible!\";\n\"lng_boost_channel_title_more\" = \"Help upgrade channel\";\n\"lng_boost_channel_title_more_group\" = \"Help upgrade group\";\n//\"lng_boost_channel_needs_more#one\" = \"{channel} needs **{count}** more boost to be able to {post}.\";\n//\"lng_boost_channel_needs_more#other\" = \"{channel} needs **{count}** more boosts to be able to {post}.\";\n\"lng_boost_channel_title_max\" = \"Maximum level reached\";\n\"lng_boost_channel_you_title\" = \"You boosted {channel}!\";\n//\"lng_boost_channel_you_first#one\" = \"This channel needs **{count}** more boost\\nto enable stories.\";\n//\"lng_boost_channel_you_first#other\" = \"This channel needs **{count}** more boosts\\nto enable stories.\";\n//\"lng_boost_channel_you_more#one\" = \"This channel needs **{count}** more boost\\nto be able to {post}.\";\n//\"lng_boost_channel_you_more#other\" = \"This channel needs **{count}** more boosts\\nto be able to {post}.\";\n\"lng_boost_channel_reached_first\" = \"This channel reached **Level 1** and can now post stories.\";\n\"lng_boost_channel_reached_more#one\" = \"This channel reached **Level {count}** and can now {post}.\";\n\"lng_boost_channel_reached_more#other\" = \"This channel reached **Level {count}** and can now {post}.\";\n//\"lng_boost_channel_you_first_group#one\" = \"This group needs **{count}** more boost\\nto enable stories.\";\n//\"lng_boost_channel_you_first_group#other\" = \"This group needs **{count}** more boosts\\nto enable stories.\";\n//\"lng_boost_channel_you_more_group#one\" = \"This group needs **{count}** more boost\\nto be able to {post}.\";\n//\"lng_boost_channel_you_more_group#other\" = \"This group needs **{count}** more boosts\\nto be able to {post}.\";\n\"lng_boost_channel_reached_first_group\" = \"This group reached **Level 1** and can now post stories.\";\n\"lng_boost_channel_reached_more_group#one\" = \"This group reached **Level {count}** and can now {post}.\";\n\"lng_boost_channel_reached_more_group#other\" = \"This group reached **Level {count}** and can now {post}.\";\n\"lng_boost_channel_post_stories#one\" = \"post **{count} story** per day\";\n\"lng_boost_channel_post_stories#other\" = \"post **{count} stories** per day\";\n\"lng_boost_channel_features\" = \"Your boosts will help {channel} to unlock new features.\";\n\"lng_boost_group_lift_restrictions\" = \"Boost the group to remove messaging restrictions.\";\n\"lng_boost_group_lift_restrictions_many#one\" = \"Boost the group **{count} time** to remove messaging restrictions.\";\n\"lng_boost_group_lift_restrictions_many#other\" = \"Boost the group **{count} times** to remove messaging restrictions.\";\n\"lng_boost_error_gifted_title\" = \"Can't boost with gifted Premium!\";\n\"lng_boost_error_gifted_text\" = \"Because your **Telegram Premium** subscription was gifted to you, you can't use it to boost channels.\";\n\"lng_boost_error_gifted_text_group\" = \"Because your **Telegram Premium** subscription was gifted to you, you can't use it to boost groups.\";\n\"lng_boost_need_more\" = \"More boosts needed\";\n\"lng_boost_need_more_text#one\" = \"To boost {channel}, gift **Telegram Premium** to a friend and get **{count}** boost.\";\n\"lng_boost_need_more_text#other\" = \"To boost {channel}, gift **Telegram Premium** to a friend and get **{count}** boosts.\";\n\"lng_boost_need_more_again#one\" = \"To boost {channel} again, gift **Telegram Premium** to a friend and get **{count}** additional boost.\";\n\"lng_boost_need_more_again#other\" = \"To boost {channel} again, gift **Telegram Premium** to a friend and get **{count}** additional boosts.\";\n\"lng_boost_error_already_title\" = \"Already Boosted!\";\n\"lng_boost_error_already_text\" = \"You are already boosting this channel.\";\n\"lng_boost_error_already_text_group\" = \"You are already boosting this group.\";\n\"lng_boost_error_premium_title\" = \"Premium needed\";\n\"lng_boost_error_premium_text_group\" = \"Only **Telegram Premium** subscribers can boost groups. Do you want to subscribe to **Telegram Premium**?\";\n\"lng_boost_error_premium_text\" = \"Only **Telegram Premium** subscribers can boost channels. Do you want to subscribe to **Telegram Premium**?\";\n\"lng_boost_error_premium_yes\" = \"Yes\";\n\"lng_boost_error_flood_title\" = \"Can't boost too often!\";\n\"lng_boost_error_flood_text_group\" = \"You can change the group you boost only once a day. Next time you can boost is in {left}.\";\n\"lng_boost_error_flood_text\" = \"You can only change the channel you boost once a day. You will be able to boost in {left}.\";\n\"lng_boost_now_instead\" = \"You currently boost {channel}. Do you want to boost {other} instead?\";\n\"lng_boost_now_replace\" = \"Replace\";\n\"lng_boost_reassign_title\" = \"Reassign boost\";\n\"lng_boost_reassign_text\" = \"To boost {channel}, reassign a previous boost or {gift}.\";\n\"lng_boost_reassign_gift#one\" = \"gift **Telegram Premium** to a friend to get **{count}** additional boost\";\n\"lng_boost_reassign_gift#other\" = \"gift **Telegram Premium** to a friend to get **{count}** additional boosts\";\n\"lng_boost_remove_title\" = \"Remove your boost from\";\n\"lng_boost_reassign_button\" = \"Reassign\";\n\"lng_boost_available_in\" = \"available in {duration}\";\n\"lng_boost_available_in_toast#one\" = \"Wait until the boost is available or get **{count}** more boost by gifting a **Telegram Premium** subscription.\";\n\"lng_boost_available_in_toast#other\" = \"Wait until the boost is available or get **{count}** more boosts by gifting a **Telegram Premium** subscription.\";\n\"lng_boost_reassign_done#one\" = \"{count} boost is reassigned from {channels}.\";\n\"lng_boost_reassign_done#other\" = \"{count} boosts are reassigned from {channels}.\";\n\"lng_boost_reassign_channels#one\" = \"{count} channel\";\n\"lng_boost_reassign_channels#other\" = \"{count} channels\";\n\"lng_boost_reassign_groups#one\" = \"{count} group\";\n\"lng_boost_reassign_groups#other\" = \"{count} groups\";\n\"lng_boost_reassign_mixed#one\" = \"{count} group or channel\";\n\"lng_boost_reassign_mixed#other\" = \"{count} groups and channels\";\n\n\"lng_boost_channel_title_color\" = \"Enable colors\";\n\"lng_boost_channel_needs_level_color#one\" = \"Your channel needs to reach **Level {count}** to change channel color.\";\n\"lng_boost_channel_needs_level_color#other\" = \"Your channel needs to reach **Level {count}** to change channel color.\";\n\n\"lng_boost_channel_title_wallpaper\" = \"Enable wallpapers\";\n\"lng_boost_channel_needs_level_wallpaper#one\" = \"Your channel needs to reach **Level {count}** to change channel wallpaper.\";\n\"lng_boost_channel_needs_level_wallpaper#other\" = \"Your channel needs to reach **Level {count}** to change channel wallpaper.\";\n\"lng_boost_group_needs_level_wallpaper#one\" = \"Your group needs to reach **Level {count}** to change group wallpaper.\";\n\"lng_boost_group_needs_level_wallpaper#other\" = \"Your group needs to reach **Level {count}** to change group wallpaper.\";\n\n\"lng_boost_channel_title_status\" = \"Enable emoji status\";\n\"lng_boost_channel_needs_level_status#one\" = \"Your channel needs to reach **Level {count}** to set emoji status.\";\n\"lng_boost_channel_needs_level_status#other\" = \"Your channel needs to reach **Level {count}** to set emoji status.\";\n\"lng_boost_group_needs_level_status#one\" = \"Your group needs to reach **Level {count}** to set emoji status.\";\n\"lng_boost_group_needs_level_status#other\" = \"Your group needs to reach **Level {count}** to set emoji status.\";\n\n\"lng_boost_channel_title_reactions\" = \"Custom reactions\";\n\"lng_boost_channel_needs_level_reactions#one\" = \"Your channel needs to reach **Level {count}** to add **{same_count}** custom emoji as a reaction.\";\n\"lng_boost_channel_needs_level_reactions#other\" = \"Your channel needs to reach **Level {count}** to add **{same_count}** custom emoji as reactions.\";\n\n\"lng_boost_channel_title_cpm\" = \"Boost Channel\";\n\"lng_boost_channel_needs_level_cpm#one\" = \"Your channel needs to reach **Level {count}** to switch off ads.\";\n\"lng_boost_channel_needs_level_cpm#other\" = \"Your channel needs to reach **Level {count}** to switch off ads.\";\n\n\"lng_boost_group_title_emoji\" = \"Enable emoji pack\";\n\"lng_boost_group_needs_level_emoji#one\" = \"Your group needs to reach **Level {count}** to set emoji pack.\";\n\"lng_boost_group_needs_level_emoji#other\" = \"Your group needs to reach **Level {count}** to set emoji pack.\";\n\n\"lng_boost_channel_title_wear\" = \"Wear Item\";\n\"lng_boost_channel_needs_level_wear#one\" = \"Your channel needs **Level {count}** to wear collectibles.\";\n\"lng_boost_channel_needs_level_wear#other\" = \"Your channel needs **Level {count}** to wear collectibles.\";\n\n\"lng_boost_channel_ask\" = \"Ask your **Premium** subscribers to boost your channel with this link:\";\n\"lng_boost_channel_ask_button\" = \"Copy Link\";\n//\"lng_boost_channel_or\" = \"or\";\n//\"lng_boost_channel_gifting\" = \"Boost your channel by gifting your subscribers Telegram Premium. {link}\";\n//\"lng_boost_channel_gifting_link\" = \"Get boosts >\";\n\"lng_boost_group_ask\" = \"Ask your **Premium** members to boost your group with this link:\";\n//\"lng_boost_group_gifting\" = \"Boost your group by gifting your members Telegram Premium. {link}\";\n\n\"lng_boost_channel_title_autotranslate\" = \"Autotranslation of Messages\";\n\"lng_boost_channel_needs_level_autotranslate#one\" = \"Your channel needs to reach **Level {count}** to enable autotranslation of messages.\";\n\"lng_boost_channel_needs_level_autotranslate#other\" = \"Your channel needs to reach **Level {count}** to enable autotranslation of messages.\";\n\n\"lng_feature_stories#one\" = \"**{count}** Story Per Day\";\n\"lng_feature_stories#other\" = \"**{count}** Stories Per Day\";\n\"lng_feature_reactions#one\" = \"**{count}** Custom Reaction\";\n\"lng_feature_reactions#other\" = \"**{count}** Custom Reactions\";\n\"lng_feature_name_color_channel#one\" = \"**{count}** Channel Name Color\";\n\"lng_feature_name_color_channel#other\" = \"**{count}** Channel Name Colors\";\n\"lng_feature_link_style_channel#one\" = \"**{count}** Style for Links and Quotes\";\n\"lng_feature_link_style_channel#other\" = \"**{count}** Styles for Links and Quotes\";\n\"lng_feature_link_emoji\" = \"Custom Logo for Links and Quotes\";\n\"lng_feature_emoji_status\" = \"**1000+** Emoji Statuses\";\n\"lng_feature_backgrounds_channel#one\" = \"**{count}** Channel Background\";\n\"lng_feature_backgrounds_channel#other\" = \"**{count}** Channel Backgrounds\";\n\"lng_feature_custom_background_channel\" = \"Custom Channel Background\";\n\"lng_feature_backgrounds_group#one\" = \"**{count}** Group Background\";\n\"lng_feature_backgrounds_group#other\" = \"**{count}** Group Backgrounds\";\n\"lng_feature_custom_background_group\" = \"Custom Group Background\";\n\"lng_feature_custom_emoji_pack\" = \"Custom Emoji Pack\";\n\"lng_feature_transcribe\" = \"Voice-to-Text Conversion\";\n\"lng_feature_autotranslate\" = \"Autotranslation of Messages\";\n\"lng_feature_profile_color_channel#one\" = \"**{count}** Color for Channel Cover\";\n\"lng_feature_profile_color_channel#other\" = \"**{count}** Colors for Channel Cover\";\n\"lng_feature_profile_color_group#one\" = \"**{count}** Color for Group Cover\";\n\"lng_feature_profile_color_group#other\" = \"**{count}** Colors for Group Cover\";\n\"lng_feature_profile_icon_channel\" = \"Custom Logo for Channel Cover\";\n\"lng_feature_profile_icon_group\" = \"Custom Logo for Group Cover\";\n\n\"lng_edit_topics_enable\" = \"Enable Topics\";\n\"lng_edit_topics_about\" = \"The group chat will be divided into topics created by admins or users.\";\n\"lng_edit_topics_layout\" = \"Topics layout\";\n\"lng_edit_topics_layout_about\" = \"Choose how topics appear for all members.\";\n\"lng_edit_topics_tabs\" = \"Tabs\";\n\"lng_edit_topics_list\" = \"List\";\n\n\"lng_giveaway_new_title\" = \"Boosts via Gifts\";\n\"lng_giveaway_new_about\" = \"Get more boosts for your channel by gifting Premium to your subscribers.\";\n\"lng_giveaway_new_about_group\" = \"Get more boosts for your group by gifting Premium to your members.\";\n\"lng_giveaway_credits_new_about\" = \"Get more boosts and subscribers for your channel by giving away prizes.\";\n\"lng_giveaway_credits_new_about_group\" = \"Get more boosts and members for your group by giving away prizes.\";\n\"lng_giveaway_create_option\" = \"Create Giveaway\";\n\"lng_giveaway_create_subtitle\" = \"winners are chosen randomly\";\n\"lng_giveaway_award_option\" = \"Award Specific Users\";\n\"lng_giveaway_award_subtitle\" = \"Select recipients >\";\n\"lng_giveaway_award_chosen#one\" = \"{count} recipient >\";\n\"lng_giveaway_award_chosen#other\" = \"{count} recipients >\";\n\"lng_giveaway_quantity_title\" = \"Quantity of prizes\";\n\"lng_giveaway_quantity#one\" = \"{count} Boost\";\n\"lng_giveaway_quantity#other\" = \"{count} Boosts\";\n\"lng_giveaway_quantity_about\" = \"Choose how many Premium subscriptions to give away and boosts to receive.\";\n\"lng_giveaway_channels_title\" = \"Channels included in the giveaway\";\n\"lng_giveaway_channels_this#one\" = \"this channel will receive {count} boost\";\n\"lng_giveaway_channels_this#other\" = \"this channel will receive {count} boosts\";\n\"lng_giveaway_channels_this_group#one\" = \"this group will receive {count} boost\";\n\"lng_giveaway_channels_this_group#other\" = \"this group will receive {count} boosts\";\n\"lng_giveaway_channels_add\" = \"Add Group or Channel\";\n\"lng_giveaway_channels_about\" = \"Choose the groups and channels users need to join to take part in the giveaway.\";\n\"lng_giveaway_users_title\" = \"Users eligible for the giveaway\";\n\"lng_giveaway_users_all\" = \"All subscribers\";\n\"lng_giveaway_users_all_group\" = \"All members\";\n\"lng_giveaway_users_from_all_countries\" = \"from all countries\";\n\"lng_giveaway_users_from_one_country\" = \"from {country}\";\n\"lng_giveaway_users_from_countries#one\" = \"from {count} country\";\n\"lng_giveaway_users_from_countries#other\" = \"from {count} countries\";\n\"lng_giveaway_users_new\" = \"Only new subscribers\";\n\"lng_giveaway_users_new_group\" = \"Only new members\";\n\"lng_giveaway_users_about\" = \"Choose if you want to limit the giveaway only to those who joined the channel after the giveaway started or to users from specific countries.\";\n\"lng_giveaway_users_about_group\" = \"Choose if you want to limit the giveaway only to those who joined the group after the giveaway started or to members from specific countries.\";\n\"lng_giveaway_start\" = \"Start Giveaway\";\n\"lng_giveaway_award\" = \"Gift Premium\";\n\"lng_giveaway_random_button\" = \"Choose randomly\";\n\"lng_giveaway_start_sure\" = \"Are you sure you want to start this prepaid giveaway now? This action cannot be undone.\";\n\"lng_giveaway_date_title\" = \"Date when giveaway ends\";\n\"lng_giveaway_date\" = \"Date and Time\";\n\"lng_giveaway_date_about#one\" = \"Choose when {count} subscriber of your channel will be randomly selected to receive Telegram Premium.\";\n\"lng_giveaway_date_about#other\" = \"Choose when {count} subscribers of your channel will be randomly selected to receive Telegram Premium.\";\n\"lng_giveaway_date_about_group#one\" = \"Choose when {count} members of your group will be randomly selected to receive Telegram Premium.\";\n\"lng_giveaway_date_about_group#other\" = \"Choose when {count} members of your group will be randomly selected to receive Telegram Premium.\";\n\"lng_giveaway_duration_title#one\" = \"Duration of Premium subscription\";\n\"lng_giveaway_duration_title#other\" = \"Duration of Premium subscriptions\";\n\"lng_giveaway_duration_price\" = \"{price} x {amount}\";\n\"lng_giveaway_date_select\" = \"Select Date and Time\";\n\"lng_giveaway_date_confirm\" = \"Confirm\";\n\"lng_giveaway_recipients_save\" = \"Save Recipients\";\n\"lng_giveaway_recipients_deselect\" = \"Deselect All\";\n\"lng_giveaway_maximum_countries_error#one\" = \"You can select up to {count} country.\";\n\"lng_giveaway_maximum_countries_error#other\" = \"You can select up to {count} countries.\";\n\"lng_giveaway_maximum_channels_error#one\" = \"You can select {count} group or channel.\";\n\"lng_giveaway_maximum_channels_error#other\" = \"You can select up to {count} groups and channels.\";\n\"lng_giveaway_maximum_users_error#one\" = \"You can select up to {count} subscribers.\";\n\"lng_giveaway_maximum_users_error#other\" = \"You can select up to {count} subscribers.\";\n\"lng_giveaway_channels_confirm_title\" = \"Channel is Private\";\n\"lng_giveaway_channels_confirm_about\" = \"Are you sure you want to add a private channel? Users won't be able to join it without an invite link.\";\n\"lng_giveaway_additional_prizes\" = \"Additional prizes\";\n\"lng_giveaway_additional_about\" = \"Turn this on if you want to give the winners your own prizes in addition to Premium subscriptions.\";\n\"lng_giveaway_additional_prizes_ph\" = \"Enter your prize\";\n\"lng_giveaway_prizes_just_premium#one\" = \"All prizes: **{count}** Telegram Premium subscription {duration}.\";\n\"lng_giveaway_prizes_just_premium#other\" = \"All prizes: **{count}** Telegram Premium subscriptions {duration}.\";\n\"lng_giveaway_prizes_additional#one\" = \"All prizes: **{count}** {prize} with Telegram Premium subscription {duration}.\";\n\"lng_giveaway_prizes_additional#other\" = \"All prizes: **{count}** {prize} with Telegram Premium subscriptions {duration}.\";\n\"lng_giveaway_additional_credits_about\" = \"Turn this on if you want to give the winners your own prizes in addition to Stars.\";\n\"lng_giveaway_prizes_just_credits#one\" = \"All prizes: **{count}** Star.\";\n\"lng_giveaway_prizes_just_credits#other\" = \"All prizes: **{count}** Stars.\";\n\"lng_giveaway_prizes_additional_credits#one\" = \"All prizes: **{count}** {prize} with {amount}.\";\n\"lng_giveaway_prizes_additional_credits#other\" = \"All prizes: **{count}** {prize} with {amount}.\";\n\"lng_giveaway_prizes_additional_credits_amount#one\" = \"{count} Star\";\n\"lng_giveaway_prizes_additional_credits_amount#other\" = \"{count} Stars\";\n\"lng_giveaway_show_winners\" = \"Show winners\";\n\"lng_giveaway_show_winners_about\" = \"Choose whether to make the list of winners public when the giveaway ends.\";\n\n\"lng_giveaway_created_title\" = \"Giveaway created\";\n\"lng_giveaway_created_body\" = \"Check your channels' {link} to see how this giveaway boosted your channel.\";\n\"lng_giveaway_created_body_group\" = \"Check your groups' {link} to see how this giveaway boosted your group.\";\n\"lng_giveaway_awarded_title\" = \"Premium subscriptions gifted\";\n\"lng_giveaway_awarded_body\" = \"Check your channels' {link} to see how gifts boosted your channel.\";\n\"lng_giveaway_awarded_body_group\" = \"Check your groups' {link} to see how gifts boosted your group.\";\n\"lng_giveaway_created_link\" = \"Statistics\";\n\n\"lng_giveaway_credits_options_title\" = \"Stars to distribute\";\n\"lng_giveaway_credits_option_status#one\" = \"{count} per user\";\n\"lng_giveaway_credits_option_status#other\" = \"{count} per user\";\n\"lng_giveaway_credits_options_about\" = \"Choose how many stars to give away and how many boosts to receive for 1 year.\";\n\"lng_giveaway_credits_quantity_title\" = \"Number of winners\";\n\"lng_giveaway_credits_quantity_about\" = \"Choose how many winners you want to distribute stars among.\";\n\n\"lng_prize_title\" = \"Congratulations!\";\n\"lng_prize_about\" = \"You won a prize in a giveaway organized by {channel}.\";\n\"lng_prize_duration\" = \"Your prize is a **Telegram Premium** subscription {duration}.\";\n\"lng_prize_credits\" = \"Your prize is {amount}.\";\n\"lng_prize_credits_amount#one\" = \"{count} Star\";\n\"lng_prize_credits_amount#other\" = \"{count} Stars\";\n\"lng_prize_gift_about\" = \"You've received a gift from {channel}.\";\n\"lng_prize_gift_duration\" = \"Your gift is a **Telegram Premium** subscription {duration}.\";\n\"lng_prize_open\" = \"Open Gift Link\";\n\"lng_prize_unclaimed_title\" = \"Unclaimed Prize\";\n\"lng_prize_unclaimed_about\" = \"You have an unclaimed prize from a giveaway by {channel}.\";\n\"lng_prize_unclaimed_duration\" = \"This prize is a **Telegram Premium** subscription {duration}.\";\n\n\"lng_prizes_title#one\" = \"Giveaway Prize\";\n\"lng_prizes_title#other\" = \"Giveaway Prizes\";\n\"lng_prizes_additional#one\" = \"**{count}** {prize}\";\n\"lng_prizes_additional#other\" = \"**{count}** {prize}\";\n\"lng_prizes_additional_with\" = \"with\";\n\"lng_prizes_about#one\" = \"**{count}** Telegram Premium Subscription {duration}.\";\n\"lng_prizes_about#other\" = \"**{count}** Telegram Premium Subscriptions {duration}.\";\n\"lng_prizes_credits_about_single\" = \"**{amount}** will be distributed to the **1** winner.\";\n\"lng_prizes_credits_about#one\" = \"**{amount}** will be distributed among {count} winner.\";\n\"lng_prizes_credits_about#other\" = \"**{amount}** will be distributed among {count} winners.\";\n\"lng_prizes_credits_about_amount#one\" = \"{count} Star\";\n\"lng_prizes_credits_about_amount#other\" = \"{count} Stars\";\n\"lng_prizes_participants\" = \"Participants\";\n\"lng_prizes_participants_all#one\" = \"All subscribers of the channel:\";\n\"lng_prizes_participants_all#other\" = \"All subscribers of the channels:\";\n\"lng_prizes_participants_all_group#one\" = \"All members of the group:\";\n\"lng_prizes_participants_all_group#other\" = \"All members of the groups:\";\n\"lng_prizes_participants_all_mixed#one\" = \"All members of the group:\";\n\"lng_prizes_participants_all_mixed#other\" = \"All members of the groups and channels:\";\n\"lng_prizes_participants_new#one\" = \"All users who joined the channel below after this announcement:\";\n\"lng_prizes_participants_new#other\" = \"All users who joined the channels below after this announcement:\";\n\"lng_prizes_participants_new_group#one\" = \"All users who joined the group below after this date:\";\n\"lng_prizes_participants_new_group#other\" = \"All users who joined the groups below after this date:\";\n\"lng_prizes_participants_new_mixed#one\" = \"All users who joined the group below after this date:\";\n\"lng_prizes_participants_new_mixed#other\" = \"All users who joined the groups and channels below after this date:\";\n\"lng_prizes_countries\" = \"from {countries}\";\n\"lng_prizes_countries_and_one\" = \"{countries}, {country}\";\n\"lng_prizes_countries_and_last\" = \"{countries} and {country}\";\n\"lng_prizes_date\" = \"Winners Selection Date\";\n\"lng_prizes_how_works\" = \"Learn more\";\n\"lng_prizes_how_title\" = \"About this giveaway\";\n\"lng_prizes_end_title\" = \"Giveaway ended\";\n\"lng_prizes_how_text\" = \"This giveaway is sponsored by {admins}.\";\n\"lng_prizes_end_text\" = \"This giveaway was sponsored by {admins}.\";\n\"lng_prizes_admins#one\" = \"the admins of {channel}, who acquired **{count} Telegram Premium** subscription {duration} for its followers\";\n\"lng_prizes_admins#other\" = \"the admins of {channel}, who acquired **{count} Telegram Premium** subscriptions {duration} for its followers\";\n\"lng_prizes_admins_group#one\" = \"the admins of {channel}, who acquired **{count} Telegram Premium** subscription {duration} for its members\";\n\"lng_prizes_admins_group#other\" = \"the admins of {channel}, who acquired **{count} Telegram Premium** subscriptions {duration} for its members\";\n\"lng_prizes_credits_admins\" = \"the admins of {channel}, who aquired **{amount}** for its followers\";\n\"lng_prizes_credits_admins_group\" = \"the admins of {channel}, who aquired **{amount}** for its members\";\n\"lng_prizes_credits_admins_amount#one\" = \"{count} Star\";\n\"lng_prizes_credits_admins_amount#other\" = \"{count} Stars\";\n\"lng_prizes_additional_added#one\" = \"{channel} also included **{count} {prize}** in the prize. Admins of the channel are responsible for delivering this prize.\";\n\"lng_prizes_additional_added#other\" = \"{channel} also included **{count} {prize}** in the prizes. Admins of the channel are responsible for delivering these prizes.\";\n\"lng_prizes_additional_added_group#one\" = \"{channel} also included **{count} {prize}** in the prize. Admins of the group are responsible for delivering this prize.\";\n\"lng_prizes_additional_added_group#other\" = \"{channel} also included **{count} {prize}** in the prizes. Admins of the group are responsible for delivering these prizes.\";\n\"lng_prizes_how_when_finish\" = \"On {date}, Telegram will automatically select {winners}.\";\n\"lng_prizes_end_when_finish\" = \"On {date}, Telegram automatically selected {winners}.\";\n\"lng_prizes_end_activated#one\" = \"**{count}** winner already used their gift link.\";\n\"lng_prizes_end_activated#other\" = \"**{count}** of the winners already used their gift links.\";\n\"lng_prizes_winners_all_of_one#one\" = \"{count} random subscriber of {channel}\";\n\"lng_prizes_winners_all_of_one#other\" = \"{count} random subscribers of {channel}\";\n\"lng_prizes_winners_all_of_one_group#one\" = \"{count} random member of {channel}\";\n\"lng_prizes_winners_all_of_one_group#other\" = \"{count} random members of {channel}\";\n\"lng_prizes_winners_all_of_many#one\" = \"{count} random subscriber of {channel} and other listed groups and channels\";\n\"lng_prizes_winners_all_of_many#other\" = \"{count} random subscribers of {channel} and other listed groups and channels\";\n\"lng_prizes_winners_all_of_many_group#one\" = \"{count} random member of {channel} and other listed groups and channels\";\n\"lng_prizes_winners_all_of_many_group#other\" = \"{count} random members of {channel} and other listed groups and channels\";\n\"lng_prizes_winners_new_of_one#one\" = \"{count} random user that joined {channel} after {start_date}\";\n\"lng_prizes_winners_new_of_one#other\" = \"{count} random users that joined {channel} after {start_date}\";\n\"lng_prizes_winners_new_of_many#one\" = \"{count} random user that joined {channel} and other listed groups and channels after {start_date}\";\n\"lng_prizes_winners_new_of_many#other\" = \"{count} random users that joined {channel} and other listed groups and channels after {start_date}\";\n\"lng_prizes_how_participate_one\" = \"To take part in this giveaway please join the channel {channel} before {date}.\";\n\"lng_prizes_how_participate_many\" = \"To take part in this giveaway please join channel {channel} and other listed groups and channels before {date}.\";\n\"lng_prizes_how_no_admin\" = \"You are not eligible to participate in this giveaway because you are an admin of a participating channel ({channel}).\";\n\"lng_prizes_how_no_admin_group\" = \"You are not eligible to participate in this giveaway, because you are an admin of participating group ({channel}).\";\n\"lng_prizes_how_no_joined\" = \"You are not eligible to participate in this giveaway because you joined this channel on {date}, which is before the giveaway started.\";\n\"lng_prizes_how_no_joined_group\" = \"You are not eligible to participate in this giveaway, because you joined this group on {date}, which is before the contest started.\";\n\"lng_prizes_how_no_country\" = \"You are not eligible to participate in this giveaway because your country is not included in the terms of the giveaway.\";\n\"lng_prizes_how_yes_joined_one\" = \"You are participating in this giveaway because you joined the channel {channel}.\";\n\"lng_prizes_how_yes_joined_many\" = \"You are participating in this giveaway because you joined the channel {channel} (and other listed groups and channels).\";\n\"lng_prizes_you_won\" = \"You won a prize in this giveaway {cup}\";\n\"lng_prizes_you_won_credits\" = \"You won {amount} in this giveaway {cup}\";\n\"lng_prizes_you_won_credits_amount#one\" = \"{count} Star\";\n\"lng_prizes_you_won_credits_amount#other\" = \"{count} Stars\";\n\"lng_prizes_view_prize\" = \"View my prize\";\n\"lng_prizes_you_didnt\" = \"You didn't win a prize in this giveaway.\";\n\"lng_prizes_cancelled\" = \"The channel canceled the prizes by refunding the payment for them.\";\n\"lng_prizes_cancelled_group\" = \"The group canceled the prizes by refunding the payment for them.\";\n\"lng_prizes_badge\" = \"x{amount}\";\n\n\"lng_prizes_results_title\" = \"Winners Selected!\";\n\"lng_prizes_results_title_one\" = \"Winner Selected!\";\n\"lng_prizes_results_about#one\" = \"**{count}** winner of the {link} was randomly selected by Telegram.\";\n\"lng_prizes_results_about#other\" = \"**{count}** winners of the {link} were randomly selected by Telegram.\";\n\"lng_prizes_results_link\" = \"Giveaway\";\n\"lng_prizes_results_winner\" = \"Winner\";\n\"lng_prizes_results_winners\" = \"Winners\";\n\"lng_prizes_results_more#one\" = \"and {count} more!\";\n\"lng_prizes_results_more#other\" = \"and {count} more!\";\n\"lng_prizes_results_one\" = \"The winner received their gift link in a private message.\";\n\"lng_prizes_results_all\" = \"All winners received gift links in private messages.\";\n\"lng_prizes_credits_results_one#one\" = \"The winner received {count} Star.\";\n\"lng_prizes_credits_results_one#other\" = \"The winner received {count} Stars.\";\n\"lng_prizes_credits_results_all#one\" = \"All winners received {count} Star in total.\";\n\"lng_prizes_credits_results_all#other\" = \"All winners received {count} Stars in total.\";\n\"lng_prizes_results_some\" = \"Some winners couldn't be selected.\";\n\n\"lng_gift_link_title\" = \"Gift Link\";\n\"lng_gift_link_about\" = \"This link allows you to activate\\na **Telegram Premium** subscription.\";\n\"lng_gift_link_label_from\" = \"From\";\n\"lng_gift_link_label_to\" = \"To\";\n\"lng_gift_link_label_to_unclaimed\" = \"No recipient\";\n\"lng_gift_link_label_gift\" = \"Gift\";\n\"lng_gift_link_gift_premium\" = \"Telegram Premium {duration}\";\n\"lng_gift_link_label_reason\" = \"Reason\";\n\"lng_gift_link_reason_giveaway\" = \"Giveaway\";\n\"lng_gift_link_reason_unclaimed\" = \"Incomplete Giveaway\";\n\"lng_gift_link_reason_chosen\" = \"You were selected by the channel\";\n\"lng_gift_link_label_date\" = \"Date\";\n\"lng_gift_link_label_first_sale\" = \"First Sale\";\n\"lng_gift_link_label_last_sale\" = \"Last Sale\";\n\"lng_gift_link_label_value\" = \"Value\";\n\"lng_gift_link_also_send\" = \"You can also {link} to a friend as a gift.\";\n\"lng_gift_link_also_send_link\" = \"send this link\";\n\"lng_gift_link_use\" = \"Use Link\";\n\"lng_gift_link_already_title\" = \"You already have Telegram Premium\";\n\"lng_gift_link_already_about\" = \"You can activate this link after {date} or {link} to a friend.\";\n\"lng_gift_link_already_link\" = \"send the link\";\n\"lng_gift_link_used_title\" = \"Used Gift Link\";\n\"lng_gift_link_used_about\" = \"This link was used to activate\\na **Telegram Premium** subscription.\";\n\"lng_gift_link_used_footer\" = \"This link was used on {date}.\";\n\"lng_gift_link_expired\" = \"Gift code link expired\";\n\"lng_gift_link_pending_about\" = \"This link allows {user} to activate\\na **Telegram Premium** subscription.\";\n\"lng_gift_link_pending_toast\" = \"Only the recipient can see the link.\";\n\"lng_gift_link_pending_footer\" = \"This link hasn't been activated yet.\";\n\n\"lng_gift_stars_title#one\" = \"{count} Star\";\n\"lng_gift_stars_title#other\" = \"{count} Stars\";\n\"lng_gift_stars_outgoing\" = \"With Stars, {user} will be able to unlock content and services on Telegram.\";\n\"lng_gift_stars_incoming\" = \"Use Stars to unlock content and services on Telegram.\";\n\"lng_gift_until\" = \"Until\";\n\n\"lng_gift_ton_amount#one\" = \"{count} TON\";\n\"lng_gift_ton_amount#other\" = \"{count} TON\";\n\n\"lng_gift_premium_title\" = \"Premium Gift\";\n\"lng_gift_premium_text#one\" = \"Subscribe to **Telegram Premium** to send up to **{count}** of these gifts and unlock access to multiple additional features.\";\n\"lng_gift_premium_text#other\" = \"Subscribe to **Telegram Premium** to send up to **{count}** of these gifts and unlock access to multiple additional features.\";\n\"lng_gift_premium_or_stars\" = \"Gift Premium or Stars\";\n\"lng_gift_premium_subtitle\" = \"Gift Premium\";\n\"lng_gift_premium_about\" = \"Give {name} access to exclusive features with Telegram Premium. {features}\";\n\"lng_gift_premium_features\" = \"See Features >\";\n\"lng_gift_premium_label\" = \"Premium\";\n\"lng_gift_premium_by_stars\" = \"or {amount}\";\n\"lng_gift_stars_subtitle\" = \"Gift Stars\";\n\"lng_gift_stars_about\" = \"Give {name} gifts that can be kept on your profile or converted to Stars. {link}\";\n\"lng_gift_stars_about_collectibles\" = \"Collectible gifts are unique digital items you can exchange or sell. {link}\";\n\"lng_gift_stars_link\" = \"What are Stars >\";\n\"lng_gift_stars_limited\" = \"limited\";\n\"lng_gift_stars_sold_out\" = \"sold out\";\n\"lng_gift_stars_resale\" = \"resale\";\n\"lng_gift_stars_on_sale\" = \"on sale\";\n\"lng_gift_on_sale_for\" = \"On sale for {price}\";\n\"lng_gift_stars_premium\" = \"premium\";\n\"lng_gift_stars_auction\" = \"auction\";\n\"lng_gift_stars_auction_join\" = \"Join\";\n\"lng_gift_stars_auction_view\" = \"View\";\n\"lng_gift_stars_auction_soon\" = \"soon\";\n\"lng_gift_stars_auction_upgraded\" = \"upgraded\";\n\"lng_gift_stars_your_left#one\" = \"{count} left\";\n\"lng_gift_stars_your_left#other\" = \"{count} left\";\n\"lng_gift_stars_your_finished\" = \"none left\";\n\"lng_gift_stars_tabs_all\" = \"All Gifts\";\n\"lng_gift_stars_tabs_my\" = \"My Gifts\";\n\"lng_gift_stars_tabs_my_empty\" = \"You don't have any gifts you can use as a profile cover.\";\n\"lng_gift_stars_tabs_my_empty_next\" = \"Browse gifts available for purchase {emoji}\";\n\"lng_gift_stars_tabs_collectibles\" = \"Collectibles\";\n\"lng_gift_send_title\" = \"Send a Gift\";\n\"lng_gift_send_message\" = \"Enter Message\";\n\"lng_gift_send_anonymous\" = \"Hide My Name\";\n\"lng_gift_send_pay_with_stars\" = \"Pay with {amount}\";\n\"lng_gift_send_stars_balance\" = \"Your balance is {amount}. {link}\";\n\"lng_gift_send_stars_balance_link\" = \"Get More Stars >\";\n\"lng_gift_send_anonymous_self\" = \"Hide my name and message from visitors to my profile.\";\n\"lng_gift_send_anonymous_about\" = \"You can hide your name and message from visitors to {user}'s profile. {recipient} will still see your name and message.\";\n\"lng_gift_send_anonymous_about_paid\" = \"You can hide your name from visitors to {user}'s profile. {recipient} will still see your name.\";\n\"lng_gift_send_anonymous_about_channel\" = \"You can hide your name and message from all visitors of this channel except its admins.\";\n\"lng_gift_send_unique\" = \"Make Unique for {price}\";\n\"lng_gift_send_unique_about\" = \"Enable this to let {user} turn your gift into a unique collectible. {link}\";\n\"lng_gift_send_unique_about_channel\" = \"Enable this to let the admins of {name} turn your gift into a unique collectible. {link}\";\n\"lng_gift_send_unique_link\" = \"Learn More >\";\n\"lng_gift_send_premium_about\" = \"Only {user} will see your message.\";\n\"lng_gift_send_limited_sold#one\" = \"{count} sold\";\n\"lng_gift_send_limited_sold#other\" = \"{count} sold\";\n\"lng_gift_send_limited_left#one\" = \"{count} left\";\n\"lng_gift_send_limited_left#other\" = \"{count} left\";\n\"lng_gift_send_button\" = \"Send a Gift for {cost}\";\n\"lng_gift_send_button_self\" = \"Buy a Gift for {cost}\";\n\"lng_gift_buy_resale_title\" = \"Buy {name}\";\n\"lng_gift_buy_resale_button\" = \"Buy for {cost}\";\n\"lng_gift_buy_resale_equals\" = \"Equals to {cost}\";\n\"lng_gift_buy_resale_only_ton\" = \"The seller only accepts TON as payment.\";\n\"lng_gift_buy_resale_pay_stars\" = \"Pay in Stars\";\n\"lng_gift_buy_resale_pay_ton\" = \"Pay in TON\";\n\"lng_gift_buy_resale_confirm\" = \"Do you want to buy {name} for {price} and gift it to {user}?\";\n\"lng_gift_buy_resale_confirm_self\" = \"Do you want to buy {name} for {price}?\";\n\"lng_gift_buy_price_change_title\" = \"Price change!\";\n\"lng_gift_buy_price_change_text\" = \"This gift price was changed and now is {price}. Do you still want to buy?\";\n\"lng_gift_sent_title\" = \"Gift Sent!\";\n\"lng_gift_sent_resale_done\" = \"{user} has been notified about your gift.\";\n\"lng_gift_sent_resale_done_self\" = \"{gift} is now yours.\";\n\"lng_gift_sent_about#one\" = \"You spent **{count}** Star from your balance.\";\n\"lng_gift_sent_about#other\" = \"You spent **{count}** Stars from your balance.\";\n\"lng_gift_sent_finished#one\" = \"You've already sent **{count}** of these gifts, and it's the limit.\";\n\"lng_gift_sent_finished#other\" = \"You've already sent **{count}** of these gifts, and it's the limit.\";\n\"lng_gift_sent_remains#one\" = \"You can send **{count}** more.\";\n\"lng_gift_sent_remains#other\" = \"You can send **{count}** more.\";\n\"lng_gift_limited_of_one\" = \"unique\";\n\"lng_gift_limited_of_count\" = \"1 of {amount}\";\n\"lng_gift_collectible_tag\" = \"gift\";\n\"lng_gift_burned_tag\" = \"burned\";\n\"lng_gift_crafted_tag\" = \"crafted\";\n\"lng_gift_uncommon_tag\" = \"uncommon\";\n\"lng_gift_rare_tag\" = \"rare\";\n\"lng_gift_epic_tag\" = \"epic\";\n\"lng_gift_legendary_tag\" = \"legendary\";\n\"lng_gift_view_unpack\" = \"Unpack\";\n\"lng_gift_anonymous_hint\" = \"Only you can see the sender's name.\";\n\"lng_gift_anonymous_hint_channel\" = \"Only admins of this channel can see the sender's name.\";\n\"lng_gift_hidden_hint\" = \"This gift is hidden. Only you can see it.\";\n\"lng_gift_hidden_unique\" = \"This gift is not displayed on your page.\";\n\"lng_gift_visible_hint\" = \"This gift is visible on your page.\";\n\"lng_gift_burned_message\" = \"This gift was burned in crafting.\";\n\"lng_gift_hidden_hint_channel\" = \"This gift is hidden from visitors of your channel.\";\n\"lng_gift_visible_hint_channel\" = \"This gift is visible in your channel's Gifts.\";\n\"lng_gift_in_blockchain\" = \"This gift is in TON blockchain. {link}\";\n\"lng_gift_in_blockchain_link_arrow\" = \"View {arrow}\";\n\"lng_gift_visible_hide_arrow\" = \"Hide {arrow}\";\n\"lng_gift_visible_show_arrow\" = \"Show {arrow}\";\n\"lng_gift_show_on_page\" = \"Display on my Page\";\n\"lng_gift_show_on_channel\" = \"Display in channel's Gifts\";\n\"lng_gift_availability\" = \"Availability\";\n\"lng_gift_from_hidden\" = \"Hidden User\";\n\"lng_gift_subtitle_birthdays\" = \"Birthdays\";\n\"lng_gift_list_birthday_status_today\" = \"{emoji} Birthday today\";\n\"lng_gift_list_birthday_status_yesterday\" = \"Birthday yesterday\";\n\"lng_gift_list_birthday_status_tomorrow\" = \"Birthday tomorrow\";\n\"lng_gift_self_status\" = \"buy yourself a gift\";\n\"lng_gift_self_title\" = \"Buy a Gift\";\n\"lng_gift_self_about\" = \"Buy yourself a gift to display on your page or reserve for later.\\n\\nLimited-edition gifts upgraded to collectibles can be gifted to others later.\";\n\"lng_gift_channel_title\" = \"Send a Gift\";\n\"lng_gift_channel_about\" = \"Select a gift to show appreciation for {name}.\";\n\"lng_gift_released_by\" = \"released by {name}\";\n\"lng_gift_unique_owner\" = \"Owner\";\n\"lng_gift_unique_address_copied\" = \"Address copied to clipboard.\";\n\"lng_gift_unique_telegram\" = \"Telegram\";\n\"lng_gift_unique_status\" = \"Status\";\n\"lng_gift_unique_status_non\" = \"Non-Unique\";\n\"lng_gift_unique_upgrade\" = \"Upgrade\";\n\"lng_gift_unique_upgrade_next\" = \"Upgrade Next Gift\";\n\"lng_gift_unique_gift_upgrade\" = \"Gift an Upgrade\";\n\"lng_gift_unique_number\" = \"Collectible #{index}\";\n\"lng_gift_unique_number_by\" = \"Collectible #{index} by {name}\";\n\"lng_gift_unique_model\" = \"Model\";\n\"lng_gift_unique_backdrop\" = \"Backdrop\";\n\"lng_gift_unique_symbol\" = \"Symbol\";\n\"lng_gift_unique_rarity\" = \"Only {percent} of such collectibles have this attribute.\";\n\"lng_gift_unique_sender\" = \"{from} sent you this gift on {date}\";\n\"lng_gift_unique_sender_you\" = \"You bought this gift on {date}\";\n\"lng_gift_unique_crafter_you\" = \"You crafted this gift on {date}\";\n\"lng_gift_unique_availability_label\" = \"Quantity\";\n\"lng_gift_unique_availability#one\" = \"{count} of {amount} issued\";\n\"lng_gift_unique_availability#other\" = \"{count} of {amount} issued\";\n\"lng_gift_unique_value\" = \"Value\";\n\"lng_gift_unique_value_learn_more\" = \"learn more\";\n\"lng_gift_unique_info\" = \"Gifted to {recipient} on {date}.\";\n\"lng_gift_unique_info_sender\" = \"Gifted by {from} to {recipient} on {date}.\";\n\"lng_gift_unique_info_sender_comment\" = \"Gifted by {from} to {recipient} on {date} with the comment \\\"{text}\\\".\";\n\"lng_gift_unique_info_reciever\" = \"Gifted to {recipient} on {date}.\";\n\"lng_gift_unique_info_reciever_comment\" = \"Gifted to {recipient} on {date} with the comment \\\"{text}\\\".\";\n\"lng_gift_unique_info_remove_title\" = \"Remove Description\";\n\"lng_gift_unique_info_remove_text\" = \"Do you want to permanently remove this description from your gift?\";\n\"lng_gift_unique_info_remove_confirm\" = \"Remove for {cost}\";\n\"lng_gift_unique_info_removed\" = \"Removed {name}'s Description!\";\n\"lng_gift_availability_left#one\" = \"{count} of {amount} left\";\n\"lng_gift_availability_left#other\" = \"{count} of {amount} left\";\n\"lng_gift_availability_none\" = \"None of {amount} left\";\n\"lng_gift_value_about_average\" = \"This is the average sale price of {gift} gifts on Telegram and Fragment over the past month.\";\n\"lng_gift_value_about_last\" = \"This is the price at which {gift} was last sold on {platform}.\";\n\"lng_gift_value_initial_sale\" = \"Initial Sale\";\n\"lng_gift_value_initial_price\" = \"Initial Price\";\n\"lng_gift_value_initial_price_value\" = \"{stars} ({amount})\";\n\"lng_gift_value_last_sale\" = \"Last Sale\";\n\"lng_gift_value_last_price\" = \"Last Price\";\n\"lng_gift_value_minimum_price\" = \"Minimum Price\";\n\"lng_gift_value_minimum_price_tooltip\" = \"{amount} is the floor price for {gift} gifts listed on Telegram and Fragment.\";\n\"lng_gift_vlaue_average_price\" = \"Average Price\";\n\"lng_gift_value_average_price_tooltip\" = \"{amount} is the average sale price of {gift} gifts on Telegram and Fragment over the past month.\";\n\"lng_gift_value_availability#one\" = \"{count} {emoji} for sale on {platform} {arrow}\";\n\"lng_gift_value_availability#other\" = \"{count} {emoji} for sale on {platform} {arrow}\";\n\"lng_gift_value_telegram\" = \"Telegram\";\n\"lng_gift_value_fragment\" = \"Fragment\";\n\"lng_gift_convert_to_stars#one\" = \"Convert to {count} Star\";\n\"lng_gift_convert_to_stars#other\" = \"Convert to {count} Stars\";\n\"lng_gift_convert_sure_title\" = \"Convert Gift to Stars\";\n\"lng_gift_convert_sure_confirm#one\" = \"Do you want to convert this gift from {user} to **{count} Star**?\";\n\"lng_gift_convert_sure_confirm#other\" = \"Do you want to convert this gift from {user} to **{count} Stars**?\";\n\"lng_gift_convert_sure_confirm_channel#one\" = \"Do you want to convert this gift to {channel} to **{count} Star**?\";\n\"lng_gift_convert_sure_confirm_channel#other\" = \"Do you want to convert this gift to {channel} to **{count} Stars**?\";\n\"lng_gift_convert_sure_limit#one\" = \"Conversion is available for the next **{count} day**.\";\n\"lng_gift_convert_sure_limit#other\" = \"Conversion is available for the next **{count} days**.\";\n\"lng_gift_convert_sure_caution\" = \"This action cannot be undone. This will permanently destroy the gift.\";\n\"lng_gift_convert_sure\" = \"Convert\";\n\"lng_gift_display_done\" = \"The gift is now shown on your profile page.\";\n\"lng_gift_display_done_channel\" = \"The gift is now shown in channel's Gifts.\";\n\"lng_gift_display_done_hide\" = \"The gift is now hidden from your profile page.\";\n\"lng_gift_display_done_hide_channel\" = \"The gift is now hidden from channel's Gifts.\";\n\"lng_gift_pinned_done_title\" = \"{gift} pinned\";\n\"lng_gift_pinned_done\" = \"The gift will always be shown on top.\";\n\"lng_gift_pinned_done_replaced\" = \"replacing {gift}\";\n\"lng_gift_got_stars#one\" = \"You got **{count} Star** for this gift.\";\n\"lng_gift_got_stars#other\" = \"You got **{count} Stars** for this gift.\";\n\"lng_gift_channel_got#one\" = \"Channel got **{count} Star** for this gift.\";\n\"lng_gift_channel_got#other\" = \"Channel got **{count} Stars** for this gift.\";\n\"lng_gift_sold_out_title\" = \"Sold Out!\";\n\"lng_gift_sold_out_text#one\" = \"All {count} gift was already sold.\";\n\"lng_gift_sold_out_text#other\" = \"All {count} gifts were already sold.\";\n\"lng_gift_send_small\" = \"send a gift\";\n\"lng_gift_sell_small#one\" = \"sell for {count} Star\";\n\"lng_gift_sell_small#other\" = \"sell for {count} Stars\";\n\"lng_gift_upgrade_title\" = \"Upgrade Gift\";\n\"lng_gift_upgrade_about\" = \"Turn your gift into a unique collectible\\nthat you can transfer or auction.\";\n\"lng_gift_upgrade_view_all\" = \"{emoji} View all variants {arrow}\";\n\"lng_gift_upgrade_preview_title\" = \"Make Unique\";\n\"lng_gift_upgrade_preview_about\" = \"Let {name} turn your gift into a unique collectible.\";\n\"lng_gift_upgrade_preview_about_channel\" = \"Let the admins of {name} turn your gift into a unique collectible.\";\n\"lng_gift_upgrade_unique_title\" = \"Unique\";\n\"lng_gift_upgrade_unique_about\" = \"Get a unique number, model, backdrop and symbol for your gift.\";\n\"lng_gift_upgrade_unique_about_user\" = \"{name} will get a unique number, model, backdrop and symbol for your gift.\";\n\"lng_gift_upgrade_unique_about_channel\" = \"Admins of {name} will get a unique number, model, backdrop and symbol for your gift.\";\n\"lng_gift_upgrade_transferable_title\" = \"Transferable\";\n\"lng_gift_upgrade_transferable_about\" = \"Send your upgraded gift to any of your friends on Telegram.\";\n\"lng_gift_upgrade_transferable_about_user\" = \"{name} will be able to send the gift to anyone on Telegram.\";\n\"lng_gift_upgrade_transferable_about_channel\" = \"Admins of {name} will be able to send the gift to anyone on Telegram.\";\n\"lng_gift_upgrade_tradable_title\" = \"Tradable\";\n\"lng_gift_upgrade_tradable_about\" = \"Sell or auction your gift on third-party NFT marketplaces.\";\n\"lng_gift_upgrade_tradable_about_user\" = \"{name} will be able to sell the gift on Telegram and NFT marketplaces.\";\n\"lng_gift_upgrade_tradable_about_channel\" = \"Admins of {name} will be able to sell the gift on Telegram and NFT marketplaces.\";\n\"lng_gift_upgrade_wearable_title\" = \"Wearable\";\n\"lng_gift_upgrade_wearable_about\" = \"Display gifts on your page and set them as profile covers or statuses.\";\n\"lng_gift_upgrade_button\" = \"Upgrade for {price}\";\n\"lng_gift_upgrade_decreases\" = \"Price decreases in {time}\";\n\"lng_gift_upgrade_see_table\" = \"See how this price will decrease {arrow}\";\n\"lng_gift_upgrade_prices_about\" = \"Upgrade cost drops every minute.\";\n\"lng_gift_upgrade_prices_title\" = \"Upgrade Cost\";\n\"lng_gift_upgrade_prices_subtitle\" = \"Users who upgrade their gifts first get collectibles with shorter numbers.\";\n\"lng_gift_upgrade_free\" = \"Upgrade for Free\";\n\"lng_gift_upgrade_confirm\" = \"Confirm\";\n\"lng_gift_upgrade_add_my\" = \"Add my name to the gift\";\n\"lng_gift_upgrade_add_my_comment\" = \"Add my name and comment\";\n\"lng_gift_upgrade_add_sender\" = \"Add sender's name to the gift\";\n\"lng_gift_upgrade_add_comment\" = \"Add sender's name and comment\";\n\"lng_gift_upgraded_title\" = \"Gift Upgraded\";\n\"lng_gift_upgraded_about\" = \"Your gift {name} now has unique attributes and can be transferred to others\";\n\"lng_gift_upgrade_gifted_title\" = \"Upgrade Gifted\";\n\"lng_gift_upgrade_gifted_about\" = \"Now {name} can turn your gift into a unique collectible.\";\n\"lng_gift_upgrade_gifted_about_channel\" = \"Now the admins of {name} can turn your gift into a unique collectible.\";\n\"lng_gift_transferred_title\" = \"Gift Transferred\";\n\"lng_gift_transferred_about\" = \"{name} was successfully transferred to {recipient}.\";\n\"lng_gift_transfer_title\" = \"Transfer {name}\";\n\"lng_gift_transfer_via_blockchain\" = \"Send via Blockchain\";\n\"lng_gift_transfer_password_title\" = \"Two-step verification\";\n\"lng_gift_transfer_password_description\" = \"Please enter your password to transfer.\";\n\"lng_gift_transfer_password_about\" = \"You can withdraw only if you have:\";\n\"lng_gift_transfer_confirm_title\" = \"Manage with Fragment\";\n\"lng_gift_transfer_confirm_text\" = \"You can use Fragment, a third-party service, to transfer {name} to your TON account. After that, you can manage it as an NFT with any TON wallet outside Telegram.\\n\\nYou can also move such NFTs back to your Telegram account via Fragment.\";\n\"lng_gift_transfer_confirm_button\" = \"Open Fragment\";\n\"lng_gift_transfer_unlocks_days#one\" = \"unlocks in {count} day\";\n\"lng_gift_transfer_unlocks_days#other\" = \"unlocks in {count} days\";\n\"lng_gift_transfer_unlocks_hours#one\" = \"unlocks in {count} hour\";\n\"lng_gift_transfer_unlocks_hours#other\" = \"unlocks in {count} hours\";\n\"lng_gift_transfer_unlocks_title\" = \"Unlocking in progress\";\n\"lng_gift_transfer_unlocks_about\" = \"{when}, you'll be able to send this collectible to any TON blockchain address outside Telegram for sale or auction.\";\n\"lng_gift_transfer_unlocks_when_days#one\" = \"In {count} day\";\n\"lng_gift_transfer_unlocks_when_days#other\" = \"In {count} days\";\n\"lng_gift_transfer_unlocks_when_hours#one\" = \"In {count} hour\";\n\"lng_gift_transfer_unlocks_when_hours#other\" = \"In {count} hours\";\n\"lng_gift_transfer_unlocks_update_title\" = \"Update required\";\n\"lng_gift_transfer_unlocks_update_about\" = \"Please update your Telegram application to the latest version.\";\n\"lng_gift_transfer_sure\" = \"Do you want to transfer ownership of {name} to {recipient}?\";\n\"lng_gift_transfer_sure_for\" = \"Do you want to transfer ownership of {name} to {recipient} for {price}?\";\n\"lng_gift_transfer_button\" = \"Transfer\";\n\"lng_gift_transfer_button_for\" = \"Transfer for {price}\";\n\"lng_gift_transfer_set_theme\" = \"Set as Theme in...\";\n\"lng_gift_transfer_choose\" = \"Choose Chat\";\n\"lng_gift_transfer_wear\" = \"Wear\";\n\"lng_gift_transfer_take_off\" = \"Take Off\";\n\"lng_gift_transfer_sell\" = \"Sell\";\n\"lng_gift_transfer_update\" = \"Change Price\";\n\"lng_gift_transfer_unlist\" = \"Unlist\";\n\"lng_gift_transfer_locked_title\" = \"Action Locked\";\n\"lng_gift_transfer_locked_text\" = \"Transfer this gift to your Telegram account on Fragment to unlock this action.\";\n\"lng_gift_offer_button\" = \"Offer to Buy\";\n\"lng_gift_offer_title\" = \"Offer to Buy\";\n\"lng_gift_offer_stars_about\" = \"Choose how many Stars you'd like to offer for {name}.\";\n\"lng_gift_offer_ton_about\" = \"Choose how many TON you'd like to offer for {name}.\";\n\"lng_gift_offer_duration\" = \"Offer Duration\";\n\"lng_gift_offer_duration_about\" = \"Choose how long {user} can accept your offer. When the time expires, the amount will be refunded.\";\n\"lng_gift_offer_cost_button\" = \"Offer {cost}\";\n\"lng_gift_offer_reject_title\" = \"Reject Offer\";\n\"lng_gift_offer_confirm_reject\" = \"Are you sure you want to reject the offer from {user}?\";\n\"lng_gift_offer_confirm_accept\" = \"Do you want to sell {name} to {user} for {cost}?\";\n\"lng_gift_offer_you_get\" = \"You will receive {cost} after fees.\";\n\"lng_gift_offer_higher\" = \"The price you are offered is {percent} higher than the average price for {name}.\";\n\"lng_gift_offer_lower\" = \"The price you are offered is {percent} lower than the average price for {name}.\";\n\"lng_gift_offer_sell_for\" = \"Sell for {price}\";\n\"lng_gift_offer_confirm_title\" = \"Confirm Offer\";\n\"lng_gift_offer_confirm_text\" = \"Do you want to offer {cost} to {user} for {name}?\";\n\"lng_gift_offer_table_offer\" = \"Offer\";\n\"lng_gift_offer_table_fee\" = \"Fee\";\n\"lng_gift_offer_table_duration\" = \"Duration\";\n\"lng_gift_sell_unlist_title\" = \"Unlist {name}\";\n\"lng_gift_sell_unlist_sure\" = \"Are you sure you want to unlist your gift?\";\n\"lng_gift_sell_title\" = \"Price in Stars\";\n\"lng_gift_sell_about\" = \"You will receive {percent} of the selected amount.\";\n\"lng_gift_sell_amount#one\" = \"You will receive **{count}** Star.\";\n\"lng_gift_sell_amount#other\" = \"You will receive **{count}** Stars.\";\n\"lng_gift_sell_min_price#one\" = \"Minimum price is {count} Star.\";\n\"lng_gift_sell_min_price#other\" = \"Minimum price is {count} Stars.\";\n\"lng_gift_sell_only_ton\" = \"Only Accept TON\";\n\"lng_gift_sell_only_ton_about\" = \"If the buyer pays you in TON, there's no risk of refunds, unlike Stars payments.\";\n\"lng_gift_sell_amount_ton#one\" = \"You will receive **{count}** TON.\";\n\"lng_gift_sell_amount_ton#other\" = \"You will receive **{count}** TON.\";\n\"lng_gift_sell_min_price_ton#one\" = \"Minimum price is {count} TON.\";\n\"lng_gift_sell_min_price_ton#other\" = \"Minimum price is {count} TON.\";\n\"lng_gift_sell_title_ton\" = \"Price in TON\";\n\"lng_gift_sell_put\" = \"Put for Sale\";\n\"lng_gift_sell_update\" = \"Update the Price\";\n\"lng_gift_sell_toast\" = \"{name} is now for sale!\";\n\"lng_gift_sell_updated\" = \"Sale price for {name} was updated.\";\n\"lng_gift_sell_removed\" = \"{name} is removed from sale.\";\n\"lng_gift_menu_show\" = \"Show\";\n\"lng_gift_menu_hide\" = \"Hide\";\n\"lng_gift_wear_title\" = \"Wear {name}\";\n\"lng_gift_wear_about\" = \"and get these benefits:\";\n\"lng_gift_wear_badge_title\" = \"Radiant Badge\";\n\"lng_gift_wear_badge_about\" = \"The glittering icon of this item will be displayed next to your name.\";\n\"lng_gift_wear_badge_about_channel\" = \"The glittering icon of this item will be displayed next to channel's name.\";\n\"lng_gift_wear_design_title\" = \"Unique Profile Design\";\n\"lng_gift_wear_design_about\" = \"Your profile page will get the color and the symbol of this item.\";\n\"lng_gift_wear_design_about_channel\" = \"Your channel page will get the color and the symbol of this item.\";\n\"lng_gift_wear_proof_title\" = \"Proof of Ownership\";\n\"lng_gift_wear_proof_about\" = \"Clicking the icon of this item next to your name will show its info and owner.\";\n\"lng_gift_wear_proof_about_channel\" = \"Clicking the icon of this item next to channel's name will show its info and owner.\";\n\"lng_gift_wear_start\" = \"Start Wearing\";\n\"lng_gift_wear_subscribe\" = \"Subscribe to {link} to wear collectibles.\";\n\"lng_gift_wear_start_toast\" = \"You put on {name}\";\n\"lng_gift_wear_end_toast\" = \"You took off {name}\";\n\"lng_gift_many_pinned_title\" = \"Too Many Pinned Gifts\";\n\"lng_gift_many_pinned_choose\" = \"Select a gift to unpin below\";\n\"lng_gift_resale_price\" = \"Price\";\n\"lng_gift_resale_number\" = \"Number\";\n\"lng_gift_resale_date\" = \"Date\";\n\"lng_gift_resale_count#one\" = \"{count} gift in resale\";\n\"lng_gift_resale_count#other\" = \"{count} gifts in resale\";\n\"lng_gift_resale_count_none\" = \"No listings\";\n\"lng_gift_resale_sort_price\" = \"Sort by Price\";\n\"lng_gift_resale_sort_date\" = \"Sort by Date\";\n\"lng_gift_resale_sort_number\" = \"Sort by Number\";\n\"lng_gift_resale_all_listings\" = \"All Listings\";\n\"lng_gift_resale_stars_only\" = \"For Stars Only\";\n\"lng_gift_resale_filter_all\" = \"Select All\";\n\"lng_gift_resale_model\" = \"Model\";\n\"lng_gift_resale_models#one\" = \"{count} Model\";\n\"lng_gift_resale_models#other\" = \"{count} Models\";\n\"lng_gift_resale_backdrop\" = \"Backdrop\";\n\"lng_gift_resale_backdrops#one\" = \"{count} Backdrop\";\n\"lng_gift_resale_backdrops#other\" = \"{count} Backdrops\";\n\"lng_gift_resale_symbol\" = \"Symbol\";\n\"lng_gift_resale_symbols#one\" = \"{count} Symbol\";\n\"lng_gift_resale_symbols#other\" = \"{count} Symbols\";\n\"lng_gift_resale_search_none\" = \"No gifts found for your search.\";\n\"lng_gift_resale_early\" = \"You will be able to resell this gift in {duration}.\";\n\"lng_gift_transfer_early\" = \"You will be able to transfer this gift in {duration}.\";\n\"lng_gift_resale_transfer_early_title\" = \"Try Later\";\n\"lng_gift_collection_add\" = \"Add Collection\";\n\"lng_gift_collection_new_title\" = \"Create a New Collection\";\n\"lng_gift_collection_new_button\" = \"New Collection\";\n\"lng_gift_collection_new_text\" = \"Choose a name for your collection and start adding your gifts there.\";\n\"lng_gift_collection_new_ph\" = \"Title\";\n\"lng_gift_collection_new_create\" = \"Create\";\n\"lng_gift_collection_empty_title\" = \"Organize Your Gifts\";\n\"lng_gift_collection_empty_text\" = \"Add some of your gifts to this collection.\";\n\"lng_gift_collection_all\" = \"All Gifts\";\n\"lng_gift_collection_add_title\" = \"Add Gifts\";\n\"lng_gift_collection_add_button\" = \"Add Gifts\";\n\"lng_gift_collection_share\" = \"Share Collection\";\n\"lng_gift_collection_edit\" = \"Edit Name\";\n\"lng_gift_collection_limit_title\" = \"Limit Reached\";\n\"lng_gift_collection_limit_text\" = \"Please remove one of the existing collections to add a new one.\";\n\"lng_gift_collection_delete\" = \"Delete Collection\";\n\"lng_gift_collection_delete_sure\" = \"Are you sure you want to delete this collection?\";\n\"lng_gift_collection_delete_button\" = \"Delete\";\n\"lng_gift_collection_add_to\" = \"Add to Collection\";\n\"lng_gift_collection_reorder\" = \"Reorder\";\n\"lng_gift_collection_reorder_exit\" = \"Apply Reorder\";\n\"lng_gift_collection_remove_from\" = \"Remove from Collection\";\n\"lng_gift_locked_title\" = \"Gift Locked\";\n\"lng_gift_craft_menu_button\" = \"Craft\";\n\"lng_gift_craft_info_title\" = \"Gift Crafting\";\n\"lng_gift_craft_info_about\" = \"Turn your gifts into rare, epic\\nand legendary versions.\";\n\"lng_gift_craft_rare_title\" = \"Get Rare Models\";\n\"lng_gift_craft_rare_about\" = \"Select up to 4 gifts to craft a new exclusive model.\";\n\"lng_gift_craft_chance_title\" = \"Maximize Chances\";\n\"lng_gift_craft_chance_about\" = \"Combine more gifts to increase your odds of success.\";\n\"lng_gift_craft_affect_title\" = \"Affect the Result\";\n\"lng_gift_craft_affect_about\" = \"Use gifts with the same attribute to boost its chance.\";\n\"lng_gift_craft_start_button\" = \"Start Crafting\";\n\"lng_gift_craft_title\" = \"Craft Gift\";\n\"lng_gift_craft_about1\" = \"Add up to **4** gifts to craft new\\n{gift}.\";\n\"lng_gift_craft_about2\" = \"If crafting fails, all used gifts\\nwill be lost.\";\n\"lng_gift_craft_view_all\" = \"{emoji} View all craftable models {arrow}\";\n\"lng_gift_craft_chance_backdrop\" = \"{percent} chance the crafted gift will have **{name}** background.\";\n\"lng_gift_craft_chance_symbol\" = \"{percent} chance the crafted gift will have **{name}** symbol.\";\n\"lng_gift_craft_button\" = \"Craft {gift}\";\n\"lng_gift_craft_button_chance\" = \"{percent} Success Chance\";\n\"lng_gift_craft_select_title\" = \"Select Gifts\";\n\"lng_gift_craft_select_about\" = \"You need to add up to **3 gifts** from the same collection for crafting.\";\n\"lng_gift_craft_select_your\" = \"Your gifts\";\n\"lng_gift_craft_select_none\" = \"You don't have other gifts from this collection.\";\n\"lng_gift_craft_search_none\" = \"Could not find other gifts from this collection.\";\n\"lng_gift_craft_select_market#one\" = \"{count} suitable gift on sale\";\n\"lng_gift_craft_select_market#other\" = \"{count} suitable gifts on sale\";\n\"lng_gift_craft_progress\" = \"Crafting\";\n\"lng_gift_craft_progress_about\" = \"Crafting **{gift}**\";\n\"lng_gift_craft_progress_chance\" = \"{percent} success chance\";\n\"lng_gift_craft_progress_fails\" = \"If crafting fails, all used\\ngifts will be lost.\";\n\"lng_gift_craft_failed\" = \"Crafting failed. Gifts consumed :(\";\n\"lng_gift_craft_failed_title\" = \"Crafting Failed\";\n\"lng_gift_craft_failed_about#one\" = \"This crafting attempt was unsuccessful.\\n**{count}** gift was lost.\";\n\"lng_gift_craft_failed_about#other\" = \"This crafting attempt was unsuccessful.\\n**{count}** gifts were lost.\";\n\"lng_gift_craft_new_button\" = \"Craft New Gift\";\n\"lng_gift_craft_another_button\" = \"Craft Another Gift\";\n\"lng_gift_craft_unavailable\" = \"Crafting unavailable\";\n\"lng_gift_craft_when\" = \"This gift will become available for crafting on {date} at {time}.\";\n\"lng_gift_craft_address_error\" = \"This gift has been exported to the blockchain and can't be used as the primary gift for crafting.\";\n\n\"lng_auction_about_title\" = \"Auction\";\n\"lng_auction_about_subtitle\" = \"Join the battle for exclusive gifts.\";\n\"lng_auction_about_top_title#one\" = \"Top {count} Bidder\";\n\"lng_auction_about_top_title#other\" = \"Top {count} Bidders\";\n\"lng_auction_about_top_rounds#one\" = \"{count} round\";\n\"lng_auction_about_top_rounds#other\" = \"{count} rounds\";\n\"lng_auction_about_top_bidders#one\" = \"top {count} bidder\";\n\"lng_auction_about_top_bidders#other\" = \"top {count} bidders\";\n\"lng_auction_about_top_about#one\" = \"{count} gift is dropped in {rounds} to the {bidders} by bid amount.\";\n\"lng_auction_about_top_about#other\" = \"{count} gifts are dropped in {rounds} to the {bidders} by bid amount.\";\n\"lng_auction_about_top_short#one\" = \"{count} gift is dropped to the {bidders} by bid amount. {link}\";\n\"lng_auction_about_top_short#other\" = \"{count} gifts are dropped to the {bidders} by bid amount. {link}\";\n\"lng_auction_about_bid_title\" = \"Bid Carryover\";\n\"lng_auction_about_bid_about#one\" = \"If your bid leaves the top {count}, it will automatically join the next round.\";\n\"lng_auction_about_bid_about#other\" = \"If your bid leaves the top {count}, it will automatically join the next round.\";\n\"lng_auction_about_missed_title\" = \"Missed Bidders\";\n\"lng_auction_about_missed_about\" = \"If your bid doesn't win after the final round, your Stars will be fully refunded.\";\n\"lng_auction_about_understood\" = \"Understood\";\n\"lng_auction_text#one\" = \"Top **{count}** bidder will get {name} gifts this round. {link}\";\n\"lng_auction_text#other\" = \"Top **{count}** bidders will get {name} gifts this round. {link}\";\n\"lng_auction_text_link\" = \"Learn more {arrow}\";\n\"lng_auction_text_ended\" = \"Auction ended.\";\n\"lng_auction_start_label\" = \"Started\";\n\"lng_auction_starts_label\" = \"Starts\";\n\"lng_auction_rounds_label\" = \"Rounds\";\n\"lng_auction_rounds_exact\" = \"Round {n}\";\n\"lng_auction_rounds_range\" = \"Rounds {n}-{last}\";\n\"lng_auction_rounds_seconds#one\" = \"{count} second each\";\n\"lng_auction_rounds_seconds#other\" = \"{count} seconds each\";\n\"lng_auction_rounds_minutes#one\" = \"{count} minute each\";\n\"lng_auction_rounds_minutes#other\" = \"{count} minutes each\";\n\"lng_auction_rounds_hours#one\" = \"{count} hour each\";\n\"lng_auction_rounds_hours#other\" = \"{count} hours each\";\n\"lng_auction_rounds_extended\" = \"{duration} + {increase} for late bids in top {n}\";\n\"lng_auction_end_label\" = \"Ends\";\n\"lng_auction_round_label\" = \"Current Round\";\n\"lng_auction_round_value\" = \"{n} of {amount}\";\n\"lng_auction_average_label\" = \"Average Price\";\n\"lng_auction_average_tooltip\" = \"{amount} is the average sale price for {gift} gifts.\";\n\"lng_auction_availability_label\" = \"Availability\";\n\"lng_auction_availability_value\" = \"{n} of {amount} left\";\n\"lng_auction_bought#one\" = \"{count} {emoji} item bought {arrow}\";\n\"lng_auction_bought#other\" = \"{count} {emoji} items bought {arrow}\";\n\"lng_auction_join_button\" = \"Join Auction\";\n\"lng_auction_join_bid\" = \"Place a Bid\";\n\"lng_auction_join_time_left\" = \"{time} left\";\n\"lng_auction_join_time_medium\" = \"{hours} h {minutes} m\";\n\"lng_auction_join_time_small\" = \"{minutes} m\";\n\"lng_auction_join_starts_in\" = \"starts in {time}\";\n\"lng_auction_join_early_bid\" = \"Place an Early Bid\";\n\"lng_auction_menu_about\" = \"About\";\n\"lng_auction_menu_copy_link\" = \"Copy Link\";\n\"lng_auction_menu_share\" = \"Share\";\n\"lng_auction_bid_title\" = \"Place a Bid\";\n\"lng_auction_bid_title_early\" = \"Place an Early Bid\";\n\"lng_auction_bid_subtitle#one\" = \"Top {count} bidder will win\";\n\"lng_auction_bid_subtitle#other\" = \"Top {count} bidders will win\";\n\"lng_auction_bid_your\" = \"your bid\";\n\"lng_auction_bid_custom\" = \"click to bid more\";\n\"lng_auction_bid_threshold#one\" = \"TOP {count}\";\n\"lng_auction_bid_threshold#other\" = \"TOP {count}\";\n\"lng_auction_bid_minimal#one\" = \"minimum bid\";\n\"lng_auction_bid_minimal#other\" = \"minimum bid\";\n\"lng_auction_bid_until\" = \"until next round\";\n\"lng_auction_bid_left#one\" = \"left\";\n\"lng_auction_bid_left#other\" = \"left\";\n\"lng_auction_bid_before_start\" = \"before start\";\n\"lng_auction_bid_your_title\" = \"Your bid will be\";\n\"lng_auction_bid_your_outbid\" = \"You've been outbid\";\n\"lng_auction_bid_your_winning\" = \"You're winning\";\n\"lng_auction_bid_winners_title\" = \"Top winners\";\n\"lng_auction_bid_place\" = \"Place a {stars} Bid\";\n\"lng_auction_bid_increase\" = \"Add {stars} to Your Bid\";\n\"lng_auction_bid_placed_title\" = \"Your bid has been placed\";\n\"lng_auction_bid_increased_title\" = \"Your bid has been increased\";\n\"lng_auction_bid_done_text#one\" = \"If you fall below the **top {count}**, your bid will roll over to the next round.\";\n\"lng_auction_bid_done_text#other\" = \"If you fall below the **top {count}**, your bid will roll over to the next round.\";\n\"lng_auction_bid_custom_title\" = \"Custom Amount\";\n\"lng_auction_preview_left#one\" = \"{count} left\";\n\"lng_auction_preview_left#other\" = \"{count} left\";\n\"lng_auction_preview_join\" = \"Join\";\n\"lng_auctino_preview_finished\" = \"Finished\";\n\"lng_auction_preview_sold_out\" = \"Sold Out\";\n\"lng_auction_preview_view_results\" = \"View Results\";\n\"lng_auction_bar_active\" = \"Active Auction\";\n\"lng_auction_bar_active_many#one\" = \"{count} Active Auction\";\n\"lng_auction_bar_active_many#other\" = \"{count} Active Auctions\";\n\"lng_auction_bar_winning#one\" = \"You're winning ({count}st place).\";\n\"lng_auction_bar_winning#other\" = \"You're winning ({count}th place).\";\n\"lng_auction_bar_outbid\" = \"You've been outbid.\";\n\"lng_auction_bar_winning_all\" = \"You're winning in all of them.\";\n\"lng_auction_bar_outbid_some#one\" = \"You've been outbid in {count} of them.\";\n\"lng_auction_bar_outbid_some#other\" = \"You've been outbid in {count} of them.\";\n\"lng_auction_bar_outbid_all\" = \"You've been outbid in all of them.\";\n\"lng_auction_bar_view\" = \"View\";\n\"lng_auction_bar_round\" = \"Round {n} of {amount}\";\n\"lng_auction_bar_bid_ranked\" = \"Your bid **{stars}** is ranked **#{n}**\";\n\"lng_auction_bar_bid_outbid\" = \"Your bid **{stars}** is outbid\";\n\"lng_auction_bar_raise_bid\" = \"Raise Bid\";\n\"lng_auction_bought_title#one\" = \"{count} Item Bought\";\n\"lng_auction_bought_title#other\" = \"{count} Items Bought\";\n\"lng_auction_bought_date\" = \"Date\";\n\"lng_auction_bought_bid\" = \"Accepted Bid\";\n\"lng_auction_bought_in_round\" = \"{name} in round {n}\";\n\"lng_auction_change_title\" = \"Change Recipient\";\n\"lng_auction_change_button\" = \"Change\";\n\"lng_auction_change_already\" = \"You've already placed a bid on this gift for {name}.\";\n\"lng_auction_change_to\" = \"Do you want to raise your bid and change the recipient to {name}?\";\n\"lng_auction_change_already_me\" = \"You've already placed a bid on this gift for yourself.\";\n\"lng_auction_change_to_me\" =  \"Do you want to raise your bid and change the recipient to yourself?\";\n\"lng_auction_preview_name\" = \"Upcoming Auction\";\n\"lng_auction_preview_learn_gifts\" = \"Learn more about Telegram Gifts {arrow}\";\n\"lng_auction_preview_variants#one\" = \"View {emoji} {count} Variant {arrow}\";\n\"lng_auction_preview_variants#other\" = \"View {emoji} {count} Variants {arrow}\";\n\"lng_auction_preview_random\" = \"Random Traits\";\n\"lng_auction_preview_selected\" = \"Selected Traits\";\n\"lng_auction_preview_randomize\" = \"Randomize Traits\";\n\"lng_auction_preview_model\" = \"model\";\n\"lng_auction_preview_models#one\" = \"This collectible features **{count}** unique model.\";\n\"lng_auction_preview_models#other\" = \"This collectible features **{count}** unique models.\";\n\"lng_auction_preview_models_button\" = \"Models\";\n\"lng_auction_preview_crafted#one\" = \"This collectible features **{count}** crafted model.\";\n\"lng_auction_preview_crafted#other\" = \"This collectible features **{count}** crafted models.\";\n\"lng_auction_preview_view_craftable\" = \"View craftable models {arrow}\";\n\"lng_auction_preview_view_primary\" = \"View primary models {arrow}\";\n\"lng_auction_preview_backdrop\" = \"backdrop\";\n\"lng_auction_preview_backdrops#one\" = \"This collectible features **{count}** unique backdrop.\";\n\"lng_auction_preview_backdrops#other\" = \"This collectible features **{count}** unique backdrops.\";\n\"lng_auction_preview_backdrops_button\" = \"Backdrops\";\n\"lng_auction_preview_symbol\" = \"symbol\";\n\"lng_auction_preview_symbols#one\" = \"This collectible features **{count}** unique symbol.\";\n\"lng_auction_preview_symbols#other\" = \"This collectible features **{count}** unique symbols.\";\n\"lng_auction_preview_symbols_button\" = \"Symbols\";\n\"lng_auction_preview_wear\" = \"More about wearing gifts {arrow}\";\n\"lng_auction_preview_free_upgrade\" = \"You can upgrade your gift for free, sell it on the market, or set it as your profile cover.\";\n\n\"lng_accounts_limit_title\" = \"Limit Reached\";\n\"lng_accounts_limit1#one\" = \"You have reached the limit of **{count}** connected account.\";\n\"lng_accounts_limit1#other\" = \"You have reached the limit of **{count}** connected accounts.\";\n\"lng_accounts_limit2\" = \"You can free one place by subscribing to **Telegram Premium** with one of these connected accounts:\";\n\n\"lng_emoji_status_for_title\" = \"Set Status for...\";\n\"lng_emoji_status_for_submit\" = \"Set Status\";\n\"lng_emoji_status_menu_duration_any\" = \"Set Status for {duration}\";\n\n\"lng_group_about_header\" = \"You created a group\";\n\"lng_group_about_text\" = \"Groups can have:\";\n\"lng_group_about1\" = \"Up to 200,000 members\";\n\"lng_group_about2\" = \"Persistent chat history\";\n\"lng_group_about3\" = \"Public links such as t.me/title\";\n\"lng_group_about4\" = \"Admins with different rights\";\n\n\"lng_switch_stickers\" = \"Stickers\";\n\"lng_switch_emoji\" = \"Emoji\";\n\"lng_switch_gifs\" = \"GIFs\";\n\"lng_switch_masks\" = \"Masks\";\n\"lng_stickers_featured_add\" = \"Add\";\n\"lng_stickers_featured_installed\" = \"Added\";\n\"lng_emoji_featured_unlock\" = \"Unlock\";\n\"lng_emoji_premium_restore\" = \"Restore\";\n\"lng_gifs_search\" = \"Search GIFs\";\n\"lng_gifs_no_saved\" = \"You have no saved GIFs yet.\";\n\n\"lng_inline_bot_no_results\" = \"No results.\";\n\"lng_inline_bot_via\" = \"via {inline_bot}\";\n\n\"lng_box_remove\" = \"Remove\";\n\n\"lng_stickers_installed_tab\" = \"Stickers\";\n\"lng_stickers_masks_tab\" = \"Masks\";\n\"lng_stickers_featured_tab\" = \"Trending\";\n\"lng_stickers_archived_tab\" = \"Archived\";\n\"lng_stickers_remove_pack\" = \"Remove \\\"{sticker_pack}\\\"?\";\n\"lng_stickers_add_pack\" = \"Add stickers\";\n\"lng_stickers_add_masks\" = \"Add masks\";\n\"lng_stickers_add_emoji\" = \"Add emoji\";\n\"lng_stickers_share_pack\" = \"Share Stickers\";\n\"lng_stickers_share_masks\" = \"Share Masks\";\n\"lng_stickers_share_emoji\" = \"Share Emoji\";\n\"lng_stickers_not_found\" = \"Sticker set not found.\";\n\"lng_stickers_packs_archived\" = \"Some of your unused stickers were archived to make room for the sets you activated.\";\n\"lng_stickers_copied\" = \"Link copied to clipboard.\";\n\"lng_stickers_copied_emoji\" = \"Emoji pack link copied to clipboard.\";\n\"lng_stickers_default_set\" = \"Great Minds\";\n\"lng_stickers_you_have\" = \"Manage sticker sets\";\n\"lng_stickers_return\" = \"Undo\";\n\"lng_stickers_count#one\" = \"{count} sticker\";\n\"lng_stickers_count#other\" = \"{count} stickers\";\n\"lng_masks_count#one\" = \"{count} mask\";\n\"lng_masks_count#other\" = \"{count} masks\";\n\"lng_custom_emoji_count#one\" = \"{count} emoji\";\n\"lng_custom_emoji_count#other\" = \"{count} emoji\";\n\"lng_stickers_attached_sets\" = \"Sets of attached stickers\";\n\"lng_custom_emoji_used_sets\" = \"Sets of used emoji\";\n\"lng_custom_emoji_remove_pack_button\" = \"Remove Emoji\";\n\"lng_stickers_group_set\" = \"Group sticker set\";\n\"lng_stickers_remove_group_set\" = \"Remove group sticker set?\";\n\"lng_stickers_group_from_your\" = \"Choose from your stickers\";\n\"lng_stickers_group_from_featured\" = \"Choose from trending stickers\";\n\"lng_stickers_nothing_found\" = \"No stickers found\";\n\"lng_stickers_remove_pack_confirm\" = \"Remove\";\n\"lng_stickers_archive_pack\" = \"Archive Stickers\";\n\"lng_stickers_has_been_archived\" = \"Sticker set has been archived.\";\n\"lng_emoji_group_set\" = \"Group emoji set\";\n\"lng_emoji_remove_group_set\" = \"Remove group emoji set?\";\n\"lng_emoji_group_from_your\" = \"Choose from your emoji\";\n\"lng_emoji_group_from_featured\" = \"Choose from trending emoji\";\n\"lng_masks_archive_pack\" = \"Archive Masks\";\n\"lng_masks_has_been_archived\" = \"Mask pack has been archived.\";\n\"lng_masks_installed\" = \"Mask pack has been installed.\";\n\"lng_emoji_nothing_found\" = \"No emoji found\";\n\"lng_stickers_context_reorder\" = \"Reorder\";\n\"lng_stickers_context_edit_name\" = \"Edit name\";\n\"lng_stickers_context_delete\" = \"Delete sticker\";\n\"lng_stickers_context_delete_sure\" = \"Are you sure you want to delete the sticker from your sticker set?\";\n\"lng_stickers_box_edit_name_title\" = \"Edit Sticker Set Name\";\n\"lng_stickers_box_edit_name_about\" = \"Choose a name for your set.\";\n\"lng_stickers_creator_badge\" = \"edit\";\n\n\"lng_in_dlg_photo\" = \"Photo\";\n\"lng_in_dlg_album\" = \"Album\";\n\"lng_in_dlg_video\" = \"Video\";\n\"lng_in_dlg_audio_file\" = \"Audio file\";\n\"lng_in_dlg_contact\" = \"Contact\";\n\"lng_in_dlg_audio\" = \"Voice message\";\n\"lng_in_dlg_audio_unread\" = \"{emoji} Voice message\";\n\"lng_in_dlg_video_message\" = \"Video message\";\n\"lng_in_dlg_voice_message_ttl\" = \"One-time Voice Message\";\n\"lng_in_dlg_video_message_ttl\" = \"One-time Video Message\";\n\"lng_in_dlg_file\" = \"File\";\n\"lng_in_dlg_sticker\" = \"Sticker\";\n\"lng_in_dlg_sticker_emoji\" = \"{emoji} Sticker\";\n\"lng_in_dlg_poll\" = \"Poll\";\n\"lng_in_dlg_story\" = \"Story\";\n\"lng_in_dlg_todo_list\" = \"Checklist\";\n\"lng_in_dlg_story_expired\" = \"Expired story\";\n\"lng_in_dlg_media_count#one\" = \"{count} media\";\n\"lng_in_dlg_media_count#other\" = \"{count} media\";\n\"lng_in_dlg_photo_count#one\" = \"{count} photo\";\n\"lng_in_dlg_photo_count#other\" = \"{count} photos\";\n\"lng_in_dlg_video_count#one\" = \"{count} video\";\n\"lng_in_dlg_video_count#other\" = \"{count} videos\";\n\"lng_in_dlg_file_count#one\" = \"{count} file\";\n\"lng_in_dlg_file_count#other\" = \"{count} files\";\n\"lng_in_dlg_audio_count#one\" = \"{count} audio\";\n\"lng_in_dlg_audio_count#other\" = \"{count} audio\";\n\n\"lng_ban_user\" = \"Ban User\";\n\"lng_ban_users\" = \"Ban users\";\n\"lng_restrict_users\" = \"Restrict users\";\n\"lng_delete_all_from_user\" = \"Delete all from {user}\";\n\"lng_delete_all_from_users\" = \"Delete all from users\";\n\"lng_restrict_user#one\" = \"Restrict user\";\n\"lng_restrict_user#other\" = \"Restrict users\";\n\"lng_restrict_user_part\" = \"Partially restrict this user {emoji}\";\n\"lng_restrict_users_part\" = \"Partially restrict users {emoji}\";\n\"lng_restrict_user_full\" = \"Fully ban this user {emoji}\";\n\"lng_restrict_users_full\" = \"Fully ban users {emoji}\";\n\"lng_restrict_users_part_single_header\" = \"What can this user do?\";\n\"lng_restrict_users_part_header#one\" = \"What can {count} selected user do?\";\n\"lng_restrict_users_part_header#other\" = \"What can {count} selected users do?\";\n\"lng_restrict_users_kick_from_common_group\" = \"Also ban from:\";\n\"lng_report_spam\" = \"Report Spam\";\n\"lng_report_spam_and_leave\" = \"Report spam and leave\";\n\"lng_report_spam_done\" = \"Thank you for your report\";\n\"lng_report_spam_sure_group\" = \"Are you sure you want to report this group for spam?\";\n\"lng_report_spam_sure_channel\" = \"Are you sure you want to report spam in this channel?\";\n\"lng_report_spam_ok\" = \"Report\";\n\"lng_new_contact_block\" = \"Block user\";\n\"lng_new_contact_block_done\" = \"{user} is now blocked\";\n\"lng_new_contact_add\" = \"Add contact\";\n\"lng_new_contact_share\" = \"Share my phone number\";\n\"lng_new_contact_share_sure\" = \"Do you want to share your phone number {phone} with {user}?\";\n\"lng_new_contact_share_done\" = \"{user} can now see your phone number.\";\n\"lng_new_contact_add_name\" = \"Add {user} to contacts\";\n\"lng_new_contact_add_done\" = \"{user} is now in your contacts\";\n\"lng_new_contact_unarchive\" = \"Unarchive\";\n\"lng_new_contact_from_request_channel\" = \"{user} is an admin of {name}, a channel you requested to join.\";\n\"lng_new_contact_from_request_group\" = \"{user} is an admin of {name}, a group you requested to join.\";\n\"lng_new_contact_about_status\" = \"This account uses {emoji} as a custom status next to its\\nname. Such emoji statuses are available to all\\nsubscribers of {link}.\";\n\"lng_new_contact_about_status_link\" = \"Telegram Premium\";\n\"lng_new_contact_not_contact\" = \"Not a contact\";\n\"lng_new_contact_phone_number\" = \"Phone number\";\n\"lng_new_contact_registration\" = \"Registration\";\n\"lng_new_contact_common_groups\" = \"Common groups\";\n\"lng_new_contact_groups#one\" = \"{count} group {emoji} {arrow}\";\n\"lng_new_contact_groups#other\" = \"{count} groups {emoji} {arrow}\";\n\"lng_new_contact_not_official\" = \"Not an official account\";\n\"lng_new_contact_updated_name\" = \"User updated name {when}\";\n\"lng_new_contact_updated_photo\" = \"User updated photo {when}\";\n\"lng_new_contact_updated_now\" = \"less than an hour ago\";\n\"lng_new_contact_updated_hours#one\" = \"{count} hour ago\";\n\"lng_new_contact_updated_hours#other\" = \"{count} hours ago\";\n\"lng_new_contact_updated_days#one\" = \"{count} day ago\";\n\"lng_new_contact_updated_days#other\" = \"{count} days ago\";\n\"lng_new_contact_updated_months#one\" = \"{count} month ago\";\n\"lng_new_contact_updated_months#other\" = \"{count} months ago\";\n\"lng_from_request_title_channel\" = \"Response to your join request\";\n\"lng_from_request_title_group\" = \"Response to your join request\";\n\"lng_from_request_body\" = \"You received this message because you requested to join {name} on {date}.\";\n\"lng_from_request_understand\" = \"I understand\";\n\"lng_cant_send_to_not_contact\" = \"Sorry, you can only send messages to\\nmutual contacts at the moment.\\n{more_info}\";\n\"lng_cant_invite_not_contact\" = \"Sorry, you can only add mutual contacts\\nto groups at the moment.\\n{more_info}\";\n\"lng_cant_more_info\" = \"Learn more »\";\n\"lng_cant_invite_banned\" = \"Sorry, only admins can add this user.\";\n\"lng_cant_invite_privacy\" = \"Sorry, you cannot add this user to groups because of their privacy settings.\";\n\"lng_cant_invite_bot_to_channel\" = \"Bots can only be added to channels as admins.\";\n\"lng_cant_do_this\" = \"Sorry, this action is unavailable.\";\n\"lng_cant_invite_offer_admin\" = \"Bots can only be added to channels as admins.\";\n\"lng_cant_invite_make_admin\" = \"Make admin\";\n\n\"lng_send_button\" = \"Send\";\n\"lng_schedule_button\" = \"Schedule\";\n\"lng_send_silent_message\" = \"Send without sound\";\n\"lng_schedule_message\" = \"Schedule message\";\n\"lng_reminder_message\" = \"Set a reminder\";\n\"lng_schedule_title\" = \"Send this message on...\";\n\"lng_remind_title\" = \"Remind me on...\";\n\"lng_schedule_repeat_label\" = \"Repeat:\";\n\"lng_schedule_repeat_never\" = \"Never\";\n\"lng_schedule_repeat_daily\" = \"Daily\";\n\"lng_schedule_repeat_weekly\" = \"Weekly\";\n\"lng_schedule_repeat_biweekly\" = \"Biweekly\";\n\"lng_schedule_repeat_monthly\" = \"Monthly\";\n\"lng_schedule_repeat_every_month#one\" = \"Every {count} month\";\n\"lng_schedule_repeat_every_month#other\" = \"Every {count} month\";\n\"lng_schedule_repeat_yearly\" = \"Yearly\";\n\"lng_schedule_repeat_promo\" = \"Subscribe to {link} to schedule repeating messages.\";\n\"lng_schedule_repeat_promo_link\" = \"Telegram Premium\";\n\"lng_schedule_at\" = \"at\";\n\"lng_message_ph\" = \"Write a message...\";\n\"lng_broadcast_ph\" = \"Broadcast a message...\";\n\"lng_broadcast_silent_ph\" = \"Silent broadcast...\";\n\"lng_send_anonymous_ph\" = \"Send anonymously...\";\n\"lng_story_reply_ph\" = \"Reply privately...\";\n\"lng_story_comment_ph\" = \"Comment story...\";\n\"lng_video_stream_comment_ph\" = \"Comment\";\n\"lng_video_stream_comment_paid_ph#one\" = \"Comment for {count} Star\";\n\"lng_video_stream_comment_paid_ph#other\" = \"Comment for {count} Stars\";\n\"lng_video_stream_comments_disabled\" = \"Comments disabled.\";\n\"lng_video_stream_stars\" = \"Add Stars to highlight your comment\";\n\"lng_video_stream_live\" = \"LIVE\";\n\"lng_video_stream_watched#one\" = \"{count} watching\";\n\"lng_video_stream_watched#other\" = \"{count} watching\";\n\"lng_video_stream_edit_stars\" = \"Edit Stars\";\n\"lng_video_stream_remove_stars\" = \"Remove Stars\";\n\"lng_message_stars_ph#one\" = \"Message for {count} Star\";\n\"lng_message_stars_ph#other\" = \"Message for {count} Stars\";\n\"lng_send_text_no\" = \"Text not allowed.\";\n\"lng_send_text_no_about\" = \"The admins of this group only allow sending {types}.\";\n\"lng_send_text_type_and_last\" = \"{types} and {last}\";\n\"lng_send_text_type_photos\" = \"Photos\";\n\"lng_send_text_type_videos\" = \"Videos\";\n\"lng_send_text_type_video_messages\" = \"Video Messages\";\n\"lng_send_text_type_music\" = \"Music\";\n\"lng_send_text_type_voice_messages\" = \"Voice Messages\";\n\"lng_send_text_type_files\" = \"Files\";\n\"lng_send_text_type_stickers\" = \"Stickers & GIFs\";\n\"lng_send_text_type_polls\" = \"Polls\";\n\"lng_send_gif_with_caption\" = \"Send GIF with caption\";\n\n\"lng_send_as_title\" = \"Send message as...\";\n\"lng_send_as_anonymous_admin\" = \"Anonymous admin\";\n\"lng_send_as_premium_required\" = \"Subscribe to {link} to be able to comment on behalf of your channels in group chats.\";\n\"lng_send_as_premium_required_link\" = \"Telegram Premium\";\n\"lng_record_cancel\" = \"Release outside this field to cancel\";\n\"lng_record_cancel_stories\" = \"Release outside to cancel\";\n\"lng_record_lock_cancel_sure\" = \"Do you want to stop recording and discard your voice message?\";\n\"lng_record_lock_cancel_sure_round\" = \"Do you want to stop recording and discard your video message?\";\n\"lng_record_listen_cancel_sure\" = \"Do you want to discard your recorded voice message?\";\n\"lng_record_listen_cancel_sure_round\" = \"Do you want to discard your recorded video message?\";\n\"lng_record_lock_discard\" = \"Discard\";\n\"lng_record_lock\" = \"Lock recording\";\n\"lng_record_lock_resume\" = \"Resume recording\";\n\"lng_record_lock_delete\" = \"Delete recording\";\n\"lng_record_lock_play\" = \"Play recording\";\n\"lng_record_lock_pause\" = \"Pause recording\";\n\"lng_record_cancel_recording\" = \"Cancel recording\";\n\"lng_record_hold_tip\" = \"Please hold the mouse button pressed to record a voice message.\";\n\"lng_record_voice_tip\" = \"Hold to record audio. Click to switch to video.\";\n\"lng_record_video_tip\" = \"Hold to record video. Click to switch to audio.\";\n\"lng_record_audio_problem\" = \"Could not start audio recording. Please check your microphone.\";\n\"lng_record_video_problem\" = \"Could not start video recording. Please check your camera.\";\n\"lng_record_once_first_tooltip\" = \"Click to set this message to **Play Once**.\";\n\"lng_record_once_active_tooltip\" = \"The recipient will be able to listen only once.\";\n\"lng_record_once_active_video\" = \"The recipient will be able to watch only once.\";\n\"lng_will_be_notified\" = \"Subscribers will be notified when you post.\";\n\"lng_wont_be_notified\" = \"Subscribers will receive a silent notification.\";\n\"lng_willbe_history\" = \"Select a chat to start messaging\";\n\"lng_from_you\" = \"You\";\n\"lng_from_draft\" = \"Draft\";\n\"lng_bot_description\" = \"What can this bot do?\";\n\"lng_unblock_button\" = \"Unblock\";\n\"lng_restart_button\" = \"Restart\";\n\"lng_channel_mute\" = \"Mute\";\n\"lng_channel_unmute\" = \"Unmute\";\n\"lng_saved_messages\" = \"Saved Messages\";\n\"lng_saved_short\" = \"Save\";\n\"lng_saved_forward_here\" = \"Forward messages here for quick access\";\n\"lng_saved_quote_here\" = \"Quote here to save\";\n\"lng_saved_open_chat\" = \"Open Chat\";\n\"lng_saved_open_channel\" = \"Open Channel\";\n\"lng_saved_open_group\" = \"Open Group\";\n\"lng_saved_about_hidden\" = \"Senders of these messages restricted to link their name when forwarding.\";\n\n\"lng_scheduled_messages\" = \"Scheduled Messages\";\n\"lng_scheduled_messages_empty\" = \"No scheduled messages here yet...\";\n\"lng_reminder_messages\" = \"Reminders\";\n\"lng_scheduled_date\" = \"Scheduled for {date}\";\n\"lng_scheduled_date_until_online\" = \"Scheduled until online\";\n\"lng_scheduled_send_until_online\" = \"Send when online\";\n\"lng_scheduled_send_now\" = \"Send message now?\";\n\"lng_scheduled_send_now_many#one\" = \"Send {count} message now?\";\n\"lng_scheduled_send_now_many#other\" = \"Send {count} messages now?\";\n\"lng_scheduled_video_tip_title\" = \"Improving video...\";\n\"lng_scheduled_video_tip_text\" = \"The video will be published after it's optimized for the best viewing experience.\";\n\"lng_scheduled_video_tip\" = \"Processing video may take a few minutes.\";\n\"lng_scheduled_video_published\" = \"Video Published.\";\n\"lng_scheduled_video_view\" = \"View\";\n\n\"lng_replies_view#one\" = \"View {count} Reply\";\n\"lng_replies_view#other\" = \"View {count} Replies\";\n\"lng_replies_view_thread\" = \"View Thread\";\n\"lng_replies_header#one\" = \"{count} reply\";\n\"lng_replies_header#other\" = \"{count} replies\";\n\"lng_replies_header_none\" = \"Replies\";\n\"lng_replies_view_topic\" = \"View in Topic\";\n\"lng_comments_header#one\" = \"{count} comment\";\n\"lng_comments_header#other\" = \"{count} comments\";\n\"lng_comments_header_none\" = \"Comments\";\n\"lng_comments_open_count#one\" = \"{count} comment\";\n\"lng_comments_open_count#other\" = \"{count} comments\";\n\"lng_comments_open_none\" = \"Leave a comment\";\n\"lng_replies_view_original\" = \"View in chat\";\n\"lng_replies_messages\" = \"Replies\";\n\"lng_hidden_author_messages\" = \"Author Hidden\";\n\"lng_my_notes\" = \"My Notes\";\n\"lng_replies_discussion_started\" = \"Discussion started\";\n\"lng_replies_no_comments\" = \"No comments here yet...\";\n\n\"lng_verification_codes\" = \"Verification Codes\";\n\"lng_verification_codes_about\" = \"Third-party services, like websites and stores, can send verification codes to your phone number via Telegram instead of SMS. Such codes will appear in this chat.\\n\\nIf you didn't request any codes — don't worry! Most likely, someone made a mistake when entering their number.\";\n\n\"lng_archived_name\" = \"Archived chats\";\n\"lng_archived_add\" = \"Archive\";\n\"lng_archived_remove\" = \"Unarchive\";\n\"lng_archived_added\" = \"Chat archived.\\nMuted chats stay archived when new messages arrive.\";\n\"lng_archived_removed\" = \"Chat restored from your archive.\";\n\"lng_archived_last_list\" = \"{accumulated}, {chat}\";\n\"lng_archived_last#one\" = \"{chats} and {count} more chat\";\n\"lng_archived_last#other\" = \"{chats} and {count} more chats\";\n\n\"lng_dialogs_text_with_from\" = \"{from_part} {message}\";\n\"lng_dialogs_text_from_wrapped\" = \"{from}:\";\n\"lng_dialogs_text_media\" = \"{media_part} {caption}\";\n\"lng_dialogs_text_media_wrapped\" = \"{media},\";\n\"lng_dialogs_text_from_in_topic\" = \"{from} {topic}\";\n\"lng_dialogs_skip_archive_in_search\" = \"Skip results from archive\";\n\"lng_dialogs_show_archive_in_search\" = \"With results from archive\";\n\n\"lng_dialogs_suggestions_birthday_title\" = \"Add your birthday! 🎂\";\n\"lng_dialogs_suggestions_birthday_about\" = \"Let your contacts know when you’re celebrating.\";\n\"lng_dialogs_suggestions_birthday_contact_title\" = \"It’s {text}'s **birthday** today! 🎂\";\n\"lng_dialogs_suggestions_birthday_contact_about\" = \"Send them a Gift.\";\n\"lng_dialogs_suggestions_birthday_contacts_title#one\" = \"{count} contact have **birthdays** today! 🎂\";\n\"lng_dialogs_suggestions_birthday_contacts_title#other\" = \"{count} contacts have **birthdays** today! 🎂\";\n\"lng_dialogs_suggestions_birthday_contacts_about\" = \"Send them a Gift.\";\n\"lng_dialogs_suggestions_birthday_contact_dismiss\" = \"You can send a Gift later in Settings\";\n\"lng_dialogs_suggestions_premium_annual_title\" = \"Telegram Premium with a {text} discount\";\n\"lng_dialogs_suggestions_premium_annual_about\" = \"Sign up for the annual payment plan for Telegram Premium now to get the discount.\";\n\"lng_dialogs_suggestions_premium_upgrade_title\" = \"Telegram Premium with a {text} discount\";\n\"lng_dialogs_suggestions_premium_upgrade_about\" = \"Upgrade to the annual payment plan for Telegram Premium now to get the discount.\";\n\"lng_dialogs_suggestions_premium_restore_title\" = \"Get Premium back with up to {text} off\";\n\"lng_dialogs_suggestions_premium_restore_about\" = \"Your Telegram Premium has recently expired. Tap here to extend it.\";\n\"lng_dialogs_suggestions_premium_grace_title\" = \"⚠️ Your Premium subscription is expiring!\";\n\"lng_dialogs_suggestions_premium_grace_about\" = \"Don’t lose access to exclusive features.\";\n\"lng_dialogs_suggestions_userpics_title\" = \"Add your photo! 📸\";\n\"lng_dialogs_suggestions_userpics_about\" = \"Help your friends spot you easily.\";\n\"lng_dialogs_suggestions_credits_sub_low_title#one\" = \"{emoji} {count} Star needed for {channels}\";\n\"lng_dialogs_suggestions_credits_sub_low_title#other\" = \"{emoji} {count} Stars needed for {channels}\";\n\"lng_dialogs_suggestions_credits_sub_low_about\" = \"Insufficient funds to cover your subscription.\";\n\n\"lng_unconfirmed_auth_title\" = \"Someone just got access to your messages!\";\n\"lng_unconfirmed_auth_confirm\" = \"Yes, it’s me\";\n\"lng_unconfirmed_auth_deny\" = \"No, it’s not me!\";\n\"lng_unconfirmed_auth_single\" = \"We detected a new login to your account from {from}, {country}. Is it you?\";\n\"lng_unconfirmed_auth_multiple#one\" = \"We detected new {count} login to your account. Is it you?\";\n\"lng_unconfirmed_auth_multiple#other\" = \"We detected new {count} logins to your account. Is it you?\";\n\"lng_unconfirmed_auth_multiple_from#one\" = \"We detected new {count} login to your account from {country}. Is it you?\";\n\"lng_unconfirmed_auth_multiple_from#other\" = \"We detected new {count} logins to your account from {country}. Is it you?\";\n\"lng_unconfirmed_auth_denied_title#one\" = \"New Login Prevented\";\n\"lng_unconfirmed_auth_denied_title#other\" = \"New Logins Prevented\";\n\"lng_unconfirmed_auth_denied_single\" = \"We have terminated the login attempt from {country}.\";\n\"lng_unconfirmed_auth_denied_multiple\" = \"We have terminated the login attempts from: {country}\";\n\"lng_unconfirmed_auth_denied_warning\" = \"Never send your login code to anyone or you can lose your Telegram account!\";\n\"lng_unconfirmed_auth_confirmed\" = \"New Login Allowed\";\n\"lng_unconfirmed_auth_confirmed_message\" = \"You can check the list of your active logins in {link}.\";\n\n\"lng_about_random\" = \"Send a {emoji} emoji to any chat to try your luck.\";\n\"lng_about_random_send\" = \"Send\";\n\"lng_about_random_stake\" = \"Stake: {amount}\";\n\"lng_about_random_stake_change\" = \"change\";\n\n\"lng_open_this_link\" = \"Open this link?\";\n\"lng_open_link\" = \"Open\";\n\"lng_allow_bot_pass\" = \"Allow {bot_name} to pass your Telegram name and ID (not your phone number) to the web pages you open via this bot?\";\n\"lng_allow_bot\" = \"Allow\";\n\"lng_allow_bot_webview_details\" = \"More about this bot {emoji}\";\n\"lng_allow_bot_webview_details_about\" = \"To launch this web app, you will connect to its website.\\n\\nIt will be able to access your **IP address** and basic device info.\";\n\"lng_url_auth_open_confirm\" = \"Do you want to open {link}?\";\n\"lng_url_auth_login_option\" = \"Log in to {domain} as {user}\";\n\"lng_url_auth_allow_messages\" = \"Allow {bot} to send me messages\";\n\"lng_url_auth_login_title\" = \"Log in to {domain}\";\n\"lng_url_auth_login_button\" = \"Log in\";\n\"lng_url_auth_site_access\" = \"This site will receive your **name**, **username** and **profile photo**.\";\n\"lng_url_auth_app_access\" = \"This app will receive your **name**, **username** and **profile photo**.\";\n\"lng_url_auth_unverified_app\" = \"Unverified App\";\n\"lng_url_auth_device_label\" = \"Device\";\n\"lng_url_auth_ip_label\" = \"IP Address\";\n\"lng_url_auth_login_attempt\" = \"This login attempt came from the device above.\";\n\"lng_url_auth_allow_messages_label\" = \"Allow Messages\";\n\"lng_url_auth_allow_messages_about\" = \"This will allow {bot} to message you\";\n\"lng_url_auth_phone_sure_title\" = \"Phone Number\";\n\"lng_url_auth_phone_sure_text\" = \"{domain} wants to access your phone number **{phone}**.\\n\\nAllow access?\";\n\"lng_url_auth_phone_sure_deny\" = \"Deny\";\n\"lng_url_auth_phone_toast_good_title\" = \"Login successful\";\n\"lng_url_auth_phone_toast_good\" = \"You're now logged in to {domain}.\";\n\"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.\";\n\"lng_url_auth_phone_toast_bad_title\" = \"Login failed\";\n\"lng_url_auth_phone_toast_bad\" = \"Please try logging in to {domain} again.\";\n\"lng_url_auth_phone_toast_bad_expired\" = \"This link is expired.\";\n\"lng_url_auth_match_code_title\" = \"Tap the emoji shown on your other device\";\n\"lng_url_auth_match_code_info\" = \"Login request from {domain}\";\n\n\"lng_bot_start\" = \"Start\";\n\"lng_bot_choose_group\" = \"Select a Group\";\n\"lng_bot_no_groups\" = \"You have no groups\";\n\"lng_bot_groups_not_found\" = \"No groups found\";\n\"lng_bot_sure_invite\" = \"Add the bot to «{group}»?\";\n\"lng_bot_already_in_group\" = \"The bot is already a member of the group.\";\n\"lng_bot_choose_chat\" = \"Select a chat\";\n\"lng_bot_no_chats\" = \"You have no chats\";\n\"lng_bot_chats_not_found\" = \"No chats found\";\n\"lng_bot_sure_share_game\" = \"Share the game with {user}?\";\n\"lng_bot_sure_share_game_group\" = \"Share the game with «{group}»?\";\n\"lng_bot_groups_manage\" = \"Groups I manage\";\n\"lng_bot_channels_manage\" = \"Channels I manage\";\n\"lng_bot_groups\" = \"Groups\";\n\"lng_bot_add_title\" = \"Add Bot\";\n\"lng_bot_as_admin_check\" = \"Admin rights\";\n\"lng_bot_add_as_admin\" = \"Add Bot as Admin\";\n\"lng_bot_add_as_member\" = \"Add Bot as Member\";\n\"lng_bot_sure_add_text_group\" = \"Are you sure you want to add this bot as an admin in the group {group}? It will have access to the list of anonymous admins.\";\n\"lng_bot_sure_add_text_channel\" = \"Are you sure you want to add this bot as an admin in the channel {group}? It will have access to lists of channel admins and subscribers.\";\n\"lng_bot_no_webview\" = \"Unfortunately, you can't open this menu with your current system configuration.\";\n\"lng_bot_remove_from_menu\" = \"Remove from menu\";\n\"lng_bot_remove_from_menu_sure\" = \"Remove {bot} from the attachment menu?\";\n\"lng_bot_remove_from_menu_done\" = \"Bot removed from the menu.\";\n\"lng_bot_remove_from_side_menu\" = \"Remove From Menu\";\n\"lng_bot_remove_from_side_menu_sure\" = \"Remove {bot} from the main menu?\";\n\"lng_bot_remove_from_side_menu_done\" = \"Bot removed from the main menu.\";\n\"lng_bot_settings\" = \"Settings\";\n\"lng_bot_open\" = \"Open Bot\";\n\"lng_bot_terms\" = \"Terms of Use\";\n\"lng_bot_privacy\" = \"Privacy Policy\";\n\"lng_bot_reload_page\" = \"Reload Page\";\n\"lng_bot_add_to_menu\" = \"{bot} asks your permission to be added as an option to your attachment menu so you can access it from any chat.\";\n\"lng_bot_add_to_menu_done\" = \"Bot added to the menu.\";\n\"lng_bot_will_be_added\" = \"{bot} shortcuts will be added to the attachment menu and the main menu.\";\n\"lng_bot_side_menu_new\" = \"NEW\";\n\"lng_bot_menu_not_supported\" = \"This bot can't be added to the attachment menu.\";\n\"lng_bot_menu_already_added\" = \"This bot is already added to your attachment menu.\";\n\"lng_bot_menu_button\" = \"Menu\";\n\"lng_bot_close_warning_title\" = \"Warning\";\n\"lng_bot_close_warning\" = \"Changes that you made may not be saved.\";\n\"lng_bot_close_warning_sure\" = \"Close anyway\";\n\"lng_bot_add_to_side_menu\" = \"{bot} asks your permission to be added as an option to your main menu so you can access it any time.\";\n\"lng_bot_add_to_side_menu_done\" = \"Bot added to the main menu.\";\n\"lng_bot_no_scan_qr\" = \"QR Codes for bots are not supported on Desktop. Please use one of Telegram's mobile apps.\";\n\"lng_bot_no_share_story\" = \"Sharing to Stories is not supported on Desktop. Please use one of Telegram's mobile apps.\";\n\"lng_bot_emoji_status_confirm\" = \"Confirm\";\n\"lng_bot_emoji_status_title\" = \"Set Emoji Status\";\n\"lng_bot_emoji_status_text\" = \"Do you want to set this emoji status suggested by {bot}?\";\n\"lng_bot_emoji_status_access_text\" = \"{bot} requests access to set your **emoji status**. You will be able to revoke this access in the profile page of {name}.\";\n\"lng_bot_emoji_status_access_allow\" = \"Allow\";\n\"lng_bot_share_prepared_title\" = \"Share Message\";\n\"lng_bot_share_prepared_about\" = \"{bot} mini app suggests you to send this message to a chat you select.\";\n\"lng_bot_share_prepared_button\" = \"Share With...\";\n\"lng_bot_download_file\" = \"Download File\";\n\"lng_bot_download_file_sure\" = \"{bot} suggests you download the following file:\";\n\"lng_bot_download_file_button\" = \"Download\";\n\"lng_bot_download_starting\" = \"Starting...\";\n\"lng_bot_download_failed\" = \"Failed. {retry}\";\n\"lng_bot_download_retry\" = \"Retry\";\n\n\"lng_bot_status_users#one\" = \"{count} monthly user\";\n\"lng_bot_status_users#other\" = \"{count} monthly users\";\n\n\"lng_typing\" = \"typing\";\n\"lng_user_typing\" = \"{user} is typing\";\n\"lng_users_typing\" = \"{user} and {second_user} are typing\";\n\"lng_many_typing#one\" = \"{count} person is typing\";\n\"lng_many_typing#other\" = \"{count} people are typing\";\n\"lng_playing_game\" = \"playing a game\";\n\"lng_user_playing_game\" = \"{user} is playing a game\";\n\"lng_users_playing_game\" = \"{user} and {second_user} are playing a game\";\n\"lng_many_playing_game#one\" = \"{count} person is playing a game\";\n\"lng_many_playing_game#other\" = \"{count} people are playing a game\";\n\"lng_send_action_record_video\" = \"recording a video\";\n\"lng_user_action_record_video\" = \"{user} is recording a video\";\n\"lng_send_action_upload_video\" = \"sending a video\";\n\"lng_user_action_upload_video\" = \"{user} is sending a video\";\n\"lng_send_action_record_audio\" = \"recording a voice message\";\n\"lng_user_action_record_audio\" = \"{user} is recording a voice message\";\n\"lng_send_action_upload_audio\" = \"sending a voice message\";\n\"lng_user_action_upload_audio\" = \"{user} is sending a voice message\";\n\"lng_send_action_record_round\" = \"recording a video message\";\n\"lng_user_action_record_round\" = \"{user} is recording a video message\";\n\"lng_send_action_upload_round\" = \"sending a video message\";\n\"lng_user_action_upload_round\" = \"{user} is sending a video message\";\n\"lng_send_action_upload_photo\" = \"sending a photo\";\n\"lng_user_action_upload_photo\" = \"{user} is sending a photo\";\n\"lng_send_action_upload_file\" = \"sending a file\";\n\"lng_user_action_upload_file\" = \"{user} is sending a file\";\n\"lng_send_action_choose_sticker\" = \"choosing a sticker\";\n\"lng_user_action_choose_sticker\" = \"{user} is choosing a sticker\";\n\"lng_user_action_watching_animations\" = \"watching {emoji}\";\n\"lng_unread_bar#one\" = \"{count} Unread Message\";\n\"lng_unread_bar#other\" = \"{count} Unread Messages\";\n\"lng_unread_bar_some\" = \"Unread messages\";\n\n\"lng_maps_point\" = \"Location\";\n\"lng_maps_select_on_map\" = \"Select on the Map\";\n\"lng_maps_point_send\" = \"Send This Location\";\n\"lng_maps_point_set\" = \"Set This Location\";\n\"lng_maps_or_choose\" = \"Or choose a venue\";\n\"lng_maps_places_in_area\" = \"Places in this area\";\n\"lng_maps_no_places\" = \"No places found\";\n\"lng_maps_choose_to_search\" = \"Choose location to see places nearby.\";\n\"lng_maps_venues_source\" = \"Powered by Foursquare\";\n\"lng_live_location\" = \"Live Location\";\n\"lng_live_location_now\" = \"updated just now\";\n\"lng_live_location_minutes#one\" = \"updated {count} minute ago\";\n\"lng_live_location_minutes#other\" = \"updated {count} minutes ago\";\n\"lng_live_location_hours#one\" = \"updated {count} hour ago\";\n\"lng_live_location_hours#other\" = \"updated {count} hours ago\";\n\"lng_live_location_today\" = \"updated today at {time}\";\n\"lng_live_location_yesterday\" = \"updated yesterday at {time}\";\n\"lng_live_location_date_time\" = \"updated {date} at {time}\";\n\"lng_save_photo\" = \"Save Image\";\n\"lng_save_video\" = \"Save Video\";\n\"lng_save_audio_file\" = \"Save Audio File\";\n\"lng_save_audio\" = \"Save voice message\";\n\"lng_save_file\" = \"Save File\";\n\"lng_save_downloaded\" = \"{ready} / {total} {mb}\";\n\"lng_duration_and_size\" = \"{duration}, {size}\";\n\"lng_duration_played\" = \"{played} / {duration}\";\n\"lng_date_and_duration\" = \"{date}, {duration}\";\n\"lng_choose_image\" = \"Choose an image\";\n\"lng_choose_file\" = \"Choose a file\";\n\"lng_choose_files\" = \"Choose Files\";\n\"lng_choose_cover\" = \"Choose video cover\";\n\"lng_choose_cover_bad\" = \"Can't use this file as a caption.\";\n\"lng_game_tag\" = \"Game\";\n\n\"lng_context_new_window\" = \"Open in new window\";\n\"lng_context_view_profile\" = \"View profile\";\n\"lng_context_send_message\" = \"Send message\";\n\"lng_context_view_group\" = \"View group info\";\n\"lng_context_view_channel\" = \"View channel info\";\n\"lng_context_view_topic\" = \"View topic info\";\n\"lng_context_view_thread\" = \"View thread info\";\n\"lng_context_hide_psa\" = \"Hide this announcement\";\n\"lng_context_pin_to_top\" = \"Pin\";\n\"lng_context_unpin_from_top\" = \"Unpin\";\n\"lng_context_mark_unread\" = \"Mark as unread\";\n\"lng_context_mark_read\" = \"Mark as read\";\n\"lng_context_mark_read_sure\" = \"Are you sure you want to mark all chats from this folder as read?\";\n\"lng_context_mark_read_all\" = \"Mark all chats as read\";\n\"lng_context_mark_read_all_sure\" = \"Are you sure you want to mark all chats as read?\";\n\"lng_context_mark_read_all_sure_2\" = \"**This action cannot be undone.**\";\n\"lng_context_mark_read_mentions_all\" = \"Mark all mentions as read\";\n\"lng_context_mark_read_reactions_all\" = \"Read all reactions\";\n\"lng_context_mark_read_poll_votes_all\" = \"Read all poll votes\";\n\"lng_context_archive_expand\" = \"Expand\";\n\"lng_context_archive_collapse\" = \"Collapse\";\n\"lng_context_archive_to_menu\" = \"Move to main menu\";\n\"lng_context_archive_to_list\" = \"Move to chat list\";\n\"lng_context_archive_to_menu_info\" = \"Archive moved to the main menu!\\nRight click the archive button to return the Archive to your chat list.\";\n\"lng_context_archive_settings\" = \"Archive settings\";\n\"lng_context_archive_how_does_it_work\" = \"How does it work?\";\n\n\"lng_context_mute\" = \"Mute notifications\";\n\"lng_context_unmute\" = \"Unmute\";\n\n\"lng_context_promote_admin\" = \"Promote to admin\";\n\"lng_context_edit_permissions\" = \"Edit admin rights\";\n\"lng_context_restrict_user\" = \"Restrict user\";\n\"lng_context_ban_user\" = \"Ban\";\n\"lng_context_delete_and_ban\" = \"Delete and ban\";\n\"lng_context_remove_from_group\" = \"Remove from group\";\n\"lng_context_add_to_group\" = \"Add to group\";\n\n\"lng_context_rate_transcription\" = \"Rate transcription\";\n\"lng_toast_sent_rate_transcription\" = \"Thank you for your feedback!\";\n\n\"lng_context_copy_link\" = \"Copy Link\";\n\"lng_context_copy_message_link\" = \"Copy Message Link\";\n\"lng_context_copy_post_link\" = \"Copy Post Link\";\n\"lng_context_copy_topic_link\" = \"Copy Topic Link\";\n\"lng_context_copy_email\" = \"Copy Email Address\";\n\"lng_context_copy_hashtag\" = \"Copy Hashtag\";\n\"lng_context_copy_mention\" = \"Copy Username\";\n\"lng_context_copy_filename\" = \"Copy Filename\";\n\"lng_context_save_image\" = \"Save As...\";\n\"lng_context_copy_image\" = \"Copy Image\";\n\"lng_context_cancel_download\" = \"Cancel Download\";\n\"lng_context_show_in_folder\" = \"Show in Folder\";\n\"lng_context_show_in_finder\" = \"Show in Finder\";\n\"lng_context_save_video\" = \"Save As...\";\n\"lng_context_save_audio_file\" = \"Save As...\";\n\"lng_context_save_audio\" = \"Save As...\";\n\"lng_context_pack_info\" = \"View Sticker Set\";\n\"lng_context_pack_add\" = \"Add Stickers\";\n\"lng_context_save_file\" = \"Save As...\";\n\"lng_context_save_music_to\" = \"Save to...\";\n\"lng_context_save_music_profile\" = \"... Profile\";\n\"lng_context_save_music_saved\" = \"... Saved Messages\";\n\"lng_context_save_music_folder\" = \"... Downloads\";\n\"lng_context_save_music_about\" = \"Choose where you want this audio to be saved.\";\n\"lng_context_copy_text\" = \"Copy Text\";\n\"lng_context_open_gif\" = \"Open GIF\";\n\"lng_context_save_gif\" = \"Add to GIFs\";\n\"lng_context_delete_gif\" = \"Delete\";\n\"lng_context_open_channel\" = \"Open Channel\";\n\"lng_context_open_group\" = \"Open Group\";\n\"lng_context_attached_stickers\" = \"Attached Stickers\";\n\"lng_context_to_msg\" = \"Go To Message\";\n\"lng_context_reply_msg\" = \"Reply\";\n\"lng_context_quote_and_reply\" = \"Quote & Reply\";\n\"lng_context_reply_with_timecode\" = \"Reply with timecode\";\n\"lng_context_reply_to_task\" = \"Reply to Task\";\n\"lng_context_reply_to_poll_option\" = \"Reply to Option\";\n\"lng_context_copy_poll_option\" = \"Copy Option\";\n\"lng_context_copy_poll_option_link\" = \"Copy Option Link\";\n\"lng_context_delete_poll_option\" = \"Delete Item\";\n\"lng_context_poll_message_tab\" = \"Poll\";\n\"lng_context_poll_option_tab\" = \"Option\";\n\"lng_context_edit_msg\" = \"Edit\";\n\"lng_context_draw\" = \"Edit Image\";\n\"lng_context_add_factcheck\" = \"Add Fact Check\";\n\"lng_context_edit_factcheck\" = \"Edit Fact Check\";\n\"lng_context_add_offer\" = \"Add Offer\";\n\"lng_context_forward_msg\" = \"Forward\";\n\"lng_context_send_now_msg\" = \"Send Now\";\n\"lng_context_reschedule\" = \"Reschedule\";\n\"lng_context_delete_msg\" = \"Delete\";\n\"lng_context_auto_delete_in\" = \"auto-deletes in {duration}\";\n\"lng_context_select_msg\" = \"Select\";\n\"lng_context_select_msg_bulk\" = \"Select up to this message\";\n\"lng_context_report_msg\" = \"Report\";\n\"lng_context_pin_msg\" = \"Pin\";\n\"lng_context_unpin_msg\" = \"Unpin\";\n\"lng_context_cancel_upload\" = \"Cancel Upload\";\n\"lng_context_upload_edit_caption\" = \"Edit Caption\";\n\"lng_context_copy_selected\" = \"Copy Selected Text\";\n\"lng_context_copy_selected_items\" = \"Copy Selected as Text\";\n\"lng_context_forward_selected\" = \"Forward Selected\";\n\"lng_context_send_now_selected\" = \"Send selected now\";\n\"lng_context_reschedule_selected\" = \"Reschedule Selected\";\n\"lng_context_delete_selected\" = \"Delete Selected\";\n\"lng_context_save_images_selected\" = \"Save Selected\";\n\"lng_context_save_documents_selected\" = \"Download Selected\";\n\"lng_context_clear_selection\" = \"Clear Selection\";\n\"lng_context_to_save_messages\" = \"To Saved Messages\";\n\"lng_context_group_items\" = \"Group Selected\";\n\n\"lng_context_seen_loading\" = \"Loading...\";\n\"lng_context_seen_text#one\" = \"{count} Seen\";\n\"lng_context_seen_text#other\" = \"{count} Seen\";\n\"lng_context_seen_text_none\" = \"Nobody Viewed\";\n\"lng_context_seen_listened#one\" = \"{count} Listened\";\n\"lng_context_seen_listened#other\" = \"{count} Listened\";\n\"lng_context_seen_listened_none\" = \"Nobody Listened\";\n\"lng_context_seen_watched#one\" = \"{count} Listened\";\n\"lng_context_seen_watched#other\" = \"{count} Listened\";\n\"lng_context_seen_watched_none\" = \"Nobody Listened\";\n\"lng_context_seen_reacted#one\" = \"{count} Reacted\";\n\"lng_context_seen_reacted#other\" = \"{count} Reacted\";\n\"lng_context_seen_reactions_count#one\" = \"{count} Reaction\";\n\"lng_context_seen_reactions_count#other\" = \"{count} Reactions\";\n\"lng_context_seen_reacted_none\" = \"Nobody Reacted\";\n\"lng_context_seen_reacted_all\" = \"Show All Reactions\";\n\"lng_context_sent_by\" = \"Sent by {user}\";\n\"lng_context_sent_today\" = \"Sent today at {time}\";\n\"lng_context_sent_yesterday\" = \"Sent yesterday at {time}\";\n\"lng_context_sent_date\" = \"Sent {date} at {time}\";\n\"lng_context_set_as_quick\" = \"Set As Quick\";\n\"lng_context_filter_by_tag\" = \"Filter by Tag\";\n\"lng_context_tag_add_name\" = \"Add Name\";\n\"lng_context_tag_edit_name\" = \"Edit Name\";\n\"lng_context_remove_tag\" = \"Remove Tag\";\n\"lng_context_delete_from_disk\" = \"Delete from disk\";\n\"lng_context_delete_all_files\" = \"Delete all files\";\n\"lng_context_save_custom_sound\" = \"Save for Notifications\";\n\"lng_context_translate\" = \"Translate\";\n\"lng_context_translate_selected\" = \"Translate Selected Text\";\n\"lng_context_read_hidden\" = \"read\";\n\"lng_context_read_show\" = \"show when\";\n\"lng_context_edit_shortcut\" = \"Edit Shortcut\";\n\"lng_context_delete_shortcut\" = \"Delete Quick Reply\";\n\"lng_context_gift_send\" = \"Send Another Gift\";\n\"lng_context_charge_fee\" = \"Charge Fee\";\n\"lng_context_remove_fee\" = \"Remove Fee\";\n\"lng_context_fee_now\" = \"{name} pays {amount} per message.\";\n\"lng_context_fee_free\" = \"{name} can send messages for free.\";\n\n\"lng_add_tag_about\" = \"Tag this message with an emoji for quick search.\";\n\"lng_subscribe_tag_about\" = \"Organize your Saved Messages with tags. {link}\";\n\"lng_subscribe_tag_link\" = \"Learn More...\";\n\"lng_edit_tag_about\" = \"You can label your emoji tag with a text name.\";\n\"lng_edit_tag_name\" = \"Name\";\n\"lng_add_tag_button\" = \"Add tags\";\n\"lng_add_tag_phrase\" = \"to messages {arrow}\";\n\"lng_add_tag_phrase_long\" = \"to your Saved Messages {arrow}\";\n\"lng_unlock_tags\" = \"Unlock\";\n\"lng_add_tag_selector#one\" = \"You can add a tag to the message\";\n\"lng_add_tag_selector#other\" = \"You can add a tag to the messages\";\n\"lng_message_tagged_with\" = \"Message tagged with {emoji}\";\n\"lng_tagged_view_saved\" = \"View\";\n\n\"lng_add_channel_to_filter_selector\" = \"You can add a channel to your folder\";\n\"lng_add_group_to_filter_selector\" = \"You can add a group to your folder\";\n\n\"lng_context_animated_emoji\" = \"This message contains emoji from **{name} pack**.\";\n\"lng_context_animated_emoji_many#one\" = \"This message contains emoji from **{count} pack**.\";\n\"lng_context_animated_emoji_many#other\" = \"This message contains emoji from **{count} packs**.\";\n\"lng_context_animated_reaction\" = \"This reaction is from the **{name}** pack.\";\n\"lng_context_animated_emoji_preview\" = \"This emoji is from the **{name}** pack.\";\n\"lng_context_animated_tag\" = \"This tag is from **{name} pack**.\";\n\"lng_context_animated_reactions\" = \"Reactions contain emoji from **{name} pack**.\";\n\"lng_context_animated_reactions_many#one\" = \"Reactions contain emoji from **{count} pack**.\";\n\"lng_context_animated_reactions_many#other\" = \"Reactions contain emoji from **{count} packs**.\";\n\"lng_context_animated_poll_option\" = \"This option contains emoji from **{name} pack**.\";\n\"lng_context_animated_poll_option_many#one\" = \"This option contains emoji from **{count} pack**.\";\n\"lng_context_animated_poll_option_many#other\" = \"This option contains emoji from **{count} packs**.\";\n\n\"lng_context_noforwards_info_channel\" = \"Copying and forwarding is not allowed in this channel.\";\n\"lng_context_noforwards_info_group\" = \"Copying and forwarding is not allowed in this group.\";\n\"lng_context_noforwards_info_bot\" = \"Copying and forwarding is not allowed from this bot.\";\n\"lng_context_noforwards_info_his\" = \"{user} disabled copying and forwarding in this chat.\";\n\"lng_context_noforwards_info_mine\" = \"You disabled copying and forwarding in this chat.\";\n\n\"lng_context_spoiler_effect\" = \"Hide with Spoiler\";\n\"lng_context_make_paid\" = \"Make This Content Paid\";\n\"lng_context_change_price\" = \"Change Price\";\n\"lng_context_edit_cover\" = \"Edit Cover\";\n\"lng_context_clear_cover\" = \"Clear Cover\";\n\n\"lng_context_mention\" = \"Mention\";\n\"lng_context_search_from\" = \"Search messages\";\n\n\"lng_factcheck_title\" = \"Fact Check\";\n\"lng_factcheck_placeholder\" = \"Add Facts or Context\";\n\"lng_factcheck_whats_this\" = \"what's this?\";\n\"lng_factcheck_about\" = \"This clarification was provided by a fact checking agency assigned by the department of the government of your country ({country}) responsible for combatting misinformation.\";\n\"lng_factcheck_add_done\" = \"Fact check added.\";\n\"lng_factcheck_edit_done\" = \"Fact check edited.\";\n\"lng_factcheck_remove_done\" = \"Fact check removed.\";\n\"lng_factcheck_bottom\" = \"This clarification was provided by a fact checking agency assigned by the department of the government of your country ({country}) responsible for combatting misinformation.\";\n\"lng_factcheck_links\" = \"Only **t.me/** links are allowed.\";\n\n\"lng_paid_title\" = \"Paid Content\";\n\"lng_paid_enter_cost\" = \"Enter Unlock Cost\";\n\"lng_paid_cost_placeholder\" = \"Stars to Unlock\";\n\"lng_paid_about\" = \"Users will have to transfer this amount of Stars to your channel in order to view this media. {link}\";\n\"lng_paid_about_link\" = \"More about stars >\";\n\"lng_paid_about_link_url\" = \"https://telegram.org/blog/telegram-stars\";\n\"lng_paid_price\" = \"Unlock for {price}\";\n\n\"lng_paid_react_title\" = \"Star Reaction\";\n\"lng_paid_react_about\" = \"Choose how many **Stars** you want to send to {channel} to support this post.\";\n\"lng_paid_react_already#one\" = \"You sent **{count} Star** to support this post.\";\n\"lng_paid_react_already#other\" = \"You sent **{count} Stars** to support this post.\";\n\"lng_paid_react_top_title\" = \"Top Senders\";\n\"lng_paid_react_send\" = \"Send {price}\";\n\"lng_paid_react_agree\" = \"By sending stars, you agree to the {link}.\";\n\"lng_paid_react_agree_link\" = \"Terms of Service\";\n\"lng_paid_react_admin_cant\" = \"You can't send Stars to your own Live Story.\";\n\"lng_paid_react_toast#one\" = \"Star Sent!\";\n\"lng_paid_react_toast#other\" = \"Stars Sent!\";\n\"lng_paid_react_toast_anonymous#one\" = \"Star sent anonymously!\";\n\"lng_paid_react_toast_anonymous#other\" = \"Stars sent anonymously!\";\n\"lng_paid_react_toast_text#one\" = \"You reacted with **{count} Star**.\";\n\"lng_paid_react_toast_text#other\" = \"You reacted with **{count} Stars**.\";\n\"lng_paid_react_undo\" = \"Undo\";\n\"lng_paid_react_show_in_top\" = \"Show me in Top Senders\";\n\"lng_paid_react_anonymous\" = \"Anonymous\";\n\n\"lng_paid_comment_title\" = \"Highlight and Pin\";\n\"lng_paid_comment_about\" = \"Highlight and pin your message by adding Stars for {name}.\";\n\"lng_paid_comment_button\" = \"Add {stars}\";\n\"lng_paid_comment_pin_about\" = \"pin in chat\";\n\"lng_paid_comment_limit_about#one\" = \"character\";\n\"lng_paid_comment_limit_about#other\" = \"characters\";\n\"lng_paid_comment_emoji_about#one\" = \"emoji\";\n\"lng_paid_comment_emoji_about#other\" = \"emoji\";\n\"lng_paid_reaction_title\" = \"React with Stars\";\n\"lng_paid_reaction_about\" = \"Highlight and pin your message by sending Stars to {name}.\";\n\"lng_paid_reaction_button\" = \"Send {stars}\";\n\"lng_paid_admin_title\" = \"Receive Stars from Viewers\";\n\"lng_paid_admin_about\" = \"Viewers can send you Star Reactions.\";\n\n\"lng_sensitive_tag\" = \"18+\";\n\"lng_sensitive_title\" = \"18+\";\n\"lng_sensitive_text\" = \"This media may contain sensitive content suitable only for adults. Do you still want to view it?\";\n\"lng_sensitive_always\" = \"Always show 18+ media\";\n\"lng_sensitive_view\" = \"View Anyway\";\n\"lng_sensitive_toast\" = \"You can update the visibility of sensitive media in **Settings > Chat Settings > Sensitive content**\";\n\n\"lng_translate_show_original\" = \"Show Original\";\n\"lng_translate_return_original\" = \"View Original ({language})\";\n\"lng_translate_bar_to\" = \"Translate to {name}\";\n\"lng_translate_bar_to_other\" = \"Translate to {name}\";\n\"lng_translate_menu_to\" = \"Translate To\";\n\"lng_translate_menu_dont\" = \"Don't translate {name}\";\n\"lng_translate_menu_dont_other\" = \"Don't translate {name}\";\n\"lng_translate_menu_hide\" = \"Hide\";\n\"lng_translate_hidden_user\" = \"Translation bar hidden for this chat.\";\n\"lng_translate_hidden_group\" = \"Translation bar is now hidden for this group.\";\n\"lng_translate_hidden_channel\" = \"Translation bar is now hidden for this channel.\";\n\"lng_translate_dont_added\" = \"{name} added to the Do Not Translate list.\";\n\"lng_translate_settings\" = \"Settings\";\n\"lng_translate_undo\" = \"Undo\";\n\n\"lng_translate_cocoon_menu\" = \"Translations are powered by\\n**{emoji} Cocoon**. {link}\";\n\"lng_translate_cocoon_link\" = \"How does it work?\";\n\"lng_translate_cocoon_title\" = \"Cocoon\";\n\"lng_translate_cocoon_subtitle\" = \"**Cocoon** ({text}) handles AI tasks **safely** and **efficiently**.\";\n\"lng_translate_cocoon_explain\" = \"**Co**nfidential **Co**mpute **O**pen **N**etwork\";\n\"lng_translate_cocoon_private_title\" = \"Private\";\n\"lng_translate_cocoon_private_text\" = \"No third party can access any data, such as translations, inside {mention}.\";\n\"lng_translate_cocoon_private_mention\" = \"@cocoon\";\n\"lng_translate_cocoon_efficient_title\" = \"Efficient\";\n\"lng_translate_cocoon_efficient_text\" = \"Cocoon has allowed Telegram to reduce translation costs by 6x.\";\n\"lng_translate_cocoon_everyone_title\" = \"For Everyone\";\n\"lng_translate_cocoon_everyone_text\" = \"Any developer can use Cocoon for AI features. Learn more at {domain}.\";\n\"lng_translate_cocoon_everyone_domain\" = \"cocoon.org\";\n\"lng_translate_cocoon_text\" = \"Want to integrate Cocoon into your projects?\\nReach out at {link}\";\n\"lng_translate_cocoon_text_link\" = \"t.me/cocoon?direct\";\n\"lng_translate_cocoon_done\" = \"Understood\";\n\n\"lng_downloads_section\" = \"Downloads\";\n\"lng_downloads_view_in_chat\" = \"View in chat\";\n\"lng_downloads_view_in_section\" = \"View in downloads\";\n\"lng_downloads_delete_sure_one\" = \"Do you want to delete this file?\";\n\"lng_downloads_delete_sure_all\" = \"Do you want to delete all files?\";\n\"lng_downloads_delete_sure#one\" = \"Do you want to delete {count} file?\";\n\"lng_downloads_delete_sure#other\" = \"Do you want to delete {count} files?\";\n\"lng_downloads_delete_in_cloud_one\" = \"It will be deleted from your disk, but will remain accessible in the cloud.\";\n\"lng_downloads_delete_in_cloud\" = \"They will be deleted from your disk, but will remain accessible in the cloud.\";\n\n\"lng_send_image_empty\" = \"File: {name} is empty and can't be sent.\";\n\"lng_send_images_selected#one\" = \"{count} image selected\";\n\"lng_send_images_selected#other\" = \"{count} images selected\";\n\"lng_send_files_selected#one\" = \"{count} file selected\";\n\"lng_send_files_selected#other\" = \"{count} files selected\";\n\"lng_send_grouped\" = \"Group items\";\n\"lng_send_compressed_one\" = \"Compress the image\";\n\"lng_send_compressed\" = \"Compress images\";\n\"lng_send_high_quality\" = \"High Quality\";\n\"lng_send_as_documents_one\" = \"Send as a document\";\n\"lng_send_as_documents\" = \"Send as documents\";\n\"lng_send_media_invalid_files\" = \"Sorry, no valid files found.\";\n\"lng_send_image\" = \"Send an image\";\n\"lng_send_file\" = \"Send as a file\";\n\"lng_send_video\" = \"Send a video file\";\n\n\"lng_forward_choose\" = \"Forward to…\";\n\"lng_forward_cant\" = \"Sorry, no way to forward here :(\";\n\"lng_forward_share_contact\" = \"Share contact to {recipient}?\";\n\"lng_forward_share_cant\" = \"Sorry, no way to share contact here :(\";\n\n\"lng_forward_send_files_cant\" = \"Sorry, no way to send media here :(\";\n\"lng_forward_send\" = \"Send\";\n\"lng_forward_messages#one\" = \"{count} forwarded message\";\n\"lng_forward_messages#other\" = \"{count} forwarded messages\";\n\"lng_forwarding_from#one\" = \"{user} and {count} other\";\n\"lng_forwarding_from#other\" = \"{user} and {count} others\";\n\"lng_forwarding_from_two\" = \"{user} and {second_user}\";\n\"lng_inline_switch_choose\" = \"Choose conversation...\";\n\"lng_inline_switch_cant\" = \"Sorry, no way to write here :(\";\n\"lng_preview_reply_to\" = \"Reply to {name}\";\n\"lng_preview_reply_to_quote\" = \"Reply to quote from {name}\";\n\"lng_preview_reply_to_task\" = \"Reply to task from {title}\";\n\"lng_preview_reply_to_poll_option\" = \"Reply to poll option from {title}\";\n\n\"lng_suggest_bar_title\" = \"Suggest a Post Below\";\n\"lng_suggest_bar_text\" = \"Click to offer a price for publishing.\";\n\"lng_suggest_bar_priced\" = \"{amount} for publishing anytime.\";\n\"lng_suggest_bar_dated\" = \"Publish on {date}\";\n\"lng_suggest_options_title\" = \"Suggest a Message\";\n\"lng_suggest_options_change\" = \"Suggest Changes\";\n\"lng_suggest_options_stars_offer\" = \"Offer Stars\";\n\"lng_suggest_options_stars_request\" = \"Request Stars\";\n\"lng_suggest_options_stars_price\" = \"Enter Price in Stars\";\n\"lng_suggest_options_stars_price_about\" = \"Choose how many Stars to pay to publish this message.\";\n\"lng_suggest_options_ton_offer\" = \"Offer TON\";\n\"lng_suggest_options_ton_request\" = \"Request TON\";\n\"lng_suggest_options_ton_price\" = \"Enter Price in TON\";\n\"lng_suggest_options_ton_price_about\" = \"Choose how many TON to pay to publish this message.\";\n\"lng_suggest_options_date\" = \"Time\";\n\"lng_suggest_options_date_any\" = \"Anytime\";\n\"lng_suggest_options_date_publish\" = \"Publish\";\n\"lng_suggest_options_date_now\" = \"Publish Now\";\n\"lng_suggest_options_date_about\" = \"Select the date and time you want the message to be published. The post will remain available for at least 24 hours from this date.\";\n\"lng_suggest_options_you_get_stars#one\" = \"You will receive {count} Star ({percent}) for publishing this post.\";\n\"lng_suggest_options_you_get_stars#other\" = \"You will receive {count} Stars ({percent}) for publishing this post.\";\n\"lng_suggest_options_you_get_ton#one\" = \"You will receive {count} TON ({percent}) for publishing this post.\";\n\"lng_suggest_options_you_get_ton#other\" = \"You will receive {count} TON ({percent}) for publishing this post.\";\n\"lng_suggest_options_stars_warning\" = \"Transactions in **Stars** may be reversed by the payment provider within **21** days. Only accept Stars from people you trust.\";\n\"lng_suggest_options_offer\" = \"Offer {amount}\";\n\"lng_suggest_options_offer_free\" = \"Offer for Free\";\n\"lng_suggest_options_update\" = \"Update Terms\";\n\"lng_suggest_options_update_date\" = \"Update Time\";\n\n\"lng_suggest_action_decline\" = \"Decline\";\n\"lng_suggest_action_accept\" = \"Accept\";\n\"lng_suggest_action_change\" = \"Suggest Changes\";\n\"lng_suggest_action_your\" = \"You suggest to post this message.\";\n\"lng_suggest_action_his\" = \"{from} suggests to post this message.\";\n\"lng_suggest_action_price_label\" = \"Price\";\n\"lng_suggest_action_price_free\" = \"Free\";\n\"lng_suggest_action_time_label\" = \"Time\";\n\"lng_suggest_action_time_any\" = \"Anytime\";\n\"lng_suggest_action_agreement\" = \"Agreement reached!\";\n\"lng_suggest_action_agree_date\" = \"The post will be automatically published on {channel} {date}.\";\n\"lng_suggest_action_your_charged_stars#one\" = \"You have been charged **{count} Star**.\";\n\"lng_suggest_action_your_charged_stars#other\" = \"You have been charged **{count} Stars**.\";\n\"lng_suggest_action_your_charged_ton#one\" = \"You have been charged **{count} TON**.\";\n\"lng_suggest_action_your_charged_ton#other\" = \"You have been charged **{count} TON**.\";\n\"lng_suggest_action_his_charged_stars#one\" = \"{from} has been charged **{count} Star**.\";\n\"lng_suggest_action_his_charged_stars#other\" = \"{from} has been charged **{count} Stars**.\";\n\"lng_suggest_action_his_charged_ton#one\" = \"{from} has been charged **{count} TON**.\";\n\"lng_suggest_action_his_charged_ton#other\" = \"{from} has been charged **{count} TON**.\";\n\"lng_suggest_action_agree_receive_stars\" = \"{channel} will receive the Stars once the post has been live for 24 hours.\";\n\"lng_suggest_action_agree_receive_ton\" = \"{channel} will receive TON once the post has been live for 24 hours.\";\n\"lng_suggest_action_agree_removed_stars\" = \"If {channel} removes the post before it has been live for 24 hours, the Stars will be refunded.\";\n\"lng_suggest_action_agree_removed_ton\" = \"If {channel} removes the post before it has been live for 24 hours, TON will be refunded.\";\n\"lng_suggest_action_your_not_enough_stars\" = \"**Transaction failed** because you didn't have enough Stars.\";\n\"lng_suggest_action_your_not_enough_ton\" = \"**Transaction failed** because you didn't have enough TON.\";\n\"lng_suggest_action_his_not_enough_stars\" = \"**Transaction failed** because the user didn't have enough Stars.\";\n\"lng_suggest_action_his_not_enough_ton\" = \"**Transaction failed** because the user didn't have enough TON.\";\n\"lng_suggest_action_declined\" = \"{from} rejected the message.\";\n\"lng_suggest_action_declined_reason\" = \"{from} rejected the message with the comment.\";\n\"lng_suggest_change_price\" = \"{from} suggests a new price for the message.\";\n\"lng_suggest_change_time\" = \"{from} suggests a new time for the message.\";\n\"lng_suggest_change_price_time\" = \"{from} suggests a new price and time for the message.\";\n\"lng_suggest_change_content\" = \"{from} suggests changes for the message.\";\n\"lng_suggest_change_price_label\" = \"New Price\";\n\"lng_suggest_change_time_label\" = \"New Time\";\n\"lng_suggest_change_text_label\" = \"Check the suggested message below\";\n\"lng_suggest_menu_edit_message\" = \"Edit Message\";\n\"lng_suggest_menu_edit_price\" = \"Edit Price\";\n\"lng_suggest_menu_edit_time\" = \"Edit Time\";\n\"lng_suggest_decline_title\" = \"Decline\";\n\"lng_suggest_decline_text\" = \"Do you want to decline publishing this post from {from}?\";\n\"lng_suggest_decline_text_to\" = \"Do you want to decline publishing this post to {channel}?\";\n\"lng_suggest_decline_reason\" = \"Add a reason (optional)\";\n\"lng_suggest_accept_title\" = \"Accept Terms\";\n\"lng_suggest_accept_text\" = \"Do you want to publish this post from {from}?\";\n\"lng_suggest_accept_text_to\" = \"Do you want to publish this post to {channel}?\";\n\"lng_suggest_accept_receive_stars#one\" = \"{channel} will receive **{count} Star** ({percent}) for publishing {date}.\";\n\"lng_suggest_accept_receive_stars#other\" = \"{channel} will receive **{count} Stars** ({percent}) for publishing {date}.\";\n\"lng_suggest_accept_receive_ton#one\" = \"{channel} will receive **{count} TON** ({percent}) for publishing {date}.\";\n\"lng_suggest_accept_receive_ton#other\" = \"{channel} will receive **{count} TON** ({percent}) for publishing {date}.\";\n\"lng_suggest_accept_receive_now_stars#one\" = \"{channel} will receive **{count} Star** ({percent}) for publishing right now.\";\n\"lng_suggest_accept_receive_now_stars#other\" = \"{channel} will receive **{count} Stars** ({percent}) for publishing right now.\";\n\"lng_suggest_accept_receive_now_ton#one\" = \"{channel} will receive **{count} TON** ({percent}) for publishing right now.\";\n\"lng_suggest_accept_receive_now_ton#other\" = \"{channel} will receive **{count} TON** ({percent}) for publishing right now.\";\n\"lng_suggest_accept_receive_if\" = \"It must remain visible for at least **24** hours after publication.\";\n\"lng_suggest_accept_pay_stars#one\" = \"You will pay **{count} Star** for publishing {date}.\";\n\"lng_suggest_accept_pay_stars#other\" = \"You will pay **{count} Stars** for publishing {date}.\";\n\"lng_suggest_accept_pay_ton#one\" = \"You will pay **{count} TON** for publishing {date}.\";\n\"lng_suggest_accept_pay_ton#other\" = \"You will pay **{count} TON** for publishing {date}.\";\n\"lng_suggest_accept_pay_now_stars#one\" = \"You will pay **{count} Star** for publishing right now.\";\n\"lng_suggest_accept_pay_now_stars#other\" = \"You will pay **{count} Stars** for publishing right now.\";\n\"lng_suggest_accept_pay_now_ton#one\" = \"You will pay **{count} TON** for publishing right now.\";\n\"lng_suggest_accept_pay_now_ton#other\" = \"You will pay **{count} TON** for publishing right now.\";\n\"lng_suggest_accept_send\" = \"Publish\";\n\"lng_suggest_stars_amount#one\" = \"{count} Star\";\n\"lng_suggest_stars_amount#other\" = \"{count} Stars\";\n\"lng_suggest_ton_amount#one\" = \"{count} TON\";\n\"lng_suggest_ton_amount#other\" = \"{count} TON\";\n\"lng_suggest_warn_title_stars\" = \"Stars will be lost\";\n\"lng_suggest_warn_title_ton\" = \"TON will be lost\";\n\"lng_suggest_warn_text_stars\" = \"You won't receive **Stars** for the post if you delete it now. The post must remain visible for at least **24 hours** after it was published.\";\n\"lng_suggest_warn_text_ton\" = \"You won't receive **TON** for the post if you delete it now. The post must remain visible for at least **24 hours** after it was published.\";\n\"lng_suggest_warn_delete_anyway\" = \"Delete Anyway\";\n\"lng_suggest_low_ton_title\" = \"{amount} TON Needed\";\n\"lng_suggest_low_ton_text\" = \"You can add funds to your balance via the third-party platform Fragment.\";\n\"lng_suggest_low_ton_fragment\" = \"Add Funds via Fragment\";\n\"lng_suggest_low_ton_fragment_url\" = \"https://fragment.com/ads/topup\";\n\n\"lng_reply_in_another_title\" = \"Reply in...\";\n\"lng_reply_in_another_chat\" = \"Reply in Another Chat\";\n\"lng_reply_in_author\" = \"Message author\";\n\"lng_reply_in_chats_list\" = \"Your chats\";\n\"lng_reply_show_in_chat\" = \"Show in Chat\";\n\"lng_reply_remove\" = \"Do Not Reply\";\n\"lng_reply_about_quote\" = \"You can select a specific part to quote.\";\n\"lng_reply_options_header\" = \"Reply to Message\";\n\"lng_reply_options_quote\" = \"Update Quote\";\n\"lng_reply_header_short\" = \"Reply\";\n\"lng_reply_quote_selected\" = \"Quote Selected\";\n\"lng_reply_from_private_chat\" = \"This reply is from a private chat.\";\n\"lng_reply_quote_long_title\" = \"Quote too long!\";\n\"lng_reply_quote_long_text\" = \"The selected text is too long to quote.\";\n\"lng_link_options_header\" = \"Link Preview Settings\";\n\"lng_link_header_short\" = \"Link\";\n\"lng_link_move_up\" = \"Move Up\";\n\"lng_link_move_down\" = \"Move Down\";\n\"lng_link_shrink_photo\" = \"Shrink Photo\";\n\"lng_link_enlarge_photo\" = \"Enlarge Photo\";\n\"lng_link_shrink_video\" = \"Shrink Video\";\n\"lng_link_enlarge_video\" = \"Enlarge Video\";\n\"lng_link_remove\" = \"Do Not Preview\";\n\"lng_link_about_choose\" = \"Click on a link to generate its preview.\";\n\n\"lng_share_cant\" = \"Sorry, no way to share here :(\";\n\"lng_reply_cant\" = \"You can't reply to messages from before this group was upgraded to a supergroup.\\n\\nGroups get automatically upgraded when they reach 200 members or when you start using advanced features like chat history permissions or granular admin settings.\";\n\"lng_reply_cant_forward\" = \"Sorry, you can't reply to a message that was sent before the group was upgraded to a supergroup. Do you wish to forward it and add your comment?\";\n\n\"lng_share_title\" = \"Share to\";\n\"lng_share_at_time_title\" = \"Share at {time} to\";\n\"lng_share_copy_link\" = \"Copy share link\";\n\"lng_share_confirm\" = \"Send\";\n\"lng_share_wrong_user\" = \"This game was opened from a different user.\";\n\"lng_share_game_link_copied\" = \"Game link copied to clipboard.\";\n\"lng_share_done\" = \"Done!\";\n\"lng_share_message_to_saved_messages\" = \"Message forwarded to **Saved Messages**.\";\n\"lng_share_messages_to_saved_messages\" = \"Messages forwarded to **Saved Messages**.\";\n\"lng_share_message_to_chat\" = \"Message forwarded to **{chat}**.\";\n\"lng_share_messages_to_chat\" = \"Messages forwarded to **{chat}**.\";\n\"lng_share_message_to_two_chats\" = \"Message forwarded to **{user}** and **{chat}**.\";\n\"lng_share_messages_to_two_chats\" = \"Messages forwarded to **{user}** and **{chat}**.\";\n\"lng_share_message_to_many_chats#one\" = \"Message forwarded to **{count} chat**.\";\n\"lng_share_message_to_many_chats#other\" = \"Message forwarded to **{count} chats**.\";\n\"lng_share_messages_to_many_chats#one\" = \"Messages forwarded to **{count} chat**.\";\n\"lng_share_messages_to_many_chats#other\" = \"Messages forwarded to **{count} chats**.\";\n\"lng_share_as_copy\" = \"As copy\";\n\"lng_share_as_copy_no_text\" = \"No text\";\n\n\"lng_contact_phone\" = \"Phone Number\";\n\"lng_enter_contact_data\" = \"New Contact\";\n\"lng_contact_mobile_hidden\" = \"Mobile hidden\";\n\"lng_contact_phone_after\" = \"Phone number will be visible once {user} adds you as a contact.\";\n\"lng_contact_share_phone\" = \"Share my phone number\";\n\"lng_contact_phone_will_be_shared\" = \"You can make your phone visible to {user}.\";\n\"lng_contact_add_notes\" = \"Note\";\n\"lng_contact_add_notes_about\" = \"Notes are only visible to you.\";\n\"lng_contact_notes_limit_reached#one\" = \"You've reached the contact note limit. Please make the note shorter by {count} character.\";\n\"lng_contact_notes_limit_reached#other\" = \"You've reached the contact note limit. Please make the note shorter by {count} characters.\";\n\"lng_suggest_photo_for\" = \"Suggest Photo for {user}\";\n\"lng_suggest_birthday\" = \"Suggest Date of Birth\";\n\"lng_suggest_birthday_box_title\" = \"{user}'s Date of Birth\";\n\"lng_suggest_birthday_box_confirm\" = \"Suggest\";\n\"lng_set_photo_for_user\" = \"Set Photo for {user}\";\n\"lng_contact_photo_replace_info\" = \"You can replace {user}'s photo with another photo that only you will see.\";\n\"lng_edit_contact_title\" = \"Edit contact\";\n\"lng_edit_channel_title\" = \"Edit channel\";\n\"lng_edit_bot_title\" = \"Edit bot\";\n\"lng_edit_autotranslate\" = \"Auto-translate messages\";\n\"lng_edit_sign_messages\" = \"Sign messages\";\n\"lng_edit_sign_messages_about\" = \"Add names of admins to the messages they post.\";\n\"lng_edit_sign_profiles\" = \"Show authors' profiles\";\n\"lng_edit_sign_profiles_about\" = \"Add names and photos of admins to the messages they post, linking to their profiles.\";\n\"lng_edit_group\" = \"Edit group\";\n\"lng_edit_channel_color\" = \"Appearance\";\n\"lng_edit_channel_level_min\" = \"Level 1+\";\n\"lng_edit_channel_wallpaper\" = \"Channel wallpaper\";\n\"lng_edit_channel_wallpaper_about\" = \"Set a wallpaper that will be visible for everyone reading your channel.\";\n\"lng_edit_channel_wallpaper_group\" = \"Set Group Wallpaper\";\n\"lng_edit_channel_wallpaper_about_group\" = \"Set a wallpaper that will be visible for everyone viewing your group.\";\n\"lng_edit_channel_status\" = \"Channel emoji status\";\n\"lng_edit_channel_status_about\" = \"Choose a status that will be shown next to the channel's name.\";\n\"lng_edit_channel_status_group\" = \"Set Group Emoji Status\";\n\"lng_edit_channel_status_about_group\" = \"Choose a status that will be shown next to the group's name.\";\n\"lng_edit_channel_personal_channel\" = \"Set as Personal Channel\";\n\"lng_edit_self_title\" = \"Edit your name\";\n\"lng_confirm_contact_data\" = \"New Contact\";\n\"lng_add_contact\" = \"Create\";\n\"lng_add_contact_button\" = \"New contact\";\n\"lng_contacts_header\" = \"Contacts\";\n\"lng_menu_not_contact\" = \"This number is not on Telegram\";\n\"lng_contacts_hidden_stories\" = \"Hidden Stories\";\n\"lng_contacts_stories_status#one\" = \"{count} story\";\n\"lng_contacts_stories_status#other\" = \"{count} stories\";\n\"lng_contacts_stories_status_new#one\" = \"{count} new story\";\n\"lng_contacts_stories_status_new#other\" = \"{count} new stories\";\n\"lng_contact_not_joined\" = \"Unfortunately {name} has not joined Telegram yet, but you can send them an invitation.\\n\\nWe will notify you when any of your contacts join Telegram.\";\n\"lng_try_other_contact\" = \"Try someone else\";\n\"lng_create_group_link\" = \"Link\";\n\"lng_create_group_invite_link\" = \"Invite link\";\n\"lng_create_group_description\" = \"Description (optional)\";\n\n\"lng_drag_images_here\" = \"Drop images here\";\n\"lng_drag_photos_here\" = \"Drop photos here\";\n\"lng_drag_files_here\" = \"Drop files here\";\n\"lng_drag_media_here\" = \"Drop photos and videos\";\n\n\"lng_drag_to_send_quick\" = \"to send them in a quick way\";\n\"lng_drag_to_send_no_compression\" = \"to send them without compression\";\n\"lng_drag_to_send_files\" = \"to send them as documents\";\n\"lng_drag_to_send_media\" = \"to send them as media files\";\n\n\"lng_selected_clear\" = \"Cancel\";\n\"lng_selected_delete\" = \"Delete\";\n\"lng_selected_forward\" = \"Forward\";\n\"lng_selected_send_now\" = \"Send now\";\n\"lng_selected_cancel_sure_this\" = \"Cancel uploading?\";\n\"lng_selected_upload_stop\" = \"Stop\";\n\"lng_selected_delete_sure_this\" = \"Do you want to delete this message?\";\n\"lng_selected_delete_sure#one\" = \"Do you want to delete {count} message?\";\n\"lng_selected_delete_sure#other\" = \"Do you want to delete {count} messages?\";\n\"lng_selected_remove_saved_music\" = \"Do you want to remove this file from your profile?\";\n\"lng_saved_music_added\" = \"Audio added to your Profile.\";\n\"lng_saved_music_removed\" = \"Audio removed from your Profile.\";\n\"lng_delete_photo_sure\" = \"Do you want to delete this photo?\";\n\"lng_delete_for_everyone_hint#one\" = \"This will delete it for everyone in this chat.\";\n\"lng_delete_for_everyone_hint#other\" = \"This will delete them for everyone in this chat.\";\n\"lng_delete_for_me_chat_hint#one\" = \"This will delete it just for you, not for other participants of the chat.\";\n\"lng_delete_for_me_chat_hint#other\" = \"This will delete them just for you, not for other participants of the chat.\";\n\"lng_delete_for_me_hint#one\" = \"This will delete it just for you.\";\n\"lng_delete_for_me_hint#other\" = \"This will delete them just for you.\";\n\"lng_delete_clear_for_me\" = \"This will clear history just for you, not for other participants of the chat.\";\n\"lng_edit_auto_delete_settings\" = \"Edit Auto-Delete Settings\";\n\"lng_enable_auto_delete\" = \"Enable Auto-Delete\";\n\"lng_selected_unsend_about_user_one\" = \"You can also delete the message you sent from {user}'s inbox by checking \\\"Unsend my messages\\\".\";\n\"lng_selected_unsend_about_user#one\" = \"You can also delete the {count} message you sent from {user}'s inbox by checking \\\"Unsend my messages\\\".\";\n\"lng_selected_unsend_about_user#other\" = \"You can also delete the {count} messages you sent from {user}'s inbox by checking \\\"Unsend my messages\\\".\";\n\"lng_selected_unsend_about_group_one\" = \"You can also delete the message you sent from the inboxes of other group members by checking \\\"Unsend my messages\\\".\";\n\"lng_selected_unsend_about_group#one\" = \"You can also delete the {count} message you sent from the inboxes of other group members by checking \\\"Unsend my messages\\\".\";\n\"lng_selected_unsend_about_group#other\" = \"You can also delete the {count} messages you sent from the inboxes of other group members by checking \\\"Unsend my messages\\\".\";\n\"lng_delete_for_everyone_check\" = \"Delete for everyone\";\n\"lng_delete_for_other_check\" = \"Also delete for {user}\";\n\"lng_delete_for_other_my\" = \"Unsend my messages\";\n\"lng_box_delete\" = \"Delete\";\n\"lng_box_leave\" = \"Leave\";\n\n\"lng_upload_sure_stop\" = \"Are you sure you want to stop uploading your files?\\n\\nIf you do, you'll need to start over.\";\n\"lng_download_sure_stop\" = \"Are you sure you want to stop downloading your files?\\n\\nIf you do, you'll need to start over.\";\n\"lng_upload_show_file\" = \"Show file\";\n\n\"lng_about_version\" = \"version {version}\";\n\"lng_about_text1\" = \"Unofficial free messaging app based on {api_link}\\nfor speed and security.\";\n\"lng_about_text1_api\" = \"Telegram API\";\n\"lng_about_text2\" = \"This software is licensed under {gpl_link} version 3.\\nSource code is available on {github_link}.\";\n\"lng_about_text3\" = \"Visit the {faq_link} for more info.\";\n\"lng_about_text3_faq\" = \"Telegram FAQ\";\n\"lng_about_done\" = \"Done\";\n\n\"lng_search_no_results\" = \"No messages found\";\n\"lng_search_found_results#one\" = \"Found {count} message\";\n\"lng_search_found_results#other\" = \"Found {count} messages\";\n\"lng_search_global_results\" = \"Global search results\";\n\"lng_search_messages_from\" = \"Show messages from\";\n\"lng_search_messages_n_of_amount\" = \"{n} of {amount}\";\n\"lng_search_messages_none\" = \"No results\";\n\"lng_search_filter_all\" = \"All chats\";\n\"lng_search_filter_private\" = \"Private chats\";\n\"lng_search_filter_group\" = \"Group chats\";\n\"lng_search_filter_channel\" = \"Channels\";\n\"lng_search_sponsored_button\" = \"Ad ⋮\";\n\n\"lng_media_save_progress\" = \"{ready} of {total} {mb}\";\n\"lng_mediaview_save_as\" = \"Save As...\";\n\"lng_mediaview_copy\" = \"Copy\";\n\"lng_mediaview_copy_frame\" = \"Copy Frame\";\n\"lng_mediaview_forward\" = \"Forward\";\n\"lng_mediaview_share_at_time\" = \"Share at {time}\";\n\"lng_mediaview_delete\" = \"Delete\";\n\"lng_mediaview_save_to_profile\" = \"Post to Profile\";\n\"lng_mediaview_pin_story_done\" = \"Story pinned\";\n\"lng_mediaview_pin_story_about\" = \"Now it will be always shown on the top.\";\n\"lng_mediaview_pin_stories_done#one\" = \"{count} story pinned\";\n\"lng_mediaview_pin_stories_done#other\" = \"{count} stories pinned\";\n\"lng_mediaview_pin_stories_about#one\" = \"Now it will be always shown on the top.\";\n\"lng_mediaview_pin_stories_about#other\" = \"Now they will be always shown on the top.\";\n\"lng_mediaview_unpin_story_done\" = \"Story unpinned.\";\n\"lng_mediaview_unpin_stories_done#one\" = \"{count} story unpinned\";\n\"lng_mediaview_unpin_stories_done#other\" = \"{count} stories unpinned\";\n\"lng_mediaview_pin_limit#one\" = \"You can't pin more than {count} story.\";\n\"lng_mediaview_pin_limit#other\" = \"You can't pin more than {count} stories.\";\n\"lng_mediaview_archive_story\" = \"Archive Story\";\n\"lng_mediaview_photos_all\" = \"View all photos\";\n\"lng_mediaview_files_all\" = \"View all files\";\n\"lng_mediaview_single_photo\" = \"Single Photo\";\n\"lng_mediaview_group_photo\" = \"Group Photo\";\n\"lng_mediaview_channel_photo\" = \"Channel Photo\";\n\"lng_mediaview_profile_photo\" = \"Profile Photo\";\n\"lng_mediaview_profile_public_photo\" = \"Public Photo\";\n\"lng_mediaview_profile_photo_by_you\" = \"Photo set by you\";\n\"lng_mediaview_file_n_of_amount\" = \"{file} {n} of {amount}\";\n\"lng_mediaview_n_of_amount\" = \"Photo {n} of {amount}\";\n\"lng_mediaview_doc_image\" = \"File\";\n\"lng_mediaview_today\" = \"today at {time}\";\n\"lng_mediaview_yesterday\" = \"yesterday at {time}\";\n\"lng_mediaview_just_now\" = \"just now\";\n\"lng_mediaview_minutes_ago#one\" = \"{count} minute ago\";\n\"lng_mediaview_minutes_ago#other\" = \"{count} minutes ago\";\n\"lng_mediaview_hours_ago#one\" = \"{count} hour ago\";\n\"lng_mediaview_hours_ago#other\" = \"{count} hours ago\";\n\"lng_mediaview_date_time\" = \"{date} at {time}\";\n\"lng_mediaview_set_userpic\" = \"Set as Main\";\n\"lng_mediaview_report_profile_photo\" = \"Report\";\n\n\"lng_mediaview_title\" = \"Media viewer\";\n\"lng_mediaview_saved_to\" = \"Image was saved to your {downloads} folder\";\n\"lng_mediaview_saved_images_to\" = \"Images were saved to your {downloads} folder\";\n\"lng_mediaview_video_saved_to\" = \"Video file was saved to your {downloads} folder\";\n\"lng_mediaview_downloads\" = \"Downloads\";\n\"lng_mediaview_playback_speed\" = \"Playback speed: {speed}\";\n\"lng_mediaview_rotate_video\" = \"Rotate video\";\n\"lng_mediaview_quality_auto\" = \"Auto\";\n\n\"lng_theme_preview_title\" = \"Theme Preview\";\n\"lng_theme_preview_generating\" = \"Generating color theme preview...\";\n\"lng_theme_preview_invalid\" = \"Invalid data in this theme file.\";\n\"lng_theme_preview_apply\" = \"Apply this theme\";\n\"lng_theme_preview_users#one\" = \"{count} person is using this theme\";\n\"lng_theme_preview_users#other\" = \"{count} people are using this theme\";\n\n\"lng_new_version_wrap\" = \"Telegram Desktop was updated to version {version}\\n\\n{changes}\\n\\nFull version history is available here:\\n{link}\";\n\"lng_new_version_minor\" = \"— Bug fixes and other minor improvements\";\n\n\"lng_menu_insert_unicode\" = \"Insert Unicode control character\";\n\"lng_menu_formatting\" = \"Formatting\";\n\"lng_menu_formatting_bold\" = \"Bold\";\n\"lng_menu_formatting_italic\" = \"Italic\";\n\"lng_menu_formatting_underline\" = \"Underline\";\n\"lng_menu_formatting_strike_out\" = \"Strikethrough\";\n\"lng_menu_formatting_blockquote\" = \"Quote\";\n\"lng_menu_formatting_monospace\" = \"Monospace\";\n\"lng_menu_formatting_spoiler\" = \"Spoiler\";\n\"lng_menu_formatting_date\" = \"Date\";\n\"lng_menu_formatting_link_create\" = \"Create link\";\n\"lng_menu_formatting_link_edit\" = \"Edit link\";\n\"lng_menu_formatting_clear\" = \"Clear formatting\";\n\"lng_formatting_date_title\" = \"Choose date and time\";\n\"lng_formatting_link_create_title\" = \"Create link\";\n\"lng_formatting_link_edit_title\" = \"Edit link\";\n\"lng_formatting_link_text\" = \"Text\";\n\"lng_formatting_link_url\" = \"URL\";\n\"lng_formatting_link_create\" = \"Create\";\n\"lng_formatting_code_title\" = \"Code Language\";\n\"lng_formatting_code_language\" = \"Language for syntax highlighting.\";\n\"lng_formatting_code_auto\" = \"Auto-Detect\";\n\n\"lng_text_copied\" = \"Text copied to clipboard.\";\n\"lng_code_copied\" = \"Code copied to clipboard.\";\n\"lng_date_copied\" = \"Date copied to clipboard.\";\n\n\"lng_date_relative_now\" = \"now\";\n\n\"lng_date_relative_seconds_ago#one\" = \"{count} second ago\";\n\"lng_date_relative_seconds_ago#other\" = \"{count} seconds ago\";\n\"lng_date_relative_minutes_ago#one\" = \"{count} minute ago\";\n\"lng_date_relative_minutes_ago#other\" = \"{count} minutes ago\";\n\"lng_date_relative_hours_ago#one\" = \"{count} hour ago\";\n\"lng_date_relative_hours_ago#other\" = \"{count} hours ago\";\n\"lng_date_relative_days_ago#one\" = \"{count} day ago\";\n\"lng_date_relative_days_ago#other\" = \"{count} days ago\";\n\"lng_date_relative_months_ago#one\" = \"{count} month ago\";\n\"lng_date_relative_months_ago#other\" = \"{count} months ago\";\n\"lng_date_relative_years_ago#one\" = \"{count} year ago\";\n\"lng_date_relative_years_ago#other\" = \"{count} years ago\";\n\n\"lng_date_relative_in_seconds#one\" = \"in {count} second\";\n\"lng_date_relative_in_seconds#other\" = \"in {count} seconds\";\n\"lng_date_relative_in_minutes#one\" = \"in {count} minute\";\n\"lng_date_relative_in_minutes#other\" = \"in {count} minutes\";\n\"lng_date_relative_in_hours#one\" = \"in {count} hour\";\n\"lng_date_relative_in_hours#other\" = \"in {count} hours\";\n\"lng_date_relative_in_days#one\" = \"in {count} day\";\n\"lng_date_relative_in_days#other\" = \"in {count} days\";\n\"lng_date_relative_in_months#one\" = \"in {count} month\";\n\"lng_date_relative_in_months#other\" = \"in {count} months\";\n\"lng_date_relative_in_years#one\" = \"in {count} year\";\n\"lng_date_relative_in_years#other\" = \"in {count} years\";\n\n\"lng_context_copy_date\" = \"Copy Date\";\n\"lng_context_add_to_calendar\" = \"Add to Calendar\";\n\"lng_context_set_reminder\" = \"Set a Reminder\";\n\"lng_reminder_scheduled_in\" = \"Reminder scheduled in {link}.\";\n\n\"lng_spellchecker_submenu\" = \"Spelling\";\n\"lng_spellchecker_add\" = \"Add to Dictionary\";\n\"lng_spellchecker_remove\" = \"Remove from Dictionary\";\n\"lng_spellchecker_ignore\" = \"Ignore word\";\n\n\"lng_full_name\" = \"{first_name} {last_name}\";\n\n\"lng_confirm_phone_link_invalid\" = \"This link is broken or expired.\";\n\"lng_confirm_phone_title\" = \"Cancel account reset\";\n\"lng_confirm_phone_about\" = \"Somebody with access to your phone number {phone} has requested to delete your Telegram account and reset your 2-Step Verification password.\\n\\nIf this wasn't you, please enter the code we've just sent you via SMS to your number. You can also cancel this by changing your phone number to a number you control.\";\n\"lng_confirm_phone_success\" = \"Success!\\n\\nThe deletion process was canceled for your account {phone}. You may close this window now.\";\n\"lng_confirm_phone_send\" = \"Send\";\n\"lng_confirm_phone_enter_code\" = \"Please enter the code.\";\n\n\"lng_theme_editor_no_keys\" = \"No keys in the palette yet\";\n\"lng_theme_editor_cant_change_theme\" = \"You can't apply a new theme while editing the color palette. Please close the theme editor first.\";\n\"lng_theme_editor_new_keys\" = \"Not in the palette yet\";\n\"lng_theme_editor_background_image\" = \"Background Image\";\n\"lng_theme_editor_saved_to_jpg\" = \"Saved to JPEG, {size}\";\n\"lng_theme_editor_read_from_jpg\" = \"JPEG image, {size}\";\n\"lng_theme_editor_read_from_png\" = \"PNG image, {size}\";\n\"lng_theme_editor_choose_image\" = \"Choose Background Image\";\n\"lng_theme_editor_choose_name\" = \"Save Theme File\";\n\"lng_theme_editor_error\" = \"The editor encountered an error :( See 'log.txt' for details.\";\n\"lng_theme_editor_sure_close\" = \"Are you sure you want to close the editor? Your changes won't be saved.\";\n\"lng_theme_editor_need_auth\" = \"You need to log in to save your theme.\";\n\"lng_theme_editor_need_unlock\" = \"You need to unlock Telegram to save your theme.\";\n\"lng_theme_editor_done\" = \"Theme exported successfully!\";\n\"lng_theme_editor_title\" = \"Edit color palette\";\n\"lng_theme_editor_save_button\" = \"Save theme\";\n\n\"lng_theme_editor_create_title\" = \"Create theme\";\n\"lng_theme_editor_create\" = \"Create\";\n\"lng_theme_editor_name\" = \"Theme name\";\n\"lng_theme_editor_create_description\" = \"Your new theme will be based on your currently selected colors and wallpaper. Alternatively, you can import an existing theme or color palette from a file.\";\n\"lng_theme_editor_attach_description\" = \"You can create desktop part of your theme based on your currently selected colors and wallpaper. Alternatively, you can import existing theme or color palette from file.\";\n\"lng_theme_editor_import_existing\" = \"Import existing theme\";\n\"lng_theme_editor_save_title\" = \"Save theme\";\n\"lng_theme_editor_link_about\" = \"Your theme will be updated for all users each time you change it. Anyone can install it using this link.\\n\\nTheme links must be longer than 5 characters and use a-z, 0-9 and underscores.\";\n\n\"lng_theme_editor_menu_export\" = \"Export theme\";\n\"lng_theme_editor_menu_import\" = \"Import theme\";\n\"lng_theme_editor_menu_show\" = \"Show palette file\";\n\n\"lng_payments_webview_no_use\" = \"Unfortunately, you can't use payments with current system configuration.\";\n\"lng_payments_webview_install_edge\" = \"Please install {link}.\";\n\"lng_payments_webview_install_webkit\" = \"Please install WebKitGTK (webkit2gtk-4.1/webkit2gtk-4.0) using your package manager.\";\n\"lng_payments_webview_update_windows\" = \"Please update your system to Windows 8.1 or later.\";\n\"lng_payments_sure_close\" = \"Are you sure you want to close this payment form? The changes you made will be lost.\";\n\"lng_payments_receipt_label\" = \"Receipt\";\n\"lng_payments_receipt_label_test\" = \"Test receipt\";\n\"lng_payments_invoice_label\" = \"Invoice\";\n\"lng_payments_invoice_label_test\" = \"Test invoice\";\n\"lng_payments_receipt_button\" = \"Receipt\";\n\"lng_payments_success\" = \"You paid {amount} for {title}.\";\n\n\"lng_payments_checkout_title\" = \"Checkout\";\n\"lng_payments_receipt_title\" = \"Receipt\";\n\"lng_payments_total_label\" = \"Total\";\n\"lng_payments_date_label\" = \"Paid\";\n\"lng_payments_pay_amount\" = \"Pay {amount}\";\n\"lng_payments_payment_method\" = \"Payment Method\";\n\"lng_payments_new_card\" = \"New Card...\";\n\"lng_payments_shipping_address\" = \"Shipping Address\";\n\"lng_payments_address_street1\" = \"Address 1\";\n\"lng_payments_address_street2\" = \"Address 2\";\n\"lng_payments_address_city\" = \"City\";\n\"lng_payments_address_state\" = \"State\";\n\"lng_payments_address_country\" = \"Country\";\n\"lng_payments_address_postcode\" = \"Postcode\";\n\n\"lng_payments_shipping_method\" = \"Shipping Method\";\n\"lng_payments_info_name\" = \"Name\";\n\"lng_payments_info_email\" = \"Email\";\n\"lng_payments_info_phone\" = \"Phone\";\n\"lng_payments_to_provider_phone_email\" = \"Phone and Email will be passed to {provider} as billing info.\";\n\"lng_payments_to_provider_email\" = \"Email will be passed to {provider} as billing info.\";\n\"lng_payments_to_provider_phone\" = \"Phone will be passed to {provider} as billing info.\";\n\"lng_payments_processed_by\" = \"Processed by {provider}\";\n\"lng_payments_warning_title\" = \"Warning\";\n\"lng_payments_warning_body\" = \"Neither Telegram, nor {bot1} will have access to your credit card information. Credit card details will be handled only by the payment system, {provider}.\\n\\nPayments will go directly to the developer of {bot2}. Telegram cannot provide any guarantees, so proceed at your own risk. In case of problems, please contact the developer of {bot3} or your bank.\";\n\"lng_payments_shipping_address_title\" = \"Shipping Information\";\n\"lng_payments_card_title\" = \"New Card\";\n\"lng_payments_card_number\" = \"Card Number\";\n\"lng_payments_card_cvc\" = \"CVC\";\n\"lng_payments_card_expire_date\" = \"MM / YY\";\n\"lng_payments_card_holder\" = \"Cardholder name\";\n\"lng_payments_billing_address\" = \"Billing Information\";\n\"lng_payments_billing_country\" = \"Country\";\n\"lng_payments_billing_zip_code\" = \"Zip Code\";\n\"lng_payments_save_information\" = \"Save Information for future use\";\n\"lng_payments_need_password\" = \"You can save your payment information for future use. Please turn on Two-Step Verification to enable this.\";\n\"lng_payments_password_title\" = \"Payment Confirmation\";\n\"lng_payments_password_description\" = \"You saved the payment method {card}. To use it for payment, please enter your 2-Step-Verification password.\";\n\"lng_payments_password_submit\" = \"Pay\";\n\"lng_payments_tips_label\" = \"Tip (Optional)\";\n\"lng_payments_tips_box_title\" = \"Add Tip\";\n\"lng_payments_tips_max\" = \"Max possible tip amount: {amount}\";\n\n\"lng_payments_shipping_not_available\" = \"Shipping to the selected country is not available.\";\n\"lng_payments_card_declined\" = \"Your card was declined.\";\n\"lng_payments_payment_failed\" = \"Payment failed. Your card has not been billed.\";\n\"lng_payments_precheckout_failed\" = \"The bot couldn't process your payment. Your card has not been billed.\";\n\"lng_payments_precheckout_timeout\" = \"The bot didn't respond in time. Your card has not been billed.\";\n\"lng_payments_precheckout_stars_failed\" = \"The bot couldn't process your payment.\";\n\"lng_payments_precheckout_stars_timeout\" = \"The bot didn't respond in time.\";\n\"lng_payments_already_paid\" = \"You have already paid for this item.\";\n\n\"lng_payments_terms_title\" = \"Terms of Service\";\n\"lng_payments_terms_text\" = \"Subscribe and accept Terms of Service of {bot}?\";\n\"lng_payments_terms_text_once\" = \"Do you accept the Terms of Service of {bot}?\";\n\"lng_payments_terms_agree\" = \"I agree to {link}\";\n\"lng_payments_terms_link\" = \"Terms of Service\";\n\"lng_payments_terms_accept\" = \"Accept\";\n\n\"lng_call_status_incoming\" = \"is calling you...\";\n\"lng_call_status_connecting\" = \"connecting...\";\n\"lng_call_status_exchanging\" = \"exchanging encryption keys...\";\n\"lng_call_status_waiting\" = \"waiting...\";\n\"lng_call_status_requesting\" = \"requesting...\";\n\"lng_call_status_hanging\" = \"hanging up...\";\n\"lng_call_status_ended\" = \"call ended\";\n\"lng_call_status_failed\" = \"failed to connect\";\n\"lng_call_status_ringing\" = \"ringing...\";\n\"lng_call_status_busy\" = \"line busy\";\n\"lng_call_status_group_invite\" = \"Telegram Group Call\";\n\"lng_call_status_sure\" = \"Click on the Camera icon if you want to start a video call.\";\n\"lng_call_fingerprint_tooltip\" = \"If the emoji on {user}'s screen are the same, this call is 100% secure\";\n\n\"lng_call_error_not_available\" = \"Sorry, you cannot call {user} because of their privacy settings. You can ask them to modify their setting or to call you instead.\";\n\"lng_call_error_outdated\" = \"{user}'s app does not support calls. They need to update their app before you can call them.\";\n\"lng_call_error_no_camera\" = \"No camera could be found. Please make sure that your camera is connected to the computer.\";\n\"lng_call_error_camera_not_started\" = \"You can switch to video call once you're connected.\";\n\"lng_call_error_camera_outdated\" = \"{user}'s app does not support video calls. They need to update their app before you can call them.\";\n\"lng_call_error_audio_io\" = \"There seems to be a problem with your sound card. Please make sure that your computer's speakers and microphone are working and try again.\";\n\"lng_call_error_add_not_started\" = \"You can add more people once you're connected.\";\n\n\"lng_call_bar_hangup\" = \"End call\";\n\"lng_call_leave_to_other_sure\" = \"End your active call and join this video chat?\";\n\"lng_call_leave_to_other_sure_channel\" = \"Do you want to end your active call and join a live stream in this channel?\";\n\n\"lng_call_box_title\" = \"Calls\";\n\"lng_call_box_about\" = \"You haven't made any Telegram calls yet.\";\n\"lng_call_box_status_today\" = \"{time}\";\n\"lng_call_box_status_yesterday\" = \"yesterday at {time}\";\n\"lng_call_box_status_date\" = \"{date} at {time}\";\n\"lng_call_box_status_group\" = \"({amount}) {status}\";\n\"lng_call_box_clear_all\" = \"Clear All\";\n\"lng_call_box_clear_sure\" = \"Are you sure you want to completely clear your calls log?\";\n\"lng_call_box_clear_button\" = \"Clear\";\n\"lng_call_box_groupcalls_subtitle\" = \"Active video chats\";\n\n\"lng_call_outgoing\" = \"Outgoing call\";\n\"lng_call_video_outgoing\" = \"Outgoing video call\";\n\"lng_call_group_outgoing\" = \"Outgoing group call\";\n\"lng_call_incoming\" = \"Incoming call\";\n\"lng_call_video_incoming\" = \"Incoming video call\";\n\"lng_call_group_incoming\" = \"Incoming group call\";\n\"lng_call_missed\" = \"Missed call\";\n\"lng_call_video_missed\" = \"Missed video call\";\n\"lng_call_group_missed\" = \"Missed group call\";\n\"lng_call_cancelled\" = \"Canceled call\";\n\"lng_call_video_cancelled\" = \"Canceled video call\";\n\"lng_call_declined\" = \"Declined call\";\n\"lng_call_video_declined\" = \"Declined video call\";\n\"lng_call_group_declined\" = \"Declined group call\";\n\"lng_call_duration_info\" = \"{time}, {duration}\";\n\"lng_call_type_and_duration\" = \"{type} ({duration})\";\n\"lng_call_invitation\" = \"Group call invitation\";\n\"lng_call_ongoing\" = \"Ongoing group call\";\n\n\"lng_call_rate_label\" = \"Please rate the quality of your call\";\n\"lng_call_rate_comment\" = \"Comment (optional)\";\n\n\"lng_call_start\" = \"Start Call\";\n\"lng_call_start_video\" = \"Start Video\";\n\"lng_call_stop_video\" = \"Stop Video\";\n\"lng_call_screencast\" = \"Screencast\";\n\"lng_call_add_people\" = \"Add People\";\n\"lng_call_end_call\" = \"End Call\";\n\"lng_call_mute_audio\" = \"Mute\";\n\"lng_call_unmute_audio\" = \"Unmute\";\n\"lng_call_accept\" = \"Accept\";\n\"lng_call_decline\" = \"Decline\";\n\"lng_call_redial\" = \"Redial\";\n\"lng_call_cancel\" = \"Cancel\";\n\n\"lng_call_microphone_off\" = \"{user}'s microphone is off\";\n\"lng_call_battery_level_low\" = \"{user}'s battery level is low\";\n\n\"lng_group_call_title\" = \"Video Chat\";\n\"lng_group_call_title_channel\" = \"Live Stream\";\n\"lng_group_call_active\" = \"speaking\";\n\"lng_group_call_inactive\" = \"listening\";\n\"lng_group_call_raised_hand_status\" = \"wants to speak\";\n\"lng_group_call_settings\" = \"Settings\";\n\"lng_group_call_video\" = \"Video\";\n\"lng_group_call_message\" = \"Message\";\n\"lng_group_call_screen_share_start\" = \"Share Screen\";\n\"lng_group_call_screen_share_stop\" = \"Stop Sharing\";\n\"lng_group_call_screen_title\" = \"Screen {index}\";\n\"lng_group_call_screen_share_audio\" = \"Share System Audio\";\n\"lng_group_call_sharing_screen_options\" = \"Sharing Options\";\n\"lng_group_call_choose_source\" = \"Choose Source\";\n\"lng_group_call_unmute\" = \"Unmute\";\n\"lng_group_call_unmute_sub\" = \"Hold space bar to temporarily unmute.\";\n\"lng_group_call_you_are_live\" = \"You are Live\";\n\"lng_group_call_force_muted\" = \"Muted by admin\";\n\"lng_group_call_force_muted_sub\" = \"You are in Listen Only mode\";\n\"lng_group_call_raised_hand\" = \"You asked to speak\";\n\"lng_group_call_connecting\" = \"Connecting...\";\n\"lng_group_call_leave\" = \"Leave\";\n\"lng_group_call_leave_title\" = \"Leave video chat\";\n\"lng_group_call_leave_title_call\" = \"Leave group call\";\n\"lng_group_call_leave_title_channel\" = \"Leave live stream\";\n\"lng_group_call_leave_sure\" = \"Do you want to leave this video chat?\";\n\"lng_group_call_leave_sure_call\" = \"Do you want to leave this group call?\";\n\"lng_group_call_leave_sure_channel\" = \"Are you sure you want to leave this live stream?\";\n\"lng_group_call_close\" = \"Close\";\n\"lng_group_call_close_sure\" = \"Video chat is scheduled. You can abort it or just close this panel.\";\n\"lng_group_call_close_sure_channel\" = \"Live stream is scheduled. You can abort it or just close this panel.\";\n\"lng_group_call_also_cancel\" = \"Abort video chat\";\n\"lng_group_call_also_cancel_channel\" = \"Abort live stream\";\n\"lng_group_call_leave_to_other_sure\" = \"Leave your currently active video chat and join this video chat?\";\n\"lng_group_call_leave_to_other_sure_channel\" = \"Do you want to leave your active video chat and join a live stream in this channel?\";\n\"lng_group_call_leave_channel_to_other_sure\" = \"Do you want to leave your active live stream and join a video chat in this group?\";\n\"lng_group_call_leave_channel_to_other_sure_channel\" = \"Do you want to leave your active live stream and join a live stream in this channel?\";\n\"lng_group_call_create_sure\" = \"Start a video chat in this group?\";\n\"lng_group_call_create_sure_channel\" = \"Are you sure you want to start a live stream in this channel as your personal account?\";\n\"lng_group_call_join_sure_personal\" = \"Are you sure you want to join this video chat as your personal account?\";\n\"lng_group_call_muted_no_camera\" = \"You can't turn on video while you're muted by admin.\";\n\"lng_group_call_muted_no_screen\" = \"You can't share your screen while you're muted by admin.\";\n\"lng_group_call_chat_no_camera\" = \"You can't turn on video in this chat.\";\n\"lng_group_call_chat_no_screen\" = \"You can't share your screen in this chat.\";\n\"lng_group_call_failed_screen\" = \"An error occurred. Screencast has stopped.\";\n\"lng_group_call_failed_camera\" = \"Couldn't enable camera. Another app may be already using the camera. Try closing other apps first.\";\n\"lng_group_call_tooltip_screen\" = \"Share screen\";\n\"lng_group_call_tooltip_camera\" = \"Your camera is off. Click here to enable camera.\";\n\"lng_group_call_tooltip_microphone\" = \"You are on mute. Click here to speak.\";\n\"lng_group_call_tooltip_camera_off\" = \"Disable camera\";\n\"lng_group_call_tooltip_force_muted\" = \"Muted by admin. Click if you want to speak.\";\n\"lng_group_call_tooltip_raised_hand\" = \"You asked to speak. We let the speakers know.\";\n\"lng_group_call_also_end\" = \"End video chat\";\n\"lng_group_call_also_end_channel\" = \"End live stream\";\n\"lng_group_call_settings_title\" = \"Settings\";\n\"lng_group_call_invite\" = \"Invite Members\";\n\"lng_group_call_invite_conf\" = \"Add People\";\n\"lng_group_call_invited_status\" = \"invited\";\n\"lng_group_call_calling_status\" = \"calling...\";\n\"lng_group_call_blockchain_only_status\" = \"listening\";\n\"lng_group_call_muted_by_me_status\" = \"muted for you\";\n\"lng_group_call_invite_title\" = \"Invite members\";\n\"lng_group_call_invite_button\" = \"Invite\";\n\"lng_group_call_confcall_add\" = \"Call\";\n\"lng_group_call_add_to_group_one\" = \"{user} isn't a member of «{group}». Add them to the group?\";\n\"lng_group_call_add_to_group_some\" = \"Some of those users aren't members of «{group}». Add them to the group?\";\n\"lng_group_call_add_to_group_all\" = \"Those users aren't members of «{group}». Add them to the group?\";\n\"lng_group_call_invite_members\" = \"Group members\";\n\"lng_group_call_invite_search_results\" = \"Search results\";\n\"lng_group_call_invite_limit\" = \"This is currently the maximum allowed number of participants.\";\n\"lng_group_call_new_muted\" = \"Mute new participants\";\n\"lng_group_call_enable_messages\" = \"Enable messages\";\n\"lng_group_call_speakers\" = \"Speakers\";\n\"lng_group_call_microphone\" = \"Microphone\";\n\"lng_group_call_push_to_talk\" = \"Push-to-Talk Shortcut\";\n\"lng_group_call_ptt_shortcut\" = \"Edit Keyboard Shortcut\";\n\"lng_group_call_ptt_recording\" = \"Stop Recording\";\n\"lng_group_call_ptt_delay_ms\" = \"{amount} ms\";\n\"lng_group_call_ptt_delay_s\" = \"{amount} s\";\n\"lng_group_call_ptt_delay\" = \"Release delay: {delay}\";\n\"lng_group_call_share\" = \"Share Invite Link\";\n\"lng_group_call_noise_suppression\" = \"Enable Noise Suppression\";\n\"lng_group_call_video_paused\" = \"Video is paused\";\n\"lng_group_call_share_speaker\" = \"Users with this link can speak\";\n\"lng_group_call_copy_speaker_link\" = \"Copy Speaker Link\";\n\"lng_group_call_copy_listener_link\" = \"Copy Listener Link\";\n\"lng_group_call_end\" = \"End Video Chat\";\n\"lng_group_call_end_channel\" = \"End Live Stream\";\n\"lng_group_call_cancel\" = \"Abort Video Chat\";\n\"lng_group_call_cancel_channel\" = \"Abort Live Stream\";\n\"lng_group_call_join\" = \"Join\";\n\"lng_group_call_join_confirm\" = \"Join the video chat {chat}?\";\n\"lng_group_call_join_confirm_channel\" = \"Do you want to join the live stream {chat}?\";\n\"lng_group_call_invite_done_user\" = \"You invited {user} to the video chat.\";\n\"lng_group_call_invite_done_many#one\" = \"You invited **{count} participant** to the video chat.\";\n\"lng_group_call_invite_done_many#other\" = \"You invited **{count} participants** to the video chat.\";\n\"lng_group_call_no_members\" = \"Click to join\";\n\"lng_group_call_members#one\" = \"{count} participant\";\n\"lng_group_call_members#other\" = \"{count} participants\";\n\"lng_group_call_context_mute\" = \"Mute\";\n\"lng_group_call_context_unmute\" = \"Allow to speak\";\n\"lng_group_call_context_remove_hand\" = \"Cancel request to speak\";\n\"lng_group_call_context_mute_for_me\" = \"Mute for me\";\n\"lng_group_call_context_unmute_for_me\" = \"Unmute for me\";\n\"lng_group_call_context_pin_camera\" = \"Pin video\";\n\"lng_group_call_context_unpin_camera\" = \"Unpin video\";\n\"lng_group_call_context_pin_screen\" = \"Pin screencast\";\n\"lng_group_call_context_unpin_screen\" = \"Unpin screencast\";\n\"lng_group_call_context_remove\" = \"Remove\";\n\"lng_group_call_context_cancel_invite\" = \"Discard invite\";\n\"lng_group_call_context_stop_ringing\" = \"Stop calling\";\n\"lng_group_call_context_ban_from_call\" = \"Ban from call\";\n\"lng_group_call_remove_channel\" = \"Remove {channel} from the video chat and ban them?\";\n\"lng_group_call_remove_channel_from_channel\" = \"Remove {channel} from the live stream?\";\n\"lng_group_call_mac_access\" = \"Telegram Desktop does not have access to system wide keyboard input required for Push to Talk.\";\n\"lng_group_call_mac_input\" = \"Please allow **Input Monitoring** for Telegram in Privacy Settings.\";\n\"lng_group_call_mac_accessibility\" = \"Please allow **Accessibility** for Telegram in Privacy Settings.\\n\\nYou may need to restart the app.\";\n\"lng_group_call_mac_screencast_access\" = \"Telegram Desktop does not have access to screen recording required for Screen Sharing.\";\n\"lng_group_call_mac_recording\" = \"Please allow **Screen Recording** for Telegram in Privacy Settings.\";\n\"lng_group_call_mac_settings\" = \"Open Settings\";\n\n\"lng_group_call_start_as_header\" = \"Start Video Chat as...\";\n\"lng_group_call_start_as_header_channel\" = \"Start Live Stream as...\";\n\"lng_group_call_join_as_header\" = \"Join Video Chat as...\";\n\"lng_group_call_join_as_header_channel\" = \"Join Live Stream as...\";\n\"lng_group_call_display_as_header\" = \"Display me as...\";\n\"lng_group_call_join_as_about\" = \"Choose whether you want to be displayed as your personal account or as your channel.\";\n\"lng_group_call_or_schedule\" = \"You can also {link}.\";\n\"lng_group_call_schedule\" = \"schedule a video chat\";\n\"lng_group_call_schedule_channel\" = \"schedule a live stream\";\n\"lng_group_call_schedule_title\" = \"Schedule Video Chat\";\n\"lng_group_call_schedule_title_channel\" = \"Schedule Live Stream\";\n\"lng_group_call_schedule_notified_group\" = \"Members of the group will be notified that the video chat will start in {duration}.\";\n\"lng_group_call_schedule_notified_channel\" = \"Subscribers of the channel will be notified that the live stream starts in {duration}.\";\n\"lng_group_call_scheduled_status\" = \"Scheduled\";\n\"lng_group_call_scheduled_title\" = \"Scheduled Video Chat\";\n\"lng_group_call_scheduled_title_channel\" = \"Scheduled Live Stream\";\n\"lng_group_call_starts_short\" = \"Starts {when}\";\n\"lng_group_call_starts\" = \"Video Chat starts {when}\";\n\"lng_group_call_starts_channel\" = \"Live Stream starts {when}\";\n\"lng_group_call_starts_today\" = \"today at {time}\";\n\"lng_group_call_starts_tomorrow\" = \"tomorrow at {time}\";\n\"lng_group_call_starts_date\" = \"{date} at {time}\";\n\"lng_group_call_starts_in\" = \"Starts in\";\n\"lng_group_call_late_by\" = \"Late by\";\n\"lng_group_call_starts_short_today\" = \"Today, {time}\";\n\"lng_group_call_starts_short_tomorrow\" = \"Tomorrow, {time}\";\n\"lng_group_call_starts_short_date\" = \"{date}, {time}\";\n\"lng_group_call_start_now\" = \"Start Now\";\n\"lng_group_call_start_now_sure\" = \"Are you sure you want to start the video chat now?\";\n\"lng_group_call_start_now_sure_channel\" = \"Are you sure you want to start the live stream now?\";\n\"lng_group_call_set_reminder\" = \"Set Reminder\";\n\"lng_group_call_cancel_reminder\" = \"Cancel Reminder\";\n\"lng_group_call_join_as_personal\" = \"personal account\";\n\"lng_group_call_edit_title\" = \"Edit Video Chat Title\";\n\"lng_group_call_edit_title_channel\" = \"Edit live stream title\";\n\"lng_group_call_recording_start\" = \"Start Recording\";\n\"lng_group_call_recording_stop\" = \"Stop Recording\";\n\"lng_group_call_recording_started\" = \"Audio recording started.\";\n\"lng_group_call_recording_started_video\" = \"Started recording the video stream.\";\n\"lng_group_call_recording_started_channel\" = \"Live stream recording started.\";\n\"lng_group_call_recording_stopped\" = \"Recording stopped.\";\n\"lng_group_call_recording_stopped_channel\" = \"Live stream recording stopped.\";\n\"lng_group_call_recording_saved\" = \"Recording of the audio stream saved to Saved Messages.\";\n\"lng_group_call_recording_saved_video\" = \"Video saved to Saved Messages.\";\n\"lng_group_call_pinned_camera_me\" = \"Your video is pinned.\";\n\"lng_group_call_pinned_screen_me\" = \"Your screencast is pinned.\";\n\"lng_group_call_pinned_camera\" = \"{user}'s video is pinned.\";\n\"lng_group_call_pinned_screen\" = \"{user}'s screencast is pinned.\";\n\"lng_group_call_unpinned_camera_me\" = \"Your video is unpinned.\";\n\"lng_group_call_unpinned_screen_me\" = \"Your screencast is unpinned.\";\n\"lng_group_call_unpinned_camera\" = \"{user}'s video is unpinned.\";\n\"lng_group_call_unpinned_screen\" = \"{user}'s screencast is unpinned.\";\n\"lng_group_call_sure_screencast\" = \"{user} is sharing their screen. This action will pin your screencast for all participants.\";\n\"lng_group_call_pinned_on_top\" = \"Live stream is pinned on top.\";\n\"lng_group_call_unpinned_on_top\" = \"Live stream is unpinned from top.\";\n\"lng_group_call_recording_start_sure\" = \"Record this chat and save the result into an audio file?\\n\\nParticipants will see that the chat is being recorded.\";\n\"lng_group_call_recording_stop_sure\" = \"Do you want to stop recording this video chat?\";\n\"lng_group_call_recording_start_field\" = \"Recording Title\";\n\"lng_group_call_recording_start_button\" = \"Start\";\n\"lng_group_call_recording_start_title\" = \"Add Title\";\n\"lng_group_call_recording_start_checkbox\" = \"Also record video\";\n\"lng_group_call_recording_start_audio_subtitle\" = \"This chat will be recorded into an audio file\";\n\"lng_group_call_recording_start_video_subtitle\" = \"Choose video orientation\";\n\"lng_group_call_is_recorded\" = \"The audio stream is being recorded.\";\n\"lng_group_call_is_recorded_video\" = \"The video stream is being recorded.\";\n\"lng_group_call_is_recorded_channel\" = \"Live stream is being recorded.\";\n\"lng_group_call_can_speak_here\" = \"You can now speak.\";\n\"lng_group_call_can_speak\" = \"You can now speak in {chat}.\";\n\"lng_group_call_title_changed\" = \"Video chat title changed to {title}\";\n\"lng_group_call_title_changed_channel\" = \"Live stream title changed to {title}\";\n\"lng_group_call_join_as_changed\" = \"Participants of this video chat will now see you as {name}\";\n\"lng_group_call_join_as_changed_channel\" = \"Participants of this live stream will now see you as {name}\";\n\"lng_group_call_no_stream_admin\" = \"Oops! Telegram doesn't see any stream coming from your streaming app. Please make sure you entered the right Server URL and Stream Key in your app.\";\n\"lng_group_call_no_stream\" = \"{group} is currently not broadcasting live stream data to Telegram.\";\n\n\"lng_menu_start_group_call\" = \"Start video chat\";\n\"lng_menu_start_group_call_scheduled\" = \"Schedule video chat\";\n\"lng_menu_start_group_call_with\" = \"Stream with...\";\n\"lng_menu_start_group_call_join\" = \"Join video chat\";\n\"lng_menu_start_group_call_options\" = \"Video chat\";\n\"lng_menu_start_group_call_channel\" = \"Start live stream\";\n\"lng_menu_start_group_call_scheduled_channel\" = \"Schedule live stream\";\n\"lng_menu_start_group_call_with_channel\" = \"Stream with...\";\n\n\"lng_group_call_rtmp_title\" = \"Stream with other apps\";\n\"lng_group_call_rtmp_url_subtitle\" = \"Server URL\";\n\"lng_group_call_rtmp_url_copy\" = \"Copy Server URL\";\n\"lng_group_call_rtmp_url_copied\" = \"Server URL copied to clipboard.\";\n\"lng_group_call_rtmp_key_subtitle\" = \"Stream Key\";\n\"lng_group_call_rtmp_key_copy\" = \"Copy Stream Key\";\n\"lng_group_call_rtmp_key_copied\" = \"Stream Key copied to clipboard.\";\n\"lng_group_call_rtmp_key_warning\" = \"**Never share your Stream Key with anyone or show it on stream!**\";\n\"lng_group_call_rtmp_info\" = \"To stream video with another app, enter these Server URL and Stream Key in your streaming app. Software encoding recommended (x264 in OBS).\\n\\nOnce you start broadcasting in your streaming app, click Start Streaming below.\";\n\"lng_group_call_rtmp_start\" = \"Start Streaming\";\n\"lng_group_call_rtmp_revoke\" = \"Revoke Stream Key\";\n\"lng_group_call_rtmp_revoke_sure\" = \"Are you sure you want to revoke your Stream Key?\";\n\"lng_group_call_rtmp_viewers#one\" = \"{count} viewer\";\n\"lng_group_call_rtmp_viewers#other\" = \"{count} viewers\";\n\n\"lng_confcall_join_title\" = \"Group Call\";\n\"lng_confcall_join_text\" = \"You are invited to join a Telegram Call.\";\n\"lng_confcall_join_text_inviter\" = \"{user} is inviting you to join a Telegram Call.\";\n\"lng_confcall_already_joined_one\" = \"{user} already joined this call.\";\n\"lng_confcall_already_joined_two\" = \"{user} and {other} already joined this call.\";\n\"lng_confcall_already_joined_three\" = \"{user}, {other} and {third} already joined this call.\";\n\"lng_confcall_already_joined_many#one\" = \"{user}, {other} and **{count}** other person already joined this call.\";\n\"lng_confcall_already_joined_many#other\" = \"{user}, {other} and **{count}** other people already joined this call.\";\n\"lng_confcall_join_button\" = \"Join Group Call\";\n\"lng_confcall_create_call\" = \"Start New Call\";\n\"lng_confcall_create_call_description#one\" = \"You can add up to {count} participant to a call.\";\n\"lng_confcall_create_call_description#other\" = \"You can add up to {count} participants to a call.\";\n\"lng_confcall_create_title\" = \"New Call\";\n\"lng_confcall_create_link\" = \"Create Call Link\";\n\"lng_confcall_create_link_description\" = \"You can create a link that will allow your friends on Telegram to join the call.\";\n\"lng_confcall_link_revoke\" = \"Revoke link\";\n\"lng_confcall_link_revoked_title\" = \"Link Revoked\";\n\"lng_confcall_link_inactive\" = \"This link is no longer active.\";\n\"lng_confcall_link_revoked_text\" = \"A new link has been generated.\";\n\"lng_confcall_link_title\" = \"Call Link\";\n\"lng_confcall_link_about\" = \"Anyone on Telegram can join your call by following the link below.\";\n\"lng_confcall_link_or\" = \"or\";\n\"lng_confcall_link_join\" = \"Be the first to join the call and add people from there. {link}\";\n\"lng_confcall_link_join_link\" = \"Open call {arrow}\";\n\"lng_confcall_inactive_title\" = \"Start Group Call\";\n\"lng_confcall_inactive_about\" = \"This call is no longer active.\\nYou can start a new one.\";\n\"lng_confcall_invite_done_user\" = \"You're calling {user} to join.\";\n\"lng_confcall_invite_done_many#one\" = \"You're calling **{count} person** to join.\";\n\"lng_confcall_invite_done_many#other\" = \"You're calling **{count} people** to join.\";\n\"lng_confcall_invite_already_user\" = \"{user} is already in the call.\";\n\"lng_confcall_invite_already_many#one\" = \"**{count} person** is already in the call.\";\n\"lng_confcall_invite_already_many#other\" = \"**{count} people** are already in the call.\";\n\"lng_confcall_invite_privacy_user\" = \"You cannot call {user} because of their privacy settings.\";\n\"lng_confcall_invite_privacy_many#one\" = \"You cannot call **{count} person** because of their privacy settings.\";\n\"lng_confcall_invite_privacy_many#other\" = \"You cannot call **{count} people** because of their privacy settings.\";\n\"lng_confcall_invite_fail_user\" = \"Couldn't call {user} to join.\";\n\"lng_confcall_invite_fail_many#one\" = \"Couldn't call **{count} person** to join.\";\n\"lng_confcall_invite_fail_many#other\" = \"Couldn't call **{count} people** to join.\";\n\"lng_confcall_invite_kicked_user\" = \"{user} was banned from the call.\";\n\"lng_confcall_invite_kicked_many#one\" = \"**{count} person** was removed from the call.\";\n\"lng_confcall_invite_kicked_many#other\" = \"**{count} people** were removed from the call.\";\n\"lng_confcall_not_accessible\" = \"This call is no longer accessible.\";\n\"lng_confcall_participants_limit\" = \"This call reached the participants limit.\";\n\"lng_confcall_sure_remove\" = \"Remove {user} from the call?\";\n\"lng_confcall_e2e_badge\" = \"End-to-End Encrypted\";\n\"lng_confcall_e2e_badge_small\" = \"E2E Encrypted\";\n\"lng_confcall_e2e_about\" = \"These four emoji represent the call's encryption key. They must match for all participants and change when someone joins or leaves.\";\n\n\"lng_no_mic_permission\" = \"Telegram needs microphone access so that you can make calls and record voice messages.\";\n\"lng_player_message_today\" = \"today at {time}\";\n\"lng_player_message_yesterday\" = \"yesterday at {time}\";\n\"lng_player_message_date\" = \"{date} at {time}\";\n\n\"lng_audio_player_reverse\" = \"Reverse order\";\n\"lng_audio_player_shuffle\" = \"Shuffle\";\n\"lng_audio_transcribe_long\" = \"This voice message is too long.\";\n\"lng_audio_transcribe_trials_left#one\" = \"You have {count} free transcription left until {date}.\";\n\"lng_audio_transcribe_trials_left#other\" = \"You have {count} free transcriptions left until {date}.\";\n\"lng_audio_transcribe_trials_over\" = \"You have used all your free transcriptions this week. Wait until {date} to use it again or subscribe to {link} now.\";\n\n\"lng_rights_edit_admin\" = \"Admin rights\";\n\"lng_rights_edit_admin_header\" = \"What can this admin do?\";\n\"lng_rights_edit_admin_rank_name\" = \"Custom title\";\n\"lng_rights_edit_admin_rank_about\" = \"A title that members will see instead of '{title}'.\";\n\"lng_context_add_my_tag\" = \"Add tag\";\n\"lng_context_edit_my_tag\" = \"Edit tag\";\n\"lng_context_add_member_tag\" = \"Add member tag\";\n\"lng_context_edit_member_tag\" = \"Edit member tag\";\n\"lng_rights_edit_tag_title\" = \"Edit tag\";\n\"lng_rights_tag_about\" = \"Add short tag next to {name}'s name.\";\n\"lng_rights_tag_about_self\" = \"Share your role, title, or how you're known in this group. Your tag is visible to all members.\";\n\n\"lng_tag_info_title_user\" = \"Member Tags\";\n\"lng_tag_info_title_admin\" = \"Admin Tags\";\n\"lng_tag_info_title_owner\" = \"Owner Tags\";\n\"lng_tag_info_text_user\" = \"This grey tag {emoji} is {author}'s member tag in {group}.\";\n\"lng_tag_info_text_admin\" = \"This green tag {emoji} is {author}'s admin tag in {group}.\";\n\"lng_tag_info_text_owner\" = \"This purple tag {emoji} is {author}'s owner tag in {group}.\";\n\"lng_tag_info_preview_member\" = \"Member Tag\";\n\"lng_tag_info_preview_admin\" = \"Admin Tag\";\n\"lng_tag_info_preview_owner\" = \"Owner Tag\";\n\"lng_tag_info_add_my_tag\" = \"Add My Tag\";\n\"lng_tag_info_edit_my_tag\" = \"Edit My Tag\";\n\"lng_tag_info_admins_only\" = \"Only admins can change tags in this group.\";\n\n\"lng_rights_promote_member\" = \"Promote to Admin\";\n\"lng_rights_remove_member\" = \"Remove from Group\";\n\"lng_rights_dismiss_admin\" = \"Dismiss Admin\";\n\"lng_rights_about_add_admins_yes\" = \"This admin will be able to add new admins with equal or fewer rights.\";\n\"lng_rights_about_add_admins_no\" = \"This admin will not be able to add new admins.\";\n\"lng_rights_about_by\" = \"This admin promoted by {user} on {date}.\";\n\n\"lng_rights_about_admin_cant_edit\" = \"You can't edit the rights of this admin.\";\n\"lng_rights_about_restriction_cant_edit\" = \"You cannot change the restrictions for this user.\";\n\"lng_rights_restriction_for_all\" = \"This option is disabled for all members in Group Permissions.\";\n\"lng_rights_permission_for_all\" = \"This option is enabled for all members in Group Permissions.\";\n\"lng_rights_permission_unavailable\" = \"This permission is not available in public groups.\";\n\"lng_rights_permission_in_discuss\" = \"This permission is not available in discussion groups.\";\n\"lng_rights_permission_cant_edit\" = \"You cannot change this permission.\";\n\"lng_rights_user_restrictions\" = \"User permissions\";\n\"lng_rights_user_restrictions_header\" = \"What can this member do?\";\n\"lng_rights_default_restrictions_header\" = \"What can members of this group do?\";\n\"lng_rights_slowmode_header\" = \"Slow mode\";\n\"lng_rights_slowmode_off\" = \"Off\";\n\"lng_rights_slowmode_about\" = \"Members will be able to send only one message per this interval.\";\n\"lng_rights_slowmode_about_interval\" = \"Members will be able to send only one message {interval}.\";\n\"lng_rights_slowmode_interval_seconds#one\" = \"every {count} second\";\n\"lng_rights_slowmode_interval_seconds#other\" = \"every {count} seconds\";\n\"lng_rights_slowmode_interval_minutes#one\" = \"every {count} minute\";\n\"lng_rights_slowmode_interval_minutes#other\" = \"every {count} minutes\";\n\"lng_rights_boosts_no_restrict\" = \"Do not restrict boosters\";\n\"lng_rights_boosts_about\" = \"Turn this on to always allow users who boosted your group to send messages and media.\";\n\"lng_rights_boosts_about_on\" = \"Choose how many boosts a user must give to the group to bypass restrictions on sending messages.\";\n\"lng_rights_charge_stars\" = \"Charge Stars for Messages\";\n\"lng_rights_charge_stars_about\" = \"If you turn this on, regular members of the group will have to pay Stars to send messages.\";\n\"lng_rights_charge_price\" = \"Set price per message\";\n\"lng_rights_charge_price_about\" = \"Your group will receive {percent} of the selected fee ({amount}) for each incoming message.\";\n\n\"lng_slowmode_enabled\" = \"Slow Mode is active.\\nYou can send your next message in {left}.\";\n\"lng_slowmode_no_many\" = \"Slow mode is enabled. You can't send more than one message at a time.\";\n\"lng_slowmode_too_long\" = \"This text is too long to send as one message.\\n\\nSlow mode is active. You can't send more than one message at once.\";\n\"lng_slowmode_seconds#one\" = \"{count} second\";\n\"lng_slowmode_seconds#other\" = \"{count} seconds\";\n\n\"lng_payment_confirm_title\" = \"Confirm payment\";\n\"lng_payment_confirm_text#one\" = \"{name} charges **{count}** Star per message.\";\n\"lng_payment_confirm_text#other\" = \"{name} charges **{count}** Stars per message.\";\n\"lng_payment_confirm_amount#one\" = \"**{count}** Star\";\n\"lng_payment_confirm_amount#other\" = \"**{count}** Stars\";\n\"lng_payment_confirm_users#one\" = \"You selected **{count}** user who charge Stars for messages.\";\n\"lng_payment_confirm_users#other\" = \"You selected **{count}** users who charge Stars for messages.\";\n\"lng_payment_confirm_chats#one\" = \"You selected **{count}** chat where you pay Stars for messages.\";\n\"lng_payment_confirm_chats#other\" = \"You selected **{count}** chats where you pay Stars for messages.\";\n\"lng_payment_confirm_sure#one\" = \"Would you like to pay {amount} to send **{count}** message?\";\n\"lng_payment_confirm_sure#other\" = \"Would you like to pay {amount} to send **{count}** messages?\";\n\"lng_payment_confirm_dont_ask\" = \"Don't ask me again\";\n\"lng_payment_confirm_button#one\" = \"Pay for {count} Message\";\n\"lng_payment_confirm_button#other\" = \"Pay for {count} Messages\";\n\"lng_payment_bar_text\" = \"{name} must pay {cost} for each message to you.\";\n\"lng_payment_bar_button\" = \"Remove Fee\";\n\"lng_payment_refund_title\" = \"Remove Fee\";\n\"lng_payment_refund_text\" = \"Are you sure you want to allow {name} to message you for free?\";\n\"lng_payment_refund_channel\" = \"Do you want to allow {name} to message the channel for free?\";\n\"lng_payment_refund_also#one\" = \"Refund already paid {count} Star\";\n\"lng_payment_refund_also#other\" = \"Refund already paid {count} Stars\";\n\"lng_payment_refund_confirm\" = \"Confirm\";\n\n\"lng_rights_gigagroup_title\" = \"Broadcast group\";\n\"lng_rights_gigagroup_convert\" = \"Convert to Broadcast Group\";\n\"lng_rights_gigagroup_about\" = \"Broadcast groups can have over 200,000 members, but only admins can send messages in them.\";\n\"lng_gigagroup_convert_title\" = \"Broadcast Groups\";\n\"lng_gigagroup_convert_feature1\" = \"No limit on the number of members.\";\n\"lng_gigagroup_convert_feature2\" = \"Only admins can send messages.\";\n\"lng_gigagroup_convert_feature3\" = \"Can't be turned back into a regular group.\";\n\"lng_gigagroup_convert_sure\" = \"Convert\";\n\"lng_gigagroup_warning_title\" = \"Are you sure?\";\n\"lng_gigagroup_warning\" = \"Regular members of the group (non-admins) will **permanently** lose their right to send messages in the group.\\n\\nThis action **can't** be undone.\";\n\"lng_gigagroup_done\" = \"Your group can now have more than 200,000 members.\";\n\"lng_gigagroup_suggest_title\" = \"Limit reached\";\n\"lng_gigagroup_suggest_text\" = \"Your group has reached a limit of **200,000** members.\\n\\nYou can increase this limit by converting the group to a **broadcast group** where only admins can post. Interested?\";\n\"lng_gigagroup_suggest_more\" = \"Learn more\";\n\n\"lng_rights_channel_info\" = \"Change channel info\";\n\"lng_rights_channel_manage\" = \"Manage messages\";\n\"lng_rights_channel_post\" = \"Post messages\";\n\"lng_rights_channel_edit\" = \"Edit messages of others\";\n\"lng_rights_channel_delete\" = \"Delete messages of others\";\n\"lng_rights_channel_manage_stories\" = \"Manage stories\";\n\"lng_rights_channel_post_stories\" = \"Post stories\";\n\"lng_rights_channel_edit_stories\" = \"Edit stories of others\";\n\"lng_rights_channel_delete_stories\" = \"Delete stories of others\";\n\"lng_rights_channel_manage_calls\" = \"Manage live streams\";\n\"lng_rights_channel_manage_direct\" = \"Manage direct messages\";\n\"lng_rights_group_info\" = \"Change group info\";\n\"lng_rights_group_ban\" = \"Ban users\";\n\"lng_rights_group_invite_link\" = \"Invite users via link\";\n\"lng_rights_group_invite\" = \"Add members\";\n\"lng_rights_group_pin\" = \"Pin messages\";\n\"lng_rights_group_topics\" = \"Manage topics\";\n\"lng_rights_group_add_topics\" = \"Create topics\";\n\"lng_rights_group_edit_rank\" = \"Edit own tags\";\n\"lng_rights_group_edit_rank_single\" = \"Edit own tag\";\n\"lng_rights_group_manage_calls\" = \"Manage video chats\";\n\"lng_rights_group_delete\" = \"Delete messages\";\n\"lng_rights_group_anonymous\" = \"Remain anonymous\";\n\"lng_rights_group_manage_ranks\" = \"Edit member tags\";\n\"lng_rights_add_admins\" = \"Add new admins\";\n\"lng_rights_chat_send_text\" = \"Send messages\";\n\"lng_rights_chat_send_media\" = \"Send media\";\n\"lng_rights_chat_send_stickers\" = \"Send stickers & GIFs\";\n\"lng_rights_chat_send_links\" = \"Embed links\";\n\"lng_rights_chat_send_polls\" = \"Send polls\";\n\"lng_rights_chat_add_members\" = \"Add members\";\n\"lng_rights_chat_photos\" = \"Photos\";\n\"lng_rights_chat_videos\" = \"Video files\";\n\"lng_rights_chat_stickers\" = \"Stickers & GIFs\";\n\"lng_rights_chat_music\" = \"Music\";\n\"lng_rights_chat_files\" = \"Files\";\n\"lng_rights_chat_voice_messages\" = \"Voice messages\";\n\"lng_rights_chat_video_messages\" = \"Video messages\";\n\"lng_rights_chat_restricted_by\" = \"Restricted by {user} on {date}.\";\n\"lng_rights_chat_banned_by\" = \"Banned by {user} on {date}.\";\n\"lng_rights_chat_banned_until_header\" = \"Restricted until\";\n\"lng_rights_chat_banned_forever\" = \"Forever\";\n\"lng_rights_chat_banned_day#one\" = \"For {count} day\";\n\"lng_rights_chat_banned_day#other\" = \"For {count} days\";\n\"lng_rights_chat_banned_week#one\" = \"For {count} week\";\n\"lng_rights_chat_banned_week#other\" = \"For {count} weeks\";\n\"lng_rights_chat_banned_custom\" = \"Custom\";\n\"lng_rights_chat_banned_custom_date\" = \"Until {date}\";\n\n\"lng_rights_transfer_group\" = \"Transfer group ownership\";\n\"lng_rights_transfer_channel\" = \"Transfer channel ownership\";\n\"lng_rights_transfer_check\" = \"Security check\";\n\"lng_rights_transfer_check_about\" = \"You can transfer this group to {user} only if you have:\";\n\"lng_rights_transfer_check_about_channel\" = \"You can transfer this channel to {user} only if you have:\";\n\"lng_rights_transfer_check_password\" = \"• Enabled **Two-Step Verification** more than **7 days** ago.\";\n\"lng_rights_transfer_check_session\" = \"• Logged in on this device more than **24 hours** ago.\";\n\"lng_rights_transfer_check_later\" = \"Please come back later.\";\n\"lng_rights_transfer_set_password\" = \"Set password\";\n\"lng_rights_transfer_about\" = \"This will transfer the full **owner rights** for {group} to {user}. The new owner will be free to remove any of your admin privileges or even ban you.\";\n\"lng_rights_transfer_sure\" = \"Change owner\";\n\"lng_rights_transfer_password_title\" = \"Two-step verification\";\n\"lng_rights_transfer_password_description\" = \"Please enter your password to complete the transfer.\";\n\"lng_rights_transfer_done_group\" = \"{user} is now the owner of the group.\";\n\"lng_rights_transfer_done_channel\" = \"{user} is now the owner of the channel.\";\n\n\"lng_bots_password_confirm_check_about\" = \"You can complete this action only if you have:\";\n\"lng_bots_password_confirm_title\" = \"Two-step verification\";\n\"lng_bots_password_confirm_description\" = \"Please enter your password to confirm this action.\";\n\n\"lng_restricted_send_message\" = \"The admins of this group have restricted your ability to send messages.\";\n\"lng_restricted_send_photos\" = \"The admins of this group restricted you from sending photos here.\";\n\"lng_restricted_send_videos\" = \"The admins of this group restricted you from sending videos here.\";\n\"lng_restricted_send_music\" = \"The admins of this group restricted you from sending music here.\";\n\"lng_restricted_send_files\" = \"The admins of this group restricted you from sending files here.\";\n\"lng_restricted_send_voice_messages_group\" = \"The admins of this group restricted you from sending voice messages here.\";\n\"lng_restricted_send_video_messages_group\" = \"The admins of this group restricted you from sending video messages here.\";\n\"lng_restricted_send_stickers\" = \"The admins of this group have restricted your ability to send stickers.\";\n\"lng_restricted_send_gifs\" = \"The admins of this group have restricted your ability to send GIFs.\";\n\"lng_restricted_send_inline\" = \"The admins of this group have restricted your ability to send inline content.\";\n\"lng_restricted_send_polls\" = \"The admins of this group have restricted your ability to send polls.\";\n\"lng_restricted_boost_group\" = \"Boost this group to send messages\";\n\n\"lng_restricted_send_message_until\" = \"The admins of this group have restricted you from sending messages until {date}, {time}.\";\n\"lng_restricted_send_photos_until\" = \"The admins of this group restricted you from sending photos here until {date}, {time}.\";\n\"lng_restricted_send_videos_until\" = \"The admins of this group restricted you from sending videos here until {date}, {time}.\";\n\"lng_restricted_send_music_until\" = \"The admins of this group restricted you from sending music here until {date}, {time}.\";\n\"lng_restricted_send_files_until\" = \"The admins of this group restricted you from sending files here until {date}, {time}.\";\n\"lng_restricted_send_voice_messages_until\" = \"The admins of this group restricted you from sending voice messages here until {date}, {time}.\";\n\"lng_restricted_send_video_messages_until\" = \"The admins of this group restricted you from sending video messages here until {date}, {time}.\";\n\"lng_restricted_send_stickers_until\" = \"The admins of this group have restricted your ability to send stickers until {date}, {time}.\";\n\"lng_restricted_send_gifs_until\" = \"The admins of this group have restricted your ability to send GIFs until {date}, {time}.\";\n\"lng_restricted_send_inline_until\" = \"The admins of this group have restricted your ability to send inline content until {date}, {time}.\";\n\"lng_restricted_send_polls_until\" = \"The admins of this group have restricted your ability to send polls until {date}, {time}.\";\n\n\"lng_restricted_send_message_all\" = \"Sending messages is not allowed in this group.\";\n\"lng_restricted_send_photos_all\" = \"Sending photos isn't allowed in this group.\";\n\"lng_restricted_send_videos_all\" = \"Sending videos isn't allowed in this group.\";\n\"lng_restricted_send_music_all\" = \"Sending music isn't allowed in this group.\";\n\"lng_restricted_send_files_all\" = \"Sending files isn't allowed in this group.\";\n\"lng_restricted_send_voice_messages_all\" = \"Sending voice messages isn't allowed in this group.\";\n\"lng_restricted_send_video_messages_all\" = \"Sending video messages isn't allowed in this group.\";\n\"lng_restricted_send_stickers_all\" = \"Stickers aren’t allowed in this group.\";\n\"lng_restricted_send_gifs_all\" = \"Sending GIFs isn't allowed in this group.\";\n\"lng_restricted_send_inline_all\" = \"Sending inline content isn't allowed in this group.\";\n\"lng_restricted_send_polls_all\" = \"Sorry, sending polls is not allowed in this group.\";\n\n\"lng_restricted_send_public_polls\" = \"Sorry, polls with visible votes can't be forwarded to channels.\";\n\"lng_restricted_send_todo_lists\" = \"Sorry, Checklists can't be forwarded to channels.\";\n\"lng_restricted_send_paid_media\" = \"Sorry, paid media can't be sent to this channel.\";\n\n\"lng_restricted_send_voice_messages\" = \"{user} doesn't accept voice messages.\";\n\"lng_restricted_send_video_messages\" = \"{user} doesn't accept video messages.\";\n\"lng_restricted_send_non_premium\" = \"Only Premium users can message {user}.\";\n\"lng_restricted_send_non_premium_more\" = \"Learn more...\";\n\n\"lng_send_non_premium_text\" = \"Subscribe to **Premium**\\nto message {user}.\";\n\"lng_send_non_premium_go\" = \"Go Premium\";\n\"lng_send_non_premium_story\" = \"Replies restricted\";\n\"lng_send_non_premium_unlock\" = \"unlock\";\n\"lng_send_non_premium_message_toast\" = \"**{user}** only accepts messages from contacts and {link} subscribers.\";\n\"lng_send_non_premium_message_toast_link\" = \"Telegram Premium\";\n\n\"lng_send_charges_stars_channel\" = \"{channel} charges {amount} per message to its admin.\";\n\"lng_send_free_channel\" = \"Send a direct message to the administrator of {channel}.\";\n\"lng_send_charges_stars_text\" = \"{user} charges {amount} for each message.\";\n\"lng_send_charges_stars_go\" = \"Buy Stars\";\n\n\"lng_exceptions_list_title\" = \"Exceptions\";\n\"lng_removed_list_title\" = \"Removed users\";\n\n\"lng_admin_log_title_all\" = \"All actions\";\n\"lng_admin_log_title_selected\" = \"Selected actions\";\n\"lng_admin_log_filter_title\" = \"Filter\";\n\"lng_admin_log_filter_all_actions\" = \"All actions\";\n\"lng_admin_log_filter_actions_type_subtitle\" = \"Filter actions by type\";\n\"lng_admin_log_filter_actions_member_section\" = \"Members And Admins\";\n\"lng_admin_log_filter_actions_subscriber_section\" = \"Subscribers And Admins\";\n\"lng_admin_log_filter_restrictions\" = \"New restrictions\";\n\"lng_admin_log_filter_admins_new\" = \"Admin rights\";\n\"lng_admin_log_filter_members_new\" = \"New members\";\n\"lng_admin_log_filter_subscribers_new\" = \"New subscribers\";\n\"lng_admin_log_filter_actions_settings_section\" = \"Group Settings\";\n\"lng_admin_log_filter_actions_channel_settings_section\" = \"Channel Settings\";\n\"lng_admin_log_filter_info_group\" = \"Group info\";\n\"lng_admin_log_filter_info_channel\" = \"Channel info\";\n\"lng_admin_log_filter_actions_messages_section\" = \"Messages\";\n\"lng_admin_log_filter_messages_deleted\" = \"Deleted messages\";\n\"lng_admin_log_filter_messages_edited\" = \"Edited messages\";\n\"lng_admin_log_filter_messages_pinned\" = \"Pinned messages\";\n\"lng_admin_log_filter_voice_chats\" = \"Video chats\";\n\"lng_admin_log_filter_voice_chats_channel\" = \"Live stream\";\n\"lng_admin_log_filter_invite_links\" = \"Invite links\";\n\"lng_admin_log_filter_members_removed\" = \"Members leaving\";\n\"lng_admin_log_filter_subscribers_removed\" = \"Subscribers leaving\";\n\"lng_admin_log_filter_topics\" = \"Topics\";\n\"lng_admin_log_filter_sub_extend\" = \"Subscription Renewals\";\n\"lng_admin_log_filter_edit_rank\" = \"Tag Changes\";\n\"lng_admin_log_filter_all_admins\" = \"All users and admins\";\n\"lng_admin_log_filter_actions_admins_subtitle\" = \"Filter actions by admins\";\n\"lng_admin_log_filter_actions_admins_section\" = \"Show Actions by All Admins\";\n\"lng_admin_log_about_text\" = \"This is a list of all notable actions by group members and admins in the last 48 hours.\";\n\"lng_admin_log_about_text_channel\" = \"This is a list of all service actions taken by the channel's admins in the last 48 hours.\";\n\"lng_admin_log_no_results_title\" = \"No actions found\";\n\"lng_admin_log_no_results_text\" = \"No recent actions that match your query were found.\";\n\"lng_admin_log_no_results_search_text\" = \"No recent actions that contain '{query}' have been found.\";\n\"lng_admin_log_no_events_title\" = \"No actions yet\";\n\"lng_admin_log_no_events_text\" = \"No notable actions taken by the members and admins of this group in the last 48 hours.\";\n\"lng_admin_log_no_events_text_channel\" = \"No notable actions taken\\nby the admins of this channel\\nin the last 48 hours.\";\n\n\"lng_admin_log_empty_text\" = \"Empty\";\n\"lng_admin_log_changed_title_channel\" = \"{from} changed channel name to «{title}»\";\n\"lng_admin_log_changed_description_group\" = \"{from} edited group description:\";\n\"lng_admin_log_removed_description_group\" = \"{from} removed group description\";\n\"lng_admin_log_changed_description_channel\" = \"{from} edited channel description:\";\n\"lng_admin_log_removed_description_channel\" = \"{from} removed channel description\";\n\"lng_admin_log_previous_description\" = \"Previous description\";\n\"lng_admin_log_changed_link_group\" = \"{from} changed group link:\";\n\"lng_admin_log_removed_link_group\" = \"{from} removed group link\";\n\"lng_admin_log_changed_link_channel\" = \"{from} changed channel link:\";\n\"lng_admin_log_removed_link_channel\" = \"{from} removed channel link\";\n\"lng_admin_log_previous_link\" = \"Previous link\";\n\"lng_admin_log_reordered_link_group\" = \"{from} reordered group links:\";\n\"lng_admin_log_reordered_link_channel\" = \"{from} reordered channel links:\";\n\"lng_admin_log_previous_links_order\" = \"Previous order\";\n\"lng_admin_log_activated_link\" = \"{from} activated @{link} username\";\n\"lng_admin_log_deactivated_link\" = \"{from} deactivated @{link} username\";\n\"lng_admin_log_changed_photo_group\" = \"{from} changed group photo\";\n\"lng_admin_log_changed_photo_channel\" = \"{from} changed channel photo\";\n\"lng_admin_log_removed_photo_group\" = \"{from} removed group photo\";\n\"lng_admin_log_removed_photo_channel\" = \"{from} removed channel photo\";\n\"lng_admin_log_invites_enabled\" = \"{from} enabled group invites\";\n\"lng_admin_log_invites_disabled\" = \"{from} disabled group invites\";\n\"lng_admin_log_signatures_enabled\" = \"{from} enabled signatures\";\n\"lng_admin_log_signatures_disabled\" = \"{from} disabled signatures\";\n\"lng_admin_log_signature_profiles_enabled\" = \"{from} enabled showing authors' profiles\";\n\"lng_admin_log_signature_profiles_disabled\" = \"{from} disabled showing authors' profiles\";\n\"lng_admin_log_forwards_enabled\" = \"{from} allowed saving content\";\n\"lng_admin_log_forwards_disabled\" = \"{from} restricted saving content\";\n\"lng_admin_log_history_made_hidden\" = \"{from} made the group history hidden for new members\";\n\"lng_admin_log_history_made_visible\" = \"{from} made group history visible for new members\";\n\"lng_admin_log_pinned_message\" = \"{from} pinned message:\";\n\"lng_admin_log_unpinned_message\" = \"{from} unpinned message\";\n\"lng_admin_log_edited_caption\" = \"{from} edited caption:\";\n\"lng_admin_log_edited_media\" = \"{from} edited media:\";\n\"lng_admin_log_edited_media_and_caption\" = \"{from} edited media and caption:\";\n\"lng_admin_log_edited_media_and_removed_caption\" = \"{from} edited media and removed caption:\";\n\"lng_admin_log_removed_caption\" = \"{from} removed caption\";\n\"lng_admin_log_previous_caption\" = \"Original caption\";\n\"lng_admin_log_edited_message\" = \"{from} edited message:\";\n\"lng_admin_log_previous_message\" = \"Original message\";\n\"lng_admin_log_deleted_message\" = \"{from} deleted message:\";\n\"lng_admin_log_sent_message\" = \"{from} sent this message:\";\n\"lng_admin_log_participant_joined\" = \"{from} joined the group\";\n\"lng_admin_log_participant_joined_channel\" = \"{from} joined the channel\";\n\"lng_admin_log_participant_joined_by_link\" = \"{from} joined the group via {link}\";\n\"lng_admin_log_participant_joined_by_link_channel\" = \"{from} joined the channel via {link}\";\n\"lng_admin_log_participant_joined_by_filter_link\" = \"{from} joined via folder invite link {link}\";\n\"lng_admin_log_participant_joined_by_filter_link_channel\" = \"{from} joined via folder invite link {link}\";\n\"lng_admin_log_participant_approved_by_link\" = \"{from} was approved to join the group via {link} by {user}\";\n\"lng_admin_log_participant_approved_by_link_channel\" = \"{from} was approved to join the channel via {link} by {user}\";\n\"lng_admin_log_participant_approved_by_request\" = \"{from} joined the group via public request, approved by {user}\";\n\"lng_admin_log_participant_approved_by_request_channel\" = \"{from} joined the channel via public request, approved by {user}\";\n\"lng_admin_log_revoke_invite_link\" = \"{from} revoked invite link {link}\";\n\"lng_admin_log_delete_invite_link\" = \"{from} deleted the invite link {link}\";\n\"lng_admin_log_participant_left\" = \"{from} left the group\";\n\"lng_admin_log_participant_left_channel\" = \"{from} left the channel\";\n\"lng_admin_log_stopped_poll\" = \"{from} stopped poll:\";\n\"lng_admin_log_invited\" = \"invited {user}\";\n\"lng_admin_log_banned\" = \"banned {user}\";\n\"lng_admin_log_banned_until\" = \"banned {user} {until}\";\n\"lng_admin_log_unbanned\" = \"unbanned {user}\";\n\"lng_admin_log_restricted\" = \"changed restrictions for {user} {until}\";\n\"lng_admin_log_promoted\" = \"changed privileges for {user}\";\n\"lng_admin_log_transferred\" = \"transferred ownership to {user}\";\n\"lng_admin_log_changed_default_permissions\" = \"changed default permissions\";\n\"lng_admin_log_changed_stickers_group\" = \"{from} changed the group {sticker_set}\";\n\"lng_admin_log_changed_stickers_set\" = \"sticker set\";\n\"lng_admin_log_removed_stickers_group\" = \"{from} removed the group sticker set\";\n\"lng_admin_log_changed_emoji_group\" = \"{from} changed the group's {sticker_set}\";\n\"lng_admin_log_changed_emoji_set\" = \"emoji set\";\n\"lng_admin_log_removed_emoji_group\" = \"{from} removed the group's emoji set\";\n\"lng_admin_log_changed_linked_chat\" = \"{from} changed the discussion group to «{chat}»\";\n\"lng_admin_log_removed_linked_chat\" = \"{from} removed the discussion group\";\n\"lng_admin_log_changed_linked_channel\" = \"{from} changed the linked channel to «{chat}»\";\n\"lng_admin_log_removed_linked_channel\" = \"{from} removed the linked channel\";\n\"lng_admin_log_changed_location_chat\" = \"{from} changed the group location to {address}\";\n\"lng_admin_log_removed_location_chat\" = \"{from} removed the group location\";\n\"lng_admin_log_changed_slow_mode\" = \"{from} changed slow mode to {duration}\";\n\"lng_admin_log_removed_slow_mode\" = \"{from} disabled slow mode\";\n\"lng_admin_log_started_group_call\" = \"{from} started a new video chat\";\n\"lng_admin_log_started_group_call_channel\" = \"{from} started a new live stream\";\n\"lng_admin_log_discarded_group_call\" = \"{from} ended the video chat\";\n\"lng_admin_log_discarded_group_call_channel\" = \"{from} ended the live stream\";\n\"lng_admin_log_muted_participant\" = \"{from} muted {user} in a video chat\";\n\"lng_admin_log_muted_participant_channel\" = \"{from} muted {user} in a live stream\";\n\"lng_admin_log_unmuted_participant\" = \"{from} unmuted {user} in a video chat\";\n\"lng_admin_log_unmuted_participant_channel\" = \"{from} unmuted {user} in a live stream\";\n\"lng_admin_log_allowed_unmute_self\" = \"{from} allowed new video chat participants to speak\";\n\"lng_admin_log_allowed_unmute_self_channel\" = \"{from} allowed new live stream participants to speak\";\n\"lng_admin_log_disallowed_unmute_self\" = \"{from} muted new video chat participants\";\n\"lng_admin_log_disallowed_unmute_self_channel\" = \"{from} muted new live stream participants\";\n\"lng_admin_log_participant_volume\" = \"{from} changed video chat volume for {user} to {percent}\";\n\"lng_admin_log_participant_volume_channel\" = \"{from} changed live stream volume for {user} to {percent}\";\n\"lng_admin_log_antispam_enabled\" = \"{from} enabled aggressive anti-spam\";\n\"lng_admin_log_antispam_disabled\" = \"{from} disabled aggressive anti-spam\";\n\"lng_admin_log_autotranslate_enabled\" = \"{from} enabled automatic translation\";\n\"lng_admin_log_autotranslate_disabled\" = \"{from} disabled automatic translation\";\n\"lng_admin_log_change_color\" = \"{from} changed channel color from {previous} to {color}\";\n\"lng_admin_log_set_background_emoji\" = \"{from} set channel background emoji to {emoji}\";\n\"lng_admin_log_change_background_emoji\" = \"{from} changed channel background emoji from {previous} to {emoji}\";\n\"lng_admin_log_removed_background_emoji\" = \"{from} removed channel background emoji {emoji}\";\n\"lng_admin_log_change_profile_color\" = \"{from} changed channel profile color from {previous} to {color}\";\n\"lng_admin_log_set_profile_background_emoji\" = \"{from} set channel profile background emoji to {emoji}\";\n\"lng_admin_log_change_profile_background_emoji\" = \"{from} changed channel profile background emoji from {previous} to {emoji}\";\n\"lng_admin_log_removed_profile_background_emoji\" = \"{from} removed channel profile background emoji {emoji}\";\n\"lng_admin_log_change_profile_color_group\" = \"{from} changed group profile color from {previous} to {color}\";\n\"lng_admin_log_set_profile_background_emoji_group\" = \"{from} set group profile background emoji to {emoji}\";\n\"lng_admin_log_change_profile_background_emoji_group\" = \"{from} changed group profile background emoji from {previous} to {emoji}\";\n\"lng_admin_log_removed_profile_background_emoji_group\" = \"{from} removed group profile background emoji {emoji}\";\n\"lng_admin_log_change_wallpaper\" = \"{from} changed channel wallpaper\";\n\"lng_admin_log_set_status\" = \"{from} set channel emoji status to {emoji}\";\n\"lng_admin_log_change_status\" = \"{from} changed channel emoji status from {previous} to {emoji}\";\n\"lng_admin_log_removed_status\" = \"{from} removed channel emoji status {emoji}\";\n\"lng_admin_log_set_status_until\" = \"{from} set channel emoji status to {emoji} until {date}\";\n\"lng_admin_log_change_status_until\" = \"{from} changed channel emoji status from {previous} to {emoji} until {date}\";\n\"lng_admin_log_user_with_username\" = \"{name} ({mention})\";\n\"lng_admin_log_messages_ttl_set\" = \"{from} enabled messages auto-delete after {duration}\";\n\"lng_admin_log_messages_ttl_changed\" = \"{from} changed messages auto-delete period from {previous} to {duration}\";\n\"lng_admin_log_messages_ttl_removed\" = \"{from} disabled messages auto-deletion after {duration}\";\n\"lng_admin_log_reactions_disabled\" = \"{from} disabled reactions\";\n\"lng_admin_log_reactions_updated\" = \"{from} updated the list of allowed reactions to: {emoji}\";\n\"lng_admin_log_reactions_allowed_all\" = \"{from} allowed all reactions\";\n\"lng_admin_log_reactions_allowed_official\" = \"{from} allowed all official reactions\";\n\"lng_admin_log_edited_invite_link\" = \"edited invite link {link}\";\n\"lng_admin_log_invite_link_expire_date\" = \"Expiry date: {previous} → {limit}\";\n\"lng_admin_log_invite_link_usage_limit\" = \"Usage limit: {previous} → {limit}\";\n\"lng_admin_log_invite_link_label\" = \"Name: {previous} → {limit}\";\n\"lng_admin_log_invite_link_request_needed\" = \"Admin approval is now required to join.\";\n\"lng_admin_log_invite_link_request_not_needed\" = \"Admin approval no longer required to join.\";\n\"lng_admin_log_topics_enabled\" = \"{from} enabled topics\";\n\"lng_admin_log_topics_disabled\" = \"{from} disabled topics\";\n\"lng_admin_log_topics_created\" = \"{from} created topic {topic}\";\n\"lng_admin_log_topics_changed\" = \"{from} renamed topic {topic} to {new_topic}\";\n\"lng_admin_log_topics_closed\" = \"{from} closed topic {topic}\";\n\"lng_admin_log_topics_reopened\" = \"{from} reopened topic {topic}\";\n\"lng_admin_log_topics_hidden\" = \"{from} hid topic {topic}\";\n\"lng_admin_log_topics_unhidden\" = \"{from} unhid topic {topic}\";\n\"lng_admin_log_topics_deleted\" = \"{from} deleted topic {topic}\";\n\"lng_admin_log_topics_pinned\" = \"{from} pinned topic {topic}\";\n\"lng_admin_log_topics_unpinned\" = \"{from} unpinned topic {topic}\";\n\"lng_admin_log_restricted_forever\" = \"indefinitely\";\n\"lng_admin_log_restricted_until\" = \"until {date}\";\n\"lng_admin_log_banned_view_messages\" = \"Read messages\";\n\"lng_admin_log_banned_send_messages\" = \"Send messages\";\n\"lng_admin_log_banned_send_photos\" = \"Send photos\";\n\"lng_admin_log_banned_send_videos\" = \"Send video files\";\n\"lng_admin_log_banned_send_music\" = \"Send music\";\n\"lng_admin_log_banned_send_files\" = \"Send files\";\n\"lng_admin_log_banned_send_voice_messages\" = \"Send voice messages\";\n\"lng_admin_log_banned_send_video_messages\" = \"Send video messages\";\n\"lng_admin_log_banned_send_stickers\" = \"Send stickers & GIFs\";\n\"lng_admin_log_banned_embed_links\" = \"Embed links\";\n\"lng_admin_log_banned_send_polls\" = \"Send polls\";\n\"lng_admin_log_admin_change_info\" = \"Change info\";\n\"lng_admin_log_admin_post_messages\" = \"Post messages\";\n\"lng_admin_log_admin_edit_messages\" = \"Edit messages\";\n\"lng_admin_log_admin_delete_messages\" = \"Delete messages\";\n\"lng_admin_log_admin_post_stories\" = \"Post stories\";\n\"lng_admin_log_admin_edit_stories\" = \"Edit stories of others\";\n\"lng_admin_log_admin_delete_stories\" = \"Delete stories\";\n\"lng_admin_log_admin_remain_anonymous\" = \"Remain anonymous\";\n\"lng_admin_log_admin_ban_users\" = \"Ban users\";\n\"lng_admin_log_admin_invite_users\" = \"Add users\";\n\"lng_admin_log_admin_invite_link\" = \"Invite users via link\";\n\"lng_admin_log_admin_pin_messages\" = \"Pin messages\";\n\"lng_admin_log_banned_edit_rank\" = \"Edit own tags\";\n\"lng_admin_log_banned_edit_rank_single\" = \"Edit own tag\";\n\"lng_admin_log_admin_manage_topics\" = \"Manage topics\";\n\"lng_admin_log_admin_create_topics\" = \"Create topics\";\n\"lng_admin_log_admin_manage_calls\" = \"Manage video chats\";\n\"lng_admin_log_admin_manage_calls_channel\" = \"Manage live streams\";\n\"lng_admin_log_admin_manage_direct\" = \"Manage direct messages\";\n\"lng_admin_log_admin_manage_ranks\" = \"Edit member tags\";\n\"lng_admin_log_admin_add_admins\" = \"Add new admins\";\n\"lng_admin_log_subscription_extend\" = \"{name} renewed subscription until {date}\";\n\n\"lng_admin_log_changed_rank_from\" = \"{from} changed tag for {user} from \\\"{previous}\\\" to \\\"{tag}\\\"\";\n\"lng_admin_log_set_rank\" = \"{from} set tag for {user} to \\\"{tag}\\\"\";\n\"lng_admin_log_removed_rank\" = \"{from} removed tag for {user} (was \\\"{previous}\\\")\";\n\"lng_admin_log_changed_own_rank_from\" = \"{from} changed own tag from \\\"{previous}\\\" to \\\"{tag}\\\"\";\n\"lng_admin_log_set_own_rank\" = \"{from} set own tag to \\\"{tag}\\\"\";\n\"lng_admin_log_removed_own_rank\" = \"{from} removed own tag (was \\\"{previous}\\\")\";\n\n\"lng_admin_log_antispam_menu_report\" = \"Report False Positive\";\n\"lng_admin_log_antispam_menu_report_toast\" = \"You can manage anti-spam settings in {link}.\";\n\"lng_admin_log_antispam_menu_report_toast_link\" = \"Group Info > Administrators\";\n\n\"lng_terms_signup\" = \"By signing up,\\nyou agree to the {link}.\";\n\"lng_terms_signup_link\" = \"Terms of Service\";\n\"lng_terms_header\" = \"Terms of Service\";\n\"lng_terms_age#one\" = \"I confirm that I am {count} or over\";\n\"lng_terms_age#other\" = \"I confirm that I am {count} or over\";\n\"lng_terms_agree\" = \"Agree & Continue\";\n\"lng_terms_decline\" = \"Decline\";\n\"lng_terms_signup_sorry\" = \"Unfortunately, this means you can't sign up for Telegram.\\n\\nUnlike other apps, Telegram does not use user data for ad targeting or other commercial purposes. Telegram only stores the information it needs to function as a feature-rich cloud service. You can adjust how your data is used (e.g., delete synced contacts) in Privacy & Security settings.\\n\\nIf you are not comfortable with Telegram's modest needs, it won't be possible for us to provide you with this service.\";\n\"lng_terms_update_sorry\" = \"We're very sorry, but this means we must part ways here. Unlike others, we don't use your data for ad targeting or other commercial purposes. Telegram only stores the information it needs to function as a secure and feature-rich cloud service. You can adjust how we use your data in Privacy & Security settings.\\n\\nBut if you're generally not OK with Telegram's modest requirements, it won't be possible for us to provide you with this service. You can delete your account now — or look around some more and delete it later if you feel you're not happy with the way we use your data.\";\n\"lng_terms_decline_and_delete\" = \"Decline & Delete\";\n\"lng_terms_back\" = \"Back\";\n\"lng_terms_delete_warning\" = \"Warning, this will irreversibly delete your Telegram account and all the data you store in the Telegram cloud.\\n\\nImportant: You can Cancel now and export your data first instead of losing it. (To do this, open the latest version of Telegram Desktop and go to Settings > Advanced > Export Telegram data.)\";\n\"lng_terms_delete_now\" = \"Delete now\";\n\"lng_terms_agree_to_proceed\" = \"Agree and proceed to {bot}.\";\n\n\"lng_date_input_day\" = \"Day\";\n\"lng_date_input_month\" = \"Month\";\n\"lng_date_input_year\" = \"Year\";\n\n\"lng_forward_title\" = \"Forward Message\";\n\"lng_forward_many_title#one\" = \"Forward {count} Message\";\n\"lng_forward_many_title#other\" = \"Forward {count} Messages\";\n\"lng_forward_about\" = \"You can remove the sender's name so that this message will look like it was sent by you.\";\n\"lng_forward_many_about\" = \"You can remove the senders’ names so that these messages will look like they were sent by you.\";\n\"lng_forward_show_sender\" = \"Show sender name\";\n\"lng_forward_show_senders\" = \"Show sender names\";\n\"lng_forward_show_caption\" = \"Show caption\";\n\"lng_forward_show_captions\" = \"Show captions\";\n\"lng_forward_change_recipient\" = \"Change recipient\";\n\"lng_forward_sender_names_removed\" = \"Sender names removed\";\n\"lng_forward_header_short\" = \"Forward\";\n\"lng_forward_action_show_sender\" = \"Show Sender Name\";\n\"lng_forward_action_show_senders\" = \"Show Sender Names\";\n\"lng_forward_action_hide_sender\" = \"Hide Sender Name\";\n\"lng_forward_action_hide_senders\" = \"Hide Sender Names\";\n\"lng_forward_action_show_caption\" = \"Show Caption\";\n\"lng_forward_action_show_captions\" = \"Show Captions\";\n\"lng_forward_action_hide_caption\" = \"Hide Caption\";\n\"lng_forward_action_hide_captions\" = \"Hide Captions\";\n\"lng_forward_action_change_recipient\" = \"Change Recipient\";\n\"lng_forward_action_remove\" = \"Do Not Forward\";\n\n\"lng_passport_title\" = \"Telegram Passport\";\n\"lng_passport_request1\" = \"{bot} requests access to your personal data\";\n\"lng_passport_request2\" = \"to sign you up for their services\";\n\"lng_passport_create_password\" = \"Please create a password which will be used\\nto encrypt your personal data.\";\n\"lng_passport_about_password\" = \"This password will also be required whenever\\nyou log in to a new device.\";\n\"lng_passport_password_create\" = \"Create a password\";\n\"lng_passport_email_validate\" = \"Validate\";\n\"lng_passport_code_sent\" = \"A confirmation code was sent to\\n{email}\";\n\"lng_passport_stop_password_sure\" = \"Are you sure you want to cancel setting up your password?\";\n\"lng_passport_password_placeholder\" = \"Your password\";\n\"lng_passport_next\" = \"Next\";\n\"lng_passport_password_wrong\" = \"The password you entered is not valid.\";\n\"lng_passport_header\" = \"Requested information\";\n\"lng_passport_identity_title\" = \"Identity document\";\n\"lng_passport_identity_description\" = \"Upload proof of your identity\";\n\"lng_passport_identity_passport\" = \"Passport\";\n\"lng_passport_identity_passport_upload\" = \"Upload a scan of your passport\";\n\"lng_passport_identity_card\" = \"Identity card\";\n\"lng_passport_identity_card_upload\" = \"Upload a scan of your identity card\";\n\"lng_passport_identity_license\" = \"Driver's licence\";\n\"lng_passport_identity_license_upload\" = \"Upload a scan of your driver's license\";\n\"lng_passport_identity_internal\" = \"Internal passport\";\n\"lng_passport_identity_internal_upload\" = \"Upload a scan of your internal passport\";\n\"lng_passport_identity_about\" = \"The document must contain your photograph, first and last name, date of birth, document number, country of issue, and expiry date.\";\n\"lng_passport_address_title\" = \"Residential address\";\n\"lng_passport_address_description\" = \"Upload proof of your address\";\n\"lng_passport_address_bill\" = \"Utility bill\";\n\"lng_passport_address_bill_upload\" = \"Upload a scan of your utility bill\";\n\"lng_passport_address_statement\" = \"Bank statement\";\n\"lng_passport_address_statement_upload\" = \"Upload a scan of your bank statement\";\n\"lng_passport_address_agreement\" = \"Tenancy agreement\";\n\"lng_passport_address_agreement_upload\" = \"Upload a scan of your tenancy agreement\";\n\"lng_passport_address_registration\" = \"Passport registration\";\n\"lng_passport_address_registration_upload\" = \"Upload a scan of your passport registration page\";\n\"lng_passport_address_temporary\" = \"Temporary registration\";\n\"lng_passport_address_temporary_upload\" = \"Upload a scan of your temporary registration\";\n\"lng_passport_address_about\" = \"To confirm your address, please upload a scan or photo of the selected document (all pages).\";\n\"lng_passport_or_title\" = \"{document} or {second_document}\";\n\"lng_passport_document_type\" = \"Please choose the type of your document:\";\n\"lng_passport_upload_document\" = \"Upload document\";\n\"lng_passport_phone_title\" = \"Phone number\";\n\"lng_passport_phone_description\" = \"Enter your phone number\";\n\"lng_passport_email_title\" = \"Email\";\n\"lng_passport_email_description\" = \"Enter your email address\";\n\"lng_passport_identity_selfie\" = \"Take a selfie with your document\";\n\"lng_passport_translation_needed\" = \"Add an English translation of your document\";\n\"lng_passport_accept_allow\" = \"You accept the {policy} and allow their {bot} to send you messages.\";\n\"lng_passport_allow\" = \"You allow {bot} to send you messages.\";\n\"lng_passport_policy\" = \"{bot} privacy policy\";\n\"lng_passport_authorize\" = \"Authorize\";\n\"lng_passport_form_error\" = \"Could not get authorization form.\";\n\"lng_passport_save_value\" = \"Save\";\n\"lng_passport_scan_index\" = \"Scan {index}\";\n\"lng_passport_upload_scans\" = \"Upload scans\";\n\"lng_passport_upload_more\" = \"Upload additional scans\";\n\"lng_passport_selfie_title\" = \"Selfie\";\n\"lng_passport_selfie_description\" = \"Upload a photo of yourself holding your document. Make sure the ID and your face are clearly visible.\";\n\"lng_passport_upload_selfie\" = \"Upload selfie\";\n\"lng_passport_reupload_selfie\" = \"Reupload selfie\";\n\"lng_passport_front_side_title\" = \"Front side\";\n\"lng_passport_front_side_description\" = \"Upload a photo of the front side of the document.\";\n\"lng_passport_upload_front_side\" = \"Upload a scan of the front side\";\n\"lng_passport_reupload_front_side\" = \"Reupload a scan of the front side\";\n\"lng_passport_reverse_side_title\" = \"Reverse side\";\n\"lng_passport_reverse_side_description\" = \"Upload a photo of the reverse side of the document.\";\n\"lng_passport_upload_reverse_side\" = \"Upload a scan of the reverse side\";\n\"lng_passport_reupload_reverse_side\" = \"Reupload a scan of the reverse side\";\n\"lng_passport_main_page_title\" = \"Main page\";\n\"lng_passport_main_page_description\" = \"Upload a scan of the main page of your document.\";\n\"lng_passport_upload_main_page\" = \"Upload a scan of the main page\";\n\"lng_passport_reupload_main_page\" = \"Reupload a scan of the main page\";\n\"lng_passport_personal_details\" = \"Personal details\";\n\"lng_passport_personal_details_enter\" = \"Fill in your personal details\";\n\"lng_passport_document_details\" = \"Document details\";\n\"lng_passport_choose_image\" = \"Choose scan image\";\n\"lng_passport_delete_scan_undo\" = \"Undo\";\n\"lng_passport_scan_uploaded\" = \"Uploaded on {date}\";\n\"lng_passport_first_name\" = \"First name\";\n\"lng_passport_middle_name\" = \"Middle name\";\n\"lng_passport_last_name\" = \"Last name\";\n\"lng_passport_birth_date\" = \"Date of birth\";\n\"lng_passport_gender\" = \"Gender\";\n\"lng_passport_gender_male\" = \"Male\";\n\"lng_passport_gender_female\" = \"Female\";\n\"lng_passport_country\" = \"Citizenship\";\n\"lng_passport_residence_country\" = \"Residence\";\n\"lng_passport_country_choose\" = \"Choose country\";\n\"lng_passport_document_number\" = \"Document Number\";\n\"lng_passport_expiry_date\" = \"Expiry date\";\n\"lng_passport_native_name_title\" = \"Name in country of residence\";\n\"lng_passport_native_name_about\" = \"Your name in the language of your country of residence ({country}).\";\n\"lng_passport_native_name_language\" = \"Your name in {language}\";\n\"lng_passport_native_name_language_about\" = \"Your name in the language of your country of residence.\";\n\"lng_passport_address\" = \"Address\";\n\"lng_passport_address_enter\" = \"Provide your address\";\n\"lng_passport_street\" = \"Street\";\n\"lng_passport_city\" = \"City\";\n\"lng_passport_state\" = \"State\";\n\"lng_passport_postcode\" = \"Postcode\";\n\"lng_passport_translation\" = \"Translation\";\n\"lng_passport_use_existing\" = \"USE {existing}\";\n\"lng_passport_use_existing_phone\" = \"Use the same phone number as on Telegram.\";\n\"lng_passport_new_phone\" = \"Or enter a new phone number\";\n\"lng_passport_new_phone_code\" = \"Note: You will receive a confirmation code on the phone number you provide.\";\n\"lng_passport_use_existing_email\" = \"Use the same email address as on Telegram.\";\n\"lng_passport_new_email\" = \"Or enter a new email\";\n\"lng_passport_new_email_code\" = \"Note: You will receive a confirmation code to the email address you provide.\";\n\"lng_passport_confirm_phone\" = \"We've sent an SMS with a confirmation code to your phone {phone}.\";\n\"lng_passport_confirm_email\" = \"We've sent a confirmation code to your email {email}.\";\n\"lng_passport_sure_cancel\" = \"If you continue, your changes will be lost.\";\n\"lng_passport_scans_limit_reached\" = \"Sorry, that's too many scans for one document.\";\n\"lng_passport_delete_document\" = \"Delete document\";\n\"lng_passport_delete_document_sure\" = \"Are you sure you want to delete this document?\";\n\"lng_passport_delete_details\" = \"Delete personal details\";\n\"lng_passport_delete_details_sure\" = \"Are you sure you want to delete your personal details?\";\n\"lng_passport_delete_address\" = \"Delete address information\";\n\"lng_passport_delete_address_sure\" = \"Are you sure you want to delete your address information?\";\n\"lng_passport_delete_email\" = \"Delete email\";\n\"lng_passport_delete_email_sure\" = \"Are you sure you want to delete your email address?\";\n\"lng_passport_delete_phone\" = \"Delete phone number\";\n\"lng_passport_delete_phone_sure\" = \"Are you sure you want to delete your phone number?\";\n\"lng_passport_success\" = \"Authorization successful!\";\n\"lng_passport_stop_sure\" = \"Are you sure you want to stop this authorization?\";\n\"lng_passport_stop\" = \"Stop\";\n\"lng_passport_restart_sure\" = \"An unexpected error has occurred. Perhaps some changes were made from a different Telegram application. Would you like to restart this authorization?\";\n\"lng_passport_restart\" = \"Restart\";\n\"lng_passport_error_too_large\" = \"Sorry, this file is too large.\";\n\"lng_passport_error_bad_size\" = \"Sorry, this image has wrong dimensions.\";\n\"lng_passport_error_cant_read\" = \"Can't read this file. Please choose an image.\";\n\"lng_passport_bad_name\" = \"Please use latin characters only.\";\n\"lng_passport_wait_upload\" = \"Please wait until the file has finished uploading.\";\n\"lng_passport_app_out_of_date\" = \"Sorry, your Telegram app is out of date and can't handle this request. Please update Telegram.\";\n\n\"lng_export_title\" = \"Export Your Data\";\n\"lng_export_progress_title\" = \"Exporting your data\";\n\"lng_export_option_info\" = \"Account information\";\n\"lng_export_option_info_about\" = \"Your chosen display name, username, phone number and profile photos.\";\n\"lng_export_option_contacts\" = \"Contacts list\";\n\"lng_export_option_contacts_about\" = \"If you allow access, contacts are continuously synced with Telegram. You can adjust this in Settings > Privacy & Security on mobile devices.\";\n\"lng_export_option_stories\" = \"Story archive\";\n\"lng_export_option_stories_about\" = \"All stories you posted from Telegram mobile apps.\";\n\"lng_export_option_profile_music\" = \"Music on Profiles\";\n\"lng_export_option_profile_music_about\" = \"All tracks you saved to your playlist.\";\n\"lng_export_option_sessions\" = \"Active sessions\";\n\"lng_export_option_sessions_about\" = \"We may store this to display your connected devices in Settings > Privacy & Security > Show all sessions.\";\n\"lng_export_header_other\" = \"Other\";\n\"lng_export_option_other\" = \"Miscellaneous data\";\n\"lng_export_option_other_about\" = \"Other types of data not mentioned above (beta).\";\n\"lng_export_header_chats\" = \"Chat export settings\";\n\"lng_export_header_topic\" = \"Topic export settings\";\n\"lng_export_option_personal_chats\" = \"Personal chats\";\n\"lng_export_option_bot_chats\" = \"Bot chats\";\n\"lng_export_option_private_groups\" = \"Private groups\";\n\"lng_export_option_private_channels\" = \"Private channels\";\n\"lng_export_option_public_groups\" = \"Public groups\";\n\"lng_export_option_public_channels\" = \"Public channels\";\n\"lng_export_option_only_my\" = \"Only my messages\";\n\"lng_export_header_media\" = \"Media export settings\";\n\"lng_export_option_photos\" = \"Photos\";\n\"lng_export_option_video_files\" = \"Videos\";\n\"lng_export_option_voice_messages\" = \"Voice messages\";\n\"lng_export_option_video_messages\" = \"Video messages\";\n\"lng_export_option_stickers\" = \"Stickers\";\n\"lng_export_option_gifs\" = \"GIFs\";\n\"lng_export_option_files\" = \"Files\";\n\"lng_export_option_size_limit\" = \"Size limit: {size}\";\n\"lng_export_header_format\" = \"Location and format\";\n\"lng_export_option_location\" = \"Download path: {path}\";\n\"lng_export_option_format_location\" = \"Format: {format}, Path: {path}\";\n\"lng_export_option_choose_format\" = \"Choose export format\";\n\"lng_export_option_html\" = \"Human-readable HTML\";\n\"lng_export_option_json\" = \"Machine-readable JSON\";\n\"lng_export_option_html_and_json\" = \"Both\";\n\"lng_export_limits\" = \"From: {from}, to: {till}\";\n\"lng_export_beginning\" = \"the oldest message\";\n\"lng_export_end\" = \"present\";\n\"lng_export_from_beginning\" = \"Reset\";\n\"lng_export_till_end\" = \"Reset\";\n\"lng_export_start\" = \"Export\";\n\"lng_export_state_initializing\" = \"Initializing...\";\n\"lng_export_state_userpics\" = \"Profile photos\";\n\"lng_export_state_chats_list\" = \"Processing chats...\";\n\"lng_export_state_chats\" = \"Chats\";\n\"lng_export_skip_file\" = \"Skip this file\";\n\"lng_export_progress\" = \"You can close this window now. Please don't quit Telegram until the data export is completed.\";\n\"lng_export_stop\" = \"Stop\";\n\"lng_export_sure_stop\" = \"Are you sure you want to stop exporting your data?\\n\\nIf you do, you'll need to start over.\";\n\"lng_export_about_done\" = \"Your data was successfully exported.\";\n\"lng_export_done\" = \"Show my data\";\n\"lng_export_finished\" = \"Data export completed.\";\n\"lng_export_total_amount\" = \"Total files: {amount}\";\n\"lng_export_total_size\" = \"Total size: {size}\";\n\"lng_export_folder\" = \"Choose export folder\";\n\"lng_export_invalid\" = \"Sorry, you started a new data export, so this data export has been canceled.\";\n\"lng_export_delay\" = \"For security reasons, you will be able to begin downloading your data in {hours}. We have notified all your devices about the export request to make sure it's authorized and give you time to react if it's not.\\n\\nPlease come back on {date} and repeat the request using the same device.\";\n\"lng_export_delay_less_than_hour\" = \"less than an hour\";\n\"lng_export_suggest_title\" = \"Data export ready\";\n\"lng_export_suggest_text\" = \"You can now download the data you requested. Start exporting data?\";\n\"lng_export_suggest_cancel\" = \"Not now\";\n\"lng_export_about_telegram\" = \"Here is the data you requested. Remember: Telegram is ad free, it doesn't use your data for ad targeting and doesn't sell it to others. Telegram only keeps the information it needs to function as a secure and feature-rich cloud service.\\n\\nCheck out Settings > Privacy & Security on Telegram's mobile apps for the relevant settings.\";\n\"lng_export_about_contacts\" = \"If you allow access, your contacts are continuously synced with Telegram. Thanks to this, you can easily switch to Telegram and immediately connect with friends across all your devices. We use data about your contacts to let you know when they join Telegram, and to display them by the name you set for them in your phone.\\n\\nYou can disable contact syncing or delete your stored contacts in Settings > Privacy & Security on Telegram's mobile apps.\";\n\"lng_export_about_frequent\" = \"This rating shows which people you are likelier to message frequently. Telegram uses this data to populate the 'People' box at the top of the Search section. This rating is also calculated for inline bots so that the app can suggest the bots you are most likely to use in the attachment menu (or when you start a new message with \\\"@\\\").\\n\\nTo delete this data, go to Settings > Privacy & Security and disable 'Suggest Frequent Contacts' (requires Telegram for iOS v.4.8.3 or Telegram for Android v.4.8.10 or higher).\";\n\"lng_export_about_sessions\" = \"We store session info to display your connected devices in Settings > Privacy & Security > Active Sessions.\";\n\"lng_export_about_web_sessions\" = \"We store this to display the websites where you logged in using authentication via Telegram. This information is shown in Settings > Privacy & Security > Active Sessions.\";\n\"lng_export_about_chats\" = \"This page lists all chats from this export.\";\n\"lng_export_about_left_chats\" = \"Below are the supergroups and channels from this export that you've left or where you were banned.\\n\\nNote that when you leave a channel or supergroup you've created, you have the option to either delete it, or simply leave (in case you want to rejoin later, or keep the community alive despite not being a member).\";\n\n\"lng_language_switch_title\" = \"Change language?\";\n\"lng_language_switch_about_official\" = \"You are about to apply a language pack ({lang_name}) that is {percent}% complete.\\n\\nThis will translate the entire interface. You can suggest corrections in the {link}.\\n\\nYou can change your language back at any time in Settings.\";\n\"lng_language_switch_about_unofficial\" = \"You are about to apply a custom language pack with the name \\\"{lang_name}\\\" that is {percent}% complete.\\n\\nThis will translate the entire interface. You can suggest corrections on the {link}.\\n\\nYou can change your language back at any time in Settings.\";\n\"lng_language_switch_link\" = \"translation platform\";\n\"lng_language_switch_apply\" = \"Apply Language\";\n\"lng_language_not_found\" = \"Sorry, this language pack doesn't exist.\";\n\"lng_language_already\" = \"You're already using this language pack. You can change your language back at any time in Settings.\";\n\"lng_language_not_ready_title\" = \"Insufficient data\";\n\"lng_language_not_ready_about\" = \"Unfortunately, this custom language pack ({lang_name}) doesn't contain data for Telegram Desktop. You can contribute to this language pack using the {link}.\";\n\"lng_language_not_ready_link\" = \"translation platform\";\n\n\"lng_translate_box_error\" = \"Translate failed.\";\n\"lng_translate_box_error_language_pack_not_installed\" = \"Translation requires a local language pack. Download it in System Settings.\";\n\n\"lng_translate_settings_subtitle\" = \"Translate Messages\";\n\"lng_translate_settings_show\" = \"Show Translate Button\";\n\"lng_translate_settings_use_platform_mac\" = \"Use Apple Translations\";\n\"lng_translate_settings_use_platform_mac_about\" = \"Translation on macOS won't work until you download local language packs in System Settings.\";\n\"lng_translate_settings_use_platform_linux\" = \"Use KDE's Crow Translate\";\n\"lng_translate_settings_chat\" = \"Translate Entire Chats\";\n\"lng_translate_settings_choose\" = \"Do Not Translate\";\n\"lng_translate_settings_about\" = \"The 'Translate' button will appear in the context menu of messages containing text.\";\n\"lng_translate_settings_one\" = \"Please choose at least one language so that it can be used as the \\\"Translate to\\\" language.\";\n\n\"lng_launch_exe_warning\" = \"This file has the extension {extension}\\nIt may harm your computer.\\nAre you sure you want to run it?\";\n\"lng_launch_other_warning\" = \"This file has {extension} extension.\\nAre you sure you want to open it?\";\n\"lng_launch_svg_warning\" = \"Opening this file can potentially expose your IP address to its creator. Continue?\";\n\"lng_launch_exe_sure\" = \"Run\";\n\"lng_launch_other_sure\" = \"Open\";\n\"lng_launch_exe_dont_ask\" = \"Don't ask me again\";\n\"lng_launch_dont_ask\" = \"Remember for this file type\";\n\"lng_launch_dont_ask_settings\" = \"You can later edit trusted file types in Settings > Privacy and Security > File open confirmations.\";\n\n\"lng_polls_anonymous\" = \"Anonymous Poll\";\n\"lng_polls_public\" = \"Poll\";\n\"lng_polls_anonymous_quiz\" = \"Anonymous Quiz\";\n\"lng_polls_public_quiz\" = \"Quiz\";\n\"lng_polls_closed\" = \"Final results\";\n\"lng_polls_votes_count#one\" = \"{count} vote\";\n\"lng_polls_votes_count#other\" = \"{count} votes\";\n\"lng_polls_votes_none\" = \"No votes\";\n\"lng_polls_answers_count#one\" = \"{count} answer\";\n\"lng_polls_answers_count#other\" = \"{count} answers\";\n\"lng_polls_answers_none\" = \"No answers\";\n\"lng_polls_submit_votes\" = \"Vote\";\n\"lng_polls_view_results\" = \"View results\";\n\"lng_polls_view_votes#one\" = \"View Votes ({count})\";\n\"lng_polls_view_votes#other\" = \"View Votes ({count})\";\n\"lng_polls_admin_votes#one\" = \"{count} vote {arrow}\";\n\"lng_polls_admin_votes#other\" = \"{count} votes {arrow}\";\n\"lng_polls_admin_back_vote\" = \"{arrow} Vote\";\n\"lng_polls_ends_in_days#one\" = \"ends in {count} day\";\n\"lng_polls_ends_in_days#other\" = \"ends in {count} days\";\n\"lng_polls_results_in_days#one\" = \"results in {count} day\";\n\"lng_polls_results_in_days#other\" = \"results in {count} days\";\n\"lng_polls_ends_in_time\" = \"ends in {time}\";\n\"lng_polls_results_in_time\" = \"results in {time}\";\n\"lng_polls_results_after_close\" = \"Results will appear after the poll ends.\";\n\"lng_polls_retract\" = \"Retract vote\";\n\"lng_polls_stop\" = \"Stop poll\";\n\"lng_polls_stop_warning\" = \"If you stop this poll now, nobody will be able to vote in it anymore. This action cannot be undone.\";\n\"lng_polls_stop_sure\" = \"Stop\";\n\"lng_polls_menu_item\" = \"Poll\";\n\"lng_polls_create\" = \"Create poll\";\n\"lng_polls_create_title\" = \"New poll\";\n\"lng_polls_create_question\" = \"Question\";\n\"lng_polls_create_question_placeholder\" = \"Ask a question\";\n\"lng_polls_create_description_placeholder\" = \"Add Description (optional)\";\n\"lng_polls_create_options\" = \"Poll options\";\n\"lng_polls_create_option_add\" = \"Add an option...\";\n\"lng_polls_create_limit#one\" = \"You can add {count} more option.\";\n\"lng_polls_create_limit#other\" = \"You can add {count} more options.\";\n\"lng_polls_create_maximum\" = \"You have added the maximum number of options.\";\n\"lng_polls_create_settings\" = \"Settings\";\n\"lng_polls_create_show_who_voted\" = \"Show Who Voted\";\n\"lng_polls_create_show_who_voted_about\" = \"Display voter name on each option.\";\n\"lng_polls_create_allow_multiple_answers\" = \"Allow Multiple Answers\";\n\"lng_polls_create_allow_multiple_answers_about\" = \"Voters can select more than one option.\";\n\"lng_polls_create_allow_adding_options\" = \"Allow Adding Options\";\n\"lng_polls_create_allow_adding_options_about\" = \"Participants can suggest new options.\";\n\"lng_polls_create_allow_revoting\" = \"Allow Revoting\";\n\"lng_polls_create_allow_revoting_about\" = \"Voters can change their vote.\";\n\"lng_polls_create_shuffle_options\" = \"Shuffle Options\";\n\"lng_polls_create_shuffle_options_about\" = \"Answers appear in random order for each voter.\";\n\"lng_polls_create_set_correct_answer\" = \"Set Correct Answer\";\n\"lng_polls_create_set_correct_answer_about\" = \"Mark one option as the right answer.\";\n\"lng_polls_create_set_correct_answer_about_multi\" = \"Mark one or more options as the right answer.\";\n\"lng_polls_create_limit_duration\" = \"Limit Duration\";\n\"lng_polls_create_limit_duration_about\" = \"Automatically close the poll at a set time.\";\n\"lng_polls_create_poll_duration\" = \"Poll Duration\";\n\"lng_polls_create_poll_ends\" = \"Poll ends\";\n\"lng_polls_create_hide_results\" = \"Hide results\";\n\"lng_polls_create_hide_results_about\" = \"If you switch this on, results will appear only after the poll closes.\";\n\"lng_polls_create_duration_custom\" = \"Custom\";\n\"lng_polls_create_deadline_title\" = \"Deadline\";\n\"lng_polls_create_deadline_button\" = \"Set Deadline\";\n\"lng_polls_create_deadline_expired\" = \"The poll deadline has already passed. Please choose a new time.\";\n\"lng_polls_create_anonymous\" = \"Anonymous Voting\";\n\"lng_polls_create_multiple_choice\" = \"Multiple Answers\";\n\"lng_polls_create_quiz_mode\" = \"Quiz Mode\";\n\"lng_polls_create_button\" = \"Create\";\n\"lng_polls_create_one_answer\" = \"Quiz has only one right answer.\";\n\"lng_polls_choose_question\" = \"Please enter a question.\";\n\"lng_polls_choose_answers\" = \"Please enter at least two options.\";\n\"lng_polls_choose_correct\" = \"Please choose the correct answer.\";\n\"lng_polls_solution_title\" = \"Explanation\";\n\"lng_polls_solution_placeholder\" = \"Add a Comment (Optional)\";\n\"lng_polls_solution_about\" = \"Users will see this comment after choosing a wrong answer, good for educational purposes.\";\n\"lng_polls_media_uploading_toast_title\" = \"Please wait\";\n\"lng_polls_media_uploading_toast\" = \"Poll media is still uploading...\";\n\"lng_polls_ends_toast\" = \"Results will appear after the poll ends.\";\n\n\"lng_polls_poll_results_title\" = \"Poll results\";\n\"lng_polls_quiz_results_title\" = \"Quiz results\";\n\"lng_polls_show_more#one\" = \"Show more ({count})\";\n\"lng_polls_show_more#other\" = \"Show more ({count})\";\n\"lng_polls_votes_collapse\" = \"Collapse\";\n\"lng_polls_vote_yesterday\" = \"yesterday\";\n\"lng_polls_option_added_by\" = \"Added by {user}\";\n\"lng_polls_context_ends\" = \"Results will appear after the poll ends.\";\n\"lng_polls_add_option\" = \"Add an Option\";\n\"lng_polls_add_option_placeholder\" = \"Option text...\";\n\"lng_polls_max_options_reached\" = \"Maximum number of options reached.\";\n\"lng_polls_add_option_duplicate\" = \"This option already exists.\";\n\"lng_polls_add_option_closed\" = \"This poll has been closed.\";\n\"lng_polls_add_option_error\" = \"Could not add the option. Please try again.\";\n\"lng_polls_add_option_save\" = \"Save\";\n\n\"lng_todo_title\" = \"Checklist\";\n\"lng_todo_title_group\" = \"Group Checklist\";\n\"lng_todo_title_user\" = \"Checklist\";\n\"lng_todo_completed#one\" = \"{count} of {total} completed\";\n\"lng_todo_completed#other\" = \"{count} of {total} completed\";\n\"lng_todo_completed_none\" = \"None of {total} completed\";\n\"lng_todo_menu_item\" = \"Checklist\";\n\"lng_todo_create\" = \"Create Checklist\";\n\"lng_todo_create_title\" = \"New Checklist\";\n\"lng_todo_create_title_placeholder\" = \"Title\";\n\"lng_todo_create_list\" = \"Tasks List\";\n\"lng_todo_create_list_add\" = \"Add a task...\";\n\"lng_todo_create_limit#one\" = \"You can add {count} more task.\";\n\"lng_todo_create_limit#other\" = \"You can add {count} more tasks.\";\n\"lng_todo_create_maximum\" = \"You have added the maximum number of tasks.\";\n\"lng_todo_create_settings\" = \"Settings\";\n\"lng_todo_create_allow_add\" = \"Allow Others to Add Tasks\";\n\"lng_todo_create_allow_mark\" = \"Allow Others to Mark As Done\";\n\"lng_todo_create_button\" = \"Create\";\n\"lng_todo_choose_title\" = \"Please enter a title.\";\n\"lng_todo_choose_tasks\" = \"Please enter at least one task.\";\n\n\"lng_todo_add_title\" = \"Add Tasks\";\n\"lng_todo_create_premium\" = \"Only subscribers of {link} can create Checklists.\";\n\"lng_todo_add_premium\" = \"Only subscribers of {link} can add tasks.\";\n\"lng_todo_mark_premium\" = \"Only subscribers of {link} can mark tasks as done.\";\n\"lng_todo_premium_link\" = \"Telegram Premium\";\n\"lng_todo_mark_restricted\" = \"{user} has restricted others from marking tasks as done.\";\n\"lng_todo_mark_forwarded\" = \"You can't change forwarded checklists.\";\n\n\"lng_outdated_title\" = \"PLEASE UPDATE YOUR OPERATING SYSTEM.\";\n\"lng_outdated_title_bits\" = \"PLEASE SWITCH TO A 64-BIT OPERATING SYSTEM.\";\n\"lng_outdated_soon\" = \"Otherwise, Telegram Desktop will stop updating on {date}.\";\n\"lng_outdated_now\" = \"So Telegram Desktop can update to newer versions.\";\n\n\"lng_screen_reader_bar_text\" = \"Telegram is working in Screen Reader mode.\";\n\"lng_screen_reader_bar_disable\" = \"Disable\";\n\"lng_screen_reader_confirm_text\" = \"Telegram detected accessibility software is being used in your system and it is working in Screen Reader friendly mode.\\n\\nThis may result in unexpected changes to the way Telegram user interface works.\\n\\nIf you do not use Screen Reader software with Telegram, please disable this mode.\";\n\"lng_screen_reader_confirm_disable\" = \"Disable\";\n\"lng_screen_reader_settings_title\" = \"Screen reader\";\n\"lng_screen_reader_settings_disable\" = \"Disable screen reader mode\";\n\n\"lng_filters_all\" = \"All chats\";\n\"lng_filters_all_short\" = \"All\";\n\"lng_filters_setup\" = \"Edit\";\n\"lng_filters_title\" = \"Folders\";\n\"lng_filters_subtitle\" = \"My folders\";\n\"lng_filters_no_chats\" = \"No chats\";\n\"lng_filters_chats_count#one\" = \"{count} chat\";\n\"lng_filters_chats_count#other\" = \"{count} chats\";\n\"lng_filters_create\" = \"Create new folder\";\n\"lng_filters_about\" = \"Create folders for different groups of chats and quickly switch between them.\";\n\"lng_filters_recommended\" = \"Recommended folders\";\n\"lng_filters_recommended_add\" = \"Add\";\n\"lng_filters_restore\" = \"Undo\";\n\"lng_filters_new\" = \"New Folder\";\n\"lng_filters_edit\" = \"Edit Folder\";\n\"lng_filters_setup_menu\" = \"Edit Folders\";\n\"lng_filters_new_name\" = \"Folder name\";\n\"lng_filters_enable_animations\" = \"Enable animations\";\n\"lng_filters_disable_animations\" = \"Disable animations\";\n\"lng_filters_add_chats\" = \"Add Chats\";\n\"lng_filters_remove_chats\" = \"Add Chats to Exclude\";\n\"lng_filters_include\" = \"Included chats\";\n\"lng_filters_include_about\" = \"Choose chats or types of chats that will appear in this folder.\";\n\"lng_filters_exclude\" = \"Excluded chats\";\n\"lng_filters_exclude_about\" = \"Choose chats or types of chats that will not appear in this folder.\";\n\"lng_filters_create_button\" = \"Create\";\n\"lng_filters_include_title\" = \"Include Chats\";\n\"lng_filters_exclude_title\" = \"Exclude Chats\";\n\"lng_filters_edit_types\" = \"Chat types\";\n\"lng_filters_edit_chats\" = \"Chats\";\n\"lng_filters_include_contacts\" = \"Contacts\";\n\"lng_filters_include_groups\" = \"Groups\";\n\"lng_filters_include_channels\" = \"Channels\";\n\"lng_filters_include_bots\" = \"Bots\";\n\"lng_filters_name_people\" = \"People\";\n\"lng_filters_name_unread\" = \"Unread\";\n\"lng_filters_name_unmuted\" = \"Unmuted\";\n\"lng_filters_empty\" = \"Please choose at least one chat for this folder.\";\n\"lng_filters_default\" = \"Please change at least one rule for this folder.\";\n\"lng_filters_type_contacts\" = \"Contacts\";\n\"lng_filters_type_non_contacts\" = \"Non-Contacts\";\n\"lng_filters_type_groups\" = \"Groups\";\n\"lng_filters_type_channels\" = \"Channels\";\n\"lng_filters_type_new\" = \"New Chats\";\n\"lng_filters_type_existing\" = \"Existing Chats\";\n\"lng_filters_type_bots\" = \"Bots\";\n\"lng_filters_type_no_archived\" = \"Archived\";\n\"lng_filters_type_no_muted\" = \"Muted\";\n\"lng_filters_type_no_read\" = \"Read\";\n\"lng_filters_icon_header\" = \"Choose an icon\";\n\"lng_filters_context_edit\" = \"Edit folder\";\n\"lng_filters_context_remove\" = \"Remove\";\n\"lng_filters_remove_sure\" = \"This will remove the folder, your chats will not be deleted.\";\n\"lng_filters_remove_yes\" = \"Remove\";\n\"lng_filters_menu_add\" = \"Add to folder\";\n\"lng_filters_toast_add\" = \"{chat} added to {folder} folder\";\n\"lng_filters_toast_remove\" = \"{chat} removed from {folder} folder\";\n\"lng_filters_shareable_status\" = \"shareable folder\";\n\"lng_filters_view_subtitle\" = \"Tabs view\";\n\"lng_filters_vertical\" = \"Tabs on the left\";\n\"lng_filters_horizontal\" = \"Tabs at the top\";\n\"lng_filters_enable_tags\" = \"Show Folder Tags\";\n\"lng_filters_enable_tags_about\" = \"Display folder names for each chat in the chat list.\";\n\"lng_filters_enable_tags_about_premium\" = \"Subscribe to **{link}** to display folder names for each chat in the chat list.\";\n\"lng_filters_tag_color_subtitle\" = \"Folder color in chat list\";\n\"lng_filters_tag_color_about\" = \"Choose a color for the tag of this folder.\";\n\"lng_filters_tag_color_no\" = \"No Tag\";\n\n\"lng_filters_delete_sure\" = \"Are you sure you want to delete this folder? This will also deactivate all the invite links created to share this folder.\";\n\"lng_filters_link\" = \"Share Folder\";\n\"lng_filters_link_has\" = \"Invite links\";\n\n\"lng_filters_checkbox_remove_bot\" = \"Remove bot from all folders\";\n\"lng_filters_checkbox_remove_group\" = \"Remove group from all folders\";\n\"lng_filters_checkbox_remove_channel\" = \"Remove channel from all folders\";\n\n\"lng_filters_link_create\" = \"Create an Invite Link\";\n\"lng_filters_link_cant\" = \"You can’t share folders which include or exclude specific chat types like 'Groups', 'Contacts', etc.\";\n\"lng_filters_link_about\" = \"Share access to some of this folder's groups and channels with others.\";\n\"lng_filters_link_about_many\" = \"Create more links to set up different access levels for different people.\";\n\"lng_filters_link_title\" = \"Share Folder\";\n\"lng_filters_link_share_about\" = \"Anyone with this link can add the {folder} folder and the chats selected below.\";\n\"lng_filters_link_subtitle\" = \"Invite link\";\n\"lng_filters_link_chats_none\" = \"No chats selected\";\n\"lng_filters_link_chats#one\" = \"{count} chat selected\";\n\"lng_filters_link_chats#other\" = \"{count} chats selected\";\n\"lng_filters_link_bot_status\" = \"chats with bots can't be shared\";\n\"lng_filters_link_bot_error\" = \"Chats with bots can't be shared.\";\n\"lng_filters_link_private_status\" = \"you can't share private chats\";\n\"lng_filters_link_private_error\" = \"Private chats can't be shared.\";\n\"lng_filters_link_noadmin_status\" = \"you can't invite users here\";\n\"lng_filters_link_noadmin_group_error\" = \"You don't have the admin rights to share invite links to this private group.\";\n\"lng_filters_link_noadmin_channel_error\" = \"You don't have the admin rights to share invite links to this private channel.\";\n\"lng_filters_link_already_group\" = \"you are already a member\";\n\"lng_filters_link_already_channel\" = \"you are already subscribed\";\n\"lng_filters_link_inaccessible\" = \"chat is inaccessible\";\n\"lng_filters_link_chats_about\" = \"Select groups and channels that you want everyone who adds the folder via invite link to join.\";\n\"lng_filters_link_no_about\" = \"There are no chats in this folder that you can share with others.\";\n\"lng_filters_link_chats_no\" = \"These chats cannot be shared\";\n\"lng_filters_link_chats_no_about\" = \"You can only share groups and channels in which you are allowed to create invite links.\";\n\"lng_filters_link_name_it\" = \"Name Link\";\n\"lng_filters_link_delete_sure\" = \"Are you sure you want to delete this link?\";\n\"lng_filters_link_qr_about\" = \"Anyone on Telegram can scan this code to add the folder and join all the chats included in its invite link.\";\n\"lng_filters_link_group_admin_error\" = \"One of the groups in this folder can’t be added because one of its admins has too many groups and channels.\";\n\"lng_filters_by_link_title\" = \"Add Folder\";\n\"lng_filters_by_link_sure\" = \"Do you want to add the chat folder {folder} and join its groups and channels?\";\n\"lng_filters_by_link_join#one\" = \"{count} chat to join\";\n\"lng_filters_by_link_join#other\" = \"{count} chats to join\";\n\"lng_filters_by_link_add_button\" = \"Add {folder}\";\n\"lng_filters_by_link_add_no\" = \"Do not add this folder\";\n\"lng_filters_by_link_more\" = \"Add Chats to Folder\";\n\"lng_filters_by_link_more_sure\" = \"Do you want to join chats and add them to the folder {folder}?\";\n\"lng_filters_by_link_about\" = \"You can deselect the chats you don't want to join.\";\n\"lng_filters_by_link_and_join_button#one\" = \"Join Chat\";\n\"lng_filters_by_link_and_join_button#other\" = \"Join Chats\";\n\"lng_filters_by_link_join_no\" = \"Do not join any chats\";\n\"lng_filters_by_link_already\" = \"Folder already added\";\n\"lng_filters_by_link_already_about\" = \"You have already added the folder {folder} and all its chats.\";\n\"lng_filters_by_link_in#one\" = \"{count} chat in this folder\";\n\"lng_filters_by_link_in#other\" = \"{count} chats in this folder\";\n\"lng_filters_by_link_remove\" = \"Remove Folder\";\n\"lng_filters_by_link_remove_sure\" = \"Do you also want to quit the chats included in the folder {folder}?\";\n\"lng_filters_by_link_quit#one\" = \"{count} chat to quit\";\n\"lng_filters_by_link_quit#other\" = \"{count} chats to quit\";\n\"lng_filters_by_link_select\" = \"Select All\";\n\"lng_filters_by_link_deselect\" = \"Deselect All\";\n\"lng_filters_by_link_about_quit\" = \"You can deselect the chats you don't want to quit.\";\n\"lng_filters_by_link_remove_button\" = \"Remove Folder and Keep Chats\";\n\"lng_filters_by_link_and_quit_button#one\" = \"Remove Folder and Chat\";\n\"lng_filters_by_link_and_quit_button#other\" = \"Remove Folder and Chats\";\n\"lng_filters_added_title\" = \"Folder {folder} Added\";\n\"lng_filters_added_also#one\" = \"You also joined {count} chat.\";\n\"lng_filters_added_also#other\" = \"You also joined {count} chats.\";\n\"lng_filters_updated_title\" = \"Folder {folder} Updated\";\n\"lng_filters_updated_also#one\" = \"You have joined {count} new chat.\";\n\"lng_filters_updated_also#other\" = \"You have joined {count} new chats.\";\n\"lng_filters_bar_you_can#one\" = \"You can join {count} new chat\";\n\"lng_filters_bar_you_can#other\" = \"You can join {count} new chats\";\n\"lng_filters_bar_view#one\" = \"Click here to view it\";\n\"lng_filters_bar_view#other\" = \"Click here to view them\";\n\n\"lng_chat_theme_change\" = \"Change colors\";\n\"lng_chat_theme_wallpaper\" = \"Set Wallpaper\";\n\"lng_chat_theme_none\" = \"No\\nTheme\";\n\"lng_chat_theme_apply\" = \"Apply Theme\";\n\"lng_chat_theme_change_wallpaper\" = \"Change Wallpaper\";\n\"lng_chat_theme_title\" = \"Select theme\";\n\"lng_chat_theme_cant_voice\" = \"Sorry, you can't change the chat theme while you have an unsent voice message.\";\n\"lng_chat_theme_gift_replace\" = \"This gift is already your theme in the chat with {name}. Remove it there and use it here instead?\";\n\n\"lng_photo_editor_menu_delete\" = \"Delete\";\n\"lng_photo_editor_menu_flip\" = \"Flip\";\n\"lng_photo_editor_menu_duplicate\" = \"Duplicate\";\n\n\"lng_photo_editor_crop_original\" = \"Original\";\n\"lng_photo_editor_crop_square\" = \"Square\";\n\"lng_photo_editor_crop_free\" = \"Free\";\n\n\"lng_voice_speed_slow\" = \"Slow\";\n\"lng_voice_speed_normal\" = \"Normal\";\n\"lng_voice_speed_medium\" = \"Medium\";\n\"lng_voice_speed_fast\" = \"Fast\";\n\"lng_voice_speed_very_fast\" = \"Very fast\";\n\"lng_voice_speed_super_fast\" = \"Super fast\";\n\n\"lng_view_button_user\" = \"Send message\";\n\"lng_view_button_bot\" = \"View bot\";\n\"lng_view_button_group\" = \"View group\";\n\"lng_view_button_channel\" = \"View channel\";\n\"lng_view_button_bot_app\" = \"Launch\";\n\"lng_view_button_background\" = \"View wallpaper\";\n\"lng_view_button_theme\" = \"View theme\";\n\"lng_view_button_message\" = \"View message\";\n\"lng_view_button_story\" = \"View story\";\n\"lng_view_button_voice_chat\" = \"Video chat\";\n\"lng_view_button_voice_chat_channel\" = \"Live stream\";\n\"lng_view_button_request_join\" = \"Request to Join\";\n\"lng_view_button_external_link\" = \"Open link\";\n\"lng_view_button_boost\" = \"Boost\";\n\"lng_view_button_giftcode\" = \"Open\";\n\"lng_view_button_iv\" = \"Instant View\";\n\"lng_view_button_stickerset\" = \"View stickers\";\n\"lng_view_button_emojipack\" = \"View emoji\";\n\"lng_view_button_collectible\" = \"View collectible\";\n\"lng_view_button_call\" = \"Join call\";\n\"lng_view_button_storyalbum\" = \"View Album\";\n\"lng_view_button_collection\" = \"View Collection\";\n\"lng_view_button_newbot\" = \"Create Bot\";\n\n\"lng_sponsored_hide_ads\" = \"Hide\";\n\"lng_sponsored_title\" = \"What are sponsored messages?\";\n\"lng_sponsored_info_description1_linked\" = \"Unlike other apps, Telegram never uses your private data to target ads. {link}\\n\\nUnlike other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can’t spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\\n\\nTelegram offers free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible advertisers at:\";\n\"lng_sponsored_info_description1_link\" = \"Learn more in the Privacy Policy\";\n\"lng_sponsored_info_description1_url\" = \"https://telegram.org/privacy#5-6-no-ads-based-on-user-data\";\n\"lng_sponsored_info_description2\" = \"Sponsored Messages are currently in test mode. Once they are fully launched and allow Telegram to cover its basic costs, we will start sharing ad revenue with the owners of public channels in which sponsored messages are displayed.\\n\\nOnline ads should no longer be synonymous with abuse of user privacy. Let us redefine how a tech company should operate – together.\";\n\"lng_sponsored_info_menu\" = \"Advertiser info\";\n\"lng_sponsored_info_submenu\" = \"Advertiser: {text}\";\n\"lng_sponsored_menu_revenued_about\" = \"About These Ads\";\n\"lng_sponsored_menu_revenued_report\" = \"Report Ad\";\n\"lng_sponsored_revenued_subtitle\" = \"Telegram Ads are very different from ads on other platforms. Ads such as this one:\";\n\"lng_sponsored_revenued_info1_title\" = \"Respect Your Privacy\";\n\"lng_sponsored_revenued_info1_description\" = \"Ads on Telegram do not use your personal information and are based on the channel in which you see them.\";\n\"lng_sponsored_revenued_info1_bot_description\" = \"Ads on Telegram do not use your personal information and are based on the mini app in which you see them.\";\n\"lng_sponsored_revenued_info1_search_description\" = \"Ads on Telegram do not use your personal information and are based on the search query you entered.\";\n\"lng_sponsored_revenued_info2_title\" = \"Help the Channel Creator\";\n\"lng_sponsored_revenued_info2_bot_title\" = \"Help the Bot Developer\";\n\"lng_sponsored_revenued_info2_description\" = \"50% of the revenue from Telegram Ads goes to the owner of the channel where they are displayed.\";\n\"lng_sponsored_revenued_info2_bot_description\" = \"50% of the revenue from Telegram Ads goes to the developer of the mini app where they are displayed.\";\n\"lng_sponsored_revenued_info3_title\" = \"Can Be Removed\";\n\"lng_sponsored_revenued_info3_description#one\" = \"You can turn off ads by subscribing to {link}, and Level {count} channels can remove them for their subscribers.\";\n\"lng_sponsored_revenued_info3_description#other\" = \"You can turn off ads by subscribing to {link}, and Level {count} channels can remove them for their subscribers.\";\n\"lng_sponsored_revenued_info3_bot_description\" = \"You can turn off ads in mini apps by subscribing to {link}.\";\n\"lng_sponsored_revenued_info3_search_description\" = \"You can turn off ads by subscribing to Telegram Premium. {link}\";\n\"lng_sponsored_revenued_info3_search_link\" = \"Subscribe {arrow}\";\n\"lng_sponsored_revenued_footer_title\" = \"Can I Launch an Ad?\";\n\"lng_sponsored_revenued_footer_description\" = \"Anyone can create an ad to display in this channel — with minimal budgets. Check out the **Telegram Ad Platform** for details. {link}\";\n\"lng_sponsored_revenued_footer_bot_description\" = \"Anyone can create an ad to display in this bot — with minimal budgets. Check out the **Telegram Ad Platform** for details. {link}\";\n\"lng_sponsored_revenued_footer_search_description\" = \"Anyone can create an ad to display in search results for any query. Check out the **Telegram Ad Platform** for details. {link}\";\n\"lng_sponsored_top_bar_hide\" = \"remove\";\n\n\"lng_telegram_features_url\" = \"https://t.me/TelegramTips\";\n\n\"lng_mini_apps_disclaimer_title\" = \"Warning\";\n\"lng_mini_apps_disclaimer_text\" = \"You are about to use a mini app operated by an independent party **not affiliated with Telegram**. You must agree to the Terms of Use for mini apps to continue.\";\n\"lng_mini_apps_disclaimer_button\" = \"I agree to the {link}\";\n\"lng_mini_apps_disclaimer_link\" = \"Terms of Use\";\n\"lng_mini_apps_tos_url\" = \"https://telegram.org/tos/mini-apps\";\n\n\"lng_ringtones_box_title\" = \"Notification Sound\";\n\"lng_ringtones_box_cloud_subtitle\" = \"Choose your tone\";\n\"lng_ringtones_box_volume\" = \"Volume\";\n\"lng_ringtones_box_upload_choose\" = \"Choose a tone\";\n\"lng_ringtones_box_upload_button\" = \"Upload Sound\";\n\"lng_ringtones_box_about\" = \"Right click on any short voice note or MP3 file in chat and select \\\"Save for Notifications\\\". It will appear here.\";\n\"lng_ringtones_box_default\" = \"Default\";\n\"lng_ringtones_box_no_sound\" = \"No sound\";\n\"lng_ringtones_toast_added\" = \"Sound added!\";\n\"lng_ringtones_error_max_size\" = \"Sorry, but your file is too big. The maximum size for ringtones is {size}.\";\n\"lng_ringtones_error_max_duration\" = \"Sorry, but your file is too long. The maximum duration for ringtones is {duration}.\";\n\n\"lng_bot_thread_edit\" = \"Edit Thread\";\n\"lng_bot_thread_title\" = \"Thread Name\";\n\"lng_bot_thread_choose_title_and_icon\" = \"Choose a thread name and icon\";\n\n\"lng_forum_topic_new\" = \"New Topic\";\n\"lng_forum_topic_edit\" = \"Edit Topic\";\n\"lng_forum_topic_title\" = \"Topic Name\";\n\"lng_forum_topic_close\" = \"Close Topic\";\n\"lng_forum_topic_reopen\" = \"Reopen Topic\";\n\"lng_forum_topic_closed\" = \"This topic is closed.\";\n\"lng_forum_topic_delete\" = \"Delete\";\n\"lng_forum_topic_delete_sure\" = \"Are you sure you want to delete this topic?\";\n\"lng_forum_topic_created_title_my\" = \"Almost done!\";\n\"lng_forum_topic_created_body_my\" = \"Send a message to\\nstart the topic.\";\n\"lng_forum_topic_created_title\" = \"Topic started!\";\n\"lng_forum_topic_created_body\" = \"Send a message\\nto start the topic.\";\n\"lng_forum_topics_switch\" = \"Topics\";\n\"lng_forum_topics_not_enough#one\" = \"Only groups with more than **{count} member** can have topics enabled.\";\n\"lng_forum_topics_not_enough#other\" = \"Only groups with more than **{count} members** can have topics enabled.\";\n\"lng_forum_topics_no_discussion\" = \"Topics are not yet available in the discussion groups of channels.\";\n\"lng_forum_choose_title_and_icon\" = \"Choose a topic name and icon\";\n\"lng_forum_replies_only\" = \"You can reply to messages in topics.\";\n\"lng_forum_message_in\" = \"Message in {topic}\";\n\"lng_forum_reply_in\" = \"Reply in {topic}\";\n\"lng_forum_no_topics\" = \"No topics currently created in this group.\";\n\"lng_forum_create_topic\" = \"Create topic\";\n\"lng_forum_discard_sure\" = \"Are you sure you want to discard this topic?\";\n\"lng_forum_view_as_messages\" = \"View as Messages\";\n\"lng_forum_view_as_topics\" = \"View as Topics\";\n\"lng_forum_no_messages\" = \"No messages\";\n\"lng_forum_messages#one\" = \"{count} message\";\n\"lng_forum_messages#other\" = \"{count} messages\";\n\"lng_forum_show_topics_list\" = \"Show Topics List\";\n\n\"lng_monoforum_choose_to_reply\" = \"Choose a message to reply.\";\n\n\"lng_request_peer_requirements\" = \"Requirements\";\n\"lng_request_peer_rights\" = \"You must have these admin rights: {rights}.\";\n\"lng_request_peer_rights_and\" = \"{rights} and {last}\";\n\"lng_request_peer_confirm\" = \"Are you sure you want to send {chat} to {bot}?\";\n\"lng_request_peer_confirm_add\" = \"This will also add {bot} to {chat}.\";\n\"lng_request_peer_confirm_rights\" = \"This will also add {bot} to {chat} with the following rights: {rights}.\";\n\"lng_request_peer_confirm_send\" = \"Send\";\n\"lng_request_user_title\" = \"Choose User\";\n\"lng_request_users_title\" = \"Choose Users\";\n\"lng_request_user_premium_yes\" = \"The user must have a Premium subscription.\";\n\"lng_request_user_premium_no\" = \"The user must not have a Premium subscription.\";\n\"lng_request_user_no\" = \"No such users\";\n\"lng_request_user_no_about\" = \"No users found that meet the requirements for this bot.\";\n\"lng_request_bot_title\" = \"Choose a Bot\";\n\"lng_request_bot_no\" = \"No bots\";\n\"lng_request_bot_no_about\" = \"You don't have any bots.\";\n\"lng_request_group_title\" = \"Choose a Group\";\n\"lng_request_group_no\" = \"No such groups\";\n\"lng_request_group_no_about\" = \"You don't have groups that meet the requirements for this bot.\";\n\"lng_request_group_public_yes\" = \"The group must be public.\";\n\"lng_request_group_public_no\" = \"The group must be private.\";\n\"lng_request_group_topics_yes\" = \"The group must have topics turned on.\";\n\"lng_request_group_topics_no\" = \"The group must have topics turned off.\";\n\"lng_request_group_am_owner\" = \"You must be the owner of the group.\";\n\"lng_request_group_change_info\" = \"change group info\";\n\"lng_request_group_delete_messages\" = \"delete messages\";\n\"lng_request_group_ban_users\" = \"ban users\";\n\"lng_request_group_invite\" = \"invite users via link\";\n\"lng_request_group_pin_messages\" = \"pin messages\";\n\"lng_request_group_manage_topics\" = \"manage topics\";\n\"lng_request_group_manage_video_chats\" = \"manage video chats\";\n\"lng_request_group_anonymous\" = \"remain anonymous\";\n\"lng_request_group_add_admins\" = \"add new admins\";\n\"lng_request_group_create\" = \"Create a New Group for This\";\n\"lng_request_channel_title\" = \"Choose a Channel\";\n\"lng_request_channel_no\" = \"No such channels\";\n\"lng_request_channel_no_about\" = \"You don't have channels that meet the requirements for this bot.\";\n\"lng_request_channel_public_yes\" = \"The channel must be public.\";\n\"lng_request_channel_public_no\" = \"The channel must be private.\";\n\"lng_request_channel_am_owner\" = \"You must be the owner of the channel.\";\n\"lng_request_channel_change_info\" = \"change channel info\";\n\"lng_request_channel_post_messages\" = \"post messages\";\n\"lng_request_channel_edit_messages\" = \"edit messages of others\";\n\"lng_request_channel_delete_messages\" = \"delete messages\";\n\"lng_request_channel_add_subscribers\" = \"add subscribers\";\n\"lng_request_channel_manage_livestreams\" = \"manage live streams\";\n\"lng_request_channel_manage_direct\" = \"manage direct messages\";\n\"lng_request_channel_add_admins\" = \"add new admins\";\n\"lng_request_channel_create\" = \"Create a New Channel for This\";\n\n\"lng_userpic_builder_color_subtitle\" = \"Choose background\";\n\"lng_userpic_builder_emoji_subtitle\" = \"Choose sticker or emoji\";\n\n\"lng_stories_my_name\" = \"My Story\";\n\"lng_stories_archive\" = \"Hide Stories\";\n\"lng_stories_unarchive\" = \"Unhide Stories\";\n\"lng_stories_view_anonymously\" = \"View Anonymously\";\n\"lng_stories_row_count#one\" = \"{count} Story\";\n\"lng_stories_row_count#other\" = \"{count} Stories\";\n\"lng_stories_views#one\" = \"{count} view\";\n\"lng_stories_views#other\" = \"{count} views\";\n\"lng_stories_no_views\" = \"No views yet\";\n\"lng_stories_view_reactions\" = \"View reactions\";\n\"lng_stories_unsupported\" = \"This story is not supported\\nby your version of Telegram.\";\n\"lng_stories_cant_reply\" = \"You can't reply to this story.\";\n\"lng_stories_about_silent\" = \"This video has no sound.\";\n\"lng_stories_about_close_friends\" = \"You are seeing this story because {user} added you to their list of **Close Friends**.\";\n\"lng_stories_about_contacts\" = \"Only {user}'s contacts can view this story.\";\n\"lng_stories_about_selected_contacts\" = \"Only some users {user} selected can view this story.\";\n\"lng_stories_about_close_friends_my\" = \"Only people from your **Close Friends** list can view this story.\";\n\"lng_stories_about_contacts_my\" = \"Only your contacts can view this story.\";\n\"lng_stories_about_selected_contacts_my\" = \"Only some users you selected can view this story.\";\n\"lng_stories_click_to_view_mine\" = \"Click here to view your story.\";\n\"lng_stories_click_to_view\" = \"Click here to view updates from {users}.\";\n\"lng_stories_click_to_view_and_one\" = \"{accumulated}, {user}\";\n\"lng_stories_click_to_view_and_last\" = \"{accumulated} and {user}\";\n\"lng_stories_show_more\" = \"Show more\";\n\n\"lng_stories_my_title\" = \"Posted Stories\";\n\"lng_stories_archive_button\" = \"Story Archive\";\n\"lng_stories_recent_button\" = \"Recent Stories\";\n\"lng_stories_archive_title\" = \"Story Archive\";\n\"lng_stories_archive_about\" = \"Only you can see archived stories unless you choose to post them to your profile.\";\n\"lng_stories_channel_archive_about\" = \"Only admins of the channel can see archived stories unless they are posted to the channel page.\";\n\"lng_stories_empty\" = \"Your stories will be here.\";\n\"lng_stories_empty_channel\" = \"Channel posts will be here.\";\n\"lng_stories_reply_sent\" = \"Message sent.\";\n\"lng_stories_hidden_to_contacts\" = \"Stories from {user} were moved to the **Archive**.\";\n\"lng_stories_shown_in_chats\" = \"Stories from {user} were returned from the **Archive**.\";\n\"lng_stories_delete_one_sure\" = \"Are you sure you want to delete this story?\";\n\"lng_stories_delete_sure#one\" = \"Are you sure you want to delete {count} story?\";\n\"lng_stories_delete_sure#other\" = \"Are you sure you want to delete {count} stories?\";\n\"lng_stories_save_sure\" = \"Do you want to post this story on your profile?\";\n\"lng_stories_save_sure_many#one\" = \"Do you want to post {count} story to your profile?\";\n\"lng_stories_save_sure_many#other\" = \"Do you want to post {count} stories to your profile?\";\n\"lng_stories_save_done\" = \"Story posted to your profile.\";\n\"lng_stories_save_done_many#one\" = \"{count} story is posted on your profile.\";\n\"lng_stories_save_done_many#other\" = \"{count} stories are posted on your profile.\";\n\"lng_stories_save_done_about\" = \"Posted stories can be viewed by others on your profile until you remove them.\";\n\"lng_stories_archive_sure\" = \"Do you want to hide this story from your profile?\";\n\"lng_stories_archive_sure_many#one\" = \"Do you want to remove {count} story from your profile?\";\n\"lng_stories_archive_sure_many#other\" = \"Do you want to remove {count} stories from your profile?\";\n\"lng_stories_archive_done\" = \"This story is hidden from your profile.\";\n\"lng_stories_archive_done_many#one\" = \"{count} story is hidden from your profile.\";\n\"lng_stories_archive_done_many#other\" = \"{count} stories are hidden from your profile.\";\n\"lng_stories_channel_save_sure\" = \"Do you want to post this story to the channel page?\";\n\"lng_stories_channel_save_sure_many#one\" = \"Do you want to post {count} story to the channel page?\";\n\"lng_stories_channel_save_sure_many#other\" = \"Do you want to post {count} stories to the channel page?\";\n\"lng_stories_channel_save_done\" = \"This story is posted to the channel page.\";\n\"lng_stories_channel_save_done_many#one\" = \"{count} story posted to the channel page.\";\n\"lng_stories_channel_save_done_many#other\" = \"{count} stories posted to the channel page.\";\n\"lng_stories_channel_save_done_about\" = \"Posted stories can be viewed by others on the channel page until an admin removes them.\";\n\"lng_stories_channel_archive_sure\" = \"Do you want to hide this story from the channel page?\";\n\"lng_stories_channel_archive_sure_many#one\" = \"Do you want to hide {count} story from the channel page?\";\n\"lng_stories_channel_archive_sure_many#other\" = \"Do you want to hide {count} stories from the channel page?\";\n\"lng_stories_channel_archive_done\" = \"This story is hidden from the channel page.\";\n\"lng_stories_channel_archive_done_many#one\" = \"{count} story is hidden from the channel page.\";\n\"lng_stories_channel_archive_done_many#other\" = \"{count} stories are hidden from the channel page.\";\n\"lng_stories_save_promo\" = \"Subscribe to {link} to save other people's stories that are not protected.\";\n\"lng_stories_reaction_as_message\" = \"Send reaction as a private message\";\n\n\"lng_stories_album_add\" = \"Add Album\";\n\"lng_stories_album_new_title\" = \"Create a New Album\";\n\"lng_stories_album_new_button\" = \"New Album\";\n\"lng_stories_album_new_text\" = \"Choose a name for your album and start adding your stories there.\";\n\"lng_stories_album_new_ph\" = \"Title\";\n\"lng_stories_album_new_create\" = \"Create\";\n\"lng_stories_album_empty_title\" = \"Organize Your Stories\";\n\"lng_stories_album_empty_text\" = \"Add some of your stories to this album.\";\n\"lng_stories_album_all\" = \"All Stories\";\n\"lng_stories_album_add_title\" = \"Add Stories\";\n\"lng_stories_album_add_button\" = \"Add Stories\";\n\"lng_stories_album_share\" = \"Share Album\";\n\"lng_stories_album_edit\" = \"Edit Name\";\n\"lng_stories_album_limit_title\" = \"Limit Reached\";\n\"lng_stories_album_limit_text\" = \"Please remove one of the existing albums to add a new one.\";\n\"lng_stories_album_delete\" = \"Delete Album\";\n\"lng_stories_album_delete_sure\" = \"Are you sure you want to delete this album?\";\n\"lng_stories_album_delete_button\" = \"Delete\";\n\"lng_stories_album_add_to\" = \"Add to Album\";\n\n\"lng_stealth_mode_menu_item\" = \"Hide My View\";\n\"lng_stealth_mode_title\" = \"Stealth Mode\";\n\"lng_stealth_mode_unlock_about\" = \"Subscribe to Telegram Premium to watch stories without appearing in the list of viewers.\";\n\"lng_stealth_mode_about\" = \"Turn Stealth Mode on to watch stories without appearing in the list of viewers.\";\n\"lng_stealth_mode_past_title\" = \"Hide Recent Views\";\n\"lng_stealth_mode_past_about\" = \"Hide my views from the past 5 minutes.\";\n\"lng_stealth_mode_next_title\" = \"Hide Upcoming Views\";\n\"lng_stealth_mode_next_about\" = \"Hide my views for the next 25 minutes.\";\n\"lng_stealth_mode_unlock\" = \"Unlock Stealth Mode\";\n\"lng_stealth_mode_enable\" = \"Enable Stealth Mode\";\n\"lng_stealth_mode_enable_and_open\" = \"Enable and open the story\";\n\"lng_stealth_mode_cooldown_in\" = \"Available in {left}\";\n\"lng_stealth_mode_cooldown_tip\" = \"Please wait until **Stealth Mode** is ready to use again.\";\n\"lng_stealth_mode_enabled_tip_title\" = \"Stealth Mode On\";\n\"lng_stealth_mode_enabled_tip\" = \"The creators of stories you viewed in the last **5 minutes** or will view in the next **25 minutes** won't see you in the viewers' lists.\";\n\"lng_stealth_mode_countdown\" = \"Stealth Mode active – {left}\";\n\"lng_stealth_mode_already_title\" = \"You are in Stealth Mode\";\n\"lng_stealth_mode_already_about\" = \"The creators of stories you view in the next **{left}** won't see you in the viewers' lists.\";\n\n\"lng_stories_link_invalid\" = \"This link is broken or has expired.\";\n\"lng_stories_live_finished\" = \"The live story has ended.\";\n\n\"lng_stats_title\" = \"Statistics\";\n\"lng_stats_message_title\" = \"Message Statistic\";\n\"lng_stats_story_title\" = \"Story Statistic\";\n\"lng_stats_zoom_out\" = \"Zoom Out\";\n\n\"lng_stats_day_month_year\" = \"{days_count} {month} {year}\";\n\"lng_stats_day_month\" = \"{days_count} {month}\";\n\"lng_stats_weekday_day_month_year\" = \"{day}, {days_count} {month} {year}\";\n\"lng_stats_weekday_day_month_time\" = \"{day}, {days_count} {month} {time}\";\n\n\"lng_stats_overview_title\" = \"Overview\";\n\"lng_stats_overview_member_count\" = \"Followers\";\n\"lng_stats_overview_mean_view_count\" = \"Views Per Post\";\n\"lng_stats_overview_mean_share_count\" = \"Shares Per Post\";\n\"lng_stats_overview_mean_reactions_count\" = \"Reactions Per Post\";\n\"lng_stats_overview_mean_story_view_count\" = \"Views Per Story\";\n\"lng_stats_overview_mean_story_share_count\" = \"Shares Per Story\";\n\"lng_stats_overview_mean_story_reactions_count\" = \"Reactions Per Story\";\n\"lng_stats_overview_enabled_notifications\" = \"Enabled Notifications\";\n\"lng_stats_overview_messages\" = \"Messages\";\n\"lng_stats_overview_group_mean_view_count\" = \"Viewing Members\";\n\"lng_stats_overview_group_mean_post_count\" = \"Posting Members\";\n\"lng_stats_overview_message_private_shares\" = \"Private Shares\";\n\"lng_stats_overview_message_public_shares\" = \"Public Shares\";\n\"lng_stats_overview_message_views\" = \"Views\";\n\"lng_stats_overview_message_public_share#one\" = \"{count} public share\";\n\"lng_stats_overview_message_public_share#other\" = \"{count} public shares\";\n\n\"lng_stats_members_title\" = \"Top members\";\n\"lng_stats_admins_title\" = \"Top admins\";\n\"lng_stats_inviters_title\" = \"Top inviters\";\n\"lng_stats_member_messages#one\" = \"{count} message\";\n\"lng_stats_member_messages#other\" = \"{count} messages\";\n\"lng_stats_member_characters#one\" = \"{count} symbol per message\";\n\"lng_stats_member_characters#other\" = \"{count} symbols per message\";\n\"lng_stats_member_deletions#one\" = \"{count} deletion\";\n\"lng_stats_member_deletions#other\" = \"{count} deletions\";\n\"lng_stats_member_bans#one\" = \"{count} ban\";\n\"lng_stats_member_bans#other\" = \"{count} bans\";\n\"lng_stats_member_restrictions#one\" = \"{count} restriction\";\n\"lng_stats_member_restrictions#other\" = \"{count} restrictions\";\n\"lng_stats_member_invitations#one\" = \"{count} invitation\";\n\"lng_stats_member_invitations#other\" = \"{count} invitations\";\n\n\"lng_stats_recent_messages_title\" = \"Recent posts\";\n\"lng_stats_recent_messages_views#one\" = \"{count} view\";\n\"lng_stats_recent_messages_views#other\" = \"{count} views\";\n\n\"lng_stats_loading\" = \"Loading stats...\";\n\"lng_stats_loading_subtext\" = \"Please wait a few moments while we generate your stats.\";\n\"lng_stats_boosts_loading\" = \"Loading boosts list...\";\n\"lng_stats_boosts_loading_subtext\" = \"Please wait a few moments while we generate your stats.\";\n\"lng_stats_earn_loading\" = \"Loading rewards info...\";\n\"lng_stats_earn_loading_subtext\" = \"Please wait a few moments while we generate your stats.\";\n\n\"lng_chart_title_member_count\" = \"Growth\";\n\"lng_chart_title_join\" = \"Followers\";\n\"lng_chart_title_mute\" = \"Notifications\";\n\"lng_chart_title_view_count_by_hour\" = \"Views by hours (UTC)\";\n\"lng_chart_title_view_count_by_source\" = \"Views by source\";\n\"lng_chart_title_join_by_source\" = \"New followers by source\";\n\"lng_chart_title_language\" = \"Languages\";\n\"lng_chart_title_message_interaction\" = \"Interactions\";\n\"lng_chart_title_instant_view_interaction\" = \"IV Interactions\";\n\"lng_chart_title_reactions_by_emotion\" = \"Reactions\";\n\"lng_chart_title_story_interactions\" = \"Story Interactions\";\n\"lng_chart_title_story_reactions_by_emotion\" = \"Story reactions\";\n\n\"lng_chart_title_group_join\" = \"Group members\";\n\"lng_chart_title_group_join_by_source\" = \"New members by source\";\n\"lng_chart_title_group_language\" = \"Members's primary language\";\n\"lng_chart_title_group_message_content\" = \"Messages\";\n\"lng_chart_title_group_action\" = \"Actions\";\n\"lng_chart_title_group_day\" = \"Views by hours\";\n\"lng_chart_title_group_week\" = \"Top days of week\";\n\n\"lng_boosts_title\" = \"Boosts\";\n\"lng_boosts_level\" = \"Level\";\n\"lng_boosts_existing\" = \"Existing boosts\";\n\"lng_boosts_premium_audience\" = \"Premium subscribers\";\n\"lng_boosts_premium_members\" = \"Premium members\";\n\"lng_boosts_next_level\" = \"Boosts to level up\";\n\"lng_boosts_list_title#one\" = \"{count} boost\";\n\"lng_boosts_list_title#other\" = \"{count} boosts\";\n\"lng_boosts_list_subtext\" = \"Your channel is currently boosted by these users.\";\n\"lng_boosts_list_subtext_group\" = \"Your group is currently boosted by these users.\";\n\"lng_boosts_show_more_boosts#one\" = \"Show {count} More Boosts\";\n\"lng_boosts_show_more_boosts#other\" = \"Show {count} More Boosts\";\n\"lng_boosts_show_more_gifts#one\" = \"Show {count} More Boosts\";\n\"lng_boosts_show_more_gifts#other\" = \"Show {count} More Boosts\";\n\"lng_boosts_list_status\" = \"boost expires on {date}\";\n\"lng_boosts_link_title\" = \"Link for boosting\";\n\"lng_boosts_link_subtext\" = \"Share this link with your subscribers to get more boosts.\";\n\"lng_boosts_link_subtext_group\" = \"Share this link with the members of your group to get more boosts.\";\n\"lng_boosts_get_boosts\" = \"Get Boosts via Gifts\";\n\"lng_boosts_get_boosts_subtext\" = \"Get more boosts for your channel by gifting Telegram Premium to your subscribers.\";\n\"lng_boosts_get_boosts_subtext_group\" = \"Get more boosts for your group by gifting Telegram Premium to the members.\";\n\"lng_boosts_list_unclaimed\" = \"Unclaimed\";\n\"lng_boosts_list_pending\" = \"To be distributed\";\n\"lng_boosts_list_pending_about\" = \"The recipient will be selected when the giveaway ends.\";\n\"lng_boosts_list_tab_gifts#one\" = \"{count} Gift\";\n\"lng_boosts_list_tab_gifts#other\" = \"{count} Gifts\";\n\n\"lng_boosts_prepaid_giveaway_title\" = \"Prepaid giveaways\";\n\"lng_boosts_prepaid_giveaway_title_subtext\" = \"Select a giveaway you already paid for to set it up.\";\n\"lng_boosts_prepaid_giveaway_single\" = \"Prepaid giveaway\";\n\"lng_boosts_prepaid_giveaway_quantity#one\" = \"{count} Telegram Premium\";\n\"lng_boosts_prepaid_giveaway_quantity#other\" = \"{count} Telegram Premium\";\n\"lng_boosts_prepaid_giveaway_moths#one\" = \"{count}-month subscriptions\";\n\"lng_boosts_prepaid_giveaway_moths#other\" = \"{count}-month subscriptions\";\n\"lng_boosts_prepaid_giveaway_status#one\" = \"{count} subscription {duration}\";\n\"lng_boosts_prepaid_giveaway_status#other\" = \"{count} subscriptions {duration}\";\n\"lng_boosts_prepaid_giveaway_credits_status#one\" = \"{amount} among {count} winner.\";\n\"lng_boosts_prepaid_giveaway_credits_status#other\" = \"{amount} among {count} winners.\";\n\n\"lng_channel_earn_title\" = \"Monetization\";\n\"lng_channel_earn_about\" = \"Telegram shares 50% of the revenue from ads displayed in your channel as rewards. {link}\";\n\"lng_channel_earn_about_bot\" = \"Telegram shares 50% of the revenue from ads displayed in your bot. {link}\";\n\"lng_channel_earn_about_link\" = \"Learn more {emoji}\";\n\"lng_channel_earn_overview_title\" = \"Rewards overview\";\n\"lng_channel_earn_available\" = \"Rewards available for collection\";\n\"lng_channel_earn_reward\" = \"Rewards since last collection\";\n\"lng_channel_earn_total\" = \"Total lifetime rewards\";\n\"lng_channel_earn_balance_title\" = \"Available reward\";\n\"lng_channel_earn_balance_button\" = \"Collect\";\n\"lng_channel_earn_balance_password_title\" = \"Two-step verification\";\n\"lng_channel_earn_balance_password_description\" = \"Please enter your password to collect.\";\n\"lng_channel_earn_balance_about\" = \"Collect your reward using Fragment, a third-party platform used by advertisers to pay for ads. {link}\";\n\"lng_channel_earn_balance_about_temp\" = \"In the coming weeks you will be able to collect your reward using Fragment, a third-party platform used by advertisers to pay ads. {link}\";\n\"lng_channel_earn_transfer_sure_about1\" = \"Check the address of the recipient:\";\n\"lng_channel_earn_transfer_sure_about2\" = \"This action can not be undone. If the address above is incorrect you will lose your TON.\";\n\"lng_channel_earn_history_title\" = \"Transaction history\";\n\"lng_channel_earn_history_in\" = \"Rewards from ads\";\n\"lng_channel_earn_history_in_about\" = \"Rewards from ads displayed in\";\n\"lng_channel_earn_history_out\" = \"Collection\";\n\"lng_channel_earn_history_out_failed\" = \"Not Completed\";\n\"lng_channel_earn_history_out_about_failed\" = \"Couldn't collect reward\";\n\"lng_channel_earn_history_out_button\" = \"View in Blockchain Explorer\";\n\"lng_channel_earn_history_return\" = \"Refund\";\n\"lng_channel_earn_history_return_about\" = \"Refunded back\";\n\"lng_channel_earn_history_pending\" = \"Pending\";\n\"lng_channel_earn_history_failed\" = \"Failed\";\n\"lng_channel_earn_history_show_more#one\" = \"Show {count} More Transaction\";\n\"lng_channel_earn_history_show_more#other\" = \"Show {count} More Transactions\";\n\"lng_channel_earn_off\" = \"Switch Off Ads\";\n\"lng_channel_earn_off_about\" = \"You will not be eligible for any rewards if you switch off ads.\";\n\"lng_channel_earn_cpm_min\" = \"No Ads\";\n\"lng_channel_earn_cpm#one\" = \"{emoji} {count} CPM\";\n\"lng_channel_earn_cpm#other\" = \"{emoji} {count} CPM\";\n\"lng_channel_earn_learn_title\" = \"Earn From Your Channel\";\n\"lng_channel_earn_bot_learn_title\" = \"Earn From Your Bot\";\n\"lng_channel_earn_learn_in_subtitle\" = \"Telegram Ads\";\n\"lng_channel_earn_learn_in_about\" = \"Telegram can display ads in your channel.\";\n\"lng_channel_earn_learn_bot_in_about\" = \"Telegram can display ads in your bot.\";\n\"lng_channel_earn_learn_split_subtitle\" = \"50:50 revenue split\";\n\"lng_channel_earn_learn_split_about\" = \"You can receive 50% of the ad revenue as rewards in TON.\";\n\"lng_channel_earn_learn_out_subtitle\" = \"Flexible withdrawals\";\n\"lng_channel_earn_learn_out_about\" = \"You can collect your TON any time.\";\n\"lng_channel_earn_learn_coin_title\" = \"What is {emoji} TON?\";\n\"lng_channel_earn_learn_coin_about\" = \"TON is a blockchain platform and cryptocurrency that Telegram uses for its high speed and low commissions on transactions. {link}\";\n\"lng_channel_earn_learn_close\" = \"Got it\";\n\"lng_channel_earn_learn_coin_link\" = \"https://telegram.org/blog/monetization-for-channels\";\n\"lng_channel_earn_chart_top_hours\" = \"Ad impressions\";\n\"lng_channel_earn_chart_revenue\" = \"Ad rewards\";\n\"lng_channel_earn_chart_overriden_detail_credits\" = \"Rewards in Stars\";\n\"lng_channel_earn_chart_overriden_detail_currency\" = \"Rewards in TON\";\n\"lng_channel_earn_chart_overriden_detail_usd\" = \"Rewards in USD\";\n\"lng_channel_earn_currency_history\" = \"TON Transactions\";\n\"lng_channel_earn_credits_history\" = \"Stars Transactions\";\n\"lng_channel_earn_out_check_password_about\" = \"You can withdraw only if you have:\";\n\n\"lng_bot_earn_title\" = \"Stars Balance\";\n\"lng_bot_earn_chart_revenue\" = \"Revenue\";\n\"lng_bot_earn_overview_title\" = \"Proceeds overview\";\n\"lng_bot_earn_available\" = \"Available balance\";\n\"lng_bot_earn_reward\" = \"Total balance\";\n\"lng_bot_earn_total\" = \"Total lifetime proceeds\";\n\"lng_bot_earn_balance_title\" = \"Available balance\";\n\"lng_bot_earn_balance_about\" = \"Stars from your total balance can be used for ads or withdrawn as rewards 21 days after they are earned.\";\n\"lng_bot_earn_balance_about_url\" = \"https://telegram.org/tos/stars\";\n\"lng_bot_earn_balance_button#one\" = \"Withdraw {emoji} {count}\";\n\"lng_bot_earn_balance_button#other\" = \"Withdraw {emoji} {count}\";\n\"lng_bot_earn_balance_button_all\" = \"Withdraw all stars\";\n\"lng_bot_earn_balance_button_locked\" = \"Withdraw\";\n\"lng_bot_earn_balance_button_buy_ads\" = \"Buy Ads\";\n\"lng_bot_earn_learn_credits_out_about\" = \"You can withdraw Stars using Fragment, or use Stars to advertise your bot. {link}\";\n\"lng_self_earn_learn_credits_out_about\" = \"You can withdraw from 10 Stars using Fragment. {link}\";\n\"lng_bot_earn_out_ph\" = \"Enter amount to withdraw\";\n\"lng_bot_earn_out_ph_max\" = \"Enter amount to withdraw (max. {amount})\";\n\"lng_bot_earn_balance_password_title\" = \"Two-step verification\";\n\"lng_bot_earn_balance_password_description\" = \"Please enter your password to collect.\";\n\"lng_bot_earn_credits_out_minimal\" = \"You cannot withdraw less than {link}.\";\n\"lng_bot_earn_credits_out_minimal_link#one\" = \"{count} star\";\n\"lng_bot_earn_credits_out_minimal_link#other\" = \"{count} stars\";\n\"lng_bot_copy_text_tooltip\" = \"Copy to Clipboard: {text}\";\n\n\"lng_contact_add\" = \"Add\";\n\"lng_contact_send_message\" = \"Message\";\n\n\"lng_iv_open_in_browser\" = \"Open in Browser\";\n\"lng_iv_share\" = \"Share\";\n\"lng_iv_join_channel\" = \"Join\";\n\"lng_iv_window_title\" = \"Instant View\";\n\"lng_iv_wrong_layout\" = \"Wrong layout?\";\n\"lng_iv_not_supported\" = \"This link appears to be invalid.\";\n\"lng_iv_zoom_tooltip_ctrl\" = \"Hold Ctrl to zoom by 5%.\\nHold Alt to zoom by 1%.\";\n\"lng_iv_zoom_tooltip_cmd\" = \"Hold Cmd to zoom by 5%.\\nHold Alt to zoom by 1%.\";\n\n\"lng_limit_download_title\" = \"Download speed limited\";\n\"lng_limit_download_subscribe\" = \"Subscribe to {link} to increase download speed {increase}.\";\n\"lng_limit_download_increase_times#one\" = \"**{count}** time\";\n\"lng_limit_download_increase_times#other\" = \"**{count}** times\";\n\"lng_limit_download_increase_speed\" = \"by **{percent}**\";\n\"lng_limit_download_subscribe_link\" = \"Telegram Premium\";\n\"lng_limit_upload_title\" = \"Upload speed limited\";\n\"lng_limit_upload_subscribe\" = \"Subscribe to {link} to increase upload speed {increase}.\";\n\"lng_limit_upload_increase_times#one\" = \"**{count}** time\";\n\"lng_limit_upload_increase_times#other\" = \"**{count}** times\";\n\"lng_limit_upload_increase_speed\" = \"by **{percent}**\";\n\"lng_limit_upload_subscribe_link\" = \"Telegram Premium\";\n\n\"lng_recent_frequent\" = \"Frequent contacts\";\n\"lng_recent_frequent_all\" = \"Show all\";\n\"lng_recent_frequent_collapse\" = \"Collapse\";\n\"lng_recent_title\" = \"Recent\";\n\"lng_recent_clear\" = \"Clear\";\n\"lng_recent_clear_sure\" = \"Do you want to clear your search history?\";\n\"lng_recent_remove\" = \"Remove from Recent\";\n\"lng_recent_clear_all\" = \"Clear all\";\n\"lng_recent_hide_top\" = \"Remove all & Disable\";\n\"lng_recent_hide_sure\" = \"Are you sure you want to clear and disable frequent contacts list?\\n\\nYou can always turn this feature back on in Settings > Privacy > Suggest Frequent Contacts.\";\n\"lng_recent_hide_button\" = \"Hide\";\n\"lng_recent_none\" = \"Recent search results\\nwill appear here.\";\n\"lng_recent_chats\" = \"Chats\";\n\"lng_recent_channels\" = \"Channels\";\n\"lng_recent_apps\" = \"Apps\";\n\"lng_recent_posts\" = \"Posts\";\n\"lng_all_photos\" = \"Photos\";\n\"lng_all_videos\" = \"Videos\";\n\"lng_all_downloads\" = \"Downloads\";\n\"lng_all_links\" = \"Links\";\n\"lng_all_files\" = \"Files\";\n\"lng_all_music\" = \"Music\";\n\"lng_all_voice\" = \"Voice\";\n\"lng_channels_none_title\" = \"No channels yet...\";\n\"lng_channels_none_about\" = \"You are not currently subscribed to any channels.\";\n\"lng_channels_your_title\" = \"Channels you joined\";\n\"lng_channels_your_more\" = \"Show more\";\n\"lng_channels_your_less\" = \"Show less\";\n\"lng_channels_recommended\" = \"Similar channels\";\n\"lng_bot_apps_your\" = \"Apps you use\";\n\"lng_bot_apps_popular\" = \"Grossing apps\";\n\"lng_bot_apps_which\" = \"Which apps are included here? {link}\";\n\"lng_bot_apps_which_link\" = \"Learn >\";\n\"lng_posts_title\" = \"Global Search\";\n\"lng_posts_start\" = \"Type a keyword to search all posts from public channels.\";\n\"lng_posts_subscribe\" = \"Subscribe to Premium\";\n\"lng_posts_need_subscribe\" = \"Global search is a Premium feature.\";\n\"lng_posts_search_button\" = \"Search {query}\";\n\"lng_posts_remaining#one\" = \"{count} free search remaining today\";\n\"lng_posts_remaining#other\" = \"{count} free searches remaining today\";\n\"lng_posts_subtitle_empty\" = \"Telegram News\";\n\"lng_posts_subtitle\" = \"Public posts\";\n\"lng_posts_limit_reached\" = \"Limit Reached\";\n\"lng_posts_limit_about#one\" = \"You can make up to {count} search query per day.\";\n\"lng_posts_limit_about#other\" = \"You can make up to {count} search queries per day.\";\n\"lng_posts_limit_search_paid\" = \"Search for {cost}\";\n\"lng_posts_limit_unlocks\" = \"free search unlocks in {duration}\";\n\"lng_posts_paid_spent#one\" = \"**{count} Star** spent on extra search.\";\n\"lng_posts_paid_spent#other\" = \"**{count} Stars** spent on extra search.\";\n\n\"lng_popular_apps_info_title\" = \"Top Mini Apps\";\n\"lng_popular_apps_info_text\" = \"This catalogue ranks mini apps based on their daily revenue, measured in Stars. To be listed, developers must set their main mini apps in {bot} (as described {link}), have over **1,000** daily users, and earn a daily revenue above **1,000** Stars, based on the weekly average.\";\n\"lng_popular_apps_info_bot\" = \"@botfather\";\n\"lng_popular_apps_info_here\" = \"here\";\n\"lng_popular_apps_info_url\" = \"https://core.telegram.org/bots/webapps#launching-the-main-mini-app\";\n\"lng_popular_apps_info_confirm\" = \"Understood\";\n\n\"lng_font_box_title\" = \"Choose font family\";\n\"lng_font_default\" = \"Default\";\n\"lng_font_system\" = \"System font\";\n\"lng_font_not_found\" = \"Font not found.\";\n\n\"lng_search_tab_my_messages\" = \"My Messages\";\n\"lng_search_tab_this_topic\" = \"This Topic\";\n\"lng_search_tab_this_chat\" = \"This Chat\";\n\"lng_search_tab_this_channel\" = \"This Channel\";\n\"lng_search_tab_this_group\" = \"This Group\";\n\"lng_search_tab_public_posts\" = \"Public Posts\";\n\"lng_search_tab_no_results\" = \"No Results\";\n\"lng_search_tab_no_results_text\" = \"There were no results for \\\"{query}\\\".\";\n\"lng_search_tab_no_results_retry\" = \"Try another hashtag.\";\n\"lng_search_tab_by_hashtag\" = \"Enter a hashtag to find messages containing it.\";\n\"lng_search_tab_try_in_all\" = \"Search in All Messages\";\n\n\"lng_contact_details_button\" = \"View Contact\";\n\"lng_contact_details_title\" = \"Contact details\";\n\"lng_contact_details_phone\" = \"Phone\";\n\"lng_contact_details_phone_main\" = \"Main Phone\";\n\"lng_contact_details_phone_home\" = \"Home Phone\";\n\"lng_contact_details_phone_mobile\" = \"Mobile Phone\";\n\"lng_contact_details_phone_work\" = \"Work Phone\";\n\"lng_contact_details_phone_other\" = \"Other Phone\";\n\"lng_contact_details_email\" = \"Email\";\n\"lng_contact_details_address\" = \"Address\";\n\"lng_contact_details_url\" = \"URL\";\n\"lng_contact_details_note\" = \"Note\";\n\"lng_contact_details_birthday\" = \"Birthday\";\n\"lng_contact_details_organization\" = \"Organization\";\n\n\"lng_qr_box_quality\" = \"Quality\";\n\"lng_qr_box_quality1\" = \"Normal\";\n\"lng_qr_box_quality2\" = \"High\";\n\"lng_qr_box_quality3\" = \"Very High\";\n\"lng_qr_box_transparent_background\" = \"Transparent Background\";\n\"lng_qr_box_font_size\" = \"Font size\";\n\n\"lng_frozen_bar_title\" = \"Your account is frozen!\";\n\"lng_frozen_bar_text\" = \"Click to view details {arrow}\";\n\"lng_frozen_restrict_title\" = \"Your account is frozen\";\n\"lng_frozen_restrict_text\" = \"Click to view details\";\n\"lng_frozen_title\" = \"Your Account is Frozen\";\n\"lng_frozen_subtitle1\" = \"Violation of Terms\";\n\"lng_frozen_text1\" = \"Your account was frozen for breaking Telegram's Terms and Conditions.\";\n\"lng_frozen_subtitle2\" = \"Read-Only Mode\";\n\"lng_frozen_text2\" = \"You can access your account but can't send messages or take actions.\";\n\"lng_frozen_subtitle3\" = \"Appeal Before Deactivation\";\n\"lng_frozen_text3\" = \"Appeal via {link} before {date}, or your account will be deleted.\";\n\"lng_frozen_appeal_button\" = \"Submit an Appeal\";\n\n\"lng_age_verify_title\" = \"Age Verification\";\n\"lng_age_verify_mobile\" = \"Please open this media in the official Telegram app for Android or iOS to verify your age.\";\n\"lng_age_verify_here\" = \"This is a one-time process using your phone's camera. Your selfie will not be stored by Telegram.\";\n\"lng_age_verify_button\" = \"Verify My Age\";\n\"lng_age_verify_sorry_title\" = \"Age Check Failed\";\n\"lng_age_verify_sorry_text\" = \"Sorry, you can't view 18+ content.\";\n\n\"lng_context_bank_card_copy\" = \"Copy Card Number\";\n\"lng_context_bank_card_copied\" = \"Card number copied to clipboard.\";\n\n\"lng_summarize_header_title\" = \"AI summary\";\n\"lng_summarize_header_about\" = \"Click to see original text\";\n\n\"lng_calendar\" = \"Calendar\";\n\n\"lng_new_window_tooltip_ctrl\" = \"Use Ctrl+Click to open in New Window.\";\n\"lng_new_window_tooltip_cmd\" = \"Use Cmd+Click to open in New Window.\";\n\n\"lng_rename_file\" = \"Rename file\";\n\n\"lng_sr_playback_order\" = \"Playback order\";\n\"lng_sr_player_close\" = \"Close media player\";\n\"lng_sr_group_call_menu\" = \"Video chat menu\";\n\"lng_sr_search_date\" = \"Search by date\";\n\"lng_sr_cancel_search\" = \"Cancel search\";\n\"lng_sr_clear_search\" = \"Clear search\";\n\"lng_sr_scroll_to_top\" = \"Scroll to top\";\n\"lng_sr_verified_badge\" = \"Verified\";\n\"lng_sr_bot_verified_badge\" = \"Verified Bot\";\n\"lng_sr_profile_menu\" = \"Profile menu\";\n\"lng_sr_close_panel\" = \"Close panel\";\n\n\"lng_ai_compose_title\" = \"AI Editor\";\n\"lng_ai_compose_apply\" = \"Apply\";\n\"lng_ai_compose_tab_translate\" = \"Translate\";\n\"lng_ai_compose_tab_style\" = \"Style\";\n\"lng_ai_compose_tab_fix\" = \"Fix\";\n\"lng_ai_compose_original\" = \"Original\";\n\"lng_ai_compose_result\" = \"Result\";\n\"lng_ai_compose_to_language\" = \"To {language}\";\n\"lng_ai_compose_name_style\" = \"{name} ({style})\";\n\"lng_ai_compose_style_neutral\" = \"Neutral\";\n\"lng_ai_compose_emojify\" = \"emojify\";\n\"lng_ai_compose_error\" = \"AI request failed.\";\n\"lng_ai_compose_tooltip\" = \"Rewrite, translate, or correct your text using AI.\";\n\"lng_ai_compose_flood_title\" = \"Daily limit reached\";\n\"lng_ai_compose_flood_text\" = \"Get {link} for **50x** more AI text transformations per day.\";\n\"lng_ai_compose_flood_link\" = \"Telegram Premium\";\n\"lng_ai_compose_increase_limit\" = \"Increase Limit\";\n\"lng_ai_compose_select_style\" = \"Select Style\";\n\"lng_ai_compose_apply_style\" = \"Apply Style\";\n\"lng_ai_compose_style_tooltip\" = \"Choose Style\";\n\n\"lng_send_as_file_tooltip\" = \"Send text as a file.\";\n\n// Wnd specific\n\n\"lng_wnd_choose_program_menu\" = \"Choose Default Program...\";\n\n\"lng_wnd_menu_undo\" = \"Undo\";\n\"lng_wnd_menu_redo\" = \"Redo\";\n\n// Linux specific\n\n\"lng_linux_menu_undo\" = \"Undo\";\n\"lng_linux_menu_redo\" = \"Redo\";\n\"lng_linux_menu_tools\" = \"Tools\";\n\"lng_linux_menu_help\" = \"Help\";\n\n\"lng_linux_no_audio_prefs\" = \"You don't have any audio configuration applications installed.\";\n\n// Mac specific\n\n\"lng_mac_choose_program_menu\" = \"Other...\";\n\n\"lng_mac_choose_app\" = \"Choose Application\";\n\"lng_mac_choose_text\" = \"Choose an application to open the document \\\"{file}\\\".\";\n\"lng_mac_enable_filter\" = \"Show:\";\n\"lng_mac_recommended_apps\" = \"Recommended Applications\";\n\"lng_mac_all_apps\" = \"All Applications\";\n\"lng_mac_always_open_with\" = \"Always Open With\";\n\"lng_mac_this_app_can_open\" = \"This application can open \\\"{file}\\\".\";\n\"lng_mac_not_known_app\" = \"It's not known if this application can open \\\"{file}\\\".\";\n\n\"lng_mac_menu_services\" = \"Services\";\n\"lng_mac_menu_hide_telegram\" = \"Hide {telegram}\";\n\"lng_mac_menu_hide_others\" = \"Hide Others\";\n\"lng_mac_menu_show_all\" = \"Show All\";\n\"lng_mac_menu_preferences\" = \"Preferences...\";\n\"lng_mac_menu_quit_telegram\" = \"Quit {telegram}\";\n\"lng_mac_menu_about_telegram\" = \"About {telegram}\";\n//\"lng_mac_menu_warn_before_quit\" = \"Warn Before Quitting ({text})\";\n\"lng_mac_menu_file\" = \"File\";\n\"lng_mac_menu_logout\" = \"Log Out\";\n\"lng_mac_menu_edit\" = \"Edit\";\n\"lng_mac_menu_undo\" = \"Undo\";\n\"lng_mac_menu_redo\" = \"Redo\";\n\"lng_mac_menu_cut\" = \"Cut\";\n\"lng_mac_menu_copy\" = \"Copy\";\n\"lng_mac_menu_paste\" = \"Paste\";\n\"lng_mac_menu_delete\" = \"Delete\";\n\"lng_mac_menu_select_all\" = \"Select All\";\n\"lng_mac_menu_window\" = \"Window\";\n\"lng_mac_menu_contacts\" = \"Contacts\";\n\"lng_mac_menu_add_contact\" = \"Add Contact\";\n\"lng_mac_menu_new_group\" = \"New Group\";\n\"lng_mac_menu_new_channel\" = \"New Channel\";\n\"lng_mac_menu_show\" = \"Show Telegram\";\n\"lng_mac_menu_emoji_and_symbols\" = \"Emoji & Symbols\";\n\"lng_mac_menu_fullscreen\" = \"Toggle Full Screen\";\n\n\"lng_mac_menu_player_pause\" = \"Pause\";\n\"lng_mac_menu_player_resume\" = \"Resume\";\n\"lng_mac_menu_player_next\" = \"Next\";\n\"lng_mac_menu_player_previous\" = \"Previous\";\n\"lng_mac_menu_profiles\" = \"Profiles\";\n\n\"lng_mac_touchbar_favorite_stickers\" = \"Favorite stickers\";\n\n\"lng_mac_hold_to_quit\" = \"Hold {text} to Quit\";\n\n// Keys finished\n"
  },
  {
    "path": "Telegram/Resources/langs/nl.lproj/Localizable.strings",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n"
  },
  {
    "path": "Telegram/Resources/langs/pt-BR.lproj/Localizable.strings",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n"
  },
  {
    "path": "Telegram/Resources/langs/refresh.py",
    "content": "import os, sys, requests, re\nfrom os.path import expanduser\n\nfilename = ''\nfor arg in sys.argv:\n  if re.match(r'.*\\.strings$', arg):\n    filename = expanduser(arg)\n\nresult = ''\nif not os.path.isfile(filename):\n  print(\"File not found.\")\n  sys.exit(1)\n\nwith open(filename) as f:\n  for line in f:\n    if re.match(r'\\s*\\/\\* new from [a-zA-Z0-9\\.]+ \\*\\/\\s*', line):\n      continue\n    if re.match(r'\\\"lng_[a-z_]+\\#(zero|two|few|many)\\\".+', line):\n      continue\n    result = result + line\n\nremove = 0\nwhile (len(result) > remove + 1) and (result[len(result) - remove - 1] == '\\n') and (result[len(result) - remove - 2] == '\\n'):\n  remove = remove + 1\nresult = result[:len(result) - remove]\n\nwith open('lang.strings', 'w') as out:\n  out.write(result)\n\nsys.exit()\n"
  },
  {
    "path": "Telegram/Resources/langs/refresh.sh",
    "content": "set -e\nFullExecPath=$PWD\npushd `dirname $0` > /dev/null\nFullScriptPath=`pwd`\npopd > /dev/null\n\nFileName=\"$1\"\n\nif [ ! -d \"$FullScriptPath/../../../../DesktopPrivate\" ]; then\n  echo \"\"\n  echo \"This script is for building the production version of Telegram Desktop.\"\n  echo \"\"\n  echo \"For building custom versions please visit the build instructions page at:\"\n  echo \"https://github.com/telegramdesktop/tdesktop/#build-instructions\"\n  exit\nfi\n\nError () {\n  cd $FullExecPath\n  echo \"$1\"\n  exit 1\n}\n\nif [ ! -f \"$FileName\" ]; then\n  Error \"File '$FileName' not found.\"\nfi\n\ncd \"$FullScriptPath\"\npython refresh.py $FileName\n\n"
  },
  {
    "path": "Telegram/Resources/langs/stale.py",
    "content": "import os, sys, requests, re\n\nos.chdir()\n\nkeys = []\nwith open('lang.strings') as f:\n    for line in f:\n        m = re.match(r'\\\"(lng_[a-z_]+)(\\#[a-z]+)?\\\"', line)\n        if m:\n            keys.append(m.group(1))\n        elif not re.match(r'^\\s*$', line):\n            print('Bad line: ' + line)\n            sys.exit(1)\n\nprint('Keys: ' + str(len(keys)))\n\nsys.exit()\n"
  },
  {
    "path": "Telegram/Resources/numbers.txt",
    "content": "1876;JM;Jamaica;1876 XXX XXXX;11;\n1869;KN;Saint Kitts & Nevis;1869 XXX XXXX;11;\n1868;TT;Trinidad & Tobago;1868 XXX XXXX;11;\n1784;VC;Saint Vincent & the Grenadines;1784 XXX XXXX;11;\n1767;DM;Dominica;1767 XXX XXXX;11;\n1758;LC;Saint Lucia;1758 XXX XXXX;11;\n1721;SX;Sint Maarten;1721 XXX XXXX;11;\n1684;AS;American Samoa;1684 XXX XXXX;11;\n1671;GU;Guam;1671 XXX XXXX;11;\n1670;MP;Northern Mariana Islands;1670 XXX XXXX;11;\n1664;MS;Montserrat;1664 XXX XXXX;11;\n1649;TC;Turks & Caicos Islands;1649 XXX XXXX;11;\n1473;GD;Grenada;1473 XXX XXXX;11;\n1441;BM;Bermuda;1441 XXX XXXX;11;\n1345;KY;Cayman Islands;1345 XXX XXXX;11;\n1340;VI;US Virgin Islands;1340 XXX XXXX;11;\n1284;VG;British Virgin Islands;1284 XXX XXXX;11;\n1268;AG;Antigua & Barbuda;1268 XXX XXXX;11;\n1264;AI;Anguilla;1264 XXX XXXX;11;\n1246;BB;Barbados;1246 XXX XXXX;11;\n1242;BS;Bahamas;1242 XXX XXXX;11;\n998;UZ;Uzbekistan;998 XX XXXXXXX;12;\n996;KG;Kyrgyzstan\n995;GE;Georgia\n994;AZ;Azerbaijan;994 XX XXX XX XX;12;\n993;TM;Turkmenistan;993 XX XXXXXX;11;\n992;TJ;Tajikistan\n977;NP;Nepal\n976;MN;Mongolia\n975;BT;Bhutan\n974;QA;Qatar\n973;BH;Bahrain;973 XXXX XXXX;11;\n972;IL;Israel;972 XX XXX XXXX;12;\n971;AE;United Arab Emirates;971 XX XXX XXXX;12;\n970;PS;Palestine;970 XXX XX XXXX;12;\n968;OM;Oman;968 XXXX XXXX;11;\n967;YE;Yemen;967 XXX XXX XXX;12;\n966;SA;Saudi Arabia;\n965;KW;Kuwait;965 XXXX XXXX;11;\n964;IQ;Iraq;964 XXX XXX XXXX;13;\n963;SY;Syria\n962;JO;Jordan;962 X XXXX XXXX;12;\n961;LB;Lebanon\n960;MV;Maldives\n886;TW;Taiwan\n880;BD;Bangladesh\n856;LA;Laos\n855;KH;Cambodia\n853;MO;Macau\n852;HK;Hong Kong\n850;KP;North Korea\n692;MH;Marshall Islands\n691;FM;Micronesia\n690;TK;Tokelau\n689;PF;French Polynesia\n688;TV;Tuvalu\n687;NC;New Caledonia\n686;KI;Kiribati\n685;WS;Samoa\n683;NU;Niue\n682;CK;Cook Islands\n681;WF;Wallis & Futuna\n680;PW;Palau\n679;FJ;Fiji\n678;VU;Vanuatu\n677;SB;Solomon Islands\n676;TO;Tonga\n675;PG;Papua New Guinea\n674;NR;Nauru\n673;BN;Brunei Darussalam;673 XXX XXXX;10;\n672;NF;Norfolk Island\n670;TL;Timor-Leste\n599;BQ;Bonaire, Sint Eustatius & Saba\n599;CW;Curaçao\n598;UY;Uruguay;598 XXXX XXXX;11;\n597;SR;Suriname;597 XXX XXXX;10;\n596;MQ;Martinique\n595;PY;Paraguay;595 XXX XXX XXX;12;\n594;GF;French Guiana\n593;EC;Ecuador\n592;GY;Guyana\n591;BO;Bolivia;591 X XXX XXXX;11;\n590;GP;Guadeloupe\n509;HT;Haiti\n508;PM;Saint Pierre & Miquelon\n507;PA;Panama;507 XXXX XXXX;11;\n506;CR;Costa Rica\n505;NI;Nicaragua;505 XXXX XXXX;11;\n504;HN;Honduras;504 XXXX XXXX;11;\n503;SV;El Salvador;503 XXXX XXXX;11;\n502;GT;Guatemala;502 X XXX XXXX;11;\n501;BZ;Belize\n500;FK;Falkland Islands\n423;LI;Liechtenstein\n421;SK;Slovakia\n420;CZ;Czech Republic\n389;MK;Macedonia\n387;BA;Bosnia & Herzegovina\n386;SI;Slovenia\n385;HR;Croatia\n383;XK;Kosovo;383 XXXX XXXX;11;\n382;ME;Montenegro\n381;RS;Serbia;381 XX XXX XXXX;12;\n380;UA;Ukraine;380 XX XXX XX XX;12;\n378;SM;San Marino;378 XXX XXX XXXX;13;\n377;MC;Monaco;377 XXXX XXXX;11;\n376;AD;Andorra;376 XX XX XX;9;\n375;BY;Belarus;375 XX XXX XXXX;12;\n374;AM;Armenia;374 XX XXX XXX;11;\n373;MD;Moldova;373 XX XXX XXX;11;\n372;EE;Estonia;\n371;LV;Latvia;371 XXX XXXXX;11;\n370;LT;Lithuania;370 XXX XXXXX;11;\n359;BG;Bulgaria;\n358;FI;Finland;\n357;CY;Cyprus;357 XXXX XXXX;11;\n356;MT;Malta;356 XX XX XX XX;11;\n355;AL;Albania;355 XX XXX XXXX;12;\n354;IS;Iceland;354 XXX XXXX;10;\n353;IE;Ireland;353 XX XXX XXXX;12;\n352;LU;Luxembourg\n351;PT;Portugal;351 X XXXX XXXX;12;\n350;GI;Gibraltar;350 XXXX XXXX;11;\n299;GL;Greenland;299 XXX XXX;9;\n298;FO;Faroe Islands;298 XXX XXX;9;\n297;AW;Aruba;297 XXX XXXX;10;\n291;ER;Eritrea;291 X XXX XXX;10;\n290;SH;Saint Helena;290 XX XXX;8;\n269;KM;Comoros;269 XXX XXXX;10;\n268;SZ;Swaziland;268 XXXX XXXX;11;\n267;BW;Botswana;267 XX XXX XXX;11;\n266;LS;Lesotho;266 XX XXX XXX;11;\n265;MW;Malawi\n264;NA;Namibia;264 XX XXX XXXX;12;\n263;ZW;Zimbabwe;263 XX XXX XXXX;12;\n262;RE;Réunion;262 XXX XXX XXX;12;\n261;MG;Madagascar;261 XX XX XXX XX;12;\n260;ZM;Zambia;260 XX XXX XXXX;12;\n258;MZ;Mozambique;258 XX XXX XXXX;12;\n257;BI;Burundi;257 XX XX XXXX;11;\n256;UG;Uganda;256 XX XXX XXXX;12;\n255;TZ;Tanzania;255 XX XXX XXXX;12;\n254;KE;Kenya;254 XXX XXX XXX;12;\n253;DJ;Djibouti;253 XX XX XX XX;11;\n252;SO;Somalia;252 XX XXX XXX;11;\n251;ET;Ethiopia;251 XX XXX XXXX;12;\n250;RW;Rwanda;250 XXX XXX XXX;12;\n249;SD;Sudan;249 XX XXX XXXX;12;\n248;SC;Seychelles;248 X XX XX XX;10;\n247;SH;Saint Helena;247 XXXX;7;\n246;IO;Diego Garcia;246 XXX XXXX;10;\n245;GW;Guinea-Bissau;245 XXX XXXX;10;\n244;AO;Angola;244 XXX XXX XXX;12;\n243;CD;Congo (Dem. Rep.);243 XX XXX XXXX;12;\n242;CG;Congo (Rep.);242 XX XXX XXXX;12;\n241;GA;Gabon;241 X XX XX XX;10;\n240;GQ;Equatorial Guinea;240 XXX XXX XXX;12;\n239;ST;São Tomé & Príncipe;239 XX XXXXX;10;\n238;CV;Cape Verde;238 XXX XXXX;10;\n237;CM;Cameroon;237 XXXX XXXX;11;\n236;CF;Central African Rep.;236 XX XX XX XX;11;\n235;TD;Chad;235 XX XX XX XX;11;\n234;NG;Nigeria\n233;GH;Ghana\n232;SL;Sierra Leone;232 XX XXX XXX;11;\n231;LR;Liberia;\n230;MU;Mauritius;\n229;BJ;Benin;229 XX XXX XXX;11;\n228;TG;Togo;228 XX XXX XXX;11;\n227;NE;Niger;227 XX XX XX XX;11;\n226;BF;Burkina Faso;226 XX XX XX XX;11;\n225;CI;Côte d`Ivoire;225 XX XXX XXX;11;\n224;GN;Guinea;224 XXX XXX XXX;12;\n223;ML;Mali;223 XXXX XXXX;11;\n222;MR;Mauritania;222 XXXX XXXX;11;\n221;SN;Senegal;221 XX XXX XXXX;12;\n220;GM;Gambia;220 XXX XXXX;10;\n218;LY;Libya;218 XX XXX XXXX;12;\n216;TN;Tunisia;216 XX XXX XXX;11;\n213;DZ;Algeria;213 XXX XX XX XX;12;\n212;MA;Morocco;212 XX XXX XXXX;12;\n211;SS;South Sudan;211 XX XXX XXXX;12;\n98;IR;Iran;98 XXX XXX XXXX;12;\n95;MM;Myanmar;\n94;LK;Sri Lanka;94 XX XXX XXXX;11;\n93;AF;Afghanistan;93 XXX XXX XXX;11;\n92;PK;Pakistan;92 XXX XXX XXXX;12;\n91;IN;India;91 XXXXX XXXXX;12;\n90;TR;Turkey;90 XXX XXX XXXX;12\n86;CN;China;86 XXX XXXX XXXX;13;\n84;VN;Vietnam;\n82;KR;South Korea;\n81;JP;Japan;81 XX XXXX XXXX;12;\n66;TH;Thailand;66 X XXXX XXXX;11;\n65;SG;Singapore;65 XXXX XXXX;10;\n64;NZ;New Zealand;\n63;PH;Philippines;63 XXX XXX XXXX;12;\n62;ID;Indonesia;\n61;AU;Australia;61 XXX XXX XXX;11;\n60;MY;Malaysia;\n58;VE;Venezuela;58 XXX XXX XXXX;12;\n57;CO;Colombia;57 XXX XXX XXXX;12;\n56;CL;Chile;56 X XXXX XXXX;11;\n55;BR;Brazil;55 XX XXXXX XXXX;13;\n54;AR;Argentina;\n53;CU;Cuba;53 XXXX XXXX;10;\n52;MX;Mexico;\n51;PE;Peru;51 XXX XXX XXX;11;\n49;DE;Germany;\n48;PL;Poland;48 XXX XXX XXX;11;\n47;NO;Norway;47 XXXX XXXX;10;\n46;SE;Sweden;46 XX XXX XXXX;11;\n45;DK;Denmark;45 XXXX XXXX;10;\n44;GB;United Kingdom;44 XXXX XXXXXX;12;\n43;AT;Austria;\n41;CH;Switzerland;41 XX XXX XXXX;11;\n40;RO;Romania;40 XXX XXX XXX;11;\n39;IT;Italy;39 XXX XXX XXXX;12;\n36;HU;Hungary;36 XX XXX XXXX;11;\n34;ES;Spain;34 XXX XXX XXX;11;\n33;FR;France;33 X XX XX XX XX;11;\n32;BE;Belgium;32 XXX XX XX XX;11;\n31;NL;Netherlands;31 X XX XX XX XX;11;\n30;GR;Greece;30 XX XXXX XXXX;12;\n27;ZA;South Africa;27 XX XXX XXXX;11;\n20;EG;Egypt;20 XX XXX XXXX;11;\n7;RU;Russian Federation;7 XXX XXX XX XX;11;\n7;KZ;Kazakhstan;7 XXX XXX XX XX;11\n1;US;USA;1 XXX XXX XXXX;11;\n1;PR;Puerto Rico;1 XXX XXX XXXX;11;\n1;DO;Dominican Rep.;1 XXX XXX XXXX;11;\n1;CA;Canada;1 XXX XXX XXXX;11;"
  },
  {
    "path": "Telegram/Resources/picker_html/picker.css",
    "content": ":root {\n\t--font-sans: -apple-system, BlinkMacSystemFont, avenir next, avenir, Segoe UI Variable Text, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, tahoma, arial, sans-serif;\n}\n\nhtml {\n\twidth: 100%;\n\theight: 100%;\n\tpadding: 0;\n\tmargin: 0;\n}\n\nbody {\n\tfont-family: var(--font-sans);\n\twidth: 100%;\n\theight: 100%;\n\tpadding: 0;\n\tmargin: 0;\n\tbackground-color: var(--td-window-bg);\n\tcolor: var(--td-window-fg);\n}\n\nhtml.custom_scroll ::-webkit-scrollbar {\n\tborder-radius: 5px !important;\n\tborder: 3px solid transparent !important;\n\tbackground-color: var(--td-scroll-bg) !important;\n\tbackground-clip: content-box !important;\n\twidth: 10px !important;\n}\nhtml.custom_scroll ::-webkit-scrollbar:hover {\n\tbackground-color: var(--td-scroll-bg-over) !important;\n}\nhtml.custom_scroll ::-webkit-scrollbar-thumb {\n\tborder-radius: 5px !important;\n\tborder: 3px solid transparent !important;\n\tbackground-color: var(--td-scroll-bar-bg) !important;\n\tbackground-clip: content-box !important;\n}\nhtml.custom_scroll ::-webkit-scrollbar-thumb:hover {\n\tbackground-color: var(--td-scroll-bar-bg-over) !important;\n}\n\n#map {\n\tposition: relative;\n\twidth: 100%;\n\theight: 100%;\n}\n#marker {\n\tpointer-events: none;\n\tdisplay: none;\n\tz-index: 2;\n\tposition: absolute;\n\twidth: 100%;\n\theight: 100%;\n\tjustify-content: center;\n\talign-items: center;\n}\n#marker_drop {\n\tmargin-bottom: 0px;\n\ttransition: margin 160ms ease-in-out;\n}\n#marker_drop.moving {\n\tmargin-bottom: 24px;\n}\n#marker_shadow {\n\tposition: absolute;\n}\n#search_venues {\n\tposition: absolute;\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\tz-index: 2;\n\ttop: -30px;\n\ttransition: top 200ms ease-in-out;\n}\n#search_venues.shown {\n\ttop: 6px;\n}\n#search_venues_inner {\n\tposition: relative;\n\toverflow: hidden;\n\tfont-size: 13px;\n\tfont-weight: 500;\n\tbackground: var(--td-window-bg);\n\tcolor: var(--td-window-active-text-fg);\n\tcursor: pointer;\n\tborder-radius: 14px;\n\tpadding: 5px 12px 6px;\n\tbox-shadow: 0 0 3px 0px var(--td-history-to-down-shadow);\n}\n#search_venues_inner:hover {\n\tbackground: var(--td-window-bg-over);\n}\n#search_venues_content {\n\tposition: relative;\n\tz-index: 2;\n}\n#search_venues_content:before {\n\tcontent: var(--td-lng-maps-places-in-area);\n}\n#search_venues_inner .ripple .inner {\n\tposition: absolute;\n\tborder-radius: 50%;\n\ttransform: scale(0);\n\topacity: 1;\n\tanimation: ripple 650ms cubic-bezier(0.22, 1, 0.36, 1) forwards;\n\tbackground-color: var(--td-window-bg-ripple);\n}\n#search_venues_inner .ripple.hiding {\n\tanimation: fadeOut 200ms linear forwards;\n}\n@keyframes ripple {\n\tto {\n\t\ttransform: scale(2);\n\t}\n}\n@keyframes fadeOut {\n\tto {\n\t\topacity: 0;\n\t}\n}\n"
  },
  {
    "path": "Telegram/Resources/picker_html/picker.js",
    "content": "var LocationPicker = {\n\tstartZoom: 14,\n\tflySpeed: 2.4,\n\tnotify: function(message) {\n\t\tif (window.external && window.external.invoke) {\n\t\t\twindow.external.invoke(JSON.stringify(message));\n\t\t}\n\t},\n\tframeKeyDown: function (e) {\n\t\tconst keyW = (e.key === 'w')\n\t\t\t|| (e.code === 'KeyW')\n\t\t\t|| (e.keyCode === 87);\n\t\tconst keyQ = (e.key === 'q')\n\t\t\t|| (e.code === 'KeyQ')\n\t\t\t|| (e.keyCode === 81);\n\t\tconst keyM = (e.key === 'm')\n\t\t\t|| (e.code === 'KeyM')\n\t\t\t|| (e.keyCode === 77);\n\t\tif ((e.metaKey || e.ctrlKey) && (keyW || keyQ || keyM)) {\n\t\t\te.preventDefault();\n\t\t\tLocationPicker.notify({\n\t\t\t\tevent: 'keydown',\n\t\t\t\tmodifier: e.ctrlKey ? 'ctrl' : 'cmd',\n\t\t\t\tkey: keyW ? 'w' : keyQ ? 'q' : 'm',\n\t\t\t});\n\t\t} else if (e.key === 'Escape' || e.keyCode === 27) {\n\t\t\te.preventDefault();\n\t\t\tLocationPicker.notify({\n\t\t\t\tevent: 'keydown',\n\t\t\t\tkey: 'escape',\n\t\t\t});\n\t\t}\n\t},\n\tisNight: function() {\n\t\tvar html = document.getElementsByTagName('html')[0];\n\t\treturn html.style.getPropertyValue('--td-night') == '1';\n\t},\n\tlightPreset: function() {\n\t\treturn LocationPicker.isNight() ? 'night' : 'day';\n\t},\n\tupdateStyles: function (styles) {\n\t\tif (LocationPicker.styles !== styles) {\n\t\t\tLocationPicker.styles = styles;\n\t\t\tdocument.getElementsByTagName('html')[0].style = styles;\n\n\t\t\tLocationPicker.map.setConfigProperty(\n\t\t\t\t'basemap',\n\t\t\t\t'lightPreset',\n\t\t\t\tLocationPicker.lightPreset());\n\t\t}\n\t},\n\tinit: function (params) {\n\t\tmapboxgl.accessToken = params.token;\n\t\tif (location.hostname != 'desktop-app-resource') {\n\t\t\tmapboxgl.config.API_URL = location.protocol + '//' + location.host + '/api.mapbox.com';\n\t\t}\n\n\t\tvar options = { container: 'map', config: {\n\t\t\tbasemap: { lightPreset: LocationPicker.lightPreset() }\n\t\t} };\n\t\tvar center = params.center;\n\t\tif (center) {\n\t\t\tcenter = [center[1], center[0]];\n\t\t\toptions.center = center;\n\t\t\toptions.zoom = LocationPicker.startZoom;\n\t\t} else if (params.bounds) {\n\t\t\toptions.bounds = params.bounds;\n\t\t\tcenter = new mapboxgl.LngLatBounds(params.bounds).getCenter();\n\t\t} else {\n\t\t\tcenter = [0, 0];\n\t\t}\n\t\tLocationPicker.map = new mapboxgl.Map(options);\n\t\tLocationPicker.createMarker(center);\n\t\tLocationPicker.trackMovement();\n\t\tLocationPicker.initSearchVenueRipple();\n\t},\n\tmarker: function() {\n\t\treturn document.getElementById('marker_drop');\n\t},\n\tcreateMarker: function(center) {\n\t\tdocument.getElementById('marker').style.display = 'flex';\n\t},\n\tclearMovingTimer: function() {\n\t\tif (LocationPicker.clearMovingTimeoutId) {\n\t\t\tclearTimeout(LocationPicker.clearMovingTimeoutId);\n\t\t\tLocationPicker.clearMovingTimeoutId = 0;\n\t\t}\n\t},\n\tstartMovingTimer: function(done) {\n\t\tLocationPicker.clearMovingTimer();\n\t\tLocationPicker.clearMovingTimeoutId = setTimeout(done, 500);\n\t},\n\ttrackMovement: function() {\n\t\tLocationPicker.map.on('movestart', function() {\n\t\t\tLocationPicker.marker().classList.add('moving');\n\t\t\tLocationPicker.clearMovingTimer();\n\t\t\tLocationPicker.toggleSearchVenues(false);\n\t\t\tLocationPicker.notify({ event: 'move_start' });\n\t\t});\n\t\tLocationPicker.map.on('moveend', function() {\n\t\t\tLocationPicker.startMovingTimer(function() {\n\t\t\t\tLocationPicker.marker().classList.remove('moving');\n\t\t\t\tLocationPicker.notify({\n\t\t\t\t\tevent: 'move_end',\n\t\t\t\t\tlatitude: LocationPicker.map.getCenter().lat,\n\t\t\t\t\tlongitude: LocationPicker.map.getCenter().lng\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\t},\n\tnarrowTo: function (point) {\n\t\tLocationPicker.map.flyTo({\n\t\t\tcenter: [point[1], point[0]],\n\t\t\tzoom: LocationPicker.startZoom,\n\t\t\tspeed: LocationPicker.flySpeed,\n\t\t});\n\t},\n\tsend: function () {\n\t\tLocationPicker.notify({\n\t\t\tevent: 'send',\n\t\t\tlatitude: LocationPicker.map.getCenter().lat,\n\t\t\tlongitude: LocationPicker.map.getCenter().lng\n\t\t});\n\t},\n\taddRipple: function (button, x, y) {\n\t\tconst ripple = document.createElement('span');\n\t\tripple.classList.add('ripple');\n\n\t\tconst inner = document.createElement('span');\n\t\tinner.classList.add('inner');\n\n\t\tvar rect = button.getBoundingClientRect();\n\t\tx -= rect.x;\n\t\ty -= rect.y;\n\n\t\tconst mx = button.clientWidth - x;\n\t\tconst my = button.clientHeight - y;\n\t\tconst sq1 = x * x + y * y;\n\t\tconst sq2 = mx * mx + y * y;\n\t\tconst sq3 = x * x + my * my;\n\t\tconst sq4 = mx * mx + my * my;\n\t\tconst radius = Math.sqrt(Math.max(sq1, sq2, sq3, sq4));\n\n\t\tinner.style.width = inner.style.height = `${2 * radius}px`;\n\t\tinner.style.left = `${x - radius}px`;\n\t\tinner.style.top = `${y - radius}px`;\n\t\tinner.classList.add('inner');\n\n\t\tripple.addEventListener('animationend', function (e) {\n\t\t\tif (e.animationName === 'fadeOut') {\n\t\t\t\tripple.remove();\n\t\t\t}\n\t\t});\n\n\t\tripple.appendChild(inner);\n\t\tbutton.appendChild(ripple);\n\t},\n\tstopRipples: function (button) {\n\t\tconst id = button.id ? button.id : button;\n\t\tbutton = document.getElementById(id);\n\t\tconst ripples = button.getElementsByClassName('ripple');\n\t\tfor (var i = 0; i < ripples.length; ++i) {\n\t\t\tconst ripple = ripples[i];\n\t\t\tif (!ripple.classList.contains('hiding')) {\n\t\t\t\tripple.classList.add('hiding');\n\t\t\t}\n\t\t}\n\t},\n\tinitSearchVenueRipple: function() {\n\t\tvar button = document.getElementById('search_venues_inner');\n\t\tbutton.addEventListener('mousedown', function (e) {\n\t\t\tLocationPicker.addRipple(e.currentTarget, e.clientX, e.clientY);\n\t\t\tLocationPicker.searchVenuesPressed = true;\n\t\t});\n\t\tbutton.addEventListener('mouseup', function (e) {\n\t\t\tconst id = e.currentTarget.id;\n\t\t\tsetTimeout(function () {\n\t\t\t\tLocationPicker.stopRipples(id);\n\t\t\t}, 0);\n\t\t\tif (LocationPicker.searchVenuesPressed) {\n\t\t\t\tLocationPicker.searchVenuesPressed = false;\n\t\t\t\tLocationPicker.toggleSearchVenues(false);\n\t\t\t\tLocationPicker.notify({\n\t\t\t\t\tevent: 'search_venues',\n\t\t\t\t\tlatitude: LocationPicker.map.getCenter().lat,\n\t\t\t\t\tlongitude: LocationPicker.map.getCenter().lng\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t\tbutton.addEventListener('mouseleave', function (e) {\n\t\t\tLocationPicker.stopRipples(e.currentTarget);\n\t\t\tLocationPicker.searchVenuesPressed = false;\n\t\t});\n\t},\n\ttoggleSearchVenues: function(shown) {\n\t\tvar button = document.getElementById('search_venues');\n\t\tbutton.classList.toggle('shown', shown);\n\t},\n};\n"
  },
  {
    "path": "Telegram/Resources/qrc/emoji_1.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/gui\">\n    <file alias=\"emoji/emoji_1.webp\">../emoji/emoji_1.webp</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/emoji_2.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/gui\">\n    <file alias=\"emoji/emoji_2.webp\">../emoji/emoji_2.webp</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/emoji_3.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/gui\">\n    <file alias=\"emoji/emoji_3.webp\">../emoji/emoji_3.webp</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/emoji_4.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/gui\">\n    <file alias=\"emoji/emoji_4.webp\">../emoji/emoji_4.webp</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/emoji_5.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/gui\">\n    <file alias=\"emoji/emoji_5.webp\">../emoji/emoji_5.webp</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/emoji_6.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/gui\">\n    <file alias=\"emoji/emoji_6.webp\">../emoji/emoji_6.webp</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/emoji_7.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/gui\">\n    <file alias=\"emoji/emoji_7.webp\">../emoji/emoji_7.webp</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/emoji_8.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/gui\">\n    <file alias=\"emoji/emoji_8.webp\">../emoji/emoji_8.webp</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/emoji_preview.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/gui\">\n    <file alias=\"emoji/set0_preview.webp\">../emoji/set0_preview.webp</file>\n    <file alias=\"emoji/set1_preview.webp\">../emoji/set1_preview.webp</file>\n    <file alias=\"emoji/set2_preview.webp\">../emoji/set2_preview.webp</file>\n    <file alias=\"emoji/set3_preview.webp\">../emoji/set3_preview.webp</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/telegram/animations.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/animations\">\n    <file alias=\"blocked_peers_empty.tgs\">../../animations/blocked_peers_empty.tgs</file>\n    <file alias=\"change_number.tgs\">../../animations/change_number.tgs</file>\n    <file alias=\"filters.tgs\">../../animations/filters.tgs</file>\n    <file alias=\"cloud_filters.tgs\">../../animations/cloud_filters.tgs</file>\n    <file alias=\"local_passcode_enter.tgs\">../../animations/local_passcode_enter.tgs</file>\n    <file alias=\"cloud_password/intro.tgs\">../../animations/cloud_password/intro.tgs</file>\n    <file alias=\"cloud_password/password_input.tgs\">../../animations/cloud_password/password_input.tgs</file>\n    <file alias=\"cloud_password/hint.tgs\">../../animations/cloud_password/hint.tgs</file>\n    <file alias=\"cloud_password/email.tgs\">../../animations/cloud_password/email.tgs</file>\n    <file alias=\"cloud_password/validate.tgs\">../../animations/cloud_password/validate.tgs</file>\n    <file alias=\"ttl.tgs\">../../animations/ttl.tgs</file>\n    <file alias=\"discussion.tgs\">../../animations/discussion.tgs</file>\n    <file alias=\"stats.tgs\">../../animations/stats.tgs</file>\n    <file alias=\"stats_boosts.tgs\">../../animations/stats_boosts.tgs</file>\n    <file alias=\"stats_earn.tgs\">../../animations/stats_earn.tgs</file>\n    <file alias=\"voice_ttl_idle.tgs\">../../animations/voice_ttl_idle.tgs</file>\n    <file alias=\"voice_ttl_start.tgs\">../../animations/voice_ttl_start.tgs</file>\n    <file alias=\"chat/voice_to_video.tgs\">../../animations/chat/voice_to_video.tgs</file>\n    <file alias=\"chat/video_to_voice.tgs\">../../animations/chat/video_to_voice.tgs</file>\n    <file alias=\"chat/sparkles_emoji.tgs\">../../animations/chat/sparkles_emoji.tgs</file>\n    <file alias=\"chat/white_flag_emoji.tgs\">../../animations/chat/white_flag_emoji.tgs</file>\n    <file alias=\"palette.tgs\">../../animations/palette.tgs</file>\n    <file alias=\"sleep.tgs\">../../animations/sleep.tgs</file>\n    <file alias=\"greeting.tgs\">../../animations/greeting.tgs</file>\n    <file alias=\"location.tgs\">../../animations/location.tgs</file>\n    <file alias=\"robot.tgs\">../../animations/robot.tgs</file>\n    <file alias=\"writing.tgs\">../../animations/writing.tgs</file>\n    <file alias=\"hours.tgs\">../../animations/hours.tgs</file>\n    <file alias=\"phone.tgs\">../../animations/phone.tgs</file>\n    <file alias=\"chat_link.tgs\">../../animations/chat_link.tgs</file>\n    <file alias=\"diamond.tgs\">../../animations/diamond.tgs</file>\n    <file alias=\"collectible_username.tgs\">../../animations/collectible_username.tgs</file>\n    <file alias=\"collectible_phone.tgs\">../../animations/collectible_phone.tgs</file>\n    <file alias=\"search.tgs\">../../animations/search.tgs</file>\n    <file alias=\"noresults.tgs\">../../animations/noresults.tgs</file>\n    <file alias=\"hello_status.tgs\">../../animations/hello_status.tgs</file>\n    <file alias=\"starref_link.tgs\">../../animations/starref_link.tgs</file>\n    <file alias=\"media_forbidden.tgs\">../../animations/media_forbidden.tgs</file>\n    <file alias=\"topics.tgs\">../../animations/edit_peers/topics.tgs</file>\n    <file alias=\"topics_tabs.tgs\">../../animations/edit_peers/topics_tabs.tgs</file>\n    <file alias=\"topics_list.tgs\">../../animations/edit_peers/topics_list.tgs</file>\n    <file alias=\"direct_messages.tgs\">../../animations/edit_peers/direct_messages.tgs</file>\n    <file alias=\"no_chats.tgs\">../../animations/no_chats.tgs</file>\n    <file alias=\"transcribe_loading.tgs\">../../animations/transcribe_loading.tgs</file>\n    <file alias=\"cake.tgs\">../../animations/cake.tgs</file>\n    <file alias=\"camera_outline.tgs\">../../animations/camera_outline.tgs</file>\n    <file alias=\"photo_suggest_icon.tgs\">../../animations/photo_suggest_icon.tgs</file>\n    <file alias=\"toast/saved_messages.tgs\">../../animations/toast/saved_messages.tgs</file>\n    <file alias=\"toast/tagged.tgs\">../../animations/toast/tagged.tgs</file>\n    <file alias=\"my_gifts_empty.tgs\">../../animations/my_gifts_empty.tgs</file>\n    <file alias=\"toast/chats_filter_in.tgs\">../../animations/toast/chats_filter_in.tgs</file>\n    <file alias=\"rtmp.tgs\">../../animations/rtmp.tgs</file>\n    <file alias=\"show_or_premium_lastseen.tgs\">../../animations/show_or_premium_lastseen.tgs</file>\n    <file alias=\"show_or_premium_readtime.tgs\">../../animations/show_or_premium_readtime.tgs</file>\n    <file alias=\"passkeys.tgs\">../../animations/passkeys.tgs</file>\n    <file alias=\"ban.tgs\">../../animations/ban.tgs</file>\n    <file alias=\"cocoon.tgs\">../../animations/cocoon.tgs</file>\n    <file alias=\"craft_failed.tgs\">../../animations/craft_failed.tgs</file>\n    <file alias=\"stop.tgs\">../../animations/stop.tgs</file>\n    <file alias=\"toast_hide_results.tgs\">../../icons/poll/toast_hide_results.tgs</file>\n    <file alias=\"uploading.tgs\">../../icons/poll/uploading.tgs</file>\n\n    <file alias=\"profile_muting.tgs\">../../animations/profile/profile_muting.tgs</file>\n    <file alias=\"profile_unmuting.tgs\">../../animations/profile/profile_unmuting.tgs</file>\n\n    <file alias=\"dice_idle.tgs\">../../animations/dice/dice_idle.tgs</file>\n    <file alias=\"dart_idle.tgs\">../../animations/dice/dart_idle.tgs</file>\n    <file alias=\"bball_idle.tgs\">../../animations/dice/bball_idle.tgs</file>\n    <file alias=\"fball_idle.tgs\">../../animations/dice/fball_idle.tgs</file>\n    <file alias=\"slot_0_idle.tgs\">../../animations/dice/slot_0_idle.tgs</file>\n    <file alias=\"slot_1_idle.tgs\">../../animations/dice/slot_1_idle.tgs</file>\n    <file alias=\"slot_2_idle.tgs\">../../animations/dice/slot_2_idle.tgs</file>\n    <file alias=\"slot_back.tgs\">../../animations/dice/slot_back.tgs</file>\n    <file alias=\"slot_pull.tgs\">../../animations/dice/slot_pull.tgs</file>\n    <file alias=\"winners.tgs\">../../animations/dice/winners.tgs</file>\n    <file alias=\"dice_6.tgs\">../../animations/dice/dice_6.tgs</file>\n\n    <file alias=\"star_reaction_appear.tgs\">../../animations/star_reaction/appear.tgs</file>\n    <file alias=\"star_reaction_center.tgs\">../../animations/star_reaction/center.tgs</file>\n    <file alias=\"star_reaction_select.tgs\">../../animations/star_reaction/select.tgs</file>\n    <file alias=\"star_reaction_toast.tgs\">../../animations/star_reaction/toast.tgs</file>\n    <file alias=\"star_reaction_effect1.tgs\">../../animations/star_reaction/effect1.tgs</file>\n    <file alias=\"star_reaction_effect2.tgs\">../../animations/star_reaction/effect2.tgs</file>\n    <file alias=\"star_reaction_effect3.tgs\">../../animations/star_reaction/effect3.tgs</file>\n\n    <file alias=\"photo_editor_pen.tgs\">../../animations/photo_editor/pen.tgs</file>\n    <file alias=\"photo_editor_arrow.tgs\">../../animations/photo_editor/arrow.tgs</file>\n    <file alias=\"photo_editor_marker.tgs\">../../animations/photo_editor/marker.tgs</file>\n    <file alias=\"photo_editor_eraser.tgs\">../../animations/photo_editor/eraser.tgs</file>\n    <file alias=\"photo_editor_blur.tgs\">../../animations/photo_editor/blur.tgs</file>\n\n    <file alias=\"swipe_archive.tgs\">../../animations/swipe_action/archive.tgs</file>\n    <file alias=\"swipe_unarchive.tgs\">../../animations/swipe_action/unarchive.tgs</file>\n    <file alias=\"swipe_delete.tgs\">../../animations/swipe_action/delete.tgs</file>\n    <file alias=\"swipe_disabled.tgs\">../../animations/swipe_action/disabled.tgs</file>\n    <file alias=\"swipe_mute.tgs\">../../animations/swipe_action/mute.tgs</file>\n    <file alias=\"swipe_unmute.tgs\">../../animations/swipe_action/unmute.tgs</file>\n    <file alias=\"swipe_pin.tgs\">../../animations/swipe_action/pin.tgs</file>\n    <file alias=\"swipe_unpin.tgs\">../../animations/swipe_action/unpin.tgs</file>\n    <file alias=\"swipe_read.tgs\">../../animations/swipe_action/read.tgs</file>\n    <file alias=\"swipe_unread.tgs\">../../animations/swipe_action/unread.tgs</file>\n    <file alias=\"craft_progress.tgs\">../../animations/craft_progress.tgs</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/telegram/export.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/export\">\n    <file alias=\"css/style.css\">../../export_html/css/style.css</file>\n    <file alias=\"images/back.png\">../../export_html/images/back.png</file>\n    <file alias=\"images/back@2x.png\">../../export_html/images/back@2x.png</file>\n    <file alias=\"images/media_call.png\">../../export_html/images/media_call.png</file>\n    <file alias=\"images/media_call@2x.png\">../../export_html/images/media_call@2x.png</file>\n    <file alias=\"images/media_contact.png\">../../export_html/images/media_contact.png</file>\n    <file alias=\"images/media_contact@2x.png\">../../export_html/images/media_contact@2x.png</file>\n    <file alias=\"images/media_file.png\">../../export_html/images/media_file.png</file>\n    <file alias=\"images/media_file@2x.png\">../../export_html/images/media_file@2x.png</file>\n    <file alias=\"images/media_game.png\">../../export_html/images/media_game.png</file>\n    <file alias=\"images/media_game@2x.png\">../../export_html/images/media_game@2x.png</file>\n    <file alias=\"images/media_location.png\">../../export_html/images/media_location.png</file>\n    <file alias=\"images/media_location@2x.png\">../../export_html/images/media_location@2x.png</file>\n    <file alias=\"images/media_music.png\">../../export_html/images/media_music.png</file>\n    <file alias=\"images/media_music@2x.png\">../../export_html/images/media_music@2x.png</file>\n    <file alias=\"images/media_photo.png\">../../export_html/images/media_photo.png</file>\n    <file alias=\"images/media_photo@2x.png\">../../export_html/images/media_photo@2x.png</file>\n    <file alias=\"images/media_shop.png\">../../export_html/images/media_shop.png</file>\n    <file alias=\"images/media_shop@2x.png\">../../export_html/images/media_shop@2x.png</file>\n    <file alias=\"images/media_video.png\">../../export_html/images/media_video.png</file>\n    <file alias=\"images/media_video@2x.png\">../../export_html/images/media_video@2x.png</file>\n    <file alias=\"images/media_voice.png\">../../export_html/images/media_voice.png</file>\n    <file alias=\"images/media_voice@2x.png\">../../export_html/images/media_voice@2x.png</file>\n    <file alias=\"images/section_calls.png\">../../export_html/images/section_calls.png</file>\n    <file alias=\"images/section_calls@2x.png\">../../export_html/images/section_calls@2x.png</file>\n    <file alias=\"images/section_chats.png\">../../export_html/images/section_chats.png</file>\n    <file alias=\"images/section_chats@2x.png\">../../export_html/images/section_chats@2x.png</file>\n    <file alias=\"images/section_contacts.png\">../../export_html/images/section_contacts.png</file>\n    <file alias=\"images/section_contacts@2x.png\">../../export_html/images/section_contacts@2x.png</file>\n    <file alias=\"images/section_frequent.png\">../../export_html/images/section_frequent.png</file>\n    <file alias=\"images/section_frequent@2x.png\">../../export_html/images/section_frequent@2x.png</file>\n    <file alias=\"images/section_music.png\">../../export_html/images/section_music.png</file>\n    <file alias=\"images/section_music@2x.png\">../../export_html/images/section_music@2x.png</file>\n    <file alias=\"images/section_other.png\">../../export_html/images/section_other.png</file>\n    <file alias=\"images/section_other@2x.png\">../../export_html/images/section_other@2x.png</file>\n    <file alias=\"images/section_photos.png\">../../export_html/images/section_photos.png</file>\n    <file alias=\"images/section_photos@2x.png\">../../export_html/images/section_photos@2x.png</file>\n    <file alias=\"images/section_sessions.png\">../../export_html/images/section_sessions.png</file>\n    <file alias=\"images/section_sessions@2x.png\">../../export_html/images/section_sessions@2x.png</file>\n    <file alias=\"images/section_stories.png\">../../export_html/images/section_stories.png</file>\n    <file alias=\"images/section_stories@2x.png\">../../export_html/images/section_stories@2x.png</file>\n    <file alias=\"images/section_web.png\">../../export_html/images/section_web.png</file>\n    <file alias=\"images/section_web@2x.png\">../../export_html/images/section_web@2x.png</file>\n    <file alias=\"js/script.js\">../../export_html/js/script.js</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/telegram/iv.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/iv\">\n    <file alias=\"page.css\">../../iv_html/page.css</file>\n    <file alias=\"page.js\">../../iv_html/page.js</file>\n    <file alias=\"highlight.css\">../../iv_html/highlight.9.12.0.css</file>\n    <file alias=\"highlight.js\">../../iv_html/highlight.9.12.0.js</file>\n    <file alias=\"morphdom.js\">../../iv_html/morphdom-umd.min.2.7.2.js</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/telegram/mac_icons.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/gui\">\n    <file alias=\"art/icon_round512@2x.png\">../../art/icon_round512@2x.png</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/telegram/picker.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/picker\">\n    <file alias=\"picker.css\">../../picker_html/picker.css</file>\n    <file alias=\"picker.js\">../../picker_html/picker.js</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/telegram/sounds.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/sounds\">\n    <file alias=\"msg_incoming.mp3\">../../sounds/msg_incoming.mp3</file>\n    <file alias=\"call_busy.mp3\">../../sounds/call_busy.mp3</file>\n    <file alias=\"call_connect.mp3\">../../sounds/call_connect.mp3</file>\n    <file alias=\"call_end.mp3\">../../sounds/call_end.mp3</file>\n    <file alias=\"call_incoming.mp3\">../../sounds/call_incoming.mp3</file>\n    <file alias=\"call_outgoing.mp3\">../../sounds/call_outgoing.mp3</file>\n    <file alias=\"group_call_start.mp3\">../../sounds/group_call_start.mp3</file>\n    <file alias=\"group_call_connect.mp3\">../../sounds/group_call_connect.mp3</file>\n    <file alias=\"group_call_end.mp3\">../../sounds/group_call_end.mp3</file>\n    <file alias=\"group_call_allowed.mp3\">../../sounds/group_call_allowed.mp3</file>\n    <file alias=\"group_call_recording_start.mp3\">../../sounds/group_call_recording_start.mp3</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/telegram/telegram.qrc",
    "content": "<RCC>\n  <qresource prefix=\"/gui\">\n    <file alias=\"art/background.tgv\">../../art/background.tgv</file>\n    <file alias=\"art/bg_thumbnail.png\">../../art/bg_thumbnail.png</file>\n    <file alias=\"art/bg_initial.jpg\">../../art/bg_initial.jpg</file>\n    <file alias=\"art/business_logo.png\">../../art/business_logo.png</file>\n    <file alias=\"art/affiliate_logo.png\">../../art/affiliate_logo.png</file>\n    <file alias=\"art/logo_256.png\">../../art/logo_256.png</file>\n    <file alias=\"art/logo_256_square.png\">../../art/logo_256_square.png</file>\n    <file alias=\"art/logo_256_no_margin.png\">../../art/logo_256_no_margin.png</file>\n    <file alias=\"art/forkgram/logo_256.png\">../../art/forkgram/logo_256.png</file>\n    <file alias=\"art/forkgram/logo_256_no_margin.png\">../../art/forkgram/logo_256_no_margin.png</file>\n    <file alias=\"art/themeimage.jpg\">../../art/themeimage.jpg</file>\n    <file alias=\"art/round_placeholder.jpg\">../../art/round_placeholder.jpg</file>\n    <file alias=\"art/cocoon.webp\">../../art/cocoon.webp</file>\n    <file alias=\"day-blue.tdesktop-theme\">../../day-blue.tdesktop-theme</file>\n    <file alias=\"night.tdesktop-theme\">../../night.tdesktop-theme</file>\n    <file alias=\"night-green.tdesktop-theme\">../../night-green.tdesktop-theme</file>\n    <file alias=\"day-custom-base.tdesktop-theme\">../../day-custom-base.tdesktop-theme</file>\n    <file alias=\"night-custom-base.tdesktop-theme\">../../night-custom-base.tdesktop-theme</file>\n    <file alias=\"icons/calls/hands.lottie\">../../icons/calls/hands.lottie</file>\n    <file alias=\"icons/calls/voice.lottie\">../../icons/calls/voice.lottie</file>\n    <file alias=\"recording/info_audio.svg\">../../art/recording/recording_info_audio.svg</file>\n    <file alias=\"recording/info_video_landscape.svg\">../../art/recording/recording_info_video_landscape.svg</file>\n    <file alias=\"recording/info_video_portrait.svg\">../../art/recording/recording_info_video_portrait.svg</file>\n    <file alias=\"ttl/video_message_icon.svg\">../../art/ttl/video_message_icon.svg</file>\n    <file alias=\"icons/settings/dino.svg\">../../icons/settings/dino.svg</file>\n    <file alias=\"icons/settings/star.svg\">../../icons/settings/star.svg</file>\n    <file alias=\"icons/settings/starmini.svg\">../../icons/settings/starmini.svg</file>\n    <file alias=\"icons/tray/monochrome.svg\">../../icons/tray_monochrome.svg</file>\n    <file alias=\"icons/tray/monochrome_attention.svg\">../../icons/tray_monochrome_attention.svg</file>\n    <file alias=\"icons/tray/monochrome_mute.svg\">../../icons/tray_monochrome_mute.svg</file>\n    <file alias=\"topic_icons/blue.svg\">../../art/topic_icons/blue.svg</file>\n    <file alias=\"topic_icons/yellow.svg\">../../art/topic_icons/yellow.svg</file>\n    <file alias=\"topic_icons/violet.svg\">../../art/topic_icons/violet.svg</file>\n    <file alias=\"topic_icons/green.svg\">../../art/topic_icons/green.svg</file>\n    <file alias=\"topic_icons/rose.svg\">../../art/topic_icons/rose.svg</file>\n    <file alias=\"topic_icons/red.svg\">../../art/topic_icons/red.svg</file>\n    <file alias=\"topic_icons/gray.svg\">../../art/topic_icons/gray.svg</file>\n    <file alias=\"topic_icons/general.svg\">../../art/topic_icons/general.svg</file>\n    <file alias=\"links_subscription.svg\">../../icons/info/edit/links_subscription.svg</file>\n    <file alias=\"plane_white.svg\">../../icons/plane_white.svg</file>\n    <file alias=\"dice/dice1.svg\">../../art/dice/dice1.svg</file>\n    <file alias=\"dice/dice2.svg\">../../art/dice/dice2.svg</file>\n    <file alias=\"dice/dice3.svg\">../../art/dice/dice3.svg</file>\n    <file alias=\"dice/dice4.svg\">../../art/dice/dice4.svg</file>\n    <file alias=\"dice/dice5.svg\">../../art/dice/dice5.svg</file>\n    <file alias=\"dice/dice6.svg\">../../art/dice/dice6.svg</file>\n  </qresource>\n  <qresource prefix=\"/icons\">\n    <file alias=\"calls/hands.lottie\">../../icons/calls/hands.lottie</file>\n    <file alias=\"calls/voice.lottie\">../../icons/calls/voice.lottie</file>\n    <file alias=\"settings/devices/device_desktop_mac.lottie\">../../icons/settings/devices/device_desktop_mac.lottie</file>\n    <file alias=\"settings/devices/device_desktop_win.lottie\">../../icons/settings/devices/device_desktop_win.lottie</file>\n    <file alias=\"settings/devices/device_linux.lottie\">../../icons/settings/devices/device_linux.lottie</file>\n    <file alias=\"settings/devices/device_linux_ubuntu.lottie\">../../icons/settings/devices/device_linux_ubuntu.lottie</file>\n    <file alias=\"settings/devices/device_phone_android.lottie\">../../icons/settings/devices/device_phone_android.lottie</file>\n    <file alias=\"settings/devices/device_phone_ios.lottie\">../../icons/settings/devices/device_phone_ios.lottie</file>\n    <file alias=\"settings/devices/device_tablet_ios.lottie\">../../icons/settings/devices/device_tablet_ios.lottie</file>\n    <file alias=\"settings/devices/device_web_chrome.lottie\">../../icons/settings/devices/device_web_chrome.lottie</file>\n    <file alias=\"settings/devices/device_web_edge.lottie\">../../icons/settings/devices/device_web_edge.lottie</file>\n    <file alias=\"settings/devices/device_web_firefox.lottie\">../../icons/settings/devices/device_web_firefox.lottie</file>\n    <file alias=\"settings/devices/device_web_safari.lottie\">../../icons/settings/devices/device_web_safari.lottie</file>\n  </qresource>\n  <qresource prefix=\"/misc\">\n    <file alias=\"default_shortcuts-custom.json\">../../default_shortcuts-custom.json</file>\n    <file alias=\"org.telegram.desktop.desktop\">../../../../lib/xdg/org.telegram.desktop.desktop</file>\n  </qresource>\n</RCC>\n"
  },
  {
    "path": "Telegram/Resources/qrc/telegram.qrc",
    "content": ""
  },
  {
    "path": "Telegram/Resources/uwp/AppX/AppxManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Package\n   xmlns=\"http://schemas.microsoft.com/appx/manifest/foundation/windows10\"\n   xmlns:uap=\"http://schemas.microsoft.com/appx/manifest/uap/windows10\"\n   xmlns:uap2=\"http://schemas.microsoft.com/appx/manifest/uap/windows10/2\"\n   xmlns:uap3=\"http://schemas.microsoft.com/appx/manifest/uap/windows10/3\"\n   xmlns:desktop=\"http://schemas.microsoft.com/appx/manifest/desktop/windows10\"\n   xmlns:rescap=\"http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities\"\n   IgnorableNamespaces=\"uap uap2 uap3 desktop rescap\">\n  <Identity Name=\"TelegramMessengerLLP.TelegramDesktop\"\n    ProcessorArchitecture=\"ARCHITECTURE\"\n    Publisher=\"CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A\"\n    Version=\"6.7.6.0\" />\n  <Properties>\n    <DisplayName>Telegram Desktop</DisplayName>\n    <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>\n    <Description>Telegram Desktop official messenger</Description>\n    <Logo>Assets\\logo\\logo.png</Logo>\n  </Properties>\n  <Resources>\n    <Resource Language=\"en-us\" />\n  </Resources>\n  <Dependencies>\n    <TargetDeviceFamily Name=\"Windows.Desktop\" MinVersion=\"10.0.14316.0\" MaxVersionTested=\"10.0.14316.0\" />\n  </Dependencies>\n  <Capabilities>\n    <rescap:Capability Name=\"runFullTrust\"/>\n  </Capabilities>\n  <Applications>\n    <Application Id=\"Telegram.TelegramDesktop.Store\" Executable=\"Telegram.exe\" EntryPoint=\"Windows.FullTrustApplication\">\n      <uap:VisualElements\n       BackgroundColor=\"#1e94d0\"\n       DisplayName=\"Telegram Desktop\"\n       Square150x150Logo=\"Assets\\logo150\\logo150.png\"\n       Square44x44Logo=\"Assets\\logo44\\logo44.png\"\n       Description=\"Telegram Desktop official messenger\" />\n      <Extensions>\n        <uap3:Extension Category=\"windows.protocol\">\n          <uap3:Protocol Name=\"tg\" Parameters=\"-- &quot;%1&quot;\" />\n        </uap3:Extension>\n        <uap3:Extension Category=\"windows.protocol\">\n          <uap3:Protocol Name=\"tonsite\" Parameters=\"-- &quot;%1&quot;\" />\n        </uap3:Extension>\n        <desktop:Extension\n          Category=\"windows.startupTask\"\n          Executable=\"StartupTask.exe\"\n          EntryPoint=\"Windows.FullTrustApplication\">\n          <desktop:StartupTask\n            TaskId=\"TelegramStartupTask\"\n            Enabled=\"false\"\n            DisplayName=\"Telegram Desktop\" />\n        </desktop:Extension>\n      </Extensions>\n    </Application>\n  </Applications>\n</Package>\n"
  },
  {
    "path": "Telegram/Resources/uwp/priconfig.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<resources targetOsVersion=\"10.0.0\" majorVersion=\"1\">\n\t<index root=\"\\\" startIndexAt=\"\\\">\n\t\t<default>\n\t\t\t<qualifier name=\"Language\" value=\"en-US\"/>\n\t\t\t<qualifier name=\"Contrast\" value=\"standard\"/>\n\t\t\t<qualifier name=\"Scale\" value=\"100\"/>\n\t\t\t<qualifier name=\"HomeRegion\" value=\"001\"/>\n\t\t\t<qualifier name=\"TargetSize\" value=\"256\"/>\n\t\t\t<qualifier name=\"LayoutDirection\" value=\"LTR\"/>\n\t\t\t<qualifier name=\"Theme\" value=\"dark\"/>\n\t\t\t<qualifier name=\"AlternateForm\" value=\"\"/>\n\t\t\t<qualifier name=\"DXFeatureLevel\" value=\"DX9\"/>\n\t\t\t<qualifier name=\"Configuration\" value=\"\"/>\n\t\t\t<qualifier name=\"DeviceFamily\" value=\"Universal\"/>\n\t\t\t<qualifier name=\"Custom\" value=\"\"/>\n\t\t</default>\n\t\t<indexer-config type=\"folder\" foldernameAsQualifier=\"true\" filenameAsQualifier=\"true\" qualifierDelimiter=\".\"/>\n\t\t<indexer-config type=\"resw\" convertDotsToSlashes=\"true\" initialPath=\"\"/>\n\t\t<indexer-config type=\"resjson\" initialPath=\"\"/>\n\t\t<indexer-config type=\"PRI\"/>\n\t</index>\n</resources>"
  },
  {
    "path": "Telegram/Resources/winrc/Telegram.manifest",
    "content": "﻿<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- Windows 10 -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n      <!-- Windows 8.1 -->\n      <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\"/>\n      <!-- Windows Vista -->\n      <supportedOS Id=\"{e2011457-1546-43c5-a5fe-008deee3d3f0}\"/>\n      <!-- Windows 7 -->\n      <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\"/>\n      <!-- Windows 8 -->\n      <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\"/>\n    </application>\n  </compatibility>\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">true/pm</dpiAware>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitor</dpiAwareness>\n      <activeCodePage xmlns=\"http://schemas.microsoft.com/SMI/2019/WindowsSettings\">UTF-8</activeCodePage>\n    </windowsSettings>\n  </application>\n</assembly>"
  },
  {
    "path": "Telegram/Resources/winrc/Telegram.rc",
    "content": "// Microsoft Visual C++ generated resource script.\n//\n\n#define APSTUDIO_READONLY_SYMBOLS\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 2 resource.\n//\n#if defined(__MINGW64__) || defined(__MINGW32__)\n\t// MinGW-w64, MinGW\n\t#if defined(__has_include) && __has_include(<winres.h>)\n\t\t#include <winres.h>\n\t#else\n\t\t#include <afxres.h>\n\t\t#include <winresrc.h>\n\t#endif\n#else\n\t// MSVC, Windows SDK\n\t#include <winres.h>\n#endif\n\n/////////////////////////////////////////////////////////////////////////////\n#undef APSTUDIO_READONLY_SYMBOLS\n\n/////////////////////////////////////////////////////////////////////////////\n// English (United States) resources\n\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\nLANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US\n#pragma code_page(1252)\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Icon\n//\n\n// Icon with lowest ID value placed first to ensure application icon\n// remains consistent on all systems.\nIDI_ICON1               ICON                    \"..\\\\art\\\\icon256.ico\"\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Version\n//\n\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION 6,7,6,0\n PRODUCTVERSION 6,7,6,0\n FILEFLAGSMASK 0x3fL\n#ifdef _DEBUG\n FILEFLAGS 0x1L\n#else\n FILEFLAGS 0x0L\n#endif\n FILEOS 0x40004L\n FILETYPE 0x0L\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904b0\"\n        BEGIN\n            VALUE \"CompanyName\", \"\"\n            VALUE \"FileDescription\", \"Telegram Desktop\"\n            VALUE \"FileVersion\", \"6.7.6.0\"\n            VALUE \"LegalCopyright\", \"Copyright (C) 2014-2026\"\n            VALUE \"ProductName\", \"Telegram Desktop\"\n            VALUE \"ProductVersion\", \"6.7.6.0\"\n        END\n    END\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x409, 1200\n    END\nEND\n\n#endif    // English (United States) resources\n/////////////////////////////////////////////////////////////////////////////\n"
  },
  {
    "path": "Telegram/Resources/winrc/Updater.rc",
    "content": "// Microsoft Visual C++ generated resource script.\n//\n\n#define APSTUDIO_READONLY_SYMBOLS\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 2 resource.\n//\n#if defined(__MINGW64__) || defined(__MINGW32__)\n\t// MinGW-w64, MinGW\n\t#if defined(__has_include) && __has_include(<winres.h>)\n\t\t#include <winres.h>\n\t#else\n\t\t#include <afxres.h>\n\t\t#include <winresrc.h>\n\t#endif\n#else\n\t// MSVC, Windows SDK\n\t#include <winres.h>\n#endif\n\n/////////////////////////////////////////////////////////////////////////////\n#undef APSTUDIO_READONLY_SYMBOLS\n\n/////////////////////////////////////////////////////////////////////////////\n// English (United States) resources\n\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\nLANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US\n#pragma code_page(1252)\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Version\n//\n\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION 6,7,6,0\n PRODUCTVERSION 6,7,6,0\n FILEFLAGSMASK 0x3fL\n#ifdef _DEBUG\n FILEFLAGS 0x1L\n#else\n FILEFLAGS 0x0L\n#endif\n FILEOS 0x40004L\n FILETYPE 0x0L\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904b0\"\n        BEGIN\n            VALUE \"CompanyName\", \"\"\n            VALUE \"FileDescription\", \"Telegram Desktop Updater\"\n            VALUE \"FileVersion\", \"6.7.6.0\"\n            VALUE \"LegalCopyright\", \"Copyright (C) 2014-2026\"\n            VALUE \"ProductName\", \"Telegram Desktop\"\n            VALUE \"ProductVersion\", \"6.7.6.0\"\n        END\n    END\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x409, 1200\n    END\nEND\n\n#endif    // English (United States) resources\n/////////////////////////////////////////////////////////////////////////////\n"
  },
  {
    "path": "Telegram/SourceFiles/_other/packer.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"packer.h\"\n\nbool BetaChannel = false;\nquint64 AlphaVersion = 0;\nbool OnlyAlphaKey = false;\n\nconst char *PublicKey = \"\\\n-----BEGIN RSA PUBLIC KEY-----\\n\\\nMIGJAoGBAMA4ViQrjkPZ9xj0lrer3r23JvxOnrtE8nI69XLGSr+sRERz9YnUptnU\\n\\\nBZpkIfKaRcl6XzNJiN28cVwO1Ui5JSa814UAiDHzWUqCaXUiUEQ6NmNTneiGx2sQ\\n\\\n+9PKKlb8mmr3BB9A45ZNwLT6G9AK3+qkZLHojeSA+m84/a6GP4svAgMBAAE=\\n\\\n-----END RSA PUBLIC KEY-----\\\n\";\n\nconst char *PublicBetaKey = \"\\\n-----BEGIN RSA PUBLIC KEY-----\\n\\\nMIGJAoGBALWu9GGs0HED7KG7BM73CFZ6o0xufKBRQsdnq3lwA8nFQEvmdu+g/I1j\\n\\\n0LQ+0IQO7GW4jAgzF/4+soPDb6uHQeNFrlVx1JS9DZGhhjZ5rf65yg11nTCIHZCG\\n\\\nw/CVnbwQOw0g5GBwwFV3r0uTTvy44xx8XXxk+Qknu4eBCsmrAFNnAgMBAAE=\\n\\\n-----END RSA PUBLIC KEY-----\\\n\";\n\nextern const char *PrivateKey;\nextern const char *PrivateBetaKey;\n#include \"../../../../DesktopPrivate/packer_private.h\" // RSA PRIVATE KEYS for update signing\n#include \"../../../../DesktopPrivate/alpha_private.h\" // private key for alpha version file generation\n\nQString countAlphaVersionSignature(quint64 version);\n\n// sha1 hash\ntypedef unsigned char uchar;\ntypedef unsigned int uint32;\ntypedef signed int int32;\n\nnamespace{\n\nstruct BIODeleter {\n\tvoid operator()(BIO *value) {\n\t\tBIO_free(value);\n\t}\n};\n\ninline auto makeBIO(const void *buf, int len) {\n\treturn std::unique_ptr<BIO, BIODeleter>{\n\t\tBIO_new_mem_buf(buf, len),\n\t};\n}\n\ninline uint32 sha1Shift(uint32 v, uint32 shift) {\n\treturn ((v << shift) | (v >> (32 - shift)));\n}\n\nvoid sha1PartHash(uint32 *sha, uint32 *temp) {\n\tuint32 a = sha[0], b = sha[1], c = sha[2], d = sha[3], e = sha[4], round = 0;\n\n#define _shiftswap(f, v) { \\\n\t\tuint32 t = sha1Shift(a, 5) + (f) + e + v + temp[round]; \\\n\t\te = d; \\\n\t\td = c; \\\n\t\tc = sha1Shift(b, 30); \\\n\t\tb = a; \\\n\t\ta = t; \\\n\t\t++round; \\\n\t}\n\n#define _shiftshiftswap(f, v) { \\\n\t\ttemp[round] = sha1Shift((temp[round - 3] ^ temp[round - 8] ^ temp[round - 14] ^ temp[round - 16]), 1); \\\n\t\t_shiftswap(f, v) \\\n\t}\n\n\twhile (round < 16) _shiftswap((b & c) | (~b & d), 0x5a827999)\n\twhile (round < 20) _shiftshiftswap((b & c) | (~b & d), 0x5a827999)\n\twhile (round < 40) _shiftshiftswap(b ^ c ^ d, 0x6ed9eba1)\n\twhile (round < 60) _shiftshiftswap((b & c) | (b & d) | (c & d), 0x8f1bbcdc)\n\twhile (round < 80) _shiftshiftswap(b ^ c ^ d, 0xca62c1d6)\n\n#undef _shiftshiftswap\n#undef _shiftswap\n\n\tsha[0] += a;\n\tsha[1] += b;\n\tsha[2] += c;\n\tsha[3] += d;\n\tsha[4] += e;\n}\n\n} // namespace\n\nint32 *hashSha1(const void *data, uint32 len, void *dest) {\n\tconst uchar *buf = (const uchar *)data;\n\n\tuint32 temp[80], block = 0, end;\n\tuint32 sha[5] = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0};\n\tfor (end = block + 64; block + 64 <= len; end = block + 64) {\n\t\tfor (uint32 i = 0; block < end; block += 4) {\n\t\t\ttemp[i++] = (uint32) buf[block + 3]\n\t\t\t\t\t| (((uint32) buf[block + 2]) << 8)\n\t\t\t\t\t| (((uint32) buf[block + 1]) << 16)\n\t\t\t\t\t| (((uint32) buf[block]) << 24);\n\t\t}\n\t\tsha1PartHash(sha, temp);\n\t}\n\n\tend = len - block;\n\tmemset(temp, 0, sizeof(uint32) * 16);\n\tuint32 last = 0;\n\tfor (; last < end; ++last) {\n\t\ttemp[last >> 2] |= (uint32)buf[last + block] << ((3 - (last & 0x03)) << 3);\n\t}\n\ttemp[last >> 2] |= 0x80 << ((3 - (last & 3)) << 3);\n\tif (end >= 56) {\n\t\tsha1PartHash(sha, temp);\n\t\tmemset(temp, 0, sizeof(uint32) * 16);\n\t}\n\ttemp[15] = len << 3;\n\tsha1PartHash(sha, temp);\n\n\tuchar *sha1To = (uchar*)dest;\n\n\tfor (int32 i = 19; i >= 0; --i) {\n\t\tsha1To[i] = (sha[i >> 2] >> (((3 - i) & 0x03) << 3)) & 0xFF;\n\t}\n\n\treturn (int32*)sha1To;\n}\n\nQString AlphaSignature;\n\nint writeAlphaKey() {\n\tif (!AlphaVersion) {\n\t\treturn 0;\n\t}\n\tQString keyName(QString(\"talpha_%1_key\").arg(AlphaVersion));\n\tQFile key(keyName);\n\tif (!key.open(QIODevice::WriteOnly)) {\n\t\tcout << \"Can't open '\" << keyName.toUtf8().constData() << \"' for write..\\n\";\n\t\treturn -1;\n\t}\n\tkey.write(AlphaSignature.toUtf8());\n\tkey.close();\n\treturn 0;\n}\n\nint main(int argc, char *argv[])\n{\n\tQString workDir;\n\n\tQString remove;\n\tint version = 0;\n\t[[maybe_unused]] bool targetwin64 = false;\n\t[[maybe_unused]] bool targetwinarm = false;\n\t[[maybe_unused]] bool targetarmac = false;\n\tQFileInfoList files;\n\tfor (int i = 0; i < argc; ++i) {\n\t\tif (string(\"-path\") == argv[i] && i + 1 < argc) {\n\t\t\tQString path = workDir + QString(argv[i + 1]);\n\t\t\tQFileInfo info(path);\n\t\t\tfiles.push_back(info);\n\t\t\tif (remove.isEmpty()) remove = info.canonicalPath() + \"/\";\n\t\t} else if (string(\"-target\") == argv[i] && i + 1 < argc) {\n\t\t\ttargetwin64 = (string(\"win64\") == argv[i + 1]);\n\t\t\ttargetwinarm = (string(\"winarm\") == argv[i + 1]);\n\t\t} else if (string(\"-arch\") == argv[i] && i + 1 < argc) {\n\t\t\ttargetarmac = (string(\"arm64\") == argv[i + 1]);\n\t\t\tif (!targetarmac && string(\"x86_64\") != argv[i + 1]) {\n\t\t\t\tcout << \"Bad -arch param value passed: \" << argv[i + 1] << \"\\n\";\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t} else if (string(\"-version\") == argv[i] && i + 1 < argc) {\n\t\t\tversion = QString(argv[i + 1]).toInt();\n\t\t} else if (string(\"-beta\") == argv[i]) {\n\t\t\tBetaChannel = true;\n\t\t} else if (string(\"-alphakey\") == argv[i]) {\n\t\t\tOnlyAlphaKey = true;\n\t\t} else if (string(\"-alpha\") == argv[i] && i + 1 < argc) {\n\t\t\tAlphaVersion = QString(argv[i + 1]).toULongLong();\n\t\t\tif (AlphaVersion > version * 1000ULL && AlphaVersion < (version + 1) * 1000ULL) {\n\t\t\t\tBetaChannel = false;\n\t\t\t\tAlphaSignature = countAlphaVersionSignature(AlphaVersion);\n\t\t\t\tif (AlphaSignature.isEmpty()) {\n\t\t\t\t\treturn -1;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcout << \"Bad -alpha param value passed, should be for the same version: \" << version << \", alpha: \" << AlphaVersion << \"\\n\";\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t}\n\tif (OnlyAlphaKey) {\n\t\treturn writeAlphaKey();\n\t}\n\n\tif (files.isEmpty() || remove.isEmpty() || version <= 1016 || version > 999999999) {\n#ifdef Q_OS_WIN\n\t\tcout << \"Usage: Packer.exe -path {file} -version {version} OR Packer.exe -path {dir} -version {version}\\n\";\n#elif defined Q_OS_MAC\n\t\tcout << \"Usage: Packer.app -path {file} -version {version} OR Packer.app -path {dir} -version {version}\\n\";\n#else\n\t\tcout << \"Usage: Packer -path {file} -version {version} OR Packer -path {dir} -version {version}\\n\";\n#endif\n\t\treturn -1;\n\t}\n\n\tbool hasDirs = true;\n\twhile (hasDirs) {\n\t\thasDirs = false;\n\t\tfor (QFileInfoList::iterator i = files.begin(); i != files.end(); ++i) {\n\t\t\tQFileInfo info(*i);\n\t\t\tQString fullPath = info.canonicalFilePath();\n\t\t\tif (info.isDir()) {\n\t\t\t\thasDirs = true;\n\t\t\t\tfiles.erase(i);\n\t\t\t\tQDir d = QDir(info.absoluteFilePath());\n\t\t\t\tQString fullDir = d.canonicalPath();\n\t\t\t\tQStringList entries = d.entryList(QDir::Files | QDir::Dirs | QDir::NoSymLinks | QDir::NoDotAndDotDot);\n\t\t\t\tfiles.append(d.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoSymLinks | QDir::NoDotAndDotDot));\n\t\t\t\tbreak;\n\t\t\t} else if (!info.isReadable()) {\n\t\t\t\tcout << \"Can't read: \" << info.absoluteFilePath().toUtf8().constData() << \"\\n\";\n\t\t\t\treturn -1;\n\t\t\t} else if (info.isHidden()) {\n\t\t\t\thasDirs = true;\n\t\t\t\tfiles.erase(i);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\tfor (QFileInfoList::iterator i = files.begin(); i != files.end(); ++i) {\n\t\tQFileInfo info(*i);\n\t\tif (!info.canonicalFilePath().startsWith(remove)) {\n\t\t\tcout << \"Can't find '\" << remove.toUtf8().constData() << \"' in file '\" << info.canonicalFilePath().toUtf8().constData() << \"' :(\\n\";\n\t\t\treturn -1;\n\t\t}\n\t}\n\n\tQByteArray result;\n\t{\n\t\tQBuffer buffer(&result);\n\t\tbuffer.open(QIODevice::WriteOnly);\n\t\tQDataStream stream(&buffer);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\n\t\tif (AlphaVersion) {\n\t\t\tstream << quint32(0x7FFFFFFF);\n\t\t\tstream << quint64(AlphaVersion);\n\t\t} else {\n\t\t\tstream << quint32(version);\n\t\t}\n\n\t\tstream << quint32(files.size());\n\t\tcout << \"Found \" << files.size() << \" file\" << (files.size() == 1 ? \"\" : \"s\") << \"..\\n\";\n\t\tfor (QFileInfoList::iterator i = files.begin(); i != files.end(); ++i) {\n\t\t\tQFileInfo info(*i);\n\t\t\tQString fullName = info.canonicalFilePath();\n\t\t\tQString name = fullName.mid(remove.length());\n\t\t\tcout << name.toUtf8().constData() << \" (\" << info.size() << \")\\n\";\n\n\t\t\tQFile f(fullName);\n\t\t\tif (!f.open(QIODevice::ReadOnly)) {\n\t\t\t\tcout << \"Can't open '\" << fullName.toUtf8().constData() << \"' for read..\\n\";\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tQByteArray inner = f.readAll();\n\t\t\tstream << name << quint32(inner.size()) << inner;\n#ifndef Q_OS_WIN\n\t\t\tstream << (QFileInfo(fullName).isExecutable() ? true : false);\n#endif\n\t\t}\n\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\tcout << \"Stream status is bad: \" << stream.status() << \"\\n\";\n\t\t\treturn -1;\n\t\t}\n\t}\n\n\tint32 resultSize = result.size();\n\tcout << \"Compression start, size: \" << resultSize << \"\\n\";\n\n\tQByteArray compressed, resultCheck;\n#if defined Q_OS_WIN && !defined PACKER_USE_PACKAGED // use Lzma SDK for win\n\tconst int32 hSigLen = 128, hShaLen = 20, hPropsLen = LZMA_PROPS_SIZE, hOriginalSizeLen = sizeof(int32), hSize = hSigLen + hShaLen + hPropsLen + hOriginalSizeLen; // header\n\n\tcompressed.resize(hSize + resultSize + 1024 * 1024); // rsa signature + sha1 + lzma props + max compressed size\n\n\tsize_t compressedLen = compressed.size() - hSize;\n\tsize_t outPropsSize = LZMA_PROPS_SIZE;\n\tuchar *_dest = (uchar*)(compressed.data() + hSize);\n\tsize_t *_destLen = &compressedLen;\n\tconst uchar *_src = (const uchar*)(result.constData());\n\tsize_t _srcLen = result.size();\n\tuchar *_outProps = (uchar*)(compressed.data() + hSigLen + hShaLen);\n\tint res = LzmaCompress(_dest, _destLen, _src, _srcLen, _outProps, &outPropsSize, 9, 64 * 1024 * 1024, 4, 0, 2, 273, 2);\n\tif (res != SZ_OK) {\n\t\tcout << \"Error in compression: \" << res << \"\\n\";\n\t\treturn -1;\n\t}\n\tcompressed.resize(int(hSize + compressedLen));\n\tmemcpy(compressed.data() + hSigLen + hShaLen + hPropsLen, &resultSize, hOriginalSizeLen);\n\n\tcout << \"Compressed to size: \" << compressedLen << \"\\n\";\n\n\tcout << \"Checking uncompressed..\\n\";\n\n\tint32 resultCheckLen;\n\tmemcpy(&resultCheckLen, compressed.constData() + hSigLen + hShaLen + hPropsLen, hOriginalSizeLen);\n\tif (resultCheckLen <= 0 || resultCheckLen > 1024 * 1024 * 1024) {\n\t\tcout << \"Bad result len: \" << resultCheckLen << \"\\n\";\n\t\treturn -1;\n\t}\n\tresultCheck.resize(resultCheckLen);\n\n\tsize_t resultLen = resultCheck.size();\n\tSizeT srcLen = compressedLen;\n\tint uncompressRes = LzmaUncompress((uchar*)resultCheck.data(), &resultLen, (const uchar*)(compressed.constData() + hSize), &srcLen, (const uchar*)(compressed.constData() + hSigLen + hShaLen), LZMA_PROPS_SIZE);\n\tif (uncompressRes != SZ_OK) {\n\t\tcout << \"Uncompress failed: \" << uncompressRes << \"\\n\";\n\t\treturn -1;\n\t}\n\tif (resultLen != size_t(result.size())) {\n\t\tcout << \"Uncompress bad size: \" << resultLen << \", was: \" << result.size() << \"\\n\";\n\t\treturn -1;\n\t}\n#else // use liblzma for others\n\tconst int32 hSigLen = 128, hShaLen = 20, hPropsLen = 0, hOriginalSizeLen = sizeof(int32), hSize = hSigLen + hShaLen + hOriginalSizeLen; // header\n\n\tcompressed.resize(hSize + resultSize + 1024 * 1024); // rsa signature + sha1 + lzma props + max compressed size\n\n\tsize_t compressedLen = compressed.size() - hSize;\n\n\tlzma_stream stream = LZMA_STREAM_INIT;\n\n\tint preset = 9 | LZMA_PRESET_EXTREME;\n\tlzma_ret ret = lzma_easy_encoder(&stream, preset, LZMA_CHECK_CRC64);\n\tif (ret != LZMA_OK) {\n\t\tconst char *msg;\n\t\tswitch (ret) {\n\t\t\tcase LZMA_MEM_ERROR: msg = \"Memory allocation failed\"; break;\n\t\t\tcase LZMA_OPTIONS_ERROR: msg = \"Specified preset is not supported\"; break;\n\t\t\tcase LZMA_UNSUPPORTED_CHECK: msg = \"Specified integrity check is not supported\"; break;\n\t\t\tdefault: msg = \"Unknown error, possibly a bug\"; break;\n\t\t}\n\t\tcout << \"Error initializing the encoder: \" << msg << \" (error code \" << ret << \")\\n\";\n\t\treturn -1;\n\t}\n\n\tstream.avail_in = resultSize;\n\tstream.next_in = (uint8_t*)result.constData();\n\tstream.avail_out = compressedLen;\n\tstream.next_out = (uint8_t*)(compressed.data() + hSize);\n\n\tlzma_ret res = lzma_code(&stream, LZMA_FINISH);\n\tcompressedLen -= stream.avail_out;\n\tlzma_end(&stream);\n\tif (res != LZMA_OK && res != LZMA_STREAM_END) {\n\t\tconst char *msg;\n\t\tswitch (res) {\n\t\t\tcase LZMA_MEM_ERROR: msg = \"Memory allocation failed\"; break;\n\t\t\tcase LZMA_DATA_ERROR: msg = \"File size limits exceeded\"; break;\n\t\t\tdefault: msg = \"Unknown error, possibly a bug\"; break;\n\t\t}\n\t\tcout << \"Error in compression: \" << msg << \" (error code \" << res << \")\\n\";\n\t\treturn -1;\n\t}\n\n\tcompressed.resize(int(hSize + compressedLen));\n\tmemcpy(compressed.data() + hSigLen + hShaLen, &resultSize, hOriginalSizeLen);\n\n\tcout << \"Compressed to size: \" << compressedLen << \"\\n\";\n\n\tcout << \"Checking uncompressed..\\n\";\n\n\tint32 resultCheckLen;\n\tmemcpy(&resultCheckLen, compressed.constData() + hSigLen + hShaLen, hOriginalSizeLen);\n\tif (resultCheckLen <= 0 || resultCheckLen > 1024 * 1024 * 1024) {\n\t\tcout << \"Bad result len: \" << resultCheckLen << \"\\n\";\n\t\treturn -1;\n\t}\n\tresultCheck.resize(resultCheckLen);\n\n\tsize_t resultLen = resultCheck.size();\n\n\tstream = LZMA_STREAM_INIT;\n\n\tret = lzma_stream_decoder(&stream, UINT64_MAX, LZMA_CONCATENATED);\n\tif (ret != LZMA_OK) {\n\t\tconst char *msg;\n\t\tswitch (ret) {\n\t\t\tcase LZMA_MEM_ERROR: msg = \"Memory allocation failed\"; break;\n\t\t\tcase LZMA_OPTIONS_ERROR: msg = \"Specified preset is not supported\"; break;\n\t\t\tcase LZMA_UNSUPPORTED_CHECK: msg = \"Specified integrity check is not supported\"; break;\n\t\t\tdefault: msg = \"Unknown error, possibly a bug\"; break;\n\t\t}\n\t\tcout << \"Error initializing the decoder: \" << msg << \" (error code \" << ret << \")\\n\";\n\t\treturn -1;\n\t}\n\n\tstream.avail_in = compressedLen;\n\tstream.next_in = (uint8_t*)(compressed.constData() + hSize);\n\tstream.avail_out = resultLen;\n\tstream.next_out = (uint8_t*)resultCheck.data();\n\n\tres = lzma_code(&stream, LZMA_FINISH);\n\tif (stream.avail_in) {\n\t\tcout << \"Error in decompression, \" << stream.avail_in << \" bytes left in _in of \" << compressedLen << \" whole.\\n\";\n\t\treturn -1;\n\t} else if (stream.avail_out) {\n\t\tcout << \"Error in decompression, \" << stream.avail_out << \" bytes free left in _out of \" << resultLen << \" whole.\\n\";\n\t\treturn -1;\n\t}\n\tlzma_end(&stream);\n\tif (res != LZMA_OK && res != LZMA_STREAM_END) {\n\t\tconst char *msg;\n\t\tswitch (res) {\n\t\t\tcase LZMA_MEM_ERROR: msg = \"Memory allocation failed\"; break;\n\t\t\tcase LZMA_FORMAT_ERROR: msg = \"The input data is not in the .xz format\"; break;\n\t\t\tcase LZMA_OPTIONS_ERROR: msg = \"Unsupported compression options\"; break;\n\t\t\tcase LZMA_DATA_ERROR: msg = \"Compressed file is corrupt\"; break;\n\t\t\tcase LZMA_BUF_ERROR: msg = \"Compressed data is truncated or otherwise corrupt\"; break;\n\t\t\tdefault: msg = \"Unknown error, possibly a bug\"; break;\n\t\t}\n\t\tcout << \"Error in decompression: \" << msg << \" (error code \" << res << \")\\n\";\n\t\treturn -1;\n\t}\n#endif\n\tif (memcmp(result.constData(), resultCheck.constData(), resultLen)) {\n\t\tcout << \"Data differ :(\\n\";\n\t\treturn -1;\n\t}\n\t/**/\n\tresult = resultCheck = QByteArray();\n\n\tcout << \"Counting SHA1 hash..\\n\";\n\n\tuchar sha1Buffer[20];\n\tmemcpy(compressed.data() + hSigLen, hashSha1(compressed.constData() + hSigLen + hShaLen, uint32(compressedLen + hPropsLen + hOriginalSizeLen), sha1Buffer), hShaLen); // count sha1\n\n\tuint32 siglen = 0;\n\n\tcout << \"Signing..\\n\";\n\tRSA *prKey = [] {\n\t\tconst auto bio = makeBIO(\n\t\t\tconst_cast<char*>(\n\t\t\t\t(BetaChannel || AlphaVersion)\n\t\t\t\t\t? PrivateBetaKey\n\t\t\t\t\t: PrivateKey),\n\t\t\t-1);\n\t\treturn PEM_read_bio_RSAPrivateKey(bio.get(), 0, 0, 0);\n\t}();\n\tif (!prKey) {\n\t\tcout << \"Could not read RSA private key!\\n\";\n\t\treturn -1;\n\t}\n\tif (RSA_size(prKey) != hSigLen) {\n\t\tcout << \"Bad private key, size: \" << RSA_size(prKey) << \"\\n\";\n\t\tRSA_free(prKey);\n\t\treturn -1;\n\t}\n\tif (RSA_sign(NID_sha1, (const uchar*)(compressed.constData() + hSigLen), hShaLen, (uchar*)(compressed.data()), &siglen, prKey) != 1) { // count signature\n\t\tcout << \"Signing failed!\\n\";\n\t\tRSA_free(prKey);\n\t\treturn -1;\n\t}\n\tRSA_free(prKey);\n\n\tif (siglen != hSigLen) {\n\t\tcout << \"Bad signature length: \" << siglen << \"\\n\";\n\t\treturn -1;\n\t}\n\t/*\n\tcout << \"Checking signature..\\n\";\n\tRSA *pbKey = [] {\n\t\tconst auto bio = makeBIO(\n\t\t\tconst_cast<char*>(\n\t\t\t\t(BetaChannel || AlphaVersion)\n\t\t\t\t\t? PublicBetaKey\n\t\t\t\t\t: PublicKey),\n\t\t\t-1);\n\t\treturn PEM_read_bio_RSAPublicKey(bio.get(), 0, 0, 0);\n\t}();\n\tif (!pbKey) {\n\t\tcout << \"Could not read RSA public key!\\n\";\n\t\treturn -1;\n\t}\n\tif (RSA_verify(NID_sha1, (const uchar*)(compressed.constData() + hSigLen), hShaLen, (const uchar*)(compressed.constData()), siglen, pbKey) != 1) { // verify signature\n\t\tRSA_free(pbKey);\n\t\tcout << \"Signature verification failed!\\n\";\n\t\treturn -1;\n\t}\n\tcout << \"Signature verified!\\n\";\n\tRSA_free(pbKey);\n\t*/\n#ifdef Q_OS_WIN\n\tQString outName((targetwinarm ? QString(\"tarm64upd%1\") : targetwin64 ? QString(\"tx64upd%1\") : QString(\"tupdate%1\")).arg(AlphaVersion ? AlphaVersion : version));\n#elif defined Q_OS_MAC\n\tQString outName((targetarmac ? QString(\"tarmacupd%1\") : QString(\"tmacupd%1\")).arg(AlphaVersion ? AlphaVersion : version));\n#else\n\tQString outName(QString(\"tlinuxupd%1\").arg(AlphaVersion ? AlphaVersion : version));\n#endif\n\tif (AlphaVersion) {\n\t\toutName += \"_\" + AlphaSignature;\n\t}\n\tQFile out(outName);\n\tif (!out.open(QIODevice::WriteOnly)) {\n\t\tcout << \"Can't open '\" << outName.toUtf8().constData() << \"' for write..\\n\";\n\t\treturn -1;\n\t}\n\tout.write(compressed);\n\tout.close();\n\n\tcout << \"Update file '\" << outName.toUtf8().constData() << \"' written successfully!\\n\";\n\n\treturn writeAlphaKey();\n}\n\nQString countAlphaVersionSignature(quint64 version) { // duplicated in autoupdater.cpp\n\t/*\n\tQByteArray cAlphaPrivateKey(AlphaPrivateKey);\n\tif (cAlphaPrivateKey.isEmpty()) {\n\t\tcout << \"Error: Trying to count alpha version signature without alpha private key!\\n\";\n\t\treturn QString();\n\t}\n\n\tQByteArray signedData = (QLatin1String(\"TelegramBeta_\") + QString::number(version, 16).toLower()).toUtf8();\n\n\tstatic const int32 shaSize = 20, keySize = 128;\n\n\tuchar sha1Buffer[shaSize];\n\thashSha1(signedData.constData(), signedData.size(), sha1Buffer); // count sha1\n\n\tuint32 siglen = 0;\n\n\tRSA *prKey = [&] {\n\t\tconst auto bio = makeBIO(\n\t\t\tconst_cast<char*>(cAlphaPrivateKey.constData()),\n\t\t\t-1);\n\t\treturn PEM_read_bio_RSAPrivateKey(bio.get(), 0, 0, 0);\n\t}();\n\tif (!prKey) {\n\t\tcout << \"Error: Could not read alpha private key!\\n\";\n\t\treturn QString();\n\t}\n\tif (RSA_size(prKey) != keySize) {\n\t\tcout << \"Error: Bad alpha private key size: \" << RSA_size(prKey) << \"\\n\";\n\t\tRSA_free(prKey);\n\t\treturn QString();\n\t}\n\tQByteArray signature;\n\tsignature.resize(keySize);\n\tif (RSA_sign(NID_sha1, (const uchar*)(sha1Buffer), shaSize, (uchar*)(signature.data()), &siglen, prKey) != 1) { // count signature\n\t\tcout << \"Error: Counting alpha version signature failed!\\n\";\n\t\tRSA_free(prKey);\n\t\treturn QString();\n\t}\n\tRSA_free(prKey);\n\n\tif (siglen != keySize) {\n\t\tcout << \"Error: Bad alpha version signature length: \" << siglen << \"\\n\";\n\t\treturn QString();\n\t}\n\n\tsignature = signature.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);\n\tsignature = signature.replace('-', '8').replace('_', 'B');\n\treturn QString::fromUtf8(signature.mid(19, 32));\n\t*/\n\treturn \"0\";\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/_other/packer.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QCoreApplication>\n#include <QtCore/QFileInfo>\n#include <QtCore/QFile>\n#include <QtCore/QDir>\n#include <QtCore/QStringList>\n#include <QtCore/QBuffer>\n#include <QtCore/QDataStream>\n\n#include <zlib.h>\n\nextern \"C\" {\n#include <openssl/bn.h>\n#include <openssl/rsa.h>\n#include <openssl/pem.h>\n#include <openssl/bio.h>\n#include <openssl/err.h>\n#include <openssl/aes.h>\n#include <openssl/evp.h>\n} // extern \"C\"\n\n#if defined Q_OS_WIN && !defined PACKER_USE_PACKAGED // use Lzma SDK for win\n#include <LzmaLib.h>\n#else\n#include <lzma.h>\n#endif\n\n#include <string>\n#include <iostream>\n#include <exception>\n\nusing std::string;\nusing std::wstring;\nusing std::cout;\n"
  },
  {
    "path": "Telegram/SourceFiles/_other/startup_task_win.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include <windows.h>\n#include <shellapi.h>\n\n#include <array>\n#include <string>\n\nusing namespace std;\n\nconstexpr auto kMaxPathLong = 32767;\n\n[[nodiscard]] std::wstring ExecutableDirectory() {\n\tauto exePath = std::array<WCHAR, kMaxPathLong + 1>{ 0 };\n\tconst auto exeLength = GetModuleFileName(\n\t\tnullptr,\n\t\texePath.data(),\n\t\tkMaxPathLong + 1);\n\tif (!exeLength || exeLength >= kMaxPathLong + 1) {\n\t\treturn {};\n\t}\n\tconst auto exe = std::wstring(exePath.data());\n\tconst auto last1 = exe.find_last_of('\\\\');\n\tconst auto last2 = exe.find_last_of('/');\n\tconst auto last = std::max(\n\t\t(last1 == std::wstring::npos) ? -1 : int(last1),\n\t\t(last2 == std::wstring::npos) ? -1 : int(last2));\n\tif (last < 0) {\n\t\treturn {};\n\t}\n\treturn exe.substr(0, last);\n}\n\nint APIENTRY wWinMain(\n\t\tHINSTANCE instance,\n\t\tHINSTANCE prevInstance,\n\t\tLPWSTR cmdParamarg,\n\t\tint cmdShow) {\n\tconst auto directory = ExecutableDirectory();\n\tif (!directory.empty()) {\n\t\tShellExecute(\n\t\t\tnullptr,\n\t\t\tnullptr,\n\t\t\t(directory + L\"\\\\Telegram.exe\").c_str(),\n\t\t\tL\"-autostart\",\n\t\t\tdirectory.data(),\n\t\t\tSW_SHOWNORMAL);\n\t}\n\treturn 0;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/_other/updater.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <string>\n\n#include <windows.h>\n#ifdef small\n#undef small\n#endif // small\n\n#pragma warning(push)\n#pragma warning(disable:4091)\n#include <DbgHelp.h>\n#include <ShlObj.h>\n#pragma warning(pop)\n\n#include <Shellapi.h>\n#include <Shlwapi.h>\n\n#include <deque>\n#include <string>\n\nusing std::deque;\nusing std::wstring;\n\nextern LPTOP_LEVEL_EXCEPTION_FILTER _oldWndExceptionFilter;\nLONG CALLBACK _exceptionFilter(EXCEPTION_POINTERS* pExceptionPointers);\nLPTOP_LEVEL_EXCEPTION_FILTER WINAPI RedirectedSetUnhandledExceptionFilter(_In_opt_ LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter);\n\nstatic int updaterVersion = 1000;\nstatic const WCHAR *updaterVersionStr = L\"0.1.0\";\n"
  },
  {
    "path": "Telegram/SourceFiles/_other/updater_linux.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#define _GLIBCXX_USE_CXX11_ABI 0\n#include <cstdio>\n#include <sys/stat.h>\n#include <sys/types.h>\n#include <cstdlib>\n#include <unistd.h>\n#include <dirent.h>\n#include <pwd.h>\n#include <string>\n#include <deque>\n#include <vector>\n#include <cstring>\n#include <cerrno>\n#include <algorithm>\n#include <cstdarg>\n#include <ctime>\n#include <iostream>\n\nusing std::string;\nusing std::deque;\nusing std::vector;\nusing std::cout;\n\nbool do_mkdir(const char *path) { // from http://stackoverflow.com/questions/675039/how-can-i-create-directory-tree-in-c-linux\n\tstruct stat statbuf;\n\tif (stat(path, &statbuf) != 0) {\n\t\t/* Directory does not exist. EEXIST for race condition */\n\t\tif (mkdir(path, S_IRWXU) != 0 && errno != EEXIST) return false;\n\t} else if (!S_ISDIR(statbuf.st_mode)) {\n\t\terrno = ENOTDIR;\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\nbool _debug = false;\nbool writeprotected = false;\nstring updaterDir;\nstring updaterName;\nstring workDir;\nstring exeName;\nstring exePath;\nstring argv0;\n\nFILE *_logFile = 0;\nvoid openLog() {\n\tif (!_debug || _logFile) return;\n\n\tif (!do_mkdir((workDir + \"DebugLogs\").c_str())) {\n\t\treturn;\n\t}\n\n\ttime_t timer;\n\n\ttime(&timer);\n\tstruct tm *t = localtime(&timer);\n\n\tstatic const int maxFileLen = 65536;\n\tchar logName[maxFileLen];\n\tsprintf(logName, \"%sDebugLogs/%04d%02d%02d_%02d%02d%02d_upd.txt\", workDir.c_str(),\n\t\tt->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);\n\t_logFile = fopen(logName, \"w\");\n}\n\nvoid closeLog() {\n\tif (!_logFile) return;\n\n\tfclose(_logFile);\n\t_logFile = 0;\n}\n\nvoid writeLog(const char *format, ...) {\n\tif (!_logFile) {\n\t\treturn;\n\t}\n\n\tva_list args;\n\tva_start(args, format);\n\tvfprintf(_logFile, format, args);\n\tfprintf(_logFile, \"\\n\");\n\tfflush(_logFile);\n\tva_end(args);\n}\n\nbool copyFile(const char *from, const char *to) {\n\tFILE *ffrom = fopen(from, \"rb\"), *fto = fopen(to, \"wb\");\n\tif (!ffrom) {\n\t\tif (fto) fclose(fto);\n\t\treturn false;\n\t}\n\tif (!fto) {\n\t\tfclose(ffrom);\n\t\treturn false;\n\t}\n\tstatic const int BufSize = 65536;\n\tchar buf[BufSize];\n\twhile (size_t size = fread(buf, 1, BufSize, ffrom)) {\n\t\tfwrite(buf, 1, size, fto);\n\t}\n\n\tstruct stat fst; // from http://stackoverflow.com/questions/5486774/keeping-fileowner-and-permissions-after-copying-file-in-c\n\t//let's say this wont fail since you already worked OK on that fp\n\tif (fstat(fileno(ffrom), &fst) != 0) {\n\t\tfclose(ffrom);\n\t\tfclose(fto);\n\t\treturn false;\n\t}\n\t//update to the same uid/gid\n\tif (!writeprotected && fchown(fileno(fto), fst.st_uid, fst.st_gid) != 0) {\n\t\tfclose(ffrom);\n\t\tfclose(fto);\n\t\treturn false;\n\t}\n\t//update the permissions\n\tif (fchmod(fileno(fto), fst.st_mode) != 0) {\n\t\tfclose(ffrom);\n\t\tfclose(fto);\n\t\treturn false;\n\t}\n\n\tfclose(ffrom);\n\tfclose(fto);\n\n\treturn true;\n}\n\nbool remove_directory(const string &path) { // from http://stackoverflow.com/questions/2256945/removing-a-non-empty-directory-programmatically-in-c-or-c\n\tDIR *d = opendir(path.c_str());\n\twriteLog(\"Removing dir '%s'\", path.c_str());\n\n\tif (!d) {\n\t\twriteLog(\"Could not open dir '%s'\", path.c_str());\n\t\treturn (errno == ENOENT);\n\t}\n\n\twhile (struct dirent *p = readdir(d)) {\n\t\t/* Skip the names \".\" and \"..\" as we don't want to recurse on them. */\n\t\tif (!strcmp(p->d_name, \".\") || !strcmp(p->d_name, \"..\")) continue;\n\n\t\tstring fname = path + '/' + p->d_name;\n\t\tstruct stat statbuf;\n\t\twriteLog(\"Trying to get stat() for '%s'\", fname.c_str());\n\t\tif (!stat(fname.c_str(), &statbuf)) {\n\t\t\tif (S_ISDIR(statbuf.st_mode)) {\n\t\t\t\tif (!remove_directory(fname.c_str())) {\n\t\t\t\t\tclosedir(d);\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twriteLog(\"Unlinking file '%s'\", fname.c_str());\n\t\t\t\tif (unlink(fname.c_str())) {\n\t\t\t\t\twriteLog(\"Failed to unlink '%s'\", fname.c_str());\n\t\t\t\t\tclosedir(d);\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\twriteLog(\"Failed to call stat() on '%s'\", fname.c_str());\n\t\t}\n\t}\n\tclosedir(d);\n\n\twriteLog(\"Finally removing dir '%s'\", path.c_str());\n\treturn !rmdir(path.c_str());\n}\n\nbool mkpath(const char *path) {\n\tint status = 0, pathsize = strlen(path) + 1;\n\tchar *copypath = new char[pathsize];\n\tmemcpy(copypath, path, pathsize);\n\n\tchar *pp = copypath, *sp;\n\twhile (status == 0 && (sp = strchr(pp, '/')) != 0) {\n\t\tif (sp != pp) {\n\t\t\t/* Neither root nor double slash in path */\n\t\t\t*sp = '\\0';\n\t\t\tif (!do_mkdir(copypath)) {\n\t\t\t\tdelete[] copypath;\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t*sp = '/';\n\t\t}\n\t\tpp = sp + 1;\n\t}\n\tdelete[] copypath;\n\treturn do_mkdir(path);\n}\n\nbool equal(string a, string b) {\n\tstd::transform(a.begin(), a.end(), a.begin(), ::tolower);\n\tstd::transform(b.begin(), b.end(), b.begin(), ::tolower);\n\treturn a == b;\n}\n\nvoid delFolder() {\n\tstring delPathOld = workDir + \"tupdates/ready\", delPath = workDir + \"tupdates/temp\", delFolder = workDir + \"tupdates\";\n\twriteLog(\"Fully clearing old path '%s'..\", delPathOld.c_str());\n\tif (!remove_directory(delPathOld)) {\n\t\twriteLog(\"Failed to clear old path! :( New path was used?..\");\n\t}\n\twriteLog(\"Fully clearing path '%s'..\", delPath.c_str());\n\tif (!remove_directory(delPath)) {\n\t\twriteLog(\"Error: failed to clear path! :(\");\n\t}\n\trmdir(delFolder.c_str());\n}\n\nbool update() {\n\twriteLog(\"Update started..\");\n\n\tstring updDir = workDir + \"tupdates/temp\", readyFilePath = workDir + \"tupdates/temp/ready\", tdataDir = workDir + \"tupdates/temp/tdata\";\n\t{\n\t\tFILE *readyFile = fopen(readyFilePath.c_str(), \"rb\");\n\t\tif (readyFile) {\n\t\t\tfclose(readyFile);\n\t\t\twriteLog(\"Ready file found! Using new path '%s'..\", updDir.c_str());\n\t\t} else {\n\t\t\tupdDir = workDir + \"tupdates/ready\"; // old\n\t\t\ttdataDir = workDir + \"tupdates/ready/tdata\";\n\t\t\twriteLog(\"Ready file not found! Using old path '%s'..\", updDir.c_str());\n\t\t}\n\t}\n\n\tdeque<string> dirs;\n\tdirs.push_back(updDir);\n\n\tdeque<string> from, to, forcedirs;\n\n\tdo {\n\t\tstring dir = dirs.front();\n\t\tdirs.pop_front();\n\n\t\tstring toDir = exePath;\n\t\tif (dir.size() > updDir.size() + 1) {\n\t\t\ttoDir += (dir.substr(updDir.size() + 1) + '/');\n\t\t\tforcedirs.push_back(toDir);\n\t\t\twriteLog(\"Parsing dir '%s' in update tree..\", toDir.c_str());\n\t\t}\n\n\t\tDIR *d = opendir(dir.c_str());\n\t\tif (!d) {\n\t\t\twriteLog(\"Failed to open dir %s\", dir.c_str());\n\t\t\treturn false;\n\t\t}\n\n\t\twhile (struct dirent *p = readdir(d)) {\n\t\t\t/* Skip the names \".\" and \"..\" as we don't want to recurse on them. */\n\t\t\tif (!strcmp(p->d_name, \".\") || !strcmp(p->d_name, \"..\")) continue;\n\n\t\t\tstring fname = dir + '/' + p->d_name;\n\t\t\tstruct stat statbuf;\n\t\t\tif (fname.substr(0, tdataDir.size()) == tdataDir && (fname.size() <= tdataDir.size() || fname.at(tdataDir.size()) == '/')) {\n\t\t\t\twriteLog(\"Skipping 'tdata' path '%s'\", fname.c_str());\n\t\t\t} else if (!stat(fname.c_str(), &statbuf)) {\n\t\t\t\tif (S_ISDIR(statbuf.st_mode)) {\n\t\t\t\t\tdirs.push_back(fname);\n\t\t\t\t\twriteLog(\"Added dir '%s' in update tree..\", fname.c_str());\n\t\t\t\t} else {\n\t\t\t\t\tstring tofname = exePath + fname.substr(updDir.size() + 1);\n\t\t\t\t\tif (equal(tofname, updaterName)) { // bad update - has Updater - delete all dir\n\t\t\t\t\t\twriteLog(\"Error: bad update, has Updater! '%s' equal '%s'\", tofname.c_str(), updaterName.c_str());\n\t\t\t\t\t\tdelFolder();\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t} else if (equal(tofname, exePath + \"Forkgram\") && exeName != \"Forkgram\") {\n\t\t\t\t\t\tstring fullBinaryPath = exePath + exeName;\n\t\t\t\t\t\twriteLog(\"Target binary found: '%s', changing to '%s'\", tofname.c_str(), fullBinaryPath.c_str());\n\t\t\t\t\t\ttofname = fullBinaryPath;\n\t\t\t\t\t}\n\t\t\t\t\tif (fname == readyFilePath) {\n\t\t\t\t\t\twriteLog(\"Skipped ready file '%s'\", fname.c_str());\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfrom.push_back(fname);\n\t\t\t\t\t\tto.push_back(tofname);\n\t\t\t\t\t\twriteLog(\"Added file '%s' to be copied to '%s'\", fname.c_str(), tofname.c_str());\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twriteLog(\"Could not get stat() for file %s\", fname.c_str());\n\t\t\t}\n\t\t}\n\t\tclosedir(d);\n\t} while (!dirs.empty());\n\n\tfor (size_t i = 0; i < forcedirs.size(); ++i) {\n\t\tstring forcedir = forcedirs[i];\n\t\twriteLog(\"Forcing dir '%s'..\", forcedir.c_str());\n\t\tif (!forcedir.empty() && !mkpath(forcedir.c_str())) {\n\t\t\twriteLog(\"Error: failed to create dir '%s'..\", forcedir.c_str());\n\t\t\tdelFolder();\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tfor (size_t i = 0; i < from.size(); ++i) {\n\t\tstring fname = from[i], tofname = to[i];\n\n\t\t// it is necessary to remove the old file to not to get an error if appimage file is used by fuse\n\t\tstruct stat statbuf;\n\t\twriteLog(\"Trying to get stat() for '%s'\", tofname.c_str());\n\t\tif (!stat(tofname.c_str(), &statbuf)) {\n\t\t\tif (S_ISDIR(statbuf.st_mode)) {\n\t\t\t\twriteLog(\"Fully clearing path '%s'..\", tofname.c_str());\n\t\t\t\tif (!remove_directory(tofname.c_str())) {\n\t\t\t\t\twriteLog(\"Error: failed to clear path '%s'\", tofname.c_str());\n\t\t\t\t\tdelFolder();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twriteLog(\"Unlinking file '%s'\", tofname.c_str());\n\t\t\t\tif (unlink(tofname.c_str())) {\n\t\t\t\t\twriteLog(\"Error: failed to unlink '%s'\", tofname.c_str());\n\t\t\t\t\tdelFolder();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\twriteLog(\"Copying file '%s' to '%s'..\", fname.c_str(), tofname.c_str());\n\t\tint copyTries = 0, triesLimit = 30;\n\t\tdo {\n\t\t\tif (!copyFile(fname.c_str(), tofname.c_str())) {\n\t\t\t\t++copyTries;\n\t\t\t\tusleep(100000);\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t} while (copyTries < triesLimit);\n\t\tif (copyTries == triesLimit) {\n\t\t\twriteLog(\"Error: failed to copy, asking to retry..\");\n\t\t\tdelFolder();\n\t\t\treturn false;\n\t\t}\n\t}\n\n\twriteLog(\"Update succeed! Clearing folder..\");\n\tdelFolder();\n\treturn true;\n}\n\nstring CurrentExecutablePath(int argc, char *argv[]) {\n\tconstexpr auto kMaxPath = 1024;\n\tchar result[kMaxPath] = { 0 };\n\tauto count = readlink(\"/proc/self/exe\", result, kMaxPath);\n\tif (count > 0) {\n\t\treturn string(result);\n\t}\n\n\t// Fallback to the first command line argument.\n\treturn argc ? string(argv[0]) : string();\n}\n\nint main(int argc, char *argv[]) {\n\tbool needupdate = true;\n\tbool autostart = false;\n\tbool debug = false;\n\tbool tosettings = false;\n\tbool startintray = false;\n\tbool customWorkingDir = false;\n\tbool justUpdate = false;\n\n\tchar *key = 0;\n\tchar *workdir = 0;\n\tfor (int i = 1; i < argc; ++i) {\n\t\tif (equal(argv[i], \"-noupdate\")) {\n\t\t\tneedupdate = false;\n\t\t} else if (equal(argv[i], \"-autostart\")) {\n\t\t\tautostart = true;\n\t\t} else if (equal(argv[i], \"-debug\")) {\n\t\t\tdebug = _debug = true;\n\t\t} else if (equal(argv[i], \"-startintray\")) {\n\t\t\tstartintray = true;\n\t\t} else if (equal(argv[i], \"-tosettings\")) {\n\t\t\ttosettings = true;\n\t\t} else if (equal(argv[i], \"-workdir_custom\")) {\n\t\t\tcustomWorkingDir = true;\n\t\t} else if (equal(argv[i], \"-writeprotected\")) {\n\t\t\twriteprotected = true;\n\t\t\tjustUpdate = true;\n\t\t} else if (equal(argv[i], \"-justupdate\")) {\n\t\t\tjustUpdate = true;\n\t\t} else if (equal(argv[i], \"-key\") && ++i < argc) {\n\t\t\tkey = argv[i];\n\t\t} else if (equal(argv[i], \"-workpath\") && ++i < argc) {\n\t\t\tworkDir = workdir = argv[i];\n\t\t} else if (equal(argv[i], \"-exename\") && ++i < argc) {\n\t\t\texeName = argv[i];\n\t\t} else if (equal(argv[i], \"-exepath\") && ++i < argc) {\n\t\t\texePath = argv[i];\n\t\t} else if (equal(argv[i], \"-argv0\") && ++i < argc) {\n\t\t\targv0 = argv[i];\n\t\t}\n\t}\n\tif (exeName.empty() || exeName.find('/') != string::npos) {\n\t\texeName = \"Forkgram\";\n\t}\n\topenLog();\n\n\twriteLog(\"Updater started, new argments formatting..\");\n\tfor (int i = 0; i < argc; ++i) {\n\t\twriteLog(\"Argument: '%s'\", argv[i]);\n\t}\n\tif (needupdate) writeLog(\"Need to update!\");\n\tif (autostart) writeLog(\"From autostart!\");\n\tif (writeprotected) writeLog(\"Write Protected folder!\");\n\n\tupdaterName = CurrentExecutablePath(argc, argv);\n\twriteLog(\"Updater binary full path is: %s\", updaterName.c_str());\n\tif (exePath.empty()) {\n\t\twriteLog(\"Executable path is not specified :(\");\n\t} else {\n\t\twriteLog(\"Executable path: %s\", exePath.c_str());\n\t}\n\tif (updaterName.size() >= 7) {\n\t\tif (equal(updaterName.substr(updaterName.size() - 7), \"Updater\")) {\n\t\t\tupdaterDir = updaterName.substr(0, updaterName.size() - 7);\n\t\t\twriteLog(\"Updater binary dir is: %s\", updaterDir.c_str());\n\t\t\tif (exePath.empty()) {\n\t\t\t\texePath = updaterDir;\n\t\t\t\twriteLog(\"Using updater binary dir.\", exePath.c_str());\n\t\t\t}\n\t\t\tif (needupdate) {\n\t\t\t\tif (workDir.empty()) { // old app launched, update prepared in tupdates/ready (not in tupdates/temp)\n\t\t\t\t\tcustomWorkingDir = false;\n\n\t\t\t\t\twriteLog(\"No workdir, trying to figure it out\");\n\t\t\t\t\tstruct passwd *pw = getpwuid(getuid());\n\t\t\t\t\tif (pw && pw->pw_dir && strlen(pw->pw_dir)) {\n\t\t\t\t\t\tstring tryDir = pw->pw_dir + string(\"/.TelegramDesktop/\");\n\t\t\t\t\t\tstruct stat statbuf;\n\t\t\t\t\t\twriteLog(\"Trying to use '%s' as workDir, getting stat() for tupdates/ready\", tryDir.c_str());\n\t\t\t\t\t\tif (!stat((tryDir + \"tupdates/ready\").c_str(), &statbuf)) {\n\t\t\t\t\t\t\twriteLog(\"Stat got\");\n\t\t\t\t\t\t\tif (S_ISDIR(statbuf.st_mode)) {\n\t\t\t\t\t\t\t\twriteLog(\"It is directory, using home work dir\");\n\t\t\t\t\t\t\t\tworkDir = tryDir;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (workDir.empty()) {\n\t\t\t\t\t\tworkDir = exePath;\n\n\t\t\t\t\t\tstruct stat statbuf;\n\t\t\t\t\t\twriteLog(\"Trying to use current as workDir, getting stat() for tupdates/ready\");\n\t\t\t\t\t\tif (!stat(\"tupdates/ready\", &statbuf)) {\n\t\t\t\t\t\t\twriteLog(\"Stat got\");\n\t\t\t\t\t\t\tif (S_ISDIR(statbuf.st_mode)) {\n\t\t\t\t\t\t\t\twriteLog(\"It is directory, using current dir\");\n\t\t\t\t\t\t\t\tworkDir = string();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\twriteLog(\"Passed workpath is '%s'\", workDir.c_str());\n\t\t\t\t}\n\t\t\t\tupdate();\n\t\t\t}\n\t\t} else {\n\t\t\twriteLog(\"Error: bad exe name!\");\n\t\t}\n\t} else {\n\t\twriteLog(\"Error: short exe name!\");\n\t}\n\n\t// let the parent launch instead\n\tif (justUpdate) {\n\t\twriteLog(\"Closing log and quitting..\");\n\t} else {\n\t\tconst auto fullBinaryPath = exePath + exeName;\n\n\t\tauto values = vector<string>();\n\t\tconst auto push = [&](string arg) {\n\t\t\t// Force null-terminated .data() call result.\n\t\t\tvalues.push_back(arg + char(0));\n\t\t};\n\t\tpush(!argv0.empty() ? argv0 : fullBinaryPath);\n\t\tpush(\"-noupdate\");\n\t\tif (autostart) push(\"-autostart\");\n\t\tif (debug) push(\"-debug\");\n\t\tif (startintray) push(\"-startintray\");\n\t\tif (tosettings) push(\"-tosettings\");\n\t\tif (key) {\n\t\t\tpush(\"-key\");\n\t\t\tpush(key);\n\t\t}\n\t\tif (customWorkingDir && workdir) {\n\t\t\tpush(\"-workdir\");\n\t\t\tpush(workdir);\n\t\t}\n\n\t\tauto args = vector<char*>();\n\t\tfor (auto &arg : values) {\n\t\t\targs.push_back(arg.data());\n\t\t}\n\t\targs.push_back(nullptr);\n\n\t\tpid_t pid = fork();\n\t\tswitch (pid) {\n\t\tcase -1:\n\t\t\twriteLog(\"fork() failed!\");\n\t\t\treturn 1;\n\t\tcase 0:\n\t\t\texecv(fullBinaryPath.c_str(), args.data());\n\t\t\treturn 1;\n\t\t}\n\n\t\twriteLog(\"Executed Forkgram, closing log and quitting..\");\n\t}\n\n\tcloseLog();\n\n\treturn 0;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/_other/updater_osx.m",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#import <Cocoa/Cocoa.h>\n#include <sys/xattr.h>\n\nNSString *appName = @\"Telegram.app\";\nNSString *appDir = nil;\nNSString *workDir = nil;\n\n#ifdef _DEBUG\nBOOL _debug = YES;\n#else\nBOOL _debug = NO;\n#endif\n\nNSFileHandle *_logFile = nil;\nvoid openLog() {\n\tif (!_debug || _logFile) return;\n\tNSString *logDir = [workDir stringByAppendingString:@\"DebugLogs\"];\n\tif (![[NSFileManager defaultManager] createDirectoryAtPath:logDir withIntermediateDirectories:YES attributes:nil error:nil]) {\n\t\treturn;\n\t}\n\n\tNSDateFormatter *fmt = [[NSDateFormatter alloc] init];\n\t[fmt setLocale:[NSLocale localeWithLocaleIdentifier:@\"en_US_POSIX\"]];\n\t[fmt setDateFormat:@\"'DebugLogs/'yyyyMMdd'_'HHmmss'_update.txt'\"];\n\tNSString *logPath = [workDir stringByAppendingString:[fmt stringFromDate:[NSDate date]]];\n\t[[NSFileManager defaultManager] createFileAtPath:logPath contents:nil attributes:nil];\n\t_logFile = [NSFileHandle fileHandleForWritingAtPath:logPath];\n}\n\nvoid closeLog() {\n\tif (!_logFile) return;\n\n\t[_logFile closeFile];\n}\n\nvoid writeLog(NSString *msg) {\n\tif (!_logFile) return;\n\n\t[_logFile writeData:[[msg stringByAppendingString:@\"\\n\"] dataUsingEncoding:NSUTF8StringEncoding]];\n\t[_logFile synchronizeFile];\n}\n\nvoid RemoveQuarantineAttribute(NSString *path) {\n\tconst char *kQuarantineAttribute = \"com.apple.quarantine\";\n\n\twriteLog([@\"Removing quarantine: \" stringByAppendingString:path]);\n\tremovexattr([path fileSystemRepresentation], kQuarantineAttribute, 0);\n}\n\nvoid RemoveQuarantineFromBundle(NSString *path) {\n\tRemoveQuarantineAttribute(path);\n\tRemoveQuarantineAttribute([path stringByAppendingString:@\"/Contents/MacOS/Telegram\"]);\n\tRemoveQuarantineAttribute([path stringByAppendingString:@\"/Contents/Helpers/crashpad_handler\"]);\n\tRemoveQuarantineAttribute([path stringByAppendingString:@\"/Contents/Frameworks/Updater\"]);\n}\n\nvoid delFolder() {\n\twriteLog([@\"Fully clearing old path: \" stringByAppendingString:[workDir stringByAppendingString:@\"tupdates/ready\"]]);\n\tif (![[NSFileManager defaultManager] removeItemAtPath:[workDir stringByAppendingString:@\"tupdates/ready\"] error:nil]) {\n\t\twriteLog(@\"Failed to clear old path! :( New path was used?..\");\n\t}\n\twriteLog([@\"Fully clearing new path: \" stringByAppendingString:[workDir stringByAppendingString:@\"tupdates/temp\"]]);\n\tif (![[NSFileManager defaultManager] removeItemAtPath:[workDir stringByAppendingString:@\"tupdates/temp\"] error:nil]) {\n\t\twriteLog(@\"Error: failed to clear new path! :(\");\n\t}\n\trmdir([[workDir stringByAppendingString:@\"tupdates\"] fileSystemRepresentation]);\n}\n\nint main(int argc, const char * argv[]) {\n\tNSString *path = [[NSBundle mainBundle] bundlePath];\n\tif (!path) {\n\t\treturn -1;\n\t}\n\tNSRange range = [path rangeOfString:@\".app/\" options:NSBackwardsSearch];\n\tif (range.location == NSNotFound) {\n\t\treturn -1;\n\t}\n\tpath = [path substringToIndex:range.location > 0 ? range.location : 0];\n\n\trange = [path rangeOfString:@\"/\" options:NSBackwardsSearch];\n\tNSString *appRealName = (range.location == NSNotFound) ? path : [path substringFromIndex:range.location + 1];\n\tappRealName = [[NSArray arrayWithObjects:appRealName, @\".app\", nil] componentsJoinedByString:@\"\"];\n\tappDir = (range.location == NSNotFound) ? @\"\" : [path substringToIndex:range.location + 1];\n\tNSString *appDirFull = [appDir stringByAppendingString:appRealName];\n\n\topenLog();\n\tpid_t procId = 0;\n\tBOOL update = YES, toSettings = NO, autoStart = NO, startInTray = NO;\n\tBOOL customWorkingDir = NO;\n\tNSString *key = nil;\n\tfor (int i = 0; i < argc; ++i) {\n\t\tif ([@\"-workpath\" isEqualToString:[NSString stringWithUTF8String:argv[i]]]) {\n\t\t\tif (++i < argc) {\n\t\t\t\tworkDir = [NSString stringWithUTF8String:argv[i]];\n\t\t\t}\n\t\t} else if ([@\"-procid\" isEqualToString:[NSString stringWithUTF8String:argv[i]]]) {\n\t\t\tif (++i < argc) {\n\t\t\t\tNSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];\n\t\t\t\t[formatter setNumberStyle:NSNumberFormatterDecimalStyle];\n\t\t\t\tprocId = [[formatter numberFromString:[NSString stringWithUTF8String:argv[i]]] intValue];\n\t\t\t}\n\t\t} else if ([@\"-noupdate\" isEqualToString:[NSString stringWithUTF8String:argv[i]]]) {\n\t\t\tupdate = NO;\n\t\t} else if ([@\"-tosettings\" isEqualToString:[NSString stringWithUTF8String:argv[i]]]) {\n\t\t\ttoSettings = YES;\n\t\t} else if ([@\"-autostart\" isEqualToString:[NSString stringWithUTF8String:argv[i]]]) {\n\t\t\tautoStart = YES;\n\t\t} else if ([@\"-debug\" isEqualToString:[NSString stringWithUTF8String:argv[i]]]) {\n\t\t\t_debug = YES;\n\t\t} else if ([@\"-startintray\" isEqualToString:[NSString stringWithUTF8String:argv[i]]]) {\n\t\t\tstartInTray = YES;\n\t\t} else if ([@\"-workdir_custom\" isEqualToString:[NSString stringWithUTF8String:argv[i]]]) {\n\t\t\tcustomWorkingDir = YES;\n\t\t} else if ([@\"-key\" isEqualToString:[NSString stringWithUTF8String:argv[i]]]) {\n\t\t\tif (++i < argc) key = [NSString stringWithUTF8String:argv[i]];\n\t\t}\n\t}\n\tif (!workDir) {\n\t\tworkDir = appDir;\n\t\tcustomWorkingDir = NO;\n\t}\n\topenLog();\n\tNSMutableArray *argsArr = [[NSMutableArray alloc] initWithCapacity:argc];\n\tfor (int i = 0; i < argc; ++i) {\n\t\t[argsArr addObject:[NSString stringWithUTF8String:argv[i]]];\n\t}\n\twriteLog([[NSArray arrayWithObjects:@\"Arguments: '\", [argsArr componentsJoinedByString:@\"' '\"], @\"'..\", nil] componentsJoinedByString:@\"\"]);\n\tif (key) writeLog([@\"Key: \" stringByAppendingString:key]);\n\tif (toSettings) writeLog(@\"To Settings!\");\n\n\tif (procId) {\n\t\tNSRunningApplication *app = [NSRunningApplication runningApplicationWithProcessIdentifier:procId];\n\t\tfor (int i = 0; i < 5 && app != nil && ![app isTerminated]; ++i) {\n\t\t\tusleep(200000);\n\t\t\tapp = [NSRunningApplication runningApplicationWithProcessIdentifier:procId];\n\t\t}\n\t\tif (app) [app forceTerminate];\n\t\tapp = [NSRunningApplication runningApplicationWithProcessIdentifier:procId];\n\t\tfor (int i = 0; i < 5 && app != nil && ![app isTerminated]; ++i) {\n\t\t\tusleep(200000);\n\t\t\tapp = [NSRunningApplication runningApplicationWithProcessIdentifier:procId];\n\t\t}\n\t}\n\n\tif (update) {\n\t\tNSFileManager *fileManager = [NSFileManager defaultManager];\n\t\tNSString *readyFilePath = [workDir stringByAppendingString:@\"tupdates/temp/ready\"];\n\t\tNSString *srcDir = [workDir stringByAppendingString:@\"tupdates/temp/\"], *srcEnum = [workDir stringByAppendingString:@\"tupdates/temp\"];\n\t\tif ([fileManager fileExistsAtPath:readyFilePath]) {\n\t\t\twriteLog([@\"Ready file found! Using new path: \" stringByAppendingString: srcEnum]);\n\t\t} else {\n\t\t\tsrcDir = [workDir stringByAppendingString:@\"tupdates/ready/\"]; // old\n\t\t\tsrcEnum = [workDir stringByAppendingString:@\"tupdates/ready\"];\n\t\t\twriteLog([@\"Ready file not found! Using old path: \" stringByAppendingString: srcEnum]);\n\t\t}\n\n\t\twriteLog([@\"Starting update files iteration, path: \" stringByAppendingString: srcEnum]);\n\n\t\t// Take the Updater (this currently running binary) from the place where it was placed by Telegram\n\t\t// and copy it to the folder with the new version of the app (ready),\n\t\t// so it won't be deleted when we will clear the \"Telegram.app/Contents\" folder.\n\t\tNSString *oldVersionUpdaterPath = [appDirFull stringByAppendingString: @\"/Contents/Frameworks/Updater\" ];\n\t\tNSString *newVersionUpdaterPath = [srcEnum stringByAppendingString:[[NSArray arrayWithObjects:@\"/\", appName, @\"/Contents/Frameworks/Updater\", nil] componentsJoinedByString:@\"\"]];\n\t\twriteLog([[NSArray arrayWithObjects: @\"Copying Updater from old path \", oldVersionUpdaterPath, @\" to new path \", newVersionUpdaterPath, nil] componentsJoinedByString:@\"\"]);\n\t\tif (![fileManager fileExistsAtPath:newVersionUpdaterPath]) {\n\t\t\tif (![fileManager copyItemAtPath:oldVersionUpdaterPath toPath:newVersionUpdaterPath error:nil]) {\n\t\t\t\twriteLog([[NSArray arrayWithObjects: @\"Failed to copy file from \", oldVersionUpdaterPath, @\" to \", newVersionUpdaterPath, nil] componentsJoinedByString:@\"\"]);\n\t\t\t\tdelFolder();\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\n\n\t\tNSString *contentsPath = [appDirFull stringByAppendingString: @\"/Contents\"];\n\t\twriteLog([[NSArray arrayWithObjects: @\"Clearing dir \", contentsPath, nil] componentsJoinedByString:@\"\"]);\n\t\tif (![fileManager removeItemAtPath:contentsPath error:nil]) {\n\t\t\twriteLog([@\"Failed to clear path for directory \" stringByAppendingString:contentsPath]);\n\t\t\tdelFolder();\n\t\t\treturn -1;\n\t\t}\n\n\t\tNSArray *keys = [NSArray arrayWithObject:NSURLIsDirectoryKey];\n\t\tNSDirectoryEnumerator *enumerator = [fileManager\n\t\t\t\t\t\t\t\t\t\t\t enumeratorAtURL:[NSURL fileURLWithPath:srcEnum]\n\t\t\t\t\t\t\t\t\t\t\t includingPropertiesForKeys:keys\n\t\t\t\t\t\t\t\t\t\t\t options:0\n\t\t\t\t\t\t\t\t\t\t\t errorHandler:^(NSURL *url, NSError *error) {\n\t\t\t\t\t\t\t\t\t\t\t\t writeLog([[[@\"Error in enumerating \" stringByAppendingString:[url absoluteString]] stringByAppendingString: @\" error is: \"] stringByAppendingString: [error description]]);\n\t\t\t\t\t\t\t\t\t\t\t\t return NO;\n\t\t\t\t\t\t\t\t\t\t\t }];\n\t\tfor (NSURL *url in enumerator) {\n\t\t\tNSString *srcPath = [url path];\n\t\t\twriteLog([@\"Handling file \" stringByAppendingString:srcPath]);\n\t\t\tNSRange r = [srcPath rangeOfString:srcDir];\n\t\t\tif (r.location != 0) {\n\t\t\t\twriteLog([@\"Bad file found, no base path \" stringByAppendingString:srcPath]);\n\t\t\t\tdelFolder();\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tNSString *pathPart = [srcPath substringFromIndex:r.length];\n\t\t\tr = [pathPart rangeOfString:appName];\n\t\t\tif (r.location != 0) {\n\t\t\t\twriteLog([@\"Skipping not app file \" stringByAppendingString:srcPath]);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tNSString *dstPath = [appDirFull stringByAppendingString:[pathPart substringFromIndex:r.length]];\n\t\t\tNSError *error;\n\t\t\tNSNumber *isDirectory = nil;\n\t\t\tif (![url getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:&error]) {\n\t\t\t\twriteLog([@\"Failed to get IsDirectory for file \" stringByAppendingString:[url path]]);\n\t\t\t\tdelFolder();\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ([isDirectory boolValue]) {\n\t\t\t\twriteLog([[NSArray arrayWithObjects: @\"Copying dir \", srcPath, @\" to \", dstPath, nil] componentsJoinedByString:@\"\"]);\n\t\t\t\tif (![fileManager createDirectoryAtPath:dstPath withIntermediateDirectories:YES attributes:nil error:nil]) {\n\t\t\t\t\twriteLog([@\"Failed to force path for directory \" stringByAppendingString:dstPath]);\n\t\t\t\t\tdelFolder();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t} else if ([srcPath isEqualToString:readyFilePath]) {\n\t\t\t\twriteLog([[NSArray arrayWithObjects: @\"Skipping ready file \", srcPath, nil] componentsJoinedByString:@\"\"]);\n\t\t\t} else if ([fileManager fileExistsAtPath:dstPath]) {\n\t\t\t\tif (![[NSData dataWithContentsOfFile:srcPath] writeToFile:dstPath atomically:YES]) {\n\t\t\t\t\twriteLog([@\"Failed to edit file \" stringByAppendingString:dstPath]);\n\t\t\t\t\tdelFolder();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (![fileManager copyItemAtPath:srcPath toPath:dstPath error:nil]) {\n\t\t\t\t\twriteLog([@\"Failed to copy file to \" stringByAppendingString:dstPath]);\n\t\t\t\t\tdelFolder();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tdelFolder();\n\t}\n\n\tNSString *appPath = [[NSArray arrayWithObjects:appDir, appRealName, nil] componentsJoinedByString:@\"\"];\n\n\tRemoveQuarantineFromBundle(appPath);\n\n\tNSMutableArray *args = [[NSMutableArray alloc] initWithObjects: @\"-noupdate\", nil];\n\tif (toSettings) [args addObject:@\"-tosettings\"];\n\tif (_debug) [args addObject:@\"-debug\"];\n\tif (startInTray) [args addObject:@\"-startintray\"];\n\tif (autoStart) [args addObject:@\"-autostart\"];\n\tif (key) {\n\t\t[args addObject:@\"-key\"];\n\t\t[args addObject:key];\n\t}\n\tif (customWorkingDir) {\n\t\t[args addObject:@\"-workdir\"];\n\t\t[args addObject:workDir];\n\t}\n\twriteLog([[NSArray arrayWithObjects:@\"Running application '\", appPath, @\"' with args '\", [args componentsJoinedByString:@\"' '\"], @\"'..\", nil] componentsJoinedByString:@\"\"]);\n\n\tfor (int i = 0; i < 5; ++i) {\n\t\tNSError *error = nil;\n\t\tNSRunningApplication *result = [[NSWorkspace sharedWorkspace]\n\t\t\t\t\tlaunchApplicationAtURL:[NSURL fileURLWithPath:appPath]\n\t\t\t\t\toptions:NSWorkspaceLaunchDefault\n\t\t\t\t\tconfiguration:[NSDictionary\n\t\t\t\t\t\t\t\t   dictionaryWithObject:args\n\t\t\t\t\t\t\t\t   forKey:NSWorkspaceLaunchConfigurationArguments]\n\t\t\t\t\terror:&error];\n\t\tif (result) {\n\t\t\tcloseLog();\n\t\t\treturn 0;\n\t\t}\n\t\twriteLog([[NSString stringWithFormat:@\"Could not run application, error %ld: \", (long)[error code]] stringByAppendingString: error ? [error localizedDescription] : @\"(nil)\"]);\n\t\tusleep(200000);\n\t}\n\tcloseLog();\n\treturn -1;\n}\n\n"
  },
  {
    "path": "Telegram/SourceFiles/_other/updater_win.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"updater.h\"\n\n#include \"base/platform/win/base_windows_safe_library.h\"\n\nbool _debug = false;\n\nwstring updaterName, updaterDir, updateTo, exeName, customWorkingDir, customKeyFile;\n\nbool equal(const wstring &a, const wstring &b) {\n\treturn !_wcsicmp(a.c_str(), b.c_str());\n}\n\nvoid updateError(const WCHAR *msg, DWORD errorCode) {\n\tWCHAR errMsg[2048];\n\tLPWSTR errorTextFormatted = nullptr;\n\tauto formatFlags = FORMAT_MESSAGE_FROM_SYSTEM\n\t\t| FORMAT_MESSAGE_ALLOCATE_BUFFER\n\t\t| FORMAT_MESSAGE_IGNORE_INSERTS;\n\tFormatMessage(\n\t\tformatFlags,\n\t\tNULL,\n\t\terrorCode,\n\t\tMAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),\n\t\t(LPWSTR)&errorTextFormatted,\n\t\t0,\n\t\t0);\n\tauto errorText = errorTextFormatted\n\t\t? errorTextFormatted\n\t\t: L\"(Unknown error)\";\n\twsprintf(errMsg, L\"%s, error code: %d\\nError message: %s\", msg, errorCode, errorText);\n\n\tMessageBox(0, errMsg, L\"Update error!\", MB_ICONERROR);\n\n\tLocalFree(errorTextFormatted);\n}\n\nHANDLE _logFile = 0;\nvoid openLog() {\n\tif (!_debug || _logFile) return;\n\twstring logPath = L\"DebugLogs\";\n\tif (!CreateDirectory(logPath.c_str(), NULL)) {\n\t\tDWORD errorCode = GetLastError();\n\t\tif (errorCode && errorCode != ERROR_ALREADY_EXISTS) {\n\t\t\tupdateError(L\"Failed to create log directory\", errorCode);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tSYSTEMTIME stLocalTime;\n\n\tGetLocalTime(&stLocalTime);\n\n\tstatic const int maxFileLen = MAX_PATH * 10;\n\tWCHAR logName[maxFileLen];\n\twsprintf(logName, L\"DebugLogs\\\\%04d%02d%02d_%02d%02d%02d_upd.txt\",\n\t\tstLocalTime.wYear, stLocalTime.wMonth, stLocalTime.wDay,\n\t\tstLocalTime.wHour, stLocalTime.wMinute, stLocalTime.wSecond);\n\t_logFile = CreateFile(logName, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);\n\tif (_logFile == INVALID_HANDLE_VALUE) { // :(\n\t\tupdateError(L\"Failed to create log file\", GetLastError());\n\t\t_logFile = 0;\n\t\treturn;\n\t}\n}\n\nvoid closeLog() {\n\tif (!_logFile) return;\n\n\tCloseHandle(_logFile);\n\t_logFile = 0;\n}\n\nvoid writeLog(const wstring &msg) {\n\tif (!_logFile) return;\n\n\twstring full = msg + L'\\n';\n\tDWORD written = 0;\n\tBOOL result = WriteFile(_logFile, full.c_str(), full.size() * sizeof(wchar_t), &written, 0);\n\tif (!result) {\n\t\tupdateError((L\"Failed to write log entry '\" + msg + L\"'\").c_str(), GetLastError());\n\t\tcloseLog();\n\t\treturn;\n\t}\n\tBOOL flushr = FlushFileBuffers(_logFile);\n\tif (!flushr) {\n\t\tupdateError((L\"Failed to flush log on entry '\" + msg + L\"'\").c_str(), GetLastError());\n\t\tcloseLog();\n\t\treturn;\n\t}\n}\n\nvoid fullClearPath(const wstring &dir) {\n\tWCHAR path[4096];\n\tmemcpy(path, dir.c_str(), (dir.size() + 1) * sizeof(WCHAR));\n\tpath[dir.size() + 1] = 0;\n\twriteLog(L\"Fully clearing path '\" + dir + L\"'..\");\n\tSHFILEOPSTRUCT file_op = {\n\t\tNULL,\n\t\tFO_DELETE,\n\t\tpath,\n\t\tL\"\",\n\t\tFOF_NOCONFIRMATION |\n\t\tFOF_NOERRORUI |\n\t\tFOF_SILENT,\n\t\tfalse,\n\t\t0,\n\t\tL\"\"\n\t};\n\tint res = SHFileOperation(&file_op);\n\tif (res) writeLog(L\"Error: failed to clear path! :(\");\n}\n\nvoid delFolder() {\n\twstring delPathOld = L\"tupdates\\\\ready\", delPath = L\"tupdates\\\\temp\", delFolder = L\"tupdates\";\n\tfullClearPath(delPathOld);\n\tfullClearPath(delPath);\n\tRemoveDirectory(delFolder.c_str());\n}\n\nDWORD versionNum = 0, versionLen = 0, readLen = 0;\nWCHAR versionStr[32] = { 0 };\n\nbool update() {\n\twriteLog(L\"Update started..\");\n\n\twstring updDir = L\"tupdates\\\\temp\", readyFilePath = L\"tupdates\\\\temp\\\\ready\", tdataDir = L\"tupdates\\\\temp\\\\tdata\";\n\t{\n\t\tHANDLE readyFile = CreateFile(readyFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);\n\t\tif (readyFile != INVALID_HANDLE_VALUE) {\n\t\t\tCloseHandle(readyFile);\n\t\t} else {\n\t\t\tupdDir = L\"tupdates\\\\ready\"; // old\n\t\t\ttdataDir = L\"tupdates\\\\ready\\\\tdata\";\n\t\t}\n\t}\n\n\tHANDLE versionFile = CreateFile((tdataDir + L\"\\\\version\").c_str(), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);\n\tif (versionFile != INVALID_HANDLE_VALUE) {\n\t\tif (!ReadFile(versionFile, &versionNum, sizeof(DWORD), &readLen, NULL) || readLen != sizeof(DWORD)) {\n\t\t\tversionNum = 0;\n\t\t} else {\n\t\t\tif (versionNum == 0x7FFFFFFF) { // alpha version\n\n\t\t\t} else if (!ReadFile(versionFile, &versionLen, sizeof(DWORD), &readLen, NULL) || readLen != sizeof(DWORD) || versionLen > 63) {\n\t\t\t\tversionNum = 0;\n\t\t\t} else if (!ReadFile(versionFile, versionStr, versionLen, &readLen, NULL) || readLen != versionLen) {\n\t\t\t\tversionNum = 0;\n\t\t\t}\n\t\t}\n\t\tCloseHandle(versionFile);\n\t\twriteLog(L\"Version file read.\");\n\t} else {\n\t\twriteLog(L\"Could not open version file to update registry :(\");\n\t}\n\n\tdeque<wstring> dirs;\n\tdirs.push_back(updDir);\n\n\tdeque<wstring> from, to, forcedirs;\n\n\tdo {\n\t\twstring dir = dirs.front();\n\t\tdirs.pop_front();\n\n\t\twstring toDir = updateTo;\n\t\tif (dir.size() > updDir.size() + 1) {\n\t\t\ttoDir += (dir.substr(updDir.size() + 1) + L\"\\\\\");\n\t\t\tforcedirs.push_back(toDir);\n\t\t\twriteLog(L\"Parsing dir '\" + toDir + L\"' in update tree..\");\n\t\t}\n\n\t\tWIN32_FIND_DATA findData;\n\t\tHANDLE findHandle = FindFirstFileEx((dir + L\"\\\\*\").c_str(), FindExInfoStandard, &findData, FindExSearchNameMatch, 0, 0);\n\t\tif (findHandle == INVALID_HANDLE_VALUE) {\n\t\t\tDWORD errorCode = GetLastError();\n\t\t\tif (errorCode == ERROR_PATH_NOT_FOUND) { // no update is ready\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\twriteLog(L\"Error: failed to find update files :(\");\n\t\t\tupdateError(L\"Failed to find update files\", errorCode);\n\t\t\tdelFolder();\n\t\t\treturn false;\n\t\t}\n\n\t\tdo {\n\t\t\twstring fname = dir + L\"\\\\\" + findData.cFileName;\n\t\t\tif (fname.substr(0, tdataDir.size()) == tdataDir && (fname.size() <= tdataDir.size() || fname.at(tdataDir.size()) == '/')) {\n\t\t\t\twriteLog(L\"Skipped 'tdata' path '\" + fname + L\"'\");\n\t\t\t} else if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {\n\t\t\t\tif (findData.cFileName != wstring(L\".\") && findData.cFileName != wstring(L\"..\")) {\n\t\t\t\t\tdirs.push_back(fname);\n\t\t\t\t\twriteLog(L\"Added dir '\" + fname + L\"' in update tree..\");\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twstring tofname = updateTo + fname.substr(updDir.size() + 1);\n\t\t\t\tif (equal(tofname, updaterName)) { // bad update - has Updater.exe - delete all dir\n\t\t\t\t\twriteLog(L\"Error: bad update, has Updater.exe! '\" + tofname + L\"' equal '\" + updaterName + L\"'\");\n\t\t\t\t\tdelFolder();\n\t\t\t\t\treturn false;\n\t\t\t\t} else if (equal(tofname, updateTo + L\"Telegram.exe\") && exeName != L\"Telegram.exe\") {\n\t\t\t\t\twstring fullBinaryPath = updateTo + exeName;\n\t\t\t\t\twriteLog(L\"Target binary found: '\" + tofname + L\"', changing to '\" + fullBinaryPath + L\"'\");\n\t\t\t\t\ttofname = fullBinaryPath;\n\t\t\t\t}\n\t\t\t\tif (equal(fname, readyFilePath)) {\n\t\t\t\t\twriteLog(L\"Skipped ready file '\" + fname + L\"'\");\n\t\t\t\t} else {\n\t\t\t\t\tfrom.push_back(fname);\n\t\t\t\t\tto.push_back(tofname);\n\t\t\t\t\twriteLog(L\"Added file '\" + fname + L\"' to be copied to '\" + tofname + L\"'\");\n\t\t\t\t}\n\t\t\t}\n\t\t} while (FindNextFile(findHandle, &findData));\n\t\tDWORD errorCode = GetLastError();\n\t\tif (errorCode && errorCode != ERROR_NO_MORE_FILES) { // everything is found\n\t\t\twriteLog(L\"Error: failed to find next update file :(\");\n\t\t\tupdateError(L\"Failed to find next update file\", errorCode);\n\t\t\tdelFolder();\n\t\t\treturn false;\n\t\t}\n\t\tFindClose(findHandle);\n\t} while (!dirs.empty());\n\n\tfor (size_t i = 0; i < forcedirs.size(); ++i) {\n\t\twstring forcedir = forcedirs[i];\n\t\twriteLog(L\"Forcing dir '\" + forcedir + L\"'..\");\n\t\tif (!forcedir.empty() && !CreateDirectory(forcedir.c_str(), NULL)) {\n\t\t\tDWORD errorCode = GetLastError();\n\t\t\tif (errorCode && errorCode != ERROR_ALREADY_EXISTS) {\n\t\t\t\twriteLog(L\"Error: failed to create dir '\" + forcedir + L\"'..\");\n\t\t\t\tupdateError(L\"Failed to create directory\", errorCode);\n\t\t\t\tdelFolder();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\twriteLog(L\"Already exists!\");\n\t\t}\n\t}\n\n\tfor (size_t i = 0; i < from.size(); ++i) {\n\t\twstring fname = from[i], tofname = to[i];\n\t\tBOOL copyResult;\n\t\tdo {\n\t\t\twriteLog(L\"Copying file '\" + fname + L\"' to '\" + tofname + L\"'..\");\n\t\t\tint copyTries = 0;\n\t\t\tdo {\n\t\t\t\tcopyResult = CopyFile(fname.c_str(), tofname.c_str(), FALSE);\n\t\t\t\tif (!copyResult) {\n\t\t\t\t\t++copyTries;\n\t\t\t\t\tSleep(100);\n\t\t\t\t} else {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t} while (copyTries < 100);\n\t\t\tif (!copyResult) {\n\t\t\t\twriteLog(L\"Error: failed to copy, asking to retry..\");\n\t\t\t\tWCHAR errMsg[2048];\n\t\t\t\twsprintf(errMsg, L\"Failed to update Telegram :(\\n%s is not accessible.\", tofname.c_str());\n\t\t\t\tif (MessageBox(0, errMsg, L\"Update error!\", MB_ICONERROR | MB_RETRYCANCEL) != IDRETRY) {\n\t\t\t\t\tdelFolder();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t} while (!copyResult);\n\t}\n\n\twriteLog(L\"Update succeed! Clearing folder..\");\n\tdelFolder();\n\treturn true;\n}\n\nvoid updateRegistry() {\n\tif (versionNum && versionNum != 0x7FFFFFFF) {\n\t\twriteLog(L\"Updating registry..\");\n\t\tversionStr[versionLen / 2] = 0;\n\t\tHKEY rkey;\n\t\tLSTATUS status = RegOpenKeyEx(HKEY_CURRENT_USER, L\"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\{53F49750-6209-4FBF-9CA8-7A333C87D1ED}_is1\", 0, KEY_QUERY_VALUE | KEY_SET_VALUE, &rkey);\n\t\tif (status == ERROR_SUCCESS) {\n\t\t\twriteLog(L\"Checking registry install location..\");\n\t\t\tstatic const int bufSize = 4096;\n\t\t\tDWORD locationType, locationSize = bufSize * 2;\n\t\t\tWCHAR locationStr[bufSize], exp[bufSize];\n\t\t\tif (RegQueryValueEx(rkey, L\"InstallLocation\", 0, &locationType, (BYTE*)locationStr, &locationSize) == ERROR_SUCCESS) {\n\t\t\t\tlocationSize /= 2;\n\t\t\t\tif (locationStr[locationSize - 1]) {\n\t\t\t\t\tlocationStr[locationSize++] = 0;\n\t\t\t\t}\n\t\t\t\tif (locationType == REG_EXPAND_SZ) {\n\t\t\t\t\tDWORD copy = ExpandEnvironmentStrings(locationStr, exp, bufSize);\n\t\t\t\t\tif (copy <= bufSize) {\n\t\t\t\t\t\tmemcpy(locationStr, exp, copy * sizeof(WCHAR));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (locationType == REG_EXPAND_SZ || locationType == REG_SZ) {\n\t\t\t\t\tif (PathCanonicalize(exp, locationStr)) {\n\t\t\t\t\t\tmemcpy(locationStr, exp, bufSize * sizeof(WCHAR));\n\t\t\t\t\t\tif (GetFullPathName(L\".\", bufSize, exp, 0) < bufSize) {\n\t\t\t\t\t\t\twstring installpath = locationStr, mypath = exp;\n\t\t\t\t\t\t\tif (installpath == mypath + L\"\\\\\" || true) { // always update reg info, if we found it\n\t\t\t\t\t\t\t\tWCHAR nameStr[bufSize], dateStr[bufSize], publisherStr[bufSize], icongroupStr[bufSize];\n\t\t\t\t\t\t\t\tSYSTEMTIME stLocalTime;\n\t\t\t\t\t\t\t\tGetLocalTime(&stLocalTime);\n\t\t\t\t\t\t\t\tRegSetValueEx(rkey, L\"DisplayVersion\", 0, REG_SZ, (const BYTE*)versionStr, ((versionLen / 2) + 1) * sizeof(WCHAR));\n\t\t\t\t\t\t\t\twsprintf(nameStr, L\"Telegram Desktop\");\n\t\t\t\t\t\t\t\tRegSetValueEx(rkey, L\"DisplayName\", 0, REG_SZ, (const BYTE*)nameStr, (wcslen(nameStr) + 1) * sizeof(WCHAR));\n\t\t\t\t\t\t\t\twsprintf(publisherStr, L\"\");\n\t\t\t\t\t\t\t\tRegSetValueEx(rkey, L\"Publisher\", 0, REG_SZ, (const BYTE*)publisherStr, (wcslen(publisherStr) + 1) * sizeof(WCHAR));\n\t\t\t\t\t\t\t\twsprintf(icongroupStr, L\"Telegram Desktop\");\n\t\t\t\t\t\t\t\tRegSetValueEx(rkey, L\"Inno Setup: Icon Group\", 0, REG_SZ, (const BYTE*)icongroupStr, (wcslen(icongroupStr) + 1) * sizeof(WCHAR));\n\t\t\t\t\t\t\t\twsprintf(dateStr, L\"%04d%02d%02d\", stLocalTime.wYear, stLocalTime.wMonth, stLocalTime.wDay);\n\t\t\t\t\t\t\t\tRegSetValueEx(rkey, L\"InstallDate\", 0, REG_SZ, (const BYTE*)dateStr, (wcslen(dateStr) + 1) * sizeof(WCHAR));\n\n\t\t\t\t\t\t\t\tconst WCHAR *appURL = L\"https://desktop.telegram.org\";\n\t\t\t\t\t\t\t\tRegSetValueEx(rkey, L\"HelpLink\", 0, REG_SZ, (const BYTE*)appURL, (wcslen(appURL) + 1) * sizeof(WCHAR));\n\t\t\t\t\t\t\t\tRegSetValueEx(rkey, L\"URLInfoAbout\", 0, REG_SZ, (const BYTE*)appURL, (wcslen(appURL) + 1) * sizeof(WCHAR));\n\t\t\t\t\t\t\t\tRegSetValueEx(rkey, L\"URLUpdateInfo\", 0, REG_SZ, (const BYTE*)appURL, (wcslen(appURL) + 1) * sizeof(WCHAR));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tRegCloseKey(rkey);\n\t\t}\n\t}\n}\n\nint APIENTRY wWinMain(HINSTANCE instance, HINSTANCE prevInstance, LPWSTR cmdParamarg, int cmdShow) {\n\tbase::Platform::InitDynamicLibraries();\n\n\topenLog();\n\n\t_oldWndExceptionFilter = SetUnhandledExceptionFilter(_exceptionFilter);\n//\tCAPIHook apiHook(\"kernel32.dll\", \"SetUnhandledExceptionFilter\", (PROC)RedirectedSetUnhandledExceptionFilter);\n\n\twriteLog(L\"Updaters started..\");\n\n\tLPWSTR *args;\n\tint argsCount;\n\n\tbool needupdate = false, autostart = false, debug = false, writeprotected = false, startintray = false;\n\targs = CommandLineToArgvW(GetCommandLine(), &argsCount);\n\tif (args) {\n\t\tfor (int i = 1; i < argsCount; ++i) {\n\t\t\twriteLog(std::wstring(L\"Argument: \") + args[i]);\n\t\t\tif (equal(args[i], L\"-update\")) {\n\t\t\t\tneedupdate = true;\n\t\t\t} else if (equal(args[i], L\"-autostart\")) {\n\t\t\t\tautostart = true;\n\t\t\t} else if (equal(args[i], L\"-debug\")) {\n\t\t\t\tdebug = _debug = true;\n\t\t\t\topenLog();\n\t\t\t} else if (equal(args[i], L\"-startintray\")) {\n\t\t\t\tstartintray = true;\n\t\t\t} else if (equal(args[i], L\"-writeprotected\") && ++i < argsCount) {\n\t\t\t\twriteLog(std::wstring(L\"Argument: \") + args[i]);\n\t\t\t\twriteprotected = true;\n\t\t\t\tupdateTo = args[i];\n\t\t\t\tfor (int j = 0, l = updateTo.size(); j < l; ++j) {\n\t\t\t\t\tif (updateTo[j] == L'/') {\n\t\t\t\t\t\tupdateTo[j] = L'\\\\';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (equal(args[i], L\"-workdir\") && ++i < argsCount) {\n\t\t\t\twriteLog(std::wstring(L\"Argument: \") + args[i]);\n\t\t\t\tcustomWorkingDir = args[i];\n\t\t\t} else if (equal(args[i], L\"-key\") && ++i < argsCount) {\n\t\t\t\twriteLog(std::wstring(L\"Argument: \") + args[i]);\n\t\t\t\tcustomKeyFile = args[i];\n\t\t\t} else if (equal(args[i], L\"-exename\") && ++i < argsCount) {\n\t\t\t\twriteLog(std::wstring(L\"Argument: \") + args[i]);\n\t\t\t\texeName = args[i];\n\t\t\t\tfor (int j = 0, l = exeName.size(); j < l; ++j) {\n\t\t\t\t\tif (exeName[j] == L'/' || exeName[j] == L'\\\\') {\n\t\t\t\t\t\texeName = L\"Telegram.exe\";\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (exeName.empty()) {\n\t\t\texeName = L\"Telegram.exe\";\n\t\t}\n\t\tif (needupdate) writeLog(L\"Need to update!\");\n\t\tif (autostart) writeLog(L\"From autostart!\");\n\t\tif (writeprotected) writeLog(L\"Write Protected folder!\");\n\t\tif (!customWorkingDir.empty()) writeLog(L\"Will pass custom working dir: \" + customWorkingDir);\n\n\t\tupdaterName = args[0];\n\t\twriteLog(L\"Updater name is: \" + updaterName);\n\t\tif (updaterName.size() > 11) {\n\t\t\tif (equal(updaterName.substr(updaterName.size() - 11), L\"Updater.exe\")) {\n\t\t\t\tupdaterDir = updaterName.substr(0, updaterName.size() - 11);\n\t\t\t\twriteLog(L\"Updater dir is: \" + updaterDir);\n\t\t\t\tif (!writeprotected) {\n\t\t\t\t\tupdateTo = updaterDir;\n\t\t\t\t}\n\t\t\t\twriteLog(L\"Update to: \" + updateTo);\n\t\t\t\tif (needupdate && update()) {\n\t\t\t\t\tupdateRegistry();\n\t\t\t\t}\n\t\t\t\tif (writeprotected) { // if we can't clear all tupdates\\ready (Updater.exe is there) - clear only version\n\t\t\t\t\tif (DeleteFile(L\"tupdates\\\\temp\\\\tdata\\\\version\") || DeleteFile(L\"tupdates\\\\ready\\\\tdata\\\\version\")) {\n\t\t\t\t\t\twriteLog(L\"Version file deleted!\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\twriteLog(L\"Error: could not delete version file\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twriteLog(L\"Error: bad exe name!\");\n\t\t\t}\n\t\t} else {\n\t\t\twriteLog(L\"Error: short exe name!\");\n\t\t}\n\t\tLocalFree(args);\n\t} else {\n\t\twriteLog(L\"Error: No command line arguments!\");\n\t}\n\n\twstring targs;\n\tif (autostart) targs += L\" -autostart\";\n\tif (debug) targs += L\" -debug\";\n\tif (startintray) targs += L\" -startintray\";\n\tif (!customWorkingDir.empty()) {\n\t\ttargs += L\" -workdir \\\"\" + customWorkingDir + L\"\\\"\";\n\t}\n\tif (!customKeyFile.empty()) {\n\t\ttargs += L\" -key \\\"\" + customKeyFile + L\"\\\"\";\n\t}\n\twriteLog(L\"Result arguments: \" + targs);\n\n\tbool executed = false;\n\tif (writeprotected) { // run un-elevated\n\t\twriteLog(L\"Trying to run un-elevated by temp.lnk\");\n\n\t\tHRESULT hres = CoInitialize(0);\n\t\tif (SUCCEEDED(hres)) {\n\t\t\tIShellLink* psl;\n\t\t\tHRESULT hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl);\n\t\t\tif (SUCCEEDED(hres)) {\n\t\t\t\tIPersistFile* ppf;\n\n\t\t\t\twstring exe = updateTo + exeName, dir = updateTo;\n\t\t\t\tpsl->SetArguments((targs.size() ? targs.substr(1) : targs).c_str());\n\t\t\t\tpsl->SetPath(exe.c_str());\n\t\t\t\tpsl->SetWorkingDirectory(dir.c_str());\n\t\t\t\tpsl->SetDescription(L\"\");\n\n\t\t\t\thres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf);\n\n\t\t\t\tif (SUCCEEDED(hres)) {\n\t\t\t\t\twstring lnk = L\"tupdates\\\\temp\\\\temp.lnk\";\n\t\t\t\t\thres = ppf->Save(lnk.c_str(), TRUE);\n\t\t\t\t\tif (!SUCCEEDED(hres)) {\n\t\t\t\t\t\tlnk = L\"tupdates\\\\ready\\\\temp.lnk\"; // old\n\t\t\t\t\t\thres = ppf->Save(lnk.c_str(), TRUE);\n\t\t\t\t\t}\n\t\t\t\t\tppf->Release();\n\n\t\t\t\t\tif (SUCCEEDED(hres)) {\n\t\t\t\t\t\twriteLog(L\"Executing un-elevated through link..\");\n\t\t\t\t\t\tShellExecute(0, 0, L\"explorer.exe\", lnk.c_str(), 0, SW_SHOWNORMAL);\n\t\t\t\t\t\texecuted = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\twriteLog(L\"Error: ppf->Save failed\");\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\twriteLog(L\"Error: Could not create interface IID_IPersistFile\");\n\t\t\t\t}\n\t\t\t\tpsl->Release();\n\t\t\t} else {\n\t\t\t\twriteLog(L\"Error: could not create instance of IID_IShellLink\");\n\t\t\t}\n\t\t\tCoUninitialize();\n\t\t} else {\n\t\t\twriteLog(L\"Error: Could not initialize COM\");\n\t\t}\n\t}\n\tif (!executed) {\n\t\tShellExecute(0, 0, (updateTo + exeName).c_str(), (L\"-noupdate\" + targs).c_str(), 0, SW_SHOWNORMAL);\n\t}\n\n\twriteLog(L\"Executed '\" + exeName + L\"', closing log and quitting..\");\n\tcloseLog();\n\n\treturn 0;\n}\n\nstatic const WCHAR *_programName = L\"Telegram Desktop\"; // folder in APPDATA, if current path is unavailable for writing\nstatic const WCHAR *_exeName = L\"Updater.exe\";\n\nLPTOP_LEVEL_EXCEPTION_FILTER _oldWndExceptionFilter = 0;\n\ntypedef BOOL (FAR STDAPICALLTYPE *t_miniDumpWriteDump)(\n\t_In_ HANDLE hProcess,\n\t_In_ DWORD ProcessId,\n\t_In_ HANDLE hFile,\n\t_In_ MINIDUMP_TYPE DumpType,\n\t_In_opt_ PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,\n\t_In_opt_ PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,\n\t_In_opt_ PMINIDUMP_CALLBACK_INFORMATION CallbackParam\n);\nt_miniDumpWriteDump miniDumpWriteDump = 0;\n\nHANDLE _generateDumpFileAtPath(const WCHAR *path) {\n\tstatic const int maxFileLen = MAX_PATH * 10;\n\n\tWCHAR szPath[maxFileLen];\n\twsprintf(szPath, L\"%stdata\\\\\", path);\n\tif (!CreateDirectory(szPath, NULL)) {\n\t\tif (GetLastError() != ERROR_ALREADY_EXISTS) {\n\t\t\treturn 0;\n\t\t}\n\t}\n\twsprintf(szPath, L\"%sdumps\\\\\", path);\n\tif (!CreateDirectory(szPath, NULL)) {\n\t\tif (GetLastError() != ERROR_ALREADY_EXISTS) {\n\t\t\treturn 0;\n\t\t}\n\t}\n\n\tWCHAR szFileName[maxFileLen];\n\tWCHAR szExeName[maxFileLen];\n\n\twcscpy_s(szExeName, _exeName);\n\tWCHAR *dotFrom = wcschr(szExeName, WCHAR(L'.'));\n\tif (dotFrom) {\n\t\twsprintf(dotFrom, L\"\");\n\t}\n\n\tSYSTEMTIME stLocalTime;\n\n\tGetLocalTime(&stLocalTime);\n\n\twsprintf(\n\t\tszFileName, L\"%s%s-%s-%04d%02d%02d-%02d%02d%02d-%ld-%ld.dmp\",\n\t\tszPath, szExeName, updaterVersionStr,\n\t\tstLocalTime.wYear, stLocalTime.wMonth, stLocalTime.wDay,\n\t\tstLocalTime.wHour, stLocalTime.wMinute, stLocalTime.wSecond,\n\t\tGetCurrentProcessId(), GetCurrentThreadId());\n\treturn CreateFile(szFileName, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_WRITE|FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0);\n}\n\nvoid _generateDump(EXCEPTION_POINTERS* pExceptionPointers) {\n\tstatic const int maxFileLen = MAX_PATH * 10;\n\n\tcloseLog();\n\n\tHMODULE hDll = LoadLibrary(L\"DBGHELP.DLL\");\n\tif (!hDll) return;\n\n\tminiDumpWriteDump = (t_miniDumpWriteDump)GetProcAddress(hDll, \"MiniDumpWriteDump\");\n\tif (!miniDumpWriteDump) return;\n\n\tHANDLE hDumpFile = 0;\n\n\tWCHAR szPath[maxFileLen];\n\tDWORD len = GetModuleFileName(GetModuleHandle(0), szPath, maxFileLen);\n\tif (!len) return;\n\n\tWCHAR *pathEnd = szPath + len;\n\n\tif (!_wcsicmp(pathEnd - wcslen(_exeName), _exeName)) {\n\t\twsprintf(pathEnd - wcslen(_exeName), L\"\");\n\t\thDumpFile = _generateDumpFileAtPath(szPath);\n\t}\n\tif (!hDumpFile || hDumpFile == INVALID_HANDLE_VALUE) {\n\t\tWCHAR wstrPath[maxFileLen];\n\t\tDWORD wstrPathLen = GetEnvironmentVariable(L\"APPDATA\", wstrPath, maxFileLen);\n\t\tif (wstrPathLen) {\n\t\t\twsprintf(wstrPath + wstrPathLen, L\"\\\\%s\\\\\", _programName);\n\t\t\thDumpFile = _generateDumpFileAtPath(wstrPath);\n\t\t}\n\t}\n\n\tif (!hDumpFile || hDumpFile == INVALID_HANDLE_VALUE) {\n\t\treturn;\n\t}\n\n\tMINIDUMP_EXCEPTION_INFORMATION ExpParam = {0};\n\tExpParam.ThreadId = GetCurrentThreadId();\n\tExpParam.ExceptionPointers = pExceptionPointers;\n\tExpParam.ClientPointers = TRUE;\n\n\tminiDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, MiniDumpWithDataSegs, &ExpParam, NULL, NULL);\n}\n\nLONG CALLBACK _exceptionFilter(EXCEPTION_POINTERS* pExceptionPointers) {\n\t_generateDump(pExceptionPointers);\n\treturn _oldWndExceptionFilter ? (*_oldWndExceptionFilter)(pExceptionPointers) : EXCEPTION_CONTINUE_SEARCH;\n}\n\n// see http://www.codeproject.com/Articles/154686/SetUnhandledExceptionFilter-and-the-C-C-Runtime-Li\nLPTOP_LEVEL_EXCEPTION_FILTER WINAPI RedirectedSetUnhandledExceptionFilter(_In_opt_ LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter) {\n\t// When the CRT calls SetUnhandledExceptionFilter with NULL parameter\n\t// our handler will not get removed.\n\t_oldWndExceptionFilter = lpTopLevelExceptionFilter;\n\treturn 0;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_as_copy.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_as_copy.h\"\n\n#include \"api/api_common.h\"\n#include \"api/api_sending.h\"\n#include \"api/api_text_entities.h\"\n#include \"apiwrap.h\"\n#include \"base/random.h\"\n#include \"base/unixtime.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_drafts.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"main/main_session.h\"\n\nnamespace Api::AsCopy {\nnamespace {\n\nMTPInputSingleMedia PrepareAlbumItemMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPInputMedia &media,\n\t\tuint64 randomId,\n\t\tbool emptyText,\n\t\tTextWithTags comment) {\n\tauto commentEntities = TextWithEntities {\n\t\tcomment.text,\n\t\tTextUtilities::ConvertTextTagsToEntities(comment.tags)\n\t};\n\n\tauto caption = item->originalText();\n\tTextUtilities::Trim(caption);\n\tauto sentEntities = Api::EntitiesToMTP(\n\t\t&item->history()->session(),\n\t\temptyText ? commentEntities.entities : caption.entities,\n\t\tApi::ConvertOption::SkipLocal);\n\tconst auto flags = !sentEntities.v.isEmpty()\n\t\t? MTPDinputSingleMedia::Flag::f_entities\n\t\t: MTPDinputSingleMedia::Flag(0);\n\n\treturn MTP_inputSingleMedia(\n\t\tMTP_flags(flags),\n\t\tmedia,\n\t\tMTP_long(randomId),\n\t\tMTP_string(emptyText ? commentEntities.text : caption.text),\n\t\tsentEntities);\n}\n\nMTPinputMedia InputMediaFromItem(not_null<HistoryItem*> i) {\n\tif (const auto document = i->media()->document()) {\n\t\treturn MTP_inputMediaDocument(\n\t\t\tMTP_flags(MTPDinputMediaDocument::Flag(0)),\n\t\t\tdocument->mtpInput(),\n\t\t\tdocument->goodThumbnailPhoto()\n\t\t\t\t? document->goodThumbnailPhoto()->mtpInput()\n\t\t\t\t: MTPInputPhoto(),\n\t\t\tMTP_int(0),\n\t\t\tMTP_int(0),\n\t\t\tMTPstring());\n\t} else if (const auto photo = i->media()->photo()) {\n\t\treturn MTP_inputMediaPhoto(\n\t\t\tMTP_flags(MTPDinputMediaPhoto::Flag(0)),\n\t\t\tphoto->mtpInput(),\n\t\t\tMTP_int(0),\n\t\t\tMTPInputDocument());\n\t} else {\n\t\treturn MTP_inputMediaEmpty();\n\t}\n}\n\nFullReplyTo ReplyToIdFromDraft(not_null<PeerData*> peer) {\n\tconst auto history = peer->owner().history(peer);\n\tconst auto replyTo = [&]() -> FullReplyTo {\n\t\tif (const auto localDraft = history->localDraft(0, 0)) {\n\t\t\treturn localDraft->reply;\n\t\t} else if (const auto cloudDraft = history->cloudDraft(0, 0)) {\n\t\t\treturn cloudDraft->reply;\n\t\t} else {\n\t\t\treturn {};\n\t\t}\n\t}();\n\tif (replyTo) {\n\t\thistory->clearCloudDraft(0, 0);\n\t\thistory->clearLocalDraft(0, 0);\n\t\tpeer->session().api().request(\n\t\t\tMTPmessages_SaveDraft(\n\t\t\t\tMTP_flags(MTPmessages_SaveDraft::Flags(0)),\n\t\t\t\tMTP_inputReplyToStory(MTP_inputPeerEmpty(), MTPint()),\n\t\t\t\thistory->peer->input(),\n\t\t\t\tMTPstring(),\n\t\t\t\tMTPVector<MTPMessageEntity>(),\n\t\t\t\tMTP_inputMediaEmpty(),\n\t\t\t\tMTP_long(0),\n\t\t\t\tSuggestToMTP({})\n\t\t)).send();\n\t}\n\treturn replyTo;\n}\n\n} // namespace\n\nvoid SendAlbumFromItems(\n\t\tHistoryItemsList items,\n\t\tToSend &&toSend,\n\t\tbool andDelete) {\n\tif (items.empty()) {\n\t\treturn;\n\t}\n\tconst auto history = items.front()->history();\n\tconst auto ids = history->owner().itemsToIds(items);\n\tauto medias = QVector<MTPInputSingleMedia>();\n\tfor (const auto &i : items) {\n\t\tmedias.push_back(PrepareAlbumItemMedia(\n\t\t\ti,\n\t\t\tInputMediaFromItem(i),\n\t\t\tbase::RandomValue<uint64>(),\n\t\t\ttoSend.emptyText,\n\t\t\tmedias.empty() ? toSend.comment : TextWithTags()));\n\t}\n\tauto &api = history->owner().session().api();\n\n\tfor (const auto &peer : toSend.peers) {\n\t\tconst auto replyTo = ReplyToIdFromDraft(peer);\n\n\t\tconst auto flags = MTPmessages_SendMultiMedia::Flags(0)\n\t\t\t| (replyTo\n\t\t\t\t? MTPmessages_SendMultiMedia::Flag::f_reply_to\n\t\t\t\t: MTPmessages_SendMultiMedia::Flag(0))\n\t\t\t| (toSend.silent\n\t\t\t\t? MTPmessages_SendMultiMedia::Flag::f_silent\n\t\t\t\t: MTPmessages_SendMultiMedia::Flag(0))\n\t\t\t| (toSend.scheduled\n\t\t\t\t? MTPmessages_SendMultiMedia::Flag::f_schedule_date\n\t\t\t\t: MTPmessages_SendMultiMedia::Flag(0));\n\t\tapi.request(MTPmessages_SendMultiMedia(\n\t\t\tMTP_flags(flags),\n\t\t\tpeer->input(),\n\t\t\tReplyToForMTP(history, replyTo),\n\t\t\tMTP_vector<MTPInputSingleMedia>(medias),\n\t\t\tMTP_int(toSend.scheduled),\n\t\t\tMTP_inputPeerEmpty(),\n\t\t\tMTPInputQuickReplyShortcut(),\n\t\t\tMTP_long(0),\n\t\t\tMTP_long(0)\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\thistory->owner().session().api().applyUpdates(result);\n\n\t\t\tif (andDelete) {\n\t\t\t\thistory->owner().histories().deleteMessages(ids, true);\n\t\t\t\thistory->owner().sendHistoryChangeNotifications();\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t}).send();\n\t}\n}\n\nvoid GuardedSendExistingAlbumFromItem(\n\t\tnot_null<HistoryItem*> item,\n\t\tApi::AsCopy::ToSend &&toSend) {\n\tif (!item->groupId()) {\n\t\treturn;\n\t}\n\tconst auto items = item->history()->owner().groups().find(item)->items;\n\tUpdateFileRef(\n\t\titems,\n\t\t[=] { SendAlbumFromItems(items, base::duplicate(toSend), false); },\n\t\t[](QString){});\n}\n\nvoid SendExistingMediaFromItem(\n\t\tnot_null<HistoryItem*> item,\n\t\tApi::AsCopy::ToSend &&toSend) {\n\tfor (const auto peer : toSend.peers) {\n\t\tconst auto history = peer->owner().history(peer);\n\t\tauto message = MessageToSend(SendAction{ history });\n\t\tif (!item->media()) {\n\t\t\tmessage.textWithTags = PrepareEditText(item);\n\t\t\thistory->session().api().sendMessage(std::move(message));\n\t\t\tcontinue;\n\t\t}\n\t\tmessage.textWithTags = toSend.emptyText\n\t\t\t? toSend.comment\n\t\t\t: PrepareEditText(item);\n\t\tmessage.action.options.silent = toSend.silent;\n\t\tmessage.action.options.scheduled = toSend.scheduled;\n\t\tmessage.action.replyTo = ReplyToIdFromDraft(peer);\n\t\tif (const auto document = item->media()->document()) {\n\t\t\tApi::SendExistingDocument(\n\t\t\t\tstd::move(message),\n\t\t\t\tdocument,\n\t\t\t\titem->fullId());\n\t\t} else if (const auto photo = item->media()->photo()) {\n\t\t\tApi::SendExistingPhoto(std::move(message), photo, item->fullId());\n\t\t}\n\t}\n}\n\nvoid UpdateFileRef(\n\t\tHistoryItemsList list,\n\t\tFn<void()> success,\n\t\tFn<void(QString)> fail) {\n\tif (list.empty()) {\n\t\treturn;\n\t}\n\tconst auto history = list.front()->history();\n\tauto inputMessages = ranges::views::all(\n\t\tlist\n\t) | ranges::views::transform([&](const auto &item) {\n\t\treturn MTP_inputMessageID(MTP_int(item->id));\n\t}) | ranges::to<QVector<MTPInputMessage>>();\n\tconst auto receive = [=](const MTPmessages_Messages &result) {\n\t\tresult.match([&](const MTPDmessages_messagesNotModified &) {\n\t\t\tfail(\"MTPDmessages_messagesNotModified\");\n\t\t}, [&](const auto &d) {\n\t\t\tauto good = true;\n\t\t\tfor (const auto &tlMessage : d.vmessages().v) {\n\t\t\t\ttlMessage.match([&](const MTPDmessage &d) {\n\t\t\t\t\thistory->session().data().updateExistingMessage(d);\n\t\t\t\t}, [&](const auto &) {\n\t\t\t\t\tgood = false;\n\t\t\t\t\tfail(\"MTPDmessageService or MTPDmessageEmpty\");\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (good) {\n\t\t\t\tsuccess();\n\t\t\t}\n\t\t});\n\t};\n\tconst auto receiveError = [=](auto error) {\n\t\tfail(\"Get Message error: \" + error.type());\n\t};\n\tif (const auto channel = history->peer->asChannel()) {\n\t\thistory->session().api().request(\n\t\t\tMTPchannels_GetMessages(\n\t\t\t\tchannel->inputChannel(),\n\t\t\t\tMTP_vector<MTPInputMessage>(std::move(inputMessages)))\n\t\t).done(receive).fail(receiveError).send();\n\t} else {\n\t\thistory->session().api().request(\n\t\t\tMTPmessages_GetMessages(\n\t\t\t\tMTP_vector<MTPInputMessage>(std::move(inputMessages)))\n\t\t).done(receive).fail(receiveError).send();\n\t}\n}\n\n} // namespace Api::AsCopy\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_as_copy.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass HistoryItem;\nclass PeerData;\nstruct TextWithTags;\n\nnamespace Api::AsCopy {\n\nstruct ToSend {\n\tstd::vector<not_null<PeerData*>> peers;\n\tTextWithTags comment;\n\tbool emptyText = false;\n\tbool silent = false;\n\tTimeId scheduled = 0;\n};\n\nvoid GuardedSendExistingAlbumFromItem(\n\tnot_null<HistoryItem*> item,\n\tToSend &&toSend);\n\nvoid SendExistingMediaFromItem(not_null<HistoryItem*> item, ToSend &&toSend);\n\nvoid SendAlbumFromItems(\n\tHistoryItemsList items,\n\tToSend &&toSend,\n\tbool andDelete);\n\nvoid UpdateFileRef(\n\tHistoryItemsList list,\n\tFn<void()> success,\n\tFn<void(QString)> fail);\n\n} // namespace Api::AsCopy\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_attached_stickers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_attached_stickers.h\"\n\n#include \"apiwrap.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"boxes/stickers_box.h\"\n#include \"data/data_document.h\"\n#include \"data/data_photo.h\"\n#include \"lang/lang_keys.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace Api {\n\nAttachedStickers::AttachedStickers(not_null<ApiWrap*> api)\n: _api(&api->instance()) {\n}\n\nvoid AttachedStickers::request(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tMTPmessages_GetAttachedStickers &&mtpRequest) {\n\tconst auto weak = base::make_weak(controller);\n\t_api.request(_requestId).cancel();\n\t_requestId = _api.request(\n\t\tstd::move(mtpRequest)\n\t).done([=](const MTPVector<MTPStickerSetCovered> &result) {\n\t\t_requestId = 0;\n\t\tconst auto strongController = weak.get();\n\t\tif (!strongController) {\n\t\t\treturn;\n\t\t}\n\t\tif (result.v.isEmpty()) {\n\t\t\tstrongController->show(\n\t\t\t\tUi::MakeInformBox(tr::lng_stickers_not_found()));\n\t\t\treturn;\n\t\t} else if (result.v.size() > 1) {\n\t\t\tstrongController->show(\n\t\t\t\tBox<StickersBox>(strongController->uiShow(), result.v));\n\t\t\treturn;\n\t\t}\n\t\t// Single attached sticker pack.\n\t\tconst auto data = result.v.front().match([&](const auto &data) {\n\t\t\treturn &data.vset().data();\n\t\t});\n\n\t\tconst auto setId = (data->vid().v && data->vaccess_hash().v)\n\t\t\t? StickerSetIdentifier{\n\t\t\t\t.id = data->vid().v,\n\t\t\t\t.accessHash = data->vaccess_hash().v }\n\t\t\t: StickerSetIdentifier{ .shortName = qs(data->vshort_name()) };\n\t\tstrongController->show(Box<StickerSetBox>(\n\t\t\tstrongController->uiShow(),\n\t\t\tsetId,\n\t\t\t(data->is_emojis()\n\t\t\t\t? Data::StickersType::Emoji\n\t\t\t\t: data->is_masks()\n\t\t\t\t? Data::StickersType::Masks\n\t\t\t\t: Data::StickersType::Stickers)));\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t\tif (const auto strongController = weak.get()) {\n\t\t\tstrongController->show(\n\t\t\t\tUi::MakeInformBox(tr::lng_stickers_not_found()));\n\t\t}\n\t}).send();\n}\n\nvoid AttachedStickers::requestAttachedStickerSets(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PhotoData*> photo) {\n\trequest(\n\t\tcontroller,\n\t\tMTPmessages_GetAttachedStickers(\n\t\t\tMTP_inputStickeredMediaPhoto(photo->mtpInput())));\n}\n\nvoid AttachedStickers::requestAttachedStickerSets(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<DocumentData*> document) {\n\trequest(\n\t\tcontroller,\n\t\tMTPmessages_GetAttachedStickers(\n\t\t\tMTP_inputStickeredMediaDocument(document->mtpInput())));\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_attached_stickers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n\nclass ApiWrap;\nclass DocumentData;\nclass PhotoData;\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Api {\n\nclass AttachedStickers final {\npublic:\n\texplicit AttachedStickers(not_null<ApiWrap*> api);\n\n\tvoid requestAttachedStickerSets(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PhotoData*> photo);\n\n\tvoid requestAttachedStickerSets(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<DocumentData*> document);\n\nprivate:\n\tvoid request(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tMTPmessages_GetAttachedStickers &&mtpRequest);\n\n\tMTP::Sender _api;\n\tmtpRequestId _requestId = 0;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_authorizations.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_authorizations.h\"\n\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"core/changelogs.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session_settings.h\"\n#include \"main/main_session.h\"\n\nnamespace Api {\nnamespace {\n\nconstexpr auto TestApiId = 17349;\nconstexpr auto SnapApiId = 611335;\nconstexpr auto DesktopApiId = 2040;\n\nAuthorizations::Entry ParseEntry(const MTPDauthorization &data) {\n\tauto result = Authorizations::Entry();\n\n\tresult.hash = data.is_current() ? 0 : data.vhash().v;\n\tresult.incomplete = data.is_password_pending();\n\tresult.callsDisabled = data.is_call_requests_disabled();\n\n\tconst auto apiId = result.apiId = data.vapi_id().v;\n\tconst auto isTest = (apiId == TestApiId);\n\tconst auto isDesktop = (apiId == DesktopApiId)\n\t\t|| (apiId == SnapApiId)\n\t\t|| isTest;\n\n\tconst auto appName = isDesktop\n\t\t? u\"Telegram Desktop%1\"_q.arg(isTest ? \" (GitHub)\" : QString())\n\t\t: qs(data.vapp_name());// + u\" for \"_q + qs(d.vplatform());\n\tconst auto appVer = [&] {\n\t\tconst auto version = qs(data.vapp_version());\n\t\tif (isDesktop) {\n\t\t\tconst auto verInt = version.toInt();\n\t\t\tif (version == QString::number(verInt)) {\n\t\t\t\treturn Core::FormatVersionDisplay(verInt);\n\t\t\t}\n\t\t} else {\n\t\t\tif (const auto index = version.indexOf('('); index >= 0) {\n\t\t\t\treturn version.mid(index);\n\t\t\t}\n\t\t}\n\t\treturn version;\n\t}();\n\n\tresult.name = result.hash\n\t\t? qs(data.vdevice_model())\n\t\t: Core::App().settings().deviceModel();\n\n\tconst auto country = qs(data.vcountry());\n\t//const auto platform = qs(data.vplatform());\n\t//const auto &countries = countriesByISO2();\n\t//const auto j = countries.constFind(country);\n\t//if (j != countries.cend()) {\n\t//\tcountry = QString::fromUtf8(j.value()->name);\n\t//}\n\tresult.system = qs(data.vsystem_version());\n\tresult.platform = qs(data.vplatform());\n\tresult.activeTime = data.vdate_active().v\n\t\t? data.vdate_active().v\n\t\t: data.vdate_created().v;\n\tresult.info = QString(\"%1%2\").arg(\n\t\tappName,\n\t\tappVer.isEmpty() ? QString() : (' ' + appVer));\n\tresult.ip = qs(data.vip());\n\tresult.active = result.hash\n\t\t? Authorizations::ActiveDateString(result.activeTime)\n\t\t: tr::lng_status_online(tr::now);\n\tresult.location = country;\n\n\treturn result;\n}\n\n} // namespace\n\nAuthorizations::Authorizations(not_null<ApiWrap*> api)\n: _api(&api->instance())\n, _autoconfirmPeriod([=] {\n\tconstexpr auto kFallbackCount = 604800;\n\treturn api->session().appConfig().get<int>(\n\t\tu\"authorization_autoconfirm_period\"_q,\n\t\tkFallbackCount);\n})\n, _saveUnreviewed([=] {\n\tapi->session().settings().setUnreviewed(_unreviewed);\n\tapi->session().saveSettingsDelayed();\n}) {\n\t_unreviewed = api->session().settings().unreviewed();\n\tcrl::on_main(&api->session(), [=] { removeExpiredUnreviewed(); });\n\tCore::App().settings().deviceModelChanges(\n\t) | rpl::on_next([=](const QString &model) {\n\t\tauto changed = false;\n\t\tfor (auto &entry : _list) {\n\t\t\tif (!entry.hash) {\n\t\t\t\tentry.name = model;\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t}\n\t\tif (changed) {\n\t\t\t_listChanges.fire({});\n\t\t}\n\t}, _lifetime);\n\n\tif (Core::App().settings().disableCallsLegacy()) {\n\t\ttoggleCallsDisabledHere(true);\n\t}\n}\n\nvoid Authorizations::reload() {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\n\t_requestId = _api.request(MTPaccount_GetAuthorizations(\n\t)).done([=](const MTPaccount_Authorizations &result) {\n\t\t_requestId = 0;\n\t\t_lastReceived = crl::now();\n\t\tconst auto &data = result.data();\n\t\t_ttlDays = data.vauthorization_ttl_days().v;\n\t\t_list = ranges::views::all(\n\t\t\tdata.vauthorizations().v\n\t\t) | ranges::views::transform([](const MTPAuthorization &auth) {\n\t\t\treturn ParseEntry(auth.data());\n\t\t}) | ranges::to<List>;\n\t\tremoveExpiredUnreviewed();\n\t\trefreshCallsDisabledHereFromCloud();\n\t\t_listChanges.fire({});\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t}).send();\n}\n\nvoid Authorizations::cancelCurrentRequest() {\n\t_api.request(base::take(_requestId)).cancel();\n}\n\nvoid Authorizations::refreshCallsDisabledHereFromCloud() {\n\tconst auto that = ranges::find(_list, 0, &Entry::hash);\n\tif (that != end(_list)\n\t\t&& !_toggleCallsDisabledRequests.contains(0)) {\n\t\t_callsDisabledHere = that->callsDisabled;\n\t}\n}\n\nvoid Authorizations::requestTerminate(\n\t\tFn<void(const MTPBool &result)> &&done,\n\t\tFn<void(const MTP::Error &error)> &&fail,\n\t\tstd::optional<uint64> hash) {\n\tconst auto send = [&](auto request) {\n\t\t_api.request(\n\t\t\tstd::move(request)\n\t\t).done([=, done = std::move(done)](const MTPBool &result) {\n\t\t\tdone(result);\n\t\t\tif (mtpIsTrue(result)) {\n\t\t\t\tif (hash) {\n\t\t\t\t\t_list.erase(\n\t\t\t\t\t\tranges::remove(_list, *hash, &Entry::hash),\n\t\t\t\t\t\tend(_list));\n\t\t\t\t} else {\n\t\t\t\t\t_list.clear();\n\t\t\t\t}\n\t\t\t\t_listChanges.fire({});\n\t\t\t}\n\t\t}).fail(\n\t\t\tstd::move(fail)\n\t\t).send();\n\t};\n\tif (hash) {\n\t\tsend(MTPaccount_ResetAuthorization(MTP_long(*hash)));\n\t} else {\n\t\tsend(MTPauth_ResetAuthorizations());\n\t}\n}\n\nAuthorizations::List Authorizations::list() const {\n\treturn _list;\n}\n\nauto Authorizations::listValue() const\n-> rpl::producer<Authorizations::List> {\n\treturn rpl::single(\n\t\tlist()\n\t) | rpl::then(\n\t\t_listChanges.events() | rpl::map([=] { return list(); })\n\t);\n}\n\nrpl::producer<int> Authorizations::totalValue() const {\n\treturn rpl::single(\n\t\ttotal()\n\t) | rpl::then(\n\t\t_listChanges.events() | rpl::map([=] { return total(); })\n\t);\n}\n\nvoid Authorizations::updateTTL(int days) {\n\t_api.request(_ttlRequestId).cancel();\n\t_ttlRequestId = _api.request(MTPaccount_SetAuthorizationTTL(\n\t\tMTP_int(days)\n\t)).done([=] {\n\t\t_ttlRequestId = 0;\n\t}).fail([=] {\n\t\t_ttlRequestId = 0;\n\t}).send();\n\t_ttlDays = days;\n}\n\nrpl::producer<int> Authorizations::ttlDays() const {\n\treturn _ttlDays.value() | rpl::filter(rpl::mappers::_1 != 0);\n}\n\nvoid Authorizations::toggleCallsDisabled(uint64 hash, bool disabled) {\n\tif (const auto sent = _toggleCallsDisabledRequests.take(hash)) {\n\t\t_api.request(*sent).cancel();\n\t}\n\tusing Flag = MTPaccount_ChangeAuthorizationSettings::Flag;\n\tconst auto id = _api.request(MTPaccount_ChangeAuthorizationSettings(\n\t\tMTP_flags(Flag::f_call_requests_disabled),\n\t\tMTP_long(hash),\n\t\tMTPBool(), // encrypted_requests_disabled\n\t\tMTP_bool(disabled)\n\t)).done([=] {\n\t\t_toggleCallsDisabledRequests.remove(hash);\n\t}).fail([=] {\n\t\t_toggleCallsDisabledRequests.remove(hash);\n\t}).send();\n\t_toggleCallsDisabledRequests.emplace(hash, id);\n\tif (!hash) {\n\t\t_callsDisabledHere = disabled;\n\t}\n}\n\nbool Authorizations::callsDisabledHere() const {\n\treturn _callsDisabledHere.current();\n}\n\nrpl::producer<bool> Authorizations::callsDisabledHereValue() const {\n\treturn _callsDisabledHere.value();\n}\n\nrpl::producer<bool> Authorizations::callsDisabledHereChanges() const {\n\treturn _callsDisabledHere.changes();\n}\n\nQString Authorizations::ActiveDateString(TimeId active) {\n\tconst auto now = QDateTime::currentDateTime();\n\tconst auto lastTime = base::unixtime::parse(active);\n\tconst auto nowDate = now.date();\n\tconst auto lastDate = lastTime.date();\n\treturn (lastDate == nowDate)\n\t\t? QLocale().toString(lastTime.time(), QLocale::ShortFormat)\n\t\t: (lastDate.year() == nowDate.year()\n\t\t\t&& lastDate.weekNumber() == nowDate.weekNumber())\n\t\t? langDayOfWeek(lastDate)\n\t\t: QLocale().toString(lastDate, QLocale::ShortFormat);\n}\n\nint Authorizations::total() const {\n\treturn ranges::count_if(\n\t\t_list,\n\t\tranges::not_fn(&Entry::incomplete));\n}\n\ncrl::time Authorizations::lastReceivedTime() {\n\treturn _lastReceived;\n}\n\nconst std::vector<Data::UnreviewedAuth> &Authorizations::unreviewed() {\n\tremoveExpiredUnreviewed();\n\treturn _unreviewed;\n}\n\nvoid Authorizations::removeExpiredUnreviewed() {\n\tconst auto now = base::unixtime::now();\n\tconst auto period = _autoconfirmPeriod();\n\n\tconst auto oldSize = _unreviewed.size();\n\t_unreviewed.erase(\n\t\tstd::remove_if(_unreviewed.begin(), _unreviewed.end(),\n\t\t\t[=](const auto &auth) {\n\t\t\t\treturn (now - auth.date) >= period;\n\t\t\t}),\n\t\t_unreviewed.end());\n\n\tif (_unreviewed.size() != oldSize) {\n\t\t_saveUnreviewed();\n\t}\n}\n\nvoid Authorizations::review(const std::vector<uint64> &hashes, bool confirm) {\n\tfor (const auto hash : hashes) {\n\t\tif (const auto sent = _reviewRequests.take(hash)) {\n\t\t\t_api.request(*sent).cancel();\n\t\t}\n\t}\n\n\tconst auto checkComplete = [=] {\n\t\tif (_reviewRequests.empty()) {\n\t\t\t_saveUnreviewed();\n\t\t\t_unreviewedChanges.fire({});\n\t\t}\n\t};\n\n\tfor (const auto hash : hashes) {\n\t\tconst auto removeFromUnreviewed = [=] {\n\t\t\t_unreviewed.erase(\n\t\t\t\tstd::remove_if(_unreviewed.begin(), _unreviewed.end(),\n\t\t\t\t\t[hash](const auto &auth) { return auth.hash == hash; }),\n\t\t\t\t_unreviewed.end());\n\t\t\t_reviewRequests.remove(hash);\n\t\t\tcheckComplete();\n\t\t};\n\n\t\tif (confirm) {\n\t\t\tusing Flag = MTPaccount_ChangeAuthorizationSettings::Flag;\n\t\t\tconst auto id = _api.request(MTPaccount_ChangeAuthorizationSettings(\n\t\t\t\tMTP_flags(Flag::f_confirmed),\n\t\t\t\tMTP_long(hash),\n\t\t\t\tMTPBool(), // encrypted_requests_disabled\n\t\t\t\tMTPBool() // call_requests_disabled\n\t\t\t)).done([=] {\n\t\t\t\tremoveFromUnreviewed();\n\t\t\t}).fail([=] {\n\t\t\t\tremoveFromUnreviewed();\n\t\t\t}).send();\n\t\t\t_reviewRequests.emplace(hash, id);\n\t\t} else {\n\t\t\tconst auto id = _api.request(MTPaccount_ResetAuthorization(\n\t\t\t\tMTP_long(hash)\n\t\t\t)).done([=](const MTPBool &result) {\n\t\t\t\tif (mtpIsTrue(result)) {\n\t\t\t\t\t_list.erase(\n\t\t\t\t\t\tranges::remove(_list, hash, &Entry::hash),\n\t\t\t\t\t\tend(_list));\n\t\t\t\t\t_listChanges.fire({});\n\t\t\t\t}\n\t\t\t\tremoveFromUnreviewed();\n\t\t\t}).fail([=] {\n\t\t\t\tremoveFromUnreviewed();\n\t\t\t}).send();\n\t\t\t_reviewRequests.emplace(hash, id);\n\t\t}\n\t}\n}\n\nrpl::producer<> Authorizations::unreviewedChanges() const {\n\treturn _unreviewedChanges.events();\n}\n\nvoid Authorizations::apply(const MTPUpdate &update) {\n\tremoveExpiredUnreviewed();\n\tupdate.match([&](const MTPDupdateNewAuthorization &data) {\n\t\tauto unreviewed = Data::UnreviewedAuth{\n\t\t\t.hash = data.vhash().v,\n\t\t\t.unconfirmed = data.is_unconfirmed(),\n\t\t\t.date = data.vdate().value_or_empty(),\n\t\t\t.device = qs(data.vdevice().value_or_empty()),\n\t\t\t.location = qs(data.vlocation().value_or_empty())\n\t\t};\n\t\tif (!unreviewed.unconfirmed) {\n\t\t\tconst auto hash = unreviewed.hash;\n\t\t\tconst auto was = _unreviewed.size();\n\t\t\t_unreviewed.erase(\n\t\t\t\tstd::remove_if(\n\t\t\t\t\t_unreviewed.begin(),\n\t\t\t\t\t_unreviewed.end(),\n\t\t\t\t\t[hash](const auto &auth) { return auth.hash == hash; }),\n\t\t\t\t_unreviewed.end());\n\t\t\tif (was != _unreviewed.size()) {\n\t\t\t\t_saveUnreviewed();\n\t\t\t\t_unreviewedChanges.fire({});\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tfor (auto &auth : _unreviewed) {\n\t\t\tif (auth.hash == unreviewed.hash) {\n\t\t\t\tauth = std::move(unreviewed);\n\t\t\t\t_saveUnreviewed();\n\t\t\t\t_unreviewedChanges.fire({});\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t_unreviewed.push_back(std::move(unreviewed));\n\t\t_saveUnreviewed();\n\t\t_unreviewedChanges.fire({});\n\t}, [](auto&&) {\n\t\tUnexpected(\"Update in Authorizations::apply.\");\n\t});\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_authorizations.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_authorization.h\"\n#include \"mtproto/sender.h\"\n\nclass ApiWrap;\n\nnamespace Api {\n\nclass Authorizations final {\npublic:\n\texplicit Authorizations(not_null<ApiWrap*> api);\n\n\tstruct Entry {\n\t\tuint64 hash = 0;\n\n\t\tbool incomplete = false;\n\t\tbool callsDisabled = false;\n\t\tint apiId = 0;\n\t\tTimeId activeTime = 0;\n\t\tQString name, active, info, ip, location, system, platform;\n\t};\n\tusing List = std::vector<Entry>;\n\n\tvoid reload();\n\tvoid cancelCurrentRequest();\n\tvoid requestTerminate(\n\t\tFn<void(const MTPBool &result)> &&done,\n\t\tFn<void(const MTP::Error &error)> &&fail,\n\t\tstd::optional<uint64> hash = std::nullopt);\n\n\tvoid apply(const MTPUpdate &update);\n\n\t[[nodiscard]] crl::time lastReceivedTime();\n\n\t[[nodiscard]] List list() const;\n\t[[nodiscard]] rpl::producer<List> listValue() const;\n\t[[nodiscard]] int total() const;\n\t[[nodiscard]] rpl::producer<int> totalValue() const;\n\n\t[[nodiscard]] const std::vector<Data::UnreviewedAuth> &unreviewed();\n\t[[nodiscard]] rpl::producer<> unreviewedChanges() const;\n\n\tvoid review(const std::vector<uint64> &hashes, bool confirm);\n\n\tvoid updateTTL(int days);\n\t[[nodiscard]] rpl::producer<int> ttlDays() const;\n\n\tvoid toggleCallsDisabledHere(bool disabled) {\n\t\ttoggleCallsDisabled(0, disabled);\n\t}\n\tvoid toggleCallsDisabled(uint64 hash, bool disabled);\n\t[[nodiscard]] bool callsDisabledHere() const;\n\t[[nodiscard]] rpl::producer<bool> callsDisabledHereValue() const;\n\t[[nodiscard]] rpl::producer<bool> callsDisabledHereChanges() const;\n\n\t[[nodiscard]] static QString ActiveDateString(TimeId active);\n\nprivate:\n\tvoid refreshCallsDisabledHereFromCloud();\n\tvoid removeExpiredUnreviewed();\n\n\tMTP::Sender _api;\n\tmtpRequestId _requestId = 0;\n\n\tList _list;\n\trpl::event_stream<> _listChanges;\n\n\tFn<int()> _autoconfirmPeriod;\n\tstd::vector<Data::UnreviewedAuth> _unreviewed;\n\trpl::event_stream<> _unreviewedChanges;\n\tFn<void()> _saveUnreviewed;\n\n\tmtpRequestId _ttlRequestId = 0;\n\trpl::variable<int> _ttlDays = 0;\n\n\tbase::flat_map<uint64, mtpRequestId> _toggleCallsDisabledRequests;\n\tbase::flat_map<uint64, mtpRequestId> _reviewRequests;\n\trpl::variable<bool> _callsDisabledHere;\n\n\tcrl::time _lastReceived = 0;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_blocked_peers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_blocked_peers.h\"\n\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_peer_id.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kBlockedFirstSlice = 16;\nconstexpr auto kBlockedPerPage = 40;\n\nBlockedPeers::Slice TLToSlice(\n\t\tconst MTPcontacts_Blocked &blocked,\n\t\tData::Session &owner) {\n\tconst auto create = [&](int count, const QVector<MTPPeerBlocked> &list) {\n\t\tauto slice = BlockedPeers::Slice();\n\t\tslice.total = std::max(count, int(list.size()));\n\t\tslice.list.reserve(list.size());\n\t\tfor (const auto &contact : list) {\n\t\t\tcontact.match([&](const MTPDpeerBlocked &data) {\n\t\t\t\tslice.list.push_back({\n\t\t\t\t\t.id = peerFromMTP(data.vpeer_id()),\n\t\t\t\t\t.date = data.vdate().v,\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t\treturn slice;\n\t};\n\treturn blocked.match([&](const MTPDcontacts_blockedSlice &data) {\n\t\towner.processUsers(data.vusers());\n\t\towner.processChats(data.vchats());\n\t\treturn create(data.vcount().v, data.vblocked().v);\n\t}, [&](const MTPDcontacts_blocked &data) {\n\t\towner.processUsers(data.vusers());\n\t\towner.processChats(data.vchats());\n\t\treturn create(0, data.vblocked().v);\n\t});\n}\n\n} // namespace\n\nBlockedPeers::BlockedPeers(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance()) {\n}\n\nbool BlockedPeers::Slice::Item::operator==(const Item &other) const {\n\treturn (id == other.id) && (date == other.date);\n}\n\nbool BlockedPeers::Slice::Item::operator!=(const Item &other) const {\n\treturn !(*this == other);\n}\n\nbool BlockedPeers::Slice::operator==(const BlockedPeers::Slice &other) const {\n\treturn (total == other.total) && (list == other.list);\n}\n\nbool BlockedPeers::Slice::operator!=(const BlockedPeers::Slice &other) const {\n\treturn !(*this == other);\n}\n\nvoid BlockedPeers::block(not_null<PeerData*> peer) {\n\tif (peer->isBlocked()) {\n\t\t_session->changes().peerUpdated(\n\t\t\tpeer,\n\t\t\tData::PeerUpdate::Flag::IsBlocked);\n\t\treturn;\n\t} else if (blockAlreadySent(peer, true)) {\n\t\treturn;\n\t}\n\tconst auto requestId = _api.request(MTPcontacts_Block(\n\t\tMTP_flags(0),\n\t\tpeer->input()\n\t)).done([=] {\n\t\tconst auto data = _blockRequests.take(peer);\n\t\tpeer->setIsBlocked(true);\n\t\tif (_slice) {\n\t\t\t_slice->list.insert(\n\t\t\t\t_slice->list.begin(),\n\t\t\t\t{ peer->id, base::unixtime::now() });\n\t\t\t++_slice->total;\n\t\t\t_changes.fire_copy(*_slice);\n\t\t}\n\t\tif (data) {\n\t\t\tfor (const auto &callback : data->callbacks) {\n\t\t\t\tcallback(false);\n\t\t\t}\n\t\t}\n\t}).fail([=] {\n\t\tif (const auto data = _blockRequests.take(peer)) {\n\t\t\tfor (const auto &callback : data->callbacks) {\n\t\t\t\tcallback(false);\n\t\t\t}\n\t\t}\n\t}).send();\n\n\t_blockRequests.emplace(peer, Request{\n\t\t.requestId = requestId,\n\t\t.blocking = true,\n\t});\n}\n\nvoid BlockedPeers::unblock(\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void(bool success)> done,\n\t\tbool force) {\n\tif (!force && !peer->isBlocked()) {\n\t\t_session->changes().peerUpdated(\n\t\t\tpeer,\n\t\t\tData::PeerUpdate::Flag::IsBlocked);\n\t\treturn;\n\t} else if (blockAlreadySent(peer, false, done)) {\n\t\treturn;\n\t}\n\tconst auto requestId = _api.request(MTPcontacts_Unblock(\n\t\tMTP_flags(0),\n\t\tpeer->input()\n\t)).done([=] {\n\t\tconst auto data = _blockRequests.take(peer);\n\t\tpeer->setIsBlocked(false);\n\t\tif (_slice) {\n\t\t\tauto &list = _slice->list;\n\t\t\tfor (auto i = list.begin(); i != list.end(); ++i) {\n\t\t\t\tif (i->id == peer->id) {\n\t\t\t\t\tlist.erase(i);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_slice->total > list.size()) {\n\t\t\t\t--_slice->total;\n\t\t\t}\n\t\t\t_changes.fire_copy(*_slice);\n\t\t}\n\t\tif (data) {\n\t\t\tfor (const auto &callback : data->callbacks) {\n\t\t\t\tcallback(true);\n\t\t\t}\n\t\t}\n\t}).fail([=] {\n\t\tif (const auto data = _blockRequests.take(peer)) {\n\t\t\tfor (const auto &callback : data->callbacks) {\n\t\t\t\tcallback(false);\n\t\t\t}\n\t\t}\n\t}).send();\n\tconst auto i = _blockRequests.emplace(peer, Request{\n\t\t.requestId = requestId,\n\t\t.blocking = false,\n\t}).first;\n\tif (done) {\n\t\ti->second.callbacks.push_back(std::move(done));\n\t}\n}\n\nbool BlockedPeers::blockAlreadySent(\n\t\tnot_null<PeerData*> peer,\n\t\tbool blocking,\n\t\tFn<void(bool success)> done) {\n\tconst auto i = _blockRequests.find(peer);\n\tif (i == end(_blockRequests)) {\n\t\treturn false;\n\t} else if (i->second.blocking == blocking) {\n\t\tif (done) {\n\t\t\ti->second.callbacks.push_back(std::move(done));\n\t\t}\n\t\treturn true;\n\t}\n\tconst auto callbacks = base::take(i->second.callbacks);\n\t_blockRequests.erase(i);\n\tfor (const auto &callback : callbacks) {\n\t\tcallback(false);\n\t}\n\treturn false;\n}\n\nvoid BlockedPeers::reload() {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\trequest(0, [=](Slice &&slice) {\n\t\tif (!_slice || *_slice != slice) {\n\t\t\t_slice = slice;\n\t\t\t_changes.fire(std::move(slice));\n\t\t}\n\t});\n}\n\nauto BlockedPeers::slice() -> rpl::producer<BlockedPeers::Slice> {\n\tif (!_slice) {\n\t\treload();\n\t}\n\treturn _slice\n\t\t? _changes.events_starting_with_copy(*_slice)\n\t\t: (_changes.events() | rpl::type_erased);\n}\n\nvoid BlockedPeers::request(int offset, Fn<void(BlockedPeers::Slice)> done) {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\t_requestId = _api.request(MTPcontacts_GetBlocked(\n\t\tMTP_flags(0),\n\t\tMTP_int(offset),\n\t\tMTP_int(offset ? kBlockedPerPage : kBlockedFirstSlice)\n\t)).done([=](const MTPcontacts_Blocked &result) {\n\t\t_requestId = 0;\n\t\tdone(TLToSlice(result, _session->data()));\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t}).send();\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_blocked_peers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n\nclass ApiWrap;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nclass BlockedPeers final {\npublic:\n\tstruct Slice {\n\t\tstruct Item {\n\t\t\tPeerId id;\n\t\t\tTimeId date = 0;\n\n\t\t\tbool operator==(const Item &other) const;\n\t\t\tbool operator!=(const Item &other) const;\n\t\t};\n\n\t\tQVector<Item> list;\n\t\tint total = 0;\n\n\t\tbool operator==(const Slice &other) const;\n\t\tbool operator!=(const Slice &other) const;\n\t};\n\n\texplicit BlockedPeers(not_null<ApiWrap*> api);\n\n\tvoid reload();\n\trpl::producer<Slice> slice();\n\tvoid request(int offset, Fn<void(Slice)> done);\n\n\tvoid block(not_null<PeerData*> peer);\n\tvoid unblock(\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void(bool success)> done = nullptr,\n\t\tbool force = false);\n\nprivate:\n\tstruct Request {\n\t\tstd::vector<Fn<void(bool success)>> callbacks;\n\t\tmtpRequestId requestId = 0;\n\t\tbool blocking = false;\n\t};\n\n\t[[nodiscard]] bool blockAlreadySent(\n\t\tnot_null<PeerData*> peer,\n\t\tbool blocking,\n\t\tFn<void(bool success)> done = nullptr);\n\n\tconst not_null<Main::Session*> _session;\n\n\tMTP::Sender _api;\n\n\tbase::flat_map<not_null<PeerData*>, Request> _blockRequests;\n\tmtpRequestId _requestId = 0;\n\tstd::optional<Slice> _slice;\n\trpl::event_stream<Slice> _changes;\n\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_bot.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_bot.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_cloud_password.h\"\n#include \"api/api_send_progress.h\"\n#include \"api/api_suggest_post.h\"\n#include \"boxes/peers/choose_peer_box.h\"\n#include \"boxes/peers/create_managed_bot_box.h\"\n#include \"boxes/passcode_box.h\"\n#include \"boxes/share_box.h\"\n#include \"boxes/url_auth_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"chat_helpers/bot_command.h\"\n#include \"core/core_cloud_password.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n#include \"payments/payments_checkout_process.h\"\n#include \"payments/payments_non_panel_process.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_peer_menu.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"styles/style_chat.h\"\n\n#include <QtCore/QDataStream>\n#include <QtGui/QGuiApplication>\n#include <QtGui/QClipboard>\n\nnamespace Api {\nnamespace {\n\nvoid SendBotCallbackData(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item,\n\t\tint row,\n\t\tint column,\n\t\tstd::optional<Core::CloudPasswordResult> password,\n\t\tFn<void()> done = nullptr,\n\t\tFn<void(const QString &)> handleError = nullptr) {\n\tif (!item->isRegular()) {\n\t\treturn;\n\t}\n\tconst auto history = item->history();\n\tconst auto session = &history->session();\n\tconst auto owner = &history->owner();\n\tconst auto api = &session->api();\n\tconst auto bot = item->getMessageBot();\n\tconst auto fullId = item->fullId();\n\tconst auto getButton = [=] {\n\t\treturn HistoryMessageMarkupButton::Get(owner, fullId, row, column);\n\t};\n\tconst auto button = getButton();\n\tif (!button || button->requestId) {\n\t\treturn;\n\t}\n\n\tusing ButtonType = HistoryMessageMarkupButton::Type;\n\tconst auto isGame = (button->type == ButtonType::Game);\n\n\tauto flags = MTPmessages_GetBotCallbackAnswer::Flags(0);\n\tQByteArray sendData;\n\tif (isGame) {\n\t\tflags |= MTPmessages_GetBotCallbackAnswer::Flag::f_game;\n\t} else if (button->type == ButtonType::Callback\n\t\t|| button->type == ButtonType::CallbackWithPassword) {\n\t\tflags |= MTPmessages_GetBotCallbackAnswer::Flag::f_data;\n\t\tsendData = button->data;\n\t}\n\tconst auto withPassword = password.has_value();\n\tif (withPassword) {\n\t\tflags |= MTPmessages_GetBotCallbackAnswer::Flag::f_password;\n\t}\n\tconst auto weak = base::make_weak(controller);\n\tconst auto show = controller->uiShow();\n\tbutton->requestId = api->request(MTPmessages_GetBotCallbackAnswer(\n\t\tMTP_flags(flags),\n\t\thistory->peer->input(),\n\t\tMTP_int(item->id),\n\t\tMTP_bytes(sendData),\n\t\tpassword ? password->result : MTP_inputCheckPasswordEmpty()\n\t)).done([=](const MTPmessages_BotCallbackAnswer &result) {\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\tif (done) {\n\t\t\t\tdone();\n\t\t\t}\n\t\t});\n\t\tconst auto item = owner->message(fullId);\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\t\tif (const auto button = getButton()) {\n\t\t\tbutton->requestId = 0;\n\t\t\towner->requestItemRepaint(item);\n\t\t}\n\t\tconst auto &data = result.data();\n\t\tconst auto message = data.vmessage()\n\t\t\t? qs(*data.vmessage())\n\t\t\t: QString();\n\t\tconst auto link = data.vurl() ? qs(*data.vurl()) : QString();\n\t\tconst auto showAlert = data.is_alert();\n\n\t\tif (!message.isEmpty()) {\n\t\t\tif (!show->valid()) {\n\t\t\t\treturn;\n\t\t\t} else if (showAlert) {\n\t\t\t\tshow->showBox(Ui::MakeInformBox(message));\n\t\t\t} else {\n\t\t\t\tif (withPassword) {\n\t\t\t\t\tshow->hideLayer();\n\t\t\t\t}\n\t\t\t\tshow->showToast(message);\n\t\t\t}\n\t\t} else if (!link.isEmpty()) {\n\t\t\tif (!isGame) {\n\t\t\t\tUrlClickHandler::Open(link);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tBotGameUrlClickHandler(bot, link).onClick({\n\t\t\t\tQt::LeftButton,\n\t\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t\t.itemId = item->fullId(),\n\t\t\t\t\t.sessionWindow = weak,\n\t\t\t\t}),\n\t\t\t});\n\t\t\tsession->sendProgressManager().update(\n\t\t\t\thistory,\n\t\t\t\tApi::SendProgressType::PlayGame);\n\t\t} else if (withPassword) {\n\t\t\tshow->hideLayer();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\tif (handleError) {\n\t\t\t\thandleError(error.type());\n\t\t\t}\n\t\t});\n\t\tconst auto item = owner->message(fullId);\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\t\t// Show error?\n\t\tif (const auto button = getButton()) {\n\t\t\tbutton->requestId = 0;\n\t\t\towner->requestItemRepaint(item);\n\t\t}\n\t}).send();\n\n\tsession->changes().messageUpdated(\n\t\titem,\n\t\tData::MessageUpdate::Flag::BotCallbackSent\n\t);\n}\n\nvoid HideSingleUseKeyboard(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item) {\n\tcontroller->content()->hideSingleUseKeyboard(item->fullId());\n}\n\n} // namespace\n\nvoid SendBotCallbackData(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item,\n\t\tint row,\n\t\tint column) {\n\tSendBotCallbackData(controller, item, row, column, std::nullopt);\n}\n\nvoid SendBotCallbackDataWithPassword(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item,\n\t\tint row,\n\t\tint column) {\n\tif (!item->isRegular()) {\n\t\treturn;\n\t}\n\tconst auto history = item->history();\n\tconst auto session = &history->session();\n\tconst auto owner = &history->owner();\n\tconst auto api = &session->api();\n\tconst auto fullId = item->fullId();\n\tconst auto getButton = [=] {\n\t\treturn HistoryMessageMarkupButton::Get(\n\t\t\towner,\n\t\t\tfullId,\n\t\t\trow,\n\t\t\tcolumn);\n\t};\n\tconst auto button = getButton();\n\tif (!button || button->requestId) {\n\t\treturn;\n\t}\n\tapi->cloudPassword().reload();\n\tconst auto weak = base::make_weak(controller);\n\tconst auto show = controller->uiShow();\n\tSendBotCallbackData(controller, item, row, column, {}, {}, [=](\n\t\t\tconst QString &error) {\n\t\tauto box = PrePasswordErrorBox(\n\t\t\terror,\n\t\t\tsession,\n\t\t\ttr::lng_bots_password_confirm_check_about(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked));\n\t\tif (box) {\n\t\t\tshow->showBox(std::move(box), Ui::LayerOption::CloseOther);\n\t\t} else {\n\t\t\tauto lifetime = std::make_shared<rpl::lifetime>();\n\t\t\tbutton->requestId = -1;\n\t\t\tapi->cloudPassword().state(\n\t\t\t) | rpl::take(\n\t\t\t\t1\n\t\t\t) | rpl::on_next([=](const Core::CloudPasswordState &state) mutable {\n\t\t\t\tif (lifetime) {\n\t\t\t\t\tbase::take(lifetime)->destroy();\n\t\t\t\t}\n\t\t\t\tif (const auto button = getButton()) {\n\t\t\t\t\tif (button->requestId == -1) {\n\t\t\t\t\t\tbutton->requestId = 0;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tauto fields = PasscodeBox::CloudFields::From(state);\n\t\t\t\tfields.customTitle = tr::lng_bots_password_confirm_title();\n\t\t\t\tfields.customDescription\n\t\t\t\t\t= tr::lng_bots_password_confirm_description(tr::now);\n\t\t\t\tfields.customSubmitButton = tr::lng_passcode_submit();\n\t\t\t\tfields.customCheckCallback = [=](\n\t\t\t\t\t\tconst Core::CloudPasswordResult &result,\n\t\t\t\t\t\tbase::weak_qptr<PasscodeBox> box) {\n\t\t\t\t\tif (const auto button = getButton()) {\n\t\t\t\t\t\tif (button->requestId) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (const auto item = owner->message(fullId)) {\n\t\t\t\t\t\tconst auto strongController = weak.get();\n\t\t\t\t\t\tif (!strongController) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tSendBotCallbackData(strongController, item, row, column, result, [=] {\n\t\t\t\t\t\t\tif (box) {\n\t\t\t\t\t\t\t\tbox->closeBox();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}, [=](const QString &error) {\n\t\t\t\t\t\t\tif (box) {\n\t\t\t\t\t\t\t\tbox->handleCustomCheckError(error);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tauto object = Box<PasscodeBox>(session, fields);\n\t\t\t\tshow->showBox(std::move(object), Ui::LayerOption::CloseOther);\n\t\t\t}, *lifetime);\n\t\t}\n\t});\n}\n\nbool SwitchInlineBotButtonReceived(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QByteArray &queryWithPeerTypes,\n\t\tUserData *samePeerBot,\n\t\tMsgId samePeerReplyTo) {\n\treturn controller->content()->notify_switchInlineBotButtonReceived(\n\t\tQString::fromUtf8(queryWithPeerTypes),\n\t\tsamePeerBot,\n\t\tsamePeerReplyTo);\n}\n\nvoid ActivateBotCommand(ClickHandlerContext context, int row, int column) {\n\tconst auto strong = context.sessionWindow.get();\n\tif (!strong) {\n\t\treturn;\n\t}\n\tconst auto controller = not_null{ strong };\n\tconst auto item = controller->session().data().message(context.itemId);\n\tif (!item) {\n\t\treturn;\n\t}\n\tconst auto button = HistoryMessageMarkupButton::Get(\n\t\t&item->history()->owner(),\n\t\titem->fullId(),\n\t\trow,\n\t\tcolumn);\n\tif (!button) {\n\t\treturn;\n\t}\n\n\tusing ButtonType = HistoryMessageMarkupButton::Type;\n\tswitch (button->type) {\n\tcase ButtonType::Default: {\n\t\t// Copy string before passing it to the sending method\n\t\t// because the original button can be destroyed inside.\n\t\tconst auto replyTo = item->isRegular()\n\t\t\t? item->fullId()\n\t\t\t: FullMsgId();\n\t\tcontroller->content()->sendBotCommand({\n\t\t\t.peer = item->history()->peer,\n\t\t\t.command = QString(button->text),\n\t\t\t.context = item->fullId(),\n\t\t\t.replyTo = { replyTo },\n\t\t});\n\t} break;\n\n\tcase ButtonType::Callback:\n\tcase ButtonType::Game: {\n\t\tSendBotCallbackData(controller, item, row, column);\n\t} break;\n\n\tcase ButtonType::CallbackWithPassword: {\n\t\tSendBotCallbackDataWithPassword(controller, item, row, column);\n\t} break;\n\n\tcase ButtonType::Buy: {\n\t\tPayments::CheckoutProcess::Start(\n\t\t\titem,\n\t\t\tPayments::Mode::Payment,\n\t\t\tcrl::guard(controller, [=](auto) {\n\t\t\t\tcontroller->widget()->activate();\n\t\t\t}),\n\t\t\tPayments::ProcessNonPanelPaymentFormFactory(controller, item));\n\t} break;\n\n\tcase ButtonType::Url: {\n\t\tauto url = QString::fromUtf8(button->data);\n\t\tauto skipConfirmation = false;\n\t\tif (const auto bot = item->getMessageBot()) {\n\t\t\tif (bot->isVerified()) {\n\t\t\t\tskipConfirmation = true;\n\t\t\t}\n\t\t}\n\t\tconst auto variant = QVariant::fromValue(context);\n\t\tif (skipConfirmation) {\n\t\t\tUrlClickHandler::Open(url, variant);\n\t\t} else {\n\t\t\tHiddenUrlClickHandler::Open(url, variant);\n\t\t}\n\t} break;\n\n\tcase ButtonType::RequestLocation: {\n\t\tHideSingleUseKeyboard(controller, item);\n\t\tcontroller->show(\n\t\t\tUi::MakeInformBox(tr::lng_bot_share_location_unavailable()));\n\t} break;\n\n\tcase ButtonType::RequestPhone: {\n\t\tHideSingleUseKeyboard(controller, item);\n\t\tconst auto itemId = item->fullId();\n\t\tconst auto topicRootId = item->topicRootId();\n\t\tconst auto history = item->history();\n\t\tcontroller->show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_bot_share_phone(),\n\t\t\t.confirmed = [=] {\n\t\t\t\tcontroller->showPeerHistory(\n\t\t\t\t\thistory,\n\t\t\t\t\tWindow::SectionShow::Way::Forward,\n\t\t\t\t\tShowAtTheEndMsgId);\n\t\t\t\tauto action = Api::SendAction(history);\n\t\t\t\taction.clearDraft = false;\n\t\t\t\taction.replyTo = {\n\t\t\t\t\t.messageId = itemId,\n\t\t\t\t\t.topicRootId = topicRootId,\n\t\t\t\t};\n\t\t\t\thistory->session().api().shareContact(\n\t\t\t\t\thistory->session().user(),\n\t\t\t\t\taction);\n\t\t\t},\n\t\t\t.confirmText = tr::lng_bot_share_phone_confirm(),\n\t\t}));\n\t} break;\n\n\tcase ButtonType::RequestPoll: {\n\t\tHideSingleUseKeyboard(controller, item);\n\t\tauto chosen = kDefaultPollCreateFlags;\n\t\tauto disabled = PollData::Flags();\n\t\tif (!button->data.isEmpty()) {\n\t\t\tdisabled |= PollData::Flag::Quiz;\n\t\t\tif (button->data[0]) {\n\t\t\t\tchosen |= PollData::Flag::Quiz;\n\t\t\t}\n\t\t}\n\t\tconst auto replyTo = FullReplyTo();\n\t\tconst auto suggest = SuggestOptions();\n\t\tWindow::PeerMenuCreatePoll(\n\t\t\tcontroller,\n\t\t\titem->history()->peer,\n\t\t\treplyTo,\n\t\t\tsuggest,\n\t\t\tchosen,\n\t\t\tdisabled);\n\t} break;\n\n\tcase ButtonType::RequestPeer: {\n\t\tHideSingleUseKeyboard(controller, item);\n\n\t\tauto query = RequestPeerQuery();\n\t\tAssert(button->data.size() == sizeof(query));\n\t\tmemcpy(&query, button->data.data(), sizeof(query));\n\t\tconst auto peer = item->history()->peer;\n\t\tconst auto itemId = item->id;\n\t\tconst auto id = int32(button->buttonId);\n\t\tconst auto chosen = [=](std::vector<not_null<PeerData*>> result) {\n\t\t\tusing Flag = MTPmessages_SendBotRequestedPeer::Flag;\n\t\t\tpeer->session().api().request(MTPmessages_SendBotRequestedPeer(\n\t\t\t\tMTP_flags(Flag::f_msg_id),\n\t\t\t\tpeer->input(),\n\t\t\t\tMTP_int(itemId),\n\t\t\t\tMTPstring(), // request_id\n\t\t\t\tMTP_int(id),\n\t\t\t\tMTP_vector_from_range(\n\t\t\t\t\tresult | ranges::views::transform([](\n\t\t\t\t\t\t\tnot_null<PeerData*> peer) {\n\t\t\t\t\t\treturn MTPInputPeer(peer->input());\n\t\t\t\t\t}))\n\t\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t\tpeer->session().api().applyUpdates(result);\n\t\t\t}).send();\n\t\t};\n\t\tif (const auto bot = item->getMessageBot()) {\n\t\t\tShowChoosePeerBox(controller, bot, query, chosen);\n\t\t} else {\n\t\t\tLOG((\"API Error: Bot not found for RequestPeer button.\"));\n\t\t}\n\t} break;\n\n\tcase ButtonType::SwitchInlineSame:\n\tcase ButtonType::SwitchInline: {\n\t\tif (const auto bot = item->getMessageBot()) {\n\t\t\tconst auto fastSwitchDone = [&] {\n\t\t\t\tconst auto samePeer = (button->type\n\t\t\t\t\t== ButtonType::SwitchInlineSame);\n\t\t\t\tif (samePeer) {\n\t\t\t\t\tSwitchInlineBotButtonReceived(\n\t\t\t\t\t\tcontroller,\n\t\t\t\t\t\tbutton->data,\n\t\t\t\t\t\tbot,\n\t\t\t\t\t\titem->id);\n\t\t\t\t\treturn true;\n\t\t\t\t} else if (bot->isBot() && bot->botInfo->inlineReturnTo.key) {\n\t\t\t\t\tconst auto switched = SwitchInlineBotButtonReceived(\n\t\t\t\t\t\tcontroller,\n\t\t\t\t\t\tbutton->data);\n\t\t\t\t\tif (switched) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}();\n\t\t\tif (!fastSwitchDone) {\n\t\t\t\tconst auto query = QString::fromUtf8(button->data);\n\t\t\t\tconst auto chosen = [=](not_null<Data::Thread*> thread) {\n\t\t\t\t\treturn controller->switchInlineQuery(\n\t\t\t\t\t\tthread,\n\t\t\t\t\t\tbot,\n\t\t\t\t\t\tquery);\n\t\t\t\t};\n\t\t\t\tWindow::ShowChooseRecipientBox(\n\t\t\t\t\tcontroller,\n\t\t\t\t\tchosen,\n\t\t\t\t\ttr::lng_inline_switch_choose(),\n\t\t\t\t\tnullptr,\n\t\t\t\t\tbutton->peerTypes);\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase ButtonType::Auth:\n\t\tUrlAuthBox::ActivateButton(controller->uiShow(), item, row, column);\n\t\tbreak;\n\n\tcase ButtonType::UserProfile: {\n\t\tconst auto session = &item->history()->session();\n\t\tconst auto userId = UserId(button->data.toULongLong());\n\t\tif (const auto user = session->data().userLoaded(userId)) {\n\t\t\tcontroller->showPeerInfo(user);\n\t\t}\n\t} break;\n\n\tcase ButtonType::WebView: {\n\t\tif (const auto bot = item->getMessageBot()) {\n\t\t\tbot->session().attachWebView().open({\n\t\t\t\t.bot = bot,\n\t\t\t\t.context = { .controller = controller },\n\t\t\t\t.button = { .text = button->text, .url = button->data },\n\t\t\t\t.source = InlineBots::WebViewSourceButton{ .simple = false },\n\t\t\t});\n\t\t}\n\t} break;\n\n\tcase ButtonType::SimpleWebView: {\n\t\tif (const auto bot = item->getMessageBot()) {\n\t\t\tbot->session().attachWebView().open({\n\t\t\t\t.bot = bot,\n\t\t\t\t.context = { .controller = controller },\n\t\t\t\t.button = { .text = button->text, .url = button->data },\n\t\t\t\t.source = InlineBots::WebViewSourceButton{ .simple = true },\n\t\t\t});\n\t\t}\n\t} break;\n\n\tcase ButtonType::CopyText: {\n\t\tconst auto text = QString::fromUtf8(button->data);\n\t\tif (!text.isEmpty()) {\n\t\t\tQGuiApplication::clipboard()->setText(text);\n\t\t\tcontroller->showToast(tr::lng_text_copied(tr::now));\n\t\t}\n\t} break;\n\n\tcase ButtonType::SuggestAccept: {\n\t\tApi::AcceptClickHandler(item)->onClick(ClickContext{\n\t\t\tQt::LeftButton,\n\t\t\tQVariant::fromValue(context),\n\t\t});\n\t} break;\n\n\tcase ButtonType::SuggestDecline: {\n\t\tApi::DeclineClickHandler(item)->onClick(ClickContext{\n\t\t\tQt::LeftButton,\n\t\t\tQVariant::fromValue(context),\n\t\t});\n\t} break;\n\n\tcase ButtonType::SuggestChange: {\n\t\tApi::SuggestChangesClickHandler(item)->onClick(ClickContext{\n\t\t\tQt::LeftButton,\n\t\t\tQVariant::fromValue(context),\n\t\t});\n\t} break;\n\n\tcase ButtonType::CreateBot: {\n\t\tHideSingleUseKeyboard(controller, item);\n\n\t\tauto suggestedName = QString();\n\t\tauto suggestedUsername = QString();\n\t\t{\n\t\t\tauto stream = QDataStream(button->data);\n\t\t\tstream >> suggestedName >> suggestedUsername;\n\t\t}\n\t\tconst auto peer = item->history()->peer;\n\t\tconst auto itemId = item->id;\n\t\tconst auto id = int32(button->buttonId);\n\t\tconst auto bot = item->getMessageBot();\n\t\tif (!bot) {\n\t\t\tbreak;\n\t\t}\n\t\tShowCreateManagedBotBox({\n\t\t\t.show = controller->uiShow(),\n\t\t\t.manager = bot,\n\t\t\t.suggestedName = suggestedName,\n\t\t\t.suggestedUsername = suggestedUsername,\n\t\t\t.done = [=](not_null<UserData*> createdBot) {\n\t\t\t\tusing Flag = MTPmessages_SendBotRequestedPeer::Flag;\n\t\t\t\tpeer->session().api().request(\n\t\t\t\t\tMTPmessages_SendBotRequestedPeer(\n\t\t\t\t\t\tMTP_flags(Flag::f_msg_id),\n\t\t\t\t\t\tpeer->input(),\n\t\t\t\t\t\tMTP_int(itemId),\n\t\t\t\t\t\tMTPstring(),\n\t\t\t\t\t\tMTP_int(id),\n\t\t\t\t\t\tMTP_vector<MTPInputPeer>(\n\t\t\t\t\t\t\t1,\n\t\t\t\t\t\t\tcreatedBot->input()))\n\t\t\t\t).done([=](const MTPUpdates &result) {\n\t\t\t\t\tpeer->session().api().applyUpdates(result);\n\t\t\t\t}).send();\n\t\t\t\tcontroller->showPeerHistory(createdBot);\n\t\t\t\tcontroller->showToast({\n\t\t\t\t\t.title = tr::lng_managed_bot_created_title(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\tcreatedBot->name()),\n\t\t\t\t\t.text = { tr::lng_managed_bot_created_text(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_parent_name,\n\t\t\t\t\t\tbot->name()) },\n\t\t\t\t\t.icon = &st::toastCheckIcon,\n\t\t\t\t});\n\t\t\t},\n\t\t});\n\t} break;\n\t}\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_bot.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nstruct ClickHandlerContext;\nclass HistoryItem;\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Api {\n\nvoid SendBotCallbackData(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<HistoryItem*> item,\n\tint row,\n\tint column);\n\nvoid SendBotCallbackDataWithPassword(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<HistoryItem*> item,\n\tint row,\n\tint column);\n\nbool SwitchInlineBotButtonReceived(\n\tnot_null<Window::SessionController*> controller,\n\tconst QByteArray &queryWithPeerTypes,\n\tUserData *samePeerBot = nullptr,\n\tMsgId samePeerReplyTo = 0);\n\nvoid ActivateBotCommand(ClickHandlerContext context, int row, int column);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_chat_filters.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_chat_filters.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"apiwrap.h\"\n#include \"base/event_filter.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"boxes/filters/edit_filter_links.h\" // FilterChatStatusText\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/filter_link_header.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/filter_icons.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_filter_icons.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Api {\nnamespace {\n\nenum class ToggleAction {\n\tAdding,\n\tRemoving,\n};\n\nclass ToggleChatsController final\n\t: public PeerListController\n\t, public base::has_weak_ptr {\npublic:\n\tToggleChatsController(\n\t\tnot_null<Window::SessionController*> window,\n\t\tToggleAction action,\n\t\tData::ChatFilterTitle title,\n\t\tstd::vector<not_null<PeerData*>> chats,\n\t\tstd::vector<not_null<PeerData*>> additional);\n\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tMain::Session &session() const override;\n\n\t[[nodiscard]] auto selectedValue() const\n\t\t-> rpl::producer<base::flat_set<not_null<PeerData*>>>;\n\n\tvoid adjust(int minHeight, int maxHeight, int addedTopHeight);\n\tvoid setRealContentHeight(rpl::producer<int> value);\n\trpl::producer<int> boxHeightValue() const override;\n\nprivate:\n\tvoid setupAboveWidget();\n\tvoid setupBelowWidget();\n\tvoid initDesiredHeightValue();\n\tvoid toggleAllSelected(bool select);\n\n\tconst not_null<Window::SessionController*> _window;\n\tUi::RpWidget *_addedTopWidget = nullptr;\n\tUi::RpWidget *_addedBottomWidget = nullptr;\n\n\tToggleAction _action = ToggleAction::Adding;\n\tbase::flat_set<not_null<PeerData*>> _checkable;\n\tstd::vector<not_null<PeerData*>> _chats;\n\tstd::vector<not_null<PeerData*>> _additional;\n\trpl::variable<base::flat_set<not_null<PeerData*>>> _selected;\n\n\tint _minTopHeight = 0;\n\trpl::variable<int> _maxTopHeight;\n\trpl::variable<int> _aboveHeight;\n\trpl::variable<int> _belowHeight;\n\trpl::variable<int> _desiredHeight;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] tr::phrase<> TitleText(Ui::FilterLinkHeaderType type) {\n\tusing Type = Ui::FilterLinkHeaderType;\n\tswitch (type) {\n\tcase Type::AddingFilter: return tr::lng_filters_by_link_title;\n\tcase Type::AddingChats: return tr::lng_filters_by_link_more;\n\tcase Type::AllAdded: return tr::lng_filters_by_link_already;\n\tcase Type::Removing: return tr::lng_filters_by_link_remove;\n\t}\n\tUnexpected(\"Ui::FilterLinkHeaderType in TitleText.\");\n}\n\n[[nodiscard]] TextWithEntities AboutText(\n\t\tUi::FilterLinkHeaderType type,\n\t\tTextWithEntities title) {\n\tusing Type = Ui::FilterLinkHeaderType;\n\tauto boldTitle = Ui::Text::Wrapped(title, EntityType::Bold);\n\treturn (type == Type::AddingFilter)\n\t\t? tr::lng_filters_by_link_sure(\n\t\t\ttr::now,\n\t\t\tlt_folder,\n\t\t\tstd::move(boldTitle),\n\t\t\ttr::marked)\n\t\t: (type == Type::AddingChats)\n\t\t? tr::lng_filters_by_link_more_sure(\n\t\t\ttr::now,\n\t\t\tlt_folder,\n\t\t\tstd::move(boldTitle),\n\t\t\ttr::marked)\n\t\t: (type == Type::AllAdded)\n\t\t? tr::lng_filters_by_link_already_about(\n\t\t\ttr::now,\n\t\t\tlt_folder,\n\t\t\tstd::move(boldTitle),\n\t\t\ttr::marked)\n\t\t: tr::lng_filters_by_link_remove_sure(\n\t\t\ttr::now,\n\t\t\tlt_folder,\n\t\t\tstd::move(boldTitle),\n\t\t\ttr::marked);\n}\n\nvoid InitFilterLinkHeader(\n\t\tnot_null<PeerListBox*> box,\n\t\tFn<void(int minHeight, int maxHeight, int addedTopHeight)> adjust,\n\t\tUi::FilterLinkHeaderType type,\n\t\tData::ChatFilterTitle title,\n\t\tQString iconEmoji,\n\t\trpl::producer<int> count,\n\t\tbool horizontalFilters) {\n\tconst auto icon = Ui::LookupFilterIcon(\n\t\tUi::LookupFilterIconByEmoji(\n\t\t\ticonEmoji\n\t\t).value_or(Ui::FilterIcon::Custom)).active;\n\tconst auto isStatic = title.isStatic;\n\tauto header = Ui::MakeFilterLinkHeader(box, {\n\t\t.type = type,\n\t\t.title = TitleText(type)(tr::now),\n\t\t.about = AboutText(type, title.text),\n\t\t.aboutContext = Core::TextContext({\n\t\t\t.session = &box->peerListUiShow()->session(),\n\t\t\t.customEmojiLoopLimit = isStatic ? -1 : 0,\n\t\t}),\n\t\t.folderTitle = title.text,\n\t\t.folderIcon = icon,\n\t\t.badge = (type == Ui::FilterLinkHeaderType::AddingChats\n\t\t\t? std::move(count)\n\t\t\t: rpl::single(0)),\n\t\t.horizontalFilters = horizontalFilters,\n\t});\n\tconst auto widget = header.widget;\n\twidget->resizeToWidth(st::boxWideWidth);\n\tUi::SendPendingMoveResizeEvents(widget);\n\n\tconst auto min = widget->minimumHeight(), max = widget->maximumHeight();\n\twidget->resize(st::boxWideWidth, max);\n\n\tbox->setAddedTopScrollSkip(max);\n\tstd::move(\n\t\theader.wheelEvents\n\t) | rpl::on_next([=](not_null<QWheelEvent*> e) {\n\t\tbox->sendScrollViewportEvent(e);\n\t}, widget->lifetime());\n\n\tstd::move(\n\t\theader.closeRequests\n\t) | rpl::on_next([=] {\n\t\tbox->closeBox();\n\t}, widget->lifetime());\n\n\tstruct State {\n\t\tbool processing = false;\n\t\tint addedTopHeight = 0;\n\t};\n\tconst auto state = widget->lifetime().make_state<State>();\n\n\tbox->scrolls(\n\t) | rpl::filter([=] {\n\t\treturn !state->processing;\n\t}) | rpl::on_next([=] {\n\t\tstate->processing = true;\n\t\tconst auto guard = gsl::finally([&] { state->processing = false; });\n\n\t\tconst auto top = box->scrollTop();\n\t\tconst auto headerHeight = std::max(max - top, min);\n\t\tconst auto addedTopHeight = max - headerHeight;\n\t\twidget->resize(widget->width(), headerHeight);\n\t\tif (state->addedTopHeight < addedTopHeight) {\n\t\t\tadjust(min, max, addedTopHeight);\n\t\t\tbox->setAddedTopScrollSkip(headerHeight);\n\t\t} else {\n\t\t\tbox->setAddedTopScrollSkip(headerHeight);\n\t\t\tadjust(min, max, addedTopHeight);\n\t\t}\n\t\tstate->addedTopHeight = addedTopHeight;\n\t\tbox->peerListRefreshRows();\n\t}, widget->lifetime());\n\n\tbox->setNoContentMargin(true);\n\tadjust(min, max, 0);\n}\n\nvoid ImportInvite(\n\t\tconst QString &slug,\n\t\tFilterId filterId,\n\t\tconst base::flat_set<not_null<PeerData*>> &peers,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail) {\n\tExpects(!peers.empty());\n\n\tconst auto peer = peers.front();\n\tconst auto api = &peer->session().api();\n\tconst auto callback = [=](const MTPUpdates &result) {\n\t\tapi->applyUpdates(result);\n\t\tif (slug.isEmpty()) {\n\t\t\tpeer->owner().chatsFilters().moreChatsHide(filterId, true);\n\t\t}\n\t\tdone();\n\t};\n\tconst auto error = [=](const MTP::Error &error) {\n\t\tfail(error.type());\n\t};\n\tauto inputs = peers | ranges::views::transform([](auto peer) {\n\t\treturn MTPInputPeer(peer->input());\n\t}) | ranges::to<QVector<MTPInputPeer>>();\n\tif (!slug.isEmpty()) {\n\t\tapi->request(MTPchatlists_JoinChatlistInvite(\n\t\t\tMTP_string(slug),\n\t\t\tMTP_vector<MTPInputPeer>(std::move(inputs))\n\t\t)).done(callback).fail(error).handleFloodErrors().send();\n\t} else {\n\t\tapi->request(MTPchatlists_JoinChatlistUpdates(\n\t\t\tMTP_inputChatlistDialogFilter(MTP_int(filterId)),\n\t\t\tMTP_vector<MTPInputPeer>(std::move(inputs))\n\t\t)).done(callback).fail(error).handleFloodErrors().send();\n\t}\n}\n\nToggleChatsController::ToggleChatsController(\n\tnot_null<Window::SessionController*> window,\n\tToggleAction action,\n\tData::ChatFilterTitle title,\n\tstd::vector<not_null<PeerData*>> chats,\n\tstd::vector<not_null<PeerData*>> additional)\n: _window(window)\n, _action(action)\n, _chats(std::move(chats))\n, _additional(std::move(additional)) {\n\tsetStyleOverrides(&st::filterLinkChatsList);\n}\n\nvoid ToggleChatsController::prepare() {\n\tauto selected = base::flat_set<not_null<PeerData*>>();\n\tconst auto disabled = [](not_null<PeerData*> peer) {\n\t\treturn peer->isChat()\n\t\t\t? peer->asChat()->isForbidden()\n\t\t\t: peer->isChannel()\n\t\t\t? peer->asChannel()->isForbidden()\n\t\t\t: false;\n\t};\n\tconst auto add = [&](not_null<PeerData*> peer, bool additional = false) {\n\t\tconst auto disable = disabled(peer);\n\t\tauto row = (additional || !disable)\n\t\t\t? std::make_unique<PeerListRow>(peer)\n\t\t\t: MakeFilterChatRow(\n\t\t\t\tpeer,\n\t\t\t\ttr::lng_filters_link_inaccessible(tr::now),\n\t\t\t\ttrue);\n\t\tif (delegate()->peerListFindRow(peer->id.value)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto raw = row.get();\n\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\tif (!disable\n\t\t\t&& (!additional || _action == ToggleAction::Removing)) {\n\t\t\t_checkable.emplace(peer);\n\t\t\tif (const auto status = FilterChatStatusText(peer)\n\t\t\t\t; !status.isEmpty()) {\n\t\t\t\traw->setCustomStatus(status);\n\t\t\t}\n\t\t}\n\t\tif (disable) {\n\t\t} else if (!additional) {\n\t\t\tdelegate()->peerListSetRowChecked(raw, true);\n\t\t\traw->finishCheckedAnimation();\n\t\t\tselected.emplace(peer);\n\t\t} else if (_action == ToggleAction::Adding) {\n\t\t\traw->setDisabledState(PeerListRow::State::DisabledChecked);\n\t\t\traw->setCustomStatus(peer->isBroadcast()\n\t\t\t\t? tr::lng_filters_link_already_channel(tr::now)\n\t\t\t\t: tr::lng_filters_link_already_group(tr::now));\n\t\t}\n\t};\n\tfor (const auto &peer : _chats) {\n\t\tif (!disabled(peer)) {\n\t\t\tadd(peer);\n\t\t}\n\t}\n\tfor (const auto &peer : _additional) {\n\t\tadd(peer, true);\n\t}\n\tfor (const auto &peer : _chats) {\n\t\tif (disabled(peer)) {\n\t\t\tadd(peer);\n\t\t}\n\t}\n\tsetupAboveWidget();\n\tsetupBelowWidget();\n\tinitDesiredHeightValue();\n\tdelegate()->peerListRefreshRows();\n\t_selected = std::move(selected);\n}\n\nvoid ToggleChatsController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto peer = row->peer();\n\tif (!_checkable.contains(peer)) {\n\t\treturn;\n\t}\n\tconst auto checked = row->checked();\n\tauto selected = _selected.current();\n\tdelegate()->peerListSetRowChecked(row, !checked);\n\tif (checked) {\n\t\tselected.remove(peer);\n\t} else {\n\t\tselected.emplace(peer);\n\t}\n\t_selected = std::move(selected);\n}\n\nvoid ToggleChatsController::setupAboveWidget() {\n\tusing namespace Settings;\n\n\tauto wrap = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tconst auto container = wrap.data();\n\n\t_addedTopWidget = container->add(object_ptr<Ui::RpWidget>(container));\n\tconst auto realAbove = container->add(\n\t\tobject_ptr<Ui::VerticalLayout>(container));\n\tUi::AddDivider(realAbove);\n\tconst auto totalCount = [&] {\n\t\tif (_chats.empty()) {\n\t\t\treturn _additional.size();\n\t\t} else if (_additional.empty()) {\n\t\t\treturn _chats.size();\n\t\t}\n\t\tauto result = _chats.size();\n\t\tfor (const auto &peer : _additional) {\n\t\t\tif (!ranges::contains(_chats, peer)) {\n\t\t\t\t++result;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\tconst auto count = (_action == ToggleAction::Removing)\n\t\t? totalCount()\n\t\t: _chats.empty()\n\t\t? _additional.size()\n\t\t: _chats.size();\n\tconst auto selectableCount = int(_checkable.size());\n\tauto selectedCount = _selected.value(\n\t) | rpl::map([](const base::flat_set<not_null<PeerData*>> &selected) {\n\t\treturn int(selected.size());\n\t});\n\tAddFilterSubtitleWithToggles(\n\t\trealAbove,\n\t\t(_action == ToggleAction::Removing\n\t\t\t? tr::lng_filters_by_link_quit\n\t\t\t: _chats.empty()\n\t\t\t? tr::lng_filters_by_link_in\n\t\t\t: tr::lng_filters_by_link_join)(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(count))),\n\t\tselectableCount,\n\t\tstd::move(selectedCount),\n\t\t[=](bool select) { toggleAllSelected(select); });\n\n\t_aboveHeight = realAbove->heightValue();\n\tdelegate()->peerListSetAboveWidget(std::move(wrap));\n}\n\nvoid ToggleChatsController::toggleAllSelected(bool select) {\n\tauto selected = _selected.current();\n\tif (!select) {\n\t\tif (selected.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const auto &peer : selected) {\n\t\t\tconst auto row = delegate()->peerListFindRow(peer->id.value);\n\t\t\tAssert(row != nullptr);\n\t\t\tdelegate()->peerListSetRowChecked(row, false);\n\t\t}\n\t\tselected = {};\n\t} else {\n\t\tconst auto count = delegate()->peerListFullRowsCount();\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tconst auto row = delegate()->peerListRowAt(i);\n\t\t\tconst auto peer = row->peer();\n\t\t\tif (_action != ToggleAction::Adding ||\n\t\t\t\t!ranges::contains(_additional, peer)) {\n\t\t\t\tdelegate()->peerListSetRowChecked(row, true);\n\t\t\t\tselected.emplace(peer);\n\t\t\t}\n\t\t}\n\t}\n\t_selected = std::move(selected);\n}\n\nvoid ToggleChatsController::setupBelowWidget() {\n\tif (_chats.empty()) {\n\t\tauto widget = object_ptr<Ui::RpWidget>((QWidget*)nullptr);\n\t\t_addedBottomWidget = widget.data();\n\t\tdelegate()->peerListSetBelowWidget(std::move(widget));\n\t\treturn;\n\t}\n\tauto layout = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tconst auto raw = layout.data();\n\tauto widget = object_ptr<Ui::DividerLabel>(\n\t\t(QWidget*)nullptr,\n\t\tstd::move(layout),\n\t\tst::defaultBoxDividerLabelPadding);\n\traw->add(object_ptr<Ui::FlatLabel>(\n\t\traw,\n\t\t(_action == ToggleAction::Removing\n\t\t\t? tr::lng_filters_by_link_about_quit\n\t\t\t: tr::lng_filters_by_link_about)(tr::now),\n\t\tst::boxDividerLabel));\n\t_addedBottomWidget = raw->add(object_ptr<Ui::RpWidget>(raw));\n\t_belowHeight = widget->heightValue() | rpl::map([=](int value) {\n\t\treturn value - _addedBottomWidget->height();\n\t});\n\tdelegate()->peerListSetBelowWidget(std::move(widget));\n}\n\nMain::Session &ToggleChatsController::session() const {\n\treturn _window->session();\n}\n\nauto ToggleChatsController::selectedValue() const\n-> rpl::producer<base::flat_set<not_null<PeerData*>>> {\n\treturn _selected.value();\n}\n\nvoid ToggleChatsController::adjust(\n\t\tint minHeight,\n\t\tint maxHeight,\n\t\tint addedTopHeight) {\n\tExpects(addedTopHeight >= 0);\n\n\t_addedTopWidget->resize(_addedTopWidget->width(), addedTopHeight);\n\t_minTopHeight = minHeight;\n\t_maxTopHeight = maxHeight;\n}\n\nvoid ToggleChatsController::setRealContentHeight(rpl::producer<int> value) {\n\tstd::move(\n\t\tvalue\n\t) | rpl::on_next([=](int height) {\n\t\tconst auto desired = _desiredHeight.current();\n\t\tif (height <= computeListSt().item.height) {\n\t\t\treturn;\n\t\t} else if (height >= desired) {\n\t\t\t_addedBottomWidget->resize(_addedBottomWidget->width(), 0);\n\t\t} else {\n\t\t\tconst auto available = desired - height;\n\t\t\tconst auto required = _maxTopHeight.current() - _minTopHeight;\n\t\t\tconst auto added = required - available;\n\t\t\t_addedBottomWidget->resize(\n\t\t\t\t_addedBottomWidget->width(),\n\t\t\t\tstd::max(added, 0));\n\t\t}\n\t}, _lifetime);\n}\n\nvoid ToggleChatsController::initDesiredHeightValue() {\n\tusing namespace rpl::mappers;\n\n\tconst auto &st = computeListSt();\n\tconst auto count = int(delegate()->peerListFullRowsCount());\n\tconst auto middle = st.padding.top()\n\t\t+ (count * st.item.height)\n\t\t+ st.padding.bottom();\n\t_desiredHeight = rpl::combine(\n\t\t_maxTopHeight.value(),\n\t\t_aboveHeight.value(),\n\t\t_belowHeight.value(),\n\t\t_1 + _2 + middle + _3);\n}\n\nrpl::producer<int> ToggleChatsController::boxHeightValue() const {\n\treturn _desiredHeight.value() | rpl::map([=](int value) {\n\t\treturn std::min(value, st::boxMaxListHeight);\n\t});\n}\n\nvoid ShowImportError(\n\t\tnot_null<Window::SessionController*> window,\n\t\tFilterId id,\n\t\tint added,\n\t\tconst QString &error) {\n\tconst auto session = &window->session();\n\tconst auto &list = session->data().chatsFilters().list();\n\tconst auto i = ranges::find(list, id, &Data::ChatFilter::id);\n\tconst auto count = added\n\t\t+ ((i != end(list)) ? int(i->always().size()) : 0);\n\tif (error == u\"CHANNELS_TOO_MUCH\"_q) {\n\t\twindow->show(Box(ChannelsLimitBox, session));\n\t} else if (error == u\"FILTER_INCLUDE_TOO_MUCH\"_q) {\n\t\twindow->show(Box(FilterChatsLimitBox, session, count, true));\n\t} else if (error == u\"CHATLISTS_TOO_MUCH\"_q) {\n\t\twindow->show(Box(ShareableFiltersLimitBox, session));\n\t} else {\n\t\twindow->showToast((error == u\"INVITE_SLUG_EXPIRED\"_q)\n\t\t\t? tr::lng_group_invite_bad_link(tr::now)\n\t\t\t: error.startsWith(u\"FLOOD_WAIT_\"_q)\n\t\t\t? tr::lng_flood_error(tr::now)\n\t\t\t: error);\n\t}\n}\n\nvoid ShowImportToast(\n\t\tbase::weak_ptr<Window::SessionController> weak,\n\t\tData::ChatFilterTitle title,\n\t\tUi::FilterLinkHeaderType type,\n\t\tint added) {\n\tconst auto strong = weak.get();\n\tif (!strong) {\n\t\treturn;\n\t}\n\tconst auto created = (type == Ui::FilterLinkHeaderType::AddingFilter);\n\tconst auto phrase = created\n\t\t? tr::lng_filters_added_title\n\t\t: tr::lng_filters_updated_title;\n\tauto text = Ui::Text::Wrapped(\n\t\tphrase(tr::now, lt_folder, title.text, tr::marked),\n\t\tEntityType::Bold);\n\tif (added > 0) {\n\t\tconst auto phrase = created\n\t\t\t? tr::lng_filters_added_also\n\t\t\t: tr::lng_filters_updated_also;\n\t\ttext.append('\\n').append(phrase(tr::now, lt_count, added));\n\t}\n\tconst auto isStatic = title.isStatic;\n\tstrong->showToast({\n\t\t.text = std::move(text),\n\t\t.textContext = Core::TextContext({\n\t\t\t.session = &strong->session(),\n\t\t\t.customEmojiLoopLimit = isStatic ? -1 : 0,\n\t\t})\n\t});\n}\n\nvoid HandleEnterInBox(not_null<Ui::BoxContent*> box) {\n\tconst auto isEnter = [=](not_null<QEvent*> event) {\n\t\tif (event->type() == QEvent::KeyPress) {\n\t\t\tif (const auto k = static_cast<QKeyEvent*>(event.get())) {\n\t\t\t\treturn (k->key() == Qt::Key_Enter)\n\t\t\t\t\t|| (k->key() == Qt::Key_Return);\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\n\tbase::install_event_filter(box, [=](not_null<QEvent*> event) {\n\t\tif (isEnter(event)) {\n\t\t\tbox->triggerButton(0);\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n}\n\nvoid ProcessFilterInvite(\n\t\tbase::weak_ptr<Window::SessionController> weak,\n\t\tconst QString &slug,\n\t\tFilterId filterId,\n\t\tData::ChatFilterTitle title,\n\t\tQString iconEmoji,\n\t\tstd::vector<not_null<PeerData*>> peers,\n\t\tstd::vector<not_null<PeerData*>> already) {\n\tconst auto strong = weak.get();\n\tif (!strong) {\n\t\treturn;\n\t}\n\tCore::App().hideMediaView();\n\tif (peers.empty() && !filterId) {\n\t\tstrong->showToast(tr::lng_group_invite_bad_link(tr::now));\n\t\treturn;\n\t}\n\tconst auto fullyAdded = (peers.empty() && filterId);\n\tauto controller = std::make_unique<ToggleChatsController>(\n\t\tstrong,\n\t\tToggleAction::Adding,\n\t\ttitle,\n\t\tstd::move(peers),\n\t\tstd::move(already));\n\tconst auto horizontalFilters = !strong->enoughSpaceForFilters()\n\t\t|| Core::App().settings().chatFiltersHorizontal();\n\tconst auto raw = controller.get();\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tbox->setStyle(st::filterInviteBox);\n\n\t\tusing Type = Ui::FilterLinkHeaderType;\n\t\tconst auto type = fullyAdded\n\t\t\t? Type::AllAdded\n\t\t\t: !filterId\n\t\t\t? Type::AddingFilter\n\t\t\t: Type::AddingChats;\n\t\tauto badge = raw->selectedValue(\n\t\t) | rpl::map([=](const base::flat_set<not_null<PeerData*>> &peers) {\n\t\t\treturn int(peers.size());\n\t\t});\n\t\tInitFilterLinkHeader(box, [=](int min, int max, int addedTop) {\n\t\t\traw->adjust(min, max, addedTop);\n\t\t}, type, title, iconEmoji, rpl::duplicate(badge), horizontalFilters);\n\n\t\traw->setRealContentHeight(box->heightValue());\n\n\t\tconst auto isStatic = title.isStatic;\n\t\tauto owned = Ui::FilterLinkProcessButton(\n\t\t\tbox,\n\t\t\ttype,\n\t\t\ttitle.text,\n\t\t\tCore::TextContext({\n\t\t\t\t.session = &strong->session(),\n\t\t\t\t.customEmojiLoopLimit = isStatic ? -1 : 0,\n\t\t\t}),\n\t\t\tstd::move(badge));\n\n\t\tconst auto button = owned.data();\n\t\tbox->widthValue(\n\t\t) | rpl::on_next([=](int width) {\n\t\t\tconst auto &padding = st::filterInviteBox.buttonPadding;\n\t\t\tbutton->resizeToWidth(width\n\t\t\t\t- padding.left()\n\t\t\t\t- padding.right());\n\t\t\tbutton->moveToLeft(padding.left(), padding.top());\n\t\t}, button->lifetime());\n\n\t\tbox->addButton(std::move(owned));\n\n\t\tHandleEnterInBox(box);\n\n\t\tstruct State {\n\t\t\tbool importing = false;\n\t\t};\n\t\tconst auto state = box->lifetime().make_state<State>();\n\n\t\traw->selectedValue(\n\t\t) | rpl::on_next([=](\n\t\t\t\tbase::flat_set<not_null<PeerData*>> &&peers) {\n\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\tif (peers.empty()) {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t} else if (!state->importing) {\n\t\t\t\t\tstate->importing = true;\n\t\t\t\t\tconst auto added = int(peers.size());\n\t\t\t\t\tImportInvite(slug, filterId, peers, crl::guard(box, [=] {\n\t\t\t\t\t\tShowImportToast(weak, title, type, peers.size());\n\t\t\t\t\t\tbox->closeBox();\n\t\t\t\t\t}), crl::guard(box, [=](QString text) {\n\t\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\t\tShowImportError(strong, filterId, added, text);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstate->importing = false;\n\t\t\t\t\t}));\n\t\t\t\t}\n\t\t\t});\n\t\t}, box->lifetime());\n\t};\n\tstrong->show(\n\t\tBox<PeerListBox>(std::move(controller), std::move(initBox)));\n}\n\nvoid ProcessFilterInvite(\n\t\tbase::weak_ptr<Window::SessionController> weak,\n\t\tconst QString &slug,\n\t\tFilterId filterId,\n\t\tstd::vector<not_null<PeerData*>> peers,\n\t\tstd::vector<not_null<PeerData*>> already) {\n\tconst auto strong = weak.get();\n\tif (!strong) {\n\t\treturn;\n\t}\n\tCore::App().hideMediaView();\n\tconst auto &list = strong->session().data().chatsFilters().list();\n\tconst auto it = ranges::find(list, filterId, &Data::ChatFilter::id);\n\tif (it == end(list)) {\n\t\tstrong->showToast(u\"Filter not found :shrug:\"_q);\n\t\treturn;\n\t}\n\tProcessFilterInvite(\n\t\tweak,\n\t\tslug,\n\t\tfilterId,\n\t\tit->title(),\n\t\tit->iconEmoji(),\n\t\tstd::move(peers),\n\t\tstd::move(already));\n}\n\n} // namespace\n\nvoid SaveNewFilterPinned(\n\t\tnot_null<Main::Session*> session,\n\t\tFilterId filterId) {\n\tconst auto &order = session->data().pinnedChatsOrder(filterId);\n\tauto &filters = session->data().chatsFilters();\n\tconst auto &filter = filters.applyUpdatedPinned(filterId, order);\n\tsession->api().request(MTPmessages_UpdateDialogFilter(\n\t\tMTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter),\n\t\tMTP_int(filterId),\n\t\tfilter.tl()\n\t)).send();\n}\n\nvoid CheckFilterInvite(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &slug) {\n\tconst auto session = &controller->session();\n\tconst auto weak = base::make_weak(controller);\n\tsession->api().checkFilterInvite(slug, [=](\n\t\t\tconst MTPchatlists_ChatlistInvite &result) {\n\t\tconst auto strong = weak.get();\n\t\tif (!strong) {\n\t\t\treturn;\n\t\t}\n\t\tauto title = Data::ChatFilterTitle();\n\t\tauto iconEmoji = QString();\n\t\tauto filterId = FilterId();\n\t\tauto peers = std::vector<not_null<PeerData*>>();\n\t\tauto already = std::vector<not_null<PeerData*>>();\n\t\tauto &owner = strong->session().data();\n\t\tresult.match([&](const auto &data) {\n\t\t\towner.processUsers(data.vusers());\n\t\t\towner.processChats(data.vchats());\n\t\t});\n\t\tconst auto parseList = [&](const MTPVector<MTPPeer> &list) {\n\t\t\tauto result = std::vector<not_null<PeerData*>>();\n\t\t\tresult.reserve(list.v.size());\n\t\t\tfor (const auto &peer : list.v) {\n\t\t\t\tresult.push_back(owner.peer(peerFromMTP(peer)));\n\t\t\t}\n\t\t\treturn result;\n\t\t};\n\t\tresult.match([&](const MTPDchatlists_chatlistInvite &data) {\n\t\t\ttitle.text = ParseTextWithEntities(session, data.vtitle());\n\t\t\ttitle.isStatic = data.is_title_noanimate();\n\t\t\ticonEmoji = data.vemoticon().value_or_empty();\n\t\t\tpeers = parseList(data.vpeers());\n\t\t}, [&](const MTPDchatlists_chatlistInviteAlready &data) {\n\t\t\tfilterId = data.vfilter_id().v;\n\t\t\tpeers = parseList(data.vmissing_peers());\n\t\t\talready = parseList(data.valready_peers());\n\t\t});\n\n\t\tconst auto notLoaded = filterId\n\t\t\t&& !ranges::contains(\n\t\t\t\towner.chatsFilters().list(),\n\t\t\t\tfilterId,\n\t\t\t\t&Data::ChatFilter::id);\n\t\tif (notLoaded) {\n\t\t\tconst auto lifetime = std::make_shared<rpl::lifetime>();\n\t\t\towner.chatsFilters().changed(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tlifetime->destroy();\n\t\t\t\tProcessFilterInvite(\n\t\t\t\t\tweak,\n\t\t\t\t\tslug,\n\t\t\t\t\tfilterId,\n\t\t\t\t\tstd::move(peers),\n\t\t\t\t\tstd::move(already));\n\t\t\t}, *lifetime);\n\t\t\towner.chatsFilters().reload();\n\t\t} else if (filterId) {\n\t\t\tProcessFilterInvite(\n\t\t\t\tweak,\n\t\t\t\tslug,\n\t\t\t\tfilterId,\n\t\t\t\tstd::move(peers),\n\t\t\t\tstd::move(already));\n\t\t} else {\n\t\t\tProcessFilterInvite(\n\t\t\t\tweak,\n\t\t\t\tslug,\n\t\t\t\tfilterId,\n\t\t\t\ttitle,\n\t\t\t\ticonEmoji,\n\t\t\t\tstd::move(peers),\n\t\t\t\tstd::move(already));\n\t\t}\n\t}, [=](const MTP::Error &error) {\n\t\tif (error.code() != 400) {\n\t\t\treturn;\n\t\t}\n\t\tProcessFilterInvite(weak, slug, {}, {}, {}, {}, {});\n\t});\n}\n\nvoid ProcessFilterUpdate(\n\t\tbase::weak_ptr<Window::SessionController> weak,\n\t\tFilterId filterId,\n\t\tstd::vector<not_null<PeerData*>> missing) {\n\tif (const auto strong = missing.empty() ? weak.get() : nullptr) {\n\t\tstrong->session().data().chatsFilters().moreChatsHide(filterId);\n\t\treturn;\n\t}\n\tProcessFilterInvite(weak, QString(), filterId, std::move(missing), {});\n}\n\nvoid ProcessFilterRemove(\n\t\tbase::weak_ptr<Window::SessionController> weak,\n\t\tData::ChatFilterTitle title,\n\t\tQString iconEmoji,\n\t\tstd::vector<not_null<PeerData*>> all,\n\t\tstd::vector<not_null<PeerData*>> suggest,\n\t\tFn<void(std::vector<not_null<PeerData*>>)> done) {\n\tconst auto strong = weak.get();\n\tif (!strong) {\n\t\treturn;\n\t}\n\tCore::App().hideMediaView();\n\tif (all.empty() && suggest.empty()) {\n\t\tdone({});\n\t\treturn;\n\t}\n\tauto controller = std::make_unique<ToggleChatsController>(\n\t\tstrong,\n\t\tToggleAction::Removing,\n\t\ttitle,\n\t\tstd::move(suggest),\n\t\tstd::move(all));\n\tconst auto horizontalFilters = !strong->enoughSpaceForFilters()\n\t\t|| Core::App().settings().chatFiltersHorizontal();\n\tconst auto raw = controller.get();\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tbox->setStyle(st::filterInviteBox);\n\n\t\tconst auto type = Ui::FilterLinkHeaderType::Removing;\n\t\tauto badge = raw->selectedValue(\n\t\t) | rpl::map([=](const base::flat_set<not_null<PeerData*>> &peers) {\n\t\t\treturn int(peers.size());\n\t\t});\n\t\tInitFilterLinkHeader(box, [=](int min, int max, int addedTop) {\n\t\t\traw->adjust(min, max, addedTop);\n\t\t}, type, title, iconEmoji, rpl::single(0), horizontalFilters);\n\n\t\tconst auto isStatic = title.isStatic;\n\t\tauto owned = Ui::FilterLinkProcessButton(\n\t\t\tbox,\n\t\t\ttype,\n\t\t\ttitle.text,\n\t\t\tCore::TextContext({\n\t\t\t\t.session = &strong->session(),\n\t\t\t\t.customEmojiLoopLimit = isStatic ? -1 : 0,\n\t\t\t}),\n\t\t\tstd::move(badge));\n\n\t\tconst auto button = owned.data();\n\t\tbox->widthValue(\n\t\t) | rpl::on_next([=](int width) {\n\t\t\tconst auto &padding = st::filterInviteBox.buttonPadding;\n\t\t\tbutton->resizeToWidth(width\n\t\t\t\t- padding.left()\n\t\t\t\t- padding.right());\n\t\t\tbutton->moveToLeft(padding.left(), padding.top());\n\t\t}, button->lifetime());\n\n\t\tbox->addButton(std::move(owned));\n\n\t\tHandleEnterInBox(box);\n\n\t\traw->selectedValue(\n\t\t) | rpl::on_next([=](\n\t\t\t\tbase::flat_set<not_null<PeerData*>> &&peers) {\n\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\tdone(peers | ranges::to_vector);\n\t\t\t\tbox->closeBox();\n\t\t\t});\n\t\t}, box->lifetime());\n\t};\n\tstrong->show(\n\t\tBox<PeerListBox>(std::move(controller), std::move(initBox)));\n}\n\n[[nodiscard]] std::vector<not_null<PeerData*>> ExtractSuggestRemoving(\n\t\tconst Data::ChatFilter &filter) {\n\tif (!filter.chatlist()) {\n\t\treturn {};\n\t}\n\treturn filter.always() | ranges::views::filter([](\n\t\tnot_null<History*> history) {\n\t\treturn history->peer->isChannel();\n\t}) | ranges::views::transform(&History::peer) | ranges::to_vector;\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_chat_filters.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Data {\nclass ChatFilter;\nstruct ChatFilterTitle;\n} // namespace Data\n\nnamespace Api {\n\nvoid SaveNewFilterPinned(\n\tnot_null<Main::Session*> session,\n\tFilterId filterId);\n\nvoid CheckFilterInvite(\n\tnot_null<Window::SessionController*> controller,\n\tconst QString &slug);\n\nvoid ProcessFilterUpdate(\n\tbase::weak_ptr<Window::SessionController> weak,\n\tFilterId filterId,\n\tstd::vector<not_null<PeerData*>> missing);\n\nvoid ProcessFilterRemove(\n\tbase::weak_ptr<Window::SessionController> weak,\n\tData::ChatFilterTitle title,\n\tQString iconEmoji,\n\tstd::vector<not_null<PeerData*>> all,\n\tstd::vector<not_null<PeerData*>> suggest,\n\tFn<void(std::vector<not_null<PeerData*>>)> done);\n\n[[nodiscard]] std::vector<not_null<PeerData*>> ExtractSuggestRemoving(\n\tconst Data::ChatFilter &filter);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_chat_filters_remove_manager.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_chat_filters_remove_manager.h\"\n\n#include \"api/api_chat_filters.h\"\n#include \"apiwrap.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Api {\nnamespace {\n\nvoid RemoveChatFilter(\n\t\tnot_null<Main::Session*> session,\n\t\tFilterId filterId,\n\t\tstd::vector<not_null<PeerData*>> leave) {\n\tconst auto api = &session->api();\n\tsession->data().chatsFilters().apply(MTP_updateDialogFilter(\n\t\tMTP_flags(MTPDupdateDialogFilter::Flag(0)),\n\t\tMTP_int(filterId),\n\t\tMTPDialogFilter()));\n\tif (leave.empty()) {\n\t\tapi->request(MTPmessages_UpdateDialogFilter(\n\t\t\tMTP_flags(MTPmessages_UpdateDialogFilter::Flag(0)),\n\t\t\tMTP_int(filterId),\n\t\t\tMTPDialogFilter()\n\t\t)).send();\n\t} else {\n\t\tapi->request(MTPchatlists_LeaveChatlist(\n\t\t\tMTP_inputChatlistDialogFilter(MTP_int(filterId)),\n\t\t\tMTP_vector<MTPInputPeer>(ranges::views::all(\n\t\t\t\tleave\n\t\t\t) | ranges::views::transform([](not_null<PeerData*> peer) {\n\t\t\t\treturn MTPInputPeer(peer->input());\n\t\t\t}) | ranges::to<QVector<MTPInputPeer>>())\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\tapi->applyUpdates(result);\n\t\t}).send();\n\t}\n}\n\n} // namespace\n\nRemoveComplexChatFilter::RemoveComplexChatFilter() = default;\n\nvoid RemoveComplexChatFilter::request(\n\t\tbase::weak_qptr<Ui::RpWidget> widget,\n\t\tbase::weak_ptr<Window::SessionController> weak,\n\t\tFilterId id) {\n\tconst auto session = &weak->session();\n\tconst auto &list = session->data().chatsFilters().list();\n\tconst auto i = ranges::find(list, id, &Data::ChatFilter::id);\n\tconst auto filter = (i != end(list)) ? *i : Data::ChatFilter();\n\tconst auto has = filter.hasMyLinks();\n\tconst auto confirm = [=](Fn<void()> action, bool onlyWhenHas = false) {\n\t\tif (!has && onlyWhenHas) {\n\t\t\taction();\n\t\t\treturn;\n\t\t}\n\t\tweak->window().show(Ui::MakeConfirmBox({\n\t\t\t.text = (has\n\t\t\t\t? tr::lng_filters_delete_sure()\n\t\t\t\t: tr::lng_filters_remove_sure()),\n\t\t\t.confirmed = [=](Fn<void()> &&close) { close(); action(); },\n\t\t\t.confirmText = (has\n\t\t\t\t? tr::lng_box_delete()\n\t\t\t\t: tr::lng_filters_remove_yes()),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t}));\n\t};\n\tconst auto simple = [=] {\n\t\tconfirm([=] { RemoveChatFilter(session, id, {}); });\n\t};\n\tconst auto suggestRemoving = Api::ExtractSuggestRemoving(filter);\n\tif (suggestRemoving.empty()) {\n\t\tsimple();\n\t\treturn;\n\t} else if (_removingRequestId) {\n\t\tif (_removingId == id) {\n\t\t\treturn;\n\t\t}\n\t\tsession->api().request(_removingRequestId).cancel();\n\t}\n\t_removingId = id;\n\t_removingRequestId = session->api().request(\n\t\tMTPchatlists_GetLeaveChatlistSuggestions(\n\t\t\tMTP_inputChatlistDialogFilter(\n\t\t\t\tMTP_int(id)))\n\t).done(crl::guard(widget, [=, this](const MTPVector<MTPPeer> &result) {\n\t\t_removingRequestId = 0;\n\t\tconst auto suggestRemovePeers = ranges::views::all(\n\t\t\tresult.v\n\t\t) | ranges::views::transform([=](const MTPPeer &peer) {\n\t\t\treturn session->data().peer(peerFromMTP(peer));\n\t\t}) | ranges::to_vector;\n\t\tconst auto chosen = crl::guard(widget, [=](\n\t\t\t\tstd::vector<not_null<PeerData*>> peers) {\n\t\t\tRemoveChatFilter(session, id, std::move(peers));\n\t\t});\n\t\tconfirm(crl::guard(widget, [=] {\n\t\t\tApi::ProcessFilterRemove(\n\t\t\t\tweak,\n\t\t\t\tfilter.title(),\n\t\t\t\tfilter.iconEmoji(),\n\t\t\t\tsuggestRemoving,\n\t\t\t\tsuggestRemovePeers,\n\t\t\t\tchosen);\n\t\t}), true);\n\t})).fail(crl::guard(widget, [=, this] {\n\t\t_removingRequestId = 0;\n\t\tsimple();\n\t})).send();\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_chat_filters_remove_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Api {\n\nclass RemoveComplexChatFilter final {\npublic:\n\tRemoveComplexChatFilter();\n\n\tvoid request(\n\t\tbase::weak_qptr<Ui::RpWidget> widget,\n\t\tbase::weak_ptr<Window::SessionController> weak,\n\t\tFilterId id);\n\nprivate:\n\tFilterId _removingId = 0;\n\tmtpRequestId _removingRequestId = 0;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_chat_invite.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_chat_invite.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_credits.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"core/application.h\"\n#include \"data/components/credits.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"info/channel_statistics/boosts/giveaway/boost_badge.h\"\n#include \"info/profile/info_profile_badge.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/effects/premium_stars_colored.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_premium.h\"\n\nnamespace Api {\n\nnamespace {\n\nstruct InviteParticipant {\n\tnot_null<UserData*> user;\n\tUi::PeerUserpicView userpic;\n};\n\nstruct ChatInvite {\n\tQString title;\n\tQString about;\n\tPhotoData *photo = nullptr;\n\tint participantsCount = 0;\n\tstd::vector<InviteParticipant> participants;\n\tbool isPublic = false;\n\tbool isChannel = false;\n\tbool isMegagroup = false;\n\tbool isBroadcast = false;\n\tbool isRequestNeeded = false;\n\tbool isFake = false;\n\tbool isScam = false;\n\tbool isVerified = false;\n};\n\n[[nodiscard]] ChatInvite ParseInvite(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDchatInvite &data) {\n\tauto participants = std::vector<InviteParticipant>();\n\tif (const auto list = data.vparticipants()) {\n\t\tparticipants.reserve(list->v.size());\n\t\tfor (const auto &participant : list->v) {\n\t\t\tif (const auto user = session->data().processUser(participant)) {\n\t\t\t\tparticipants.push_back(InviteParticipant{ user });\n\t\t\t}\n\t\t}\n\t}\n\tconst auto photo = session->data().processPhoto(data.vphoto());\n\treturn {\n\t\t.title = qs(data.vtitle()),\n\t\t.about = data.vabout().value_or_empty(),\n\t\t.photo = (photo->isNull() ? nullptr : photo.get()),\n\t\t.participantsCount = data.vparticipants_count().v,\n\t\t.participants = std::move(participants),\n\t\t.isPublic = data.is_public(),\n\t\t.isChannel = data.is_channel(),\n\t\t.isMegagroup = data.is_megagroup(),\n\t\t.isBroadcast = data.is_broadcast(),\n\t\t.isRequestNeeded = data.is_request_needed(),\n\t\t.isFake = data.is_fake(),\n\t\t.isScam = data.is_scam(),\n\t\t.isVerified = data.is_verified(),\n\t};\n}\n\n[[nodiscard]] Info::Profile::BadgeType BadgeForInvite(\n\t\tconst ChatInvite &invite) {\n\tusing Type = Info::Profile::BadgeType;\n\treturn invite.isVerified\n\t\t? Type::Verified\n\t\t: invite.isScam\n\t\t? Type::Scam\n\t\t: invite.isFake\n\t\t? Type::Fake\n\t\t: Type::None;\n}\n\nvoid SubmitChatInvite(\n\t\tbase::weak_ptr<Window::SessionController> weak,\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &hash,\n\t\tbool isGroup) {\n\tsession->api().request(MTPmessages_ImportChatInvite(\n\t\tMTP_string(hash)\n\t)).done([=](const MTPUpdates &result) {\n\t\tsession->api().applyUpdates(result);\n\t\tconst auto strongController = weak.get();\n\t\tif (!strongController) {\n\t\t\treturn;\n\t\t}\n\n\t\tstrongController->hideLayer();\n\t\tconst auto handleChats = [&](const MTPVector<MTPChat> &chats) {\n\t\t\tif (chats.v.isEmpty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto peerId = chats.v[0].match([](const MTPDchat &data) {\n\t\t\t\treturn peerFromChat(data.vid().v);\n\t\t\t}, [](const MTPDchannel &data) {\n\t\t\t\treturn peerFromChannel(data.vid().v);\n\t\t\t}, [](auto&&) {\n\t\t\t\treturn PeerId(0);\n\t\t\t});\n\t\t\tif (const auto peer = session->data().peerLoaded(peerId)) {\n\t\t\t\t// Shows in the primary window anyway.\n\t\t\t\tstrongController->showPeerHistory(\n\t\t\t\t\tpeer,\n\t\t\t\t\tWindow::SectionShow::Way::Forward);\n\t\t\t}\n\t\t};\n\t\tresult.match([&](const MTPDupdates &data) {\n\t\t\thandleChats(data.vchats());\n\t\t}, [&](const MTPDupdatesCombined &data) {\n\t\t\thandleChats(data.vchats());\n\t\t}, [&](auto &&) {\n\t\t\tLOG((\"API Error: unexpected update cons %1 \"\n\t\t\t\t\"(ApiWrap::importChatInvite)\").arg(result.type()));\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto &type = error.type();\n\n\t\tconst auto strongController = weak.get();\n\t\tif (!strongController) {\n\t\t\treturn;\n\t\t} else if (type == u\"CHANNELS_TOO_MUCH\"_q) {\n\t\t\tstrongController->show(\n\t\t\t\tBox(ChannelsLimitBox, &strongController->session()));\n\t\t\treturn;\n\t\t}\n\n\t\tstrongController->hideLayer();\n\t\tstrongController->showToast([&] {\n\t\t\tif (type == u\"INVITE_REQUEST_SENT\"_q) {\n\t\t\t\treturn isGroup\n\t\t\t\t\t? tr::lng_group_request_sent(tr::now)\n\t\t\t\t\t: tr::lng_group_request_sent_channel(tr::now);\n\t\t\t} else if (type == u\"USERS_TOO_MUCH\"_q) {\n\t\t\t\treturn tr::lng_group_invite_no_room(tr::now);\n\t\t\t} else {\n\t\t\t\treturn tr::lng_group_invite_bad_link(tr::now);\n\t\t\t}\n\t\t}(), ApiWrap::kJoinErrorDuration);\n\t}).send();\n}\n\nvoid ConfirmSubscriptionBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &hash,\n\t\tconst MTPDchatInvite *data) {\n\tbox->setWidth(st::boxWideWidth);\n\tconst auto amount = data->vsubscription_pricing()->data().vamount().v;\n\tconst auto formId = data->vsubscription_form_id()->v;\n\tconst auto name = qs(data->vtitle());\n\tconst auto maybePhoto = session->data().processPhoto(data->vphoto());\n\tconst auto photo = maybePhoto->isNull() ? nullptr : maybePhoto.get();\n\n\tstruct State final {\n\t\tstd::shared_ptr<Data::PhotoMedia> photoMedia;\n\t\tstd::unique_ptr<Ui::EmptyUserpic> photoEmpty;\n\t\tQImage frame;\n\n\t\tstd::optional<MTP::Sender> api;\n\t\tUi::RpWidget* saveButton = nullptr;\n\t\trpl::variable<bool> loading;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\n\tconst auto content = box->verticalLayout();\n\n\tUi::AddSkip(content, st::confirmInvitePhotoTop);\n\tconst auto userpic = content->add(\n\t\tobject_ptr<Ui::RpWidget>(content),\n\t\tstyle::al_top);\n\tconst auto photoSize = st::confirmInvitePhotoSize;\n\tuserpic->resize(Size(photoSize));\n\tuserpic->setNaturalWidth(photoSize);\n\tconst auto creditsIconSize = photoSize / 3;\n\tconst auto creditsIconCallback =\n\t\tUi::PaintOutlinedColoredCreditsIconCallback(\n\t\t\tcreditsIconSize,\n\t\t\t1.5);\n\tstate->frame = QImage(\n\t\tSize(photoSize * style::DevicePixelRatio()),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tstate->frame.setDevicePixelRatio(style::DevicePixelRatio());\n\tconst auto options = Images::Option::RoundCircle;\n\tuserpic->paintRequest(\n\t) | rpl::on_next([=, small = Data::PhotoSize::Small] {\n\t\tstate->frame.fill(Qt::transparent);\n\t\t{\n\t\t\tauto p = QPainter(&state->frame);\n\t\t\tif (state->photoMedia) {\n\t\t\t\tif (const auto image = state->photoMedia->image(small)) {\n\t\t\t\t\tp.drawPixmap(\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\timage->pix(Size(photoSize), { .options = options }));\n\t\t\t\t}\n\t\t\t} else if (state->photoEmpty) {\n\t\t\t\tstate->photoEmpty->paintCircle(\n\t\t\t\t\tp,\n\t\t\t\t\t0,\n\t\t\t\t\t0,\n\t\t\t\t\tuserpic->width(),\n\t\t\t\t\tphotoSize);\n\t\t\t}\n\t\t\tif (creditsIconCallback) {\n\t\t\t\tp.translate(\n\t\t\t\t\tphotoSize - creditsIconSize,\n\t\t\t\t\tphotoSize - creditsIconSize);\n\t\t\t\tcreditsIconCallback(p);\n\t\t\t}\n\t\t}\n\t\tauto p = QPainter(userpic);\n\t\tp.drawImage(0, 0, state->frame);\n\t}, userpic->lifetime());\n\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tif (photo) {\n\t\tstate->photoMedia = photo->createMediaView();\n\t\tstate->photoMedia->wanted(Data::PhotoSize::Small, Data::FileOrigin());\n\t\tif (!state->photoMedia->image(Data::PhotoSize::Small)) {\n\t\t\tsession->downloaderTaskFinished(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tuserpic->update();\n\t\t\t}, userpic->lifetime());\n\t\t}\n\t} else {\n\t\tstate->photoEmpty = std::make_unique<Ui::EmptyUserpic>(\n\t\t\tUi::EmptyUserpic::UserpicColor(0),\n\t\t\tname);\n\t}\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\n\tSettings::AddMiniStars(\n\t\tcontent,\n\t\tUi::CreateChild<Ui::RpWidget>(content),\n\t\tphotoSize,\n\t\tbox->width(),\n\t\t2.);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_channel_invite_subscription_title(),\n\t\t\tst::inviteLinkSubscribeBoxTitle),\n\t\tstyle::al_top);\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_channel_invite_subscription_about(\n\t\t\t\tlt_channel,\n\t\t\t\trpl::single(tr::bold(name)),\n\t\t\t\tlt_price,\n\t\t\t\ttr::lng_credits_summary_options_credits(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(amount) | tr::to_count(),\n\t\t\t\t\ttr::bold),\n\t\t\t\ttr::marked),\n\t\t\tst::inviteLinkSubscribeBoxAbout),\n\t\tstyle::al_top);\n\tUi::AddSkip(content);\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_channel_invite_subscription_terms(\n\t\t\t\tlt_link,\n\t\t\t\trpl::combine(\n\t\t\t\t\ttr::lng_paid_react_agree_link(),\n\t\t\t\t\ttr::lng_group_invite_subscription_about_url()\n\t\t\t\t) | rpl::map([](const QString &text, const QString &url) {\n\t\t\t\t\treturn tr::link(text, url);\n\t\t\t\t}),\n\t\t\t\ttr::rich),\n\t\t\tst::inviteLinkSubscribeBoxTerms),\n\t\tstyle::al_top);\n\n\t{\n\t\tconst auto balance = Settings::AddBalanceWidget(\n\t\t\tcontent,\n\t\t\tsession,\n\t\t\tsession->credits().balanceValue(),\n\t\t\ttrue);\n\t\tsession->credits().load(true);\n\n\t\trpl::combine(\n\t\t\tbalance->sizeValue(),\n\t\t\tcontent->sizeValue()\n\t\t) | rpl::on_next([=](const QSize &, const QSize &) {\n\t\t\tbalance->moveToRight(\n\t\t\t\tst::creditsHistoryRightSkip * 2,\n\t\t\t\tst::creditsHistoryRightSkip);\n\t\t\tbalance->update();\n\t\t}, balance->lifetime());\n\t}\n\n\tconst auto sendCredits = [=, weak = base::make_weak(box)] {\n\t\tconst auto show = box->uiShow();\n\t\tconst auto buttonWidth = state->saveButton\n\t\t\t? state->saveButton->width()\n\t\t\t: 0;\n\t\tconst auto finish = [=] {\n\t\t\tstate->api = std::nullopt;\n\t\t\tstate->loading.force_assign(false);\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t};\n\t\tstate->api->request(\n\t\t\tMTPpayments_SendStarsForm(\n\t\t\t\tMTP_long(formId),\n\t\t\t\tMTP_inputInvoiceChatInviteSubscription(MTP_string(hash)))\n\t\t).done([=](const MTPpayments_PaymentResult &result) {\n\t\t\tresult.match([&](const MTPDpayments_paymentResult &data) {\n\t\t\t\tsession->api().applyUpdates(data.vupdates());\n\t\t\t}, [](const MTPDpayments_paymentVerificationNeeded &data) {\n\t\t\t});\n\t\t\tconst auto refill = session->data().activeCreditsSubsRebuilder();\n\t\t\tconst auto strong = weak.get();\n\t\t\tif (!strong) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!refill) {\n\t\t\t\treturn finish();\n\t\t\t}\n\t\t\tconst auto api\n\t\t\t\t= strong->lifetime().make_state<Api::CreditsHistory>(\n\t\t\t\t\tsession->user(),\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue);\n\t\t\tapi->requestSubscriptions({}, [=](Data::CreditsStatusSlice d) {\n\t\t\t\trefill->fire(std::move(d));\n\t\t\t\tfinish();\n\t\t\t});\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconst auto id = error.type();\n\t\t\tif (weak) {\n\t\t\t\tstate->api = std::nullopt;\n\t\t\t}\n\t\t\tshow->showToast(id);\n\t\t\tstate->loading.force_assign(false);\n\t\t}).send();\n\t\tif (state->saveButton) {\n\t\t\tstate->saveButton->resizeToWidth(buttonWidth);\n\t\t}\n\t};\n\n\tauto confirmText = tr::lng_channel_invite_subscription_button();\n\tstate->saveButton = box->addButton(std::move(confirmText), [=] {\n\t\tif (state->api) {\n\t\t\treturn;\n\t\t}\n\t\tstate->api.emplace(&session->mtp());\n\t\tstate->loading.force_assign(true);\n\n\t\tconst auto done = [=](Settings::SmallBalanceResult result) {\n\t\t\tif (result == Settings::SmallBalanceResult::Success\n\t\t\t\t|| result == Settings::SmallBalanceResult::Already) {\n\t\t\t\tsendCredits();\n\t\t\t} else {\n\t\t\t\tstate->api = std::nullopt;\n\t\t\t\tstate->loading.force_assign(false);\n\t\t\t}\n\t\t};\n\t\tSettings::MaybeRequestBalanceIncrease(\n\t\t\tMain::MakeSessionShow(box->uiShow(), session),\n\t\t\tamount,\n\t\t\tSettings::SmallBalanceSubscription{ .name = name },\n\t\t\tdone);\n\t});\n\n\tif (const auto saveButton = state->saveButton) {\n\t\tusing namespace Info::Statistics;\n\t\tconst auto loadingAnimation = InfiniteRadialAnimationWidget(\n\t\t\tsaveButton,\n\t\t\tsaveButton->height() / 2,\n\t\t\t&st::editStickerSetNameLoading);\n\t\tAddChildToWidgetCenter(saveButton, loadingAnimation);\n\t\tloadingAnimation->showOn(\n\t\t\tstate->loading.value() | rpl::map(rpl::mappers::_1));\n\t}\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\nvoid ConfirmInviteBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDchatInvite *invitePtr,\n\t\tChannelData *invitePeekChannel,\n\t\tFn<void()> submit) {\n\tauto invite = ParseInvite(session, *invitePtr);\n\tconst auto isChannel = invite.isChannel && !invite.isMegagroup;\n\tconst auto requestApprove = invite.isRequestNeeded;\n\tconst auto count = invite.participantsCount;\n\n\tstruct State {\n\t\tstd::shared_ptr<Data::PhotoMedia> photoMedia;\n\t\tstd::unique_ptr<Ui::EmptyUserpic> photoEmpty;\n\t\tstd::vector<InviteParticipant> participants;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tstate->participants = std::move(invite.participants);\n\n\tconst auto status = [&] {\n\t\treturn invitePeekChannel\n\t\t\t? tr::lng_channel_invite_private(tr::now)\n\t\t\t: (!state->participants.empty()\n\t\t\t\t&& int(state->participants.size()) < count)\n\t\t\t? tr::lng_group_invite_members(tr::now, lt_count, count)\n\t\t\t: (count > 0 && isChannel)\n\t\t\t? tr::lng_chat_status_subscribers(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\tcount)\n\t\t\t: (count > 0)\n\t\t\t? tr::lng_chat_status_members(tr::now, lt_count_decimal, count)\n\t\t\t: isChannel\n\t\t\t? tr::lng_channel_status(tr::now)\n\t\t\t: tr::lng_group_status(tr::now);\n\t}();\n\n\tbox->setNoContentMargin(true);\n\tbox->setWidth(st::boxWideWidth);\n\tconst auto content = box->verticalLayout();\n\n\tUi::AddSkip(content, st::confirmInvitePhotoTop);\n\tconst auto userpic = content->add(\n\t\tobject_ptr<Ui::RpWidget>(content),\n\t\tstyle::al_top);\n\tconst auto photoSize = st::confirmInvitePhotoSize;\n\tuserpic->resize(Size(photoSize));\n\tuserpic->setNaturalWidth(photoSize);\n\tuserpic->paintRequest(\n\t) | rpl::on_next([=, small = Data::PhotoSize::Small] {\n\t\tauto p = QPainter(userpic);\n\t\tif (state->photoMedia) {\n\t\t\tif (const auto image = state->photoMedia->image(small)) {\n\t\t\t\tp.drawPixmap(\n\t\t\t\t\t0,\n\t\t\t\t\t0,\n\t\t\t\t\timage->pix(\n\t\t\t\t\t\tSize(photoSize),\n\t\t\t\t\t\t{ .options = Images::Option::RoundCircle }));\n\t\t\t}\n\t\t} else if (state->photoEmpty) {\n\t\t\tstate->photoEmpty->paintCircle(\n\t\t\t\tp,\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\tuserpic->width(),\n\t\t\t\tphotoSize);\n\t\t}\n\t}, userpic->lifetime());\n\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tif (const auto photo = invite.photo) {\n\t\tstate->photoMedia = photo->createMediaView();\n\t\tstate->photoMedia->wanted(\n\t\t\tData::PhotoSize::Small,\n\t\t\tData::FileOrigin());\n\t\tif (!state->photoMedia->image(Data::PhotoSize::Small)) {\n\t\t\tsession->downloaderTaskFinished(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tuserpic->update();\n\t\t\t}, userpic->lifetime());\n\t\t}\n\t} else {\n\t\tstate->photoEmpty = std::make_unique<Ui::EmptyUserpic>(\n\t\t\tUi::EmptyUserpic::UserpicColor(0),\n\t\t\tinvite.title);\n\t}\n\n\tUi::AddSkip(content);\n\tconst auto title = box->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\tinvite.title,\n\t\t\tst::confirmInviteTitle),\n\t\tstyle::al_top);\n\n\tconst auto badgeType = BadgeForInvite(invite);\n\tif (badgeType != Info::Profile::BadgeType::None) {\n\t\tconst auto badgeParent = title->parentWidget();\n\t\tconst auto badge = box->lifetime().make_state<Info::Profile::Badge>(\n\t\t\tbadgeParent,\n\t\t\tst::infoPeerBadge,\n\t\t\tsession,\n\t\t\trpl::single(Info::Profile::Badge::Content{ badgeType }),\n\t\t\tnullptr,\n\t\t\t[] { return false; });\n\t\ttitle->geometryValue(\n\t\t) | rpl::on_next([=](const QRect &r) {\n\t\t\tbadge->move(r.x() + r.width(), r.y(), r.y() + r.height());\n\t\t}, title->lifetime());\n\t}\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\tstatus,\n\t\t\tst::confirmInviteStatus),\n\t\tstyle::al_top);\n\n\tif (!invite.about.isEmpty()) {\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\tinvite.about,\n\t\t\t\tst::confirmInviteAbout),\n\t\t\tst::confirmInviteAboutPadding,\n\t\t\tstyle::al_top);\n\t}\n\n\tif (requestApprove) {\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\t(isChannel\n\t\t\t\t\t? tr::lng_group_request_about_channel(tr::now)\n\t\t\t\t\t: tr::lng_group_request_about(tr::now)),\n\t\t\t\tst::confirmInviteStatus),\n\t\t\tst::confirmInviteAboutRequestsPadding,\n\t\t\tstyle::al_top);\n\t}\n\n\tif (!state->participants.empty()) {\n\t\twhile (state->participants.size() > 4) {\n\t\t\tstate->participants.pop_back();\n\t\t}\n\t\tconst auto padding = (st::confirmInviteUsersWidth\n\t\t\t- 4 * st::confirmInviteUserPhotoSize) / 10;\n\t\tconst auto userWidth = st::confirmInviteUserPhotoSize + 2 * padding;\n\n\t\tauto strip = object_ptr<Ui::RpWidget>(content);\n\t\tconst auto rawStrip = strip.data();\n\t\trawStrip->resize(st::boxWideWidth, st::confirmInviteUserHeight);\n\t\trawStrip->setNaturalWidth(st::boxWideWidth);\n\n\t\tconst auto shown = int(state->participants.size());\n\t\tconst auto sumWidth = shown * userWidth;\n\t\tconst auto baseLeft = (st::boxWideWidth - sumWidth) / 2;\n\t\tfor (auto i = 0; i != shown; ++i) {\n\t\t\tconst auto &participant = state->participants[i];\n\t\t\tconst auto name = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\trawStrip,\n\t\t\t\tst::confirmInviteUserName);\n\t\t\tname->resizeToWidth(\n\t\t\t\tst::confirmInviteUserPhotoSize + padding);\n\t\t\tname->setText(participant.user->firstName.isEmpty()\n\t\t\t\t? participant.user->name()\n\t\t\t\t: participant.user->firstName);\n\t\t\tname->moveToLeft(\n\t\t\t\tbaseLeft + i * userWidth + (padding / 2),\n\t\t\t\tst::confirmInviteUserNameTop - st::confirmInviteUserPhotoTop);\n\t\t}\n\n\t\trawStrip->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = Painter(rawStrip);\n\t\t\tconst auto total = int(state->participants.size());\n\t\t\tconst auto totalWidth = total * userWidth;\n\t\t\tauto left = (rawStrip->width() - totalWidth) / 2;\n\t\t\tfor (auto &participant : state->participants) {\n\t\t\t\tparticipant.user->paintUserpicLeft(\n\t\t\t\t\tp,\n\t\t\t\t\tparticipant.userpic,\n\t\t\t\t\tleft + (userWidth - st::confirmInviteUserPhotoSize) / 2,\n\t\t\t\t\t0,\n\t\t\t\t\trawStrip->width(),\n\t\t\t\t\tst::confirmInviteUserPhotoSize);\n\t\t\t\tleft += userWidth;\n\t\t\t}\n\t\t}, rawStrip->lifetime());\n\n\t\tUi::AddSkip(content, st::boxPadding.bottom());\n\t\tcontent->add(std::move(strip), style::margins());\n\t}\n\n\tbox->addButton((requestApprove\n\t\t? tr::lng_group_request_to_join()\n\t\t: isChannel\n\t\t? tr::lng_profile_join_channel()\n\t\t: tr::lng_profile_join_group()), submit);\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\n} // namespace\n\nvoid CheckChatInvite(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &hash,\n\t\tChannelData *invitePeekChannel,\n\t\tFn<void()> loaded) {\n\tconst auto session = &controller->session();\n\tconst auto weak = base::make_weak(controller);\n\tsession->api().checkChatInvite(hash, [=](const MTPChatInvite &result) {\n\t\tconst auto strong = weak.get();\n\t\tif (!strong) {\n\t\t\treturn;\n\t\t}\n\t\tif (loaded) {\n\t\t\tloaded();\n\t\t}\n\t\tCore::App().hideMediaView();\n\t\tconst auto show = [&](not_null<PeerData*> chat) {\n\t\t\tconst auto way = Window::SectionShow::Way::Forward;\n\t\t\tif (const auto forum = chat->forum()) {\n\t\t\t\tstrong->showForum(forum, way);\n\t\t\t} else {\n\t\t\t\tstrong->showPeerHistory(chat, way);\n\t\t\t}\n\t\t};\n\t\tresult.match([=](const MTPDchatInvite &data) {\n\t\t\tconst auto isGroup = !data.is_broadcast();\n\t\t\tconst auto hasPricing = !!data.vsubscription_pricing();\n\t\t\tconst auto canRefulfill = data.is_can_refulfill_subscription();\n\t\t\tif (hasPricing\n\t\t\t\t&& !canRefulfill\n\t\t\t\t&& !data.vsubscription_form_id()) {\n\t\t\t\tstrong->uiShow()->showToast(\n\t\t\t\t\ttr::lng_confirm_phone_link_invalid(tr::now));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto box = (hasPricing && !canRefulfill)\n\t\t\t\t? strong->show(Box(\n\t\t\t\t\tConfirmSubscriptionBox,\n\t\t\t\t\tsession,\n\t\t\t\t\thash,\n\t\t\t\t\t&data))\n\t\t\t\t: strong->show(Box(\n\t\t\t\t\tConfirmInviteBox,\n\t\t\t\t\tsession,\n\t\t\t\t\t&data,\n\t\t\t\t\tinvitePeekChannel,\n\t\t\t\t\t[=] { SubmitChatInvite(weak, session, hash, isGroup); }));\n\t\t\tif (invitePeekChannel) {\n\t\t\t\tbox->boxClosing(\n\t\t\t\t) | rpl::filter([=] {\n\t\t\t\t\treturn !invitePeekChannel->amIn();\n\t\t\t\t}) | rpl::on_next([=] {\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->clearSectionStack(Window::SectionShow(\n\t\t\t\t\t\t\tWindow::SectionShow::Way::ClearStack,\n\t\t\t\t\t\t\tanim::type::normal,\n\t\t\t\t\t\t\tanim::activation::background));\n\t\t\t\t\t}\n\t\t\t\t}, box->lifetime());\n\t\t\t}\n\t\t}, [=](const MTPDchatInviteAlready &data) {\n\t\t\tif (const auto chat = session->data().processChat(data.vchat())) {\n\t\t\t\tif (const auto channel = chat->asChannel()) {\n\t\t\t\t\tchannel->clearInvitePeek();\n\t\t\t\t}\n\t\t\t\tshow(chat);\n\t\t\t}\n\t\t}, [=](const MTPDchatInvitePeek &data) {\n\t\t\tif (const auto chat = session->data().processChat(data.vchat())) {\n\t\t\t\tif (const auto channel = chat->asChannel()) {\n\t\t\t\t\tchannel->setInvitePeek(hash, data.vexpires().v);\n\t\t\t\t\tshow(chat);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}, [=](const MTP::Error &error) {\n\t\tif (MTP::IsFloodError(error)) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->show(Ui::MakeInformBox(tr::lng_flood_error()));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (error.code() != 400) {\n\t\t\treturn;\n\t\t}\n\t\tCore::App().hideMediaView();\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->show(Ui::MakeInformBox(tr::lng_group_invite_bad_link()));\n\t\t}\n\t});\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_chat_invite.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass ChannelData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Api {\n\nvoid CheckChatInvite(\n\tnot_null<Window::SessionController*> controller,\n\tconst QString &hash,\n\tChannelData *invitePeekChannel = nullptr,\n\tFn<void()> loaded = nullptr);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_chat_links.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_chat_links.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"apiwrap.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n\nnamespace Api {\nnamespace {\n\n[[nodiscard]] ChatLink FromMTP(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPBusinessChatLink &link) {\n\tconst auto &data = link.data();\n\treturn {\n\t\t.link = qs(data.vlink()),\n\t\t.title = qs(data.vtitle().value_or_empty()),\n\t\t.message = {\n\t\t\tqs(data.vmessage()),\n\t\t\tEntitiesFromMTP(\n\t\t\t\tsession,\n\t\t\t\tdata.ventities().value_or_empty())\n\t\t},\n\t\t.clicks = data.vviews().v,\n\t};\n}\n\n[[nodiscard]] MTPInputBusinessChatLink ToMTP(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &title,\n\t\tconst TextWithEntities &message) {\n\tauto entities = EntitiesToMTP(\n\t\tsession,\n\t\tmessage.entities,\n\t\tConvertOption::SkipLocal);\n\tusing Flag = MTPDinputBusinessChatLink::Flag;\n\tconst auto flags = (title.isEmpty() ? Flag() : Flag::f_title)\n\t\t| (entities.v.isEmpty() ? Flag() : Flag::f_entities);\n\treturn MTP_inputBusinessChatLink(\n\t\tMTP_flags(flags),\n\t\tMTP_string(message.text),\n\t\tstd::move(entities),\n\t\tMTP_string(title));\n}\n\n} // namespace\n\nChatLinks::ChatLinks(not_null<ApiWrap*> api) : _api(api) {\n}\n\nvoid ChatLinks::create(\n\t\tconst QString &title,\n\t\tconst TextWithEntities &message,\n\t\tFn<void(Link)> done) {\n\tconst auto session = &_api->session();\n\t_api->request(MTPaccount_CreateBusinessChatLink(\n\t\tToMTP(session, title, message)\n\t)).done([=](const MTPBusinessChatLink &result) {\n\t\tconst auto link = FromMTP(session, result);\n\t\t_list.push_back(link);\n\t\t_updates.fire({ .was = QString(), .now = link });\n\t\tif (done) done(link);\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto type = error.type();\n\t\tif (done) done(Link());\n\t}).send();\n}\n\nvoid ChatLinks::edit(\n\t\tconst QString &link,\n\t\tconst QString &title,\n\t\tconst TextWithEntities &message,\n\t\tFn<void(Link)> done) {\n\tconst auto session = &_api->session();\n\t_api->request(MTPaccount_EditBusinessChatLink(\n\t\tMTP_string(link),\n\t\tToMTP(session, title, message)\n\t)).done([=](const MTPBusinessChatLink &result) {\n\t\tconst auto parsed = FromMTP(session, result);\n\t\tif (parsed.link != link) {\n\t\t\tLOG((\"API Error: EditBusinessChatLink changed the link.\"));\n\t\t\tif (done) done(Link());\n\t\t\treturn;\n\t\t}\n\t\tconst auto i = ranges::find(_list, link, &Link::link);\n\t\tif (i != end(_list)) {\n\t\t\t*i = parsed;\n\t\t\t_updates.fire({ .was = link, .now = parsed });\n\t\t\tif (done) done(parsed);\n\t\t} else {\n\t\t\tLOG((\"API Error: EditBusinessChatLink link not found.\"));\n\t\t\tif (done) done(Link());\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto type = error.type();\n\t\tif (done) done(Link());\n\t}).send();\n}\n\nvoid ChatLinks::destroy(\n\t\tconst QString &link,\n\t\tFn<void()> done) {\n\t_api->request(MTPaccount_DeleteBusinessChatLink(\n\t\tMTP_string(link)\n\t)).done([=] {\n\t\tconst auto i = ranges::find(_list, link, &Link::link);\n\t\tif (i != end(_list)) {\n\t\t\t_list.erase(i);\n\t\t\t_updates.fire({ .was = link });\n\t\t\tif (done) done();\n\t\t} else {\n\t\t\tLOG((\"API Error: DeleteBusinessChatLink link not found.\"));\n\t\t\tif (done) done();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto type = error.type();\n\t\tif (done) done();\n\t}).send();\n}\n\nvoid ChatLinks::preload() {\n\tif (_loaded || _requestId) {\n\t\treturn;\n\t}\n\t_requestId = _api->request(MTPaccount_GetBusinessChatLinks(\n\t)).done([=](const MTPaccount_BusinessChatLinks &result) {\n\t\tconst auto &data = result.data();\n\t\tconst auto session = &_api->session();\n\t\tconst auto owner = &session->data();\n\t\towner->processUsers(data.vusers());\n\t\towner->processChats(data.vchats());\n\t\tauto links = std::vector<Link>();\n\t\tlinks.reserve(data.vlinks().v.size());\n\t\tfor (const auto &link : data.vlinks().v) {\n\t\t\tlinks.push_back(FromMTP(session, link));\n\t\t}\n\t\t_list = std::move(links);\n\t\t_loaded = true;\n\t\t_loadedUpdates.fire({});\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t\t_loaded = true;\n\t\t_loadedUpdates.fire({});\n\t}).send();\n}\n\nconst std::vector<ChatLink> &ChatLinks::list() const {\n\treturn _list;\n}\n\nbool ChatLinks::loaded() const {\n\treturn _loaded;\n}\n\nrpl::producer<> ChatLinks::loadedUpdates() const {\n\treturn _loadedUpdates.events();\n}\n\nrpl::producer<ChatLinks::Update> ChatLinks::updates() const {\n\treturn _updates.events();\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_chat_links.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass ApiWrap;\n\nnamespace Api {\n\nstruct ChatLink {\n\tQString link;\n\tQString title;\n\tTextWithEntities message;\n\tint clicks = 0;\n};\n\nstruct ChatLinkUpdate {\n\tQString was;\n\tstd::optional<ChatLink> now;\n};\n\nclass ChatLinks final {\npublic:\n\texplicit ChatLinks(not_null<ApiWrap*> api);\n\n\tusing Link = ChatLink;\n\tusing Update = ChatLinkUpdate;\n\n\tvoid create(\n\t\tconst QString &title,\n\t\tconst TextWithEntities &message,\n\t\tFn<void(Link)> done = nullptr);\n\tvoid edit(\n\t\tconst QString &link,\n\t\tconst QString &title,\n\t\tconst TextWithEntities &message,\n\t\tFn<void(Link)> done = nullptr);\n\tvoid destroy(\n\t\tconst QString &link,\n\t\tFn<void()> done = nullptr);\n\n\tvoid preload();\n\t[[nodiscard]] const std::vector<ChatLink> &list() const;\n\t[[nodiscard]] bool loaded() const;\n\t[[nodiscard]] rpl::producer<> loadedUpdates() const;\n\t[[nodiscard]] rpl::producer<Update> updates() const;\n\nprivate:\n\tconst not_null<ApiWrap*> _api;\n\n\tstd::vector<Link> _list;\n\trpl::event_stream<> _loadedUpdates;\n\tmtpRequestId _requestId = 0;\n\tbool _loaded = false;\n\n\trpl::event_stream<Update> _updates;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_chat_participants.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_chat_participants.h\"\n\n#include \"apiwrap.h\"\n#include \"boxes/add_contact_box.h\" // ShowAddParticipantsError\n#include \"boxes/peers/add_participants_box.h\" // ChatInviteForbidden\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_channel_admins.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/mtproto_config.h\"\n\nnamespace Api {\nnamespace {\n\nusing Members = ChatParticipants::Members;\n\nconstexpr auto kSmallDelayMs = crl::time(5);\n\n// 1 second wait before reload members in channel after adding.\nconstexpr auto kReloadChannelMembersTimeout = 1000;\n\n// Max users in one super group invite request.\nconstexpr auto kMaxUsersPerInvite = 100;\n\n// How many messages from chat history server should forward to user,\n// that was added to this chat.\nconstexpr auto kForwardMessagesOnAdd = 100;\n\nstd::vector<ChatParticipant> ParseList(\n\t\tconst ChatParticipants::TLMembers &data,\n\t\tnot_null<PeerData*> peer) {\n\treturn ranges::views::all(\n\t\tdata.vparticipants().v\n\t) | ranges::views::transform([&](const MTPChannelParticipant &p) {\n\t\treturn ChatParticipant(p, peer);\n\t}) | ranges::to_vector;\n}\n\nvoid ApplyMegagroupAdmins(not_null<ChannelData*> channel, Members list) {\n\tExpects(channel->isMegagroup());\n\n\tconst auto creatorIt = ranges::find_if(\n\t\tlist,\n\t\t&Api::ChatParticipant::isCreator);\n\n\tauto adding = base::flat_set<UserId>();\n\tauto addingRanks = base::flat_map<UserId, QString>();\n\tfor (const auto &p : list) {\n\t\tif (p.isUser()) {\n\t\t\tadding.emplace(p.userId());\n\t\t\tif (!p.rank().isEmpty()) {\n\t\t\t\taddingRanks.emplace(p.userId(), p.rank());\n\t\t\t}\n\t\t}\n\t}\n\tif (creatorIt != list.end() && creatorIt->isUser()) {\n\t\tadding.emplace(creatorIt->userId());\n\t\tif (!creatorIt->rank().isEmpty()) {\n\t\t\taddingRanks.emplace(creatorIt->userId(), creatorIt->rank());\n\t\t}\n\t}\n\tauto removing = channel->mgInfo->admins;\n\tif (removing.empty() && adding.empty()) {\n\t\tLOG((\"API Error: Got empty admins list from server.\"));\n\t\tadding.emplace(UserId(0));\n\t}\n\n\tData::ChannelAdminChanges changes(channel);\n\tif (creatorIt != list.end()) {\n\t\tcreatorIt->tryApplyCreatorTo(channel);\n\t} else {\n\t\tchannel->mgInfo->creator = nullptr;\n\t}\n\tfor (const auto &addingId : adding) {\n\t\tconst auto r = addingRanks.find(addingId);\n\t\tconst auto rank = (r != end(addingRanks))\n\t\t\t? r->second\n\t\t\t: QString();\n\t\tremoving.remove(addingId);\n\t\tchanges.add(addingId, rank);\n\t}\n\tfor (const auto &removingId : removing) {\n\t\tchanges.remove(removingId);\n\t}\n}\n\nvoid RefreshChannelAdmins(\n\t\tnot_null<ChannelData*> channel,\n\t\tMembers participants) {\n\tData::ChannelAdminChanges changes(channel);\n\tfor (const auto &p : participants) {\n\t\tif (p.isUser()) {\n\t\t\tif (p.isCreatorOrAdmin()) {\n\t\t\t\tp.tryApplyCreatorTo(channel);\n\t\t\t\tchanges.add(p.userId(), p.rank());\n\t\t\t} else {\n\t\t\t\tchanges.remove(p.userId());\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ApplyLastList(\n\t\tnot_null<ChannelData*> channel,\n\t\tint availableCount,\n\t\tMembers list) {\n\tchannel->mgInfo->lastAdmins.clear();\n\tchannel->mgInfo->lastRestricted.clear();\n\tchannel->mgInfo->lastParticipants.clear();\n\tchannel->mgInfo->memberRanks.clear();\n\tchannel->mgInfo->lastParticipantsStatus\n\t\t= MegagroupInfo::LastParticipantsUpToDate\n\t\t\t| MegagroupInfo::LastParticipantsOnceReceived;\n\n\tauto botStatus = channel->mgInfo->botStatus;\n\tfor (const auto &p : list) {\n\t\tconst auto participant = channel->owner().peer(p.id());\n\t\tconst auto user = participant->asUser();\n\t\tconst auto adminRights = p.rights();\n\t\tconst auto restrictedRights = p.restrictions();\n\t\tif (p.isCreator()) {\n\t\t\tAssert(user != nullptr);\n\t\t\tp.tryApplyCreatorTo(channel);\n\t\t\tif (!channel->mgInfo->admins.empty()) {\n\t\t\t\tData::ChannelAdminChanges(channel).add(p.userId(), p.rank());\n\t\t\t}\n\t\t}\n\t\tif (user\n\t\t\t&& !base::contains(channel->mgInfo->lastParticipants, user)) {\n\t\t\tchannel->mgInfo->lastParticipants.push_back(user);\n\t\t\tif (adminRights.flags) {\n\t\t\t\tchannel->mgInfo->lastAdmins.emplace(\n\t\t\t\t\tuser,\n\t\t\t\t\tMegagroupInfo::Admin{ adminRights, p.canBeEdited() });\n\t\t\t} else if (restrictedRights.flags) {\n\t\t\t\tchannel->mgInfo->lastRestricted.emplace(\n\t\t\t\t\tuser,\n\t\t\t\t\tMegagroupInfo::Restricted{ restrictedRights });\n\t\t\t}\n\t\t\tif (user->isBot()) {\n\t\t\t\tchannel->mgInfo->bots.insert(user);\n\t\t\t\tif (channel->mgInfo->botStatus == Data::BotStatus::NoBots) {\n\t\t\t\t\tchannel->mgInfo->botStatus = Data::BotStatus::HasBots;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!p.rank().isEmpty()) {\n\t\t\t\tchannel->mgInfo->memberRanks[p.userId()] = p.rank();\n\t\t\t}\n\t\t}\n\t}\n\t//\n\t// getParticipants(Recent) sometimes can't return all members,\n\t// only some last subset, size of this subset is availableCount.\n\t//\n\t// So both list size and availableCount have nothing to do with\n\t// the full supergroup members count.\n\t//\n\t//if (list.isEmpty()) {\n\t//\tchannel->setMembersCount(channel->mgInfo->lastParticipants.size());\n\t//} else {\n\t//\tchannel->setMembersCount(availableCount);\n\t//}\n\tchannel->session().changes().peerUpdated(\n\t\tchannel,\n\t\t(Data::PeerUpdate::Flag::Members | Data::PeerUpdate::Flag::Admins));\n\n\tchannel->mgInfo->botStatus = botStatus;\n\tchannel->session().changes().peerUpdated(\n\t\tchannel,\n\t\tData::PeerUpdate::Flag::FullInfo);\n}\n\nvoid ApplyBotsList(\n\t\tnot_null<ChannelData*> channel,\n\t\tint availableCount,\n\t\tMembers list) {\n\tconst auto history = channel->owner().historyLoaded(channel);\n\tchannel->mgInfo->bots.clear();\n\tchannel->mgInfo->botStatus = Data::BotStatus::NoBots;\n\n\tauto needBotsInfos = false;\n\tauto botStatus = channel->mgInfo->botStatus;\n\tauto keyboardBotFound = !history || !history->lastKeyboardFrom;\n\tfor (const auto &p : list) {\n\t\tconst auto participant = channel->owner().peer(p.id());\n\t\tconst auto user = participant->asUser();\n\t\tif (user && user->isBot()) {\n\t\t\tchannel->mgInfo->bots.insert(user);\n\t\t\tbotStatus = Data::BotStatus::HasBots;\n\t\t\tif (!user->botInfo->inited) {\n\t\t\t\tneedBotsInfos = true;\n\t\t\t}\n\t\t}\n\t\tif (!keyboardBotFound\n\t\t\t&& participant->id == history->lastKeyboardFrom) {\n\t\t\tkeyboardBotFound = true;\n\t\t}\n\t}\n\tif (needBotsInfos) {\n\t\tchannel->session().api().requestFullPeer(channel);\n\t}\n\tif (!keyboardBotFound) {\n\t\thistory->clearLastKeyboard();\n\t}\n\n\tchannel->mgInfo->botStatus = botStatus;\n\tchannel->session().changes().peerUpdated(\n\t\tchannel,\n\t\tData::PeerUpdate::Flag::FullInfo);\n}\n\n[[nodiscard]] ChatParticipants::Peers ParseSimilarChannels(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPmessages_Chats &chats) {\n\tauto result = ChatParticipants::Peers();\n\tchats.match([&](const auto &data) {\n\t\tconst auto &list = data.vchats().v;\n\t\tresult.list.reserve(list.size());\n\t\tfor (const auto &chat : list) {\n\t\t\tconst auto peer = session->data().processChat(chat);\n\t\t\tif (const auto channel = peer->asChannel()) {\n\t\t\t\tresult.list.push_back(channel);\n\t\t\t}\n\t\t}\n\t\tif constexpr (MTPDmessages_chatsSlice::Is<decltype(data)>()) {\n\t\t\tif (session->premiumPossible()) {\n\t\t\t\tresult.more = data.vcount().v - data.vchats().v.size();\n\t\t\t}\n\t\t}\n\t});\n\treturn result;\n}\n\n[[nodiscard]] ChatParticipants::Peers ParseSimilarChannels(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst MTPmessages_Chats &chats) {\n\treturn ParseSimilarChannels(&channel->session(), chats);\n}\n\n[[nodiscard]] ChatParticipants::Peers ParseSimilarBots(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPusers_Users &users) {\n\tauto result = ChatParticipants::Peers();\n\tusers.match([&](const auto &data) {\n\t\tconst auto &list = data.vusers().v;\n\t\tresult.list.reserve(list.size());\n\t\tfor (const auto &user : list) {\n\t\t\tresult.list.push_back(session->data().processUser(user));\n\t\t}\n\t\tif constexpr (MTPDusers_usersSlice::Is<decltype(data)>()) {\n\t\t\tif (session->premiumPossible()) {\n\t\t\t\tresult.more = data.vcount().v - data.vusers().v.size();\n\t\t\t}\n\t\t}\n\t});\n\treturn result;\n}\n\n} // namespace\n\nChatParticipant::ChatParticipant(\n\t\tconst MTPChannelParticipant &p,\n\t\tnot_null<PeerData*> peer) {\n\t_peer = p.match([](const MTPDchannelParticipantBanned &data) {\n\t\treturn peerFromMTP(data.vpeer());\n\t}, [](const MTPDchannelParticipantLeft &data) {\n\t\treturn peerFromMTP(data.vpeer());\n\t}, [](const auto &data) {\n\t\treturn peerFromUser(data.vuser_id());\n\t});\n\n\tp.match([&](const MTPDchannelParticipantCreator &data) {\n\t\t_canBeEdited = (peer->session().userPeerId() == _peer);\n\t\t_type = Type::Creator;\n\t\t_rights = ChatAdminRightsInfo(data.vadmin_rights());\n\t\t_rank = qs(data.vrank().value_or_empty());\n\t}, [&](const MTPDchannelParticipantAdmin &data) {\n\t\t_canBeEdited = data.is_can_edit();\n\t\t_type = Type::Admin;\n\t\t_rank = qs(data.vrank().value_or_empty());\n\t\t_rights = ChatAdminRightsInfo(data.vadmin_rights());\n\t\t_by = peerToUser(peerFromUser(data.vpromoted_by()));\n\t\t_date = data.vdate().v;\n\t}, [&](const MTPDchannelParticipantSelf &data) {\n\t\t_type = Type::Member;\n\t\t_date = data.vdate().v;\n\t\t_by = peerToUser(peerFromUser(data.vinviter_id()));\n\t\t_rank = qs(data.vrank().value_or_empty());\n\t\tif (data.vsubscription_until_date()) {\n\t\t\t_subscriptionDate = data.vsubscription_until_date()->v;\n\t\t}\n\t}, [&](const MTPDchannelParticipant &data) {\n\t\t_type = Type::Member;\n\t\t_date = data.vdate().v;\n\t\t_rank = qs(data.vrank().value_or_empty());\n\t\tif (data.vsubscription_until_date()) {\n\t\t\t_subscriptionDate = data.vsubscription_until_date()->v;\n\t\t}\n\t}, [&](const MTPDchannelParticipantBanned &data) {\n\t\t_restrictions = ChatRestrictionsInfo(data.vbanned_rights());\n\t\t_by = peerToUser(peerFromUser(data.vkicked_by()));\n\t\t_date = data.vdate().v;\n\t\t_rank = qs(data.vrank().value_or_empty());\n\n\t\t_type = (_restrictions.flags & ChatRestriction::ViewMessages)\n\t\t\t? Type::Banned\n\t\t\t: Type::Restricted;\n\t}, [&](const MTPDchannelParticipantLeft &data) {\n\t\t_type = Type::Left;\n\t});\n}\n\nChatParticipant::ChatParticipant(\n\tType type,\n\tPeerId peerId,\n\tUserId by,\n\tChatRestrictionsInfo restrictions,\n\tChatAdminRightsInfo rights,\n\tbool canBeEdited,\n\tQString rank)\n: _type(type)\n, _peer(peerId)\n, _by(by)\n, _canBeEdited(canBeEdited)\n, _rank(rank)\n, _restrictions(std::move(restrictions))\n, _rights(std::move(rights)) {\n}\n\nvoid ChatParticipant::tryApplyCreatorTo(\n\t\tnot_null<ChannelData*> channel) const {\n\tif (isCreator() && isUser()) {\n\t\tif (const auto info = channel->mgInfo.get()) {\n\t\t\tinfo->creator = channel->owner().userLoaded(userId());\n\t\t\tif (!rank().isEmpty()) {\n\t\t\t\tinfo->memberRanks[userId()] = rank();\n\t\t\t} else {\n\t\t\t\tinfo->memberRanks.remove(userId());\n\t\t\t}\n\t\t}\n\t}\n}\n\nbool ChatParticipant::isUser() const {\n\treturn peerIsUser(_peer);\n}\n\nbool ChatParticipant::isCreator() const {\n\treturn _type == Type::Creator;\n}\n\nbool ChatParticipant::isCreatorOrAdmin() const {\n\treturn _type == Type::Creator || _type == Type::Admin;\n}\n\nbool ChatParticipant::isKicked() const {\n\treturn _type == Type::Banned;\n}\n\nbool ChatParticipant::canBeEdited() const {\n\treturn _canBeEdited;\n}\n\nUserId ChatParticipant::by() const {\n\treturn _by;\n}\n\nPeerId ChatParticipant::id() const {\n\treturn _peer;\n}\n\nUserId ChatParticipant::userId() const {\n\treturn peerToUser(_peer);\n}\n\nChatRestrictionsInfo ChatParticipant::restrictions() const {\n\treturn _restrictions;\n}\n\nChatAdminRightsInfo ChatParticipant::rights() const {\n\treturn _rights;\n}\n\nTimeId ChatParticipant::subscriptionDate() const {\n\treturn _subscriptionDate;\n}\n\nTimeId ChatParticipant::promotedSince() const {\n\treturn (_type == Type::Admin) ? _date : TimeId(0);\n}\n\nTimeId ChatParticipant::restrictedSince() const {\n\treturn (_type == Type::Restricted || _type == Type::Banned)\n\t\t? _date\n\t\t: TimeId(0);\n}\n\nTimeId ChatParticipant::memberSince() const {\n\treturn (_type == Type::Member) ? _date : TimeId(0);\n}\n\nChatParticipant::Type ChatParticipant::type() const {\n\treturn _type;\n}\n\nQString ChatParticipant::rank() const {\n\treturn _rank;\n}\n\nChatParticipants::ChatParticipants(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance()) {\n}\n\nvoid ChatParticipants::requestForAdd(\n\t\tnot_null<ChannelData*> channel,\n\t\tFn<void(const TLMembers&)> callback) {\n\tExpects(callback != nullptr);\n\t_forAdd.callback = std::move(callback);\n\tif (_forAdd.channel == channel) {\n\t\treturn;\n\t}\n\t_api.request(base::take(_forAdd.requestId)).cancel();\n\n\tconst auto offset = 0;\n\tconst auto participantsHash = uint64(0);\n\n\t_forAdd.channel = channel;\n\t_forAdd.requestId = _api.request(MTPchannels_GetParticipants(\n\t\tchannel->inputChannel(),\n\t\tMTP_channelParticipantsRecent(),\n\t\tMTP_int(offset),\n\t\tMTP_int(channel->session().serverConfig().chatSizeMax),\n\t\tMTP_long(participantsHash)\n\t)).done([=](const MTPchannels_ChannelParticipants &result) {\n\t\tresult.match([&](const MTPDchannels_channelParticipants &data) {\n\t\t\tbase::take(_forAdd).callback(data);\n\t\t}, [&](const MTPDchannels_channelParticipantsNotModified &) {\n\t\t\tbase::take(_forAdd);\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"channels.channelParticipantsNotModified received!\"));\n\t\t});\n\t}).fail([=] {\n\t\tbase::take(_forAdd);\n\t}).send();\n}\n\nvoid ChatParticipants::requestLast(not_null<ChannelData*> channel) {\n\tif (!channel->isMegagroup()\n\t\t|| !channel->canViewMembers()\n\t\t|| _participantsRequests.contains(channel)) {\n\t\treturn;\n\t}\n\n\tconst auto offset = 0;\n\tconst auto participantsHash = uint64(0);\n\tconst auto requestId = _api.request(MTPchannels_GetParticipants(\n\t\tchannel->inputChannel(),\n\t\tMTP_channelParticipantsRecent(),\n\t\tMTP_int(offset),\n\t\tMTP_int(channel->session().serverConfig().chatSizeMax),\n\t\tMTP_long(participantsHash)\n\t)).done([=](const MTPchannels_ChannelParticipants &result) {\n\t\t_participantsRequests.remove(channel);\n\n\t\tresult.match([&](const MTPDchannels_channelParticipants &data) {\n\t\t\tconst auto &[availableCount, list] = Parse(channel, data);\n\t\t\tApplyLastList(channel, availableCount, list);\n\t\t}, [](const MTPDchannels_channelParticipantsNotModified &) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"channels.channelParticipantsNotModified received!\"));\n\t\t});\n\t}).fail([this, channel] {\n\t\t_participantsRequests.remove(channel);\n\t}).send();\n\n\t_participantsRequests[channel] = requestId;\n}\n\nvoid ChatParticipants::requestBots(not_null<ChannelData*> channel) {\n\tif (!channel->isMegagroup() || _botsRequests.contains(channel)) {\n\t\treturn;\n\t}\n\n\tconst auto offset = 0;\n\tconst auto participantsHash = uint64(0);\n\tconst auto requestId = _api.request(MTPchannels_GetParticipants(\n\t\tchannel->inputChannel(),\n\t\tMTP_channelParticipantsBots(),\n\t\tMTP_int(offset),\n\t\tMTP_int(channel->session().serverConfig().chatSizeMax),\n\t\tMTP_long(participantsHash)\n\t)).done([=](const MTPchannels_ChannelParticipants &result) {\n\t\t_botsRequests.remove(channel);\n\t\tresult.match([&](const MTPDchannels_channelParticipants &data) {\n\t\t\tconst auto &[availableCount, list] = Parse(channel, data);\n\t\t\tApplyBotsList(channel, availableCount, list);\n\t\t}, [](const MTPDchannels_channelParticipantsNotModified &) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"channels.channelParticipantsNotModified received!\"));\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\t_botsRequests.remove(channel);\n\t\tif (error.type() == u\"CHANNEL_MONOFORUM_UNSUPPORTED\"_q) {\n\t\t\tchannel->mgInfo->bots.clear();\n\t\t\tchannel->mgInfo->botStatus = Data::BotStatus::NoBots;\n\t\t\tchannel->session().changes().peerUpdated(\n\t\t\t\tchannel,\n\t\t\t\tData::PeerUpdate::Flag::FullInfo);\n\t\t}\n\t}).send();\n\n\t_botsRequests[channel] = requestId;\n}\n\nvoid ChatParticipants::requestAdmins(not_null<ChannelData*> channel) {\n\tif (!channel->isMegagroup() || _adminsRequests.contains(channel)) {\n\t\treturn;\n\t}\n\n\tconst auto offset = 0;\n\tconst auto participantsHash = uint64(0);\n\tconst auto requestId = _api.request(MTPchannels_GetParticipants(\n\t\tchannel->inputChannel(),\n\t\tMTP_channelParticipantsAdmins(),\n\t\tMTP_int(offset),\n\t\tMTP_int(channel->session().serverConfig().chatSizeMax),\n\t\tMTP_long(participantsHash)\n\t)).done([=](const MTPchannels_ChannelParticipants &result) {\n\t\tchannel->mgInfo->adminsLoaded = true;\n\t\t_adminsRequests.remove(channel);\n\t\tresult.match([&](const MTPDchannels_channelParticipants &data) {\n\t\t\tchannel->owner().processUsers(data.vusers());\n\t\t\tApplyMegagroupAdmins(channel, ParseList(data, channel));\n\t\t}, [](const MTPDchannels_channelParticipantsNotModified &) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"channels.channelParticipantsNotModified received!\"));\n\t\t});\n\t}).fail([=] {\n\t\tchannel->mgInfo->adminsLoaded = true;\n\t\t_adminsRequests.remove(channel);\n\t}).send();\n\n\t_adminsRequests[channel] = requestId;\n}\n\nvoid ChatParticipants::requestCountDelayed(\n\t\tnot_null<ChannelData*> channel) {\n\t_participantsCountRequestTimer.call(\n\t\tkReloadChannelMembersTimeout,\n\t\t[=] { channel->updateFullForced(); });\n}\n\nvoid ChatParticipants::add(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tconst std::vector<not_null<UserData*>> &users,\n\t\tbool passGroupHistory,\n\t\tFn<void(bool)> done) {\n\tif (const auto chat = peer->asChat()) {\n\t\tfor (const auto &user : users) {\n\t\t\t_api.request(MTPmessages_AddChatUser(\n\t\t\t\tchat->inputChat(),\n\t\t\t\tuser->inputUser(),\n\t\t\t\tMTP_int(passGroupHistory ? kForwardMessagesOnAdd : 0)\n\t\t\t)).done([=](const MTPmessages_InvitedUsers &result) {\n\t\t\t\tconst auto &data = result.data();\n\t\t\t\tchat->session().api().applyUpdates(data.vupdates());\n\t\t\t\tif (done) done(true);\n\t\t\t\tChatInviteForbidden(\n\t\t\t\t\tshow,\n\t\t\t\t\tchat,\n\t\t\t\t\tCollectForbiddenUsers(&chat->session(), result));\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tconst auto type = error.type();\n\t\t\t\tShowAddParticipantsError(show, type, peer, user);\n\t\t\t\tif (done) done(false);\n\t\t\t}).afterDelay(kSmallDelayMs).send();\n\t\t}\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tconst auto hasBot = ranges::any_of(users, &UserData::isBot);\n\t\tif (!peer->isMegagroup() && hasBot) {\n\t\t\tShowAddParticipantsError(\n\t\t\t\tshow,\n\t\t\t\tu\"USER_BOT\"_q,\n\t\t\t\tpeer,\n\t\t\t\t{ .users = users });\n\t\t\treturn;\n\t\t}\n\t\tauto list = QVector<MTPInputUser>();\n\t\tlist.reserve(std::min(int(users.size()), int(kMaxUsersPerInvite)));\n\t\tconst auto send = [&] {\n\t\t\tconst auto callback = base::take(done);\n\t\t\t_api.request(MTPchannels_InviteToChannel(\n\t\t\t\tchannel->inputChannel(),\n\t\t\t\tMTP_vector<MTPInputUser>(list)\n\t\t\t)).done([=](const MTPmessages_InvitedUsers &result) {\n\t\t\t\tconst auto &data = result.data();\n\t\t\t\tchannel->session().api().applyUpdates(data.vupdates());\n\t\t\t\trequestCountDelayed(channel);\n\t\t\t\tif (callback) callback(true);\n\t\t\t\tChatInviteForbidden(\n\t\t\t\t\tshow,\n\t\t\t\t\tchannel,\n\t\t\t\t\tCollectForbiddenUsers(&channel->session(), result));\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tShowAddParticipantsError(show, error.type(), peer, {\n\t\t\t\t\t.users = users,\n\t\t\t\t});\n\t\t\t\tif (callback) callback(false);\n\t\t\t}).afterDelay(kSmallDelayMs).send();\n\t\t};\n\t\tfor (const auto &user : users) {\n\t\t\tlist.push_back(user->inputUser());\n\t\t\tif (list.size() == kMaxUsersPerInvite) {\n\t\t\t\tsend();\n\t\t\t\tlist.clear();\n\t\t\t}\n\t\t}\n\t\tif (!list.empty()) {\n\t\t\tsend();\n\t\t}\n\t} else {\n\t\tUnexpected(\"User in ChatParticipants::add.\");\n\t}\n}\n\nChatParticipants::Parsed ChatParticipants::Parse(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst TLMembers &data) {\n\tchannel->owner().processUsers(data.vusers());\n\tchannel->owner().processChats(data.vchats());\n\tauto list = ParseList(data, channel);\n\tif (channel->mgInfo) {\n\t\tRefreshChannelAdmins(channel, list);\n\t}\n\treturn { data.vcount().v, std::move(list) };\n}\n\nChatParticipants::Parsed ChatParticipants::ParseRecent(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst TLMembers &data) {\n\tconst auto result = Parse(channel, data);\n\tconst auto applyLast = channel->isMegagroup()\n\t\t&& channel->canViewMembers()\n\t\t&& (channel->mgInfo->lastParticipants.size() <= result.list.size());\n\tif (applyLast) {\n\t\tApplyLastList(channel, result.availableCount, result.list);\n\t}\n\treturn result;\n}\n\nvoid ChatParticipants::Restrict(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo oldRights,\n\t\tChatRestrictionsInfo newRights,\n\t\tFn<void()> onDone,\n\t\tFn<void(const QString&)> onFail) {\n\tchannel->session().api().request(MTPchannels_EditBanned(\n\t\tchannel->inputChannel(),\n\t\tparticipant->input(),\n\t\tRestrictionsToMTP(newRights)\n\t)).done([=](const MTPUpdates &result) {\n\t\tchannel->session().api().applyUpdates(result);\n\t\tchannel->applyEditBanned(participant, oldRights, newRights);\n\t\tif (onDone) {\n\t\t\tonDone();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (onFail) {\n\t\t\tonFail(error.type());\n\t\t}\n\t}).send();\n}\n\nvoid ChatParticipants::requestSelf(not_null<ChannelData*> channel) {\n\tif (_selfParticipantRequests.contains(channel)) {\n\t\treturn;\n\t}\n\n\tconst auto finalize = [=](\n\t\t\tUserId inviter = -1,\n\t\t\tTimeId inviteDate = 0,\n\t\t\tbool inviteViaRequest = false) {\n\t\tchannel->inviter = inviter;\n\t\tchannel->inviteDate = inviteDate;\n\t\tchannel->inviteViaRequest = inviteViaRequest;\n\t\tif (const auto history = channel->owner().historyLoaded(channel)) {\n\t\t\tif (history->lastMessageKnown()) {\n\t\t\t\thistory->checkLocalMessages();\n\t\t\t\thistory->owner().sendHistoryChangeNotifications();\n\t\t\t} else {\n\t\t\t\thistory->owner().histories().requestDialogEntry(history);\n\t\t\t}\n\t\t}\n\t};\n\t_selfParticipantRequests.emplace(channel);\n\t_api.request(MTPchannels_GetParticipant(\n\t\tchannel->inputChannel(),\n\t\tMTP_inputPeerSelf()\n\t)).done([=](const MTPchannels_ChannelParticipant &result) {\n\t\t_selfParticipantRequests.erase(channel);\n\t\tresult.match([&](const MTPDchannels_channelParticipant &data) {\n\t\t\tchannel->owner().processUsers(data.vusers());\n\n\t\t\tconst auto &participant = data.vparticipant();\n\t\t\tparticipant.match([&](const MTPDchannelParticipantSelf &data) {\n\t\t\t\tfinalize(\n\t\t\t\t\tdata.vinviter_id().v,\n\t\t\t\t\tdata.vdate().v,\n\t\t\t\t\tdata.is_via_request());\n\t\t\t}, [&](const MTPDchannelParticipantCreator &) {\n\t\t\t\tif (channel->mgInfo) {\n\t\t\t\t\tchannel->mgInfo->creator = channel->session().user();\n\t\t\t\t}\n\t\t\t\tfinalize(channel->session().userId(), channel->date);\n\t\t\t}, [&](const MTPDchannelParticipantAdmin &data) {\n\t\t\t\tconst auto inviter = data.is_self()\n\t\t\t\t\t? data.vinviter_id().value_or(-1)\n\t\t\t\t\t: -1;\n\t\t\t\tfinalize(inviter, data.vdate().v);\n\t\t\t}, [&](const MTPDchannelParticipantBanned &data) {\n\t\t\t\tLOG((\"API Error: Got self banned participant.\"));\n\t\t\t\tfinalize();\n\t\t\t}, [&](const MTPDchannelParticipant &data) {\n\t\t\t\tLOG((\"API Error: Got self regular participant.\"));\n\t\t\t\tfinalize();\n\t\t\t}, [&](const MTPDchannelParticipantLeft &data) {\n\t\t\t\tLOG((\"API Error: Got self left participant.\"));\n\t\t\t\tfinalize();\n\t\t\t});\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\t_selfParticipantRequests.erase(channel);\n\t\tif (error.type() == u\"CHANNEL_PRIVATE\"_q) {\n\t\t\tchannel->privateErrorReceived();\n\t\t}\n\t\tfinalize();\n\t}).afterDelay(kSmallDelayMs).send();\n}\n\nvoid ChatParticipants::kick(\n\t\tnot_null<ChatData*> chat,\n\t\tnot_null<PeerData*> participant) {\n\tExpects(participant->isUser());\n\n\t_api.request(MTPmessages_DeleteChatUser(\n\t\tMTP_flags(0),\n\t\tchat->inputChat(),\n\t\tparticipant->asUser()->inputUser()\n\t)).done([=](const MTPUpdates &result) {\n\t\tchat->session().api().applyUpdates(result);\n\t}).send();\n}\n\nvoid ChatParticipants::kick(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo currentRights) {\n\tconst auto kick = KickRequest(channel, participant);\n\tif (_kickRequests.contains(kick)) return;\n\n\tconst auto rights = ChannelData::KickedRestrictedRights(participant);\n\tconst auto requestId = _api.request(MTPchannels_EditBanned(\n\t\tchannel->inputChannel(),\n\t\tparticipant->input(),\n\t\tRestrictionsToMTP(rights)\n\t)).done([=](const MTPUpdates &result) {\n\t\tchannel->session().api().applyUpdates(result);\n\n\t\t_kickRequests.remove(KickRequest(channel, participant));\n\t\tchannel->applyEditBanned(participant, currentRights, rights);\n\t}).fail([this, kick] {\n\t\t_kickRequests.remove(kick);\n\t}).send();\n\n\t_kickRequests.emplace(kick, requestId);\n}\n\nvoid ChatParticipants::unblock(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<PeerData*> participant) {\n\tconst auto kick = KickRequest(channel, participant);\n\tif (_kickRequests.contains(kick)) {\n\t\treturn;\n\t}\n\n\tconst auto requestId = _api.request(MTPchannels_EditBanned(\n\t\tchannel->inputChannel(),\n\t\tparticipant->input(),\n\t\tMTP_chatBannedRights(MTP_flags(0), MTP_int(0))\n\t)).done([=](const MTPUpdates &result) {\n\t\tchannel->session().api().applyUpdates(result);\n\n\t\t_kickRequests.remove(KickRequest(channel, participant));\n\t\tif (channel->kickedCount() > 0) {\n\t\t\tchannel->setKickedCount(channel->kickedCount() - 1);\n\t\t} else {\n\t\t\tchannel->updateFullForced();\n\t\t}\n\t}).fail([=] {\n\t\t_kickRequests.remove(kick);\n\t}).send();\n\n\t_kickRequests.emplace(kick, requestId);\n}\n\nvoid ChatParticipants::loadSimilarPeers(not_null<PeerData*> peer) {\n\tif (const auto i = _similar.find(peer); i != end(_similar)) {\n\t\tif (i->second.requestId\n\t\t\t|| !i->second.peers.more\n\t\t\t|| !peer->session().premium()) {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (const auto channel = peer->asBroadcast()) {\n\t\tusing Flag = MTPchannels_GetChannelRecommendations::Flag;\n\t\t_similar[peer].requestId = _api.request(\n\t\t\tMTPchannels_GetChannelRecommendations(\n\t\t\t\tMTP_flags(Flag::f_channel),\n\t\t\t\tchannel->inputChannel())\n\t\t).done([=](const MTPmessages_Chats &result) {\n\t\t\tauto &similar = _similar[channel];\n\t\t\tsimilar.requestId = 0;\n\t\t\tauto parsed = ParseSimilarChannels(channel, result);\n\t\t\tif (similar.peers == parsed) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsimilar.peers = std::move(parsed);\n\t\t\tif (const auto history = channel->owner().historyLoaded(channel)) {\n\t\t\t\tif (const auto item = history->joinedMessageInstance()) {\n\t\t\t\t\thistory->owner().requestItemResize(item);\n\t\t\t\t}\n\t\t\t}\n\t\t\t_similarLoaded.fire_copy(channel);\n\t\t}).send();\n\t} else if (const auto bot = peer->asBot()) {\n\t\t_similar[peer].requestId = _api.request(\n\t\t\tMTPbots_GetBotRecommendations(bot->inputUser())\n\t\t).done([=](const MTPusers_Users &result) {\n\t\t\tauto &similar = _similar[peer];\n\t\t\tsimilar.requestId = 0;\n\t\t\tauto parsed = ParseSimilarBots(&peer->session(), result);\n\t\t\tif (similar.peers == parsed) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsimilar.peers = std::move(parsed);\n\t\t\t_similarLoaded.fire_copy(peer);\n\t\t}).send();\n\t}\n}\n\nauto ChatParticipants::similar(not_null<PeerData*> peer)\n-> const Peers & {\n\tconst auto i = (peer->isBroadcast() || peer->isBot())\n\t\t? _similar.find(peer)\n\t\t: end(_similar);\n\tif (i != end(_similar)) {\n\t\treturn i->second.peers;\n\t}\n\tstatic const auto empty = Peers();\n\treturn empty;\n}\n\nauto ChatParticipants::similarLoaded() const\n-> rpl::producer<not_null<PeerData*>> {\n\treturn _similarLoaded.events();\n}\n\nvoid ChatParticipants::loadRecommendations() {\n\tif (_recommendationsLoaded.current() || _recommendations.requestId) {\n\t\treturn;\n\t}\n\t_recommendations.requestId = _api.request(\n\t\tMTPchannels_GetChannelRecommendations(\n\t\t\tMTP_flags(0),\n\t\t\tMTP_inputChannelEmpty())\n\t).done([=](const MTPmessages_Chats &result) {\n\t\t_recommendations.requestId = 0;\n\t\tauto parsed = ParseSimilarChannels(_session, result);\n\t\t_recommendations.peers = std::move(parsed);\n\t\t_recommendations.peers.more = 0;\n\t\t_recommendationsLoaded = true;\n\t}).send();\n}\n\nconst ChatParticipants::Peers &ChatParticipants::recommendations() const {\n\treturn _recommendations.peers;\n}\n\nrpl::producer<> ChatParticipants::recommendationsLoaded() const {\n\treturn _recommendationsLoaded.changes() | rpl::to_empty;\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_chat_participants.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_chat_participant_status.h\"\n#include \"mtproto/sender.h\"\n#include \"base/timer.h\"\n\nclass ApiWrap;\nclass ChannelData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass Show;\n} // namespace Ui\n\nnamespace Api {\n\nclass ChatParticipant final {\npublic:\n\tenum class Type {\n\t\tCreator,\n\t\tAdmin,\n\t\tMember,\n\t\tRestricted,\n\t\tLeft,\n\t\tBanned,\n\t};\n\n\texplicit ChatParticipant(\n\t\tconst MTPChannelParticipant &p,\n\t\tnot_null<PeerData*> peer);\n\tChatParticipant(\n\t\tType type,\n\t\tPeerId peerId,\n\t\tUserId by,\n\t\tChatRestrictionsInfo restrictions,\n\t\tChatAdminRightsInfo rights,\n\t\tbool canBeEdited = false,\n\t\tQString rank = QString());\n\n\tbool isUser() const;\n\tbool isCreator() const;\n\tbool isCreatorOrAdmin() const;\n\tbool isKicked() const;\n\tbool canBeEdited() const;\n\n\tUserId by() const;\n\tPeerId id() const;\n\tUserId userId() const;\n\n\tChatRestrictionsInfo restrictions() const;\n\tChatAdminRightsInfo rights() const;\n\n\tTimeId subscriptionDate() const;\n\tTimeId promotedSince() const;\n\tTimeId restrictedSince() const;\n\tTimeId memberSince() const;\n\n\tType type() const;\n\tQString rank() const;\n\n\tvoid tryApplyCreatorTo(not_null<ChannelData*> channel) const;\nprivate:\n\tType _type = Type::Member;\n\n\tPeerId _peer;\n\tUserId _by; // Banned/Restricted/Promoted.\n\n\tbool _canBeEdited = false;\n\n\tQString _rank;\n\tTimeId _subscriptionDate = 0;\n\tTimeId _date = 0;\n\n\tChatRestrictionsInfo _restrictions;\n\tChatAdminRightsInfo _rights;\n};\n\nclass ChatParticipants final {\npublic:\n\tstruct Parsed {\n\t\tconst int availableCount;\n\t\tconst std::vector<ChatParticipant> list;\n\t};\n\n\tusing TLMembers = MTPDchannels_channelParticipants;\n\tusing Members = const std::vector<ChatParticipant> &;\n\texplicit ChatParticipants(not_null<ApiWrap*> api);\n\n\tvoid requestLast(not_null<ChannelData*> channel);\n\tvoid requestBots(not_null<ChannelData*> channel);\n\tvoid requestAdmins(not_null<ChannelData*> channel);\n\tvoid requestCountDelayed(not_null<ChannelData*> channel);\n\n\tstatic Parsed Parse(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst TLMembers &data);\n\tstatic Parsed ParseRecent(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst TLMembers &data);\n\tstatic void Restrict(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo oldRights,\n\t\tChatRestrictionsInfo newRights,\n\t\tFn<void()> onDone,\n\t\tFn<void(const QString&)> onFail);\n\tvoid add(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tconst std::vector<not_null<UserData*>> &users,\n\t\tbool passGroupHistory = true,\n\t\tFn<void(bool)> done = nullptr);\n\n\tvoid requestSelf(not_null<ChannelData*> channel);\n\n\tvoid requestForAdd(\n\t\tnot_null<ChannelData*> channel,\n\t\tFn<void(const TLMembers&)> callback);\n\n\tvoid kick(\n\t\tnot_null<ChatData*> chat,\n\t\tnot_null<PeerData*> participant);\n\tvoid kick(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo currentRights);\n\tvoid unblock(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<PeerData*> participant);\n\n\tvoid loadSimilarPeers(not_null<PeerData*> peer);\n\n\tstruct Peers {\n\t\tstd::vector<not_null<PeerData*>> list;\n\t\tint more = 0;\n\n\t\tfriend inline bool operator==(\n\t\t\tconst Peers &,\n\t\t\tconst Peers &) = default;\n\t};\n\t[[nodiscard]] const Peers &similar(not_null<PeerData*> peer);\n\t[[nodiscard]] auto similarLoaded() const\n\t\t-> rpl::producer<not_null<PeerData*>>;\n\n\tvoid loadRecommendations();\n\t[[nodiscard]] const Peers &recommendations() const;\n\t[[nodiscard]] rpl::producer<> recommendationsLoaded() const;\n\nprivate:\n\tstruct SimilarPeers {\n\t\tPeers peers;\n\t\tmtpRequestId requestId = 0;\n\t};\n\n\tconst not_null<Main::Session*> _session;\n\n\tMTP::Sender _api;\n\n\tusing PeerRequests = base::flat_map<PeerData*, mtpRequestId>;\n\n\tPeerRequests _participantsRequests;\n\tPeerRequests _botsRequests;\n\tPeerRequests _adminsRequests;\n\tbase::DelayedCallTimer _participantsCountRequestTimer;\n\n\tstruct {\n\t\tChannelData *channel = nullptr;\n\t\tmtpRequestId requestId = 0;\n\t\tFn<void(const TLMembers&)> callback;\n\t} _forAdd;\n\n\tbase::flat_set<not_null<ChannelData*>> _selfParticipantRequests;\n\n\tusing KickRequest = std::pair<\n\t\tnot_null<ChannelData*>,\n\t\tnot_null<PeerData*>>;\n\tbase::flat_map<KickRequest, mtpRequestId> _kickRequests;\n\n\tbase::flat_map<not_null<PeerData*>, SimilarPeers> _similar;\n\trpl::event_stream<not_null<PeerData*>> _similarLoaded;\n\n\tSimilarPeers _recommendations;\n\trpl::variable<bool> _recommendationsLoaded = false;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_cloud_password.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_cloud_password.h\"\n\n#include \"apiwrap.h\"\n#include \"base/random.h\"\n#include \"core/core_cloud_password.h\"\n#include \"passport/passport_encryption.h\"\n\n#include \"base/unixtime.h\"\n#include \"base/call_delayed.h\"\n\nnamespace Api {\nnamespace {\n\n[[nodiscard]] Core::CloudPasswordState ProcessMtpState(\n\t\tconst MTPaccount_password &state) {\n\treturn state.match([&](const MTPDaccount_password &data) {\n\t\tbase::RandomAddSeed(bytes::make_span(data.vsecure_random().v));\n\t\treturn Core::ParseCloudPasswordState(data);\n\t});\n}\n\n} // namespace\n\nCloudPassword::CloudPassword(not_null<ApiWrap*> api)\n: _api(&api->instance()) {\n}\n\nvoid CloudPassword::apply(Core::CloudPasswordState state) {\n\tif (_state) {\n\t\t*_state = std::move(state);\n\t} else {\n\t\t_state = std::make_unique<Core::CloudPasswordState>(std::move(state));\n\t}\n\t_stateChanges.fire_copy(*_state);\n}\n\nvoid CloudPassword::reload() {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\t_requestId = _api.request(MTPaccount_GetPassword(\n\t)).done([=](const MTPaccount_Password &result) {\n\t\t_requestId = 0;\n\t\tapply(ProcessMtpState(result));\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t}).send();\n}\n\nvoid CloudPassword::clearUnconfirmedPassword() {\n\t_requestId = _api.request(MTPaccount_CancelPasswordEmail(\n\t)).done([=] {\n\t\t_requestId = 0;\n\t\treload();\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t\treload();\n\t}).send();\n}\n\nrpl::producer<Core::CloudPasswordState> CloudPassword::state() const {\n\treturn _state\n\t\t? _stateChanges.events_starting_with_copy(*_state)\n\t\t: (_stateChanges.events() | rpl::type_erased);\n}\n\nauto CloudPassword::stateCurrent() const\n-> std::optional<Core::CloudPasswordState> {\n\treturn _state\n\t\t? base::make_optional(*_state)\n\t\t: std::nullopt;\n}\n\nauto CloudPassword::resetPassword()\n-> rpl::producer<CloudPassword::ResetRetryDate, QString> {\n\treturn [=](auto consumer) {\n\t\t_api.request(MTPaccount_ResetPassword(\n\t\t)).done([=](const MTPaccount_ResetPasswordResult &result) {\n\t\t\tresult.match([&](const MTPDaccount_resetPasswordOk &data) {\n\t\t\t\treload();\n\t\t\t}, [&](const MTPDaccount_resetPasswordRequestedWait &data) {\n\t\t\t\tif (!_state) {\n\t\t\t\t\treload();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto until = data.vuntil_date().v;\n\t\t\t\tif (_state->pendingResetDate != until) {\n\t\t\t\t\t_state->pendingResetDate = until;\n\t\t\t\t\t_stateChanges.fire_copy(*_state);\n\t\t\t\t}\n\t\t\t}, [&](const MTPDaccount_resetPasswordFailedWait &data) {\n\t\t\t\tconsumer.put_next_copy(data.vretry_date().v);\n\t\t\t});\n\t\t\tconsumer.put_done();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).send();\n\n\t\treturn rpl::lifetime();\n\t};\n}\n\nauto CloudPassword::cancelResetPassword()\n-> rpl::producer<rpl::no_value, QString> {\n\treturn [=](auto consumer) {\n\t\t_api.request(MTPaccount_DeclinePasswordReset(\n\t\t)).done([=] {\n\t\t\treload();\n\t\t\tconsumer.put_done();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).send();\n\n\t\treturn rpl::lifetime();\n\t};\n}\n\nrpl::producer<CloudPassword::SetOk, QString> CloudPassword::set(\n\t\tconst QString &oldPassword,\n\t\tconst QString &newPassword,\n\t\tconst QString &hint,\n\t\tbool hasRecoveryEmail,\n\t\tconst QString &recoveryEmail) {\n\n\tconst auto generatePasswordCheck = [=](\n\t\t\tconst Core::CloudPasswordState &latestState) {\n\t\tif (oldPassword.isEmpty() || !latestState.hasPassword) {\n\t\t\treturn Core::CloudPasswordResult{\n\t\t\t\tMTP_inputCheckPasswordEmpty()\n\t\t\t};\n\t\t}\n\t\tconst auto hash = Core::ComputeCloudPasswordHash(\n\t\t\tlatestState.mtp.request.algo,\n\t\t\tbytes::make_span(oldPassword.toUtf8()));\n\t\treturn Core::ComputeCloudPasswordCheck(\n\t\t\tlatestState.mtp.request,\n\t\t\thash);\n\t};\n\n\tconst auto finish = [=](auto consumer, int unconfirmedEmailLengthCode) {\n\t\t_api.request(MTPaccount_GetPassword(\n\t\t)).done([=](const MTPaccount_Password &result) {\n\t\t\tapply(ProcessMtpState(result));\n\t\t\tif (unconfirmedEmailLengthCode) {\n\t\t\t\tconsumer.put_next(SetOk{ unconfirmedEmailLengthCode });\n\t\t\t} else {\n\t\t\t\tconsumer.put_done();\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).handleFloodErrors().send();\n\t};\n\n\tconst auto sendMTPaccountUpdatePasswordSettings = [=](\n\t\t\tconst Core::CloudPasswordState &latestState,\n\t\t\tconst QByteArray &secureSecret,\n\t\t\tauto consumer) {\n\t\tconst auto newPasswordBytes = newPassword.toUtf8();\n\t\tconst auto newPasswordHash = Core::ComputeCloudPasswordDigest(\n\t\t\tlatestState.mtp.newPassword,\n\t\t\tbytes::make_span(newPasswordBytes));\n\t\tif (!newPassword.isEmpty() && newPasswordHash.modpow.empty()) {\n\t\t\tconsumer.put_error(\"INTERNAL_SERVER_ERROR\");\n\t\t\treturn;\n\t\t}\n\t\tusing Flag = MTPDaccount_passwordInputSettings::Flag;\n\t\tconst auto flags = Flag::f_new_algo\n\t\t\t| Flag::f_new_password_hash\n\t\t\t| Flag::f_hint\n\t\t\t| (secureSecret.isEmpty() ? Flag(0) : Flag::f_new_secure_settings)\n\t\t\t| ((!hasRecoveryEmail) ? Flag(0) : Flag::f_email);\n\n\t\tauto newSecureSecret = bytes::vector();\n\t\tauto newSecureSecretId = 0ULL;\n\t\tif (!secureSecret.isEmpty()) {\n\t\t\tnewSecureSecretId = Passport::CountSecureSecretId(\n\t\t\t\tbytes::make_span(secureSecret));\n\t\t\tnewSecureSecret = Passport::EncryptSecureSecret(\n\t\t\t\tbytes::make_span(secureSecret),\n\t\t\t\tCore::ComputeSecureSecretHash(\n\t\t\t\t\tlatestState.mtp.newSecureSecret,\n\t\t\t\t\tbytes::make_span(newPasswordBytes)));\n\t\t}\n\t\tconst auto settings = MTP_account_passwordInputSettings(\n\t\t\tMTP_flags(flags),\n\t\t\tCore::PrepareCloudPasswordAlgo(newPassword.isEmpty()\n\t\t\t\t? v::null\n\t\t\t\t: latestState.mtp.newPassword),\n\t\t\tnewPassword.isEmpty()\n\t\t\t\t? MTP_bytes()\n\t\t\t\t: MTP_bytes(newPasswordHash.modpow),\n\t\t\tMTP_string(hint),\n\t\t\tMTP_string(recoveryEmail),\n\t\t\tMTP_secureSecretSettings(\n\t\t\t\tCore::PrepareSecureSecretAlgo(\n\t\t\t\t\tlatestState.mtp.newSecureSecret),\n\t\t\t\tMTP_bytes(newSecureSecret),\n\t\t\t\tMTP_long(newSecureSecretId)));\n\t\t_api.request(MTPaccount_UpdatePasswordSettings(\n\t\t\tgeneratePasswordCheck(latestState).result,\n\t\t\tsettings\n\t\t)).done([=] {\n\t\t\tfinish(consumer, 0);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconst auto &type = error.type();\n\t\t\tconst auto prefix = u\"EMAIL_UNCONFIRMED_\"_q;\n\t\t\tif (type.startsWith(prefix)) {\n\t\t\t\tconst auto codeLength = base::StringViewMid(\n\t\t\t\t\ttype,\n\t\t\t\t\tprefix.size()).toInt();\n\n\t\t\t\tfinish(consumer, codeLength);\n\t\t\t} else {\n\t\t\t\tconsumer.put_error_copy(type);\n\t\t\t}\n\t\t}).handleFloodErrors().send();\n\t};\n\n\treturn [=](auto consumer) {\n\t\t_api.request(MTPaccount_GetPassword(\n\t\t)).done([=](const MTPaccount_Password &result) {\n\t\t\tconst auto latestState = ProcessMtpState(result);\n\n\t\t\tif (latestState.hasPassword\n\t\t\t\t\t&& !oldPassword.isEmpty()\n\t\t\t\t\t&& !newPassword.isEmpty()) {\n\n\t\t\t\t_api.request(MTPaccount_GetPasswordSettings(\n\t\t\t\t\tgeneratePasswordCheck(latestState).result\n\t\t\t\t)).done([=](const MTPaccount_PasswordSettings &result) {\n\t\t\t\t\tusing Settings = MTPDaccount_passwordSettings;\n\t\t\t\t\tconst auto &data = result.match([&](\n\t\t\t\t\t\t\tconst Settings &data) -> const Settings & {\n\t\t\t\t\t\treturn data;\n\t\t\t\t\t});\n\t\t\t\t\tauto secureSecret = QByteArray();\n\t\t\t\t\tif (const auto wrapped = data.vsecure_settings()) {\n\t\t\t\t\t\tusing Secure = MTPDsecureSecretSettings;\n\t\t\t\t\t\tconst auto &settings = wrapped->match([](\n\t\t\t\t\t\t\t\tconst Secure &data) -> const Secure & {\n\t\t\t\t\t\t\treturn data;\n\t\t\t\t\t\t});\n\t\t\t\t\t\tconst auto passwordUtf = oldPassword.toUtf8();\n\t\t\t\t\t\tconst auto secret = Passport::DecryptSecureSecret(\n\t\t\t\t\t\t\tbytes::make_span(settings.vsecure_secret().v),\n\t\t\t\t\t\t\tCore::ComputeSecureSecretHash(\n\t\t\t\t\t\t\t\tCore::ParseSecureSecretAlgo(\n\t\t\t\t\t\t\t\t\tsettings.vsecure_algo()),\n\t\t\t\t\t\t\t\tbytes::make_span(passwordUtf)));\n\t\t\t\t\t\tif (secret.empty()) {\n\t\t\t\t\t\t\tLOG((\"API Error: \"\n\t\t\t\t\t\t\t\t\"Failed to decrypt secure secret.\"));\n\t\t\t\t\t\t\tconsumer.put_error(\"SUGGEST_SECRET_RESET\");\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t} else if (Passport::CountSecureSecretId(secret)\n\t\t\t\t\t\t\t\t!= settings.vsecure_secret_id().v) {\n\t\t\t\t\t\t\tLOG((\"API Error: Wrong secure secret id.\"));\n\t\t\t\t\t\t\tconsumer.put_error(\"SUGGEST_SECRET_RESET\");\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsecureSecret = QByteArray(\n\t\t\t\t\t\t\t\treinterpret_cast<const char*>(secret.data()),\n\t\t\t\t\t\t\t\tsecret.size());\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t_api.request(MTPaccount_GetPassword(\n\t\t\t\t\t)).done([=](const MTPaccount_Password &result) {\n\t\t\t\t\t\tconst auto latestState = ProcessMtpState(result);\n\t\t\t\t\t\tsendMTPaccountUpdatePasswordSettings(\n\t\t\t\t\t\t\tlatestState,\n\t\t\t\t\t\t\tsecureSecret,\n\t\t\t\t\t\t\tconsumer);\n\t\t\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\t\t\tconsumer.put_error_copy(error.type());\n\t\t\t\t\t}).send();\n\t\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\t\tconsumer.put_error_copy(error.type());\n\t\t\t\t}).send();\n\t\t\t} else {\n\t\t\t\tsendMTPaccountUpdatePasswordSettings(\n\t\t\t\t\tlatestState,\n\t\t\t\t\tQByteArray(),\n\t\t\t\t\tconsumer);\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).send();\n\t\treturn rpl::lifetime();\n\t};\n}\n\nrpl::producer<rpl::no_value, QString> CloudPassword::check(\n\t\tconst QString &password) {\n\treturn [=](auto consumer) {\n\t\t_api.request(MTPaccount_GetPassword(\n\t\t)).done([=](const MTPaccount_Password &result) {\n\t\t\tconst auto latestState = ProcessMtpState(result);\n\t\t\tconst auto input = [&] {\n\t\t\t\tif (password.isEmpty()) {\n\t\t\t\t\treturn Core::CloudPasswordResult{\n\t\t\t\t\t\tMTP_inputCheckPasswordEmpty()\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tconst auto hash = Core::ComputeCloudPasswordHash(\n\t\t\t\t\tlatestState.mtp.request.algo,\n\t\t\t\t\tbytes::make_span(password.toUtf8()));\n\t\t\t\treturn Core::ComputeCloudPasswordCheck(\n\t\t\t\t\tlatestState.mtp.request,\n\t\t\t\t\thash);\n\t\t\t}();\n\n\t\t\t_api.request(MTPaccount_GetPasswordSettings(\n\t\t\t\tinput.result\n\t\t\t)).done([=](const MTPaccount_PasswordSettings &result) {\n\t\t\t\tconsumer.put_done();\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tconsumer.put_error_copy(error.type());\n\t\t\t}).send();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).send();\n\n\t\treturn rpl::lifetime();\n\t};\n}\n\nrpl::producer<rpl::no_value, QString> CloudPassword::confirmEmail(\n\t\tconst QString &code) {\n\treturn [=](auto consumer) {\n\t\t_api.request(MTPaccount_ConfirmPasswordEmail(\n\t\t\tMTP_string(code)\n\t\t)).done([=] {\n\t\t\t_api.request(MTPaccount_GetPassword(\n\t\t\t)).done([=](const MTPaccount_Password &result) {\n\t\t\t\tapply(ProcessMtpState(result));\n\t\t\t\tconsumer.put_done();\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tconsumer.put_error_copy(error.type());\n\t\t\t}).send();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).handleFloodErrors().send();\n\n\t\treturn rpl::lifetime();\n\t};\n}\n\nrpl::producer<rpl::no_value, QString> CloudPassword::resendEmailCode() {\n\treturn [=](auto consumer) {\n\t\t_api.request(MTPaccount_ResendPasswordEmail(\n\t\t)).done([=] {\n\t\t\t_api.request(MTPaccount_GetPassword(\n\t\t\t)).done([=](const MTPaccount_Password &result) {\n\t\t\t\tapply(ProcessMtpState(result));\n\t\t\t\tconsumer.put_done();\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tconsumer.put_error_copy(error.type());\n\t\t\t}).send();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).handleFloodErrors().send();\n\n\t\treturn rpl::lifetime();\n\t};\n}\n\nrpl::producer<CloudPassword::SetOk, QString> CloudPassword::setEmail(\n\t\tconst QString &oldPassword,\n\t\tconst QString &recoveryEmail) {\n\tconst auto generatePasswordCheck = [=](\n\t\t\tconst Core::CloudPasswordState &latestState) {\n\t\tif (oldPassword.isEmpty() || !latestState.hasPassword) {\n\t\t\treturn Core::CloudPasswordResult{\n\t\t\t\tMTP_inputCheckPasswordEmpty()\n\t\t\t};\n\t\t}\n\t\tconst auto hash = Core::ComputeCloudPasswordHash(\n\t\t\tlatestState.mtp.request.algo,\n\t\t\tbytes::make_span(oldPassword.toUtf8()));\n\t\treturn Core::ComputeCloudPasswordCheck(\n\t\t\tlatestState.mtp.request,\n\t\t\thash);\n\t};\n\n\tconst auto finish = [=](auto consumer, int unconfirmedEmailLengthCode) {\n\t\t_api.request(MTPaccount_GetPassword(\n\t\t)).done([=](const MTPaccount_Password &result) {\n\t\t\tapply(ProcessMtpState(result));\n\t\t\tif (unconfirmedEmailLengthCode) {\n\t\t\t\tconsumer.put_next(SetOk{ unconfirmedEmailLengthCode });\n\t\t\t} else {\n\t\t\t\tconsumer.put_done();\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).handleFloodErrors().send();\n\t};\n\n\tconst auto sendMTPaccountUpdatePasswordSettings = [=](\n\t\t\tconst Core::CloudPasswordState &latestState,\n\t\t\tauto consumer) {\n\t\tconst auto settings = MTP_account_passwordInputSettings(\n\t\t\tMTP_flags(MTPDaccount_passwordInputSettings::Flag::f_email),\n\t\t\tMTP_passwordKdfAlgoUnknown(),\n\t\t\tMTP_bytes(),\n\t\t\tMTP_string(),\n\t\t\tMTP_string(recoveryEmail),\n\t\t\tMTPSecureSecretSettings());\n\t\t_api.request(MTPaccount_UpdatePasswordSettings(\n\t\t\tgeneratePasswordCheck(latestState).result,\n\t\t\tsettings\n\t\t)).done([=] {\n\t\t\tfinish(consumer, 0);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconst auto &type = error.type();\n\t\t\tconst auto prefix = u\"EMAIL_UNCONFIRMED_\"_q;\n\t\t\tif (type.startsWith(prefix)) {\n\t\t\t\tconst auto codeLength = base::StringViewMid(\n\t\t\t\t\ttype,\n\t\t\t\t\tprefix.size()).toInt();\n\n\t\t\t\tfinish(consumer, codeLength);\n\t\t\t} else {\n\t\t\t\tconsumer.put_error_copy(type);\n\t\t\t}\n\t\t}).handleFloodErrors().send();\n\t};\n\n\treturn [=](auto consumer) {\n\t\t_api.request(MTPaccount_GetPassword(\n\t\t)).done([=](const MTPaccount_Password &result) {\n\t\t\tconst auto latestState = ProcessMtpState(result);\n\t\t\tsendMTPaccountUpdatePasswordSettings(latestState, consumer);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).send();\n\t\treturn rpl::lifetime();\n\t};\n}\n\nrpl::producer<rpl::no_value, QString> CloudPassword::recoverPassword(\n\t\tconst QString &code,\n\t\tconst QString &newPassword,\n\t\tconst QString &newHint) {\n\n\tconst auto finish = [=](auto consumer) {\n\t\t_api.request(MTPaccount_GetPassword(\n\t\t)).done([=](const MTPaccount_Password &result) {\n\t\t\tapply(ProcessMtpState(result));\n\t\t\tconsumer.put_done();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).handleFloodErrors().send();\n\t};\n\n\tconst auto sendMTPaccountUpdatePasswordSettings = [=](\n\t\t\tconst Core::CloudPasswordState &latestState,\n\t\t\tauto consumer) {\n\t\tconst auto newPasswordBytes = newPassword.toUtf8();\n\t\tconst auto newPasswordHash = Core::ComputeCloudPasswordDigest(\n\t\t\tlatestState.mtp.newPassword,\n\t\t\tbytes::make_span(newPasswordBytes));\n\t\tif (!newPassword.isEmpty() && newPasswordHash.modpow.empty()) {\n\t\t\tconsumer.put_error(\"INTERNAL_SERVER_ERROR\");\n\t\t\treturn;\n\t\t}\n\t\tusing Flag = MTPDaccount_passwordInputSettings::Flag;\n\t\tconst auto flags = Flag::f_new_algo\n\t\t\t| Flag::f_new_password_hash\n\t\t\t| Flag::f_hint;\n\n\t\tconst auto settings = MTP_account_passwordInputSettings(\n\t\t\tMTP_flags(flags),\n\t\t\tCore::PrepareCloudPasswordAlgo(newPassword.isEmpty()\n\t\t\t\t? v::null\n\t\t\t\t: latestState.mtp.newPassword),\n\t\t\tnewPassword.isEmpty()\n\t\t\t\t? MTP_bytes()\n\t\t\t\t: MTP_bytes(newPasswordHash.modpow),\n\t\t\tMTP_string(newHint),\n\t\t\tMTP_string(),\n\t\t\tMTPSecureSecretSettings());\n\n\t\t_api.request(MTPauth_RecoverPassword(\n\t\t\tMTP_flags(newPassword.isEmpty()\n\t\t\t\t? MTPauth_RecoverPassword::Flags(0)\n\t\t\t\t: MTPauth_RecoverPassword::Flag::f_new_settings),\n\t\t\tMTP_string(code),\n\t\t\tsettings\n\t\t)).done([=](const MTPauth_Authorization &result) {\n\t\t\tfinish(consumer);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconst auto &type = error.type();\n\t\t\tconsumer.put_error_copy(type);\n\t\t}).handleFloodErrors().send();\n\t};\n\n\treturn [=](auto consumer) {\n\t\t_api.request(MTPaccount_GetPassword(\n\t\t)).done([=](const MTPaccount_Password &result) {\n\t\t\tconst auto latestState = ProcessMtpState(result);\n\t\t\tsendMTPaccountUpdatePasswordSettings(latestState, consumer);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).send();\n\t\treturn rpl::lifetime();\n\t};\n}\n\nrpl::producer<QString, QString> CloudPassword::requestPasswordRecovery() {\n\treturn [=](auto consumer) {\n\t\t_api.request(MTPauth_RequestPasswordRecovery(\n\t\t)).done([=](const MTPauth_PasswordRecovery &result) {\n\t\t\tresult.match([&](const MTPDauth_passwordRecovery &data) {\n\t\t\t\tconsumer.put_next(qs(data.vemail_pattern().v));\n\t\t\t});\n\t\t\tconsumer.put_done();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).send();\n\t\treturn rpl::lifetime();\n\t};\n}\n\nauto CloudPassword::checkRecoveryEmailAddressCode(const QString &code)\n-> rpl::producer<rpl::no_value, QString> {\n\treturn [=](auto consumer) {\n\t\t_api.request(MTPauth_CheckRecoveryPassword(\n\t\t\tMTP_string(code)\n\t\t)).done([=] {\n\t\t\tconsumer.put_done();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).handleFloodErrors().send();\n\n\t\treturn rpl::lifetime();\n\t};\n}\n\nvoid RequestLoginEmailCode(\n\t\tMTP::Sender &api,\n\t\tconst QString &sendToEmail,\n\t\tFn<void(int length, const QString &pattern)> done,\n\t\tFn<void(const QString &error)> fail) {\n\tapi.request(MTPaccount_SendVerifyEmailCode(\n\t\tMTP_emailVerifyPurposeLoginChange(),\n\t\tMTP_string(sendToEmail)\n\t)).done([=](const MTPaccount_SentEmailCode &result) {\n\t\tdone(result.data().vlength().v, qs(result.data().vemail_pattern()));\n\t}).fail([=](const MTP::Error &error) {\n\t\tfail(error.type());\n\t}).send();\n}\n\nvoid VerifyLoginEmail(\n\t\tMTP::Sender &api,\n\t\tconst QString &code,\n\t\tFn<void()> done,\n\t\tFn<void(const QString &error)> fail) {\n\tapi.request(MTPaccount_VerifyEmail(\n\t\tMTP_emailVerifyPurposeLoginChange(),\n\t\tMTP_emailVerificationCode(MTP_string(code))\n\t)).done([=](const MTPaccount_EmailVerified &result) {\n\t\tresult.match([=](const MTPDaccount_emailVerified &data) {\n\t\t\tdone();\n\t\t}, [=](const MTPDaccount_emailVerifiedLogin &data) {\n\t\t\tfail(QString());\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\tfail(error.type());\n\t}).send();\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_cloud_password.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n\nnamespace Core {\nstruct CloudPasswordState;\n} // namespace Core\n\nclass ApiWrap;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nclass CloudPassword final {\npublic:\n\tstruct SetOk {\n\t\tint unconfirmedEmailLengthCode = 0;\n\t};\n\n\tusing ResetRetryDate = int;\n\texplicit CloudPassword(not_null<ApiWrap*> api);\n\n\tvoid reload();\n\tvoid clearUnconfirmedPassword();\n\trpl::producer<Core::CloudPasswordState> state() const;\n\tstd::optional<Core::CloudPasswordState> stateCurrent() const;\n\n\trpl::producer<ResetRetryDate, QString> resetPassword();\n\trpl::producer<rpl::no_value, QString> cancelResetPassword();\n\n\trpl::producer<SetOk, QString> set(\n\t\tconst QString &oldPassword,\n\t\tconst QString &newPassword,\n\t\tconst QString &hint,\n\t\tbool hasRecoveryEmail,\n\t\tconst QString &recoveryEmail);\n\trpl::producer<rpl::no_value, QString> check(const QString &password);\n\n\trpl::producer<rpl::no_value, QString> confirmEmail(const QString &code);\n\trpl::producer<rpl::no_value, QString> resendEmailCode();\n\trpl::producer<SetOk, QString> setEmail(\n\t\tconst QString &oldPassword,\n\t\tconst QString &recoveryEmail);\n\n\trpl::producer<rpl::no_value, QString> recoverPassword(\n\t\tconst QString &code,\n\t\tconst QString &newPassword,\n\t\tconst QString &newHint);\n\trpl::producer<QString, QString> requestPasswordRecovery();\n\trpl::producer<rpl::no_value, QString> checkRecoveryEmailAddressCode(\n\t\tconst QString &code);\n\nprivate:\n\tvoid apply(Core::CloudPasswordState state);\n\n\tMTP::Sender _api;\n\tmtpRequestId _requestId = 0;\n\tstd::unique_ptr<Core::CloudPasswordState> _state;\n\trpl::event_stream<Core::CloudPasswordState> _stateChanges;\n\n};\n\nvoid RequestLoginEmailCode(\n\tMTP::Sender &api,\n\tconst QString &sendToEmail,\n\tFn<void(int length, const QString &pattern)> done,\n\tFn<void(const QString &error)> fail);\nvoid VerifyLoginEmail(\n\tMTP::Sender &api,\n\tconst QString &code,\n\tFn<void()> done,\n\tFn<void(const QString &error)> fail);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_common.h\"\n\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_thread.h\"\n#include \"history/history.h\"\n\nnamespace Api {\n\nMTPSuggestedPost SuggestToMTP(SuggestOptions suggest) {\n\tusing Flag = MTPDsuggestedPost::Flag;\n\treturn suggest.exists\n\t\t? MTP_suggestedPost(\n\t\t\tMTP_flags((suggest.date ? Flag::f_schedule_date : Flag())\n\t\t\t\t| (suggest.price().empty() ? Flag() : Flag::f_price)),\n\t\t\tStarsAmountToTL(suggest.price()),\n\t\t\tMTP_int(suggest.date))\n\t\t: MTPSuggestedPost();\n}\n\nSendAction::SendAction(\n\tnot_null<Data::Thread*> thread,\n\tSendOptions options)\n: history(thread->owningHistory())\n, options(options)\n, replyTo({ .messageId = { history->peer->id, thread->topicRootId() } }) {\n\treplyTo.topicRootId = replyTo.messageId.msg;\n}\n\nSendOptions DefaultSendWhenOnlineOptions() {\n\treturn {\n\t\t.scheduled = kScheduledUntilOnlineTimestamp,\n\t\t.silent = base::IsCtrlPressed(),\n\t};\n}\n\nMTPInputReplyTo SendAction::mtpReplyTo() const {\n\treturn Data::ReplyToForMTP(history, replyTo);\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_drafts.h\"\n\nclass History;\n\nnamespace Data {\nclass Thread;\n} // namespace Data\n\nnamespace Api {\n\ninline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE);\n\n[[nodiscard]] MTPSuggestedPost SuggestToMTP(SuggestOptions suggest);\n\nstruct SendOptions {\n\tuint64 price = 0;\n\tPeerData *sendAs = nullptr;\n\tTimeId scheduled = 0;\n\tTimeId scheduleRepeatPeriod = 0;\n\tBusinessShortcutId shortcutId = 0;\n\tEffectId effectId = 0;\n\tQByteArray stakeSeedHash;\n\tint64 stakeNanoTon = 0;\n\tint starsApproved = 0;\n\tbool silent = false;\n\tbool handleSupportSwitch = false;\n\tbool invertCaption = false;\n\tbool hideViaBot = false;\n\tbool mediaSpoiler = false;\n\tcrl::time ttlSeconds = 0;\n\tSuggestOptions suggest;\n\n\tfriend inline bool operator==(\n\t\tconst SendOptions &,\n\t\tconst SendOptions &) = default;\n};\n[[nodiscard]] SendOptions DefaultSendWhenOnlineOptions();\n\nenum class SendType {\n\tNormal,\n\tScheduled,\n\tScheduledToUser, // For \"Send when online\".\n};\n\nstruct SendAction {\n\texplicit SendAction(\n\t\tnot_null<Data::Thread*> thread,\n\t\tSendOptions options = SendOptions());\n\n\tnot_null<History*> history;\n\tSendOptions options;\n\tFullReplyTo replyTo;\n\tbool clearDraft = true;\n\tbool generateLocal = true;\n\tMsgId replaceMediaOf = 0;\n\n\t[[nodiscard]] MTPInputReplyTo mtpReplyTo() const;\n\n\tfriend inline bool operator==(\n\t\tconst SendAction &,\n\t\tconst SendAction &) = default;\n};\n\nstruct MessageToSend {\n\texplicit MessageToSend(SendAction action) : action(action) {\n\t}\n\n\tSendAction action;\n\tTextWithTags textWithTags;\n\tData::WebPageDraft webPage;\n};\n\nstruct RemoteFileInfo {\n\tMTPInputFile file;\n\tstd::optional<MTPInputFile> thumb;\n\tstd::optional<MTPInputPhoto> videoCover;\n\tstd::vector<MTPInputDocument> attachedStickers;\n\tbool forceFile = false;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_compose_with_ai.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_compose_with_ai.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"apiwrap.h\"\n\nnamespace Api {\nnamespace {\n\n[[nodiscard]] MTPTextWithEntities Serialize(\n\t\tnot_null<Main::Session*> session,\n\t\tconst TextWithEntities &text) {\n\treturn MTP_textWithEntities(\n\t\tMTP_string(text.text),\n\t\tEntitiesToMTP(session, text.entities, ConvertOption::SkipLocal));\n}\n\n} // namespace\n\nComposeWithAi::ComposeWithAi(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance()) {\n}\n\nmtpRequestId ComposeWithAi::request(\n\t\tRequest request,\n\t\tFn<void(Result &&)> done,\n\t\tFn<void(const MTP::Error &)> fail) {\n\tusing Flag = MTPmessages_composeMessageWithAI::Flag;\n\tauto flags = MTPmessages_composeMessageWithAI::Flags(0);\n\tif (request.proofread) {\n\t\tflags |= Flag::f_proofread;\n\t}\n\tif (!request.translateToLang.isEmpty()) {\n\t\tflags |= Flag::f_translate_to_lang;\n\t}\n\tif (!request.changeTone.isEmpty()) {\n\t\tflags |= Flag::f_change_tone;\n\t}\n\tif (request.emojify) {\n\t\tflags |= Flag::f_emojify;\n\t}\n\tconst auto session = _session;\n\treturn _api.request(MTPmessages_ComposeMessageWithAI(\n\t\tMTP_flags(flags),\n\t\tSerialize(session, request.text),\n\t\trequest.translateToLang.isEmpty()\n\t\t\t? MTPstring()\n\t\t\t: MTP_string(request.translateToLang),\n\t\trequest.changeTone.isEmpty()\n\t\t\t? MTPstring()\n\t\t\t: MTP_string(request.changeTone)\n\t)).done([=, done = std::move(done)](\n\t\t\tconst MTPmessages_ComposedMessageWithAI &result) mutable {\n\t\tconst auto &data = result.data();\n\t\tauto parsed = Result{\n\t\t\t.resultText = ParseTextWithEntities(session, data.vresult_text()),\n\t\t};\n\t\tif (const auto diff = data.vdiff_text()) {\n\t\t\tparsed.diffText = ParseDiff(session, *diff);\n\t\t}\n\t\tdone(std::move(parsed));\n\t}).fail([=, fail = std::move(fail)](const MTP::Error &error) mutable {\n\t\tif (fail) {\n\t\t\tfail(error);\n\t\t}\n\t}).send();\n}\n\nvoid ComposeWithAi::cancel(mtpRequestId requestId) {\n\tif (requestId) {\n\t\t_api.request(requestId).cancel();\n\t}\n}\n\nComposeWithAi::Diff ComposeWithAi::ParseDiff(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPTextWithEntities &text) {\n\tconst auto &data = text.data();\n\tauto result = Diff{\n\t\t.text = ParseTextWithEntities(session, text),\n\t};\n\tconst auto &entities = data.ventities().v;\n\tresult.entities.reserve(entities.size());\n\tfor (const auto &entity : entities) {\n\t\tentity.match([&](const MTPDmessageEntityDiffInsert &data) {\n\t\t\tresult.entities.push_back({\n\t\t\t\t.type = DiffEntity::Type::Insert,\n\t\t\t\t.offset = data.voffset().v,\n\t\t\t\t.length = data.vlength().v,\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityDiffReplace &data) {\n\t\t\tresult.entities.push_back({\n\t\t\t\t.type = DiffEntity::Type::Replace,\n\t\t\t\t.offset = data.voffset().v,\n\t\t\t\t.length = data.vlength().v,\n\t\t\t\t.oldText = qs(data.vold_text()),\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityDiffDelete &data) {\n\t\t\tresult.entities.push_back({\n\t\t\t\t.type = DiffEntity::Type::Delete,\n\t\t\t\t.offset = data.voffset().v,\n\t\t\t\t.length = data.vlength().v,\n\t\t\t});\n\t\t}, [](const auto &) {\n\t\t});\n\t}\n\treturn result;\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_compose_with_ai.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n#include \"ui/text/text_entity.h\"\n\n#include <optional>\n#include <vector>\n\nclass ApiWrap;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nclass ComposeWithAi final {\npublic:\n\tstruct Request {\n\t\tTextWithEntities text;\n\t\tQString translateToLang;\n\t\tQString changeTone;\n\t\tbool proofread = false;\n\t\tbool emojify = false;\n\t};\n\n\tstruct DiffEntity {\n\t\tenum class Type {\n\t\t\tInsert,\n\t\t\tReplace,\n\t\t\tDelete,\n\t\t};\n\n\t\tType type = Type::Insert;\n\t\tint offset = 0;\n\t\tint length = 0;\n\t\tQString oldText;\n\t};\n\n\tstruct Diff {\n\t\tTextWithEntities text;\n\t\tstd::vector<DiffEntity> entities;\n\t};\n\n\tstruct Result {\n\t\tTextWithEntities resultText;\n\t\tstd::optional<Diff> diffText;\n\t};\n\n\texplicit ComposeWithAi(not_null<ApiWrap*> api);\n\n\t[[nodiscard]] mtpRequestId request(\n\t\tRequest request,\n\t\tFn<void(Result &&)> done,\n\t\tFn<void(const MTP::Error &)> fail = nullptr);\n\tvoid cancel(mtpRequestId requestId);\n\nprivate:\n\t[[nodiscard]] static Diff ParseDiff(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPTextWithEntities &text);\n\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_confirm_phone.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_confirm_phone.h\"\n\n#include \"apiwrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/confirm_phone_box.h\"\n#include \"ui/text/format_values.h\" // Ui::FormatPhone\n#include \"window/window_session_controller.h\"\n\nnamespace Api {\n\nConfirmPhone::ConfirmPhone(not_null<ApiWrap*> api)\n: _api(&api->instance()) {\n}\n\nvoid ConfirmPhone::resolve(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &phone,\n\t\tconst QString &hash) {\n\tif (_sendRequestId) {\n\t\treturn;\n\t}\n\t_sendRequestId = _api.request(MTPaccount_SendConfirmPhoneCode(\n\t\tMTP_string(hash),\n\t\tMTP_codeSettings(\n\t\t\tMTP_flags(0),\n\t\t\tMTPVector<MTPbytes>(),\n\t\t\tMTPstring(),\n\t\t\tMTPBool())\n\t)).done([=](const MTPauth_SentCode &result) {\n\t\t_sendRequestId = 0;\n\n\t\tresult.match([&](const MTPDauth_sentCode &data) {\n\t\t\tconst auto bad = [](const char *type) {\n\t\t\t\tLOG((\"API Error: Should not be '%1'.\").arg(type));\n\t\t\t\treturn 0;\n\t\t\t};\n\t\t\tconst auto sentCodeLength = data.vtype().match([&](\n\t\t\t\t\tconst MTPDauth_sentCodeTypeApp &data) {\n\t\t\t\tLOG((\"Error: should not be in-app code!\"));\n\t\t\t\treturn 0;\n\t\t\t}, [&](const MTPDauth_sentCodeTypeSms &data) {\n\t\t\t\treturn data.vlength().v;\n\t\t\t}, [&](const MTPDauth_sentCodeTypeFragmentSms &data) {\n\t\t\t\treturn data.vlength().v;\n\t\t\t}, [&](const MTPDauth_sentCodeTypeCall &data) {\n\t\t\t\treturn data.vlength().v;\n\t\t\t}, [&](const MTPDauth_sentCodeTypeFlashCall &) {\n\t\t\t\treturn bad(\"FlashCall\");\n\t\t\t}, [&](const MTPDauth_sentCodeTypeMissedCall &) {\n\t\t\t\treturn bad(\"MissedCall\");\n\t\t\t}, [&](const MTPDauth_sentCodeTypeFirebaseSms &) {\n\t\t\t\treturn bad(\"FirebaseSms\");\n\t\t\t}, [&](const MTPDauth_sentCodeTypeEmailCode &) {\n\t\t\t\treturn bad(\"EmailCode\");\n\t\t\t}, [&](const MTPDauth_sentCodeTypeSmsWord &) {\n\t\t\t\treturn bad(\"SmsWord\");\n\t\t\t}, [&](const MTPDauth_sentCodeTypeSmsPhrase &) {\n\t\t\t\treturn bad(\"SmsPhrase\");\n\t\t\t}, [&](const MTPDauth_sentCodeTypeSetUpEmailRequired &) {\n\t\t\t\treturn bad(\"SetUpEmailRequired\");\n\t\t\t});\n\t\t\tconst auto fragmentUrl = data.vtype().match([](\n\t\t\t\t\tconst MTPDauth_sentCodeTypeFragmentSms &data) {\n\t\t\t\treturn qs(data.vurl());\n\t\t\t}, [](const auto &) { return QString(); });\n\t\t\tconst auto phoneHash = qs(data.vphone_code_hash());\n\t\t\tconst auto timeout = [&]() -> std::optional<int> {\n\t\t\t\tif (const auto nextType = data.vnext_type()) {\n\t\t\t\t\tif (nextType->type() == mtpc_auth_codeTypeCall) {\n\t\t\t\t\t\treturn data.vtimeout().value_or(60);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn std::nullopt;\n\t\t\t}();\n\t\t\tauto box = Box<Ui::ConfirmPhoneBox>(\n\t\t\t\tphone,\n\t\t\t\tsentCodeLength,\n\t\t\t\tfragmentUrl,\n\t\t\t\ttimeout);\n\t\t\tconst auto boxWeak = base::make_weak(box.data());\n\t\t\tusing LoginCode = rpl::event_stream<QString>;\n\t\t\tconst auto codeHandles = box->lifetime().make_state<LoginCode>();\n\t\t\tcontroller->session().account().setHandleLoginCode([=](\n\t\t\t\t\tconst QString &code) {\n\t\t\t\tcodeHandles->fire_copy(code);\n\t\t\t});\n\t\t\tbox->resendRequests(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\t_api.request(MTPauth_ResendCode(\n\t\t\t\t\tMTP_flags(0),\n\t\t\t\t\tMTP_string(phone),\n\t\t\t\t\tMTP_string(phoneHash),\n\t\t\t\t\tMTPstring() // reason\n\t\t\t\t)).done([=] {\n\t\t\t\t\tif (boxWeak) {\n\t\t\t\t\t\tboxWeak->callDone();\n\t\t\t\t\t}\n\t\t\t\t}).send();\n\t\t\t}, box->lifetime());\n\t\t\trpl::merge(\n\t\t\t\tcodeHandles->events(),\n\t\t\t\tbox->checkRequests()\n\t\t\t) | rpl::on_next([=](const QString &code) {\n\t\t\t\tif (_checkRequestId) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t_checkRequestId = _api.request(MTPaccount_ConfirmPhone(\n\t\t\t\t\tMTP_string(phoneHash),\n\t\t\t\t\tMTP_string(code)\n\t\t\t\t)).done([=] {\n\t\t\t\t\t_checkRequestId = 0;\n\t\t\t\t\tcontroller->show(\n\t\t\t\t\t\tUi::MakeInformBox(\n\t\t\t\t\t\t\ttr::lng_confirm_phone_success(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_phone,\n\t\t\t\t\t\t\t\tUi::FormatPhone(phone))),\n\t\t\t\t\t\tUi::LayerOption::CloseOther);\n\t\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\t\t_checkRequestId = 0;\n\t\t\t\t\tif (!boxWeak) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst auto errorText = MTP::IsFloodError(error)\n\t\t\t\t\t\t? tr::lng_flood_error(tr::now)\n\t\t\t\t\t\t: (error.type() == (u\"PHONE_CODE_EMPTY\"_q)\n\t\t\t\t\t\t\t|| error.type() == (u\"PHONE_CODE_INVALID\"_q))\n\t\t\t\t\t\t? tr::lng_bad_code(tr::now)\n\t\t\t\t\t\t: Lang::Hard::ServerError();\n\t\t\t\t\tboxWeak->showServerError(errorText);\n\t\t\t\t}).handleFloodErrors().send();\n\t\t\t}, box->lifetime());\n\t\t\tbox->boxClosing(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tcontroller->session().account().setHandleLoginCode(nullptr);\n\t\t\t}, box->lifetime());\n\n\t\t\tcontroller->show(std::move(box), Ui::LayerOption::CloseOther);\n\t\t}, [](const MTPDauth_sentCodeSuccess &) {\n\t\t\tLOG((\"API Error: Unexpected auth.sentCodeSuccess \"\n\t\t\t\t\"(Api::ConfirmPhone).\"));\n\t\t}, [](const MTPDauth_sentCodePaymentRequired &) {\n\t\t\tLOG((\"API Error: Unexpected auth.sentCodePaymentRequired \"\n\t\t\t\t\"(Api::ConfirmPhone).\"));\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\t_sendRequestId = 0;\n\t\t_checkRequestId = 0;\n\n\t\tconst auto errorText = MTP::IsFloodError(error)\n\t\t\t? tr::lng_flood_error(tr::now)\n\t\t\t: (error.code() == 400)\n\t\t\t? tr::lng_confirm_phone_link_invalid(tr::now)\n\t\t\t: Lang::Hard::ServerError();\n\t\tcontroller->show(\n\t\t\tUi::MakeInformBox(errorText),\n\t\t\tUi::LayerOption::CloseOther);\n\t}).handleFloodErrors().send();\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_confirm_phone.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n\nclass ApiWrap;\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Api {\n\nclass ConfirmPhone final {\npublic:\n\texplicit ConfirmPhone(not_null<ApiWrap*> api);\n\n\tvoid resolve(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &phone,\n\t\tconst QString &hash);\n\nprivate:\n\tMTP::Sender _api;\n\tmtpRequestId _sendRequestId = 0;\n\tmtpRequestId _checkRequestId = 0;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_credits.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_credits.h\"\n\n#include \"api/api_credits_history_entry.h\"\n#include \"api/api_premium.h\"\n#include \"api/api_statistics_data_deserialize.h\"\n#include \"api/api_updates.h\"\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"data/components/credits.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kTransactionsLimit = 100;\n\n[[nodiscard]] Data::SubscriptionEntry SubscriptionFromTL(\n\t\tconst MTPStarsSubscription &tl,\n\t\tnot_null<PeerData*> peer) {\n\treturn Data::SubscriptionEntry{\n\t\t.id = qs(tl.data().vid()),\n\t\t.inviteHash = qs(tl.data().vchat_invite_hash().value_or_empty()),\n\t\t.title = qs(tl.data().vtitle().value_or_empty()),\n\t\t.slug = qs(tl.data().vinvoice_slug().value_or_empty()),\n\t\t.until = base::unixtime::parse(tl.data().vuntil_date().v),\n\t\t.subscription = Data::PeerSubscription{\n\t\t\t.credits = tl.data().vpricing().data().vamount().v,\n\t\t\t.period = tl.data().vpricing().data().vperiod().v,\n\t\t},\n\t\t.barePeerId = peerFromMTP(tl.data().vpeer()).value,\n\t\t.photoId = (tl.data().vphoto()\n\t\t\t? peer->owner().photoFromWeb(\n\t\t\t\t*tl.data().vphoto(),\n\t\t\t\tImageLocation())->id\n\t\t\t: 0),\n\t\t.cancelled = tl.data().is_canceled(),\n\t\t.cancelledByBot = tl.data().is_bot_canceled(),\n\t\t.expired = (base::unixtime::now() > tl.data().vuntil_date().v),\n\t\t.canRefulfill = tl.data().is_can_refulfill(),\n\t};\n}\n\n[[nodiscard]] Data::CreditsStatusSlice StatusFromTL(\n\t\tconst MTPpayments_StarsStatus &status,\n\t\tnot_null<PeerData*> peer) {\n\tconst auto &data = status.data();\n\tpeer->owner().processUsers(data.vusers());\n\tpeer->owner().processChats(data.vchats());\n\tauto entries = std::vector<Data::CreditsHistoryEntry>();\n\tif (const auto history = data.vhistory()) {\n\t\tentries.reserve(history->v.size());\n\t\tfor (const auto &tl : history->v) {\n\t\t\tentries.push_back(CreditsHistoryEntryFromTL(tl, peer));\n\t\t}\n\t}\n\tauto subscriptions = std::vector<Data::SubscriptionEntry>();\n\tif (const auto history = data.vsubscriptions()) {\n\t\tsubscriptions.reserve(history->v.size());\n\t\tfor (const auto &tl : history->v) {\n\t\t\tsubscriptions.push_back(SubscriptionFromTL(tl, peer));\n\t\t}\n\t}\n\treturn Data::CreditsStatusSlice{\n\t\t.list = std::move(entries),\n\t\t.subscriptions = std::move(subscriptions),\n\t\t.balance = CreditsAmountFromTL(status.data().vbalance()),\n\t\t.subscriptionsMissingBalance\n\t\t\t= status.data().vsubscriptions_missing_balance().value_or_empty(),\n\t\t.allLoaded = !status.data().vnext_offset().has_value()\n\t\t\t&& !status.data().vsubscriptions_next_offset().has_value(),\n\t\t.token = qs(status.data().vnext_offset().value_or_empty()),\n\t\t.tokenSubscriptions = qs(\n\t\t\tstatus.data().vsubscriptions_next_offset().value_or_empty()),\n\t};\n}\n\n} // namespace\n\nCreditsTopupOptions::CreditsTopupOptions(not_null<PeerData*> peer)\n: _peer(peer)\n, _api(&peer->session().api().instance()) {\n}\n\nrpl::producer<rpl::no_value, QString> CreditsTopupOptions::request() {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tconst auto giftBarePeerId = !_peer->isSelf() ? _peer->id.value : 0;\n\n\t\tconst auto optionsFromTL = [giftBarePeerId](const auto &options) {\n\t\t\treturn ranges::views::all(\n\t\t\t\toptions\n\t\t\t) | ranges::views::transform([=](const auto &option) {\n\t\t\t\treturn Data::CreditTopupOption{\n\t\t\t\t\t.credits = option.data().vstars().v,\n\t\t\t\t\t.product = qs(\n\t\t\t\t\t\toption.data().vstore_product().value_or_empty()),\n\t\t\t\t\t.currency = qs(option.data().vcurrency()),\n\t\t\t\t\t.amount = option.data().vamount().v,\n\t\t\t\t\t.extended = option.data().is_extended(),\n\t\t\t\t\t.giftBarePeerId = giftBarePeerId,\n\t\t\t\t};\n\t\t\t}) | ranges::to_vector;\n\t\t};\n\t\tconst auto fail = [=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t};\n\n\t\tif (_peer->isSelf()) {\n\t\t\tusing TLOption = MTPStarsTopupOption;\n\t\t\t_api.request(MTPpayments_GetStarsTopupOptions(\n\t\t\t)).done([=](const MTPVector<TLOption> &result) {\n\t\t\t\t_options = optionsFromTL(result.v);\n\t\t\t\tconsumer.put_done();\n\t\t\t}).fail(fail).send();\n\t\t} else if (const auto user = _peer->asUser()) {\n\t\t\tusing TLOption = MTPStarsGiftOption;\n\t\t\t_api.request(MTPpayments_GetStarsGiftOptions(\n\t\t\t\tMTP_flags(MTPpayments_GetStarsGiftOptions::Flag::f_user_id),\n\t\t\t\tuser->inputUser()\n\t\t\t)).done([=](const MTPVector<TLOption> &result) {\n\t\t\t\t_options = optionsFromTL(result.v);\n\t\t\t\tconsumer.put_done();\n\t\t\t}).fail(fail).send();\n\t\t}\n\n\t\treturn lifetime;\n\t};\n}\n\nData::CreditTopupOptions CreditsTopupOptions::options() const {\n\treturn _options;\n}\n\nCreditsStatus::CreditsStatus(not_null<PeerData*> peer)\n: _peer(peer)\n, _api(&peer->session().api().instance()) {\n}\n\nvoid CreditsStatus::request(\n\t\tconst Data::CreditsStatusSlice::OffsetToken &token,\n\t\tFn<void(Data::CreditsStatusSlice)> done) {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\n\tusing TLResult = MTPpayments_StarsStatus;\n\n\t_requestId = _api.request(MTPpayments_GetStarsStatus(\n\t\tMTP_flags(0),\n\t\t_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input()\n\t)).done([=](const TLResult &result) {\n\t\t_requestId = 0;\n\t\tconst auto &balance = result.data().vbalance();\n\t\t_peer->session().credits().apply(\n\t\t\t_peer->id,\n\t\t\tCreditsAmountFromTL(balance));\n\t\tif (const auto onstack = done) {\n\t\t\tonstack(StatusFromTL(result, _peer));\n\t\t}\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t\tif (const auto onstack = done) {\n\t\t\tonstack({});\n\t\t}\n\t}).send();\n}\n\nCreditsHistory::CreditsHistory(\n\tnot_null<PeerData*> peer,\n\tbool in,\n\tbool out,\n\tbool currency)\n: _peer(peer)\n, _flags(((in == out)\n\t? HistoryTL::Flags(0)\n\t: HistoryTL::Flags(0)\n\t\t| (in ? HistoryTL::Flag::f_inbound : HistoryTL::Flags(0))\n\t\t| (out ? HistoryTL::Flag::f_outbound : HistoryTL::Flags(0)))\n\t| (currency ? HistoryTL::Flag::f_ton : HistoryTL::Flags(0)))\n, _api(&peer->session().api().instance()) {\n}\n\nvoid CreditsHistory::request(\n\t\tconst Data::CreditsStatusSlice::OffsetToken &token,\n\t\tFn<void(Data::CreditsStatusSlice)> done) {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\t_requestId = _api.request(MTPpayments_GetStarsTransactions(\n\t\tMTP_flags(_flags),\n\t\tMTPstring(), // subscription_id\n\t\t_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input(),\n\t\tMTP_string(token),\n\t\tMTP_int(kTransactionsLimit)\n\t)).done([=](const MTPpayments_StarsStatus &result) {\n\t\t_requestId = 0;\n\t\tdone(StatusFromTL(result, _peer));\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t\tdone({});\n\t}).send();\n}\n\nvoid CreditsHistory::requestSubscriptions(\n\t\tconst Data::CreditsStatusSlice::OffsetToken &token,\n\t\tFn<void(Data::CreditsStatusSlice)> done,\n\t\tbool missingBalance) {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\t_requestId = _api.request(MTPpayments_GetStarsSubscriptions(\n\t\tMTP_flags(missingBalance\n\t\t\t? MTPpayments_getStarsSubscriptions::Flag::f_missing_balance\n\t\t\t: MTPpayments_getStarsSubscriptions::Flags(0)),\n\t\t_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input(),\n\t\tMTP_string(token)\n\t)).done([=](const MTPpayments_StarsStatus &result) {\n\t\t_requestId = 0;\n\t\tdone(StatusFromTL(result, _peer));\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t\tdone({});\n\t}).send();\n}\n\nrpl::producer<not_null<PeerData*>> PremiumPeerBot(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto username = session->appConfig().get<QString>(\n\t\tu\"premium_bot_username\"_q,\n\t\tQString());\n\tif (username.isEmpty()) {\n\t\treturn rpl::never<not_null<PeerData*>>();\n\t}\n\tif (const auto p = session->data().peerByUsername(username)) {\n\t\treturn rpl::single<not_null<PeerData*>>(p);\n\t}\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tconst auto api = lifetime.make_state<MTP::Sender>(&session->mtp());\n\n\t\tapi->request(MTPcontacts_ResolveUsername(\n\t\t\tMTP_flags(0),\n\t\t\tMTP_string(username),\n\t\t\tMTP_string()\n\t\t)).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\t\tsession->data().processUsers(result.data().vusers());\n\t\t\tsession->data().processChats(result.data().vchats());\n\t\t\tconst auto botPeer = session->data().peerLoaded(\n\t\t\t\tpeerFromMTP(result.data().vpeer()));\n\t\t\tif (!botPeer) {\n\t\t\t\treturn consumer.put_done();\n\t\t\t}\n\t\t\tconsumer.put_next(not_null{ botPeer });\n\t\t}).send();\n\n\t\treturn lifetime;\n\t};\n}\n\nCreditsEarnStatistics::CreditsEarnStatistics(not_null<PeerData*> peer)\n: StatisticsRequestSender(peer)\n, _isUser(peer->isUser()) {\n}\n\nrpl::producer<rpl::no_value, QString> CreditsEarnStatistics::request() {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tconst auto finish = [=](const QString &url) {\n\t\t\tapi().request(MTPpayments_GetStarsRevenueStats(\n\t\t\t\tMTP_flags(0),\n\t\t\t\t(_isUser ? user()->input() : channel()->input())\n\t\t\t)).done([=](const MTPpayments_StarsRevenueStats &result) {\n\t\t\t\tconst auto &data = result.data();\n\t\t\t\tconst auto &status = data.vstatus().data();\n\t\t\t\t_data = Data::CreditsEarnStatistics{\n\t\t\t\t\t.revenueGraph = StatisticalGraphFromTL(\n\t\t\t\t\t\tdata.vrevenue_graph()),\n\t\t\t\t\t.currentBalance = CreditsAmountFromTL(\n\t\t\t\t\t\tstatus.vcurrent_balance()),\n\t\t\t\t\t.availableBalance = CreditsAmountFromTL(\n\t\t\t\t\t\tstatus.vavailable_balance()),\n\t\t\t\t\t.overallRevenue = CreditsAmountFromTL(\n\t\t\t\t\t\tstatus.voverall_revenue()),\n\t\t\t\t\t.usdRate = data.vusd_rate().v,\n\t\t\t\t\t.isWithdrawalEnabled = status.is_withdrawal_enabled(),\n\t\t\t\t\t.nextWithdrawalAt = status.vnext_withdrawal_at()\n\t\t\t\t\t\t? base::unixtime::parse(\n\t\t\t\t\t\t\tstatus.vnext_withdrawal_at()->v)\n\t\t\t\t\t\t: QDateTime(),\n\t\t\t\t\t.buyAdsUrl = url,\n\t\t\t\t};\n\n\t\t\t\tconsumer.put_done();\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tconsumer.put_error_copy(error.type());\n\t\t\t}).send();\n\t\t};\n\n\t\tapi().request(\n\t\t\tMTPpayments_GetStarsRevenueAdsAccountUrl(\n\t\t\t\t(_isUser ? user()->input() : channel()->input()))\n\t\t).done([=](const MTPpayments_StarsRevenueAdsAccountUrl &result) {\n\t\t\tfinish(qs(result.data().vurl()));\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tfinish({});\n\t\t}).send();\n\n\t\treturn lifetime;\n\t};\n}\n\nData::CreditsEarnStatistics CreditsEarnStatistics::data() const {\n\treturn _data;\n}\n\nCreditsGiveawayOptions::CreditsGiveawayOptions(not_null<PeerData*> peer)\n: _peer(peer)\n, _api(&peer->session().api().instance()) {\n}\n\nrpl::producer<rpl::no_value, QString> CreditsGiveawayOptions::request() {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tusing TLOption = MTPStarsGiveawayOption;\n\n\t\tconst auto optionsFromTL = [=](const auto &options) {\n\t\t\treturn ranges::views::all(\n\t\t\t\toptions\n\t\t\t) | ranges::views::transform([=](const auto &option) {\n\t\t\t\treturn Data::CreditsGiveawayOption{\n\t\t\t\t\t.winners = ranges::views::all(\n\t\t\t\t\t\toption.data().vwinners().v\n\t\t\t\t\t) | ranges::views::transform([](const auto &winner) {\n\t\t\t\t\t\treturn Data::CreditsGiveawayOption::Winner{\n\t\t\t\t\t\t\t.users = winner.data().vusers().v,\n\t\t\t\t\t\t\t.perUserStars = winner.data().vper_user_stars().v,\n\t\t\t\t\t\t\t.isDefault = winner.data().is_default(),\n\t\t\t\t\t\t};\n\t\t\t\t\t}) | ranges::to_vector,\n\t\t\t\t\t.storeProduct = qs(\n\t\t\t\t\t\toption.data().vstore_product().value_or_empty()),\n\t\t\t\t\t.currency = qs(option.data().vcurrency()),\n\t\t\t\t\t.amount = option.data().vamount().v,\n\t\t\t\t\t.credits = option.data().vstars().v,\n\t\t\t\t\t.yearlyBoosts = option.data().vyearly_boosts().v,\n\t\t\t\t\t.isExtended = option.data().is_extended(),\n\t\t\t\t\t.isDefault = option.data().is_default(),\n\t\t\t\t};\n\t\t\t}) | ranges::to_vector;\n\t\t};\n\t\tconst auto fail = [=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t};\n\n\t\t_api.request(MTPpayments_GetStarsGiveawayOptions(\n\t\t)).done([=](const MTPVector<TLOption> &result) {\n\t\t\t_options = optionsFromTL(result.v);\n\t\t\tconsumer.put_done();\n\t\t}).fail(fail).send();\n\n\t\treturn lifetime;\n\t};\n}\n\nData::CreditsGiveawayOptions CreditsGiveawayOptions::options() const {\n\treturn _options;\n}\n\nvoid EditCreditsSubscription(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &id,\n\t\tbool cancel,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail) {\n\tusing Flag = MTPpayments_ChangeStarsSubscription::Flag;\n\tsession->api().request(\n\t\tMTPpayments_ChangeStarsSubscription(\n\t\t\tMTP_flags(Flag::f_canceled),\n\t\t\tMTP_inputPeerSelf(),\n\t\t\tMTP_string(id),\n\t\t\tMTP_bool(cancel)\n\t)).done(done).fail([=](const MTP::Error &e) { fail(e.type()); }).send();\n}\n\nMTPInputSavedStarGift InputSavedStarGiftId(\n\t\tconst Data::SavedStarGiftId &id,\n\t\tconst std::shared_ptr<Data::UniqueGift> &unique) {\n\treturn (!id && unique)\n\t\t? MTP_inputSavedStarGiftSlug(MTP_string(unique->slug))\n\t\t: id.isUser()\n\t\t? MTP_inputSavedStarGiftUser(MTP_int(id.userMessageId().bare))\n\t\t: MTP_inputSavedStarGiftChat(\n\t\t\tid.chat()->input(),\n\t\t\tMTP_long(id.chatSavedId()));\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_credits.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_statistics_sender.h\"\n#include \"data/data_credits.h\"\n#include \"data/data_credits_earn.h\"\n#include \"mtproto/sender.h\"\n\nnamespace Data {\nclass SavedStarGiftId;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nclass UserData;\n\nnamespace Api {\n\nclass CreditsTopupOptions final {\npublic:\n\tCreditsTopupOptions(not_null<PeerData*> peer);\n\n\t[[nodiscard]] rpl::producer<rpl::no_value, QString> request();\n\t[[nodiscard]] Data::CreditTopupOptions options() const;\n\nprivate:\n\tconst not_null<PeerData*> _peer;\n\n\tData::CreditTopupOptions _options;\n\n\tMTP::Sender _api;\n\n};\n\nclass CreditsGiveawayOptions final {\npublic:\n\tCreditsGiveawayOptions(not_null<PeerData*> peer);\n\n\t[[nodiscard]] rpl::producer<rpl::no_value, QString> request();\n\t[[nodiscard]] Data::CreditsGiveawayOptions options() const;\n\nprivate:\n\tconst not_null<PeerData*> _peer;\n\n\tData::CreditsGiveawayOptions _options;\n\n\tMTP::Sender _api;\n\n};\n\nclass CreditsStatus final {\npublic:\n\tCreditsStatus(not_null<PeerData*> peer);\n\n\tvoid request(\n\t\tconst Data::CreditsStatusSlice::OffsetToken &token,\n\t\tFn<void(Data::CreditsStatusSlice)> done);\n\nprivate:\n\tconst not_null<PeerData*> _peer;\n\n\tmtpRequestId _requestId = 0;\n\n\tMTP::Sender _api;\n\n};\n\nclass CreditsHistory final {\npublic:\n\tCreditsHistory(\n\t\tnot_null<PeerData*> peer,\n\t\tbool in,\n\t\tbool out,\n\t\tbool currency = false);\n\n\tvoid request(\n\t\tconst Data::CreditsStatusSlice::OffsetToken &token,\n\t\tFn<void(Data::CreditsStatusSlice)> done);\n\tvoid requestSubscriptions(\n\t\tconst Data::CreditsStatusSlice::OffsetToken &token,\n\t\tFn<void(Data::CreditsStatusSlice)> done,\n\t\tbool missingBalance = false);\n\nprivate:\n\tusing HistoryTL = MTPpayments_GetStarsTransactions;\n\tconst not_null<PeerData*> _peer;\n\tconst HistoryTL::Flags _flags;\n\n\tmtpRequestId _requestId = 0;\n\n\tMTP::Sender _api;\n\n};\n\nclass CreditsEarnStatistics final : public StatisticsRequestSender {\npublic:\n\texplicit CreditsEarnStatistics(not_null<PeerData*>);\n\n\t[[nodiscard]] rpl::producer<rpl::no_value, QString> request();\n\t[[nodiscard]] Data::CreditsEarnStatistics data() const;\n\nprivate:\n\tconst bool _isUser = false;\n\tData::CreditsEarnStatistics _data;\n\n};\n\n[[nodiscard]] rpl::producer<not_null<PeerData*>> PremiumPeerBot(\n\tnot_null<Main::Session*> session);\n\nvoid EditCreditsSubscription(\n\tnot_null<Main::Session*> session,\n\tconst QString &id,\n\tbool cancel,\n\tFn<void()> done,\n\tFn<void(QString)> fail);\n\n[[nodiscard]] MTPInputSavedStarGift InputSavedStarGiftId(\n\tconst Data::SavedStarGiftId &id,\n\tconst std::shared_ptr<Data::UniqueGift> &unique = nullptr);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_credits_history_entry.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_credits_history_entry.h\"\n\n#include \"api/api_premium.h\"\n#include \"base/unixtime.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_credits.h\"\n#include \"data/data_document.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"main/main_session.h\"\n\nnamespace Api {\n\nData::CreditsHistoryEntry CreditsHistoryEntryFromTL(\n\t\tconst MTPStarsTransaction &tl,\n\t\tnot_null<PeerData*> peer) {\n\tusing HistoryPeerTL = MTPDstarsTransactionPeer;\n\tusing namespace Data;\n\tconst auto owner = &peer->owner();\n\tconst auto photo = tl.data().vphoto()\n\t\t? owner->photoFromWeb(*tl.data().vphoto(), ImageLocation())\n\t\t: nullptr;\n\tauto extended = std::vector<CreditsHistoryMedia>();\n\tif (const auto list = tl.data().vextended_media()) {\n\t\textended.reserve(list->v.size());\n\t\tfor (const auto &media : list->v) {\n\t\t\tmedia.match([&](const MTPDmessageMediaPhoto &data) {\n\t\t\t\tif (const auto inner = data.vphoto()) {\n\t\t\t\t\tconst auto photo = owner->processPhoto(*inner);\n\t\t\t\t\tif (!photo->isNull()) {\n\t\t\t\t\t\textended.push_back(CreditsHistoryMedia{\n\t\t\t\t\t\t\t.type = CreditsHistoryMediaType::Photo,\n\t\t\t\t\t\t\t.id = photo->id,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, [&](const MTPDmessageMediaDocument &data) {\n\t\t\t\tif (const auto inner = data.vdocument()) {\n\t\t\t\t\tconst auto document = owner->processDocument(\n\t\t\t\t\t\t*inner,\n\t\t\t\t\t\tdata.valt_documents());\n\t\t\t\t\tif (document->isAnimation()\n\t\t\t\t\t\t|| document->isVideoFile()\n\t\t\t\t\t\t|| document->isGifv()) {\n\t\t\t\t\t\textended.push_back(CreditsHistoryMedia{\n\t\t\t\t\t\t\t.type = CreditsHistoryMediaType::Video,\n\t\t\t\t\t\t\t.id = document->id,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, [&](const auto &) {});\n\t\t}\n\t}\n\tconst auto barePeerId = tl.data().vpeer().match([](\n\t\t\tconst HistoryPeerTL &p) {\n\t\treturn peerFromMTP(p.vpeer());\n\t}, [](const auto &) {\n\t\treturn PeerId(0);\n\t}).value;\n\tconst auto stargift = tl.data().vstargift();\n\tconst auto nonUniqueGift = stargift\n\t\t? stargift->match([&](const MTPDstarGift &data) {\n\t\t\treturn &data;\n\t\t}, [](const auto &) { return (const MTPDstarGift*)nullptr; })\n\t\t: nullptr;\n\tconst auto reaction = tl.data().is_reaction();\n\tconst auto amount = CreditsAmountFromTL(tl.data().vamount());\n\tconst auto starrefAmount = CreditsAmountFromTL(\n\t\ttl.data().vstarref_amount());\n\tconst auto starrefCommission\n\t\t= tl.data().vstarref_commission_permille().value_or_empty();\n\tconst auto starrefBarePeerId = tl.data().vstarref_peer()\n\t\t? peerFromMTP(*tl.data().vstarref_peer()).value\n\t\t: 0;\n\tconst auto incoming = (amount >= CreditsAmount());\n\tconst auto paidMessagesCount\n\t\t= tl.data().vpaid_messages().value_or_empty();\n\tconst auto premiumMonthsForStars\n\t\t= tl.data().vpremium_gift_months().value_or_empty();\n\tconst auto saveActorId = (reaction\n\t\t|| !extended.empty()\n\t\t|| paidMessagesCount) && incoming;\n\tconst auto parsedGift = stargift\n\t\t? FromTL(&peer->session(), *stargift)\n\t\t: std::optional<Data::StarGift>();\n\tconst auto giftStickerId = parsedGift ? parsedGift->document->id : 0;\n\treturn Data::CreditsHistoryEntry{\n\t\t.id = qs(tl.data().vid()),\n\t\t.title = qs(tl.data().vtitle().value_or_empty()),\n\t\t.description = { qs(tl.data().vdescription().value_or_empty()) },\n\t\t.date = base::unixtime::parse(\n\t\t\ttl.data().vads_proceeds_from_date().value_or(\n\t\t\t\ttl.data().vdate().v)),\n\t\t.photoId = photo ? photo->id : 0,\n\t\t.extended = std::move(extended),\n\t\t.credits = CreditsAmountFromTL(tl.data().vamount()),\n\t\t.bareMsgId = uint64(tl.data().vmsg_id().value_or_empty()),\n\t\t.barePeerId = saveActorId ? peer->id.value : barePeerId,\n\t\t.bareGiveawayMsgId = uint64(\n\t\t\ttl.data().vgiveaway_post_id().value_or_empty()),\n\t\t.bareGiftStickerId = giftStickerId,\n\t\t.bareActorId = saveActorId ? barePeerId : uint64(0),\n\t\t.uniqueGift = parsedGift ? parsedGift->unique : nullptr,\n\t\t.starrefAmount = paidMessagesCount ? CreditsAmount() : starrefAmount,\n\t\t.starrefCommission = paidMessagesCount ? 0 : starrefCommission,\n\t\t.starrefRecipientId = paidMessagesCount ? 0 : starrefBarePeerId,\n\t\t.peerType = tl.data().vpeer().match([](const HistoryPeerTL &) {\n\t\t\treturn Data::CreditsHistoryEntry::PeerType::Peer;\n\t\t}, [](const MTPDstarsTransactionPeerPlayMarket &) {\n\t\t\treturn Data::CreditsHistoryEntry::PeerType::PlayMarket;\n\t\t}, [](const MTPDstarsTransactionPeerFragment &) {\n\t\t\treturn Data::CreditsHistoryEntry::PeerType::Fragment;\n\t\t}, [](const MTPDstarsTransactionPeerAppStore &) {\n\t\t\treturn Data::CreditsHistoryEntry::PeerType::AppStore;\n\t\t}, [](const MTPDstarsTransactionPeerUnsupported &) {\n\t\t\treturn Data::CreditsHistoryEntry::PeerType::Unsupported;\n\t\t}, [](const MTPDstarsTransactionPeerPremiumBot &) {\n\t\t\treturn Data::CreditsHistoryEntry::PeerType::PremiumBot;\n\t\t}, [](const MTPDstarsTransactionPeerAds &) {\n\t\t\treturn Data::CreditsHistoryEntry::PeerType::Ads;\n\t\t}, [](const MTPDstarsTransactionPeerAPI &) {\n\t\t\treturn Data::CreditsHistoryEntry::PeerType::API;\n\t\t}),\n\t\t.subscriptionUntil = tl.data().vsubscription_period()\n\t\t\t? base::unixtime::parse(base::unixtime::now()\n\t\t\t\t+ tl.data().vsubscription_period()->v)\n\t\t\t: QDateTime(),\n\t\t.adsProceedsToDate = tl.data().vads_proceeds_to_date()\n\t\t\t? base::unixtime::parse(tl.data().vads_proceeds_to_date()->v)\n\t\t\t: QDateTime(),\n\t\t.successDate = tl.data().vtransaction_date()\n\t\t\t? base::unixtime::parse(tl.data().vtransaction_date()->v)\n\t\t\t: QDateTime(),\n\t\t.successLink = qs(tl.data().vtransaction_url().value_or_empty()),\n\t\t.paidMessagesCount = paidMessagesCount,\n\t\t.paidMessagesAmount = (paidMessagesCount\n\t\t\t? starrefAmount\n\t\t\t: CreditsAmount()),\n\t\t.paidMessagesCommission = paidMessagesCount ? starrefCommission : 0,\n\t\t.limitedCount = parsedGift ? parsedGift->limitedCount : 0,\n\t\t.limitedLeft = parsedGift ? parsedGift->limitedLeft : 0,\n\t\t.starsConverted = int(nonUniqueGift\n\t\t\t? nonUniqueGift->vconvert_stars().v\n\t\t\t: 0),\n\t\t.premiumMonthsForStars = premiumMonthsForStars,\n\t\t.floodSkip = int(tl.data().vfloodskip_number().value_or(0)),\n\t\t.converted = stargift && incoming,\n\t\t.stargift = stargift.has_value(),\n\t\t.postsSearch = tl.data().is_posts_search(),\n\t\t.giftUpgraded = tl.data().is_stargift_upgrade(),\n\t\t.giftResale = tl.data().is_stargift_resale(),\n\t\t.reaction = tl.data().is_reaction(),\n\t\t.refunded = tl.data().is_refund(),\n\t\t.pending = tl.data().is_pending(),\n\t\t.failed = tl.data().is_failed(),\n\t\t.in = incoming,\n\t\t.gift = tl.data().is_gift() || stargift.has_value(),\n\t};\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_credits_history_entry.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\n\nnamespace Data {\nstruct CreditsHistoryEntry;\n} // namespace Data\n\nnamespace Api {\n\n[[nodiscard]] Data::CreditsHistoryEntry CreditsHistoryEntryFromTL(\n\tconst MTPStarsTransaction &tl,\n\tnot_null<PeerData*> peer);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_earn.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_earn.h\"\n\n#include \"api/api_cloud_password.h\"\n#include \"apiwrap.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"boxes/passcode_box.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"ui/widgets/buttons.h\"\n\nnamespace Api {\n\nvoid RestrictSponsored(\n\t\tnot_null<ChannelData*> channel,\n\t\tbool restricted,\n\t\tFn<void(QString)> failed) {\n\tchannel->session().api().request(MTPchannels_RestrictSponsoredMessages(\n\t\tchannel->inputChannel(),\n\t\tMTP_bool(restricted))\n\t).done([=](const MTPUpdates &updates) {\n\t\tchannel->session().api().applyUpdates(updates);\n\t}).fail([=](const MTP::Error &error) {\n\t\tfailed(error.type());\n\t}).send();\n}\n\nvoid HandleWithdrawalButton(\n\t\tRewardReceiver receiver,\n\t\tnot_null<Ui::RippleButton*> button,\n\t\tstd::shared_ptr<Ui::Show> show) {\n\tExpects(receiver.currencyReceiver\n\t\t|| (receiver.creditsReceiver && receiver.creditsAmount));\n\n\tstruct State {\n\t\trpl::lifetime lifetime;\n\t\tbool loading = false;\n\t};\n\n\tconst auto currencyReceiver = receiver.currencyReceiver;\n\tconst auto creditsReceiver = receiver.creditsReceiver;\n\tconst auto isChannel = receiver.currencyReceiver\n\t\t&& receiver.currencyReceiver->isChannel();\n\n\tconst auto state = button->lifetime().make_state<State>();\n\tconst auto session = (currencyReceiver\n\t\t? &currencyReceiver->session()\n\t\t: &creditsReceiver->session());\n\n\tusing CreditsOutUrl = MTPpayments_StarsRevenueWithdrawalUrl;\n\n\tsession->api().cloudPassword().reload();\n\tconst auto processOut = [=] {\n\t\tif (state->loading) {\n\t\t\treturn;\n\t\t} else if (creditsReceiver && !receiver.creditsAmount()) {\n\t\t\treturn;\n\t\t}\n\t\tstate->loading = true;\n\t\tstate->lifetime = session->api().cloudPassword().state(\n\t\t) | rpl::take(\n\t\t\t1\n\t\t) | rpl::on_next([=](const Core::CloudPasswordState &pass) {\n\t\t\tstate->loading = false;\n\n\t\t\tauto fields = PasscodeBox::CloudFields::From(pass);\n\t\t\tfields.customTitle = isChannel\n\t\t\t\t? tr::lng_channel_earn_balance_password_title()\n\t\t\t\t: tr::lng_bot_earn_balance_password_title();\n\t\t\tfields.customDescription = isChannel\n\t\t\t\t? tr::lng_channel_earn_balance_password_description(tr::now)\n\t\t\t\t: tr::lng_bot_earn_balance_password_description(tr::now);\n\t\t\tfields.customSubmitButton = tr::lng_passcode_submit();\n\t\t\tfields.customCheckCallback = crl::guard(button, [=](\n\t\t\t\t\tconst Core::CloudPasswordResult &result,\n\t\t\t\t\tbase::weak_qptr<PasscodeBox> box) {\n\t\t\t\tconst auto done = [=](const QString &result) {\n\t\t\t\t\tif (!result.isEmpty()) {\n\t\t\t\t\t\tUrlClickHandler::Open(result);\n\t\t\t\t\t\tif (box) {\n\t\t\t\t\t\t\tbox->closeBox();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tconst auto fail = [=](const MTP::Error &error) {\n\t\t\t\t\tconst auto message = error.type();\n\t\t\t\t\tif (box && !box->handleCustomCheckError(message)) {\n\t\t\t\t\t\tshow->showToast(message);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tif (currencyReceiver || creditsReceiver) {\n\t\t\t\t\tusing F = MTPpayments_getStarsRevenueWithdrawalUrl::Flag;\n\t\t\t\t\tsession->api().request(\n\t\t\t\t\t\tMTPpayments_GetStarsRevenueWithdrawalUrl(\n\t\t\t\t\t\t\tMTP_flags(currencyReceiver\n\t\t\t\t\t\t\t\t? F::f_ton\n\t\t\t\t\t\t\t\t: F::f_amount),\n\t\t\t\t\t\t\tcurrencyReceiver\n\t\t\t\t\t\t\t\t? currencyReceiver->input()\n\t\t\t\t\t\t\t\t: creditsReceiver->input(),\n\t\t\t\t\t\t\tMTP_long(creditsReceiver\n\t\t\t\t\t\t\t\t? receiver.creditsAmount()\n\t\t\t\t\t\t\t\t: 0),\n\t\t\t\t\t\t\tresult.result\n\t\t\t\t\t)).done([=](const CreditsOutUrl &r) {\n\t\t\t\t\t\tdone(qs(r.data().vurl()));\n\t\t\t\t\t}).fail(fail).send();\n\t\t\t\t}\n\t\t\t});\n\t\t\tshow->show(Box<PasscodeBox>(session, fields));\n\t\t});\n\t};\n\tbutton->setClickedCallback([=] {\n\t\tif (state->loading) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto fail = [=](const MTP::Error &error) {\n\t\t\tauto box = PrePasswordErrorBox(\n\t\t\t\terror.type(),\n\t\t\t\tsession,\n\t\t\t\tTextWithEntities{\n\t\t\t\t\ttr::lng_channel_earn_out_check_password_about(tr::now),\n\t\t\t\t});\n\t\t\tif (box) {\n\t\t\t\tshow->show(std::move(box));\n\t\t\t\tstate->loading = false;\n\t\t\t} else {\n\t\t\t\tprocessOut();\n\t\t\t}\n\t\t};\n\t\tif (currencyReceiver || creditsReceiver) {\n\t\t\tusing F = MTPpayments_getStarsRevenueWithdrawalUrl::Flag;\n\t\t\tsession->api().request(\n\t\t\t\tMTPpayments_GetStarsRevenueWithdrawalUrl(\n\t\t\t\t\tMTP_flags(currencyReceiver\n\t\t\t\t\t\t? F::f_ton\n\t\t\t\t\t\t: F::f_amount),\n\t\t\t\t\tcurrencyReceiver\n\t\t\t\t\t\t? currencyReceiver->input()\n\t\t\t\t\t\t: creditsReceiver->input(),\n\t\t\t\t\tMTP_long(creditsReceiver\n\t\t\t\t\t\t? receiver.creditsAmount()\n\t\t\t\t\t\t: 0),\n\t\t\t\t\tMTP_inputCheckPasswordEmpty()\n\t\t\t)).fail(fail).send();\n\t\t}\n\t});\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_earn.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass ChannelData;\n\nnamespace Ui {\nclass RippleButton;\nclass Show;\n} // namespace Ui\n\nnamespace Api {\n\nvoid RestrictSponsored(\n\tnot_null<ChannelData*> channel,\n\tbool restricted,\n\tFn<void(QString)> failed);\n\nstruct RewardReceiver final {\n\tPeerData *currencyReceiver = nullptr;\n\tPeerData *creditsReceiver = nullptr;\n\tFn<uint64()> creditsAmount;\n};\n\nvoid HandleWithdrawalButton(\n\tRewardReceiver receiver,\n\tnot_null<Ui::RippleButton*> button,\n\tstd::shared_ptr<Ui::Show> show);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_editing.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_editing.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_media.h\"\n#include \"api/api_text_entities.h\"\n#include \"base/random.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/components/scheduled_messages.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_todo_list.h\"\n#include \"data/data_web_page.h\"\n#include \"history/view/controls/history_view_compose_media_edit_manager.h\"\n#include \"history/history.h\"\n#include \"history/history_item_components.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/mtproto_response.h\"\n#include \"boxes/abstract_box.h\" // Ui::show().\n\nnamespace Api {\nnamespace {\n\nusing namespace rpl::details;\n\ntemplate <typename T>\nconstexpr auto WithId\n\t= is_callable_plain_v<T, Fn<void()>, mtpRequestId>;\ntemplate <typename T>\nconstexpr auto WithoutId\n\t= is_callable_plain_v<T, Fn<void()>>;\ntemplate <typename T>\nconstexpr auto WithoutCallback\n\t= is_callable_plain_v<T>;\ntemplate <typename T>\nconstexpr auto ErrorWithId\n\t= is_callable_plain_v<T, QString, mtpRequestId>;\ntemplate <typename T>\nconstexpr auto ErrorWithoutId\n\t= is_callable_plain_v<T, QString>;\n\ntemplate <typename DoneCallback, typename FailCallback>\nmtpRequestId SuggestMessage(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst TextWithEntities &textWithEntities,\n\t\tData::WebPageDraft webpage,\n\t\tSendOptions options,\n\t\tDoneCallback &&done,\n\t\tFailCallback &&fail) {\n\tExpects(options.suggest.exists);\n\tExpects(!options.scheduled);\n\n\tconst auto session = &item->history()->session();\n\tconst auto api = &session->api();\n\n\tconst auto thread = item->history()->amMonoforumAdmin()\n\t\t? item->savedSublist()\n\t\t: (Data::Thread*)item->history();\n\tauto action = SendAction(thread, options);\n\taction.replyTo = FullReplyTo{\n\t\t.messageId = item->fullId(),\n\t\t.monoforumPeerId = (item->history()->amMonoforumAdmin()\n\t\t\t? item->sublistPeerId()\n\t\t\t: PeerId()),\n\t};\n\n\tauto message = MessageToSend(std::move(action));\n\tmessage.textWithTags = TextWithTags{\n\t\ttextWithEntities.text,\n\t\tTextUtilities::ConvertEntitiesToTextTags(textWithEntities.entities)\n\t};\n\tmessage.webPage = webpage;\n\tapi->sendMessage(std::move(message));\n\n\tconst auto requestId = -1;\n\tcrl::on_main(session, [=] {\n\t\tconst auto type = u\"MESSAGE_NOT_MODIFIED\"_q;\n\t\tif constexpr (ErrorWithId<FailCallback>) {\n\t\t\tfail(type, requestId);\n\t\t} else if constexpr (ErrorWithoutId<FailCallback>) {\n\t\t\tfail(type);\n\t\t} else if constexpr (WithoutCallback<FailCallback>) {\n\t\t\tfail();\n\t\t} else {\n\t\t\tt_bad_callback(fail);\n\t\t}\n\t});\n\treturn requestId;\n}\n\ntemplate <typename DoneCallback, typename FailCallback>\nmtpRequestId SuggestMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst TextWithEntities &textWithEntities,\n\t\tData::WebPageDraft webpage,\n\t\tSendOptions options,\n\t\tDoneCallback &&done,\n\t\tFailCallback &&fail,\n\t\tstd::optional<MTPInputMedia> inputMedia) {\n\tExpects(options.suggest.exists);\n\tExpects(!options.scheduled);\n\n\tconst auto session = &item->history()->session();\n\tconst auto api = &session->api();\n\n\tconst auto text = textWithEntities.text;\n\tconst auto sentEntities = EntitiesToMTP(\n\t\tsession,\n\t\ttextWithEntities.entities,\n\t\tConvertOption::SkipLocal);\n\n\tconst auto updateRecentStickers = inputMedia\n\t\t? Api::HasAttachedStickers(*inputMedia)\n\t\t: false;\n\n\tconst auto emptyFlag = MTPmessages_SendMedia::Flag(0);\n\tauto replyTo = FullReplyTo{\n\t\t.messageId = item->fullId(),\n\t\t.monoforumPeerId = (item->history()->amMonoforumAdmin()\n\t\t\t? item->sublistPeerId()\n\t\t\t: PeerId()),\n\t};\n\tconst auto flags = emptyFlag\n\t\t| MTPmessages_SendMedia::Flag::f_reply_to\n\t\t| MTPmessages_SendMedia::Flag::f_suggested_post\n\t\t| (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert)\n\t\t\t|| options.invertCaption)\n\t\t\t? MTPmessages_SendMedia::Flag::f_invert_media\n\t\t\t: emptyFlag)\n\t\t| (!sentEntities.v.isEmpty()\n\t\t\t? MTPmessages_SendMedia::Flag::f_entities\n\t\t\t: emptyFlag)\n\t\t| (options.starsApproved\n\t\t\t? MTPmessages_SendMedia::Flag::f_allow_paid_stars\n\t\t\t: emptyFlag);\n\tconst auto randomId = base::RandomValue<uint64>();\n\treturn api->request(MTPmessages_SendMedia(\n\t\tMTP_flags(flags),\n\t\titem->history()->peer->input(),\n\t\tReplyToForMTP(item->history(), replyTo),\n\t\tinputMedia.value_or(Data::WebPageForMTP(webpage, text.isEmpty())),\n\t\tMTP_string(text),\n\t\tMTP_long(randomId),\n\t\tMTPReplyMarkup(),\n\t\tsentEntities,\n\t\tMTPint(), // schedule_date\n\t\tMTPint(), // schedule_repeat_period\n\t\tMTPInputPeer(), // send_as\n\t\tMTPInputQuickReplyShortcut(), // quick_reply_shortcut\n\t\tMTPlong(), // effect\n\t\tMTP_long(options.starsApproved),\n\t\tApi::SuggestToMTP(options.suggest)\n\t)).done([=](\n\t\t\tconst MTPUpdates &result,\n\t\t\t[[maybe_unused]] mtpRequestId requestId) {\n\t\tconst auto apply = [=] { api->applyUpdates(result); };\n\n\t\tif constexpr (WithId<DoneCallback>) {\n\t\t\tdone(apply, requestId);\n\t\t} else if constexpr (WithoutId<DoneCallback>) {\n\t\t\tdone(apply);\n\t\t} else if constexpr (WithoutCallback<DoneCallback>) {\n\t\t\tdone();\n\t\t\tapply();\n\t\t} else {\n\t\t\tt_bad_callback(done);\n\t\t}\n\n\t\tif (updateRecentStickers) {\n\t\t\tapi->requestSpecialStickersForce(false, false, true);\n\t\t}\n\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\tif constexpr (ErrorWithId<FailCallback>) {\n\t\t\tfail(error.type(), requestId);\n\t\t} else if constexpr (ErrorWithoutId<FailCallback>) {\n\t\t\tfail(error.type());\n\t\t} else if constexpr (WithoutCallback<FailCallback>) {\n\t\t\tfail();\n\t\t} else {\n\t\t\tt_bad_callback(fail);\n\t\t}\n\t}).send();\n}\n\ntemplate <typename DoneCallback, typename FailCallback>\nmtpRequestId SuggestMessageOrMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst TextWithEntities &textWithEntities,\n\t\tData::WebPageDraft webpage,\n\t\tSendOptions options,\n\t\tDoneCallback &&done,\n\t\tFailCallback &&fail,\n\t\tstd::optional<MTPInputMedia> inputMedia) {\n\tconst auto wasMedia = item->media();\n\tif (!inputMedia && wasMedia && wasMedia->allowsEditCaption()) {\n\t\tif (const auto photo = wasMedia->photo()) {\n\t\t\tinputMedia = MTP_inputMediaPhoto(\n\t\t\t\tMTP_flags(0),\n\t\t\t\tphoto->mtpInput(),\n\t\t\t\tMTPint(), // ttl_seconds\n\t\t\t\tMTPInputDocument()); // video\n\t\t} else if (const auto document = wasMedia->document()) {\n\t\t\tinputMedia = MTP_inputMediaDocument(\n\t\t\t\tMTP_flags(0),\n\t\t\t\tdocument->mtpInput(),\n\t\t\t\tMTPInputPhoto(), // video_cover\n\t\t\t\tMTPint(), // video_timestamp\n\t\t\t\tMTPint(), // ttl_seconds\n\t\t\t\tMTPstring()); // query\n\t\t}\n\t}\n\tif (inputMedia) {\n\t\treturn SuggestMedia(\n\t\t\titem,\n\t\t\ttextWithEntities,\n\t\t\twebpage,\n\t\t\toptions,\n\t\t\tstd::move(done),\n\t\t\tstd::move(fail),\n\t\t\tinputMedia);\n\t}\n\treturn SuggestMessage(\n\t\titem,\n\t\ttextWithEntities,\n\t\twebpage,\n\t\toptions,\n\t\tstd::move(done),\n\t\tstd::move(fail));\n}\n\ntemplate <typename DoneCallback, typename FailCallback>\nmtpRequestId EditMessage(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst TextWithEntities &textWithEntities,\n\t\tData::WebPageDraft webpage,\n\t\tSendOptions options,\n\t\tDoneCallback &&done,\n\t\tFailCallback &&fail,\n\t\tstd::optional<MTPInputMedia> inputMedia = std::nullopt) {\n\tif (item->computeSuggestionActions()\n\t\t== SuggestionActions::AcceptAndDecline) {\n\t\treturn SuggestMessageOrMedia(\n\t\t\titem,\n\t\t\ttextWithEntities,\n\t\t\twebpage,\n\t\t\toptions,\n\t\t\tstd::move(done),\n\t\t\tstd::move(fail),\n\t\t\tinputMedia);\n\t}\n\n\tconst auto session = &item->history()->session();\n\tconst auto api = &session->api();\n\n\tconst auto text = textWithEntities.text;\n\tconst auto sentEntities = EntitiesToMTP(\n\t\tsession,\n\t\ttextWithEntities.entities,\n\t\tConvertOption::SkipLocal);\n\tconst auto media = item->media();\n\n\tconst auto updateRecentStickers = inputMedia.has_value()\n\t\t? Api::HasAttachedStickers(*inputMedia)\n\t\t: false;\n\n\tconst auto emptyFlag = MTPmessages_EditMessage::Flag(0);\n\tconst auto flags = emptyFlag\n\t\t| ((!text.isEmpty() || media)\n\t\t\t? MTPmessages_EditMessage::Flag::f_message\n\t\t\t: emptyFlag)\n\t\t| ((media && inputMedia.has_value())\n\t\t\t? MTPmessages_EditMessage::Flag::f_media\n\t\t\t: emptyFlag)\n\t\t| (webpage.removed\n\t\t\t? MTPmessages_EditMessage::Flag::f_no_webpage\n\t\t\t: emptyFlag)\n\t\t| ((!webpage.removed && !webpage.url.isEmpty())\n\t\t\t? MTPmessages_EditMessage::Flag::f_media\n\t\t\t: emptyFlag)\n\t\t| (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert)\n\t\t\t|| options.invertCaption)\n\t\t\t? MTPmessages_EditMessage::Flag::f_invert_media\n\t\t\t: emptyFlag)\n\t\t| (!sentEntities.v.isEmpty()\n\t\t\t? MTPmessages_EditMessage::Flag::f_entities\n\t\t\t: emptyFlag)\n\t\t| (options.scheduled\n\t\t\t? MTPmessages_EditMessage::Flag::f_schedule_date\n\t\t\t: emptyFlag)\n\t\t| ((options.scheduled && options.scheduleRepeatPeriod)\n\t\t\t? MTPmessages_EditMessage::Flag::f_schedule_repeat_period\n\t\t\t: emptyFlag)\n\t\t| (item->isBusinessShortcut()\n\t\t\t? MTPmessages_EditMessage::Flag::f_quick_reply_shortcut_id\n\t\t\t: emptyFlag);\n\n\tconst auto id = item->isScheduled()\n\t\t? session->scheduledMessages().lookupId(item)\n\t\t: item->isBusinessShortcut()\n\t\t? session->data().shortcutMessages().lookupId(item)\n\t\t: item->id;\n\treturn api->request(MTPmessages_EditMessage(\n\t\tMTP_flags(flags),\n\t\titem->history()->peer->input(),\n\t\tMTP_int(id),\n\t\tMTP_string(text),\n\t\tinputMedia.value_or(Data::WebPageForMTP(webpage, text.isEmpty())),\n\t\tMTPReplyMarkup(),\n\t\tsentEntities,\n\t\tMTP_int(options.scheduled),\n\t\tMTP_int(options.scheduleRepeatPeriod),\n\t\tMTP_int(item->shortcutId())\n\t)).done([=](\n\t\t\tconst MTPUpdates &result,\n\t\t\t[[maybe_unused]] mtpRequestId requestId) {\n\t\tconst auto apply = [=] { api->applyUpdates(result); };\n\n\t\tif constexpr (WithId<DoneCallback>) {\n\t\t\tdone(apply, requestId);\n\t\t} else if constexpr (WithoutId<DoneCallback>) {\n\t\t\tdone(apply);\n\t\t} else if constexpr (WithoutCallback<DoneCallback>) {\n\t\t\tdone();\n\t\t\tapply();\n\t\t} else {\n\t\t\tt_bad_callback(done);\n\t\t}\n\n\t\tif (updateRecentStickers) {\n\t\t\tapi->requestSpecialStickersForce(false, false, true);\n\t\t}\n\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\tif constexpr (ErrorWithId<FailCallback>) {\n\t\t\tfail(error.type(), requestId);\n\t\t} else if constexpr (ErrorWithoutId<FailCallback>) {\n\t\t\tfail(error.type());\n\t\t} else if constexpr (WithoutCallback<FailCallback>) {\n\t\t\tfail();\n\t\t} else {\n\t\t\tt_bad_callback(fail);\n\t\t}\n\t}).send();\n}\n\ntemplate <typename DoneCallback, typename FailCallback>\nmtpRequestId EditMessage(\n\t\tnot_null<HistoryItem*> item,\n\t\tSendOptions options,\n\t\tDoneCallback &&done,\n\t\tFailCallback &&fail,\n\t\tstd::optional<MTPInputMedia> inputMedia = std::nullopt) {\n\tconst auto &text = item->originalText();\n\tconst auto webpage = (!item->media() || !item->media()->webpage())\n\t\t? Data::WebPageDraft{ .removed = true }\n\t\t: Data::WebPageDraft::FromItem(item);\n\treturn EditMessage(\n\t\titem,\n\t\ttext,\n\t\twebpage,\n\t\toptions,\n\t\tstd::forward<DoneCallback>(done),\n\t\tstd::forward<FailCallback>(fail),\n\t\tinputMedia);\n}\n\nvoid EditMessageWithUploadedMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tSendOptions options,\n\t\tMTPInputMedia media) {\n\tconst auto done = [=](Fn<void()> applyUpdates) {\n\t\tif (item) {\n\t\t\titem->removeFromSharedMediaIndex();\n\t\t\titem->clearSavedMedia();\n\t\t\titem->setIsLocalUpdateMedia(true);\n\t\t\tapplyUpdates();\n\t\t\titem->setIsLocalUpdateMedia(false);\n\t\t}\n\t};\n\tconst auto fail = [=](const QString &error) {\n\t\tconst auto session = &item->history()->session();\n\t\tconst auto notModified = (error == u\"MESSAGE_NOT_MODIFIED\"_q);\n\t\tconst auto mediaInvalid = (error == u\"MEDIA_NEW_INVALID\"_q);\n\t\tif (notModified || mediaInvalid) {\n\t\t\titem->returnSavedMedia();\n\t\t\tsession->data().sendHistoryChangeNotifications();\n\t\t\tif (mediaInvalid) {\n\t\t\t\tUi::show(\n\t\t\t\t\tUi::MakeInformBox(tr::lng_edit_media_invalid_file()),\n\t\t\t\t\tUi::LayerOption::KeepOther);\n\t\t\t}\n\t\t} else {\n\t\t\tsession->api().sendMessageFail(error, item->history()->peer);\n\t\t}\n\t};\n\n\tEditMessage(item, options, done, fail, media);\n}\n\n} // namespace\n\nvoid RescheduleMessage(\n\t\tnot_null<HistoryItem*> item,\n\t\tSendOptions options) {\n\tconst auto empty = [] {};\n\toptions.invertCaption = item->invertMedia();\n\tEditMessage(item, options, empty, empty);\n}\n\nvoid EditMessageWithUploadedDocument(\n\t\tHistoryItem *item,\n\t\tRemoteFileInfo info,\n\t\tSendOptions options) {\n\tif (!item || !item->media() || !item->media()->document()) {\n\t\treturn;\n\t}\n\tEditMessageWithUploadedMedia(\n\t\titem,\n\t\toptions,\n\t\tPrepareUploadedDocument(item, std::move(info)));\n}\n\nvoid EditMessageWithUploadedPhoto(\n\t\tHistoryItem *item,\n\t\tRemoteFileInfo info,\n\t\tSendOptions options) {\n\tif (!item || !item->media() || !item->media()->photo()) {\n\t\treturn;\n\t}\n\tEditMessageWithUploadedMedia(\n\t\titem,\n\t\toptions,\n\t\tPrepareUploadedPhoto(item, std::move(info)));\n}\n\nmtpRequestId EditCaption(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst TextWithEntities &caption,\n\t\tSendOptions options,\n\t\tFn<void()> done,\n\t\tFn<void(const QString &)> fail) {\n\treturn EditMessage(\n\t\titem,\n\t\tcaption,\n\t\tData::WebPageDraft(),\n\t\toptions,\n\t\tdone,\n\t\tfail);\n}\n\nmtpRequestId EditTextMessage(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst TextWithEntities &caption,\n\t\tData::WebPageDraft webpage,\n\t\tSendOptions options,\n\t\tFn<void(mtpRequestId requestId)> done,\n\t\tFn<void(const QString &error, mtpRequestId requestId)> fail,\n\t\tbool spoilered) {\n\tconst auto media = item->media();\n\tif (media\n\t\t&& HistoryView::MediaEditManager::CanBeSpoilered(item)\n\t\t&& spoilered != media->hasSpoiler()) {\n\t\tauto takeInputMedia = Fn<std::optional<MTPInputMedia>()>(nullptr);\n\t\tauto takeFileReference = Fn<QByteArray()>(nullptr);\n\t\tif (const auto photo = media->photo()) {\n\t\t\tusing Flag = MTPDinputMediaPhoto::Flag;\n\t\t\tconst auto flags = Flag()\n\t\t\t\t| (media->ttlSeconds() ? Flag::f_ttl_seconds : Flag())\n\t\t\t\t| (spoilered ? Flag::f_spoiler : Flag());\n\t\t\ttakeInputMedia = [=] {\n\t\t\t\treturn MTP_inputMediaPhoto(\n\t\t\t\t\tMTP_flags(flags),\n\t\t\t\t\tphoto->mtpInput(),\n\t\t\t\t\tMTP_int(media->ttlSeconds()),\n\t\t\t\t\tMTPInputDocument()); // video\n\t\t\t};\n\t\t\ttakeFileReference = [=] { return photo->fileReference(); };\n\t\t} else if (const auto document = media->document()) {\n\t\t\tusing Flag = MTPDinputMediaDocument::Flag;\n\t\t\tconst auto videoCover = media->videoCover();\n\t\t\tconst auto videoTimestamp = media->videoTimestamp();\n\t\t\tconst auto flags = Flag()\n\t\t\t\t| (media->ttlSeconds() ? Flag::f_ttl_seconds : Flag())\n\t\t\t\t| (spoilered ? Flag::f_spoiler : Flag())\n\t\t\t\t| (videoTimestamp ? Flag::f_video_timestamp : Flag())\n\t\t\t\t| (videoCover ? Flag::f_video_cover : Flag());\n\t\t\ttakeInputMedia = [=] {\n\t\t\t\treturn MTP_inputMediaDocument(\n\t\t\t\t\tMTP_flags(flags),\n\t\t\t\t\tdocument->mtpInput(),\n\t\t\t\t\t(videoCover\n\t\t\t\t\t\t? videoCover->mtpInput()\n\t\t\t\t\t\t: MTPInputPhoto()),\n\t\t\t\t\tMTP_int(media->ttlSeconds()),\n\t\t\t\t\tMTP_int(videoTimestamp),\n\t\t\t\t\tMTPstring()); // query\n\t\t\t};\n\t\t\ttakeFileReference = [=] { return document->fileReference(); };\n\t\t}\n\n\t\tconst auto usedFileReference = takeFileReference\n\t\t\t? takeFileReference()\n\t\t\t: QByteArray();\n\t\tconst auto origin = item->fullId();\n\t\tconst auto api = &item->history()->session().api();\n\t\tconst auto performRequest = [=](\n\t\t\t\tconst auto &repeatRequest,\n\t\t\t\tmtpRequestId originalRequestId) -> mtpRequestId {\n\t\t\tconst auto handleReference = [=](\n\t\t\t\t\tconst QString &error,\n\t\t\t\t\tmtpRequestId requestId) {\n\t\t\t\tif (error.startsWith(u\"FILE_REFERENCE_\"_q)) {\n\t\t\t\t\tapi->refreshFileReference(origin, [=](const auto &) {\n\t\t\t\t\t\tif (takeFileReference &&\n\t\t\t\t\t\t\t(takeFileReference() != usedFileReference)) {\n\t\t\t\t\t\t\trepeatRequest(\n\t\t\t\t\t\t\t\trepeatRequest,\n\t\t\t\t\t\t\t\toriginalRequestId\n\t\t\t\t\t\t\t\t\t? originalRequestId\n\t\t\t\t\t\t\t\t\t: requestId);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tfail(error, requestId);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tfail(error, requestId);\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto callback = [=](\n\t\t\t\t\tFn<void()> applyUpdates,\n\t\t\t\t\tmtpRequestId requestId) {\n\t\t\t\tapplyUpdates();\n\t\t\t\tdone(originalRequestId ? originalRequestId : requestId);\n\t\t\t};\n\t\t\tconst auto requestId = EditMessage(\n\t\t\t\titem,\n\t\t\t\tcaption,\n\t\t\t\twebpage,\n\t\t\t\toptions,\n\t\t\t\tcallback,\n\t\t\t\thandleReference,\n\t\t\t\ttakeInputMedia ? takeInputMedia() : std::nullopt);\n\t\t\treturn originalRequestId ? originalRequestId : requestId;\n\t\t};\n\t\treturn performRequest(performRequest, 0);\n\t}\n\n\tconst auto callback = [=](Fn<void()> applyUpdates, mtpRequestId id) {\n\t\tapplyUpdates();\n\t\tdone(id);\n\t};\n\treturn EditMessage(\n\t\titem,\n\t\tcaption,\n\t\twebpage,\n\t\toptions,\n\t\tcallback,\n\t\tfail,\n\t\tstd::nullopt);\n}\n\nvoid EditTodoList(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst TodoListData &data,\n\t\tSendOptions options,\n\t\tFn<void(mtpRequestId requestId)> done,\n\t\tFn<void(const QString &error, mtpRequestId requestId)> fail) {\n\tconst auto callback = [=](Fn<void()> applyUpdates, mtpRequestId id) {\n\t\tapplyUpdates();\n\t\tdone(id);\n\t};\n\tEditMessage(\n\t\titem,\n\t\toptions,\n\t\tcallback,\n\t\tfail,\n\t\tMTP_inputMediaTodo(TodoListDataToMTP(&data)));\n}\n\nnamespace Fork {\n\nvoid EditMessageMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tApi::SendOptions options,\n\t\tMTPInputMedia media,\n\t\tFn<void(QString)> fail) {\n\tEditMessage(item, options, []{}, fail, media);\n\t// EditMessageWithUploadedMedia(item, options, media, fail);\n}\n\n} // namespace Fork\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_editing.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass HistoryItem;\n\nnamespace Data {\nstruct WebPageDraft;\n} // namespace Data\n\nnamespace MTP {\nclass Error;\n} // namespace MTP\n\nnamespace Api {\n\nstruct SendOptions;\nstruct RemoteFileInfo;\n\nconst auto kDefaultEditMessagesErrors = {\n\tu\"MESSAGE_ID_INVALID\"_q,\n\tu\"CHAT_ADMIN_REQUIRED\"_q,\n\tu\"MESSAGE_EDIT_TIME_EXPIRED\"_q,\n};\n\nvoid RescheduleMessage(\n\tnot_null<HistoryItem*> item,\n\tSendOptions options);\n\nvoid EditMessageWithUploadedDocument(\n\tHistoryItem *item,\n\tRemoteFileInfo info,\n\tSendOptions options);\n\nvoid EditMessageWithUploadedPhoto(\n\tHistoryItem *item,\n\tRemoteFileInfo info,\n\tSendOptions options);\n\nmtpRequestId EditCaption(\n\tnot_null<HistoryItem*> item,\n\tconst TextWithEntities &caption,\n\tSendOptions options,\n\tFn<void()> done,\n\tFn<void(const QString &)> fail);\n\nmtpRequestId EditTextMessage(\n\tnot_null<HistoryItem*> item,\n\tconst TextWithEntities &caption,\n\tData::WebPageDraft webpage,\n\tSendOptions options,\n\tFn<void(mtpRequestId requestId)> done,\n\tFn<void(const QString &error, mtpRequestId requestId)> fail,\n\tbool spoilered);\n\nvoid EditTodoList(\n\tnot_null<HistoryItem*> item,\n\tconst TodoListData &data,\n\tSendOptions options,\n\tFn<void(mtpRequestId requestId)> done,\n\tFn<void(const QString &error, mtpRequestId requestId)> fail);\n\n\nnamespace Fork {\n\nvoid EditMessageMedia(\n\tnot_null<HistoryItem*> item,\n\tApi::SendOptions options,\n\tMTPInputMedia media,\n\tFn<void(QString)> fail);\n\n} // namespace Fork\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_filter_updates.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Api {\n\ntemplate <typename Type>\nvoid PerformForUpdate(\n\t\tconst MTPUpdates &updates,\n\t\tFn<void(const Type &)> callback) {\n\tupdates.match([&](const MTPDupdates &updates) {\n\t\tfor (const auto &update : updates.vupdates().v) {\n\t\t\tupdate.match([&](const Type &d) {\n\t\t\t\tcallback(d);\n\t\t\t}, [](const auto &) {\n\t\t\t});\n\t\t}\n\t}, [](const auto &) {\n\t});\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_global_privacy.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_global_privacy.h\"\n\n#include \"apiwrap.h\"\n#include \"data/components/promo_suggestions.h\"\n#include \"data/data_user.h\"\n#include \"main/main_session.h\"\n#include \"main/main_app_config.h\"\n\nnamespace Api {\n\nPeerId ParsePaidReactionShownPeer(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPPaidReactionPrivacy &value) {\n\treturn value.match([&](const MTPDpaidReactionPrivacyDefault &) {\n\t\treturn session->userPeerId();\n\t}, [](const MTPDpaidReactionPrivacyAnonymous &) {\n\t\treturn PeerId();\n\t}, [&](const MTPDpaidReactionPrivacyPeer &data) {\n\t\treturn data.vpeer().match([&](const MTPDinputPeerSelf &) {\n\t\t\treturn session->userPeerId();\n\t\t}, [](const MTPDinputPeerUser &data) {\n\t\t\treturn peerFromUser(data.vuser_id());\n\t\t}, [](const MTPDinputPeerChat &data) {\n\t\t\treturn peerFromChat(data.vchat_id());\n\t\t}, [](const MTPDinputPeerChannel &data) {\n\t\t\treturn peerFromChannel(data.vchannel_id());\n\t\t}, [](const MTPDinputPeerUserFromMessage &data) -> PeerId {\n\t\t\tUnexpected(\"From message peer in ParsePaidReactionShownPeer.\");\n\t\t}, [](const MTPDinputPeerChannelFromMessage &data) -> PeerId {\n\t\t\tUnexpected(\"From message peer in ParsePaidReactionShownPeer.\");\n\t\t}, [](const MTPDinputPeerEmpty &) -> PeerId {\n\t\t\tUnexpected(\"Empty peer in ParsePaidReactionShownPeer.\");\n\t\t});\n\t});\n}\n\nGlobalPrivacy::GlobalPrivacy(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance()) {\n}\n\nvoid GlobalPrivacy::reload(Fn<void()> callback) {\n\tif (callback) {\n\t\t_callbacks.push_back(std::move(callback));\n\t}\n\tif (_requestId) {\n\t\treturn;\n\t}\n\t_requestId = _api.request(MTPaccount_GetGlobalPrivacySettings(\n\t)).done([=](const MTPGlobalPrivacySettings &result) {\n\t\t_requestId = 0;\n\t\tapply(result);\n\t\tfor (const auto &callback : base::take(_callbacks)) {\n\t\t\tcallback();\n\t\t}\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t\tfor (const auto &callback : base::take(_callbacks)) {\n\t\t\tcallback();\n\t\t}\n\t}).send();\n\n\t_session->appConfig().value(\n\t) | rpl::on_next([=] {\n\t\t_showArchiveAndMute = _session->appConfig().get<bool>(\n\t\t\tu\"autoarchive_setting_available\"_q,\n\t\t\tfalse);\n\t}, _session->lifetime());\n}\n\nbool GlobalPrivacy::archiveAndMuteCurrent() const {\n\treturn _archiveAndMute.current();\n}\n\nrpl::producer<bool> GlobalPrivacy::archiveAndMute() const {\n\treturn _archiveAndMute.value();\n}\n\nUnarchiveOnNewMessage GlobalPrivacy::unarchiveOnNewMessageCurrent() const {\n\treturn _unarchiveOnNewMessage.current();\n}\n\nauto GlobalPrivacy::unarchiveOnNewMessage() const\n-> rpl::producer<UnarchiveOnNewMessage> {\n\treturn _unarchiveOnNewMessage.value();\n}\n\nrpl::producer<bool> GlobalPrivacy::showArchiveAndMute() const {\n\tusing namespace rpl::mappers;\n\n\treturn rpl::combine(\n\t\tarchiveAndMute(),\n\t\t_showArchiveAndMute.value(),\n\t\t_1 || _2);\n}\n\nrpl::producer<> GlobalPrivacy::suggestArchiveAndMute() const {\n\treturn _session->promoSuggestions().requested(u\"AUTOARCHIVE_POPULAR\"_q);\n}\n\nvoid GlobalPrivacy::dismissArchiveAndMuteSuggestion() {\n\t_session->promoSuggestions().dismiss(u\"AUTOARCHIVE_POPULAR\"_q);\n}\n\nvoid GlobalPrivacy::updateHideReadTime(bool hide) {\n\tupdate(\n\t\tarchiveAndMuteCurrent(),\n\t\tunarchiveOnNewMessageCurrent(),\n\t\thide,\n\t\tnewRequirePremiumCurrent(),\n\t\tnewChargeStarsCurrent(),\n\t\tdisallowedGiftTypesCurrent());\n}\n\nbool GlobalPrivacy::hideReadTimeCurrent() const {\n\treturn _hideReadTime.current();\n}\n\nrpl::producer<bool> GlobalPrivacy::hideReadTime() const {\n\treturn _hideReadTime.value();\n}\n\nbool GlobalPrivacy::newRequirePremiumCurrent() const {\n\treturn _newRequirePremium.current();\n}\n\nrpl::producer<bool> GlobalPrivacy::newRequirePremium() const {\n\treturn _newRequirePremium.value();\n}\n\nint GlobalPrivacy::newChargeStarsCurrent() const {\n\treturn _newChargeStars.current();\n}\n\nrpl::producer<int> GlobalPrivacy::newChargeStars() const {\n\treturn _newChargeStars.value();\n}\n\nvoid GlobalPrivacy::updateMessagesPrivacy(\n\t\tbool requirePremium,\n\t\tint chargeStars) {\n\tupdate(\n\t\tarchiveAndMuteCurrent(),\n\t\tunarchiveOnNewMessageCurrent(),\n\t\thideReadTimeCurrent(),\n\t\trequirePremium,\n\t\tchargeStars,\n\t\tdisallowedGiftTypesCurrent());\n}\n\nDisallowedGiftTypes GlobalPrivacy::disallowedGiftTypesCurrent() const {\n\treturn _disallowedGiftTypes.current();\n}\n\nauto GlobalPrivacy::disallowedGiftTypes() const\n\t\t-> rpl::producer<DisallowedGiftTypes> {\n\treturn _disallowedGiftTypes.value();\n}\n\nvoid GlobalPrivacy::updateDisallowedGiftTypes(DisallowedGiftTypes types) {\n\tupdate(\n\t\tarchiveAndMuteCurrent(),\n\t\tunarchiveOnNewMessageCurrent(),\n\t\thideReadTimeCurrent(),\n\t\tnewRequirePremiumCurrent(),\n\t\tnewChargeStarsCurrent(),\n\t\ttypes);\n}\n\nvoid GlobalPrivacy::loadPaidReactionShownPeer() {\n\tif (_paidReactionShownPeerLoaded) {\n\t\treturn;\n\t}\n\t_paidReactionShownPeerLoaded = true;\n\t_api.request(MTPmessages_GetPaidReactionPrivacy(\n\t)).done([=](const MTPUpdates &result) {\n\t\t_session->api().applyUpdates(result);\n\t}).send();\n}\n\nvoid GlobalPrivacy::updatePaidReactionShownPeer(PeerId shownPeer) {\n\t_paidReactionShownPeer = shownPeer;\n}\n\nPeerId GlobalPrivacy::paidReactionShownPeerCurrent() const {\n\treturn _paidReactionShownPeer.current();\n}\n\nrpl::producer<PeerId> GlobalPrivacy::paidReactionShownPeer() const {\n\treturn _paidReactionShownPeer.value();\n}\n\nvoid GlobalPrivacy::updateArchiveAndMute(bool value) {\n\tupdate(\n\t\tvalue,\n\t\tunarchiveOnNewMessageCurrent(),\n\t\thideReadTimeCurrent(),\n\t\tnewRequirePremiumCurrent(),\n\t\tnewChargeStarsCurrent(),\n\t\tdisallowedGiftTypesCurrent());\n}\n\nvoid GlobalPrivacy::updateUnarchiveOnNewMessage(\n\t\tUnarchiveOnNewMessage value) {\n\tupdate(\n\t\tarchiveAndMuteCurrent(),\n\t\tvalue,\n\t\thideReadTimeCurrent(),\n\t\tnewRequirePremiumCurrent(),\n\t\tnewChargeStarsCurrent(),\n\t\tdisallowedGiftTypesCurrent());\n}\n\nvoid GlobalPrivacy::update(\n\t\tbool archiveAndMute,\n\t\tUnarchiveOnNewMessage unarchiveOnNewMessage,\n\t\tbool hideReadTime,\n\t\tbool newRequirePremium,\n\t\tint newChargeStars,\n\t\tDisallowedGiftTypes disallowedGiftTypes) {\n\tusing Flag = MTPDglobalPrivacySettings::Flag;\n\tusing DisallowedFlag = MTPDdisallowedGiftsSettings::Flag;\n\n\t_api.request(_requestId).cancel();\n\tconst auto newRequirePremiumAllowed = _session->premium()\n\t\t|| _session->appConfig().newRequirePremiumFree();\n\tconst auto showGiftIcon\n\t\t= (disallowedGiftTypes & DisallowedGiftType::SendHide);\n\tconst auto flags = Flag()\n\t\t| (archiveAndMute\n\t\t\t? Flag::f_archive_and_mute_new_noncontact_peers\n\t\t\t: Flag())\n\t\t| (unarchiveOnNewMessage == UnarchiveOnNewMessage::None\n\t\t\t? Flag::f_keep_archived_unmuted\n\t\t\t: Flag())\n\t\t| (unarchiveOnNewMessage != UnarchiveOnNewMessage::AnyUnmuted\n\t\t\t? Flag::f_keep_archived_folders\n\t\t\t: Flag())\n\t\t| (hideReadTime ? Flag::f_hide_read_marks : Flag())\n\t\t| ((newRequirePremium && newRequirePremiumAllowed)\n\t\t\t? Flag::f_new_noncontact_peers_require_premium\n\t\t\t: Flag())\n\t\t| Flag::f_noncontact_peers_paid_stars\n\t\t| (showGiftIcon ? Flag::f_display_gifts_button : Flag())\n\t\t| Flag::f_disallowed_gifts;\n\tconst auto disallowedFlags = DisallowedFlag()\n\t\t| ((disallowedGiftTypes & DisallowedGiftType::Premium)\n\t\t\t? DisallowedFlag::f_disallow_premium_gifts\n\t\t\t: DisallowedFlag())\n\t\t| ((disallowedGiftTypes & DisallowedGiftType::Unlimited)\n\t\t\t? DisallowedFlag::f_disallow_unlimited_stargifts\n\t\t\t: DisallowedFlag())\n\t\t| ((disallowedGiftTypes & DisallowedGiftType::Limited)\n\t\t\t? DisallowedFlag::f_disallow_limited_stargifts\n\t\t\t: DisallowedFlag())\n\t\t| ((disallowedGiftTypes & DisallowedGiftType::Unique)\n\t\t\t? DisallowedFlag::f_disallow_unique_stargifts\n\t\t\t: DisallowedFlag())\n\t\t| ((disallowedGiftTypes & DisallowedGiftType::FromChannels)\n\t\t\t? DisallowedFlag::f_disallow_stargifts_from_channels\n\t\t\t: DisallowedFlag());\n\tconst auto typesWas = _disallowedGiftTypes.current();\n\tconst auto typesChanged = (typesWas != disallowedGiftTypes);\n\t_requestId = _api.request(MTPaccount_SetGlobalPrivacySettings(\n\t\tMTP_globalPrivacySettings(\n\t\t\tMTP_flags(flags),\n\t\t\tMTP_long(newChargeStars),\n\t\t\tMTP_disallowedGiftsSettings(MTP_flags(disallowedFlags)))\n\t)).done([=](const MTPGlobalPrivacySettings &result) {\n\t\t_requestId = 0;\n\t\tapply(result);\n\t\tif (typesChanged) {\n\t\t\t_session->user()->updateFullForced();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\t_requestId = 0;\n\t\tif (error.type() == u\"PREMIUM_ACCOUNT_REQUIRED\"_q) {\n\t\t\tupdate(\n\t\t\t\tarchiveAndMute,\n\t\t\t\tunarchiveOnNewMessage,\n\t\t\t\thideReadTime,\n\t\t\t\tfalse,\n\t\t\t\t0,\n\t\t\t\tDisallowedGiftTypes());\n\t\t}\n\t}).send();\n\t_archiveAndMute = archiveAndMute;\n\t_unarchiveOnNewMessage = unarchiveOnNewMessage;\n\t_hideReadTime = hideReadTime;\n\t_newRequirePremium = newRequirePremium;\n\t_newChargeStars = newChargeStars;\n\t_disallowedGiftTypes = disallowedGiftTypes;\n}\n\nvoid GlobalPrivacy::apply(const MTPGlobalPrivacySettings &settings) {\n\tconst auto &data = settings.data();\n\t_archiveAndMute = data.is_archive_and_mute_new_noncontact_peers();\n\t_unarchiveOnNewMessage = data.is_keep_archived_unmuted()\n\t\t? UnarchiveOnNewMessage::None\n\t\t: data.is_keep_archived_folders()\n\t\t? UnarchiveOnNewMessage::NotInFoldersUnmuted\n\t\t: UnarchiveOnNewMessage::AnyUnmuted;\n\t_hideReadTime = data.is_hide_read_marks();\n\t_newRequirePremium = data.is_new_noncontact_peers_require_premium();\n\t_newChargeStars = data.vnoncontact_peers_paid_stars().value_or_empty();\n\tif (const auto gifts = data.vdisallowed_gifts()) {\n\t\tconst auto &disallow = gifts->data();\n\t\t_disallowedGiftTypes = DisallowedGiftType()\n\t\t\t| (disallow.is_disallow_unlimited_stargifts()\n\t\t\t\t? DisallowedGiftType::Unlimited\n\t\t\t\t: DisallowedGiftType())\n\t\t\t| (disallow.is_disallow_limited_stargifts()\n\t\t\t\t? DisallowedGiftType::Limited\n\t\t\t\t: DisallowedGiftType())\n\t\t\t| (disallow.is_disallow_unique_stargifts()\n\t\t\t\t? DisallowedGiftType::Unique\n\t\t\t\t: DisallowedGiftType())\n\t\t\t| (disallow.is_disallow_premium_gifts()\n\t\t\t\t? DisallowedGiftType::Premium\n\t\t\t\t: DisallowedGiftType())\n\t\t\t| (disallow.is_disallow_stargifts_from_channels()\n\t\t\t\t? DisallowedGiftType::FromChannels\n\t\t\t\t: DisallowedGiftType())\n\t\t\t| (data.is_display_gifts_button()\n\t\t\t\t? DisallowedGiftType::SendHide\n\t\t\t\t: DisallowedGiftType());\n\t} else {\n\t\t_disallowedGiftTypes = data.is_display_gifts_button()\n\t\t\t? DisallowedGiftType::SendHide\n\t\t\t: DisallowedGiftType();\n\t}\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_global_privacy.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n#include \"mtproto/sender.h\"\n\nclass ApiWrap;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nenum class UnarchiveOnNewMessage {\n\tNone,\n\tNotInFoldersUnmuted,\n\tAnyUnmuted,\n};\n\nenum class DisallowedGiftType : uchar {\n\tLimited      = 0x01,\n\tUnlimited    = 0x02,\n\tUnique       = 0x04,\n\tFromChannels = 0x08,\n\tPremium      = 0x10,\n\tSendHide     = 0x20,\n};\ninline constexpr bool is_flag_type(DisallowedGiftType) { return true; }\n\nusing DisallowedGiftTypes = base::flags<DisallowedGiftType>;\n\n[[nodiscard]] PeerId ParsePaidReactionShownPeer(\n\tnot_null<Main::Session*> session,\n\tconst MTPPaidReactionPrivacy &value);\n\nclass GlobalPrivacy final {\npublic:\n\texplicit GlobalPrivacy(not_null<ApiWrap*> api);\n\n\tvoid reload(Fn<void()> callback = nullptr);\n\tvoid updateArchiveAndMute(bool value);\n\tvoid updateUnarchiveOnNewMessage(UnarchiveOnNewMessage value);\n\n\t[[nodiscard]] bool archiveAndMuteCurrent() const;\n\t[[nodiscard]] rpl::producer<bool> archiveAndMute() const;\n\t[[nodiscard]] auto unarchiveOnNewMessageCurrent() const\n\t\t-> UnarchiveOnNewMessage;\n\t[[nodiscard]] auto unarchiveOnNewMessage() const\n\t\t-> rpl::producer<UnarchiveOnNewMessage>;\n\t[[nodiscard]] rpl::producer<bool> showArchiveAndMute() const;\n\t[[nodiscard]] rpl::producer<> suggestArchiveAndMute() const;\n\tvoid dismissArchiveAndMuteSuggestion();\n\n\tvoid updateHideReadTime(bool hide);\n\t[[nodiscard]] bool hideReadTimeCurrent() const;\n\t[[nodiscard]] rpl::producer<bool> hideReadTime() const;\n\n\t[[nodiscard]] bool newRequirePremiumCurrent() const;\n\t[[nodiscard]] rpl::producer<bool> newRequirePremium() const;\n\n\t[[nodiscard]] int newChargeStarsCurrent() const;\n\t[[nodiscard]] rpl::producer<int> newChargeStars() const;\n\n\tvoid updateMessagesPrivacy(bool requirePremium, int chargeStars);\n\n\t[[nodiscard]] DisallowedGiftTypes disallowedGiftTypesCurrent() const;\n\t[[nodiscard]] auto disallowedGiftTypes() const\n\t\t-> rpl::producer<DisallowedGiftTypes>;\n\tvoid updateDisallowedGiftTypes(DisallowedGiftTypes types);\n\n\tvoid loadPaidReactionShownPeer();\n\tvoid updatePaidReactionShownPeer(PeerId shownPeer);\n\t[[nodiscard]] PeerId paidReactionShownPeerCurrent() const;\n\t[[nodiscard]] rpl::producer<PeerId> paidReactionShownPeer() const;\n\nprivate:\n\tvoid apply(const MTPGlobalPrivacySettings &settings);\n\n\tvoid update(\n\t\tbool archiveAndMute,\n\t\tUnarchiveOnNewMessage unarchiveOnNewMessage,\n\t\tbool hideReadTime,\n\t\tbool newRequirePremium,\n\t\tint newChargeStars,\n\t\tDisallowedGiftTypes disallowedGiftTypes);\n\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\tmtpRequestId _requestId = 0;\n\trpl::variable<bool> _archiveAndMute = false;\n\trpl::variable<UnarchiveOnNewMessage> _unarchiveOnNewMessage\n\t\t= UnarchiveOnNewMessage::None;\n\trpl::variable<bool> _showArchiveAndMute = false;\n\trpl::variable<bool> _hideReadTime = false;\n\trpl::variable<bool> _newRequirePremium = false;\n\trpl::variable<int> _newChargeStars = 0;\n\trpl::variable<DisallowedGiftTypes> _disallowedGiftTypes;\n\trpl::variable<PeerId> _paidReactionShownPeer = false;\n\tstd::vector<Fn<void()>> _callbacks;\n\tbool _paidReactionShownPeerLoaded = false;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_hash.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_hash.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"main/main_session.h\"\n\nnamespace Api {\nnamespace {\n\n[[nodiscard]] uint64 CountDocumentVectorHash(\n\t\tconst QVector<DocumentData*> vector) {\n\tauto result = HashInit();\n\tfor (const auto document : vector) {\n\t\tHashUpdate(result, document->id);\n\t}\n\treturn HashFinalize(result);\n}\n\n[[nodiscard]] uint64 CountSpecialStickerSetHash(\n\t\tnot_null<Main::Session*> session,\n\t\tuint64 setId) {\n\tconst auto &sets = session->data().stickers().sets();\n\tconst auto it = sets.find(setId);\n\tif (it != sets.cend()) {\n\t\treturn CountDocumentVectorHash(it->second->stickers);\n\t}\n\treturn 0;\n}\n\n[[nodiscard]] uint64 CountStickersOrderHash(\n\t\tnot_null<Main::Session*> session,\n\t\tconst Data::StickersSetsOrder &order,\n\t\tbool checkOutdatedInfo) {\n\tusing Flag = Data::StickersSetFlag;\n\tauto result = HashInit();\n\tbool foundOutdated = false;\n\tconst auto &sets = session->data().stickers().sets();\n\tfor (auto i = order.cbegin(), e = order.cend(); i != e; ++i) {\n\t\tauto it = sets.find(*i);\n\t\tif (it != sets.cend()) {\n\t\t\tconst auto set = it->second.get();\n\t\t\tif (set->id == Data::Stickers::DefaultSetId) {\n\t\t\t\tfoundOutdated = true;\n\t\t\t} else if (!(set->flags & Flag::Special)\n\t\t\t\t&& !(set->flags & Flag::Archived)) {\n\t\t\t\tHashUpdate(result, set->hash);\n\t\t\t}\n\t\t}\n\t}\n\treturn (!checkOutdatedInfo || !foundOutdated)\n\t\t? HashFinalize(result)\n\t\t: 0;\n}\n\n[[nodiscard]] uint64 CountFeaturedHash(\n\t\tnot_null<Main::Session*> session,\n\t\tconst Data::StickersSetsOrder &order) {\n\tauto result = HashInit();\n\tconst auto &sets = session->data().stickers().sets();\n\tfor (const auto setId : order) {\n\t\tHashUpdate(result, setId);\n\n\t\tconst auto it = sets.find(setId);\n\t\tif (it != sets.cend()\n\t\t\t&& (it->second->flags & Data::StickersSetFlag::Unread)) {\n\t\t\tHashUpdate(result, 1);\n\t\t}\n\t}\n\treturn HashFinalize(result);\n}\n\n} // namespace\n\nuint64 CountStickersHash(\n\t\tnot_null<Main::Session*> session,\n\t\tbool checkOutdatedInfo) {\n\treturn CountStickersOrderHash(\n\t\tsession,\n\t\tsession->data().stickers().setsOrder(),\n\t\tcheckOutdatedInfo);\n}\n\nuint64 CountMasksHash(\n\t\tnot_null<Main::Session*> session,\n\t\tbool checkOutdatedInfo) {\n\treturn CountStickersOrderHash(\n\t\tsession,\n\t\tsession->data().stickers().maskSetsOrder(),\n\t\tcheckOutdatedInfo);\n}\n\nuint64 CountCustomEmojiHash(\n\t\tnot_null<Main::Session*> session,\n\t\tbool checkOutdatedInfo) {\n\treturn CountStickersOrderHash(\n\t\tsession,\n\t\tsession->data().stickers().emojiSetsOrder(),\n\t\tcheckOutdatedInfo);\n}\n\nuint64 CountRecentStickersHash(\n\t\tnot_null<Main::Session*> session,\n\t\tbool attached) {\n\treturn CountSpecialStickerSetHash(\n\t\tsession,\n\t\tattached\n\t\t\t? Data::Stickers::CloudRecentAttachedSetId\n\t\t\t: Data::Stickers::CloudRecentSetId);\n}\n\nuint64 CountFavedStickersHash(not_null<Main::Session*> session) {\n\treturn CountSpecialStickerSetHash(session, Data::Stickers::FavedSetId);\n}\n\nuint64 CountFeaturedStickersHash(not_null<Main::Session*> session) {\n\treturn CountFeaturedHash(\n\t\tsession,\n\t\tsession->data().stickers().featuredSetsOrder());\n}\n\nuint64 CountFeaturedEmojiHash(not_null<Main::Session*> session) {\n\treturn CountFeaturedHash(\n\t\tsession,\n\t\tsession->data().stickers().featuredEmojiSetsOrder());\n}\n\nuint64 CountSavedGifsHash(not_null<Main::Session*> session) {\n\treturn CountDocumentVectorHash(session->data().stickers().savedGifs());\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_hash.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\n[[nodiscard]] uint64 CountStickersHash(\n\tnot_null<Main::Session*> session,\n\tbool checkOutdatedInfo = false);\n[[nodiscard]] uint64 CountMasksHash(\n\tnot_null<Main::Session*> session,\n\tbool checkOutdatedInfo = false);\n[[nodiscard]] uint64 CountCustomEmojiHash(\n\tnot_null<Main::Session*> session,\n\tbool checkOutdatedInfo = false);\n[[nodiscard]] uint64 CountRecentStickersHash(\n\tnot_null<Main::Session*> session,\n\tbool attached = false);\n[[nodiscard]] uint64 CountFavedStickersHash(\n\tnot_null<Main::Session*> session);\n[[nodiscard]] uint64 CountFeaturedStickersHash(\n\tnot_null<Main::Session*> session);\n[[nodiscard]] uint64 CountFeaturedEmojiHash(\n\tnot_null<Main::Session*> session);\n[[nodiscard]] uint64 CountSavedGifsHash(not_null<Main::Session*> session);\n\n[[nodiscard]] inline uint64 HashInit() {\n\treturn 0;\n}\n\ninline void HashUpdate(uint64 &already, uint64 value) {\n\talready ^= (already >> 21);\n\talready ^= (already << 35);\n\talready ^= (already >> 4);\n\talready += value;\n}\n\ninline void HashUpdate(uint64 &already, int64 value) {\n\tHashUpdate(already, uint64(value));\n}\n\ninline void HashUpdate(uint64 &already, uint32 value) {\n\tHashUpdate(already, uint64(value));\n}\n\ninline void HashUpdate(uint64 &already, int32 value) {\n\tHashUpdate(already, int64(value));\n}\n\n[[nodiscard]] inline uint64 HashFinalize(uint64 already) {\n\treturn already;\n}\n\ntemplate <typename IntRange>\n[[nodiscard]] inline uint64 CountHash(IntRange &&range) {\n\tauto result = HashInit();\n\tfor (const auto value : range) {\n\t\tHashUpdate(result, value);\n\t}\n\treturn HashFinalize(result);\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_invite_links.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_invite_links.h\"\n\n#include \"api/api_chat_participants.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"main/main_session.h\"\n#include \"base/unixtime.h\"\n#include \"apiwrap.h\"\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kFirstPage = 10;\nconstexpr auto kPerPage = 50;\nconstexpr auto kJoinedFirstPage = 10;\n\nvoid BringPermanentToFront(PeerInviteLinks &links) {\n\tauto &list = links.links;\n\tconst auto i = ranges::find_if(list, [](const InviteLink &link) {\n\t\treturn link.permanent && !link.revoked;\n\t});\n\tif (i != end(list) && i != begin(list)) {\n\t\tranges::rotate(begin(list), i, i + 1);\n\t}\n}\n\nvoid RemovePermanent(PeerInviteLinks &links) {\n\tauto &list = links.links;\n\tlist.erase(ranges::remove_if(list, [](const InviteLink &link) {\n\t\treturn link.permanent && !link.revoked;\n\t}), end(list));\n}\n\n} // namespace\n\nJoinedByLinkSlice ParseJoinedByLinkSlice(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPmessages_ChatInviteImporters &slice) {\n\tauto result = JoinedByLinkSlice();\n\tslice.match([&](const MTPDmessages_chatInviteImporters &data) {\n\t\tauto &owner = peer->session().data();\n\t\towner.processUsers(data.vusers());\n\t\tresult.count = data.vcount().v;\n\t\tresult.users.reserve(data.vimporters().v.size());\n\t\tfor (const auto &importer : data.vimporters().v) {\n\t\t\timporter.match([&](const MTPDchatInviteImporter &data) {\n\t\t\t\tresult.users.push_back({\n\t\t\t\t\t.user = owner.user(data.vuser_id()),\n\t\t\t\t\t.date = data.vdate().v,\n\t\t\t\t\t.viaFilterLink = data.is_via_chatlist(),\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t});\n\treturn result;\n}\n\nInviteLinks::InviteLinks(not_null<ApiWrap*> api) : _api(api) {\n}\n\nvoid InviteLinks::create(const CreateInviteLinkArgs &args) {\n\tperformCreate(args, false);\n}\n\nvoid InviteLinks::performCreate(\n\t\tconst CreateInviteLinkArgs &args,\n\t\tbool revokeLegacyPermanent) {\n\tif (const auto i = _createCallbacks.find(args.peer)\n\t\t; i != end(_createCallbacks)) {\n\t\tif (args.done) {\n\t\t\ti->second.push_back(std::move(args.done));\n\t\t}\n\t\treturn;\n\t}\n\tauto &callbacks = _createCallbacks[args.peer];\n\tif (args.done) {\n\t\tcallbacks.push_back(std::move(args.done));\n\t}\n\n\tconst auto requestApproval = !args.subscription && args.requestApproval;\n\tusing Flag = MTPmessages_ExportChatInvite::Flag;\n\t_api->request(MTPmessages_ExportChatInvite(\n\t\tMTP_flags((revokeLegacyPermanent\n\t\t\t? Flag::f_legacy_revoke_permanent\n\t\t\t: Flag(0))\n\t\t\t| (!args.label.isEmpty() ? Flag::f_title : Flag(0))\n\t\t\t| (args.expireDate ? Flag::f_expire_date : Flag(0))\n\t\t\t| ((!requestApproval && args.usageLimit)\n\t\t\t\t? Flag::f_usage_limit\n\t\t\t\t: Flag(0))\n\t\t\t| (requestApproval ? Flag::f_request_needed : Flag(0))\n\t\t\t| (args.subscription ? Flag::f_subscription_pricing : Flag(0))),\n\t\targs.peer->input(),\n\t\tMTP_int(args.expireDate),\n\t\tMTP_int(args.usageLimit),\n\t\tMTP_string(args.label),\n\t\tMTP_starsSubscriptionPricing(\n\t\t\tMTP_int(args.subscription.period),\n\t\t\tMTP_long(args.subscription.credits))\n\t)).done([=, peer = args.peer](const MTPExportedChatInvite &result) {\n\t\tconst auto callbacks = _createCallbacks.take(peer);\n\t\tconst auto link = prepend(peer, peer->session().user(), result);\n\t\tif (link && callbacks) {\n\t\t\tfor (const auto &callback : *callbacks) {\n\t\t\t\tcallback(*link);\n\t\t\t}\n\t\t}\n\t}).fail([=, peer = args.peer] {\n\t\t_createCallbacks.erase(peer);\n\t}).send();\n}\n\nauto InviteLinks::lookupMyPermanent(not_null<PeerData*> peer) -> Link* {\n\tauto i = _firstSlices.find(peer);\n\treturn (i != end(_firstSlices)) ? lookupMyPermanent(i->second) : nullptr;\n}\n\nauto InviteLinks::lookupMyPermanent(Links &links) -> Link* {\n\tconst auto first = links.links.begin();\n\treturn (first != end(links.links) && first->permanent && !first->revoked)\n\t\t? &*first\n\t\t: nullptr;\n}\n\nauto InviteLinks::lookupMyPermanent(const Links &links) const -> const Link* {\n\tconst auto first = links.links.begin();\n\treturn (first != end(links.links) && first->permanent && !first->revoked)\n\t\t? &*first\n\t\t: nullptr;\n}\n\nauto InviteLinks::prepend(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst MTPExportedChatInvite &invite) -> std::optional<Link> {\n\tconst auto link = parse(peer, invite);\n\tif (!link) {\n\t\treturn link;\n\t}\n\tif (admin->isSelf()) {\n\t\tprependMyToFirstSlice(peer, admin, *link);\n\t}\n\t_updates.fire(Update{\n\t\t.peer = peer,\n\t\t.admin = admin,\n\t\t.now = *link\n\t});\n\treturn link;\n}\n\nvoid InviteLinks::prependMyToFirstSlice(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst Link &link) {\n\tExpects(admin->isSelf());\n\n\tauto i = _firstSlices.find(peer);\n\tif (i == end(_firstSlices)) {\n\t\ti = _firstSlices.emplace(peer).first;\n\t}\n\tauto &links = i->second;\n\tconst auto permanent = lookupMyPermanent(links);\n\tconst auto hadPermanent = (permanent != nullptr);\n\tauto updateOldPermanent = Update{\n\t\t.peer = peer,\n\t\t.admin = admin,\n\t};\n\tif (link.permanent && hadPermanent) {\n\t\tupdateOldPermanent.was = permanent->link;\n\t\tupdateOldPermanent.now = *permanent;\n\t\tupdateOldPermanent.now->revoked = true;\n\t\tlinks.links.erase(begin(links.links));\n\t\tif (links.count > 0) {\n\t\t\t--links.count;\n\t\t}\n\t}\n\t// Must not dereference 'permanent' pointer after that.\n\n\t++links.count;\n\tif (hadPermanent && !link.permanent) {\n\t\tlinks.links.insert(begin(links.links) + 1, link);\n\t} else {\n\t\tlinks.links.insert(begin(links.links), link);\n\t}\n\n\tif (link.permanent) {\n\t\teditPermanentLink(peer, link.link);\n\t}\n\tnotify(peer);\n\n\tif (updateOldPermanent.now) {\n\t\t_updates.fire(std::move(updateOldPermanent));\n\t}\n}\n\nvoid InviteLinks::edit(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst QString &link,\n\t\tconst QString &label,\n\t\tTimeId expireDate,\n\t\tint usageLimit,\n\t\tbool requestApproval,\n\t\tFn<void(Link)> done) {\n\tperformEdit(\n\t\tpeer,\n\t\tadmin,\n\t\tlink,\n\t\tstd::move(done),\n\t\tfalse,\n\t\tlabel,\n\t\texpireDate,\n\t\tusageLimit,\n\t\trequestApproval);\n}\n\nvoid InviteLinks::editTitle(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst QString &link,\n\t\tconst QString &label,\n\t\tFn<void(Link)> done) {\n\tperformEdit(peer, admin, link, done, false, label, 0, 0, false, true);\n}\n\nvoid InviteLinks::performEdit(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst QString &link,\n\t\tFn<void(Link)> done,\n\t\tbool revoke,\n\t\tconst QString &label,\n\t\tTimeId expireDate,\n\t\tint usageLimit,\n\t\tbool requestApproval,\n\t\tbool editOnlyTitle) {\n\tconst auto key = LinkKey{ peer, link };\n\tif (_deleteCallbacks.contains(key)) {\n\t\treturn;\n\t} else if (const auto i = _editCallbacks.find(key)\n\t\t; i != end(_editCallbacks)) {\n\t\tif (done) {\n\t\t\ti->second.push_back(std::move(done));\n\t\t}\n\t\treturn;\n\t}\n\n\tauto &callbacks = _editCallbacks[key];\n\tif (done) {\n\t\tcallbacks.push_back(std::move(done));\n\t}\n\tusing Flag = MTPmessages_EditExportedChatInvite::Flag;\n\tconst auto flags = (revoke ? Flag::f_revoked : Flag(0))\n\t\t| (!revoke ? Flag::f_title : Flag(0))\n\t\t| (!revoke ? Flag::f_expire_date : Flag(0))\n\t\t| ((!revoke && !requestApproval) ? Flag::f_usage_limit : Flag(0))\n\t\t| ((!revoke && (requestApproval || !usageLimit))\n\t\t\t? Flag::f_request_needed\n\t\t\t: Flag(0));\n\t_api->request(MTPmessages_EditExportedChatInvite(\n\t\tMTP_flags(editOnlyTitle ? Flag::f_title : flags),\n\t\tpeer->input(),\n\t\tMTP_string(link),\n\t\tMTP_int(expireDate),\n\t\tMTP_int(usageLimit),\n\t\tMTP_bool(requestApproval),\n\t\tMTP_string(label)\n\t)).done([=](const MTPmessages_ExportedChatInvite &result) {\n\t\tconst auto callbacks = _editCallbacks.take(key);\n\t\tconst auto peer = key.peer;\n\t\tresult.match([&](const auto &data) {\n\t\t\t_api->session().data().processUsers(data.vusers());\n\t\t\tconst auto link = parse(peer, data.vinvite());\n\t\t\tif (!link) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto i = _firstSlices.find(peer);\n\t\t\tif (i != end(_firstSlices)) {\n\t\t\t\tconst auto j = ranges::find(\n\t\t\t\t\ti->second.links,\n\t\t\t\t\tkey.link,\n\t\t\t\t\t&Link::link);\n\t\t\t\tif (j != end(i->second.links)) {\n\t\t\t\t\tif (link->revoked && !j->revoked) {\n\t\t\t\t\t\ti->second.links.erase(j);\n\t\t\t\t\t\tif (i->second.count > 0) {\n\t\t\t\t\t\t\t--i->second.count;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t*j = *link;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const auto &callback : *callbacks) {\n\t\t\t\tcallback(*link);\n\t\t\t}\n\t\t\t_updates.fire(Update{\n\t\t\t\t.peer = peer,\n\t\t\t\t.admin = admin,\n\t\t\t\t.was = key.link,\n\t\t\t\t.now = link,\n\t\t\t});\n\n\t\t\tusing Replaced = MTPDmessages_exportedChatInviteReplaced;\n\t\t\tif constexpr (Replaced::Is<decltype(data)>()) {\n\t\t\t\tprepend(peer, admin, data.vnew_invite());\n\t\t\t}\n\t\t});\n\t}).fail([=] {\n\t\t_editCallbacks.erase(key);\n\t}).send();\n}\n\nvoid InviteLinks::revoke(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst QString &link,\n\t\tFn<void(Link)> done) {\n\tperformEdit(peer, admin, link, std::move(done), true);\n}\n\nvoid InviteLinks::revokePermanent(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst QString &link,\n\t\tFn<void()> done) {\n\tconst auto callback = [=](auto&&) { done(); };\n\tif (!link.isEmpty()) {\n\t\tperformEdit(peer, admin, link, callback, true);\n\t} else if (!admin->isSelf()) {\n\t\tcrl::on_main(&peer->session(), done);\n\t} else {\n\t\tperformCreate({ peer, callback }, true);\n\t}\n}\n\nvoid InviteLinks::destroy(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst QString &link,\n\t\tFn<void()> done) {\n\tconst auto key = LinkKey{ peer, link };\n\n\tif (const auto i = _deleteCallbacks.find(key)\n\t\t; i != end(_deleteCallbacks)) {\n\t\tif (done) {\n\t\t\ti->second.push_back(std::move(done));\n\t\t}\n\t\treturn;\n\t}\n\n\tauto &callbacks = _deleteCallbacks[key];\n\tif (done) {\n\t\tcallbacks.push_back(std::move(done));\n\t}\n\t_api->request(MTPmessages_DeleteExportedChatInvite(\n\t\tpeer->input(),\n\t\tMTP_string(link)\n\t)).done([=] {\n\t\tconst auto callbacks = _deleteCallbacks.take(key);\n\t\tif (callbacks) {\n\t\t\tfor (const auto &callback : *callbacks) {\n\t\t\t\tcallback();\n\t\t\t}\n\t\t}\n\t\t_updates.fire(Update{\n\t\t\t.peer = peer,\n\t\t\t.admin = admin,\n\t\t\t.was = key.link,\n\t\t});\n\t}).fail([=] {\n\t\t_deleteCallbacks.erase(key);\n\t}).send();\n}\n\nvoid InviteLinks::destroyAllRevoked(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tFn<void()> done) {\n\tif (const auto i = _deleteRevokedCallbacks.find(peer)\n\t\t; i != end(_deleteRevokedCallbacks)) {\n\t\tif (done) {\n\t\t\ti->second.push_back(std::move(done));\n\t\t}\n\t\treturn;\n\t}\n\tauto &callbacks = _deleteRevokedCallbacks[peer];\n\tif (done) {\n\t\tcallbacks.push_back(std::move(done));\n\t}\n\t_api->request(MTPmessages_DeleteRevokedExportedChatInvites(\n\t\tpeer->input(),\n\t\tadmin->inputUser()\n\t)).done([=] {\n\t\tif (const auto callbacks = _deleteRevokedCallbacks.take(peer)) {\n\t\t\tfor (const auto &callback : *callbacks) {\n\t\t\t\tcallback();\n\t\t\t}\n\t\t}\n\t\t_allRevokedDestroyed.fire({ peer, admin });\n\t}).send();\n}\n\nvoid InviteLinks::requestMyLinks(not_null<PeerData*> peer) {\n\tif (_firstSliceRequests.contains(peer)) {\n\t\treturn;\n\t}\n\tconst auto requestId = _api->request(MTPmessages_GetExportedChatInvites(\n\t\tMTP_flags(0),\n\t\tpeer->input(),\n\t\tMTP_inputUserSelf(),\n\t\tMTPint(), // offset_date\n\t\tMTPstring(), // offset_link\n\t\tMTP_int(kFirstPage)\n\t)).done([=](const MTPmessages_ExportedChatInvites &result) {\n\t\t_firstSliceRequests.remove(peer);\n\t\tauto slice = parseSlice(peer, result);\n\t\tauto i = _firstSlices.find(peer);\n\t\tconst auto permanent = (i != end(_firstSlices))\n\t\t\t? lookupMyPermanent(i->second)\n\t\t\t: nullptr;\n\t\tif (!permanent) {\n\t\t\tBringPermanentToFront(slice);\n\t\t\tconst auto j = _firstSlices.emplace_or_assign(\n\t\t\t\tpeer,\n\t\t\t\tstd::move(slice)).first;\n\t\t\tif (const auto permanent = lookupMyPermanent(j->second)) {\n\t\t\t\teditPermanentLink(peer, permanent->link);\n\t\t\t}\n\t\t} else {\n\t\t\tRemovePermanent(slice);\n\t\t\tauto &existing = i->second.links;\n\t\t\texisting.erase(begin(existing) + 1, end(existing));\n\t\t\texisting.insert(\n\t\t\t\tend(existing),\n\t\t\t\tbegin(slice.links),\n\t\t\t\tend(slice.links));\n\t\t\ti->second.count = std::max(slice.count, int(existing.size()));\n\t\t}\n\t\tnotify(peer);\n\t}).fail([=] {\n\t\t_firstSliceRequests.remove(peer);\n\t}).send();\n\t_firstSliceRequests.emplace(peer, requestId);\n}\n\nvoid InviteLinks::processRequest(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &link,\n\t\tnot_null<UserData*> user,\n\t\tbool approved,\n\t\tFn<void()> done,\n\t\tFn<void()> fail) {\n\tif (_processRequests.contains({ peer, user })) {\n\t\treturn;\n\t}\n\t_processRequests.emplace(\n\t\tstd::pair{ peer, user },\n\t\tProcessRequest{ std::move(done), std::move(fail) });\n\tusing Flag = MTPmessages_HideChatJoinRequest::Flag;\n\t_api->request(MTPmessages_HideChatJoinRequest(\n\t\tMTP_flags(approved ? Flag::f_approved : Flag(0)),\n\t\tpeer->input(),\n\t\tuser->inputUser()\n\t)).done([=](const MTPUpdates &result) {\n\t\tif (const auto chat = peer->asChat()) {\n\t\t\tif (chat->count > 0) {\n\t\t\t\tif (chat->participants.size() >= chat->count) {\n\t\t\t\t\tchat->participants.emplace(user);\n\t\t\t\t}\n\t\t\t\t++chat->count;\n\t\t\t}\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\t_api->chatParticipants().requestCountDelayed(channel);\n\t\t}\n\t\t_api->applyUpdates(result);\n\t\tif (link.isEmpty() && approved) {\n\t\t\t// We don't know the link that was used for this user.\n\t\t\t// Prune all the cache.\n\t\t\tfor (auto i = begin(_firstJoined); i != end(_firstJoined);) {\n\t\t\t\tif (i->first.peer == peer) {\n\t\t\t\t\ti = _firstJoined.erase(i);\n\t\t\t\t} else {\n\t\t\t\t\t++i;\n\t\t\t\t}\n\t\t\t}\n\t\t\t_firstSlices.remove(peer);\n\t\t} else if (approved) {\n\t\t\tconst auto i = _firstJoined.find({ peer, link });\n\t\t\tif (i != end(_firstJoined)) {\n\t\t\t\t++i->second.count;\n\t\t\t\ti->second.users.insert(\n\t\t\t\t\tbegin(i->second.users),\n\t\t\t\t\tJoinedByLinkUser{ user, base::unixtime::now() });\n\t\t\t}\n\t\t}\n\t\tif (const auto callbacks = _processRequests.take({ peer, user })) {\n\t\t\tif (const auto &done = callbacks->done) {\n\t\t\t\tdone();\n\t\t\t}\n\t\t}\n\t}).fail([=] {\n\t\tif (const auto callbacks = _processRequests.take({ peer, user })) {\n\t\t\tif (const auto &fail = callbacks->fail) {\n\t\t\t\tfail();\n\t\t\t}\n\t\t}\n\t}).send();\n}\n\nvoid InviteLinks::applyExternalUpdate(\n\t\tnot_null<PeerData*> peer,\n\t\tInviteLink updated) {\n\tif (const auto i = _firstSlices.find(peer); i != end(_firstSlices)) {\n\t\tfor (auto &link : i->second.links) {\n\t\t\tif (link.link == updated.link) {\n\t\t\t\tlink = updated;\n\t\t\t}\n\t\t}\n\t}\n\t_updates.fire({\n\t\t.peer = peer,\n\t\t.admin = updated.admin,\n\t\t.was = updated.link,\n\t\t.now = updated,\n\t});\n}\n\nstd::optional<JoinedByLinkSlice> InviteLinks::lookupJoinedFirstSlice(\n\t\tLinkKey key) const {\n\tconst auto i = _firstJoined.find(key);\n\treturn (i != end(_firstJoined))\n\t\t? std::make_optional(i->second)\n\t\t: std::nullopt;\n}\n\nstd::optional<JoinedByLinkSlice> InviteLinks::joinedFirstSliceLoaded(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &link) const {\n\treturn lookupJoinedFirstSlice({ peer, link });\n}\n\nrpl::producer<JoinedByLinkSlice> InviteLinks::joinedFirstSliceValue(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &link,\n\t\tint fullCount) {\n\tconst auto key = LinkKey{ peer, link };\n\tauto current = lookupJoinedFirstSlice(key).value_or(JoinedByLinkSlice());\n\tif (current.count == fullCount\n\t\t&& (!fullCount || !current.users.empty())) {\n\t\treturn rpl::single(current);\n\t}\n\tcurrent.count = fullCount;\n\tconst auto remove = int(current.users.size()) - current.count;\n\tif (remove > 0) {\n\t\tcurrent.users.erase(end(current.users) - remove, end(current.users));\n\t}\n\trequestJoinedFirstSlice(key);\n\tusing namespace rpl::mappers;\n\treturn rpl::single(\n\t\tcurrent\n\t) | rpl::then(_joinedFirstSliceLoaded.events(\n\t) | rpl::filter(\n\t\t_1 == key\n\t) | rpl::map([=] {\n\t\treturn lookupJoinedFirstSlice(key).value_or(JoinedByLinkSlice());\n\t}));\n}\n\nauto InviteLinks::updates(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin) const -> rpl::producer<Update> {\n\treturn _updates.events() | rpl::filter([=](const Update &update) {\n\t\treturn update.peer == peer && update.admin == admin;\n\t});\n}\n\nrpl::producer<> InviteLinks::allRevokedDestroyed(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin) const {\n\treturn _allRevokedDestroyed.events(\n\t) | rpl::filter([=](const AllRevokedDestroyed &which) {\n\t\treturn which.peer == peer && which.admin == admin;\n\t}) | rpl::to_empty;\n}\n\nvoid InviteLinks::requestJoinedFirstSlice(LinkKey key) {\n\tif (_firstJoinedRequests.contains(key)) {\n\t\treturn;\n\t}\n\tconst auto requestId = _api->request(MTPmessages_GetChatInviteImporters(\n\t\tMTP_flags(MTPmessages_GetChatInviteImporters::Flag::f_link),\n\t\tkey.peer->input(),\n\t\tMTP_string(key.link),\n\t\tMTPstring(), // q\n\t\tMTP_int(0), // offset_date\n\t\tMTP_inputUserEmpty(), // offset_user\n\t\tMTP_int(kJoinedFirstPage)\n\t)).done([=](const MTPmessages_ChatInviteImporters &result) {\n\t\t_firstJoinedRequests.remove(key);\n\t\t_firstJoined[key] = ParseJoinedByLinkSlice(key.peer, result);\n\t\t_joinedFirstSliceLoaded.fire_copy(key);\n\t}).fail([=] {\n\t\t_firstJoinedRequests.remove(key);\n\t}).send();\n\t_firstJoinedRequests.emplace(key, requestId);\n}\n\nvoid InviteLinks::setMyPermanent(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPExportedChatInvite &invite) {\n\tauto link = parse(peer, invite);\n\tif (!link) {\n\t\tLOG((\"API Error: \"\n\t\t\t\"InviteLinks::setPermanent called with non-link.\"));\n\t\treturn;\n\t} else if (!link->permanent) {\n\t\tLOG((\"API Error: \"\n\t\t\t\"InviteLinks::setPermanent called with non-permanent link.\"));\n\t\treturn;\n\t}\n\tauto i = _firstSlices.find(peer);\n\tif (i == end(_firstSlices)) {\n\t\ti = _firstSlices.emplace(peer).first;\n\t}\n\tauto &links = i->second;\n\tauto updateOldPermanent = Update{\n\t\t.peer = peer,\n\t\t.admin = peer->session().user(),\n\t};\n\tif (const auto permanent = lookupMyPermanent(links)) {\n\t\tif (permanent->link == link->link) {\n\t\t\tif (permanent->usage != link->usage) {\n\t\t\t\tpermanent->usage = link->usage;\n\t\t\t\t_updates.fire(Update{\n\t\t\t\t\t.peer = peer,\n\t\t\t\t\t.admin = peer->session().user(),\n\t\t\t\t\t.was = link->link,\n\t\t\t\t\t.now = *permanent\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tupdateOldPermanent.was = permanent->link;\n\t\tupdateOldPermanent.now = *permanent;\n\t\tupdateOldPermanent.now->revoked = true;\n\t\tlinks.links.erase(begin(links.links));\n\t\tif (links.count > 0) {\n\t\t\t--links.count;\n\t\t}\n\t}\n\tlinks.links.insert(begin(links.links), *link);\n\n\teditPermanentLink(peer, link->link);\n\tnotify(peer);\n\n\tif (updateOldPermanent.now) {\n\t\t_updates.fire(std::move(updateOldPermanent));\n\t}\n\t_updates.fire(Update{\n\t\t.peer = peer,\n\t\t.admin = peer->session().user(),\n\t\t.now = link\n\t});\n}\n\nvoid InviteLinks::clearMyPermanent(not_null<PeerData*> peer) {\n\tauto i = _firstSlices.find(peer);\n\tif (i == end(_firstSlices)) {\n\t\treturn;\n\t}\n\tauto &links = i->second;\n\tconst auto permanent = lookupMyPermanent(links);\n\tif (!permanent) {\n\t\treturn;\n\t}\n\n\tauto updateOldPermanent = Update{\n\t\t.peer = peer,\n\t\t.admin = peer->session().user()\n\t};\n\tupdateOldPermanent.was = permanent->link;\n\tupdateOldPermanent.now = *permanent;\n\tupdateOldPermanent.now->revoked = true;\n\tlinks.links.erase(begin(links.links));\n\tif (links.count > 0) {\n\t\t--links.count;\n\t}\n\n\teditPermanentLink(peer, QString());\n\tnotify(peer);\n\n\tif (updateOldPermanent.now) {\n\t\t_updates.fire(std::move(updateOldPermanent));\n\t}\n}\n\nvoid InviteLinks::notify(not_null<PeerData*> peer) {\n\tpeer->session().changes().peerUpdated(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::InviteLinks);\n}\n\nauto InviteLinks::myLinks(not_null<PeerData*> peer) const -> const Links & {\n\tstatic const auto kEmpty = Links();\n\tconst auto i = _firstSlices.find(peer);\n\treturn (i != end(_firstSlices)) ? i->second : kEmpty;\n}\n\nauto InviteLinks::parseSlice(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPmessages_ExportedChatInvites &slice) const -> Links {\n\tauto i = _firstSlices.find(peer);\n\tconst auto permanent = (i != end(_firstSlices))\n\t\t? lookupMyPermanent(i->second)\n\t\t: nullptr;\n\tauto result = Links();\n\tslice.match([&](const MTPDmessages_exportedChatInvites &data) {\n\t\tpeer->session().data().processUsers(data.vusers());\n\t\tresult.count = data.vcount().v;\n\t\tfor (const auto &invite : data.vinvites().v) {\n\t\t\tif (const auto link = parse(peer, invite)) {\n\t\t\t\tif (!permanent || link->link != permanent->link) {\n\t\t\t\t\tresult.links.push_back(*link);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\treturn result;\n}\n\nauto InviteLinks::parse(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPExportedChatInvite &invite) const -> std::optional<Link> {\n\treturn invite.match([&](const MTPDchatInviteExported &data) {\n\t\treturn std::optional<Link>(Link{\n\t\t\t.link = qs(data.vlink()),\n\t\t\t.label = qs(data.vtitle().value_or_empty()),\n\t\t\t.subscription = data.vsubscription_pricing()\n\t\t\t\t? Data::PeerSubscription{\n\t\t\t\t\tdata.vsubscription_pricing()->data().vamount().v,\n\t\t\t\t\tdata.vsubscription_pricing()->data().vperiod().v,\n\t\t\t\t}\n\t\t\t\t: Data::PeerSubscription(),\n\t\t\t.admin = peer->session().data().user(data.vadmin_id()),\n\t\t\t.date = data.vdate().v,\n\t\t\t.startDate = data.vstart_date().value_or_empty(),\n\t\t\t.expireDate = data.vexpire_date().value_or_empty(),\n\t\t\t.usageLimit = data.vusage_limit().value_or_empty(),\n\t\t\t.usage = data.vusage().value_or_empty(),\n\t\t\t.requested = data.vrequested().value_or_empty(),\n\t\t\t.requestApproval = data.is_request_needed(),\n\t\t\t.permanent = data.is_permanent(),\n\t\t\t.revoked = data.is_revoked(),\n\t\t});\n\t}, [&](const MTPDchatInvitePublicJoinRequests &data) {\n\t\treturn std::optional<Link>();\n\t});\n}\n\nvoid InviteLinks::requestMoreLinks(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tTimeId lastDate,\n\t\tconst QString &lastLink,\n\t\tbool revoked,\n\t\tFn<void(Links)> done) {\n\tusing Flag = MTPmessages_GetExportedChatInvites::Flag;\n\t_api->request(MTPmessages_GetExportedChatInvites(\n\t\tMTP_flags(Flag::f_offset_link\n\t\t\t| (revoked ? Flag::f_revoked : Flag(0))),\n\t\tpeer->input(),\n\t\tadmin->inputUser(),\n\t\tMTP_int(lastDate),\n\t\tMTP_string(lastLink),\n\t\tMTP_int(kPerPage)\n\t)).done([=](const MTPmessages_ExportedChatInvites &result) {\n\t\tdone(parseSlice(peer, result));\n\t}).fail([=] {\n\t\tdone(Links());\n\t}).send();\n}\n\nvoid InviteLinks::editPermanentLink(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &link) {\n\tif (const auto chat = peer->asChat()) {\n\t\tchat->setInviteLink(link);\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tchannel->setInviteLink(link);\n\t} else {\n\t\tUnexpected(\"Peer in InviteLinks::editMainLink.\");\n\t}\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_invite_links.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass ApiWrap;\n\n#include \"data/data_subscriptions.h\"\n\nnamespace Api {\n\nstruct InviteLink {\n\tQString link;\n\tQString label;\n\tData::PeerSubscription subscription;\n\tnot_null<UserData*> admin;\n\tTimeId date = 0;\n\tTimeId startDate = 0;\n\tTimeId expireDate = 0;\n\tint usageLimit = 0;\n\tint usage = 0;\n\tint requested = 0;\n\tbool requestApproval = false;\n\tbool permanent = false;\n\tbool revoked = false;\n};\n\nstruct PeerInviteLinks {\n\tstd::vector<InviteLink> links;\n\tint count = 0;\n};\n\nstruct JoinedByLinkUser {\n\tnot_null<UserData*> user;\n\tTimeId date = 0;\n\tbool viaFilterLink = false;\n};\n\nstruct JoinedByLinkSlice {\n\tstd::vector<JoinedByLinkUser> users;\n\tint count = 0;\n};\n\nstruct InviteLinkUpdate {\n\tnot_null<PeerData*> peer;\n\tnot_null<UserData*> admin;\n\tQString was;\n\tstd::optional<InviteLink> now;\n};\n\n[[nodiscard]] JoinedByLinkSlice ParseJoinedByLinkSlice(\n\tnot_null<PeerData*> peer,\n\tconst MTPmessages_ChatInviteImporters &slice);\n\nstruct CreateInviteLinkArgs {\n\tnot_null<PeerData*> peer;\n\tFn<void(InviteLink)> done = nullptr;\n\tQString label;\n\tTimeId expireDate = 0;\n\tint usageLimit = 0;\n\tbool requestApproval = false;\n\tData::PeerSubscription subscription;\n};\n\nclass InviteLinks final {\npublic:\n\texplicit InviteLinks(not_null<ApiWrap*> api);\n\n\tusing Link = InviteLink;\n\tusing Links = PeerInviteLinks;\n\tusing Update = InviteLinkUpdate;\n\n\tvoid create(const CreateInviteLinkArgs &args);\n\tvoid edit(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst QString &link,\n\t\tconst QString &label,\n\t\tTimeId expireDate,\n\t\tint usageLimit,\n\t\tbool requestApproval,\n\t\tFn<void(Link)> done = nullptr);\n\tvoid editTitle(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst QString &link,\n\t\tconst QString &label,\n\t\tFn<void(Link)> done = nullptr);\n\tvoid revoke(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst QString &link,\n\t\tFn<void(Link)> done = nullptr);\n\tvoid revokePermanent(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst QString &link,\n\t\tFn<void()> done = nullptr);\n\tvoid destroy(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst QString &link,\n\t\tFn<void()> done = nullptr);\n\tvoid destroyAllRevoked(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tFn<void()> done = nullptr);\n\n\tvoid setMyPermanent(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPExportedChatInvite &invite);\n\tvoid clearMyPermanent(not_null<PeerData*> peer);\n\n\tvoid requestMyLinks(not_null<PeerData*> peer);\n\t[[nodiscard]] const Links &myLinks(not_null<PeerData*> peer) const;\n\n\tvoid processRequest(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &link,\n\t\tnot_null<UserData*> user,\n\t\tbool approved,\n\t\tFn<void()> done,\n\t\tFn<void()> fail);\n\tvoid applyExternalUpdate(not_null<PeerData*> peer, InviteLink updated);\n\n\t[[nodiscard]] rpl::producer<JoinedByLinkSlice> joinedFirstSliceValue(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &link,\n\t\tint fullCount);\n\t[[nodiscard]] std::optional<JoinedByLinkSlice> joinedFirstSliceLoaded(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &link) const;\n\t[[nodiscard]] rpl::producer<Update> updates(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin) const;\n\t[[nodiscard]] rpl::producer<> allRevokedDestroyed(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin) const;\n\n\tvoid requestMoreLinks(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tTimeId lastDate,\n\t\tconst QString &lastLink,\n\t\tbool revoked,\n\t\tFn<void(Links)> done);\n\nprivate:\n\tstruct LinkKey {\n\t\tnot_null<PeerData*> peer;\n\t\tQString link;\n\n\t\tfriend inline bool operator<(const LinkKey &a, const LinkKey &b) {\n\t\t\treturn (a.peer == b.peer)\n\t\t\t\t? (a.link < b.link)\n\t\t\t\t: (a.peer < b.peer);\n\t\t}\n\t\tfriend inline bool operator==(const LinkKey &a, const LinkKey &b) {\n\t\t\treturn (a.peer == b.peer) && (a.link == b.link);\n\t\t}\n\t};\n\tstruct ProcessRequest {\n\t\tFn<void()> done;\n\t\tFn<void()> fail;\n\t};\n\n\t[[nodiscard]] Links parseSlice(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPmessages_ExportedChatInvites &slice) const;\n\t[[nodiscard]] std::optional<Link> parse(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPExportedChatInvite &invite) const;\n\t[[nodiscard]] Link *lookupMyPermanent(not_null<PeerData*> peer);\n\t[[nodiscard]] Link *lookupMyPermanent(Links &links);\n\t[[nodiscard]] const Link *lookupMyPermanent(const Links &links) const;\n\tstd::optional<Link> prepend(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst MTPExportedChatInvite &invite);\n\tvoid prependMyToFirstSlice(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst Link &link);\n\tvoid notify(not_null<PeerData*> peer);\n\n\tvoid editPermanentLink(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &link);\n\n\tvoid performEdit(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst QString &link,\n\t\tFn<void(Link)> done,\n\t\tbool revoke,\n\t\tconst QString &label = QString(),\n\t\tTimeId expireDate = 0,\n\t\tint usageLimit = 0,\n\t\tbool requestApproval = false,\n\t\tbool editOnlyTitle = false);\n\tvoid performCreate(\n\t\tconst CreateInviteLinkArgs &args,\n\t\tbool revokeLegacyPermanent);\n\n\tvoid requestJoinedFirstSlice(LinkKey key);\n\t[[nodiscard]] std::optional<JoinedByLinkSlice> lookupJoinedFirstSlice(\n\t\tLinkKey key) const;\n\n\tconst not_null<ApiWrap*> _api;\n\n\tbase::flat_map<not_null<PeerData*>, Links> _firstSlices;\n\tbase::flat_map<not_null<PeerData*>, mtpRequestId> _firstSliceRequests;\n\n\tbase::flat_map<LinkKey, JoinedByLinkSlice> _firstJoined;\n\tbase::flat_map<LinkKey, mtpRequestId> _firstJoinedRequests;\n\trpl::event_stream<LinkKey> _joinedFirstSliceLoaded;\n\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tstd::vector<Fn<void(Link)>>> _createCallbacks;\n\tbase::flat_map<LinkKey, std::vector<Fn<void(Link)>>> _editCallbacks;\n\tbase::flat_map<LinkKey, std::vector<Fn<void()>>> _deleteCallbacks;\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tstd::vector<Fn<void()>>> _deleteRevokedCallbacks;\n\n\tbase::flat_map<\n\t\tstd::pair<not_null<PeerData*>, not_null<UserData*>>,\n\t\tProcessRequest> _processRequests;\n\n\trpl::event_stream<Update> _updates;\n\n\tstruct AllRevokedDestroyed {\n\t\tnot_null<PeerData*> peer;\n\t\tnot_null<UserData*> admin;\n\t};\n\trpl::event_stream<AllRevokedDestroyed> _allRevokedDestroyed;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_media.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_media.h\"\n\n#include \"api/api_common.h\"\n#include \"data/data_document.h\"\n#include \"data/stickers/data_stickers_set.h\"\n#include \"history/history_item.h\"\n\nnamespace Api {\nnamespace {\n\nMTPVector<MTPDocumentAttribute> ComposeSendingDocumentAttributes(\n\t\tnot_null<DocumentData*> document) {\n\tconst auto filenameAttribute = MTP_documentAttributeFilename(\n\t\tMTP_string(document->filename()));\n\tconst auto dimensions = document->dimensions;\n\tauto attributes = QVector<MTPDocumentAttribute>(1, filenameAttribute);\n\tif (dimensions.width() > 0 && dimensions.height() > 0) {\n\t\tif (document->hasDuration() && !document->hasMimeType(u\"image/gif\"_q)) {\n\t\t\tauto flags = MTPDdocumentAttributeVideo::Flags(0);\n\t\t\tusing VideoFlag = MTPDdocumentAttributeVideo::Flag;\n\t\t\tif (document->isVideoMessage()) {\n\t\t\t\tflags |= VideoFlag::f_round_message;\n\t\t\t}\n\t\t\tif (document->supportsStreaming()) {\n\t\t\t\tflags |= VideoFlag::f_supports_streaming;\n\t\t\t}\n\t\t\tattributes.push_back(MTP_documentAttributeVideo(\n\t\t\t\tMTP_flags(flags),\n\t\t\t\tMTP_double(document->duration() / 1000.),\n\t\t\t\tMTP_int(dimensions.width()),\n\t\t\t\tMTP_int(dimensions.height()),\n\t\t\t\tMTPint(), // preload_prefix_size\n\t\t\t\tMTPdouble(), // video_start_ts\n\t\t\t\tMTPstring())); // video_codec\n\t\t} else {\n\t\t\tattributes.push_back(MTP_documentAttributeImageSize(\n\t\t\t\tMTP_int(dimensions.width()),\n\t\t\t\tMTP_int(dimensions.height())));\n\t\t}\n\t}\n\tif (document->type == AnimatedDocument) {\n\t\tattributes.push_back(MTP_documentAttributeAnimated());\n\t} else if (document->type == StickerDocument && document->sticker()) {\n\t\tattributes.push_back(MTP_documentAttributeSticker(\n\t\t\tMTP_flags(0),\n\t\t\tMTP_string(document->sticker()->alt),\n\t\t\tData::InputStickerSet(document->sticker()->set),\n\t\t\tMTPMaskCoords()));\n\t} else if (const auto song = document->song()) {\n\t\tconst auto flags = MTPDdocumentAttributeAudio::Flag::f_title\n\t\t\t| MTPDdocumentAttributeAudio::Flag::f_performer;\n\t\tattributes.push_back(MTP_documentAttributeAudio(\n\t\t\tMTP_flags(flags),\n\t\t\tMTP_int(document->duration() / 1000),\n\t\t\tMTP_string(song->title),\n\t\t\tMTP_string(song->performer),\n\t\t\tMTPstring()));\n\t} else if (const auto voice = document->voice()) {\n\t\tconst auto flags = MTPDdocumentAttributeAudio::Flag::f_voice\n\t\t\t| MTPDdocumentAttributeAudio::Flag::f_waveform;\n\t\tattributes.push_back(MTP_documentAttributeAudio(\n\t\t\tMTP_flags(flags),\n\t\t\tMTP_int(document->duration() / 1000),\n\t\t\tMTPstring(),\n\t\t\tMTPstring(),\n\t\t\tMTP_bytes(documentWaveformEncode5bit(voice->waveform))));\n\t}\n\treturn MTP_vector<MTPDocumentAttribute>(attributes);\n}\n\n} // namespace\n\nMTPInputMedia PrepareUploadedPhoto(\n\t\tnot_null<HistoryItem*> item,\n\t\tRemoteFileInfo info) {\n\tusing Flag = MTPDinputMediaUploadedPhoto::Flag;\n\tconst auto spoiler = item->media() && item->media()->hasSpoiler();\n\tconst auto ttlSeconds = item->media()\n\t\t? item->media()->ttlSeconds()\n\t\t: 0;\n\tconst auto flags = (spoiler ? Flag::f_spoiler : Flag())\n\t\t| (info.attachedStickers.empty() ? Flag() : Flag::f_stickers)\n\t\t| (ttlSeconds ? Flag::f_ttl_seconds : Flag());\n\treturn MTP_inputMediaUploadedPhoto(\n\t\tMTP_flags(flags),\n\t\tinfo.file,\n\t\tMTP_vector<MTPInputDocument>(\n\t\t\tranges::to<QVector<MTPInputDocument>>(info.attachedStickers)),\n\t\tMTP_int(ttlSeconds),\n\t\tMTPInputDocument()); // video\n}\n\nMTPInputMedia PrepareUploadedDocument(\n\t\tnot_null<HistoryItem*> item,\n\t\tRemoteFileInfo info) {\n\tif (!item || !item->media() || !item->media()->document()) {\n\t\treturn MTP_inputMediaEmpty();\n\t}\n\tusing Flag = MTPDinputMediaUploadedDocument::Flag;\n\tconst auto spoiler = item->media() && item->media()->hasSpoiler();\n\tconst auto ttlSeconds = item->media()\n\t\t? item->media()->ttlSeconds()\n\t\t: 0;\n\tconst auto flags = (spoiler ? Flag::f_spoiler : Flag())\n\t\t| (info.thumb ? Flag::f_thumb : Flag())\n\t\t| (item->groupId() ? Flag::f_nosound_video : Flag())\n\t\t| (info.forceFile ? Flag::f_force_file : Flag())\n\t\t| (info.attachedStickers.empty() ? Flag::f_stickers : Flag())\n\t\t| (ttlSeconds ? Flag::f_ttl_seconds : Flag())\n\t\t| (info.videoCover ? Flag::f_video_cover : Flag());\n\tconst auto document = item->media()->document();\n\treturn MTP_inputMediaUploadedDocument(\n\t\tMTP_flags(flags),\n\t\tinfo.file,\n\t\tinfo.thumb.value_or(MTPInputFile()),\n\t\tMTP_string(document->mimeString()),\n\t\tComposeSendingDocumentAttributes(document),\n\t\tMTP_vector<MTPInputDocument>(\n\t\t\tranges::to<QVector<MTPInputDocument>>(info.attachedStickers)),\n\t\tinfo.videoCover.value_or(MTPInputPhoto()),\n\t\tMTP_int(0), // video_timestamp\n\t\tMTP_int(ttlSeconds));\n}\n\nbool HasAttachedStickers(MTPInputMedia media) {\n\treturn media.match([&](const MTPDinputMediaUploadedPhoto &photo) -> bool {\n\t\treturn (photo.vflags().v\n\t\t\t& MTPDinputMediaUploadedPhoto::Flag::f_stickers);\n\t}, [&](const MTPDinputMediaUploadedDocument &document) -> bool {\n\t\treturn (document.vflags().v\n\t\t\t& MTPDinputMediaUploadedDocument::Flag::f_stickers);\n\t}, [](const auto &d) {\n\t\treturn false;\n\t});\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_media.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass HistoryItem;\n\nnamespace Api {\n\nstruct RemoteFileInfo;\n\nMTPInputMedia PrepareUploadedPhoto(\n\tnot_null<HistoryItem*> item,\n\tRemoteFileInfo info);\n\nMTPInputMedia PrepareUploadedDocument(\n\tnot_null<HistoryItem*> item,\n\tRemoteFileInfo info);\n\nbool HasAttachedStickers(MTPInputMedia media);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_messages_search.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_messages_search.h\"\n\n#include \"apiwrap.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_message_reaction_id.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"main/main_session.h\"\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kSearchPerPage = 50;\n\n[[nodiscard]] MessageIdsList HistoryItemsFromTL(\n\t\tnot_null<Data::Session*> data,\n\t\tconst QVector<MTPMessage> &messages) {\n\tauto result = MessageIdsList();\n\tfor (const auto &message : messages) {\n\t\tconst auto peerId = PeerFromMessage(message);\n\t\tif (data->peerLoaded(peerId)) {\n\t\t\tif (DateFromMessage(message)) {\n\t\t\t\tconst auto item = data->addNewMessage(\n\t\t\t\t\tmessage,\n\t\t\t\t\tMessageFlags(),\n\t\t\t\t\tNewMessageType::Existing);\n\t\t\t\tresult.push_back(item->fullId());\n\t\t\t}\n\t\t} else {\n\t\t\tLOG((\"API Error: a search results with not loaded peer %1\"\n\t\t\t\t).arg(peerId.value));\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] QString RequestToToken(\n\t\tconst MessagesSearch::Request &request) {\n\tauto result = request.query;\n\tif (request.from) {\n\t\tresult += '\\n' + QString::number(request.from->id.value);\n\t}\n\tfor (const auto &tag : request.tags) {\n\t\tresult += '\\n';\n\t\tif (const auto customId = tag.custom()) {\n\t\t\tresult += u\"custom\"_q + QString::number(customId);\n\t\t} else {\n\t\t\tresult += u\"emoji\"_q + tag.emoji();\n\t\t}\n\t}\n\tswitch (request.filter) {\n\tcase SearchFilter::NoFilter: break;\n\tcase SearchFilter::Pinned: result += u\"\\npinned\"_q; break;\n\t}\n\treturn result;\n}\n\n[[nodiscard]] MTPMessagesFilter PrepareFilter(SearchFilter filter) {\n\tswitch (filter) {\n\tcase SearchFilter::Pinned:\n\t\treturn MTP_inputMessagesFilterPinned();\n\tcase SearchFilter::NoFilter:\n\t\treturn MTP_inputMessagesFilterEmpty();\n\t}\n\treturn MTP_inputMessagesFilterEmpty();\n}\n\n} // namespace\n\nMessagesSearch::MessagesSearch(not_null<History*> history)\n: _history(history) {\n}\n\nMessagesSearch::~MessagesSearch() {\n\t_history->owner().histories().cancelRequest(\n\t\tbase::take(_searchInHistoryRequest));\n}\n\nvoid MessagesSearch::searchMessages(Request request) {\n\t_request = std::move(request);\n\t_offsetId = {};\n\tsearchRequest();\n}\n\nvoid MessagesSearch::searchMore() {\n\tif (_searchInHistoryRequest || _requestId) {\n\t\treturn;\n\t}\n\tsearchRequest();\n}\n\nvoid MessagesSearch::searchRequest() {\n\tconst auto nextToken = RequestToToken(_request);\n\tif (!_offsetId) {\n\t\tconst auto it = _cacheOfStartByToken.find(nextToken);\n\t\tif (it != end(_cacheOfStartByToken)) {\n\t\t\t_requestId = 0;\n\t\t\tsearchReceived(it->second, _requestId, nextToken);\n\t\t\treturn;\n\t\t}\n\t}\n\tauto callback = [=](Fn<void()> finish) {\n\t\tusing Flag = MTPmessages_Search::Flag;\n\t\tconst auto from = _request.from;\n\t\tconst auto fromPeer = _history->peer->isUser() ? nullptr : from;\n\t\tconst auto savedPeer = _history->peer->isSelf() ? from : nullptr;\n\t\t_requestId = _history->session().api().request(MTPmessages_Search(\n\t\t\tMTP_flags((fromPeer ? Flag::f_from_id : Flag())\n\t\t\t\t| (savedPeer ? Flag::f_saved_peer_id : Flag())\n\t\t\t\t| (_request.topMsgId ? Flag::f_top_msg_id : Flag())\n\t\t\t\t| (_request.tags.empty() ? Flag() : Flag::f_saved_reaction)),\n\t\t\t_history->peer->input(),\n\t\t\tMTP_string(_request.query),\n\t\t\t(fromPeer ? fromPeer->input() : MTP_inputPeerEmpty()),\n\t\t\t(savedPeer ? savedPeer->input() : MTP_inputPeerEmpty()),\n\t\t\tMTP_vector_from_range(_request.tags | ranges::views::transform(\n\t\t\t\tData::ReactionToMTP\n\t\t\t)),\n\t\t\tMTP_int(_request.topMsgId), // top_msg_id\n\t\t\tPrepareFilter(_request.filter),\n\t\t\tMTP_int(0), // min_date\n\t\t\tMTP_int(0), // max_date\n\t\t\tMTP_int(_offsetId), // offset_id\n\t\t\tMTP_int(0), // add_offset\n\t\t\tMTP_int(kSearchPerPage),\n\t\t\tMTP_int(0), // max_id\n\t\t\tMTP_int(0), // min_id\n\t\t\tMTP_long(0) // hash\n\t\t)).done([=](const TLMessages &result, mtpRequestId id) {\n\t\t\t_searchInHistoryRequest = 0;\n\t\t\tsearchReceived(result, id, nextToken);\n\t\t\tfinish();\n\t\t}).fail([=](const MTP::Error &error, mtpRequestId id) {\n\t\t\t_searchInHistoryRequest = 0;\n\n\t\t\tif (_requestId == id) {\n\t\t\t\t_requestId = 0;\n\t\t\t}\n\t\t\tif (error.type() == u\"SEARCH_QUERY_EMPTY\"_q) {\n\t\t\t\t_messagesFounds.fire({ 0, MessageIdsList(), nextToken });\n\t\t\t}\n\n\t\t\tfinish();\n\t\t}).send();\n\t\treturn _requestId;\n\t};\n\t_searchInHistoryRequest = _history->owner().histories().sendRequest(\n\t\t_history,\n\t\tData::Histories::RequestType::History,\n\t\tstd::move(callback));\n}\n\nvoid MessagesSearch::searchReceived(\n\t\tconst TLMessages &result,\n\t\tmtpRequestId requestId,\n\t\tconst QString &nextToken) {\n\tif (requestId != _requestId) {\n\t\treturn;\n\t}\n\tauto &owner = _history->owner();\n\tauto found = result.match([&](const MTPDmessages_messages &data) {\n\t\tif (_requestId != 0) {\n\t\t\t// Don't apply cached data!\n\t\t\towner.processUsers(data.vusers());\n\t\t\towner.processChats(data.vchats());\n\t\t\t_history->peer->processTopics(data.vtopics());\n\t\t}\n\t\tauto items = HistoryItemsFromTL(&owner, data.vmessages().v);\n\t\tconst auto total = int(data.vmessages().v.size());\n\t\treturn FoundMessages{ total, std::move(items), nextToken };\n\t}, [&](const MTPDmessages_messagesSlice &data) {\n\t\tif (_requestId != 0) {\n\t\t\t// Don't apply cached data!\n\t\t\towner.processUsers(data.vusers());\n\t\t\towner.processChats(data.vchats());\n\t\t\t_history->peer->processTopics(data.vtopics());\n\t\t}\n\t\tauto items = HistoryItemsFromTL(&owner, data.vmessages().v);\n\t\t// data.vnext_rate() is used only in global search.\n\t\tconst auto total = int(data.vcount().v);\n\t\treturn FoundMessages{ total, std::move(items), nextToken };\n\t}, [&](const MTPDmessages_channelMessages &data) {\n\t\tif (_requestId != 0) {\n\t\t\t// Don't apply cached data!\n\t\t\towner.processUsers(data.vusers());\n\t\t\towner.processChats(data.vchats());\n\t\t\tif (const auto channel = _history->peer->asChannel()) {\n\t\t\t\tchannel->ptsReceived(data.vpts().v);\n\t\t\t} else {\n\t\t\t\tLOG((\"API Error: \"\n\t\t\t\t\t\"received messages.channelMessages when no channel \"\n\t\t\t\t\t\"was passed!\"));\n\t\t\t}\n\t\t\t_history->peer->processTopics(data.vtopics());\n\t\t}\n\t\tauto items = HistoryItemsFromTL(&owner, data.vmessages().v);\n\t\tconst auto total = int(data.vcount().v);\n\t\treturn FoundMessages{ total, std::move(items), nextToken };\n\t}, [](const MTPDmessages_messagesNotModified &data) {\n\t\treturn FoundMessages{};\n\t});\n\tif (!_offsetId) {\n\t\t_cacheOfStartByToken.emplace(nextToken, result);\n\t}\n\t_requestId = 0;\n\t_offsetId = found.messages.empty()\n\t\t? MsgId()\n\t\t: found.messages.back().msg;\n\t_messagesFounds.fire(std::move(found));\n}\n\nrpl::producer<FoundMessages> MessagesSearch::messagesFounds() const {\n\treturn _messagesFounds.events();\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_messages_search.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/qt/qt_compare.h\"\n#include \"data/data_message_reaction_id.h\"\n\nclass HistoryItem;\nclass History;\nclass PeerData;\n\nnamespace Data {\nstruct ReactionId;\n} // namespace Data\n\nnamespace Api {\n\nenum class SearchFilter {\n\tNoFilter,\n\tPinned,\n};\n\nstruct FoundMessages {\n\tint total = -1;\n\tMessageIdsList messages;\n\tQString nextToken;\n};\n\nclass MessagesSearch final {\npublic:\n\tstruct Request {\n\t\tQString query;\n\t\tPeerData *from = nullptr;\n\t\tstd::vector<Data::ReactionId> tags;\n\t\tMsgId topMsgId;\n\t\tSearchFilter filter = SearchFilter::NoFilter;\n\n\t\tfriend inline bool operator==(\n\t\t\tconst Request &,\n\t\t\tconst Request &) = default;\n\t\tfriend inline auto operator<=>(\n\t\t\tconst Request &,\n\t\t\tconst Request &) = default;\n\t};\n\n\texplicit MessagesSearch(not_null<History*> history);\n\t~MessagesSearch();\n\n\tvoid searchMessages(Request request);\n\tvoid searchMore();\n\n\t[[nodiscard]] rpl::producer<FoundMessages> messagesFounds() const;\n\nprivate:\n\tusing TLMessages = MTPmessages_Messages;\n\tvoid searchRequest();\n\tvoid searchReceived(\n\t\tconst TLMessages &result,\n\t\tmtpRequestId requestId,\n\t\tconst QString &nextToken);\n\n\tconst not_null<History*> _history;\n\n\tbase::flat_map<QString, TLMessages> _cacheOfStartByToken;\n\n\tRequest _request;\n\tMsgId _offsetId;\n\n\tint _searchInHistoryRequest = 0; // Not real mtpRequestId.\n\tmtpRequestId _requestId = 0;\n\n\trpl::event_stream<FoundMessages> _messagesFounds;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_messages_search_merged.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_messages_search_merged.h\"\n\n#include \"history/history.h\"\n\nnamespace Api {\n\nMessagesSearchMerged::MessagesSearchMerged(not_null<History*> history)\n: _apiSearch(history) {\n\tif (const auto migrated = history->migrateFrom()) {\n\t\t_migratedSearch.emplace(migrated);\n\t}\n\tconst auto checkWaitingForTotal = [=] {\n\t\tif (_waitingForTotal) {\n\t\t\tif (_concatedFound.total >= 0 && _migratedFirstFound.total >= 0) {\n\t\t\t\t_waitingForTotal = false;\n\t\t\t\t_concatedFound.total += _migratedFirstFound.total;\n\t\t\t\t_newFounds.fire({});\n\t\t\t}\n\t\t} else {\n\t\t\t_newFounds.fire({});\n\t\t}\n\t};\n\n\tconst auto checkFull = [=](const FoundMessages &data) {\n\t\tif (data.total == int(_concatedFound.messages.size())) {\n\t\t\t_isFull = true;\n\t\t\taddFound(_migratedFirstFound);\n\t\t}\n\t};\n\n\t_apiSearch.messagesFounds(\n\t) | rpl::on_next([=](const FoundMessages &data) {\n\t\tif (data.nextToken == _concatedFound.nextToken) {\n\t\t\taddFound(data);\n\t\t\tcheckFull(data);\n\t\t\t_nextFounds.fire({});\n\t\t} else {\n\t\t\t_concatedFound = data;\n\t\t\tcheckFull(data);\n\t\t\tcheckWaitingForTotal();\n\t\t}\n\t}, _lifetime);\n\n\tif (_migratedSearch) {\n\t\t_migratedSearch->messagesFounds(\n\t\t) | rpl::on_next([=](const FoundMessages &data) {\n\t\t\tif (_isFull) {\n\t\t\t\taddFound(data);\n\t\t\t}\n\t\t\tif (data.nextToken == _migratedFirstFound.nextToken) {\n\t\t\t\t_nextFounds.fire({});\n\t\t\t} else {\n\t\t\t\t_migratedFirstFound = data;\n\t\t\t\tcheckWaitingForTotal();\n\t\t\t}\n\t\t}, _lifetime);\n\t}\n}\n\nvoid MessagesSearchMerged::disableMigrated() {\n\t_migratedSearch = std::nullopt;\n\t_waitingForTotal = false;\n\t_isFull = false;\n}\n\nvoid MessagesSearchMerged::addFound(const FoundMessages &data) {\n\tfor (const auto &message : data.messages) {\n\t\t_concatedFound.messages.push_back(message);\n\t}\n}\n\nconst FoundMessages &MessagesSearchMerged::messages() const {\n\treturn _concatedFound;\n}\n\nconst MessagesSearch::Request &MessagesSearchMerged::request() const {\n\treturn _request;\n}\n\nvoid MessagesSearchMerged::clear() {\n\t_concatedFound = {};\n\t_migratedFirstFound = {};\n\t_waitingForTotal = false;\n\t_isFull = false;\n}\n\nvoid MessagesSearchMerged::search(const Request &search) {\n\t_request = search;\n\t_isFull = false;\n\t_waitingForTotal = (_migratedSearch != std::nullopt);\n\tif (_migratedSearch) {\n\t\t_migratedSearch->searchMessages(search);\n\t}\n\t_apiSearch.searchMessages(search);\n}\n\nvoid MessagesSearchMerged::searchMore() {\n\tif (_migratedSearch && _isFull) {\n\t\t_migratedSearch->searchMore();\n\t} else {\n\t\t_apiSearch.searchMore();\n\t}\n}\n\nrpl::producer<> MessagesSearchMerged::newFounds() const {\n\treturn _newFounds.events();\n}\n\nrpl::producer<> MessagesSearchMerged::nextFounds() const {\n\treturn _nextFounds.events();\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_messages_search_merged.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_messages_search.h\"\n\nclass History;\nclass PeerData;\n\nnamespace Data {\nstruct ReactionId;\n} // namespace Data\n\nnamespace Api {\n\n// Search in both of history and migrated history, if it exists.\nclass MessagesSearchMerged final {\npublic:\n\tusing Request = MessagesSearch::Request;\n\tusing CachedRequests = base::flat_set<Request>;\n\n\tMessagesSearchMerged(not_null<History*> history);\n\n\tvoid clear();\n\tvoid search(const Request &search);\n\tvoid searchMore();\n\tvoid disableMigrated();\n\n\t[[nodiscard]] const FoundMessages &messages() const;\n\t[[nodiscard]] const Request &request() const;\n\n\t[[nodiscard]] rpl::producer<> newFounds() const;\n\t[[nodiscard]] rpl::producer<> nextFounds() const;\n\nprivate:\n\tvoid addFound(const FoundMessages &data);\n\n\tMessagesSearch _apiSearch;\n\tRequest _request;\n\n\tstd::optional<MessagesSearch> _migratedSearch;\n\tFoundMessages _migratedFirstFound;\n\n\tFoundMessages _concatedFound;\n\n\tbool _waitingForTotal = false;\n\tbool _isFull = false;\n\n\trpl::event_stream<> _newFounds;\n\trpl::event_stream<> _nextFounds;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_peer_colors.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_peer_colors.h\"\n\n#include \"apiwrap.h\"\n#include \"data/data_peer.h\"\n#include \"window/themes/window_theme.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/color_int_conversion.h\"\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kRequestEach = 3600 * crl::time(1000);\n\n} // namespace\n\nPeerColors::PeerColors(not_null<ApiWrap*> api)\n: _api(&api->instance())\n, _timer([=] { request(); requestProfile(); }) {\n\trequest();\n\trequestProfile();\n\t_timer.callEach(kRequestEach);\n}\n\nPeerColors::~PeerColors() = default;\n\nvoid PeerColors::request() {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\t_requestId = _api.request(MTPhelp_GetPeerColors(\n\t\tMTP_int(_hash)\n\t)).done([=](const MTPhelp_PeerColors &result) {\n\t\t_requestId = 0;\n\t\tresult.match([&](const MTPDhelp_peerColors &data) {\n\t\t\t_hash = data.vhash().v;\n\t\t\tapply(data);\n\t\t}, [](const MTPDhelp_peerColorsNotModified &) {\n\t\t});\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t}).send();\n}\n\nvoid PeerColors::requestProfile() {\n\tif (_profileRequestId) {\n\t\treturn;\n\t}\n\t_profileRequestId = _api.request(MTPhelp_GetPeerProfileColors(\n\t\tMTP_int(_profileHash)\n\t)).done([=](const MTPhelp_PeerColors &result) {\n\t\t_profileRequestId = 0;\n\t\tresult.match([&](const MTPDhelp_peerColors &data) {\n\t\t\t_profileHash = data.vhash().v;\n\t\t\tapplyProfile(data);\n\t\t}, [](const MTPDhelp_peerColorsNotModified &) {\n\t\t});\n\t}).fail([=] {\n\t\t_profileRequestId = 0;\n\t}).send();\n}\n\nstd::vector<uint8> PeerColors::suggested() const {\n\treturn _suggested.current();\n}\n\nrpl::producer<std::vector<uint8>> PeerColors::suggestedValue() const {\n\treturn _suggested.value();\n}\n\nauto PeerColors::indicesValue() const\n-> rpl::producer<Ui::ColorIndicesCompressed> {\n\treturn rpl::single(\n\t\tindicesCurrent()\n\t) | rpl::then(_colorIndicesChanged.events() | rpl::map([=] {\n\t\treturn indicesCurrent();\n\t}));\n}\n\nUi::ColorIndicesCompressed PeerColors::indicesCurrent() const {\n\treturn _colorIndicesCurrent\n\t\t? *_colorIndicesCurrent\n\t\t: Ui::ColorIndicesCompressed();\n}\n\nconst base::flat_map<uint8, int> &PeerColors::requiredLevelsGroup() const {\n\treturn _requiredLevelsGroup;\n}\n\nconst base::flat_map<uint8, int> &PeerColors::requiredLevelsChannel() const {\n\treturn _requiredLevelsChannel;\n}\n\nint PeerColors::requiredLevelFor(\n\t\tPeerId channel,\n\t\tuint8 index,\n\t\tbool isMegagroup,\n\t\tbool profile) const {\n\tif (Data::DecideColorIndex(channel) == index) {\n\t\treturn 0;\n\t}\n\tif (profile) {\n\t\tconst auto it = _profileColors.find(index);\n\t\tif (it != end(_profileColors)) {\n\t\t\treturn isMegagroup\n\t\t\t\t? it->second.requiredLevelsGroup\n\t\t\t\t: it->second.requiredLevelsChannel;\n\t\t}\n\t\treturn 1;\n\t}\n\tconst auto &levels = isMegagroup\n\t\t? _requiredLevelsGroup\n\t\t: _requiredLevelsChannel;\n\tif (const auto i = levels.find(index); i != end(levels)) {\n\t\treturn i->second;\n\t}\n\treturn 1;\n}\n\nvoid PeerColors::apply(const MTPDhelp_peerColors &data) {\n\tauto suggested = std::vector<uint8>();\n\tauto colors = std::make_shared<\n\t\tstd::array<Ui::ColorIndexData, Ui::kColorIndexCount>>();\n\n\tusing ParsedColor = std::array<uint32, Ui::kColorPatternsCount>;\n\tconst auto parseColors = [](const MTPhelp_PeerColorSet &set) {\n\t\treturn set.match([&](const MTPDhelp_peerColorSet &data) {\n\t\t\tauto result = ParsedColor();\n\t\t\tconst auto &list = data.vcolors().v;\n\t\t\tif (list.empty() || list.size() > Ui::kColorPatternsCount) {\n\t\t\t\tLOG((\"API Error: Bad count for PeerColorSet.colors: %1\"\n\t\t\t\t\t).arg(list.size()));\n\t\t\t\treturn ParsedColor();\n\t\t\t}\n\t\t\tauto fill = result.data();\n\t\t\tfor (const auto &color : list) {\n\t\t\t\t*fill++ = (uint32(1) << 24) | uint32(color.v);\n\t\t\t}\n\t\t\treturn result;\n\t\t}, [](const MTPDhelp_peerColorProfileSet &) {\n\t\t\tLOG((\"API Error: peerColorProfileSet in colors result!\"));\n\t\t\treturn ParsedColor();\n\t\t});\n\t};\n\n\tconst auto &list = data.vcolors().v;\n\t_requiredLevelsGroup.clear();\n\t_requiredLevelsChannel.clear();\n\tsuggested.reserve(list.size());\n\tfor (const auto &color : list) {\n\t\tconst auto &data = color.data();\n\t\tconst auto colorIndexBare = data.vcolor_id().v;\n\t\tif (colorIndexBare < 0 || colorIndexBare >= Ui::kColorIndexCount) {\n\t\t\tLOG((\"API Error: Bad color index: %1\").arg(colorIndexBare));\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto colorIndex = uint8(colorIndexBare);\n\t\tif (const auto min = data.vgroup_min_level()) {\n\t\t\t_requiredLevelsGroup[colorIndex] = min->v;\n\t\t}\n\t\tif (const auto min = data.vchannel_min_level()) {\n\t\t\t_requiredLevelsChannel[colorIndex] = min->v;\n\t\t}\n\t\tif (!data.is_hidden()) {\n\t\t\tsuggested.push_back(colorIndex);\n\t\t}\n\t\tif (const auto light = data.vcolors()) {\n\t\t\tauto &fields = (*colors)[colorIndex];\n\t\t\tfields.light = parseColors(*light);\n\t\t\tif (const auto dark = data.vdark_colors()) {\n\t\t\t\tfields.dark = parseColors(*dark);\n\t\t\t} else {\n\t\t\t\tfields.dark = fields.light;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!_colorIndicesCurrent) {\n\t\t_colorIndicesCurrent = std::make_unique<Ui::ColorIndicesCompressed>(\n\t\t\tUi::ColorIndicesCompressed{ std::move(colors) });\n\t\t_colorIndicesChanged.fire({});\n\t} else if (*_colorIndicesCurrent->colors != *colors) {\n\t\t_colorIndicesCurrent->colors = std::move(colors);\n\t\t_colorIndicesChanged.fire({});\n\t}\n\t_suggested = std::move(suggested);\n}\n\nvoid PeerColors::applyProfile(const MTPDhelp_peerColors &data) {\n\tconst auto parseColors = [](const MTPhelp_PeerColorSet &set) {\n\t\tconst auto toUint = [](const MTPint &c) {\n\t\t\treturn (uint32(1) << 24) | uint32(c.v);\n\t\t};\n\t\treturn set.match([&](const MTPDhelp_peerColorSet &) {\n\t\t\tLOG((\"API Error: peerColorSet in profile colors result!\"));\n\t\t\treturn Data::ColorProfileSet();\n\t\t}, [&](const MTPDhelp_peerColorProfileSet &data) {\n\t\t\tauto set = Data::ColorProfileSet();\n\t\t\tset.palette.reserve(data.vpalette_colors().v.size());\n\t\t\tset.bg.reserve(data.vbg_colors().v.size());\n\t\t\tset.story.reserve(data.vstory_colors().v.size());\n\t\t\tfor (const auto &c : data.vpalette_colors().v) {\n\t\t\t\tset.palette.push_back(Ui::ColorFromSerialized(toUint(c)));\n\t\t\t}\n\t\t\tfor (const auto &c : data.vbg_colors().v) {\n\t\t\t\tset.bg.push_back(Ui::ColorFromSerialized(toUint(c)));\n\t\t\t}\n\t\t\tfor (const auto &c : data.vstory_colors().v) {\n\t\t\t\tset.story.push_back(Ui::ColorFromSerialized(toUint(c)));\n\t\t\t}\n\t\t\treturn set;\n\t\t});\n\t};\n\n\tauto suggested = std::vector<Data::ColorProfileData>();\n\tconst auto &list = data.vcolors().v;\n\tsuggested.reserve(list.size());\n\tfor (const auto &color : list) {\n\t\tconst auto &data = color.data();\n\t\tconst auto colorIndexBare = data.vcolor_id().v;\n\t\tif (colorIndexBare < 0 || colorIndexBare >= Ui::kColorIndexCount) {\n\t\t\tLOG((\"API Error: Bad color index: %1\").arg(colorIndexBare));\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto colorIndex = uint8(colorIndexBare);\n\t\tauto result = ProfileColorOption();\n\t\tresult.isHidden = data.is_hidden();\n\t\tif (const auto min = data.vgroup_min_level()) {\n\t\t\tresult.requiredLevelsGroup = min->v;\n\t\t}\n\t\tif (const auto min = data.vchannel_min_level()) {\n\t\t\tresult.requiredLevelsChannel = min->v;\n\t\t}\n\t\tif (const auto light = data.vcolors()) {\n\t\t\tresult.data.light = parseColors(*light);\n\t\t}\n\t\tif (const auto dark = data.vdark_colors()) {\n\t\t\tresult.data.dark = parseColors(*dark);\n\t\t}\n\t\t_profileColors[colorIndex] = std::move(result);\n\t}\n}\n\nstd::optional<Data::ColorProfileSet> PeerColors::colorProfileFor(\n\t\tnot_null<PeerData*> peer) const {\n\tif (const auto colorProfileIndex = peer->colorProfileIndex()) {\n\t\treturn colorProfileFor(*colorProfileIndex);\n\t}\n\treturn std::nullopt;\n}\n\nstd::optional<Data::ColorProfileSet> PeerColors::colorProfileFor(\n\t\tuint8 index) const {\n\tconst auto i = _profileColors.find(index);\n\tif (i != end(_profileColors)) {\n\t\treturn Window::Theme::IsNightMode()\n\t\t\t? i->second.data.dark\n\t\t\t: i->second.data.light;\n\t}\n\treturn std::nullopt;\n}\n\nstd::vector<uint8> PeerColors::profileColorIndices() const {\n\tauto result = std::vector<uint8>();\n\tresult.reserve(_profileColors.size());\n\tfor (const auto &[index, option] : _profileColors) {\n\t\tresult.push_back(index);\n\t}\n\treturn result;\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_peer_colors.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"data/data_peer_colors.h\"\n#include \"mtproto/sender.h\"\n\nclass ApiWrap;\n\nnamespace Ui {\nstruct ColorIndicesCompressed;\n} // namespace Ui\n\nnamespace Api {\n\nclass PeerColors final {\npublic:\n\texplicit PeerColors(not_null<ApiWrap*> api);\n\t~PeerColors();\n\n\t[[nodiscard]] std::vector<uint8> suggested() const;\n\t[[nodiscard]] rpl::producer<std::vector<uint8>> suggestedValue() const;\n\t[[nodiscard]] Ui::ColorIndicesCompressed indicesCurrent() const;\n\t[[nodiscard]] auto indicesValue() const\n\t\t-> rpl::producer<Ui::ColorIndicesCompressed>;\n\n\t[[nodiscard]] auto requiredLevelsGroup() const\n\t\t-> const base::flat_map<uint8, int> &;\n\t[[nodiscard]] auto requiredLevelsChannel() const\n\t\t-> const base::flat_map<uint8, int> &;\n\n\t[[nodiscard]] int requiredLevelFor(\n\t\tPeerId channel,\n\t\tuint8 index,\n\t\tbool isMegagroup,\n\t\tbool profile) const;\n\n\t[[nodiscard]] std::optional<Data::ColorProfileSet> colorProfileFor(\n\t\tnot_null<PeerData*> peer) const;\n\t[[nodiscard]] std::optional<Data::ColorProfileSet> colorProfileFor(\n\t\tuint8 index) const;\n\n\t[[nodiscard]] std::vector<uint8> profileColorIndices() const;\n\nprivate:\n\tstruct ProfileColorOption {\n\t\tData::ColorProfileData data;\n\t\tint requiredLevelsChannel = 0;\n\t\tint requiredLevelsGroup = 0;\n\t\tbool isHidden = false;\n\t};\n\n\tvoid request();\n\tvoid requestProfile();\n\tvoid apply(const MTPDhelp_peerColors &data);\n\tvoid applyProfile(const MTPDhelp_peerColors &data);\n\n\tMTP::Sender _api;\n\tint32 _hash = 0;\n\tint32 _profileHash = 0;\n\n\tmtpRequestId _requestId = 0;\n\tmtpRequestId _profileRequestId = 0;\n\tbase::Timer _timer;\n\trpl::variable<std::vector<uint8>> _suggested;\n\tbase::flat_map<uint8, int> _requiredLevelsGroup;\n\tbase::flat_map<uint8, int> _requiredLevelsChannel;\n\trpl::event_stream<> _colorIndicesChanged;\n\tstd::unique_ptr<Ui::ColorIndicesCompressed> _colorIndicesCurrent;\n\tbase::flat_map<uint8, ProfileColorOption> _profileColors;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_peer_photo.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_peer_photo.h\"\n\n#include \"api/api_updates.h\"\n#include \"apiwrap.h\"\n#include \"base/random.h\"\n#include \"base/unixtime.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_user_photos.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n#include \"storage/file_upload.h\"\n#include \"storage/localimageloader.h\"\n#include \"storage/storage_user_photos.h\"\n\n#include <QtCore/QBuffer>\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kSharedMediaLimit = 100;\n\n[[nodiscard]] std::shared_ptr<FilePrepareResult> PreparePeerPhoto(\n\t\tMTP::DcId dcId,\n\t\tPeerId peerId,\n\t\tQImage &&image) {\n\tPreparedPhotoThumbs photoThumbs;\n\tQVector<MTPPhotoSize> photoSizes;\n\n\tQByteArray jpeg;\n\tQBuffer jpegBuffer(&jpeg);\n\timage.save(&jpegBuffer, \"JPG\", 87);\n\n\tconst auto scaled = [&](int size) {\n\t\treturn image.scaled(\n\t\t\tsize,\n\t\t\tsize,\n\t\t\tQt::KeepAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t};\n\tconst auto push = [&](\n\t\t\tconst char *type,\n\t\t\tQImage &&image,\n\t\t\tQByteArray bytes = QByteArray()) {\n\t\tphotoSizes.push_back(MTP_photoSize(\n\t\t\tMTP_string(type),\n\t\t\tMTP_int(image.width()),\n\t\t\tMTP_int(image.height()), MTP_int(0)));\n\t\tphotoThumbs.emplace(type[0], PreparedPhotoThumb{\n\t\t\t.image = std::move(image),\n\t\t\t.bytes = std::move(bytes)\n\t\t});\n\t};\n\tpush(\"a\", scaled(160));\n\tpush(\"b\", scaled(320));\n\tpush(\"c\", std::move(image), jpeg);\n\n\tconst auto id = base::RandomValue<PhotoId>();\n\tconst auto photo = MTP_photo(\n\t\tMTP_flags(0),\n\t\tMTP_long(id),\n\t\tMTP_long(0),\n\t\tMTP_bytes(),\n\t\tMTP_int(base::unixtime::now()),\n\t\tMTP_vector<MTPPhotoSize>(photoSizes),\n\t\tMTPVector<MTPVideoSize>(),\n\t\tMTP_int(dcId));\n\n\tauto result = MakePreparedFile({\n\t\t.id = id,\n\t\t.type = SendMediaType::Photo,\n\t});\n\tresult->type = SendMediaType::Photo;\n\tresult->setFileData(jpeg);\n\tresult->thumbId = id;\n\tresult->thumbname = \"thumb.jpg\";\n\tresult->photo = photo;\n\tresult->photoThumbs = photoThumbs;\n\treturn result;\n}\n\n[[nodiscard]] std::optional<MTPVideoSize> PrepareMtpMarkup(\n\t\tnot_null<Main::Session*> session,\n\t\tconst PeerPhoto::UserPhoto &d) {\n\tconst auto &documentId = d.markupDocumentId;\n\tconst auto &colors = d.markupColors;\n\tif (!documentId || colors.empty()) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto document = session->data().document(documentId);\n\tif (const auto sticker = document->sticker()) {\n\t\tif (sticker->isStatic()) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\tconst auto serializeColor = [](const QColor &color) {\n\t\t\treturn (quint32(std::clamp(color.red(), 0, 255)) << 16)\n\t\t\t\t| (quint32(std::clamp(color.green(), 0, 255)) << 8)\n\t\t\t\t| quint32(std::clamp(color.blue(), 0, 255));\n\t\t};\n\n\t\tauto mtpColors = QVector<MTPint>();\n\t\tmtpColors.reserve(colors.size());\n\t\tranges::transform(\n\t\t\tcolors,\n\t\t\tranges::back_inserter(mtpColors),\n\t\t\t[&](const QColor &c) { return MTP_int(serializeColor(c)); });\n\t\tif (sticker->setType == Data::StickersType::Emoji) {\n\t\t\treturn MTP_videoSizeEmojiMarkup(\n\t\t\t\tMTP_long(document->id),\n\t\t\t\tMTP_vector(mtpColors));\n\t\t} else if (sticker->set.id && sticker->set.accessHash) {\n\t\t\treturn MTP_videoSizeStickerMarkup(\n\t\t\t\tMTP_inputStickerSetID(\n\t\t\t\t\tMTP_long(sticker->set.id),\n\t\t\t\t\tMTP_long(sticker->set.accessHash)),\n\t\t\t\tMTP_long(document->id),\n\t\t\t\tMTP_vector(mtpColors));\n\t\t} else if (!sticker->set.shortName.isEmpty()) {\n\t\t\treturn MTP_videoSizeStickerMarkup(\n\t\t\t\tMTP_inputStickerSetShortName(\n\t\t\t\t\tMTP_string(sticker->set.shortName)),\n\t\t\t\tMTP_long(document->id),\n\t\t\t\tMTP_vector(mtpColors));\n\t\t} else {\n\t\t\treturn MTP_videoSizeEmojiMarkup(\n\t\t\t\tMTP_long(document->id),\n\t\t\t\tMTP_vector(mtpColors));\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\n} // namespace\n\nPeerPhoto::PeerPhoto(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance()) {\n\tcrl::on_main(_session, [=] {\n\t\tauto &uploader = _session->uploader();\n\n\t\t// You can't use _session->lifetime() in the constructor,\n\t\t// only queued, because it is not constructed yet.\n\t\tuploader.photoReady(\n\t\t) | rpl::on_next([=](const Storage::UploadedMedia &data) {\n\t\t\tready(data.fullId, data.info.file, std::nullopt);\n\t\t}, _session->lifetime());\n\n\t\tuploader.photoProgress(\n\t\t) | rpl::on_next([=](const FullMsgId &id) {\n\t\t\tconst auto i = _uploads.find(id);\n\t\t\tif (i == end(_uploads) || !i->second.photoId) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto peer = i->second.peer;\n\t\t\tconst auto photo = _session->data().photo(\n\t\t\t\ti->second.photoId);\n\t\t\t_uploadProgress.fire({ peer, photo->progress() });\n\t\t}, _session->lifetime());\n\n\t\tuploader.photoFailed(\n\t\t) | rpl::on_next([=](const FullMsgId &id) {\n\t\t\tconst auto i = _uploads.find(id);\n\t\t\tif (i == end(_uploads)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto peer = i->second.peer;\n\t\t\t_uploads.erase(i);\n\t\t\t_uploadFailed.fire_copy(peer);\n\t\t}, _session->lifetime());\n\t});\n}\n\nvoid PeerPhoto::upload(\n\t\tnot_null<PeerData*> peer,\n\t\tUserPhoto &&photo,\n\t\tFn<void()> done) {\n\tupload(peer, std::move(photo), UploadType::Default, std::move(done));\n}\n\nvoid PeerPhoto::uploadFallback(not_null<PeerData*> peer, UserPhoto &&photo) {\n\tupload(peer, std::move(photo), UploadType::Fallback, nullptr);\n}\n\nvoid PeerPhoto::updateSelf(\n\t\tnot_null<PhotoData*> photo,\n\t\tData::FileOrigin origin,\n\t\tFn<void()> done) {\n\tconst auto send = [=](auto resend) -> void {\n\t\tconst auto usedFileReference = photo->fileReference();\n\t\t_api.request(MTPphotos_UpdateProfilePhoto(\n\t\t\tMTP_flags(0),\n\t\t\tMTPInputUser(), // bot\n\t\t\tphoto->mtpInput()\n\t\t)).done([=](const MTPphotos_Photo &result) {\n\t\t\tresult.match([&](const MTPDphotos_photo &data) {\n\t\t\t\t_session->data().processPhoto(data.vphoto());\n\t\t\t\t_session->data().processUsers(data.vusers());\n\t\t\t});\n\t\t\tif (done) {\n\t\t\t\tdone();\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tif (error.code() == 400\n\t\t\t\t&& error.type().startsWith(u\"FILE_REFERENCE_\"_q)) {\n\t\t\t\tphoto->session().api().refreshFileReference(origin, [=](\n\t\t\t\t\t\tconst auto &) {\n\t\t\t\t\tif (photo->fileReference() != usedFileReference) {\n\t\t\t\t\t\tresend(resend);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t}).send();\n\t};\n\tsend(send);\n}\n\nvoid PeerPhoto::upload(\n\t\tnot_null<PeerData*> peer,\n\t\tUserPhoto &&photo,\n\t\tUploadType type,\n\t\tFn<void()> done) {\n\tpeer = peer->migrateToOrMe();\n\tconst auto mtpMarkup = PrepareMtpMarkup(_session, photo);\n\n\tconst auto fakeId = FullMsgId(\n\t\tpeer->id,\n\t\t_session->data().nextLocalMessageId());\n\tconst auto already = ranges::find(\n\t\t_uploads,\n\t\tpeer,\n\t\t[](const auto &pair) { return pair.second.peer; });\n\tif (already != end(_uploads)) {\n\t\t_session->uploader().cancel(already->first);\n\t\t_uploads.erase(already);\n\t}\n\tconst auto &[it, ok] = _uploads.emplace(\n\t\tfakeId,\n\t\tUploadValue{ peer, type, std::move(done), PhotoId(0) });\n\tif (mtpMarkup) {\n\t\tready(fakeId, std::nullopt, mtpMarkup);\n\t} else {\n\t\tconst auto prepared = PreparePeerPhoto(\n\t\t\t_api.instance().mainDcId(),\n\t\t\tpeer->id,\n\t\t\tbase::take(photo.image));\n\t\tit->second.photoId = prepared->thumbId;\n\t\t_session->uploader().upload(fakeId, prepared);\n\t}\n}\n\nvoid PeerPhoto::suggest(not_null<PeerData*> peer, UserPhoto &&photo) {\n\tupload(peer, std::move(photo), UploadType::Suggestion, nullptr);\n}\n\nvoid PeerPhoto::subscribeToUpload(\n\t\tnot_null<PeerData*> peer,\n\t\trpl::lifetime &lifetime,\n\t\tUploadCallbacks callbacks) {\n\tuploadProgress(\n\t) | rpl::filter([=](const UploadProgress &data) {\n\t\treturn (data.peer == peer);\n\t}) | rpl::on_next([cb = callbacks.progress](const UploadProgress &data) {\n\t\tif (cb) {\n\t\t\tcb(data.progress);\n\t\t}\n\t}, lifetime);\n\n\tuploadDone(\n\t) | rpl::filter([=](not_null<PeerData*> p) {\n\t\treturn (p == peer);\n\t}) | rpl::on_next([cb = callbacks.done](not_null<PeerData*>) {\n\t\tif (cb) {\n\t\t\tcb();\n\t\t}\n\t}, lifetime);\n\n\tuploadFailed(\n\t) | rpl::filter([=](not_null<PeerData*> p) {\n\t\treturn (p == peer);\n\t}) | rpl::on_next([cb = callbacks.failed](not_null<PeerData*>) {\n\t\tif (cb) {\n\t\t\tcb();\n\t\t}\n\t}, lifetime);\n}\n\nauto PeerPhoto::uploadProgress() const\n-> rpl::producer<UploadProgress> {\n\treturn _uploadProgress.events();\n}\n\nauto PeerPhoto::uploadDone() const\n-> rpl::producer<not_null<PeerData*>> {\n\treturn _uploadDone.events();\n}\n\nauto PeerPhoto::uploadFailed() const\n-> rpl::producer<not_null<PeerData*>> {\n\treturn _uploadFailed.events();\n}\n\nvoid PeerPhoto::cancelUpload(not_null<PeerData*> peer) {\n\tpeer = peer->migrateToOrMe();\n\tconst auto i = ranges::find(\n\t\t_uploads,\n\t\tpeer,\n\t\t[](const auto &pair) { return pair.second.peer; });\n\tif (i == end(_uploads)) {\n\t\treturn;\n\t}\n\tconst auto fakeId = i->first;\n\t_uploads.erase(i);\n\t_session->uploader().cancel(fakeId);\n\t_uploadFailed.fire_copy(peer);\n}\n\nvoid PeerPhoto::clear(not_null<PhotoData*> photo) {\n\tconst auto self = _session->user();\n\tif (self->userpicPhotoId() == photo->id) {\n\t\tconst auto photoId = photo->id;\n\t\tconst auto peerId = self->id;\n\t\t_api.request(MTPphotos_UpdateProfilePhoto(\n\t\t\tMTP_flags(0),\n\t\t\tMTPInputUser(), // bot\n\t\t\tMTP_inputPhotoEmpty()\n\t\t)).done([=](const MTPphotos_Photo &result) {\n\t\t\tself->setPhoto(MTP_userProfilePhotoEmpty());\n\t\t\t_session->storage().remove(\n\t\t\t\tStorage::UserPhotosRemoveOne(peerToUser(peerId), photoId));\n\t\t}).send();\n\t} else if (photo->peer && photo->peer->userpicPhotoId() == photo->id) {\n\t\tconst auto applier = [=](const MTPUpdates &result) {\n\t\t\t_session->updates().applyUpdates(result);\n\t\t};\n\t\tif (const auto chat = photo->peer->asChat()) {\n\t\t\t_api.request(MTPmessages_EditChatPhoto(\n\t\t\t\tchat->inputChat(),\n\t\t\t\tMTP_inputChatPhotoEmpty()\n\t\t\t)).done(applier).send();\n\t\t} else if (const auto channel = photo->peer->asChannel()) {\n\t\t\t_api.request(MTPchannels_EditPhoto(\n\t\t\t\tchannel->inputChannel(),\n\t\t\t\tMTP_inputChatPhotoEmpty()\n\t\t\t)).done(applier).send();\n\t\t}\n\t} else {\n\t\tconst auto fallbackPhotoId = SyncUserFallbackPhotoViewer(self);\n\t\tif (fallbackPhotoId && (*fallbackPhotoId) == photo->id) {\n\t\t\t_api.request(MTPphotos_UpdateProfilePhoto(\n\t\t\t\tMTP_flags(MTPphotos_UpdateProfilePhoto::Flag::f_fallback),\n\t\t\t\tMTPInputUser(), // bot\n\t\t\t\tMTP_inputPhotoEmpty()\n\t\t\t)).send();\n\t\t\t_session->storage().add(Storage::UserPhotosSetBack(\n\t\t\t\tpeerToUser(self->id),\n\t\t\t\tPhotoId()));\n\t\t} else {\n\t\t\t_api.request(MTPphotos_DeletePhotos(\n\t\t\t\tMTP_vector<MTPInputPhoto>(1, photo->mtpInput())\n\t\t\t)).send();\n\t\t\t_session->storage().remove(Storage::UserPhotosRemoveOne(\n\t\t\t\tpeerToUser(self->id),\n\t\t\t\tphoto->id));\n\t\t}\n\t}\n}\n\nvoid PeerPhoto::clearPersonal(not_null<UserData*> user) {\n\t_api.request(MTPphotos_UploadContactProfilePhoto(\n\t\tMTP_flags(MTPphotos_UploadContactProfilePhoto::Flag::f_save),\n\t\tuser->inputUser(),\n\t\tMTPInputFile(),\n\t\tMTPInputFile(), // video\n\t\tMTPdouble(), // video_start_ts\n\t\tMTPVideoSize() // video_emoji_markup\n\t)).done([=](const MTPphotos_Photo &result) {\n\t\tresult.match([&](const MTPDphotos_photo &data) {\n\t\t\t_session->data().processPhoto(data.vphoto());\n\t\t\t_session->data().processUsers(data.vusers());\n\t\t});\n\t}).send();\n\n\tif (!user->userpicPhotoUnknown() && user->hasPersonalPhoto()) {\n\t\t_session->storage().remove(Storage::UserPhotosRemoveOne(\n\t\t\tpeerToUser(user->id),\n\t\t\tuser->userpicPhotoId()));\n\t}\n}\n\nvoid PeerPhoto::set(not_null<PeerData*> peer, not_null<PhotoData*> photo) {\n\tif (peer->userpicPhotoId() == photo->id) {\n\t\treturn;\n\t}\n\tif (peer == _session->user()) {\n\t\tconst auto photoId = photo->id;\n\t\tconst auto peerId = peer->id;\n\t\t_api.request(MTPphotos_UpdateProfilePhoto(\n\t\t\tMTP_flags(0),\n\t\t\tMTPInputUser(), // bot\n\t\t\tphoto->mtpInput()\n\t\t)).done([=](const MTPphotos_Photo &result) {\n\t\t\tconst auto newPhoto = _session->data().processPhoto(\n\t\t\t\tresult.data().vphoto());\n\t\t\t_session->data().processUsers(result.data().vusers());\n\t\t\t_session->storage().replace(Storage::UserPhotosReplace(\n\t\t\t\tpeerToUser(peerId),\n\t\t\t\tphotoId,\n\t\t\t\tnewPhoto->id));\n\t\t}).send();\n\t} else {\n\t\tconst auto applier = [=](const MTPUpdates &result) {\n\t\t\t_session->updates().applyUpdates(result);\n\t\t};\n\t\tif (const auto chat = peer->asChat()) {\n\t\t\t_api.request(MTPmessages_EditChatPhoto(\n\t\t\t\tchat->inputChat(),\n\t\t\t\tMTP_inputChatPhoto(photo->mtpInput())\n\t\t\t)).done(applier).send();\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\t_api.request(MTPchannels_EditPhoto(\n\t\t\t\tchannel->inputChannel(),\n\t\t\t\tMTP_inputChatPhoto(photo->mtpInput())\n\t\t\t)).done(applier).send();\n\t\t}\n\t}\n}\n\nvoid PeerPhoto::ready(\n\t\tconst FullMsgId &msgId,\n\t\tstd::optional<MTPInputFile> file,\n\t\tstd::optional<MTPVideoSize> videoSize) {\n\tconst auto maybeUploadValue = _uploads.take(msgId);\n\tif (!maybeUploadValue) {\n\t\treturn;\n\t}\n\tconst auto peer = maybeUploadValue->peer;\n\tconst auto type = maybeUploadValue->type;\n\tconst auto done = maybeUploadValue->done;\n\tconst auto finish = [=] {\n\t\t_uploadDone.fire_copy(peer);\n\t\tif (done) {\n\t\t\tdone();\n\t\t}\n\t};\n\tconst auto fail = [=](const MTP::Error &error) {\n\t\t_uploadFailed.fire_copy(peer);\n\t};\n\tconst auto applier = [=](const MTPUpdates &result) {\n\t\t_session->updates().applyUpdates(result);\n\t\tfinish();\n\t};\n\tconst auto botUserInput = [&] {\n\t\tconst auto user = peer->asUser();\n\t\treturn (user && user->botInfo && user->botInfo->canEditInformation)\n\t\t\t? std::make_optional<MTPInputUser>(user->inputUser())\n\t\t\t: std::nullopt;\n\t}();\n\tif (peer->isSelf() || botUserInput) {\n\t\tusing Flag = MTPphotos_UploadProfilePhoto::Flag;\n\t\tconst auto none = MTPphotos_UploadProfilePhoto::Flags(0);\n\t\t_api.request(MTPphotos_UploadProfilePhoto(\n\t\t\tMTP_flags((file ? Flag::f_file : none)\n\t\t\t\t| (botUserInput ? Flag::f_bot : none)\n\t\t\t\t| (videoSize ? Flag::f_video_emoji_markup : none)\n\t\t\t\t| ((type == UploadType::Fallback) ? Flag::f_fallback : none)),\n\t\t\tbotUserInput ? (*botUserInput) : MTPInputUser(), // bot\n\t\t\tfile ? (*file) : MTPInputFile(),\n\t\t\tMTPInputFile(), // video\n\t\t\tMTPdouble(), // video_start_ts\n\t\t\tvideoSize ? (*videoSize) : MTPVideoSize() // video_emoji_markup\n\t\t)).done([=](const MTPphotos_Photo &result) {\n\t\t\tconst auto photoId = _session->data().processPhoto(\n\t\t\t\tresult.data().vphoto())->id;\n\t\t\t_session->data().processUsers(result.data().vusers());\n\t\t\tif (type == UploadType::Fallback) {\n\t\t\t\t_session->storage().add(Storage::UserPhotosSetBack(\n\t\t\t\t\tpeerToUser(peer->id),\n\t\t\t\t\tphotoId));\n\t\t\t} else {\n\t\t\t\t_session->storage().add(Storage::UserPhotosAddNew(\n\t\t\t\t\tpeerToUser(peer->id),\n\t\t\t\t\tphotoId));\n\t\t\t}\n\t\t\tfinish();\n\t\t}).fail(fail).send();\n\t} else if (const auto chat = peer->asChat()) {\n\t\tconst auto history = _session->data().history(chat);\n\t\tusing Flag = MTPDinputChatUploadedPhoto::Flag;\n\t\tconst auto none = MTPDinputChatUploadedPhoto::Flags(0);\n\t\thistory->sendRequestId = _api.request(MTPmessages_EditChatPhoto(\n\t\t\tchat->inputChat(),\n\t\t\tMTP_inputChatUploadedPhoto(\n\t\t\t\tMTP_flags((file ? Flag::f_file : none)\n\t\t\t\t\t| (videoSize ? Flag::f_video_emoji_markup : none)),\n\t\t\t\tfile ? (*file) : MTPInputFile(),\n\t\t\t\tMTPInputFile(), // video\n\t\t\t\tMTPdouble(), // video_start_ts\n\t\t\t\tvideoSize ? (*videoSize) : MTPVideoSize()) // video_emoji_markup\n\t\t)).done(applier).fail(fail).afterRequest(history->sendRequestId).send();\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tusing Flag = MTPDinputChatUploadedPhoto::Flag;\n\t\tconst auto none = MTPDinputChatUploadedPhoto::Flags(0);\n\t\tconst auto history = _session->data().history(channel);\n\t\thistory->sendRequestId = _api.request(MTPchannels_EditPhoto(\n\t\t\tchannel->inputChannel(),\n\t\t\tMTP_inputChatUploadedPhoto(\n\t\t\t\tMTP_flags((file ? Flag::f_file : none)\n\t\t\t\t\t| (videoSize ? Flag::f_video_emoji_markup : none)),\n\t\t\t\tfile ? (*file) : MTPInputFile(),\n\t\t\t\tMTPInputFile(), // video\n\t\t\t\tMTPdouble(), // video_start_ts\n\t\t\t\tvideoSize ? (*videoSize) : MTPVideoSize()) // video_emoji_markup\n\t\t)).done(applier).fail(fail).afterRequest(history->sendRequestId).send();\n\t} else if (const auto user = peer->asUser()) {\n\t\tusing Flag = MTPphotos_UploadContactProfilePhoto::Flag;\n\t\tconst auto none = MTPphotos_UploadContactProfilePhoto::Flags(0);\n\t\t_api.request(MTPphotos_UploadContactProfilePhoto(\n\t\t\tMTP_flags((file ? Flag::f_file : none)\n\t\t\t\t| (videoSize ? Flag::f_video_emoji_markup : none)\n\t\t\t\t| ((type == UploadType::Suggestion)\n\t\t\t\t\t? Flag::f_suggest\n\t\t\t\t\t: Flag::f_save)),\n\t\t\tuser->inputUser(),\n\t\t\tfile ? (*file) : MTPInputFile(),\n\t\t\tMTPInputFile(), // video\n\t\t\tMTPdouble(), // video_start_ts\n\t\t\tvideoSize ? (*videoSize) : MTPVideoSize() // video_emoji_markup\n\t\t)).done([=](const MTPphotos_Photo &result) {\n\t\t\tresult.match([&](const MTPDphotos_photo &data) {\n\t\t\t\t_session->data().processPhoto(data.vphoto());\n\t\t\t\t_session->data().processUsers(data.vusers());\n\t\t\t});\n\t\t\tif (type != UploadType::Suggestion) {\n\t\t\t\tuser->updateFullForced();\n\t\t\t}\n\t\t\tfinish();\n\t\t}).fail(fail).send();\n\t}\n}\n\nvoid PeerPhoto::requestUserPhotos(\n\t\tnot_null<UserData*> user,\n\t\tUserPhotoId afterId) {\n\tif (_userPhotosRequests.contains(user)) {\n\t\treturn;\n\t}\n\n\tconst auto requestId = _api.request(MTPphotos_GetUserPhotos(\n\t\tuser->inputUser(),\n\t\tMTP_int(0),\n\t\tMTP_long(afterId),\n\t\tMTP_int(kSharedMediaLimit)\n\t)).done([this, user](const MTPphotos_Photos &result) {\n\t\t_userPhotosRequests.remove(user);\n\n\t\tauto fullCount = result.match([](const MTPDphotos_photos &d) {\n\t\t\treturn int(d.vphotos().v.size());\n\t\t}, [](const MTPDphotos_photosSlice &d) {\n\t\t\treturn d.vcount().v;\n\t\t});\n\n\t\tauto &owner = _session->data();\n\t\tauto photoIds = result.match([&](const auto &data) {\n\t\t\towner.processUsers(data.vusers());\n\n\t\t\tauto photoIds = std::vector<PhotoId>();\n\t\t\tphotoIds.reserve(data.vphotos().v.size());\n\n\t\t\tfor (const auto &single : data.vphotos().v) {\n\t\t\t\tconst auto photo = owner.processPhoto(single);\n\t\t\t\tif (!photo->isNull()) {\n\t\t\t\t\tphotoIds.push_back(photo->id);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn photoIds;\n\t\t});\n\t\tif (!user->userpicPhotoUnknown() && user->hasPersonalPhoto()) {\n\t\t\tconst auto photo = owner.photo(user->userpicPhotoId());\n\t\t\tif (!photo->isNull()) {\n\t\t\t\t++fullCount;\n\t\t\t\tphotoIds.insert(begin(photoIds), photo->id);\n\t\t\t}\n\t\t}\n\n\t\t_session->storage().add(Storage::UserPhotosAddSlice(\n\t\t\tpeerToUser(user->id),\n\t\t\tstd::move(photoIds),\n\t\t\tfullCount\n\t\t));\n\t}).fail([this, user] {\n\t\t_userPhotosRequests.remove(user);\n\t}).send();\n\t_userPhotosRequests.emplace(user, requestId);\n}\n\nauto PeerPhoto::emojiList(EmojiListType type) -> EmojiListData & {\n\tswitch (type) {\n\tcase EmojiListType::Profile: return _profileEmojiList;\n\tcase EmojiListType::Group: return _groupEmojiList;\n\tcase EmojiListType::Background: return _backgroundEmojiList;\n\tcase EmojiListType::NoChannelStatus: return _noChannelStatusEmojiList;\n\t}\n\tUnexpected(\"Type in PeerPhoto::emojiList.\");\n}\n\nauto PeerPhoto::emojiList(EmojiListType type) const\n-> const EmojiListData & {\n\treturn const_cast<PeerPhoto*>(this)->emojiList(type);\n}\n\nvoid PeerPhoto::requestEmojiList(EmojiListType type) {\n\tauto &list = emojiList(type);\n\tif (list.requestId) {\n\t\treturn;\n\t}\n\tconst auto send = [&](auto &&request) {\n\t\treturn _api.request(\n\t\t\tstd::move(request)\n\t\t).done([=](const MTPEmojiList &result) {\n\t\t\tauto &list = emojiList(type);\n\t\t\tlist.requestId = 0;\n\t\t\tresult.match([](const MTPDemojiListNotModified &data) {\n\t\t\t}, [&](const MTPDemojiList &data) {\n\t\t\t\tlist.list = ranges::views::all(\n\t\t\t\t\tdata.vdocument_id().v\n\t\t\t\t) | ranges::views::transform(\n\t\t\t\t\t&MTPlong::v\n\t\t\t\t) | ranges::to_vector;\n\t\t\t});\n\t\t}).fail([=] {\n\t\t\temojiList(type).requestId = 0;\n\t\t}).send();\n\t};\n\tlist.requestId = (type == EmojiListType::Profile)\n\t\t? send(MTPaccount_GetDefaultProfilePhotoEmojis())\n\t\t: (type == EmojiListType::Group)\n\t\t? send(MTPaccount_GetDefaultGroupPhotoEmojis())\n\t\t: (type == EmojiListType::NoChannelStatus)\n\t\t? send(MTPaccount_GetChannelRestrictedStatusEmojis())\n\t\t: send(MTPaccount_GetDefaultBackgroundEmojis());\n}\n\nrpl::producer<PeerPhoto::EmojiList> PeerPhoto::emojiListValue(\n\t\tEmojiListType type) {\n\tauto &list = emojiList(type);\n\tif (list.list.current().empty() && !list.requestId) {\n\t\trequestEmojiList(type);\n\t}\n\treturn list.list.value();\n}\n\n// Non-personal photo in case a personal photo is set.\nvoid PeerPhoto::registerNonPersonalPhoto(\n\t\tnot_null<UserData*> user,\n\t\tnot_null<PhotoData*> photo) {\n\t_nonPersonalPhotos.emplace_or_assign(user, photo);\n}\n\nvoid PeerPhoto::unregisterNonPersonalPhoto(not_null<UserData*> user) {\n\t_nonPersonalPhotos.erase(user);\n}\n\nPhotoData *PeerPhoto::nonPersonalPhoto(\n\t\tnot_null<UserData*> user) const {\n\tconst auto i = _nonPersonalPhotos.find(user);\n\treturn (i != end(_nonPersonalPhotos)) ? i->second.get() : nullptr;\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_peer_photo.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n\nclass ApiWrap;\nclass PeerData;\nclass UserData;\n\nnamespace Data {\nstruct FileOrigin;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nclass PeerPhoto final {\npublic:\n\tusing UserPhotoId = PhotoId;\n\texplicit PeerPhoto(not_null<ApiWrap*> api);\n\n\tenum class EmojiListType {\n\t\tProfile,\n\t\tGroup,\n\t\tBackground,\n\t\tNoChannelStatus,\n\t};\n\n\tstruct UserPhoto {\n\t\tQImage image;\n\t\tDocumentId markupDocumentId = 0;\n\t\tstd::vector<QColor> markupColors;\n\t};\n\n\tstruct UploadProgress {\n\t\tnot_null<PeerData*> peer;\n\t\tfloat64 progress = 0.;\n\t};\n\n\tvoid upload(\n\t\tnot_null<PeerData*> peer,\n\t\tUserPhoto &&photo,\n\t\tFn<void()> done = nullptr);\n\tvoid uploadFallback(not_null<PeerData*> peer, UserPhoto &&photo);\n\tvoid updateSelf(\n\t\tnot_null<PhotoData*> photo,\n\t\tData::FileOrigin origin,\n\t\tFn<void()> done = nullptr);\n\tvoid suggest(not_null<PeerData*> peer, UserPhoto &&photo);\n\tvoid clear(not_null<PhotoData*> photo);\n\tvoid clearPersonal(not_null<UserData*> user);\n\tvoid set(not_null<PeerData*> peer, not_null<PhotoData*> photo);\n\n\tstruct UploadCallbacks {\n\t\tFn<void(float64)> progress;\n\t\tFn<void()> done;\n\t\tFn<void()> failed;\n\t};\n\tvoid subscribeToUpload(\n\t\tnot_null<PeerData*> peer,\n\t\trpl::lifetime &lifetime,\n\t\tUploadCallbacks callbacks);\n\n\t[[nodiscard]] auto uploadProgress() const\n\t\t-> rpl::producer<UploadProgress>;\n\t[[nodiscard]] auto uploadDone() const\n\t\t-> rpl::producer<not_null<PeerData*>>;\n\t[[nodiscard]] auto uploadFailed() const\n\t\t-> rpl::producer<not_null<PeerData*>>;\n\tvoid cancelUpload(not_null<PeerData*> peer);\n\n\tvoid requestUserPhotos(not_null<UserData*> user, UserPhotoId afterId);\n\n\tvoid requestEmojiList(EmojiListType type);\n\tusing EmojiList = std::vector<DocumentId>;\n\t[[nodiscard]] rpl::producer<EmojiList> emojiListValue(EmojiListType type);\n\n\t// Non-personal photo in case a personal photo is set.\n\tvoid registerNonPersonalPhoto(\n\t\tnot_null<UserData*> user,\n\t\tnot_null<PhotoData*> photo);\n\tvoid unregisterNonPersonalPhoto(not_null<UserData*> user);\n\t[[nodiscard]] PhotoData *nonPersonalPhoto(\n\t\tnot_null<UserData*> user) const;\n\nprivate:\n\tenum class UploadType {\n\t\tDefault,\n\t\tSuggestion,\n\t\tFallback,\n\t};\n\tstruct EmojiListData {\n\t\trpl::variable<EmojiList> list;\n\t\tmtpRequestId requestId = 0;\n\t};\n\n\tvoid ready(\n\t\tconst FullMsgId &msgId,\n\t\tstd::optional<MTPInputFile> file,\n\t\tstd::optional<MTPVideoSize> videoSize);\n\tvoid upload(\n\t\tnot_null<PeerData*> peer,\n\t\tUserPhoto &&photo,\n\t\tUploadType type,\n\t\tFn<void()> done);\n\n\t[[nodiscard]] EmojiListData &emojiList(EmojiListType type);\n\t[[nodiscard]] const EmojiListData &emojiList(EmojiListType type) const;\n\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\n\tstruct UploadValue {\n\t\tnot_null<PeerData*> peer;\n\t\tUploadType type = UploadType::Default;\n\t\tFn<void()> done;\n\t\tPhotoId photoId = 0;\n\t};\n\n\tbase::flat_map<FullMsgId, UploadValue> _uploads;\n\trpl::event_stream<UploadProgress> _uploadProgress;\n\trpl::event_stream<not_null<PeerData*>> _uploadDone;\n\trpl::event_stream<not_null<PeerData*>> _uploadFailed;\n\n\tbase::flat_map<not_null<UserData*>, mtpRequestId> _userPhotosRequests;\n\n\tbase::flat_map<\n\t\tnot_null<UserData*>,\n\t\tnot_null<PhotoData*>> _nonPersonalPhotos;\n\n\tEmojiListData _profileEmojiList;\n\tEmojiListData _groupEmojiList;\n\tEmojiListData _backgroundEmojiList;\n\tEmojiListData _noChannelStatusEmojiList;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_peer_search.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_peer_search.h\"\n\n#include \"api/api_single_message_search.h\"\n#include \"apiwrap.h\"\n#include \"data/data_session.h\"\n#include \"dialogs/ui/chat_search_in.h\" // IsHashOrCashtagSearchQuery\n#include \"main/main_session.h\"\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kMinSponsoredQueryLength = 4;\n\n} // namespace\n\nPeerSearch::PeerSearch(not_null<Main::Session*> session, Type type)\n: _session(session)\n, _type(type) {\n}\n\nPeerSearch::~PeerSearch() {\n\tclear();\n}\n\nvoid PeerSearch::request(\n\t\tconst QString &query,\n\t\tFn<void(PeerSearchResult)> callback,\n\t\tRequestType type) {\n\tusing namespace Dialogs;\n\t_query = Api::ConvertPeerSearchQuery(query);\n\t_callback = callback;\n\tif (_query.isEmpty()\n\t\t|| IsHashOrCashtagSearchQuery(_query) != HashOrCashtag::None) {\n\t\tfinish(PeerSearchResult{});\n\t\treturn;\n\t}\n\tauto &cache = _cache[_query];\n\tif (cache.peersReady && cache.sponsoredReady) {\n\t\tfinish(cache.result);\n\t\treturn;\n\t} else if (type == RequestType::CacheOnly) {\n\t\t_callback = nullptr;\n\t\treturn;\n\t} else if (cache.requested) {\n\t\treturn;\n\t}\n\tcache.requested = true;\n\tcache.result.query = _query;\n\tif (_query.size() < kMinSponsoredQueryLength) {\n\t\tcache.sponsoredReady = true;\n\t} else if (_type == Type::WithSponsored) {\n\t\trequestSponsored();\n\t}\n\trequestPeers();\n}\n\nvoid PeerSearch::requestPeers() {\n\tconst auto requestId = _session->api().request(MTPcontacts_Search(\n\t\tMTP_string(_query),\n\t\tMTP_int(SearchPeopleLimit)\n\t)).done([=](const MTPcontacts_Found &result, mtpRequestId requestId) {\n\t\tconst auto &data = result.data();\n\t\t_session->data().processUsers(data.vusers());\n\t\t_session->data().processChats(data.vchats());\n\t\tauto parsed = PeerSearchResult();\n\t\tparsed.my.reserve(data.vmy_results().v.size());\n\t\tfor (const auto &id : data.vmy_results().v) {\n\t\t\tconst auto peerId = peerFromMTP(id);\n\t\t\tparsed.my.push_back(_session->data().peer(peerId));\n\t\t}\n\t\tparsed.peers.reserve(data.vresults().v.size());\n\t\tfor (const auto &id : data.vresults().v) {\n\t\t\tconst auto peerId = peerFromMTP(id);\n\t\t\tparsed.peers.push_back(_session->data().peer(peerId));\n\t\t}\n\t\tfinishPeers(requestId, std::move(parsed));\n\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\tfinishPeers(requestId, PeerSearchResult{});\n\t}).send();\n\t_peerRequests.emplace(requestId, _query);\n}\n\nvoid PeerSearch::requestSponsored() {\n\tconst auto requestId = _session->api().request(\n\t\tMTPcontacts_GetSponsoredPeers(MTP_string(_query))\n\t).done([=](\n\t\t\tconst MTPcontacts_SponsoredPeers &result,\n\t\t\tmtpRequestId requestId) {\n\t\tresult.match([&](const MTPDcontacts_sponsoredPeersEmpty &) {\n\t\t\tfinishSponsored(requestId, PeerSearchResult{});\n\t\t}, [&](const MTPDcontacts_sponsoredPeers &data) {\n\t\t\t_session->data().processUsers(data.vusers());\n\t\t\t_session->data().processChats(data.vchats());\n\t\t\tauto parsed = PeerSearchResult();\n\t\t\tparsed.sponsored.reserve(data.vpeers().v.size());\n\t\t\tfor (const auto &peer : data.vpeers().v) {\n\t\t\t\tconst auto &data = peer.data();\n\t\t\t\tconst auto peerId = peerFromMTP(data.vpeer());\n\t\t\t\tparsed.sponsored.push_back({\n\t\t\t\t\t.peer = _session->data().peer(peerId),\n\t\t\t\t\t.randomId = data.vrandom_id().v,\n\t\t\t\t\t.sponsorInfo = TextWithEntities::Simple(\n\t\t\t\t\t\tqs(data.vsponsor_info().value_or_empty())),\n\t\t\t\t\t.additionalInfo = TextWithEntities::Simple(\n\t\t\t\t\t\tqs(data.vadditional_info().value_or_empty())),\n\t\t\t\t});\n\t\t\t}\n\t\t\tfinishSponsored(requestId, std::move(parsed));\n\t\t});\n\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\tfinishSponsored(requestId, PeerSearchResult{});\n\t}).send();\n\t_sponsoredRequests.emplace(requestId, _query);\n}\n\nvoid PeerSearch::finishPeers(\n\t\tmtpRequestId requestId,\n\t\tPeerSearchResult result) {\n\tconst auto query = _peerRequests.take(requestId);\n\tAssert(query.has_value());\n\n\tauto &cache = _cache[*query];\n\tcache.peersReady = true;\n\tcache.result.my = std::move(result.my);\n\tcache.result.peers = std::move(result.peers);\n\tif (cache.sponsoredReady && _query == *query) {\n\t\tfinish(cache.result);\n\t}\n}\n\nvoid PeerSearch::finishSponsored(\n\t\tmtpRequestId requestId,\n\t\tPeerSearchResult result) {\n\tconst auto query = _sponsoredRequests.take(requestId);\n\tAssert(query.has_value());\n\n\tauto &cache = _cache[*query];\n\tcache.sponsoredReady = true;\n\tcache.result.sponsored = std::move(result.sponsored);\n\tif (cache.peersReady && _query == *query) {\n\t\tfinish(cache.result);\n\t}\n}\n\nvoid PeerSearch::finish(PeerSearchResult result) {\n\tif (const auto onstack = base::take(_callback)) {\n\t\tonstack(std::move(result));\n\t}\n}\n\nvoid PeerSearch::clear() {\n\t_query = QString();\n\t_callback = nullptr;\n\t_cache.clear();\n\tfor (const auto &[requestId, query] : base::take(_peerRequests)) {\n\t\t_session->api().request(requestId).cancel();\n\t}\n\tfor (const auto &[requestId, query] : base::take(_sponsoredRequests)) {\n\t\t_session->api().request(requestId).cancel();\n\t}\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_peer_search.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nstruct SponsoredSearchResult {\n    not_null<PeerData*> peer;\n    QByteArray randomId;\n    TextWithEntities sponsorInfo;\n    TextWithEntities additionalInfo;\n};\n\nstruct PeerSearchResult {\n    QString query;\n    std::vector<not_null<PeerData*>> my;\n    std::vector<not_null<PeerData*>> peers;\n    std::vector<SponsoredSearchResult> sponsored;\n};\n\nclass PeerSearch final {\npublic:\n    enum class Type {\n        WithSponsored,\n        JustPeers,\n    };\n    PeerSearch(not_null<Main::Session*> session, Type type);\n    ~PeerSearch();\n\n    enum class RequestType {\n        CacheOnly,\n        CacheOrRemote,\n    };\n    void request(\n        const QString &query,\n        Fn<void(PeerSearchResult)> callback,\n        RequestType type = RequestType::CacheOrRemote);\n    void clear();\n\nprivate:\n    struct CacheEntry {\n        PeerSearchResult result;\n        bool requested = false;\n        bool peersReady = false;\n        bool sponsoredReady = false;\n    };\n\n    void requestPeers();\n    void requestSponsored();\n\n\tvoid finish(PeerSearchResult result);\n\tvoid finishPeers(mtpRequestId requestId, PeerSearchResult result);\n    void finishSponsored(mtpRequestId requestId, PeerSearchResult result);\n\n    const not_null<Main::Session*> _session;\n    const Type _type;\n\n    QString _query;\n    Fn<void(PeerSearchResult)> _callback;\n\n\tbase::flat_map<QString, CacheEntry> _cache;\n\tbase::flat_map<mtpRequestId, QString> _peerRequests;\n\tbase::flat_map<mtpRequestId, QString> _sponsoredRequests;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_polls.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_polls.h\"\n\n#include \"api/api_common.h\"\n#include \"api/api_text_entities.h\"\n#include \"api/api_updates.h\"\n#include \"apiwrap.h\"\n#include \"base/random.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\" // ShouldSendSilent\n#include \"main/main_session.h\"\n\nnamespace Api {\n\nPolls::Polls(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance()) {\n}\n\nvoid Polls::create(\n\t\tconst PollData &data,\n\t\tconst TextWithEntities &text,\n\t\tSendAction action,\n\t\tFn<void()> done,\n\t\tFn<void(bool fileReferenceExpired)> fail) {\n\t_session->api().sendAction(action);\n\n\tconst auto history = action.history;\n\tconst auto peer = history->peer;\n\tconst auto topicRootId = action.replyTo.messageId\n\t\t? action.replyTo.topicRootId\n\t\t: 0;\n\tconst auto monoforumPeerId = action.replyTo.monoforumPeerId;\n\tauto sendFlags = MTPmessages_SendMedia::Flags(0);\n\tif (action.replyTo) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;\n\t}\n\tconst auto clearCloudDraft = action.clearDraft;\n\tif (clearCloudDraft) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_clear_draft;\n\t\thistory->clearLocalDraft(topicRootId, monoforumPeerId);\n\t\thistory->clearCloudDraft(topicRootId, monoforumPeerId);\n\t\thistory->startSavingCloudDraft(topicRootId, monoforumPeerId);\n\t}\n\tconst auto silentPost = ShouldSendSilent(peer, action.options);\n\tconst auto starsPaid = std::min(\n\t\tpeer->starsPerMessageChecked(),\n\t\taction.options.starsApproved);\n\tif (silentPost) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_silent;\n\t}\n\tif (action.options.scheduled) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;\n\t\tif (action.options.scheduleRepeatPeriod) {\n\t\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;\n\t\t}\n\t}\n\tif (action.options.shortcutId) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;\n\t}\n\tif (action.options.effectId) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_effect;\n\t}\n\tif (action.options.suggest) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;\n\t}\n\tif (starsPaid) {\n\t\taction.options.starsApproved -= starsPaid;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;\n\t}\n\tconst auto sendAs = action.options.sendAs;\n\tif (sendAs) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_send_as;\n\t}\n\tauto sentEntities = Api::EntitiesToMTP(\n\t\t_session,\n\t\ttext.entities,\n\t\tApi::ConvertOption::SkipLocal);\n\tif (!sentEntities.v.isEmpty()) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_entities;\n\t}\n\tauto &histories = history->owner().histories();\n\tconst auto randomId = base::RandomValue<uint64>();\n\thistories.sendPreparedMessage(\n\t\thistory,\n\t\taction.replyTo,\n\t\trandomId,\n\t\tData::Histories::PrepareMessage<MTPmessages_SendMedia>(\n\t\t\tMTP_flags(sendFlags),\n\t\t\tpeer->input(),\n\t\t\tData::Histories::ReplyToPlaceholder(),\n\t\t\tPollDataToInputMedia(&data),\n\t\t\tMTP_string(text.text),\n\t\t\tMTP_long(randomId),\n\t\t\tMTPReplyMarkup(),\n\t\t\tsentEntities,\n\t\t\tMTP_int(action.options.scheduled),\n\t\t\tMTP_int(action.options.scheduleRepeatPeriod),\n\t\t\t(sendAs ? sendAs->input() : MTP_inputPeerEmpty()),\n\t\t\tData::ShortcutIdToMTP(_session, action.options.shortcutId),\n\t\t\tMTP_long(action.options.effectId),\n\t\t\tMTP_long(starsPaid),\n\t\t\tSuggestToMTP(action.options.suggest)\n\t\t), [=](const MTPUpdates &result, const MTP::Response &response) {\n\t\tif (clearCloudDraft) {\n\t\t\thistory->finishSavingCloudDraft(\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tUnixtimeFromMsgId(response.outerMsgId));\n\t\t}\n\t\t_session->changes().historyUpdated(\n\t\t\thistory,\n\t\t\t(action.options.scheduled\n\t\t\t\t? Data::HistoryUpdate::Flag::ScheduledSent\n\t\t\t\t: Data::HistoryUpdate::Flag::MessageSent));\n\t\tdone();\n\t}, [=](const MTP::Error &error, const MTP::Response &response) {\n\t\tif (clearCloudDraft) {\n\t\t\thistory->finishSavingCloudDraft(\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tUnixtimeFromMsgId(response.outerMsgId));\n\t\t}\n\t\tconst auto expired = (error.code() == 400)\n\t\t\t&& error.type().startsWith(u\"FILE_REFERENCE_\"_q);\n\t\tfail(expired);\n\t});\n}\n\nvoid Polls::sendVotes(\n\t\tFullMsgId itemId,\n\t\tconst std::vector<QByteArray> &options) {\n\tif (_pollVotesRequestIds.contains(itemId)) {\n\t\treturn;\n\t}\n\tconst auto item = _session->data().message(itemId);\n\tconst auto media = item ? item->media() : nullptr;\n\tconst auto poll = media ? media->poll() : nullptr;\n\tif (!item) {\n\t\treturn;\n\t}\n\n\tconst auto showSending = poll && !options.empty();\n\tconst auto hideSending = [=] {\n\t\tif (showSending) {\n\t\t\tif (const auto item = _session->data().message(itemId)) {\n\t\t\t\tpoll->sendingVotes.clear();\n\t\t\t\t_session->data().requestItemRepaint(item);\n\t\t\t}\n\t\t}\n\t};\n\tif (showSending) {\n\t\tpoll->sendingVotes = options;\n\t\t_session->data().requestItemRepaint(item);\n\t}\n\n\tauto prepared = QVector<MTPbytes>();\n\tprepared.reserve(options.size());\n\tranges::transform(\n\t\toptions,\n\t\tranges::back_inserter(prepared),\n\t\t[](const QByteArray &option) { return MTP_bytes(option); });\n\tconst auto requestId = _api.request(MTPmessages_SendVote(\n\t\titem->history()->peer->input(),\n\t\tMTP_int(item->id),\n\t\tMTP_vector<MTPbytes>(prepared)\n\t)).done([=](const MTPUpdates &result) {\n\t\t_pollVotesRequestIds.erase(itemId);\n\t\thideSending();\n\t\t_session->updates().applyUpdates(result);\n\t}).fail([=] {\n\t\t_pollVotesRequestIds.erase(itemId);\n\t\thideSending();\n\t}).send();\n\t_pollVotesRequestIds.emplace(itemId, requestId);\n}\n\nvoid Polls::addAnswer(\n\t\tFullMsgId itemId,\n\t\tconst TextWithEntities &text,\n\t\tconst PollMedia &media,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail) {\n\tif (_pollAddAnswerRequestIds.contains(itemId)) {\n\t\treturn;\n\t}\n\tconst auto item = _session->data().message(itemId);\n\tif (!item) {\n\t\treturn;\n\t}\n\tconst auto sentEntities = Api::EntitiesToMTP(\n\t\t_session,\n\t\ttext.entities,\n\t\tApi::ConvertOption::SkipLocal);\n\tusing Flag = MTPDinputPollAnswer::Flag;\n\tconst auto flags = media\n\t\t? Flag::f_media\n\t\t: Flag();\n\tconst auto answer = MTP_inputPollAnswer(\n\t\tMTP_flags(flags),\n\t\tMTP_textWithEntities(\n\t\t\tMTP_string(text.text),\n\t\t\tsentEntities),\n\t\tmedia ? PollMediaToMTP(media) : MTPInputMedia());\n\tconst auto requestId = _api.request(MTPmessages_AddPollAnswer(\n\t\titem->history()->peer->input(),\n\t\tMTP_int(item->id),\n\t\tanswer\n\t)).done([=](const MTPUpdates &result) {\n\t\t_pollAddAnswerRequestIds.erase(itemId);\n\t\t_session->updates().applyUpdates(result);\n\t\tif (done) {\n\t\t\tdone();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\t_pollAddAnswerRequestIds.erase(itemId);\n\t\tif (fail) {\n\t\t\tfail(error.type());\n\t\t}\n\t}).send();\n\t_pollAddAnswerRequestIds.emplace(itemId, requestId);\n}\n\nvoid Polls::deleteAnswer(FullMsgId itemId, const QByteArray &option) {\n\tif (_pollVotesRequestIds.contains(itemId)) {\n\t\treturn;\n\t}\n\tconst auto item = _session->data().message(itemId);\n\tif (!item) {\n\t\treturn;\n\t}\n\tconst auto requestId = _api.request(MTPmessages_DeletePollAnswer(\n\t\titem->history()->peer->input(),\n\t\tMTP_int(item->id),\n\t\tMTP_bytes(option)\n\t)).done([=](const MTPUpdates &result) {\n\t\t_pollVotesRequestIds.erase(itemId);\n\t\t_session->updates().applyUpdates(result);\n\t}).fail([=] {\n\t\t_pollVotesRequestIds.erase(itemId);\n\t}).send();\n\t_pollVotesRequestIds.emplace(itemId, requestId);\n}\n\nvoid Polls::close(not_null<HistoryItem*> item) {\n\tconst auto itemId = item->fullId();\n\tif (_pollCloseRequestIds.contains(itemId)) {\n\t\treturn;\n\t}\n\tconst auto media = item ? item->media() : nullptr;\n\tconst auto poll = media ? media->poll() : nullptr;\n\tif (!poll) {\n\t\treturn;\n\t}\n\tconst auto requestId = _api.request(MTPmessages_EditMessage(\n\t\tMTP_flags(MTPmessages_EditMessage::Flag::f_media),\n\t\titem->history()->peer->input(),\n\t\tMTP_int(item->id),\n\t\tMTPstring(),\n\t\tPollDataToInputMedia(poll, true),\n\t\tMTPReplyMarkup(),\n\t\tMTPVector<MTPMessageEntity>(),\n\t\tMTP_int(0), // schedule_date\n\t\tMTP_int(0), // schedule_repeat_period\n\t\tMTPint() // quick_reply_shortcut_id\n\t)).done([=](const MTPUpdates &result) {\n\t\t_pollCloseRequestIds.erase(itemId);\n\t\t_session->updates().applyUpdates(result);\n\t}).fail([=] {\n\t\t_pollCloseRequestIds.erase(itemId);\n\t}).send();\n\t_pollCloseRequestIds.emplace(itemId, requestId);\n}\n\nvoid Polls::reloadResults(not_null<HistoryItem*> item) {\n\tconst auto itemId = item->fullId();\n\tif (!item->isRegular() || _pollReloadRequestIds.contains(itemId)) {\n\t\treturn;\n\t}\n\tconst auto media = item->media();\n\tconst auto poll = media ? media->poll() : nullptr;\n\tconst auto pollHash = poll ? poll->hash : uint64(0);\n\tconst auto requestId = _api.request(MTPmessages_GetPollResults(\n\t\titem->history()->peer->input(),\n\t\tMTP_int(item->id),\n\t\tMTP_long(pollHash)\n\t)).done([=](const MTPUpdates &result) {\n\t\t_pollReloadRequestIds.erase(itemId);\n\t\t_session->updates().applyUpdates(result);\n\t}).fail([=] {\n\t\t_pollReloadRequestIds.erase(itemId);\n\t}).send();\n\t_pollReloadRequestIds.emplace(itemId, requestId);\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_polls.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n#include \"ui/text/text_entity.h\"\n\nclass ApiWrap;\nclass HistoryItem;\nstruct PollData;\nstruct PollMedia;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nstruct SendAction;\n\nclass Polls final {\npublic:\n\texplicit Polls(not_null<ApiWrap*> api);\n\n\tvoid create(\n\t\tconst PollData &data,\n\t\tconst TextWithEntities &text,\n\t\tSendAction action,\n\t\tFn<void()> done,\n\t\tFn<void(bool fileReferenceExpired)> fail);\n\tvoid sendVotes(\n\t\tFullMsgId itemId,\n\t\tconst std::vector<QByteArray> &options);\n\tvoid addAnswer(\n\t\tFullMsgId itemId,\n\t\tconst TextWithEntities &text,\n\t\tconst PollMedia &media,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail);\n\tvoid deleteAnswer(FullMsgId itemId, const QByteArray &option);\n\tvoid close(not_null<HistoryItem*> item);\n\tvoid reloadResults(not_null<HistoryItem*> item);\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\n\tbase::flat_map<FullMsgId, mtpRequestId> _pollVotesRequestIds;\n\tbase::flat_map<FullMsgId, mtpRequestId> _pollAddAnswerRequestIds;\n\tbase::flat_map<FullMsgId, mtpRequestId> _pollCloseRequestIds;\n\tbase::flat_map<FullMsgId, mtpRequestId> _pollReloadRequestIds;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_premium.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_premium.h\"\n\n#include \"api/api_premium_option.h\"\n#include \"api/api_text_entities.h\"\n#include \"apiwrap.h\"\n#include \"base/random.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"payments/payments_form.h\"\n#include \"ui/chat/chat_style.h\" // ColorCollectible\n#include \"ui/text/format_values.h\"\n\nnamespace Api {\nnamespace {\n\n[[nodiscard]] GiftCode Parse(const MTPDpayments_checkedGiftCode &data) {\n\treturn {\n\t\t.from = data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(),\n\t\t.to = data.vto_id() ? peerFromUser(*data.vto_id()) : PeerId(),\n\t\t.giveawayId = data.vgiveaway_msg_id().value_or_empty(),\n\t\t.date = data.vdate().v,\n\t\t.used = data.vused_date().value_or_empty(),\n\t\t.days = data.vdays().v,\n\t\t.giveaway = data.is_via_giveaway(),\n\t};\n}\n\n[[nodiscard]] Data::PremiumSubscriptionOptions GiftCodesFromTL(\n\t\tconst QVector<MTPPremiumGiftCodeOption> &tlOptions) {\n\tauto options = PremiumSubscriptionOptionsFromTL(tlOptions);\n\tfor (auto i = 0; i < options.size(); i++) {\n\t\tconst auto &tlOption = tlOptions[i].data();\n\t\tconst auto currency = qs(tlOption.vcurrency());\n\t\tconst auto perUserText = Ui::FillAmountAndCurrency(\n\t\t\ttlOption.vamount().v / float64(tlOption.vusers().v),\n\t\t\tcurrency,\n\t\t\tfalse);\n\t\toptions[i].costPerMonth = perUserText\n\t\t\t+ ' '\n\t\t\t+ QChar(0x00D7)\n\t\t\t+ ' '\n\t\t\t+ QString::number(tlOption.vusers().v);\n\t\toptions[i].total = Ui::FillAmountAndCurrency(\n\t\t\ttlOption.vamount().v,\n\t\t\tcurrency);\n\t\toptions[i].currency = currency;\n\t}\n\treturn options;\n}\n\n[[nodiscard]] int FindStarsForResale(const MTPVector<MTPStarsAmount> *list) {\n\tif (!list) {\n\t\treturn 0;\n\t}\n\tfor (const auto &amount : list->v) {\n\t\tif (amount.type() == mtpc_starsAmount) {\n\t\t\treturn int(amount.c_starsAmount().vamount().v);\n\t\t}\n\t}\n\treturn 0;\n}\n\n[[nodiscard]] int64 FindTonForResale(const MTPVector<MTPStarsAmount> *list) {\n\tif (!list) {\n\t\treturn 0;\n\t}\n\tfor (const auto &amount : list->v) {\n\t\tif (amount.type() == mtpc_starsTonAmount) {\n\t\t\treturn int64(amount.c_starsTonAmount().vamount().v);\n\t\t}\n\t}\n\treturn 0;\n}\n\n} // namespace\n\nPremium::Premium(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance()) {\n\tcrl::on_main(_session, [=] {\n\t\t// You can't use _session->user() in the constructor,\n\t\t// only queued, because it is not constructed yet.\n\t\tData::AmPremiumValue(\n\t\t\t_session\n\t\t) | rpl::on_next([=] {\n\t\t\treload();\n\t\t\tif (_session->premium()) {\n\t\t\t\treloadCloudSet();\n\t\t\t}\n\t\t}, _session->lifetime());\n\t});\n}\n\nrpl::producer<TextWithEntities> Premium::statusTextValue() const {\n\treturn _statusTextUpdates.events_starting_with_copy(\n\t\t_statusText.value_or(TextWithEntities()));\n}\n\nauto Premium::videos() const\n-> const base::flat_map<QString, not_null<DocumentData*>> & {\n\treturn _videos;\n}\n\nrpl::producer<> Premium::videosUpdated() const {\n\treturn _videosUpdated.events();\n}\n\nauto Premium::stickers() const\n-> const std::vector<not_null<DocumentData*>> & {\n\treturn _stickers;\n}\n\nrpl::producer<> Premium::stickersUpdated() const {\n\treturn _stickersUpdated.events();\n}\n\nauto Premium::cloudSet() const\n-> const std::vector<not_null<DocumentData*>> & {\n\treturn _cloudSet;\n}\n\nrpl::producer<> Premium::cloudSetUpdated() const {\n\treturn _cloudSetUpdated.events();\n}\n\nauto Premium::helloStickers() const\n-> const std::vector<not_null<DocumentData*>> & {\n\tif (_helloStickers.empty()) {\n\t\tconst_cast<Premium*>(this)->reloadHelloStickers();\n\t}\n\treturn _helloStickers;\n}\n\nrpl::producer<> Premium::helloStickersUpdated() const {\n\treturn _helloStickersUpdated.events();\n}\n\nint64 Premium::monthlyAmount() const {\n\treturn _monthlyAmount;\n}\n\nQString Premium::monthlyCurrency() const {\n\treturn _monthlyCurrency;\n}\n\nvoid Premium::reload() {\n\treloadPromo();\n\treloadStickers();\n}\n\nvoid Premium::reloadPromo() {\n\tif (_promoRequestId) {\n\t\treturn;\n\t}\n\t_promoRequestId = _api.request(MTPhelp_GetPremiumPromo(\n\t)).done([=](const MTPhelp_PremiumPromo &result) {\n\t\t_promoRequestId = 0;\n\t\tconst auto &data = result.data();\n\t\t_session->data().processUsers(data.vusers());\n\n\t\t_subscriptionOptions = PremiumSubscriptionOptionsFromTL(\n\t\t\tdata.vperiod_options().v);\n\t\tfor (const auto &option : data.vperiod_options().v) {\n\t\t\tif (option.data().vmonths().v == 1) {\n\t\t\t\t_monthlyAmount = option.data().vamount().v;\n\t\t\t\t_monthlyCurrency = qs(option.data().vcurrency());\n\t\t\t}\n\t\t}\n\t\tauto text = TextWithEntities{\n\t\t\tqs(data.vstatus_text()),\n\t\t\tEntitiesFromMTP(_session, data.vstatus_entities().v),\n\t\t};\n\t\t_statusText = text;\n\t\t_statusTextUpdates.fire(std::move(text));\n\t\tauto videos = base::flat_map<QString, not_null<DocumentData*>>();\n\t\tconst auto count = int(std::min(\n\t\t\tdata.vvideo_sections().v.size(),\n\t\t\tdata.vvideos().v.size()));\n\t\tvideos.reserve(count);\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tconst auto document = _session->data().processDocument(\n\t\t\t\tdata.vvideos().v[i]);\n\t\t\tif ((!document->isVideoFile() && !document->isGifv())\n\t\t\t\t|| !document->supportsStreaming()) {\n\t\t\t\tdocument->forceIsStreamedAnimation();\n\t\t\t}\n\t\t\tvideos.emplace(\n\t\t\t\tqs(data.vvideo_sections().v[i]),\n\t\t\t\tdocument);\n\t\t}\n\t\tif (_videos != videos) {\n\t\t\t_videos = std::move(videos);\n\t\t\t_videosUpdated.fire({});\n\t\t}\n\t}).fail([=] {\n\t\t_promoRequestId = 0;\n\t}).send();\n}\n\nvoid Premium::reloadStickers() {\n\tif (_stickersRequestId) {\n\t\treturn;\n\t}\n\t_stickersRequestId = _api.request(MTPmessages_GetStickers(\n\t\tMTP_string(\"\\xe2\\xad\\x90\\xef\\xb8\\x8f\\xe2\\xad\\x90\\xef\\xb8\\x8f\"),\n\t\tMTP_long(_stickersHash)\n\t)).done([=](const MTPmessages_Stickers &result) {\n\t\t_stickersRequestId = 0;\n\t\tresult.match([&](const MTPDmessages_stickersNotModified &) {\n\t\t}, [&](const MTPDmessages_stickers &data) {\n\t\t\t_stickersHash = data.vhash().v;\n\t\t\tconst auto owner = &_session->data();\n\t\t\t_stickers.clear();\n\t\t\tfor (const auto &sticker : data.vstickers().v) {\n\t\t\t\tconst auto document = owner->processDocument(sticker);\n\t\t\t\tif (document->isPremiumSticker()) {\n\t\t\t\t\t_stickers.push_back(document);\n\t\t\t\t}\n\t\t\t}\n\t\t\t_stickersUpdated.fire({});\n\t\t});\n\t}).fail([=] {\n\t\t_stickersRequestId = 0;\n\t}).send();\n}\n\nvoid Premium::reloadCloudSet() {\n\tif (_cloudSetRequestId) {\n\t\treturn;\n\t}\n\t_cloudSetRequestId = _api.request(MTPmessages_GetStickers(\n\t\tMTP_string(\"\\xf0\\x9f\\x93\\x82\\xe2\\xad\\x90\\xef\\xb8\\x8f\"),\n\t\tMTP_long(_cloudSetHash)\n\t)).done([=](const MTPmessages_Stickers &result) {\n\t\t_cloudSetRequestId = 0;\n\t\tresult.match([&](const MTPDmessages_stickersNotModified &) {\n\t\t}, [&](const MTPDmessages_stickers &data) {\n\t\t\t_cloudSetHash = data.vhash().v;\n\t\t\tconst auto owner = &_session->data();\n\t\t\t_cloudSet.clear();\n\t\t\tfor (const auto &sticker : data.vstickers().v) {\n\t\t\t\tconst auto document = owner->processDocument(sticker);\n\t\t\t\tif (document->isPremiumSticker()) {\n\t\t\t\t\t_cloudSet.push_back(document);\n\t\t\t\t}\n\t\t\t}\n\t\t\t_cloudSetUpdated.fire({});\n\t\t});\n\t}).fail([=] {\n\t\t_cloudSetRequestId = 0;\n\t}).send();\n}\n\nvoid Premium::reloadHelloStickers() {\n\tif (_helloStickersRequestId) {\n\t\treturn;\n\t}\n\t_helloStickersRequestId = _api.request(MTPmessages_GetStickers(\n\t\tMTP_string(\"\\xf0\\x9f\\x91\\x8b\\xe2\\xad\\x90\\xef\\xb8\\x8f\"),\n\t\tMTP_long(_helloStickersHash)\n\t)).done([=](const MTPmessages_Stickers &result) {\n\t\t_helloStickersRequestId = 0;\n\t\tresult.match([&](const MTPDmessages_stickersNotModified &) {\n\t\t}, [&](const MTPDmessages_stickers &data) {\n\t\t\t_helloStickersHash = data.vhash().v;\n\t\t\tconst auto owner = &_session->data();\n\t\t\t_helloStickers.clear();\n\t\t\tfor (const auto &sticker : data.vstickers().v) {\n\t\t\t\tconst auto document = owner->processDocument(sticker);\n\t\t\t\tif (document->sticker()) {\n\t\t\t\t\t_helloStickers.push_back(document);\n\t\t\t\t}\n\t\t\t}\n\t\t\t_helloStickersUpdated.fire({});\n\t\t});\n\t}).fail([=] {\n\t\t_helloStickersRequestId = 0;\n\t}).send();\n}\n\nvoid Premium::checkGiftCode(\n\t\tconst QString &slug,\n\t\tFn<void(GiftCode)> done) {\n\tif (_giftCodeRequestId) {\n\t\tif (_giftCodeSlug == slug) {\n\t\t\treturn;\n\t\t}\n\t\t_api.request(_giftCodeRequestId).cancel();\n\t}\n\t_giftCodeSlug = slug;\n\t_giftCodeRequestId = _api.request(MTPpayments_CheckGiftCode(\n\t\tMTP_string(slug)\n\t)).done([=](const MTPpayments_CheckedGiftCode &result) {\n\t\t_giftCodeRequestId = 0;\n\n\t\tconst auto &data = result.data();\n\t\t_session->data().processUsers(data.vusers());\n\t\t_session->data().processChats(data.vchats());\n\t\tdone(updateGiftCode(slug, Parse(data)));\n\t}).fail([=](const MTP::Error &error) {\n\t\t_giftCodeRequestId = 0;\n\n\t\tdone(updateGiftCode(slug, {}));\n\t}).send();\n}\n\nGiftCode Premium::updateGiftCode(\n\t\tconst QString &slug,\n\t\tconst GiftCode &code) {\n\tauto &now = _giftCodes[slug];\n\tif (now != code) {\n\t\tnow = code;\n\t\t_giftCodeUpdated.fire_copy(slug);\n\t}\n\treturn code;\n}\n\nrpl::producer<GiftCode> Premium::giftCodeValue(const QString &slug) const {\n\treturn _giftCodeUpdated.events_starting_with_copy(\n\t\tslug\n\t) | rpl::filter(rpl::mappers::_1 == slug) | rpl::map([=] {\n\t\tconst auto i = _giftCodes.find(slug);\n\t\treturn (i != end(_giftCodes)) ? i->second : GiftCode();\n\t});\n}\n\nvoid Premium::applyGiftCode(const QString &slug, Fn<void(QString)> done) {\n\t_api.request(MTPpayments_ApplyGiftCode(\n\t\tMTP_string(slug)\n\t)).done([=](const MTPUpdates &result) {\n\t\t_session->api().applyUpdates(result);\n\t\tdone({});\n\t}).fail([=](const MTP::Error &error) {\n\t\tdone(error.type());\n\t}).send();\n}\n\nvoid Premium::resolveGiveawayInfo(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId messageId,\n\t\tFn<void(GiveawayInfo)> done) {\n\tExpects(done != nullptr);\n\n\t_giveawayInfoDone = std::move(done);\n\tif (_giveawayInfoRequestId) {\n\t\tif (_giveawayInfoPeer == peer\n\t\t\t&& _giveawayInfoMessageId == messageId) {\n\t\t\treturn;\n\t\t}\n\t\t_api.request(_giveawayInfoRequestId).cancel();\n\t}\n\t_giveawayInfoPeer = peer;\n\t_giveawayInfoMessageId = messageId;\n\t_giveawayInfoRequestId = _api.request(MTPpayments_GetGiveawayInfo(\n\t\t_giveawayInfoPeer->input(),\n\t\tMTP_int(_giveawayInfoMessageId.bare)\n\t)).done([=](const MTPpayments_GiveawayInfo &result) {\n\t\t_giveawayInfoRequestId = 0;\n\n\t\tauto info = GiveawayInfo();\n\t\tresult.match([&](const MTPDpayments_giveawayInfo &data) {\n\t\t\tinfo.participating = data.is_participating();\n\t\t\tinfo.state = data.is_preparing_results()\n\t\t\t\t? GiveawayState::Preparing\n\t\t\t\t: GiveawayState::Running;\n\t\t\tinfo.adminChannelId = data.vadmin_disallowed_chat_id()\n\t\t\t\t? ChannelId(*data.vadmin_disallowed_chat_id())\n\t\t\t\t: ChannelId();\n\t\t\tinfo.disallowedCountry = qs(\n\t\t\t\tdata.vdisallowed_country().value_or_empty());\n\t\t\tinfo.tooEarlyDate\n\t\t\t\t= data.vjoined_too_early_date().value_or_empty();\n\t\t\tinfo.startDate = data.vstart_date().v;\n\t\t}, [&](const MTPDpayments_giveawayInfoResults &data) {\n\t\t\tinfo.state = data.is_refunded()\n\t\t\t\t? GiveawayState::Refunded\n\t\t\t\t: GiveawayState::Finished;\n\t\t\tinfo.giftCode = qs(data.vgift_code_slug().value_or_empty());\n\t\t\tinfo.activatedCount = data.vactivated_count().value_or_empty();\n\t\t\tinfo.finishDate = data.vfinish_date().v;\n\t\t\tinfo.startDate = data.vstart_date().v;\n\t\t\tinfo.credits = data.vstars_prize().value_or_empty();\n\t\t});\n\t\t_giveawayInfoDone(std::move(info));\n\t}).fail([=] {\n\t\t_giveawayInfoRequestId = 0;\n\t\t_giveawayInfoDone({});\n\t}).send();\n}\n\nconst Data::PremiumSubscriptionOptions &Premium::subscriptionOptions() const {\n\treturn _subscriptionOptions;\n}\n\nrpl::producer<> Premium::someMessageMoneyRestrictionsResolved() const {\n\treturn _someMessageMoneyRestrictionsResolved.events();\n}\n\nvoid Premium::resolveMessageMoneyRestrictions(not_null<UserData*> user) {\n\t_resolveMessageMoneyRequiredUsers.emplace(user);\n\tif (!_messageMoneyRequestScheduled\n\t\t&& _resolveMessageMoneyRequestedUsers.empty()) {\n\t\t_messageMoneyRequestScheduled = true;\n\t\tcrl::on_main(_session, [=] {\n\t\t\trequestPremiumRequiredSlice();\n\t\t});\n\t}\n}\n\nvoid Premium::requestPremiumRequiredSlice() {\n\t_messageMoneyRequestScheduled = false;\n\tif (!_resolveMessageMoneyRequestedUsers.empty()\n\t\t|| _resolveMessageMoneyRequiredUsers.empty()) {\n\t\treturn;\n\t}\n\tconstexpr auto kPerRequest = 100;\n\tauto users = MTP_vector_from_range(_resolveMessageMoneyRequiredUsers\n\t\t| ranges::views::transform(&UserData::inputUser));\n\tif (users.v.size() > kPerRequest) {\n\t\tauto shortened = users.v;\n\t\tshortened.resize(kPerRequest);\n\t\tusers = MTP_vector<MTPInputUser>(std::move(shortened));\n\t\tconst auto from = begin(_resolveMessageMoneyRequiredUsers);\n\t\t_resolveMessageMoneyRequestedUsers = { from, from + kPerRequest };\n\t\t_resolveMessageMoneyRequiredUsers.erase(from, from + kPerRequest);\n\t} else {\n\t\t_resolveMessageMoneyRequestedUsers\n\t\t\t= base::take(_resolveMessageMoneyRequiredUsers);\n\t}\n\tconst auto finish = [=](const QVector<MTPRequirementToContact> &list) {\n\n\t\tauto index = 0;\n\t\tfor (const auto &user : base::take(_resolveMessageMoneyRequestedUsers)) {\n\t\t\tconst auto set = [&](bool requirePremium, int stars) {\n\t\t\t\tusing Flag = UserDataFlag;\n\t\t\t\tconstexpr auto me = Flag::RequiresPremiumToWrite;\n\t\t\t\tconstexpr auto known = Flag::MessageMoneyRestrictionsKnown;\n\t\t\t\tconstexpr auto hasPrem = Flag::HasRequirePremiumToWrite;\n\t\t\t\tconstexpr auto hasStars = Flag::HasStarsPerMessage;\n\t\t\t\tuser->setStarsPerMessage(stars);\n\t\t\t\tuser->setFlags((user->flags() & ~me)\n\t\t\t\t\t| known\n\t\t\t\t\t| (requirePremium ? (me | hasPrem) : Flag())\n\t\t\t\t\t| (stars ? hasStars : Flag()));\n\t\t\t};\n\t\t\tif (index >= list.size()) {\n\t\t\t\tset(false, 0);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tlist[index++].match([&](const MTPDrequirementToContactEmpty &) {\n\t\t\t\tset(false, 0);\n\t\t\t}, [&](const MTPDrequirementToContactPremium &) {\n\t\t\t\tset(true, 0);\n\t\t\t}, [&](const MTPDrequirementToContactPaidMessages &data) {\n\t\t\t\tset(false, data.vstars_amount().v);\n\t\t\t});\n\t\t}\n\t\tif (!_messageMoneyRequestScheduled\n\t\t\t&& !_resolveMessageMoneyRequiredUsers.empty()) {\n\t\t\t_messageMoneyRequestScheduled = true;\n\t\t\tcrl::on_main(_session, [=] {\n\t\t\t\trequestPremiumRequiredSlice();\n\t\t\t});\n\t\t}\n\t\t_someMessageMoneyRestrictionsResolved.fire({});\n\t};\n\t_session->api().request(\n\t\tMTPusers_GetRequirementsToContact(std::move(users))\n\t).done([=](const MTPVector<MTPRequirementToContact> &result) {\n\t\tfinish(result.v);\n\t}).fail([=] {\n\t\tfinish({});\n\t}).send();\n}\n\nPremiumGiftCodeOptions::PremiumGiftCodeOptions(not_null<PeerData*> peer)\n: _peer(peer)\n, _api(&peer->session().api().instance()) {\n}\n\nrpl::producer<rpl::no_value, QString> PremiumGiftCodeOptions::request() {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tusing TLOption = MTPPremiumGiftCodeOption;\n\t\t_api.request(MTPpayments_GetPremiumGiftCodeOptions(\n\t\t\tMTP_flags(_peer->isChannel()\n\t\t\t\t? MTPpayments_GetPremiumGiftCodeOptions::Flag::f_boost_peer\n\t\t\t\t: MTPpayments_GetPremiumGiftCodeOptions::Flag(0)),\n\t\t\t_peer->input()\n\t\t)).done([=](const MTPVector<TLOption> &result) {\n\t\t\tauto tlMapOptions = base::flat_map<Amount, QVector<TLOption>>();\n\t\t\tfor (const auto &tlOption : result.v) {\n\t\t\t\tconst auto &data = tlOption.data();\n\t\t\t\ttlMapOptions[data.vusers().v].push_back(tlOption);\n\t\t\t\tif (qs(data.vcurrency()) == Ui::kCreditsCurrency) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst auto token = Token{ data.vusers().v, data.vmonths().v };\n\t\t\t\t_stores[token] = Store{\n\t\t\t\t\t.amount = data.vamount().v,\n\t\t\t\t\t.currency = qs(data.vcurrency()),\n\t\t\t\t\t.product = qs(data.vstore_product().value_or_empty()),\n\t\t\t\t\t.quantity = data.vstore_quantity().value_or_empty(),\n\t\t\t\t};\n\t\t\t\tif (!ranges::contains(_availablePresets, data.vusers().v)) {\n\t\t\t\t\t_availablePresets.push_back(data.vusers().v);\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const auto &[amount, tlOptions] : tlMapOptions) {\n\t\t\t\tif (amount == 1 && _optionsForOnePerson.currencies.empty()) {\n\t\t\t\t\tfor (const auto &option : tlOptions) {\n\t\t\t\t\t\t_optionsForOnePerson.months.push_back(\n\t\t\t\t\t\t\toption.data().vmonths().v);\n\t\t\t\t\t\t_optionsForOnePerson.totalCosts.push_back(\n\t\t\t\t\t\t\toption.data().vamount().v);\n\t\t\t\t\t\t_optionsForOnePerson.currencies.push_back(\n\t\t\t\t\t\t\tqs(option.data().vcurrency()));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t_subscriptionOptions[amount] = GiftCodesFromTL(tlOptions);\n\t\t\t}\n\t\t\tconsumer.put_done();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).send();\n\n\t\treturn lifetime;\n\t};\n}\n\nrpl::producer<rpl::no_value, QString> PremiumGiftCodeOptions::applyPrepaid(\n\t\tconst Payments::InvoicePremiumGiftCode &invoice,\n\t\tuint64 prepaidId) {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tconst auto channel = _peer->asChannel();\n\t\tif (!channel) {\n\t\t\treturn lifetime;\n\t\t}\n\n\t\t_api.request(MTPpayments_LaunchPrepaidGiveaway(\n\t\t\t_peer->input(),\n\t\t\tMTP_long(prepaidId),\n\t\t\tinvoice.giveawayCredits\n\t\t\t\t? Payments::InvoiceCreditsGiveawayToTL(invoice)\n\t\t\t\t: Payments::InvoicePremiumGiftCodeGiveawayToTL(invoice)\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t_peer->session().api().applyUpdates(result);\n\t\t\tconsumer.put_done();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).send();\n\n\t\treturn lifetime;\n\t};\n}\n\nconst std::vector<int> &PremiumGiftCodeOptions::availablePresets() const {\n\treturn _availablePresets;\n}\n\n[[nodiscard]] int PremiumGiftCodeOptions::monthsFromPreset(int monthsIndex) {\n\tExpects(monthsIndex >= 0 && monthsIndex < _availablePresets.size());\n\n\treturn _optionsForOnePerson.months[monthsIndex];\n}\n\nPayments::InvoicePremiumGiftCode PremiumGiftCodeOptions::invoice(\n\t\tint users,\n\t\tint months) {\n\tconst auto randomId = base::RandomValue<uint64>();\n\tconst auto token = Token{ users, months };\n\tconst auto &store = _stores[token];\n\treturn Payments::InvoicePremiumGiftCode{\n\t\t.currency = store.currency,\n\t\t.storeProduct = store.product,\n\t\t.randomId = randomId,\n\t\t.amount = store.amount,\n\t\t.storeQuantity = store.quantity,\n\t\t.users = token.users,\n\t\t.months = token.months,\n\t};\n}\n\nstd::vector<GiftOptionData> PremiumGiftCodeOptions::optionsForPeer() const {\n\tauto result = std::vector<GiftOptionData>();\n\n\tif (!_optionsForOnePerson.currencies.empty()) {\n\t\tconst auto count = int(_optionsForOnePerson.months.size());\n\t\tresult.reserve(count);\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tAssert(i < _optionsForOnePerson.totalCosts.size());\n\t\t\tAssert(i < _optionsForOnePerson.currencies.size());\n\t\t\tresult.push_back({\n\t\t\t\t.cost = _optionsForOnePerson.totalCosts[i],\n\t\t\t\t.currency = _optionsForOnePerson.currencies[i],\n\t\t\t\t.months = _optionsForOnePerson.months[i],\n\t\t\t});\n\t\t}\n\t}\n\treturn result;\n}\n\nData::PremiumSubscriptionOptions PremiumGiftCodeOptions::optionsForGiveaway(\n\t\tint usersCount) {\n\tconst auto skipForStars = [&](Data::PremiumSubscriptionOptions options) {\n\t\tconst auto proj = &Data::PremiumSubscriptionOption::currency;\n\t\toptions.erase(\n\t\t\tranges::remove(options, Ui::kCreditsCurrency, proj),\n\t\t\tend(options));\n\t\treturn options;\n\t};\n\tconst auto it = _subscriptionOptions.find(usersCount);\n\tif (it != end(_subscriptionOptions)) {\n\t\treturn skipForStars(it->second);\n\t} else {\n\t\tauto tlOptions = QVector<MTPPremiumGiftCodeOption>();\n\t\tfor (auto i = 0; i < _optionsForOnePerson.months.size(); i++) {\n\t\t\ttlOptions.push_back(MTP_premiumGiftCodeOption(\n\t\t\t\tMTP_flags(MTPDpremiumGiftCodeOption::Flags(0)),\n\t\t\t\tMTP_int(usersCount),\n\t\t\t\tMTP_int(_optionsForOnePerson.months[i]),\n\t\t\t\tMTPstring(),\n\t\t\t\tMTPint(),\n\t\t\t\tMTP_string(_optionsForOnePerson.currencies[i]),\n\t\t\t\tMTP_long(_optionsForOnePerson.totalCosts[i] * usersCount)));\n\t\t}\n\t\t_subscriptionOptions[usersCount] = GiftCodesFromTL(tlOptions);\n\t\treturn skipForStars(_subscriptionOptions[usersCount]);\n\t}\n}\n\nauto PremiumGiftCodeOptions::requestStarGifts()\n-> rpl::producer<rpl::no_value, QString> {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\t_api.request(MTPpayments_GetStarGifts(\n\t\t\tMTP_int(0)\n\t\t)).done([=](const MTPpayments_StarGifts &result) {\n\t\t\tresult.match([&](const MTPDpayments_starGifts &data) {\n\t\t\t\t_peer->owner().processUsers(data.vusers());\n\t\t\t\t_peer->owner().processChats(data.vchats());\n\t\t\t\t_giftsHash = data.vhash().v;\n\t\t\t\tconst auto &list = data.vgifts().v;\n\t\t\t\tconst auto session = &_peer->session();\n\t\t\t\tauto gifts = std::vector<Data::StarGift>();\n\t\t\t\tgifts.reserve(list.size());\n\t\t\t\tfor (const auto &gift : list) {\n\t\t\t\t\tif (auto parsed = FromTL(session, gift)) {\n\t\t\t\t\t\tgifts.push_back(std::move(*parsed));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t_gifts = std::move(gifts);\n\t\t\t}, [&](const MTPDpayments_starGiftsNotModified &) {\n\t\t\t});\n\t\t\tconsumer.put_done();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).send();\n\n\t\treturn lifetime;\n\t};\n}\n\nauto PremiumGiftCodeOptions::starGifts() const\n-> const std::vector<Data::StarGift> & {\n\treturn _gifts;\n}\n\nint PremiumGiftCodeOptions::giveawayBoostsPerPremium() const {\n\tconstexpr auto kFallbackCount = 4;\n\treturn _peer->session().appConfig().get<int>(\n\t\tu\"giveaway_boosts_per_premium\"_q,\n\t\tkFallbackCount);\n}\n\nint PremiumGiftCodeOptions::giveawayCountriesMax() const {\n\tconstexpr auto kFallbackCount = 10;\n\treturn _peer->session().appConfig().get<int>(\n\t\tu\"giveaway_countries_max\"_q,\n\t\tkFallbackCount);\n}\n\nint PremiumGiftCodeOptions::giveawayAddPeersMax() const {\n\tconstexpr auto kFallbackCount = 10;\n\treturn _peer->session().appConfig().get<int>(\n\t\tu\"giveaway_add_peers_max\"_q,\n\t\tkFallbackCount);\n}\n\nint PremiumGiftCodeOptions::giveawayPeriodMax() const {\n\tconstexpr auto kFallbackCount = 3600 * 24 * 7;\n\treturn _peer->session().appConfig().get<int>(\n\t\tu\"giveaway_period_max\"_q,\n\t\tkFallbackCount);\n}\n\nbool PremiumGiftCodeOptions::giveawayGiftsPurchaseAvailable() const {\n\treturn _peer->session().appConfig().get<bool>(\n\t\tu\"giveaway_gifts_purchase_available\"_q,\n\t\tfalse);\n}\n\nSponsoredToggle::SponsoredToggle(not_null<Main::Session*> session)\n: _api(&session->api().instance()) {\n}\n\nrpl::producer<bool> SponsoredToggle::toggled() {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\t_api.request(MTPusers_GetFullUser(\n\t\t\tMTP_inputUserSelf()\n\t\t)).done([=](const MTPusers_UserFull &result) {\n\t\t\tconsumer.put_next_copy(\n\t\t\t\tresult.data().vfull_user().data().is_sponsored_enabled());\n\t\t}).fail([=] { consumer.put_next(false); }).send();\n\n\t\treturn lifetime;\n\t};\n}\n\nrpl::producer<rpl::no_value, QString> SponsoredToggle::setToggled(bool v) {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\t_api.request(MTPaccount_ToggleSponsoredMessages(\n\t\t\tMTP_bool(v)\n\t\t)).done([=] {\n\t\t\tconsumer.put_done();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).send();\n\n\t\treturn lifetime;\n\t};\n}\n\nMessageMoneyRestriction ResolveMessageMoneyRestrictions(\n\t\tnot_null<PeerData*> peer,\n\t\tHistory *maybeHistory) {\n\tif (const auto channel = peer->asChannel()) {\n\t\treturn {\n\t\t\t.starsPerMessage = channel->starsPerMessageChecked(),\n\t\t\t.known = true,\n\t\t};\n\t}\n\tconst auto user = peer->asUser();\n\tif (!user) {\n\t\treturn { .known = true };\n\t} else if (user->messageMoneyRestrictionsKnown()) {\n\t\treturn {\n\t\t\t.starsPerMessage = user->starsPerMessageChecked(),\n\t\t\t.premiumRequired = (user->requiresPremiumToWrite()\n\t\t\t\t&& !user->session().premium()),\n\t\t\t.known = true,\n\t\t};\n\t} else if (user->hasStarsPerMessage()) {\n\t\treturn {};\n\t} else if (!user->hasRequirePremiumToWrite()) {\n\t\treturn { .known = true };\n\t} else if (user->flags() & UserDataFlag::MutualContact) {\n\t\treturn { .known = true };\n\t} else if (!maybeHistory) {\n\t\treturn {};\n\t}\n\tconst auto update = [&](bool require) {\n\t\tusing Flag = UserDataFlag;\n\t\tconstexpr auto known = Flag::MessageMoneyRestrictionsKnown;\n\t\tconstexpr auto me = Flag::RequiresPremiumToWrite;\n\t\tuser->setFlags((user->flags() & ~me)\n\t\t\t| known\n\t\t\t| (require ? me : Flag()));\n\t};\n\t// We allow this potentially-heavy loop because in case we've opened\n\t// the chat and have a lot of messages `requires_premium` will be known.\n\tfor (const auto &block : maybeHistory->blocks) {\n\t\tfor (const auto &view : block->messages) {\n\t\t\tconst auto item = view->data();\n\t\t\tif (!item->out() && !item->isService()) {\n\t\t\t\tupdate(false);\n\t\t\t\treturn { .known = true };\n\t\t\t}\n\t\t}\n\t}\n\tif (user->isContact() // Here we know, that we're not in his contacts.\n\t\t&& maybeHistory->loadedAtTop() // And no incoming messages.\n\t\t&& maybeHistory->loadedAtBottom()) {\n\t\treturn {\n\t\t\t.premiumRequired = !user->session().premium(),\n\t\t\t.known = true,\n\t\t};\n\t}\n\treturn {};\n}\n\nrpl::producer<DocumentData*> RandomHelloStickerValue(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto premium = &session->api().premium();\n\tconst auto random = [=] {\n\t\tconst auto &v = premium->helloStickers();\n\t\tAssert(!v.empty());\n\t\treturn v[base::RandomIndex(v.size())].get();\n\t};\n\tconst auto &v = premium->helloStickers();\n\tif (!v.empty()) {\n\t\treturn rpl::single(random());\n\t}\n\treturn rpl::single<DocumentData*>(\n\t\tnullptr\n\t) | rpl::then(premium->helloStickersUpdated(\n\t) | rpl::filter([=] {\n\t\treturn !premium->helloStickers().empty();\n\t}) | rpl::take(1) | rpl::map(random));\n}\n\nstd::optional<Data::StarGift> FromTL(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPstarGift &gift) {\n\treturn gift.match([&](const MTPDstarGift &data) {\n\t\tconst auto document = session->data().processDocument(\n\t\t\tdata.vsticker());\n\t\tconst auto resellPrice = data.vresell_min_stars().value_or_empty();\n\t\tconst auto remaining = data.vavailability_remains();\n\t\tconst auto total = data.vavailability_total();\n\t\tif (!document->sticker()) {\n\t\t\treturn std::optional<Data::StarGift>();\n\t\t}\n\t\tconst auto releasedById = data.vreleased_by()\n\t\t\t? peerFromMTP(*data.vreleased_by())\n\t\t\t: PeerId();\n\t\tconst auto releasedBy = releasedById\n\t\t\t? session->data().peer(releasedById).get()\n\t\t\t: nullptr;\n\t\tconst auto background = [&] {\n\t\t\tif (!data.vbackground()) {\n\t\t\t\treturn std::shared_ptr<Data::StarGiftBackground>();\n\t\t\t}\n\t\t\tconst auto &fields = data.vbackground()->data();\n\t\t\tusing namespace Ui;\n\t\t\treturn std::make_shared<Data::StarGiftBackground>(\n\t\t\t\tData::StarGiftBackground{\n\t\t\t\t\t.center = ColorFromSerialized(fields.vcenter_color()),\n\t\t\t\t\t.edge = ColorFromSerialized(fields.vedge_color()),\n\t\t\t\t\t.text = ColorFromSerialized(fields.vtext_color()),\n\t\t\t\t});\n\t\t};\n\t\treturn std::optional<Data::StarGift>(Data::StarGift{\n\t\t\t.id = uint64(data.vid().v),\n\t\t\t.background = background(),\n\t\t\t.stars = int64(data.vstars().v),\n\t\t\t.starsConverted = int64(data.vconvert_stars().v),\n\t\t\t.starsToUpgrade = int64(data.vupgrade_stars().value_or_empty()),\n\t\t\t.starsResellMin = int64(resellPrice),\n\t\t\t.document = document,\n\t\t\t.releasedBy = releasedBy,\n\t\t\t.resellTitle = qs(data.vtitle().value_or_empty()),\n\t\t\t.resellCount = int(data.vavailability_resale().value_or_empty()),\n\t\t\t.auctionSlug = qs(data.vauction_slug().value_or_empty()),\n\t\t\t.auctionGiftsPerRound = data.vgifts_per_round().value_or_empty(),\n\t\t\t.auctionStartDate = data.vauction_start_date().value_or_empty(),\n\t\t\t.limitedLeft = remaining.value_or_empty(),\n\t\t\t.limitedCount = total.value_or_empty(),\n\t\t\t.perUserTotal = data.vper_user_total().value_or_empty(),\n\t\t\t.perUserRemains = data.vper_user_remains().value_or_empty(),\n\t\t\t.upgradeVariants = data.vupgrade_variants().value_or_empty(),\n\t\t\t.firstSaleDate = data.vfirst_sale_date().value_or_empty(),\n\t\t\t.lastSaleDate = data.vlast_sale_date().value_or_empty(),\n\t\t\t.lockedUntilDate = data.vlocked_until_date().value_or_empty(),\n\t\t\t.requirePremium = data.is_require_premium(),\n\t\t\t.peerColorAvailable = data.is_peer_color_available(),\n\t\t\t.upgradable = data.vupgrade_stars().has_value(),\n\t\t\t.birthday = data.is_birthday(),\n\t\t\t.soldOut = data.is_sold_out(),\n\t\t});\n\t}, [&](const MTPDstarGiftUnique &data) {\n\t\tconst auto total = data.vavailability_total().v;\n\t\tauto model = std::optional<Data::UniqueGiftModel>();\n\t\tauto pattern = std::optional<Data::UniqueGiftPattern>();\n\t\tfor (const auto &attribute : data.vattributes().v) {\n\t\t\tattribute.match([&](const MTPDstarGiftAttributeModel &data) {\n\t\t\t\tmodel = FromTL(session, data);\n\t\t\t}, [&](const MTPDstarGiftAttributePattern &data) {\n\t\t\t\tpattern = FromTL(session, data);\n\t\t\t}, [&](const MTPDstarGiftAttributeBackdrop &data) {\n\t\t\t}, [&](const MTPDstarGiftAttributeOriginalDetails &data) {\n\t\t\t});\n\t\t}\n\t\tif (!model\n\t\t\t|| !model->document->sticker()\n\t\t\t|| !pattern\n\t\t\t|| !pattern->document->sticker()) {\n\t\t\treturn std::optional<Data::StarGift>();\n\t\t}\n\t\tconst auto releasedById = data.vreleased_by()\n\t\t\t? peerFromMTP(*data.vreleased_by())\n\t\t\t: PeerId();\n\t\tconst auto themeUserId = data.vtheme_peer()\n\t\t\t? peerFromMTP(*data.vtheme_peer())\n\t\t\t: PeerId();\n\t\tconst auto releasedBy = releasedById\n\t\t\t? session->data().peer(releasedById).get()\n\t\t\t: nullptr;\n\t\tconst auto themeUser = themeUserId\n\t\t\t? session->data().peer(themeUserId).get()\n\t\t\t: nullptr;\n\t\tconst auto colorCollectible = (data.vpeer_color()\n\t\t\t&& data.vpeer_color()->type() == mtpc_peerColorCollectible)\n\t\t\t? std::make_shared<Ui::ColorCollectible>(\n\t\t\t\tData::ParseColorCollectible(\n\t\t\t\t\tdata.vpeer_color()->c_peerColorCollectible()))\n\t\t\t: nullptr;\n\t\tauto result = Data::StarGift{\n\t\t\t.id = data.vid().v,\n\t\t\t.unique = std::make_shared<Data::UniqueGift>(Data::UniqueGift{\n\t\t\t\t.id = data.vid().v,\n\t\t\t\t.initialGiftId = data.vgift_id().v,\n\t\t\t\t.slug = qs(data.vslug()),\n\t\t\t\t.title = qs(data.vtitle()),\n\t\t\t\t.giftAddress = qs(data.vgift_address().value_or_empty()),\n\t\t\t\t.ownerAddress = qs(data.vowner_address().value_or_empty()),\n\t\t\t\t.ownerName = qs(data.vowner_name().value_or_empty()),\n\t\t\t\t.ownerId = (data.vowner_id()\n\t\t\t\t\t? peerFromMTP(*data.vowner_id())\n\t\t\t\t\t: PeerId()),\n\t\t\t\t.hostId = (data.vhost_id()\n\t\t\t\t\t? peerFromMTP(*data.vhost_id())\n\t\t\t\t\t: PeerId()),\n\t\t\t\t.releasedBy = releasedBy,\n\t\t\t\t.themeUser = themeUser,\n\t\t\t\t.nanoTonForResale = FindTonForResale(data.vresell_amount()),\n\t\t\t\t.craftChancePermille\n\t\t\t\t\t= data.vcraft_chance_permille().value_or_empty(),\n\t\t\t\t.starsForResale = FindStarsForResale(data.vresell_amount()),\n\t\t\t\t.starsMinOffer = data.voffer_min_stars().value_or(-1),\n\t\t\t\t.number = data.vnum().v,\n\t\t\t\t.onlyAcceptTon = data.is_resale_ton_only(),\n\t\t\t\t.canBeTheme = data.is_theme_available(),\n\t\t\t\t.crafted = data.is_crafted(),\n\t\t\t\t.burned = data.is_burned(),\n\t\t\t\t.model = *model,\n\t\t\t\t.pattern = *pattern,\n\t\t\t\t.value = (data.vvalue_amount()\n\t\t\t\t\t? std::make_shared<Data::UniqueGiftValue>(\n\t\t\t\t\t\tData::UniqueGiftValue{\n\t\t\t\t\t\t\t.currency = qs(\n\t\t\t\t\t\t\t\tdata.vvalue_currency().value_or_empty()),\n\t\t\t\t\t\t\t.valuePrice = int64(\n\t\t\t\t\t\t\t\tdata.vvalue_amount().value_or_empty()),\n\t\t\t\t\t\t\t.valuePriceUsd = int64(\n\t\t\t\t\t\t\t\tdata.vvalue_usd_amount().value_or_empty()),\n\t\t\t\t\t\t})\n\t\t\t\t\t: nullptr),\n\t\t\t\t.peerColor = colorCollectible,\n\t\t\t}),\n\t\t\t.document = model->document,\n\t\t\t.releasedBy = releasedBy,\n\t\t\t.limitedLeft = (total - data.vavailability_issued().v),\n\t\t\t.limitedCount = total,\n\t\t\t.resellTonOnly = data.is_resale_ton_only(),\n\t\t\t.requirePremium = data.is_require_premium(),\n\t\t};\n\t\tconst auto unique = result.unique.get();\n\t\tfor (const auto &attribute : data.vattributes().v) {\n\t\t\tattribute.match([&](const MTPDstarGiftAttributeModel &data) {\n\t\t\t}, [&](const MTPDstarGiftAttributePattern &data) {\n\t\t\t}, [&](const MTPDstarGiftAttributeBackdrop &data) {\n\t\t\t\tunique->backdrop = FromTL(data);\n\t\t\t}, [&](const MTPDstarGiftAttributeOriginalDetails &data) {\n\t\t\t\tunique->originalDetails = FromTL(session, data);\n\t\t\t});\n\t\t}\n\t\treturn std::make_optional(std::move(result));\n\t});\n}\n\nstd::optional<Data::SavedStarGift> FromTL(\n\t\tnot_null<PeerData*> to,\n\t\tconst MTPsavedStarGift &gift) {\n\tconst auto session = &to->session();\n\tconst auto &data = gift.data();\n\tauto parsed = FromTL(session, data.vgift());\n\tif (!parsed) {\n\t\treturn {};\n\t} else if (const auto unique = parsed->unique.get()) {\n\t\tunique->starsForTransfer = data.vtransfer_stars().value_or(-1);\n\t\tunique->exportAt = data.vcan_export_at().value_or_empty();\n\t\tunique->canTransferAt = data.vcan_transfer_at().value_or_empty();\n\t\tunique->canResellAt = data.vcan_resell_at().value_or_empty();\n\t\tunique->canCraftAt = data.vcan_craft_at().value_or_empty();\n\t}\n\tusing Id = Data::SavedStarGiftId;\n\tconst auto hasUnique = parsed->unique != nullptr;\n\treturn Data::SavedStarGift{\n\t\t.info = std::move(*parsed),\n\t\t.manageId = (to->isUser()\n\t\t\t? Id::User(data.vmsg_id().value_or_empty())\n\t\t\t: Id::Chat(to, data.vsaved_id().value_or_empty())),\n\t\t.collectionIds = (data.vcollection_id()\n\t\t\t? (data.vcollection_id()->v\n\t\t\t\t| ranges::views::transform(&MTPint::v)\n\t\t\t\t| ranges::to_vector)\n\t\t\t: std::vector<int>()),\n\t\t.message = (data.vmessage()\n\t\t\t? Api::ParseTextWithEntities(\n\t\t\t\tsession,\n\t\t\t\t*data.vmessage())\n\t\t\t: TextWithEntities()),\n\t\t.starsConverted = int64(data.vconvert_stars().value_or_empty()),\n\t\t.starsUpgradedBySender = int64(\n\t\t\tdata.vupgrade_stars().value_or_empty()),\n\t\t.starsForDetailsRemove = int64(\n\t\t\tdata.vdrop_original_details_stars().value_or_empty()),\n\t\t.giftPrepayUpgradeHash = qs(\n\t\t\tdata.vprepaid_upgrade_hash().value_or_empty()),\n\t\t.fromId = (data.vfrom_id()\n\t\t\t? peerFromMTP(*data.vfrom_id())\n\t\t\t: PeerId()),\n\t\t.date = data.vdate().v,\n\t\t.giftNum = data.vgift_num().value_or_empty(),\n\t\t.upgradeSeparate = data.is_upgrade_separate(),\n\t\t.upgradable = data.is_can_upgrade(),\n\t\t.anonymous = data.is_name_hidden(),\n\t\t.pinned = data.is_pinned_to_top() && hasUnique,\n\t\t.hidden = data.is_unsaved(),\n\t\t.mine = to->isSelf(),\n\t};\n}\n\nint ParseRarity(const MTPStarGiftAttributeRarity &rarity) {\n\treturn rarity.match([&](const MTPDstarGiftAttributeRarity &data) {\n\t\treturn std::max(data.vpermille().v, 0);\n\t}, [&](const MTPDstarGiftAttributeRarityUncommon &) {\n\t\treturn int(Data::UniqueGiftRarity::Uncommon);\n\t}, [&](const MTPDstarGiftAttributeRarityRare &) {\n\t\treturn int(Data::UniqueGiftRarity::Rare);\n\t}, [&](const MTPDstarGiftAttributeRarityEpic &) {\n\t\treturn int(Data::UniqueGiftRarity::Epic);\n\t}, [&](const MTPDstarGiftAttributeRarityLegendary &) {\n\t\treturn int(Data::UniqueGiftRarity::Legendary);\n\t});\n}\n\nData::UniqueGiftModel FromTL(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDstarGiftAttributeModel &data) {\n\tauto result = Data::UniqueGiftModel{\n\t\t.document = session->data().processDocument(data.vdocument()),\n\t};\n\tresult.name = qs(data.vname());\n\tresult.rarityValue = ParseRarity(data.vrarity());\n\treturn result;\n}\n\nData::UniqueGiftPattern FromTL(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDstarGiftAttributePattern &data) {\n\tauto result = Data::UniqueGiftPattern{\n\t\t.document = session->data().processDocument(data.vdocument()),\n\t};\n\tresult.document->overrideEmojiUsesTextColor(true);\n\tresult.name = qs(data.vname());\n\tresult.rarityValue = ParseRarity(data.vrarity());\n\treturn result;\n}\n\nData::UniqueGiftBackdrop FromTL(const MTPDstarGiftAttributeBackdrop &data) {\n\tauto result = Data::UniqueGiftBackdrop{ .id = data.vbackdrop_id().v };\n\tresult.name = qs(data.vname());\n\tresult.rarityValue = ParseRarity(data.vrarity());\n\tresult.centerColor = Ui::ColorFromSerialized(\n\t\tdata.vcenter_color());\n\tresult.edgeColor = Ui::ColorFromSerialized(\n\t\tdata.vedge_color());\n\tresult.patternColor = Ui::ColorFromSerialized(\n\t\tdata.vpattern_color());\n\tresult.textColor = Ui::ColorFromSerialized(\n\t\tdata.vtext_color());\n\treturn result;\n}\n\nData::UniqueGiftOriginalDetails FromTL(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDstarGiftAttributeOriginalDetails &data) {\n\tauto result = Data::UniqueGiftOriginalDetails();\n\tresult.date = data.vdate().v;\n\tresult.senderId = data.vsender_id()\n\t\t? peerFromMTP(*data.vsender_id())\n\t\t: PeerId();\n\tresult.recipientId = peerFromMTP(data.vrecipient_id());\n\tresult.message = data.vmessage()\n\t\t? ParseTextWithEntities(session, *data.vmessage())\n\t\t: TextWithEntities();\n\treturn result;\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_premium.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_premium_subscription_option.h\"\n#include \"data/data_star_gift.h\"\n#include \"mtproto/sender.h\"\n\nclass History;\nclass ApiWrap;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Payments {\nstruct InvoicePremiumGiftCode;\n} // namespace Payments\n\nnamespace Api {\n\nstruct GiftCode {\n\tPeerId from = 0;\n\tPeerId to = 0;\n\tMsgId giveawayId = 0;\n\tTimeId date = 0;\n\tTimeId used = 0; // 0 if not used.\n\tint days = 0;\n\tbool giveaway = false;\n\n\texplicit operator bool() const {\n\t\treturn days != 0;\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst GiftCode&,\n\t\tconst GiftCode&) = default;\n};\n\nenum class GiveawayState {\n\tInvalid,\n\tRunning,\n\tPreparing,\n\tFinished,\n\tRefunded,\n};\n\nstruct GiveawayInfo {\n\tQString giftCode;\n\tQString disallowedCountry;\n\tChannelId adminChannelId = 0;\n\tGiveawayState state = GiveawayState::Invalid;\n\tTimeId tooEarlyDate = 0;\n\tTimeId finishDate = 0;\n\tTimeId startDate = 0;\n\tuint64 credits = 0;\n\tint winnersCount = 0;\n\tint activatedCount = 0;\n\tbool participating = false;\n\n\texplicit operator bool() const {\n\t\treturn state != GiveawayState::Invalid;\n\t}\n};\n\nstruct GiftOptionData {\n\tint64 cost = 0;\n\tQString currency;\n\tint months = 0;\n};\n\nclass Premium final {\npublic:\n\texplicit Premium(not_null<ApiWrap*> api);\n\n\tvoid reload();\n\t[[nodiscard]] rpl::producer<TextWithEntities> statusTextValue() const;\n\n\t[[nodiscard]] auto videos() const\n\t\t-> const base::flat_map<QString, not_null<DocumentData*>> &;\n\t[[nodiscard]] rpl::producer<> videosUpdated() const;\n\n\t[[nodiscard]] auto stickers() const\n\t\t-> const std::vector<not_null<DocumentData*>> &;\n\t[[nodiscard]] rpl::producer<> stickersUpdated() const;\n\n\t[[nodiscard]] auto cloudSet() const\n\t\t-> const std::vector<not_null<DocumentData*>> &;\n\t[[nodiscard]] rpl::producer<> cloudSetUpdated() const;\n\n\t[[nodiscard]] auto helloStickers() const\n\t\t-> const std::vector<not_null<DocumentData*>> &;\n\t[[nodiscard]] rpl::producer<> helloStickersUpdated() const;\n\n\t[[nodiscard]] int64 monthlyAmount() const;\n\t[[nodiscard]] QString monthlyCurrency() const;\n\n\tvoid checkGiftCode(\n\t\tconst QString &slug,\n\t\tFn<void(GiftCode)> done);\n\tGiftCode updateGiftCode(const QString &slug, const GiftCode &code);\n\t[[nodiscard]] rpl::producer<GiftCode> giftCodeValue(\n\t\tconst QString &slug) const;\n\tvoid applyGiftCode(const QString &slug, Fn<void(QString)> done);\n\n\tvoid resolveGiveawayInfo(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId messageId,\n\t\tFn<void(GiveawayInfo)> done);\n\n\t[[nodiscard]] auto subscriptionOptions() const\n\t\t-> const Data::PremiumSubscriptionOptions &;\n\n\t[[nodiscard]] auto someMessageMoneyRestrictionsResolved() const\n\t\t-> rpl::producer<>;\n\tvoid resolveMessageMoneyRestrictions(not_null<UserData*> user);\n\nprivate:\n\tvoid reloadPromo();\n\tvoid reloadStickers();\n\tvoid reloadCloudSet();\n\tvoid reloadHelloStickers();\n\tvoid requestPremiumRequiredSlice();\n\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\n\tmtpRequestId _promoRequestId = 0;\n\tstd::optional<TextWithEntities> _statusText;\n\trpl::event_stream<TextWithEntities> _statusTextUpdates;\n\n\tbase::flat_map<QString, not_null<DocumentData*>> _videos;\n\trpl::event_stream<> _videosUpdated;\n\n\tmtpRequestId _stickersRequestId = 0;\n\tuint64 _stickersHash = 0;\n\tstd::vector<not_null<DocumentData*>> _stickers;\n\trpl::event_stream<> _stickersUpdated;\n\n\tmtpRequestId _cloudSetRequestId = 0;\n\tuint64 _cloudSetHash = 0;\n\tstd::vector<not_null<DocumentData*>> _cloudSet;\n\trpl::event_stream<> _cloudSetUpdated;\n\n\tmtpRequestId _helloStickersRequestId = 0;\n\tuint64 _helloStickersHash = 0;\n\tstd::vector<not_null<DocumentData*>> _helloStickers;\n\trpl::event_stream<> _helloStickersUpdated;\n\n\tint64 _monthlyAmount = 0;\n\tQString _monthlyCurrency;\n\n\tmtpRequestId _giftCodeRequestId = 0;\n\tQString _giftCodeSlug;\n\tbase::flat_map<QString, GiftCode> _giftCodes;\n\trpl::event_stream<QString> _giftCodeUpdated;\n\n\tmtpRequestId _giveawayInfoRequestId = 0;\n\tPeerData *_giveawayInfoPeer = nullptr;\n\tMsgId _giveawayInfoMessageId = 0;\n\tFn<void(GiveawayInfo)> _giveawayInfoDone;\n\n\tData::PremiumSubscriptionOptions _subscriptionOptions;\n\n\trpl::event_stream<> _someMessageMoneyRestrictionsResolved;\n\tbase::flat_set<not_null<UserData*>> _resolveMessageMoneyRequiredUsers;\n\tbase::flat_set<not_null<UserData*>> _resolveMessageMoneyRequestedUsers;\n\tbool _messageMoneyRequestScheduled = false;\n\n};\n\nclass PremiumGiftCodeOptions final {\npublic:\n\tPremiumGiftCodeOptions(not_null<PeerData*> peer);\n\n\t[[nodiscard]] rpl::producer<rpl::no_value, QString> request();\n\t[[nodiscard]] std::vector<GiftOptionData> optionsForPeer() const;\n\t[[nodiscard]] Data::PremiumSubscriptionOptions optionsForGiveaway(\n\t\t\tint usersCount);\n\t[[nodiscard]] const std::vector<int> &availablePresets() const;\n\t[[nodiscard]] int monthsFromPreset(int monthsIndex);\n\t[[nodiscard]] Payments::InvoicePremiumGiftCode invoice(\n\t\tint users,\n\t\tint months);\n\t[[nodiscard]] rpl::producer<rpl::no_value, QString> applyPrepaid(\n\t\tconst Payments::InvoicePremiumGiftCode &invoice,\n\t\tuint64 prepaidId);\n\n\t[[nodiscard]] int giveawayBoostsPerPremium() const;\n\t[[nodiscard]] int giveawayCountriesMax() const;\n\t[[nodiscard]] int giveawayAddPeersMax() const;\n\t[[nodiscard]] int giveawayPeriodMax() const;\n\t[[nodiscard]] bool giveawayGiftsPurchaseAvailable() const;\n\n\t[[nodiscard]] rpl::producer<rpl::no_value, QString> requestStarGifts();\n\t[[nodiscard]] const std::vector<Data::StarGift> &starGifts() const;\n\nprivate:\n\tstruct Token final {\n\t\tint users = 0;\n\t\tint months = 0;\n\n\t\tfriend inline constexpr auto operator<=>(Token, Token) = default;\n\n\t};\n\tstruct Store final {\n\t\tuint64 amount = 0;\n\t\tQString currency;\n\t\tQString product;\n\t\tint quantity = 0;\n\t};\n\tusing Amount = int;\n\tusing PremiumSubscriptionOptions = Data::PremiumSubscriptionOptions;\n\tconst not_null<PeerData*> _peer;\n\tbase::flat_map<Amount, PremiumSubscriptionOptions> _subscriptionOptions;\n\tstruct {\n\t\tstd::vector<int> months;\n\t\tstd::vector<int64> totalCosts;\n\t\tstd::vector<QString> currencies;\n\t} _optionsForOnePerson;\n\n\tstd::vector<int> _availablePresets;\n\n\tbase::flat_map<Token, Store> _stores;\n\n\tint32 _giftsHash = 0;\n\tstd::vector<Data::StarGift> _gifts;\n\n\tMTP::Sender _api;\n\n};\n\nclass SponsoredToggle final {\npublic:\n\texplicit SponsoredToggle(not_null<Main::Session*> session);\n\n\t[[nodiscard]] rpl::producer<bool> toggled();\n\t[[nodiscard]] rpl::producer<rpl::no_value, QString> setToggled(bool);\n\nprivate:\n\tMTP::Sender _api;\n\n};\n\nstruct MessageMoneyRestriction {\n\tint starsPerMessage = 0;\n\tbool premiumRequired = false;\n\tbool known = false;\n\n\texplicit operator bool() const {\n\t\treturn starsPerMessage != 0 || premiumRequired;\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst MessageMoneyRestriction &,\n\t\tconst MessageMoneyRestriction &) = default;\n};\n[[nodiscard]] MessageMoneyRestriction ResolveMessageMoneyRestrictions(\n\tnot_null<PeerData*> peer,\n\tHistory *maybeHistory);\n\n[[nodiscard]] rpl::producer<DocumentData*> RandomHelloStickerValue(\n\tnot_null<Main::Session*> session);\n\n[[nodiscard]] std::optional<Data::StarGift> FromTL(\n\tnot_null<Main::Session*> session,\n\tconst MTPstarGift &gift);\n[[nodiscard]] std::optional<Data::SavedStarGift> FromTL(\n\tnot_null<PeerData*> to,\n\tconst MTPsavedStarGift &gift);\n\n[[nodiscard]] Data::UniqueGiftModel FromTL(\n\tnot_null<Main::Session*> session,\n\tconst MTPDstarGiftAttributeModel &data);\n[[nodiscard]] Data::UniqueGiftPattern FromTL(\n\tnot_null<Main::Session*> session,\n\tconst MTPDstarGiftAttributePattern &data);\n[[nodiscard]] Data::UniqueGiftBackdrop FromTL(\n\tconst MTPDstarGiftAttributeBackdrop &data);\n[[nodiscard]] Data::UniqueGiftOriginalDetails FromTL(\n\tnot_null<Main::Session*> session,\n\tconst MTPDstarGiftAttributeOriginalDetails &data);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_premium_option.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_premium_option.h\"\n\n#include \"ui/text/format_values.h\"\n\nnamespace Api {\n\nconstexpr auto kDiscountDivider = 1.;\n\nData::PremiumSubscriptionOption CreateSubscriptionOption(\n\t\tint months,\n\t\tfloat64 monthlyAmount,\n\t\tint64 amount,\n\t\tconst QString &currency,\n\t\tconst QString &botUrl) {\n\tconst auto baselineAmount = monthlyAmount * months;\n\tconst auto discount = [&] {\n\t\tconst auto percent = 1. - float64(amount) / baselineAmount;\n\t\treturn base::SafeRound(percent * 100. / kDiscountDivider)\n\t\t\t* kDiscountDivider;\n\t}();\n\tconst auto hasDiscount = (discount > 0);\n\treturn {\n\t\t.months = months,\n\t\t.duration = Ui::FormatTTL(months * 86400 * 31),\n\t\t.discount = hasDiscount\n\t\t\t? QString::fromUtf8(\"\\xe2\\x88\\x92%1%\").arg(discount)\n\t\t\t: QString(),\n\t\t.costPerMonth = Ui::FillAmountAndCurrency(\n\t\t\tint64(base::SafeRound(amount / float64(months))),\n\t\t\tcurrency),\n\t\t.costNoDiscount = hasDiscount\n\t\t\t? Ui::FillAmountAndCurrency(\n\t\t\t\tint64(base::SafeRound(baselineAmount)),\n\t\t\t\tcurrency)\n\t\t\t: QString(),\n\t\t.costPerYear = Ui::FillAmountAndCurrency(\n\t\t\tint64(base::SafeRound(amount / float64(months / 12.))),\n\t\t\tcurrency),\n\t\t.botUrl = botUrl,\n\t};\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_premium_option.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_premium_subscription_option.h\"\n\nnamespace Api {\n\n[[nodiscard]] Data::PremiumSubscriptionOption CreateSubscriptionOption(\n\tint months,\n\tfloat64 monthlyAmount,\n\tint64 amount,\n\tconst QString &currency,\n\tconst QString &botUrl);\n\ntemplate<typename Option>\n[[nodiscard]] auto PremiumSubscriptionOptionsFromTL(\n\t\tconst QVector<Option> &tlOpts) -> Data::PremiumSubscriptionOptions {\n\tif (tlOpts.isEmpty()) {\n\t\treturn {};\n\t}\n\tauto monthlyAmountPerCurrency = base::flat_map<QString, float64>();\n\tauto result = Data::PremiumSubscriptionOptions();\n\tconst auto monthlyAmount = [&](const QString &currency) -> float64 {\n\t\tconst auto it = monthlyAmountPerCurrency.find(currency);\n\t\tif (it != end(monthlyAmountPerCurrency)) {\n\t\t\treturn it->second;\n\t\t}\n\t\tconst auto &min = ranges::min_element(\n\t\t\ttlOpts,\n\t\t\tranges::less(),\n\t\t\t[&](const Option &o) {\n\t\t\t\treturn currency == qs(o.data().vcurrency())\n\t\t\t\t\t? o.data().vamount().v\n\t\t\t\t\t: std::numeric_limits<int64_t>::max();\n\t\t\t}\n\t\t)->data();\n\t\tconst auto monthly = min.vamount().v / float64(min.vmonths().v);\n\t\tmonthlyAmountPerCurrency.emplace(currency, monthly);\n\t\treturn monthly;\n\t};\n\tresult.reserve(tlOpts.size());\n\tfor (const auto &tlOption : tlOpts) {\n\t\tconst auto &option = tlOption.data();\n\t\tauto botUrl = QString();\n\t\tif constexpr (!std::is_same_v<Option, MTPPremiumGiftCodeOption>) {\n\t\t\tbotUrl = qs(option.vbot_url());\n\t\t}\n\t\tconst auto months = option.vmonths().v;\n\t\tconst auto amount = option.vamount().v;\n\t\tconst auto currency = qs(option.vcurrency());\n\t\tresult.push_back(CreateSubscriptionOption(\n\t\t\tmonths,\n\t\t\tmonthlyAmount(currency),\n\t\t\tamount,\n\t\t\tcurrency,\n\t\t\tbotUrl));\n\t}\n\treturn result;\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_reactions_notify_settings.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_reactions_notify_settings.h\"\n\n#include \"apiwrap.h\"\n#include \"main/main_session.h\"\n\nnamespace Api {\nnamespace {\n\n[[nodiscard]] ReactionsNotifyFrom ParseFrom(\n\t\tconst MTPReactionNotificationsFrom &from) {\n\treturn from.match([](const MTPDreactionNotificationsFromContacts &) {\n\t\treturn ReactionsNotifyFrom::Contacts;\n\t}, [](const MTPDreactionNotificationsFromAll &) {\n\t\treturn ReactionsNotifyFrom::All;\n\t});\n}\n\n[[nodiscard]] MTPReactionNotificationsFrom SerializeFrom(\n\t\tReactionsNotifyFrom from) {\n\tswitch (from) {\n\tcase ReactionsNotifyFrom::Contacts:\n\t\treturn MTP_reactionNotificationsFromContacts();\n\tcase ReactionsNotifyFrom::All:\n\t\treturn MTP_reactionNotificationsFromAll();\n\t}\n\tUnexpected(\"Value in SerializeFrom.\");\n}\n\n} // namespace\n\nReactionsNotifySettings::ReactionsNotifySettings(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance()) {\n}\n\nvoid ReactionsNotifySettings::reload(Fn<void()> callback) {\n\tif (callback) {\n\t\t_callbacks.push_back(std::move(callback));\n\t}\n\tif (_requestId) {\n\t\treturn;\n\t}\n\t_requestId = _api.request(MTPaccount_GetReactionsNotifySettings(\n\t)).done([=](const MTPReactionsNotifySettings &result) {\n\t\t_requestId = 0;\n\t\tapply(result);\n\t\tfor (const auto &callback : base::take(_callbacks)) {\n\t\t\tcallback();\n\t\t}\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t\tfor (const auto &callback : base::take(_callbacks)) {\n\t\t\tcallback();\n\t\t}\n\t}).send();\n}\n\nvoid ReactionsNotifySettings::updateMessagesFrom(ReactionsNotifyFrom value) {\n\t_messagesFrom = value;\n\tsave();\n}\n\nvoid ReactionsNotifySettings::updatePollVotesFrom(\n\t\tReactionsNotifyFrom value) {\n\t_pollVotesFrom = value;\n\tsave();\n}\n\nvoid ReactionsNotifySettings::setAllFrom(ReactionsNotifyFrom value) {\n\t_messagesFrom = value;\n\t_pollVotesFrom = value;\n\tsave();\n}\n\nvoid ReactionsNotifySettings::updateShowPreviews(bool value) {\n\t_showPreviews = value;\n\tsave();\n}\n\nReactionsNotifyFrom ReactionsNotifySettings::messagesFromCurrent() const {\n\treturn _messagesFrom.current();\n}\n\nrpl::producer<ReactionsNotifyFrom> ReactionsNotifySettings::messagesFrom() const {\n\treturn _messagesFrom.value();\n}\n\nReactionsNotifyFrom ReactionsNotifySettings::pollVotesFromCurrent() const {\n\treturn _pollVotesFrom.current();\n}\n\nrpl::producer<ReactionsNotifyFrom> ReactionsNotifySettings::pollVotesFrom() const {\n\treturn _pollVotesFrom.value();\n}\n\nbool ReactionsNotifySettings::showPreviewsCurrent() const {\n\treturn _showPreviews.current();\n}\n\nrpl::producer<bool> ReactionsNotifySettings::showPreviews() const {\n\treturn _showPreviews.value();\n}\n\nbool ReactionsNotifySettings::enabledCurrent() const {\n\treturn (_messagesFrom.current() != ReactionsNotifyFrom::None)\n\t\t|| (_pollVotesFrom.current() != ReactionsNotifyFrom::None);\n}\n\nrpl::producer<bool> ReactionsNotifySettings::enabled() const {\n\treturn rpl::combine(\n\t\t_messagesFrom.value(),\n\t\t_pollVotesFrom.value()\n\t) | rpl::map([](\n\t\t\tReactionsNotifyFrom messages,\n\t\t\tReactionsNotifyFrom pollVotes) {\n\t\treturn (messages != ReactionsNotifyFrom::None)\n\t\t\t|| (pollVotes != ReactionsNotifyFrom::None);\n\t}) | rpl::distinct_until_changed();\n}\n\nvoid ReactionsNotifySettings::apply(\n\t\tconst MTPReactionsNotifySettings &settings) {\n\tconst auto &data = settings.data();\n\tconst auto messages = data.vmessages_notify_from();\n\tconst auto stories = data.vstories_notify_from();\n\tconst auto pollVotes = data.vpoll_votes_notify_from();\n\t_messagesFrom = messages\n\t\t? ParseFrom(*messages)\n\t\t: ReactionsNotifyFrom::None;\n\t_storiesFrom = stories\n\t\t? ParseFrom(*stories)\n\t\t: ReactionsNotifyFrom::None;\n\t_pollVotesFrom = pollVotes\n\t\t? ParseFrom(*pollVotes)\n\t\t: ReactionsNotifyFrom::None;\n\t_showPreviews = mtpIsTrue(data.vshow_previews());\n}\n\nvoid ReactionsNotifySettings::save() {\n\tusing Flag = MTPDreactionsNotifySettings::Flag;\n\tconst auto messages = _messagesFrom.current();\n\tconst auto stories = _storiesFrom.current();\n\tconst auto pollVotes = _pollVotesFrom.current();\n\tconst auto previews = _showPreviews.current();\n\tconst auto flags = Flag()\n\t\t| ((messages != ReactionsNotifyFrom::None)\n\t\t\t? Flag::f_messages_notify_from\n\t\t\t: Flag())\n\t\t| ((stories != ReactionsNotifyFrom::None)\n\t\t\t? Flag::f_stories_notify_from\n\t\t\t: Flag())\n\t\t| ((pollVotes != ReactionsNotifyFrom::None)\n\t\t\t? Flag::f_poll_votes_notify_from\n\t\t\t: Flag());\n\t_api.request(base::take(_requestId)).cancel();\n\t_requestId = _api.request(MTPaccount_SetReactionsNotifySettings(\n\t\tMTP_reactionsNotifySettings(\n\t\t\tMTP_flags(flags),\n\t\t\t((messages != ReactionsNotifyFrom::None)\n\t\t\t\t? SerializeFrom(messages)\n\t\t\t\t: MTPReactionNotificationsFrom()),\n\t\t\t((stories != ReactionsNotifyFrom::None)\n\t\t\t\t? SerializeFrom(stories)\n\t\t\t\t: MTPReactionNotificationsFrom()),\n\t\t\t((pollVotes != ReactionsNotifyFrom::None)\n\t\t\t\t? SerializeFrom(pollVotes)\n\t\t\t\t: MTPReactionNotificationsFrom()),\n\t\t\tMTP_notificationSoundDefault(),\n\t\t\tMTP_bool(previews))\n\t)).done([=](const MTPReactionsNotifySettings &result) {\n\t\t_requestId = 0;\n\t\tapply(result);\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t}).send();\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_reactions_notify_settings.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n\nclass ApiWrap;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nenum class ReactionsNotifyFrom : uchar {\n\tNone,\n\tContacts,\n\tAll,\n};\n\nclass ReactionsNotifySettings final {\npublic:\n\texplicit ReactionsNotifySettings(not_null<ApiWrap*> api);\n\n\tvoid reload(Fn<void()> callback = nullptr);\n\n\tvoid updateMessagesFrom(ReactionsNotifyFrom value);\n\tvoid updatePollVotesFrom(ReactionsNotifyFrom value);\n\tvoid setAllFrom(ReactionsNotifyFrom value);\n\tvoid updateShowPreviews(bool value);\n\n\t[[nodiscard]] ReactionsNotifyFrom messagesFromCurrent() const;\n\t[[nodiscard]] rpl::producer<ReactionsNotifyFrom> messagesFrom() const;\n\t[[nodiscard]] ReactionsNotifyFrom pollVotesFromCurrent() const;\n\t[[nodiscard]] rpl::producer<ReactionsNotifyFrom> pollVotesFrom() const;\n\t[[nodiscard]] bool showPreviewsCurrent() const;\n\t[[nodiscard]] rpl::producer<bool> showPreviews() const;\n\n\t[[nodiscard]] bool enabledCurrent() const;\n\t[[nodiscard]] rpl::producer<bool> enabled() const;\n\nprivate:\n\tvoid apply(const MTPReactionsNotifySettings &settings);\n\tvoid save();\n\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\tmtpRequestId _requestId = 0;\n\trpl::variable<ReactionsNotifyFrom> _messagesFrom\n\t\t= ReactionsNotifyFrom::All;\n\trpl::variable<ReactionsNotifyFrom> _storiesFrom\n\t\t= ReactionsNotifyFrom::All;\n\trpl::variable<ReactionsNotifyFrom> _pollVotesFrom\n\t\t= ReactionsNotifyFrom::All;\n\trpl::variable<bool> _showPreviews = true;\n\tstd::vector<Fn<void()>> _callbacks;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_read_metrics.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_read_metrics.h\"\n\n#include \"apiwrap.h\"\n#include \"data/data_peer.h\"\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kSendTimeout = crl::time(5000);\n\n} // namespace\n\nReadMetrics::ReadMetrics(not_null<ApiWrap*> api)\n: _api(&api->instance())\n, _timer([=] { send(); }) {\n}\n\nvoid ReadMetrics::add(\n\t\tnot_null<PeerData*> peer,\n\t\tFinalizedReadMetric metric) {\n\t_pending[peer].push_back(metric);\n\tif (!_timer.isActive()) {\n\t\t_timer.callOnce(kSendTimeout);\n\t}\n}\n\nvoid ReadMetrics::send() {\n\tfor (auto i = _pending.begin(); i != _pending.end();) {\n\t\tif (_requests.contains(i->first)) {\n\t\t\t++i;\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto metrics = QVector<MTPInputMessageReadMetric>();\n\t\tmetrics.reserve(i->second.size());\n\t\tfor (const auto &m : i->second) {\n\t\t\tmetrics.push_back(MTP_inputMessageReadMetric(\n\t\t\t\tMTP_int(m.msgId.bare),\n\t\t\t\tMTP_long(m.viewId),\n\t\t\t\tMTP_int(m.timeInViewMs),\n\t\t\t\tMTP_int(m.activeTimeInViewMs),\n\t\t\t\tMTP_int(m.heightToViewportRatioPermille),\n\t\t\t\tMTP_int(m.seenRangeRatioPermille)));\n\t\t}\n\t\tconst auto peer = i->first;\n\t\tconst auto finish = [=] {\n\t\t\t_requests.erase(peer);\n\t\t\tif (!_pending.empty() && !_timer.isActive()) {\n\t\t\t\t_timer.callOnce(kSendTimeout);\n\t\t\t}\n\t\t};\n\t\tconst auto requestId = _api.request(MTPmessages_ReportReadMetrics(\n\t\t\tpeer->input(),\n\t\t\tMTP_vector<MTPInputMessageReadMetric>(std::move(metrics))\n\t\t)).done([=](const MTPBool &) {\n\t\t\tfinish();\n\t\t}).fail([=](const MTP::Error &) {\n\t\t\tfinish();\n\t\t}).send();\n\n\t\t_requests.emplace(peer, requestId);\n\t\ti = _pending.erase(i);\n\t}\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_read_metrics.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n#include \"base/timer.h\"\n\nclass ApiWrap;\nclass PeerData;\n\nnamespace Api {\n\nstruct FinalizedReadMetric {\n\tMsgId msgId = 0;\n\tuint64 viewId = 0;\n\tint timeInViewMs = 0;\n\tint activeTimeInViewMs = 0;\n\tint heightToViewportRatioPermille = 0;\n\tint seenRangeRatioPermille = 0;\n};\n\nclass ReadMetrics final {\npublic:\n\texplicit ReadMetrics(not_null<ApiWrap*> api);\n\n\tvoid add(not_null<PeerData*> peer, FinalizedReadMetric metric);\n\nprivate:\n\tvoid send();\n\n\tMTP::Sender _api;\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tstd::vector<FinalizedReadMetric>> _pending;\n\tbase::flat_map<not_null<PeerData*>, mtpRequestId> _requests;\n\tbase::Timer _timer;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_report.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_report.h\"\n\n#include \"apiwrap.h\"\n#include \"data/data_session.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_report.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/report_box_graphics.h\"\n#include \"ui/layers/show.h\"\n\nnamespace Api {\n\nnamespace {\n\nMTPreportReason ReasonToTL(const Ui::ReportReason &reason) {\n\tusing Reason = Ui::ReportReason;\n\tswitch (reason) {\n\tcase Reason::Spam: return MTP_inputReportReasonSpam();\n\tcase Reason::Fake: return MTP_inputReportReasonFake();\n\tcase Reason::Violence: return MTP_inputReportReasonViolence();\n\tcase Reason::ChildAbuse: return MTP_inputReportReasonChildAbuse();\n\tcase Reason::Pornography: return MTP_inputReportReasonPornography();\n\tcase Reason::Copyright: return MTP_inputReportReasonCopyright();\n\tcase Reason::IllegalDrugs: return MTP_inputReportReasonIllegalDrugs();\n\tcase Reason::PersonalDetails:\n\t\treturn MTP_inputReportReasonPersonalDetails();\n\tcase Reason::Other: return MTP_inputReportReasonOther();\n\t}\n\tUnexpected(\"Bad reason group value.\");\n}\n\n} // namespace\n\nvoid SendPhotoReport(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tUi::ReportReason reason,\n\t\tconst QString &comment,\n\t\tnot_null<PhotoData*> photo) {\n\tpeer->session().api().request(MTPaccount_ReportProfilePhoto(\n\t\tpeer->input(),\n\t\tphoto->mtpInput(),\n\t\tReasonToTL(reason),\n\t\tMTP_string(comment)\n\t)).done([=] {\n\t\tshow->showToast(tr::lng_report_thanks(tr::now));\n\t}).send();\n}\n\nauto CreateReportMessagesOrStoriesCallback(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<PeerData*> peer)\n-> Fn<void(Data::ReportInput, Fn<void(ReportResult)>)> {\n\tusing TLChoose = MTPDreportResultChooseOption;\n\tusing TLAddComment = MTPDreportResultAddComment;\n\tusing TLReported = MTPDreportResultReported;\n\tusing Result = ReportResult;\n\n\tstruct State final {\n#ifdef _DEBUG\n\t\t~State() {\n\t\t\tqDebug() << \"Messages or Stories Report ~State().\";\n\t\t}\n#endif\n\t\tmtpRequestId requestId = 0;\n\t};\n\tconst auto state = std::make_shared<State>();\n\n\treturn [=](\n\t\t\tData::ReportInput reportInput,\n\t\t\tFn<void(Result)> done) {\n\t\tauto apiIds = QVector<MTPint>();\n\t\tapiIds.reserve(reportInput.ids.size() + reportInput.stories.size());\n\t\tfor (const auto &id : reportInput.ids) {\n\t\t\tapiIds.push_back(MTP_int(id));\n\t\t}\n\t\tfor (const auto &story : reportInput.stories) {\n\t\t\tapiIds.push_back(MTP_int(story));\n\t\t}\n\n\t\tconst auto received = [=](\n\t\t\t\tconst MTPReportResult &result,\n\t\t\t\tmtpRequestId requestId) {\n\t\t\tif (state->requestId != requestId) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->requestId = 0;\n\t\t\tdone(result.match([&](const TLChoose &data) {\n\t\t\t\tconst auto t = qs(data.vtitle());\n\t\t\t\tauto list = Result::Options();\n\t\t\t\tlist.reserve(data.voptions().v.size());\n\t\t\t\tfor (const auto &tl : data.voptions().v) {\n\t\t\t\t\tlist.emplace_back(Result::Option{\n\t\t\t\t\t\t.id = tl.data().voption().v,\n\t\t\t\t\t\t.text = qs(tl.data().vtext()),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn Result{ .options = std::move(list), .title = t };\n\t\t\t}, [&](const TLAddComment &data) -> Result {\n\t\t\t\treturn {\n\t\t\t\t\t.commentOption = ReportResult::CommentOption{\n\t\t\t\t\t\t.optional = data.is_optional(),\n\t\t\t\t\t\t.id = data.voption().v,\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t}, [&](const TLReported &data) -> Result {\n\t\t\t\treturn { .successful = true };\n\t\t\t}));\n\t\t};\n\n\t\tconst auto fail = [=](const MTP::Error &error) {\n\t\t\tstate->requestId = 0;\n\t\t\tdone({ .error = error.type() });\n\t\t};\n\n\t\tif (!reportInput.stories.empty()) {\n\t\t\tstate->requestId = peer->session().api().request(\n\t\t\t\tMTPstories_Report(\n\t\t\t\t\tpeer->input(),\n\t\t\t\t\tMTP_vector<MTPint>(apiIds),\n\t\t\t\t\tMTP_bytes(reportInput.optionId),\n\t\t\t\t\tMTP_string(reportInput.comment))\n\t\t\t).done(received).fail(fail).send();\n\t\t} else {\n\t\t\tstate->requestId = peer->session().api().request(\n\t\t\t\tMTPmessages_Report(\n\t\t\t\t\tpeer->input(),\n\t\t\t\t\tMTP_vector<MTPint>(apiIds),\n\t\t\t\t\tMTP_bytes(reportInput.optionId),\n\t\t\t\t\tMTP_string(reportInput.comment))\n\t\t\t).done(received).fail(fail).send();\n\t\t}\n\t};\n}\n\nvoid ReportSpam(\n\t\tnot_null<PeerData*> sender,\n\t\tconst MessageIdsList &ids) {\n\tif (ids.empty()) {\n\t\treturn;\n\t}\n\tconst auto peer = sender->owner().peer(ids.front().peer);\n\tconst auto channel = peer->asChannel();\n\tif (!channel) {\n\t\treturn;\n\t}\n\n\tauto msgIds = QVector<MTPint>();\n\tmsgIds.reserve(ids.size());\n\tfor (const auto &fullId : ids) {\n\t\tmsgIds.push_back(MTP_int(fullId.msg));\n\t}\n\n\tsender->session().api().request(MTPchannels_ReportSpam(\n\t\tchannel->inputChannel(),\n\t\tsender->input(),\n\t\tMTP_vector<MTPint>(msgIds)\n\t)).send();\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_report.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass HistoryItem;\nclass PeerData;\nclass PhotoData;\n\nnamespace Ui {\nclass Show;\nenum class ReportReason;\n} // namespace Ui\n\nnamespace Data {\nstruct ReportInput;\n} // namespace Data\n\nnamespace Api {\n\nstruct ReportResult final {\n\tusing Id = QByteArray;\n\tstruct Option final {\n\t\tId id = 0;\n\t\tQString text;\n\t};\n\tusing Options = std::vector<Option>;\n\tOptions options;\n\tQString title;\n\tQString error;\n\tQString comment;\n\tstruct CommentOption {\n\t\tbool optional = false;\n\t\tId id = 0;\n\t};\n\tstd::optional<CommentOption> commentOption;\n\tbool successful = false;\n};\n\nvoid SendPhotoReport(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<PeerData*> peer,\n\tUi::ReportReason reason,\n\tconst QString &comment,\n\tnot_null<PhotoData*> photo);\n\n[[nodiscard]] auto CreateReportMessagesOrStoriesCallback(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<PeerData*> peer)\n-> Fn<void(Data::ReportInput, Fn<void(ReportResult)>)>;\n\nvoid ReportSpam(\n\tnot_null<PeerData*> sender,\n\tconst MessageIdsList &ids);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_ringtones.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_ringtones.h\"\n\n#include \"api/api_toggling_media.h\"\n#include \"apiwrap.h\"\n#include \"base/random.h\"\n#include \"base/unixtime.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_session.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"storage/file_upload.h\"\n#include \"storage/localimageloader.h\"\n\nnamespace Api {\nnamespace {\n\nstd::shared_ptr<FilePrepareResult> PrepareRingtoneDocument(\n\t\tMTP::DcId dcId,\n\t\tconst QString &filename,\n\t\tconst QString &filemime,\n\t\tconst QByteArray &content) {\n\tconst auto id = base::RandomValue<DocumentId>();\n\tauto attributes = QVector<MTPDocumentAttribute>(\n\t\t1,\n\t\tMTP_documentAttributeFilename(MTP_string(filename)));\n\n\tauto result = MakePreparedFile({\n\t\t.id = id,\n\t\t.type = SendMediaType::File,\n\t});\n\tresult->filename = filename;\n\tresult->content = content;\n\tresult->filesize = content.size();\n\tresult->setFileData(content);\n\tresult->document = MTP_document(\n\t\tMTP_flags(0),\n\t\tMTP_long(id),\n\t\tMTP_long(0),\n\t\tMTP_bytes(),\n\t\tMTP_int(base::unixtime::now()),\n\t\tMTP_string(filemime),\n\t\tMTP_long(content.size()),\n\t\tMTP_vector<MTPPhotoSize>(),\n\t\tMTPVector<MTPVideoSize>(),\n\t\tMTP_int(dcId),\n\t\tMTP_vector<MTPDocumentAttribute>(std::move(attributes)));\n\treturn result;\n}\n\n} // namespace\n\nRingtones::Ringtones(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance()) {\n\tcrl::on_main(_session, [=] {\n\t\t// You can't use _session->lifetime() in the constructor,\n\t\t// only queued, because it is not constructed yet.\n\t\t_session->uploader().documentReady(\n\t\t) | rpl::on_next([=](const Storage::UploadedMedia &data) {\n\t\t\tready(data.fullId, data.info.file);\n\t\t}, _session->lifetime());\n\t});\n}\n\nvoid Ringtones::upload(\n\t\tconst QString &filename,\n\t\tconst QString &filemime,\n\t\tconst QByteArray &content) {\n\tconst auto ready = PrepareRingtoneDocument(\n\t\t_api.instance().mainDcId(),\n\t\tfilename,\n\t\tfilemime,\n\t\tcontent);\n\n\tconst auto uploadedData = UploadedData{ filename, filemime, content };\n\tconst auto fakeId = FullMsgId(\n\t\t_session->userPeerId(),\n\t\t_session->data().nextLocalMessageId());\n\tconst auto already = ranges::find_if(\n\t\t_uploads,\n\t\t[&](const auto &d) {\n\t\t\treturn uploadedData.filemime == d.second.filemime\n\t\t\t\t&& uploadedData.filename == d.second.filename;\n\t\t});\n\tif (already != end(_uploads)) {\n\t\t_session->uploader().cancel(already->first);\n\t\t_uploads.erase(already);\n\t}\n\t_uploads.emplace(fakeId, uploadedData);\n\t_session->uploader().upload(fakeId, ready);\n}\n\nvoid Ringtones::ready(const FullMsgId &msgId, const MTPInputFile &file) {\n\tconst auto maybeUploadedData = _uploads.take(msgId);\n\tif (!maybeUploadedData) {\n\t\treturn;\n\t}\n\tconst auto uploadedData = *maybeUploadedData;\n\t_api.request(MTPaccount_UploadRingtone(\n\t\tfile,\n\t\tMTP_string(uploadedData.filename),\n\t\tMTP_string(uploadedData.filemime)\n\t)).done([=, content = uploadedData.content](const MTPDocument &result) {\n\t\tconst auto document = _session->data().processDocument(result);\n\t\t_list.documents.insert(_list.documents.begin(), document->id);\n\t\tconst auto media = document->createMediaView();\n\t\tmedia->setBytes(content);\n\t\tdocument->owner().notifySettings().cacheSound(document);\n\t\t_uploadDones.fire_copy(document->id);\n\t}).fail([=](const MTP::Error &error) {\n\t\t_uploadFails.fire_copy(error.type());\n\t}).send();\n}\n\nvoid Ringtones::requestList() {\n\tif (_list.requestId) {\n\t\treturn;\n\t}\n\t_list.requestId = _api.request(\n\t\tMTPaccount_GetSavedRingtones(MTP_long(_list.hash))\n\t).done([=](const MTPaccount_SavedRingtones &result) {\n\t\t_list.requestId = 0;\n\t\tresult.match([&](const MTPDaccount_savedRingtones &data) {\n\t\t\t_list.hash = data.vhash().v;\n\t\t\t_list.documents.clear();\n\t\t\t_list.documents.reserve(data.vringtones().v.size());\n\t\t\tfor (const auto &d : data.vringtones().v) {\n\t\t\t\tconst auto document = _session->data().processDocument(d);\n\t\t\t\tdocument->forceToCache(true);\n\t\t\t\t_list.documents.emplace_back(document->id);\n\t\t\t}\n\t\t\t_list.updates.fire({});\n\t\t}, [&](const MTPDaccount_savedRingtonesNotModified &) {\n\t\t});\n\t}).fail([=] {\n\t\t_list.requestId = 0;\n\t}).send();\n}\n\nconst Ringtones::Ids &Ringtones::list() const {\n\treturn _list.documents;\n}\n\nrpl::producer<> Ringtones::listUpdates() const {\n\treturn _list.updates.events();\n}\n\nrpl::producer<QString> Ringtones::uploadFails() const {\n\treturn _uploadFails.events();\n}\n\nrpl::producer<DocumentId> Ringtones::uploadDones() const {\n\treturn _uploadDones.events();\n}\n\nvoid Ringtones::applyUpdate() {\n\t_list.hash = 0;\n\t_list.documents.clear();\n\trequestList();\n}\n\nvoid Ringtones::remove(DocumentId id) {\n\tif (const auto document = _session->data().document(id)) {\n\t\tToggleSavedRingtone(\n\t\t\tdocument,\n\t\t\tData::FileOriginRingtones(),\n\t\t\tcrl::guard(&document->session(), [=] {\n\t\t\t\tconst auto it = ranges::find(_list.documents, id);\n\t\t\t\tif (it != end(_list.documents)) {\n\t\t\t\t\t_list.documents.erase(it);\n\t\t\t\t}\n\t\t\t}),\n\t\t\tfalse);\n\t}\n}\n\nint64 Ringtones::maxSize() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"ringtone_size_max\"_q,\n\t\t100 * 1024);\n}\n\nint Ringtones::maxSavedCount() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"ringtone_saved_count_max\"_q,\n\t\t100);\n}\n\ncrl::time Ringtones::maxDuration() const {\n\treturn crl::time(1000) * _session->appConfig().get<int>(\n\t\tu\"ringtone_duration_max\"_q,\n\t\t5);\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_ringtones.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n\nclass ApiWrap;\nclass PeerData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nclass Ringtones final {\npublic:\n\texplicit Ringtones(not_null<ApiWrap*> api);\n\n\tusing Ids = std::vector<DocumentId>;\n\n\tvoid requestList();\n\tvoid applyUpdate();\n\tvoid remove(DocumentId id);\n\n\tvoid upload(\n\t\tconst QString &filename,\n\t\tconst QString &filemime,\n\t\tconst QByteArray &content);\n\n\t[[nodiscard]] const Ids &list() const;\n\t[[nodiscard]] rpl::producer<> listUpdates() const;\n\t[[nodiscard]] rpl::producer<QString> uploadFails() const;\n\t[[nodiscard]] rpl::producer<DocumentId> uploadDones() const;\n\n\t[[nodiscard]] int64 maxSize() const;\n\t[[nodiscard]] int maxSavedCount() const;\n\t[[nodiscard]] crl::time maxDuration() const;\n\nprivate:\n\tstruct UploadedData {\n\t\tQString filename;\n\t\tQString filemime;\n\t\tQByteArray content;\n\t};\n\tvoid ready(const FullMsgId &msgId, const MTPInputFile &file);\n\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\n\tbase::flat_map<FullMsgId, UploadedData> _uploads;\n\trpl::event_stream<QString> _uploadFails;\n\trpl::event_stream<DocumentId> _uploadDones;\n\n\tstruct {\n\t\tuint64 hash = 0;\n\t\tIds documents;\n\t\trpl::event_stream<> updates;\n\t\tmtpRequestId requestId = 0;\n\t} _list;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_self_destruct.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_self_destruct.h\"\n\n#include \"apiwrap.h\"\n\nnamespace Api {\n\nSelfDestruct::SelfDestruct(not_null<ApiWrap*> api)\n: _api(&api->instance()) {\n}\n\nvoid SelfDestruct::reload() {\n\tif (!_accountTTL.requestId) {\n\t\t_accountTTL.requestId = _api.request(MTPaccount_GetAccountTTL(\n\t\t)).done([=](const MTPAccountDaysTTL &result) {\n\t\t\t_accountTTL.requestId = 0;\n\t\t\t_accountTTL.days = result.data().vdays().v;\n\t\t}).fail([=] {\n\t\t\t_accountTTL.requestId = 0;\n\t\t}).send();\n\t}\n\tif (!_defaultHistoryTTL.requestId) {\n\t\t_defaultHistoryTTL.requestId = _api.request(\n\t\t\tMTPmessages_GetDefaultHistoryTTL()\n\t\t).done([=](const MTPDefaultHistoryTTL &result) {\n\t\t\t_defaultHistoryTTL.requestId = 0;\n\t\t\t_defaultHistoryTTL.period = result.data().vperiod().v;\n\t\t}).fail([=] {\n\t\t\t_defaultHistoryTTL.requestId = 0;\n\t\t}).send();\n\t}\n}\n\nrpl::producer<int> SelfDestruct::daysAccountTTL() const {\n\treturn _accountTTL.days.value() | rpl::filter(rpl::mappers::_1 != 0);\n}\n\nrpl::producer<TimeId> SelfDestruct::periodDefaultHistoryTTL() const {\n\treturn _defaultHistoryTTL.period.value();\n}\n\nTimeId SelfDestruct::periodDefaultHistoryTTLCurrent() const {\n\treturn _defaultHistoryTTL.period.current();\n}\n\nvoid SelfDestruct::updateAccountTTL(int days) {\n\t_api.request(_accountTTL.requestId).cancel();\n\t_accountTTL.requestId = _api.request(MTPaccount_SetAccountTTL(\n\t\tMTP_accountDaysTTL(MTP_int(days))\n\t)).done([=] {\n\t\t_accountTTL.requestId = 0;\n\t}).fail([=] {\n\t\t_accountTTL.requestId = 0;\n\t}).send();\n\t_accountTTL.days = days;\n}\n\nvoid SelfDestruct::updateDefaultHistoryTTL(TimeId period) {\n\t_api.request(_defaultHistoryTTL.requestId).cancel();\n\t_defaultHistoryTTL.requestId = _api.request(\n\t\tMTPmessages_SetDefaultHistoryTTL(MTP_int(period))\n\t).done([=] {\n\t\t_defaultHistoryTTL.requestId = 0;\n\t}).fail([=] {\n\t\t_defaultHistoryTTL.requestId = 0;\n\t}).send();\n\t_defaultHistoryTTL.period = period;\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_self_destruct.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n\nclass ApiWrap;\n\nnamespace Api {\n\nclass SelfDestruct final {\npublic:\n\texplicit SelfDestruct(not_null<ApiWrap*> api);\n\n\tvoid reload();\n\tvoid updateAccountTTL(int days);\n\tvoid updateDefaultHistoryTTL(TimeId period);\n\n\t[[nodiscard]] rpl::producer<int> daysAccountTTL() const;\n\t[[nodiscard]] rpl::producer<TimeId> periodDefaultHistoryTTL() const;\n\t[[nodiscard]] TimeId periodDefaultHistoryTTLCurrent() const;\n\nprivate:\n\tMTP::Sender _api;\n\tstruct {\n\t\tmtpRequestId requestId = 0;\n\t\trpl::variable<int> days = 0;\n\t} _accountTTL;\n\n\tstruct {\n\t\tmtpRequestId requestId = 0;\n\t\trpl::variable<TimeId> period = 0;\n\t} _defaultHistoryTTL;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_send_progress.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_send_progress.h\"\n\n#include \"main/main_session.h\"\n#include \"history/history.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"base/unixtime.h\"\n#include \"data/data_peer_values.h\"\n#include \"apiwrap.h\"\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kCancelTypingActionTimeout = crl::time(5000);\nconstexpr auto kSendMySpeakingInterval = 3 * crl::time(1000);\nconstexpr auto kSendMyTypingInterval = 5 * crl::time(1000);\nconstexpr auto kSendTypingsToOfflineFor = TimeId(30);\n\n} // namespace\n\nSendProgressManager::SendProgressManager(not_null<Main::Session*> session)\n: _session(session)\n, _stopTypingTimer([=] { cancelTyping(base::take(_stopTypingHistory)); }) {\n}\n\nvoid SendProgressManager::cancel(\n\t\tnot_null<History*> history,\n\t\tSendProgressType type) {\n\tcancel(history, 0, type);\n}\n\nvoid SendProgressManager::cancel(\n\t\tnot_null<History*> history,\n\t\tMsgId topMsgId,\n\t\tSendProgressType type) {\n\tconst auto i = _requests.find(Key{ history, topMsgId, type });\n\tif (i != _requests.end()) {\n\t\t_session->api().request(i->second).cancel();\n\t\t_requests.erase(i);\n\t}\n}\n\nvoid SendProgressManager::cancelTyping(not_null<History*> history) {\n\t_stopTypingTimer.cancel();\n\tcancel(history, SendProgressType::Typing);\n}\n\nvoid SendProgressManager::update(\n\t\tnot_null<History*> history,\n\t\tSendProgressType type,\n\t\tint progress) {\n\tupdate(history, 0, type, progress);\n}\n\nvoid SendProgressManager::update(\n\t\tnot_null<History*> history,\n\t\tMsgId topMsgId,\n\t\tSendProgressType type,\n\t\tint progress) {\n\tconst auto peer = history->peer;\n\tif (peer->isSelf()\n\t\t|| (peer->isChannel()\n\t\t\t&& !peer->isMegagroup()\n\t\t\t&& type != SendProgressType::Speaking)) {\n\t\treturn;\n\t}\n\n\tconst auto doing = (progress >= 0);\n\tconst auto key = Key{ history, topMsgId, type };\n\tif (updated(key, doing)) {\n\t\tcancel(history, topMsgId, type);\n\t\tif (doing) {\n\t\t\tsend(key, progress);\n\t\t}\n\t}\n}\n\nbool SendProgressManager::updated(const Key &key, bool doing) {\n\tconst auto now = crl::now();\n\tconst auto i = _updated.find(key);\n\tif (doing) {\n\t\tconst auto sendEach = (key.type == SendProgressType::Speaking)\n\t\t\t? kSendMySpeakingInterval\n\t\t\t: kSendMyTypingInterval;\n\t\tif (i == end(_updated)) {\n\t\t\t_updated.emplace(key, now + 2 * sendEach);\n\t\t} else if (i->second > now + sendEach) {\n\t\t\treturn false;\n\t\t} else {\n\t\t\ti->second = now + 2 * sendEach;\n\t\t}\n\t} else {\n\t\tif (i == end(_updated)) {\n\t\t\treturn false;\n\t\t} else if (i->second <= now) {\n\t\t\treturn false;\n\t\t} else {\n\t\t\t_updated.erase(i);\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid SendProgressManager::send(const Key &key, int progress) {\n\tif (skipRequest(key)) {\n\t\treturn;\n\t}\n\tusing Type = SendProgressType;\n\tconst auto action = [&]() -> MTPsendMessageAction {\n\t\tconst auto p = MTP_int(progress);\n\t\tswitch (key.type) {\n\t\tcase Type::Typing: return MTP_sendMessageTypingAction();\n\t\tcase Type::RecordVideo: return MTP_sendMessageRecordVideoAction();\n\t\tcase Type::UploadVideo: return MTP_sendMessageUploadVideoAction(p);\n\t\tcase Type::RecordVoice: return MTP_sendMessageRecordAudioAction();\n\t\tcase Type::UploadVoice: return MTP_sendMessageUploadAudioAction(p);\n\t\tcase Type::RecordRound: return MTP_sendMessageRecordRoundAction();\n\t\tcase Type::UploadRound: return MTP_sendMessageUploadRoundAction(p);\n\t\tcase Type::UploadPhoto: return MTP_sendMessageUploadPhotoAction(p);\n\t\tcase Type::UploadFile: return MTP_sendMessageUploadDocumentAction(p);\n\t\tcase Type::ChooseLocation: return MTP_sendMessageGeoLocationAction();\n\t\tcase Type::ChooseContact: return MTP_sendMessageChooseContactAction();\n\t\tcase Type::PlayGame: return MTP_sendMessageGamePlayAction();\n\t\tcase Type::Speaking: return MTP_speakingInGroupCallAction();\n\t\tcase Type::ChooseSticker: return MTP_sendMessageChooseStickerAction();\n\t\tdefault: return MTP_sendMessageTypingAction();\n\t\t}\n\t}();\n\tconst auto requestId = _session->api().request(MTPmessages_SetTyping(\n\t\tMTP_flags(key.topMsgId\n\t\t\t? MTPmessages_SetTyping::Flag::f_top_msg_id\n\t\t\t: MTPmessages_SetTyping::Flag(0)),\n\t\tkey.history->peer->input(),\n\t\tMTP_int(key.topMsgId),\n\t\taction\n\t)).done([=](const MTPBool &result, mtpRequestId requestId) {\n\t\tdone(requestId);\n\t}).send();\n\t_requests.emplace(key, requestId);\n\n\tif (key.type == Type::Typing) {\n\t\t_stopTypingHistory = key.history;\n\t\t_stopTypingTimer.callOnce(kCancelTypingActionTimeout);\n\t}\n}\n\nbool SendProgressManager::skipRequest(const Key &key) const {\n\tconst auto user = key.history->peer->asUser();\n\tif (!user) {\n\t\treturn false;\n\t} else if (user->isSelf()) {\n\t\treturn true;\n\t} else if (user->isBot() && !user->isSupport()) {\n\t\treturn true;\n\t}\n\tconst auto recently = base::unixtime::now() - kSendTypingsToOfflineFor;\n\tconst auto lastseen = user->lastseen();\n\tif (lastseen.isRecently()) {\n\t\treturn false;\n\t} else if (const auto value = lastseen.onlineTill()) {\n\t\treturn (value < recently);\n\t}\n\treturn true;\n}\n\nvoid SendProgressManager::done(mtpRequestId requestId) {\n\tfor (auto i = _requests.begin(), e = _requests.end(); i != e; ++i) {\n\t\tif (i->second == requestId) {\n\t\t\t_requests.erase(i);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_send_progress.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_common.h\"\n#include \"base/timer.h\"\n\nclass History;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nenum class SendProgressType {\n\tTyping,\n\tRecordVideo,\n\tUploadVideo,\n\tRecordVoice,\n\tUploadVoice,\n\tRecordRound,\n\tUploadRound,\n\tUploadPhoto,\n\tUploadFile,\n\tChooseLocation,\n\tChooseContact,\n\tChooseSticker,\n\tPlayGame,\n\tSpeaking,\n};\n\nstruct SendProgress {\n\tSendProgress(\n\t\tSendProgressType type,\n\t\tcrl::time until,\n\t\tint progress = 0)\n\t: type(type)\n\t, until(until)\n\t, progress(progress) {\n\t}\n\tSendProgressType type = SendProgressType::Typing;\n\tcrl::time until = 0;\n\tint progress = 0;\n\n};\n\nclass SendProgressManager final {\npublic:\n\tSendProgressManager(not_null<Main::Session*> session);\n\n\tvoid update(\n\t\tnot_null<History*> history,\n\t\tSendProgressType type,\n\t\tint progress = 0);\n\tvoid update(\n\t\tnot_null<History*> history,\n\t\tMsgId topMsgId,\n\t\tSendProgressType type,\n\t\tint progress = 0);\n\tvoid cancel(\n\t\tnot_null<History*> history,\n\t\tMsgId topMsgId,\n\t\tSendProgressType type);\n\tvoid cancel(\n\t\tnot_null<History*> history,\n\t\tSendProgressType type);\n\tvoid cancelTyping(not_null<History*> history);\n\nprivate:\n\tstruct Key {\n\t\tnot_null<History*> history;\n\t\tMsgId topMsgId = 0;\n\t\tSendProgressType type = SendProgressType();\n\n\t\tinline bool operator<(const Key &other) const {\n\t\t\treturn (history < other.history)\n\t\t\t\t|| (history == other.history && topMsgId < other.topMsgId)\n\t\t\t\t|| (history == other.history\n\t\t\t\t\t&& topMsgId == other.topMsgId\n\t\t\t\t\t&& type < other.type);\n\t\t}\n\t};\n\n\tbool updated(const Key &key, bool doing);\n\n\tvoid send(const Key &key, int progress);\n\tvoid done(mtpRequestId requestId);\n\n\t[[nodiscard]] bool skipRequest(const Key &key) const;\n\n\tconst not_null<Main::Session*> _session;\n\tbase::flat_map<Key, mtpRequestId> _requests;\n\tbase::flat_map<Key, crl::time> _updated;\n\tbase::Timer _stopTypingTimer;\n\tHistory *_stopTypingHistory = nullptr;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_sending.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_sending.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"base/random.h\"\n#include \"base/unixtime.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/data_document.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_channel.h\" // ChannelData::addsSignature.\n#include \"data/data_user.h\" // UserData::name\n#include \"data/data_session.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_changes.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\" // NewMessageFlags.\n#include \"chat_helpers/message_field.h\" // ConvertTextTagsToEntities.\n#include \"chat_helpers/stickers_dice_pack.h\" // DicePacks::kDiceString.\n#include \"ui/text/text_entity.h\" // TextWithEntities.\n#include \"ui/item_text_options.h\" // Ui::ItemTextOptions.\n#include \"main/main_session.h\"\n#include \"main/main_app_config.h\"\n#include \"storage/localimageloader.h\"\n#include \"storage/file_upload.h\"\n#include \"mainwidget.h\"\n#include \"apiwrap.h\"\n\nnamespace Api {\nnamespace {\n\nvoid InnerFillMessagePostFlags(\n\t\tconst SendOptions &options,\n\t\tnot_null<PeerData*> peer,\n\t\tMessageFlags &flags) {\n\tif (ShouldSendSilent(peer, options)) {\n\t\tflags |= MessageFlag::Silent;\n\t}\n\tif (!peer->amAnonymous()\n\t\t|| (!peer->isBroadcast()\n\t\t\t&& options.sendAs\n\t\t\t&& options.sendAs != peer)) {\n\t\tflags |= MessageFlag::HasFromId;\n\t}\n\tconst auto channel = peer->asBroadcast();\n\tif (!channel) {\n\t\treturn;\n\t}\n\tflags |= MessageFlag::Post;\n\t// Don't display views and author of a new post when it's scheduled.\n\tif (options.scheduled) {\n\t\treturn;\n\t}\n\tflags |= MessageFlag::HasViews;\n\tif (channel->addsSignature()) {\n\t\tflags |= MessageFlag::HasPostAuthor;\n\t}\n}\n\nvoid SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) {\n\tconst auto history = action.history;\n\tconst auto peer = history->peer;\n\tconst auto session = &history->session();\n\tconst auto api = &session->api();\n\n\taction.clearDraft = false;\n\taction.generateLocal = false;\n\tapi->sendAction(action);\n\n\tconst auto randomId = base::RandomValue<uint64>();\n\n\tauto flags = NewMessageFlags(peer);\n\tauto sendFlags = MTPmessages_SendMedia::Flags(0);\n\tif (action.replyTo) {\n\t\tflags |= MessageFlag::HasReplyInfo;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;\n\t}\n\tconst auto silentPost = ShouldSendSilent(peer, action.options);\n\tInnerFillMessagePostFlags(action.options, peer, flags);\n\tif (silentPost) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_silent;\n\t}\n\tconst auto sendAs = action.options.sendAs;\n\tif (sendAs) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_send_as;\n\t}\n\tconst auto messagePostAuthor = peer->isBroadcast()\n\t\t? session->user()->name()\n\t\t: QString();\n\tconst auto starsPaid = std::min(\n\t\tpeer->starsPerMessageChecked(),\n\t\taction.options.starsApproved);\n\tif (action.options.scheduled) {\n\t\tflags |= MessageFlag::IsOrWasScheduled;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;\n\t\tif (action.options.scheduleRepeatPeriod) {\n\t\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;\n\t\t}\n\t}\n\tif (action.options.shortcutId) {\n\t\tflags |= MessageFlag::ShortcutMessage;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;\n\t}\n\tif (action.options.effectId) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_effect;\n\t}\n\tif (action.options.suggest) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;\n\t}\n\tif (action.options.invertCaption) {\n\t\tflags |= MessageFlag::InvertMedia;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;\n\t}\n\tif (starsPaid) {\n\t\taction.options.starsApproved -= starsPaid;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;\n\t}\n\n\tauto &histories = history->owner().histories();\n\thistories.sendPreparedMessage(\n\t\thistory,\n\t\taction.replyTo,\n\t\trandomId,\n\t\tData::Histories::PrepareMessage<MTPmessages_SendMedia>(\n\t\t\tMTP_flags(sendFlags),\n\t\t\tpeer->input(),\n\t\t\tData::Histories::ReplyToPlaceholder(),\n\t\t\tstd::move(inputMedia),\n\t\t\tMTPstring(),\n\t\t\tMTP_long(randomId),\n\t\t\tMTPReplyMarkup(),\n\t\t\tMTPvector<MTPMessageEntity>(),\n\t\t\tMTP_int(action.options.scheduled),\n\t\t\tMTP_int(action.options.scheduleRepeatPeriod),\n\t\t\t(sendAs ? sendAs->input() : MTP_inputPeerEmpty()),\n\t\t\tData::ShortcutIdToMTP(session, action.options.shortcutId),\n\t\t\tMTP_long(action.options.effectId),\n\t\t\tMTP_long(starsPaid),\n\t\t\tSuggestToMTP(action.options.suggest)\n\t\t), [=](const MTPUpdates &result, const MTP::Response &response) {\n\t}, [=](const MTP::Error &error, const MTP::Response &response) {\n\t\tapi->sendMessageFail(error, peer, randomId);\n\t});\n\n\tapi->finishForwarding(action);\n}\n\ntemplate <typename MediaData>\nvoid SendExistingMedia(\n\t\tMessageToSend &&message,\n\t\tnot_null<MediaData*> media,\n\t\tFn<MTPInputMedia()> inputMedia,\n\t\tData::FileOrigin origin,\n\t\tstd::optional<MsgId> localMessageId) {\n\tconst auto history = message.action.history;\n\tconst auto peer = history->peer;\n\tconst auto session = &history->session();\n\tconst auto api = &session->api();\n\n\tmessage.action.clearDraft = false;\n\tmessage.action.generateLocal = true;\n\tapi->sendAction(message.action);\n\n\tconst auto newId = FullMsgId(\n\t\tpeer->id,\n\t\tlocalMessageId\n\t\t\t? (*localMessageId)\n\t\t\t: session->data().nextLocalMessageId());\n\tconst auto randomId = base::RandomValue<uint64>();\n\tauto &action = message.action;\n\n\tauto flags = NewMessageFlags(peer);\n\tauto sendFlags = MTPmessages_SendMedia::Flags(0);\n\tif (action.replyTo) {\n\t\tflags |= MessageFlag::HasReplyInfo;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;\n\t}\n\tconst auto silentPost = ShouldSendSilent(peer, action.options);\n\tInnerFillMessagePostFlags(action.options, peer, flags);\n\tif (silentPost) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_silent;\n\t}\n\tconst auto sendAs = action.options.sendAs;\n\tif (sendAs) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_send_as;\n\t}\n\tauto caption = TextWithEntities{\n\t\tmessage.textWithTags.text,\n\t\tTextUtilities::ConvertTextTagsToEntities(message.textWithTags.tags)\n\t};\n\tTextUtilities::Trim(caption);\n\tauto sentEntities = EntitiesToMTP(\n\t\tsession,\n\t\tcaption.entities,\n\t\tConvertOption::SkipLocal);\n\tif (!sentEntities.v.isEmpty()) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_entities;\n\t}\n\tconst auto captionText = caption.text;\n\tconst auto starsPaid = std::min(\n\t\tpeer->starsPerMessageChecked(),\n\t\taction.options.starsApproved);\n\tif (action.options.scheduled) {\n\t\tflags |= MessageFlag::IsOrWasScheduled;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;\n\t\tif (action.options.scheduleRepeatPeriod) {\n\t\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;\n\t\t}\n\t}\n\tif (action.options.shortcutId) {\n\t\tflags |= MessageFlag::ShortcutMessage;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;\n\t}\n\tif (action.options.effectId) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_effect;\n\t}\n\tif (action.options.suggest) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;\n\t}\n\tif (action.options.invertCaption) {\n\t\tflags |= MessageFlag::InvertMedia;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;\n\t}\n\tif (starsPaid) {\n\t\taction.options.starsApproved -= starsPaid;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;\n\t}\n\n\tsession->data().registerMessageRandomId(randomId, newId);\n\n\thistory->addNewLocalMessage({\n\t\t.id = newId.msg,\n\t\t.flags = flags,\n\t\t.from = NewMessageFromId(action),\n\t\t.replyTo = action.replyTo,\n\t\t.date = NewMessageDate(action.options),\n\t\t.shortcutId = action.options.shortcutId,\n\t\t.starsPaid = starsPaid,\n\t\t.postAuthor = NewMessagePostAuthor(action),\n\t\t.effectId = action.options.effectId,\n\t\t.suggest = HistoryMessageSuggestInfo(action.options),\n\t\t.mediaSpoiler = action.options.mediaSpoiler,\n\t}, media, caption);\n\n\tconst auto performRequest = [=](const auto &repeatRequest) -> void {\n\t\tauto &histories = history->owner().histories();\n\t\tconst auto session = &history->session();\n\t\tconst auto usedFileReference = media->fileReference();\n\t\thistories.sendPreparedMessage(\n\t\t\thistory,\n\t\t\taction.replyTo,\n\t\t\trandomId,\n\t\t\tData::Histories::PrepareMessage<MTPmessages_SendMedia>(\n\t\t\t\tMTP_flags(sendFlags),\n\t\t\t\tpeer->input(),\n\t\t\t\tData::Histories::ReplyToPlaceholder(),\n\t\t\t\tinputMedia(),\n\t\t\t\tMTP_string(captionText),\n\t\t\t\tMTP_long(randomId),\n\t\t\t\tMTPReplyMarkup(),\n\t\t\t\tsentEntities,\n\t\t\t\tMTP_int(action.options.scheduled),\n\t\t\t\tMTP_int(action.options.scheduleRepeatPeriod),\n\t\t\t\t(sendAs ? sendAs->input() : MTP_inputPeerEmpty()),\n\t\t\t\tData::ShortcutIdToMTP(session, action.options.shortcutId),\n\t\t\t\tMTP_long(action.options.effectId),\n\t\t\t\tMTP_long(starsPaid),\n\t\t\t\tSuggestToMTP(action.options.suggest)\n\t\t\t), [=](const MTPUpdates &result, const MTP::Response &response) {\n\t\t}, [=](const MTP::Error &error, const MTP::Response &response) {\n\t\t\tif (error.code() == 400\n\t\t\t\t&& error.type().startsWith(u\"FILE_REFERENCE_\"_q)) {\n\t\t\t\tapi->refreshFileReference(origin, [=](const auto &result) {\n\t\t\t\t\tif (media->fileReference() != usedFileReference) {\n\t\t\t\t\t\trepeatRequest(repeatRequest);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tapi->sendMessageFail(error, peer, randomId, newId);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tapi->sendMessageFail(error, peer, randomId, newId);\n\t\t\t}\n\t\t});\n\t};\n\tperformRequest(performRequest);\n\n\tapi->finishForwarding(action);\n}\n\n} // namespace\n\nvoid SendExistingDocument(\n\t\tMessageToSend &&message,\n\t\tnot_null<DocumentData*> document,\n\t\tData::FileOrigin origin) {\n\tconst auto inputMedia = [=] {\n\t\treturn MTP_inputMediaDocument(\n\t\t\tMTP_flags(0),\n\t\t\tdocument->mtpInput(),\n\t\t\tdocument->goodThumbnailPhoto()\n\t\t\t\t? document->goodThumbnailPhoto()->mtpInput()\n\t\t\t\t: MTPInputPhoto(),\n\t\t\tMTPint(),\n\t\t\tMTPint(), // ttl_seconds\n\t\t\tMTPstring()); // query\n\t};\n\tSendExistingMedia(\n\t\tstd::move(message),\n\t\tdocument,\n\t\tinputMedia,\n\t\torigin,\n\t\tstd::nullopt);\n\n\tif (document->sticker()) {\n\t\tdocument->owner().stickers().incrementSticker(document);\n\t}\n}\n\nvoid SendExistingPhoto(\n\t\tMessageToSend &&message,\n\t\tnot_null<PhotoData*> photo,\n\t\tData::FileOrigin origin) {\n\tconst auto inputMedia = [=] {\n\t\treturn MTP_inputMediaPhoto(\n\t\t\tMTP_flags(0),\n\t\t\tphoto->mtpInput(),\n\t\t\tMTPint(),\n\t\t\tMTPInputDocument());\n\t};\n\tSendExistingMedia(\n\t\tstd::move(message),\n\t\tphoto,\n\t\tinputMedia,\n\t\torigin,\n\t\tstd::nullopt);\n}\n\nvoid SendExistingDocument(\n\t\tMessageToSend &&message,\n\t\tnot_null<DocumentData*> document,\n\t\tstd::optional<MsgId> localMessageId) {\n\tconst auto inputMedia = [=] {\n\t\treturn MTP_inputMediaDocument(\n\t\t\tMTP_flags(message.action.options.mediaSpoiler\n\t\t\t\t? MTPDinputMediaDocument::Flag::f_spoiler\n\t\t\t\t: MTPDinputMediaDocument::Flags(0)),\n\t\t\tdocument->mtpInput(),\n\t\t\tMTPInputPhoto(), // video_cover\n\t\t\tMTPint(), // ttl_seconds\n\t\t\tMTPint(), // video_timestamp\n\t\t\tMTPstring()); // query\n\t};\n\tSendExistingMedia(\n\t\tstd::move(message),\n\t\tdocument,\n\t\tinputMedia,\n\t\tdocument->stickerOrGifOrigin(),\n\t\tstd::move(localMessageId));\n\n\tif (document->sticker()) {\n\t\tdocument->owner().stickers().incrementSticker(document);\n\t}\n}\n\nvoid SendExistingPhoto(\n\t\tMessageToSend &&message,\n\t\tnot_null<PhotoData*> photo,\n\t\tstd::optional<MsgId> localMessageId) {\n\tconst auto inputMedia = [=] {\n\t\treturn MTP_inputMediaPhoto(\n\t\t\tMTP_flags(0),\n\t\t\tphoto->mtpInput(),\n\t\t\tMTPint(), // ttl_seconds\n\t\t\tMTPInputDocument()); // video\n\t};\n\tSendExistingMedia(\n\t\tstd::move(message),\n\t\tphoto,\n\t\tinputMedia,\n\t\tData::FileOrigin(),\n\t\tstd::move(localMessageId));\n}\n\nbool SendDice(MessageToSend &message) {\n\tconst auto full = QStringView(message.textWithTags.text).trimmed();\n\tauto length = 0;\n\tif (!Ui::Emoji::Find(full.data(), full.data() + full.size(), &length)\n\t\t|| length != full.size()\n\t\t|| !message.textWithTags.tags.isEmpty()) {\n\t\treturn false;\n\t}\n\tauto &config = message.action.history->session().appConfig();\n\tstatic const auto hardcoded = std::vector<QString>{\n\t\tStickers::DicePacks::kDiceString,\n\t\tStickers::DicePacks::kDartString,\n\t\tStickers::DicePacks::kSlotString,\n\t\tStickers::DicePacks::kFballString,\n\t\tStickers::DicePacks::kFballString + QChar(0xFE0F),\n\t\tStickers::DicePacks::kBballString,\n\t};\n\tconst auto list = config.get<std::vector<QString>>(\n\t\t\"emojies_send_dice\",\n\t\thardcoded);\n\tconst auto emoji = full.toString();\n\tif (!ranges::contains(list, emoji)) {\n\t\treturn false;\n\t}\n\tconst auto history = message.action.history;\n\tconst auto peer = history->peer;\n\tconst auto session = &history->session();\n\tconst auto api = &session->api();\n\n\tmessage.textWithTags = TextWithTags();\n\tmessage.action.clearDraft = false;\n\tmessage.action.generateLocal = true;\n\n\tauto &action = message.action;\n\tapi->sendAction(action);\n\n\tconst auto newId = FullMsgId(\n\t\tpeer->id,\n\t\tsession->data().nextLocalMessageId());\n\tconst auto randomId = base::RandomValue<uint64>();\n\n\tauto &histories = history->owner().histories();\n\tauto flags = NewMessageFlags(peer);\n\tauto sendFlags = MTPmessages_SendMedia::Flags(0);\n\tif (action.replyTo) {\n\t\tflags |= MessageFlag::HasReplyInfo;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;\n\t}\n\tconst auto silentPost = ShouldSendSilent(peer, action.options);\n\tInnerFillMessagePostFlags(action.options, peer, flags);\n\tif (silentPost) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_silent;\n\t}\n\tconst auto sendAs = action.options.sendAs;\n\tif (sendAs) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_send_as;\n\t}\n\tif (action.options.scheduled) {\n\t\tflags |= MessageFlag::IsOrWasScheduled;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;\n\t\tif (action.options.scheduleRepeatPeriod) {\n\t\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;\n\t\t}\n\t}\n\tif (action.options.shortcutId) {\n\t\tflags |= MessageFlag::ShortcutMessage;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;\n\t}\n\tif (action.options.effectId) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_effect;\n\t}\n\tif (action.options.suggest) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;\n\t}\n\tif (action.options.invertCaption) {\n\t\tflags |= MessageFlag::InvertMedia;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;\n\t}\n\tconst auto starsPaid = std::min(\n\t\tpeer->starsPerMessageChecked(),\n\t\taction.options.starsApproved);\n\tif (starsPaid) {\n\t\taction.options.starsApproved -= starsPaid;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;\n\t}\n\n\tsession->data().registerMessageRandomId(randomId, newId);\n\n\tauto seed = QByteArray(32, Qt::Uninitialized);\n\tbase::RandomFill(bytes::make_detached_span(seed));\n\tconst auto stake = action.options.stakeSeedHash.isEmpty()\n\t\t? 0\n\t\t: action.options.stakeNanoTon;\n\thistory->addNewLocalMessage({\n\t\t.id = newId.msg,\n\t\t.flags = flags,\n\t\t.from = NewMessageFromId(action),\n\t\t.replyTo = action.replyTo,\n\t\t.date = NewMessageDate(action.options),\n\t\t.shortcutId = action.options.shortcutId,\n\t\t.starsPaid = starsPaid,\n\t\t.postAuthor = NewMessagePostAuthor(action),\n\t\t.effectId = action.options.effectId,\n\t\t.suggest = HistoryMessageSuggestInfo(action.options),\n\t}, TextWithEntities(), MTP_messageMediaDice(\n\t\tMTP_flags(stake\n\t\t\t? MTPDmessageMediaDice::Flag::f_game_outcome\n\t\t\t: MTPDmessageMediaDice::Flag()),\n\t\tMTP_int(0),\n\t\tMTP_string(emoji),\n\t\tMTP_messages_emojiGameOutcome(\n\t\t\tMTP_bytes(seed),\n\t\t\tMTP_long(stake),\n\t\t\tMTP_long(0))));\n\thistories.sendPreparedMessage(\n\t\thistory,\n\t\taction.replyTo,\n\t\trandomId,\n\t\tData::Histories::PrepareMessage<MTPmessages_SendMedia>(\n\t\t\tMTP_flags(sendFlags),\n\t\t\tpeer->input(),\n\t\t\tData::Histories::ReplyToPlaceholder(),\n\t\t\t(stake\n\t\t\t\t? MTP_inputMediaStakeDice(\n\t\t\t\t\tMTP_bytes(action.options.stakeSeedHash),\n\t\t\t\t\tMTP_long(stake),\n\t\t\t\t\tMTP_bytes(seed))\n\t\t\t\t: MTP_inputMediaDice(MTP_string(emoji))),\n\t\t\tMTP_string(),\n\t\t\tMTP_long(randomId),\n\t\t\tMTPReplyMarkup(),\n\t\t\tMTP_vector<MTPMessageEntity>(),\n\t\t\tMTP_int(action.options.scheduled),\n\t\t\tMTP_int(action.options.scheduleRepeatPeriod),\n\t\t\t(sendAs ? sendAs->input() : MTP_inputPeerEmpty()),\n\t\t\tData::ShortcutIdToMTP(session, action.options.shortcutId),\n\t\t\tMTP_long(action.options.effectId),\n\t\t\tMTP_long(starsPaid),\n\t\t\tSuggestToMTP(action.options.suggest)\n\t\t), [=](const MTPUpdates &result, const MTP::Response &response) {\n\t}, [=](const MTP::Error &error, const MTP::Response &response) {\n\t\tapi->sendMessageFail(error, peer, randomId, newId);\n\t});\n\tapi->finishForwarding(action);\n\treturn true;\n}\n\nvoid SendLocation(SendAction action, float64 lat, float64 lon) {\n\tSendSimpleMedia(\n\t\taction,\n\t\tMTP_inputMediaGeoPoint(\n\t\t\tMTP_inputGeoPoint(\n\t\t\t\tMTP_flags(0),\n\t\t\t\tMTP_double(lat),\n\t\t\t\tMTP_double(lon),\n\t\t\t\tMTPint()))); // accuracy_radius\n}\n\nvoid SendVenue(SendAction action, Data::InputVenue venue) {\n\tSendSimpleMedia(\n\t\taction,\n\t\tMTP_inputMediaVenue(\n\t\t\tMTP_inputGeoPoint(\n\t\t\t\tMTP_flags(0),\n\t\t\t\tMTP_double(venue.lat),\n\t\t\t\tMTP_double(venue.lon),\n\t\t\t\tMTPint()), // accuracy_radius\n\t\t\tMTP_string(venue.title),\n\t\t\tMTP_string(venue.address),\n\t\t\tMTP_string(venue.provider),\n\t\t\tMTP_string(venue.id),\n\t\t\tMTP_string(venue.venueType)));\n}\n\nvoid FillMessagePostFlags(\n\t\tconst SendAction &action,\n\t\tnot_null<PeerData*> peer,\n\t\tMessageFlags &flags) {\n\tInnerFillMessagePostFlags(action.options, peer, flags);\n}\n\nvoid SendConfirmedFile(\n\t\tnot_null<Main::Session*> session,\n\t\tconst std::shared_ptr<FilePrepareResult> &file) {\n\tconst auto isEditing = (file->type != SendMediaType::Audio)\n\t\t&& (file->type != SendMediaType::Round)\n\t\t&& (file->to.replaceMediaOf != 0);\n\tconst auto newId = FullMsgId(\n\t\tfile->to.peer,\n\t\t(isEditing\n\t\t\t? file->to.replaceMediaOf\n\t\t\t: session->data().nextLocalMessageId()));\n\tconst auto groupId = file->album ? file->album->groupId : uint64(0);\n\tif (file->album) {\n\t\tconst auto proj = [](const SendingAlbum::Item &item) {\n\t\t\treturn item.taskId;\n\t\t};\n\t\tconst auto it = ranges::find(file->album->items, file->taskId, proj);\n\t\tAssert(it != file->album->items.end());\n\n\t\tit->msgId = newId;\n\t}\n\n\tconst auto itemToEdit = isEditing\n\t\t? session->data().message(newId)\n\t\t: nullptr;\n\tconst auto history = session->data().history(file->to.peer);\n\tconst auto peer = history->peer;\n\n\tif (!isEditing) {\n\t\tconst auto histories = &session->data().histories();\n\t\tfile->to.replyTo.messageId = histories->convertTopicReplyToId(\n\t\t\thistory,\n\t\t\tfile->to.replyTo.messageId);\n\t\tfile->to.replyTo.topicRootId = histories->convertTopicReplyToId(\n\t\t\thistory,\n\t\t\tfile->to.replyTo.topicRootId);\n\t}\n\n\tsession->uploader().upload(newId, file);\n\n\tauto action = SendAction(history, file->to.options);\n\taction.clearDraft = false;\n\taction.replyTo = file->to.replyTo;\n\taction.generateLocal = true;\n\taction.replaceMediaOf = file->to.replaceMediaOf;\n\tsession->api().sendAction(action);\n\n\tauto caption = TextWithEntities{\n\t\tfile->caption.text,\n\t\tTextUtilities::ConvertTextTagsToEntities(file->caption.tags)\n\t};\n\tconst auto prepareFlags = Ui::ItemTextOptions(\n\t\thistory,\n\t\tsession->user()).flags;\n\tTextUtilities::PrepareForSending(caption, prepareFlags);\n\tTextUtilities::Trim(caption);\n\n\tauto flags = isEditing ? MessageFlags() : NewMessageFlags(peer);\n\tif (file->to.replyTo) {\n\t\tflags |= MessageFlag::HasReplyInfo;\n\t}\n\tFillMessagePostFlags(action, peer, flags);\n\tif (file->to.options.scheduled) {\n\t\tflags |= MessageFlag::IsOrWasScheduled;\n\n\t\t// Scheduled messages have no 'edited' badge.\n\t\tflags |= MessageFlag::HideEdited;\n\t}\n\tif (file->to.options.shortcutId) {\n\t\tflags |= MessageFlag::ShortcutMessage;\n\n\t\t// Shortcut messages have no 'edited' badge.\n\t\tflags |= MessageFlag::HideEdited;\n\t}\n\tif (file->type == SendMediaType::Audio\n\t\t|| file->type == SendMediaType::Round) {\n\t\tif (!peer->isChannel() || peer->isMegagroup()) {\n\t\t\tflags |= MessageFlag::MediaIsUnread;\n\t\t}\n\t}\n\tif (file->to.options.invertCaption) {\n\t\tflags |= MessageFlag::InvertMedia;\n\t}\n\tconst auto media = MTPMessageMedia([&] {\n\t\tif (file->type == SendMediaType::Photo) {\n\t\t\tusing Flag = MTPDmessageMediaPhoto::Flag;\n\t\t\treturn MTP_messageMediaPhoto(\n\t\t\t\tMTP_flags(Flag::f_photo\n\t\t\t\t\t| (file->spoiler ? Flag::f_spoiler : Flag())),\n\t\t\t\tfile->photo,\n\t\t\t\tMTPint(), // ttl_seconds\n\t\t\t\tMTPDocument()); // video\n\t\t} else if (file->type == SendMediaType::File) {\n\t\t\tusing Flag = MTPDmessageMediaDocument::Flag;\n\t\t\treturn MTP_messageMediaDocument(\n\t\t\t\tMTP_flags(Flag::f_document\n\t\t\t\t\t| (file->spoiler ? Flag::f_spoiler : Flag())\n\t\t\t\t\t| (file->videoCover ? Flag::f_video_cover : Flag())),\n\t\t\t\tfile->document,\n\t\t\t\tMTPVector<MTPDocument>(), // alt_documents\n\t\t\t\tfile->videoCover ? file->videoCover->photo : MTPPhoto(),\n\t\t\t\tMTPint(), // video_timestamp\n\t\t\t\tMTPint());\n\t\t} else if (file->type == SendMediaType::Audio) {\n\t\t\tconst auto ttlSeconds = file->to.options.ttlSeconds;\n\t\t\tusing Flag = MTPDmessageMediaDocument::Flag;\n\t\t\treturn MTP_messageMediaDocument(\n\t\t\t\tMTP_flags(Flag::f_document\n\t\t\t\t\t| Flag::f_voice\n\t\t\t\t\t| (ttlSeconds ? Flag::f_ttl_seconds : Flag())\n\t\t\t\t\t| (file->videoCover ? Flag::f_video_cover : Flag())),\n\t\t\t\tfile->document,\n\t\t\t\tMTPVector<MTPDocument>(), // alt_documents\n\t\t\t\tfile->videoCover ? file->videoCover->photo : MTPPhoto(),\n\t\t\t\tMTPint(), // video_timestamp\n\t\t\t\tMTP_int(ttlSeconds));\n\t\t} else if (file->type == SendMediaType::Round) {\n\t\t\tusing Flag = MTPDmessageMediaDocument::Flag;\n\t\t\tconst auto ttlSeconds = file->to.options.ttlSeconds;\n\t\t\treturn MTP_messageMediaDocument(\n\t\t\t\tMTP_flags(Flag::f_document\n\t\t\t\t\t| Flag::f_round\n\t\t\t\t\t| (ttlSeconds ? Flag::f_ttl_seconds : Flag())\n\t\t\t\t\t| (file->spoiler ? Flag::f_spoiler : Flag())),\n\t\t\t\tfile->document,\n\t\t\t\tMTPVector<MTPDocument>(), // alt_documents\n\t\t\t\tMTPPhoto(), // video_cover\n\t\t\t\tMTPint(), // video_timestamp\n\t\t\t\tMTP_int(ttlSeconds));\n\t\t} else {\n\t\t\tUnexpected(\"Type in sendFilesConfirmed.\");\n\t\t}\n\t}());\n\n\tif (itemToEdit) {\n\t\tauto edition = HistoryMessageEdition();\n\t\tedition.isEditHide = (flags & MessageFlag::HideEdited);\n\t\tedition.editDate = 0;\n\t\tedition.ttl = 0;\n\t\tedition.mtpMedia = &media;\n\t\tedition.textWithEntities = caption;\n\t\tedition.invertMedia = file->to.options.invertCaption;\n\t\tedition.useSameViews = true;\n\t\tedition.useSameForwards = true;\n\t\tedition.useSameMarkup = true;\n\t\tedition.useSameReplies = true;\n\t\tedition.useSameReactions = true;\n\t\tedition.useSameSuggest = true;\n\t\tedition.savePreviousMedia = true;\n\t\titemToEdit->applyEdition(std::move(edition));\n\t} else {\n\t\thistory->addNewLocalMessage({\n\t\t\t.id = newId.msg,\n\t\t\t.flags = flags,\n\t\t\t.from = NewMessageFromId(action),\n\t\t\t.replyTo = file->to.replyTo,\n\t\t\t.date = NewMessageDate(file->to.options),\n\t\t\t.shortcutId = file->to.options.shortcutId,\n\t\t\t.starsPaid = std::min(\n\t\t\t\thistory->peer->starsPerMessageChecked(),\n\t\t\t\tfile->to.options.starsApproved),\n\t\t\t.postAuthor = NewMessagePostAuthor(action),\n\t\t\t.groupedId = groupId,\n\t\t\t.effectId = file->to.options.effectId,\n\t\t\t.suggest = HistoryMessageSuggestInfo(file->to.options),\n\t\t}, caption, media);\n\t}\n\n\tif (isEditing) {\n\t\treturn;\n\t}\n\n\tsession->data().sendHistoryChangeNotifications();\n\tif (!itemToEdit) {\n\t\tsession->changes().historyUpdated(\n\t\t\thistory,\n\t\t\t(action.options.scheduled\n\t\t\t\t? Data::HistoryUpdate::Flag::ScheduledSent\n\t\t\t\t: Data::HistoryUpdate::Flag::MessageSent));\n\t}\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_sending.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_file_origin.h\"\n\nclass History;\nclass PhotoData;\nclass DocumentData;\nstruct FilePrepareResult;\n\nnamespace Data {\nstruct InputVenue;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nstruct MessageToSend;\nstruct SendAction;\n\nvoid SendExistingDocument(\n\tMessageToSend &&message,\n\tnot_null<DocumentData*> document,\n\tstd::optional<MsgId> localMessageId = std::nullopt);\n\nvoid SendExistingPhoto(\n\tMessageToSend &&message,\n\tnot_null<PhotoData*> photo,\n\tstd::optional<MsgId> localMessageId = std::nullopt);\n\nvoid SendExistingDocument(\n\tMessageToSend &&message,\n\tnot_null<DocumentData*> document,\n\tData::FileOrigin origin);\n\nvoid SendExistingPhoto(\n\tMessageToSend &&message,\n\tnot_null<PhotoData*> photo,\n\tData::FileOrigin origin);\n\nbool SendDice(MessageToSend &message);\n\n// We can't create Data::LocationPoint() and use it\n// for a local sending message, because we can't request\n// map thumbnail in messages history without access hash.\nvoid SendLocation(SendAction action, float64 lat, float64 lon);\n\nvoid SendVenue(SendAction action, Data::InputVenue venue);\n\nvoid FillMessagePostFlags(\n\tconst SendAction &action,\n\tnot_null<PeerData*> peer,\n\tMessageFlags &flags);\n\nvoid SendConfirmedFile(\n\tnot_null<Main::Session*> session,\n\tconst std::shared_ptr<FilePrepareResult> &file);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_sensitive_content.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_sensitive_content.h\"\n\n#include \"apiwrap.h\"\n#include \"main/main_session.h\"\n#include \"main/main_app_config.h\"\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kRefreshAppConfigTimeout = crl::time(1);\n\n} // namespace\n\nSensitiveContent::SensitiveContent(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance())\n, _appConfigReloadTimer([=] { _session->appConfig().refresh(); }) {\n}\n\nvoid SensitiveContent::preload() {\n\tif (!_loaded && !_loadRequestId) {\n\t\treload();\n\t}\n}\n\nvoid SensitiveContent::reload(bool force) {\n\tif (_loadRequestId) {\n\t\tif (force) {\n\t\t\t_loadPending = true;\n\t\t}\n\t\treturn;\n\t}\n\t_loadRequestId = _api.request(MTPaccount_GetContentSettings(\n\t)).done([=](const MTPaccount_ContentSettings &result) {\n\t\t_loadRequestId = 0;\n\t\tconst auto &data = result.data();\n\t\tconst auto enabled = data.is_sensitive_enabled();\n\t\tconst auto canChange = data.is_sensitive_can_change();\n\t\tconst auto changed = (_enabled.current() != enabled)\n\t\t\t|| (_canChange.current() != canChange);\n\t\tif (changed) {\n\t\t\t_enabled = enabled;\n\t\t\t_canChange = canChange;\n\t\t}\n\t\tif (!_loaded) {\n\t\t\t_loaded = true;\n\t\t\t_loadedChanged.fire({});\n\t\t}\n\t\tif (base::take(_appConfigReloadForce) || changed) {\n\t\t\t_appConfigReloadTimer.callOnce(kRefreshAppConfigTimeout);\n\t\t}\n\t\tif (base::take(_loadPending)) {\n\t\t\treload();\n\t\t}\n\t}).fail([=] {\n\t\t_loadRequestId = 0;\n\t}).send();\n}\n\nbool SensitiveContent::loaded() const {\n\treturn _loaded;\n}\n\nrpl::producer<bool> SensitiveContent::loadedValue() const {\n\tif (_loaded) {\n\t\treturn rpl::single(true);\n\t}\n\treturn rpl::single(false) | rpl::then(\n\t\t_loadedChanged.events() | rpl::map_to(true)\n\t);\n}\n\nbool SensitiveContent::enabledCurrent() const {\n\treturn _enabled.current();\n}\n\nrpl::producer<bool> SensitiveContent::enabled() const {\n\treturn _enabled.value();\n}\n\nbool SensitiveContent::canChangeCurrent() const {\n\treturn _canChange.current();\n}\n\nrpl::producer<bool> SensitiveContent::canChange() const {\n\treturn _canChange.value();\n}\n\nvoid SensitiveContent::update(bool enabled) {\n\tif (!_canChange.current()) {\n\t\treturn;\n\t}\n\tusing Flag = MTPaccount_SetContentSettings::Flag;\n\t_api.request(_saveRequestId).cancel();\n\tif (const auto load = base::take(_loadRequestId)) {\n\t\t_api.request(load).cancel();\n\t\t_loadPending = true;\n\t}\n\tconst auto finish = [=] {\n\t\t_saveRequestId = 0;\n\t\tif (base::take(_loadPending)) {\n\t\t\t_appConfigReloadForce = true;\n\t\t\treload(true);\n\t\t} else {\n\t\t\t_appConfigReloadTimer.callOnce(kRefreshAppConfigTimeout);\n\t\t}\n\t};\n\t_saveRequestId = _api.request(MTPaccount_SetContentSettings(\n\t\tMTP_flags(enabled ? Flag::f_sensitive_enabled : Flag(0))\n\t)).done(finish).fail(finish).send();\n\t_enabled = enabled;\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_sensitive_content.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n#include \"base/timer.h\"\n\nclass ApiWrap;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nclass SensitiveContent final {\npublic:\n\texplicit SensitiveContent(not_null<ApiWrap*> api);\n\n\tvoid preload();\n\tvoid reload(bool force = false);\n\tvoid update(bool enabled);\n\n\t[[nodiscard]] bool loaded() const;\n\t[[nodiscard]] rpl::producer<bool> loadedValue() const;\n\t[[nodiscard]] bool enabledCurrent() const;\n\t[[nodiscard]] rpl::producer<bool> enabled() const;\n\t[[nodiscard]] bool canChangeCurrent() const;\n\t[[nodiscard]] rpl::producer<bool> canChange() const;\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\trpl::event_stream<> _loadedChanged;\n\tMTP::Sender _api;\n\tmtpRequestId _loadRequestId = 0;\n\tmtpRequestId _saveRequestId = 0;\n\trpl::variable<bool> _enabled = false;\n\trpl::variable<bool> _canChange = false;\n\tbase::Timer _appConfigReloadTimer;\n\tbool _appConfigReloadForce = false;\n\tbool _loadPending = false;\n\tbool _loaded = false;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_single_message_search.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_single_message_search.h\"\n\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_search_controller.h\"\n#include \"core/local_url_handlers.h\"\n#include \"history/history_item.h\"\n#include \"base/qthelp_url.h\"\n#include \"apiwrap.h\"\n\nnamespace Api {\nnamespace {\n\nusing Key = details::SingleMessageSearchKey;\n\nKey ExtractKey(const QString &query) {\n\tconst auto trimmed = query.trimmed();\n\tconst auto local = Core::TryConvertUrlToLocal(trimmed);\n\tconst auto check = local.isEmpty() ? trimmed : local;\n\tconst auto parse = [&] {\n\t\tconst auto delimeter = check.indexOf('?');\n\t\treturn (delimeter > 0)\n\t\t\t? qthelp::url_parse_params(\n\t\t\t\tcheck.mid(delimeter + 1),\n\t\t\t\tqthelp::UrlParamNameTransform::ToLower)\n\t\t\t: QMap<QString, QString>();\n\t};\n\tif (check.startsWith(u\"tg://privatepost\"_q, Qt::CaseInsensitive)) {\n\t\tconst auto params = parse();\n\t\tconst auto channel = params.value(\"channel\");\n\t\tconst auto post = params.value(\"post\").toInt();\n\t\treturn (channel.toULongLong() && post) ? Key{ channel, post } : Key();\n\t} else if (check.startsWith(u\"tg://resolve\"_q, Qt::CaseInsensitive)) {\n\t\tconst auto params = parse();\n\t\tconst auto domain = params.value(\"domain\");\n\t\tconst auto post = params.value(\"post\").toInt();\n\t\treturn (!domain.isEmpty() && post) ? Key{ domain, post } : Key();\n\t}\n\treturn Key();\n}\n\n} // namespace\n\nSingleMessageSearch::SingleMessageSearch(not_null<Main::Session*> session)\n: _session(session) {\n}\n\nSingleMessageSearch::~SingleMessageSearch() {\n\tclear();\n}\n\nvoid SingleMessageSearch::clear() {\n\t_cache.clear();\n\t_requestKey = Key();\n\t_session->api().request(base::take(_requestId)).cancel();\n}\n\nstd::optional<HistoryItem*> SingleMessageSearch::lookup(\n\t\tconst QString &query,\n\t\tFn<void()> ready) {\n\tconst auto key = ExtractKey(query);\n\tif (!key) {\n\t\treturn nullptr;\n\t}\n\tconst auto i = _cache.find(key);\n\tif (i != end(_cache)) {\n\t\treturn _session->data().message(i->second);\n\t}\n\tif (!(_requestKey == key)) {\n\t\t_session->api().request(base::take(_requestId)).cancel();\n\t\t_requestKey = key;\n\t}\n\treturn performLookup(ready);\n}\n\nstd::optional<HistoryItem*> SingleMessageSearch::performLookupByChannel(\n\t\tnot_null<ChannelData*> channel,\n\t\tFn<void()> ready) {\n\tExpects(!_requestKey.empty());\n\n\tconst auto postId = _requestKey.postId;\n\tif (const auto item = _session->data().message(channel->id, postId)) {\n\t\t_cache.emplace(_requestKey, item->fullId());\n\t\treturn item;\n\t} else if (!ready) {\n\t\treturn nullptr;\n\t}\n\n\tconst auto fail = [=] {\n\t\t_cache.emplace(_requestKey, FullMsgId());\n\t\tready();\n\t};\n\t_requestId = _session->api().request(MTPchannels_GetMessages(\n\t\tchannel->inputChannel(),\n\t\tMTP_vector<MTPInputMessage>(1, MTP_inputMessageID(MTP_int(postId)))\n\t)).done([=](const MTPmessages_Messages &result) {\n\t\tconst auto received = Api::ParseSearchResult(\n\t\t\tchannel,\n\t\t\tStorage::SharedMediaType::kCount,\n\t\t\tpostId,\n\t\t\tData::LoadDirection::Around,\n\t\t\tresult);\n\t\tif (!received.messageIds.empty()\n\t\t\t&& received.messageIds.front() == postId) {\n\t\t\t_cache.emplace(\n\t\t\t\t_requestKey,\n\t\t\t\tFullMsgId(channel->id, postId));\n\t\t\tready();\n\t\t} else {\n\t\t\tfail();\n\t\t}\n\t}).fail([=] {\n\t\tfail();\n\t}).send();\n\n\treturn std::nullopt;\n}\n\nstd::optional<HistoryItem*> SingleMessageSearch::performLookupById(\n\t\tChannelId channelId,\n\t\tFn<void()> ready) {\n\tExpects(!_requestKey.empty());\n\n\tif (const auto channel = _session->data().channelLoaded(channelId)) {\n\t\treturn performLookupByChannel(channel, ready);\n\t} else if (!ready) {\n\t\treturn nullptr;\n\t}\n\n\tconst auto fail = [=] {\n\t\t_cache.emplace(_requestKey, FullMsgId());\n\t\tready();\n\t};\n\t_requestId = _session->api().request(MTPchannels_GetChannels(\n\t\tMTP_vector<MTPInputChannel>(\n\t\t\t1,\n\t\t\tMTP_inputChannel(MTP_long(channelId.bare), MTP_long(0)))\n\t)).done([=](const MTPmessages_Chats &result) {\n\t\tresult.match([&](const auto &data) {\n\t\t\tconst auto peer = _session->data().processChats(data.vchats());\n\t\t\tif (peer && peer->id == peerFromChannel(channelId)) {\n\t\t\t\tif (performLookupByChannel(peer->asChannel(), ready)) {\n\t\t\t\t\tready();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfail();\n\t\t\t}\n\t\t});\n\t}).fail([=] {\n\t\tfail();\n\t}).send();\n\n\treturn std::nullopt;\n}\n\nstd::optional<HistoryItem*> SingleMessageSearch::performLookupByUsername(\n\t\tconst QString &username,\n\t\tFn<void()> ready) {\n\tExpects(!_requestKey.empty());\n\n\tif (const auto peer = _session->data().peerByUsername(username)) {\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\treturn performLookupByChannel(channel, ready);\n\t\t}\n\t\t_cache.emplace(_requestKey, FullMsgId());\n\t\treturn nullptr;\n\t} else if (!ready) {\n\t\treturn nullptr;\n\t}\n\n\tconst auto fail = [=] {\n\t\t_cache.emplace(_requestKey, FullMsgId());\n\t\tready();\n\t};\n\t_requestId = _session->api().request(MTPcontacts_ResolveUsername(\n\t\tMTP_flags(0),\n\t\tMTP_string(username),\n\t\tMTP_string()\n\t)).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\tresult.match([&](const MTPDcontacts_resolvedPeer &data) {\n\t\t\t_session->data().processUsers(data.vusers());\n\t\t\t_session->data().processChats(data.vchats());\n\t\t\tconst auto peerId = peerFromMTP(data.vpeer());\n\t\t\tconst auto peer = peerId\n\t\t\t\t? _session->data().peerLoaded(peerId)\n\t\t\t\t: nullptr;\n\t\t\tif (const auto channel = peer ? peer->asChannel() : nullptr) {\n\t\t\t\tif (performLookupByChannel(channel, ready)) {\n\t\t\t\t\tready();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfail();\n\t\t\t}\n\t\t});\n\t}).fail([=] {\n\t\tfail();\n\t}).send();\n\n\treturn std::nullopt;\n}\n\nstd::optional<HistoryItem*> SingleMessageSearch::performLookup(\n\t\tFn<void()> ready) {\n\tExpects(!_requestKey.empty());\n\n\tif (!_requestKey.domainOrId[0].isDigit()) {\n\t\treturn performLookupByUsername(_requestKey.domainOrId, ready);\n\t}\n\tconst auto channelId = ChannelId(_requestKey.domainOrId.toULongLong());\n\treturn performLookupById(channelId, ready);\n}\n\nQString ConvertPeerSearchQuery(const QString &query) {\n\tconst auto trimmed = query.trimmed();\n\tconst auto local = Core::TryConvertUrlToLocal(trimmed);\n\tconst auto check = local.isEmpty() ? trimmed : local;\n\tif (!check.startsWith(u\"tg://resolve\"_q, Qt::CaseInsensitive)) {\n\t\treturn query;\n\t}\n\tconst auto delimeter = check.indexOf('?');\n\tconst auto params = (delimeter > 0)\n\t\t? qthelp::url_parse_params(\n\t\t\tcheck.mid(delimeter + 1),\n\t\t\tqthelp::UrlParamNameTransform::ToLower)\n\t\t: QMap<QString, QString>();\n\treturn params.value(\"domain\", query);\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_single_message_search.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\nnamespace details {\n\nstruct SingleMessageSearchKey {\n\tQString domainOrId;\n\tMsgId postId = 0;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn domainOrId.isEmpty() || !postId;\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n\t[[nodiscard]] bool operator<(const SingleMessageSearchKey &other) const {\n\t\treturn std::tie(domainOrId, postId)\n\t\t\t< std::tie(other.domainOrId, other.postId);\n\t}\n\t[[nodiscard]] bool operator==(\n\t\t\tconst SingleMessageSearchKey &other) const {\n\t\treturn std::tie(domainOrId, postId)\n\t\t\t== std::tie(other.domainOrId, other.postId);\n\t}\n};\n\n} // namespace details\n\nclass SingleMessageSearch {\npublic:\n\texplicit SingleMessageSearch(not_null<Main::Session*> session);\n\t~SingleMessageSearch();\n\n\tvoid clear();\n\n\t// If 'ready' callback is empty, the result must not be 'nullopt'.\n\t[[nodiscard]] std::optional<HistoryItem*> lookup(\n\t\tconst QString &query,\n\t\tFn<void()> ready = nullptr);\n\nprivate:\n\tusing Key = details::SingleMessageSearchKey;\n\n\t[[nodiscard]] std::optional<HistoryItem*> performLookup(\n\t\tFn<void()> ready);\n\t[[nodiscard]] std::optional<HistoryItem*> performLookupById(\n\t\tChannelId channelId,\n\t\tFn<void()> ready);\n\t[[nodiscard]] std::optional<HistoryItem*> performLookupByUsername(\n\t\tconst QString &username,\n\t\tFn<void()> ready);\n\t[[nodiscard]] std::optional<HistoryItem*> performLookupByChannel(\n\t\tnot_null<ChannelData*> channel,\n\t\tFn<void()> ready);\n\n\tconst not_null<Main::Session*> _session;\n\tstd::map<Key, FullMsgId> _cache;\n\tmtpRequestId _requestId = 0;\n\tKey _requestKey;\n\n};\n\n[[nodiscard]] QString ConvertPeerSearchQuery(const QString &query);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_statistics.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_statistics.h\"\n\n#include \"api/api_credits_history_entry.h\"\n#include \"api/api_statistics_data_deserialize.h\"\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_story.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n\nnamespace Api {\nnamespace {\n\n[[nodiscard]] Data::StatisticalValue StatisticalValueFromTL(\n\t\tconst MTPStatsAbsValueAndPrev &tl) {\n\tconst auto current = tl.data().vcurrent().v;\n\tconst auto previous = tl.data().vprevious().v;\n\treturn Data::StatisticalValue{\n\t\t.value = current,\n\t\t.previousValue = previous,\n\t\t.growthRatePercentage = previous\n\t\t\t? std::abs((current - previous) / float64(previous) * 100.)\n\t\t\t: 0,\n\t};\n}\n\n[[nodiscard]] Data::ChannelStatistics ChannelStatisticsFromTL(\n\t\tconst MTPDstats_broadcastStats &data) {\n\tconst auto &tlUnmuted = data.venabled_notifications().data();\n\tconst auto unmuted = (!tlUnmuted.vtotal().v)\n\t\t? 0.\n\t\t: std::clamp(\n\t\t\ttlUnmuted.vpart().v / tlUnmuted.vtotal().v * 100.,\n\t\t\t0.,\n\t\t\t100.);\n\tusing Recent = MTPPostInteractionCounters;\n\tauto recentMessages = ranges::views::all(\n\t\tdata.vrecent_posts_interactions().v\n\t) | ranges::views::transform([&](const Recent &tl) {\n\t\treturn tl.match([&](const MTPDpostInteractionCountersStory &data) {\n\t\t\treturn Data::StatisticsMessageInteractionInfo{\n\t\t\t\t.storyId = data.vstory_id().v,\n\t\t\t\t.viewsCount = data.vviews().v,\n\t\t\t\t.forwardsCount = data.vforwards().v,\n\t\t\t\t.reactionsCount = data.vreactions().v,\n\t\t\t};\n\t\t}, [&](const MTPDpostInteractionCountersMessage &data) {\n\t\t\treturn Data::StatisticsMessageInteractionInfo{\n\t\t\t\t.messageId = data.vmsg_id().v,\n\t\t\t\t.viewsCount = data.vviews().v,\n\t\t\t\t.forwardsCount = data.vforwards().v,\n\t\t\t\t.reactionsCount = data.vreactions().v,\n\t\t\t};\n\t\t});\n\t}) | ranges::to_vector;\n\n\treturn {\n\t\t.startDate = data.vperiod().data().vmin_date().v,\n\t\t.endDate = data.vperiod().data().vmax_date().v,\n\n\t\t.memberCount = StatisticalValueFromTL(data.vfollowers()),\n\t\t.meanViewCount = StatisticalValueFromTL(data.vviews_per_post()),\n\t\t.meanShareCount = StatisticalValueFromTL(data.vshares_per_post()),\n\t\t.meanReactionCount = StatisticalValueFromTL(\n\t\t\tdata.vreactions_per_post()),\n\n\t\t.meanStoryViewCount = StatisticalValueFromTL(\n\t\t\tdata.vviews_per_story()),\n\t\t.meanStoryShareCount = StatisticalValueFromTL(\n\t\t\tdata.vshares_per_story()),\n\t\t.meanStoryReactionCount = StatisticalValueFromTL(\n\t\t\tdata.vreactions_per_story()),\n\n\t\t.enabledNotificationsPercentage = unmuted,\n\n\t\t.memberCountGraph = StatisticalGraphFromTL(\n\t\t\tdata.vgrowth_graph()),\n\n\t\t.joinGraph = StatisticalGraphFromTL(\n\t\t\tdata.vfollowers_graph()),\n\n\t\t.muteGraph = StatisticalGraphFromTL(\n\t\t\tdata.vmute_graph()),\n\n\t\t.viewCountByHourGraph = StatisticalGraphFromTL(\n\t\t\tdata.vtop_hours_graph()),\n\n\t\t.viewCountBySourceGraph = StatisticalGraphFromTL(\n\t\t\tdata.vviews_by_source_graph()),\n\n\t\t.joinBySourceGraph = StatisticalGraphFromTL(\n\t\t\tdata.vnew_followers_by_source_graph()),\n\n\t\t.languageGraph = StatisticalGraphFromTL(\n\t\t\tdata.vlanguages_graph()),\n\n\t\t.messageInteractionGraph = StatisticalGraphFromTL(\n\t\t\tdata.vinteractions_graph()),\n\n\t\t.instantViewInteractionGraph = StatisticalGraphFromTL(\n\t\t\tdata.viv_interactions_graph()),\n\n\t\t.reactionsByEmotionGraph = StatisticalGraphFromTL(\n\t\t\tdata.vreactions_by_emotion_graph()),\n\n\t\t.storyInteractionsGraph = StatisticalGraphFromTL(\n\t\t\tdata.vstory_interactions_graph()),\n\n\t\t.storyReactionsByEmotionGraph = StatisticalGraphFromTL(\n\t\t\tdata.vstory_reactions_by_emotion_graph()),\n\n\t\t.recentMessageInteractions = std::move(recentMessages),\n\t};\n}\n\n[[nodiscard]] Data::SupergroupStatistics SupergroupStatisticsFromTL(\n\t\tconst MTPDstats_megagroupStats &data) {\n\tusing Senders = MTPStatsGroupTopPoster;\n\tusing Administrators = MTPStatsGroupTopAdmin;\n\tusing Inviters = MTPStatsGroupTopInviter;\n\n\tauto topSenders = ranges::views::all(\n\t\tdata.vtop_posters().v\n\t) | ranges::views::transform([&](const Senders &tl) {\n\t\treturn Data::StatisticsMessageSenderInfo{\n\t\t\t.userId = UserId(tl.data().vuser_id().v),\n\t\t\t.sentMessageCount = tl.data().vmessages().v,\n\t\t\t.averageCharacterCount = tl.data().vavg_chars().v,\n\t\t};\n\t}) | ranges::to_vector;\n\tauto topAdministrators = ranges::views::all(\n\t\tdata.vtop_admins().v\n\t) | ranges::views::transform([&](const Administrators &tl) {\n\t\treturn Data::StatisticsAdministratorActionsInfo{\n\t\t\t.userId = UserId(tl.data().vuser_id().v),\n\t\t\t.deletedMessageCount = tl.data().vdeleted().v,\n\t\t\t.bannedUserCount = tl.data().vkicked().v,\n\t\t\t.restrictedUserCount = tl.data().vbanned().v,\n\t\t};\n\t}) | ranges::to_vector;\n\tauto topInviters = ranges::views::all(\n\t\tdata.vtop_inviters().v\n\t) | ranges::views::transform([&](const Inviters &tl) {\n\t\treturn Data::StatisticsInviterInfo{\n\t\t\t.userId = UserId(tl.data().vuser_id().v),\n\t\t\t.addedMemberCount = tl.data().vinvitations().v,\n\t\t};\n\t}) | ranges::to_vector;\n\n\treturn {\n\t\t.startDate = data.vperiod().data().vmin_date().v,\n\t\t.endDate = data.vperiod().data().vmax_date().v,\n\n\t\t.memberCount = StatisticalValueFromTL(data.vmembers()),\n\t\t.messageCount = StatisticalValueFromTL(data.vmessages()),\n\t\t.viewerCount = StatisticalValueFromTL(data.vviewers()),\n\t\t.senderCount = StatisticalValueFromTL(data.vposters()),\n\n\t\t.memberCountGraph = StatisticalGraphFromTL(\n\t\t\tdata.vgrowth_graph()),\n\n\t\t.joinGraph = StatisticalGraphFromTL(\n\t\t\tdata.vmembers_graph()),\n\n\t\t.joinBySourceGraph = StatisticalGraphFromTL(\n\t\t\tdata.vnew_members_by_source_graph()),\n\n\t\t.languageGraph = StatisticalGraphFromTL(\n\t\t\tdata.vlanguages_graph()),\n\n\t\t.messageContentGraph = StatisticalGraphFromTL(\n\t\t\tdata.vmessages_graph()),\n\n\t\t.actionGraph = StatisticalGraphFromTL(\n\t\t\tdata.vactions_graph()),\n\n\t\t.dayGraph = StatisticalGraphFromTL(\n\t\t\tdata.vtop_hours_graph()),\n\n\t\t.weekGraph = StatisticalGraphFromTL(\n\t\t\tdata.vweekdays_graph()),\n\n\t\t.topSenders = std::move(topSenders),\n\t\t.topAdministrators = std::move(topAdministrators),\n\t\t.topInviters = std::move(topInviters),\n\t};\n}\n\n} // namespace\n\nStatistics::Statistics(not_null<ChannelData*> channel)\n: StatisticsRequestSender(channel) {\n}\n\nrpl::producer<rpl::no_value, QString> Statistics::request() {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tif (!channel()->isMegagroup()) {\n\t\t\tmakeRequest(MTPstats_GetBroadcastStats(\n\t\t\t\tMTP_flags(MTPstats_GetBroadcastStats::Flags(0)),\n\t\t\t\tchannel()->inputChannel()\n\t\t\t)).done([=](const MTPstats_BroadcastStats &result) {\n\t\t\t\t_channelStats = ChannelStatisticsFromTL(result.data());\n\t\t\t\tconsumer.put_done();\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tconsumer.put_error_copy(error.type());\n\t\t\t}).send();\n\t\t} else {\n\t\t\tmakeRequest(MTPstats_GetMegagroupStats(\n\t\t\t\tMTP_flags(MTPstats_GetMegagroupStats::Flags(0)),\n\t\t\t\tchannel()->inputChannel()\n\t\t\t)).done([=](const MTPstats_MegagroupStats &result) {\n\t\t\t\tconst auto &data = result.data();\n\t\t\t\t_supergroupStats = SupergroupStatisticsFromTL(data);\n\t\t\t\tchannel()->owner().processUsers(data.vusers());\n\t\t\t\tconsumer.put_done();\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tconsumer.put_error_copy(error.type());\n\t\t\t}).send();\n\t\t}\n\n\t\treturn lifetime;\n\t};\n}\n\nStatistics::GraphResult Statistics::requestZoom(\n\t\tconst QString &token,\n\t\tfloat64 x) {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tconst auto wasEmpty = _zoomDeque.empty();\n\t\t_zoomDeque.push_back([=] {\n\t\t\tmakeRequest(MTPstats_LoadAsyncGraph(\n\t\t\t\tMTP_flags(x\n\t\t\t\t\t? MTPstats_LoadAsyncGraph::Flag::f_x\n\t\t\t\t\t: MTPstats_LoadAsyncGraph::Flag(0)),\n\t\t\t\tMTP_string(token),\n\t\t\t\tMTP_long(x)\n\t\t\t)).done([=](const MTPStatsGraph &result) {\n\t\t\t\tconsumer.put_next(StatisticalGraphFromTL(result));\n\t\t\t\tconsumer.put_done();\n\t\t\t\tif (!_zoomDeque.empty()) {\n\t\t\t\t\t_zoomDeque.pop_front();\n\t\t\t\t\tif (!_zoomDeque.empty()) {\n\t\t\t\t\t\t_zoomDeque.front()();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tconsumer.put_error_copy(error.type());\n\t\t\t}).send();\n\t\t});\n\t\tif (wasEmpty) {\n\t\t\t_zoomDeque.front()();\n\t\t}\n\n\t\treturn lifetime;\n\t};\n}\n\nData::ChannelStatistics Statistics::channelStats() const {\n\treturn _channelStats;\n}\n\nData::SupergroupStatistics Statistics::supergroupStats() const {\n\treturn _supergroupStats;\n}\n\nPublicForwards::PublicForwards(\n\tnot_null<ChannelData*> channel,\n\tData::RecentPostId fullId)\n: StatisticsRequestSender(channel)\n, _fullId(fullId) {\n}\n\nvoid PublicForwards::request(\n\t\tconst Data::PublicForwardsSlice::OffsetToken &token,\n\t\tFn<void(Data::PublicForwardsSlice)> done) {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\tconst auto channel = StatisticsRequestSender::channel();\n\tconst auto processResult = [=](const MTPstats_PublicForwards &tl) {\n\t\tusing Messages = QVector<Data::RecentPostId>;\n\t\t_requestId = 0;\n\n\t\tconst auto &data = tl.data();\n\t\tauto &owner = channel->owner();\n\n\t\towner.processUsers(data.vusers());\n\t\towner.processChats(data.vchats());\n\n\t\tconst auto nextToken = data.vnext_offset()\n\t\t\t? qs(*data.vnext_offset())\n\t\t\t: Data::PublicForwardsSlice::OffsetToken();\n\n\t\tconst auto fullCount = data.vcount().v;\n\n\t\tauto recentList = Messages(data.vforwards().v.size());\n\t\tfor (const auto &tlForward : data.vforwards().v) {\n\t\t\ttlForward.match([&](const MTPDpublicForwardMessage &data) {\n\t\t\t\tconst auto &message = data.vmessage();\n\t\t\t\tconst auto msgId = IdFromMessage(message);\n\t\t\t\tconst auto peerId = PeerFromMessage(message);\n\t\t\t\tconst auto lastDate = DateFromMessage(message);\n\t\t\t\tif (owner.peerLoaded(peerId)) {\n\t\t\t\t\tif (!lastDate) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\towner.addNewMessage(\n\t\t\t\t\t\tmessage,\n\t\t\t\t\t\tMessageFlags(),\n\t\t\t\t\t\tNewMessageType::Existing);\n\t\t\t\t\trecentList.push_back({ .messageId = { peerId, msgId } });\n\t\t\t\t}\n\t\t\t}, [&](const MTPDpublicForwardStory &data) {\n\t\t\t\tconst auto story = owner.stories().applySingle(\n\t\t\t\t\tpeerFromMTP(data.vpeer()),\n\t\t\t\t\tdata.vstory());\n\t\t\t\tif (story) {\n\t\t\t\t\trecentList.push_back({ .storyId = story->fullId() });\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tconst auto allLoaded = nextToken.isEmpty() || (nextToken == token);\n\t\t_lastTotal = std::max(_lastTotal, fullCount);\n\t\tdone({\n\t\t\t.list = std::move(recentList),\n\t\t\t.total = _lastTotal,\n\t\t\t.allLoaded = allLoaded,\n\t\t\t.token = nextToken,\n\t\t});\n\t};\n\tconst auto processFail = [=] {\n\t\t_requestId = 0;\n\t\tdone({});\n\t};\n\n\tconstexpr auto kLimit = tl::make_int(100);\n\tif (_fullId.messageId) {\n\t\t_requestId = makeRequest(MTPstats_GetMessagePublicForwards(\n\t\t\tchannel->inputChannel(),\n\t\t\tMTP_int(_fullId.messageId.msg),\n\t\t\tMTP_string(token),\n\t\t\tkLimit\n\t\t)).done(processResult).fail(processFail).send();\n\t} else if (_fullId.storyId) {\n\t\t_requestId = makeRequest(MTPstats_GetStoryPublicForwards(\n\t\t\tchannel->input(),\n\t\t\tMTP_int(_fullId.storyId.story),\n\t\t\tMTP_string(token),\n\t\t\tkLimit\n\t\t)).done(processResult).fail(processFail).send();\n\t}\n}\n\nMessageStatistics::MessageStatistics(\n\tnot_null<ChannelData*> channel,\n\tFullMsgId fullId)\n: StatisticsRequestSender(channel)\n, _publicForwards(channel, { .messageId = fullId })\n, _fullId(fullId) {\n}\n\nMessageStatistics::MessageStatistics(\n\tnot_null<ChannelData*> channel,\n\tFullStoryId storyId)\n: StatisticsRequestSender(channel)\n, _publicForwards(channel, { .storyId = storyId })\n, _storyId(storyId) {\n}\n\nData::PublicForwardsSlice MessageStatistics::firstSlice() const {\n\treturn _firstSlice;\n}\n\nvoid MessageStatistics::request(Fn<void(Data::MessageStatistics)> done) {\n\tif (channel()->isMegagroup() && !_storyId) {\n\t\treturn;\n\t}\n\tconst auto requestFirstPublicForwards = [=](\n\t\t\tconst Data::StatisticalGraph &messageGraph,\n\t\t\tconst Data::StatisticalGraph &reactionsGraph,\n\t\t\tconst Data::StatisticsMessageInteractionInfo &info) {\n\t\tconst auto callback = [=](Data::PublicForwardsSlice slice) {\n\t\t\tconst auto total = slice.total;\n\t\t\t_firstSlice = std::move(slice);\n\t\t\tdone({\n\t\t\t\t.messageInteractionGraph = messageGraph,\n\t\t\t\t.reactionsByEmotionGraph = reactionsGraph,\n\t\t\t\t.publicForwards = total,\n\t\t\t\t.privateForwards = info.forwardsCount - total,\n\t\t\t\t.views = info.viewsCount,\n\t\t\t\t.reactions = info.reactionsCount,\n\t\t\t});\n\t\t};\n\t\t_publicForwards.request({}, callback);\n\t};\n\n\tconst auto requestPrivateForwards = [=](\n\t\t\tconst Data::StatisticalGraph &messageGraph,\n\t\t\tconst Data::StatisticalGraph &reactionsGraph) {\n\t\tapi().request(MTPchannels_GetMessages(\n\t\t\tchannel()->inputChannel(),\n\t\t\tMTP_vector<MTPInputMessage>(\n\t\t\t\t1,\n\t\t\t\tMTP_inputMessageID(MTP_int(_fullId.msg))))\n\t\t).done([=](const MTPmessages_Messages &result) {\n\t\t\tconst auto process = [&](const MTPVector<MTPMessage> &messages) {\n\t\t\t\tconst auto &message = messages.v.front();\n\t\t\t\treturn message.match([&](const MTPDmessage &data) {\n\t\t\t\t\tauto reactionsCount = 0;\n\t\t\t\t\tif (const auto tlReactions = data.vreactions()) {\n\t\t\t\t\t\tconst auto &tlCounts = tlReactions->data().vresults();\n\t\t\t\t\t\tfor (const auto &tlCount : tlCounts.v) {\n\t\t\t\t\t\t\treactionsCount += tlCount.data().vcount().v;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn Data::StatisticsMessageInteractionInfo{\n\t\t\t\t\t\t.messageId = IdFromMessage(message),\n\t\t\t\t\t\t.viewsCount = data.vviews()\n\t\t\t\t\t\t\t? data.vviews()->v\n\t\t\t\t\t\t\t: 0,\n\t\t\t\t\t\t.forwardsCount = data.vforwards()\n\t\t\t\t\t\t\t? data.vforwards()->v\n\t\t\t\t\t\t\t: 0,\n\t\t\t\t\t\t.reactionsCount = reactionsCount,\n\t\t\t\t\t};\n\t\t\t\t}, [](const MTPDmessageEmpty &) {\n\t\t\t\t\treturn Data::StatisticsMessageInteractionInfo();\n\t\t\t\t}, [](const MTPDmessageService &) {\n\t\t\t\t\treturn Data::StatisticsMessageInteractionInfo();\n\t\t\t\t});\n\t\t\t};\n\n\t\t\tauto info = result.match([&](const MTPDmessages_messages &data) {\n\t\t\t\treturn process(data.vmessages());\n\t\t\t}, [&](const MTPDmessages_messagesSlice &data) {\n\t\t\t\treturn process(data.vmessages());\n\t\t\t}, [&](const MTPDmessages_channelMessages &data) {\n\t\t\t\treturn process(data.vmessages());\n\t\t\t}, [](const MTPDmessages_messagesNotModified &) {\n\t\t\t\treturn Data::StatisticsMessageInteractionInfo();\n\t\t\t});\n\n\t\t\trequestFirstPublicForwards(\n\t\t\t\tmessageGraph,\n\t\t\t\treactionsGraph,\n\t\t\t\tstd::move(info));\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\trequestFirstPublicForwards(messageGraph, reactionsGraph, {});\n\t\t}).send();\n\t};\n\n\tconst auto requestStoryPrivateForwards = [=](\n\t\t\tconst Data::StatisticalGraph &messageGraph,\n\t\t\tconst Data::StatisticalGraph &reactionsGraph) {\n\t\tapi().request(MTPstories_GetStoriesByID(\n\t\t\tchannel()->input(),\n\t\t\tMTP_vector<MTPint>(1, MTP_int(_storyId.story)))\n\t\t).done([=](const MTPstories_Stories &result) {\n\t\t\tconst auto &storyItem = result.data().vstories().v.front();\n\t\t\tauto info = storyItem.match([&](const MTPDstoryItem &data) {\n\t\t\t\tif (!data.vviews()) {\n\t\t\t\t\treturn Data::StatisticsMessageInteractionInfo();\n\t\t\t\t}\n\t\t\t\tconst auto &tlViews = data.vviews()->data();\n\t\t\t\treturn Data::StatisticsMessageInteractionInfo{\n\t\t\t\t\t.storyId = data.vid().v,\n\t\t\t\t\t.viewsCount = tlViews.vviews_count().v,\n\t\t\t\t\t.forwardsCount = tlViews.vforwards_count().value_or(0),\n\t\t\t\t\t.reactionsCount = tlViews.vreactions_count().value_or(0),\n\t\t\t\t};\n\t\t\t}, [](const auto &) {\n\t\t\t\treturn Data::StatisticsMessageInteractionInfo();\n\t\t\t});\n\n\t\t\trequestFirstPublicForwards(\n\t\t\t\tmessageGraph,\n\t\t\t\treactionsGraph,\n\t\t\t\tstd::move(info));\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\trequestFirstPublicForwards(messageGraph, reactionsGraph, {});\n\t\t}).send();\n\t};\n\n\tif (_storyId) {\n\t\tmakeRequest(MTPstats_GetStoryStats(\n\t\t\tMTP_flags(MTPstats_GetStoryStats::Flags(0)),\n\t\t\tchannel()->input(),\n\t\t\tMTP_int(_storyId.story)\n\t\t)).done([=](const MTPstats_StoryStats &result) {\n\t\t\tconst auto &data = result.data();\n\t\t\trequestStoryPrivateForwards(\n\t\t\t\tStatisticalGraphFromTL(data.vviews_graph()),\n\t\t\t\tStatisticalGraphFromTL(data.vreactions_by_emotion_graph()));\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\trequestStoryPrivateForwards({}, {});\n\t\t}).send();\n\t} else {\n\t\tmakeRequest(MTPstats_GetMessageStats(\n\t\t\tMTP_flags(MTPstats_GetMessageStats::Flags(0)),\n\t\t\tchannel()->inputChannel(),\n\t\t\tMTP_int(_fullId.msg.bare)\n\t\t)).done([=](const MTPstats_MessageStats &result) {\n\t\t\tconst auto &data = result.data();\n\t\t\trequestPrivateForwards(\n\t\t\t\tStatisticalGraphFromTL(data.vviews_graph()),\n\t\t\t\tStatisticalGraphFromTL(data.vreactions_by_emotion_graph()));\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\trequestPrivateForwards({}, {});\n\t\t}).send();\n\t}\n}\n\nBoosts::Boosts(not_null<PeerData*> peer)\n: _peer(peer)\n, _api(&peer->session().api().instance()) {\n}\n\nrpl::producer<rpl::no_value, QString> Boosts::request() {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tconst auto channel = _peer->asChannel();\n\t\tif (!channel) {\n\t\t\treturn lifetime;\n\t\t}\n\n\t\t_api.request(MTPpremium_GetBoostsStatus(\n\t\t\t_peer->input()\n\t\t)).done([=](const MTPpremium_BoostsStatus &result) {\n\t\t\tconst auto &data = result.data();\n\t\t\tchannel->updateLevelHint(data.vlevel().v);\n\t\t\tconst auto hasPremium = !!data.vpremium_audience();\n\t\t\tconst auto premiumMemberCount = hasPremium\n\t\t\t\t? std::max(0, int(data.vpremium_audience()->data().vpart().v))\n\t\t\t\t: 0;\n\t\t\tconst auto participantCount = hasPremium\n\t\t\t\t? std::max(\n\t\t\t\t\tint(data.vpremium_audience()->data().vtotal().v),\n\t\t\t\t\tpremiumMemberCount)\n\t\t\t\t: 0;\n\t\t\tconst auto premiumMemberPercentage = (participantCount > 0)\n\t\t\t\t? (100. * premiumMemberCount / participantCount)\n\t\t\t\t: 0;\n\n\t\t\tconst auto slots = data.vmy_boost_slots();\n\t\t\t_boostStatus.overview = Data::BoostsOverview{\n\t\t\t\t.group = channel->isMegagroup(),\n\t\t\t\t.mine = slots ? int(slots->v.size()) : 0,\n\t\t\t\t.level = std::max(data.vlevel().v, 0),\n\t\t\t\t.boostCount = std::max(\n\t\t\t\t\tdata.vboosts().v,\n\t\t\t\t\tdata.vcurrent_level_boosts().v),\n\t\t\t\t.currentLevelBoostCount = data.vcurrent_level_boosts().v,\n\t\t\t\t.nextLevelBoostCount = data.vnext_level_boosts()\n\t\t\t\t\t? data.vnext_level_boosts()->v\n\t\t\t\t\t: 0,\n\t\t\t\t.premiumMemberCount = premiumMemberCount,\n\t\t\t\t.premiumMemberPercentage = premiumMemberPercentage,\n\t\t\t};\n\t\t\t_boostStatus.link = qs(data.vboost_url());\n\n\t\t\tif (data.vprepaid_giveaways()) {\n\t\t\t\t_boostStatus.prepaidGiveaway = ranges::views::all(\n\t\t\t\t\tdata.vprepaid_giveaways()->v\n\t\t\t\t) | ranges::views::transform([](const MTPPrepaidGiveaway &r) {\n\t\t\t\t\treturn r.match([&](const MTPDprepaidGiveaway &data) {\n\t\t\t\t\t\treturn Data::BoostPrepaidGiveaway{\n\t\t\t\t\t\t\t.date = base::unixtime::parse(data.vdate().v),\n\t\t\t\t\t\t\t.id = data.vid().v,\n\t\t\t\t\t\t\t.months = data.vmonths().v,\n\t\t\t\t\t\t\t.quantity = data.vquantity().v,\n\t\t\t\t\t\t};\n\t\t\t\t\t}, [&](const MTPDprepaidStarsGiveaway &data) {\n\t\t\t\t\t\treturn Data::BoostPrepaidGiveaway{\n\t\t\t\t\t\t\t.date = base::unixtime::parse(data.vdate().v),\n\t\t\t\t\t\t\t.id = data.vid().v,\n\t\t\t\t\t\t\t.credits = data.vstars().v,\n\t\t\t\t\t\t\t.quantity = data.vquantity().v,\n\t\t\t\t\t\t\t.boosts = data.vboosts().v,\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t\t}) | ranges::to_vector;\n\t\t\t}\n\n\t\t\tusing namespace Data;\n\t\t\trequestBoosts({ .gifts = false }, [=](BoostsListSlice &&slice) {\n\t\t\t\t_boostStatus.firstSliceBoosts = std::move(slice);\n\t\t\t\trequestBoosts({ .gifts = true }, [=](BoostsListSlice &&s) {\n\t\t\t\t\t_boostStatus.firstSliceGifts = std::move(s);\n\t\t\t\t\tconsumer.put_done();\n\t\t\t\t});\n\t\t\t});\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).send();\n\n\t\treturn lifetime;\n\t};\n}\n\nvoid Boosts::requestBoosts(\n\t\tconst Data::BoostsListSlice::OffsetToken &token,\n\t\tFn<void(Data::BoostsListSlice)> done) {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\tconstexpr auto kTlFirstSlice = tl::make_int(kFirstSlice);\n\tconstexpr auto kTlLimit = tl::make_int(kLimit);\n\tconst auto gifts = token.gifts;\n\t_requestId = _api.request(MTPpremium_GetBoostsList(\n\t\tgifts\n\t\t\t? MTP_flags(MTPpremium_GetBoostsList::Flag::f_gifts)\n\t\t\t: MTP_flags(0),\n\t\t_peer->input(),\n\t\tMTP_string(token.next),\n\t\ttoken.next.isEmpty() ? kTlFirstSlice : kTlLimit\n\t)).done([=](const MTPpremium_BoostsList &result) {\n\t\t_requestId = 0;\n\n\t\tconst auto &data = result.data();\n\t\t_peer->owner().processUsers(data.vusers());\n\n\t\tauto list = std::vector<Data::Boost>();\n\t\tlist.reserve(data.vboosts().v.size());\n\t\tconstexpr auto kMonthsDivider = int(30 * 86400);\n\t\tfor (const auto &boost : data.vboosts().v) {\n\t\t\tconst auto &data = boost.data();\n\t\t\tconst auto path = data.vused_gift_slug()\n\t\t\t\t? (u\"giftcode/\"_q + qs(data.vused_gift_slug()->v))\n\t\t\t\t: QString();\n\t\t\tauto giftCodeLink = !path.isEmpty()\n\t\t\t\t? Data::GiftCodeLink{\n\t\t\t\t\t_peer->session().createInternalLink(path),\n\t\t\t\t\t_peer->session().createInternalLinkFull(path),\n\t\t\t\t\tqs(data.vused_gift_slug()->v),\n\t\t\t\t}\n\t\t\t\t: Data::GiftCodeLink();\n\t\t\tlist.push_back({\n\t\t\t\t.id = qs(data.vid()),\n\t\t\t\t.userId = UserId(data.vuser_id().value_or_empty()),\n\t\t\t\t.giveawayMessage = data.vgiveaway_msg_id()\n\t\t\t\t\t? FullMsgId{ _peer->id, data.vgiveaway_msg_id()->v }\n\t\t\t\t\t: FullMsgId(),\n\t\t\t\t.date = base::unixtime::parse(data.vdate().v),\n\t\t\t\t.expiresAt = base::unixtime::parse(data.vexpires().v),\n\t\t\t\t.expiresAfterMonths = ((data.vexpires().v - data.vdate().v)\n\t\t\t\t\t/ kMonthsDivider),\n\t\t\t\t.giftCodeLink = std::move(giftCodeLink),\n\t\t\t\t.multiplier = data.vmultiplier().value_or_empty(),\n\t\t\t\t.credits = data.vstars().value_or_empty(),\n\t\t\t\t.isGift = data.is_gift(),\n\t\t\t\t.isGiveaway = data.is_giveaway(),\n\t\t\t\t.isUnclaimed = data.is_unclaimed(),\n\t\t\t});\n\t\t}\n\t\tdone(Data::BoostsListSlice{\n\t\t\t.list = std::move(list),\n\t\t\t.multipliedTotal = data.vcount().v,\n\t\t\t.allLoaded = (data.vcount().v == data.vboosts().v.size()),\n\t\t\t.token = Data::BoostsListSlice::OffsetToken{\n\t\t\t\t.next = data.vnext_offset()\n\t\t\t\t\t? qs(*data.vnext_offset())\n\t\t\t\t\t: QString(),\n\t\t\t\t.gifts = gifts,\n\t\t\t},\n\t\t});\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t}).send();\n}\n\nData::BoostStatus Boosts::boostStatus() const {\n\treturn _boostStatus;\n}\n\nEarnStatistics::EarnStatistics(not_null<PeerData*> peer)\n: StatisticsRequestSender(peer)\n, _isUser(peer->isUser()) {\n}\n\nrpl::producer<rpl::no_value, QString> EarnStatistics::request() {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tapi().request(MTPpayments_GetStarsRevenueStats(\n\t\t\tMTP_flags(MTPpayments_getStarsRevenueStats::Flag::f_ton),\n\t\t\t(_isUser ? user()->input() : channel()->input())\n\t\t)).done([=](const MTPpayments_StarsRevenueStats &result) {\n\t\t\tconst auto &data = result.data();\n\t\t\tconst auto &balances = data.vstatus().data();\n\t\t\tconst auto amount = [](const auto &a) {\n\t\t\t\treturn CreditsAmountFromTL(a);\n\t\t\t};\n\t\t\t_data = Data::EarnStatistics{\n\t\t\t\t.topHoursGraph = data.vtop_hours_graph()\n\t\t\t\t\t? StatisticalGraphFromTL(*data.vtop_hours_graph())\n\t\t\t\t\t: Data::StatisticalGraph(),\n\t\t\t\t.revenueGraph = StatisticalGraphFromTL(data.vrevenue_graph()),\n\t\t\t\t.currentBalance = amount(balances.vcurrent_balance()),\n\t\t\t\t.availableBalance = amount(balances.vavailable_balance()),\n\t\t\t\t.overallRevenue = amount(balances.voverall_revenue()),\n\t\t\t\t.usdRate = data.vusd_rate().v,\n\t\t\t};\n\n\t\t\trequestHistory({}, [=](Data::EarnHistorySlice &&slice) {\n\t\t\t\t_data.firstHistorySlice = std::move(slice);\n\n\t\t\t\tif (!_isUser) {\n\t\t\t\t\tapi().request(\n\t\t\t\t\t\tMTPchannels_GetFullChannel(channel()->inputChannel())\n\t\t\t\t\t).done([=](const MTPmessages_ChatFull &result) {\n\t\t\t\t\t\tresult.data().vfull_chat().match([&](\n\t\t\t\t\t\t\t\tconst MTPDchannelFull &d) {\n\t\t\t\t\t\t\t_data.switchedOff = d.is_restricted_sponsored();\n\t\t\t\t\t\t}, [](const auto &) {\n\t\t\t\t\t\t});\n\t\t\t\t\t\tconsumer.put_done();\n\t\t\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\t\t\tconsumer.put_error_copy(error.type());\n\t\t\t\t\t}).send();\n\t\t\t\t} else {\n\t\t\t\t\tconsumer.put_done();\n\t\t\t\t}\n\t\t\t});\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_error_copy(error.type());\n\t\t}).send();\n\n\t\treturn lifetime;\n\t};\n}\n\nvoid EarnStatistics::requestHistory(\n\t\tconst Data::EarnHistorySlice::OffsetToken &token,\n\t\tFn<void(Data::EarnHistorySlice)> done) {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\n\tconstexpr auto kTlFirstSlice = tl::make_int(kFirstSlice);\n\tconstexpr auto kTlLimit = tl::make_int(kLimit);\n\n\t_requestId = api().request(MTPpayments_GetStarsTransactions(\n\t\tMTP_flags(MTPpayments_getStarsTransactions::Flag::f_ton),\n\t\tMTP_string(), // Subscription ID.\n\t\t(_isUser ? user()->input() : channel()->input()),\n\t\tMTP_string(token),\n\t\ttoken.isEmpty() ? kTlFirstSlice : kTlLimit\n\t)).done([=](const MTPpayments_StarsStatus &result) {\n\t\t_requestId = 0;\n\n\t\tconst auto nextToken = result.data().vnext_offset().value_or_empty();\n\n\t\tconst auto tlTransactions\n\t\t\t= result.data().vhistory().value_or_empty();\n\n\t\tconst auto peer = _isUser ? (PeerData*)user() : (PeerData*)channel();\n\t\tauto list = ranges::views::all(\n\t\t\ttlTransactions\n\t\t) | ranges::views::transform([=](const auto &d) {\n\t\t\treturn CreditsHistoryEntryFromTL(d, peer);\n\t\t}) | ranges::to_vector;\n\t\tdone(Data::EarnHistorySlice{\n\t\t\t.list = std::move(list),\n\t\t\t.total = int(tlTransactions.size()),\n\t\t\t// .total = result.data().vcount().v,\n\t\t\t.allLoaded = nextToken.isEmpty(),\n\t\t\t.token = Data::EarnHistorySlice::OffsetToken(nextToken),\n\t\t});\n\t}).fail([=] {\n\t\tdone({});\n\t\t_requestId = 0;\n\t}).send();\n}\n\nData::EarnStatistics EarnStatistics::data() const {\n\treturn _data;\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_statistics.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_statistics_sender.h\"\n#include \"data/data_boosts.h\"\n#include \"data/data_channel_earn.h\"\n#include \"data/data_statistics.h\"\n\nclass ChannelData;\nclass PeerData;\n\nnamespace Api {\n\nclass Statistics final : public StatisticsRequestSender {\npublic:\n\texplicit Statistics(not_null<ChannelData*> channel);\n\n\t[[nodiscard]] rpl::producer<rpl::no_value, QString> request();\n\tusing GraphResult = rpl::producer<Data::StatisticalGraph, QString>;\n\t[[nodiscard]] GraphResult requestZoom(\n\t\tconst QString &token,\n\t\tfloat64 x);\n\n\t[[nodiscard]] Data::ChannelStatistics channelStats() const;\n\t[[nodiscard]] Data::SupergroupStatistics supergroupStats() const;\n\nprivate:\n\tData::ChannelStatistics _channelStats;\n\tData::SupergroupStatistics _supergroupStats;\n\n\tstd::deque<Fn<void()>> _zoomDeque;\n\n};\n\nclass PublicForwards final : public StatisticsRequestSender {\npublic:\n\tPublicForwards(\n\t\tnot_null<ChannelData*> channel,\n\t\tData::RecentPostId fullId);\n\n\tvoid request(\n\t\tconst Data::PublicForwardsSlice::OffsetToken &token,\n\t\tFn<void(Data::PublicForwardsSlice)> done);\n\nprivate:\n\tconst Data::RecentPostId _fullId;\n\tmtpRequestId _requestId = 0;\n\tint _lastTotal = 0;\n\n};\n\nclass MessageStatistics final : public StatisticsRequestSender {\npublic:\n\texplicit MessageStatistics(\n\t\tnot_null<ChannelData*> channel,\n\t\tFullMsgId fullId);\n\texplicit MessageStatistics(\n\t\tnot_null<ChannelData*> channel,\n\t\tFullStoryId storyId);\n\n\tvoid request(Fn<void(Data::MessageStatistics)> done);\n\n\t[[nodiscard]] Data::PublicForwardsSlice firstSlice() const;\n\nprivate:\n\tPublicForwards _publicForwards;\n\tconst FullMsgId _fullId;\n\tconst FullStoryId _storyId;\n\n\tData::PublicForwardsSlice _firstSlice;\n\n};\n\nclass EarnStatistics final : public StatisticsRequestSender {\npublic:\n\texplicit EarnStatistics(not_null<PeerData*> peer);\n\n\t[[nodiscard]] rpl::producer<rpl::no_value, QString> request();\n\tvoid requestHistory(\n\t\tconst Data::EarnHistorySlice::OffsetToken &token,\n\t\tFn<void(Data::EarnHistorySlice)> done);\n\n\t[[nodiscard]] Data::EarnStatistics data() const;\n\n\tstatic constexpr auto kFirstSlice = int(5);\n\tstatic constexpr auto kLimit = int(10);\n\nprivate:\n\tconst bool _isUser = false;\n\tData::EarnStatistics _data;\n\n\tmtpRequestId _requestId = 0;\n\n};\n\nclass Boosts final {\npublic:\n\texplicit Boosts(not_null<PeerData*> peer);\n\n\t[[nodiscard]] rpl::producer<rpl::no_value, QString> request();\n\tvoid requestBoosts(\n\t\tconst Data::BoostsListSlice::OffsetToken &token,\n\t\tFn<void(Data::BoostsListSlice)> done);\n\n\t[[nodiscard]] Data::BoostStatus boostStatus() const;\n\n\tstatic constexpr auto kFirstSlice = int(10);\n\tstatic constexpr auto kLimit = int(40);\n\nprivate:\n\tconst not_null<PeerData*> _peer;\n\tData::BoostStatus _boostStatus;\n\n\tMTP::Sender _api;\n\tmtpRequestId _requestId = 0;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_statistics_data_deserialize.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_statistics_data_deserialize.h\"\n\n#include \"data/data_statistics_chart.h\"\n#include \"statistics/statistics_data_deserialize.h\"\n\nnamespace Api {\n\nData::StatisticalGraph StatisticalGraphFromTL(const MTPStatsGraph &tl) {\n\treturn tl.match([&](const MTPDstatsGraph &d) {\n\t\tusing namespace Statistic;\n\t\tconst auto zoomToken = d.vzoom_token().has_value()\n\t\t\t? qs(*d.vzoom_token()).toUtf8()\n\t\t\t: QByteArray();\n\t\treturn Data::StatisticalGraph{\n\t\t\tStatisticalChartFromJSON(qs(d.vjson().data().vdata()).toUtf8()),\n\t\t\tzoomToken,\n\t\t};\n\t}, [&](const MTPDstatsGraphAsync &data) {\n\t\treturn Data::StatisticalGraph{\n\t\t\t.zoomToken = qs(data.vtoken()).toUtf8(),\n\t\t};\n\t}, [&](const MTPDstatsGraphError &data) {\n\t\treturn Data::StatisticalGraph{ .error = qs(data.verror()) };\n\t});\n}\n\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_statistics_data_deserialize.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nstruct StatisticalGraph;\n} // namespace Data\n\nnamespace Api {\n\n[[nodiscard]] Data::StatisticalGraph StatisticalGraphFromTL(\n\tconst MTPStatsGraph &tl);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_statistics_sender.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_statistics_sender.h\"\n\n#include \"apiwrap.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n\nnamespace Api {\n\nStatisticsRequestSender::StatisticsRequestSender(\n\tnot_null<PeerData*> peer)\n: _peer(peer)\n, _channel(peer->asChannel())\n, _user(peer->asUser())\n, _api(&_peer->session().api().instance())\n, _timer([=] { checkRequests(); }) {\n}\n\nMTP::Sender &StatisticsRequestSender::api() {\n\treturn _api;\n}\n\nnot_null<ChannelData*> StatisticsRequestSender::channel() {\n\tExpects(_channel);\n\treturn _channel;\n}\n\nnot_null<UserData*> StatisticsRequestSender::user() {\n\tExpects(_user);\n\treturn _user;\n}\n\nvoid StatisticsRequestSender::checkRequests() {\n\tfor (auto i = begin(_requests); i != end(_requests);) {\n\t\tfor (auto j = begin(i->second); j != end(i->second);) {\n\t\t\tif (_api.pending(*j)) {\n\t\t\t\t++j;\n\t\t\t} else {\n\t\t\t\t_peer->session().api().unregisterStatsRequest(\n\t\t\t\t\ti->first,\n\t\t\t\t\t*j);\n\t\t\t\tj = i->second.erase(j);\n\t\t\t}\n\t\t}\n\t\tif (i->second.empty()) {\n\t\t\ti = _requests.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tif (_requests.empty()) {\n\t\t_timer.cancel();\n\t}\n}\n\nauto StatisticsRequestSender::ensureRequestIsRegistered()\n-> StatisticsRequestSender::Registered {\n\tconst auto id = _api.allocateRequestId();\n\tconst auto dcId = _peer->owner().statsDcId(_peer);\n\tif (dcId) {\n\t\t_peer->session().api().registerStatsRequest(dcId, id);\n\t\t_requests[dcId].emplace(id);\n\t\tif (!_timer.isActive()) {\n\t\t\tconstexpr auto kCheckRequestsTimer = 10 * crl::time(1000);\n\t\t\t_timer.callEach(kCheckRequestsTimer);\n\t\t}\n\t}\n\treturn StatisticsRequestSender::Registered{ id, dcId };\n}\n\nStatisticsRequestSender::~StatisticsRequestSender() {\n\tfor (const auto &[dcId, ids] : _requests) {\n\t\tfor (const auto id : ids) {\n\t\t\t_peer->session().api().unregisterStatsRequest(dcId, id);\n\t\t}\n\t}\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_statistics_sender.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"mtproto/sender.h\"\n\nclass ChannelData;\nclass PeerData;\nclass UserData;\n\nnamespace Api {\n\nclass StatisticsRequestSender {\nprotected:\n\texplicit StatisticsRequestSender(not_null<PeerData*> peer);\n\t~StatisticsRequestSender();\n\n\ttemplate <\n\t\ttypename Request,\n\t\ttypename = std::enable_if_t<!std::is_reference_v<Request>>,\n\t\ttypename = typename Request::Unboxed>\n\t[[nodiscard]] auto makeRequest(Request &&request) {\n\t\tconst auto [id, dcId] = ensureRequestIsRegistered();\n\t\treturn std::move(_api.request(\n\t\t\tstd::forward<Request>(request)\n\t\t).toDC(\n\t\t\tdcId ? MTP::ShiftDcId(dcId, MTP::kStatsDcShift) : 0\n\t\t).overrideId(id));\n\t}\n\n\t[[nodiscard]] MTP::Sender &api();\n\t[[nodiscard]] not_null<ChannelData*> channel();\n\t[[nodiscard]] not_null<UserData*> user();\n\nprivate:\n\tstruct Registered final {\n\t\tmtpRequestId id;\n\t\tMTP::DcId dcId;\n\t};\n\t[[nodiscard]] Registered ensureRequestIsRegistered();\n\tvoid checkRequests();\n\n\tconst not_null<PeerData*> _peer;\n\tChannelData * const _channel;\n\tUserData * const _user;\n\tMTP::Sender _api;\n\tbase::Timer _timer;\n\tbase::flat_map<MTP::DcId, base::flat_set<mtpRequestId>> _requests;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_suggest_post.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_suggest_post.h\"\n\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/transfer_gift_box.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/components/credits.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"history/view/controls/history_view_suggest_options.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mainwindow.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"ui/boxes/choose_date_time.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/emoji_stake_box.h\" // InsufficientTonBox\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Api {\nnamespace {\n\nvoid SendApproval(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<HistoryItem*> item,\n\t\tTimeId scheduleDate = 0) {\n\tusing Flag = MTPmessages_ToggleSuggestedPostApproval::Flag;\n\tconst auto suggestion = item->Get<HistoryMessageSuggestion>();\n\tif (!suggestion\n\t\t|| suggestion->accepted\n\t\t|| suggestion->rejected\n\t\t|| suggestion->requestId) {\n\t\treturn;\n\t}\n\n\tconst auto id = item->fullId();\n\tconst auto session = &show->session();\n\tconst auto finish = [=] {\n\t\tif (const auto item = session->data().message(id)) {\n\t\t\tconst auto suggestion = item->Get<HistoryMessageSuggestion>();\n\t\t\tif (suggestion) {\n\t\t\t\tsuggestion->requestId = 0;\n\t\t\t}\n\t\t}\n\t};\n\tsuggestion->requestId = session->api().request(\n\t\tMTPmessages_ToggleSuggestedPostApproval(\n\t\t\tMTP_flags(scheduleDate ? Flag::f_schedule_date : Flag()),\n\t\t\titem->history()->peer->input(),\n\t\t\tMTP_int(item->id.bare),\n\t\t\tMTP_int(scheduleDate),\n\t\t\tMTPstring()) // reject_comment\n\t).done([=](const MTPUpdates &result) {\n\t\tsession->api().applyUpdates(result);\n\t\tfinish();\n\t}).fail([=](const MTP::Error &error) {\n\t\tshow->showToast(error.type());\n\t\tfinish();\n\t}).send();\n}\n\nvoid ConfirmApproval(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<HistoryItem*> item,\n\t\tTimeId scheduleDate = 0,\n\t\tFn<void()> accepted = nullptr) {\n\tconst auto suggestion = item->Get<HistoryMessageSuggestion>();\n\tif (!suggestion\n\t\t|| suggestion->accepted\n\t\t|| suggestion->rejected\n\t\t|| suggestion->requestId) {\n\t\treturn;\n\t}\n\tconst auto id = item->fullId();\n\tconst auto price = suggestion->price;\n\tconst auto admin = item->history()->amMonoforumAdmin();\n\tif (!admin && !price.empty()) {\n\t\tconst auto credits = &item->history()->session().credits();\n\t\tif (price.ton()) {\n\t\t\tif (!credits->tonLoaded()) {\n\t\t\t\tcredits->tonLoad();\n\t\t\t\treturn;\n\t\t\t} else if (price > credits->tonBalance()) {\n\t\t\t\tconst auto session = &item->history()->session();\n\t\t\t\tshow->show(\n\t\t\t\t\tBox(Ui::InsufficientTonBox, session, price));\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else {\n\t\t\tif (!credits->loaded()) {\n\t\t\t\tcredits->load();\n\t\t\t\treturn;\n\t\t\t} else if (price > credits->balance()) {\n\t\t\t\tusing namespace Settings;\n\t\t\t\tconst auto peer = item->history()->peer;\n\t\t\t\tconst auto broadcast = peer->monoforumBroadcast();\n\t\t\t\tconst auto broadcastId = (broadcast ? broadcast : peer)->id;\n\t\t\t\tconst auto done = [=](SmallBalanceResult result) {\n\t\t\t\t\tif (result == SmallBalanceResult::Success\n\t\t\t\t\t\t|| result == SmallBalanceResult::Already) {\n\t\t\t\t\t\tconst auto item = peer->owner().message(id);\n\t\t\t\t\t\tif (item) {\n\t\t\t\t\t\t\tConfirmApproval(\n\t\t\t\t\t\t\t\tshow,\n\t\t\t\t\t\t\t\titem,\n\t\t\t\t\t\t\t\tscheduleDate,\n\t\t\t\t\t\t\t\taccepted);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tMaybeRequestBalanceIncrease(\n\t\t\t\t\tshow,\n\t\t\t\t\tint(base::SafeRound(price.value())),\n\t\t\t\t\tSmallBalanceForSuggest{ broadcastId },\n\t\t\t\t\tdone);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto peer = item->history()->peer;\n\tconst auto session = &peer->session();\n\tconst auto broadcast = peer->monoforumBroadcast();\n\tconst auto channelName = (broadcast ? broadcast : peer)->name();\n\tconst auto amount = admin\n\t\t? HistoryView::PriceAfterCommission(session, price)\n\t\t: price;\n\tconst auto commission = HistoryView::FormatAfterCommissionPercent(\n\t\tsession,\n\t\tprice);\n\tconst auto date = langDateTime(base::unixtime::parse(scheduleDate));\n\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto callback = std::make_shared<Fn<void()>>();\n\t\tauto text = admin\n\t\t\t? tr::lng_suggest_accept_text(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\ttr::bold(item->from()->shortName()),\n\t\t\t\ttr::marked)\n\t\t\t: tr::lng_suggest_accept_text_to(\n\t\t\t\ttr::now,\n\t\t\t\tlt_channel,\n\t\t\t\ttr::bold(channelName),\n\t\t\t\ttr::marked);\n\t\tif (price) {\n\t\t\ttext.append(\"\\n\\n\").append(admin\n\t\t\t\t? (scheduleDate\n\t\t\t\t\t? (amount.stars()\n\t\t\t\t\t\t? tr::lng_suggest_accept_receive_stars\n\t\t\t\t\t\t: tr::lng_suggest_accept_receive_ton)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\t\tamount.value(),\n\t\t\t\t\t\t\tlt_channel,\n\t\t\t\t\t\t\ttr::bold(channelName),\n\t\t\t\t\t\t\tlt_percent,\n\t\t\t\t\t\t\tTextWithEntities{ commission },\n\t\t\t\t\t\t\tlt_date,\n\t\t\t\t\t\t\ttr::bold(date),\n\t\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: (amount.stars()\n\t\t\t\t\t\t? tr::lng_suggest_accept_receive_now_stars\n\t\t\t\t\t\t: tr::lng_suggest_accept_receive_now_ton)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\t\tamount.value(),\n\t\t\t\t\t\t\tlt_channel,\n\t\t\t\t\t\t\ttr::bold(channelName),\n\t\t\t\t\t\t\tlt_percent,\n\t\t\t\t\t\t\tTextWithEntities{ commission },\n\t\t\t\t\t\t\ttr::rich))\n\t\t\t\t: (scheduleDate\n\t\t\t\t\t? (amount.stars()\n\t\t\t\t\t\t? tr::lng_suggest_accept_pay_stars\n\t\t\t\t\t\t: tr::lng_suggest_accept_pay_ton)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\t\tamount.value(),\n\t\t\t\t\t\t\tlt_date,\n\t\t\t\t\t\t\ttr::bold(date),\n\t\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: (amount.stars()\n\t\t\t\t\t\t? tr::lng_suggest_accept_pay_now_stars\n\t\t\t\t\t\t: tr::lng_suggest_accept_pay_now_ton)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\t\tamount.value(),\n\t\t\t\t\t\t\ttr::rich)));\n\t\t\tif (admin) {\n\t\t\t\ttext.append(' ').append(\n\t\t\t\t\ttr::lng_suggest_accept_receive_if(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::rich));\n\t\t\t\tif (price.stars()) {\n\t\t\t\t\ttext.append(\"\\n\\n\").append(\n\t\t\t\t\t\ttr::lng_suggest_options_stars_warning(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\ttr::rich));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = text,\n\t\t\t.confirmed = [=](Fn<void()> close) { (*callback)(); close(); },\n\t\t\t.confirmText = tr::lng_suggest_accept_send(),\n\t\t\t.title = tr::lng_suggest_accept_title(),\n\t\t});\n\t\t*callback = [=, weak = base::make_weak(box)] {\n\t\t\tif (const auto onstack = accepted) {\n\t\t\t\tonstack();\n\t\t\t}\n\t\t\tconst auto item = show->session().data().message(id);\n\t\t\tif (!item) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tSendApproval(show, item, scheduleDate);\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t};\n\t}));\n}\n\nvoid SendDecline(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<HistoryItem*> item,\n\t\tconst QString &comment) {\n\tusing Flag = MTPmessages_ToggleSuggestedPostApproval::Flag;\n\tconst auto suggestion = item->Get<HistoryMessageSuggestion>();\n\tif (!suggestion\n\t\t|| suggestion->accepted\n\t\t|| suggestion->rejected\n\t\t|| suggestion->requestId) {\n\t\treturn;\n\t}\n\n\tconst auto id = item->fullId();\n\tconst auto session = &show->session();\n\tconst auto finish = [=] {\n\t\tif (const auto item = session->data().message(id)) {\n\t\t\tconst auto suggestion = item->Get<HistoryMessageSuggestion>();\n\t\t\tif (suggestion) {\n\t\t\t\tsuggestion->requestId = 0;\n\t\t\t}\n\t\t}\n\t};\n\tsuggestion->requestId = session->api().request(\n\t\tMTPmessages_ToggleSuggestedPostApproval(\n\t\t\tMTP_flags(Flag::f_reject\n\t\t\t\t| (comment.isEmpty() ? Flag() : Flag::f_reject_comment)),\n\t\t\titem->history()->peer->input(),\n\t\t\tMTP_int(item->id.bare),\n\t\t\tMTPint(), // schedule_date\n\t\t\tMTP_string(comment))\n\t).done([=](const MTPUpdates &result) {\n\t\tsession->api().applyUpdates(result);\n\t\tfinish();\n\t}).fail([=](const MTP::Error &error) {\n\t\tshow->showToast(error.type());\n\t\tfinish();\n\t}).send();\n}\n\nvoid RequestApprovalDate(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto id = item->fullId();\n\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tconst auto close = [=] {\n\t\tif (const auto strong = weak->get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t};\n\tconst auto done = [=](TimeId result) {\n\t\tif (const auto item = show->session().data().message(id)) {\n\t\t\tConfirmApproval(show, item, result, close);\n\t\t} else {\n\t\t\tclose();\n\t\t}\n\t};\n\tusing namespace HistoryView;\n\tauto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{\n\t\t.session = &show->session(),\n\t\t.done = done,\n\t\t.mode = SuggestMode::Publish,\n\t});\n\t*weak = dateBox.data();\n\tshow->show(std::move(dateBox));\n}\n\nvoid RequestDeclineComment(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto id = item->fullId();\n\tconst auto admin = item->history()->amMonoforumAdmin();\n\tconst auto peer = item->history()->peer;\n\tconst auto broadcast = peer->monoforumBroadcast();\n\tconst auto channelName = (broadcast ? broadcast : peer)->name();\n\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto callback = std::make_shared<Fn<void()>>();\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = (admin\n\t\t\t\t? tr::lng_suggest_decline_text(\n\t\t\t\t\tlt_from,\n\t\t\t\t\trpl::single(tr::bold(item->from()->shortName())),\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_suggest_decline_text_to(\n\t\t\t\t\tlt_channel,\n\t\t\t\t\trpl::single(tr::bold(channelName)),\n\t\t\t\t\ttr::marked)),\n\t\t\t.confirmed = [=](Fn<void()> close) { (*callback)(); close(); },\n\t\t\t.confirmText = tr::lng_suggest_action_decline(),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t.title = tr::lng_suggest_decline_title(),\n\t\t});\n\t\tconst auto reason = box->addRow(object_ptr<Ui::InputField>(\n\t\t\tbox,\n\t\t\tst::factcheckField,\n\t\t\tUi::InputField::Mode::NoNewlines,\n\t\t\ttr::lng_suggest_decline_reason()));\n\t\tbox->setFocusCallback([=] {\n\t\t\treason->setFocusFast();\n\t\t});\n\t\t*callback = [=, weak = base::make_weak(box)] {\n\t\t\tconst auto item = show->session().data().message(id);\n\t\t\tif (!item) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tSendDecline(show, item, reason->getLastText().trimmed());\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t};\n\t\treason->submits(\n\t\t) | rpl::on_next([=](Qt::KeyboardModifiers modifiers) {\n\t\t\tif (!(modifiers & Qt::ShiftModifier)) {\n\t\t\t\t(*callback)();\n\t\t\t}\n\t\t}, box->lifetime());\n\t}));\n}\n\nstruct SendSuggestState {\n\tSendPaymentHelper sendPayment;\n};\nvoid SendSuggest(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<HistoryItem*> item,\n\t\tstd::shared_ptr<SendSuggestState> state,\n\t\tFn<void(SuggestOptions&)> modify,\n\t\tFn<void()> done = nullptr,\n\t\tint starsApproved = 0) {\n\tconst auto suggestion = item->Get<HistoryMessageSuggestion>();\n\tconst auto id = item->fullId();\n\tconst auto withPaymentApproved = [=](int stars) {\n\t\tif (const auto item = show->session().data().message(id)) {\n\t\t\tSendSuggest(show, item, state, modify, done, stars);\n\t\t}\n\t};\n\tconst auto isForward = item->Get<HistoryMessageForwarded>();\n\tauto action = SendAction(item->history());\n\taction.options.suggest.exists = 1;\n\tif (suggestion) {\n\t\taction.options.suggest.date = suggestion->date;\n\t\taction.options.suggest.priceWhole = suggestion->price.whole();\n\t\taction.options.suggest.priceNano = suggestion->price.nano();\n\t\taction.options.suggest.ton = suggestion->price.ton() ? 1 : 0;\n\t}\n\tmodify(action.options.suggest);\n\taction.options.starsApproved = starsApproved;\n\taction.replyTo.monoforumPeerId = item->history()->amMonoforumAdmin()\n\t\t? item->sublistPeerId()\n\t\t: PeerId();\n\taction.replyTo.messageId = item->fullId();\n\n\tconst auto checked = state->sendPayment.check(\n\t\tshow,\n\t\titem->history()->peer,\n\t\taction.options,\n\t\t1,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn;\n\t}\n\n\tshow->session().api().sendAction(action);\n\tshow->session().api().forwardMessages({\n\t\t.items = { item },\n\t\t.options = (isForward\n\t\t\t? Data::ForwardOptions::PreserveInfo\n\t\t\t: Data::ForwardOptions::NoSenderNames),\n\t\t}, action);\n\tif (const auto onstack = done) {\n\t\tonstack();\n\t}\n}\n\nvoid SuggestApprovalDate(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto suggestion = item->Get<HistoryMessageSuggestion>();\n\tif (!suggestion) {\n\t\treturn;\n\t}\n\tconst auto id = item->fullId();\n\tconst auto state = std::make_shared<SendSuggestState>();\n\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tconst auto done = [=](TimeId result) {\n\t\tconst auto item = show->session().data().message(id);\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto close = [=] {\n\t\t\tif (const auto strong = weak->get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t};\n\t\tSendSuggest(\n\t\t\tshow,\n\t\t\titem,\n\t\t\tstate,\n\t\t\t[=](SuggestOptions &options) { options.date = result; },\n\t\t\tclose);\n\t};\n\tusing namespace HistoryView;\n\tauto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{\n\t\t.session = &show->session(),\n\t\t.done = done,\n\t\t.value = suggestion->date,\n\t\t.mode = SuggestMode::Change,\n\t});\n\t*weak = dateBox.data();\n\tshow->show(std::move(dateBox));\n}\n\nvoid SuggestOfferForMessage(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<HistoryItem*> item,\n\t\tSuggestOptions values,\n\t\tHistoryView::SuggestMode mode) {\n\tconst auto id = item->fullId();\n\tconst auto state = std::make_shared<SendSuggestState>();\n\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tconst auto done = [=](SuggestOptions result) {\n\t\tconst auto item = show->session().data().message(id);\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto close = [=] {\n\t\t\tif (const auto strong = weak->get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t};\n\t\tSendSuggest(\n\t\t\tshow,\n\t\t\titem,\n\t\t\tstate,\n\t\t\t[=](SuggestOptions &options) { options = result; },\n\t\t\tclose);\n\t};\n\tusing namespace HistoryView;\n\tauto priceBox = Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{\n\t\t.peer = item->history()->peer,\n\t\t.done = done,\n\t\t.value = values,\n\t\t.mode = mode,\n\t});\n\t*weak = priceBox.data();\n\tshow->show(std::move(priceBox));\n}\n\nvoid SuggestApprovalPrice(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto suggestion = item->Get<HistoryMessageSuggestion>();\n\tif (!suggestion) {\n\t\treturn;\n\t}\n\tusing namespace HistoryView;\n\tSuggestOfferForMessage(show, item, {\n\t\t.exists = uint32(1),\n\t\t.priceWhole = uint32(suggestion->price.whole()),\n\t\t.priceNano = uint32(suggestion->price.nano()),\n\t\t.ton = uint32(suggestion->price.ton() ? 1 : 0),\n\t\t.date = suggestion->date,\n\t}, SuggestMode::Change);\n}\n\nvoid ConfirmGiftSaleAccept(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<HistoryMessageSuggestion*> suggestion) {\n\tShowGiftSaleAcceptBox(window, item, suggestion);\n}\n\nvoid ConfirmGiftSaleDecline(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<HistoryMessageSuggestion*> suggestion) {\n\tShowGiftSaleRejectBox(window, item, suggestion);\n}\n\nvoid RespondToNoForwardsRequest(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<HistoryServiceNoForwardsRequest*> request,\n\t\tbool accept) {\n\tif (request->requestId) {\n\t\treturn;\n\t}\n\tconst auto id = item->fullId();\n\tconst auto session = &item->history()->session();\n\tconst auto peer = item->history()->peer;\n\tconst auto msgId = item->id;\n\tconst auto finish = [=] {\n\t\tif (const auto item = session->data().message(id)) {\n\t\t\tif (const auto r = item->Get<HistoryServiceNoForwardsRequest>()) {\n\t\t\t\tr->requestId = 0;\n\t\t\t}\n\t\t}\n\t};\n\tusing Flag = MTPmessages_ToggleNoForwards::Flag;\n\trequest->requestId = session->api().request(MTPmessages_ToggleNoForwards(\n\t\tMTP_flags(Flag::f_request_msg_id),\n\t\tpeer->input(),\n\t\tMTP_bool(!accept),\n\t\tMTP_int(msgId)\n\t)).done([=](const MTPUpdates &result) {\n\t\tsession->api().applyUpdates(result);\n\t\tfinish();\n\t}).fail([=](const MTP::Error &error) {\n\t\tcontroller->showToast(error.type());\n\t\tfinish();\n\t}).send();\n}\n\n} // namespace\n\nstd::shared_ptr<ClickHandler> AcceptClickHandler(\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto session = &item->history()->session();\n\tconst auto id = item->fullId();\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tconst auto controller = my.sessionWindow.get();\n\t\tif (!controller || &controller->session() != session) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto item = session->data().message(id);\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto show = controller->uiShow();\n\t\tconst auto suggestion = item->Get<HistoryMessageSuggestion>();\n\t\tconst auto nfRequest = item->Get<HistoryServiceNoForwardsRequest>();\n\t\tif (!suggestion && !nfRequest) {\n\t\t\treturn;\n\t\t} else if (nfRequest) {\n\t\t\tRespondToNoForwardsRequest(controller, item, nfRequest, true);\n\t\t\treturn;\n\t\t} else if (suggestion->gift) {\n\t\t\tConfirmGiftSaleAccept(controller, item, suggestion);\n\t\t} else if (!suggestion->date) {\n\t\t\tRequestApprovalDate(show, item);\n\t\t} else {\n\t\t\tConfirmApproval(show, item);\n\t\t}\n\t});\n}\n\nstd::shared_ptr<ClickHandler> DeclineClickHandler(\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto session = &item->history()->session();\n\tconst auto id = item->fullId();\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tconst auto controller = my.sessionWindow.get();\n\t\tif (!controller || &controller->session() != session) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto item = session->data().message(id);\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto nfRequest = item->Get<HistoryServiceNoForwardsRequest>();\n\t\tif (nfRequest) {\n\t\t\tRespondToNoForwardsRequest(controller, item, nfRequest, false);\n\t\t\treturn;\n\t\t}\n\t\tconst auto suggestion = item->Get<HistoryMessageSuggestion>();\n\t\tif (suggestion && suggestion->gift) {\n\t\t\tConfirmGiftSaleDecline(controller, item, suggestion);\n\t\t} else {\n\t\t\tRequestDeclineComment(controller->uiShow(), item);\n\t\t}\n\t});\n}\n\nstd::shared_ptr<ClickHandler> SuggestChangesClickHandler(\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto session = &item->history()->session();\n\tconst auto id = item->fullId();\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tconst auto window = my.sessionWindow.get();\n\t\tif (!window || &window->session() != session) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto item = session->data().message(id);\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto menu = Ui::CreateChild<Ui::PopupMenu>(\n\t\t\twindow->widget(),\n\t\t\tst::popupMenuWithIcons);\n\t\tif (HistoryView::CanEditSuggestedMessage(item)) {\n\t\t\tmenu->addAction(tr::lng_suggest_menu_edit_message(tr::now), [=] {\n\t\t\t\tconst auto item = session->data().message(id);\n\t\t\t\tif (!item) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto suggestion = item->Get<HistoryMessageSuggestion>();\n\t\t\t\tif (!suggestion) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto history = item->history();\n\t\t\t\tconst auto editData = PrepareEditText(item);\n\t\t\t\tconst auto cursor = MessageCursor{\n\t\t\t\t\tint(editData.text.size()),\n\t\t\t\t\tint(editData.text.size()),\n\t\t\t\t\tUi::kQFixedMax\n\t\t\t\t};\n\t\t\t\tconst auto monoforumPeerId = history->amMonoforumAdmin()\n\t\t\t\t\t? item->sublistPeerId()\n\t\t\t\t\t: PeerId();\n\t\t\t\tconst auto previewDraft = Data::WebPageDraft::FromItem(item);\n\t\t\t\thistory->setLocalEditDraft(std::make_unique<Data::Draft>(\n\t\t\t\t\teditData,\n\t\t\t\t\tFullReplyTo{\n\t\t\t\t\t\t.messageId = FullMsgId(history->peer->id, item->id),\n\t\t\t\t\t\t.monoforumPeerId = monoforumPeerId,\n\t\t\t\t\t},\n\t\t\t\t\tSuggestOptions{\n\t\t\t\t\t\t.exists = uint32(1),\n\t\t\t\t\t\t.priceWhole = uint32(suggestion->price.whole()),\n\t\t\t\t\t\t.priceNano = uint32(suggestion->price.nano()),\n\t\t\t\t\t\t.ton = uint32(suggestion->price.ton() ? 1 : 0),\n\t\t\t\t\t\t.date = suggestion->date,\n\t\t\t\t\t},\n\t\t\t\t\tcursor,\n\t\t\t\t\tpreviewDraft));\n\t\t\t\thistory->session().changes().entryUpdated(\n\t\t\t\t\t(monoforumPeerId\n\t\t\t\t\t\t? item->savedSublist()\n\t\t\t\t\t\t: (Data::Thread*)history.get()),\n\t\t\t\t\tData::EntryUpdate::Flag::LocalDraftSet);\n\t\t\t}, &st::menuIconEdit);\n\t\t}\n\t\tmenu->addAction(tr::lng_suggest_menu_edit_price(tr::now), [=] {\n\t\t\tif (const auto item = session->data().message(id)) {\n\t\t\t\tSuggestApprovalPrice(window->uiShow(), item);\n\t\t\t}\n\t\t}, &st::menuIconTagSell);\n\t\tmenu->addAction(tr::lng_suggest_menu_edit_time(tr::now), [=] {\n\t\t\tif (const auto item = session->data().message(id)) {\n\t\t\t\tSuggestApprovalDate(window->uiShow(), item);\n\t\t\t}\n\t\t}, &st::menuIconSchedule);\n\t\tmenu->popup(QCursor::pos());\n\t});\n}\n\nvoid AddOfferToMessage(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tFullMsgId itemId) {\n\tconst auto session = &show->session();\n\tconst auto item = session->data().message(itemId);\n\tif (!item || !HistoryView::CanAddOfferToMessage(item)) {\n\t\treturn;\n\t}\n\tSuggestOfferForMessage(show, item, {}, HistoryView::SuggestMode::New);\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_suggest_post.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass ClickHandler;\n\nnamespace Main {\nclass SessionShow;\n} // namespace Main\n\nnamespace Api {\n\n[[nodiscard]] std::shared_ptr<ClickHandler> AcceptClickHandler(\n\tnot_null<HistoryItem*> item);\n[[nodiscard]] std::shared_ptr<ClickHandler> DeclineClickHandler(\n\tnot_null<HistoryItem*> item);\n[[nodiscard]] std::shared_ptr<ClickHandler> SuggestChangesClickHandler(\n\tnot_null<HistoryItem*> item);\n\nvoid AddOfferToMessage(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tFullMsgId itemId);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_text_entities.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_text_entities.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/stickers/data_stickers_set.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"main/main_session.h\"\n\nnamespace Api {\nnamespace {\n\nusing namespace TextUtilities;\n\n[[nodiscard]] QString CustomEmojiEntityData(\n\t\tconst MTPDmessageEntityCustomEmoji &data) {\n\treturn Data::SerializeCustomEmojiId(data.vdocument_id().v);\n}\n\n[[nodiscard]] std::optional<MTPMessageEntity> CustomEmojiEntity(\n\t\tMTPint offset,\n\t\tMTPint length,\n\t\tconst QString &data) {\n\tconst auto parsed = Data::ParseCustomEmojiData(data);\n\tif (!parsed) {\n\t\treturn {};\n\t}\n\treturn MTP_messageEntityCustomEmoji(\n\t\toffset,\n\t\tlength,\n\t\tMTP_long(parsed));\n}\n\n[[nodiscard]] std::optional<MTPMessageEntity> MentionNameEntity(\n\t\tnot_null<Main::Session*> session,\n\t\tMTPint offset,\n\t\tMTPint length,\n\t\tconst QString &data) {\n\tconst auto parsed = MentionNameDataToFields(data);\n\tif (!parsed.userId || parsed.selfId != session->userId().bare) {\n\t\treturn {};\n\t}\n\tconst auto user = session->data().user(UserId(parsed.userId));\n\tconst auto item = user->isLoaded()\n\t\t? nullptr\n\t\t: user->owner().messageWithPeer(user->id);\n\tconst auto input = item\n\t\t? MTP_inputUserFromMessage(\n\t\t\titem->history()->peer->input(),\n\t\t\tMTP_int(item->id.bare),\n\t\t\tMTP_long(parsed.userId))\n\t\t: (parsed.userId == parsed.selfId)\n\t\t? MTP_inputUserSelf()\n\t\t: user->isLoaded()\n\t\t? user->inputUser()\n\t\t: MTP_inputUser(\n\t\t\tMTP_long(parsed.userId),\n\t\t\tMTP_long(parsed.accessHash));\n\treturn MTP_inputMessageEntityMentionName(offset, length, input);\n}\n\n} // namespace\n\nEntitiesInText EntitiesFromMTP(\n\t\tMain::Session *session,\n\t\tconst QVector<MTPMessageEntity> &entities) {\n\tif (entities.isEmpty()) {\n\t\treturn {};\n\t}\n\tauto result = EntitiesInText();\n\tresult.reserve(entities.size());\n\n\tfor (const auto &entity : entities) {\n\t\tentity.match([&](const MTPDmessageEntityUnknown &d) {\n\t\t}, [&](const MTPDmessageEntityMention &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::Mention,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityHashtag &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::Hashtag,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityBotCommand &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::BotCommand,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityUrl &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::Url,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityEmail &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::Email,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityBold &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::Bold,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityItalic &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::Italic,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityCode &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::Code,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityPre &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::Pre,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t\tqs(d.vlanguage()),\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityTextUrl &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::CustomUrl,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t\tqs(d.vurl()),\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityMentionName &d) {\n\t\t\tif (!session) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto userId = UserId(d.vuser_id());\n\t\t\tconst auto user = session->data().userLoaded(userId);\n\t\t\tconst auto data = MentionNameDataFromFields({\n\t\t\t\t.selfId = session->userId().bare,\n\t\t\t\t.userId = userId.bare,\n\t\t\t\t.accessHash = user ? user->accessHash() : 0,\n\t\t\t});\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::MentionName,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t\tdata,\n\t\t\t});\n\t\t}, [&](const MTPDinputMessageEntityMentionName &d) {\n\t\t\tif (!session) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto data = d.vuser_id().match([&](\n\t\t\t\t\tconst MTPDinputUserSelf &) {\n\t\t\t\treturn MentionNameDataFromFields({\n\t\t\t\t\t.selfId = session->userId().bare,\n\t\t\t\t\t.userId = session->userId().bare,\n\t\t\t\t\t.accessHash = session->user()->accessHash(),\n\t\t\t\t});\n\t\t\t}, [&](const MTPDinputUser &data) {\n\t\t\t\treturn MentionNameDataFromFields({\n\t\t\t\t\t.selfId = session->userId().bare,\n\t\t\t\t\t.userId = UserId(data.vuser_id()).bare,\n\t\t\t\t\t.accessHash = data.vaccess_hash().v,\n\t\t\t\t});\n\t\t\t}, [](const auto &) {\n\t\t\t\treturn QString();\n\t\t\t});\n\t\t\tif (!data.isEmpty()) {\n\t\t\t\tresult.push_back({\n\t\t\t\t\tEntityType::MentionName,\n\t\t\t\t\td.voffset().v,\n\t\t\t\t\td.vlength().v,\n\t\t\t\t\tdata,\n\t\t\t\t});\n\t\t\t}\n\t\t}, [&](const MTPDmessageEntityPhone &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::Phone,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityCashtag &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::Cashtag,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityUnderline &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::Underline,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityStrike &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::StrikeOut,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityBankCard &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::BankCard,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntitySpoiler &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::Spoiler,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityCustomEmoji &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::CustomEmoji,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t\tCustomEmojiEntityData(d),\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityBlockquote &d) {\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::Blockquote,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t\td.is_collapsed() ? u\"1\"_q : QString(),\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityFormattedDate &d) {\n\t\t\tauto flags = FormattedDateFlags();\n\t\t\tif (d.is_relative()) {\n\t\t\t\tflags |= FormattedDateFlag::Relative;\n\t\t\t}\n\t\t\tif (d.is_short_time()) {\n\t\t\t\tflags |= FormattedDateFlag::ShortTime;\n\t\t\t}\n\t\t\tif (d.is_long_time()) {\n\t\t\t\tflags |= FormattedDateFlag::LongTime;\n\t\t\t}\n\t\t\tif (d.is_short_date()) {\n\t\t\t\tflags |= FormattedDateFlag::ShortDate;\n\t\t\t}\n\t\t\tif (d.is_long_date()) {\n\t\t\t\tflags |= FormattedDateFlag::LongDate;\n\t\t\t}\n\t\t\tif (d.is_day_of_week()) {\n\t\t\t\tflags |= FormattedDateFlag::DayOfWeek;\n\t\t\t}\n\t\t\tresult.push_back({\n\t\t\t\tEntityType::FormattedDate,\n\t\t\t\td.voffset().v,\n\t\t\t\td.vlength().v,\n\t\t\t\tSerializeFormattedDateData(d.vdate().v, flags),\n\t\t\t});\n\t\t}, [&](const MTPDmessageEntityDiffInsert &) {\n\t\t}, [&](const MTPDmessageEntityDiffReplace &) {\n\t\t}, [&](const MTPDmessageEntityDiffDelete &) {\n\t\t});\n\t}\n\treturn result;\n}\n\nMTPVector<MTPMessageEntity> EntitiesToMTP(\n\t\tMain::Session *session,\n\t\tconst EntitiesInText &entities,\n\t\tConvertOption option) {\n\tauto v = QVector<MTPMessageEntity>();\n\tv.reserve(entities.size());\n\tfor (const auto &entity : entities) {\n\t\tif (entity.length() <= 0) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (option == ConvertOption::SkipLocal\n\t\t\t&& entity.type() != EntityType::Bold\n\t\t\t//&& entity.type() != EntityType::Semibold // Not in API.\n\t\t\t&& entity.type() != EntityType::Italic\n\t\t\t&& entity.type() != EntityType::Underline\n\t\t\t&& entity.type() != EntityType::StrikeOut\n\t\t\t&& entity.type() != EntityType::Code // #TODO entities\n\t\t\t&& entity.type() != EntityType::Pre\n\t\t\t&& entity.type() != EntityType::Blockquote\n\t\t\t&& entity.type() != EntityType::Spoiler\n\t\t\t&& entity.type() != EntityType::MentionName\n\t\t\t&& entity.type() != EntityType::CustomUrl\n\t\t\t&& entity.type() != EntityType::CustomEmoji\n\t\t\t&& entity.type() != EntityType::FormattedDate) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto offset = MTP_int(entity.offset());\n\t\tauto length = MTP_int(entity.length());\n\t\tswitch (entity.type()) {\n\t\tcase EntityType::Url: {\n\t\t\tv.push_back(MTP_messageEntityUrl(offset, length));\n\t\t} break;\n\t\tcase EntityType::CustomUrl: {\n\t\t\tv.push_back(\n\t\t\t\tMTP_messageEntityTextUrl(\n\t\t\t\t\toffset,\n\t\t\t\t\tlength,\n\t\t\t\t\tMTP_string(entity.data())));\n\t\t} break;\n\t\tcase EntityType::Email: {\n\t\t\tv.push_back(MTP_messageEntityEmail(offset, length));\n\t\t} break;\n\t\tcase EntityType::Phone: {\n\t\t\tv.push_back(MTP_messageEntityPhone(offset, length));\n\t\t} break;\n\t\tcase EntityType::BankCard: {\n\t\t\tv.push_back(MTP_messageEntityBankCard(offset, length));\n\t\t} break;\n\t\tcase EntityType::Hashtag: {\n\t\t\tv.push_back(MTP_messageEntityHashtag(offset, length));\n\t\t} break;\n\t\tcase EntityType::Cashtag: {\n\t\t\tv.push_back(MTP_messageEntityCashtag(offset, length));\n\t\t} break;\n\t\tcase EntityType::Mention: {\n\t\t\tv.push_back(MTP_messageEntityMention(offset, length));\n\t\t} break;\n\t\tcase EntityType::MentionName: {\n\t\t\tAssert(session != nullptr);\n\t\t\tconst auto valid = MentionNameEntity(\n\t\t\t\tsession,\n\t\t\t\toffset,\n\t\t\t\tlength,\n\t\t\t\tentity.data());\n\t\t\tif (valid) {\n\t\t\t\tv.push_back(*valid);\n\t\t\t}\n\t\t} break;\n\t\tcase EntityType::BotCommand: {\n\t\t\tv.push_back(MTP_messageEntityBotCommand(offset, length));\n\t\t} break;\n\t\tcase EntityType::Bold: {\n\t\t\tv.push_back(MTP_messageEntityBold(offset, length));\n\t\t} break;\n\t\tcase EntityType::Italic: {\n\t\t\tv.push_back(MTP_messageEntityItalic(offset, length));\n\t\t} break;\n\t\tcase EntityType::Underline: {\n\t\t\tv.push_back(MTP_messageEntityUnderline(offset, length));\n\t\t} break;\n\t\tcase EntityType::StrikeOut: {\n\t\t\tv.push_back(MTP_messageEntityStrike(offset, length));\n\t\t} break;\n\t\tcase EntityType::Code: {\n\t\t\t// #TODO entities.\n\t\t\tv.push_back(MTP_messageEntityCode(offset, length));\n\t\t} break;\n\t\tcase EntityType::Pre: {\n\t\t\tv.push_back(\n\t\t\t\tMTP_messageEntityPre(\n\t\t\t\t\toffset,\n\t\t\t\t\tlength,\n\t\t\t\t\tMTP_string(entity.data())));\n\t\t} break;\n\t\tcase EntityType::Blockquote: {\n\t\t\tusing Flag = MTPDmessageEntityBlockquote::Flag;\n\t\t\tconst auto collapsed = !entity.data().isEmpty();\n\t\t\tv.push_back(\n\t\t\t\tMTP_messageEntityBlockquote(\n\t\t\t\t\tMTP_flags(collapsed ? Flag::f_collapsed : Flag()),\n\t\t\t\t\toffset,\n\t\t\t\t\tlength));\n\t\t} break;\n\t\tcase EntityType::Spoiler: {\n\t\t\tv.push_back(MTP_messageEntitySpoiler(offset, length));\n\t\t} break;\n\t\tcase EntityType::CustomEmoji: {\n\t\t\tconst auto valid = CustomEmojiEntity(\n\t\t\t\toffset,\n\t\t\t\tlength,\n\t\t\t\tentity.data());\n\t\t\tif (valid) {\n\t\t\t\tv.push_back(*valid);\n\t\t\t}\n\t\t} break;\n\t\tcase EntityType::FormattedDate: {\n\t\t\tconst auto [date, dateFlags] = DeserializeFormattedDateData(\n\t\t\t\tentity.data());\n\t\t\tif (date) {\n\t\t\t\tusing Flag = MTPDmessageEntityFormattedDate::Flag;\n\t\t\t\tauto mtpFlags = MTPDmessageEntityFormattedDate::Flags();\n\t\t\t\tif (dateFlags & FormattedDateFlag::Relative) {\n\t\t\t\t\tmtpFlags |= Flag::f_relative;\n\t\t\t\t}\n\t\t\t\tif (dateFlags & FormattedDateFlag::ShortTime) {\n\t\t\t\t\tmtpFlags |= Flag::f_short_time;\n\t\t\t\t}\n\t\t\t\tif (dateFlags & FormattedDateFlag::LongTime) {\n\t\t\t\t\tmtpFlags |= Flag::f_long_time;\n\t\t\t\t}\n\t\t\t\tif (dateFlags & FormattedDateFlag::ShortDate) {\n\t\t\t\t\tmtpFlags |= Flag::f_short_date;\n\t\t\t\t}\n\t\t\t\tif (dateFlags & FormattedDateFlag::LongDate) {\n\t\t\t\t\tmtpFlags |= Flag::f_long_date;\n\t\t\t\t}\n\t\t\t\tif (dateFlags & FormattedDateFlag::DayOfWeek) {\n\t\t\t\t\tmtpFlags |= Flag::f_day_of_week;\n\t\t\t\t}\n\t\t\t\tv.push_back(MTP_messageEntityFormattedDate(\n\t\t\t\t\tMTP_flags(mtpFlags),\n\t\t\t\t\toffset,\n\t\t\t\t\tlength,\n\t\t\t\t\tMTP_int(date)));\n\t\t\t}\n\t\t} break;\n\t\t}\n\t}\n\treturn MTP_vector<MTPMessageEntity>(std::move(v));\n}\n\nTextWithEntities ParseTextWithEntities(\n\t\tMain::Session *session,\n\t\tconst MTPTextWithEntities &text) {\n\tconst auto &data = text.data();\n\treturn {\n\t\t.text = qs(data.vtext()),\n\t\t.entities = EntitiesFromMTP(session, data.ventities().v),\n\t};\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_text_entities.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/text/text_entity.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nenum class ConvertOption {\n\tWithLocal,\n\tSkipLocal,\n};\n\n[[nodiscard]] EntitiesInText EntitiesFromMTP(\n\tMain::Session *session,\n\tconst QVector<MTPMessageEntity> &entities);\n\n[[nodiscard]] MTPVector<MTPMessageEntity> EntitiesToMTP(\n\tMain::Session *session,\n\tconst EntitiesInText &entities,\n\tConvertOption option = ConvertOption::WithLocal);\n\n[[nodiscard]] TextWithEntities ParseTextWithEntities(\n\tMain::Session *session,\n\tconst MTPTextWithEntities &text);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_todo_lists.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_todo_lists.h\"\n\n#include \"api/api_editing.h\"\n#include \"apiwrap.h\"\n#include \"base/random.h\"\n#include \"data/business/data_shortcut_messages.h\" // ShortcutIdToMTP\n#include \"data/data_changes.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_todo_list.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\" // ShouldSendSilent\n#include \"main/main_session.h\"\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kSendTogglesDelay = 3 * crl::time(1000);\n\n} // namespace\n\nTodoLists::TodoLists(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance())\n, _sendTimer([=] { sendAccumulatedToggles(false); }) {\n}\n\nvoid TodoLists::create(\n\t\tconst TodoListData &data,\n\t\tSendAction action,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail) {\n\t_session->api().sendAction(action);\n\n\tconst auto history = action.history;\n\tconst auto peer = history->peer;\n\tconst auto topicRootId = action.replyTo.messageId\n\t\t? action.replyTo.topicRootId\n\t\t: 0;\n\tconst auto monoforumPeerId = action.replyTo.monoforumPeerId;\n\tauto sendFlags = MTPmessages_SendMedia::Flags(0);\n\tif (action.replyTo) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;\n\t}\n\tconst auto clearCloudDraft = action.clearDraft;\n\tif (clearCloudDraft) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_clear_draft;\n\t\thistory->clearLocalDraft(topicRootId, monoforumPeerId);\n\t\thistory->clearCloudDraft(topicRootId, monoforumPeerId);\n\t\thistory->startSavingCloudDraft(topicRootId, monoforumPeerId);\n\t}\n\tconst auto silentPost = ShouldSendSilent(peer, action.options);\n\tconst auto starsPaid = std::min(\n\t\tpeer->starsPerMessageChecked(),\n\t\taction.options.starsApproved);\n\tif (silentPost) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_silent;\n\t}\n\tif (action.options.scheduled) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;\n\t\tif (action.options.scheduleRepeatPeriod) {\n\t\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;\n\t\t}\n\t}\n\tif (action.options.shortcutId) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;\n\t}\n\tif (action.options.effectId) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_effect;\n\t}\n\tif (action.options.suggest) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;\n\t}\n\tif (starsPaid) {\n\t\taction.options.starsApproved -= starsPaid;\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;\n\t}\n\tconst auto sendAs = action.options.sendAs;\n\tif (sendAs) {\n\t\tsendFlags |= MTPmessages_SendMedia::Flag::f_send_as;\n\t}\n\tauto &histories = history->owner().histories();\n\tconst auto randomId = base::RandomValue<uint64>();\n\thistories.sendPreparedMessage(\n\t\thistory,\n\t\taction.replyTo,\n\t\trandomId,\n\t\tData::Histories::PrepareMessage<MTPmessages_SendMedia>(\n\t\t\tMTP_flags(sendFlags),\n\t\t\tpeer->input(),\n\t\t\tData::Histories::ReplyToPlaceholder(),\n\t\t\tTodoListDataToInputMedia(&data),\n\t\t\tMTP_string(),\n\t\t\tMTP_long(randomId),\n\t\t\tMTPReplyMarkup(),\n\t\t\tMTPVector<MTPMessageEntity>(),\n\t\t\tMTP_int(action.options.scheduled),\n\t\t\tMTP_int(action.options.scheduleRepeatPeriod),\n\t\t\t(sendAs ? sendAs->input() : MTP_inputPeerEmpty()),\n\t\t\tData::ShortcutIdToMTP(_session, action.options.shortcutId),\n\t\t\tMTP_long(action.options.effectId),\n\t\t\tMTP_long(starsPaid),\n\t\t\tSuggestToMTP(action.options.suggest)\n\t\t), [=](const MTPUpdates &result, const MTP::Response &response) {\n\t\tif (clearCloudDraft) {\n\t\t\thistory->finishSavingCloudDraft(\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tUnixtimeFromMsgId(response.outerMsgId));\n\t\t}\n\t\t_session->changes().historyUpdated(\n\t\t\thistory,\n\t\t\t(action.options.scheduled\n\t\t\t\t? Data::HistoryUpdate::Flag::ScheduledSent\n\t\t\t\t: Data::HistoryUpdate::Flag::MessageSent));\n\t\tif (const auto onstack = done) {\n\t\t\tonstack();\n\t\t}\n\t}, [=](const MTP::Error &error, const MTP::Response &response) {\n\t\tif (clearCloudDraft) {\n\t\t\thistory->finishSavingCloudDraft(\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tUnixtimeFromMsgId(response.outerMsgId));\n\t\t}\n\t\tif (const auto onstack = fail) {\n\t\t\tonstack(error.type());\n\t\t}\n\t});\n}\n\nvoid TodoLists::edit(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst TodoListData &data,\n\t\tSendOptions options,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail) {\n\tEditTodoList(item, data, options, [=](mtpRequestId) {\n\t\tif (const auto onstack = done) {\n\t\t\tonstack();\n\t\t}\n\t}, [=](const QString &error, mtpRequestId) {\n\t\tif (const auto onstack = fail) {\n\t\t\tonstack(error);\n\t\t}\n\t});\n}\n\nvoid TodoLists::add(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst std::vector<TodoListItem> &items,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail) {\n\tif (items.empty()) {\n\t\treturn;\n\t}\n\tconst auto session = _session;\n\t_session->api().request(MTPmessages_AppendTodoList(\n\t\titem->history()->peer->input(),\n\t\tMTP_int(item->id.bare),\n\t\tTodoListItemsToMTP(&item->history()->session(), items)\n\t)).done([=](const MTPUpdates &result) {\n\t\tsession->api().applyUpdates(result);\n\t\tif (const auto onstack = done) {\n\t\t\tonstack();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (const auto onstack = fail) {\n\t\t\tonstack(error.type());\n\t\t}\n\t}).send();\n}\n\nvoid TodoLists::toggleCompletion(FullMsgId itemId, int id, bool completed) {\n\tauto &entry = _toggles[itemId];\n\tif (completed) {\n\t\tconst auto changed1 = entry.completed.emplace(id).second;\n\t\tconst auto changed2 = entry.incompleted.remove(id);\n\t\tif (!changed1 && !changed2) {\n\t\t\treturn;\n\t\t}\n\t} else {\n\t\tconst auto changed1 = entry.incompleted.emplace(id).second;\n\t\tconst auto changed2 = entry.completed.remove(id);\n\t\tif (!changed1 && !changed2) {\n\t\t\treturn;\n\t\t}\n\t}\n\tentry.scheduled = crl::now();\n\tif (!entry.requestId && !_sendTimer.isActive()) {\n\t\t_sendTimer.callOnce(kSendTogglesDelay);\n\t}\n}\n\nvoid TodoLists::sendAccumulatedToggles(bool force) {\n\tconst auto now = crl::now();\n\tauto nearest = crl::time(0);\n\tfor (auto &[itemId, entry] : _toggles) {\n\t\tif (entry.requestId) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto wait = entry.scheduled + kSendTogglesDelay - now;\n\t\tif (wait <= 0) {\n\t\t\tentry.scheduled = 0;\n\t\t\tsend(itemId, entry);\n\t\t} else if (!nearest || nearest > wait) {\n\t\t\tnearest = wait;\n\t\t}\n\t}\n\tif (nearest > 0) {\n\t\t_sendTimer.callOnce(nearest);\n\t}\n}\n\nvoid TodoLists::send(FullMsgId itemId, Accumulated &entry) {\n\tconst auto item = _session->data().message(itemId);\n\tif (!item) {\n\t\treturn;\n\t}\n\tauto completed = entry.completed\n\t\t| ranges::views::transform([](int id) { return MTP_int(id); });\n\tauto incompleted = entry.incompleted\n\t\t| ranges::views::transform([](int id) { return MTP_int(id); });\n\tentry.requestId = _api.request(MTPmessages_ToggleTodoCompleted(\n\t\titem->history()->peer->input(),\n\t\tMTP_int(item->id),\n\t\tMTP_vector_from_range(completed),\n\t\tMTP_vector_from_range(incompleted)\n\t)).done([=](const MTPUpdates &result) {\n\t\t_session->api().applyUpdates(result);\n\t\tfinishRequest(itemId);\n\t}).fail([=](const MTP::Error &error) {\n\t\tfinishRequest(itemId);\n\t}).send();\n\tentry.completed.clear();\n\tentry.incompleted.clear();\n}\n\nvoid TodoLists::finishRequest(FullMsgId itemId) {\n\tauto &entry = _toggles[itemId];\n\tentry.requestId = 0;\n\tif (entry.completed.empty() && entry.incompleted.empty()) {\n\t\t_toggles.remove(itemId);\n\t} else {\n\t\tsendAccumulatedToggles(false);\n\t}\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_todo_lists.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"mtproto/sender.h\"\n\nclass ApiWrap;\nclass HistoryItem;\nstruct TodoListItem;\nstruct TodoListData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nstruct SendAction;\nstruct SendOptions;\n\nclass TodoLists final {\npublic:\n\texplicit TodoLists(not_null<ApiWrap*> api);\n\n\tvoid create(\n\t\tconst TodoListData &data,\n\t\tSendAction action,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail);\n\tvoid edit(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst TodoListData &data,\n\t\tSendOptions options,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail);\n\tvoid add(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst std::vector<TodoListItem> &items,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail);\n\tvoid toggleCompletion(FullMsgId itemId, int id, bool completed);\n\nprivate:\n\tstruct Accumulated {\n\t\tbase::flat_set<int> completed;\n\t\tbase::flat_set<int> incompleted;\n\t\tcrl::time scheduled = 0;\n\t\tmtpRequestId requestId = 0;\n\t};\n\n\tvoid sendAccumulatedToggles(bool force);\n\tvoid send(FullMsgId itemId, Accumulated &entry);\n\tvoid finishRequest(FullMsgId itemId);\n\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\n\tbase::flat_map<FullMsgId, Accumulated> _toggles;\n\tbase::Timer _sendTimer;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_toggling_media.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_toggling_media.h\"\n\n#include \"apiwrap.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"window/window_session_controller.h\"\n#include \"main/main_session.h\"\n\nnamespace Api {\nnamespace {\n\ntemplate <typename ToggleRequestCallback, typename DoneCallback>\nvoid ToggleExistingMedia(\n\t\tnot_null<DocumentData*> document,\n\t\tData::FileOrigin origin,\n\t\tToggleRequestCallback toggleRequest,\n\t\tDoneCallback &&done) {\n\tconst auto api = &document->session().api();\n\n\tauto performRequest = [=](const auto &repeatRequest) -> void {\n\t\tconst auto usedFileReference = document->fileReference();\n\t\tapi->request(\n\t\t\ttoggleRequest()\n\t\t).done(done).fail([=](const MTP::Error &error) {\n\t\t\tif (error.code() == 400\n\t\t\t\t&& error.type().startsWith(u\"FILE_REFERENCE_\"_q)) {\n\t\t\t\tauto refreshed = [=](const Data::UpdatedFileReferences &d) {\n\t\t\t\t\tif (document->fileReference() != usedFileReference) {\n\t\t\t\t\t\trepeatRequest(repeatRequest);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tapi->refreshFileReference(origin, std::move(refreshed));\n\t\t\t}\n\t\t}).send();\n\t};\n\tperformRequest(performRequest);\n}\n\n} // namespace\n\nvoid ToggleFavedSticker(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document,\n\t\tData::FileOrigin origin) {\n\tToggleFavedSticker(\n\t\tstd::move(show),\n\t\tdocument,\n\t\tstd::move(origin),\n\t\t!document->owner().stickers().isFaved(document));\n}\n\nvoid ToggleFavedSticker(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document,\n\t\tData::FileOrigin origin,\n\t\tbool faved) {\n\tif (faved && !document->sticker()) {\n\t\treturn;\n\t}\n\tauto done = [=] {\n\t\tdocument->owner().stickers().setFaved(show, document, faved);\n\t};\n\tToggleExistingMedia(\n\t\tdocument,\n\t\tstd::move(origin),\n\t\t[=, d = document] {\n\t\t\treturn MTPmessages_FaveSticker(d->mtpInput(), MTP_bool(!faved));\n\t\t},\n\t\tstd::move(done));\n}\n\nvoid ToggleRecentSticker(\n\t\tnot_null<DocumentData*> document,\n\t\tData::FileOrigin origin,\n\t\tbool saved) {\n\tif (!document->sticker()) {\n\t\treturn;\n\t}\n\tauto done = [=] {\n\t\tif (!saved) {\n\t\t\tdocument->owner().stickers().removeFromRecentSet(document);\n\t\t}\n\t};\n\tToggleExistingMedia(\n\t\tdocument,\n\t\tstd::move(origin),\n\t\t[=] {\n\t\t\treturn MTPmessages_SaveRecentSticker(\n\t\t\t\tMTP_flags(MTPmessages_SaveRecentSticker::Flag(0)),\n\t\t\t\tdocument->mtpInput(),\n\t\t\t\tMTP_bool(!saved));\n\t\t},\n\t\tstd::move(done));\n}\n\nvoid ToggleSavedGif(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document,\n\t\tData::FileOrigin origin,\n\t\tbool saved) {\n\tif (saved && !document->isGifv()) {\n\t\treturn;\n\t}\n\tauto done = [=] {\n\t\tif (saved) {\n\t\t\tdocument->owner().stickers().addSavedGif(show, document);\n\t\t}\n\t};\n\tToggleExistingMedia(\n\t\tdocument,\n\t\tstd::move(origin),\n\t\t[=, d = document] {\n\t\t\treturn MTPmessages_SaveGif(d->mtpInput(), MTP_bool(!saved));\n\t\t},\n\t\tstd::move(done));\n}\n\nvoid ToggleSavedRingtone(\n\t\tnot_null<DocumentData*> document,\n\t\tData::FileOrigin origin,\n\t\tFn<void()> &&done,\n\t\tbool saved) {\n\tToggleExistingMedia(\n\t\tdocument,\n\t\tstd::move(origin),\n\t\t[=, d = document] {\n\t\t\treturn MTPaccount_SaveRingtone(d->mtpInput(), MTP_bool(!saved));\n\t\t},\n\t\tstd::move(done));\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_toggling_media.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Api {\n\nvoid ToggleFavedSticker(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<DocumentData*> document,\n\tData::FileOrigin origin);\n\nvoid ToggleFavedSticker(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<DocumentData*> document,\n\tData::FileOrigin origin,\n\tbool faved);\n\nvoid ToggleRecentSticker(\n\tnot_null<DocumentData*> document,\n\tData::FileOrigin origin,\n\tbool saved);\n\nvoid ToggleSavedGif(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<DocumentData*> document,\n\tData::FileOrigin origin,\n\tbool saved);\n\nvoid ToggleSavedRingtone(\n\tnot_null<DocumentData*> document,\n\tData::FileOrigin origin,\n\tFn<void()> &&done,\n\tbool saved);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_transcribes.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_transcribes.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_text_entities.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"spellcheck/spellcheck_types.h\"\n\nnamespace Api {\n\nTranscribes::Transcribes(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance()) {\n}\n\nbool Transcribes::isRated(not_null<HistoryItem*> item) const {\n\tconst auto fullId = item->fullId();\n\tfor (const auto &[transcribeId, id] : _ids) {\n\t\tif (id == fullId) {\n\t\t\treturn _session->settings().isTranscriptionRated(transcribeId);\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Transcribes::rate(not_null<HistoryItem*> item, bool isGood) {\n\tconst auto fullId = item->fullId();\n\tfor (const auto &[transcribeId, id] : _ids) {\n\t\tif (id == fullId) {\n\t\t\t_api.request(MTPmessages_RateTranscribedAudio(\n\t\t\t\titem->history()->peer->input(),\n\t\t\t\tMTP_int(item->id),\n\t\t\t\tMTP_long(transcribeId),\n\t\t\t\tMTP_bool(isGood))).send();\n\t\t\t_session->settings().markTranscriptionAsRated(transcribeId);\n\t\t\t_session->saveSettings();\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nbool Transcribes::freeFor(not_null<HistoryItem*> item) const {\n\tif (const auto channel = item->history()->peer->asMegagroup()) {\n\t\tconst auto owner = &channel->owner();\n\t\treturn channel->levelHint() >= owner->groupFreeTranscribeLevel();\n\t}\n\treturn false;\n}\n\nbool Transcribes::trialsSupport() {\n\tif (!_trialsSupport) {\n\t\tconst auto count = _session->appConfig().get<int>(\n\t\t\tu\"transcribe_audio_trial_weekly_number\"_q,\n\t\t\t0);\n\t\tconst auto until = _session->appConfig().get<int>(\n\t\t\tu\"transcribe_audio_trial_cooldown_until\"_q,\n\t\t\t0);\n\t\t_trialsSupport = (count > 0) || (until > 0);\n\t}\n\treturn *_trialsSupport;\n}\n\nTimeId Transcribes::trialsRefreshAt() {\n\tif (_trialsRefreshAt < 0) {\n\t\t_trialsRefreshAt = _session->appConfig().get<int>(\n\t\t\tu\"transcribe_audio_trial_cooldown_until\"_q,\n\t\t\t0);\n\t}\n\treturn _trialsRefreshAt;\n}\n\nint Transcribes::trialsCount() {\n\tif (_trialsCount < 0) {\n\t\t_trialsCount = _session->appConfig().get<int>(\n\t\t\tu\"transcribe_audio_trial_weekly_number\"_q,\n\t\t\t-1);\n\t\treturn std::max(_trialsCount, 0);\n\t}\n\treturn _trialsCount;\n}\n\ncrl::time Transcribes::trialsMaxLengthMs() const {\n\treturn 1000 * _session->appConfig().get<int>(\n\t\tu\"transcribe_audio_trial_duration_max\"_q,\n\t\t300);\n}\n\nvoid Transcribes::toggle(not_null<HistoryItem*> item) {\n\tconst auto id = item->fullId();\n\tauto i = _map.find(id);\n\tif (i == _map.end()) {\n\t\tload(item);\n\t\t_session->data().requestItemResize(item);\n\t} else if (!i->second.requestId) {\n\t\ti->second.shown = !i->second.shown;\n\t\tif (i->second.roundview) {\n\t\t\t_session->data().requestItemViewRefresh(item);\n\t\t}\n\t\t_session->data().requestItemResize(item);\n\t}\n}\n\nvoid Transcribes::toggleSummary(not_null<HistoryItem*> item) {\n\tconst auto id = item->fullId();\n\tauto i = _summaries.find(id);\n\tif (i == _summaries.end()) {\n\t\tsummarize(item);\n\t} else if (!i->second.loading) {\n\t\tauto &entry = i->second;\n\t\tif (entry.result.empty()) {\n\t\t\tsummarize(item);\n\t\t} else {\n\t\t\tentry.shown = entry.premiumRequired ? false : !entry.shown;\n\t\t\t_session->data().requestItemResize(item);\n\t\t\tif (entry.shown) {\n\t\t\t\t_session->data().requestItemShowHighlight(item);\n\t\t\t}\n\t\t}\n\t}\n}\n\nconst Transcribes::Entry &Transcribes::entry(\n\t\tnot_null<HistoryItem*> item) const {\n\tstatic auto empty = Entry();\n\tconst auto i = _map.find(item->fullId());\n\treturn (i != _map.end()) ? i->second : empty;\n}\n\nconst SummaryEntry &Transcribes::summary(\n\t\tnot_null<const HistoryItem*> item) const {\n\tstatic const auto empty = SummaryEntry();\n\tconst auto i = _summaries.find(item->fullId());\n\treturn (i != _summaries.end()) ? i->second : empty;\n}\n\nvoid Transcribes::apply(const MTPDupdateTranscribedAudio &update) {\n\tconst auto id = update.vtranscription_id().v;\n\tconst auto i = _ids.find(id);\n\tif (i == _ids.end()) {\n\t\treturn;\n\t}\n\tconst auto j = _map.find(i->second);\n\tif (j == _map.end()) {\n\t\treturn;\n\t}\n\tconst auto text = qs(update.vtext());\n\tj->second.result = text;\n\tj->second.pending = update.is_pending();\n\tif (const auto item = _session->data().message(i->second)) {\n\t\tif (j->second.roundview) {\n\t\t\t_session->data().requestItemViewRefresh(item);\n\t\t}\n\t\t_session->data().requestItemResize(item);\n\t}\n}\n\nvoid Transcribes::load(not_null<HistoryItem*> item) {\n\tif (!item->isHistoryEntry() || item->isLocal()) {\n\t\treturn;\n\t}\n\tconst auto toggleRound = [](not_null<HistoryItem*> item, Entry &entry) {\n\t\tif (const auto media = item->media()) {\n\t\t\tif (const auto document = media->document()) {\n\t\t\t\tif (document->isVideoMessage()) {\n\t\t\t\t\tentry.roundview = true;\n\t\t\t\t\tdocument->owner().requestItemViewRefresh(item);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\tconst auto id = item->fullId();\n\tconst auto requestId = _api.request(MTPmessages_TranscribeAudio(\n\t\titem->history()->peer->input(),\n\t\tMTP_int(item->id)\n\t)).done([=](const MTPmessages_TranscribedAudio &result) {\n\t\tconst auto &data = result.data();\n\n\t\t{\n\t\t\tconst auto trialsCountChanged = data.vtrial_remains_num()\n\t\t\t\t&& (_trialsCount != data.vtrial_remains_num()->v);\n\t\t\tif (trialsCountChanged) {\n\t\t\t\t_trialsCount = data.vtrial_remains_num()->v;\n\t\t\t}\n\t\t\tconst auto refreshAtChanged = data.vtrial_remains_until_date()\n\t\t\t\t&& (_trialsRefreshAt != data.vtrial_remains_until_date()->v);\n\t\t\tif (refreshAtChanged) {\n\t\t\t\t_trialsRefreshAt = data.vtrial_remains_until_date()->v;\n\t\t\t}\n\t\t\tif (trialsCountChanged) {\n\t\t\t\tShowTrialTranscribesToast(_trialsCount, _trialsRefreshAt);\n\t\t\t}\n\t\t}\n\n\t\tauto &entry = _map[id];\n\t\tentry.requestId = 0;\n\t\tentry.pending = data.is_pending();\n\t\tentry.result = qs(data.vtext());\n\t\t_ids.emplace(data.vtranscription_id().v, id);\n\t\tif (const auto item = _session->data().message(id)) {\n\t\t\ttoggleRound(item, entry);\n\t\t\t_session->data().requestItemResize(item);\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tauto &entry = _map[id];\n\t\tentry.requestId = 0;\n\t\tentry.pending = false;\n\t\tentry.failed = true;\n\t\tif (error.type() == u\"MSG_VOICE_TOO_LONG\"_q) {\n\t\t\tentry.toolong = true;\n\t\t}\n\t\tif (const auto item = _session->data().message(id)) {\n\t\t\ttoggleRound(item, entry);\n\t\t\t_session->data().requestItemResize(item);\n\t\t}\n\t}).send();\n\tauto &entry = _map.emplace(id).first->second;\n\tentry.requestId = requestId;\n\tentry.shown = true;\n\tentry.failed = false;\n\tentry.pending = false;\n}\n\nvoid Transcribes::summarize(not_null<HistoryItem*> item) {\n\tif (!item->isHistoryEntry() || item->isLocal()) {\n\t\treturn;\n\t}\n\n\tconst auto id = item->fullId();\n\tconst auto translatedTo = item->history()->translatedTo();\n\tconst auto langCode = translatedTo\n\t\t? translatedTo.twoLetterCode()\n\t\t: QString();\n\tconst auto requestId = _api.request(MTPmessages_SummarizeText(\n\t\tlangCode.isEmpty()\n\t\t\t? MTP_flags(0)\n\t\t\t: MTP_flags(MTPmessages_summarizeText::Flag::f_to_lang),\n\t\titem->history()->peer->input(),\n\t\tMTP_int(item->id),\n\t\tlangCode.isEmpty() ? MTPstring() : MTP_string(langCode),\n\t\tMTPstring() // tone\n\t)).done([=](const MTPTextWithEntities &result) {\n\t\tconst auto &data = result.data();\n\t\tauto &entry = _summaries[id];\n\t\tentry.requestId = 0;\n\t\tentry.loading = false;\n\t\tentry.premiumRequired = false;\n\t\tentry.languageId = translatedTo;\n\t\tentry.result = TextWithEntities(\n\t\t\tqs(data.vtext()),\n\t\t\tApi::EntitiesFromMTP(_session, data.ventities().v));\n\t\tif (const auto item = _session->data().message(id)) {\n\t\t\t_session->data().requestItemTextRefresh(item);\n\t\t\t_session->data().requestItemShowHighlight(item);\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tauto &entry = _summaries[id];\n\t\tif (error.type() == u\"SUMMARY_FLOOD_PREMIUM\"_q) {\n\t\t\tentry.premiumRequired = true;\n\t\t}\n\t\tentry.requestId = 0;\n\t\tentry.shown = false;\n\t\tentry.loading = false;\n\t\tif (const auto item = _session->data().message(id)) {\n\t\t\t_session->data().requestItemTextRefresh(item);\n\t\t}\n\t}).send();\n\n\tauto &entry = _summaries.emplace(id).first->second;\n\tentry.requestId = requestId;\n\tentry.shown = true;\n\tentry.loading = true;\n\n\titem->setHasSummaryEntry();\n\t_session->data().requestItemResize(item);\n}\n\nvoid Transcribes::checkSummaryToTranslate(FullMsgId id) {\n\tconst auto i = _summaries.find(id);\n\tif (i == _summaries.end() || i->second.result.empty()) {\n\t\treturn;\n\t}\n\tconst auto item = _session->data().message(id);\n\tif (!item) {\n\t\treturn;\n\t}\n\tconst auto translatedTo = item->history()->translatedTo();\n\tif (i->second.languageId != translatedTo) {\n\t\ti->second.result = tr::lng_contacts_loading(tr::now, tr::italic);\n\t\tsummarize(item);\n\t}\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_transcribes.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n#include \"spellcheck/spellcheck_types.h\"\n\nclass ApiWrap;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nstruct SummaryEntry {\n\tTextWithEntities result;\n\tLanguageId languageId;\n\tbool shown = false;\n\tbool loading = false;\n\tbool premiumRequired = false;\n\tmtpRequestId requestId = 0;\n};\n\nclass Transcribes final {\npublic:\n\texplicit Transcribes(not_null<ApiWrap*> api);\n\n\tstruct Entry {\n\t\tQString result;\n\t\tbool shown = false;\n\t\tbool failed = false;\n\t\tbool toolong = false;\n\t\tbool pending = false;\n\t\tbool roundview = false;\n\t\tmtpRequestId requestId = 0;\n\t};\n\n\tvoid toggle(not_null<HistoryItem*> item);\n\t[[nodiscard]] const Entry &entry(not_null<HistoryItem*> item) const;\n\n\tvoid toggleSummary(not_null<HistoryItem*> item);\n\t[[nodiscard]] const SummaryEntry &summary(\n\t\tnot_null<const HistoryItem*> item) const;\n\tvoid checkSummaryToTranslate(FullMsgId id);\n\n\tvoid apply(const MTPDupdateTranscribedAudio &update);\n\n\t[[nodiscard]] bool freeFor(not_null<HistoryItem*> item) const;\n\t[[nodiscard]] bool isRated(not_null<HistoryItem*> item) const;\n\tvoid rate(not_null<HistoryItem*> item, bool isGood);\n\n\t[[nodiscard]] bool trialsSupport();\n\t[[nodiscard]] TimeId trialsRefreshAt();\n\t[[nodiscard]] int trialsCount();\n\t[[nodiscard]] crl::time trialsMaxLengthMs() const;\n\nprivate:\n\tvoid load(not_null<HistoryItem*> item);\n\tvoid summarize(not_null<HistoryItem*> item);\n\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\n\tint _trialsCount = -1;\n\tstd::optional<bool> _trialsSupport;\n\tTimeId _trialsRefreshAt = -1;\n\n\tbase::flat_map<FullMsgId, Entry> _map;\n\tbase::flat_map<uint64, FullMsgId> _ids;\n\n\tbase::flat_map<FullMsgId, SummaryEntry> _summaries;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_unread_things.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_unread_things.h\"\n\n#include \"data/data_peer.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_unread_things.h\"\n#include \"apiwrap.h\"\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kPreloadIfLess = 5;\nconstexpr auto kFirstRequestLimit = 10;\nconstexpr auto kNextRequestLimit = 100;\n\n} // namespace\n\nUnreadThings::UnreadThings(not_null<ApiWrap*> api) : _api(api) {\n}\n\nbool UnreadThings::trackMentions(Data::Thread *thread) const {\n\tconst auto peer = thread ? thread->peer().get() : nullptr;\n\treturn peer\n\t\t&& (peer->isChat() || peer->isMegagroup())\n\t\t&& !peer->isMonoforum();\n}\n\nbool UnreadThings::trackReactions(Data::Thread *thread) const {\n\tconst auto peer = thread ? thread->peer().get() : nullptr;\n\treturn peer && (peer->isUser() || peer->isChat() || peer->isMegagroup());\n}\n\nbool UnreadThings::trackPollVotes(Data::Thread *thread) const {\n\tconst auto peer = thread ? thread->peer().get() : nullptr;\n\treturn peer\n\t\t&& (peer->isChat() || peer->isMegagroup())\n\t\t&& !peer->isMonoforum();\n}\n\nvoid UnreadThings::preloadEnough(Data::Thread *thread) {\n\tif (trackMentions(thread)) {\n\t\tpreloadEnoughMentions(thread);\n\t}\n\tif (trackReactions(thread)) {\n\t\tpreloadEnoughReactions(thread);\n\t}\n\tif (trackPollVotes(thread)) {\n\t\tpreloadEnoughPollVotes(thread);\n\t}\n}\n\nvoid UnreadThings::mediaAndMentionsRead(\n\t\tconst base::flat_set<MsgId> &readIds,\n\t\tChannelData *channel) {\n\tfor (const auto &msgId : readIds) {\n\t\t_api->requestMessageData(channel, msgId, [=] {\n\t\t\tconst auto item = channel\n\t\t\t\t? _api->session().data().message(channel->id, msgId)\n\t\t\t\t: _api->session().data().nonChannelMessage(msgId);\n\t\t\tif (item && item->mentionsMe()) {\n\t\t\t\titem->markMediaAndMentionRead();\n\t\t\t}\n\t\t});\n\t}\n}\n\nvoid UnreadThings::preloadEnoughMentions(not_null<Data::Thread*> thread) {\n\tconst auto fullCount = thread->unreadMentions().count();\n\tconst auto loadedCount = thread->unreadMentions().loadedCount();\n\tconst auto allLoaded = (fullCount >= 0) && (loadedCount >= fullCount);\n\tif (fullCount >= 0 && loadedCount < kPreloadIfLess && !allLoaded) {\n\t\trequestMentions(thread, loadedCount);\n\t}\n}\n\nvoid UnreadThings::preloadEnoughReactions(not_null<Data::Thread*> thread) {\n\tconst auto fullCount = thread->unreadReactions().count();\n\tconst auto loadedCount = thread->unreadReactions().loadedCount();\n\tconst auto allLoaded = (fullCount >= 0) && (loadedCount >= fullCount);\n\tif (fullCount >= 0 && loadedCount < kPreloadIfLess && !allLoaded) {\n\t\trequestReactions(thread, loadedCount);\n\t}\n}\n\nvoid UnreadThings::preloadEnoughPollVotes(not_null<Data::Thread*> thread) {\n\tconst auto fullCount = thread->unreadPollVotes().count();\n\tconst auto loadedCount = thread->unreadPollVotes().loadedCount();\n\tconst auto allLoaded = (fullCount >= 0) && (loadedCount >= fullCount);\n\tif (fullCount >= 0 && loadedCount < kPreloadIfLess && !allLoaded) {\n\t\trequestPollVotes(thread, loadedCount);\n\t}\n}\n\nvoid UnreadThings::cancelRequests(not_null<Data::Thread*> thread) {\n\tif (const auto requestId = _mentionsRequests.take(thread)) {\n\t\t_api->request(*requestId).cancel();\n\t}\n\tif (const auto requestId = _reactionsRequests.take(thread)) {\n\t\t_api->request(*requestId).cancel();\n\t}\n\tif (const auto requestId = _pollVotesRequests.take(thread)) {\n\t\t_api->request(*requestId).cancel();\n\t}\n}\n\nvoid UnreadThings::requestMentions(\n\t\tnot_null<Data::Thread*> thread,\n\t\tint loaded) {\n\tif (_mentionsRequests.contains(thread) || thread->asSublist()) {\n\t\treturn;\n\t}\n\tconst auto offsetId = std::max(\n\t\tthread->unreadMentions().maxLoaded(),\n\t\tMsgId(1));\n\tconst auto limit = loaded ? kNextRequestLimit : kFirstRequestLimit;\n\tconst auto addOffset = loaded ? -(limit + 1) : -limit;\n\tconst auto maxId = 0;\n\tconst auto minId = 0;\n\tconst auto history = thread->owningHistory();\n\tconst auto topic = thread->asTopic();\n\tusing Flag = MTPmessages_GetUnreadMentions::Flag;\n\tconst auto requestId = _api->request(MTPmessages_GetUnreadMentions(\n\t\tMTP_flags(topic ? Flag::f_top_msg_id : Flag()),\n\t\thistory->peer->input(),\n\t\tMTP_int(topic ? topic->rootId() : 0),\n\t\tMTP_int(offsetId),\n\t\tMTP_int(addOffset),\n\t\tMTP_int(limit),\n\t\tMTP_int(maxId),\n\t\tMTP_int(minId)\n\t)).done([=](const MTPmessages_Messages &result) {\n\t\t_mentionsRequests.remove(thread);\n\t\tthread->unreadMentions().addSlice(result, loaded);\n\t}).fail([=] {\n\t\t_mentionsRequests.remove(thread);\n\t}).send();\n\t_mentionsRequests.emplace(thread, requestId);\n}\n\nvoid UnreadThings::requestReactions(\n\t\tnot_null<Data::Thread*> thread,\n\t\tint loaded) {\n\tif (_reactionsRequests.contains(thread)) {\n\t\treturn;\n\t}\n\tconst auto offsetId = loaded\n\t\t? std::max(thread->unreadReactions().maxLoaded(), MsgId(1))\n\t\t: MsgId(1);\n\tconst auto limit = loaded ? kNextRequestLimit : kFirstRequestLimit;\n\tconst auto addOffset = loaded ? -(limit + 1) : -limit;\n\tconst auto maxId = 0;\n\tconst auto minId = 0;\n\tconst auto history = thread->owningHistory();\n\tconst auto sublist = thread->asSublist();\n\tconst auto topic = thread->asTopic();\n\tusing Flag = MTPmessages_GetUnreadReactions::Flag;\n\tconst auto requestId = _api->request(MTPmessages_GetUnreadReactions(\n\t\tMTP_flags((topic ? Flag::f_top_msg_id : Flag())\n\t\t\t| (sublist ? Flag::f_saved_peer_id : Flag())),\n\t\thistory->peer->input(),\n\t\tMTP_int(topic ? topic->rootId() : 0),\n\t\t(sublist ? sublist->sublistPeer()->input() : MTPInputPeer()),\n\t\tMTP_int(offsetId),\n\t\tMTP_int(addOffset),\n\t\tMTP_int(limit),\n\t\tMTP_int(maxId),\n\t\tMTP_int(minId)\n\t)).done([=](const MTPmessages_Messages &result) {\n\t\t_reactionsRequests.remove(thread);\n\t\tthread->unreadReactions().addSlice(result, loaded);\n\t}).fail([=] {\n\t\t_reactionsRequests.remove(thread);\n\t}).send();\n\t_reactionsRequests.emplace(thread, requestId);\n}\n\nvoid UnreadThings::requestPollVotes(\n\t\tnot_null<Data::Thread*> thread,\n\t\tint loaded) {\n\tif (_pollVotesRequests.contains(thread) || thread->asSublist()) {\n\t\treturn;\n\t}\n\tconst auto offsetId = loaded\n\t\t? std::max(thread->unreadPollVotes().maxLoaded(), MsgId(1))\n\t\t: MsgId(1);\n\tconst auto limit = loaded ? kNextRequestLimit : kFirstRequestLimit;\n\tconst auto addOffset = loaded ? -(limit + 1) : -limit;\n\tconst auto maxId = 0;\n\tconst auto minId = 0;\n\tconst auto history = thread->owningHistory();\n\tconst auto topic = thread->asTopic();\n\tusing Flag = MTPmessages_GetUnreadPollVotes::Flag;\n\tconst auto requestId = _api->request(MTPmessages_GetUnreadPollVotes(\n\t\tMTP_flags(topic ? Flag::f_top_msg_id : Flag()),\n\t\thistory->peer->input(),\n\t\tMTP_int(topic ? topic->rootId() : 0),\n\t\tMTP_int(offsetId),\n\t\tMTP_int(addOffset),\n\t\tMTP_int(limit),\n\t\tMTP_int(maxId),\n\t\tMTP_int(minId)\n\t)).done([=](const MTPmessages_Messages &result) {\n\t\t_pollVotesRequests.remove(thread);\n\t\tthread->unreadPollVotes().addSlice(result, loaded);\n\t}).fail([=] {\n\t\t_pollVotesRequests.remove(thread);\n\t}).send();\n\t_pollVotesRequests.emplace(thread, requestId);\n}\n\n} // namespace UnreadThings\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_unread_things.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass ApiWrap;\nclass PeerData;\nclass ChannelData;\n\nnamespace Data {\nclass Thread;\n} // namespace Data\n\nnamespace Api {\n\nclass UnreadThings final {\npublic:\n\texplicit UnreadThings(not_null<ApiWrap*> api);\n\n\t[[nodiscard]] bool trackMentions(Data::Thread *thread) const;\n\t[[nodiscard]] bool trackReactions(Data::Thread *thread) const;\n\t[[nodiscard]] bool trackPollVotes(Data::Thread *thread) const;\n\n\tvoid preloadEnough(Data::Thread *thread);\n\n\tvoid mediaAndMentionsRead(\n\t\tconst base::flat_set<MsgId> &readIds,\n\t\tChannelData *channel = nullptr);\n\n\tvoid cancelRequests(not_null<Data::Thread*> thread);\n\nprivate:\n\tvoid preloadEnoughMentions(not_null<Data::Thread*> thread);\n\tvoid preloadEnoughReactions(not_null<Data::Thread*> thread);\n\tvoid preloadEnoughPollVotes(not_null<Data::Thread*> thread);\n\n\tvoid requestMentions(not_null<Data::Thread*> thread, int loaded);\n\tvoid requestReactions(not_null<Data::Thread*> thread, int loaded);\n\tvoid requestPollVotes(not_null<Data::Thread*> thread, int loaded);\n\n\tconst not_null<ApiWrap*> _api;\n\n\tbase::flat_map<not_null<Data::Thread*>, mtpRequestId> _mentionsRequests;\n\tbase::flat_map<not_null<Data::Thread*>, mtpRequestId> _reactionsRequests;\n\tbase::flat_map<not_null<Data::Thread*>, mtpRequestId> _pollVotesRequests;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_updates.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_updates.h\"\n\n#include \"api/api_authorizations.h\"\n#include \"api/api_user_names.h\"\n#include \"api/api_chat_participants.h\"\n#include \"api/api_global_privacy.h\"\n#include \"api/api_ringtones.h\"\n#include \"api/api_text_entities.h\"\n#include \"api/api_user_privacy.h\"\n#include \"api/api_unread_things.h\"\n#include \"api/api_transcribes.h\"\n#include \"main/main_session.h\"\n#include \"main/main_account.h\"\n#include \"mtproto/mtp_instance.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"chat_helpers/stickers_dice_pack.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/components/credits.h\"\n#include \"data/components/gift_auctions.h\"\n#include \"data/components/promo_suggestions.h\"\n#include \"data/components/scheduled_messages.h\"\n#include \"data/components/top_peers.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_drafts.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_send_action.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_message_reactions.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n#include \"chat_helpers/emoji_interactions.h\"\n#include \"lang/lang_cloud_manager.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history_streamed_drafts.h\"\n#include \"history/history_unread_things.h\"\n#include \"core/application.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/storage_facade.h\"\n#include \"storage/storage_user_photos.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"calls/calls_instance.h\"\n#include \"base/unixtime.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"apiwrap.h\"\n#include \"ui/text/format_values.h\" // Ui::FormatPhone\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kChannelGetDifferenceLimit = 100;\n\n// 1s wait after show channel history before sending getChannelDifference.\nconstexpr auto kWaitForChannelGetDifference = crl::time(1000);\n\n// If nothing is received in 1 min we ping.\nconstexpr auto kNoUpdatesTimeout = 60 * 1000;\n\n// If nothing is received in 1 min when was a sleepmode we ping.\nconstexpr auto kNoUpdatesAfterSleepTimeout = 60 * crl::time(1000);\n\nenum class DataIsLoadedResult {\n\tNotLoaded = 0,\n\tFromNotLoaded = 1,\n\tMentionNotLoaded = 2,\n\tOk = 3,\n};\n\n[[nodiscard]] bool PeerDataIsLoaded(\n\t\tnot_null<Data::Session*> owner,\n\t\tPeerId peerId) {\n\treturn !peerId || owner->peerLoaded(peerId);\n}\n\n[[nodiscard]] bool MentionUsersDataIsLoaded(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst MTPVector<MTPMessageEntity> &entities) {\n\tfor (const auto &entity : entities.v) {\n\t\tauto loaded = true;\n\t\tentity.match([&](const MTPDmessageEntityMentionName &data) {\n\t\t\tloaded = owner->userLoaded(data.vuser_id());\n\t\t}, [&](const MTPDinputMessageEntityMentionName &data) {\n\t\t\tdata.vuser_id().match([&](const MTPDinputUser &data) {\n\t\t\t\tloaded = owner->userLoaded(data.vuser_id());\n\t\t\t}, [](const auto &) {\n\t\t\t});\n\t\t}, [](const auto &) {\n\t\t});\n\t\tif (!loaded) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n[[nodiscard]] bool ForwardedInfoDataIsLoaded(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst MTPMessageFwdHeader &header) {\n\treturn header.match([&](const MTPDmessageFwdHeader &data) {\n\t\treturn (!data.vfrom_id()\n\t\t\t\t|| PeerDataIsLoaded(owner, peerFromMTP(*data.vfrom_id())))\n\t\t\t&& (!data.vsaved_from_peer()\n\t\t\t\t|| PeerDataIsLoaded(owner, peerFromMTP(*data.vsaved_from_peer())))\n\t\t\t&& (!data.vsaved_from_id()\n\t\t\t\t|| PeerDataIsLoaded(owner, peerFromMTP(*data.vsaved_from_id())));\n\t});\n}\n\n[[nodiscard]] bool ReplyDataIsLoaded(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst MTPMessageReplyHeader &header) {\n\treturn header.match([&](const MTPDmessageReplyHeader &data) {\n\t\treturn (!data.vreply_to_peer_id()\n\t\t\t\t|| PeerDataIsLoaded(owner, peerFromMTP(*data.vreply_to_peer_id())))\n\t\t\t&& (!data.vreply_from()\n\t\t\t\t|| ForwardedInfoDataIsLoaded(owner, *data.vreply_from()))\n\t\t\t&& (!data.vquote_entities()\n\t\t\t\t|| MentionUsersDataIsLoaded(owner, *data.vquote_entities()));\n\t}, [&](const MTPDmessageReplyStoryHeader &data) {\n\t\treturn PeerDataIsLoaded(owner, peerFromMTP(data.vpeer()));\n\t});\n}\n\n[[nodiscard]] bool DataIsLoaded(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst MTPDupdateShortMessage &data) {\n\treturn owner->userLoaded(data.vuser_id())\n\t\t&& (!data.vfwd_from()\n\t\t\t|| ForwardedInfoDataIsLoaded(owner, *data.vfwd_from()))\n\t\t&& (!data.vvia_bot_id()\n\t\t\t|| owner->userLoaded(*data.vvia_bot_id()))\n\t\t&& (!data.vreply_to()\n\t\t\t|| ReplyDataIsLoaded(owner, *data.vreply_to()))\n\t\t&& (!data.ventities()\n\t\t\t|| MentionUsersDataIsLoaded(owner, *data.ventities()));\n}\n\n[[nodiscard]] bool DataIsLoaded(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst MTPDupdateShortChatMessage &data) {\n\treturn owner->chatLoaded(data.vchat_id())\n\t\t&& owner->userLoaded(data.vfrom_id())\n\t\t&& (!data.vfwd_from()\n\t\t\t|| ForwardedInfoDataIsLoaded(owner, *data.vfwd_from()))\n\t\t&& (!data.vvia_bot_id()\n\t\t\t|| owner->userLoaded(*data.vvia_bot_id()))\n\t\t&& (!data.vreply_to()\n\t\t\t|| ReplyDataIsLoaded(owner, *data.vreply_to()))\n\t\t&& (!data.ventities()\n\t\t\t|| MentionUsersDataIsLoaded(owner, *data.ventities()));\n}\n\nvoid ProcessScheduledMessageWithElapsedTime(\n\t\tnot_null<Main::Session*> session,\n\t\tbool needToAdd,\n\t\tconst MTPDmessage &data) {\n\tif (needToAdd && !data.is_from_scheduled()) {\n\t\t// If we still need to add a new message,\n\t\t// we should first check if this message is in\n\t\t// the list of scheduled messages.\n\t\t// This is necessary to correctly update the file reference.\n\t\t// Note that when a message is scheduled until online\n\t\t// while the recipient is already online, the server sends\n\t\t// an ordinary new message with skipped \"from_scheduled\" flag.\n\t\tsession->scheduledMessages().checkEntitiesAndUpdate(data);\n\t}\n}\n\nbool IsForceLogoutNotification(const MTPDupdateServiceNotification &data) {\n\treturn qs(data.vtype()).startsWith(u\"AUTH_KEY_DROP_\"_q);\n}\n\nbool HasForceLogoutNotification(const MTPUpdates &updates) {\n\tconst auto checkUpdate = [](const MTPUpdate &update) {\n\t\tif (update.type() != mtpc_updateServiceNotification) {\n\t\t\treturn false;\n\t\t}\n\t\treturn IsForceLogoutNotification(\n\t\t\tupdate.c_updateServiceNotification());\n\t};\n\tconst auto checkVector = [&](const MTPVector<MTPUpdate> &list) {\n\t\tfor (const auto &update : list.v) {\n\t\t\tif (checkUpdate(update)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\tswitch (updates.type()) {\n\tcase mtpc_updates:\n\t\treturn checkVector(updates.c_updates().vupdates());\n\tcase mtpc_updatesCombined:\n\t\treturn checkVector(updates.c_updatesCombined().vupdates());\n\tcase mtpc_updateShort:\n\t\treturn checkUpdate(updates.c_updateShort().vupdate());\n\t}\n\treturn false;\n}\n\n} // namespace\n\nUpdates::Updates(not_null<Main::Session*> session)\n: _session(session)\n, _noUpdatesTimer([=] { sendPing(); })\n, _onlineTimer([=] { updateOnline(); })\n, _ptsWaiter(this)\n, _byPtsTimer([=] { getDifferenceByPts(); })\n, _bySeqTimer([=] { getDifference(); })\n, _byMinChannelTimer([=] { getDifference(); })\n, _failDifferenceTimer([=] { getDifferenceAfterFail(); })\n, _idleFinishTimer([=] { checkIdleFinish(); }) {\n\t_ptsWaiter.setRequesting(true);\n\n\tsession->account().mtpUpdates(\n\t) | rpl::on_next([=](const MTPUpdates &updates) {\n\t\tmtpUpdateReceived(updates);\n\t}, _lifetime);\n\n\tsession->account().mtpNewSessionCreated(\n\t) | rpl::on_next([=] {\n\t\tmtpNewSessionCreated();\n\t}, _lifetime);\n\n\tapi().request(MTPupdates_GetState(\n\t)).done([=](const MTPupdates_State &result) {\n\t\tstateDone(result);\n\t}).send();\n\n\tusing namespace rpl::mappers;\n\tsession->changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::FullInfo\n\t) | rpl::filter([](const Data::PeerUpdate &update) {\n\t\treturn update.peer->isChat() || update.peer->isMegagroup();\n\t}) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tconst auto peer = update.peer;\n\t\tif (const auto list = _pendingSpeakingCallParticipants.take(peer)) {\n\t\t\tif (const auto call = peer->groupCall()) {\n\t\t\t\tfor (const auto &[participantPeerId, when] : *list) {\n\t\t\t\t\tcall->applyActiveUpdate(\n\t\t\t\t\t\tparticipantPeerId,\n\t\t\t\t\t\tData::LastSpokeTimes{\n\t\t\t\t\t\t\t.anything = when,\n\t\t\t\t\t\t\t.voice = when\n\t\t\t\t\t\t},\n\t\t\t\t\t\tpeer->owner().peerLoaded(participantPeerId));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n}\n\nMain::Session &Updates::session() const {\n\treturn *_session;\n}\n\nApiWrap &Updates::api() const {\n\treturn _session->api();\n}\n\nvoid Updates::checkLastUpdate(bool afterSleep) {\n\tconst auto now = crl::now();\n\tconst auto skip = afterSleep\n\t\t? kNoUpdatesAfterSleepTimeout\n\t\t: kNoUpdatesTimeout;\n\tif (_lastUpdateTime && now > _lastUpdateTime + skip) {\n\t\t_lastUpdateTime = now;\n\t\tsendPing();\n\t}\n}\n\nvoid Updates::feedUpdateVector(\n\t\tconst MTPVector<MTPUpdate> &updates,\n\t\tSkipUpdatePolicy policy) {\n\tauto list = updates.v;\n\tconst auto hasGroupCallParticipantUpdates = ranges::contains(\n\t\tlist,\n\t\ttrue,\n\t\t[](const MTPUpdate &update) {\n\t\t\treturn update.type() == mtpc_updateGroupCallParticipants\n\t\t\t\t|| update.type() == mtpc_updateGroupCallChainBlocks;\n\t\t});\n\tif (hasGroupCallParticipantUpdates) {\n\t\tranges::stable_sort(list, std::less<>(), [](const MTPUpdate &entry) {\n\t\t\tif (entry.type() == mtpc_updateGroupCallChainBlocks) {\n\t\t\t\treturn 0;\n\t\t\t} else if (entry.type() == mtpc_updateGroupCallParticipants) {\n\t\t\t\treturn 1;\n\t\t\t} else {\n\t\t\t\treturn 2;\n\t\t\t}\n\t\t});\n\t} else if (policy == SkipUpdatePolicy::SkipExceptGroupCallParticipants) {\n\t\treturn;\n\t}\n\tif (policy == SkipUpdatePolicy::SkipNone) {\n\t\tapplyConvertToScheduledOnSend(updates);\n\t}\n\tfor (const auto &entry : std::as_const(list)) {\n\t\tconst auto type = entry.type();\n\t\tif ((policy == SkipUpdatePolicy::SkipMessageIds\n\t\t\t&& type == mtpc_updateMessageID)\n\t\t\t|| (policy == SkipUpdatePolicy::SkipExceptGroupCallParticipants\n\t\t\t\t&& type != mtpc_updateGroupCallParticipants\n\t\t\t\t&& type != mtpc_updateGroupCallChainBlocks)) {\n\t\t\tcontinue;\n\t\t}\n\t\tfeedUpdate(entry);\n\t}\n\tsession().data().sendHistoryChangeNotifications();\n}\n\nvoid Updates::checkForSentToScheduled(const MTPUpdates &updates) {\n\tupdates.match([&](const MTPDupdates &data) {\n\t\tapplyConvertToScheduledOnSend(data.vupdates(), true);\n\t}, [&](const MTPDupdatesCombined &data) {\n\t\tapplyConvertToScheduledOnSend(data.vupdates(), true);\n\t}, [](const auto &) {\n\t});\n}\n\nvoid Updates::feedMessageIds(const MTPVector<MTPUpdate> &updates) {\n\tfor (const auto &update : updates.v) {\n\t\tif (update.type() == mtpc_updateMessageID) {\n\t\t\tfeedUpdate(update);\n\t\t}\n\t}\n}\n\nvoid Updates::setState(int32 pts, int32 date, int32 qts, int32 seq) {\n\tif (pts) {\n\t\t_ptsWaiter.init(pts);\n\t}\n\tif (_updatesDate < date && !_byMinChannelTimer.isActive()) {\n\t\t_updatesDate = date;\n\t}\n\tif (qts && _updatesQts < qts) {\n\t\t_updatesQts = qts;\n\t}\n\tif (seq && seq != _updatesSeq) {\n\t\t_updatesSeq = seq;\n\t\tif (_bySeqTimer.isActive()) {\n\t\t\t_bySeqTimer.cancel();\n\t\t}\n\t\twhile (!_bySeqUpdates.empty()) {\n\t\t\tconst auto s = _bySeqUpdates.front().first;\n\t\t\tif (s <= seq + 1) {\n\t\t\t\tconst auto v = _bySeqUpdates.front().second;\n\t\t\t\t_bySeqUpdates.erase(_bySeqUpdates.begin());\n\t\t\t\tif (s == seq + 1) {\n\t\t\t\t\treturn applyUpdates(v);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (!_bySeqTimer.isActive()) {\n\t\t\t\t\t_bySeqTimer.callOnce(PtsWaiter::kWaitForSkippedTimeout);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Updates::channelDifferenceDone(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst MTPupdates_ChannelDifference &difference) {\n\t_channelFailDifferenceTimeout.remove(channel);\n\n\tconst auto timeout = difference.match([&](const auto &data) {\n\t\treturn data.vtimeout().value_or_empty();\n\t});\n\tconst auto isFinal = difference.match([&](const auto &data) {\n\t\treturn data.is_final();\n\t});\n\tdifference.match([&](const MTPDupdates_channelDifferenceEmpty &data) {\n\t\tchannel->ptsInit(data.vpts().v);\n\t}, [&](const MTPDupdates_channelDifferenceTooLong &data) {\n\t\tsession().data().processUsers(data.vusers());\n\t\tsession().data().processChats(data.vchats());\n\t\tconst auto history = session().data().historyLoaded(channel->id);\n\t\tif (history) {\n\t\t\thistory->setNotLoadedAtBottom();\n\t\t\trequestChannelRangeDifference(history);\n\t\t}\n\t\tdata.vdialog().match([&](const MTPDdialog &data) {\n\t\t\tif (const auto pts = data.vpts()) {\n\t\t\t\tchannel->ptsInit(pts->v);\n\t\t\t}\n\t\t}, [&](const MTPDdialogFolder &) {\n\t\t});\n\t\tsession().data().applyDialogs(\n\t\t\tnullptr,\n\t\t\tdata.vmessages().v,\n\t\t\tQVector<MTPDialog>(1, data.vdialog()));\n\t\tsession().data().channelDifferenceTooLong(channel);\n\t\tif (const auto forum = channel->forum()) {\n\t\t\tforum->reloadTopics();\n\t\t}\n\t}, [&](const MTPDupdates_channelDifference &data) {\n\t\tfeedChannelDifference(data);\n\t\tchannel->ptsInit(data.vpts().v);\n\t});\n\n\tchannel->ptsSetRequesting(false);\n\n\tif (!isFinal) {\n\t\tMTP_LOG(0, (\"getChannelDifference \"\n\t\t\t\"{ good - after not final channelDifference was received }%1\"\n\t\t\t).arg(_session->mtp().isTestMode() ? \" TESTMODE\" : \"\"));\n\t\tgetChannelDifference(channel);\n\t} else if (inActiveChats(channel)) {\n\t\tchannel->ptsSetWaitingForShortPoll(timeout\n\t\t\t? (timeout * crl::time(1000))\n\t\t\t: kWaitForChannelGetDifference);\n\t} else {\n\t\tchannel->ptsSetWaitingForShortPoll(-1);\n\t}\n}\n\nvoid Updates::feedChannelDifference(\n\t\tconst MTPDupdates_channelDifference &data) {\n\tsession().data().processUsers(data.vusers());\n\tsession().data().processChats(data.vchats());\n\n\t_handlingChannelDifference = true;\n\tapplyConvertToScheduledOnSend(data.vother_updates());\n\tfeedMessageIds(data.vother_updates());\n\tsession().data().processMessages(\n\t\tdata.vnew_messages(),\n\t\tNewMessageType::Unread);\n\tfeedUpdateVector(\n\t\tdata.vother_updates(),\n\t\tSkipUpdatePolicy::SkipMessageIds);\n\t_handlingChannelDifference = false;\n}\n\nvoid Updates::channelDifferenceFail(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst MTP::Error &error) {\n\tLOG((\"RPC Error in getChannelDifference: %1 %2: %3\").arg(\n\t\tQString::number(error.code()),\n\t\terror.type(),\n\t\terror.description()));\n\tfailDifferenceStartTimerFor(channel);\n}\n\nvoid Updates::stateDone(const MTPupdates_State &state) {\n\tconst auto &d = state.c_updates_state();\n\tsetState(d.vpts().v, d.vdate().v, d.vqts().v, d.vseq().v);\n\n\t_lastUpdateTime = crl::now();\n\t_noUpdatesTimer.callOnce(kNoUpdatesTimeout);\n\t_ptsWaiter.setRequesting(false);\n\n\tsession().api().requestDialogs();\n\tupdateOnline();\n}\n\nvoid Updates::differenceDone(const MTPupdates_Difference &result) {\n\t_failDifferenceTimeout = 1;\n\n\tswitch (result.type()) {\n\tcase mtpc_updates_differenceEmpty: {\n\t\tauto &d = result.c_updates_differenceEmpty();\n\t\tsetState(_ptsWaiter.current(), d.vdate().v, _updatesQts, d.vseq().v);\n\n\t\t_lastUpdateTime = crl::now();\n\t\t_noUpdatesTimer.callOnce(kNoUpdatesTimeout);\n\n\t\t_ptsWaiter.setRequesting(false);\n\t} break;\n\tcase mtpc_updates_differenceSlice: {\n\t\tauto &d = result.c_updates_differenceSlice();\n\t\tfeedDifference(d.vusers(), d.vchats(), d.vnew_messages(), d.vother_updates());\n\n\t\tauto &s = d.vintermediate_state().c_updates_state();\n\t\tsetState(s.vpts().v, s.vdate().v, s.vqts().v, s.vseq().v);\n\n\t\t_ptsWaiter.setRequesting(false);\n\n\t\tMTP_LOG(0, (\"getDifference \"\n\t\t\t\"{ good - after a slice of difference was received }%1\"\n\t\t\t).arg(_session->mtp().isTestMode() ? \" TESTMODE\" : \"\"));\n\t\tgetDifference();\n\t} break;\n\tcase mtpc_updates_difference: {\n\t\tauto &d = result.c_updates_difference();\n\t\tfeedDifference(d.vusers(), d.vchats(), d.vnew_messages(), d.vother_updates());\n\n\t\tstateDone(d.vstate());\n\t} break;\n\tcase mtpc_updates_differenceTooLong: {\n\t\tLOG((\"API Error: updates.differenceTooLong is not supported by Telegram Desktop!\"));\n\t} break;\n\t};\n}\n\nbool Updates::whenGetDiffChanged(\n\t\tChannelData *channel,\n\t\tint32 ms,\n\t\tbase::flat_map<not_null<ChannelData*>, crl::time> &whenMap,\n\t\tcrl::time &curTime) {\n\tif (channel) {\n\t\tif (ms <= 0) {\n\t\t\tconst auto i = whenMap.find(channel);\n\t\t\tif (i != whenMap.cend()) {\n\t\t\t\twhenMap.erase(i);\n\t\t\t} else {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else {\n\t\t\tauto when = crl::now() + ms;\n\t\t\tconst auto i = whenMap.find(channel);\n\t\t\tif (i != whenMap.cend()) {\n\t\t\t\tif (i->second > when) {\n\t\t\t\t\ti->second = when;\n\t\t\t\t} else {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twhenMap.emplace(channel, when);\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (ms <= 0) {\n\t\t\tif (curTime) {\n\t\t\t\tcurTime = 0;\n\t\t\t} else {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else {\n\t\t\tauto when = crl::now() + ms;\n\t\t\tif (!curTime || curTime > when) {\n\t\t\t\tcurTime = when;\n\t\t\t} else {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid Updates::ptsWaiterStartTimerFor(ChannelData *channel, crl::time ms) {\n\tif (whenGetDiffChanged(channel, ms, _whenGetDiffByPts, _getDifferenceTimeByPts)) {\n\t\tgetDifferenceByPts();\n\t}\n}\n\nvoid Updates::failDifferenceStartTimerFor(ChannelData *channel) {\n\tauto &timeout = [&]() -> crl::time & {\n\t\tif (!channel) {\n\t\t\treturn _failDifferenceTimeout;\n\t\t}\n\t\tconst auto i = _channelFailDifferenceTimeout.find(channel);\n\t\treturn (i == _channelFailDifferenceTimeout.end())\n\t\t\t? _channelFailDifferenceTimeout.emplace(channel, 1).first->second\n\t\t\t: i->second;\n\t}();\n\tif (whenGetDiffChanged(channel, timeout * 1000, _whenGetDiffAfterFail, _getDifferenceTimeAfterFail)) {\n\t\tgetDifferenceAfterFail();\n\t}\n\tif (timeout < 64) timeout *= 2;\n}\n\nbool Updates::updateAndApply(\n\t\tint32 pts,\n\t\tint32 ptsCount,\n\t\tconst MTPUpdates &updates) {\n\treturn _ptsWaiter.updateAndApply(nullptr, pts, ptsCount, updates);\n}\n\nbool Updates::updateAndApply(\n\t\tint32 pts,\n\t\tint32 ptsCount,\n\t\tconst MTPUpdate &update) {\n\treturn _ptsWaiter.updateAndApply(nullptr, pts, ptsCount, update);\n}\n\nbool Updates::updateAndApply(int32 pts, int32 ptsCount) {\n\treturn _ptsWaiter.updateAndApply(nullptr, pts, ptsCount);\n}\n\nvoid Updates::feedDifference(\n\t\tconst MTPVector<MTPUser> &users,\n\t\tconst MTPVector<MTPChat> &chats,\n\t\tconst MTPVector<MTPMessage> &msgs,\n\t\tconst MTPVector<MTPUpdate> &other) {\n\tCore::App().checkAutoLock();\n\tsession().data().processUsers(users);\n\tsession().data().processChats(chats);\n\tapplyConvertToScheduledOnSend(other);\n\tfeedMessageIds(other);\n\tsession().data().processMessages(msgs, NewMessageType::Unread);\n\tfeedUpdateVector(other, SkipUpdatePolicy::SkipMessageIds);\n}\n\nvoid Updates::differenceFail(const MTP::Error &error) {\n\tLOG((\"RPC Error in getDifference: %1 %2: %3\").arg(\n\t\tQString::number(error.code()),\n\t\terror.type(),\n\t\terror.description()));\n\tfailDifferenceStartTimerFor(nullptr);\n}\n\nvoid Updates::getDifferenceByPts() {\n\tauto now = crl::now(), wait = crl::time(0);\n\tif (_getDifferenceTimeByPts) {\n\t\tif (_getDifferenceTimeByPts > now) {\n\t\t\twait = _getDifferenceTimeByPts - now;\n\t\t} else {\n\t\t\tgetDifference();\n\t\t}\n\t}\n\tfor (auto i = _whenGetDiffByPts.begin(); i != _whenGetDiffByPts.cend();) {\n\t\tif (i->second > now) {\n\t\t\twait = wait ? std::min(wait, i->second - now) : (i->second - now);\n\t\t\t++i;\n\t\t} else {\n\t\t\tgetChannelDifference(\n\t\t\t\ti->first,\n\t\t\t\tChannelDifferenceRequest::PtsGapOrShortPoll);\n\t\t\ti = _whenGetDiffByPts.erase(i);\n\t\t}\n\t}\n\tif (wait) {\n\t\t_byPtsTimer.callOnce(wait);\n\t} else {\n\t\t_byPtsTimer.cancel();\n\t}\n}\n\nvoid Updates::getDifferenceAfterFail() {\n\tauto now = crl::now(), wait = crl::time(0);\n\tif (_getDifferenceTimeAfterFail) {\n\t\tif (_getDifferenceTimeAfterFail > now) {\n\t\t\twait = _getDifferenceTimeAfterFail - now;\n\t\t} else {\n\t\t\t_ptsWaiter.setRequesting(false);\n\t\t\tMTP_LOG(0, (\"getDifference \"\n\t\t\t\t\"{ force - after get difference failed }%1\"\n\t\t\t\t).arg(_session->mtp().isTestMode() ? \" TESTMODE\" : \"\"));\n\t\t\tgetDifference();\n\t\t}\n\t}\n\tfor (auto i = _whenGetDiffAfterFail.begin(); i != _whenGetDiffAfterFail.cend();) {\n\t\tif (i->second > now) {\n\t\t\twait = wait ? std::min(wait, i->second - now) : (i->second - now);\n\t\t\t++i;\n\t\t} else {\n\t\t\ti->first->ptsSetRequesting(false);\n\t\t\tgetChannelDifference(i->first, ChannelDifferenceRequest::AfterFail);\n\t\t\ti = _whenGetDiffAfterFail.erase(i);\n\t\t}\n\t}\n\tif (wait) {\n\t\t_failDifferenceTimer.callOnce(wait);\n\t} else {\n\t\t_failDifferenceTimer.cancel();\n\t}\n}\n\nvoid Updates::getDifference() {\n\t_getDifferenceTimeByPts = 0;\n\n\tif (requestingDifference()) {\n\t\treturn;\n\t}\n\n\t_bySeqUpdates.clear();\n\t_bySeqTimer.cancel();\n\n\t_noUpdatesTimer.cancel();\n\t_getDifferenceTimeAfterFail = 0;\n\n\t_ptsWaiter.setRequesting(true);\n\n\tapi().request(MTPupdates_GetDifference(\n\t\tMTP_flags(0),\n\t\tMTP_int(_ptsWaiter.current()),\n\t\tMTPint(), // pts_limit\n\t\tMTPint(), // pts_total_limit\n\t\tMTP_int(_updatesDate),\n\t\tMTP_int(_updatesQts),\n\t\tMTPint() // qts_limit\n\t)).done([=](const MTPupdates_Difference &result) {\n\t\tdifferenceDone(result);\n\t}).fail([=](const MTP::Error &error) {\n\t\tdifferenceFail(error);\n\t}).send();\n}\n\nvoid Updates::getChannelDifference(\n\t\tnot_null<ChannelData*> channel,\n\t\tChannelDifferenceRequest from) {\n\tif (from != ChannelDifferenceRequest::PtsGapOrShortPoll) {\n\t\t_whenGetDiffByPts.remove(channel);\n\t}\n\n\tif (!channel->ptsInited() || channel->ptsRequesting()) {\n\t\treturn;\n\t}\n\n\tif (from != ChannelDifferenceRequest::AfterFail) {\n\t\t_whenGetDiffAfterFail.remove(channel);\n\t}\n\n\tchannel->ptsSetRequesting(true);\n\n\tauto filter = MTP_channelMessagesFilterEmpty();\n\tauto flags = MTPupdates_GetChannelDifference::Flag::f_force | 0;\n\tif (from != ChannelDifferenceRequest::PtsGapOrShortPoll) {\n\t\tif (!channel->ptsWaitingForSkipped()) {\n\t\t\tflags = 0; // No force flag when requesting for short poll.\n\t\t}\n\t}\n\tapi().request(MTPupdates_GetChannelDifference(\n\t\tMTP_flags(flags),\n\t\tchannel->inputChannel(),\n\t\tfilter,\n\t\tMTP_int(channel->pts()),\n\t\tMTP_int(kChannelGetDifferenceLimit)\n\t)).done([=](const MTPupdates_ChannelDifference &result) {\n\t\tchannelDifferenceDone(channel, result);\n\t}).fail([=](const MTP::Error &error) {\n\t\tchannelDifferenceFail(channel, error);\n\t}).send();\n}\n\nvoid Updates::sendPing() {\n\t_session->mtp().ping();\n}\n\nvoid Updates::addActiveChat(rpl::producer<PeerData*> chat) {\n\tconst auto key = _activeChats.empty() ? 0 : _activeChats.back().first + 1;\n\tstd::move(\n\t\tchat\n\t) | rpl::on_next_done([=](PeerData *peer) {\n\t\tauto &active = _activeChats[key];\n\t\tconst auto was = active.peer;\n\t\tif (was != peer) {\n\t\t\tactive.peer = peer;\n\t\t\tif (const auto channel = was ? was->asChannel() : nullptr) {\n\t\t\t\tif (!inActiveChats(channel)) {\n\t\t\t\t\tchannel->ptsSetWaitingForShortPoll(-1);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (const auto channel = peer ? peer->asChannel() : nullptr) {\n\t\t\t\tchannel->ptsSetWaitingForShortPoll(\n\t\t\t\t\tkWaitForChannelGetDifference);\n\t\t\t}\n\t\t}\n\t}, [=] {\n\t\t_activeChats.erase(key);\n\t}, _activeChats[key].lifetime);\n}\n\nbool Updates::inActiveChats(not_null<PeerData*> peer) const {\n\treturn ranges::contains(\n\t\t_activeChats,\n\t\tpeer.get(),\n\t\t[](const auto &pair) { return pair.second.peer; });\n}\n\nvoid Updates::requestChannelRangeDifference(not_null<History*> history) {\n\tExpects(history->peer->isChannel());\n\n\tconst auto channel = history->peer->asChannel();\n\tif (const auto requestId = _rangeDifferenceRequests.take(channel)) {\n\t\tapi().request(*requestId).cancel();\n\t}\n\tconst auto range = history->rangeForDifferenceRequest();\n\tif (!(range.from < range.till) || !channel->pts()) {\n\t\treturn;\n\t}\n\n\tMTP_LOG(0, (\"getChannelDifference \"\n\t\t\"{ good - after channelDifferenceTooLong was received, \"\n\t\t\"validating history part }%1\"\n\t\t).arg(_session->mtp().isTestMode() ? \" TESTMODE\" : \"\"));\n\tchannelRangeDifferenceSend(channel, range, channel->pts());\n}\n\nvoid Updates::channelRangeDifferenceSend(\n\t\tnot_null<ChannelData*> channel,\n\t\tMsgRange range,\n\t\tint32 pts) {\n\tExpects(range.from < range.till);\n\n\tconst auto limit = range.till - range.from;\n\tconst auto filter = MTP_channelMessagesFilter(\n\t\tMTP_flags(0),\n\t\tMTP_vector<MTPMessageRange>(1, MTP_messageRange(\n\t\t\tMTP_int(range.from),\n\t\t\tMTP_int(range.till - 1))));\n\tconst auto requestId = api().request(MTPupdates_GetChannelDifference(\n\t\tMTP_flags(MTPupdates_GetChannelDifference::Flag::f_force),\n\t\tchannel->inputChannel(),\n\t\tfilter,\n\t\tMTP_int(pts),\n\t\tMTP_int(limit)\n\t)).done([=](const MTPupdates_ChannelDifference &result) {\n\t\t_rangeDifferenceRequests.remove(channel);\n\t\tchannelRangeDifferenceDone(channel, range, result);\n\t}).fail([=] {\n\t\t_rangeDifferenceRequests.remove(channel);\n\t}).send();\n\t_rangeDifferenceRequests.emplace(channel, requestId);\n}\n\nvoid Updates::channelRangeDifferenceDone(\n\t\tnot_null<ChannelData*> channel,\n\t\tMsgRange range,\n\t\tconst MTPupdates_ChannelDifference &result) {\n\tauto nextRequestPts = int32(0);\n\tauto isFinal = true;\n\n\tswitch (result.type()) {\n\tcase mtpc_updates_channelDifferenceEmpty: {\n\t\tconst auto &d = result.c_updates_channelDifferenceEmpty();\n\t\tnextRequestPts = d.vpts().v;\n\t\tisFinal = d.is_final();\n\t} break;\n\n\tcase mtpc_updates_channelDifferenceTooLong: {\n\t\tconst auto &d = result.c_updates_channelDifferenceTooLong();\n\n\t\t_session->data().processUsers(d.vusers());\n\t\t_session->data().processChats(d.vchats());\n\n\t\tnextRequestPts = d.vdialog().match([&](const MTPDdialog &data) {\n\t\t\treturn data.vpts().value_or_empty();\n\t\t}, [&](const MTPDdialogFolder &data) {\n\t\t\treturn 0;\n\t\t});\n\t\tisFinal = d.is_final();\n\t} break;\n\n\tcase mtpc_updates_channelDifference: {\n\t\tconst auto &d = result.c_updates_channelDifference();\n\n\t\tfeedChannelDifference(d);\n\n\t\tnextRequestPts = d.vpts().v;\n\t\tisFinal = d.is_final();\n\t} break;\n\t}\n\n\tif (!isFinal && nextRequestPts) {\n\t\tMTP_LOG(0, (\"getChannelDifference \"\n\t\t\t\"{ good - after not final channelDifference was received, \"\n\t\t\t\"validating history part }%1\"\n\t\t\t).arg(_session->mtp().isTestMode() ? \" TESTMODE\" : \"\"));\n\t\tchannelRangeDifferenceSend(channel, range, nextRequestPts);\n\t}\n}\n\nvoid Updates::mtpNewSessionCreated() {\n\tCore::App().checkAutoLock();\n\t_updatesSeq = 0;\n\tMTP_LOG(0, (\"getDifference { after new_session_created }%1\"\n\t\t).arg(_session->mtp().isTestMode() ? \" TESTMODE\" : \"\"));\n\tgetDifference();\n}\n\nvoid Updates::mtpUpdateReceived(const MTPUpdates &updates) {\n\tCore::App().checkAutoLock();\n\t_lastUpdateTime = crl::now();\n\t_noUpdatesTimer.callOnce(kNoUpdatesTimeout);\n\tif (!requestingDifference()\n\t\t|| HasForceLogoutNotification(updates)) {\n\t\tapplyUpdates(updates);\n\t} else {\n\t\tapplyGroupCallParticipantUpdates(updates);\n\t}\n}\n\nvoid Updates::applyConvertToScheduledOnSend(\n\t\tconst MTPVector<MTPUpdate> &other,\n\t\tbool skipScheduledCheck) {\n\tfor (const auto &update : other.v) {\n\t\tupdate.match([&](const MTPDupdateNewScheduledMessage &data) {\n\t\t\tconst auto &message = data.vmessage();\n\t\t\tconst auto id = IdFromMessage(message);\n\t\t\tconst auto scheduledMessages = &_session->scheduledMessages();\n\t\t\tconst auto scheduledId = scheduledMessages->localMessageId(id);\n\t\t\tfor (const auto &updateId : other.v) {\n\t\t\t\tupdateId.match([&](const MTPDupdateMessageID &dataId) {\n\t\t\t\t\tif (dataId.vid().v == id) {\n\t\t\t\t\t\tauto &owner = session().data();\n\t\t\t\t\t\tif (skipScheduledCheck) {\n\t\t\t\t\t\t\tconst auto peerId = PeerFromMessage(message);\n\t\t\t\t\t\t\tconst auto history = owner.historyLoaded(peerId);\n\t\t\t\t\t\t\tif (history) {\n\t\t\t\t\t\t\t\t_session->data().sentToScheduled({\n\t\t\t\t\t\t\t\t\t.history = history,\n\t\t\t\t\t\t\t\t\t.scheduledId = scheduledId,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst auto rand = dataId.vrandom_id().v;\n\t\t\t\t\t\tconst auto localId = owner.messageIdByRandomId(rand);\n\t\t\t\t\t\tif (const auto local = owner.message(localId)) {\n\t\t\t\t\t\t\tif (!local->isScheduled()) {\n\t\t\t\t\t\t\t\t_session->data().sentToScheduled({\n\t\t\t\t\t\t\t\t\t.history = local->history(),\n\t\t\t\t\t\t\t\t\t.scheduledId = scheduledId,\n\t\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t\t// We've sent a non-scheduled message,\n\t\t\t\t\t\t\t\t// but it was converted to a scheduled.\n\t\t\t\t\t\t\t\tlocal->destroy();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}, [](const auto &) {});\n\t\t\t}\n\t\t}, [](const auto &) {});\n\t}\n}\n\nvoid Updates::applyGroupCallParticipantUpdates(const MTPUpdates &updates) {\n\tupdates.match([&](const MTPDupdates &data) {\n\t\tsession().data().processUsers(data.vusers());\n\t\tsession().data().processChats(data.vchats());\n\t\tfeedUpdateVector(\n\t\t\tdata.vupdates(),\n\t\t\tSkipUpdatePolicy::SkipExceptGroupCallParticipants);\n\t}, [&](const MTPDupdatesCombined &data) {\n\t\tsession().data().processUsers(data.vusers());\n\t\tsession().data().processChats(data.vchats());\n\t\tfeedUpdateVector(\n\t\t\tdata.vupdates(),\n\t\t\tSkipUpdatePolicy::SkipExceptGroupCallParticipants);\n\t}, [&](const MTPDupdateShort &data) {\n\t\tif (data.vupdate().type() == mtpc_updateGroupCallParticipants\n\t\t\t|| data.vupdate().type() == mtpc_updateGroupCallChainBlocks) {\n\t\t\tfeedUpdate(data.vupdate());\n\t\t}\n\t}, [](const auto &) {\n\t});\n}\n\nint32 Updates::pts() const {\n\treturn _ptsWaiter.current();\n}\n\nvoid Updates::updateOnline(crl::time lastNonIdleTime) {\n\tupdateOnline(lastNonIdleTime, false);\n}\n\nbool Updates::isIdle() const {\n\treturn _isIdle.current();\n}\n\nrpl::producer<bool> Updates::isIdleValue() const {\n\treturn _isIdle.value();\n}\n\nvoid Updates::updateOnline(crl::time lastNonIdleTime, bool gotOtherOffline) {\n\tif (!lastNonIdleTime) {\n\t\tlastNonIdleTime = Core::App().lastNonIdleTime();\n\t}\n\tcrl::on_main(&session(), [=] {\n\t\tCore::App().checkAutoLock(lastNonIdleTime);\n\t});\n\n\tconst auto &config = _session->serverConfig();\n\tbool isOnline = Core::App().hasActiveWindow(&session());\n\tint updateIn = config.onlineUpdatePeriod;\n\tAssert(updateIn >= 0);\n\tif (isOnline) {\n\t\tconst auto idle = crl::now() - lastNonIdleTime;\n\t\tif (idle >= config.offlineIdleTimeout) {\n\t\t\tisOnline = false;\n\t\t\tif (!isIdle()) {\n\t\t\t\t_isIdle = true;\n\t\t\t\t_idleFinishTimer.callOnce(900);\n\t\t\t}\n\t\t} else {\n\t\t\tupdateIn = qMin(updateIn, int(config.offlineIdleTimeout - idle));\n\t\t\tAssert(updateIn >= 0);\n\t\t}\n\t}\n\tauto ms = crl::now();\n\tif (isOnline != _lastWasOnline\n\t\t|| (isOnline && _lastSetOnline + config.onlineUpdatePeriod <= ms)\n\t\t|| (isOnline && gotOtherOffline)) {\n\t\tapi().request(base::take(_onlineRequest)).cancel();\n\n\t\t_lastWasOnline = isOnline;\n\t\t_lastSetOnline = ms;\n\t\tif (!Core::Quitting()) {\n\t\t\t_onlineRequest = api().request(MTPaccount_UpdateStatus(\n\t\t\t\tMTP_bool(!isOnline)\n\t\t\t)).send();\n\t\t} else {\n\t\t\t_onlineRequest = api().request(MTPaccount_UpdateStatus(\n\t\t\t\tMTP_bool(!isOnline)\n\t\t\t)).done([=] {\n\t\t\t\tCore::App().quitPreventFinished();\n\t\t\t}).fail([=] {\n\t\t\t\tCore::App().quitPreventFinished();\n\t\t\t}).send();\n\t\t}\n\n\t\tconst auto self = session().user();\n\t\tconst auto onlineFor = (config.onlineUpdatePeriod / 1000);\n\t\tself->updateLastseen(Data::LastseenStatus::OnlineTill(\n\t\t\tbase::unixtime::now() + (isOnline ? onlineFor : -1)));\n\t\tsession().changes().peerUpdated(\n\t\t\tself,\n\t\t\tData::PeerUpdate::Flag::OnlineStatus);\n\t\tif (!isOnline) { // Went offline, so we need to save message draft to the cloud.\n\t\t\tapi().saveCurrentDraftToCloud();\n\t\t\tsession().data().maybeStopWatchForOffline(self);\n\t\t}\n\n\t\t_lastSetOnline = ms;\n\t} else if (isOnline) {\n\t\tupdateIn = qMin(updateIn, int(_lastSetOnline + config.onlineUpdatePeriod - ms));\n\t\tAssert(updateIn >= 0);\n\t}\n\t_onlineTimer.callOnce(updateIn);\n}\n\nvoid Updates::checkIdleFinish(crl::time lastNonIdleTime) {\n\tif (!lastNonIdleTime) {\n\t\tlastNonIdleTime = Core::App().lastNonIdleTime();\n\t}\n\tif (crl::now() - lastNonIdleTime\n\t\t< _session->serverConfig().offlineIdleTimeout) {\n\t\tupdateOnline(lastNonIdleTime);\n\t\t_idleFinishTimer.cancel();\n\t\t_isIdle = false;\n\t} else {\n\t\t_idleFinishTimer.callOnce(900);\n\t}\n}\n\nbool Updates::lastWasOnline() const {\n\treturn _lastWasOnline;\n}\n\ncrl::time Updates::lastSetOnline() const {\n\treturn _lastSetOnline;\n}\n\nbool Updates::isQuitPrevent() {\n\tif (!_lastWasOnline) {\n\t\treturn false;\n\t}\n\tLOG((\"Api::Updates prevents quit, sending offline status...\"));\n\tupdateOnline(crl::now());\n\treturn true;\n}\n\nvoid Updates::handleSendActionUpdate(\n\t\tPeerId peerId,\n\t\tMsgId rootId,\n\t\tPeerId fromId,\n\t\tconst MTPSendMessageAction &action) {\n\tconst auto history = session().data().historyLoaded(peerId);\n\tif (!history) {\n\t\treturn;\n\t}\n\tconst auto peer = history->peer;\n\tconst auto from = (fromId == session().userPeerId())\n\t\t? session().user().get()\n\t\t: session().data().peerLoaded(fromId);\n\tconst auto when = requestingDifference()\n\t\t? 0\n\t\t: base::unixtime::now();\n\tif (action.type() == mtpc_speakingInGroupCallAction) {\n\t\thandleSpeakingInCall(peer, fromId, from);\n\t}\n\tif (!from || !from->isUser() || from->isSelf()) {\n\t\treturn;\n\t} else if (action.type() == mtpc_sendMessageEmojiInteraction) {\n\t\thandleEmojiInteraction(peer, action.c_sendMessageEmojiInteraction());\n\t\treturn;\n\t} else if (action.type() == mtpc_sendMessageEmojiInteractionSeen) {\n\t\tconst auto &data = action.c_sendMessageEmojiInteractionSeen();\n\t\thandleEmojiInteraction(peer, qs(data.vemoticon()));\n\t\treturn;\n\t} else if (action.type() == mtpc_sendMessageTextDraftAction) {\n\t\tconst auto &data = action.c_sendMessageTextDraftAction();\n\t\thistory->streamedDrafts().apply(rootId, fromId, when, data);\n\t\treturn;\n\t}\n\tsession().data().sendActionManager().registerFor(\n\t\thistory,\n\t\trootId,\n\t\tfrom->asUser(),\n\t\taction,\n\t\twhen);\n}\n\nvoid Updates::handleEmojiInteraction(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPDsendMessageEmojiInteraction &data) {\n\tconst auto json = data.vinteraction().match([&](\n\t\t\tconst MTPDdataJSON &data) {\n\t\treturn data.vdata().v;\n\t});\n\thandleEmojiInteraction(\n\t\tpeer,\n\t\tdata.vmsg_id().v,\n\t\tqs(data.vemoticon()),\n\t\tChatHelpers::EmojiInteractions::Parse(json));\n}\n\nvoid Updates::handleSpeakingInCall(\n\t\tnot_null<PeerData*> peer,\n\t\tPeerId participantPeerId,\n\t\tPeerData *participantPeerLoaded) {\n\tif (!peer->isChat() && !peer->isChannel()) {\n\t\treturn;\n\t}\n\tconst auto call = peer->groupCall();\n\tconst auto now = crl::now();\n\tif (call) {\n\t\tcall->applyActiveUpdate(\n\t\t\tparticipantPeerId,\n\t\t\tData::LastSpokeTimes{ .anything = now, .voice = now },\n\t\t\tparticipantPeerLoaded);\n\t} else {\n\t\tconst auto chat = peer->asChat();\n\t\tconst auto channel = peer->asChannel();\n\t\tconst auto active = chat\n\t\t\t? (chat->flags() & ChatDataFlag::CallActive)\n\t\t\t: (channel->flags() & ChannelDataFlag::CallActive);\n\t\tif (active) {\n\t\t\t_pendingSpeakingCallParticipants.emplace(\n\t\t\t\tpeer).first->second[participantPeerId] = now;\n\t\t\tif (peerIsUser(participantPeerId)) {\n\t\t\t\tsession().api().requestFullPeer(peer);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Updates::handleEmojiInteraction(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId messageId,\n\t\tconst QString &emoticon,\n\t\tChatHelpers::EmojiInteractionsBunch bunch) {\n\tif (session().windows().empty()) {\n\t\treturn;\n\t}\n\tconst auto window = session().windows().front();\n\twindow->emojiInteractions().startIncoming(\n\t\tpeer,\n\t\tmessageId,\n\t\temoticon,\n\t\tstd::move(bunch));\n}\n\nvoid Updates::handleEmojiInteraction(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &emoticon) {\n\tif (session().windows().empty()) {\n\t\treturn;\n\t}\n\tconst auto window = session().windows().front();\n\twindow->emojiInteractions().seenOutgoing(peer, emoticon);\n}\n\nvoid Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {\n\tswitch (updates.type()) {\n\tcase mtpc_updateShortMessage: {\n\t\tconst auto &d = updates.c_updateShortMessage();\n\t\tconst auto flags = mtpCastFlags(d.vflags().v)\n\t\t\t| MTPDmessage::Flag::f_from_id;\n\t\t_session->data().addNewMessage(\n\t\t\tMTP_message(\n\t\t\t\tMTP_flags(flags),\n\t\t\t\td.vid(),\n\t\t\t\t(d.is_out()\n\t\t\t\t\t? peerToMTP(_session->userPeerId())\n\t\t\t\t\t: MTP_peerUser(d.vuser_id())),\n\t\t\t\tMTPint(), // from_boosts_applied\n\t\t\t\tMTPstring(), // from_rank\n\t\t\t\tMTP_peerUser(d.vuser_id()),\n\t\t\t\tMTPPeer(), // saved_peer_id\n\t\t\t\td.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(),\n\t\t\t\tMTP_long(d.vvia_bot_id().value_or_empty()),\n\t\t\t\tMTPlong(), // via_business_bot_id\n\t\t\t\td.vreply_to() ? *d.vreply_to() : MTPMessageReplyHeader(),\n\t\t\t\td.vdate(),\n\t\t\t\td.vmessage(),\n\t\t\t\tMTP_messageMediaEmpty(),\n\t\t\t\tMTPReplyMarkup(),\n\t\t\t\tMTP_vector<MTPMessageEntity>(d.ventities().value_or_empty()),\n\t\t\t\tMTPint(), // views\n\t\t\t\tMTPint(), // forwards\n\t\t\t\tMTPMessageReplies(),\n\t\t\t\tMTPint(), // edit_date\n\t\t\t\tMTPstring(),\n\t\t\t\tMTPlong(),\n\t\t\t\tMTPMessageReactions(),\n\t\t\t\tMTPVector<MTPRestrictionReason>(),\n\t\t\t\tMTP_int(d.vttl_period().value_or_empty()),\n\t\t\t\tMTPint(), // quick_reply_shortcut_id\n\t\t\t\tMTPlong(), // effect\n\t\t\t\tMTPFactCheck(),\n\t\t\t\tMTPint(), // report_delivery_until_date\n\t\t\t\tMTPlong(), // paid_message_stars\n\t\t\t\tMTPSuggestedPost(),\n\t\t\t\tMTPint(), // schedule_repeat_period\n\t\t\t\tMTPstring()), // summary_from_language\n\t\t\tMessageFlags(),\n\t\t\tNewMessageType::Unread);\n\t} break;\n\n\tcase mtpc_updateShortChatMessage: {\n\t\tconst auto &d = updates.c_updateShortChatMessage();\n\t\tconst auto flags = mtpCastFlags(d.vflags().v)\n\t\t\t| MTPDmessage::Flag::f_from_id;\n\t\t_session->data().addNewMessage(\n\t\t\tMTP_message(\n\t\t\t\tMTP_flags(flags),\n\t\t\t\td.vid(),\n\t\t\t\tMTP_peerUser(d.vfrom_id()),\n\t\t\t\tMTPint(), // from_boosts_applied\n\t\t\t\tMTPstring(), // from_rank\n\t\t\t\tMTP_peerChat(d.vchat_id()),\n\t\t\t\tMTPPeer(), // saved_peer_id\n\t\t\t\td.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(),\n\t\t\t\tMTP_long(d.vvia_bot_id().value_or_empty()),\n\t\t\t\tMTPlong(), // via_business_bot_id\n\t\t\t\td.vreply_to() ? *d.vreply_to() : MTPMessageReplyHeader(),\n\t\t\t\td.vdate(),\n\t\t\t\td.vmessage(),\n\t\t\t\tMTP_messageMediaEmpty(),\n\t\t\t\tMTPReplyMarkup(),\n\t\t\t\tMTP_vector<MTPMessageEntity>(d.ventities().value_or_empty()),\n\t\t\t\tMTPint(), // views\n\t\t\t\tMTPint(), // forwards\n\t\t\t\tMTPMessageReplies(),\n\t\t\t\tMTPint(), // edit_date\n\t\t\t\tMTPstring(),\n\t\t\t\tMTPlong(),\n\t\t\t\tMTPMessageReactions(),\n\t\t\t\tMTPVector<MTPRestrictionReason>(),\n\t\t\t\tMTP_int(d.vttl_period().value_or_empty()),\n\t\t\t\tMTPint(), // quick_reply_shortcut_id\n\t\t\t\tMTPlong(), // effect\n\t\t\t\tMTPFactCheck(),\n\t\t\t\tMTPint(), // report_delivery_until_date\n\t\t\t\tMTPlong(), // paid_message_stars\n\t\t\t\tMTPSuggestedPost(),\n\t\t\t\tMTPint(), // schedule_repeat_period\n\t\t\t\tMTPstring()), // summary_from_language\n\t\t\tMessageFlags(),\n\t\t\tNewMessageType::Unread);\n\t} break;\n\n\tcase mtpc_updateShortSentMessage: {\n\t\tauto &d = updates.c_updateShortSentMessage();\n\t\tQ_UNUSED(d); // Sent message data was applied anyway.\n\t} break;\n\n\tdefault: Unexpected(\"Type in applyUpdatesNoPtsCheck()\");\n\t}\n}\n\nvoid Updates::applyUpdateNoPtsCheck(const MTPUpdate &update) {\n\tswitch (update.type()) {\n\tcase mtpc_updateNewMessage: {\n\t\tauto &d = update.c_updateNewMessage();\n\t\tauto needToAdd = true;\n\t\tif (d.vmessage().type() == mtpc_message) { // index forwarded messages to links _overview\n\t\t\tconst auto &data = d.vmessage().c_message();\n\t\t\tif (_session->data().updateExistingMessage(data)) { // already in blocks\n\t\t\t\tLOG((\"Skipping message, because it is already in blocks!\"));\n\t\t\t\tneedToAdd = false;\n\t\t\t}\n\t\t\tProcessScheduledMessageWithElapsedTime(_session, needToAdd, data);\n\t\t}\n\t\tif (needToAdd) {\n\t\t\t_session->data().addNewMessage(\n\t\t\t\td.vmessage(),\n\t\t\t\tMessageFlags(),\n\t\t\t\tNewMessageType::Unread);\n\t\t}\n\t} break;\n\n\tcase mtpc_updateReadMessagesContents: {\n\t\tconst auto &d = update.c_updateReadMessagesContents();\n\t\tauto unknownReadIds = base::flat_set<MsgId>();\n\t\tfor (const auto &msgId : d.vmessages().v) {\n\t\t\tif (const auto item = _session->data().nonChannelMessage(msgId.v)) {\n\t\t\t\tif (item->isUnreadMedia() || item->isUnreadMention()) {\n\t\t\t\t\titem->markMediaAndMentionRead();\n\t\t\t\t\t_session->data().requestItemRepaint(item);\n\n\t\t\t\t\tif (item->out()) {\n\t\t\t\t\t\tconst auto user = item->history()->peer->asUser();\n\t\t\t\t\t\tif (user && !requestingDifference()) {\n\t\t\t\t\t\t\tuser->madeAction(base::unixtime::now());\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\titem->clearMediaAsExpired();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Perhaps it was an unread mention!\n\t\t\t\tunknownReadIds.insert(msgId.v);\n\t\t\t}\n\t\t}\n\t\tsession().api().unreadThings().mediaAndMentionsRead(unknownReadIds);\n\t} break;\n\n\tcase mtpc_updateReadHistoryInbox: {\n\t\tconst auto &d = update.c_updateReadHistoryInbox();\n\t\tconst auto peer = peerFromMTP(d.vpeer());\n\t\tif (const auto history = _session->data().historyLoaded(peer)) {\n\t\t\tconst auto folderId = d.vfolder_id().value_or_empty();\n\t\t\thistory->applyInboxReadUpdate(\n\t\t\t\tfolderId,\n\t\t\t\td.vmax_id().v,\n\t\t\t\td.vstill_unread_count().v);\n\t\t}\n\t} break;\n\n\tcase mtpc_updateReadHistoryOutbox: {\n\t\tconst auto &d = update.c_updateReadHistoryOutbox();\n\t\tconst auto peer = peerFromMTP(d.vpeer());\n\t\tif (const auto history = _session->data().historyLoaded(peer)) {\n\t\t\thistory->outboxRead(d.vmax_id().v);\n\t\t\tif (!requestingDifference()) {\n\t\t\t\tif (const auto user = history->peer->asUser()) {\n\t\t\t\t\tuser->madeAction(base::unixtime::now());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updateWebPage: {\n\t\tauto &d = update.c_updateWebPage();\n\t\tQ_UNUSED(d); // Web page was updated anyway.\n\t} break;\n\n\tcase mtpc_updateFolderPeers: {\n\t\tconst auto &data = update.c_updateFolderPeers();\n\t\tauto &owner = _session->data();\n\t\tfor (const auto &peer : data.vfolder_peers().v) {\n\t\t\tpeer.match([&](const MTPDfolderPeer &data) {\n\t\t\t\tconst auto peerId = peerFromMTP(data.vpeer());\n\t\t\t\tif (const auto history = owner.historyLoaded(peerId)) {\n\t\t\t\t\tif (const auto folderId = data.vfolder_id().v) {\n\t\t\t\t\t\thistory->setFolder(owner.folder(folderId));\n\t\t\t\t\t} else {\n\t\t\t\t\t\thistory->clearFolder();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t} break;\n\n\tcase mtpc_updateDeleteMessages: {\n\t\tauto &d = update.c_updateDeleteMessages();\n\t\t_session->data().processNonChannelMessagesDeleted(d.vmessages().v);\n\t} break;\n\n\tcase mtpc_updateNewChannelMessage: {\n\t\tauto &d = update.c_updateNewChannelMessage();\n\t\tauto needToAdd = true;\n\t\tif (d.vmessage().type() == mtpc_message) { // index forwarded messages to links _overview\n\t\t\tconst auto &data = d.vmessage().c_message();\n\t\t\tif (_session->data().updateExistingMessage(data)) { // already in blocks\n\t\t\t\tLOG((\"Skipping message, because it is already in blocks!\"));\n\t\t\t\tneedToAdd = false;\n\t\t\t}\n\t\t\tProcessScheduledMessageWithElapsedTime(_session, needToAdd, data);\n\t\t}\n\t\tif (needToAdd) {\n\t\t\t_session->data().addNewMessage(\n\t\t\t\td.vmessage(),\n\t\t\t\tMessageFlags(),\n\t\t\t\tNewMessageType::Unread);\n\t\t}\n\t} break;\n\n\tcase mtpc_updateEditChannelMessage: {\n\t\tauto &d = update.c_updateEditChannelMessage();\n\t\t_session->data().updateEditedMessage(d.vmessage());\n\t} break;\n\n\tcase mtpc_updatePinnedChannelMessages: {\n\t\tconst auto &d = update.c_updatePinnedChannelMessages();\n\t\tconst auto peerId = peerFromChannel(d.vchannel_id());\n\t\tfor (const auto &msgId : d.vmessages().v) {\n\t\t\tconst auto item = session().data().message(peerId, msgId.v);\n\t\t\tif (item) {\n\t\t\t\titem->setIsPinned(d.is_pinned());\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updateEditMessage: {\n\t\tauto &d = update.c_updateEditMessage();\n\t\t_session->data().updateEditedMessage(d.vmessage());\n\t} break;\n\n\tcase mtpc_updateChannelWebPage: {\n\t\tauto &d = update.c_updateChannelWebPage();\n\t\tQ_UNUSED(d); // Web page was updated anyway.\n\t} break;\n\n\tcase mtpc_updateDeleteChannelMessages: {\n\t\tauto &d = update.c_updateDeleteChannelMessages();\n\t\t_session->data().processMessagesDeleted(\n\t\t\tpeerFromChannel(d.vchannel_id().v),\n\t\t\td.vmessages().v);\n\t} break;\n\n\tcase mtpc_updatePinnedMessages: {\n\t\tconst auto &d = update.c_updatePinnedMessages();\n\t\tconst auto peerId = peerFromMTP(d.vpeer());\n\t\tfor (const auto &msgId : d.vmessages().v) {\n\t\t\tconst auto item = session().data().message(peerId, msgId.v);\n\t\t\tif (item) {\n\t\t\t\titem->setIsPinned(d.is_pinned());\n\t\t\t}\n\t\t}\n\t} break;\n\n\tdefault: Unexpected(\"Type in applyUpdateNoPtsCheck()\");\n\t}\n}\n\nvoid Updates::applyUpdates(\n\t\tconst MTPUpdates &updates,\n\t\tuint64 sentMessageRandomId) {\n\tconst auto randomId = sentMessageRandomId;\n\n\tswitch (updates.type()) {\n\tcase mtpc_updates: {\n\t\tauto &d = updates.c_updates();\n\t\tif (d.vseq().v) {\n\t\t\tif (d.vseq().v <= _updatesSeq) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (d.vseq().v > _updatesSeq + 1) {\n\t\t\t\t_bySeqUpdates.emplace(d.vseq().v, updates);\n\t\t\t\t_bySeqTimer.callOnce(PtsWaiter::kWaitForSkippedTimeout);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tsession().data().processUsers(d.vusers());\n\t\tsession().data().processChats(d.vchats());\n\t\tfeedUpdateVector(d.vupdates());\n\n\t\tsetState(0, d.vdate().v, _updatesQts, d.vseq().v);\n\t} break;\n\n\tcase mtpc_updatesCombined: {\n\t\tauto &d = updates.c_updatesCombined();\n\t\tif (d.vseq_start().v) {\n\t\t\tif (d.vseq_start().v <= _updatesSeq) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (d.vseq_start().v > _updatesSeq + 1) {\n\t\t\t\t_bySeqUpdates.emplace(d.vseq_start().v, updates);\n\t\t\t\t_bySeqTimer.callOnce(PtsWaiter::kWaitForSkippedTimeout);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tsession().data().processUsers(d.vusers());\n\t\tsession().data().processChats(d.vchats());\n\t\tfeedUpdateVector(d.vupdates());\n\n\t\tsetState(0, d.vdate().v, _updatesQts, d.vseq().v);\n\t} break;\n\n\tcase mtpc_updateShort: {\n\t\tauto &d = updates.c_updateShort();\n\t\tfeedUpdate(d.vupdate());\n\n\t\tsetState(0, d.vdate().v, _updatesQts, _updatesSeq);\n\t} break;\n\n\tcase mtpc_updateShortMessage: {\n\t\tauto &d = updates.c_updateShortMessage();\n\t\tif (!DataIsLoaded(&_session->data(), d)) {\n\t\t\tMTP_LOG(0, (\"getDifference \"\n\t\t\t\t\"{ good - after not all data loaded in updateShortMessage }%1\"\n\t\t\t).arg(_session->mtp().isTestMode() ? \" TESTMODE\" : \"\"));\n\t\t\treturn getDifference();\n\t\t}\n\t\t_session->data().fillMessagePeers(d);\n\t\tif (updateAndApply(d.vpts().v, d.vpts_count().v, updates)) {\n\t\t\t// Update date as well.\n\t\t\tsetState(0, d.vdate().v, _updatesQts, _updatesSeq);\n\t\t}\n\t} break;\n\n\tcase mtpc_updateShortChatMessage: {\n\t\tauto &d = updates.c_updateShortChatMessage();\n\t\tif (!DataIsLoaded(&_session->data(), d)) {\n\t\t\tMTP_LOG(0, (\"getDifference \"\n\t\t\t\t\"{ good - after not all data loaded in updateShortChatMessage }%1\"\n\t\t\t).arg(_session->mtp().isTestMode() ? \" TESTMODE\" : \"\"));\n\t\t\treturn getDifference();\n\t\t}\n\t\t_session->data().fillMessagePeers(d);\n\t\tif (updateAndApply(d.vpts().v, d.vpts_count().v, updates)) {\n\t\t\t// Update date as well.\n\t\t\tsetState(0, d.vdate().v, _updatesQts, _updatesSeq);\n\t\t}\n\t} break;\n\n\tcase mtpc_updateShortSentMessage: {\n\t\tauto &d = updates.c_updateShortSentMessage();\n\t\tif (!IsServerMsgId(d.vid().v)) {\n\t\t\tLOG((\"API Error: Bad msgId got from server: %1\").arg(d.vid().v));\n\t\t} else if (randomId) {\n\t\t\tauto &owner = session().data();\n\t\t\tconst auto sent = owner.messageSentData(randomId);\n\t\t\tconst auto lookupMessage = [&] {\n\t\t\t\treturn sent.peerId\n\t\t\t\t\t? owner.message(sent.peerId, d.vid().v)\n\t\t\t\t\t: nullptr;\n\t\t\t};\n\t\t\tif (const auto id = owner.messageIdByRandomId(randomId)) {\n\t\t\t\tconst auto local = owner.message(id);\n\t\t\t\tif (local && local->isScheduled()) {\n\t\t\t\t\tsession().scheduledMessages().sendNowSimpleMessage(\n\t\t\t\t\t\td,\n\t\t\t\t\t\tlocal);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto wasAlready = (lookupMessage() != nullptr);\n\t\t\tfeedUpdate(MTP_updateMessageID(d.vid(), MTP_long(randomId))); // ignore real date\n\t\t\tif (const auto item = lookupMessage()) {\n\t\t\t\t_session->data().fillMessagePeers(item->fullId(), d);\n\t\t\t\titem->applySentMessage(sent.text, d, wasAlready);\n\t\t\t}\n\t\t}\n\n\t\tif (updateAndApply(d.vpts().v, d.vpts_count().v, updates)) {\n\t\t\t// Update date as well.\n\t\t\tsetState(0, d.vdate().v, _updatesQts, _updatesSeq);\n\t\t}\n\t} break;\n\n\tcase mtpc_updatesTooLong: {\n\t\tMTP_LOG(0, (\"getDifference \"\n\t\t\t\"{ good - updatesTooLong received }%1\"\n\t\t\t).arg(_session->mtp().isTestMode() ? \" TESTMODE\" : \"\"));\n\t\treturn getDifference();\n\t} break;\n\t}\n\tsession().data().sendHistoryChangeNotifications();\n}\n\nvoid Updates::feedUpdate(const MTPUpdate &update) {\n\tswitch (update.type()) {\n\n\t// New messages.\n\tcase mtpc_updateNewMessage: {\n\t\tauto &d = update.c_updateNewMessage();\n\t\tif (!requestingDifference()) {\n\t\t\tconst auto peerId = PeerFromMessage(d.vmessage());\n\t\t\tconst auto peer = session().data().peerLoaded(peerId);\n\t\t\tif (peerId && !peer) {\n\t\t\t\tMTP_LOG(0, (\"getDifference \"\n\t\t\t\t\t\"{ good - getting peer for updateNewMessage }%1\"\n\t\t\t\t).arg(_session->mtp().isTestMode() ? \" TESTMODE\" : \"\"));\n\t\t\t\treturn getDifference();\n\t\t\t}\n\t\t}\n\t\tupdateAndApply(d.vpts().v, d.vpts_count().v, update);\n\t} break;\n\n\tcase mtpc_updateNewChannelMessage: {\n\t\tauto &d = update.c_updateNewChannelMessage();\n\t\tauto channel = session().data().channelLoaded(peerToChannel(PeerFromMessage(d.vmessage())));\n\t\t{\n\t\t\t// Todo delete.\n\t\t\tconst auto messageId = IdFromMessage(d.vmessage());\n\t\t\tif (const auto history = channel ? session().data().historyLoaded(channel) : nullptr) {\n\t\t\t\tif (history->isUnknownMessageDeleted(messageId)) {\n\t\t\t\t\tLOG((\"Unknown message deleted detected for channel %1, message %2\")\n\t\t\t\t\t\t.arg(channel->id.value & PeerId::kChatTypeMask)\n\t\t\t\t\t\t.arg(messageId.bare));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!requestingDifference() && !channel) {\n\t\t\tMTP_LOG(0, (\"getDifference \"\n\t\t\t\t\"{ good - after not all data loaded in updateNewChannelMessage }%1\"\n\t\t\t\t).arg(_session->mtp().isTestMode() ? \" TESTMODE\" : \"\"));\n\t\t\tif (!_byMinChannelTimer.isActive()) { // getDifference after timeout\n\t\t\t\t_byMinChannelTimer.callOnce(PtsWaiter::kWaitForSkippedTimeout);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (channel && !_handlingChannelDifference) {\n\t\t\tif (channel->ptsRequesting()) { // skip global updates while getting channel difference\n\t\t\t\tMTP_LOG(0, (\"Skipping new channel message because getting the difference.\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tchannel->ptsUpdateAndApply(d.vpts().v, d.vpts_count().v, update);\n\t\t} else {\n\t\t\tapplyUpdateNoPtsCheck(update);\n\t\t}\n\t} break;\n\n\tcase mtpc_updateMessageID: {\n\t\tconst auto &d = update.c_updateMessageID();\n\t\tconst auto randomId = d.vrandom_id().v;\n\t\tif (const auto id = session().data().messageIdByRandomId(randomId)) {\n\t\t\tconst auto newId = d.vid().v;\n\t\t\tauto &owner = session().data();\n\t\t\tif (const auto local = owner.message(id)) {\n\t\t\t\tif (local->isScheduled()) {\n\t\t\t\t\tsession().scheduledMessages().apply(d, local);\n\t\t\t\t} else if (local->isBusinessShortcut()) {\n\t\t\t\t\tsession().data().shortcutMessages().apply(d, local);\n\t\t\t\t} else {\n\t\t\t\t\tconst auto existing = session().data().message(\n\t\t\t\t\t\tid.peer,\n\t\t\t\t\t\tnewId);\n\t\t\t\t\tif (existing && !local->mainView()) {\n\t\t\t\t\t\tconst auto history = local->history();\n\t\t\t\t\t\tlocal->destroy();\n\t\t\t\t\t\thistory->requestChatListMessage();\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\texisting->destroy();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Not the server-side date, but close enough.\n\t\t\t\t\t\t\tsession().topPeers().increment(\n\t\t\t\t\t\t\t\tlocal->history()->peer,\n\t\t\t\t\t\t\t\tlocal->date());\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlocal->setRealId(newId);\n\t\t\t\t\t\tif (const auto topic = local->topic()) {\n\t\t\t\t\t\t\ttopic->applyMaybeLast(local);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (const auto sublist = local->savedSublist()) {\n\t\t\t\t\t\t\tsublist->applyMaybeLast(local);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\towner.histories().checkTopicCreated(id, newId);\n\t\t\t}\n\t\t\tsession().data().unregisterMessageRandomId(randomId);\n\t\t} else {\n\t\t\tCore::App().calls().handleUpdate(&session(), update);\n\t\t}\n\t\tsession().data().unregisterMessageSentData(randomId);\n\t} break;\n\n\t// Message contents being read.\n\tcase mtpc_updateReadMessagesContents: {\n\t\tauto &d = update.c_updateReadMessagesContents();\n\t\tupdateAndApply(d.vpts().v, d.vpts_count().v, update);\n\t} break;\n\n\tcase mtpc_updateChannelReadMessagesContents: {\n\t\tauto &d = update.c_updateChannelReadMessagesContents();\n\t\tauto channel = session().data().channelLoaded(d.vchannel_id());\n\t\tif (!channel) {\n\t\t\tif (!_byMinChannelTimer.isActive()) {\n\t\t\t\t// getDifference after timeout.\n\t\t\t\t_byMinChannelTimer.callOnce(PtsWaiter::kWaitForSkippedTimeout);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tauto unknownReadIds = base::flat_set<MsgId>();\n\t\tfor (const auto &msgId : d.vmessages().v) {\n\t\t\tif (auto item = session().data().message(channel->id, msgId.v)) {\n\t\t\t\tif (item->isUnreadMedia() || item->isUnreadMention()) {\n\t\t\t\t\titem->markMediaAndMentionRead();\n\t\t\t\t\tsession().data().requestItemRepaint(item);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Perhaps it was an unread mention!\n\t\t\t\tunknownReadIds.insert(msgId.v);\n\t\t\t}\n\t\t}\n\t\tsession().api().unreadThings().mediaAndMentionsRead(\n\t\t\tunknownReadIds,\n\t\t\tchannel);\n\t} break;\n\n\t// Edited messages.\n\tcase mtpc_updateEditMessage: {\n\t\tauto &d = update.c_updateEditMessage();\n\t\tupdateAndApply(d.vpts().v, d.vpts_count().v, update);\n\t} break;\n\n\tcase mtpc_updateEditChannelMessage: {\n\t\tauto &d = update.c_updateEditChannelMessage();\n\t\tauto channel = session().data().channelLoaded(peerToChannel(PeerFromMessage(d.vmessage())));\n\n\t\tif (channel && !_handlingChannelDifference) {\n\t\t\tif (channel->ptsRequesting()) { // skip global updates while getting channel difference\n\t\t\t\tMTP_LOG(0, (\"Skipping channel message edit because getting the difference.\"));\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tchannel->ptsUpdateAndApply(d.vpts().v, d.vpts_count().v, update);\n\t\t\t}\n\t\t} else {\n\t\t\tapplyUpdateNoPtsCheck(update);\n\t\t}\n\t} break;\n\n\tcase mtpc_updatePinnedChannelMessages: {\n\t\tauto &d = update.c_updatePinnedChannelMessages();\n\t\tauto channel = session().data().channelLoaded(d.vchannel_id());\n\n\t\tif (channel && !_handlingChannelDifference) {\n\t\t\tif (channel->ptsRequesting()) { // skip global updates while getting channel difference\n\t\t\t\tMTP_LOG(0, (\"Skipping pinned channel messages because getting the difference.\"));\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tchannel->ptsUpdateAndApply(d.vpts().v, d.vpts_count().v, update);\n\t\t\t}\n\t\t} else {\n\t\t\tapplyUpdateNoPtsCheck(update);\n\t\t}\n\t} break;\n\n\tcase mtpc_updateMessageReactions: {\n\t\tconst auto &d = update.c_updateMessageReactions();\n\t\tconst auto peer = peerFromMTP(d.vpeer());\n\t\tif (const auto history = session().data().historyLoaded(peer)) {\n\t\t\tconst auto item = session().data().message(\n\t\t\t\tpeer,\n\t\t\t\td.vmsg_id().v);\n\t\t\tif (item) {\n\t\t\t\titem->updateReactions(&d.vreactions());\n\t\t\t} else {\n\t\t\t\tconst auto hasUnreadReaction = Data::Reactions::HasUnread(\n\t\t\t\t\td.vreactions());\n\t\t\t\tif (hasUnreadReaction || history->unreadReactions().has()) {\n\t\t\t\t\t// The unread reactions count could change.\n\t\t\t\t\thistory->owner().histories().requestDialogEntry(history);\n\t\t\t\t}\n\t\t\t\tif (hasUnreadReaction) {\n\t\t\t\t\thistory->unreadReactions().checkAdd(d.vmsg_id().v);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updateMessageExtendedMedia: {\n\t\tconst auto &d = update.c_updateMessageExtendedMedia();\n\t\tconst auto peerId = peerFromMTP(d.vpeer());\n\t\tconst auto msgId = d.vmsg_id().v;\n\t\tif (const auto item = session().data().message(peerId, msgId)) {\n\t\t\titem->applyEdition(d.vextended_media().v);\n\t\t}\n\t} break;\n\n\t// Messages being read.\n\tcase mtpc_updateReadHistoryInbox: {\n\t\tauto &d = update.c_updateReadHistoryInbox();\n\t\tupdateAndApply(d.vpts().v, d.vpts_count().v, update);\n\t} break;\n\n\tcase mtpc_updateReadHistoryOutbox: {\n\t\tauto &d = update.c_updateReadHistoryOutbox();\n\t\tupdateAndApply(d.vpts().v, d.vpts_count().v, update);\n\t} break;\n\n\tcase mtpc_updateReadChannelInbox: {\n\t\tconst auto &d = update.c_updateReadChannelInbox();\n\t\tconst auto peer = peerFromChannel(d.vchannel_id().v);\n\t\tif (const auto history = session().data().historyLoaded(peer)) {\n\t\t\thistory->applyInboxReadUpdate(\n\t\t\t\td.vfolder_id().value_or_empty(),\n\t\t\t\td.vmax_id().v,\n\t\t\t\td.vstill_unread_count().v,\n\t\t\t\td.vpts().v);\n\t\t}\n\t} break;\n\n\tcase mtpc_updateReadChannelOutbox: {\n\t\tconst auto &d = update.c_updateReadChannelOutbox();\n\t\tconst auto peer = peerFromChannel(d.vchannel_id().v);\n\t\tif (const auto history = session().data().historyLoaded(peer)) {\n\t\t\thistory->outboxRead(d.vmax_id().v);\n\t\t\tif (!requestingDifference()) {\n\t\t\t\tif (const auto user = history->peer->asUser()) {\n\t\t\t\t\tuser->madeAction(base::unixtime::now());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updateDialogUnreadMark: {\n\t\tconst auto &data = update.c_updateDialogUnreadMark();\n\t\tdata.vpeer().match(\n\t\t[&](const MTPDdialogPeer &dialog) {\n\t\t\tconst auto id = peerFromMTP(dialog.vpeer());\n\t\t\tif (const auto history = session().data().historyLoaded(id)) {\n\t\t\t\thistory->setUnreadMark(data.is_unread());\n\t\t\t}\n\t\t}, [](const MTPDdialogPeerFolder &dialog) {\n\t\t});\n\t} break;\n\n\tcase mtpc_updateFolderPeers: {\n\t\tconst auto &data = update.c_updateFolderPeers();\n\n\t\tupdateAndApply(data.vpts().v, data.vpts_count().v, update);\n\t} break;\n\n\tcase mtpc_updateDialogFilter:\n\tcase mtpc_updateDialogFilterOrder:\n\tcase mtpc_updateDialogFilters: {\n\t\tsession().data().chatsFilters().apply(update);\n\t} break;\n\n\t// Deleted messages.\n\tcase mtpc_updateDeleteMessages: {\n\t\tauto &d = update.c_updateDeleteMessages();\n\n\t\tupdateAndApply(d.vpts().v, d.vpts_count().v, update);\n\t} break;\n\n\tcase mtpc_updateDeleteChannelMessages: {\n\t\tauto &d = update.c_updateDeleteChannelMessages();\n\t\tauto channel = session().data().channelLoaded(d.vchannel_id());\n\n\t\tif (channel && !_handlingChannelDifference) {\n\t\t\tif (channel->ptsRequesting()) { // skip global updates while getting channel difference\n\t\t\t\tMTP_LOG(0, (\"Skipping delete channel messages because getting the difference.\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tchannel->ptsUpdateAndApply(d.vpts().v, d.vpts_count().v, update);\n\t\t} else {\n\t\t\tapplyUpdateNoPtsCheck(update);\n\t\t}\n\t} break;\n\n\tcase mtpc_updateNewScheduledMessage: {\n\t\tconst auto &d = update.c_updateNewScheduledMessage();\n\t\tsession().scheduledMessages().apply(d);\n\t} break;\n\n\tcase mtpc_updateDeleteScheduledMessages: {\n\t\tconst auto &d = update.c_updateDeleteScheduledMessages();\n\t\tsession().scheduledMessages().apply(d);\n\t} break;\n\n\tcase mtpc_updateQuickReplies: {\n\t\tconst auto &d = update.c_updateQuickReplies();\n\t\tsession().data().shortcutMessages().apply(d);\n\t} break;\n\n\tcase mtpc_updateNewQuickReply: {\n\t\tconst auto &d = update.c_updateNewQuickReply();\n\t\tsession().data().shortcutMessages().apply(d);\n\t} break;\n\n\tcase mtpc_updateDeleteQuickReply: {\n\t\tconst auto &d = update.c_updateDeleteQuickReply();\n\t\tsession().data().shortcutMessages().apply(d);\n\t} break;\n\n\tcase mtpc_updateQuickReplyMessage: {\n\t\tconst auto &d = update.c_updateQuickReplyMessage();\n\t\tsession().data().shortcutMessages().apply(d);\n\t} break;\n\n\tcase mtpc_updateDeleteQuickReplyMessages: {\n\t\tconst auto &d = update.c_updateDeleteQuickReplyMessages();\n\t\tsession().data().shortcutMessages().apply(d);\n\t} break;\n\n\tcase mtpc_updateWebPage: {\n\t\tauto &d = update.c_updateWebPage();\n\n\t\t// Update web page anyway.\n\t\tsession().data().processWebpage(d.vwebpage());\n\t\tsession().data().sendWebPageGamePollTodoListNotifications();\n\n\t\tupdateAndApply(d.vpts().v, d.vpts_count().v, update);\n\t} break;\n\n\tcase mtpc_updateChannelWebPage: {\n\t\tauto &d = update.c_updateChannelWebPage();\n\n\t\t// Update web page anyway.\n\t\tsession().data().processWebpage(d.vwebpage());\n\t\tsession().data().sendWebPageGamePollTodoListNotifications();\n\n\t\tauto channel = session().data().channelLoaded(d.vchannel_id());\n\t\tif (channel && !_handlingChannelDifference) {\n\t\t\tif (channel->ptsRequesting()) { // skip global updates while getting channel difference\n\t\t\t\tMTP_LOG(0, (\"Skipping channel web page update because getting the difference.\"));\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tchannel->ptsUpdateAndApply(d.vpts().v, d.vpts_count().v, update);\n\t\t\t}\n\t\t} else {\n\t\t\tapplyUpdateNoPtsCheck(update);\n\t\t}\n\t} break;\n\n\tcase mtpc_updateMessagePoll: {\n\t\tconst auto &d = update.c_updateMessagePoll();\n\t\tconst auto wasRecentVoters = session().data().pollRecentVoters(\n\t\t\td.vpoll_id().v);\n\t\tsession().data().applyUpdate(d);\n\t\tconst auto notifyItem = session().data().findItemForPoll(\n\t\t\td.vpoll_id().v);\n\t\tif (notifyItem) {\n\t\t\tCheckPollVoteNotificationSchedule(\n\t\t\t\tnotifyItem,\n\t\t\t\twasRecentVoters);\n\t\t}\n\t\tif (const auto tlPeer = d.vpeer()) {\n\t\t\tconst auto &results = d.vresults();\n\t\t\tconst auto hasUnread = results.match([](\n\t\t\t\t\tconst MTPDpollResults &data) {\n\t\t\t\treturn data.is_has_unread_votes();\n\t\t\t});\n\t\t\tconst auto isMin = results.match([](\n\t\t\t\t\tconst MTPDpollResults &data) {\n\t\t\t\treturn data.is_min();\n\t\t\t});\n\t\t\tconst auto peer = peerFromMTP(*tlPeer);\n\t\t\tconst auto msgId = d.vmsg_id()->v;\n\t\t\tif (const auto history = session().data().historyLoaded(peer)) {\n\t\t\t\tif (const auto item = session().data().message(\n\t\t\t\t\t\tpeer,\n\t\t\t\t\t\tmsgId)) {\n\t\t\t\t\tif (hasUnread) {\n\t\t\t\t\t\tif (!item->hasUnreadPollVote()) {\n\t\t\t\t\t\t\titem->setHasUnreadPollVote();\n\t\t\t\t\t\t\titem->addToUnreadThings(\n\t\t\t\t\t\t\t\tHistoryUnreadThings::AddType::New);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (!isMin && item->hasUnreadPollVote()) {\n\t\t\t\t\t\titem->markPollVotesRead();\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif (history->unreadPollVotes().has()) {\n\t\t\t\t\t\tif (hasUnread) {\n\t\t\t\t\t\t\thistory->unreadPollVotes().checkAdd(msgId);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\thistory->owner().histories().requestDialogEntry(\n\t\t\t\t\t\thistory);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updateUserTyping: {\n\t\tauto &d = update.c_updateUserTyping();\n\t\thandleSendActionUpdate(\n\t\t\tpeerFromUser(d.vuser_id()),\n\t\t\td.vtop_msg_id().value_or_empty(),\n\t\t\tpeerFromUser(d.vuser_id()),\n\t\t\td.vaction());\n\t} break;\n\n\tcase mtpc_updateChatUserTyping: {\n\t\tauto &d = update.c_updateChatUserTyping();\n\t\thandleSendActionUpdate(\n\t\t\tpeerFromChat(d.vchat_id()),\n\t\t\t0,\n\t\t\tpeerFromMTP(d.vfrom_id()),\n\t\t\td.vaction());\n\t} break;\n\n\tcase mtpc_updateChannelUserTyping: {\n\t\tconst auto &d = update.c_updateChannelUserTyping();\n\t\thandleSendActionUpdate(\n\t\t\tpeerFromChannel(d.vchannel_id()),\n\t\t\td.vtop_msg_id().value_or_empty(),\n\t\t\tpeerFromMTP(d.vfrom_id()),\n\t\t\td.vaction());\n\t} break;\n\n\tcase mtpc_updateChatParticipants: {\n\t\tsession().data().applyUpdate(update.c_updateChatParticipants());\n\t} break;\n\n\tcase mtpc_updateChatParticipantAdd: {\n\t\tsession().data().applyUpdate(update.c_updateChatParticipantAdd());\n\t} break;\n\n\tcase mtpc_updateChatParticipantDelete: {\n\t\tsession().data().applyUpdate(update.c_updateChatParticipantDelete());\n\t} break;\n\n\tcase mtpc_updateChatParticipantAdmin: {\n\t\tsession().data().applyUpdate(update.c_updateChatParticipantAdmin());\n\t} break;\n\n\tcase mtpc_updateChatParticipantRank: {\n\t\tsession().data().applyUpdate(update.c_updateChatParticipantRank());\n\t} break;\n\n\tcase mtpc_updateChatDefaultBannedRights: {\n\t\tsession().data().applyUpdate(update.c_updateChatDefaultBannedRights());\n\t} break;\n\n\tcase mtpc_updateUserStatus: {\n\t\tauto &d = update.c_updateUserStatus();\n\t\tif (const auto user = session().data().userLoaded(d.vuser_id())) {\n\t\t\tconst auto now = LastseenFromMTP(d.vstatus(), user->lastseen());\n\t\t\tif (user->updateLastseen(now)) {\n\t\t\t\tsession().changes().peerUpdated(\n\t\t\t\t\tuser,\n\t\t\t\t\tData::PeerUpdate::Flag::OnlineStatus);\n\t\t\t}\n\t\t}\n\t\tif (UserId(d.vuser_id()) == session().userId()) {\n\t\t\tif (d.vstatus().type() == mtpc_userStatusOffline\n\t\t\t\t|| d.vstatus().type() == mtpc_userStatusEmpty) {\n\t\t\t\tupdateOnline(Core::App().lastNonIdleTime(), true);\n\t\t\t\tif (d.vstatus().type() == mtpc_userStatusOffline) {\n\t\t\t\t\tcSetOtherOnline(\n\t\t\t\t\t\td.vstatus().c_userStatusOffline().vwas_online().v);\n\t\t\t\t}\n\t\t\t} else if (d.vstatus().type() == mtpc_userStatusOnline) {\n\t\t\t\tcSetOtherOnline(\n\t\t\t\t\td.vstatus().c_userStatusOnline().vexpires().v);\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updateUserName: {\n\t\tconst auto &d = update.c_updateUserName();\n\t\tif (const auto user = session().data().userLoaded(d.vuser_id())) {\n\t\t\tconst auto contact = user->isContact();\n\t\t\tconst auto first = contact\n\t\t\t\t? user->firstName\n\t\t\t\t: qs(d.vfirst_name());\n\t\t\tconst auto last = contact ? user->lastName : qs(d.vlast_name());\n\t\t\t// #TODO usernames\n\t\t\tconst auto username = d.vusernames().v.isEmpty()\n\t\t\t\t? QString()\n\t\t\t\t: qs(d.vusernames().v.front().data().vusername());\n\t\t\tuser->setName(\n\t\t\t\tTextUtilities::SingleLine(first),\n\t\t\t\tTextUtilities::SingleLine(last),\n\t\t\t\tuser->nameOrPhone,\n\t\t\t\tTextUtilities::SingleLine(username));\n\t\t\tuser->setUsernames(Api::Usernames::FromTL(d.vusernames()));\n\t\t}\n\t} break;\n\n\tcase mtpc_updateUser: {\n\t\tauto &d = update.c_updateUser();\n\t\tif (const auto user = session().data().userLoaded(d.vuser_id())) {\n\t\t\tif (user->wasFullUpdated()) {\n\t\t\t\tuser->updateFullForced();\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updatePeerSettings: {\n\t\tconst auto &d = update.c_updatePeerSettings();\n\t\tconst auto peerId = peerFromMTP(d.vpeer());\n\t\tif (const auto peer = session().data().peerLoaded(peerId)) {\n\t\t\tpeer->setBarSettings(d.vsettings());\n\t\t}\n\t} break;\n\n\tcase mtpc_updateNotifySettings: {\n\t\tauto &d = update.c_updateNotifySettings();\n\t\tsession().data().notifySettings().apply(\n\t\t\td.vpeer(),\n\t\t\td.vnotify_settings());\n\t} break;\n\n\tcase mtpc_updateDcOptions: {\n\t\tauto &d = update.c_updateDcOptions();\n\t\tsession().mtp().dcOptions().addFromList(d.vdc_options());\n\t} break;\n\n\tcase mtpc_updateConfig: {\n\t\tsession().mtp().requestConfig();\n\t\tsession().promoSuggestions().invalidate();\n\t} break;\n\n\tcase mtpc_updateUserPhone: {\n\t\tconst auto &d = update.c_updateUserPhone();\n\t\tif (const auto user = session().data().userLoaded(d.vuser_id())) {\n\t\t\tconst auto newPhone = qs(d.vphone());\n\t\t\tif (newPhone != user->phone()) {\n\t\t\t\tuser->setPhone(newPhone);\n\t\t\t\tuser->setName(\n\t\t\t\t\tuser->firstName,\n\t\t\t\t\tuser->lastName,\n\t\t\t\t\t((user->isContact()\n\t\t\t\t\t\t|| user->isServiceUser()\n\t\t\t\t\t\t|| user->isSelf()\n\t\t\t\t\t\t|| user->phone().isEmpty())\n\t\t\t\t\t\t? QString()\n\t\t\t\t\t\t: Ui::FormatPhone(user->phone())),\n\t\t\t\t\tuser->username());\n\n\t\t\t\tsession().changes().peerUpdated(\n\t\t\t\t\tuser,\n\t\t\t\t\tData::PeerUpdate::Flag::PhoneNumber);\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updatePeerHistoryTTL: {\n\t\tconst auto &d = update.c_updatePeerHistoryTTL();\n\t\tconst auto peerId = peerFromMTP(d.vpeer());\n\t\tif (const auto peer = session().data().peerLoaded(peerId)) {\n\t\t\tpeer->setMessagesTTL(d.vttl_period().value_or_empty());\n\t\t}\n\t} break;\n\n\tcase mtpc_updateNewEncryptedMessage: {\n\t} break;\n\n\tcase mtpc_updateEncryptedChatTyping: {\n\t} break;\n\n\tcase mtpc_updateEncryption: {\n\t} break;\n\n\tcase mtpc_updateEncryptedMessagesRead: {\n\t} break;\n\n\tcase mtpc_updatePhoneCall:\n\tcase mtpc_updatePhoneCallSignalingData:\n\tcase mtpc_updateGroupCallParticipants:\n\tcase mtpc_updateGroupCallChainBlocks:\n\tcase mtpc_updateGroupCallConnection:\n\tcase mtpc_updateGroupCall:\n\tcase mtpc_updateGroupCallMessage:\n\tcase mtpc_updateGroupCallEncryptedMessage:\n\tcase mtpc_updateDeleteGroupCallMessages: {\n\t\tCore::App().calls().handleUpdate(&session(), update);\n\t} break;\n\n\tcase mtpc_updatePeerBlocked: {\n\t\tconst auto &d = update.c_updatePeerBlocked();\n\t\tif (const auto peer = session().data().peerLoaded(peerFromMTP(d.vpeer_id()))) {\n\t\t\tpeer->setIsBlocked(d.is_blocked());\n\t\t}\n\t} break;\n\n\tcase mtpc_updatePeerWallpaper: {\n\t\tconst auto &d = update.c_updatePeerWallpaper();\n\t\tif (const auto peer = session().data().peerLoaded(peerFromMTP(d.vpeer()))) {\n\t\t\tif (const auto paper = d.vwallpaper()) {\n\t\t\t\tpeer->setWallPaper(\n\t\t\t\t\tData::WallPaper::Create(&session(), *paper),\n\t\t\t\t\td.is_wallpaper_overridden());\n\t\t\t} else {\n\t\t\t\tpeer->setWallPaper({});\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updateBotCommands: {\n\t\tconst auto &d = update.c_updateBotCommands();\n\t\tif (const auto peer = session().data().peerLoaded(peerFromMTP(d.vpeer()))) {\n\t\t\tconst auto botId = UserId(d.vbot_id().v);\n\t\t\tconst auto commands = Data::BotCommands{\n\t\t\t\t.userId = UserId(d.vbot_id().v),\n\t\t\t\t.commands = ranges::views::all(\n\t\t\t\t\td.vcommands().v\n\t\t\t\t) | ranges::views::transform(\n\t\t\t\t\tData::BotCommandFromTL\n\t\t\t\t) | ranges::to_vector,\n\t\t\t};\n\n\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\tif (user->isBot() && user->id == peerFromUser(botId)) {\n\t\t\t\t\tconst auto equal = ranges::equal(\n\t\t\t\t\t\tuser->botInfo->commands,\n\t\t\t\t\t\tcommands.commands);\n\t\t\t\t\tuser->botInfo->commands = commands.commands;\n\t\t\t\t\tif (!equal) {\n\t\t\t\t\t\tsession().data().botCommandsChanged(user);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (const auto chat = peer->asChat()) {\n\t\t\t\tchat->setBotCommands({ commands });\n\t\t\t} else if (const auto megagroup = peer->asMegagroup()) {\n\t\t\t\tif (megagroup->mgInfo->setBotCommands({ commands })) {\n\t\t\t\t\tsession().data().botCommandsChanged(megagroup);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updateAttachMenuBots: {\n\t\tsession().attachWebView().requestBots();\n\t} break;\n\n\tcase mtpc_updateWebViewResultSent: {\n\t\tconst auto &d = update.c_updateWebViewResultSent();\n\t\tsession().data().webViewResultSent({ .queryId = d.vquery_id().v });\n\t} break;\n\n\tcase mtpc_updateBotMenuButton: {\n\t\tconst auto &d = update.c_updateBotMenuButton();\n\t\tif (const auto bot = session().data().userLoaded(d.vbot_id())) {\n\t\t\tif (const auto info = bot->botInfo.get(); info && info->inited) {\n\t\t\t\tif (Data::ApplyBotMenuButton(info, &d.vbutton())) {\n\t\t\t\t\tsession().data().botCommandsChanged(bot);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updatePendingJoinRequests: {\n\t\tconst auto &d = update.c_updatePendingJoinRequests();\n\t\tif (const auto peer = session().data().peerLoaded(peerFromMTP(d.vpeer()))) {\n\t\t\tconst auto count = d.vrequests_pending().v;\n\t\t\tconst auto &requesters = d.vrecent_requesters().v;\n\t\t\tif (const auto chat = peer->asChat()) {\n\t\t\t\tchat->setPendingRequestsCount(count, requesters);\n\t\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\t\tchannel->setPendingRequestsCount(count, requesters);\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updateNewAuthorization: {\n\t\tsession().api().authorizations().apply(update);\n\t} break;\n\n\tcase mtpc_updateServiceNotification: {\n\t\tconst auto &d = update.c_updateServiceNotification();\n\t\tconst auto text = TextWithEntities {\n\t\t\tqs(d.vmessage()),\n\t\t\tApi::EntitiesFromMTP(&session(), d.ventities().v)\n\t\t};\n\t\tif (IsForceLogoutNotification(d)) {\n\t\t\tCore::App().forceLogOut(&session().account(), text);\n\t\t} else if (IsWithdrawalNotification(d)) {\n\t\t\treturn;\n\t\t} else if (d.is_popup()) {\n\t\t\tconst auto &windows = session().windows();\n\t\t\tif (!windows.empty()) {\n\t\t\t\twindows.front()->window().show(Ui::MakeInformBox(text));\n\t\t\t}\n\t\t} else {\n\t\t\tsession().data().serviceNotification(\n\t\t\t\ttext,\n\t\t\t\td.vmedia(),\n\t\t\t\td.is_invert_media());\n\t\t\tsession().api().authorizations().reload();\n\t\t}\n\t} break;\n\n\tcase mtpc_updatePrivacy: {\n\t\tauto &d = update.c_updatePrivacy();\n\t\tconst auto allChatsLoaded = [&](const MTPVector<MTPlong> &ids) {\n\t\t\tfor (const auto &chatId : ids.v) {\n\t\t\t\tif (!session().data().chatLoaded(chatId)\n\t\t\t\t\t&& !session().data().channelLoaded(chatId)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\t\tconst auto allLoaded = [&] {\n\t\t\tfor (const auto &rule : d.vrules().v) {\n\t\t\t\tconst auto loaded = rule.match([&](\n\t\t\t\t\tconst MTPDprivacyValueAllowChatParticipants & data) {\n\t\t\t\t\treturn allChatsLoaded(data.vchats());\n\t\t\t\t}, [&](const MTPDprivacyValueDisallowChatParticipants & data) {\n\t\t\t\t\treturn allChatsLoaded(data.vchats());\n\t\t\t\t}, [](auto &&) { return true; });\n\t\t\t\tif (!loaded) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\t\tsession().api().userPrivacy().apply(\n\t\t\td.vkey().type(),\n\t\t\td.vrules(),\n\t\t\tallLoaded());\n\t} break;\n\n\tcase mtpc_updatePinnedDialogs: {\n\t\tconst auto &d = update.c_updatePinnedDialogs();\n\t\tconst auto folderId = d.vfolder_id().value_or_empty();\n\t\tconst auto loaded = !folderId\n\t\t\t|| (session().data().folderLoaded(folderId) != nullptr);\n\t\tconst auto folder = folderId\n\t\t\t? session().data().folder(folderId).get()\n\t\t\t: nullptr;\n\t\tconst auto done = [&] {\n\t\t\tconst auto list = d.vorder();\n\t\t\tif (!list) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto &order = list->v;\n\t\t\tconst auto notLoaded = [&](const MTPDialogPeer &peer) {\n\t\t\t\treturn peer.match([&](const MTPDdialogPeer &data) {\n\t\t\t\t\treturn !session().data().historyLoaded(\n\t\t\t\t\t\tpeerFromMTP(data.vpeer()));\n\t\t\t\t}, [&](const MTPDdialogPeerFolder &data) {\n\t\t\t\t\tif (folderId) {\n\t\t\t\t\t\tLOG((\"API Error: \"\n\t\t\t\t\t\t\t\"updatePinnedDialogs has nested folders.\"));\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\treturn !session().data().folderLoaded(data.vfolder_id().v);\n\t\t\t\t});\n\t\t\t};\n\t\t\tif (!ranges::none_of(order, notLoaded)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tsession().data().applyPinnedChats(folder, order);\n\t\t\treturn true;\n\t\t}();\n\t\tif (!done) {\n\t\t\tsession().api().requestPinnedDialogs(folder);\n\t\t}\n\t\tif (!loaded) {\n\t\t\tsession().data().histories().requestDialogEntry(folder);\n\t\t}\n\t} break;\n\n\tcase mtpc_updateDialogPinned: {\n\t\tconst auto &d = update.c_updateDialogPinned();\n\t\tconst auto folderId = d.vfolder_id().value_or_empty();\n\t\tconst auto folder = folderId\n\t\t\t? session().data().folder(folderId).get()\n\t\t\t: nullptr;\n\t\tconst auto done = d.vpeer().match([&](const MTPDdialogPeer &data) {\n\t\t\tconst auto id = peerFromMTP(data.vpeer());\n\t\t\tif (const auto history = session().data().historyLoaded(id)) {\n\t\t\t\thistory->applyPinnedUpdate(d);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tDEBUG_LOG((\"API Error: \"\n\t\t\t\t\"pinned chat not loaded for peer %1, folder: %2\"\n\t\t\t\t).arg(id.value\n\t\t\t\t).arg(folderId\n\t\t\t\t));\n\t\t\treturn false;\n\t\t}, [&](const MTPDdialogPeerFolder &data) {\n\t\t\tif (folderId != 0) {\n\t\t\t\tDEBUG_LOG((\"API Error: Nested folders updateDialogPinned.\"));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto id = data.vfolder_id().v;\n\t\t\tif (const auto folder = session().data().folderLoaded(id)) {\n\t\t\t\tfolder->applyPinnedUpdate(d);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tDEBUG_LOG((\"API Error: \"\n\t\t\t\t\"pinned folder not loaded for folderId %1, folder: %2\"\n\t\t\t\t).arg(id\n\t\t\t\t).arg(folderId\n\t\t\t\t));\n\t\t\treturn false;\n\t\t});\n\t\tif (!done) {\n\t\t\tsession().api().requestPinnedDialogs(folder);\n\t\t}\n\t} break;\n\n\tcase mtpc_updatePinnedSavedDialogs: {\n\t\tsession().data().savedMessages().apply(\n\t\t\tupdate.c_updatePinnedSavedDialogs());\n\t} break;\n\n\tcase mtpc_updateSavedDialogPinned: {\n\t\tsession().data().savedMessages().apply(\n\t\t\tupdate.c_updateSavedDialogPinned());\n\t} break;\n\n\tcase mtpc_updateChannel: {\n\t\tauto &d = update.c_updateChannel();\n\t\tif (const auto channel = session().data().channelLoaded(d.vchannel_id())) {\n\t\t\tchannel->inviter = UserId(0);\n\t\t\tchannel->inviteViaRequest = false;\n\t\t\tif (channel->amIn()) {\n\t\t\t\tif (channel->isMegagroup()\n\t\t\t\t\t&& !channel->amCreator()\n\t\t\t\t\t&& !channel->hasAdminRights()) {\n\t\t\t\t\tchannel->updateFullForced();\n\t\t\t\t}\n\t\t\t\tconst auto history = channel->owner().history(channel);\n\t\t\t\thistory->requestChatListMessage();\n\t\t\t\tif (!history->folderKnown()\n\t\t\t\t\t|| (!history->unreadCountKnown()\n\t\t\t\t\t\t&& !history->isForum())) {\n\t\t\t\t\thistory->owner().histories().requestDialogEntry(history);\n\t\t\t\t}\n\t\t\t\tif (!channel->amCreator()) {\n\t\t\t\t\tsession().api().chatParticipants().requestSelf(channel);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updateChannelTooLong: {\n\t\tconst auto &d = update.c_updateChannelTooLong();\n\t\tif (const auto channel = session().data().channelLoaded(d.vchannel_id())) {\n\t\t\tconst auto pts = d.vpts();\n\t\t\tif (!pts || channel->pts() < pts->v) {\n\t\t\t\tgetChannelDifference(channel);\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updateChannelMessageViews: {\n\t\tconst auto &d = update.c_updateChannelMessageViews();\n\t\tconst auto peerId = peerFromChannel(d.vchannel_id());\n\t\tif (const auto item = session().data().message(peerId, d.vid().v)) {\n\t\t\tif (item->changeViewsCount(d.vviews().v)) {\n\t\t\t\tsession().data().notifyItemDataChange(item);\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updateChannelMessageForwards: {\n\t\tconst auto &d = update.c_updateChannelMessageForwards();\n\t\tconst auto peerId = peerFromChannel(d.vchannel_id());\n\t\tif (const auto item = session().data().message(peerId, d.vid().v)) {\n\t\t\titem->setForwardsCount(d.vforwards().v);\n\t\t}\n\t} break;\n\n\tcase mtpc_updateReadChannelDiscussionInbox: {\n\t\tconst auto &d = update.c_updateReadChannelDiscussionInbox();\n\t\tconst auto id = FullMsgId(\n\t\t\tpeerFromChannel(d.vchannel_id()),\n\t\t\td.vtop_msg_id().v);\n\t\tconst auto readTillId = d.vread_max_id().v;\n\t\tsession().data().updateRepliesReadTill({ id, readTillId, false });\n\t\tconst auto item = session().data().message(id);\n\t\tif (item) {\n\t\t\titem->setCommentsInboxReadTill(readTillId);\n\t\t\tif (const auto post = item->lookupDiscussionPostOriginal()) {\n\t\t\t\tpost->setCommentsInboxReadTill(readTillId);\n\t\t\t}\n\t\t}\n\t\tif (const auto broadcastId = d.vbroadcast_id()) {\n\t\t\tif (const auto post = session().data().message(\n\t\t\t\t\tpeerFromChannel(*broadcastId),\n\t\t\t\t\td.vbroadcast_post()->v)) {\n\t\t\t\tpost->setCommentsInboxReadTill(readTillId);\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updateReadChannelDiscussionOutbox: {\n\t\tconst auto &d = update.c_updateReadChannelDiscussionOutbox();\n\t\tconst auto id = FullMsgId(\n\t\t\tpeerFromChannel(d.vchannel_id()),\n\t\t\td.vtop_msg_id().v);\n\t\tconst auto readTillId = d.vread_max_id().v;\n\t\tsession().data().updateRepliesReadTill({ id, readTillId, true });\n\t} break;\n\n\tcase mtpc_updateReadMonoForumInbox: {\n\t\tconst auto &d = update.c_updateReadMonoForumInbox();\n\t\tconst auto parentChatId = ChannelId(d.vchannel_id());\n\t\tconst auto sublistPeerId = peerFromMTP(d.vsaved_peer_id());\n\t\tconst auto readTillId = d.vread_max_id().v;\n\t\tsession().data().updateSublistReadTill({\n\t\t\tparentChatId,\n\t\t\tsublistPeerId,\n\t\t\treadTillId,\n\t\t\tfalse,\n\t\t});\n\t} break;\n\n\tcase mtpc_updateReadMonoForumOutbox: {\n\t\tconst auto &d = update.c_updateReadMonoForumOutbox();\n\t\tconst auto parentChatId = ChannelId(d.vchannel_id());\n\t\tconst auto sublistPeerId = peerFromMTP(d.vsaved_peer_id());\n\t\tconst auto readTillId = d.vread_max_id().v;\n\t\tsession().data().updateSublistReadTill({\n\t\t\tparentChatId,\n\t\t\tsublistPeerId,\n\t\t\treadTillId,\n\t\t\ttrue,\n\t\t});\n\t} break;\n\n\tcase mtpc_updateChannelAvailableMessages: {\n\t\tauto &d = update.c_updateChannelAvailableMessages();\n\t\tif (const auto channel = session().data().channelLoaded(d.vchannel_id())) {\n\t\t\tchannel->setAvailableMinId(d.vavailable_min_id().v);\n\t\t\tif (const auto history = session().data().historyLoaded(channel)) {\n\t\t\t\thistory->clearUpTill(d.vavailable_min_id().v);\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updatePinnedForumTopic: {\n\t\tconst auto &d = update.c_updatePinnedForumTopic();\n\t\tconst auto peerId = peerFromMTP(d.vpeer());\n\t\tif (const auto peer = session().data().peerLoaded(peerId)) {\n\t\t\tconst auto rootId = d.vtopic_id().v;\n\t\t\tif (const auto topic = peer->forumTopicFor(rootId)) {\n\t\t\t\tsession().data().setChatPinned(topic, 0, d.is_pinned());\n\t\t\t} else if (const auto forum = peer->forum()) {\n\t\t\t\tforum->requestTopic(rootId);\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updatePinnedForumTopics: {\n\t\tconst auto &d = update.c_updatePinnedForumTopics();\n\t\tconst auto peerId = peerFromMTP(d.vpeer());\n\t\tif (const auto peer = session().data().peerLoaded(peerId)) {\n\t\t\tif (const auto forum = peer->forum()) {\n\t\t\t\tconst auto done = [&] {\n\t\t\t\t\tconst auto list = d.vorder();\n\t\t\t\t\tif (!list) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\tconst auto &order = list->v;\n\t\t\t\t\tconst auto notLoaded = [&](const MTPint &topicId) {\n\t\t\t\t\t\treturn !forum->topicFor(topicId.v);\n\t\t\t\t\t};\n\t\t\t\t\tif (!ranges::none_of(order, notLoaded)) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\tsession().data().applyPinnedTopics(forum, order);\n\t\t\t\t\treturn true;\n\t\t\t\t}();\n\t\t\t\tif (!done) {\n\t\t\t\t\tforum->reloadTopics();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase mtpc_updateChannelViewForumAsMessages: {\n\t\tconst auto &d = update.c_updateChannelViewForumAsMessages();\n\t\tconst auto id = ChannelId(d.vchannel_id());\n\t\tif (const auto channel = session().data().channelLoaded(id)) {\n\t\t\tchannel->setViewAsMessagesFlag(mtpIsTrue(d.venabled()));\n\t\t}\n\t} break;\n\n\t// Pinned message.\n\tcase mtpc_updatePinnedMessages: {\n\t\tconst auto &d = update.c_updatePinnedMessages();\n\t\tupdateAndApply(d.vpts().v, d.vpts_count().v, update);\n\t} break;\n\n\t////// Cloud sticker sets\n\tcase mtpc_updateNewStickerSet: {\n\t\tconst auto &d = update.c_updateNewStickerSet();\n\t\td.vstickerset().match([&](const MTPDmessages_stickerSet &data) {\n\t\t\tsession().data().stickers().newSetReceived(data);\n\t\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t\t});\n\t} break;\n\n\tcase mtpc_updateStickerSetsOrder: {\n\t\tauto &d = update.c_updateStickerSetsOrder();\n\t\tauto &stickers = session().data().stickers();\n\t\tconst auto isEmoji = d.is_emojis();\n\t\tconst auto isMasks = d.is_masks();\n\t\tconst auto &order = d.vorder().v;\n\t\tconst auto &sets = stickers.sets();\n\t\tData::StickersSetsOrder result;\n\t\tfor (const auto &item : order) {\n\t\t\tif (sets.find(item.v) == sets.cend()) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tresult.push_back(item.v);\n\t\t}\n\t\tconst auto localSize = isEmoji\n\t\t\t? stickers.emojiSetsOrder().size()\n\t\t\t: isMasks\n\t\t\t? stickers.maskSetsOrder().size()\n\t\t\t: stickers.setsOrder().size();\n\t\tif ((result.size() != localSize) || (result.size() != order.size())) {\n\t\t\tif (isEmoji) {\n\t\t\t\tstickers.setLastEmojiUpdate(0);\n\t\t\t\tsession().api().updateCustomEmoji();\n\t\t\t} else if (isMasks) {\n\t\t\t\tstickers.setLastMasksUpdate(0);\n\t\t\t\tsession().api().updateMasks();\n\t\t\t} else {\n\t\t\t\tstickers.setLastUpdate(0);\n\t\t\t\tsession().api().updateStickers();\n\t\t\t}\n\t\t} else {\n\t\t\tif (isEmoji) {\n\t\t\t\tstickers.emojiSetsOrderRef() = std::move(result);\n\t\t\t\tsession().local().writeInstalledCustomEmoji();\n\t\t\t} else if (isMasks) {\n\t\t\t\tstickers.maskSetsOrderRef() = std::move(result);\n\t\t\t\tsession().local().writeInstalledMasks();\n\t\t\t} else {\n\t\t\t\tstickers.setsOrderRef() = std::move(result);\n\t\t\t\tsession().local().writeInstalledStickers();\n\t\t\t}\n\t\t\tstickers.notifyUpdated(isEmoji\n\t\t\t\t? Data::StickersType::Emoji\n\t\t\t\t: isMasks\n\t\t\t\t? Data::StickersType::Masks\n\t\t\t\t: Data::StickersType::Stickers);\n\t\t}\n\t} break;\n\n\tcase mtpc_updateMoveStickerSetToTop: {\n\t\tconst auto &d = update.c_updateMoveStickerSetToTop();\n\t\tauto &stickers = session().data().stickers();\n\t\tconst auto isEmoji = d.is_emojis();\n\t\tconst auto setId = d.vstickerset().v;\n\t\tauto &order = isEmoji\n\t\t\t? stickers.emojiSetsOrderRef()\n\t\t\t: stickers.setsOrderRef();\n\t\tconst auto i = ranges::find(order, setId);\n\t\tif (i == order.end()) {\n\t\t\tif (isEmoji) {\n\t\t\t\tstickers.setLastEmojiUpdate(0);\n\t\t\t\tsession().api().updateCustomEmoji();\n\t\t\t} else {\n\t\t\t\tstickers.setLastUpdate(0);\n\t\t\t\tsession().api().updateStickers();\n\t\t\t}\n\t\t} else if (i != order.begin()) {\n\t\t\tstd::rotate(order.begin(), i, i + 1);\n\t\t\tif (isEmoji) {\n\t\t\t\tsession().local().writeInstalledCustomEmoji();\n\t\t\t} else {\n\t\t\t\tsession().local().writeInstalledStickers();\n\t\t\t}\n\t\t\tstickers.notifyUpdated(isEmoji\n\t\t\t\t? Data::StickersType::Emoji\n\t\t\t\t: Data::StickersType::Stickers);\n\t\t}\n\t} break;\n\n\tcase mtpc_updateStickerSets: {\n\t\tconst auto &d = update.c_updateStickerSets();\n\t\tif (d.is_emojis()) {\n\t\t\tsession().data().stickers().setLastEmojiUpdate(0);\n\t\t\tsession().api().updateCustomEmoji();\n\t\t} else if (d.is_masks()) {\n\t\t\tsession().data().stickers().setLastMasksUpdate(0);\n\t\t\tsession().api().updateMasks();\n\t\t} else {\n\t\t\tsession().data().stickers().setLastUpdate(0);\n\t\t\tsession().api().updateStickers();\n\t\t}\n\t} break;\n\n\tcase mtpc_updateRecentStickers: {\n\t\tsession().data().stickers().setLastRecentUpdate(0);\n\t\tsession().api().updateStickers();\n\t} break;\n\n\tcase mtpc_updateFavedStickers: {\n\t\tsession().data().stickers().setLastFavedUpdate(0);\n\t\tsession().api().updateStickers();\n\t} break;\n\n\tcase mtpc_updateReadFeaturedStickers: {\n\t\t// We read some of the featured stickers, perhaps not all of them.\n\t\t// Here we don't know what featured sticker sets were read, so we\n\t\t// request all of them once again.\n\t\tsession().data().stickers().setLastFeaturedUpdate(0);\n\t\tsession().api().updateStickers();\n\t} break;\n\n\tcase mtpc_updateReadFeaturedEmojiStickers: {\n\t\t// We don't track read status of them for now.\n\t} break;\n\n\tcase mtpc_updateUserEmojiStatus: {\n\t\tconst auto &d = update.c_updateUserEmojiStatus();\n\t\tif (const auto user = session().data().userLoaded(d.vuser_id())) {\n\t\t\tuser->setEmojiStatus(d.vemoji_status());\n\t\t}\n\t} break;\n\n\tcase mtpc_updateRecentEmojiStatuses: {\n\t\tsession().data().emojiStatuses().refreshRecentDelayed();\n\t} break;\n\n\tcase mtpc_updateRecentReactions: {\n\t\tsession().data().reactions().refreshRecentDelayed();\n\t} break;\n\n\tcase mtpc_updateSavedReactionTags: {\n\t\tsession().data().reactions().refreshMyTagsDelayed();\n\t} break;\n\n\t////// Cloud saved GIFs\n\tcase mtpc_updateSavedGifs: {\n\t\tsession().data().stickers().setLastSavedGifsUpdate(0);\n\t\tsession().api().updateSavedGifs();\n\t} break;\n\n\t////// Cloud drafts\n\tcase mtpc_updateDraftMessage: {\n\t\tconst auto &data = update.c_updateDraftMessage();\n\t\tconst auto peerId = peerFromMTP(data.vpeer());\n\t\tconst auto topicRootId = data.vtop_msg_id().value_or_empty();\n\t\tconst auto monoforumPeerId = data.vsaved_peer_id()\n\t\t\t? peerFromMTP(*data.vsaved_peer_id())\n\t\t\t: PeerId();\n\t\tdata.vdraft().match([&](const MTPDdraftMessage &data) {\n\t\t\tData::ApplyPeerCloudDraft(\n\t\t\t\t&session(),\n\t\t\t\tpeerId,\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tdata);\n\t\t}, [&](const MTPDdraftMessageEmpty &data) {\n\t\t\tData::ClearPeerCloudDraft(\n\t\t\t\t&session(),\n\t\t\t\tpeerId,\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tdata.vdate().value_or_empty());\n\t\t});\n\t} break;\n\n\t////// Cloud langpacks\n\tcase mtpc_updateLangPack: {\n\t\tconst auto &data = update.c_updateLangPack();\n\t\tLang::CurrentCloudManager().applyLangPackDifference(data.vdifference());\n\t} break;\n\n\tcase mtpc_updateLangPackTooLong: {\n\t\tconst auto &data = update.c_updateLangPackTooLong();\n\t\tconst auto code = qs(data.vlang_code());\n\t\tif (!code.isEmpty()) {\n\t\t\tLang::CurrentCloudManager().requestLangPackDifference(code);\n\t\t}\n\t} break;\n\n\t////// Cloud themes\n\tcase mtpc_updateTheme: {\n\t\tconst auto &data = update.c_updateTheme();\n\t\tsession().data().cloudThemes().applyUpdate(data.vtheme());\n\t} break;\n\n\tcase mtpc_updateSavedRingtones: {\n\t\tsession().api().ringtones().applyUpdate();\n\t} break;\n\n\tcase mtpc_updateTranscribedAudio: {\n\t\tconst auto &data = update.c_updateTranscribedAudio();\n\t\t_session->api().transcribes().apply(data);\n\t} break;\n\n\tcase mtpc_updateStory: {\n\t\t_session->data().stories().apply(update.c_updateStory());\n\t} break;\n\n\tcase mtpc_updateReadStories: {\n\t\t_session->data().stories().apply(update.c_updateReadStories());\n\t} break;\n\n\tcase mtpc_updateStoriesStealthMode: {\n\t\tconst auto &data = update.c_updateStoriesStealthMode();\n\t\t_session->data().stories().apply(data.vstealth_mode());\n\t} break;\n\n\tcase mtpc_updateStarsBalance: {\n\t\tconst auto &data = update.c_updateStarsBalance();\n\t\t_session->credits().apply(data);\n\t} break;\n\n\tcase mtpc_updatePaidReactionPrivacy: {\n\t\tconst auto &data = update.c_updatePaidReactionPrivacy();\n\t\t_session->api().globalPrivacy().updatePaidReactionShownPeer(\n\t\t\tApi::ParsePaidReactionShownPeer(_session, data.vprivate()));\n\t} break;\n\n\tcase mtpc_updateStarGiftAuctionState: {\n\t\tconst auto &data = update.c_updateStarGiftAuctionState();\n\t\t_session->giftAuctions().apply(data);\n\t} break;\n\n\tcase mtpc_updateStarGiftAuctionUserState: {\n\t\tconst auto &data = update.c_updateStarGiftAuctionUserState();\n\t\t_session->giftAuctions().apply(data);\n\t} break;\n\n\tcase mtpc_updateEmojiGameInfo: {\n\t\tconst auto &data = update.c_updateEmojiGameInfo();\n\t\t_session->diceStickersPacks().apply(data);\n\t} break;\n\n\t}\n}\n\nbool IsWithdrawalNotification(const MTPDupdateServiceNotification &data) {\n\treturn qs(data.vtype()).startsWith(u\"API_WITHDRAWAL_FEATURE_DISABLED_\"_q);\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_updates.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_pts_waiter.h\"\n#include \"base/timer.h\"\n\nclass ApiWrap;\nclass History;\n\nnamespace MTP {\nclass Error;\n} // namespace MTP\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace ChatHelpers {\nstruct EmojiInteractionsBunch;\n} // namespace ChatHelpers\n\nnamespace Api {\n\nclass Updates final {\npublic:\n\texplicit Updates(not_null<Main::Session*> session);\n\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] ApiWrap &api() const;\n\n\tvoid applyUpdates(\n\t\tconst MTPUpdates &updates,\n\t\tuint64 sentMessageRandomId = 0);\n\tvoid applyUpdatesNoPtsCheck(const MTPUpdates &updates);\n\tvoid applyUpdateNoPtsCheck(const MTPUpdate &update);\n\n\tvoid checkForSentToScheduled(const MTPUpdates &updates);\n\n\t[[nodiscard]] int32 pts() const;\n\n\tvoid updateOnline(crl::time lastNonIdleTime = 0);\n\t[[nodiscard]] bool isIdle() const;\n\t[[nodiscard]] rpl::producer<bool> isIdleValue() const;\n\tvoid checkIdleFinish(crl::time lastNonIdleTime = 0);\n\tbool lastWasOnline() const;\n\tcrl::time lastSetOnline() const;\n\tbool isQuitPrevent();\n\n\tbool updateAndApply(int32 pts, int32 ptsCount, const MTPUpdates &updates);\n\tbool updateAndApply(int32 pts, int32 ptsCount, const MTPUpdate &update);\n\tbool updateAndApply(int32 pts, int32 ptsCount);\n\n\tvoid checkLastUpdate(bool afterSleep);\n\n\t// ms <= 0 - stop timer\n\tvoid ptsWaiterStartTimerFor(ChannelData *channel, crl::time ms);\n\n\tvoid getDifference();\n\tvoid requestChannelRangeDifference(not_null<History*> history);\n\n\tvoid addActiveChat(rpl::producer<PeerData*> chat);\n\t[[nodiscard]] bool inActiveChats(not_null<PeerData*> peer) const;\n\nprivate:\n\tenum class ChannelDifferenceRequest {\n\t\tUnknown,\n\t\tPtsGapOrShortPoll,\n\t\tAfterFail,\n\t};\n\n\tenum class SkipUpdatePolicy {\n\t\tSkipNone,\n\t\tSkipMessageIds,\n\t\tSkipExceptGroupCallParticipants,\n\t};\n\n\tstruct ActiveChatTracker {\n\t\tPeerData *peer = nullptr;\n\t\trpl::lifetime lifetime;\n\t};\n\n\tvoid channelRangeDifferenceSend(\n\t\tnot_null<ChannelData*> channel,\n\t\tMsgRange range,\n\t\tint32 pts);\n\tvoid channelRangeDifferenceDone(\n\t\tnot_null<ChannelData*> channel,\n\t\tMsgRange range,\n\t\tconst MTPupdates_ChannelDifference &result);\n\n\tvoid updateOnline(crl::time lastNonIdleTime, bool gotOtherOffline);\n\tvoid sendPing();\n\tvoid getDifferenceByPts();\n\tvoid getDifferenceAfterFail();\n\n\t[[nodiscard]] bool requestingDifference() const {\n\t\treturn _ptsWaiter.requesting();\n\t}\n\tvoid getChannelDifference(\n\t\tnot_null<ChannelData*> channel,\n\t\tChannelDifferenceRequest from = ChannelDifferenceRequest::Unknown);\n\tvoid differenceDone(const MTPupdates_Difference &result);\n\tvoid differenceFail(const MTP::Error &error);\n\tvoid feedDifference(\n\t\tconst MTPVector<MTPUser> &users,\n\t\tconst MTPVector<MTPChat> &chats,\n\t\tconst MTPVector<MTPMessage> &msgs,\n\t\tconst MTPVector<MTPUpdate> &other);\n\tvoid stateDone(const MTPupdates_State &state);\n\tvoid setState(int32 pts, int32 date, int32 qts, int32 seq);\n\tvoid channelDifferenceDone(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst MTPupdates_ChannelDifference &diff);\n\tvoid channelDifferenceFail(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst MTP::Error &error);\n\tvoid failDifferenceStartTimerFor(ChannelData *channel);\n\tvoid feedChannelDifference(const MTPDupdates_channelDifference &data);\n\n\tvoid mtpUpdateReceived(const MTPUpdates &updates);\n\tvoid mtpNewSessionCreated();\n\tvoid feedUpdateVector(\n\t\tconst MTPVector<MTPUpdate> &updates,\n\t\tSkipUpdatePolicy policy = SkipUpdatePolicy::SkipNone);\n\t// Doesn't call sendHistoryChangeNotifications itself.\n\tvoid feedMessageIds(const MTPVector<MTPUpdate> &updates);\n\t// Doesn't call sendHistoryChangeNotifications itself.\n\tvoid feedUpdate(const MTPUpdate &update);\n\n\tvoid applyConvertToScheduledOnSend(\n\t\tconst MTPVector<MTPUpdate> &other,\n\t\tbool skipScheduledCheck = false);\n\tvoid applyGroupCallParticipantUpdates(const MTPUpdates &updates);\n\n\tbool whenGetDiffChanged(\n\t\tChannelData *channel,\n\t\tint32 ms,\n\t\tbase::flat_map<not_null<ChannelData*>, crl::time> &whenMap,\n\t\tcrl::time &curTime);\n\n\tvoid handleSendActionUpdate(\n\t\tPeerId peerId,\n\t\tMsgId rootId,\n\t\tPeerId fromId,\n\t\tconst MTPSendMessageAction &action);\n\tvoid handleEmojiInteraction(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPDsendMessageEmojiInteraction &data);\n\tvoid handleSpeakingInCall(\n\t\tnot_null<PeerData*> peer,\n\t\tPeerId participantPeerId,\n\t\tPeerData *participantPeerLoaded);\n\tvoid handleEmojiInteraction(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId messageId,\n\t\tconst QString &emoticon,\n\t\tChatHelpers::EmojiInteractionsBunch bunch);\n\tvoid handleEmojiInteraction(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &emoticon);\n\n\tconst not_null<Main::Session*> _session;\n\n\tint32 _updatesDate = 0;\n\tint32 _updatesQts = -1;\n\tint32 _updatesSeq = 0;\n\tbase::Timer _noUpdatesTimer;\n\tbase::Timer _onlineTimer;\n\n\tPtsWaiter _ptsWaiter;\n\n\tbase::flat_map<not_null<ChannelData*>, crl::time> _whenGetDiffByPts;\n\tbase::flat_map<not_null<ChannelData*>, crl::time> _whenGetDiffAfterFail;\n\tcrl::time _getDifferenceTimeByPts = 0;\n\tcrl::time _getDifferenceTimeAfterFail = 0;\n\n\tbase::Timer _byPtsTimer;\n\n\tbase::flat_map<int32, MTPUpdates> _bySeqUpdates;\n\tbase::Timer _bySeqTimer;\n\n\tbase::Timer _byMinChannelTimer;\n\n\t// growing timeout for getDifference calls, if it fails\n\tcrl::time _failDifferenceTimeout = 1;\n\t// growing timeout for getChannelDifference calls, if it fails\n\tbase::flat_map<\n\t\tnot_null<ChannelData*>,\n\t\tcrl::time> _channelFailDifferenceTimeout;\n\tbase::Timer _failDifferenceTimer;\n\n\tbase::flat_map<\n\t\tnot_null<ChannelData*>,\n\t\tmtpRequestId> _rangeDifferenceRequests;\n\n\tcrl::time _lastUpdateTime = 0;\n\tbool _handlingChannelDifference = false;\n\n\tbase::flat_map<int, ActiveChatTracker> _activeChats;\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tbase::flat_map<PeerId, crl::time>> _pendingSpeakingCallParticipants;\n\n\tmtpRequestId _onlineRequest = 0;\n\tbase::Timer _idleFinishTimer;\n\tcrl::time _lastSetOnline = 0;\n\tbool _lastWasOnline = false;\n\trpl::variable<bool> _isIdle = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] bool IsWithdrawalNotification(\n\tconst MTPDupdateServiceNotification &);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_user_names.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_user_names.h\"\n\n#include \"apiwrap.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"main/main_session.h\"\n\nnamespace Api {\nnamespace {\n\n[[nodiscard]] Data::Username UsernameFromTL(const MTPUsername &username) {\n\treturn {\n\t\t.username = qs(username.data().vusername()),\n\t\t.active = username.data().is_active(),\n\t\t.editable = username.data().is_editable(),\n\t};\n}\n\n[[nodiscard]] std::optional<MTPInputUser> BotUserInput(\n\t\tnot_null<PeerData*> peer) {\n\tconst auto user = peer->asUser();\n\treturn (user && user->botInfo && user->botInfo->canEditInformation)\n\t\t? std::make_optional<MTPInputUser>(user->inputUser())\n\t\t: std::nullopt;\n}\n\n} // namespace\n\nUsernames::Usernames(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance()) {\n}\n\nrpl::producer<Data::Usernames> Usernames::loadUsernames(\n\t\tnot_null<PeerData*> peer) const {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tconst auto push = [consumer](\n\t\t\t\tconst auto &usernames,\n\t\t\t\tconst auto &username) {\n\t\t\tif (usernames) {\n\t\t\t\tif (usernames->v.empty()) {\n\t\t\t\t\t// Probably will never happen.\n\t\t\t\t\tconsumer.put_next({});\n\t\t\t\t} else {\n\t\t\t\t\tauto parsed = FromTL(*usernames);\n\t\t\t\t\tif ((parsed.size() == 1)\n\t\t\t\t\t\t&& username\n\t\t\t\t\t\t&& (parsed.front().username == qs(*username))) {\n\t\t\t\t\t\t// Probably will never happen.\n\t\t\t\t\t\tconsumer.put_next({});\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconsumer.put_next(std::move(parsed));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconsumer.put_next({});\n\t\t\t}\n\t\t};\n\n\t\tconst auto requestUser = [&](const MTPInputUser &data) {\n\t\t\t_session->api().request(MTPusers_GetUsers(\n\t\t\t\tMTP_vector<MTPInputUser>(1, data)\n\t\t\t)).done([=](const MTPVector<MTPUser> &result) {\n\t\t\t\tresult.v.front().match([&](const MTPDuser &data) {\n\t\t\t\t\tpush(data.vusernames(), data.vusername());\n\t\t\t\t\tconsumer.put_done();\n\t\t\t\t}, [&](const MTPDuserEmpty&) {\n\t\t\t\t\tconsumer.put_next({});\n\t\t\t\t\tconsumer.put_done();\n\t\t\t\t});\n\t\t\t}).send();\n\t\t};\n\t\tconst auto requestChannel = [&](const MTPInputChannel &data) {\n\t\t\t_session->api().request(MTPchannels_GetChannels(\n\t\t\t\tMTP_vector<MTPInputChannel>(1, data)\n\t\t\t)).done([=](const MTPmessages_Chats &result) {\n\t\t\t\tresult.match([&](const auto &data) {\n\t\t\t\t\tdata.vchats().v.front().match([&](const MTPDchannel &c) {\n\t\t\t\t\t\tpush(c.vusernames(), c.vusername());\n\t\t\t\t\t\tconsumer.put_done();\n\t\t\t\t\t}, [&](auto &&) {\n\t\t\t\t\t\tconsumer.put_next({});\n\t\t\t\t\t\tconsumer.put_done();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}).send();\n\t\t};\n\t\tif (peer->isSelf()) {\n\t\t\trequestUser(MTP_inputUserSelf());\n\t\t} else if (const auto user = peer->asUser()) {\n\t\t\trequestUser(user->inputUser());\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\trequestChannel(channel->inputChannel());\n\t\t}\n\t\treturn lifetime;\n\t};\n}\n\nrpl::producer<rpl::no_value, Usernames::Error> Usernames::toggle(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &username,\n\t\tbool active) {\n\tconst auto peerId = peer->id;\n\tconst auto it = _toggleRequests.find(peerId);\n\tconst auto found = (it != end(_toggleRequests));\n\tauto &entry = (!found\n\t\t? _toggleRequests.emplace(\n\t\t\tpeerId,\n\t\t\tEntry{ .usernames = { username } }).first\n\t\t: it)->second;\n\tif (ranges::contains(entry.usernames, username)) {\n\t\tif (found) {\n\t\t\treturn entry.done.events();\n\t\t}\n\t} else {\n\t\tentry.usernames.push_back(username);\n\t}\n\n\tconst auto pop = [=](Error error) {\n\t\tconst auto it = _toggleRequests.find(peerId);\n\t\tif (it != end(_toggleRequests)) {\n\t\t\tauto &list = it->second.usernames;\n\t\t\tlist.erase(ranges::remove(list, username), end(list));\n\t\t\tif (list.empty()) {\n\t\t\t\tif (error == Error::Unknown) {\n\t\t\t\t\tit->second.done.fire_done();\n\t\t\t\t} else {\n\t\t\t\t\tit->second.done.fire_error_copy(error);\n\t\t\t\t}\n\t\t\t\t_toggleRequests.remove(peerId);\n\t\t\t}\n\t\t}\n\t};\n\n\tconst auto done = [=] {\n\t\tpop(Error::Unknown);\n\t};\n\tconst auto fail = [=](const MTP::Error &error) {\n\t\tconst auto type = error.type();\n\t\tif (type == u\"USERNAMES_ACTIVE_TOO_MUCH\"_q) {\n\t\t\tpop(Error::TooMuch);\n\t\t} else if (type.startsWith(u\"FLOOD_WAIT_\"_q)) {\n\t\t\tpop(Error::Flood);\n\t\t} else {\n\t\t\tpop(Error::Unknown);\n\t\t}\n\t};\n\n\tif (peer->isSelf()) {\n\t\t_api.request(MTPaccount_ToggleUsername(\n\t\t\tMTP_string(username),\n\t\t\tMTP_bool(active)\n\t\t)).done(done).fail(fail).handleFloodErrors().send();\n\t} else if (const auto channel = peer->asChannel()) {\n\t\t_api.request(MTPchannels_ToggleUsername(\n\t\t\tchannel->inputChannel(),\n\t\t\tMTP_string(username),\n\t\t\tMTP_bool(active)\n\t\t)).done(done).fail(fail).handleFloodErrors().send();\n\t} else if (const auto botUserInput = BotUserInput(peer)) {\n\t\t_api.request(MTPbots_ToggleUsername(\n\t\t\t*botUserInput,\n\t\t\tMTP_string(username),\n\t\t\tMTP_bool(active)\n\t\t)).done(done).fail(fail).handleFloodErrors().send();\n\t} else {\n\t\treturn rpl::never<rpl::no_value, Error>();\n\t}\n\treturn entry.done.events();\n}\n\nrpl::producer<> Usernames::reorder(\n\t\tnot_null<PeerData*> peer,\n\t\tconst std::vector<QString> &usernames) {\n\tconst auto peerId = peer->id;\n\tconst auto it = _reorderRequests.find(peerId);\n\tif (it != end(_reorderRequests)) {\n\t\t_api.request(it->second).cancel();\n\t\t_reorderRequests.erase(peerId);\n\t}\n\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tauto tlUsernames = ranges::views::all(\n\t\t\tusernames\n\t\t) | ranges::views::transform([](const QString &username) {\n\t\t\treturn MTP_string(username);\n\t\t}) | ranges::to<QVector<MTPstring>>;\n\n\t\tconst auto finish = [=] {\n\t\t\tif (_reorderRequests.contains(peerId)) {\n\t\t\t\t_reorderRequests.erase(peerId);\n\t\t\t}\n\t\t\tconsumer.put_done();\n\t\t};\n\t\tif (usernames.empty()) {\n\t\t\tcrl::on_main([=] { consumer.put_done(); });\n\t\t\treturn lifetime;\n\t\t}\n\n\t\tif (peer->isSelf()) {\n\t\t\tconst auto requestId = _api.request(MTPaccount_ReorderUsernames(\n\t\t\t\tMTP_vector<MTPstring>(std::move(tlUsernames))\n\t\t\t)).done(finish).fail(finish).send();\n\t\t\t_reorderRequests.emplace(peerId, requestId);\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\tconst auto requestId = _api.request(MTPchannels_ReorderUsernames(\n\t\t\t\tchannel->inputChannel(),\n\t\t\t\tMTP_vector<MTPstring>(std::move(tlUsernames))\n\t\t\t)).done(finish).fail(finish).send();\n\t\t\t_reorderRequests.emplace(peerId, requestId);\n\t\t} else if (const auto botUserInput = BotUserInput(peer)) {\n\t\t\tconst auto requestId = _api.request(MTPbots_ReorderUsernames(\n\t\t\t\t*botUserInput,\n\t\t\t\tMTP_vector<MTPstring>(std::move(tlUsernames))\n\t\t\t)).done(finish).fail(finish).send();\n\t\t\t_reorderRequests.emplace(peerId, requestId);\n\t\t}\n\t\treturn lifetime;\n\t};\n}\n\nData::Usernames Usernames::FromTL(const MTPVector<MTPUsername> &usernames) {\n\treturn ranges::views::all(\n\t\tusernames.v\n\t) | ranges::views::transform(UsernameFromTL) | ranges::to_vector;\n}\n\nvoid Usernames::requestToCache(not_null<PeerData*> peer) {\n\t_tinyCache = {};\n\tif (const auto user = peer->asUser()) {\n\t\tif (user->usernames().empty()) {\n\t\t\treturn;\n\t\t}\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tif (channel->usernames().empty()) {\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto lifetime = std::make_shared<rpl::lifetime>();\n\t*lifetime = loadUsernames(\n\t\tpeer\n\t) | rpl::on_next([=, id = peer->id](Data::Usernames usernames) {\n\t\t_tinyCache = std::make_pair(id, std::move(usernames));\n\t\tlifetime->destroy();\n\t});\n}\n\nData::Usernames Usernames::cacheFor(PeerId id) {\n\treturn (_tinyCache.first == id) ? _tinyCache.second : Data::Usernames();\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_user_names.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_user_names.h\"\n#include \"mtproto/sender.h\"\n\nclass ApiWrap;\nclass PeerData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nclass Usernames final {\npublic:\n\tenum class Error {\n\t\tTooMuch,\n\t\tFlood,\n\t\tUnknown,\n\t};\n\n\texplicit Usernames(not_null<ApiWrap*> api);\n\n\t[[nodiscard]] rpl::producer<Data::Usernames> loadUsernames(\n\t\tnot_null<PeerData*> peer) const;\n\t[[nodiscard]] rpl::producer<rpl::no_value, Error> toggle(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &username,\n\t\tbool active);\n\t[[nodiscard]] rpl::producer<> reorder(\n\t\tnot_null<PeerData*> peer,\n\t\tconst std::vector<QString> &usernames);\n\n\tvoid requestToCache(not_null<PeerData*> peer);\n\t[[nodiscard]] Data::Usernames cacheFor(PeerId id);\n\n\tstatic Data::Usernames FromTL(const MTPVector<MTPUsername> &usernames);\n\nprivate:\n\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\n\tusing Key = PeerId;\n\tstruct Entry final {\n\t\trpl::event_stream<rpl::no_value, Error> done;\n\t\tstd::vector<QString> usernames;\n\t};\n\tbase::flat_map<Key, Entry> _toggleRequests;\n\tbase::flat_map<Key, mtpRequestId> _reorderRequests;\n\t// Used for a seamless display of usernames list.\n\tstd::pair<Key, Data::Usernames> _tinyCache;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_user_privacy.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_user_privacy.h\"\n\n#include \"apiwrap.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_peer_id.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_premium.h\" // Settings::ShowPremium.\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kMaxRules = 3; // Allow users, disallow users, Option.\n\nusing TLInputRules = MTPVector<MTPInputPrivacyRule>;\nusing TLRules = MTPVector<MTPPrivacyRule>;\n\nTLInputRules RulesToTL(const UserPrivacy::Rule &rule) {\n\tusing Exceptions = UserPrivacy::Exceptions;\n\tconst auto collectInputUsers = [](const Exceptions &exceptions) {\n\t\tconst auto &peers = exceptions.peers;\n\t\tauto result = QVector<MTPInputUser>();\n\t\tresult.reserve(peers.size());\n\t\tfor (const auto &peer : peers) {\n\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\tresult.push_back(user->inputUser());\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\tconst auto collectInputChats = [](const Exceptions &exceptions) {\n\t\tconst auto &peers = exceptions.peers;\n\t\tauto result = QVector<MTPlong>();\n\t\tresult.reserve(peers.size());\n\t\tfor (const auto &peer : peers) {\n\t\t\tif (!peer->isUser()) {\n\t\t\t\tresult.push_back(peerToBareMTPInt(peer->id));\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\n\tusing Option = UserPrivacy::Option;\n\tauto result = QVector<MTPInputPrivacyRule>();\n\tresult.reserve(kMaxRules);\n\tif (!rule.ignoreAlways) {\n\t\tconst auto users = collectInputUsers(rule.always);\n\t\tconst auto chats = collectInputChats(rule.always);\n\t\tif (!users.empty()) {\n\t\t\tresult.push_back(\n\t\t\t\tMTP_inputPrivacyValueAllowUsers(\n\t\t\t\t\tMTP_vector<MTPInputUser>(users)));\n\t\t}\n\t\tif (!chats.empty()) {\n\t\t\tresult.push_back(\n\t\t\t\tMTP_inputPrivacyValueAllowChatParticipants(\n\t\t\t\t\tMTP_vector<MTPlong>(chats)));\n\t\t}\n\t\tif (rule.always.premiums && (rule.option != Option::Everyone)) {\n\t\t\tresult.push_back(MTP_inputPrivacyValueAllowPremium());\n\t\t}\n\t\tif (rule.always.miniapps && (rule.option != Option::Everyone)) {\n\t\t\tresult.push_back(MTP_inputPrivacyValueAllowBots());\n\t\t}\n\t}\n\tif (!rule.ignoreNever) {\n\t\tconst auto users = collectInputUsers(rule.never);\n\t\tconst auto chats = collectInputChats(rule.never);\n\t\tif (!users.empty()) {\n\t\t\tresult.push_back(\n\t\t\t\tMTP_inputPrivacyValueDisallowUsers(\n\t\t\t\t\tMTP_vector<MTPInputUser>(users)));\n\t\t}\n\t\tif (!chats.empty()) {\n\t\t\tresult.push_back(\n\t\t\t\tMTP_inputPrivacyValueDisallowChatParticipants(\n\t\t\t\t\tMTP_vector<MTPlong>(chats)));\n\t\t}\n\t\tif (rule.never.miniapps && (rule.option != Option::Nobody)) {\n\t\t\tresult.push_back(MTP_inputPrivacyValueDisallowBots());\n\t\t}\n\t}\n\tresult.push_back([&] {\n\t\tswitch (rule.option) {\n\t\tcase Option::Everyone: return MTP_inputPrivacyValueAllowAll();\n\t\tcase Option::Contacts: return MTP_inputPrivacyValueAllowContacts();\n\t\tcase Option::CloseFriends:\n\t\t\treturn MTP_inputPrivacyValueAllowCloseFriends();\n\t\tcase Option::Nobody: return MTP_inputPrivacyValueDisallowAll();\n\t\t}\n\t\tUnexpected(\"Option value in Api::UserPrivacy::RulesToTL.\");\n\t}());\n\n\n\treturn MTP_vector<MTPInputPrivacyRule>(std::move(result));\n}\n\nUserPrivacy::Rule TLToRules(const TLRules &rules, Data::Session &owner) {\n\t// This is simplified version of privacy rules interpretation.\n\t// But it should be fine for all the apps\n\t// that use the same subset of features.\n\tusing Option = UserPrivacy::Option;\n\tauto result = UserPrivacy::Rule();\n\tauto optionSet = false;\n\tconst auto setOption = [&](Option option) {\n\t\tif (optionSet) {\n\t\t\treturn;\n\t\t}\n\t\toptionSet = true;\n\t\tresult.option = option;\n\t};\n\tauto &always = result.always.peers;\n\tauto &never = result.never.peers;\n\tconst auto feed = [&](const MTPPrivacyRule &rule) {\n\t\trule.match([&](const MTPDprivacyValueAllowAll &) {\n\t\t\tsetOption(Option::Everyone);\n\t\t}, [&](const MTPDprivacyValueAllowContacts &) {\n\t\t\tsetOption(Option::Contacts);\n\t\t}, [&](const MTPDprivacyValueAllowCloseFriends &) {\n\t\t\tsetOption(Option::CloseFriends);\n\t\t}, [&](const MTPDprivacyValueAllowPremium &) {\n\t\t\tresult.always.premiums = true;\n\t\t}, [&](const MTPDprivacyValueAllowBots &) {\n\t\t\tresult.always.miniapps = true;\n\t\t}, [&](const MTPDprivacyValueDisallowBots &) {\n\t\t\tresult.never.miniapps = true;\n\t\t}, [&](const MTPDprivacyValueAllowUsers &data) {\n\t\t\tconst auto &users = data.vusers().v;\n\t\t\talways.reserve(always.size() + users.size());\n\t\t\tfor (const auto &userId : users) {\n\t\t\t\tconst auto user = owner.user(UserId(userId.v));\n\t\t\t\tif (!base::contains(never, user)\n\t\t\t\t\t&& !base::contains(always, user)) {\n\t\t\t\t\talways.emplace_back(user);\n\t\t\t\t}\n\t\t\t}\n\t\t}, [&](const MTPDprivacyValueAllowChatParticipants &data) {\n\t\t\tconst auto &chats = data.vchats().v;\n\t\t\talways.reserve(always.size() + chats.size());\n\t\t\tfor (const auto &chatId : chats) {\n\t\t\t\tconst auto chat = owner.chatLoaded(chatId);\n\t\t\t\tconst auto peer = chat\n\t\t\t\t\t? static_cast<PeerData*>(chat)\n\t\t\t\t\t: owner.channelLoaded(chatId);\n\t\t\t\tif (peer\n\t\t\t\t\t&& !base::contains(never, peer)\n\t\t\t\t\t&& !base::contains(always, peer)) {\n\t\t\t\t\talways.emplace_back(peer);\n\t\t\t\t}\n\t\t\t}\n\t\t}, [&](const MTPDprivacyValueDisallowContacts &) {\n\t\t\t// Not supported\n\t\t}, [&](const MTPDprivacyValueDisallowAll &) {\n\t\t\tsetOption(Option::Nobody);\n\t\t}, [&](const MTPDprivacyValueDisallowUsers &data) {\n\t\t\tconst auto &users = data.vusers().v;\n\t\t\tnever.reserve(never.size() + users.size());\n\t\t\tfor (const auto &userId : users) {\n\t\t\t\tconst auto user = owner.user(UserId(userId.v));\n\t\t\t\tif (!base::contains(always, user)\n\t\t\t\t\t&& !base::contains(never, user)) {\n\t\t\t\t\tnever.emplace_back(user);\n\t\t\t\t}\n\t\t\t}\n\t\t}, [&](const MTPDprivacyValueDisallowChatParticipants &data) {\n\t\t\tconst auto &chats = data.vchats().v;\n\t\t\tnever.reserve(never.size() + chats.size());\n\t\t\tfor (const auto &chatId : chats) {\n\t\t\t\tconst auto chat = owner.chatLoaded(chatId);\n\t\t\t\tconst auto peer = chat\n\t\t\t\t\t? static_cast<PeerData*>(chat)\n\t\t\t\t\t: owner.channelLoaded(chatId);\n\t\t\t\tif (peer\n\t\t\t\t\t&& !base::contains(always, peer)\n\t\t\t\t\t&& !base::contains(never, peer)) {\n\t\t\t\t\tnever.emplace_back(peer);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t};\n\tfor (const auto &rule : rules.v) {\n\t\tfeed(rule);\n\t}\n\tfeed(MTP_privacyValueDisallowAll()); // Disallow by default.\n\treturn result;\n}\n\nMTPInputPrivacyKey KeyToTL(UserPrivacy::Key key) {\n\tusing Key = UserPrivacy::Key;\n\tswitch (key) {\n\tcase Key::Calls: return MTP_inputPrivacyKeyPhoneCall();\n\tcase Key::Invites: return MTP_inputPrivacyKeyChatInvite();\n\tcase Key::PhoneNumber: return MTP_inputPrivacyKeyPhoneNumber();\n\tcase Key::AddedByPhone: return MTP_inputPrivacyKeyAddedByPhone();\n\tcase Key::LastSeen: return MTP_inputPrivacyKeyStatusTimestamp();\n\tcase Key::CallsPeer2Peer: return MTP_inputPrivacyKeyPhoneP2P();\n\tcase Key::Forwards: return MTP_inputPrivacyKeyForwards();\n\tcase Key::ProfilePhoto: return MTP_inputPrivacyKeyProfilePhoto();\n\tcase Key::Voices: return MTP_inputPrivacyKeyVoiceMessages();\n\tcase Key::About: return MTP_inputPrivacyKeyAbout();\n\tcase Key::Birthday: return MTP_inputPrivacyKeyBirthday();\n\tcase Key::GiftsAutoSave: return MTP_inputPrivacyKeyStarGiftsAutoSave();\n\tcase Key::NoPaidMessages: return MTP_inputPrivacyKeyNoPaidMessages();\n\tcase Key::SavedMusic: return MTP_inputPrivacyKeySavedMusic();\n\t}\n\tUnexpected(\"Key in Api::UserPrivacy::KetToTL.\");\n}\n\nstd::optional<UserPrivacy::Key> TLToKey(mtpTypeId type) {\n\tusing Key = UserPrivacy::Key;\n\tswitch (type) {\n\tcase mtpc_privacyKeyPhoneNumber:\n\tcase mtpc_inputPrivacyKeyPhoneNumber: return Key::PhoneNumber;\n\tcase mtpc_privacyKeyAddedByPhone:\n\tcase mtpc_inputPrivacyKeyAddedByPhone: return Key::AddedByPhone;\n\tcase mtpc_privacyKeyStatusTimestamp:\n\tcase mtpc_inputPrivacyKeyStatusTimestamp: return Key::LastSeen;\n\tcase mtpc_privacyKeyChatInvite:\n\tcase mtpc_inputPrivacyKeyChatInvite: return Key::Invites;\n\tcase mtpc_privacyKeyPhoneCall:\n\tcase mtpc_inputPrivacyKeyPhoneCall: return Key::Calls;\n\tcase mtpc_privacyKeyPhoneP2P:\n\tcase mtpc_inputPrivacyKeyPhoneP2P: return Key::CallsPeer2Peer;\n\tcase mtpc_privacyKeyForwards:\n\tcase mtpc_inputPrivacyKeyForwards: return Key::Forwards;\n\tcase mtpc_privacyKeyProfilePhoto:\n\tcase mtpc_inputPrivacyKeyProfilePhoto: return Key::ProfilePhoto;\n\tcase mtpc_privacyKeyVoiceMessages:\n\tcase mtpc_inputPrivacyKeyVoiceMessages: return Key::Voices;\n\tcase mtpc_privacyKeyAbout:\n\tcase mtpc_inputPrivacyKeyAbout: return Key::About;\n\tcase mtpc_privacyKeyBirthday:\n\tcase mtpc_inputPrivacyKeyBirthday: return Key::Birthday;\n\tcase mtpc_privacyKeyStarGiftsAutoSave:\n\tcase mtpc_inputPrivacyKeyStarGiftsAutoSave: return Key::GiftsAutoSave;\n\tcase mtpc_privacyKeyNoPaidMessages:\n\tcase mtpc_inputPrivacyKeyNoPaidMessages: return Key::NoPaidMessages;\n\tcase mtpc_privacyKeySavedMusic:\n\tcase mtpc_inputPrivacyKeySavedMusic: return Key::SavedMusic;\n\t}\n\treturn std::nullopt;\n}\n\n} // namespace\n\nUserPrivacy::UserPrivacy(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance()) {\n}\n\nvoid UserPrivacy::save(\n\t\tKey key,\n\t\tconst UserPrivacy::Rule &rule) {\n\tconst auto tlKey = KeyToTL(key);\n\tconst auto keyTypeId = tlKey.type();\n\tconst auto it = _privacySaveRequests.find(keyTypeId);\n\tif (it != _privacySaveRequests.cend()) {\n\t\t_api.request(it->second).cancel();\n\t\t_privacySaveRequests.erase(it);\n\t}\n\n\tconst auto requestId = _api.request(MTPaccount_SetPrivacy(\n\t\ttlKey,\n\t\tRulesToTL(rule)\n\t)).done([=](const MTPaccount_PrivacyRules &result) {\n\t\tresult.match([&](const MTPDaccount_privacyRules &data) {\n\t\t\t_session->data().processUsers(data.vusers());\n\t\t\t_session->data().processChats(data.vchats());\n\t\t\t_privacySaveRequests.remove(keyTypeId);\n\t\t\tapply(keyTypeId, data.vrules(), true);\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto message = error.type();\n\t\tif (message == u\"PREMIUM_ACCOUNT_REQUIRED\"_q) {\n\t\t\tSettings::ShowPremium(_session, QString());\n\t\t}\n\t\t_privacySaveRequests.remove(keyTypeId);\n\t}).send();\n\n\t_privacySaveRequests.emplace(keyTypeId, requestId);\n}\n\nvoid UserPrivacy::apply(\n\t\tmtpTypeId type,\n\t\tconst TLRules &rules,\n\t\tbool allLoaded) {\n\tif (const auto key = TLToKey(type)) {\n\t\tif (!allLoaded) {\n\t\t\treload(*key);\n\t\t\treturn;\n\t\t}\n\t\tpushPrivacy(*key, rules);\n\t\tif ((*key) == Key::LastSeen) {\n\t\t\t_session->api().updatePrivacyLastSeens();\n\t\t}\n\t}\n}\n\nvoid UserPrivacy::reload(Key key) {\n\tif (_privacyRequestIds.contains(key)) {\n\t\treturn;\n\t}\n\tconst auto requestId = _api.request(MTPaccount_GetPrivacy(\n\t\tKeyToTL(key)\n\t)).done([=](const MTPaccount_PrivacyRules &result) {\n\t\t_privacyRequestIds.erase(key);\n\t\tresult.match([&](const MTPDaccount_privacyRules &data) {\n\t\t\t_session->data().processUsers(data.vusers());\n\t\t\t_session->data().processChats(data.vchats());\n\t\t\tpushPrivacy(key, data.vrules());\n\t\t});\n\t}).fail([=] {\n\t\t_privacyRequestIds.erase(key);\n\t}).send();\n\t_privacyRequestIds.emplace(key, requestId);\n}\n\nvoid UserPrivacy::pushPrivacy(Key key, const TLRules &rules) {\n\tconst auto &saved\n\t\t= (_privacyValues[key] = TLToRules(rules, _session->data()));\n\tconst auto i = _privacyChanges.find(key);\n\tif (i != end(_privacyChanges)) {\n\t\ti->second.fire_copy(saved);\n\t}\n}\n\nauto UserPrivacy::value(Key key) -> rpl::producer<UserPrivacy::Rule> {\n\tif (const auto i = _privacyValues.find(key); i != end(_privacyValues)) {\n\t\treturn _privacyChanges[key].events_starting_with_copy(i->second);\n\t} else {\n\t\treturn _privacyChanges[key].events();\n\t}\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_user_privacy.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n\nclass ApiWrap;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nclass UserPrivacy final {\npublic:\n\tenum class Key {\n\t\tPhoneNumber,\n\t\tAddedByPhone,\n\t\tLastSeen,\n\t\tCalls,\n\t\tInvites,\n\t\tCallsPeer2Peer,\n\t\tForwards,\n\t\tProfilePhoto,\n\t\tVoices,\n\t\tAbout,\n\t\tBirthday,\n\t\tGiftsAutoSave,\n\t\tNoPaidMessages,\n\t\tSavedMusic,\n\t};\n\tenum class Option {\n\t\tEveryone,\n\t\tContacts,\n\t\tCloseFriends,\n\t\tNobody,\n\t};\n\tstruct Exceptions {\n\t\tstd::vector<not_null<PeerData*>> peers;\n\t\tbool premiums = false;\n\t\tbool miniapps = false;\n\t};\n\tstruct Rule {\n\t\tOption option = Option::Everyone;\n\t\tExceptions always;\n\t\tExceptions never;\n\t\tbool ignoreAlways = false;\n\t\tbool ignoreNever = false;\n\t};\n\n\texplicit UserPrivacy(not_null<ApiWrap*> api);\n\n\tvoid save(\n\t\tKey key,\n\t\tconst UserPrivacy::Rule &rule);\n\tvoid apply(\n\t\tmtpTypeId type,\n\t\tconst MTPVector<MTPPrivacyRule> &rules,\n\t\tbool allLoaded);\n\n\tvoid reload(Key key);\n\trpl::producer<Rule> value(Key key);\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\n\tvoid pushPrivacy(Key key, const MTPVector<MTPPrivacyRule> &rules);\n\n\tbase::flat_map<mtpTypeId, mtpRequestId> _privacySaveRequests;\n\n\tbase::flat_map<Key, mtpRequestId> _privacyRequestIds;\n\tbase::flat_map<Key, Rule> _privacyValues;\n\tstd::map<Key, rpl::event_stream<Rule>> _privacyChanges;\n\n\tMTP::Sender _api;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_views.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_views.h\"\n\n#include \"apiwrap.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_peer_id.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"main/main_session.h\"\n\nnamespace Api {\nnamespace {\n\n// Send channel views each second.\nconstexpr auto kSendViewsTimeout = crl::time(1000);\nconstexpr auto kPollExtendedMediaPeriod = 30 * crl::time(1000);\nconstexpr auto kMaxPollPerRequest = 100;\n\n} // namespace\n\nViewsManager::ViewsManager(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance())\n, _incrementTimer([=] { viewsIncrement(); })\n, _pollTimer([=] { sendPollRequests(); }) {\n}\n\nvoid ViewsManager::scheduleIncrement(not_null<HistoryItem*> item) {\n\tauto peer = item->history()->peer;\n\tauto i = _incremented.find(peer);\n\tif (i != _incremented.cend()) {\n\t\tif (i->second.contains(item->id)) {\n\t\t\treturn;\n\t\t}\n\t} else {\n\t\ti = _incremented.emplace(peer).first;\n\t}\n\ti->second.emplace(item->id);\n\tauto j = _toIncrement.find(peer);\n\tif (j == _toIncrement.cend()) {\n\t\tj = _toIncrement.emplace(peer).first;\n\t\t_incrementTimer.callOnce(kSendViewsTimeout);\n\t}\n\tj->second.emplace(item->id);\n}\n\nvoid ViewsManager::removeIncremented(not_null<PeerData*> peer) {\n\t_incremented.remove(peer);\n}\n\nvoid ViewsManager::pollExtendedMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tbool force) {\n\tif (!item->isRegular()) {\n\t\treturn;\n\t}\n\tconst auto id = item->id;\n\tconst auto peer = item->history()->peer;\n\tauto &request = _pollRequests[peer];\n\tif (request.ids.contains(id) || request.sent.contains(id)) {\n\t\tif (!force || request.forced) {\n\t\t\treturn;\n\t\t}\n\t}\n\trequest.ids.emplace(id);\n\tif (force) {\n\t\trequest.forced = true;\n\t}\n\tconst auto delay = force ? 1 : kPollExtendedMediaPeriod;\n\tif (!request.id && (!request.when || force)) {\n\t\trequest.when = crl::now() + delay;\n\t}\n\tif (!_pollTimer.isActive() || force) {\n\t\t_pollTimer.callOnce(delay);\n\t}\n}\n\nvoid ViewsManager::viewsIncrement() {\n\tfor (auto i = _toIncrement.begin(); i != _toIncrement.cend();) {\n\t\tif (_incrementRequests.contains(i->first)) {\n\t\t\t++i;\n\t\t\tcontinue;\n\t\t}\n\n\t\tQVector<MTPint> ids;\n\t\tids.reserve(i->second.size());\n\t\tfor (const auto &msgId : i->second) {\n\t\t\tids.push_back(MTP_int(msgId));\n\t\t}\n\t\tconst auto requestId = _api.request(MTPmessages_GetMessagesViews(\n\t\t\ti->first->input(),\n\t\t\tMTP_vector<MTPint>(ids),\n\t\t\tMTP_bool(true)\n\t\t)).done([=](\n\t\t\t\tconst MTPmessages_MessageViews &result,\n\t\t\t\tmtpRequestId requestId) {\n\t\t\tdone(ids, result, requestId);\n\t\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\t\tfail(error, requestId);\n\t\t}).afterDelay(5).send();\n\n\t\t_incrementRequests.emplace(i->first, requestId);\n\t\ti = _toIncrement.erase(i);\n\t}\n}\n\nvoid ViewsManager::sendPollRequests() {\n\tconst auto now = crl::now();\n\tauto toRequest = base::flat_map<not_null<PeerData*>, QVector<MTPint>>();\n\tauto nearest = crl::time();\n\tfor (auto &[peer, request] : _pollRequests) {\n\t\tif (request.id) {\n\t\t\tcontinue;\n\t\t} else if (request.when <= now) {\n\t\t\tAssert(request.sent.empty());\n\t\t\tauto &list = toRequest[peer];\n\t\t\tconst auto count = int(request.ids.size());\n\t\t\tif (count < kMaxPollPerRequest) {\n\t\t\t\trequest.sent = base::take(request.ids);\n\t\t\t} else {\n\t\t\t\tconst auto from = begin(request.ids);\n\t\t\t\tconst auto end = from + kMaxPollPerRequest;\n\t\t\t\trequest.sent = { from, end };\n\t\t\t\trequest.ids.erase(from, end);\n\t\t\t}\n\t\t\tlist.reserve(request.sent.size());\n\t\t\tfor (const auto &id : request.sent) {\n\t\t\t\tlist.push_back(MTP_int(id.bare));\n\t\t\t}\n\t\t\tif (!request.ids.empty()) {\n\t\t\t\tnearest = now;\n\t\t\t}\n\t\t} else if (!nearest || nearest > request.when) {\n\t\t\tnearest = request.when;\n\t\t}\n\t}\n\tsendPollRequests(toRequest);\n\tif (nearest) {\n\t\t_pollTimer.callOnce(std::max(nearest - now, crl::time(1)));\n\t}\n}\n\nvoid ViewsManager::sendPollRequests(\n\tconst base::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tQVector<MTPint>> &batched) {\n\tfor (auto &[peer, list] : batched) {\n\t\tconst auto finish = [=, list = list](mtpRequestId id) {\n\t\t\tconst auto now = crl::now();\n\t\t\tconst auto owner = &_session->data();\n\t\t\tfor (auto i = begin(_pollRequests); i != end(_pollRequests);) {\n\t\t\t\tif (i->second.id == id) {\n\t\t\t\t\tconst auto peer = i->first->id;\n\t\t\t\t\tfor (const auto &itemId : i->second.sent) {\n\t\t\t\t\t\tif (const auto item = owner->message(peer, itemId)) {\n\t\t\t\t\t\t\towner->requestItemRepaint(item);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\ti->second.sent.clear();\n\t\t\t\t\ti->second.id = 0;\n\t\t\t\t\tif (i->second.ids.empty()) {\n\t\t\t\t\t\ti = _pollRequests.erase(i);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst auto delay = i->second.forced\n\t\t\t\t\t\t\t? 1\n\t\t\t\t\t\t\t: kPollExtendedMediaPeriod;\n\t\t\t\t\t\ti->second.when = now + delay;\n\t\t\t\t\t\tif (!_pollTimer.isActive() || i->second.forced) {\n\t\t\t\t\t\t\t_pollTimer.callOnce(delay);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t++i;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t++i;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tconst auto requestId = _api.request(MTPmessages_GetExtendedMedia(\n\t\t\tpeer->input(),\n\t\t\tMTP_vector<MTPint>(list)\n\t\t)).done([=](const MTPUpdates &result, mtpRequestId id) {\n\t\t\t_session->api().applyUpdates(result);\n\t\t\tfinish(id);\n\t\t}).fail([=](const MTP::Error &error, mtpRequestId id) {\n\t\t\tfinish(id);\n\t\t}).send();\n\n\t\t_pollRequests[peer].id = requestId;\n\t}\n}\n\nvoid ViewsManager::done(\n\t\tQVector<MTPint> ids,\n\t\tconst MTPmessages_MessageViews &result,\n\t\tmtpRequestId requestId) {\n\tconst auto &data = result.c_messages_messageViews();\n\tauto &owner = _session->data();\n\towner.processUsers(data.vusers());\n\towner.processChats(data.vchats());\n\tauto &v = data.vviews().v;\n\tif (ids.size() == v.size()) {\n\t\tfor (const auto &[peer, id] : _incrementRequests) {\n\t\t\tif (id != requestId) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (auto j = 0, l = int(ids.size()); j < l; ++j) {\n\t\t\t\tif (const auto item = owner.message(peer->id, ids[j].v)) {\n\t\t\t\t\tv[j].match([&](const MTPDmessageViews &data) {\n\t\t\t\t\t\tif (const auto views = data.vviews()) {\n\t\t\t\t\t\t\tif (item->changeViewsCount(views->v)) {\n\t\t\t\t\t\t\t\t_session->data().notifyItemDataChange(item);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (const auto forwards = data.vforwards()) {\n\t\t\t\t\t\t\titem->setForwardsCount(forwards->v);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (const auto replies = data.vreplies()) {\n\t\t\t\t\t\t\titem->setReplies(\n\t\t\t\t\t\t\t\tHistoryMessageRepliesData(replies));\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\t_incrementRequests.erase(peer);\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (!_toIncrement.empty() && !_incrementTimer.isActive()) {\n\t\t_incrementTimer.callOnce(kSendViewsTimeout);\n\t}\n}\n\nvoid ViewsManager::fail(const MTP::Error &error, mtpRequestId requestId) {\n\tfor (const auto &[peer, id] : _incrementRequests) {\n\t\tif (id == requestId) {\n\t\t\t_incrementRequests.erase(peer);\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (!_toIncrement.empty() && !_incrementTimer.isActive()) {\n\t\t_incrementTimer.callOnce(kSendViewsTimeout);\n\t}\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_views.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n#include \"base/timer.h\"\n\nclass ApiWrap;\nclass PeerData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nclass ViewsManager final {\npublic:\n\texplicit ViewsManager(not_null<ApiWrap*> api);\n\n\tvoid scheduleIncrement(not_null<HistoryItem*> item);\n\tvoid removeIncremented(not_null<PeerData*> peer);\n\n\tvoid pollExtendedMedia(not_null<HistoryItem*> item, bool force = false);\n\nprivate:\n\tstruct PollExtendedMediaRequest {\n\t\tcrl::time when = 0;\n\t\tmtpRequestId id = 0;\n\t\tbase::flat_set<MsgId> ids;\n\t\tbase::flat_set<MsgId> sent;\n\t\tbool forced = false;\n\t};\n\n\tvoid viewsIncrement();\n\tvoid sendPollRequests();\n\tvoid sendPollRequests(\n\t\tconst base::flat_map<\n\t\t\tnot_null<PeerData*>,\n\t\t\tQVector<MTPint>> &prepared);\n\n\tvoid done(\n\t\tQVector<MTPint> ids,\n\t\tconst MTPmessages_MessageViews &result,\n\t\tmtpRequestId requestId);\n\tvoid fail(const MTP::Error &error, mtpRequestId requestId);\n\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\n\tbase::flat_map<not_null<PeerData*>, base::flat_set<MsgId>> _incremented;\n\tbase::flat_map<not_null<PeerData*>, base::flat_set<MsgId>> _toIncrement;\n\tbase::flat_map<not_null<PeerData*>, mtpRequestId> _incrementRequests;\n\tbase::flat_map<mtpRequestId, not_null<PeerData*>> _incrementByRequest;\n\tbase::Timer _incrementTimer;\n\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tPollExtendedMediaRequest> _pollRequests;\n\tbase::Timer _pollTimer;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_websites.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_websites.h\"\n\n#include \"api/api_authorizations.h\"\n#include \"api/api_blocked_peers.h\"\n#include \"apiwrap.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"main/main_session.h\"\n\nnamespace Api {\nnamespace {\n\n[[nodiscard]] Websites::Entry ParseEntry(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst MTPDwebAuthorization &data) {\n\tauto result = Websites::Entry{\n\t\t.hash = data.vhash().v,\n\t\t.bot = owner->user(data.vbot_id()),\n\t\t.platform = qs(data.vplatform()),\n\t\t.domain = qs(data.vdomain()),\n\t\t.browser = qs(data.vbrowser()),\n\t\t.ip = qs(data.vip()),\n\t\t.location = qs(data.vregion()),\n\t};\n\tresult.activeTime = data.vdate_active().v\n\t\t? data.vdate_active().v\n\t\t: data.vdate_created().v;\n\tresult.active = Authorizations::ActiveDateString(result.activeTime);\n\treturn result;\n}\n\n} // namespace\n\nWebsites::Websites(not_null<ApiWrap*> api)\n: _session(&api->session())\n, _api(&api->instance()) {\n}\n\nvoid Websites::reload() {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\n\t_requestId = _api.request(MTPaccount_GetWebAuthorizations(\n\t)).done([=](const MTPaccount_WebAuthorizations &result) {\n\t\t_requestId = 0;\n\t\t_lastReceived = crl::now();\n\t\tconst auto owner = &_session->data();\n\t\tconst auto &data = result.data();\n\t\towner->processUsers(data.vusers());\n\t\t_list = ranges::views::all(\n\t\t\tdata.vauthorizations().v\n\t\t) | ranges::views::transform([&](const MTPwebAuthorization &auth) {\n\t\t\treturn ParseEntry(owner, auth.data());\n\t\t}) | ranges::to<List>;\n\t\t_listChanges.fire({});\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t}).send();\n}\n\nvoid Websites::cancelCurrentRequest() {\n\t_api.request(base::take(_requestId)).cancel();\n}\n\nvoid Websites::requestTerminate(\n\t\tFn<void(const MTPBool &result)> &&done,\n\t\tFn<void(const MTP::Error &error)> &&fail,\n\t\tstd::optional<uint64> hash,\n\t\tUserData *botToBlock) {\n\tconst auto send = [&](auto request) {\n\t\t_api.request(\n\t\t\tstd::move(request)\n\t\t).done([=, done = std::move(done)](const MTPBool &result) {\n\t\t\tdone(result);\n\t\t\tif (hash) {\n\t\t\t\t_list.erase(\n\t\t\t\t\tranges::remove(_list, *hash, &Entry::hash),\n\t\t\t\t\tend(_list));\n\t\t\t} else {\n\t\t\t\t_list.clear();\n\t\t\t}\n\t\t\t_listChanges.fire({});\n\t\t}).fail(\n\t\t\tstd::move(fail)\n\t\t).send();\n\t};\n\tif (hash) {\n\t\tsend(MTPaccount_ResetWebAuthorization(MTP_long(*hash)));\n\t\tif (botToBlock) {\n\t\t\tbotToBlock->session().api().blockedPeers().block(botToBlock);\n\t\t}\n\t} else {\n\t\tsend(MTPaccount_ResetWebAuthorizations());\n\t}\n}\n\nWebsites::List Websites::list() const {\n\treturn _list;\n}\n\nauto Websites::listValue() const\n-> rpl::producer<Websites::List> {\n\treturn rpl::single(\n\t\tlist()\n\t) | rpl::then(\n\t\t_listChanges.events() | rpl::map([=] { return list(); })\n\t);\n}\n\nrpl::producer<int> Websites::totalValue() const {\n\treturn rpl::single(\n\t\ttotal()\n\t) | rpl::then(\n\t\t_listChanges.events() | rpl::map([=] { return total(); })\n\t);\n}\n\nint Websites::total() const {\n\treturn _list.size();\n}\n\ncrl::time Websites::lastReceivedTime() {\n\treturn _lastReceived;\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_websites.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n\nclass ApiWrap;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Api {\n\nclass Websites final {\npublic:\n\texplicit Websites(not_null<ApiWrap*> api);\n\n\tstruct Entry {\n\t\tuint64 hash = 0;\n\n\t\tnot_null<UserData*> bot;\n\t\tTimeId activeTime = 0;\n\t\tQString active, platform, domain, browser, ip, location;\n\t};\n\tusing List = std::vector<Entry>;\n\n\tvoid reload();\n\tvoid cancelCurrentRequest();\n\tvoid requestTerminate(\n\t\tFn<void(const MTPBool &result)> &&done,\n\t\tFn<void(const MTP::Error &error)> &&fail,\n\t\tstd::optional<uint64> hash = std::nullopt,\n\t\tUserData *botToBlock = nullptr);\n\n\t[[nodiscard]] crl::time lastReceivedTime();\n\n\t[[nodiscard]] List list() const;\n\t[[nodiscard]] rpl::producer<List> listValue() const;\n\t[[nodiscard]] int total() const;\n\t[[nodiscard]] rpl::producer<int> totalValue() const;\n\nprivate:\n\tnot_null<Main::Session*> _session;\n\n\tMTP::Sender _api;\n\tmtpRequestId _requestId = 0;\n\n\tList _list;\n\trpl::event_stream<> _listChanges;\n\n\tcrl::time _lastReceived = 0;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_who_reacted.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"api/api_who_reacted.h\"\n\n#include \"api/api_global_privacy.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_user.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_session.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_message_reaction_id.h\"\n#include \"data/data_peer_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"base/unixtime.h\"\n#include \"base/weak_ptr.h\"\n#include \"ui/controls/who_reacted_context_action.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kContextReactionsLimit = 50;\n\nusing Data::ReactionId;\nusing WhoReadState = Ui::WhoReadState;\n\nstruct Peers {\n\tstd::vector<WhoReadPeer> list;\n\tWhoReadState state = WhoReadState::Empty;\n\n\tfriend inline bool operator==(\n\t\tconst Peers &a,\n\t\tconst Peers &b) noexcept = default;\n};\n\nstruct PeerWithReaction {\n\tWhoReadPeer peerWithDate;\n\tReactionId reaction;\n\n\tfriend inline bool operator==(\n\t\tconst PeerWithReaction &a,\n\t\tconst PeerWithReaction &b) noexcept = default;\n};\n\nstruct PeersWithReactions {\n\tstd::vector<PeerWithReaction> list;\n\tstd::vector<WhoReadPeer> read;\n\tint fullReactionsCount = 0;\n\tWhoReadState state = WhoReadState::Empty;\n\n\tfriend inline bool operator==(\n\t\tconst PeersWithReactions &a,\n\t\tconst PeersWithReactions &b) noexcept = default;\n};\n\nstruct CachedRead {\n\tCachedRead()\n\t: data(Peers{ .state = WhoReadState::Unknown }) {\n\t}\n\trpl::variable<Peers> data;\n\tmtpRequestId requestId = 0;\n};\n\nstruct CachedReacted {\n\tCachedReacted()\n\t: data(PeersWithReactions{ .state = WhoReadState::Unknown }) {\n\t}\n\trpl::variable<PeersWithReactions> data;\n\tmtpRequestId requestId = 0;\n};\n\nstruct Context {\n\tbase::flat_map<not_null<HistoryItem*>, CachedRead> cachedRead;\n\tbase::flat_map<\n\t\tnot_null<HistoryItem*>,\n\t\tbase::flat_map<ReactionId, CachedReacted>> cachedReacted;\n\tbase::flat_map<not_null<Main::Session*>, rpl::lifetime> subscriptions;\n\n\t[[nodiscard]] CachedRead &cacheRead(not_null<HistoryItem*> item) {\n\t\tconst auto i = cachedRead.find(item);\n\t\tif (i != end(cachedRead)) {\n\t\t\treturn i->second;\n\t\t}\n\t\treturn cachedRead.emplace(item, CachedRead()).first->second;\n\t}\n\n\t[[nodiscard]] CachedReacted &cacheReacted(\n\t\t\tnot_null<HistoryItem*> item,\n\t\t\tconst ReactionId &reaction) {\n\t\tauto &map = cachedReacted[item];\n\t\tconst auto i = map.find(reaction);\n\t\tif (i != end(map)) {\n\t\t\treturn i->second;\n\t\t}\n\t\treturn map.emplace(reaction, CachedReacted()).first->second;\n\t}\n};\n\nstruct Userpic {\n\tnot_null<PeerData*> peer;\n\tTimeId date = 0;\n\tbool dateReacted = false;\n\tQString customEntityData;\n\tmutable Ui::PeerUserpicView view;\n\tmutable InMemoryKey uniqueKey;\n};\n\nstruct State {\n\tstd::vector<Userpic> userpics;\n\tUi::WhoReadContent current;\n\tbase::has_weak_ptr guard;\n\tbool someUserpicsNotLoaded = false;\n\tbool scheduled = false;\n};\n\n[[nodiscard]] auto Contexts()\n-> base::flat_map<not_null<QWidget*>, std::unique_ptr<Context>> & {\n\tstatic auto result = base::flat_map<\n\t\tnot_null<QWidget*>,\n\t\tstd::unique_ptr<Context>>();\n\treturn result;\n}\n\n[[nodiscard]] not_null<Context*> ContextAt(not_null<QWidget*> key) {\n\tauto &contexts = Contexts();\n\tconst auto i = contexts.find(key);\n\tif (i != end(contexts)) {\n\t\treturn i->second.get();\n\t}\n\tconst auto result = contexts.emplace(\n\t\tkey,\n\t\tstd::make_unique<Context>()).first->second.get();\n\tQObject::connect(key.get(), &QObject::destroyed, [=] {\n\t\tauto &contexts = Contexts();\n\t\tconst auto i = contexts.find(key);\n\t\tfor (auto &[item, entry] : i->second->cachedRead) {\n\t\t\tif (const auto requestId = entry.requestId) {\n\t\t\t\titem->history()->session().api().request(requestId).cancel();\n\t\t\t}\n\t\t}\n\t\tfor (auto &[item, map] : i->second->cachedReacted) {\n\t\t\tfor (auto &[reaction, entry] : map) {\n\t\t\t\tif (const auto requestId = entry.requestId) {\n\t\t\t\t\titem->history()->session().api().request(requestId).cancel();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcontexts.erase(i);\n\t});\n\treturn result;\n}\n\n[[nodiscard]] not_null<Context*> PreparedContextAt(\n\t\tnot_null<QWidget*> key,\n\t\tnot_null<Main::Session*> session) {\n\tconst auto context = ContextAt(key);\n\tif (context->subscriptions.contains(session)) {\n\t\treturn context;\n\t}\n\tsession->changes().messageUpdates(\n\t\tData::MessageUpdate::Flag::Destroyed\n\t) | rpl::on_next([=](const Data::MessageUpdate &update) {\n\t\tconst auto i = context->cachedRead.find(update.item);\n\t\tif (i != end(context->cachedRead)) {\n\t\t\tsession->api().request(i->second.requestId).cancel();\n\t\t\tcontext->cachedRead.erase(i);\n\t\t}\n\t\tconst auto j = context->cachedReacted.find(update.item);\n\t\tif (j != end(context->cachedReacted)) {\n\t\t\tfor (auto &[reaction, entry] : j->second) {\n\t\t\t\tsession->api().request(entry.requestId).cancel();\n\t\t\t}\n\t\t\tcontext->cachedReacted.erase(j);\n\t\t}\n\t}, context->subscriptions[session]);\n\tData::AmPremiumValue(\n\t\tsession\n\t) | rpl::skip(1) | rpl::filter(\n\t\trpl::mappers::_1\n\t) | rpl::on_next([=] {\n\t\tfor (auto &[item, cache] : context->cachedRead) {\n\t\t\tif (cache.data.current().state == Ui::WhoReadState::MyHidden) {\n\t\t\t\tcache.data = Peers{ .state = Ui::WhoReadState::Unknown };\n\t\t\t}\n\t\t}\n\t}, context->subscriptions[session]);\n\tsession->api().globalPrivacy().hideReadTime(\n\t) | rpl::skip(1) | rpl::filter(\n\t\t!rpl::mappers::_1\n\t) | rpl::on_next([=] {\n\t\tfor (auto &[item, cache] : context->cachedRead) {\n\t\t\tif (cache.data.current().state == Ui::WhoReadState::MyHidden) {\n\t\t\t\tcache.data = Peers{ .state = Ui::WhoReadState::Unknown };\n\t\t\t}\n\t\t}\n\t}, context->subscriptions[session]);\n\treturn context;\n}\n\n[[nodiscard]] QImage GenerateUserpic(Userpic &userpic, int size) {\n\tsize *= style::DevicePixelRatio();\n\tauto result = PeerData::GenerateUserpicImage(\n\t\tuserpic.peer,\n\t\tuserpic.view,\n\t\tsize);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\treturn result;\n}\n\n[[nodiscard]] Ui::WhoReadType DetectSeenType(not_null<HistoryItem*> item) {\n\tif (const auto media = item->media()) {\n\t\tif (!media->webpage()) {\n\t\t\tif (const auto document = media->document()) {\n\t\t\t\tif (document->isVoiceMessage()) {\n\t\t\t\t\treturn Ui::WhoReadType::Listened;\n\t\t\t\t} else if (document->isVideoMessage()) {\n\t\t\t\t\treturn Ui::WhoReadType::Watched;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn Ui::WhoReadType::Seen;\n}\n\n[[nodiscard]] rpl::producer<Peers> WhoReadIds(\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<QWidget*> context) {\n\tauto weak = base::make_weak(context);\n\tconst auto session = &item->history()->session();\n\treturn [=](auto consumer) {\n\t\tif (!weak) {\n\t\t\treturn rpl::lifetime();\n\t\t}\n\t\tconst auto context = PreparedContextAt(weak.get(), session);\n\t\tauto &entry = context->cacheRead(item);\n\t\tif (entry.requestId) {\n\t\t} else if (const auto user = item->history()->peer->asUser()) {\n\t\t\tentry.requestId = session->api().request(\n\t\t\t\tMTPmessages_GetOutboxReadDate(\n\t\t\t\t\tuser->input(),\n\t\t\t\t\tMTP_int(item->id)\n\t\t\t\t)\n\t\t\t).done([=](const MTPOutboxReadDate &result) {\n\t\t\t\tconst auto &data = result.data();\n\t\t\t\tauto &entry = context->cacheRead(item);\n\t\t\t\tentry.requestId = 0;\n\t\t\t\tauto parsed = Peers();\n\t\t\t\tparsed.list.push_back({\n\t\t\t\t\t.peer = user->id,\n\t\t\t\t\t.date = data.vdate().v,\n\t\t\t\t});\n\t\t\t\tentry.data = std::move(parsed);\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tauto &entry = context->cacheRead(item);\n\t\t\t\tentry.requestId = 0;\n\t\t\t\tif (entry.data.current().state == WhoReadState::Unknown) {\n\t\t\t\t\tconst auto &text = error.type();\n\t\t\t\t\tentry.data = (text == u\"YOUR_PRIVACY_RESTRICTED\"_q)\n\t\t\t\t\t\t? Peers{ .state = WhoReadState::MyHidden }\n\t\t\t\t\t\t: (text == u\"USER_PRIVACY_RESTRICTED\"_q)\n\t\t\t\t\t\t? Peers{ .state = WhoReadState::HisHidden }\n\t\t\t\t\t\t: (text == u\"MESSAGE_TOO_OLD\"_q)\n\t\t\t\t\t\t? Peers{ .state = WhoReadState::TooOld }\n\t\t\t\t\t\t: Peers{ .state = WhoReadState::Empty };\n\t\t\t\t}\n\t\t\t}).send();\n\t\t} else {\n\t\t\tentry.requestId = session->api().request(\n\t\t\t\tMTPmessages_GetMessageReadParticipants(\n\t\t\t\t\titem->history()->peer->input(),\n\t\t\t\t\tMTP_int(item->id)\n\t\t\t\t)\n\t\t\t).done([=](const MTPVector<MTPReadParticipantDate> &result) {\n\t\t\t\tauto &entry = context->cacheRead(item);\n\t\t\t\tentry.requestId = 0;\n\t\t\t\tauto parsed = Peers();\n\t\t\t\tparsed.list.reserve(result.v.size());\n\t\t\t\tfor (const auto &id : result.v) {\n\t\t\t\t\tparsed.list.push_back({\n\t\t\t\t\t\t.peer = UserId(id.data().vuser_id()),\n\t\t\t\t\t\t.date = id.data().vdate().v,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tentry.data = std::move(parsed);\n\t\t\t}).fail([=] {\n\t\t\t\tauto &entry = context->cacheRead(item);\n\t\t\t\tentry.requestId = 0;\n\t\t\t\tif (entry.data.current().state == WhoReadState::Unknown) {\n\t\t\t\t\tentry.data = Peers{ .state = WhoReadState::Empty };\n\t\t\t\t}\n\t\t\t}).send();\n\t\t}\n\t\treturn entry.data.value().start_existing(consumer);\n\t};\n}\n\n[[nodiscard]] PeersWithReactions WithEmptyReactions(\n\t\tPeers &&peers) {\n\tauto result = PeersWithReactions{\n\t\t.list = peers.list | ranges::views::transform([](WhoReadPeer peer) {\n\t\t\treturn PeerWithReaction{ .peerWithDate = peer };\n\t\t}) | ranges::to_vector,\n\t\t.state = peers.state,\n\t};\n\tresult.read = std::move(peers.list);\n\treturn result;\n}\n\n[[nodiscard]] rpl::producer<PeersWithReactions> WhoReactedIds(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst ReactionId &reaction,\n\t\tnot_null<QWidget*> context) {\n\tauto weak = base::make_weak(context);\n\tconst auto session = &item->history()->session();\n\treturn [=](auto consumer) {\n\t\tif (!weak) {\n\t\t\treturn rpl::lifetime();\n\t\t}\n\t\tconst auto context = PreparedContextAt(weak.get(), session);\n\t\tauto &entry = context->cacheReacted(item, reaction);\n\t\tif (!entry.requestId) {\n\t\t\tusing Flag = MTPmessages_GetMessageReactionsList::Flag;\n\t\t\tentry.requestId = session->api().request(\n\t\t\t\tMTPmessages_GetMessageReactionsList(\n\t\t\t\t\tMTP_flags(reaction.empty()\n\t\t\t\t\t\t? Flag(0)\n\t\t\t\t\t\t: Flag::f_reaction),\n\t\t\t\t\titem->history()->peer->input(),\n\t\t\t\t\tMTP_int(item->id),\n\t\t\t\t\tReactionToMTP(reaction),\n\t\t\t\t\tMTPstring(), // offset\n\t\t\t\t\tMTP_int(kContextReactionsLimit)\n\t\t\t\t)\n\t\t\t).done([=](const MTPmessages_MessageReactionsList &result) {\n\t\t\t\tauto &entry = context->cacheReacted(item, reaction);\n\t\t\t\tentry.requestId = 0;\n\n\t\t\t\tresult.match([&](\n\t\t\t\t\t\tconst MTPDmessages_messageReactionsList &data) {\n\t\t\t\t\tsession->data().processUsers(data.vusers());\n\t\t\t\t\tsession->data().processChats(data.vchats());\n\n\t\t\t\t\tauto parsed = PeersWithReactions{\n\t\t\t\t\t\t.fullReactionsCount = data.vcount().v,\n\t\t\t\t\t};\n\t\t\t\t\tparsed.list.reserve(data.vreactions().v.size());\n\t\t\t\t\tfor (const auto &vote : data.vreactions().v) {\n\t\t\t\t\t\tconst auto &data = vote.data();\n\t\t\t\t\t\tparsed.list.push_back(PeerWithReaction{\n\t\t\t\t\t\t\t.peerWithDate = {\n\t\t\t\t\t\t\t\t.peer = peerFromMTP(data.vpeer_id()),\n\t\t\t\t\t\t\t\t.date = data.vdate().v,\n\t\t\t\t\t\t\t\t.dateReacted = true,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t.reaction = Data::ReactionFromMTP(\n\t\t\t\t\t\t\t\tdata.vreaction()),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tentry.data = std::move(parsed);\n\t\t\t\t});\n\t\t\t}).fail([=] {\n\t\t\t\tauto &entry = context->cacheReacted(item, reaction);\n\t\t\t\tentry.requestId = 0;\n\t\t\t\tif (entry.data.current().state == WhoReadState::Unknown) {\n\t\t\t\t\tentry.data = PeersWithReactions{\n\t\t\t\t\t\t.state = WhoReadState::Empty,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}).send();\n\t\t}\n\t\treturn entry.data.value().start_existing(consumer);\n\t};\n}\n\n[[nodiscard]] auto WhoReadOrReactedIds(\n\tnot_null<HistoryItem*> item,\n\tnot_null<QWidget*> context)\n-> rpl::producer<PeersWithReactions> {\n\treturn rpl::combine(\n\t\tWhoReactedIds(item, {}, context),\n\t\tWhoReadIds(item, context)\n\t) | rpl::map([=](PeersWithReactions &&reacted, Peers &&read) {\n\t\tif (reacted.state == WhoReadState::Unknown\n\t\t\t|| read.state == WhoReadState::Unknown) {\n\t\t\treturn PeersWithReactions{ .state = WhoReadState::Unknown};\n\t\t}\n\t\tauto &list = reacted.list;\n\t\tfor (const auto &peerWithDate : read.list) {\n\t\t\tconst auto i = ranges::find(\n\t\t\t\tlist,\n\t\t\t\tpeerWithDate.peer,\n\t\t\t\t[](const PeerWithReaction &p) {\n\t\t\t\t\treturn p.peerWithDate.peer; });\n\t\t\tif (i == end(list)) {\n\t\t\t\tlist.push_back({ .peerWithDate = peerWithDate });\n\t\t\t} else if (!i->peerWithDate.date) {\n\t\t\t\ti->peerWithDate.date = peerWithDate.date;\n\t\t\t\ti->peerWithDate.dateReacted = peerWithDate.dateReacted;\n\t\t\t}\n\t\t}\n\t\treacted.read = std::move(read.list);\n\t\treturn std::move(reacted);\n\t});\n}\n\nbool UpdateUserpics(\n\t\tnot_null<State*> state,\n\t\tnot_null<HistoryItem*> item,\n\t\tconst std::vector<PeerWithReaction> &ids) {\n\tauto &owner = item->history()->owner();\n\n\tstruct ResolvedPeer {\n\t\tPeerData *peer = nullptr;\n\t\tTimeId date = 0;\n\t\tbool dateReacted = false;\n\t\tReactionId reaction;\n\t};\n\tconst auto peers = ranges::views::all(\n\t\tids\n\t) | ranges::views::transform([&](PeerWithReaction id) {\n\t\treturn ResolvedPeer{\n\t\t\t.peer = owner.peerLoaded(id.peerWithDate.peer),\n\t\t\t.date = id.peerWithDate.date,\n\t\t\t.dateReacted = id.peerWithDate.dateReacted,\n\t\t\t.reaction = id.reaction,\n\t\t};\n\t}) | ranges::views::filter([](ResolvedPeer resolved) {\n\t\treturn resolved.peer != nullptr;\n\t}) | ranges::to_vector;\n\n\tconst auto same = ranges::equal(\n\t\tstate->userpics,\n\t\tpeers,\n\t\tranges::equal_to(),\n\t\t[](const Userpic &u) { return std::pair(u.peer.get(), u.date); },\n\t\t[](const ResolvedPeer &r) { return std::pair(r.peer, r.date); });\n\tif (same) {\n\t\treturn false;\n\t}\n\tauto &was = state->userpics;\n\tauto now = std::vector<Userpic>();\n\tfor (const auto &resolved : peers) {\n\t\tconst auto peer = not_null{ resolved.peer };\n\t\tconst auto &data = ReactionEntityData(resolved.reaction);\n\t\tconst auto i = ranges::find(was, peer, &Userpic::peer);\n\t\tif (i != end(was) && i->view.cloud) {\n\t\t\ti->date = resolved.date;\n\t\t\ti->dateReacted = resolved.dateReacted;\n\t\t\tnow.push_back(std::move(*i));\n\t\t\tnow.back().customEntityData = data;\n\t\t\tcontinue;\n\t\t}\n\t\tnow.push_back(Userpic{\n\t\t\t.peer = peer,\n\t\t\t.date = resolved.date,\n\t\t\t.dateReacted = resolved.dateReacted,\n\t\t\t.customEntityData = data,\n\t\t});\n\t\tauto &userpic = now.back();\n\t\tuserpic.uniqueKey = peer->userpicUniqueKey(userpic.view);\n\t\tpeer->loadUserpic();\n\t}\n\twas = std::move(now);\n\treturn true;\n}\n\nvoid RegenerateUserpics(not_null<State*> state, int small, int large) {\n\tExpects(state->userpics.size() == state->current.participants.size());\n\n\tstate->someUserpicsNotLoaded = false;\n\tconst auto count = int(state->userpics.size());\n\tfor (auto i = 0; i != count; ++i) {\n\t\tauto &userpic = state->userpics[i];\n\t\tauto &participant = state->current.participants[i];\n\t\tconst auto peer = userpic.peer;\n\t\tconst auto key = peer->userpicUniqueKey(userpic.view);\n\t\tif (peer->hasUserpic() && peer->useEmptyUserpic(userpic.view)) {\n\t\t\tstate->someUserpicsNotLoaded = true;\n\t\t}\n\t\tif (userpic.uniqueKey == key) {\n\t\t\tcontinue;\n\t\t}\n\t\tparticipant.userpicKey = userpic.uniqueKey = key;\n\t\tparticipant.userpicLarge = GenerateUserpic(userpic, large);\n\t\tif (i < Ui::WhoReadParticipant::kMaxSmallUserpics) {\n\t\t\tparticipant.userpicSmall = GenerateUserpic(userpic, small);\n\t\t}\n\t}\n}\n\nvoid RegenerateParticipants(not_null<State*> state, int small, int large) {\n\tconst auto currentDate = QDateTime::currentDateTime();\n\tauto old = base::take(state->current.participants);\n\tauto &now = state->current.participants;\n\tnow.reserve(state->userpics.size());\n\tfor (auto &userpic : state->userpics) {\n\t\tconst auto peer = userpic.peer;\n\t\tconst auto date = userpic.date;\n\t\tconst auto id = peer->id.value;\n\t\tconst auto was = ranges::find(old, id, &Ui::WhoReadParticipant::id);\n\t\tif (was != end(old)) {\n\t\t\twas->name = peer->name();\n\t\t\twas->date = FormatReadDate(date, currentDate);\n\t\t\twas->dateReacted = userpic.dateReacted;\n\t\t\tnow.push_back(std::move(*was));\n\t\t\tcontinue;\n\t\t}\n\t\tnow.push_back({\n\t\t\t.name = peer->name(),\n\t\t\t.date = FormatReadDate(date, currentDate),\n\t\t\t.dateReacted = userpic.dateReacted,\n\t\t\t.customEntityData = userpic.customEntityData,\n\t\t\t.userpicLarge = GenerateUserpic(userpic, large),\n\t\t\t.userpicKey = userpic.uniqueKey,\n\t\t\t.id = id,\n\t\t});\n\t\tif (now.size() <= Ui::WhoReadParticipant::kMaxSmallUserpics) {\n\t\t\tnow.back().userpicSmall = GenerateUserpic(userpic, small);\n\t\t}\n\t}\n\tRegenerateUserpics(state, small, large);\n}\n\nrpl::producer<Ui::WhoReadContent> WhoReacted(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst ReactionId &reaction,\n\t\tnot_null<QWidget*> context,\n\t\tconst style::WhoRead &st,\n\t\tstd::shared_ptr<WhoReadList> whoReadIds) {\n\tconst auto small = st.userpics.size;\n\tconst auto large = st.photoSize;\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tconst auto resolveWhoRead = reaction.empty()\n\t\t\t&& WhoReadExists(item);\n\n\t\tconst auto state = lifetime.make_state<State>();\n\t\tconst auto pushNext = [=] {\n\t\t\tconsumer.put_next_copy(state->current);\n\t\t};\n\n\t\tconst auto resolveWhoReacted = !reaction.empty()\n\t\t\t|| item->canViewReactions();\n\t\tauto idsWithReactions = (resolveWhoRead && resolveWhoReacted)\n\t\t\t? WhoReadOrReactedIds(item, context)\n\t\t\t: resolveWhoRead\n\t\t\t? (WhoReadIds(item, context) | rpl::map(WithEmptyReactions))\n\t\t\t: WhoReactedIds(item, reaction, context);\n\t\tstate->current.type = resolveWhoRead\n\t\t\t? DetectSeenType(item)\n\t\t\t: Ui::WhoReadType::Reacted;\n\t\tif (resolveWhoReacted) {\n\t\t\tconst auto &list = item->reactions();\n\t\t\tstate->current.fullReactionsCount = [&] {\n\t\t\t\tif (reaction.empty()) {\n\t\t\t\t\treturn ranges::accumulate(\n\t\t\t\t\t\tlist,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tranges::plus{},\n\t\t\t\t\t\t&Data::MessageReaction::count);\n\t\t\t\t}\n\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\tlist,\n\t\t\t\t\treaction,\n\t\t\t\t\t&Data::MessageReaction::id);\n\t\t\t\treturn (i != end(list)) ? i->count : 0;\n\t\t\t}();\n\t\t\tstate->current.singleCustomEntityData = ReactionEntityData(\n\t\t\t\t!reaction.empty()\n\t\t\t\t? reaction\n\t\t\t\t: (list.size() == 1)\n\t\t\t\t? list.front().id\n\t\t\t\t: ReactionId());\n\t\t}\n\t\tstd::move(\n\t\t\tidsWithReactions\n\t\t) | rpl::on_next([=](PeersWithReactions &&peers) {\n\t\t\tif (peers.state == WhoReadState::Unknown) {\n\t\t\t\tstate->userpics.clear();\n\t\t\t\tconsumer.put_next(Ui::WhoReadContent{\n\t\t\t\t\t.type = state->current.type,\n\t\t\t\t\t.fullReactionsCount = state->current.fullReactionsCount,\n\t\t\t\t\t.fullReadCount = state->current.fullReadCount,\n\t\t\t\t\t.state = WhoReadState::Unknown,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->current.state = peers.state;\n\t\t\tstate->current.fullReadCount = int(peers.read.size());\n\t\t\tstate->current.fullReactionsCount = peers.fullReactionsCount;\n\t\t\tif (whoReadIds) {\n\t\t\t\tconst auto reacted = peers.list.size() - ranges::count(\n\t\t\t\t\tpeers.list,\n\t\t\t\t\tReactionId(),\n\t\t\t\t\t&PeerWithReaction::reaction);\n\t\t\t\twhoReadIds->list = (peers.read.size() > reacted)\n\t\t\t\t\t? std::move(peers.read)\n\t\t\t\t\t: std::vector<WhoReadPeer>();\n\t\t\t}\n\t\t\tif (UpdateUserpics(state, item, peers.list)) {\n\t\t\t\tRegenerateParticipants(state, small, large);\n\t\t\t\tpushNext();\n\t\t\t} else if (peers.list.empty()) {\n\t\t\t\tpushNext();\n\t\t\t}\n\t\t}, lifetime);\n\n\t\titem->history()->session().downloaderTaskFinished(\n\t\t) | rpl::filter([=] {\n\t\t\treturn state->someUserpicsNotLoaded && !state->scheduled;\n\t\t}) | rpl::on_next([=] {\n\t\t\tfor (const auto &userpic : state->userpics) {\n\t\t\t\tif (userpic.peer->userpicUniqueKey(userpic.view)\n\t\t\t\t\t!= userpic.uniqueKey) {\n\t\t\t\t\tstate->scheduled = true;\n\t\t\t\t\tcrl::on_main(&state->guard, [=] {\n\t\t\t\t\t\tstate->scheduled = false;\n\t\t\t\t\t\tRegenerateUserpics(state, small, large);\n\t\t\t\t\t\tpushNext();\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime);\n\n\t\treturn lifetime;\n\t};\n}\n\n} // namespace\n\nQString FormatReadDate(TimeId date, const QDateTime &now) {\n\tif (!date) {\n\t\treturn {};\n\t}\n\tconst auto parsed = base::unixtime::parse(date);\n\tconst auto readDate = parsed.date();\n\tconst auto nowDate = now.date();\n\tif (readDate == nowDate) {\n\t\treturn tr::lng_mediaview_today(\n\t\t\ttr::now,\n\t\t\tlt_time,\n\t\t\tQLocale().toString(parsed.time(), QLocale::ShortFormat));\n\t} else if (readDate.addDays(1) == nowDate) {\n\t\treturn tr::lng_mediaview_yesterday(\n\t\t\ttr::now,\n\t\t\tlt_time,\n\t\t\tQLocale().toString(parsed.time(), QLocale::ShortFormat));\n\t}\n\treturn tr::lng_mediaview_date_time(\n\t\ttr::now,\n\t\tlt_date,\n\t\tlangDayOfMonthShort(readDate),\n\t\tlt_time,\n\t\tQLocale().toString(parsed.time(), QLocale::ShortFormat));\n}\n\nbool WhoReadExists(not_null<HistoryItem*> item) {\n\tif (!item->out()) {\n\t\treturn false;\n\t}\n\tconst auto type = DetectSeenType(item);\n\tconst auto thread = item->topic()\n\t\t? (Data::Thread*)item->topic()\n\t\t: item->history();\n\tconst auto unseen = (type == Ui::WhoReadType::Seen)\n\t\t? item->unread(thread)\n\t\t: item->isUnreadMedia();\n\tif (unseen) {\n\t\treturn false;\n\t}\n\tconst auto history = item->history();\n\tconst auto peer = history->peer;\n\tif (const auto user = peer->asUser()) {\n\t\tif (user->isSelf()\n\t\t\t|| user->isBot()\n\t\t\t|| user->isServiceUser()\n\t\t\t|| user->readDatesPrivate()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto &appConfig = peer->session().appConfig();\n\t\tconst auto expirePeriod = appConfig.get<int>(\n\t\t\t\"pm_read_date_expire_period\",\n\t\t\t7 * 86400);\n\t\tif (item->date() + int64(expirePeriod) <= int64(base::unixtime::now())) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\tconst auto chat = peer->asChat();\n\tconst auto megagroup = peer->asMegagroup();\n\tif ((!chat && !megagroup)\n\t\t|| (megagroup\n\t\t\t&& (megagroup->flags() & ChannelDataFlag::ParticipantsHidden))\n\t\t|| (megagroup && megagroup->isMonoforum())) {\n\t\treturn false;\n\t}\n\tconst auto &appConfig = peer->session().appConfig();\n\tconst auto expirePeriod = appConfig.get<int>(\n\t\t\"chat_read_mark_expire_period\",\n\t\t7 * 86400);\n\tif (item->date() + int64(expirePeriod) <= int64(base::unixtime::now())) {\n\t\treturn false;\n\t}\n\tconst auto maxCount = appConfig.get<int>(\n\t\t\"chat_read_mark_size_threshold\",\n\t\t50);\n\tconst auto count = megagroup ? megagroup->membersCount() : chat->count;\n\tif (count <= 0 || count > maxCount) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nbool WhoReactedExists(\n\t\tnot_null<HistoryItem*> item,\n\t\tWhoReactedList list) {\n\tif (item->canViewReactions() || WhoReadExists(item)) {\n\t\treturn true;\n\t}\n\treturn (list == WhoReactedList::One) && item->history()->peer->isUser();\n}\n\nrpl::producer<Ui::WhoReadContent> WhoReacted(\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<QWidget*> context,\n\t\tconst style::WhoRead &st,\n\t\tstd::shared_ptr<WhoReadList> whoReadIds) {\n\treturn WhoReacted(item, {}, context, st, std::move(whoReadIds));\n}\n\nrpl::producer<Ui::WhoReadContent> WhoReacted(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst Data::ReactionId &reaction,\n\t\tnot_null<QWidget*> context,\n\t\tconst style::WhoRead &st) {\n\treturn WhoReacted(item, reaction, context, st, nullptr);\n}\n\n[[nodiscard]] rpl::producer<Ui::WhoReadContent> WhenDate(\n\t\tnot_null<PeerData*> author,\n\t\tTimeId date,\n\t\tUi::WhoReadType type) {\n\treturn rpl::single(Ui::WhoReadContent{\n\t\t.participants = { Ui::WhoReadParticipant{\n\t\t\t.name = author->name(),\n\t\t\t.date = FormatReadDate(date, QDateTime::currentDateTime()),\n\t\t\t.id = author->id.value,\n\t\t} },\n\t\t.type = type,\n\t\t.fullReadCount = 1,\n\t});\n}\n\nrpl::producer<Ui::WhoReadContent> WhenEdited(\n\t\tnot_null<PeerData*> author,\n\t\tTimeId date) {\n\treturn WhenDate(author, date, Ui::WhoReadType::Edited);\n}\n\nrpl::producer<Ui::WhoReadContent> WhenOriginal(\n\t\tnot_null<PeerData*> author,\n\t\tTimeId date) {\n\treturn WhenDate(author, date, Ui::WhoReadType::Original);\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/api/api_who_reacted.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass HistoryItem;\n\nnamespace style {\nstruct WhoRead;\n} // namespace style\n\nnamespace Ui {\nstruct WhoReadContent;\nenum class WhoReadType;\n} // namespace Ui\n\nnamespace Data {\nstruct ReactionId;\n} // namespace Data\n\nnamespace Api {\n\nenum class WhoReactedList {\n\tAll,\n\tOne,\n};\n\n[[nodiscard]] QString FormatReadDate(TimeId date, const QDateTime &now);\n[[nodiscard]] bool WhoReadExists(not_null<HistoryItem*> item);\n[[nodiscard]] bool WhoReactedExists(\n\tnot_null<HistoryItem*> item,\n\tWhoReactedList list);\n\nstruct WhoReadPeer {\n\tPeerId peer = 0;\n\tTimeId date = 0;\n\tbool dateReacted = false;\n\n\tfriend inline bool operator==(\n\t\tconst WhoReadPeer &a,\n\t\tconst WhoReadPeer &b) noexcept = default;\n};\n\nstruct WhoReadList {\n\tstd::vector<WhoReadPeer> list;\n\tUi::WhoReadType type = {};\n};\n\n// The context must be destroyed before the session holding this item.\n[[nodiscard]] rpl::producer<Ui::WhoReadContent> WhoReacted(\n\tnot_null<HistoryItem*> item,\n\tnot_null<QWidget*> context, // Cache results for this lifetime.\n\tconst style::WhoRead &st,\n\tstd::shared_ptr<WhoReadList> whoReadIds = nullptr);\n[[nodiscard]] rpl::producer<Ui::WhoReadContent> WhoReacted(\n\tnot_null<HistoryItem*> item,\n\tconst Data::ReactionId &reaction,\n\tnot_null<QWidget*> context, // Cache results for this lifetime.\n\tconst style::WhoRead &st);\n[[nodiscard]] rpl::producer<Ui::WhoReadContent> WhenEdited(\n\tnot_null<PeerData*> author,\n\tTimeId date);\n[[nodiscard]] rpl::producer<Ui::WhoReadContent> WhenOriginal(\n\tnot_null<PeerData*> author,\n\tTimeId date);\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/apiwrap.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"apiwrap.h\"\n\n#include \"api/api_authorizations.h\"\n#include \"api/api_attached_stickers.h\"\n#include \"api/api_blocked_peers.h\"\n#include \"api/api_chat_links.h\"\n#include \"api/api_chat_participants.h\"\n#include \"api/api_cloud_password.h\"\n#include \"api/api_hash.h\"\n#include \"api/api_invite_links.h\"\n#include \"api/api_media.h\"\n#include \"api/api_peer_colors.h\"\n#include \"api/api_peer_photo.h\"\n#include \"api/api_polls.h\"\n#include \"api/api_sending.h\"\n#include \"api/api_text_entities.h\"\n#include \"api/api_todo_lists.h\"\n#include \"api/api_self_destruct.h\"\n#include \"api/api_sensitive_content.h\"\n#include \"api/api_global_privacy.h\"\n#include \"api/api_reactions_notify_settings.h\"\n#include \"api/api_updates.h\"\n#include \"api/api_user_privacy.h\"\n#include \"api/api_read_metrics.h\"\n#include \"api/api_views.h\"\n#include \"api/api_confirm_phone.h\"\n#include \"api/api_unread_things.h\"\n#include \"api/api_ringtones.h\"\n#include \"api/api_compose_with_ai.h\"\n#include \"api/api_transcribes.h\"\n#include \"api/api_premium.h\"\n#include \"api/api_user_names.h\"\n#include \"api/api_websites.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/components/scheduled_messages.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_web_page.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_music.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_search_controller.h\"\n#include \"data/data_session.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_history_messages.h\"\n#include \"core/core_cloud_password.h\"\n#include \"core/application.h\"\n#include \"base/unixtime.h\"\n#include \"base/random.h\"\n#include \"base/call_delayed.h\"\n#include \"lang/lang_keys.h\"\n#include \"mainwidget.h\"\n#include \"boxes/add_contact_box.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"history/history.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"main/main_account.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"window/notifications_manager.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_lock_widgets.h\"\n#include \"window/window_session_controller.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"ui/item_text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/toast/toast.h\"\n#include \"support/support_helper.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"storage/localimageloader.h\"\n#include \"storage/download_manager_mtproto.h\"\n#include \"storage/file_upload.h\"\n#include \"storage/storage_account.h\"\n\nnamespace {\n\n// Save draft to the cloud with 1 sec extra delay.\nconstexpr auto kSaveCloudDraftTimeout = 1000;\n\nconstexpr auto kSmallDelayMs = 5;\nconstexpr auto kReadFeaturedSetsTimeout = crl::time(1000);\nconstexpr auto kFileLoaderQueueStopTimeout = crl::time(5000);\nconstexpr auto kStickersByEmojiInvalidateTimeout = crl::time(6 * 1000);\nconstexpr auto kNotifySettingSaveTimeout = crl::time(1000);\nconstexpr auto kDialogsFirstLoad = 20;\nconstexpr auto kDialogsPerPage = 500;\nconstexpr auto kStatsSessionKillTimeout = 10 * crl::time(1000);\n\nusing PhotoFileLocationId = Data::PhotoFileLocationId;\nusing DocumentFileLocationId = Data::DocumentFileLocationId;\nusing UpdatedFileReferences = Data::UpdatedFileReferences;\n\n[[nodiscard]] std::shared_ptr<ChatHelpers::Show> ShowForPeer(\n\t\tnot_null<PeerData*> peer) {\n\tif (const auto window = Core::App().windowFor(peer)) {\n\t\tif (const auto controller = window->sessionController()) {\n\t\t\tif (&controller->session() == &peer->session()) {\n\t\t\t\treturn controller->uiShow();\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid ShowChannelsLimitBox(not_null<PeerData*> peer) {\n\tif (const auto window = Core::App().windowFor(peer)) {\n\t\twindow->invokeForSessionController(\n\t\t\t&peer->session().account(),\n\t\t\tpeer,\n\t\t\t[&](not_null<Window::SessionController*> controller) {\n\t\t\t\tcontroller->show(Box(ChannelsLimitBox, &peer->session()));\n\t\t\t});\n\t}\n}\n\n[[nodiscard]] FileLoadTo FileLoadTaskOptions(const Api::SendAction &action) {\n\tconst auto peer = action.history->peer;\n\treturn FileLoadTo(\n\t\tpeer->id,\n\t\taction.options,\n\t\taction.replyTo,\n\t\taction.replaceMediaOf);\n}\n\n[[nodiscard]] QString FormatVideoTimestamp(TimeId seconds) {\n\tconst auto minutes = seconds / 60;\n\tconst auto hours = minutes / 60;\n\treturn hours\n\t\t? u\"%1h%2m%3s\"_q.arg(hours).arg(minutes % 60).arg(seconds % 60)\n\t\t: minutes\n\t\t? u\"%1m%2s\"_q.arg(minutes).arg(seconds % 60)\n\t\t: QString::number(seconds);\n}\n\n} // namespace\n\nnamespace Api {\n\nTimeId UnixtimeFromMsgId(mtpMsgId msgId) {\n\treturn TimeId(msgId >> 32);\n}\n\n} // namespace Api\n\nApiWrap::ApiWrap(not_null<Main::Session*> session)\n: MTP::Sender(&session->account().mtp())\n, _session(session)\n, _messageDataResolveDelayed([=] { resolveMessageDatas(); })\n, _webPagesTimer([=] { resolveWebPages(); })\n, _draftsSaveTimer([=] { saveDraftsToCloud(); })\n, _featuredSetsReadTimer([=] { readFeaturedSets(); })\n, _dialogsLoadState(std::make_unique<DialogsLoadState>())\n, _fileLoader(std::make_unique<TaskQueue>(kFileLoaderQueueStopTimeout))\n, _updateNotifyTimer([=] { sendNotifySettingsUpdates(); })\n, _statsSessionKillTimer([=] { checkStatsSessions(); })\n, _authorizations(std::make_unique<Api::Authorizations>(this))\n, _attachedStickers(std::make_unique<Api::AttachedStickers>(this))\n, _blockedPeers(std::make_unique<Api::BlockedPeers>(this))\n, _cloudPassword(std::make_unique<Api::CloudPassword>(this))\n, _selfDestruct(std::make_unique<Api::SelfDestruct>(this))\n, _sensitiveContent(std::make_unique<Api::SensitiveContent>(this))\n, _globalPrivacy(std::make_unique<Api::GlobalPrivacy>(this))\n, _reactionsNotifySettings(\n\tstd::make_unique<Api::ReactionsNotifySettings>(this))\n, _userPrivacy(std::make_unique<Api::UserPrivacy>(this))\n, _inviteLinks(std::make_unique<Api::InviteLinks>(this))\n, _chatLinks(std::make_unique<Api::ChatLinks>(this))\n, _views(std::make_unique<Api::ViewsManager>(this))\n, _readMetrics(std::make_unique<Api::ReadMetrics>(this))\n, _confirmPhone(std::make_unique<Api::ConfirmPhone>(this))\n, _peerPhoto(std::make_unique<Api::PeerPhoto>(this))\n, _polls(std::make_unique<Api::Polls>(this))\n, _todoLists(std::make_unique<Api::TodoLists>(this))\n, _chatParticipants(std::make_unique<Api::ChatParticipants>(this))\n, _unreadThings(std::make_unique<Api::UnreadThings>(this))\n, _ringtones(std::make_unique<Api::Ringtones>(this))\n, _composeWithAi(std::make_unique<Api::ComposeWithAi>(this))\n, _transcribes(std::make_unique<Api::Transcribes>(this))\n, _premium(std::make_unique<Api::Premium>(this))\n, _usernames(std::make_unique<Api::Usernames>(this))\n, _websites(std::make_unique<Api::Websites>(this))\n, _peerColors(std::make_unique<Api::PeerColors>(this)) {\n\tcrl::on_main(session, [=] {\n\t\t// You can't use _session->lifetime() in the constructor,\n\t\t// only queued, because it is not constructed yet.\n\t\t_session->data().chatsFilters().changed(\n\t\t) | rpl::filter([=] {\n\t\t\treturn _session->data().chatsFilters().archiveNeeded();\n\t\t}) | rpl::on_next([=] {\n\t\t\trequestMoreDialogsIfNeeded();\n\t\t}, _session->lifetime());\n\n\t\t_reactionsNotifySettings->reload();\n\t\tsetupSupportMode();\n\t});\n}\n\nApiWrap::~ApiWrap() = default;\n\nvoid ApiWrap::ProcessRecentSelfForwards(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPUpdates &updates,\n\t\tPeerId targetPeerId,\n\t\tPeerId fromPeerId) {\n\tauto newIds = MessageIdsList();\n\tupdates.match([&](const MTPDupdates &data) {\n\t\tfor (const auto &update : data.vupdates().v) {\n\t\t\tupdate.match([&](const MTPDupdateMessageID &d) {\n\t\t\t\tnewIds.push_back(FullMsgId(targetPeerId, d.vid().v));\n\t\t\t}, [](const auto &) {});\n\t\t}\n\t}, [](const auto &) {});\n\tif (!newIds.empty()) {\n\t\tsession->data().addRecentSelfForwards({\n\t\t\t.fromPeerId = fromPeerId,\n\t\t\t.ids = newIds,\n\t\t});\n\t}\n}\n\nMain::Session &ApiWrap::session() const {\n\treturn *_session;\n}\n\nStorage::Account &ApiWrap::local() const {\n\treturn _session->local();\n}\n\nApi::Updates &ApiWrap::updates() const {\n\treturn _session->updates();\n}\n\nvoid ApiWrap::setupSupportMode() {\n\tif (!_session->supportMode()) {\n\t\treturn;\n\t}\n\n\t_session->settings().supportChatsTimeSliceValue(\n\t) | rpl::on_next([=](int seconds) {\n\t\t_dialogsLoadTill = seconds ? std::max(base::unixtime::now() - seconds, 0) : 0;\n\t\trefreshDialogsLoadBlocked();\n\t}, _session->lifetime());\n}\n\nvoid ApiWrap::requestChangelog(\n\t\tconst QString &sinceVersion,\n\t\tFn<void(const MTPUpdates &result)> callback) {\n\t//request(MTPhelp_GetAppChangelog(\n\t//\tMTP_string(sinceVersion)\n\t//)).done(\n\t//\tcallback\n\t//).send();\n}\n\nvoid ApiWrap::requestDeepLinkInfo(\n\t\tconst QString &path,\n\t\tFn<void(TextWithEntities message, bool updateRequired)> callback) {\n\trequest(_deepLinkInfoRequestId).cancel();\n\t_deepLinkInfoRequestId = request(MTPhelp_GetDeepLinkInfo(\n\t\tMTP_string(path)\n\t)).done([=](const MTPhelp_DeepLinkInfo &result) {\n\t\t_deepLinkInfoRequestId = 0;\n\t\tif (result.type() == mtpc_help_deepLinkInfo) {\n\t\t\tconst auto &data = result.c_help_deepLinkInfo();\n\t\t\tcallback(TextWithEntities{\n\t\t\t\tqs(data.vmessage()),\n\t\t\t\tApi::EntitiesFromMTP(\n\t\t\t\t\t_session,\n\t\t\t\t\tdata.ventities().value_or_empty())\n\t\t\t}, data.is_update_app());\n\t\t}\n\t}).fail([=] {\n\t\t_deepLinkInfoRequestId = 0;\n\t}).send();\n}\n\nvoid ApiWrap::requestTermsUpdate() {\n\tif (_termsUpdateRequestId) {\n\t\treturn;\n\t}\n\tconst auto now = crl::now();\n\tif (_termsUpdateSendAt && now < _termsUpdateSendAt) {\n\t\tbase::call_delayed(_termsUpdateSendAt - now, _session, [=] {\n\t\t\trequestTermsUpdate();\n\t\t});\n\t\treturn;\n\t}\n\n\tconstexpr auto kTermsUpdateTimeoutMin = 10 * crl::time(1000);\n\tconstexpr auto kTermsUpdateTimeoutMax = 86400 * crl::time(1000);\n\n\t_termsUpdateRequestId = request(MTPhelp_GetTermsOfServiceUpdate(\n\t)).done([=](const MTPhelp_TermsOfServiceUpdate &result) {\n\t\t_termsUpdateRequestId = 0;\n\n\t\tconst auto requestNext = [&](auto &&data) {\n\t\t\tconst auto timeout = (data.vexpires().v - base::unixtime::now());\n\t\t\t_termsUpdateSendAt = crl::now() + std::clamp(\n\t\t\t\ttimeout * crl::time(1000),\n\t\t\t\tkTermsUpdateTimeoutMin,\n\t\t\t\tkTermsUpdateTimeoutMax);\n\t\t\trequestTermsUpdate();\n\t\t};\n\t\tswitch (result.type()) {\n\t\tcase mtpc_help_termsOfServiceUpdateEmpty: {\n\t\t\tconst auto &data = result.c_help_termsOfServiceUpdateEmpty();\n\t\t\trequestNext(data);\n\t\t} break;\n\t\tcase mtpc_help_termsOfServiceUpdate: {\n\t\t\tconst auto &data = result.c_help_termsOfServiceUpdate();\n\t\t\tconst auto &terms = data.vterms_of_service();\n\t\t\tconst auto &fields = terms.c_help_termsOfService();\n\t\t\tsession().lockByTerms(\n\t\t\t\tWindow::TermsLock::FromMTP(_session, fields));\n\t\t\trequestNext(data);\n\t\t} break;\n\t\tdefault: Unexpected(\"Type in requestTermsUpdate().\");\n\t\t}\n\t}).fail([=] {\n\t\t_termsUpdateRequestId = 0;\n\t\t_termsUpdateSendAt = crl::now() + kTermsUpdateTimeoutMin;\n\t\trequestTermsUpdate();\n\t}).send();\n}\n\nvoid ApiWrap::acceptTerms(bytes::const_span id) {\n\trequest(MTPhelp_AcceptTermsOfService(\n\t\tMTP_dataJSON(MTP_bytes(id))\n\t)).done([=] {\n\t\trequestTermsUpdate();\n\t}).send();\n}\n\nvoid ApiWrap::checkChatInvite(\n\t\tconst QString &hash,\n\t\tFnMut<void(const MTPChatInvite &)> done,\n\t\tFn<void(const MTP::Error &)> fail) {\n\trequest(base::take(_checkInviteRequestId)).cancel();\n\t_checkInviteRequestId = request(MTPmessages_CheckChatInvite(\n\t\tMTP_string(hash)\n\t)).done(std::move(done)).fail(std::move(fail)).handleFloodErrors().send();\n}\n\nvoid ApiWrap::checkFilterInvite(\n\t\tconst QString &slug,\n\t\tFnMut<void(const MTPchatlists_ChatlistInvite &)> done,\n\t\tFn<void(const MTP::Error &)> fail) {\n\trequest(base::take(_checkFilterInviteRequestId)).cancel();\n\t_checkFilterInviteRequestId = request(\n\t\tMTPchatlists_CheckChatlistInvite(MTP_string(slug))\n\t).done(std::move(done)).fail(std::move(fail)).send();\n}\n\nvoid ApiWrap::savePinnedOrder(Data::Folder *folder) {\n\tconst auto &order = _session->data().pinnedChatsOrder(folder);\n\tconst auto input = [](Dialogs::Key key) {\n\t\tif (const auto history = key.history()) {\n\t\t\treturn MTP_inputDialogPeer(history->peer->input());\n\t\t} else if (const auto folder = key.folder()) {\n\t\t\treturn MTP_inputDialogPeerFolder(MTP_int(folder->id()));\n\t\t}\n\t\tUnexpected(\"Key type in pinnedDialogsOrder().\");\n\t};\n\tauto peers = QVector<MTPInputDialogPeer>();\n\tpeers.reserve(order.size());\n\tranges::transform(\n\t\torder,\n\t\tranges::back_inserter(peers),\n\t\tinput);\n\trequest(MTPmessages_ReorderPinnedDialogs(\n\t\tMTP_flags(MTPmessages_ReorderPinnedDialogs::Flag::f_force),\n\t\tMTP_int(folder ? folder->id() : 0),\n\t\tMTP_vector(peers)\n\t)).send();\n}\n\nvoid ApiWrap::savePinnedOrder(not_null<Data::Forum*> forum) {\n\tconst auto &order = _session->data().pinnedChatsOrder(forum);\n\tconst auto input = [](Dialogs::Key key) {\n\t\tif (const auto topic = key.topic()) {\n\t\t\treturn MTP_int(topic->rootId().bare);\n\t\t}\n\t\tUnexpected(\"Key type in pinnedDialogsOrder().\");\n\t};\n\tauto topics = QVector<MTPint>();\n\ttopics.reserve(order.size());\n\tranges::transform(\n\t\torder,\n\t\tranges::back_inserter(topics),\n\t\tinput);\n\trequest(MTPmessages_ReorderPinnedForumTopics(\n\t\tMTP_flags(MTPmessages_ReorderPinnedForumTopics::Flag::f_force),\n\t\tforum->peer()->input(),\n\t\tMTP_vector(topics)\n\t)).done([=](const MTPUpdates &result) {\n\t\tapplyUpdates(result);\n\t}).send();\n}\n\nvoid ApiWrap::savePinnedOrder(not_null<Data::SavedMessages*> saved) {\n\tif (saved->parentChat()) {\n\t\treturn;\n\t}\n\tconst auto &order = _session->data().pinnedChatsOrder(saved);\n\tconst auto input = [](Dialogs::Key key) {\n\t\tif (const auto sublist = key.sublist()) {\n\t\t\treturn MTP_inputDialogPeer(sublist->sublistPeer()->input());\n\t\t}\n\t\tUnexpected(\"Key type in pinnedDialogsOrder().\");\n\t};\n\tauto peers = QVector<MTPInputDialogPeer>();\n\tpeers.reserve(order.size());\n\tranges::transform(\n\t\torder,\n\t\tranges::back_inserter(peers),\n\t\tinput);\n\trequest(MTPmessages_ReorderPinnedSavedDialogs(\n\t\tMTP_flags(MTPmessages_ReorderPinnedSavedDialogs::Flag::f_force),\n\t\tMTP_vector(peers)\n\t)).send();\n}\n\nvoid ApiWrap::toggleHistoryArchived(\n\t\tnot_null<History*> history,\n\t\tbool archived,\n\t\tFn<void()> callback) {\n\tif (const auto already = _historyArchivedRequests.take(history)) {\n\t\trequest(already->first).cancel();\n\t}\n\tconst auto isPinned = history->isPinnedDialog(0);\n\tconst auto archiveId = Data::Folder::kId;\n\tconst auto requestId = request(MTPfolders_EditPeerFolders(\n\t\tMTP_vector<MTPInputFolderPeer>(\n\t\t\t1,\n\t\t\tMTP_inputFolderPeer(\n\t\t\t\thistory->peer->input(),\n\t\t\t\tMTP_int(archived ? archiveId : 0)))\n\t)).done([=](const MTPUpdates &result) {\n\t\tapplyUpdates(result);\n\t\tif (archived) {\n\t\t\thistory->setFolder(_session->data().folder(archiveId));\n\t\t} else {\n\t\t\thistory->clearFolder();\n\t\t}\n\t\tif (const auto data = _historyArchivedRequests.take(history)) {\n\t\t\tdata->second();\n\t\t}\n\t\tif (isPinned) {\n\t\t\t_session->data().notifyPinnedDialogsOrderUpdated();\n\t\t}\n\t}).fail([=] {\n\t\t_historyArchivedRequests.remove(history);\n\t}).send();\n\t_historyArchivedRequests.emplace(history, requestId, callback);\n}\n\nvoid ApiWrap::sendMessageFail(\n\t\tconst MTP::Error &error,\n\t\tnot_null<PeerData*> peer,\n\t\tuint64 randomId,\n\t\tFullMsgId itemId) {\n\tsendMessageFail(error.type(), peer, randomId, itemId);\n}\n\nvoid ApiWrap::sendMessageFail(\n\t\tconst QString &error,\n\t\tnot_null<PeerData*> peer,\n\t\tuint64 randomId,\n\t\tFullMsgId itemId) {\n\tconst auto show = ShowForPeer(peer);\n\tconst auto paidStarsPrefix = u\"ALLOW_PAYMENT_REQUIRED_\"_q;\n\tif (show && error == u\"PEER_FLOOD\"_q) {\n\t\tshow->showBox(\n\t\t\tUi::MakeInformBox(\n\t\t\t\tPeerFloodErrorText(&session(), PeerFloodType::Send)),\n\t\t\tUi::LayerOption::CloseOther);\n\t} else if (show && error == u\"USER_BANNED_IN_CHANNEL\"_q) {\n\t\tconst auto link = tr::link(\n\t\t\ttr::lng_cant_more_info(tr::now),\n\t\t\tsession().createInternalLinkFull(u\"spambot\"_q));\n\t\tshow->showBox(\n\t\t\tUi::MakeInformBox(\n\t\t\t\ttr::lng_error_public_groups_denied(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_more_info,\n\t\t\t\t\tlink,\n\t\t\t\t\ttr::marked)),\n\t\t\tUi::LayerOption::CloseOther);\n\t} else if (error.startsWith(u\"SLOWMODE_WAIT_\"_q)) {\n\t\tconst auto chop = u\"SLOWMODE_WAIT_\"_q.size();\n\t\tconst auto left = base::StringViewMid(error, chop).toInt();\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\tconst auto seconds = channel->slowmodeSeconds();\n\t\t\tif (seconds >= left) {\n\t\t\t\tchannel->growSlowmodeLastMessage(\n\t\t\t\t\tbase::unixtime::now() - (left - seconds));\n\t\t\t} else {\n\t\t\t\trequestFullPeer(peer);\n\t\t\t}\n\t\t}\n\t} else if (error == u\"SCHEDULE_STATUS_PRIVATE\"_q) {\n\t\tauto &scheduled = _session->scheduledMessages();\n\t\tAssert(peer->isUser());\n\t\tif (const auto item = scheduled.lookupItem(peer->id, itemId.msg)) {\n\t\t\tscheduled.removeSending(item);\n\t\t\tif (show) {\n\t\t\t\tshow->showBox(\n\t\t\t\t\tUi::MakeInformBox(tr::lng_cant_do_this()),\n\t\t\t\t\tUi::LayerOption::CloseOther);\n\t\t\t}\n\t\t}\n\t} else if (show && error == u\"CHAT_FORWARDS_RESTRICTED\"_q) {\n\t\tshow->showToast(peer->isBroadcast()\n\t\t\t? tr::lng_error_noforwards_channel(tr::now)\n\t\t\t: peer->isUser()\n\t\t\t? tr::lng_error_noforwards_user(tr::now)\n\t\t\t: tr::lng_error_noforwards_group(tr::now), kJoinErrorDuration);\n\t} else if (error == u\"PREMIUM_ACCOUNT_REQUIRED\"_q) {\n\t\tSettings::ShowPremium(&session(), \"premium_stickers\");\n\t} else if (error == u\"SCHEDULE_TOO_MUCH\"_q) {\n\t\tauto &scheduled = _session->scheduledMessages();\n\t\tif (const auto item = scheduled.lookupItem(peer->id, itemId.msg)) {\n\t\t\tscheduled.removeSending(item);\n\t\t}\n\t\tif (show) {\n\t\t\tshow->showToast(tr::lng_error_schedule_limit(tr::now));\n\t\t}\n\t} else if (error.startsWith(paidStarsPrefix)) {\n\t\tif (show) {\n\t\t\tshow->showToast(\n\t\t\t\tu\"Payment requirements changed. Please, try again.\"_q);\n\t\t}\n\t\tif (const auto stars = error.mid(paidStarsPrefix.size()).toInt()) {\n\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\tuser->setStarsPerMessage(stars);\n\t\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\t\tchannel->setStarsPerMessage(stars);\n\t\t\t}\n\t\t}\n\t\tpeer->updateFull();\n\t} else if (show) {\n\t\tshow->showToast(error);\n\t}\n\tif (const auto item = _session->data().message(itemId)) {\n\t\tAssert(randomId != 0);\n\t\t_session->data().unregisterMessageRandomId(randomId);\n\t\titem->sendFailed();\n\n\t\tif (error == u\"TOPIC_CLOSED\"_q) {\n\t\t\tif (const auto topic = item->topic()) {\n\t\t\t\ttopic->setClosed(true);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ApiWrap::requestMessageData(\n\t\tPeerData *peer,\n\t\tMsgId msgId,\n\t\tFn<void()> done) {\n\tauto &requests = (peer && peer->isChannel())\n\t\t? _channelMessageDataRequests[peer->asChannel()][msgId]\n\t\t: _messageDataRequests[msgId];\n\tif (done) {\n\t\trequests.callbacks.push_back(std::move(done));\n\t}\n\tif (!requests.requestId) {\n\t\t_messageDataResolveDelayed.call();\n\t}\n}\n\nQVector<MTPInputMessage> ApiWrap::collectMessageIds(\n\t\tconst MessageDataRequests &requests) {\n\tauto result = QVector<MTPInputMessage>();\n\tresult.reserve(requests.size());\n\tfor (const auto &[msgId, request] : requests) {\n\t\tif (request.requestId > 0) {\n\t\t\tcontinue;\n\t\t}\n\t\tresult.push_back(MTP_inputMessageID(MTP_int(msgId)));\n\t}\n\treturn result;\n}\n\nauto ApiWrap::messageDataRequests(ChannelData *channel, bool onlyExisting)\n-> MessageDataRequests* {\n\tif (!channel) {\n\t\treturn &_messageDataRequests;\n\t}\n\tconst auto i = _channelMessageDataRequests.find(channel);\n\tif (i != end(_channelMessageDataRequests)) {\n\t\treturn &i->second;\n\t} else if (onlyExisting) {\n\t\treturn nullptr;\n\t}\n\treturn &_channelMessageDataRequests.emplace(\n\t\tchannel,\n\t\tMessageDataRequests()\n\t).first->second;\n}\n\nvoid ApiWrap::resolveMessageDatas() {\n\tif (_messageDataRequests.empty() && _channelMessageDataRequests.empty()) {\n\t\treturn;\n\t}\n\n\tconst auto ids = collectMessageIds(_messageDataRequests);\n\tif (!ids.isEmpty()) {\n\t\tconst auto requestId = request(MTPmessages_GetMessages(\n\t\t\tMTP_vector<MTPInputMessage>(ids)\n\t\t)).done([=](\n\t\t\t\tconst MTPmessages_Messages &result,\n\t\t\t\tmtpRequestId requestId) {\n\t\t\t_session->data().processExistingMessages(nullptr, result);\n\t\t\tfinalizeMessageDataRequest(nullptr, requestId);\n\t\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\t\tfinalizeMessageDataRequest(nullptr, requestId);\n\t\t}).afterDelay(kSmallDelayMs).send();\n\n\t\tfor (auto &[msgId, request] : _messageDataRequests) {\n\t\t\tif (request.requestId > 0) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\trequest.requestId = requestId;\n\t\t}\n\t}\n\tfor (auto j = _channelMessageDataRequests.begin(); j != _channelMessageDataRequests.cend();) {\n\t\tif (j->second.empty()) {\n\t\t\tj = _channelMessageDataRequests.erase(j);\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto ids = collectMessageIds(j->second);\n\t\tif (!ids.isEmpty()) {\n\t\t\tconst auto channel = j->first;\n\t\t\tconst auto requestId = request(MTPchannels_GetMessages(\n\t\t\t\tchannel->inputChannel(),\n\t\t\t\tMTP_vector<MTPInputMessage>(ids)\n\t\t\t)).done([=](\n\t\t\t\t\tconst MTPmessages_Messages &result,\n\t\t\t\t\tmtpRequestId requestId) {\n\t\t\t\t_session->data().processExistingMessages(channel, result);\n\t\t\t\tfinalizeMessageDataRequest(channel, requestId);\n\t\t\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\t\t\tfinalizeMessageDataRequest(channel, requestId);\n\t\t\t}).afterDelay(kSmallDelayMs).send();\n\n\t\t\tfor (auto &[msgId, request] : j->second) {\n\t\t\t\tif (request.requestId > 0) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\trequest.requestId = requestId;\n\t\t\t}\n\t\t}\n\t\t++j;\n\t}\n}\n\nvoid ApiWrap::finalizeMessageDataRequest(\n\t\tChannelData *channel,\n\t\tmtpRequestId requestId) {\n\tauto requests = messageDataRequests(channel, true);\n\tif (!requests) {\n\t\treturn;\n\t}\n\tauto callbacks = std::vector<Fn<void()>>();\n\tfor (auto i = requests->begin(); i != requests->cend();) {\n\t\tif (i->second.requestId == requestId) {\n\t\t\tauto &list = i->second.callbacks;\n\t\t\tif (callbacks.empty()) {\n\t\t\t\tcallbacks = std::move(list);\n\t\t\t} else {\n\t\t\t\tcallbacks.insert(\n\t\t\t\t\tend(callbacks),\n\t\t\t\t\tstd::make_move_iterator(begin(list)),\n\t\t\t\t\tstd::make_move_iterator(end(list)));\n\t\t\t}\n\t\t\ti = requests->erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tif (channel && requests->empty()) {\n\t\t_channelMessageDataRequests.remove(channel);\n\t}\n\tfor (const auto &callback : callbacks) {\n\t\tcallback();\n\t}\n}\n\nQString ApiWrap::exportDirectMessageLink(\n\t\tnot_null<HistoryItem*> item,\n\t\tbool inRepliesContext,\n\t\tbool forceNonPublicLink,\n\t\tstd::optional<TimeId> videoTimestamp) {\n\tExpects(item->history()->peer->isChannel());\n\n\tconst auto itemId = item->fullId();\n\tconst auto channel = item->history()->peer->asChannel();\n\tconst auto fallback = [&] {\n\t\tauto linkChannel = channel;\n\t\tauto linkItemId = item->id;\n\t\tauto linkCommentId = MsgId();\n\t\tauto linkThreadId = MsgId();\n\t\tauto linkThreadIsTopic = false;\n\t\tif (inRepliesContext) {\n\t\t\tlinkThreadIsTopic = item->history()->isForum();\n\t\t\tconst auto rootId = linkThreadIsTopic\n\t\t\t\t? item->topicRootId()\n\t\t\t\t: item->replyToTop();\n\t\t\tif (rootId) {\n\t\t\t\tconst auto root = item->history()->owner().message(\n\t\t\t\t\tchannel->id,\n\t\t\t\t\trootId);\n\t\t\t\tconst auto sender = root\n\t\t\t\t\t? root->discussionPostOriginalSender()\n\t\t\t\t\t: nullptr;\n\t\t\t\tif (sender && sender->hasUsername() && !forceNonPublicLink) {\n\t\t\t\t\t// Comment to a public channel.\n\t\t\t\t\tconst auto forwarded = root->Get<HistoryMessageForwarded>();\n\t\t\t\t\tlinkItemId = forwarded->savedFromMsgId;\n\t\t\t\t\tif (linkItemId) {\n\t\t\t\t\t\tlinkChannel = sender;\n\t\t\t\t\t\tlinkCommentId = item->id;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlinkItemId = item->id;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Reply in a thread, maybe comment in a private channel.\n\t\t\t\t\tlinkThreadId = rootId;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst auto base = (linkChannel->hasUsername() && !forceNonPublicLink)\n\t\t\t? linkChannel->username()\n\t\t\t: \"c/\" + QString::number(peerToChannel(linkChannel->id).bare);\n\t\tconst auto post = QString::number(linkItemId.bare);\n\t\tconst auto query = base\n\t\t\t+ '/'\n\t\t\t+ (linkCommentId\n\t\t\t\t? (post + \"?comment=\" + QString::number(linkCommentId.bare))\n\t\t\t\t: (linkThreadId && !linkThreadIsTopic)\n\t\t\t\t? (post + \"?thread=\" + QString::number(linkThreadId.bare))\n\t\t\t\t: linkThreadId\n\t\t\t\t? (QString::number(linkThreadId.bare) + '/' + post)\n\t\t\t\t: post);\n\t\treturn session().createInternalLinkFull(query);\n\t};\n\tif (forceNonPublicLink) {\n\t\treturn fallback();\n\t}\n\tconst auto i = _unlikelyMessageLinks.find(itemId);\n\tconst auto current = (i != end(_unlikelyMessageLinks))\n\t\t? i->second\n\t\t: fallback();\n\trequest(MTPchannels_ExportMessageLink(\n\t\tMTP_flags(inRepliesContext\n\t\t\t? MTPchannels_ExportMessageLink::Flag::f_thread\n\t\t\t: MTPchannels_ExportMessageLink::Flag(0)),\n\t\tchannel->inputChannel(),\n\t\tMTP_int(item->id)\n\t)).done([=](const MTPExportedMessageLink &result) {\n\t\tconst auto link = qs(result.data().vlink());\n\t\tif (current != link) {\n\t\t\t_unlikelyMessageLinks.emplace_or_assign(itemId, link);\n\t\t}\n\t}).send();\n\tconst auto addTimestamp = channel->hasUsername()\n\t\t&& !inRepliesContext\n\t\t&& videoTimestamp.has_value();\n\tconst auto addedSeparator = (current.indexOf('?') >= 0) ? '&' : '?';\n\tconst auto addedTimestamp = addTimestamp\n\t\t? (addedSeparator + u\"t=\"_q + FormatVideoTimestamp(*videoTimestamp))\n\t\t: QString();\n\treturn current + addedTimestamp;\n}\n\nQString ApiWrap::exportDirectStoryLink(not_null<Data::Story*> story) {\n\tconst auto storyId = story->fullId();\n\tconst auto peer = story->peer();\n\tconst auto fallback = [&] {\n\t\tconst auto base = peer->username();\n\t\tconst auto id = story->call()\n\t\t\t? u\"live\"_q\n\t\t\t: QString::number(storyId.story);\n\t\tconst auto query = base + \"/s/\" + id;\n\t\treturn session().createInternalLinkFull(query);\n\t};\n\tconst auto i = _unlikelyStoryLinks.find(storyId);\n\tconst auto current = (i != end(_unlikelyStoryLinks))\n\t\t? i->second\n\t\t: fallback();\n\trequest(MTPstories_ExportStoryLink(\n\t\tpeer->input(),\n\t\tMTP_int(story->id())\n\t)).done([=](const MTPExportedStoryLink &result) {\n\t\tconst auto link = qs(result.data().vlink());\n\t\tif (current != link) {\n\t\t\t_unlikelyStoryLinks.emplace_or_assign(storyId, link);\n\t\t}\n\t}).send();\n\treturn current;\n}\n\nvoid ApiWrap::requestContacts() {\n\tif (_session->data().contactsLoaded().current() || _contactsRequestId) {\n\t\treturn;\n\t}\n\t_contactsRequestId = request(MTPcontacts_GetContacts(\n\t\tMTP_long(0) // hash\n\t)).done([=](const MTPcontacts_Contacts &result) {\n\t\t_contactsRequestId = 0;\n\t\tif (result.type() == mtpc_contacts_contactsNotModified) {\n\t\t\treturn;\n\t\t}\n\t\tAssert(result.type() == mtpc_contacts_contacts);\n\t\tconst auto &d = result.c_contacts_contacts();\n\t\t_session->data().processUsers(d.vusers());\n\t\tfor (const auto &contact : d.vcontacts().v) {\n\t\t\tif (contact.type() != mtpc_contact) continue;\n\n\t\t\tconst auto userId = UserId(contact.c_contact().vuser_id());\n\t\t\tif (userId == _session->userId()) {\n\t\t\t\t_session->user()->setIsContact(true);\n\t\t\t}\n\t\t}\n\t\t_session->data().contactsLoaded() = true;\n\t}).fail([=] {\n\t\t_contactsRequestId = 0;\n\t}).send();\n}\n\nvoid ApiWrap::requestDialogs(Data::Folder *folder) {\n\tif (folder && !_foldersLoadState.contains(folder)) {\n\t\t_foldersLoadState.emplace(folder, DialogsLoadState());\n\t}\n\trequestMoreDialogs(folder);\n}\n\nvoid ApiWrap::requestMoreDialogs(Data::Folder *folder) {\n\tconst auto state = dialogsLoadState(folder);\n\tif (!state) {\n\t\treturn;\n\t} else if (state->requestId) {\n\t\treturn;\n\t} else if (_dialogsLoadBlockedByDate.current()) {\n\t\treturn;\n\t}\n\n\tconst auto firstLoad = !state->offsetDate;\n\tconst auto loadCount = firstLoad ? kDialogsFirstLoad : kDialogsPerPage;\n\tconst auto flags = MTPmessages_GetDialogs::Flag::f_exclude_pinned\n\t\t| MTPmessages_GetDialogs::Flag::f_folder_id;\n\tconst auto hash = uint64(0);\n\tstate->requestId = request(MTPmessages_GetDialogs(\n\t\tMTP_flags(flags),\n\t\tMTP_int(folder ? folder->id() : 0),\n\t\tMTP_int(state->offsetDate),\n\t\tMTP_int(state->offsetId),\n\t\t(state->offsetPeer\n\t\t\t? state->offsetPeer->input()\n\t\t\t: MTP_inputPeerEmpty()),\n\t\tMTP_int(loadCount),\n\t\tMTP_long(hash)\n\t)).done([=](const MTPmessages_Dialogs &result) {\n\t\tconst auto state = dialogsLoadState(folder);\n\t\tconst auto count = result.match([](\n\t\t\t\tconst MTPDmessages_dialogsNotModified &) {\n\t\t\tLOG((\"API Error: not-modified received for requested dialogs.\"));\n\t\t\treturn 0;\n\t\t}, [&](const MTPDmessages_dialogs &data) {\n\t\t\tif (state) {\n\t\t\t\tstate->listReceived = true;\n\t\t\t\tdialogsLoadFinish(folder); // may kill 'state'.\n\t\t\t}\n\t\t\treturn int(data.vdialogs().v.size());\n\t\t}, [&](const MTPDmessages_dialogsSlice &data) {\n\t\t\tupdateDialogsOffset(\n\t\t\t\tfolder,\n\t\t\t\tdata.vdialogs().v,\n\t\t\t\tdata.vmessages().v);\n\t\t\treturn data.vcount().v;\n\t\t});\n\t\tresult.match([](const MTPDmessages_dialogsNotModified & data) {\n\t\t\tLOG((\"API Error: not-modified received for requested dialogs.\"));\n\t\t}, [&](const auto &data) {\n\t\t\t_session->data().processUsers(data.vusers());\n\t\t\t_session->data().processChats(data.vchats());\n\t\t\t_session->data().applyDialogs(\n\t\t\t\tfolder,\n\t\t\t\tdata.vmessages().v,\n\t\t\t\tdata.vdialogs().v,\n\t\t\t\tcount);\n\t\t});\n\n\t\tif (!folder\n\t\t\t&& (!_dialogsLoadState || !_dialogsLoadState->listReceived)) {\n\t\t\trefreshDialogsLoadBlocked();\n\t\t}\n\t\trequestMoreDialogsIfNeeded();\n\t\t_session->data().chatsListChanged(folder);\n\t}).fail([=] {\n\t\tdialogsLoadState(folder)->requestId = 0;\n\t}).send();\n\n\tif (!state->pinnedReceived) {\n\t\trequestPinnedDialogs(folder);\n\t}\n\tif (!folder) {\n\t\trefreshDialogsLoadBlocked();\n\t}\n}\n\nvoid ApiWrap::refreshDialogsLoadBlocked() {\n\t_dialogsLoadMayBlockByDate = _dialogsLoadState\n\t\t&& !_dialogsLoadState->listReceived\n\t\t&& (_dialogsLoadTill > 0);\n\t_dialogsLoadBlockedByDate = _dialogsLoadState\n\t\t&& !_dialogsLoadState->listReceived\n\t\t&& !_dialogsLoadState->requestId\n\t\t&& (_dialogsLoadTill > 0)\n\t\t&& (_dialogsLoadState->offsetDate > 0)\n\t\t&& (_dialogsLoadState->offsetDate <= _dialogsLoadTill);\n}\n\nvoid ApiWrap::requestMoreDialogsIfNeeded() {\n\tconst auto dialogsReady = !_dialogsLoadState\n\t\t|| _dialogsLoadState->listReceived;\n\tif (_session->data().chatsFilters().loadNextExceptions(dialogsReady)) {\n\t\treturn;\n\t} else if (_dialogsLoadState && !_dialogsLoadState->listReceived) {\n\t\tif (_dialogsLoadState->requestId) {\n\t\t\treturn;\n\t\t}\n\t\trequestDialogs(nullptr);\n\t} else if (const auto folder = _session->data().folderLoaded(\n\t\t\tData::Folder::kId)) {\n\t\tif (_session->data().chatsFilters().archiveNeeded()) {\n\t\t\trequestMoreDialogs(folder);\n\t\t}\n\t}\n\trequestContacts();\n\t_session->data().shortcutMessages().preloadShortcuts();\n}\n\nvoid ApiWrap::updateDialogsOffset(\n\t\tData::Folder *folder,\n\t\tconst QVector<MTPDialog> &dialogs,\n\t\tconst QVector<MTPMessage> &messages) {\n\tauto lastDate = TimeId(0);\n\tauto lastPeer = PeerId(0);\n\tauto lastMsgId = MsgId(0);\n\tfor (const auto &dialog : ranges::views::reverse(dialogs)) {\n\t\tdialog.match([&](const auto &dialog) {\n\t\t\tconst auto peer = peerFromMTP(dialog.vpeer());\n\t\t\tconst auto messageId = dialog.vtop_message().v;\n\t\t\tif (!peer || !messageId) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!lastPeer) {\n\t\t\t\tlastPeer = peer;\n\t\t\t}\n\t\t\tif (!lastMsgId) {\n\t\t\t\tlastMsgId = messageId;\n\t\t\t}\n\t\t\tfor (const auto &message : ranges::views::reverse(messages)) {\n\t\t\t\tif (IdFromMessage(message) == messageId\n\t\t\t\t\t&& PeerFromMessage(message) == peer) {\n\t\t\t\t\tif (const auto date = DateFromMessage(message)) {\n\t\t\t\t\t\tlastDate = date;\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tif (lastDate) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (const auto state = dialogsLoadState(folder)) {\n\t\tif (lastDate) {\n\t\t\tstate->offsetDate = lastDate;\n\t\t\tstate->offsetId = lastMsgId;\n\t\t\tstate->offsetPeer = _session->data().peer(lastPeer);\n\t\t\tstate->requestId = 0;\n\t\t} else {\n\t\t\tstate->listReceived = true;\n\t\t\tdialogsLoadFinish(folder);\n\t\t}\n\t}\n}\n\nauto ApiWrap::dialogsLoadState(Data::Folder *folder) -> DialogsLoadState* {\n\tif (!folder) {\n\t\treturn _dialogsLoadState.get();\n\t}\n\tconst auto i = _foldersLoadState.find(folder);\n\treturn (i != end(_foldersLoadState)) ? &i->second : nullptr;\n}\n\nvoid ApiWrap::dialogsLoadFinish(Data::Folder *folder) {\n\tconst auto notify = [&] {\n\t\tCore::App().postponeCall(crl::guard(_session, [=] {\n\t\t\t_session->data().chatsListDone(folder);\n\t\t}));\n\t};\n\tconst auto state = dialogsLoadState(folder);\n\tif (!state || !state->listReceived || !state->pinnedReceived) {\n\t\treturn;\n\t}\n\tif (folder) {\n\t\t_foldersLoadState.remove(folder);\n\t\tnotify();\n\t} else {\n\t\t_dialogsLoadState = nullptr;\n\t\tnotify();\n\t}\n}\n\nvoid ApiWrap::requestPinnedDialogs(Data::Folder *folder) {\n\tconst auto state = dialogsLoadState(folder);\n\tif (!state || state->pinnedReceived || state->pinnedRequestId) {\n\t\treturn;\n\t}\n\n\tconst auto finalize = [=] {\n\t\tif (const auto state = dialogsLoadState(folder)) {\n\t\t\tstate->pinnedRequestId = 0;\n\t\t\tstate->pinnedReceived = true;\n\t\t\tdialogsLoadFinish(folder);\n\t\t}\n\t};\n\tstate->pinnedRequestId = request(MTPmessages_GetPinnedDialogs(\n\t\tMTP_int(folder ? folder->id() : 0)\n\t)).done([=](const MTPmessages_PeerDialogs &result) {\n\t\tfinalize();\n\t\tresult.match([&](const MTPDmessages_peerDialogs &data) {\n\t\t\t_session->data().processUsers(data.vusers());\n\t\t\t_session->data().processChats(data.vchats());\n\t\t\t_session->data().clearPinnedChats(folder);\n\t\t\t_session->data().applyDialogs(\n\t\t\t\tfolder,\n\t\t\t\tdata.vmessages().v,\n\t\t\t\tdata.vdialogs().v);\n\t\t\t_session->data().chatsListChanged(folder);\n\t\t\t_session->data().notifyPinnedDialogsOrderUpdated();\n\t\t});\n\t}).fail([=] {\n\t\tfinalize();\n\t}).send();\n}\n\nvoid ApiWrap::requestMoreBlockedByDateDialogs() {\n\tif (!_dialogsLoadState) {\n\t\treturn;\n\t}\n\tconst auto max = _session->settings().supportChatsTimeSlice();\n\t_dialogsLoadTill = _dialogsLoadState->offsetDate\n\t\t? (_dialogsLoadState->offsetDate - max)\n\t\t: (base::unixtime::now() - max);\n\trefreshDialogsLoadBlocked();\n\trequestDialogs();\n}\n\nrpl::producer<bool> ApiWrap::dialogsLoadMayBlockByDate() const {\n\treturn _dialogsLoadMayBlockByDate.value();\n}\n\nrpl::producer<bool> ApiWrap::dialogsLoadBlockedByDate() const {\n\treturn _dialogsLoadBlockedByDate.value();\n}\n\nvoid ApiWrap::requestWallPaper(\n\t\tconst QString &slug,\n\t\tFn<void(const Data::WallPaper &)> done,\n\t\tFn<void()> fail) {\n\tif (_wallPaperSlug != slug) {\n\t\t_wallPaperSlug = slug;\n\t\tif (_wallPaperRequestId) {\n\t\t\trequest(base::take(_wallPaperRequestId)).cancel();\n\t\t}\n\t}\n\t_wallPaperDone = std::move(done);\n\t_wallPaperFail = std::move(fail);\n\tif (_wallPaperRequestId) {\n\t\treturn;\n\t}\n\t_wallPaperRequestId = request(MTPaccount_GetWallPaper(\n\t\tMTP_inputWallPaperSlug(MTP_string(slug))\n\t)).done([=](const MTPWallPaper &result) {\n\t\t_wallPaperRequestId = 0;\n\t\t_wallPaperSlug = QString();\n\t\tif (const auto paper = Data::WallPaper::Create(_session, result)) {\n\t\t\tif (const auto done = base::take(_wallPaperDone)) {\n\t\t\t\tdone(*paper);\n\t\t\t}\n\t\t} else if (const auto fail = base::take(_wallPaperFail)) {\n\t\t\tfail();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\t_wallPaperRequestId = 0;\n\t\t_wallPaperSlug = QString();\n\t\tif (const auto fail = base::take(_wallPaperFail)) {\n\t\t\tfail();\n\t\t}\n\t}).send();\n}\n\nvoid ApiWrap::requestFullPeer(not_null<PeerData*> peer) {\n\tif (_fullPeerRequests.contains(peer)) {\n\t\treturn;\n\t} else if (!peer->isUser() && !peer->barSettings().has_value()) {\n\t\trequestPeerSettings(peer);\n\t}\n\n\tconst auto requestId = [&] {\n\t\tconst auto failHandler = [=](const MTP::Error &error) {\n\t\t\t_fullPeerRequests.remove(peer);\n\t\t\tmigrateFail(peer, error.type());\n\t\t};\n\t\tif (const auto user = peer->asUser()) {\n\t\t\tif (_session->supportMode()) {\n\t\t\t\t_session->supportHelper().refreshInfo(user);\n\t\t\t}\n\t\t\treturn request(MTPusers_GetFullUser(\n\t\t\t\tuser->inputUser()\n\t\t\t)).done([=](const MTPusers_UserFull &result) {\n\t\t\t\tresult.match([&](const MTPDusers_userFull &data) {\n\t\t\t\t\t_session->data().processUsers(data.vusers());\n\t\t\t\t\t_session->data().processChats(data.vchats());\n\t\t\t\t});\n\t\t\t\tgotUserFull(user, result);\n\t\t\t}).fail(failHandler).send();\n\t\t} else if (const auto chat = peer->asChat()) {\n\t\t\treturn request(MTPmessages_GetFullChat(\n\t\t\t\tchat->inputChat()\n\t\t\t)).done([=](const MTPmessages_ChatFull &result) {\n\t\t\t\tgotChatFull(peer, result);\n\t\t\t}).fail(failHandler).send();\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\treturn request(MTPchannels_GetFullChannel(\n\t\t\t\tchannel->inputChannel()\n\t\t\t)).done([=](const MTPmessages_ChatFull &result) {\n\t\t\t\tgotChatFull(peer, result);\n\t\t\t\tmigrateDone(channel, channel);\n\t\t\t}).fail(failHandler).send();\n\t\t}\n\t\tUnexpected(\"Peer type in requestFullPeer.\");\n\t}();\n\t_fullPeerRequests.emplace(peer, requestId);\n}\n\nvoid ApiWrap::processFullPeer(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPmessages_ChatFull &result) {\n\tgotChatFull(peer, result);\n}\n\nvoid ApiWrap::gotChatFull(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPmessages_ChatFull &result) {\n\tconst auto &d = result.c_messages_chatFull();\n\t_session->data().applyMaximumChatVersions(d.vchats());\n\n\t_session->data().processUsers(d.vusers());\n\t_session->data().processChats(d.vchats());\n\n\td.vfull_chat().match([&](const MTPDchatFull &data) {\n\t\tif (const auto chat = peer->asChat()) {\n\t\t\tData::ApplyChatUpdate(chat, data);\n\t\t} else {\n\t\t\tLOG((\"MTP Error: bad type in gotChatFull for channel: %1\"\n\t\t\t\t).arg(d.vfull_chat().type()));\n\t\t}\n\t}, [&](const MTPDchannelFull &data) {\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\tData::ApplyChannelUpdate(channel, data);\n\t\t} else {\n\t\t\tLOG((\"MTP Error: bad type in gotChatFull for chat: %1\"\n\t\t\t\t).arg(d.vfull_chat().type()));\n\t\t}\n\t});\n\n\t_fullPeerRequests.remove(peer);\n\t_session->changes().peerUpdated(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::FullInfo);\n}\n\nvoid ApiWrap::gotUserFull(\n\t\tnot_null<UserData*> user,\n\t\tconst MTPusers_UserFull &result) {\n\tresult.match([&](const MTPDusers_userFull &data) {\n\t\tdata.vfull_user().match([&](const MTPDuserFull &fields) {\n\t\t\tif (user == _session->user() && !_session->validateSelf(fields.vid().v)) {\n\t\t\t\tconstexpr auto kRequestUserAgainTimeout = crl::time(10000);\n\t\t\t\tbase::call_delayed(kRequestUserAgainTimeout, _session, [=] {\n\t\t\t\t\trequestFullPeer(user);\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tData::ApplyUserUpdate(user, fields);\n\t\t});\n\t});\n\t_fullPeerRequests.remove(user);\n\t_session->changes().peerUpdated(\n\t\tuser,\n\t\tData::PeerUpdate::Flag::FullInfo);\n}\n\nvoid ApiWrap::requestPeerSettings(not_null<PeerData*> peer) {\n\tif (!_requestedPeerSettings.emplace(peer).second) {\n\t\treturn;\n\t} else if (peer->isMonoforum()) {\n\t\tpeer->setBarSettings(PeerBarSettings());\n\t\t_requestedPeerSettings.erase(peer);\n\t\treturn;\n\t}\n\trequest(MTPmessages_GetPeerSettings(\n\t\tpeer->input()\n\t)).done([=](const MTPmessages_PeerSettings &result) {\n\t\tresult.match([&](const MTPDmessages_peerSettings &data) {\n\t\t\t_session->data().processUsers(data.vusers());\n\t\t\t_session->data().processChats(data.vchats());\n\t\t\tpeer->setBarSettings(data.vsettings());\n\t\t\t_requestedPeerSettings.erase(peer);\n\t\t});\n\t}).fail([=] {\n\t\tpeer->setBarSettings(PeerBarSettings());\n\t\t_requestedPeerSettings.erase(peer);\n\t}).send();\n}\n\nvoid ApiWrap::migrateChat(\n\t\tnot_null<ChatData*> chat,\n\t\tFnMut<void(not_null<ChannelData*>)> done,\n\t\tFn<void(const QString &)> fail) {\n\tconst auto callback = [&] {\n\t\treturn MigrateCallbacks{ std::move(done), std::move(fail) };\n\t};\n\tconst auto i = _migrateCallbacks.find(chat);\n\tif (i != _migrateCallbacks.end()) {\n\t\ti->second.push_back(callback());\n\t\treturn;\n\t}\n\t_migrateCallbacks.emplace(chat).first->second.push_back(callback());\n\tif (const auto channel = chat->migrateTo()) {\n\t\tsession().changes().peerUpdated(\n\t\t\tchat,\n\t\t\tData::PeerUpdate::Flag::Migration);\n\t\tcrl::on_main([=] {\n\t\t\tmigrateDone(chat, channel);\n\t\t});\n\t} else if (chat->isDeactivated()) {\n\t\tcrl::on_main([=] {\n\t\t\tmigrateFail(\n\t\t\t\tchat,\n\t\t\t\tMTP::Error::Local(\n\t\t\t\t\t\"BAD_MIGRATION\",\n\t\t\t\t\t\"Chat is already deactivated\").type());\n\t\t});\n\t\treturn;\n\t} else if (!chat->amCreator()) {\n\t\tcrl::on_main([=] {\n\t\t\tmigrateFail(\n\t\t\t\tchat,\n\t\t\t\tMTP::Error::Local(\n\t\t\t\t\t\"BAD_MIGRATION\",\n\t\t\t\t\t\"Current user is not the creator of that chat\").type());\n\t\t});\n\t\treturn;\n\t}\n\n\trequest(MTPmessages_MigrateChat(\n\t\tchat->inputChat()\n\t)).done([=](const MTPUpdates &result) {\n\t\tapplyUpdates(result);\n\t\tsession().changes().sendNotifications();\n\n\t\tif (const auto channel = chat->migrateTo()) {\n\t\t\tif (auto handlers = _migrateCallbacks.take(chat)) {\n\t\t\t\t_migrateCallbacks.emplace(channel, std::move(*handlers));\n\t\t\t}\n\t\t\trequestFullPeer(channel);\n\t\t} else {\n\t\t\tmigrateFail(\n\t\t\t\tchat,\n\t\t\t\tMTP::Error::Local(\"MIGRATION_FAIL\", \"No channel\").type());\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tmigrateFail(chat, error.type());\n\t}).send();\n}\n\nvoid ApiWrap::migrateDone(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<ChannelData*> channel) {\n\tsession().changes().sendNotifications();\n\tif (auto handlers = _migrateCallbacks.take(peer)) {\n\t\tfor (auto &handler : *handlers) {\n\t\t\tif (handler.done) {\n\t\t\t\thandler.done(channel);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ApiWrap::migrateFail(not_null<PeerData*> peer, const QString &error) {\n\tif (error == u\"CHANNELS_TOO_MUCH\"_q) {\n\t\tShowChannelsLimitBox(peer);\n\t}\n\tif (auto handlers = _migrateCallbacks.take(peer)) {\n\t\tfor (auto &handler : *handlers) {\n\t\t\tif (handler.fail) {\n\t\t\t\thandler.fail(error);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ApiWrap::markContentsRead(\n\t\tconst base::flat_set<not_null<HistoryItem*>> &items) {\n\tauto markedIds = QVector<MTPint>();\n\tauto channelMarkedIds = base::flat_map<\n\t\tnot_null<ChannelData*>,\n\t\tQVector<MTPint>>();\n\tmarkedIds.reserve(items.size());\n\tfor (const auto &item : items) {\n\t\tif (!item->markContentsRead(true) || !item->isRegular()) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (const auto channel = item->history()->peer->asChannel()) {\n\t\t\tchannelMarkedIds[channel].push_back(MTP_int(item->id));\n\t\t} else {\n\t\t\tmarkedIds.push_back(MTP_int(item->id));\n\t\t}\n\t}\n\tif (!markedIds.isEmpty()) {\n\t\trequest(MTPmessages_ReadMessageContents(\n\t\t\tMTP_vector<MTPint>(markedIds)\n\t\t)).done([=](const MTPmessages_AffectedMessages &result) {\n\t\t\tapplyAffectedMessages(result);\n\t\t}).send();\n\t}\n\tfor (const auto &channelIds : channelMarkedIds) {\n\t\trequest(MTPchannels_ReadMessageContents(\n\t\t\tchannelIds.first->inputChannel(),\n\t\t\tMTP_vector<MTPint>(channelIds.second)\n\t\t)).send();\n\t}\n}\n\nvoid ApiWrap::markContentsRead(not_null<HistoryItem*> item) {\n\tif (!item->markContentsRead(true) || !item->isRegular()) {\n\t\treturn;\n\t}\n\tconst auto ids = MTP_vector<MTPint>(1, MTP_int(item->id));\n\tif (const auto channel = item->history()->peer->asChannel()) {\n\t\trequest(MTPchannels_ReadMessageContents(\n\t\t\tchannel->inputChannel(),\n\t\t\tids\n\t\t)).send();\n\t} else {\n\t\trequest(MTPmessages_ReadMessageContents(\n\t\t\tids\n\t\t)).done([=](const MTPmessages_AffectedMessages &result) {\n\t\t\tapplyAffectedMessages(result);\n\t\t}).send();\n\t}\n}\n\nvoid ApiWrap::deleteAllFromParticipant(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<PeerData*> from) {\n\tconst auto history = _session->data().historyLoaded(channel);\n\tconst auto ids = history\n\t\t? history->collectMessagesFromParticipantToDelete(from)\n\t\t: std::vector<MsgId>();\n\tfor (const auto &msgId : ids) {\n\t\tif (const auto item = _session->data().message(channel->id, msgId)) {\n\t\t\titem->destroy();\n\t\t}\n\t}\n\n\t_session->data().sendHistoryChangeNotifications();\n\n\tdeleteAllFromParticipantSend(channel, from);\n}\n\nvoid ApiWrap::deleteAllFromParticipantSend(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<PeerData*> from) {\n\trequest(MTPchannels_DeleteParticipantHistory(\n\t\tchannel->inputChannel(),\n\t\tfrom->input()\n\t)).done([=](const MTPmessages_AffectedHistory &result) {\n\t\tconst auto offset = applyAffectedHistory(channel, result);\n\t\tif (offset > 0) {\n\t\t\tdeleteAllFromParticipantSend(channel, from);\n\t\t} else if (const auto history = _session->data().historyLoaded(channel)) {\n\t\t\thistory->requestChatListMessage();\n\t\t}\n\t}).send();\n}\n\nvoid ApiWrap::deleteSublistHistory(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<PeerData*> sublistPeer) {\n\tdeleteSublistHistorySend(channel, sublistPeer);\n}\n\nvoid ApiWrap::deleteSublistHistorySend(\n\t\tnot_null<ChannelData*> parentChat,\n\t\tnot_null<PeerData*> sublistPeer) {\n\trequest(MTPmessages_DeleteSavedHistory(\n\t\tMTP_flags(MTPmessages_DeleteSavedHistory::Flag::f_parent_peer),\n\t\tparentChat->input(),\n\t\tsublistPeer->input(),\n\t\tMTP_int(0), // max_id\n\t\tMTP_int(0), // min_date\n\t\tMTP_int(0) // max_date\n\t)).done([=](const MTPmessages_AffectedHistory &result) {\n\t\tconst auto offset = applyAffectedHistory(parentChat, result);\n\t\tif (offset > 0) {\n\t\t\tdeleteSublistHistorySend(parentChat, sublistPeer);\n\t\t} else if (const auto monoforum = parentChat->monoforum()) {\n\t\t\tmonoforum->applySublistDeleted(sublistPeer);\n\t\t}\n\t}).send();\n}\n\nvoid ApiWrap::scheduleStickerSetRequest(uint64 setId, uint64 access) {\n\tif (!_stickerSetRequests.contains(setId)) {\n\t\t_stickerSetRequests.emplace(setId, StickerSetRequest{ access });\n\t}\n}\n\nvoid ApiWrap::requestStickerSets() {\n\tfor (auto &[id, info] : _stickerSetRequests) {\n\t\tif (info.id) {\n\t\t\tcontinue;\n\t\t}\n\t\tinfo.id = request(MTPmessages_GetStickerSet(\n\t\t\tMTP_inputStickerSetID(\n\t\t\t\tMTP_long(id),\n\t\t\t\tMTP_long(info.accessHash)),\n\t\t\tMTP_int(0) // hash\n\t\t)).done([=, setId = id](const MTPmessages_StickerSet &result) {\n\t\t\tgotStickerSet(setId, result);\n\t\t}).fail([=, setId = id] {\n\t\t\t_stickerSetRequests.remove(setId);\n\t\t}).afterDelay(kSmallDelayMs).send();\n\t}\n}\n\nvoid ApiWrap::saveStickerSets(\n\t\tconst Data::StickersSetsOrder &localOrder,\n\t\tconst Data::StickersSetsOrder &localRemoved,\n\t\tData::StickersType type) {\n\tauto &setDisenableRequests = (type == Data::StickersType::Emoji)\n\t\t? _customEmojiSetDisenableRequests\n\t\t: (type == Data::StickersType::Masks)\n\t\t? _maskSetDisenableRequests\n\t\t: _stickerSetDisenableRequests;\n\tconst auto reorderRequestId = [=]() -> mtpRequestId & {\n\t\treturn (type == Data::StickersType::Emoji)\n\t\t\t? _customEmojiReorderRequestId\n\t\t\t: (type == Data::StickersType::Masks)\n\t\t\t? _masksReorderRequestId\n\t\t\t: _stickersReorderRequestId;\n\t};\n\tfor (auto requestId : base::take(setDisenableRequests)) {\n\t\trequest(requestId).cancel();\n\t}\n\trequest(base::take(reorderRequestId())).cancel();\n\trequest(base::take(_stickersClearRecentRequestId)).cancel();\n\trequest(base::take(_stickersClearRecentAttachedRequestId)).cancel();\n\n\tconst auto stickersSaveOrder = [=] {\n\t\tif (localOrder.size() < 2) {\n\t\t\treturn;\n\t\t}\n\t\tQVector<MTPlong> mtpOrder;\n\t\tmtpOrder.reserve(localOrder.size());\n\t\tfor (const auto setId : std::as_const(localOrder)) {\n\t\t\tmtpOrder.push_back(MTP_long(setId));\n\t\t}\n\n\t\tusing Flag = MTPmessages_ReorderStickerSets::Flag;\n\t\tconst auto flags = (type == Data::StickersType::Emoji)\n\t\t\t? Flag::f_emojis\n\t\t\t: (type == Data::StickersType::Masks)\n\t\t\t? Flag::f_masks\n\t\t\t: Flag(0);\n\t\treorderRequestId() = request(MTPmessages_ReorderStickerSets(\n\t\t\tMTP_flags(flags),\n\t\t\tMTP_vector<MTPlong>(mtpOrder)\n\t\t)).done([=] {\n\t\t\treorderRequestId() = 0;\n\t\t}).fail([=] {\n\t\t\treorderRequestId() = 0;\n\t\t\tif (type == Data::StickersType::Emoji) {\n\t\t\t\t_session->data().stickers().setLastEmojiUpdate(0);\n\t\t\t\tupdateCustomEmoji();\n\t\t\t} else if (type == Data::StickersType::Masks) {\n\t\t\t\t_session->data().stickers().setLastMasksUpdate(0);\n\t\t\t\tupdateMasks();\n\t\t\t} else {\n\t\t\t\t_session->data().stickers().setLastUpdate(0);\n\t\t\t\tupdateStickers();\n\t\t\t}\n\t\t}).send();\n\t};\n\n\tconst auto stickerSetDisenabled = [=](mtpRequestId requestId) {\n\t\tauto &setDisenableRequests = (type == Data::StickersType::Emoji)\n\t\t\t? _customEmojiSetDisenableRequests\n\t\t\t: (type == Data::StickersType::Masks)\n\t\t\t? _maskSetDisenableRequests\n\t\t\t: _stickerSetDisenableRequests;\n\t\tsetDisenableRequests.remove(requestId);\n\t\tif (setDisenableRequests.empty()) {\n\t\t\tstickersSaveOrder();\n\t\t}\n\t};\n\n\tauto writeInstalled = true,\n\t\twriteRecent = false,\n\t\twriteCloudRecent = false,\n\t\twriteCloudRecentAttached = false,\n\t\twriteFaved = false,\n\t\twriteArchived = false;\n\tauto &recent = _session->data().stickers().getRecentPack();\n\tauto &sets = _session->data().stickers().setsRef();\n\n\tauto &order = (type == Data::StickersType::Emoji)\n\t\t? _session->data().stickers().emojiSetsOrder()\n\t\t: (type == Data::StickersType::Masks)\n\t\t? _session->data().stickers().maskSetsOrder()\n\t\t: _session->data().stickers().setsOrder();\n\tauto &orderRef = (type == Data::StickersType::Emoji)\n\t\t? _session->data().stickers().emojiSetsOrderRef()\n\t\t: (type == Data::StickersType::Masks)\n\t\t? _session->data().stickers().maskSetsOrderRef()\n\t\t: _session->data().stickers().setsOrderRef();\n\n\tusing Flag = Data::StickersSetFlag;\n\tfor (const auto removedSetId : localRemoved) {\n\t\tif ((removedSetId == Data::Stickers::CloudRecentSetId)\n\t\t\t|| (removedSetId == Data::Stickers::CloudRecentAttachedSetId)) {\n\t\t\tif (sets.remove(Data::Stickers::CloudRecentSetId) != 0) {\n\t\t\t\twriteCloudRecent = true;\n\t\t\t}\n\t\t\tif (sets.remove(Data::Stickers::CloudRecentAttachedSetId) != 0) {\n\t\t\t\twriteCloudRecentAttached = true;\n\t\t\t}\n\t\t\tif (sets.remove(Data::Stickers::CustomSetId)) {\n\t\t\t\twriteInstalled = true;\n\t\t\t}\n\t\t\tif (!recent.isEmpty()) {\n\t\t\t\trecent.clear();\n\t\t\t\twriteRecent = true;\n\t\t\t}\n\n\t\t\tconst auto isAttached\n\t\t\t\t= (removedSetId == Data::Stickers::CloudRecentAttachedSetId);\n\t\t\tconst auto flags = isAttached\n\t\t\t\t? MTPmessages_ClearRecentStickers::Flag::f_attached\n\t\t\t\t: MTPmessages_ClearRecentStickers::Flags(0);\n\t\t\tauto &requestId = isAttached\n\t\t\t\t? _stickersClearRecentAttachedRequestId\n\t\t\t\t: _stickersClearRecentRequestId;\n\t\t\tconst auto finish = [=] {\n\t\t\t\t(isAttached\n\t\t\t\t\t? _stickersClearRecentAttachedRequestId\n\t\t\t\t\t: _stickersClearRecentRequestId) = 0;\n\t\t\t};\n\t\t\trequestId = request(MTPmessages_ClearRecentStickers(\n\t\t\t\tMTP_flags(flags)\n\t\t\t)).done(finish).fail(finish).send();\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto it = sets.find(removedSetId);\n\t\tif (it != sets.cend()) {\n\t\t\tconst auto set = it->second.get();\n\t\t\tfor (auto i = recent.begin(); i != recent.cend();) {\n\t\t\t\tif (set->stickers.indexOf(i->first) >= 0) {\n\t\t\t\t\ti = recent.erase(i);\n\t\t\t\t\twriteRecent = true;\n\t\t\t\t} else {\n\t\t\t\t\t++i;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto archived = !!(set->flags & Flag::Archived);\n\t\t\tif (!archived) {\n\t\t\t\tconst auto featured = !!(set->flags & Flag::Featured);\n\t\t\t\tconst auto special = !!(set->flags & Flag::Special);\n\t\t\t\tconst auto emoji = !!(set->flags & Flag::Emoji);\n\t\t\t\tconst auto locked = (set->locked > 0);\n\t\t\t\tconst auto setId = set->mtpInput();\n\n\t\t\t\tauto requestId = request(MTPmessages_UninstallStickerSet(\n\t\t\t\t\tsetId\n\t\t\t\t)).done([=](const MTPBool &result, mtpRequestId requestId) {\n\t\t\t\t\tstickerSetDisenabled(requestId);\n\t\t\t\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\t\t\t\tstickerSetDisenabled(requestId);\n\t\t\t\t}).afterDelay(kSmallDelayMs).send();\n\n\t\t\t\tsetDisenableRequests.insert(requestId);\n\n\t\t\t\tconst auto removeIndex = order.indexOf(set->id);\n\t\t\t\tif (removeIndex >= 0) {\n\t\t\t\t\torderRef.removeAt(removeIndex);\n\t\t\t\t}\n\t\t\t\tif (!featured && !special && !emoji && !locked) {\n\t\t\t\t\tsets.erase(it);\n\t\t\t\t} else {\n\t\t\t\t\tif (archived) {\n\t\t\t\t\t\twriteArchived = true;\n\t\t\t\t\t}\n\t\t\t\t\tset->flags &= ~(Flag::Installed | Flag::Archived);\n\t\t\t\t\tset->installDate = TimeId(0);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Clear all installed flags, set only for sets from order.\n\tfor (auto &[id, set] : sets) {\n\t\tconst auto archived = !!(set->flags & Flag::Archived);\n\t\tconst auto thatType = !!(set->flags & Flag::Emoji)\n\t\t\t? Data::StickersType::Emoji\n\t\t\t: !!(set->flags & Flag::Masks)\n\t\t\t? Data::StickersType::Masks\n\t\t\t: Data::StickersType::Stickers;\n\t\tif (!archived && (type == thatType)) {\n\t\t\tset->flags &= ~Flag::Installed;\n\t\t}\n\t}\n\n\torderRef.clear();\n\tfor (const auto setId : std::as_const(localOrder)) {\n\t\tauto it = sets.find(setId);\n\t\tif (it == sets.cend()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto set = it->second.get();\n\t\tconst auto archived = !!(set->flags & Flag::Archived);\n\t\tif (archived && !localRemoved.contains(set->id)) {\n\t\t\tconst auto mtpSetId = set->mtpInput();\n\n\t\t\tconst auto requestId = request(MTPmessages_InstallStickerSet(\n\t\t\t\tmtpSetId,\n\t\t\t\tMTP_boolFalse()\n\t\t\t)).done([=](\n\t\t\t\t\tconst MTPmessages_StickerSetInstallResult &result,\n\t\t\t\t\tmtpRequestId requestId) {\n\t\t\t\tstickerSetDisenabled(requestId);\n\t\t\t}).fail([=](\n\t\t\t\t\tconst MTP::Error &error,\n\t\t\t\t\tmtpRequestId requestId) {\n\t\t\t\tstickerSetDisenabled(requestId);\n\t\t\t}).afterDelay(kSmallDelayMs).send();\n\n\t\t\tsetDisenableRequests.insert(requestId);\n\n\t\t\tset->flags &= ~Flag::Archived;\n\t\t\twriteArchived = true;\n\t\t}\n\t\torderRef.push_back(setId);\n\t\tset->flags |= Flag::Installed;\n\t\tif (!set->installDate) {\n\t\t\tset->installDate = base::unixtime::now();\n\t\t}\n\t}\n\n\tfor (auto it = sets.begin(); it != sets.cend();) {\n\t\tconst auto set = it->second.get();\n\t\tif ((set->flags & Flag::Featured)\n\t\t\t|| (set->flags & Flag::Installed)\n\t\t\t|| (set->flags & Flag::Archived)\n\t\t\t|| (set->flags & Flag::Special)\n\t\t\t|| (set->flags & Flag::Emoji)\n\t\t\t|| (set->locked > 0)) {\n\t\t\t++it;\n\t\t} else {\n\t\t\tit = sets.erase(it);\n\t\t}\n\t}\n\n\tauto &storage = local();\n\tif (writeInstalled) {\n\t\tif (type == Data::StickersType::Emoji) {\n\t\t\tstorage.writeInstalledCustomEmoji();\n\t\t} else if (type == Data::StickersType::Masks) {\n\t\t\tstorage.writeInstalledMasks();\n\t\t} else {\n\t\t\tstorage.writeInstalledStickers();\n\t\t}\n\t}\n\tif (writeRecent) {\n\t\tsession().saveSettings();\n\t}\n\tif (writeArchived) {\n\t\tif (type == Data::StickersType::Emoji) {\n\t\t} else if (type == Data::StickersType::Masks) {\n\t\t\tstorage.writeArchivedMasks();\n\t\t} else {\n\t\t\tstorage.writeArchivedStickers();\n\t\t}\n\t}\n\tif (writeCloudRecent) {\n\t\tstorage.writeRecentStickers();\n\t}\n\tif (writeCloudRecentAttached) {\n\t\tstorage.writeRecentMasks();\n\t}\n\tif (writeFaved) {\n\t\tstorage.writeFavedStickers();\n\t}\n\t_session->data().stickers().notifyUpdated(type);\n\n\tif (setDisenableRequests.empty()) {\n\t\tstickersSaveOrder();\n\t} else {\n\t\trequestSendDelayed();\n\t}\n}\n\nvoid ApiWrap::joinChannel(not_null<ChannelData*> channel) {\n\tif (channel->amIn()) {\n\t\tsession().changes().peerUpdated(\n\t\t\tchannel,\n\t\t\tData::PeerUpdate::Flag::ChannelAmIn);\n\t} else if (!_channelAmInRequests.contains(channel)) {\n\t\tconst auto requestId = request(MTPchannels_JoinChannel(\n\t\t\tchannel->inputChannel()\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t_channelAmInRequests.remove(channel);\n\t\t\tapplyUpdates(result);\n\n\t\t\tsession().data().addRecentJoinChat({\n\t\t\t\t.fromPeerId = channel->id,\n\t\t\t\t.joinedPeerId = channel->id,\n\t\t\t});\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconst auto &type = error.type();\n\n\t\t\tconst auto show = ShowForPeer(channel);\n\t\t\tif (type == u\"CHANNEL_PRIVATE\"_q\n\t\t\t\t&& channel->invitePeekExpires()) {\n\t\t\t\tchannel->privateErrorReceived();\n\t\t\t} else if (type == u\"CHANNELS_TOO_MUCH\"_q) {\n\t\t\t\tShowChannelsLimitBox(channel);\n\t\t\t} else {\n\t\t\t\tconst auto text = [&] {\n\t\t\t\t\tif (type == u\"INVITE_REQUEST_SENT\"_q) {\n\t\t\t\t\t\treturn channel->isMegagroup()\n\t\t\t\t\t\t\t? tr::lng_group_request_sent(tr::now)\n\t\t\t\t\t\t\t: tr::lng_group_request_sent_channel(tr::now);\n\t\t\t\t\t} else if (type == u\"CHANNEL_PRIVATE\"_q\n\t\t\t\t\t\t|| type == u\"CHANNEL_PUBLIC_GROUP_NA\"_q\n\t\t\t\t\t\t|| type == u\"USER_BANNED_IN_CHANNEL\"_q) {\n\t\t\t\t\t\treturn channel->isMegagroup()\n\t\t\t\t\t\t\t? tr::lng_group_not_accessible(tr::now)\n\t\t\t\t\t\t\t: tr::lng_channel_not_accessible(tr::now);\n\t\t\t\t\t} else if (type == u\"USERS_TOO_MUCH\"_q) {\n\t\t\t\t\t\treturn tr::lng_group_full(tr::now);\n\t\t\t\t\t}\n\t\t\t\t\treturn QString();\n\t\t\t\t}();\n\t\t\t\tif (show && !text.isEmpty()) {\n\t\t\t\t\tshow->showToast(text, kJoinErrorDuration);\n\t\t\t\t}\n\t\t\t}\n\t\t\t_channelAmInRequests.remove(channel);\n\t\t}).send();\n\n\t\t_channelAmInRequests.emplace(channel, requestId);\n\n\t\tusing Flag = ChannelDataFlag;\n\t\tchatParticipants().loadSimilarPeers(channel);\n\t\tchannel->setFlags(channel->flags() | Flag::SimilarExpanded);\n\t}\n}\n\nvoid ApiWrap::leaveChannel(not_null<ChannelData*> channel) {\n\tif (!channel->amIn()) {\n\t\tsession().changes().peerUpdated(\n\t\t\tchannel,\n\t\t\tData::PeerUpdate::Flag::ChannelAmIn);\n\t} else if (!_channelAmInRequests.contains(channel)) {\n\t\tauto requestId = request(MTPchannels_LeaveChannel(\n\t\t\tchannel->inputChannel()\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t_channelAmInRequests.remove(channel);\n\t\t\tapplyUpdates(result);\n\t\t}).fail([=] {\n\t\t\t_channelAmInRequests.remove(channel);\n\t\t}).send();\n\n\t\t_channelAmInRequests.emplace(channel, requestId);\n\t}\n}\n\nvoid ApiWrap::requestNotifySettings(const MTPInputNotifyPeer &peer) {\n\tconst auto bad = peer.match([](const MTPDinputNotifyUsers &) {\n\t\treturn false;\n\t}, [](const MTPDinputNotifyChats &) {\n\t\treturn false;\n\t}, [](const MTPDinputNotifyBroadcasts &) {\n\t\treturn false;\n\t}, [&](const MTPDinputNotifyPeer &data) {\n\t\tif (data.vpeer().type() == mtpc_inputPeerEmpty) {\n\t\t\tLOG((\"Api Error: Requesting settings for empty peer.\"));\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}, [&](const MTPDinputNotifyForumTopic &data) {\n\t\tif (data.vpeer().type() == mtpc_inputPeerEmpty) {\n\t\t\tLOG((\"Api Error: Requesting settings for empty peer topic.\"));\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t});\n\tif (bad) {\n\t\treturn;\n\t}\n\n\tconst auto peerFromInput = [&](const MTPInputPeer &inputPeer) {\n\t\treturn inputPeer.match([&](const MTPDinputPeerSelf &) {\n\t\t\treturn _session->userPeerId();\n\t\t}, [](const MTPDinputPeerEmpty &) {\n\t\t\treturn PeerId(0);\n\t\t}, [](const MTPDinputPeerChannel &data) {\n\t\t\treturn peerFromChannel(data.vchannel_id());\n\t\t}, [](const MTPDinputPeerChannelFromMessage &data) {\n\t\t\treturn peerFromChannel(data.vchannel_id());\n\t\t}, [](const MTPDinputPeerChat &data) {\n\t\t\treturn peerFromChat(data.vchat_id());\n\t\t}, [](const MTPDinputPeerUser &data) {\n\t\t\treturn peerFromUser(data.vuser_id());\n\t\t}, [](const MTPDinputPeerUserFromMessage &data) {\n\t\t\treturn peerFromUser(data.vuser_id());\n\t\t});\n\t};\n\tconst auto key = peer.match([](const MTPDinputNotifyUsers &) {\n\t\treturn NotifySettingsKey{ peerFromUser(1) };\n\t}, [](const MTPDinputNotifyChats &) {\n\t\treturn NotifySettingsKey{ peerFromChat(1) };\n\t}, [](const MTPDinputNotifyBroadcasts &) {\n\t\treturn NotifySettingsKey{ peerFromChannel(1) };\n\t}, [&](const MTPDinputNotifyPeer &data) {\n\t\treturn NotifySettingsKey{ peerFromInput(data.vpeer()) };\n\t}, [&](const MTPDinputNotifyForumTopic &data) {\n\t\treturn NotifySettingsKey{\n\t\t\tpeerFromInput(data.vpeer()),\n\t\t\tdata.vtop_msg_id().v,\n\t\t};\n\t});\n\tif (_notifySettingRequests.contains(key)) {\n\t\treturn;\n\t}\n\tconst auto requestId = request(MTPaccount_GetNotifySettings(\n\t\tpeer\n\t)).done([=](const MTPPeerNotifySettings &result) {\n\t\t_session->data().notifySettings().apply(peer, result);\n\t\t_notifySettingRequests.remove(key);\n\t}).fail([=] {\n\t\t_session->data().notifySettings().apply(\n\t\t\tpeer,\n\t\t\tMTP_peerNotifySettings(\n\t\t\t\tMTP_flags(0),\n\t\t\t\tMTPBool(),\n\t\t\t\tMTPBool(),\n\t\t\t\tMTPint(),\n\t\t\t\tMTPNotificationSound(),\n\t\t\t\tMTPNotificationSound(),\n\t\t\t\tMTPNotificationSound(),\n\t\t\t\tMTPBool(),\n\t\t\t\tMTPBool(),\n\t\t\t\tMTPNotificationSound(),\n\t\t\t\tMTPNotificationSound(),\n\t\t\t\tMTPNotificationSound()));\n\t\t_notifySettingRequests.erase(key);\n\t}).send();\n\t_notifySettingRequests.emplace(key, requestId);\n}\n\nvoid ApiWrap::updateNotifySettingsDelayed(\n\t\tnot_null<const Data::Thread*> thread) {\n\tconst auto topic = thread->asTopic();\n\tif (!topic) {\n\t\treturn updateNotifySettingsDelayed(thread->peer());\n\t}\n\tif (_updateNotifyTopics.emplace(topic).second) {\n\t\ttopic->destroyed(\n\t\t) | rpl::on_next([=] {\n\t\t\t_updateNotifyTopics.remove(topic);\n\t\t}, _updateNotifyQueueLifetime);\n\t\t_updateNotifyTimer.callOnce(kNotifySettingSaveTimeout);\n\t}\n}\n\nvoid ApiWrap::updateNotifySettingsDelayed(not_null<const PeerData*> peer) {\n\tif (_updateNotifyPeers.emplace(peer).second) {\n\t\t_updateNotifyTimer.callOnce(kNotifySettingSaveTimeout);\n\t}\n}\n\nvoid ApiWrap::updateNotifySettingsDelayed(Data::DefaultNotify type) {\n\tif (_updateNotifyDefaults.emplace(type).second) {\n\t\t_updateNotifyTimer.callOnce(kNotifySettingSaveTimeout);\n\t}\n}\n\nvoid ApiWrap::sendNotifySettingsUpdates() {\n\t_updateNotifyQueueLifetime.destroy();\n\tfor (const auto &topic : base::take(_updateNotifyTopics)) {\n\t\trequest(MTPaccount_UpdateNotifySettings(\n\t\t\tMTP_inputNotifyForumTopic(\n\t\t\t\ttopic->peer()->input(),\n\t\t\t\tMTP_int(topic->rootId())),\n\t\t\ttopic->notify().serialize()\n\t\t)).afterDelay(kSmallDelayMs).send();\n\t}\n\tfor (const auto &peer : base::take(_updateNotifyPeers)) {\n\t\trequest(MTPaccount_UpdateNotifySettings(\n\t\t\tMTP_inputNotifyPeer(peer->input()),\n\t\t\tpeer->notify().serialize()\n\t\t)).afterDelay(kSmallDelayMs).send();\n\t}\n\tconst auto &settings = session().data().notifySettings();\n\tfor (const auto type : base::take(_updateNotifyDefaults)) {\n\t\trequest(MTPaccount_UpdateNotifySettings(\n\t\t\tData::DefaultNotifyToMTP(type),\n\t\t\tsettings.defaultSettings(type).serialize()\n\t\t)).afterDelay(kSmallDelayMs).send();\n\t}\n\tsession().mtp().sendAnything();\n}\n\nvoid ApiWrap::saveDraftToCloudDelayed(not_null<Data::Thread*> thread) {\n\t_draftsSaveRequestIds.emplace(base::make_weak(thread), 0);\n\tif (!_draftsSaveTimer.isActive()) {\n\t\t_draftsSaveTimer.callOnce(kSaveCloudDraftTimeout);\n\t}\n}\n\nvoid ApiWrap::updatePrivacyLastSeens() {\n\tconst auto now = base::unixtime::now();\n\tif (!_session->premium()) {\n\t\t_session->data().enumerateUsers([&](not_null<UserData*> user) {\n\t\t\tif (user->isSelf()\n\t\t\t\t|| !user->isLoaded()\n\t\t\t\t|| user->lastseen().isHidden()) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst auto till = user->lastseen().onlineTill();\n\t\t\tuser->updateLastseen((till + 3 * 86400 >= now)\n\t\t\t\t? Data::LastseenStatus::Recently(true)\n\t\t\t\t: (till + 7 * 86400 >= now)\n\t\t\t\t? Data::LastseenStatus::WithinWeek(true)\n\t\t\t\t: (till + 30 * 86400 >= now)\n\t\t\t\t? Data::LastseenStatus::WithinMonth(true)\n\t\t\t\t: Data::LastseenStatus::LongAgo(true));\n\t\t\tsession().changes().peerUpdated(\n\t\t\t\tuser,\n\t\t\t\tData::PeerUpdate::Flag::OnlineStatus);\n\t\t\tsession().data().maybeStopWatchForOffline(user);\n\t\t});\n\t}\n\n\tif (_contactsStatusesRequestId) {\n\t\trequest(_contactsStatusesRequestId).cancel();\n\t}\n\t_contactsStatusesRequestId = request(MTPcontacts_GetStatuses(\n\t)).done([=](const MTPVector<MTPContactStatus> &result) {\n\t\t_contactsStatusesRequestId = 0;\n\t\tfor (const auto &status : result.v) {\n\t\t\tconst auto &data = status.data();\n\t\t\tconst auto userId = UserId(data.vuser_id());\n\t\t\tif (const auto user = _session->data().userLoaded(userId)) {\n\t\t\t\tconst auto status = LastseenFromMTP(\n\t\t\t\t\tdata.vstatus(),\n\t\t\t\t\tuser->lastseen());\n\t\t\t\tif (user->updateLastseen(status)) {\n\t\t\t\t\tsession().changes().peerUpdated(\n\t\t\t\t\t\tuser,\n\t\t\t\t\t\tData::PeerUpdate::Flag::OnlineStatus);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}).fail([this] {\n\t\t_contactsStatusesRequestId = 0;\n\t}).send();\n}\n\nvoid ApiWrap::clearHistory(not_null<PeerData*> peer, bool revoke) {\n\tdeleteHistory(peer, true, revoke);\n}\n\nvoid ApiWrap::deleteConversation(not_null<PeerData*> peer, bool revoke) {\n\tif (const auto chat = peer->asChat()) {\n\t\trequest(MTPmessages_DeleteChatUser(\n\t\t\tMTP_flags(0),\n\t\t\tchat->inputChat(),\n\t\t\t_session->user()->inputUser()\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\tapplyUpdates(result);\n\t\t\tdeleteHistory(peer, false, revoke);\n\t\t}).fail([=] {\n\t\t\tdeleteHistory(peer, false, revoke);\n\t\t}).send();\n\t} else {\n\t\tdeleteHistory(peer, false, revoke);\n\t}\n}\n\nvoid ApiWrap::deleteHistory(\n\t\tnot_null<PeerData*> peer,\n\t\tbool justClear,\n\t\tbool revoke) {\n\tauto deleteTillId = MsgId(0);\n\tconst auto history = _session->data().history(peer);\n\tif (justClear) {\n\t\t// In case of clear history we need to know the last server message.\n\t\twhile (history->lastMessageKnown()) {\n\t\t\tconst auto last = history->lastMessage();\n\t\t\tif (!last) {\n\t\t\t\t// History is empty.\n\t\t\t\treturn;\n\t\t\t} else if (!last->isRegular()) {\n\t\t\t\t// Destroy client-side message locally.\n\t\t\t\tlast->destroy();\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (!history->lastMessageKnown()) {\n\t\t\thistory->owner().histories().requestDialogEntry(history, [=] {\n\t\t\t\tExpects(history->lastMessageKnown());\n\n\t\t\t\tdeleteHistory(peer, justClear, revoke);\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tdeleteTillId = history->lastMessage()->id;\n\t}\n\tif (const auto channel = peer->asChannel()) {\n\t\tif (!justClear && !revoke) {\n\t\t\tchannel->ptsSetWaitingForShortPoll(-1);\n\t\t\tleaveChannel(channel);\n\t\t} else {\n\t\t\tif (const auto migrated = peer->migrateFrom()) {\n\t\t\t\tdeleteHistory(migrated, justClear, revoke);\n\t\t\t}\n\t\t\tif (deleteTillId || (!justClear && revoke)) {\n\t\t\t\thistory->owner().histories().deleteAllMessages(\n\t\t\t\t\thistory,\n\t\t\t\t\tdeleteTillId,\n\t\t\t\t\tjustClear,\n\t\t\t\t\trevoke);\n\t\t\t}\n\t\t}\n\t} else {\n\t\thistory->owner().histories().deleteAllMessages(\n\t\t\thistory,\n\t\t\tdeleteTillId,\n\t\t\tjustClear,\n\t\t\trevoke);\n\t}\n\tif (!justClear) {\n\t\t_session->data().deleteConversationLocally(peer);\n\t} else if (history) {\n\t\thistory->clear(History::ClearType::ClearHistory);\n\t}\n}\n\nvoid ApiWrap::applyUpdates(\n\t\tconst MTPUpdates &updates,\n\t\tuint64 sentMessageRandomId) const {\n\tthis->updates().applyUpdates(updates, sentMessageRandomId);\n}\n\nint ApiWrap::applyAffectedHistory(\n\t\tPeerData *peer,\n\t\tconst MTPmessages_AffectedHistory &result) const {\n\tconst auto &data = result.c_messages_affectedHistory();\n\tif (const auto channel = peer ? peer->asChannel() : nullptr) {\n\t\tchannel->ptsUpdateAndApply(data.vpts().v, data.vpts_count().v);\n\t} else {\n\t\tupdates().updateAndApply(data.vpts().v, data.vpts_count().v);\n\t}\n\treturn data.voffset().v;\n}\n\nvoid ApiWrap::applyAffectedMessages(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPmessages_AffectedMessages &result) {\n\tconst auto &data = result.c_messages_affectedMessages();\n\tif (const auto channel = peer->asChannel()) {\n\t\tchannel->ptsUpdateAndApply(data.vpts().v, data.vpts_count().v);\n\t} else {\n\t\tapplyAffectedMessages(result);\n\t}\n}\n\nvoid ApiWrap::applyAffectedMessages(\n\t\tconst MTPmessages_AffectedMessages &result) const {\n\tconst auto &data = result.c_messages_affectedMessages();\n\tupdates().updateAndApply(data.vpts().v, data.vpts_count().v);\n}\n\nvoid ApiWrap::saveCurrentDraftToCloud() {\n\tCore::App().materializeLocalDrafts();\n\tfor (const auto &controller : _session->windows()) {\n\t\tif (const auto thread = controller->activeChatCurrent().thread()) {\n\t\t\tconst auto topic = thread->asTopic();\n\t\t\tif (topic && topic->creating()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto history = thread->owningHistory();\n\t\t\t_session->local().writeDrafts(history);\n\n\t\t\tconst auto topicRootId = thread->topicRootId();\n\t\t\tconst auto monoforumPeerId = thread->monoforumPeerId();\n\t\t\tconst auto localDraft = history->localDraft(\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId);\n\t\t\tconst auto cloudDraft = history->cloudDraft(\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId);\n\t\t\tif (!Data::DraftsAreEqual(localDraft, cloudDraft)\n\t\t\t\t&& !_session->supportMode()) {\n\t\t\t\tsaveDraftToCloudDelayed(thread);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ApiWrap::saveDraftsToCloud() {\n\tfor (auto i = begin(_draftsSaveRequestIds); i != end(_draftsSaveRequestIds);) {\n\t\tconst auto weak = i->first;\n\t\tconst auto thread = weak.get();\n\t\tif (!thread) {\n\t\t\ti = _draftsSaveRequestIds.erase(i);\n\t\t\tcontinue;\n\t\t} else if (i->second) {\n\t\t\t++i;\n\t\t\tcontinue; // sent already\n\t\t}\n\n\t\tconst auto history = thread->owningHistory();\n\t\tconst auto topicRootId = thread->topicRootId();\n\t\tconst auto monoforumPeerId = thread->monoforumPeerId();\n\t\tauto cloudDraft = history->cloudDraft(topicRootId, monoforumPeerId);\n\t\tauto localDraft = history->localDraft(topicRootId, monoforumPeerId);\n\t\tif (cloudDraft && cloudDraft->saveRequestId) {\n\t\t\trequest(base::take(cloudDraft->saveRequestId)).cancel();\n\t\t}\n\t\tif (!_session->supportMode()) {\n\t\t\tcloudDraft = history->createCloudDraft(\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tlocalDraft);\n\t\t} else if (!cloudDraft) {\n\t\t\tcloudDraft = history->createCloudDraft(\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tnullptr);\n\t\t}\n\n\t\tauto flags = MTPmessages_SaveDraft::Flags(0);\n\t\tauto &textWithTags = cloudDraft->textWithTags;\n\t\tif (cloudDraft->webpage.removed) {\n\t\t\tflags |= MTPmessages_SaveDraft::Flag::f_no_webpage;\n\t\t} else if (!cloudDraft->webpage.url.isEmpty()) {\n\t\t\tflags |= MTPmessages_SaveDraft::Flag::f_media;\n\t\t}\n\t\tif (cloudDraft->reply.messageId\n\t\t\t|| cloudDraft->reply.topicRootId\n\t\t\t|| cloudDraft->reply.monoforumPeerId) {\n\t\t\tflags |= MTPmessages_SaveDraft::Flag::f_reply_to;\n\t\t}\n\t\tif (!textWithTags.tags.isEmpty()) {\n\t\t\tflags |= MTPmessages_SaveDraft::Flag::f_entities;\n\t\t}\n\t\tif (cloudDraft->suggest) {\n\t\t\tflags |= MTPmessages_SaveDraft::Flag::f_suggested_post;\n\t\t}\n\t\tauto entities = Api::EntitiesToMTP(\n\t\t\t_session,\n\t\t\tTextUtilities::ConvertTextTagsToEntities(textWithTags.tags),\n\t\t\tApi::ConvertOption::SkipLocal);\n\n\t\thistory->startSavingCloudDraft(topicRootId, monoforumPeerId);\n\t\tcloudDraft->saveRequestId = request(MTPmessages_SaveDraft(\n\t\t\tMTP_flags(flags),\n\t\t\tReplyToForMTP(history, cloudDraft->reply),\n\t\t\thistory->peer->input(),\n\t\t\tMTP_string(textWithTags.text),\n\t\t\tentities,\n\t\t\tData::WebPageForMTP(\n\t\t\t\tcloudDraft->webpage,\n\t\t\t\ttextWithTags.text.isEmpty()),\n\t\t\tMTP_long(0), // effect\n\t\t\tApi::SuggestToMTP(cloudDraft->suggest)\n\t\t)).done([=](const MTPBool &result, const MTP::Response &response) {\n\t\t\tconst auto requestId = response.requestId;\n\t\t\thistory->finishSavingCloudDraft(\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tApi::UnixtimeFromMsgId(response.outerMsgId));\n\t\t\tconst auto cloudDraft = history->cloudDraft(\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId);\n\t\t\tif (cloudDraft) {\n\t\t\t\tif (cloudDraft->saveRequestId == requestId) {\n\t\t\t\t\tcloudDraft->saveRequestId = 0;\n\t\t\t\t\thistory->draftSavedToCloud(topicRootId, monoforumPeerId);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto i = _draftsSaveRequestIds.find(weak);\n\t\t\tif (i != _draftsSaveRequestIds.cend()\n\t\t\t\t&& i->second == requestId) {\n\t\t\t\t_draftsSaveRequestIds.erase(i);\n\t\t\t\tcheckQuitPreventFinished();\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error, const MTP::Response &response) {\n\t\t\tconst auto requestId = response.requestId;\n\t\t\thistory->finishSavingCloudDraft(\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tApi::UnixtimeFromMsgId(response.outerMsgId));\n\t\t\tconst auto cloudDraft = history->cloudDraft(\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId);\n\t\t\tif (cloudDraft) {\n\t\t\t\tif (cloudDraft->saveRequestId == requestId) {\n\t\t\t\t\thistory->clearCloudDraft(topicRootId, monoforumPeerId);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto i = _draftsSaveRequestIds.find(weak);\n\t\t\tif (i != _draftsSaveRequestIds.cend()\n\t\t\t\t&& i->second == requestId) {\n\t\t\t\t_draftsSaveRequestIds.erase(i);\n\t\t\t\tcheckQuitPreventFinished();\n\t\t\t}\n\t\t}).send();\n\n\t\ti->second = cloudDraft->saveRequestId;\n\t\t++i;\n\t}\n}\n\nbool ApiWrap::isQuitPrevent() {\n\tif (_draftsSaveRequestIds.empty()) {\n\t\treturn false;\n\t}\n\tLOG((\"ApiWrap prevents quit, saving drafts...\"));\n\tsaveDraftsToCloud();\n\treturn true;\n}\n\nvoid ApiWrap::checkQuitPreventFinished() {\n\tif (_draftsSaveRequestIds.empty()) {\n\t\tif (Core::Quitting()) {\n\t\t\tLOG((\"ApiWrap doesn't prevent quit any more.\"));\n\t\t}\n\t\tCore::App().quitPreventFinished();\n\t}\n}\n\nvoid ApiWrap::registerModifyRequest(\n\t\tconst QString &key,\n\t\tmtpRequestId requestId) {\n\tconst auto i = _modifyRequests.find(key);\n\tif (i != end(_modifyRequests)) {\n\t\trequest(i->second).cancel();\n\t\ti->second = requestId;\n\t} else {\n\t\t_modifyRequests.emplace(key, requestId);\n\t}\n}\n\nvoid ApiWrap::clearModifyRequest(const QString &key) {\n\t_modifyRequests.remove(key);\n}\n\nvoid ApiWrap::gotStickerSet(\n\t\tuint64 setId,\n\t\tconst MTPmessages_StickerSet &result) {\n\t_stickerSetRequests.remove(setId);\n\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t_session->data().stickers().feedSetFull(data);\n\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t});\n}\n\nvoid ApiWrap::requestWebPageDelayed(not_null<WebPageData*> page) {\n\tif (page->failed || !page->pendingTill) {\n\t\treturn;\n\t}\n\t_webPagesPending.emplace(page, 0);\n\tauto left = (page->pendingTill - base::unixtime::now()) * 1000;\n\tif (!_webPagesTimer.isActive() || left <= _webPagesTimer.remainingTime()) {\n\t\t_webPagesTimer.callOnce((left < 0 ? 0 : left) + 1);\n\t}\n}\n\nvoid ApiWrap::clearWebPageRequest(not_null<WebPageData*> page) {\n\t_webPagesPending.remove(page);\n\tif (_webPagesPending.empty() && _webPagesTimer.isActive()) {\n\t\t_webPagesTimer.cancel();\n\t}\n}\n\nvoid ApiWrap::clearWebPageRequests() {\n\t_webPagesPending.clear();\n\t_webPagesTimer.cancel();\n}\n\nvoid ApiWrap::resolveWebPages() {\n\tauto ids = QVector<MTPInputMessage>(); // temp_req_id = -1\n\tusing IndexAndMessageIds = QPair<int32, QVector<MTPInputMessage>>;\n\tusing MessageIdsByChannel = base::flat_map<ChannelData*, IndexAndMessageIds>;\n\tMessageIdsByChannel idsByChannel; // temp_req_id = -index - 2\n\n\tids.reserve(_webPagesPending.size());\n\tint32 t = base::unixtime::now(), m = INT_MAX;\n\tfor (auto &[page, requestId] : _webPagesPending) {\n\t\tif (requestId > 0) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (page->pendingTill <= t) {\n\t\t\tif (const auto item = _session->data().findWebPageItem(page)) {\n\t\t\t\tif (const auto channel = item->history()->peer->asChannel()) {\n\t\t\t\t\tauto channelMap = idsByChannel.find(channel);\n\t\t\t\t\tif (channelMap == idsByChannel.cend()) {\n\t\t\t\t\t\tchannelMap = idsByChannel.emplace(\n\t\t\t\t\t\t\tchannel,\n\t\t\t\t\t\t\tIndexAndMessageIds(\n\t\t\t\t\t\t\t\tidsByChannel.size(),\n\t\t\t\t\t\t\t\tQVector<MTPInputMessage>(\n\t\t\t\t\t\t\t\t\t1,\n\t\t\t\t\t\t\t\t\tMTP_inputMessageID(MTP_int(item->id))))).first;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchannelMap->second.second.push_back(\n\t\t\t\t\t\t\tMTP_inputMessageID(MTP_int(item->id)));\n\t\t\t\t\t}\n\t\t\t\t\trequestId = -channelMap->second.first - 2;\n\t\t\t\t} else {\n\t\t\t\t\tids.push_back(MTP_inputMessageID(MTP_int(item->id)));\n\t\t\t\t\trequestId = -1;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tm = std::min(m, page->pendingTill - t);\n\t\t}\n\t}\n\n\tauto requestId = mtpRequestId(0);\n\tif (!ids.isEmpty()) {\n\t\trequestId = request(MTPmessages_GetMessages(\n\t\t\tMTP_vector<MTPInputMessage>(ids)\n\t\t)).done([=](\n\t\t\t\tconst MTPmessages_Messages &result,\n\t\t\t\tmtpRequestId requestId) {\n\t\t\tgotWebPages(nullptr, result, requestId);\n\t\t}).afterDelay(kSmallDelayMs).send();\n\t}\n\tQVector<mtpRequestId> reqsByIndex(idsByChannel.size(), 0);\n\tfor (auto i = idsByChannel.cbegin(), e = idsByChannel.cend(); i != e; ++i) {\n\t\treqsByIndex[i->second.first] = request(MTPchannels_GetMessages(\n\t\t\ti->first->inputChannel(),\n\t\t\tMTP_vector<MTPInputMessage>(i->second.second)\n\t\t)).done([=, channel = i->first](\n\t\t\t\tconst MTPmessages_Messages &result,\n\t\t\t\tmtpRequestId requestId) {\n\t\t\tgotWebPages(channel, result, requestId);\n\t\t}).afterDelay(kSmallDelayMs).send();\n\t}\n\tif (requestId || !reqsByIndex.isEmpty()) {\n\t\tfor (auto &[page, pendingRequestId] : _webPagesPending) {\n\t\t\tif (pendingRequestId > 0) {\n\t\t\t\tcontinue;\n\t\t\t} else if (pendingRequestId < 0) {\n\t\t\t\tif (pendingRequestId == -1) {\n\t\t\t\t\tpendingRequestId = requestId;\n\t\t\t\t} else {\n\t\t\t\t\tpendingRequestId = reqsByIndex[-pendingRequestId - 2];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (m < INT_MAX) {\n\t\t_webPagesTimer.callOnce(std::min(m, 86400) * crl::time(1000));\n\t}\n}\n\ntemplate <typename Request>\nvoid ApiWrap::requestFileReference(\n\t\tData::FileOrigin origin,\n\t\tFileReferencesHandler &&handler,\n\t\tRequest &&data) {\n\tconst auto i = _fileReferenceHandlers.find(origin);\n\tif (i != end(_fileReferenceHandlers)) {\n\t\ti->second.push_back(std::move(handler));\n\t\treturn;\n\t}\n\tauto handlers = std::vector<FileReferencesHandler>();\n\thandlers.push_back(std::move(handler));\n\t_fileReferenceHandlers.emplace(origin, std::move(handlers));\n\n\trequest(std::move(data)).done([=](const auto &result) {\n\t\tconst auto parsed = Data::GetFileReferences(result);\n\t\tfor (const auto &p : parsed.data) {\n\t\t\t// Unpack here the parsed pair by hand to workaround a GCC bug.\n\t\t\t// See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=87122\n\t\t\tconst auto &origin = p.first;\n\t\t\tconst auto &reference = p.second;\n\t\t\tconst auto documentId = std::get_if<DocumentFileLocationId>(\n\t\t\t\t&origin);\n\t\t\tif (documentId) {\n\t\t\t\t_session->data().document(\n\t\t\t\t\tdocumentId->id\n\t\t\t\t)->refreshFileReference(reference);\n\t\t\t}\n\t\t\tconst auto photoId = std::get_if<PhotoFileLocationId>(&origin);\n\t\t\tif (photoId) {\n\t\t\t\t_session->data().photo(\n\t\t\t\t\tphotoId->id\n\t\t\t\t)->refreshFileReference(reference);\n\t\t\t}\n\t\t}\n\t\tconst auto i = _fileReferenceHandlers.find(origin);\n\t\tAssert(i != end(_fileReferenceHandlers));\n\t\tauto handlers = std::move(i->second);\n\t\t_fileReferenceHandlers.erase(i);\n\t\tfor (auto &handler : handlers) {\n\t\t\thandler(parsed);\n\t\t}\n\t}).fail([=] {\n\t\tconst auto i = _fileReferenceHandlers.find(origin);\n\t\tAssert(i != end(_fileReferenceHandlers));\n\t\tauto handlers = std::move(i->second);\n\t\t_fileReferenceHandlers.erase(i);\n\t\tfor (auto &handler : handlers) {\n\t\t\thandler(UpdatedFileReferences());\n\t\t}\n\t}).send();\n}\n\nvoid ApiWrap::refreshFileReference(\n\t\tData::FileOrigin origin,\n\t\tnot_null<Storage::DownloadMtprotoTask*> task,\n\t\tint requestId,\n\t\tconst QByteArray &current) {\n\treturn refreshFileReference(origin, crl::guard(task, [=](\n\t\t\tconst UpdatedFileReferences &data) {\n\t\ttask->refreshFileReferenceFrom(data, requestId, current);\n\t}));\n}\n\nvoid ApiWrap::refreshFileReference(\n\t\tData::FileOrigin origin,\n\t\tFileReferencesHandler &&handler) {\n\tconst auto fail = [&] {\n\t\thandler(UpdatedFileReferences());\n\t};\n\tconst auto request = [&](\n\t\t\tauto &&data,\n\t\t\tFn<void()> &&additional = nullptr) {\n\t\trequestFileReference(\n\t\t\torigin,\n\t\t\tstd::move(handler),\n\t\t\tstd::move(data));\n\t\tif (additional) {\n\t\t\tconst auto i = _fileReferenceHandlers.find(origin);\n\t\t\tAssert(i != end(_fileReferenceHandlers));\n\t\t\tif (i->second.size() == 1) {\n\t\t\t\ti->second.push_back([=](auto&&) {\n\t\t\t\t\tadditional();\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t};\n\tv::match(origin.data, [&](Data::FileOriginMessage data) {\n\t\tif (const auto item = _session->data().message(data)) {\n\t\t\tconst auto media = item->media();\n\t\t\tconst auto mediaStory = media ? media->storyId() : FullStoryId();\n\t\t\tconst auto storyId = mediaStory\n\t\t\t\t? mediaStory\n\t\t\t\t: FullStoryId{\n\t\t\t\t\t(IsStoryMsgId(item->id)\n\t\t\t\t\t\t? item->history()->peer->id\n\t\t\t\t\t\t: PeerId()),\n\t\t\t\t\t(IsStoryMsgId(item->id)\n\t\t\t\t\t\t? StoryIdFromMsgId(item->id)\n\t\t\t\t\t\t: StoryId())\n\t\t\t\t};\n\t\t\tif (storyId) {\n\t\t\t\trequest(MTPstories_GetStoriesByID(\n\t\t\t\t\t_session->data().peer(storyId.peer)->input(),\n\t\t\t\t\tMTP_vector<MTPint>(1, MTP_int(storyId.story))));\n\t\t\t} else if (item->isScheduled()) {\n\t\t\t\tconst auto realId = _session->scheduledMessages().lookupId(\n\t\t\t\t\titem);\n\t\t\t\trequest(MTPmessages_GetScheduledMessages(\n\t\t\t\t\titem->history()->peer->input(),\n\t\t\t\t\tMTP_vector<MTPint>(1, MTP_int(realId))));\n\t\t\t} else if (item->isSavedMusicItem()) {\n\t\t\t\tconst auto user = item->history()->peer->asUser();\n\t\t\t\tconst auto media = item->media();\n\t\t\t\tconst auto document = media ? media->document() : nullptr;\n\t\t\t\tif (user && document) {\n\t\t\t\t\trequest(MTPusers_GetSavedMusicByID(\n\t\t\t\t\t\tuser->inputUser(),\n\t\t\t\t\t\tMTP_vector<MTPInputDocument>(1, document->mtpInput())));\n\t\t\t\t} else {\n\t\t\t\t\tfail();\n\t\t\t\t}\n\t\t\t} else if (item->isBusinessShortcut()) {\n\t\t\t\tconst auto &shortcuts = _session->data().shortcutMessages();\n\t\t\t\tconst auto realId = shortcuts.lookupId(item);\n\t\t\t\trequest(MTPmessages_GetQuickReplyMessages(\n\t\t\t\t\tMTP_flags(MTPmessages_GetQuickReplyMessages::Flag::f_id),\n\t\t\t\t\tMTP_int(item->shortcutId()),\n\t\t\t\t\tMTP_vector<MTPint>(1, MTP_int(realId)),\n\t\t\t\t\tMTP_long(0)));\n\t\t\t} else if (const auto channel = item->history()->peer->asChannel()) {\n\t\t\t\trequest(MTPchannels_GetMessages(\n\t\t\t\t\tchannel->inputChannel(),\n\t\t\t\t\tMTP_vector<MTPInputMessage>(\n\t\t\t\t\t\t1,\n\t\t\t\t\t\tMTP_inputMessageID(MTP_int(item->id)))));\n\t\t\t} else {\n\t\t\t\trequest(MTPmessages_GetMessages(\n\t\t\t\t\tMTP_vector<MTPInputMessage>(\n\t\t\t\t\t\t1,\n\t\t\t\t\t\tMTP_inputMessageID(MTP_int(item->id)))));\n\t\t\t}\n\t\t} else {\n\t\t\tfail();\n\t\t}\n\t}, [&](Data::FileOriginUserPhoto data) {\n\t\tif (const auto user = _session->data().user(data.userId)) {\n\t\t\trequest(MTPphotos_GetUserPhotos(\n\t\t\t\tuser->inputUser(),\n\t\t\t\tMTP_int(-1),\n\t\t\t\tMTP_long(data.photoId),\n\t\t\t\tMTP_int(1)));\n\t\t} else {\n\t\t\tfail();\n\t\t}\n\t}, [&](Data::FileOriginFullUser data) {\n\t\tif (const auto user = _session->data().user(data.userId)) {\n\t\t\trequest(MTPusers_GetFullUser(user->inputUser()));\n\t\t} else {\n\t\t\tfail();\n\t\t}\n\t}, [&](Data::FileOriginPeerPhoto data) {\n\t\tconst auto peer = _session->data().peer(data.peerId);\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\trequest(MTPchannels_GetFullChannel(\n\t\t\t\tchannel->inputChannel()));\n\t\t} else if (const auto chat = peer->asChat()) {\n\t\t\trequest(MTPmessages_GetFullChat(chat->inputChat()));\n\t\t} else {\n\t\t\tfail();\n\t\t}\n\t}, [&](Data::FileOriginStickerSet data) {\n\t\tconst auto isRecentAttached\n\t\t\t= (data.setId == Data::Stickers::CloudRecentAttachedSetId);\n\t\tif (data.setId == Data::Stickers::CloudRecentSetId\n\t\t\t|| data.setId == Data::Stickers::RecentSetId\n\t\t\t|| isRecentAttached) {\n\t\t\tauto done = [=] { crl::on_main(_session, [=] {\n\t\t\t\tif (isRecentAttached) {\n\t\t\t\t\tlocal().writeRecentMasks();\n\t\t\t\t} else {\n\t\t\t\t\tlocal().writeRecentStickers();\n\t\t\t\t}\n\t\t\t}); };\n\t\t\trequest(MTPmessages_GetRecentStickers(\n\t\t\t\tMTP_flags(isRecentAttached\n\t\t\t\t\t? MTPmessages_GetRecentStickers::Flag::f_attached\n\t\t\t\t\t: MTPmessages_GetRecentStickers::Flags(0)),\n\t\t\t\tMTP_long(0)),\n\t\t\t\tstd::move(done));\n\t\t} else if (data.setId == Data::Stickers::FavedSetId) {\n\t\t\trequest(MTPmessages_GetFavedStickers(MTP_long(0)),\n\t\t\t\t[=] { crl::on_main(_session, [=] { local().writeFavedStickers(); }); });\n\t\t} else {\n\t\t\trequest(MTPmessages_GetStickerSet(\n\t\t\t\tMTP_inputStickerSetID(\n\t\t\t\t\tMTP_long(data.setId),\n\t\t\t\t\tMTP_long(data.accessHash)),\n\t\t\t\tMTP_int(0)), // hash\n\t\t\t\t[=] { crl::on_main(_session, [=] {\n\t\t\t\t\tlocal().writeInstalledStickers();\n\t\t\t\t\tlocal().writeRecentStickers();\n\t\t\t\t\tlocal().writeFavedStickers();\n\t\t\t\t}); });\n\t\t}\n\t}, [&](Data::FileOriginSavedGifs data) {\n\t\trequest(\n\t\t\tMTPmessages_GetSavedGifs(MTP_long(0)),\n\t\t\t[=] { crl::on_main(_session, [=] { local().writeSavedGifs(); }); });\n\t}, [&](Data::FileOriginWallpaper data) {\n\t\tconst auto useSlug = data.ownerId\n\t\t\t&& (data.ownerId != session().userId())\n\t\t\t&& !data.slug.isEmpty();\n\t\trequest(MTPaccount_GetWallPaper(useSlug\n\t\t\t? MTP_inputWallPaperSlug(MTP_string(data.slug))\n\t\t\t: MTP_inputWallPaper(\n\t\t\t\tMTP_long(data.paperId),\n\t\t\t\tMTP_long(data.accessHash))));\n\t}, [&](Data::FileOriginTheme data) {\n\t\trequest(MTPaccount_GetTheme(\n\t\t\tMTP_string(Data::CloudThemes::Format()),\n\t\t\tMTP_inputTheme(\n\t\t\t\tMTP_long(data.themeId),\n\t\t\t\tMTP_long(data.accessHash))));\n\t}, [&](Data::FileOriginRingtones data) {\n\t\trequest(MTPaccount_GetSavedRingtones(MTP_long(0)));\n\t}, [&](Data::FileOriginPremiumPreviews data) {\n\t\trequest(MTPhelp_GetPremiumPromo());\n\t}, [&](Data::FileOriginWebPage data) {\n\t\trequest(MTPmessages_GetWebPage(\n\t\t\tMTP_string(data.url),\n\t\t\tMTP_int(0)));\n\t}, [&](Data::FileOriginStory data) {\n\t\trequest(MTPstories_GetStoriesByID(\n\t\t\t_session->data().peer(data.peer)->input(),\n\t\t\tMTP_vector<MTPint>(1, MTP_int(data.story))));\n\t}, [&](v::null_t) {\n\t\tfail();\n\t});\n}\n\nvoid ApiWrap::gotWebPages(\n\t\tChannelData *channel,\n\t\tconst MTPmessages_Messages &result,\n\t\tmtpRequestId req) {\n\tWebPageData::ApplyChanges(_session, channel, result);\n\tfor (auto i = _webPagesPending.begin(); i != _webPagesPending.cend();) {\n\t\tif (i->second == req) {\n\t\t\tif (i->first->pendingTill > 0) {\n\t\t\t\ti->first->pendingTill = 0;\n\t\t\t\ti->first->failed = 1;\n\t\t\t\t_session->data().notifyWebPageUpdateDelayed(i->first);\n\t\t\t}\n\t\t\ti = _webPagesPending.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\t_session->data().sendWebPageGamePollTodoListNotifications();\n}\n\nvoid ApiWrap::updateStickers() {\n\tconst auto now = crl::now();\n\trequestStickers(now);\n\trequestRecentStickers(now, false);\n\trequestFavedStickers(now);\n\trequestFeaturedStickers(now);\n}\n\nvoid ApiWrap::updateSavedGifs() {\n\tconst auto now = crl::now();\n\trequestSavedGifs(now);\n}\n\nvoid ApiWrap::updateMasks() {\n\tconst auto now = crl::now();\n\trequestMasks(now);\n\trequestRecentStickers(now, true);\n}\n\nvoid ApiWrap::updateCustomEmoji() {\n\tconst auto now = crl::now();\n\trequestCustomEmoji(now);\n\trequestFeaturedEmoji(now);\n}\n\nvoid ApiWrap::requestSpecialStickersForce(\n\t\tbool faved,\n\t\tbool recent,\n\t\tbool attached) {\n\tif (faved) {\n\t\trequestFavedStickers(std::nullopt);\n\t} else if (recent || attached) {\n\t\trequestRecentStickers(std::nullopt, attached);\n\t}\n}\n\nvoid ApiWrap::setGroupStickerSet(\n\t\tnot_null<ChannelData*> megagroup,\n\t\tconst StickerSetIdentifier &set) {\n\tExpects(megagroup->mgInfo != nullptr);\n\n\tmegagroup->mgInfo->stickerSet = set;\n\trequest(MTPchannels_SetStickers(\n\t\tmegagroup->inputChannel(),\n\t\tData::InputStickerSet(set)\n\t)).send();\n\t_session->data().stickers().notifyUpdated(Data::StickersType::Stickers);\n}\n\nvoid ApiWrap::setGroupEmojiSet(\n\t\tnot_null<ChannelData*> megagroup,\n\t\tconst StickerSetIdentifier &set) {\n\tExpects(megagroup->mgInfo != nullptr);\n\n\tmegagroup->mgInfo->emojiSet = set;\n\trequest(MTPchannels_SetEmojiStickers(\n\t\tmegagroup->inputChannel(),\n\t\tData::InputStickerSet(set)\n\t)).send();\n\t_session->changes().peerUpdated(\n\t\tmegagroup,\n\t\tData::PeerUpdate::Flag::EmojiSet);\n\t_session->data().stickers().notifyUpdated(Data::StickersType::Emoji);\n}\n\nstd::vector<not_null<DocumentData*>> *ApiWrap::stickersByEmoji(\n\t\tconst QString &key) {\n\tconst auto it = _stickersByEmoji.find(key);\n\tconst auto sendRequest = [&] {\n\t\tif (it == _stickersByEmoji.end()) {\n\t\t\treturn true;\n\t\t}\n\t\tconst auto received = it->second.received;\n\t\tconst auto now = crl::now();\n\t\treturn (received > 0)\n\t\t\t&& (received + kStickersByEmojiInvalidateTimeout) <= now;\n\t}();\n\tif (sendRequest) {\n\t\tconst auto hash = (it != _stickersByEmoji.end())\n\t\t\t? it->second.hash\n\t\t\t: uint64(0);\n\t\trequest(MTPmessages_GetStickers(\n\t\t\tMTP_string(key),\n\t\t\tMTP_long(hash)\n\t\t)).done([=](const MTPmessages_Stickers &result) {\n\t\t\tif (result.type() == mtpc_messages_stickersNotModified) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tAssert(result.type() == mtpc_messages_stickers);\n\t\t\tconst auto &data = result.c_messages_stickers();\n\t\t\tauto &entry = _stickersByEmoji[key];\n\t\t\tentry.list.clear();\n\t\t\tentry.list.reserve(data.vstickers().v.size());\n\t\t\tfor (const auto &sticker : data.vstickers().v) {\n\t\t\t\tconst auto document = _session->data().processDocument(\n\t\t\t\t\tsticker);\n\t\t\t\tif (document->sticker()) {\n\t\t\t\t\tentry.list.push_back(document);\n\t\t\t\t}\n\t\t\t}\n\t\t\tentry.hash = data.vhash().v;\n\t\t\tentry.received = crl::now();\n\t\t\t_session->data().stickers().notifyUpdated(\n\t\t\t\tData::StickersType::Stickers);\n\t\t}).send();\n\t}\n\tif (it == _stickersByEmoji.end()) {\n\t\t_stickersByEmoji.emplace(key, StickersByEmoji());\n\t} else if (it->second.received > 0) {\n\t\treturn &it->second.list;\n\t}\n\treturn nullptr;\n}\n\nvoid ApiWrap::requestStickers(TimeId now) {\n\tif (!_session->data().stickers().updateNeeded(now)\n\t\t|| _stickersUpdateRequest) {\n\t\treturn;\n\t}\n\tconst auto done = [=](const MTPmessages_AllStickers &result) {\n\t\t_session->data().stickers().setLastUpdate(crl::now());\n\t\t_stickersUpdateRequest = 0;\n\n\t\tresult.match([&](const MTPDmessages_allStickersNotModified&) {\n\t\t}, [&](const MTPDmessages_allStickers &data) {\n\t\t\t_session->data().stickers().setsReceived(\n\t\t\t\tdata.vsets().v,\n\t\t\t\tdata.vhash().v);\n\t\t});\n\t};\n\t_stickersUpdateRequest = request(MTPmessages_GetAllStickers(\n\t\tMTP_long(Api::CountStickersHash(_session, true))\n\t)).done(done).fail([=] {\n\t\tLOG((\"App Fail: Failed to get stickers!\"));\n\t\tdone(MTP_messages_allStickersNotModified());\n\t}).send();\n}\n\nvoid ApiWrap::requestMasks(TimeId now) {\n\tif (!_session->data().stickers().masksUpdateNeeded(now)\n\t\t|| _masksUpdateRequest) {\n\t\treturn;\n\t}\n\tconst auto done = [=](const MTPmessages_AllStickers &result) {\n\t\t_session->data().stickers().setLastMasksUpdate(crl::now());\n\t\t_masksUpdateRequest = 0;\n\n\t\tresult.match([&](const MTPDmessages_allStickersNotModified&) {\n\t\t}, [&](const MTPDmessages_allStickers &data) {\n\t\t\t_session->data().stickers().masksReceived(\n\t\t\t\tdata.vsets().v,\n\t\t\t\tdata.vhash().v);\n\t\t});\n\t};\n\t_masksUpdateRequest = request(MTPmessages_GetMaskStickers(\n\t\tMTP_long(Api::CountMasksHash(_session, true))\n\t)).done(done).fail([=] {\n\t\tLOG((\"App Fail: Failed to get masks!\"));\n\t\tdone(MTP_messages_allStickersNotModified());\n\t}).send();\n}\n\nvoid ApiWrap::requestCustomEmoji(TimeId now) {\n\tif (!_session->data().stickers().emojiUpdateNeeded(now)\n\t\t|| _customEmojiUpdateRequest) {\n\t\treturn;\n\t}\n\tconst auto done = [=](const MTPmessages_AllStickers &result) {\n\t\t_session->data().stickers().setLastEmojiUpdate(crl::now());\n\t\t_customEmojiUpdateRequest = 0;\n\n\t\tresult.match([&](const MTPDmessages_allStickersNotModified&) {\n\t\t}, [&](const MTPDmessages_allStickers &data) {\n\t\t\t_session->data().stickers().emojiReceived(\n\t\t\t\tdata.vsets().v,\n\t\t\t\tdata.vhash().v);\n\t\t});\n\t};\n\t_customEmojiUpdateRequest = request(MTPmessages_GetEmojiStickers(\n\t\tMTP_long(Api::CountCustomEmojiHash(_session, true))\n\t)).done(done).fail([=] {\n\t\tLOG((\"App Fail: Failed to get custom emoji!\"));\n\t\tdone(MTP_messages_allStickersNotModified());\n\t}).send();\n}\n\nvoid ApiWrap::requestRecentStickers(\n\t\tstd::optional<TimeId> now,\n\t\tbool attached) {\n\tconst auto needed = !now\n\t\t? true\n\t\t: attached\n\t\t? _session->data().stickers().recentAttachedUpdateNeeded(*now)\n\t\t: _session->data().stickers().recentUpdateNeeded(*now);\n\tif (!needed) {\n\t\treturn;\n\t}\n\tconst auto requestId = [=]() -> mtpRequestId & {\n\t\treturn attached\n\t\t\t? _recentAttachedStickersUpdateRequest\n\t\t\t: _recentStickersUpdateRequest;\n\t};\n\tif (requestId()) {\n\t\treturn;\n\t}\n\tconst auto finish = [=] {\n\t\tauto &stickers = _session->data().stickers();\n\t\tif (attached) {\n\t\t\tstickers.setLastRecentAttachedUpdate(crl::now());\n\t\t} else {\n\t\t\tstickers.setLastRecentUpdate(crl::now());\n\t\t}\n\t\trequestId() = 0;\n\t};\n\tconst auto flags = attached\n\t\t? MTPmessages_getRecentStickers::Flag::f_attached\n\t\t: MTPmessages_getRecentStickers::Flags(0);\n\trequestId() = request(MTPmessages_GetRecentStickers(\n\t\tMTP_flags(flags),\n\t\tMTP_long(now ? Api::CountRecentStickersHash(_session, attached) : 0)\n\t)).done([=](const MTPmessages_RecentStickers &result) {\n\t\tfinish();\n\n\t\tswitch (result.type()) {\n\t\tcase mtpc_messages_recentStickersNotModified: return;\n\t\tcase mtpc_messages_recentStickers: {\n\t\t\tauto &d = result.c_messages_recentStickers();\n\t\t\t_session->data().stickers().specialSetReceived(\n\t\t\t\tattached\n\t\t\t\t\t? Data::Stickers::CloudRecentAttachedSetId\n\t\t\t\t\t: Data::Stickers::CloudRecentSetId,\n\t\t\t\ttr::lng_recent_stickers(tr::now),\n\t\t\t\td.vstickers().v,\n\t\t\t\td.vhash().v,\n\t\t\t\td.vpacks().v,\n\t\t\t\td.vdates().v);\n\t\t} return;\n\t\tdefault: Unexpected(\"Type in ApiWrap::recentStickersDone()\");\n\t\t}\n\t}).fail([=] {\n\t\tfinish();\n\n\t\tLOG((\"App Fail: Failed to get recent stickers!\"));\n\t}).send();\n}\n\nvoid ApiWrap::requestFavedStickers(std::optional<TimeId> now) {\n\tif (now) {\n\t\tif (!_session->data().stickers().favedUpdateNeeded(*now)\n\t\t\t|| _favedStickersUpdateRequest) {\n\t\t\treturn;\n\t\t}\n\t}\n\t_favedStickersUpdateRequest = request(MTPmessages_GetFavedStickers(\n\t\tMTP_long(now ? Api::CountFavedStickersHash(_session) : 0)\n\t)).done([=](const MTPmessages_FavedStickers &result) {\n\t\t_session->data().stickers().setLastFavedUpdate(crl::now());\n\t\t_favedStickersUpdateRequest = 0;\n\n\t\tswitch (result.type()) {\n\t\tcase mtpc_messages_favedStickersNotModified: return;\n\t\tcase mtpc_messages_favedStickers: {\n\t\t\tauto &d = result.c_messages_favedStickers();\n\t\t\t_session->data().stickers().specialSetReceived(\n\t\t\t\tData::Stickers::FavedSetId,\n\t\t\t\tLang::Hard::FavedSetTitle(),\n\t\t\t\td.vstickers().v,\n\t\t\t\td.vhash().v,\n\t\t\t\td.vpacks().v);\n\t\t} return;\n\t\tdefault: Unexpected(\"Type in ApiWrap::favedStickersDone()\");\n\t\t}\n\t}).fail([=] {\n\t\t_session->data().stickers().setLastFavedUpdate(crl::now());\n\t\t_favedStickersUpdateRequest = 0;\n\n\t\tLOG((\"App Fail: Failed to get faved stickers!\"));\n\t}).send();\n}\n\nvoid ApiWrap::requestFeaturedStickers(TimeId now) {\n\tif (!_session->data().stickers().featuredUpdateNeeded(now)\n\t\t|| _featuredStickersUpdateRequest) {\n\t\treturn;\n\t}\n\t_featuredStickersUpdateRequest = request(MTPmessages_GetFeaturedStickers(\n\t\tMTP_long(Api::CountFeaturedStickersHash(_session))\n\t)).done([=](const MTPmessages_FeaturedStickers &result) {\n\t\t_featuredStickersUpdateRequest = 0;\n\t\t_session->data().stickers().featuredSetsReceived(result);\n\t}).fail([=] {\n\t\t_featuredStickersUpdateRequest = 0;\n\t\t_session->data().stickers().setLastFeaturedUpdate(crl::now());\n\t\tLOG((\"App Fail: Failed to get featured stickers!\"));\n\t}).send();\n}\n\nvoid ApiWrap::requestFeaturedEmoji(TimeId now) {\n\tif (!_session->data().stickers().featuredEmojiUpdateNeeded(now)\n\t\t|| _featuredEmojiUpdateRequest) {\n\t\treturn;\n\t}\n\t_featuredEmojiUpdateRequest = request(\n\t\tMTPmessages_GetFeaturedEmojiStickers(\n\t\t\tMTP_long(Api::CountFeaturedStickersHash(_session)))\n\t).done([=](const MTPmessages_FeaturedStickers &result) {\n\t\t_featuredEmojiUpdateRequest = 0;\n\t\t_session->data().stickers().featuredEmojiSetsReceived(result);\n\t}).fail([=] {\n\t\t_featuredEmojiUpdateRequest = 0;\n\t\t_session->data().stickers().setLastFeaturedEmojiUpdate(crl::now());\n\t\tLOG((\"App Fail: Failed to get featured emoji!\"));\n\t}).send();\n}\n\nvoid ApiWrap::requestSavedGifs(TimeId now) {\n\tif (!_session->data().stickers().savedGifsUpdateNeeded(now)\n\t\t|| _savedGifsUpdateRequest) {\n\t\treturn;\n\t}\n\t_savedGifsUpdateRequest = request(MTPmessages_GetSavedGifs(\n\t\tMTP_long(Api::CountSavedGifsHash(_session))\n\t)).done([=](const MTPmessages_SavedGifs &result) {\n\t\t_session->data().stickers().setLastSavedGifsUpdate(crl::now());\n\t\t_savedGifsUpdateRequest = 0;\n\n\t\tswitch (result.type()) {\n\t\tcase mtpc_messages_savedGifsNotModified: return;\n\t\tcase mtpc_messages_savedGifs: {\n\t\t\tauto &d = result.c_messages_savedGifs();\n\t\t\t_session->data().stickers().gifsReceived(\n\t\t\t\td.vgifs().v,\n\t\t\t\td.vhash().v);\n\t\t} return;\n\t\tdefault: Unexpected(\"Type in ApiWrap::savedGifsDone()\");\n\t\t}\n\t}).fail([=] {\n\t\t_session->data().stickers().setLastSavedGifsUpdate(crl::now());\n\t\t_savedGifsUpdateRequest = 0;\n\n\t\tLOG((\"App Fail: Failed to get saved gifs!\"));\n\t}).send();\n}\n\nvoid ApiWrap::readFeaturedSetDelayed(uint64 setId) {\n\tif (!_featuredSetsRead.contains(setId)) {\n\t\t_featuredSetsRead.insert(setId);\n\t\t_featuredSetsReadTimer.callOnce(kReadFeaturedSetsTimeout);\n\t}\n}\n\nvoid ApiWrap::readFeaturedSets() {\n\tconst auto &sets = _session->data().stickers().sets();\n\tauto count = _session->data().stickers().featuredSetsUnreadCount();\n\tQVector<MTPlong> wrappedIds;\n\twrappedIds.reserve(_featuredSetsRead.size());\n\tfor (const auto setId : _featuredSetsRead) {\n\t\tconst auto it = sets.find(setId);\n\t\tif (it != sets.cend()) {\n\t\t\tit->second->flags &= ~Data::StickersSetFlag::Unread;\n\t\t\twrappedIds.append(MTP_long(setId));\n\t\t\tif (count) {\n\t\t\t\t--count;\n\t\t\t}\n\t\t}\n\t}\n\t_featuredSetsRead.clear();\n\n\tif (!wrappedIds.empty()) {\n\t\tauto requestData = MTPmessages_ReadFeaturedStickers(\n\t\t\tMTP_vector<MTPlong>(wrappedIds));\n\t\trequest(std::move(requestData)).done([=] {\n\t\t\tlocal().writeFeaturedStickers();\n\t\t\t_session->data().stickers().notifyUpdated(\n\t\t\t\tData::StickersType::Stickers);\n\t\t}).send();\n\n\t\t_session->data().stickers().setFeaturedSetsUnreadCount(count);\n\t}\n}\n\nvoid ApiWrap::resolveJumpToDate(\n\t\tDialogs::Key chat,\n\t\tconst QDate &date,\n\t\tFn<void(not_null<PeerData*>, MsgId)> callback) {\n\tif (const auto peer = chat.peer()) {\n\t\tconst auto topic = chat.topic();\n\t\tconst auto sublist = chat.sublist();\n\t\tconst auto rootId = topic ? topic->rootId() : MsgId();\n\t\tconst auto monoforumPeerId = sublist\n\t\t\t? sublist->sublistPeer()->id\n\t\t\t: PeerId();\n\t\tresolveJumpToHistoryDate(\n\t\t\tpeer,\n\t\t\trootId,\n\t\t\tmonoforumPeerId,\n\t\t\tdate,\n\t\t\tstd::move(callback));\n\t}\n}\n\ntemplate <typename Callback>\nvoid ApiWrap::requestMessageAfterDate(\n\tnot_null<PeerData*> peer,\n\tMsgId topicRootId,\n\tPeerId monoforumPeerId,\n\tconst QDate &date,\n\tCallback &&callback) {\n\t// API returns a message with date <= offset_date.\n\t// So we request a message with offset_date = desired_date - 1 and add_offset = -1.\n\t// This should give us the first message with date >= desired_date.\n\tconst auto offsetId = 0;\n\tconst auto offsetDate = static_cast<int>(date.startOfDay().toSecsSinceEpoch()) - 1;\n\tconst auto addOffset = -1;\n\tconst auto limit = 1;\n\tconst auto maxId = 0;\n\tconst auto minId = 0;\n\tconst auto historyHash = uint64(0);\n\n\tauto send = [&](auto &&serialized) {\n\t\trequest(std::move(serialized)).done([\n\t\t\t=,\n\t\t\tcallback = std::forward<Callback>(callback)\n\t\t](const MTPmessages_Messages &result) {\n\t\t\tconst auto handleMessages = [&](auto &messages) {\n\t\t\t\t_session->data().processUsers(messages.vusers());\n\t\t\t\t_session->data().processChats(messages.vchats());\n\t\t\t\treturn &messages.vmessages().v;\n\t\t\t};\n\t\t\tconst auto list = result.match([&](\n\t\t\t\t\tconst MTPDmessages_messages &data) {\n\t\t\t\tpeer->processTopics(data.vtopics());\n\t\t\t\treturn handleMessages(data);\n\t\t\t}, [&](const MTPDmessages_messagesSlice &data) {\n\t\t\t\tpeer->processTopics(data.vtopics());\n\t\t\t\treturn handleMessages(data);\n\t\t\t}, [&](const MTPDmessages_channelMessages &data) {\n\t\t\t\tif (const auto channel = peer->asChannel()) {\n\t\t\t\t\tchannel->ptsReceived(data.vpts().v);\n\t\t\t\t} else {\n\t\t\t\t\tLOG((\"API Error: received messages.channelMessages when \"\n\t\t\t\t\t\t\"no channel was passed! (ApiWrap::jumpToDate)\"));\n\t\t\t\t}\n\t\t\t\tpeer->processTopics(data.vtopics());\n\t\t\t\treturn handleMessages(data);\n\t\t\t}, [&](const MTPDmessages_messagesNotModified &) {\n\t\t\t\tLOG((\"API Error: received messages.messagesNotModified! \"\n\t\t\t\t\t\"(ApiWrap::jumpToDate)\"));\n\t\t\t\treturn (const QVector<MTPMessage>*)nullptr;\n\t\t\t});\n\t\t\tif (list) {\n\t\t\t\t_session->data().processMessages(\n\t\t\t\t\t*list,\n\t\t\t\t\tNewMessageType::Existing);\n\t\t\t\tfor (const auto &message : *list) {\n\t\t\t\t\tif (DateFromMessage(message) >= offsetDate) {\n\t\t\t\t\t\tcallback(IdFromMessage(message));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcallback(ShowAtUnreadMsgId);\n\t\t}).send();\n\t};\n\tif (topicRootId) {\n\t\tsend(MTPmessages_GetReplies(\n\t\t\tpeer->input(),\n\t\t\tMTP_int(topicRootId),\n\t\t\tMTP_int(offsetId),\n\t\t\tMTP_int(offsetDate),\n\t\t\tMTP_int(addOffset),\n\t\t\tMTP_int(limit),\n\t\t\tMTP_int(maxId),\n\t\t\tMTP_int(minId),\n\t\t\tMTP_long(historyHash)));\n\t} else if (monoforumPeerId) {\n\t\tsend(MTPmessages_GetSavedHistory(\n\t\t\tMTP_flags(MTPmessages_GetSavedHistory::Flag::f_parent_peer),\n\t\t\tpeer->input(),\n\t\t\tsession().data().peer(monoforumPeerId)->input(),\n\t\t\tMTP_int(offsetId),\n\t\t\tMTP_int(offsetDate),\n\t\t\tMTP_int(addOffset),\n\t\t\tMTP_int(limit),\n\t\t\tMTP_int(maxId),\n\t\t\tMTP_int(minId),\n\t\t\tMTP_long(historyHash)));\n\t} else {\n\t\tsend(MTPmessages_GetHistory(\n\t\t\tpeer->input(),\n\t\t\tMTP_int(offsetId),\n\t\t\tMTP_int(offsetDate),\n\t\t\tMTP_int(addOffset),\n\t\t\tMTP_int(limit),\n\t\t\tMTP_int(maxId),\n\t\t\tMTP_int(minId),\n\t\t\tMTP_long(historyHash)));\n\t}\n}\n\nvoid ApiWrap::resolveJumpToHistoryDate(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tconst QDate &date,\n\t\tFn<void(not_null<PeerData*>, MsgId)> callback) {\n\tif (const auto channel = peer->migrateTo()) {\n\t\treturn resolveJumpToHistoryDate(\n\t\t\tchannel,\n\t\t\ttopicRootId,\n\t\t\tmonoforumPeerId,\n\t\t\tdate,\n\t\t\tstd::move(callback));\n\t}\n\tconst auto jumpToDateInPeer = [=] {\n\t\trequestMessageAfterDate(\n\t\t\tpeer,\n\t\t\ttopicRootId,\n\t\t\tmonoforumPeerId,\n\t\t\tdate,\n\t\t\t[=](MsgId itemId) { callback(peer, itemId); });\n\t};\n\tconst auto migrated = (topicRootId || monoforumPeerId)\n\t\t? nullptr\n\t\t: peer->migrateFrom();\n\tif (migrated) {\n\t\trequestMessageAfterDate(\n\t\t\tmigrated,\n\t\t\tMsgId(),\n\t\t\tPeerId(),\n\t\t\tdate,\n\t\t\t[=](MsgId itemId) {\n\t\t\t\tif (itemId) {\n\t\t\t\t\tcallback(migrated, itemId);\n\t\t\t\t} else {\n\t\t\t\t\tjumpToDateInPeer();\n\t\t\t\t}\n\t\t\t});\n\t} else {\n\t\tjumpToDateInPeer();\n\t}\n}\n\nvoid ApiWrap::requestHistory(\n\t\tnot_null<History*> history,\n\t\tMsgId messageId,\n\t\tSliceType slice) {\n\tconst auto peer = history->peer;\n\tconst auto key = HistoryRequest{\n\t\tpeer,\n\t\tmessageId,\n\t\tslice,\n\t};\n\tif (_historyRequests.contains(key)) {\n\t\treturn;\n\t}\n\n\tconst auto prepared = Api::PrepareHistoryRequest(peer, messageId, slice);\n\tauto &histories = history->owner().histories();\n\tconst auto requestType = Data::Histories::RequestType::History;\n\thistories.sendRequest(history, requestType, [=](Fn<void()> finish) {\n\t\treturn request(\n\t\t\tstd::move(prepared)\n\t\t).done([=](const Api::HistoryRequestResult &result) {\n\t\t\t_historyRequests.remove(key);\n\t\t\tauto parsed = Api::ParseHistoryResult(\n\t\t\t\tpeer,\n\t\t\t\tmessageId,\n\t\t\t\tslice,\n\t\t\t\tresult);\n\t\t\thistory->messages().addSlice(\n\t\t\t\tstd::move(parsed.messageIds),\n\t\t\t\tparsed.noSkipRange,\n\t\t\t\tparsed.fullCount);\n\t\t\tfinish();\n\t\t}).fail([=] {\n\t\t\t_historyRequests.remove(key);\n\t\t\tfinish();\n\t\t}).send();\n\t});\n\t_historyRequests.emplace(key);\n}\n\nvoid ApiWrap::requestSharedMedia(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tSharedMediaType type,\n\t\tMsgId messageId,\n\t\tSliceType slice) {\n\tconst auto key = SharedMediaRequest{\n\t\tpeer,\n\t\ttopicRootId,\n\t\tmonoforumPeerId,\n\t\ttype,\n\t\tmessageId,\n\t\tslice,\n\t};\n\tif (_sharedMediaRequests.contains(key)) {\n\t\treturn;\n\t}\n\n\tconst auto prepared = Api::PrepareSearchRequest(\n\t\tpeer,\n\t\ttopicRootId,\n\t\tmonoforumPeerId,\n\t\ttype,\n\t\tQString(),\n\t\tmessageId,\n\t\tslice);\n\tif (!prepared) {\n\t\treturn;\n\t}\n\n\tconst auto history = _session->data().history(peer);\n\tauto &histories = history->owner().histories();\n\tconst auto requestType = Data::Histories::RequestType::History;\n\thistories.sendRequest(history, requestType, [=](Fn<void()> finish) {\n\t\treturn request(\n\t\t\tstd::move(*prepared)\n\t\t).done([=](const Api::SearchRequestResult &result) {\n\t\t\t_sharedMediaRequests.remove(key);\n\t\t\tauto parsed = Api::ParseSearchResult(\n\t\t\t\tpeer,\n\t\t\t\ttype,\n\t\t\t\tmessageId,\n\t\t\t\tslice,\n\t\t\t\tresult);\n\t\t\tsharedMediaDone(\n\t\t\t\tpeer,\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\ttype,\n\t\t\t\tstd::move(parsed));\n\t\t\tfinish();\n\t\t}).fail([=] {\n\t\t\t_sharedMediaRequests.remove(key);\n\t\t\tfinish();\n\t\t}).send();\n\t});\n\t_sharedMediaRequests.emplace(key);\n}\n\nvoid ApiWrap::sharedMediaDone(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tSharedMediaType type,\n\t\tApi::SearchResult &&parsed) {\n\tconst auto topic = peer->forumTopicFor(topicRootId);\n\tconst auto sublist = peer->monoforumSublistFor(monoforumPeerId);\n\tif ((topicRootId && !topic) || (monoforumPeerId && !sublist)) {\n\t\treturn;\n\t}\n\tconst auto hasMessages = !parsed.messageIds.empty();\n\t_session->storage().add(Storage::SharedMediaAddSlice(\n\t\tpeer->id,\n\t\ttopicRootId,\n\t\tmonoforumPeerId,\n\t\ttype,\n\t\tstd::move(parsed.messageIds),\n\t\tparsed.noSkipRange,\n\t\tparsed.fullCount\n\t));\n\tif (type == SharedMediaType::Pinned && hasMessages) {\n\t\tpeer->owner().history(peer)->setHasPinnedMessages(true);\n\t\tif (topic) {\n\t\t\ttopic->setHasPinnedMessages(true);\n\t\t}\n\t\tif (sublist) {\n\t\t\tsublist->setHasPinnedMessages(true);\n\t\t}\n\t}\n}\n\nmtpRequestId ApiWrap::requestGlobalMedia(\n\t\tStorage::SharedMediaType type,\n\t\tconst QString &query,\n\t\tint32 offsetRate,\n\t\tData::MessagePosition offsetPosition,\n\t\tFn<void(Api::GlobalMediaResult)> done) {\n\tauto prepared = Api::PrepareGlobalMediaRequest(\n\t\t_session,\n\t\toffsetRate,\n\t\toffsetPosition,\n\t\ttype,\n\t\tquery);\n\tif (!prepared) {\n\t\tdone({});\n\t\treturn 0;\n\t}\n\treturn request(\n\t\tstd::move(*prepared)\n\t).done([=](const Api::SearchRequestResult &result) {\n\t\tdone(Api::ParseGlobalMediaResult(_session, result));\n\t}).fail([=] {\n\t\tdone({});\n\t}).send();\n}\n\nvoid ApiWrap::sendAction(const SendAction &action) {\n\tif (!action.options.scheduled\n\t\t&& !action.options.shortcutId\n\t\t&& !action.replaceMediaOf) {\n\t\tconst auto topicRootId = action.replyTo.topicRootId;\n\t\tconst auto topic = topicRootId\n\t\t\t? action.history->peer->forumTopicFor(topicRootId)\n\t\t\t: nullptr;\n\t\tconst auto monoforumPeerId = action.replyTo.monoforumPeerId;\n\t\tconst auto sublist = monoforumPeerId\n\t\t\t? action.history->peer->monoforumSublistFor(monoforumPeerId)\n\t\t\t: nullptr;\n\t\tif (topic) {\n\t\t\ttopic->readTillEnd();\n\t\t} else if (sublist) {\n\t\t\tsublist->readTillEnd();\n\t\t} else {\n\t\t\t_session->data().histories().readInbox(action.history);\n\t\t}\n\t\taction.history->getReadyFor(ShowAtTheEndMsgId);\n\t}\n\t_sendActions.fire_copy(action);\n}\n\nvoid ApiWrap::finishForwarding(const SendAction &action) {\n\tconst auto history = action.history;\n\tconst auto topicRootId = action.replyTo.topicRootId;\n\tconst auto monoforumPeerId = action.replyTo.monoforumPeerId;\n\tauto toForward = history->resolveForwardDraft(\n\t\ttopicRootId,\n\t\tmonoforumPeerId);\n\tif (!toForward.items.empty()) {\n\t\tconst auto error = GetErrorForSending(\n\t\t\thistory->peer,\n\t\t\t{\n\t\t\t\t.topicRootId = topicRootId,\n\t\t\t\t.forward = &toForward.items,\n\t\t\t});\n\t\tif (error) {\n\t\t\treturn;\n\t\t}\n\n\t\thistory->setForwardDraft(topicRootId, monoforumPeerId, {});\n\t\tforwardMessages(std::move(toForward), action);\n\t}\n\n\t_session->data().sendHistoryChangeNotifications();\n\tif (!action.options.shortcutId) {\n\t\t_session->changes().historyUpdated(\n\t\t\thistory,\n\t\t\t(action.options.scheduled\n\t\t\t\t? Data::HistoryUpdate::Flag::ScheduledSent\n\t\t\t\t: Data::HistoryUpdate::Flag::MessageSent));\n\t}\n}\n\nvoid ApiWrap::forwardMessages(\n\t\tData::ResolvedForwardDraft &&draft,\n\t\tSendAction action,\n\t\tFnMut<void()> &&successCallback) {\n\tExpects(!draft.items.empty());\n\n\tauto &histories = _session->data().histories();\n\n\tfor (auto i = begin(draft.items); i != end(draft.items);) {\n\t\tconst auto item = *i;\n\t\tif (item->isSavedMusicItem()) {\n\t\t\tSendExistingDocument(MessageToSend(action), item->media()->document());\n\t\t\ti = draft.items.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tif (draft.items.empty()) {\n\t\tif (successCallback) {\n\t\t\tsuccessCallback();\n\t\t}\n\t\treturn;\n\t}\n\n\tstruct SharedCallback {\n\t\tint requestsLeft = 0;\n\t\tFnMut<void()> callback;\n\t};\n\tconst auto shared = successCallback\n\t\t? std::make_shared<SharedCallback>()\n\t\t: std::shared_ptr<SharedCallback>();\n\tif (successCallback) {\n\t\tshared->callback = std::move(successCallback);\n\t}\n\n\tconst auto count = int(draft.items.size());\n\tconst auto genClientSideMessage = action.generateLocal\n\t\t&& (count < 2)\n\t\t&& (draft.options == Data::ForwardOptions::PreserveInfo);\n\tconst auto history = action.history;\n\tconst auto peer = history->peer;\n\n\tif (!action.options.scheduled && !action.options.shortcutId) {\n\t\thistories.readInbox(history);\n\t}\n\tconst auto sendAs = action.options.sendAs;\n\tconst auto silentPost = ShouldSendSilent(peer, action.options);\n\n\tusing SendFlag = MTPmessages_ForwardMessages::Flag;\n\tauto flags = MessageFlags();\n\tauto sendFlags = SendFlag() | SendFlag();\n\tFillMessagePostFlags(action, peer, flags);\n\tif (silentPost) {\n\t\tsendFlags |= SendFlag::f_silent;\n\t}\n\tif (action.options.scheduled) {\n\t\tflags |= MessageFlag::IsOrWasScheduled;\n\t\tsendFlags |= SendFlag::f_schedule_date;\n\t\tif (action.options.scheduleRepeatPeriod) {\n\t\t\tsendFlags |= SendFlag::f_schedule_repeat_period;\n\t\t}\n\t}\n\tif (action.options.shortcutId) {\n\t\tflags |= MessageFlag::ShortcutMessage;\n\t\tsendFlags |= SendFlag::f_quick_reply_shortcut;\n\t}\n\tif (action.options.effectId) {\n\t\tsendFlags |= SendFlag::f_effect;\n\t}\n\tif (draft.options != Data::ForwardOptions::PreserveInfo) {\n\t\tsendFlags |= SendFlag::f_drop_author;\n\t}\n\tif (draft.options == Data::ForwardOptions::NoNamesAndCaptions) {\n\t\tsendFlags |= SendFlag::f_drop_media_captions;\n\t}\n\tif (sendAs) {\n\t\tsendFlags |= SendFlag::f_send_as;\n\t}\n\tif (action.options.suggest) {\n\t\tsendFlags |= SendFlag::f_suggested_post;\n\t}\n\tconst auto kGeneralId = Data::ForumTopic::kGeneralId;\n\tconst auto topicRootId = action.replyTo.topicRootId;\n\tconst auto topMsgId = (topicRootId == kGeneralId)\n\t\t? MsgId(0)\n\t\t: topicRootId;\n\tif (topMsgId) {\n\t\tsendFlags |= SendFlag::f_top_msg_id;\n\t}\n\tconst auto monoforumPeerId = action.replyTo.monoforumPeerId;\n\tconst auto monoforumPeer = monoforumPeerId\n\t\t? session().data().peer(monoforumPeerId).get()\n\t\t: nullptr;\n\tif (monoforumPeer || (action.options.suggest && action.replyTo)) {\n\t\tsendFlags |= SendFlag::f_reply_to;\n\t}\n\n\tauto forwardFrom = draft.items.front()->history()->peer;\n\tauto ids = QVector<MTPint>();\n\tauto randomIds = QVector<MTPlong>();\n\tauto localIds = std::shared_ptr<base::flat_map<uint64, FullMsgId>>();\n\n\tconst auto sendAccumulated = [&] {\n\t\tif (shared) {\n\t\t\t++shared->requestsLeft;\n\t\t}\n\t\tconst auto idsCopy = localIds;\n\t\tconst auto scheduled = action.options.scheduled;\n\t\tconst auto starsPaid = std::min(\n\t\t\taction.options.starsApproved,\n\t\t\tint(ids.size() * peer->starsPerMessageChecked()));\n\t\tauto oneFlags = sendFlags;\n\t\tif (starsPaid) {\n\t\t\taction.options.starsApproved -= starsPaid;\n\t\t\toneFlags |= SendFlag::f_allow_paid_stars;\n\t\t}\n\t\tauto buildMessage = [=](\n\t\t\t\tnot_null<History*> history,\n\t\t\t\tFullReplyTo replyTo)\n\t\t\t-> Data::Histories::PreparedMessage {\n\t\t\tconst auto kGeneralId = Data::ForumTopic::kGeneralId;\n\t\t\tconst auto realTopMsgId = (replyTo.topicRootId == kGeneralId)\n\t\t\t\t? MsgId(0)\n\t\t\t\t: replyTo.topicRootId;\n\t\t\tauto flags = oneFlags;\n\t\t\tif (realTopMsgId) {\n\t\t\t\tflags |= SendFlag::f_top_msg_id;\n\t\t\t} else {\n\t\t\t\tflags &= ~SendFlag::f_top_msg_id;\n\t\t\t}\n\t\t\treturn MTPmessages_ForwardMessages(\n\t\t\t\tMTP_flags(flags),\n\t\t\t\tforwardFrom->input(),\n\t\t\t\tMTP_vector<MTPint>(ids),\n\t\t\t\tMTP_vector<MTPlong>(randomIds),\n\t\t\t\thistory->peer->input(),\n\t\t\t\tMTP_int(realTopMsgId),\n\t\t\t\t(action.options.suggest\n\t\t\t\t\t? ReplyToForMTP(history, replyTo)\n\t\t\t\t\t: monoforumPeer\n\t\t\t\t\t? MTP_inputReplyToMonoForum(\n\t\t\t\t\t\tmonoforumPeer->input())\n\t\t\t\t\t: MTPInputReplyTo()),\n\t\t\t\tMTP_int(action.options.scheduled),\n\t\t\t\tMTP_int(action.options.scheduleRepeatPeriod),\n\t\t\t\t(sendAs\n\t\t\t\t\t? sendAs->input()\n\t\t\t\t\t: MTP_inputPeerEmpty()),\n\t\t\t\tData::ShortcutIdToMTP(\n\t\t\t\t\t&history->session(),\n\t\t\t\t\taction.options.shortcutId),\n\t\t\t\tMTP_long(action.options.effectId),\n\t\t\t\tMTPint(),\n\t\t\t\tMTP_long(starsPaid),\n\t\t\t\tApi::SuggestToMTP(action.options.suggest));\n\t\t};\n\t\thistories.sendPreparedMessage(\n\t\t\thistory,\n\t\t\tFullReplyTo{ .topicRootId = topicRootId },\n\t\t\tuint64(0),\n\t\t\tstd::move(buildMessage),\n\t\t\t[=](const MTPUpdates &result, const MTP::Response &) {\n\t\t\t\t{ // Delete after forward.\n\t\t\t\t\tauto toDelete = MessageIdsList();\n\t\t\t\t\tfor (const auto &id : ids) {\n\t\t\t\t\t\tconst auto it = ranges::find_if(_deleteAfterForward, [&](const FullMsgId &m) {\n\t\t\t\t\t\t\treturn m.msg.bare == id.v && forwardFrom->id == m.peer;\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif (it != _deleteAfterForward.end()) {\n\t\t\t\t\t\t\ttoDelete.push_back(*it);\n\t\t\t\t\t\t\t_deleteAfterForward.erase(it);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tconst auto count = toDelete.size();\n\t\t\t\t\tif (count > 0) {\n\t\t\t\t\t\t_session->data().histories().deleteMessages(\n\t\t\t\t\t\t\ttoDelete,\n\t\t\t\t\t\t\ttrue);\n\t\t\t\t\t\tif (const auto w = Core::App().activeWindow()) {\n\t\t\t\t\t\t\tif (const auto c = w->sessionController()) {\n\t\t\t\t\t\t\t\tc->uiShow()->showToast((count > 1)\n\t\t\t\t\t\t\t\t\t? (u\"Delete %1 messages.\"_q.arg(count))\n\t\t\t\t\t\t\t\t\t: (u\"Delete %1 message.\"_q.arg(count)));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!scheduled) {\n\t\t\t\t\t_session->api().updates().checkForSentToScheduled(\n\t\t\t\t\t\tresult);\n\t\t\t\t}\n\t\t\t\tif (shared && !--shared->requestsLeft) {\n\t\t\t\t\tshared->callback();\n\t\t\t\t}\n\t\t\t\tif (peer->isSelf() && _session->premium()) {\n\t\t\t\t\tProcessRecentSelfForwards(\n\t\t\t\t\t\t_session,\n\t\t\t\t\t\tresult,\n\t\t\t\t\t\tpeer->id,\n\t\t\t\t\t\tforwardFrom->id);\n\t\t\t\t}\n\t\t\t},\n\t\t\t[=](const MTP::Error &error, const MTP::Response &) {\n\t\t\t\tif (idsCopy) {\n\t\t\t\t\tfor (const auto &[randomId, itemId] : *idsCopy) {\n\t\t\t\t\t\t_session->api().sendMessageFail(\n\t\t\t\t\t\t\terror,\n\t\t\t\t\t\t\tpeer,\n\t\t\t\t\t\t\trandomId,\n\t\t\t\t\t\t\titemId);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t_session->api().sendMessageFail(error, peer);\n\t\t\t\t}\n\t\t\t});\n\n\t\tids.resize(0);\n\t\trandomIds.resize(0);\n\t\tlocalIds = nullptr;\n\t};\n\n\tids.reserve(count);\n\trandomIds.reserve(count);\n\tfor (const auto &item : draft.items) {\n\t\tconst auto randomId = base::RandomValue<uint64>();\n\t\tif (genClientSideMessage) {\n\t\t\tconst auto newId = FullMsgId(\n\t\t\t\tpeer->id,\n\t\t\t\t_session->data().nextLocalMessageId());\n\t\t\thistory->addNewLocalMessage({\n\t\t\t\t.id = newId.msg,\n\t\t\t\t.flags = flags,\n\t\t\t\t.from = NewMessageFromId(action),\n\t\t\t\t.replyTo = {\n\t\t\t\t\t.topicRootId = topMsgId,\n\t\t\t\t\t.monoforumPeerId = monoforumPeerId,\n\t\t\t\t},\n\t\t\t\t.date = NewMessageDate(action.options),\n\t\t\t\t.shortcutId = action.options.shortcutId,\n\t\t\t\t.starsPaid = action.options.starsApproved,\n\t\t\t\t.postAuthor = NewMessagePostAuthor(action),\n\t\t\t\t.suggest = HistoryMessageSuggestInfo(action.options),\n\t\t\t\t// forwarded messages don't have effects\n\t\t\t\t//.effectId = action.options.effectId,\n\t\t\t}, item);\n\t\t\t_session->data().registerMessageRandomId(randomId, newId);\n\t\t\tif (!localIds) {\n\t\t\t\tlocalIds = std::make_shared<base::flat_map<uint64, FullMsgId>>();\n\t\t\t}\n\t\t\tlocalIds->emplace(randomId, newId);\n\t\t}\n\t\tconst auto newFrom = item->history()->peer;\n\t\tif (forwardFrom != newFrom) {\n\t\t\tsendAccumulated();\n\t\t\tforwardFrom = newFrom;\n\t\t}\n\t\tids.push_back(MTP_int(item->id));\n\t\trandomIds.push_back(MTP_long(randomId));\n\t}\n\tsendAccumulated();\n\t_session->data().sendHistoryChangeNotifications();\n}\n\nvoid ApiWrap::shareContact(\n\t\tconst QString &phone,\n\t\tconst QString &firstName,\n\t\tconst QString &lastName,\n\t\tconst SendAction &action,\n\t\tFn<void(bool)> done) {\n\tconst auto userId = UserId(0);\n\tsendSharedContact(\n\t\tphone,\n\t\tfirstName,\n\t\tlastName,\n\t\tuserId,\n\t\taction,\n\t\tstd::move(done));\n}\n\nvoid ApiWrap::shareContact(\n\t\tnot_null<UserData*> user,\n\t\tconst SendAction &action,\n\t\tFn<void(bool)> done) {\n\tconst auto userId = peerToUser(user->id);\n\tconst auto phone = _session->data().findContactPhone(user);\n\tif (phone.isEmpty()) {\n\t\tif (done) {\n\t\t\tdone(false);\n\t\t}\n\t\treturn;\n\t}\n\treturn sendSharedContact(\n\t\tphone,\n\t\tuser->firstName,\n\t\tuser->lastName,\n\t\tuserId,\n\t\taction,\n\t\tstd::move(done));\n}\n\nvoid ApiWrap::sendSharedContact(\n\t\tconst QString &phone,\n\t\tconst QString &firstName,\n\t\tconst QString &lastName,\n\t\tUserId userId,\n\t\tconst SendAction &action,\n\t\tFn<void(bool)> done) {\n\tsendAction(action);\n\n\tconst auto history = action.history;\n\tconst auto peer = history->peer;\n\n\tconst auto newId = FullMsgId(\n\t\tpeer->id,\n\t\t_session->data().nextLocalMessageId());\n\tauto flags = NewMessageFlags(peer);\n\tif (action.replyTo) {\n\t\tflags |= MessageFlag::HasReplyInfo;\n\t}\n\tFillMessagePostFlags(action, peer, flags);\n\tif (action.options.scheduled) {\n\t\tflags |= MessageFlag::IsOrWasScheduled;\n\t}\n\tif (action.options.shortcutId) {\n\t\tflags |= MessageFlag::ShortcutMessage;\n\t}\n\tconst auto item = history->addNewLocalMessage({\n\t\t.id = newId.msg,\n\t\t.flags = flags,\n\t\t.from = NewMessageFromId(action),\n\t\t.replyTo = action.replyTo,\n\t\t.date = NewMessageDate(action.options),\n\t\t.shortcutId = action.options.shortcutId,\n\t\t.starsPaid = action.options.starsApproved,\n\t\t.postAuthor = NewMessagePostAuthor(action),\n\t\t.effectId = action.options.effectId,\n\t\t.suggest = HistoryMessageSuggestInfo(action.options),\n\t}, TextWithEntities(), MTP_messageMediaContact(\n\t\tMTP_string(phone),\n\t\tMTP_string(firstName),\n\t\tMTP_string(lastName),\n\t\tMTP_string(), // vcard\n\t\tMTP_long(userId.bare)));\n\n\tconst auto media = MTP_inputMediaContact(\n\t\tMTP_string(phone),\n\t\tMTP_string(firstName),\n\t\tMTP_string(lastName),\n\t\tMTP_string()); // vcard\n\tsendMedia(item, media, action.options, std::move(done));\n\n\t_session->data().sendHistoryChangeNotifications();\n\t_session->changes().historyUpdated(\n\t\thistory,\n\t\t(action.options.scheduled\n\t\t\t? Data::HistoryUpdate::Flag::ScheduledSent\n\t\t\t: Data::HistoryUpdate::Flag::MessageSent));\n}\n\nvoid ApiWrap::sendVoiceMessage(\n\t\tQByteArray result,\n\t\tVoiceWaveform waveform,\n\t\tcrl::time duration,\n\t\tbool video,\n\t\tconst SendAction &action) {\n\tconst auto caption = TextWithTags();\n\tconst auto to = FileLoadTaskOptions(action);\n\t_fileLoader->addTask(\n\t\tstd::make_unique<FileLoadTask>(FileLoadTask::VoiceArgs{\n\t\t\t.session = &session(),\n\t\t\t.voice = result,\n\t\t\t.duration = duration,\n\t\t\t.waveform = waveform,\n\t\t\t.video = video,\n\t\t\t.to = to,\n\t\t\t.caption = caption,\n\t\t}));\n}\n\nvoid ApiWrap::editMedia(\n\t\tUi::PreparedList &&list,\n\t\tSendMediaType type,\n\t\tTextWithTags &&caption,\n\t\tconst SendAction &action) {\n\tif (list.files.empty()) return;\n\n\tauto &file = list.files.front();\n\tauto to = FileLoadTaskOptions(action);\n\tconst auto existing = to.replaceMediaOf\n\t\t? session().data().message(action.history->peer, to.replaceMediaOf)\n\t\t: nullptr;\n\tif (existing && existing->computeSuggestionActions()\n\t\t== SuggestionActions::AcceptAndDecline) {\n\t\tto.replyTo.messageId = {\n\t\t\taction.history->peer->id,\n\t\t\tto.replaceMediaOf\n\t\t};\n\t\tto.replyTo.monoforumPeerId = existing->sublistPeerId();\n\t\tto.replaceMediaOf = MsgId();\n\t}\n\tconst auto forceFile = (type == SendMediaType::File)\n\t\t&& (file.type == Ui::PreparedFile::Type::Video);\n\t_fileLoader->addTask(std::make_unique<FileLoadTask>(FileLoadTask::Args{\n\t\t.session = &session(),\n\t\t.filepath = file.path,\n\t\t.content = file.content,\n\t\t.information = std::move(file.information),\n\t\t.videoCover = (file.videoCover\n\t\t\t? std::make_unique<FileLoadTask>(FileLoadTask::Args{\n\t\t\t\t.session = &session(),\n\t\t\t\t.filepath = file.videoCover->path,\n\t\t\t\t.content = file.videoCover->content,\n\t\t\t\t.information = std::move(file.videoCover->information),\n\t\t\t\t.videoCover = nullptr,\n\t\t\t\t.type = SendMediaType::Photo,\n\t\t\t\t.to = to,\n\t\t\t\t.caption = TextWithTags(),\n\t\t\t\t.spoiler = false,\n\t\t\t\t.album = nullptr,\n\t\t\t\t.forceFile = false,\n\t\t\t\t.sendLargePhotos = false,\n\t\t\t\t.idOverride = 0,\n\t\t\t})\n\t\t\t: nullptr),\n\t\t.type = type,\n\t\t.to = to,\n\t\t.caption = caption,\n\t\t.spoiler = file.spoiler,\n\t\t.album = nullptr,\n\t\t.forceFile = forceFile,\n\t\t.sendLargePhotos = file.sendLargePhotos,\n\t\t.idOverride = 0,\n\t\t.displayName = file.displayName,\n\t}));\n}\n\nvoid ApiWrap::sendFiles(\n\t\tUi::PreparedList &&list,\n\t\tSendMediaType type,\n\t\tstd::shared_ptr<SendingAlbum> album,\n\t\tconst SendAction &action) {\n\tconst auto to = FileLoadTaskOptions(action);\n\tif (album) {\n\t\talbum->options = to.options;\n\t}\n\tauto tasks = std::vector<std::unique_ptr<Task>>();\n\ttasks.reserve(list.files.size());\n\tfor (auto &file : list.files) {\n\t\tconst auto uploadWithType = !album\n\t\t\t? type\n\t\t\t: (file.type == Ui::PreparedFile::Type::Photo\n\t\t\t\t&& type != SendMediaType::File)\n\t\t\t? SendMediaType::Photo\n\t\t\t: SendMediaType::File;\n\t\tconst auto forceFile = (type == SendMediaType::File)\n\t\t\t&& (file.type == Ui::PreparedFile::Type::Video);\n\t\ttasks.push_back(std::make_unique<FileLoadTask>(FileLoadTask::Args{\n\t\t\t.session = &session(),\n\t\t\t.filepath = file.path,\n\t\t\t.content = file.content,\n\t\t\t.information = std::move(file.information),\n\t\t\t.videoCover = (file.videoCover\n\t\t\t\t? std::make_unique<FileLoadTask>(FileLoadTask::Args{\n\t\t\t\t\t.session = &session(),\n\t\t\t\t\t.filepath = file.videoCover->path,\n\t\t\t\t\t.content = file.videoCover->content,\n\t\t\t\t\t.information = std::move(file.videoCover->information),\n\t\t\t\t\t.videoCover = nullptr,\n\t\t\t\t\t.type = SendMediaType::Photo,\n\t\t\t\t\t.to = to,\n\t\t\t\t\t.caption = TextWithTags(),\n\t\t\t\t\t.spoiler = false,\n\t\t\t\t\t.album = nullptr,\n\t\t\t\t\t.forceFile = false,\n\t\t\t\t\t.sendLargePhotos = false,\n\t\t\t\t\t.idOverride = 0,\n\t\t\t\t})\n\t\t\t\t: nullptr),\n\t\t\t.type = uploadWithType,\n\t\t\t.to = to,\n\t\t\t.caption = std::move(file.caption),\n\t\t\t.spoiler = file.spoiler,\n\t\t\t.album = album,\n\t\t\t.forceFile = forceFile,\n\t\t\t.sendLargePhotos = file.sendLargePhotos,\n\t\t\t.idOverride = 0,\n\t\t\t.displayName = file.displayName,\n\t\t}));\n\t}\n\tif (album) {\n\t\t_sendingAlbums.emplace(album->groupId, album);\n\t\talbum->items.reserve(tasks.size());\n\t\tfor (const auto &task : tasks) {\n\t\t\talbum->items.emplace_back(task->id());\n\t\t}\n\t}\n\t_fileLoader->addTasks(std::move(tasks));\n}\n\nvoid ApiWrap::sendFile(\n\t\tconst QByteArray &fileContent,\n\t\tSendMediaType type,\n\t\tconst SendAction &action) {\n\tconst auto to = FileLoadTaskOptions(action);\n\tauto caption = TextWithTags();\n\tconst auto spoiler = false;\n\t_fileLoader->addTask(std::make_unique<FileLoadTask>(FileLoadTask::Args{\n\t\t.session = &session(),\n\t\t.filepath = QString(),\n\t\t.content = fileContent,\n\t\t.information = nullptr,\n\t\t.videoCover = nullptr,\n\t\t.type = type,\n\t\t.to = to,\n\t\t.caption = caption,\n\t\t.spoiler = spoiler,\n\t\t.album = nullptr,\n\t\t.forceFile = false,\n\t\t.idOverride = 0\n\t}));\n}\n\nvoid ApiWrap::sendUploadedPhoto(\n\t\tFullMsgId localId,\n\t\tApi::RemoteFileInfo info,\n\t\tApi::SendOptions options) {\n\tif (const auto item = _session->data().message(localId)) {\n\t\tconst auto media = Api::PrepareUploadedPhoto(item, std::move(info));\n\t\tif (const auto groupId = item->groupId()) {\n\t\t\tuploadAlbumMedia(item, groupId, media);\n\t\t} else {\n\t\t\tsendMedia(item, media, options);\n\t\t}\n\t}\n}\n\nvoid ApiWrap::sendUploadedDocument(\n\t\tFullMsgId localId,\n\t\tApi::RemoteFileInfo info,\n\t\tApi::SendOptions options) {\n\tif (const auto item = _session->data().message(localId)) {\n\t\tif (!item->media() || !item->media()->document()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto media = Api::PrepareUploadedDocument(\n\t\t\titem,\n\t\t\tstd::move(info));\n\t\tconst auto groupId = item->groupId();\n\t\tif (groupId) {\n\t\t\tuploadAlbumMedia(item, groupId, media);\n\t\t} else {\n\t\t\tsendMedia(item, media, options);\n\t\t}\n\t}\n}\n\nvoid ApiWrap::cancelLocalItem(not_null<HistoryItem*> item) {\n\tExpects(item->isSending());\n\n\tif (const auto groupId = item->groupId()) {\n\t\tsendAlbumWithCancelled(item, groupId);\n\t}\n}\n\nvoid ApiWrap::sendShortcutMessages(\n\t\tnot_null<PeerData*> peer,\n\t\tBusinessShortcutId id) {\n\tauto ids = QVector<MTPint>();\n\tauto randomIds = QVector<MTPlong>();\n\trequest(MTPmessages_SendQuickReplyMessages(\n\t\tpeer->input(),\n\t\tMTP_int(id),\n\t\tMTP_vector<MTPint>(ids),\n\t\tMTP_vector<MTPlong>(randomIds)\n\t)).done([=](const MTPUpdates &result) {\n\t\tapplyUpdates(result);\n\t}).fail([=](const MTP::Error &error) {\n\t}).send();\n}\n\nvoid ApiWrap::sendMessage(\n\t\tMessageToSend &&message,\n\t\tstd::optional<MsgId> localMessageId) {\n\tconst auto history = message.action.history;\n\tconst auto peer = history->peer;\n\tauto &textWithTags = message.textWithTags;\n\n\tauto action = message.action;\n\taction.generateLocal = true;\n\tsendAction(action);\n\n\tconst auto clearCloudDraft = action.clearDraft;\n\tconst auto draftTopicRootId = action.replyTo.topicRootId;\n\tconst auto draftMonoforumPeerId = action.replyTo.monoforumPeerId;\n\tconst auto replyTo = action.replyTo.messageId\n\t\t? peer->owner().message(action.replyTo.messageId)\n\t\t: nullptr;\n\tconst auto topicRootId = draftTopicRootId\n\t\t? draftTopicRootId\n\t\t: replyTo\n\t\t? replyTo->topicRootId()\n\t\t: Data::ForumTopic::kGeneralId;\n\tconst auto topic = peer->forumTopicFor(topicRootId);\n\tif (!(topic ? Data::CanSendTexts(topic) : Data::CanSendTexts(peer))\n\t\t|| Api::SendDice(message)) {\n\t\treturn;\n\t}\n\tlocal().saveRecentSentHashtags(textWithTags.text);\n\n\tauto sending = TextWithEntities();\n\tauto left = TextWithEntities {\n\t\ttextWithTags.text,\n\t\tTextUtilities::ConvertTextTagsToEntities(textWithTags.tags)\n\t};\n\tauto prepareFlags = Ui::ItemTextOptions(\n\t\thistory,\n\t\t_session->user()).flags;\n\tTextUtilities::PrepareForSending(left, prepareFlags);\n\n\tHistoryItem *lastMessage = nullptr;\n\n\tauto &histories = history->owner().histories();\n\n\tconst auto exactWebPage = !message.webPage.url.isEmpty();\n\tauto isFirst = true;\n\twhile (TextUtilities::CutPart(sending, left, MaxMessageSize)\n\t\t|| (isFirst && exactWebPage)) {\n\t\tTextUtilities::Trim(left);\n\t\tconst auto isLast = left.empty();\n\n\t\tauto newId = FullMsgId(\n\t\t\tpeer->id,\n\t\t\tlocalMessageId\n\t\t\t\t? std::exchange(localMessageId, std::nullopt).value()\n\t\t\t\t: _session->data().nextLocalMessageId());\n\t\tauto randomId = base::RandomValue<uint64>();\n\n\t\tTextUtilities::Trim(sending);\n\n\t\t_session->data().registerMessageRandomId(randomId, newId);\n\t\t_session->data().registerMessageSentData(\n\t\t\trandomId,\n\t\t\tpeer->id,\n\t\t\tsending.text);\n\n\t\tMTPstring msgText(MTP_string(sending.text));\n\t\tauto flags = NewMessageFlags(peer);\n\t\tauto sendFlags = MTPmessages_SendMessage::Flags(0);\n\t\tauto mediaFlags = MTPmessages_SendMedia::Flags(0);\n\t\tif (action.replyTo) {\n\t\t\tflags |= MessageFlag::HasReplyInfo;\n\t\t\tsendFlags |= MTPmessages_SendMessage::Flag::f_reply_to;\n\t\t\tmediaFlags |= MTPmessages_SendMedia::Flag::f_reply_to;\n\t\t}\n\t\tconst auto ignoreWebPage = message.webPage.removed\n\t\t\t|| (exactWebPage && !isLast);\n\t\tconst auto manualWebPage = exactWebPage\n\t\t\t&& !ignoreWebPage\n\t\t\t&& (message.webPage.manual || (isLast && !isFirst));\n\t\tMTPMessageMedia media = MTP_messageMediaEmpty();\n\t\tif (ignoreWebPage) {\n\t\t\tsendFlags |= MTPmessages_SendMessage::Flag::f_no_webpage;\n\t\t} else if (exactWebPage) {\n\t\t\tusing PageFlag = MTPDmessageMediaWebPage::Flag;\n\t\t\tusing PendingFlag = MTPDwebPagePending::Flag;\n\t\t\tconst auto &fields = message.webPage;\n\t\t\tconst auto page = _session->data().webpage(fields.id);\n\t\t\tmedia = MTP_messageMediaWebPage(\n\t\t\t\tMTP_flags(PageFlag()\n\t\t\t\t\t| (manualWebPage ? PageFlag::f_manual : PageFlag())\n\t\t\t\t\t| (fields.forceLargeMedia\n\t\t\t\t\t\t? PageFlag::f_force_large_media\n\t\t\t\t\t\t: PageFlag())\n\t\t\t\t\t| (fields.forceSmallMedia\n\t\t\t\t\t\t? PageFlag::f_force_small_media\n\t\t\t\t\t\t: PageFlag())),\n\t\t\t\tMTP_webPagePending(\n\t\t\t\t\tMTP_flags(PendingFlag::f_url),\n\t\t\t\t\tMTP_long(fields.id),\n\t\t\t\t\tMTP_string(fields.url),\n\t\t\t\t\tMTP_int(page->pendingTill)));\n\t\t}\n\t\tconst auto silentPost = ShouldSendSilent(peer, action.options);\n\t\tFillMessagePostFlags(action, peer, flags);\n\t\tif ((exactWebPage && !ignoreWebPage && message.webPage.invert)\n\t\t\t|| action.options.invertCaption) {\n\t\t\tflags |= MessageFlag::InvertMedia;\n\t\t\tsendFlags |= MTPmessages_SendMessage::Flag::f_invert_media;\n\t\t\tmediaFlags |= MTPmessages_SendMedia::Flag::f_invert_media;\n\t\t}\n\t\tif (silentPost) {\n\t\t\tsendFlags |= MTPmessages_SendMessage::Flag::f_silent;\n\t\t\tmediaFlags |= MTPmessages_SendMedia::Flag::f_silent;\n\t\t}\n\t\tconst auto sentEntities = Api::EntitiesToMTP(\n\t\t\t_session,\n\t\t\tsending.entities,\n\t\t\tApi::ConvertOption::SkipLocal);\n\t\tif (!sentEntities.v.isEmpty()) {\n\t\t\tsendFlags |= MTPmessages_SendMessage::Flag::f_entities;\n\t\t\tmediaFlags |= MTPmessages_SendMedia::Flag::f_entities;\n\t\t}\n\t\tif (clearCloudDraft) {\n\t\t\tsendFlags |= MTPmessages_SendMessage::Flag::f_clear_draft;\n\t\t\tmediaFlags |= MTPmessages_SendMedia::Flag::f_clear_draft;\n\t\t\thistory->clearCloudDraft(draftTopicRootId, draftMonoforumPeerId);\n\t\t\thistory->startSavingCloudDraft(\n\t\t\t\tdraftTopicRootId,\n\t\t\t\tdraftMonoforumPeerId);\n\t\t}\n\t\tconst auto sendAs = action.options.sendAs;\n\t\tif (sendAs) {\n\t\t\tsendFlags |= MTPmessages_SendMessage::Flag::f_send_as;\n\t\t\tmediaFlags |= MTPmessages_SendMedia::Flag::f_send_as;\n\t\t}\n\t\tif (action.options.scheduled) {\n\t\t\tflags |= MessageFlag::IsOrWasScheduled;\n\t\t\tsendFlags |= MTPmessages_SendMessage::Flag::f_schedule_date;\n\t\t\tmediaFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;\n\t\t\tif (action.options.scheduleRepeatPeriod) {\n\t\t\t\tsendFlags |= MTPmessages_SendMessage::Flag::f_schedule_repeat_period;\n\t\t\t\tmediaFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;\n\t\t\t}\n\t\t}\n\t\tif (action.options.shortcutId) {\n\t\t\tflags |= MessageFlag::ShortcutMessage;\n\t\t\tsendFlags |= MTPmessages_SendMessage::Flag::f_quick_reply_shortcut;\n\t\t\tmediaFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;\n\t\t}\n\t\tif (action.options.effectId) {\n\t\t\tsendFlags |= MTPmessages_SendMessage::Flag::f_effect;\n\t\t\tmediaFlags |= MTPmessages_SendMedia::Flag::f_effect;\n\t\t}\n\t\tif (action.options.suggest) {\n\t\t\tsendFlags |= MTPmessages_SendMessage::Flag::f_suggested_post;\n\t\t\tmediaFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;\n\t\t}\n\t\tconst auto starsPaid = std::min(\n\t\t\tpeer->starsPerMessageChecked(),\n\t\t\taction.options.starsApproved);\n\t\tif (starsPaid) {\n\t\t\taction.options.starsApproved -= starsPaid;\n\t\t\tsendFlags |= MTPmessages_SendMessage::Flag::f_allow_paid_stars;\n\t\t\tmediaFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;\n\t\t}\n\t\tlastMessage = history->addNewLocalMessage({\n\t\t\t.id = newId.msg,\n\t\t\t.flags = flags,\n\t\t\t.from = NewMessageFromId(action),\n\t\t\t.replyTo = action.replyTo,\n\t\t\t.date = NewMessageDate(action.options),\n\t\t\t.scheduleRepeatPeriod = action.options.scheduleRepeatPeriod,\n\t\t\t.shortcutId = action.options.shortcutId,\n\t\t\t.starsPaid = starsPaid,\n\t\t\t.postAuthor = NewMessagePostAuthor(action),\n\t\t\t.effectId = action.options.effectId,\n\t\t\t.suggest = HistoryMessageSuggestInfo(action.options),\n\t\t}, sending, media);\n\t\tconst auto done = [=](\n\t\t\t\tconst MTPUpdates &result,\n\t\t\t\tconst MTP::Response &response) {\n\t\t\tif (clearCloudDraft) {\n\t\t\t\thistory->finishSavingCloudDraft(\n\t\t\t\t\tdraftTopicRootId,\n\t\t\t\t\tdraftMonoforumPeerId,\n\t\t\t\t\tApi::UnixtimeFromMsgId(response.outerMsgId));\n\t\t\t}\n\t\t};\n\t\tconst auto fail = [=](\n\t\t\t\tconst MTP::Error &error,\n\t\t\t\tconst MTP::Response &response) {\n\t\t\tif (error.type() == u\"MESSAGE_EMPTY\"_q) {\n\t\t\t\tlastMessage->destroy();\n\t\t\t} else {\n\t\t\t\tsendMessageFail(error, peer, randomId, newId);\n\t\t\t}\n\t\t\tif (clearCloudDraft) {\n\t\t\t\thistory->finishSavingCloudDraft(\n\t\t\t\t\tdraftTopicRootId,\n\t\t\t\t\tdraftMonoforumPeerId,\n\t\t\t\t\tApi::UnixtimeFromMsgId(response.outerMsgId));\n\t\t\t}\n\t\t};\n\t\tconst auto mtpShortcut = Data::ShortcutIdToMTP(\n\t\t\t_session,\n\t\t\taction.options.shortcutId);\n\t\tif (exactWebPage\n\t\t\t&& !ignoreWebPage\n\t\t\t&& (manualWebPage || sending.empty())) {\n\t\t\thistories.sendPreparedMessage(\n\t\t\t\thistory,\n\t\t\t\taction.replyTo,\n\t\t\t\trandomId,\n\t\t\t\tData::Histories::PrepareMessage<MTPmessages_SendMedia>(\n\t\t\t\t\tMTP_flags(mediaFlags),\n\t\t\t\t\tpeer->input(),\n\t\t\t\t\tData::Histories::ReplyToPlaceholder(),\n\t\t\t\t\tData::WebPageForMTP(message.webPage, true),\n\t\t\t\t\tmsgText,\n\t\t\t\t\tMTP_long(randomId),\n\t\t\t\t\tMTPReplyMarkup(),\n\t\t\t\t\tsentEntities,\n\t\t\t\t\tMTP_int(action.options.scheduled),\n\t\t\t\t\tMTP_int(action.options.scheduleRepeatPeriod),\n\t\t\t\t\t(sendAs ? sendAs->input() : MTP_inputPeerEmpty()),\n\t\t\t\t\tmtpShortcut,\n\t\t\t\t\tMTP_long(action.options.effectId),\n\t\t\t\t\tMTP_long(starsPaid),\n\t\t\t\t\tApi::SuggestToMTP(action.options.suggest)\n\t\t\t\t), done, fail);\n\t\t} else {\n\t\t\thistories.sendPreparedMessage(\n\t\t\t\thistory,\n\t\t\t\taction.replyTo,\n\t\t\t\trandomId,\n\t\t\t\tData::Histories::PrepareMessage<MTPmessages_SendMessage>(\n\t\t\t\t\tMTP_flags(sendFlags),\n\t\t\t\t\tpeer->input(),\n\t\t\t\t\tData::Histories::ReplyToPlaceholder(),\n\t\t\t\t\tmsgText,\n\t\t\t\t\tMTP_long(randomId),\n\t\t\t\t\tMTPReplyMarkup(),\n\t\t\t\t\tsentEntities,\n\t\t\t\t\tMTP_int(action.options.scheduled),\n\t\t\t\t\tMTP_int(action.options.scheduleRepeatPeriod),\n\t\t\t\t\t(sendAs ? sendAs->input() : MTP_inputPeerEmpty()),\n\t\t\t\t\tmtpShortcut,\n\t\t\t\t\tMTP_long(action.options.effectId),\n\t\t\t\t\tMTP_long(starsPaid),\n\t\t\t\t\tApi::SuggestToMTP(action.options.suggest)\n\t\t\t\t), done, fail);\n\t\t}\n\t\tisFirst = false;\n\t}\n\n\tfinishForwarding(action);\n}\n\nvoid ApiWrap::sendBotStart(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<UserData*> bot,\n\t\tPeerData *chat,\n\t\tconst QString &startTokenForChat) {\n\tExpects(bot->isBot());\n\n\tif (chat && chat->isChannel() && !chat->isMegagroup()) {\n\t\tShowAddParticipantsError(show, \"USER_BOT\", chat, bot);\n\t\treturn;\n\t}\n\n\tauto &info = bot->botInfo;\n\tconst auto token = chat ? startTokenForChat : info->startToken;\n\tif (token.isEmpty()) {\n\t\tauto message = MessageToSend(\n\t\t\tApi::SendAction(_session->data().history(chat\n\t\t\t\t? chat\n\t\t\t\t: bot.get())));\n\t\tmessage.textWithTags = { u\"/start\"_q, TextWithTags::Tags() };\n\t\tif (chat) {\n\t\t\tmessage.textWithTags.text += '@' + bot->username();\n\t\t}\n\t\tsendMessage(std::move(message));\n\t\treturn;\n\t}\n\tconst auto randomId = base::RandomValue<uint64>();\n\tif (!chat) {\n\t\tinfo->startToken = QString();\n\t}\n\trequest(MTPmessages_StartBot(\n\t\tbot->inputUser(),\n\t\tchat ? chat->input() : MTP_inputPeerEmpty(),\n\t\tMTP_long(randomId),\n\t\tMTP_string(token)\n\t)).done([=](const MTPUpdates &result) {\n\t\tapplyUpdates(result);\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (chat) {\n\t\t\tconst auto type = error.type();\n\t\t\tShowAddParticipantsError(show, type, chat, bot);\n\t\t}\n\t}).send();\n}\n\nvoid ApiWrap::sendInlineResult(\n\t\tnot_null<UserData*> bot,\n\t\tnot_null<InlineBots::Result*> data,\n\t\tSendAction action,\n\t\tstd::optional<MsgId> localMessageId,\n\t\tFn<void(bool)> done) {\n\tsendAction(action);\n\n\tconst auto history = action.history;\n\tconst auto peer = history->peer;\n\tconst auto newId = FullMsgId(\n\t\tpeer->id,\n\t\tlocalMessageId\n\t\t\t? (*localMessageId)\n\t\t\t: _session->data().nextLocalMessageId());\n\tconst auto randomId = base::RandomValue<uint64>();\n\tconst auto topicRootId = action.replyTo.messageId\n\t\t? action.replyTo.topicRootId\n\t\t: 0;\n\tconst auto monoforumPeerId = action.replyTo.monoforumPeerId;\n\n\tusing SendFlag = MTPmessages_SendInlineBotResult::Flag;\n\tauto flags = NewMessageFlags(peer);\n\tauto sendFlags = SendFlag::f_clear_draft | SendFlag();\n\tif (action.replyTo) {\n\t\tflags |= MessageFlag::HasReplyInfo;\n\t\tsendFlags |= SendFlag::f_reply_to;\n\t}\n\tconst auto silentPost = ShouldSendSilent(peer, action.options);\n\tFillMessagePostFlags(action, peer, flags);\n\tif (silentPost) {\n\t\tsendFlags |= SendFlag::f_silent;\n\t}\n\tif (action.options.scheduled) {\n\t\tflags |= MessageFlag::IsOrWasScheduled;\n\t\tsendFlags |= SendFlag::f_schedule_date;\n\t}\n\tif (action.options.shortcutId) {\n\t\tflags |= MessageFlag::ShortcutMessage;\n\t\tsendFlags |= SendFlag::f_quick_reply_shortcut;\n\t}\n\tif (action.options.hideViaBot) {\n\t\tsendFlags |= SendFlag::f_hide_via;\n\t}\n\tconst auto starsPaid = std::min(\n\t\tpeer->starsPerMessageChecked(),\n\t\taction.options.starsApproved);\n\tif (starsPaid) {\n\t\taction.options.starsApproved -= starsPaid;\n\t\tsendFlags |= SendFlag::f_allow_paid_stars;\n\t}\n\n\tconst auto sendAs = action.options.sendAs;\n\tif (sendAs) {\n\t\tsendFlags |= MTPmessages_SendInlineBotResult::Flag::f_send_as;\n\t}\n\t_session->data().registerMessageRandomId(randomId, newId);\n\n\tdata->addToHistory(history, {\n\t\t.id = newId.msg,\n\t\t.flags = flags,\n\t\t.from = NewMessageFromId(action),\n\t\t.replyTo = action.replyTo,\n\t\t.date = NewMessageDate(action.options),\n\t\t.shortcutId = action.options.shortcutId,\n\t\t.starsPaid = starsPaid,\n\t\t.viaBotId = ((bot && !action.options.hideViaBot)\n\t\t\t? peerToUser(bot->id)\n\t\t\t: UserId()),\n\t\t.postAuthor = NewMessagePostAuthor(action),\n\t});\n\n\thistory->clearCloudDraft(topicRootId, monoforumPeerId);\n\thistory->startSavingCloudDraft(topicRootId, monoforumPeerId);\n\n\tauto &histories = history->owner().histories();\n\thistories.sendPreparedMessage(\n\t\thistory,\n\t\taction.replyTo,\n\t\trandomId,\n\t\tData::Histories::PrepareMessage<MTPmessages_SendInlineBotResult>(\n\t\t\tMTP_flags(sendFlags),\n\t\t\tpeer->input(),\n\t\t\tData::Histories::ReplyToPlaceholder(),\n\t\t\tMTP_long(randomId),\n\t\t\tMTP_long(data->getQueryId()),\n\t\t\tMTP_string(data->getId()),\n\t\t\tMTP_int(action.options.scheduled),\n\t\t\t(sendAs ? sendAs->input() : MTP_inputPeerEmpty()),\n\t\t\tData::ShortcutIdToMTP(_session, action.options.shortcutId),\n\t\t\tMTP_long(starsPaid)\n\t\t), [=](const MTPUpdates &result, const MTP::Response &response) {\n\t\thistory->finishSavingCloudDraft(\n\t\t\ttopicRootId,\n\t\t\tmonoforumPeerId,\n\t\t\tApi::UnixtimeFromMsgId(response.outerMsgId));\n\t\tif (done) {\n\t\t\tdone(true);\n\t\t}\n\t}, [=](const MTP::Error &error, const MTP::Response &response) {\n\t\tsendMessageFail(error, peer, randomId, newId);\n\t\thistory->finishSavingCloudDraft(\n\t\t\ttopicRootId,\n\t\t\tmonoforumPeerId,\n\t\t\tApi::UnixtimeFromMsgId(response.outerMsgId));\n\t\tif (done) {\n\t\t\tdone(false);\n\t\t}\n\t});\n\tfinishForwarding(action);\n}\n\nvoid ApiWrap::uploadAlbumMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MessageGroupId &groupId,\n\t\tconst MTPInputMedia &media) {\n\tconst auto localId = item->fullId();\n\tconst auto failed = [=] {\n\n\t};\n\trequest(MTPmessages_UploadMedia(\n\t\tMTP_flags(0),\n\t\tMTPstring(), // business_connection_id\n\t\titem->history()->peer->input(),\n\t\tmedia\n\t)).done([=](const MTPMessageMedia &result) {\n\t\tconst auto item = _session->data().message(localId);\n\t\tif (!item) {\n\t\t\tfailed();\n\t\t\treturn;\n\t\t}\n\t\tauto spoiler = false;\n\t\tif (const auto media = item->media()) {\n\t\t\tspoiler = media->hasSpoiler();\n\t\t\tif (const auto photo = media->photo()) {\n\t\t\t\tphoto->setWaitingForAlbum();\n\t\t\t} else if (const auto document = media->document()) {\n\t\t\t\tdocument->setWaitingForAlbum();\n\t\t\t}\n\t\t}\n\n\t\tswitch (result.type()) {\n\t\tcase mtpc_messageMediaPhoto: {\n\t\t\tconst auto &data = result.c_messageMediaPhoto();\n\t\t\tconst auto photo = data.vphoto();\n\t\t\tif (!photo || photo->type() != mtpc_photo) {\n\t\t\t\tfailed();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto &fields = photo->c_photo();\n\t\t\tusing Flag = MTPDinputMediaPhoto::Flag;\n\t\t\tconst auto flags = Flag()\n\t\t\t\t| (data.vttl_seconds() ? Flag::f_ttl_seconds : Flag())\n\t\t\t\t| (spoiler ? Flag::f_spoiler : Flag());\n\t\t\tconst auto media = MTP_inputMediaPhoto(\n\t\t\t\tMTP_flags(flags),\n\t\t\t\tMTP_inputPhoto(\n\t\t\t\t\tfields.vid(),\n\t\t\t\t\tfields.vaccess_hash(),\n\t\t\t\t\tfields.vfile_reference()),\n\t\t\t\tMTP_int(data.vttl_seconds().value_or_empty()),\n\t\t\t\tMTPInputDocument()); // video\n\t\t\tsendAlbumWithUploaded(item, groupId, media);\n\t\t} break;\n\n\t\tcase mtpc_messageMediaDocument: {\n\t\t\tconst auto &data = result.c_messageMediaDocument();\n\t\t\tconst auto document = data.vdocument();\n\t\t\tif (!document || document->type() != mtpc_document) {\n\t\t\t\tfailed();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto &fields = document->c_document();\n\t\t\tconst auto mtpCover = data.vvideo_cover();\n\t\t\tconst auto cover = (mtpCover && mtpCover->type() == mtpc_photo)\n\t\t\t\t? &(mtpCover->c_photo())\n\t\t\t\t: (const MTPDphoto*)nullptr;\n\t\t\tusing Flag = MTPDinputMediaDocument::Flag;\n\t\t\tconst auto flags = Flag()\n\t\t\t\t| (data.vttl_seconds() ? Flag::f_ttl_seconds : Flag())\n\t\t\t\t| (spoiler ? Flag::f_spoiler : Flag())\n\t\t\t\t| (data.vvideo_timestamp() ? Flag::f_video_timestamp : Flag())\n\t\t\t\t| (cover ? Flag::f_video_cover : Flag());\n\t\t\tconst auto media = MTP_inputMediaDocument(\n\t\t\t\tMTP_flags(flags),\n\t\t\t\tMTP_inputDocument(\n\t\t\t\t\tfields.vid(),\n\t\t\t\t\tfields.vaccess_hash(),\n\t\t\t\t\tfields.vfile_reference()),\n\t\t\t\t(cover\n\t\t\t\t\t? MTP_inputPhoto(\n\t\t\t\t\t\tcover->vid(),\n\t\t\t\t\t\tcover->vaccess_hash(),\n\t\t\t\t\t\tcover->vfile_reference())\n\t\t\t\t\t: MTPInputPhoto()),\n\t\t\t\tMTP_int(data.vvideo_timestamp().value_or_empty()),\n\t\t\t\tMTP_int(data.vttl_seconds().value_or_empty()),\n\t\t\t\tMTPstring()); // query\n\t\t\tsendAlbumWithUploaded(item, groupId, media);\n\t\t} break;\n\t\t}\n\t}).fail([=] {\n\t\tfailed();\n\t}).send();\n}\n\nvoid ApiWrap::sendMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPInputMedia &media,\n\t\tApi::SendOptions options,\n\t\tFn<void(bool)> done) {\n\tconst auto randomId = base::RandomValue<uint64>();\n\t_session->data().registerMessageRandomId(randomId, item->fullId());\n\n\tsendMediaWithRandomId(item, media, options, randomId, std::move(done));\n}\n\nvoid ApiWrap::sendMediaWithRandomId(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPInputMedia &media,\n\t\tApi::SendOptions options,\n\t\tuint64 randomId,\n\t\tFn<void(bool)> done) {\n\tconst auto history = item->history();\n\tconst auto replyTo = item->replyTo();\n\tconst auto peer = history->peer;\n\n\tauto caption = item->originalText();\n\tTextUtilities::Trim(caption);\n\tauto sentEntities = Api::EntitiesToMTP(\n\t\t_session,\n\t\tcaption.entities,\n\t\tApi::ConvertOption::SkipLocal);\n\n\tconst auto updateRecentStickers = Api::HasAttachedStickers(media);\n\tconst auto starsPaid = std::min(\n\t\tpeer->starsPerMessageChecked(),\n\t\toptions.starsApproved);\n\tif (starsPaid) {\n\t\toptions.starsApproved -= starsPaid;\n\t}\n\n\tusing Flag = MTPmessages_SendMedia::Flag;\n\tconst auto flags = Flag(0)\n\t\t| (replyTo ? Flag::f_reply_to : Flag(0))\n\t\t| (ShouldSendSilent(history->peer, options)\n\t\t\t? Flag::f_silent\n\t\t\t: Flag(0))\n\t\t| (!sentEntities.v.isEmpty() ? Flag::f_entities : Flag(0))\n\t\t| (options.scheduled ? Flag::f_schedule_date : Flag(0))\n\t\t| ((options.scheduled && options.scheduleRepeatPeriod)\n\t\t\t? Flag::f_schedule_repeat_period\n\t\t\t: Flag(0))\n\t\t| (options.sendAs ? Flag::f_send_as : Flag(0))\n\t\t| (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0))\n\t\t| (options.effectId ? Flag::f_effect : Flag(0))\n\t\t| (options.suggest ? Flag::f_suggested_post : Flag(0))\n\t\t| (options.invertCaption ? Flag::f_invert_media : Flag(0))\n\t\t| (starsPaid ? Flag::f_allow_paid_stars : Flag(0));\n\n\tauto &histories = history->owner().histories();\n\tconst auto itemId = item->fullId();\n\thistories.sendPreparedMessage(\n\t\thistory,\n\t\treplyTo,\n\t\trandomId,\n\t\tData::Histories::PrepareMessage<MTPmessages_SendMedia>(\n\t\t\tMTP_flags(flags),\n\t\t\tpeer->input(),\n\t\t\tData::Histories::ReplyToPlaceholder(),\n\t\t\t(options.price\n\t\t\t\t? MTPInputMedia(MTP_inputMediaPaidMedia(\n\t\t\t\t\tMTP_flags(0),\n\t\t\t\t\tMTP_long(options.price),\n\t\t\t\t\tMTP_vector<MTPInputMedia>(1, media),\n\t\t\t\t\tMTPstring()))\n\t\t\t\t: media),\n\t\t\tMTP_string(caption.text),\n\t\t\tMTP_long(randomId),\n\t\t\tMTPReplyMarkup(),\n\t\t\tsentEntities,\n\t\t\tMTP_int(options.scheduled),\n\t\t\tMTP_int(options.scheduleRepeatPeriod),\n\t\t\t(options.sendAs ? options.sendAs->input() : MTP_inputPeerEmpty()),\n\t\t\tData::ShortcutIdToMTP(_session, options.shortcutId),\n\t\t\tMTP_long(options.effectId),\n\t\t\tMTP_long(starsPaid),\n\t\t\tApi::SuggestToMTP(options.suggest)\n\t\t), [=](const MTPUpdates &result, const MTP::Response &response) {\n\t\tif (done) done(true);\n\t\tif (updateRecentStickers) {\n\t\t\trequestRecentStickers(std::nullopt, true);\n\t\t}\n\t}, [=](const MTP::Error &error, const MTP::Response &response) {\n\t\tif (done) done(false);\n\t\tsendMessageFail(error, peer, randomId, itemId);\n\t});\n}\n\nvoid ApiWrap::sendMultiPaidMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<SendingAlbum*> album,\n\t\tFn<void(bool)> done) {\n\tExpects(album->options.price > 0);\n\n\tconst auto groupId = album->groupId;\n\tauto &options = album->options;\n\tconst auto randomId = album->items.front().randomId;\n\tauto medias = album->items | ranges::view::transform([](\n\t\t\tconst SendingAlbum::Item &part) {\n\t\tAssert(part.media.has_value());\n\t\treturn MTPInputMedia(part.media->data().vmedia());\n\t}) | ranges::to<QVector<MTPInputMedia>>();\n\n\tconst auto history = item->history();\n\tconst auto replyTo = item->replyTo();\n\tconst auto peer = history->peer;\n\n\tauto caption = item->originalText();\n\tTextUtilities::Trim(caption);\n\tauto sentEntities = Api::EntitiesToMTP(\n\t\t_session,\n\t\tcaption.entities,\n\t\tApi::ConvertOption::SkipLocal);\n\tconst auto starsPaid = std::min(\n\t\tpeer->starsPerMessageChecked(),\n\t\toptions.starsApproved);\n\tif (starsPaid) {\n\t\toptions.starsApproved -= starsPaid;\n\t}\n\n\tusing Flag = MTPmessages_SendMedia::Flag;\n\tconst auto flags = Flag(0)\n\t\t| (replyTo ? Flag::f_reply_to : Flag(0))\n\t\t| (ShouldSendSilent(history->peer, options)\n\t\t\t? Flag::f_silent\n\t\t\t: Flag(0))\n\t\t| (!sentEntities.v.isEmpty() ? Flag::f_entities : Flag(0))\n\t\t| (options.scheduled ? Flag::f_schedule_date : Flag(0))\n\t\t| (options.scheduleRepeatPeriod\n\t\t\t? Flag::f_schedule_repeat_period\n\t\t\t: Flag(0))\n\t\t| (options.sendAs ? Flag::f_send_as : Flag(0))\n\t\t| (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0))\n\t\t| (options.effectId ? Flag::f_effect : Flag(0))\n\t\t| (options.suggest ? Flag::f_suggested_post : Flag(0))\n\t\t| (options.invertCaption ? Flag::f_invert_media : Flag(0))\n\t\t| (starsPaid ? Flag::f_allow_paid_stars : Flag(0));\n\n\tauto &histories = history->owner().histories();\n\tconst auto itemId = item->fullId();\n\talbum->sent = true;\n\thistories.sendPreparedMessage(\n\t\thistory,\n\t\treplyTo,\n\t\trandomId,\n\t\tData::Histories::PrepareMessage<MTPmessages_SendMedia>(\n\t\t\tMTP_flags(flags),\n\t\t\tpeer->input(),\n\t\t\tData::Histories::ReplyToPlaceholder(),\n\t\t\tMTP_inputMediaPaidMedia(\n\t\t\t\tMTP_flags(0),\n\t\t\t\tMTP_long(options.price),\n\t\t\t\tMTP_vector<MTPInputMedia>(std::move(medias)),\n\t\t\t\tMTPstring()),\n\t\t\tMTP_string(caption.text),\n\t\t\tMTP_long(randomId),\n\t\t\tMTPReplyMarkup(),\n\t\t\tsentEntities,\n\t\t\tMTP_int(options.scheduled),\n\t\t\tMTP_int(options.scheduleRepeatPeriod),\n\t\t\t(options.sendAs ? options.sendAs->input() : MTP_inputPeerEmpty()),\n\t\t\tData::ShortcutIdToMTP(_session, options.shortcutId),\n\t\t\tMTP_long(options.effectId),\n\t\t\tMTP_long(starsPaid),\n\t\t\tApi::SuggestToMTP(options.suggest)\n\t\t), [=](const MTPUpdates &result, const MTP::Response &response) {\n\t\tif (const auto album = _sendingAlbums.take(groupId)) {\n\t\t\tconst auto copy = (*album)->items;\n\t\t\tfor (const auto &part : copy) {\n\t\t\t\tif (const auto item = history->owner().message(part.msgId)) {\n\t\t\t\t\titem->destroy();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (done) done(true);\n\t}, [=](const MTP::Error &error, const MTP::Response &response) {\n\t\tif (done) done(false);\n\t\tsendMessageFail(error, peer, randomId, itemId);\n\t});\n}\n\nvoid ApiWrap::sendAlbumWithUploaded(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MessageGroupId &groupId,\n\t\tconst MTPInputMedia &media) {\n\tconst auto localId = item->fullId();\n\tconst auto randomId = base::RandomValue<uint64>();\n\t_session->data().registerMessageRandomId(randomId, localId);\n\n\tconst auto albumIt = _sendingAlbums.find(groupId.raw());\n\tAssert(albumIt != _sendingAlbums.end());\n\tconst auto &album = albumIt->second;\n\talbum->fillMedia(item, media, randomId);\n\tsendAlbumIfReady(album.get());\n}\n\nvoid ApiWrap::sendAlbumWithCancelled(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MessageGroupId &groupId) {\n\tconst auto albumIt = _sendingAlbums.find(groupId.raw());\n\tif (albumIt == _sendingAlbums.end()) {\n\t\t// Sometimes we destroy item being sent already after the album\n\t\t// was sent successfully. For example the message could be loaded\n\t\t// from server (by messages.getHistory or updateNewMessage) and\n\t\t// added to history and after that updateMessageID was received with\n\t\t// the same message id, in this case we destroy a detached local\n\t\t// item and sendAlbumWithCancelled is called for already sent album.\n\t\treturn;\n\t}\n\tconst auto &album = albumIt->second;\n\talbum->removeItem(item);\n\tsendAlbumIfReady(album.get());\n}\n\nvoid ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {\n\tif (album->sent) {\n\t\treturn;\n\t}\n\tconst auto groupId = album->groupId;\n\tif (album->items.empty()) {\n\t\t_sendingAlbums.remove(groupId);\n\t\treturn;\n\t}\n\tauto sample = (HistoryItem*)nullptr;\n\tauto medias = QVector<MTPInputSingleMedia>();\n\tmedias.reserve(album->items.size());\n\tfor (const auto &item : album->items) {\n\t\tif (!item.media) {\n\t\t\treturn;\n\t\t} else if (!sample) {\n\t\t\tsample = _session->data().message(item.msgId);\n\t\t}\n\t\tmedias.push_back(*item.media);\n\t}\n\tif (!sample) {\n\t\t_sendingAlbums.remove(groupId);\n\t\treturn;\n\t} else if (album->options.price > 0) {\n\t\tsendMultiPaidMedia(sample, album);\n\t\treturn;\n\t} else if (medias.size() < 2) {\n\t\tconst auto &single = medias.front().data();\n\t\talbum->sent = true;\n\t\tsendMediaWithRandomId(\n\t\t\tsample,\n\t\t\tsingle.vmedia(),\n\t\t\talbum->options,\n\t\t\tsingle.vrandom_id().v);\n\t\t_sendingAlbums.remove(groupId);\n\t\treturn;\n\t}\n\tconst auto history = sample->history();\n\tconst auto replyTo = sample->replyTo();\n\tconst auto sendAs = album->options.sendAs;\n\tconst auto starsPaid = std::min(\n\t\thistory->peer->starsPerMessageChecked() * int(medias.size()),\n\t\talbum->options.starsApproved);\n\tif (starsPaid) {\n\t\talbum->options.starsApproved -= starsPaid;\n\t}\n\tusing Flag = MTPmessages_SendMultiMedia::Flag;\n\tconst auto flags = Flag(0)\n\t\t| (replyTo ? Flag::f_reply_to : Flag(0))\n\t\t| (ShouldSendSilent(history->peer, album->options)\n\t\t\t? Flag::f_silent\n\t\t\t: Flag(0))\n\t\t| (album->options.scheduled ? Flag::f_schedule_date : Flag(0))\n\t\t//| (album->options.scheduleRepeatPeriod\n\t\t//\t? Flag::f_schedule_repeat_period\n\t\t//\t: Flag(0))\n\t\t| (sendAs ? Flag::f_send_as : Flag(0))\n\t\t| (album->options.shortcutId\n\t\t\t? Flag::f_quick_reply_shortcut\n\t\t\t: Flag(0))\n\t\t| (album->options.effectId ? Flag::f_effect : Flag(0))\n\t\t| (album->options.invertCaption ? Flag::f_invert_media : Flag(0))\n\t\t| (starsPaid ? Flag::f_allow_paid_stars : Flag(0));\n\tauto &histories = history->owner().histories();\n\tconst auto peer = history->peer;\n\talbum->sent = true;\n\thistories.sendPreparedMessage(\n\t\thistory,\n\t\treplyTo,\n\t\tuint64(0), // randomId\n\t\tData::Histories::PrepareMessage<MTPmessages_SendMultiMedia>(\n\t\t\tMTP_flags(flags),\n\t\t\tpeer->input(),\n\t\t\tData::Histories::ReplyToPlaceholder(),\n\t\t\tMTP_vector<MTPInputSingleMedia>(medias),\n\t\t\tMTP_int(album->options.scheduled),\n\t\t\t//MTP_int(album->options.scheduleRepeatPeriod),\n\t\t\t(sendAs ? sendAs->input() : MTP_inputPeerEmpty()),\n\t\t\tData::ShortcutIdToMTP(_session, album->options.shortcutId),\n\t\t\tMTP_long(album->options.effectId),\n\t\t\tMTP_long(starsPaid)\n\t\t), [=](const MTPUpdates &result, const MTP::Response &response) {\n\t\t_sendingAlbums.remove(groupId);\n\t}, [=](const MTP::Error &error, const MTP::Response &response) {\n\t\tif (const auto album = _sendingAlbums.take(groupId)) {\n\t\t\tfor (const auto &item : (*album)->items) {\n\t\t\t\tsendMessageFail(error, peer, item.randomId, item.msgId);\n\t\t\t}\n\t\t} else {\n\t\t\tsendMessageFail(error, peer);\n\t\t}\n\t});\n}\n\nvoid ApiWrap::reloadContactSignupSilent() {\n\tif (_contactSignupSilentRequestId) {\n\t\treturn;\n\t}\n\tconst auto requestId = request(MTPaccount_GetContactSignUpNotification(\n\t)).done([=](const MTPBool &result) {\n\t\t_contactSignupSilentRequestId = 0;\n\t\tconst auto silent = mtpIsTrue(result);\n\t\t_contactSignupSilent = silent;\n\t\t_contactSignupSilentChanges.fire_copy(silent);\n\t}).fail([=] {\n\t\t_contactSignupSilentRequestId = 0;\n\t}).send();\n\t_contactSignupSilentRequestId = requestId;\n}\n\nrpl::producer<bool> ApiWrap::contactSignupSilent() const {\n\treturn _contactSignupSilent\n\t\t? _contactSignupSilentChanges.events_starting_with_copy(\n\t\t\t*_contactSignupSilent)\n\t\t: (_contactSignupSilentChanges.events() | rpl::type_erased);\n}\n\nstd::optional<bool> ApiWrap::contactSignupSilentCurrent() const {\n\treturn _contactSignupSilent;\n}\n\nvoid ApiWrap::saveContactSignupSilent(bool silent) {\n\trequest(base::take(_contactSignupSilentRequestId)).cancel();\n\n\tconst auto requestId = request(MTPaccount_SetContactSignUpNotification(\n\t\tMTP_bool(silent)\n\t)).done([=] {\n\t\t_contactSignupSilentRequestId = 0;\n\t\t_contactSignupSilent = silent;\n\t\t_contactSignupSilentChanges.fire_copy(silent);\n\t}).fail([=] {\n\t\t_contactSignupSilentRequestId = 0;\n\t}).send();\n\t_contactSignupSilentRequestId = requestId;\n}\n\nauto ApiWrap::botCommonGroups(not_null<UserData*> bot) const\n-> std::optional<std::vector<not_null<PeerData*>>> {\n\tconst auto i = _botCommonGroups.find(bot);\n\treturn (i != end(_botCommonGroups))\n\t\t? i->second\n\t\t: std::optional<std::vector<not_null<PeerData*>>>();\n}\n\nvoid ApiWrap::requestBotCommonGroups(\n\t\tnot_null<UserData*> bot,\n\t\tFn<void()> done) {\n\tif (_botCommonGroupsRequests.contains(bot)) {\n\t\treturn;\n\t}\n\t_botCommonGroupsRequests.emplace(bot, done);\n\tconst auto finish = [=](std::vector<not_null<PeerData*>> list) {\n\t\t_botCommonGroups.emplace(bot, std::move(list));\n\t\tif (const auto callback = _botCommonGroupsRequests.take(bot)) {\n\t\t\t(*callback)();\n\t\t}\n\t};\n\tconst auto limit = 100;\n\trequest(MTPmessages_GetCommonChats(\n\t\tbot->inputUser(),\n\t\tMTP_long(0), // max_id\n\t\tMTP_int(limit)\n\t)).done([=](const MTPmessages_Chats &result) {\n\t\tconst auto chats = result.match([](const auto &data) {\n\t\t\treturn &data.vchats().v;\n\t\t});\n\t\tauto &owner = session().data();\n\t\tauto list = std::vector<not_null<PeerData*>>();\n\t\tlist.reserve(chats->size());\n\t\tfor (const auto &chat : *chats) {\n\t\t\tif (const auto peer = owner.processChat(chat)) {\n\t\t\t\tlist.push_back(peer);\n\t\t\t}\n\t\t}\n\t\tfinish(std::move(list));\n\t}).fail([=] {\n\t\tfinish({});\n\t}).send();\n}\n\nvoid ApiWrap::saveSelfBio(const QString &text) {\n\tif (_bio.requestId) {\n\t\tif (text != _bio.requestedText) {\n\t\t\trequest(_bio.requestId).cancel();\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t}\n\t_bio.requestedText = text;\n\t_bio.requestId = request(MTPaccount_UpdateProfile(\n\t\tMTP_flags(MTPaccount_UpdateProfile::Flag::f_about),\n\t\tMTPstring(),\n\t\tMTPstring(),\n\t\tMTP_string(text)\n\t)).done([=](const MTPUser &result) {\n\t\t_bio.requestId = 0;\n\n\t\t_session->data().processUser(result);\n\t\t_session->user()->setAbout(_bio.requestedText);\n\t}).fail([=] {\n\t\t_bio.requestId = 0;\n\t}).send();\n}\n\nvoid ApiWrap::registerStatsRequest(MTP::DcId dcId, mtpRequestId id) {\n\t_statsRequests[dcId].emplace(id);\n}\n\nvoid ApiWrap::unregisterStatsRequest(MTP::DcId dcId, mtpRequestId id) {\n\tconst auto i = _statsRequests.find(dcId);\n\tAssert(i != end(_statsRequests));\n\tconst auto removed = i->second.remove(id);\n\tAssert(removed);\n\tif (i->second.empty()) {\n\t\t_statsSessionKillTimer.callOnce(kStatsSessionKillTimeout);\n\t}\n}\n\nvoid ApiWrap::checkStatsSessions() {\n\tfor (auto i = begin(_statsRequests); i != end(_statsRequests);) {\n\t\tif (i->second.empty()) {\n\t\t\tinstance().killSession(\n\t\t\t\tMTP::ShiftDcId(i->first, MTP::kStatsDcShift));\n\t\t\ti = _statsRequests.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n}\n\nApi::Authorizations &ApiWrap::authorizations() {\n\treturn *_authorizations;\n}\n\nApi::AttachedStickers &ApiWrap::attachedStickers() {\n\treturn *_attachedStickers;\n}\n\nApi::BlockedPeers &ApiWrap::blockedPeers() {\n\treturn *_blockedPeers;\n}\n\nApi::CloudPassword &ApiWrap::cloudPassword() {\n\treturn *_cloudPassword;\n}\n\nApi::SelfDestruct &ApiWrap::selfDestruct() {\n\treturn *_selfDestruct;\n}\n\nApi::SensitiveContent &ApiWrap::sensitiveContent() {\n\treturn *_sensitiveContent;\n}\n\nApi::GlobalPrivacy &ApiWrap::globalPrivacy() {\n\treturn *_globalPrivacy;\n}\n\nApi::ReactionsNotifySettings &ApiWrap::reactionsNotifySettings() {\n\treturn *_reactionsNotifySettings;\n}\n\nApi::UserPrivacy &ApiWrap::userPrivacy() {\n\treturn *_userPrivacy;\n}\n\nApi::InviteLinks &ApiWrap::inviteLinks() {\n\treturn *_inviteLinks;\n}\n\nApi::ChatLinks &ApiWrap::chatLinks() {\n\treturn *_chatLinks;\n}\n\nApi::ViewsManager &ApiWrap::views() {\n\treturn *_views;\n}\n\nApi::ReadMetrics &ApiWrap::readMetrics() {\n\treturn *_readMetrics;\n}\n\nApi::ConfirmPhone &ApiWrap::confirmPhone() {\n\treturn *_confirmPhone;\n}\n\nApi::PeerPhoto &ApiWrap::peerPhoto() {\n\treturn *_peerPhoto;\n}\n\nApi::Polls &ApiWrap::polls() {\n\treturn *_polls;\n}\n\nApi::TodoLists &ApiWrap::todoLists() {\n\treturn *_todoLists;\n}\n\nApi::ChatParticipants &ApiWrap::chatParticipants() {\n\treturn *_chatParticipants;\n}\n\nApi::UnreadThings &ApiWrap::unreadThings() {\n\treturn *_unreadThings;\n}\n\nApi::Ringtones &ApiWrap::ringtones() {\n\treturn *_ringtones;\n}\n\nApi::ComposeWithAi &ApiWrap::composeWithAi() {\n\treturn *_composeWithAi;\n}\n\nApi::Transcribes &ApiWrap::transcribes() {\n\treturn *_transcribes;\n}\n\nApi::Premium &ApiWrap::premium() {\n\treturn *_premium;\n}\n\nApi::Usernames &ApiWrap::usernames() {\n\treturn *_usernames;\n}\n\nApi::Websites &ApiWrap::websites() {\n\treturn *_websites;\n}\n\nApi::PeerColors &ApiWrap::peerColors() {\n\treturn *_peerColors;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/apiwrap.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_common.h\"\n#include \"base/timer.h\"\n#include \"mtproto/sender.h\"\n#include \"data/stickers/data_stickers_set.h\"\n#include \"data/data_messages.h\"\n\nclass TaskQueue;\nstruct MessageGroupId;\nstruct SendingAlbum;\nenum class SendMediaType;\nstruct FileLoadTo;\nstruct ChatRestrictionsInfo;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\nstruct UpdatedFileReferences;\nclass WallPaper;\nstruct ResolvedForwardDraft;\nenum class DefaultNotify : uint8_t;\nenum class StickersType : uchar;\nclass Forum;\nclass ForumTopic;\nclass Thread;\nclass Story;\nclass SavedMessages;\n} // namespace Data\n\nnamespace InlineBots {\nclass Result;\n} // namespace InlineBots\n\nnamespace Storage {\nenum class SharedMediaType : signed char;\nclass DownloadMtprotoTask;\nclass Account;\n} // namespace Storage\n\nnamespace Dialogs {\nclass Key;\n} // namespace Dialogs\n\nnamespace Ui {\nstruct PreparedList;\nclass Show;\n} // namespace Ui\n\nnamespace Api {\n\nstruct SearchResult;\nstruct GlobalMediaResult;\n\nclass Updates;\nclass Authorizations;\nclass AttachedStickers;\nclass BlockedPeers;\nclass CloudPassword;\nclass SelfDestruct;\nclass SensitiveContent;\nclass GlobalPrivacy;\nclass ReactionsNotifySettings;\nclass UserPrivacy;\nclass InviteLinks;\nclass ChatLinks;\nclass ViewsManager;\nclass ConfirmPhone;\nclass PeerPhoto;\nclass PeerColors;\nclass Polls;\nclass TodoLists;\nclass ChatParticipants;\nclass UnreadThings;\nclass Ringtones;\nclass ComposeWithAi;\nclass Transcribes;\nclass Premium;\nclass ReadMetrics;\nclass Usernames;\nclass Websites;\n\nnamespace details {\n\ninline QString ToString(const QString &value) {\n\treturn value;\n}\n\ninline QString ToString(int32 value) {\n\treturn QString::number(value);\n}\n\ninline QString ToString(uint64 value) {\n\treturn QString::number(value);\n}\n\ntemplate <uchar Shift>\ninline QString ToString(ChatIdType<Shift> value) {\n\treturn QString::number(value.bare);\n}\n\ninline QString ToString(PeerId value) {\n\treturn QString::number(value.value);\n}\n\n} // namespace details\n\ntemplate <\n\ttypename ...Types,\n\ttypename = std::enable_if_t<(sizeof...(Types) > 0)>>\nQString RequestKey(Types &&...values) {\n\tconst auto strings = { details::ToString(values)... };\n\tif (strings.size() == 1) {\n\t\treturn *strings.begin();\n\t}\n\n\tauto result = QString();\n\tresult.reserve(\n\t\tranges::accumulate(strings, 0, ranges::plus(), &QString::size));\n\tfor (const auto &string : strings) {\n\t\tresult.append(string);\n\t}\n\treturn result;\n}\n\n[[nodiscard]] TimeId UnixtimeFromMsgId(mtpMsgId msgId);\n\n} // namespace Api\n\nclass ApiWrap final : public MTP::Sender {\npublic:\n\tusing SendAction = Api::SendAction;\n\tusing MessageToSend = Api::MessageToSend;\n\n\texplicit ApiWrap(not_null<Main::Session*> session);\n\t~ApiWrap();\n\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] Storage::Account &local() const;\n\t[[nodiscard]] Api::Updates &updates() const;\n\n\tvoid applyUpdates(\n\t\tconst MTPUpdates &updates,\n\t\tuint64 sentMessageRandomId = 0) const;\n\tint applyAffectedHistory(\n\t\tPeerData *peer, // May be nullptr, like for deletePhoneCallHistory.\n\t\tconst MTPmessages_AffectedHistory &result) const;\n\n\tvoid registerModifyRequest(const QString &key, mtpRequestId requestId);\n\tvoid clearModifyRequest(const QString &key);\n\n\tvoid saveCurrentDraftToCloud();\n\n\tvoid savePinnedOrder(Data::Folder *folder);\n\tvoid savePinnedOrder(not_null<Data::Forum*> forum);\n\tvoid savePinnedOrder(not_null<Data::SavedMessages*> saved);\n\tvoid toggleHistoryArchived(\n\t\tnot_null<History*> history,\n\t\tbool archived,\n\t\tFn<void()> callback);\n\n\tvoid requestMessageData(PeerData *peer, MsgId msgId, Fn<void()> done);\n\tQString exportDirectMessageLink(\n\t\tnot_null<HistoryItem*> item,\n\t\tbool inRepliesContext,\n\t\tbool forceNonPublicLink = false,\n\t\tstd::optional<TimeId> videoTimestamp = {});\n\tQString exportDirectStoryLink(not_null<Data::Story*> item);\n\n\tvoid requestContacts();\n\tvoid requestDialogs(Data::Folder *folder = nullptr);\n\tvoid requestPinnedDialogs(Data::Folder *folder = nullptr);\n\tvoid requestMoreBlockedByDateDialogs();\n\tvoid requestMoreDialogsIfNeeded();\n\trpl::producer<bool> dialogsLoadMayBlockByDate() const;\n\trpl::producer<bool> dialogsLoadBlockedByDate() const;\n\n\tvoid requestWallPaper(\n\t\tconst QString &slug,\n\t\tFn<void(const Data::WallPaper &)> done,\n\t\tFn<void()> fail);\n\n\tvoid requestFullPeer(not_null<PeerData*> peer);\n\tvoid requestPeerSettings(not_null<PeerData*> peer);\n\n\tusing UpdatedFileReferences = Data::UpdatedFileReferences;\n\tusing FileReferencesHandler = FnMut<void(const UpdatedFileReferences&)>;\n\tvoid refreshFileReference(\n\t\tData::FileOrigin origin,\n\t\tFileReferencesHandler &&handler);\n\tvoid refreshFileReference(\n\t\tData::FileOrigin origin,\n\t\tnot_null<Storage::DownloadMtprotoTask*> task,\n\t\tint requestId,\n\t\tconst QByteArray &current);\n\n\tvoid requestChangelog(\n\t\tconst QString &sinceVersion,\n\t\tFn<void(const MTPUpdates &result)> callback);\n\tvoid requestDeepLinkInfo(\n\t\tconst QString &path,\n\t\tFn<void(TextWithEntities message, bool updateRequired)> callback);\n\tvoid requestTermsUpdate();\n\tvoid acceptTerms(bytes::const_span termsId);\n\n\tvoid checkChatInvite(\n\t\tconst QString &hash,\n\t\tFnMut<void(const MTPChatInvite &)> done,\n\t\tFn<void(const MTP::Error &)> fail);\n\tvoid checkFilterInvite(\n\t\tconst QString &slug,\n\t\tFnMut<void(const MTPchatlists_ChatlistInvite &)> done,\n\t\tFn<void(const MTP::Error &)> fail);\n\n\tvoid processFullPeer(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPmessages_ChatFull &result);\n\n\tvoid migrateChat(\n\t\tnot_null<ChatData*> chat,\n\t\tFnMut<void(not_null<ChannelData*>)> done,\n\t\tFn<void(const QString &)> fail = nullptr);\n\n\tvoid markContentsRead(\n\t\tconst base::flat_set<not_null<HistoryItem*>> &items);\n\tvoid markContentsRead(not_null<HistoryItem*> item);\n\n\tvoid deleteAllFromParticipant(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<PeerData*> from);\n\tvoid deleteSublistHistory(\n\t\tnot_null<ChannelData*> parentChat,\n\t\tnot_null<PeerData*> sublistPeer);\n\n\tvoid requestWebPageDelayed(not_null<WebPageData*> page);\n\tvoid clearWebPageRequest(not_null<WebPageData*> page);\n\tvoid clearWebPageRequests();\n\n\tvoid scheduleStickerSetRequest(uint64 setId, uint64 access);\n\tvoid requestStickerSets();\n\tvoid saveStickerSets(\n\t\tconst Data::StickersSetsOrder &localOrder,\n\t\tconst Data::StickersSetsOrder &localRemoved,\n\t\tData::StickersType type);\n\tvoid updateStickers();\n\tvoid updateSavedGifs();\n\tvoid updateMasks();\n\tvoid updateCustomEmoji();\n\tvoid requestSpecialStickersForce(\n\t\tbool faved,\n\t\tbool recent,\n\t\tbool attached);\n\tvoid setGroupStickerSet(\n\t\tnot_null<ChannelData*> megagroup,\n\t\tconst StickerSetIdentifier &set);\n\tvoid setGroupEmojiSet(\n\t\tnot_null<ChannelData*> megagroup,\n\t\tconst StickerSetIdentifier &set);\n\t[[nodiscard]] std::vector<not_null<DocumentData*>> *stickersByEmoji(\n\t\tconst QString &key);\n\n\tvoid joinChannel(not_null<ChannelData*> channel);\n\tvoid leaveChannel(not_null<ChannelData*> channel);\n\n\tvoid requestNotifySettings(const MTPInputNotifyPeer &peer);\n\tvoid updateNotifySettingsDelayed(not_null<const Data::Thread*> thread);\n\tvoid updateNotifySettingsDelayed(not_null<const PeerData*> peer);\n\tvoid updateNotifySettingsDelayed(Data::DefaultNotify type);\n\tvoid saveDraftToCloudDelayed(not_null<Data::Thread*> thread);\n\n\tvoid clearHistory(not_null<PeerData*> peer, bool revoke);\n\tvoid deleteConversation(not_null<PeerData*> peer, bool revoke);\n\n\tbool isQuitPrevent();\n\n\tvoid resolveJumpToDate(\n\t\tDialogs::Key chat,\n\t\tconst QDate &date,\n\t\tFn<void(not_null<PeerData*>, MsgId)> callback);\n\n\tusing SliceType = Data::LoadDirection;\n\tvoid requestHistory(\n\t\tnot_null<History*> history,\n\t\tMsgId messageId,\n\t\tSliceType slice);\n\tvoid requestSharedMedia(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tStorage::SharedMediaType type,\n\t\tMsgId messageId,\n\t\tSliceType slice);\n\tmtpRequestId requestGlobalMedia(\n\t\tStorage::SharedMediaType type,\n\t\tconst QString &query,\n\t\tint32 offsetRate,\n\t\tData::MessagePosition offsetPosition,\n\t\tFn<void(Api::GlobalMediaResult)> done);\n\n\tvoid readFeaturedSetDelayed(uint64 setId);\n\n\trpl::producer<SendAction> sendActions() const {\n\t\treturn _sendActions.events();\n\t}\n\tvoid sendAction(const SendAction &action);\n\tvoid finishForwarding(const SendAction &action);\n\tvoid forwardMessages(\n\t\tData::ResolvedForwardDraft &&draft,\n\t\tSendAction action,\n\t\tFnMut<void()> &&successCallback = nullptr);\n\tvoid shareContact(\n\t\tconst QString &phone,\n\t\tconst QString &firstName,\n\t\tconst QString &lastName,\n\t\tconst SendAction &action,\n\t\tFn<void(bool)> done = nullptr);\n\tvoid shareContact(\n\t\tnot_null<UserData*> user,\n\t\tconst SendAction &action,\n\t\tFn<void(bool)> done = nullptr);\n\tvoid applyAffectedMessages(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPmessages_AffectedMessages &result);\n\n\tvoid sendVoiceMessage(\n\t\tQByteArray result,\n\t\tVoiceWaveform waveform,\n\t\tcrl::time duration,\n\t\tbool video,\n\t\tconst SendAction &action);\n\tvoid sendFiles(\n\t\tUi::PreparedList &&list,\n\t\tSendMediaType type,\n\t\tstd::shared_ptr<SendingAlbum> album,\n\t\tconst SendAction &action);\n\tvoid sendFile(\n\t\tconst QByteArray &fileContent,\n\t\tSendMediaType type,\n\t\tconst SendAction &action);\n\n\tvoid editMedia(\n\t\tUi::PreparedList &&list,\n\t\tSendMediaType type,\n\t\tTextWithTags &&caption,\n\t\tconst SendAction &action);\n\n\tvoid sendUploadedPhoto(\n\t\tFullMsgId localId,\n\t\tApi::RemoteFileInfo info,\n\t\tApi::SendOptions options);\n\tvoid sendUploadedDocument(\n\t\tFullMsgId localId,\n\t\tApi::RemoteFileInfo file,\n\t\tApi::SendOptions options);\n\n\tvoid cancelLocalItem(not_null<HistoryItem*> item);\n\n\tvoid sendShortcutMessages(\n\t\tnot_null<PeerData*> peer,\n\t\tBusinessShortcutId id);\n\tvoid sendMessage(\n\t\tMessageToSend &&message,\n\t\tstd::optional<MsgId> localMessageId = std::nullopt);\n\tvoid sendBotStart(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<UserData*> bot,\n\t\tPeerData *chat = nullptr,\n\t\tconst QString &startTokenForChat = QString());\n\tvoid sendInlineResult(\n\t\tnot_null<UserData*> bot,\n\t\tnot_null<InlineBots::Result*> data,\n\t\tSendAction action,\n\t\tstd::optional<MsgId> localMessageId,\n\t\tFn<void(bool)> done = nullptr);\n\tvoid sendMessageFail(\n\t\tconst MTP::Error &error,\n\t\tnot_null<PeerData*> peer,\n\t\tuint64 randomId = 0,\n\t\tFullMsgId itemId = FullMsgId());\n\tvoid sendMessageFail(\n\t\tconst QString &error,\n\t\tnot_null<PeerData*> peer,\n\t\tuint64 randomId = 0,\n\t\tFullMsgId itemId = FullMsgId());\n\n\tvoid reloadContactSignupSilent();\n\trpl::producer<bool> contactSignupSilent() const;\n\tstd::optional<bool> contactSignupSilentCurrent() const;\n\tvoid saveContactSignupSilent(bool silent);\n\n\t[[nodiscard]] auto botCommonGroups(not_null<UserData*> bot) const\n\t\t-> std::optional<std::vector<not_null<PeerData*>>>;\n\tvoid requestBotCommonGroups(not_null<UserData*> bot, Fn<void()> done);\n\n\tvoid saveSelfBio(const QString &text);\n\n\tvoid registerStatsRequest(MTP::DcId dcId, mtpRequestId id);\n\tvoid unregisterStatsRequest(MTP::DcId dcId, mtpRequestId id);\n\n\tvoid rememberToDeleteAfterForward(MessageIdsList ids) {\n\t\t_deleteAfterForward.insert(_deleteAfterForward.end(), ids.begin(), ids.end());\n\t}\n\n\t[[nodiscard]] Api::Authorizations &authorizations();\n\t[[nodiscard]] Api::AttachedStickers &attachedStickers();\n\t[[nodiscard]] Api::BlockedPeers &blockedPeers();\n\t[[nodiscard]] Api::CloudPassword &cloudPassword();\n\t[[nodiscard]] Api::SelfDestruct &selfDestruct();\n\t[[nodiscard]] Api::SensitiveContent &sensitiveContent();\n\t[[nodiscard]] Api::GlobalPrivacy &globalPrivacy();\n\t[[nodiscard]] Api::ReactionsNotifySettings &reactionsNotifySettings();\n\t[[nodiscard]] Api::UserPrivacy &userPrivacy();\n\t[[nodiscard]] Api::InviteLinks &inviteLinks();\n\t[[nodiscard]] Api::ChatLinks &chatLinks();\n\t[[nodiscard]] Api::ViewsManager &views();\n\t[[nodiscard]] Api::ReadMetrics &readMetrics();\n\t[[nodiscard]] Api::ConfirmPhone &confirmPhone();\n\t[[nodiscard]] Api::PeerPhoto &peerPhoto();\n\t[[nodiscard]] Api::Polls &polls();\n\t[[nodiscard]] Api::TodoLists &todoLists();\n\t[[nodiscard]] Api::ChatParticipants &chatParticipants();\n\t[[nodiscard]] Api::UnreadThings &unreadThings();\n\t[[nodiscard]] Api::Ringtones &ringtones();\n\t[[nodiscard]] Api::ComposeWithAi &composeWithAi();\n\t[[nodiscard]] Api::Transcribes &transcribes();\n\t[[nodiscard]] Api::Premium &premium();\n\t[[nodiscard]] Api::Usernames &usernames();\n\t[[nodiscard]] Api::Websites &websites();\n\t[[nodiscard]] Api::PeerColors &peerColors();\n\n\tvoid updatePrivacyLastSeens();\n\n\tstatic constexpr auto kJoinErrorDuration = 5 * crl::time(1000);\n\n\tstatic void ProcessRecentSelfForwards(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPUpdates &updates,\n\t\tPeerId targetPeerId,\n\t\tPeerId fromPeerId);\n\nprivate:\n\tstruct MessageDataRequest {\n\t\tusing Callbacks = std::vector<Fn<void()>>;\n\n\t\tmtpRequestId requestId = 0;\n\t\tCallbacks callbacks;\n\t};\n\tusing MessageDataRequests = base::flat_map<MsgId, MessageDataRequest>;\n\tusing SharedMediaType = Storage::SharedMediaType;\n\n\tstruct StickersByEmoji {\n\t\tstd::vector<not_null<DocumentData*>> list;\n\t\tuint64 hash = 0;\n\t\tcrl::time received = 0;\n\t};\n\n\tstruct DialogsLoadState {\n\t\tTimeId offsetDate = 0;\n\t\tMsgId offsetId = 0;\n\t\tPeerData *offsetPeer = nullptr;\n\t\tmtpRequestId requestId = 0;\n\t\tbool listReceived = false;\n\n\t\tmtpRequestId pinnedRequestId = 0;\n\t\tbool pinnedReceived = false;\n\t};\n\n\tvoid setupSupportMode();\n\tvoid refreshDialogsLoadBlocked();\n\tvoid updateDialogsOffset(\n\t\tData::Folder *folder,\n\t\tconst QVector<MTPDialog> &dialogs,\n\t\tconst QVector<MTPMessage> &messages);\n\tvoid requestMoreDialogs(Data::Folder *folder);\n\tDialogsLoadState *dialogsLoadState(Data::Folder *folder);\n\tvoid dialogsLoadFinish(Data::Folder *folder);\n\n\tvoid checkQuitPreventFinished();\n\n\tvoid saveDraftsToCloud();\n\n\tvoid resolveMessageDatas();\n\tvoid finalizeMessageDataRequest(\n\t\tChannelData *channel,\n\t\tmtpRequestId requestId);\n\n\t[[nodiscard]] QVector<MTPInputMessage> collectMessageIds(\n\t\tconst MessageDataRequests &requests);\n\t[[nodiscard]] MessageDataRequests *messageDataRequests(\n\t\tChannelData *channel,\n\t\tbool onlyExisting = false);\n\n\tvoid gotChatFull(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPmessages_ChatFull &result);\n\tvoid gotUserFull(\n\t\tnot_null<UserData*> user,\n\t\tconst MTPusers_UserFull &result);\n\tvoid resolveWebPages();\n\tvoid gotWebPages(\n\t\tChannelData *channel,\n\t\tconst MTPmessages_Messages &result,\n\t\tmtpRequestId req);\n\tvoid gotStickerSet(uint64 setId, const MTPmessages_StickerSet &result);\n\n\tvoid requestStickers(TimeId now);\n\tvoid requestMasks(TimeId now);\n\tvoid requestCustomEmoji(TimeId now);\n\tvoid requestRecentStickers(\n\t\tstd::optional<TimeId> now,\n\t\tbool attached);\n\tvoid requestFavedStickers(std::optional<TimeId> now);\n\tvoid requestFeaturedStickers(TimeId now);\n\tvoid requestFeaturedEmoji(TimeId now);\n\tvoid requestSavedGifs(TimeId now);\n\tvoid readFeaturedSets();\n\n\tvoid resolveJumpToHistoryDate(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tconst QDate &date,\n\t\tFn<void(not_null<PeerData*>, MsgId)> callback);\n\ttemplate <typename Callback>\n\tvoid requestMessageAfterDate(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tconst QDate &date,\n\t\tCallback &&callback);\n\n\tvoid sharedMediaDone(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tSharedMediaType type,\n\t\tApi::SearchResult &&parsed);\n\tvoid globalMediaDone(\n\t\tSharedMediaType type,\n\t\tFullMsgId messageId,\n\t\tApi::GlobalMediaResult &&parsed);\n\n\tvoid sendSharedContact(\n\t\tconst QString &phone,\n\t\tconst QString &firstName,\n\t\tconst QString &lastName,\n\t\tUserId userId,\n\t\tconst SendAction &action,\n\t\tFn<void(bool)> done);\n\n\tvoid deleteHistory(\n\t\tnot_null<PeerData*> peer,\n\t\tbool justClear,\n\t\tbool revoke);\n\tvoid applyAffectedMessages(\n\t\tconst MTPmessages_AffectedMessages &result) const;\n\n\tvoid deleteAllFromParticipantSend(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<PeerData*> from);\n\tvoid deleteSublistHistorySend(\n\t\tnot_null<ChannelData*> parentChat,\n\t\tnot_null<PeerData*> sublistPeer);\n\n\tvoid uploadAlbumMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MessageGroupId &groupId,\n\t\tconst MTPInputMedia &media);\n\tvoid sendAlbumWithUploaded(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MessageGroupId &groupId,\n\t\tconst MTPInputMedia &media);\n\tvoid sendAlbumWithCancelled(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MessageGroupId &groupId);\n\tvoid sendAlbumIfReady(not_null<SendingAlbum*> album);\n\tvoid sendMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPInputMedia &media,\n\t\tApi::SendOptions options,\n\t\tFn<void(bool)> done = nullptr);\n\tvoid sendMediaWithRandomId(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPInputMedia &media,\n\t\tApi::SendOptions options,\n\t\tuint64 randomId,\n\t\tFn<void(bool)> done = nullptr);\n\tvoid sendMultiPaidMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<SendingAlbum*> album,\n\t\tFn<void(bool)> done = nullptr);\n\n\tvoid sendNotifySettingsUpdates();\n\n\ttemplate <typename Request>\n\tvoid requestFileReference(\n\t\tData::FileOrigin origin,\n\t\tFileReferencesHandler &&handler,\n\t\tRequest &&data);\n\n\tvoid migrateDone(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<ChannelData*> channel);\n\tvoid migrateFail(not_null<PeerData*> peer, const QString &error);\n\n\tvoid checkStatsSessions();\n\n\tconst not_null<Main::Session*> _session;\n\n\tbase::flat_map<QString, int> _modifyRequests;\n\n\tMessageDataRequests _messageDataRequests;\n\tbase::flat_map<\n\t\tnot_null<ChannelData*>,\n\t\tMessageDataRequests> _channelMessageDataRequests;\n\tSingleQueuedInvokation _messageDataResolveDelayed;\n\n\tusing PeerRequests = base::flat_map<PeerData*, mtpRequestId>;\n\tPeerRequests _fullPeerRequests;\n\tbase::flat_set<not_null<PeerData*>> _requestedPeerSettings;\n\n\tbase::flat_map<\n\t\tnot_null<History*>,\n\t\tstd::pair<mtpRequestId,Fn<void()>>> _historyArchivedRequests;\n\n\tbase::flat_map<not_null<WebPageData*>, mtpRequestId> _webPagesPending;\n\tbase::Timer _webPagesTimer;\n\n\tstruct StickerSetRequest {\n\t\tuint64 accessHash = 0;\n\t\tmtpRequestId id = 0;\n\t};\n\tbase::flat_map<uint64, StickerSetRequest> _stickerSetRequests;\n\n\tbase::flat_map<\n\t\tnot_null<ChannelData*>,\n\t\tmtpRequestId> _channelAmInRequests;\n\n\tstruct NotifySettingsKey {\n\t\tPeerId peerId = 0;\n\t\tMsgId topicRootId = 0;\n\n\t\tfriend inline constexpr auto operator<=>(\n\t\t\tNotifySettingsKey,\n\t\t\tNotifySettingsKey) = default;\n\t};\n\tbase::flat_map<NotifySettingsKey, mtpRequestId> _notifySettingRequests;\n\n\tbase::flat_map<\n\t\tbase::weak_ptr<Data::Thread>,\n\t\tmtpRequestId> _draftsSaveRequestIds;\n\tbase::Timer _draftsSaveTimer;\n\n\tbase::flat_set<mtpRequestId> _stickerSetDisenableRequests;\n\tbase::flat_set<mtpRequestId> _maskSetDisenableRequests;\n\tbase::flat_set<mtpRequestId> _customEmojiSetDisenableRequests;\n\tmtpRequestId _masksReorderRequestId = 0;\n\tmtpRequestId _customEmojiReorderRequestId = 0;\n\tmtpRequestId _stickersReorderRequestId = 0;\n\tmtpRequestId _stickersClearRecentRequestId = 0;\n\tmtpRequestId _stickersClearRecentAttachedRequestId = 0;\n\n\tmtpRequestId _stickersUpdateRequest = 0;\n\tmtpRequestId _masksUpdateRequest = 0;\n\tmtpRequestId _customEmojiUpdateRequest = 0;\n\tmtpRequestId _recentStickersUpdateRequest = 0;\n\tmtpRequestId _recentAttachedStickersUpdateRequest = 0;\n\tmtpRequestId _favedStickersUpdateRequest = 0;\n\tmtpRequestId _featuredStickersUpdateRequest = 0;\n\tmtpRequestId _featuredEmojiUpdateRequest = 0;\n\tmtpRequestId _savedGifsUpdateRequest = 0;\n\n\tbase::Timer _featuredSetsReadTimer;\n\tbase::flat_set<uint64> _featuredSetsRead;\n\n\tbase::flat_map<QString, StickersByEmoji> _stickersByEmoji;\n\n\tmtpRequestId _contactsRequestId = 0;\n\tmtpRequestId _contactsStatusesRequestId = 0;\n\n\tstruct SharedMediaRequest {\n\t\tnot_null<PeerData*> peer;\n\t\tMsgId topicRootId = 0;\n\t\tPeerId monoforumPeerId = 0;\n\t\tSharedMediaType mediaType = {};\n\t\tMsgId aroundId = 0;\n\t\tSliceType sliceType = {};\n\n\t\tfriend inline auto operator<=>(\n\t\t\tconst SharedMediaRequest&,\n\t\t\tconst SharedMediaRequest&) = default;\n\t};\n\tbase::flat_set<SharedMediaRequest> _sharedMediaRequests;\n\n\tstruct HistoryRequest {\n\t\tnot_null<PeerData*> peer;\n\t\tMsgId aroundId = 0;\n\t\tSliceType sliceType = {};\n\n\t\tfriend inline auto operator<=>(\n\t\t\tconst HistoryRequest&,\n\t\t\tconst HistoryRequest&) = default;\n\t};\n\tbase::flat_set<HistoryRequest> _historyRequests;\n\n\tstruct GlobalMediaRequest {\n\t\tSharedMediaType mediaType = {};\n\t\tFullMsgId aroundId;\n\t\tSliceType sliceType = {};\n\n\t\tfriend inline auto operator<=>(\n\t\t\tconst GlobalMediaRequest&,\n\t\t\tconst GlobalMediaRequest&) = default;\n\t};\n\tbase::flat_set<GlobalMediaRequest> _globalMediaRequests;\n\n\tstd::unique_ptr<DialogsLoadState> _dialogsLoadState;\n\tTimeId _dialogsLoadTill = 0;\n\trpl::variable<bool> _dialogsLoadMayBlockByDate = false;\n\trpl::variable<bool> _dialogsLoadBlockedByDate = false;\n\n\tbase::flat_map<\n\t\tnot_null<Data::Folder*>,\n\t\tDialogsLoadState> _foldersLoadState;\n\n\trpl::event_stream<SendAction> _sendActions;\n\n\tstd::unique_ptr<TaskQueue> _fileLoader;\n\tbase::flat_map<uint64, std::shared_ptr<SendingAlbum>> _sendingAlbums;\n\n\tbase::flat_set<not_null<const Data::ForumTopic*>> _updateNotifyTopics;\n\tbase::flat_set<not_null<const PeerData*>> _updateNotifyPeers;\n\tbase::flat_set<Data::DefaultNotify> _updateNotifyDefaults;\n\tbase::Timer _updateNotifyTimer;\n\trpl::lifetime _updateNotifyQueueLifetime;\n\n\tstd::map<\n\t\tData::FileOrigin,\n\t\tstd::vector<FileReferencesHandler>> _fileReferenceHandlers;\n\n\tmtpRequestId _deepLinkInfoRequestId = 0;\n\n\tcrl::time _termsUpdateSendAt = 0;\n\tmtpRequestId _termsUpdateRequestId = 0;\n\n\tmtpRequestId _checkInviteRequestId = 0;\n\tmtpRequestId _checkFilterInviteRequestId = 0;\n\n\tstruct MigrateCallbacks {\n\t\tFnMut<void(not_null<ChannelData*>)> done;\n\t\tFn<void(const QString&)> fail;\n\t};\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tstd::vector<MigrateCallbacks>> _migrateCallbacks;\n\n\tstruct {\n\t\tmtpRequestId requestId = 0;\n\t\tQString requestedText;\n\t} _bio;\n\n\tbase::flat_map<MTP::DcId, base::flat_set<mtpRequestId>> _statsRequests;\n\tbase::Timer _statsSessionKillTimer;\n\n\tconst std::unique_ptr<Api::Authorizations> _authorizations;\n\tconst std::unique_ptr<Api::AttachedStickers> _attachedStickers;\n\tconst std::unique_ptr<Api::BlockedPeers> _blockedPeers;\n\tconst std::unique_ptr<Api::CloudPassword> _cloudPassword;\n\tconst std::unique_ptr<Api::SelfDestruct> _selfDestruct;\n\tconst std::unique_ptr<Api::SensitiveContent> _sensitiveContent;\n\tconst std::unique_ptr<Api::GlobalPrivacy> _globalPrivacy;\n\tconst std::unique_ptr<Api::ReactionsNotifySettings> _reactionsNotifySettings;\n\tconst std::unique_ptr<Api::UserPrivacy> _userPrivacy;\n\tconst std::unique_ptr<Api::InviteLinks> _inviteLinks;\n\tconst std::unique_ptr<Api::ChatLinks> _chatLinks;\n\tconst std::unique_ptr<Api::ViewsManager> _views;\n\tconst std::unique_ptr<Api::ReadMetrics> _readMetrics;\n\tconst std::unique_ptr<Api::ConfirmPhone> _confirmPhone;\n\tconst std::unique_ptr<Api::PeerPhoto> _peerPhoto;\n\tconst std::unique_ptr<Api::Polls> _polls;\n\tconst std::unique_ptr<Api::TodoLists> _todoLists;\n\tconst std::unique_ptr<Api::ChatParticipants> _chatParticipants;\n\tconst std::unique_ptr<Api::UnreadThings> _unreadThings;\n\tconst std::unique_ptr<Api::Ringtones> _ringtones;\n\tconst std::unique_ptr<Api::ComposeWithAi> _composeWithAi;\n\tconst std::unique_ptr<Api::Transcribes> _transcribes;\n\tconst std::unique_ptr<Api::Premium> _premium;\n\tconst std::unique_ptr<Api::Usernames> _usernames;\n\tconst std::unique_ptr<Api::Websites> _websites;\n\tconst std::unique_ptr<Api::PeerColors> _peerColors;\n\n\tMessageIdsList _deleteAfterForward;\n\n\tmtpRequestId _wallPaperRequestId = 0;\n\tQString _wallPaperSlug;\n\tFn<void(const Data::WallPaper &)> _wallPaperDone;\n\tFn<void()> _wallPaperFail;\n\n\tmtpRequestId _contactSignupSilentRequestId = 0;\n\tstd::optional<bool> _contactSignupSilent;\n\trpl::event_stream<bool> _contactSignupSilentChanges;\n\n\tbase::flat_map<\n\t\tnot_null<UserData*>,\n\t\tstd::vector<not_null<PeerData*>>> _botCommonGroups;\n\tbase::flat_map<not_null<UserData*>, Fn<void()>> _botCommonGroupsRequests;\n\n\tbase::flat_map<FullMsgId, QString> _unlikelyMessageLinks;\n\tbase::flat_map<FullStoryId, QString> _unlikelyStoryLinks;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/about_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/about_box.h\"\n\n#include \"base/platform/base_platform_info.h\"\n#include \"core/application.h\"\n#include \"core/file_utilities.h\"\n#include \"core/update_checker.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_channel_earn.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QClipboard>\n\nnamespace {\n\nrpl::producer<TextWithEntities> Text1() {\n\treturn rpl::single<TextWithEntities>({\n\t\t.text = \"Forkgram based on the Telegram Desktop.\" });\n\treturn tr::lng_about_text1(\n\t\tlt_api_link,\n\t\ttr::lng_about_text1_api(tr::url(u\"https://core.telegram.org/api\"_q)),\n\t\ttr::marked);\n}\n\nrpl::producer<TextWithEntities> Text2() {\n\treturn tr::lng_about_text2(\n\t\tlt_gpl_link,\n\t\trpl::single(tr::link(\n\t\t\t\"GNU GPL\",\n\t\t\t\"https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE\")),\n\t\tlt_github_link,\n\t\trpl::single(tr::link(\n\t\t\t\"GitHub\",\n\t\t\t\"https://github.com/telegramdesktop/tdesktop\")),\n\t\ttr::marked);\n}\n\nrpl::producer<TextWithEntities> Text3() {\n\treturn tr::lng_about_text3(\n\t\tlt_faq_link,\n\t\ttr::lng_about_text3_faq(tr::url(telegramFaqLink())),\n\t\ttr::marked);\n}\n\n} // namespace\n\nvoid AboutBox(not_null<Ui::GenericBox*> box) {\n\tbox->setTitle(rpl::single(QString(AppNameF.utf8())));\n\n\tauto layout = box->verticalLayout();\n\n\tconst auto version = layout->add(\n\t\tobject_ptr<Ui::LinkButton>(\n\t\t\tbox,\n\t\t\ttr::lng_about_version(\n\t\t\t\ttr::now,\n\t\t\t\tlt_version,\n\t\t\t\tcurrentVersionText()),\n\t\t\tst::aboutVersionLink),\n\t\tQMargins(\n\t\t\tst::boxRowPadding.left(),\n\t\t\t-st::lineWidth * 3,\n\t\t\tst::boxRowPadding.right(),\n\t\t\tst::boxRowPadding.bottom()));\n\tversion->setClickedCallback([=] {\n\t\tif (cRealAlphaVersion()) {\n\t\t\tauto url = u\"https://tdesktop.com/\"_q;\n\t\t\tif (Platform::IsWindows32Bit()) {\n\t\t\t\turl += u\"win/%1.zip\"_q;\n\t\t\t} else if (Platform::IsWindows64Bit()) {\n\t\t\t\turl += u\"win64/%1.zip\"_q;\n\t\t\t} else if (Platform::IsWindowsARM64()) {\n\t\t\t\turl += u\"winarm/%1.zip\"_q;\n\t\t\t} else if (Platform::IsMac()) {\n\t\t\t\turl += u\"mac/%1.zip\"_q;\n\t\t\t} else if (Platform::IsLinux()) {\n\t\t\t\turl += u\"linux/%1.tar.xz\"_q;\n\t\t\t} else {\n\t\t\t\tUnexpected(\"Platform value.\");\n\t\t\t}\n\t\t\turl = url.arg(u\"talpha%1_%2\"_q\n\t\t\t\t.arg(cRealAlphaVersion())\n\t\t\t\t.arg(Core::countAlphaVersionSignature(cRealAlphaVersion())));\n\n\t\t\tQGuiApplication::clipboard()->setText(url);\n\n\t\t\tbox->getDelegate()->show(\n\t\t\t\tUi::MakeInformBox(\n\t\t\t\t\t\"The link to the current private alpha \"\n\t\t\t\t\t\"version of Telegram Desktop was copied \"\n\t\t\t\t\t\"to the clipboard.\"));\n\t\t} else {\n\t\t\tFile::OpenUrl(Core::App().changelogLink());\n\t\t}\n\t});\n\n\tUi::AddSkip(layout, st::aboutTopSkip);\n\n\tconst auto addText = [&](rpl::producer<TextWithEntities> text) {\n\t\tconst auto label = layout->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(box, std::move(text), st::aboutLabel),\n\t\t\tst::boxRowPadding);\n\t\tlabel->setLinksTrusted();\n\t\tUi::AddSkip(layout, st::aboutSkip);\n\t};\n\n\taddText(Text1());\n\taddText(Text2());\n\taddText(Text3());\n\n\tbox->addButton(tr::lng_close(), [=] { box->closeBox(); });\n\n\tbox->setWidth(st::aboutWidth);\n}\n\nQString telegramFaqLink() {\n\tconst auto result = u\"https://telegram.org/faq\"_q;\n\tconst auto langpacked = [&](const char *language) {\n\t\treturn result + '/' + language;\n\t};\n\tconst auto current = Lang::Id();\n\tfor (const auto language : { \"de\", \"es\", \"it\", \"ko\" }) {\n\t\tif (current.startsWith(QLatin1String(language))) {\n\t\t\treturn langpacked(language);\n\t\t}\n\t}\n\tif (current.startsWith(u\"pt-br\"_q)) {\n\t\treturn langpacked(\"br\");\n\t}\n\treturn result;\n}\n\nQString currentVersionText() {\n\tauto result = QString::fromLatin1(AppVersionStr);\n\tif (cAlphaVersion()) {\n\t\tresult += u\" alpha %1\"_q.arg(cAlphaVersion() % 1000);\n\t} else if (AppBetaVersion) {\n\t\tresult += \" beta\";\n\t}\n\tif (Platform::IsWindows64Bit()) {\n\t\tresult += \" x64\";\n\t} else if (Platform::IsWindowsARM64()) {\n\t\tresult += \" arm64\";\n\t}\n#ifdef _DEBUG\n\tresult += \" DEBUG\";\n#endif\n\treturn result;\n}\n\nvoid ArchiveHintBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tbool unarchiveOnNewMessage,\n\t\tFn<void()> onUnarchive) {\n\tbox->setNoContentMargin(true);\n\n\tconst auto content = box->verticalLayout().get();\n\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\t{\n\t\tconst auto &icon = st::dialogsArchiveUserpic;\n\t\tconst auto rect = Rect(icon.size() * 2);\n\t\tauto owned = object_ptr<Ui::RpWidget>(content);\n\t\towned->resize(rect.size());\n\t\towned->setNaturalWidth(rect.width());\n\t\tconst auto widget = box->addRow(std::move(owned), style::al_top);\n\t\twidget->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = Painter(widget);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(st::activeButtonBg);\n\t\t\tp.drawEllipse(rect);\n\t\t\ticon.paintInCenter(p, rect);\n\t\t}, widget->lifetime());\n\t}\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_archive_hint_title(),\n\t\t\tst::boxTitle),\n\t\tstyle::al_top);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\t{\n\t\tconst auto label = box->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\t(unarchiveOnNewMessage\n\t\t\t\t\t\t? tr::lng_archive_hint_about_unmuted\n\t\t\t\t\t\t: tr::lng_archive_hint_about)(\n\t\t\t\t\tlt_link,\n\t\t\t\t\ttr::lng_archive_hint_about_link(\n\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\trpl::single(\n\t\t\t\t\t\t\tUi::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\t\t\t\ttr::rich\n\t\t\t\t\t) | rpl::map([](TextWithEntities text) {\n\t\t\t\t\t\treturn tr::link(std::move(text), 1);\n\t\t\t\t\t}),\n\t\t\t\t\ttr::rich),\n\t\t\t\tst::channelEarnHistoryRecipientLabel));\n\t\tlabel->resizeToWidth(box->width()\n\t\t\t- rect::m::sum::h(st::boxRowPadding));\n\t\tlabel->setLink(\n\t\t\t1,\n\t\t\tstd::make_shared<GenericClickHandler>([=](ClickContext context) {\n\t\t\t\tif (context.button == Qt::LeftButton) {\n\t\t\t\t\tonUnarchive();\n\t\t\t\t}\n\t\t\t}));\n\t}\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\t{\n\t\tconst auto padding = QMargins(\n\t\t\tst::settingsButton.padding.left(),\n\t\t\tst::boxRowPadding.top(),\n\t\t\tst::boxRowPadding.right(),\n\t\t\tst::boxRowPadding.bottom());\n\t\tconst auto addEntry = [&](\n\t\t\t\trpl::producer<QString> title,\n\t\t\t\trpl::producer<QString> about,\n\t\t\t\tconst style::icon &icon) {\n\t\t\tconst auto top = content->add(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tcontent,\n\t\t\t\t\tstd::move(title),\n\t\t\t\t\tst::channelEarnSemiboldLabel),\n\t\t\t\tpadding);\n\t\t\tUi::AddSkip(content, st::channelEarnHistoryThreeSkip);\n\t\t\tcontent->add(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tcontent,\n\t\t\t\t\tstd::move(about),\n\t\t\t\t\tst::channelEarnHistoryRecipientLabel),\n\t\t\t\tpadding);\n\t\t\tconst auto left = Ui::CreateChild<Ui::RpWidget>(\n\t\t\t\tbox->verticalLayout().get());\n\t\t\tleft->paintRequest(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tauto p = Painter(left);\n\t\t\t\ticon.paint(p, 0, 0, left->width());\n\t\t\t}, left->lifetime());\n\t\t\tleft->resize(icon.size());\n\t\t\ttop->geometryValue(\n\t\t\t) | rpl::on_next([=](const QRect &g) {\n\t\t\t\tleft->moveToLeft(\n\t\t\t\t\t(g.left() - left->width()) / 2,\n\t\t\t\t\tg.top() + st::channelEarnHistoryThreeSkip);\n\t\t\t}, left->lifetime());\n\t\t};\n\t\taddEntry(\n\t\t\ttr::lng_archive_hint_section_1(),\n\t\t\ttr::lng_archive_hint_section_1_info(),\n\t\t\tst::menuIconArchive);\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\t\taddEntry(\n\t\t\ttr::lng_archive_hint_section_2(),\n\t\t\ttr::lng_archive_hint_section_2_info(),\n\t\t\tst::menuIconStealth);\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\t\taddEntry(\n\t\t\ttr::lng_archive_hint_section_3(),\n\t\t\ttr::lng_archive_hint_section_3_info(),\n\t\t\tst::menuIconStoriesSavedSection);\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\t}\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\t{\n\t\tconst auto &st = st::premiumPreviewDoubledLimitsBox;\n\t\tbox->setStyle(st);\n\t\tauto button = object_ptr<Ui::RoundButton>(\n\t\t\tbox,\n\t\t\ttr::lng_archive_hint_button(),\n\t\t\tst::defaultActiveButton);\n\t\tbutton->resizeToWidth(box->width()\n\t\t\t- st.buttonPadding.left()\n\t\t\t- st.buttonPadding.left());\n\t\tbutton->setClickedCallback([=] { box->closeBox(); });\n\t\tbox->addButton(std::move(button));\n\t}\n}\n\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/about_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n\nvoid AboutBox(not_null<Ui::GenericBox*> box);\nvoid ArchiveHintBox(\n\tnot_null<Ui::GenericBox*> box,\n\tbool unarchiveOnNewMessage,\n\tFn<void()> onUnarchive);\n\nQString telegramFaqLink();\nQString currentVersionText();\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/about_sponsored_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/about_sponsored_box.h\"\n\n#include \"core/file_utilities.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kUrl = \"https://ads.telegram.org\"_cs;\n\n} // namespace\n\nvoid AboutSponsoredBox(not_null<Ui::GenericBox*> box) {\n\tbox->setTitle(tr::lng_sponsored_title());\n\tbox->setWidth(st::boxWideWidth);\n\tbox->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });\n\n\tconst auto addUrl = [&] {\n\t\tconst auto &st = st::sponsoredUrlButton;\n\t\tconst auto row = box->addRow(object_ptr<RpWidget>(box));\n\t\trow->resize(0, st.height + st.padding.top() + st.padding.bottom());\n\t\tconst auto button = Ui::CreateChild<RoundButton>(\n\t\t\trow,\n\t\t\trpl::single<QString>(kUrl.utf8()),\n\t\t\tst);\n\t\tbutton->setBrushOverride(Qt::NoBrush);\n\t\tbutton->setPenOverride(QPen(st::historyLinkInFg));\n\t\trpl::combine(\n\t\t\trow->sizeValue(),\n\t\t\tbutton->sizeValue()\n\t\t) | rpl::on_next([=](\n\t\t\t\tconst QSize &rowSize,\n\t\t\t\tconst QSize &buttonSize) {\n\t\t\tbutton->moveToLeft(\n\t\t\t\t(rowSize.width() - buttonSize.width()) / 2,\n\t\t\t\t(rowSize.height() - buttonSize.height()) / 2);\n\t\t}, row->lifetime());\n\t\tbutton->addClickHandler([=] {\n\t\t\tFile::OpenUrl(kUrl.utf8());\n\t\t});\n\t};\n\n\tconst auto &stLabel = st::aboutLabel;\n\tauto text1 = tr::lng_sponsored_info_description1_linked(\n\t\tlt_link,\n\t\trpl::combine(\n\t\t\ttr::lng_sponsored_info_description1_link(),\n\t\t\ttr::lng_sponsored_info_description1_url()\n\t\t) | rpl::map([](const QString &text, const QString &url) {\n\t\t\treturn tr::link(text, url);\n\t\t}),\n\t\ttr::rich);\n\tbox->addRow(object_ptr<FlatLabel>(box, std::move(text1), stLabel));\n\n\tbox->addSkip(st::sponsoredUrlButtonSkip);\n\taddUrl();\n\tbox->addSkip(st::sponsoredUrlButtonSkip);\n\n\tconst auto info2 = box->addRow(object_ptr<FlatLabel>(box, stLabel));\n\tinfo2->setText(tr::lng_sponsored_info_description2(tr::now));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/about_sponsored_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nclass GenericBox;\n\nvoid AboutSponsoredBox(not_null<Ui::GenericBox*> box);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/abstract_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/abstract_box.h\"\n\n#include \"core/application.h\"\n#include \"window/window_controller.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n\nnamespace Ui {\nnamespace internal {\n\nvoid showBox(\n\t\tobject_ptr<BoxContent> content,\n\t\tLayerOptions options,\n\t\tanim::type animated) {\n\tconst auto window = Core::IsAppLaunched()\n\t\t? Core::App().activePrimaryWindow()\n\t\t: nullptr;\n\tif (window) {\n\t\twindow->show(std::move(content), options, animated);\n\t}\n}\n\n} // namespace internal\n\nvoid hideLayer(anim::type animated) {\n\tconst auto window = Core::IsAppLaunched()\n\t\t? Core::App().activePrimaryWindow()\n\t\t: nullptr;\n\tif (window) {\n\t\twindow->hideLayer(animated);\n\t}\n}\n\nbool isLayerShown() {\n\tconst auto window = Core::IsAppLaunched()\n\t\t? Core::App().activePrimaryWindow()\n\t\t: nullptr;\n\treturn window && window->isLayerShown();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/abstract_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_layer_widget.h\"\n\nclass Painter;\n\nnamespace style {\nstruct RoundButton;\nstruct IconButton;\nstruct ScrollArea;\n} // namespace style\n\nnamespace Ui {\nclass RoundButton;\nclass IconButton;\nclass ScrollArea;\nclass FlatLabel;\nclass FadeShadow;\n} // namespace Ui\n\n// Legacy global method.\nnamespace Ui {\nnamespace internal {\n\nvoid showBox(\n\tobject_ptr<BoxContent> content,\n\tUi::LayerOptions options,\n\tanim::type animated);\n\n} // namespace internal\n\ntemplate <typename BoxType>\nbase::weak_qptr<BoxType> show(\n\t\tobject_ptr<BoxType> content,\n\t\tUi::LayerOptions options = Ui::LayerOption::CloseOther,\n\t\tanim::type animated = anim::type::normal) {\n\tauto result = base::weak_qptr<BoxType>(content.data());\n\tinternal::showBox(std::move(content), options, animated);\n\treturn result;\n}\n\nvoid hideLayer(anim::type animated = anim::type::normal);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/add_contact_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/add_contact_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"base/call_delayed.h\"\n#include \"base/random.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/abstract_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"boxes/peers/add_participants_box.h\"\n#include \"boxes/peers/edit_peer_common.h\"\n#include \"boxes/peers/edit_participant_box.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"countries/countries_instance.h\" // Countries::ExtractPhoneCode.\n#include \"history/history_item_reply_markup.h\"\n#include \"window/window_session_controller.h\"\n#include \"menu/menu_ttl.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/fields/special_fields.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"apiwrap.h\"\n#include \"api/api_invite_links.h\"\n#include \"api/api_peer_photo.h\"\n#include \"api/api_self_destruct.h\"\n#include \"main/main_session.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QClipboard>\n\nnamespace {\n\nbool IsValidPhone(QString phone) {\n\tphone = phone.replace(QRegularExpression(u\"[^\\\\d]\"_q), QString());\n\treturn (phone.length() >= 8)\n\t\t|| (phone == u\"333\"_q)\n\t\t|| (phone.startsWith(u\"42\"_q)\n\t\t\t&& (phone.length() == 2\n\t\t\t\t|| phone.length() == 5\n\t\t\t\t|| phone.length() == 6\n\t\t\t\t|| phone == u\"4242\"_q));\n}\n\nvoid ChatCreateDone(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tQImage image,\n\t\tTimeId ttlPeriod,\n\t\tconst MTPmessages_InvitedUsers &result,\n\t\tFn<void(not_null<PeerData*>)> done) {\n\tconst auto &data = result.data();\n\tnavigation->session().api().applyUpdates(data.vupdates());\n\n\tconst auto success = base::make_optional(&data.vupdates())\n\t\t| [](auto updates) -> std::optional<const QVector<MTPChat>*> {\n\t\t\tswitch (updates->type()) {\n\t\t\tcase mtpc_updates:\n\t\t\t\treturn &updates->c_updates().vchats().v;\n\t\t\tcase mtpc_updatesCombined:\n\t\t\t\treturn &updates->c_updatesCombined().vchats().v;\n\t\t\t}\n\t\t\tLOG((\"API Error: unexpected update cons %1 \"\n\t\t\t\t\"(GroupInfoBox::creationDone)\").arg(updates->type()));\n\t\t\treturn std::nullopt;\n\t\t}\n\t\t| [](auto chats) {\n\t\t\treturn (!chats->empty()\n\t\t\t\t&& chats->front().type() == mtpc_chat)\n\t\t\t\t? base::make_optional(chats)\n\t\t\t\t: std::nullopt;\n\t\t}\n\t\t| [&](auto chats) {\n\t\t\treturn navigation->session().data().chat(\n\t\t\t\tchats->front().c_chat().vid());\n\t\t}\n\t\t| [&](not_null<ChatData*> chat) {\n\t\t\tif (!image.isNull()) {\n\t\t\t\tchat->session().api().peerPhoto().upload(\n\t\t\t\t\tchat,\n\t\t\t\t\t{ std::move(image) });\n\t\t\t}\n\t\t\tif (ttlPeriod) {\n\t\t\t\tchat->setMessagesTTL(ttlPeriod);\n\t\t\t}\n\t\t\tif (done) {\n\t\t\t\tdone(chat);\n\t\t\t} else {\n\t\t\t\tconst auto show = navigation->uiShow();\n\t\t\t\tnavigation->showPeerHistory(chat);\n\t\t\t\tChatInviteForbidden(\n\t\t\t\t\tshow,\n\t\t\t\t\tchat,\n\t\t\t\t\tCollectForbiddenUsers(&chat->session(), result));\n\t\t\t\tchat->owner().addRecentJoinChat({\n\t\t\t\t\t.fromPeerId = chat->id,\n\t\t\t\t\t.joinedPeerId = chat->id,\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\tif (!success) {\n\t\tLOG((\"API Error: chat not found in updates \"\n\t\t\t\"(ContactsBox::creationDone)\"));\n\t}\n}\n\nvoid MustBePublicDestroy(not_null<ChannelData*> channel) {\n\tconst auto session = &channel->session();\n\tsession->api().request(MTPchannels_DeleteChannel(\n\t\tchannel->inputChannel()\n\t)).done([=](const MTPUpdates &result) {\n\t\tsession->api().applyUpdates(result);\n\t}).send();\n}\n\nvoid MustBePublicFailed(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChannelData*> channel) {\n\tconst auto text = channel->isMegagroup()\n\t\t? \"Can't create a public group :(\"\n\t\t: \"Can't create a public channel :(\";\n\tnavigation->showToast(text);\n\tMustBePublicDestroy(channel);\n}\n\n[[nodiscard]] Fn<void(not_null<PeerData*>)> WrapPeerDoneFromChannelDone(\n\t\tFn<void(not_null<ChannelData*>)> channelDone) {\n\tif (!channelDone) {\n\t\treturn nullptr;\n\t}\n\treturn [=](not_null<PeerData*> peer) {\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\tconst auto onstack = channelDone;\n\t\t\tonstack(channel);\n\t\t}\n\t};\n}\n\n} // namespace\n\nTextWithEntities PeerFloodErrorText(\n\t\tnot_null<Main::Session*> session,\n\t\tPeerFloodType type) {\n\tconst auto link = tr::link(\n\t\ttr::lng_cant_more_info(tr::now),\n\t\tsession->createInternalLinkFull(u\"spambot\"_q));\n\treturn ((type == PeerFloodType::InviteGroup)\n\t\t? tr::lng_cant_invite_not_contact\n\t\t: tr::lng_cant_send_to_not_contact)(\n\t\t\ttr::now,\n\t\t\tlt_more_info,\n\t\t\tlink,\n\t\t\ttr::marked);\n}\n\nvoid ShowAddParticipantsError(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tconst QString &error,\n\t\tnot_null<PeerData*> chat,\n\t\tnot_null<UserData*> user) {\n\tShowAddParticipantsError(\n\t\tstd::move(show),\n\t\terror,\n\t\tchat,\n\t\t{ .users = { 1, user } });\n}\n\nvoid ShowAddParticipantsError(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tconst QString &error,\n\t\tnot_null<PeerData*> chat,\n\t\tconst ForbiddenInvites &forbidden) {\n\tif (error == u\"USER_BOT\"_q) {\n\t\tconst auto channel = chat->asChannel();\n\t\tif ((forbidden.users.size() == 1)\n\t\t\t&& forbidden.users.front()->isBot()\n\t\t\t&& channel\n\t\t\t&& !channel->isMegagroup()\n\t\t\t&& channel->canAddAdmins()) {\n\t\t\tconst auto makeAdmin = [=](Fn<void()> close) {\n\t\t\t\tconst auto user = forbidden.users.front();\n\t\t\t\tconst auto weak = std::make_shared<base::weak_qptr<EditAdminBox>>();\n\t\t\t\tconst auto done = [=](auto&&...) {\n\t\t\t\t\tif (const auto strong = weak->get()) {\n\t\t\t\t\t\tstrong->uiShow()->showToast(\n\t\t\t\t\t\t\ttr::lng_box_done(tr::now));\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tconst auto fail = [=] {\n\t\t\t\t\tif (const auto strong = weak->get()) {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tconst auto saveCallback = SaveAdminCallback(\n\t\t\t\t\tshow,\n\t\t\t\t\tchannel,\n\t\t\t\t\tuser,\n\t\t\t\t\tdone,\n\t\t\t\t\tfail);\n\t\t\t\tauto box = Box<EditAdminBox>(\n\t\t\t\t\tchannel,\n\t\t\t\t\tuser,\n\t\t\t\t\tChatAdminRightsInfo(),\n\t\t\t\t\tQString(),\n\t\t\t\t\t0,\n\t\t\t\t\tnullptr);\n\t\t\t\tbox->setSaveCallback(saveCallback);\n\t\t\t\t*weak = box.data();\n\t\t\t\tshow->showBox(std::move(box));\n\t\t\t\tclose();\n\t\t\t};\n\t\t\tshow->showBox(\n\t\t\t\tUi::MakeConfirmBox({\n\t\t\t\t\t.text = tr::lng_cant_invite_offer_admin(),\n\t\t\t\t\t.confirmed = makeAdmin,\n\t\t\t\t\t.confirmText = tr::lng_cant_invite_make_admin(),\n\t\t\t\t}),\n\t\t\t\tUi::LayerOption::KeepOther);\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto hasBot = ranges::any_of(forbidden.users, &UserData::isBot);\n\tif (error == u\"PEER_FLOOD\"_q) {\n\t\tconst auto type = (chat->isChat() || chat->isMegagroup())\n\t\t\t? PeerFloodType::InviteGroup\n\t\t\t: PeerFloodType::InviteChannel;\n\t\tconst auto text = PeerFloodErrorText(&chat->session(), type);\n\t\tUi::show(Ui::MakeInformBox(text), Ui::LayerOption::KeepOther);\n\t\treturn;\n\t} else if (error == u\"USER_PRIVACY_RESTRICTED\"_q) {\n\t\tChatInviteForbidden(show, chat, forbidden);\n\t\treturn;\n\t}\n\tconst auto text = [&] {\n\t\tif (error == u\"USER_BOT\"_q) {\n\t\t\treturn tr::lng_cant_invite_bot_to_channel(tr::now);\n\t\t} else if (error == u\"USER_LEFT_CHAT\"_q) {\n\t\t\t// Trying to return a user who has left.\n\t\t} else if (error == u\"USER_KICKED\"_q) {\n\t\t\t// Trying to return a user who was kicked by admin.\n\t\t\treturn tr::lng_cant_invite_banned(tr::now);\n\t\t} else if (error == u\"USER_NOT_MUTUAL_CONTACT\"_q) {\n\t\t\t// Trying to return user who does not have me in contacts.\n\t\t\treturn tr::lng_failed_add_not_mutual(tr::now);\n\t\t} else if (error == u\"USER_ALREADY_PARTICIPANT\"_q && hasBot) {\n\t\t\treturn tr::lng_bot_already_in_group(tr::now);\n\t\t} else if (error == u\"BOT_GROUPS_BLOCKED\"_q) {\n\t\t\treturn tr::lng_error_cant_add_bot(tr::now);\n\t\t} else if (error == u\"YOU_BLOCKED_USER\"_q) {\n\t\t\treturn tr::lng_error_you_blocked_user(tr::now);\n\t\t} else if (error == u\"CHAT_ADMIN_INVITE_REQUIRED\"_q) {\n\t\t\treturn tr::lng_error_add_admin_not_member(tr::now);\n\t\t} else if (error == u\"USER_ADMIN_INVALID\"_q) {\n\t\t\treturn tr::lng_error_user_admin_invalid(tr::now);\n\t\t} else if (error == u\"BOTS_TOO_MUCH\"_q) {\n\t\t\treturn (chat->isChannel()\n\t\t\t\t? tr::lng_error_channel_bots_too_much\n\t\t\t\t: tr::lng_error_group_bots_too_much)(tr::now);\n\t\t}\n\t\treturn tr::lng_failed_add_participant(tr::now);\n\t}();\n\tshow->show(Ui::MakeInformBox(text), Ui::LayerOption::KeepOther);\n}\n\nAddContactBox::AddContactBox(\n\tQWidget*,\n\tnot_null<Main::Session*> session)\n: AddContactBox(nullptr, session, QString(), QString(), QString()) {\n}\n\nAddContactBox::AddContactBox(\n\tQWidget*,\n\tnot_null<Main::Session*> session,\n\tQString fname,\n\tQString lname,\n\tQString phone)\n: _session(session)\n, _first(this, st::defaultInputField, tr::lng_signup_firstname(), fname)\n, _last(this, st::defaultInputField, tr::lng_signup_lastname(), lname)\n, _phone(\n\tthis,\n\tst::defaultInputField,\n\ttr::lng_contact_phone(),\n\tCountries::ExtractPhoneCode(session->user()->phone()),\n\tphone,\n\t[](const QString &s) { return Countries::Groups(s); })\n, _invertOrder(langFirstNameGoesSecond()) {\n\tif (!phone.isEmpty()) {\n\t\t_phone->setDisabled(true);\n\t}\n}\n\nvoid AddContactBox::prepare() {\n\tif (_invertOrder) {\n\t\tsetTabOrder(_last, _first);\n\t}\n\tconst auto readyToAdd = !_phone->getLastText().isEmpty()\n\t\t&& (!_first->getLastText().isEmpty()\n\t\t\t|| !_last->getLastText().isEmpty());\n\tsetTitle(readyToAdd\n\t\t? tr::lng_confirm_contact_data()\n\t\t: tr::lng_enter_contact_data());\n\tupdateButtons();\n\n\tconst auto submitted = [=] { submit(); };\n\t_first->submits(\n\t) | rpl::on_next(submitted, _first->lifetime());\n\t_last->submits(\n\t) | rpl::on_next(submitted, _last->lifetime());\n\tconnect(_phone, &Ui::PhoneInput::submitted, [=] { submit(); });\n\n\tsetDimensions(\n\t\tst::boxWideWidth,\n\t\tst::contactPadding.top()\n\t\t\t+ _first->height()\n\t\t\t+ st::contactSkip\n\t\t\t+ _last->height()\n\t\t\t+ st::contactPhoneSkip\n\t\t\t+ _phone->height()\n\t\t\t+ st::contactPadding.bottom()\n\t\t\t+ st::boxPadding.bottom());\n}\n\nvoid AddContactBox::setInnerFocus() {\n\tif ((_first->getLastText().isEmpty() && _last->getLastText().isEmpty())\n\t\t|| !_phone->isEnabled()) {\n\t\t(_invertOrder ? _last : _first)->setFocusFast();\n\t\t_phone->finishAnimating();\n\t} else {\n\t\t_phone->setFocusFast();\n\t}\n}\n\nvoid AddContactBox::paintEvent(QPaintEvent *e) {\n\tBoxContent::paintEvent(e);\n\n\tauto p = QPainter(this);\n\tif (_retrying) {\n\t\tp.setPen(st::boxTextFg);\n\t\tp.setFont(st::boxTextFont);\n\t\tconst auto textHeight = height()\n\t\t\t- st::contactPadding.top()\n\t\t\t- st::contactPadding.bottom()\n\t\t\t- st::boxPadding.bottom();\n\t\tp.drawText(\n\t\t\tQRect(\n\t\t\t\tst::boxPadding.left(),\n\t\t\t\tst::contactPadding.top(),\n\t\t\t\twidth() - st::boxPadding.left() - st::boxPadding.right(),\n\t\t\t\ttextHeight),\n\t\t\ttr::lng_contact_not_joined(tr::now, lt_name, _sentName),\n\t\t\tstyle::al_topleft);\n\t} else {\n\t\tst::contactUserIcon.paint(\n\t\t\tp,\n\t\t\tst::boxPadding.left() + st::contactIconPosition.x(),\n\t\t\t_first->y() + st::contactIconPosition.y(),\n\t\t\twidth());\n\t\tst::contactPhoneIcon.paint(\n\t\t\tp,\n\t\t\tst::boxPadding.left() + st::contactIconPosition.x(),\n\t\t\t_phone->y() + st::contactIconPosition.y(),\n\t\t\twidth());\n\t}\n}\n\nvoid AddContactBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\t_first->resize(\n\t\twidth()\n\t\t\t- st::boxPadding.left()\n\t\t\t- st::contactPadding.left()\n\t\t\t- st::boxPadding.right(),\n\t\t_first->height());\n\t_last->resize(_first->width(), _last->height());\n\t_phone->resize(_first->width(), _last->height());\n\tconst auto left = st::boxPadding.left() + st::contactPadding.left();\n\tconst auto &firstRow = _invertOrder ? _last : _first;\n\tconst auto &secondRow = _invertOrder ? _first : _last;\n\tconst auto &thirdRow = _phone;\n\tfirstRow->moveToLeft(left, st::contactPadding.top());\n\tsecondRow->moveToLeft(\n\t\tleft,\n\t\tfirstRow->y() + firstRow->height() + st::contactSkip);\n\tthirdRow->moveToLeft(\n\t\tleft,\n\t\tsecondRow->y() + secondRow->height() + st::contactPhoneSkip);\n}\n\nvoid AddContactBox::submit() {\n\tif (_first->hasFocus()) {\n\t\t_last->setFocus();\n\t} else if (_last->hasFocus()) {\n\t\tif (_phone->isEnabled()) {\n\t\t\t_phone->setFocus();\n\t\t} else {\n\t\t\tsave();\n\t\t}\n\t} else if (_phone->hasFocus()) {\n\t\tsave();\n\t}\n}\n\nvoid AddContactBox::save() {\n\tif (_addRequest) {\n\t\treturn;\n\t}\n\n\tauto firstName = TextUtilities::PrepareForSending(\n\t\t_first->getLastText());\n\tauto lastName = TextUtilities::PrepareForSending(\n\t\t_last->getLastText());\n\tconst auto phone = _phone->getLastText().trimmed();\n\tif (firstName.isEmpty() && lastName.isEmpty()) {\n\t\tif (_invertOrder) {\n\t\t\t_last->setFocus();\n\t\t\t_last->showError();\n\t\t} else {\n\t\t\t_first->setFocus();\n\t\t\t_first->showError();\n\t\t}\n\t\treturn;\n\t} else if (!IsValidPhone(phone)) {\n\t\t_phone->setFocus();\n\t\t_phone->showError();\n\t\treturn;\n\t}\n\tif (firstName.isEmpty()) {\n\t\tfirstName = lastName;\n\t\tlastName = QString();\n\t}\n\tconst auto weak = base::make_weak(this);\n\tconst auto session = _session;\n\t_sentName = firstName;\n\t_contactId = base::RandomValue<uint64>();\n\t_addRequest = _session->api().request(MTPcontacts_ImportContacts(\n\t\tMTP_vector<MTPInputContact>(\n\t\t\t1,\n\t\t\tMTP_inputPhoneContact(\n\t\t\t\tMTP_flags(0),\n\t\t\t\tMTP_long(_contactId),\n\t\t\t\tMTP_string(phone),\n\t\t\t\tMTP_string(firstName),\n\t\t\t\tMTP_string(lastName),\n\t\t\t\tMTPTextWithEntities())) // note\n\t)).done(crl::guard(weak, [=](\n\t\t\tconst MTPcontacts_ImportedContacts &result) {\n\t\tconst auto &data = result.data();\n\t\tsession->data().processUsers(data.vusers());\n\t\tif (!weak) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto extractUser = [&](const MTPImportedContact &data) {\n\t\t\treturn data.match([&](const MTPDimportedContact &data) {\n\t\t\t\treturn (data.vclient_id().v == _contactId)\n\t\t\t\t\t? session->data().userLoaded(data.vuser_id())\n\t\t\t\t\t: nullptr;\n\t\t\t});\n\t\t};\n\t\tconst auto &list = data.vimported().v;\n\t\tconst auto user = list.isEmpty()\n\t\t\t? nullptr\n\t\t\t: extractUser(list.front());\n\t\tif (user) {\n\t\t\tif (user->isContact() || user->session().supportMode()) {\n\t\t\t\tif (const auto window = user->session().tryResolveWindow()) {\n\t\t\t\t\twindow->showPeerHistory(user);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (weak) { // showPeerHistory could close the box.\n\t\t\t\tgetDelegate()->hideLayer();\n\t\t\t}\n\t\t} else if (isBoxShown()) {\n\t\t\thideChildren();\n\t\t\t_retrying = true;\n\t\t\tupdateButtons();\n\t\t\tupdate();\n\t\t}\n\t})).send();\n}\n\nvoid AddContactBox::retry() {\n\t_addRequest = 0;\n\t_contactId = 0;\n\tshowChildren();\n\t_retrying = false;\n\tupdateButtons();\n\t_first->setText(QString());\n\t_last->setText(QString());\n\t_phone->clearText();\n\t_phone->setDisabled(false);\n\t_first->setFocus();\n\tupdate();\n}\n\nvoid AddContactBox::updateButtons() {\n\tclearButtons();\n\tif (_retrying) {\n\t\taddButton(tr::lng_try_other_contact(), [=] { retry(); });\n\t} else {\n\t\taddButton(tr::lng_add_contact(), [=] { save(); });\n\t\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\t}\n}\n\nGroupInfoBox::GroupInfoBox(\n\tQWidget*,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tType type,\n\tconst QString &title,\n\tFn<void(not_null<ChannelData*>)> channelDone)\n: _navigation(navigation)\n, _api(&_navigation->session().mtp())\n, _type(type)\n, _initialTitle(title)\n, _done(WrapPeerDoneFromChannelDone(std::move(channelDone))) {\n}\n\nGroupInfoBox::GroupInfoBox(\n\tQWidget*,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<UserData*> bot,\n\tRequestPeerQuery query,\n\tFn<void(not_null<PeerData*>)> done)\n: _navigation(navigation)\n, _api(&_navigation->session().mtp())\n, _type((query.type == RequestPeerQuery::Type::Broadcast)\n\t? Type::Channel\n\t: (query.groupIsForum == RequestPeerQuery::Restriction::Yes)\n\t? Type::Forum\n\t: (query.hasUsername == RequestPeerQuery::Restriction::Yes)\n\t? Type::Megagroup\n\t: Type::Group)\n, _mustBePublic(query.hasUsername == RequestPeerQuery::Restriction::Yes)\n, _canAddBot(query.isBotParticipant ? bot.get() : nullptr)\n, _done(std::move(done)) {\n}\n\nvoid GroupInfoBox::prepare() {\n\tsetMouseTracking(true);\n\n\t_photo.create(\n\t\tthis,\n\t\t&_navigation->parentController()->window(),\n\t\tUi::UserpicButton::Role::ChoosePhoto,\n\t\tst::defaultUserpicButton,\n\t\t(_type == Type::Forum) ? Ui::PeerUserpicShape::Forum : Ui::PeerUserpicShape::Auto);\n\t_photo->showCustomOnChosen();\n\t_title.create(\n\t\tthis,\n\t\tst::defaultInputField,\n\t\t(_type == Type::Channel\n\t\t\t? tr::lng_dlg_new_channel_name\n\t\t\t: tr::lng_dlg_new_group_name)(),\n\t\t_initialTitle);\n\t_title->setMaxLength(Ui::EditPeer::kMaxGroupChannelTitle);\n\t_title->setInstantReplaces(Ui::InstantReplaces::Default());\n\t_title->setInstantReplacesEnabled(\n\t\tCore::App().settings().replaceEmojiValue(),\n\t\tCore::App().settings().systemTextReplaceValue());\n\tUi::Emoji::SuggestionsController::Init(\n\t\tgetDelegate()->outerContainer(),\n\t\t_title,\n\t\t&_navigation->session());\n\n\tif (_type != Type::Group) {\n\t\t_description.create(\n\t\t\tthis,\n\t\t\tst::newGroupDescription,\n\t\t\tUi::InputField::Mode::MultiLine,\n\t\t\ttr::lng_create_group_description());\n\t\t_description->show();\n\t\t_description->setMaxLength(Ui::EditPeer::kMaxChannelDescription);\n\t\t_description->setInstantReplaces(Ui::InstantReplaces::Default());\n\t\t_description->setInstantReplacesEnabled(\n\t\t\tCore::App().settings().replaceEmojiValue(),\n\t\t\tCore::App().settings().systemTextReplaceValue());\n\t\t_description->setSubmitSettings(\n\t\t\tCore::App().settings().sendSubmitWay());\n\n\t\t_description->heightChanges(\n\t\t) | rpl::on_next([=] {\n\t\t\tdescriptionResized();\n\t\t}, _description->lifetime());\n\t\t_description->submits(\n\t\t) | rpl::on_next([=] { submit(); }, _description->lifetime());\n\t\t_description->cancelled(\n\t\t) | rpl::on_next([=] {\n\t\t\tcloseBox();\n\t\t}, _description->lifetime());\n\n\t\tUi::Emoji::SuggestionsController::Init(\n\t\t\tgetDelegate()->outerContainer(),\n\t\t\t_description,\n\t\t\t&_navigation->session());\n\t}\n\t_title->submits(\n\t) | rpl::on_next([=] { submitName(); }, _title->lifetime());\n\n\taddButton(\n\t\t((_type != Type::Group || _canAddBot)\n\t\t\t? tr::lng_create_group_create()\n\t\t\t: tr::lng_create_group_next()),\n\t\t[=] { submit(); });\n\taddButton(tr::lng_cancel(), [this] { closeBox(); });\n\n\tif (_type == Type::Group) {\n\t\t_navigation->session().api().selfDestruct().reload();\n\n\t\tconst auto top = addTopButton(st::infoTopBarMenu);\n\t\tconst auto menu\n\t\t\t= top->lifetime().make_state<base::unique_qptr<Ui::PopupMenu>>();\n\t\ttop->setClickedCallback([=] {\n\t\t\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\ttop,\n\t\t\t\tst::popupMenuWithIcons);\n\n\t\t\tconst auto ttl = ttlPeriod();\n\t\t\tconst auto text = tr::lng_manage_messages_ttl_menu(tr::now)\n\t\t\t\t+ (ttl ? ('\\t' + Ui::FormatTTLTiny(ttl)) : QString());\n\t\t\t(*menu)->addAction(\n\t\t\t\ttext,\n\t\t\t\t[=, show = uiShow()] {\n\t\t\t\t\tshow->showBox(Box(TTLMenu::TTLBox, TTLMenu::Args{\n\t\t\t\t\t\t.show = show,\n\t\t\t\t\t\t.startTtl = ttlPeriod(),\n\t\t\t\t\t\t.about = nullptr,\n\t\t\t\t\t\t.callback = crl::guard(this, [=](\n\t\t\t\t\t\t\t\tTimeId t,\n\t\t\t\t\t\t\t\tFn<void()> close) {\n\t\t\t\t\t\t\t_ttlPeriod = t;\n\t\t\t\t\t\t\t_ttlPeriodOverridden = true;\n\t\t\t\t\t\t\tclose();\n\t\t\t\t\t\t}),\n\t\t\t\t\t}));\n\t\t\t\t}, &st::menuIconTTL);\n\t\t\t(*menu)->popup(QCursor::pos());\n\t\t\treturn true;\n\t\t});\n\t}\n\n\tupdateMaxHeight();\n}\n\nvoid GroupInfoBox::setInnerFocus() {\n\t_title->setFocusFast();\n}\n\nvoid GroupInfoBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\t_photo->moveToLeft(\n\t\tst::boxPadding.left() + st::newGroupInfoPadding.left(),\n\t\tst::boxPadding.top() + st::newGroupInfoPadding.top());\n\n\tconst auto nameLeft = st::defaultUserpicButton.size.width()\n\t\t+ st::newGroupNamePosition.x();\n\t_title->resize(\n\t\twidth()\n\t\t\t- st::boxPadding.left()\n\t\t\t- st::newGroupInfoPadding.left()\n\t\t\t- st::boxPadding.right()\n\t\t\t- nameLeft,\n\t\t_title->height());\n\t_title->moveToLeft(\n\t\tst::boxPadding.left() + st::newGroupInfoPadding.left() + nameLeft,\n\t\tst::boxPadding.top()\n\t\t\t+ st::newGroupInfoPadding.top()\n\t\t\t+ st::newGroupNamePosition.y());\n\tif (_description) {\n\t\t_description->resize(\n\t\t\twidth()\n\t\t\t\t- st::boxPadding.left()\n\t\t\t\t- st::newGroupInfoPadding.left()\n\t\t\t\t- st::boxPadding.right(),\n\t\t\t_description->height());\n\t\tconst auto descriptionLeft = st::boxPadding.left()\n\t\t\t+ st::newGroupInfoPadding.left();\n\t\tconst auto descriptionTop = st::boxPadding.top()\n\t\t\t+ st::newGroupInfoPadding.top()\n\t\t\t+ st::defaultUserpicButton.size.height()\n\t\t\t+ st::newGroupDescriptionPadding.top();\n\t\t_description->moveToLeft(descriptionLeft, descriptionTop);\n\t}\n}\n\nvoid GroupInfoBox::submitName() {\n\tif (_title->getLastText().trimmed().isEmpty()) {\n\t\t_title->setFocus();\n\t\t_title->showError();\n\t} else if (_description) {\n\t\t_description->setFocus();\n\t} else {\n\t\tsubmit();\n\t}\n}\n\nTimeId GroupInfoBox::ttlPeriod() const {\n\treturn _ttlPeriodOverridden\n\t\t? _ttlPeriod\n\t\t: _navigation->session().api().selfDestruct()\n\t\t\t.periodDefaultHistoryTTLCurrent();\n}\n\nvoid GroupInfoBox::createGroup(\n\t\tbase::weak_qptr<Ui::BoxContent> selectUsersBox,\n\t\tconst QString &title,\n\t\tconst std::vector<not_null<PeerData*>> &users) {\n\tif (_creationRequestId) {\n\t\treturn;\n\t}\n\tusing TLUsers = MTPInputUser;\n\tauto inputs = QVector<TLUsers>();\n\tinputs.reserve(users.size());\n\tfor (auto peer : users) {\n\t\tauto user = peer->asUser();\n\t\tAssert(user != nullptr);\n\t\tif (!user->isSelf()) {\n\t\t\tinputs.push_back(user->inputUser());\n\t\t}\n\t}\n\t_creationRequestId = _api.request(MTPmessages_CreateChat(\n\t\tMTP_flags(MTPmessages_CreateChat::Flag::f_ttl_period),\n\t\tMTP_vector<TLUsers>(inputs),\n\t\tMTP_string(title),\n\t\tMTP_int(ttlPeriod())\n\t)).done([=](const MTPmessages_InvitedUsers &result) {\n\t\tauto image = _photo->takeResultImage();\n\t\tconst auto period = ttlPeriod();\n\t\tconst auto navigation = _navigation;\n\t\tconst auto done = _done;\n\n\t\tgetDelegate()->hideLayer(); // Destroys 'this'.\n\t\tChatCreateDone(navigation, std::move(image), period, result, done);\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto &type = error.type();\n\t\t_creationRequestId = 0;\n\t\tconst auto controller = _navigation->parentController();\n\t\tif (type == u\"NO_CHAT_TITLE\"_q) {\n\t\t\tconst auto weak = base::make_weak(this);\n\t\t\tif (const auto strong = selectUsersBox.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t\tif (weak) {\n\t\t\t\t_title->showError();\n\t\t\t}\n\t\t} else if (type == u\"USERS_TOO_FEW\"_q) {\n\t\t\tcontroller->show(\n\t\t\t\tUi::MakeInformBox(tr::lng_cant_invite_privacy()));\n\t\t} else if (type == u\"PEER_FLOOD\"_q) {\n\t\t\tcontroller->show(Ui::MakeInformBox(\n\t\t\t\tPeerFloodErrorText(\n\t\t\t\t\t&_navigation->session(),\n\t\t\t\t\tPeerFloodType::InviteGroup)));\n\t\t} else if (type == u\"USER_RESTRICTED\"_q) {\n\t\t\tcontroller->show(Ui::MakeInformBox(tr::lng_cant_do_this()));\n\t\t}\n\t}).send();\n}\n\nvoid GroupInfoBox::submit() {\n\tif (_creationRequestId || _creatingInviteLink) {\n\t\treturn;\n\t}\n\n\tauto title = TextUtilities::PrepareForSending(_title->getLastText());\n\tauto description = _description\n\t\t? TextUtilities::PrepareForSending(\n\t\t\t_description->getLastText(),\n\t\t\tTextUtilities::PrepareTextOption::CheckLinks)\n\t\t: QString();\n\tif (title.isEmpty()) {\n\t\t_title->setFocus();\n\t\t_title->showError();\n\t\treturn;\n\t}\n\tif (_type != Type::Group) {\n\t\tcreateChannel(title, description);\n\t} else if (_canAddBot) {\n\t\tcreateGroup(nullptr, title, { not_null<PeerData*>(_canAddBot) });\n\t} else {\n\t\tauto initBox = [title, weak = base::make_weak(this)](\n\t\t\t\tnot_null<PeerListBox*> box) {\n\t\t\tauto create = [box, title, weak] {\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->createGroup(\n\t\t\t\t\t\tbox.get(),\n\t\t\t\t\t\ttitle,\n\t\t\t\t\t\tbox->collectSelectedRows());\n\t\t\t\t}\n\t\t\t};\n\t\t\tbox->addButton(tr::lng_create_group_create(), std::move(create));\n\t\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t\t};\n\t\tUi::show(\n\t\t\tBox<PeerListBox>(\n\t\t\t\tstd::make_unique<AddParticipantsBoxController>(\n\t\t\t\t\t&_navigation->session()),\n\t\t\t\tstd::move(initBox)),\n\t\t\tUi::LayerOption::KeepOther);\n\t}\n}\n\nvoid GroupInfoBox::createChannel(\n\t\tconst QString &title,\n\t\tconst QString &description) {\n\tExpects(!_creationRequestId);\n\n\tusing Flag = MTPchannels_CreateChannel::Flag;\n\tconst auto flags = Flag()\n\t\t| ((_type == Type::Megagroup || _type == Type::Forum)\n\t\t\t? Flag::f_megagroup\n\t\t\t: Flag::f_broadcast)\n\t\t| ((_type == Type::Forum) ? Flag::f_forum : Flag())\n\t\t| ((_type == Type::Megagroup)\n\t\t\t? MTPchannels_CreateChannel::Flag::f_ttl_period\n\t\t\t: MTPchannels_CreateChannel::Flags(0));\n\tconst auto ttl = ttlPeriod();\n\t_creationRequestId = _api.request(MTPchannels_CreateChannel(\n\t\tMTP_flags(flags),\n\t\tMTP_string(title),\n\t\tMTP_string(description),\n\t\tMTPInputGeoPoint(), // geo_point\n\t\tMTPstring(), // address\n\t\tMTP_int((_type == Type::Megagroup) ? ttl : 0)\n\t)).done([=](const MTPUpdates &result) {\n\t\t_navigation->session().api().applyUpdates(result);\n\n\t\tconst auto success = base::make_optional(&result)\n\t\t\t| [](auto updates) -> std::optional<const QVector<MTPChat>*> {\n\t\t\t\tswitch (updates->type()) {\n\t\t\t\tcase mtpc_updates:\n\t\t\t\t\treturn &updates->c_updates().vchats().v;\n\t\t\t\tcase mtpc_updatesCombined:\n\t\t\t\t\treturn &updates->c_updatesCombined().vchats().v;\n\t\t\t\t}\n\t\t\t\tLOG((\"API Error: unexpected update cons %1 \"\n\t\t\t\t\t\"(GroupInfoBox::createChannel)\").arg(updates->type()));\n\t\t\t\treturn std::nullopt;\n\t\t\t}\n\t\t\t| [](auto chats) {\n\t\t\t\treturn (!chats->empty()\n\t\t\t\t\t\t&& chats->front().type() == mtpc_channel)\n\t\t\t\t\t? base::make_optional(chats)\n\t\t\t\t\t: std::nullopt;\n\t\t\t}\n\t\t\t| [&](auto chats) {\n\t\t\t\treturn _navigation->session().data().channel(\n\t\t\t\t\tchats->front().c_channel().vid());\n\t\t\t}\n\t\t\t| [&](not_null<ChannelData*> channel) {\n\t\t\t\tauto image = _photo->takeResultImage();\n\t\t\t\tif (!image.isNull()) {\n\t\t\t\t\tchannel->session().api().peerPhoto().upload(\n\t\t\t\t\t\tchannel,\n\t\t\t\t\t\t{ std::move(image) });\n\t\t\t\t}\n\t\t\t\tif (ttl && channel->isMegagroup()) {\n\t\t\t\t\tchannel->setMessagesTTL(ttl);\n\t\t\t\t}\n\t\t\t\tchannel->session().api().requestFullPeer(channel);\n\t\t\t\t_createdChannel = channel;\n\t\t\t\tcheckInviteLink();\n\t\t\t};\n\t\tif (!success) {\n\t\t\tLOG((\"API Error: channel not found in updates \"\n\t\t\t\t\"(GroupInfoBox::creationDone)\"));\n\t\t\tcloseBox();\n\t\t}\n\t}).fail([this](const MTP::Error &error) {\n\t\tconst auto &type = error.type();\n\t\t_creationRequestId = 0;\n\t\tconst auto controller = _navigation->parentController();\n\t\tif (type == u\"NO_CHAT_TITLE\"_q) {\n\t\t\t_title->setFocus();\n\t\t\t_title->showError();\n\t\t} else if (type == u\"USER_RESTRICTED\"_q) {\n\t\t\tcontroller->show(\n\t\t\t\tUi::MakeInformBox(tr::lng_cant_do_this()),\n\t\t\t\tUi::LayerOption::CloseOther);\n\t\t} else if (type == u\"CHANNELS_TOO_MUCH\"_q) {\n\t\t\tcontroller->show(\n\t\t\t\tBox(ChannelsLimitBox, &controller->session()),\n\t\t\t\tUi::LayerOption::CloseOther); // TODO\n\t\t}\n\t}).send();\n}\n\nvoid GroupInfoBox::checkInviteLink() {\n\tExpects(_createdChannel != nullptr);\n\n\tif (!_createdChannel->inviteLink().isEmpty()) {\n\t\tchannelReady();\n\t} else if (_createdChannel->isFullLoaded() && !_creatingInviteLink) {\n\t\t_creatingInviteLink = true;\n\t\t_createdChannel->session().api().inviteLinks().create({\n\t\t\t_createdChannel,\n\t\t\tcrl::guard(this, [=](auto&&) { channelReady(); }),\n\t\t});\n\t} else {\n\t\t_createdChannel->session().changes().peerUpdates(\n\t\t\t_createdChannel,\n\t\t\tData::PeerUpdate::Flag::FullInfo\n\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\tcheckInviteLink();\n\t\t}, lifetime());\n\t}\n}\n\nvoid GroupInfoBox::channelReady() {\n\tif (_done && !_mustBePublic) {\n\t\tconst auto callback = _done;\n\t\tconst auto argument = _createdChannel;\n\t\tcloseBox();\n\t\tcallback(argument);\n\t} else {\n\t\t_navigation->parentController()->show(\n\t\t\tBox<SetupChannelBox>(\n\t\t\t\t_navigation,\n\t\t\t\t_createdChannel,\n\t\t\t\t_mustBePublic,\n\t\t\t\t_done),\n\t\t\tUi::LayerOption::CloseOther);\n\t}\n}\n\nvoid GroupInfoBox::descriptionResized() {\n\tupdateMaxHeight();\n\tupdate();\n}\n\nvoid GroupInfoBox::updateMaxHeight() {\n\tauto newHeight = st::boxPadding.top()\n\t\t+ st::newGroupInfoPadding.top()\n\t\t+ st::defaultUserpicButton.size.height()\n\t\t+ st::boxPadding.bottom()\n\t\t+ st::newGroupInfoPadding.bottom();\n\tif (_description) {\n\t\tnewHeight += st::newGroupDescriptionPadding.top()\n\t\t\t+ _description->height()\n\t\t\t+ st::newGroupDescriptionPadding.bottom();\n\t}\n\tsetDimensions(st::boxWideWidth, newHeight);\n}\n\nSetupChannelBox::SetupChannelBox(\n\tQWidget*,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<ChannelData*> channel,\n\tbool mustBePublic,\n\tFn<void(not_null<PeerData*>)> done)\n: _navigation(navigation)\n, _channel(channel)\n, _api(&_channel->session().mtp())\n, _mustBePublic(mustBePublic)\n, _done(std::move(done))\n, _privacyGroup(\n\tstd::make_shared<Ui::RadioenumGroup<Privacy>>(Privacy::Public))\n, _public(\n\tthis,\n\t_privacyGroup,\n\tPrivacy::Public,\n\t(channel->isMegagroup()\n\t\t? tr::lng_create_public_group_title\n\t\t: tr::lng_create_public_channel_title)(tr::now),\n\tst::defaultBoxCheckbox)\n, _private(\n\tthis,\n\t_privacyGroup,\n\tPrivacy::Private,\n\t(channel->isMegagroup()\n\t\t? tr::lng_create_private_group_title\n\t\t: tr::lng_create_private_channel_title)(tr::now),\n\tst::defaultBoxCheckbox)\n, _aboutPublicWidth(st::boxWideWidth\n\t- st::boxPadding.left()\n\t- st::defaultBox.buttonPadding.right()\n\t- st::newGroupPadding.left()\n\t- st::defaultRadio.diameter\n\t- st::defaultBoxCheckbox.textPosition.x())\n, _aboutPublic(\n\tst::defaultTextStyle,\n\t(channel->isMegagroup()\n\t\t? tr::lng_create_public_group_about\n\t\t: tr::lng_create_public_channel_about)(tr::now),\n\tkDefaultTextOptions,\n\t_aboutPublicWidth)\n, _aboutPrivate(\n\tst::defaultTextStyle,\n\t(channel->isMegagroup()\n\t\t? tr::lng_create_private_group_about\n\t\t: tr::lng_create_private_channel_about)(tr::now),\n\tkDefaultTextOptions,\n\t_aboutPublicWidth)\n, _link(\n\tthis,\n\tst::setupChannelLink,\n\tnullptr,\n\tchannel->username(),\n\tchannel->session().createInternalLink(QString()))\n, _checkTimer([=] { check(); }) {\n\tif (_mustBePublic) {\n\t\t_public.destroy();\n\t\t_private.destroy();\n\t}\n}\n\nvoid SetupChannelBox::prepare() {\n\t_aboutPublicHeight = _aboutPublic.countHeight(_aboutPublicWidth);\n\n\tif (_channel->inviteLink().isEmpty()) {\n\t\t_channel->session().api().requestFullPeer(_channel);\n\t}\n\n\tsetMouseTracking(true);\n\n\t_checkRequestId = _api.request(MTPchannels_CheckUsername(\n\t\t_channel->inputChannel(),\n\t\tMTP_string(\"preston\")\n\t)).fail([=](const MTP::Error &error) {\n\t\t_checkRequestId = 0;\n\t\tfirstCheckFail(parseError(error.type()));\n\t}).send();\n\n\taddButton(tr::lng_settings_save(), [=] { save(); });\n\n\tconst auto cancel = [=] {\n\t\tif (_mustBePublic) {\n\t\t\tMustBePublicDestroy(_channel);\n\t\t}\n\t\tcloseBox();\n\t};\n\taddButton(\n\t\t_mustBePublic ? tr::lng_cancel() : tr::lng_create_group_skip(),\n\t\tcancel);\n\n\tconnect(_link, &Ui::MaskedInputField::changed, [=] { handleChange(); });\n\t_link->setVisible(_privacyGroup->current() == Privacy::Public);\n\n\t_privacyGroup->setChangedCallback([=](Privacy value) {\n\t\tprivacyChanged(value);\n\t});\n\n\t_channel->session().changes().peerUpdates(\n\t\t_channel,\n\t\tData::PeerUpdate::Flag::InviteLinks\n\t) | rpl::on_next([=] {\n\t\trtlupdate(_invitationLink);\n\t}, lifetime());\n\n\tboxClosing() | rpl::on_next([=] {\n\t\tif (!_mustBePublic) {\n\t\t\tAddParticipantsBoxController::Start(_navigation, _channel);\n\t\t}\n\t}, lifetime());\n\n\tupdateMaxHeight();\n}\n\nvoid SetupChannelBox::setInnerFocus() {\n\tif (!_link->isHidden()) {\n\t\t_link->setFocusFast();\n\t} else {\n\t\tBoxContent::setInnerFocus();\n\t}\n}\n\nvoid SetupChannelBox::updateMaxHeight() {\n\tauto newHeight = st::boxPadding.top()\n\t\t+ st::newGroupPadding.top()\n\t\t+ (_public\n\t\t\t? (_public->heightNoMargins()\n\t\t\t\t+ _aboutPublicHeight\n\t\t\t\t+ st::newGroupSkip)\n\t\t\t: 0)\n\t\t+ (_private\n\t\t\t? (_private->heightNoMargins()\n\t\t\t\t+ _aboutPrivate.countHeight(_aboutPublicWidth)\n\t\t\t\t+ st::newGroupSkip)\n\t\t\t: 0)\n\t\t+ st::newGroupPadding.bottom();\n\tif (!_channel->isMegagroup()\n\t\t|| _privacyGroup->current() == Privacy::Public) {\n\t\tnewHeight += st::newGroupLinkPadding.top()\n\t\t\t+ _link->height()\n\t\t\t+ st::newGroupLinkPadding.bottom();\n\t}\n\tsetDimensions(st::boxWideWidth, newHeight);\n}\n\nvoid SetupChannelBox::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {\n\t\tif (_link->hasFocus()) {\n\t\t\tif (_link->text().trimmed().isEmpty()) {\n\t\t\t\t_link->setFocus();\n\t\t\t\t_link->showError();\n\t\t\t} else {\n\t\t\t\tsave();\n\t\t\t}\n\t\t}\n\t} else {\n\t\tBoxContent::keyPressEvent(e);\n\t}\n}\n\nvoid SetupChannelBox::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tp.fillRect(e->rect(), st::boxBg);\n\tp.setPen(st::newGroupAboutFg);\n\n\tif (_public) {\n\t\tconst auto aboutPublic = QRect(\n\t\t\tst::boxPadding.left()\n\t\t\t\t+ st::newGroupPadding.left()\n\t\t\t\t+ st::defaultRadio.diameter\n\t\t\t\t+ st::defaultBoxCheckbox.textPosition.x(),\n\t\t\t_public->bottomNoMargins(),\n\t\t\t_aboutPublicWidth,\n\t\t\t_aboutPublicHeight);\n\t\t_aboutPublic.drawLeft(\n\t\t\tp,\n\t\t\taboutPublic.x(),\n\t\t\taboutPublic.y(),\n\t\t\taboutPublic.width(),\n\t\t\twidth());\n\t}\n\tif (_private) {\n\t\tconst auto aboutPrivate = QRect(\n\t\t\tst::boxPadding.left()\n\t\t\t\t+ st::newGroupPadding.left()\n\t\t\t\t+ st::defaultRadio.diameter\n\t\t\t\t+ st::defaultBoxCheckbox.textPosition.x(),\n\t\t\t_private->bottomNoMargins(),\n\t\t\t_aboutPublicWidth,\n\t\t\t_aboutPublicHeight);\n\t\t_aboutPrivate.drawLeft(\n\t\t\tp,\n\t\t\taboutPrivate.x(),\n\t\t\taboutPrivate.y(),\n\t\t\taboutPrivate.width(),\n\t\t\twidth());\n\t}\n\tif (!_channel->isMegagroup() || !_link->isHidden()) {\n\t\tp.setPen(st::boxTextFg);\n\t\tp.setFont(st::newGroupLinkFont);\n\t\tp.drawTextLeft(\n\t\t\tst::boxPadding.left()\n\t\t\t\t+ st::newGroupPadding.left()\n\t\t\t\t+ st::defaultInputField.textMargins.left(),\n\t\t\t_link->y() - st::newGroupLinkPadding.top() + st::newGroupLinkTop,\n\t\t\twidth(),\n\t\t\t(_link->isHidden()\n\t\t\t\t? tr::lng_create_group_invite_link\n\t\t\t\t: tr::lng_create_group_link)(tr::now));\n\t}\n\n\tif (_link->isHidden()) {\n\t\tif (!_channel->isMegagroup()) {\n\t\t\tQTextOption option(style::al_left);\n\t\t\toption.setWrapMode(QTextOption::WrapAnywhere);\n\t\t\tp.setFont(_linkOver\n\t\t\t\t? st::boxTextFont->underline()\n\t\t\t\t: st::boxTextFont);\n\t\t\tp.setPen(st::defaultLinkButton.color);\n\t\t\tconst auto inviteLinkText = _channel->inviteLink().isEmpty()\n\t\t\t\t? tr::lng_group_invite_create(tr::now)\n\t\t\t\t: _channel->inviteLink();\n\t\t\tp.drawText(_invitationLink, inviteLinkText, option);\n\t\t}\n\t} else {\n\t\tconst auto top = _link->y()\n\t\t\t- st::newGroupLinkPadding.top()\n\t\t\t+ st::newGroupLinkTop\n\t\t\t+ st::newGroupLinkFont->ascent\n\t\t\t- st::boxTextFont->ascent;\n\t\tif (!_errorText.isEmpty()) {\n\t\t\tp.setPen(st::boxTextFgError);\n\t\t\tp.setFont(st::boxTextFont);\n\t\t\tp.drawTextRight(st::boxPadding.right(), top, width(), _errorText);\n\t\t} else if (!_goodText.isEmpty()) {\n\t\t\tp.setPen(st::boxTextFgGood);\n\t\t\tp.setFont(st::boxTextFont);\n\t\t\tp.drawTextRight(st::boxPadding.right(), top, width(), _goodText);\n\t\t}\n\t}\n}\n\nvoid SetupChannelBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\tconst auto left = st::boxPadding.left() + st::newGroupPadding.left();\n\tif (_public && _private) {\n\t\t_public->moveToLeft(\n\t\t\tleft,\n\t\t\tst::boxPadding.top() + st::newGroupPadding.top());\n\t\t_private->moveToLeft(\n\t\t\tleft,\n\t\t\t_public->bottomNoMargins() + _aboutPublicHeight + st::newGroupSkip);\n\t}\n\t_link->resize(\n\t\twidth()\n\t\t\t- st::boxPadding.left()\n\t\t\t- st::newGroupLinkPadding.left()\n\t\t\t- st::boxPadding.right(),\n\t\t_link->height());\n\t_link->moveToLeft(\n\t\tst::boxPadding.left() + st::newGroupLinkPadding.left(),\n\t\t(st::boxPadding.top()\n\t\t\t+ st::newGroupPadding.top()\n\t\t\t+ (_public\n\t\t\t\t? (_public->heightNoMargins()\n\t\t\t\t\t+ _aboutPublicHeight\n\t\t\t\t\t+ st::newGroupSkip)\n\t\t\t\t: 0)\n\t\t\t+ (_private\n\t\t\t\t? (_private->heightNoMargins()\n\t\t\t\t\t+ _aboutPrivate.countHeight(_aboutPublicWidth)\n\t\t\t\t\t+ st::newGroupSkip)\n\t\t\t\t: 0)\n\t\t\t+ st::newGroupPadding.bottom()\n\t\t\t+ st::newGroupLinkPadding.top()));\n\t_invitationLink = QRect(\n\t\t_link->x(),\n\t\t_link->y() + (_link->height() / 2) - st::boxTextFont->height,\n\t\t_link->width(),\n\t\t2 * st::boxTextFont->height);\n}\n\nvoid SetupChannelBox::mouseMoveEvent(QMouseEvent *e) {\n\tupdateSelected(e->globalPos());\n}\n\nvoid SetupChannelBox::mousePressEvent(QMouseEvent *e) {\n\tif (!_linkOver) {\n\t\treturn;\n\t} else if (!_channel->inviteLink().isEmpty()) {\n\t\tQGuiApplication::clipboard()->setText(_channel->inviteLink());\n\t\tshowToast(tr::lng_create_channel_link_copied(tr::now));\n\t} else if (_channel->isFullLoaded() && !_creatingInviteLink) {\n\t\t_creatingInviteLink = true;\n\t\t_channel->session().api().inviteLinks().create({ _channel });\n\t}\n}\n\nvoid SetupChannelBox::leaveEventHook(QEvent *e) {\n\tupdateSelected(QCursor::pos());\n}\n\nvoid SetupChannelBox::updateSelected(const QPoint &cursorGlobalPosition) {\n\tQPoint p(mapFromGlobal(cursorGlobalPosition));\n\n\tbool linkOver = _invitationLink.contains(p);\n\tif (linkOver != _linkOver) {\n\t\t_linkOver = linkOver;\n\t\tupdate();\n\t\tsetCursor(_linkOver ? style::cur_pointer : style::cur_default);\n\t}\n}\n\nvoid SetupChannelBox::save() {\n\tconst auto saveUsername = [&](const QString &link) {\n\t\t_sentUsername = link;\n\t\t_saveRequestId = _api.request(MTPchannels_UpdateUsername(\n\t\t\t_channel->inputChannel(),\n\t\t\tMTP_string(_sentUsername)\n\t\t)).done([=] {\n\t\t\tconst auto done = _done;\n\t\t\tconst auto channel = _channel;\n\t\t\t_channel->setName(\n\t\t\t\tTextUtilities::SingleLine(_channel->name()),\n\t\t\t\t_sentUsername);\n\t\t\tcloseBox(); // Deletes `this`.\n\t\t\tif (done) {\n\t\t\t\tdone(channel);\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t_saveRequestId = 0;\n\t\t\tupdateFail(parseError(error.type()));\n\t\t}).send();\n\t};\n\tif (_saveRequestId) {\n\t\treturn;\n\t} else if (_privacyGroup->current() == Privacy::Private) {\n\t\tcloseBox();\n\t} else {\n\t\tconst auto link = _link->text().trimmed();\n\t\tif (link.isEmpty()) {\n\t\t\t_link->setFocus();\n\t\t\t_link->showError();\n\t\t\treturn;\n\t\t}\n\t\tsaveUsername(link);\n\t}\n}\n\nvoid SetupChannelBox::handleChange() {\n\tconst auto name = _link->text().trimmed();\n\tif (name.isEmpty()) {\n\t\tif (!_errorText.isEmpty() || !_goodText.isEmpty()) {\n\t\t\t_errorText = _goodText = QString();\n\t\t\tupdate();\n\t\t}\n\t\t_checkTimer.cancel();\n\t} else {\n\t\tconst auto len = int(name.size());\n\t\tfor (auto i = 0; i < len; ++i) {\n\t\t\tconst auto ch = name.at(i);\n\t\t\tif ((ch < 'A' || ch > 'Z')\n\t\t\t\t&& (ch < 'a' || ch > 'z')\n\t\t\t\t&& (ch < '0' || ch > '9')\n\t\t\t\t&& ch != '_') {\n\t\t\t\tconst auto badSymbols\n\t\t\t\t\t= tr::lng_create_channel_link_bad_symbols(tr::now);\n\t\t\t\tif (_errorText != badSymbols) {\n\t\t\t\t\t_errorText = badSymbols;\n\t\t\t\t\tupdate();\n\t\t\t\t}\n\t\t\t\t_checkTimer.cancel();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (name.size() < Ui::EditPeer::kMinUsernameLength) {\n\t\t\tconst auto tooShort\n\t\t\t\t= tr::lng_create_channel_link_too_short(tr::now);\n\t\t\tif (_errorText != tooShort) {\n\t\t\t\t_errorText = tooShort;\n\t\t\t\tupdate();\n\t\t\t}\n\t\t\t_checkTimer.cancel();\n\t\t} else {\n\t\t\tif (!_errorText.isEmpty() || !_goodText.isEmpty()) {\n\t\t\t\t_errorText = _goodText = QString();\n\t\t\t\tupdate();\n\t\t\t}\n\t\t\t_checkTimer.callOnce(Ui::EditPeer::kUsernameCheckTimeout);\n\t\t}\n\t}\n}\n\nvoid SetupChannelBox::check() {\n\tif (_checkRequestId) {\n\t\t_api.request(_checkRequestId).cancel();\n\t}\n\tconst auto link = _link->text().trimmed();\n\tif (link.size() >= Ui::EditPeer::kMinUsernameLength) {\n\t\t_checkUsername = link;\n\t\t_checkRequestId = _api.request(MTPchannels_CheckUsername(\n\t\t\t_channel->inputChannel(),\n\t\t\tMTP_string(link)\n\t\t)).done([=](const MTPBool &result) {\n\t\t\t_checkRequestId = 0;\n\t\t\t_errorText = (mtpIsTrue(result)\n\t\t\t\t\t|| _checkUsername == _channel->username())\n\t\t\t\t? QString()\n\t\t\t\t: tr::lng_create_channel_link_occupied(tr::now);\n\t\t\t_goodText = _errorText.isEmpty()\n\t\t\t\t? tr::lng_create_channel_link_available(tr::now)\n\t\t\t\t: QString();\n\t\t\tupdate();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t_checkRequestId = 0;\n\t\t\tcheckFail(parseError(error.type()));\n\t\t}).send();\n\t}\n}\n\nvoid SetupChannelBox::privacyChanged(Privacy value) {\n\tif (value == Privacy::Public) {\n\t\tif (_tooMuchUsernames) {\n\t\t\t_privacyGroup->setValue(Privacy::Private);\n\t\t\tconst auto callback = crl::guard(this, [=] {\n\t\t\t\t_tooMuchUsernames = false;\n\t\t\t\t_privacyGroup->setValue(Privacy::Public);\n\t\t\t\tcheck();\n\t\t\t});\n\t\t\tUi::show(\n\t\t\t\tBox(PublicLinksLimitBox, _navigation, callback),\n\t\t\t\tUi::LayerOption::KeepOther);\n\t\t\treturn;\n\t\t}\n\t\t_link->show();\n\t\t_link->setDisplayFocused(true);\n\t\t_link->setFocus();\n\t} else {\n\t\t_link->hide();\n\t\tsetFocus();\n\t}\n\tif (_channel->isMegagroup()) {\n\t\tupdateMaxHeight();\n\t}\n\tupdate();\n}\n\nSetupChannelBox::UsernameResult SetupChannelBox::parseError(\n\t\tconst QString &error) {\n\tif (error == u\"USERNAME_NOT_MODIFIED\"_q) {\n\t\treturn UsernameResult::Ok;\n\t} else if (error == u\"USERNAME_INVALID\"_q) {\n\t\treturn UsernameResult::Invalid;\n\t} else if (error == u\"USERNAME_OCCUPIED\"_q) {\n\t\treturn UsernameResult::Occupied;\n\t} else if (error == u\"USERNAME_PURCHASE_AVAILABLE\"_q) {\n\t\treturn UsernameResult::Occupied;\n\t} else if (error == u\"USERNAMES_UNAVAILABLE\"_q) {\n\t\treturn UsernameResult::Occupied;\n\t} else if (error == u\"CHANNEL_PUBLIC_GROUP_NA\"_q) {\n\t\treturn UsernameResult::NA;\n\t} else if (error == u\"CHANNELS_ADMIN_PUBLIC_TOO_MUCH\"_q) {\n\t\treturn UsernameResult::ChatsTooMuch;\n\t} else {\n\t\treturn UsernameResult::Unknown;\n\t}\n}\n\nvoid SetupChannelBox::updateFail(UsernameResult result) {\n\tif ((result == UsernameResult::Ok)\n\t\t|| (_sentUsername == _channel->username())) {\n\t\t_channel->setName(\n\t\t\tTextUtilities::SingleLine(_channel->name()),\n\t\t\tTextUtilities::SingleLine(_sentUsername));\n\t\tcloseBox();\n\t} else if (result == UsernameResult::Invalid) {\n\t\t_link->setFocus();\n\t\t_link->showError();\n\t\t_errorText = tr::lng_create_channel_link_invalid(tr::now);\n\t\tupdate();\n\t} else if (result == UsernameResult::Occupied) {\n\t\t_link->setFocus();\n\t\t_link->showError();\n\t\t_errorText = tr::lng_create_channel_link_occupied(tr::now);\n\t\tupdate();\n\t} else {\n\t\t_link->setFocus();\n\t}\n}\n\nvoid SetupChannelBox::checkFail(UsernameResult result) {\n\tif (result == UsernameResult::NA) {\n\t\tif (_mustBePublic) {\n\t\t\tmustBePublicFailed();\n\t\t}\n\t\tgetDelegate()->hideLayer();\n\t} else if (result == UsernameResult::ChatsTooMuch) {\n\t\tif (_mustBePublic) {\n\t\t\tshowRevokePublicLinkBoxForEdit();\n\t\t} else {\n\t\t\t_tooMuchUsernames = true;\n\t\t\t_privacyGroup->setValue(Privacy::Private);\n\t\t}\n\t} else if (result == UsernameResult::Invalid) {\n\t\t_errorText = tr::lng_create_channel_link_invalid(tr::now);\n\t\tupdate();\n\t} else if ((result == UsernameResult::Occupied)\n\t\t\t&& _checkUsername != _channel->username()) {\n\t\t_errorText = tr::lng_create_channel_link_occupied(tr::now);\n\t\tupdate();\n\t} else {\n\t\t_goodText = QString();\n\t\t_link->setFocus();\n\t}\n}\n\nvoid SetupChannelBox::showRevokePublicLinkBoxForEdit() {\n\tconst auto channel = _channel;\n\tconst auto mustBePublic = _mustBePublic;\n\tconst auto done = _done;\n\tconst auto navigation = _navigation;\n\tconst auto revoked = std::make_shared<bool>(false);\n\tconst auto callback = [=] {\n\t\t*revoked = true;\n\t\tnavigation->parentController()->show(\n\t\t\tBox<SetupChannelBox>(navigation, channel, mustBePublic, done));\n\t};\n\tconst auto revoker = navigation->parentController()->show(\n\t\tBox(PublicLinksLimitBox, navigation, callback));\n\tconst auto session = &navigation->session();\n\trevoker->boxClosing(\n\t) | rpl::on_next(crl::guard(session, [=] {\n\t\tbase::call_delayed(200, session, [=] {\n\t\t\tif (*revoked) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tMustBePublicDestroy(channel);\n\t\t});\n\t}), revoker->lifetime());\n\tcloseBox();\n}\n\nvoid SetupChannelBox::mustBePublicFailed() {\n\tMustBePublicFailed(_navigation, _channel);\n}\n\nvoid SetupChannelBox::firstCheckFail(UsernameResult result) {\n\tif (result == UsernameResult::NA) {\n\t\tif (_mustBePublic) {\n\t\t\tmustBePublicFailed();\n\t\t}\n\t\tgetDelegate()->hideLayer();\n\t} else if (result == UsernameResult::ChatsTooMuch) {\n\t\tif (_mustBePublic) {\n\t\t\tshowRevokePublicLinkBoxForEdit();\n\t\t} else {\n\t\t\t_tooMuchUsernames = true;\n\t\t\t_privacyGroup->setValue(Privacy::Private);\n\t\t}\n\t} else {\n\t\t_goodText = QString();\n\t\t_link->setFocus();\n\t}\n}\n\nEditNameBox::EditNameBox(\n\tQWidget*,\n\tnot_null<UserData*> user,\n\tFocus focus)\n: _user(user)\n, _api(&_user->session().mtp())\n, _first(\n\tthis,\n\tst::defaultInputField,\n\ttr::lng_signup_firstname(),\n\t_user->firstName)\n, _last(\n\tthis,\n\tst::defaultInputField,\n\ttr::lng_signup_lastname(),\n\t_user->lastName)\n, _invertOrder(langFirstNameGoesSecond())\n, _focus(focus) {\n}\n\nvoid EditNameBox::prepare() {\n\tauto newHeight = st::contactPadding.top() + _first->height();\n\n\tsetTitle(tr::lng_edit_self_title());\n\tnewHeight += st::contactSkip + _last->height();\n\n\tnewHeight += st::boxPadding.bottom() + st::contactPadding.bottom();\n\tsetDimensions(st::boxWidth, newHeight);\n\n\taddButton(tr::lng_settings_save(), [=] { save(); });\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\tif (_invertOrder) {\n\t\tsetTabOrder(_last, _first);\n\t}\n\t_first->setMaxLength(Ui::EditPeer::kMaxUserFirstLastName);\n\t_last->setMaxLength(Ui::EditPeer::kMaxUserFirstLastName);\n\n\t_first->submits(\n\t) | rpl::on_next([=] { submit(); }, _first->lifetime());\n\t_last->submits(\n\t) | rpl::on_next([=] { submit(); }, _last->lifetime());\n\n\t_first->tabbed(\n\t) | rpl::on_next([=](not_null<bool*> handled) {\n\t\t_last->setFocus();\n\t\t*handled = true;\n\t}, _first->lifetime());\n\t_last->tabbed(\n\t) | rpl::on_next([=](not_null<bool*> handled) {\n\t\t_first->setFocus();\n\t\t*handled = true;\n\t}, _last->lifetime());\n}\n\nvoid EditNameBox::setInnerFocus() {\n\tconst auto focusLast = (_focus == Focus::LastName)\n\t\t|| (_focus == Focus::FirstName && _invertOrder);\n\t(focusLast ? _last : _first)->setFocusFast();\n}\n\nvoid EditNameBox::submit() {\n\tif (_first->hasFocus()) {\n\t\t_last->setFocus();\n\t} else if (_last->hasFocus()) {\n\t\tif (_first->getLastText().trimmed().isEmpty()) {\n\t\t\t_first->setFocus();\n\t\t\t_first->showError();\n\t\t} else if (_last->getLastText().trimmed().isEmpty()) {\n\t\t\t_last->setFocus();\n\t\t\t_last->showError();\n\t\t} else {\n\t\t\tsave();\n\t\t}\n\t}\n}\n\nvoid EditNameBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\t_first->resize(\n\t\twidth()\n\t\t\t- st::boxPadding.left()\n\t\t\t- st::newGroupInfoPadding.left()\n\t\t\t- st::boxPadding.right(),\n\t\t_first->height());\n\t_last->resize(_first->size());\n\tconst auto left = st::boxPadding.left() + st::newGroupInfoPadding.left();\n\tconst auto skip = st::contactSkip;\n\tif (_invertOrder) {\n\t\t_last->moveToLeft(left, st::contactPadding.top());\n\t\t_first->moveToLeft(left, _last->y() + _last->height() + skip);\n\t} else {\n\t\t_first->moveToLeft(left, st::contactPadding.top());\n\t\t_last->moveToLeft(left, _first->y() + _first->height() + skip);\n\t}\n}\n\nvoid EditNameBox::save() {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\n\tauto first = TextUtilities::PrepareForSending(_first->getLastText());\n\tauto last = TextUtilities::PrepareForSending(_last->getLastText());\n\tif (first.isEmpty() && last.isEmpty()) {\n\t\tif (_invertOrder) {\n\t\t\t_last->setFocus();\n\t\t\t_last->showError();\n\t\t} else {\n\t\t\t_first->setFocus();\n\t\t\t_first->showError();\n\t\t}\n\t\treturn;\n\t}\n\tif (first.isEmpty()) {\n\t\tfirst = last;\n\t\tlast = QString();\n\t}\n\t_sentName = first;\n\tauto flags = MTPaccount_UpdateProfile::Flag::f_first_name\n\t\t| MTPaccount_UpdateProfile::Flag::f_last_name;\n\t_requestId = _api.request(MTPaccount_UpdateProfile(\n\t\tMTP_flags(flags),\n\t\tMTP_string(first),\n\t\tMTP_string(last),\n\t\tMTPstring()\n\t)).done([=](const MTPUser &user) {\n\t\t_user->owner().processUser(user);\n\t\tcloseBox();\n\t}).fail([=](const MTP::Error &error) {\n\t\t_requestId = 0;\n\t\tsaveSelfFail(error.type());\n\t}).send();\n}\n\nvoid EditNameBox::saveSelfFail(const QString &error) {\n\tif (error == \"NAME_NOT_MODIFIED\") {\n\t\t_user->setName(\n\t\t\tTextUtilities::SingleLine(_first->getLastText().trimmed()),\n\t\t\tTextUtilities::SingleLine(_last->getLastText().trimmed()),\n\t\t\tQString(),\n\t\t\tTextUtilities::SingleLine(_user->username()));\n\t\tcloseBox();\n\t} else if (error == \"FIRSTNAME_INVALID\") {\n\t\t_first->setFocus();\n\t\t_first->showError();\n\t} else if (error == \"LASTNAME_INVALID\") {\n\t\t_last->setFocus();\n\t\t_last->showError();\n\t} else {\n\t\t_first->setFocus();\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/add_contact_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n#include \"base/timer.h\"\n#include \"mtproto/sender.h\"\n\nclass PeerListBox;\nstruct RequestPeerQuery;\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass FlatLabel;\nclass InputField;\nclass PhoneInput;\nclass UsernameInput;\nclass Checkbox;\ntemplate <typename Enum>\nclass RadioenumGroup;\ntemplate <typename Enum>\nclass Radioenum;\nclass LinkButton;\nclass UserpicButton;\nclass Show;\n} // namespace Ui\n\nenum class PeerFloodType {\n\tSend,\n\tInviteGroup,\n\tInviteChannel,\n};\n\nstruct ForbiddenInvites;\n\n[[nodiscard]] TextWithEntities PeerFloodErrorText(\n\tnot_null<Main::Session*> session,\n\tPeerFloodType type);\nvoid ShowAddParticipantsError(\n\tstd::shared_ptr<Ui::Show> show,\n\tconst QString &error,\n\tnot_null<PeerData*> chat,\n\tconst ForbiddenInvites &forbidden);\nvoid ShowAddParticipantsError(\n\tstd::shared_ptr<Ui::Show> show,\n\tconst QString &error,\n\tnot_null<PeerData*> chat,\n\tnot_null<UserData*> user);\n\nclass AddContactBox : public Ui::BoxContent {\npublic:\n\tAddContactBox(QWidget*, not_null<Main::Session*> session);\n\tAddContactBox(\n\t\tQWidget*,\n\t\tnot_null<Main::Session*> session,\n\t\tQString fname,\n\t\tQString lname,\n\t\tQString phone);\n\nprotected:\n\tvoid prepare() override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\tvoid setInnerFocus() override;\n\nprivate:\n\tvoid submit();\n\tvoid retry();\n\tvoid save();\n\tvoid updateButtons();\n\n\tconst not_null<Main::Session*> _session;\n\n\tobject_ptr<Ui::InputField> _first;\n\tobject_ptr<Ui::InputField> _last;\n\tobject_ptr<Ui::PhoneInput> _phone;\n\n\tbool _retrying = false;\n\tbool _invertOrder = false;\n\n\tuint64 _contactId = 0;\n\n\tmtpRequestId _addRequest = 0;\n\tQString _sentName;\n\n};\n\nclass GroupInfoBox : public Ui::BoxContent {\npublic:\n\tenum class Type {\n\t\tGroup,\n\t\tChannel,\n\t\tMegagroup,\n\t\tForum,\n\t};\n\tGroupInfoBox(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tType type,\n\t\tconst QString &title = QString(),\n\t\tFn<void(not_null<ChannelData*>)> channelDone = nullptr);\n\tGroupInfoBox(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<UserData*> bot,\n\t\tRequestPeerQuery query,\n\t\tFn<void(not_null<PeerData*>)> done);\n\nprotected:\n\tvoid prepare() override;\n\tvoid setInnerFocus() override;\n\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid createChannel(const QString &title, const QString &description);\n\tvoid createGroup(\n\t\tbase::weak_qptr<Ui::BoxContent> selectUsersBox,\n\t\tconst QString &title,\n\t\tconst std::vector<not_null<PeerData*>> &users);\n\tvoid submitName();\n\tvoid submit();\n\tvoid checkInviteLink();\n\tvoid channelReady();\n\n\tvoid descriptionResized();\n\tvoid updateMaxHeight();\n\n\t[[nodiscard]] TimeId ttlPeriod() const;\n\n\tconst not_null<Window::SessionNavigation*> _navigation;\n\tMTP::Sender _api;\n\n\tType _type = Type::Group;\n\tQString _initialTitle;\n\tbool _mustBePublic = false;\n\tUserData *_canAddBot = nullptr;\n\tFn<void(not_null<PeerData*>)> _done;\n\n\tobject_ptr<Ui::UserpicButton> _photo = { nullptr };\n\tobject_ptr<Ui::InputField> _title = { nullptr };\n\tobject_ptr<Ui::InputField> _description = { nullptr };\n\n\t// group / channel creation\n\tmtpRequestId _creationRequestId = 0;\n\tbool _creatingInviteLink = false;\n\tChannelData *_createdChannel = nullptr;\n\tTimeId _ttlPeriod = 0;\n\tbool _ttlPeriodOverridden = false;\n\n};\n\nclass SetupChannelBox final : public Ui::BoxContent {\npublic:\n\tSetupChannelBox(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChannelData*> channel,\n\t\tbool mustBePublic,\n\t\tFn<void(not_null<PeerData*>)> done);\n\n\tvoid setInnerFocus() override;\n\nprotected:\n\tvoid prepare() override;\n\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\nprivate:\n\tenum class Privacy {\n\t\tPublic,\n\t\tPrivate,\n\t};\n\tenum class UsernameResult {\n\t\tOk,\n\t\tInvalid,\n\t\tOccupied,\n\t\tChatsTooMuch,\n\t\tNA,\n\t\tUnknown,\n\t};\n\t[[nodiscard]] UsernameResult parseError(const QString &error);\n\n\tvoid privacyChanged(Privacy value);\n\tvoid updateSelected(const QPoint &cursorGlobalPosition);\n\tvoid handleChange();\n\tvoid check();\n\tvoid save();\n\n\tvoid updateFail(UsernameResult result);\n\n\tvoid mustBePublicFailed();\n\tvoid checkFail(UsernameResult result);\n\tvoid firstCheckFail(UsernameResult result);\n\n\tvoid updateMaxHeight();\n\n\tvoid showRevokePublicLinkBoxForEdit();\n\n\tconst not_null<Window::SessionNavigation*> _navigation;\n\tconst not_null<ChannelData*> _channel;\n\tMTP::Sender _api;\n\n\tbool _creatingInviteLink = false;\n\tbool _mustBePublic = false;\n\tFn<void(not_null<PeerData*>)> _done;\n\n\tstd::shared_ptr<Ui::RadioenumGroup<Privacy>> _privacyGroup;\n\tobject_ptr<Ui::Radioenum<Privacy>> _public;\n\tobject_ptr<Ui::Radioenum<Privacy>> _private;\n\tint32 _aboutPublicWidth, _aboutPublicHeight;\n\tUi::Text::String _aboutPublic, _aboutPrivate;\n\n\tobject_ptr<Ui::UsernameInput> _link;\n\n\tQRect _invitationLink;\n\tbool _linkOver = false;\n\tbool _tooMuchUsernames = false;\n\n\tmtpRequestId _saveRequestId = 0;\n\tmtpRequestId _checkRequestId = 0;\n\tQString _sentUsername, _checkUsername, _errorText, _goodText;\n\n\tbase::Timer _checkTimer;\n\n};\n\nclass EditNameBox : public Ui::BoxContent {\npublic:\n\tenum class Focus {\n\t\tFirstName,\n\t\tLastName,\n\t};\n\tEditNameBox(\n\t\tQWidget*,\n\t\tnot_null<UserData*> user,\n\t\tFocus focus = Focus::FirstName);\n\nprotected:\n\tvoid setInnerFocus() override;\n\tvoid prepare() override;\n\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid submit();\n\tvoid save();\n\tvoid saveSelfFail(const QString &error);\n\n\tconst not_null<UserData*> _user;\n\tMTP::Sender _api;\n\n\tobject_ptr<Ui::InputField> _first;\n\tobject_ptr<Ui::InputField> _last;\n\n\tbool _invertOrder = false;\n\tFocus _focus = Focus::FirstName;\n\n\tmtpRequestId _requestId = 0;\n\tQString _sentName;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/auto_download_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/auto_download_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"data/data_session.h\"\n#include \"data/data_auto_download.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/wrap.h\"\n#include \"storage/localstorage.h\"\n#include \"settings/settings_common.h\"\n#include \"export/view/export_view_settings.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_settings.h\"\n\nnamespace {\n\nconstexpr auto kMegabyte = 1024 * 1024;\nconstexpr auto kDefaultDownloadLimit = 10 * kMegabyte;\nconstexpr auto kDefaultAutoPlayLimit = 50 * kMegabyte;\n\nusing Type = Data::AutoDownload::Type;\n\nnot_null<int64*> AddSizeLimitSlider(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst base::flat_map<Type, int64> &values,\n\t\tint64 defaultValue) {\n\tusing Pair = base::flat_map<Type, int64>::value_type;\n\n\tconst auto limits = Ui::CreateChild<rpl::event_stream<int64>>(\n\t\tcontainer.get());\n\tconst auto currentLimit = ranges::max_element(\n\t\tvalues,\n\t\tstd::less<>(),\n\t\t[](Pair pair) { return pair.second; })->second;\n\tconst auto startLimit = currentLimit ? currentLimit : defaultValue;\n\tconst auto result = Ui::CreateChild<int64>(container.get(), startLimit);\n\tSettings::AddButtonWithLabel(\n\t\tcontainer,\n\t\ttr::lng_media_size_limit(),\n\t\tlimits->events_starting_with_copy(\n\t\t\tstartLimit\n\t\t) | rpl::map([](int64 value) {\n\t\t\treturn tr::lng_media_size_up_to(\n\t\t\t\ttr::now,\n\t\t\t\tlt_size,\n\t\t\t\tQString::number(value / kMegabyte) + \" MB\");\n\t\t}),\n\t\tst::autoDownloadLimitButton\n\t)->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tconst auto slider = container->add(\n\t\tobject_ptr<Ui::MediaSlider>(container, st::autoDownloadLimitSlider),\n\t\tst::autoDownloadLimitPadding);\n\tslider->resize(st::autoDownloadLimitSlider.seekSize);\n\tslider->setPseudoDiscrete(\n\t\tExport::View::kSizeValueCount,\n\t\tExport::View::SizeLimitByIndex,\n\t\t*result,\n\t\t[=](int64 value) {\n\t\t\t*result = value;\n\t\t\tlimits->fire_copy(value);\n\t\t});\n\treturn result;\n}\n} // namespace\n\nAutoDownloadBox::AutoDownloadBox(\n\tQWidget*,\n\tnot_null<Main::Session*> session,\n\tData::AutoDownload::Source source)\n: _session(session)\n, _source(source) {\n}\n\nvoid AutoDownloadBox::prepare() {\n\tsetupContent();\n}\n\nvoid AutoDownloadBox::setupContent() {\n\tusing namespace rpl::mappers;\n\tusing namespace Settings;\n\tusing namespace Data::AutoDownload;\n\tusing Type = Data::AutoDownload::Type;\n\tusing Pair = base::flat_map<Type, int64>::value_type;\n\n\tsetTitle(tr::lng_profile_settings_section());\n\n\tconst auto settings = &_session->settings().autoDownload();\n\n\tauto wrap = object_ptr<Ui::VerticalLayout>(this);\n\tconst auto content = wrap.data();\n\tsetInnerWidget(object_ptr<Ui::OverrideMargins>(\n\t\tthis,\n\t\tstd::move(wrap)));\n\n\tconst auto add = [&](\n\t\t\tnot_null<base::flat_map<Type, int64>*> values,\n\t\t\tType type,\n\t\t\trpl::producer<QString> label) {\n\t\tconst auto value = settings->bytesLimit(_source, type);\n\t\tcontent->add(object_ptr<Ui::SettingsButton>(\n\t\t\tcontent,\n\t\t\tstd::move(label),\n\t\t\tst::settingsButtonNoIcon\n\t\t))->toggleOn(\n\t\t\trpl::single(value > 0)\n\t\t)->toggledChanges(\n\t\t) | rpl::on_next([=](bool enabled) {\n\t\t\t(*values)[type] = enabled ? 1 : 0;\n\t\t}, content->lifetime());\n\t\tvalues->emplace(type, value);\n\t};\n\n\tAddSubsectionTitle(content, tr::lng_media_auto_title());\n\n\tconst auto downloadValues = Ui::CreateChild<base::flat_map<Type, int64>>(\n\t\tcontent);\n\tadd(downloadValues, Type::Photo, tr::lng_media_photo_title());\n\tadd(downloadValues, Type::File, tr::lng_media_file_title());\n\n\tconst auto downloadLimit = AddSizeLimitSlider(\n\t\tcontent,\n\t\t*downloadValues,\n\t\tkDefaultDownloadLimit);\n\n\tAddSkip(content);\n\tAddSubsectionTitle(content, tr::lng_media_auto_play());\n\n\tconst auto autoPlayValues = Ui::CreateChild<base::flat_map<Type, int64>>(\n\t\tcontent);\n\tadd(\n\t\tautoPlayValues,\n\t\tType::AutoPlayVideoMessage,\n\t\ttr::lng_media_video_messages_title());\n\tadd(autoPlayValues, Type::AutoPlayVideo, tr::lng_media_video_title());\n\tadd(autoPlayValues, Type::AutoPlayGIF, tr::lng_media_animation_title());\n\n\tconst auto autoPlayLimit = AddSizeLimitSlider(\n\t\tcontent,\n\t\t*autoPlayValues,\n\t\tkDefaultAutoPlayLimit);\n\n\tconst auto limitByType = [=](Type type) {\n\t\treturn (ranges::find(kAutoPlayTypes, type) != end(kAutoPlayTypes))\n\t\t\t? *autoPlayLimit\n\t\t\t: *downloadLimit;\n\t};\n\n\taddButton(tr::lng_connection_save(), [=] {\n\t\tauto &&values = ranges::views::concat(\n\t\t\t*downloadValues,\n\t\t\t*autoPlayValues);\n\t\tauto allowMore = values | ranges::views::filter([&](Pair pair) {\n\t\t\tconst auto &[type, enabled] = pair;\n\t\t\tconst auto value = enabled ? limitByType(type) : 0;\n\t\t\tconst auto old = settings->bytesLimit(_source, type);\n\t\t\treturn (old < value);\n\t\t}) | ranges::views::transform([](Pair pair) {\n\t\t\treturn pair.first;\n\t\t});\n\t\tconst auto less = ranges::any_of(*autoPlayValues, [&](Pair pair) {\n\t\t\tconst auto &[type, enabled] = pair;\n\t\t\tconst auto value = enabled ? limitByType(type) : 0;\n\t\t\treturn value < settings->bytesLimit(_source, type);\n\t\t});\n\t\tconst auto allowMoreTypes = base::flat_set<Type>(\n\t\t\tallowMore.begin(),\n\t\t\tallowMore.end());\n\n\t\tconst auto changed = ranges::any_of(values, [&](Pair pair) {\n\t\t\tconst auto &[type, enabled] = pair;\n\t\t\tconst auto value = enabled ? limitByType(type) : 0;\n\t\t\treturn value != settings->bytesLimit(_source, type);\n\t\t});\n\n\t\tconst auto &kHidden = kStreamedTypes;\n\t\tconst auto hiddenChanged = ranges::any_of(kHidden, [&](Type type) {\n\t\t\tconst auto now = settings->bytesLimit(_source, type);\n\t\t\treturn (now > 0) && (now != limitByType(type));\n\t\t});\n\n\t\tif (changed) {\n\t\t\tfor (const auto &[type, enabled] : values) {\n\t\t\t\tconst auto value = enabled ? limitByType(type) : 0;\n\t\t\t\tsettings->setBytesLimit(_source, type, value);\n\t\t\t}\n\t\t}\n\t\tif (hiddenChanged) {\n\t\t\tfor (const auto type : kHidden) {\n\t\t\t\tconst auto now = settings->bytesLimit(_source, type);\n\t\t\t\tif (now > 0) {\n\t\t\t\t\tsettings->setBytesLimit(\n\t\t\t\t\t\t_source,\n\t\t\t\t\t\ttype,\n\t\t\t\t\t\tlimitByType(type));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (changed || hiddenChanged) {\n\t\t\t_session->saveSettingsDelayed();\n\t\t}\n\t\tif (allowMoreTypes.contains(Type::Photo)) {\n\t\t\t_session->data().photoLoadSettingsChanged();\n\t\t}\n\t\tif (ranges::any_of(allowMoreTypes, _1 != Type::Photo)) {\n\t\t\t_session->data().documentLoadSettingsChanged();\n\t\t}\n\t\tif (less) {\n\t\t\t_session->data().checkPlayingAnimations();\n\t\t}\n\t\tcloseBox();\n\t});\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\n\tsetDimensionsToContent(st::boxWidth, content);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/auto_download_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\nnamespace AutoDownload {\nenum class Source;\n} // namespace AutoDownload\n} // namespace Data\n\nclass AutoDownloadBox : public Ui::BoxContent {\npublic:\n\tAutoDownloadBox(\n\t\tQWidget*,\n\t\tnot_null<Main::Session*> session,\n\t\tData::AutoDownload::Source source);\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\tvoid setupContent();\n\n\tconst not_null<Main::Session*> _session;\n\n\tData::AutoDownload::Source _source;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/auto_lock_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/auto_lock_box.h\"\n\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/time_input.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace {\n\nconstexpr auto kCustom = std::numeric_limits<int>::max();\nconstexpr auto kOptions = { 60, 300, 3600, 18000, kCustom };\nconstexpr auto kDefaultCustom = \"10:00\"_cs;\n\nauto TimeString(int seconds) {\n\tconst auto hours = seconds / 3600;\n\tconst auto minutes = (seconds - hours * 3600) / 60;\n\treturn QString(\"%1:%2\").arg(hours).arg(minutes, 2, 10, QLatin1Char('0'));\n}\n\n} // namespace\n\nAutoLockBox::AutoLockBox(QWidget*) {\n}\n\nvoid AutoLockBox::prepare() {\n\tsetTitle(tr::lng_passcode_autolock());\n\n\taddButton(tr::lng_box_ok(), [=] { closeBox(); });\n\n\tconst auto currentTime = Core::App().settings().autoLock();\n\n\tconst auto group = std::make_shared<Ui::RadiobuttonGroup>(\n\t\tranges::contains(kOptions, currentTime) ? currentTime : kCustom);\n\n\tconst auto x = st::boxPadding.left() + st::boxOptionListPadding.left();\n\tauto y = st::boxOptionListPadding.top() + st::autolockButton.margin.top();\n\tconst auto count = int(kOptions.size());\n\t_options.reserve(count);\n\tfor (const auto seconds : kOptions) {\n\t\tconst auto text = [&] {\n\t\t\tif (seconds == kCustom) {\n\t\t\t\treturn QString();\n\t\t\t}\n\t\t\tconst auto minutes = (seconds % 3600);\n\t\t\treturn (minutes\n\t\t\t\t? tr::lng_minutes\n\t\t\t\t: tr::lng_hours)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tminutes ? (seconds / 60) : (seconds / 3600));\n\t\t}();\n\t\t_options.emplace_back(\n\t\t\tthis,\n\t\t\tgroup,\n\t\t\tseconds,\n\t\t\ttext,\n\t\t\tst::autolockButton);\n\t\t_options.back()->moveToLeft(x, y);\n\t\ty += _options.back()->heightNoMargins() + st::boxOptionListSkip;\n\t}\n\n\tconst auto timeInput = [&] {\n\t\tconst auto &last = _options.back();\n\t\tconst auto &st = st::autolockButton;\n\n\t\tconst auto textLeft = st.checkPosition.x()\n\t\t\t+ last->checkRect().width()\n\t\t\t+ st.textPosition.x();\n\t\tconst auto textTop = st.margin.top() + st.textPosition.y();\n\n\t\tconst auto timeInput = Ui::CreateChild<Ui::TimeInput>(\n\t\t\tthis,\n\t\t\t(group->current() == kCustom\n\t\t\t\t? TimeString(currentTime)\n\t\t\t\t: kDefaultCustom.utf8()),\n\t\t\tst::autolockTimeField,\n\t\t\tst::autolockDateField,\n\t\t\tst::scheduleTimeSeparator,\n\t\t\tst::scheduleTimeSeparatorPadding);\n\t\ttimeInput->resizeToWidth(st::autolockTimeWidth);\n\t\ttimeInput->moveToLeft(last->x() + textLeft, last->y() + textTop);\n\t\treturn timeInput;\n\t}();\n\n\tconst auto collect = [=] {\n\t\tconst auto timeValue = timeInput->valueCurrent().split(':');\n\t\tif (timeValue.size() != 2) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn timeValue[0].toInt() * 3600 + timeValue[1].toInt() * 60;\n\t};\n\n\ttimeInput->focuses(\n\t) | rpl::on_next([=] {\n\t\tgroup->setValue(kCustom);\n\t}, lifetime());\n\n\tgroup->setChangedCallback([=](int value) {\n\t\tif (value != kCustom) {\n\t\t\tdurationChanged(value);\n\t\t} else {\n\t\t\ttimeInput->setFocusFast();\n\t\t}\n\t});\n\n\trpl::merge(\n\t\tboxClosing() | rpl::filter(\n\t\t\t[=] { return group->current() == kCustom; }\n\t\t),\n\t\ttimeInput->submitRequests()\n\t) | rpl::on_next([=] {\n\t\tif (const auto result = collect()) {\n\t\t\tdurationChanged(result);\n\t\t} else {\n\t\t\ttimeInput->showError();\n\t\t}\n\t}, lifetime());\n\n\tconst auto timeInputBottom = timeInput->y() + timeInput->height();\n\tsetDimensions(\n\t\tst::autolockWidth,\n\t\tst::boxOptionListPadding.top()\n\t\t\t+ (timeInputBottom - _options.back()->bottomNoMargins())\n\t\t\t+ count * _options.back()->heightNoMargins()\n\t\t\t+ (count - 1) * st::boxOptionListSkip\n\t\t\t+ st::boxOptionListPadding.bottom()\n\t\t\t+ st::boxPadding.bottom());\n}\n\nvoid AutoLockBox::durationChanged(int seconds) {\n\tif (Core::App().settings().autoLock() == seconds) {\n\t\tcloseBox();\n\t\treturn;\n\t}\n\tCore::App().settings().setAutoLock(seconds);\n\tCore::App().saveSettingsDelayed();\n\n\tCore::App().checkAutoLock(crl::now());\n\tcloseBox();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/auto_lock_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n\nnamespace Ui {\nclass Radiobutton;\n} // namespace Ui\n\nclass AutoLockBox : public Ui::BoxContent {\npublic:\n\tAutoLockBox(QWidget*);\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\tvoid durationChanged(int seconds);\n\n\tstd::vector<object_ptr<Ui::Radiobutton>> _options;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/background_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/background_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/round_checkbox.h\"\n#include \"ui/image/image.h\"\n#include \"ui/chat/attach/attach_extensions.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"mtproto/sender.h\"\n#include \"core/file_utilities.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"boxes/background_preview_box.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/themes/window_theme.h\"\n#include \"styles/style_overview.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_info.h\"\n\nnamespace {\n\nconstexpr auto kBackgroundsInRow = 3;\n\nQImage TakeMiddleSample(QImage original, QSize size) {\n\tsize *= style::DevicePixelRatio();\n\tconst auto from = original.size();\n\tif (from.isEmpty()) {\n\t\tauto result = original.scaled(size);\n\t\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\t\treturn result;\n\t}\n\n\tconst auto take = (from.width() * size.height()\n\t\t> from.height() * size.width())\n\t\t? QSize(size.width() * from.height() / size.height(), from.height())\n\t\t: QSize(from.width(), size.height() * from.width() / size.width());\n\tauto result = original.copy(\n\t\t(from.width() - take.width()) / 2,\n\t\t(from.height() - take.height()) / 2,\n\t\ttake.width(),\n\t\ttake.height()\n\t).scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\treturn result;\n}\n\n} // namespace\n\nclass BackgroundBox::Inner final : public Ui::RpWidget {\npublic:\n\tInner(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Session*> session,\n\t\tPeerData *forPeer);\n\t~Inner();\n\n\t[[nodiscard]] rpl::producer<Data::WallPaper> chooseEvents() const;\n\t[[nodiscard]] rpl::producer<Data::WallPaper> removeRequests() const;\n\n\t[[nodiscard]] auto resolveResetCustomPaper() const\n\t\t->std::optional<Data::WallPaper>;\n\n\tvoid removePaper(const Data::WallPaper &data);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\n\tstruct Paper {\n\t\tData::WallPaper data;\n\t\tmutable std::shared_ptr<Data::DocumentMedia> dataMedia;\n\t\tmutable QPixmap thumbnail;\n\t};\n\tstruct Selected {\n\t\tint index = 0;\n\t\tinline bool operator==(const Selected &other) const {\n\t\t\treturn index == other.index;\n\t\t}\n\t\tinline bool operator!=(const Selected &other) const {\n\t\t\treturn !(*this == other);\n\t\t}\n\t};\n\tstruct DeleteSelected {\n\t\tint index = 0;\n\t\tinline bool operator==(const DeleteSelected &other) const {\n\t\t\treturn index == other.index;\n\t\t}\n\t\tinline bool operator!=(const DeleteSelected &other) const {\n\t\t\treturn !(*this == other);\n\t\t}\n\t};\n\tusing Selection = std::variant<v::null_t, Selected, DeleteSelected>;\n\n\tint getSelectionIndex(const Selection &selection) const;\n\tvoid repaintPaper(int index);\n\tvoid resizeToContentAndPreload();\n\tvoid updatePapers();\n\tvoid requestPapers();\n\tvoid pushCustomPapers();\n\tvoid sortPapers();\n\tvoid paintPaper(\n\t\tQPainter &p,\n\t\tconst Paper &paper,\n\t\tint column,\n\t\tint row) const;\n\tvoid validatePaperThumbnail(const Paper &paper) const;\n\n\t[[nodiscard]] bool forChannel() const;\n\n\tconst not_null<Main::Session*> _session;\n\tPeerData * const _forPeer = nullptr;\n\n\tMTP::Sender _api;\n\n\tstd::vector<Paper> _papers;\n\tuint64 _currentId = 0;\n\tuint64 _insertedResetId = 0;\n\n\tSelection _over;\n\tSelection _overDown;\n\n\tstd::unique_ptr<Ui::RoundCheckbox> _check; // this is not a widget\n\trpl::event_stream<Data::WallPaper> _backgroundChosen;\n\trpl::event_stream<Data::WallPaper> _backgroundRemove;\n\n};\n\nBackgroundBox::BackgroundBox(\n\tQWidget*,\n\tnot_null<Window::SessionController*> controller,\n\tPeerData *forPeer)\n: _controller(controller)\n, _forPeer(forPeer) {\n}\n\nvoid BackgroundBox::prepare() {\n\tsetTitle(tr::lng_backgrounds_header());\n\n\taddButton(tr::lng_close(), [=] { closeBox(); });\n\n\tsetDimensions(st::boxWideWidth, st::boxMaxListHeight);\n\n\tauto wrap = object_ptr<Ui::VerticalLayout>(this);\n\tconst auto container = wrap.data();\n\n\tUi::AddSkip(container);\n\n\tconst auto button = container->add(object_ptr<Ui::SettingsButton>(\n\t\tcontainer,\n\t\ttr::lng_settings_bg_from_file(),\n\t\tst::infoProfileButton));\n\tobject_ptr<Info::Profile::FloatingIcon>(\n\t\tbutton,\n\t\tst::infoIconMediaPhoto,\n\t\tst::infoSharedMediaButtonIconPosition);\n\n\tif (forChannel() && _forPeer->wallPaper()) {\n\t\tconst auto remove = container->add(object_ptr<Ui::SettingsButton>(\n\t\t\tcontainer,\n\t\t\ttr::lng_settings_bg_remove(),\n\t\t\tst::infoBlockButton));\n\t\tobject_ptr<Info::Profile::FloatingIcon>(\n\t\t\tremove,\n\t\t\tst::infoIconDeleteRed,\n\t\t\tst::infoSharedMediaButtonIconPosition);\n\n\t\tremove->setClickedCallback([=] {\n\t\t\tif (const auto resolved = _inner->resolveResetCustomPaper()) {\n\t\t\t\tchosen(*resolved);\n\t\t\t}\n\t\t});\n\t}\n\n\tbutton->setClickedCallback([=] {\n\t\tchooseFromFile();\n\t});\n\n\tUi::AddSkip(container);\n\tUi::AddDivider(container);\n\n\t_inner = container->add(\n\t\tobject_ptr<Inner>(this, &_controller->session(), _forPeer));\n\n\tcontainer->resizeToWidth(st::boxWideWidth);\n\n\tsetInnerWidget(std::move(wrap), st::backgroundScroll);\n\tsetInnerTopSkip(st::lineWidth);\n\n\t_inner->chooseEvents(\n\t) | rpl::on_next([=](const Data::WallPaper &paper) {\n\t\tchosen(paper);\n\t}, _inner->lifetime());\n\n\t_inner->removeRequests(\n\t) | rpl::on_next([=](const Data::WallPaper &paper) {\n\t\tremovePaper(paper);\n\t}, _inner->lifetime());\n}\n\nvoid BackgroundBox::chooseFromFile() {\n\tconst auto filterStart = _forPeer\n\t\t? u\"Image files (*\"_q\n\t\t: u\"Theme files (*.tdesktop-theme *.tdesktop-palette *\"_q;\n\tauto filters = QStringList(\n\t\tfilterStart\n\t\t+ Ui::ImageExtensions().join(u\" *\"_q)\n\t\t+ u\")\"_q);\n\tfilters.push_back(FileDialog::AllFilesFilter());\n\tconst auto callback = [=](const FileDialog::OpenResult &result) {\n\t\tif (result.paths.isEmpty() && result.remoteContent.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!_forPeer && !result.paths.isEmpty()) {\n\t\t\tconst auto filePath = result.paths.front();\n\t\t\tconst auto hasExtension = [&](QLatin1String extension) {\n\t\t\t\treturn filePath.endsWith(extension, Qt::CaseInsensitive);\n\t\t\t};\n\t\t\tif (hasExtension(qstr(\".tdesktop-theme\"))\n\t\t\t\t|| hasExtension(qstr(\".tdesktop-palette\"))) {\n\t\t\t\tWindow::Theme::Apply(filePath);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tauto image = Images::Read({\n\t\t\t.path = result.paths.isEmpty() ? QString() : result.paths.front(),\n\t\t\t.content = result.remoteContent,\n\t\t\t.forceOpaque = true,\n\t\t}).image;\n\t\tif (image.isNull() || image.width() <= 0 || image.height() <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tauto local = Data::CustomWallPaper();\n\t\tlocal.setLocalImageAsThumbnail(std::make_shared<Image>(\n\t\t\tstd::move(image)));\n\t\t_controller->show(Box<BackgroundPreviewBox>(\n\t\t\t_controller,\n\t\t\tlocal,\n\t\t\tBackgroundPreviewArgs{ _forPeer }));\n\t};\n\tFileDialog::GetOpenPath(\n\t\tthis,\n\t\ttr::lng_choose_image(tr::now),\n\t\tfilters.join(u\";;\"_q),\n\t\tcrl::guard(this, callback));\n}\n\nbool BackgroundBox::hasDefaultForPeer() const {\n\tExpects(_forPeer != nullptr);\n\n\tconst auto paper = _forPeer->wallPaper();\n\tif (!paper) {\n\t\treturn true;\n\t}\n\tconst auto reset = _inner->resolveResetCustomPaper();\n\tAssert(reset.has_value());\n\treturn (paper->id() == reset->id());\n}\n\nbool BackgroundBox::chosenDefaultForPeer(\n\t\tconst Data::WallPaper &paper) const {\n\tif (!_forPeer) {\n\t\treturn false;\n\t}\n\n\tconst auto reset = _inner->resolveResetCustomPaper();\n\tAssert(reset.has_value());\n\treturn (paper.id() == reset->id());\n}\n\nvoid BackgroundBox::chosen(const Data::WallPaper &paper) {\n\tif (chosenDefaultForPeer(paper)) {\n\t\tif (!hasDefaultForPeer()) {\n\t\t\tconst auto reset = crl::guard(this, [=](Fn<void()> close) {\n\t\t\t\tresetForPeer();\n\t\t\t\tclose();\n\t\t\t});\n\t\t\t_controller->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_background_sure_reset_default(),\n\t\t\t\t.confirmed = reset,\n\t\t\t\t.confirmText = tr::lng_background_reset_default(),\n\t\t\t}));\n\t\t} else {\n\t\t\tcloseBox();\n\t\t}\n\t\treturn;\n\t} else if (forChannel()) {\n\t\tif (_forPeer->wallPaper() && _forPeer->wallPaper()->equals(paper)) {\n\t\t\tcloseBox();\n\t\t\treturn;\n\t\t}\n\t\tconst auto &themes = _forPeer->owner().cloudThemes();\n\t\tfor (const auto &theme : themes.chatThemes()) {\n\t\t\tfor (const auto &[type, themed] : theme.settings) {\n\t\t\t\tif (themed.paper && themed.paper->equals(paper)) {\n\t\t\t\t\t_controller->show(Box<BackgroundPreviewBox>(\n\t\t\t\t\t\t_controller,\n\t\t\t\t\t\tData::WallPaper::FromEmojiId(theme.emoticon),\n\t\t\t\t\t\tBackgroundPreviewArgs{ _forPeer }));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t_controller->show(Box<BackgroundPreviewBox>(\n\t\t_controller,\n\t\tpaper,\n\t\tBackgroundPreviewArgs{ _forPeer }));\n}\n\nvoid BackgroundBox::resetForPeer() {\n\tconst auto api = &_controller->session().api();\n\tapi->request(MTPmessages_SetChatWallPaper(\n\t\tMTP_flags(0),\n\t\t_forPeer->input(),\n\t\tMTPInputWallPaper(),\n\t\tMTPWallPaperSettings(),\n\t\tMTPint()\n\t)).done([=](const MTPUpdates &result) {\n\t\tapi->applyUpdates(result);\n\t}).send();\n\n\tconst auto weak = base::make_weak(this);\n\t_forPeer->setWallPaper({});\n\tif (weak) {\n\t\t_controller->finishChatThemeEdit(_forPeer);\n\t}\n}\n\nbool BackgroundBox::forChannel() const {\n\treturn _forPeer && _forPeer->isChannel();\n}\n\nvoid BackgroundBox::removePaper(const Data::WallPaper &paper) {\n\tconst auto session = &_controller->session();\n\tconst auto remove = [=, weak = base::make_weak(this)](Fn<void()> &&close) {\n\t\tclose();\n\t\tif (weak) {\n\t\t\tweak->_inner->removePaper(paper);\n\t\t}\n\t\tsession->data().removeWallpaper(paper);\n\t\tsession->api().request(MTPaccount_SaveWallPaper(\n\t\t\tpaper.mtpInput(session),\n\t\t\tMTP_bool(true),\n\t\t\tpaper.mtpSettings()\n\t\t)).send();\n\t};\n\t_controller->show(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_background_sure_delete(),\n\t\t.confirmed = remove,\n\t\t.confirmText = tr::lng_selected_delete(),\n\t}));\n}\n\nBackgroundBox::Inner::Inner(\n\tQWidget *parent,\n\tnot_null<Main::Session*> session,\n\tPeerData *forPeer)\n: RpWidget(parent)\n, _session(session)\n, _forPeer(forPeer)\n, _api(&_session->mtp())\n, _check(\n\tstd::make_unique<Ui::RoundCheckbox>(\n\t\tst::overviewCheck,\n\t\t[=] { update(); })) {\n\t_check->setChecked(true, anim::type::instant);\n\tresize(\n\t\tst::boxWideWidth,\n\t\t(2 * (st::backgroundSize.height() + st::backgroundPadding)\n\t\t\t+ st::backgroundPadding));\n\n\tWindow::Theme::IsNightModeValue(\n\t) | rpl::on_next([=] {\n\t\tupdatePapers();\n\t}, lifetime());\n\trequestPapers();\n\n\t_session->downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_check->invalidateCache();\n\t}, lifetime());\n\n\tif (forChannel()) {\n\t\t_session->data().cloudThemes().chatThemesUpdated(\n\t\t) | rpl::on_next([=] {\n\t\t\tupdatePapers();\n\t\t}, lifetime());\n\t} else {\n\t\tusing Update = Window::Theme::BackgroundUpdate;\n\t\tWindow::Theme::Background()->updates(\n\t\t) | rpl::on_next([=](const Update &update) {\n\t\t\tif (update.type == Update::Type::New) {\n\t\t\t\tsortPapers();\n\t\t\t\trequestPapers();\n\t\t\t\tthis->update();\n\t\t\t}\n\t\t}, lifetime());\n\t}\n\n\tsetMouseTracking(true);\n}\n\nvoid BackgroundBox::Inner::requestPapers() {\n\tif (forChannel()) {\n\t\t_session->data().cloudThemes().refreshChatThemes();\n\t\treturn;\n\t}\n\t_api.request(MTPaccount_GetWallPapers(\n\t\tMTP_long(_session->data().wallpapersHash())\n\t)).done([=](const MTPaccount_WallPapers &result) {\n\t\tif (_session->data().updateWallpapers(result)) {\n\t\t\tupdatePapers();\n\t\t}\n\t}).send();\n}\n\nauto BackgroundBox::Inner::resolveResetCustomPaper() const\n-> std::optional<Data::WallPaper> {\n\tif (!_forPeer) {\n\t\treturn {};\n\t}\n\tconst auto nonCustom = Window::Theme::Background()->paper();\n\tconst auto themeToken = _forPeer->themeToken();\n\tif (forChannel() || themeToken.isEmpty()) {\n\t\treturn nonCustom;\n\t}\n\tconst auto &themes = _forPeer->owner().cloudThemes();\n\tconst auto theme = themes.themeForToken(themeToken);\n\tif (!theme) {\n\t\treturn nonCustom;\n\t}\n\tusing Type = Data::CloudTheme::Type;\n\tconst auto dark = Window::Theme::IsNightMode();\n\tconst auto i = theme->settings.find(dark ? Type::Dark : Type::Light);\n\tif (i != end(theme->settings) && i->second.paper) {\n\t\treturn *i->second.paper;\n\t}\n\treturn nonCustom;\n}\n\nvoid BackgroundBox::Inner::pushCustomPapers() {\n\tauto customId = uint64();\n\tif (const auto custom = _forPeer ? _forPeer->wallPaper() : nullptr) {\n\t\tcustomId = custom->id();\n\t\tconst auto j = ranges::find(\n\t\t\t_papers,\n\t\t\tcustom->id(),\n\t\t\t[](const Paper &paper) { return paper.data.id(); });\n\t\tif (j != end(_papers)) {\n\t\t\tj->data = j->data.withParamsFrom(*custom);\n\t\t} else {\n\t\t\t_papers.insert(begin(_papers), Paper{ *custom });\n\t\t}\n\t}\n\tif (const auto reset = resolveResetCustomPaper()) {\n\t\t_insertedResetId = reset->id();\n\t\tconst auto j = ranges::find(\n\t\t\t_papers,\n\t\t\t_insertedResetId,\n\t\t\t[](const Paper &paper) { return paper.data.id(); });\n\t\tif (j != end(_papers)) {\n\t\t\tif (_insertedResetId != customId) {\n\t\t\t\tj->data = j->data.withParamsFrom(*reset);\n\t\t\t}\n\t\t} else {\n\t\t\t_papers.insert(begin(_papers), Paper{ *reset });\n\t\t}\n\t}\n}\n\nvoid BackgroundBox::Inner::sortPapers() {\n\tExpects(!forChannel());\n\n\tconst auto currentCustom = _forPeer ? _forPeer->wallPaper() : nullptr;\n\t_currentId = currentCustom\n\t\t? currentCustom->id()\n\t\t: _insertedResetId\n\t\t? _insertedResetId\n\t\t: Window::Theme::Background()->id();\n\tconst auto dark = Window::Theme::IsNightMode();\n\tranges::stable_sort(_papers, std::greater<>(), [&](const Paper &paper) {\n\t\tconst auto &data = paper.data;\n\t\treturn std::make_tuple(\n\t\t\t_insertedResetId && (_insertedResetId == data.id()),\n\t\t\tdata.id() == _currentId,\n\t\t\tdark ? data.isDark() : !data.isDark(),\n\t\t\tData::IsDefaultWallPaper(data),\n\t\t\t!data.isDefault() && !Data::IsLegacy1DefaultWallPaper(data),\n\t\t\tData::IsLegacy3DefaultWallPaper(data),\n\t\t\tData::IsLegacy2DefaultWallPaper(data),\n\t\t\tData::IsLegacy1DefaultWallPaper(data));\n\t});\n\tif (!_papers.empty()\n\t\t&& _papers.front().data.id() == _currentId\n\t\t&& !currentCustom\n\t\t&& !_insertedResetId) {\n\t\t_papers.front().data = _papers.front().data.withParamsFrom(\n\t\t\tWindow::Theme::Background()->paper());\n\t}\n}\n\nvoid BackgroundBox::Inner::updatePapers() {\n\tif (forChannel()) {\n\t\tif (_session->data().cloudThemes().chatThemes().empty()) {\n\t\t\treturn;\n\t\t}\n\t} else {\n\t\tif (_session->data().wallpapers().empty()) {\n\t\t\treturn;\n\t\t}\n\t}\n\t_over = _overDown = Selection();\n\n\tconst auto was = base::take(_papers);\n\tif (forChannel()) {\n\t\tconst auto now = _forPeer->wallPaper();\n\t\tconst auto &list = _session->data().cloudThemes().chatThemes();\n\t\tif (list.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tusing Type = Data::CloudThemeType;\n\t\tconst auto type = Window::Theme::IsNightMode()\n\t\t\t? Type::Dark\n\t\t\t: Type::Light;\n\t\t_papers.reserve(list.size() + 1);\n\t\tconst auto nowEmojiId = now ? now->emojiId() : QString();\n\t\tif (!now || !now->emojiId().isEmpty()) {\n\t\t\t_papers.push_back({ Window::Theme::Background()->paper() });\n\t\t\t_currentId = _papers.back().data.id();\n\t\t} else {\n\t\t\t_papers.push_back({ *now });\n\t\t\t_currentId = now->id();\n\t\t}\n\t\tfor (const auto &theme : list) {\n\t\t\tconst auto i = theme.settings.find(type);\n\t\t\tif (i != end(theme.settings) && i->second.paper) {\n\t\t\t\t_papers.push_back({ *i->second.paper });\n\t\t\t\tif (nowEmojiId == theme.emoticon) {\n\t\t\t\t\t_currentId = _papers.back().data.id();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\t_papers = _session->data().wallpapers(\n\t\t) | ranges::views::filter([&](const Data::WallPaper &paper) {\n\t\t\treturn (!paper.isPattern() || !paper.backgroundColors().empty())\n\t\t\t\t&& (!_forPeer\n\t\t\t\t\t|| (!Data::IsDefaultWallPaper(paper)\n\t\t\t\t\t\t&& (Data::IsCloudWallPaper(paper)\n\t\t\t\t\t\t\t|| Data::IsCustomWallPaper(paper))));\n\t\t}) | ranges::views::transform([](const Data::WallPaper &paper) {\n\t\t\treturn Paper{ paper };\n\t\t}) | ranges::to_vector;\n\t\tpushCustomPapers();\n\t\tsortPapers();\n\t}\n\tresizeToContentAndPreload();\n}\n\nvoid BackgroundBox::Inner::resizeToContentAndPreload() {\n\tconst auto count = _papers.size();\n\tconst auto rows = (count / kBackgroundsInRow)\n\t\t+ (count % kBackgroundsInRow ? 1 : 0);\n\n\tresize(\n\t\tst::boxWideWidth,\n\t\t(rows * (st::backgroundSize.height() + st::backgroundPadding)\n\t\t\t+ st::backgroundPadding));\n\n\tconst auto preload = kBackgroundsInRow * 3;\n\tfor (const auto &paper : _papers | ranges::views::take(preload)) {\n\t\tif (!paper.data.localThumbnail() && !paper.dataMedia) {\n\t\t\tif (const auto document = paper.data.document()) {\n\t\t\t\tpaper.dataMedia = document->createMediaView();\n\t\t\t\tpaper.dataMedia->thumbnailWanted(paper.data.fileOrigin());\n\t\t\t}\n\t\t}\n\t}\n\tupdate();\n}\n\nvoid BackgroundBox::Inner::paintEvent(QPaintEvent *e) {\n\tQRect r(e->rect());\n\tauto p = QPainter(this);\n\n\tif (_papers.empty()) {\n\t\tp.setFont(st::noContactsFont);\n\t\tp.setPen(st::noContactsColor);\n\t\tp.drawText(QRect(0, 0, width(), st::noContactsHeight), tr::lng_contacts_loading(tr::now), style::al_center);\n\t\treturn;\n\t}\n\tauto row = 0;\n\tauto column = 0;\n\tfor (const auto &paper : _papers) {\n\t\tconst auto increment = gsl::finally([&] {\n\t\t\t++column;\n\t\t\tif (column == kBackgroundsInRow) {\n\t\t\t\tcolumn = 0;\n\t\t\t\t++row;\n\t\t\t}\n\t\t});\n\t\tif ((st::backgroundSize.height() + st::backgroundPadding) * (row + 1) <= r.top()) {\n\t\t\tcontinue;\n\t\t} else if ((st::backgroundSize.height() + st::backgroundPadding) * row >= r.top() + r.height()) {\n\t\t\tbreak;\n\t\t}\n\t\tpaintPaper(p, paper, column, row);\n\t}\n}\n\nvoid BackgroundBox::Inner::validatePaperThumbnail(\n\t\tconst Paper &paper) const {\n\tif (!paper.thumbnail.isNull()) {\n\t\treturn;\n\t}\n\tconst auto localThumbnail = paper.data.localThumbnail();\n\tif (!localThumbnail) {\n\t\tif (const auto document = paper.data.document()) {\n\t\t\tif (!paper.dataMedia) {\n\t\t\t\tpaper.dataMedia = document->createMediaView();\n\t\t\t\tpaper.dataMedia->thumbnailWanted(paper.data.fileOrigin());\n\t\t\t}\n\t\t\tif (!paper.dataMedia->thumbnail()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else if (!paper.data.backgroundColors().empty()) {\n\t\t\tpaper.thumbnail = Ui::PixmapFromImage(\n\t\t\t\tUi::GenerateBackgroundImage(\n\t\t\t\t\tst::backgroundSize * style::DevicePixelRatio(),\n\t\t\t\t\tpaper.data.backgroundColors(),\n\t\t\t\t\tpaper.data.gradientRotation()));\n\t\t\tpaper.thumbnail.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\treturn;\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto thumbnail = localThumbnail\n\t\t? localThumbnail\n\t\t: paper.dataMedia->thumbnail();\n\tauto original = thumbnail->original();\n\tif (paper.data.isPattern()) {\n\t\toriginal = Ui::PreparePatternImage(\n\t\t\tstd::move(original),\n\t\t\tpaper.data.backgroundColors(),\n\t\t\tpaper.data.gradientRotation(),\n\t\t\tpaper.data.patternOpacity());\n\t}\n\tpaper.thumbnail = Ui::PixmapFromImage(TakeMiddleSample(\n\t\toriginal,\n\t\tst::backgroundSize));\n\tpaper.thumbnail.setDevicePixelRatio(style::DevicePixelRatio());\n}\n\nbool BackgroundBox::Inner::forChannel() const {\n\treturn _forPeer && _forPeer->isChannel();\n}\n\nvoid BackgroundBox::Inner::paintPaper(\n\t\tQPainter &p,\n\t\tconst Paper &paper,\n\t\tint column,\n\t\tint row) const {\n\tconst auto x = st::backgroundPadding + column * (st::backgroundSize.width() + st::backgroundPadding);\n\tconst auto y = st::backgroundPadding + row * (st::backgroundSize.height() + st::backgroundPadding);\n\tvalidatePaperThumbnail(paper);\n\tif (!paper.thumbnail.isNull()) {\n\t\tp.drawPixmap(x, y, paper.thumbnail);\n\t}\n\n\tconst auto over = !v::is_null(_overDown) ? _overDown : _over;\n\tif (paper.data.id() == _currentId) {\n\t\tconst auto checkLeft = x + st::backgroundSize.width() - st::overviewCheckSkip - st::overviewCheck.size;\n\t\tconst auto checkTop = y + st::backgroundSize.height() - st::overviewCheckSkip - st::overviewCheck.size;\n\t\t_check->paint(p, checkLeft, checkTop, width());\n\t} else if (!forChannel()\n\t\t&& Data::IsCloudWallPaper(paper.data)\n\t\t&& !Data::IsDefaultWallPaper(paper.data)\n\t\t&& !Data::IsLegacy2DefaultWallPaper(paper.data)\n\t\t&& !Data::IsLegacy3DefaultWallPaper(paper.data)\n\t\t&& !v::is_null(over)\n\t\t&& (&paper == &_papers[getSelectionIndex(over)])) {\n\t\tconst auto deleteSelected = v::is<DeleteSelected>(over);\n\t\tconst auto deletePos = QPoint(x + st::backgroundSize.width() - st::stickerPanDeleteIconBg.width(), y);\n\t\tp.setOpacity(deleteSelected ? st::stickerPanDeleteOpacityBgOver : st::stickerPanDeleteOpacityBg);\n\t\tst::stickerPanDeleteIconBg.paint(p, deletePos, width());\n\t\tp.setOpacity(deleteSelected ? st::stickerPanDeleteOpacityFgOver : st::stickerPanDeleteOpacityFg);\n\t\tst::stickerPanDeleteIconFg.paint(p, deletePos, width());\n\t\tp.setOpacity(1.);\n\t}\n}\n\nvoid BackgroundBox::Inner::mouseMoveEvent(QMouseEvent *e) {\n\tconst auto newOver = [&] {\n\t\tconst auto x = e->pos().x();\n\t\tconst auto y = e->pos().y();\n\t\tconst auto width = st::backgroundSize.width();\n\t\tconst auto height = st::backgroundSize.height();\n\t\tconst auto skip = st::backgroundPadding;\n\t\tconst auto row = int((y - skip) / (height + skip));\n\t\tconst auto column = int((x - skip) / (width + skip));\n\t\tconst auto result = row * kBackgroundsInRow + column;\n\t\tif (y - row * (height + skip) > skip + height) {\n\t\t\treturn Selection();\n\t\t} else if (x - column * (width + skip) > skip + width) {\n\t\t\treturn Selection();\n\t\t} else if (result >= _papers.size()) {\n\t\t\treturn Selection();\n\t\t}\n\t\tauto &data = _papers[result].data;\n\t\tconst auto deleteLeft = (column + 1) * (width + skip)\n\t\t\t- st::stickerPanDeleteIconBg.width();\n\t\tconst auto deleteBottom = row * (height + skip) + skip\n\t\t\t+ st::stickerPanDeleteIconBg.height();\n\t\tconst auto inDelete = !forChannel()\n\t\t\t&& (x >= deleteLeft)\n\t\t\t&& (y < deleteBottom)\n\t\t\t&& Data::IsCloudWallPaper(data)\n\t\t\t&& !Data::IsDefaultWallPaper(data)\n\t\t\t&& !Data::IsLegacy2DefaultWallPaper(data)\n\t\t\t&& !Data::IsLegacy3DefaultWallPaper(data)\n\t\t\t&& (_currentId != data.id());\n\t\treturn (result >= _papers.size())\n\t\t\t? Selection()\n\t\t\t: inDelete\n\t\t\t? Selection(DeleteSelected{ result })\n\t\t\t: Selection(Selected{ result });\n\t}();\n\tif (_over != newOver) {\n\t\trepaintPaper(getSelectionIndex(_over));\n\t\t_over = newOver;\n\t\trepaintPaper(getSelectionIndex(_over));\n\t\tsetCursor((!v::is_null(_over) || !v::is_null(_overDown))\n\t\t\t? style::cur_pointer\n\t\t\t: style::cur_default);\n\t}\n}\n\nvoid BackgroundBox::Inner::repaintPaper(int index) {\n\tif (index < 0 || index >= _papers.size()) {\n\t\treturn;\n\t}\n\tconst auto row = (index / kBackgroundsInRow);\n\tconst auto column = (index % kBackgroundsInRow);\n\tconst auto width = st::backgroundSize.width();\n\tconst auto height = st::backgroundSize.height();\n\tconst auto skip = st::backgroundPadding;\n\tupdate(\n\t\t(width + skip) * column + skip,\n\t\t(height + skip) * row + skip,\n\t\twidth,\n\t\theight);\n}\n\nvoid BackgroundBox::Inner::mousePressEvent(QMouseEvent *e) {\n\t_overDown = _over;\n}\n\nint BackgroundBox::Inner::getSelectionIndex(\n\t\tconst Selection &selection) const {\n\treturn v::match(selection, [](const Selected &data) {\n\t\treturn data.index;\n\t}, [](const DeleteSelected &data) {\n\t\treturn data.index;\n\t}, [](v::null_t) {\n\t\treturn -1;\n\t});\n}\n\nvoid BackgroundBox::Inner::mouseReleaseEvent(QMouseEvent *e) {\n\tif (base::take(_overDown) == _over && !v::is_null(_over)) {\n\t\tconst auto index = getSelectionIndex(_over);\n\t\tif (index >= 0 && index < _papers.size()) {\n\t\t\tif (std::get_if<DeleteSelected>(&_over)) {\n\t\t\t\t_backgroundRemove.fire_copy(_papers[index].data);\n\t\t\t} else if (std::get_if<Selected>(&_over)) {\n\t\t\t\tauto &paper = _papers[index];\n\t\t\t\tif (!paper.dataMedia) {\n\t\t\t\t\tif (const auto document = paper.data.document()) {\n\t\t\t\t\t\t// Keep it alive while it is on the screen.\n\t\t\t\t\t\tpaper.dataMedia = document->createMediaView();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t_backgroundChosen.fire_copy(paper.data);\n\t\t\t}\n\t\t}\n\t} else if (v::is_null(_over)) {\n\t\tsetCursor(style::cur_default);\n\t}\n}\n\nvoid BackgroundBox::Inner::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tfor (auto i = 0, count = int(_papers.size()); i != count; ++i) {\n\t\tconst auto row = (i / kBackgroundsInRow);\n\t\tconst auto height = st::backgroundSize.height();\n\t\tconst auto skip = st::backgroundPadding;\n\t\tconst auto top = skip + row * (height + skip);\n\t\tconst auto bottom = top + height;\n\t\tif ((bottom <= visibleTop || top >= visibleBottom)\n\t\t\t&& !_papers[i].thumbnail.isNull()) {\n\t\t\t_papers[i].dataMedia = nullptr;\n\t\t}\n\t}\n}\n\nrpl::producer<Data::WallPaper> BackgroundBox::Inner::chooseEvents() const {\n\treturn _backgroundChosen.events();\n}\n\nauto BackgroundBox::Inner::removeRequests() const\n-> rpl::producer<Data::WallPaper> {\n\treturn _backgroundRemove.events();\n}\n\nvoid BackgroundBox::Inner::removePaper(const Data::WallPaper &data) {\n\tconst auto i = ranges::find(\n\t\t_papers,\n\t\tdata.id(),\n\t\t[](const Paper &paper) { return paper.data.id(); });\n\tif (i != end(_papers)) {\n\t\t_papers.erase(i);\n\t\t_over = _overDown = Selection();\n\t\tresizeToContentAndPreload();\n\t}\n}\n\nBackgroundBox::Inner::~Inner() = default;\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/background_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n\nclass PeerData;\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Data {\nclass WallPaper;\n} // namespace Data\n\nclass BackgroundBox : public Ui::BoxContent {\npublic:\n\tBackgroundBox(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tPeerData *forPeer = nullptr);\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\tclass Inner;\n\n\tvoid chosen(const Data::WallPaper &paper);\n\t[[nodiscard]] bool hasDefaultForPeer() const;\n\t[[nodiscard]] bool chosenDefaultForPeer(\n\t\tconst Data::WallPaper &paper) const;\n\tvoid removePaper(const Data::WallPaper &paper);\n\tvoid resetForPeer();\n\t[[nodiscard]] bool forChannel() const;\n\n\tvoid chooseFromFile();\n\n\tconst not_null<Window::SessionController*> _controller;\n\n\tQPointer<Inner> _inner;\n\tPeerData *_forPeer = nullptr;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/background_preview_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/background_preview_box.h\"\n\n#include \"base/unixtime.h\"\n#include \"boxes/peers/edit_peer_color_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"mainwidget.h\"\n#include \"window/themes/window_theme.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/boost_box.h\"\n#include \"ui/controls/chat_service_checkbox.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/image/image.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/ui_utility.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/view/history_view_message.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_document_resolver.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_premium_limits.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"storage/file_upload.h\"\n#include \"storage/localimageloader.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/themes/window_themes_embedded.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\n#include <QtGui/QClipboard>\n#include <QtGui/QGuiApplication>\n\nnamespace {\n\nconstexpr auto kMaxWallPaperSlugLength = 255;\n\n[[nodiscard]] bool IsValidWallPaperSlug(const QString &slug) {\n\tif (slug.isEmpty() || slug.size() > kMaxWallPaperSlugLength) {\n\t\treturn false;\n\t}\n\treturn ranges::none_of(slug, [](QChar ch) {\n\t\treturn (ch != '.')\n\t\t\t&& (ch != '_')\n\t\t\t&& (ch != '-')\n\t\t\t&& (ch < '0' || ch > '9')\n\t\t\t&& (ch < 'a' || ch > 'z')\n\t\t\t&& (ch < 'A' || ch > 'Z');\n\t});\n}\n\n[[nodiscard]] AdminLog::OwnedItem GenerateServiceItem(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate,\n\t\tnot_null<History*> history,\n\t\tconst QString &text,\n\t\tbool out) {\n\tExpects(history->peer->isUser());\n\n\tconst auto flags = MessageFlag::FakeHistoryItem\n\t\t| MessageFlag::HasFromId\n\t\t| (out ? MessageFlag::Outgoing : MessageFlag(0));\n\tconst auto item = history->makeMessage({\n\t\t.id = history->owner().nextLocalMessageId(),\n\t\t.flags = flags,\n\t\t.date = base::unixtime::now(),\n\t}, PreparedServiceText{ { text } });\n\treturn AdminLog::OwnedItem(delegate, item);\n}\n\n[[nodiscard]] AdminLog::OwnedItem GenerateTextItem(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate,\n\t\tnot_null<History*> history,\n\t\tconst QString &text,\n\t\tbool out) {\n\tExpects(history->peer->isUser());\n\n\tconst auto item = history->makeMessage({\n\t\t.id = history->nextNonHistoryEntryId(),\n\t\t.flags = (MessageFlag::FakeHistoryItem\n\t\t\t| MessageFlag::HasFromId\n\t\t\t| (out ? MessageFlag::Outgoing : MessageFlag(0))),\n\t\t.from = (out\n\t\t\t? history->session().userId()\n\t\t\t: peerToUser(history->peer->id)),\n\t\t.date = base::unixtime::now(),\n\t}, TextWithEntities{ text }, MTP_messageMediaEmpty());\n\treturn AdminLog::OwnedItem(delegate, item);\n}\n\n[[nodiscard]] QImage PrepareScaledNonPattern(\n\t\tconst QImage &image,\n\t\tImages::Option blur) {\n\tconst auto size = st::boxWideWidth;\n\tconst auto width = std::max(image.width(), 1);\n\tconst auto height = std::max(image.height(), 1);\n\tconst auto takeWidth = (width > height)\n\t\t? (width * size / height)\n\t\t: size;\n\tconst auto takeHeight = (width > height)\n\t\t? size\n\t\t: (height * size / width);\n\tconst auto ratio = style::DevicePixelRatio();\n\treturn Images::Prepare(image, QSize(takeWidth, takeHeight) * ratio, {\n\t\t.options = Images::Option::TransparentBackground | blur,\n\t\t.outer = { size, size },\n\t});\n}\n\n[[nodiscard]] QImage PrepareScaledFromFull(\n\t\tconst QImage &image,\n\t\tbool isPattern,\n\t\tconst std::vector<QColor> &background,\n\t\tint gradientRotation,\n\t\tfloat64 patternOpacity,\n\t\tImages::Option blur = Images::Option(0)) {\n\tauto result = PrepareScaledNonPattern(image, blur);\n\tif (isPattern) {\n\t\tresult = Ui::PreparePatternImage(\n\t\t\tstd::move(result),\n\t\t\tbackground,\n\t\t\tgradientRotation,\n\t\t\tpatternOpacity);\n\t}\n\treturn std::move(result).convertToFormat(\n\t\tQImage::Format_ARGB32_Premultiplied);\n}\n\n[[nodiscard]] QImage BlackImage(QSize size) {\n\tauto result = QImage(size, QImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::black);\n\treturn result;\n}\n\n[[nodiscard]] Data::WallPaper Resolve(\n\t\tnot_null<Main::Session*> session,\n\t\tconst Data::WallPaper &paper,\n\t\tbool dark) {\n\tif (paper.emojiId().isEmpty()) {\n\t\treturn paper;\n\t}\n\tconst auto &themes = session->data().cloudThemes();\n\tif (const auto theme = themes.themeForToken(paper.emojiId())) {\n\t\tusing Type = Data::CloudThemeType;\n\t\tconst auto type = dark ? Type::Dark : Type::Light;\n\t\tconst auto i = theme->settings.find(type);\n\t\tif (i != end(theme->settings) && i->second.paper) {\n\t\t\treturn *i->second.paper;\n\t\t}\n\t}\n\treturn paper;\n}\n\n} // namespace\n\nstruct BackgroundPreviewBox::OverridenStyle {\n\tstyle::Box box;\n\tstyle::IconButton toggle;\n\tstyle::MediaSlider slider;\n\tstyle::FlatLabel subtitle;\n};\n\nBackgroundPreviewBox::BackgroundPreviewBox(\n\tQWidget*,\n\tnot_null<Window::SessionController*> controller,\n\tconst Data::WallPaper &paper,\n\tBackgroundPreviewArgs args)\n: SimpleElementDelegate(controller, [=] { update(); })\n, _controller(controller)\n, _forPeer(args.forPeer)\n, _fromMessageId(args.fromMessageId)\n, _chatStyle(std::make_unique<Ui::ChatStyle>(\n\tcontroller->session().colorIndicesValue()))\n, _serviceHistory(_controller->session().data().history(\n\tPeerData::kServiceNotificationsId))\n, _service(nullptr)\n, _text1(GenerateTextItem(\n\tdelegate(),\n\t_serviceHistory,\n\t(_forPeer\n\t\t? tr::lng_background_apply1(tr::now)\n\t\t: tr::lng_background_text1(tr::now)),\n\tfalse))\n, _text2(GenerateTextItem(\n\tdelegate(),\n\t_serviceHistory,\n\t(_forPeer\n\t\t? tr::lng_background_apply2(tr::now)\n\t\t: tr::lng_background_text2(tr::now)),\n\ttrue))\n, _paperEmojiId(paper.emojiId())\n, _paper(\n\tResolve(&controller->session(), paper, Window::Theme::IsNightMode()))\n, _media(_paper.document() ? _paper.document()->createMediaView() : nullptr)\n, _radial([=](crl::time now) { radialAnimationCallback(now); })\n, _appNightMode(Window::Theme::IsNightModeValue())\n, _boxDarkMode(_appNightMode.current())\n, _dimmingIntensity(std::clamp(_paper.patternIntensity(), 0, 100))\n, _dimmed(_forPeer\n\t&& (_paper.document() || _paper.localThumbnail())\n\t&& !_paper.isPattern()) {\n\tif (_media) {\n\t\t_media->thumbnailWanted(_paper.fileOrigin());\n\t}\n\tgenerateBackground();\n\t_controller->session().downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\t_appNightMode.changes(\n\t) | rpl::on_next([=](bool night) {\n\t\t_boxDarkMode = night;\n\t\tupdate();\n\t}, lifetime());\n\n\t_boxDarkMode.changes(\n\t) | rpl::on_next([=](bool dark) {\n\t\tapplyDarkMode(dark);\n\t}, lifetime());\n\n\tconst auto prepare = [=](bool dark, auto pointer) {\n\t\tconst auto weak = base::make_weak(this);\n\t\tcrl::async([=] {\n\t\t\tauto result = std::make_unique<style::palette>();\n\t\t\tWindow::Theme::PreparePaletteCallback(dark, {})(*result);\n\t\t\tcrl::on_main([=, result = std::move(result)]() mutable {\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->*pointer = std::move(result);\n\t\t\t\t\tstrong->paletteReady();\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t};\n\tprepare(false, &BackgroundPreviewBox::_lightPalette);\n\tprepare(true, &BackgroundPreviewBox::_darkPalette);\n}\n\nBackgroundPreviewBox::~BackgroundPreviewBox() = default;\n\nvoid BackgroundPreviewBox::recreate(bool dark) {\n\t_paper = Resolve(\n\t\t&_controller->session(),\n\t\tData::WallPaper::FromEmojiId(_paperEmojiId),\n\t\tdark);\n\t_media = _paper.document()\n\t\t? _paper.document()->createMediaView()\n\t\t: nullptr;\n\tif (_media) {\n\t\t_media->thumbnailWanted(_paper.fileOrigin());\n\t}\n\t_full = QImage();\n\t_generated = _scaled = _blurred = _fadeOutThumbnail = QPixmap();\n\t_generating = {};\n\tgenerateBackground();\n\t_paper.loadDocument();\n\tif (const auto document = _paper.document()) {\n\t\tif (document->loading()) {\n\t\t\t_radial.start(_media->progress());\n\t\t}\n\t}\n\tcheckLoadedDocument();\n\tupdateServiceBg(_paper.backgroundColors());\n\tupdate();\n}\n\nvoid BackgroundPreviewBox::applyDarkMode(bool dark) {\n\tif (!_paperEmojiId.isEmpty()) {\n\t\trecreate(dark);\n\t}\n\tconst auto equals = (dark == Window::Theme::IsNightMode());\n\tconst auto &palette = (dark ? _darkPalette : _lightPalette);\n\tif (!equals && !palette) {\n\t\t_waitingForPalette = true;\n\t\treturn;\n\t}\n\t_waitingForPalette = false;\n\tif (equals) {\n\t\tsetStyle(st::defaultBox);\n\t\t_chatStyle->applyCustomPalette(nullptr);\n\t\t_paletteServiceBg = rpl::single(\n\t\t\trpl::empty\n\t\t) | rpl::then(\n\t\t\tstyle::PaletteChanged()\n\t\t) | rpl::map([=] {\n\t\t\treturn st::msgServiceBg->c;\n\t\t});\n\t} else {\n\t\tsetStyle(overridenStyle(dark));\n\t\t_chatStyle->applyCustomPalette(palette.get());\n\t\t_paletteServiceBg = palette->msgServiceBg()->c;\n\t}\n\tresetTitle();\n\trebuildButtons(dark);\n\tupdate();\n\tif (const auto parent = parentWidget()) {\n\t\tparent->update();\n\t}\n\n\tif (_dimmed) {\n\t\tcreateDimmingSlider(dark);\n\t}\n}\n\nvoid BackgroundPreviewBox::createDimmingSlider(bool dark) {\n\tconst auto created = !_dimmingWrap;\n\tif (created) {\n\t\t_dimmingWrap.create(this, object_ptr<Ui::RpWidget>(this));\n\t\t_dimmingContent = _dimmingWrap->entity();\n\t}\n\t_dimmingSlider = nullptr;\n\tfor (const auto &child : _dimmingContent->children()) {\n\t\tif (child->isWidgetType()) {\n\t\t\tstatic_cast<QWidget*>(child)->hide();\n\t\t\tchild->deleteLater();\n\t\t}\n\t}\n\tconst auto equals = (dark == Window::Theme::IsNightMode());\n\tconst auto inner = Ui::CreateChild<Ui::VerticalLayout>(_dimmingContent);\n\tinner->show();\n\tUi::AddSubsectionTitle(\n\t\tinner,\n\t\ttr::lng_background_dimming(),\n\t\tstyle::margins(0, st::defaultVerticalListSkip, 0, 0),\n\t\tequals ? nullptr : dark ? &_dark->subtitle : &_light->subtitle);\n\t_dimmingSlider = inner->add(\n\t\tobject_ptr<Ui::MediaSlider>(\n\t\t\tinner,\n\t\t\t(equals\n\t\t\t\t? st::defaultContinuousSlider\n\t\t\t\t: dark\n\t\t\t\t? _dark->slider\n\t\t\t\t: _light->slider)),\n\t\tst::localStorageLimitMargin);\n\t_dimmingSlider->setValue(_dimmingIntensity / 100.);\n\t_dimmingSlider->setAlwaysDisplayMarker(true);\n\t_dimmingSlider->resize(st::defaultContinuousSlider.seekSize);\n\tconst auto handle = [=](float64 value) {\n\t\tconst auto intensity = std::clamp(\n\t\t\tint(base::SafeRound(value * 100)),\n\t\t\t0,\n\t\t\t100);\n\t\t_paper = _paper.withPatternIntensity(intensity);\n\t\t_dimmingIntensity = intensity;\n\t\tupdate();\n\t};\n\t_dimmingSlider->setChangeProgressCallback(handle);\n\t_dimmingSlider->setChangeFinishedCallback(handle);\n\tinner->resizeToWidth(st::boxWideWidth);\n\tUi::SendPendingMoveResizeEvents(inner);\n\tinner->move(0, 0);\n\t_dimmingContent->resize(inner->size());\n\n\t_dimmingContent->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(_dimmingContent);\n\t\tconst auto palette = (dark ? _darkPalette : _lightPalette).get();\n\t\tp.fillRect(clip, equals ? st::boxBg : palette->boxBg());\n\t}, _dimmingContent->lifetime());\n\n\t_dimmingToggleScheduled = true;\n\n\tif (created) {\n\t\trpl::combine(\n\t\t\theightValue(),\n\t\t\t_dimmingWrap->heightValue(),\n\t\t\trpl::mappers::_1 - rpl::mappers::_2\n\t\t) | rpl::on_next([=](int top) {\n\t\t\t_dimmingWrap->move(0, top);\n\t\t}, _dimmingWrap->lifetime());\n\n\t\t_dimmingWrap->toggle(dark, anim::type::instant);\n\t\t_dimmingHeight = _dimmingWrap->heightValue();\n\t\t_dimmingHeight.changes() | rpl::on_next([=] {\n\t\t\tupdate();\n\t\t}, _dimmingWrap->lifetime());\n\t}\n}\n\nvoid BackgroundPreviewBox::paletteReady() {\n\tif (_waitingForPalette) {\n\t\tapplyDarkMode(_boxDarkMode.current());\n\t}\n}\n\nconst style::Box &BackgroundPreviewBox::overridenStyle(bool dark) {\n\tauto &st = dark ? _dark : _light;\n\tif (!st) {\n\t\tst = std::make_unique<OverridenStyle>(prepareOverridenStyle(dark));\n\t}\n\treturn st->box;\n}\n\nauto BackgroundPreviewBox::prepareOverridenStyle(bool dark)\n-> OverridenStyle {\n\tconst auto p = (dark ? _darkPalette : _lightPalette).get();\n\tAssert(p != nullptr);\n\n\tconst auto &toggle = dark\n\t\t? st::backgroundSwitchToLight\n\t\t: st::backgroundSwitchToDark;\n\tauto result = OverridenStyle{\n\t\t.box = st::defaultBox,\n\t\t.toggle = toggle,\n\t\t.slider = st::defaultContinuousSlider,\n\t\t.subtitle = st::defaultSubsectionTitle,\n\t};\n\tresult.box.button.textFg = p->lightButtonFg();\n\tresult.box.button.textFgOver = p->lightButtonFgOver();\n\tresult.box.button.numbersTextFg = p->lightButtonFg();\n\tresult.box.button.numbersTextFgOver = p->lightButtonFgOver();\n\tresult.box.button.textBg = p->lightButtonBg();\n\tresult.box.button.textBgOver = p->lightButtonBgOver();\n\tresult.box.button.ripple.color = p->lightButtonBgRipple();\n\tresult.box.title.textFg = p->boxTitleFg();\n\tresult.box.bg = p->boxBg();\n\tresult.box.titleAdditionalFg = p->boxTitleAdditionalFg();\n\n\tresult.toggle.ripple.color = p->windowBgOver();\n\tresult.toggle.icon = toggle.icon.withPalette(*p);\n\tresult.toggle.iconOver = toggle.iconOver.withPalette(*p);\n\n\tresult.slider.activeFg = p->mediaPlayerActiveFg();\n\tresult.slider.inactiveFg = p->mediaPlayerInactiveFg();\n\tresult.slider.activeFgOver = p->mediaPlayerActiveFg();\n\tresult.slider.inactiveFgOver = p->mediaPlayerInactiveFg();\n\tresult.slider.activeFgDisabled = p->mediaPlayerInactiveFg();\n\tresult.slider.inactiveFgDisabled = p->windowBg();\n\tresult.slider.receivedTillFg = p->mediaPlayerInactiveFg();\n\n\tresult.subtitle.textFg = p->windowActiveTextFg();\n\n\treturn result;\n}\n\nbool BackgroundPreviewBox::forChannel() const {\n\treturn _forPeer && _forPeer->isChannel();\n}\n\nbool BackgroundPreviewBox::forGroup() const {\n\treturn forChannel() && _forPeer->isMegagroup();\n}\n\nvoid BackgroundPreviewBox::generateBackground() {\n\tif (_paper.backgroundColors().empty()) {\n\t\treturn;\n\t}\n\tconst auto size = QSize(st::boxWideWidth, st::boxWideWidth)\n\t\t* style::DevicePixelRatio();\n\t_generated = Ui::PixmapFromImage((_paper.patternOpacity() >= 0.)\n\t\t? Ui::GenerateBackgroundImage(\n\t\t\tsize,\n\t\t\t_paper.backgroundColors(),\n\t\t\t_paper.gradientRotation())\n\t\t: BlackImage(size));\n\t_generated.setDevicePixelRatio(style::DevicePixelRatio());\n}\n\nnot_null<HistoryView::ElementDelegate*> BackgroundPreviewBox::delegate() {\n\treturn static_cast<HistoryView::ElementDelegate*>(this);\n}\n\nvoid BackgroundPreviewBox::resetTitle() {\n\tsetTitle(tr::lng_background_header());\n}\n\nvoid BackgroundPreviewBox::rebuildButtons(bool dark) {\n\tclearButtons();\n\taddButton(forGroup()\n\t\t? tr::lng_background_apply_group()\n\t\t: forChannel()\n\t\t? tr::lng_background_apply_channel()\n\t\t: _forPeer\n\t\t? tr::lng_background_apply_button()\n\t\t: tr::lng_settings_apply(), [=] { apply(); });\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\tif (!_forPeer && _paper.hasShareUrl()) {\n\t\taddLeftButton(tr::lng_background_share(), [=] { share(); });\n\t}\n\tconst auto equals = (dark == Window::Theme::IsNightMode());\n\tauto toggle = object_ptr<Ui::IconButton>(this, equals\n\t\t? (dark ? st::backgroundSwitchToLight : st::backgroundSwitchToDark)\n\t\t: dark ? _dark->toggle : _light->toggle);\n\ttoggle->setClickedCallback([=] {\n\t\t_boxDarkMode = !_boxDarkMode.current();\n\t});\n\taddTopButton(std::move(toggle));\n}\n\nvoid BackgroundPreviewBox::prepare() {\n\tapplyDarkMode(Window::Theme::IsNightMode());\n\n\t_paper.loadDocument();\n\tif (const auto document = _paper.document()) {\n\t\tif (document->loading()) {\n\t\t\t_radial.start(_media->progress());\n\t\t}\n\t}\n\n\tupdateServiceBg(_paper.backgroundColors());\n\n\tsetScaledFromThumb();\n\tcheckLoadedDocument();\n\n\t_text1->setDisplayDate(false);\n\t_text1->initDimensions();\n\t_text1->resizeGetHeight(st::boxWideWidth);\n\t_text2->initDimensions();\n\t_text2->resizeGetHeight(st::boxWideWidth);\n\n\tsetDimensions(st::boxWideWidth, st::boxWideWidth);\n}\n\nvoid BackgroundPreviewBox::recreateBlurCheckbox() {\n\tconst auto document = _paper.document();\n\tif (_paper.isPattern()\n\t\t|| (!_paper.localThumbnail()\n\t\t\t&& (!document || !document->hasThumbnail()))) {\n\t\treturn;\n\t}\n\n\tconst auto blurred = _blur ? _blur->checked() : _paper.isBlurred();\n\t_blur = Ui::MakeChatServiceCheckbox(\n\t\tthis,\n\t\ttr::lng_background_blur(tr::now),\n\t\tst::backgroundCheckbox,\n\t\tst::backgroundCheck,\n\t\tblurred,\n\t\t[=] { return _serviceBg.value_or(QColor(255, 255, 255, 0)); });\n\t_blur->show();\n\n\trpl::combine(\n\t\tsizeValue(),\n\t\t_blur->sizeValue(),\n\t\t_dimmingHeight.value()\n\t) | rpl::on_next([=](QSize outer, QSize inner, int dimming) {\n\t\tconst auto bottom = st::historyPaddingBottom;\n\t\t_blur->move(\n\t\t\t(outer.width() - inner.width()) / 2,\n\t\t\touter.height() - dimming - bottom - inner.height());\n\t}, _blur->lifetime());\n\n\t_blur->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tcheckBlurAnimationStart();\n\t\tupdate();\n\t}, _blur->lifetime());\n\n\t_blur->setDisabled(_paper.document() && _full.isNull());\n\n\tif (_forBothOverlay) {\n\t\t_forBothOverlay->raise();\n\t}\n}\n\nvoid BackgroundPreviewBox::apply() {\n\tif (_forPeer) {\n\t\tapplyForPeer();\n\t} else {\n\t\tapplyForEveryone();\n\t}\n}\n\nvoid BackgroundPreviewBox::uploadForPeer(bool both) {\n\tExpects(_forPeer != nullptr);\n\n\tif (_uploadId) {\n\t\treturn;\n\t}\n\n\tconst auto session = &_controller->session();\n\tconst auto ready = Window::Theme::PrepareWallPaper(\n\t\tsession->mainDcId(),\n\t\t_paper.localThumbnail()->original());\n\tconst auto documentId = ready->id;\n\t_uploadId = FullMsgId(\n\t\tsession->userPeerId(),\n\t\tsession->data().nextLocalMessageId());\n\tsession->uploader().upload(_uploadId, ready);\n\tif (_uploadLifetime) {\n\t\treturn;\n\t}\n\n\tconst auto document = session->data().document(documentId);\n\tdocument->uploadingData = std::make_unique<Data::UploadState>(\n\t\tdocument->size);\n\n\tsession->uploader().documentProgress(\n\t) | rpl::on_next([=](const FullMsgId &fullId) {\n\t\tif (fullId != _uploadId) {\n\t\t\treturn;\n\t\t}\n\t\t_uploadProgress = document->uploading()\n\t\t\t? ((document->uploadingData->offset * 100)\n\t\t\t\t/ document->uploadingData->size)\n\t\t\t: 0.;\n\t\tupdate(radialRect());\n\t}, _uploadLifetime);\n\n\tsession->uploader().documentReady(\n\t) | rpl::on_next([=](const Storage::UploadedMedia &data) {\n\t\tif (data.fullId != _uploadId) {\n\t\t\treturn;\n\t\t}\n\t\t_uploadProgress = 1.;\n\t\t_uploadLifetime.destroy();\n\t\tupdate(radialRect());\n\t\tsession->api().request(MTPaccount_UploadWallPaper(\n\t\t\tMTP_flags(MTPaccount_UploadWallPaper::Flag::f_for_chat),\n\t\t\tdata.info.file,\n\t\t\tMTP_string(\"image/jpeg\"),\n\t\t\t_paper.mtpSettings()\n\t\t)).done([=](const MTPWallPaper &result) {\n\t\t\tresult.match([&](const MTPDwallPaper &data) {\n\t\t\t\tsession->data().documentConvert(\n\t\t\t\t\tsession->data().document(documentId),\n\t\t\t\t\tdata.vdocument());\n\t\t\t}, [&](const MTPDwallPaperNoFile &data) {\n\t\t\t\tLOG((\"API Error: \"\n\t\t\t\t\t\"Got wallPaperNoFile after account.UploadWallPaper.\"));\n\t\t\t});\n\t\t\tif (const auto paper = Data::WallPaper::Create(session, result)) {\n\t\t\t\tsetExistingForPeer(*paper, both);\n\t\t\t}\n\t\t}).send();\n\t}, _uploadLifetime);\n\n\t_uploadProgress = 0.;\n\t_radial.start(_uploadProgress);\n}\n\nvoid BackgroundPreviewBox::setExistingForPeer(\n\t\tconst Data::WallPaper &paper,\n\t\tbool both) {\n\tExpects(_forPeer != nullptr);\n\n\tif (const auto already = _forPeer->wallPaper()) {\n\t\tif (already->equals(paper)) {\n\t\t\t_controller->finishChatThemeEdit(_forPeer);\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto api = &_controller->session().api();\n\tusing Flag = MTPmessages_SetChatWallPaper::Flag;\n\tapi->request(MTPmessages_SetChatWallPaper(\n\t\tMTP_flags((_fromMessageId ? Flag::f_id : Flag())\n\t\t\t| (_fromMessageId ? Flag() : Flag::f_wallpaper)\n\t\t\t| (both ? Flag::f_for_both : Flag())\n\t\t\t| Flag::f_settings),\n\t\t_forPeer->input(),\n\t\tpaper.mtpInput(&_controller->session()),\n\t\tpaper.mtpSettings(),\n\t\tMTP_int(_fromMessageId.msg)\n\t)).done([=](const MTPUpdates &result) {\n\t\tapi->applyUpdates(result);\n\t}).send();\n\n\t_forPeer->setWallPaper(paper);\n\t_controller->finishChatThemeEdit(_forPeer);\n}\n\nvoid BackgroundPreviewBox::checkLevelForChannel() {\n\tExpects(forChannel());\n\n\tconst auto show = _controller->uiShow();\n\t_forPeerLevelCheck = true;\n\tconst auto weak = base::make_weak(this);\n\tCheckBoostLevel(show, _forPeer, [=](int level) {\n\t\tif (!weak) {\n\t\t\treturn std::optional<Ui::AskBoostReason>();\n\t\t}\n\t\tconst auto limits = Data::LevelLimits(&_forPeer->session());\n\t\tconst auto required = _paperEmojiId.isEmpty()\n\t\t\t? limits.channelCustomWallpaperLevelMin()\n\t\t\t: limits.channelWallpaperLevelMin();\n\t\tif (level >= required) {\n\t\t\tapplyForPeer(false);\n\t\t\treturn std::optional<Ui::AskBoostReason>();\n\t\t}\n\t\treturn std::make_optional(Ui::AskBoostReason{\n\t\t\tUi::AskBoostWallpaper{ required, _forPeer->isMegagroup()}\n\t\t});\n\t}, [=] { _forPeerLevelCheck = false; });\n}\n\nvoid BackgroundPreviewBox::applyForPeer() {\n\tExpects(_forPeer != nullptr);\n\n\tif (!Data::IsCustomWallPaper(_paper)) {\n\t\tif (const auto already = _forPeer->wallPaper()) {\n\t\t\tif (already->equals(_paper)) {\n\t\t\t\t_controller->finishChatThemeEdit(_forPeer);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (forChannel()) {\n\t\tcheckLevelForChannel();\n\t\treturn;\n\t} else if (_fromMessageId || !_forPeer->session().premiumPossible()) {\n\t\tapplyForPeer(false);\n\t\treturn;\n\t} else if (_forBothOverlay) {\n\t\treturn;\n\t}\n\tconst auto size = this->size() * style::DevicePixelRatio();\n\tconst auto bg = Images::DitherImage(\n\t\tImages::BlurLargeImage(\n\t\t\tUi::GrabWidgetToImage(this).scaled(\n\t\t\t\tsize / style::ConvertScale(4),\n\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\tQt::SmoothTransformation),\n\t\t\t24).scaled(\n\t\t\t\tsize,\n\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\tQt::SmoothTransformation));\n\n\t_forBothOverlay = std::make_unique<Ui::FadeWrap<>>(\n\t\tthis,\n\t\tobject_ptr<Ui::RpWidget>(this));\n\tconst auto overlay = _forBothOverlay->entity();\n\n\tsizeValue() | rpl::on_next([=](QSize size) {\n\t\t_forBothOverlay->setGeometry({ QPoint(), size });\n\t\toverlay->setGeometry({ QPoint(), size });\n\t}, _forBothOverlay->lifetime());\n\n\toverlay->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(overlay);\n\t\tp.drawImage(0, 0, bg);\n\t\tp.fillRect(clip, QColor(0, 0, 0, 64));\n\t}, overlay->lifetime());\n\n\tusing namespace Ui;\n\tconst auto forMe = CreateChild<RoundButton>(\n\t\toverlay,\n\t\ttr::lng_background_apply_me(),\n\t\tst::backgroundConfirm);\n\tforMe->setClickedCallback([=] {\n\t\tapplyForPeer(false);\n\t});\n\tusing namespace rpl::mappers;\n\tconst auto forBoth = ::Settings::CreateLockedButton(\n\t\toverlay,\n\t\ttr::lng_background_apply_both(\n\t\t\tlt_user,\n\t\t\trpl::single(_forPeer->shortName())),\n\t\tst::backgroundConfirm,\n\t\tData::AmPremiumValue(&_forPeer->session()) | rpl::map(!_1));\n\tforBoth->setClickedCallback([=] {\n\t\tif (_forPeer->session().premium()) {\n\t\t\tapplyForPeer(true);\n\t\t} else {\n\t\t\tShowPremiumPreviewBox(\n\t\t\t\t_controller->uiShow(),\n\t\t\t\tPremiumFeature::Wallpapers);\n\t\t}\n\t});\n\tconst auto cancel = CreateChild<RoundButton>(\n\t\toverlay,\n\t\ttr::lng_cancel(),\n\t\tst::backgroundConfirmCancel);\n\tcancel->setClickedCallback([=] {\n\t\tconst auto raw = _forBothOverlay.release();\n\t\traw->shownValue() | rpl::filter(\n\t\t\t!rpl::mappers::_1\n\t\t) | rpl::take(1) | rpl::on_next(crl::guard(raw, [=] {\n\t\t\tdelete raw;\n\t\t}), raw->lifetime());\n\t\traw->toggle(false, anim::type::normal);\n\t});\n\toverlay->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tconst auto padding = st::backgroundConfirmPadding;\n\t\tconst auto width = size.width()\n\t\t\t- padding.left()\n\t\t\t- padding.right();\n\t\tconst auto height = cancel->height();\n\t\tauto top = size.height() - padding.bottom() - height;\n\t\tcancel->setGeometry(padding.left(), top, width, height);\n\t\ttop -= height + padding.top();\n\t\tforBoth->setGeometry(padding.left(), top, width, height);\n\t\ttop -= height + padding.top();\n\t\tforMe->setGeometry(padding.left(), top, width, height);\n\t}, _forBothOverlay->lifetime());\n\n\t_forBothOverlay->hide(anim::type::instant);\n\t_forBothOverlay->show(anim::type::normal);\n}\n\nvoid BackgroundPreviewBox::applyForPeer(bool both) {\n\tusing namespace Data;\n\tif (forChannel() && !_paperEmojiId.isEmpty()) {\n\t\tsetExistingForPeer(WallPaper::FromEmojiId(_paperEmojiId), both);\n\t} else if (IsCustomWallPaper(_paper)) {\n\t\tuploadForPeer(both);\n\t} else {\n\t\tsetExistingForPeer(_paper, both);\n\t}\n}\n\nvoid BackgroundPreviewBox::applyForEveryone() {\n\tconst auto install = (_paper.id() != Window::Theme::Background()->id())\n\t\t&& Data::IsCloudWallPaper(_paper);\n\t_controller->content()->setChatBackground(_paper, std::move(_full));\n\tif (install) {\n\t\t_controller->session().api().request(MTPaccount_InstallWallPaper(\n\t\t\t_paper.mtpInput(&_controller->session()),\n\t\t\t_paper.mtpSettings()\n\t\t)).send();\n\t}\n\tcloseBox();\n}\n\nvoid BackgroundPreviewBox::share() {\n\tQGuiApplication::clipboard()->setText(\n\t\t_paper.shareUrl(&_controller->session()));\n\tshowToast(tr::lng_background_link_copied(tr::now));\n}\n\nvoid BackgroundPreviewBox::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tconst auto ms = crl::now();\n\tif (_scaled.isNull()) {\n\t\tsetScaledFromThumb();\n\t}\n\tif (!_generated.isNull()\n\t\t&& (_scaled.isNull()\n\t\t\t|| (_fadeOutThumbnail.isNull() && _fadeIn.animating()))) {\n\t\tp.drawPixmap(0, 0, _generated);\n\t}\n\tif (!_scaled.isNull()) {\n\t\tpaintImage(p);\n\t\tconst auto dimming = (_dimmed && _boxDarkMode.current())\n\t\t\t? _dimmingIntensity\n\t\t\t: 0;\n\t\tif (dimming > 0) {\n\t\t\tconst auto alpha = 255 * dimming / 100;\n\t\t\tp.fillRect(e->rect(), QColor(0, 0, 0, alpha));\n\t\t}\n\t\tpaintRadial(p);\n\t} else if (_generated.isNull()) {\n\t\tp.fillRect(e->rect(), st::boxBg);\n\t\treturn;\n\t} else {\n\t\t// Progress of pattern loading.\n\t\tpaintRadial(p);\n\t}\n\tpaintTexts(p, ms);\n\tif (_dimmingToggleScheduled) {\n\t\tcrl::on_main(this, [=] {\n\t\t\tif (!_dimmingToggleScheduled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_dimmingToggleScheduled = false;\n\t\t\t_dimmingWrap->toggle(_boxDarkMode.current(), anim::type::normal);\n\t\t});\n\t}\n}\n\nvoid BackgroundPreviewBox::paintImage(Painter &p) {\n\tExpects(!_scaled.isNull());\n\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto size = st::boxWideWidth;\n\tconst auto from = QRect(\n\t\t0,\n\t\t(size - height()) / 2 * factor,\n\t\tsize * factor,\n\t\theight() * factor);\n\tconst auto guard = gsl::finally([&] { p.setOpacity(1.); });\n\n\tconst auto fade = _fadeIn.value(1.);\n\tif (fade < 1. && !_fadeOutThumbnail.isNull()) {\n\t\tp.drawPixmap(rect(), _fadeOutThumbnail, from);\n\t}\n\tconst auto &pixmap = (!_blurred.isNull() && _paper.isBlurred())\n\t\t? _blurred\n\t\t: _scaled;\n\tp.setOpacity(fade);\n\tp.drawPixmap(rect(), pixmap, from);\n\tcheckBlurAnimationStart();\n}\n\nvoid BackgroundPreviewBox::paintRadial(Painter &p) {\n\tconst auto radial = _radial.animating();\n\tconst auto radialOpacity = radial ? _radial.opacity() : 0.;\n\tif (!radial) {\n\t\treturn;\n\t}\n\tauto inner = radialRect();\n\n\tp.setPen(Qt::NoPen);\n\tp.setOpacity(radialOpacity);\n\tp.setBrush(st::radialBg);\n\n\t{\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.drawEllipse(inner);\n\t}\n\n\tp.setOpacity(1);\n\tQRect arc(inner.marginsRemoved(QMargins(st::radialLine, st::radialLine, st::radialLine, st::radialLine)));\n\t_radial.draw(p, arc, st::radialLine, st::radialFg);\n}\n\nint BackgroundPreviewBox::textsTop() const {\n\tconst auto bottom = _blur\n\t\t? _blur->y()\n\t\t: (height() - _dimmingHeight.current());\n\treturn bottom\n\t\t- st::historyPaddingBottom\n\t\t- (_service ? _service->height() : 0)\n\t\t- _text1->height()\n\t\t- (forChannel() ? 0 : _text2->height());\n}\n\nQRect BackgroundPreviewBox::radialRect() const {\n\tconst auto available = textsTop() - st::historyPaddingBottom;\n\treturn QRect(\n\t\tQPoint(\n\t\t\t(width() - st::radialSize.width()) / 2,\n\t\t\t(available - st::radialSize.height()) / 2),\n\t\tst::radialSize);\n}\n\nvoid BackgroundPreviewBox::paintTexts(Painter &p, crl::time ms) {\n\tconst auto heights = _service ? _service->height() : 0;\n\tconst auto height1 = _text1->height();\n\tconst auto height2 = _text2->height();\n\tauto context = _controller->defaultChatTheme()->preparePaintContext(\n\t\t_chatStyle.get(),\n\t\trect(),\n\t\trect(),\n\t\trect(),\n\t\t_controller->isGifPausedAtLeastFor(Window::GifPauseReason::Layer));\n\tp.translate(0, textsTop());\n\tif (_service) {\n\t\t_service->draw(p, context);\n\t\tp.translate(0, heights);\n\t}\n\n\tcontext.outbg = _text1->hasOutLayout();\n\t_text1->draw(p, context);\n\tp.translate(0, height1);\n\tif (!forChannel()) {\n\t\tcontext.outbg = _text2->hasOutLayout();\n\t\t_text2->draw(p, context);\n\t\tp.translate(0, height2);\n\t}\n}\n\nvoid BackgroundPreviewBox::radialAnimationCallback(crl::time now) {\n\tconst auto document = _paper.document();\n\tconst auto wasAnimating = _radial.animating();\n\tconst auto updated = _uploadId\n\t\t? _radial.update(_uploadProgress, !_uploadLifetime, now)\n\t\t: _radial.update(_media->progress(), !document->loading(), now);\n\tif ((wasAnimating || _radial.animating())\n\t\t&& (!anim::Disabled() || updated)) {\n\t\tupdate(radialRect());\n\t}\n\tcheckLoadedDocument();\n}\n\nvoid BackgroundPreviewBox::setScaledFromThumb() {\n\tif (!_scaled.isNull()) {\n\t\treturn;\n\t}\n\tconst auto localThumbnail = _paper.localThumbnail();\n\tconst auto thumbnail = localThumbnail\n\t\t? localThumbnail\n\t\t: _media\n\t\t? _media->thumbnail()\n\t\t: nullptr;\n\tif (!thumbnail) {\n\t\treturn;\n\t} else if (_paper.isPattern() && _paper.document() != nullptr) {\n\t\treturn;\n\t}\n\tauto scaled = PrepareScaledFromFull(\n\t\tthumbnail->original(),\n\t\t_paper.isPattern(),\n\t\t_paper.backgroundColors(),\n\t\t_paper.gradientRotation(),\n\t\t_paper.patternOpacity(),\n\t\t_paper.document() ? Images::Option::Blur : Images::Option());\n\tauto blurred = (_paper.document() || _paper.isPattern())\n\t\t? QImage()\n\t\t: PrepareScaledNonPattern(\n\t\t\tUi::PrepareBlurredBackground(thumbnail->original()),\n\t\t\tImages::Option(0));\n\tsetScaledFromImage(std::move(scaled), std::move(blurred));\n}\n\nvoid BackgroundPreviewBox::setScaledFromImage(\n\t\tQImage &&image,\n\t\tQImage &&blurred) {\n\tupdateServiceBg({ Ui::CountAverageColor(image) });\n\tif (!_full.isNull()) {\n\t\tstartFadeInFrom(std::move(_scaled));\n\t}\n\t_scaled = Ui::PixmapFromImage(std::move(image));\n\t_blurred = Ui::PixmapFromImage(std::move(blurred));\n\tif (_blur) {\n\t\t_blur->setDisabled(_paper.document() && _full.isNull());\n\t}\n}\n\nvoid BackgroundPreviewBox::startFadeInFrom(QPixmap previous) {\n\t_fadeOutThumbnail = std::move(previous);\n\t_fadeIn.start([=] { update(); }, 0., 1., st::backgroundCheck.duration);\n}\n\nvoid BackgroundPreviewBox::checkBlurAnimationStart() {\n\tif (_fadeIn.animating()\n\t\t|| _blurred.isNull()\n\t\t|| !_blur\n\t\t|| _paper.isBlurred() == _blur->checked()) {\n\t\treturn;\n\t}\n\t_paper = _paper.withBlurred(_blur->checked());\n\tstartFadeInFrom(_paper.isBlurred() ? _scaled : _blurred);\n}\n\nvoid BackgroundPreviewBox::updateServiceBg(const std::vector<QColor> &bg) {\n\tconst auto count = int(bg.size());\n\tif (!count) {\n\t\treturn;\n\t}\n\tauto red = 0LL, green = 0LL, blue = 0LL;\n\tfor (const auto &color : bg) {\n\t\tred += color.red();\n\t\tgreen += color.green();\n\t\tblue += color.blue();\n\t}\n\n\t_serviceBgLifetime = _paletteServiceBg.value(\n\t) | rpl::on_next([=](QColor color) {\n\t\t_serviceBg = Ui::ThemeAdjustedColor(\n\t\t\tcolor,\n\t\t\tQColor(red / count, green / count, blue / count));\n\t\t_chatStyle->applyAdjustedServiceBg(*_serviceBg);\n\t\trecreateBlurCheckbox();\n\t});\n\n\t_service = GenerateServiceItem(\n\t\tdelegate(),\n\t\t_serviceHistory,\n\t\t(forGroup()\n\t\t\t? tr::lng_background_other_group(tr::now)\n\t\t\t: forChannel()\n\t\t\t? tr::lng_background_other_channel(tr::now)\n\t\t\t: (_forPeer\n\t\t\t\t&& !_fromMessageId\n\t\t\t\t&& !_forPeer->starsPerMessageChecked())\n\t\t\t? tr::lng_background_other_info(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\t_forPeer->shortName())\n\t\t\t: ItemDateText(_text1->data(), false)),\n\t\tfalse);\n\t_service->initDimensions();\n\t_service->resizeGetHeight(st::boxWideWidth);\n}\n\nvoid BackgroundPreviewBox::checkLoadedDocument() {\n\tconst auto document = _paper.document();\n\tif (!_full.isNull()\n\t\t|| !document\n\t\t|| !_media->loaded(true)\n\t\t|| _generating) {\n\t\treturn;\n\t}\n\tconst auto generateCallback = [=](QImage &&image) {\n\t\tif (image.isNull()) {\n\t\t\treturn;\n\t\t}\n\t\tcrl::async([\n\t\t\tthis,\n\t\t\timage = std::move(image),\n\t\t\tisPattern = _paper.isPattern(),\n\t\t\tbackground = _paper.backgroundColors(),\n\t\t\tgradientRotation = _paper.gradientRotation(),\n\t\t\tpatternOpacity = _paper.patternOpacity(),\n\t\t\tguard = _generating.make_guard()\n\t\t]() mutable {\n\t\t\tauto scaled = PrepareScaledFromFull(\n\t\t\t\timage,\n\t\t\t\tisPattern,\n\t\t\t\tbackground,\n\t\t\t\tgradientRotation,\n\t\t\t\tpatternOpacity);\n\t\t\tauto blurred = !isPattern\n\t\t\t\t? PrepareScaledNonPattern(\n\t\t\t\t\tUi::PrepareBlurredBackground(image),\n\t\t\t\t\tImages::Option(0))\n\t\t\t\t: QImage();\n\t\t\tcrl::on_main(std::move(guard), [\n\t\t\t\tthis,\n\t\t\t\timage = std::move(image),\n\t\t\t\tscaled = std::move(scaled),\n\t\t\t\tblurred = std::move(blurred)\n\t\t\t]() mutable {\n\t\t\t\t_full = std::move(image);\n\t\t\t\tsetScaledFromImage(std::move(scaled), std::move(blurred));\n\t\t\t\tupdate();\n\t\t\t});\n\t\t});\n\t};\n\t_generating = Data::ReadBackgroundImageAsync(\n\t\t_media.get(),\n\t\tUi::PreprocessBackgroundImage,\n\t\tgenerateCallback);\n}\n\nbool BackgroundPreviewBox::Start(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &slug,\n\t\tconst QMap<QString, QString> &params) {\n\tif (const auto paper = Data::WallPaper::FromColorsSlug(slug)) {\n\t\tcontroller->show(Box<BackgroundPreviewBox>(\n\t\t\tcontroller,\n\t\t\tpaper->withUrlParams(params)));\n\t\treturn true;\n\t}\n\tif (!IsValidWallPaperSlug(slug)) {\n\t\tcontroller->show(Ui::MakeInformBox(tr::lng_background_bad_link()));\n\t\treturn false;\n\t}\n\tcontroller->session().api().requestWallPaper(slug, crl::guard(controller, [=](\n\t\t\tconst Data::WallPaper &result) {\n\t\tcontroller->show(Box<BackgroundPreviewBox>(\n\t\t\tcontroller,\n\t\t\tresult.withUrlParams(params)));\n\t}), crl::guard(controller, [=] {\n\t\tcontroller->show(Ui::MakeInformBox(tr::lng_background_bad_link()));\n\t}));\n\treturn true;\n}\n\nHistoryView::Context BackgroundPreviewBox::elementContext() {\n\treturn HistoryView::Context::ContactPreview;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/background_preview_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n#include \"base/binary_guard.h\"\n#include \"history/admin_log/history_admin_log_item.h\"\n#include \"history/view/history_view_element.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"data/data_wall_paper.h\"\n\nnamespace Data {\nclass DocumentMedia;\n} // namespace Data\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass Checkbox;\nclass ChatStyle;\nclass MediaSlider;\ntemplate <typename Widget>\nclass SlideWrap;\ntemplate <typename Widget>\nclass FadeWrap;\n} // namespace Ui\n\nstruct BackgroundPreviewArgs {\n\tPeerData *forPeer = nullptr;\n\tFullMsgId fromMessageId;\n};\n\nclass BackgroundPreviewBox\n\t: public Ui::BoxContent\n\t, private HistoryView::SimpleElementDelegate {\npublic:\n\tBackgroundPreviewBox(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst Data::WallPaper &paper,\n\t\tBackgroundPreviewArgs args = {});\n\t~BackgroundPreviewBox();\n\n\tstatic bool Start(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &slug,\n\t\tconst QMap<QString, QString> &params);\n\nprotected:\n\tvoid prepare() override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tstruct OverridenStyle;\n\n\tusing Element = HistoryView::Element;\n\tnot_null<HistoryView::ElementDelegate*> delegate();\n\tHistoryView::Context elementContext() override;\n\n\tvoid apply();\n\tvoid applyForPeer();\n\tvoid applyForPeer(bool both);\n\tvoid applyForEveryone();\n\tvoid uploadForPeer(bool both);\n\tvoid setExistingForPeer(const Data::WallPaper &paper, bool both);\n\tvoid share();\n\tvoid radialAnimationCallback(crl::time now);\n\tQRect radialRect() const;\n\n\tvoid generateBackground();\n\tvoid checkLoadedDocument();\n\tvoid setScaledFromThumb();\n\tvoid setScaledFromImage(QImage &&image, QImage &&blurred);\n\tvoid updateServiceBg(const std::vector<QColor> &bg);\n\tvoid paintImage(Painter &p);\n\tvoid paintRadial(Painter &p);\n\tvoid paintTexts(Painter &p, crl::time ms);\n\tvoid recreateBlurCheckbox();\n\tint textsTop() const;\n\tvoid startFadeInFrom(QPixmap previous);\n\tvoid checkBlurAnimationStart();\n\n\t[[nodiscard]] const style::Box &overridenStyle(bool dark);\n\tvoid paletteReady();\n\tvoid applyDarkMode(bool dark);\n\t[[nodiscard]] OverridenStyle prepareOverridenStyle(bool dark);\n\n\t[[nodiscard]] bool forChannel() const;\n\t[[nodiscard]] bool forGroup() const;\n\tvoid checkLevelForChannel();\n\n\tvoid recreate(bool dark);\n\tvoid resetTitle();\n\tvoid rebuildButtons(bool dark);\n\tvoid createDimmingSlider(bool dark);\n\n\tconst not_null<Window::SessionController*> _controller;\n\tPeerData * const _forPeer = nullptr;\n\tbool _forPeerLevelCheck = false;\n\tFullMsgId _fromMessageId;\n\tstd::unique_ptr<Ui::ChatStyle> _chatStyle;\n\tconst not_null<History*> _serviceHistory;\n\tAdminLog::OwnedItem _service;\n\tAdminLog::OwnedItem _text1;\n\tAdminLog::OwnedItem _text2;\n\tQString _paperEmojiId;\n\tData::WallPaper _paper;\n\tstd::shared_ptr<Data::DocumentMedia> _media;\n\tQImage _full;\n\tQPixmap _generated, _scaled, _blurred, _fadeOutThumbnail;\n\tUi::Animations::Simple _fadeIn;\n\tUi::RadialAnimation _radial;\n\tbase::binary_guard _generating;\n\tstd::optional<QColor> _serviceBg;\n\tobject_ptr<Ui::Checkbox> _blur = { nullptr };\n\n\trpl::variable<bool> _appNightMode;\n\trpl::variable<bool> _boxDarkMode;\n\tstd::unique_ptr<OverridenStyle> _light, _dark;\n\tstd::unique_ptr<style::palette> _lightPalette, _darkPalette;\n\tbool _waitingForPalette = false;\n\n\tobject_ptr<Ui::SlideWrap<Ui::RpWidget>> _dimmingWrap = { nullptr };\n\tUi::RpWidget *_dimmingContent = nullptr;\n\tUi::MediaSlider *_dimmingSlider = nullptr;\n\tint _dimmingIntensity = 0;\n\trpl::variable<int> _dimmingHeight = 0;\n\tbool _dimmed = false;\n\tbool _dimmingToggleScheduled = false;\n\n\tFullMsgId _uploadId;\n\tfloat64 _uploadProgress = 0.;\n\trpl::lifetime _uploadLifetime;\n\n\tstd::unique_ptr<Ui::FadeWrap<Ui::RpWidget>> _forBothOverlay;\n\n\trpl::variable<QColor> _paletteServiceBg;\n\trpl::lifetime _serviceBgLifetime;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/boxes.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\n\nusing \"ui/layers/layers.style\";\nusing \"ui/widgets/widgets.style\";\nusing \"intro/intro.style\";\n\nUserpicButton {\n\tsize: size;\n\tphotoSize: pixels;\n\tphotoPosition: point;\n\tchangeButton: RoundButton;\n\tchangeIcon: icon;\n\tchangeIconPosition: point;\n\tduration: int;\n\tuploadHeight: pixels;\n\tuploadBg: color;\n\tuploadIcon: icon;\n\tuploadIconPosition: point;\n\tuploadProgressLine: pixels;\n\tuploadProgressMargin: pixels;\n}\nUserpicsRow {\n\tbutton: UserpicButton;\n\tbg: color;\n\tshift: pixels;\n\tstroke: pixels;\n\tcomplex: bool;\n\tinvert: bool;\n}\nShortInfoBox {\n\tlabel: FlatLabel;\n\tlabeled: FlatLabel;\n\tlabeledOneLine: FlatLabel;\n}\n\ncountryRowHeight: 36px;\ncountryRowNameFont: semiboldFont;\ncountryRowNameFg: boxTextFg;\ncountryRowPadding: margins(22px, 9px, 8px, 0px);\ncountryRowCodeFont: font(fsize);\ncountryRowBg: windowBg;\ncountryRowBgOver: windowBgOver;\ncountryRowCodeFg: windowSubTextFg;\ncountryRowCodeFgOver: windowSubTextFgOver;\ncountriesSkip: 12px;\ncountriesScroll: ScrollArea(boxScroll) {\n\tdeltat: 9px;\n\tdeltab: 3px;\n}\n\nboxPhotoTitlePosition: point(28px, 20px);\nboxPhotoPadding: margins(28px, 28px, 28px, 18px);\nboxPhotoCompressedSkip: 20px;\nboxPhotoCaptionSkip: 8px;\n\ndefaultChangeUserpicIcon: icon {{ \"new_chat_photo\", activeButtonFg }};\ndefaultUploadUserpicIcon: icon {{ \"upload_chat_photo\", msgDateImgFg }};\ndefaultUserpicButton: UserpicButton {\n\tsize: size(72px, 72px);\n\tphotoSize: 72px;\n\tphotoPosition: point(-1px, -1px);\n\tchangeButton: defaultActiveButton;\n\tchangeIcon: defaultChangeUserpicIcon;\n\tchangeIconPosition: point(21px, 23px);\n\tduration: 500;\n\tuploadHeight: 24px;\n\tuploadBg: msgDateImgBgOver;\n\tuploadIcon: defaultUploadUserpicIcon;\n\tuploadIconPosition: point(-1px, 1px);\n\tuploadProgressLine: 3px;\n\tuploadProgressMargin: 8px;\n}\nuploadUserpicSize: 32px;\nuploadUserpicButton: UserpicButton(defaultUserpicButton) {\n\tsize: size(uploadUserpicSize, uploadUserpicSize);\n\tphotoSize: uploadUserpicSize;\n\tchangeIcon: icon {{ \"settings/photo\", activeButtonFg }};\n\tchangeIconPosition: point(4px, 4px);\n}\nuploadUserpicButtonBorder: 2px;\nuserpicUploadCancel: icon {{ \"history_file_cancel\", historyFileThumbIconFg }};\nrestoreUserpicIcon: UserpicButton(defaultUserpicButton) {\n\tsize: size(22px, 22px);\n\tphotoSize: 22px;\n}\n\nconfirmInviteTitle: FlatLabel(defaultFlatLabel) {\n\talign: align(top);\n\tminWidth: 320px;\n\tmaxHeight: 24px;\n\ttextFg: windowBoldFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(18px semibold);\n\t}\n}\nconfirmInviteAbout: FlatLabel(boxLabel) {\n\talign: align(top);\n\tminWidth: 320px;\n\tstyle: TextStyle(boxLabelStyle) {\n\t\tlineHeight: 19px;\n\t}\n}\nconfirmInviteStatus: FlatLabel(confirmInviteAbout) {\n\ttextFg: windowSubTextFg;\n\tstyle: boxLabelStyle;\n\tmaxHeight: 0px;\n}\nconfirmInviteAboutPadding: margins(36px, 4px, 36px, 10px);\nconfirmInviteAboutRequestsPadding: margins(36px, 9px, 36px, 15px);\nconfirmInviteTitleTop: 141px;\nconfirmInvitePhotoSize: 96px;\nconfirmInvitePhotoTop: 33px;\nconfirmInviteStatusTop: 164px;\nconfirmInviteUserHeight: 100px;\nconfirmInviteUserPhotoSize: 50px;\nconfirmInviteUserPhotoTop: 210px;\nconfirmInviteUsersWidth: 320px;\nconfirmInviteUserName: FlatLabel(defaultFlatLabel) {\n\talign: align(center);\n\tminWidth: 66px;\n\tmaxHeight: 20px;\n}\nconfirmInviteUserNameTop: 264px;\n\nconfirmPhoneAboutLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 272px;\n}\nconfirmPhoneCodeField: InputField(defaultInputField) {\n}\n\naboutRevokePublicLabel: FlatLabel(defaultFlatLabel) {\n\talign: align(topleft);\n\tminWidth: 320px;\n}\n\ncontactUserIcon: icon {{ \"settings/settings_name\", menuIconFg }};\ncontactPhoneIcon: icon {{ \"settings/settings_phone_number\", menuIconFg }};\ncontactIconPosition: point(-5px, 23px);\n\ncontactPadding: margins(49px, 2px, 0px, 14px);\ncontactSkip: 9px;\ncontactPhoneSkip: 30px;\n\ncontactsPhotoSize: 42px;\ncontactsPadding: margins(16px, 7px, 16px, 7px);\ncontactsNameTop: 2px;\ncontactsNameStyle: TextStyle(defaultTextStyle) {\n\tfont: semiboldFont;\n}\ncontactsStatusTop: 23px;\ncontactsStatusFont: font(fsize);\ncontactsCheckPosition: point(8px, 16px);\n\ncontactsSortButton: IconButton(defaultIconButton) {\n\twidth: 48px;\n\theight: 54px;\n\ticon: icon{{ \"contacts_alphabet\", boxTitleCloseFg }};\n\ticonOver: icon{{ \"contacts_alphabet\", boxTitleCloseFgOver }};\n\ticonPosition: point(10px, -1px);\n\trippleAreaPosition: point(1px, 6px);\n\trippleAreaSize: 42px;\n\tripple: defaultRippleAnimationBgOver;\n}\ncontactsSortOnlineIcon: icon{{ \"contacts_online\", boxTitleCloseFg }};\ncontactsSortOnlineIconOver: icon{{ \"contacts_online\", boxTitleCloseFgOver }};\n\nmembersMarginTop: 10px;\nmembersMarginBottom: 10px;\n\npeerListBoxItem: PeerListItem(defaultPeerListItem) {\n\theight: 56px;\n\tphotoSize: contactsPhotoSize;\n\tphotoPosition: point(16px, 7px);\n\tnamePosition: point(74px, 9px);\n\tstatusPosition: point(74px, 30px);\n\tbutton: OutlineButton(defaultPeerListButton) {\n\t\ttextBg: contactsBg;\n\t\ttextBgOver: contactsBgOver;\n\t\tripple: defaultRippleAnimation;\n\t}\n\tstatusFg: contactsStatusFg;\n\tstatusFgOver: contactsStatusFgOver;\n\tstatusFgActive: contactsStatusFgOnline;\n}\npeerListBox: PeerList(defaultPeerList) {\n\tpadding: margins(\n\t\t0px,\n\t\tmembersMarginTop,\n\t\t0px,\n\t\tmembersMarginBottom);\n\titem: peerListBoxItem;\n}\n\nlocalStorageRowHeight: 50px;\nlocalStorageRowPadding: margins(22px, 5px, 20px, 5px);\nlocalStorageRowTitle: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowBoldFg;\n\tmaxHeight: 20px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px semibold);\n\t}\n}\nlocalStorageRowSize: FlatLabel(defaultFlatLabel) {\n\ttextFg: contactsStatusFg;\n\tmaxHeight: 20px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px);\n\t}\n}\nlocalStorageClear: defaultBoxButton;\nlocalStorageLimitLabel: LabelSimple(defaultLabelSimple) {\n\tfont: boxTextFont;\n}\nlocalStorageLimitLabelMargin: margins(22px, 10px, 20px, 5px);\nlocalStorageLimitSlider: MediaSlider(defaultContinuousSlider) {\n\tseekSize: size(15px, 15px);\n}\nlocalStorageLimitMargin: margins(22px, 5px, 20px, 10px);\n\nshareRowsTop: 12px;\nshareRowHeight: 108px;\nsharePhotoTop: 6px;\nshareBoxListItem: PeerListItem(defaultPeerListItem) {\n\tnameStyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(11px);\n\t}\n\tnameFg: windowFg;\n\tnameFgChecked: windowActiveTextFg;\n\tcheckbox: RoundImageCheckbox(defaultPeerListCheckbox) {\n\t\timageRadius: 28px;\n\t\timageSmallRadius: 24px;\n\t}\n}\nshareBoxList: PeerList(defaultPeerList) {\n\tbg: boxBg;\n\titem: shareBoxListItem;\n}\nshareNameTop: 6px;\nshareColumnSkip: 6px;\nshareActivateDuration: 150;\nshareScrollDuration: 300;\nshareComment: InputField(defaultInputField) {\n\tstyle: defaultTextStyle;\n\ttextMargins: margins(8px, 8px, 8px, 6px);\n\theightMin: 36px;\n\theightMax: 72px;\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(2px, 0px, 2px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n\tborder: 0px;\n\tborderActive: 0px;\n}\nshareCommentPadding: margins(5px, 5px, 5px, 5px);\n\nnotificationsBoxMonitor: icon {{ \"monitor\", notificationsBoxMonitorFg }};\nnotificationsBoxScreenTop: 10px;\nnotificationsBoxScreenSize: size(280px, 160px);\n\nnotificationsSampleSkip: 5px;\nnotificationsSampleTopSkip: 5px;\nnotificationsSampleBottomSkip: 5px;\nnotificationsSampleMargin: 2px;\n\nnotificationSampleOpacity: 0.5;\nnotificationSampleSize: size(64px, 16px);\n\nmembersAboutLimitPadding: margins(0px, 16px, 0px, 16px);\nmembersAbout: FlatLabel(defaultFlatLabel) {\n\tminWidth: 240px;\n\ttextFg: membersAboutLimitFg;\n\talign: align(top);\n\tstyle: boxLabelStyle;\n}\n\nfragmentBoxButton: RoundButton(introFragmentButton) {\n\twidth: 256px;\n}\n\npasscodeHeaderFont: font(19px);\npasscodeHeaderHeight: 80px;\npasscodeInput: InputField(introPhone) {\n\ttextMargins: margins(1px, 27px, 1px, 6px);\n}\npasscodeSubmit: RoundButton(introNextButton) {\n\twidth: 225px;\n}\npasscodeSubmitSkip: 40px;\npasscodePadding: margins(0px, 0px, 0px, 5px);\npasscodeTextLine: 28px;\npasscodeLittleSkip: 5px;\npasscodeAboutSkip: 7px;\npasscodeSkip: 23px;\npasscodeSystemUnlock: IconButton(defaultIconButton) {\n\twidth: 32px;\n\theight: 36px;\n\ticon: icon{{ \"menu/passcode_winhello\", lightButtonFg }};\n\ticonOver: icon{{ \"menu/passcode_winhello\", lightButtonFg }};\n\ticonPosition: point(4px, 4px);\n\trippleAreaSize: 32px;\n\trippleAreaPosition: point(0px, 0px);\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: lightButtonBgOver;\n\t}\n}\npasscodeSystemTouchID: icon{{ \"menu/passcode_finger\", lightButtonFg }};\npasscodeSystemAppleWatch: icon{{ \"menu/passcode_watch\", lightButtonFg }};\npasscodeSystemSystemPwd: icon{{ \"menu/permissions\", lightButtonFg }};\npasscodeSystemUnlockLater: FlatLabel(defaultFlatLabel) {\n\talign: align(top);\n\ttextFg: windowSubTextFg;\n}\npasscodeSystemUnlockSkip: 12px;\n\nnewGroupAboutFg: windowSubTextFg;\nnewGroupPadding: margins(4px, 6px, 4px, 3px);\nnewGroupSkip: 27px;\nnewGroupInfoPadding: margins(0px, -4px, 0px, 1px);\n\nnewGroupLinkPadding: margins(4px, 27px, 4px, 21px);\nnewGroupLinkTop: 3px;\nnewGroupLinkFont: font(16px);\n\nnewGroupNamePosition: point(27px, 5px);\n\nnewGroupDescriptionPadding: margins(0px, 13px, 0px, 4px);\nnewGroupDescription: InputField(defaultInputField) {\n\ttextMargins: margins(1px, 26px, 1px, 4px);\n\theightMax: 116px;\n}\n\nsetupChannelLink: InputField(defaultInputField) {\n\ttextMargins: margins(0px, 6px, 0px, 4px);\n\theightMin: 32px;\n}\n\nthemeWarningWidth: boxWideWidth;\nthemeWarningHeight: 150px;\nthemeWarningTextTop: 60px;\n\naboutWidth: 390px;\naboutVersionTop: -3px;\naboutVersionLink: LinkButton(defaultLinkButton) {\n\tcolor: windowSubTextFg;\n\toverColor: windowSubTextFg;\n}\naboutTopSkip: 19px;\naboutSkip: 14px;\naboutLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 300px;\n\talign: align(topleft);\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tlineHeight: 22px;\n\t}\n}\n\nautoDownloadTitleFont: font(15px semibold);\nautoDownloadLimitSlider: MediaSlider(defaultContinuousSlider) {\n\tseekSize: size(15px, 15px);\n}\nautoDownloadLimitPadding: margins(22px, 8px, 22px, 8px);\n\nconfirmMaxHeight: 245px;\n\nsupportInfoField: InputField(defaultInputField) {\n\theightMax: 256px;\n}\n\nconnectionHostInputField: InputField(defaultInputField) {\n\twidth: 160px;\n}\nconnectionPortInputField: InputField(defaultInputField) {\n\twidth: 55px;\n}\nconnectionUserInputField: InputField(defaultInputField) {\n}\nconnectionPasswordInputField: InputField(defaultInputField) {\n}\nconnectionIPv6Skip: 11px;\n\nautolockWidth: 256px;\nautolockButton: Checkbox(defaultBoxCheckbox) {\n\twidth: 200px;\n}\n\nlangsRadio: Radio(defaultRadio) {\n\tbg: boxBg;\n}\n\nbackgroundPadding: 10px;\nbackgroundSize: size(108px, 193px);\nbackgroundScroll: ScrollArea(boxScroll) {\n\tdeltax: 3px;\n\twidth: 10px;\n\tdeltat: 10px;\n\tdeltab: 10px;\n}\n\nsendMediaPreviewSize: 308px;\nsendMediaPreviewHeightMax: 1280;\nsendMediaRowSkip: 10px;\n\neditMediaHintLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n\tminWidth: sendMediaPreviewSize;\n}\n\ncalendarTitleHeight: boxTitleHeight;\ncalendarPrevious: IconButton {\n\twidth: calendarTitleHeight;\n\theight: calendarTitleHeight;\n\n\ticon: icon {{ \"calendar_down-flip_vertical\", boxTitleFg }};\n\ticonPosition: point(-1px, -1px);\n\n\trippleAreaPosition: point(2px, 2px);\n\trippleAreaSize: 44px;\n\tripple: defaultRippleAnimationBgOver;\n}\ncalendarPreviousDisabled: icon {{ \"calendar_down-flip_vertical\", menuIconFg }};\ncalendarNext: IconButton(calendarPrevious) {\n\ticon: icon {{ \"calendar_down\", boxTitleFg }};\n}\nCalendarSizes {\n\twidth: pixels;\n\tdaysHeight: pixels;\n\tcellSize: size;\n\tcellInner: pixels;\n\tpadding: margins;\n}\ncalendarNextDisabled: icon {{ \"calendar_down\", menuIconFg }};\ncalendarTitleFont: boxTitleFont;\ndefaultCalendarSizes: CalendarSizes {\n\twidth: boxWideWidth;\n\tdaysHeight: 40px;\n\tcellSize: size(48px, 40px);\n\tcellInner: 34px;\n\tpadding: margins(14px, 0px, 14px, 0px);\n}\ncalendarDaysFont: normalFont;\ncalendarDaysFg: boxTitleAdditionalFg;\ncalendarScroll: ScrollArea(defaultSolidScroll) {\n\tdeltat: 3px;\n\tdeltab: 3px;\n\tround: 1px;\n\twidth: 8px;\n\tdeltax: 3px;\n\thiding: 1000;\n}\nCalendarColors {\n\tdayTextColor: color;\n\tdayTextGrayedOutColor: color;\n\n\ticonButtonNext: IconButton;\n\ticonButtonNextDisabled: icon;\n\ticonButtonPrevious: IconButton;\n\ticonButtonPreviousDisabled: icon;\n\n\ticonButtonRippleColorDisabled: color;\n\n\trippleColor: color;\n\trippleColorHighlighted: color;\n\trippleGrayedOutColor: color;\n\n\ttitleTextColor: color;\n}\n\ndefaultCalendarColors: CalendarColors {\n\tdayTextColor: boxTextFg;\n\tdayTextGrayedOutColor: windowSubTextFg;\n\n\ticonButtonNext: calendarNext;\n\ticonButtonNextDisabled: calendarNextDisabled;\n\ticonButtonPrevious: calendarPrevious;\n\ticonButtonPreviousDisabled: calendarPreviousDisabled;\n\n\ticonButtonRippleColorDisabled: boxBg;\n\n\trippleColor: windowBgOver;\n\trippleColorHighlighted: dialogsRippleBgActive;\n\trippleGrayedOutColor: windowBgRipple;\n\n\ttitleTextColor: boxTitleFg;\n}\n\npasscodeTextStyle: TextStyle(defaultTextStyle) {\n\tlineHeight: 20px;\n}\n\nusernamePadding: margins(24px, 2px, 24px, 10px);\nusernameSkip: 44px;\nusernameDefaultFg: windowSubTextFg;\n\neditMediaLabelMargins: margins(0px, 11px, 0px, 0px);\neditMediaCheckboxMargins: margins(0px, 15px, 23px, 15px);\n\ndownloadPathSkip: 10px;\n\ncolorEditWidth: 390px;\ncolorEditSkip: 10px;\ncolorPickerSize: 256px;\ncolorPickerMarkRadius: 6px;\ncolorPickerMarkLine: 1px;\ncolorSliderSkip: 8px;\ncolorSliderArrowLeft: icon {{ \"color_slider_arrow\", sliderBgActive }};\ncolorSliderArrowRight: icon {{ \"color_slider_arrow-flip_horizontal\", sliderBgActive }};\ncolorSliderArrowTop: icon {{ \"color_slider_arrow_vertical\", sliderBgActive }};\ncolorSliderArrowBottom: icon {{ \"color_slider_arrow_vertical-flip_vertical\", sliderBgActive }};\ncolorSliderWidth: 19px;\ncolorSampleSize: size(60px, 34px);\ncolorFieldSkip: 13px;\ncolorValueInput: InputField(defaultInputField) {\n\ttextMargins: margins(16px, 3px, 0px, 2px);\n\theightMin: 27px;\n}\ncolorResultInput: InputField(colorValueInput) {\n}\n\nchangePhoneButton: RoundButton(defaultActiveButton) {\n\twidth: 256px;\n}\nchangePhoneButtonPadding: margins(0px, 32px, 0px, 44px);\nchangePhoneTitle: FlatLabel(boxTitle) {\n}\nchangePhoneTitlePadding: margins(0px, 8px, 0px, 8px);\nchangePhoneDescription: FlatLabel(defaultFlatLabel) {\n\tminWidth: 332px;\n\talign: align(top);\n\ttextFg: windowSubTextFg;\n}\nchangePhoneDescriptionPadding: margins(0px, 1px, 0px, 8px);\nchangePhoneIconPadding: margins(0px, 39px, 0px, 5px);\nchangePhoneLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 275px;\n\ttextFg: windowSubTextFg;\n}\nchangePhoneError: FlatLabel(changePhoneLabel) {\n\ttextFg: boxTextFgError;\n}\n\nnormalBoxLottieSize: size(120px, 120px);\n\nadminLogFilterUserpicLeft: 15px;\nadminLogFilterLittleSkip: 16px;\nadminLogFilterCheckbox: Checkbox(defaultBoxCheckbox) {\n\tstyle: TextStyle(boxTextStyle) {\n\t\tfont: font(boxFontSize semibold);\n\t}\n}\nadminLogFilterSkip: 32px;\nadminLogFilterUserCheckbox: Checkbox(defaultBoxCheckbox) {\n\tmargin: margins(8px, 6px, 8px, 6px);\n\tcheckPosition: point(8px, 6px);\n}\nrightsCheckbox: Checkbox(defaultCheckbox) {\n\ttextPosition: point(10px, 1px);\n\trippleBg: attentionButtonBgOver;\n}\nrightsToggle: Toggle(defaultToggle) {\n\ttoggledFg: windowBgActive;\n\tuntoggledFg: attentionButtonFg;\n\tlockIcon: icon {{ \"info/info_rights_lock\", windowBgActive }};\n\txsize: 8px;\n\tvsize: 5px;\n\tvshift: 1px;\n\tstroke: 2px;\n\tduration: universalDuration;\n}\n\nrightsButton: SettingsButton(defaultSettingsButton) {\n\tpadding: margins(22px, 8px, 22px, 8px);\n\n\ttoggle: rightsToggle;\n\ttoggleOver: rightsToggle;\n\ttoggleSkip: 20px;\n}\nrightsButtonToggleWidth: 70px;\nrightsDividerMargin: margins(0px, 0px, 0px, 20px);\nrightsHeaderMargin: margins(22px, 13px, 22px, 7px);\nrightsToggleMargin: margins(22px, 8px, 22px, 8px);\nrightsAboutMargin: margins(22px, 8px, 22px, 8px);\nrightsPhotoButton: UserpicButton(defaultUserpicButton) {\n\tsize: size(60px, 60px);\n\tphotoSize: 60px;\n}\nrightsPhotoMargin: margins(20px, 0px, 15px, 18px);\nrightsNameStyle: TextStyle(semiboldTextStyle) {\n\tfont: font(15px semibold);\n}\nrightsNameTop: 8px;\nrightsStatusTop: 32px;\nrightsHeaderLabel: FlatLabel(boxLabel) {\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(boxFontSize semibold);\n\t}\n\ttextFg: windowActiveTextFg;\n}\n\ngroupStickersRemove: defaultMultiSelectSearchCancel;\ngroupStickersRemovePosition: point(6px, 6px);\ngroupStickersFieldPadding: margins(8px, 6px, 8px, 6px);\ngroupStickersField: InputField(defaultMultiSelectSearchField) {\n\tplaceholderFont: boxTextFont;\n\tstyle: boxTextStyle;\n\tplaceholderMargins: margins(0px, 0px, 0px, 0px);\n\ttextMargins: margins(0px, 7px, 0px, 0px);\n\ttextBg: boxBg;\n\theightMin: 32px;\n}\ngroupStickersSubTitleHeight: 48px;\n\nproxyUsePadding: margins(22px, 6px, 22px, 5px);\nproxyTryIPv6Padding: margins(22px, 8px, 22px, 5px);\nproxyRowPadding: margins(22px, 8px, 8px, 8px);\nproxyRowIconSkip: 32px;\nproxyRowSkip: 2px;\nproxyRowRipple: defaultRippleAnimationBgOver;\nproxyRowTitleFg: windowFg;\nproxyRowTitlePalette: TextPalette(defaultTextPalette) {\n\tlinkFg: windowSubTextFg;\n}\nproxyRowTitleStyle: TextStyle(defaultTextStyle) {\n\tfont: semiboldFont;\n}\nproxyRowStatusFg: windowSubTextFg;\nproxyRowStatusFgOnline: windowActiveTextFg;\nproxyRowStatusFgOffline: boxTextFgError;\nproxyRowStatusFgAvailable: boxTextFgGood;\n\nproxyEditTitle: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: autoDownloadTitleFont;\n\t}\n\ttextFg: boxTitleFg;\n}\nproxyEditTitlePadding: margins(22px, 16px, 22px, 0px);\nproxyEditTypePadding: margins(22px, 4px, 22px, 8px);\nproxyEditInputPadding: margins(22px, 0px, 22px, 0px);\nproxyEditSkip: 16px;\n\nproxyEmptyListLabel: FlatLabel(defaultFlatLabel) {\n\talign: align(top);\n\ttextFg: windowSubTextFg;\n}\nproxyEmptyListPadding: margins(22px, 48px, 22px, 0px);\nproxyCheckingPosition: point(2px, 5px);\nproxyCheckingSkip: 6px;\nproxyCheckingAnimation: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {\n\tcolor: windowSubTextFg;\n\tthickness: 1px;\n\tsize: size(8px, 8px);\n}\nproxyDropdownDownPosition: point(2px, 35px);\nproxyDropdownUpPosition: point(2px, 20px);\n\nproxyAboutPadding: margins(22px, 7px, 22px, 14px);\nproxyAboutSponsorPadding: margins(22px, 7px, 22px, 0px);\n\nproxyApplyBoxLabel : FlatLabel(defaultFlatLabel) {\n\tmaxHeight: 30px;\n}\nproxyApplyBoxTable: Table(defaultTable) {\n\tlabelMinWidth: 91px;\n}\nproxyApplyBoxTableMargin: margins(24px, 4px, 24px, 4px);\nproxyApplyBoxTableLabelMargin: margins(13px, 10px, 13px, 10px);\nproxyApplyBoxTableValueMargin: margins(13px, 9px, 13px, 9px);\nproxyApplyBoxValueMultiline: FlatLabel(defaultTableValue) {\n\tminWidth: 128px;\n\tmaxHeight: 100px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(10px);\n\t\tlinkUnderline: kLinkUnderlineNever;\n\t}\n}\nproxyApplyBoxSponsorLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowBoldFg;\n\tminWidth: 100px;\n\talign: align(top);\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(11px);\n\t}\n}\nproxyApplyBoxSponsorMargin: margins(13px, 8px, 13px, 8px);\nproxyApplyBoxButton: RoundButton(defaultActiveButton) {\n\theight: 38px;\n\ttextTop: 10px;\n\tstyle: semiboldTextStyle;\n}\nproxyApplyBox: Box(defaultBox) {\n\tbuttonPadding: margins(22px, 11px, 22px, 22px);\n\tbuttonHeight: 38px;\n\tbuttonWide: true;\n\tbutton: proxyApplyBoxButton;\n\tshadowIgnoreTopSkip: true;\n}\n\nmarkdownLinkFieldPadding: margins(22px, 0px, 22px, 10px);\n\ntermsContent: FlatLabel(defaultFlatLabel) {\n\tminWidth: 285px;\n}\ntermsPadding: margins(22px, 4px, 16px, 16px);\ntermsAgePadding: margins(22px, 16px, 16px, 0px);\n\nthemesSmallSkip: 10px;\nthemesMenuToggle: IconButton(defaultIconButton) {\n\twidth: 44px;\n\theight: 44px;\n\n\ticon: menuToggleIcon;\n\ticonOver: menuToggleIconOver;\n\ticonPosition: point(-1px, -1px);\n\n\trippleAreaPosition: point(4px, 4px);\n\trippleAreaSize: 36px;\n\tripple: defaultRippleAnimationBgOver;\n}\nthemesMenuPosition: point(2px, 25px);\n\nsendGifWithCaptionEmojiPosition: point(-30px, 23px);\n\nnotesFieldWithEmoji: InputField(defaultInputField) {\n\ttextMargins: margins(0px, 28px, 30px, 4px);\n//\tborder: 0px;\n//\tborderActive: 0px;\n}\n\nbackgroundCheckbox: Checkbox(defaultCheckbox) {\n\ttextFg: msgServiceFg;\n\ttextFgActive: msgServiceFg;\n\n\twidth: -10px;\n\tmargin: margins(0px, 0px, 0px, 0px);\n\n\ttextPosition: point(0px, 6px);\n\tcheckPosition: point(0px, 0px);\n\n\tstyle: semiboldTextStyle;\n}\n\nbackgroundCheck: ServiceCheck {\n\tmargin: margins(10px, 6px, 8px, 6px);\n\tdiameter: 18px;\n\tshift: 2px;\n\tthickness: 2px;\n\ttip: point(7px, 13px);\n\tsmall: 3px;\n\tlarge: 6px;\n\tstroke: 2px;\n\tcolor: msgServiceFg;\n\tduration: 200;\n}\nbackgroundConfirmPadding: margins(24px, 16px, 24px, 16px);\nbackgroundConfirm: RoundButton(defaultActiveButton) {\n\theight: 44px;\n\ttextTop: 12px;\n\tstyle: semiboldTextStyle;\n}\nbackgroundConfirmCancel: RoundButton(backgroundConfirm) {\n\ttextFg: mediaviewSaveMsgFg;\n\ttextFgOver: mediaviewSaveMsgFg;\n\tnumbersTextFg: mediaviewSaveMsgFg;\n\tnumbersTextFgOver: mediaviewSaveMsgFg;\n\ttextBg: shadowFg;\n\ttextBgOver: shadowFg;\n\n\theight: 44px;\n\ttextTop: 12px;\n\tstyle: semiboldTextStyle;\n\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: shadowFg;\n\t}\n}\nurlAuthBox: Box(defaultBox) {\n\tbuttonPadding: margins(0px, 0px, 0px, 12px);\n\tbuttonHeight: 0px;\n}\nurlAuthCheckbox: Checkbox(defaultBoxCheckbox) {\n\twidth: 240px;\n}\nurlAuthCodesTitle: FlatLabel(defaultPeerListAbout) {\n\ttextFg: boxTitleFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: boxTitleFont;\n\t}\n}\nurlAuthCheckboxAbout: FlatLabel(defaultPeerListAbout) {\n\tstyle: TextStyle(boxTextStyle) {\n\t\tfont: font(12px);\n\t}\n}\nurlAuthCodesButton: RoundButton(defaultLightButton) {\n\theight: 58px;\n\ttextTop: 19px;\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(20px semibold);\n\t}\n}\nurlAuthBoxRowTopLabel: FlatLabel(boxLabel) {\n\tmaxHeight: 30px;\n}\nurlAuthBoxRowBottomLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n\tmaxHeight: 30px;\n}\n\naddContactFieldMargin: margins(19px, 0px, 19px, 10px);\naddContactWarningMargin: margins(19px, 10px, 19px, 5px);\neditContactSuggestBirthday: icon{{ \"settings/birthday_add-22x22\", lightButtonFg }};\nblockUserConfirmation: FlatLabel(boxLabel) {\n\tminWidth: 240px;\n}\n\ntransferCheckWidth: 320px;\n\nslowmodeLabelsMargin: margins(0px, 5px, 0px, 0px);\nslowmodeLabel: LabelSimple(defaultLabelSimple) {\n\ttextFg: windowSubTextFg;\n}\nboostsUnrestrictLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n}\n\ncustomBadgeField: InputField(defaultInputField) {\n\ttextMargins: margins(2px, 0px, 2px, 0px);\n\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(0px, 0px, 0px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n\n\tborder: 0px;\n\tborderActive: 0px;\n\n\theightMin: 32px;\n}\n\ntagPreviewLineHeight: 8px;\ntagPreviewLineSpacing: 4px;\ntagPreviewInputSkip: 16px;\n\ninviteViaLinkButton: SettingsButton(defaultSettingsButton) {\n\ttextFg: lightButtonFg;\n\ttextFgOver: lightButtonFgOver;\n\ttextBg: windowBg;\n\ttextBgOver: windowBgOver;\n\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px semibold);\n\t}\n\n\theight: 20px;\n\tpadding: margins(74px, 8px, 8px, 9px);\n\n\tripple: defaultRippleAnimation;\n}\ninviteViaLinkIcon: icon {{ \"info/edit/group_manage_links\", lightButtonFg }};\ninviteViaLinkIconPosition: point(23px, 2px);\npeerListWithInviteViaLink: PeerList(peerListBox) {\n\tpadding: margins(\n\t\t0px,\n\t\t0px,\n\t\t0px,\n\t\tmembersMarginBottom);\n}\npeerListSingleRow: PeerList(peerListBox) {\n\tpadding: margins(0px, 0px, 0px, 0px);\n}\npeerListSmallSkips: PeerList(peerListBox) {\n\tpadding: margins(\n\t\t0px,\n\t\tdefaultVerticalListSkip,\n\t\t0px,\n\t\tdefaultVerticalListSkip);\n}\n\nscheduleHeight: 95px;\nscheduleDateTop: 38px;\nscheduleDateField: InputField(defaultInputField) {\n\ttextMargins: margins(2px, 0px, 2px, 0px);\n\tplaceholderScale: 0.;\n\theightMin: 30px;\n\ttextAlign: align(top);\n}\nscheduleTimeField: InputField(scheduleDateField) {\n\tborder: 0px;\n\tborderActive: 0px;\n\theightMin: 28px;\n\tplaceholderFont: font(14px);\n\tplaceholderFgActive: placeholderFgActive;\n}\nscheduleDateWidth: 136px;\nscheduleTimeWidth: 72px;\nscheduleAtSkip: 24px;\nscheduleAtTop: 42px;\nscheduleAtLabel: FlatLabel(defaultFlatLabel) {\n}\nscheduleTimeSeparator: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px);\n\t}\n}\nscheduleTimeSeparatorPadding: margins(2px, 0px, 2px, 0px);\nscheduleRepeatDropdownLock: IconEmoji {\n\ticon: icon {{ \"emoji/premium_lock\", windowActiveTextFg }};\n\tpadding: margins(-2px, 1px, 0px, 0px);\n}\nscheduleRepeatDropdownArrow: IconEmoji {\n\ticon: icon {{ \"intro_country_dropdown\", windowActiveTextFg }};\n\tpadding: margins(3px, 6px, 3px, 0px);\n}\n\nmuteBoxTimeField: InputField(scheduleDateField) {\n\ttextMargins: margins(0px, 0px, 0px, 0px);\n\tplaceholderMargins: margins(0px, 0px, 0px, 0px);\n\tplaceholderScale: 0.;\n\theightMin: 30px;\n\ttextAlign: align(left);\n}\nmuteBoxTimeFieldPadding: margins(5px, 0px, 5px, 0px);\n\nboxAttentionDividerLabel: FlatLabel(boxDividerLabel) {\n\ttextFg: boxTextFgError;\n}\n\nautolockDateField: InputField(scheduleDateField) {\n\theightMin: 22px;\n}\nautolockTimeField: InputField(scheduleTimeField) {\n\theightMin: 20px;\n}\nautolockTimeWidth: 52px;\n\nsponsoredUrlButtonSkip: 11px;\nsponsoredUrlButton: RoundButton(defaultActiveButton) {\n\theight: 32px;\n\twidth: -42px;\n\ttextBg: transparent;\n\ttextBgOver: transparent;\n\tradius: roundRadiusLarge;\n\tpadding: margins(2px, 2px, 2px, 2px);\n\ttextFg: historyLinkInFg;\n\ttextFgOver: historyLinkInFg;\n\ttextTop: 7px;\n\tstyle: defaultTextStyle;\n\n\tripple: defaultRippleAnimationBgOver;\n}\n\nrequestPeerRestriction: FlatLabel(defaultFlatLabel) {\n\tminWidth: 240px;\n\ttextFg: membersAboutLimitFg;\n\tstyle: TextStyle(boxTextStyle) {\n\t\tlineHeight: 22px;\n\t}\n}\n\nrequestsBoxItem: PeerListItem(peerListBoxItem) {\n\theight: 99px;\n\tbutton: OutlineButton(defaultPeerListButton) {\n\t\ttextBg: contactsBg;\n\t\ttextBgOver: contactsBg;\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: contactsBgOver;\n\t\t}\n\t}\n}\nrequestsBoxList: PeerList(peerListBox) {\n\tpadding: margins(0px, 12px, 0px, 12px);\n\titem: requestsBoxItem;\n}\ncontactsWithStories: PeerList(peerListBox) {\n\tpadding: margins(0px, 0px, 0px, 0px);\n\titem: PeerListItem(peerListBoxItem) {\n\t\theight: 52px;\n\t\tphotoPosition: point(18px, 5px);\n\t\tnamePosition: point(70px, 7px);\n\t\tstatusPosition: point(70px, 27px);\n\n\t\tcheckbox: RoundImageCheckbox(defaultPeerListCheckbox) {\n\t\t\timageRadius: 21px;\n\t\t\timageSmallRadius: 18px;\n\t\t\tcheck: RoundCheckbox(defaultPeerListCheck) {\n\t\t\t\tsize: 0px;\n\t\t\t}\n\t\t}\n\t\tnameFgChecked: contactsNameFg;\n\t}\n}\nstoriesReadLineTwice: 2px;\nstoriesUnreadLineTwice: 4px;\nrequestsAcceptButton: RoundButton(defaultActiveButton) {\n\twidth: -28px;\n\theight: 30px;\n\ttextTop: 6px;\n}\nrequestsRejectButton: RoundButton(defaultLightButton) {\n\twidth: -28px;\n\theight: 30px;\n\ttextTop: 6px;\n}\nrequestAcceptPosition: point(71px, 58px);\nrequestButtonsSkip: 9px;\n\nringtonesBoxButton: SettingsButton(defaultSettingsButton) {\n\ttextFg: lightButtonFg;\n\ttextFgOver: lightButtonFgOver;\n\tpadding: margins(56px, 10px, 22px, 8px);\n\ticonLeft: 25px;\n}\nringtonesBoxSkip: 7px;\n\ngradientButtonGlareDuration: 700;\ngradientButtonGlareTimeout: 2000;\ngradientButtonGlareWidth: 100px;\n\ninfoLabeledOneLine: FlatLabel(defaultFlatLabel) {\n\tmaxHeight: 20px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tlineHeight: 19px;\n\t}\n\tmargin: margins(5px, 5px, 5px, 5px);\n}\ninfoLabelSkip: 2px;\ninfoLabeled: FlatLabel(infoLabeledOneLine) {\n\tminWidth: 180px;\n\tmaxHeight: 0px;\n\tmargin: margins(5px, 5px, 5px, 5px);\n}\ninfoLabel: FlatLabel(infoLabeled) {\n\ttextFg: windowSubTextFg;\n}\n\nshortInfoBox: ShortInfoBox {\n\tlabel: infoLabel;\n\tlabeled: infoLabeled;\n\tlabeledOneLine: infoLabeledOneLine;\n}\n\nbirthdayLabeled: FlatLabel(infoLabeled) {\n\tmargin: margins(0px, 0px, 0px, 0px);\n}\nbirthdayLabel: FlatLabel(infoLabel) {\n\tmargin: margins(0px, 0px, 0px, 0px);\n}\nbirthdayTodayIcon: icon {{ \"menu/gift_premium\", windowActiveTextFg }};\n\ninviteForbiddenUserpicsPadding: margins(10px, 10px, 10px, 0px);\ninviteForbiddenInfo: FlatLabel(defaultFlatLabel) {\n\tminWidth: 240px;\n\talign: align(top);\n}\ninviteForbiddenInfoPadding: margins(32px, 10px, 32px, 4px);\ninviteForbiddenSubscribePadding: margins(16px, 12px, 16px, 16px);\ninviteForbiddenOrLabelPadding: margins(32px, 0px, 32px, 0px);\ninviteForbiddenTitle: FlatLabel(boxTitle) {\n\tminWidth: 120px;\n\talign: align(top);\n}\ninviteForbiddenTitlePadding: margins(32px, 4px, 32px, 0px);\ninviteForbiddenLockBg: dialogsUnreadBgMuted;\ninviteForbiddenLockIcon: icon {{ \"emoji/premium_lock\", dialogsUnreadFg }};\n\ncollectibleIconDiameter: 72px;\ncollectibleIcon: 64px;\ncollectibleIconPadding: margins(24px, 32px, 24px, 12px);\ncollectibleHeader: FlatLabel(boxTitle) {\n\tminWidth: 120px;\n\tmaxHeight: 0px;\n\talign: align(top);\n}\ncollectibleHeaderPadding: margins(24px, 16px, 24px, 12px);\ncollectibleOwnerPadding: margins(24px, 4px, 24px, 8px);\ncollectibleInfo: inviteForbiddenInfo;\ncollectibleInfoPadding: margins(24px, 12px, 24px, 12px);\ncollectibleInfoTonMargins: margins(0px, 3px, 0px, 0px);\ncollectibleMore: RoundButton(defaultActiveButton) {\n\theight: 36px;\n\ttextTop: 9px;\n\tradius: 6px;\n}\ncollectibleMorePadding: margins(24px, 12px, 24px, 0px);\ncollectibleCopy: RoundButton(defaultLightButton) {\n\theight: 36px;\n\ttextTop: 9px;\n\tradius: 6px;\n}\ncollectibleBox: Box(defaultBox) {\n\tbuttonPadding: margins(24px, 12px, 24px, 12px);\n\tbuttonHeight: 36px;\n\tbutton: collectibleCopy;\n}\n\nmoderateBoxUserpic: UserpicButton(defaultUserpicButton) {\n\tsize: size(34px, 42px);\n\tphotoSize: 34px;\n\tphotoPosition: point(0px, 4px);\n}\nmoderateBoxExpand: icon {{ \"chat/reply_type_group\", boxTextFg }};\nmoderateBoxExpandHeight: 20px;\nmoderateBoxExpandRight: 10px;\nmoderateBoxExpandInnerSkip: 2px;\nmoderateBoxExpandFont: font(11px);\nmoderateBoxExpandToggleSize: 4px;\nmoderateBoxExpandToggleFourStrokes: 3px;\nmoderateBoxExpandIcon: IconEmoji{\n\ticon: icon{{ \"info/edit/expand_arrow_small-flip_vertical\", windowActiveTextFg }};\n\tpadding: margins(-2px, -1px, 0px, 0px);\n\tuseIconColor: true;\n}\nmoderateBoxExpandIconDown: IconEmoji{\n\ticon: icon{{ \"info/edit/expand_arrow_small\", windowActiveTextFg }};\n\tpadding: margins(-2px, -1px, 0px, 0px);\n\tuseIconColor: true;\n}\nmoderateBoxDividerLabel: FlatLabel(boxDividerLabel) {\n\tpalette: TextPalette(defaultTextPalette) {\n\t\tselectLinkFg: windowActiveTextFg;\n\t}\n}\n\nprofileQrFont: font(fsize bold);\nprofileQrCenterSize: 34px;\nprofileQrBackgroundRadius: 12px;\nprofileQrIcon: icon{{ \"qr_mini\", windowActiveTextFg }};\nprofileQrBackgroundMargins: margins(36px, 12px, 36px, 12px);\nprofileQrBackgroundPadding: margins(0px, 24px, 0px, 24px);\n\nfoldersMenu: PopupMenu(popupMenuWithIcons) {\n\tmaxHeight: 320px;\n\tmenu: Menu(menuWithIcons) {\n\t\titemPadding: margins(54px, 8px, 44px, 8px);\n\t}\n}\n\nfakeUserpicButton: UserpicButton(defaultUserpicButton) {\n\tsize: size(1px, 1px);\n\tphotoSize: 1px;\n\tchangeIcon: icon {{ \"settings/photo\", transparent }};\n\tuploadBg: transparent;\n}\n\nmoderateCommonGroupsCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) {\n\timageRadius: 12px;\n\timageSmallRadius: 11px;\n\tselectWidth: 2px;\n\tcheck: RoundCheckbox(defaultPeerListCheck) {\n\t\tsize: 16px;\n\t\tsizeSmall: 0.3;\n\t\tbgInactive: overviewCheckBg;\n\t\tbgActive: overviewCheckBgActive;\n\t\tcheck: icon {{\n\t\t\t\"default_checkbox_check\",\n\t\t\toverviewCheckFgActive,\n\t\t\tpoint(1px, 4px)\n\t\t}};\n\t}\n}\n\nfutureOwnerBox: Box(defaultBox) {\n\tbuttonPadding: margins(0px, 0px, 0px, 14px);\n\tbuttonHeight: 0px;\n}\nfutureOwnerBoxSelect: collectibleBox;\n\ndisableSharingIconPadding: margins(12px, 12px, 12px, 8px);\ndisableSharingButtonLock: IconEmoji {\n\ticon: icon {{ \"emoji/premium_lock\", activeButtonFg }};\n\tpadding: margins(-2px, 1px, 0px, 0px);\n}\n\ncreateBotBox: Box(defaultBox) {\n\tshadowIgnoreTopSkip: true;\n}\ncreateBotUserpicPadding: margins(0px, 16px, 0px, 10px);\ncreateBotTitlePadding: margins(0px, 0px, 0px, 8px);\ncreateBotSubtitlePadding: margins(0px, 0px, 0px, 4px);\ncreateBotCenteredText: FlatLabel(defaultFlatLabel) {\n\talign: align(top);\n\tminWidth: 40px;\n}\ncreateBotFieldSpacing: 8px;\ncreateBotUsernameField: InputField(defaultInputField) {\n\ttextMargins: margins(0px, 28px, 0px, 4px);\n}\ncreateBotUsernamePrefix: FlatLabel(defaultFlatLabel) {\n\tstyle: boxTextStyle;\n}\ncreateBotUsernameSuffix: FlatLabel(createBotUsernamePrefix) {\n\ttextFg: windowSubTextFg;\n}\n\nmanagedBotIconEmoji: IconEmoji {\n\ticon: icon {{ \"chat/code_tags\", windowSubTextFg }};\n\tpadding: margins(-2px, -2px, -2px, 0px);\n}\n\ncreateBotStatusLabel: FlatLabel(aboutRevokePublicLabel) {\n\tmaxHeight: 20px;\n}\n\naiComposeBoxSectionSkip: 8px;\naiComposeBoxStyleTabsSkip: 8px;\naiComposeContentMargin: margins(16px, 0px, 16px, 0px);\n\naiComposeButtonBgActive: windowActiveTextFg;\naiComposeButtonBgActiveOpacity: 0.1;\naiComposeButtonFg: windowBoldFg;\naiComposeButtonFgActive: windowActiveTextFg;\naiComposeButtonRippleInactive: RippleAnimation(defaultRippleAnimation) {\n\tcolor: windowBoldFg;\n}\naiComposeButtonRippleInactiveOpacity: 0.1;\naiComposeButtonRippleActive: RippleAnimation(defaultRippleAnimation) {\n\tcolor: windowActiveTextFg;\n}\naiComposeButtonRippleActiveOpacity: 0.1;\n\naiComposeTabsBg: boxBg;\naiComposeTabsHeight: 58px;\naiComposeTabsRadius: 29px;\naiComposeTabsPadding: margins(6px, 6px, 6px, 6px);\naiComposeTabsSkip: 4px;\naiComposeTabButtonBgActive: aiComposeButtonBgActive;\naiComposeTabLabelFg: aiComposeButtonFg;\naiComposeTabLabelFgActive: aiComposeButtonFgActive;\naiComposeTabLabelFont: font(12px semibold);\naiComposeTabIconTop: 6px;\naiComposeTabLabelTop: 26px;\naiComposeTabTranslateIcon: icon {{ \"menu/translate\", aiComposeButtonFg }};\naiComposeTabTranslateIconActive: icon {{ \"menu/translate\", aiComposeButtonFgActive }};\naiComposeTabStyleIcon: icon {{ \"menu/edit_stars\", aiComposeButtonFg }};\naiComposeTabStyleIconActive: icon {{ \"menu/edit_stars\", aiComposeButtonFgActive }};\naiComposeTabFixIcon: icon {{ \"menu/search_check\", aiComposeButtonFg }};\naiComposeTabFixIconActive: icon {{ \"menu/search_check\", aiComposeButtonFgActive }};\n\naiComposeStyleTabsBg: boxBg;\naiComposeStyleTabsHeight: 64px;\naiComposeStyleTabsRadius: 14px;\naiComposeStyleTabsPadding: margins(6px, 6px, 6px, 6px);\naiComposeStyleTabsSkip: 4px;\naiComposeStyleButtonBgActive: aiComposeButtonBgActive;\naiComposeStyleLabelFg: aiComposeButtonFg;\naiComposeStyleLabelFgActive: aiComposeButtonFgActive;\naiComposeStyleEmojiTop: 5px;\naiComposeStyleLabelTop: 30px;\naiComposeStyleLabelFont: font(12px semibold);\naiComposeStyleTabsScroll: ScrollArea(defaultScrollArea) {\n\tbarHidden: true;\n}\naiComposeStyleButtonPadding: margins(12px, 0px, 12px, 0px);\naiComposeStyleFadeWidth: 24px;\naiComposeBadge: RoundButton(customEmojiTextBadge) {\n\ttextFg: activeButtonBg;\n\ttextBg: activeButtonFg;\n\twidth: -8px;\n\theight: 16px;\n\tradius: 4px;\n\ttextTop: 1px;\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(10px semibold);\n\t}\n}\naiComposeBadgeMargin: margins(0px, 2px, 0px, 0px);\n\naiComposeCardBg: boxBg;\naiComposeCardRadius: 22px;\naiComposeCardPadding: margins(12px, 16px, 16px, 16px);\naiComposeCardDivider: shadowFg;\naiComposeCardSectionSkip: 12px;\naiComposeCardTextSkip: 0px;\naiComposeCardControlSkip: 8px;\naiComposeCardTitle: FlatLabel(defaultSubsectionTitle) {\n\ttextFg: windowFg;\n\tminWidth: 0px;\n\tmaxHeight: 22px;\n}\naiComposeEmojifyCheckbox: Checkbox(defaultBoxCheckbox) {\n\twidth: 0px;\n}\n\naiComposeExpandIcon: icon{{ \"info/edit/expand_arrow_small\", windowActiveTextFg }};\naiComposeCollapseIcon: icon{{ \"info/edit/expand_arrow_small-flip_vertical\", windowActiveTextFg }};\naiComposeExpandButton: IconButton(defaultIconButton) {\n\twidth: 32px;\n\theight: 32px;\n\ticon: aiComposeExpandIcon;\n\ticonOver: aiComposeExpandIcon;\n\ticonPosition: point(5px, 5px);\n\trippleAreaPosition: point(0px, 0px);\n\trippleAreaSize: 32px;\n\tripple: defaultRippleAnimationBgOver;\n}\n\naiComposeCopyIcon: icon{{ \"menu/copy\", windowActiveTextFg }};\naiComposeCopyButton: IconButton(aiComposeExpandButton) {\n\ticon: aiComposeCopyIcon;\n\ticonOver: aiComposeCopyIcon;\n}\n\naiComposeBoxButton: RoundButton(defaultActiveButton) {\n\theight: 42px;\n\ttextTop: 12px;\n\tstyle: semiboldTextStyle;\n}\naiComposeBox: Box(defaultBox) {\n\tbuttonPadding: margins(16px, 12px, 16px, 12px);\n\tbuttonHeight: 42px;\n\tbuttonWide: true;\n\tbutton: aiComposeBoxButton;\n\tshadowIgnoreTopSkip: true;\n\tbg: boxDividerBg;\n}\naiComposeBoxWithSend: Box(aiComposeBox) {\n\tbuttonPadding: margins(16px, 12px, 66px, 12px);\n}\naiComposeSendButtonSkip: 8px;\naiComposeBoxClose: IconButton(boxTitleClose) {\n\tripple: defaultRippleAnimation;\n}\naiComposeBoxInfoButton: IconButton(boxTitleClose) {\n\ticon: icon {{ \"menu/info\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"menu/info\", boxTitleCloseFgOver }};\n\tripple: defaultRippleAnimation;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/choose_filter_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/choose_filter_box.h\"\n\n#include \"apiwrap.h\"\n#include \"boxes/filters/edit_filter_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"core/application.h\" // primaryWindow\n#include \"core/ui_integration.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_session.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/filter_icons.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\" // tr::bold\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"main/main_session_settings.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_media_player.h\" // mediaPlayerMenuCheck\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\nnamespace {\n\n[[nodiscard]] QImage Icon(const Data::ChatFilter &f) {\n\tconstexpr auto kScale = 0.75;\n\tconst auto icon = Ui::LookupFilterIcon(Ui::ComputeFilterIcon(f)).normal;\n\tconst auto originalWidth = icon->width();\n\tconst auto originalHeight = icon->height();\n\n\tconst auto scaledWidth = int(originalWidth * kScale);\n\tconst auto scaledHeight = int(originalHeight * kScale);\n\n\tauto image = QImage(\n\t\tscaledWidth * style::DevicePixelRatio(),\n\t\tscaledHeight * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\timage.fill(Qt::transparent);\n\n\t{\n\t\tauto p = QPainter(&image);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tconst auto x = int((scaledWidth - originalWidth * kScale) / 2);\n\t\tconst auto y = int((scaledHeight - originalHeight * kScale) / 2);\n\n\t\tp.scale(kScale, kScale);\n\t\ticon->paint(p, x, y, scaledWidth, st::dialogsUnreadBgMuted->c);\n\t\tif (const auto color = f.colorIndex()) {\n\t\t\tp.resetTransform();\n\t\t\tconst auto circleSize = scaledWidth / 3.;\n\t\t\tconst auto r = QRectF(\n\t\t\t\tx + scaledWidth - circleSize,\n\t\t\t\ty + scaledHeight - circleSize - circleSize / 3.,\n\t\t\t\tcircleSize,\n\t\t\t\tcircleSize);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_Clear);\n\t\t\tp.setBrush(Qt::transparent);\n\t\t\tp.drawEllipse(r + Margins(st::lineWidth * 1.5));\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\t\tp.setBrush(Ui::EmptyUserpic::UserpicColor(*color).color2);\n\t\t\tp.drawEllipse(r);\n\t\t}\n\t}\n\n\treturn image;\n}\n\nclass FilterAction : public Ui::Menu::Action {\npublic:\n\tusing Ui::Menu::Action::Action;\n\n\tvoid setIcon(QImage &&image) {\n\t\t_icon = std::move(image);\n\t}\n\nprotected:\n\tvoid paintEvent(QPaintEvent *event) override {\n\t\tUi::Menu::Action::paintEvent(event);\n\t\tif (!_icon.isNull()) {\n\t\t\tconst auto size = _icon.size() / style::DevicePixelRatio();\n\t\t\tauto p = QPainter(this);\n\t\t\tp.drawImage(\n\t\t\t\twidth()\n\t\t\t\t\t- size.width()\n\t\t\t\t\t- st::menuWithIcons.itemPadding.right(),\n\t\t\t\t(height() - size.height()) / 2,\n\t\t\t\t_icon);\n\t\t}\n\t}\n\nprivate:\n\tQImage _icon;\n\n};\n\nData::ChatFilter ChangedFilter(\n\t\tconst Data::ChatFilter &filter,\n\t\tnot_null<History*> history,\n\t\tbool add) {\n\tauto always = base::duplicate(filter.always());\n\tauto never = base::duplicate(filter.never());\n\tif (add) {\n\t\tnever.remove(history);\n\t} else {\n\t\talways.remove(history);\n\t}\n\tconst auto result = Data::ChatFilter(\n\t\tfilter.id(),\n\t\tfilter.title(),\n\t\tfilter.iconEmoji(),\n\t\tfilter.colorIndex(),\n\t\tfilter.flags(),\n\t\tstd::move(always),\n\t\tfilter.pinned(),\n\t\tstd::move(never));\n\tconst auto in = result.contains(history);\n\tif (in == add) {\n\t\treturn result;\n\t}\n\talways = base::duplicate(result.always());\n\tnever = base::duplicate(result.never());\n\tif (add) {\n\t\talways.insert(history);\n\t} else {\n\t\tnever.insert(history);\n\t}\n\treturn Data::ChatFilter(\n\t\tfilter.id(),\n\t\tfilter.title(),\n\t\tfilter.iconEmoji(),\n\t\tfilter.colorIndex(),\n\t\tfilter.flags(),\n\t\tstd::move(always),\n\t\tfilter.pinned(),\n\t\tstd::move(never));\n}\n\nvoid ChangeFilterById(\n\t\tFilterId filterId,\n\t\tnot_null<History*> history,\n\t\tbool add) {\n\tExpects(filterId != 0);\n\n\tconst auto list = history->owner().chatsFilters().list();\n\tconst auto i = ranges::find(list, filterId, &Data::ChatFilter::id);\n\tif (i != end(list)) {\n\t\tconst auto was = *i;\n\t\tconst auto filter = ChangedFilter(was, history, add);\n\t\thistory->owner().chatsFilters().set(filter);\n\t\thistory->session().api().request(MTPmessages_UpdateDialogFilter(\n\t\t\tMTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter),\n\t\t\tMTP_int(filter.id()),\n\t\t\tfilter.tl()\n\t\t)).done([=, chat = history->peer->name(), name = filter.title()] {\n\t\t\tconst auto account = not_null(&history->session().account());\n\t\t\tif (const auto controller = Core::App().windowFor(account)) {\n\t\t\t\tconst auto isStatic = name.isStatic;\n\t\t\t\tcontroller->showToast({\n\t\t\t\t\t.text = (add\n\t\t\t\t\t\t? tr::lng_filters_toast_add\n\t\t\t\t\t\t: tr::lng_filters_toast_remove)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_chat,\n\t\t\t\t\t\t\ttr::bold(chat),\n\t\t\t\t\t\t\tlt_folder,\n\t\t\t\t\t\t\tUi::Text::Wrapped(name.text, EntityType::Bold),\n\t\t\t\t\t\t\ttr::marked),\n\t\t\t\t\t.textContext = Core::TextContext({\n\t\t\t\t\t\t.session = &history->session(),\n\t\t\t\t\t\t.customEmojiLoopLimit = isStatic ? -1 : 0,\n\t\t\t\t\t}),\n\t\t\t\t});\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tLOG((\"API Error: failed to %1 a dialog to a folder. %2\")\n\t\t\t\t.arg(add ? u\"add\"_q : u\"remove\"_q)\n\t\t\t\t.arg(error.type()));\n\t\t\t// Revert filter on fail.\n\t\t\thistory->owner().chatsFilters().set(was);\n\t\t}).send();\n\t}\n}\n\n} // namespace\n\nChooseFilterValidator::ChooseFilterValidator(not_null<History*> history)\n: _history(history) {\n}\n\nbool ChooseFilterValidator::canAdd() const {\n\tfor (const auto &filter : _history->owner().chatsFilters().list()) {\n\t\tif (filter.id() && !filter.contains(_history)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool ChooseFilterValidator::canAdd(FilterId filterId) const {\n\tExpects(filterId != 0);\n\n\tconst auto list = _history->owner().chatsFilters().list();\n\tconst auto i = ranges::find(list, filterId, &Data::ChatFilter::id);\n\tif (i != end(list)) {\n\t\treturn !i->contains(_history);\n\t}\n\treturn false;\n}\n\nbool ChooseFilterValidator::canRemove(FilterId filterId) const {\n\tExpects(filterId != 0);\n\n\tconst auto list = _history->owner().chatsFilters().list();\n\tconst auto i = ranges::find(list, filterId, &Data::ChatFilter::id);\n\tif (i != end(list)) {\n\t\treturn Data::CanRemoveFromChatFilter(*i, _history);\n\t}\n\treturn false;\n}\n\nChooseFilterValidator::LimitData ChooseFilterValidator::limitReached(\n\t\tFilterId filterId,\n\t\tbool always) const {\n\tExpects(filterId != 0);\n\n\tconst auto list = _history->owner().chatsFilters().list();\n\tconst auto i = ranges::find(list, filterId, &Data::ChatFilter::id);\n\tconst auto limit = _history->owner().pinnedChatsLimit(filterId);\n\tconst auto &chatsList = always ? i->always() : i->never();\n\treturn {\n\t\t.reached = (i != end(list))\n\t\t\t&& !ranges::contains(chatsList, _history)\n\t\t\t&& (chatsList.size() >= limit),\n\t\t.count = int(chatsList.size()),\n\t};\n}\n\nvoid ChooseFilterValidator::add(FilterId filterId) const {\n\tChangeFilterById(filterId, _history, true);\n}\n\nvoid ChooseFilterValidator::remove(FilterId filterId) const {\n\tChangeFilterById(filterId, _history, false);\n}\n\nvoid FillChooseFilterMenu(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<History*> history) {\n\tconst auto weak = base::make_weak(controller);\n\tconst auto validator = ChooseFilterValidator(history);\n\tconst auto &list = history->owner().chatsFilters().list();\n\tconst auto showColors = history->owner().chatsFilters().tagsEnabled();\n\tfor (const auto &filter : list) {\n\t\tconst auto id = filter.id();\n\t\tif (!id) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto callback = [=] {\n\t\t\tconst auto toAdd = !filter.contains(history);\n\t\t\tconst auto r = validator.limitReached(id, toAdd);\n\t\t\tif (r.reached) {\n\t\t\t\tcontroller->show(Box(\n\t\t\t\t\tFilterChatsLimitBox,\n\t\t\t\t\t&controller->session(),\n\t\t\t\t\tr.count,\n\t\t\t\t\ttoAdd));\n\t\t\t\treturn;\n\t\t\t} else if (toAdd ? validator.canAdd() : validator.canRemove(id)) {\n\t\t\t\tif (toAdd) {\n\t\t\t\t\tvalidator.add(id);\n\t\t\t\t} else {\n\t\t\t\t\tvalidator.remove(id);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tconst auto contains = filter.contains(history);\n\t\tconst auto title = filter.title();\n\t\tauto item = base::make_unique_q<FilterAction>(\n\t\t\tmenu->menu(),\n\t\t\tmenu->st().menu,\n\t\t\tUi::Menu::CreateAction(\n\t\t\t\tmenu.get(),\n\t\t\t\tUi::Text::FixAmpersandInAction(title.text.text),\n\t\t\t\tstd::move(callback)),\n\t\t\tcontains ? &st::mediaPlayerMenuCheck : nullptr,\n\t\t\tcontains ? &st::mediaPlayerMenuCheck : nullptr);\n\t\titem->setMarkedText(title.text, QString(), Core::TextContext({\n\t\t\t.session = &history->session(),\n\t\t\t.repaint = [raw = item.get()] { raw->update(); },\n\t\t\t.customEmojiLoopLimit = title.isStatic ? -1 : 0,\n\t\t}));\n\n\t\titem->setIcon(Icon(showColors ? filter : filter.withColorIndex({})));\n\t\tconst auto action = menu->addAction(std::move(item));\n\t\taction->setEnabled(contains\n\t\t\t? validator.canRemove(id)\n\t\t\t: validator.canAdd());\n\t}\n\n\tconst auto limit = [session = &controller->session()] {\n\t\treturn Data::PremiumLimits(session).dialogFiltersCurrent();\n\t};\n\tif ((list.size() - 1) < limit()) {\n\t\tmenu->addAction(tr::lng_filters_create(tr::now), [=] {\n\t\t\tconst auto strong = weak.get();\n\t\t\tif (!strong) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto session = &strong->session();\n\t\t\tconst auto &list = session->data().chatsFilters().list();\n\t\t\tif ((list.size() - 1) >= limit()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto chooseNextId = [&] {\n\t\t\t\tauto id = 2;\n\t\t\t\twhile (ranges::contains(list, id, &Data::ChatFilter::id)) {\n\t\t\t\t\t++id;\n\t\t\t\t}\n\t\t\t\treturn id;\n\t\t\t};\n\t\t\tauto filter =\n\t\t\t\tData::ChatFilter({}, {}, {}, {}, {}, { history }, {}, {});\n\t\t\tconst auto send = [=](const Data::ChatFilter &filter) {\n\t\t\t\tsession->api().request(MTPmessages_UpdateDialogFilter(\n\t\t\t\t\tMTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter),\n\t\t\t\t\tMTP_int(chooseNextId()),\n\t\t\t\t\tfilter.tl()\n\t\t\t\t)).done([=] {\n\t\t\t\t\tsession->data().chatsFilters().reload();\n\t\t\t\t}).send();\n\t\t\t};\n\t\t\tstrong->uiShow()->show(\n\t\t\t\tBox(EditFilterBox, strong, std::move(filter), send, nullptr));\n\t\t}, &st::menuIconShowInFolder);\n\t}\n\n\thistory->owner().chatsFilters().changed(\n\t) | rpl::on_next([=] {\n\t\tmenu->hideMenu();\n\t}, menu->lifetime());\n}\n\nbool FillChooseFilterWithAdminedGroupsMenu(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<UserData*> user,\n\t\tstd::shared_ptr<rpl::event_stream<>> listUpdates,\n\t\tstd::vector<not_null<PeerData*>> common,\n\t\tstd::shared_ptr<std::vector<PeerId>> collectCommon) {\n\tconst auto weak = base::make_weak(controller);\n\tconst auto session = &controller->session();\n\tconst auto &list = session->data().chatsFilters().list();\n\tconst auto showColors = session->data().chatsFilters().tagsEnabled();\n\tauto added = 0;\n\tfor (const auto &filter : list) {\n\t\tconst auto id = filter.id();\n\t\tif (!id) {\n\t\t\tcontinue;\n\t\t}\n\t\tauto canRestrictList = std::vector<not_null<PeerData*>>();\n\t\tconst auto maybeAppend = [&](not_null<History*> chat) {\n\t\t\tconst auto channel = chat->peer->asChannel();\n\t\t\tif (channel && channel->canRestrictParticipant(user)) {\n\t\t\t\tif (channel->isGroupAdmin(user) && !channel->amCreator()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcanRestrictList.push_back(chat->peer);\n\t\t\t}\n\t\t};\n\t\tfor (const auto &chat : filter.always()) {\n\t\t\tmaybeAppend(chat);\n\t\t}\n\t\tfor (const auto &chat : filter.pinned()) {\n\t\t\tmaybeAppend(chat);\n\t\t}\n\t\tif (canRestrictList.empty()) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto checked = std::make_shared<bool>(false);\n\n\t\tconst auto contains = false;\n\t\tconst auto title = filter.title();\n\t\tauto item = base::make_unique_q<FilterAction>(\n\t\t\tmenu->menu(),\n\t\t\tmenu->st().menu,\n\t\t\tnew QAction(\n\t\t\t\tUi::Text::FixAmpersandInAction(title.text.text),\n\t\t\t\tmenu.get()),\n\t\t\tcontains ? &st::mediaPlayerMenuCheck : nullptr,\n\t\t\tcontains ? &st::mediaPlayerMenuCheck : nullptr);\n\t\tconst auto triggered = [=, raw = item.get()] {\n\t\t\t*checked = !*checked;\n\t\t\tif (*checked) {\n\t\t\t\tfor (const auto &peer : canRestrictList) {\n\t\t\t\t\tif (ranges::contains(common, peer)) {\n\t\t\t\t\t\tcollectCommon->push_back(peer->id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor (const auto &peer : canRestrictList) {\n\t\t\t\t\tif (const auto i = ranges::find(*collectCommon, peer->id);\n\t\t\t\t\t\t\ti != collectCommon->end()) {\n\t\t\t\t\t\tcollectCommon->erase(i);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\traw->Ui::Menu::Action::setIcon(\n\t\t\t\t*checked ? &st::mediaPlayerMenuCheck : nullptr,\n\t\t\t\t*checked ? &st::mediaPlayerMenuCheck : nullptr);\n\t\t\tlistUpdates->fire({});\n\t\t};\n\t\titem->setActionTriggered([=] {\n\t\t\ttriggered();\n\n\t\t\tauto groups = session->settings().moderateCommonGroups();\n\t\t\tif (*checked && !ranges::contains(groups, id)) {\n\t\t\t\tgroups.push_back(id);\n\t\t\t} else if (!*checked) {\n\t\t\t\tgroups.erase(ranges::remove(groups, id), groups.end());\n\t\t\t}\n\t\t\tsession->settings().setModerateCommonGroups(groups);\n\t\t\tsession->saveSettingsDelayed();\n\t\t});\n\t\tif (ranges::contains(\n\t\t\t\tsession->settings().moderateCommonGroups(),\n\t\t\t\tid)) {\n\t\t\ttriggered();\n\t\t}\n\t\titem->setPreventClose(true);\n\t\titem->setMarkedText(title.text, QString(), Core::TextContext({\n\t\t\t.session = session,\n\t\t\t.repaint = [raw = item.get()] { raw->update(); },\n\t\t\t.customEmojiLoopLimit = title.isStatic ? -1 : 0,\n\t\t}));\n\n\t\titem->setIcon(Icon(showColors ? filter : filter.withColorIndex({})));\n\t\tmenu->addAction(std::move(item));\n\t\tadded++;\n\t}\n\n\tsession->data().chatsFilters().changed(\n\t) | rpl::on_next([=] {\n\t\tmenu->hideMenu();\n\t}, menu->lifetime());\n\n\treturn added;\n}\n\nHistory *HistoryFromMimeData(\n\t\tconst QMimeData *mime,\n\t\tnot_null<Main::Session*> session) {\n\tconst auto mimeFormat = u\"application/x-telegram-dialog\"_q;\n\tif (mime->hasFormat(mimeFormat)) {\n\t\tauto peerId = int64(-1);\n\t\tauto isTestMode = false;\n\t\tauto stream = QDataStream(mime->data(mimeFormat));\n\t\tstream >> peerId;\n\t\tstream >> isTestMode;\n\t\tif (isTestMode != session->isTestMode()) {\n\t\t\treturn nullptr;\n\t\t}\n\t\treturn session->data().historyLoaded(PeerId(peerId));\n\t}\n\tif (mime->hasText()) {\n\t\tauto text = mime->text().trimmed();\n\t\tif (text.startsWith('@')) {\n\t\t\ttext = text.mid(1);\n\t\t} else if (text.startsWith(u\"https://t.me/\"_q)) {\n\t\t\ttext = text.mid(13);\n\t\t} else {\n\t\t\treturn nullptr;\n\t\t}\n\t\tif (const auto peer = session->data().peerByUsername(text)) {\n\t\t\treturn session->data().historyLoaded(peer->id);\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid SetupFilterDragAndDrop(\n\t\tnot_null<Ui::RpWidget*> outer,\n\t\tnot_null<Main::Session*> session,\n\t\tFn<std::optional<FilterId>(QPoint)> filterIdAtPosition,\n\t\tFn<FilterId()> activeFilterId,\n\t\tFn<void(FilterId)> selectByFilterId) {\n\tconst auto hasAction = [=](not_null<QDropEvent*> drop, bool perform) {\n\t\tconst auto mimeData = drop->mimeData();\n\t\tconst auto filterId = filterIdAtPosition(\n\t\t\touter->mapToGlobal(drop->pos()));\n\t\tif (!filterId) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto id = *filterId;\n\t\tif (const auto h = HistoryFromMimeData(mimeData, session)) {\n\t\t\tauto v = ChooseFilterValidator(h);\n\t\t\tif (id) {\n\t\t\t\tif (v.canAdd(id)) {\n\t\t\t\t\tif (!v.limitReached(id, true).reached) {\n\t\t\t\t\t\tif (perform) {\n\t\t\t\t\t\t\tv.add(id);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tselectByFilterId(perform ? FilterId(-1) : id);\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (const auto active = activeFilterId();\n\t\t\t\t\t\tactive && v.canRemove(active)) {\n\t\t\t\t\tif (perform) {\n\t\t\t\t\t\tv.remove(active);\n\t\t\t\t\t}\n\t\t\t\t\tselectByFilterId(perform ? FilterId(-1) : active);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tselectByFilterId(-1);\n\t\treturn false;\n\t};\n\touter->setAcceptDrops(true);\n\touter->events(\n\t) | rpl::filter([](not_null<QEvent*> e) {\n\t\treturn e->type() == QEvent::DragEnter\n\t\t\t|| e->type() == QEvent::DragMove\n\t\t\t|| e->type() == QEvent::DragLeave\n\t\t\t|| e->type() == QEvent::Drop;\n\t}) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::DragEnter) {\n\t\t\tconst auto de = static_cast<QDragEnterEvent*>(e.get());\n\t\t\tif (hasAction(de, false)) {\n\t\t\t\tde->acceptProposedAction();\n\t\t\t} else {\n\t\t\t\tde->ignore();\n\t\t\t}\n\t\t} else if (e->type() == QEvent::DragMove) {\n\t\t\tconst auto dm = static_cast<QDragMoveEvent*>(e.get());\n\t\t\tif (hasAction(dm, false)) {\n\t\t\t\tdm->acceptProposedAction();\n\t\t\t} else {\n\t\t\t\tdm->ignore();\n\t\t\t}\n\t\t} else if (e->type() == QEvent::DragLeave) {\n\t\t\tselectByFilterId(-1);\n\t\t} else if (e->type() == QEvent::Drop) {\n\t\t\tconst auto drop = static_cast<QDropEvent*>(e.get());\n\t\t\tif (hasAction(drop, true)) {\n\t\t\t\tdrop->acceptProposedAction();\n\t\t\t} else {\n\t\t\t\tdrop->ignore();\n\t\t\t}\n\t\t}\n\t}, outer->lifetime());\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/choose_filter_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass RpWidget;\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nclass History;\n\nclass ChooseFilterValidator final {\npublic:\n\tChooseFilterValidator(not_null<History*> history);\n\tstruct LimitData {\n\t\tconst bool reached = false;\n\t\tconst int count = 0;\n\t};\n\n\t[[nodiscard]] bool canAdd() const;\n\t[[nodiscard]] bool canAdd(FilterId filterId) const;\n\t[[nodiscard]] bool canRemove(FilterId filterId) const;\n\t[[nodiscard]] LimitData limitReached(\n\t\tFilterId filterId,\n\t\tbool always) const;\n\n\tvoid add(FilterId filterId) const;\n\tvoid remove(FilterId filterId) const;\n\nprivate:\n\tconst not_null<History*> _history;\n\n};\n\nvoid FillChooseFilterMenu(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<History*> history);\n\nbool FillChooseFilterWithAdminedGroupsMenu(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<UserData*> user,\n\tstd::shared_ptr<rpl::event_stream<>> listUpdates,\n\tstd::vector<not_null<PeerData*>> common,\n\tstd::shared_ptr<std::vector<PeerId>> collectCommon);\n\nvoid SetupFilterDragAndDrop(\n\tnot_null<Ui::RpWidget*> outer,\n\tnot_null<Main::Session*> session,\n\tFn<std::optional<FilterId>(QPoint)> filterIdAtPosition,\n\tFn<FilterId()> activeFilterId,\n\tFn<void(FilterId)> selectByFilterId);\n\n[[nodiscard]] History *HistoryFromMimeData(\n\tconst QMimeData *mime,\n\tnot_null<Main::Session*> session);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/compose_ai_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/compose_ai_box.h\"\n\n#include \"api/api_compose_with_ai.h\"\n#include \"apiwrap.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/core_settings.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_document.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/session/session_show.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"spellcheck/platform/platform_language.h\"\n#include \"ui/boxes/about_cocoon_box.h\"\n#include \"ui/boxes/choose_language_box.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/controls/labeled_emoji_tabs.h\"\n#include \"ui/controls/send_button.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/skeleton_animation.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/custom_emoji_text_badge.h\"\n#include \"ui/text/text_extended_data.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"styles/style_basic.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_widgets.h\"\n\n#include <algorithm>\n#include <array>\n\nnamespace HistoryView::Controls {\nnamespace {\n\nconstexpr auto kAiComposeStyleTooltipHiddenPref = \"ai_compose_style_tooltip_hidden\"_cs;\n\nenum class ComposeAiMode {\n\tTranslate,\n\tStyle,\n\tFix,\n};\n\nenum class CardState {\n\tWaiting,\n\tLoading,\n\tReady,\n\tFailed,\n};\n\n[[nodiscard]] QColor ComposeAiColorWithAlpha(\n\t\tconst style::color &color,\n\t\tfloat64 alpha) {\n\tauto result = color->c;\n\tresult.setAlphaF(result.alphaF() * alpha);\n\treturn result;\n}\n\n[[nodiscard]] TextWithEntities HighlightDiff(TextWithEntities text) {\n\treturn Ui::Text::Colorized(\n\t\tUi::Text::Wrapped(std::move(text), EntityType::Underline), 1);\n}\n\n[[nodiscard]] TextWithEntities StrikeOutDiff(TextWithEntities text) {\n\treturn Ui::Text::Colorized(\n\t\tUi::Text::Wrapped(std::move(text), EntityType::StrikeOut), 2);\n}\n\n[[nodiscard]] TextWithEntities BuildDiffDisplay(\n\t\tconst Api::ComposeWithAi::Diff &diff) {\n\tauto result = TextWithEntities();\n\tauto entities = diff.entities;\n\tstd::stable_sort(\n\t\tentities.begin(),\n\t\tentities.end(),\n\t\t[](const auto &a, const auto &b) {\n\t\t\treturn a.offset < b.offset;\n\t\t});\n\tconst auto size = int(diff.text.text.size());\n\tauto taken = 0;\n\tfor (const auto &entity : entities) {\n\t\tconst auto offset = std::clamp(entity.offset, 0, size);\n\t\tconst auto length = std::clamp(entity.length, 0, size - offset);\n\t\tif (offset > taken) {\n\t\t\tresult.append(Ui::Text::Mid(diff.text, taken, offset - taken));\n\t\t}\n\t\tauto part = Ui::Text::Mid(diff.text, offset, length);\n\t\tswitch (entity.type) {\n\t\tcase Api::ComposeWithAi::DiffEntity::Type::Insert:\n\t\t\tresult.append(HighlightDiff(std::move(part)));\n\t\t\tbreak;\n\t\tcase Api::ComposeWithAi::DiffEntity::Type::Replace:\n\t\t\tif (!entity.oldText.isEmpty()) {\n\t\t\t\tresult.append(\n\t\t\t\t\tStrikeOutDiff(\n\t\t\t\t\t\tTextWithEntities::Simple(entity.oldText)));\n\t\t\t}\n\t\t\tresult.append(HighlightDiff(std::move(part)));\n\t\t\tbreak;\n\t\tcase Api::ComposeWithAi::DiffEntity::Type::Delete:\n\t\t\tresult.append(StrikeOutDiff(std::move(part)));\n\t\t\tbreak;\n\t\t}\n\t\ttaken = std::max(taken, offset + length);\n\t}\n\tif (taken < size) {\n\t\tresult.append(Ui::Text::Mid(diff.text, taken));\n\t}\n\treturn result;\n}\n\n[[nodiscard]] QString FromTitle(LanguageId id) {\n\treturn tr::lng_ai_compose_original(tr::now);\n}\n\n[[nodiscard]] TextWithEntities ToTitle(\n\t\tLanguageId id,\n\t\tconst QString &style) {\n\tconst auto name = style.isEmpty()\n\t\t? tr::link(Ui::LanguageName(id))\n\t\t: tr::link(tr::lng_ai_compose_name_style(\n\t\t\ttr::now,\n\t\t\tlt_name,\n\t\t\ttr::marked(Ui::LanguageName(id)),\n\t\t\tlt_style,\n\t\t\ttr::marked(style),\n\t\t\ttr::marked));\n\treturn tr::lng_ai_compose_to_language(\n\t\ttr::now,\n\t\tlt_language,\n\t\tname,\n\t\ttr::marked);\n}\n\n[[nodiscard]] LanguageId DefaultAiTranslateTo(LanguageId offeredFrom) {\n\tconst auto current = LanguageId{\n\t\tQLocale(Lang::LanguageIdOrDefault(Lang::Id())).language()\n\t};\n\tif (current && (current != offeredFrom)) {\n\t\treturn current;\n\t}\n\tconst auto english = LanguageId{ QLocale::English };\n\tif (english != offeredFrom) {\n\t\treturn english;\n\t}\n\treturn LanguageId{ QLocale::Spanish };\n}\n\n[[nodiscard]] const style::icon &ModeIcon(\n\t\tComposeAiMode mode,\n\t\tbool active) {\n\tswitch (mode) {\n\tcase ComposeAiMode::Translate:\n\t\treturn active\n\t\t\t? st::aiComposeTabTranslateIconActive\n\t\t\t: st::aiComposeTabTranslateIcon;\n\tcase ComposeAiMode::Style:\n\t\treturn active\n\t\t\t? st::aiComposeTabStyleIconActive\n\t\t\t: st::aiComposeTabStyleIcon;\n\tcase ComposeAiMode::Fix:\n\t\treturn active\n\t\t\t? st::aiComposeTabFixIconActive\n\t\t\t: st::aiComposeTabFixIcon;\n\t}\n\treturn active\n\t\t? st::aiComposeTabTranslateIconActive\n\t\t: st::aiComposeTabTranslateIcon;\n}\n\n[[nodiscard]] qreal ComposeAiPillRadius(int height) {\n\treturn height / 2.;\n}\n\n[[nodiscard]] QColor ComposeAiActiveBackgroundColor(\n\t\tconst style::color &color) {\n\treturn ComposeAiColorWithAlpha(\n\t\tcolor,\n\t\tst::aiComposeButtonBgActiveOpacity);\n}\n\n[[nodiscard]] QColor ComposeAiRippleColor(\n\t\tconst style::RippleAnimation &ripple,\n\t\tfloat64 opacity) {\n\treturn ComposeAiColorWithAlpha(\n\t\tripple.color,\n\t\topacity);\n}\n\n[[nodiscard]] Ui::LabeledEmojiTab ResolveStyleDescriptor(\n\t\tconst Main::AppConfig::AiComposeStyle &style) {\n\treturn {\n\t\t.id = style.type,\n\t\t.label = style.title,\n\t\t.customEmojiData = Data::SerializeCustomEmojiId(style.emojiId),\n\t};\n}\n\n[[nodiscard]] std::vector<Ui::LabeledEmojiTab> ResolveStyleDescriptors(\n\t\tconst std::vector<Main::AppConfig::AiComposeStyle> &styles) {\n\tauto result = std::vector<Ui::LabeledEmojiTab>();\n\tresult.reserve(styles.size());\n\tfor (const auto &style : styles) {\n\t\tresult.push_back(ResolveStyleDescriptor(style));\n\t}\n\treturn result;\n}\n\n[[nodiscard]] std::vector<Ui::LabeledEmojiTab> ResolveTranslateStyleDescriptors(\n\t\tnot_null<Main::Session*> session,\n\t\tconst std::vector<Ui::LabeledEmojiTab> &styles) {\n\tconst auto neutral = ChatHelpers::GenerateLocalTgsSticker(\n\t\tsession,\n\t\tu\"chat/white_flag_emoji\"_q);\n\tauto result = std::vector<Ui::LabeledEmojiTab>();\n\tresult.reserve(styles.size() + 1);\n\tresult.push_back({\n\t\t.id = QString(),\n\t\t.label = tr::lng_ai_compose_style_neutral(tr::now),\n\t\t.customEmojiData = Data::SerializeCustomEmojiId(neutral->id),\n\t});\n\tresult.insert(end(result), begin(styles), end(styles));\n\treturn result;\n}\n\n[[nodiscard]] TextWithEntities LoadingTitleSparkle(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto sparkles = ChatHelpers::GenerateLocalTgsSticker(\n\t\tsession,\n\t\tu\"chat/sparkles_emoji\"_q);\n\treturn tr::marked(u\" \"_q)\n\t\t.append(Data::SingleCustomEmoji(sparkles->id));\n}\n\nclass ComposeAiModeButton final : public Ui::RippleButton {\npublic:\n\tComposeAiModeButton(\n\t\tQWidget *parent,\n\t\tComposeAiMode mode,\n\t\tQString label);\n\n\tvoid setSelected(bool selected);\n\t[[nodiscard]] ComposeAiMode mode() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\t[[nodiscard]] QImage prepareRippleMask() const override;\n\nprivate:\n\tconst ComposeAiMode _mode;\n\tconst QString _label;\n\tbool _selected = false;\n\n};\n\nclass ComposeAiModeTabs final : public Ui::RpWidget {\npublic:\n\tComposeAiModeTabs(QWidget *parent);\n\n\tvoid setActive(ComposeAiMode mode);\n\tvoid setChangedCallback(Fn<void(ComposeAiMode)> callback);\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tconst not_null<ComposeAiModeButton*> _translate;\n\tconst not_null<ComposeAiModeButton*> _style;\n\tconst not_null<ComposeAiModeButton*> _fix;\n\tFn<void(ComposeAiMode)> _changed;\n\tComposeAiMode _active = ComposeAiMode::Style;\n\n};\n\nclass ComposeAiPreviewCard final : public Ui::RpWidget {\npublic:\n\tComposeAiPreviewCard(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Session*> session,\n\t\tTextWithEntities original,\n\t\tstd::shared_ptr<Ui::ChatStyle> chatStyle);\n\n\tvoid setResizeCallback(Fn<void()> callback);\n\tvoid setChooseCallback(Fn<void()> callback);\n\tvoid setCopyCallback(Fn<void()> callback);\n\tvoid setEmojifyChangedCallback(Fn<void(bool)> callback);\n\tvoid setOriginalTitle(const QString &title);\n\tvoid setOriginalVisible(bool visible);\n\tvoid setResultTitle(const TextWithEntities &title);\n\tvoid setEmojifyVisible(bool visible);\n\tvoid setEmojifyChecked(bool checked);\n\tvoid setState(CardState state);\n\tvoid setResultText(TextWithEntities text);\n\tvoid setShow(std::shared_ptr<Ui::Show> show);\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid refreshGeometry();\n\tvoid updateOriginalToggleIcon();\n\n\tconst Ui::Text::MarkedContext _context;\n\tconst TextWithEntities _original;\n\tconst not_null<Ui::FlatLabel*> _originalTitle;\n\tconst not_null<Ui::FlatLabel*> _originalBody;\n\tconst not_null<Ui::IconButton*> _originalToggle;\n\tconst not_null<Ui::FlatLabel*> _resultTitle;\n\tconst not_null<Ui::FlatLabel*> _resultBody;\n\tconst not_null<Ui::IconButton*> _copy;\n\tconst not_null<Ui::Checkbox*> _emojify;\n\tFn<void()> _resized;\n\tFn<void()> _chooseCallback;\n\tFn<void()> _copyCallback;\n\tFn<void(bool)> _emojifyChanged;\n\tbool _ignoreResizedCallback = false;\n\tbool _originalExpanded = false;\n\tbool _originalVisible = true;\n\tbool _emojifyVisible = false;\n\tbool _dividerVisible = false;\n\tint _dividerTop = 0;\n\tCardState _state = CardState::Waiting;\n\tUi::SkeletonAnimation _skeleton;\n\tstd::array<Ui::Text::SpecialColor, 2> _diffColors;\n\n};\n\nclass ComposeAiContent final : public Ui::RpWidget {\npublic:\n\tComposeAiContent(\n\t\tQWidget *parent,\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tComposeAiBoxArgs args);\n\t~ComposeAiContent();\n\n\t[[nodiscard]] bool hasResult() const;\n\t[[nodiscard]] const TextWithEntities &result() const;\n\t[[nodiscard]] const std::vector<Ui::LabeledEmojiTab> &stylesData() const;\n\tvoid setReadyChangedCallback(Fn<void(bool)> callback);\n\tvoid setLoadingChangedCallback(Fn<void(bool)> callback);\n\tvoid setPremiumFloodCallback(Fn<void()> callback);\n\tvoid setModeChangedCallback(Fn<void(ComposeAiMode)> callback);\n\tvoid setStyleSelectedCallback(Fn<void()> callback);\n\t[[nodiscard]] ComposeAiMode mode() const;\n\t[[nodiscard]] bool hasStyleSelection() const;\n\tvoid setModeTabs(not_null<ComposeAiModeTabs*> tabs);\n\tvoid setStyleTabs(not_null<Ui::SlideWrap<Ui::LabeledEmojiScrollTabs>*> stylesWrap);\n\tvoid start();\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tvoid refreshLayout();\n\tvoid chooseLanguage();\n\tvoid copyResult();\n\tvoid setMode(ComposeAiMode mode);\n\tvoid updateTitles();\n\tvoid updatePinnedTabs(anim::type animated);\n\tvoid cancelRequest();\n\tvoid request();\n\tvoid resetState(CardState state);\n\tvoid applyResult(Api::ComposeWithAi::Result &&result);\n\tvoid showError(const QString &error = {});\n\tvoid notifyLoadingChanged();\n\tvoid notifyReadyChanged();\n\t[[nodiscard]] QString currentTranslateStyle() const;\n\t[[nodiscard]] QString currentTranslateStyleLabel() const;\n\n\tconst not_null<Ui::GenericBox*> _box;\n\tconst not_null<Main::Session*> _session;\n\tconst TextWithEntities _original;\n\tconst LanguageId _detectedFrom;\n\tLanguageId _to;\n\tconst std::vector<Ui::LabeledEmojiTab> _stylesData;\n\tconst std::vector<Ui::LabeledEmojiTab> _translateStylesData;\n\tQPointer<ComposeAiModeTabs> _tabs;\n\tQPointer<Ui::LabeledEmojiScrollTabs> _styles;\n\tQPointer<Ui::SlideWrap<Ui::LabeledEmojiScrollTabs>> _stylesWrap;\n\tconst not_null<ComposeAiPreviewCard*> _preview;\n\tFn<void(bool)> _readyChanged;\n\tFn<void(bool)> _loadingChanged;\n\tFn<void()> _premiumFlood;\n\tFn<void(ComposeAiMode)> _modeChanged;\n\tFn<void()> _styleSelected;\n\tComposeAiMode _mode = ComposeAiMode::Style;\n\tint _styleIndex = -1;\n\tint _translateStyleIndex = 0;\n\tbool _emojify = false;\n\tCardState _state = CardState::Waiting;\n\tmtpRequestId _requestId = 0;\n\tint _requestToken = 0;\n\tTextWithEntities _result;\n\n};\n\n// ComposeAiModeButton\n\nComposeAiModeButton::ComposeAiModeButton(\n\tQWidget *parent,\n\tComposeAiMode mode,\n\tQString label)\n: RippleButton(parent, st::aiComposeButtonRippleInactive)\n, _mode(mode)\n, _label(std::move(label)) {\n\tsetCursor(style::cur_pointer);\n}\n\nvoid ComposeAiModeButton::setSelected(bool selected) {\n\tif (_selected == selected) {\n\t\treturn;\n\t}\n\t_selected = selected;\n\tupdate();\n}\n\nComposeAiMode ComposeAiModeButton::mode() const {\n\treturn _mode;\n}\n\nvoid ComposeAiModeButton::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\tPainterHighQualityEnabler hq(p);\n\n\tconst auto radius = ComposeAiPillRadius(height());\n\tif (_selected) {\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(ComposeAiActiveBackgroundColor(\n\t\t\tst::aiComposeTabButtonBgActive));\n\t\tp.drawRoundedRect(\n\t\t\trect(),\n\t\t\tradius,\n\t\t\tradius);\n\t}\n\tconst auto ripple = ComposeAiRippleColor(\n\t\t_selected\n\t\t\t? st::aiComposeButtonRippleActive\n\t\t\t: st::aiComposeButtonRippleInactive,\n\t\t_selected\n\t\t\t? st::aiComposeButtonRippleActiveOpacity\n\t\t\t: st::aiComposeButtonRippleInactiveOpacity);\n\tpaintRipple(p, 0, 0, &ripple);\n\n\tconst auto &icon = ModeIcon(_mode, _selected);\n\tconst auto iconLeft = (width() - icon.width()) / 2;\n\ticon.paint(p, iconLeft, st::aiComposeTabIconTop, width());\n\n\tp.setPen(_selected\n\t\t? st::aiComposeTabLabelFgActive\n\t\t: st::aiComposeTabLabelFg);\n\tp.setFont(st::aiComposeTabLabelFont);\n\tp.drawText(\n\t\tQRect(\n\t\t\t0,\n\t\t\tst::aiComposeTabLabelTop,\n\t\t\twidth(),\n\t\t\theight() - st::aiComposeTabLabelTop),\n\t\tQt::AlignHCenter | Qt::AlignTop,\n\t\t_label);\n}\n\nQImage ComposeAiModeButton::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::MaskByDrawer(size(), false, [&](QPainter &p) {\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(Qt::white);\n\t\tconst auto radius = ComposeAiPillRadius(height());\n\t\tp.drawRoundedRect(\n\t\t\trect(),\n\t\t\tradius,\n\t\t\tradius);\n\t});\n}\n\n// ComposeAiModeTabs\n\nComposeAiModeTabs::ComposeAiModeTabs(QWidget *parent)\n: RpWidget(parent)\n, _translate(Ui::CreateChild<ComposeAiModeButton>(\n\tthis,\n\tComposeAiMode::Translate,\n\ttr::lng_ai_compose_tab_translate(tr::now)))\n, _style(Ui::CreateChild<ComposeAiModeButton>(\n\tthis,\n\tComposeAiMode::Style,\n\ttr::lng_ai_compose_tab_style(tr::now)))\n, _fix(Ui::CreateChild<ComposeAiModeButton>(\n\tthis,\n\tComposeAiMode::Fix,\n\ttr::lng_ai_compose_tab_fix(tr::now))) {\n\tconst auto bind = [=](not_null<ComposeAiModeButton*> button) {\n\t\tbutton->setClickedCallback([=] {\n\t\t\tsetActive(button->mode());\n\t\t\tif (_changed) {\n\t\t\t\t_changed(button->mode());\n\t\t\t}\n\t\t});\n\t};\n\tbind(_translate);\n\tbind(_style);\n\tbind(_fix);\n\tsetActive(ComposeAiMode::Style);\n}\n\nvoid ComposeAiModeTabs::setActive(ComposeAiMode mode) {\n\t_active = mode;\n\t_translate->setSelected(mode == ComposeAiMode::Translate);\n\t_style->setSelected(mode == ComposeAiMode::Style);\n\t_fix->setSelected(mode == ComposeAiMode::Fix);\n}\n\nvoid ComposeAiModeTabs::setChangedCallback(Fn<void(ComposeAiMode)> callback) {\n\t_changed = std::move(callback);\n}\n\nint ComposeAiModeTabs::resizeGetHeight(int newWidth) {\n\tconst auto padding = st::aiComposeTabsPadding;\n\tconst auto skip = st::aiComposeTabsSkip;\n\tconst auto innerWidth = newWidth - padding.left() - padding.right();\n\tconst auto buttonWidth = (innerWidth - (2 * skip)) / 3;\n\tconst auto buttonHeight = st::aiComposeTabsHeight\n\t\t- padding.top()\n\t\t- padding.bottom();\n\tconst auto top = padding.top();\n\tauto left = padding.left();\n\tfor (const auto &button : { _translate, _style, _fix }) {\n\t\tbutton->setGeometry(left, top, buttonWidth, buttonHeight);\n\t\tleft += buttonWidth + skip;\n\t}\n\treturn st::aiComposeTabsHeight;\n}\n\nvoid ComposeAiModeTabs::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(st::aiComposeTabsBg);\n\tconst auto radius = st::aiComposeTabsRadius;\n\tp.drawRoundedRect(\n\t\trect(),\n\t\tradius,\n\t\tradius);\n}\n\n// ComposeAiPreviewCard\n\nComposeAiPreviewCard::ComposeAiPreviewCard(\n\tQWidget *parent,\n\tnot_null<Main::Session*> session,\n\tTextWithEntities original,\n\tstd::shared_ptr<Ui::ChatStyle> chatStyle)\n: RpWidget(parent)\n, _context(Core::TextContext({ .session = session }))\n, _original(std::move(original))\n, _originalTitle(Ui::CreateChild<Ui::FlatLabel>(\n\tthis,\n\tst::aiComposeCardTitle))\n, _originalBody(Ui::CreateChild<Ui::FlatLabel>(\n\tthis,\n\tst::aiComposeBodyLabel))\n, _originalToggle(Ui::CreateChild<Ui::IconButton>(\n\tthis,\n\tst::aiComposeExpandButton))\n, _resultTitle(Ui::CreateChild<Ui::FlatLabel>(\n\tthis,\n\tst::aiComposeCardTitle))\n, _resultBody(Ui::CreateChild<Ui::FlatLabel>(\n\tthis,\n\tst::aiComposeBodyLabel))\n, _copy(Ui::CreateChild<Ui::IconButton>(\n\tthis,\n\tst::aiComposeCopyButton))\n, _emojify(\n\tUi::CreateChild<Ui::Checkbox>(\n\t\tthis,\n\t\ttr::lng_ai_compose_emojify(tr::now),\n\t\tst::aiComposeEmojifyCheckbox,\n\t\tstd::make_unique<Ui::RoundCheckView>(st::defaultCheck,false)))\n, _skeleton(_resultBody) {\n\t_originalBody->setSelectable(true);\n\t_originalBody->setMarkedText(_original, _context);\n\t_resultTitle->setClickHandlerFilter([=](const auto &...) {\n\t\tif (_chooseCallback) {\n\t\t\t_chooseCallback();\n\t\t}\n\t\treturn false;\n\t});\n\t_resultBody->setSelectable(true);\n\tconst auto watchHeight = [=](not_null<Ui::FlatLabel*> label) {\n\t\tlabel->heightValue(\n\t\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\t\tif (_resized && !_ignoreResizedCallback) {\n\t\t\t\t_resized();\n\t\t\t}\n\t\t}, lifetime());\n\t};\n\twatchHeight(_originalBody);\n\twatchHeight(_resultBody);\n\t_diffColors[0] = { &st::boxTextFgGood->p, &st::boxTextFgGood->p };\n\t_diffColors[1] = { &st::attentionButtonFg->p, &st::attentionButtonFg->p };\n\t_resultBody->setColors(_diffColors);\n\t_originalToggle->setClickedCallback([=] {\n\t\t_originalExpanded = !_originalExpanded;\n\t\tupdateOriginalToggleIcon();\n\t\tif (_resized) {\n\t\t\t_resized();\n\t\t}\n\t});\n\t_copy->setClickedCallback([=] {\n\t\tif (_copyCallback) {\n\t\t\t_copyCallback();\n\t\t}\n\t});\n\t_emojify->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tif (_emojifyChanged) {\n\t\t\t_emojifyChanged(checked);\n\t\t}\n\t}, _emojify->lifetime());\n\tsetOriginalTitle(tr::lng_ai_compose_original(tr::now));\n\tsetResultTitle(tr::lng_ai_compose_result(tr::now, tr::marked));\n\t_resultBody->setMarkedText(_original, _context);\n\t_copy->setVisible(false);\n\tupdateOriginalToggleIcon();\n\tif (chatStyle) {\n\t\tconst auto style = chatStyle;\n\t\tconst auto s = session.get();\n\t\tconst auto setupCaches = [=](not_null<Ui::FlatLabel*> label) {\n\t\t\tlabel->setPreCache([=] {\n\t\t\t\treturn style->messageStyle(false, false).preCache.get();\n\t\t\t});\n\t\t\tlabel->setBlockquoteCache([=] {\n\t\t\t\treturn style->coloredQuoteCache(\n\t\t\t\t\tfalse,\n\t\t\t\t\ts->user()->colorIndex());\n\t\t\t});\n\t\t};\n\t\tsetupCaches(_originalBody);\n\t\tsetupCaches(_resultBody);\n\t}\n}\n\nvoid ComposeAiPreviewCard::setResizeCallback(Fn<void()> callback) {\n\t_resized = std::move(callback);\n}\n\nvoid ComposeAiPreviewCard::setChooseCallback(Fn<void()> callback) {\n\t_chooseCallback = std::move(callback);\n}\n\nvoid ComposeAiPreviewCard::setCopyCallback(Fn<void()> callback) {\n\t_copyCallback = std::move(callback);\n}\n\nvoid ComposeAiPreviewCard::setEmojifyChangedCallback(Fn<void(bool)> callback) {\n\t_emojifyChanged = std::move(callback);\n}\n\nvoid ComposeAiPreviewCard::setOriginalTitle(const QString &title) {\n\t_originalTitle->setText(title);\n\trefreshGeometry();\n}\n\nvoid ComposeAiPreviewCard::setOriginalVisible(bool visible) {\n\tif (_originalVisible == visible) {\n\t\treturn;\n\t}\n\t_originalVisible = visible;\n\t_originalTitle->setVisible(visible);\n\t_originalBody->setVisible(visible);\n\t_originalToggle->setVisible(false);\n\trefreshGeometry();\n}\n\nvoid ComposeAiPreviewCard::setResultTitle(const TextWithEntities &title) {\n\t_resultTitle->setMarkedText(title);\n\trefreshGeometry();\n}\n\nvoid ComposeAiPreviewCard::setEmojifyVisible(bool visible) {\n\t_emojifyVisible = visible;\n\t_emojify->setVisible(visible);\n\trefreshGeometry();\n}\n\nvoid ComposeAiPreviewCard::setEmojifyChecked(bool checked) {\n\t_emojify->setChecked(checked, Ui::Checkbox::NotifyAboutChange::DontNotify);\n\trefreshGeometry();\n}\n\nvoid ComposeAiPreviewCard::setState(CardState state) {\n\tif (_state == state) {\n\t\treturn;\n\t}\n\tconst auto wasLoading = (_state == CardState::Loading);\n\t_state = state;\n\tswitch (_state) {\n\tcase CardState::Waiting:\n\tcase CardState::Failed:\n\t\t_resultBody->setMarkedText(_original, _context);\n\t\t_copy->setVisible(false);\n\t\tif (wasLoading) {\n\t\t\t_skeleton.stop();\n\t\t}\n\t\tbreak;\n\tcase CardState::Loading:\n\t\t_resultBody->setMarkedText(_original, _context);\n\t\t_copy->setVisible(false);\n\t\t_skeleton.start();\n\t\tbreak;\n\tcase CardState::Ready:\n\t\t_copy->setVisible(true);\n\t\tif (wasLoading) {\n\t\t\t_skeleton.stop();\n\t\t}\n\t\tbreak;\n\t}\n\trefreshGeometry();\n}\n\nvoid ComposeAiPreviewCard::setResultText(TextWithEntities text) {\n\t_resultBody->setMarkedText(std::move(text), _context);\n\trefreshGeometry();\n}\n\nvoid ComposeAiPreviewCard::setShow(std::shared_ptr<Ui::Show> show) {\n\tconst auto setupFilter = [&](not_null<Ui::FlatLabel*> label) {\n\t\tlabel->setClickHandlerFilter([=](\n\t\t\t\tconst ClickHandlerPtr &handler,\n\t\t\t\tQt::MouseButton button) {\n\t\t\tif (dynamic_cast<Ui::Text::PreClickHandler*>(handler.get())) {\n\t\t\t\tActivateClickHandler(label, handler, ClickContext{\n\t\t\t\t\t.button = button,\n\t\t\t\t\t.other = QVariant::fromValue(ClickHandlerContext{\n\t\t\t\t\t\t.show = show,\n\t\t\t\t\t})\n\t\t\t\t});\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\t};\n\tsetupFilter(_originalBody);\n\tsetupFilter(_resultBody);\n}\n\nint ComposeAiPreviewCard::resizeGetHeight(int newWidth) {\n\tconst auto padding = st::aiComposeCardPadding;\n\tconst auto contentWidth = newWidth - padding.left() - padding.right();\n\tauto y = padding.top();\n\n\t_dividerVisible = false;\n\tif (_originalVisible) {\n\t\t_originalTitle->show();\n\t\t_originalBody->show();\n\t\t_originalTitle->resizeToWidth(contentWidth);\n\t\t_originalToggle->setVisible(false);\n\n\t\tconst auto toggleTop = y\n\t\t\t+ (_originalTitle->height() - _originalToggle->height()) / 2;\n\t\t_originalToggle->moveToRight(padding.right(), toggleTop, newWidth);\n\t\tconst auto originalTitleWidth = contentWidth\n\t\t\t- _originalToggle->width()\n\t\t\t- st::aiComposeCardControlSkip;\n\t\t_originalTitle->setGeometryToLeft(\n\t\t\tpadding.left(),\n\t\t\ty,\n\t\t\tstd::max(originalTitleWidth, 0),\n\t\t\t_originalTitle->height(),\n\t\t\tnewWidth);\n\t\ty = std::max(\n\t\t\ty + _originalTitle->height(),\n\t\t\ttoggleTop + _originalToggle->height());\n\n\t\t_ignoreResizedCallback = true;\n\t\tconst auto wasOriginalSize = _originalBody->size();\n\t\t_originalBody->resizeToWidth(contentWidth);\n\t\tconst auto fullOriginalHeight = _originalBody->height();\n\t\t_originalBody->resize(wasOriginalSize);\n\t\t_ignoreResizedCallback = false;\n\n\t\tconst auto lineHeight = _originalBody->st().style.lineHeight;\n\t\tconst auto originalHeight = _originalExpanded\n\t\t\t? fullOriginalHeight\n\t\t\t: std::min(fullOriginalHeight, lineHeight);\n\t\t_originalBody->setGeometryToLeft(\n\t\t\tpadding.left(),\n\t\t\ty,\n\t\t\tcontentWidth,\n\t\t\toriginalHeight,\n\t\t\tnewWidth);\n\t\tconst auto expandable = fullOriginalHeight > lineHeight;\n\t\t_originalToggle->setVisible(expandable);\n\t\ty += originalHeight + st::aiComposeCardSectionSkip;\n\t\t_dividerTop = y;\n\t\t_dividerVisible = true;\n\t\ty += st::lineWidth + st::aiComposeCardSectionSkip;\n\t} else {\n\t\t_originalTitle->hide();\n\t\t_originalBody->hide();\n\t\t_originalToggle->hide();\n\t}\n\n\t_resultTitle->show();\n\tauto controlsWidth = 0;\n\tif (_emojifyVisible) {\n\t\t_emojify->show();\n\t\t_emojify->resizeToNaturalWidth(contentWidth);\n\t\tcontrolsWidth += _emojify->width()\n\t\t\t+ st::aiComposeCardControlSkip;\n\t} else {\n\t\t_emojify->hide();\n\t}\n\tconst auto resultTitleWidth = std::max(\n\t\tcontentWidth - controlsWidth,\n\t\t0);\n\t_resultTitle->resizeToWidth(resultTitleWidth);\n\tauto right = padding.right();\n\tif (_emojifyVisible) {\n\t\t_emojify->moveToRight(right, y, newWidth);\n\t\tright += _emojify->width() + st::aiComposeCardControlSkip;\n\t}\n\t_resultTitle->setGeometryToLeft(\n\t\tpadding.left(),\n\t\ty,\n\t\tresultTitleWidth,\n\t\t_resultTitle->height(),\n\t\tnewWidth);\n\ty = std::max(\n\t\ty + _resultTitle->height(),\n\t\t(_emojifyVisible\n\t\t\t? (y - _emojify->getMargins().top() + _emojify->height())\n\t\t\t: 0));\n\n\tconst auto lineHeight = _resultBody->st().style.lineHeight\n\t\t? _resultBody->st().style.lineHeight\n\t\t: _resultBody->st().style.font->height;\n\tif (!_copy->isHidden()) {\n\t\t_resultBody->setSkipBlock(\n\t\t\t_copy->width(),\n\t\t\tlineHeight);\n\t} else {\n\t\t_resultBody->setSkipBlock(0, 0);\n\t}\n\t_resultBody->resizeToWidth(contentWidth);\n\t_resultBody->setGeometryToLeft(\n\t\tpadding.left(),\n\t\ty,\n\t\tcontentWidth,\n\t\t_resultBody->height(),\n\t\tnewWidth);\n\tif (!_copy->isHidden()) {\n\t\t_copy->moveToRight(\n\t\t\tpadding.right(),\n\t\t\ty + _resultBody->height() - lineHeight,\n\t\t\tnewWidth);\n\t}\n\ty += _resultBody->height();\n\n\treturn y + padding.bottom();\n}\n\nvoid ComposeAiPreviewCard::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(st::aiComposeCardBg);\n\tp.drawRoundedRect(\n\t\trect(),\n\t\tst::aiComposeCardRadius,\n\t\tst::aiComposeCardRadius);\n\tif (_dividerVisible) {\n\t\tp.setBrush(Qt::NoBrush);\n\t\tp.setPen(st::aiComposeCardDivider);\n\t\tp.drawLine(\n\t\t\tst::aiComposeCardPadding.left(),\n\t\t\t_dividerTop,\n\t\t\twidth() - st::aiComposeCardPadding.right(),\n\t\t\t_dividerTop);\n\t}\n}\n\nvoid ComposeAiPreviewCard::refreshGeometry() {\n\tif (width() > 0) {\n\t\tresizeToWidth(width());\n\t}\n\tif (_resized) {\n\t\t_resized();\n\t}\n}\n\nvoid ComposeAiPreviewCard::updateOriginalToggleIcon() {\n\t_originalToggle->setIconOverride(\n\t\t_originalExpanded ? &st::aiComposeCollapseIcon : nullptr,\n\t\t_originalExpanded ? &st::aiComposeCollapseIcon : nullptr);\n}\n\n// ComposeAiContent\n\nComposeAiContent::ComposeAiContent(\n\tQWidget *parent,\n\tnot_null<Ui::GenericBox*> box,\n\tComposeAiBoxArgs args)\n: RpWidget(parent)\n, _box(box)\n, _session(args.session)\n, _original(std::move(args.text))\n, _detectedFrom(Platform::Language::Recognize(_original.text))\n, _to(DefaultAiTranslateTo(_detectedFrom))\n, _stylesData(ResolveStyleDescriptors(\n\t_session->appConfig().aiComposeStyles()))\n, _translateStylesData(ResolveTranslateStyleDescriptors(_session, _stylesData))\n, _preview(\n\tUi::CreateChild<ComposeAiPreviewCard>(\n\t\tthis,\n\t\t_session,\n\t\t_original,\n\t\targs.chatStyle)) {\n\t_preview->setResizeCallback([=] { refreshLayout(); });\n\t_preview->setChooseCallback([=] { chooseLanguage(); });\n\t_preview->setCopyCallback([=] { copyResult(); });\n\t_preview->setEmojifyChangedCallback([=](bool checked) {\n\t\t_emojify = checked;\n\t\tif (_mode != ComposeAiMode::Fix) {\n\t\t\trequest();\n\t\t}\n\t});\n\t_preview->setShow(_box->uiShow());\n}\n\nComposeAiContent::~ComposeAiContent() {\n\tcancelRequest();\n}\n\nbool ComposeAiContent::hasResult() const {\n\treturn _state == CardState::Ready;\n}\n\nconst TextWithEntities &ComposeAiContent::result() const {\n\treturn _result;\n}\n\nconst std::vector<Ui::LabeledEmojiTab> &ComposeAiContent::stylesData() const {\n\treturn _stylesData;\n}\n\nvoid ComposeAiContent::setReadyChangedCallback(Fn<void(bool)> callback) {\n\t_readyChanged = std::move(callback);\n}\n\nvoid ComposeAiContent::setLoadingChangedCallback(Fn<void(bool)> callback) {\n\t_loadingChanged = std::move(callback);\n\tnotifyLoadingChanged();\n}\n\nvoid ComposeAiContent::setModeTabs(not_null<ComposeAiModeTabs*> tabs) {\n\t_tabs = tabs;\n\t_tabs->setChangedCallback([=](ComposeAiMode mode) {\n\t\tsetMode(mode);\n\t});\n\t_tabs->setActive(_mode);\n}\n\nvoid ComposeAiContent::setStyleTabs(\n\t\tnot_null<Ui::SlideWrap<Ui::LabeledEmojiScrollTabs>*> stylesWrap) {\n\t_stylesWrap = stylesWrap;\n\t_stylesWrap->setDuration(0);\n\t_styles = stylesWrap->entity();\n\t_styles->setChangedCallback([=](int index) {\n\t\tif (index >= 0 && index < int(_stylesData.size())) {\n\t\t\tconst auto wasNoSelection = (_styleIndex < 0);\n\t\t\t_styleIndex = index;\n\t\t\tupdateTitles();\n\t\t\tif (_mode == ComposeAiMode::Style) {\n\t\t\t\trequest();\n\t\t\t\tif (wasNoSelection && _styleSelected) {\n\t\t\t\t\t_styleSelected();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\t_styles->setActive(_styleIndex);\n\t_stylesWrap->toggle(_mode == ComposeAiMode::Style, anim::type::instant);\n}\n\nvoid ComposeAiContent::start() {\n\tupdatePinnedTabs(anim::type::instant);\n\tupdateTitles();\n\trequest();\n}\n\nint ComposeAiContent::resizeGetHeight(int newWidth) {\n\t_preview->resizeToWidth(newWidth);\n\t_preview->moveToLeft(0, 0, newWidth);\n\treturn _preview->height();\n}\n\nvoid ComposeAiContent::refreshLayout() {\n\tif (width() > 0) {\n\t\tresizeToWidth(width());\n\t}\n}\n\nvoid ComposeAiContent::chooseLanguage() {\n\tif (_mode != ComposeAiMode::Translate) {\n\t\treturn;\n\t}\n\tconst auto weak = QPointer<ComposeAiContent>(this);\n\tconst auto session = _session;\n\tconst auto styles = _translateStylesData;\n\tconst auto selectedStyle = std::make_shared<int>(_translateStyleIndex);\n\t_box->uiShow()->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto apply = [=](LanguageId id, int styleIndex) {\n\t\t\tif (!weak) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tweak->_to = id;\n\t\t\tif (const auto count = int(weak->_translateStylesData.size())) {\n\t\t\t\tweak->_translateStyleIndex = std::clamp(styleIndex, 0, count - 1);\n\t\t\t}\n\t\t\tweak->updateTitles();\n\t\t\tweak->request();\n\t\t};\n\t\tUi::ChooseLanguageBox(\n\t\t\tbox,\n\t\t\ttr::lng_languages(),\n\t\t\t[=](std::vector<LanguageId> ids) {\n\t\t\t\tif (ids.empty()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tapply(ids.front(), *selectedStyle);\n\t\t\t},\n\t\t\t{ _to },\n\t\t\tfalse,\n\t\t\tnullptr);\n\t\tconst auto bottom = box->setPinnedToBottomContent(\n\t\t\tobject_ptr<Ui::VerticalLayout>(box));\n\t\tconst auto skip = st::defaultSubsectionTitlePadding.left();\n\t\tconst auto tabs = bottom->add(\n\t\t\tobject_ptr<Ui::LabeledEmojiScrollTabs>(\n\t\t\t\tbottom,\n\t\t\t\tstyles,\n\t\t\t\tsession->data().customEmojiManager().factory(\n\t\t\t\t\tData::CustomEmojiSizeTag::Large)),\n\t\t\tQMargins(\n\t\t\t\t(skip - st::aiComposeStyleTabsPadding.left()),\n\t\t\t\t0,\n\t\t\t\t(skip - st::aiComposeStyleTabsPadding.right()),\n\t\t\t\t0));\n\t\ttabs->setPaintOuterCorners(false);\n\t\ttabs->setChangedCallback([=](int index) {\n\t\t\tif (index >= 0 && index < int(styles.size())) {\n\t\t\t\t*selectedStyle = index;\n\t\t\t\tapply(_to, index);\n\t\t\t\tbox->closeBox();\n\t\t\t}\n\t\t});\n\t\ttabs->setActive(std::clamp(*selectedStyle, 0, int(styles.size()) - 1));\n\t\ttabs->scrollToActive();\n\t}));\n}\n\nvoid ComposeAiContent::copyResult() {\n\tif (_state != CardState::Ready) {\n\t\treturn;\n\t}\n\tTextUtilities::SetClipboardText(\n\t\tTextForMimeData::WithExpandedLinks(_result));\n}\n\nvoid ComposeAiContent::setMode(ComposeAiMode mode) {\n\tif (_mode == mode) {\n\t\treturn;\n\t}\n\tif (mode != ComposeAiMode::Style) {\n\t\t_styleIndex = -1;\n\t}\n\t_mode = mode;\n\t_state = CardState::Waiting;\n\t_preview->setState(CardState::Waiting);\n\tnotifyLoadingChanged();\n\tif (_modeChanged) {\n\t\t_modeChanged(_mode);\n\t}\n\tupdatePinnedTabs(anim::type::normal);\n\tupdateTitles();\n\trefreshLayout();\n\trequest();\n}\n\nvoid ComposeAiContent::updateTitles() {\n\tconst auto hasResult = (_state == CardState::Loading)\n\t\t|| (_state == CardState::Ready);\n\t_preview->setOriginalVisible(hasResult);\n\t_preview->setOriginalTitle(\n\t\t(_mode == ComposeAiMode::Translate)\n\t\t\t? FromTitle(_detectedFrom)\n\t\t\t: tr::lng_ai_compose_original(tr::now));\n\t_preview->setResultTitle(\n\t\thasResult\n\t\t\t? ((_mode == ComposeAiMode::Translate)\n\t\t\t\t? ToTitle(_to, currentTranslateStyleLabel())\n\t\t\t\t: tr::lng_ai_compose_result(tr::now, tr::marked))\n\t\t\t: tr::lng_ai_compose_original(tr::now, tr::marked));\n\tconst auto emojifyOnlyMode = !hasResult\n\t\t&& (_mode == ComposeAiMode::Style)\n\t\t&& (_styleIndex < 0);\n\t_preview->setEmojifyVisible(\n\t\t(hasResult && (_mode != ComposeAiMode::Fix))\n\t\t|| emojifyOnlyMode);\n\t_preview->setEmojifyChecked(_emojify);\n}\n\nvoid ComposeAiContent::updatePinnedTabs(anim::type animated) {\n\tif (_tabs) {\n\t\t_tabs->setActive(_mode);\n\t}\n\tif (_styles) {\n\t\t_styles->setActive(_styleIndex);\n\t}\n\tif (_stylesWrap) {\n\t\t_stylesWrap->toggle(_mode == ComposeAiMode::Style, animated);\n\t}\n}\n\nvoid ComposeAiContent::cancelRequest() {\n\t++_requestToken;\n\tif (_requestId) {\n\t\t_session->api().composeWithAi().cancel(_requestId);\n\t\t_requestId = 0;\n\t}\n}\n\nvoid ComposeAiContent::request() {\n\tcancelRequest();\n\tif (_mode == ComposeAiMode::Style && _styleIndex < 0 && !_emojify) {\n\t\tif (_state != CardState::Waiting) {\n\t\t\tresetState(CardState::Waiting);\n\t\t}\n\t\treturn;\n\t}\n\tresetState(CardState::Loading);\n\n\tauto request = Api::ComposeWithAi::Request{\n\t\t.text = _original,\n\t\t.emojify = (_mode != ComposeAiMode::Fix) && _emojify,\n\t};\n\tswitch (_mode) {\n\tcase ComposeAiMode::Translate:\n\t\trequest.translateToLang = _to.twoLetterCode();\n\t\trequest.changeTone = currentTranslateStyle();\n\t\tbreak;\n\tcase ComposeAiMode::Style:\n\t\tif (_styleIndex >= 0) {\n\t\t\trequest.changeTone = _stylesData[_styleIndex].id;\n\t\t}\n\t\tbreak;\n\tcase ComposeAiMode::Fix:\n\t\trequest.proofread = true;\n\t\tbreak;\n\t}\n\n\tconst auto token = ++_requestToken;\n\tconst auto weak = QPointer<ComposeAiContent>(this);\n\t_requestId = _session->api().composeWithAi().request(\n\t\tstd::move(request),\n\t\t[=](Api::ComposeWithAi::Result &&result) {\n\t\t\tif (!weak || weak->_requestToken != token) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tweak->_requestId = 0;\n\t\t\tweak->applyResult(std::move(result));\n\t\t},\n\t\t[=](const MTP::Error &error) {\n\t\t\tif (!weak || weak->_requestToken != token) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tweak->_requestId = 0;\n\t\t\tweak->showError(error.type());\n\t\t});\n}\n\nvoid ComposeAiContent::resetState(CardState state) {\n\t_state = state;\n\t_result = {};\n\t_preview->setState(state);\n\tnotifyLoadingChanged();\n\tupdateTitles();\n\tnotifyReadyChanged();\n}\n\nvoid ComposeAiContent::applyResult(Api::ComposeWithAi::Result &&result) {\n\t_result = std::move(result.resultText);\n\tif (_result.text.isEmpty()) {\n\t\tshowError({});\n\t\treturn;\n\t}\n\tauto display = (_mode == ComposeAiMode::Fix && result.diffText)\n\t\t? BuildDiffDisplay(*result.diffText)\n\t\t: _result;\n\t_state = _result.text.isEmpty() ? CardState::Failed : CardState::Ready;\n\t_preview->setState(_state);\n\tnotifyLoadingChanged();\n\tif (_state == CardState::Ready) {\n\t\t_preview->setResultText(std::move(display));\n\t}\n\tupdateTitles();\n\tnotifyReadyChanged();\n\trefreshLayout();\n}\n\nvoid ComposeAiContent::showError(const QString &error) {\n\t_state = CardState::Failed;\n\t_preview->setState(CardState::Failed);\n\tnotifyLoadingChanged();\n\tupdateTitles();\n\tnotifyReadyChanged();\n\trefreshLayout();\n\tif (error == u\"AICOMPOSE_FLOOD_PREMIUM\"_q) {\n\t\tconst auto show = Main::MakeSessionShow(\n\t\t\t_box->uiShow(),\n\t\t\t_session);\n\t\tSettings::ShowPremiumPromoToast(\n\t\t\tshow,\n\t\t\tChatHelpers::ResolveWindowDefault(),\n\t\t\ttr::lng_ai_compose_flood_text(\n\t\t\t\ttr::now,\n\t\t\t\tlt_link,\n\t\t\t\ttr::link(tr::lng_ai_compose_flood_link(tr::now, tr::bold)),\n\t\t\t\ttr::rich),\n\t\t\tu\"ai_compose\"_q);\n\t\tif (_premiumFlood) {\n\t\t\t_premiumFlood();\n\t\t}\n\t\treturn;\n\t}\n\t_box->showToast(error.isEmpty()\n\t\t? tr::lng_ai_compose_error(tr::now)\n\t\t: error);\n}\n\nvoid ComposeAiContent::notifyLoadingChanged() {\n\tif (_loadingChanged) {\n\t\t_loadingChanged(_state == CardState::Loading);\n\t}\n}\n\nvoid ComposeAiContent::notifyReadyChanged() {\n\tif (_readyChanged) {\n\t\t_readyChanged(_state == CardState::Ready);\n\t}\n}\n\nvoid ComposeAiContent::setPremiumFloodCallback(Fn<void()> callback) {\n\t_premiumFlood = std::move(callback);\n}\n\nvoid ComposeAiContent::setModeChangedCallback(\n\t\tFn<void(ComposeAiMode)> callback) {\n\t_modeChanged = std::move(callback);\n}\n\nvoid ComposeAiContent::setStyleSelectedCallback(Fn<void()> callback) {\n\t_styleSelected = std::move(callback);\n}\n\nQString ComposeAiContent::currentTranslateStyle() const {\n\treturn (_translateStyleIndex >= 0\n\t\t&& _translateStyleIndex < int(_translateStylesData.size()))\n\t\t? _translateStylesData[_translateStyleIndex].id\n\t\t: QString();\n}\n\nQString ComposeAiContent::currentTranslateStyleLabel() const {\n\tif (const auto style = currentTranslateStyle(); !style.isEmpty()) {\n\t\treturn (_translateStyleIndex >= 0\n\t\t\t&& _translateStyleIndex < int(_translateStylesData.size()))\n\t\t\t? _translateStylesData[_translateStyleIndex].label\n\t\t\t: QString();\n\t}\n\treturn QString();\n}\n\nComposeAiMode ComposeAiContent::mode() const {\n\treturn _mode;\n}\n\nbool ComposeAiContent::hasStyleSelection() const {\n\treturn _styleIndex >= 0;\n}\n\n[[nodiscard]] Fn<void(bool)> SetupStyleTooltip(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Ui::RpWidget*> pinnedToTop,\n\t\tnot_null<Ui::RpWidget*> stylesWrap,\n\t\tFn<ComposeAiMode()> currentMode) {\n\tconst auto tooltip = Ui::CreateChild<Ui::ImportantTooltip>(\n\t\tbox,\n\t\tobject_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\t\tbox,\n\t\t\tUi::MakeNiceTooltipLabel(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_ai_compose_style_tooltip(tr::rich),\n\t\t\t\tst::historyMessagesTTLLabel.minWidth,\n\t\t\t\tst::ttlMediaImportantTooltipLabel),\n\t\t\tst::historyRecordTooltip.padding),\n\t\tst::historyRecordTooltip);\n\ttooltip->toggleFast(false);\n\n\tstruct State {\n\t\tbool shown = false;\n\t\tbool shownOnce = false;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\n\tconst auto updateGeometry = [=] {\n\t\tconst auto local = stylesWrap->geometry();\n\t\tif (local.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto geometry = Ui::MapFrom(box, pinnedToTop, local);\n\t\tconst auto countPosition = [=](QSize size) {\n\t\t\tconst auto left = geometry.x()\n\t\t\t\t+ (geometry.width() - size.width()) / 2;\n\t\t\treturn QPoint(\n\t\t\t\tstd::max(std::min(left, box->width() - size.width()), 0),\n\t\t\t\t(geometry.y()\n\t\t\t\t\t+ geometry.height()\n\t\t\t\t\t- st::historyRecordTooltip.arrow\n\t\t\t\t\t- (st::aiComposeBoxStyleTabsSkip / 2)));\n\t\t};\n\t\ttooltip->pointAt(geometry, RectPart::Bottom, countPosition);\n\t};\n\n\tconst auto updateVisibility = [=](bool visible) {\n\t\tconst auto show = visible\n\t\t\t&& !Core::App().settings().readPref<bool>(\n\t\t\t\tkAiComposeStyleTooltipHiddenPref);\n\t\tif (state->shown != show) {\n\t\t\tstate->shown = show;\n\t\t\tif (show) {\n\t\t\t\tupdateGeometry();\n\t\t\t\ttooltip->raise();\n\t\t\t}\n\t\t\tif (show && !state->shownOnce) {\n\t\t\t\tstate->shownOnce = true;\n\t\t\t\ttooltip->toggleFast(true);\n\t\t\t} else {\n\t\t\t\ttooltip->toggleAnimated(show);\n\t\t\t}\n\t\t}\n\t};\n\n\tstylesWrap->geometryValue(\n\t) | rpl::on_next([=](const QRect &geometry) {\n\t\tif (!geometry.isEmpty()) {\n\t\t\tif (state->shown) {\n\t\t\t\tupdateGeometry();\n\t\t\t} else {\n\t\t\t\tupdateVisibility(currentMode() == ComposeAiMode::Style);\n\t\t\t}\n\t\t}\n\t}, tooltip->lifetime());\n\n\treturn updateVisibility;\n}\n\n} // namespace\n\nvoid ComposeAiBox(not_null<Ui::GenericBox*> box, ComposeAiBoxArgs &&args) {\n\tconst auto sendButtonHeight = st::aiComposeSendButton.inner.height;\n\tconst auto buttonHeight = st::aiComposeSendButton.inner.icon.height()\n\t\t+ 2 * st::aiComposeSendButton.sendIconFillPadding;\n\tconst auto boxStyle = [&](const style::Box &base) {\n\t\tconst auto result = box->lifetime().make_state<style::Box>(base);\n\t\tresult->button.height = buttonHeight;\n\t\tresult->buttonHeight = buttonHeight;\n\t\tresult->button.textTop = base.button.textTop\n\t\t\t- (base.button.height - buttonHeight) / 2;\n\t\treturn result;\n\t};\n\tconst auto boxStyleNoSend = boxStyle(st::aiComposeBox);\n\tconst auto boxStyleWithSend = boxStyle(st::aiComposeBoxWithSend);\n\tbox->setStyle(*boxStyleNoSend);\n\tbox->setNoContentMargin(true);\n\tbox->setWidth(st::boxWideWidth);\n\tconst auto session = args.session;\n\tbox->addTopButton(st::aiComposeBoxClose, [=] {\n\t\tbox->closeBox();\n\t});\n\tbox->addTopButton(st::aiComposeBoxInfoButton, [=] {\n\t\tbox->uiShow()->show(Box(Ui::AboutCocoonBox));\n\t});\n\n\tconst auto body = box->verticalLayout();\n\tconst auto tabsSkip = QMargins(0, 0, 0, st::aiComposeBoxStyleTabsSkip);\n\tconst auto pinnedToTop = box->setPinnedToTopContent(\n\t\tobject_ptr<Ui::VerticalLayout>(box));\n\tconst auto tabs = pinnedToTop->add(\n\t\tobject_ptr<ComposeAiModeTabs>(pinnedToTop),\n\t\tst::aiComposeContentMargin + tabsSkip);\n\tconst auto content = body->add(\n\t\tobject_ptr<ComposeAiContent>(box, box, args),\n\t\tst::aiComposeContentMargin);\n\tauto emojiFactory = session->data().customEmojiManager().factory(\n\t\tData::CustomEmojiSizeTag::Large);\n\tconst auto stylesWrap = pinnedToTop->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::LabeledEmojiScrollTabs>>(\n\t\t\tpinnedToTop,\n\t\t\tobject_ptr<Ui::LabeledEmojiScrollTabs>(\n\t\t\t\tpinnedToTop,\n\t\t\t\tcontent->stylesData(),\n\t\t\t\tstd::move(emojiFactory)),\n\t\t\ttabsSkip),\n\t\tst::aiComposeContentMargin);\n\tstylesWrap->hide(anim::type::instant);\n\tcontent->setModeTabs(tabs);\n\tcontent->setStyleTabs(stylesWrap);\n\n\tconst auto updateStyleTooltipVisibility = SetupStyleTooltip(\n\t\tbox,\n\t\tpinnedToTop,\n\t\tstylesWrap,\n\t\t[=] { return content->mode(); });\n\n\tconst auto sparkle = LoadingTitleSparkle(session);\n\tconst auto loading = box->lifetime().make_state<\n\t\trpl::variable<bool>>();\n\n\tcontent->setLoadingChangedCallback([=](bool value) {\n\t\t*loading = value;\n\t});\n\n\tbox->setTitle(rpl::combine(\n\t\tloading->value(),\n\t\ttr::lng_ai_compose_title(tr::marked)\n\t) | rpl::map([=](bool loading, TextWithEntities title) {\n\t\treturn loading ? title.append(sparkle) : title;\n\t}), Core::TextContext({ .session = session }));\n\n\tauto premiumFlooded = std::make_shared<bool>(false);\n\tauto sendButton = std::make_shared<QPointer<Ui::SendButton>>();\n\n\tconst auto applyAndClose = [=] {\n\t\tif (!content->hasResult()) {\n\t\t\treturn;\n\t\t}\n\t\targs.apply(TextWithEntities(content->result()));\n\t\tbox->closeBox();\n\t};\n\tconst auto sendResult = [=](Api::SendOptions options) {\n\t\tif (!args.send || !content->hasResult()) {\n\t\t\treturn;\n\t\t}\n\t\targs.send(\n\t\t\tTextWithEntities(content->result()),\n\t\t\toptions,\n\t\t\tcrl::guard(box, [=] {\n\t\t\t\tbox->closeBox();\n\t\t\t}));\n\t};\n\tconst auto addApplyButton = [=](\n\t\t\tconst style::Box &style,\n\t\t\trpl::producer<QString> text,\n\t\t\tFn<void()> callback) {\n\t\tbox->setStyle(style);\n\t\tconst auto result = box->addButton(std::move(text), std::move(callback));\n\t\tresult->setFullRadius(true);\n\t\treturn result;\n\t};\n\tconst auto disableButton = [=](not_null<Ui::RoundButton*> button) {\n\t\tbutton->clearState();\n\t\tbutton->setDisabled(true);\n\t\tbutton->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tbutton->setTextFgOverride(\n\t\t\tanim::color(st::activeButtonBg, st::activeButtonFg, 0.5));\n\t\tbutton->setClickedCallback([] {\n\t\t});\n\t};\n\n\tconst auto rebuildButtons = [=] {\n\t\tif (*sendButton) {\n\t\t\tdelete sendButton->data();\n\t\t}\n\t\t*sendButton = nullptr;\n\t\tbox->clearButtons();\n\t\tbox->addTopButton(st::aiComposeBoxClose, [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t\tbox->addTopButton(st::aiComposeBoxInfoButton, [=] {\n\t\t\tbox->uiShow()->show(Box(Ui::AboutCocoonBox));\n\t\t});\n\n\t\tif (*premiumFlooded) {\n\t\t\tauto helper = Ui::Text::CustomEmojiHelper();\n\t\t\tconst auto badge = helper.paletteDependent(\n\t\t\t\tUi::Text::CustomEmojiTextBadge(\n\t\t\t\t\tu\"x50\"_q,\n\t\t\t\t\tst::aiComposeBadge,\n\t\t\t\t\tst::aiComposeBadgeMargin));\n\t\t\tconst auto btn = addApplyButton(\n\t\t\t\t*boxStyleNoSend,\n\t\t\t\ttr::lng_ai_compose_increase_limit(), nullptr);\n\t\t\tbtn->setContext(helper.context());\n\t\t\tbtn->setText(rpl::single(\n\t\t\t\ttr::lng_ai_compose_increase_limit(tr::now, tr::marked)\n\t\t\t\t\t.append(' ')\n\t\t\t\t\t.append(badge)));\n\t\t\tconst auto resolve = ChatHelpers::ResolveWindowDefault();\n\t\t\tconst auto close = crl::guard(box, [=] {\n\t\t\t\tbox->closeBox();\n\t\t\t});\n\t\t\tbtn->setClickedCallback([=] {\n\t\t\t\tif (const auto controller = resolve(session)) {\n\t\t\t\t\tShowPremiumPreviewBox(\n\t\t\t\t\t\tcontroller,\n\t\t\t\t\t\tPremiumFeature::AiCompose);\n\t\t\t\t}\n\t\t\t\tclose();\n\t\t\t});\n\t\t} else if (content->mode() == ComposeAiMode::Style\n\t\t\t\t&& !content->hasStyleSelection()\n\t\t\t\t&& !content->hasResult()) {\n\t\t\tconst auto btn = addApplyButton(\n\t\t\t\t*boxStyleNoSend,\n\t\t\t\ttr::lng_ai_compose_select_style(), nullptr);\n\t\t\tdisableButton(btn);\n\t\t} else if (content->hasResult()) {\n\t\t\tconst auto isStyle =\n\t\t\t\t(content->mode() == ComposeAiMode::Style);\n\t\t\tconst auto btn = addApplyButton(\n\t\t\t\targs.send ? *boxStyleWithSend : *boxStyleNoSend,\n\t\t\t\tisStyle\n\t\t\t\t\t? tr::lng_ai_compose_apply_style()\n\t\t\t\t\t: tr::lng_ai_compose_apply(),\n\t\t\t\tapplyAndClose);\n\t\t\tif (args.send) {\n\t\t\t\tconst auto send = Ui::CreateChild<Ui::SendButton>(\n\t\t\t\t\tbtn->parentWidget(),\n\t\t\t\t\tst::aiComposeSendButton);\n\t\t\t\tsend->setState({ .type = Ui::SendButton::Type::Send });\n\t\t\t\tsend->show();\n\t\t\t\tbtn->geometryValue(\n\t\t\t\t) | rpl::on_next([=](QRect geometry) {\n\t\t\t\t\tconst auto size = sendButtonHeight;\n\t\t\t\t\tsend->resize(size, size);\n\t\t\t\t\tsend->moveToLeft(\n\t\t\t\t\t\tgeometry.x() + geometry.width()\n\t\t\t\t\t\t\t+ st::aiComposeSendButtonSkip,\n\t\t\t\t\t\tgeometry.y() + (geometry.height() - size) / 2);\n\t\t\t\t}, send->lifetime());\n\t\t\t\tsend->setClickedCallback([=] {\n\t\t\t\t\tsendResult({});\n\t\t\t\t});\n\t\t\t\tif (args.setupMenu) {\n\t\t\t\t\targs.setupMenu(\n\t\t\t\t\t\tsend,\n\t\t\t\t\t\t[=](Api::SendOptions options) {\n\t\t\t\t\t\t\tsendResult(options);\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\t*sendButton = send;\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto isStyle =\n\t\t\t\t(content->mode() == ComposeAiMode::Style);\n\t\t\tconst auto btn = addApplyButton(\n\t\t\t\t*boxStyleNoSend,\n\t\t\t\tisStyle\n\t\t\t\t\t? tr::lng_ai_compose_apply_style()\n\t\t\t\t\t: tr::lng_ai_compose_apply(),\n\t\t\t\tnullptr);\n\t\t\tdisableButton(btn);\n\t\t}\n\t};\n\n\tcontent->setReadyChangedCallback([=](bool) {\n\t\trebuildButtons();\n\t});\n\tcontent->setPremiumFloodCallback([=] {\n\t\t*premiumFlooded = true;\n\t\trebuildButtons();\n\t});\n\tcontent->setModeChangedCallback([=](ComposeAiMode mode) {\n\t\trebuildButtons();\n\t\tupdateStyleTooltipVisibility(mode == ComposeAiMode::Style);\n\t});\n\tcontent->setStyleSelectedCallback([=] {\n\t\trebuildButtons();\n\t\tif (!Core::App().settings().readPref<bool>(kAiComposeStyleTooltipHiddenPref)) {\n\t\t\tCore::App().settings().writePref<bool>(kAiComposeStyleTooltipHiddenPref, true);\n\t\t}\n\t\tupdateStyleTooltipVisibility(false);\n\t});\n\n\trebuildButtons();\n\tcontent->start();\n}\n\nvoid ShowComposeAiBox(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tComposeAiBoxArgs &&args) {\n\tshow->show(Box(ComposeAiBox, std::move(args)));\n}\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/compose_ai_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_common.h\"\n#include \"base/object_ptr.h\"\n#include \"ui/text/text_entity.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass ChatStyle;\nclass GenericBox;\nclass RpWidget;\nclass Show;\n} // namespace Ui\n\nnamespace HistoryView::Controls {\n\nstruct ComposeAiBoxArgs {\n\tnot_null<Main::Session*> session;\n\tTextWithEntities text;\n\tstd::shared_ptr<Ui::ChatStyle> chatStyle;\n\tFn<void(TextWithEntities)> apply;\n\tFn<void(TextWithEntities, Api::SendOptions, Fn<void()>)> send;\n\tFn<void(not_null<Ui::RpWidget*>, Fn<void(Api::SendOptions)>)> setupMenu;\n};\n\nvoid ComposeAiBox(not_null<Ui::GenericBox*> box, ComposeAiBoxArgs &&args);\nvoid ShowComposeAiBox(std::shared_ptr<Ui::Show> show, ComposeAiBoxArgs &&args);\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/confirm_box.cpp",
    "content": ""
  },
  {
    "path": "Telegram/SourceFiles/boxes/connection_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/connection_box.h\"\n\n#include \"base/call_delayed.h\"\n#include \"base/qthelp_regex.h\"\n#include \"base/qthelp_url.h\"\n#include \"base/weak_ptr.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/local_url_handlers.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/facade.h\"\n#include \"settings/settings_common.h\"\n#include \"storage/localstorage.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/peer_qr_box.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/fields/number_input.h\"\n#include \"ui/widgets/fields/password_input.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/table_layout.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/ui_utility.h\"\n#include \"boxes/abstract_box.h\" // Ui::show().\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QClipboard>\n\nnamespace {\n\nconstexpr auto kSaveSettingsDelayedTimeout = crl::time(1000);\n\nusing ProxyData = MTP::ProxyData;\n\n[[nodiscard]] std::vector<QString> ExtractUrlsSimple(const QString &input) {\n\tauto urls = std::vector<QString>();\n\tstatic auto urlRegex = QRegularExpression(R\"((https?:\\/\\/[^\\s]+))\");\n\n\tauto it = urlRegex.globalMatch(input);\n\twhile (it.hasNext()) {\n\t\turls.push_back(it.next().captured(1));\n\t}\n\n\treturn urls;\n}\n\n[[nodiscard]] QString ProxyDataToString(const ProxyData &proxy) {\n\tusing Type = ProxyData::Type;\n\treturn u\"tg://\"_q\n\t\t+ (proxy.type == Type::Socks5 ? \"socks\" : \"proxy\")\n\t\t+ \"?server=\" + proxy.host + \"&port=\" + QString::number(proxy.port)\n\t\t+ ((proxy.type == Type::Socks5 && !proxy.user.isEmpty())\n\t\t\t? \"&user=\" + qthelp::url_encode(proxy.user) : \"\")\n\t\t+ ((proxy.type == Type::Socks5 && !proxy.password.isEmpty())\n\t\t\t? \"&pass=\" + qthelp::url_encode(proxy.password) : \"\")\n\t\t+ ((proxy.type == Type::Mtproto && !proxy.password.isEmpty())\n\t\t\t? \"&secret=\" + proxy.password : \"\");\n}\n\n[[nodiscard]] ProxyData ProxyDataFromFields(\n\t\tProxyData::Type type,\n\t\tconst QMap<QString, QString> &fields) {\n\tauto proxy = ProxyData();\n\tproxy.type = type;\n\tproxy.host = fields.value(u\"server\"_q);\n\tproxy.port = fields.value(u\"port\"_q).toUInt();\n\tif (type == ProxyData::Type::Socks5) {\n\t\tproxy.user = fields.value(u\"user\"_q);\n\t\tproxy.password = fields.value(u\"pass\"_q);\n\t} else if (type == ProxyData::Type::Mtproto) {\n\t\tproxy.password = fields.value(u\"secret\"_q);\n\t}\n\treturn proxy;\n};\n\nvoid AddProxyFromClipboard(\n\t\tnot_null<ProxiesBoxController*> controller,\n\t\tstd::shared_ptr<Ui::Show> show) {\n\tconst auto proxyString = u\"proxy\"_q;\n\tconst auto socksString = u\"socks\"_q;\n\tconst auto protocol = u\"tg://\"_q;\n\n\tconst auto maybeUrls = ExtractUrlsSimple(\n\t\tQGuiApplication::clipboard()->text());\n\tconst auto isSingle = maybeUrls.size() == 1;\n\n\tenum class Result {\n\t\tSuccess,\n\t\tFailed,\n\t\tUnsupported,\n\t\tIncorrectSecret,\n\t\tInvalid,\n\t};\n\n\tconst auto proceedUrl = [=](const auto &local) {\n\t\tconst auto command = base::StringViewMid(\n\t\t\tlocal,\n\t\t\tprotocol.size(),\n\t\t\t8192);\n\n\t\tif (local.startsWith(protocol + proxyString)\n\t\t\t|| local.startsWith(protocol + socksString)) {\n\n\t\t\tusing namespace qthelp;\n\t\t\tconst auto options = RegExOption::CaseInsensitive;\n\t\t\tfor (const auto &[expression, _] : Core::LocalUrlHandlers()) {\n\t\t\t\tconst auto midExpression = base::StringViewMid(\n\t\t\t\t\texpression,\n\t\t\t\t\t1);\n\t\t\t\tconst auto isSocks = midExpression.startsWith(\n\t\t\t\t\tsocksString);\n\t\t\t\tif (!midExpression.startsWith(proxyString)\n\t\t\t\t\t&& !isSocks) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto match = regex_match(\n\t\t\t\t\texpression,\n\t\t\t\t\tcommand,\n\t\t\t\t\toptions);\n\t\t\t\tif (!match) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto type = isSocks\n\t\t\t\t\t? ProxyData::Type::Socks5\n\t\t\t\t\t: ProxyData::Type::Mtproto;\n\t\t\t\tauto fields = url_parse_params(\n\t\t\t\t\tmatch->captured(1),\n\t\t\t\t\tqthelp::UrlParamNameTransform::ToLower);\n\t\t\t\tif (type == ProxyData::Type::Mtproto) {\n\t\t\t\t\tauto &secret = fields[u\"secret\"_q];\n\t\t\t\t\tsecret.replace('+', '-').replace('/', '_');\n\t\t\t\t}\n\t\t\t\tconst auto proxy = ProxyDataFromFields(type, fields);\n\t\t\t\tif (!proxy) {\n\t\t\t\t\tconst auto status = proxy.status();\n\t\t\t\t\treturn (status == ProxyData::Status::Unsupported)\n\t\t\t\t\t\t? Result::Unsupported\n\t\t\t\t\t\t: (status == ProxyData::Status::IncorrectSecret)\n\t\t\t\t\t\t? Result::IncorrectSecret\n\t\t\t\t\t\t: Result::Invalid;\n\t\t\t\t}\n\t\t\t\tconst auto contains = controller->contains(proxy);\n\t\t\t\tconst auto toast = (contains\n\t\t\t\t\t? tr::lng_proxy_add_from_clipboard_existing_toast\n\t\t\t\t\t: tr::lng_proxy_add_from_clipboard_good_toast)(tr::now);\n\t\t\t\tif (isSingle) {\n\t\t\t\t\tshow->showToast(toast);\n\t\t\t\t}\n\t\t\t\tif (!contains) {\n\t\t\t\t\tcontroller->addNewItem(proxy);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\treturn Result::Success;\n\t\t}\n\t\treturn Result::Failed;\n\t};\n\n\tauto success = Result::Failed;\n\tfor (const auto &maybeUrl : maybeUrls) {\n\t\tconst auto result = proceedUrl(Core::TryConvertUrlToLocal(maybeUrl));\n\t\tif (success != Result::Success) {\n\t\t\tsuccess = result;\n\t\t}\n\t}\n\n\tif (success != Result::Success) {\n\t\tif (success == Result::Failed) {\n\t\t\tshow->showToast(\n\t\t\t\ttr::lng_proxy_add_from_clipboard_failed_toast(tr::now));\n\t\t} else {\n\t\t\tshow->showBox(Ui::MakeInformBox(\n\t\t\t\t((success == Result::IncorrectSecret)\n\t\t\t\t\t? tr::lng_proxy_incorrect_secret(tr::now, tr::rich)\n\t\t\t\t\t: (success == Result::Unsupported)\n\t\t\t\t\t? tr::lng_proxy_unsupported(tr::now, tr::rich)\n\t\t\t\t\t: tr::lng_proxy_invalid(tr::now, tr::rich))));\n\t\t}\n\t}\n}\n\nclass HostInput : public Ui::MaskedInputField {\npublic:\n\tHostInput(\n\t\tQWidget *parent,\n\t\tconst style::InputField &st,\n\t\trpl::producer<QString> placeholder,\n\t\tconst QString &val);\n\nprotected:\n\tvoid correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) override;\n};\n\nHostInput::HostInput(\n\tQWidget *parent,\n\tconst style::InputField &st,\n\trpl::producer<QString> placeholder,\n\tconst QString &val)\n: MaskedInputField(parent, st, std::move(placeholder), val) {\n}\n\nvoid HostInput::correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) {\n\tQString newText;\n\tint newCursor = nowCursor;\n\tnewText.reserve(now.size());\n\tfor (auto i = 0, l = int(now.size()); i < l; ++i) {\n\t\tif (now[i] == ',') {\n\t\t\tnewText.append('.');\n\t\t} else {\n\t\t\tnewText.append(now[i]);\n\t\t}\n\t}\n\tsetCorrectedText(now, nowCursor, newText, newCursor);\n}\n\nclass Base64UrlInput : public Ui::MaskedInputField {\npublic:\n\tBase64UrlInput(\n\t\tQWidget *parent,\n\t\tconst style::InputField &st,\n\t\trpl::producer<QString> placeholder,\n\t\tconst QString &val);\n\nprotected:\n\tvoid correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) override;\n\n};\n\nBase64UrlInput::Base64UrlInput(\n\tQWidget *parent,\n\tconst style::InputField &st,\n\trpl::producer<QString> placeholder,\n\tconst QString &val)\n: MaskedInputField(parent, st, std::move(placeholder), val) {\n\tstatic const auto RegExp = QRegularExpression(\"^[a-zA-Z0-9_\\\\-]+$\");\n\tif (!RegExp.match(val).hasMatch()) {\n\t\tsetText(QString());\n\t}\n}\n\nvoid Base64UrlInput::correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) {\n\tQString newText;\n\tnewText.reserve(now.size());\n\tauto newPos = nowCursor;\n\tfor (auto i = 0, l = int(now.size()); i < l; ++i) {\n\t\tconst auto ch = now[i];\n\t\tif ((ch >= '0' && ch <= '9')\n\t\t\t|| (ch >= 'a' && ch <= 'z')\n\t\t\t|| (ch >= 'A' && ch <= 'Z')\n\t\t\t|| (ch == '-')\n\t\t\t|| (ch == '_')) {\n\t\t\tnewText.append(ch);\n\t\t} else if (i < nowCursor) {\n\t\t\t--newPos;\n\t\t}\n\t}\n\tsetCorrectedText(now, nowCursor, newText, newPos);\n}\n\nclass ProxyRow : public Ui::RippleButton {\npublic:\n\tusing View = ProxiesBoxController::ItemView;\n\tusing State = ProxiesBoxController::ItemState;\n\n\tProxyRow(QWidget *parent, View &&view);\n\n\tvoid updateFields(View &&view);\n\n\trpl::producer<> deleteClicks() const;\n\trpl::producer<> restoreClicks() const;\n\trpl::producer<> editClicks() const;\n\trpl::producer<> shareClicks() const;\n\trpl::producer<> showQrClicks() const;\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid setupControls(View &&view);\n\tint countAvailableWidth() const;\n\tvoid radialAnimationCallback();\n\tvoid paintCheck(Painter &p);\n\tvoid showMenu();\n\n\tView _view;\n\n\tUi::Text::String _title;\n\tobject_ptr<Ui::IconButton> _menuToggle;\n\trpl::event_stream<> _deleteClicks;\n\trpl::event_stream<> _restoreClicks;\n\trpl::event_stream<> _editClicks;\n\trpl::event_stream<> _shareClicks;\n\trpl::event_stream<> _showQrClicks;\n\tbase::unique_qptr<Ui::DropdownMenu> _menu;\n\n\tbool _set = false;\n\tUi::Animations::Simple _toggled;\n\tUi::Animations::Simple _setAnimation;\n\tstd::unique_ptr<Ui::InfiniteRadialAnimation> _progress;\n\tstd::unique_ptr<Ui::InfiniteRadialAnimation> _checking;\n\n\tint _skipLeft = 0;\n\tint _skipRight = 0;\n\n};\n\nclass ProxiesBox : public Ui::BoxContent {\npublic:\n\tusing View = ProxiesBoxController::ItemView;\n\n\tProxiesBox(\n\t\tQWidget*,\n\t\tnot_null<ProxiesBoxController*> controller,\n\t\tCore::SettingsProxy &settings,\n\t\tconst QString &highlightId = QString());\n\nprotected:\n\tvoid prepare() override;\n\tvoid showFinished() override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tvoid setupContent();\n\tvoid setupTopButton();\n\tvoid createNoRowsLabel();\n\tvoid addNewProxy();\n\tvoid applyView(View &&view);\n\tvoid setupButtons(int id, not_null<ProxyRow*> button);\n\tint rowHeight() const;\n\tvoid refreshProxyForCalls();\n\n\tnot_null<ProxiesBoxController*> _controller;\n\tCore::SettingsProxy &_settings;\n\tQPointer<Ui::Checkbox> _tryIPv6;\n\tstd::shared_ptr<Ui::RadioenumGroup<ProxyData::Settings>> _proxySettings;\n\tQPointer<Ui::SlideWrap<Ui::Checkbox>> _proxyForCalls;\n\tQPointer<Ui::DividerLabel> _about;\n\tbase::unique_qptr<Ui::RpWidget> _noRows;\n\tobject_ptr<Ui::VerticalLayout> _initialWrap;\n\tQPointer<Ui::VerticalLayout> _wrap;\n\tint _currentProxySupportsCallsId = 0;\n\n\tbase::flat_map<int, base::unique_qptr<ProxyRow>> _rows;\n\n\tQPointer<Ui::RpWidget> _addProxyButton;\n\tQPointer<Ui::RpWidget> _shareListButton;\n\tQString _highlightId;\n\n};\n\nclass ProxyBox final : public Ui::BoxContent {\npublic:\n\tProxyBox(\n\t\tQWidget*,\n\t\tconst ProxyData &data,\n\t\tFn<void(ProxyData)> callback,\n\t\tFn<void(ProxyData)> shareCallback);\n\nprivate:\n\tusing Type = ProxyData::Type;\n\n\tvoid prepare() override;\n\tvoid setInnerFocus() override {\n\t\t_host->setFocusFast();\n\t}\n\n\tvoid refreshButtons();\n\tProxyData collectData();\n\tvoid save();\n\tvoid share();\n\tvoid setupControls(const ProxyData &data);\n\tvoid setupTypes();\n\tvoid setupSocketAddress(const ProxyData &data);\n\tvoid setupCredentials(const ProxyData &data);\n\tvoid setupMtprotoCredentials(const ProxyData &data);\n\n\tvoid addLabel(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tconst QString &text) const;\n\n\tFn<void(ProxyData)> _callback;\n\tFn<void(ProxyData)> _shareCallback;\n\n\tobject_ptr<Ui::VerticalLayout> _content;\n\n\tstd::shared_ptr<Ui::RadioenumGroup<Type>> _type;\n\n\tQPointer<Ui::SlideWrap<>> _aboutSponsored;\n\tQPointer<HostInput> _host;\n\tQPointer<Ui::NumberInput> _port;\n\tQPointer<Ui::InputField> _user;\n\tQPointer<Ui::PasswordInput> _password;\n\tQPointer<Base64UrlInput> _secret;\n\n\tQPointer<Ui::SlideWrap<Ui::VerticalLayout>> _credentials;\n\tQPointer<Ui::SlideWrap<Ui::VerticalLayout>> _mtprotoCredentials;\n\n};\n\nProxyRow::ProxyRow(QWidget *parent, View &&view)\n: RippleButton(parent, st::proxyRowRipple)\n, _menuToggle(this, st::topBarMenuToggle) {\n\tsetupControls(std::move(view));\n}\n\nrpl::producer<> ProxyRow::deleteClicks() const {\n\treturn _deleteClicks.events();\n}\n\nrpl::producer<> ProxyRow::restoreClicks() const {\n\treturn _restoreClicks.events();\n}\n\nrpl::producer<> ProxyRow::editClicks() const {\n\treturn _editClicks.events();\n}\n\nrpl::producer<> ProxyRow::shareClicks() const {\n\treturn _shareClicks.events();\n}\n\nrpl::producer<> ProxyRow::showQrClicks() const {\n\treturn _showQrClicks.events();\n}\n\nvoid ProxyRow::setupControls(View &&view) {\n\tupdateFields(std::move(view));\n\t_toggled.stop();\n\t_setAnimation.stop();\n\n\t_menuToggle->addClickHandler([=] { showMenu(); });\n}\n\nint ProxyRow::countAvailableWidth() const {\n\treturn width() - _skipLeft - _skipRight;\n}\n\nvoid ProxyRow::updateFields(View &&view) {\n\tif (_view.selected != view.selected) {\n\t\t_toggled.start(\n\t\t\t[=] { update(); },\n\t\t\tview.selected ? 0. : 1.,\n\t\t\tview.selected ? 1. : 0.,\n\t\t\tst::defaultRadio.duration);\n\t}\n\t_view = std::move(view);\n\tconst auto endpoint = _view.host + ':' + QString::number(_view.port);\n\t_title.setMarkedText(\n\t\tst::proxyRowTitleStyle,\n\t\tTextWithEntities()\n\t\t\t.append(_view.type)\n\t\t\t.append(' ')\n\t\t\t.append(tr::link(endpoint, QString())),\n\t\tUi::ItemTextDefaultOptions());\n\n\tconst auto state = _view.state;\n\tif (state == State::Connecting) {\n\t\tif (!_progress) {\n\t\t\t_progress = std::make_unique<Ui::InfiniteRadialAnimation>(\n\t\t\t\t[=] { radialAnimationCallback(); },\n\t\t\t\tst::proxyCheckingAnimation);\n\t\t}\n\t\t_progress->start();\n\t} else if (_progress) {\n\t\t_progress->stop();\n\t}\n\tif (state == State::Checking) {\n\t\tif (!_checking) {\n\t\t\t_checking = std::make_unique<Ui::InfiniteRadialAnimation>(\n\t\t\t\t[=] { radialAnimationCallback(); },\n\t\t\t\tst::proxyCheckingAnimation);\n\t\t\t_checking->start();\n\t\t}\n\t} else {\n\t\t_checking = nullptr;\n\t}\n\tconst auto set = (state == State::Connecting || state == State::Online);\n\tif (_set != set) {\n\t\t_set = set;\n\t\t_setAnimation.start(\n\t\t\t[=] { update(); },\n\t\t\t_set ? 0. : 1.,\n\t\t\t_set ? 1. : 0.,\n\t\t\tst::defaultRadio.duration);\n\t}\n\n\tsetPointerCursor(!_view.deleted);\n\n\tupdate();\n}\n\nvoid ProxyRow::radialAnimationCallback() {\n\tif (!anim::Disabled()) {\n\t\tupdate();\n\t}\n}\n\nint ProxyRow::resizeGetHeight(int newWidth) {\n\tconst auto result = st::proxyRowPadding.top()\n\t\t+ st::semiboldFont->height\n\t\t+ st::proxyRowSkip\n\t\t+ st::normalFont->height\n\t\t+ st::proxyRowPadding.bottom();\n\tauto right = st::proxyRowPadding.right();\n\t_menuToggle->moveToRight(\n\t\tright,\n\t\t(result - _menuToggle->height()) / 2,\n\t\tnewWidth);\n\tright += _menuToggle->width();\n\t_skipRight = right;\n\t_skipLeft = st::proxyRowPadding.left()\n\t\t+ st::proxyRowIconSkip;\n\treturn result;\n}\n\nvoid ProxyRow::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tif (!_view.deleted) {\n\t\tpaintRipple(p, 0, 0);\n\t}\n\n\tconst auto left = _skipLeft;\n\tconst auto availableWidth = countAvailableWidth();\n\tauto top = st::proxyRowPadding.top();\n\n\tif (_view.deleted) {\n\t\tp.setOpacity(st::stickersRowDisabledOpacity);\n\t}\n\n\tpaintCheck(p);\n\n\tp.setPen(st::proxyRowTitleFg);\n\tp.setFont(st::semiboldFont);\n\tp.setTextPalette(st::proxyRowTitlePalette);\n\t_title.drawLeftElided(p, left, top, availableWidth, width());\n\ttop += st::semiboldFont->height + st::proxyRowSkip;\n\n\tconst auto statusFg = [&] {\n\t\tswitch (_view.state) {\n\t\tcase State::Online:\n\t\t\treturn st::proxyRowStatusFgOnline;\n\t\tcase State::Unavailable:\n\t\t\treturn st::proxyRowStatusFgOffline;\n\t\tcase State::Available:\n\t\t\treturn st::proxyRowStatusFgAvailable;\n\t\tdefault:\n\t\t\treturn st::proxyRowStatusFg;\n\t\t}\n\t}();\n\tconst auto status = [&] {\n\t\tswitch (_view.state) {\n\t\tcase State::Available:\n\t\t\treturn tr::lng_proxy_available(\n\t\t\t\ttr::now,\n\t\t\t\tlt_ping,\n\t\t\t\tQString::number(_view.ping));\n\t\tcase State::Checking:\n\t\t\treturn tr::lng_proxy_checking(tr::now);\n\t\tcase State::Connecting:\n\t\t\treturn tr::lng_proxy_connecting(tr::now);\n\t\tcase State::Online:\n\t\t\treturn tr::lng_proxy_online(tr::now);\n\t\tcase State::Unavailable:\n\t\t\treturn tr::lng_proxy_unavailable(tr::now);\n\t\t}\n\t\tUnexpected(\"State in ProxyRow::paintEvent.\");\n\t}();\n\tp.setPen(_view.deleted ? st::proxyRowStatusFg : statusFg);\n\tp.setFont(st::normalFont);\n\n\tauto statusLeft = left;\n\tif (_checking) {\n\t\t_checking->draw(\n\t\t\tp,\n\t\t\t{\n\t\t\t\tst::proxyCheckingPosition.x() + statusLeft,\n\t\t\t\tst::proxyCheckingPosition.y() + top\n\t\t\t},\n\t\t\twidth());\n\t\tstatusLeft += st::proxyCheckingPosition.x()\n\t\t\t+ st::proxyCheckingAnimation.size.width()\n\t\t\t+ st::proxyCheckingSkip;\n\t}\n\tp.drawTextLeft(statusLeft, top, width(), status);\n\ttop += st::normalFont->height + st::proxyRowPadding.bottom();\n}\n\nvoid ProxyRow::paintCheck(Painter &p) {\n\tconst auto loading = _progress\n\t\t? _progress->computeState()\n\t\t: Ui::RadialState{ 0., 0, arc::kFullLength };\n\tconst auto toggled = _toggled.value(_view.selected ? 1. : 0.)\n\t\t* (1. - loading.shown);\n\tconst auto _st = &st::defaultRadio;\n\tconst auto set = _setAnimation.value(_set ? 1. : 0.);\n\n\tPainterHighQualityEnabler hq(p);\n\n\tconst auto left = st::proxyRowPadding.left();\n\tconst auto top = (height() - _st->diameter - _st->thickness) / 2;\n\tconst auto outerWidth = width();\n\n\tauto pen = anim::pen(_st->untoggledFg, _st->toggledFg, toggled * set);\n\tpen.setWidth(_st->thickness);\n\tpen.setCapStyle(Qt::RoundCap);\n\tp.setPen(pen);\n\tp.setBrush(_st->bg);\n\tconst auto rect = style::rtlrect(QRectF(left, top, _st->diameter, _st->diameter).marginsRemoved(QMarginsF(_st->thickness / 2., _st->thickness / 2., _st->thickness / 2., _st->thickness / 2.)), outerWidth);\n\tif (_progress && loading.shown > 0 && anim::Disabled()) {\n\t\tanim::DrawStaticLoading(\n\t\t\tp,\n\t\t\trect,\n\t\t\t_st->thickness,\n\t\t\tpen.color(),\n\t\t\t_st->bg);\n\t} else if (loading.arcLength < arc::kFullLength) {\n\t\tp.drawArc(rect, loading.arcFrom, loading.arcLength);\n\t} else {\n\t\tp.drawEllipse(rect);\n\t}\n\n\tif (toggled > 0 && (!_progress || !anim::Disabled())) {\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(anim::brush(_st->untoggledFg, _st->toggledFg, toggled * set));\n\n\t\tauto skip0 = _st->diameter / 2., skip1 = _st->skip / 10., checkSkip = skip0 * (1. - toggled) + skip1 * toggled;\n\t\tp.drawEllipse(style::rtlrect(QRectF(left, top, _st->diameter, _st->diameter).marginsRemoved(QMarginsF(checkSkip, checkSkip, checkSkip, checkSkip)), outerWidth));\n\t}\n}\n\nvoid ProxyRow::showMenu() {\n\tif (_menu) {\n\t\treturn;\n\t}\n\t_menu = base::make_unique_q<Ui::DropdownMenu>(\n\t\twindow(),\n\t\tst::dropdownMenuWithIcons);\n\tconst auto weak = _menu.get();\n\t_menu->setHiddenCallback([=] {\n\t\tweak->deleteLater();\n\t\tif (_menu == weak) {\n\t\t\t_menuToggle->setForceRippled(false);\n\t\t}\n\t});\n\t_menu->setShowStartCallback([=] {\n\t\tif (_menu == weak) {\n\t\t\t_menuToggle->setForceRippled(true);\n\t\t}\n\t});\n\t_menu->setHideStartCallback([=] {\n\t\tif (_menu == weak) {\n\t\t\t_menuToggle->setForceRippled(false);\n\t\t}\n\t});\n\t_menuToggle->installEventFilter(_menu);\n\tconst auto addAction = [&](\n\t\t\tconst QString &text,\n\t\t\tFn<void()> callback,\n\t\t\tconst style::icon *icon) {\n\t\treturn _menu->addAction(text, std::move(callback), icon);\n\t};\n\taddAction(tr::lng_proxy_menu_edit(tr::now), [=] {\n\t\t_editClicks.fire({});\n\t}, &st::menuIconEdit);\n\tif (_view.supportsShare) {\n\t\taddAction(tr::lng_proxy_edit_share(tr::now), [=] {\n\t\t\t_shareClicks.fire({});\n\t\t}, &st::menuIconShare);\n\t\taddAction(tr::lng_group_invite_context_qr(tr::now), [=] {\n\t\t\t_showQrClicks.fire({});\n\t\t}, &st::menuIconQrCode);\n\t}\n\tif (_view.deleted) {\n\t\taddAction(tr::lng_proxy_menu_restore(tr::now), [=] {\n\t\t\t_restoreClicks.fire({});\n\t\t}, &st::menuIconRestore);\n\t} else {\n\t\taddAction(tr::lng_proxy_menu_delete(tr::now), [=] {\n\t\t\t_deleteClicks.fire({});\n\t\t}, &st::menuIconDelete);\n\t}\n\tconst auto parentTopLeft = window()->mapToGlobal(QPoint());\n\tconst auto buttonTopLeft = _menuToggle->mapToGlobal(QPoint());\n\tconst auto parent = QRect(parentTopLeft, window()->size());\n\tconst auto button = QRect(buttonTopLeft, _menuToggle->size());\n\tconst auto bottom = button.y()\n\t\t+ st::proxyDropdownDownPosition.y()\n\t\t+ _menu->height()\n\t\t- parent.y();\n\tconst auto top = button.y()\n\t\t+ st::proxyDropdownUpPosition.y()\n\t\t- _menu->height()\n\t\t- parent.y();\n\tif (bottom > parent.height() && top >= 0) {\n\t\tconst auto left = button.x()\n\t\t\t+ button.width()\n\t\t\t+ st::proxyDropdownUpPosition.x()\n\t\t\t- _menu->width()\n\t\t\t- parent.x();\n\t\t_menu->move(left, top);\n\t\t_menu->showAnimated(Ui::PanelAnimation::Origin::BottomRight);\n\t} else {\n\t\tconst auto left = button.x()\n\t\t\t+ button.width()\n\t\t\t+ st::proxyDropdownDownPosition.x()\n\t\t\t- _menu->width()\n\t\t\t- parent.x();\n\t\t_menu->move(left, bottom - _menu->height());\n\t\t_menu->showAnimated(Ui::PanelAnimation::Origin::TopRight);\n\t}\n}\n\nProxiesBox::ProxiesBox(\n\tQWidget*,\n\tnot_null<ProxiesBoxController*> controller,\n\tCore::SettingsProxy &settings,\n\tconst QString &highlightId)\n: _controller(controller)\n, _settings(settings)\n, _initialWrap(this)\n, _highlightId(highlightId) {\n\t_controller->views(\n\t) | rpl::on_next([=](View &&view) {\n\t\tapplyView(std::move(view));\n\t}, lifetime());\n}\n\nvoid ProxiesBox::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Copy\n\t\t|| (e->key() == Qt::Key_C && e->modifiers() == Qt::ControlModifier)) {\n\t\t_controller->shareItems();\n\t} else if (e->key() == Qt::Key_Paste\n\t\t|| (e->key() == Qt::Key_V && e->modifiers() == Qt::ControlModifier)) {\n\t\tAddProxyFromClipboard(_controller, uiShow());\n\t} else {\n\t\tBoxContent::keyPressEvent(e);\n\t}\n}\n\nvoid ProxiesBox::prepare() {\n\tsetTitle(tr::lng_proxy_settings());\n\n\t_addProxyButton = addButton(tr::lng_proxy_add(), [=] { addNewProxy(); });\n\taddButton(tr::lng_close(), [=] { closeBox(); });\n\n\tsetupTopButton();\n\tsetupContent();\n}\n\nvoid ProxiesBox::showFinished() {\n\tif (_highlightId == u\"proxy/add-proxy\"_q) {\n\t\tif (_addProxyButton) {\n\t\t\t_highlightId = QString();\n\t\t\tSettings::HighlightWidget(\n\t\t\t\t_addProxyButton,\n\t\t\t\t{ .rippleShape = true });\n\t\t}\n\t} else if (_highlightId == u\"proxy/share-list\"_q) {\n\t\tif (_shareListButton) {\n\t\t\t_highlightId = QString();\n\t\t\tSettings::HighlightWidget(_shareListButton);\n\t\t}\n\t}\n}\n\nvoid ProxiesBox::setupTopButton() {\n\tconst auto top = addTopButton(st::infoTopBarMenu);\n\tconst auto menu\n\t\t= top->lifetime().make_state<base::unique_qptr<Ui::PopupMenu>>();\n\n\ttop->setClickedCallback([=] {\n\t\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\ttop,\n\t\t\tst::popupMenuWithIcons);\n\t\tconst auto raw = menu->get();\n\t\tconst auto addAction = Ui::Menu::CreateAddActionCallback(raw);\n\t\taddAction({\n\t\t\t.text = tr::lng_proxy_add_from_clipboard(tr::now),\n\t\t\t.handler = [=] { AddProxyFromClipboard(_controller, uiShow()); },\n\t\t\t.icon = &st::menuIconImportTheme,\n\t\t});\n\t\tif (!_rows.empty()) {\n\t\t\taddAction({\n\t\t\t\t.text = tr::lng_group_invite_context_delete_all(tr::now),\n\t\t\t\t.handler = [=] { _controller->deleteItems(); },\n\t\t\t\t.icon = &st::menuIconDeleteAttention,\n\t\t\t\t.isAttention = true,\n\t\t\t});\n\t\t}\n\t\traw->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);\n\t\ttop->setForceRippled(true);\n\t\traw->setDestroyedCallback([=] {\n\t\t\tif (const auto strong = top.data()) {\n\t\t\t\tstrong->setForceRippled(false);\n\t\t\t}\n\t\t});\n\t\traw->popup(\n\t\t\ttop->mapToGlobal(\n\t\t\t\tQPoint(top->width(), top->height() - st::lineWidth * 3)));\n\t\treturn true;\n\t});\n}\n\nvoid ProxiesBox::setupContent() {\n\tconst auto inner = setInnerWidget(object_ptr<Ui::VerticalLayout>(this));\n\n\t_tryIPv6 = inner->add(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tinner,\n\t\t\ttr::lng_connection_try_ipv6(tr::now),\n\t\t\t_settings.tryIPv6()),\n\t\tst::proxyTryIPv6Padding);\n\t_proxySettings\n\t\t= std::make_shared<Ui::RadioenumGroup<ProxyData::Settings>>(\n\t\t\t_settings.settings());\n\tinner->add(\n\t\tobject_ptr<Ui::Radioenum<ProxyData::Settings>>(\n\t\t\tinner,\n\t\t\t_proxySettings,\n\t\t\tProxyData::Settings::Disabled,\n\t\t\ttr::lng_proxy_disable(tr::now)),\n\t\tst::proxyUsePadding);\n\tinner->add(\n\t\tobject_ptr<Ui::Radioenum<ProxyData::Settings>>(\n\t\t\tinner,\n\t\t\t_proxySettings,\n\t\t\tProxyData::Settings::System,\n\t\t\ttr::lng_proxy_use_system_settings(tr::now)),\n\t\tst::proxyUsePadding);\n\tinner->add(\n\t\tobject_ptr<Ui::Radioenum<ProxyData::Settings>>(\n\t\t\tinner,\n\t\t\t_proxySettings,\n\t\t\tProxyData::Settings::Enabled,\n\t\t\ttr::lng_proxy_use_custom(tr::now)),\n\t\tst::proxyUsePadding);\n\t_proxyForCalls = inner->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::Checkbox>>(\n\t\t\tinner,\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tinner,\n\t\t\t\ttr::lng_proxy_use_for_calls(tr::now),\n\t\t\t\t_settings.useProxyForCalls()),\n\t\t\tstyle::margins(\n\t\t\t\t0,\n\t\t\t\tst::proxyUsePadding.top(),\n\t\t\t\t0,\n\t\t\t\tst::proxyUsePadding.bottom())),\n\t\tstyle::margins(\n\t\t\tst::proxyTryIPv6Padding.left(),\n\t\t\t0,\n\t\t\tst::proxyTryIPv6Padding.right(),\n\t\t\tst::proxyTryIPv6Padding.top()));\n\n\t_about = inner->add(\n\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\tinner,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tinner,\n\t\t\t\ttr::lng_proxy_about(tr::now),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::proxyAboutPadding),\n\t\tstyle::margins(0, 0, 0, st::proxyRowPadding.top()));\n\n\t_wrap = inner->add(std::move(_initialWrap));\n\tinner->add(object_ptr<Ui::FixedHeightWidget>(\n\t\tinner,\n\t\tst::proxyRowPadding.bottom()));\n\n\t_proxySettings->setChangedCallback([=](ProxyData::Settings value) {\n\t\tif (!_controller->setProxySettings(value)) {\n\t\t\t_proxySettings->setValue(_settings.settings());\n\t\t\taddNewProxy();\n\t\t}\n\t\trefreshProxyForCalls();\n\t});\n\t_tryIPv6->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\t_controller->setTryIPv6(checked);\n\t}, _tryIPv6->lifetime());\n\n\t_controller->proxySettingsValue(\n\t) | rpl::on_next([=](ProxyData::Settings value) {\n\t\t_proxySettings->setValue(value);\n\t}, inner->lifetime());\n\n\t_proxyForCalls->entity()->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\t_controller->setProxyForCalls(checked);\n\t}, _proxyForCalls->lifetime());\n\n\tif (_rows.empty()) {\n\t\tcreateNoRowsLabel();\n\t}\n\trefreshProxyForCalls();\n\t_proxyForCalls->finishAnimating();\n\n\t{\n\t\tconst auto wrap = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\t\tconst auto shareList = Settings::AddButtonWithIcon(\n\t\t\twrap->entity(),\n\t\t\ttr::lng_proxy_edit_share_list_button(),\n\t\t\tst::settingsButton,\n\t\t\t{ &st::menuIconCopy });\n\t\t_shareListButton = shareList;\n\t\tshareList->setClickedCallback([=] {\n\t\t\t_controller->shareItems();\n\t\t});\n\t\twrap->toggleOn(_controller->listShareableChanges());\n\t\twrap->finishAnimating();\n\t}\n\n\tinner->resizeToWidth(st::boxWideWidth);\n\n\tinner->heightValue(\n\t) | rpl::map([=](int height) {\n\t\treturn std::min(\n\t\t\tstd::max(height, _about->y()\n\t\t\t\t+ _about->height()\n\t\t\t\t+ 3 * rowHeight()),\n\t\t\tst::boxMaxListHeight);\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](int height) {\n\t\tsetDimensions(st::boxWideWidth, height);\n\t}, inner->lifetime());\n}\n\nvoid ProxiesBox::refreshProxyForCalls() {\n\tif (!_proxyForCalls) {\n\t\treturn;\n\t}\n\t_proxyForCalls->toggle(\n\t\t(_proxySettings->current() == ProxyData::Settings::Enabled\n\t\t\t&& _currentProxySupportsCallsId != 0),\n\t\tanim::type::normal);\n}\n\nint ProxiesBox::rowHeight() const {\n\treturn st::proxyRowPadding.top()\n\t\t+ st::semiboldFont->height\n\t\t+ st::proxyRowSkip\n\t\t+ st::normalFont->height\n\t\t+ st::proxyRowPadding.bottom();\n}\n\nvoid ProxiesBox::addNewProxy() {\n\tgetDelegate()->show(_controller->addNewItemBox());\n}\n\nvoid ProxiesBox::applyView(View &&view) {\n\tif (view.selected) {\n\t\t_currentProxySupportsCallsId = view.supportsCalls ? view.id : 0;\n\t} else if (view.id == _currentProxySupportsCallsId) {\n\t\t_currentProxySupportsCallsId = 0;\n\t}\n\trefreshProxyForCalls();\n\n\tconst auto id = view.id;\n\tconst auto i = _rows.find(id);\n\tif (i == _rows.end()) {\n\t\tconst auto wrap = _wrap\n\t\t\t? _wrap.data()\n\t\t\t: _initialWrap.data();\n\t\tconst auto &[i, ok] = _rows.emplace(id, nullptr);\n\t\ti->second.reset(wrap->insert(\n\t\t\t0,\n\t\t\tobject_ptr<ProxyRow>(\n\t\t\t\twrap,\n\t\t\t\tstd::move(view))));\n\t\tsetupButtons(id, i->second.get());\n\t\tif (_noRows) {\n\t\t\t_noRows.reset();\n\t\t}\n\t\twrap->resizeToWidth(width());\n\t} else if (view.host.isEmpty()) {\n\t\t_rows.erase(i);\n\t} else {\n\t\ti->second->updateFields(std::move(view));\n\t}\n}\n\nvoid ProxiesBox::createNoRowsLabel() {\n\t_noRows.reset(_wrap->add(\n\t\tobject_ptr<Ui::FixedHeightWidget>(\n\t\t\t_wrap,\n\t\t\trowHeight()),\n\t\tst::proxyEmptyListPadding));\n\t_noRows->resize(\n\t\t(st::boxWideWidth\n\t\t\t- st::proxyEmptyListPadding.left()\n\t\t\t- st::proxyEmptyListPadding.right()),\n\t\t_noRows->height());\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\t_noRows.get(),\n\t\ttr::lng_proxy_description(tr::now),\n\t\tst::proxyEmptyListLabel);\n\t_noRows->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tlabel->resizeToWidth(width);\n\t\tlabel->moveToLeft(0, 0);\n\t}, label->lifetime());\n}\n\nvoid ProxiesBox::setupButtons(int id, not_null<ProxyRow*> button) {\n\tbutton->deleteClicks(\n\t) | rpl::on_next([=] {\n\t\t_controller->deleteItem(id);\n\t}, button->lifetime());\n\n\tbutton->restoreClicks(\n\t) | rpl::on_next([=] {\n\t\t_controller->restoreItem(id);\n\t}, button->lifetime());\n\n\tbutton->editClicks(\n\t) | rpl::on_next([=] {\n\t\tgetDelegate()->show(_controller->editItemBox(id));\n\t}, button->lifetime());\n\n\trpl::merge(\n\t\tbutton->shareClicks() | rpl::map_to(false),\n\t\tbutton->showQrClicks() | rpl::map_to(true)\n\t) | rpl::on_next([=](bool qr) {\n\t\t_controller->shareItem(id, qr);\n\t}, button->lifetime());\n\n\tbutton->clicks(\n\t) | rpl::on_next([=] {\n\t\t_controller->applyItem(id);\n\t}, button->lifetime());\n}\n\nProxyBox::ProxyBox(\n\tQWidget*,\n\tconst ProxyData &data,\n\tFn<void(ProxyData)> callback,\n\tFn<void(ProxyData)> shareCallback)\n: _callback(std::move(callback))\n, _shareCallback(std::move(shareCallback))\n, _content(this) {\n\tsetupControls(data);\n}\n\nvoid ProxyBox::prepare() {\n\tsetTitle(tr::lng_proxy_edit());\n\n\tconnect(_host.data(), &HostInput::changed, [=] {\n\t\tUi::PostponeCall(_host, [=] {\n\t\t\tconst auto host = _host->getLastText().trimmed();\n\t\t\tstatic const auto mask = QRegularExpression(\n\t\t\t\tu\"^\\\\d+\\\\.\\\\d+\\\\.\\\\d+\\\\.\\\\d+:(\\\\d*)$\"_q);\n\t\t\tconst auto match = mask.match(host);\n\t\t\tif (_host->cursorPosition() == host.size()\n\t\t\t\t&& match.hasMatch()) {\n\t\t\t\tconst auto port = match.captured(1);\n\t\t\t\t_port->setText(port);\n\t\t\t\t_port->setCursorPosition(port.size());\n\t\t\t\t_port->setFocus();\n\t\t\t\t_host->setText(host.mid(0, host.size() - port.size() - 1));\n\t\t\t}\n\t\t});\n\t});\n\t_port.data()->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::KeyPress\n\t\t\t&& (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Backspace)\n\t\t\t&& _port->cursorPosition() == 0) {\n\t\t\t_host->setCursorPosition(_host->getLastText().size());\n\t\t\t_host->setFocus();\n\t\t}\n\t}, _port->lifetime());\n\n\tconst auto submit = [=] {\n\t\tif (_host->hasFocus()\n\t\t\t&& !_host->getLastText().trimmed().isEmpty()) {\n\t\t\t_port->setFocus();\n\t\t} else if (_port->hasFocus()\n\t\t\t&& !_port->getLastText().trimmed().isEmpty()) {\n\t\t\tif (_type->current() == Type::Mtproto) {\n\t\t\t\t_secret->setFocus();\n\t\t\t} else {\n\t\t\t\t_user->setFocus();\n\t\t\t}\n\t\t} else if (_user->hasFocus()) {\n\t\t\t_password->setFocus();\n\t\t} else {\n\t\t\tsave();\n\t\t}\n\t};\n\tconnect(_host.data(), &Ui::MaskedInputField::submitted, submit);\n\tconnect(_port.data(), &Ui::MaskedInputField::submitted, submit);\n\t_user->submits(\n\t) | rpl::on_next(submit, _user->lifetime());\n\tconnect(_password.data(), &Ui::MaskedInputField::submitted, submit);\n\tconnect(_secret.data(), &Ui::MaskedInputField::submitted, submit);\n\n\trefreshButtons();\n\tsetDimensionsToContent(st::boxWideWidth, _content);\n}\n\nvoid ProxyBox::refreshButtons() {\n\tclearButtons();\n\taddButton(tr::lng_settings_save(), [=] { save(); });\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\n\tconst auto type = _type->current();\n\tif (type == Type::Socks5 || type == Type::Mtproto) {\n\t\taddLeftButton(tr::lng_proxy_share(), [=] { share(); });\n\t}\n}\n\nvoid ProxyBox::save() {\n\tif (const auto data = collectData()) {\n\t\t_callback(data);\n\t\tcloseBox();\n\t}\n}\n\nvoid ProxyBox::share() {\n\tif (const auto data = collectData()) {\n\t\t_shareCallback(data);\n\t}\n}\n\nProxyData ProxyBox::collectData() {\n\tauto result = ProxyData();\n\tresult.type = _type->current();\n\tresult.host = _host->getLastText().trimmed();\n\tresult.port = _port->getLastText().trimmed().toInt();\n\tresult.user = (result.type == Type::Mtproto)\n\t\t? QString()\n\t\t: _user->getLastText();\n\tresult.password = (result.type == Type::Mtproto)\n\t\t? _secret->getLastText()\n\t\t: _password->getLastText();\n\tif (result.host.isEmpty()) {\n\t\t_host->showError();\n\t} else if (!result.port) {\n\t\t_port->showError();\n\t} else if ((result.type == Type::Http || result.type == Type::Socks5)\n\t\t&& !result.password.isEmpty() && result.user.isEmpty()) {\n\t\t_user->showError();\n\t} else if (result.type == Type::Mtproto && !result.valid()) {\n\t\t_secret->showError();\n\t} else if (!result) {\n\t\t_host->showError();\n\t} else {\n\t\treturn result;\n\t}\n\treturn ProxyData();\n}\n\nvoid ProxyBox::setupTypes() {\n\tconst auto types = std::map<Type, QString>{\n\t\t{ Type::Http, \"HTTP\" },\n\t\t{ Type::Socks5, \"SOCKS5\" },\n\t\t{ Type::Mtproto, \"MTPROTO\" },\n\t};\n\tfor (const auto &[type, label] : types) {\n\t\t_content->add(\n\t\t\tobject_ptr<Ui::Radioenum<Type>>(\n\t\t\t\t_content,\n\t\t\t\t_type,\n\t\t\t\ttype,\n\t\t\t\tlabel),\n\t\t\tst::proxyEditTypePadding);\n\t}\n\t_aboutSponsored = _content->add(object_ptr<Ui::SlideWrap<>>(\n\t\t_content,\n\t\tobject_ptr<Ui::PaddingWrap<>>(\n\t\t\t_content,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t_content,\n\t\t\t\ttr::lng_proxy_sponsor_warning(tr::now),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::proxyAboutSponsorPadding)));\n}\n\nvoid ProxyBox::setupSocketAddress(const ProxyData &data) {\n\taddLabel(_content, tr::lng_proxy_address_label(tr::now));\n\tconst auto address = _content->add(\n\t\tobject_ptr<Ui::FixedHeightWidget>(\n\t\t\t_content,\n\t\t\tst::connectionHostInputField.heightMin),\n\t\tst::proxyEditInputPadding);\n\t_host = Ui::CreateChild<HostInput>(\n\t\taddress,\n\t\tst::connectionHostInputField,\n\t\ttr::lng_connection_host_ph(),\n\t\tdata.host);\n\t_port = Ui::CreateChild<Ui::NumberInput>(\n\t\taddress,\n\t\tst::connectionPortInputField,\n\t\ttr::lng_connection_port_ph(),\n\t\tdata.port ? QString::number(data.port) : QString(),\n\t\t65535);\n\taddress->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_port->moveToRight(0, 0);\n\t\t_host->resize(\n\t\t\twidth - _port->width() - st::proxyEditSkip,\n\t\t\t_host->height());\n\t\t_host->moveToLeft(0, 0);\n\t}, address->lifetime());\n}\n\nvoid ProxyBox::setupCredentials(const ProxyData &data) {\n\t_credentials = _content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_content,\n\t\t\tobject_ptr<Ui::VerticalLayout>(_content)));\n\tconst auto credentials = _credentials->entity();\n\taddLabel(credentials, tr::lng_proxy_credentials_optional(tr::now));\n\t_user = credentials->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tcredentials,\n\t\t\tst::connectionUserInputField,\n\t\t\ttr::lng_connection_user_ph(),\n\t\t\tdata.user),\n\t\tst::proxyEditInputPadding);\n\n\tauto passwordWrap = object_ptr<Ui::RpWidget>(credentials);\n\t_password = Ui::CreateChild<Ui::PasswordInput>(\n\t\tpasswordWrap.data(),\n\t\tst::connectionPasswordInputField,\n\t\ttr::lng_connection_password_ph(),\n\t\t(data.type == Type::Mtproto) ? QString() : data.password);\n\t_password->move(0, 0);\n\t_password->heightValue(\n\t) | rpl::on_next([=, wrap = passwordWrap.data()](int height) {\n\t\twrap->resize(wrap->width(), height);\n\t}, _password->lifetime());\n\tpasswordWrap->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_password->resize(width, _password->height());\n\t}, _password->lifetime());\n\tcredentials->add(std::move(passwordWrap), st::proxyEditInputPadding);\n}\n\nvoid ProxyBox::setupMtprotoCredentials(const ProxyData &data) {\n\t_mtprotoCredentials = _content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_content,\n\t\t\tobject_ptr<Ui::VerticalLayout>(_content)));\n\tconst auto mtproto = _mtprotoCredentials->entity();\n\taddLabel(mtproto, tr::lng_proxy_credentials(tr::now));\n\n\tauto secretWrap = object_ptr<Ui::RpWidget>(mtproto);\n\t_secret = Ui::CreateChild<Base64UrlInput>(\n\t\tsecretWrap.data(),\n\t\tst::connectionUserInputField,\n\t\ttr::lng_connection_proxy_secret_ph(),\n\t\t(data.type == Type::Mtproto) ? data.password : QString());\n\t_secret->move(0, 0);\n\t_secret->heightValue(\n\t) | rpl::on_next([=, wrap = secretWrap.data()](int height) {\n\t\twrap->resize(wrap->width(), height);\n\t}, _secret->lifetime());\n\tsecretWrap->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_secret->resize(width, _secret->height());\n\t}, _secret->lifetime());\n\tmtproto->add(std::move(secretWrap), st::proxyEditInputPadding);\n}\n\nvoid ProxyBox::setupControls(const ProxyData &data) {\n\t_type = std::make_shared<Ui::RadioenumGroup<Type>>(\n\t\t(data.type == Type::None\n\t\t\t? Type::Socks5\n\t\t\t: data.type));\n\t_content.create(this);\n\t_content->resizeToWidth(st::boxWideWidth);\n\t_content->moveToLeft(0, 0);\n\n\tsetupTypes();\n\tsetupSocketAddress(data);\n\tsetupCredentials(data);\n\tsetupMtprotoCredentials(data);\n\n\tconst auto handleType = [=](Type type) {\n\t\t_credentials->toggle(\n\t\t\ttype == Type::Http || type == Type::Socks5,\n\t\t\tanim::type::instant);\n\t\t_mtprotoCredentials->toggle(\n\t\t\ttype == Type::Mtproto,\n\t\t\tanim::type::instant);\n\t\t_aboutSponsored->toggle(\n\t\t\ttype == Type::Mtproto,\n\t\t\tanim::type::instant);\n\t};\n\t_type->setChangedCallback([=](Type type) {\n\t\thandleType(type);\n\t\trefreshButtons();\n\t});\n\thandleType(_type->current());\n}\n\nvoid ProxyBox::addLabel(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tconst QString &text) const {\n\tparent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tparent,\n\t\t\ttext,\n\t\t\tst::proxyEditTitle),\n\t\tst::proxyEditTitlePadding);\n}\n\nusing Connection = MTP::details::AbstractConnection;\nusing Checker = MTP::details::ConnectionPointer;\n\nvoid ResetProxyCheckers(Checker &v4, Checker &v6) {\n\tv4 = nullptr;\n\tv6 = nullptr;\n}\n\nvoid DropProxyChecker(Checker &v4, Checker &v6, not_null<Connection*> raw) {\n\tif (v4.get() == raw) {\n\t\tv4 = nullptr;\n\t} else if (v6.get() == raw) {\n\t\tv6 = nullptr;\n\t}\n}\n\n[[nodiscard]] bool HasProxyCheckers(const Checker &v4, const Checker &v6) {\n\treturn v4 || v6;\n}\n\nvoid StartProxyCheck(\n\t\tnot_null<MTP::Instance*> mtproto,\n\t\tconst ProxyData &proxy,\n\t\tChecker &v4,\n\t\tChecker &v6,\n\t\tFn<void(Connection *raw, int ping)> done,\n\t\tFn<void(Connection *raw)> fail) {\n\tusing Variants = MTP::DcOptions::Variants;\n\n\tResetProxyCheckers(v4, v6);\n\tconst auto connType = (proxy.type == ProxyData::Type::Http)\n\t\t? Variants::Http\n\t\t: Variants::Tcp;\n\tconst auto dcId = mtproto->mainDcId();\n\tconst auto setup = [&](Checker &checker, const bytes::vector &secret) {\n\t\tchecker = Connection::Create(\n\t\t\tmtproto,\n\t\t\tconnType,\n\t\t\tQThread::currentThread(),\n\t\t\tsecret,\n\t\t\tproxy);\n\t\tconst auto raw = checker.get();\n\t\traw->connect(raw, &Connection::connected, [=] {\n\t\t\tif (done) {\n\t\t\t\tdone(raw, raw->pingTime());\n\t\t\t}\n\t\t});\n\t\tconst auto failed = [=] {\n\t\t\tif (fail) {\n\t\t\t\tfail(raw);\n\t\t\t}\n\t\t};\n\t\traw->connect(raw, &Connection::disconnected, failed);\n\t\traw->connect(raw, &Connection::error, failed);\n\t};\n\tif (proxy.type == ProxyData::Type::Mtproto) {\n\t\tconst auto secret = proxy.secretFromMtprotoPassword();\n\t\tsetup(v4, secret);\n\t\tv4->connectToServer(\n\t\t\tproxy.host,\n\t\t\tproxy.port,\n\t\t\tsecret,\n\t\t\tdcId,\n\t\t\tfalse);\n\t\treturn;\n\t}\n\tconst auto options = mtproto->dcOptions().lookup(\n\t\tdcId,\n\t\tMTP::DcType::Regular,\n\t\ttrue);\n\tconst auto tryConnect = [&](Checker &checker, Variants::Address address) {\n\t\tconst auto &list = options.data[address][connType];\n\t\tif (list.empty()\n\t\t\t|| ((address == Variants::IPv6)\n\t\t\t\t&& !Core::App().settings().proxy().tryIPv6())) {\n\t\t\tchecker = nullptr;\n\t\t\treturn;\n\t\t}\n\t\tconst auto &endpoint = list.front();\n\t\tsetup(checker, endpoint.secret);\n\t\tchecker->connectToServer(\n\t\t\tQString::fromStdString(endpoint.ip),\n\t\t\tendpoint.port,\n\t\t\tendpoint.secret,\n\t\t\tdcId,\n\t\t\tfalse);\n\t};\n\ttryConnect(v4, Variants::IPv4);\n\ttryConnect(v6, Variants::IPv6);\n}\n\n} // namespace\n\nProxiesBoxController::ProxiesBoxController(not_null<Main::Account*> account)\n: _account(account)\n, _settings(Core::App().settings().proxy())\n, _saveTimer([] { Local::writeSettings(); }) {\n\t_list = ranges::views::all(\n\t\t_settings.list()\n\t) | ranges::views::transform([&](const ProxyData &proxy) {\n\t\treturn Item{ ++_idCounter, proxy };\n\t}) | ranges::to_vector;\n\n\t_settings.connectionTypeChanges(\n\t) | rpl::on_next([=] {\n\t\t_proxySettingsChanges.fire_copy(_settings.settings());\n\t\tconst auto i = findByProxy(_settings.selected());\n\t\tif (i != end(_list)) {\n\t\t\tupdateView(*i);\n\t\t}\n\t}, _lifetime);\n\n\tfor (auto &item : _list) {\n\t\trefreshChecker(item);\n\t}\n}\n\nvoid ProxiesBoxController::ShowApplyConfirmation(\n\t\tWindow::SessionController *controller,\n\t\tType type,\n\t\tconst QMap<QString, QString> &fields) {\n\tconst auto proxy = ProxyDataFromFields(type, fields);\n\tif (!proxy) {\n\t\tconst auto status = proxy.status();\n\t\tauto box = Ui::MakeInformBox(\n\t\t\t((status == ProxyData::Status::Unsupported)\n\t\t\t\t? tr::lng_proxy_unsupported(tr::now, tr::rich)\n\t\t\t\t: (status == ProxyData::Status::IncorrectSecret)\n\t\t\t\t? tr::lng_proxy_incorrect_secret(tr::now, tr::rich)\n\t\t\t\t: tr::lng_proxy_invalid(tr::now, tr::rich)));\n\t\tif (controller) {\n\t\t\tcontroller->uiShow()->showBox(std::move(box));\n\t\t} else {\n\t\t\tUi::show(std::move(box));\n\t\t}\n\t\treturn;\n\t}\n\tstatic const auto UrlStartRegExp = QRegularExpression(\n\t\t\"^https://\",\n\t\tQRegularExpression::CaseInsensitiveOption);\n\tstatic const auto UrlEndRegExp = QRegularExpression(\"/$\");\n\tconst auto displayed = \"https://\" + proxy.host + \"/\";\n\tconst auto parsed = QUrl::fromUserInput(displayed);\n\tconst auto displayUrl = !UrlClickHandler::IsSuspicious(displayed)\n\t\t? displayed\n\t\t: parsed.isValid()\n\t\t? QString::fromUtf8(parsed.toEncoded())\n\t\t: UrlClickHandler::ShowEncoded(displayed);\n\tconst auto displayServer = QString(\n\t\tdisplayUrl\n\t).replace(\n\t\tUrlStartRegExp,\n\t\tQString()\n\t).replace(UrlEndRegExp, QString());\n\tconst auto box = [=](not_null<Ui::GenericBox*> box) {\n\t\tbox->setTitle(tr::lng_proxy_box_table_title());\n\t\tbox->setStyle(st::proxyApplyBox);\n\t\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\n\t\tconst auto table = box->addRow(\n\t\t\tobject_ptr<Ui::TableLayout>(\n\t\t\t\tbox,\n\t\t\t\tst::proxyApplyBoxTable),\n\t\t\tst::proxyApplyBoxTableMargin);\n\t\tconst auto addRow = [&](\n\t\t\t\trpl::producer<QString> label,\n\t\t\t\tobject_ptr<Ui::RpWidget> value) {\n\t\t\ttable->addRow(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\ttable,\n\t\t\t\t\tstd::move(label),\n\t\t\t\t\ttable->st().defaultLabel),\n\t\t\t\tstd::move(value),\n\t\t\t\tst::proxyApplyBoxTableLabelMargin,\n\t\t\t\tst::proxyApplyBoxTableValueMargin);\n\t\t};\n\t\tconst auto add = [&](\n\t\t\t\tconst QString &value,\n\t\t\t\trpl::producer<QString> label) {\n\t\t\tif (!value.isEmpty()) {\n\t\t\t\tconstexpr auto kOneLineCount = 20;\n\t\t\t\tconst auto oneLine = value.length() <= kOneLineCount;\n\t\t\t\tauto widget = object_ptr<Ui::FlatLabel>(\n\t\t\t\t\ttable,\n\t\t\t\t\trpl::single(Ui::Text::Wrapped(\n\t\t\t\t\t\t{ value },\n\t\t\t\t\t\tEntityType::Code,\n\t\t\t\t\t\t{})),\n\t\t\t\t\t(oneLine\n\t\t\t\t\t\t? table->st().defaultValue\n\t\t\t\t\t\t: st::proxyApplyBoxValueMultiline),\n\t\t\t\t\tst::defaultPopupMenu);\n\t\t\t\taddRow(std::move(label), std::move(widget));\n\t\t\t}\n\t\t};\n\t\tif (!displayServer.isEmpty()) {\n\t\t\tadd(displayServer, tr::lng_proxy_box_server());\n\t\t}\n\t\tadd(QString::number(proxy.port), tr::lng_proxy_box_port());\n\t\tif (type == Type::Socks5) {\n\t\t\tadd(proxy.user, tr::lng_proxy_box_username());\n\t\t\tadd(proxy.password, tr::lng_proxy_box_password());\n\t\t} else if (type == Type::Mtproto) {\n\t\t\tadd(proxy.password, tr::lng_proxy_box_secret());\n\t\t}\n\n\t\t{\n\t\t\tstruct ProxyCheckStatusState {\n\t\t\t\tChecker v4;\n\t\t\t\tChecker v6;\n\t\t\t\trpl::variable<TextWithEntities> statusValue;\n\t\t\t\tbool finished = false;\n\t\t\t};\n\t\t\tconst auto state\n\t\t\t\t= box->lifetime().make_state<ProxyCheckStatusState>();\n\t\t\tstate->statusValue = Ui::Text::Link(\n\t\t\t\ttr::lng_proxy_box_check_status(tr::now));\n\t\t\tconst auto weak = base::make_weak(box);\n\t\t\tauto statusWidget = object_ptr<Ui::FlatLabel>(\n\t\t\t\ttable,\n\t\t\t\tstate->statusValue.value(),\n\t\t\t\ttable->st().defaultValue,\n\t\t\t\tst::defaultPopupMenu);\n\t\t\tconst auto statusLabel = statusWidget.data();\n\t\t\taddRow(tr::lng_proxy_box_status(), std::move(statusWidget));\n\t\t\tconst auto relayout = [=] {\n\t\t\t\ttable->resizeToWidth(table->width());\n\t\t\t};\n\t\t\tconst auto setUnavailable = [=] {\n\t\t\t\tstate->statusValue = TextWithEntities{\n\t\t\t\t\ttr::lng_proxy_box_table_unavailable(tr::now),\n\t\t\t\t};\n\t\t\t\tstatusLabel->setTextColorOverride(\n\t\t\t\t\tst::proxyRowStatusFgOffline->c);\n\t\t\t\trelayout();\n\t\t\t};\n\t\t\tconst auto runCheck = [=] {\n\t\t\t\tif (!weak) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto account = controller\n\t\t\t\t\t? &controller->session().account()\n\t\t\t\t\t: &Core::App().activeAccount();\n\t\t\t\tstate->finished = false;\n\t\t\t\tstate->statusValue = TextWithEntities{\n\t\t\t\t\ttr::lng_proxy_box_table_checking(tr::now),\n\t\t\t\t};\n\t\t\t\tstatusLabel->setTextColorOverride(st::proxyRowStatusFg->c);\n\t\t\t\trelayout();\n\t\t\t\tStartProxyCheck(\n\t\t\t\t\t&account->mtp(),\n\t\t\t\t\tproxy,\n\t\t\t\t\tstate->v4,\n\t\t\t\t\tstate->v6,\n\t\t\t\t\t[=](Connection *raw, int ping) {\n\t\t\t\t\t\tif (!weak || state->finished) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tDropProxyChecker(state->v4, state->v6, raw);\n\t\t\t\t\t\tstate->finished = true;\n\t\t\t\t\t\tResetProxyCheckers(state->v4, state->v6);\n\t\t\t\t\t\tstate->statusValue = TextWithEntities{\n\t\t\t\t\t\t\ttr::lng_proxy_box_table_available(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_ping,\n\t\t\t\t\t\t\t\tQString::number(ping)),\n\t\t\t\t\t\t};\n\t\t\t\t\t\tstatusLabel->setTextColorOverride(\n\t\t\t\t\t\t\tst::proxyRowStatusFgAvailable->c);\n\t\t\t\t\t\trelayout();\n\t\t\t\t\t},\n\t\t\t\t\t[=](Connection *raw) {\n\t\t\t\t\t\tif (!weak || state->finished) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tDropProxyChecker(state->v4, state->v6, raw);\n\t\t\t\t\t\tif (!HasProxyCheckers(state->v4, state->v6)) {\n\t\t\t\t\t\t\tstate->finished = true;\n\t\t\t\t\t\t\tsetUnavailable();\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\tif (!HasProxyCheckers(state->v4, state->v6)) {\n\t\t\t\t\tstate->finished = true;\n\t\t\t\t\tsetUnavailable();\n\t\t\t\t}\n\t\t\t};\n\t\t\tstatusLabel->setClickHandlerFilter([=](const auto &...) {\n\t\t\t\tauto &proxy = Core::App().settings().proxy();\n\t\t\t\tif (proxy.checkIpWarningShown()) {\n\t\t\t\t\trunCheck();\n\t\t\t\t} else {\n\t\t\t\t\tbox->uiShow()->showBox(Ui::MakeConfirmBox({\n\t\t\t\t\t\t.text = tr::lng_proxy_check_ip_warning(),\n\t\t\t\t\t\t.confirmed = [=] {\n\t\t\t\t\t\t\tauto &proxy = Core::App().settings().proxy();\n\t\t\t\t\t\t\tproxy.setCheckIpWarningShown(true);\n\t\t\t\t\t\t\tLocal::writeSettings();\n\t\t\t\t\t\t\trunCheck();\n\t\t\t\t\t\t},\n\t\t\t\t\t\t.confirmText = tr::lng_proxy_check_ip_proceed(),\n\t\t\t\t\t\t.title = tr::lng_proxy_check_ip_warning_title(),\n\t\t\t\t\t}));\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t});\n\t\t}\n\n\t\tif (type == Type::Mtproto) {\n\t\t\ttable->addRow(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\ttable,\n\t\t\t\t\ttr::lng_proxy_sponsor_warning(),\n\t\t\t\t\tst::proxyApplyBoxSponsorLabel),\n\t\t\t\tobject_ptr<Ui::RpWidget>(nullptr),\n\t\t\t\tst::proxyApplyBoxSponsorMargin,\n\t\t\t\tst::proxyApplyBoxSponsorMargin);\n\t\t}\n\n\t\tconst auto enableButton = box->addButton(\n\t\t\ttr::lng_proxy_box_table_button(),\n\t\t\t[=] {\n\t\t\t\tauto &proxies = Core::App().settings().proxy().list();\n\t\t\t\tif (!ranges::contains(proxies, proxy)) {\n\t\t\t\t\tproxies.push_back(proxy);\n\t\t\t\t}\n\t\t\t\tCore::App().setCurrentProxy(\n\t\t\t\t\tproxy,\n\t\t\t\t\tProxyData::Settings::Enabled);\n\t\t\t\tLocal::writeSettings();\n\t\t\t\tbox->closeBox();\n\t\t\t});\n\t\tenableButton->setFullRadius(true);\n\t\tbox->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\t\tif ((e->type() != QEvent::KeyPress) || !enableButton) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto k = static_cast<QKeyEvent*>(e.get());\n\t\t\tif (k->key() == Qt::Key_Enter || k->key() == Qt::Key_Return) {\n\t\t\t\tenableButton->clicked({}, Qt::LeftButton);\n\t\t\t}\n\t\t}, box->lifetime());\n\t};\n\tif (controller) {\n\t\tcontroller->uiShow()->showBox(Box(box));\n\t} else {\n\t\tUi::show(Box(box));\n\t}\n}\n\nauto ProxiesBoxController::proxySettingsValue() const\n-> rpl::producer<ProxyData::Settings> {\n\treturn _proxySettingsChanges.events_starting_with_copy(\n\t\t_settings.settings()\n\t) | rpl::distinct_until_changed();\n}\n\nvoid ProxiesBoxController::refreshChecker(Item &item) {\n\titem.state = ItemState::Checking;\n\tconst auto id = item.id;\n\tStartProxyCheck(\n\t\t&_account->mtp(),\n\t\titem.data,\n\t\titem.checker,\n\t\titem.checkerv6,\n\t\t[=](Connection *raw, int pingTime) {\n\t\t\tconst auto item = ranges::find(\n\t\t\t\t_list,\n\t\t\t\tid,\n\t\t\t\t[](const Item &item) { return item.id; });\n\t\t\tif (item == end(_list)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tDropProxyChecker(item->checker, item->checkerv6, raw);\n\t\t\tResetProxyCheckers(item->checker, item->checkerv6);\n\t\t\tif (item->state == ItemState::Checking) {\n\t\t\t\titem->state = ItemState::Available;\n\t\t\t\titem->ping = pingTime;\n\t\t\t\tupdateView(*item);\n\t\t\t}\n\t\t},\n\t\t[=](Connection *raw) {\n\t\t\tconst auto item = ranges::find(\n\t\t\t\t_list,\n\t\t\t\tid,\n\t\t\t\t[](const Item &item) { return item.id; });\n\t\t\tif (item == end(_list)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tDropProxyChecker(item->checker, item->checkerv6, raw);\n\t\t\tif (!HasProxyCheckers(item->checker, item->checkerv6)\n\t\t\t\t&& item->state == ItemState::Checking) {\n\t\t\t\titem->state = ItemState::Unavailable;\n\t\t\t\tupdateView(*item);\n\t\t\t}\n\t\t});\n\tif (!HasProxyCheckers(item.checker, item.checkerv6)) {\n\t\titem.state = ItemState::Unavailable;\n\t}\n}\n\nobject_ptr<Ui::BoxContent> ProxiesBoxController::CreateOwningBox(\n\t\tnot_null<Main::Account*> account,\n\t\tconst QString &highlightId) {\n\tauto controller = std::make_unique<ProxiesBoxController>(account);\n\tauto box = controller->create(highlightId);\n\tUi::AttachAsChild(box, std::move(controller));\n\treturn box;\n}\n\nobject_ptr<Ui::BoxContent> ProxiesBoxController::create(\n\t\tconst QString &highlightId) {\n\tauto result = Box<ProxiesBox>(this, _settings, highlightId);\n\t_show = result->uiShow();\n\tfor (const auto &item : _list) {\n\t\tupdateView(item);\n\t}\n\treturn result;\n}\n\nauto ProxiesBoxController::findById(int id) -> std::vector<Item>::iterator {\n\tconst auto result = ranges::find(\n\t\t_list,\n\t\tid,\n\t\t[](const Item &item) { return item.id; });\n\tAssert(result != end(_list));\n\treturn result;\n}\n\nauto ProxiesBoxController::findByProxy(const ProxyData &proxy)\n->std::vector<Item>::iterator {\n\treturn ranges::find(\n\t\t_list,\n\t\tproxy,\n\t\t[](const Item &item) { return item.data; });\n}\n\nvoid ProxiesBoxController::deleteItem(int id) {\n\tsetDeleted(id, true);\n}\n\nvoid ProxiesBoxController::deleteItems() {\n\tfor (const auto &item : _list) {\n\t\tsetDeleted(item.id, true);\n\t}\n}\n\nvoid ProxiesBoxController::restoreItem(int id) {\n\tsetDeleted(id, false);\n}\n\nvoid ProxiesBoxController::shareItem(int id, bool qr) {\n\tshare(findById(id)->data, qr);\n}\n\nvoid ProxiesBoxController::shareItems() {\n\tauto result = QString();\n\tfor (const auto &item : _list) {\n\t\tif (!item.deleted) {\n\t\t\tresult += ProxyDataToString(item.data) + '\\n' + '\\n';\n\t\t}\n\t}\n\tif (result.isEmpty()) {\n\t\treturn;\n\t}\n\tQGuiApplication::clipboard()->setText(result);\n\t_show->showToast(tr::lng_proxy_edit_share_list_toast(tr::now));\n}\n\nvoid ProxiesBoxController::applyItem(int id) {\n\tauto item = findById(id);\n\tif (_settings.isEnabled() && (_settings.selected() == item->data)) {\n\t\treturn;\n\t} else if (item->deleted) {\n\t\treturn;\n\t}\n\n\tauto j = findByProxy(_settings.selected());\n\n\tCore::App().setCurrentProxy(\n\t\titem->data,\n\t\tProxyData::Settings::Enabled);\n\tsaveDelayed();\n\n\tif (j != end(_list)) {\n\t\tupdateView(*j);\n\t}\n\tupdateView(*item);\n}\n\nvoid ProxiesBoxController::setDeleted(int id, bool deleted) {\n\tauto item = findById(id);\n\titem->deleted = deleted;\n\n\tif (deleted) {\n\t\tauto &proxies = _settings.list();\n\t\tproxies.erase(ranges::remove(proxies, item->data), end(proxies));\n\n\t\tif (item->data == _settings.selected()) {\n\t\t\t_lastSelectedProxy = _settings.selected();\n\t\t\t_settings.setSelected(MTP::ProxyData());\n\t\t\tif (_settings.isEnabled()) {\n\t\t\t\t_lastSelectedProxyUsed = true;\n\t\t\t\tCore::App().setCurrentProxy(\n\t\t\t\t\tProxyData(),\n\t\t\t\t\tProxyData::Settings::System);\n\t\t\t\tsaveDelayed();\n\t\t\t} else {\n\t\t\t\t_lastSelectedProxyUsed = false;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tauto &proxies = _settings.list();\n\t\tif (ranges::find(proxies, item->data) == end(proxies)) {\n\t\t\tauto insertBefore = item + 1;\n\t\t\twhile (insertBefore != end(_list) && insertBefore->deleted) {\n\t\t\t\t++insertBefore;\n\t\t\t}\n\t\t\tauto insertBeforeIt = (insertBefore == end(_list))\n\t\t\t\t? end(proxies)\n\t\t\t\t: ranges::find(proxies, insertBefore->data);\n\t\t\tproxies.insert(insertBeforeIt, item->data);\n\t\t}\n\n\t\tif (!_settings.selected() && _lastSelectedProxy == item->data) {\n\t\t\tAssert(!_settings.isEnabled());\n\n\t\t\tif (base::take(_lastSelectedProxyUsed)) {\n\t\t\t\tCore::App().setCurrentProxy(\n\t\t\t\t\tbase::take(_lastSelectedProxy),\n\t\t\t\t\tProxyData::Settings::Enabled);\n\t\t\t} else {\n\t\t\t\t_settings.setSelected(base::take(_lastSelectedProxy));\n\t\t\t}\n\t\t}\n\t}\n\tsaveDelayed();\n\tupdateView(*item);\n}\n\nobject_ptr<Ui::BoxContent> ProxiesBoxController::editItemBox(int id) {\n\treturn Box<ProxyBox>(findById(id)->data, [=](const ProxyData &result) {\n\t\tauto i = findById(id);\n\t\tauto j = ranges::find(\n\t\t\t_list,\n\t\t\tresult,\n\t\t\t[](const Item &item) { return item.data; });\n\t\tif (j != end(_list) && j != i) {\n\t\t\treplaceItemWith(i, j);\n\t\t} else {\n\t\t\treplaceItemValue(i, result);\n\t\t}\n\t}, [=](const ProxyData &proxy) {\n\t\tshare(proxy);\n\t});\n}\n\nvoid ProxiesBoxController::replaceItemWith(\n\t\tstd::vector<Item>::iterator which,\n\t\tstd::vector<Item>::iterator with) {\n\tauto &proxies = _settings.list();\n\tproxies.erase(ranges::remove(proxies, which->data), end(proxies));\n\n\t_views.fire({ which->id });\n\t_list.erase(which);\n\n\tif (with->deleted) {\n\t\trestoreItem(with->id);\n\t}\n\tapplyItem(with->id);\n\tsaveDelayed();\n}\n\nvoid ProxiesBoxController::replaceItemValue(\n\t\tstd::vector<Item>::iterator which,\n\t\tconst ProxyData &proxy) {\n\tif (which->deleted) {\n\t\trestoreItem(which->id);\n\t}\n\n\tauto &proxies = _settings.list();\n\tconst auto i = ranges::find(proxies, which->data);\n\tAssert(i != end(proxies));\n\t*i = proxy;\n\twhich->data = proxy;\n\trefreshChecker(*which);\n\n\tapplyItem(which->id);\n\tsaveDelayed();\n}\n\nobject_ptr<Ui::BoxContent> ProxiesBoxController::addNewItemBox() {\n\treturn Box<ProxyBox>(ProxyData(), [=](const ProxyData &result) {\n\t\tauto j = ranges::find(\n\t\t\t_list,\n\t\t\tresult,\n\t\t\t[](const Item &item) { return item.data; });\n\t\tif (j != end(_list)) {\n\t\t\tif (j->deleted) {\n\t\t\t\trestoreItem(j->id);\n\t\t\t}\n\t\t\tapplyItem(j->id);\n\t\t} else {\n\t\t\taddNewItem(result);\n\t\t}\n\t}, [=](const ProxyData &proxy) {\n\t\tshare(proxy);\n\t});\n}\n\nbool ProxiesBoxController::contains(const ProxyData &proxy) const {\n\tconst auto j = ranges::find(\n\t\t_list,\n\t\tproxy,\n\t\t[](const Item &item) { return item.data; });\n\treturn (j != end(_list));\n}\n\nvoid ProxiesBoxController::addNewItem(const ProxyData &proxy) {\n\tauto &proxies = _settings.list();\n\tproxies.push_back(proxy);\n\n\t_list.push_back({ ++_idCounter, proxy });\n\trefreshChecker(_list.back());\n\tapplyItem(_list.back().id);\n}\n\nbool ProxiesBoxController::setProxySettings(ProxyData::Settings value) {\n\tif (_settings.settings() == value) {\n\t\treturn true;\n\t} else if (value == ProxyData::Settings::Enabled) {\n\t\tif (_settings.list().empty()) {\n\t\t\treturn false;\n\t\t} else if (!_settings.selected()) {\n\t\t\t_settings.setSelected(_settings.list().back());\n\t\t\tauto j = findByProxy(_settings.selected());\n\t\t\tif (j != end(_list)) {\n\t\t\t\tupdateView(*j);\n\t\t\t}\n\t\t}\n\t}\n\tCore::App().setCurrentProxy(_settings.selected(), value);\n\tsaveDelayed();\n\treturn true;\n}\n\nvoid ProxiesBoxController::setProxyForCalls(bool enabled) {\n\tif (_settings.useProxyForCalls() == enabled) {\n\t\treturn;\n\t}\n\t_settings.setUseProxyForCalls(enabled);\n\tif (_settings.isEnabled() && _settings.selected().supportsCalls()) {\n\t\t_settings.connectionTypeChangesNotify();\n\t}\n\tsaveDelayed();\n}\n\nvoid ProxiesBoxController::setTryIPv6(bool enabled) {\n\tif (Core::App().settings().proxy().tryIPv6() == enabled) {\n\t\treturn;\n\t}\n\tCore::App().settings().proxy().setTryIPv6(enabled);\n\t_account->mtp().restart();\n\t_settings.connectionTypeChangesNotify();\n\tsaveDelayed();\n}\n\nvoid ProxiesBoxController::saveDelayed() {\n\t_saveTimer.callOnce(kSaveSettingsDelayedTimeout);\n}\n\nauto ProxiesBoxController::views() const -> rpl::producer<ItemView> {\n\treturn _views.events();\n}\n\nrpl::producer<bool> ProxiesBoxController::listShareableChanges() const {\n\treturn _views.events_starting_with(ItemView()) | rpl::map([=] {\n\t\tfor (const auto &item : _list) {\n\t\t\tif (!item.deleted) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t});\n}\n\nvoid ProxiesBoxController::updateView(const Item &item) {\n\tconst auto selected = (_settings.selected() == item.data);\n\tconst auto deleted = item.deleted;\n\tconst auto type = [&] {\n\t\tswitch (item.data.type) {\n\t\tcase Type::Http: return u\"HTTP\"_q;\n\t\tcase Type::Socks5: return u\"SOCKS5\"_q;\n\t\tcase Type::Mtproto: return u\"MTPROTO\"_q;\n\t\t}\n\t\tUnexpected(\"Proxy type in ProxiesBoxController::updateView.\");\n\t}();\n\tconst auto state = [&] {\n\t\tif (!selected || !_settings.isEnabled()) {\n\t\t\treturn item.state;\n\t\t} else if (_account->mtp().dcstate() == MTP::ConnectedState) {\n\t\t\treturn ItemState::Online;\n\t\t}\n\t\treturn ItemState::Connecting;\n\t}();\n\tconst auto supportsShare = (item.data.type == Type::Socks5)\n\t\t|| (item.data.type == Type::Mtproto);\n\tconst auto supportsCalls = item.data.supportsCalls();\n\t_views.fire({\n\t\titem.id,\n\t\ttype,\n\t\titem.data.host,\n\t\titem.data.port,\n\t\titem.ping,\n\t\t!deleted && selected,\n\t\tdeleted,\n\t\t!deleted && supportsShare,\n\t\tsupportsCalls,\n\t\tstate,\n\t});\n}\n\nvoid ProxiesBoxController::share(const ProxyData &proxy, bool qr) {\n\tif (proxy.type == Type::Http) {\n\t\treturn;\n\t}\n\tconst auto link = ProxyDataToString(proxy);\n\tif (qr) {\n\t\t_show->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tUi::FillPeerQrBox(box, nullptr, link, rpl::single(QString()));\n\t\t\tbox->setTitle(tr::lng_proxy_edit_share_qr_box_title());\n\t\t}));\n\t\treturn;\n\t}\n\tQGuiApplication::clipboard()->setText(link);\n\t_show->showToast(tr::lng_username_copied(tr::now));\n}\n\nvoid ProxiesBoxController::Show(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &highlightId) {\n\tcontroller->show(\n\t\tCreateOwningBox(&controller->session().account(), highlightId));\n}\n\nProxiesBoxController::~ProxiesBoxController() {\n\tif (_saveTimer.isActive()) {\n\t\tbase::call_delayed(\n\t\t\tkSaveSettingsDelayedTimeout,\n\t\t\tQCoreApplication::instance(),\n\t\t\t[] { Local::writeSettings(); });\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/connection_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"base/object_ptr.h\"\n#include \"core/core_settings_proxy.h\"\n#include \"mtproto/connection_abstract.h\"\n#include \"mtproto/mtproto_proxy_data.h\"\n\nnamespace Ui {\nclass Show;\nclass BoxContent;\nclass InputField;\nclass PortInput;\nclass PasswordInput;\nclass Checkbox;\ntemplate <typename Enum>\nclass RadioenumGroup;\ntemplate <typename Enum>\nclass Radioenum;\n} // namespace Ui\n\nnamespace Main {\nclass Account;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nclass ProxiesBoxController {\npublic:\n\tusing ProxyData = MTP::ProxyData;\n\tusing Type = ProxyData::Type;\n\n\texplicit ProxiesBoxController(not_null<Main::Account*> account);\n\n\tstatic void ShowApplyConfirmation(\n\t\tWindow::SessionController *controller,\n\t\tType type,\n\t\tconst QMap<QString, QString> &fields);\n\n\tstatic object_ptr<Ui::BoxContent> CreateOwningBox(\n\t\tnot_null<Main::Account*> account,\n\t\tconst QString &highlightId = QString());\n\tstatic void Show(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &highlightId = QString());\n\tobject_ptr<Ui::BoxContent> create(const QString &highlightId = QString());\n\n\tenum class ItemState {\n\t\tConnecting,\n\t\tOnline,\n\t\tChecking,\n\t\tAvailable,\n\t\tUnavailable\n\t};\n\tstruct ItemView {\n\t\tint id = 0;\n\t\tQString type;\n\t\tQString host;\n\t\tuint32 port = 0;\n\t\tint ping = 0;\n\t\tbool selected = false;\n\t\tbool deleted = false;\n\t\tbool supportsShare = false;\n\t\tbool supportsCalls = false;\n\t\tItemState state = ItemState::Checking;\n\n\t};\n\n\tvoid deleteItem(int id);\n\tvoid deleteItems();\n\tvoid restoreItem(int id);\n\tvoid shareItem(int id, bool qr);\n\tvoid shareItems();\n\tvoid applyItem(int id);\n\tobject_ptr<Ui::BoxContent> editItemBox(int id);\n\tobject_ptr<Ui::BoxContent> addNewItemBox();\n\tbool setProxySettings(ProxyData::Settings value);\n\tvoid setProxyForCalls(bool enabled);\n\tvoid setTryIPv6(bool enabled);\n\trpl::producer<ProxyData::Settings> proxySettingsValue() const;\n\n\t[[nodiscard]] bool contains(const ProxyData &proxy) const;\n\tvoid addNewItem(const ProxyData &proxy);\n\n\trpl::producer<ItemView> views() const;\n\n\trpl::producer<bool> listShareableChanges() const;\n\n\t~ProxiesBoxController();\n\nprivate:\n\tusing Checker = MTP::details::ConnectionPointer;\n\tstruct Item {\n\t\tint id = 0;\n\t\tProxyData data;\n\t\tbool deleted = false;\n\t\tChecker checker;\n\t\tChecker checkerv6;\n\t\tItemState state = ItemState::Checking;\n\t\tint ping = 0;\n\n\t};\n\n\tstd::vector<Item>::iterator findById(int id);\n\tstd::vector<Item>::iterator findByProxy(const ProxyData &proxy);\n\tvoid setDeleted(int id, bool deleted);\n\tvoid updateView(const Item &item);\n\tvoid share(const ProxyData &proxy, bool qr = false);\n\tvoid saveDelayed();\n\tvoid refreshChecker(Item &item);\n\n\tvoid replaceItemWith(\n\t\tstd::vector<Item>::iterator which,\n\t\tstd::vector<Item>::iterator with);\n\tvoid replaceItemValue(\n\t\tstd::vector<Item>::iterator which,\n\t\tconst ProxyData &proxy);\n\n\tconst not_null<Main::Account*> _account;\n\tCore::SettingsProxy &_settings;\n\tint _idCounter = 0;\n\tstd::vector<Item> _list;\n\trpl::event_stream<ItemView> _views;\n\tbase::Timer _saveTimer;\n\trpl::event_stream<ProxyData::Settings> _proxySettingsChanges;\n\tstd::shared_ptr<Ui::Show> _show;\n\n\tProxyData _lastSelectedProxy;\n\tbool _lastSelectedProxyUsed = false;\n\n\trpl::lifetime _lifetime;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/contacts_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop version of Telegram messaging app, see https://telegram.org\n\nTelegram Desktop is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nIt is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nIn addition, as a special exception, the copyright holders give permission\nto link the code of portions of this program with the OpenSSL library.\n\nFull license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE\nCopyright (c) 2014-2017 John Preston, https://desktop.telegram.org\n*/\n#include \"boxes/contacts_box.h\"\n\n#include \"dialogs/dialogs_indexed_list.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_history.h\"\n#include \"styles/style_profile.h\"\n#include \"lang/lang_keys.h\"\n#include \"boxes/add_contact_box.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"messenger.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"core/file_utilities.h\"\n#include \"ui/widgets/multi_select.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/effects/widget_slide_wrap.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"boxes/photo_crop_box.h\"\n#include \"boxes/confirm_box.h\"\n#include \"boxes/edit_participant_box.h\"\n#include \"window/themes/window_theme.h\"\n#include \"observer_peer.h\"\n#include \"apiwrap.h\"\n#include \"auth_session.h\"\n#include \"storage/file_download.h\"\n\nQString PeerFloodErrorText(PeerFloodType type) {\n\tauto link = textcmdLink(Messenger::Instance().createInternalLinkFull(qsl(\"spambot\")), lang(lng_cant_more_info));\n\tif (type == PeerFloodType::InviteGroup) {\n\t\treturn lng_cant_invite_not_contact(lt_more_info, link);\n\t}\n\treturn lng_cant_send_to_not_contact(lt_more_info, link);\n}\n\nContactsBox::ContactsBox(QWidget*, ChatData *chat, MembersFilter filter)\n: _chat(chat)\n, _membersFilter(filter)\n, _select(createMultiSelect())\n, _searchTimer(this) {\n}\n\nContactsBox::ContactsBox(QWidget*, ChannelData *channel)\n: _channel(channel)\n, _creating(CreatingGroupChannel)\n, _select(createMultiSelect())\n, _searchTimer(this) {\n}\n\nContactsBox::ContactsBox(QWidget*, ChannelData *channel, MembersFilter filter, const MembersAlreadyIn &already)\n: _channel(channel)\n, _membersFilter(filter)\n, _alreadyIn(already)\n, _select(createMultiSelect())\n, _searchTimer(this) {\n}\n\nContactsBox::ContactsBox(QWidget*, UserData *bot)\n: _bot(bot)\n, _select(createMultiSelect())\n, _searchTimer(this) {\n}\n\nContactsBox::ContactsBox(QWidget*, const QString &name, const QImage &photo)\n: _creating(CreatingGroupGroup)\n, _select(createMultiSelect())\n, _searchTimer(this)\n, _creationName(name)\n, _creationPhoto(photo) {\n}\n\nContactsBox::ContactsBox(QWidget*)\n: _select(createMultiSelect())\n, _searchTimer(this) {\n}\n\nvoid ContactsBox::prepare() {\n\t_select->resizeToWidth(st::boxWideWidth);\n\tmyEnsureResized(_select);\n\n\tauto createInner = [this] {\n\t\tif (_chat) {\n\t\t\treturn object_ptr<Inner>(this, _chat, _membersFilter);\n\t\t} else if (_channel) {\n\t\t\treturn object_ptr<Inner>(this, _channel, _membersFilter, _alreadyIn);\n\t\t} else if (_bot) {\n\t\t\treturn object_ptr<Inner>(this, _bot);\n\t\t}\n\t\treturn object_ptr<Inner>(this, _creating);\n\t};\n\t_inner = setInnerWidget(createInner(), getTopScrollSkip());\n\n\tupdateTitle();\n\tif (_chat) {\n\t\tif (_membersFilter == MembersFilter::Admins) {\n\t\t\taddButton(langFactory(lng_settings_save), [this] { saveChatAdmins(); });\n\t\t} else {\n\t\t\taddButton(langFactory(lng_participant_invite), [this] { inviteParticipants(); });\n\t\t}\n\t\taddButton(langFactory(lng_cancel), [this] { closeBox(); });\n\t} else if (_channel) {\n\t\tif (_membersFilter != MembersFilter::Admins) {\n\t\t\taddButton(langFactory(lng_participant_invite), [this] { inviteParticipants(); });\n\t\t}\n\t\taddButton(langFactory((_creating == CreatingGroupChannel) ? lng_create_group_skip : lng_cancel), [this] { closeBox(); });\n\t} else if (_bot) {\n\t\taddButton(langFactory(lng_close), [this] { closeBox(); });\n\t} else if (_creating == CreatingGroupGroup) {\n\t\taddButton(langFactory(lng_create_group_create), [this] { createGroup(); });\n\t\taddButton(langFactory(lng_create_group_back), [this] { closeBox(); });\n\t} else {\n\t\taddButton(langFactory(lng_close), [this] { closeBox(); });\n\t\taddLeftButton(langFactory(lng_profile_add_contact), [] { App::wnd()->onShowAddContact(); });\n\t}\n\n\t_inner->setPeerSelectedChangedCallback([this](PeerData *peer, bool checked) {\n\t\tonPeerSelectedChanged(peer, checked);\n\t});\n\tfor (auto i : _inner->selected()) {\n\t\taddPeerToMultiSelect(i, true);\n\t}\n\t_inner->setAllAdminsChangedCallback([this] {\n\t\t_select->toggleAnimated(!_inner->allAdmins());\n\t\tif (_inner->allAdmins()) {\n\t\t\t_select->entity()->clearQuery();\n\t\t\t_inner->setFocus();\n\t\t} else {\n\t\t\t_select->entity()->setInnerFocus();\n\t\t}\n\t\tupdateScrollSkips();\n\t});\n\t_select->toggleFast(!_inner->chat() || (_inner->membersFilter() != MembersFilter::Admins) || !_inner->allAdmins());\n\t_select->entity()->setQueryChangedCallback([this](const QString &query) { onFilterUpdate(query); });\n\t_select->entity()->setItemRemovedCallback([this](uint64 itemId) {\n\t\tif (auto peer = App::peerLoaded(itemId)) {\n\t\t\t_inner->peerUnselected(peer);\n\t\t\tupdate();\n\t\t}\n\t});\n\t_select->entity()->setSubmittedCallback([this](bool) { onSubmit(); });\n\tconnect(_inner, SIGNAL(mustScrollTo(int, int)), this, SLOT(onScrollToY(int, int)));\n\tconnect(_inner, SIGNAL(searchByUsername()), this, SLOT(onNeedSearchByUsername()));\n\tconnect(_inner, SIGNAL(adminAdded()), this, SIGNAL(adminAdded()));\n\n\t_searchTimer->setSingleShot(true);\n\tconnect(_searchTimer, SIGNAL(timeout()), this, SLOT(onSearchByUsername()));\n\n\tsetDimensions(st::boxWideWidth, st::boxMaxListHeight);\n\n\t_select->raise();\n}\n\nbool ContactsBox::onSearchByUsername(bool searchCache) {\n\tauto q = _select->entity()->getQuery();\n\tif (q.isEmpty()) {\n\t\tif (_peopleRequest) {\n\t\t\t_peopleRequest = 0;\n\t\t}\n\t\treturn true;\n\t}\n\tif (q.size() >= MinUsernameLength) {\n\t\tif (searchCache) {\n\t\t\tPeopleCache::const_iterator i = _peopleCache.constFind(q);\n\t\t\tif (i != _peopleCache.cend()) {\n\t\t\t\t_peopleQuery = q;\n\t\t\t\t_peopleRequest = 0;\n\t\t\t\tpeopleReceived(i.value(), 0);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (_peopleQuery != q) {\n\t\t\t_peopleQuery = q;\n\t\t\t_peopleFull = false;\n\t\t\t_peopleRequest = MTP::send(MTPcontacts_Search(MTP_string(_peopleQuery), MTP_int(SearchPeopleLimit)), rpcDone(&ContactsBox::peopleReceived), rpcFail(&ContactsBox::peopleFailed));\n\t\t\t_peopleQueries.insert(_peopleRequest, _peopleQuery);\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid ContactsBox::updateTitle() {\n\tif (_chat && _membersFilter == MembersFilter::Admins) {\n\t\tsetTitle(langFactory(lng_channel_admins));\n\t} else if (_chat || _creating != CreatingGroupNone) {\n\t\tauto addingAdmin = _channel && (_membersFilter == MembersFilter::Admins);\n\t\tauto additional = (addingAdmin || (_inner->channel() && !_inner->channel()->isMegagroup())) ? QString() : QString(\"%1 / %2\").arg(_inner->selectedCount()).arg(Global::MegagroupSizeMax());\n\t\tsetTitle(langFactory(addingAdmin ? lng_channel_add_admin : lng_profile_add_participant));\n\t\tsetAdditionalTitle([additional] { return additional; });\n\t} else if (_inner->sharingBotGame()) {\n\t\tsetTitle(langFactory(lng_bot_choose_chat));\n\t} else if (_inner->bot()) {\n\t\tsetTitle(langFactory(lng_bot_choose_group));\n\t} else {\n\t\tsetTitle(langFactory(lng_contacts_header));\n\t}\n}\n\nvoid ContactsBox::onNeedSearchByUsername() {\n\tif (!onSearchByUsername(true)) {\n\t\t_searchTimer->start(AutoSearchTimeout);\n\t}\n}\n\nvoid ContactsBox::peopleReceived(const MTPcontacts_Found &result, mtpRequestId req) {\n\tQString q = _peopleQuery;\n\n\tPeopleQueries::iterator i = _peopleQueries.find(req);\n\tif (i != _peopleQueries.cend()) {\n\t\tq = i.value();\n\t\t_peopleCache[q] = result;\n\t\t_peopleQueries.erase(i);\n\t}\n\n\tif (_peopleRequest == req) {\n\t\tswitch (result.type()) {\n\t\tcase mtpc_contacts_found: {\n\t\t\tApp::feedUsers(result.c_contacts_found().vusers);\n\t\t\tApp::feedChats(result.c_contacts_found().vchats);\n\t\t\t_inner->peopleReceived(q, result.c_contacts_found().vresults.v);\n\t\t} break;\n\t\t}\n\n\t\t_peopleRequest = 0;\n\t\t_inner->updateSelection();\n\t}\n}\n\nbool ContactsBox::peopleFailed(const RPCError &error, mtpRequestId req) {\n\tif (MTP::isDefaultHandledError(error)) return false;\n\n\tif (_peopleRequest == req) {\n\t\t_peopleRequest = 0;\n\t\t_peopleFull = true;\n\t}\n\treturn true;\n}\n\nvoid ContactsBox::setInnerFocus() {\n\tif (_select->isHidden()) {\n\t\t_inner->setFocus();\n\t} else {\n\t\t_select->entity()->setInnerFocus();\n\t}\n}\n\nvoid ContactsBox::onSubmit() {\n\t_inner->chooseParticipant();\n}\n\nvoid ContactsBox::keyPressEvent(QKeyEvent *e) {\n\tauto focused = focusWidget();\n\tif (_select == focused || _select->isAncestorOf(focusWidget())) {\n\t\tif (e->key() == Qt::Key_Down) {\n\t\t\t_inner->selectSkip(1);\n\t\t} else if (e->key() == Qt::Key_Up) {\n\t\t\t_inner->selectSkip(-1);\n\t\t} else if (e->key() == Qt::Key_PageDown) {\n\t\t\t_inner->selectSkipPage(height() - getTopScrollSkip(), 1);\n\t\t} else if (e->key() == Qt::Key_PageUp) {\n\t\t\t_inner->selectSkipPage(height() - getTopScrollSkip(), -1);\n\t\t} else {\n\t\t\tBoxContent::keyPressEvent(e);\n\t\t}\n\t} else {\n\t\tBoxContent::keyPressEvent(e);\n\t}\n}\n\nobject_ptr<Ui::WidgetSlideWrap<Ui::MultiSelect>> ContactsBox::createMultiSelect() {\n\tauto entity = object_ptr<Ui::MultiSelect>(this, st::contactsMultiSelect, langFactory(lng_participant_filter));\n\tauto margins = style::margins(0, 0, 0, 0);\n\tauto callback = [this] { updateScrollSkips(); };\n\treturn object_ptr<Ui::WidgetSlideWrap<Ui::MultiSelect>>(this, std::move(entity), margins, std::move(callback));\n}\n\nint ContactsBox::getTopScrollSkip() const {\n\tauto result = 0;\n\tif (!_select->isHidden()) {\n\t\tresult += _select->height();\n\t}\n\treturn result;\n}\n\nvoid ContactsBox::updateScrollSkips() {\n\tsetInnerTopSkip(getTopScrollSkip(), true);\n}\n\nvoid ContactsBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\t_select->resizeToWidth(width());\n\t_select->moveToLeft(0, 0);\n\n\tupdateScrollSkips();\n\n\t_inner->resize(width(), _inner->height());\n}\n\nvoid ContactsBox::closeHook() {\n\tif (_channel && _creating == CreatingGroupChannel) {\n\t\tUi::showPeerHistory(_channel, ShowAtTheEndMsgId);\n\t}\n}\n\nvoid ContactsBox::onFilterUpdate(const QString &filter) {\n\tonScrollToY(0);\n\t_inner->updateFilter(filter);\n}\n\nvoid ContactsBox::addPeerToMultiSelect(PeerData *peer, bool skipAnimation) {\n\tusing AddItemWay = Ui::MultiSelect::AddItemWay;\n\tauto addItemWay = skipAnimation ? AddItemWay::SkipAnimation : AddItemWay::Default;\n\t_select->entity()->addItem(peer->id, peer->shortName(), st::activeButtonBg, PaintUserpicCallback(peer), addItemWay);\n}\n\nvoid ContactsBox::onPeerSelectedChanged(PeerData *peer, bool checked) {\n\tif (checked) {\n\t\taddPeerToMultiSelect(peer);\n\t\t_select->entity()->clearQuery();\n\t} else {\n\t\t_select->entity()->removeItem(peer->id);\n\t}\n\tupdateTitle();\n}\n\nvoid ContactsBox::inviteParticipants() {\n\tauto users = _inner->selected();\n\tif (users.empty()) {\n\t\t_select->entity()->setInnerFocus();\n\t\treturn;\n\t}\n\n\tApp::main()->addParticipants(_inner->chat() ? (PeerData*)_inner->chat() : _inner->channel(), users);\n\tif (_inner->chat()) {\n\t\tUi::hideLayer();\n\t\tUi::showPeerHistory(_inner->chat(), ShowAtTheEndMsgId);\n\t} else {\n\t\tcloseBox();\n\t}\n}\n\nvoid ContactsBox::createGroup() {\n\tif (_saveRequestId) return;\n\n\tauto users = _inner->selectedInputs();\n\tif (users.empty() || (users.size() == 1 && users.at(0).type() == mtpc_inputUserSelf)) {\n\t\t_select->entity()->setInnerFocus();\n\t\treturn;\n\t}\n\t_saveRequestId = MTP::send(MTPmessages_CreateChat(MTP_vector<MTPInputUser>(users), MTP_string(_creationName)), rpcDone(&ContactsBox::creationDone), rpcFail(&ContactsBox::creationFail));\n}\n\nvoid ContactsBox::saveChatAdmins() {\n\tif (_saveRequestId) return;\n\n\t_inner->saving(true);\n\t_saveRequestId = MTP::send(MTPmessages_ToggleChatAdmins(_inner->chat()->inputChat, MTP_bool(!_inner->allAdmins())), rpcDone(&ContactsBox::saveAdminsDone), rpcFail(&ContactsBox::saveAdminsFail));\n}\n\nvoid ContactsBox::saveAdminsDone(const MTPUpdates &result) {\n\tApp::main()->sentUpdatesReceived(result);\n\tsaveSelectedAdmins();\n}\n\nvoid ContactsBox::saveSelectedAdmins() {\n\tif (_inner->allAdmins() && !_inner->chat()->participants.isEmpty()) {\n\t\tcloseBox();\n\t} else {\n\t\t_saveRequestId = MTP::send(MTPmessages_GetFullChat(_inner->chat()->inputChat), rpcDone(&ContactsBox::getAdminsDone), rpcFail(&ContactsBox::saveAdminsFail));\n\t}\n}\n\nvoid ContactsBox::getAdminsDone(const MTPmessages_ChatFull &result) {\n\tApp::api()->processFullPeer(_inner->chat(), result);\n\tif (_inner->allAdmins()) {\n\t\tcloseBox();\n\t\treturn;\n\t}\n\tauto curadmins = _inner->chat()->admins;\n\tauto newadmins = _inner->selected();\n\tauto appoint = decltype(newadmins)();\n\tif (!newadmins.empty()) {\n\t\tappoint.reserve(newadmins.size());\n\t\tfor (auto &user : newadmins) {\n\t\t\tauto c = curadmins.find(user);\n\t\t\tif (c == curadmins.cend()) {\n\t\t\t\tif (user->id != peerFromUser(_inner->chat()->creator)) {\n\t\t\t\t\tappoint.push_back(user);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcuradmins.erase(c);\n\t\t\t}\n\t\t}\n\t}\n\t_saveRequestId = 0;\n\n\tfor_const (auto user, curadmins) {\n\t\tMTP::send(MTPmessages_EditChatAdmin(_inner->chat()->inputChat, user->inputUser, MTP_boolFalse()), rpcDone(&ContactsBox::removeAdminDone, user), rpcFail(&ContactsBox::editAdminFail), 0, 10);\n\t}\n\tfor_const (auto user, appoint) {\n\t\tMTP::send(MTPmessages_EditChatAdmin(_inner->chat()->inputChat, user->inputUser, MTP_boolTrue()), rpcDone(&ContactsBox::setAdminDone, user), rpcFail(&ContactsBox::editAdminFail), 0, 10);\n\t}\n\tMTP::sendAnything();\n\n\t_saveRequestId = curadmins.size() + appoint.size();\n\tif (!_saveRequestId) {\n\t\tcloseBox();\n\t}\n}\n\nvoid ContactsBox::setAdminDone(gsl::not_null<UserData*> user, const MTPBool &result) {\n\tif (mtpIsTrue(result)) {\n\t\tif (_inner->chat()->noParticipantInfo()) {\n\t\t\tApp::api()->requestFullPeer(_inner->chat());\n\t\t} else {\n\t\t\t_inner->chat()->admins.insert(user);\n\t\t}\n\t}\n\t--_saveRequestId;\n\tif (!_saveRequestId) {\n\t\temit App::main()->peerUpdated(_inner->chat());\n\t\tcloseBox();\n\t}\n}\n\nvoid ContactsBox::removeAdminDone(gsl::not_null<UserData*> user, const MTPBool &result) {\n\tif (mtpIsTrue(result)) {\n\t\t_inner->chat()->admins.remove(user);\n\t}\n\t--_saveRequestId;\n\tif (!_saveRequestId) {\n\t\temit App::main()->peerUpdated(_inner->chat());\n\t\tcloseBox();\n\t}\n}\n\nbool ContactsBox::saveAdminsFail(const RPCError &error) {\n\tif (MTP::isDefaultHandledError(error)) return true;\n\t_saveRequestId = 0;\n\t_inner->saving(false);\n\tif (error.type() == qstr(\"CHAT_NOT_MODIFIED\")) {\n\t\tsaveSelectedAdmins();\n\t}\n\treturn false;\n}\n\nbool ContactsBox::editAdminFail(const RPCError &error) {\n\tif (MTP::isDefaultHandledError(error)) return true;\n\t--_saveRequestId;\n\t_inner->chat()->invalidateParticipants();\n\tif (!_saveRequestId) {\n\t\tif (error.type() == qstr(\"USER_RESTRICTED\")) {\n\t\t\tUi::show(Box<InformBox>(lang(lng_cant_do_this)));\n\t\t\treturn true;\n\t\t}\n\t\tcloseBox();\n\t}\n\treturn false;\n}\n\nvoid ContactsBox::creationDone(const MTPUpdates &updates) {\n\tUi::hideLayer();\n\n\tApp::main()->sentUpdatesReceived(updates);\n\tconst QVector<MTPChat> *v = 0;\n\tswitch (updates.type()) {\n\tcase mtpc_updates: v = &updates.c_updates().vchats.v; break;\n\tcase mtpc_updatesCombined: v = &updates.c_updatesCombined().vchats.v; break;\n\tdefault: LOG((\"API Error: unexpected update cons %1 (ContactsBox::creationDone)\").arg(updates.type())); break;\n\t}\n\n\tPeerData *peer = 0;\n\tif (v && !v->isEmpty() && v->front().type() == mtpc_chat) {\n\t\tpeer = App::chat(v->front().c_chat().vid.v);\n\t\tif (peer) {\n\t\t\tif (!_creationPhoto.isNull()) {\n\t\t\t\tApp::app()->uploadProfilePhoto(_creationPhoto, peer->id);\n\t\t\t}\n\t\t\tUi::showPeerHistory(peer, ShowAtUnreadMsgId);\n\t\t}\n\t} else {\n\t\tLOG((\"API Error: chat not found in updates (ContactsBox::creationDone)\"));\n\t}\n}\n\nbool ContactsBox::creationFail(const RPCError &error) {\n\tif (MTP::isDefaultHandledError(error)) return false;\n\n\t_saveRequestId = 0;\n\tif (error.type() == \"NO_CHAT_TITLE\") {\n\t\tcloseBox();\n\t\treturn true;\n\t} else if (error.type() == \"USERS_TOO_FEW\") {\n\t\t_select->entity()->setInnerFocus();\n\t\treturn true;\n\t} else if (error.type() == \"PEER_FLOOD\") {\n\t\tUi::show(Box<InformBox>(PeerFloodErrorText(PeerFloodType::InviteGroup)), KeepOtherLayers);\n\t\treturn true;\n\t} else if (error.type() == qstr(\"USER_RESTRICTED\")) {\n\t\tUi::show(Box<InformBox>(lang(lng_cant_do_this)));\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nContactsBox::Inner::ContactData::ContactData() = default;\n\nContactsBox::Inner::ContactData::ContactData(PeerData *peer, base::lambda<void()> updateCallback)\n: checkbox(std::make_unique<Ui::RoundImageCheckbox>(st::contactsPhotoCheckbox, updateCallback, PaintUserpicCallback(peer))) {\n}\n\nContactsBox::Inner::ContactData::~ContactData() = default;\n\nContactsBox::Inner::Inner(QWidget *parent, CreatingGroupType creating) : TWidget(parent)\n, _rowHeight(st::contactsPadding.top() + st::contactsPhotoSize + st::contactsPadding.bottom())\n, _creating(creating)\n, _allAdmins(this, lang(lng_chat_all_members_admins), false, st::defaultBoxCheckbox)\n, _contacts(App::main()->contactsList())\n, _addContactLnk(this, lang(lng_add_contact_button)) {\n\tinit();\n}\n\nContactsBox::Inner::Inner(QWidget *parent, ChannelData *channel, MembersFilter membersFilter, const MembersAlreadyIn &already) : TWidget(parent)\n, _rowHeight(st::contactsPadding.top() + st::contactsPhotoSize + st::contactsPadding.bottom())\n, _channel(channel)\n, _membersFilter(membersFilter)\n, _creating(CreatingGroupChannel)\n, _already(already)\n, _allAdmins(this, lang(lng_chat_all_members_admins), false, st::defaultBoxCheckbox)\n, _contacts(App::main()->contactsList())\n, _addContactLnk(this, lang(lng_add_contact_button)) {\n\tinit();\n}\n\nnamespace {\n\tbool _sortByName(UserData *a, UserData *b) {\n\t\treturn a->name.compare(b->name, Qt::CaseInsensitive) < 0;\n\t}\n}\n\nContactsBox::Inner::Inner(QWidget *parent, ChatData *chat, MembersFilter membersFilter) : TWidget(parent)\n, _rowHeight(st::contactsPadding.top() + st::contactsPhotoSize + st::contactsPadding.bottom())\n, _chat(chat)\n, _membersFilter(membersFilter)\n, _allAdmins(this, lang(lng_chat_all_members_admins), !_chat->adminsEnabled(), st::defaultBoxCheckbox)\n, _aboutWidth(st::boxWideWidth - st::contactsPadding.left() - st::contactsPadding.right())\n, _aboutAllAdmins(st::defaultTextStyle, lang(lng_chat_about_all_admins), _defaultOptions, _aboutWidth)\n, _aboutAdmins(st::defaultTextStyle, lang(lng_chat_about_admins), _defaultOptions, _aboutWidth)\n, _customList((membersFilter == MembersFilter::Recent) ? std::unique_ptr<Dialogs::IndexedList>() : std::make_unique<Dialogs::IndexedList>(Dialogs::SortMode::Add))\n, _contacts((membersFilter == MembersFilter::Recent) ? App::main()->contactsList() : _customList.get())\n, _addContactLnk(this, lang(lng_add_contact_button)) {\n\tinitList();\n\tif (membersFilter == MembersFilter::Admins) {\n\t\t_aboutHeight = st::contactsAboutTop + qMax(_aboutAllAdmins.countHeight(_aboutWidth), _aboutAdmins.countHeight(_aboutWidth)) + st::contactsAboutBottom;\n\t\tif (_contacts->isEmpty()) {\n\t\t\tApp::api()->requestFullPeer(_chat);\n\t\t}\n\t}\n\tinit();\n}\n\ntemplate <typename FilterCallback>\nvoid ContactsBox::Inner::addDialogsToList(FilterCallback callback) {\n\tauto v = App::main()->dialogsList();\n\tfor_const (auto row, *v) {\n\t\tauto peer = row->history()->peer;\n\t\tif (callback(peer)) {\n\t\t\t_contacts->addToEnd(row->history());\n\t\t}\n\t}\n}\n\nContactsBox::Inner::Inner(QWidget *parent, UserData *bot) : TWidget(parent)\n, _rowHeight(st::contactsPadding.top() + st::contactsPhotoSize + st::contactsPadding.bottom())\n, _bot(bot)\n, _allAdmins(this, lang(lng_chat_all_members_admins), false, st::defaultBoxCheckbox)\n, _customList(std::make_unique<Dialogs::IndexedList>(Dialogs::SortMode::Add))\n, _contacts(_customList.get())\n, _addContactLnk(this, lang(lng_add_contact_button)) {\n\tif (sharingBotGame()) {\n\t\taddDialogsToList([](PeerData *peer) {\n\t\t\tif (peer->canWrite()) {\n\t\t\t\tif (auto channel = peer->asChannel()) {\n\t\t\t\t\treturn !channel->isBroadcast();\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t} else {\n\t\taddDialogsToList([](PeerData *peer) {\n\t\t\tif (peer->isChat() && peer->asChat()->canEdit()) {\n\t\t\t\treturn true;\n\t\t\t} else if (peer->isMegagroup()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t}\n\tinit();\n}\n\nvoid ContactsBox::Inner::init() {\n\tsubscribe(AuthSession::CurrentDownloaderTaskFinished(), [this] { update(); });\n\tconnect(_addContactLnk, SIGNAL(clicked()), App::wnd(), SLOT(onShowAddContact()));\n\tconnect(_allAdmins, SIGNAL(changed()), this, SLOT(onAllAdminsChanged()));\n\n\t_rowsTop = st::contactsMarginTop;\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\n\tfor_const (auto row, _contacts->all()) {\n\t\trow->attached = nullptr;\n\t}\n\n\t_filter = qsl(\"a\");\n\tupdateFilter();\n\n\tconnect(App::main(), SIGNAL(dialogRowReplaced(Dialogs::Row*,Dialogs::Row*)), this, SLOT(onDialogRowReplaced(Dialogs::Row*,Dialogs::Row*)));\n\tconnect(App::main(), SIGNAL(peerUpdated(PeerData*)), this, SLOT(peerUpdated(PeerData *)));\n\tconnect(App::main(), SIGNAL(peerNameChanged(PeerData*,const PeerData::Names&,const PeerData::NameFirstChars&)), this, SLOT(onPeerNameChanged(PeerData*,const PeerData::Names&,const PeerData::NameFirstChars&)));\n\tconnect(App::main(), SIGNAL(peerPhotoChanged(PeerData*)), this, SLOT(peerUpdated(PeerData*)));\n\n\tsubscribe(Window::Theme::Background(), [this](const Window::Theme::BackgroundUpdate &update) {\n\t\tif (update.paletteChanged()) {\n\t\t\tinvalidateCache();\n\t\t}\n\t});\n}\n\nvoid ContactsBox::Inner::invalidateCache() {\n\tfor_const (auto data, _contactsData) {\n\t\tif (data->checkbox) {\n\t\t\tdata->checkbox->invalidateCache();\n\t\t}\n\t}\n\tfor_const (auto data, _byUsernameDatas) {\n\t\tif (data->checkbox) {\n\t\t\tdata->checkbox->invalidateCache();\n\t\t}\n\t}\n\tfor_const (auto data, d_byUsername) {\n\t\tif (data->checkbox) {\n\t\t\tdata->checkbox->invalidateCache();\n\t\t}\n\t}\n}\n\nvoid ContactsBox::Inner::initList() {\n\tif (!_chat || _membersFilter != MembersFilter::Admins) return;\n\n\tQList<UserData*> admins, others;\n\tadmins.reserve(_chat->admins.size() + 1);\n\tif (!_chat->participants.isEmpty()) {\n\t\tothers.reserve(_chat->participants.size());\n\t}\n\n\tfor (auto i = _chat->participants.cbegin(), e = _chat->participants.cend(); i != e; ++i) {\n\t\tif (i.key()->id == peerFromUser(_chat->creator)) continue;\n\t\tif (!_allAdmins->checked() && _chat->admins.contains(i.key())) {\n\t\t\tadmins.push_back(i.key());\n\t\t\tif (!_checkedContacts.contains(i.key())) {\n\t\t\t\t_checkedContacts.insert(i.key());\n\t\t\t}\n\t\t} else {\n\t\t\tothers.push_back(i.key());\n\t\t}\n\t}\n\tstd::sort(admins.begin(), admins.end(), _sortByName);\n\tstd::sort(others.begin(), others.end(), _sortByName);\n\tif (auto creator = App::userLoaded(_chat->creator)) {\n\t\tif (_chat->participants.contains(creator)) {\n\t\t\tadmins.push_front(creator);\n\t\t}\n\t}\n\tfor_const (auto user, admins) {\n\t\t_contacts->addToEnd(App::history(user->id));\n\t}\n\tfor_const (auto user, others) {\n\t\t_contacts->addToEnd(App::history(user->id));\n\t}\n}\n\nvoid ContactsBox::Inner::onPeerNameChanged(PeerData *peer, const PeerData::Names &oldNames, const PeerData::NameFirstChars &oldChars) {\n\tif (bot()) {\n\t\t_contacts->peerNameChanged(peer, oldNames, oldChars);\n\t}\n\tpeerUpdated(peer);\n}\n\nvoid ContactsBox::Inner::addBot() {\n\tif (auto &info = _bot->botInfo) {\n\t\tif (!info->shareGameShortName.isEmpty()) {\n\t\t\tauto history = App::historyLoaded(_addToPeer);\n\t\t\tauto afterRequestId = history ? history->sendRequestId : 0;\n\t\t\tauto randomId = rand_value<uint64>();\n\t\t\tauto requestId = MTP::send(MTPmessages_SendMedia(MTP_flags(0), _addToPeer->input, MTP_int(0), MTP_inputMediaGame(MTP_inputGameShortName(_bot->inputUser, MTP_string(info->shareGameShortName))), MTP_long(randomId), MTPnullMarkup), App::main()->rpcDone(&MainWidget::sentUpdatesReceived), App::main()->rpcFail(&MainWidget::sendMessageFail), 0, 0, afterRequestId);\n\t\t\tif (history) {\n\t\t\t\thistory->sendRequestId = requestId;\n\t\t\t}\n\t\t} else if (!info->startGroupToken.isEmpty()) {\n\t\t\tMTP::send(MTPmessages_StartBot(_bot->inputUser, _addToPeer->input, MTP_long(rand_value<uint64>()), MTP_string(info->startGroupToken)), App::main()->rpcDone(&MainWidget::sentUpdatesReceived), App::main()->rpcFail(&MainWidget::addParticipantFail, { _bot, _addToPeer }));\n\t\t} else {\n\t\t\tApp::main()->addParticipants(_addToPeer, std::vector<gsl::not_null<UserData*>>(1, _bot));\n\t\t}\n\t} else {\n\t\tApp::main()->addParticipants(_addToPeer, std::vector<gsl::not_null<UserData*>>(1, _bot));\n\t}\n\tUi::hideLayer();\n\tUi::showPeerHistory(_addToPeer, ShowAtUnreadMsgId);\n}\n\nvoid ContactsBox::Inner::onAllAdminsChanged() {\n\tif (_saving && _allAdmins->checked() != _allAdminsChecked) {\n\t\t_allAdmins->setChecked(_allAdminsChecked);\n\t} else if (_allAdminsChangedCallback) {\n\t\t_allAdminsChangedCallback();\n\t}\n\tupdate();\n}\n\nvoid ContactsBox::Inner::addAdminDone(MTPChannelAdminRights rights, const MTPUpdates &result, mtpRequestId req) {\n\tif (App::main()) App::main()->sentUpdatesReceived(result);\n\tif (req != _addAdminRequestId) return;\n\n\t_addAdminRequestId = 0;\n\tif (_addAdmin && _channel) {\n\t\t_channel->applyEditAdmin(_addAdmin, rights);\n\t}\n\tif (_addAdminBox) _addAdminBox->closeBox();\n\temit adminAdded();\n}\n\nbool ContactsBox::Inner::addAdminFail(const RPCError &error, mtpRequestId req) {\n\tif (MTP::isDefaultHandledError(error)) return false;\n\n\tif (req != _addAdminRequestId) return true;\n\n\t_addAdminRequestId = 0;\n\tif (_addAdminBox) _addAdminBox->closeBox();\n\tif (error.type() == \"USERS_TOO_MUCH\") {\n\t\tUi::show(Box<MaxInviteBox>(_channel->inviteLink()), KeepOtherLayers);\n\t} else if (error.type() == \"ADMINS_TOO_MUCH\") {\n\t\tUi::show(Box<InformBox>(lang(lng_channel_admins_too_much)), KeepOtherLayers);\n\t} else if (error.type() == qstr(\"USER_RESTRICTED\")) {\n\t\tUi::show(Box<InformBox>(lang(lng_cant_do_this)), KeepOtherLayers);\n\t} else  {\n\t\temit adminAdded();\n\t}\n\treturn true;\n}\n\nvoid ContactsBox::Inner::saving(bool flag) {\n\t_saving = flag;\n\t_allAdminsChecked = _allAdmins->checked();\n\tupdate();\n}\n\nvoid ContactsBox::Inner::peerUpdated(PeerData *peer) {\n\tif (_chat && (!peer || peer == _chat)) {\n\t\tbool inited = false;\n\t\tif (_membersFilter == MembersFilter::Admins && _contacts->isEmpty() && !_chat->participants.isEmpty()) {\n\t\t\tinitList();\n\t\t\tinited = true;\n\t\t}\n\t\tif (!_chat->canEdit()) {\n\t\t\tUi::hideLayer();\n\t\t} else if (!_chat->participants.isEmpty()) {\n\t\t\tfor (ContactsData::iterator i = _contactsData.begin(), e = _contactsData.end(); i != e; ++i) {\n\t\t\t\tdelete i.value();\n\t\t\t}\n\t\t\t_contactsData.clear();\n\t\t\tfor_const (auto row, _contacts->all()) {\n\t\t\t\trow->attached = nullptr;\n\t\t\t}\n\t\t\tif (!_filter.isEmpty()) {\n\t\t\t\tfor (int32 j = 0, s = _filtered.size(); j < s; ++j) {\n\t\t\t\t\t_filtered[j]->attached = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (inited) {\n\t\t\t_filter += 'a';\n\t\t\tupdateFilter(_lastQuery);\n\t\t}\n\t\tupdate();\n\t} else {\n\t\tContactsData::iterator i = _contactsData.find(peer);\n\t\tif (i != _contactsData.cend()) {\n\t\t\tfor_const (auto row, _contacts->all()) {\n\t\t\t\tif (row->attached == i.value()) {\n\t\t\t\t\trow->attached = nullptr;\n\t\t\t\t\tupdate(0, _rowsTop + _aboutHeight + _rowHeight * row->pos(), width(), _rowHeight);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!_filter.isEmpty()) {\n\t\t\t\tfor (int32 j = 0, s = _filtered.size(); j < s; ++j) {\n\t\t\t\t\tif (_filtered[j]->attached == i.value()) {\n\t\t\t\t\t\t_filtered[j]->attached = 0;\n\t\t\t\t\t\tupdate(0, _rowsTop + _rowHeight * j, width(), _rowHeight);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tdelete i.value();\n\t\t\t_contactsData.erase(i);\n\t\t}\n\t}\n}\n\nvoid ContactsBox::Inner::loadProfilePhotos() {\n\tif (_visibleTop >= _visibleBottom) return;\n\n\tauto yFrom = _visibleTop - _rowsTop;\n\tauto yTo = yFrom + (_visibleBottom - _visibleTop) * 5;\n\tAuthSession::Current().downloader().clearPriorities();\n\n\tif (yTo < 0) return;\n\tif (yFrom < 0) yFrom = 0;\n\n\tif (_filter.isEmpty()) {\n\t\tif (!_contacts->isEmpty()) {\n\t\t\tauto i = _contacts->cfind(yFrom - _aboutHeight, _rowHeight);\n\t\t\tfor (auto end = _contacts->cend(); i != end; ++i) {\n\t\t\t\tif ((_aboutHeight + (*i)->pos() * _rowHeight) >= yTo) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t(*i)->history()->peer->loadUserpic();\n\t\t\t}\n\t\t}\n\t} else if (!_filtered.isEmpty()) {\n\t\tauto from = yFrom / _rowHeight;\n\t\tif (from < 0) from = 0;\n\t\tif (from < _filtered.size()) {\n\t\t\tauto to = (yTo / _rowHeight) + 1;\n\t\t\tif (to > _filtered.size()) to = _filtered.size();\n\n\t\t\tfor (; from < to; ++from) {\n\t\t\t\t_filtered[from]->history()->peer->loadUserpic();\n\t\t\t}\n\t\t}\n\t}\n}\n\nContactsBox::Inner::ContactData *ContactsBox::Inner::contactData(Dialogs::Row *row) {\n\tContactData *data = (ContactData*)row->attached;\n\tif (!data) {\n\t\tPeerData *peer = row->history()->peer;\n\t\tContactsData::const_iterator i = _contactsData.constFind(peer);\n\t\tif (i == _contactsData.cend()) {\n\t\t\tdata = usingMultiSelect() ? new ContactData(peer, [this, peer] { updateRowWithPeer(peer); }) : new ContactData();\n\t\t\t_contactsData.insert(peer, data);\n\t\t\tif (peer->isUser()) {\n\t\t\t\tif (_chat) {\n\t\t\t\t\tif (_membersFilter == MembersFilter::Recent) {\n\t\t\t\t\t\tdata->disabledChecked = _chat->participants.contains(peer->asUser());\n\t\t\t\t\t}\n\t\t\t\t} else if (_creating == CreatingGroupGroup) {\n\t\t\t\t\tdata->disabledChecked = (peer->id == AuthSession::CurrentUserPeerId());\n\t\t\t\t} else if (_channel) {\n\t\t\t\t\tdata->disabledChecked = (peer->id == AuthSession::CurrentUserPeerId()) || _already.contains(peer->asUser());\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (usingMultiSelect() && _checkedContacts.contains(peer)) {\n\t\t\t\tdata->checkbox->setChecked(true, Ui::RoundImageCheckbox::SetStyle::Fast);\n\t\t\t}\n\t\t\tdata->name.setText(st::contactsNameStyle, peer->name, _textNameOptions);\n\t\t\tif (peer->isUser()) {\n\t\t\t\tdata->statusText = App::onlineText(peer->asUser(), _time);\n\t\t\t\tdata->statusHasOnlineColor = App::onlineColorUse(peer->asUser(), _time);\n\t\t\t} else if (peer->isChat()) {\n\t\t\t\tauto chat = peer->asChat();\n\t\t\t\tif (!chat->amIn()) {\n\t\t\t\t\tdata->statusText = lang(lng_chat_status_unaccessible);\n\t\t\t\t} else if (chat->count > 0) {\n\t\t\t\t\tdata->statusText = lng_chat_status_members(lt_count, chat->count);\n\t\t\t\t} else {\n\t\t\t\t\tdata->statusText = lang(lng_group_status);\n\t\t\t\t}\n\t\t\t} else if (peer->isMegagroup()) {\n\t\t\t\tdata->statusText = lang(lng_group_status);\n\t\t\t} else if (peer->isChannel()) {\n\t\t\t\tdata->statusText = lang(lng_channel_status);\n\t\t\t}\n\t\t} else {\n\t\t\tdata = i.value();\n\t\t}\n\t\trow->attached = data;\n\t}\n\treturn data;\n}\n\nbool ContactsBox::Inner::isRowDisabled(PeerData *peer, ContactData *data) const {\n\tif (_chat && _membersFilter == MembersFilter::Admins) {\n\t\treturn (_saving || _allAdmins->checked() || peer->id == peerFromUser(_chat->creator));\n\t}\n\treturn (data->disabledChecked || selectedCount() >= Global::MegagroupSizeMax());\n}\n\nvoid ContactsBox::Inner::paintDialog(Painter &p, TimeMs ms, PeerData *peer, ContactData *data, bool selected) {\n\tauto user = peer->asUser();\n\n\tif (isRowDisabled(peer, data)) {\n\t\tselected = false;\n\t}\n\n\tauto paintDisabledCheck = data->disabledChecked;\n\tif (_chat && _membersFilter == MembersFilter::Admins) {\n\t\tif (peer->id == peerFromUser(_chat->creator) || _allAdmins->checked()) {\n\t\t\tpaintDisabledCheck = true;\n\t\t}\n\t}\n\n\tauto checkedRatio = 0.;\n\tp.fillRect(0, 0, width(), _rowHeight, selected ? st::contactsBgOver : st::contactsBg);\n\tif (data->ripple) {\n\t\tdata->ripple->paint(p, 0, 0, width(), ms);\n\t\tif (data->ripple->empty()) {\n\t\t\tdata->ripple.reset();\n\t\t}\n\t}\n\tif (paintDisabledCheck) {\n\t\tpaintDisabledCheckUserpic(p, peer, st::contactsPadding.left(), st::contactsPadding.top(), width());\n\t} else if (usingMultiSelect()) {\n\t\tcheckedRatio = data->checkbox->checkedAnimationRatio();\n\t\tdata->checkbox->paint(p, ms, st::contactsPadding.left(), st::contactsPadding.top(), width());\n\t} else {\n\t\tpeer->paintUserpicLeft(p, st::contactsPadding.left(), st::contactsPadding.top(), width(), st::contactsPhotoSize);\n\t}\n\n\tint namex = st::contactsPadding.left() + st::contactsPhotoSize + st::contactsPadding.left();\n\tint namew = width() - namex - st::contactsPadding.right();\n\tif (peer->isVerified()) {\n\t\tauto icon = &st::dialogsVerifiedIcon;\n\t\tnamew -= icon->width();\n\t\ticon->paint(p, namex + qMin(data->name.maxWidth(), namew), st::contactsPadding.top() + st::contactsNameTop, width());\n\t}\n\tp.setPen(anim::pen(st::contactsNameFg, st::contactsNameCheckedFg, checkedRatio));\n\tdata->name.drawLeftElided(p, namex, st::contactsPadding.top() + st::contactsNameTop, namew, width());\n\n\tbool uname = (user || peer->isChannel()) && (data->statusText.at(0) == '@');\n\tp.setFont(st::contactsStatusFont);\n\tif (uname && !_lastQuery.isEmpty() && peer->userName().startsWith(_lastQuery, Qt::CaseInsensitive)) {\n\t\tint availw = width() - namex - st::contactsPadding.right();\n\t\tQString first = '@' + peer->userName().mid(0, _lastQuery.size()), second = peer->userName().mid(_lastQuery.size());\n\t\tint w = st::contactsStatusFont->width(first);\n\t\tif (w >= availw || second.isEmpty()) {\n\t\t\tp.setPen(st::contactsStatusFgOnline);\n\t\t\tp.drawTextLeft(namex, st::contactsPadding.top() + st::contactsStatusTop, width(), st::contactsStatusFont->elided(first, availw));\n\t\t} else {\n\t\t\tsecond = st::contactsStatusFont->elided(second, availw - w);\n\t\t\tint32 secondw = st::contactsStatusFont->width(second);\n\t\t\tp.setPen(st::contactsStatusFgOnline);\n\t\t\tp.drawTextLeft(namex, st::contactsPadding.top() + st::contactsStatusTop, width() - secondw, first);\n\t\t\tp.setPen(selected ? st::contactsStatusFgOver : st::contactsStatusFg);\n\t\t\tp.drawTextLeft(namex + w, st::contactsPadding.top() + st::contactsStatusTop, width() + w, second);\n\t\t}\n\t} else {\n\t\tif ((user && (uname || data->statusHasOnlineColor)) || (peer->isChannel() && uname)) {\n\t\t\tp.setPen(st::contactsStatusFgOnline);\n\t\t} else {\n\t\t\tp.setPen(selected ? st::contactsStatusFgOver : st::contactsStatusFg);\n\t\t}\n\t\tp.drawTextLeft(namex, st::contactsPadding.top() + st::contactsStatusTop, width(), data->statusText);\n\t}\n}\n\n// Emulates Ui::RoundImageCheckbox::paint() in a checked state.\nvoid ContactsBox::Inner::paintDisabledCheckUserpic(Painter &p, PeerData *peer, int x, int y, int outerWidth) const {\n\tauto userpicRadius = st::contactsPhotoCheckbox.imageSmallRadius;\n\tauto userpicShift = st::contactsPhotoCheckbox.imageRadius - userpicRadius;\n\tauto userpicDiameter = st::contactsPhotoCheckbox.imageRadius * 2;\n\tauto userpicLeft = x + userpicShift;\n\tauto userpicTop = y + userpicShift;\n\tauto userpicEllipse = rtlrect(x, y, userpicDiameter, userpicDiameter, outerWidth);\n\tauto userpicBorderPen = st::contactsPhotoDisabledCheckFg->p;\n\tuserpicBorderPen.setWidth(st::contactsPhotoCheckbox.selectWidth);\n\n\tauto iconDiameter = st::contactsPhotoCheckbox.check.size;\n\tauto iconLeft = x + userpicDiameter + st::contactsPhotoCheckbox.selectWidth - iconDiameter;\n\tauto iconTop = y + userpicDiameter + st::contactsPhotoCheckbox.selectWidth - iconDiameter;\n\tauto iconEllipse = rtlrect(iconLeft, iconTop, iconDiameter, iconDiameter, outerWidth);\n\tauto iconBorderPen = st::contactsPhotoCheckbox.check.border->p;\n\ticonBorderPen.setWidth(st::contactsPhotoCheckbox.selectWidth);\n\n\tpeer->paintUserpicLeft(p, userpicLeft, userpicTop, outerWidth, userpicRadius * 2);\n\n\t{\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tp.setPen(userpicBorderPen);\n\t\tp.setBrush(Qt::NoBrush);\n\t\tp.drawRect(userpicEllipse);\n\n\t\tp.setPen(iconBorderPen);\n\t\tp.setBrush(st::contactsPhotoDisabledCheckFg);\n\t\tp.drawRect(iconEllipse);\n\t}\n\n\tst::contactsPhotoCheckbox.check.check.paint(p, iconEllipse.topLeft(), outerWidth);\n}\n\nvoid ContactsBox::Inner::paintEvent(QPaintEvent *e) {\n\tQRect r(e->rect());\n\tPainter p(this);\n\n\tp.setClipRect(r);\n\t_time = unixtime();\n\tp.fillRect(r, st::contactsBg);\n\n\tauto ms = getms();\n\tauto yFrom = r.y(), yTo = r.y() + r.height();\n\tauto skip = _rowsTop;\n\tif (_filter.isEmpty()) {\n\t\tskip += _aboutHeight;\n\t\tif (!_contacts->isEmpty() || !_byUsername.isEmpty()) {\n\t\t\tif (_aboutHeight) {\n\t\t\t\tauto infoTop = _allAdmins->bottomNoMargins() + st::contactsAllAdminsTop - st::lineWidth;\n\n\t\t\t\tauto infoRect = rtlrect(0, infoTop, width(), _aboutHeight - infoTop - st::contactsPadding.bottom(), width());\n\t\t\t\tp.fillRect(infoRect, st::contactsAboutBg);\n\t\t\t\tauto dividerFillTop = rtlrect(0, infoRect.y(), width(), st::profileDividerTop.height(), width());\n\t\t\t\tst::profileDividerTop.fill(p, dividerFillTop);\n\t\t\t\tauto dividerFillBottom = rtlrect(0, infoRect.y() + infoRect.height() - st::profileDividerBottom.height(), width(), st::profileDividerBottom.height(), width());\n\t\t\t\tst::profileDividerBottom.fill(p, dividerFillBottom);\n\n\t\t\t\tint aboutw = width() - st::contactsPadding.left() - st::contactsPadding.right();\n\t\t\t\tp.setPen(st::contactsAboutFg);\n\t\t\t\t(_allAdmins->checked() ? _aboutAllAdmins : _aboutAdmins).draw(p, st::contactsPadding.left(), st::contactsAboutTop, aboutw);\n\t\t\t}\n\t\t\tyFrom -= skip;\n\t\t\tyTo -= skip;\n\t\t\tp.translate(0, skip);\n\t\t\tif (!_contacts->isEmpty()) {\n\t\t\t\tauto i = _contacts->cfind(yFrom, _rowHeight);\n\t\t\t\tp.translate(0, (*i)->pos() * _rowHeight);\n\t\t\t\tfor (auto end = _contacts->cend(); i != end; ++i) {\n\t\t\t\t\tif ((*i)->pos() * _rowHeight >= yTo) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tauto selected = _pressed ? (*i == _pressed) : (*i == _selected);\n\t\t\t\t\tpaintDialog(p, ms, (*i)->history()->peer, contactData(*i), selected);\n\t\t\t\t\tp.translate(0, _rowHeight);\n\t\t\t\t}\n\t\t\t\tyFrom -= _contacts->size() * _rowHeight;\n\t\t\t\tyTo -= _contacts->size() * _rowHeight;\n\t\t\t}\n\t\t\tif (!_byUsername.isEmpty()) {\n\t\t\t\tp.fillRect(0, 0, width(), st::searchedBarHeight, st::searchedBarBg);\n\t\t\t\tp.setFont(st::searchedBarFont);\n\t\t\t\tp.setPen(st::searchedBarFg);\n\t\t\t\tp.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), lang(lng_search_global_results), style::al_center);\n\n\t\t\t\tyFrom -= st::searchedBarHeight;\n\t\t\t\tyTo -= st::searchedBarHeight;\n\t\t\t\tp.translate(0, st::searchedBarHeight);\n\n\t\t\t\tauto from = floorclamp(yFrom, _rowHeight, 0, _byUsername.size());\n\t\t\t\tauto to = ceilclamp(yTo, _rowHeight, 0, _byUsername.size());\n\t\t\t\tp.translate(0, from * _rowHeight);\n\t\t\t\tfor (; from < to; ++from) {\n\t\t\t\t\tauto selected = (_searchedPressed >= 0) ? (_searchedPressed == from) : (_searchedSelected == from);\n\t\t\t\t\tpaintDialog(p, ms, _byUsername[from], d_byUsername[from], selected);\n\t\t\t\t\tp.translate(0, _rowHeight);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tQString text;\n\t\t\tskip = 0;\n\t\t\tif (bot()) {\n\t\t\t\ttext = lang((AuthSession::Current().data().allChatsLoaded().value() && !_searching) ? (sharingBotGame() ? lng_bot_no_chats : lng_bot_no_groups) : lng_contacts_loading);\n\t\t\t} else if (_chat && _membersFilter == MembersFilter::Admins) {\n\t\t\t\ttext = lang(lng_contacts_loading);\n\t\t\t\tp.fillRect(0, 0, width(), _aboutHeight - st::contactsPadding.bottom() - st::lineWidth, st::contactsAboutBg);\n\t\t\t\tp.fillRect(0, _aboutHeight - st::contactsPadding.bottom() - st::lineWidth, width(), st::lineWidth, st::shadowFg);\n\n\t\t\t\tint aboutw = width() - st::contactsPadding.left() - st::contactsPadding.right();\n\t\t\t\t(_allAdmins->checked() ? _aboutAllAdmins : _aboutAdmins).draw(p, st::contactsPadding.left(), st::contactsAboutTop, aboutw);\n\t\t\t\tp.translate(0, _aboutHeight);\n\t\t\t} else if (AuthSession::Current().data().contactsLoaded().value() && !_searching) {\n\t\t\t\ttext = lang(lng_no_contacts);\n\t\t\t\tskip = st::noContactsFont->height;\n\t\t\t} else {\n\t\t\t\ttext = lang(lng_contacts_loading);\n\t\t\t}\n\t\t\tp.setFont(st::noContactsFont->f);\n\t\t\tp.setPen(st::noContactsColor->p);\n\t\t\tp.drawText(QRect(0, 0, width(), st::noContactsHeight - skip), text, style::al_center);\n\t\t}\n\t} else {\n\t\tif (_filtered.isEmpty() && _byUsernameFiltered.isEmpty()) {\n\t\t\tp.setFont(st::noContactsFont);\n\t\t\tp.setPen(st::noContactsColor);\n\t\t\tQString text;\n\t\t\tif (bot()) {\n\t\t\t\ttext = lang((AuthSession::Current().data().allChatsLoaded().value() && !_searching) ? (sharingBotGame() ? lng_bot_chats_not_found : lng_bot_groups_not_found) : lng_contacts_loading);\n\t\t\t} else if (_chat && _membersFilter == MembersFilter::Admins) {\n\t\t\t\ttext = lang(_chat->participants.isEmpty() ? lng_contacts_loading : lng_contacts_not_found);\n\t\t\t} else {\n\t\t\t\ttext = lang((AuthSession::Current().data().contactsLoaded().value() && !_searching) ? lng_contacts_not_found : lng_contacts_loading);\n\t\t\t}\n\t\t\tp.drawText(QRect(0, 0, width(), st::noContactsHeight), text, style::al_center);\n\t\t} else {\n\t\t\tyFrom -= skip;\n\t\t\tyTo -= skip;\n\t\t\tp.translate(0, skip);\n\t\t\tif (!_filtered.isEmpty()) {\n\t\t\t\tint32 from = floorclamp(yFrom, _rowHeight, 0, _filtered.size());\n\t\t\t\tint32 to = ceilclamp(yTo, _rowHeight, 0, _filtered.size());\n\t\t\t\tp.translate(0, from * _rowHeight);\n\t\t\t\tfor (; from < to; ++from) {\n\t\t\t\t\tauto selected = (_filteredPressed >= 0) ? (_filteredPressed == from) : (_filteredSelected == from);\n\t\t\t\t\tpaintDialog(p, ms, _filtered[from]->history()->peer, contactData(_filtered[from]), selected);\n\t\t\t\t\tp.translate(0, _rowHeight);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!_byUsernameFiltered.isEmpty()) {\n\t\t\t\tp.fillRect(0, 0, width(), st::searchedBarHeight, st::searchedBarBg);\n\t\t\t\tp.setFont(st::searchedBarFont);\n\t\t\t\tp.setPen(st::searchedBarFg);\n\t\t\t\tp.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), lang(lng_search_global_results), style::al_center);\n\t\t\t\tp.translate(0, st::searchedBarHeight);\n\n\t\t\t\tyFrom -= _filtered.size() * _rowHeight + st::searchedBarHeight;\n\t\t\t\tyTo -= _filtered.size() * _rowHeight + st::searchedBarHeight;\n\t\t\t\tint32 from = floorclamp(yFrom, _rowHeight, 0, _byUsernameFiltered.size());\n\t\t\t\tint32 to = ceilclamp(yTo, _rowHeight, 0, _byUsernameFiltered.size());\n\t\t\t\tp.translate(0, from * _rowHeight);\n\t\t\t\tfor (; from < to; ++from) {\n\t\t\t\t\tauto selected = (_searchedPressed >= 0) ? (_searchedPressed == from) : (_searchedSelected == from);\n\t\t\t\t\tpaintDialog(p, ms, _byUsernameFiltered[from], d_byUsernameFiltered[from], selected);\n\t\t\t\t\tp.translate(0, _rowHeight);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ContactsBox::Inner::enterEventHook(QEvent *e) {\n\tsetMouseTracking(true);\n}\n\nint ContactsBox::Inner::getSelectedRowTop() const {\n\tif (_filter.isEmpty()) {\n\t\tif (_selected) {\n\t\t\treturn _rowsTop + _aboutHeight + (_selected->pos() * _rowHeight);\n\t\t} else if (_searchedSelected >= 0) {\n\t\t\treturn _rowsTop + _aboutHeight + (_contacts->size() * _rowHeight) + st::searchedBarHeight + (_searchedSelected * _rowHeight);\n\t\t}\n\t} else {\n\t\tif (_filteredSelected >= 0) {\n\t\t\treturn _rowsTop + (_filteredSelected * _rowHeight);\n\t\t} else if (_searchedSelected >= 0) {\n\t\t\treturn _rowsTop + (_filtered.size() * _rowHeight + st::searchedBarHeight + _searchedSelected * _rowHeight);\n\t\t}\n\t}\n\treturn -1;\n}\n\nvoid ContactsBox::Inner::updateSelectedRow() {\n\tauto rowTop = getSelectedRowTop();\n\tif (rowTop >= 0) {\n\t\tupdateRowWithTop(rowTop);\n\t}\n}\n\nvoid ContactsBox::Inner::updateRowWithTop(int rowTop) {\n\tupdate(0, rowTop, width(), _rowHeight);\n}\n\nint ContactsBox::Inner::getRowTopWithPeer(PeerData *peer) const {\n\tif (_filter.isEmpty()) {\n\t\tfor (auto i = _contacts->cbegin(), end = _contacts->cend(); i != end; ++i) {\n\t\t\tif ((*i)->history()->peer == peer) {\n\t\t\t\treturn _rowsTop + _aboutHeight + ((*i)->pos() * _rowHeight);\n\t\t\t}\n\t\t}\n\t\tfor (auto i = 0, count = _byUsername.size(); i != count; ++i) {\n\t\t\tif (_byUsername[i] == peer) {\n\t\t\t\treturn _rowsTop + _aboutHeight + (_contacts->size() * _rowHeight) + st::searchedBarHeight + (i * _rowHeight);\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor (auto i = 0, count = _filtered.size(); i != count; ++i) {\n\t\t\tif (_filtered[i]->history()->peer == peer) {\n\t\t\t\treturn _rowsTop + (i * _rowHeight);\n\t\t\t}\n\t\t}\n\t\tfor (auto i = 0, count = _byUsernameFiltered.size(); i != count; ++i) {\n\t\t\tif (_byUsernameFiltered[i] == peer) {\n\t\t\t\treturn _rowsTop + (_contacts->size() * _rowHeight) + st::searchedBarHeight + (i * _rowHeight);\n\t\t\t}\n\t\t}\n\t}\n\treturn -1;\n}\n\nvoid ContactsBox::Inner::updateRowWithPeer(PeerData *peer) {\n\tauto rowTop = getRowTopWithPeer(peer);\n\tif (rowTop >= 0) {\n\t\tupdateRowWithTop(rowTop);\n\t}\n}\n\nvoid ContactsBox::Inner::leaveEventHook(QEvent *e) {\n\t_mouseSelection = false;\n\tsetMouseTracking(false);\n\tif (_selected || _filteredSelected >= 0 || _searchedSelected >= 0) {\n\t\tupdateSelectedRow();\n\t\t_selected = nullptr;\n\t\t_filteredSelected = _searchedSelected = -1;\n\t}\n}\n\nvoid ContactsBox::Inner::mouseMoveEvent(QMouseEvent *e) {\n\t_mouseSelection = true;\n\t_lastMousePos = e->globalPos();\n\tupdateSelection();\n}\n\nvoid ContactsBox::Inner::mousePressEvent(QMouseEvent *e) {\n\t_mouseSelection = true;\n\t_lastMousePos = e->globalPos();\n\tupdateSelection();\n\n\tsetPressed(_selected);\n\tsetFilteredPressed(_filteredSelected);\n\tsetSearchedPressed(_searchedSelected);\n\tif (_selected) {\n\t\taddRipple(_selected->history()->peer, contactData(_selected));\n\t} else if (_filteredSelected >= 0 && _filteredSelected < _filtered.size()) {\n\t\taddRipple(_filtered[_filteredSelected]->history()->peer, contactData(_filtered[_filteredSelected]));\n\t} else if (_searchedSelected >= 0) {\n\t\tif (_filter.isEmpty() && _searchedSelected < d_byUsername.size()) {\n\t\t\taddRipple(_byUsername[_searchedSelected], d_byUsername[_searchedSelected]);\n\t\t} else if (!_filter.isEmpty() && _searchedSelected < d_byUsernameFiltered.size()) {\n\t\t\taddRipple(_byUsernameFiltered[_searchedSelected], d_byUsernameFiltered[_searchedSelected]);\n\t\t}\n\t}\n}\n\nvoid ContactsBox::Inner::mouseReleaseEvent(QMouseEvent *e) {\n\tauto pressed = _pressed;\n\tsetPressed(nullptr);\n\tauto filteredPressed = _filteredPressed;\n\tsetFilteredPressed(-1);\n\tauto searchedPressed = _searchedPressed;\n\tsetSearchedPressed(-1);\n\tupdateSelectedRow();\n\tif (e->button() == Qt::LeftButton) {\n\t\tif (pressed && pressed == _selected) {\n\t\t\tchooseParticipant();\n\t\t} else if (filteredPressed >= 0 && filteredPressed == _filteredSelected) {\n\t\t\tchooseParticipant();\n\t\t} else if (searchedPressed >= 0 && searchedPressed == _searchedSelected) {\n\t\t\tchooseParticipant();\n\t\t}\n\t}\n}\n\nvoid ContactsBox::Inner::addRipple(PeerData *peer, ContactData *data) {\n\tif (isRowDisabled(peer, data)) return;\n\n\tauto rowTop = getSelectedRowTop();\n\tif (!data->ripple) {\n\t\tauto mask = Ui::RippleAnimation::rectMask(QSize(width(), _rowHeight));\n\t\tdata->ripple = std::make_unique<Ui::RippleAnimation>(st::contactsRipple, std::move(mask), [this, data] {\n\t\t\tupdateRowWithTop(data->rippleRowTop);\n\t\t});\n\t}\n\tdata->rippleRowTop = rowTop;\n\tdata->ripple->add(mapFromGlobal(QCursor::pos()) - QPoint(0, rowTop));\n}\n\nvoid ContactsBox::Inner::stopLastRipple(ContactData *data) {\n\tif (data->ripple) {\n\t\tdata->ripple->lastStop();\n\t}\n}\n\nvoid ContactsBox::Inner::setPressed(Dialogs::Row *pressed) {\n\tif (_pressed != pressed) {\n\t\tif (_pressed) {\n\t\t\tstopLastRipple(contactData(_pressed));\n\t\t}\n\t\t_pressed = pressed;\n\t}\n}\n\nvoid ContactsBox::Inner::setFilteredPressed(int pressed) {\n\tif (_filteredPressed >= 0 && _filteredPressed < _filtered.size()) {\n\t\tstopLastRipple(contactData(_filtered[_filteredPressed]));\n\t}\n\t_filteredPressed = pressed;\n}\n\nvoid ContactsBox::Inner::setSearchedPressed(int pressed) {\n\tif (_searchedPressed >= 0) {\n\t\tif (_searchedPressed < d_byUsername.size()) {\n\t\t\tstopLastRipple(d_byUsername[_searchedPressed]);\n\t\t}\n\t\tif (_searchedPressed < d_byUsernameFiltered.size()) {\n\t\t\tstopLastRipple(d_byUsernameFiltered[_searchedPressed]);\n\t\t}\n\t}\n\t_searchedPressed = pressed;\n}\n\nvoid ContactsBox::Inner::changeMultiSelectCheckState() {\n\t_time = unixtime();\n\tif (_filter.isEmpty()) {\n\t\tif (_searchedSelected >= 0 && _searchedSelected < _byUsername.size()) {\n\t\t\tauto data = d_byUsername[_searchedSelected];\n\t\t\tauto peer = _byUsername[_searchedSelected];\n\t\t\tif (data->disabledChecked) return;\n\n\t\t\tchangeCheckState(data, peer);\n\t\t} else if (_selected) {\n\t\t\tauto data = contactData(_selected);\n\t\t\tauto peer = _selected->history()->peer;\n\t\t\tif (data->disabledChecked) return;\n\n\t\t\tchangeCheckState(_selected);\n\t\t}\n\t} else {\n\t\tif (_searchedSelected >= 0 && _searchedSelected < _byUsernameFiltered.size()) {\n\t\t\tauto data = d_byUsernameFiltered[_searchedSelected];\n\t\t\tauto peer = _byUsernameFiltered[_searchedSelected];\n\t\t\tif (data->disabledChecked) return;\n\n\t\t\tint i = 0, l = d_byUsername.size();\n\t\t\tfor (; i < l; ++i) {\n\t\t\t\tif (d_byUsername[i] == data) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (i == l) {\n\t\t\t\td_byUsername.push_back(data);\n\t\t\t\t_byUsername.push_back(peer);\n\t\t\t\tfor (i = 0, l = _byUsernameDatas.size(); i < l;) {\n\t\t\t\t\tif (_byUsernameDatas[i] == data) {\n\t\t\t\t\t\t_byUsernameDatas.removeAt(i);\n\t\t\t\t\t\t--l;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t++i;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tchangeCheckState(data, peer);\n\t\t} else if (_filteredSelected >= 0 && _filteredSelected < _filtered.size()) {\n\t\t\tauto data = contactData(_filtered[_filteredSelected]);\n\t\t\tauto peer = _filtered[_filteredSelected]->history()->peer;\n\t\t\tif (data->disabledChecked) return;\n\n\t\t\tchangeCheckState(data, peer);\n\t\t}\n\t}\n}\n\nPeerData *ContactsBox::Inner::selectedPeer() const {\n\tif (_filter.isEmpty()) {\n\t\tif (_searchedSelected >= 0 && _searchedSelected < _byUsername.size()) {\n\t\t\treturn _byUsername[_searchedSelected];\n\t\t} else if (_selected) {\n\t\t\treturn _selected->history()->peer;\n\t\t}\n\t} else {\n\t\tif (_searchedSelected >= 0 && _searchedSelected < _byUsernameFiltered.size()) {\n\t\t\treturn _byUsernameFiltered[_searchedSelected];\n\t\t} else if (_filteredSelected >= 0 && _filteredSelected < _filtered.size()) {\n\t\t\treturn _filtered[_filteredSelected]->history()->peer;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid ContactsBox::Inner::chooseParticipant() {\n\tif (_saving) {\n\t\treturn;\n\t}\n\n\tif (usingMultiSelect()) {\n\t\tchangeMultiSelectCheckState();\n\t} else {\n\t\tif (_channel && _membersFilter == MembersFilter::Admins) {\n\t\t\taddSelectedAsChannelAdmin();\n\t\t} else if (sharingBotGame()) {\n\t\t\tshareBotGameToSelected();\n\t\t} else if (bot()) {\n\t\t\taddBotToSelectedGroup();\n\t\t} else if (auto peer = selectedPeer()) {\n\t\t\tUi::hideSettingsAndLayer(true);\n\t\t\tApp::main()->chooseThread(peer, ShowAtUnreadMsgId);\n\t\t}\n\t}\n\tupdate();\n}\n\nvoid ContactsBox::Inner::addSelectedAsChannelAdmin() {\n\tauto peer = selectedPeer();\n\tif (!peer) {\n\t\treturn;\n\t}\n\n\t_addAdmin = peer->asUser();\n\tt_assert(_addAdmin != nullptr);\n\n\tif (_addAdminRequestId) {\n\t\tMTP::cancel(_addAdminRequestId);\n\t\t_addAdminRequestId = 0;\n\t}\n\tif (_addAdminBox) _addAdminBox->deleteLater();\n\n\tauto showBox = [this](auto &&currentRights, bool hasAdminRights) {\n\t\t_addAdminBox = Ui::show(Box<EditAdminBox>(_channel, _addAdmin, hasAdminRights, currentRights, base::lambda_guarded(this, [this](const MTPChannelAdminRights &rights) {\n\t\t\tif (_addAdminRequestId) return;\n\t\t\t_addAdminRequestId = MTP::send(MTPchannels_EditAdmin(_channel->inputChannel, _addAdmin->inputUser, rights), rpcDone(&Inner::addAdminDone, rights), rpcFail(&Inner::addAdminFail));\n\t\t})), KeepOtherLayers);\n\t};\n\n\tauto loadedRights = [this]() -> const MegagroupInfo::Admin * {\n\t\tif (_channel->isMegagroup()) {\n\t\t\tauto it = _channel->mgInfo->lastAdmins.constFind(_addAdmin);\n\t\t\tif (it != _channel->mgInfo->lastAdmins.cend()) {\n\t\t\t\treturn &it.value();\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t};\n\n\tif (auto rights = loadedRights()) {\n\t\tif (rights->canEdit) {\n\t\t\tshowBox(rights->rights, true);\n\t\t} else {\n\t\t\tUi::show(Box<InformBox>(lang(lng_error_cant_edit_admin)), KeepOtherLayers);\n\t\t}\n\t} else {\n\t\t// We don't have current rights yet.\n\t\t_addAdminRequestId = MTP::send(MTPchannels_GetParticipant(_channel->inputChannel, _addAdmin->inputUser), ::rpcDone(base::lambda_guarded(this, [this, showBox](const MTPchannels_ChannelParticipant &result) {\n\t\t\tExpects(result.type() == mtpc_channels_channelParticipant);\n\t\t\tauto &participant = result.c_channels_channelParticipant();\n\t\t\tApp::feedUsers(participant.vusers);\n\t\t\t_addAdminRequestId = 0;\n\t\t\tif (participant.vparticipant.type() == mtpc_channelParticipantAdmin) {\n\t\t\t\tif (participant.vparticipant.c_channelParticipantAdmin().is_can_edit()) {\n\t\t\t\t\tshowBox(participant.vparticipant.c_channelParticipantAdmin().vadmin_rights, true);\n\t\t\t\t} else {\n\t\t\t\t\tUi::show(Box<InformBox>(lang(lng_error_cant_edit_admin)), KeepOtherLayers);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tshowBox(EditAdminBox::DefaultRights(_channel), false);\n\t\t\t}\n\t\t})), ::rpcFail(base::lambda_guarded(this, [this](const RPCError &error) {\n\t\t\tif (MTP::isDefaultHandledError(error)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t_addAdminRequestId = 0;\n\t\t\treturn true;\n\t\t})));\n\t}\n}\n\nvoid ContactsBox::Inner::shareBotGameToSelected() {\n\t_addToPeer = selectedPeer();\n\tif (!_addToPeer) {\n\t\treturn;\n\t}\n\n\tauto confirmText = [this] {\n\t\tif (_addToPeer->isUser()) {\n\t\t\treturn lng_bot_sure_share_game(lt_user, App::peerName(_addToPeer));\n\t\t}\n\t\treturn lng_bot_sure_share_game_group(lt_group, _addToPeer->name);\n\t};\n\tUi::show(Box<ConfirmBox>(confirmText(), base::lambda_guarded(this, [this] {\n\t\taddBot();\n\t})), KeepOtherLayers);\n}\n\nvoid ContactsBox::Inner::addBotToSelectedGroup() {\n\t_addToPeer = selectedPeer();\n\tif (!_addToPeer) {\n\t\treturn;\n\t}\n\n\tif (auto megagroup = _addToPeer->asMegagroup()) {\n\t\tif (!megagroup->canAddMembers()) {\n\t\t\tUi::show(Box<InformBox>(lang(lng_error_cant_add_member)), KeepOtherLayers);\n\t\t\treturn;\n\t\t}\n\t}\n\tif (_addToPeer->isChat() || _addToPeer->isMegagroup()) {\n\t\tUi::show(Box<ConfirmBox>(lng_bot_sure_invite(lt_group, _addToPeer->name), base::lambda_guarded(this, [this] {\n\t\t\taddBot();\n\t\t})), KeepOtherLayers);\n\t}\n}\n\nvoid ContactsBox::Inner::changeCheckState(Dialogs::Row *row) {\n\tchangeCheckState(contactData(row), row->history()->peer);\n}\n\nvoid ContactsBox::Inner::changeCheckState(ContactData *data, PeerData *peer) {\n\tExpects(usingMultiSelect());\n\n\tif (isRowDisabled(peer, data)) {\n\t} else if (data->checkbox->checked()) {\n\t\tchangePeerCheckState(data, peer, false);\n\t} else if (selectedCount() < ((_channel && _channel->isMegagroup()) ? Global::MegagroupSizeMax() : Global::ChatSizeMax())) {\n\t\tchangePeerCheckState(data, peer, true);\n\t} else if (_channel && !_channel->isMegagroup()) {\n\t\tUi::show(Box<MaxInviteBox>(_channel->inviteLink()), KeepOtherLayers);\n\t} else if (!_channel && selectedCount() >= Global::ChatSizeMax() && selectedCount() < Global::MegagroupSizeMax()) {\n\t\tUi::show(Box<InformBox>(lng_profile_add_more_after_upgrade(lt_count, Global::MegagroupSizeMax())), KeepOtherLayers);\n\t}\n}\n\nvoid ContactsBox::Inner::peerUnselected(PeerData *peer) {\n\t// If data is nullptr we simply won't do anything.\n\tauto data = _contactsData.value(peer, nullptr);\n\tchangePeerCheckState(data, peer, false, ChangeStateWay::SkipCallback);\n}\n\nvoid ContactsBox::Inner::setPeerSelectedChangedCallback(base::lambda<void(PeerData *peer, bool selected)> callback) {\n\t_peerSelectedChangedCallback = std::move(callback);\n}\n\nvoid ContactsBox::Inner::changePeerCheckState(ContactData *data, PeerData *peer, bool checked, ChangeStateWay useCallback) {\n\tif (data) {\n\t\tdata->checkbox->setChecked(checked);\n\t}\n\tif (checked) {\n\t\t_checkedContacts.insert(peer);\n\t} else {\n\t\t_checkedContacts.remove(peer);\n\t}\n\tif (useCallback != ChangeStateWay::SkipCallback && _peerSelectedChangedCallback) {\n\t\t_peerSelectedChangedCallback(peer, checked);\n\t}\n}\n\nint ContactsBox::Inner::selectedCount() const {\n\tauto result = _checkedContacts.size();\n\tif (_chat) {\n\t\tresult += qMax(_chat->count, 1);\n\t} else if (_channel) {\n\t\tresult += qMax(_channel->membersCount(), _already.size());\n\t} else if (_creating == CreatingGroupGroup) {\n\t\tresult += 1;\n\t}\n\treturn result;\n}\n\nvoid ContactsBox::Inner::setVisibleTopBottom(int visibleTop, int visibleBottom) {\n\t_visibleTop = visibleTop;\n\t_visibleBottom = visibleBottom;\n\tloadProfilePhotos();\n}\n\nvoid ContactsBox::Inner::updateSelection() {\n\tif (!_mouseSelection) return;\n\n\tauto p = mapFromGlobal(_lastMousePos);\n\tauto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(_lastMousePos));\n\tp.setY(p.y() - _rowsTop);\n\tif (_filter.isEmpty()) {\n\t\t_filteredSelected = -1;\n\t\tsetFilteredPressed(-1);\n\t\tif (_aboutHeight) {\n\t\t\tp.setY(p.y() - _aboutHeight);\n\t\t}\n\t\tauto selected = (in && (p.y() >= 0) && (p.y() < _contacts->size() * _rowHeight)) ? _contacts->rowAtY(p.y(), _rowHeight) : nullptr;\n\t\tauto searchedSelected = (in && (p.y() >= _contacts->size() * _rowHeight + st::searchedBarHeight)) ? ((p.y() - _contacts->size() * _rowHeight - st::searchedBarHeight) / _rowHeight) : -1;\n\t\tif (searchedSelected >= _byUsername.size()) searchedSelected = -1;\n\t\tif (_selected != selected || _searchedSelected != searchedSelected) {\n\t\t\tupdateSelectedRow();\n\t\t\t_selected = selected;\n\t\t\t_searchedSelected = searchedSelected;\n\t\t\tupdateSelectedRow();\n\t\t}\n\t} else {\n\t\t_selected = nullptr;\n\t\tsetPressed(nullptr);\n\t\tauto filteredSelected = (in && (p.y() >= 0) && (p.y() < _filtered.size() * _rowHeight)) ? (p.y() / _rowHeight) : -1;\n\t\tauto searchedSelected = (in && (p.y() >= _filtered.size() * _rowHeight + st::searchedBarHeight)) ? ((p.y() - _filtered.size() * _rowHeight - st::searchedBarHeight) / _rowHeight) : -1;\n\t\tif (searchedSelected >= _byUsernameFiltered.size()) searchedSelected = -1;\n\t\tif (_filteredSelected != filteredSelected || _searchedSelected != searchedSelected) {\n\t\t\tupdateSelectedRow();\n\t\t\t_filteredSelected = filteredSelected;\n\t\t\t_searchedSelected = searchedSelected;\n\t\t\tupdateSelectedRow();\n\t\t}\n\t}\n}\n\nvoid ContactsBox::Inner::updateFilter(QString filter) {\n\t_lastQuery = filter.toLower().trimmed();\n\tfilter = textSearchKey(filter);\n\n\t_time = unixtime();\n\tQStringList f;\n\tif (!filter.isEmpty()) {\n\t\tQStringList filterList = filter.split(cWordSplit(), QString::SkipEmptyParts);\n\t\tint l = filterList.size();\n\n\t\tf.reserve(l);\n\t\tfor (int i = 0; i < l; ++i) {\n\t\t\tQString filterName = filterList[i].trimmed();\n\t\t\tif (filterName.isEmpty()) continue;\n\t\t\tf.push_back(filterName);\n\t\t}\n\t\tfilter = f.join(' ');\n\t}\n\tif (_filter != filter) {\n\t\t_filter = filter;\n\n\t\t_byUsernameFiltered.clear();\n\t\td_byUsernameFiltered.clear();\n\t\tclearSearchedContactDatas();\n\n\t\t_selected = nullptr;\n\t\tsetPressed(nullptr);\n\t\t_filteredSelected = -1;\n\t\tsetFilteredPressed(-1);\n\t\t_searchedSelected = -1;\n\t\tsetSearchedPressed(-1);\n\t\tif (_filter.isEmpty()) {\n\t\t\trefresh();\n\t\t} else {\n\t\t\tif (!_addContactLnk->isHidden()) _addContactLnk->hide();\n\t\t\tif (!_allAdmins->isHidden()) _allAdmins->hide();\n\t\t\tQStringList::const_iterator fb = f.cbegin(), fe = f.cend(), fi;\n\n\t\t\t_filtered.clear();\n\t\t\tif (!f.isEmpty()) {\n\t\t\t\tconst Dialogs::List *toFilter = nullptr;\n\t\t\t\tif (!_contacts->isEmpty()) {\n\t\t\t\t\tfor (fi = fb; fi != fe; ++fi) {\n\t\t\t\t\t\tauto found = _contacts->filtered(fi->at(0));\n\t\t\t\t\t\tif (found->isEmpty()) {\n\t\t\t\t\t\t\ttoFilter = nullptr;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!toFilter || toFilter->size() > found->size()) {\n\t\t\t\t\t\t\ttoFilter = found;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (toFilter) {\n\t\t\t\t\t_filtered.reserve(toFilter->size());\n\t\t\t\t\tfor_const (auto row, *toFilter) {\n\t\t\t\t\t\tconst PeerData::Names &names(row->history()->peer->names);\n\t\t\t\t\t\tPeerData::Names::const_iterator nb = names.cbegin(), ne = names.cend(), ni;\n\t\t\t\t\t\tfor (fi = fb; fi != fe; ++fi) {\n\t\t\t\t\t\t\tQString filterName(*fi);\n\t\t\t\t\t\t\tfor (ni = nb; ni != ne; ++ni) {\n\t\t\t\t\t\t\t\tif (ni->startsWith(*fi)) {\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (ni == ne) {\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (fi == fe) {\n\t\t\t\t\t\t\trow->attached = nullptr;\n\t\t\t\t\t\t\t_filtered.push_back(row);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t_byUsernameFiltered.reserve(_byUsername.size());\n\t\t\t\td_byUsernameFiltered.reserve(d_byUsername.size());\n\t\t\t\tfor (int32 i = 0, l = _byUsername.size(); i < l; ++i) {\n\t\t\t\t\tconst PeerData::Names &names(_byUsername[i]->names);\n\t\t\t\t\tPeerData::Names::const_iterator nb = names.cbegin(), ne = names.cend(), ni;\n\t\t\t\t\tfor (fi = fb; fi != fe; ++fi) {\n\t\t\t\t\t\tQString filterName(*fi);\n\t\t\t\t\t\tfor (ni = nb; ni != ne; ++ni) {\n\t\t\t\t\t\t\tif (ni->startsWith(*fi)) {\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (ni == ne) {\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (fi == fe) {\n\t\t\t\t\t\t_byUsernameFiltered.push_back(_byUsername[i]);\n\t\t\t\t\t\td_byUsernameFiltered.push_back(d_byUsername[i]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!_filtered.isEmpty()) {\n\t\t\t\tfor (_filteredSelected = 0; (_filteredSelected < _filtered.size()) && contactData(_filtered[_filteredSelected])->disabledChecked;) {\n\t\t\t\t\t++_filteredSelected;\n\t\t\t\t}\n\t\t\t\tif (_filteredSelected == _filtered.size()) _filteredSelected = -1;\n\t\t\t}\n\t\t\tif (_filteredSelected < 0 && !_byUsernameFiltered.isEmpty()) {\n\t\t\t\tfor (_searchedSelected = 0; (_searchedSelected < _byUsernameFiltered.size()) && d_byUsernameFiltered[_searchedSelected]->disabledChecked;) {\n\t\t\t\t\t++_searchedSelected;\n\t\t\t\t}\n\t\t\t\tif (_searchedSelected == _byUsernameFiltered.size()) _searchedSelected = -1;\n\t\t\t}\n\t\t\t_mouseSelection = false;\n\t\t\trefresh();\n\n\t\t\tif ((!bot() || sharingBotGame()) && (!_chat || _membersFilter != MembersFilter::Admins)) {\n\t\t\t\t_searching = true;\n\t\t\t\temit searchByUsername();\n\t\t\t}\n\t\t}\n\t\tupdate();\n\t\tloadProfilePhotos();\n\t}\n}\n\nvoid ContactsBox::Inner::clearSearchedContactDatas() {\n\tfor (auto data : base::take(_byUsernameDatas)) {\n\t\tdelete data;\n\t}\n}\n\nvoid ContactsBox::Inner::onDialogRowReplaced(Dialogs::Row *oldRow, Dialogs::Row *newRow) {\n\tif (!_filter.isEmpty()) {\n\t\tfor (auto i = _filtered.begin(), e = _filtered.end(); i != e;) {\n\t\t\tif (*i == oldRow) { // this row is shown in filtered and maybe is in contacts!\n\t\t\t\tif (newRow) {\n\t\t\t\t\t*i = newRow;\n\t\t\t\t\t++i;\n\t\t\t\t} else {\n\t\t\t\t\ti = _filtered.erase(i);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t\tif (_filteredSelected >= _filtered.size()) {\n\t\t\t_filteredSelected = -1;\n\t\t}\n\t\tif (_filteredPressed >= _filtered.size()) {\n\t\t\t_filteredPressed = -1;\n\t\t}\n\t} else {\n\t\tif (_selected == oldRow) {\n\t\t\t_selected = newRow;\n\t\t}\n\t\tif (_pressed == oldRow) {\n\t\t\tsetPressed(newRow);\n\t\t}\n\t}\n\trefresh();\n}\n\nvoid ContactsBox::Inner::peopleReceived(const QString &query, const QVector<MTPPeer> &people) {\n\t_lastQuery = query.toLower().trimmed();\n\tif (_lastQuery.at(0) == '@') _lastQuery = _lastQuery.mid(1);\n\tint32 already = _byUsernameFiltered.size();\n\t_byUsernameFiltered.reserve(already + people.size());\n\td_byUsernameFiltered.reserve(already + people.size());\n\tfor (QVector<MTPPeer>::const_iterator i = people.cbegin(), e = people.cend(); i != e; ++i) {\n\t\tauto peerId = peerFromMTP(*i);\n\t\tint j = 0;\n\t\tfor (; j < already; ++j) {\n\t\t\tif (_byUsernameFiltered[j]->id == peerId) break;\n\t\t}\n\t\tif (j == already) {\n\t\t\tauto peer = App::peer(peerId);\n\t\t\tif (!peer) continue;\n\n\t\t\tif (_channel || _chat || _creating != CreatingGroupNone) {\n\t\t\t\tif (peer->isUser()) {\n\t\t\t\t\tif (peer->asUser()->botInfo) {\n\t\t\t\t\t\tif (_chat || _creating == CreatingGroupGroup) { // skip bot's that can't be invited to groups\n\t\t\t\t\t\t\tif (peer->asUser()->botInfo->cantJoinGroups) continue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (_channel) {\n\t\t\t\t\t\t\tif (!_channel->isMegagroup() && _membersFilter != MembersFilter::Admins) continue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tcontinue; // skip\n\t\t\t\t}\n\t\t\t} else if (sharingBotGame()) {\n\t\t\t\tif (!peer->canWrite()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (auto channel = peer->asChannel()) {\n\t\t\t\t\tif (channel->isBroadcast()) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tauto data = usingMultiSelect() ? new ContactData(peer, [this, peer] { updateRowWithPeer(peer); }) : new ContactData();\n\t\t\t_byUsernameDatas.push_back(data);\n\t\t\tdata->disabledChecked = _chat ? _chat->participants.contains(peer->asUser()) : ((_creating == CreatingGroupGroup || _channel) ? (peer == App::self()) : false);\n\t\t\tif (usingMultiSelect() && _checkedContacts.contains(peer)) {\n\t\t\t\tdata->checkbox->setChecked(true, Ui::RoundImageCheckbox::SetStyle::Fast);\n\t\t\t}\n\t\t\tdata->name.setText(st::contactsNameStyle, peer->name, _textNameOptions);\n\t\t\tdata->statusText = '@' + peer->userName();\n\n\t\t\t_byUsernameFiltered.push_back(peer);\n\t\t\td_byUsernameFiltered.push_back(data);\n\t\t}\n\t}\n\t_searching = false;\n\trefresh();\n}\n\nvoid ContactsBox::Inner::refresh() {\n\tif (_filter.isEmpty()) {\n\t\tif (_chat && _membersFilter == MembersFilter::Admins) {\n\t\t\tif (_allAdmins->isHidden()) _allAdmins->show();\n\t\t} else {\n\t\t\tif (!_allAdmins->isHidden()) _allAdmins->hide();\n\t\t}\n\t\tif (!_contacts->isEmpty() || !_byUsername.isEmpty()) {\n\t\t\tif (!_addContactLnk->isHidden()) _addContactLnk->hide();\n\t\t\tresize(width(), _rowsTop + _aboutHeight + (_contacts->size() * _rowHeight) + (_byUsername.isEmpty() ? 0 : (st::searchedBarHeight + _byUsername.size() * _rowHeight)) + st::contactsMarginBottom);\n\t\t} else if (_chat && _membersFilter == MembersFilter::Admins) {\n\t\t\tif (!_addContactLnk->isHidden()) _addContactLnk->hide();\n\t\t\tresize(width(), _rowsTop + _aboutHeight + st::noContactsHeight + st::contactsMarginBottom);\n\t\t} else {\n\t\t\tif (AuthSession::Current().data().contactsLoaded().value() && !bot()) {\n\t\t\t\tif (_addContactLnk->isHidden()) _addContactLnk->show();\n\t\t\t} else {\n\t\t\t\tif (!_addContactLnk->isHidden()) _addContactLnk->hide();\n\t\t\t}\n\t\t\tresize(width(), st::noContactsHeight);\n\t\t}\n\t} else {\n\t\tif (!_allAdmins->isHidden()) _allAdmins->hide();\n\t\tif (_filtered.isEmpty() && _byUsernameFiltered.isEmpty()) {\n\t\t\tif (!_addContactLnk->isHidden()) _addContactLnk->hide();\n\t\t\tresize(width(), st::noContactsHeight);\n\t\t} else {\n\t\t\tresize(width(), _rowsTop + (_filtered.size() * _rowHeight) + (_byUsernameFiltered.isEmpty() ? 0 : (st::searchedBarHeight + _byUsernameFiltered.size() * _rowHeight)) + st::contactsMarginBottom);\n\t\t}\n\t}\n\tloadProfilePhotos();\n\tupdate();\n}\n\nChatData *ContactsBox::Inner::chat() const {\n\treturn _chat;\n}\n\nChannelData *ContactsBox::Inner::channel() const {\n\treturn _channel;\n}\n\nMembersFilter ContactsBox::Inner::membersFilter() const {\n\treturn _membersFilter;\n}\n\nUserData *ContactsBox::Inner::bot() const {\n\treturn _bot;\n}\n\nbool ContactsBox::Inner::sharingBotGame() const {\n\treturn (_bot && _bot->botInfo) ? !_bot->botInfo->shareGameShortName.isEmpty() : false;\n}\n\nCreatingGroupType ContactsBox::Inner::creating() const {\n\treturn _creating;\n}\n\nContactsBox::Inner::~Inner() {\n\tfor (auto contactData : base::take(_contactsData)) {\n\t\tdelete contactData;\n\t}\n\tclearSearchedContactDatas();\n\tfor (auto data : base::take(d_byUsername)) {\n\t\tdelete data;\n\t}\n\tif (_bot) {\n\t\tif (auto &info = _bot->botInfo) {\n\t\t\tinfo->startGroupToken = QString();\n\t\t\tinfo->shareGameShortName = QString();\n\t\t}\n\t}\n}\n\nvoid ContactsBox::Inner::resizeEvent(QResizeEvent *e) {\n\t_addContactLnk->move((width() - _addContactLnk->width()) / 2, (st::noContactsHeight + st::noContactsFont->height) / 2);\n\t_allAdmins->moveToLeft(st::contactsPadding.left(), st::contactsAllAdminsTop);\n}\n\nvoid ContactsBox::Inner::selectSkip(int32 dir) {\n\t_time = unixtime();\n\t_mouseSelection = false;\n\tif (_filter.isEmpty()) {\n\t\tint cur = 0;\n\t\tif (_selected) {\n\t\t\tfor (auto i = _contacts->cbegin(); *i != _selected; ++i) {\n\t\t\t\t++cur;\n\t\t\t}\n\t\t} else if (_searchedSelected >= 0) {\n\t\t\tcur = (_contacts->size() + _searchedSelected);\n\t\t} else {\n\t\t\tcur = -1;\n\t\t}\n\t\tcur += dir;\n\t\tif (cur <= 0) {\n\t\t\t_selected = (!_contacts->isEmpty()) ? *_contacts->cbegin() : nullptr;\n\t\t\t_searchedSelected = (_contacts->isEmpty() && !_byUsername.isEmpty()) ? 0 : -1;\n\t\t} else if (cur >= _contacts->size()) {\n\t\t\tif (_byUsername.isEmpty()) {\n\t\t\t\t_selected = _contacts->isEmpty() ? nullptr : *(_contacts->cend() - 1);\n\t\t\t\t_searchedSelected = -1;\n\t\t\t} else {\n\t\t\t\t_selected = nullptr;\n\t\t\t\t_searchedSelected = cur - _contacts->size();\n\t\t\t\tif (_searchedSelected >= _byUsername.size()) _searchedSelected = _byUsername.size() - 1;\n\t\t\t}\n\t\t} else {\n\t\t\tfor (auto i = _contacts->cbegin(); ; ++i) {\n\t\t\t\t_selected = *i;\n\t\t\t\tif (!cur) {\n\t\t\t\t\tbreak;\n\t\t\t\t} else {\n\t\t\t\t\t--cur;\n\t\t\t\t}\n\t\t\t}\n\t\t\t_searchedSelected = -1;\n\t\t}\n\t\tif (dir > 0) {\n\t\t\tfor (auto i = _contacts->cfind(_selected), end = _contacts->cend(); i != end && contactData(*i)->disabledChecked; ++i) {\n\t\t\t\t_selected = *i;\n\t\t\t}\n\t\t\tif (_selected && contactData(_selected)->disabledChecked) {\n\t\t\t\t_selected = nullptr;\n\t\t\t}\n\t\t\tif (!_selected) {\n\t\t\t\tif (!_byUsername.isEmpty()) {\n\t\t\t\t\tif (_searchedSelected < 0) _searchedSelected = 0;\n\t\t\t\t\tfor (; _searchedSelected < _byUsername.size() && d_byUsername[_searchedSelected]->disabledChecked;) {\n\t\t\t\t\t\t++_searchedSelected;\n\t\t\t\t\t}\n\t\t\t\t\tif (_searchedSelected == _byUsername.size()) _searchedSelected = -1;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\twhile (_searchedSelected >= 0 && d_byUsername[_searchedSelected]->disabledChecked) {\n\t\t\t\t--_searchedSelected;\n\t\t\t}\n\t\t\tif (_searchedSelected < 0) {\n\t\t\t\tif (!_contacts->isEmpty()) {\n\t\t\t\t\tif (!_selected) _selected = *(_contacts->cend() - 1);\n\t\t\t\t\tif (_selected) {\n\t\t\t\t\t\tfor (auto i = _contacts->cfind(_selected), b = _contacts->cbegin(); i != b && contactData(*i)->disabledChecked; --i) {\n\t\t\t\t\t\t\t_selected = *i;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (contactData(_selected)->disabledChecked) {\n\t\t\t\t\t\t\t_selected = nullptr;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (_selected) {\n\t\t\temit mustScrollTo(_rowsTop + _aboutHeight + _selected->pos() * _rowHeight, _rowsTop + _aboutHeight + (_selected->pos() + 1) * _rowHeight);\n\t\t} else if (_searchedSelected >= 0) {\n\t\t\temit mustScrollTo(_rowsTop + _aboutHeight + (_contacts->size() + _searchedSelected) * _rowHeight + st::searchedBarHeight, _rowsTop + _aboutHeight + (_contacts->size() + _searchedSelected + 1) * _rowHeight + st::searchedBarHeight);\n\t\t}\n\t} else {\n\t\tint cur = (_filteredSelected >= 0) ? _filteredSelected : ((_searchedSelected >= 0) ? (_filtered.size() + _searchedSelected) : -1);\n\t\tcur += dir;\n\t\tif (cur <= 0) {\n\t\t\t_filteredSelected = _filtered.isEmpty() ? -1 : 0;\n\t\t\t_searchedSelected = (_filtered.isEmpty() && !_byUsernameFiltered.isEmpty()) ? 0 : -1;\n\t\t} else if (cur >= _filtered.size()) {\n\t\t\t_filteredSelected = -1;\n\t\t\t_searchedSelected = cur - _filtered.size();\n\t\t\tif (_searchedSelected >= _byUsernameFiltered.size()) _searchedSelected = _byUsernameFiltered.size() - 1;\n\t\t} else {\n\t\t\t_filteredSelected = cur;\n\t\t\t_searchedSelected = -1;\n\t\t}\n\t\tif (dir > 0) {\n\t\t\twhile (_filteredSelected >= 0 && _filteredSelected < _filtered.size() && contactData(_filtered[_filteredSelected])->disabledChecked) {\n\t\t\t\t++_filteredSelected;\n\t\t\t}\n\t\t\tif (_filteredSelected < 0 || _filteredSelected >= _filtered.size()) {\n\t\t\t\t_filteredSelected = -1;\n\t\t\t\tif (!_byUsernameFiltered.isEmpty()) {\n\t\t\t\t\tif (_searchedSelected < 0) _searchedSelected = 0;\n\t\t\t\t\tfor (; _searchedSelected < _byUsernameFiltered.size() && d_byUsernameFiltered[_searchedSelected]->disabledChecked;) {\n\t\t\t\t\t\t++_searchedSelected;\n\t\t\t\t\t}\n\t\t\t\t\tif (_searchedSelected == _byUsernameFiltered.size()) _searchedSelected = -1;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\twhile (_searchedSelected >= 0 && d_byUsernameFiltered[_searchedSelected]->disabledChecked) {\n\t\t\t\t--_searchedSelected;\n\t\t\t}\n\t\t\tif (_searchedSelected < 0) {\n\t\t\t\tif (!_filtered.isEmpty()) {\n\t\t\t\t\tif (_filteredSelected < 0) _filteredSelected = _filtered.size() - 1;\n\t\t\t\t\tfor (; _filteredSelected >= 0 && contactData(_filtered[_filteredSelected])->disabledChecked;) {\n\t\t\t\t\t\t--_filteredSelected;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (_filteredSelected >= 0) {\n\t\t\temit mustScrollTo(_rowsTop + _filteredSelected * _rowHeight, _rowsTop + (_filteredSelected + 1) * _rowHeight);\n\t\t} else if (_searchedSelected >= 0) {\n\t\t\tint skip = _filtered.size() * _rowHeight + st::searchedBarHeight;\n\t\t\temit mustScrollTo(_rowsTop + skip + _searchedSelected * _rowHeight, _rowsTop + skip + (_searchedSelected + 1) * _rowHeight);\n\t\t}\n\t}\n\tupdate();\n}\n\nvoid ContactsBox::Inner::selectSkipPage(int32 h, int32 dir) {\n\tint32 points = h / _rowHeight;\n\tif (!points) return;\n\tselectSkip(points * dir);\n}\n\nstd::vector<gsl::not_null<UserData*>> ContactsBox::Inner::selected() {\n\tstd::vector<gsl::not_null<UserData*>> result;\n\tif (!usingMultiSelect()) {\n\t\treturn result;\n\t}\n\n\tfor_const (auto row, *_contacts) {\n\t\tif (_checkedContacts.contains(row->history()->peer)) {\n\t\t\tcontactData(row); // fill _contactsData\n\t\t}\n\t}\n\tresult.reserve(_contactsData.size());\n\tfor (auto i = _contactsData.cbegin(), e = _contactsData.cend(); i != e; ++i) {\n\t\tif (i.value()->checkbox->checked()) {\n\t\t\tif (auto user = i.key()->asUser()) {\n\t\t\t\tresult.push_back(user);\n\t\t\t}\n\t\t}\n\t}\n\tfor (int i = 0, l = _byUsername.size(); i < l; ++i) {\n\t\tif (d_byUsername[i]->checkbox->checked()) {\n\t\t\tif (auto user = _byUsername[i]->asUser()) {\n\t\t\t\tresult.push_back(user);\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nQVector<MTPInputUser> ContactsBox::Inner::selectedInputs() {\n\tQVector<MTPInputUser> result;\n\tif (!usingMultiSelect()) {\n\t\treturn result;\n\t}\n\n\tfor_const (auto row, *_contacts) {\n\t\tif (_checkedContacts.contains(row->history()->peer)) {\n\t\t\tcontactData(row); // fill _contactsData\n\t\t}\n\t}\n\tresult.reserve(_contactsData.size());\n\tfor (auto i = _contactsData.cbegin(), e = _contactsData.cend(); i != e; ++i) {\n\t\tif (i.value()->checkbox->checked() && i.key()->isUser()) {\n\t\t\tresult.push_back(i.key()->asUser()->inputUser);\n\t\t}\n\t}\n\tfor (int i = 0, l = _byUsername.size(); i < l; ++i) {\n\t\tif (d_byUsername[i]->checkbox->checked() && _byUsername[i]->isUser()) {\n\t\t\tresult.push_back(_byUsername[i]->asUser()->inputUser);\n\t\t}\n\t}\n\treturn result;\n}\n\nbool ContactsBox::Inner::allAdmins() const {\n\treturn _allAdmins->checked();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/create_poll_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/create_poll_box.h\"\n\n#include \"poll/poll_media_upload.h\"\n#include \"base/call_delayed.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"base/event_filter.h\"\n#include \"base/random.h\"\n#include \"base/unique_qptr.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/file_utilities.h\"\n#include \"core/mime_type.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/shortcuts.h\"\n#include \"core/ui_integration.h\"\n#include \"ui/power_saving.h\"\n#include \"data/data_cloud_file.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_location.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"history/view/media/menu/history_view_poll_menu.h\"\n#include \"history/view/history_view_schedule_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"layout/layout_document_generic_preview.h\"\n#include \"main/main_app_config.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"platform/platform_file_utilities.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_send.h\"\n#include \"settings/detailed_settings_button.h\"\n#include \"settings/settings_common.h\"\n#include \"storage/file_upload.h\"\n#include \"storage/localimageloader.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/controls/emoji_button_factory.h\"\n#include \"ui/controls/location_picker.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/ttl_icon.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/boxes/choose_date_time.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/vertical_layout_reorder.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_session_controller.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\" // defaultComposeFiles.\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_overview.h\"\n#include \"styles/style_polls.h\"\n#include \"styles/style_settings.h\"\n\n#include <QtCore/QBuffer>\n#include <QtCore/QMimeData>\n\nnamespace {\n\nconstexpr auto kQuestionLimit = 255;\nconstexpr auto kMaxOptionsCount = PollData::kMaxOptions;\nconstexpr auto kOptionLimit = 100;\nconstexpr auto kWarnQuestionLimit = 80;\nconstexpr auto kWarnOptionLimit = 30;\nconstexpr auto kSolutionLimit = 200;\nconstexpr auto kWarnSolutionLimit = 60;\nconstexpr auto kErrorLimit = 99;\nconstexpr auto kMediaUploadMaxAge = 45 * 60 * crl::time(1000);\n\nusing PollMediaState = PollMediaUpload::PollMediaState;\nusing PollMediaButton = PollMediaUpload::PollMediaButton;\nusing PollMediaUploader = PollMediaUpload::PollMediaUploader;\n\nusing PollMediaUpload::FileListFromMimeData;\nusing PollMediaUpload::GenerateDocumentFilePreview;\nusing PollMediaUpload::LocalImageThumbnail;\nusing PollMediaUpload::PreparePollMediaTask;\nusing PollMediaUpload::UploadContext;\nusing PollMediaUpload::ValidateFileDragData;\n\nclass Options {\npublic:\n\tusing AttachCallback = Fn<void(\n\t\tnot_null<Ui::RpWidget*>,\n\t\tstd::shared_ptr<PollMediaState>)>;\n\tusing FieldDropCallback = Fn<void(\n\t\tnot_null<Ui::InputField*>,\n\t\tstd::shared_ptr<PollMediaState>)>;\n\tusing WidgetDropCallback = Fn<void(\n\t\tnot_null<QWidget*>,\n\t\tstd::shared_ptr<PollMediaState>)>;\n\n\tOptions(\n\t\tnot_null<Ui::BoxContent*> box,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tChatHelpers::TabbedPanel *emojiPanel,\n\t\tbool chooseCorrectEnabled,\n\t\tAttachCallback attachCallback,\n\t\tFieldDropCallback fieldDropCallback,\n\t\tWidgetDropCallback widgetDropCallback);\n\n\t[[nodiscard]] bool hasOptions() const;\n\t[[nodiscard]] bool isValid() const;\n\t[[nodiscard]] bool hasCorrect() const;\n\t[[nodiscard]] bool hasUploadingMedia() const;\n\tbool refreshStaleMedia(crl::time threshold);\n\t[[nodiscard]] std::vector<PollAnswer> toPollAnswers() const;\n\tvoid focusFirst();\n\n\tvoid enableChooseCorrect(bool enabled, bool multiCorrect = false);\n\n\t[[nodiscard]] not_null<Ui::RpWidget*> layoutWidget() const;\n\t[[nodiscard]] rpl::producer<int> usedCount() const;\n\t[[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const;\n\t[[nodiscard]] rpl::producer<> backspaceInFront() const;\n\t[[nodiscard]] rpl::producer<> tabbed() const;\n\nprivate:\n\tclass Option {\n\tpublic:\n\t\tOption(\n\t\t\tnot_null<QWidget*> outer,\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Main::Session*> session,\n\t\t\tint position,\n\t\t\tstd::shared_ptr<Ui::RadiobuttonGroup> group,\n\t\t\tAttachCallback attachCallback,\n\t\t\tFieldDropCallback fieldDropCallback,\n\t\t\tWidgetDropCallback widgetDropCallback);\n\n\t\tOption(const Option &other) = delete;\n\t\tOption &operator=(const Option &other) = delete;\n\n\t\tvoid enableChooseCorrect(\n\t\t\tstd::shared_ptr<Ui::RadiobuttonGroup> group,\n\t\t\tbool multiCorrect = false,\n\t\t\tFn<void()> checkboxChanged = nullptr);\n\n\t\tvoid show(anim::type animated);\n\t\tvoid destroy(FnMut<void()> done);\n\n\t\t[[nodiscard]] bool hasShadow() const;\n\t\tvoid createShadow();\n\t\tvoid destroyShadow();\n\n\t\t[[nodiscard]] bool isEmpty() const;\n\t\t[[nodiscard]] bool isGood() const;\n\t\t[[nodiscard]] bool isTooLong() const;\n\t\t[[nodiscard]] bool isCorrect() const;\n\t\t[[nodiscard]] bool uploadingMedia() const;\n\t\tbool refreshMediaIfStale(crl::time threshold);\n\t\t[[nodiscard]] bool hasFocus() const;\n\t\tvoid setFocus() const;\n\n\t\tvoid setPlaceholder() const;\n\t\tvoid removePlaceholder() const;\n\t\tvoid showAddIcon(bool show);\n\n\t\t[[nodiscard]] not_null<Ui::InputField*> field() const;\n\n\t\t[[nodiscard]] PollAnswer toPollAnswer(int index) const;\n\n\t\t[[nodiscard]] Ui::RpWidget *handleWidget() const;\n\n\tprivate:\n\t\tvoid createAttach();\n\t\tvoid createWarning();\n\t\tvoid createHandle();\n\t\tvoid updateFieldGeometry();\n\n\t\tbase::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _wrap;\n\t\tnot_null<Ui::RpWidget*> _content;\n\t\tbase::unique_qptr<Ui::FadeWrapScaled<Ui::Checkbox>> _correct;\n\t\tbase::unique_qptr<Ui::FadeWrapScaled<Ui::RpWidget>> _handle;\n\t\tbool _hasCorrect = false;\n\t\tUi::InputField *_field = nullptr;\n\t\tbase::unique_qptr<Ui::PlainShadow> _shadow;\n\t\tbase::unique_qptr<PollMediaButton> _attach;\n\t\tAttachCallback _attachCallback;\n\t\tFieldDropCallback _fieldDropCallback;\n\t\tWidgetDropCallback _widgetDropCallback;\n\t\tstd::shared_ptr<PollMediaState> _media;\n\t\tUi::FadeWrapScaled<Ui::RpWidget> *_addIcon = nullptr;\n\n\t};\n\n\t[[nodiscard]] bool full() const;\n\t[[nodiscard]] bool correctShadows() const;\n\tvoid fixShadows();\n\tvoid removeEmptyTail();\n\tvoid addEmptyOption();\n\tvoid checkLastOption();\n\tvoid validateState();\n\tvoid fixAfterErase();\n\tvoid destroy(std::unique_ptr<Option> option);\n\tvoid removeDestroyed(not_null<Option*> field);\n\tint findField(not_null<Ui::InputField*> field) const;\n\t[[nodiscard]] auto createChooseCorrectGroup()\n\t\t-> std::shared_ptr<Ui::RadiobuttonGroup>;\n\tvoid setupReorder();\n\tvoid restartReorder();\n\n\tnot_null<Ui::BoxContent*> _box;\n\tnot_null<Ui::VerticalLayout*> _container;\n\tconst not_null<Window::SessionController*> _controller;\n\tChatHelpers::TabbedPanel * const _emojiPanel;\n\tconst AttachCallback _attachCallback;\n\tconst FieldDropCallback _fieldDropCallback;\n\tconst WidgetDropCallback _widgetDropCallback;\n\tstd::shared_ptr<Ui::RadiobuttonGroup> _chooseCorrectGroup;\n\tbool _multiCorrect = false;\n\tFn<void()> _multiCorrectChanged;\n\tUi::VerticalLayout *_optionsLayout = nullptr;\n\tstd::unique_ptr<Ui::VerticalLayoutReorder> _reorder;\n\tint _reordering = 0;\n\tstd::vector<std::unique_ptr<Option>> _list;\n\tstd::vector<std::unique_ptr<Option>> _destroyed;\n\trpl::variable<int> _usedCount = 0;\n\tbool _hasOptions = false;\n\tbool _isValid = false;\n\tbool _hasCorrect = false;\n\trpl::event_stream<not_null<QWidget*>> _scrollToWidget;\n\trpl::event_stream<> _backspaceInFront;\n\trpl::event_stream<> _tabbed;\n\trpl::lifetime _emojiPanelLifetime;\n\n};\n\nvoid InitField(\n\t\tnot_null<QWidget*> container,\n\t\tnot_null<Ui::InputField*> field,\n\t\tnot_null<Main::Session*> session,\n\t\tstd::shared_ptr<Main::SessionShow> show = nullptr,\n\t\tbase::flat_set<QString> markdownTags = {}) {\n\tInitMessageFieldHandlers({\n\t\t.session = session,\n\t\t.show = std::move(show),\n\t\t.field = field,\n\t\t.allowMarkdownTags = std::move(markdownTags),\n\t});\n\tauto options = Ui::Emoji::SuggestionsController::Options();\n\toptions.suggestExactFirstWord = false;\n\tUi::Emoji::SuggestionsController::Init(\n\t\tcontainer,\n\t\tfield,\n\t\tsession,\n\t\toptions);\n}\n\nnot_null<Ui::FlatLabel*> CreateWarningLabel(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Ui::InputField*> field,\n\t\tint valueLimit,\n\t\tint warnLimit) {\n\tconst auto result = Ui::CreateChild<Ui::FlatLabel>(\n\t\tparent.get(),\n\t\tQString(),\n\t\tst::createPollWarning);\n\tresult->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tfield->changes(\n\t) | rpl::on_next([=] {\n\t\tUi::PostponeCall(crl::guard(field, [=] {\n\t\t\tconst auto length = field->getLastText().size();\n\t\t\tconst auto value = valueLimit - length;\n\t\t\tconst auto shown = (value < warnLimit)\n\t\t\t\t&& (field->height() > st::createPollOptionField.heightMin);\n\t\t\tif (value >= 0) {\n\t\t\t\tresult->setText(QString::number(value));\n\t\t\t} else {\n\t\t\t\tconstexpr auto kMinus = QChar(0x2212);\n\t\t\t\tresult->setMarkedText(Ui::Text::Colorized(\n\t\t\t\t\tkMinus + QString::number(std::abs(value))));\n\t\t\t}\n\t\t\tresult->setVisible(shown);\n\t\t}));\n\t}, field->lifetime());\n\treturn result;\n}\n\nvoid FocusAtEnd(not_null<Ui::InputField*> field) {\n\tfield->setFocus();\n\tfield->setCursorPosition(field->getLastText().size());\n\tfield->ensureCursorVisible();\n}\n\nnot_null<DetailedSettingsButton*> AddPollToggleButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> title,\n\t\trpl::producer<QString> description,\n\t\tSettings::IconDescriptor icon,\n\t\trpl::producer<bool> toggled,\n\t\tconst style::DetailedSettingsButtonStyle &rowStyle) {\n\treturn AddDetailedSettingsButton(\n\t\tcontainer,\n\t\tstd::move(title),\n\t\tstd::move(description),\n\t\tstd::move(icon),\n\t\tstd::move(toggled),\n\t\trowStyle);\n}\n\nOptions::Option::Option(\n\tnot_null<QWidget*> outer,\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<Main::Session*> session,\n\tint position,\n\tstd::shared_ptr<Ui::RadiobuttonGroup> group,\n\tAttachCallback attachCallback,\n\tFieldDropCallback fieldDropCallback,\n\tWidgetDropCallback widgetDropCallback)\n: _wrap(container->insert(\n\tposition,\n\tobject_ptr<Ui::SlideWrap<Ui::RpWidget>>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::RpWidget>(container))))\n, _content(_wrap->entity())\n, _field(\n\tUi::CreateChild<Ui::InputField>(\n\t\t_content.get(),\n\t\tst::createPollOptionFieldPremium,\n\t\tUi::InputField::Mode::NoNewlines,\n\t\ttr::lng_polls_create_option_add()))\n, _attachCallback(std::move(attachCallback))\n, _fieldDropCallback(std::move(fieldDropCallback))\n, _widgetDropCallback(std::move(widgetDropCallback))\n, _media(std::make_shared<PollMediaState>()) {\n\tInitField(outer, _field, session);\n\t_field->setMaxLength(kOptionLimit + kErrorLimit);\n\t_field->show();\n\tif (_fieldDropCallback) {\n\t\t_fieldDropCallback(_field, _media);\n\t}\n\n\t_wrap->hide(anim::type::instant);\n\n\t_content->paintRequest(\n\t) | rpl::on_next([content = _content.get()] {\n\t\tauto p = QPainter(content);\n\t\tp.fillRect(content->rect(), st::boxBg);\n\t}, _content->lifetime());\n\n\t_content->widthValue(\n\t) | rpl::on_next([=] {\n\t\tupdateFieldGeometry();\n\t}, _field->lifetime());\n\n\t_field->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\t_content->resize(_content->width(), height);\n\t}, _field->lifetime());\n\n\t_field->changes(\n\t) | rpl::on_next([=] {\n\t\tUi::PostponeCall(crl::guard(_field, [=] {\n\t\t\tif (_hasCorrect) {\n\t\t\t\t_correct->toggle(isGood(), anim::type::normal);\n\t\t\t} else if (_handle) {\n\t\t\t\t_handle->toggle(isGood(), anim::type::normal);\n\t\t\t}\n\t\t}));\n\t}, _field->lifetime());\n\n\tcreateShadow();\n\tcreateAttach();\n\tcreateWarning();\n\tcreateHandle();\n\tenableChooseCorrect(group);\n\tif (_correct) {\n\t\t_correct->finishAnimating();\n\t}\n\tif (_handle) {\n\t\t_handle->finishAnimating();\n\t}\n\tupdateFieldGeometry();\n}\n\nbool Options::Option::hasShadow() const {\n\treturn (_shadow != nullptr);\n}\n\nvoid Options::Option::createShadow() {\n\tExpects(_content != nullptr);\n\n\tif (_shadow) {\n\t\treturn;\n\t}\n\t_shadow.reset(Ui::CreateChild<Ui::PlainShadow>(field().get()));\n\t_shadow->show();\n\tfield()->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tconst auto left = st::createPollFieldPadding.left();\n\t\t_shadow->setGeometry(\n\t\t\tleft,\n\t\t\tsize.height() - st::lineWidth,\n\t\t\tsize.width() - left,\n\t\t\tst::lineWidth);\n\t}, _shadow->lifetime());\n}\n\nvoid Options::Option::destroyShadow() {\n\t_shadow = nullptr;\n}\n\nvoid Options::Option::createAttach() {\n\tconst auto field = Option::field();\n\tconst auto attach = Ui::CreateChild<PollMediaButton>(\n\t\tfield.get(),\n\t\tst::pollAttach,\n\t\t_media);\n\tattach->show();\n\tfield->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tattach->moveToRight(\n\t\t\tst::createPollOptionRemovePosition.x(),\n\t\t\tst::createPollOptionRemovePosition.y() - st::lineWidth * 2,\n\t\t\tsize.width());\n\t}, attach->lifetime());\n\tattach->clicks(\n\t) | rpl::on_next([=](Qt::MouseButton button) {\n\t\tif (button != Qt::LeftButton) {\n\t\t\treturn;\n\t\t}\n\t\tif (_attachCallback) {\n\t\t\t_attachCallback(not_null<Ui::RpWidget*>(attach), _media);\n\t\t}\n\t}, attach->lifetime());\n\tif (_widgetDropCallback) {\n\t\t_widgetDropCallback(attach, _media);\n\t}\n\t_attach.reset(attach);\n}\n\nvoid Options::Option::createWarning() {\n\tusing namespace rpl::mappers;\n\n\tconst auto field = this->field();\n\tconst auto warning = CreateWarningLabel(\n\t\tfield,\n\t\tfield,\n\t\tkOptionLimit,\n\t\tkWarnOptionLimit);\n\trpl::combine(\n\t\tfield->sizeValue(),\n\t\twarning->sizeValue()\n\t) | rpl::on_next([=](QSize size, QSize label) {\n\t\twarning->moveToLeft(\n\t\t\t(size.width()\n\t\t\t\t- label.width()\n\t\t\t\t- st::createPollWarningPosition.x()),\n\t\t\t(size.height()\n\t\t\t\t- label.height()\n\t\t\t\t- st::createPollWarningPosition.y()),\n\t\t\tsize.width());\n\t}, warning->lifetime());\n}\n\nvoid Options::Option::createHandle() {\n\tauto widget = object_ptr<Ui::RpWidget>(_content.get());\n\tconst auto raw = widget.data();\n\tconst auto &icon = st::pollBoxMenuPollOrderIcon;\n\traw->resize(icon.width(), icon.height());\n\traw->setCursor(Qt::SizeVerCursor);\n\traw->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\ticon.paint(p, 0, 0, raw->width());\n\t}, raw->lifetime());\n\n\tconst auto wrap = Ui::CreateChild<Ui::FadeWrapScaled<Ui::RpWidget>>(\n\t\t_content.get(),\n\t\tstd::move(widget));\n\twrap->hide(anim::type::instant);\n\n\t_content->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tconst auto left = st::createPollFieldPadding.left();\n\t\twrap->moveToLeft(\n\t\t\tleft,\n\t\t\t(size.height() - wrap->heightNoMargins()) / 2);\n\t}, wrap->lifetime());\n\n\t_handle.reset(wrap);\n}\n\nUi::RpWidget *Options::Option::handleWidget() const {\n\treturn _handle ? _handle->entity() : nullptr;\n}\n\nbool Options::Option::isEmpty() const {\n\treturn field()->getLastText().trimmed().isEmpty();\n}\n\nbool Options::Option::isGood() const {\n\treturn !field()->getLastText().trimmed().isEmpty() && !isTooLong();\n}\n\nbool Options::Option::isTooLong() const {\n\treturn (field()->getLastText().size() > kOptionLimit);\n}\n\nbool Options::Option::isCorrect() const {\n\treturn isGood() && _correct && _correct->entity()->Checkbox::checked();\n}\n\nbool Options::Option::uploadingMedia() const {\n\treturn _media->uploading;\n}\n\nbool Options::Option::refreshMediaIfStale(crl::time threshold) {\n\tif (_media->media\n\t\t&& _media->uploadedAt > 0\n\t\t&& (!threshold\n\t\t\t|| (crl::now() - _media->uploadedAt > threshold))\n\t\t&& _media->reupload) {\n\t\t_media->reupload();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool Options::Option::hasFocus() const {\n\treturn field()->hasFocus();\n}\n\nvoid Options::Option::setFocus() const {\n\tFocusAtEnd(field());\n}\n\nvoid Options::Option::setPlaceholder() const {\n\tfield()->setPlaceholder(tr::lng_polls_create_option_add());\n}\n\nvoid Options::Option::enableChooseCorrect(\n\t\tstd::shared_ptr<Ui::RadiobuttonGroup> group,\n\t\tbool multiCorrect,\n\t\tFn<void()> checkboxChanged) {\n\tif (!group && !multiCorrect) {\n\t\t_hasCorrect = false;\n\t\tif (_correct) {\n\t\t\t_correct->hide(anim::type::normal);\n\t\t}\n\t\tif (_handle) {\n\t\t\t_handle->toggle(isGood(), anim::type::normal);\n\t\t}\n\t\treturn;\n\t}\n\tstatic auto Index = 0;\n\tauto checkbox = multiCorrect\n\t\t? object_ptr<Ui::Checkbox>(\n\t\t\t_content.get(),\n\t\t\tQString(),\n\t\t\tfalse,\n\t\t\tst::defaultCheckbox,\n\t\t\tst::defaultCheck)\n\t\t: object_ptr<Ui::Checkbox>(object_ptr<Ui::Radiobutton>(\n\t\t\t_content.get(),\n\t\t\tgroup,\n\t\t\t++Index,\n\t\t\tQString(),\n\t\t\tst::defaultCheckbox));\n\tconst auto button = Ui::CreateChild<Ui::FadeWrapScaled<Ui::Checkbox>>(\n\t\t_content.get(),\n\t\tstd::move(checkbox));\n\tbutton->entity()->resize(\n\t\tbutton->entity()->height(),\n\t\tbutton->entity()->height());\n\tbutton->hide(anim::type::instant);\n\t_content->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tconst auto left = st::createPollFieldPadding.left();\n\t\tbutton->moveToLeft(\n\t\t\tleft,\n\t\t\t(size.height() - button->heightNoMargins()) / 2);\n\t}, button->lifetime());\n\t_correct.reset(button);\n\t_hasCorrect = true;\n\tif (multiCorrect && checkboxChanged) {\n\t\tbutton->entity()->checkedChanges(\n\t\t) | rpl::on_next([=](bool) {\n\t\t\tcheckboxChanged();\n\t\t}, button->lifetime());\n\t}\n\tif (isGood()) {\n\t\t_correct->show(anim::type::normal);\n\t} else {\n\t\t_correct->hide(anim::type::instant);\n\t}\n\tif (_handle) {\n\t\t_handle->hide(anim::type::normal);\n\t}\n}\n\nvoid Options::Option::updateFieldGeometry() {\n\tconst auto skip = st::defaultRadio.diameter\n\t\t+ st::defaultCheckbox.textPosition.x();\n\t_field->resizeToWidth(_content->width() - skip);\n\t_field->moveToLeft(skip, 0);\n}\n\nnot_null<Ui::InputField*> Options::Option::field() const {\n\treturn _field;\n}\n\nvoid Options::Option::removePlaceholder() const {\n\tfield()->setPlaceholder(rpl::single(QString()));\n}\n\nvoid Options::Option::showAddIcon(bool show) {\n\tif (show && !_addIcon) {\n\t\tauto icon = Settings::Icon(Settings::IconDescriptor{\n\t\t\t&st::settingsIconAdd,\n\t\t\tSettings::IconType::Round,\n\t\t\t&st::windowBgActive,\n\t\t});\n\t\tconst auto iconSize = icon.size();\n\t\tauto widget = object_ptr<Ui::RpWidget>(_content.get());\n\t\tconst auto raw = widget.data();\n\t\traw->resize(iconSize);\n\t\tconst auto iconPtr = std::make_shared<Settings::Icon>(\n\t\t\tstd::move(icon));\n\t\traw->paintOn([=](QPainter &p) {\n\t\t\ticonPtr->paint(p, 0, 0);\n\t\t});\n\n\t\tconst auto wrap =\n\t\t\tUi::CreateChild<Ui::FadeWrapScaled<Ui::RpWidget>>(\n\t\t\t\t_content.get(),\n\t\t\t\tstd::move(widget));\n\t\twrap->hide(anim::type::instant);\n\n\t\t_content->sizeValue(\n\t\t) | rpl::on_next([=](QSize size) {\n\t\t\tconst auto &handleIcon = st::pollBoxMenuPollOrderIcon;\n\t\t\tconst auto left = st::createPollFieldPadding.left()\n\t\t\t\t+ (handleIcon.width() - iconSize.width()) / 2;\n\t\t\twrap->moveToLeft(\n\t\t\t\tleft,\n\t\t\t\t(size.height() - wrap->heightNoMargins()) / 2);\n\t\t}, wrap->lifetime());\n\n\t\t_addIcon = wrap;\n\t}\n\tif (_addIcon) {\n\t\tif (show) {\n\t\t\t_addIcon->show(anim::type::normal);\n\t\t} else {\n\t\t\t_addIcon->hide(anim::type::normal);\n\t\t}\n\t}\n}\n\nPollAnswer Options::Option::toPollAnswer(int index) const {\n\tExpects(index >= 0 && index < kMaxOptionsCount);\n\n\tconst auto text = field()->getTextWithAppliedMarkdown();\n\n\tauto result = PollAnswer{\n\t\tTextWithEntities{\n\t\t\t.text = text.text,\n\t\t\t.entities = TextUtilities::ConvertTextTagsToEntities(text.tags),\n\t\t},\n\t\tQByteArray(1, ('0' + index)),\n\t};\n\tresult.media = _media->media;\n\tTextUtilities::Trim(result.text);\n\tresult.correct = _correct ? _correct->entity()->Checkbox::checked() : false;\n\treturn result;\n}\n\nOptions::Options(\n\tnot_null<Ui::BoxContent*> box,\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<Window::SessionController*> controller,\n\tChatHelpers::TabbedPanel *emojiPanel,\n\tbool chooseCorrectEnabled,\n\tAttachCallback attachCallback,\n\tFieldDropCallback fieldDropCallback,\n\tWidgetDropCallback widgetDropCallback)\n: _box(box)\n, _container(container)\n, _controller(controller)\n, _emojiPanel(emojiPanel)\n, _attachCallback(std::move(attachCallback))\n, _fieldDropCallback(std::move(fieldDropCallback))\n, _widgetDropCallback(std::move(widgetDropCallback))\n, _chooseCorrectGroup(chooseCorrectEnabled\n\t? createChooseCorrectGroup()\n\t: nullptr) {\n\tauto optionsObj = object_ptr<Ui::VerticalLayout>(container);\n\t_optionsLayout = optionsObj.data();\n\tcontainer->add(std::move(optionsObj));\n\tsetupReorder();\n\tcheckLastOption();\n}\n\nbool Options::full() const {\n\tconst auto limit = _controller->session().appConfig().pollOptionsLimit();\n\treturn (_list.size() >= limit);\n}\n\nbool Options::hasOptions() const {\n\treturn _hasOptions;\n}\n\nbool Options::isValid() const {\n\treturn _isValid;\n}\n\nbool Options::hasCorrect() const {\n\treturn _hasCorrect;\n}\n\nbool Options::hasUploadingMedia() const {\n\treturn ranges::any_of(_list, &Option::uploadingMedia);\n}\n\nbool Options::refreshStaleMedia(crl::time threshold) {\n\tauto refreshed = false;\n\tfor (const auto &option : _list) {\n\t\tif (option->refreshMediaIfStale(threshold)) {\n\t\t\trefreshed = true;\n\t\t}\n\t}\n\treturn refreshed;\n}\n\nnot_null<Ui::RpWidget*> Options::layoutWidget() const {\n\treturn _optionsLayout;\n}\n\nrpl::producer<int> Options::usedCount() const {\n\treturn _usedCount.value();\n}\n\nrpl::producer<not_null<QWidget*>> Options::scrollToWidget() const {\n\treturn _scrollToWidget.events();\n}\n\nrpl::producer<> Options::backspaceInFront() const {\n\treturn _backspaceInFront.events();\n}\n\nrpl::producer<> Options::tabbed() const {\n\treturn _tabbed.events();\n}\n\nvoid Options::Option::show(anim::type animated) {\n\t_wrap->show(animated);\n}\n\nvoid Options::Option::destroy(FnMut<void()> done) {\n\tif (anim::Disabled() || _wrap->isHidden()) {\n\t\tUi::PostponeCall(std::move(done));\n\t\treturn;\n\t}\n\t_wrap->hide(anim::type::normal);\n\tbase::call_delayed(\n\t\tst::slideWrapDuration * 2,\n\t\t_content.get(),\n\t\tstd::move(done));\n}\n\nstd::vector<PollAnswer> Options::toPollAnswers() const {\n\tauto result = std::vector<PollAnswer>();\n\tresult.reserve(_list.size());\n\tauto counter = int(0);\n\tconst auto makeAnswer = [&](const std::unique_ptr<Option> &option) {\n\t\treturn option->toPollAnswer(counter++);\n\t};\n\tranges::copy(\n\t\t_list\n\t\t| ranges::views::filter(&Option::isGood)\n\t\t| ranges::views::transform(makeAnswer),\n\t\tranges::back_inserter(result));\n\treturn result;\n}\n\nvoid Options::focusFirst() {\n\tExpects(!_list.empty());\n\n\t_list.front()->setFocus();\n}\n\nstd::shared_ptr<Ui::RadiobuttonGroup> Options::createChooseCorrectGroup() {\n\tauto result = std::make_shared<Ui::RadiobuttonGroup>(0);\n\tresult->setChangedCallback([=](int) {\n\t\tvalidateState();\n\t});\n\treturn result;\n}\n\nvoid Options::enableChooseCorrect(bool enabled, bool multiCorrect) {\n\t_multiCorrect = enabled && multiCorrect;\n\tif (_multiCorrect) {\n\t\t_chooseCorrectGroup = nullptr;\n\t\t_multiCorrectChanged = [=] { validateState(); };\n\t\tfor (auto &option : _list) {\n\t\t\toption->enableChooseCorrect(\n\t\t\t\tnullptr,\n\t\t\t\ttrue,\n\t\t\t\t_multiCorrectChanged);\n\t\t}\n\t} else {\n\t\t_multiCorrectChanged = nullptr;\n\t\t_chooseCorrectGroup = enabled\n\t\t\t? createChooseCorrectGroup()\n\t\t\t: nullptr;\n\t\tfor (auto &option : _list) {\n\t\t\toption->enableChooseCorrect(_chooseCorrectGroup);\n\t\t}\n\t}\n\tvalidateState();\n\trestartReorder();\n}\n\nbool Options::correctShadows() const {\n\t// Last one should be without shadow.\n\tconst auto noShadow = ranges::find(\n\t\t_list,\n\t\ttrue,\n\t\tranges::not_fn(&Option::hasShadow));\n\treturn (noShadow == end(_list) - 1);\n}\n\nvoid Options::fixShadows() {\n\tif (correctShadows()) {\n\t\treturn;\n\t}\n\tfor (auto &option : _list) {\n\t\toption->createShadow();\n\t}\n\t_list.back()->destroyShadow();\n}\n\nvoid Options::removeEmptyTail() {\n\t// Only one option at the end of options list can be empty.\n\t// Remove all other trailing empty options.\n\t// Only last empty and previous option have non-empty placeholders.\n\tconst auto focused = ranges::find_if(\n\t\t_list,\n\t\t&Option::hasFocus);\n\tconst auto end = _list.end();\n\tconst auto reversed = ranges::views::reverse(_list);\n\tconst auto emptyItem = ranges::find_if(\n\t\treversed,\n\t\tranges::not_fn(&Option::isEmpty)).base();\n\tconst auto focusLast = (focused > emptyItem) && (focused < end);\n\tif (emptyItem == end) {\n\t\treturn;\n\t}\n\tif (focusLast) {\n\t\t(*emptyItem)->setFocus();\n\t}\n\tfor (auto i = emptyItem + 1; i != end; ++i) {\n\t\tdestroy(std::move(*i));\n\t}\n\t_list.erase(emptyItem + 1, end);\n\tfixAfterErase();\n}\n\nvoid Options::destroy(std::unique_ptr<Option> option) {\n\tif (_reorder) {\n\t\t_reorder->cancel();\n\t}\n\tconst auto value = option.get();\n\toption->destroy([=] { removeDestroyed(value); });\n\t_destroyed.push_back(std::move(option));\n}\n\nvoid Options::fixAfterErase() {\n\tExpects(!_list.empty());\n\n\tconst auto last = _list.end() - 1;\n\t(*last)->setPlaceholder();\n\t(*last)->showAddIcon(true);\n\tif (last != begin(_list)) {\n\t\t(*(last - 1))->setPlaceholder();\n\t\t(*(last - 1))->showAddIcon(false);\n\t}\n\tfixShadows();\n}\n\nvoid Options::addEmptyOption() {\n\tif (full()) {\n\t\treturn;\n\t} else if (!_list.empty() && _list.back()->isEmpty()) {\n\t\treturn;\n\t}\n\tif (!_list.empty()) {\n\t\t_list.back()->showAddIcon(false);\n\t}\n\tif (_list.size() > 1) {\n\t\t(*(_list.end() - 2))->removePlaceholder();\n\t}\n\t_list.push_back(std::make_unique<Option>(\n\t\t_box,\n\t\t_optionsLayout,\n\t\t&_controller->session(),\n\t\t_optionsLayout->count(),\n\t\t_chooseCorrectGroup,\n\t\t_attachCallback,\n\t\t_fieldDropCallback,\n\t\t_widgetDropCallback));\n\tif (_multiCorrect) {\n\t\t_list.back()->enableChooseCorrect(\n\t\t\tnullptr,\n\t\t\ttrue,\n\t\t\t_multiCorrectChanged);\n\t}\n\tconst auto field = _list.back()->field();\n\tif (const auto emojiPanel = _emojiPanel) {\n\t\tconst auto isPremium = _controller->session().user()->isPremium();\n\t\tconst auto emojiToggle = Ui::AddEmojiToggleToField(\n\t\t\tfield,\n\t\t\t_box,\n\t\t\t_controller,\n\t\t\temojiPanel,\n\t\t\tQPoint(\n\t\t\t\t-st::createPollOptionFieldPremium.textMargins.right(),\n\t\t\t\tst::createPollOptionEmojiPositionSkip));\n\t\temojiToggle->shownValue() | rpl::on_next([=](bool shown) {\n\t\t\tif (!shown) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_emojiPanelLifetime.destroy();\n\t\t\temojiPanel->selector()->emojiChosen(\n\t\t\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\t\t\tif (field->hasFocus()) {\n\t\t\t\t\tUi::InsertEmojiAtCursor(field->textCursor(), data.emoji);\n\t\t\t\t}\n\t\t\t}, _emojiPanelLifetime);\n\t\t\tif (isPremium) {\n\t\t\t\temojiPanel->selector()->customEmojiChosen(\n\t\t\t\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\t\t\t\tif (field->hasFocus()) {\n\t\t\t\t\t\tData::InsertCustomEmoji(field, data.document);\n\t\t\t\t\t}\n\t\t\t\t}, _emojiPanelLifetime);\n\t\t\t}\n\t\t}, emojiToggle->lifetime());\n\t}\n\tfield->submits(\n\t) | rpl::on_next([=] {\n\t\tconst auto index = findField(field);\n\t\tif (_list[index]->isGood() && index + 1 < _list.size()) {\n\t\t\t_list[index + 1]->setFocus();\n\t\t}\n\t}, field->lifetime());\n\tfield->changes(\n\t) | rpl::on_next([=] {\n\t\tUi::PostponeCall(crl::guard(field, [=] {\n\t\t\tvalidateState();\n\t\t}));\n\t}, field->lifetime());\n\tfield->focusedChanges(\n\t) | rpl::filter(rpl::mappers::_1) | rpl::on_next([=] {\n\t\t_scrollToWidget.fire_copy(field);\n\t}, field->lifetime());\n\tfield->tabbed(\n\t) | rpl::on_next([=](not_null<bool*> handled) {\n\t\tconst auto index = findField(field);\n\t\tif (index + 1 < _list.size()) {\n\t\t\t_list[index + 1]->setFocus();\n\t\t} else {\n\t\t\t_tabbed.fire({});\n\t\t}\n\t\t*handled = true;\n\t}, field->lifetime());\n\tbase::install_event_filter(field, [=](not_null<QEvent*> event) {\n\t\tif (event->type() != QEvent::KeyPress\n\t\t\t|| !field->getLastText().isEmpty()) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\t\tconst auto key = static_cast<QKeyEvent*>(event.get())->key();\n\t\tif (key != Qt::Key_Backspace) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\n\t\tconst auto index = findField(field);\n\t\tif (index > 0) {\n\t\t\t_list[index - 1]->setFocus();\n\t\t} else {\n\t\t\t_backspaceInFront.fire({});\n\t\t}\n\t\treturn base::EventFilterResult::Cancel;\n\t});\n\n\t_list.back()->showAddIcon(true);\n\t_list.back()->show((_list.size() == 1)\n\t\t? anim::type::instant\n\t\t: anim::type::normal);\n\tfixShadows();\n\trestartReorder();\n}\n\nvoid Options::removeDestroyed(not_null<Option*> option) {\n\tconst auto i = ranges::find(\n\t\t_destroyed,\n\t\toption.get(),\n\t\t&std::unique_ptr<Option>::get);\n\tAssert(i != end(_destroyed));\n\t_destroyed.erase(i);\n\trestartReorder();\n}\n\nvoid Options::validateState() {\n\tcheckLastOption();\n\t_hasOptions = (ranges::count_if(_list, &Option::isGood) > 0);\n\t_isValid = _hasOptions && ranges::none_of(_list, &Option::isTooLong);\n\t_hasCorrect = ranges::any_of(_list, &Option::isCorrect);\n\n\tconst auto lastEmpty = !_list.empty() && _list.back()->isEmpty();\n\t_usedCount = _list.size() - (lastEmpty ? 1 : 0);\n}\n\nint Options::findField(not_null<Ui::InputField*> field) const {\n\tconst auto result = ranges::find(\n\t\t_list,\n\t\tfield,\n\t\t&Option::field) - begin(_list);\n\n\tEnsures(result >= 0 && result < _list.size());\n\treturn result;\n}\n\nvoid Options::checkLastOption() {\n\tremoveEmptyTail();\n\taddEmptyOption();\n}\n\nvoid Options::setupReorder() {\n\t_reorder = std::make_unique<Ui::VerticalLayoutReorder>(\n\t\t_optionsLayout);\n\t_reorder->setMouseEventProxy([=](int i)\n\t\t\t-> not_null<Ui::RpWidget*> {\n\t\tif (i < int(_list.size())) {\n\t\t\tif (const auto handle = _list[i]->handleWidget()) {\n\t\t\t\treturn handle;\n\t\t\t}\n\t\t}\n\t\treturn _optionsLayout->widgetAt(i);\n\t});\n\t_reorder->updates(\n\t) | rpl::on_next([=](Ui::VerticalLayoutReorder::Single data) {\n\t\tusing State = Ui::VerticalLayoutReorder::State;\n\t\tif (data.state == State::Started) {\n\t\t\t++_reordering;\n\t\t} else {\n\t\t\tUi::PostponeCall(_optionsLayout, [=] {\n\t\t\t\t--_reordering;\n\t\t\t});\n\t\t\tif (data.state == State::Applied) {\n\t\t\t\tbase::reorder(\n\t\t\t\t\t_list,\n\t\t\t\t\tdata.oldPosition,\n\t\t\t\t\tdata.newPosition);\n\t\t\t\tfixShadows();\n\t\t\t}\n\t\t}\n\t}, _box->lifetime());\n}\n\nvoid Options::restartReorder() {\n\tif (!_reorder) {\n\t\treturn;\n\t}\n\t_reorder->cancel();\n\n\tif (!_destroyed.empty()) {\n\t\treturn;\n\t}\n\tif (_chooseCorrectGroup || _multiCorrect) {\n\t\treturn;\n\t}\n\n\t_reorder->clearPinnedIntervals();\n\n\tconst auto count = int(_list.size());\n\tif (count < 2) {\n\t\treturn;\n\t}\n\tif (_list.back()->isEmpty()) {\n\t\t_reorder->addPinnedInterval(count - 1, 1);\n\t}\n\t_reorder->start();\n}\n\nclass DurationIconAction final : public Ui::Menu::Action {\npublic:\n\tDurationIconAction(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::Menu &st,\n\t\tnot_null<QAction*> action,\n\t\tconst QString &tinyText);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tQString _tinyText;\n\n};\n\nDurationIconAction::DurationIconAction(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::Menu &st,\n\tnot_null<QAction*> action,\n\tconst QString &tinyText)\n: Ui::Menu::Action(parent, st, action, nullptr, nullptr)\n, _tinyText(tinyText) {\n}\n\nvoid DurationIconAction::paintEvent(QPaintEvent *e) {\n\tUi::Menu::Action::paintEvent(e);\n\n\tconst auto &st = this->st();\n\tconst auto iconPos = st.itemIconPosition;\n\tconst auto size = st::createPollDurationIconSize;\n\tconst auto &pos = st::createPollDurationIconPosition;\n\tconst auto rect = QRect(\n\t\ticonPos.x() + pos.x(),\n\t\ticonPos.y() + pos.y(),\n\t\tsize,\n\t\tsize);\n\tconst auto innerRect = rect - st::createPollDurationIconMargins;\n\n\tPainter p(this);\n\tPainterHighQualityEnabler hq(p);\n\n\tUi::PaintTimerIcon(p, innerRect, _tinyText, st::menuIconColor->c);\n}\n\nvoid ShowMediaUploadingToast() {\n\tUi::Toast::Show({\n\t\t.title = tr::lng_polls_media_uploading_toast_title(tr::now),\n\t\t.text = tr::lng_polls_media_uploading_toast(tr::now, tr::marked),\n\t\t.iconLottie = u\"uploading\"_q,\n\t\t.iconLottieSize = st::pollToastUploadingIconSize,\n\t\t.duration = crl::time(3000),\n\t});\n}\n\n} // namespace\n\nCreatePollBox::CreatePollBox(\n\tQWidget*,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\tPollData::Flags chosen,\n\tPollData::Flags disabled,\n\trpl::producer<int> starsRequired,\n\tApi::SendType sendType,\n\tSendMenu::Details sendMenuDetails)\n: _controller(controller)\n, _peer(peer)\n, _chosen(chosen)\n, _disabled(disabled)\n, _sendType(sendType)\n, _sendMenuDetails([result = sendMenuDetails] { return result; })\n, _starsRequired(std::move(starsRequired)) {\n}\n\nrpl::producer<CreatePollBox::Result> CreatePollBox::submitRequests() const {\n\treturn _submitRequests.events();\n}\n\nvoid CreatePollBox::setInnerFocus() {\n\t_setInnerFocus();\n}\n\nvoid CreatePollBox::submitFailed(const QString &error) {\n\tshowToast(error);\n}\n\nvoid CreatePollBox::submitMediaExpired() {\n\tif (_refreshExpiredMedia) {\n\t\t_refreshExpiredMedia();\n\t\tShowMediaUploadingToast();\n\t}\n}\n\nnot_null<Ui::InputField*> CreatePollBox::setupQuestion(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tusing namespace Settings;\n\n\tconst auto session = &_controller->session();\n\tconst auto isPremium = session->user()->isPremium();\n\tUi::AddSubsectionTitle(container, tr::lng_polls_create_question());\n\n\tconst auto question = container->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tcontainer,\n\t\t\tst::createPollField,\n\t\t\tUi::InputField::Mode::MultiLine,\n\t\t\ttr::lng_polls_create_question_placeholder()),\n\t\tst::createPollFieldPadding\n\t\t\t+ QMargins(0, 0, st::defaultComposeFiles.emoji.inner.width, 0));\n\tInitField(\n\t\tgetDelegate()->outerContainer(),\n\t\tquestion,\n\t\tsession,\n\t\t_controller->uiShow());\n\tquestion->setMaxLength(kQuestionLimit + kErrorLimit);\n\tquestion->setSubmitSettings(Ui::InputField::SubmitSettings::Both);\n\n\t{\n\t\tusing Selector = ChatHelpers::TabbedSelector;\n\t\tconst auto outer = getDelegate()->outerContainer();\n\t\t_emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(\n\t\t\touter,\n\t\t\t_controller,\n\t\t\tobject_ptr<Selector>(\n\t\t\t\tnullptr,\n\t\t\t\t_controller->uiShow(),\n\t\t\t\tWindow::GifPauseReason::Layer,\n\t\t\t\tSelector::Mode::EmojiOnly));\n\t\tconst auto emojiPanel = _emojiPanel.get();\n\t\temojiPanel->setDesiredHeightValues(\n\t\t\t1.,\n\t\t\tst::emojiPanMinHeight / 2,\n\t\t\tst::emojiPanMinHeight);\n\t\temojiPanel->hide();\n\t\temojiPanel->selector()->setCurrentPeer(session->user());\n\n\t\tconst auto emojiToggle = Ui::AddEmojiToggleToField(\n\t\t\tquestion,\n\t\t\tthis,\n\t\t\t_controller,\n\t\t\temojiPanel,\n\t\t\tst::createPollOptionFieldPremiumEmojiPosition);\n\t\temojiToggle->show();\n\t\temojiPanel->selector()->emojiChosen(\n\t\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\t\tif (question->hasFocus()) {\n\t\t\t\tUi::InsertEmojiAtCursor(question->textCursor(), data.emoji);\n\t\t\t}\n\t\t}, emojiToggle->lifetime());\n\t\tif (isPremium) {\n\t\t\temojiPanel->selector()->customEmojiChosen(\n\t\t\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\t\t\tif (question->hasFocus()) {\n\t\t\t\t\tData::InsertCustomEmoji(question, data.document);\n\t\t\t\t}\n\t\t\t}, emojiToggle->lifetime());\n\t\t}\n\t}\n\n\tconst auto warning = CreateWarningLabel(\n\t\tcontainer,\n\t\tquestion,\n\t\tkQuestionLimit,\n\t\tkWarnQuestionLimit);\n\trpl::combine(\n\t\tquestion->geometryValue(),\n\t\twarning->sizeValue()\n\t) | rpl::on_next([=](QRect geometry, QSize label) {\n\t\twarning->moveToLeft(\n\t\t\t(container->width()\n\t\t\t\t- label.width()\n\t\t\t\t- st::createPollWarningPosition.x()),\n\t\t\t(geometry.y()\n\t\t\t\t- st::createPollFieldPadding.top()\n\t\t\t\t- st::defaultSubsectionTitlePadding.bottom()\n\t\t\t\t- st::defaultSubsectionTitle.style.font->height\n\t\t\t\t+ st::defaultSubsectionTitle.style.font->ascent\n\t\t\t\t- st::createPollWarning.style.font->ascent),\n\t\t\tgeometry.width());\n\t}, warning->lifetime());\n\n\treturn question;\n}\n\nnot_null<Ui::InputField*> CreatePollBox::setupDescription(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tconst auto session = &_controller->session();\n\tconst auto isPremium = session->user()->isPremium();\n\tconst auto description = container->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tcontainer,\n\t\t\tst::pollDescriptionField,\n\t\t\tUi::InputField::Mode::MultiLine,\n\t\t\ttr::lng_polls_create_description_placeholder()),\n\t\tst::pollDescriptionFieldPadding);\n\tInitField(\n\t\tgetDelegate()->outerContainer(),\n\t\tdescription,\n\t\tsession,\n\t\t_controller->uiShow());\n\tdescription->setSubmitSettings(Ui::InputField::SubmitSettings::Both);\n\n\tif (const auto emojiPanel = _emojiPanel.get()) {\n\t\tconst auto emojiToggle = Ui::AddEmojiToggleToField(\n\t\t\tdescription,\n\t\t\tthis,\n\t\t\t_controller,\n\t\t\temojiPanel,\n\t\t\tQPoint(\n\t\t\t\t-st::pollDescriptionField.textMargins.right(),\n\t\t\t\t-st::lineWidth));\n\t\temojiToggle->shownValue() | rpl::on_next([=](bool shown) {\n\t\t\tif (!shown) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\temojiPanel->selector()->emojiChosen(\n\t\t\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\t\t\tif (description->hasFocus()) {\n\t\t\t\t\tUi::InsertEmojiAtCursor(\n\t\t\t\t\t\tdescription->textCursor(),\n\t\t\t\t\t\tdata.emoji);\n\t\t\t\t}\n\t\t\t}, emojiToggle->lifetime());\n\t\t\tif (isPremium) {\n\t\t\t\temojiPanel->selector()->customEmojiChosen(\n\t\t\t\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\t\t\t\tif (description->hasFocus()) {\n\t\t\t\t\t\tData::InsertCustomEmoji(description, data.document);\n\t\t\t\t\t}\n\t\t\t\t}, emojiToggle->lifetime());\n\t\t\t}\n\t\t}, emojiToggle->lifetime());\n\t}\n\n\treturn description;\n}\n\nnot_null<Ui::InputField*> CreatePollBox::setupSolution(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<bool> shown) {\n\tusing namespace Settings;\n\n\tconst auto outer = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container))\n\t)->toggleOn(std::move(shown));\n\tconst auto inner = outer->entity();\n\n\tconst auto session = &_controller->session();\n\tUi::AddSkip(inner);\n\tUi::AddSubsectionTitle(inner, tr::lng_polls_solution_title());\n\tconst auto solution = inner->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tinner,\n\t\t\tst::pollMediaField,\n\t\t\tUi::InputField::Mode::MultiLine,\n\t\t\ttr::lng_polls_solution_placeholder()),\n\t\tst::createPollFieldPadding);\n\tInitField(\n\t\tgetDelegate()->outerContainer(),\n\t\tsolution,\n\t\tsession,\n\t\t_controller->uiShow(),\n\t\t{\n\t\t\tUi::InputField::kTagBold,\n\t\t\tUi::InputField::kTagItalic,\n\t\t\tUi::InputField::kTagUnderline,\n\t\t\tUi::InputField::kTagStrikeOut,\n\t\t\tUi::InputField::kTagCode,\n\t\t\tUi::InputField::kTagSpoiler,\n\t\t});\n\tsolution->setMaxLength(kSolutionLimit + kErrorLimit);\n\n\tconst auto warning = CreateWarningLabel(\n\t\tinner,\n\t\tsolution,\n\t\tkSolutionLimit,\n\t\tkWarnSolutionLimit);\n\trpl::combine(\n\t\tsolution->geometryValue(),\n\t\twarning->sizeValue()\n\t) | rpl::on_next([=](QRect geometry, QSize label) {\n\t\twarning->moveToLeft(\n\t\t\t(inner->width()\n\t\t\t\t- label.width()\n\t\t\t\t- st::createPollWarningPosition.x()),\n\t\t\t(geometry.y()\n\t\t\t\t- st::createPollFieldPadding.top()\n\t\t\t\t- st::defaultSubsectionTitlePadding.bottom()\n\t\t\t\t- st::defaultSubsectionTitle.style.font->height\n\t\t\t\t+ st::defaultSubsectionTitle.style.font->ascent\n\t\t\t\t- st::createPollWarning.style.font->ascent),\n\t\t\tgeometry.width());\n\t}, warning->lifetime());\n\n\tUi::AddDividerText(\n\t\tinner,\n\t\ttr::lng_polls_solution_about());\n\n\treturn solution;\n}\n\nobject_ptr<Ui::RpWidget> CreatePollBox::setupContent() {\n\tusing namespace Settings;\n\n\tconst auto id = base::RandomValue<uint64>();\n\tstruct UploadContext {\n\t\tstd::weak_ptr<PollMediaState> media;\n\t\tuint64 token = 0;\n\t\tQString filename;\n\t\tQString filemime;\n\t\tQVector<MTPDocumentAttribute> attributes;\n\t\tbool forceFile = true;\n\t};\n\tusing StartUploadFn = Fn<void(\n\t\tstd::shared_ptr<PollMediaState>,\n\t\tUi::PreparedFile)>;\n\tstruct State final {\n\t\tErrors error = Error::Question;\n\t\tstd::unique_ptr<Options> options;\n\t\trpl::event_stream<bool> multipleForceOff;\n\t\trpl::event_stream<bool> addOptionsForceOff;\n\t\trpl::event_stream<bool> revotingForceOff;\n\t\trpl::event_stream<bool> quizForceOff;\n\t\trpl::event_stream<bool> showWhoVotedForceOn;\n\t\trpl::variable<int> closePeriod = 0;\n\t\trpl::variable<TimeId> closeDate = TimeId(0);\n\t\tstd::shared_ptr<PollMediaState> descriptionMedia\n\t\t\t= std::make_shared<PollMediaState>();\n\t\tstd::shared_ptr<PollMediaState> solutionMedia\n\t\t\t= std::make_shared<PollMediaState>();\n\t\tstd::weak_ptr<PollMediaState> stickerTarget;\n\t\tbase::flat_map<FullMsgId, UploadContext> uploads;\n\t\tbase::unique_qptr<Ui::PopupMenu> mediaMenu;\n\t\tbase::unique_qptr<Ui::PopupMenu> durationMenu;\n\t\tbase::unique_qptr<ChatHelpers::TabbedPanel> stickerPanel;\n\t\tstd::unique_ptr<TaskQueue> prepareQueue;\n\t\tStartUploadFn startPhotoUpload;\n\t\tStartUploadFn startDocumentUpload;\n\t\tStartUploadFn startVideoUpload;\n\t};\n\tconst auto state = lifetime().make_state<State>();\n\tstate->prepareQueue = std::make_unique<TaskQueue>();\n\n\tauto result = object_ptr<Ui::VerticalLayout>(this);\n\tconst auto container = result.data();\n\n\tconst auto updateMedia = [=](\n\t\t\tconst std::shared_ptr<PollMediaState> &media) {\n\t\tif (media->update) {\n\t\t\tmedia->update();\n\t\t}\n\t};\n\tconst auto setMedia = [=](\n\t\t\tconst std::shared_ptr<PollMediaState> &media,\n\t\t\tPollMedia value,\n\t\t\tstd::shared_ptr<Ui::DynamicImage> thumbnail,\n\t\t\tbool rounded) {\n\t\tconst auto wasUploading = media->uploading;\n\t\tmedia->token++;\n\t\tmedia->media = value;\n\t\tmedia->thumbnail = std::move(thumbnail);\n\t\tmedia->rounded = rounded;\n\t\tmedia->progress = (media->uploading && media->media)\n\t\t\t? 1.\n\t\t\t: 0.;\n\t\tmedia->uploadDataId = 0;\n\t\tmedia->uploading = false;\n\t\tif (wasUploading && value) {\n\t\t\tmedia->uploadedAt = crl::now();\n\t\t} else {\n\t\t\tmedia->uploadedAt = 0;\n\t\t\tmedia->reupload = nullptr;\n\t\t}\n\t\tupdateMedia(media);\n\t};\n\tstruct UploadedMedia final {\n\t\tPollMedia input;\n\t\tstd::shared_ptr<Ui::DynamicImage> thumbnail;\n\t};\n\tconst auto parseUploaded = [=](\n\t\t\tconst MTPMessageMedia &result,\n\t\t\tFullMsgId fullId) {\n\t\tauto parsed = UploadedMedia();\n\t\tauto &owner = _controller->session().data();\n\t\tresult.match([&](const MTPDmessageMediaPhoto &media) {\n\t\t\tif (const auto photo = media.vphoto()) {\n\t\t\t\tphoto->match([&](const MTPDphoto &) {\n\t\t\t\t\tparsed.input.photo = owner.processPhoto(*photo);\n\t\t\t\t\tparsed.thumbnail = Ui::MakePhotoThumbnail(\n\t\t\t\t\t\tparsed.input.photo,\n\t\t\t\t\t\tfullId);\n\t\t\t\t}, [](const auto &) {\n\t\t\t\t});\n\t\t\t}\n\t\t}, [&](const MTPDmessageMediaDocument &media) {\n\t\t\tif (const auto document = media.vdocument()) {\n\t\t\t\tdocument->match([&](const MTPDdocument &) {\n\t\t\t\t\tparsed.input.document = owner.processDocument(\n\t\t\t\t\t\t*document);\n\t\t\t\t\tparsed.thumbnail\n\t\t\t\t\t\t= Ui::MakeDocumentFilePreviewThumbnail(\n\t\t\t\t\t\t\tparsed.input.document,\n\t\t\t\t\t\t\tfullId);\n\t\t\t\t}, [](const auto &) {\n\t\t\t\t});\n\t\t\t}\n\t\t}, [](const auto &) {\n\t\t});\n\t\treturn parsed;\n\t};\n\tconst auto applyUploaded = [=](\n\t\t\tconst std::shared_ptr<PollMediaState> &media,\n\t\t\tuint64 token,\n\t\t\tFullMsgId fullId,\n\t\t\tconst MTPInputFile &file) {\n\t\tconst auto uploaded = MTP_inputMediaUploadedPhoto(\n\t\t\tMTP_flags(0),\n\t\t\tfile,\n\t\t\tMTP_vector<MTPInputDocument>(QVector<MTPInputDocument>()),\n\t\t\tMTPint(),\n\t\t\tMTPInputDocument());\n\t\t_controller->session().api().request(MTPmessages_UploadMedia(\n\t\t\tMTP_flags(0),\n\t\t\tMTPstring(),\n\t\t\t_peer->input(),\n\t\t\tuploaded\n\t\t)).done([=](const MTPMessageMedia &result) {\n\t\t\tif (media->token != token) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto parsed = parseUploaded(result, fullId);\n\t\t\tif (!parsed.input) {\n\t\t\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t\t\tshowToast(tr::lng_attach_failed(tr::now));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsetMedia(\n\t\t\t\tmedia,\n\t\t\t\tparsed.input,\n\t\t\t\tmedia->thumbnail\n\t\t\t\t\t? media->thumbnail\n\t\t\t\t\t: std::move(parsed.thumbnail),\n\t\t\t\ttrue);\n\t\t}).fail([=](const MTP::Error &) {\n\t\t\tif (media->token != token) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t\tshowToast(tr::lng_attach_failed(tr::now));\n\t\t}).send();\n\t};\n\tconst auto applyUploadedDocument = [=](\n\t\t\tconst std::shared_ptr<PollMediaState> &media,\n\t\t\tuint64 token,\n\t\t\tFullMsgId fullId,\n\t\t\tconst Api::RemoteFileInfo &info,\n\t\t\tconst UploadContext &context) {\n\t\tusing Flag = MTPDinputMediaUploadedDocument::Flag;\n\t\tconst auto flags = (context.forceFile ? Flag::f_force_file : Flag())\n\t\t\t| (info.thumb ? Flag::f_thumb : Flag());\n\t\tauto attributes = !context.attributes.isEmpty()\n\t\t\t? context.attributes\n\t\t\t: QVector<MTPDocumentAttribute>{\n\t\t\t\tMTP_documentAttributeFilename(\n\t\t\t\t\tMTP_string(context.filename)),\n\t\t\t};\n\t\tconst auto uploaded = MTP_inputMediaUploadedDocument(\n\t\t\tMTP_flags(flags),\n\t\t\tinfo.file,\n\t\t\tinfo.thumb.value_or(MTPInputFile()),\n\t\t\tMTP_string(context.filemime),\n\t\t\tMTP_vector<MTPDocumentAttribute>(std::move(attributes)),\n\t\t\tMTP_vector<MTPInputDocument>(),\n\t\t\tMTPInputPhoto(),\n\t\t\tMTP_int(0),\n\t\t\tMTP_int(0));\n\t\t_controller->session().api().request(MTPmessages_UploadMedia(\n\t\t\tMTP_flags(0),\n\t\t\tMTPstring(),\n\t\t\t_peer->input(),\n\t\t\tuploaded\n\t\t)).done([=](const MTPMessageMedia &result) {\n\t\t\tif (media->token != token) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto parsed = parseUploaded(result, fullId);\n\t\t\tif (!parsed.input) {\n\t\t\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t\t\tshowToast(tr::lng_attach_failed(tr::now));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto isVideo = parsed.input.document\n\t\t\t\t&& parsed.input.document->isVideoFile();\n\t\t\tsetMedia(\n\t\t\t\tmedia,\n\t\t\t\tparsed.input,\n\t\t\t\tisVideo\n\t\t\t\t\t? (media->thumbnail\n\t\t\t\t\t\t? media->thumbnail\n\t\t\t\t\t\t: std::move(parsed.thumbnail))\n\t\t\t\t\t: std::move(parsed.thumbnail),\n\t\t\t\tisVideo);\n\t\t}).fail([=](const MTP::Error &) {\n\t\t\tif (media->token != token) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t\tshowToast(tr::lng_attach_failed(tr::now));\n\t\t}).send();\n\t};\n\t_controller->session().uploader().photoReady(\n\t) | rpl::on_next([=](const Storage::UploadedMedia &data) {\n\t\tconst auto context = state->uploads.take(data.fullId);\n\t\tif (!context) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto media = context->media.lock();\n\t\tif (!media || (media->token != context->token)) {\n\t\t\treturn;\n\t\t}\n\t\tapplyUploaded(media, context->token, data.fullId, data.info.file);\n\t}, lifetime());\n\t_controller->session().uploader().photoProgress(\n\t) | rpl::on_next([=](const FullMsgId &id) {\n\t\tconst auto i = state->uploads.find(id);\n\t\tif (i == state->uploads.end()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto &context = i->second;\n\t\tconst auto media = context.media.lock();\n\t\tif (!media\n\t\t\t|| (media->token != context.token)\n\t\t\t|| !media->uploadDataId) {\n\t\t\treturn;\n\t\t}\n\t\tmedia->progress = _controller->session().data().photo(\n\t\t\tmedia->uploadDataId)->progress();\n\t\tupdateMedia(media);\n\t}, lifetime());\n\t_controller->session().uploader().photoFailed(\n\t) | rpl::on_next([=](const FullMsgId &id) {\n\t\tconst auto context = state->uploads.take(id);\n\t\tif (!context) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto media = context->media.lock();\n\t\tif (!media || (media->token != context->token)) {\n\t\t\treturn;\n\t\t}\n\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\tshowToast(tr::lng_attach_failed(tr::now));\n\t}, lifetime());\n\t_controller->session().uploader().documentReady(\n\t) | rpl::on_next([=](const Storage::UploadedMedia &data) {\n\t\tconst auto context = state->uploads.take(data.fullId);\n\t\tif (!context) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto media = context->media.lock();\n\t\tif (!media || (media->token != context->token)) {\n\t\t\treturn;\n\t\t}\n\t\tapplyUploadedDocument(\n\t\t\tmedia,\n\t\t\tcontext->token,\n\t\t\tdata.fullId,\n\t\t\tdata.info,\n\t\t\t*context);\n\t}, lifetime());\n\t_controller->session().uploader().documentProgress(\n\t) | rpl::on_next([=](const FullMsgId &id) {\n\t\tconst auto i = state->uploads.find(id);\n\t\tif (i == state->uploads.end()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto &context = i->second;\n\t\tconst auto media = context.media.lock();\n\t\tif (!media\n\t\t\t|| (media->token != context.token)\n\t\t\t|| !media->uploadDataId) {\n\t\t\treturn;\n\t\t}\n\t\tmedia->progress = _controller->session().data().document(\n\t\t\tmedia->uploadDataId)->progress();\n\t\tupdateMedia(media);\n\t}, lifetime());\n\t_controller->session().uploader().documentFailed(\n\t) | rpl::on_next([=](const FullMsgId &id) {\n\t\tconst auto context = state->uploads.take(id);\n\t\tif (!context) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto media = context->media.lock();\n\t\tif (!media || (media->token != context->token)) {\n\t\t\treturn;\n\t\t}\n\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\tshowToast(tr::lng_attach_failed(tr::now));\n\t}, lifetime());\n\tconst auto emojiPaused = [=] {\n\t\tusing namespace Window;\n\t\treturn _controller->isGifPausedAtLeastFor(GifPauseReason::Any);\n\t};\n\tconst auto updateStickerPanelGeometry = [=] {\n\t\tif (!state->stickerPanel) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto panel = state->stickerPanel.get();\n\t\tconst auto parent = panel->parentWidget();\n\t\tconst auto left = std::max(\n\t\t\t(parent->width() - panel->width()) / 2,\n\t\t\t0);\n\t\tconst auto top = std::max(\n\t\t\t(parent->height() - panel->height()) / 2,\n\t\t\t0);\n\t\tpanel->moveTopRight(top, left + panel->width());\n\t};\n\tconst auto showStickerPanel = [=](\n\t\t\tnot_null<Ui::RpWidget*>,\n\t\t\tstd::shared_ptr<PollMediaState> media) {\n\t\tif (!state->stickerPanel) {\n\t\t\tconst auto body = getDelegate()->outerContainer();\n\t\t\tstate->stickerPanel = HistoryView::CreatePollStickerPanel(\n\t\t\t\tbody,\n\t\t\t\t_controller);\n\t\t\tstate->stickerPanel->setDropDown(true);\n\t\t\tbase::install_event_filter(\n\t\t\t\tbody,\n\t\t\t\t[=](not_null<QEvent*> event) {\n\t\t\t\t\tconst auto type = event->type();\n\t\t\t\t\tif (type == QEvent::Move || type == QEvent::Resize) {\n\t\t\t\t\t\tcrl::on_main(this, updateStickerPanelGeometry);\n\t\t\t\t\t}\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t},\n\t\tlifetime());\n\t\t\tstate->stickerPanel->selector()->fileChosen(\n\t\t\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\t\t\tif (Window::ShowSendPremiumError(\n\t\t\t\t\t\t_controller,\n\t\t\t\t\t\tdata.document)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto target = state->stickerTarget.lock();\n\t\t\t\tif (!target) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsetMedia(\n\t\t\t\t\ttarget,\n\t\t\t\t\tPollMedia{ .document = data.document },\n\t\t\t\t\tUi::MakeEmojiThumbnail(\n\t\t\t\t\t\t&_controller->session().data(),\n\t\t\t\t\t\tData::SerializeCustomEmojiId(data.document),\n\t\t\t\t\t\temojiPaused),\n\t\t\t\t\tfalse);\n\t\t\t\tstate->stickerPanel->hideAnimated();\n\t\t\t}, state->stickerPanel->lifetime());\n\t\t}\n\t\tstate->stickerTarget = media;\n\t\tconst auto panel = state->stickerPanel.get();\n\t\tupdateStickerPanelGeometry();\n\t\tpanel->toggleAnimated();\n\t};\n\tconst auto asyncReupload = [=](\n\t\t\tstd::shared_ptr<PollMediaState> media,\n\t\t\tFn<Ui::PreparedList()> prepare,\n\t\t\tFn<bool(const Ui::PreparedList&)> validate,\n\t\t\tconst QString &name,\n\t\t\tconst StartUploadFn &startUpload) {\n\t\tconst auto reuploadToken = ++media->token;\n\t\tmedia->media = PollMedia();\n\t\tmedia->uploading = true;\n\t\tmedia->progress = 0.;\n\t\tmedia->uploadDataId = 0;\n\t\tupdateMedia(media);\n\t\tconst auto weak = QPointer<CreatePollBox>(this);\n\t\tcrl::async([=, prepare = std::move(prepare)] {\n\t\t\tauto list = prepare();\n\t\t\tcrl::on_main([=, list = std::move(list)]() mutable {\n\t\t\t\tif (!weak || media->token != reuploadToken) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (list.error != Ui::PreparedList::Error::None\n\t\t\t\t\t|| list.files.empty()\n\t\t\t\t\t|| (validate && !validate(list))) {\n\t\t\t\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t\t\t\tshowToast(tr::lng_attach_failed(tr::now));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tauto &f = list.files.front();\n\t\t\t\tif (!name.isEmpty()) {\n\t\t\t\t\tf.displayName = name;\n\t\t\t\t}\n\t\t\t\tstartUpload(media, std::move(f));\n\t\t\t});\n\t\t});\n\t};\n\tconst auto setFileReupload = [=](\n\t\t\tstd::shared_ptr<PollMediaState> media,\n\t\t\tconst QString &path,\n\t\t\tconst QString &name,\n\t\t\tconst StartUploadFn &startUpload) {\n\t\tmedia->reupload = crl::guard(this, [=,\n\t\t\t\tweak = std::weak_ptr(media)] {\n\t\t\tconst auto strong = weak.lock();\n\t\t\tif (!strong) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (path.isEmpty()) {\n\t\t\t\tsetMedia(strong, PollMedia(), nullptr, false);\n\t\t\t\tshowToast(tr::lng_attach_failed(tr::now));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto premium = _controller->session().premium();\n\t\t\tasyncReupload(\n\t\t\t\tstrong,\n\t\t\t\t[=] {\n\t\t\t\t\treturn Storage::PrepareMediaList(\n\t\t\t\t\t\tQStringList{ path },\n\t\t\t\t\t\tst::sendMediaPreviewSize,\n\t\t\t\t\t\tpremium);\n\t\t\t\t},\n\t\t\t\tnullptr,\n\t\t\t\tname,\n\t\t\t\tstartUpload);\n\t\t});\n\t};\n\tconst auto startPreparedPhotoUpload = [=](\n\t\t\tstd::shared_ptr<PollMediaState> media,\n\t\t\tUi::PreparedFile file) {\n\t\tconst auto sourceImage = (file.path.isEmpty()\n\t\t\t&& file.content.isEmpty()\n\t\t\t&& file.information)\n\t\t\t? [&]() -> QImage {\n\t\t\t\tconst auto img = std::get_if<\n\t\t\t\t\tUi::PreparedFileInformation::Image>(\n\t\t\t\t\t\t&file.information->media);\n\t\t\t\treturn (img && !img->data.isNull())\n\t\t\t\t\t? img->data\n\t\t\t\t\t: QImage();\n\t\t\t}()\n\t\t\t: QImage();\n\t\tmedia->reupload = crl::guard(this, [=,\n\t\t\t\tweak = std::weak_ptr(media),\n\t\t\t\tpath = file.path,\n\t\t\t\tcontent = file.content,\n\t\t\t\tname = file.displayName] {\n\t\t\tconst auto strong = weak.lock();\n\t\t\tif (!strong) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto premium = _controller->session().premium();\n\t\t\tasyncReupload(\n\t\t\t\tstrong,\n\t\t\t\t[=] {\n\t\t\t\t\tif (!path.isEmpty()) {\n\t\t\t\t\t\treturn Storage::PrepareMediaList(\n\t\t\t\t\t\t\tQStringList{ path },\n\t\t\t\t\t\t\tst::sendMediaPreviewSize,\n\t\t\t\t\t\t\tpremium);\n\t\t\t\t\t}\n\t\t\t\t\tif (!content.isEmpty()) {\n\t\t\t\t\t\tauto image = QImage::fromData(content);\n\t\t\t\t\t\tif (!image.isNull()) {\n\t\t\t\t\t\t\treturn Storage::PrepareMediaFromImage(\n\t\t\t\t\t\t\t\tstd::move(image),\n\t\t\t\t\t\t\t\tQByteArray(content),\n\t\t\t\t\t\t\t\tst::sendMediaPreviewSize);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (!sourceImage.isNull()) {\n\t\t\t\t\t\tauto bytes = QByteArray();\n\t\t\t\t\t\tauto buffer = QBuffer(&bytes);\n\t\t\t\t\t\tbuffer.open(QIODevice::WriteOnly);\n\t\t\t\t\t\tsourceImage.save(&buffer, \"PNG\");\n\t\t\t\t\t\treturn Storage::PrepareMediaFromImage(\n\t\t\t\t\t\t\tQImage(sourceImage),\n\t\t\t\t\t\t\tstd::move(bytes),\n\t\t\t\t\t\t\tst::sendMediaPreviewSize);\n\t\t\t\t\t}\n\t\t\t\t\treturn Ui::PreparedList(\n\t\t\t\t\t\tUi::PreparedList::Error::EmptyFile,\n\t\t\t\t\t\tQString());\n\t\t\t\t},\n\t\t\t\t[](const Ui::PreparedList &list) {\n\t\t\t\t\treturn list.files.front().type\n\t\t\t\t\t\t== Ui::PreparedFile::Type::Photo;\n\t\t\t\t},\n\t\t\t\tname,\n\t\t\t\tstate->startPhotoUpload);\n\t\t});\n\t\tconst auto token = ++media->token;\n\t\tmedia->media = PollMedia();\n\t\tmedia->thumbnail = std::make_shared<LocalImageThumbnail>(\n\t\t\tstd::move(file.preview));\n\t\tmedia->rounded = true;\n\t\tmedia->uploading = true;\n\t\tmedia->progress = 0.;\n\t\tmedia->uploadDataId = 0;\n\t\tupdateMedia(media);\n\t\tusing PreparePoll = PreparePollMediaTask;\n\t\tstate->prepareQueue->addTask(std::make_unique<PreparePoll>(\n\t\t\tFileLoadTask::Args{\n\t\t\t\t.session = &_controller->session(),\n\t\t\t\t.filepath = file.path,\n\t\t\t\t.content = file.content,\n\t\t\t\t.information = std::move(file.information),\n\t\t\t\t.videoCover = nullptr,\n\t\t\t\t.type = SendMediaType::Photo,\n\t\t\t\t.to = FileLoadTo(\n\t\t\t\t\t_peer->id,\n\t\t\t\t\tApi::SendOptions(),\n\t\t\t\t\tFullReplyTo(),\n\t\t\t\t\tMsgId()),\n\t\t\t\t.caption = TextWithTags(),\n\t\t\t\t.spoiler = false,\n\t\t\t\t.album = nullptr,\n\t\t\t\t.forceFile = false,\n\t\t\t\t.idOverride = 0,\n\t\t\t\t.displayName = file.displayName,\n\t\t\t},\n\t\t\t[=](std::shared_ptr<FilePrepareResult> prepared) {\n\t\t\t\tif ((media->token != token)\n\t\t\t\t\t|| !prepared\n\t\t\t\t\t|| (prepared->type != SendMediaType::Photo)) {\n\t\t\t\t\tif (media->token == token) {\n\t\t\t\t\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t\t\t\t\tshowToast(tr::lng_attach_failed(tr::now));\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto uploadId = FullMsgId(\n\t\t\t\t\t_peer->id,\n\t\t\t\t\t_controller->session().data().nextLocalMessageId());\n\t\t\t\tstate->uploads.emplace(uploadId, UploadContext{\n\t\t\t\t\t.media = media,\n\t\t\t\t\t.token = token,\n\t\t\t\t});\n\t\t\t\tmedia->uploadDataId = prepared->id;\n\t\t\t\t_controller->session().uploader().upload(\n\t\t\t\t\tuploadId,\n\t\t\t\t\tprepared);\n\t\t\t}));\n\t};\n\tconst auto startPreparedDocumentUpload = [=](\n\t\t\tstd::shared_ptr<PollMediaState> media,\n\t\t\tUi::PreparedFile file) {\n\t\tconst auto displayName = file.displayName.isEmpty()\n\t\t\t? QFileInfo(file.path).fileName()\n\t\t\t: file.displayName;\n\t\tsetFileReupload(\n\t\t\tmedia,\n\t\t\tfile.path,\n\t\t\tdisplayName,\n\t\t\tstate->startDocumentUpload);\n\t\tauto audioAttributes = PollMediaUpload::ExtractAudioAttributes(file);\n\t\tconst auto isAudio = !audioAttributes.isEmpty();\n\t\tconst auto token = ++media->token;\n\t\tmedia->media = PollMedia();\n\t\tmedia->thumbnail = std::make_shared<LocalImageThumbnail>(\n\t\t\tGenerateDocumentFilePreview(\n\t\t\t\tdisplayName,\n\t\t\t\tst::pollAttach.rippleAreaSize));\n\t\tmedia->rounded = false;\n\t\tmedia->uploading = true;\n\t\tmedia->progress = 0.;\n\t\tmedia->uploadDataId = 0;\n\t\tupdateMedia(media);\n\t\tusing PreparePoll = PreparePollMediaTask;\n\t\tstate->prepareQueue->addTask(std::make_unique<PreparePoll>(\n\t\t\tFileLoadTask::Args{\n\t\t\t\t.session = &_controller->session(),\n\t\t\t\t.filepath = file.path,\n\t\t\t\t.content = file.content,\n\t\t\t\t.information = std::move(file.information),\n\t\t\t\t.videoCover = nullptr,\n\t\t\t\t.type = SendMediaType::File,\n\t\t\t\t.to = FileLoadTo(\n\t\t\t\t\t_peer->id,\n\t\t\t\t\tApi::SendOptions(),\n\t\t\t\t\tFullReplyTo(),\n\t\t\t\t\tMsgId()),\n\t\t\t\t.caption = TextWithTags(),\n\t\t\t\t.spoiler = false,\n\t\t\t\t.album = nullptr,\n\t\t\t\t.forceFile = !isAudio,\n\t\t\t\t.idOverride = 0,\n\t\t\t\t.displayName = displayName,\n\t\t\t},\n\t\t\t[=, attributes = std::move(audioAttributes)](\n\t\t\t\t\tstd::shared_ptr<FilePrepareResult> prepared) {\n\t\t\t\tif ((media->token != token) || !prepared) {\n\t\t\t\t\tif (media->token == token) {\n\t\t\t\t\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t\t\t\t\tshowToast(tr::lng_attach_failed(tr::now));\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto uploadId = FullMsgId(\n\t\t\t\t\t_peer->id,\n\t\t\t\t\t_controller->session().data().nextLocalMessageId());\n\t\t\t\tstate->uploads.emplace(uploadId, UploadContext{\n\t\t\t\t\t.media = media,\n\t\t\t\t\t.token = token,\n\t\t\t\t\t.filename = prepared->filename,\n\t\t\t\t\t.filemime = prepared->filemime,\n\t\t\t\t\t.attributes = attributes,\n\t\t\t\t\t.forceFile = !isAudio,\n\t\t\t\t});\n\t\t\t\tmedia->uploadDataId = prepared->id;\n\t\t\t\t_controller->session().uploader().upload(\n\t\t\t\t\tuploadId,\n\t\t\t\t\tprepared);\n\t\t\t}));\n\t};\n\tconst auto startPreparedVideoUpload = [=](\n\t\t\tstd::shared_ptr<PollMediaState> media,\n\t\t\tUi::PreparedFile file) {\n\t\tsetFileReupload(\n\t\t\tmedia,\n\t\t\tfile.path,\n\t\t\tfile.displayName,\n\t\t\tstate->startVideoUpload);\n\t\tconst auto token = ++media->token;\n\t\tmedia->media = PollMedia();\n\t\tmedia->thumbnail = std::make_shared<LocalImageThumbnail>(\n\t\t\tstd::move(file.preview));\n\t\tmedia->rounded = true;\n\t\tmedia->uploading = true;\n\t\tmedia->progress = 0.;\n\t\tmedia->uploadDataId = 0;\n\t\tupdateMedia(media);\n\t\tusing PreparePoll = PreparePollMediaTask;\n\t\tstate->prepareQueue->addTask(std::make_unique<PreparePoll>(\n\t\t\tFileLoadTask::Args{\n\t\t\t\t.session = &_controller->session(),\n\t\t\t\t.filepath = file.path,\n\t\t\t\t.content = file.content,\n\t\t\t\t.information = std::move(file.information),\n\t\t\t\t.videoCover = nullptr,\n\t\t\t\t.type = SendMediaType::File,\n\t\t\t\t.to = FileLoadTo(\n\t\t\t\t\t_peer->id,\n\t\t\t\t\tApi::SendOptions(),\n\t\t\t\t\tFullReplyTo(),\n\t\t\t\t\tMsgId()),\n\t\t\t\t.caption = TextWithTags(),\n\t\t\t\t.spoiler = false,\n\t\t\t\t.album = nullptr,\n\t\t\t\t.forceFile = false,\n\t\t\t\t.idOverride = 0,\n\t\t\t\t.displayName = file.displayName,\n\t\t\t},\n\t\t\t[=](std::shared_ptr<FilePrepareResult> prepared) {\n\t\t\t\tif ((media->token != token) || !prepared) {\n\t\t\t\t\tif (media->token == token) {\n\t\t\t\t\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t\t\t\t\tshowToast(tr::lng_attach_failed(tr::now));\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tauto attributes = QVector<MTPDocumentAttribute>();\n\t\t\t\tprepared->document.match([&](const MTPDdocument &data) {\n\t\t\t\t\tattributes = data.vattributes().v;\n\t\t\t\t}, [](const auto &) {\n\t\t\t\t});\n\t\t\t\tconst auto uploadId = FullMsgId(\n\t\t\t\t\t_peer->id,\n\t\t\t\t\t_controller->session().data().nextLocalMessageId());\n\t\t\t\tstate->uploads.emplace(uploadId, UploadContext{\n\t\t\t\t\t.media = media,\n\t\t\t\t\t.token = token,\n\t\t\t\t\t.filename = prepared->filename,\n\t\t\t\t\t.filemime = prepared->filemime,\n\t\t\t\t\t.attributes = std::move(attributes),\n\t\t\t\t\t.forceFile = false,\n\t\t\t\t});\n\t\t\t\tmedia->uploadDataId = prepared->id;\n\t\t\t\t_controller->session().uploader().upload(\n\t\t\t\t\tuploadId,\n\t\t\t\t\tprepared);\n\t\t\t}));\n\t};\n\tstate->startPhotoUpload = startPreparedPhotoUpload;\n\tstate->startDocumentUpload = startPreparedDocumentUpload;\n\tstate->startVideoUpload = startPreparedVideoUpload;\n\tconst auto applyPreparedPhotoList = [=](\n\t\t\tstd::shared_ptr<PollMediaState> media,\n\t\t\tUi::PreparedList &&list) {\n\t\tif (list.error != Ui::PreparedList::Error::None\n\t\t\t|| (list.files.size() != 1)\n\t\t\t|| (list.files.front().type != Ui::PreparedFile::Type::Photo)) {\n\t\t\treturn false;\n\t\t}\n\t\tstartPreparedPhotoUpload(media, std::move(list.files.front()));\n\t\treturn true;\n\t};\n\tusing ValidateFn = Fn<bool(not_null<const QMimeData*>)>;\n\tusing ApplyDropFn = Fn<bool(\n\t\tstd::shared_ptr<PollMediaState>,\n\t\tnot_null<const QMimeData*>)>;\n\tconst auto installDropToWidget = [=](\n\t\t\tnot_null<QWidget*> widget,\n\t\t\tstd::shared_ptr<PollMediaState> media,\n\t\t\tValidateFn validate,\n\t\t\tApplyDropFn apply) {\n\t\twidget->setAcceptDrops(true);\n\t\tbase::install_event_filter(widget, [=](not_null<QEvent*> event) {\n\t\t\tconst auto type = event->type();\n\t\t\tif (type != QEvent::DragEnter\n\t\t\t\t&& type != QEvent::DragMove\n\t\t\t\t&& type != QEvent::Drop) {\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t}\n\t\t\tconst auto drop = static_cast<QDropEvent*>(event.get());\n\t\t\tconst auto data = drop->mimeData();\n\t\t\tif (!data || !validate(data)) {\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t}\n\t\t\tif (type == QEvent::Drop && !apply(media, data)) {\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t}\n\t\t\tdrop->acceptProposedAction();\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t});\n\t};\n\tconst auto installDropToField = [=](\n\t\t\tnot_null<Ui::InputField*> field,\n\t\t\tstd::shared_ptr<PollMediaState> media,\n\t\t\tValidateFn validate,\n\t\t\tApplyDropFn apply) {\n\t\tfield->setMimeDataHook([=](\n\t\t\t\tnot_null<const QMimeData*> data,\n\t\t\t\tUi::InputField::MimeAction action) {\n\t\t\tif (action == Ui::InputField::MimeAction::Check) {\n\t\t\t\treturn validate(data);\n\t\t\t} else if (action == Ui::InputField::MimeAction::Insert) {\n\t\t\t\treturn apply(media, data);\n\t\t\t}\n\t\t\tUnexpected(\"Polls: action in MimeData hook.\");\n\t\t});\n\t};\n\tconst auto applyPhotoOrVideoDrop = ApplyDropFn([=](\n\t\t\tstd::shared_ptr<PollMediaState> media,\n\t\t\tnot_null<const QMimeData*> data) {\n\t\tauto list = FileListFromMimeData(\n\t\t\tdata,\n\t\t\t_controller->session().premium());\n\t\tif (list.error != Ui::PreparedList::Error::None\n\t\t\t|| list.files.empty()) {\n\t\t\treturn false;\n\t\t}\n\t\tauto &file = list.files.front();\n\t\tif (file.type == Ui::PreparedFile::Type::Photo) {\n\t\t\tstartPreparedPhotoUpload(media, std::move(file));\n\t\t\treturn true;\n\t\t} else if (file.type == Ui::PreparedFile::Type::Video) {\n\t\t\tstartPreparedVideoUpload(media, std::move(file));\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t});\n\tconst auto validatePhotoOrVideo = ValidateFn([](\n\t\t\tnot_null<const QMimeData*> data) {\n\t\tif (data->hasImage()) {\n\t\t\treturn true;\n\t\t}\n\t\tconst auto urls = Core::ReadMimeUrls(data);\n\t\tif (urls.size() != 1 || !urls.front().isLocalFile()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto file = Platform::File::UrlToLocal(urls.front());\n\t\tconst auto mime = Core::MimeTypeForFile(QFileInfo(file)).name();\n\t\treturn Core::IsMimeAcceptedForPhotoVideoAlbum(mime);\n\t});\n\tconst auto installPhotoDropToWidget = [=](\n\t\t\tnot_null<QWidget*> widget,\n\t\t\tstd::shared_ptr<PollMediaState> media) {\n\t\tinstallDropToWidget(\n\t\t\twidget,\n\t\t\tmedia,\n\t\t\tvalidatePhotoOrVideo,\n\t\t\tapplyPhotoOrVideoDrop);\n\t};\n\tconst auto installPhotoDropToField = [=](\n\t\t\tnot_null<Ui::InputField*> field,\n\t\t\tstd::shared_ptr<PollMediaState> media) {\n\t\tinstallDropToField(\n\t\t\tfield,\n\t\t\tmedia,\n\t\t\tvalidatePhotoOrVideo,\n\t\t\tapplyPhotoOrVideoDrop);\n\t};\n\tconst auto applyFileDrop = ApplyDropFn([=](\n\t\t\tstd::shared_ptr<PollMediaState> media,\n\t\t\tnot_null<const QMimeData*> data) {\n\t\tauto list = FileListFromMimeData(\n\t\t\tdata,\n\t\t\t_controller->session().premium());\n\t\tif (list.error == Ui::PreparedList::Error::TooLargeFile) {\n\t\t\tconst auto fileSize = list.files.empty()\n\t\t\t\t? 0\n\t\t\t\t: list.files.front().size;\n\t\t\t_controller->show(Box(\n\t\t\t\tFileSizeLimitBox,\n\t\t\t\t&_controller->session(),\n\t\t\t\tfileSize,\n\t\t\t\tnullptr));\n\t\t\treturn false;\n\t\t}\n\t\tif (list.error != Ui::PreparedList::Error::None\n\t\t\t|| list.files.empty()) {\n\t\t\treturn false;\n\t\t}\n\t\tauto &file = list.files.front();\n\t\tif (file.type == Ui::PreparedFile::Type::Photo) {\n\t\t\tstartPreparedPhotoUpload(media, std::move(file));\n\t\t} else if (file.type == Ui::PreparedFile::Type::Video) {\n\t\t\tstartPreparedVideoUpload(media, std::move(file));\n\t\t} else {\n\t\t\tstartPreparedDocumentUpload(media, std::move(file));\n\t\t}\n\t\treturn true;\n\t});\n\tconst auto validateFile = ValidateFn(ValidateFileDragData);\n\tconst auto choosePhotoOrVideo = [=](\n\t\t\tstd::shared_ptr<PollMediaState> media) {\n\t\tconst auto callback = crl::guard(this, [=](\n\t\t\t\tFileDialog::OpenResult &&result) {\n\t\t\tconst auto checkResult = [&](const Ui::PreparedList &list) {\n\t\t\t\tusing namespace Ui;\n\t\t\t\tif (list.files.size() != 1) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst auto type = list.files.front().type;\n\t\t\t\treturn (type == PreparedFile::Type::Photo)\n\t\t\t\t\t|| (type == PreparedFile::Type::Video);\n\t\t\t};\n\t\t\tconst auto showError = [=](tr::phrase<> text) {\n\t\t\t\tshowToast(text(tr::now));\n\t\t\t};\n\t\t\tauto list = Storage::PreparedFileFromFilesDialog(\n\t\t\t\tstd::move(result),\n\t\t\t\tcheckResult,\n\t\t\t\tshowError,\n\t\t\t\tst::sendMediaPreviewSize,\n\t\t\t\t_controller->session().premium());\n\t\t\tif (!list) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto &file = list->files.front();\n\t\t\tif (file.type == Ui::PreparedFile::Type::Photo) {\n\t\t\t\tapplyPreparedPhotoList(media, std::move(*list));\n\t\t\t} else {\n\t\t\t\tstartPreparedVideoUpload(media, std::move(file));\n\t\t\t}\n\t\t});\n\t\tFileDialog::GetOpenPath(\n\t\t\tthis,\n\t\t\ttr::lng_attach_photo_or_video(tr::now),\n\t\t\tFileDialog::PhotoVideoFilesFilter(),\n\t\t\tcallback);\n\t};\n\tconst auto chooseDocument = [=](std::shared_ptr<PollMediaState> media) {\n\t\tconst auto callback = crl::guard(this, [=](\n\t\t\t\tFileDialog::OpenResult &&result) {\n\t\t\tif (result.paths.isEmpty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto list = Storage::PrepareMediaList(\n\t\t\t\tresult.paths.mid(0, 1),\n\t\t\t\tst::sendMediaPreviewSize,\n\t\t\t\t_controller->session().premium());\n\t\t\tif (list.error == Ui::PreparedList::Error::TooLargeFile) {\n\t\t\t\tconst auto fileSize = list.files.empty()\n\t\t\t\t\t? 0\n\t\t\t\t\t: list.files.front().size;\n\t\t\t\t_controller->show(Box(\n\t\t\t\t\tFileSizeLimitBox,\n\t\t\t\t\t&_controller->session(),\n\t\t\t\t\tfileSize,\n\t\t\t\t\tnullptr));\n\t\t\t\treturn;\n\t\t\t} else if (list.error != Ui::PreparedList::Error::None\n\t\t\t\t|| list.files.empty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstartPreparedDocumentUpload(\n\t\t\t\tmedia,\n\t\t\t\tstd::move(list.files.front()));\n\t\t});\n\t\tFileDialog::GetOpenPath(\n\t\t\tthis,\n\t\t\ttr::lng_attach_file(tr::now),\n\t\t\tFileDialog::AllFilesFilter(),\n\t\t\tcallback);\n\t};\n\tconst auto clearMedia = [=](std::shared_ptr<PollMediaState> media) {\n\t\tauto toCancel = std::vector<FullMsgId>();\n\t\tfor (auto i = state->uploads.begin(); i != state->uploads.end();) {\n\t\t\tif (i->second.media.lock() == media) {\n\t\t\t\ttoCancel.push_back(i->first);\n\t\t\t\ti = state->uploads.erase(i);\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t\tfor (const auto &id : toCancel) {\n\t\t\t_controller->session().uploader().cancel(id);\n\t\t}\n\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t};\n\tconst auto chooseLocation = [=](\n\t\t\tstd::shared_ptr<PollMediaState> media) {\n\t\tconst auto session = &_controller->session();\n\t\tconst auto &appConfig = session->appConfig();\n\t\tauto map = appConfig.get<base::flat_map<QString, QString>>(\n\t\t\tu\"tdesktop_config_map\"_q,\n\t\t\tbase::flat_map<QString, QString>());\n\t\tconst auto config = Ui::LocationPickerConfig{\n\t\t\t.mapsToken = map[u\"maps\"_q],\n\t\t\t.geoToken = map[u\"geo\"_q],\n\t\t};\n\t\tconst auto applyGeo = [=](float64 lat, float64 lon) {\n\t\t\tconst auto point = Data::LocationPoint(\n\t\t\t\tlat,\n\t\t\t\tlon,\n\t\t\t\tData::LocationPoint::NoAccessHash);\n\t\t\tauto pollMedia = PollMedia();\n\t\t\tpollMedia.geo = point;\n\t\t\tconst auto cloudImage = session->data().location(point);\n\t\t\tauto thumbnail = Ui::MakeGeoThumbnailWithPin(\n\t\t\t\tcloudImage,\n\t\t\t\tsession,\n\t\t\t\tData::FileOrigin());\n\t\t\tsetMedia(media, pollMedia, std::move(thumbnail), true);\n\t\t};\n\t\tif (base::IsCtrlPressed()) {\n\t\t\tconst auto lat = 48.8566 + base::RandomValue<uint32>()\n\t\t\t\t/ float64(std::numeric_limits<uint32>::max()) * 0.02 - 0.01;\n\t\t\tconst auto lon = 2.3522 + base::RandomValue<uint32>()\n\t\t\t\t/ float64(std::numeric_limits<uint32>::max()) * 0.02 - 0.01;\n\t\t\tapplyGeo(lat, lon);\n\t\t\treturn;\n\t\t}\n\t\tif (!Ui::LocationPicker::Available(config)) {\n\t\t\treturn;\n\t\t}\n\t\tUi::LocationPicker::Show({\n\t\t\t.parent = _controller->widget().get(),\n\t\t\t.config = config,\n\t\t\t.chooseLabel = tr::lng_maps_point_send(),\n\t\t\t.session = session,\n\t\t\t.callback = crl::guard(this, [=](Data::InputVenue venue) {\n\t\t\t\tapplyGeo(venue.lat, venue.lon);\n\t\t\t}),\n\t\t\t.quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); },\n\t\t\t.storageId = session->local().resolveStorageIdBots(),\n\t\t\t.closeRequests = _controller->content()->death(),\n\t\t});\n\t};\n\tconst auto showMediaMenu = [=](\n\t\t\tnot_null<Ui::RpWidget*> button,\n\t\t\tstd::shared_ptr<PollMediaState> media,\n\t\t\tbool allowDocuments = false,\n\t\t\tbool allowStickers = true) {\n\t\tif (HistoryView::ShowPollMediaPreview(_controller, media, {\n\t\t\t.choosePhotoOrVideo = [=] { choosePhotoOrVideo(media); },\n\t\t\t.chooseDocument = [=] { chooseDocument(media); },\n\t\t\t.chooseSticker = [=] {\n\t\t\t\tshowStickerPanel(button, media);\n\t\t\t},\n\t\t\t.editPhoto = crl::guard(this, [=](Ui::PreparedList list) {\n\t\t\t\tapplyPreparedPhotoList(media, std::move(list));\n\t\t\t}),\n\t\t\t.remove = [=] { clearMedia(media); },\n\t\t})) {\n\t\t\treturn;\n\t\t}\n\t\tstate->mediaMenu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tbutton,\n\t\t\tst::popupMenuWithIcons);\n\t\tstate->mediaMenu->setForcedOrigin(\n\t\t\tUi::PanelAnimation::Origin::TopRight);\n\t\tstate->mediaMenu->addAction(\n\t\t\ttr::lng_attach_photo_or_video(tr::now),\n\t\t\t[=] { choosePhotoOrVideo(media); },\n\t\t\t&st::menuIconPhoto);\n\t\tif (allowDocuments) {\n\t\t\tstate->mediaMenu->addAction(\n\t\t\t\ttr::lng_attach_file(tr::now),\n\t\t\t\t[=] { chooseDocument(media); },\n\t\t\t\t&st::menuIconFile);\n\t\t}\n\t\t{\n\t\t\tconst auto &appConfig = _controller->session().appConfig();\n\t\t\tauto map = appConfig.get<base::flat_map<QString, QString>>(\n\t\t\t\tu\"tdesktop_config_map\"_q,\n\t\t\t\tbase::flat_map<QString, QString>());\n\t\t\tconst auto config = Ui::LocationPickerConfig{\n\t\t\t\t.mapsToken = map[u\"maps\"_q],\n\t\t\t\t.geoToken = map[u\"geo\"_q],\n\t\t\t};\n\t\t\tif (Ui::LocationPicker::Available(config)) {\n\t\t\t\tstate->mediaMenu->addAction(\n\t\t\t\t\ttr::lng_maps_point(tr::now),\n\t\t\t\t\t[=] { chooseLocation(media); },\n\t\t\t\t\t&st::menuIconAddress);\n\t\t\t}\n\t\t}\n\t\tif (allowStickers) {\n\t\t\tstate->mediaMenu->addAction(\n\t\t\t\ttr::lng_chat_intro_choose_sticker(tr::now),\n\t\t\t\t[=] { showStickerPanel(button, media); },\n\t\t\t\t&st::menuIconStickers);\n\t\t}\n\t\tif (media->media || media->uploading) {\n\t\t\tstate->mediaMenu->addAction(\n\t\t\t\ttr::lng_box_remove(tr::now),\n\t\t\t\t[=] { clearMedia(media); },\n\t\t\t\t&st::menuIconDelete);\n\t\t}\n\t\tstate->mediaMenu->popup(QCursor::pos());\n\t};\n\tconst auto addMediaButton = [=](\n\t\t\tnot_null<Ui::InputField*> field,\n\t\t\tstd::shared_ptr<PollMediaState> media) {\n\t\tconst auto button = Ui::CreateChild<PollMediaButton>(\n\t\t\tfield,\n\t\t\tst::pollAttach,\n\t\t\tmedia);\n\t\tbutton->show();\n\t\tinstallDropToField(field, media, validateFile, applyFileDrop);\n\t\tinstallDropToWidget(button, media, validateFile, applyFileDrop);\n\t\tfield->sizeValue(\n\t\t) | rpl::on_next([=](QSize size) {\n\t\t\tbutton->moveToRight(\n\t\t\t\tst::createPollAttachPosition.x(),\n\t\t\t\tst::createPollAttachPosition.y(),\n\t\t\t\tsize.width());\n\t\t}, button->lifetime());\n\t\tbutton->clicks(\n\t\t) | rpl::on_next([=](Qt::MouseButton buttonType) {\n\t\t\tif (buttonType != Qt::LeftButton) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tshowMediaMenu(button, media, true, false);\n\t\t}, button->lifetime());\n\t};\n\n\tconst auto question = setupQuestion(container);\n\tconst auto description = setupDescription(container);\n\taddMediaButton(description, state->descriptionMedia);\n\tUi::AddDivider(container);\n\tUi::AddSkip(container);\n\tcontainer->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_polls_create_options(),\n\t\t\tst::defaultSubsectionTitle),\n\t\tst::createPollFieldTitlePadding);\n\tstate->options = std::make_unique<Options>(\n\t\tthis,\n\t\tcontainer,\n\t\t_controller,\n\t\t_emojiPanel ? _emojiPanel.get() : nullptr,\n\t\t(_chosen & PollData::Flag::Quiz),\n\t\tshowMediaMenu,\n\t\tinstallPhotoDropToField,\n\t\tinstallPhotoDropToWidget);\n\tconst auto options = state->options.get();\n\tauto limit = options->usedCount() | rpl::after_next([=](int count) {\n\t\tsetCloseByEscape(!count);\n\t\tsetCloseByOutsideClick(!count);\n\t}) | rpl::map([=](int count) {\n\t\tconst auto appConfig = &_controller->session().appConfig();\n\t\tconst auto max = appConfig->pollOptionsLimit();\n\t\treturn (count < max)\n\t\t\t? tr::lng_polls_create_limit(tr::now, lt_count, max - count)\n\t\t\t: tr::lng_polls_create_maximum(tr::now);\n\t}) | rpl::after_next([=] {\n\t\tcontainer->resizeToWidth(container->widthNoMargins());\n\t});\n\tcontainer->add(\n\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\tstd::move(limit),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::createPollLimitPadding));\n\n\tquestion->tabbed(\n\t) | rpl::on_next([=](not_null<bool*> handled) {\n\t\tdescription->setFocus();\n\t\t*handled = true;\n\t}, question->lifetime());\n\n\tdescription->tabbed(\n\t) | rpl::on_next([=](not_null<bool*> handled) {\n\t\toptions->focusFirst();\n\t\t*handled = true;\n\t}, description->lifetime());\n\n\tUi::AddSkip(container);\n\tUi::AddSubsectionTitle(container, tr::lng_polls_create_settings());\n\n\tconst auto showWhoVoted = (!(_disabled & PollData::Flag::PublicVotes))\n\t\t? AddPollToggleButton(\n\t\t\tcontainer,\n\t\t\ttr::lng_polls_create_show_who_voted(),\n\t\t\ttr::lng_polls_create_show_who_voted_about(),\n\t\t\t{\n\t\t\t\t.icon = &st::pollBoxFilledPollViewIcon,\n\t\t\t\t.background = &st::settingsIconBg4,\n\t\t\t},\n\t\t\trpl::single(!!(_chosen & PollData::Flag::PublicVotes))\n\t\t\t\t| rpl::then(state->showWhoVotedForceOn.events()),\n\t\t\tst::detailedSettingsButtonStyle).get()\n\t\t: nullptr;\n\tconst auto multiple = AddPollToggleButton(\n\t\tcontainer,\n\t\ttr::lng_polls_create_allow_multiple_answers(),\n\t\ttr::lng_polls_create_allow_multiple_answers_about(),\n\t\t{\n\t\t\t.icon = &st::pollBoxFilledPollMultipleIcon,\n\t\t\t.background = &st::settingsIconBg3,\n\t\t},\n\t\trpl::single(!!(_chosen & PollData::Flag::MultiChoice))\n\t\t\t| rpl::then(state->multipleForceOff.events()),\n\t\tst::detailedSettingsButtonStyle);\n\tconst auto addOptions = (!(_disabled & PollData::Flag::OpenAnswers))\n\t\t? AddPollToggleButton(\n\t\t\tcontainer,\n\t\t\ttr::lng_polls_create_allow_adding_options(),\n\t\t\ttr::lng_polls_create_allow_adding_options_about(),\n\t\t\t{\n\t\t\t\t.icon = &st::pollBoxFilledPollAddIcon,\n\t\t\t\t.background = &st::settingsIconBg4,\n\t\t\t},\n\t\t\trpl::single(!!(_chosen & PollData::Flag::OpenAnswers))\n\t\t\t\t| rpl::then(state->addOptionsForceOff.events()),\n\t\t\tst::detailedSettingsButtonStyle).get()\n\t\t: nullptr;\n\tconst auto revoting = AddPollToggleButton(\n\t\tcontainer,\n\t\ttr::lng_polls_create_allow_revoting(),\n\t\ttr::lng_polls_create_allow_revoting_about(),\n\t\t{\n\t\t\t.icon = &st::pollBoxFilledPollRevoteIcon,\n\t\t\t.background = &st::settingsIconBg6,\n\t\t},\n\t\trpl::single(!(_chosen & PollData::Flag::RevotingDisabled))\n\t\t\t| rpl::then(state->revotingForceOff.events()),\n\t\tst::detailedSettingsButtonStyle);\n\tconst auto shuffle = AddPollToggleButton(\n\t\tcontainer,\n\t\ttr::lng_polls_create_shuffle_options(),\n\t\ttr::lng_polls_create_shuffle_options_about(),\n\t\t{\n\t\t\t.icon = &st::pollBoxFilledPollShuffleIcon,\n\t\t\t.background = &st::settingsIconBg8,\n\t\t},\n\t\trpl::single(!!(_chosen & PollData::Flag::ShuffleAnswers)),\n\t\tst::detailedSettingsButtonStyle);\n\tconst auto quiz = AddPollToggleButton(\n\t\tcontainer,\n\t\ttr::lng_polls_create_set_correct_answer(),\n\t\trpl::single(multiple->toggled()) | rpl::then(\n\t\t\tmultiple->toggledChanges()\n\t\t) | rpl::map([](bool multi) {\n\t\t\treturn multi\n\t\t\t\t? tr::lng_polls_create_set_correct_answer_about_multi(\n\t\t\t\t\ttr::now)\n\t\t\t\t: tr::lng_polls_create_set_correct_answer_about(tr::now);\n\t\t}),\n\t\t{\n\t\t\t.icon = &st::pollBoxFilledPollCorrectIcon,\n\t\t\t.background = &st::settingsIconBg2,\n\t\t},\n\t\trpl::single(!!(_chosen & PollData::Flag::Quiz))\n\t\t\t| rpl::then(state->quizForceOff.events()),\n\t\tst::detailedSettingsButtonStyle);\n\n\tconst auto duration = AddPollToggleButton(\n\t\tcontainer,\n\t\ttr::lng_polls_create_limit_duration(),\n\t\ttr::lng_polls_create_limit_duration_about(),\n\t\t{\n\t\t\t.icon = &st::pollBoxFilledPollDeadlineIcon,\n\t\t\t.background = &st::settingsIconBg1,\n\t\t},\n\t\trpl::single(false),\n\t\tst::detailedSettingsButtonStyle);\n\n\tconst auto durationWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container))\n\t)->toggleOn(\n\t\trpl::single(duration->toggled())\n\t\t\t| rpl::then(duration->toggledChanges()));\n\tconst auto durationInner = durationWrap->entity();\n\n\tauto pollEndsLabelText = state->closePeriod.value(\n\t) | rpl::map([=](int) {\n\t\tconst auto date = state->closeDate.current();\n\t\tif (date > 0) {\n\t\t\treturn langDateTime(base::unixtime::parse(date));\n\t\t}\n\t\tconst auto period = state->closePeriod.current();\n\t\tif (period > 0) {\n\t\t\tconst auto target = base::unixtime::now() + period;\n\t\t\treturn langDateTime(base::unixtime::parse(target));\n\t\t}\n\t\treturn QString();\n\t});\n\tstate->closeDate.value(\n\t) | rpl::on_next([=](TimeId) {\n\t\tstate->closePeriod.force_assign(state->closePeriod.current());\n\t}, durationInner->lifetime());\n\n\tconst auto pollEndsLabel = AddButtonWithLabel(\n\t\tdurationInner,\n\t\ttr::lng_polls_create_poll_ends(),\n\t\tstd::move(pollEndsLabelText),\n\t\tst::settingsButtonNoIcon);\n\n\tconst auto show = uiShow();\n\tpollEndsLabel->setClickedCallback([=] {\n\t\tstate->durationMenu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tpollEndsLabel,\n\t\t\tst::popupMenuWithIcons);\n\t\tconst auto &menuSt = state->durationMenu->st().menu;\n\t\tconst auto presets = {\n\t\t\t3600,\n\t\t\t3 * 3600,\n\t\t\t8 * 3600,\n\t\t\t24 * 3600,\n\t\t\t72 * 3600,\n\t\t};\n\t\tfor (const auto seconds : presets) {\n\t\t\tconst auto text = Ui::FormatMuteFor(seconds);\n\t\t\tauto item = base::make_unique_q<DurationIconAction>(\n\t\t\t\tstate->durationMenu->menu(),\n\t\t\t\tmenuSt,\n\t\t\t\tUi::Menu::CreateAction(\n\t\t\t\t\tstate->durationMenu->menu().get(),\n\t\t\t\t\ttext,\n\t\t\t\t\t[=] {\n\t\t\t\t\t\tstate->closePeriod = seconds;\n\t\t\t\t\t\tstate->closeDate = TimeId(0);\n\t\t\t\t\t}),\n\t\t\t\tUi::FormatTTLTiny(seconds));\n\t\t\tstate->durationMenu->addAction(std::move(item));\n\t\t}\n\t\tstate->durationMenu->addAction(\n\t\t\ttr::lng_polls_create_duration_custom(tr::now),\n\t\t\t[=] {\n\t\t\t\tconst auto now = base::unixtime::now();\n\t\t\t\tconst auto current = (state->closeDate.current() > now)\n\t\t\t\t\t? state->closeDate.current()\n\t\t\t\t\t: (state->closePeriod.current() > 0)\n\t\t\t\t\t? (now + state->closePeriod.current())\n\t\t\t\t\t: (now + 24 * 3600);\n\t\t\t\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\t\tUi::ChooseDateTimeBox(box, {\n\t\t\t\t\t\t.title = tr::lng_polls_create_deadline_title(),\n\t\t\t\t\t\t.submit = tr::lng_polls_create_deadline_button(),\n\t\t\t\t\t\t.done = [=](TimeId time) {\n\t\t\t\t\t\t\tstate->closeDate = time;\n\t\t\t\t\t\t\tstate->closePeriod = 0;\n\t\t\t\t\t\t\tbox->closeBox();\n\t\t\t\t\t\t},\n\t\t\t\t\t\t.min = [=] { return base::unixtime::now() + 60; },\n\t\t\t\t\t\t.time = current,\n\t\t\t\t\t\t.max = [=] {\n\t\t\t\t\t\t\treturn base::unixtime::now() + 365 * 24 * 3600;\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}));\n\t\t\t},\n\t\t\t&st::menuIconCustomize);\n\t\tstate->durationMenu->popup(QCursor::pos());\n\t});\n\n\tduration->toggledChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tif (checked && state->closePeriod.current() == 0\n\t\t\t\t&& state->closeDate.current() == 0) {\n\t\t\tstate->closePeriod = 24 * 3600;\n\t\t}\n\t}, duration->lifetime());\n\n\tconst auto hideResults = durationInner->add(\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\tdurationInner,\n\t\t\ttr::lng_polls_create_hide_results(),\n\t\t\tst::settingsButtonNoIcon)\n\t)->toggleOn(rpl::single(false));\n\n\tUi::AddSkip(durationInner);\n\tUi::AddDividerText(\n\t\tdurationInner,\n\t\ttr::lng_polls_create_hide_results_about());\n\n\tconst auto solution = setupSolution(\n\t\tcontainer,\n\t\trpl::single(quiz->toggled()) | rpl::then(quiz->toggledChanges()));\n\taddMediaButton(solution, state->solutionMedia);\n\n\toptions->tabbed(\n\t) | rpl::on_next([=] {\n\t\tif (quiz->toggled()) {\n\t\t\tsolution->setFocus();\n\t\t} else {\n\t\t\tquestion->setFocus();\n\t\t}\n\t}, question->lifetime());\n\n\tsolution->tabbed(\n\t) | rpl::on_next([=](not_null<bool*> handled) {\n\t\tquestion->setFocus();\n\t\t*handled = true;\n\t}, solution->lifetime());\n\n\tconst auto updateAddOptionsLocked = [=] {\n\t\tif (addOptions) {\n\t\t\tconst auto locked = (_disabled & PollData::Flag::OpenAnswers)\n\t\t\t\t|| quiz->toggled()\n\t\t\t\t|| (showWhoVoted && !showWhoVoted->toggled());\n\t\t\taddOptions->setToggleLocked(locked);\n\t\t\tif (locked) {\n\t\t\t\tstate->addOptionsForceOff.fire(false);\n\t\t\t}\n\t\t}\n\t};\n\tconst auto updateQuizDependentLocks = [=](bool checked) {\n\t\tupdateAddOptionsLocked();\n\t\trevoting->setToggleLocked(\n\t\t\t_disabled & PollData::Flag::RevotingDisabled);\n\t};\n\tquiz->setToggleLocked(_disabled & PollData::Flag::Quiz);\n\tshuffle->setToggleLocked(_disabled & PollData::Flag::ShuffleAnswers);\n\tupdateQuizDependentLocks(quiz->toggled());\n\n\tusing namespace rpl::mappers;\n\tquiz->toggledChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tif (checked && (_disabled & PollData::Flag::Quiz)) {\n\t\t\tstate->quizForceOff.fire(false);\n\t\t\treturn;\n\t\t}\n\t\tif (checked) {\n\t\t\tstate->addOptionsForceOff.fire(false);\n\t\t\tstate->revotingForceOff.fire(false);\n\t\t\tsolution->setFocus();\n\t\t}\n\t\tupdateQuizDependentLocks(checked);\n\t\toptions->enableChooseCorrect(checked, multiple->toggled());\n\t}, quiz->lifetime());\n\n\tmultiple->toggledChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tif (quiz->toggled()) {\n\t\t\toptions->enableChooseCorrect(true, checked);\n\t\t}\n\t}, multiple->lifetime());\n\n\tif (addOptions && showWhoVoted) {\n\t\tupdateAddOptionsLocked();\n\t\tshowWhoVoted->toggledChanges(\n\t\t) | rpl::on_next([=](bool) {\n\t\t\tupdateAddOptionsLocked();\n\t\t}, showWhoVoted->lifetime());\n\t}\n\n\tconst auto isValidQuestion = [=] {\n\t\tconst auto text = question->getLastText().trimmed();\n\t\treturn !text.isEmpty() && (text.size() <= kQuestionLimit);\n\t};\n\tquestion->submits(\n\t) | rpl::on_next([=] {\n\t\tif (isValidQuestion()) {\n\t\t\tdescription->setFocus();\n\t\t}\n\t}, question->lifetime());\n\n\tdescription->submits(\n\t) | rpl::on_next([=] {\n\t\toptions->focusFirst();\n\t}, description->lifetime());\n\n\t_setInnerFocus = [=] {\n\t\tquestion->setFocusFast();\n\t};\n\n\tconst auto collectResult = [=] {\n\t\tconst auto textWithTags = question->getTextWithAppliedMarkdown();\n\t\tconst auto descriptionWithTags = description->getTextWithTags();\n\t\tusing Flag = PollData::Flag;\n\t\tauto result = PollData(&_controller->session().data(), id);\n\t\tresult.question.text = textWithTags.text;\n\t\tresult.question.entities = TextUtilities::ConvertTextTagsToEntities(\n\t\t\ttextWithTags.tags);\n\t\tTextUtilities::Trim(result.question);\n\t\tresult.answers = options->toPollAnswers();\n\t\tconst auto solutionWithTags = quiz->toggled()\n\t\t\t? solution->getTextWithAppliedMarkdown()\n\t\t\t: TextWithTags();\n\t\tresult.solution = TextWithEntities{\n\t\t\tsolutionWithTags.text,\n\t\t\tTextUtilities::ConvertTextTagsToEntities(solutionWithTags.tags)\n\t\t};\n\t\tresult.attachedMedia = state->descriptionMedia->media;\n\t\tif (quiz->toggled()) {\n\t\t\tresult.solutionMedia = state->solutionMedia->media;\n\t\t}\n\t\tif (duration->toggled()) {\n\t\t\tconst auto closeDate = state->closeDate.current();\n\t\t\tconst auto closePeriod = state->closePeriod.current();\n\t\t\tif (closeDate > 0) {\n\t\t\t\tresult.closeDate = closeDate;\n\t\t\t\tresult.closePeriod = closeDate - base::unixtime::now();\n\t\t\t} else if (closePeriod > 0) {\n\t\t\t\tresult.closePeriod = closePeriod;\n\t\t\t\tresult.closeDate = base::unixtime::now() + closePeriod;\n\t\t\t}\n\t\t}\n\t\tconst auto publicVotes = (showWhoVoted && showWhoVoted->toggled());\n\t\tconst auto multiChoice = multiple->toggled();\n\t\tconst auto hideResultsEnabled = duration->toggled()\n\t\t\t&& hideResults->toggled();\n\t\tresult.setFlags(Flag(0)\n\t\t\t| (publicVotes ? Flag::PublicVotes : Flag(0))\n\t\t\t| (multiChoice ? Flag::MultiChoice : Flag(0))\n\t\t\t| ((addOptions && addOptions->toggled()) ? Flag::OpenAnswers : Flag(0))\n\t\t\t| (!revoting->toggled() ? Flag::RevotingDisabled : Flag(0))\n\t\t\t| (shuffle->toggled() ? Flag::ShuffleAnswers : Flag(0))\n\t\t\t| (quiz->toggled() ? Flag::Quiz : Flag(0))\n\t\t\t| (hideResultsEnabled\n\t\t\t\t? Flag::HideResultsUntilClose\n\t\t\t\t: Flag(0)));\n\t\tauto text = TextWithEntities{\n\t\t\tdescriptionWithTags.text,\n\t\t\tTextUtilities::ConvertTextTagsToEntities(\n\t\t\t\tdescriptionWithTags.tags),\n\t\t};\n\t\tTextUtilities::Trim(text);\n\t\treturn Result{\n\t\t\tstd::move(result),\n\t\t\tstd::move(text),\n\t\t\tApi::SendOptions(),\n\t\t};\n\t};\n\tconst auto collectError = [=] {\n\t\tif (isValidQuestion()) {\n\t\t\tstate->error &= ~Error::Question;\n\t\t} else {\n\t\t\tstate->error |= Error::Question;\n\t\t}\n\t\tif (!options->hasOptions()) {\n\t\t\tstate->error |= Error::Options;\n\t\t} else if (!options->isValid()) {\n\t\t\tstate->error |= Error::Other;\n\t\t} else {\n\t\t\tstate->error &= ~(Error::Options | Error::Other);\n\t\t}\n\t\tif (quiz->toggled() && !options->hasCorrect()) {\n\t\t\tstate->error |= Error::Correct;\n\t\t} else {\n\t\t\tstate->error &= ~Error::Correct;\n\t\t}\n\t\tif (quiz->toggled()\n\t\t\t&& solution->getLastText().trimmed().size() > kSolutionLimit) {\n\t\t\tstate->error |= Error::Solution;\n\t\t} else {\n\t\t\tstate->error &= ~Error::Solution;\n\t\t}\n\t\tif (state->descriptionMedia->uploading\n\t\t\t|| (quiz->toggled() && state->solutionMedia->uploading)\n\t\t\t|| options->hasUploadingMedia()) {\n\t\t\tstate->error |= Error::Media;\n\t\t} else {\n\t\t\tstate->error &= ~Error::Media;\n\t\t}\n\t\tif (duration->toggled()) {\n\t\t\tconst auto now = base::unixtime::now();\n\t\t\tconst auto closeDate = state->closeDate.current();\n\t\t\tconst auto closePeriod = state->closePeriod.current();\n\t\t\tconst auto deadline = (closeDate > 0)\n\t\t\t\t? closeDate\n\t\t\t\t: (closePeriod > 0)\n\t\t\t\t? (now + closePeriod)\n\t\t\t\t: 0;\n\t\t\tif (deadline > 0 && deadline <= now) {\n\t\t\t\tstate->error |= Error::Deadline;\n\t\t\t} else {\n\t\t\t\tstate->error &= ~Error::Deadline;\n\t\t\t}\n\t\t} else {\n\t\t\tstate->error &= ~Error::Deadline;\n\t\t}\n\t};\n\tconst auto showError = [show = uiShow()](\n\t\t\ttr::phrase<> text) {\n\t\tshow->showToast(text(tr::now));\n\t};\n\t_refreshExpiredMedia = [=] {\n\t\tconst auto forceRefresh = [](\n\t\t\t\tconst std::shared_ptr<PollMediaState> &m) {\n\t\t\tif (m->media && m->reupload) {\n\t\t\t\tm->reupload();\n\t\t\t}\n\t\t};\n\t\tforceRefresh(state->descriptionMedia);\n\t\tforceRefresh(state->solutionMedia);\n\t\toptions->refreshStaleMedia(0);\n\t};\n\tconst auto send = [=](Api::SendOptions sendOptions) {\n\t\tconst auto kStaleTimeout = kMediaUploadMaxAge;\n\t\tauto refreshedAny = false;\n\t\tconst auto tryRefresh = [&](\n\t\t\t\tconst std::shared_ptr<PollMediaState> &m) {\n\t\t\tif (m->media\n\t\t\t\t&& m->uploadedAt > 0\n\t\t\t\t&& (crl::now() - m->uploadedAt > kStaleTimeout)\n\t\t\t\t&& m->reupload) {\n\t\t\t\tm->reupload();\n\t\t\t\trefreshedAny = true;\n\t\t\t}\n\t\t};\n\t\ttryRefresh(state->descriptionMedia);\n\t\tif (quiz->toggled()) {\n\t\t\ttryRefresh(state->solutionMedia);\n\t\t}\n\t\tif (options->refreshStaleMedia(kStaleTimeout)) {\n\t\t\trefreshedAny = true;\n\t\t}\n\t\tif (refreshedAny) {\n\t\t\tcollectError();\n\t\t\tif (state->error & Error::Media) {\n\t\t\t\tShowMediaUploadingToast();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tcollectError();\n\t\tif (state->error & Error::Question) {\n\t\t\tshowError(tr::lng_polls_choose_question);\n\t\t\tquestion->setFocus();\n\t\t} else if (state->error & Error::Options) {\n\t\t\tshowError(tr::lng_polls_choose_answers);\n\t\t\toptions->focusFirst();\n\t\t} else if (state->error & Error::Correct) {\n\t\t\tshowError(tr::lng_polls_choose_correct);\n\t\t\tscrollToWidget(options->layoutWidget());\n\t\t} else if (state->error & Error::Solution) {\n\t\t\tsolution->showError();\n\t\t} else if (state->error & Error::Media) {\n\t\t\tShowMediaUploadingToast();\n\t\t} else if (state->error & Error::Deadline) {\n\t\t\tshowError(tr::lng_polls_create_deadline_expired);\n\t\t} else if (!state->error) {\n\t\t\tauto result = collectResult();\n\t\t\tresult.options = sendOptions;\n\t\t\t_submitRequests.fire(std::move(result));\n\t\t}\n\t};\n\tconst auto sendAction = SendMenu::DefaultCallback(\n\t\t_controller->uiShow(),\n\t\tcrl::guard(this, send));\n\n\toptions->scrollToWidget(\n\t) | rpl::on_next([=](not_null<QWidget*> widget) {\n\t\tscrollToWidget(widget);\n\t}, lifetime());\n\n\toptions->backspaceInFront(\n\t) | rpl::on_next([=] {\n\t\tFocusAtEnd(description);\n\t}, lifetime());\n\n\tconst auto isNormal = (_sendType == Api::SendType::Normal);\n\tconst auto schedule = [=] {\n\t\tsendAction(\n\t\t\t{ .type = SendMenu::ActionType::Schedule },\n\t\t\t_sendMenuDetails());\n\t};\n\tconst auto submit = addButton(\n\t\ttr::lng_polls_create_button(),\n\t\t[=] { isNormal ? send({}) : schedule(); });\n\tsubmit->setText(PaidSendButtonText(_starsRequired.value(), isNormal\n\t\t? tr::lng_polls_create_button()\n\t\t: tr::lng_schedule_button()));\n\tconst auto sendMenuDetails = [=] {\n\t\tcollectError();\n\t\treturn (state->error) ? SendMenu::Details() : _sendMenuDetails();\n\t};\n\tSendMenu::SetupMenuAndShortcuts(\n\t\tsubmit.data(),\n\t\t_controller->uiShow(),\n\t\tsendMenuDetails,\n\t\tsendAction);\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\n\tif (showWhoVoted) {\n\t\tshowWhoVoted->finishAnimating();\n\t}\n\tmultiple->finishAnimating();\n\tif (addOptions) {\n\t\taddOptions->finishAnimating();\n\t}\n\trevoting->finishAnimating();\n\tshuffle->finishAnimating();\n\tquiz->finishAnimating();\n\tduration->finishAnimating();\n\tdurationWrap->finishAnimating();\n\thideResults->finishAnimating();\n\n\treturn result;\n}\n\nvoid CreatePollBox::prepare() {\n\tsetTitle(tr::lng_polls_create_title());\n\n\tconst auto inner = setInnerWidget(setupContent());\n\n\tsetDimensionsToContent(st::boxWideWidth, inner);\n\n\tUi::SetStickyBottomScroll(this, inner);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/create_poll_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n#include \"api/api_common.h\"\n#include \"data/data_poll.h\"\n#include \"base/flags.h\"\n\nstruct PollData;\n\nnamespace ChatHelpers {\nclass TabbedPanel;\n} // namespace ChatHelpers\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nclass PeerData;\n\nclass CreatePollBox : public Ui::BoxContent {\npublic:\n\tstruct Result {\n\t\tPollData poll;\n\t\tTextWithEntities text;\n\t\tApi::SendOptions options;\n\t};\n\n\tCreatePollBox(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tPollData::Flags chosen,\n\t\tPollData::Flags disabled,\n\t\trpl::producer<int> starsRequired,\n\t\tApi::SendType sendType,\n\t\tSendMenu::Details sendMenuDetails);\n\n\t[[nodiscard]] rpl::producer<Result> submitRequests() const;\n\tvoid submitFailed(const QString &error);\n\tvoid submitMediaExpired();\n\n\tvoid setInnerFocus() override;\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\tenum class Error {\n\t\tQuestion = 0x01,\n\t\tOptions  = 0x02,\n\t\tCorrect  = 0x04,\n\t\tOther    = 0x08,\n\t\tSolution = 0x10,\n\t\tMedia    = 0x20,\n\t\tDeadline = 0x40,\n\t};\n\tfriend constexpr inline bool is_flag_type(Error) { return true; }\n\tusing Errors = base::flags<Error>;\n\n\t[[nodiscard]] object_ptr<Ui::RpWidget> setupContent();\n\t[[nodiscard]] not_null<Ui::InputField*> setupQuestion(\n\t\tnot_null<Ui::VerticalLayout*> container);\n\t[[nodiscard]] not_null<Ui::InputField*> setupDescription(\n\t\tnot_null<Ui::VerticalLayout*> container);\n\t[[nodiscard]] not_null<Ui::InputField*> setupSolution(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<bool> shown);\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<PeerData*> _peer;\n\tconst PollData::Flags _chosen = PollData::Flags();\n\tconst PollData::Flags _disabled = PollData::Flags();\n\tconst Api::SendType _sendType = Api::SendType();\n\tconst Fn<SendMenu::Details()> _sendMenuDetails;\n\trpl::variable<int> _starsRequired;\n\tbase::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;\n\tFn<void()> _setInnerFocus;\n\tFn<void()> _refreshExpiredMedia;\n\tFn<rpl::producer<bool>()> _dataIsValidValue;\n\trpl::event_stream<Result> _submitRequests;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/delete_messages_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/delete_messages_box.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_chat_participants.h\"\n#include \"api/api_messages_search.h\"\n#include \"api/api_report.h\"\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_ttl_validator.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/rect.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace {\n\nconstexpr auto kDeleteMessagesBoxAnimationDuration = crl::time(80);\n\n} // namespace\n\nDeleteMessagesBox::DeleteMessagesBox(\n\tQWidget*,\n\tnot_null<HistoryItem*> item,\n\tbool suggestModerateActions)\n: _session(&item->history()->session())\n, _ids(1, item->fullId()) {\n\tif (suggestModerateActions) {\n\t\t_moderateBan = item->suggestBanReport();\n\t\t_moderateDeleteAll = item->suggestDeleteAllReport();\n\t\tif (_moderateBan || _moderateDeleteAll) {\n\t\t\t_moderateFrom = item->from();\n\t\t\t_moderateInChannel = item->history()->peer->asChannel();\n\t\t}\n\t}\n}\n\nDeleteMessagesBox::DeleteMessagesBox(\n\tQWidget*,\n\tnot_null<Main::Session*> session,\n\tMessageIdsList &&selected)\n: _session(session)\n, _ids(std::move(selected)) {\n\tExpects(!_ids.empty());\n}\n\nDeleteMessagesBox::DeleteMessagesBox(\n\tQWidget*,\n\tnot_null<PeerData*> peer,\n\tQDate firstDayToDelete,\n\tQDate lastDayToDelete)\n: _session(&peer->session())\n, _wipeHistoryPeer(peer)\n, _wipeHistoryJustClear(true)\n, _wipeHistoryFirstToDelete(firstDayToDelete)\n, _wipeHistoryLastToDelete(lastDayToDelete) {\n}\n\nDeleteMessagesBox::DeleteMessagesBox(\n\tQWidget*,\n\tnot_null<PeerData*> peer,\n\tbool justClear)\n: _session(&peer->session())\n, _wipeHistoryPeer(peer)\n, _wipeHistoryJustClear(justClear) {\n}\n\ncrl::time DeleteMessagesBox::layerAnimationDuration() const {\n\treturn kDeleteMessagesBoxAnimationDuration;\n}\n\nvoid DeleteMessagesBox::prepare() {\n\tauto details = TextWithEntities();\n\tconst auto appendDetails = [&](TextWithEntities &&text) {\n\t\tdetails.append(u\"\\n\\n\"_q).append(std::move(text));\n\t};\n\tauto deleteText = lifetime().make_state<rpl::variable<QString>>();\n\t*deleteText = tr::lng_box_delete();\n\tauto deleteStyle = &st::defaultBoxButton;\n\tauto canDelete = true;\n\tif (const auto peer = _wipeHistoryPeer) {\n\t\tif (!_wipeHistoryFirstToDelete.isNull()) {\n\t\t\tdetails = (_wipeHistoryFirstToDelete\n\t\t\t\t== _wipeHistoryLastToDelete)\n\t\t\t\t? tr::lng_sure_delete_by_date_one(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_date,\n\t\t\t\t\tTextWithEntities{\n\t\t\t\t\t\tlangDayOfMonthFull(_wipeHistoryFirstToDelete) },\n\t\t\t\t\ttr::rich)\n\t\t\t\t: tr::lng_sure_delete_by_date_many(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_days,\n\t\t\t\t\ttr::lng_sure_delete_selected_days(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t_wipeHistoryFirstToDelete.daysTo(\n\t\t\t\t\t\t\t_wipeHistoryLastToDelete) + 1,\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\ttr::rich);\n\t\t\tdeleteStyle = &st::attentionBoxButton;\n\t\t} else if (_wipeHistoryJustClear) {\n\t\t\tconst auto isChannel = peer->isChannel() && !peer->isMegagroup();\n\t\t\t_revokeJustClearForChannel = isChannel;\n\t\t\tdetails.text = isChannel\n\t\t\t\t? tr::lng_sure_delete_channel_history(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_channel,\n\t\t\t\t\tpeer->name())\n\t\t\t\t: peer->isSelf()\n\t\t\t\t? tr::lng_sure_delete_saved_messages(tr::now)\n\t\t\t\t: peer->isUser()\n\t\t\t\t? tr::lng_sure_delete_history(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_contact,\n\t\t\t\t\tpeer->name())\n\t\t\t\t: tr::lng_sure_delete_group_history(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_group,\n\t\t\t\t\tpeer->name());\n\t\t\tdetails = tr::rich(details.text);\n\t\t\tdeleteStyle = &st::attentionBoxButton;\n\t\t} else {\n\t\t\tdetails.text = peer->isSelf()\n\t\t\t\t? tr::lng_sure_delete_saved_messages(tr::now)\n\t\t\t\t: peer->isUser()\n\t\t\t\t? tr::lng_sure_delete_history(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_contact,\n\t\t\t\t\tpeer->name())\n\t\t\t\t: peer->isChat()\n\t\t\t\t? tr::lng_sure_delete_and_exit(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_group,\n\t\t\t\t\tpeer->name())\n\t\t\t\t: peer->isMegagroup()\n\t\t\t\t? tr::lng_sure_leave_group(tr::now)\n\t\t\t\t: tr::lng_sure_leave_channel(tr::now);\n\t\t\tdetails = tr::rich(details.text);\n\t\t\tif (!peer->isUser()) {\n\t\t\t\t*deleteText = tr::lng_box_leave();\n\t\t\t}\n\t\t\tdeleteStyle = &st::attentionBoxButton;\n\t\t}\n\t\tif (_revokeJustClearForChannel) {\n\t\t} else if (auto revoke = revokeText(peer)) {\n\t\t\t_revoke.create(\n\t\t\t\tthis,\n\t\t\t\trevoke->checkbox,\n\t\t\t\ttrue,\n\t\t\t\tst::defaultBoxCheckbox);\n\t\t\tappendDetails(std::move(revoke->description));\n\t\t\tif (!peer->isUser() && !_wipeHistoryJustClear) {\n\t\t\t\t_revoke->checkedValue(\n\t\t\t\t) | rpl::on_next([=](bool revokeForAll) {\n\t\t\t\t\t*deleteText = revokeForAll\n\t\t\t\t\t\t? tr::lng_box_delete()\n\t\t\t\t\t\t: tr::lng_box_leave();\n\t\t\t\t}, _revoke->lifetime());\n\t\t\t}\n\t\t} else if (canDelete\n\t\t\t&& _wipeHistoryJustClear\n\t\t\t&& (peer->isMegagroup() || peer->isChat())) {\n\t\t\tappendDetails({\n\t\t\t\ttr::lng_delete_clear_for_me(tr::now)\n\t\t\t});\n\t\t}\n\t} else if (_moderateFrom) {\n\t\tAssert(_moderateInChannel != nullptr);\n\n\t\tdetails.text = tr::lng_selected_delete_sure_this(tr::now);\n\t\tif (_moderateBan) {\n\t\t\t_banUser.create(\n\t\t\t\tthis,\n\t\t\t\ttr::lng_ban_user(tr::now),\n\t\t\t\tfalse,\n\t\t\t\tst::defaultBoxCheckbox);\n\t\t}\n\t\t_reportSpam.create(\n\t\t\tthis,\n\t\t\ttr::lng_report_spam(tr::now),\n\t\t\tfalse,\n\t\t\tst::defaultBoxCheckbox);\n\t\tif (_moderateDeleteAll) {\n\t\t\tconst auto search = lifetime().make_state<Api::MessagesSearch>(\n\t\t\t\t_session->data().message(_ids.front())->history());\n\t\t\t_deleteAll.create(\n\t\t\t\tthis,\n\t\t\t\ttr::lng_delete_all_from_user(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::bold(_moderateFrom->name()),\n\t\t\t\t\ttr::marked),\n\t\t\t\tfalse,\n\t\t\t\tst::defaultBoxCheckbox);\n\n\t\t\t*deleteText = rpl::combine(\n\t\t\t\trpl::single(\n\t\t\t\t\t0\n\t\t\t\t) | rpl::then(\n\t\t\t\t\tsearch->messagesFounds(\n\t\t\t\t\t) | rpl::map([](const Api::FoundMessages &found) {\n\t\t\t\t\t\treturn found.total;\n\t\t\t\t\t})\n\t\t\t\t),\n\t\t\t\t_deleteAll->checkedValue()\n\t\t\t) | rpl::map([](int total, bool checked) {\n\t\t\t\treturn tr::lng_box_delete(tr::now)\n\t\t\t\t\t+ ((total <= 0 || !checked)\n\t\t\t\t\t\t? QString()\n\t\t\t\t\t\t: QString(\" (%1)\").arg(total));\n\t\t\t});\n\t\t\tsearch->searchMessages({ .from = _moderateFrom });\n\t\t}\n\t} else {\n\t\tdetails.text = hasSavedMusicMessages()\n\t\t\t? tr::lng_selected_remove_saved_music(tr::now)\n\t\t\t: (_ids.size() == 1)\n\t\t\t? tr::lng_selected_delete_sure_this(tr::now)\n\t\t\t: tr::lng_selected_delete_sure(tr::now, lt_count, _ids.size());\n\t\tif (const auto peer = checkFromSinglePeer()) {\n\t\t\tauto count = int(_ids.size());\n\t\t\tif (hasScheduledMessages() || hasSavedMusicMessages()) {\n\t\t\t} else if (auto revoke = revokeText(peer)) {\n\t\t\t\tconst auto &settings = Core::App().settings();\n\t\t\t\tconst auto revokeByDefault\n\t\t\t\t\t= !settings.rememberedDeleteMessageOnlyForYou();\n\t\t\t\t_revoke.create(\n\t\t\t\t\tthis,\n\t\t\t\t\trevoke->checkbox,\n\t\t\t\t\trevokeByDefault,\n\t\t\t\t\tst::defaultBoxCheckbox);\n\t\t\t\t_revokeRemember.create(\n\t\t\t\t\tthis,\n\t\t\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\t\t\tthis,\n\t\t\t\t\t\ttr::lng_remember(),\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\tst::defaultBoxCheckbox));\n\t\t\t\t_revokeRemember->hide(anim::type::instant);\n\t\t\t\t_revoke->checkedValue(\n\t\t\t\t) | rpl::on_next([=](bool checked) {\n\t\t\t\t\t_revokeRemember->toggle(\n\t\t\t\t\t\tchecked != revokeByDefault,\n\t\t\t\t\t\tanim::type::normal);\n\t\t\t\t}, _revokeRemember->lifetime());\n\t\t\t\t_revokeRemember->heightValue(\n\t\t\t\t) | rpl::on_next([=](int h) {\n\t\t\t\t\tsetDimensions(st::boxWidth, _fullHeight + h);\n\t\t\t\t}, lifetime());\n\t\t\t\tappendDetails(std::move(revoke->description));\n\t\t\t} else if (peer->isChannel()) {\n\t\t\t\tif (peer->isMegagroup()) {\n\t\t\t\t\tappendDetails({\n\t\t\t\t\t\ttr::lng_delete_for_everyone_hint(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\tcount)\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else if (peer->isChat()) {\n\t\t\t\tappendDetails({\n\t\t\t\t\ttr::lng_delete_for_me_chat_hint(tr::now, lt_count, count)\n\t\t\t\t});\n\t\t\t} else if (!peer->isSelf()\n\t\t\t\t&& (!peer->isUser() || !peer->asUser()->isInaccessible())) {\n\t\t\t\tif (const auto user = peer->asUser(); user && user->isBot()) {\n\t\t\t\t\t_revokeForBot = true;\n\t\t\t\t}\n\t\t\t\tappendDetails({\n\t\t\t\t\ttr::lng_delete_for_me_hint(tr::now, lt_count, count)\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\t_text.create(this, rpl::single(std::move(details)), st::boxLabel);\n\t_text->resizeToWidth(st::boxWidth - rect::m::sum::h(st::boxPadding));\n\n\tif (_wipeHistoryJustClear && _wipeHistoryPeer) {\n\t\tconst auto validator = TTLMenu::TTLValidator(\n\t\t\tuiShow(),\n\t\t\t_wipeHistoryPeer);\n\t\tif (validator.can()) {\n\t\t\t_wipeHistoryPeer->updateFull();\n\t\t\t_autoDeleteSettings.create(\n\t\t\t\tthis,\n\t\t\t\t(_wipeHistoryPeer->messagesTTL()\n\t\t\t\t\t? tr::lng_edit_auto_delete_settings(tr::now)\n\t\t\t\t\t: tr::lng_enable_auto_delete(tr::now)),\n\t\t\t\tst::boxLinkButton);\n\t\t\t_autoDeleteSettings->setClickedCallback([=] {\n\t\t\t\tvalidator.showBox();\n\t\t\t});\n\t\t}\n\t}\n\n\tif (canDelete) {\n\t\taddButton(\n\t\t\tdeleteText->value(),\n\t\t\t[=] { deleteAndClear(); },\n\t\t\t*deleteStyle);\n\t\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\t} else {\n\t\taddButton(tr::lng_about_done(), [=] { closeBox(); });\n\t}\n\n\tconst auto &padding = st::boxPadding;\n\trpl::combine(\n\t\twidthValue(),\n\t\t_text->naturalWidthValue()\n\t) | rpl::on_next([=](int full, int) {\n\t\t_text->resizeToNaturalWidth(full - padding.left() - padding.right());\n\n\t\tauto fullHeight = st::boxPadding.top()\n\t\t\t+ _text->height()\n\t\t\t+ st::boxPadding.bottom();\n\t\tif (_moderateFrom) {\n\t\t\tfullHeight += st::boxMediumSkip;\n\t\t\tif (_banUser) {\n\t\t\t\tfullHeight += _banUser->heightNoMargins() + st::boxLittleSkip;\n\t\t\t}\n\t\t\tfullHeight += _reportSpam->heightNoMargins();\n\t\t\tif (_deleteAll) {\n\t\t\t\tfullHeight += st::boxLittleSkip + _deleteAll->heightNoMargins();\n\t\t\t}\n\t\t} else if (_revoke) {\n\t\t\tfullHeight += st::boxMediumSkip + _revoke->heightNoMargins();\n\t\t}\n\t\tif (_autoDeleteSettings) {\n\t\t\tfullHeight += st::boxMediumSkip\n\t\t\t\t+ _autoDeleteSettings->height()\n\t\t\t\t+ st::boxLittleSkip;\n\t\t}\n\t\tsetDimensions(st::boxWidth, fullHeight);\n\t\t_fullHeight = fullHeight;\n\t}, lifetime());\n}\n\nbool DeleteMessagesBox::hasScheduledMessages() const {\n\tfor (const auto &fullId : _ids) {\n\t\tif (const auto item = _session->data().message(fullId)) {\n\t\t\tif (item->isScheduled()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nbool DeleteMessagesBox::hasSavedMusicMessages() const {\n\tfor (const auto &fullId : _ids) {\n\t\tif (const auto item = _session->data().message(fullId)) {\n\t\t\tif (item->isSavedMusicItem()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nPeerData *DeleteMessagesBox::checkFromSinglePeer() const {\n\tauto result = (PeerData*)nullptr;\n\tfor (const auto &fullId : _ids) {\n\t\tif (const auto item = _session->data().message(fullId)) {\n\t\t\tconst auto peer = item->history()->peer;\n\t\t\tif (!result) {\n\t\t\t\tresult = peer;\n\t\t\t} else if (result != peer) {\n\t\t\t\treturn nullptr;\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nauto DeleteMessagesBox::revokeText(not_null<PeerData*> peer) const\n-> std::optional<RevokeConfig> {\n\tauto result = RevokeConfig();\n\tif (peer == _wipeHistoryPeer) {\n\t\tif (!peer->canRevokeFullHistory()) {\n\t\t\treturn std::nullopt;\n\t\t} else if (const auto user = peer->asUser()) {\n\t\t\tresult.checkbox = tr::lng_delete_for_other_check(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\t{ user->firstName },\n\t\t\t\ttr::rich);\n\t\t} else {\n\t\t\tresult.checkbox.text = tr::lng_delete_for_everyone_check(tr::now);\n\t\t}\n\t\treturn result;\n\t}\n\n\tconst auto items = peer->owner().idsToItems(_ids);\n\n\tif (items.size() != _ids.size()) {\n\t\t// We don't have information about all messages.\n\t\treturn std::nullopt;\n\t}\n\n\tconst auto now = base::unixtime::now();\n\tconst auto canRevoke = [&](HistoryItem * item) {\n\t\treturn item->canDeleteForEveryone(now);\n\t};\n\tconst auto cannotRevoke = [&](HistoryItem *item) {\n\t\treturn !item->canDeleteForEveryone(now);\n\t};\n\tconst auto canRevokeAll = ranges::none_of(items, cannotRevoke);\n\tauto outgoing = items | ranges::views::filter(&HistoryItem::out);\n\tconst auto canRevokeOutgoingCount = canRevokeAll\n\t\t? -1\n\t\t: ranges::count_if(outgoing, canRevoke);\n\n\tif (canRevokeAll) {\n\t\tif (const auto user = peer->asUser()) {\n\t\t\tresult.checkbox = tr::lng_delete_for_other_check(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\t{ user->firstName },\n\t\t\t\ttr::rich);\n\t\t} else {\n\t\t\tresult.checkbox.text = tr::lng_delete_for_everyone_check(tr::now);\n\t\t}\n\t\treturn result;\n\t} else if (canRevokeOutgoingCount > 0) {\n\t\tresult.checkbox.text = tr::lng_delete_for_other_my(tr::now);\n\t\tif (const auto user = peer->asUser()) {\n\t\t\tif (canRevokeOutgoingCount == 1) {\n\t\t\t\tresult.description = tr::lng_selected_unsend_about_user_one(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::bold(user->shortName()),\n\t\t\t\t\ttr::marked);\n\t\t\t} else {\n\t\t\t\tresult.description = tr::lng_selected_unsend_about_user(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tcanRevokeOutgoingCount,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::bold(user->shortName()),\n\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t} else if (canRevokeOutgoingCount == 1) {\n\t\t\tresult.description = tr::lng_selected_unsend_about_group_one(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.description = tr::lng_selected_unsend_about_group(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tcanRevokeOutgoingCount,\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t}\n\treturn std::nullopt;\n}\n\nvoid DeleteMessagesBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\tconst auto &padding = st::boxPadding;\n\t_text->moveToLeft(padding.left(), padding.top());\n\tauto top = _text->bottomNoMargins() + st::boxMediumSkip;\n\tif (_moderateFrom) {\n\t\tif (_banUser) {\n\t\t\t_banUser->moveToLeft(padding.left(), top);\n\t\t\ttop += _banUser->heightNoMargins() + st::boxLittleSkip;\n\t\t}\n\t\t_reportSpam->moveToLeft(padding.left(), top);\n\t\ttop += _reportSpam->heightNoMargins() + st::boxLittleSkip;\n\t\tif (_deleteAll) {\n\t\t\tconst auto availableWidth = width() - 2 * padding.left();\n\t\t\t_deleteAll->resizeToNaturalWidth(availableWidth);\n\t\t\t_deleteAll->moveToLeft(padding.left(), top);\n\t\t\ttop += _deleteAll->heightNoMargins() + st::boxLittleSkip;\n\t\t}\n\t} else if (_revoke) {\n\t\tconst auto availableWidth = width() - 2 * padding.left();\n\t\t_revoke->resizeToNaturalWidth(availableWidth);\n\t\t_revoke->moveToLeft(padding.left(), top);\n\t\ttop += _revoke->heightNoMargins() + st::boxLittleSkip;\n\t\tif (_revokeRemember) {\n\t\t\t_revokeRemember->resizeToNaturalWidth(availableWidth);\n\t\t\t_revokeRemember->moveToLeft(padding.left(),top);\n\t\t\ttop += _revokeRemember->heightNoMargins();\n\t\t}\n\t}\n\tif (_autoDeleteSettings) {\n\t\ttop += st::boxMediumSkip - st::boxLittleSkip;\n\t\t_autoDeleteSettings->moveToLeft(padding.left(), top);\n\t}\n}\n\nvoid DeleteMessagesBox::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {\n\t\t// Don't make the clearing history so easy.\n\t\tif (!_wipeHistoryPeer) {\n\t\t\tdeleteAndClear();\n\t\t}\n\t} else {\n\t\tBoxContent::keyPressEvent(e);\n\t}\n}\n\nPaidPostType DeleteMessagesBox::paidPostType() const {\n\tauto result = PaidPostType::None;\n\tconst auto now = base::unixtime::now();\n\tfor (const auto &id : _ids) {\n\t\tif (const auto item = _session->data().message(id)) {\n\t\t\tconst auto type = item->paidType();\n\t\t\tif (type != PaidPostType::None) {\n\t\t\t\tconst auto date = item->date();\n\t\t\t\tconst auto config = &item->history()->session().appConfig();\n\t\t\t\tconst auto limit = config->suggestedPostAgeMin();\n\t\t\t\tif (now < date || now - date <= limit) {\n\t\t\t\t\tif (type == PaidPostType::Ton) {\n\t\t\t\t\t\treturn type;\n\t\t\t\t\t} else if (type == PaidPostType::Stars) {\n\t\t\t\t\t\tresult = type;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid DeleteMessagesBox::deleteAndClear() {\n\tconst auto warnPaidType = _confirmedDeletePaidSuggestedPosts\n\t\t? PaidPostType::None\n\t\t: paidPostType();\n\tif (warnPaidType != PaidPostType::None) {\n\t\tconst auto weak = base::make_weak(this);\n\t\tconst auto callback = [=](Fn<void()> close) {\n\t\t\tclose();\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->_confirmedDeletePaidSuggestedPosts = true;\n\t\t\t\tstrong->deleteAndClear();\n\t\t\t}\n\t\t};\n\t\tconst auto ton = (warnPaidType == PaidPostType::Ton);\n\t\tuiShow()->show(Ui::MakeConfirmBox({\n\t\t\t.text = (ton\n\t\t\t\t? tr::lng_suggest_warn_text_ton\n\t\t\t\t: tr::lng_suggest_warn_text_stars)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\ttr::rich),\n\t\t\t.confirmed = callback,\n\t\t\t.confirmText = tr::lng_suggest_warn_delete_anyway(tr::now),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t.title = (ton\n\t\t\t\t? tr::lng_suggest_warn_title_ton\n\t\t\t\t: tr::lng_suggest_warn_title_stars)(tr::now),\n\t\t}));\n\t\treturn;\n\t}\n\tif (_revoke\n\t\t&& _revokeRemember\n\t\t&& _revokeRemember->toggled()\n\t\t&& _revokeRemember->entity()->checked()) {\n\t\tCore::App().settings().setRememberedDeleteMessageOnlyForYou(\n\t\t\t!_revoke->checked());\n\t\tCore::App().saveSettingsDelayed();\n\t}\n\tconst auto revoke = _revoke\n\t\t? _revoke->checked()\n\t\t: (_revokeForBot || _revokeJustClearForChannel);\n\tconst auto session = _session;\n\tconst auto invokeCallbackAndClose = [&] {\n\t\t// deleteMessages can initiate closing of the current section,\n\t\t// which will cause this box to be destroyed.\n\t\tconst auto weak = base::make_weak(this);\n\t\tif (hasSavedMusicMessages()) {\n\t\t\tuiShow()->showToast(tr::lng_saved_music_removed(tr::now));\n\t\t}\n\t\tif (const auto callback = _deleteConfirmedCallback) {\n\t\t\tcallback();\n\t\t}\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t};\n\tif (!_wipeHistoryFirstToDelete.isNull()) {\n\t\tconst auto peer = _wipeHistoryPeer;\n\t\tconst auto firstDayToDelete = _wipeHistoryFirstToDelete;\n\t\tconst auto lastDayToDelete = _wipeHistoryLastToDelete;\n\n\t\tinvokeCallbackAndClose();\n\t\tsession->data().histories().deleteMessagesByDates(\n\t\t\tsession->data().history(peer),\n\t\t\tfirstDayToDelete,\n\t\t\tlastDayToDelete,\n\t\t\trevoke);\n\t\tsession->data().sendHistoryChangeNotifications();\n\t\treturn;\n\t} else if (const auto peer = _wipeHistoryPeer) {\n\t\tconst auto justClear = _wipeHistoryJustClear;\n\t\tinvokeCallbackAndClose();\n\n\t\tif (justClear) {\n\t\t\tsession->api().clearHistory(peer, revoke);\n\t\t} else {\n\t\t\tCore::App().closeChatFromWindows(peer);\n\t\t\t// Don't delete old history by default,\n\t\t\t// because Android app doesn't.\n\t\t\t//\n\t\t\t//if (const auto from = peer->migrateFrom()) {\n\t\t\t//\tpeer->session().api().deleteConversation(from, false);\n\t\t\t//}\n\t\t\tsession->api().deleteConversation(peer, revoke);\n\t\t}\n\t\treturn;\n\t}\n\tif (_moderateFrom) {\n\t\tif (_banUser && _banUser->checked()) {\n\t\t\t_moderateInChannel->session().api().chatParticipants().kick(\n\t\t\t\t_moderateInChannel,\n\t\t\t\t_moderateFrom,\n\t\t\t\tChatRestrictionsInfo());\n\t\t}\n\t\tif (_reportSpam->checked()) {\n\t\t\tApi::ReportSpam(_moderateFrom, { _ids[0] });\n\t\t}\n\t\tif (_deleteAll && _deleteAll->checked()) {\n\t\t\t_moderateInChannel->session().api().deleteAllFromParticipant(\n\t\t\t\t_moderateInChannel,\n\t\t\t\t_moderateFrom);\n\t\t}\n\t}\n\n\tconst auto ids = _ids;\n\tinvokeCallbackAndClose();\n\tsession->data().histories().deleteMessages(ids, revoke);\n\tsession->data().sendHistoryChangeNotifications();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/delete_messages_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n\nenum class PaidPostType : uchar;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass Checkbox;\nclass FlatLabel;\nclass LinkButton;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nclass DeleteMessagesBox final : public Ui::BoxContent {\npublic:\n\tDeleteMessagesBox(\n\t\tQWidget*,\n\t\tnot_null<HistoryItem*> item,\n\t\tbool suggestModerateActions);\n\tDeleteMessagesBox(\n\t\tQWidget*,\n\t\tnot_null<Main::Session*> session,\n\t\tMessageIdsList &&selected);\n\tDeleteMessagesBox(\n\t\tQWidget*,\n\t\tnot_null<PeerData*> peer,\n\t\tQDate firstDayToDelete,\n\t\tQDate lastDayToDelete);\n\tDeleteMessagesBox(QWidget*, not_null<PeerData*> peer, bool justClear);\n\n\tvoid setDeleteConfirmedCallback(Fn<void()> callback) {\n\t\t_deleteConfirmedCallback = std::move(callback);\n\t}\n\t[[nodiscard]] crl::time layerAnimationDuration() const override;\n\nprotected:\n\tvoid prepare() override;\n\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tstruct RevokeConfig {\n\t\tTextWithEntities checkbox;\n\t\tTextWithEntities description;\n\t};\n\tvoid deleteAndClear();\n\t[[nodiscard]] PeerData *checkFromSinglePeer() const;\n\t[[nodiscard]] bool hasScheduledMessages() const;\n\t[[nodiscard]] bool hasSavedMusicMessages() const;\n\t[[nodiscard]] std::optional<RevokeConfig> revokeText(\n\t\tnot_null<PeerData*> peer) const;\n\t[[nodiscard]] PaidPostType paidPostType() const;\n\n\tconst not_null<Main::Session*> _session;\n\n\tPeerData * const _wipeHistoryPeer = nullptr;\n\tconst bool _wipeHistoryJustClear = false;\n\tconst QDate _wipeHistoryFirstToDelete;\n\tconst QDate _wipeHistoryLastToDelete;\n\tconst MessageIdsList _ids;\n\tPeerData *_moderateFrom = nullptr;\n\tChannelData *_moderateInChannel = nullptr;\n\tbool _moderateBan = false;\n\tbool _moderateDeleteAll = false;\n\n\tbool _revokeForBot = false;\n\tbool _revokeJustClearForChannel = false;\n\n\tobject_ptr<Ui::FlatLabel> _text = { nullptr };\n\tobject_ptr<Ui::Checkbox> _revoke = { nullptr };\n\tobject_ptr<Ui::SlideWrap<Ui::Checkbox>> _revokeRemember = { nullptr };\n\tobject_ptr<Ui::Checkbox> _banUser = { nullptr };\n\tobject_ptr<Ui::Checkbox> _reportSpam = { nullptr };\n\tobject_ptr<Ui::Checkbox> _deleteAll = { nullptr };\n\tobject_ptr<Ui::LinkButton> _autoDeleteSettings = { nullptr };\n\n\tint _fullHeight = 0;\n\tbool _confirmedDeletePaidSuggestedPosts = false;\n\n\tFn<void()> _deleteConfirmedCallback;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/dictionaries_manager.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/dictionaries_manager.h\"\n\n#ifndef TDESKTOP_DISABLE_SPELLCHECK\n\n#include \"base/event_filter.h\"\n#include \"chat_helpers/spellchecker_common.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"mtproto/dedicated_file_loader.h\"\n#include \"spellcheck/spellcheck_utils.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/multi_select.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/effects/animations.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Ui {\nnamespace {\n\nusing Dictionaries = std::vector<int>;\nusing namespace Storage::CloudBlob;\n\nusing Loading = MTP::DedicatedLoader::Progress;\nusing DictState = BlobState;\nusing QueryCallback = Fn<void(const QString &)>;\nconstexpr auto kMaxQueryLength = 15;\n\nclass Inner : public Ui::RpWidget {\npublic:\n\tInner(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Session*> session,\n\t\tDictionaries enabledDictionaries);\n\n\tDictionaries enabledRows() const;\n\tQueryCallback queryCallback() const;\n\nprivate:\n\tvoid setupContent(\n\t\tnot_null<Main::Session*> session,\n\t\tDictionaries enabledDictionaries);\n\n\tDictionaries _enabledRows;\n\tQueryCallback _queryCallback;\n\n};\n\ninline auto DictExists(int langId) {\n\treturn Spellchecker::DictionaryExists(langId);\n}\n\ninline auto FilterEnabledDict(Dictionaries dicts) {\n\treturn dicts | ranges::views::filter(\n\t\tDictExists\n\t) | ranges::to_vector;\n}\n\nDictState ComputeState(int id, bool enabled) {\n\tconst auto result = enabled ? DictState(Active()) : DictState(Ready());\n\tif (DictExists(id)) {\n\t\treturn result;\n\t}\n\treturn Available{ Spellchecker::GetDownloadSize(id) };\n}\n\nQString StateDescription(const DictState &state) {\n\treturn StateDescription(\n\t\tstate,\n\t\ttr::lng_settings_manage_enabled_dictionary);\n}\n\nauto CreateMultiSelect(QWidget *parent) {\n\tconst auto result = Ui::CreateChild<Ui::MultiSelect>(\n\t\tparent,\n\t\tst::defaultMultiSelect,\n\t\ttr::lng_participant_filter());\n\n\tresult->resizeToWidth(st::boxWidth);\n\tresult->moveToLeft(0, 0);\n\treturn result;\n}\n\nInner::Inner(\n\tQWidget *parent,\n\tnot_null<Main::Session*> session,\n\tDictionaries enabledDictionaries)\n: RpWidget(parent) {\n\tsetupContent(session, std::move(enabledDictionaries));\n}\n\nQueryCallback Inner::queryCallback() const {\n\treturn _queryCallback;\n}\n\nDictionaries Inner::enabledRows() const {\n\treturn _enabledRows;\n}\n\nauto AddButtonWithLoader(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tnot_null<Main::Session*> session,\n\t\tconst Spellchecker::Dict &dict,\n\t\tbool buttonEnabled,\n\t\trpl::producer<QStringView> query) {\n\tconst auto id = dict.id;\n\tbuttonEnabled &= DictExists(id);\n\n\tconst auto locale = Spellchecker::LocaleFromLangId(id);\n\tconst std::vector<QString> indexList = {\n\t\tdict.name,\n\t\tQLocale::languageToString(locale.language()),\n\t\tQLocale::countryToString(locale.country())\n\t};\n\n\tconst auto wrap = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tcontent,\n\t\t\t\trpl::single(dict.name),\n\t\t\t\tst::dictionariesSectionButton\n\t\t\t)\n\t\t)\n\t);\n\tconst auto button = wrap->entity();\n\n\tstd::move(\n\t\tquery\n\t) | rpl::on_next([=](auto string) {\n\t\twrap->toggle(\n\t\t\tranges::any_of(indexList, [&](const QString &s) {\n\t\t\t\treturn s.startsWith(string, Qt::CaseInsensitive);\n\t\t\t}),\n\t\t\tanim::type::instant);\n\t}, button->lifetime());\n\n\tusing Loader = Spellchecker::DictLoader;\n\tusing GlobalLoaderPtr = std::shared_ptr<base::unique_qptr<Loader>>;\n\n\tconst auto localLoader = button->lifetime()\n\t\t.make_state<base::unique_qptr<Loader>>();\n\tconst auto localLoaderValues = button->lifetime()\n\t\t.make_state<rpl::event_stream<Loader*>>();\n\tconst auto setLocalLoader = [=](base::unique_qptr<Loader> loader) {\n\t\t*localLoader = std::move(loader);\n\t\tlocalLoaderValues->fire(localLoader->get());\n\t};\n\tconst auto destroyLocalLoader = [=] {\n\t\tsetLocalLoader(nullptr);\n\t};\n\n\tconst auto buttonState = button->lifetime()\n\t\t.make_state<rpl::variable<DictState>>();\n\tconst auto dictionaryRemoved = button->lifetime()\n\t\t.make_state<rpl::event_stream<>>();\n\tconst auto dictionaryFromGlobalLoader = button->lifetime()\n\t\t.make_state<rpl::event_stream<>>();\n\n\tconst auto globalLoader = button->lifetime()\n\t\t.make_state<GlobalLoaderPtr>();\n\n\tconst auto rawGlobalLoaderPtr = [=]() -> Loader* {\n\t\tif (!globalLoader || !*globalLoader || !*globalLoader->get()) {\n\t\t\treturn nullptr;\n\t\t}\n\t\treturn globalLoader->get()->get();\n\t};\n\n\tconst auto setGlobalLoaderPtr = [=](GlobalLoaderPtr loader) {\n\t\tif (localLoader->get()) {\n\t\t\tif (loader && loader->get()) {\n\t\t\t\tloader->get()->destroy();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t*globalLoader = std::move(loader);\n\t\tlocalLoaderValues->fire(rawGlobalLoaderPtr());\n\t\tif (rawGlobalLoaderPtr()) {\n\t\t\tdictionaryFromGlobalLoader->fire({});\n\t\t}\n\t};\n\n\tSpellchecker::GlobalLoaderChanged(\n\t) | rpl::on_next([=](int langId) {\n\t\tif (!langId && rawGlobalLoaderPtr()) {\n\t\t\tsetGlobalLoaderPtr(nullptr);\n\t\t} else if (langId == id) {\n\t\t\tsetGlobalLoaderPtr(Spellchecker::GlobalLoader());\n\t\t}\n\t}, button->lifetime());\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton,\n\t\tbuttonState->value() | rpl::map(StateDescription),\n\t\tst::settingsUpdateState);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\trpl::combine(\n\t\tbutton->widthValue(),\n\t\tlabel->widthValue()\n\t) | rpl::on_next([=] {\n\t\tlabel->moveToLeft(\n\t\t\tst::settingsUpdateStatePosition.x(),\n\t\t\tst::settingsUpdateStatePosition.y());\n\t}, label->lifetime());\n\n\tbuttonState->value(\n\t) | rpl::on_next([=](const DictState &state) {\n\t\tconst auto isToggledSet = v::is<Active>(state);\n\t\tconst auto toggled = isToggledSet ? 1. : 0.;\n\t\tconst auto over = !button->isDisabled()\n\t\t\t&& (button->isDown() || button->isOver());\n\n\t\tif (toggled == 0. && !over) {\n\t\t\tlabel->setTextColorOverride(std::nullopt);\n\t\t} else {\n\t\t\tlabel->setTextColorOverride(anim::color(\n\t\t\t\tover ? st::contactsStatusFgOver : st::contactsStatusFg,\n\t\t\t\tst::contactsStatusFgOnline,\n\t\t\t\ttoggled));\n\t\t}\n\t}, label->lifetime());\n\n\tbutton->toggleOn(\n\t\trpl::single(\n\t\t\tbuttonEnabled\n\t\t) | rpl::then(\n\t\t\trpl::merge(\n\t\t\t\t// Events to toggle on.\n\t\t\t\tdictionaryFromGlobalLoader->events() | rpl::map_to(true),\n\t\t\t\t// Events to toggle off.\n\t\t\t\trpl::merge(\n\t\t\t\t\tdictionaryRemoved->events(),\n\t\t\t\t\tbuttonState->value(\n\t\t\t\t\t) | rpl::filter([](const DictState &state) {\n\t\t\t\t\t\treturn v::is<Failed>(state);\n\t\t\t\t\t}) | rpl::to_empty\n\t\t\t\t) | rpl::map_to(false)\n\t\t\t)\n\t\t)\n\t);\n\n\t*buttonState = localLoaderValues->events_starting_with(\n\t\trawGlobalLoaderPtr() ? rawGlobalLoaderPtr() : localLoader->get()\n\t) | rpl::map([=](Loader *loader) {\n\t\treturn (loader && loader->id() == id)\n\t\t\t? loader->state()\n\t\t\t: rpl::single(\n\t\t\t\tbuttonEnabled\n\t\t\t) | rpl::then(\n\t\t\t\trpl::merge(\n\t\t\t\t\tdictionaryRemoved->events() | rpl::map_to(false),\n\t\t\t\t\tbutton->toggledValue()\n\t\t\t\t)\n\t\t\t) | rpl::map([=](auto enabled) {\n\t\t\t\treturn ComputeState(id, enabled);\n\t\t\t});\n\t}) | rpl::flatten_latest(\n\t) | rpl::filter([=](const DictState &state) {\n\t\treturn !v::is<Failed>(buttonState->current())\n\t\t\t|| !v::is<Available>(state);\n\t});\n\n\tbutton->toggledValue(\n\t) | rpl::on_next([=](bool toggled) {\n\t\tconst auto &state = buttonState->current();\n\t\tif (toggled && (v::is<Available>(state) || v::is<Failed>(state))) {\n\t\t\tconst auto weak = base::make_weak(button);\n\t\t\tsetLocalLoader(base::make_unique_q<Loader>(\n\t\t\t\tQCoreApplication::instance(),\n\t\t\t\tsession,\n\t\t\t\tid,\n\t\t\t\tSpellchecker::GetDownloadLocation(id),\n\t\t\t\tSpellchecker::DictPathByLangId(id),\n\t\t\t\tSpellchecker::GetDownloadSize(id),\n\t\t\t\tcrl::guard(weak, destroyLocalLoader)));\n\t\t} else if (!toggled && v::is<Loading>(state)) {\n\t\t\tif (const auto g = rawGlobalLoaderPtr()) {\n\t\t\t\tg->destroy();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (localLoader && localLoader->get()->id() == id) {\n\t\t\t\tdestroyLocalLoader();\n\t\t\t}\n\t\t}\n\t}, button->lifetime());\n\n\tconst auto contextMenu = button->lifetime()\n\t\t.make_state<base::unique_qptr<Ui::PopupMenu>>();\n\tconst auto showMenu = [=] {\n\t\tif (!DictExists(id)) {\n\t\t\treturn false;\n\t\t}\n\t\t*contextMenu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tbutton,\n\t\t\tst::popupMenuWithIcons);\n\t\tcontextMenu->get()->addAction(\n\t\t\ttr::lng_settings_manage_remove_dictionary(tr::now), [=] {\n\t\t\t\tSpellchecker::RemoveDictionary(id);\n\t\t\t\tdictionaryRemoved->fire({});\n\t\t\t}, &st::menuIconDelete);\n\t\tcontextMenu->get()->popup(QCursor::pos());\n\t\treturn true;\n\t};\n\n\tbase::install_event_filter(button, [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::ContextMenu && showMenu()) {\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\tif (const auto g = Spellchecker::GlobalLoader()) {\n\t\tif (g.get() && g->get()->id() == id) {\n\t\t\tsetGlobalLoaderPtr(g);\n\t\t}\n\t}\n\n\treturn button;\n}\n\nvoid Inner::setupContent(\n\t\tnot_null<Main::Session*> session,\n\t\tDictionaries enabledDictionaries) {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tconst auto queryStream = content->lifetime()\n\t\t.make_state<rpl::event_stream<QStringView>>();\n\n\tfor (const auto &dict : Spellchecker::Dictionaries()) {\n\t\tconst auto id = dict.id;\n\t\tconst auto row = AddButtonWithLoader(\n\t\t\tcontent,\n\t\t\tsession,\n\t\t\tdict,\n\t\t\tranges::contains(enabledDictionaries, id),\n\t\t\tqueryStream->events());\n\t\trow->toggledValue(\n\t\t) | rpl::on_next([=](auto enabled) {\n\t\t\tif (enabled) {\n\t\t\t\t_enabledRows.push_back(id);\n\t\t\t} else {\n\t\t\t\tauto &rows = _enabledRows;\n\t\t\t\trows.erase(ranges::remove(rows, id), end(rows));\n\t\t\t}\n\t\t}, row->lifetime());\n\t}\n\n\t_queryCallback = [=](const QString &query) {\n\t\tif (query.size() >= kMaxQueryLength) {\n\t\t\treturn;\n\t\t}\n\t\tqueryStream->fire_copy(query);\n\t};\n\n\tcontent->resizeToWidth(st::boxWidth);\n\tUi::ResizeFitChild(this, content);\n}\n\n} // namespace\n\nManageDictionariesBox::ManageDictionariesBox(\n\tQWidget*,\n\tnot_null<Main::Session*> session)\n: _session(session) {\n}\n\nvoid ManageDictionariesBox::setInnerFocus() {\n\t_setInnerFocus();\n}\n\nvoid ManageDictionariesBox::prepare() {\n\tconst auto multiSelect = CreateMultiSelect(this);\n\n\tconst auto inner = setInnerWidget(\n\t\tobject_ptr<Inner>(\n\t\t\tthis,\n\t\t\t_session,\n\t\t\tCore::App().settings().dictionariesEnabled()),\n\t\tst::boxScroll,\n\t\tmultiSelect->height()\n\t);\n\n\tmultiSelect->setQueryChangedCallback(inner->queryCallback());\n\t_setInnerFocus = [=] {\n\t\tmultiSelect->setInnerFocus();\n\t};\n\n\t// The initial list of enabled rows may differ from the list of languages\n\t// in settings, so we should store it when box opens\n\t// and save it when box closes (don't do it when \"Save\" was pressed).\n\tconst auto initialEnabledRows = inner->enabledRows();\n\n\tsetTitle(tr::lng_settings_manage_dictionaries());\n\n\taddButton(tr::lng_settings_save(), [=] {\n\t\tCore::App().settings().setDictionariesEnabled(\n\t\t\tFilterEnabledDict(inner->enabledRows()));\n\t\tCore::App().saveSettingsDelayed();\n\t\t// Ignore boxClosing() when the Save button was pressed.\n\t\tlifetime().destroy();\n\t\tcloseBox();\n\t});\n\taddButton(tr::lng_close(), [=] { closeBox(); });\n\n\tboxClosing() | rpl::on_next([=] {\n\t\tCore::App().settings().setDictionariesEnabled(\n\t\t\tFilterEnabledDict(initialEnabledRows));\n\t\tCore::App().saveSettingsDelayed();\n\t}, lifetime());\n\n\tsetDimensionsToContent(st::boxWidth, inner);\n\n\tusing namespace rpl::mappers;\n\tconst auto max = lifetime().make_state<int>(0);\n\trpl::combine(\n\t\tinner->heightValue(),\n\t\tmultiSelect->heightValue(),\n\t\t_1 + _2\n\t) | rpl::on_next([=](int height) {\n\t\tusing std::min;\n\t\taccumulate_max(*max, height);\n\t\tsetDimensions(st::boxWidth, min(*max, st::boxMaxListHeight), true);\n\t}, inner->lifetime());\n}\n\n} // namespace Ui\n\n#endif // !TDESKTOP_DISABLE_SPELLCHECK\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/dictionaries_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#ifndef TDESKTOP_DISABLE_SPELLCHECK\n\n#include \"ui/layers/box_content.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\n\nclass ManageDictionariesBox : public Ui::BoxContent {\npublic:\n\tManageDictionariesBox(QWidget*, not_null<Main::Session*> session);\n\nprotected:\n\tvoid prepare() override;\n\tvoid setInnerFocus() override;\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\tFn<void()> _setInnerFocus;\n\n};\n\n} // namespace Ui\n\n#endif // !TDESKTOP_DISABLE_SPELLCHECK\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/download_path_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/download_path_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"core/file_utilities.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"platform/platform_specific.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"storage/storage_account.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\nDownloadPathBox::DownloadPathBox(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: _controller(controller)\n, _path(Core::App().settings().downloadPath())\n, _pathBookmark(Core::App().settings().downloadPathBookmark())\n, _group(std::make_shared<Ui::RadioenumGroup<Directory>>(typeFromPath(_path)))\n, _default(Core::App().canReadDefaultDownloadPath()\n\t? object_ptr<Ui::Radioenum<Directory>>(\n\t\tthis,\n\t\t_group,\n\t\tDirectory::Downloads,\n\t\ttr::lng_download_path_default_radio(tr::now),\n\t\tst::defaultBoxCheckbox)\n\t: nullptr)\n, _temp(this, _group, Directory::Temp, tr::lng_download_path_temp_radio(tr::now), st::defaultBoxCheckbox)\n, _dir(this, _group, Directory::Custom, tr::lng_download_path_dir_radio(tr::now), st::defaultBoxCheckbox)\n, _pathLink(this, QString(), st::boxLinkButton) {\n}\n\nvoid DownloadPathBox::prepare() {\n\taddButton(tr::lng_connection_save(), [this] { save(); });\n\taddButton(tr::lng_cancel(), [this] { closeBox(); });\n\n\tsetTitle(tr::lng_download_path_header());\n\n\t_group->setChangedCallback([this](Directory value) {\n\t\tradioChanged(value);\n\t});\n\n\t_pathLink->addClickHandler([=] { editPath(); });\n\tif (!_path.isEmpty() && _path != FileDialog::Tmp()) {\n\t\tsetPathText(QDir::toNativeSeparators(_path));\n\t}\n\tupdateControlsVisibility();\n}\n\nvoid DownloadPathBox::updateControlsVisibility() {\n\tauto custom = (_group->current() == Directory::Custom);\n\t_pathLink->setVisible(custom);\n\n\tauto newHeight = st::boxOptionListPadding.top() + (_default ? _default->getMargins().top() + _default->heightNoMargins() : 0) + st::boxOptionListSkip + _temp->heightNoMargins() + st::boxOptionListSkip + _dir->heightNoMargins();\n\tif (custom) {\n\t\tnewHeight += st::downloadPathSkip + _pathLink->height();\n\t}\n\tnewHeight += st::boxOptionListPadding.bottom() + _dir->getMargins().bottom();\n\n\tsetDimensions(st::boxWideWidth, newHeight);\n}\n\nvoid DownloadPathBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\tif (_default) {\n\t\t_default->moveToLeft(st::boxPadding.left() + st::boxOptionListPadding.left(), st::boxOptionListPadding.top() + _default->getMargins().top());\n\t}\n\t_temp->moveToLeft(st::boxPadding.left() + st::boxOptionListPadding.left(), (_default ? _default->bottomNoMargins() : 0) + st::boxOptionListSkip);\n\t_dir->moveToLeft(st::boxPadding.left() + st::boxOptionListPadding.left(), _temp->bottomNoMargins() + st::boxOptionListSkip);\n\tauto inputx = st::boxPadding.left() + st::boxOptionListPadding.left() + st::defaultCheck.diameter + st::defaultBoxCheckbox.textPosition.x();\n\tauto inputy = _dir->bottomNoMargins() + st::downloadPathSkip;\n\n\t_pathLink->moveToLeft(inputx, inputy);\n}\n\nvoid DownloadPathBox::radioChanged(Directory value) {\n\tif (value == Directory::Custom) {\n\t\tif (_path.isEmpty() || _path == FileDialog::Tmp()) {\n\t\t\t_group->setValue(_path.isEmpty() ? Directory::Downloads : Directory::Temp);\n\t\t\teditPath();\n\t\t} else {\n\t\t\tsetPathText(QDir::toNativeSeparators(_path));\n\t\t}\n\t} else if (value == Directory::Temp) {\n\t\t_path = FileDialog::Tmp();\n\t} else {\n\t\t_path = QString();\n\t}\n\tupdateControlsVisibility();\n\tupdate();\n}\n\nvoid DownloadPathBox::editPath() {\n\tconst auto initialPath = [] {\n\t\tconst auto path = Core::App().settings().downloadPath();\n\t\tif (!path.isEmpty() && path != FileDialog::Tmp()) {\n\t\t\treturn path.left(path.size() - (path.endsWith('/') ? 1 : 0));\n\t\t}\n\t\treturn QString();\n\t}();\n\tconst auto handleFolder = [=](const QString &result) {\n\t\tif (!result.isEmpty()) {\n\t\t\t_path = result.endsWith('/') ? result : (result + '/');\n\t\t\t_pathBookmark = psDownloadPathBookmark(_path);\n\t\t\tsetPathText(QDir::toNativeSeparators(_path));\n\t\t\t_group->setValue(Directory::Custom);\n\t\t}\n\t};\n\tFileDialog::GetFolder(\n\t\tthis,\n\t\ttr::lng_download_path_choose(tr::now),\n\t\tinitialPath,\n\t\tcrl::guard(this, handleFolder));\n}\n\nvoid DownloadPathBox::save() {\n#ifndef OS_WIN_STORE\n\tauto value = _group->current();\n\tauto computePath = [this, value] {\n\t\tif (value == Directory::Custom) {\n\t\t\treturn _path;\n\t\t} else if (value == Directory::Temp) {\n\t\t\treturn FileDialog::Tmp();\n\t\t}\n\t\treturn QString();\n\t};\n\tCore::App().settings().setDownloadPathBookmark(\n\t\t(value == Directory::Custom) ? _pathBookmark : QByteArray());\n\tCore::App().settings().setDownloadPath(computePath());\n\tCore::App().saveSettings();\n\tcloseBox();\n#endif // OS_WIN_STORE\n}\n\nvoid DownloadPathBox::setPathText(const QString &text) {\n\tauto availw = st::boxWideWidth - st::boxPadding.left() - st::defaultCheck.diameter - st::defaultBoxCheckbox.textPosition.x() - st::boxPadding.right();\n\t_pathLink->setText(st::boxTextFont->elided(text, availw));\n}\n\nDownloadPathBox::Directory DownloadPathBox::typeFromPath(\n\t\tconst QString &path) {\n\tif (path.isEmpty()) {\n\t\treturn Core::App().canReadDefaultDownloadPath()\n\t\t\t? Directory::Downloads\n\t\t\t: Directory::Temp;\n\t} else if (path == FileDialog::Tmp()) {\n\t\treturn Directory::Temp;\n\t}\n\treturn Directory::Custom;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/download_path_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n\nnamespace Ui {\ntemplate <typename Enum>\nclass RadioenumGroup;\ntemplate <typename Enum>\nclass Radioenum;\nclass LinkButton;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nclass DownloadPathBox : public Ui::BoxContent {\npublic:\n\tDownloadPathBox(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\nprotected:\n\tvoid prepare() override;\n\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tenum class Directory {\n\t\tDownloads,\n\t\tTemp,\n\t\tCustom,\n\t};\n\tvoid radioChanged(Directory value);\n\tDirectory typeFromPath(const QString &path);\n\n\tvoid save();\n\tvoid updateControlsVisibility();\n\tvoid setPathText(const QString &text);\n\tvoid editPath();\n\n\tconst not_null<Window::SessionController*> _controller;\n\tQString _path;\n\tQByteArray _pathBookmark;\n\n\tstd::shared_ptr<Ui::RadioenumGroup<Directory>> _group;\n\tobject_ptr<Ui::Radioenum<Directory>> _default;\n\tobject_ptr<Ui::Radioenum<Directory>> _temp;\n\tobject_ptr<Ui::Radioenum<Directory>> _dir;\n\tobject_ptr<Ui::LinkButton> _pathLink;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/edit_caption_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/edit_caption_box.h\"\n\n#include \"api/api_editing.h\"\n#include \"api/api_text_entities.h\"\n#include \"apiwrap.h\"\n#include \"base/event_filter.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"chat_helpers/field_autocomplete.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/file_utilities.h\"\n#include \"core/mime_type.h\"\n#include \"data/data_document.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"editor/editor_layer_widget.h\"\n#include \"editor/photo_editor.h\"\n#include \"editor/photo_editor_layer_widget.h\"\n#include \"history/history_drag_area.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"history/view/controls/history_view_compose_ai_button.h\"\n#include \"lang/lang_keys.h\"\n#include \"menu/menu_checked_action.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"mainwidget.h\" // controller->content() -> QWidget*\n#include \"menu/menu_send.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"platform/platform_specific.h\"\n#include \"storage/localimageloader.h\" // SendMediaType\n#include \"storage/storage_media_prepare.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/attach/attach_item_single_file_preview.h\"\n#include \"ui/chat/attach/attach_item_single_media_preview.h\"\n#include \"ui/chat/attach/attach_single_file_preview.h\"\n#include \"ui/chat/attach/attach_single_media_preview.h\"\n#include \"ui/controls/compose_ai_button_factory.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/effects/scroll_content_shadow.h\"\n#include \"ui/image/image.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtCore/QMimeData>\n\nnamespace {\n\nconstexpr auto kChangesDebounceTimeout = crl::time(1000);\n\n[[nodiscard]] Ui::PreparedList ListFromMimeData(\n\t\tnot_null<const QMimeData*> data,\n\t\tbool premium) {\n\tusing Error = Ui::PreparedList::Error;\n\tconst auto list = Core::ReadMimeUrls(data);\n\tauto result = !list.isEmpty()\n\t\t? Storage::PrepareMediaList(\n\t\t\tlist.mid(0, 1), // When we edit media, we need only 1 file.\n\t\t\tst::sendMediaPreviewSize,\n\t\t\tpremium)\n\t\t: Ui::PreparedList(Error::EmptyFile, QString());\n\tif (result.error == Error::None) {\n\t\treturn result;\n\t} else if (auto read = Core::ReadMimeImage(data)) {\n\t\treturn Storage::PrepareMediaFromImage(\n\t\t\tstd::move(read.image),\n\t\t\tstd::move(read.content),\n\t\t\tst::sendMediaPreviewSize);\n\t}\n\treturn result;\n}\n\n[[nodiscard]] Ui::AlbumType ComputeAlbumType(not_null<HistoryItem*> item) {\n\tif (item->groupId().empty()) {\n\t\treturn Ui::AlbumType();\n\t}\n\tconst auto media = item->media();\n\n\tif (media->photo()) {\n\t\treturn Ui::AlbumType::PhotoVideo;\n\t} else if (const auto document = media->document()) {\n\t\tif (document->isVideoFile()) {\n\t\t\treturn Ui::AlbumType::PhotoVideo;\n\t\t} else if (document->isSong()) {\n\t\t\treturn Ui::AlbumType::Music;\n\t\t} else {\n\t\t\treturn Ui::AlbumType::File;\n\t\t}\n\t}\n\treturn Ui::AlbumType();\n}\n\n[[nodiscard]] bool CanToggleCompressed(Ui::AlbumType type) {\n\treturn (type == Ui::AlbumType::None);\n}\n\n[[nodiscard]] bool AlbumTypeCompressed(Ui::AlbumType type) {\n\treturn (type == Ui::AlbumType::None)\n\t\t|| (type == Ui::AlbumType::PhotoVideo);\n}\n\nvoid ChooseReplacement(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tUi::AlbumType type,\n\t\tFn<void(Ui::PreparedList&&)> chosen) {\n\tconst auto weak = base::make_weak(controller);\n\tconst auto callback = [=](FileDialog::OpenResult &&result) {\n\t\tconst auto strong = weak.get();\n\t\tif (!strong) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto showError = [=](tr::phrase<> t) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->showToast(t(tr::now));\n\t\t\t}\n\t\t};\n\n\t\tconst auto checkResult = [=](const Ui::PreparedList &list) {\n\t\t\tif (list.files.size() != 1) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto &file = list.files.front();\n\t\t\tconst auto mime = file.information->filemime;\n\t\t\tif (Core::IsMimeSticker(mime)) {\n\t\t\t\tshowError(tr::lng_edit_media_invalid_file);\n\t\t\t\treturn false;\n\t\t\t} else if (type != Ui::AlbumType::None\n\t\t\t\t&& !file.canBeInAlbumType(type)) {\n\t\t\t\tshowError(tr::lng_edit_media_album_error);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\t\tconst auto premium = strong->session().premium();\n\t\tauto list = Storage::PreparedFileFromFilesDialog(\n\t\t\tstd::move(result),\n\t\t\tcheckResult,\n\t\t\tshowError,\n\t\t\tst::sendMediaPreviewSize,\n\t\t\tpremium);\n\n\t\tif (list) {\n\t\t\tchosen(std::move(*list));\n\t\t}\n\t};\n\n\tconst auto filters = (type == Ui::AlbumType::PhotoVideo)\n\t\t? FileDialog::PhotoVideoFilesFilter()\n\t\t: FileDialog::AllFilesFilter();\n\tFileDialog::GetOpenPath(\n\t\tcontroller->content().get(),\n\t\ttr::lng_choose_file(tr::now),\n\t\tfilters,\n\t\tcrl::guard(controller, callback));\n}\n\nvoid EditPhotoImage(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tstd::shared_ptr<Data::PhotoMedia> media,\n\t\tbool spoilered,\n\t\tint sideLimit,\n\t\tFn<void(Ui::PreparedList)> done) {\n\tconst auto large = media\n\t\t? media->image(Data::PhotoSize::Large)\n\t\t: nullptr;\n\tconst auto parent = controller->content();\n\tconst auto previewWidth = st::sendMediaPreviewSize;\n\tauto callback = [=](const Editor::PhotoModifications &mods) {\n\t\tif (!mods) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto large = media->image(Data::PhotoSize::Large);\n\t\tif (!large) {\n\t\t\treturn;\n\t\t}\n\t\tauto copy = large->original();\n\t\tauto list = Storage::PrepareMediaFromImage(\n\t\t\tstd::move(copy),\n\t\t\tQByteArray(),\n\t\t\tpreviewWidth);\n\n\t\tusing ImageInfo = Ui::PreparedFileInformation::Image;\n\t\tauto &file = list.files.front();\n\t\tfile.spoiler = spoilered;\n\t\tconst auto image = std::get_if<ImageInfo>(&file.information->media);\n\n\t\timage->modifications = mods;\n\t\tStorage::UpdateImageDetails(file, previewWidth, sideLimit);\n\t\tdone(std::move(list));\n\t};\n\tif (!large) {\n\t\treturn;\n\t}\n\tconst auto fileImage = std::make_shared<Image>(*large);\n\tauto editor = base::make_unique_q<Editor::PhotoEditor>(\n\t\tparent,\n\t\t&controller->window(),\n\t\tfileImage,\n\t\tEditor::PhotoModifications());\n\tconst auto raw = editor.get();\n\tauto layer = std::make_unique<Editor::LayerWidget>(\n\t\tparent,\n\t\tstd::move(editor));\n\tEditor::InitEditorLayer(layer.get(), raw, std::move(callback));\n\tcontroller->showLayer(std::move(layer), Ui::LayerOption::KeepOther);\n}\n\n} // namespace\n\nEditCaptionBox::EditCaptionBox(\n\tQWidget*,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<HistoryItem*> item,\n\tTextWithTags &&text,\n\tSuggestOptions suggest,\n\tbool spoilered,\n\tbool invertCaption,\n\tUi::PreparedList &&list,\n\tFn<void()> saved)\n: _controller(controller)\n, _historyItem(item)\n, _suggest(suggest)\n, _isAllowedEditMedia(item->allowsEditMedia())\n, _albumType(ComputeAlbumType(item))\n, _controls(base::make_unique_q<Ui::VerticalLayout>(this))\n, _scroll(base::make_unique_q<Ui::ScrollArea>(this, st::boxScroll))\n, _field(base::make_unique_q<Ui::InputField>(\n\tthis,\n\tst::defaultComposeFiles.caption,\n\tUi::InputField::Mode::MultiLine,\n\ttr::lng_photo_caption()))\n, _emojiToggle(base::make_unique_q<Ui::EmojiButton>(\n\tthis,\n\tst::defaultComposeFiles.emoji))\n, _initialText(std::move(text))\n, _initialList(std::move(list))\n, _saved(std::move(saved)) {\n\tExpects(!_initialList.files.empty());\n\tExpects(item->allowsEditMedia());\n\n\t_asFile = !AlbumTypeCompressed(_albumType);\n\t_sendLargePhotos = Core::App().settings().sendFilesWay().sendLargePhotos();\n\n\t_mediaEditManager.start(item, spoilered, invertCaption);\n\n\t_controller->session().data().itemRemoved(\n\t\t_historyItem->fullId()\n\t) | rpl::on_next([=] {\n\t\tcloseBox();\n\t}, lifetime());\n}\n\nEditCaptionBox::~EditCaptionBox() = default;\n\nvoid EditCaptionBox::StartMediaReplace(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFullMsgId itemId,\n\t\tTextWithTags text,\n\t\tSuggestOptions suggest,\n\t\tbool spoilered,\n\t\tbool invertCaption,\n\t\tFn<void()> saved) {\n\tconst auto session = &controller->session();\n\tconst auto item = session->data().message(itemId);\n\tif (!item) {\n\t\treturn;\n\t}\n\tconst auto show = [=](Ui::PreparedList &&list) mutable {\n\t\tcontroller->show(Box<EditCaptionBox>(\n\t\t\tcontroller,\n\t\t\titem,\n\t\t\tstd::move(text),\n\t\t\tsuggest,\n\t\t\tspoilered,\n\t\t\tinvertCaption,\n\t\t\tstd::move(list),\n\t\t\tstd::move(saved)));\n\t};\n\tChooseReplacement(\n\t\tcontroller,\n\t\tComputeAlbumType(item),\n\t\tcrl::guard(controller, show));\n}\n\nvoid EditCaptionBox::StartMediaReplace(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFullMsgId itemId,\n\t\tUi::PreparedList &&list,\n\t\tTextWithTags text,\n\t\tSuggestOptions suggest,\n\t\tbool spoilered,\n\t\tbool invertCaption,\n\t\tFn<void()> saved) {\n\tconst auto session = &controller->session();\n\tconst auto item = session->data().message(itemId);\n\tif (!item) {\n\t\treturn;\n\t}\n\tconst auto type = ComputeAlbumType(item);\n\tconst auto showError = [=](tr::phrase<> t) {\n\t\tcontroller->showToast(t(tr::now));\n\t};\n\tconst auto checkResult = [=](const Ui::PreparedList &list) {\n\t\tif (list.files.size() != 1) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto &file = list.files.front();\n\t\tconst auto mime = file.information->filemime;\n\t\tif (Core::IsMimeSticker(mime)) {\n\t\t\tshowError(tr::lng_edit_media_invalid_file);\n\t\t\treturn false;\n\t\t} else if (type != Ui::AlbumType::None\n\t\t\t&& !file.canBeInAlbumType(type)) {\n\t\t\tshowError(tr::lng_edit_media_album_error);\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t};\n\tif (list.error != Ui::PreparedList::Error::None) {\n\t\tshowError(tr::lng_send_media_invalid_files);\n\t} else if (checkResult(list)) {\n\t\tcontroller->show(Box<EditCaptionBox>(\n\t\t\tcontroller,\n\t\t\titem,\n\t\t\tstd::move(text),\n\t\t\tsuggest,\n\t\t\tspoilered,\n\t\t\tinvertCaption,\n\t\t\tstd::move(list),\n\t\t\tstd::move(saved)));\n\t}\n}\n\nvoid EditCaptionBox::StartPhotoEdit(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tstd::shared_ptr<Data::PhotoMedia> media,\n\t\tFullMsgId itemId,\n\t\tTextWithTags text,\n\t\tSuggestOptions suggest,\n\t\tbool spoilered,\n\t\tbool invertCaption,\n\t\tFn<void()> saved) {\n\tconst auto session = &controller->session();\n\tconst auto item = session->data().message(itemId);\n\tif (!item) {\n\t\treturn;\n\t}\n\tEditPhotoImage(\n\t\tcontroller,\n\t\tmedia,\n\t\tspoilered,\n\t\tPhotoSideLimit(true),\n\t\t[=](Ui::PreparedList &&list) mutable {\n\t\t\tconst auto item = session->data().message(itemId);\n\t\t\tif (!item) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcontroller->show(Box<EditCaptionBox>(\n\t\t\t\tcontroller,\n\t\t\t\titem,\n\t\t\t\tstd::move(text),\n\t\t\t\tsuggest,\n\t\t\t\tspoilered,\n\t\t\t\tinvertCaption,\n\t\t\t\tstd::move(list),\n\t\t\t\tstd::move(saved)));\n\t\t});\n}\n\nvoid EditCaptionBox::showFinished() {\n\tif (const auto raw = _autocomplete.get()) {\n\t\tInvokeQueued(raw, [=] {\n\t\t\traw->raise();\n\t\t});\n\t}\n}\n\nvoid EditCaptionBox::prepare() {\n\tconst auto button = addButton(tr::lng_settings_save(), [=] { save(); });\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\n\tconst auto details = crl::guard(this, [=] {\n\t\tauto result = SendMenu::Details();\n\t\tconst auto allWithSpoilers = ranges::all_of(\n\t\t\t_preparedList.files,\n\t\t\t&Ui::PreparedFile::spoiler);\n\t\tresult.spoiler = !_preparedList.hasSpoilerMenu(!_asFile)\n\t\t\t? SendMenu::SpoilerState::None\n\t\t\t: allWithSpoilers\n\t\t\t? SendMenu::SpoilerState::Enabled\n\t\t\t: SendMenu::SpoilerState::Possible;\n\t\tconst auto canMoveCaption = _preparedList.canMoveCaption(\n\t\t\tfalse,\n\t\t\t!_asFile\n\t\t) && _field && HasSendText(_field);\n\t\tresult.caption = !canMoveCaption\n\t\t\t? SendMenu::CaptionState::None\n\t\t\t: _mediaEditManager.invertCaption()\n\t\t\t? SendMenu::CaptionState::Above\n\t\t\t: SendMenu::CaptionState::Below;\n\t\tresult.photoQuality = !hasSendLargePhotosOption()\n\t\t\t? SendMenu::PhotoQualityState::None\n\t\t\t: _sendLargePhotos\n\t\t\t? SendMenu::PhotoQualityState::High\n\t\t\t: SendMenu::PhotoQualityState::Standard;\n\t\treturn result;\n\t});\n\tconst auto callback = [=](SendMenu::Action action, const auto &) {\n\t\tusing Type = SendMenu::ActionType;\n\t\tswitch (action.type) {\n\t\tcase Type::PhotoQualityOn: _sendLargePhotos = true; break;\n\t\tcase Type::PhotoQualityOff: _sendLargePhotos = false; break;\n\t\tdefault: _mediaEditManager.apply(action); break;\n\t\t}\n\t\trebuildPreview();\n\t};\n\tSendMenu::SetupMenuAndShortcuts(\n\t\tbutton,\n\t\tnullptr,\n\t\tdetails,\n\t\tcrl::guard(this, callback));\n\n\tupdateBoxSize();\n\n\tsetupField();\n\tsetupEmojiPanel();\n\tsetInitialText();\n\n\tif (!setPreparedList(std::move(_initialList))) {\n\t\tcrl::on_main(this, [=] { closeBox(); });\n\t\treturn;\n\t}\n\tsetupEditEventHandler();\n\tSetupShadowsToScrollContent(this, _scroll, _contentHeight.events());\n\n\tsetupControls();\n\tsetupPhotoEditorEventHandler();\n\n\tsetupDragArea();\n\n\tcaptionResized();\n}\n\nvoid EditCaptionBox::rebuildPreview() {\n\tconst auto gifPaused = [controller = _controller] {\n\t\treturn controller->isGifPausedAtLeastFor(\n\t\t\tWindow::GifPauseReason::Layer);\n\t};\n\n\tapplyChanges();\n\n\tif (_preparedList.files.empty()) {\n\t\tconst auto media = _historyItem->media();\n\t\tconst auto photo = media->photo();\n\t\tconst auto document = media->document();\n\t\t_isPhoto = (photo != nullptr);\n\t\t_isVideo = (document != nullptr) && document->isVideoFile();\n\t\tif (_isPhoto || _isVideo || document->isAnimation()) {\n\t\t\tconst auto media = Ui::CreateChild<Ui::ItemSingleMediaPreview>(\n\t\t\t\tthis,\n\t\t\t\tst::defaultComposeControls,\n\t\t\t\tgifPaused,\n\t\t\t\t_historyItem,\n\t\t\t\tUi::AttachControls::Type::EditOnly);\n\t\t\t_photoMedia = media->sharedPhotoMedia();\n\t\t\t_content.reset(media);\n\t\t} else {\n\t\t\t_content.reset(Ui::CreateChild<Ui::ItemSingleFilePreview>(\n\t\t\t\tthis,\n\t\t\t\tst::defaultComposeControls,\n\t\t\t\t_historyItem,\n\t\t\t\tUi::AttachControls::Type::EditOnly));\n\t\t}\n\t} else {\n\t\tconst auto &file = _preparedList.files.front();\n\t\t_isVideo = file.isVideoFile();\n\t\tconst auto media = Ui::SingleMediaPreview::Create(\n\t\t\tthis,\n\t\t\tst::defaultComposeControls,\n\t\t\tgifPaused,\n\t\t\tfile,\n\t\t\tUi::AttachControls::Type::EditOnly);\n\t\t_isPhoto = (media && media->isPhoto());\n\t\tif (media && !_asFile) {\n\t\t\tmedia->setSendWay(currentSendWay());\n\t\t\tmedia->setCanShowHighQualityBadge(file.canUseHighQualityPhoto());\n\t\t\t_content.reset(media);\n\t\t} else {\n\t\t\t_content.reset(Ui::CreateChild<Ui::SingleFilePreview>(\n\t\t\t\tthis,\n\t\t\t\tst::defaultComposeControls,\n\t\t\t\tfile,\n\t\t\t\tUi::AttachControls::Type::EditOnly));\n\t\t}\n\t}\n\tAssert(_content != nullptr);\n\n\trpl::combine(\n\t\t_content->heightValue(),\n\t\t_footerHeight.value(),\n\t\trpl::single(st::boxPhotoPadding.top()),\n\t\trpl::mappers::_1 + rpl::mappers::_2 + rpl::mappers::_3\n\t) | rpl::on_next([=](int height) {\n\t\tsetDimensions(\n\t\t\tst::boxWideWidth,\n\t\t\tstd::min(st::sendMediaPreviewHeightMax, height),\n\t\t\ttrue);\n\t}, _content->lifetime());\n\n\t_content->editRequests(\n\t) | rpl::start_to_stream(_editMediaClicks, _content->lifetime());\n\n\t_content->modifyRequests(\n\t) | rpl::start_to_stream(_photoEditorOpens, _content->lifetime());\n\n\t_content->heightValue(\n\t) | rpl::start_to_stream(_contentHeight, _content->lifetime());\n\n\t_scroll->setOwnedWidget(\n\t\tobject_ptr<Ui::RpWidget>::fromRaw(_content.get()));\n\n\t_previewRebuilds.fire({});\n\n\tcaptionResized();\n}\n\nvoid EditCaptionBox::setupField() {\n\tconst auto peer = _historyItem->history()->peer;\n\tconst auto allow = [=](not_null<DocumentData*> emoji) {\n\t\treturn Data::AllowEmojiWithoutPremium(peer, emoji);\n\t};\n\tconst auto chatStyle = InitMessageFieldHandlers({\n\t\t.session = &_controller->session(),\n\t\t.show = _controller->uiShow(),\n\t\t.field = _field.get(),\n\t\t.customEmojiPaused = [=] {\n\t\t\treturn _controller->isGifPausedAtLeastFor(\n\t\t\t\tWindow::GifPauseReason::Layer);\n\t\t},\n\t\t.allowPremiumEmoji = allow,\n\t});\n\tsetupFieldAutocomplete();\n\tUi::Emoji::SuggestionsController::Init(\n\t\tgetDelegate()->outerContainer(),\n\t\t_field,\n\t\t&_controller->session(),\n\t\t{ .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow });\n\n\t_field->setSubmitSettings(\n\t\tCore::App().settings().sendSubmitWay());\n\t_field->setMaxHeight(st::defaultComposeFiles.caption.heightMax);\n\n\t_field->submits(\n\t) | rpl::on_next([=] { save(); }, _field->lifetime());\n\t_field->cancelled(\n\t) | rpl::on_next([=] {\n\t\tcloseBox();\n\t}, _field->lifetime());\n\t_field->heightChanges(\n\t) | rpl::on_next([=] {\n\t\tcaptionResized();\n\t}, _field->lifetime());\n\t_field->setMimeDataHook([=](\n\t\t\tnot_null<const QMimeData*> data,\n\t\t\tUi::InputField::MimeAction action) {\n\t\tif (action == Ui::InputField::MimeAction::Check) {\n\t\t\tif (!data->hasText() && !_isAllowedEditMedia) {\n\t\t\t\treturn false;\n\t\t\t} else if (Storage::ValidateEditMediaDragData(data, _albumType)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn data->hasText();\n\t\t} else if (action == Ui::InputField::MimeAction::Insert) {\n\t\t\treturn fileFromClipboard(data);\n\t\t}\n\t\tUnexpected(\"Action in MimeData hook.\");\n\t});\n\n\t_aiButton = Ui::SetupCaptionAiButton({\n\t\t.parent = this,\n\t\t.field = _field.get(),\n\t\t.session = &_controller->session(),\n\t\t.show = _controller->uiShow(),\n\t\t.chatStyle = chatStyle,\n\t});\n}\n\nvoid EditCaptionBox::setupFieldAutocomplete() {\n\tconst auto parent = getDelegate()->outerContainer();\n\tChatHelpers::InitFieldAutocomplete(_autocomplete, {\n\t\t.parent = parent,\n\t\t.show = _controller->uiShow(),\n\t\t.field = _field.get(),\n\t\t.peer = _historyItem->history()->peer,\n\t\t.features = [=] {\n\t\t\tauto result = ChatHelpers::ComposeFeatures();\n\t\t\tresult.autocompleteCommands = false;\n\t\t\tresult.suggestStickersByEmoji = false;\n\t\t\treturn result;\n\t\t},\n\t});\n\tconst auto raw = _autocomplete.get();\n\tconst auto scheduled = std::make_shared<bool>();\n\tconst auto recountPostponed = [=] {\n\t\tif (*scheduled) {\n\t\t\treturn;\n\t\t}\n\t\t*scheduled = true;\n\t\tUi::PostponeCall(raw, [=] {\n\t\t\t*scheduled = false;\n\n\t\t\tauto field = Ui::MapFrom(parent, this, _field->geometry());\n\t\t\t_autocomplete->setBoundings(QRect(\n\t\t\t\tfield.x() - _field->x(),\n\t\t\t\tst::defaultBox.margin.top(),\n\t\t\t\twidth(),\n\t\t\t\t(field.y()\n\t\t\t\t\t+ st::defaultComposeFiles.caption.textMargins.top()\n\t\t\t\t\t+ st::defaultComposeFiles.caption.placeholderShift\n\t\t\t\t\t+ st::defaultComposeFiles.caption.placeholderFont->height\n\t\t\t\t\t- st::defaultBox.margin.top())));\n\t\t});\n\t};\n\tfor (auto w = (QWidget*)_field.get(); w; w = w->parentWidget()) {\n\t\tbase::install_event_filter(raw, w, [=](not_null<QEvent*> e) {\n\t\t\tif (e->type() == QEvent::Move || e->type() == QEvent::Resize) {\n\t\t\t\trecountPostponed();\n\t\t\t}\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t});\n\t\tif (w == parent) {\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nvoid EditCaptionBox::setInitialText() {\n\t_field->setTextWithTags(\n\t\t_initialText,\n\t\tUi::InputField::HistoryAction::Clear);\n\tauto cursor = _field->textCursor();\n\tcursor.movePosition(QTextCursor::End);\n\t_field->setTextCursor(cursor);\n\n\t_checkChangedTimer.setCallback([=] {\n\t\tif (_field->getTextWithAppliedMarkdown() == _initialText\n\t\t\t&& _preparedList.files.empty()) {\n\t\t\tsetCloseByOutsideClick(true);\n\t\t}\n\t});\n\t_field->changes(\n\t) | rpl::on_next([=] {\n\t\t_checkChangedTimer.callOnce(kChangesDebounceTimeout);\n\t\tsetCloseByOutsideClick(false);\n\t}, _field->lifetime());\n}\n\nvoid EditCaptionBox::setupControls() {\n\tauto hintLabelToggleOn = _previewRebuilds.events_starting_with(\n\t\t{}\n\t) | rpl::map([=] {\n\t\treturn _isPhoto\n\t\t\t&& !_asFile\n\t\t\t&& _controller->session().settings().photoEditorHintShown();\n\t});\n\n\t_controls->add(object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(\n\t\tthis,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\ttr::lng_edit_photo_editor_hint(tr::now),\n\t\t\tst::editMediaHintLabel),\n\t\tst::editMediaLabelMargins)\n\t)->toggleOn(std::move(hintLabelToggleOn), anim::type::instant);\n\n\t_controls->add(object_ptr<Ui::SlideWrap<Ui::Checkbox>>(\n\t\tthis,\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tthis,\n\t\t\ttr::lng_send_as_documents_one(tr::now),\n\t\t\t_asFile,\n\t\t\tst::defaultBoxCheckbox),\n\t\tst::editMediaCheckboxMargins)\n\t)->toggleOn(\n\t\t_previewRebuilds.events_starting_with({}) | rpl::map([=] {\n\t\t\treturn (_isPhoto || _isVideo)\n\t\t\t\t&& CanToggleCompressed(_albumType)\n\t\t\t\t&& !_preparedList.files.empty();\n\t\t}),\n\t\tanim::type::instant\n\t)->entity()->checkedChanges(\n\t) | rpl::on_next([&](bool checked) {\n\t\tapplyChanges();\n\t\t_asFile = checked;\n\t\trebuildPreview();\n\t}, _controls->lifetime());\n\n\t_controls->resizeToWidth(st::sendMediaPreviewSize);\n}\n\nvoid EditCaptionBox::setupEditEventHandler() {\n\tconst auto menu\n\t\t= lifetime().make_state<base::unique_qptr<Ui::PopupMenu>>();\n\t_editMediaClicks.events(\n\t) | rpl::on_next([=] {\n\t\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tthis,\n\t\t\tst::popupMenuWithIcons);\n\t\t(*menu)->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);\n\t\tif (_isAllowedEditMedia) {\n\t\t\t(*menu)->addAction(tr::lng_attach_replace(tr::now), [=] {\n\t\t\t\tChooseReplacement(\n\t\t\t\t\t_controller,\n\t\t\t\t\t_albumType,\n\t\t\t\t\tcrl::guard(this, [=](Ui::PreparedList &&list) {\n\t\t\t\t\t\tsetPreparedList(std::move(list));\n\t\t\t\t\t}));\n\t\t\t}, &st::menuIconReplace);\n\t\t}\n\t\tusing Type = Ui::PreparedFile::Type;\n\t\tconst auto canDraw = !_preparedList.files.empty()\n\t\t\t? (_preparedList.files.front().type == Type::Photo)\n\t\t\t: (_isPhoto && !_asFile);\n\t\tif (canDraw) {\n\t\t\t(*menu)->addAction(tr::lng_context_draw(tr::now), [=] {\n\t\t\t\t_photoEditorOpens.fire({});\n\t\t\t}, &st::menuIconDraw);\n\t\t}\n\t\tif (!_asFile && (_isPhoto || _isVideo)) {\n\t\t\tif (hasSendLargePhotosOption()) {\n\t\t\t\tconst auto enabled = _sendLargePhotos;\n\t\t\t\tMenu::AddCheckedAction(\n\t\t\t\t\tmenu->get(),\n\t\t\t\t\ttr::lng_send_high_quality(tr::now),\n\t\t\t\t\t[=] {\n\t\t\t\t\t\t_sendLargePhotos = !enabled;\n\t\t\t\t\t\trebuildPreview();\n\t\t\t\t\t},\n\t\t\t\t\t&st::menuIconQualityHigh,\n\t\t\t\t\tenabled);\n\t\t\t}\n\t\t\tif (_preparedList.hasSpoilerMenu(!_asFile)) {\n\t\t\t\tconst auto spoilered = hasSpoiler();\n\t\t\t\tMenu::AddCheckedAction(\n\t\t\t\t\tmenu->get(),\n\t\t\t\t\ttr::lng_context_spoiler_effect(tr::now),\n\t\t\t\t\t[=] {\n\t\t\t\t\t\t_mediaEditManager.apply({ .type = spoilered\n\t\t\t\t\t\t\t? SendMenu::ActionType::SpoilerOff\n\t\t\t\t\t\t\t: SendMenu::ActionType::SpoilerOn\n\t\t\t\t\t\t});\n\t\t\t\t\t\trebuildPreview();\n\t\t\t\t\t},\n\t\t\t\t\t&st::menuIconSpoiler,\n\t\t\t\t\tspoilered);\n\t\t\t}\n\t\t\tif (_isVideo && !_preparedList.files.empty()) {\n\t\t\t\t(*menu)->addAction(tr::lng_context_edit_cover(tr::now), [=] {\n\t\t\t\t\tsetupEditCoverHandler();\n\t\t\t\t}, &st::menuIconEdit);\n\t\t\t\tif (_preparedList.files.front().videoCover != nullptr) {\n\t\t\t\t\t(*menu)->addAction(\n\t\t\t\t\t\ttr::lng_context_clear_cover(tr::now),\n\t\t\t\t\t\t[=] { setupClearCoverHandler(); },\n\t\t\t\t\t\t&st::menuIconCancel);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif ((*menu)->empty()) {\n\t\t\t*menu = nullptr;\n\t\t} else {\n\t\t\t(*menu)->popup(QCursor::pos());\n\t\t}\n\t}, lifetime());\n}\n\nvoid EditCaptionBox::setupPhotoEditorEventHandler() {\n\tconst auto openedOnce = lifetime().make_state<bool>(false);\n\t_photoEditorOpens.events(\n\t) | rpl::on_next([=, controller = _controller] {\n\t\tif (_preparedList.files.empty()\n\t\t\t&& (!_photoMedia\n\t\t\t\t|| !_photoMedia->image(Data::PhotoSize::Large))) {\n\t\t\treturn;\n\t\t} else if (!*openedOnce) {\n\t\t\t*openedOnce = true;\n\t\t\tcontroller->session().settings().incrementPhotoEditorHintShown();\n\t\t\tcontroller->session().saveSettings();\n\t\t}\n\t\tif (!_error.isEmpty()) {\n\t\t\t_error = QString();\n\t\t\tupdate();\n\t\t}\n\t\tif (!_preparedList.files.empty()) {\n\t\t\tEditor::OpenWithPreparedFile(\n\t\t\t\tthis,\n\t\t\t\tcontroller->uiShow(),\n\t\t\t\t&_preparedList.files.front(),\n\t\t\t\tst::sendMediaPreviewSize,\n\t\t\t\t[=](bool ok) { if (ok) rebuildPreview(); },\n\t\t\t\tPhotoSideLimit(true));\n\t\t} else {\n\t\t\tEditPhotoImage(\n\t\t\t\t_controller,\n\t\t\t\t_photoMedia,\n\t\t\t\thasSpoiler(),\n\t\t\t\tPhotoSideLimit(true),\n\t\t\t\t[=](Ui::PreparedList &&list) {\n\t\t\t\t\tsetPreparedList(std::move(list));\n\t\t\t\t});\n\t\t}\n\t}, lifetime());\n}\n\nvoid EditCaptionBox::setupEditCoverHandler() {\n\tif (_preparedList.files.empty()) {\n\t\treturn;\n\t}\n\tconst auto &file = _preparedList.files.front();\n\tif (!file.isVideoFile()) {\n\t\treturn;\n\t}\n\tconst auto show = _controller->uiShow();\n\tconst auto replace = [=](Ui::PreparedList list) {\n\t\tif (list.files.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tauto &entry = _preparedList.files.front();\n\t\tconst auto video = entry.information\n\t\t\t? std::get_if<Ui::PreparedFileInformation::Video>(\n\t\t\t\t&entry.information->media)\n\t\t\t: nullptr;\n\t\tif (!video) {\n\t\t\treturn;\n\t\t}\n\t\tauto old = std::shared_ptr<Ui::PreparedFile>(\n\t\t\tstd::move(entry.videoCover));\n\t\tentry.videoCover = std::make_unique<Ui::PreparedFile>(\n\t\t\tstd::move(list.files.front()));\n\t\tEditor::OpenWithPreparedFile(\n\t\t\tthis,\n\t\t\tshow,\n\t\t\tentry.videoCover.get(),\n\t\t\tst::sendMediaPreviewSize,\n\t\t\tcrl::guard(this, [=](bool ok) {\n\t\t\t\tif (!ok) {\n\t\t\t\t\t_preparedList.files.front().videoCover = old\n\t\t\t\t\t\t? std::make_unique<Ui::PreparedFile>(\n\t\t\t\t\t\t\tstd::move(*old))\n\t\t\t\t\t\t: nullptr;\n\t\t\t\t}\n\t\t\t\trebuildPreview();\n\t\t\t}),\n\t\t\tPhotoSideLimit(true),\n\t\t\tvideo->thumbnail.size());\n\t};\n\tconst auto checkResult = [=](const Ui::PreparedList &list) {\n\t\tif (list.files.empty()) {\n\t\t\treturn true;\n\t\t}\n\t\tif (list.files.front().type != Ui::PreparedFile::Type::Photo) {\n\t\t\tshow->showToast(tr::lng_choose_cover_bad(tr::now));\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t};\n\tconst auto callback = [=](FileDialog::OpenResult &&result) {\n\t\tconst auto premium = show->session().premium();\n\t\tconst auto showError = [=](tr::phrase<> t) {\n\t\t\tshow->showToast(t(tr::now));\n\t\t};\n\t\tauto list = Storage::PreparedFileFromFilesDialog(\n\t\t\tstd::move(result),\n\t\t\tcheckResult,\n\t\t\tshowError,\n\t\t\tst::sendMediaPreviewSize,\n\t\t\tpremium);\n\t\tif (list) {\n\t\t\treplace(std::move(*list));\n\t\t}\n\t};\n\n\tFileDialog::GetOpenPath(\n\t\tthis,\n\t\ttr::lng_choose_cover(tr::now),\n\t\tFileDialog::ImagesFilter(),\n\t\tcrl::guard(this, callback));\n}\n\nvoid EditCaptionBox::setupClearCoverHandler() {\n\tif (_preparedList.files.empty()) {\n\t\treturn;\n\t}\n\tauto &entry = _preparedList.files.front();\n\tentry.videoCover = nullptr;\n\trebuildPreview();\n}\n\nvoid EditCaptionBox::setupDragArea() {\n\tauto enterFilter = [=](not_null<const QMimeData*> data) {\n\t\treturn !_isAllowedEditMedia\n\t\t\t? false\n\t\t\t: Storage::ValidateEditMediaDragData(data, _albumType);\n\t};\n\t// Avoid both drag areas appearing at one time.\n\tauto computeState = [=](const QMimeData *data) {\n\t\tusing DragState = Storage::MimeDataState;\n\t\tconst auto state = Storage::ComputeMimeDataState(data);\n\t\treturn (state == DragState::PhotoFiles\n\t\t\t|| state == DragState::Image\n\t\t\t|| state == DragState::MediaFiles)\n\t\t\t? (_asFile ? DragState::Files : DragState::Image)\n\t\t\t: state;\n\t};\n\tconst auto areas = DragArea::SetupDragAreaToContainer(\n\t\tthis,\n\t\tstd::move(enterFilter),\n\t\t[=](bool f) { _field->setAcceptDrops(f); },\n\t\tnullptr,\n\t\tstd::move(computeState));\n\n\tconst auto droppedCallback = [=](bool compress) {\n\t\treturn [=](const QMimeData *data) {\n\t\t\tfileFromClipboard(data);\n\t\t\tWindow::ActivateWindow(_controller);\n\t\t};\n\t};\n\tareas.document->setDroppedCallback(droppedCallback(false));\n\tareas.photo->setDroppedCallback(droppedCallback(true));\n}\n\nvoid EditCaptionBox::setupEmojiPanel() {\n\tconst auto container = getDelegate()->outerContainer();\n\tusing Selector = ChatHelpers::TabbedSelector;\n\t_emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(\n\t\tcontainer,\n\t\t_controller,\n\t\tobject_ptr<Selector>(\n\t\t\tnullptr,\n\t\t\t_controller->uiShow(),\n\t\t\tWindow::GifPauseReason::Layer,\n\t\t\tSelector::Mode::EmojiOnly));\n\t_emojiPanel->setDesiredHeightValues(\n\t\t1.,\n\t\tst::emojiPanMinHeight / 2,\n\t\tst::emojiPanMinHeight);\n\t_emojiPanel->hide();\n\t_emojiPanel->selector()->setCurrentPeer(_historyItem->history()->peer);\n\t_emojiPanel->selector()->emojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\tUi::InsertEmojiAtCursor(_field->textCursor(), data.emoji);\n\t}, lifetime());\n\t_emojiPanel->selector()->customEmojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\tconst auto info = data.document->sticker();\n\t\tif (info\n\t\t\t&& info->setType == Data::StickersType::Emoji\n\t\t\t&& !_controller->session().premium()) {\n\t\t\tShowPremiumPreviewBox(\n\t\t\t\t_controller,\n\t\t\t\tPremiumFeature::AnimatedEmoji);\n\t\t} else {\n\t\t\tData::InsertCustomEmoji(_field.get(), data.document);\n\t\t}\n\t}, lifetime());\n\n\tconst auto filterCallback = [=](not_null<QEvent*> event) {\n\t\temojiFilterForGeometry(event);\n\t\treturn base::EventFilterResult::Continue;\n\t};\n\t_emojiFilter.reset(base::install_event_filter(container, filterCallback));\n\n\tif (!Core::App().settings().fork().emojiPopupOnClick()) {\n\t_emojiToggle->installEventFilter(_emojiPanel);\n\t}\n\t_emojiToggle->addClickHandler([=] {\n\t\t_emojiPanel->toggleAnimated();\n\t});\n}\n\nvoid EditCaptionBox::emojiFilterForGeometry(not_null<QEvent*> event) {\n\tconst auto type = event->type();\n\tif (type == QEvent::Move || type == QEvent::Resize) {\n\t\t// updateEmojiPanelGeometry uses not only container geometry, but\n\t\t// also container children geometries that will be updated later.\n\t\tcrl::on_main(this, [=] { updateEmojiPanelGeometry(); });\n\t}\n}\n\nvoid EditCaptionBox::updateEmojiPanelGeometry() {\n\tconst auto parent = _emojiPanel->parentWidget();\n\tconst auto global = _emojiToggle->mapToGlobal({ 0, 0 });\n\tconst auto local = parent->mapFromGlobal(global);\n\t_emojiPanel->moveBottomRight(\n\t\tlocal.y(),\n\t\tlocal.x() + _emojiToggle->width() * 3);\n}\n\nbool EditCaptionBox::fileFromClipboard(not_null<const QMimeData*> data) {\n\tconst auto premium = _controller->session().premium();\n\treturn setPreparedList(ListFromMimeData(data, premium));\n}\n\nbool EditCaptionBox::setPreparedList(Ui::PreparedList &&list) {\n\tif (!_isAllowedEditMedia) {\n\t\treturn false;\n\t}\n\tusing Error = Ui::PreparedList::Error;\n\tif (list.error != Error::None || list.files.empty()) {\n\t\treturn false;\n\t}\n\tauto file = &list.files.front();\n\tconst auto invalidForAlbum = (_albumType != Ui::AlbumType::None)\n\t\t&& !file->canBeInAlbumType(_albumType);\n\tif (_albumType == Ui::AlbumType::PhotoVideo) {\n\t\tusing Video = Ui::PreparedFileInformation::Video;\n\t\tif (const auto video = std::get_if<Video>(\n\t\t\t\t&file->information->media)) {\n\t\t\tvideo->isGifv = false;\n\t\t}\n\t}\n\tif (invalidForAlbum) {\n\t\tshowToast(tr::lng_edit_media_album_error(tr::now));\n\t\treturn false;\n\t}\n\tconst auto wasSpoiler = hasSpoiler();\n\t_preparedList = std::move(list);\n\t_preparedList.files.front().spoiler = wasSpoiler;\n\tsetCloseByOutsideClick(false);\n\trebuildPreview();\n\treturn true;\n}\n\nbool EditCaptionBox::hasSpoiler() const {\n\treturn _mediaEditManager.spoilered();\n}\n\nbool EditCaptionBox::hasSendLargePhotosOption() const {\n\tconst auto compressed = CanToggleCompressed(_albumType)\n\t\t? (!_asFile)\n\t\t: AlbumTypeCompressed(_albumType);\n\treturn compressed\n\t\t&& !_preparedList.files.empty()\n\t\t&& _preparedList.hasSendLargePhotosOption(compressed);\n}\n\nUi::SendFilesWay EditCaptionBox::currentSendWay() const {\n\tauto way = Core::App().settings().sendFilesWay();\n\tway.setSendImagesAsPhotos(!_asFile);\n\tway.setSendLargePhotos(_sendLargePhotos);\n\treturn way;\n}\n\nvoid EditCaptionBox::saveSendWaySettings() {\n\tauto way = Core::App().settings().sendFilesWay();\n\tif (way.sendLargePhotos() == _sendLargePhotos) {\n\t\treturn;\n\t}\n\tway.setSendLargePhotos(_sendLargePhotos);\n\tCore::App().settings().setSendFilesWay(way);\n\tCore::App().saveSettingsDelayed();\n}\n\nvoid EditCaptionBox::captionResized() {\n\tupdateBoxSize();\n\tresizeEvent(0);\n\tupdateEmojiPanelGeometry();\n\tupdate();\n}\n\nvoid EditCaptionBox::updateBoxSize() {\n\tauto footerHeight = 0;\n\tfooterHeight += st::normalFont->height + errorTopSkip();\n\tif (_field) {\n\t\tfooterHeight += st::boxPhotoCaptionSkip + _field->height();\n\t}\n\tif (_controls && !_controls->isHidden()) {\n\t\tfooterHeight += _controls->heightNoMargins();\n\t}\n\t_footerHeight = footerHeight;\n}\n\nint EditCaptionBox::errorTopSkip() const {\n\treturn (st::defaultBox.buttonPadding.top() / 2);\n}\n\nvoid EditCaptionBox::paintEvent(QPaintEvent *e) {\n\tBoxContent::paintEvent(e);\n\n\tPainter p(this);\n\n\tif (!_error.isEmpty()) {\n\t\tp.setFont(st::normalFont);\n\t\tp.setPen(st::boxTextFgError);\n\t\tp.drawTextLeft(\n\t\t\t_field->x(),\n\t\t\t_field->y() + _field->height() + errorTopSkip(),\n\t\t\twidth(),\n\t\t\t_error);\n\t}\n\n}\n\nvoid EditCaptionBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\tconst auto errorHeight = st::normalFont->height + errorTopSkip();\n\tauto bottom = height();\n\t{\n\t\tconst auto resultScrollHeight = bottom\n\t\t\t- _field->height()\n\t\t\t- st::boxPhotoCaptionSkip\n\t\t\t- (_controls->isHidden() ? 0 : _controls->heightNoMargins())\n\t\t\t- st::boxPhotoPadding.top()\n\t\t\t- errorHeight;\n\t\tconst auto minThumbH = st::sendBoxAlbumGroupSize.height()\n\t\t\t+ st::sendBoxAlbumGroupSkipTop * 2;\n\t\tconst auto diff = resultScrollHeight - minThumbH;\n\t\tif (diff < 0) {\n\t\t\tbottom -= diff;\n\t\t}\n\t}\n\n\tbottom -= errorHeight;\n\t_field->resize(st::sendMediaPreviewSize, _field->height());\n\t_field->moveToLeft(\n\t\tst::boxPhotoPadding.left(),\n\t\tbottom - _field->height());\n\tbottom -= st::boxPhotoCaptionSkip + _field->height();\n\n\t_emojiToggle->moveToLeft(\n\t\t(st::boxPhotoPadding.left()\n\t\t\t+ st::sendMediaPreviewSize\n\t\t\t- _emojiToggle->width()),\n\t\t_field->y() + st::boxAttachEmojiTop);\n\t_emojiToggle->update();\n\n\tif (_aiButton) {\n\t\tUi::UpdateCaptionAiButtonGeometry(_aiButton, _field.get());\n\t\t_aiButton->raise();\n\t}\n\n\tif (!_controls->isHidden()) {\n\t\t_controls->resizeToWidth(width());\n\t\t_controls->moveToLeft(\n\t\t\tst::boxPhotoPadding.left(),\n\t\t\tbottom - _controls->heightNoMargins());\n\t\tbottom -= _controls->heightNoMargins();\n\t}\n\t_scroll->resize(width(), bottom - st::boxPhotoPadding.top());\n\t_scroll->move(0, st::boxPhotoPadding.top());\n\n\tif (_content) {\n\t\t_content->resize(_scroll->width(), _content->height());\n\t}\n}\n\nvoid EditCaptionBox::setInnerFocus() {\n\t_field->setFocusFast();\n}\n\nbool EditCaptionBox::validateLength(const QString &text) const {\n\tconst auto session = &_controller->session();\n\tconst auto limit = Data::PremiumLimits(session).captionLengthCurrent();\n\tconst auto remove = int(text.size()) - limit;\n\tif (remove <= 0) {\n\t\treturn true;\n\t}\n\t_controller->show(\n\t\tBox(CaptionLimitReachedBox, session, remove, nullptr));\n\treturn false;\n}\n\nvoid EditCaptionBox::applyChanges() {\n\tif (!_preparedList.files.empty()) {\n\t\t_preparedList.files.front().spoiler = _mediaEditManager.spoilered();\n\t\t_preparedList.files.front().sendLargePhotos = _sendLargePhotos;\n\t}\n}\n\nvoid EditCaptionBox::save() {\n\tif (_saveRequestId) {\n\t\treturn;\n\t}\n\n\tconst auto item = _controller->session().data().message(\n\t\t_historyItem->fullId());\n\tif (!item) {\n\t\t_error = tr::lng_edit_deleted(tr::now);\n\t\tupdate();\n\t\treturn;\n\t}\n\n\tconst auto textWithTags = _field->getTextWithAppliedMarkdown();\n\tif (!validateLength(textWithTags.text)) {\n\t\treturn;\n\t}\n\tconst auto sending = TextWithEntities{\n\t\ttextWithTags.text,\n\t\tTextUtilities::ConvertTextTagsToEntities(textWithTags.tags)\n\t};\n\n\tauto options = Api::SendOptions();\n\toptions.suggest = _suggest;\n\toptions.scheduled = item->isScheduled() ? item->date() : 0;\n\toptions.shortcutId = item->shortcutId();\n\toptions.invertCaption = _mediaEditManager.invertCaption();\n\n\tif (!_preparedList.files.empty()) {\n\t\tif ((_albumType != Ui::AlbumType::None)\n\t\t\t\t&& !_preparedList.files.front().canBeInAlbumType(\n\t\t\t\t\t_albumType)) {\n\t\t\t_error = tr::lng_edit_media_album_error(tr::now);\n\t\t\tupdate();\n\t\t\treturn;\n\t\t}\n\t\tauto action = Api::SendAction(item->history(), options);\n\t\taction.replaceMediaOf = item->fullId().msg;\n\n\t\tStorage::ApplyModifications(_preparedList);\n\t\tif (!_preparedList.files.empty()) {\n\t\t\t_preparedList.files.front().spoiler = false;\n\t\t\tapplyChanges();\n\t\t}\n\n\t\tconst auto compressed = CanToggleCompressed(_albumType)\n\t\t\t? (!_asFile)\n\t\t\t: AlbumTypeCompressed(_albumType);\n\t\tsaveSendWaySettings();\n\t\t_controller->session().api().editMedia(\n\t\t\tstd::move(_preparedList),\n\t\t\t(compressed ? SendMediaType::Photo : SendMediaType::File),\n\t\t\t_field->getTextWithAppliedMarkdown(),\n\t\t\taction);\n\t\tcloseAfterSave();\n\t\treturn;\n\t}\n\n\tconst auto done = crl::guard(this, [=] {\n\t\t_saveRequestId = 0;\n\t\tcloseAfterSave();\n\t});\n\n\tconst auto fail = crl::guard(this, [=](const QString &error) {\n\t\t_saveRequestId = 0;\n\t\tif (ranges::contains(Api::kDefaultEditMessagesErrors, error)) {\n\t\t\t_error = tr::lng_edit_error(tr::now);\n\t\t\tupdate();\n\t\t} else if (error == u\"MESSAGE_NOT_MODIFIED\"_q) {\n\t\t\tcloseAfterSave();\n\t\t} else if (error == u\"MESSAGE_EMPTY\"_q) {\n\t\t\t_field->setFocus();\n\t\t\t_field->showError();\n\t\t\tupdate();\n\t\t} else {\n\t\t\t_error = tr::lng_edit_error(tr::now);\n\t\t\tupdate();\n\t\t}\n\t});\n\n\tlifetime().add([=] {\n\t\tif (_saveRequestId) {\n\t\t\tauto &session = _controller->session();\n\t\t\tsession.api().request(base::take(_saveRequestId)).cancel();\n\t\t}\n\t});\n\n\t_saveRequestId = Api::EditCaption(item, sending, options, done, fail);\n}\n\nvoid EditCaptionBox::closeAfterSave() {\n\tconst auto weak = base::make_weak(this);\n\tif (_saved) {\n\t\t_saved();\n\t}\n\tif (weak) {\n\t\tcloseBox();\n\t}\n}\n\nvoid EditCaptionBox::keyPressEvent(QKeyEvent *e) {\n\tconst auto ctrl = e->modifiers().testFlag(Qt::ControlModifier);\n\tif ((e->key() == Qt::Key_E) && ctrl) {\n\t\t_photoEditorOpens.fire({});\n\t} else if ((e->key() == Qt::Key_O) && ctrl) {\n\t\t_editMediaClicks.fire({});\n\t} else {\n\t\te->ignore();\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/edit_caption_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/controls/history_view_compose_media_edit_manager.h\"\n#include \"ui/layers/box_content.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n\nnamespace ChatHelpers {\nclass TabbedPanel;\nclass FieldAutocomplete;\n} // namespace ChatHelpers\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Data {\nclass PhotoMedia;\n} // namespace Data\n\nnamespace HistoryView::Controls {\nclass ComposeAiButton;\n} // namespace HistoryView::Controls\n\nnamespace Ui {\nclass AbstractSinglePreview;\nclass InputField;\nclass EmojiButton;\nclass VerticalLayout;\nenum class AlbumType;\n} // namespace Ui\n\nclass EditCaptionBox final : public Ui::BoxContent {\npublic:\n\tEditCaptionBox(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item,\n\t\tTextWithTags &&text,\n\t\tSuggestOptions suggest,\n\t\tbool spoilered,\n\t\tbool invertCaption,\n\t\tUi::PreparedList &&list,\n\t\tFn<void()> saved);\n\t~EditCaptionBox();\n\n\tstatic void StartMediaReplace(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFullMsgId itemId,\n\t\tTextWithTags text,\n\t\tSuggestOptions suggest,\n\t\tbool spoilered,\n\t\tbool invertCaption,\n\t\tFn<void()> saved);\n\tstatic void StartMediaReplace(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFullMsgId itemId,\n\t\tUi::PreparedList &&list,\n\t\tTextWithTags text,\n\t\tSuggestOptions suggest,\n\t\tbool spoilered,\n\t\tbool invertCaption,\n\t\tFn<void()> saved);\n\tstatic void StartPhotoEdit(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tstd::shared_ptr<Data::PhotoMedia> media,\n\t\tFullMsgId itemId,\n\t\tTextWithTags text,\n\t\tSuggestOptions suggest,\n\t\tbool spoilered,\n\t\tbool invertCaption,\n\t\tFn<void()> saved);\n\n\tvoid showFinished() override;\n\nprotected:\n\tvoid prepare() override;\n\tvoid setInnerFocus() override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tvoid rebuildPreview();\n\tvoid setupEditEventHandler();\n\tvoid setupPhotoEditorEventHandler();\n\tvoid setupEditCoverHandler();\n\tvoid setupClearCoverHandler();\n\tvoid setupField();\n\tvoid setupFieldAutocomplete();\n\tvoid setupControls();\n\tvoid setInitialText();\n\n\tvoid updateBoxSize();\n\tvoid captionResized();\n\n\tvoid setupEmojiPanel();\n\tvoid updateEmojiPanelGeometry();\n\tvoid emojiFilterForGeometry(not_null<QEvent*> event);\n\n\tvoid setupDragArea();\n\n\tbool validateLength(const QString &text) const;\n\tvoid applyChanges();\n\tvoid save();\n\tvoid closeAfterSave();\n\n\tbool fileFromClipboard(not_null<const QMimeData*> data);\n\n\t[[nodiscard]] int errorTopSkip() const;\n\t[[nodiscard]] bool hasSpoiler() const;\n\t[[nodiscard]] bool hasSendLargePhotosOption() const;\n\t[[nodiscard]] Ui::SendFilesWay currentSendWay() const;\n\tvoid saveSendWaySettings();\n\n\tbool setPreparedList(Ui::PreparedList &&list);\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<HistoryItem*> _historyItem;\n\tconst SuggestOptions _suggest;\n\tconst bool _isAllowedEditMedia;\n\tconst Ui::AlbumType _albumType;\n\n\tconst base::unique_qptr<Ui::VerticalLayout> _controls;\n\tconst base::unique_qptr<Ui::ScrollArea> _scroll;\n\tconst base::unique_qptr<Ui::InputField> _field;\n\tconst base::unique_qptr<Ui::EmojiButton> _emojiToggle;\n\tHistoryView::Controls::ComposeAiButton *_aiButton = nullptr;\n\n\tstd::unique_ptr<ChatHelpers::FieldAutocomplete> _autocomplete;\n\n\tbase::unique_qptr<Ui::AbstractSinglePreview> _content;\n\tbase::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;\n\tbase::unique_qptr<QObject> _emojiFilter;\n\n\tconst TextWithTags _initialText;\n\tUi::PreparedList _initialList;\n\tFn<void()> _saved;\n\n\tstd::shared_ptr<Data::PhotoMedia> _photoMedia;\n\n\tUi::PreparedList _preparedList;\n\tHistoryView::MediaEditManager _mediaEditManager;\n\n\tmtpRequestId _saveRequestId = 0;\n\n\tbase::Timer _checkChangedTimer;\n\tbool _isPhoto = false;\n\tbool _isVideo = false;\n\tbool _asFile = false;\n\tbool _sendLargePhotos = false;\n\n\tQString _error;\n\n\trpl::variable<int> _footerHeight = 0;\n\n\trpl::event_stream<> _editMediaClicks;\n\trpl::event_stream<> _photoEditorOpens;\n\trpl::event_stream<> _previewRebuilds;\n\trpl::event_stream<int> _contentHeight;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/edit_privacy_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/edit_privacy_box.h\"\n\n#include \"api/api_global_privacy.h\"\n#include \"apiwrap.h\"\n#include \"boxes/filters/edit_filter_chats_list.h\"\n#include \"boxes/peers/edit_peer_invite_link.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"settings/settings_privacy_controllers.h\"\n#include \"settings/sections/settings_privacy_security.h\"\n#include \"ui/boxes/peer_qr_box.h\"\n#include \"ui/controls/invite_link_buttons.h\"\n#include \"ui/controls/invite_link_label.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_window.h\"\n\nnamespace {\n\nconstexpr auto kPremiumsRowId = PeerId(FakeChatId(BareId(1))).value;\nconstexpr auto kMiniAppsRowId = PeerId(FakeChatId(BareId(2))).value;\nconstexpr auto kDefaultPrivateMessagesPrice = 10;\n\nusing Exceptions = Api::UserPrivacy::Exceptions;\n\nenum class SpecialRowType {\n\tPremiums,\n\tMiniApps,\n};\n\n[[nodiscard]] PaintRoundImageCallback GeneratePremiumsUserpicCallback(\n\t\tbool forceRound) {\n\treturn [=](QPainter &p, int x, int y, int outerWidth, int size) {\n\t\tauto gradient = QLinearGradient(\n\t\t\tQPointF(x, y),\n\t\t\tQPointF(x + size, y + size));\n\t\tgradient.setStops(Ui::Premium::ButtonGradientStops());\n\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(gradient);\n\t\tif (forceRound) {\n\t\t\tp.drawEllipse(x, y, size, size);\n\t\t} else {\n\t\t\tconst auto radius = size * Ui::ForumUserpicRadiusMultiplier();\n\t\t\tp.drawRoundedRect(x, y, size, size, radius, radius);\n\t\t}\n\t\tst::settingsPrivacyPremium.paintInCenter(p, QRect(x, y, size, size));\n\t};\n}\n\n[[nodiscard]] PaintRoundImageCallback GenerateMiniAppsUserpicCallback(\n\t\tbool forceRound) {\n\treturn [=](QPainter &p, int x, int y, int outerWidth, int size) {\n\t\tconst auto &color1 = st::historyPeer6UserpicBg;\n\t\tconst auto &color2 = st::historyPeer6UserpicBg2;\n\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tauto gradient = QLinearGradient(x, y, x, y + size);\n\t\tgradient.setStops({ { 0., color1->c }, { 1., color2->c } });\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(gradient);\n\t\tif (forceRound) {\n\t\t\tp.drawEllipse(x, y, size, size);\n\t\t} else {\n\t\t\tconst auto radius = size * Ui::ForumUserpicRadiusMultiplier();\n\t\t\tp.drawRoundedRect(x, y, size, size, radius, radius);\n\t\t}\n\t\tst::windowFilterTypeBots.paintInCenter(p, QRect(x, y, size, size));\n\t};\n}\n\nvoid CreateRadiobuttonLock(\n\t\tnot_null<Ui::RpWidget*> widget,\n\t\tconst style::Checkbox &st) {\n\tconst auto lock = Ui::CreateChild<Ui::RpWidget>(widget.get());\n\tlock->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tlock->resize(st::defaultRadio.diameter, st::defaultRadio.diameter);\n\n\twidget->sizeValue(\n\t) | rpl::on_next([=, &st](QSize size) {\n\t\tlock->move(st.checkPosition);\n\t}, lock->lifetime());\n\n\tlock->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(lock);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto &icon = st::messagePrivacyLock;\n\t\tconst auto size = st::defaultRadio.diameter;\n\t\tconst auto image = icon.instance(st::checkboxFg->c);\n\t\tp.drawImage(QRectF(\n\t\t\t(size - icon.width()) / 2.,\n\t\t\t(size - icon.height()) / 2.,\n\t\t\ticon.width(),\n\t\t\ticon.height()), image);\n\t}, lock->lifetime());\n}\n\nvoid AddPremiumRequiredRow(\n\t\tnot_null<Ui::RpWidget*> widget,\n\t\tnot_null<Main::Session*> session,\n\t\tFn<void()> clickedCallback,\n\t\tFn<void()> setDefaultOption,\n\t\tconst style::Checkbox &st) {\n\tconst auto row = Ui::CreateChild<Ui::AbstractButton>(widget.get());\n\n\twidget->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\trow->resize(s);\n\t}, row->lifetime());\n\trow->setClickedCallback(std::move(clickedCallback));\n\n\tCreateRadiobuttonLock(row, st);\n\n\tData::AmPremiumValue(\n\t\tsession\n\t) | rpl::on_next([=](bool premium) {\n\t\trow->setVisible(!premium);\n\t\tif (!premium) {\n\t\t\tsetDefaultOption();\n\t\t}\n\t}, row->lifetime());\n}\n\nclass PrivacyExceptionsBoxController : public ChatsListBoxController {\npublic:\n\tPrivacyExceptionsBoxController(\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<QString> title,\n\t\tconst Exceptions &selected,\n\t\tstd::optional<SpecialRowType> allowChooseSpecial);\n\n\tMain::Session &session() const override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tbool isForeignRow(PeerListRowId itemId) override;\n\tbool handleDeselectForeignRow(PeerListRowId itemId) override;\n\n\t[[nodiscard]] bool premiumsSelected() const;\n\t[[nodiscard]] bool miniAppsSelected() const;\n\nprotected:\n\tvoid prepareViewHook() override;\n\tstd::unique_ptr<Row> createRow(not_null<History*> history) override;\n\nprivate:\n\t[[nodiscard]] object_ptr<Ui::RpWidget> prepareSpecialRowList(\n\t\tSpecialRowType type);\n\n\tconst not_null<Main::Session*> _session;\n\trpl::producer<QString> _title;\n\tExceptions _selected;\n\tstd::optional<SpecialRowType> _allowChooseSpecial;\n\n\tPeerListContentDelegate *_typesDelegate = nullptr;\n\tFn<void(PeerListRowId)> _deselectOption;\n\n};\n\nstruct RowSelectionChange {\n\tnot_null<PeerListRow*> row;\n\tbool checked = false;\n};\n\nclass SpecialRow final : public PeerListRow {\npublic:\n\texplicit SpecialRow(SpecialRowType type);\n\n\tQString generateName() override;\n\tQString generateShortName() override;\n\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\tbool useForumLikeUserpic() const override;\n\n};\n\nclass TypesController final : public PeerListController {\npublic:\n\tTypesController(not_null<Main::Session*> session, SpecialRowType type);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\n\t[[nodiscard]] bool specialSelected() const;\n\t[[nodiscard]] rpl::producer<bool> specialChanges() const;\n\t[[nodiscard]] auto rowSelectionChanges() const\n\t\t-> rpl::producer<RowSelectionChange>;\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\tconst SpecialRowType _type;\n\n\trpl::event_stream<> _selectionChanged;\n\trpl::event_stream<RowSelectionChange> _rowSelectionChanges;\n\n};\n\nSpecialRow::SpecialRow(SpecialRowType type)\n: PeerListRow((type == SpecialRowType::Premiums)\n\t? kPremiumsRowId\n\t: kMiniAppsRowId) {\n\tsetCustomStatus((id() == kPremiumsRowId)\n\t\t? tr::lng_edit_privacy_premium_status(tr::now)\n\t\t: tr::lng_edit_privacy_miniapps_status(tr::now));\n}\n\nQString SpecialRow::generateName() {\n\treturn (id() == kPremiumsRowId)\n\t\t? tr::lng_edit_privacy_premium(tr::now)\n\t\t: tr::lng_edit_privacy_miniapps(tr::now);\n}\n\nQString SpecialRow::generateShortName() {\n\treturn generateName();\n}\n\nPaintRoundImageCallback SpecialRow::generatePaintUserpicCallback(\n\t\tbool forceRound) {\n\treturn (id() == kPremiumsRowId)\n\t\t? GeneratePremiumsUserpicCallback(forceRound)\n\t\t: GenerateMiniAppsUserpicCallback(forceRound);\n}\n\nbool SpecialRow::useForumLikeUserpic() const {\n\treturn true;\n}\n\nTypesController::TypesController(\n\tnot_null<Main::Session*> session,\n\tSpecialRowType type)\n: _session(session)\n, _type(type) {\n}\n\nMain::Session &TypesController::session() const {\n\treturn *_session;\n}\n\nvoid TypesController::prepare() {\n\tdelegate()->peerListAppendRow(std::make_unique<SpecialRow>(_type));\n\tdelegate()->peerListRefreshRows();\n}\n\nbool TypesController::specialSelected() const {\n\tconst auto premiums = (_type == SpecialRowType::Premiums);\n\tconst auto row = delegate()->peerListFindRow(premiums\n\t\t? kPremiumsRowId\n\t\t: kMiniAppsRowId);\n\tAssert(row != nullptr);\n\n\treturn row->checked();\n}\n\nvoid TypesController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto checked = !row->checked();\n\tdelegate()->peerListSetRowChecked(row, checked);\n\t_rowSelectionChanges.fire({ row, checked });\n}\n\nrpl::producer<bool> TypesController::specialChanges() const {\n\treturn _rowSelectionChanges.events(\n\t) | rpl::map([=] {\n\t\treturn specialSelected();\n\t});\n}\n\nauto TypesController::rowSelectionChanges() const\n-> rpl::producer<RowSelectionChange> {\n\treturn _rowSelectionChanges.events();\n}\n\nPrivacyExceptionsBoxController::PrivacyExceptionsBoxController(\n\tnot_null<Main::Session*> session,\n\trpl::producer<QString> title,\n\tconst Exceptions &selected,\n\tstd::optional<SpecialRowType> allowChooseSpecial)\n: ChatsListBoxController(session)\n, _session(session)\n, _title(std::move(title))\n, _selected(selected)\n, _allowChooseSpecial(allowChooseSpecial) {\n}\n\nMain::Session &PrivacyExceptionsBoxController::session() const {\n\treturn *_session;\n}\n\nvoid PrivacyExceptionsBoxController::prepareViewHook() {\n\tdelegate()->peerListSetTitle(std::move(_title));\n\tif (_allowChooseSpecial || _selected.premiums || _selected.miniapps) {\n\t\tdelegate()->peerListSetAboveWidget(prepareSpecialRowList(\n\t\t\t_allowChooseSpecial.value_or(_selected.premiums\n\t\t\t\t? SpecialRowType::Premiums\n\t\t\t\t: SpecialRowType::MiniApps)));\n\t}\n\tdelegate()->peerListAddSelectedPeers(_selected.peers);\n}\n\nbool PrivacyExceptionsBoxController::isForeignRow(PeerListRowId itemId) {\n\treturn (itemId == kPremiumsRowId)\n\t\t|| (itemId == kMiniAppsRowId);\n}\n\nbool PrivacyExceptionsBoxController::handleDeselectForeignRow(\n\t\tPeerListRowId itemId) {\n\tif (isForeignRow(itemId)) {\n\t\t_deselectOption(itemId);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nauto PrivacyExceptionsBoxController::prepareSpecialRowList(\n\tSpecialRowType type)\n-> object_ptr<Ui::RpWidget> {\n\tauto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tconst auto container = result.data();\n\tcontainer->add(CreatePeerListSectionSubtitle(\n\t\tcontainer,\n\t\ttr::lng_edit_privacy_user_types()));\n\tauto &lifetime = container->lifetime();\n\t_typesDelegate = lifetime.make_state<PeerListContentDelegateSimple>();\n\tconst auto controller = lifetime.make_state<TypesController>(\n\t\t&session(),\n\t\ttype);\n\tconst auto content = result->add(object_ptr<PeerListContent>(\n\t\tcontainer,\n\t\tcontroller));\n\t_typesDelegate->setContent(content);\n\tcontroller->setDelegate(_typesDelegate);\n\n\tconst auto selectType = [&](PeerListRowId id) {\n\t\tconst auto row = _typesDelegate->peerListFindRow(id);\n\t\tif (row) {\n\t\t\tcontent->changeCheckState(row, true, anim::type::instant);\n\t\t\tthis->delegate()->peerListSetForeignRowChecked(\n\t\t\t\trow,\n\t\t\t\ttrue,\n\t\t\t\tanim::type::instant);\n\t\t}\n\t};\n\tif (_selected.premiums) {\n\t\tselectType(kPremiumsRowId);\n\t} else if (_selected.miniapps) {\n\t\tselectType(kMiniAppsRowId);\n\t}\n\tcontainer->add(CreatePeerListSectionSubtitle(\n\t\tcontainer,\n\t\ttr::lng_edit_privacy_users_and_groups()));\n\n\tcontroller->specialChanges(\n\t) | rpl::on_next([=](bool chosen) {\n\t\tif (type == SpecialRowType::Premiums) {\n\t\t\t_selected.premiums = chosen;\n\t\t} else {\n\t\t\t_selected.miniapps = chosen;\n\t\t}\n\t}, lifetime);\n\n\tcontroller->rowSelectionChanges(\n\t) | rpl::on_next([=](RowSelectionChange update) {\n\t\tthis->delegate()->peerListSetForeignRowChecked(\n\t\t\tupdate.row,\n\t\t\tupdate.checked,\n\t\t\tanim::type::normal);\n\t}, lifetime);\n\n\t_deselectOption = [=](PeerListRowId itemId) {\n\t\tif (const auto row = _typesDelegate->peerListFindRow(itemId)) {\n\t\t\tif (itemId == kPremiumsRowId) {\n\t\t\t\t_selected.premiums = false;\n\t\t\t} else if (itemId == kMiniAppsRowId) {\n\t\t\t\t_selected.miniapps = false;\n\t\t\t}\n\t\t\t_typesDelegate->peerListSetRowChecked(row, false);\n\t\t}\n\t};\n\n\treturn result;\n}\n\nbool PrivacyExceptionsBoxController::premiumsSelected() const {\n\treturn _selected.premiums;\n}\n\nbool PrivacyExceptionsBoxController::miniAppsSelected() const {\n\treturn _selected.miniapps;\n}\n\nvoid PrivacyExceptionsBoxController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto peer = row->peer();\n\n\t// This call may delete row, if it was a search result row.\n\tdelegate()->peerListSetRowChecked(row, !row->checked());\n\n\tif (const auto channel = peer->asChannel()) {\n\t\tif (!channel->membersCountKnown()) {\n\t\t\tchannel->updateFull();\n\t\t}\n\t}\n}\n\nauto PrivacyExceptionsBoxController::createRow(not_null<History*> history)\n-> std::unique_ptr<Row> {\n\tconst auto peer = history->peer;\n\tif (peer->isSelf() || peer->isRepliesChat() || peer->isVerifyCodes()) {\n\t\treturn nullptr;\n\t} else if (!peer->isUser()\n\t\t&& !peer->isChat()\n\t\t&& !peer->isMegagroup()) {\n\t\treturn nullptr;\n\t}\n\tauto result = std::make_unique<Row>(history);\n\tconst auto count = [&] {\n\t\tif (const auto chat = history->peer->asChat()) {\n\t\t\treturn chat->count;\n\t\t} else if (const auto channel = history->peer->asChannel()) {\n\t\t\treturn channel->membersCountKnown()\n\t\t\t\t? channel->membersCount()\n\t\t\t\t: 0;\n\t\t}\n\t\treturn 0;\n\t}();\n\tif (count > 0) {\n\t\tresult->setCustomStatus(\n\t\t\ttr::lng_chat_status_members(tr::now, lt_count_decimal, count));\n\t}\n\treturn result;\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeChargeStarsSlider(\n\t\tQWidget *parent,\n\t\tnot_null<const style::MediaSlider*> sliderStyle,\n\t\tnot_null<const style::FlatLabel*> labelStyle,\n\t\tint valuesCount,\n\t\tFn<int(int)> valueByIndex,\n\t\tint value,\n\t\tint minValue,\n\t\tint maxValue,\n\t\tFn<void(int)> valueProgress,\n\t\tFn<void(int)> valueFinished) {\n\tauto result = object_ptr<Ui::VerticalLayout>(parent);\n\tconst auto raw = result.data();\n\n\tconst auto labels = raw->add(object_ptr<Ui::RpWidget>(raw));\n\tconst auto min = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\tLang::FormatCountDecimal(minValue),\n\t\t*labelStyle);\n\tconst auto max = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\tLang::FormatCountDecimal(maxValue),\n\t\t*labelStyle);\n\tconst auto current = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\tLang::FormatCountDecimal(value),\n\t\t*labelStyle);\n\tmin->setTextColorOverride(st::windowSubTextFg->c);\n\tmax->setTextColorOverride(st::windowSubTextFg->c);\n\tconst auto slider = raw->add(object_ptr<Ui::MediaSliderWheelless>(\n\t\traw,\n\t\t*sliderStyle));\n\tlabels->resize(\n\t\tlabels->width(),\n\t\tcurrent->height() + st::defaultVerticalListSkip);\n\tstruct State {\n\t\tint indexMin = 0;\n\t\tint index = 0;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>();\n\tconst auto updateByIndex = [=] {\n\t\tconst auto outer = labels->width();\n\t\tconst auto minWidth = min->width();\n\t\tconst auto maxWidth = max->width();\n\t\tconst auto currentWidth = current->width();\n\t\tif (minWidth + maxWidth + currentWidth > outer) {\n\t\t\treturn;\n\t\t}\n\n\t\tmin->moveToLeft(0, 0, outer);\n\t\tmax->moveToRight(0, 0, outer);\n\t\tcurrent->moveToLeft((outer - current->width()) / 2, 0, outer);\n\t};\n\tconst auto updateByValue = [=](int value) {\n\t\tcurrent->setText(value > 0\n\t\t\t? tr::lng_action_gift_for_stars(tr::now, lt_count_decimal, value)\n\t\t\t: tr::lng_manage_monoforum_free(tr::now));\n\n\t\tstate->index = 0;\n\t\tauto maxIndex = valuesCount - 1;\n\t\twhile (state->index < maxIndex) {\n\t\t\tconst auto mid = (state->index + maxIndex) / 2;\n\t\t\tconst auto midValue = valueByIndex(mid);\n\t\t\tif (midValue == value) {\n\t\t\t\tstate->index = mid;\n\t\t\t\tbreak;\n\t\t\t} else if (midValue < value) {\n\t\t\t\tstate->index = mid + 1;\n\t\t\t} else {\n\t\t\t\tmaxIndex = mid - 1;\n\t\t\t}\n\t\t}\n\t\tupdateByIndex();\n\t};\n\tconst auto progress = [=](int value) {\n\t\tupdateByValue(value);\n\t\tvalueProgress(value);\n\t};\n\tconst auto finished = [=](int value) {\n\t\tupdateByValue(value);\n\t\tvalueFinished(value);\n\t};\n\tstyle::PaletteChanged() | rpl::on_next([=] {\n\t\tmin->setTextColorOverride(st::windowSubTextFg->c);\n\t\tmax->setTextColorOverride(st::windowSubTextFg->c);\n\t}, raw->lifetime());\n\tupdateByValue(value);\n\tstate->indexMin = 0;\n\n\tslider->setPseudoDiscrete(\n\t\tvaluesCount,\n\t\tvalueByIndex,\n\t\tvalue,\n\t\tprogress,\n\t\tfinished,\n\t\tstate->indexMin);\n\tslider->resize(slider->width(), sliderStyle->seekSize.height());\n\n\traw->widthValue() | rpl::on_next([=](int width) {\n\t\tlabels->resizeToWidth(width);\n\t\tupdateByIndex();\n\t}, slider->lifetime());\n\n\treturn result;\n}\n\nvoid EditNoPaidMessagesExceptions(\n\t\tnot_null<Window::SessionController*> window,\n\t\tconst Api::UserPrivacy::Rule &value) {\n\tauto controller = std::make_unique<PrivacyExceptionsBoxController>(\n\t\t&window->session(),\n\t\ttr::lng_messages_privacy_remove_fee(),\n\t\tvalue.always,\n\t\tstd::optional<SpecialRowType>());\n\tauto initBox = [=, controller = controller.get()](\n\t\t\tnot_null<PeerListBox*> box) {\n\t\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\t\tauto copy = value;\n\t\t\tauto &setTo = copy.always;\n\t\t\tsetTo.peers = box->collectSelectedRows();\n\t\t\tsetTo.premiums = false;\n\t\t\tsetTo.miniapps = false;\n\t\t\tauto &removeFrom = copy.never;\n\t\t\tfor (const auto &peer : setTo.peers) {\n\t\t\t\tremoveFrom.peers.erase(\n\t\t\t\t\tranges::remove(removeFrom.peers, peer),\n\t\t\t\t\tend(removeFrom.peers));\n\t\t\t}\n\t\t\twindow->session().api().userPrivacy().save(\n\t\t\t\tApi::UserPrivacy::Key::NoPaidMessages,\n\t\t\t\tcopy);\n\t\t\tbox->closeBox();\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t};\n\twindow->show(\n\t\tBox<PeerListBox>(std::move(controller), std::move(initBox)));\n}\n\n} // namespace\n\nbool EditPrivacyController::hasOption(Option option) const {\n\treturn (option != Option::CloseFriends);\n}\n\nQString EditPrivacyController::optionLabel(Option option) const {\n\tswitch (option) {\n\tcase Option::Everyone: return tr::lng_edit_privacy_everyone(tr::now);\n\tcase Option::Contacts: return tr::lng_edit_privacy_contacts(tr::now);\n\tcase Option::CloseFriends:\n\t\treturn tr::lng_edit_privacy_close_friends(tr::now);\n\tcase Option::Nobody: return tr::lng_edit_privacy_nobody(tr::now);\n\t}\n\tUnexpected(\"Option value in optionsLabelKey.\");\n}\n\nEditPrivacyBox::EditPrivacyBox(\n\tQWidget*,\n\tnot_null<Window::SessionController*> window,\n\tstd::unique_ptr<EditPrivacyController> controller,\n\tconst Value &value)\n: _window(window)\n, _controller(std::move(controller))\n, _value(value) {\n\tif (_controller->allowPremiumsToggle(Exception::Always)\n\t\t&& _value.option == Option::Everyone) {\n\t\t// If we switch from Everyone to Contacts or Nobody suggest Premiums.\n\t\t_value.always.premiums = true;\n\t}\n\tif (_controller->allowMiniAppsToggle(Exception::Always)\n\t\t&& _value.option == Option::Everyone) {\n\t\t// If we switch from Everyone to Contacts or Nobody suggest MiniApps.\n\t\t_value.always.miniapps = true;\n\t}\n}\n\nvoid EditPrivacyBox::prepare() {\n\t_controller->setView(this);\n\n\tsetupContent();\n}\n\nvoid EditPrivacyBox::editExceptions(\n\t\tException exception,\n\t\tFn<void()> done) {\n\tauto controller = std::make_unique<PrivacyExceptionsBoxController>(\n\t\t&_window->session(),\n\t\t_controller->exceptionBoxTitle(exception),\n\t\texceptions(exception),\n\t\t(_controller->allowPremiumsToggle(exception)\n\t\t\t? SpecialRowType::Premiums\n\t\t\t: _controller->allowMiniAppsToggle(exception)\n\t\t\t? SpecialRowType::MiniApps\n\t\t\t: std::optional<SpecialRowType>()));\n\tauto initBox = [=, controller = controller.get()](\n\t\t\tnot_null<PeerListBox*> box) {\n\t\tbox->addButton(tr::lng_settings_save(), crl::guard(this, [=] {\n\t\t\tauto &setTo = exceptions(exception);\n\t\t\tsetTo.peers = box->collectSelectedRows();\n\t\t\tsetTo.premiums = controller->premiumsSelected();\n\t\t\tsetTo.miniapps = controller->miniAppsSelected();\n\t\t\tconst auto type = [&] {\n\t\t\t\tswitch (exception) {\n\t\t\t\tcase Exception::Always: return Exception::Never;\n\t\t\t\tcase Exception::Never: return Exception::Always;\n\t\t\t\t}\n\t\t\t\tUnexpected(\"Invalid exception value.\");\n\t\t\t}();\n\t\t\tauto &removeFrom = exceptions(type);\n\t\t\tfor (const auto &peer : exceptions(exception).peers) {\n\t\t\t\tremoveFrom.peers.erase(\n\t\t\t\t\tranges::remove(removeFrom.peers, peer),\n\t\t\t\t\tend(removeFrom.peers));\n\t\t\t}\n\t\t\tif (setTo.premiums) {\n\t\t\t\tremoveFrom.premiums = false;\n\t\t\t}\n\t\t\tif (setTo.miniapps) {\n\t\t\t\tremoveFrom.miniapps = false;\n\t\t\t}\n\t\t\tdone();\n\t\t\tbox->closeBox();\n\t\t}));\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t};\n\t_window->show(\n\t\tBox<PeerListBox>(std::move(controller), std::move(initBox)));\n}\n\nEditPrivacyBox::Exceptions &EditPrivacyBox::exceptions(Exception exception) {\n\tswitch (exception) {\n\tcase Exception::Always: return _value.always;\n\tcase Exception::Never: return _value.never;\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nbool EditPrivacyBox::showExceptionLink(Exception exception) const {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn (_value.option == Option::Contacts)\n\t\t\t|| (_value.option == Option::CloseFriends)\n\t\t\t|| (_value.option == Option::Nobody);\n\tcase Exception::Never:\n\t\treturn (_value.option == Option::Everyone)\n\t\t\t|| (_value.option == Option::Contacts)\n\t\t\t|| (_value.option == Option::CloseFriends);\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nUi::Radioenum<EditPrivacyBox::Option> *EditPrivacyBox::AddOption(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<EditPrivacyController*> controller,\n\t\tconst std::shared_ptr<Ui::RadioenumGroup<Option>> &group,\n\t\tOption option) {\n\treturn container->add(\n\t\tobject_ptr<Ui::Radioenum<Option>>(\n\t\t\tcontainer,\n\t\t\tgroup,\n\t\t\toption,\n\t\t\tcontroller->optionLabel(option),\n\t\t\tst::settingsPrivacyOption),\n\t\t(st::settingsSendTypePadding + style::margins(\n\t\t\t-st::lineWidth,\n\t\t\tst::settingsPrivacySkipTop,\n\t\t\t0,\n\t\t\t0)));\n}\n\nUi::FlatLabel *EditPrivacyBox::addLabel(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<TextWithEntities> text,\n\t\tint topSkip) {\n\tif (!text) {\n\t\treturn nullptr;\n\t}\n\tauto label = object_ptr<Ui::FlatLabel>(\n\t\tcontainer,\n\t\trpl::duplicate(text),\n\t\tst::boxDividerLabel);\n\tconst auto result = label.data();\n\tcontainer->add(\n\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\tcontainer,\n\t\t\tstd::move(label),\n\t\t\tst::defaultBoxDividerLabelPadding),\n\t\t{ 0, topSkip, 0, 0 });\n\treturn result;\n}\n\nUi::FlatLabel *EditPrivacyBox::addLabelOrDivider(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<TextWithEntities> text,\n\t\tint topSkip) {\n\tif (const auto result = addLabel(container, std::move(text), topSkip)) {\n\t\treturn result;\n\t}\n\tcontainer->add(\n\t\tobject_ptr<Ui::BoxContentDivider>(container),\n\t\t{ 0, topSkip, 0, 0 });\n\treturn nullptr;\n}\n\nvoid EditPrivacyBox::setupContent() {\n\tusing namespace Settings;\n\n\tsetTitle(_controller->title());\n\n\tauto wrap = object_ptr<Ui::VerticalLayout>(this);\n\tconst auto content = wrap.data();\n\tsetInnerWidget(object_ptr<Ui::OverrideMargins>(\n\t\tthis,\n\t\tstd::move(wrap)));\n\n\tconst auto group = std::make_shared<Ui::RadioenumGroup<Option>>(\n\t\t_value.option);\n\tconst auto toggle = Ui::CreateChild<rpl::event_stream<Option>>(content);\n\tgroup->setChangedCallback([=](Option value) {\n\t\t_value.option = value;\n\t\ttoggle->fire_copy(value);\n\t});\n\tauto optionValue = toggle->events_starting_with_copy(_value.option);\n\n\tconst auto addOptionRow = [&](Option option) {\n\t\treturn (_controller->hasOption(option) || (_value.option == option))\n\t\t\t? AddOption(content, _controller.get(), group, option)\n\t\t\t: nullptr;\n\t};\n\tconst auto addExceptionLink = [=](Exception exception) {\n\t\tconst auto update = Ui::CreateChild<rpl::event_stream<>>(content);\n\t\tauto label = update->events_starting_with({}) | rpl::map([=] {\n\t\t\tconst auto &value = exceptions(exception);\n\t\t\tconst auto count = Settings::ExceptionUsersCount(value.peers);\n\t\t\tconst auto users = count\n\t\t\t\t? tr::lng_edit_privacy_exceptions_count(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tcount)\n\t\t\t\t: tr::lng_edit_privacy_exceptions_add(tr::now);\n\t\t\treturn value.premiums\n\t\t\t\t? (!count\n\t\t\t\t\t? tr::lng_edit_privacy_premium(tr::now)\n\t\t\t\t\t: tr::lng_edit_privacy_exceptions_premium_and(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_users,\n\t\t\t\t\t\tusers))\n\t\t\t\t: value.miniapps\n\t\t\t\t? (!count\n\t\t\t\t\t? tr::lng_edit_privacy_miniapps(tr::now)\n\t\t\t\t\t: tr::lng_edit_privacy_exceptions_miniapps_and(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_users,\n\t\t\t\t\t\tusers))\n\t\t\t\t: users;\n\t\t});\n\t\t_controller->handleExceptionsChange(\n\t\t\texception,\n\t\t\tupdate->events_starting_with({}) | rpl::map([=] {\n\t\t\t\treturn Settings::ExceptionUsersCount(\n\t\t\t\t\texceptions(exception).peers);\n\t\t\t}));\n\t\tauto text = _controller->exceptionButtonTextKey(exception);\n\t\tconst auto button = content->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Button>>(\n\t\t\t\tcontent,\n\t\t\t\tobject_ptr<Button>(\n\t\t\t\t\tcontent,\n\t\t\t\t\trpl::duplicate(text),\n\t\t\t\t\tst::settingsButtonNoIcon)));\n\t\tCreateRightLabel(\n\t\t\tbutton->entity(),\n\t\t\tstd::move(label),\n\t\t\tst::settingsButtonNoIcon,\n\t\t\tstd::move(text));\n\t\tbutton->toggleOn(rpl::duplicate(\n\t\t\toptionValue\n\t\t) | rpl::map([=] {\n\t\t\treturn showExceptionLink(exception);\n\t\t}))->entity()->addClickHandler([=] {\n\t\t\teditExceptions(exception, [=] { update->fire({}); });\n\t\t});\n\t\treturn button;\n\t};\n\n\tauto above = _controller->setupAboveWidget(\n\t\t_window,\n\t\tcontent,\n\t\trpl::duplicate(optionValue),\n\t\tgetDelegate()->outerContainer());\n\tif (above) {\n\t\tcontent->add(std::move(above));\n\t}\n\n\tUi::AddSubsectionTitle(\n\t\tcontent,\n\t\t_controller->optionsTitleKey(),\n\t\t{ 0, st::settingsPrivacySkipTop, 0, 0 });\n\n\tconst auto options = {\n\t\tOption::Everyone,\n\t\tOption::Contacts,\n\t\tOption::CloseFriends,\n\t\tOption::Nobody,\n\t};\n\tfor (const auto &option : options) {\n\t\tif (const auto row = addOptionRow(option)) {\n\t\t\tconst auto premiumCallback = _controller->premiumClickedCallback(\n\t\t\t\toption,\n\t\t\t\t_window);\n\t\t\tif (premiumCallback) {\n\t\t\t\tAddPremiumRequiredRow(\n\t\t\t\t\trow,\n\t\t\t\t\t&_window->session(),\n\t\t\t\t\tpremiumCallback,\n\t\t\t\t\t[=] { group->setValue(Option::Everyone); },\n\t\t\t\t\tst::messagePrivacyCheck);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst auto warning = addLabelOrDivider(\n\t\tcontent,\n\t\t_controller->warning(),\n\t\tst::defaultVerticalListSkip + st::settingsPrivacySkipTop);\n\tif (warning) {\n\t\t_controller->prepareWarningLabel(warning);\n\t}\n\n\tauto middle = _controller->setupMiddleWidget(\n\t\t_window,\n\t\tcontent,\n\t\trpl::duplicate(optionValue));\n\tif (middle) {\n\t\tcontent->add(std::move(middle));\n\t}\n\n\tUi::AddSkip(content);\n\tUi::AddSubsectionTitle(\n\t\tcontent,\n\t\ttr::lng_edit_privacy_exceptions(),\n\t\t{ 0, st::settingsPrivacySkipTop, 0, 0 });\n\tconst auto always = addExceptionLink(Exception::Always);\n\tconst auto never = addExceptionLink(Exception::Never);\n\t_always = always->entity();\n\t_never = never->entity();\n\taddLabel(\n\t\tcontent,\n\t\t_controller->exceptionsDescription() | rpl::map(tr::marked),\n\t\tst::defaultVerticalListSkip);\n\n\tauto below = _controller->setupBelowWidget(\n\t\t_window,\n\t\tcontent,\n\t\trpl::duplicate(optionValue));\n\tif (below) {\n\t\tcontent->add(std::move(below));\n\t}\n\n\taddButton(tr::lng_settings_save(), [=] {\n\t\tconst auto someAreDisallowed = (_value.option != Option::Everyone)\n\t\t\t|| !_value.never.peers.empty();\n\t\t_controller->confirmSave(someAreDisallowed, crl::guard(this, [=] {\n\t\t\t_value.ignoreAlways = !showExceptionLink(Exception::Always);\n\t\t\t_value.ignoreNever = !showExceptionLink(Exception::Never);\n\n\t\t\t_controller->saveAdditional();\n\t\t\t_window->session().api().userPrivacy().save(\n\t\t\t\t_controller->key(),\n\t\t\t\t_value);\n\t\t\tcloseBox();\n\t\t}));\n\t});\n\taddButton(tr::lng_cancel(), [this] { closeBox(); });\n\n\tconst auto linkHeight = st::settingsButtonNoIcon.padding.top()\n\t\t+ st::settingsButtonNoIcon.height\n\t\t+ st::settingsButtonNoIcon.padding.bottom();\n\n\twidthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tcontent->resizeToWidth(width);\n\t}, content->lifetime());\n\n\tcontent->heightValue(\n\t) | rpl::map([=](int height) {\n\t\treturn height - always->height() - never->height() + 2 * linkHeight;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](int height) {\n\t\tsetDimensions(st::boxWideWidth, height);\n\t}, content->lifetime());\n}\n\nvoid EditPrivacyBox::showFinished() {\n\t_window->checkHighlightControl(u\"privacy/always\"_q, _always.data());\n\t_window->checkHighlightControl(u\"privacy/never\"_q, _never.data());\n\t_controller->checkHighlightControls(_window);\n}\n\nvoid EditMessagesPrivacyBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &highlightControlId) {\n\tbox->setTitle(tr::lng_messages_privacy_title());\n\tbox->setWidth(st::boxWideWidth);\n\n\tconstexpr auto kOptionAll = 0;\n\tconstexpr auto kOptionPremium = 1;\n\tconstexpr auto kOptionCharge = 2;\n\n\tconst auto session = &controller->session();\n\tconst auto allowed = [=] {\n\t\treturn session->premium()\n\t\t\t|| session->appConfig().newRequirePremiumFree();\n\t};\n\tconst auto privacy = &session->api().globalPrivacy();\n\tconst auto inner = box->verticalLayout();\n\tinner->add(object_ptr<Ui::PlainShadow>(box));\n\n\tauto highlightCharged = (Ui::RpWidget*)nullptr;\n\tauto highlightRemoveFee = (Ui::RpWidget*)nullptr;\n\n\tUi::AddSkip(inner, st::messagePrivacyTopSkip);\n\tUi::AddSubsectionTitle(inner, tr::lng_messages_privacy_subtitle());\n\tconst auto group = std::make_shared<Ui::RadiobuttonGroup>(\n\t\t(!allowed()\n\t\t\t? kOptionAll\n\t\t\t: privacy->newRequirePremiumCurrent()\n\t\t\t? kOptionPremium\n\t\t\t: privacy->newChargeStarsCurrent()\n\t\t\t? kOptionCharge\n\t\t\t: kOptionAll));\n\tinner->add(\n\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\tinner,\n\t\t\tgroup,\n\t\t\tkOptionAll,\n\t\t\ttr::lng_messages_privacy_everyone(tr::now),\n\t\t\tst::messagePrivacyCheck),\n\t\tst::settingsSendTypePadding);\n\tconst auto restricted = inner->add(\n\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\tinner,\n\t\t\tgroup,\n\t\t\tkOptionPremium,\n\t\t\ttr::lng_messages_privacy_restricted(tr::now),\n\t\t\tst::messagePrivacyCheck),\n\t\tst::settingsSendTypePadding + style::margins(\n\t\t\t0,\n\t\t\tst::messagePrivacyRadioSkip,\n\t\t\t0,\n\t\t\tst::messagePrivacyBottomSkip));\n\n\tUi::AddDividerText(inner, tr::lng_messages_privacy_about());\n\n\tconst auto available = session->appConfig().paidMessagesAvailable();\n\n\tconst auto charged = available\n\t\t? inner->add(\n\t\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\t\tinner,\n\t\t\t\tgroup,\n\t\t\t\tkOptionCharge,\n\t\t\t\ttr::lng_messages_privacy_charge(tr::now),\n\t\t\t\tst::messagePrivacyCheck),\n\t\t\tst::settingsSendTypePadding + style::margins(\n\t\t\t\t0,\n\t\t\t\tst::messagePrivacyBottomSkip,\n\t\t\t\t0,\n\t\t\t\tst::messagePrivacyBottomSkip))\n\t\t: nullptr;\n\thighlightCharged = charged;\n\n\tstruct State {\n\t\trpl::variable<int> stars;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tconst auto savedValue = privacy->newChargeStarsCurrent();\n\n\tif (available) {\n\t\tUi::AddDividerText(inner, tr::lng_messages_privacy_charge_about());\n\n\t\tconst auto chargeWrap = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\t\tconst auto chargeInner = chargeWrap->entity();\n\n\t\tUi::AddSkip(chargeInner);\n\n\t\tstate->stars = SetupChargeSlider(\n\t\t\tchargeInner,\n\t\t\tsession->user(),\n\t\t\t(savedValue > 0) ? savedValue : std::optional<int>(),\n\t\t\tkDefaultPrivateMessagesPrice);\n\n\t\tUi::AddSkip(chargeInner);\n\t\tUi::AddSubsectionTitle(\n\t\t\tchargeInner,\n\t\t\ttr::lng_messages_privacy_exceptions());\n\n\t\tconst auto key = Api::UserPrivacy::Key::NoPaidMessages;\n\t\tsession->api().userPrivacy().reload(key);\n\t\tauto label = session->api().userPrivacy().value(\n\t\t\tkey\n\t\t) | rpl::map([=](const Api::UserPrivacy::Rule &value) {\n\t\t\tusing namespace Settings;\n\t\t\tconst auto always = ExceptionUsersCount(value.always.peers);\n\t\t\treturn always\n\t\t\t\t? tr::lng_edit_privacy_exceptions_count(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\talways)\n\t\t\t\t: tr::lng_edit_privacy_exceptions_add(tr::now);\n\t\t});\n\n\t\tconst auto exceptions = Settings::AddButtonWithLabel(\n\t\t\tchargeInner,\n\t\t\ttr::lng_messages_privacy_remove_fee(),\n\t\t\tstd::move(label),\n\t\t\tst::settingsButtonNoIcon);\n\t\thighlightRemoveFee = exceptions;\n\n\t\tconst auto shower = exceptions->lifetime().make_state<rpl::lifetime>();\n\t\texceptions->setClickedCallback([=] {\n\t\t\t*shower = session->api().userPrivacy().value(\n\t\t\t\tkey\n\t\t\t) | rpl::take(\n\t\t\t\t1\n\t\t\t) | rpl::on_next([=](const Api::UserPrivacy::Rule &value) {\n\t\t\t\tEditNoPaidMessagesExceptions(controller, value);\n\t\t\t});\n\t\t});\n\t\tUi::AddSkip(chargeInner);\n\t\tUi::AddDividerText(\n\t\t\tchargeInner,\n\t\t\ttr::lng_messages_privacy_remove_about());\n\n\t\tusing namespace rpl::mappers;\n\t\tchargeWrap->toggleOn(group->value() | rpl::map(_1 == kOptionCharge));\n\t\tchargeWrap->finishAnimating();\n\t}\n\tusing WeakToast = base::weak_ptr<Ui::Toast::Instance>;\n\tconst auto toast = std::make_shared<WeakToast>();\n\tconst auto showToast = [=] {\n\t\tauto link = tr::link(\n\t\t\ttr::semibold(\n\t\t\t\ttr::lng_messages_privacy_premium_link(tr::now)));\n\t\t(*toast) = controller->showToast({\n\t\t\t.text = tr::lng_messages_privacy_premium(\n\t\t\t\ttr::now,\n\t\t\t\tlt_link,\n\t\t\t\tlink,\n\t\t\t\ttr::marked),\n\t\t\t.filter = crl::guard(&controller->session(), [=](\n\t\t\t\t\tconst ClickHandlerPtr &,\n\t\t\t\t\tQt::MouseButton button) {\n\t\t\t\tif (button == Qt::LeftButton) {\n\t\t\t\t\tif (const auto strong = toast->get()) {\n\t\t\t\t\t\tstrong->hideAnimated();\n\t\t\t\t\t\t(*toast) = nullptr;\n\t\t\t\t\t\tSettings::ShowPremium(\n\t\t\t\t\t\t\tcontroller,\n\t\t\t\t\t\t\tu\"noncontact_peers_require_premium\"_q);\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}),\n\t\t});\n\t};\n\n\tif (!allowed()) {\n\t\tCreateRadiobuttonLock(restricted, st::messagePrivacyCheck);\n\t\tif (charged) {\n\t\t\tCreateRadiobuttonLock(charged, st::messagePrivacyCheck);\n\t\t}\n\n\t\tgroup->setChangedCallback([=](int value) {\n\t\t\tif (value == kOptionPremium || value == kOptionCharge) {\n\t\t\t\tgroup->setValue(kOptionAll);\n\t\t\t\tshowToast();\n\t\t\t}\n\t\t});\n\n\t\tUi::AddSkip(inner);\n\t\tSettings::AddButtonWithIcon(\n\t\t\tinner,\n\t\t\ttr::lng_messages_privacy_premium_button(),\n\t\t\tst::messagePrivacySubscribe,\n\t\t\t{ .icon = &st::menuBlueIconPremium }\n\t\t)->setClickedCallback([=] {\n\t\t\tSettings::ShowPremium(\n\t\t\t\tcontroller,\n\t\t\t\tu\"noncontact_peers_require_premium\"_q);\n\t\t});\n\t\tUi::AddSkip(inner);\n\t\tUi::AddDividerText(inner, tr::lng_messages_privacy_premium_about());\n\t\tbox->addButton(tr::lng_about_done(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t} else {\n\t\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\t\tif (allowed()) {\n\t\t\t\tconst auto value = group->current();\n\t\t\t\tconst auto premiumRequired = (value == kOptionPremium);\n\t\t\t\tconst auto chargeStars = (value == kOptionCharge)\n\t\t\t\t\t? state->stars.current()\n\t\t\t\t\t: 0;\n\t\t\t\tprivacy->updateMessagesPrivacy(premiumRequired, chargeStars);\n\t\t\t\tbox->closeBox();\n\t\t\t} else {\n\t\t\t\tshowToast();\n\t\t\t}\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t}\n\n\tif (!highlightControlId.isEmpty()) {\n\t\tbox->showFinishes() | rpl::take(1) | rpl::on_next([=] {\n\t\t\tif (highlightControlId == u\"privacy/set-price\"_q) {\n\t\t\t\tSettings::HighlightWidget(\n\t\t\t\t\thighlightCharged,\n\t\t\t\t\t{ .radius = st::boxRadius });\n\t\t\t} else if (highlightControlId == u\"privacy/remove-fee\"_q) {\n\t\t\t\tSettings::HighlightWidget(highlightRemoveFee);\n\t\t\t}\n\t\t}, box->lifetime());\n\t}\n}\n\nrpl::producer<int> SetupChargeSlider(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<PeerData*> peer,\n\t\tstd::optional<int> savedValue,\n\t\tint defaultValue,\n\t\tbool allowZero) {\n\tstruct State {\n\t\trpl::variable<int> stars;\n\t};\n\tconst auto broadcast = peer->isBroadcast();\n\tconst auto group = !broadcast && !peer->isUser();\n\tconst auto state = container->lifetime().make_state<State>();\n\tconst auto chargeStars = savedValue.value_or(defaultValue);\n\tstate->stars = chargeStars;\n\n\tUi::AddSubsectionTitle(container, broadcast\n\t\t? tr::lng_manage_monoforum_price()\n\t\t: group\n\t\t? tr::lng_rights_charge_price()\n\t\t: tr::lng_messages_privacy_price());\n\n\tauto values = std::vector<int>();\n\tconst auto minStars = allowZero ? 0 : 1;\n\tconst auto maxStars = peer->session().appConfig().paidMessageStarsMax();\n\tif (chargeStars < minStars) {\n\t\tvalues.push_back(chargeStars);\n\t}\n\tfor (auto i = minStars; i < std::min(100, maxStars); ++i) {\n\t\tvalues.push_back(i);\n\t}\n\tfor (auto i = 100; i < std::min(1000, maxStars); i += 10) {\n\t\tif (i < chargeStars + 10 && chargeStars < i) {\n\t\t\tvalues.push_back(chargeStars);\n\t\t}\n\t\tvalues.push_back(i);\n\t}\n\tfor (auto i = 1000; i < maxStars + 1; i += 100) {\n\t\tif (i < chargeStars + 100 && chargeStars < i) {\n\t\t\tvalues.push_back(chargeStars);\n\t\t}\n\t\tvalues.push_back(i);\n\t}\n\tconst auto valuesCount = int(values.size());\n\tconst auto setStars = [=](int value) {\n\t\tstate->stars = value;\n\t};\n\tcontainer->add(\n\t\tMakeChargeStarsSlider(\n\t\t\tcontainer,\n\t\t\t&st::settingsScale,\n\t\t\t&st::settingsScaleLabel,\n\t\t\tvaluesCount,\n\t\t\t[=](int index) { return values[index]; },\n\t\t\tchargeStars,\n\t\t\tminStars,\n\t\t\tmaxStars,\n\t\t\tsetStars,\n\t\t\tsetStars),\n\t\tst::boxRowPadding);\n\n\tconst auto skip = 2 * st::defaultVerticalListSkip;\n\tUi::AddSkip(container, skip);\n\n\tconst auto details = container->add(\n\t\tobject_ptr<Ui::VerticalLayout>(container));\n\tstate->stars.value() | rpl::on_next([=](int stars) {\n\t\twhile (details->count()) {\n\t\t\tdelete details->widgetAt(0);\n\t\t}\n\t\tif (!stars) {\n\t\t\tUi::AddDivider(details);\n\t\t\treturn;\n\t\t}\n\t\tconst auto &appConfig = peer->session().appConfig();\n\t\tconst auto percent = appConfig.paidMessageCommission();\n\t\tconst auto ratio = appConfig.starsWithdrawRate();\n\t\tconst auto dollars = int(base::SafeRound(stars * ratio));\n\t\tconst auto amount = Ui::FillAmountAndCurrency(dollars, u\"USD\"_q);\n\t\tUi::AddDividerText(\n\t\t\tdetails,\n\t\t\t(broadcast\n\t\t\t\t? tr::lng_manage_monoforum_price_about\n\t\t\t\t: group\n\t\t\t\t? tr::lng_rights_charge_price_about\n\t\t\t\t: tr::lng_messages_privacy_price_about)(\n\t\t\t\t\tlt_percent,\n\t\t\t\t\trpl::single(QString::number(percent / 10.) + '%'),\n\t\t\t\t\tlt_amount,\n\t\t\t\t\trpl::single('~' + amount)));\n\t}, details->lifetime());\n\treturn state->stars.value();\n}\n\nvoid EditDirectMessagesPriceBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<ChannelData*> channel,\n\t\tstd::optional<int> savedValue,\n\t\tFn<void(std::optional<int>)> callback) {\n\tbox->setTitle(tr::lng_manage_monoforum());\n\tbox->setWidth(st::boxWideWidth);\n\n\tconst auto container = box->verticalLayout();\n\n\tSettings::AddDividerTextWithLottie(container, {\n\t\t.lottie = u\"direct_messages\"_q,\n\t\t.lottieSize = st::settingsFilterIconSize,\n\t\t.lottieMargins = st::settingsFilterIconPadding,\n\t\t.showFinished = box->showFinishes(),\n\t\t.about = tr::lng_manage_monoforum_about(\n\t\t\ttr::rich\n\t\t),\n\t\t.aboutMargins = st::settingsFilterDividerLabelPadding,\n\t});\n\n\tUi::AddSkip(container);\n\n\tconst auto toggle = container->add(object_ptr<Ui::SettingsButton>(\n\t\tbox,\n\t\ttr::lng_manage_monoforum_allow(),\n\t\tst::settingsButtonNoIcon));\n\ttoggle->toggleOn(rpl::single(savedValue.has_value()));\n\n\tUi::AddSkip(container);\n\tUi::AddDivider(container);\n\tUi::AddSkip(container);\n\n\tconst auto wrap = box->addRow(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tbox,\n\t\t\tobject_ptr<Ui::VerticalLayout>(box)),\n\t\tstyle::margins());\n\twrap->toggle(savedValue.has_value(), anim::type::instant);\n\twrap->toggleOn(toggle->toggledChanges());\n\n\tconst auto result = box->lifetime().make_state<int>(\n\t\tsavedValue.value_or(0));\n\n\tconst auto inner = wrap->entity();\n\tUi::AddSkip(inner);\n\tSetupChargeSlider(\n\t\tinner,\n\t\tchannel,\n\t\tsavedValue,\n\t\tchannel->session().appConfig().paidMessageChannelStarsDefault(),\n\t\ttrue\n\t) | rpl::on_next([=](int stars) {\n\t\t*result = stars;\n\t}, box->lifetime());\n\n\tif (const auto username = channel->username(); !username.isEmpty()) {\n\t\tUi::AddSkip(inner);\n\t\tUi::AddSubsectionTitle(\n\t\t\tinner,\n\t\t\ttr::lng_manage_monoforum_link_subtitle());\n\n\t\tconstexpr auto kDirectParam = \"?direct\"_cs;\n\t\tconst auto link = channel->session().createInternalLinkFull(username)\n\t\t\t+ kDirectParam.utf8();\n\t\tconst auto copyLink = [=] {\n\t\t\tTextUtilities::SetClipboardText(TextForMimeData::Simple(link));\n\t\t\tbox->uiShow()->showToast(tr::lng_group_invite_copied(tr::now));\n\t\t};\n\t\tconst auto shareLink = [=] {\n\t\t\tbox->uiShow()->showBox(ShareInviteLinkBox(channel, link));\n\t\t};\n\t\tconst auto createMenu = [=] {\n\t\t\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\tinner,\n\t\t\t\tst::popupMenuWithIcons);\n\t\t\tresult->addAction(\n\t\t\t\ttr::lng_group_invite_context_qr(tr::now),\n\t\t\t\t[=] {\n\t\t\t\t\tbox->uiShow()->showBox(Box([=](\n\t\t\t\t\t\t\tnot_null<Ui::GenericBox*> qrBox) {\n\t\t\t\t\t\tUi::FillPeerQrBox(qrBox, channel, link, nullptr);\n\t\t\t\t\t}));\n\t\t\t\t},\n\t\t\t\t&st::menuIconQrCode);\n\t\t\treturn result;\n\t\t};\n\n\t\tauto linkText = Ui::Text::StripUrlProtocol(link);\n\t\tconst auto label = inner->lifetime().make_state<Ui::InviteLinkLabel>(\n\t\t\tinner,\n\t\t\trpl::single(std::move(linkText)),\n\t\t\tcreateMenu);\n\t\tinner->add(\n\t\t\tlabel->take(),\n\t\t\tst::inviteLinkFieldPadding);\n\n\t\tlabel->clicks() | rpl::on_next(copyLink, label->lifetime());\n\n\t\tUi::AddSkip(inner);\n\n\t\tAddCopyShareLinkButtons(inner, copyLink, shareLink);\n\t\tUi::AddSkip(inner);\n\t\tUi::AddSkip(inner);\n\n\t\tUi::AddDivider(inner);\n\t}\n\n\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\tconst auto weak = base::make_weak(box);\n\t\tcallback(toggle->toggled() ? *result : std::optional<int>());\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/edit_privacy_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n#include \"mtproto/sender.h\"\n#include \"api/api_user_privacy.h\"\n\nnamespace Ui {\nclass GenericBox;\nclass VerticalLayout;\nclass FlatLabel;\nclass LinkButton;\ntemplate <typename Enum>\nclass RadioenumGroup;\ntemplate <typename Enum>\nclass Radioenum;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nclass EditPrivacyBox;\n\nclass EditPrivacyController {\npublic:\n\tusing Key = Api::UserPrivacy::Key;\n\tusing Option = Api::UserPrivacy::Option;\n\tenum class Exception {\n\t\tAlways,\n\t\tNever,\n\t};\n\n\t[[nodiscard]] virtual Key key() const = 0;\n\n\t[[nodiscard]] virtual rpl::producer<QString> title() const = 0;\n\t[[nodiscard]] virtual bool hasOption(Option option) const;\n\t[[nodiscard]] virtual rpl::producer<QString> optionsTitleKey() const = 0;\n\t[[nodiscard]] virtual QString optionLabel(Option option) const;\n\t[[nodiscard]] virtual rpl::producer<TextWithEntities> warning() const {\n\t\treturn nullptr;\n\t}\n\tvirtual void prepareWarningLabel(\n\t\t\tnot_null<Ui::FlatLabel*> warning) const {\n\t}\n\t[[nodiscard]] virtual rpl::producer<QString> exceptionButtonTextKey(\n\t\tException exception) const = 0;\n\t[[nodiscard]] virtual rpl::producer<QString> exceptionBoxTitle(\n\t\tException exception) const = 0;\n\t[[nodiscard]] virtual auto exceptionsDescription()\n\t\tconst -> rpl::producer<QString> = 0;\n\t[[nodiscard]] virtual bool allowPremiumsToggle(\n\t\t\tException exception) const {\n\t\treturn false;\n\t}\n\t[[nodiscard]] virtual bool allowMiniAppsToggle(\n\t\t\tException exception) const {\n\t\treturn false;\n\t}\n\tvirtual void handleExceptionsChange(\n\t\tException exception,\n\t\trpl::producer<int> value) {\n\t}\n\n\t[[nodiscard]] virtual object_ptr<Ui::RpWidget> setupAboveWidget(\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tnot_null<QWidget*> parent,\n\t\t\trpl::producer<Option> option,\n\t\t\tnot_null<QWidget*> outerContainer) {\n\t\treturn { nullptr };\n\t}\n\t[[nodiscard]] virtual object_ptr<Ui::RpWidget> setupMiddleWidget(\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tnot_null<QWidget*> parent,\n\t\t\trpl::producer<Option> option) {\n\t\treturn { nullptr };\n\t}\n\t[[nodiscard]] virtual object_ptr<Ui::RpWidget> setupBelowWidget(\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tnot_null<QWidget*> parent,\n\t\t\trpl::producer<Option> option) {\n\t\treturn { nullptr };\n\t}\n\n\tvirtual void confirmSave(\n\t\t\tbool someAreDisallowed,\n\t\t\tFn<void()> saveCallback) {\n\t\tsaveCallback();\n\t}\n\tvirtual void saveAdditional() {\n\t}\n\n\t[[nodiscard]] virtual Fn<void()> premiumClickedCallback(\n\t\t\tOption option,\n\t\t\tnot_null<Window::SessionController*> controller) {\n\t\treturn nullptr;\n\t}\n\n\tvirtual void checkHighlightControls(\n\t\t\tnot_null<Window::SessionController*> controller) {\n\t}\n\n\tvirtual ~EditPrivacyController() = default;\n\nprotected:\n\t[[nodiscard]] EditPrivacyBox *view() const {\n\t\treturn _view;\n\t}\n\nprivate:\n\tvoid setView(EditPrivacyBox *box) {\n\t\t_view = box;\n\t}\n\n\tEditPrivacyBox *_view = nullptr;\n\n\tfriend class EditPrivacyBox;\n\n};\n\nclass EditPrivacyBox final : public Ui::BoxContent {\npublic:\n\tusing Value = Api::UserPrivacy::Rule;\n\tusing Option = Api::UserPrivacy::Option;\n\tusing Exceptions = Api::UserPrivacy::Exceptions;\n\tusing Exception = EditPrivacyController::Exception;\n\n\tEditPrivacyBox(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionController*> window,\n\t\tstd::unique_ptr<EditPrivacyController> controller,\n\t\tconst Value &value);\n\n\tstatic Ui::Radioenum<Option> *AddOption(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<EditPrivacyController*> controller,\n\t\tconst std::shared_ptr<Ui::RadioenumGroup<Option>> &group,\n\t\tOption option);\n\nprotected:\n\tvoid prepare() override;\n\tvoid showFinished() override;\n\nprivate:\n\tbool showExceptionLink(Exception exception) const;\n\tvoid setupContent();\n\n\tUi::FlatLabel *addLabel(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<TextWithEntities> text,\n\t\tint topSkip);\n\tUi::FlatLabel *addLabelOrDivider(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<TextWithEntities> text,\n\t\tint topSkip);\n\n\tvoid editExceptions(Exception exception, Fn<void()> done);\n\tExceptions &exceptions(Exception exception);\n\n\tconst not_null<Window::SessionController*> _window;\n\tstd::unique_ptr<EditPrivacyController> _controller;\n\tValue _value;\n\tQPointer<QWidget> _always;\n\tQPointer<QWidget> _never;\n\n};\n\nvoid EditMessagesPrivacyBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> controller,\n\tconst QString &highlightControlId = QString());\n\n[[nodiscard]] rpl::producer<int> SetupChargeSlider(\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<PeerData*> peer,\n\tstd::optional<int> savedValue,\n\tint defaultValue,\n\tbool allowZero = false);\n\nvoid EditDirectMessagesPriceBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<ChannelData*> channel,\n\tstd::optional<int> savedValue,\n\tFn<void(std::optional<int>)> callback);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/edit_todo_list_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/edit_todo_list_box.h\"\n\n#include \"base/call_delayed.h\"\n#include \"base/event_filter.h\"\n#include \"base/random.h\"\n#include \"base/unique_qptr.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_session.h\"\n#include \"data/data_todo_list.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"history/view/history_view_schedule_box.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_send.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/controls/emoji_button_factory.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\" // defaultComposeFiles.\n#include \"styles/style_layers.h\"\n#include \"styles/style_polls.h\"\n#include \"styles/style_settings.h\"\n\nnamespace {\n\nconstexpr auto kMaxOptionsCount = TodoListData::kMaxOptions;\nconstexpr auto kWarnTitleLimit = 12;\nconstexpr auto kWarnTaskLimit = 24;\nconstexpr auto kErrorLimit = 99;\n\nclass Tasks {\npublic:\n\tTasks(\n\t\tnot_null<Ui::BoxContent*> box,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tChatHelpers::TabbedPanel *emojiPanel,\n\t\tstd::vector<TodoListItem> existing = {},\n\t\tbool existingLocked = false);\n\n\t[[nodiscard]] bool hasTasks() const;\n\t[[nodiscard]] bool isValid() const;\n\t[[nodiscard]] std::vector<TodoListItem> toTodoListItems() const;\n\tvoid focusFirst();\n\n\t[[nodiscard]] rpl::producer<int> addedCount() const;\n\t[[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const;\n\t[[nodiscard]] rpl::producer<> backspaceInFront() const;\n\t[[nodiscard]] rpl::producer<> tabbed() const;\n\nprivate:\n\tclass Task {\n\tpublic:\n\t\tTask(\n\t\t\tnot_null<QWidget*> outer,\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Main::Session*> session,\n\t\t\tint id,\n\t\t\tint position,\n\t\t\tbool locked);\n\n\t\tTask(const Task &other) = delete;\n\t\tTask &operator=(const Task &other) = delete;\n\n\t\tvoid toggleRemoveAlways(bool toggled);\n\n\t\tvoid show(anim::type animated);\n\t\tvoid destroy(FnMut<void()> done);\n\n\t\t[[nodiscard]] bool hasShadow() const;\n\t\tvoid createShadow();\n\t\tvoid destroyShadow();\n\n\t\t[[nodiscard]] int id() const;\n\t\t[[nodiscard]] bool locked() const;\n\t\t[[nodiscard]] bool isEmpty() const;\n\t\t[[nodiscard]] bool isGood() const;\n\t\t[[nodiscard]] bool isTooLong() const;\n\t\t[[nodiscard]] bool hasFocus() const;\n\t\tvoid setFocus() const;\n\t\tvoid clearValue();\n\n\t\tvoid setPlaceholder() const;\n\t\tvoid removePlaceholder() const;\n\n\t\t[[nodiscard]] not_null<Ui::InputField*> field() const;\n\n\t\t[[nodiscard]] TodoListItem toTodoListItem(int nextId) const;\n\n\t\t[[nodiscard]] rpl::producer<Qt::MouseButton> removeClicks() const;\n\n\tprivate:\n\t\tvoid createRemove();\n\t\tvoid createWarning();\n\t\tvoid updateFieldGeometry();\n\n\t\tint _id = 0;\n\t\tbase::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _wrap;\n\t\tnot_null<Ui::RpWidget*> _content;\n\t\tUi::InputField *_field = nullptr;\n\t\tbase::unique_qptr<Ui::PlainShadow> _shadow;\n\t\tbase::unique_qptr<Ui::CrossButton> _remove;\n\t\trpl::variable<bool> *_removeAlways = nullptr;\n\t\tint _limit = 0;\n\n\t};\n\n\t[[nodiscard]] bool full() const;\n\t[[nodiscard]] bool correctShadows() const;\n\tvoid fixShadows();\n\tvoid removeEmptyTail();\n\tvoid addEmptyTask();\n\tvoid addTask(\n\t\tint id,\n\t\tTextWithEntities text,\n\t\tanim::type animated);\n\tvoid insertTask(\n\t\tint beforeIndex,\n\t\tint id,\n\t\tTextWithEntities text,\n\t\tanim::type animated);\n\tvoid initTaskField(not_null<Task*> task, TextWithEntities text);\n\tvoid checkLastTask();\n\tvoid validateState();\n\tvoid fixAfterErase();\n\tvoid destroy(std::unique_ptr<Task> task);\n\tvoid removeDestroyed(not_null<Task*> field);\n\tint findField(not_null<Ui::InputField*> field) const;\n\tvoid handlePaste(\n\t\tnot_null<Ui::InputField*> field,\n\t\tconst QStringList &list);\n\n\tnot_null<Ui::BoxContent*> _box;\n\tnot_null<Ui::VerticalLayout*> _container;\n\tconst not_null<Window::SessionController*> _controller;\n\tconst int _existingCount = 0;\n\tconst bool _existingLocked = false;\n\tChatHelpers::TabbedPanel * const _emojiPanel;\n\tint _position = 0;\n\tint _tasksLimit = 0;\n\tstd::vector<std::unique_ptr<Task>> _list;\n\tstd::vector<std::unique_ptr<Task>> _destroyed;\n\trpl::variable<int> _addedCount = 0;\n\tbool _hasTasks = false;\n\tbool _isValid = false;\n\trpl::event_stream<not_null<QWidget*>> _scrollToWidget;\n\trpl::event_stream<> _backspaceInFront;\n\trpl::event_stream<> _tabbed;\n\trpl::lifetime _emojiPanelLifetime;\n\n};\n\nvoid InitField(\n\t\tnot_null<QWidget*> container,\n\t\tnot_null<Ui::InputField*> field,\n\t\tnot_null<Main::Session*> session) {\n\tfield->setInstantReplaces(Ui::InstantReplaces::Default());\n\tfield->setInstantReplacesEnabled(\n\t\tCore::App().settings().replaceEmojiValue(),\n\t\tCore::App().settings().systemTextReplaceValue());\n\tauto options = Ui::Emoji::SuggestionsController::Options();\n\toptions.suggestExactFirstWord = false;\n\tUi::Emoji::SuggestionsController::Init(\n\t\tcontainer,\n\t\tfield,\n\t\tsession,\n\t\toptions);\n}\n\n[[nodiscard]] QStringList ParsePastedList(const QString &text) {\n\tauto list = QStringView(text).split('\\n');\n\tfor (auto i = list.begin(); i != list.end();) {\n\t\tauto text = i->trimmed();\n\t\tif (text.isEmpty() && (i + 1 != list.end())) {\n\t\t\ti = list.erase(i);\n\t\t} else {\n\t\t\t*i++ = text;\n\t\t}\n\t}\n\tif (list.size() < 2) {\n\t\treturn {};\n\t}\n\tauto result = QStringList();\n\tresult.reserve(list.size());\n\tfor (const auto &view : list) {\n\t\tresult.push_back(view.toString());\n\t}\n\treturn result;\n}\n\nnot_null<Ui::FlatLabel*> CreateWarningLabel(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Ui::InputField*> field,\n\t\tint valueLimit,\n\t\tint warnLimit) {\n\tconst auto result = Ui::CreateChild<Ui::FlatLabel>(\n\t\tparent.get(),\n\t\tQString(),\n\t\tst::createPollWarning);\n\tresult->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tfield->changes(\n\t) | rpl::on_next([=] {\n\t\tUi::PostponeCall(crl::guard(field, [=] {\n\t\t\tconst auto length = field->getLastText().size();\n\t\t\tconst auto value = valueLimit - length;\n\t\t\tconst auto shown = (value < warnLimit)\n\t\t\t\t&& (field->height() > st::createPollOptionField.heightMin);\n\t\t\tif (value >= 0) {\n\t\t\t\tresult->setText(QString::number(value));\n\t\t\t} else {\n\t\t\t\tconstexpr auto kMinus = QChar(0x2212);\n\t\t\t\tresult->setMarkedText(Ui::Text::Colorized(\n\t\t\t\t\tkMinus + QString::number(std::abs(value))));\n\t\t\t}\n\t\t\tresult->setVisible(shown);\n\t\t}));\n\t}, field->lifetime());\n\treturn result;\n}\n\nvoid FocusAtEnd(not_null<Ui::InputField*> field) {\n\tfield->setFocus();\n\tfield->setCursorPosition(field->getLastText().size());\n\tfield->ensureCursorVisible();\n}\n\n[[nodiscard]] base::unique_qptr<ChatHelpers::TabbedPanel> MakeEmojiPanel(\n\t\tnot_null<QWidget*> outer,\n\t\tnot_null<Window::SessionController*> controller) {\n\tauto result = base::make_unique_q<ChatHelpers::TabbedPanel>(\n\t\touter,\n\t\tcontroller,\n\t\tobject_ptr<ChatHelpers::TabbedSelector>(\n\t\t\tnullptr,\n\t\t\tcontroller->uiShow(),\n\t\t\tWindow::GifPauseReason::Layer,\n\t\t\tChatHelpers::TabbedSelector::Mode::EmojiOnly));\n\tresult->setDesiredHeightValues(\n\t\t1.,\n\t\tst::emojiPanMinHeight / 2,\n\t\tst::emojiPanMinHeight);\n\tresult ->hide();\n\tresult->selector()->setCurrentPeer(controller->session().user());\n\treturn result;\n}\n\nTasks::Task::Task(\n\tnot_null<QWidget*> outer,\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<Main::Session*> session,\n\tint id,\n\tint position,\n\tbool locked)\n: _id(id)\n, _wrap(container->insert(\n\tposition,\n\tobject_ptr<Ui::SlideWrap<Ui::RpWidget>>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::RpWidget>(container))))\n, _content(_wrap->entity())\n, _field(\n\tUi::CreateChild<Ui::InputField>(\n\t\t_content.get(),\n\t\tsession->user()->isPremium()\n\t\t\t? st::createTodoOptionField\n\t\t\t: st::createPollOptionField,\n\t\tUi::InputField::Mode::MultiLine,\n\t\ttr::lng_todo_create_list_add()))\n, _limit(session->appConfig().todoListItemTextLimit()) {\n\tInitField(outer, _field, session);\n\n\t// Don't limit max length, because user can paste long list of items.\n\t//_field->setMaxLength(_limit + kErrorLimit);\n\n\t_field->show();\n\tif (locked) {\n\t\t_field->setDisabled(true);\n\t}\n\n\t_wrap->hide(anim::type::instant);\n\n\t_content->widthValue(\n\t) | rpl::on_next([=] {\n\t\tupdateFieldGeometry();\n\t}, _field->lifetime());\n\n\t_field->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\t_content->resize(_content->width(), height);\n\t}, _field->lifetime());\n\n\tcreateShadow();\n\tif (!locked) {\n\t\tcreateRemove();\n\t\tcreateWarning();\n\t}\n\tupdateFieldGeometry();\n}\n\nbool Tasks::Task::hasShadow() const {\n\treturn (_shadow != nullptr);\n}\n\nvoid Tasks::Task::createShadow() {\n\tExpects(_content != nullptr);\n\n\tif (_shadow) {\n\t\treturn;\n\t}\n\t_shadow.reset(Ui::CreateChild<Ui::PlainShadow>(field().get()));\n\t_shadow->show();\n\tfield()->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tconst auto left = st::createPollFieldPadding.left();\n\t\t_shadow->setGeometry(\n\t\t\tleft,\n\t\t\tsize.height() - st::lineWidth,\n\t\t\tsize.width() - left,\n\t\t\tst::lineWidth);\n\t}, _shadow->lifetime());\n}\n\nvoid Tasks::Task::destroyShadow() {\n\t_shadow = nullptr;\n}\n\nvoid Tasks::Task::createRemove() {\n\tusing namespace rpl::mappers;\n\n\tconst auto field = this->field();\n\tauto &lifetime = field->lifetime();\n\n\tconst auto remove = Ui::CreateChild<Ui::CrossButton>(\n\t\tfield.get(),\n\t\tst::createPollOptionRemove);\n\tremove->show(anim::type::instant);\n\n\tconst auto toggle = lifetime.make_state<rpl::variable<bool>>(false);\n\t_removeAlways = lifetime.make_state<rpl::variable<bool>>(false);\n\n\tfield->changes(\n\t) | rpl::on_next([field, toggle] {\n\t\t// Don't capture 'this'! Because Option is a value type.\n\t\t*toggle = !field->getLastText().isEmpty();\n\t}, field->lifetime());\n#if 0\n\trpl::combine(\n\t\ttoggle->value(),\n\t\t_removeAlways->value(),\n\t\t_1 || _2\n\t) | rpl::on_next([=](bool shown) {\n\t\tremove->toggle(shown, anim::type::normal);\n\t}, remove->lifetime());\n#endif\n\n\tfield->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tremove->moveToRight(\n\t\t\tst::createPollOptionRemovePosition.x(),\n\t\t\tst::createPollOptionRemovePosition.y(),\n\t\t\twidth);\n\t}, remove->lifetime());\n\n\t_remove.reset(remove);\n}\n\nvoid Tasks::Task::createWarning() {\n\tusing namespace rpl::mappers;\n\n\tconst auto field = this->field();\n\tconst auto warning = CreateWarningLabel(\n\t\tfield,\n\t\tfield,\n\t\t_limit,\n\t\tkWarnTaskLimit);\n\trpl::combine(\n\t\tfield->sizeValue(),\n\t\twarning->sizeValue()\n\t) | rpl::on_next([=](QSize size, QSize label) {\n\t\twarning->moveToLeft(\n\t\t\t(size.width()\n\t\t\t\t- label.width()\n\t\t\t\t- st::createPollWarningPosition.x()),\n\t\t\t(size.height()\n\t\t\t\t- label.height()\n\t\t\t\t- st::createPollWarningPosition.y()),\n\t\t\tsize.width());\n\t}, warning->lifetime());\n}\n\nbool Tasks::Task::isEmpty() const {\n\treturn field()->getLastText().trimmed().isEmpty();\n}\n\nbool Tasks::Task::isGood() const {\n\treturn !locked()\n\t\t&& !field()->getLastText().trimmed().isEmpty()\n\t\t&& !isTooLong();\n}\n\nbool Tasks::Task::isTooLong() const {\n\treturn (field()->getLastText().size() > _limit);\n}\n\nbool Tasks::Task::hasFocus() const {\n\treturn field()->hasFocus();\n}\n\nvoid Tasks::Task::setFocus() const {\n\tif (!locked()) {\n\t\tFocusAtEnd(field());\n\t}\n}\n\nvoid Tasks::Task::clearValue() {\n\tfield()->setText(QString());\n}\n\nvoid Tasks::Task::setPlaceholder() const {\n\tfield()->setPlaceholder(tr::lng_todo_create_list_add());\n}\n\nvoid Tasks::Task::toggleRemoveAlways(bool toggled) {\n\tif (_removeAlways) {\n\t\t*_removeAlways = toggled;\n\t}\n}\n\nvoid Tasks::Task::updateFieldGeometry() {\n\t_field->resizeToWidth(_content->width());\n\t_field->moveToLeft(0, 0);\n}\n\nnot_null<Ui::InputField*> Tasks::Task::field() const {\n\treturn _field;\n}\n\nvoid Tasks::Task::removePlaceholder() const {\n\tfield()->setPlaceholder(rpl::single(QString()));\n}\n\nint Tasks::Task::id() const {\n\treturn _id;\n}\n\nbool Tasks::Task::locked() const {\n\treturn !_remove;\n}\n\nTodoListItem Tasks::Task::toTodoListItem(int nextId) const {\n\tconst auto text = field()->getTextWithTags();\n\tauto result = TodoListItem{\n\t\t.text = TextWithEntities{\n\t\t\t.text = text.text,\n\t\t\t.entities = TextUtilities::ConvertTextTagsToEntities(text.tags),\n\t\t},\n\t\t.id = _id ? _id : nextId,\n\t};\n\tTextUtilities::Trim(result.text);\n\treturn result;\n}\n\nrpl::producer<Qt::MouseButton> Tasks::Task::removeClicks() const {\n\treturn _remove ? _remove->clicks() : rpl::never<Qt::MouseButton>();\n}\n\nTasks::Tasks(\n\tnot_null<Ui::BoxContent*> box,\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<Window::SessionController*> controller,\n\tChatHelpers::TabbedPanel *emojiPanel,\n\tstd::vector<TodoListItem> existing,\n\tbool existingLocked)\n: _box(box)\n, _container(container)\n, _controller(controller)\n, _existingCount(existing.size())\n, _existingLocked(existingLocked)\n, _emojiPanel(emojiPanel)\n, _position(_container->count())\n, _tasksLimit(controller->session().appConfig().todoListItemsLimit()) {\n\tfor (const auto &task : existing) {\n\t\taddTask(task.id, task.text, anim::type::instant);\n\t}\n\tvalidateState();\n}\n\nbool Tasks::full() const {\n\treturn (_list.size() >= _tasksLimit);\n}\n\nbool Tasks::hasTasks() const {\n\treturn _hasTasks;\n}\n\nbool Tasks::isValid() const {\n\treturn _isValid;\n}\n\nrpl::producer<int> Tasks::addedCount() const {\n\treturn _addedCount.value();\n}\n\nrpl::producer<not_null<QWidget*>> Tasks::scrollToWidget() const {\n\treturn _scrollToWidget.events();\n}\n\nrpl::producer<> Tasks::backspaceInFront() const {\n\treturn _backspaceInFront.events();\n}\n\nrpl::producer<> Tasks::tabbed() const {\n\treturn _tabbed.events();\n}\n\nvoid Tasks::Task::show(anim::type animated) {\n\t_wrap->show(animated);\n}\n\nvoid Tasks::Task::destroy(FnMut<void()> done) {\n\tif (anim::Disabled() || _wrap->isHidden()) {\n\t\tUi::PostponeCall(std::move(done));\n\t\treturn;\n\t}\n\t_wrap->hide(anim::type::normal);\n\tbase::call_delayed(\n\t\tst::slideWrapDuration * 2,\n\t\t_content.get(),\n\t\tstd::move(done));\n}\n\nstd::vector<TodoListItem> Tasks::toTodoListItems() const {\n\tauto result = std::vector<TodoListItem>();\n\tresult.reserve(_list.size());\n\tauto usedId = 0;\n\tfor (const auto &task : _list) {\n\t\tif (const auto id = task->id()) {\n\t\t\tusedId = id;\n\t\t} else if (task->isGood()) {\n\t\t\t++usedId;\n\t\t}\n\t\tif (task->isGood()) {\n\t\t\tresult.push_back(task->toTodoListItem(usedId));\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid Tasks::focusFirst() {\n\tconst auto locked = _existingLocked ? _existingCount : 0;\n\tAssert(locked < _list.size());\n\tFocusAtEnd((_list.begin() + locked)->get()->field());\n}\n\nbool Tasks::correctShadows() const {\n\t// Last one should be without shadow.\n\tconst auto noShadow = ranges::find(\n\t\t_list,\n\t\ttrue,\n\t\tranges::not_fn(&Task::hasShadow));\n\treturn (noShadow == end(_list) - 1);\n}\n\nvoid Tasks::fixShadows() {\n\tif (correctShadows()) {\n\t\treturn;\n\t}\n\tfor (auto &option : _list) {\n\t\toption->createShadow();\n\t}\n\t_list.back()->destroyShadow();\n}\n\nvoid Tasks::removeEmptyTail() {\n\t// Only one option at the end of options list can be empty.\n\t// Remove all other trailing empty options.\n\t// Only last empty and previous option have non-empty placeholders.\n\tconst auto focused = ranges::find_if(\n\t\t_list,\n\t\t&Task::hasFocus);\n\tconst auto end = _list.end();\n\tconst auto reversed = ranges::views::reverse(_list);\n\tconst auto emptyItem = ranges::find_if(\n\t\treversed,\n\t\tranges::not_fn(&Task::isEmpty)).base();\n\tconst auto focusLast = (focused > emptyItem) && (focused < end);\n\tif (emptyItem == end) {\n\t\treturn;\n\t}\n\tif (focusLast) {\n\t\t(*emptyItem)->setFocus();\n\t}\n\tfor (auto i = emptyItem + 1; i != end; ++i) {\n\t\tdestroy(std::move(*i));\n\t}\n\t_list.erase(emptyItem + 1, end);\n\tfixAfterErase();\n}\n\nvoid Tasks::destroy(std::unique_ptr<Task> task) {\n\tconst auto value = task.get();\n\ttask->destroy([=] { removeDestroyed(value); });\n\t_destroyed.push_back(std::move(task));\n}\n\nvoid Tasks::fixAfterErase() {\n\tExpects(!_list.empty());\n\n\tconst auto last = _list.end() - 1;\n\t(*last)->setPlaceholder();\n\t(*last)->toggleRemoveAlways(false);\n\tif (last != begin(_list)) {\n\t\t(*(last - 1))->setPlaceholder();\n\t\t(*(last - 1))->toggleRemoveAlways(false);\n\t}\n\tfixShadows();\n}\n\nvoid Tasks::addEmptyTask() {\n\tif (!_list.empty() && _list.back()->isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto locked = _existingLocked ? _existingCount : 0;\n\taddTask(\n\t\t0, // id\n\t\tTextWithEntities(),\n\t\t(locked < _list.size()) ? anim::type::normal : anim::type::instant);\n}\n\nvoid Tasks::addTask(\n\t\tint id,\n\t\tTextWithEntities text,\n\t\tanim::type animated) {\n\tinsertTask(_list.size(), id, std::move(text), animated);\n}\n\nvoid Tasks::insertTask(\n\t\tint beforeIndex,\n\t\tint id,\n\t\tTextWithEntities text,\n\t\tanim::type animated) {\n\tif (full()) {\n\t\treturn;\n\t}\n\tAssert(beforeIndex >= 0 && beforeIndex <= _list.size());\n\tif (_list.size() > 1) {\n\t\t(*(_list.end() - 2))->removePlaceholder();\n\t\t(*(_list.end() - 2))->toggleRemoveAlways(true);\n\t}\n\tconst auto locked = id && _existingLocked;\n\tconst auto i = _list.insert(\n\t\tbegin(_list) + beforeIndex,\n\t\tstd::make_unique<Task>(\n\t\t\t_box,\n\t\t\t_container,\n\t\t\t&_controller->session(),\n\t\t\tid,\n\t\t\t_position + beforeIndex + _destroyed.size(),\n\t\t\tlocked));\n\tconst auto field = i->get()->field();\n\tif (!locked) {\n\t\tinitTaskField(i->get(), std::move(text));\n\t} else {\n\t\tInitMessageFieldHandlers(\n\t\t\t_controller,\n\t\t\tfield,\n\t\t\tWindow::GifPauseReason::Layer,\n\t\t\t[](not_null<DocumentData*>) { return true; });\n\t\tfield->setTextWithTags({\n\t\t\ttext.text,\n\t\t\tTextUtilities::ConvertEntitiesToTextTags(text.entities)\n\t\t});\n\t}\n\tfield->finishAnimating();\n\ti->get()->show(animated);\n\tfixShadows();\n}\n\nvoid Tasks::initTaskField(not_null<Task*> task, TextWithEntities text) {\n\tconst auto field = task->field();\n\tif (const auto emojiPanel = _emojiPanel) {\n\t\tconst auto emojiToggle = Ui::AddEmojiToggleToField(\n\t\t\tfield,\n\t\t\t_box,\n\t\t\t_controller,\n\t\t\temojiPanel,\n\t\t\tQPoint(\n\t\t\t\t-st::createTodoOptionField.textMargins.right(),\n\t\t\t\tst::createPollOptionEmojiPositionSkip));\n\t\temojiToggle->shownValue() | rpl::on_next([=](bool shown) {\n\t\t\tif (!shown) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_emojiPanelLifetime.destroy();\n\t\t\temojiPanel->selector()->emojiChosen(\n\t\t\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\t\t\tif (field->hasFocus()) {\n\t\t\t\t\tUi::InsertEmojiAtCursor(field->textCursor(), data.emoji);\n\t\t\t\t}\n\t\t\t}, _emojiPanelLifetime);\n\t\t\temojiPanel->selector()->customEmojiChosen(\n\t\t\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\t\t\tif (field->hasFocus()) {\n\t\t\t\t\tData::InsertCustomEmoji(field, data.document);\n\t\t\t\t}\n\t\t\t}, _emojiPanelLifetime);\n\t\t}, emojiToggle->lifetime());\n\t}\n\tfield->setTextWithTags({\n\t\ttext.text,\n\t\tTextUtilities::ConvertEntitiesToTextTags(text.entities)\n\t});\n\tfield->submits(\n\t) | rpl::on_next([=] {\n\t\tconst auto index = findField(field);\n\t\tif (_list[index]->isGood() && index + 1 < _list.size()) {\n\t\t\t_list[index + 1]->setFocus();\n\t\t}\n\t}, field->lifetime());\n\tfield->changes(\n\t) | rpl::on_next([=] {\n\t\tauto list = ParsePastedList(field->getLastText());\n\t\tif (!list.empty()) {\n\t\t\tfield->setText(list.front());\n\t\t\tfield->forceProcessContentsChanges();\n\n\t\t\tlist.pop_front();\n\t\t\thandlePaste(field, list);\n\t\t}\n\t\tUi::PostponeCall(crl::guard(field, [=] {\n\t\t\tvalidateState();\n\t\t}));\n\t}, field->lifetime());\n\tfield->focusedChanges(\n\t) | rpl::filter(rpl::mappers::_1) | rpl::on_next([=] {\n\t\t_scrollToWidget.fire_copy(field);\n\t}, field->lifetime());\n\tfield->tabbed(\n\t) | rpl::on_next([=](not_null<bool*> handled) {\n\t\tconst auto index = findField(field);\n\t\tif (index + 1 < _list.size()) {\n\t\t\t_list[index + 1]->setFocus();\n\t\t} else {\n\t\t\t_tabbed.fire({});\n\t\t}\n\t\t*handled = true;\n\t}, field->lifetime());\n\tbase::install_event_filter(field, [=](not_null<QEvent*> event) {\n\t\tif (event->type() != QEvent::KeyPress\n\t\t\t|| !field->getLastText().isEmpty()) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\t\tconst auto key = static_cast<QKeyEvent*>(event.get())->key();\n\t\tif (key != Qt::Key_Backspace) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\n\t\tconst auto index = findField(field);\n\t\tif (index > 0) {\n\t\t\t_list[index - 1]->setFocus();\n\t\t} else {\n\t\t\t_backspaceInFront.fire({});\n\t\t}\n\t\treturn base::EventFilterResult::Cancel;\n\t});\n\n\ttask->removeClicks(\n\t) | rpl::on_next([=] {\n\t\tUi::PostponeCall(crl::guard(field, [=] {\n\t\t\tExpects(!_list.empty());\n\n\t\t\tconst auto item = begin(_list) + findField(field);\n\t\t\tif (item == _list.end() - 1) {\n\t\t\t\t(*item)->clearValue();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ((*item)->hasFocus()) {\n\t\t\t\t(*(item + 1))->setFocus();\n\t\t\t}\n\t\t\tdestroy(std::move(*item));\n\t\t\t_list.erase(item);\n\t\t\tfixAfterErase();\n\t\t\tvalidateState();\n\t\t}));\n\t}, field->lifetime());\n}\n\nvoid Tasks::removeDestroyed(not_null<Task*> task) {\n\tconst auto i = ranges::find(\n\t\t_destroyed,\n\t\ttask.get(),\n\t\t&std::unique_ptr<Task>::get);\n\tAssert(i != end(_destroyed));\n\t_destroyed.erase(i);\n}\n\nvoid Tasks::validateState() {\n\tcheckLastTask();\n\t_hasTasks = (ranges::count_if(_list, &Task::isGood) > 0);\n\t_isValid = _hasTasks && ranges::none_of(_list, &Task::isTooLong);\n\n\tconst auto lastEmpty = !_list.empty() && _list.back()->isEmpty();\n\t_addedCount = _list.size()\n\t\t- (lastEmpty ? 1 : 0)\n\t\t- (_existingLocked ? _existingCount : 0);\n}\n\nint Tasks::findField(not_null<Ui::InputField*> field) const {\n\tconst auto result = ranges::find(\n\t\t_list,\n\t\tfield,\n\t\t&Task::field) - begin(_list);\n\n\tEnsures(result >= 0 && result < _list.size());\n\treturn result;\n}\n\nvoid Tasks::handlePaste(\n\t\tnot_null<Ui::InputField*> field,\n\t\tconst QStringList &list) {\n\tconst auto index = findField(field);\n\tfor (auto i = 0, count = int(list.size()); i != count; ++i) {\n\t\tinsertTask(\n\t\t\tindex + 1 + i,\n\t\t\t0, // id\n\t\t\tTextWithEntities{ list[i] },\n\t\t\tanim::type::instant);\n\t}\n\tconst auto last = std::min(\n\t\tint(index + list.size()),\n\t\tint(_list.size()) - 1);\n\tconst auto add = _list[last]->field();\n\tcrl::on_main(add, [=] {\n\t\tadd->setCursorPosition(add->getLastText().size());\n\t\tadd->setFocus();\n\t});\n}\n\nvoid Tasks::checkLastTask() {\n\tremoveEmptyTail();\n\taddEmptyTask();\n}\n\n} // namespace\n\nEditTodoListBox::EditTodoListBox(\n\tQWidget*,\n\tnot_null<Window::SessionController*> controller,\n\trpl::producer<int> starsRequired,\n\tApi::SendType sendType,\n\tSendMenu::Details sendMenuDetails)\n: _controller(controller)\n, _sendType(sendType)\n, _sendMenuDetails([result = sendMenuDetails] { return result; })\n, _starsRequired(std::move(starsRequired))\n, _titleLimit(controller->session().appConfig().todoListTitleLimit()) {\n}\n\nEditTodoListBox::EditTodoListBox(\n\tQWidget*,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<HistoryItem*> item)\n: _controller(controller)\n, _sendMenuDetails([] { return SendMenu::Details(); })\n, _editingItem(item)\n, _titleLimit(controller->session().appConfig().todoListTitleLimit()) {\n\t_controller->session().changes().messageUpdates(\n\t\tData::MessageUpdate::Flag::Destroyed\n\t) | rpl::on_next([=](const Data::MessageUpdate &update) {\n\t\tif (update.item == item) {\n\t\t\tcloseBox();\n\t\t}\n\t}, lifetime());\n}\n\nauto EditTodoListBox::submitRequests() const -> rpl::producer<Result> {\n\treturn _submitRequests.events();\n}\n\nvoid EditTodoListBox::setInnerFocus() {\n\t_setInnerFocus();\n}\n\nvoid EditTodoListBox::submitFailed(const QString &error) {\n\tshowToast(error);\n}\n\nnot_null<Ui::InputField*> EditTodoListBox::setupTitle(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tusing namespace Settings;\n\n\tconst auto session = &_controller->session();\n\tconst auto isPremium = session->premium();\n\tconst auto title = container->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tcontainer,\n\t\t\tst::createPollField,\n\t\t\tUi::InputField::Mode::MultiLine,\n\t\t\ttr::lng_todo_create_title_placeholder()),\n\t\tst::createPollFieldPadding\n\t\t\t+ (isPremium\n\t\t\t\t? QMargins(0, 0, st::defaultComposeFiles.emoji.inner.width, 0)\n\t\t\t\t: QMargins()));\n\tInitField(getDelegate()->outerContainer(), title, session);\n\ttitle->setMaxLength(_titleLimit + kErrorLimit);\n\ttitle->setSubmitSettings(Ui::InputField::SubmitSettings::Both);\n\n\tif (isPremium) {\n\t\t_emojiPanel = MakeEmojiPanel(\n\t\t\tgetDelegate()->outerContainer(),\n\t\t\t_controller);\n\t\tconst auto emojiToggle = Ui::AddEmojiToggleToField(\n\t\t\ttitle,\n\t\t\tthis,\n\t\t\t_controller,\n\t\t\t_emojiPanel.get(),\n\t\t\tst::createTodoOptionFieldEmojiPosition);\n\t\t_emojiPanel->selector()->emojiChosen(\n\t\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\t\tif (title->hasFocus()) {\n\t\t\t\tUi::InsertEmojiAtCursor(title->textCursor(), data.emoji);\n\t\t\t}\n\t\t}, emojiToggle->lifetime());\n\t\t_emojiPanel->selector()->customEmojiChosen(\n\t\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\t\tif (title->hasFocus()) {\n\t\t\t\tData::InsertCustomEmoji(title, data.document);\n\t\t\t}\n\t\t}, emojiToggle->lifetime());\n\t}\n\n\tconst auto media = _editingItem ? _editingItem->media() : nullptr;\n\tif (const auto todolist = media ? media->todolist() : nullptr) {\n\t\tconst auto &text = todolist->title;\n\t\ttitle->setTextWithTags({\n\t\t\ttext.text,\n\t\t\tTextUtilities::ConvertEntitiesToTextTags(text.entities)\n\t\t});\n\t}\n\n\tconst auto warning = CreateWarningLabel(\n\t\tcontainer,\n\t\ttitle,\n\t\t_titleLimit,\n\t\tkWarnTitleLimit);\n\trpl::combine(\n\t\ttitle->geometryValue(),\n\t\twarning->sizeValue()\n\t) | rpl::on_next([=](QRect geometry, QSize label) {\n\t\twarning->moveToLeft(\n\t\t\t(container->width()\n\t\t\t\t- label.width()\n\t\t\t\t- st::createPollWarningPosition.x()),\n\t\t\t(geometry.y()\n\t\t\t\t- st::createPollFieldPadding.top()\n\t\t\t\t- st::defaultSubsectionTitlePadding.bottom()\n\t\t\t\t- st::defaultSubsectionTitle.style.font->height\n\t\t\t\t+ st::defaultSubsectionTitle.style.font->ascent\n\t\t\t\t- st::createPollWarning.style.font->ascent),\n\t\t\tgeometry.width());\n\t}, warning->lifetime());\n\n\treturn title;\n}\n\nobject_ptr<Ui::RpWidget> EditTodoListBox::setupContent() {\n\tusing namespace Settings;\n\n\tconst auto id = FullMsgId{\n\t\tPeerId(),\n\t\t_controller->session().data().nextNonHistoryEntryId(),\n\t};\n\tconst auto error = lifetime().make_state<Errors>(Error::Title);\n\n\tauto result = object_ptr<Ui::VerticalLayout>(this);\n\tconst auto container = result.data();\n\n\tconst auto title = setupTitle(container);\n\tUi::AddDivider(container);\n\tUi::AddSkip(container);\n\tcontainer->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_todo_create_list(),\n\t\t\tst::defaultSubsectionTitle),\n\t\tst::createPollFieldTitlePadding);\n\tconst auto media = _editingItem ? _editingItem->media() : nullptr;\n\tconst auto todolist = media ? media->todolist() : nullptr;\n\tconst auto tasks = lifetime().make_state<Tasks>(\n\t\tthis,\n\t\tcontainer,\n\t\t_controller,\n\t\t_emojiPanel ? _emojiPanel.get() : nullptr,\n\t\ttodolist ? todolist->items : std::vector<TodoListItem>());\n\tauto limit = tasks->addedCount() | rpl::after_next([=](int count) {\n\t\tsetCloseByEscape(!count);\n\t\tsetCloseByOutsideClick(!count);\n\t}) | rpl::map([=](int count) {\n\t\tconst auto appConfig = &_controller->session().appConfig();\n\t\tconst auto max = appConfig->todoListItemsLimit();\n\t\treturn (count < max)\n\t\t\t? tr::lng_todo_create_limit(tr::now, lt_count, max - count)\n\t\t\t: tr::lng_todo_create_maximum(tr::now);\n\t}) | rpl::after_next([=] {\n\t\tcontainer->resizeToWidth(container->widthNoMargins());\n\t});\n\tcontainer->add(\n\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\tstd::move(limit),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::createPollLimitPadding));\n\n\ttitle->tabbed(\n\t) | rpl::on_next([=](not_null<bool*> handled) {\n\t\ttasks->focusFirst();\n\t\t*handled = true;\n\t}, title->lifetime());\n\n\tUi::AddSkip(container);\n\tUi::AddSubsectionTitle(container, tr::lng_todo_create_settings());\n\n\tconst auto allowAdd = container->add(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tcontainer,\n\t\t\ttr::lng_todo_create_allow_add(tr::now),\n\t\t\t!todolist || todolist->othersCanAppend(),\n\t\t\tst::defaultCheckbox),\n\t\tst::createPollCheckboxMargin);\n\tconst auto allowMark = container->add(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tcontainer,\n\t\t\ttr::lng_todo_create_allow_mark(tr::now),\n\t\t\t!todolist || todolist->othersCanComplete(),\n\t\t\tst::defaultCheckbox),\n\t\tst::createPollCheckboxMargin);\n\n\ttasks->tabbed(\n\t) | rpl::on_next([=] {\n\t\ttitle->setFocus();\n\t}, title->lifetime());\n\n\tconst auto isValidTitle = [=] {\n\t\tconst auto text = title->getLastText().trimmed();\n\t\treturn !text.isEmpty() && (text.size() <= _titleLimit);\n\t};\n\ttitle->submits(\n\t) | rpl::on_next([=] {\n\t\tif (isValidTitle()) {\n\t\t\ttasks->focusFirst();\n\t\t}\n\t}, title->lifetime());\n\n\t_setInnerFocus = [=] {\n\t\ttitle->setFocusFast();\n\t};\n\n\tconst auto collectResult = [=] {\n\t\tconst auto textWithTags = title->getTextWithTags();\n\t\tusing Flag = TodoListData::Flag;\n\t\tauto result = TodoListData(&_controller->session().data(), id);\n\t\tresult.title.text = textWithTags.text;\n\t\tresult.title.entities = TextUtilities::ConvertTextTagsToEntities(\n\t\t\ttextWithTags.tags);\n\t\tTextUtilities::Trim(result.title);\n\t\tresult.items = tasks->toTodoListItems();\n\t\tconst auto allowAddTasks = allowAdd->checked();\n\t\tconst auto allowMarkTasks = allowMark->checked();\n\t\tresult.setFlags(Flag(0)\n\t\t\t| (allowAddTasks ? Flag::OthersCanAppend : Flag(0))\n\t\t\t| (allowMarkTasks ? Flag::OthersCanComplete : Flag(0)));\n\t\treturn result;\n\t};\n\tconst auto collectError = [=] {\n\t\tif (isValidTitle()) {\n\t\t\t*error &= ~Error::Title;\n\t\t} else {\n\t\t\t*error |= Error::Title;\n\t\t}\n\t\tif (!tasks->hasTasks()) {\n\t\t\t*error |= Error::Tasks;\n\t\t} else if (!tasks->isValid()) {\n\t\t\t*error |= Error::Other;\n\t\t} else {\n\t\t\t*error &= ~(Error::Tasks | Error::Other);\n\t\t}\n\t};\n\tconst auto showError = [show = uiShow()](\n\t\t\ttr::phrase<> text) {\n\t\tshow->showToast(text(tr::now));\n\t};\n\n\tconst auto send = [=](Api::SendOptions sendOptions) {\n\t\tcollectError();\n\t\tif (*error & Error::Title) {\n\t\t\tshowError(tr::lng_todo_choose_title);\n\t\t\ttitle->setFocus();\n\t\t} else if (*error & Error::Tasks) {\n\t\t\tshowError(tr::lng_todo_choose_tasks);\n\t\t\ttasks->focusFirst();\n\t\t} else if (!*error) {\n\t\t\tif (_editingItem) {\n\t\t\t\tsendOptions = {\n\t\t\t\t\t.scheduled = (_editingItem->isScheduled()\n\t\t\t\t\t\t? _editingItem->date()\n\t\t\t\t\t\t: TimeId()),\n\t\t\t\t\t.shortcutId = _editingItem->shortcutId(),\n\t\t\t\t};\n\t\t\t}\n\t\t\t_submitRequests.fire({ collectResult(), sendOptions });\n\t\t}\n\t};\n\tconst auto sendAction = SendMenu::DefaultCallback(\n\t\t_controller->uiShow(),\n\t\tcrl::guard(this, send));\n\n\ttasks->scrollToWidget(\n\t) | rpl::on_next([=](not_null<QWidget*> widget) {\n\t\tscrollToWidget(widget);\n\t}, lifetime());\n\n\ttasks->backspaceInFront(\n\t) | rpl::on_next([=] {\n\t\tFocusAtEnd(title);\n\t}, lifetime());\n\n\tconst auto isNormal = (_sendType == Api::SendType::Normal);\n\tconst auto schedule = [=] {\n\t\tsendAction(\n\t\t\t{ .type = SendMenu::ActionType::Schedule },\n\t\t\t_sendMenuDetails());\n\t};\n\tconst auto submit = addButton(\n\t\t(_editingItem\n\t\t\t? tr::lng_settings_save()\n\t\t\t: tr::lng_todo_create_button()),\n\t\t[=] { isNormal ? send({}) : schedule(); });\n\tsubmit->setText(PaidSendButtonText(_starsRequired.value(), _editingItem\n\t\t? tr::lng_settings_save()\n\t\t: isNormal\n\t\t? tr::lng_todo_create_button()\n\t\t: tr::lng_schedule_button()));\n\tconst auto sendMenuDetails = [=] {\n\t\tcollectError();\n\t\treturn (*error) ? SendMenu::Details() : _sendMenuDetails();\n\t};\n\tSendMenu::SetupMenuAndShortcuts(\n\t\tsubmit.data(),\n\t\t_controller->uiShow(),\n\t\tsendMenuDetails,\n\t\tsendAction);\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\n\treturn result;\n}\n\nvoid EditTodoListBox::prepare() {\n\tsetTitle(tr::lng_todo_create_title());\n\n\tconst auto inner = setInnerWidget(setupContent());\n\n\tsetDimensionsToContent(st::boxWideWidth, inner);\n}\n\nAddTodoListTasksBox::AddTodoListTasksBox(\n\tQWidget*,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<HistoryItem*> item)\n: _controller(controller)\n, _item(item) {\n\t_controller->session().changes().messageUpdates(\n\t\tData::MessageUpdate::Flag::Destroyed\n\t) | rpl::on_next([=](const Data::MessageUpdate &update) {\n\t\tif (update.item == item) {\n\t\t\tcloseBox();\n\t\t}\n\t}, lifetime());\n}\n\nvoid AddTodoListTasksBox::prepare() {\n\tsetTitle(tr::lng_todo_add_title());\n\n\tconst auto inner = setInnerWidget(setupContent());\n\n\tsetDimensionsToContent(st::boxWideWidth, inner);\n\n\tscrollToY(ScrollMax);\n}\n\nobject_ptr<Ui::RpWidget> AddTodoListTasksBox::setupContent() {\n\tauto result = object_ptr<Ui::VerticalLayout>(this);\n\tconst auto container = result.data();\n\n\tif (_controller->session().premium()) {\n\t\t_emojiPanel = MakeEmojiPanel(\n\t\t\tgetDelegate()->outerContainer(),\n\t\t\t_controller);\n\t}\n\n\tconst auto media = _item->media();\n\tconst auto todolist = media ? media->todolist() : nullptr;\n\tAssert(todolist != nullptr);\n\tconst auto tasks = lifetime().make_state<Tasks>(\n\t\tthis,\n\t\tcontainer,\n\t\t_controller,\n\t\t_emojiPanel ? _emojiPanel.get() : nullptr,\n\t\ttodolist->items,\n\t\ttrue);\n\tconst auto already = int(todolist->items.size());\n\tauto limit = tasks->addedCount() | rpl::after_next([=](int count) {\n\t\tsetCloseByEscape(!count);\n\t\tsetCloseByOutsideClick(!count);\n\t}) | rpl::map([=](int count) {\n\t\tconst auto appConfig = &_controller->session().appConfig();\n\t\tconst auto max = appConfig->todoListItemsLimit();\n\t\tconst auto total = already + count;\n\t\treturn (total < max)\n\t\t\t? tr::lng_todo_create_limit(tr::now, lt_count, max - total)\n\t\t\t: tr::lng_todo_create_maximum(tr::now);\n\t}) | rpl::after_next([=] {\n\t\tcontainer->resizeToWidth(container->widthNoMargins());\n\t});\n\tcontainer->add(\n\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\tstd::move(limit),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::createPollLimitPadding));\n\n\t_setInnerFocus = [=] {\n\t\ttasks->focusFirst();\n\t};\n\n\ttasks->scrollToWidget(\n\t) | rpl::on_next([=](not_null<QWidget*> widget) {\n\t\tscrollToWidget(widget);\n\t}, lifetime());\n\n\tconst auto submit = addButton(tr::lng_settings_save(), [=] {\n\t\t_submitRequests.fire({ tasks->toTodoListItems() });\n\t});\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\n\treturn result;\n}\n\nauto AddTodoListTasksBox::submitRequests() const -> rpl::producer<Result> {\n\treturn _submitRequests.events();\n}\n\nvoid AddTodoListTasksBox::setInnerFocus() {\n\t_setInnerFocus();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/edit_todo_list_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n#include \"api/api_common.h\"\n#include \"data/data_todo_list.h\"\n#include \"base/flags.h\"\n\nstruct TodoListData;\n\nnamespace ChatHelpers {\nclass TabbedPanel;\n} // namespace ChatHelpers\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nclass EditTodoListBox : public Ui::BoxContent {\npublic:\n\tstruct Result {\n\t\tTodoListData todolist;\n\t\tApi::SendOptions options;\n\t};\n\n\tEditTodoListBox(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionController*> controller,\n\t\trpl::producer<int> starsRequired,\n\t\tApi::SendType sendType,\n\t\tSendMenu::Details sendMenuDetails);\n\tEditTodoListBox(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item);\n\n\t[[nodiscard]] rpl::producer<Result> submitRequests() const;\n\tvoid submitFailed(const QString &error);\n\n\tvoid setInnerFocus() override;\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\tenum class Error {\n\t\tTitle = 0x01,\n\t\tTasks = 0x02,\n\t\tOther = 0x04,\n\t};\n\tfriend constexpr inline bool is_flag_type(Error) { return true; }\n\tusing Errors = base::flags<Error>;\n\n\t[[nodiscard]] object_ptr<Ui::RpWidget> setupContent();\n\t[[nodiscard]] not_null<Ui::InputField*> setupTitle(\n\t\tnot_null<Ui::VerticalLayout*> container);\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst Api::SendType _sendType = Api::SendType();\n\tconst Fn<SendMenu::Details()> _sendMenuDetails;\n\tHistoryItem *_editingItem = nullptr;\n\trpl::variable<int> _starsRequired;\n\tbase::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;\n\tFn<void()> _setInnerFocus;\n\tFn<rpl::producer<bool>()> _dataIsValidValue;\n\trpl::event_stream<Result> _submitRequests;\n\tint _titleLimit = 0;\n\n};\n\nclass AddTodoListTasksBox : public Ui::BoxContent {\npublic:\n\tstruct Result {\n\t\tstd::vector<TodoListItem> items;\n\t};\n\n\tAddTodoListTasksBox(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item);\n\n\t[[nodiscard]] rpl::producer<Result> submitRequests() const;\n\n\tvoid setInnerFocus() override;\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\t[[nodiscard]] object_ptr<Ui::RpWidget> setupContent();\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<HistoryItem*> _item;\n\tbase::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;\n\tFn<void()> _setInnerFocus;\n\trpl::event_stream<Result> _submitRequests;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/filters/edit_filter_box.h\"\n\n#include \"apiwrap.h\"\n#include \"base/event_filter.h\"\n#include \"boxes/filters/edit_filter_chats_list.h\"\n#include \"boxes/filters/edit_filter_chats_preview.h\"\n#include \"boxes/filters/edit_filter_links.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/ui_integration.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_document.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_peer_values.h\" // Data::AmPremiumValue.\n#include \"data/data_premium_limits.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"info/userpic/info_userpic_color_circle_button.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/chat/chats_filter_tag.h\"\n#include \"ui/controls/emoji_button_factory.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/effects/animation_value_f.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/panel_animation.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/filter_icon_panel.h\"\n#include \"ui/filter_icons.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_info_userpic_builder.h\"\n\nnamespace {\n\nusing namespace Settings;\n\nconstexpr auto kMaxFilterTitleLength = 12;\n\nusing Flag = Data::ChatFilter::Flag;\nusing Flags = Data::ChatFilter::Flags;\nusing ExceptionPeersRef = const base::flat_set<not_null<History*>> &;\nusing ExceptionPeersGetter = ExceptionPeersRef(Data::ChatFilter::*)() const;\n\nstruct NameEditing {\n\tnot_null<Ui::InputField*> field;\n\tbool custom = false;\n\tbool settingDefault = false;\n};\n\nnot_null<FilterChatsPreview*> SetupChatsPreview(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tnot_null<rpl::variable<Data::ChatFilter>*> data,\n\t\tFn<void(const Data::ChatFilter&)> updateDefaultTitle,\n\t\tFlags flags,\n\t\tExceptionPeersGetter peers) {\n\tconst auto rules = data->current();\n\tconst auto preview = content->add(object_ptr<FilterChatsPreview>(\n\t\tcontent,\n\t\trules.flags() & flags,\n\t\t(rules.*peers)()));\n\n\tpreview->flagRemoved(\n\t) | rpl::on_next([=](Flag flag) {\n\t\tconst auto rules = data->current();\n\t\tauto computed = Data::ChatFilter(\n\t\t\trules.id(),\n\t\t\trules.title(),\n\t\t\trules.iconEmoji(),\n\t\t\trules.colorIndex(),\n\t\t\t(rules.flags() & ~flag),\n\t\t\trules.always(),\n\t\t\trules.pinned(),\n\t\t\trules.never());\n\t\tupdateDefaultTitle(computed);\n\t\t*data = std::move(computed);\n\t}, preview->lifetime());\n\n\tpreview->peerRemoved(\n\t) | rpl::on_next([=](not_null<History*> history) {\n\t\tconst auto rules = data->current();\n\t\tauto always = rules.always();\n\t\tauto pinned = rules.pinned();\n\t\tauto never = rules.never();\n\t\talways.remove(history);\n\t\tpinned.erase(ranges::remove(pinned, history), end(pinned));\n\t\tnever.remove(history);\n\t\tauto computed = Data::ChatFilter(\n\t\t\trules.id(),\n\t\t\trules.title(),\n\t\t\trules.iconEmoji(),\n\t\t\trules.colorIndex(),\n\t\t\trules.flags(),\n\t\t\tstd::move(always),\n\t\t\tstd::move(pinned),\n\t\t\tstd::move(never));\n\t\tupdateDefaultTitle(computed);\n\t\t*data = std::move(computed);\n\t}, preview->lifetime());\n\n\treturn preview;\n}\n\nvoid EditExceptions(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<QObject*> context,\n\t\tFlags options,\n\t\tnot_null<rpl::variable<Data::ChatFilter>*> data,\n\t\tFn<void(const Data::ChatFilter&)> updateDefaultTitle,\n\t\tFn<void()> refresh) {\n\tconst auto include = (options & Flag::Contacts) != Flags(0);\n\tconst auto rules = data->current();\n\tconst auto session = &window->session();\n\tconst auto limit = Data::PremiumLimits(\n\t\tsession\n\t).dialogFiltersChatsCurrent();\n\tconst auto showLimitReached = [=] {\n\t\twindow->show(Box(FilterChatsLimitBox, session, limit, include));\n\t};\n\tauto controller = std::make_unique<EditFilterChatsListController>(\n\t\tsession,\n\t\t(include\n\t\t\t? tr::lng_filters_include_title()\n\t\t\t: tr::lng_filters_exclude_title()),\n\t\toptions,\n\t\trules.flags() & options,\n\t\tinclude ? rules.always() : rules.never(),\n\t\tlimit,\n\t\tshowLimitReached);\n\tconst auto rawController = controller.get();\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tbox->setCloseByOutsideClick(false);\n\t\tbox->addButton(tr::lng_settings_save(), crl::guard(context, [=] {\n\t\t\tconst auto peers = box->collectSelectedRows();\n\t\t\tconst auto rules = data->current();\n\t\t\tauto &&histories = ranges::views::all(\n\t\t\t\tpeers\n\t\t\t) | ranges::views::transform([=](not_null<PeerData*> peer) {\n\t\t\t\treturn window->session().data().history(peer);\n\t\t\t});\n\t\t\tauto changed = base::flat_set<not_null<History*>>{\n\t\t\t\thistories.begin(),\n\t\t\t\thistories.end()\n\t\t\t};\n\t\t\tauto removeFrom = include ? rules.never() : rules.always();\n\t\t\tfor (const auto &history : changed) {\n\t\t\t\tremoveFrom.remove(history);\n\t\t\t}\n\t\t\tauto pinned = rules.pinned();\n\t\t\tpinned.erase(ranges::remove_if(pinned, [&](\n\t\t\t\t\tnot_null<History*> history) {\n\t\t\t\tconst auto contains = changed.contains(history);\n\t\t\t\treturn include ? !contains : contains;\n\t\t\t}), end(pinned));\n\t\t\tauto computed = Data::ChatFilter(\n\t\t\t\trules.id(),\n\t\t\t\trules.title(),\n\t\t\t\trules.iconEmoji(),\n\t\t\t\trules.colorIndex(),\n\t\t\t\t((rules.flags() & ~options)\n\t\t\t\t\t| rawController->chosenOptions()),\n\t\t\t\tinclude ? std::move(changed) : std::move(removeFrom),\n\t\t\t\tstd::move(pinned),\n\t\t\t\tinclude ? std::move(removeFrom) : std::move(changed));\n\t\t\tupdateDefaultTitle(computed);\n\t\t\t*data = computed;\n\t\t\trefresh();\n\t\t\tbox->closeBox();\n\t\t}));\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t};\n\twindow->window().show(\n\t\tBox<PeerListBox>(std::move(controller), std::move(initBox)));\n}\n\nvoid CreateIconSelector(\n\t\tnot_null<QWidget*> outer,\n\t\tnot_null<QWidget*> box,\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Ui::InputField*> input,\n\t\tnot_null<rpl::variable<Data::ChatFilter>*> data) {\n\tconst auto rules = data->current();\n\tconst auto toggle = Ui::CreateChild<Ui::AbstractButton>(parent.get());\n\ttoggle->resize(st::windowFilterIconToggleSize);\n\n\tconst auto type = toggle->lifetime().make_state<Ui::FilterIcon>();\n\tdata->value(\n\t) | rpl::map([=](const Data::ChatFilter &filter) {\n\t\treturn Ui::ComputeFilterIcon(filter);\n\t}) | rpl::on_next([=](Ui::FilterIcon icon) {\n\t\t*type = icon;\n\t\ttoggle->update();\n\t}, toggle->lifetime());\n\n\tinput->geometryValue(\n\t) | rpl::on_next([=](QRect geometry) {\n\t\tconst auto left = geometry.x() + geometry.width() - toggle->width();\n\t\tconst auto position = st::windowFilterIconTogglePosition;\n\t\ttoggle->move(\n\t\t\tleft - position.x(),\n\t\t\tgeometry.y() + position.y());\n\t}, toggle->lifetime());\n\n\ttoggle->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(toggle);\n\t\tconst auto icons = Ui::LookupFilterIcon(*type);\n\t\ticons.normal->paintInCenter(\n\t\t\tp,\n\t\t\ttoggle->rect(),\n\t\t\tst::dialogsUnreadBgMuted->c);\n\t}, toggle->lifetime());\n\n\tconst auto panel = toggle->lifetime().make_state<Ui::FilterIconPanel>(\n\t\touter);\n\ttoggle->installEventFilter(panel);\n\ttoggle->addClickHandler([=] {\n\t\tpanel->toggleAnimated();\n\t});\n\tpanel->chosen(\n\t) | rpl::filter([=](Ui::FilterIcon icon) {\n\t\treturn icon != Ui::ComputeFilterIcon(data->current());\n\t}) | rpl::on_next([=](Ui::FilterIcon icon) {\n\t\tpanel->hideAnimated();\n\t\tconst auto rules = data->current();\n\t\t*data = Data::ChatFilter(\n\t\t\trules.id(),\n\t\t\trules.title(),\n\t\t\tUi::LookupFilterIcon(icon).emoji,\n\t\t\trules.colorIndex(),\n\t\t\trules.flags(),\n\t\t\trules.always(),\n\t\t\trules.pinned(),\n\t\t\trules.never());\n\t}, panel->lifetime());\n\n\tconst auto updatePanelGeometry = [=] {\n\t\tconst auto global = toggle->mapToGlobal({\n\t\t\ttoggle->width(),\n\t\t\ttoggle->height()\n\t\t});\n\t\tconst auto local = outer->mapFromGlobal(global);\n\t\tconst auto position = st::windwoFilterIconPanelPosition;\n\t\tconst auto padding = panel->innerPadding();\n\t\tpanel->move(\n\t\t\tlocal.x() - panel->width() + position.x() + padding.right(),\n\t\t\tlocal.y() + position.y() - padding.top());\n\t};\n\n\tconst auto filterForGeometry = [=](not_null<QEvent*> event) {\n\t\tconst auto type = event->type();\n\t\tif (type == QEvent::Move || type == QEvent::Resize) {\n\t\t\t// updatePanelGeometry uses not only container geometry, but\n\t\t\t// also container children geometries that will be updated later.\n\t\t\tcrl::on_main(panel, [=] { updatePanelGeometry(); });\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t};\n\n\tconst auto installFilterForGeometry = [&](not_null<QWidget*> target) {\n\t\tpanel->lifetime().make_state<base::unique_qptr<QObject>>(\n\t\t\tbase::install_event_filter(target, filterForGeometry));\n\t};\n\tinstallFilterForGeometry(outer);\n\tinstallFilterForGeometry(box);\n}\n\n[[nodiscard]] QString DefaultTitle(const Data::ChatFilter &filter) {\n\tusing Icon = Ui::FilterIcon;\n\tconst auto icon = Ui::ComputeDefaultFilterIcon(filter);\n\tswitch (icon) {\n\tcase Icon::Private:\n\t\treturn (filter.flags() & Data::ChatFilter::Flag::NonContacts)\n\t\t\t? tr::lng_filters_name_people(tr::now)\n\t\t\t: tr::lng_filters_include_contacts(tr::now);\n\tcase Icon::Groups:\n\t\treturn tr::lng_filters_include_groups(tr::now);\n\tcase Icon::Channels:\n\t\treturn tr::lng_filters_include_channels(tr::now);\n\tcase Icon::Bots:\n\t\treturn tr::lng_filters_include_bots(tr::now);\n\tcase Icon::Unread:\n\t\treturn tr::lng_filters_name_unread(tr::now);\n\tcase Icon::Unmuted:\n\t\treturn tr::lng_filters_name_unmuted(tr::now);\n\t}\n\treturn QString();\n}\n\nnot_null<Ui::SettingsButton*> AddToggledButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<bool> shown,\n\t\trpl::producer<QString> text,\n\t\tconst style::SettingsButton &st,\n\t\tIconDescriptor &&descriptor) {\n\tconst auto toggled = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\tcontainer,\n\t\t\tCreateButtonWithIcon(\n\t\t\t\tcontainer,\n\t\t\t\tstd::move(text),\n\t\t\t\tst,\n\t\t\t\tstd::move(descriptor)))\n\t)->toggleOn(std::move(shown), anim::type::instant)->setDuration(0);\n\treturn toggled->entity();\n}\n\n[[nodiscard]] QString TrimDefaultTitle(const QString &title) {\n\treturn (title.size() <= kMaxFilterTitleLength) ? title : QString();\n}\n\n} // namespace\n\nvoid EditFilterBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> window,\n\t\tconst Data::ChatFilter &filter,\n\t\tFn<void(const Data::ChatFilter &)> doneCallback,\n\t\tFn<void(\n\t\t\tconst Data::ChatFilter &data,\n\t\t\tFn<void(Data::ChatFilter)> next)> saveAnd) {\n\tusing namespace rpl::mappers;\n\tconstexpr auto kColorsCount = 8;\n\tconstexpr auto kNoTag = kColorsCount - 1;\n\n\tstruct State {\n\t\trpl::variable<Data::ChatFilter> rules;\n\t\trpl::variable<std::vector<Data::ChatFilterLink>> links;\n\t\trpl::variable<bool> hasLinks;\n\t\trpl::variable<bool> chatlist;\n\t\trpl::variable<bool> creating;\n\t\trpl::variable<TextWithEntities> title;\n\t\trpl::variable<bool> staticTitle;\n\t\trpl::variable<int> colorIndex;\n\t\tbase::unique_qptr<ChatHelpers::TabbedPanel> emojiPanel;\n\t};\n\tconst auto owner = &window->session().data();\n\tconst auto state = box->lifetime().make_state<State>(State{\n\t\t.rules = filter,\n\t\t.chatlist = filter.chatlist(),\n\t\t.creating = filter.title().empty(),\n\t\t.title = filter.titleText(),\n\t\t.staticTitle = filter.staticTitle(),\n\t});\n\tstate->colorIndex = filter.colorIndex().value_or(kNoTag);\n\tstate->links = owner->chatsFilters().chatlistLinks(filter.id()),\n\tstate->hasLinks = state->links.value() | rpl::map([=](const auto &v) {\n\t\treturn !v.empty();\n\t});\n\tstate->hasLinks.value() | rpl::filter(\n\t\t_1\n\t) | rpl::on_next([=] {\n\t\tstate->chatlist = true;\n\t}, box->lifetime());\n\n\tconst auto data = &state->rules;\n\n\towner->chatsFilters().isChatlistChanged(\n\t) | rpl::filter([=](FilterId id) {\n\t\treturn (id == data->current().id());\n\t}) | rpl::on_next([=](FilterId id) {\n\t\tconst auto filters = &owner->chatsFilters();\n\t\tconst auto &list = filters->list();\n\t\tconst auto i = ranges::find(list, id, &Data::ChatFilter::id);\n\t\tif (i == end(list)) {\n\t\t\treturn;\n\t\t}\n\t\t*data = data->current().withChatlist(i->chatlist(), i->hasMyLinks());\n\t\tif (!i->chatlist() && !state->hasLinks.current()) {\n\t\t\tstate->chatlist = false;\n\t\t}\n\t}, box->lifetime());\n\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setTitle(rpl::conditional(\n\t\tstate->creating.value(),\n\t\ttr::lng_filters_new(),\n\t\ttr::lng_filters_edit()));\n\tbox->setCloseByOutsideClick(false);\n\n\tconst auto session = &window->session();\n\tData::AmPremiumValue(\n\t\tsession\n\t) | rpl::on_next([=] {\n\t\tbox->closeBox();\n\t}, box->lifetime());\n\n\tconst auto content = box->verticalLayout();\n\tconst auto current = state->title.current();\n\tconst auto name = content->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tbox,\n\t\t\tst::windowFilterNameInput,\n\t\t\tUi::InputField::Mode::SingleLine,\n\t\t\ttr::lng_filters_new_name()),\n\t\tst::markdownLinkFieldPadding);\n\tInitMessageFieldHandlers(window, name, ChatHelpers::PauseReason::Layer);\n\tname->setTextWithTags({\n\t\tcurrent.text,\n\t\tTextUtilities::ConvertEntitiesToTextTags(current.entities),\n\t}, Ui::InputField::HistoryAction::Clear);\n\tUi::AddLengthLimitLabel(\n\t\tname,\n\t\tkMaxFilterTitleLength,\n\t\tUi::LengthLimitLabelOptions{\n\t\t\t.customThreshold = 0,\n\t\t\t.customUpdatePosition = [=](QSize parent, QSize label) {\n\t\t\t\treturn QPoint(\n\t\t\t\t\tparent.width()\n\t\t\t\t\t\t- st::windowFilterNameCharsLimitRightPosition.x()\n\t\t\t\t\t\t- label.width() / 2,\n\t\t\t\t\tst::windowFilterNameCharsLimitRightPosition.y());\n\t\t\t},\n\t\t\t.customCharactersCount = [=] {\n\t\t\t\treturn Ui::ComputeFieldCharacterCount(name);\n\t\t\t},\n\t\t});\n\n\tconst auto nameEditing = box->lifetime().make_state<NameEditing>(\n\t\tNameEditing{ name });\n\n\tconst auto staticTitle = Ui::CreateChild<Ui::LinkButton>(\n\t\tname,\n\t\tQString());\n\tstaticTitle->setClickedCallback([=] {\n\t\tstate->staticTitle = !state->staticTitle.current();\n\t});\n\tstate->staticTitle.value() | rpl::on_next([=](bool value) {\n\t\tstaticTitle->setText(value\n\t\t\t? tr::lng_filters_enable_animations(tr::now)\n\t\t\t: tr::lng_filters_disable_animations(tr::now));\n\t\tconst auto paused = [=] {\n\t\t\tusing namespace Window;\n\t\t\treturn window->isGifPausedAtLeastFor(GifPauseReason::Layer);\n\t\t};\n\t\tname->setCustomTextContext(Core::TextContext({\n\t\t\t.session = session,\n\t\t\t.customEmojiLoopLimit = value ? -1 : 0,\n\t\t}), [paused] {\n\t\t\treturn On(PowerSaving::kEmojiChat) || paused();\n\t\t}, [paused] {\n\t\t\treturn On(PowerSaving::kChatSpoiler) || paused();\n\t\t});\n\t\tname->update();\n\t}, staticTitle->lifetime());\n\n\trpl::combine(\n\t\tstaticTitle->widthValue(),\n\t\tname->widthValue()\n\t) | rpl::on_next([=](int inner, int outer) {\n\t\tstaticTitle->moveToRight(\n\t\t\tst::windowFilterStaticTitlePosition.x(),\n\t\t\tst::windowFilterStaticTitlePosition.y(),\n\t\t\touter);\n\t}, staticTitle->lifetime());\n\n\tstate->creating.value(\n\t) | rpl::filter(!_1) | rpl::on_next([=] {\n\t\tnameEditing->custom = true;\n\t}, box->lifetime());\n\n\tusing Selector = ChatHelpers::TabbedSelector;\n\tstate->emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(\n\t\tbox->getDelegate()->outerContainer(),\n\t\twindow,\n\t\tobject_ptr<Selector>(\n\t\t\tnullptr,\n\t\t\twindow->uiShow(),\n\t\t\tWindow::GifPauseReason::Layer,\n\t\t\tSelector::Mode::EmojiOnly));\n\tstate->emojiPanel->setDesiredHeightValues(\n\t\t1.,\n\t\tst::emojiPanMinHeight / 2,\n\t\tst::emojiPanMinHeight);\n\tstate->emojiPanel->hide();\n\tstate->emojiPanel->selector()->setCurrentPeer(window->session().user());\n\tstate->emojiPanel->selector()->emojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\tUi::InsertEmojiAtCursor(name->textCursor(), data.emoji);\n\t}, name->lifetime());\n\tstate->emojiPanel->selector()->customEmojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\tconst auto info = data.document->sticker();\n\t\tif (info\n\t\t\t&& info->setType == Data::StickersType::Emoji\n\t\t\t&& !window->session().premium()) {\n\t\t\tShowPremiumPreviewBox(\n\t\t\t\twindow,\n\t\t\t\tPremiumFeature::AnimatedEmoji);\n\t\t} else {\n\t\t\tData::InsertCustomEmoji(name, data.document);\n\t\t}\n\t}, name->lifetime());\n\n\tconst auto emojiButton = Ui::AddEmojiToggleToField(\n\t\tname,\n\t\tbox,\n\t\twindow,\n\t\tstate->emojiPanel.get(),\n\t\tst::windowFilterNameEmojiPosition);\n\temojiButton->show();\n\n\tname->changes(\n\t) | rpl::on_next([=] {\n\t\tif (!nameEditing->settingDefault) {\n\t\t\tnameEditing->custom = true;\n\t\t}\n\t\tauto entered = name->getTextWithTags();\n\t\tstate->title = TextWithEntities{\n\t\t\tstd::move(entered.text),\n\t\t\tTextUtilities::ConvertTextTagsToEntities(entered.tags),\n\t\t};\n\t}, name->lifetime());\n\n\tconst auto updateDefaultTitle = [=](const Data::ChatFilter &filter) {\n\t\tif (nameEditing->custom) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto title = TrimDefaultTitle(DefaultTitle(filter));\n\t\tif (nameEditing->field->getLastText() != title) {\n\t\t\tnameEditing->settingDefault = true;\n\t\t\tnameEditing->field->setText(title);\n\t\t\tnameEditing->settingDefault = false;\n\t\t}\n\t};\n\n\tstate->title.value(\n\t) | rpl::on_next([=](const TextWithEntities &value) {\n\t\tstaticTitle->setVisible(!value.entities.isEmpty());\n\t}, staticTitle->lifetime());\n\n\tconst auto outer = box->getDelegate()->outerContainer();\n\tCreateIconSelector(\n\t\touter,\n\t\tbox,\n\t\tcontent,\n\t\tname,\n\t\tdata);\n\n\tconstexpr auto kTypes = Flag::Contacts\n\t\t| Flag::NonContacts\n\t\t| Flag::Groups\n\t\t| Flag::Channels\n\t\t| Flag::Bots;\n\tconstexpr auto kExcludeTypes = Flag::NoMuted\n\t\t| Flag::NoArchived\n\t\t| Flag::NoRead;\n\n\tbox->setFocusCallback([=] {\n\t\tname->setFocusFast();\n\t});\n\n\tUi::AddSkip(content);\n\tUi::AddDivider(content);\n\tUi::AddSkip(content);\n\tUi::AddSubsectionTitle(content, tr::lng_filters_include());\n\n\tconst auto includeAdd = AddButtonWithIcon(\n\t\tcontent,\n\t\ttr::lng_filters_add_chats(),\n\t\tst::settingsButtonActive,\n\t\t{ &st::settingsIconAdd, IconType::Round, &st::windowBgActive });\n\n\tconst auto include = SetupChatsPreview(\n\t\tcontent,\n\t\tdata,\n\t\tupdateDefaultTitle,\n\t\tkTypes,\n\t\t&Data::ChatFilter::always);\n\n\tUi::AddSkip(content);\n\tUi::AddDividerText(content, tr::lng_filters_include_about());\n\tUi::AddSkip(content);\n\n\tauto excludeWrap = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::VerticalLayout>(content))\n\t)->setDuration(0);\n\texcludeWrap->toggleOn(state->chatlist.value() | rpl::map(!_1));\n\tconst auto excludeInner = excludeWrap->entity();\n\n\tUi::AddSubsectionTitle(excludeInner, tr::lng_filters_exclude());\n\n\tconst auto excludeAdd = AddButtonWithIcon(\n\t\texcludeInner,\n\t\ttr::lng_filters_remove_chats(),\n\t\tst::settingsButtonActive,\n\t\t{ &st::settingsIconRemove, IconType::Round, &st::windowBgActive });\n\n\tconst auto exclude = SetupChatsPreview(\n\t\texcludeInner,\n\t\tdata,\n\t\tupdateDefaultTitle,\n\t\tkExcludeTypes,\n\t\t&Data::ChatFilter::never);\n\n\tUi::AddSkip(excludeInner);\n\tUi::AddDividerText(excludeInner, tr::lng_filters_exclude_about());\n\tUi::AddSkip(excludeInner);\n\n\t{\n\t\tconst auto wrap = content->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontent,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(content)));\n\t\tconst auto colors = wrap->entity();\n\t\tconst auto session = &window->session();\n\n\t\twrap->toggleOn(\n\t\t\trpl::combine(\n\t\t\t\tsession->premiumPossibleValue(),\n\t\t\t\tsession->data().chatsFilters().tagsEnabledValue(),\n\t\t\t\tData::AmPremiumValue(session)\n\t\t\t) | rpl::map([=] (bool possible, bool tagsEnabled, bool premium) {\n\t\t\t\treturn possible && (tagsEnabled || !premium);\n\t\t\t}),\n\t\t\tanim::type::instant);\n\n\t\tconst auto &padding = st::defaultSubsectionTitlePadding;\n\t\tconst auto isPremium = session->premium();\n\t\tconst auto titleWrap = colors->add(\n\t\t\tobject_ptr<Ui::FixedHeightWidget>(\n\t\t\t\tcolors,\n\t\t\t\trect::m::sum::v(padding)\n\t\t\t\t\t+ st::defaultSubsectionTitle.style.font->height));\n\t\tconst auto title = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\ttitleWrap,\n\t\t\ttr::lng_filters_tag_color_subtitle(),\n\t\t\tst::defaultSubsectionTitle);\n\t\ttitle->move(rect::m::pos::tl(padding));\n\t\tconst auto preview = Ui::CreateChild<Ui::RpWidget>(titleWrap);\n\t\trpl::combine(\n\t\t\ttitle->sizeValue(),\n\t\t\ttitleWrap->widthValue()\n\t\t) | rpl::on_next([=](const QSize &s, int w) {\n\t\t\tconst auto h = st::normalFont->height;\n\t\t\tconst auto left = padding.left()\n\t\t\t\t+ s.width()\n\t\t\t\t+ st::settingsFilterTagPreviewSkip;\n\t\t\tpreview->setGeometry(\n\t\t\t\tleft,\n\t\t\t\tpadding.top() + (s.height() - h) / 2,\n\t\t\t\tw - left,\n\t\t\t\th);\n\t\t}, preview->lifetime());\n\n\t\tstruct TagState {\n\t\t\tUi::Animations::Simple animation;\n\t\t\tUi::ChatsFilterTagContext context;\n\t\t\tQImage frame;\n\t\t\tfloat64 alpha = 1.;\n\t\t};\n\t\tconst auto tag = preview->lifetime().make_state<TagState>();\n\t\ttag->context.textContext = Core::TextContext({ .session = session });\n\t\tconst auto shift = st::settingsFilterTagPreviewSkip / 2;\n\t\tpreview->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(preview);\n\t\t\tp.setOpacity(tag->alpha);\n\t\t\tconst auto size = tag->frame.size() / style::DevicePixelRatio();\n\t\t\tconst auto rect = QRect(\n\t\t\t\tpreview->width()\n\t\t\t\t\t- size.width()\n\t\t\t\t\t- st::boxRowPadding.right()\n\t\t\t\t\t- shift,\n\t\t\t\t(st::normalFont->height - size.height()) / 2,\n\t\t\t\tsize.width(),\n\t\t\t\tsize.height());\n\t\t\tp.drawImage(rect.topLeft(), tag->frame);\n\t\t\tif (p.opacity() < 1) {\n\t\t\t\tp.setOpacity(1. - p.opacity());\n\t\t\t\tp.setFont(st::normalFont);\n\t\t\t\tp.setPen(st::windowSubTextFg);\n\t\t\t\tp.drawText(\n\t\t\t\t\tpreview->rect().translated(-shift, 0) - st::boxRowPadding,\n\t\t\t\t\ttr::lng_filters_tag_color_no(tr::now),\n\t\t\t\t\tstyle::al_right);\n\t\t\t}\n\t\t}, preview->lifetime());\n\n\t\tconst auto side = st::userpicBuilderEmojiAccentColorSize;\n\t\tconst auto line = colors->add(\n\t\t\tUi::CreateSkipWidget(colors, side),\n\t\t\tst::boxRowPadding);\n\t\tauto buttons = std::vector<not_null<UserpicBuilder::CircleButton*>>();\n\t\tconst auto palette = [](int i) {\n\t\t\treturn Ui::EmptyUserpic::UserpicColor(i).color2;\n\t\t};\n\t\tconst auto upperTitle = [=] {\n\t\t\tauto value = state->title.current();\n\t\t\tvalue.text = value.text.toUpper();\n\t\t\treturn value;\n\t\t};\n\t\tstate->title.changes(\n\t\t) | rpl::on_next([=] {\n\t\t\ttag->context.color = palette(state->colorIndex.current())->c;\n\t\t\ttag->frame = Ui::ChatsFilterTag(\n\t\t\t\tupperTitle(),\n\t\t\t\ttag->context);\n\t\t\tpreview->update();\n\t\t}, preview->lifetime());\n\t\tfor (auto i = 0; i < kColorsCount; ++i) {\n\t\t\tconst auto button = Ui::CreateChild<UserpicBuilder::CircleButton>(\n\t\t\t\tline);\n\t\t\tbutton->resize(side, side);\n\t\t\tconst auto progress = isPremium\n\t\t\t\t? (state->colorIndex.current() == i)\n\t\t\t\t: (i == kNoTag);\n\t\t\tbutton->setSelectedProgress(progress);\n\t\t\tconst auto color = palette(i);\n\t\t\tbutton->setBrush(color);\n\t\t\tif (progress == 1) {\n\t\t\t\ttag->context.color = color->c;\n\t\t\t\ttag->frame = Ui::ChatsFilterTag(\n\t\t\t\t\tupperTitle(),\n\t\t\t\t\ttag->context);\n\t\t\t\tif (i == kNoTag) {\n\t\t\t\t\ttag->alpha = 0.;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbuttons.push_back(button);\n\t\t}\n\t\tfor (auto i = 0; i < kColorsCount; ++i) {\n\t\t\tconst auto &button = buttons[i];\n\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\tconst auto was = state->colorIndex.current();\n\t\t\t\tconst auto now = i;\n\t\t\t\tif (was != now) {\n\t\t\t\t\tconst auto c1 = palette(was);\n\t\t\t\t\tconst auto c2 = palette(now);\n\t\t\t\t\tconst auto a1 = (was == kNoTag) ? 0. : 1.;\n\t\t\t\t\tconst auto a2 = (now == kNoTag) ? 0. : 1.;\n\t\t\t\t\ttag->animation.stop();\n\t\t\t\t\ttag->animation.start([=](float64 progress) {\n\t\t\t\t\t\tif (was >= 0) {\n\t\t\t\t\t\t\tbuttons[was]->setSelectedProgress(1. - progress);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbuttons[now]->setSelectedProgress(progress);\n\t\t\t\t\t\ttag->context.color = anim::color(c1, c2, progress);\n\t\t\t\t\t\ttag->frame = Ui::ChatsFilterTag(\n\t\t\t\t\t\t\tupperTitle(),\n\t\t\t\t\t\t\ttag->context);\n\t\t\t\t\t\ttag->alpha = anim::interpolateF(a1, a2, progress);\n\t\t\t\t\t\tpreview->update();\n\t\t\t\t\t}, 0., 1., st::universalDuration);\n\t\t\t\t}\n\t\t\t\tstate->colorIndex = now;\n\t\t\t});\n\t\t\tif (!session->premium()) {\n\t\t\t\tbutton->setClickedCallback([w = window] {\n\t\t\t\t\tShowPremiumPreviewToBuy(w, PremiumFeature::FilterTags);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tline->sizeValue() | rpl::on_next([=](const QSize &size) {\n\t\t\tconst auto totalWidth = buttons.size() * side;\n\t\t\tconst auto spacing = (size.width() - totalWidth)\n\t\t\t\t/ (buttons.size() - 1);\n\t\t\tfor (auto i = 0; i < kColorsCount; ++i) {\n\t\t\t\tconst auto &button = buttons[i];\n\t\t\t\tbutton->moveToLeft(i * (side + spacing), 0);\n\t\t\t}\n\t\t}, line->lifetime());\n\n\t\t{\n\t\t\tconst auto last = buttons.back();\n\t\t\tconst auto icon = Ui::CreateChild<Ui::RpWidget>(last);\n\t\t\ticon->resize(side, side);\n\t\t\ticon->paintRequest() | rpl::on_next([=] {\n\t\t\t\tauto p = QPainter(icon);\n\t\t\t\t(session->premium()\n\t\t\t\t\t? st::windowFilterSmallRemove.icon\n\t\t\t\t\t: st::historySendDisabledIcon).paintInCenter(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tQRectF(icon->rect()),\n\t\t\t\t\t\tst::historyPeerUserpicFg->c);\n\t\t\t}, icon->lifetime());\n\t\t\ticon->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\tlast->setBrush(st::historyPeerArchiveUserpicBg);\n\t\t}\n\n\t\tUi::AddSkip(colors);\n\t\tUi::AddSkip(colors);\n\t\tUi::AddDividerText(colors, tr::lng_filters_tag_color_about());\n\t\tUi::AddSkip(colors);\n\t}\n\n\tconst auto collect = [=]() -> std::optional<Data::ChatFilter> {\n\t\tauto title = state->title.current();\n\t\tconst auto staticTitle = !title.entities.isEmpty()\n\t\t\t&& state->staticTitle.current();\n\t\tconst auto rules = data->current();\n\t\tif (Ui::ComputeFieldCharacterCount(name) > kMaxFilterTitleLength\n\t\t\t|| title.empty()) {\n\t\t\tname->showError();\n\t\t\tbox->scrollToY(0);\n\t\t\treturn {};\n\t\t} else if (!(rules.flags() & kTypes) && rules.always().empty()) {\n\t\t\twindow->window().showToast(tr::lng_filters_empty(tr::now));\n\t\t\treturn {};\n\t\t} else if ((rules.flags() == (kTypes | Flag::NoArchived))\n\t\t\t&& rules.always().empty()\n\t\t\t&& rules.never().empty()) {\n\t\t\twindow->window().showToast(tr::lng_filters_default(tr::now));\n\t\t\treturn {};\n\t\t}\n\t\tconst auto rawColorIndex = state->colorIndex.current();\n\t\tconst auto colorIndex = (rawColorIndex >= kNoTag\n\t\t\t? std::nullopt\n\t\t\t: std::make_optional(rawColorIndex));\n\t\treturn rules.withTitle(\n\t\t\t{ std::move(title), staticTitle }\n\t\t).withColorIndex(colorIndex);\n\t};\n\n\tUi::AddSubsectionTitle(\n\t\tcontent,\n\t\trpl::conditional(\n\t\t\tstate->hasLinks.value(),\n\t\t\ttr::lng_filters_link_has(),\n\t\t\ttr::lng_filters_link()));\n\n\tstate->hasLinks.changes() | rpl::on_next([=] {\n\t\tcontent->resizeToWidth(content->widthNoMargins());\n\t}, content->lifetime());\n\n\tif (filter.chatlist()) {\n\t\twindow->session().data().chatsFilters().reloadChatlistLinks(\n\t\t\tfilter.id());\n\t}\n\n\tconst auto createLink = AddToggledButton(\n\t\tcontent,\n\t\tstate->hasLinks.value() | rpl::map(!rpl::mappers::_1),\n\t\ttr::lng_filters_link_create(),\n\t\tst::settingsButtonActive,\n\t\t{ &st::settingsFolderShareIcon, IconType::Simple });\n\tconst auto addLink = AddToggledButton(\n\t\tcontent,\n\t\tstate->hasLinks.value(),\n\t\ttr::lng_group_invite_add(),\n\t\tst::settingsButtonActive,\n\t\t{ &st::settingsIconAdd, IconType::Round, &st::windowBgActive });\n\n\tSetupFilterLinks(\n\t\tcontent,\n\t\twindow,\n\t\tstate->links.value(),\n\t\t[=] { return collect().value_or(Data::ChatFilter()); });\n\n\trpl::merge(\n\t\tcreateLink->clicks(),\n\t\taddLink->clicks()\n\t) | rpl::filter(\n\t\t(rpl::mappers::_1 == Qt::LeftButton)\n\t) | rpl::on_next([=](Qt::MouseButton button) {\n\t\tconst auto result = collect();\n\t\tif (!result || !GoodForExportFilterLink(window, *result)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto shared = CollectFilterLinkChats(*result);\n\t\tif (shared.empty()) {\n\t\t\twindow->show(ShowLinkBox(window, *result, {}));\n\t\t\treturn;\n\t\t}\n\t\tsaveAnd(*result, crl::guard(box, [=](Data::ChatFilter updated) {\n\t\t\tstate->creating = false;\n\n\t\t\t// Comparison of ChatFilter-s don't take id into account!\n\t\t\tdata->force_assign(updated);\n\t\t\tconst auto id = updated.id();\n\t\t\tstate->links = owner->chatsFilters().chatlistLinks(id);\n\t\t\tExportFilterLink(id, shared, crl::guard(box, [=](\n\t\t\t\t\tData::ChatFilterLink link) {\n\t\t\t\tExpects(link.id == id);\n\n\t\t\t\t*data = data->current().withChatlist(true, true);\n\t\t\t\twindow->show(ShowLinkBox(window, updated, link));\n\t\t\t}), crl::guard(box, [=](QString error) {\n\t\t\t\tconst auto session = &window->session();\n\t\t\t\tif (error == u\"CHATLISTS_TOO_MUCH\"_q) {\n\t\t\t\t\twindow->show(Box(ShareableFiltersLimitBox, session));\n\t\t\t\t} else if (error == u\"INVITES_TOO_MUCH\"_q) {\n\t\t\t\t\twindow->show(Box(FilterLinksLimitBox, session));\n\t\t\t\t} else if (error == u\"CHANNELS_TOO_MUCH\"_q) {\n\t\t\t\t\twindow->show(Box(ChannelsLimitBox, session));\n\t\t\t\t} else if (error == u\"USER_CHANNELS_TOO_MUCH\"_q) {\n\t\t\t\t\twindow->showToast(\n\t\t\t\t\t\t{ tr::lng_filters_link_group_admin_error(tr::now) });\n\t\t\t\t} else {\n\t\t\t\t\twindow->show(ShowLinkBox(window, updated, { .id = id }));\n\t\t\t\t}\n\t\t\t}));\n\t\t}));\n\t}, createLink->lifetime());\n\tUi::AddSkip(content);\n\tUi::AddDividerText(\n\t\tcontent,\n\t\trpl::conditional(\n\t\t\tstate->hasLinks.value(),\n\t\t\ttr::lng_filters_link_about_many(),\n\t\t\ttr::lng_filters_link_about()));\n\n\tconst auto show = box->uiShow();\n\tconst auto refreshPreviews = [=] {\n\t\tinclude->updateData(\n\t\t\tdata->current().flags() & kTypes,\n\t\t\tdata->current().always());\n\t\texclude->updateData(\n\t\t\tdata->current().flags() & kExcludeTypes,\n\t\t\tdata->current().never());\n\t};\n\tincludeAdd->setClickedCallback([=] {\n\t\tEditExceptions(\n\t\t\twindow,\n\t\t\tbox,\n\t\t\tkTypes | (state->chatlist.current() ? Flag::Chatlist : Flag()),\n\t\t\tdata,\n\t\t\tupdateDefaultTitle,\n\t\t\trefreshPreviews);\n\t});\n\texcludeAdd->setClickedCallback([=] {\n\t\tEditExceptions(\n\t\t\twindow,\n\t\t\tbox,\n\t\t\tkExcludeTypes,\n\t\t\tdata,\n\t\t\tupdateDefaultTitle,\n\t\t\trefreshPreviews);\n\t});\n\n\tconst auto save = [=] {\n\t\tif (const auto result = collect()) {\n\t\t\tbox->closeBox();\n\t\t\tdoneCallback(*result);\n\t\t}\n\t};\n\n\tbox->addButton(rpl::conditional(\n\t\tstate->creating.value(),\n\t\ttr::lng_filters_create_button(),\n\t\ttr::lng_settings_save()\n\t), save);\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\nvoid EditExistingFilter(\n\t\tnot_null<Window::SessionController*> window,\n\t\tFilterId id) {\n\tExpects(id != 0);\n\n\tconst auto session = &window->session();\n\tconst auto &list = session->data().chatsFilters().list();\n\tconst auto i = ranges::find(list, id, &Data::ChatFilter::id);\n\tif (i == end(list)) {\n\t\treturn;\n\t}\n\tconst auto doneCallback = [=](const Data::ChatFilter &result) {\n\t\tExpects(id == result.id());\n\n\t\tconst auto tl = result.tl();\n\t\tsession->data().chatsFilters().apply(MTP_updateDialogFilter(\n\t\t\tMTP_flags(MTPDupdateDialogFilter::Flag::f_filter),\n\t\t\tMTP_int(id),\n\t\t\ttl));\n\t\tsession->api().request(MTPmessages_UpdateDialogFilter(\n\t\t\tMTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter),\n\t\t\tMTP_int(id),\n\t\t\ttl\n\t\t)).send();\n\t};\n\tconst auto saveAnd = [=](\n\t\t\tconst Data::ChatFilter &data,\n\t\t\tFn<void(Data::ChatFilter)> next) {\n\t\tdoneCallback(data);\n\t\tnext(data);\n\t};\n\twindow->window().show(Box(\n\t\tEditFilterBox,\n\t\twindow,\n\t\t*i,\n\t\tcrl::guard(session, doneCallback),\n\t\tcrl::guard(session, saveAnd)));\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/filters/edit_filter_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nnamespace Data {\nclass ChatFilter;\n} // namespace Data\n\nvoid EditFilterBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> window,\n\tconst Data::ChatFilter &filter,\n\tFn<void(const Data::ChatFilter &)> doneCallback,\n\tFn<void(const Data::ChatFilter &, Fn<void(Data::ChatFilter)>)> saveAnd);\n\nvoid EditExistingFilter(\n\tnot_null<Window::SessionController*> window,\n\tFilterId id);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/filters/edit_filter_chats_list.h\"\n\n#include \"core/ui_integration.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"window/window_session_controller.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/painter.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"base/object_ptr.h\"\n#include \"data/data_user.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace {\n\nusing Flag = Data::ChatFilter::Flag;\nusing Flags = Data::ChatFilter::Flags;\n\nconstexpr auto kAllTypes = {\n\tFlag::NewChats,\n\tFlag::ExistingChats,\n\tFlag::Contacts,\n\tFlag::NonContacts,\n\tFlag::Groups,\n\tFlag::Channels,\n\tFlag::Bots,\n\tFlag::NoMuted,\n\tFlag::NoRead,\n\tFlag::NoArchived,\n};\n\nstruct RowSelectionChange {\n\tnot_null<PeerListRow*> row;\n\tbool checked = false;\n};\n\nclass TypeRow final : public PeerListRow {\npublic:\n\texplicit TypeRow(Flag flag);\n\n\tQString generateName() override;\n\tQString generateShortName() override;\n\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\nprivate:\n\t[[nodiscard]] Flag flag() const;\n\n};\n\nclass ExceptionRow final : public ChatsListBoxController::Row {\npublic:\n\tExceptionRow(\n\t\tnot_null<History*> history,\n\t\tnot_null<PeerListDelegate*> delegate);\n\n\tQString generateName() override;\n\tQString generateShortName() override;\n\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\n\tvoid paintStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) override;\n\nprivate:\n\tUi::Text::String _filtersText;\n\n};\n\nclass TypeController final : public PeerListController {\npublic:\n\tTypeController(\n\t\tnot_null<Main::Session*> session,\n\t\tFlags options,\n\t\tFlags selected);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\n\t[[nodiscard]] rpl::producer<Flags> selectedChanges() const;\n\t[[nodiscard]] auto rowSelectionChanges() const\n\t\t-> rpl::producer<RowSelectionChange>;\n\nprivate:\n\t[[nodiscard]] std::unique_ptr<PeerListRow> createRow(Flag flag) const;\n\t[[nodiscard]] Flags collectSelectedOptions() const;\n\n\tconst not_null<Main::Session*> _session;\n\tFlags _options;\n\n\trpl::event_stream<> _selectionChanged;\n\trpl::event_stream<RowSelectionChange> _rowSelectionChanges;\n\n};\n\n[[nodiscard]] uint64 TypeId(Flag flag) {\n\treturn PeerId(FakeChatId(static_cast<BareId>(flag))).value;\n}\n\nTypeRow::TypeRow(Flag flag) : PeerListRow(TypeId(flag)) {\n}\n\nQString TypeRow::generateName() {\n\treturn FilterChatsTypeName(flag());\n}\n\nQString TypeRow::generateShortName() {\n\treturn generateName();\n}\n\nPaintRoundImageCallback TypeRow::generatePaintUserpicCallback(\n\t\tbool forceRound) {\n\tconst auto flag = this->flag();\n\treturn [=](QPainter &p, int x, int y, int outerWidth, int size) {\n\t\tPaintFilterChatsTypeIcon(p, flag, x, y, outerWidth, size);\n\t};\n}\n\nFlag TypeRow::flag() const {\n\treturn static_cast<Flag>(id() & 0xFFFF);\n}\n\nExceptionRow::ExceptionRow(\n\tnot_null<History*> history,\n\tnot_null<PeerListDelegate*> delegate)\n: Row(history) {\n\tauto filters = TextWithEntities();\n\tfor (const auto &filter : history->owner().chatsFilters().list()) {\n\t\tif (filter.contains(history) && filter.id()) {\n\t\t\tif (!filters.empty()) {\n\t\t\t\tfilters.append(u\", \"_q);\n\t\t\t}\n\t\t\tauto title = filter.title();\n\t\t\tfilters.append(title.isStatic\n\t\t\t\t? Data::ForceCustomEmojiStatic(std::move(title.text))\n\t\t\t\t: std::move(title.text));\n\t\t}\n\t}\n\tif (!filters.empty()) {\n\t\tconst auto repaint = [=] { delegate->peerListUpdateRow(this); };\n\t\t_filtersText.setMarkedText(\n\t\t\tst::defaultTextStyle,\n\t\t\tfilters,\n\t\t\tkMarkupTextOptions,\n\t\t\tCore::TextContext({\n\t\t\t\t.session = &history->session(),\n\t\t\t\t.repaint = repaint,\n\t\t\t}));\n\t} else if (peer()->isSelf()) {\n\t\tsetCustomStatus(tr::lng_saved_forward_here(tr::now));\n\t}\n}\n\nQString ExceptionRow::generateName() {\n\tconst auto peer = this->peer();\n\treturn peer->isSelf()\n\t\t? tr::lng_saved_messages(tr::now)\n\t\t: peer->isRepliesChat()\n\t\t? tr::lng_replies_messages(tr::now)\n\t\t: peer->isVerifyCodes()\n\t\t? tr::lng_verification_codes(tr::now)\n\t\t: Row::generateName();\n}\n\nQString ExceptionRow::generateShortName() {\n\treturn generateName();\n}\n\nPaintRoundImageCallback ExceptionRow::generatePaintUserpicCallback(\n\t\tbool forceRound) {\n\tconst auto peer = this->peer();\n\tconst auto saved = peer->isSelf();\n\tconst auto replies = peer->isRepliesChat();\n\tauto userpic = saved ? Ui::PeerUserpicView() : ensureUserpicView();\n\tif (forceRound && peer->isForum()) {\n\t\treturn ForceRoundUserpicCallback(peer);\n\t}\n\treturn [=](Painter &p, int x, int y, int outerWidth, int size) mutable {\n\t\tusing namespace Ui;\n\t\tif (saved) {\n\t\t\tEmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);\n\t\t} else if (replies) {\n\t\t\tEmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);\n\t\t} else {\n\t\t\tpeer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);\n\t\t}\n\t};\n}\n\nvoid ExceptionRow::paintStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) {\n\tif (_filtersText.isEmpty()) {\n\t\tRow::paintStatusText(\n\t\t\tp,\n\t\t\tst,\n\t\t\tx,\n\t\t\ty,\n\t\t\tavailableWidth,\n\t\t\touterWidth,\n\t\t\tselected);\n\t} else {\n\t\tp.setPen(selected ? st.statusFgOver : st.statusFg);\n\t\t_filtersText.draw(p, {\n\t\t\t.position = { x, y },\n\t\t\t.outerWidth = outerWidth,\n\t\t\t.availableWidth = availableWidth,\n\t\t\t.palette = &st::defaultTextPalette,\n\t\t\t.now = crl::now(),\n\t\t\t.pausedEmoji = false,\n\t\t\t.elisionLines = 1,\n\t\t});\n\t}\n}\n\nTypeController::TypeController(\n\tnot_null<Main::Session*> session,\n\tFlags options,\n\tFlags selected)\n: _session(session)\n, _options(options) {\n}\n\nMain::Session &TypeController::session() const {\n\treturn *_session;\n}\n\nvoid TypeController::prepare() {\n\tfor (const auto flag : kAllTypes) {\n\t\tif (_options & flag) {\n\t\t\tdelegate()->peerListAppendRow(createRow(flag));\n\t\t}\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nFlags TypeController::collectSelectedOptions() const {\n\tauto result = Flags();\n\tfor (const auto flag : kAllTypes) {\n\t\tif (const auto row = delegate()->peerListFindRow(TypeId(flag))) {\n\t\t\tif (row->checked()) {\n\t\t\t\tresult |= flag;\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid TypeController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto checked = !row->checked();\n\tdelegate()->peerListSetRowChecked(row, checked);\n\t_rowSelectionChanges.fire({ row, checked });\n}\n\nstd::unique_ptr<PeerListRow> TypeController::createRow(Flag flag) const {\n\treturn std::make_unique<TypeRow>(flag);\n}\n\nrpl::producer<Flags> TypeController::selectedChanges() const {\n\treturn _rowSelectionChanges.events(\n\t) | rpl::map([=] {\n\t\treturn collectSelectedOptions();\n\t});\n}\n\nauto TypeController::rowSelectionChanges() const\n-> rpl::producer<RowSelectionChange> {\n\treturn _rowSelectionChanges.events();\n}\n\n} // namespace\n\n[[nodiscard]] QString FilterChatsTypeName(Flag flag) {\n\tswitch (flag) {\n\tcase Flag::NewChats: return tr::lng_filters_type_new(tr::now);\n\tcase Flag::ExistingChats: return tr::lng_filters_type_existing(tr::now);\n\tcase Flag::Contacts: return tr::lng_filters_type_contacts(tr::now);\n\tcase Flag::NonContacts:\n\t\treturn tr::lng_filters_type_non_contacts(tr::now);\n\tcase Flag::Groups: return tr::lng_filters_type_groups(tr::now);\n\tcase Flag::Channels: return tr::lng_filters_type_channels(tr::now);\n\tcase Flag::Bots: return tr::lng_filters_type_bots(tr::now);\n\tcase Flag::NoMuted: return tr::lng_filters_type_no_muted(tr::now);\n\tcase Flag::NoArchived: return tr::lng_filters_type_no_archived(tr::now);\n\tcase Flag::NoRead: return tr::lng_filters_type_no_read(tr::now);\n\t}\n\tUnexpected(\"Flag in TypeName.\");\n}\n\nvoid PaintFilterChatsTypeIcon(\n\t\tQPainter &p,\n\t\tData::ChatFilter::Flag flag,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size) {\n\tconst auto &color1 = [&]() -> const style::color& {\n\t\tswitch (flag) {\n\t\tcase Flag::NewChats: return st::historyPeer5UserpicBg;\n\t\tcase Flag::ExistingChats: return st::historyPeer8UserpicBg;\n\t\tcase Flag::Contacts: return st::historyPeer4UserpicBg;\n\t\tcase Flag::NonContacts: return st::historyPeer7UserpicBg;\n\t\tcase Flag::Groups: return st::historyPeer2UserpicBg;\n\t\tcase Flag::Channels: return st::historyPeer1UserpicBg;\n\t\tcase Flag::Bots: return st::historyPeer6UserpicBg;\n\t\tcase Flag::NoMuted: return st::historyPeer6UserpicBg;\n\t\tcase Flag::NoArchived: return st::historyPeer4UserpicBg;\n\t\tcase Flag::NoRead: return st::historyPeer7UserpicBg;\n\t\t}\n\t\tUnexpected(\"Flag in color paintFlagIcon.\");\n\t}();\n\tconst auto &color2 = [&]() -> const style::color& {\n\t\tswitch (flag) {\n\t\tcase Flag::NewChats: return st::historyPeer5UserpicBg2;\n\t\tcase Flag::ExistingChats: return st::historyPeer8UserpicBg2;\n\t\tcase Flag::Contacts: return st::historyPeer4UserpicBg2;\n\t\tcase Flag::NonContacts: return st::historyPeer7UserpicBg2;\n\t\tcase Flag::Groups: return st::historyPeer2UserpicBg2;\n\t\tcase Flag::Channels: return st::historyPeer1UserpicBg2;\n\t\tcase Flag::Bots: return st::historyPeer6UserpicBg2;\n\t\tcase Flag::NoMuted: return st::historyPeer6UserpicBg2;\n\t\tcase Flag::NoArchived: return st::historyPeer4UserpicBg2;\n\t\tcase Flag::NoRead: return st::historyPeer7UserpicBg2;\n\t\t}\n\t\tUnexpected(\"Flag in color paintFlagIcon.\");\n\t}();\n\tconst auto &icon = [&]() -> const style::icon& {\n\t\tswitch (flag) {\n\t\tcase Flag::NewChats: return st::windowFilterTypeNewChats;\n\t\tcase Flag::ExistingChats: return st::windowFilterTypeExistingChats;\n\t\tcase Flag::Contacts: return st::windowFilterTypeContacts;\n\t\tcase Flag::NonContacts: return st::windowFilterTypeNonContacts;\n\t\tcase Flag::Groups: return st::windowFilterTypeGroups;\n\t\tcase Flag::Channels: return st::windowFilterTypeChannels;\n\t\tcase Flag::Bots: return st::windowFilterTypeBots;\n\t\tcase Flag::NoMuted: return st::windowFilterTypeNoMuted;\n\t\tcase Flag::NoArchived: return st::windowFilterTypeNoArchived;\n\t\tcase Flag::NoRead: return st::windowFilterTypeNoRead;\n\t\t}\n\t\tUnexpected(\"Flag in icon paintFlagIcon.\");\n\t}();\n\tconst auto rect = style::rtlrect(x, y, size, size, outerWidth);\n\tauto hq = PainterHighQualityEnabler(p);\n\tauto bg = QLinearGradient(x, y, x, y + size);\n\tbg.setStops({ { 0., color1->c }, { 1., color2->c } });\n\tp.setBrush(bg);\n\tp.setPen(Qt::NoPen);\n\tp.drawEllipse(rect);\n\ticon.paintInCenter(p, rect);\n}\n\nobject_ptr<Ui::RpWidget> CreatePeerListSectionSubtitle(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<QString> text) {\n\tauto result = object_ptr<Ui::FixedHeightWidget>(\n\t\tparent,\n\t\tst::windowFilterChatsSectionSubtitleHeight);\n\n\tconst auto raw = result.data();\n\traw->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(raw);\n\t\tp.fillRect(clip, st::searchedBarBg);\n\t}, raw->lifetime());\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\tstd::move(text),\n\t\tst::windowFilterChatsSectionSubtitle);\n\traw->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto padding = st::windowFilterChatsSectionSubtitlePadding;\n\t\tconst auto available = width - padding.left() - padding.right();\n\t\tlabel->resizeToNaturalWidth(available);\n\t\tlabel->moveToLeft(padding.left(), padding.top(), width);\n\t}, label->lifetime());\n\n\treturn result;\n}\n\nEditFilterChatsListController::EditFilterChatsListController(\n\tnot_null<Main::Session*> session,\n\trpl::producer<QString> title,\n\tFlags options,\n\tFlags selected,\n\tconst base::flat_set<not_null<History*>> &peers,\n\tint limit,\n\tFn<void()> showLimitReached)\n: ChatsListBoxController(session)\n, _session(session)\n, _showLimitReached(std::move(showLimitReached))\n, _title(std::move(title))\n, _peers(peers)\n, _options(options & ~Flag::Chatlist)\n, _selected(selected)\n, _limit(limit)\n, _chatlist(options & Flag::Chatlist) {\n}\n\nMain::Session &EditFilterChatsListController::session() const {\n\treturn *_session;\n}\n\nint EditFilterChatsListController::selectedTypesCount() const {\n\tExpects(_chatlist || !_options || _typesDelegate != nullptr);\n\n\tif (_chatlist || !_options) {\n\t\treturn 0;\n\t}\n\tauto result = 0;\n\tfor (auto i = 0; i != _typesDelegate->peerListFullRowsCount(); ++i) {\n\t\tif (_typesDelegate->peerListRowAt(i)->checked()) {\n\t\t\t++result;\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid EditFilterChatsListController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto count = delegate()->peerListSelectedRowsCount()\n\t\t- selectedTypesCount();\n\tif (count < _limit || row->checked()) {\n\t\tdelegate()->peerListSetRowChecked(row, !row->checked());\n\t\tupdateTitle();\n\t} else if (const auto copy = _showLimitReached) {\n\t\tcopy();\n\t}\n}\n\nvoid EditFilterChatsListController::itemDeselectedHook(\n\t\tnot_null<PeerData*> peer) {\n\tupdateTitle();\n}\n\nbool EditFilterChatsListController::isForeignRow(PeerListRowId itemId) {\n\treturn ranges::contains(kAllTypes, itemId, TypeId);\n}\n\nbool EditFilterChatsListController::handleDeselectForeignRow(\n\t\tPeerListRowId itemId) {\n\tif (isForeignRow(itemId)) {\n\t\t_deselectOption(itemId);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid EditFilterChatsListController::prepareViewHook() {\n\tdelegate()->peerListSetTitle(std::move(_title));\n\tif (!_chatlist && _options) {\n\t\tdelegate()->peerListSetAboveWidget(prepareTypesList());\n\t}\n\n\tconst auto count = int(_peers.size());\n\tconst auto rows = std::make_unique<std::optional<ExceptionRow>[]>(count);\n\tauto i = 0;\n\tfor (const auto &history : _peers) {\n\t\trows[i++].emplace(history, delegate());\n\t}\n\tauto pointers = std::vector<ExceptionRow*>();\n\tpointers.reserve(count);\n\tfor (auto i = 0; i != count; ++i) {\n\t\tpointers.push_back(&*rows[i]);\n\t}\n\tdelegate()->peerListAddSelectedRows(pointers);\n\tupdateTitle();\n}\n\nobject_ptr<Ui::RpWidget> EditFilterChatsListController::prepareTypesList() {\n\tauto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tconst auto container = result.data();\n\tcontainer->add(CreatePeerListSectionSubtitle(\n\t\tcontainer,\n\t\ttr::lng_filters_edit_types()));\n\tcontainer->add(object_ptr<Ui::FixedHeightWidget>(\n\t\tcontainer,\n\t\tst::membersMarginTop));\n\t_typesDelegate = container->lifetime().make_state<\n\t\tPeerListContentDelegateSimple\n\t>();\n\tconst auto controller = container->lifetime().make_state<TypeController>(\n\t\t&session(),\n\t\t_options,\n\t\t_selected);\n\tcontroller->setStyleOverrides(&st::windowFilterSmallList);\n\tconst auto content = result->add(object_ptr<PeerListContent>(\n\t\tcontainer,\n\t\tcontroller));\n\t_typesDelegate->setContent(content);\n\tcontroller->setDelegate(_typesDelegate);\n\tfor (const auto flag : kAllTypes) {\n\t\tif (_selected & flag) {\n\t\t\tif (const auto row = _typesDelegate->peerListFindRow(TypeId(flag))) {\n\t\t\t\tcontent->changeCheckState(row, true, anim::type::instant);\n\t\t\t\tthis->delegate()->peerListSetForeignRowChecked(\n\t\t\t\t\trow,\n\t\t\t\t\ttrue,\n\t\t\t\t\tanim::type::instant);\n\t\t\t}\n\t\t}\n\t}\n\tcontainer->add(object_ptr<Ui::FixedHeightWidget>(\n\t\tcontainer,\n\t\tst::membersMarginBottom));\n\tcontainer->add(CreatePeerListSectionSubtitle(\n\t\tcontainer,\n\t\ttr::lng_filters_edit_chats()));\n\n\tcontroller->selectedChanges(\n\t) | rpl::on_next([=](Flags selected) {\n\t\t_selected = selected;\n\t}, _lifetime);\n\n\tcontroller->rowSelectionChanges(\n\t) | rpl::on_next([=](RowSelectionChange update) {\n\t\tthis->delegate()->peerListSetForeignRowChecked(\n\t\t\tupdate.row,\n\t\t\tupdate.checked,\n\t\t\tanim::type::normal);\n\t}, _lifetime);\n\n\t_deselectOption = [=](PeerListRowId itemId) {\n\t\tif (const auto row = _typesDelegate->peerListFindRow(itemId)) {\n\t\t\t_typesDelegate->peerListSetRowChecked(row, false);\n\t\t}\n\t};\n\n\treturn result;\n}\n\nauto EditFilterChatsListController::createRow(not_null<History*> history)\n-> std::unique_ptr<Row> {\n\tconst auto business = (_options & (Flag::NewChats | Flag::ExistingChats))\n\t\t|| (!_options && !_chatlist);\n\tif (business && (history->peer->isSelf() || !history->peer->isUser())) {\n\t\treturn nullptr;\n\t}\n\treturn history->inChatList()\n\t\t? std::make_unique<ExceptionRow>(history, delegate())\n\t\t: nullptr;\n}\n\nvoid EditFilterChatsListController::updateTitle() {\n\tconst auto count = delegate()->peerListSelectedRowsCount()\n\t\t- selectedTypesCount();\n\tconst auto additional = u\"%1 / %2\"_q.arg(count).arg(_limit);\n\tdelegate()->peerListSetAdditionalTitle(rpl::single(additional));\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peer_list_controllers.h\"\n#include \"data/data_chat_filters.h\"\n\nclass History;\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nclass Painter;\n\n[[nodiscard]] QString FilterChatsTypeName(Data::ChatFilter::Flag flag);\nvoid PaintFilterChatsTypeIcon(\n\tQPainter &p,\n\tData::ChatFilter::Flag flag,\n\tint x,\n\tint y,\n\tint outerWidth,\n\tint size);\n\n[[nodiscard]] object_ptr<Ui::RpWidget> CreatePeerListSectionSubtitle(\n\tnot_null<QWidget*> parent,\n\trpl::producer<QString> text);\n\nclass EditFilterChatsListController final : public ChatsListBoxController {\npublic:\n\tusing Flag = Data::ChatFilter::Flag;\n\tusing Flags = Data::ChatFilter::Flags;\n\n\tEditFilterChatsListController(\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<QString> title,\n\t\tFlags options,\n\t\tFlags selected,\n\t\tconst base::flat_set<not_null<History*>> &peers,\n\t\tint limit,\n\t\tFn<void()> showLimitReached);\n\n\t[[nodiscard]] Main::Session &session() const override;\n\t[[nodiscard]] Flags chosenOptions() const {\n\t\treturn _selected;\n\t}\n\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid itemDeselectedHook(not_null<PeerData*> peer) override;\n\tbool isForeignRow(PeerListRowId itemId) override;\n\tbool handleDeselectForeignRow(PeerListRowId itemId) override;\n\nprivate:\n\tint selectedTypesCount() const;\n\tvoid prepareViewHook() override;\n\tstd::unique_ptr<Row> createRow(not_null<History*> history) override;\n\t[[nodiscard]] object_ptr<Ui::RpWidget> prepareTypesList();\n\n\tvoid updateTitle();\n\n\tconst not_null<Main::Session*> _session;\n\tconst Fn<void()> _showLimitReached;\n\trpl::producer<QString> _title;\n\tbase::flat_set<not_null<History*>> _peers;\n\tFlags _options;\n\tFlags _selected;\n\tint _limit = 0;\n\tbool _chatlist = false;\n\n\tFn<void(PeerListRowId)> _deselectOption;\n\n\tPeerListContentDelegate *_typesDelegate = nullptr;\n\n\trpl::lifetime _lifetime;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/filters/edit_filter_chats_preview.h\"\n\n#include \"boxes/filters/edit_filter_chats_list.h\"\n#include \"data/data_peer.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_window.h\"\n\nnamespace {\n\nusing Flag = Data::ChatFilter::Flag;\n\nconstexpr auto kAllTypes = {\n\tFlag::NewChats,\n\tFlag::ExistingChats,\n\tFlag::Contacts,\n\tFlag::NonContacts,\n\tFlag::Groups,\n\tFlag::Channels,\n\tFlag::Bots,\n\tFlag::NoMuted,\n\tFlag::NoRead,\n\tFlag::NoArchived,\n};\n\n} // namespace\n\nFilterChatsPreview::FilterChatsPreview(\n\tnot_null<QWidget*> parent,\n\tFlags flags,\n\tconst base::flat_set<not_null<History*>> &peers)\n: RpWidget(parent) {\n\tupdateData(flags, peers);\n}\n\nvoid FilterChatsPreview::refresh() {\n\tresizeToWidth(width());\n}\n\nvoid FilterChatsPreview::updateData(\n\t\tFlags flags,\n\t\tconst base::flat_set<not_null<History*>> &peers) {\n\t_removeFlag.clear();\n\t_removePeer.clear();\n\tconst auto makeButton = [&](Fn<void()> handler) {\n\t\tauto result = base::make_unique_q<Ui::IconButton>(\n\t\t\tthis,\n\t\t\tst::windowFilterSmallRemove);\n\t\tresult->setClickedCallback(std::move(handler));\n\t\tresult->show();\n\t\treturn result;\n\t};\n\tfor (const auto flag : kAllTypes) {\n\t\tif (flags & flag) {\n\t\t\t_removeFlag.push_back({\n\t\t\t\tflag,\n\t\t\t\tmakeButton([=] { removeFlag(flag); }) });\n\t\t}\n\t}\n\tfor (const auto &history : peers) {\n\t\t_removePeer.push_back(PeerButton{\n\t\t\t.history = history,\n\t\t\t.button = makeButton([=] { removePeer(history); })\n\t\t});\n\t}\n\trefresh();\n}\n\nint FilterChatsPreview::resizeGetHeight(int newWidth) {\n\tconst auto right = st::windowFilterSmallRemoveRight;\n\tconst auto add = (st::windowFilterSmallItem.height\n\t\t- st::windowFilterSmallRemove.height) / 2;\n\tauto top = 0;\n\tconst auto moveNextButton = [&](not_null<Ui::IconButton*> button) {\n\t\tbutton->moveToRight(right, top + add, newWidth);\n\t\ttop += st::windowFilterSmallItem.height;\n\t};\n\tfor (const auto &[flag, button] : _removeFlag) {\n\t\tmoveNextButton(button.get());\n\t}\n\tfor (const auto &[history, userpic, name, button] : _removePeer) {\n\t\tmoveNextButton(button.get());\n\t}\n\treturn top;\n}\n\nvoid FilterChatsPreview::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\tauto top = 0;\n\tconst auto &st = st::windowFilterSmallItem;\n\tconst auto iconLeft = st.photoPosition.x();\n\tconst auto iconTop = st.photoPosition.y();\n\tconst auto nameLeft = st.namePosition.x();\n\tp.setFont(st::windowFilterSmallItem.nameStyle.font);\n\tconst auto nameTop = st.namePosition.y();\n\tfor (const auto &[flag, button] : _removeFlag) {\n\t\tPaintFilterChatsTypeIcon(\n\t\t\tp,\n\t\t\tflag,\n\t\t\ticonLeft,\n\t\t\ttop + iconTop,\n\t\t\twidth(),\n\t\t\tst.photoSize);\n\n\t\tp.setPen(st::contactsNameFg);\n\t\tp.drawTextLeft(\n\t\t\tnameLeft,\n\t\t\ttop + nameTop,\n\t\t\twidth(),\n\t\t\tFilterChatsTypeName(flag));\n\t\ttop += st.height;\n\t}\n\tfor (auto &[history, userpic, name, button] : _removePeer) {\n\t\tconst auto peer = history->peer;\n\t\tconst auto savedMessages = peer->isSelf();\n\t\tconst auto repliesMessages = peer->isRepliesChat();\n\t\tconst auto verifyCodes = peer->isVerifyCodes();\n\t\tif (savedMessages || repliesMessages || verifyCodes) {\n\t\t\tif (savedMessages) {\n\t\t\t\tUi::EmptyUserpic::PaintSavedMessages(\n\t\t\t\t\tp,\n\t\t\t\t\ticonLeft,\n\t\t\t\t\ttop + iconTop,\n\t\t\t\t\twidth(),\n\t\t\t\t\tst.photoSize);\n\t\t\t} else if (repliesMessages) {\n\t\t\t\tUi::EmptyUserpic::PaintRepliesMessages(\n\t\t\t\t\tp,\n\t\t\t\t\ticonLeft,\n\t\t\t\t\ttop + iconTop,\n\t\t\t\t\twidth(),\n\t\t\t\t\tst.photoSize);\n\t\t\t} else {\n\t\t\t\thistory->peer->paintUserpicLeft(\n\t\t\t\t\tp,\n\t\t\t\t\tuserpic,\n\t\t\t\t\ticonLeft,\n\t\t\t\t\ttop + iconTop,\n\t\t\t\t\twidth(),\n\t\t\t\t\tst.photoSize);\n\t\t\t}\n\t\t\tp.setPen(st::contactsNameFg);\n\t\t\tp.drawTextLeft(\n\t\t\t\tnameLeft,\n\t\t\t\ttop + nameTop,\n\t\t\t\twidth(),\n\t\t\t\t(savedMessages\n\t\t\t\t\t? tr::lng_saved_messages(tr::now)\n\t\t\t\t\t: repliesMessages\n\t\t\t\t\t? tr::lng_replies_messages(tr::now)\n\t\t\t\t\t: tr::lng_verification_codes(tr::now)));\n\t\t} else {\n\t\t\thistory->peer->paintUserpicLeft(\n\t\t\t\tp,\n\t\t\t\tuserpic,\n\t\t\t\ticonLeft,\n\t\t\t\ttop + iconTop,\n\t\t\t\twidth(),\n\t\t\t\tst.photoSize);\n\t\t\tp.setPen(st::contactsNameFg);\n\t\t\tif (name.isEmpty()) {\n\t\t\t\tname.setText(\n\t\t\t\t\tst::msgNameStyle,\n\t\t\t\t\thistory->peer->name(),\n\t\t\t\t\tUi::NameTextOptions());\n\t\t\t}\n\t\t\tname.drawLeftElided(\n\t\t\t\tp,\n\t\t\t\tnameLeft,\n\t\t\t\ttop + nameTop,\n\t\t\t\tbutton->x() - nameLeft,\n\t\t\t\twidth());\n\t\t}\n\t\ttop += st.height;\n\t}\n}\n\nvoid FilterChatsPreview::removeFlag(Flag flag) {\n\tconst auto i = ranges::find(_removeFlag, flag, &FlagButton::flag);\n\tAssert(i != end(_removeFlag));\n\t_removeFlag.erase(i);\n\trefresh();\n\t_flagRemoved.fire_copy(flag);\n}\n\nvoid FilterChatsPreview::removePeer(not_null<History*> history) {\n\tconst auto i = ranges::find(_removePeer, history, &PeerButton::history);\n\tAssert(i != end(_removePeer));\n\t_removePeer.erase(i);\n\trefresh();\n\t_peerRemoved.fire_copy(history);\n}\n\nrpl::producer<Flag> FilterChatsPreview::flagRemoved() const {\n\treturn _flagRemoved.events();\n}\n\nrpl::producer<not_null<History*>> FilterChatsPreview::peerRemoved() const {\n\treturn _peerRemoved.events();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_chat_filters.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/userpic_view.h\"\n\nclass History;\n\nnamespace Ui {\nclass IconButton;\n} // namespace Ui\n\nclass FilterChatsPreview final : public Ui::RpWidget {\npublic:\n\tusing Flag = Data::ChatFilter::Flag;\n\tusing Flags = Data::ChatFilter::Flags;\n\n\tFilterChatsPreview(\n\t\tnot_null<QWidget*> parent,\n\t\tFlags flags,\n\t\tconst base::flat_set<not_null<History*>> &peers);\n\n\t[[nodiscard]] rpl::producer<Flag> flagRemoved() const;\n\t[[nodiscard]] rpl::producer<not_null<History*>> peerRemoved() const;\n\n\tvoid updateData(\n\t\tFlags flags,\n\t\tconst base::flat_set<not_null<History*>> &peers);\n\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tusing Button = base::unique_qptr<Ui::IconButton>;\n\tstruct FlagButton {\n\t\tFlag flag = Flag();\n\t\tButton button;\n\t};\n\tstruct PeerButton {\n\t\tnot_null<History*> history;\n\t\tUi::PeerUserpicView userpic;\n\t\tUi::Text::String name;\n\t\tButton button;\n\t};\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid refresh();\n\tvoid removeFlag(Flag flag);\n\tvoid removePeer(not_null<History*> history);\n\n\tstd::vector<FlagButton> _removeFlag;\n\tstd::vector<PeerButton> _removePeer;\n\n\trpl::event_stream<Flag> _flagRemoved;\n\trpl::event_stream<not_null<History*>> _peerRemoved;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/filters/edit_filter_links.h\"\n\n#include \"apiwrap.h\"\n#include \"boxes/peers/edit_peer_invite_link.h\" // InviteLinkQrBox.\n#include \"boxes/peer_list_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/invite_link_buttons.h\"\n#include \"ui/controls/invite_link_label.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\nnamespace {\n\nconstexpr auto kMaxLinkTitleLength = 32;\n\nusing InviteLinkData = Data::ChatFilterLink;\nclass LinkRow;\n\nenum class Color {\n\tPermanent,\n\n\tCount,\n};\n\nstruct InviteLinkAction {\n\tenum class Type {\n\t\tCopy,\n\t\tShare,\n\t\tEdit,\n\t\tDelete,\n\t};\n\tQString link;\n\tType type = Type::Copy;\n};\n\nstruct Errors {\n\tQString status;\n\tQString toast;\n};\n\n[[nodiscard]] std::optional<Errors> ErrorForSharing(\n\t\tnot_null<History*> history) {\n\tconst auto result = [](const QString &status, const QString &toast) {\n\t\treturn Errors{ status, toast };\n\t};\n\tconst auto peer = history->peer;\n\tif (const auto user = peer->asUser()) {\n\t\treturn user->isBot()\n\t\t\t? result(\n\t\t\t\ttr::lng_filters_link_bot_status(tr::now),\n\t\t\t\ttr::lng_filters_link_bot_error(tr::now))\n\t\t\t: result(\n\t\t\t\ttr::lng_filters_link_private_status(tr::now),\n\t\t\t\ttr::lng_filters_link_private_error(tr::now));\n\t} else if (const auto chat = history->peer->asChat()) {\n\t\tif (!chat->canHaveInviteLink()) {\n\t\t\treturn result(\n\t\t\t\ttr::lng_filters_link_noadmin_status(tr::now),\n\t\t\t\ttr::lng_filters_link_noadmin_group_error(tr::now));\n\t\t}\n\t\treturn std::nullopt;\n\t} else if (const auto channel = history->peer->asChannel()) {\n\t\tif (!channel->canHaveInviteLink()\n\t\t\t&& (!channel->hasUsername() || channel->requestToJoin())) {\n\t\t\treturn result(\n\t\t\t\ttr::lng_filters_link_noadmin_status(tr::now),\n\t\t\t\t(channel->isMegagroup()\n\t\t\t\t\t? tr::lng_filters_link_noadmin_group_error(tr::now)\n\t\t\t\t\t: tr::lng_filters_link_noadmin_channel_error(tr::now)));\n\t\t}\n\t\treturn std::nullopt;\n\t}\n\tUnexpected(\"Peer type in ErrorForSharing.\");\n}\n\nvoid ShowSaveError(\n\t\tnot_null<Window::SessionController*> window,\n\t\tQString error) {\n\tconst auto session = &window->session();\n\tif (error == u\"CHATLISTS_TOO_MUCH\"_q) {\n\t\twindow->show(Box(ShareableFiltersLimitBox, session));\n\t} else if (error == u\"INVITES_TOO_MUCH\"_q) {\n\t\twindow->show(Box(FilterLinksLimitBox, session));\n\t} else if (error == u\"CHANNELS_TOO_MUCH\"_q) {\n\t\twindow->show(Box(ChannelsLimitBox, session));\n\t} else if (error == u\"USER_CHANNELS_TOO_MUCH\"_q) {\n\t\twindow->showToast(\n\t\t\t{ tr::lng_filters_link_group_admin_error(tr::now) });\n\t} else {\n\t\twindow->showToast(error);\n\t}\n}\n\nvoid ShowEmptyLinkError(not_null<Window::SessionController*> window) {\n\tShowSaveError(window, tr::lng_filters_empty(tr::now));\n}\n\nvoid ChatFilterLinkBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tData::ChatFilterLink data) {\n\tusing namespace rpl::mappers;\n\n\tconst auto link = data.url;\n\tbox->setTitle(tr::lng_group_invite_edit_title());\n\n\tconst auto container = box->verticalLayout();\n\tconst auto labelField = container->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tcontainer,\n\t\t\tst::defaultInputField,\n\t\t\ttr::lng_group_invite_label_header(),\n\t\t\tdata.title),\n\t\tstyle::margins(\n\t\t\tst::defaultSubsectionTitlePadding.left(),\n\t\t\tst::defaultVerticalListSkip,\n\t\t\tst::defaultSubsectionTitlePadding.right(),\n\t\t\tst::defaultVerticalListSkip * 2));\n\tlabelField->setMaxLength(kMaxLinkTitleLength);\n\tAddDivider(container);\n\n\tbox->setFocusCallback([=] {\n\t\tlabelField->setFocusFast();\n\t});\n\n\tconst auto &saveLabel = link.isEmpty()\n\t\t? tr::lng_formatting_link_create\n\t\t: tr::lng_settings_save;\n\tbox->addButton(saveLabel(), [=] {\n\t\tsession->data().chatsFilters().edit(\n\t\t\tdata.id,\n\t\t\tdata.url,\n\t\t\tlabelField->getLastText().trimmed());\n\t\tbox->closeBox();\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\nclass LinkRowDelegate {\npublic:\n\tvirtual void rowUpdateRow(not_null<LinkRow*> row) = 0;\n\tvirtual void rowPaintIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tColor color) = 0;\n};\n\nclass LinkRow final : public PeerListRow {\npublic:\n\tLinkRow(not_null<LinkRowDelegate*> delegate, const InviteLinkData &data);\n\n\tvoid update(const InviteLinkData &data);\n\n\t[[nodiscard]] InviteLinkData data() const;\n\n\tQString generateName() override;\n\tQString generateShortName() override;\n\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\n\tQSize rightActionSize() const override;\n\tQMargins rightActionMargins() const override;\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\nprivate:\n\tconst not_null<LinkRowDelegate*> _delegate;\n\tInviteLinkData _data;\n\tQString _status;\n\tColor _color = Color::Permanent;\n\n};\n\nclass ChatRow final : public PeerListRow {\npublic:\n\tChatRow(not_null<PeerData*> peer, const QString &status, bool disabled);\n\n\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\nprivate:\n\tconst bool _disabled = false;\n\tQImage _disabledFrame;\n\tInMemoryKey _userpicKey;\n\tint _paletteVersion = 0;\n\n};\n\n[[nodiscard]] Color ComputeColor(const InviteLinkData &link) {\n\treturn Color::Permanent;\n}\n\n[[nodiscard]] QString ComputeStatus(const InviteLinkData &link) {\n\treturn tr::lng_filters_chats_count(tr::now, lt_count, link.chats.size());\n}\n\nLinkRow::LinkRow(\n\tnot_null<LinkRowDelegate*> delegate,\n\tconst InviteLinkData &data)\n: PeerListRow(UniqueRowIdFromString(data.url))\n, _delegate(delegate)\n, _data(data)\n, _color(ComputeColor(data)) {\n\tsetCustomStatus(ComputeStatus(data));\n}\n\nvoid LinkRow::update(const InviteLinkData &data) {\n\t_data = data;\n\t_color = ComputeColor(data);\n\tsetCustomStatus(ComputeStatus(data));\n\trefreshName(st::inviteLinkList.item);\n\t_delegate->rowUpdateRow(this);\n}\n\nInviteLinkData LinkRow::data() const {\n\treturn _data;\n}\n\nQString LinkRow::generateName() {\n\tif (!_data.title.isEmpty()) {\n\t\treturn _data.title;\n\t}\n\tauto result = _data.url;\n\treturn result.replace(\n\t\tu\"https://\"_q,\n\t\tQString()\n\t).replace(\n\t\tu\"t.me/+\"_q,\n\t\tQString()\n\t).replace(\n\t\tu\"t.me/joinchat/\"_q,\n\t\tQString()\n\t);\n}\n\nQString LinkRow::generateShortName() {\n\treturn generateName();\n}\n\nPaintRoundImageCallback LinkRow::generatePaintUserpicCallback(\n\t\tbool forceRound) {\n\treturn [=](\n\t\t\tQPainter &p,\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tint outerWidth,\n\t\t\tint size) {\n\t\t_delegate->rowPaintIcon(p, x, y, size, _color);\n\t};\n}\n\nQSize LinkRow::rightActionSize() const {\n\treturn QSize(\n\t\tst::inviteLinkThreeDotsIcon.width(),\n\t\tst::inviteLinkThreeDotsIcon.height());\n}\n\nQMargins LinkRow::rightActionMargins() const {\n\treturn QMargins(\n\t\t0,\n\t\t(st::inviteLinkList.item.height - rightActionSize().height()) / 2,\n\t\tst::inviteLinkThreeDotsSkip,\n\t\t0);\n}\n\nvoid LinkRow::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\t(actionSelected\n\t\t? st::inviteLinkThreeDotsIconOver\n\t\t: st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth);\n}\n\nChatRow::ChatRow(\n\tnot_null<PeerData*> peer,\n\tconst QString &status,\n\tbool disabled)\n: PeerListRow(peer)\n, _disabled(disabled) {\n\tif (!status.isEmpty()) {\n\t\tsetCustomStatus(status);\n\t}\n}\n\nPaintRoundImageCallback ChatRow::generatePaintUserpicCallback(\n\t\tbool forceRound) {\n\tconst auto peer = this->peer();\n\tconst auto saved = peer->isSelf();\n\tconst auto replies = peer->isRepliesChat();\n\tauto userpic = (saved || replies)\n\t\t? Ui::PeerUserpicView()\n\t\t: ensureUserpicView();\n\tauto paint = [=](\n\t\t\tPainter &p,\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tint outerWidth,\n\t\t\tint size) mutable {\n\t\tusing namespace Ui;\n\t\tif (forceRound && peer->isForum()) {\n\t\t\tForceRoundUserpicCallback(peer)(p, x, y, outerWidth, size);\n\t\t} else if (saved) {\n\t\t\tEmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);\n\t\t} else if (replies) {\n\t\t\tEmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);\n\t\t} else {\n\t\t\tpeer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);\n\t\t}\n\t};\n\tif (!_disabled) {\n\t\treturn paint;\n\t}\n\treturn [=](\n\t\t\tPainter &p,\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tint outerWidth,\n\t\t\tint size) mutable {\n\t\tconst auto wide = size + style::ConvertScale(3);\n\t\tconst auto full = QSize(wide, wide) * style::DevicePixelRatio();\n\t\tauto repaint = false;\n\t\tif (_disabledFrame.size() != full) {\n\t\t\trepaint = true;\n\t\t\t_disabledFrame = QImage(\n\t\t\t\tfull,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_disabledFrame.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t} else {\n\t\t\trepaint = (_paletteVersion != style::PaletteVersion())\n\t\t\t\t|| (!saved\n\t\t\t\t\t&& !replies\n\t\t\t\t\t&& (_userpicKey != peer->userpicUniqueKey(userpic)));\n\t\t}\n\t\tif (repaint) {\n\t\t\t_paletteVersion = style::PaletteVersion();\n\t\t\t_userpicKey = peer->userpicUniqueKey(userpic);\n\n\t\t\t_disabledFrame.fill(Qt::transparent);\n\t\t\tauto p = Painter(&_disabledFrame);\n\t\t\tpaint(p, 0, 0, wide, size);\n\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setBrush(st::boxBg);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tconst auto two = style::ConvertScaleExact(2.5);\n\t\t\tconst auto half = size / 2.;\n\t\t\tconst auto rect = QRectF(half, half, half, half).translated(\n\t\t\t\t{ two, two });\n\t\t\tp.drawEllipse(rect);\n\n\t\t\tauto pen = st::windowSubTextFg->p;\n\t\t\tconst auto width = style::ConvertScaleExact(1.5);\n\t\t\tconst auto dash = 0.55;\n\t\t\tconst auto dashWithCaps = dash + 1.;\n\t\t\tpen.setWidthF(width);\n\t\t\t// 11 parts = M_PI * half / ((dashWithCaps + space) * width)\n\t\t\t// 11 = M_PI * half / ((dashWithCaps + space) * width)\n\t\t\t// space = (M_PI * half / (11 * width)) - dashWithCaps\n\t\t\tconst auto space = M_PI * half / (11 * width) - dashWithCaps;\n\t\t\tpen.setDashPattern(QVector<qreal>{ dash, space });\n\t\t\tpen.setDashOffset(1.);\n\t\t\tpen.setCapStyle(Qt::RoundCap);\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\tp.setPen(pen);\n\t\t\tp.drawEllipse(rect.marginsRemoved({ two, two, two, two }));\n\t\t}\n\t\tp.drawImage(x, y, _disabledFrame);\n\t};\n}\n\nclass LinksController final\n\t: public PeerListController\n\t, public LinkRowDelegate\n\t, public base::has_weak_ptr {\npublic:\n\tLinksController(\n\t\tnot_null<Window::SessionController*> window,\n\t\trpl::producer<std::vector<InviteLinkData>> content,\n\t\tFn<Data::ChatFilter()> currentFilter);\n\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override;\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\tMain::Session &session() const override;\n\n\tvoid rowUpdateRow(not_null<LinkRow*> row) override;\n\tvoid rowPaintIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tColor color) override;\n\nprivate:\n\tvoid appendRow(const InviteLinkData &data);\n\n\tvoid rebuild(const std::vector<InviteLinkData> &rows);\n\n\t[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row);\n\n\tconst not_null<Window::SessionController*> _window;\n\tFn<Data::ChatFilter()> _currentFilter;\n\trpl::variable<std::vector<InviteLinkData>> _rows;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tstd::array<QImage, int(Color::Count)> _icons;\n\trpl::lifetime _lifetime;\n\n};\n\nclass LinkController final\n\t: public PeerListController\n\t, public base::has_weak_ptr {\npublic:\n\tLinkController(\n\t\tnot_null<Window::SessionController*> window,\n\t\tconst Data::ChatFilter &filter,\n\t\tInviteLinkData data);\n\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tMain::Session &session() const override;\n\n\tvoid showFinished() override;\n\n\t[[nodiscard]] rpl::producer<bool> hasChangesValue() const;\n\t[[nodiscard]] base::flat_set<not_null<PeerData*>> selected() const;\n\nprivate:\n\tvoid setupAboveWidget();\n\tvoid setupBelowWidget();\n\tvoid addHeader(not_null<Ui::VerticalLayout*> container);\n\tvoid addLinkBlock(not_null<Ui::VerticalLayout*> container);\n\tvoid toggleAllSelected(bool select);\n\n\tconst not_null<Window::SessionController*> _window;\n\tInviteLinkData _data;\n\n\tData::ChatFilterTitle _filterTitle;\n\tbase::flat_set<not_null<History*>> _filterChats;\n\tbase::flat_map<not_null<PeerData*>, QString> _denied;\n\trpl::variable<base::flat_set<not_null<PeerData*>>> _selected;\n\tbase::flat_set<not_null<PeerData*>> _initial;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tQString _link;\n\n\trpl::variable<bool> _hasChanges = false;\n\n\trpl::event_stream<> _showFinished;\n\n\trpl::lifetime _lifetime;\n\n};\n\nLinkController::LinkController(\n\tnot_null<Window::SessionController*> window,\n\tconst Data::ChatFilter &filter,\n\tInviteLinkData data)\n: _window(window)\n, _filterTitle(filter.title())\n, _filterChats(filter.always()) {\n\t_data = std::move(data);\n\t_link = _data.url;\n}\n\nvoid LinkController::addHeader(not_null<Ui::VerticalLayout*> container) {\n\tusing namespace Settings;\n\n\tconst auto divider = Ui::CreateChild<Ui::BoxContentDivider>(\n\t\tcontainer.get());\n\tconst auto verticalLayout = container->add(\n\t\tobject_ptr<Ui::VerticalLayout>(container.get()));\n\n\tauto icon = CreateLottieIcon(\n\t\tverticalLayout,\n\t\t{\n\t\t\t.name = u\"cloud_filters\"_q,\n\t\t\t.sizeOverride = {\n\t\t\t\tst::settingsFilterIconSize,\n\t\t\t\tst::settingsFilterIconSize,\n\t\t\t},\n\t\t},\n\t\tst::settingsFilterIconPadding);\n\t_showFinished.events(\n\t) | rpl::on_next([animate = std::move(icon.animate)] {\n\t\tanimate(anim::repeat::once);\n\t}, verticalLayout->lifetime());\n\tverticalLayout->add(std::move(icon.widget));\n\n\tconst auto isStatic = _filterTitle.isStatic;\n\tverticalLayout->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tverticalLayout,\n\t\t\t(_data.url.isEmpty()\n\t\t\t\t? tr::lng_filters_link_no_about(tr::marked)\n\t\t\t\t: tr::lng_filters_link_share_about(\n\t\t\t\t\tlt_folder,\n\t\t\t\t\trpl::single(Ui::Text::Wrapped(\n\t\t\t\t\t\t_filterTitle.text,\n\t\t\t\t\t\tEntityType::Bold)),\n\t\t\t\t\ttr::marked)),\n\t\t\tst::settingsFilterDividerLabel,\n\t\t\tst::defaultPopupMenu,\n\t\t\tCore::TextContext({\n\t\t\t\t.session = &_window->session(),\n\t\t\t\t.customEmojiLoopLimit = isStatic ? -1 : 0,\n\t\t\t})),\n\t\tst::filterLinkDividerLabelPadding,\n\t\tstyle::al_top)->setTryMakeSimilarLines(true);\n\n\tverticalLayout->geometryValue(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tdivider->setGeometry(r);\n\t}, divider->lifetime());\n}\n\nobject_ptr<Ui::BoxContent> DeleteLinkBox(\n\t\tnot_null<Window::SessionController*> window,\n\t\tconst InviteLinkData &link) {\n\tconst auto sure = [=](Fn<void()> &&close) {\n\t\twindow->session().data().chatsFilters().destroy(link.id, link.url);\n\t\tclose();\n\t};\n\treturn Ui::MakeConfirmBox({\n\t\t.text = tr::lng_filters_link_delete_sure(tr::now),\n\t\t.confirmed = sure,\n\t\t.confirmText = tr::lng_box_delete(tr::now),\n\t});\n}\n\nvoid LinkController::addLinkBlock(not_null<Ui::VerticalLayout*> container) {\n\tusing namespace Settings;\n\n\tconst auto link = _data.url;\n\tconst auto weak = base::make_weak(container);\n\tconst auto copyLink = crl::guard(weak, [=] {\n\t\tCopyInviteLink(delegate()->peerListUiShow(), link);\n\t});\n\tconst auto shareLink = crl::guard(weak, [=] {\n\t\tdelegate()->peerListUiShow()->showBox(\n\t\t\tShareInviteLinkBox(&_window->session(), link));\n\t});\n\tconst auto getLinkQr = crl::guard(weak, [=] {\n\t\tdelegate()->peerListUiShow()->showBox(InviteLinkQrBox(\n\t\t\tnullptr,\n\t\t\tlink,\n\t\t\ttr::lng_group_invite_qr_title(),\n\t\t\ttr::lng_filters_link_qr_about()));\n\t});\n\tconst auto editLink = crl::guard(weak, [=] {\n\t\tdelegate()->peerListUiShow()->showBox(\n\t\t\tBox(ChatFilterLinkBox, &_window->session(), _data));\n\t});\n\tconst auto deleteLink = crl::guard(weak, [=] {\n\t\tdelegate()->peerListUiShow()->showBox(DeleteLinkBox(_window, _data));\n\t});\n\n\tconst auto createMenu = [=] {\n\t\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tcontainer,\n\t\t\tst::popupMenuWithIcons);\n\t\tresult->addAction(\n\t\t\ttr::lng_group_invite_context_copy(tr::now),\n\t\t\tcopyLink,\n\t\t\t&st::menuIconCopy);\n\t\tresult->addAction(\n\t\t\ttr::lng_group_invite_context_share(tr::now),\n\t\t\tshareLink,\n\t\t\t&st::menuIconShare);\n\t\tresult->addAction(\n\t\t\ttr::lng_group_invite_context_qr(tr::now),\n\t\t\tgetLinkQr,\n\t\t\t&st::menuIconQrCode);\n\t\tresult->addAction(\n\t\t\ttr::lng_filters_link_name_it(tr::now),\n\t\t\teditLink,\n\t\t\t&st::menuIconEdit);\n\t\tresult->addAction(\n\t\t\ttr::lng_group_invite_context_delete(tr::now),\n\t\t\tdeleteLink,\n\t\t\t&st::menuIconDelete);\n\t\treturn result;\n\t};\n\tUi::AddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_filters_link_subtitle(),\n\t\tst::filterLinkSubsectionTitlePadding);\n\n\tconst auto prefix = u\"https://\"_q;\n\tconst auto label = container->lifetime().make_state<Ui::InviteLinkLabel>(\n\t\tcontainer,\n\t\trpl::single(link.startsWith(prefix)\n\t\t\t? link.mid(prefix.size())\n\t\t\t: link),\n\t\tcreateMenu);\n\tcontainer->add(\n\t\tlabel->take(),\n\t\tst::inviteLinkFieldPadding);\n\n\tlabel->clicks(\n\t) | rpl::on_next(copyLink, label->lifetime());\n\n\tAddCopyShareLinkButtons(container, copyLink, shareLink);\n\n\tUi::AddSkip(container, st::inviteLinkJoinedRowPadding.bottom() * 2);\n\n\tUi::AddSkip(container);\n\n\tUi::AddDivider(container);\n}\n\nvoid LinkController::prepare() {\n\tExpects(!_data.url.isEmpty() || _data.chats.empty());\n\n\tfor (const auto &history : _data.chats) {\n\t\tconst auto peer = history->peer;\n\t\tauto row = std::make_unique<ChatRow>(\n\t\t\tpeer,\n\t\t\tFilterChatStatusText(peer),\n\t\t\tfalse);\n\t\tconst auto raw = row.get();\n\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\tdelegate()->peerListSetRowChecked(raw, true);\n\t\traw->finishCheckedAnimation();\n\t\t_initial.emplace(peer);\n\t}\n\tfor (const auto &history : _filterChats) {\n\t\tif (delegate()->peerListFindRow(history->peer->id.value)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto peer = history->peer;\n\t\tconst auto error = ErrorForSharing(history);\n\t\tauto row = std::make_unique<ChatRow>(\n\t\t\tpeer,\n\t\t\terror ? error->status : FilterChatStatusText(peer),\n\t\t\terror.has_value());\n\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\tif (error) {\n\t\t\t_denied.emplace(peer, error->toast);\n\t\t} else if (_data.url.isEmpty()) {\n\t\t\t_denied.emplace(peer);\n\t\t}\n\t}\n\tsetupAboveWidget();\n\tsetupBelowWidget();\n\tdelegate()->peerListRefreshRows();\n\t_selected = _initial;\n}\n\nvoid LinkController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto peer = row->peer();\n\tif (const auto i = _denied.find(peer); i != end(_denied)) {\n\t\tif (!i->second.isEmpty()) {\n\t\t\tdelegate()->peerListUiShow()->showToast(i->second);\n\t\t}\n\t} else {\n\t\tconst auto checked = row->checked();\n\t\tauto selected = _selected.current();\n\t\tdelegate()->peerListSetRowChecked(row, !checked);\n\t\tif (checked) {\n\t\t\tselected.remove(peer);\n\t\t} else {\n\t\t\tselected.emplace(peer);\n\t\t}\n\t\tconst auto has = (_initial != selected);\n\t\t_selected = std::move(selected);\n\t\t_hasChanges = has;\n\t}\n}\n\nvoid LinkController::toggleAllSelected(bool select) {\n\tauto selected = _selected.current();\n\tif (!select) {\n\t\tif (selected.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const auto &peer : selected) {\n\t\t\tconst auto row = delegate()->peerListFindRow(peer->id.value);\n\t\t\tAssert(row != nullptr);\n\t\t\tdelegate()->peerListSetRowChecked(row, false);\n\t\t}\n\t\tselected = {};\n\t} else {\n\t\tconst auto count = delegate()->peerListFullRowsCount();\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tconst auto row = delegate()->peerListRowAt(i);\n\t\t\tconst auto peer = row->peer();\n\t\t\tif (!_denied.contains(peer)) {\n\t\t\t\tdelegate()->peerListSetRowChecked(row, true);\n\t\t\t\tselected.emplace(peer);\n\t\t\t}\n\t\t}\n\t}\n\tconst auto has = (_initial != selected);\n\t_selected = std::move(selected);\n\t_hasChanges = has;\n}\n\nvoid LinkController::showFinished() {\n\t_showFinished.fire({});\n}\n\nvoid LinkController::setupAboveWidget() {\n\tusing namespace Settings;\n\n\tauto wrap = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tconst auto container = wrap.data();\n\n\taddHeader(container);\n\tif (!_data.url.isEmpty()) {\n\t\taddLinkBlock(container);\n\t}\n\n\tauto subtitle = _selected.value(\n\t) | rpl::map([=](const base::flat_set<not_null<PeerData*>> &selected) {\n\t\treturn _data.url.isEmpty()\n\t\t\t? tr::lng_filters_link_chats_no(tr::now)\n\t\t\t: selected.empty()\n\t\t\t? tr::lng_filters_link_chats_none(tr::now)\n\t\t\t: tr::lng_filters_link_chats(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tfloat64(selected.size()));\n\t});\n\tconst auto mayBeSelected = delegate()->peerListFullRowsCount()\n\t\t- int(_denied.size());\n\tauto selectedCount = _selected.value(\n\t) | rpl::map([](const base::flat_set<not_null<PeerData*>> &selected) {\n\t\treturn int(selected.size());\n\t});\n\tAddFilterSubtitleWithToggles(\n\t\tcontainer,\n\t\tstd::move(subtitle),\n\t\tmayBeSelected,\n\t\tstd::move(selectedCount),\n\t\t[=](bool select) { toggleAllSelected(select); });\n\n\t// Fix label cutting on text change from smaller to longer.\n\t_selected.changes() | rpl::on_next([=] {\n\t\tcontainer->resizeToWidth(container->widthNoMargins());\n\t}, container->lifetime());\n\n\tdelegate()->peerListSetAboveWidget(std::move(wrap));\n}\n\nvoid LinkController::setupBelowWidget() {\n\tdelegate()->peerListSetBelowWidget(\n\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\t(QWidget*)nullptr,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t(QWidget*)nullptr,\n\t\t\t\t(_data.url.isEmpty()\n\t\t\t\t\t? tr::lng_filters_link_chats_no_about()\n\t\t\t\t\t: tr::lng_filters_link_chats_about()),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::defaultBoxDividerLabelPadding));\n}\n\nMain::Session &LinkController::session() const {\n\treturn _window->session();\n}\n\nrpl::producer<bool> LinkController::hasChangesValue() const {\n\treturn _hasChanges.value();\n}\n\nbase::flat_set<not_null<PeerData*>> LinkController::selected() const {\n\treturn _selected.current();\n}\n\nLinksController::LinksController(\n\tnot_null<Window::SessionController*> window,\n\trpl::producer<std::vector<InviteLinkData>> content,\n\tFn<Data::ChatFilter()> currentFilter)\n: _window(window)\n, _currentFilter(std::move(currentFilter))\n, _rows(std::move(content)) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tfor (auto &image : _icons) {\n\t\t\timage = QImage();\n\t\t}\n\t}, _lifetime);\n}\n\nvoid LinksController::prepare() {\n\t_rows.value(\n\t) | rpl::on_next([=](const std::vector<InviteLinkData> &rows) {\n\t\trebuild(rows);\n\t}, _lifetime);\n}\n\nvoid LinksController::rebuild(const std::vector<InviteLinkData> &rows) {\n\tauto i = 0;\n\tauto count = delegate()->peerListFullRowsCount();\n\twhile (i < rows.size()) {\n\t\tif (i < count) {\n\t\t\tconst auto row = delegate()->peerListRowAt(i);\n\t\t\tstatic_cast<LinkRow*>(row.get())->update(rows[i]);\n\t\t} else {\n\t\t\tappendRow(rows[i]);\n\t\t}\n\t\t++i;\n\t}\n\twhile (i < count) {\n\t\tdelegate()->peerListRemoveRow(delegate()->peerListRowAt(i));\n\t\t--count;\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid LinksController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto link = static_cast<LinkRow*>(row.get())->data();\n\tdelegate()->peerListUiShow()->showBox(\n\t\tShowLinkBox(_window, _currentFilter(), link));\n}\n\nvoid LinksController::rowRightActionClicked(not_null<PeerListRow*> row) {\n\tdelegate()->peerListShowRowMenu(row, true);\n}\n\nbase::unique_qptr<Ui::PopupMenu> LinksController::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tauto result = createRowContextMenu(parent, row);\n\n\tif (result) {\n\t\t// First clear _menu value, so that we don't check row positions yet.\n\t\tbase::take(_menu);\n\n\t\t// Here unique_qptr is used like a shared pointer, where\n\t\t// not the last destroyed pointer destroys the object, but the first.\n\t\t_menu = base::unique_qptr<Ui::PopupMenu>(result.get());\n\t}\n\n\treturn result;\n}\n\nbase::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tconst auto real = static_cast<LinkRow*>(row.get());\n\tconst auto data = real->data();\n\tconst auto link = data.url;\n\tconst auto copyLink = [=] {\n\t\tCopyInviteLink(delegate()->peerListUiShow(), link);\n\t};\n\tconst auto shareLink = [=] {\n\t\tdelegate()->peerListUiShow()->showBox(\n\t\t\tShareInviteLinkBox(&_window->session(), link));\n\t};\n\tconst auto getLinkQr = [=] {\n\t\tdelegate()->peerListUiShow()->showBox(InviteLinkQrBox(\n\t\t\tnullptr,\n\t\t\tlink,\n\t\t\ttr::lng_group_invite_qr_title(),\n\t\t\ttr::lng_filters_link_qr_about()));\n\t};\n\tconst auto editLink = [=] {\n\t\tdelegate()->peerListUiShow()->showBox(\n\t\t\tBox(ChatFilterLinkBox, &_window->session(), data));\n\t};\n\tconst auto deleteLink = [=] {\n\t\tdelegate()->peerListUiShow()->showBox(DeleteLinkBox(_window, data));\n\t};\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\tresult->addAction(\n\t\ttr::lng_group_invite_context_copy(tr::now),\n\t\tcopyLink,\n\t\t&st::menuIconCopy);\n\tresult->addAction(\n\t\ttr::lng_group_invite_context_share(tr::now),\n\t\tshareLink,\n\t\t&st::menuIconShare);\n\tresult->addAction(\n\t\ttr::lng_group_invite_context_qr(tr::now),\n\t\tgetLinkQr,\n\t\t&st::menuIconQrCode);\n\tresult->addAction(\n\t\ttr::lng_filters_link_name_it(tr::now),\n\t\teditLink,\n\t\t&st::menuIconEdit);\n\tresult->addAction(\n\t\ttr::lng_group_invite_context_delete(tr::now),\n\t\tdeleteLink,\n\t\t&st::menuIconDelete);\n\treturn result;\n}\n\nMain::Session &LinksController::session() const {\n\treturn _window->session();\n}\n\nvoid LinksController::appendRow(const InviteLinkData &data) {\n\tdelegate()->peerListAppendRow(std::make_unique<LinkRow>(this, data));\n}\n\nvoid LinksController::rowUpdateRow(not_null<LinkRow*> row) {\n\tdelegate()->peerListUpdateRow(row);\n}\n\nvoid LinksController::rowPaintIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tColor color) {\n\tconst auto skip = st::inviteLinkIconSkip;\n\tconst auto inner = size - 2 * skip;\n\tconst auto bg = [&] {\n\t\tswitch (color) {\n\t\tcase Color::Permanent: return &st::msgFile1Bg;\n\t\t}\n\t\tUnexpected(\"Color in LinksController::rowPaintIcon.\");\n\t}();\n\tauto &icon = _icons[int(color)];\n\tif (icon.isNull()) {\n\t\ticon = QImage(\n\t\t\tQSize(inner, inner) * style::DevicePixelRatio(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\ticon.fill(Qt::transparent);\n\t\ticon.setDevicePixelRatio(style::DevicePixelRatio());\n\n\t\tauto p = QPainter(&icon);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(*bg);\n\t\t{\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.drawEllipse(Rect(Size(inner)));\n\t\t}\n\t\tst::inviteLinkIcon.paintInCenter(p, Rect(Size(inner)));\n\t}\n\tp.drawImage(x + skip, y + skip, icon);\n}\n\n} // namespace\n\nstd::vector<not_null<PeerData*>> CollectFilterLinkChats(\n\t\tconst Data::ChatFilter &filter) {\n\treturn filter.always() | ranges::views::filter([](\n\t\t\tnot_null<History*> history) {\n\t\treturn !ErrorForSharing(history);\n\t}) | ranges::views::transform(&History::peer) | ranges::to_vector;\n}\n\nbool GoodForExportFilterLink(\n\t\tnot_null<Window::SessionController*> window,\n\t\tconst Data::ChatFilter &filter) {\n\tusing Flag = Data::ChatFilter::Flag;\n\tif (!filter.never().empty() || (filter.flags() & Flag::RulesMask)) {\n\t\twindow->showToast(tr::lng_filters_link_cant(tr::now));\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nvoid ExportFilterLink(\n\t\tFilterId id,\n\t\tconst std::vector<not_null<PeerData*>> &peers,\n\t\tFn<void(Data::ChatFilterLink)> done,\n\t\tFn<void(QString)> fail) {\n\tExpects(!peers.empty());\n\n\tconst auto front = peers.front();\n\tconst auto session = &front->session();\n\tauto mtpPeers = peers | ranges::views::transform(\n\t\t[](not_null<PeerData*> peer) { return MTPInputPeer(peer->input()); }\n\t) | ranges::to<QVector<MTPInputPeer>>();\n\tsession->api().request(MTPchatlists_ExportChatlistInvite(\n\t\tMTP_inputChatlistDialogFilter(MTP_int(id)),\n\t\tMTP_string(), // title\n\t\tMTP_vector<MTPInputPeer>(std::move(mtpPeers))\n\t)).done([=](const MTPchatlists_ExportedChatlistInvite &result) {\n\t\tconst auto &data = result.data();\n\t\tsession->data().chatsFilters().apply(MTP_updateDialogFilter(\n\t\t\tMTP_flags(MTPDupdateDialogFilter::Flag::f_filter),\n\t\t\tMTP_int(id),\n\t\t\tdata.vfilter()));\n\t\tconst auto link = session->data().chatsFilters().add(\n\t\t\tid,\n\t\t\tdata.vinvite());\n\t\tdone(link);\n\t}).fail([=](const MTP::Error &error) {\n\t\tfail(error.type());\n\t}).send();\n}\n\nvoid EditLinkChats(\n\t\tconst Data::ChatFilterLink &link,\n\t\tbase::flat_set<not_null<PeerData*>> peers,\n\t\tFn<void(QString)> done) {\n\tExpects(!peers.empty());\n\tExpects(link.id != 0);\n\tExpects(!link.url.isEmpty());\n\n\tconst auto id = link.id;\n\tconst auto front = peers.front();\n\tconst auto session = &front->session();\n\tauto mtpPeers = peers | ranges::views::transform(\n\t\t[](not_null<PeerData*> peer) { return MTPInputPeer(peer->input()); }\n\t) | ranges::to<QVector<MTPInputPeer>>();\n\tsession->api().request(MTPchatlists_EditExportedInvite(\n\t\tMTP_flags(MTPchatlists_EditExportedInvite::Flag::f_peers),\n\t\tMTP_inputChatlistDialogFilter(MTP_int(link.id)),\n\t\tMTP_string(link.url),\n\t\tMTPstring(), // title\n\t\tMTP_vector<MTPInputPeer>(std::move(mtpPeers))\n\t)).done([=](const MTPExportedChatlistInvite &result) {\n\t\tconst auto link = session->data().chatsFilters().add(id, result);\n\t\tdone(QString());\n\t}).fail([=](const MTP::Error &error) {\n\t\tdone(error.type());\n\t}).send();\n}\n\nobject_ptr<Ui::BoxContent> ShowLinkBox(\n\t\tnot_null<Window::SessionController*> window,\n\t\tconst Data::ChatFilter &filter,\n\t\tconst Data::ChatFilterLink &link) {\n\tauto controller = std::make_unique<LinkController>(window, filter, link);\n\tcontroller->setStyleOverrides(&st::inviteLinkChatList);\n\tconst auto raw = controller.get();\n\tauto initBox = [=](not_null<Ui::BoxContent*> box) {\n\t\tbox->setTitle(!link.title.isEmpty()\n\t\t\t? rpl::single(link.title)\n\t\t\t: tr::lng_filters_link_title());\n\n\t\tconst auto saving = std::make_shared<bool>(false);\n\t\traw->hasChangesValue(\n\t\t) | rpl::on_next([=](bool has) {\n\t\t\tbox->setCloseByOutsideClick(!has);\n\t\t\tbox->setCloseByEscape(!has);\n\t\t\tbox->clearButtons();\n\t\t\tif (has) {\n\t\t\t\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\t\t\t\tif (*saving) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst auto chosen = raw->selected();\n\t\t\t\t\tif (chosen.empty()) {\n\t\t\t\t\t\tShowEmptyLinkError(window);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t*saving = true;\n\t\t\t\t\t\tEditLinkChats(link, chosen, crl::guard(box, [=](\n\t\t\t\t\t\t\t\tQString error) {\n\t\t\t\t\t\t\t*saving = false;\n\t\t\t\t\t\t\tif (error.isEmpty()) {\n\t\t\t\t\t\t\t\tbox->closeBox();\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tShowSaveError(window, error);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t\t\t} else {\n\t\t\t\tbox->addButton(tr::lng_about_done(), [=] {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t});\n\t\t\t}\n\t\t}, box->lifetime());\n\t};\n\treturn Box<PeerListBox>(std::move(controller), std::move(initBox));\n}\n\nQString FilterChatStatusText(not_null<PeerData*> peer) {\n\tif (const auto chat = peer->asChat()) {\n\t\tif (const auto count = chat->count; count > 0) {\n\t\t\treturn tr::lng_chat_status_members(tr::now, lt_count, count);\n\t\t}\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tif (channel->membersCountKnown()) {\n\t\t\treturn (channel->isBroadcast()\n\t\t\t\t? tr::lng_chat_status_subscribers\n\t\t\t\t: tr::lng_chat_status_members)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\tchannel->membersCount());\n\t\t}\n\t}\n\treturn QString();\n}\n\nvoid SetupFilterLinks(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> window,\n\t\trpl::producer<std::vector<Data::ChatFilterLink>> value,\n\t\tFn<Data::ChatFilter()> currentFilter) {\n\tauto &lifetime = container->lifetime();\n\tconst auto delegate = lifetime.make_state<PeerListContentDelegateShow>(\n\t\twindow->uiShow());\n\tconst auto controller = lifetime.make_state<LinksController>(\n\t\twindow,\n\t\tstd::move(value),\n\t\tstd::move(currentFilter));\n\tcontroller->setStyleOverrides(&st::inviteLinkList);\n\tconst auto content = container->add(object_ptr<PeerListContent>(\n\t\tcontainer,\n\t\tcontroller));\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n}\n\nvoid AddFilterSubtitleWithToggles(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> text,\n\t\tint selectableCount,\n\t\trpl::producer<int> selectedCount,\n\t\tFn<void(bool select)> toggle) {\n\tusing namespace rpl::mappers;\n\n\tconst auto selectable = (selectableCount > 0);\n\tauto padding = st::filterLinkSubsectionTitlePadding;\n\tif (selectable) {\n\t\tconst auto font = st::boxLinkButton.font;\n\t\tpadding.setRight(padding.right() + font->spacew + std::max(\n\t\t\tfont->width(tr::lng_filters_by_link_select(tr::now)),\n\t\t\tfont->width(tr::lng_filters_by_link_deselect(tr::now))));\n\t}\n\tconst auto title = Ui::AddSubsectionTitle(\n\t\tcontainer,\n\t\tstd::move(text),\n\t\tpadding);\n\tif (!selectable) {\n\t\treturn;\n\t}\n\tconst auto link = Ui::CreateChild<Ui::LinkButton>(\n\t\tcontainer.get(),\n\t\ttr::lng_filters_by_link_select(tr::now),\n\t\tst::boxLinkButton);\n\tconst auto canSelect = link->lifetime().make_state<rpl::variable<bool>>(\n\t\tstd::move(selectedCount) | rpl::map(_1 < selectableCount));\n\tcanSelect->value(\n\t) | rpl::on_next([=](bool can) {\n\t\tlink->setText(can\n\t\t\t? tr::lng_filters_by_link_select(tr::now)\n\t\t\t: tr::lng_filters_by_link_deselect(tr::now));\n\t}, link->lifetime());\n\tlink->setClickedCallback([=] {\n\t\ttoggle(canSelect->current());\n\t});\n\n\trpl::combine(\n\t\tcontainer->widthValue(),\n\t\ttitle->topValue(),\n\t\tlink->widthValue()\n\t) | rpl::on_next([=](int outer, int y, int width) {\n\t\tlink->move(outer - st::boxRowPadding.right() - width, y);\n\t}, link->lifetime());\n}\n\nstd::unique_ptr<PeerListRow> MakeFilterChatRow(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &status,\n\t\tbool disabled) {\n\treturn std::make_unique<ChatRow>(peer, status, disabled);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/filters/edit_filter_links.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nclass PeerListRow;\n\nnamespace Ui {\nclass Show;\nclass BoxContent;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Data {\nclass ChatFilter;\nstruct ChatFilterLink;\n} // namespace Data\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\n[[nodiscard]] std::vector<not_null<PeerData*>> CollectFilterLinkChats(\n\tconst Data::ChatFilter &filter);\n[[nodiscard]] bool GoodForExportFilterLink(\n\tnot_null<Window::SessionController*> window,\n\tconst Data::ChatFilter &filter);\n\nvoid ExportFilterLink(\n\tFilterId id,\n\tconst std::vector<not_null<PeerData*>> &peers,\n\tFn<void(Data::ChatFilterLink)> done,\n\tFn<void(QString)> fail);\n\n[[nodiscard]] object_ptr<Ui::BoxContent> ShowLinkBox(\n\tnot_null<Window::SessionController*> window,\n\tconst Data::ChatFilter &filter,\n\tconst Data::ChatFilterLink &link);\n[[nodiscard]] QString FilterChatStatusText(not_null<PeerData*> peer);\n\nvoid SetupFilterLinks(\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<Window::SessionController*> window,\n\trpl::producer<std::vector<Data::ChatFilterLink>> value,\n\tFn<Data::ChatFilter()> currentFilter);\n\nvoid AddFilterSubtitleWithToggles(\n\tnot_null<Ui::VerticalLayout*> container,\n\trpl::producer<QString> text,\n\tint selectableCount,\n\trpl::producer<int> selectedCount,\n\tFn<void(bool select)> toggle);\n\n[[nodiscard]] std::unique_ptr<PeerListRow> MakeFilterChatRow(\n\tnot_null<PeerData*> peer,\n\tconst QString &status,\n\tbool disabled);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/gift_credits_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/gift_credits_box.h\"\n\n#include \"api/api_credits.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"core/ui_integration.h\" // TextContext.\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/session/session_show.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/effects/premium_stars_colored.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_channel_earn.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_premium.h\"\n\nnamespace Ui {\n\nvoid GiftCreditsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> gifted) {\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setStyle(st::creditsGiftBox);\n\tbox->setNoContentMargin(true);\n\tbox->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); });\n\n\tconst auto content = box->setPinnedToTopContent(\n\t\tobject_ptr<Ui::VerticalLayout>(box));\n\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tconst auto &stUser = st::premiumGiftsUserpicButton;\n\tconst auto userpicWrap = content->add(\n\t\tobject_ptr<Ui::UserpicButton>(content, peer, stUser),\n\t\tstyle::al_top);\n\tuserpicWrap->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\n\tSettings::AddMiniStars(\n\t\tcontent,\n\t\tUi::CreateChild<Ui::RpWidget>(content),\n\t\tstUser.photoSize,\n\t\tbox->width(),\n\t\t2.);\n\t{\n\t\tUi::AddSkip(content);\n\t\tauto link = tr::lng_credits_box_history_entry_gift_about_link(\n\t\t\tlt_emoji,\n\t\t\trpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\ttr::rich\n\t\t) | rpl::map([](TextWithEntities text) {\n\t\t\treturn tr::link(\n\t\t\t\tstd::move(text),\n\t\t\t\tu\"internal:stars_examples\"_q);\n\t\t});\n\t\tcontent->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_credits_box_history_entry_gift_out_about(\n\t\t\t\t\tlt_user,\n\t\t\t\t\trpl::single(TextWithEntities{ peer->shortName() }),\n\t\t\t\t\tlt_link,\n\t\t\t\t\tstd::move(link),\n\t\t\t\t\ttr::rich),\n\t\t\t\tst::creditsBoxAbout),\n\t\t\tst::boxRowPadding,\n\t\t\tstyle::al_top);\n\t}\n\tUi::AddSkip(content);\n\tUi::AddSkip(box->verticalLayout());\n\n\tSettings::FillCreditOptions(\n\t\tMain::MakeSessionShow(box->uiShow(), &peer->session()),\n\t\tbox->verticalLayout(),\n\t\tpeer,\n\t\tCreditsAmount(),\n\t\t[=] { gifted(); box->uiShow()->hideLayer(); },\n\t\tbox->showFinishes(),\n\t\ttr::lng_credits_summary_options_subtitle(),\n\t\t{});\n\n\tbox->setPinnedToBottomContent(\n\t\tobject_ptr<Ui::VerticalLayout>(box));\n}\n\nvoid ShowGiftCreditsBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFn<void()> gifted) {\n\n\tclass Controller final : public ContactsBoxController {\n\tpublic:\n\t\tController(\n\t\t\tnot_null<Main::Session*> session,\n\t\t\tFn<void(not_null<PeerData*>)> choose)\n\t\t: ContactsBoxController(session)\n\t\t, _choose(std::move(choose)) {\n\t\t}\n\n\tprotected:\n\t\tstd::unique_ptr<PeerListRow> createRow(\n\t\t\t\tnot_null<UserData*> user) override {\n\t\t\tif (user->isSelf()\n\t\t\t\t|| user->isBot()\n\t\t\t\t|| user->isServiceUser()\n\t\t\t\t|| user->isInaccessible()) {\n\t\t\t\treturn nullptr;\n\t\t\t}\n\t\t\treturn ContactsBoxController::createRow(user);\n\t\t}\n\n\t\tvoid rowClicked(not_null<PeerListRow*> row) override {\n\t\t\t_choose(row->peer());\n\t\t}\n\n\tprivate:\n\t\tconst Fn<void(not_null<PeerData*>)> _choose;\n\n\t};\n\tauto initBox = [=](not_null<PeerListBox*> peersBox) {\n\t\tpeersBox->setTitle(tr::lng_credits_gift_title());\n\t\tpeersBox->addButton(tr::lng_cancel(), [=] { peersBox->closeBox(); });\n\t};\n\n\tconst auto show = controller->uiShow();\n\tauto listController = std::make_unique<Controller>(\n\t\t&controller->session(),\n\t\t[=](not_null<PeerData*> peer) {\n\t\t\tshow->showBox(Box(GiftCreditsBox, peer, gifted));\n\t\t});\n\tshow->showBox(\n\t\tBox<PeerListBox>(std::move(listController), std::move(initBox)),\n\t\tUi::LayerOption::KeepOther);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/gift_credits_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\n\nvoid ShowGiftCreditsBox(\n\tnot_null<Window::SessionController*> controller,\n\tFn<void()> gifted);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/gift_premium_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/gift_premium_box.h\"\n\n#include \"api/api_premium.h\"\n#include \"api/api_premium_option.h\"\n#include \"apiwrap.h\"\n#include \"base/event_filter.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/unixtime.h\"\n#include \"base/weak_ptr.h\"\n#include \"boxes/peer_list_controllers.h\" // ContactsBoxController.\n#include \"boxes/peers/prepare_short_info_box.h\"\n#include \"boxes/peers/replace_boost_box.h\" // BoostsForGift.\n#include \"boxes/premium_preview_box.h\" // ShowPremiumPreviewBox.\n#include \"boxes/star_gift_box.h\" // ShowStarGiftBox.\n#include \"boxes/star_gift_preview_box.h\" // StarGiftPreviewBox.\n#include \"core/ui_integration.h\"\n#include \"data/components/gift_auctions.h\"\n#include \"data/data_boosts.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_credits.h\"\n#include \"data/data_document.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_media_types.h\" // Data::GiveawayStart.\n#include \"data/data_peer_values.h\" // Data::PeerPremiumValue.\n#include \"data/data_session.h\"\n#include \"data/data_premium_subscription_option.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n//#include \"info/channel_statistics/boosts/giveaway/boost_badge.h\" // InfiniteRadialAnimationWidget.\n#include \"info/channel_statistics/earn/earn_icons.h\"\n//#include \"info/profile/info_profile_badge.h\"\n//#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"payments/payments_checkout_process.h\"\n#include \"payments/payments_form.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/basic_click_handlers.h\" // UrlClickHandler::Open.\n#include \"ui/boxes/boost_box.h\" // StartFireworks.\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/controls/table_rows.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/effects/premium_stars_colored.h\"\n#include \"ui/effects/premium_top_bar.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/gradient_round_button.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/table_layout.h\"\n#include \"window/window_peer_menu.h\" // ShowChooseRecipientBox.\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_premium.h\"\n\n#include <QtGui/QGuiApplication>\n\nnamespace {\n\nconstexpr auto kTooltipDuration = 3 * crl::time(1000);\nconstexpr auto kHorizontalBar = QChar(0x2015);\nconstexpr auto kSpinnerRows = 6;\nconstexpr auto kSpinDuration = crl::time(120);\n\nusing Ui::AddTableRow;\nusing Ui::TableRowTooltipData;\nusing SpinnerState = Data::GiftUpgradeSpinner::State;\n\n[[nodiscard]] QString CreateMessageLink(\n\t\tnot_null<Main::Session*> session,\n\t\tPeerId peerId,\n\t\tuint64 messageId) {\n\tif (const auto msgId = MsgId(peerId ? messageId : 0)) {\n\t\tconst auto peer = session->data().peer(peerId);\n\t\tif (const auto channel = peer->asBroadcast()) {\n\t\t\tconst auto username = channel->username();\n\t\t\tconst auto base = username.isEmpty()\n\t\t\t\t? u\"c/%1\"_q.arg(peerToChannel(channel->id).bare)\n\t\t\t\t: username;\n\t\t\tconst auto query = base + '/' + QString::number(msgId.bare);\n\t\t\treturn session->createInternalLink(query);\n\t\t}\n\t}\n\treturn QString();\n};\n\n[[nodiscard]] QString FixupTransactionId(QString origin) {\n\treturn origin.replace(kHorizontalBar, QChar('-'));\n}\n\n[[nodiscard]] Data::GiftCodeLink MakeGiftCodeLink(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &slug) {\n\tconst auto path = u\"giftcode/\"_q + slug;\n\treturn {\n\t\tsession->createInternalLink(path),\n\t\tsession->createInternalLinkFull(path),\n\t};\n}\n\n[[nodiscard]] TextWithEntities FormatValuePrice(\n\t\tint64 price,\n\t\tQString currency,\n\t\tbool approximately = false) {\n\tauto result = TextWithEntities();\n\tif (approximately) {\n\t\tresult.append('~');\n\t}\n\treturn result.append(Ui::FillAmountAndCurrency(price, currency));\n}\n\n[[nodiscard]] TextWithEntities FormatValueDate(TimeId date) {\n\tconst auto parsed = base::unixtime::parse(date).date();\n\tconst auto day = parsed.day();\n\tconst auto month = parsed.month();\n\tconst auto year = parsed.year();\n\treturn { tr::lng_month_day_year(\n\t\t\ttr::now,\n\t\t\tlt_month,\n\t\t\tLang::MonthDay(month)(tr::now),\n\t\t\tlt_day,\n\t\t\tQString::number(day),\n\t\t\tlt_year,\n\t\t\tQString::number(year))\n\t};\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeLinkCopyIcon(\n\t\tnot_null<QWidget*> parent) {\n\tauto result = object_ptr<Ui::RpWidget>(parent);\n\tconst auto raw = result.data();\n\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\tconst auto &icon = st::giveawayGiftCodeLinkCopy;\n\t\tconst auto left = (raw->width() - icon.width()) / 2;\n\t\tconst auto top = (raw->height() - icon.height()) / 2;\n\t\ticon.paint(p, left, top, raw->width());\n\t}, raw->lifetime());\n\n\traw->resize(\n\t\tst::giveawayGiftCodeLinkCopyWidth,\n\t\tst::giveawayGiftCodeLinkHeight);\n\n\traw->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\treturn result;\n}\n\n[[nodiscard]] tr::phrase<lngtag_count> GiftDurationPhrase(int days) {\n\treturn (days < 30)\n\t\t? tr::lng_premium_gift_duration_days\n\t\t: (days < 30 * 12)\n\t\t? tr::lng_premium_gift_duration_months\n\t\t: tr::lng_premium_gift_duration_years;\n}\n\n[[nodiscard]] object_ptr<Ui::FlatLabel> MakeMaybeMultilineTokenValue(\n\t\tnot_null<Ui::TableLayout*> table,\n\t\tQString token,\n\t\tSettings::CreditsEntryBoxStyleOverrides st) {\n\tconstexpr auto kOneLineCount = 24;\n\ttoken = token.replace(QChar('-'), kHorizontalBar);\n\tconst auto oneLine = token.length() <= kOneLineCount;\n\treturn object_ptr<Ui::FlatLabel>(\n\t\ttable,\n\t\trpl::single(\n\t\t\tUi::Text::Wrapped({ token }, EntityType::Code, {})),\n\t\t(oneLine\n\t\t\t? table->st().defaultValue\n\t\t\t: st.tableValueMultiline\n\t\t\t? *st.tableValueMultiline\n\t\t\t: st::giveawayGiftCodeValueMultiline));\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakePriceWithChangePercentValue(\n\t\tnot_null<Ui::TableLayout*> table,\n\t\tconst std::shared_ptr<Data::UniqueGiftValue> &value) {\n\tauto label = object_ptr<Ui::FlatLabel>(\n\t\ttable,\n\t\trpl::single(FormatValuePrice(value->lastSalePrice, value->currency)),\n\t\ttable->st().defaultValue);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto initial = value->initialSalePrice;\n\tif (!initial) {\n\t\treturn label;\n\t}\n\n\tconst auto diff = (100 * (value->lastSalePrice - initial))\n\t\t/ float64(initial);\n\tconst auto use = (std::abs(diff) >= 10.)\n\t\t? base::SafeRound(diff)\n\t\t: (int(base::SafeRound(diff * 100)) / 100.);\n\tconst auto prefix = (use > 0) ? u\"+\"_q : QString();\n\tconst auto percent = Lang::FormatExactCountDecimal(use) + '%';\n\tauto text = rpl::single(prefix + percent);\n\treturn MakeValueWithSmallButton(table, label, std::move(text)).widget;\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeMinimumPriceValue(\n\t\tnot_null<Ui::TableLayout*> table,\n\t\tstd::shared_ptr<TableRowTooltipData> tooltip,\n\t\tconst std::shared_ptr<Data::UniqueGift> &unique) {\n\tconst auto &value = unique->value;\n\tconst auto text = FormatValuePrice(value->minimumPrice, value->currency);\n\treturn Ui::MakeTableValueWithTooltip(\n\t\ttable,\n\t\tstd::move(tooltip),\n\t\ttext,\n\t\ttr::lng_gift_value_minimum_price_tooltip(\n\t\t\ttr::now,\n\t\t\tlt_amount,\n\t\t\ttr::bold(text.text),\n\t\t\tlt_gift,\n\t\t\ttr::bold(unique->title),\n\t\t\ttr::marked)).widget;\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeAveragePriceValue(\n\t\tnot_null<Ui::TableLayout*> table,\n\t\tstd::shared_ptr<TableRowTooltipData> tooltip,\n\t\tconst std::shared_ptr<Data::UniqueGift> &unique) {\n\tconst auto &value = unique->value;\n\tconst auto text = FormatValuePrice(value->averagePrice, value->currency);\n\treturn Ui::MakeTableValueWithTooltip(\n\t\ttable,\n\t\tstd::move(tooltip),\n\t\ttext,\n\t\ttr::lng_gift_value_average_price_tooltip(\n\t\t\ttr::now,\n\t\t\tlt_amount,\n\t\t\ttr::bold(text.text),\n\t\t\tlt_gift,\n\t\t\ttr::bold(unique->title),\n\t\t\ttr::marked)).widget;\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeAttributeValue(\n\t\tnot_null<Ui::TableLayout*> table,\n\t\tconst Data::UniqueGiftAttribute &attribute,\n\t\tFn<void(not_null<Ui::RpWidget*>, int)> showTooltip) {\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\ttable,\n\t\tattribute.name,\n\t\ttable->st().defaultValue);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto permille = attribute.rarityPermille();\n\tconst auto rarity = attribute.rarityType();\n\tauto text = rpl::single(Data::UniqueGiftAttributeText(attribute));\n\n\tconst auto handler = [=](not_null<Ui::RpWidget*> button) {\n\t\tshowTooltip(button, permille);\n\t};\n\tauto result = MakeValueWithSmallButton(\n\t\ttable,\n\t\tlabel,\n\t\tstd::move(text),\n\t\thandler);\n\tif (rarity != Data::UniqueGiftRarity::Default) {\n\t\tconst auto colors = Data::UniqueGiftRarityBadgeColors(rarity);\n\t\tresult.button->setBrushOverride(colors.bg);\n\t\tresult.button->setTextFgOverride(colors.fg);\n\t\tresult.button->setRippleOverride(colors.bg);\n\t}\n\treturn std::move(result.widget);\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeAttributeValue(\n\t\tnot_null<Ui::TableLayout*> table,\n\t\tconst Data::UniqueGiftAttribute &attribute,\n\t\tFn<void(not_null<Ui::RpWidget*>, int)> showTooltip,\n\t\tstd::shared_ptr<Data::GiftUpgradeSpinner> spinner,\n\t\tstd::vector<Data::UniqueGiftAttribute> spinning,\n\t\tSpinnerState finishedState) {\n\tif (!spinner) {\n\t\treturn MakeAttributeValue(table, attribute, showTooltip);\n\t}\n\tauto result = object_ptr<Ui::RpWidget>(table);\n\tconst auto raw = result.get();\n\n\tstruct Row {\n\t\tData::UniqueGiftAttribute attribute;\n\t\tobject_ptr<Ui::RpWidget> widget;\n\t\tQImage frame;\n\t};\n\tstruct State {\n\t\tint wasIndex = 0;\n\t\tint nowIndex = 0;\n\t\tstd::vector<Row> rows;\n\t\tUi::Animations::Simple animation;\n\t\tQImage fading;\n\t\tFn<void()> repaint;\n\t\tbool finished = false;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>();\n\tstate->repaint = [=] { raw->update(); };\n\n\tconst auto margin = st::giveawayGiftCodeValueMargin;\n\tconst auto startWithin = [=] {\n\t\tExpects(!state->rows.empty());\n\n\t\tstate->wasIndex = state->nowIndex;\n\t\tstate->nowIndex = (state->nowIndex + 1) % state->rows.size();\n\t\tstate->animation.start(state->repaint, 0., 1., kSpinDuration);\n\t};\n\tconst auto startToTarget = [=] {\n\t\tif (state->nowIndex != 0) {\n\t\t\tstate->wasIndex = state->nowIndex;\n\t\t\tstate->nowIndex = 0;\n\t\t\tstate->animation.start(\n\t\t\t\tstate->repaint,\n\t\t\t\t0.,\n\t\t\t\t1.,\n\t\t\t\tkSpinDuration * 3,\n\t\t\t\tanim::easeOutCubic);\n\t\t}\n\t};\n\n\tconst auto add = [&](const Data::UniqueGiftAttribute &value) {\n\t\tif (state->rows.size() >= kSpinnerRows) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto already = ranges::contains(\n\t\t\tstate->rows,\n\t\t\tvalue,\n\t\t\t&Row::attribute);\n\t\tif (!already) {\n\t\t\tstate->rows.push_back(Row{\n\t\t\t\t.attribute = value,\n\t\t\t\t.widget = MakeAttributeValue(table, value, showTooltip),\n\t\t\t});\n\t\t\tconst auto widget = state->rows.back().widget.get();\n\t\t\twidget->setParent(raw);\n\t\t\twidget->hide();\n\t\t}\n\t\treturn true;\n\t};\n\tadd(attribute);\n\tfor (const auto &item : spinning) {\n\t\tif (!add(item)) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\traw->widthValue() | rpl::on_next([=](int width) {\n\t\tauto height = 0;\n\t\tconst auto inner = width - margin.left() - margin.right();\n\t\tif (inner > 0) {\n\t\t\tfor (const auto &row : state->rows) {\n\t\t\t\trow.widget->resizeToWidth(inner);\n\t\t\t\trow.widget->move(margin.left(), margin.top());\n\t\t\t\theight = std::max(height, row.widget->height());\n\t\t\t}\n\t\t}\n\t\traw->resize(width, margin.top() + height + margin.bottom());\n\t\tcrl::on_main(raw, [=] {\n\t\t\tfor (const auto &row : state->rows) {\n\t\t\t\tUi::SendPendingMoveResizeEvents(row.widget.get());\n\t\t\t}\n\t\t});\n\t}, raw->lifetime());\n\n\tstyle::PaletteChanged() | rpl::on_next([=] {\n\t\tfor (auto &row : state->rows) {\n\t\t\trow.frame = QImage();\n\t\t}\n\t\tstate->fading = QImage();\n\t}, raw->lifetime());\n\n\traw->paintOn([=](QPainter &p) {\n\t\tif (state->finished) {\n\t\t\treturn;\n\t\t}\n\t\tauto progress = state->animation.value(1.);\n\t\tif (progress >= 1.) {\n\t\t\tconst auto ending = (spinner->state.current() >= finishedState);\n\t\t\tif (ending) {\n\t\t\t\tstartToTarget();\n\t\t\t} else {\n\t\t\t\tstartWithin();\n\t\t\t}\n\t\t\tprogress = state->animation.value(1.);\n\t\t\tif (ending && progress >= 1.) {\n\t\t\t\tstate->rows.front().widget->show();\n\t\t\t\tstate->finished = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto h = raw->height();\n\t\tif (state->fading.height() != h * ratio) {\n\t\t\tstate->fading = QImage(\n\t\t\t\tQSize(1, h) * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tstate->fading.setDevicePixelRatio(ratio);\n\t\t\tstate->fading.fill(Qt::transparent);\n\t\t\tauto q = QPainter(&state->fading);\n\t\t\tauto brush = QLinearGradient(0, 0, 0, margin.top());\n\t\t\tbrush.setStops({\n\t\t\t\t{ 0., anim::with_alpha(st::boxBg->c, 1.) },\n\t\t\t\t{ 1., anim::with_alpha(st::boxBg->c, 0.) },\n\t\t\t});\n\t\t\tq.fillRect(0, 0, 1, margin.top(), brush);\n\t\t\tbrush.setStart(0, h);\n\t\t\tbrush.setFinalStop(0, h - margin.bottom());\n\t\t\tq.fillRect(0, h - margin.bottom(), 1, margin.bottom(), brush);\n\t\t}\n\t\tauto &was = state->rows[state->wasIndex];\n\t\tauto &now = state->rows[state->nowIndex];\n\t\tconst auto validate = [&](Row &row) {\n\t\t\tconst auto size = row.widget->size();\n\t\t\tif (row.frame.size() != size * ratio) {\n\t\t\t\trow.frame = Ui::GrabWidgetToImage(row.widget.get());\n\t\t\t}\n\t\t};\n\t\tvalidate(was);\n\t\tvalidate(now);\n\t\tconst auto t = progress * (margin.top() + was.widget->height());\n\t\tconst auto b = progress * (now.widget->height() + margin.bottom());\n\t\tp.drawImage(\n\t\t\tmargin.left(),\n\t\t\tmargin.top() - int(base::SafeRound(t)),\n\t\t\twas.frame);\n\t\tp.drawImage(\n\t\t\tmargin.left(),\n\t\t\traw->height() - int(base::SafeRound(b)),\n\t\t\tnow.frame);\n\t\tp.drawImage(raw->rect(), state->fading);\n\t});\n\n\tstartWithin();\n\n\treturn result;\n}\n\nvoid AddUniqueGiftPropertyRows(\n\t\tnot_null<Ui::RpWidget*> container,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<Ui::TableLayout*> table,\n\t\tstd::shared_ptr<Data::UniqueGift> unique,\n\t\tstd::shared_ptr<Data::GiftUpgradeSpinner> spinner) {\n\tconst auto tooltip = std::make_shared<TableRowTooltipData>(\n\t\tTableRowTooltipData{ .parent = container });\n\tconst auto showTooltip = [=](\n\t\t\tnot_null<Ui::RpWidget*> widget,\n\t\t\trpl::producer<TextWithEntities> text) {\n\t\tShowTableRowTooltip(\n\t\t\ttooltip,\n\t\t\twidget,\n\t\t\tstd::move(text),\n\t\t\tkTooltipDuration);\n\t};\n\n\tstruct VariantsList {\n\t\trpl::variable<Data::UniqueGiftAttributes> attributes;\n\t\tbool requested = false;\n\t\tbool inited = false;\n\t\trpl::lifetime clickLifetime;\n\t};\n\tconst auto variants = container->lifetime().make_state<VariantsList>();\n\n\tconst auto session = &unique->model.document->session();\n\tconst auto giftId = unique->initialGiftId;\n\tconst auto initVariants = [=] {\n\t\tif (variants->requested || variants->inited) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto auctions = &session->giftAuctions();\n\t\tif (auto attributes = auctions->attributes(giftId)) {\n\t\t\tvariants->inited = true;\n\t\t\tvariants->attributes = std::move(*attributes);\n\t\t} else {\n\t\t\tvariants->requested = true;\n\t\t\tauctions->requestAttributes(giftId, crl::guard(container, [=] {\n\t\t\t\tvariants->inited = true;\n\t\t\t\tvariants->attributes.force_assign(\n\t\t\t\t\t*auctions->attributes(giftId));\n\t\t\t}));\n\t\t}\n\t};\n\n\tconst auto title = unique->title;\n\tconst auto showRarity = [=](Data::GiftAttributeId id) {\n\t\treturn [=](\n\t\t\t\tnot_null<Ui::RpWidget*> widget,\n\t\t\t\tint rarityPermille) {\n\t\t\tinitVariants();\n\n\t\t\tconst auto weak = base::make_weak(widget);\n\t\t\tvariants->clickLifetime = variants->attributes.value(\n\t\t\t) | rpl::filter([=] {\n\t\t\t\treturn variants->inited;\n\t\t\t}) | rpl::take(1) | rpl::on_next([=](\n\t\t\t\t\tconst Data::UniqueGiftAttributes &list) {\n\t\t\t\tif (!list.models.empty()) {\n\t\t\t\t\tshow->show(Box(\n\t\t\t\t\t\tUi::StarGiftPreviewBox,\n\t\t\t\t\t\ttitle,\n\t\t\t\t\t\tlist,\n\t\t\t\t\t\tid.type,\n\t\t\t\t\t\tunique));\n\t\t\t\t} else if (const auto widget = weak.get()) {\n\t\t\t\t\tconst auto percent = Data::UniqueGiftAttributeText(\n\t\t\t\t\t\t{ .rarityValue = rarityPermille });\n\t\t\t\t\tshowTooltip(widget, tr::lng_gift_unique_rarity(\n\t\t\t\t\t\tlt_percent,\n\t\t\t\t\t\trpl::single(tr::marked(percent)),\n\t\t\t\t\t\ttr::marked));\n\t\t\t\t}\n\t\t\t});\n\t\t};\n\t};\n\tconst auto empty = std::vector<Data::UniqueGiftAttribute>();\n\tconst auto extract = [&](const auto &list) {\n\t\tauto result = empty;\n\t\tresult.reserve(list.size());\n\t\tfor (const auto &item : list) {\n\t\t\tresult.push_back(item);\n\t\t}\n\t\treturn result;\n\t};\n\tconst auto attributes = spinner ? &spinner->attributes : nullptr;\n\tconst auto models = spinner ? extract(attributes->models) : empty;\n\tconst auto patterns = spinner ? extract(attributes->patterns) : empty;\n\tconst auto backdrops = spinner ? extract(attributes->backdrops) : empty;\n\tconst auto margin = spinner\n\t\t? style::margins()\n\t\t: st::giveawayGiftCodeValueMargin;\n\tAddTableRow(\n\t\ttable,\n\t\ttr::lng_gift_unique_model(),\n\t\tMakeAttributeValue(\n\t\t\ttable,\n\t\t\tunique->model,\n\t\t\tshowRarity(IdFor(unique->model)),\n\t\t\tspinner,\n\t\t\tmodels,\n\t\t\tSpinnerState::FinishedModel),\n\t\tmargin);\n\tAddTableRow(\n\t\ttable,\n\t\ttr::lng_gift_unique_symbol(),\n\t\tMakeAttributeValue(\n\t\t\ttable,\n\t\t\tunique->pattern,\n\t\t\tshowRarity(IdFor(unique->pattern)),\n\t\t\tspinner,\n\t\t\tpatterns,\n\t\t\tSpinnerState::FinishedPattern),\n\t\tmargin);\n\tAddTableRow(\n\t\ttable,\n\t\ttr::lng_gift_unique_backdrop(),\n\t\tMakeAttributeValue(\n\t\t\ttable,\n\t\t\tunique->backdrop,\n\t\t\tshowRarity(IdFor(unique->backdrop)),\n\t\t\tspinner,\n\t\t\tbackdrops,\n\t\t\tSpinnerState::FinishedBackdrop),\n\t\tmargin);\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeStarGiftStarsValue(\n\t\tnot_null<Ui::TableLayout*> table,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::CreditsHistoryEntry &entry,\n\t\tFn<void()> convertToStars) {\n\tauto helper = Ui::Text::CustomEmojiHelper();\n\tconst auto addUpgradeToValue = !entry.credits.ton()\n\t\t&& !entry.giftUpgradeGifted\n\t\t&& !entry.giftUpgradeSeparate\n\t\t&& entry.starsUpgradedBySender;\n\tconst auto amount = addUpgradeToValue\n\t\t? CreditsAmount(\n\t\t\tentry.credits.whole() + entry.starsUpgradedBySender,\n\t\t\tentry.credits.nano())\n\t\t: entry.credits;\n\tconst auto price = helper.paletteDependent(Ui::Earn::IconCreditsEmoji(\n\t)).append(' ').append(Lang::FormatCreditsAmountDecimal(amount));\n\tauto label = object_ptr<Ui::FlatLabel>(\n\t\ttable,\n\t\trpl::single(price),\n\t\ttable->st().defaultValue,\n\t\tst::defaultPopupMenu,\n\t\thelper.context());\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tif (!convertToStars) {\n\t\treturn label;\n\t}\n\tconst auto handler = [=](not_null<Ui::RpWidget*> button) {\n\t\tconvertToStars();\n\t};\n\tauto text = tr::lng_gift_sell_small(\n\t\tlt_count_decimal,\n\t\trpl::single(entry.starsConverted * 1.));\n\treturn MakeValueWithSmallButton(\n\t\ttable,\n\t\tlabel.release(),\n\t\tstd::move(text),\n\t\thandler).widget;\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeUniqueGiftValueValue(\n\t\tnot_null<Ui::TableLayout*> table,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::CreditsHistoryEntry &entry,\n\t\tSettings::CreditsEntryBoxStyleOverrides st) {\n\tconst auto unique = entry.uniqueGift;\n\tconst auto value = unique ? unique->value : nullptr;\n\tconst auto loading = std::make_shared<bool>(false);\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\ttable,\n\t\trpl::single(\n\t\t\tFormatValuePrice(value->valuePrice, value->currency, true)),\n\t\ttable->st().defaultValue,\n\t\tst::defaultPopupMenu);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto handler = [=](not_null<Ui::RpWidget*> button) {\n\t\tif (value->initialPriceStars) {\n\t\t\tshow->show(Box(Settings::UniqueGiftValueBox, show, entry, st));\n\t\t\treturn;\n\t\t} else if (*loading) {\n\t\t\treturn;\n\t\t}\n\t\t*loading = true;\n\t\tshow->session().api().request(MTPpayments_GetUniqueStarGiftValueInfo(\n\t\t\tMTP_string(unique->slug)\n\t\t)).done([=](const MTPpayments_UniqueStarGiftValueInfo &result) {\n\t\t\t*loading = false;\n\n\t\t\tconst auto &data = result.data();\n\t\t\tvalue->currency = qs(data.vcurrency());\n\t\t\tvalue->valuePrice = data.vvalue().v;\n\t\t\tvalue->initialSaleDate = data.vinitial_sale_date().v;\n\t\t\tvalue->initialPriceStars = CreditsAmount(\n\t\t\t\tdata.vinitial_sale_stars().v);\n\t\t\tvalue->initialSalePrice = data.vinitial_sale_price().v;\n\t\t\tvalue->lastSaleDate = data.vlast_sale_date().value_or_empty();\n\t\t\tvalue->lastSalePrice = data.vlast_sale_price().value_or_empty();\n\t\t\tvalue->lastSaleFragment = data.is_last_sale_on_fragment();\n\t\t\tvalue->minimumPrice = data.vfloor_price().value_or_empty();\n\t\t\tvalue->averagePrice = data.vaverage_price().value_or_empty();\n\t\t\tvalue->forSaleOnTelegram = data.vlisted_count().value_or_empty();\n\t\t\tvalue->forSaleOnFragment = int(\n\t\t\t\tdata.vfragment_listed_count().value_or_empty());\n\t\t\tvalue->fragmentUrl = qs(\n\t\t\t\tdata.vfragment_listed_url().value_or_empty());\n\n\t\t\tshow->show(Box(Settings::UniqueGiftValueBox, show, entry, st));\n\t\t}).send();\n\t};\n\treturn MakeValueWithSmallButton(\n\t\ttable,\n\t\tlabel,\n\t\ttr::lng_gift_unique_value_learn_more(),\n\t\thandler).widget;\n}\n\nvoid AddTable(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tSettings::CreditsEntryBoxStyleOverrides st,\n\t\tconst Api::GiftCode &current,\n\t\tbool skipReason) {\n\tauto table = container->add(\n\t\tobject_ptr<Ui::TableLayout>(\n\t\t\tcontainer,\n\t\t\tst.table ? *st.table : st::giveawayGiftCodeTable),\n\t\tst::giveawayGiftCodeTableMargin);\n\tif (current.from) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_link_label_from(),\n\t\t\tshow,\n\t\t\tcurrent.from);\n\t}\n\tif (current.from && current.to) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_link_label_to(),\n\t\t\tshow,\n\t\t\tcurrent.to);\n\t} else if (current.from) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_link_label_to(),\n\t\t\ttr::lng_gift_link_label_to_unclaimed(tr::marked));\n\t}\n\tAddTableRow(\n\t\ttable,\n\t\ttr::lng_gift_link_label_gift(),\n\t\ttr::lng_gift_link_gift_premium(\n\t\t\tlt_duration,\n\t\t\tGiftDurationValue(current.days) | rpl::map(tr::marked),\n\t\t\ttr::marked));\n\tif (!skipReason && current.from) {\n\t\tconst auto reason = AddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_link_label_reason(),\n\t\t\t(current.giveawayId\n\t\t\t\t? ((current.to\n\t\t\t\t\t? tr::lng_gift_link_reason_giveaway\n\t\t\t\t\t: tr::lng_gift_link_reason_unclaimed)(tr::link))\n\t\t\t\t: current.giveaway\n\t\t\t\t? ((current.to\n\t\t\t\t\t? tr::lng_gift_link_reason_giveaway\n\t\t\t\t\t: tr::lng_gift_link_reason_unclaimed)(\n\t\t\t\t\t\ttr::marked\n\t\t\t\t\t) | rpl::type_erased)\n\t\t\t\t: tr::lng_gift_link_reason_chosen(tr::marked)));\n\t\treason->setClickHandlerFilter([=](const auto &...) {\n\t\t\tif (const auto window = show->resolveWindow()) {\n\t\t\t\twindow->showPeerHistory(\n\t\t\t\t\tcurrent.from,\n\t\t\t\t\tWindow::SectionShow::Way::Forward,\n\t\t\t\t\tcurrent.giveawayId);\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t}\n\tif (current.date) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_link_label_date(),\n\t\t\trpl::single(tr::marked(\n\t\t\t\tlangDateTime(base::unixtime::parse(current.date)))));\n\t}\n}\n\nvoid ShareWithFriend(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tconst QString &slug) {\n\tconst auto chosen = [=](not_null<Data::Thread*> thread) {\n\t\tconst auto content = navigation->parentController()->content();\n\t\treturn content->shareUrl(\n\t\t\tthread,\n\t\t\tMakeGiftCodeLink(&navigation->session(), slug).link,\n\t\t\tQString());\n\t};\n\tWindow::ShowChooseRecipientBox(navigation, chosen);\n}\n\nvoid ShowAlreadyPremiumToast(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tconst QString &slug,\n\t\tTimeId date) {\n\tconst auto instance = std::make_shared<\n\t\tbase::weak_ptr<Ui::Toast::Instance>\n\t>();\n\tconst auto shareLink = [=](\n\t\t\tconst ClickHandlerPtr &,\n\t\t\tQt::MouseButton button) {\n\t\tif (button == Qt::LeftButton) {\n\t\t\tif (const auto strong = instance->get()) {\n\t\t\t\tstrong->hideAnimated();\n\t\t\t}\n\t\t\tShareWithFriend(navigation, slug);\n\t\t}\n\t\treturn false;\n\t};\n\t*instance = navigation->showToast({\n\t\t.title = tr::lng_gift_link_already_title(tr::now),\n\t\t.text = tr::lng_gift_link_already_about(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\ttr::bold(langDateTime(base::unixtime::parse(date))),\n\t\t\tlt_link,\n\t\t\ttr::link(\n\t\t\t\ttr::bold(tr::lng_gift_link_already_link(tr::now))),\n\t\t\ttr::marked),\n\t\t.filter = crl::guard(navigation, shareLink),\n\t\t.duration = 6 * crl::time(1000),\n\t});\n}\n\n} // namespace\n\nrpl::producer<QString> GiftDurationValue(int days) {\n\treturn GiftDurationPhrase(days)(\n\t\tlt_count,\n\t\trpl::single(float64((days < 30)\n\t\t\t? days\n\t\t\t: (days < 30 * 12)\n\t\t\t? (days / 30)\n\t\t\t: (days / (30 * 12)))));\n}\n\nQString GiftDuration(int days) {\n\treturn GiftDurationPhrase(days)(tr::now, lt_count, (days < 30)\n\t\t? days\n\t\t: (days < 30 * 12)\n\t\t? (days / 30)\n\t\t: (days / (30 * 12)));\n}\n\nvoid GiftCodeBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionNavigation*> controller,\n\t\tconst QString &slug) {\n\tstruct State {\n\t\trpl::variable<Api::GiftCode> data;\n\t\trpl::variable<bool> used;\n\t\tbool sent = false;\n\t};\n\tconst auto session = &controller->session();\n\tconst auto state = box->lifetime().make_state<State>(State{});\n\tstate->data = session->api().premium().giftCodeValue(slug);\n\tstate->used = state->data.value(\n\t) | rpl::map([=](const Api::GiftCode &data) {\n\t\treturn data.used != 0;\n\t});\n\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setStyle(st::giveawayGiftCodeBox);\n\tbox->setNoContentMargin(true);\n\n\tconst auto bar = box->setPinnedToTopContent(\n\t\tobject_ptr<Ui::Premium::TopBar>(\n\t\t\tbox,\n\t\t\tst::giveawayGiftCodeCover,\n\t\t\tUi::Premium::TopBarDescriptor{\n\t\t\t\t.clickContextOther = nullptr,\n\t\t\t\t.title = rpl::conditional(\n\t\t\t\t\tstate->used.value(),\n\t\t\t\t\ttr::lng_gift_link_used_title(),\n\t\t\t\t\ttr::lng_gift_link_title()),\n\t\t\t\t.about = rpl::conditional(\n\t\t\t\t\tstate->used.value(),\n\t\t\t\t\ttr::lng_gift_link_used_about(tr::rich),\n\t\t\t\t\ttr::lng_gift_link_about(tr::rich)),\n\t\t\t\t.light = true,\n\t\t\t}));\n\n\tconst auto max = st::giveawayGiftCodeTopHeight;\n\tbar->setMaximumHeight(max);\n\tbar->setMinimumHeight(st::infoLayerTopBarHeight);\n\n\tbar->resize(bar->width(), bar->maximumHeight());\n\n\tconst auto link = MakeGiftCodeLink(&controller->session(), slug);\n\tbox->addRow(\n\t\tUi::MakeLinkLabel(\n\t\t\tbox,\n\t\t\trpl::single(link.text),\n\t\t\trpl::single(link.link),\n\t\t\tbox->uiShow(),\n\t\t\tMakeLinkCopyIcon(box)),\n\t\tst::giveawayGiftCodeLinkMargin);\n\n\tconst auto show = controller->uiShow();\n\tAddTable(box->verticalLayout(), show, {}, state->data.current(), false);\n\n\tauto shareLink = tr::lng_gift_link_also_send_link(\n\t) | rpl::map([](const QString &text) {\n\t\treturn tr::link(text);\n\t});\n\tauto richDate = [](const Api::GiftCode &data) {\n\t\treturn TextWithEntities{\n\t\t\tlangDateTime(base::unixtime::parse(data.used)),\n\t\t};\n\t};\n\tconst auto footer = box->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\trpl::conditional(\n\t\t\t\tstate->used.value(),\n\t\t\t\ttr::lng_gift_link_used_footer(\n\t\t\t\t\tlt_date,\n\t\t\t\t\tstate->data.value() | rpl::map(richDate),\n\t\t\t\t\ttr::marked),\n\t\t\t\ttr::lng_gift_link_also_send(\n\t\t\t\t\tlt_link,\n\t\t\t\t\tstd::move(shareLink),\n\t\t\t\t\ttr::marked)),\n\t\t\tst::giveawayGiftCodeFooter),\n\t\tst::giveawayGiftCodeFooterMargin,\n\t\tstyle::al_top);\n\tfooter->setClickHandlerFilter([=](const auto &...) {\n\t\tShareWithFriend(controller, slug);\n\t\treturn false;\n\t});\n\n\tconst auto close = Ui::CreateChild<Ui::IconButton>(\n\t\tbox.get(),\n\t\tst::boxTitleClose);\n\tclose->setClickedCallback([=] {\n\t\tbox->closeBox();\n\t});\n\tbox->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tclose->moveToRight(0, 0);\n\t}, box->lifetime());\n\n\tbox->addButton(rpl::conditional(\n\t\tstate->used.value(),\n\t\ttr::lng_box_ok(),\n\t\ttr::lng_gift_link_use()\n\t), [=] {\n\t\tif (state->used.current()) {\n\t\t\tbox->closeBox();\n\t\t} else if (!state->sent) {\n\t\t\tstate->sent = true;\n\t\t\tconst auto done = crl::guard(box, [=](const QString &error) {\n\t\t\t\tconst auto activePrefix = u\"PREMIUM_SUB_ACTIVE_UNTIL_\"_q;\n\t\t\t\tif (error.isEmpty()) {\n\t\t\t\t\tauto copy = state->data.current();\n\t\t\t\t\tcopy.used = base::unixtime::now();\n\t\t\t\t\tstate->data = std::move(copy);\n\n\t\t\t\t\tUi::StartFireworks(box->parentWidget());\n\t\t\t\t} else if (error.startsWith(activePrefix)) {\n\t\t\t\t\tconst auto date = error.mid(activePrefix.size()).toInt();\n\t\t\t\t\tShowAlreadyPremiumToast(controller, slug, date);\n\t\t\t\t\tstate->sent = false;\n\t\t\t\t} else {\n\t\t\t\t\tbox->uiShow()->showToast(error);\n\t\t\t\t\tstate->sent = false;\n\t\t\t\t}\n\t\t\t});\n\t\t\tcontroller->session().api().premium().applyGiftCode(slug, done);\n\t\t}\n\t});\n}\n\nvoid GiftCodePendingBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionNavigation*> controller,\n\t\tconst Api::GiftCode &data) {\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setStyle(st::giveawayGiftCodeBox);\n\tbox->setNoContentMargin(true);\n\n\t{\n\t\tconst auto peerTo = controller->session().data().peer(data.to);\n\t\tconst auto clickContext = [=, weak = base::make_weak(controller)] {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->uiShow()->showBox(\n\t\t\t\t\tPrepareShortInfoBox(peerTo, strong));\n\t\t\t}\n\t\t\treturn QVariant();\n\t\t};\n\t\tconst auto &st = st::giveawayGiftCodeCover;\n\t\tconst auto resultToName = st.about.style.font->elided(\n\t\t\tpeerTo->shortName(),\n\t\t\tst.about.minWidth / 2,\n\t\t\tQt::ElideMiddle);\n\t\tconst auto bar = box->setPinnedToTopContent(\n\t\t\tobject_ptr<Ui::Premium::TopBar>(\n\t\t\t\tbox,\n\t\t\t\tst,\n\t\t\t\tUi::Premium::TopBarDescriptor{\n\t\t\t\t\t.clickContextOther = clickContext,\n\t\t\t\t\t.title = tr::lng_gift_link_title(),\n\t\t\t\t\t.about = tr::lng_gift_link_pending_about(\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\trpl::single(tr::link(resultToName)),\n\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t.light = true,\n\t\t\t\t}));\n\n\t\tconst auto max = st::giveawayGiftCodeTopHeight;\n\t\tbar->setMaximumHeight(max);\n\t\tbar->setMinimumHeight(st::infoLayerTopBarHeight);\n\n\t\tbar->resize(bar->width(), bar->maximumHeight());\n\t}\n\n\t{\n\t\tconst auto linkLabel = box->addRow(\n\t\t\tUi::MakeLinkLabel(box, nullptr, nullptr, nullptr, nullptr),\n\t\t\tst::giveawayGiftCodeLinkMargin);\n\t\tconst auto spoiler = Ui::CreateChild<Ui::AbstractButton>(linkLabel);\n\t\tspoiler->lifetime().make_state<Ui::Animations::Basic>([=] {\n\t\t\tspoiler->update();\n\t\t})->start();\n\t\tlinkLabel->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\tspoiler->setGeometry(Rect(s));\n\t\t}, spoiler->lifetime());\n\t\tconst auto spoilerCached = Ui::SpoilerMessCached(\n\t\t\tUi::DefaultTextSpoilerMask(),\n\t\t\tst::giveawayGiftCodeLink.textFg->c);\n\t\tconst auto textHeight = st::giveawayGiftCodeLink.style.font->height;\n\t\tspoiler->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = QPainter(spoiler);\n\t\t\tconst auto rect = spoiler->rect();\n\t\t\tconst auto r = rect\n\t\t\t\t- QMargins(\n\t\t\t\t\tst::boxRowPadding.left(),\n\t\t\t\t\t(rect.height() - textHeight) / 2,\n\t\t\t\t\tst::boxRowPadding.right(),\n\t\t\t\t\t(rect.height() - textHeight) / 2);\n\t\t\tUi::FillSpoilerRect(p, r, spoilerCached.frame());\n\t\t}, spoiler->lifetime());\n\t\tspoiler->setClickedCallback([show = box->uiShow()] {\n\t\t\tshow->showToast(tr::lng_gift_link_pending_toast(tr::now));\n\t\t});\n\t\tspoiler->show();\n\t}\n\n\tconst auto show = controller->uiShow();\n\tAddTable(box->verticalLayout(), show, {}, data, true);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_gift_link_pending_footer(),\n\t\t\tst::giveawayGiftCodeFooter),\n\t\tst::giveawayGiftCodeFooterMargin,\n\t\tstyle::al_top);\n\n\tconst auto close = Ui::CreateChild<Ui::IconButton>(\n\t\tbox.get(),\n\t\tst::boxTitleClose);\n\tconst auto closeCallback = [=] { box->closeBox(); };\n\tclose->setClickedCallback(closeCallback);\n\tbox->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tclose->moveToRight(0, 0);\n\t}, box->lifetime());\n\n\tbox->addButton(tr::lng_close(), closeCallback);\n}\n\nvoid ResolveGiftCode(\n\t\tnot_null<Window::SessionNavigation*> controller,\n\t\tconst QString &slug,\n\t\tPeerId fromId,\n\t\tPeerId toId) {\n\tconst auto done = [=](Api::GiftCode code) {\n\t\tconst auto session = &controller->session();\n\t\tconst auto selfId = session->userPeerId();\n\t\tif (!code) {\n\t\t\tcontroller->showToast(tr::lng_gift_link_expired(tr::now));\n\t\t} else if (!code.from && fromId == selfId) {\n\t\t\tcode.from = fromId;\n\t\t\tcode.to = toId;\n\t\t\tconst auto self = (fromId == selfId);\n\t\t\tconst auto peer = session->data().peer(self ? toId : fromId);\n\t\t\tconst auto days = code.days;\n\t\t\tconst auto parent = controller->parentController();\n\t\t\tSettings::ShowGiftPremium(parent, peer, days, self);\n\t\t} else {\n\t\t\tcontroller->uiShow()->showBox(Box(GiftCodeBox, controller, slug));\n\t\t}\n\t};\n\tcontroller->session().api().premium().checkGiftCode(\n\t\tslug,\n\t\tcrl::guard(controller, done));\n}\n\nvoid GiveawayInfoBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionNavigation*> controller,\n\t\tstd::optional<Data::GiveawayStart> start,\n\t\tstd::optional<Data::GiveawayResults> results,\n\t\tApi::GiveawayInfo info) {\n\tExpects(start || results);\n\n\tusing State = Api::GiveawayState;\n\tconst auto finished = (info.state == State::Finished)\n\t\t|| (info.state == State::Refunded);\n\n\tbox->setTitle((finished\n\t\t? tr::lng_prizes_end_title\n\t\t: tr::lng_prizes_how_title)());\n\n\tconst auto first = results\n\t\t? results->channel->name()\n\t\t: !start->channels.empty()\n\t\t? start->channels.front()->name()\n\t\t: u\"channel\"_q;\n\n\tauto resultText = (!info.giftCode.isEmpty())\n\t\t? tr::lng_prizes_you_won(\n\t\t\tlt_cup,\n\t\t\trpl::single(\n\t\t\t\tTextWithEntities{ QString::fromUtf8(\"\\xf0\\x9f\\x8f\\x86\") }),\n\t\t\ttr::marked)\n\t\t: (info.credits)\n\t\t? tr::lng_prizes_you_won_credits(\n\t\t\tlt_amount,\n\t\t\ttr::lng_prizes_you_won_credits_amount(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(info.credits)),\n\t\t\t\ttr::bold),\n\t\t\tlt_cup,\n\t\t\trpl::single(\n\t\t\t\tTextWithEntities{ QString::fromUtf8(\"\\xf0\\x9f\\x8f\\x86\") }),\n\t\t\ttr::marked)\n\t\t: (info.state == State::Finished)\n\t\t? tr::lng_prizes_you_didnt(tr::marked)\n\t\t: (rpl::producer<TextWithEntities>)(nullptr);\n\n\tif (resultText) {\n\t\tconst auto &st = st::changePhoneDescription;\n\t\tconst auto skip = st.style.font->height * 0.5;\n\t\tauto label = object_ptr<Ui::FlatLabel>(\n\t\t\tbox.get(),\n\t\t\tstd::move(resultText),\n\t\t\tst);\n\t\tif ((!info.giftCode.isEmpty()) || info.credits) {\n\t\t\tlabel->setTextColorOverride(st::windowActiveTextFg->c);\n\t\t}\n\t\tconst auto result = box->addRow(\n\t\t\tobject_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\t\t\tbox.get(),\n\t\t\t\tstd::move(label),\n\t\t\t\tQMargins(0, skip, 0, skip)),\n\t\t\tstyle::al_justify);\n\t\tresult->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(result);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(st::boxDividerBg);\n\t\t\tp.drawRoundedRect(result->rect(), st::boxRadius, st::boxRadius);\n\t\t}, result->lifetime());\n\t\tUi::AddSkip(box->verticalLayout());\n\t}\n\n\tauto text = TextWithEntities();\n\n\tconst auto quantity = start\n\t\t? start->quantity\n\t\t: (results->winnersCount + results->unclaimedCount);\n\tconst auto months = start ? start->months : results->months;\n\tconst auto group = results\n\t\t? results->channel->isMegagroup()\n\t\t: (!start->channels.empty()\n\t\t\t&& start->channels.front()->isMegagroup());\n\tconst auto credits = start\n\t\t? start->credits\n\t\t: (results ? results->credits : 0);\n\ttext.append((finished\n\t\t? tr::lng_prizes_end_text\n\t\t: tr::lng_prizes_how_text)(\n\t\t\ttr::now,\n\t\t\tlt_admins,\n\t\t\tcredits\n\t\t\t\t? (group\n\t\t\t\t\t? tr::lng_prizes_credits_admins_group\n\t\t\t\t\t: tr::lng_prizes_credits_admins)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_channel,\n\t\t\t\t\t\ttr::bold(first),\n\t\t\t\t\t\tlt_amount,\n\t\t\t\t\t\ttr::lng_prizes_credits_admins_amount(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\t\tfloat64(credits),\n\t\t\t\t\t\t\ttr::bold),\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t: (group\n\t\t\t\t\t? tr::lng_prizes_admins_group\n\t\t\t\t\t: tr::lng_prizes_admins)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tquantity,\n\t\t\t\t\t\tlt_channel,\n\t\t\t\t\t\ttr::bold(first),\n\t\t\t\t\t\tlt_duration,\n\t\t\t\t\t\tTextWithEntities{ GiftDuration(months * 30) },\n\t\t\t\t\t\ttr::rich),\n\t\t\ttr::rich));\n\tconst auto many = start\n\t\t? (start->channels.size() > 1)\n\t\t: (results->additionalPeersCount > 0);\n\tconst auto count = info.winnersCount\n\t\t? info.winnersCount\n\t\t: quantity;\n\tconst auto all = start ? start->all : results->all;\n\tauto winners = all\n\t\t? (many\n\t\t\t? (group\n\t\t\t\t? tr::lng_prizes_winners_all_of_many_group\n\t\t\t\t: tr::lng_prizes_winners_all_of_many)\n\t\t\t: (group\n\t\t\t\t? tr::lng_prizes_winners_all_of_one_group\n\t\t\t\t: tr::lng_prizes_winners_all_of_one))(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tcount,\n\t\t\t\tlt_channel,\n\t\t\t\ttr::bold(first),\n\t\t\t\ttr::rich)\n\t\t: (many\n\t\t\t? tr::lng_prizes_winners_new_of_many\n\t\t\t: tr::lng_prizes_winners_new_of_one)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tcount,\n\t\t\t\tlt_channel,\n\t\t\t\ttr::bold(first),\n\t\t\t\tlt_start_date,\n\t\t\t\ttr::bold(\n\t\t\t\t\tlangDateTime(base::unixtime::parse(info.startDate))),\n\t\t\t\ttr::rich);\n\tconst auto additionalPrize = results\n\t\t? results->additionalPrize\n\t\t: start->additionalPrize;\n\tif (!additionalPrize.isEmpty()) {\n\t\ttext.append(\"\\n\\n\").append((group\n\t\t\t? tr::lng_prizes_additional_added_group\n\t\t\t: tr::lng_prizes_additional_added)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tcount,\n\t\t\t\tlt_channel,\n\t\t\t\ttr::bold(first),\n\t\t\t\tlt_prize,\n\t\t\t\tTextWithEntities{ additionalPrize },\n\t\t\t\ttr::rich));\n\t}\n\tconst auto untilDate = start\n\t\t? start->untilDate\n\t\t: results->untilDate;\n\ttext.append(\"\\n\\n\").append((finished\n\t\t? tr::lng_prizes_end_when_finish\n\t\t: tr::lng_prizes_how_when_finish)(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\ttr::bold(langDayOfMonthFull(\n\t\t\t\tbase::unixtime::parse(untilDate).date())),\n\t\t\tlt_winners,\n\t\t\twinners,\n\t\t\ttr::rich));\n\tif (info.activatedCount > 0) {\n\t\ttext.append(' ').append(tr::lng_prizes_end_activated(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tinfo.activatedCount,\n\t\t\ttr::rich));\n\t}\n\tif (!info.giftCode.isEmpty()\n\t\t|| info.state == State::Finished\n\t\t|| info.state == State::Preparing) {\n\t} else if (info.state != State::Refunded) {\n\t\tif (info.adminChannelId) {\n\t\t\tconst auto channel = controller->session().data().channel(\n\t\t\t\tinfo.adminChannelId);\n\t\t\ttext.append(\"\\n\\n\").append((channel->isMegagroup()\n\t\t\t\t? tr::lng_prizes_how_no_admin_group\n\t\t\t\t: tr::lng_prizes_how_no_admin)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_channel,\n\t\t\t\t\ttr::bold(channel->name()),\n\t\t\t\t\ttr::rich));\n\t\t} else if (info.tooEarlyDate) {\n\t\t\tconst auto channel = controller->session().data().channel(\n\t\t\t\tinfo.adminChannelId);\n\t\t\ttext.append(\"\\n\\n\").append((channel->isMegagroup()\n\t\t\t\t? tr::lng_prizes_how_no_joined_group\n\t\t\t\t: tr::lng_prizes_how_no_joined)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_date,\n\t\t\t\t\ttr::bold(\n\t\t\t\t\t\tlangDateTime(\n\t\t\t\t\t\t\tbase::unixtime::parse(info.tooEarlyDate))),\n\t\t\t\t\ttr::rich));\n\t\t} else if (!info.disallowedCountry.isEmpty()) {\n\t\t\ttext.append(\"\\n\\n\").append(tr::lng_prizes_how_no_country(\n\t\t\t\ttr::now,\n\t\t\t\ttr::rich));\n\t\t} else if (info.participating) {\n\t\t\ttext.append(\"\\n\\n\").append((many\n\t\t\t\t? tr::lng_prizes_how_yes_joined_many\n\t\t\t\t: tr::lng_prizes_how_yes_joined_one)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_channel,\n\t\t\t\t\ttr::bold(first),\n\t\t\t\t\ttr::rich));\n\t\t} else {\n\t\t\ttext.append(\"\\n\\n\").append((many\n\t\t\t\t? tr::lng_prizes_how_participate_many\n\t\t\t\t: tr::lng_prizes_how_participate_one)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_channel,\n\t\t\t\t\ttr::bold(first),\n\t\t\t\t\tlt_date,\n\t\t\t\t\ttr::bold(langDayOfMonthFull(\n\t\t\t\t\t\tbase::unixtime::parse(untilDate).date())),\n\t\t\t\t\ttr::rich));\n\t\t}\n\t}\n\tconst auto padding = st::boxPadding;\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox.get(),\n\t\t\trpl::single(std::move(text)),\n\t\t\tst::boxLabel),\n\t\t{ padding.left(), 0, padding.right(), padding.bottom() });\n\n\tif (info.state == State::Refunded) {\n\t\tconst auto wrap = box->addRow(\n\t\t\tobject_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\t\t\tbox.get(),\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tbox.get(),\n\t\t\t\t\t(group\n\t\t\t\t\t\t? tr::lng_prizes_cancelled_group()\n\t\t\t\t\t\t: tr::lng_prizes_cancelled()),\n\t\t\t\t\tst::giveawayRefundedLabel),\n\t\t\t\tst::giveawayRefundedPadding),\n\t\t\t{ padding.left(), 0, padding.right(), padding.bottom() },\n\t\t\tstyle::al_top);\n\t\tconst auto bg = wrap->lifetime().make_state<Ui::RoundRect>(\n\t\t\tst::boxRadius,\n\t\t\tst::attentionBoxButton.textBgOver);\n\t\twrap->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(wrap);\n\t\t\tbg->paint(p, wrap->rect());\n\t\t}, wrap->lifetime());\n\t}\n\tif (const auto slug = info.giftCode; !slug.isEmpty()) {\n\t\tbox->addButton(tr::lng_prizes_view_prize(), [=] {\n\t\t\tResolveGiftCode(controller, slug);\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t} else {\n\t\tbox->addButton(tr::lng_close(), [=] { box->closeBox(); });\n\t}\n}\n\nvoid ResolveGiveawayInfo(\n\t\tnot_null<Window::SessionNavigation*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId messageId,\n\t\tstd::optional<Data::GiveawayStart> start,\n\t\tstd::optional<Data::GiveawayResults> results) {\n\tconst auto show = [=](Api::GiveawayInfo info) {\n\t\tif (!info) {\n\t\t\tcontroller->showToast(\n\t\t\t\ttr::lng_confirm_phone_link_invalid(tr::now));\n\t\t} else {\n\t\t\tcontroller->uiShow()->showBox(\n\t\t\t\tBox(GiveawayInfoBox, controller, start, results, info));\n\t\t}\n\t};\n\tcontroller->session().api().premium().resolveGiveawayInfo(\n\t\tpeer,\n\t\tmessageId,\n\t\tcrl::guard(controller, show));\n}\n\nQString TonAddressUrl(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &address) {\n\tconst auto prefix = session->appConfig().get<QString>(\n\t\tu\"ton_blockchain_explorer_url\"_q,\n\t\tu\"https://tonviewer.com/\"_q);\n\treturn prefix + address;\n}\n\nstruct AddedUniqueDetails {\n\tobject_ptr<Ui::RpWidget> widget;\n\tnot_null<Ui::FlatLabel*> label;\n};\n[[nodiscard]] AddedUniqueDetails MakeUniqueDetails(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<TextWithEntities> text,\n\t\tSettings::CreditsEntryBoxStyleOverrides st,\n\t\tUi::Text::MarkedContext context,\n\t\tint removeCost,\n\t\tFn<void(Fn<void()> removed)> remove) {\n\tauto owned = object_ptr<Ui::FlatLabel>(\n\t\tparent,\n\t\trpl::duplicate(text),\n\t\t(st.tableValueMessage\n\t\t\t? *st.tableValueMessage\n\t\t\t: st::giveawayGiftMessage),\n\t\tst::defaultPopupMenu,\n\t\tcontext);\n\tconst auto label = owned.data();\n\tif (!remove) {\n\t\treturn {\n\t\t\t.widget = std::move(owned),\n\t\t\t.label = label,\n\t\t};\n\t}\n\towned.release();\n\n\tauto result = object_ptr<Ui::RpWidget>(parent);\n\tconst auto raw = result.data();\n\n\tlabel->setParent(raw);\n\tlabel->show();\n\tconst auto icon = Ui::CreateChild<Ui::IconButton>(\n\t\traw,\n\t\tst::giveawayGiftMessageRemove);\n\traw->widthValue() | rpl::on_next([=](int outer) {\n\t\tlabel->resizeToWidth(outer - icon->width());\n\t\tconst auto height = std::max(label->height(), icon->height());\n\n\t\ticon->moveToRight(0, (height - icon->height()) / 2);\n\t\tlabel->move(0, (height - label->height()) / 2);\n\n\t\traw->resize(outer, height);\n\t}, raw->lifetime());\n\ticon->setClickedCallback([=] {\n\t\tconst auto weak = base::make_weak(raw);\n\t\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tconst auto confirming = base::make_weak(box);\n\t\t\tconst auto confirmed = [=] {\n\t\t\t\tremove([=] {\n\t\t\t\t\tdelete weak.get();\n\t\t\t\t\tif (const auto strong = confirming.get()) {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t};\n\t\t\tUi::ConfirmBox(box, {\n\t\t\t\t.text = tr::lng_gift_unique_info_remove_text(),\n\t\t\t\t.confirmed = confirmed,\n\t\t\t\t.confirmText = tr::lng_gift_unique_info_remove_confirm(\n\t\t\t\t\tlt_cost,\n\t\t\t\t\trpl::single(\n\t\t\t\t\t\tUi::Text::IconEmoji(&st::starIconEmoji).append(\n\t\t\t\t\t\t\tLang::FormatCountDecimal(removeCost))),\n\t\t\t\t\ttr::rich),\n\t\t\t\t.title = tr::lng_gift_unique_info_remove_title(),\n\t\t\t});\n\t\t\tbox->addRow(\n\t\t\t\tobject_ptr<Ui::TableLayout>(\n\t\t\t\t\tbox,\n\t\t\t\t\tst.table ? *st.table : st::giveawayGiftCodeTable)\n\t\t\t)->addRow(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tparent,\n\t\t\t\t\trpl::duplicate(text),\n\t\t\t\t\t(st.tableValueMessage\n\t\t\t\t\t\t? *st.tableValueMessage\n\t\t\t\t\t\t: st::giveawayGiftMessage),\n\t\t\t\t\tst::defaultPopupMenu,\n\t\t\t\t\tcontext),\n\t\t\t\tnullptr,\n\t\t\t\tst::giveawayGiftCodeLabelMargin,\n\t\t\t\tst::giveawayGiftCodeValueMargin);\n\t\t}));\n\t});\n\n\treturn {\n\t\t.widget = std::move(result),\n\t\t.label = label,\n\t};\n}\n\nvoid AddStarGiftTable(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tSettings::CreditsEntryBoxStyleOverrides st,\n\t\tconst Data::CreditsHistoryEntry &entry,\n\t\tstd::shared_ptr<Data::GiftUpgradeSpinner> spinner,\n\t\tFn<void()> convertToStars,\n\t\tbool canStartUpgrade,\n\t\tFn<void(Fn<void()> removed)> removeDetails) {\n\tconst auto table = container->add(\n\t\tobject_ptr<Ui::TableLayout>(\n\t\t\tcontainer,\n\t\t\tst.table ? *st.table : st::giveawayGiftCodeTable),\n\t\tst::giveawayGiftCodeTableMargin);\n\tconst auto peerId = PeerId(entry.barePeerId);\n\tconst auto session = &show->session();\n\tconst auto unique = entry.uniqueGift.get();\n\tconst auto selfBareId = session->userPeerId().value;\n\tconst auto giftToSelf = (peerId == session->userPeerId())\n\t\t&& (entry.in || entry.bareGiftOwnerId == selfBareId);\n\tconst auto giftToChannel = entry.giftChannelSavedId\n\t\t&& peerIsChannel(PeerId(entry.bareEntryOwnerId));\n\n\tconst auto tooltip = std::make_shared<TableRowTooltipData>(\n\t\tTableRowTooltipData{ .parent = container });\n\tconst auto showTooltip = [=](\n\t\t\tnot_null<Ui::RpWidget*> widget,\n\t\t\trpl::producer<TextWithEntities> text) {\n\t\tShowTableRowTooltip(tooltip, widget, std::move(text), kTooltipDuration);\n\t};\n\n\tif (unique && entry.bareGiftResaleRecipientId) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_peer(),\n\t\t\tMakePeerTableValue(\n\t\t\t\ttable,\n\t\t\t\tshow,\n\t\t\t\tPeerId(entry.bareGiftResaleRecipientId)),\n\t\t\tst::giveawayGiftCodePeerMargin);\n\t} else if (unique && entry.bareGiftOwnerId) {\n\t\tconst auto ownerId = PeerId(entry.bareGiftOwnerId);\n\t\tconst auto was = std::make_shared<std::optional<CollectibleId>>();\n\t\tconst auto handleChange = [=](\n\t\t\t\tnot_null<Ui::RpWidget*> badge,\n\t\t\t\tEmojiStatusId emojiStatusId) {\n\t\t\tconst auto id = emojiStatusId.collectible\n\t\t\t\t? emojiStatusId.collectible->id\n\t\t\t\t: 0;\n\t\t\tconst auto show = [&](const auto &phrase) {\n\t\t\t\tshowTooltip(badge, phrase(\n\t\t\t\t\tlt_name,\n\t\t\t\t\trpl::single(tr::bold(UniqueGiftName(*unique))),\n\t\t\t\t\ttr::marked));\n\t\t\t};\n\t\t\tif (!*was || *was == id) {\n\t\t\t\t*was = id;\n\t\t\t\treturn;\n\t\t\t} else if (*was == unique->id) {\n\t\t\t\tshow(tr::lng_gift_wear_end_toast);\n\t\t\t} else if (id == unique->id) {\n\t\t\t\tshow(tr::lng_gift_wear_start_toast);\n\t\t\t}\n\t\t\t*was = id;\n\t\t};\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_unique_owner(),\n\t\t\tMakePeerWithStatusValue(table, show, ownerId, handleChange),\n\t\t\tst::giveawayGiftCodePeerMargin);\n\t} else if (unique) {\n\t\tif (!unique->ownerName.isEmpty()) {\n\t\t\tAddTableRow(\n\t\t\t\ttable,\n\t\t\t\ttr::lng_gift_unique_owner(),\n\t\t\t\trpl::single(TextWithEntities{ unique->ownerName }));\n\t\t} else if (auto address = unique->ownerAddress; !address.isEmpty()) {\n\t\t\tauto label = MakeMaybeMultilineTokenValue(table, address, st);\n\t\t\tlabel->setClickHandlerFilter([=](const auto &...) {\n\t\t\t\tTextUtilities::SetClipboardText(\n\t\t\t\t\tTextForMimeData::Simple(FixupTransactionId(address)));\n\t\t\t\tshow->showToast(\n\t\t\t\t\ttr::lng_gift_unique_address_copied(tr::now));\n\t\t\t\treturn false;\n\t\t\t});\n\t\t\tAddTableRow(\n\t\t\t\ttable,\n\t\t\t\ttr::lng_gift_unique_owner(),\n\t\t\t\tstd::move(label));\n\t\t}\n\n\t\tif (const auto hostId = PeerId(entry.bareGiftHostId)) {\n\t\t\tconst auto was = std::make_shared<std::optional<CollectibleId>>();\n\t\t\tconst auto handleChange = [=](\n\t\t\t\t\tnot_null<Ui::RpWidget*> badge,\n\t\t\t\t\tEmojiStatusId emojiStatusId) {\n\t\t\t\tconst auto id = emojiStatusId.collectible\n\t\t\t\t\t? emojiStatusId.collectible->id\n\t\t\t\t\t: 0;\n\t\t\t\tconst auto show = [&](const auto &phrase) {\n\t\t\t\t\tshowTooltip(badge, phrase(\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\trpl::single(tr::bold(UniqueGiftName(*unique))),\n\t\t\t\t\t\ttr::marked));\n\t\t\t\t};\n\t\t\t\tif (!*was || *was == id) {\n\t\t\t\t\t*was = id;\n\t\t\t\t\treturn;\n\t\t\t\t} else if (*was == unique->id) {\n\t\t\t\t\tshow(tr::lng_gift_wear_end_toast);\n\t\t\t\t} else if (id == unique->id) {\n\t\t\t\t\tshow(tr::lng_gift_wear_start_toast);\n\t\t\t\t}\n\t\t\t\t*was = id;\n\t\t\t};\n\t\t\tAddTableRow(\n\t\t\t\ttable,\n\t\t\t\ttr::lng_gift_unique_telegram(),\n\t\t\t\tMakePeerWithStatusValue(table, show, hostId, handleChange),\n\t\t\t\tst::giveawayGiftCodePeerMargin);\n\t\t}\n\n\t} else if (giftToChannel) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_peer_in(),\n\t\t\t(entry.bareActorId\n\t\t\t\t? MakePeerTableValue(table, show, PeerId(entry.bareActorId))\n\t\t\t\t: MakeHiddenPeerTableValue(table)),\n\t\t\tst::giveawayGiftCodePeerMargin);\n\t\tif (entry.bareEntryOwnerId) {\n\t\t\tAddTableRow(\n\t\t\t\ttable,\n\t\t\t\ttr::lng_credits_box_history_entry_peer(),\n\t\t\t\tMakePeerTableValue(\n\t\t\t\t\ttable,\n\t\t\t\t\tshow,\n\t\t\t\t\tPeerId(entry.bareEntryOwnerId)),\n\t\t\t\tst::giveawayGiftCodePeerMargin);\n\t\t}\n\t} else if (entry.auction && entry.bareGiftOwnerId) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_peer(),\n\t\t\tMakePeerTableValue(\n\t\t\t\ttable,\n\t\t\t\tshow,\n\t\t\t\tPeerId(entry.bareGiftOwnerId)),\n\t\t\tst::giveawayGiftCodePeerMargin);\n\t} else if (peerId && !giftToSelf) {\n\t\tconst auto user = session->data().peer(peerId)->asUser();\n\t\tconst auto withSendButton = entry.in && user && !user->isBot();\n\t\tauto send = withSendButton ? tr::lng_gift_send_small() : nullptr;\n\t\tauto handler = send ? Fn<void()>([=] {\n\t\t\tif (const auto window = show->resolveWindow()) {\n\t\t\t\tUi::ShowStarGiftBox(window, user);\n\t\t\t}\n\t\t}) : nullptr;\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_peer_in(),\n\t\t\tMakePeerTableValue(table, show, peerId, send, handler),\n\t\t\tst::giveawayGiftCodePeerMargin);\n\t} else if (!entry.soldOutInfo && !giftToSelf) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_peer_in(),\n\t\t\tMakeHiddenPeerTableValue(table),\n\t\t\tst::giveawayGiftCodePeerMargin);\n\t}\n\tif (!unique && !entry.firstSaleDate.isNull()) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_link_label_first_sale(),\n\t\t\trpl::single(tr::marked(\n\t\t\t\tlangDateTime(entry.firstSaleDate))));\n\t}\n\tif (!unique && !entry.lastSaleDate.isNull()) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_link_label_last_sale(),\n\t\t\trpl::single(tr::marked(\n\t\t\t\tlangDateTime(entry.lastSaleDate))));\n\t}\n\tif (!unique && !entry.date.isNull()) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_link_label_date(),\n\t\t\trpl::single(tr::marked(langDateTime(entry.date))));\n\t}\n\tif (unique) {\n\t\tconst auto shared = entry.uniqueGift;\n\t\tAddUniqueGiftPropertyRows(container, show, table, shared, spinner);\n\t} else {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_link_label_value(),\n\t\t\tMakeStarGiftStarsValue(\n\t\t\t\ttable,\n\t\t\t\tshow,\n\t\t\t\tentry,\n\t\t\t\tstd::move(convertToStars)));\n\t}\n\tif (entry.limitedCount > 0 && !entry.giftRefunded) {\n\t\tauto amount = rpl::single(TextWithEntities{\n\t\t\tLang::FormatCountDecimal(entry.limitedCount)\n\t\t});\n\t\tconst auto count = unique\n\t\t\t? (entry.limitedCount - entry.limitedLeft)\n\t\t\t: entry.limitedLeft;\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\t(unique\n\t\t\t\t? tr::lng_gift_unique_availability_label()\n\t\t\t\t: tr::lng_gift_availability()),\n\t\t\t((!unique && !count)\n\t\t\t\t? tr::lng_gift_availability_none(\n\t\t\t\t\tlt_amount,\n\t\t\t\t\tstd::move(amount),\n\t\t\t\t\ttr::marked)\n\t\t\t\t: (unique\n\t\t\t\t\t? tr::lng_gift_unique_availability\n\t\t\t\t\t: tr::lng_gift_availability_left)(\n\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\trpl::single(count * 1.),\n\t\t\t\t\t\tlt_amount,\n\t\t\t\t\t\tstd::move(amount),\n\t\t\t\t\t\ttr::marked)));\n\t}\n\tif (!unique && !entry.soldOutInfo && canStartUpgrade) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_unique_status(),\n\t\t\ttr::lng_gift_unique_status_non(tr::marked));\n\t}\n\tif (unique) {\n\t\tif (unique->value) {\n\t\t\tAddTableRow(\n\t\t\t\ttable,\n\t\t\t\ttr::lng_gift_unique_value(),\n\t\t\t\tMakeUniqueGiftValueValue(table, show, entry, st));\n\t\t}\n\t\tconst auto &original = unique->originalDetails;\n\t\tif (original.recipientId) {\n\t\t\tconst auto owner = &show->session().data();\n\t\t\tconst auto to = owner->peer(original.recipientId);\n\t\t\tconst auto from = original.senderId\n\t\t\t\t? owner->peer(original.senderId).get()\n\t\t\t\t: nullptr;\n\t\t\tconst auto date = base::unixtime::parse(original.date).date();\n\t\t\tconst auto dateText = TextWithEntities{ langDayOfMonth(date) };\n\t\t\tauto details = from\n\t\t\t\t? (original.message.empty()\n\t\t\t\t\t? tr::lng_gift_unique_info_sender(\n\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\trpl::single(tr::link(from->name(), 2)),\n\t\t\t\t\t\tlt_recipient,\n\t\t\t\t\t\trpl::single(tr::link(to->name(), 1)),\n\t\t\t\t\t\tlt_date,\n\t\t\t\t\t\trpl::single(dateText),\n\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t: tr::lng_gift_unique_info_sender_comment(\n\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\trpl::single(tr::link(from->name(), 2)),\n\t\t\t\t\t\tlt_recipient,\n\t\t\t\t\t\trpl::single(tr::link(to->name(), 1)),\n\t\t\t\t\t\tlt_date,\n\t\t\t\t\t\trpl::single(dateText),\n\t\t\t\t\t\tlt_text,\n\t\t\t\t\t\trpl::single(original.message),\n\t\t\t\t\t\ttr::marked))\n\t\t\t\t: (original.message.empty()\n\t\t\t\t\t? tr::lng_gift_unique_info_reciever(\n\t\t\t\t\t\tlt_recipient,\n\t\t\t\t\t\trpl::single(tr::link(to->name(), 1)),\n\t\t\t\t\t\tlt_date,\n\t\t\t\t\t\trpl::single(dateText),\n\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t: tr::lng_gift_unique_info_reciever_comment(\n\t\t\t\t\t\tlt_recipient,\n\t\t\t\t\t\trpl::single(tr::link(to->name(), 1)),\n\t\t\t\t\t\tlt_date,\n\t\t\t\t\t\trpl::single(dateText),\n\t\t\t\t\t\tlt_text,\n\t\t\t\t\t\trpl::single(original.message),\n\t\t\t\t\t\ttr::marked));\n\t\t\tconst auto tmp = std::make_shared<Ui::RpWidget*>(nullptr);\n\t\t\tauto made = MakeUniqueDetails(\n\t\t\t\tshow,\n\t\t\t\ttable,\n\t\t\t\tstd::move(details),\n\t\t\t\tst,\n\t\t\t\tCore::TextContext({ .session = session }),\n\t\t\t\tentry.starsForDetailsRemove,\n\t\t\t\tstd::move(removeDetails));\n\t\t\tconst auto showBoxLink = [=](not_null<PeerData*> peer) {\n\t\t\t\treturn std::make_shared<LambdaClickHandler>([=] {\n\t\t\t\t\tshow->showBox(PrepareShortInfoBox(peer, show));\n\t\t\t\t});\n\t\t\t};\n\t\t\tmade.label->setLink(1, showBoxLink(to));\n\t\t\tif (from) {\n\t\t\t\tmade.label->setLink(2, showBoxLink(from));\n\t\t\t}\n\t\t\tmade.label->setSelectable(true);\n\t\t\t*tmp = made.widget.data();\n\t\t\ttable->addRow(\n\t\t\t\tstd::move(made.widget),\n\t\t\t\tnullptr,\n\t\t\t\tst::giveawayGiftCodeLabelMargin,\n\t\t\t\tst::giveawayGiftCodeValueMargin);\n\t\t}\n\t} else if (!entry.description.empty()) {\n\t\tauto label = object_ptr<Ui::FlatLabel>(\n\t\t\ttable,\n\t\t\trpl::single(entry.description),\n\t\t\t(st.tableValueMessage\n\t\t\t\t? *st.tableValueMessage\n\t\t\t\t: st::giveawayGiftMessage),\n\t\t\tst::defaultPopupMenu,\n\t\t\tCore::TextContext({ .session = session }));\n\t\tlabel->setSelectable(true);\n\t\ttable->addRow(\n\t\t\tnullptr,\n\t\t\tstd::move(label),\n\t\t\tst::giveawayGiftCodeLabelMargin,\n\t\t\tst::giveawayGiftCodeValueMargin);\n\t}\n}\n\nvoid AddTransferGiftTable(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tstd::shared_ptr<Data::UniqueGift> unique) {\n\tconst auto table = container->add(\n\t\tobject_ptr<Ui::TableLayout>(\n\t\t\tcontainer,\n\t\t\tst::giveawayGiftCodeTable),\n\t\tst::giveawayGiftCodeTableMargin);\n\tAddUniqueGiftPropertyRows(container, show, table, unique, nullptr);\n\tif (const auto value = unique->value.get()) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_unique_value(),\n\t\t\trpl::single(\n\t\t\t\tFormatValuePrice(value->valuePrice, value->currency, true)));\n\t}\n}\n\nvoid AddCreditsHistoryEntryTable(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tSettings::CreditsEntryBoxStyleOverrides st,\n\t\tconst Data::CreditsHistoryEntry &entry) {\n\tif (!entry) {\n\t\treturn;\n\t}\n\tauto table = container->add(\n\t\tobject_ptr<Ui::TableLayout>(\n\t\t\tcontainer,\n\t\t\tst.table ? *st.table : st::giveawayGiftCodeTable),\n\t\tst::giveawayGiftCodeTableMargin);\n\tconst auto peerId = PeerId(entry.barePeerId);\n\tconst auto actorId = PeerId(entry.bareActorId);\n\tconst auto starrefRecipientId = PeerId(entry.starrefRecipientId);\n\tconst auto session = &show->session();\n\tif (entry.starrefCommission) {\n\t\tif (entry.giftResale && entry.starrefCommission < 1000) {\n\t\t\tconst auto full = int(base::SafeRound(entry.credits.value()\n\t\t\t\t/ (1. - (entry.starrefCommission / 1000.))));\n\t\t\tauto value = Ui::Text::IconEmoji(&st::starIconEmojiColored);\n\t\t\tconst auto starsText = Lang::FormatCreditsAmountDecimal(\n\t\t\t\tCreditsAmount{ full });\n\t\t\tAddTableRow(\n\t\t\t\ttable,\n\t\t\t\ttr::lng_credits_box_history_entry_gift_full_price(),\n\t\t\t\trpl::single(value.append(' ' + starsText)));\n\t\t} else if (entry.starrefAmount) {\n\t\t\tAddTableRow(\n\t\t\t\ttable,\n\t\t\t\ttr::lng_star_ref_commission_title(),\n\t\t\t\trpl::single(TextWithEntities{\n\t\t\t\t\tQString::number(entry.starrefCommission / 10.) + '%' }));\n\t\t} else {\n\t\t\tAddTableRow(\n\t\t\t\ttable,\n\t\t\t\ttr::lng_gift_link_label_reason(),\n\t\t\t\ttr::lng_credits_box_history_entry_reason_star_ref(\n\t\t\t\t\ttr::marked));\n\t\t}\n\t}\n\tif (starrefRecipientId && entry.starrefAmount && !entry.giftResale) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_affiliate(),\n\t\t\tshow,\n\t\t\tstarrefRecipientId);\n\t}\n\tif (peerId && entry.starrefCommission) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\t(entry.giftResale\n\t\t\t\t? tr::lng_credits_box_history_entry_gift_sold_to\n\t\t\t\t: entry.starrefAmount\n\t\t\t\t? tr::lng_credits_box_history_entry_referred\n\t\t\t\t: tr::lng_credits_box_history_entry_miniapp)(),\n\t\t\tshow,\n\t\t\tpeerId);\n\t}\n\tif (!entry.postsSearch\n\t\t&& (actorId || (!entry.starrefCommission && peerId))) {\n\t\tauto text = entry.starrefCommission\n\t\t\t? tr::lng_credits_box_history_entry_referred()\n\t\t\t: entry.in\n\t\t\t? tr::lng_credits_box_history_entry_peer_in()\n\t\t\t: entry.giftResale\n\t\t\t? tr::lng_credits_box_history_entry_gift_bought_from()\n\t\t\t: entry.giftUpgraded\n\t\t\t? tr::lng_credits_box_history_entry_gift_from()\n\t\t\t: tr::lng_credits_box_history_entry_peer();\n\t\tconst auto targetId = actorId ? actorId : peerId;\n\t\tconst auto isPeerDefault = !entry.starrefCommission\n\t\t\t&& !entry.in\n\t\t\t&& !entry.giftResale\n\t\t\t&& !entry.giftUpgraded;\n\t\tconst auto user = isPeerDefault\n\t\t\t? session->data().peer(targetId)->asUser()\n\t\t\t: nullptr;\n\t\tconst auto withSendButton = user\n\t\t\t&& !user->isInaccessible()\n\t\t\t&& !user->isBot();\n\t\tauto send = withSendButton ? tr::lng_gift_send_small() : nullptr;\n\t\tauto handler = send\n\t\t\t? Fn<void()>([=] {\n\t\t\t\tif (const auto window = show->resolveWindow()) {\n\t\t\t\t\tUi::ShowStarGiftBox(window, user);\n\t\t\t\t}\n\t\t\t})\n\t\t\t: nullptr;\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\tstd::move(text),\n\t\t\tMakePeerTableValue(table, show, targetId, send, handler),\n\t\t\tst::giveawayGiftCodePeerMargin);\n\t}\n\tif (const auto msgId = MsgId(peerId ? entry.bareMsgId : 0)) {\n\t\tconst auto peer = session->data().peer(peerId);\n\t\tif (const auto channel = peer->asBroadcast()) {\n\t\t\tconst auto link = CreateMessageLink(\n\t\t\t\tsession,\n\t\t\t\tpeerId,\n\t\t\t\tentry.bareMsgId);\n\t\t\tauto label = object_ptr<Ui::FlatLabel>(\n\t\t\t\ttable,\n\t\t\t\trpl::single(tr::link(link)),\n\t\t\t\ttable->st().defaultValue);\n\t\t\tlabel->setClickHandlerFilter([=](const auto &...) {\n\t\t\t\tif (const auto window = show->resolveWindow()) {\n\t\t\t\t\twindow->showPeerHistory(channel, {}, msgId);\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t});\n\t\t\tAddTableRow(\n\t\t\t\ttable,\n\t\t\t\t(entry.reaction\n\t\t\t\t\t? tr::lng_credits_box_history_entry_message\n\t\t\t\t\t: tr::lng_credits_box_history_entry_media)(),\n\t\t\t\tstd::move(label));\n\t\t}\n\t}\n\tusing Type = Data::CreditsHistoryEntry::PeerType;\n\tif (entry.peerType == Type::AppStore) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_via(),\n\t\t\ttr::lng_credits_box_history_entry_app_store(\n\t\t\t\ttr::rich));\n\t} else if (entry.peerType == Type::PlayMarket) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_via(),\n\t\t\ttr::lng_credits_box_history_entry_play_market(\n\t\t\t\ttr::rich));\n\t} else if (entry.peerType == Type::Fragment) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\t(entry.gift\n\t\t\t\t? tr::lng_credits_box_history_entry_peer_in\n\t\t\t\t: tr::lng_credits_box_history_entry_via)(),\n\t\t\t((entry.gift && entry.credits.stars())\n\t\t\t\t? tr::lng_credits_box_history_entry_anonymous\n\t\t\t\t: tr::lng_credits_box_history_entry_fragment)(\n\t\t\t\t\ttr::rich));\n\t} else if (entry.peerType == Type::Ads) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_via(),\n\t\t\ttr::lng_credits_box_history_entry_ads(tr::rich));\n\t} else if (entry.peerType == Type::PremiumBot) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_via(),\n\t\t\ttr::lng_credits_box_history_entry_via_premium_bot(\n\t\t\t\ttr::rich));\n\t}\n\tif (entry.bareGiveawayMsgId) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_link_label_to(),\n\t\t\tshow,\n\t\t\tshow->session().userId());\n\t}\n\tif (entry.bareGiveawayMsgId && entry.credits) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_link_label_gift(),\n\t\t\ttr::lng_gift_stars_title(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(entry.credits.value()),\n\t\t\t\ttr::rich));\n\t}\n\t{\n\t\tconst auto link = CreateMessageLink(\n\t\t\tsession,\n\t\t\tpeerId,\n\t\t\tentry.bareGiveawayMsgId);\n\t\tif (!link.isEmpty()) {\n\t\t\tAddTableRow(\n\t\t\t\ttable,\n\t\t\t\ttr::lng_gift_link_label_reason(),\n\t\t\t\ttr::lng_gift_link_reason_giveaway(\n\t\t\t\t) | rpl::map([link](const QString &text) {\n\t\t\t\t\treturn tr::link(text, link);\n\t\t\t\t}));\n\t\t}\n\t}\n\tif (!entry.subscriptionUntil.isNull() && !entry.title.isEmpty()) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_link_label_reason(),\n\t\t\ttr::lng_credits_box_history_entry_subscription(\n\t\t\t\ttr::marked));\n\t}\n\tif (entry.paidMessagesAmount) {\n\t\tauto value = Ui::Text::IconEmoji(&st::starIconEmojiColored);\n\t\tconst auto full = (entry.in ? 1 : -1)\n\t\t\t* (entry.credits + entry.paidMessagesAmount);\n\t\tconst auto starsText = Lang::FormatCreditsAmountDecimal(full);\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_paid_messages_full(),\n\t\t\trpl::single(value.append(' ' + starsText)));\n\t}\n\tif (const auto months = entry.premiumMonthsForStars) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_premium_gift_duration(),\n\t\t\ttr::lng_months(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(1. * months),\n\t\t\t\ttr::marked));\n\t}\n\tif (!entry.id.isEmpty()) {\n\t\tauto label = MakeMaybeMultilineTokenValue(table, entry.id, st);\n\t\tlabel->setClickHandlerFilter([=](const auto &...) {\n\t\t\tTextUtilities::SetClipboardText(\n\t\t\t\tTextForMimeData::Simple(FixupTransactionId(entry.id)));\n\t\t\tshow->showToast(\n\t\t\t\ttr::lng_credits_box_history_entry_id_copied(tr::now));\n\t\t\treturn false;\n\t\t});\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_id(),\n\t\t\tstd::move(label));\n\t}\n\tif (entry.floodSkip) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_floodskip_row(),\n\t\t\trpl::single(\n\t\t\t\ttr::marked(\n\t\t\t\t\tLang::FormatCountDecimal(entry.floodSkip))));\n\t}\n\tif (!entry.date.isNull()) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_link_label_date(),\n\t\t\trpl::single(tr::marked(langDateTime(entry.date))));\n\t}\n\tif (!entry.successDate.isNull()) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_success_date(),\n\t\t\trpl::single(tr::marked(langDateTime(entry.date))));\n\t}\n\tif (!entry.successLink.isEmpty()) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_success_url(),\n\t\t\trpl::single(\n\t\t\t\ttr::link(entry.successLink, entry.successLink)));\n\t}\n\tif (entry.limitedCount > 0 && entry.limitedLeft >= 0) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_availability(),\n\t\t\ttr::lng_gift_availability_left(\n\t\t\t\tlt_count_decimal,\n\t\t\t\trpl::single(entry.limitedLeft) | tr::to_count(),\n\t\t\t\tlt_amount,\n\t\t\t\trpl::single(TextWithEntities{\n\t\t\t\t\tLang::FormatCountDecimal(entry.limitedCount)\n\t\t\t\t}),\n\t\t\t\ttr::marked));\n\t}\n}\n\nvoid AddSubscriptionEntryTable(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tSettings::CreditsEntryBoxStyleOverrides st,\n\t\tconst Data::SubscriptionEntry &s) {\n\tif (!s) {\n\t\treturn;\n\t}\n\tauto table = container->add(\n\t\tobject_ptr<Ui::TableLayout>(\n\t\t\tcontainer,\n\t\t\tst.table ? *st.table : st::giveawayGiftCodeTable),\n\t\tst::giveawayGiftCodeTableMargin);\n\tconst auto peerId = PeerId(s.barePeerId);\n\tconst auto user = peerIsUser(peerId)\n\t\t? show->session().data().peer(peerId)->asUser()\n\t\t: nullptr;\n\tAddTableRow(\n\t\ttable,\n\t\t(!s.title.isEmpty() && user && user->botInfo)\n\t\t\t? tr::lng_credits_subscription_row_to_bot()\n\t\t\t: (!s.title.isEmpty() && user && !user->botInfo)\n\t\t\t? tr::lng_credits_subscription_row_to_business()\n\t\t\t: tr::lng_credits_subscription_row_to(),\n\t\tshow,\n\t\tpeerId);\n\tif (!s.title.isEmpty()) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_subscription_row_to(),\n\t\t\trpl::single(tr::marked(s.title)));\n\t}\n\tif (!s.until.isNull()) {\n\t\tif (s.subscription.period > 0) {\n\t\t\tconst auto subscribed = s.until.addSecs(-s.subscription.period);\n\t\t\tif (subscribed.isValid()) {\n\t\t\t\tAddTableRow(\n\t\t\t\t\ttable,\n\t\t\t\t\ttr::lng_group_invite_joined_row_date(),\n\t\t\t\t\trpl::single(\n\t\t\t\t\t\ttr::marked(langDateTime(subscribed))));\n\t\t\t}\n\t\t}\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ts.expired\n\t\t\t\t? tr::lng_credits_subscription_row_next_none()\n\t\t\t\t: s.cancelled\n\t\t\t\t? tr::lng_credits_subscription_row_next_off()\n\t\t\t\t: tr::lng_credits_subscription_row_next_on(),\n\t\t\trpl::single(tr::marked(langDateTime(s.until))));\n\t}\n}\n\nvoid AddSubscriberEntryTable(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tSettings::CreditsEntryBoxStyleOverrides st,\n\t\tnot_null<PeerData*> peer,\n\t\tTimeId date) {\n\tauto table = container->add(\n\t\tobject_ptr<Ui::TableLayout>(\n\t\t\tcontainer,\n\t\t\tst.table ? *st.table : st::giveawayGiftCodeTable),\n\t\tst::giveawayGiftCodeTableMargin);\n\tAddTableRow(\n\t\ttable,\n\t\ttr::lng_group_invite_joined_row_subscriber(),\n\t\tshow,\n\t\tpeer->id);\n\tif (const auto d = base::unixtime::parse(date); !d.isNull()) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_group_invite_joined_row_date(),\n\t\t\trpl::single(tr::marked(langDateTime(d))));\n\t}\n}\n\nvoid AddCreditsBoostTable(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tSettings::CreditsEntryBoxStyleOverrides st,\n\t\tconst Data::Boost &b) {\n\tauto table = container->add(\n\t\tobject_ptr<Ui::TableLayout>(\n\t\t\tcontainer,\n\t\t\tst.table ? *st.table : st::giveawayGiftCodeTable),\n\t\tst::giveawayGiftCodeTableMargin);\n\tconst auto peerId = b.giveawayMessage.peer;\n\tif (!peerId) {\n\t\treturn;\n\t}\n\tconst auto from = show->session().data().peer(peerId);\n\tAddTableRow(\n\t\ttable,\n\t\ttr::lng_credits_box_history_entry_peer_in(),\n\t\tshow,\n\t\tfrom->id);\n\tif (b.credits) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_link_label_gift(),\n\t\t\ttr::lng_gift_stars_title(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(b.credits)),\n\t\t\t\ttr::rich));\n\t}\n\t{\n\t\tconst auto link = CreateMessageLink(\n\t\t\t&show->session(),\n\t\t\tpeerId,\n\t\t\tb.giveawayMessage.msg.bare);\n\t\tif (!link.isEmpty()) {\n\t\t\tAddTableRow(\n\t\t\t\ttable,\n\t\t\t\ttr::lng_gift_link_label_reason(),\n\t\t\t\ttr::lng_gift_link_reason_giveaway(\n\t\t\t\t) | rpl::map([link](const QString &text) {\n\t\t\t\t\treturn tr::link(text, link);\n\t\t\t\t}));\n\t\t}\n\t}\n\tif (!b.date.isNull()) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_link_label_date(),\n\t\t\trpl::single(tr::marked(langDateTime(b.date))));\n\t}\n\tif (!b.expiresAt.isNull()) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_until(),\n\t\t\trpl::single(tr::marked(langDateTime(b.expiresAt))));\n\t}\n}\n\nvoid AddChannelEarnTable(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst Data::CreditsHistoryEntry &entry) {\n\tconst auto table = container->add(\n\t\tobject_ptr<Ui::TableLayout>(\n\t\t\tcontainer,\n\t\t\tst::giveawayGiftCodeTable),\n\t\tst::giveawayGiftCodeTableMargin);\n\tif (!entry.id.isEmpty()) {\n\t\tauto label = MakeMaybeMultilineTokenValue(table, entry.id, {});\n\t\tlabel->setClickHandlerFilter([=](const auto &...) {\n\t\t\tTextUtilities::SetClipboardText(\n\t\t\t\tTextForMimeData::Simple(FixupTransactionId(entry.id)));\n\t\t\tshow->showToast(\n\t\t\t\ttr::lng_credits_box_history_entry_id_copied(tr::now));\n\t\t\treturn false;\n\t\t});\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_id(),\n\t\t\tstd::move(label));\n\t}\n}\n\nvoid AddUniqueGiftValueTable(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tSettings::CreditsEntryBoxStyleOverrides st,\n\t\tconst Data::CreditsHistoryEntry &entry) {\n\tconst auto value = entry.uniqueGift ? entry.uniqueGift->value : nullptr;\n\tauto table = container->add(\n\t\tobject_ptr<Ui::TableLayout>(\n\t\t\tcontainer,\n\t\t\tst.table ? *st.table : st::giveawayGiftCodeTable),\n\t\tst::giveawayGiftCodeTableMargin);\n\tif (value->initialSaleDate) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_value_initial_sale(),\n\t\t\trpl::single(FormatValueDate(value->initialSaleDate)));\n\t}\n\tauto helper = Ui::Text::CustomEmojiHelper();\n\tauto starIcon = helper.paletteDependent(\n\t\tUi::Earn::IconCreditsEmoji());\n\tAddTableRow(\n\t\ttable,\n\t\ttr::lng_gift_value_initial_price(),\n\t\ttr::lng_gift_value_initial_price_value(\n\t\t\tlt_stars,\n\t\t\trpl::single(starIcon.append(' ').append(\n\t\t\t\tLang::FormatCreditsAmountDecimal(value->initialPriceStars)\n\t\t\t)),\n\t\t\tlt_amount,\n\t\t\trpl::single(FormatValuePrice(\n\t\t\t\tvalue->initialSalePrice,\n\t\t\t\tvalue->currency,\n\t\t\t\ttrue)),\n\t\t\ttr::marked),\n\t\thelper.context());\n\tif (value->lastSaleDate) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_value_last_sale(),\n\t\t\trpl::single(FormatValueDate(value->lastSaleDate)));\n\t}\n\tif (value->lastSalePrice) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_value_last_price(),\n\t\t\tMakePriceWithChangePercentValue(table, value));\n\t}\n\n\tconst auto tooltip = std::make_shared<TableRowTooltipData>(\n\t\tTableRowTooltipData{ .parent = container });\n\tif (value->minimumPrice) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_value_minimum_price(),\n\t\t\tMakeMinimumPriceValue(table, tooltip, entry.uniqueGift));\n\t}\n\tif (value->averagePrice) {\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_gift_vlaue_average_price(),\n\t\t\tMakeAveragePriceValue(table, tooltip, entry.uniqueGift));\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/gift_premium_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n\nnamespace Api {\nstruct GiftCode;\n} // namespace Api\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nstruct Boost;\nstruct CreditsHistoryEntry;\nstruct GiveawayStart;\nstruct GiveawayResults;\nstruct SubscriptionEntry;\nstruct UniqueGift;\nstruct GiftUpgradeSpinner;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Settings {\nstruct CreditsEntryBoxStyleOverrides;\n} // namespace Settings\n\nnamespace Ui {\nclass Show;\nclass GenericBox;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\n[[nodiscard]] rpl::producer<QString> GiftDurationValue(int days);\n[[nodiscard]] QString GiftDuration(int days);\n\nvoid GiftCodeBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionNavigation*> controller,\n\tconst QString &slug);\nvoid GiftCodePendingBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionNavigation*> controller,\n\tconst Api::GiftCode &data);\nvoid ResolveGiftCode(\n\tnot_null<Window::SessionNavigation*> controller,\n\tconst QString &slug,\n\tPeerId fromId = 0,\n\tPeerId toId = 0);\n\nvoid ResolveGiveawayInfo(\n\tnot_null<Window::SessionNavigation*> controller,\n\tnot_null<PeerData*> peer,\n\tMsgId messageId,\n\tstd::optional<Data::GiveawayStart> start,\n\tstd::optional<Data::GiveawayResults> results);\n\n[[nodiscard]] QString TonAddressUrl(\n\tnot_null<Main::Session*> session,\n\tconst QString &address);\n\nvoid AddStarGiftTable(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<Ui::VerticalLayout*> container,\n\tSettings::CreditsEntryBoxStyleOverrides st,\n\tconst Data::CreditsHistoryEntry &entry,\n\tstd::shared_ptr<Data::GiftUpgradeSpinner> spinner,\n\tFn<void()> convertToStars,\n\tbool canStartUpgrade,\n\tFn<void(Fn<void()> removed)> removeDetails);\nvoid AddTransferGiftTable(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<Ui::VerticalLayout*> container,\n\tstd::shared_ptr<Data::UniqueGift> unique);\nvoid AddCreditsHistoryEntryTable(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<Ui::VerticalLayout*> container,\n\tSettings::CreditsEntryBoxStyleOverrides st,\n\tconst Data::CreditsHistoryEntry &entry);\n\nvoid AddSubscriptionEntryTable(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<Ui::VerticalLayout*> container,\n\tSettings::CreditsEntryBoxStyleOverrides st,\n\tconst Data::SubscriptionEntry &s);\nvoid AddSubscriberEntryTable(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<Ui::VerticalLayout*> container,\n\tSettings::CreditsEntryBoxStyleOverrides st,\n\tnot_null<PeerData*> peer,\n\tTimeId date);\n\nvoid AddCreditsBoostTable(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<Ui::VerticalLayout*> container,\n\tSettings::CreditsEntryBoxStyleOverrides st,\n\tconst Data::Boost &boost);\n\nvoid AddChannelEarnTable(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<Ui::VerticalLayout*> container,\n\tconst Data::CreditsHistoryEntry &entry);\n\nvoid AddUniqueGiftValueTable(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<Ui::VerticalLayout*> container,\n\tSettings::CreditsEntryBoxStyleOverrides st,\n\tconst Data::CreditsHistoryEntry &entry);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/language_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/language_box.h\"\n\n#include \"data/data_peer_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/boxes/choose_language_box.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/multi_select.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/widgets/box_content_divider.h\"\n#include \"ui/text/text_entity.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/ui_utility.h\"\n#include \"storage/localstorage.h\"\n#include \"boxes/abstract_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"boxes/translate_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"core/application.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"lang/lang_instance.h\"\n#include \"lang/lang_cloud_manager.h\"\n#include \"platform/platform_translate_provider.h\"\n#include \"settings/settings_common.h\"\n#include \"spellcheck/spellcheck_types.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_passport.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QClipboard>\n\nnamespace {\n\nusing Language = Lang::Language;\nusing Languages = Lang::CloudManager::Languages;\n\nclass Rows : public Ui::RpWidget {\npublic:\n\tRows(\n\t\tQWidget *parent,\n\t\tconst Languages &data,\n\t\tconst QString &chosen,\n\t\tbool areOfficial);\n\n\tvoid filter(const QString &query);\n\n\tint count() const;\n\tint selected() const;\n\tvoid setSelected(int selected);\n\trpl::producer<bool> hasSelection() const;\n\trpl::producer<bool> isEmpty() const;\n\n\tvoid activateSelected();\n\trpl::producer<Language> activations() const;\n\tvoid changeChosen(const QString &chosen);\n\n\tUi::ScrollToRequest rowScrollRequest(int index) const;\n\n\tstatic int DefaultRowHeight();\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\nprivate:\n\tstruct Row {\n\t\tLanguage data;\n\t\tUi::Text::String title = { st::boxWideWidth / 2 };\n\t\tUi::Text::String description = { st::boxWideWidth / 2 };\n\t\tint top = 0;\n\t\tint height = 0;\n\t\tmutable std::unique_ptr<Ui::RippleAnimation> ripple;\n\t\tmutable std::unique_ptr<Ui::RippleAnimation> menuToggleRipple;\n\t\tbool menuToggleForceRippled = false;\n\t\tint titleHeight = 0;\n\t\tint descriptionHeight = 0;\n\t\tQStringList keywords;\n\t\tstd::unique_ptr<Ui::RadioView> check;\n\t\tbool removed = false;\n\t};\n\tstruct RowSelection {\n\t\tint index = 0;\n\n\t\tinline bool operator==(const RowSelection &other) const {\n\t\t\treturn (index == other.index);\n\t\t}\n\t};\n\tstruct MenuSelection {\n\t\tint index = 0;\n\n\t\tinline bool operator==(const MenuSelection &other) const {\n\t\t\treturn (index == other.index);\n\t\t}\n\t};\n\tusing Selection = std::variant<v::null_t, RowSelection, MenuSelection>;\n\n\tvoid updateSelected(Selection selected);\n\tvoid updatePressed(Selection pressed);\n\tRows::Row &rowByIndex(int index);\n\tconst Rows::Row &rowByIndex(int index) const;\n\tRows::Row &rowBySelection(Selection selected);\n\tconst Rows::Row &rowBySelection(Selection selected) const;\n\tstd::unique_ptr<Ui::RippleAnimation> &rippleBySelection(\n\t\tSelection selected);\n\t[[maybe_unused]] const std::unique_ptr<Ui::RippleAnimation> &rippleBySelection(\n\t\tSelection selected) const;\n\tstd::unique_ptr<Ui::RippleAnimation> &rippleBySelection(\n\t\tnot_null<Row*> row,\n\t\tSelection selected);\n\t[[maybe_unused]] const std::unique_ptr<Ui::RippleAnimation> &rippleBySelection(\n\t\tnot_null<const Row*> row,\n\t\tSelection selected) const;\n\tvoid addRipple(Selection selected, QPoint position);\n\tvoid ensureRippleBySelection(Selection selected);\n\tvoid ensureRippleBySelection(not_null<Row*> row, Selection selected);\n\tint indexFromSelection(Selection selected) const;\n\tint countAvailableWidth() const;\n\tint countAvailableWidth(int newWidth) const;\n\tQRect menuToggleArea() const;\n\tQRect menuToggleArea(not_null<const Row*> row) const;\n\tvoid repaint(Selection selected);\n\tvoid repaint(int index);\n\tvoid repaint(const Row &row);\n\tvoid repaintChecked(not_null<const Row*> row);\n\tvoid activateByIndex(int index);\n\n\tvoid showMenu(int index);\n\tvoid setForceRippled(not_null<Row*> row, bool rippled);\n\tbool canShare(not_null<const Row*> row) const;\n\tbool canRemove(not_null<const Row*> row) const;\n\tbool hasMenu(not_null<const Row*> row) const;\n\tvoid share(not_null<const Row*> row) const;\n\tvoid remove(not_null<Row*> row);\n\tvoid restore(not_null<Row*> row);\n\n\tstd::vector<Row> _rows;\n\tstd::vector<not_null<Row*>> _filtered;\n\tSelection _selected;\n\tSelection _pressed;\n\tQString _chosen;\n\tQStringList _query;\n\n\tbool _areOfficial = false;\n\tbool _mouseSelection = false;\n\tQPoint _globalMousePosition;\n\tbase::unique_qptr<Ui::DropdownMenu> _menu;\n\tint _menuShownIndex = -1;\n\tbool _menuOtherEntered = false;\n\n\trpl::event_stream<bool> _hasSelection;\n\trpl::event_stream<Language> _activations;\n\trpl::event_stream<bool> _isEmpty;\n\n};\n\nclass Content : public Ui::RpWidget {\npublic:\n\tContent(\n\t\tQWidget *parent,\n\t\tconst Languages &recent,\n\t\tconst Languages &official);\n\n\tUi::ScrollToRequest jump(int rows);\n\tvoid filter(const QString &query);\n\trpl::producer<Language> activations() const;\n\tvoid changeChosen(const QString &chosen);\n\tvoid activateBySubmit();\n\nprivate:\n\tvoid setupContent(\n\t\tconst Languages &recent,\n\t\tconst Languages &official);\n\n\tFn<Ui::ScrollToRequest(int rows)> _jump;\n\tFn<void(const QString &query)> _filter;\n\tFn<rpl::producer<Language>()> _activations;\n\tFn<void(const QString &chosen)> _changeChosen;\n\tFn<void()> _activateBySubmit;\n\n};\n\nstd::pair<Languages, Languages> PrepareLists() {\n\tconst auto projId = [](const Language &language) {\n\t\treturn language.id;\n\t};\n\tconst auto current = Lang::LanguageIdOrDefault(Lang::Id());\n\tauto official = Lang::CurrentCloudManager().languageList();\n\tauto recent = Local::readRecentLanguages();\n\tranges::stable_partition(recent, [&](const Language &language) {\n\t\treturn (language.id == current);\n\t});\n\tif (recent.empty() || recent.front().id != current) {\n\t\tif (ranges::find(official, current, projId) == end(official)) {\n\t\t\tconst auto generate = [&] {\n\t\t\t\tconst auto name = (current == \"#custom\")\n\t\t\t\t\t? \"Custom lang pack\"\n\t\t\t\t\t: Lang::GetInstance().name();\n\t\t\t\treturn Language{\n\t\t\t\t\tcurrent,\n\t\t\t\t\tQString(),\n\t\t\t\t\tQString(),\n\t\t\t\t\tname,\n\t\t\t\t\tLang::GetInstance().nativeName()\n\t\t\t\t};\n\t\t\t};\n\t\t\trecent.insert(begin(recent), generate());\n\t\t}\n\t}\n\tauto i = begin(official), e = end(official);\n\tconst auto remover = [&](const Language &language) {\n\t\tauto k = ranges::find(i, e, language.id, projId);\n\t\tif (k == e) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (; k != i; --k) {\n\t\t\tstd::swap(*k, *(k - 1));\n\t\t}\n\t\t++i;\n\t\treturn true;\n\t};\n\trecent.erase(ranges::remove_if(recent, remover), end(recent));\n\treturn { std::move(recent), std::move(official) };\n}\n\nRows::Rows(\n\tQWidget *parent,\n\tconst Languages &data,\n\tconst QString &chosen,\n\tbool areOfficial)\n: RpWidget(parent)\n, _chosen(chosen)\n, _areOfficial(areOfficial) {\n\tconst auto descriptionOptions = TextParseOptions{\n\t\tTextParseMultiline,\n\t\t0,\n\t\t0,\n\t\tQt::LayoutDirectionAuto\n\t};\n\t_rows.reserve(data.size());\n\tfor (const auto &item : data) {\n\t\t_rows.push_back(Row{ item });\n\t\tauto &row = _rows.back();\n\t\trow.check = std::make_unique<Ui::RadioView>(\n\t\t\tst::langsRadio,\n\t\t\t(row.data.id == _chosen),\n\t\t\t[=, row = &row] { repaint(*row); });\n\t\trow.title.setText(\n\t\t\tst::semiboldTextStyle,\n\t\t\titem.nativeName,\n\t\t\tUi::NameTextOptions());\n\t\trow.description.setText(\n\t\t\tst::defaultTextStyle,\n\t\t\titem.name,\n\t\t\tdescriptionOptions);\n\t\trow.keywords = TextUtilities::PrepareSearchWords(\n\t\t\titem.name + ' ' + item.nativeName);\n\t}\n\tresizeToWidth(width());\n\tsetAttribute(Qt::WA_MouseTracking);\n\tupdate();\n}\n\nvoid Rows::mouseMoveEvent(QMouseEvent *e) {\n\tconst auto position = e->globalPos();\n\tif (_menu) {\n\t\tconst auto rect = (_menuShownIndex >= 0)\n\t\t\t? menuToggleArea(&rowByIndex(_menuShownIndex))\n\t\t\t: QRect();\n\t\tif (rect.contains(e->pos())) {\n\t\t\tif (!_menuOtherEntered) {\n\t\t\t\t_menuOtherEntered = true;\n\t\t\t\t_menu->otherEnter();\n\t\t\t}\n\t\t} else {\n\t\t\tif (_menuOtherEntered) {\n\t\t\t\t_menuOtherEntered = false;\n\t\t\t\t_menu->otherLeave();\n\t\t\t}\n\t\t}\n\t}\n\tif (!_mouseSelection && position == _globalMousePosition) {\n\t\treturn;\n\t}\n\t_mouseSelection = true;\n\t_globalMousePosition = position;\n\tconst auto index = [&] {\n\t\tconst auto y = e->pos().y();\n\t\tif (y < 0) {\n\t\t\treturn -1;\n\t\t}\n\t\tfor (auto i = 0, till = count(); i != till; ++i) {\n\t\t\tconst auto &row = rowByIndex(i);\n\t\t\tif (row.top + row.height > y) {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t}();\n\tconst auto row = (index >= 0) ? &rowByIndex(index) : nullptr;\n\tconst auto inMenuToggle = (index >= 0 && hasMenu(row))\n\t\t? menuToggleArea(row).contains(e->pos())\n\t\t: false;\n\tif (index < 0) {\n\t\tupdateSelected({});\n\t} else if (inMenuToggle) {\n\t\tupdateSelected(MenuSelection{ index });\n\t} else if (!row->removed) {\n\t\tupdateSelected(RowSelection{ index });\n\t} else {\n\t\tupdateSelected({});\n\t}\n}\n\nvoid Rows::mousePressEvent(QMouseEvent *e) {\n\tupdatePressed(_selected);\n\tif (!v::is_null(_pressed)\n\t\t&& !rowBySelection(_pressed).menuToggleForceRippled) {\n\t\taddRipple(_pressed, e->pos());\n\t}\n}\n\nQRect Rows::menuToggleArea() const {\n\tconst auto size = st::topBarSearch.width;\n\tconst auto top = (DefaultRowHeight() - size) / 2;\n\tconst auto skip = st::boxScroll.width\n\t\t- st::boxScroll.deltax\n\t\t+ top;\n\tconst auto left = width() - skip - size;\n\treturn QRect(left, top, size, size);\n}\n\nQRect Rows::menuToggleArea(not_null<const Row*> row) const {\n\treturn menuToggleArea().translated(0, row->top);\n}\n\nvoid Rows::addRipple(Selection selected, QPoint position) {\n\tExpects(!v::is_null(selected));\n\n\tensureRippleBySelection(selected);\n\n\tconst auto menu = v::is<MenuSelection>(selected);\n\tconst auto &row = rowBySelection(selected);\n\tconst auto menuArea = menuToggleArea(&row);\n\tauto &ripple = rippleBySelection(&row, selected);\n\tconst auto topleft = menu ? menuArea.topLeft() : QPoint(0, row.top);\n\tripple->add(position - topleft);\n}\n\nvoid Rows::ensureRippleBySelection(Selection selected) {\n\tensureRippleBySelection(&rowBySelection(selected), selected);\n}\n\nvoid Rows::ensureRippleBySelection(not_null<Row*> row, Selection selected) {\n\tauto &ripple = rippleBySelection(row, selected);\n\tif (ripple) {\n\t\treturn;\n\t}\n\tconst auto menu = v::is<MenuSelection>(selected);\n\tconst auto menuArea = menuToggleArea(row);\n\tauto mask = menu\n\t\t? Ui::RippleAnimation::EllipseMask(menuArea.size())\n\t\t: Ui::RippleAnimation::RectMask({ width(), row->height });\n\tripple = std::make_unique<Ui::RippleAnimation>(\n\t\tst::defaultRippleAnimation,\n\t\tstd::move(mask),\n\t\t[=] { repaintChecked(row); });\n}\n\nvoid Rows::mouseReleaseEvent(QMouseEvent *e) {\n\tif (_menu && e->button() == Qt::LeftButton) {\n\t\tif (_menu->isHiding()) {\n\t\t\t_menu->otherEnter();\n\t\t} else {\n\t\t\t_menu->otherLeave();\n\t\t}\n\t}\n\tconst auto pressed = _pressed;\n\tupdatePressed({});\n\tif (pressed == _selected) {\n\t\tv::match(pressed, [&](RowSelection data) {\n\t\t\tactivateByIndex(data.index);\n\t\t}, [&](MenuSelection data) {\n\t\t\tshowMenu(data.index);\n\t\t}, [](v::null_t) {});\n\t}\n}\n\nbool Rows::canShare(not_null<const Row*> row) const {\n\treturn !_areOfficial && !row->data.id.startsWith('#');\n}\n\nbool Rows::canRemove(not_null<const Row*> row) const {\n\treturn !_areOfficial && !row->check->checked();\n}\n\nbool Rows::hasMenu(not_null<const Row*> row) const {\n\treturn canShare(row) || canRemove(row);\n}\n\nvoid Rows::share(not_null<const Row*> row) const {\n\tconst auto link = u\"https://t.me/setlanguage/\"_q + row->data.id;\n\tQGuiApplication::clipboard()->setText(link);\n\tUi::Toast::Show(tr::lng_username_copied(tr::now));\n}\n\nvoid Rows::remove(not_null<Row*> row) {\n\trow->removed = true;\n\tLocal::removeRecentLanguage(row->data.id);\n}\n\nvoid Rows::restore(not_null<Row*> row) {\n\trow->removed = false;\n\tLocal::saveRecentLanguages(ranges::views::all(\n\t\t_rows\n\t) | ranges::views::filter([](const Row &row) {\n\t\treturn !row.removed;\n\t}) | ranges::views::transform([](const Row &row) {\n\t\treturn row.data;\n\t}) | ranges::to_vector);\n}\n\nvoid Rows::showMenu(int index) {\n\tconst auto row = &rowByIndex(index);\n\tif (_menu || !hasMenu(row)) {\n\t\treturn;\n\t}\n\t_menu = base::make_unique_q<Ui::DropdownMenu>(\n\t\twindow(),\n\t\tst::dropdownMenuWithIcons);\n\tconst auto weak = _menu.get();\n\t_menu->setHiddenCallback([=] {\n\t\tweak->deleteLater();\n\t\tif (_menu == weak) {\n\t\t\tsetForceRippled(row, false);\n\t\t\t_menuShownIndex = -1;\n\t\t}\n\t});\n\t_menu->setShowStartCallback([=] {\n\t\tif (_menu == weak) {\n\t\t\tsetForceRippled(row, true);\n\t\t\t_menuShownIndex = index;\n\t\t}\n\t});\n\t_menu->setHideStartCallback([=] {\n\t\tif (_menu == weak) {\n\t\t\tsetForceRippled(row, false);\n\t\t\t_menuShownIndex = -1;\n\t\t}\n\t});\n\tconst auto addAction = [&](\n\t\t\tconst QString &text,\n\t\t\tFn<void()> callback,\n\t\t\tconst style::icon *icon) {\n\t\treturn _menu->addAction(text, std::move(callback), icon);\n\t};\n\tif (canShare(row)) {\n\t\taddAction(\n\t\t\ttr::lng_proxy_edit_share(tr::now),\n\t\t\t[=] { share(row); },\n\t\t\t&st::menuIconShare);\n\t}\n\tif (canRemove(row)) {\n\t\tif (row->removed) {\n\t\t\taddAction(tr::lng_proxy_menu_restore(tr::now), [=] {\n\t\t\t\trestore(row);\n\t\t\t}, &st::menuIconRestore);\n\t\t} else {\n\t\t\taddAction(tr::lng_proxy_menu_delete(tr::now), [=] {\n\t\t\t\tremove(row);\n\t\t\t}, &st::menuIconDelete);\n\t\t}\n\t}\n\tconst auto toggle = menuToggleArea(row);\n\tconst auto parentTopLeft = window()->mapToGlobal(QPoint());\n\tconst auto buttonTopLeft = mapToGlobal(toggle.topLeft());\n\tconst auto parent = QRect(parentTopLeft, window()->size());\n\tconst auto button = QRect(buttonTopLeft, toggle.size());\n\tconst auto bottom = button.y()\n\t\t+ st::proxyDropdownDownPosition.y()\n\t\t+ _menu->height()\n\t\t- parent.y();\n\tconst auto top = button.y()\n\t\t+ st::proxyDropdownUpPosition.y()\n\t\t- _menu->height()\n\t\t- parent.y();\n\t_menuShownIndex = index;\n\t_menuOtherEntered = true;\n\tif (bottom > parent.height() && top >= 0) {\n\t\tconst auto left = button.x()\n\t\t\t+ button.width()\n\t\t\t+ st::proxyDropdownUpPosition.x()\n\t\t\t- _menu->width()\n\t\t\t- parent.x();\n\t\t_menu->move(left, top);\n\t\t_menu->showAnimated(Ui::PanelAnimation::Origin::BottomRight);\n\t} else {\n\t\tconst auto left = button.x()\n\t\t\t+ button.width()\n\t\t\t+ st::proxyDropdownDownPosition.x()\n\t\t\t- _menu->width()\n\t\t\t- parent.x();\n\t\t_menu->move(left, bottom - _menu->height());\n\t\t_menu->showAnimated(Ui::PanelAnimation::Origin::TopRight);\n\t}\n}\n\nvoid Rows::setForceRippled(not_null<Row*> row, bool rippled) {\n\tif (row->menuToggleForceRippled != rippled) {\n\t\trow->menuToggleForceRippled = rippled;\n\t\tauto &ripple = rippleBySelection(row, MenuSelection{});\n\t\tif (row->menuToggleForceRippled) {\n\t\t\tensureRippleBySelection(row, MenuSelection{});\n\t\t\tif (ripple->empty()) {\n\t\t\t\tripple->addFading();\n\t\t\t} else {\n\t\t\t\tripple->lastUnstop();\n\t\t\t}\n\t\t} else {\n\t\t\tif (ripple) {\n\t\t\t\tripple->lastStop();\n\t\t\t}\n\t\t}\n\t}\n\trepaint(*row);\n}\n\nvoid Rows::activateByIndex(int index) {\n\t_activations.fire_copy(rowByIndex(index).data);\n}\n\nvoid Rows::leaveEventHook(QEvent *e) {\n\tupdateSelected({});\n\tif (_menu && _menuOtherEntered) {\n\t\t_menuOtherEntered = false;\n\t\t_menu->otherLeave();\n\t}\n}\n\nvoid Rows::filter(const QString &query) {\n\tupdateSelected({});\n\tupdatePressed({});\n\t_menu = nullptr;\n\t_menuShownIndex = -1;\n\n\t_query = TextUtilities::PrepareSearchWords(query);\n\n\tconst auto skip = [](\n\t\t\tconst QStringList &haystack,\n\t\t\tconst QStringList &needles) {\n\t\tconst auto find = [](\n\t\t\t\tconst QStringList &haystack,\n\t\t\t\tconst QString &needle) {\n\t\t\tfor (const auto &item : haystack) {\n\t\t\t\tif (item.startsWith(needle)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\t\tfor (const auto &needle : needles) {\n\t\t\tif (!find(haystack, needle)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\n\tif (!_query.isEmpty()) {\n\t\t_filtered.clear();\n\t\t_filtered.reserve(_rows.size());\n\t\tfor (auto &row : _rows) {\n\t\t\tif (!skip(row.keywords, _query)) {\n\t\t\t\t_filtered.push_back(&row);\n\t\t\t} else {\n\t\t\t\trow.ripple = nullptr;\n\t\t\t}\n\t\t}\n\t}\n\n\tresizeToWidth(width());\n\tUi::SendPendingMoveResizeEvents(this);\n\n\t_isEmpty.fire(count() == 0);\n}\n\nint Rows::count() const {\n\treturn _query.isEmpty() ? _rows.size() : _filtered.size();\n}\n\nint Rows::indexFromSelection(Selection selected) const {\n\treturn v::match(selected, [&](RowSelection data) {\n\t\treturn data.index;\n\t}, [&](MenuSelection data) {\n\t\treturn data.index;\n\t}, [](v::null_t) {\n\t\treturn -1;\n\t});\n}\n\nint Rows::selected() const {\n\treturn indexFromSelection(_selected);\n}\n\nvoid Rows::activateSelected() {\n\tconst auto index = selected();\n\tif (index >= 0) {\n\t\tactivateByIndex(index);\n\t}\n}\n\nrpl::producer<Language> Rows::activations() const {\n\treturn _activations.events();\n}\n\nvoid Rows::changeChosen(const QString &chosen) {\n\tfor (const auto &row : _rows) {\n\t\trow.check->setChecked(row.data.id == chosen, anim::type::normal);\n\t}\n}\n\nvoid Rows::setSelected(int selected) {\n\t_mouseSelection = false;\n\tconst auto limit = count();\n\tif (selected >= 0 && selected < limit) {\n\t\tupdateSelected(RowSelection{ selected });\n\t} else {\n\t\tupdateSelected({});\n\t}\n}\n\nrpl::producer<bool> Rows::hasSelection() const {\n\treturn _hasSelection.events();\n}\n\nrpl::producer<bool> Rows::isEmpty() const {\n\treturn _isEmpty.events_starting_with(\n\t\tcount() == 0\n\t) | rpl::distinct_until_changed();\n}\n\nvoid Rows::repaint(Selection selected) {\n\tv::match(selected, [](v::null_t) {\n\t}, [&](const auto &data) {\n\t\trepaint(data.index);\n\t});\n}\n\nvoid Rows::repaint(int index) {\n\tif (index >= 0) {\n\t\trepaint(rowByIndex(index));\n\t}\n}\n\nvoid Rows::repaint(const Row &row) {\n\tupdate(0, row.top, width(), row.height);\n}\n\nvoid Rows::repaintChecked(not_null<const Row*> row) {\n\tconst auto found = (ranges::find(_filtered, row) != end(_filtered));\n\tif (_query.isEmpty() || found) {\n\t\trepaint(*row);\n\t}\n}\n\nvoid Rows::updateSelected(Selection selected) {\n\tconst auto changed = (v::is_null(_selected) != v::is_null(selected));\n\trepaint(_selected);\n\t_selected = selected;\n\trepaint(_selected);\n\tif (changed) {\n\t\t_hasSelection.fire(!v::is_null(_selected));\n\t}\n}\n\nvoid Rows::updatePressed(Selection pressed) {\n\tif (!v::is_null(_pressed)) {\n\t\tif (!rowBySelection(_pressed).menuToggleForceRippled) {\n\t\t\tif (const auto ripple = rippleBySelection(_pressed).get()) {\n\t\t\t\tripple->lastStop();\n\t\t\t}\n\t\t}\n\t}\n\t_pressed = pressed;\n}\n\nRows::Row &Rows::rowByIndex(int index) {\n\tExpects(index >= 0 && index < count());\n\n\treturn _query.isEmpty() ? _rows[index] : *_filtered[index];\n}\n\nconst Rows::Row &Rows::rowByIndex(int index) const {\n\tExpects(index >= 0 && index < count());\n\n\treturn _query.isEmpty() ? _rows[index] : *_filtered[index];\n}\n\nRows::Row &Rows::rowBySelection(Selection selected) {\n\treturn rowByIndex(indexFromSelection(selected));\n}\n\nconst Rows::Row &Rows::rowBySelection(Selection selected) const {\n\treturn rowByIndex(indexFromSelection(selected));\n}\n\nstd::unique_ptr<Ui::RippleAnimation> &Rows::rippleBySelection(\n\t\tSelection selected) {\n\treturn rippleBySelection(&rowBySelection(selected), selected);\n}\n\nconst std::unique_ptr<Ui::RippleAnimation> &Rows::rippleBySelection(\n\t\tSelection selected) const {\n\treturn rippleBySelection(&rowBySelection(selected), selected);\n}\n\nstd::unique_ptr<Ui::RippleAnimation> &Rows::rippleBySelection(\n\t\tnot_null<Row*> row,\n\t\tSelection selected) {\n\treturn v::is<MenuSelection>(selected)\n\t\t? row->menuToggleRipple\n\t\t: row->ripple;\n}\n\nconst std::unique_ptr<Ui::RippleAnimation> &Rows::rippleBySelection(\n\t\tnot_null<const Row*> row,\n\t\tSelection selected) const {\n\treturn const_cast<Rows*>(this)->rippleBySelection(\n\t\tconst_cast<Row*>(row.get()),\n\t\tselected);\n}\n\nUi::ScrollToRequest Rows::rowScrollRequest(int index) const {\n\tconst auto &row = rowByIndex(index);\n\treturn Ui::ScrollToRequest(row.top, row.top + row.height);\n}\n\nint Rows::DefaultRowHeight() {\n\treturn st::passportRowPadding.top()\n\t\t+ st::semiboldFont->height\n\t\t+ st::passportRowSkip\n\t\t+ st::normalFont->height\n\t\t+ st::passportRowPadding.bottom();\n}\n\nint Rows::resizeGetHeight(int newWidth) {\n\tconst auto availableWidth = countAvailableWidth(newWidth);\n\tauto result = 0;\n\tfor (auto i = 0, till = count(); i != till; ++i) {\n\t\tauto &row = rowByIndex(i);\n\t\trow.top = result;\n\t\trow.titleHeight = row.title.countHeight(availableWidth);\n\t\trow.descriptionHeight = row.description.countHeight(availableWidth);\n\t\trow.height = st::passportRowPadding.top()\n\t\t\t+ row.titleHeight\n\t\t\t+ st::passportRowSkip\n\t\t\t+ row.descriptionHeight\n\t\t\t+ st::passportRowPadding.bottom();\n\t\tresult += row.height;\n\t}\n\treturn result;\n}\n\nint Rows::countAvailableWidth(int newWidth) const {\n\tconst auto right = width() - menuToggleArea().x();\n\treturn newWidth\n\t\t- st::passportRowPadding.left()\n\t\t- st::langsRadio.diameter\n\t\t- st::passportRowPadding.left()\n\t\t- right\n\t\t- st::passportRowIconSkip;\n}\n\nint Rows::countAvailableWidth() const {\n\treturn countAvailableWidth(width());\n}\n\nvoid Rows::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tconst auto clip = e->rect();\n\n\tconst auto checkLeft = st::passportRowPadding.left();\n\tconst auto left = checkLeft\n\t\t+ st::langsRadio.diameter\n\t\t+ st::passportRowPadding.left();\n\tconst auto availableWidth = countAvailableWidth();\n\tconst auto menu = menuToggleArea();\n\tconst auto selectedIndex = (_menuShownIndex >= 0)\n\t\t? _menuShownIndex\n\t\t: indexFromSelection(!v::is_null(_pressed) ? _pressed : _selected);\n\tfor (auto i = 0, till = count(); i != till; ++i) {\n\t\tconst auto &row = rowByIndex(i);\n\t\tif (row.top + row.height <= clip.y()) {\n\t\t\tcontinue;\n\t\t} else if (row.top >= clip.y() + clip.height()) {\n\t\t\tbreak;\n\t\t}\n\t\tp.setOpacity(row.removed ? st::stickersRowDisabledOpacity : 1.);\n\t\tp.translate(0, row.top);\n\t\tconst auto guard = gsl::finally([&] { p.translate(0, -row.top); });\n\n\t\tconst auto selected = (selectedIndex == i);\n\t\tif (selected && !row.removed) {\n\t\t\tp.fillRect(0, 0, width(), row.height, st::windowBgOver);\n\t\t}\n\n\t\tif (row.ripple) {\n\t\t\trow.ripple->paint(p, 0, 0, width());\n\t\t\tif (row.ripple->empty()) {\n\t\t\t\trow.ripple.reset();\n\t\t\t}\n\t\t}\n\n\t\tconst auto checkTop = (row.height - st::defaultRadio.diameter) / 2;\n\t\trow.check->paint(p, checkLeft, checkTop, width());\n\n\t\tauto top = st::passportRowPadding.top();\n\n\t\tp.setPen(st::passportRowTitleFg);\n\t\trow.title.drawLeft(p, left, top, availableWidth, width());\n\t\ttop += row.titleHeight + st::passportRowSkip;\n\n\t\tp.setPen(selected ? st::windowSubTextFgOver : st::windowSubTextFg);\n\t\trow.description.drawLeft(p, left, top, availableWidth, width());\n\t\ttop += row.descriptionHeight + st::passportRowPadding.bottom();\n\n\t\tif (hasMenu(&row)) {\n\t\t\tp.setOpacity(1.);\n\t\t\tif (selected && row.removed) {\n\t\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(st::windowBgOver);\n\t\t\t\tp.drawEllipse(menu);\n\t\t\t}\n\t\t\tif (row.menuToggleRipple) {\n\t\t\t\trow.menuToggleRipple->paint(p, menu.x(), menu.y(), width());\n\t\t\t\tif (row.menuToggleRipple->empty()) {\n\t\t\t\t\trow.menuToggleRipple.reset();\n\t\t\t\t}\n\t\t\t}\n\t\t\t(selected\n\t\t\t\t? st::topBarMenuToggle.iconOver\n\t\t\t\t: st::topBarMenuToggle.icon).paintInCenter(p, menu);\n\t\t}\n\t}\n}\n\nContent::Content(\n\tQWidget *parent,\n\tconst Languages &recent,\n\tconst Languages &official)\n: RpWidget(parent) {\n\tsetupContent(recent, official);\n}\n\nvoid Content::setupContent(\n\t\tconst Languages &recent,\n\t\tconst Languages &official) {\n\tusing namespace rpl::mappers;\n\n\tconst auto current = Lang::LanguageIdOrDefault(Lang::Id());\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tconst auto add = [&](const Languages &list, bool areOfficial) {\n\t\tif (list.empty()) {\n\t\t\treturn (Rows*)nullptr;\n\t\t}\n\t\tconst auto wrap = content->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontent,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(content)));\n\t\tconst auto inner = wrap->entity();\n\t\tinner->add(object_ptr<Ui::FixedHeightWidget>(\n\t\t\tinner,\n\t\t\tst::defaultBox.margin.top()));\n\t\tconst auto rows = inner->add(object_ptr<Rows>(\n\t\t\tinner,\n\t\t\tlist,\n\t\t\tcurrent,\n\t\t\tareOfficial));\n\t\tinner->add(object_ptr<Ui::FixedHeightWidget>(\n\t\t\tinner,\n\t\t\tst::defaultBox.margin.top()));\n\n\t\trows->isEmpty() | rpl::on_next([=](bool empty) {\n\t\t\twrap->toggle(!empty, anim::type::instant);\n\t\t}, rows->lifetime());\n\n\t\treturn rows;\n\t};\n\tconst auto main = add(recent, false);\n\tconst auto divider = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::BoxContentDivider>(content)));\n\tconst auto other = add(official, true);\n\tconst auto empty = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::FixedHeightWidget>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::FixedHeightWidget>(\n\t\t\t\tcontent,\n\t\t\t\tst::membersAbout.style.font->height * 9)));\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tempty->entity(),\n\t\ttr::lng_languages_none(),\n\t\tst::membersAbout);\n\tempty->entity()->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tlabel->move(\n\t\t\t(size.width() - label->width()) / 2,\n\t\t\t(size.height() - label->height()) / 2);\n\t}, label->lifetime());\n\n\tempty->toggleOn(\n\t\trpl::combine(\n\t\t\tmain ? main->isEmpty() : rpl::single(true),\n\t\t\tother ? other->isEmpty() : rpl::single(true),\n\t\t\t_1 && _2),\n\t\tanim::type::instant);\n\n\tUi::ResizeFitChild(this, content);\n\n\tif (main && other) {\n\t\trpl::combine(\n\t\t\tmain->isEmpty(),\n\t\t\tother->isEmpty(),\n\t\t\t_1 || _2\n\t\t) | rpl::on_next([=](bool empty) {\n\t\t\tdivider->toggle(!empty, anim::type::instant);\n\t\t}, divider->lifetime());\n\n\t\tconst auto excludeSelections = [](Rows *a, Rows *b) {\n\t\t\ta->hasSelection(\n\t\t\t) | rpl::filter(\n\t\t\t\t_1\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tb->setSelected(-1);\n\t\t\t}, a->lifetime());\n\t\t};\n\t\texcludeSelections(main, other);\n\t\texcludeSelections(other, main);\n\t} else {\n\t\tdivider->hide(anim::type::instant);\n\t}\n\n\tconst auto count = [](Rows *widget) {\n\t\treturn widget ? widget->count() : 0;\n\t};\n\tconst auto selected = [](Rows *widget) {\n\t\treturn widget ? widget->selected() : -1;\n\t};\n\tconst auto rowsCount = [=] {\n\t\treturn count(main) + count(other);\n\t};\n\tconst auto selectedIndex = [=] {\n\t\tif (const auto index = selected(main); index >= 0) {\n\t\t\treturn index;\n\t\t} else if (const auto index = selected(other); index >= 0) {\n\t\t\treturn count(main) + index;\n\t\t}\n\t\treturn -1;\n\t};\n\tconst auto setSelectedIndex = [=](int index) {\n\t\tconst auto first = count(main);\n\t\tif (index >= first) {\n\t\t\tif (main) {\n\t\t\t\tmain->setSelected(-1);\n\t\t\t}\n\t\t\tif (other) {\n\t\t\t\tother->setSelected(index - first);\n\t\t\t}\n\t\t} else {\n\t\t\tif (main) {\n\t\t\t\tmain->setSelected(index);\n\t\t\t}\n\t\t\tif (other) {\n\t\t\t\tother->setSelected(-1);\n\t\t\t}\n\t\t}\n\t};\n\tconst auto selectedCoords = [=] {\n\t\tconst auto coords = [=](Rows *rows, int index) {\n\t\t\tconst auto result = rows->rowScrollRequest(index);\n\t\t\tconst auto shift = rows->mapToGlobal({ 0, 0 }).y()\n\t\t\t\t- mapToGlobal({ 0, 0 }).y();\n\t\t\treturn Ui::ScrollToRequest(\n\t\t\t\tresult.ymin + shift,\n\t\t\t\tresult.ymax + shift);\n\t\t};\n\t\tif (const auto index = selected(main); index >= 0) {\n\t\t\treturn coords(main, index);\n\t\t} else if (const auto index = selected(other); index >= 0) {\n\t\t\treturn coords(other, index);\n\t\t}\n\t\treturn Ui::ScrollToRequest(-1, -1);\n\t};\n\t_jump = [=](int rows) {\n\t\tconst auto count = rowsCount();\n\t\tconst auto now = selectedIndex();\n\t\tif (now >= 0) {\n\t\t\tconst auto changed = now + rows;\n\t\t\tif (changed < 0) {\n\t\t\t\tsetSelectedIndex((now > 0) ? 0 : -1);\n\t\t\t} else if (changed >= count) {\n\t\t\t\tsetSelectedIndex(count - 1);\n\t\t\t} else {\n\t\t\t\tsetSelectedIndex(changed);\n\t\t\t}\n\t\t} else if (rows > 0) {\n\t\t\tsetSelectedIndex(0);\n\t\t}\n\t\treturn selectedCoords();\n\t};\n\tconst auto filter = [](Rows *widget, const QString &query) {\n\t\tif (widget) {\n\t\t\twidget->filter(query);\n\t\t}\n\t};\n\t_filter = [=](const QString &query) {\n\t\tfilter(main, query);\n\t\tfilter(other, query);\n\t};\n\t_activations = [=] {\n\t\tif (!main && !other) {\n\t\t\treturn rpl::never<Language>() | rpl::type_erased;\n\t\t} else if (!main) {\n\t\t\treturn other->activations();\n\t\t} else if (!other) {\n\t\t\treturn main->activations();\n\t\t}\n\t\treturn rpl::merge(\n\t\t\tmain->activations(),\n\t\t\tother->activations()\n\t\t) | rpl::type_erased;\n\t};\n\t_changeChosen = [=](const QString &chosen) {\n\t\tif (main) {\n\t\t\tmain->changeChosen(chosen);\n\t\t}\n\t\tif (other) {\n\t\t\tother->changeChosen(chosen);\n\t\t}\n\t};\n\t_activateBySubmit = [=] {\n\t\tif (selectedIndex() < 0) {\n\t\t\t_jump(1);\n\t\t}\n\t\tif (main) {\n\t\t\tmain->activateSelected();\n\t\t}\n\t\tif (other) {\n\t\t\tother->activateSelected();\n\t\t}\n\t};\n}\n\nvoid Content::filter(const QString &query) {\n\t_filter(query);\n}\n\nrpl::producer<Language> Content::activations() const {\n\treturn _activations();\n}\n\nvoid Content::changeChosen(const QString &chosen) {\n\t_changeChosen(chosen);\n}\n\nvoid Content::activateBySubmit() {\n\t_activateBySubmit();\n}\n\nUi::ScrollToRequest Content::jump(int rows) {\n\treturn _jump(rows);\n}\n\n} // namespace\n\nLanguageBox::LanguageBox(\n\tQWidget*,\n\tWindow::SessionController *controller,\n\tconst QString &highlightId)\n: _controller(controller)\n, _highlightId(highlightId) {\n}\n\nvoid LanguageBox::prepare() {\n\taddButton(tr::lng_box_ok(), [=] { closeBox(); });\n\n\tsetTitle(tr::lng_languages());\n\n\tconst auto topContainer = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tsetupTop(topContainer);\n\tconst auto select = topContainer->add(\n\t\tobject_ptr<Ui::MultiSelect>(\n\t\t\ttopContainer,\n\t\t\tst::defaultMultiSelect,\n\t\t\ttr::lng_participant_filter()));\n\ttopContainer->resizeToWidth(st::boxWidth);\n\n\tusing namespace rpl::mappers;\n\n\tconst auto &[recent, official] = PrepareLists();\n\tconst auto inner = setInnerWidget(\n\t\tobject_ptr<Content>(this, recent, official),\n\t\tst::boxScroll,\n\t\ttopContainer->height());\n\tinner->resizeToWidth(st::boxWidth);\n\n\tconst auto max = lifetime().make_state<int>(0);\n\trpl::combine(\n\t\tinner->heightValue(),\n\t\ttopContainer->heightValue(),\n\t\t_1 + _2\n\t) | rpl::on_next([=](int height) {\n\t\taccumulate_max(*max, height);\n\t\tsetDimensions(st::boxWidth, qMin(*max, st::boxMaxListHeight));\n\t}, inner->lifetime());\n\ttopContainer->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tsetInnerTopSkip(height);\n\t}, inner->lifetime());\n\n\tselect->setSubmittedCallback([=](Qt::KeyboardModifiers) {\n\t\tinner->activateBySubmit();\n\t});\n\tselect->setQueryChangedCallback([=](const QString &query) {\n\t\tinner->filter(query);\n\t});\n\tselect->setCancelledCallback([=] {\n\t\tselect->clearQuery();\n\t});\n\n\tinner->activations(\n\t) | rpl::on_next([=](const Language &language) {\n\t\t// \"#custom\" is applied each time it's passed to switchToLanguage().\n\t\t// So we check that the language really has changed.\n\t\tconst auto currentId = [] {\n\t\t\treturn Lang::LanguageIdOrDefault(Lang::Id());\n\t\t};\n\t\tif (language.id != currentId()) {\n\t\t\tLang::CurrentCloudManager().switchToLanguage(language);\n\t\t\tif (inner) {\n\t\t\t\tinner->changeChosen(currentId());\n\t\t\t}\n\t\t}\n\t}, inner->lifetime());\n\n\t_setInnerFocus = [=] {\n\t\tselect->setInnerFocus();\n\t};\n\t_jump = [=](int rows) {\n\t\treturn inner->jump(rows);\n\t};\n}\n\nvoid LanguageBox::showFinished() {\n\tif (_controller && !_highlightId.isEmpty()) {\n\t\tif (const auto window = Core::App().findWindow(this)) {\n\t\t\twindow->checkHighlightControl(\n\t\t\t\tu\"language/show-button\"_q,\n\t\t\t\t_showButtonToggle.data());\n\t\t\twindow->checkHighlightControl(\n\t\t\t\tu\"language/translate-chats\"_q,\n\t\t\t\t_translateChatsToggle.data());\n\t\t\twindow->checkHighlightControl(\n\t\t\t\tu\"language/do-not-translate\"_q,\n\t\t\t\t_doNotTranslateButton.data());\n\t\t}\n\t}\n}\n\nvoid LanguageBox::setupTop(not_null<Ui::VerticalLayout*> container) {\n\tif (!_controller) {\n\t\treturn;\n\t}\n\tconst auto translateEnabled = container->add(\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\tcontainer,\n\t\t\ttr::lng_translate_settings_show(),\n\t\t\tst::settingsButtonNoIcon))->toggleOn(\n\t\t\t\trpl::single(Core::App().settings().translateButtonEnabled()));\n\t_showButtonToggle = translateEnabled;\n\n\ttranslateEnabled->toggledValue(\n\t) | rpl::filter([](bool checked) {\n\t\treturn (checked != Core::App().settings().translateButtonEnabled());\n\t}) | rpl::on_next([=](bool checked) {\n\t\tCore::App().settings().setTranslateButtonEnabled(checked);\n\t\tCore::App().saveSettingsDelayed();\n\t}, translateEnabled->lifetime());\n\n\tif (Platform::IsTranslateProviderAvailable()) {\n\t\tconst auto platformTranslateWrap = container->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\t\tplatformTranslateWrap->toggle(\n\t\t\ttranslateEnabled->toggled(),\n\t\t\tanim::type::instant);\n\t\tplatformTranslateWrap->toggleOn(translateEnabled->toggledValue());\n\t\tconst auto platformTranslateEnabled = platformTranslateWrap->entity()->add(\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tplatformTranslateWrap->entity(),\n\t\t\t\tPlatform::IsMac()\n\t\t\t\t\t? tr::lng_translate_settings_use_platform_mac()\n\t\t\t\t\t: tr::lng_translate_settings_use_platform_linux(),\n\t\t\t\tst::settingsButtonNoIcon))->toggleOn(\n\t\t\t\t\trpl::single(\n\t\t\t\t\t\tCore::App().settings().usePlatformTranslation()));\n\t\tplatformTranslateEnabled->toggledValue(\n\t\t) | rpl::filter([](bool checked) {\n\t\t\treturn (checked\n\t\t\t\t!= Core::App().settings().usePlatformTranslation());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tCore::App().settings().setUsePlatformTranslation(checked);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}, platformTranslateEnabled->lifetime());\n\t\tif (Platform::IsMac()) {\n\t\t\tUi::AddSkip(platformTranslateWrap->entity());\n\t\t\tUi::AddDividerText(\n\t\t\t\tplatformTranslateWrap->entity(),\n\t\t\t\ttr::lng_translate_settings_use_platform_mac_about());\n\t\t}\n\t}\n\n\tusing namespace rpl::mappers;\n\tauto premium = Data::AmPremiumValue(&_controller->session());\n\tconst auto translateChat = container->add(object_ptr<Ui::SettingsButton>(\n\t\tcontainer,\n\t\ttr::lng_translate_settings_chat(),\n\t\tst::settingsButtonNoIconLocked\n\t))->toggleOn(rpl::merge(\n\t\trpl::combine(\n\t\t\tCore::App().settings().translateChatEnabledValue(),\n\t\t\trpl::duplicate(premium),\n\t\t\t_1 && _2),\n\t\t_translateChatTurnOff.events()));\n\t_translateChatsToggle = translateChat;\n\tstd::move(premium) | rpl::on_next([=](bool value) {\n\t\ttranslateChat->setToggleLocked(!value);\n\t}, translateChat->lifetime());\n\n\ttranslateChat->toggledValue(\n\t) | rpl::filter([=](bool checked) {\n\t\tconst auto premium = _controller->session().premium();\n\t\tif (checked && !premium) {\n\t\t\tShowPremiumPreviewToBuy(\n\t\t\t\t_controller,\n\t\t\t\tPremiumFeature::RealTimeTranslation);\n\t\t\t_translateChatTurnOff.fire(false);\n\t\t}\n\t\treturn premium\n\t\t\t&& (checked != Core::App().settings().translateChatEnabled());\n\t}) | rpl::on_next([=](bool checked) {\n\t\tCore::App().settings().setTranslateChatEnabled(checked);\n\t\tCore::App().saveSettingsDelayed();\n\t}, translateChat->lifetime());\n\n\tusing Languages = std::vector<LanguageId>;\n\tconst auto translateSkipWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\ttranslateSkipWrap->toggle(\n\t\ttranslateEnabled->toggled(),\n\t\tanim::type::normal);\n\ttranslateSkipWrap->toggleOn(rpl::combine(\n\t\ttranslateEnabled->toggledValue(),\n\t\ttranslateChat->toggledValue(),\n\t\trpl::mappers::_1 || rpl::mappers::_2));\n\tconst auto translateSkip = Settings::AddButtonWithLabel(\n\t\ttranslateSkipWrap->entity(),\n\t\ttr::lng_translate_settings_choose(),\n\t\tCore::App().settings().skipTranslationLanguagesValue(\n\t\t) | rpl::map([](const Languages &list) {\n\t\t\treturn (list.size() > 1)\n\t\t\t\t? tr::lng_languages_count(tr::now, lt_count, list.size())\n\t\t\t\t: Ui::LanguageName(list.front());\n\t\t}),\n\t\tst::settingsButtonNoIcon);\n\t_doNotTranslateButton = translateSkip;\n\n\ttranslateSkip->setClickedCallback([=] {\n\t\tuiShow()->showBox(Ui::EditSkipTranslationLanguages());\n\t});\n\tUi::AddSkip(container);\n\tUi::AddDividerText(container, tr::lng_translate_settings_about());\n}\n\nvoid LanguageBox::keyPressEvent(QKeyEvent *e) {\n\tconst auto key = e->key();\n\tif (key == Qt::Key_Escape) {\n\t\tcloseBox();\n\t\treturn;\n\t}\n\tconst auto selected = [&] {\n\t\tif (key == Qt::Key_Up) {\n\t\t\treturn _jump(-1);\n\t\t} else if (key == Qt::Key_Down) {\n\t\t\treturn _jump(1);\n\t\t} else if (key == Qt::Key_PageUp) {\n\t\t\treturn _jump(-rowsInPage());\n\t\t} else if (key == Qt::Key_PageDown) {\n\t\t\treturn _jump(rowsInPage());\n\t\t}\n\t\treturn Ui::ScrollToRequest(-1, -1);\n\t}();\n\tif (selected.ymin >= 0 && selected.ymax >= 0) {\n\t\tscrollToY(selected.ymin, selected.ymax);\n\t}\n}\n\nint LanguageBox::rowsInPage() const {\n\treturn std::max(height() / Rows::DefaultRowHeight(), 1);\n}\n\nvoid LanguageBox::setInnerFocus() {\n\t_setInnerFocus();\n}\n\nbase::binary_guard LanguageBox::Show(\n\t\tWindow::SessionController *controller,\n\t\tconst QString &highlightId) {\n\tauto result = base::binary_guard();\n\n\tauto &manager = Lang::CurrentCloudManager();\n\tif (manager.languageList().empty()) {\n\t\tconst auto weak = base::make_weak(controller);\n\t\tauto guard = std::make_shared<base::binary_guard>(\n\t\t\tresult.make_guard());\n\t\tauto lifetime = std::make_shared<rpl::lifetime>();\n\t\tmanager.languageListChanged(\n\t\t) | rpl::take(\n\t\t\t1\n\t\t) | rpl::on_next([=]() mutable {\n\t\t\tconst auto show = guard->alive();\n\t\t\tif (lifetime) {\n\t\t\t\tbase::take(lifetime)->destroy();\n\t\t\t}\n\t\t\tif (show) {\n\t\t\t\tUi::show(Box<LanguageBox>(weak.get(), highlightId));\n\t\t\t}\n\t\t}, *lifetime);\n\t} else {\n\t\tUi::show(Box<LanguageBox>(controller, highlightId));\n\t}\n\tmanager.requestLanguageList();\n\n\treturn result;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/language_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n#include \"base/binary_guard.h\"\n\nstruct LanguageId;\n\nnamespace Ui {\nclass MultiSelect;\nstruct ScrollToRequest;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nclass LanguageBox : public Ui::BoxContent {\npublic:\n\tLanguageBox(\n\t\tQWidget*,\n\t\tWindow::SessionController *controller,\n\t\tconst QString &highlightId = QString());\n\n\tvoid setInnerFocus() override;\n\n\t[[nodiscard]] static base::binary_guard Show(\n\t\tWindow::SessionController *controller,\n\t\tconst QString &highlightId = QString());\n\nprotected:\n\tvoid prepare() override;\n\tvoid showFinished() override;\n\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tvoid setupTop(not_null<Ui::VerticalLayout*> container);\n\t[[nodiscard]] int rowsInPage() const;\n\n\tWindow::SessionController *_controller = nullptr;\n\tQString _highlightId;\n\tQPointer<Ui::RpWidget> _showButtonToggle;\n\tQPointer<Ui::RpWidget> _translateChatsToggle;\n\tQPointer<Ui::RpWidget> _doNotTranslateButton;\n\trpl::event_stream<bool> _translateChatTurnOff;\n\tFn<void()> _setInnerFocus;\n\tFn<Ui::ScrollToRequest(int rows)> _jump;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/local_storage_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/local_storage_box.h\"\n\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/emoji_config.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/cache/storage_cache_database.h\"\n#include \"data/data_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace {\n\nconstexpr auto kMegabyte = int64(1024 * 1024);\nconstexpr auto kTotalSizeLimitsCount = 18;\nconstexpr auto kMediaSizeLimitsCount = 18;\nconstexpr auto kMinimalSizeLimit = 100 * kMegabyte;\nconstexpr auto kTimeLimitsCount = 16;\nconstexpr auto kMaxTimeLimitValue = std::numeric_limits<size_type>::max();\nconstexpr auto kFakeMediaCacheTag = uint16(0xFFFF);\n\nint64 TotalSizeLimitInMB(int index) {\n\tif (index < 8) {\n\t\treturn int64(index + 2) * 100;\n\t}\n\treturn int64(index - 7) * 1024;\n}\n\nint64 TotalSizeLimit(int index) {\n\treturn TotalSizeLimitInMB(index) * kMegabyte;\n}\n\nint64 MediaSizeLimitInMB(int index) {\n\tif (index < 9) {\n\t\treturn int64(index + 1) * 100;\n\t}\n\treturn int64(index - 8) * 1024;\n}\n\nint64 MediaSizeLimit(int index) {\n\treturn MediaSizeLimitInMB(index) * kMegabyte;\n}\n\nQString SizeLimitText(int64 limit) {\n\tconst auto mb = (limit / (1024 * 1024));\n\tconst auto gb = (mb / 1024);\n\treturn (gb > 0)\n\t\t? (QString::number(gb) + \" GB\")\n\t\t: (QString::number(mb) + \" MB\");\n}\n\nsize_type TimeLimitInDays(int index) {\n\tif (index < 3) {\n\t\tconst auto weeks = (index + 1);\n\t\treturn size_type(weeks) * 7;\n\t} else if (index < 15) {\n\t\tconst auto month = (index - 2);\n\t\treturn (size_type(month) * 30)\n\t\t\t+ ((month >= 12) ? 5 :\n\t\t\t\t(month >= 10) ? 4 :\n\t\t\t\t(month >= 8) ? 3 :\n\t\t\t\t(month >= 7) ? 2 :\n\t\t\t\t(month >= 5) ? 1 :\n\t\t\t\t(month >= 3) ? 0 :\n\t\t\t\t(month >= 2) ? -1 :\n\t\t\t\t(month >= 1) ? 1 : 0);\n\t\t\t//+ (month >= 1 ? 1 : 0)\n\t\t\t//- (month >= 2 ? 2 : 0)\n\t\t\t//+ (month >= 3 ? 1 : 0)\n\t\t\t//+ (month >= 5 ? 1 : 0)\n\t\t\t//+ (month >= 7 ? 1 : 0)\n\t\t\t//+ (month >= 8 ? 1 : 0)\n\t\t\t//+ (month >= 10 ? 1 : 0)\n\t\t\t//+ (month >= 12 ? 1 : 0);\n\t}\n\treturn 0;\n}\n\nsize_type TimeLimit(int index) {\n\tconst auto days = TimeLimitInDays(index);\n\treturn days\n\t\t? (days * 24 * 60 * 60)\n\t\t: kMaxTimeLimitValue;\n}\n\nQString TimeLimitText(size_type limit) {\n\tconst auto days = (limit / (24 * 60 * 60));\n\tconst auto weeks = (days / 7);\n\tconst auto months = (days / 29);\n\treturn (months > 0)\n\t\t? tr::lng_months(tr::now, lt_count, months)\n\t\t: (limit > 0)\n\t\t? tr::lng_weeks(tr::now, lt_count, weeks)\n\t\t: tr::lng_local_storage_limit_never(tr::now);\n}\n\nsize_type LimitToValue(size_type timeLimit) {\n\treturn timeLimit ? timeLimit : kMaxTimeLimitValue;\n}\n\nsize_type ValueToLimit(size_type timeLimit) {\n\treturn (timeLimit != kMaxTimeLimitValue) ? timeLimit : 0;\n}\n\n} // namespace\n\nclass LocalStorageBox::Row : public Ui::RpWidget {\npublic:\n\tRow(\n\t\tQWidget *parent,\n\t\tFn<QString(size_type)> title,\n\t\trpl::producer<QString> clear,\n\t\tconst Database::TaggedSummary &data);\n\n\tvoid update(const Database::TaggedSummary &data);\n\tvoid toggleProgress(bool shown);\n\n\trpl::producer<> clearRequests() const;\n\t[[nodiscard]] not_null<Ui::RoundButton*> clearButton() const;\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tQString titleText(const Database::TaggedSummary &data) const;\n\tQString sizeText(const Database::TaggedSummary &data) const;\n\tvoid radialAnimationCallback();\n\n\tFn<QString(size_type)> _titleFactory;\n\tobject_ptr<Ui::FlatLabel> _title;\n\tobject_ptr<Ui::FlatLabel> _description;\n\tobject_ptr<Ui::FlatLabel> _clearing = { nullptr };\n\tobject_ptr<Ui::RoundButton> _clear;\n\tstd::unique_ptr<Ui::InfiniteRadialAnimation> _progress;\n\n};\n\nLocalStorageBox::Row::Row(\n\tQWidget *parent,\n\tFn<QString(size_type)> title,\n\trpl::producer<QString> clear,\n\tconst Database::TaggedSummary &data)\n: RpWidget(parent)\n, _titleFactory(std::move(title))\n, _title(\n\tthis,\n\ttitleText(data),\n\tst::localStorageRowTitle)\n, _description(\n\tthis,\n\tsizeText(data),\n\tst::localStorageRowSize)\n, _clear(this, std::move(clear), st::localStorageClear) {\n\t_clear->setVisible(data.count != 0);\n}\n\nvoid LocalStorageBox::Row::update(const Database::TaggedSummary &data) {\n\tif (data.count != 0) {\n\t\t_title->setText(titleText(data));\n\t}\n\t_description->setText(sizeText(data));\n\t_clear->setVisible(data.count != 0);\n}\n\nvoid LocalStorageBox::Row::toggleProgress(bool shown) {\n\tif (!shown) {\n\t\t_progress = nullptr;\n\t\t_description->show();\n\t\t_clearing.destroy();\n\t} else if (!_progress) {\n\t\t_progress = std::make_unique<Ui::InfiniteRadialAnimation>(\n\t\t\t[=] { radialAnimationCallback(); },\n\t\t\tst::proxyCheckingAnimation);\n\t\t_progress->start();\n\t\t_clearing = object_ptr<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\ttr::lng_local_storage_clearing(tr::now),\n\t\t\tst::localStorageRowSize);\n\t\t_clearing->show();\n\t\t_description->hide();\n\t\tresizeToWidth(width());\n\t\tRpWidget::update();\n\t}\n}\n\nvoid LocalStorageBox::Row::radialAnimationCallback() {\n\tif (!anim::Disabled()) {\n\t\tRpWidget::update();\n\t}\n}\n\nrpl::producer<> LocalStorageBox::Row::clearRequests() const {\n\treturn _clear->clicks() | rpl::to_empty;\n}\n\nnot_null<Ui::RoundButton*> LocalStorageBox::Row::clearButton() const {\n\treturn _clear.data();\n}\n\nint LocalStorageBox::Row::resizeGetHeight(int newWidth) {\n\tconst auto height = st::localStorageRowHeight;\n\tconst auto padding = st::localStorageRowPadding;\n\tconst auto available = newWidth - padding.left() - padding.right();\n\t_title->resizeToWidth(available);\n\t_description->resizeToWidth(available);\n\t_title->moveToLeft(padding.left(), padding.top(), newWidth);\n\t_description->moveToLeft(\n\t\tpadding.left(),\n\t\theight - padding.bottom() - _description->height(),\n\t\tnewWidth);\n\tif (_clearing) {\n\t\tconst auto progressShift = st::proxyCheckingPosition.x()\n\t\t\t+ st::proxyCheckingAnimation.size.width()\n\t\t\t+ st::proxyCheckingSkip;\n\t\t_clearing->resizeToWidth(available - progressShift);\n\t\t_clearing->moveToLeft(\n\t\t\tpadding.left(),// + progressShift,\n\t\t\t_description->y(),\n\t\t\tnewWidth);\n\t}\n\t_clear->moveToRight(\n\t\tst::layerBox.buttonPadding.right(),\n\t\t(height - _clear->height()) / 2,\n\t\tnewWidth);\n\treturn height;\n}\n\nvoid LocalStorageBox::Row::paintEvent(QPaintEvent *e) {\n#if 0 // not used\n\tif (!_progress) {\n\t\treturn;\n\t}\n\tauto p = QPainter(this);\n\tconst auto padding = st::localStorageRowPadding;\n\tconst auto height = st::localStorageRowHeight;\n\tconst auto bottom = height - padding.bottom() - _description->height();\n\t_progress->draw(\n\t\tp,\n\t\t{\n\t\t\tst::proxyCheckingPosition.x() + padding.left(),\n\t\t\tst::proxyCheckingPosition.y() + bottom\n\t\t},\n\t\twidth());\n#endif\n}\n\nQString LocalStorageBox::Row::titleText(const Database::TaggedSummary &data) const {\n\treturn _titleFactory(data.count);\n}\n\nQString LocalStorageBox::Row::sizeText(const Database::TaggedSummary &data) const {\n\treturn data.totalSize\n\t\t? Ui::FormatSizeText(data.totalSize)\n\t\t: tr::lng_local_storage_empty(tr::now);\n}\n\nLocalStorageBox::LocalStorageBox(\n\tQWidget*,\n\tnot_null<Main::Session*> session,\n\tCreateTag)\n: _session(session)\n, _db(&session->data().cache())\n, _dbBig(&session->data().cacheBigFile()) {\n\tconst auto &settings = session->local().cacheSettings();\n\tconst auto &settingsBig = session->local().cacheBigFileSettings();\n\t_totalSizeLimit = settings.totalSizeLimit + settingsBig.totalSizeLimit;\n\t_mediaSizeLimit = settingsBig.totalSizeLimit;\n\t_timeLimit = settings.totalTimeLimit;\n}\n\nvoid LocalStorageBox::Show(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &highlightId) {\n\tauto shared = std::make_shared<object_ptr<LocalStorageBox>>(\n\t\tBox<LocalStorageBox>(&controller->session(), CreateTag()));\n\tconst auto weak = shared->data();\n\tweak->_highlightId = highlightId;\n\trpl::combine(\n\t\tcontroller->session().data().cache().statsOnMain(),\n\t\tcontroller->session().data().cacheBigFile().statsOnMain()\n\t) | rpl::on_next([=](\n\t\t\tDatabase::Stats &&stats,\n\t\t\tDatabase::Stats &&statsBig) {\n\t\tweak->update(std::move(stats), std::move(statsBig));\n\t\tif (auto &strong = *shared) {\n\t\t\tcontroller->uiShow()->show(std::move(strong));\n\t\t}\n\t}, weak->lifetime());\n}\n\nvoid LocalStorageBox::prepare() {\n\tsetTitle(tr::lng_local_storage_title());\n\n\taddButton(tr::lng_box_ok(), [this] { closeBox(); });\n\n\tsetupControls();\n}\n\nvoid LocalStorageBox::showFinished() {\n\tif (_highlightId.isEmpty()) {\n\t\treturn;\n\t}\n\tif (_highlightId == u\"storage/clear-cache\"_q) {\n\t\tif (const auto i = _rows.find(0); i != _rows.end()) {\n\t\t\tSettings::HighlightWidget(\n\t\t\t\ti->second->entity()->clearButton(),\n\t\t\t\t{ .rippleShape = true });\n\t\t}\n\t} else if (_highlightId == u\"storage/max-cache\"_q) {\n\t\tif (_totalSlider) {\n\t\t\tconst auto add = st::roundRadiusSmall;\n\t\t\tSettings::HighlightWidget(\n\t\t\t\t_totalSlider,\n\t\t\t\t{ .margin = { -add, -add, -add, -add }, .radius = add });\n\t\t}\n\t}\n}\n\nvoid LocalStorageBox::updateRow(\n\t\tnot_null<Ui::SlideWrap<Row>*> row,\n\t\tconst Database::TaggedSummary *data) {\n\tconst auto summary = (_rows.find(0)->second == row);\n\tconst auto shown = (data && data->count && data->totalSize) || summary;\n\tif (shown) {\n\t\trow->entity()->update(*data);\n\t}\n\trow->toggle(shown, anim::type::normal);\n}\n\nvoid LocalStorageBox::update(\n\t\tDatabase::Stats &&stats,\n\t\tDatabase::Stats &&statsBig) {\n\t_stats = std::move(stats);\n\t_statsBig = std::move(statsBig);\n\tif (const auto i = _rows.find(0); i != end(_rows)) {\n\t\ti->second->entity()->toggleProgress(\n\t\t\t_stats.clearing || _statsBig.clearing);\n\t}\n\tfor (const auto &entry : _rows) {\n\t\tif (entry.first == kFakeMediaCacheTag) {\n\t\t\tupdateRow(entry.second, &_statsBig.full);\n\t\t} else if (entry.first) {\n\t\t\tconst auto i = _stats.tagged.find(entry.first);\n\t\t\tupdateRow(\n\t\t\t\tentry.second,\n\t\t\t\t(i != end(_stats.tagged)) ? &i->second : nullptr);\n\t\t} else {\n\t\t\tconst auto full = summary();\n\t\t\tupdateRow(entry.second, &full);\n\t\t}\n\t}\n}\n\nauto LocalStorageBox::summary() const -> Database::TaggedSummary {\n\tauto result = _stats.full;\n\tresult.count += _statsBig.full.count;\n\tresult.totalSize += _statsBig.full.totalSize;\n\treturn result;\n}\n\nvoid LocalStorageBox::clearByTag(uint16 tag) {\n\tif (tag == kFakeMediaCacheTag) {\n\t\t_dbBig->clear();\n\t} else if (tag) {\n\t\t_db->clearByTag(tag);\n\t} else {\n\t\t_db->clear();\n\t\t_dbBig->clear();\n\t\tUi::Emoji::ClearIrrelevantCache();\n\t}\n}\n\nvoid LocalStorageBox::setupControls() {\n\tconst auto container = setInnerWidget(\n\t\tobject_ptr<Ui::VerticalLayout>(this),\n\t\tst::defaultMultiSelect.scroll);\n\tconst auto createRow = [&](\n\t\t\tuint16 tag,\n\t\t\tFn<QString(size_type)> title,\n\t\t\trpl::producer<QString> clear,\n\t\t\tconst Database::TaggedSummary &data) {\n\t\tauto result = container->add(object_ptr<Ui::SlideWrap<Row>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Row>(\n\t\t\t\tcontainer,\n\t\t\t\tstd::move(title),\n\t\t\t\tstd::move(clear),\n\t\t\t\tdata)));\n\t\tconst auto shown = (data.count && data.totalSize) || !tag;\n\t\tresult->toggle(shown, anim::type::instant);\n\t\tresult->entity()->clearRequests(\n\t\t) | rpl::on_next([=] {\n\t\t\tclearByTag(tag);\n\t\t}, result->lifetime());\n\t\t_rows.emplace(tag, result);\n\t\treturn result;\n\t};\n\tauto tracker = Ui::MultiSlideTracker();\n\tconst auto createTagRow = [&](uint8 tag, auto &&titleFactory) {\n\t\tstatic const auto empty = Database::TaggedSummary();\n\t\tconst auto i = _stats.tagged.find(tag);\n\t\tconst auto &data = (i != end(_stats.tagged)) ? i->second : empty;\n\t\tauto factory = std::forward<decltype(titleFactory)>(titleFactory);\n\t\tauto title = [factory = std::move(factory)](size_type count) {\n\t\t\treturn factory(tr::now, lt_count, count);\n\t\t};\n\t\ttracker.track(createRow(\n\t\t\ttag,\n\t\t\tstd::move(title),\n\t\t\ttr::lng_local_storage_clear_some(),\n\t\t\tdata));\n\t};\n\tauto summaryTitle = [](size_type) {\n\t\treturn tr::lng_local_storage_summary(tr::now);\n\t};\n\tauto mediaCacheTitle = [](size_type) {\n\t\treturn tr::lng_local_storage_media(tr::now);\n\t};\n\tcreateRow(\n\t\t0,\n\t\tstd::move(summaryTitle),\n\t\ttr::lng_local_storage_clear(),\n\t\tsummary());\n\tsetupLimits(container);\n\tconst auto shadow = container->add(object_ptr<Ui::SlideWrap<>>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::PlainShadow>(container),\n\t\tst::localStorageRowPadding));\n\tcreateTagRow(Data::kImageCacheTag, tr::lng_local_storage_image);\n\tcreateTagRow(Data::kStickerCacheTag, tr::lng_local_storage_sticker);\n\tcreateTagRow(Data::kVoiceMessageCacheTag, tr::lng_local_storage_voice);\n\tcreateTagRow(Data::kVideoMessageCacheTag, tr::lng_local_storage_round);\n\tcreateTagRow(Data::kAnimationCacheTag, tr::lng_local_storage_animation);\n\ttracker.track(createRow(\n\t\tkFakeMediaCacheTag,\n\t\tstd::move(mediaCacheTitle),\n\t\ttr::lng_local_storage_clear_some(),\n\t\t_statsBig.full));\n\tshadow->toggleOn(\n\t\tstd::move(tracker).atLeastOneShownValue()\n\t);\n\tcontainer->resizeToWidth(st::boxWidth);\n\tcontainer->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tsetDimensions(st::boxWidth, height);\n\t}, container->lifetime());\n}\n\ntemplate <\n\ttypename Value,\n\ttypename Convert,\n\ttypename Callback,\n\ttypename>\nnot_null<Ui::MediaSlider*> LocalStorageBox::createLimitsSlider(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tint valuesCount,\n\t\tConvert &&convert,\n\t\tValue currentValue,\n\t\tCallback &&callback) {\n\tconst auto label = container->add(\n\t\tobject_ptr<Ui::LabelSimple>(container, st::localStorageLimitLabel),\n\t\tst::localStorageLimitLabelMargin);\n\tcallback(label, currentValue);\n\tconst auto slider = container->add(\n\t\tobject_ptr<Ui::MediaSlider>(container, st::localStorageLimitSlider),\n\t\tst::localStorageLimitMargin);\n\tslider->resize(st::localStorageLimitSlider.seekSize);\n\tslider->setPseudoDiscrete(\n\t\tvaluesCount,\n\t\tstd::forward<Convert>(convert),\n\t\tcurrentValue,\n\t\t[=, callback = std::forward<Callback>(callback)](Value value) {\n\t\t\tcallback(label, value);\n\t\t});\n\treturn slider;\n}\n\nvoid LocalStorageBox::updateMediaLimit() {\n\tconst auto good = [&](int64 mediaLimit) {\n\t\treturn (_totalSizeLimit - mediaLimit >= kMinimalSizeLimit);\n\t};\n\tif (good(_mediaSizeLimit) || !_mediaSlider || !_mediaLabel) {\n\t\treturn;\n\t}\n\tauto index = 1;\n\twhile ((index < kMediaSizeLimitsCount)\n\t\t&& (MediaSizeLimit(index) * 2 <= _totalSizeLimit)) {\n\t\t++index;\n\t}\n\t--index;\n\t_mediaSizeLimit = MediaSizeLimit(index);\n\t_mediaSlider->setValue(index / float64(kMediaSizeLimitsCount - 1));\n\tupdateMediaLabel();\n\n\tEnsures(good(_mediaSizeLimit));\n}\n\nvoid LocalStorageBox::updateTotalLimit() {\n\tconst auto good = [&](int64 totalLimit) {\n\t\treturn (totalLimit - _mediaSizeLimit >= kMinimalSizeLimit);\n\t};\n\tif (good(_totalSizeLimit) || !_totalSlider || !_totalLabel) {\n\t\treturn;\n\t}\n\tauto index = kTotalSizeLimitsCount - 1;\n\twhile ((index > 0)\n\t\t&& (TotalSizeLimit(index - 1) >= 2 * _mediaSizeLimit)) {\n\t\t--index;\n\t}\n\t_totalSizeLimit = TotalSizeLimit(index);\n\t_totalSlider->setValue(index / float64(kTotalSizeLimitsCount - 1));\n\tupdateTotalLabel();\n\n\tEnsures(good(_totalSizeLimit));\n}\n\nvoid LocalStorageBox::updateTotalLabel() {\n\tExpects(_totalLabel != nullptr);\n\n\tconst auto text = SizeLimitText(_totalSizeLimit);\n\t_totalLabel->setText(tr::lng_local_storage_size_limit(tr::now, lt_size, text));\n}\n\nvoid LocalStorageBox::updateMediaLabel() {\n\tExpects(_mediaLabel != nullptr);\n\n\tconst auto text = SizeLimitText(_mediaSizeLimit);\n\t_mediaLabel->setText(tr::lng_local_storage_media_limit(tr::now, lt_size, text));\n}\n\nvoid LocalStorageBox::setupLimits(not_null<Ui::VerticalLayout*> container) {\n\tcontainer->add(\n\t\tobject_ptr<Ui::PlainShadow>(container),\n\t\tst::localStorageRowPadding);\n\n\t_totalSlider = createLimitsSlider(\n\t\tcontainer,\n\t\tkTotalSizeLimitsCount,\n\t\tTotalSizeLimit,\n\t\t_totalSizeLimit,\n\t\t[=](not_null<Ui::LabelSimple*> label, int64 limit) {\n\t\t\t_totalSizeLimit = limit;\n\t\t\t_totalLabel = label;\n\t\t\tupdateTotalLabel();\n\t\t\tupdateMediaLimit();\n\t\t\tlimitsChanged();\n\t\t});\n\n\t_mediaSlider = createLimitsSlider(\n\t\tcontainer,\n\t\tkMediaSizeLimitsCount,\n\t\tMediaSizeLimit,\n\t\t_mediaSizeLimit,\n\t\t[=](not_null<Ui::LabelSimple*> label, int64 limit) {\n\t\t\t_mediaSizeLimit = limit;\n\t\t\t_mediaLabel = label;\n\t\t\tupdateMediaLabel();\n\t\t\tupdateTotalLimit();\n\t\t\tlimitsChanged();\n\t\t});\n\n\tcreateLimitsSlider(\n\t\tcontainer,\n\t\tkTimeLimitsCount,\n\t\tTimeLimit,\n\t\tLimitToValue(_timeLimit),\n\t\t[=](not_null<Ui::LabelSimple*> label, size_type limit) {\n\t\t\t_timeLimit = ValueToLimit(limit);\n\t\t\tconst auto text = TimeLimitText(_timeLimit);\n\t\t\tlabel->setText(tr::lng_local_storage_time_limit(tr::now, lt_limit, text));\n\t\t\tlimitsChanged();\n\t\t});\n}\n\nvoid LocalStorageBox::limitsChanged() {\n\tconst auto &settings = _session->local().cacheSettings();\n\tconst auto &settingsBig = _session->local().cacheBigFileSettings();\n\tconst auto sizeLimit = _totalSizeLimit - _mediaSizeLimit;\n\tconst auto changed = (settings.totalSizeLimit != sizeLimit)\n\t\t|| (settingsBig.totalSizeLimit != _mediaSizeLimit)\n\t\t|| (settings.totalTimeLimit != _timeLimit)\n\t\t|| (settingsBig.totalTimeLimit != _timeLimit);\n\tif (_limitsChanged != changed) {\n\t\t_limitsChanged = changed;\n\t\tclearButtons();\n\t\tif (_limitsChanged) {\n\t\t\taddButton(tr::lng_settings_save(), [=] { save(); });\n\t\t\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\t\t} else {\n\t\t\taddButton(tr::lng_box_ok(), [=] { closeBox(); });\n\t\t}\n\t}\n}\n\nvoid LocalStorageBox::save() {\n\tif (!_limitsChanged) {\n\t\tcloseBox();\n\t\treturn;\n\t}\n\tauto update = Storage::Cache::Database::SettingsUpdate();\n\tupdate.totalSizeLimit = _totalSizeLimit - _mediaSizeLimit;\n\tupdate.totalTimeLimit = _timeLimit;\n\tauto updateBig = Storage::Cache::Database::SettingsUpdate();\n\tupdateBig.totalSizeLimit = _mediaSizeLimit;\n\tupdateBig.totalTimeLimit = _timeLimit;\n\t_session->local().updateCacheSettings(update, updateBig);\n\t_session->data().cache().updateSettings(update);\n\tcloseBox();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/local_storage_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n#include \"storage/cache/storage_cache_database.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Storage {\nnamespace Cache {\nclass Database;\n} // namespace Cache\n} // namespace Storage\n\nnamespace Ui {\nclass VerticalLayout;\ntemplate <typename Widget>\nclass SlideWrap;\nclass LabelSimple;\nclass MediaSlider;\n} // namespace Ui\n\nclass LocalStorageBox : public Ui::BoxContent {\n\tstruct CreateTag {\n\t};\n\npublic:\n\tusing Database = Storage::Cache::Database;\n\n\tLocalStorageBox(\n\t\tQWidget*,\n\t\tnot_null<Main::Session*> session,\n\t\tCreateTag);\n\n\tstatic void Show(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &highlightId = QString());\n\nprotected:\n\tvoid prepare() override;\n\tvoid showFinished() override;\n\nprivate:\n\tclass Row;\n\n\tvoid clearByTag(uint16 tag);\n\tvoid update(Database::Stats &&stats, Database::Stats &&statsBig);\n\tvoid updateRow(\n\t\tnot_null<Ui::SlideWrap<Row>*> row,\n\t\tconst Database::TaggedSummary *data);\n\tvoid setupControls();\n\tvoid setupLimits(not_null<Ui::VerticalLayout*> container);\n\tvoid updateMediaLimit();\n\tvoid updateTotalLimit();\n\tvoid updateTotalLabel();\n\tvoid updateMediaLabel();\n\tvoid limitsChanged();\n\tvoid save();\n\n\tDatabase::TaggedSummary summary() const;\n\n\ttemplate <\n\t\ttypename Value,\n\t\ttypename Convert,\n\t\ttypename Callback,\n\t\ttypename = std::enable_if_t<\n\t\t\trpl::details::is_callable_plain_v<\n\t\t\t\tCallback,\n\t\t\t\tnot_null<Ui::LabelSimple*>,\n\t\t\t\tValue>\n\t\t\t&& std::is_same_v<Value, decltype(std::declval<Convert>()(1))>>>\n\tnot_null<Ui::MediaSlider*> createLimitsSlider(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tint valuesCount,\n\t\tConvert &&convert,\n\t\tValue currentValue,\n\t\tCallback &&callback);\n\n\tconst not_null<Main::Session*> _session;\n\tconst not_null<Storage::Cache::Database*> _db;\n\tconst not_null<Storage::Cache::Database*> _dbBig;\n\n\tDatabase::Stats _stats;\n\tDatabase::Stats _statsBig;\n\n\tbase::flat_map<uint16, not_null<Ui::SlideWrap<Row>*>> _rows;\n\tUi::MediaSlider *_totalSlider = nullptr;\n\tUi::LabelSimple *_totalLabel = nullptr;\n\tUi::MediaSlider *_mediaSlider = nullptr;\n\tUi::LabelSimple *_mediaLabel = nullptr;\n\n\tint64 _totalSizeLimit = 0;\n\tint64 _mediaSizeLimit = 0;\n\tsize_type _timeLimit = 0;\n\tbool _limitsChanged = false;\n\tQString _highlightId;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/max_invite_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/max_invite_box.h\"\n\n#include \"api/api_invite_links.h\"\n#include \"apiwrap.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QClipboard>\n\nnamespace {\n\nTextParseOptions kInformBoxTextOptions = {\n\t(TextParseLinks\n\t\t| TextParseMultiline\n\t\t| TextParseMarkdown), // flags\n\t0, // maxw\n\t0, // maxh\n\tQt::LayoutDirectionAuto, // dir\n};\n\n} // namespace\n\nMaxInviteBox::MaxInviteBox(QWidget*, not_null<ChannelData*> channel)\n: BoxContent()\n, _channel(channel)\n, _text(\n\tst::boxLabelStyle,\n\ttr::lng_participant_invite_sorry(\n\t\ttr::now,\n\t\tlt_count,\n\t\tchannel->session().serverConfig().chatSizeMax),\n\tkInformBoxTextOptions,\n\t(st::boxWidth\n\t\t- st::boxPadding.left()\n\t\t- st::defaultBox.buttonPadding.right())) {\n}\n\nvoid MaxInviteBox::prepare() {\n\tsetMouseTracking(true);\n\n\taddButton(tr::lng_box_ok(), [=] { closeBox(); });\n\n\t_textWidth = st::boxWidth\n\t\t- st::boxPadding.left()\n\t\t- st::defaultBox.buttonPadding.right();\n\t_textHeight = std::min(\n\t\t_text.countHeight(_textWidth),\n\t\t16 * st::boxLabelStyle.lineHeight);\n\tsetDimensions(\n\t\tst::boxWidth,\n\t\tst::boxPadding.top()\n\t\t\t+ _textHeight\n\t\t\t+ st::boxTextFont->height\n\t\t\t+ st::boxTextFont->height * 2\n\t\t\t+ st::newGroupLinkPadding.bottom());\n\n\tif (_channel->inviteLink().isEmpty()) {\n\t\t_channel->session().api().requestFullPeer(_channel);\n\t}\n\t_channel->session().changes().peerUpdates(\n\t\t_channel,\n\t\tData::PeerUpdate::Flag::InviteLinks\n\t) | rpl::on_next([=] {\n\t\trtlupdate(_invitationLink);\n\t}, lifetime());\n}\n\nvoid MaxInviteBox::mouseMoveEvent(QMouseEvent *e) {\n\tupdateSelected(e->globalPos());\n}\n\nvoid MaxInviteBox::mousePressEvent(QMouseEvent *e) {\n\tmouseMoveEvent(e);\n\tif (_linkOver) {\n\t\tif (!_channel->inviteLink().isEmpty()) {\n\t\t\tQGuiApplication::clipboard()->setText(_channel->inviteLink());\n\t\t\tshowToast(tr::lng_create_channel_link_copied(tr::now));\n\t\t} else if (_channel->isFullLoaded() && !_creatingInviteLink) {\n\t\t\t_creatingInviteLink = true;\n\t\t\t_channel->session().api().inviteLinks().create({ _channel });\n\t\t}\n\t}\n}\n\nvoid MaxInviteBox::leaveEventHook(QEvent *e) {\n\tupdateSelected(QCursor::pos());\n}\n\nvoid MaxInviteBox::updateSelected(const QPoint &cursorGlobalPosition) {\n\tconst auto p = QPoint(mapFromGlobal(cursorGlobalPosition));\n\n\tconst auto linkOver = _invitationLink.contains(p);\n\tif (linkOver != _linkOver) {\n\t\t_linkOver = linkOver;\n\t\tupdate();\n\t\tsetCursor(_linkOver ? style::cur_pointer : style::cur_default);\n\t}\n}\n\nvoid MaxInviteBox::paintEvent(QPaintEvent *e) {\n\tBoxContent::paintEvent(e);\n\n\tPainter p(this);\n\n\t// draw box title / text\n\tp.setPen(st::boxTextFg);\n\t_text.drawLeftElided(\n\t\tp,\n\t\tst::boxPadding.left(),\n\t\tst::boxPadding.top(),\n\t\t_textWidth,\n\t\twidth(),\n\t\t16,\n\t\tstyle::al_left);\n\n\tauto option = QTextOption(style::al_left);\n\toption.setWrapMode(QTextOption::WrapAnywhere);\n\tp.setFont(_linkOver\n\t\t? st::defaultInputField.style.font->underline()\n\t\t: st::defaultInputField.style.font);\n\tp.setPen(st::defaultLinkButton.color);\n\tconst auto inviteLinkText = _channel->inviteLink().isEmpty()\n\t\t? tr::lng_group_invite_create(tr::now)\n\t\t: _channel->inviteLink();\n\tp.drawText(_invitationLink, inviteLinkText, option);\n}\n\nvoid MaxInviteBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\t_invitationLink = myrtlrect(\n\t\tst::boxPadding.left(),\n\t\tst::boxPadding.top() + _textHeight + st::boxTextFont->height,\n\t\twidth() - st::boxPadding.left() - st::boxPadding.right(),\n\t\t2 * st::boxTextFont->height);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/max_invite_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n\nclass MaxInviteBox final : public Ui::BoxContent {\npublic:\n\tMaxInviteBox(QWidget*, not_null<ChannelData*> channel);\n\nprotected:\n\tvoid prepare() override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\nprivate:\n\tvoid updateSelected(const QPoint &cursorGlobalPosition);\n\n\tnot_null<ChannelData*> _channel;\n\n\tUi::Text::String _text;\n\tint32 _textWidth, _textHeight;\n\n\tQRect _invitationLink;\n\tbool _linkOver = false;\n\tbool _creatingInviteLink = false;\n\n\tQPoint _lastMousePos;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/moderate_messages_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/moderate_messages_box.h\"\n\n#include \"api/api_blocked_peers.h\"\n#include \"api/api_chat_participants.h\"\n#include \"api/api_messages_search.h\"\n#include \"api/api_report.h\"\n#include \"apiwrap.h\"\n#include \"base/event_filter.h\"\n#include \"base/options.h\"\n#include \"base/timer.h\"\n#include \"boxes/delete_messages_box.h\"\n#include \"boxes/peers/edit_peer_permissions_box.h\"\n#include \"core/application.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/rect_part.h\"\n#include \"ui/text/text_lottie_custom_emoji.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/expandable_peer_list.h\"\n#include \"ui/widgets/participants_check_view.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_window.h\"\n\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"boxes/choose_filter_box.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"main/main_session_settings.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_multiline_action.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"ui/effects/round_checkbox.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_media_player.h\" // mediaPlayerMenuCheck\n\nbase::options::toggle ModerateCommonGroups({\n\t.id = kModerateCommonGroups,\n\t.name = \"Ban users from several groups at once.\",\n});\n\nconst char kModerateCommonGroups[] = \"moderate-common-groups\";\n\nnamespace {\n\nconstexpr auto kModerateMessagesBoxAnimationDuration = crl::time(80);\n\nstruct ModerateOptions final {\n\tbool allCanBan = false;\n\tbool allCanDelete = false;\n\tParticipants participants;\n};\n\nModerateOptions CalculateModerateOptions(const HistoryItemsList &items) {\n\tExpects(!items.empty());\n\n\tauto result = ModerateOptions{\n\t\t.allCanBan = true,\n\t\t.allCanDelete = true,\n\t};\n\n\tconst auto peer = items.front()->history()->peer;\n\tfor (const auto &item : items) {\n\t\tif (!result.allCanBan && !result.allCanDelete) {\n\t\t\treturn {};\n\t\t}\n\t\tif (peer != item->history()->peer) {\n\t\t\treturn {};\n\t\t}\n\t\t{\n\t\t\tconst auto author = item->author();\n\t\t\tif (author == peer) {\n\t\t\t\treturn {};\n\t\t\t} else if (const auto channel = author->asChannel()) {\n\t\t\t\tif (channel->discussionLink() == peer) {\n\t\t\t\t\treturn {};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!item->suggestBanReport()) {\n\t\t\tresult.allCanBan = false;\n\t\t}\n\t\tif (!item->suggestDeleteAllReport()) {\n\t\t\tresult.allCanDelete = false;\n\t\t}\n\t\tif (const auto p = item->from()) {\n\t\t\tif (!ranges::contains(result.participants, not_null{ p })) {\n\t\t\t\tresult.participants.push_back(p);\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] rpl::producer<base::flat_map<PeerId, int>> MessagesCountValue(\n\t\tnot_null<History*> history,\n\t\tstd::vector<not_null<PeerData*>> from) {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tstruct State final {\n\t\t\tbase::flat_map<PeerId, int> messagesCounts;\n\t\t\tint index = 0;\n\t\t\trpl::lifetime apiLifetime;\n\t\t};\n\t\tconst auto search = lifetime.make_state<Api::MessagesSearch>(history);\n\t\tconst auto state = lifetime.make_state<State>();\n\t\tconst auto send = [=](auto repeat) -> void {\n\t\t\tif (state->index >= from.size()) {\n\t\t\t\tconsumer.put_next_copy(state->messagesCounts);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto peer = from[state->index];\n\t\t\tconst auto peerId = peer->id;\n\t\t\tstate->apiLifetime = search->messagesFounds(\n\t\t\t) | rpl::on_next([=](const Api::FoundMessages &found) {\n\t\t\t\tstate->messagesCounts[peerId] = found.total;\n\t\t\t\tstate->index++;\n\t\t\t\trepeat(repeat);\n\t\t\t});\n\t\t\tsearch->searchMessages({ .from = peer });\n\t\t};\n\t\tconsumer.put_next({});\n\t\tsend(send);\n\n\t\treturn lifetime;\n\t};\n}\n\nusing CommonGroups = std::vector<not_null<PeerData*>>;\nusing CollectCommon = std::shared_ptr<std::vector<PeerId>>;\n\nvoid FillMenuModerateCommonGroups(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tCommonGroups common,\n\t\tCollectCommon collectCommon,\n\t\tnot_null<UserData*> user,\n\t\tFn<void()> onDestroyedCallback) {\n\tconst auto resultList\n\t\t= menu->lifetime().make_state<base::flat_set<PeerId>>();\n\tconst auto rememberCheckbox = Ui::CreateChild<Ui::Checkbox>(\n\t\tmenu,\n\t\tQString());\n\tauto multiline = base::make_unique_q<Ui::Menu::MultilineAction>(\n\t\tmenu->menu(),\n\t\tmenu->st().menu,\n\t\tst::historyHasCustomEmoji,\n\t\tst::historyHasCustomEmojiPosition,\n\t\ttr::lng_restrict_users_kick_from_common_group(tr::now, tr::rich));\n\tmultiline->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tmenu->addAction(std::move(multiline));\n\tconst auto session = &common.front()->session();\n\tconst auto settingsOnStart = session->settings().moderateCommonGroups();\n\tconst auto checkboxesUpdate = std::make_shared<rpl::event_stream<>>();\n\tconst auto save = [=] {\n\t\tauto result = std::vector<PeerId>(\n\t\t\tresultList->begin(),\n\t\t\tresultList->end());\n\t\t*collectCommon = std::move(result);\n\t};\n\tfor (const auto &group : common) {\n\t\tstruct State {\n\t\t\tstd::optional<Ui::RoundImageCheckbox> checkbox;\n\t\t\tUi::RpWidget *checkboxWidget = nullptr;\n\t\t};\n\t\tauto item = base::make_unique_q<Ui::Menu::Action>(\n\t\t\tmenu->menu(),\n\t\t\tmenu->st().menu,\n\t\t\tUi::Menu::CreateAction(\n\t\t\t\tmenu->menu(),\n\t\t\t\tgroup->name(),\n\t\t\t\t[] {}),\n\t\t\tnullptr,\n\t\t\tnullptr);\n\t\tconst auto state = item->lifetime().make_state<State>();\n\t\tconst auto setChecked = [=, peerId = group->id](bool checked) {\n\t\t\tstate->checkbox->setChecked(checked);\n\t\t\tif (state->checkbox->checked()) {\n\t\t\t\tresultList->insert(peerId);\n\t\t\t} else {\n\t\t\t\tresultList->erase(peerId);\n\t\t\t}\n\t\t\tsave();\n\t\t};\n\t\titem->setActionTriggered([=] {\n\t\t\tsetChecked(!state->checkbox->checked());\n\t\t});\n\t\tconst auto raw = item.get();\n\t\tcheckboxesUpdate->events() | rpl::on_next([=, peerId = group->id] {\n\t\t\tsetChecked(ranges::contains(*collectCommon, peerId));\n\t\t}, raw->lifetime());\n\t\tstate->checkboxWidget = Ui::CreateChild<Ui::RpWidget>(raw);\n\t\tstate->checkboxWidget->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tstate->checkboxWidget->resize(item->width() * 2, item->height());\n\t\tstate->checkboxWidget->show();\n\t\tstate->checkbox.emplace(\n\t\t\tst::moderateCommonGroupsCheckbox,\n\t\t\t[=] { state->checkboxWidget->update(); },\n\t\t\tPaintUserpicCallback(group, true),\n\t\t\t[=](int size) { return (group->isForum() || group->isMonoforum())\n\t\t\t\t? int(size * Ui::ForumUserpicRadiusMultiplier())\n\t\t\t\t: std::optional<int>(); });\n\t\tstate->checkbox->setChecked(\n\t\t\t/*ranges::contains(\n\t\t\t\tsession->settings().moderateCommonGroups(),\n\t\t\t\tgroup->id)\n\t\t\t|| */(collectCommon\n\t\t\t\t&& ranges::contains(*collectCommon, group->id)),\n\t\t\tanim::type::instant);\n\t\tstate->checkboxWidget->paintOn([=](QPainter &p) {\n\t\t\tauto pp = Painter(state->checkboxWidget);\n\t\t\tstate->checkbox->paint(\n\t\t\t\tpp,\n\t\t\t\tst::menuWithIcons.itemIconPosition.x(),\n\t\t\t\tst::menuWithIcons.itemIconPosition.y(),\n\t\t\t\traw->width());\n\t\t});\n\t\tmenu->addAction(std::move(item));\n\t}\n\tmenu->addSeparator();\n\tif (const auto window = Core::App().findWindow(menu->parentWidget())) {\n\t\tauto hasActions = false;\n\t\tUi::Menu::CreateAddActionCallback(menu)(Ui::Menu::MenuCallback::Args{\n\t\t\t.text = tr::lng_restrict_users_kick_from_common_group(tr::now),\n\t\t\t.handler = nullptr,\n\t\t\t.icon = &st::menuIconAddToFolder,\n\t\t\t.fillSubmenu = [&](not_null<Ui::PopupMenu*> menu) {\n\t\t\t\thasActions = FillChooseFilterWithAdminedGroupsMenu(\n\t\t\t\t\twindow->sessionController(),\n\t\t\t\t\tmenu,\n\t\t\t\t\tuser,\n\t\t\t\t\tcheckboxesUpdate,\n\t\t\t\t\tcommon,\n\t\t\t\t\tcollectCommon);\n\t\t\t},\n\t\t\t.submenuSt = &st::foldersMenu,\n\t\t});\n\t\tif (!hasActions) {\n\t\t\tmenu->removeAction(menu->actions().size() - 1);\n\t\t\tmenu->removeAction(menu->actions().size() - 1); // Separator.\n\t\t}\n\t}\n\tmenu->addSeparator();\n\t{\n\t\tauto item = base::make_unique_q<Ui::Menu::Action>(\n\t\t\tmenu->menu(),\n\t\t\tmenu->st().menu,\n\t\t\tUi::Menu::CreateAction(\n\t\t\t\tmenu->menu(),\n\t\t\t\ttr::lng_remember(tr::now),\n\t\t\t\t[] {}),\n\t\t\tnullptr,\n\t\t\tnullptr);\n\t\titem->setPreventClose(true);\n\t\titem->setActionTriggered([=] {\n\t\t\trememberCheckbox->setChecked(!rememberCheckbox->checked());\n\t\t});\n\t\trememberCheckbox->setParent(item.get());\n\t\trememberCheckbox->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\trememberCheckbox->move(st::lineWidth * 8, -st::lineWidth * 2);\n\t\trememberCheckbox->show();\n\t\tmenu->addAction(std::move(item));\n\t}\n\tmenu->setDestroyedCallback([=] {\n\t\tonDestroyedCallback();\n\t\tif (!rememberCheckbox->checked()) {\n\t\t\tsession->settings().setModerateCommonGroups(settingsOnStart);\n\t\t\tsession->saveSettingsDelayed();\n\t\t}\n\t});\n}\n\nvoid ProccessCommonGroups(\n\t\tconst HistoryItemsList &items,\n\t\tFn<void(CommonGroups, not_null<UserData*>)> processHas) {\n\tconst auto moderateOptions = CalculateModerateOptions(items);\n\tif (moderateOptions.participants.size() != 1\n\t\t|| !moderateOptions.allCanBan) {\n\t\treturn;\n\t}\n\tconst auto participant = moderateOptions.participants.front();\n\tconst auto user = participant->asUser();\n\tif (!user) {\n\t\treturn;\n\t}\n\tconst auto currentGroupId = items.front()->history()->peer->id;\n\tuser->session().api().requestBotCommonGroups(user, [=] {\n\t\tconst auto commonGroups = user->session().api().botCommonGroups(user);\n\t\tif (!commonGroups || commonGroups->empty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tauto filtered = CommonGroups();\n\t\tfor (const auto &group : *commonGroups) {\n\t\t\tif (group->id == currentGroupId) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto channel = group->asChannel();\n\t\t\tif (channel && channel->canRestrictParticipant(user)) {\n\t\t\t\tif (channel->isGroupAdmin(user) && !channel->amCreator()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tfiltered.push_back(group);\n\t\t\t}\n\t\t}\n\n\t\tif (!filtered.empty()) {\n\t\t\tprocessHas(filtered, user);\n\t\t}\n\t});\n}\n\n} // namespace\n\nvoid CreateModerateMessagesBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst HistoryItemsList &items,\n\t\tFn<void()> confirmed,\n\t\tModerateMessagesBoxOptions options) {\n\tExpects(!items.empty());\n\tbox->setLayerAnimationDuration(kModerateMessagesBoxAnimationDuration);\n\n\tusing Controller = Ui::ExpandablePeerListController;\n\n\tconst auto [allCanBan, allCanDelete, participants]\n\t\t= CalculateModerateOptions(items);\n\tconst auto inner = box->verticalLayout();\n\n\tAssert(!participants.empty());\n\n\tconst auto confirms = inner->lifetime().make_state<rpl::event_stream<>>();\n\tconst auto collectCommon = std::make_shared<std::vector<PeerId>>();\n\n\tconst auto isSingle = participants.size() == 1;\n\tconst auto buttonPadding = isSingle\n\t\t? QMargins()\n\t\t: QMargins(\n\t\t\t0,\n\t\t\t0,\n\t\t\tUi::ParticipantsCheckView::ComputeSize(\n\t\t\t\tparticipants.size()).width(),\n\t\t\t0);\n\n\tconst auto itemsCount = int(items.size());\n\tconst auto firstItem = items.front();\n\tconst auto history = firstItem->history();\n\tconst auto session = &history->session();\n\tconst auto historyPeerId = history->peer->id;\n\tconst auto ids = session->data().itemsToIds(items);\n\n\t{\n\t\tconst auto remainingIds\n\t\t\t= box->lifetime().make_state<base::flat_set<FullMsgId>>(\n\t\t\t\tids.begin(),\n\t\t\t\tids.end());\n\t\tsession->data().itemRemoved(\n\t\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\t\tremainingIds->erase(item->fullId());\n\t\t\tif (remainingIds->empty()) {\n\t\t\t\tbox->closeBox();\n\t\t\t}\n\t\t}, box->lifetime());\n\t}\n\n\tif (ModerateCommonGroups.value() || session->supportMode()) {\n\tProccessCommonGroups(\n\t\titems,\n\t\tcrl::guard(box, [=](CommonGroups groups, not_null<UserData*> user) {\n\t\t\tusing namespace Ui;\n\t\t\tconst auto top = box->addTopButton(st::infoTopBarMenu);\n\t\t\tauto &lifetime = top->lifetime();\n\t\t\tconst auto menu\n\t\t\t\t= lifetime.make_state<base::unique_qptr<Ui::PopupMenu>>();\n\n\t\t\t{\n\t\t\t\tconst auto was = collectCommon->size();\n\t\t\t\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\t\ttop,\n\t\t\t\t\tst::popupMenuExpandedSeparator);\n\t\t\t\tFillMenuModerateCommonGroups(\n\t\t\t\t\t*menu,\n\t\t\t\t\tgroups,\n\t\t\t\t\tcollectCommon,\n\t\t\t\t\tuser,\n\t\t\t\t\t[]{});\n\t\t\t\t*menu = nullptr;\n\t\t\t\tif (was != collectCommon->size()) {\n\t\t\t\t\ttop->setIconOverride(\n\t\t\t\t\t\t&st::infoTopBarMenuActive,\n\t\t\t\t\t\t&st::infoTopBarMenuActive);\n\t\t\t\t\tconst auto minicheck = Ui::CreateChild<Ui::RpWidget>(top);\n\t\t\t\t\tminicheck->paintRequest() | rpl::on_next([=] {\n\t\t\t\t\t\tauto p = Painter(minicheck);\n\t\t\t\t\t\tconst auto rect = minicheck->rect();\n\t\t\t\t\t\tconst auto iconSize = QSize(\n\t\t\t\t\t\t\tst::mediaPlayerMenuCheck.width(),\n\t\t\t\t\t\t\tst::mediaPlayerMenuCheck.height());\n\t\t\t\t\t\tconst auto scale = std::min(\n\t\t\t\t\t\t\trect.width() / float64(iconSize.width()),\n\t\t\t\t\t\t\trect.height() / float64(iconSize.height()));\n\t\t\t\t\t\tif (scale < 1.0) {\n\t\t\t\t\t\t\tp.save();\n\t\t\t\t\t\t\tp.translate(rect.center());\n\t\t\t\t\t\t\tp.scale(scale, scale);\n\t\t\t\t\t\t\tp.translate(-rect.center());\n\t\t\t\t\t\t}\n\t\t\t\t\t\tst::mediaPlayerMenuCheck.paintInCenter(\n\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\trect,\n\t\t\t\t\t\t\tst::windowActiveTextFg->c);\n\t\t\t\t\t\tif (scale < 1.0) {\n\t\t\t\t\t\t\tp.restore();\n\t\t\t\t\t\t}\n\t\t\t\t\t}, minicheck->lifetime());\n\t\t\t\t\tminicheck->resize(\n\t\t\t\t\t\tst::mediaPlayerMenuCheck.width() / 1.5,\n\t\t\t\t\t\tst::mediaPlayerMenuCheck.width() / 1.5);\n\t\t\t\t\tminicheck->show();\n\t\t\t\t\tminicheck->moveToLeft(\n\t\t\t\t\t\ttop->width() - st::lineWidth * 26,\n\t\t\t\t\t\ttop->height() - st::lineWidth * 29);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttop->setClickedCallback([=] {\n\t\t\t\ttop->setForceRippled(true);\n\t\t\t\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\t\ttop,\n\t\t\t\t\tst::popupMenuExpandedSeparator);\n\t\t\t\tconst auto onDestroyedCallback = [=, weak = top] {\n\t\t\t\t\tif (const auto strong = weak.data()) {\n\t\t\t\t\t\tstrong->setForceRippled(false);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tFillMenuModerateCommonGroups(\n\t\t\t\t\t*menu,\n\t\t\t\t\tgroups,\n\t\t\t\t\tcollectCommon,\n\t\t\t\t\tuser,\n\t\t\t\t\tonDestroyedCallback);\n\t\t\t\t(*menu)->setForcedOrigin(PanelAnimation::Origin::TopRight);\n\t\t\t\tconst auto point = QPoint(top->width(), top->height());\n\t\t\t\t(*menu)->popup(top->mapToGlobal(point));\n\t\t\t});\n\t\t}));\n\t}\n\n\tusing Request = Fn<void(not_null<PeerData*>, not_null<ChannelData*>)>;\n\tconst auto sequentiallyRequest = [=](\n\t\t\tRequest request,\n\t\t\tParticipants participants,\n\t\t\tstd::optional<std::vector<PeerId>> channelIds = {}) {\n\t\tconstexpr auto kSmallDelayMs = 5;\n\t\tconst auto participantIds = ranges::views::all(\n\t\t\tparticipants\n\t\t) | ranges::views::transform([](not_null<PeerData*> peer) {\n\t\t\treturn peer->id;\n\t\t}) | ranges::to_vector;\n\t\tconst auto channelIdList = channelIds.value_or(\n\t\t\tstd::vector<PeerId>{ historyPeerId });\n\t\tconst auto lifetime = std::make_shared<rpl::lifetime>();\n\t\tconst auto participantIndex = lifetime->make_state<int>(0);\n\t\tconst auto channelIndex = lifetime->make_state<int>(0);\n\t\tconst auto timer = lifetime->make_state<base::Timer>();\n\t\ttimer->setCallback(crl::guard(session, [=] {\n\t\t\tif ((*participantIndex) < participantIds.size()) {\n\t\t\t\tif ((*channelIndex) < channelIdList.size()) {\n\t\t\t\t\tconst auto from = session->data().peer(\n\t\t\t\t\t\tparticipantIds[*participantIndex]);\n\t\t\t\t\tconst auto channel = session->data().peer(\n\t\t\t\t\t\tchannelIdList[*channelIndex])->asChannel();\n\t\t\t\t\tif (from && channel) {\n\t\t\t\t\t\trequest(from, channel);\n\t\t\t\t\t}\n\t\t\t\t\t(*channelIndex)++;\n\t\t\t\t} else {\n\t\t\t\t\t(*participantIndex)++;\n\t\t\t\t\t*channelIndex = 0;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlifetime->destroy();\n\t\t\t}\n\t\t}));\n\t\ttimer->callEach(kSmallDelayMs);\n\t};\n\n\tconst auto handleConfirmation = [=](\n\t\t\tnot_null<Ui::Checkbox*> checkbox,\n\t\t\tnot_null<Controller*> controller,\n\t\t\tRequest request) {\n\t\tconfirms->events() | rpl::on_next([=] {\n\t\t\tif (checkbox->checked() && controller->collectRequests) {\n\t\t\t\tsequentiallyRequest(request, controller->collectRequests());\n\t\t\t}\n\t\t}, checkbox->lifetime());\n\t};\n\n\tconst auto isEnter = [=](not_null<QEvent*> event) {\n\t\tif (event->type() == QEvent::KeyPress) {\n\t\t\tif (const auto k = static_cast<QKeyEvent*>(event.get())) {\n\t\t\t\treturn (k->key() == Qt::Key_Enter)\n\t\t\t\t\t|| (k->key() == Qt::Key_Return);\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\n\tbase::install_event_filter(box, [=](not_null<QEvent*> event) {\n\t\tif (isEnter(event)) {\n\t\t\tbox->triggerButton(0);\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\tconst auto handleSubmition = [=](not_null<Ui::Checkbox*> checkbox) {\n\t\tbase::install_event_filter(box, [=](not_null<QEvent*> event) {\n\t\t\tif (!isEnter(event) || !checkbox->checked()) {\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t}\n\t\t\tbox->uiShow()->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_gigagroup_warning_title(),\n\t\t\t\t.confirmed = [=](Fn<void()> close) {\n\t\t\t\t\tbox->triggerButton(0);\n\t\t\t\t\tclose();\n\t\t\t\t},\n\t\t\t\t.confirmText = tr::lng_box_yes(),\n\t\t\t\t.cancelText = tr::lng_box_no(),\n\t\t\t}));\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t});\n\t};\n\n\tUi::AddSkip(inner);\n\tconst auto title = box->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\t(itemsCount == 1)\n\t\t\t\t? tr::lng_selected_delete_sure_this()\n\t\t\t\t: tr::lng_selected_delete_sure(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(itemsCount) | tr::to_count()),\n\t\t\tst::boxLabel));\n\tUi::AddSkip(inner);\n\tUi::AddSkip(inner);\n\tUi::AddSkip(inner);\n\t{\n\t\tconst auto report = box->addRow(\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_report_spam(tr::now),\n\t\t\t\toptions.reportSpam,\n\t\t\t\tst::defaultBoxCheckbox),\n\t\t\tst::boxRowPadding + buttonPadding);\n\t\tconst auto controller = box->lifetime().make_state<Controller>(\n\t\t\tController::Data{ .participants = participants });\n\t\tUi::AddExpandablePeerList(report, controller, inner);\n\t\thandleSubmition(report);\n\n\t\thandleConfirmation(report, controller, [=](\n\t\t\t\tnot_null<PeerData*> p,\n\t\t\t\tnot_null<ChannelData*> c) {\n\t\t\tApi::ReportSpam(p, ids);\n\t\t});\n\t}\n\n\tif (allCanDelete) {\n\t\tUi::AddSkip(inner);\n\t\tUi::AddSkip(inner);\n\n\t\tconst auto deleteAll = inner->add(\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tinner,\n\t\t\t\t!(isSingle)\n\t\t\t\t\t? tr::lng_delete_all_from_users(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t: tr::lng_delete_all_from_user(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\ttr::bold(firstItem->from()->name()),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\toptions.deleteAll,\n\t\t\t\tst::defaultBoxCheckbox),\n\t\t\tst::boxRowPadding + buttonPadding);\n\t\tauto messagesCounts = MessagesCountValue(history, participants);\n\n\t\tconst auto controller = box->lifetime().make_state<Controller>(\n\t\t\tController::Data{\n\t\t\t\t.messagesCounts = rpl::duplicate(messagesCounts),\n\t\t\t\t.participants = participants,\n\t\t\t});\n\t\tUi::AddExpandablePeerList(deleteAll, controller, inner);\n\t\t{\n\t\t\tauto itemFromIds = items | ranges::views::transform([](\n\t\t\t\t\tconst auto &item) {\n\t\t\t\treturn item->from()->id;\n\t\t\t}) | ranges::to_vector;\n\n\t\t\trpl::combine(\n\t\t\t\tstd::move(messagesCounts),\n\t\t\t\tisSingle\n\t\t\t\t\t? deleteAll->checkedValue()\n\t\t\t\t\t: rpl::merge(\n\t\t\t\t\t\tcontroller->toggleRequestsFromInner.events(),\n\t\t\t\t\t\tcontroller->checkAllRequests.events())\n\t\t\t) | rpl::map([=](const auto &map, bool c) {\n\t\t\t\tconst auto checked = (isSingle && !c)\n\t\t\t\t\t? Participants()\n\t\t\t\t\t: controller->collectRequests\n\t\t\t\t\t? controller->collectRequests()\n\t\t\t\t\t: Participants();\n\t\t\t\tauto result = 0;\n\t\t\t\tfor (const auto &[peerId, count] : map) {\n\t\t\t\t\tfor (const auto &peer : checked) {\n\t\t\t\t\t\tif (peer->id == peerId) {\n\t\t\t\t\t\t\tresult += count;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor (const auto &fromId : itemFromIds) {\n\t\t\t\t\tfor (const auto &peer : checked) {\n\t\t\t\t\t\tif (peer->id == fromId) {\n\t\t\t\t\t\t\tresult--;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tresult++;\n\t\t\t\t}\n\t\t\t\treturn float64(result);\n\t\t\t}) | rpl::on_next([=](int amount) {\n\t\t\t\tauto text = tr::lng_selected_delete_sure(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tfloat64(amount));\n\t\t\t\tif (amount > 0) {\n\t\t\t\t\ttitle->setText(std::move(text));\n\t\t\t\t} else {\n\t\t\t\t\tconst auto zeroIndex = text.indexOf('0');\n\t\t\t\t\tif (zeroIndex != -1) {\n\t\t\t\t\t\tauto descriptor = Lottie::IconDescriptor{\n\t\t\t\t\t\t\t.name = u\"transcribe_loading\"_q,\n\t\t\t\t\t\t\t.color = &st::attentionButtonFg, // Any contrast.\n\t\t\t\t\t\t\t.sizeOverride = Size(\n\t\t\t\t\t\t\t\tst::historyTranscribeLoadingSize),\n\t\t\t\t\t\t\t.colorizeUsingAlpha = true,\n\t\t\t\t\t\t};\n\t\t\t\t\t\tauto result = TextWithEntities()\n\t\t\t\t\t\t\t.append(text.mid(0, zeroIndex))\n\t\t\t\t\t\t\t.append(Ui::Text::LottieEmoji(descriptor))\n\t\t\t\t\t\t\t.append(text.mid(zeroIndex + 1));\n\t\t\t\t\t\tusing namespace Ui::Text;\n\t\t\t\t\t\ttitle->setMarkedText(\n\t\t\t\t\t\t\tstd::move(result),\n\t\t\t\t\t\t\tLottieEmojiContext(std::move(descriptor)));\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttitle->setText(std::move(text));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ttitle->resizeToWidth(inner->width()\n\t\t\t\t\t- rect::m::sum::h(st::boxRowPadding));\n\t\t\t}, title->lifetime());\n\t\t}\n\t\thandleSubmition(deleteAll);\n\n\t\thandleConfirmation(deleteAll, controller, [=](\n\t\t\t\tnot_null<PeerData*> p,\n\t\t\t\tnot_null<ChannelData*> c) {\n\t\t\tp->session().api().deleteAllFromParticipant(c, p);\n\t\t});\n\t}\n\tif (allCanBan) {\n\t\tconst auto peer = items.front()->history()->peer;\n\t\tauto ownedWrap = peer->isMonoforum()\n\t\t\t? nullptr\n\t\t\t: object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner));\n\t\tauto computeRestrictions = Fn<ChatRestrictions()>();\n\t\tconst auto wrap = ownedWrap.data();\n\n\t\tUi::AddSkip(inner);\n\t\tUi::AddSkip(inner);\n\t\tconst auto ban = inner->add(\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tbox,\n\t\t\t\trpl::conditional(\n\t\t\t\t\t(ownedWrap\n\t\t\t\t\t\t? ownedWrap->toggledValue()\n\t\t\t\t\t\t: rpl::single(false) | rpl::type_erased),\n\t\t\t\t\ttr::lng_restrict_user(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single(participants.size()) | tr::to_count()),\n\t\t\t\t\trpl::conditional(\n\t\t\t\t\t\trpl::single(isSingle),\n\t\t\t\t\t\ttr::lng_ban_user(),\n\t\t\t\t\t\ttr::lng_ban_users())),\n\t\t\t\toptions.banUser,\n\t\t\t\tst::defaultBoxCheckbox),\n\t\t\tst::boxRowPadding + buttonPadding);\n\t\tconst auto controller = box->lifetime().make_state<Controller>(\n\t\t\tController::Data{ .participants = participants });\n\t\tUi::AddExpandablePeerList(ban, controller, inner);\n\t\thandleSubmition(ban);\n\n\t\tUi::AddSkip(inner);\n\t\tUi::AddSkip(inner);\n\n\t\tif (ownedWrap) {\n\t\t\tinner->add(std::move(ownedWrap));\n\n\t\t\tconst auto container = wrap->entity();\n\t\t\twrap->toggle(false, anim::type::instant);\n\n\t\t\tconst auto emojiUp = Ui::Text::IconEmoji(\n\t\t\t\t&st::moderateBoxExpandIcon);\n\t\t\tconst auto emojiDown = Ui::Text::IconEmoji(\n\t\t\t\t&st::moderateBoxExpandIconDown);\n\n\t\t\tauto label = object_ptr<Ui::FlatLabel>(\n\t\t\t\tinner,\n\t\t\t\tQString(),\n\t\t\t\tst::moderateBoxDividerLabel);\n\t\t\tconst auto raw = label.data();\n\n\t\t\tauto &lifetime = wrap->lifetime();\n\t\t\tconst auto scrollLifetime = lifetime.make_state<rpl::lifetime>();\n\t\t\tlabel->setClickHandlerFilter([=](\n\t\t\t\t\tconst ClickHandlerPtr &handler,\n\t\t\t\t\tQt::MouseButton button) {\n\t\t\t\tif (button != Qt::LeftButton) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\twrap->toggle(!wrap->toggled(), anim::type::normal);\n\t\t\t\t{\n\t\t\t\t\tinner->heightValue() | rpl::on_next([=] {\n\t\t\t\t\t\tif (!wrap->animating()) {\n\t\t\t\t\t\t\tscrollLifetime->destroy();\n\t\t\t\t\t\t\tUi::PostponeCall(crl::guard(box, [=] {\n\t\t\t\t\t\t\t\tbox->scrollToY(std::numeric_limits<int>::max());\n\t\t\t\t\t\t\t}));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tbox->scrollToY(std::numeric_limits<int>::max());\n\t\t\t\t\t\t}\n\t\t\t\t\t}, *scrollLifetime);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\t\t\twrap->toggledValue(\n\t\t\t) | rpl::map([isSingle, emojiUp, emojiDown](bool toggled) {\n\t\t\t\treturn ((toggled && isSingle)\n\t\t\t\t\t? tr::lng_restrict_user_part\n\t\t\t\t\t: (toggled && !isSingle)\n\t\t\t\t\t? tr::lng_restrict_users_part\n\t\t\t\t\t: isSingle\n\t\t\t\t\t? tr::lng_restrict_user_full\n\t\t\t\t\t: tr::lng_restrict_users_full)(\n\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\trpl::single(toggled ? emojiUp : emojiDown),\n\t\t\t\t\t\ttr::marked);\n\t\t\t}) | rpl::flatten_latest(\n\t\t\t) | rpl::on_next([=](const TextWithEntities &text) {\n\t\t\t\traw->setMarkedText(tr::link(text, u\"internal:\"_q));\n\t\t\t}, label->lifetime());\n\n\t\t\tUi::AddSkip(inner);\n\t\t\tinner->add(object_ptr<Ui::DividerLabel>(\n\t\t\t\tinner,\n\t\t\t\tstd::move(label),\n\t\t\t\tst::defaultBoxDividerLabelPadding));\n\n\t\t\tusing Flag = ChatRestriction;\n\t\t\tusing Flags = ChatRestrictions;\n\t\t\tconst auto chat = peer->asChat();\n\t\t\tconst auto channel = peer->asChannel();\n\t\t\tconst auto defaultRestrictions = chat\n\t\t\t\t? chat->defaultRestrictions()\n\t\t\t\t: channel->defaultRestrictions();\n\t\t\tconst auto prepareFlags = FixDependentRestrictions(\n\t\t\t\tdefaultRestrictions\n\t\t\t\t| ((channel && channel->isPublic())\n\t\t\t\t\t? (Flag::ChangeInfo | Flag::PinMessages)\n\t\t\t\t\t: Flags(0)));\n\t\t\tconst auto disabledMessages = [&] {\n\t\t\t\tauto result = base::flat_map<Flags, QString>();\n\t\t\t\t{\n\t\t\t\t\tconst auto disabled = FixDependentRestrictions(\n\t\t\t\t\t\tdefaultRestrictions\n\t\t\t\t\t\t| ((channel && channel->isPublic())\n\t\t\t\t\t\t\t? (Flag::ChangeInfo | Flag::PinMessages)\n\t\t\t\t\t\t\t: Flags(0)));\n\t\t\t\t\tresult.emplace(\n\t\t\t\t\t\tdisabled,\n\t\t\t\t\t\ttr::lng_rights_restriction_for_all(tr::now));\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t}();\n\n\t\t\tauto [checkboxes, getRestrictions, changes, highlightWidget] = CreateEditRestrictions(\n\t\t\t\tbox,\n\t\t\t\tprepareFlags,\n\t\t\t\tdisabledMessages,\n\t\t\t\t{ .isForum = peer->isForum(), .isUserSpecific = true });\n\t\t\tcomputeRestrictions = getRestrictions;\n\t\t\tstd::move(changes) | rpl::on_next([=] {\n\t\t\t\tban->setChecked(true);\n\t\t\t}, ban->lifetime());\n\t\t\tUi::AddSkip(container);\n\t\t\tUi::AddDivider(container);\n\t\t\tUi::AddSkip(container);\n\t\t\tUi::AddSubsectionTitle(\n\t\t\t\tcontainer,\n\t\t\t\trpl::conditional(\n\t\t\t\t\trpl::single(isSingle),\n\t\t\t\t\ttr::lng_restrict_users_part_single_header(),\n\t\t\t\t\ttr::lng_restrict_users_part_header(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single(participants.size()) | tr::to_count())));\n\t\t\tcontainer->add(std::move(checkboxes));\n\t\t}\n\n\t\t// Handle confirmation manually.\n\t\tconfirms->events() | rpl::on_next([=] {\n\t\t\tif (ban->checked() && controller->collectRequests) {\n\t\t\t\tconst auto kick = !wrap || !wrap->toggled();\n\t\t\t\tconst auto restrictions = computeRestrictions\n\t\t\t\t\t? computeRestrictions()\n\t\t\t\t\t: ChatRestrictions();\n\t\t\t\tconst auto request = [=](\n\t\t\t\t\t\tnot_null<PeerData*> peer,\n\t\t\t\t\t\tnot_null<ChannelData*> channel) {\n\t\t\t\t\tif (base::IsAltPressed() || base::IsCtrlPressed()) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (!kick) {\n\t\t\t\t\t\tApi::ChatParticipants::Restrict(\n\t\t\t\t\t\t\tchannel,\n\t\t\t\t\t\t\tpeer,\n\t\t\t\t\t\t\tChatRestrictionsInfo(), // Unused.\n\t\t\t\t\t\t\tChatRestrictionsInfo(restrictions, 0),\n\t\t\t\t\t\t\tnullptr,\n\t\t\t\t\t\t\tnullptr);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst auto block = channel->isMonoforum()\n\t\t\t\t\t\t\t? channel->monoforumBroadcast()\n\t\t\t\t\t\t\t: channel.get();\n\t\t\t\t\t\tif (block) {\n\t\t\t\t\t\t\tblock->session().api().chatParticipants().kick(\n\t\t\t\t\t\t\t\tblock,\n\t\t\t\t\t\t\t\tpeer,\n\t\t\t\t\t\t\t\t{ block->restrictions(), 0 });\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tif (collectCommon && !collectCommon->empty()) {\n\t\t\t\t\tsequentiallyRequest(\n\t\t\t\t\t\trequest,\n\t\t\t\t\t\tcontroller->collectRequests(),\n\t\t\t\t\t\t*collectCommon);\n\t\t\t\t} else {\n\t\t\t\t\tsequentiallyRequest(\n\t\t\t\t\t\trequest,\n\t\t\t\t\t\tcontroller->collectRequests());\n\t\t\t\t}\n\t\t\t}\n\t\t}, ban->lifetime());\n\t}\n\n\tconst auto close = crl::guard(box, [=] { box->closeBox(); });\n\t{\n\t\tconst auto data = &participants.front()->session().data();\n\t\tconst auto ids = data->itemsToIds(items);\n\t\tbox->addButton(tr::lng_box_delete(), [=] {\n\t\t\tconfirms->fire({});\n\t\t\tif (confirmed) {\n\t\t\t\tconfirmed();\n\t\t\t}\n\t\t\tdata->histories().deleteMessages(ids, true);\n\t\t\tdata->sendHistoryChangeNotifications();\n\t\t\tclose();\n\t\t});\n\t}\n\tbox->addButton(tr::lng_cancel(), close);\n}\n\nbool CanCreateModerateMessagesBox(const HistoryItemsList &items) {\n\tconst auto options = CalculateModerateOptions(items);\n\treturn (options.allCanBan || options.allCanDelete)\n\t\t&& !options.participants.empty();\n}\n\nvoid SafeSubmitOnEnter(not_null<Ui::GenericBox*> box) {\n\tbase::install_event_filter(box, [=](not_null<QEvent*> event) {\n\t\tif (event->type() == QEvent::KeyPress) {\n\t\t\tif (const auto k = static_cast<QKeyEvent*>(event.get())) {\n\t\t\t\tif ((k->key() == Qt::Key_Enter)\n\t\t\t\t\t|| (k->key() == Qt::Key_Return)) {\n\t\t\t\t\tbox->uiShow()->show(Ui::MakeConfirmBox({\n\t\t\t\t\t\t.text = tr::lng_gigagroup_warning_title(),\n\t\t\t\t\t\t.confirmed = [=](Fn<void()> close) {\n\t\t\t\t\t\t\tbox->triggerButton(0);\n\t\t\t\t\t\t\tclose();\n\t\t\t\t\t\t},\n\t\t\t\t\t\t.confirmText = tr::lng_box_yes(),\n\t\t\t\t\t\t.cancelText = tr::lng_box_no(),\n\t\t\t\t\t\t}));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n}\n\nvoid DeleteChatBox(not_null<Ui::GenericBox*> box, not_null<PeerData*> peer) {\n\tconst auto container = box->verticalLayout();\n\n\tconst auto userpicPeer = peer->userpicPaintingPeer();\n\tconst auto maybeUser = peer->asUser();\n\tconst auto isBot = maybeUser && maybeUser->isBot();\n\n\tUi::AddSkip(container);\n\tUi::AddSkip(container);\n\n\tSafeSubmitOnEnter(box);\n\n\tconst auto userpic = Ui::CreateChild<Ui::UserpicButton>(\n\t\tcontainer,\n\t\tuserpicPeer,\n\t\tst::mainMenuUserpic,\n\t\tpeer->userpicShape());\n\tuserpic->showSavedMessagesOnSelf(true);\n\tUi::IconWithTitle(\n\t\tcontainer,\n\t\tuserpic,\n\t\tUi::CreateChild<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\tpeer->isSelf()\n\t\t\t\t? tr::lng_saved_messages(tr::bold)\n\t\t\t\t: maybeUser\n\t\t\t\t? tr::lng_profile_delete_conversation(tr::bold)\n\t\t\t\t: rpl::single(\n\t\t\t\t\ttr::bold(userpicPeer->name())\n\t\t\t\t) | rpl::type_erased,\n\t\t\tbox->getDelegate()->style().title));\n\n\tUi::AddSkip(container);\n\tUi::AddSkip(container);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\tpeer->isSelf()\n\t\t\t\t? tr::lng_sure_delete_saved_messages()\n\t\t\t\t: maybeUser\n\t\t\t\t? tr::lng_sure_delete_history(\n\t\t\t\t\tlt_contact,\n\t\t\t\t\trpl::single(peer->name()))\n\t\t\t\t: (peer->isChannel() && !peer->isMegagroup())\n\t\t\t\t? tr::lng_sure_leave_channel()\n\t\t\t\t: tr::lng_sure_leave_group(),\n\t\t\tst::boxLabel));\n\n\tconst auto maybeCheckbox = [&]() -> Ui::Checkbox* {\n\t\tif (!peer->canRevokeFullHistory()) {\n\t\t\treturn nullptr;\n\t\t}\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\treturn box->addRow(\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tcontainer,\n\t\t\t\tmaybeUser\n\t\t\t\t\t? tr::lng_delete_for_other_check(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\tTextWithEntities{ maybeUser->firstName },\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: tr::lng_delete_for_everyone_check(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::marked),\n\t\t\t\tfalse,\n\t\t\t\tst::defaultBoxCheckbox));\n\t}();\n\n\tconst auto maybeBotCheckbox = [&]() -> Ui::Checkbox* {\n\t\tif (!isBot) {\n\t\t\treturn nullptr;\n\t\t}\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\treturn box->addRow(\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_profile_block_bot(tr::now, tr::marked),\n\t\t\t\tfalse,\n\t\t\t\tst::defaultBoxCheckbox));\n\t}();\n\n\tconst auto removeFromChatsFilters = [=](\n\t\t\tnot_null<History*> history) -> std::vector<FilterId> {\n\t\tauto result = std::vector<FilterId>();\n\t\tfor (const auto &filter : peer->owner().chatsFilters().list()) {\n\t\t\tif (filter.withoutAlways(history) != filter) {\n\t\t\t\tresult.push_back(filter.id());\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\n\tconst auto maybeChatsFiltersCheckbox = [&]() -> Ui::Checkbox* {\n\t\tconst auto history = (isBot || !maybeUser)\n\t\t\t? peer->owner().history(peer).get()\n\t\t\t: nullptr;\n\t\tif (!history || removeFromChatsFilters(history).empty()) {\n\t\t\treturn nullptr;\n\t\t}\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\treturn box->addRow(\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tcontainer,\n\t\t\t\t(maybeBotCheckbox\n\t\t\t\t\t? tr::lng_filters_checkbox_remove_bot\n\t\t\t\t\t: (peer->isChannel() && !peer->isMegagroup())\n\t\t\t\t\t? tr::lng_filters_checkbox_remove_channel\n\t\t\t\t\t: tr::lng_filters_checkbox_remove_group)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::marked),\n\t\t\t\tfalse,\n\t\t\t\tst::defaultBoxCheckbox));\n\t}();\n\n\tUi::AddSkip(container);\n\n\tauto buttonText = maybeUser\n\t\t? tr::lng_box_delete()\n\t\t: !maybeCheckbox\n\t\t? tr::lng_box_leave()\n\t\t: maybeCheckbox->checkedValue() | rpl::map([](bool checked) {\n\t\t\treturn checked ? tr::lng_box_delete() : tr::lng_box_leave();\n\t\t}) | rpl::flatten_latest();\n\n\tconst auto close = crl::guard(box, [=] { box->closeBox(); });\n\tbox->addButton(std::move(buttonText), [=] {\n\t\tconst auto revoke = maybeCheckbox && maybeCheckbox->checked();\n\t\tconst auto stopBot = maybeBotCheckbox && maybeBotCheckbox->checked();\n\t\tconst auto removeFromChats = maybeChatsFiltersCheckbox\n\t\t\t&& maybeChatsFiltersCheckbox->checked();\n\t\tCore::App().closeChatFromWindows(peer);\n\t\tif (stopBot) {\n\t\t\tpeer->session().api().blockedPeers().block(peer);\n\t\t}\n\t\tif (removeFromChats) {\n\t\t\tconst auto history = peer->owner().history(peer).get();\n\t\t\tconst auto removeFrom = removeFromChatsFilters(history);\n\t\t\tfor (const auto &filter : peer->owner().chatsFilters().list()) {\n\t\t\t\tif (!ranges::contains(removeFrom, filter.id())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto result = filter.withoutAlways(history);\n\t\t\t\tif (result == filter) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto tl = result.tl();\n\t\t\t\tpeer->owner().chatsFilters().apply(MTP_updateDialogFilter(\n\t\t\t\t\tMTP_flags(MTPDupdateDialogFilter::Flag::f_filter),\n\t\t\t\t\tMTP_int(filter.id()),\n\t\t\t\t\ttl));\n\t\t\t\tpeer->session().api().request(MTPmessages_UpdateDialogFilter(\n\t\t\t\t\tMTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter),\n\t\t\t\t\tMTP_int(filter.id()),\n\t\t\t\t\ttl\n\t\t\t\t)).send();\n\t\t\t}\n\t\t}\n\t\t// Don't delete old history by default,\n\t\t// because Android app doesn't.\n\t\t//\n\t\t//if (const auto from = peer->migrateFrom()) {\n\t\t//\tpeer->session().api().deleteConversation(from, false);\n\t\t//}\n\t\tpeer->session().api().deleteConversation(peer, revoke);\n\t\tclose();\n\t}, st::attentionBoxButton);\n\tbox->addButton(tr::lng_cancel(), close);\n}\n\nvoid DeleteSublistBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Data::SavedSublist*> sublist) {\n\tconst auto container = box->verticalLayout();\n\n\tconst auto weak = base::make_weak(sublist.get());\n\tconst auto peer = sublist->sublistPeer();\n\n\tUi::AddSkip(container);\n\tUi::AddSkip(container);\n\n\tSafeSubmitOnEnter(box);\n\n\tconst auto userpic = Ui::CreateChild<Ui::UserpicButton>(\n\t\tcontainer,\n\t\tpeer,\n\t\tst::mainMenuUserpic);\n\tUi::IconWithTitle(\n\t\tcontainer,\n\t\tuserpic,\n\t\tUi::CreateChild<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_profile_delete_conversation(tr::bold),\n\t\t\tbox->getDelegate()->style().title));\n\n\tUi::AddSkip(container);\n\tUi::AddSkip(container);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_sure_delete_history(\n\t\t\t\tlt_contact,\n\t\t\t\trpl::single(peer->name())),\n\t\t\tst::boxLabel));\n\n\tUi::AddSkip(container);\n\n\tconst auto close = crl::guard(box, [=] { box->closeBox(); });\n\tbox->addButton(tr::lng_box_delete(), [=] {\n\t\tconst auto strong = weak.get();\n\t\tconst auto parentChat = strong ? strong->parentChat() : nullptr;\n\t\tif (!parentChat) {\n\t\t\treturn;\n\t\t}\n\t\tpeer->session().api().deleteSublistHistory(parentChat, peer);\n\t\tclose();\n\t}, st::attentionBoxButton);\n\tbox->addButton(tr::lng_cancel(), close);\n}\n\nModerateMessagesBoxOptions DefaultModerateMessagesBoxOptions() {\n\treturn base::IsCtrlPressed()\n\t\t? ModerateMessagesBoxOptions{\n\t\t\t.reportSpam = true,\n\t\t\t.deleteAll = true,\n\t\t\t.banUser = true,\n\t\t}\n\t\t: ModerateMessagesBoxOptions{};\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/moderate_messages_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\n\nnamespace Data {\nclass SavedSublist;\n} // namespace Data\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nextern const char kModerateCommonGroups[];\n\nstruct ModerateMessagesBoxOptions final {\n\tbool reportSpam = false;\n\tbool deleteAll = false;\n\tbool banUser = false;\n};\n\n[[nodiscard]] ModerateMessagesBoxOptions DefaultModerateMessagesBoxOptions();\n\nvoid CreateModerateMessagesBox(\n\tnot_null<Ui::GenericBox*> box,\n\tconst HistoryItemsList &items,\n\tFn<void()> confirmed,\n\tModerateMessagesBoxOptions options);\n\n[[nodiscard]] bool CanCreateModerateMessagesBox(const HistoryItemsList &);\n\nvoid DeleteChatBox(not_null<Ui::GenericBox*> box, not_null<PeerData*> peer);\nvoid DeleteSublistBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Data::SavedSublist*> sublist);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/passcode_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/passcode_box.h\"\n\n#include \"base/bytes.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"base/unixtime.h\"\n#include \"mainwindow.h\"\n#include \"apiwrap.h\"\n#include \"api/api_cloud_password.h\"\n#include \"main/main_session.h\"\n#include \"main/main_domain.h\"\n#include \"core/application.h\"\n#include \"storage/storage_domain.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/fields/password_input.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"passport/passport_encryption.h\"\n#include \"passport/passport_panel_edit_contact.h\"\n#include \"settings/sections/settings_privacy_security.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_passport.h\"\n#include \"styles/style_boxes.h\"\n#include \"base/qt/qt_common_adapters.h\"\n\nnamespace {\n\nenum class PasswordErrorType {\n\tNone,\n\tNoPassword,\n\tLater,\n};\n\nvoid SetCloudPassword(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session) {\n\tsession->api().cloudPassword().state(\n\t) | rpl::on_next([=] {\n\t\tusing namespace Settings;\n\t\tconst auto weak = base::make_weak(box);\n\t\tif (CheckEditCloudPassword(session)) {\n\t\t\tbox->getDelegate()->show(\n\t\t\t\tEditCloudPasswordBox(session));\n\t\t} else {\n\t\t\tbox->getDelegate()->show(CloudPasswordAppOutdatedBox());\n\t\t}\n\t\tif (weak) {\n\t\t\tweak->closeBox();\n\t\t}\n\t}, box->lifetime());\n}\n\nvoid TransferPasswordError(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tTextWithEntities &&about,\n\t\tPasswordErrorType error) {\n\tbox->setTitle(tr::lng_rights_transfer_check());\n\tbox->setWidth(st::transferCheckWidth);\n\n\tauto text = std::move(about).append('\\n').append('\\n').append(\n\t\ttr::lng_rights_transfer_check_password(\n\t\t\ttr::now,\n\t\t\ttr::rich)\n\t).append('\\n').append('\\n').append(\n\t\ttr::lng_rights_transfer_check_session(\n\t\t\ttr::now,\n\t\t\ttr::rich)\n\t);\n\tif (error == PasswordErrorType::Later) {\n\t\ttext.append('\\n').append('\\n').append(\n\t\t\ttr::lng_rights_transfer_check_later(\n\t\t\t\ttr::now,\n\t\t\t\ttr::rich));\n\t}\n\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\tbox,\n\t\trpl::single(text),\n\t\tst::boxLabel));\n\tif (error == PasswordErrorType::Later) {\n\t\tbox->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });\n\t} else {\n\t\tbox->addButton(tr::lng_rights_transfer_set_password(), [=] {\n\t\t\tSetCloudPassword(box, session);\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t}\n}\n\nvoid StartPendingReset(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<Ui::BoxContent*> context,\n\t\tFn<void()> close) {\n\tconst auto weak = base::make_weak(context.get());\n\tauto lifetime = std::make_shared<rpl::lifetime>();\n\n\tauto finish = [=](const QString &message) mutable {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tif (!message.isEmpty()) {\n\t\t\t\tstrong->getDelegate()->show(Ui::MakeInformBox(message));\n\t\t\t}\n\t\t\tstrong->closeBox();\n\t\t}\n\t\tclose();\n\t\tif (lifetime) {\n\t\t\tbase::take(lifetime)->destroy();\n\t\t}\n\t};\n\n\tsession->api().cloudPassword().resetPassword(\n\t) | rpl::on_next_error_done([=](\n\t\t\tApi::CloudPassword::ResetRetryDate retryDate) {\n\t\tconstexpr auto kMinute = 60;\n\t\tconstexpr auto kHour = 3600;\n\t\tconstexpr auto kDay = 86400;\n\t\tconst auto left = std::max(\n\t\t\tretryDate - base::unixtime::now(),\n\t\t\tkMinute);\n\t\tconst auto days = (left / kDay);\n\t\tconst auto hours = (left / kHour);\n\t\tconst auto minutes = (left / kMinute);\n\t\tconst auto duration = days\n\t\t\t? tr::lng_days(tr::now, lt_count, days)\n\t\t\t: hours\n\t\t\t? tr::lng_hours(tr::now, lt_count, hours)\n\t\t\t: tr::lng_minutes(tr::now, lt_count, minutes);\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->getDelegate()->show(Ui::MakeInformBox(\n\t\t\t\ttr::lng_cloud_password_reset_later(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_duration,\n\t\t\t\t\tduration)));\n\t\t}\n\t}, [=](const QString &error) mutable {\n\t\tfinish(\"Error: \" + error);\n\t}, [=]() mutable {\n\t\tfinish({});\n\t}, *lifetime);\n}\n\n} // namespace\n\nPasscodeBox::CloudFields PasscodeBox::CloudFields::From(\n\t\tconst Core::CloudPasswordState &current) {\n\tauto result = CloudFields();\n\tresult.hasPassword = current.hasPassword;\n\tresult.mtp.curRequest = current.mtp.request;\n\tresult.mtp.newAlgo = current.mtp.newPassword;\n\tresult.mtp.newSecureSecretAlgo = current.mtp.newSecureSecret;\n\tresult.hasRecovery = current.hasRecovery;\n\tresult.notEmptyPassport = current.notEmptyPassport;\n\tresult.hint = current.hint;\n\tresult.pendingResetDate = current.pendingResetDate;\n\treturn result;\n}\n\nPasscodeBox::PasscodeBox(\n\tQWidget*,\n\tnot_null<Main::Session*> session,\n\tbool turningOff)\n: _session(session)\n, _api(&_session->mtp())\n, _textWidth(st::boxWidth - st::boxPadding.left() * 1.5)\n, _turningOff(turningOff)\n, _about(_textWidth)\n, _oldPasscode(this, st::defaultInputField, tr::lng_passcode_enter_old())\n, _newPasscode(\n\tthis,\n\tst::defaultInputField,\n\tsession->domain().local().hasLocalPasscode()\n\t\t? tr::lng_passcode_enter_new()\n\t\t: tr::lng_passcode_enter_first())\n, _reenterPasscode(this, st::defaultInputField, tr::lng_passcode_confirm_new())\n, _passwordHint(this, st::defaultInputField, tr::lng_cloud_password_hint())\n, _recoverEmail(this, st::defaultInputField, tr::lng_cloud_password_email())\n, _recover(this, tr::lng_signin_recover(tr::now)) {\n}\n\nPasscodeBox::PasscodeBox(\n\tQWidget*,\n\tnot_null<MTP::Instance*> mtp,\n\tMain::Session *session,\n\tconst CloudFields &fields)\n: _session(session)\n, _api(mtp)\n, _textWidth(st::boxWidth - st::boxPadding.left() * 1.5)\n, _turningOff(fields.turningOff)\n, _cloudPwd(true)\n, _cloudFields(fields)\n, _about(_textWidth)\n, _oldPasscode(this, st::defaultInputField, tr::lng_cloud_password_enter_old())\n, _newPasscode(\n\tthis,\n\tst::defaultInputField,\n\t(fields.hasPassword\n\t\t? tr::lng_cloud_password_enter_new()\n\t\t: tr::lng_cloud_password_enter_first()))\n, _reenterPasscode(this, st::defaultInputField, tr::lng_cloud_password_confirm_new())\n, _passwordHint(\n\tthis,\n\tst::defaultInputField,\n\t(fields.hasPassword\n\t\t? tr::lng_cloud_password_change_hint()\n\t\t: tr::lng_cloud_password_hint()))\n, _recoverEmail(this, st::defaultInputField, tr::lng_cloud_password_email())\n, _recover(this, tr::lng_signin_recover(tr::now))\n, _showRecoverLink(_cloudFields.hasRecovery || !_cloudFields.pendingResetDate) {\n\tExpects(session != nullptr || !fields.fromRecoveryCode.isEmpty());\n\tExpects(!_turningOff || _cloudFields.hasPassword);\n\n\tif (!_cloudFields.hint.isEmpty()) {\n\t\t_hintText.setText(\n\t\t\tst::passcodeTextStyle,\n\t\t\ttr::lng_signin_hint(tr::now, lt_password_hint, _cloudFields.hint));\n\t}\n}\n\nPasscodeBox::PasscodeBox(\n\tQWidget*,\n\tnot_null<Main::Session*> session,\n\tconst CloudFields &fields)\n: PasscodeBox(nullptr, &session->mtp(), session, fields) {\n}\n\nrpl::producer<QByteArray> PasscodeBox::newPasswordSet() const {\n\treturn _newPasswordSet.events();\n}\n\nrpl::producer<> PasscodeBox::passwordReloadNeeded() const {\n\treturn _passwordReloadNeeded.events();\n}\n\nrpl::producer<> PasscodeBox::clearUnconfirmedPassword() const {\n\treturn _clearUnconfirmedPassword.events();\n}\n\nrpl::producer<MTPauth_Authorization> PasscodeBox::newAuthorization() const {\n\treturn _newAuthorization.events();\n}\n\nbool PasscodeBox::currentlyHave() const {\n\treturn _cloudPwd\n\t\t? _cloudFields.hasPassword\n\t\t: _session->domain().local().hasLocalPasscode();\n}\n\nbool PasscodeBox::onlyCheckCurrent() const {\n\treturn _turningOff || _cloudFields.customCheckCallback;\n}\n\nvoid PasscodeBox::prepare() {\n\taddButton(\n\t\t(_cloudFields.customSubmitButton\n\t\t\t? std::move(_cloudFields.customSubmitButton)\n\t\t\t: _turningOff\n\t\t\t? tr::lng_passcode_remove_button()\n\t\t\t: tr::lng_settings_save()),\n\t\t[=] { save(); });\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\n\t_about.setText(\n\t\tst::passcodeTextStyle,\n\t\t(_cloudFields.customDescription\n\t\t\t? *_cloudFields.customDescription\n\t\t\t: _cloudPwd\n\t\t\t? tr::lng_cloud_password_about(tr::now)\n\t\t\t: tr::lng_passcode_about(tr::now)));\n\t_aboutHeight = _about.countHeight(_textWidth);\n\tconst auto onlyCheck = onlyCheckCurrent();\n\tif (onlyCheck) {\n\t\t_oldPasscode->show();\n\t\tsetTitle(_cloudFields.customTitle\n\t\t\t? std::move(_cloudFields.customTitle)\n\t\t\t: _cloudPwd\n\t\t\t? tr::lng_cloud_password_remove()\n\t\t\t: tr::lng_passcode_remove());\n\t\tsetDimensions(st::boxWidth, st::passcodePadding.top() + _oldPasscode->height() + st::passcodeTextLine + ((_showRecoverLink && !_hintText.isEmpty()) ? st::passcodeTextLine : 0) + st::passcodeAboutSkip + _aboutHeight + st::passcodePadding.bottom());\n\t} else {\n\t\tif (currentlyHave()) {\n\t\t\t_oldPasscode->show();\n\t\t\tsetTitle(_cloudPwd\n\t\t\t\t? tr::lng_cloud_password_change()\n\t\t\t\t: tr::lng_passcode_change());\n\t\t\tsetDimensions(st::boxWidth, st::passcodePadding.top() + _oldPasscode->height() + st::passcodeTextLine + ((_showRecoverLink && !_hintText.isEmpty()) ? st::passcodeTextLine : 0) + _newPasscode->height() + st::passcodeLittleSkip + _reenterPasscode->height() + st::passcodeSkip + (_cloudPwd ? _passwordHint->height() + st::passcodeLittleSkip : 0) + st::passcodeAboutSkip + _aboutHeight + st::passcodePadding.bottom());\n\t\t} else {\n\t\t\t_oldPasscode->hide();\n\t\t\tsetTitle(_cloudPwd\n\t\t\t\t? (_cloudFields.fromRecoveryCode.isEmpty()\n\t\t\t\t\t? tr::lng_cloud_password_create()\n\t\t\t\t\t: tr::lng_cloud_password_change())\n\t\t\t\t: tr::lng_passcode_create());\n\t\t\tsetDimensions(st::boxWidth, st::passcodePadding.top() + _newPasscode->height() + st::passcodeLittleSkip + _reenterPasscode->height() + st::passcodeSkip + (_cloudPwd ? _passwordHint->height() + st::passcodeLittleSkip : 0) + st::passcodeAboutSkip + _aboutHeight + ((_cloudPwd && _cloudFields.fromRecoveryCode.isEmpty()) ? (st::passcodeLittleSkip + _recoverEmail->height() + st::passcodeSkip) : st::passcodePadding.bottom()));\n\t\t}\n\t}\n\n\tconnect(_oldPasscode, &Ui::MaskedInputField::changed, [=] { oldChanged(); });\n\tconnect(_newPasscode, &Ui::MaskedInputField::changed, [=] { newChanged(); });\n\tconnect(_reenterPasscode, &Ui::MaskedInputField::changed, [=] { newChanged(); });\n\t_passwordHint->changes(\n\t) | rpl::on_next([=] {\n\t\tnewChanged();\n\t}, _passwordHint->lifetime());\n\t_recoverEmail->changes(\n\t) | rpl::on_next([=] {\n\t\tif (!_emailError.isEmpty()) {\n\t\t\t_emailError = QString();\n\t\t\tupdate();\n\t\t}\n\t}, _recoverEmail->lifetime());\n\n\tconst auto fieldSubmit = [=] { submit(); };\n\tconnect(_oldPasscode, &Ui::MaskedInputField::submitted, fieldSubmit);\n\tconnect(_newPasscode, &Ui::MaskedInputField::submitted, fieldSubmit);\n\tconnect(_reenterPasscode, &Ui::MaskedInputField::submitted, fieldSubmit);\n\t_passwordHint->submits(\n\t) | rpl::on_next(fieldSubmit, _passwordHint->lifetime());\n\t_recoverEmail->submits(\n\t) | rpl::on_next(fieldSubmit, _recoverEmail->lifetime());\n\n\t_recover->addClickHandler([=] { recoverByEmail(); });\n\n\tconst auto has = currentlyHave();\n\t_oldPasscode->setVisible(onlyCheck || has);\n\t_recover->setVisible((onlyCheck || has)\n\t\t&& _cloudPwd\n\t\t&& _showRecoverLink);\n\t_newPasscode->setVisible(!onlyCheck);\n\t_reenterPasscode->setVisible(!onlyCheck);\n\t_passwordHint->setVisible(!onlyCheck && _cloudPwd);\n\t_recoverEmail->setVisible(!onlyCheck\n\t\t&& _cloudPwd\n\t\t&& !has\n\t\t&& _cloudFields.fromRecoveryCode.isEmpty());\n}\n\nvoid PasscodeBox::submit() {\n\tconst auto has = currentlyHave();\n\tif (_oldPasscode->hasFocus()) {\n\t\tif (onlyCheckCurrent()) {\n\t\t\tsave();\n\t\t} else {\n\t\t\t_newPasscode->setFocus();\n\t\t}\n\t} else if (_newPasscode->hasFocus()) {\n\t\t_reenterPasscode->setFocus();\n\t} else if (_reenterPasscode->hasFocus()) {\n\t\tif (has && _oldPasscode->text().isEmpty()) {\n\t\t\t_oldPasscode->setFocus();\n\t\t\t_oldPasscode->showError();\n\t\t} else if (_newPasscode->text().isEmpty()) {\n\t\t\t_newPasscode->setFocus();\n\t\t\t_newPasscode->showError();\n\t\t} else if (_reenterPasscode->text().isEmpty()) {\n\t\t\t_reenterPasscode->showError();\n\t\t} else if (!_passwordHint->isHidden()) {\n\t\t\t_passwordHint->setFocus();\n\t\t} else {\n\t\t\tsave();\n\t\t}\n\t} else if (_passwordHint->hasFocus()) {\n\t\tif (_recoverEmail->isHidden()) {\n\t\t\tsave();\n\t\t} else {\n\t\t\t_recoverEmail->setFocus();\n\t\t}\n\t} else if (_recoverEmail->hasFocus()) {\n\t\tsave();\n\t}\n}\n\nvoid PasscodeBox::paintEvent(QPaintEvent *e) {\n\tBoxContent::paintEvent(e);\n\n\tPainter p(this);\n\n\tint32 abouty = (_passwordHint->isHidden() ? ((_reenterPasscode->isHidden() ? (_oldPasscode->y() + (_showRecoverLink && !_hintText.isEmpty() ? st::passcodeTextLine : 0)) : _reenterPasscode->y()) + st::passcodeSkip) : _passwordHint->y()) + _oldPasscode->height() + st::passcodeLittleSkip + st::passcodeAboutSkip;\n\tp.setPen(st::boxTextFg);\n\t_about.drawLeft(p, st::boxPadding.left(), abouty, _textWidth, width());\n\n\tif (!_hintText.isEmpty() && _oldError.isEmpty()) {\n\t\t_hintText.drawLeftElided(p, st::boxPadding.left(), _oldPasscode->y() + _oldPasscode->height() + ((st::passcodeTextLine - st::normalFont->height) / 2), _textWidth, width(), 1, style::al_topleft);\n\t}\n\n\tif (!_oldError.isEmpty()) {\n\t\tp.setPen(st::boxTextFgError);\n\t\tp.drawText(QRect(st::boxPadding.left(), _oldPasscode->y() + _oldPasscode->height(), _textWidth, st::passcodeTextLine), _oldError, style::al_left);\n\t}\n\n\tif (!_newError.isEmpty()) {\n\t\tp.setPen(st::boxTextFgError);\n\t\tp.drawText(QRect(st::boxPadding.left(), _reenterPasscode->y() + _reenterPasscode->height(), _textWidth, st::passcodeTextLine), _newError, style::al_left);\n\t}\n\n\tif (!_emailError.isEmpty()) {\n\t\tp.setPen(st::boxTextFgError);\n\t\tp.drawText(QRect(st::boxPadding.left(), _recoverEmail->y() + _recoverEmail->height(), _textWidth, st::passcodeTextLine), _emailError, style::al_left);\n\t}\n}\n\nvoid PasscodeBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\tconst auto has = currentlyHave();\n\tint32 w = st::boxWidth - st::boxPadding.left() - st::boxPadding.right();\n\t_oldPasscode->resize(w, _oldPasscode->height());\n\t_oldPasscode->moveToLeft(st::boxPadding.left(), st::passcodePadding.top());\n\t_newPasscode->resize(w, _newPasscode->height());\n\t_newPasscode->moveToLeft(st::boxPadding.left(), _oldPasscode->y() + ((_turningOff || has) ? (_oldPasscode->height() + st::passcodeTextLine + ((_showRecoverLink && !_hintText.isEmpty()) ? st::passcodeTextLine : 0)) : 0));\n\t_reenterPasscode->resize(w, _reenterPasscode->height());\n\t_reenterPasscode->moveToLeft(st::boxPadding.left(), _newPasscode->y() + _newPasscode->height() + st::passcodeLittleSkip);\n\t_passwordHint->resize(w, _passwordHint->height());\n\t_passwordHint->moveToLeft(st::boxPadding.left(), _reenterPasscode->y() + _reenterPasscode->height() + st::passcodeSkip);\n\t_recoverEmail->resize(w, _passwordHint->height());\n\t_recoverEmail->moveToLeft(st::boxPadding.left(), _passwordHint->y() + _passwordHint->height() + st::passcodeLittleSkip + _aboutHeight + st::passcodeLittleSkip);\n\n\tif (!_recover->isHidden()) {\n\t\t_recover->moveToLeft(st::boxPadding.left(), _oldPasscode->y() + _oldPasscode->height() + (_hintText.isEmpty() ? ((st::passcodeTextLine - _recover->height()) / 2) : st::passcodeTextLine));\n\t}\n}\n\nvoid PasscodeBox::setInnerFocus() {\n\tif (_skipEmailWarning && !_recoverEmail->isHidden()) {\n\t\t_recoverEmail->setFocusFast();\n\t} else if (_oldPasscode->isHidden()) {\n\t\t_newPasscode->setFocusFast();\n\t} else {\n\t\t_oldPasscode->setFocusFast();\n\t}\n}\n\nvoid PasscodeBox::recoverPasswordDone(\n\t\tconst QByteArray &newPasswordBytes,\n\t\tconst MTPauth_Authorization &result) {\n\tif (_replacedBy) {\n\t\t_replacedBy->closeBox();\n\t}\n\t_setRequest = 0;\n\tconst auto weak = base::make_weak(this);\n\t_newAuthorization.fire_copy(result);\n\tif (weak) {\n\t\t_newPasswordSet.fire_copy(newPasswordBytes);\n\t\tif (weak) {\n\t\t\tgetDelegate()->show(Ui::MakeInformBox(\n\t\t\t\ttr::lng_cloud_password_updated()));\n\t\t\tif (weak) {\n\t\t\t\tcloseBox();\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid PasscodeBox::setPasswordDone(const QByteArray &newPasswordBytes) {\n\tif (_replacedBy) {\n\t\t_replacedBy->closeBox();\n\t}\n\t_setRequest = 0;\n\tconst auto weak = base::make_weak(this);\n\t_newPasswordSet.fire_copy(newPasswordBytes);\n\tif (weak) {\n\t\tauto text = _reenterPasscode->isHidden()\n\t\t\t? tr::lng_cloud_password_removed()\n\t\t\t: _oldPasscode->isHidden()\n\t\t\t? tr::lng_cloud_password_was_set()\n\t\t\t: tr::lng_cloud_password_updated();\n\t\tgetDelegate()->show(Ui::MakeInformBox(std::move(text)));\n\t\tif (weak) {\n\t\t\tcloseBox();\n\t\t}\n\t}\n}\n\nvoid PasscodeBox::closeReplacedBy() {\n\tif (isHidden()) {\n\t\tif (_replacedBy && !_replacedBy->isHidden()) {\n\t\t\t_replacedBy->closeBox();\n\t\t}\n\t}\n}\n\nvoid PasscodeBox::setPasswordFail(const QString &type) {\n\tif (MTP::IsFloodError(type)) {\n\t\tcloseReplacedBy();\n\t\t_setRequest = 0;\n\n\t\t_oldPasscode->selectAll();\n\t\t_oldPasscode->setFocus();\n\t\t_oldPasscode->showError();\n\t\t_oldError = tr::lng_flood_error(tr::now);\n\t\tif (_showRecoverLink && _hintText.isEmpty()) {\n\t\t\t_recover->hide();\n\t\t}\n\t\tupdate();\n\t\treturn;\n\t}\n\n\tcloseReplacedBy();\n\t_setRequest = 0;\n\tif (type == u\"PASSWORD_HASH_INVALID\"_q\n\t\t|| type == u\"SRP_PASSWORD_CHANGED\"_q) {\n\t\tif (_oldPasscode->isHidden()) {\n\t\t\t_passwordReloadNeeded.fire({});\n\t\t\tcloseBox();\n\t\t} else {\n\t\t\tbadOldPasscode();\n\t\t}\n\t} else if (type == u\"SRP_ID_INVALID\"_q) {\n\t\thandleSrpIdInvalid();\n\t//} else if (type == u\"NEW_PASSWORD_BAD\"_q) {\n\t//} else if (type == u\"NEW_SALT_INVALID\"_q) {\n\t} else if (type == u\"EMAIL_INVALID\"_q) {\n\t\t_emailError = tr::lng_cloud_password_bad_email(tr::now);\n\t\t_recoverEmail->setFocus();\n\t\t_recoverEmail->showError();\n\t\tupdate();\n\t}\n}\n\nvoid PasscodeBox::setPasswordFail(\n\t\tconst QByteArray &newPasswordBytes,\n\t\tconst QString &email,\n\t\tconst MTP::Error &error) {\n\tconst auto prefix = u\"EMAIL_UNCONFIRMED_\"_q;\n\tif (error.type().startsWith(prefix)) {\n\t\tconst auto codeLength = base::StringViewMid(error.type(), prefix.size()).toInt();\n\n\t\tcloseReplacedBy();\n\t\t_setRequest = 0;\n\n\t\tvalidateEmail(email, codeLength, newPasswordBytes);\n\t} else {\n\t\tsetPasswordFail(error.type());\n\t}\n}\n\nvoid PasscodeBox::validateEmail(\n\t\tconst QString &email,\n\t\tint codeLength,\n\t\tconst QByteArray &newPasswordBytes) {\n\tconst auto errors = std::make_shared<rpl::event_stream<QString>>();\n\tconst auto resent = std::make_shared<rpl::event_stream<QString>>();\n\tconst auto set = std::make_shared<bool>(false);\n\tconst auto submit = crl::guard(this, [=](QString code) {\n\t\tif (_setRequest) {\n\t\t\treturn;\n\t\t}\n\t\t_setRequest = _api.request(MTPaccount_ConfirmPasswordEmail(\n\t\t\tMTP_string(code)\n\t\t)).done([=] {\n\t\t\t*set = true;\n\t\t\tsetPasswordDone(newPasswordBytes);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t_setRequest = 0;\n\t\t\tif (MTP::IsFloodError(error)) {\n\t\t\t\terrors->fire(tr::lng_flood_error(tr::now));\n\t\t\t} else if (error.type() == u\"CODE_INVALID\"_q) {\n\t\t\t\terrors->fire(tr::lng_signin_wrong_code(tr::now));\n\t\t\t} else if (error.type() == u\"EMAIL_HASH_EXPIRED\"_q) {\n\t\t\t\tconst auto weak = base::make_weak(this);\n\t\t\t\t_clearUnconfirmedPassword.fire({});\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->getDelegate()->show(\n\t\t\t\t\t\tUi::MakeInformBox(\n\t\t\t\t\t\t\tLang::Hard::EmailConfirmationExpired()),\n\t\t\t\t\t\tUi::LayerOption::CloseOther);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\terrors->fire(Lang::Hard::ServerError());\n\t\t\t}\n\t\t}).handleFloodErrors().send();\n\t});\n\tconst auto resend = crl::guard(this, [=] {\n\t\tif (_setRequest) {\n\t\t\treturn;\n\t\t}\n\t\t_setRequest = _api.request(MTPaccount_ResendPasswordEmail(\n\t\t)).done([=] {\n\t\t\t_setRequest = 0;\n\t\t\tresent->fire(tr::lng_cloud_password_resent(tr::now));\n\t\t}).fail([=] {\n\t\t\t_setRequest = 0;\n\t\t\terrors->fire(Lang::Hard::ServerError());\n\t\t}).send();\n\t});\n\tconst auto box = _replacedBy = getDelegate()->show(\n\t\tPassport::VerifyEmailBox(\n\t\t\temail,\n\t\t\tcodeLength,\n\t\t\tsubmit,\n\t\t\tresend,\n\t\t\terrors->events(),\n\t\t\tresent->events()));\n\n\tbox->setCloseByOutsideClick(false);\n\tbox->setCloseByEscape(false);\n\tbox->boxClosing(\n\t) | rpl::filter([=] {\n\t\treturn !*set;\n\t}) | on_next([=, weak = base::make_weak(this)] {\n\t\tif (weak) {\n\t\t\tweak->_clearUnconfirmedPassword.fire({});\n\t\t}\n\t\tif (weak) {\n\t\t\tweak->closeBox();\n\t\t}\n\t}, box->lifetime());\n}\n\nvoid PasscodeBox::handleSrpIdInvalid() {\n\tconst auto now = crl::now();\n\tif (_lastSrpIdInvalidTime > 0\n\t\t&& now - _lastSrpIdInvalidTime < Core::kHandleSrpIdInvalidTimeout) {\n\t\t_cloudFields.mtp.curRequest.id = 0;\n\t\t_oldError = Lang::Hard::ServerError();\n\t\tupdate();\n\t} else {\n\t\t_lastSrpIdInvalidTime = now;\n\t\trequestPasswordData();\n\t}\n}\n\nvoid PasscodeBox::save(bool force) {\n\tif (_setRequest) return;\n\n\tQString old = _oldPasscode->text(), pwd = _newPasscode->text(), conf = _reenterPasscode->text();\n\tconst auto has = currentlyHave();\n\tif (!_cloudPwd && (_turningOff || has)) {\n\t\tif (!passcodeCanTry()) {\n\t\t\t_oldError = tr::lng_flood_error(tr::now);\n\t\t\t_oldPasscode->setFocus();\n\t\t\t_oldPasscode->showError();\n\t\t\tupdate();\n\t\t\treturn;\n\t\t}\n\n\t\tif (_session->domain().local().checkPasscode(old.toUtf8())) {\n\t\t\tcSetPasscodeBadTries(0);\n\t\t\tif (_turningOff) pwd = conf = QString();\n\t\t} else {\n\t\t\tcSetPasscodeBadTries(cPasscodeBadTries() + 1);\n\t\t\tcSetPasscodeLastTry(crl::now());\n\t\t\tbadOldPasscode();\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto onlyCheck = onlyCheckCurrent();\n\tif (!onlyCheck && pwd.isEmpty()) {\n\t\t_newPasscode->setFocus();\n\t\t_newPasscode->showError();\n\t\tcloseReplacedBy();\n\t\treturn;\n\t}\n\tif (!onlyCheck && pwd != conf) {\n\t\t_reenterPasscode->selectAll();\n\t\t_reenterPasscode->setFocus();\n\t\t_reenterPasscode->showError();\n\t\tif (!conf.isEmpty()) {\n\t\t\t_newError = _cloudPwd\n\t\t\t\t? tr::lng_cloud_password_differ(tr::now)\n\t\t\t\t: tr::lng_passcode_differ(tr::now);\n\t\t\tupdate();\n\t\t}\n\t\tcloseReplacedBy();\n\t} else if (!onlyCheck && has && old == pwd) {\n\t\t_newPasscode->setFocus();\n\t\t_newPasscode->showError();\n\t\t_newError = _cloudPwd\n\t\t\t? tr::lng_cloud_password_is_same(tr::now)\n\t\t\t: tr::lng_passcode_is_same(tr::now);\n\t\tupdate();\n\t\tcloseReplacedBy();\n\t} else if (_cloudPwd) {\n\t\tQString hint = _passwordHint->getLastText(), email = _recoverEmail->getLastText().trimmed();\n\t\tif (!onlyCheck\n\t\t\t&& !_passwordHint->isHidden()\n\t\t\t&& !_newPasscode->isHidden()\n\t\t\t&& pwd == hint) {\n\t\t\t_newPasscode->setFocus();\n\t\t\t_newPasscode->showError();\n\t\t\t_newError = tr::lng_cloud_password_bad(tr::now);\n\t\t\tupdate();\n\t\t\tcloseReplacedBy();\n\t\t\treturn;\n\t\t}\n\t\tif (!onlyCheck && !_recoverEmail->isHidden() && email.isEmpty() && !force) {\n\t\t\t_skipEmailWarning = true;\n\t\t\t_replacedBy = getDelegate()->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = { tr::lng_cloud_password_about_recover() },\n\t\t\t\t.confirmed = crl::guard(this, [this] { save(true); }),\n\t\t\t\t.confirmText = tr::lng_cloud_password_skip_email(),\n\t\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t}));\n\t\t} else if (onlyCheck) {\n\t\t\tsubmitOnlyCheckCloudPassword(old);\n\t\t} else if (_oldPasscode->isHidden()) {\n\t\t\tsetNewCloudPassword(pwd);\n\t\t} else {\n\t\t\tchangeCloudPassword(old, pwd);\n\t\t}\n\t} else {\n\t\tcloseReplacedBy();\n\t\tconst auto weak = base::make_weak(this);\n\t\tcSetPasscodeBadTries(0);\n\t\t_session->domain().local().setPasscode(pwd.toUtf8());\n\t\tCore::App().localPasscodeChanged();\n\t\tif (weak) {\n\t\t\tcloseBox();\n\t\t}\n\t}\n}\n\nvoid PasscodeBox::submitOnlyCheckCloudPassword(const QString &oldPassword) {\n\tExpects(!_oldPasscode->isHidden());\n\n\tconst auto send = [=] {\n\t\tsendOnlyCheckCloudPassword(oldPassword);\n\t};\n\tif (_cloudFields.turningOff && _cloudFields.notEmptyPassport) {\n\t\tAssert(!_cloudFields.customCheckCallback);\n\n\t\tgetDelegate()->show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_cloud_password_passport_losing(),\n\t\t\t.confirmed = [=](Fn<void()> &&close) { send(); close(); },\n\t\t\t.confirmText = tr::lng_continue(),\n\t\t}));\n\t} else {\n\t\tsend();\n\t}\n}\n\nvoid PasscodeBox::sendOnlyCheckCloudPassword(const QString &oldPassword) {\n\tcheckPassword(oldPassword, [=](const Core::CloudPasswordResult &check) {\n\t\tif (const auto onstack = _cloudFields.customCheckCallback) {\n\t\t\tonstack(check, base::make_weak(this));\n\t\t} else {\n\t\t\tAssert(_cloudFields.turningOff);\n\t\t\tsendClearCloudPassword(check);\n\t\t}\n\t});\n}\n\nvoid PasscodeBox::checkPassword(\n\t\tconst QString &oldPassword,\n\t\tCheckPasswordCallback callback) {\n\tconst auto passwordUtf = oldPassword.toUtf8();\n\t_checkPasswordHash = Core::ComputeCloudPasswordHash(\n\t\t_cloudFields.mtp.curRequest.algo,\n\t\tbytes::make_span(passwordUtf));\n\tcheckPasswordHash(std::move(callback));\n}\n\nvoid PasscodeBox::checkPasswordHash(CheckPasswordCallback callback) {\n\t_checkPasswordCallback = std::move(callback);\n\tif (_cloudFields.mtp.curRequest.id) {\n\t\tpasswordChecked();\n\t} else {\n\t\trequestPasswordData();\n\t}\n}\n\nvoid PasscodeBox::passwordChecked() {\n\tif (!_cloudFields.mtp.curRequest || !_cloudFields.mtp.curRequest.id || !_checkPasswordCallback) {\n\t\treturn serverError();\n\t}\n\tconst auto check = Core::ComputeCloudPasswordCheck(\n\t\t_cloudFields.mtp.curRequest,\n\t\t_checkPasswordHash);\n\tif (!check) {\n\t\treturn serverError();\n\t}\n\t_cloudFields.mtp.curRequest.id = 0;\n\t_checkPasswordCallback(check);\n}\n\nvoid PasscodeBox::requestPasswordData() {\n\tif (!_checkPasswordCallback) {\n\t\treturn serverError();\n\t}\n\n\t_api.request(base::take(_setRequest)).cancel();\n\t_setRequest = _api.request(\n\t\tMTPaccount_GetPassword()\n\t).done([=](const MTPaccount_Password &result) {\n\t\t_setRequest = 0;\n\t\tresult.match([&](const MTPDaccount_password &data) {\n\t\t\t_cloudFields.mtp.curRequest = Core::ParseCloudPasswordCheckRequest(data);\n\t\t\tpasswordChecked();\n\t\t});\n\t}).send();\n}\n\nvoid PasscodeBox::serverError() {\n\tgetDelegate()->show(Ui::MakeInformBox(Lang::Hard::ServerError()));\n\tcloseBox();\n}\n\nbool PasscodeBox::handleCustomCheckError(const MTP::Error &error) {\n\treturn handleCustomCheckError(error.type());\n}\n\nbool PasscodeBox::handleCustomCheckError(const QString &type) {\n\tif (MTP::IsFloodError(type)\n\t\t|| type == u\"PASSWORD_HASH_INVALID\"_q\n\t\t|| type == u\"SRP_PASSWORD_CHANGED\"_q\n\t\t|| type == u\"SRP_ID_INVALID\"_q) {\n\t\tsetPasswordFail(type);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid PasscodeBox::sendClearCloudPassword(\n\t\tconst Core::CloudPasswordResult &check) {\n\tconst auto hint = QString();\n\tconst auto email = QString();\n\tconst auto flags = MTPDaccount_passwordInputSettings::Flag::f_new_algo\n\t\t| MTPDaccount_passwordInputSettings::Flag::f_new_password_hash\n\t\t| MTPDaccount_passwordInputSettings::Flag::f_hint\n\t\t| MTPDaccount_passwordInputSettings::Flag::f_email;\n\t_setRequest = _api.request(MTPaccount_UpdatePasswordSettings(\n\t\tcheck.result,\n\t\tMTP_account_passwordInputSettings(\n\t\t\tMTP_flags(flags),\n\t\t\tCore::PrepareCloudPasswordAlgo(_cloudFields.mtp.newAlgo),\n\t\t\tMTP_bytes(), // new_password_hash\n\t\t\tMTP_string(hint),\n\t\t\tMTP_string(email),\n\t\t\tMTPSecureSecretSettings())\n\t)).done([=] {\n\t\tsetPasswordDone({});\n\t}).fail([=](const MTP::Error &error) mutable {\n\t\tsetPasswordFail({}, QString(), error);\n\t}).handleFloodErrors().send();\n}\n\nvoid PasscodeBox::setNewCloudPassword(const QString &newPassword) {\n\tconst auto newPasswordBytes = newPassword.toUtf8();\n\tconst auto newPasswordHash = Core::ComputeCloudPasswordDigest(\n\t\t_cloudFields.mtp.newAlgo,\n\t\tbytes::make_span(newPasswordBytes));\n\tif (newPasswordHash.modpow.empty()) {\n\t\treturn serverError();\n\t}\n\tconst auto hint = _passwordHint->getLastText();\n\tconst auto email = _recoverEmail->getLastText().trimmed();\n\tusing Flag = MTPDaccount_passwordInputSettings::Flag;\n\tconst auto flags = Flag::f_new_algo\n\t\t| Flag::f_new_password_hash\n\t\t| Flag::f_hint\n\t\t| (_cloudFields.fromRecoveryCode.isEmpty() ? Flag::f_email : Flag(0));\n\t_checkPasswordCallback = nullptr;\n\n\tconst auto settings = MTP_account_passwordInputSettings(\n\t\tMTP_flags(flags),\n\t\tCore::PrepareCloudPasswordAlgo(_cloudFields.mtp.newAlgo),\n\t\tMTP_bytes(newPasswordHash.modpow),\n\t\tMTP_string(hint),\n\t\tMTP_string(email),\n\t\tMTPSecureSecretSettings());\n\tif (_cloudFields.fromRecoveryCode.isEmpty()) {\n\t\t_setRequest = _api.request(MTPaccount_UpdatePasswordSettings(\n\t\t\tMTP_inputCheckPasswordEmpty(),\n\t\t\tsettings\n\t\t)).done([=] {\n\t\t\tsetPasswordDone(newPasswordBytes);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tsetPasswordFail(newPasswordBytes, email, error);\n\t\t}).handleFloodErrors().send();\n\t} else {\n\t\t_setRequest = _api.request(MTPauth_RecoverPassword(\n\t\t\tMTP_flags(MTPauth_RecoverPassword::Flag::f_new_settings),\n\t\t\tMTP_string(_cloudFields.fromRecoveryCode),\n\t\t\tsettings\n\t\t)).done([=](const MTPauth_Authorization &result) {\n\t\t\trecoverPasswordDone(newPasswordBytes, result);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tif (MTP::IsFloodError(error)) {\n\t\t\t\t_newError = tr::lng_flood_error(tr::now);\n\t\t\t\tupdate();\n\t\t\t}\n\t\t\tsetPasswordFail(newPasswordBytes, email, error);\n\t\t}).handleFloodErrors().send();\n\t}\n}\n\nvoid PasscodeBox::changeCloudPassword(\n\t\tconst QString &oldPassword,\n\t\tconst QString &newPassword) {\n\tcheckPassword(oldPassword, [=](const Core::CloudPasswordResult &check) {\n\t\tchangeCloudPassword(oldPassword, check, newPassword);\n\t});\n}\n\nvoid PasscodeBox::changeCloudPassword(\n\t\tconst QString &oldPassword,\n\t\tconst Core::CloudPasswordResult &check,\n\t\tconst QString &newPassword) {\n\t_setRequest = _api.request(MTPaccount_GetPasswordSettings(\n\t\tcheck.result\n\t)).done([=](const MTPaccount_PasswordSettings &result) {\n\t\t_setRequest = 0;\n\n\t\tExpects(result.type() == mtpc_account_passwordSettings);\n\t\tconst auto &data = result.c_account_passwordSettings();\n\n\t\tconst auto wrapped = data.vsecure_settings();\n\t\tif (!wrapped) {\n\t\t\tcheckPasswordHash([=](const Core::CloudPasswordResult &check) {\n\t\t\t\tconst auto empty = QByteArray();\n\t\t\t\tsendChangeCloudPassword(check, newPassword, empty);\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tconst auto &settings = wrapped->c_secureSecretSettings();\n\t\tconst auto passwordUtf = oldPassword.toUtf8();\n\t\tconst auto secret = Passport::DecryptSecureSecret(\n\t\t\tbytes::make_span(settings.vsecure_secret().v),\n\t\t\tCore::ComputeSecureSecretHash(\n\t\t\t\tCore::ParseSecureSecretAlgo(settings.vsecure_algo()),\n\t\t\t\tbytes::make_span(passwordUtf)));\n\t\tif (secret.empty()) {\n\t\t\tLOG((\"API Error: Failed to decrypt secure secret.\"));\n\t\t\tsuggestSecretReset(newPassword);\n\t\t} else if (Passport::CountSecureSecretId(secret)\n\t\t\t\t!= settings.vsecure_secret_id().v) {\n\t\t\tLOG((\"API Error: Wrong secure secret id.\"));\n\t\t\tsuggestSecretReset(newPassword);\n\t\t} else {\n\t\t\tconst auto secureSecret = QByteArray(\n\t\t\t\treinterpret_cast<const char*>(secret.data()),\n\t\t\t\tsecret.size());\n\t\t\tcheckPasswordHash([=](const Core::CloudPasswordResult &check) {\n\t\t\t\tsendChangeCloudPassword(check, newPassword, secureSecret);\n\t\t\t});\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tsetPasswordFail(error.type());\n\t}).handleFloodErrors().send();\n}\n\nvoid PasscodeBox::suggestSecretReset(const QString &newPassword) {\n\tauto resetSecretAndSave = [=](Fn<void()> &&close) {\n\t\tcheckPasswordHash([=, close = std::move(close)](\n\t\t\t\tconst Core::CloudPasswordResult &check) {\n\t\t\tresetSecret(check, newPassword, std::move(close));\n\t\t});\n\t};\n\tgetDelegate()->show(Ui::MakeConfirmBox({\n\t\t.text = { Lang::Hard::PassportCorruptedChange() },\n\t\t.confirmed = std::move(resetSecretAndSave),\n\t\t.confirmText = Lang::Hard::PassportCorruptedReset(),\n\t}));\n}\n\nvoid PasscodeBox::resetSecret(\n\t\tconst Core::CloudPasswordResult &check,\n\t\tconst QString &newPassword,\n\t\tFn<void()> callback) {\n\tusing Flag = MTPDaccount_passwordInputSettings::Flag;\n\t_setRequest = _api.request(MTPaccount_UpdatePasswordSettings(\n\t\tcheck.result,\n\t\tMTP_account_passwordInputSettings(\n\t\t\tMTP_flags(Flag::f_new_secure_settings),\n\t\t\tMTPPasswordKdfAlgo(), // new_algo\n\t\t\tMTPbytes(), // new_password_hash\n\t\t\tMTPstring(), // hint\n\t\t\tMTPstring(), // email\n\t\t\tMTP_secureSecretSettings(\n\t\t\t\tMTP_securePasswordKdfAlgoUnknown(), // secure_algo\n\t\t\t\tMTP_bytes(), // secure_secret\n\t\t\t\tMTP_long(0))) // secure_secret_id\n\t)).done([=] {\n\t\t_setRequest = 0;\n\t\tcallback();\n\t\tcheckPasswordHash([=](const Core::CloudPasswordResult &check) {\n\t\t\tconst auto empty = QByteArray();\n\t\t\tsendChangeCloudPassword(check, newPassword, empty);\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\t_setRequest = 0;\n\t\tif (error.type() == u\"SRP_ID_INVALID\"_q) {\n\t\t\thandleSrpIdInvalid();\n\t\t}\n\t}).send();\n}\n\nvoid PasscodeBox::sendChangeCloudPassword(\n\t\tconst Core::CloudPasswordResult &check,\n\t\tconst QString &newPassword,\n\t\tconst QByteArray &secureSecret) {\n\tconst auto newPasswordBytes = newPassword.toUtf8();\n\tconst auto newPasswordHash = Core::ComputeCloudPasswordDigest(\n\t\t_cloudFields.mtp.newAlgo,\n\t\tbytes::make_span(newPasswordBytes));\n\tif (newPasswordHash.modpow.empty()) {\n\t\treturn serverError();\n\t}\n\tconst auto hint = _passwordHint->getLastText();\n\tauto flags = MTPDaccount_passwordInputSettings::Flag::f_new_algo\n\t\t| MTPDaccount_passwordInputSettings::Flag::f_new_password_hash\n\t\t| MTPDaccount_passwordInputSettings::Flag::f_hint;\n\tauto newSecureSecret = bytes::vector();\n\tauto newSecureSecretId = 0ULL;\n\tif (!secureSecret.isEmpty()) {\n\t\tflags |= MTPDaccount_passwordInputSettings::Flag::f_new_secure_settings;\n\t\tnewSecureSecretId = Passport::CountSecureSecretId(\n\t\t\tbytes::make_span(secureSecret));\n\t\tnewSecureSecret = Passport::EncryptSecureSecret(\n\t\t\tbytes::make_span(secureSecret),\n\t\t\tCore::ComputeSecureSecretHash(\n\t\t\t\t_cloudFields.mtp.newSecureSecretAlgo,\n\t\t\t\tbytes::make_span(newPasswordBytes)));\n\t}\n\t_setRequest = _api.request(MTPaccount_UpdatePasswordSettings(\n\t\tcheck.result,\n\t\tMTP_account_passwordInputSettings(\n\t\t\tMTP_flags(flags),\n\t\t\tCore::PrepareCloudPasswordAlgo(_cloudFields.mtp.newAlgo),\n\t\t\tMTP_bytes(newPasswordHash.modpow),\n\t\t\tMTP_string(hint),\n\t\t\tMTPstring(), // email is not changing\n\t\t\tMTP_secureSecretSettings(\n\t\t\t\tCore::PrepareSecureSecretAlgo(_cloudFields.mtp.newSecureSecretAlgo),\n\t\t\t\tMTP_bytes(newSecureSecret),\n\t\t\t\tMTP_long(newSecureSecretId)))\n\t)).done([=] {\n\t\tsetPasswordDone(newPasswordBytes);\n\t}).fail([=](const MTP::Error &error) {\n\t\tsetPasswordFail(newPasswordBytes, QString(), error);\n\t}).handleFloodErrors().send();\n}\n\nvoid PasscodeBox::badOldPasscode() {\n\t_oldPasscode->selectAll();\n\t_oldPasscode->setFocus();\n\t_oldPasscode->showError();\n\t_oldError = _cloudPwd\n\t\t? tr::lng_cloud_password_wrong(tr::now)\n\t\t: tr::lng_passcode_wrong(tr::now);\n\tif (_showRecoverLink && _hintText.isEmpty()) {\n\t\t_recover->hide();\n\t}\n\tupdate();\n}\n\nvoid PasscodeBox::oldChanged() {\n\tif (!_oldError.isEmpty()) {\n\t\t_oldError = QString();\n\t\tif (_showRecoverLink && _hintText.isEmpty()) {\n\t\t\t_recover->show();\n\t\t}\n\t\tupdate();\n\t}\n}\n\nvoid PasscodeBox::newChanged() {\n\tif (!_newError.isEmpty()) {\n\t\t_newError = QString();\n\t\tupdate();\n\t}\n}\n\nvoid PasscodeBox::recoverByEmail() {\n\tif (!_cloudFields.hasRecovery) {\n\t\tAssert(_session != nullptr);\n\t\tconst auto session = _session;\n\t\tconst auto reset = crl::guard(this, [=](Fn<void()> &&close) {\n\t\t\tStartPendingReset(session, this, std::move(close));\n\t\t});\n\t\tgetDelegate()->show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_cloud_password_reset_no_email(tr::now),\n\t\t\t.confirmed = reset,\n\t\t\t.confirmText = tr::lng_cloud_password_reset_ok(tr::now),\n\t\t}));\n\t} else if (_pattern.isEmpty()) {\n\t\t_pattern = \"-\";\n\t\t_api.request(MTPauth_RequestPasswordRecovery(\n\t\t)).done([=](const MTPauth_PasswordRecovery &result) {\n\t\t\trecoverStarted(result);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\trecoverStartFail(error);\n\t\t}).send();\n\t} else {\n\t\trecover();\n\t}\n}\n\nvoid PasscodeBox::recoverExpired() {\n\t_pattern = QString();\n}\n\nvoid PasscodeBox::recover() {\n\tif (_pattern == \"-\" || !_session) {\n\t\treturn;\n\t}\n\n\tconst auto weak = base::make_weak(this);\n\tconst auto box = getDelegate()->show(Box<RecoverBox>(\n\t\t&_api.instance(),\n\t\t_session,\n\t\t_pattern,\n\t\t_cloudFields,\n\t\t[weak] { if (weak) { weak->closeBox(); } }));\n\n\tbox->newPasswordSet(\n\t) | rpl::start_to_stream(_newPasswordSet, lifetime());\n\n\tbox->recoveryExpired(\n\t) | rpl::on_next([=] {\n\t\trecoverExpired();\n\t}, lifetime());\n\n\t_replacedBy = box;\n}\n\nvoid PasscodeBox::recoverStarted(const MTPauth_PasswordRecovery &result) {\n\t_pattern = qs(result.c_auth_passwordRecovery().vemail_pattern());\n\trecover();\n}\n\nvoid PasscodeBox::recoverStartFail(const MTP::Error &error) {\n\t_pattern = QString();\n\tcloseBox();\n}\n\nRecoverBox::RecoverBox(\n\tQWidget*,\n\tnot_null<MTP::Instance*> mtp,\n\tMain::Session *session,\n\tconst QString &pattern,\n\tconst PasscodeBox::CloudFields &fields,\n\tFn<void()> closeParent)\n: _session(session)\n, _api(mtp)\n, _textWidth(st::boxWidth - st::boxPadding.left() * 1.5)\n, _cloudFields(fields)\n, _recoverCode(this, st::defaultInputField, tr::lng_signin_code())\n, _noEmailAccess(this, tr::lng_signin_try_password(tr::now))\n, _patternLabel(\n\tthis,\n\ttr::lng_signin_recover_hint(\n\t\tlt_recover_email,\n\t\trpl::single(Ui::Text::WrapEmailPattern(pattern)),\n\t\ttr::marked),\n\tst::termsContent,\n\tst::defaultPopupMenu)\n, _closeParent(std::move(closeParent)) {\n\t_patternLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tif (_cloudFields.pendingResetDate != 0 || !session) {\n\t\t_noEmailAccess.destroy();\n\t} else {\n\t\t_noEmailAccess->setClickedCallback([=] {\n\t\t\tconst auto reset = crl::guard(this, [=](Fn<void()> &&close) {\n\t\t\t\tconst auto closeParent = _closeParent;\n\t\t\t\tStartPendingReset(session, this, [=, c = std::move(close)] {\n\t\t\t\t\tif (closeParent) {\n\t\t\t\t\t\tcloseParent();\n\t\t\t\t\t}\n\t\t\t\t\tc();\n\t\t\t\t});\n\t\t\t});\n\t\t\tgetDelegate()->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_cloud_password_reset_with_email(),\n\t\t\t\t.confirmed = reset,\n\t\t\t\t.confirmText = tr::lng_cloud_password_reset_ok(),\n\t\t\t}));\n\t\t});\n\t}\n}\n\nrpl::producer<QByteArray> RecoverBox::newPasswordSet() const {\n\treturn _newPasswordSet.events();\n}\n\nrpl::producer<> RecoverBox::recoveryExpired() const {\n\treturn _recoveryExpired.events();\n}\n\nvoid RecoverBox::updateHeight() {\n\tsetDimensions(\n\t\tst::boxWidth,\n\t\t(st::passcodePadding.top()\n\t\t\t+ st::passcodePadding.bottom()\n\t\t\t+ st::passcodeTextLine\n\t\t\t+ _recoverCode->height()\n\t\t\t+ _patternLabel->height()\n\t\t\t+ st::passcodeTextLine));\n}\n\nvoid RecoverBox::prepare() {\n\tsetTitle(tr::lng_signin_recover_title());\n\n\taddButton(tr::lng_passcode_submit(), [=] { submit(); });\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\n\tupdateHeight();\n\n\t_recoverCode->changes(\n\t) | rpl::on_next([=] {\n\t\tcodeChanged();\n\t}, _recoverCode->lifetime());\n\t_recoverCode->submits(\n\t) | rpl::on_next([=] { submit(); }, _recoverCode->lifetime());\n}\n\nvoid RecoverBox::paintEvent(QPaintEvent *e) {\n\tBoxContent::paintEvent(e);\n\n\tPainter p(this);\n\n\tp.setFont(st::normalFont);\n\tp.setPen(st::boxTextFg);\n\n\tif (!_error.isEmpty()) {\n\t\tp.setPen(st::boxTextFgError);\n\t\tp.drawText(\n\t\t\tQRect(\n\t\t\t\tst::boxPadding.left(),\n\t\t\t\t_recoverCode->y() + _recoverCode->height(),\n\t\t\t\t_textWidth,\n\t\t\t\tst::passcodeTextLine),\n\t\t\t_error,\n\t\t\tstyle::al_left);\n\t}\n}\n\nvoid RecoverBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\t_patternLabel->resizeToWidth(_textWidth);\n\t_patternLabel->moveToLeft(\n\t\tst::boxPadding.left(),\n\t\tst::passcodePadding.top());\n\n\t_recoverCode->resize(\n\t\tst::boxWidth - st::boxPadding.left() - st::boxPadding.right(),\n\t\t_recoverCode->height());\n\t_recoverCode->moveToLeft(\n\t\tst::boxPadding.left(),\n\t\trect::m::sum::v(st::passcodePadding) + _patternLabel->height());\n\tif (_noEmailAccess) {\n\t\t_noEmailAccess->moveToLeft(\n\t\t\tst::boxPadding.left(),\n\t\t\trect::bottom(_recoverCode)\n\t\t\t\t+ (st::passcodeTextLine - _noEmailAccess->height()) / 2);\n\t}\n\n\tupdateHeight();\n}\n\nvoid RecoverBox::setInnerFocus() {\n\t_recoverCode->setFocusFast();\n}\n\nvoid RecoverBox::submit() {\n\tif (_submitRequest) return;\n\n\tQString code = _recoverCode->getLastText().trimmed();\n\tif (code.isEmpty()) {\n\t\t_recoverCode->setFocus();\n\t\t_recoverCode->showError();\n\t\treturn;\n\t}\n\n\tconst auto send = crl::guard(this, [=] {\n\t\tif (_cloudFields.turningOff) {\n\t\t\t// From \"Disable cloud password\".\n\t\t\t_submitRequest = _api.request(MTPauth_RecoverPassword(\n\t\t\t\tMTP_flags(0),\n\t\t\t\tMTP_string(code),\n\t\t\t\tMTPaccount_PasswordInputSettings()\n\t\t\t)).done([=](const MTPauth_Authorization &result) {\n\t\t\t\tproceedToClear();\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tcheckSubmitFail(error);\n\t\t\t}).handleFloodErrors().send();\n\t\t} else {\n\t\t\t// From \"Change cloud password\".\n\t\t\t_submitRequest = _api.request(MTPauth_CheckRecoveryPassword(\n\t\t\t\tMTP_string(code)\n\t\t\t)).done([=] {\n\t\t\t\tproceedToChange(code);\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tcheckSubmitFail(error);\n\t\t\t}).handleFloodErrors().send();\n\t\t}\n\t});\n\tif (_cloudFields.notEmptyPassport) {\n\t\tgetDelegate()->show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_cloud_password_passport_losing(),\n\t\t\t.confirmed = [=](Fn<void()> &&close) { send(); close(); },\n\t\t\t.confirmText = tr::lng_continue(),\n\t\t}));\n\t} else {\n\t\tsend();\n\t}\n}\n\nvoid RecoverBox::setError(const QString &error) {\n\t_error = error;\n\tif (_noEmailAccess) {\n\t\t_noEmailAccess->setVisible(error.isEmpty());\n\t}\n\tupdate();\n}\n\nvoid RecoverBox::codeChanged() {\n\tsetError(QString());\n}\n\nvoid RecoverBox::proceedToClear() {\n\t_submitRequest = 0;\n\t_newPasswordSet.fire({});\n\tgetDelegate()->show(\n\t\tUi::MakeInformBox(tr::lng_cloud_password_removed()),\n\t\tUi::LayerOption::CloseOther);\n}\n\nvoid RecoverBox::proceedToChange(const QString &code) {\n\tExpects(!_cloudFields.turningOff);\n\t_submitRequest = 0;\n\n\tauto fields = _cloudFields;\n\tfields.fromRecoveryCode = code;\n\tfields.hasRecovery = false;\n\t// we could've been turning off, no need to force new password then\n\t// like if (_cloudFields.turningOff) { just RecoverPassword else Check }\n\tfields.mtp.curRequest = {};\n\tfields.hasPassword = false;\n\tfields.customCheckCallback = nullptr;\n\tauto box = Box<PasscodeBox>(_session, fields);\n\n\tbox->boxClosing(\n\t) | rpl::on_next([=] {\n\t\tconst auto weak = base::make_weak(this);\n\t\tif (const auto onstack = _closeParent) {\n\t\t\tonstack();\n\t\t}\n\t\tif (weak) {\n\t\t\tweak->closeBox();\n\t\t}\n\t}, lifetime());\n\n\tbox->newPasswordSet(\n\t) | rpl::on_next([=](QByteArray &&password) {\n\t\t_newPasswordSet.fire(std::move(password));\n\t}, lifetime());\n\n\tgetDelegate()->show(std::move(box));\n}\n\nvoid RecoverBox::checkSubmitFail(const MTP::Error &error) {\n\tif (MTP::IsFloodError(error)) {\n\t\t_submitRequest = 0;\n\t\tsetError(tr::lng_flood_error(tr::now));\n\t\t_recoverCode->showError();\n\t\treturn;\n\t}\n\t_submitRequest = 0;\n\n\tconst QString &err = error.type();\n\tif (err == u\"PASSWORD_EMPTY\"_q) {\n\t\t_newPasswordSet.fire(QByteArray());\n\t\tgetDelegate()->show(\n\t\t\tUi::MakeInformBox(tr::lng_cloud_password_removed()),\n\t\t\tUi::LayerOption::CloseOther);\n\t} else if (err == u\"PASSWORD_RECOVERY_NA\"_q) {\n\t\tcloseBox();\n\t} else if (err == u\"PASSWORD_RECOVERY_EXPIRED\"_q) {\n\t\t_recoveryExpired.fire({});\n\t\tcloseBox();\n\t} else if (err == u\"CODE_INVALID\"_q) {\n\t\tsetError(tr::lng_signin_wrong_code(tr::now));\n\t\t_recoverCode->selectAll();\n\t\t_recoverCode->setFocus();\n\t\t_recoverCode->showError();\n\t} else {\n\t\tsetError(Logs::DebugEnabled() // internal server error\n\t\t\t? (err + \": \" + error.description())\n\t\t\t: Lang::Hard::ServerError());\n\t\t_recoverCode->setFocus();\n\t}\n}\n\nRecoveryEmailValidation ConfirmRecoveryEmail(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &pattern) {\n\tconst auto errors = std::make_shared<rpl::event_stream<QString>>();\n\tconst auto resent = std::make_shared<rpl::event_stream<QString>>();\n\tconst auto requestId = std::make_shared<mtpRequestId>(0);\n\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tconst auto reloads = std::make_shared<rpl::event_stream<>>();\n\tconst auto cancels = std::make_shared<rpl::event_stream<>>();\n\n\tconst auto submit = [=](QString code) {\n\t\tif (*requestId) {\n\t\t\treturn;\n\t\t}\n\t\t*requestId = session->api().request(MTPaccount_ConfirmPasswordEmail(\n\t\t\tMTP_string(code)\n\t\t)).done([=] {\n\t\t\t*requestId = 0;\n\t\t\treloads->fire({});\n\t\t\tif (*weak) {\n\t\t\t\t(*weak)->getDelegate()->show(\n\t\t\t\t\tUi::MakeInformBox(tr::lng_cloud_password_was_set()),\n\t\t\t\t\tUi::LayerOption::CloseOther);\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t*requestId = 0;\n\t\t\tif (MTP::IsFloodError(error)) {\n\t\t\t\terrors->fire(tr::lng_flood_error(tr::now));\n\t\t\t} else if (error.type() == u\"CODE_INVALID\"_q) {\n\t\t\t\terrors->fire(tr::lng_signin_wrong_code(tr::now));\n\t\t\t} else if (error.type() == u\"EMAIL_HASH_EXPIRED\"_q) {\n\t\t\t\tcancels->fire({});\n\t\t\t\tif (*weak) {\n\t\t\t\t\tauto box = Ui::MakeInformBox(\n\t\t\t\t\t\tLang::Hard::EmailConfirmationExpired());\n\t\t\t\t\t(*weak)->getDelegate()->show(\n\t\t\t\t\t\tstd::move(box),\n\t\t\t\t\t\tUi::LayerOption::CloseOther);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\terrors->fire(Lang::Hard::ServerError());\n\t\t\t}\n\t\t}).handleFloodErrors().send();\n\t};\n\tconst auto resend = [=] {\n\t\tif (*requestId) {\n\t\t\treturn;\n\t\t}\n\t\t*requestId = session->api().request(MTPaccount_ResendPasswordEmail(\n\t\t)).done([=] {\n\t\t\t*requestId = 0;\n\t\t\tresent->fire(tr::lng_cloud_password_resent(tr::now));\n\t\t}).fail([=] {\n\t\t\t*requestId = 0;\n\t\t\terrors->fire(Lang::Hard::ServerError());\n\t\t}).send();\n\t};\n\n\tauto box = Passport::VerifyEmailBox(\n\t\tpattern,\n\t\t0,\n\t\tsubmit,\n\t\tresend,\n\t\terrors->events(),\n\t\tresent->events());\n\n\t*weak = box.data();\n\treturn { std::move(box), reloads->events(), cancels->events() };\n}\n\n[[nodiscard]] object_ptr<Ui::GenericBox> PrePasswordErrorBox(\n\t\tconst QString &error,\n\t\tnot_null<Main::Session*> session,\n\t\tTextWithEntities &&about) {\n\tconst auto type = [&] {\n\t\tif (error == u\"PASSWORD_MISSING\"_q) {\n\t\t\treturn PasswordErrorType::NoPassword;\n\t\t} else if (error.startsWith(u\"PASSWORD_TOO_FRESH_\"_q)\n\t\t\t|| error.startsWith(u\"SESSION_TOO_FRESH_\"_q)) {\n\t\t\treturn PasswordErrorType::Later;\n\t\t}\n\t\treturn PasswordErrorType::None;\n\t}();\n\tif (type == PasswordErrorType::None) {\n\t\treturn nullptr;\n\t}\n\n\treturn Box(\n\t\tTransferPasswordError,\n\t\tsession,\n\t\tstd::move(about),\n\t\ttype);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/passcode_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n#include \"mtproto/sender.h\"\n#include \"core/core_cloud_password.h\"\n\nnamespace MTP {\nclass Instance;\n} // namespace MTP\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass InputField;\nclass PasswordInput;\nclass LinkButton;\n} // namespace Ui\n\nnamespace Core {\nstruct CloudPasswordState;\n} // namespace Core\n\nclass PasscodeBox : public Ui::BoxContent {\npublic:\n\tPasscodeBox(QWidget*, not_null<Main::Session*> session, bool turningOff);\n\n\tstruct CloudFields {\n\t\tstatic CloudFields From(const Core::CloudPasswordState &current);\n\n\t\tstruct Mtp {\n\t\t\tCore::CloudPasswordCheckRequest curRequest;\n\t\t\tCore::CloudPasswordAlgo newAlgo;\n\t\t\tCore::SecureSecretAlgo newSecureSecretAlgo;\n\t\t};\n\t\tMtp mtp;\n\t\tbool hasPassword = false;\n\t\tbool hasRecovery = false;\n\t\tQString fromRecoveryCode;\n\t\tbool notEmptyPassport = false;\n\t\tQString hint;\n\t\tbool turningOff = false;\n\t\tTimeId pendingResetDate = 0;\n\n\t\t// Check cloud password for some action.\n\t\tusing CustomCheck = Fn<void(\n\t\t\tconst Core::CloudPasswordResult &,\n\t\t\tbase::weak_qptr<PasscodeBox>)>;\n\t\tCustomCheck customCheckCallback;\n\t\trpl::producer<QString> customTitle;\n\t\tstd::optional<QString> customDescription;\n\t\trpl::producer<QString> customSubmitButton;\n\t};\n\tPasscodeBox(\n\t\tQWidget*,\n\t\tnot_null<MTP::Instance*> mtp,\n\t\tMain::Session *session,\n\t\tconst CloudFields &fields);\n\tPasscodeBox(\n\t\tQWidget*,\n\t\tnot_null<Main::Session*> session,\n\t\tconst CloudFields &fields);\n\n\trpl::producer<QByteArray> newPasswordSet() const;\n\trpl::producer<> passwordReloadNeeded() const;\n\trpl::producer<> clearUnconfirmedPassword() const;\n\n\trpl::producer<MTPauth_Authorization> newAuthorization() const;\n\n\tbool handleCustomCheckError(const MTP::Error &error);\n\tbool handleCustomCheckError(const QString &type);\n\nprotected:\n\tvoid prepare() override;\n\tvoid setInnerFocus() override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tusing CheckPasswordCallback = Fn<void(\n\t\tconst Core::CloudPasswordResult &check)>;\n\n\tvoid submit();\n\tvoid closeReplacedBy();\n\tvoid oldChanged();\n\tvoid newChanged();\n\tvoid save(bool force = false);\n\tvoid badOldPasscode();\n\tvoid recoverByEmail();\n\tvoid recoverExpired();\n\tbool currentlyHave() const;\n\tbool onlyCheckCurrent() const;\n\n\tvoid setPasswordDone(const QByteArray &newPasswordBytes);\n\tvoid recoverPasswordDone(\n\t\tconst QByteArray &newPasswordBytes,\n\t\tconst MTPauth_Authorization &result);\n\tvoid setPasswordFail(const QString &type);\n\tvoid setPasswordFail(\n\t\tconst QByteArray &newPasswordBytes,\n\t\tconst QString &email,\n\t\tconst MTP::Error &error);\n\tvoid validateEmail(\n\t\tconst QString &email,\n\t\tint codeLength,\n\t\tconst QByteArray &newPasswordBytes);\n\n\tvoid recoverStarted(const MTPauth_PasswordRecovery &result);\n\tvoid recoverStartFail(const MTP::Error &error);\n\n\tvoid recover();\n\tvoid submitOnlyCheckCloudPassword(const QString &oldPassword);\n\tvoid setNewCloudPassword(const QString &newPassword);\n\n\tvoid checkPassword(\n\t\tconst QString &oldPassword,\n\t\tCheckPasswordCallback callback);\n\tvoid checkPasswordHash(CheckPasswordCallback callback);\n\n\tvoid changeCloudPassword(\n\t\tconst QString &oldPassword,\n\t\tconst QString &newPassword);\n\tvoid changeCloudPassword(\n\t\tconst QString &oldPassword,\n\t\tconst Core::CloudPasswordResult &check,\n\t\tconst QString &newPassword);\n\n\tvoid sendChangeCloudPassword(\n\t\tconst Core::CloudPasswordResult &check,\n\t\tconst QString &newPassword,\n\t\tconst QByteArray &secureSecret);\n\tvoid suggestSecretReset(const QString &newPassword);\n\tvoid resetSecret(\n\t\tconst Core::CloudPasswordResult &check,\n\t\tconst QString &newPassword,\n\t\tFn<void()> callback);\n\n\tvoid sendOnlyCheckCloudPassword(const QString &oldPassword);\n\tvoid sendClearCloudPassword(const Core::CloudPasswordResult &check);\n\n\tvoid handleSrpIdInvalid();\n\tvoid requestPasswordData();\n\tvoid passwordChecked();\n\tvoid serverError();\n\n\tMain::Session *_session = nullptr;\n\tMTP::Sender _api;\n\tconst int _textWidth;\n\n\tQString _pattern;\n\n\tbase::weak_qptr<Ui::BoxContent> _replacedBy;\n\tbool _turningOff = false;\n\tbool _cloudPwd = false;\n\tCloudFields _cloudFields;\n\tmtpRequestId _setRequest = 0;\n\n\tcrl::time _lastSrpIdInvalidTime = 0;\n\tbool _skipEmailWarning = false;\n\tCheckPasswordCallback _checkPasswordCallback;\n\tbytes::vector _checkPasswordHash;\n\n\tint _aboutHeight = 0;\n\n\tUi::Text::String _about, _hintText;\n\n\tobject_ptr<Ui::PasswordInput> _oldPasscode;\n\tobject_ptr<Ui::PasswordInput> _newPasscode;\n\tobject_ptr<Ui::PasswordInput> _reenterPasscode;\n\tobject_ptr<Ui::InputField> _passwordHint;\n\tobject_ptr<Ui::InputField> _recoverEmail;\n\tobject_ptr<Ui::LinkButton> _recover;\n\tbool _showRecoverLink = false;\n\n\tQString _oldError, _newError, _emailError;\n\n\trpl::event_stream<QByteArray> _newPasswordSet;\n\trpl::event_stream<MTPauth_Authorization> _newAuthorization;\n\trpl::event_stream<> _passwordReloadNeeded;\n\trpl::event_stream<> _clearUnconfirmedPassword;\n\n};\n\nclass RecoverBox final : public Ui::BoxContent {\npublic:\n\tRecoverBox(\n\t\tQWidget*,\n\t\tnot_null<MTP::Instance*> mtp,\n\t\tMain::Session *session,\n\t\tconst QString &pattern,\n\t\tconst PasscodeBox::CloudFields &fields,\n\t\tFn<void()> closeParent = nullptr);\n\n\t[[nodiscard]] rpl::producer<QByteArray> newPasswordSet() const;\n\t[[nodiscard]] rpl::producer<> recoveryExpired() const;\n\n\t//void reloadPassword();\n\t//void recoveryExpired();\n\nprotected:\n\tvoid prepare() override;\n\tvoid setInnerFocus() override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid submit();\n\tvoid codeChanged();\n\tvoid proceedToClear();\n\tvoid proceedToChange(const QString &code);\n\tvoid checkSubmitFail(const MTP::Error &error);\n\tvoid setError(const QString &error);\n\tvoid updateHeight();\n\n\tMain::Session *_session = nullptr;\n\tMTP::Sender _api;\n\tconst int _textWidth;\n\tmtpRequestId _submitRequest = 0;\n\n\tPasscodeBox::CloudFields _cloudFields;\n\n\tobject_ptr<Ui::InputField> _recoverCode;\n\tobject_ptr<Ui::LinkButton> _noEmailAccess;\n\tobject_ptr<Ui::FlatLabel> _patternLabel;\n\tFn<void()> _closeParent;\n\n\tQString _error;\n\n\trpl::event_stream<QByteArray> _newPasswordSet;\n\trpl::event_stream<> _recoveryExpired;\n\n};\n\nstruct RecoveryEmailValidation {\n\tobject_ptr<Ui::BoxContent> box;\n\trpl::producer<> reloadRequests;\n\trpl::producer<> cancelRequests;\n};\n[[nodiscard]] RecoveryEmailValidation ConfirmRecoveryEmail(\n\tnot_null<Main::Session*> session,\n\tconst QString &pattern);\n\n[[nodiscard]] object_ptr<Ui::GenericBox> PrePasswordErrorBox(\n\tconst QString &error,\n\tnot_null<Main::Session*> session,\n\tTextWithEntities &&about);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peer_list_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peer_list_box.h\"\n\n#include \"history/history.h\" // chatListNameSortKey.\n#include \"main/session/session_show.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"ui/effects/loading_element.h\"\n#include \"ui/effects/outline_segments.h\"\n#include \"ui/effects/round_checkbox.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/widgets/multi_select.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"lang/lang_keys.h\"\n#include \"storage/file_download.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"base/unixtime.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_widgets.h\"\n\n#include <xxhash.h> // XXH64.\n#include <QtWidgets/QApplication>\n\n[[nodiscard]] PeerListRowId UniqueRowIdFromString(const QString &d) {\n\treturn XXH64(d.data(), d.size() * sizeof(ushort), 0);\n}\n\nPaintRoundImageCallback PaintUserpicCallback(\n\t\tnot_null<PeerData*> peer,\n\t\tbool respectSavedMessagesChat) {\n\tif (respectSavedMessagesChat) {\n\t\tif (peer->isSelf()) {\n\t\t\treturn [](QPainter &p, int x, int y, int outerWidth, int size) {\n\t\t\t\tUi::EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);\n\t\t\t};\n\t\t} else if (peer->isRepliesChat()) {\n\t\t\treturn [](QPainter &p, int x, int y, int outerWidth, int size) {\n\t\t\t\tUi::EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);\n\t\t\t};\n\t\t}\n\t}\n\tauto userpic = Ui::PeerUserpicView();\n\treturn [=](Painter &p, int x, int y, int outerWidth, int size) mutable {\n\t\tpeer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);\n\t};\n}\n\nPaintRoundImageCallback ForceRoundUserpicCallback(not_null<PeerData*> peer) {\n\tauto userpic = Ui::PeerUserpicView();\n\tauto cache = std::make_shared<QImage>();\n\treturn [=](Painter &p, int x, int y, int outerWidth, int size) mutable {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto cacheSize = QSize(size, size) * ratio;\n\t\tif (cache->size() != cacheSize) {\n\t\t\t*cache = QImage(cacheSize, QImage::Format_ARGB32_Premultiplied);\n\t\t\tcache->setDevicePixelRatio(ratio);\n\t\t}\n\t\tauto q = Painter(cache.get());\n\t\tpeer->paintUserpicLeft(q, userpic, 0, 0, outerWidth, size);\n\t\tq.end();\n\n\t\t*cache = Images::Circle(std::move(*cache));\n\t\tp.drawImage(x, y, *cache);\n\t};\n}\n\nPeerListContentDelegateShow::PeerListContentDelegateShow(\n\tstd::shared_ptr<Main::SessionShow> show)\n: _show(show) {\n}\n\nauto PeerListContentDelegateShow::peerListUiShow()\n-> std::shared_ptr<Main::SessionShow>{\n\treturn _show;\n}\n\nPeerListBox::PeerListBox(\n\tQWidget*,\n\tstd::unique_ptr<PeerListController> controller,\n\tFn<void(not_null<PeerListBox*>)> init)\n: _show(Main::MakeSessionShow(uiShow(), &controller->session()))\n, _controller(std::move(controller))\n, _init(std::move(init)) {\n\tExpects(_controller != nullptr);\n}\n\nvoid PeerListBox::createMultiSelect() {\n\tExpects(_select == nullptr);\n\n\tauto entity = object_ptr<Ui::MultiSelect>(\n\t\tthis,\n\t\t(_controller->selectSt()\n\t\t\t? *_controller->selectSt()\n\t\t\t: st::defaultMultiSelect),\n\t\ttr::lng_participant_filter());\n\t_select.create(this, std::move(entity));\n\t_select->heightValue(\n\t) | rpl::on_next(\n\t\t[this] { updateScrollSkips(); },\n\t\tlifetime());\n\t_select->entity()->setSubmittedCallback([=](Qt::KeyboardModifiers) {\n\t\tcontent()->submitted();\n\t});\n\t_select->entity()->setQueryChangedCallback([=](const QString &query) {\n\t\tif (_customQueryChangedCallback) {\n\t\t\t_customQueryChangedCallback(query);\n\t\t}\n\t\tsearchQueryChanged(query);\n\t});\n\t_select->entity()->setItemRemovedCallback([=](uint64 itemId) {\n\t\tif (_controller->handleDeselectForeignRow(itemId)) {\n\t\t\treturn;\n\t\t}\n\t\tif (const auto peer = _controller->session().data().peerLoaded(PeerId(itemId))) {\n\t\t\tif (const auto row = peerListFindRow(itemId)) {\n\t\t\t\tcontent()->changeCheckState(row, false, anim::type::normal);\n\t\t\t\tupdate();\n\t\t\t}\n\t\t\t_controller->itemDeselectedHook(peer);\n\t\t}\n\t});\n\t_select->resizeToWidth(_controller->contentWidth());\n\t_select->moveToLeft(0, topSelectSkip());\n}\n\nvoid PeerListBox::appendQueryChangedCallback(Fn<void(QString)> callback) {\n\t_customQueryChangedCallback = std::move(callback);\n}\n\nvoid PeerListBox::setAddedTopScrollSkip(int skip, bool aboveSearch) {\n\t_addedTopScrollSkip = skip;\n\t_addedTopScrollAboveSearch = aboveSearch;\n\t_scrollBottomFixed = false;\n\tupdateScrollSkips();\n}\n\nvoid PeerListBox::showFinished() {\n\t_controller->showFinished();\n}\n\nint PeerListBox::topScrollSkip() const {\n\tauto result = _addedTopScrollSkip;\n\tif (_select && !_select->isHidden()) {\n\t\tresult += _select->height();\n\t}\n\treturn result;\n}\n\nint PeerListBox::topSelectSkip() const {\n\treturn _addedTopScrollAboveSearch ? _addedTopScrollSkip : 0;\n}\n\nvoid PeerListBox::updateScrollSkips() {\n\t// If we show / hide the search field scroll top is fixed.\n\t// If we resize search field by bubbles scroll bottom is fixed.\n\tsetInnerTopSkip(topScrollSkip(), _scrollBottomFixed);\n\tif (_select) {\n\t\t_select->moveToLeft(0, topSelectSkip());\n\t\tif (!_select->animating()) {\n\t\t\t_scrollBottomFixed = true;\n\t\t}\n\t}\n}\n\nvoid PeerListBox::prepare() {\n\tsetContent(setInnerWidget(\n\t\tobject_ptr<PeerListContent>(\n\t\t\tthis,\n\t\t\t_controller.get()),\n\t\tst::boxScroll));\n\tcontent()->resizeToWidth(_controller->contentWidth());\n\n\t_controller->setDelegate(this);\n\n\t_controller->boxHeightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tsetDimensions(_controller->contentWidth(), height);\n\t}, lifetime());\n\n\tif (_select) {\n\t\t_select->finishAnimating();\n\t\tUi::SendPendingMoveResizeEvents(_select);\n\t\t_scrollBottomFixed = true;\n\t\tscrollToY(0);\n\t}\n\n\tcontent()->scrollToRequests(\n\t) | rpl::on_next([this](Ui::ScrollToRequest request) {\n\t\tscrollToY(request.ymin, request.ymax);\n\t}, lifetime());\n\n\tif (_init) {\n\t\t_init(this);\n\t}\n\n\t{\n\t\tsetDimensions(\n\t\t\t_controller->contentWidth(),\n\t\t\tstd::clamp(\n\t\t\t\tcontent()->height(),\n\t\t\t\tst::boxMaxListHeight,\n\t\t\t\tst::boxMaxListHeight * 3));\n\t}\n}\n\nvoid PeerListBox::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Down) {\n\t\tcontent()->selectSkip(1);\n\t} else if (e->key() == Qt::Key_Up) {\n\t\tcontent()->selectSkip(-1);\n\t} else if (e->key() == Qt::Key_PageDown) {\n\t\tcontent()->selectSkipPage(height(), 1);\n\t} else if (e->key() == Qt::Key_PageUp) {\n\t\tcontent()->selectSkipPage(height(), -1);\n\t} else if (e->key() == Qt::Key_Escape\n\t\t\t&& _select\n\t\t\t&& !_select->entity()->getQuery().isEmpty()) {\n\t\t_select->entity()->clearQuery();\n\t} else {\n\t\tBoxContent::keyPressEvent(e);\n\t}\n}\n\nvoid PeerListBox::searchQueryChanged(const QString &query) {\n\tscrollToY(0);\n\tcontent()->searchQueryChanged(query);\n}\n\nvoid PeerListBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\tif (_select) {\n\t\t_select->resizeToWidth(width());\n\t\tupdateScrollSkips();\n\t}\n\n\tcontent()->resizeToWidth(width());\n}\n\nvoid PeerListBox::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto &bg = _controller->computeListSt().bg;\n\tconst auto fill = QRect(\n\t\t0,\n\t\t_addedTopScrollSkip,\n\t\twidth(),\n\t\theight() - _addedTopScrollSkip);\n\tfor (const auto &rect : e->region()) {\n\t\tif (const auto part = rect.intersected(fill); !part.isEmpty()) {\n\t\t\tp.fillRect(part, bg);\n\t\t}\n\t}\n}\n\nvoid PeerListBox::setInnerFocus() {\n\tif (!_select || !_select->toggled()) {\n\t\tcontent()->setFocus();\n\t} else {\n\t\t_select->entity()->setInnerFocus();\n\t}\n}\n\nvoid PeerListBox::peerListSetRowChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked) {\n\tif (checked) {\n\t\tif (_controller->trackSelectedList()) {\n\t\t\taddSelectItem(row, anim::type::normal);\n\t\t}\n\t\tPeerListContentDelegate::peerListSetRowChecked(row, checked);\n\t\tpeerListUpdateRow(row);\n\n\t\t// This call deletes row from _searchRows.\n\t\tif (_select) {\n\t\t\t_select->entity()->clearQuery();\n\t\t}\n\t} else {\n\t\t// The itemRemovedCallback will call changeCheckState() here.\n\t\tif (_select) {\n\t\t\t_select->entity()->removeItem(row->id());\n\t\t} else {\n\t\t\tPeerListContentDelegate::peerListSetRowChecked(row, checked);\n\t\t}\n\t\tpeerListUpdateRow(row);\n\t}\n}\n\nvoid PeerListBox::peerListSetForeignRowChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked,\n\t\tanim::type animated) {\n\tif (checked) {\n\t\taddSelectItem(row, animated);\n\n\t\t// This call deletes row from _searchRows.\n\t\t_select->entity()->clearQuery();\n\t} else {\n\t\t// The itemRemovedCallback will call changeCheckState() here.\n\t\t_select->entity()->removeItem(row->id());\n\t}\n}\n\nvoid PeerListBox::peerListScrollToTop() {\n\tscrollToY(0);\n}\n\nvoid PeerListBox::peerListSetSearchMode(PeerListSearchMode mode) {\n\tPeerListContentDelegate::peerListSetSearchMode(mode);\n\n\tauto selectVisible = (mode != PeerListSearchMode::Disabled);\n\tif (selectVisible && !_select) {\n\t\tcreateMultiSelect();\n\t\t_select->toggle(!selectVisible, anim::type::instant);\n\t}\n\tif (_select) {\n\t\t_select->toggle(selectVisible, anim::type::normal);\n\t\t_scrollBottomFixed = false;\n\t\tsetInnerFocus();\n\t}\n}\n\nstd::shared_ptr<Main::SessionShow> PeerListBox::peerListUiShow() {\n\treturn _show;\n}\n\nPeerListController::PeerListController(\n\tstd::unique_ptr<PeerListSearchController> searchController)\n: _searchController(std::move(searchController)) {\n\tif (_searchController) {\n\t\t_searchController->setDelegate(this);\n\t}\n}\n\nconst style::PeerList &PeerListController::computeListSt() const {\n\treturn _listSt ? *_listSt : st::peerListBox;\n}\n\nconst style::MultiSelect &PeerListController::computeSelectSt() const {\n\treturn _selectSt ? *_selectSt : st::defaultMultiSelect;\n}\n\nvoid PeerListController::showFinished() {\n\tif (const auto onstack = _showFinished) {\n\t\tonstack();\n\t}\n}\n\nvoid PeerListController::setShowFinishedCallback(Fn<void()> callback) {\n\t_showFinished = std::move(callback);\n}\n\nbool PeerListController::hasComplexSearch() const {\n\treturn (_searchController != nullptr);\n}\n\nvoid PeerListController::search(const QString &query) {\n\tExpects(hasComplexSearch());\n\n\t_searchController->searchQuery(query);\n}\n\nvoid PeerListController::peerListSearchAddRow(not_null<PeerData*> peer) {\n\tif (auto row = delegate()->peerListFindRow(peer->id.value)) {\n\t\tAssert(row->id() == row->peer()->id.value);\n\t\tdelegate()->peerListAppendFoundRow(row);\n\t} else if (auto row = createSearchRow(peer)) {\n\t\tAssert(row->id() == row->peer()->id.value);\n\t\tdelegate()->peerListAppendSearchRow(std::move(row));\n\t}\n}\n\nvoid PeerListController::peerListSearchAddRow(PeerListRowId id) {\n\tif (auto row = delegate()->peerListFindRow(id)) {\n\t\tdelegate()->peerListAppendFoundRow(row);\n\t} else if (auto row = createSearchRow(id)) {\n\t\tdelegate()->peerListAppendSearchRow(std::move(row));\n\t}\n}\n\nvoid PeerListController::peerListSearchRefreshRows() {\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid PeerListController::setDescriptionText(const QString &text) {\n\tif (text.isEmpty()) {\n\t\tsetDescription(nullptr);\n\t} else {\n\t\tsetDescription(object_ptr<Ui::FlatLabel>(nullptr, text, computeListSt().about));\n\t}\n}\n\nvoid PeerListController::setSearchNoResultsText(const QString &text) {\n\tif (text.isEmpty()) {\n\t\tsetSearchNoResults(nullptr);\n\t} else {\n\t\tsetSearchNoResults(\n\t\t\tobject_ptr<Ui::FlatLabel>(nullptr, text, st::membersAbout));\n\t}\n}\n\nvoid PeerListController::sortByName() {\n\tauto keys = base::flat_map<PeerListRowId, QString>();\n\tkeys.reserve(delegate()->peerListFullRowsCount());\n\tconst auto key = [&](const PeerListRow &row) {\n\t\tconst auto id = row.id();\n\t\tconst auto i = keys.find(id);\n\t\tif (i != end(keys)) {\n\t\t\treturn i->second;\n\t\t}\n\t\tconst auto peer = row.peer();\n\t\tconst auto history = peer->owner().history(peer);\n\t\treturn keys.emplace(\n\t\t\tid,\n\t\t\thistory->chatListNameSortKey()).first->second;\n\t};\n\tconst auto predicate = [&](const PeerListRow &a, const PeerListRow &b) {\n\t\treturn (key(a).compare(key(b)) < 0);\n\t};\n\tdelegate()->peerListSortRows(predicate);\n}\n\nbase::unique_qptr<Ui::PopupMenu> PeerListController::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\treturn nullptr;\n}\n\nstd::unique_ptr<PeerListRow> PeerListController::createSearchRow(\n\t\tPeerListRowId id) {\n\tif (const auto peer = session().data().peerLoaded(PeerId(id))) {\n\t\treturn createSearchRow(peer);\n\t}\n\treturn nullptr;\n}\n\nstd::unique_ptr<PeerListState> PeerListController::saveState() const {\n\treturn delegate()->peerListSaveState();\n}\n\nvoid PeerListController::restoreState(\n\t\tstd::unique_ptr<PeerListState> state) {\n\tdelegate()->peerListRestoreState(std::move(state));\n}\n\nint PeerListController::contentWidth() const {\n\treturn st::boxWideWidth;\n}\n\nrpl::producer<int> PeerListController::boxHeightValue() const {\n\treturn rpl::single(st::boxMaxListHeight);\n}\n\nint PeerListController::descriptionTopSkipMin() const {\n\treturn computeListSt().item.height;\n}\n\nvoid PeerListBox::addSelectItem(\n\t\tnot_null<PeerData*> peer,\n\t\tanim::type animated) {\n\tconst auto respect = !_controller->savedMessagesChatStatus().isEmpty();\n\tconst auto text = (respect && peer->isSelf())\n\t\t? tr::lng_saved_short(tr::now)\n\t\t: (respect && peer->isRepliesChat())\n\t\t? tr::lng_replies_messages(tr::now)\n\t\t: (respect && peer->isVerifyCodes())\n\t\t? tr::lng_verification_codes(tr::now)\n\t\t: peer->shortName();\n\taddSelectItem(\n\t\tpeer->id.value,\n\t\ttext,\n\t\t(peer->isForum()\n\t\t\t? ForceRoundUserpicCallback(peer)\n\t\t\t: PaintUserpicCallback(peer, respect)),\n\t\tanimated);\n}\n\nvoid PeerListBox::addSelectItem(\n\t\tnot_null<PeerListRow*> row,\n\t\tanim::type animated) {\n\taddSelectItem(\n\t\trow->id(),\n\t\trow->generateShortName(),\n\t\trow->generatePaintUserpicCallback(true),\n\t\tanimated);\n}\n\nvoid PeerListBox::addSelectItem(\n\t\tuint64 itemId,\n\t\tconst QString &text,\n\t\tPaintRoundImageCallback paintUserpic,\n\t\tanim::type animated) {\n\tif (!_select) {\n\t\tcreateMultiSelect();\n\t\t_select->hide(anim::type::instant);\n\t}\n\tconst auto &activeBg = (_controller->selectSt()\n\t\t? *_controller->selectSt()\n\t\t: st::defaultMultiSelect).item.textActiveBg;\n\tif (animated == anim::type::instant) {\n\t\t_select->entity()->addItemInBunch(\n\t\t\titemId,\n\t\t\ttext,\n\t\t\tactiveBg,\n\t\t\tstd::move(paintUserpic));\n\t} else {\n\t\t_select->entity()->addItem(\n\t\t\titemId,\n\t\t\ttext,\n\t\t\tactiveBg,\n\t\t\tstd::move(paintUserpic));\n\t}\n}\n\nvoid PeerListBox::peerListFinishSelectedRowsBunch() {\n\tExpects(_select != nullptr);\n\n\t_select->entity()->finishItemsBunch();\n}\n\nbool PeerListBox::peerListIsRowChecked(not_null<PeerListRow*> row) {\n\treturn _select ? _select->entity()->hasItem(row->id()) : false;\n}\n\nint PeerListBox::peerListSelectedRowsCount() {\n\treturn _select ? _select->entity()->getItemsCount() : 0;\n}\n\nstd::vector<PeerListRowId> PeerListBox::collectSelectedIds() {\n\tauto result = std::vector<PeerListRowId>();\n\tauto items = _select\n\t\t? _select->entity()->getItems()\n\t\t: QVector<uint64>();\n\tif (!items.empty()) {\n\t\tresult.reserve(items.size());\n\t\tfor (const auto itemId : items) {\n\t\t\tif (!_controller->isForeignRow(itemId)) {\n\t\t\t\tresult.push_back(itemId);\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nauto PeerListBox::collectSelectedRows()\n-> std::vector<not_null<PeerData*>> {\n\tauto result = std::vector<not_null<PeerData*>>();\n\tauto items = _select\n\t\t? _select->entity()->getItems()\n\t\t: QVector<uint64>();\n\tif (!items.empty()) {\n\t\tresult.reserve(items.size());\n\t\tfor (const auto itemId : items) {\n\t\t\tif (!_controller->isForeignRow(itemId)) {\n\t\t\t\tresult.push_back(_controller->session().data().peer(PeerId(itemId)));\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nrpl::producer<int> PeerListBox::multiSelectHeightValue() const {\n\treturn _select ? _select->heightValue() : rpl::single(0);\n}\n\nrpl::producer<> PeerListBox::noSearchSubmits() const {\n\treturn content()->noSearchSubmits();\n}\n\nPeerListRow::PeerListRow(not_null<PeerData*> peer)\n: PeerListRow(peer, peer->id.value) {\n}\n\nPeerListRow::PeerListRow(not_null<PeerData*> peer, PeerListRowId id)\n: _id(id)\n, _peer(peer) {\n}\n\nPeerListRow::PeerListRow(PeerListRowId id)\n: _id(id) {\n}\n\nPeerListRow::~PeerListRow() = default;\n\nbool PeerListRow::checked() const {\n\treturn _checkbox && _checkbox->checked();\n}\n\nvoid PeerListRow::preloadUserpic() {\n\tif (_peer) {\n\t\t_peer->loadUserpic();\n\t}\n}\n\nvoid PeerListRow::setCustomStatus(const QString &status, bool active) {\n\tsetStatusText(status);\n\t_statusType = active ? StatusType::CustomActive : StatusType::Custom;\n\t_statusValidTill = 0;\n}\n\nvoid PeerListRow::clearCustomStatus() {\n\t_statusType = StatusType::Online;\n\trefreshStatus();\n}\n\nvoid PeerListRow::refreshStatus() {\n\tif (!_initialized\n\t\t|| special()\n\t\t|| _statusType == StatusType::Custom\n\t\t|| _statusType == StatusType::CustomActive) {\n\t\treturn;\n\t}\n\t_statusType = StatusType::LastSeen;\n\t_statusValidTill = 0;\n\tif (auto user = peer()->asUser()) {\n\t\tif (!_savedMessagesStatus.isEmpty()) {\n\t\t\tsetStatusText(_savedMessagesStatus);\n\t\t} else {\n\t\t\tauto time = base::unixtime::now();\n\t\t\tsetStatusText(Data::OnlineText(user, time));\n\t\t\tif (Data::OnlineTextActive(user, time)) {\n\t\t\t\t_statusType = StatusType::Online;\n\t\t\t}\n\t\t\t_statusValidTill = crl::now()\n\t\t\t\t+ Data::OnlineChangeTimeout(user, time);\n\t\t}\n\t} else if (auto chat = peer()->asChat()) {\n\t\tif (!chat->amIn()) {\n\t\t\tsetStatusText(tr::lng_chat_status_unaccessible(tr::now));\n\t\t} else if (chat->count > 0) {\n\t\t\tsetStatusText(tr::lng_chat_status_members(tr::now, lt_count_decimal, chat->count));\n\t\t} else {\n\t\t\tsetStatusText(tr::lng_group_status(tr::now));\n\t\t}\n\t} else if (peer()->isMegagroup()) {\n\t\tsetStatusText(tr::lng_group_status(tr::now));\n\t} else if (peer()->isChannel()) {\n\t\tsetStatusText(tr::lng_channel_status(tr::now));\n\t}\n}\n\ncrl::time PeerListRow::refreshStatusTime() const {\n\treturn _statusValidTill;\n}\n\nvoid PeerListRow::refreshName(const style::PeerListItem &st) {\n\tif (!_initialized) {\n\t\treturn;\n\t}\n\tconst auto text = !_savedMessagesStatus.isEmpty()\n\t\t? tr::lng_saved_messages(tr::now)\n\t\t: _isRepliesMessagesChat\n\t\t? tr::lng_replies_messages(tr::now)\n\t\t: _isVerifyCodesChat\n\t\t? tr::lng_verification_codes(tr::now)\n\t\t: generateName();\n\t_name.setText(st.nameStyle, text, Ui::NameTextOptions());\n}\n\nint PeerListRow::elementsCount() const {\n\treturn 1;\n}\n\nQRect PeerListRow::elementGeometry(int element, int outerWidth) const {\n\tif (element != 1) {\n\t\treturn QRect();\n\t}\n\tconst auto size = rightActionSize();\n\tif (size.isEmpty()) {\n\t\treturn QRect();\n\t}\n\tconst auto margins = rightActionMargins();\n\tconst auto right = margins.right();\n\tconst auto top = margins.top();\n\tconst auto left = outerWidth - right - size.width();\n\treturn QRect(QPoint(left, top), size);\n}\n\nbool PeerListRow::elementDisabled(int element) const {\n\treturn (element == 1) && rightActionDisabled();\n}\n\nbool PeerListRow::elementOnlySelect(int element) const {\n\treturn false;\n}\n\nvoid PeerListRow::elementAddRipple(\n\t\tint element,\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) {\n\tif (element == 1) {\n\t\trightActionAddRipple(point, std::move(updateCallback));\n\t}\n}\n\nvoid PeerListRow::elementsStopLastRipple() {\n\trightActionStopLastRipple();\n}\n\nvoid PeerListRow::elementsPaint(\n\t\tPainter &p,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tint selectedElement) {\n\tconst auto geometry = elementGeometry(1, outerWidth);\n\tif (!geometry.isEmpty()) {\n\t\trightActionPaint(\n\t\t\tp,\n\t\t\tgeometry.x(),\n\t\t\tgeometry.y(),\n\t\t\touterWidth,\n\t\t\tselected,\n\t\t\t(selectedElement == 1));\n\t}\n}\n\nQString PeerListRow::generateName() {\n\treturn peer()->userpicPaintingPeer()->name();\n}\n\nQString PeerListRow::generateShortName() {\n\treturn !_savedMessagesStatus.isEmpty()\n\t\t? tr::lng_saved_short(tr::now)\n\t\t: _isRepliesMessagesChat\n\t\t? tr::lng_replies_messages(tr::now)\n\t\t: _isVerifyCodesChat\n\t\t? tr::lng_verification_codes(tr::now)\n\t\t: peer()->userpicPaintingPeer()->shortName();\n}\n\nUi::PeerUserpicView &PeerListRow::ensureUserpicView() {\n\tif (!_userpic.cloud && peer()->hasUserpic()) {\n\t\t_userpic = peer()->createUserpicView();\n\t}\n\treturn _userpic;\n}\n\nPaintRoundImageCallback PeerListRow::generatePaintUserpicCallback(\n\t\tbool forceRound) {\n\tconst auto saved = !_savedMessagesStatus.isEmpty();\n\tconst auto replies = _isRepliesMessagesChat;\n\tconst auto peer = this->peer();\n\tauto userpic = saved ? Ui::PeerUserpicView() : ensureUserpicView();\n\tif (forceRound && (peer->isForum() || peer->isMonoforum())) {\n\t\treturn ForceRoundUserpicCallback(peer);\n\t}\n\treturn [=](Painter &p, int x, int y, int outerWidth, int size) mutable {\n\t\tusing namespace Ui;\n\t\tif (saved) {\n\t\t\tEmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);\n\t\t} else if (replies) {\n\t\t\tEmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);\n\t\t} else {\n\t\t\tpeer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);\n\t\t}\n\t};\n}\n\n\nauto PeerListRow::generateNameFirstLetters() const\n-> const base::flat_set<QChar> & {\n\treturn peer()->nameFirstLetters();\n}\n\nauto PeerListRow::generateNameWords() const\n-> const base::flat_set<QString> & {\n\treturn peer()->nameWords();\n}\n\nconst style::PeerListItem &PeerListRow::computeSt(\n\t\tconst style::PeerListItem &st) const {\n\treturn st;\n}\n\nvoid PeerListRow::invalidatePixmapsCache() {\n\tif (_checkbox) {\n\t\t_checkbox->invalidateCache();\n\t}\n}\n\nint PeerListRow::paintNameIconGetWidth(\n\t\tPainter &p,\n\t\tFn<void()> repaint,\n\t\tcrl::time now,\n\t\tint nameLeft,\n\t\tint nameTop,\n\t\tint nameWidth,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) {\n\tif (_skipPeerBadge\n\t\t|| special()\n\t\t|| !_savedMessagesStatus.isEmpty()\n\t\t|| _isRepliesMessagesChat\n\t\t|| _isVerifyCodesChat) {\n\t\treturn 0;\n\t}\n\treturn _badge.drawGetWidth(p, {\n\t\t.peer = peer(),\n\t\t.rectForName = QRect(\n\t\t\tnameLeft,\n\t\t\tnameTop,\n\t\t\tavailableWidth,\n\t\t\tst::semiboldFont->height),\n\t\t.nameWidth = nameWidth,\n\t\t.outerWidth = outerWidth,\n\t\t.verified = &(selected\n\t\t\t? st::dialogsVerifiedIconOver\n\t\t\t: st::dialogsVerifiedIcon),\n\t\t.premium = &(selected\n\t\t\t? st::dialogsPremiumIcon.over\n\t\t\t: st::dialogsPremiumIcon.icon),\n\t\t.scam = &(selected ? st::dialogsScamFgOver : st::dialogsScamFg),\n\t\t.direct = &(selected\n\t\t\t? st::windowSubTextFgOver\n\t\t\t: st::windowSubTextFg),\n\t\t.premiumFg = &(selected\n\t\t\t? st::dialogsVerifiedIconBgOver\n\t\t\t: st::dialogsVerifiedIconBg),\n\t\t.customEmojiRepaint = repaint,\n\t\t.now = now,\n\t\t.paused = false,\n\t});\n}\n\nvoid PeerListRow::paintStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) {\n\tauto statusHasOnlineColor = (_statusType == PeerListRow::StatusType::Online)\n\t\t|| (_statusType == PeerListRow::StatusType::CustomActive);\n\tp.setFont(st::contactsStatusFont);\n\tp.setPen(statusHasOnlineColor ? st.statusFgActive : (selected ? st.statusFgOver : st.statusFg));\n\t_status.drawLeftElided(p, x, y, availableWidth, outerWidth);\n}\n\ntemplate <typename MaskGenerator, typename UpdateCallback>\nvoid PeerListRow::addRipple(const style::PeerListItem &st, MaskGenerator &&maskGenerator, QPoint point, UpdateCallback &&updateCallback) {\n\tif (!_ripple) {\n\t\tauto mask = maskGenerator();\n\t\tif (mask.isNull()) {\n\t\t\treturn;\n\t\t}\n\t\t_ripple = std::make_unique<Ui::RippleAnimation>(st.button.ripple, std::move(mask), std::forward<UpdateCallback>(updateCallback));\n\t}\n\t_ripple->add(point);\n}\n\nvoid PeerListRow::stopLastRipple() {\n\tif (_ripple) {\n\t\t_ripple->lastStop();\n\t}\n}\n\nvoid PeerListRow::paintRipple(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth) {\n\tif (_ripple) {\n\t\t_ripple->paint(p, x, y, outerWidth, &st.button.ripple.color->c);\n\t\tif (_ripple->empty()) {\n\t\t\t_ripple.reset();\n\t\t}\n\t}\n}\n\nvoid PeerListRow::paintUserpic(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth) {\n\tif (_disabledState == State::DisabledChecked) {\n\t\tpaintDisabledCheckUserpic(p, st, x, y, outerWidth);\n\t} else if (_checkbox) {\n\t\t_checkbox->paint(p, x, y, outerWidth);\n\t} else if (const auto callback = generatePaintUserpicCallback(false)) {\n\t\tcallback(p, x, y, outerWidth, st.photoSize);\n\t}\n\tpaintUserpicOverlay(p, st, x, y, outerWidth);\n}\n\n// Emulates Ui::RoundImageCheckbox::paint() in a checked state.\nvoid PeerListRow::paintDisabledCheckUserpic(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth) const {\n\tauto userpicRadius = st.checkbox.imageSmallRadius;\n\tauto userpicShift = st.checkbox.imageRadius - userpicRadius;\n\tauto userpicDiameter = st.checkbox.imageRadius * 2;\n\tauto userpicLeft = x + userpicShift;\n\tauto userpicTop = y + userpicShift;\n\tauto userpicEllipse = style::rtlrect(x, y, userpicDiameter, userpicDiameter, outerWidth);\n\tauto userpicBorderPen = st.disabledCheckFg->p;\n\tuserpicBorderPen.setWidth(st.checkbox.selectWidth);\n\n\tauto iconDiameter = st.checkbox.check.size;\n\tauto iconLeft = x + userpicDiameter + st.checkbox.selectWidth - iconDiameter;\n\tauto iconTop = y + userpicDiameter + st.checkbox.selectWidth - iconDiameter;\n\tauto iconEllipse = style::rtlrect(iconLeft, iconTop, iconDiameter, iconDiameter, outerWidth);\n\tauto iconBorderPen = st.checkbox.check.border->p;\n\ticonBorderPen.setWidth(st.checkbox.selectWidth);\n\n\tconst auto size = userpicRadius * 2;\n\tif (!_savedMessagesStatus.isEmpty()) {\n\t\tUi::EmptyUserpic::PaintSavedMessages(p, userpicLeft, userpicTop, outerWidth, size);\n\t} else if (_isRepliesMessagesChat) {\n\t\tUi::EmptyUserpic::PaintRepliesMessages(p, userpicLeft, userpicTop, outerWidth, size);\n\t} else {\n\t\tpeer()->paintUserpicLeft(p, _userpic, userpicLeft, userpicTop, outerWidth, size);\n\t}\n\n\t{\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tp.setPen(userpicBorderPen);\n\t\tp.setBrush(Qt::NoBrush);\n\t\tif (peer()->forum()) {\n\t\t\tconst auto radius = userpicDiameter\n\t\t\t\t* Ui::ForumUserpicRadiusMultiplier();\n\t\t\tp.drawRoundedRect(userpicEllipse, radius, radius);\n\t\t} else {\n\t\t\tp.drawEllipse(userpicEllipse);\n\t\t}\n\n\t\tp.setPen(iconBorderPen);\n\t\tp.setBrush(st.disabledCheckFg);\n\t\tp.drawEllipse(iconEllipse);\n\t}\n\n\tst.checkbox.check.check.paint(p, iconEllipse.topLeft(), outerWidth);\n}\n\nvoid PeerListRow::setStatusText(const QString &text) {\n\t_status.setText(st::defaultTextStyle, text, Ui::NameTextOptions());\n}\n\nfloat64 PeerListRow::checkedRatio() {\n\treturn _checkbox ? _checkbox->checkedAnimationRatio() : 0.;\n}\n\nvoid PeerListRow::lazyInitialize(const style::PeerListItem &st) {\n\tif (_initialized) {\n\t\treturn;\n\t}\n\t_initialized = true;\n\trefreshName(st);\n\trefreshStatus();\n}\n\nbool PeerListRow::useForumLikeUserpic() const {\n\treturn !special() && peer()->isForum();\n}\n\nvoid PeerListRow::createCheckbox(\n\t\tconst style::RoundImageCheckbox &st,\n\t\tFn<void()> updateCallback) {\n\tconst auto generateRadius = [=](int size) {\n\t\treturn useForumLikeUserpic()\n\t\t\t? int(size * Ui::ForumUserpicRadiusMultiplier())\n\t\t\t: std::optional<int>();\n\t};\n\t_checkbox = std::make_unique<Ui::RoundImageCheckbox>(\n\t\tst,\n\t\tstd::move(updateCallback),\n\t\tgeneratePaintUserpicCallback(false),\n\t\tgenerateRadius);\n}\n\nvoid PeerListRow::setCheckedInternal(bool checked, anim::type animated) {\n\tExpects(!checked || _checkbox != nullptr);\n\n\tif (_checkbox) {\n\t\t_checkbox->setChecked(checked, animated);\n\t}\n}\n\nvoid PeerListRow::setCustomizedCheckSegments(\n\t\tstd::vector<Ui::OutlineSegment> segments,\n\t\tbool liveBadge) {\n\tExpects(_checkbox != nullptr);\n\n\t_checkbox->setCustomizedSegments(std::move(segments), liveBadge);\n}\n\nvoid PeerListRow::finishCheckedAnimation() {\n\t_checkbox->setChecked(_checkbox->checked(), anim::type::instant);\n}\n\nPeerListContent::PeerListContent(\n\tQWidget *parent,\n\tnot_null<PeerListController*> controller)\n: RpWidget(parent)\n, _st(controller->computeListSt())\n, _controller(controller)\n, _rowHeight(_st.item.height) {\n\t_controller->session().downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\tusing UpdateFlag = Data::PeerUpdate::Flag;\n\t_controller->session().changes().peerUpdates(\n\t\tUpdateFlag::Name | UpdateFlag::Photo | UpdateFlag::EmojiStatus\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tif (update.flags & UpdateFlag::Name) {\n\t\t\thandleNameChanged(update.peer);\n\t\t}\n\t\tif (update.flags & UpdateFlag::Photo) {\n\t\t\tthis->update();\n\t\t}\n\t}, lifetime());\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tinvalidatePixmapsCache();\n\t}, lifetime());\n\n\t_repaintByStatus.setCallback([this] { update(); });\n}\n\nvoid PeerListContent::setMode(Mode mode) {\n\tif (mode == Mode::Default && _mode == Mode::Default) {\n\t\treturn;\n\t}\n\t_mode = mode;\n\tswitch (_mode) {\n\tcase Mode::Default:\n\t\t_rowHeight = _st.item.height;\n\t\tbreak;\n\tcase Mode::Custom:\n\t\t_rowHeight = _controller->customRowHeight();\n\t\tbreak;\n\t}\n\tconst auto wasMouseSelection = _mouseSelection;\n\tconst auto wasLastMousePosition = _lastMousePosition;\n\t_contextMenu = nullptr;\n\tif (wasMouseSelection) {\n\t\tsetSelected(Selected());\n\t}\n\tsetPressed(Selected());\n\trefreshRows();\n\tif (wasMouseSelection && wasLastMousePosition) {\n\t\tselectByMouse(*wasLastMousePosition);\n\t}\n}\n\nvoid PeerListContent::appendRow(std::unique_ptr<PeerListRow> row) {\n\tExpects(row != nullptr);\n\n\tif (_rowsById.find(row->id()) == _rowsById.cend()) {\n\t\trow->setAbsoluteIndex(_rows.size());\n\t\taddRowEntry(row.get());\n\t\tif (!_hiddenRows.empty()) {\n\t\t\tAssert(!row->hidden());\n\t\t\t_filterResults.push_back(row.get());\n\t\t}\n\t\t_rows.push_back(std::move(row));\n\t}\n}\n\nvoid PeerListContent::appendSearchRow(std::unique_ptr<PeerListRow> row) {\n\tExpects(row != nullptr);\n\tExpects(showingSearch());\n\n\tif (_rowsById.find(row->id()) == _rowsById.cend()) {\n\t\trow->setAbsoluteIndex(_searchRows.size());\n\t\trow->setIsSearchResult(true);\n\t\taddRowEntry(row.get());\n\t\t_filterResults.push_back(row.get());\n\t\t_searchRows.push_back(std::move(row));\n\t}\n}\n\nvoid PeerListContent::appendFoundRow(not_null<PeerListRow*> row) {\n\tExpects(showingSearch());\n\n\tauto index = findRowIndex(row);\n\tif (index.value < 0) {\n\t\t_filterResults.push_back(row);\n\t}\n}\n\nvoid PeerListContent::changeCheckState(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked,\n\t\tanim::type animated) {\n\trow->setChecked(checked, _st.item.checkbox, animated, [=] {\n\t\tupdateRow(row);\n\t});\n}\n\nvoid PeerListContent::setRowHidden(not_null<PeerListRow*> row, bool hidden) {\n\tExpects(!row->isSearchResult());\n\n\trow->setHidden(hidden);\n\tif (hidden) {\n\t\t_hiddenRows.emplace(row);\n\t} else {\n\t\t_hiddenRows.remove(row);\n\t}\n}\n\nvoid PeerListContent::addRowEntry(not_null<PeerListRow*> row) {\n\tconst auto savedMessagesStatus = _controller->savedMessagesChatStatus();\n\tif (!savedMessagesStatus.isEmpty() && !row->special()) {\n\t\tconst auto peer = row->peer();\n\t\tif (peer->isSelf()) {\n\t\t\trow->setSavedMessagesChatStatus(savedMessagesStatus);\n\t\t} else if (peer->isRepliesChat()) {\n\t\t\trow->setIsRepliesMessagesChat(true);\n\t\t} else if (peer->isVerifyCodes()) {\n\t\t\trow->setIsVerifyCodesChat(true);\n\t\t}\n\t}\n\t_rowsById.emplace(row->id(), row);\n\tif (!row->special()) {\n\t\t_rowsByPeer[row->peer()].push_back(row);\n\t}\n\tif (addingToSearchIndex()) {\n\t\taddToSearchIndex(row);\n\t}\n\tif (_controller->isRowSelected(row)) {\n\t\tAssert(row->special() || row->id() == row->peer()->id.value);\n\t\tchangeCheckState(row, true, anim::type::instant);\n\t}\n}\n\nvoid PeerListContent::invalidatePixmapsCache() {\n\tauto invalidate = [](auto &&row) { row->invalidatePixmapsCache(); };\n\tranges::for_each(_rows, invalidate);\n\tranges::for_each(_searchRows, invalidate);\n}\n\nbool PeerListContent::addingToSearchIndex() const {\n\t// If we started indexing already, we continue.\n\treturn (_searchMode != PeerListSearchMode::Disabled) || !_searchIndex.empty();\n}\n\nvoid PeerListContent::addToSearchIndex(not_null<PeerListRow*> row) {\n\tif (row->isSearchResult()) {\n\t\treturn;\n\t}\n\n\tremoveFromSearchIndex(row);\n\trow->setNameFirstLetters(row->generateNameFirstLetters());\n\tfor (auto ch : row->nameFirstLetters()) {\n\t\t_searchIndex[ch].push_back(row);\n\t}\n}\n\nvoid PeerListContent::removeFromSearchIndex(not_null<PeerListRow*> row) {\n\tconst auto &nameFirstLetters = row->nameFirstLetters();\n\tif (!nameFirstLetters.empty()) {\n\t\tfor (auto ch : row->nameFirstLetters()) {\n\t\t\tauto it = _searchIndex.find(ch);\n\t\t\tif (it != _searchIndex.cend()) {\n\t\t\t\tauto &entry = it->second;\n\t\t\t\tentry.erase(ranges::remove(entry, row), end(entry));\n\t\t\t\tif (entry.empty()) {\n\t\t\t\t\t_searchIndex.erase(it);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\trow->setNameFirstLetters({});\n\t}\n}\n\nvoid PeerListContent::prependRow(std::unique_ptr<PeerListRow> row) {\n\tExpects(row != nullptr);\n\n\tif (_rowsById.find(row->id()) == _rowsById.cend()) {\n\t\taddRowEntry(row.get());\n\t\tif (!_hiddenRows.empty()) {\n\t\t\tAssert(!row->hidden());\n\t\t\t_filterResults.insert(_filterResults.begin(), row.get());\n\t\t}\n\t\t_rows.insert(_rows.begin(), std::move(row));\n\t\trefreshIndices();\n\t}\n}\n\nvoid PeerListContent::prependRowFromSearchResult(not_null<PeerListRow*> row) {\n\tif (!row->isSearchResult()) {\n\t\treturn;\n\t}\n\tAssert(_rowsById.find(row->id()) != _rowsById.cend());\n\tauto index = row->absoluteIndex();\n\tAssert(index >= 0 && index < _searchRows.size());\n\tAssert(_searchRows[index].get() == row);\n\n\trow->setIsSearchResult(false);\n\tif (!_hiddenRows.empty()) {\n\t\tAssert(!row->hidden());\n\t\t_filterResults.insert(_filterResults.begin(), row);\n\t}\n\t_rows.insert(_rows.begin(), std::move(_searchRows[index]));\n\trefreshIndices();\n\tremoveRowAtIndex(_searchRows, index);\n\n\tif (addingToSearchIndex()) {\n\t\taddToSearchIndex(row);\n\t}\n}\n\nvoid PeerListContent::refreshIndices() {\n\tauto index = 0;\n\tfor (auto &row : _rows) {\n\t\trow->setAbsoluteIndex(index++);\n\t}\n}\n\nvoid PeerListContent::removeRowAtIndex(\n\t\tstd::vector<std::unique_ptr<PeerListRow>> &from,\n\t\tint index) {\n\tfrom.erase(from.begin() + index);\n\tfor (auto i = index, count = int(from.size()); i != count; ++i) {\n\t\tfrom[i]->setAbsoluteIndex(i);\n\t}\n}\n\nPeerListRow *PeerListContent::findRow(PeerListRowId id) {\n\tauto it = _rowsById.find(id);\n\treturn (it == _rowsById.cend()) ? nullptr : it->second.get();\n}\n\nstd::optional<QPoint> PeerListContent::lastRowMousePosition() const {\n\tif (!_lastMousePosition) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto point = mapFromGlobal(*_lastMousePosition);\n\tauto in = parentWidget()->rect().contains(\n\t\tparentWidget()->mapFromGlobal(*_lastMousePosition));\n\tauto rowsPointY = point.y() - rowsTop();\n\tconst auto index = (in\n\t\t&& rowsPointY >= 0\n\t\t&& rowsPointY < shownRowsCount() * _rowHeight)\n\t\t? (rowsPointY / _rowHeight)\n\t\t: -1;\n\treturn (index >= 0 && index == _selected.index.value)\n\t\t? QPoint(point.x(), rowsPointY)\n\t\t: std::optional<QPoint>();\n}\n\nvoid PeerListContent::removeRow(not_null<PeerListRow*> row) {\n\tauto index = row->absoluteIndex();\n\tauto isSearchResult = row->isSearchResult();\n\tauto &eraseFrom = isSearchResult ? _searchRows : _rows;\n\n\tAssert(index >= 0 && index < eraseFrom.size());\n\tAssert(eraseFrom[index].get() == row);\n\n\tauto pressedData = saveSelectedData(_pressed);\n\tauto contextedData = saveSelectedData(_contexted);\n\tsetSelected(Selected());\n\tsetPressed(Selected());\n\tsetContexted(Selected());\n\n\t_rowsById.erase(row->id());\n\tif (!row->special()) {\n\t\tauto &byPeer = _rowsByPeer[row->peer()];\n\t\tbyPeer.erase(ranges::remove(byPeer, row), end(byPeer));\n\t}\n\tremoveFromSearchIndex(row);\n\t_filterResults.erase(\n\t\tranges::remove(_filterResults, row),\n\t\tend(_filterResults));\n\t_hiddenRows.remove(row);\n\tremoveRowAtIndex(eraseFrom, index);\n\n\trestoreSelection();\n\tsetPressed(restoreSelectedData(pressedData));\n\tsetContexted(restoreSelectedData(contextedData));\n}\n\nvoid PeerListContent::clearAllContent() {\n\tsetSelected(Selected());\n\tsetPressed(Selected());\n\tsetContexted(Selected());\n\t_mouseSelection = false;\n\t_lastMousePosition = std::nullopt;\n\t_rowsById.clear();\n\t_rowsByPeer.clear();\n\t_filterResults.clear();\n\t_searchIndex.clear();\n\t_rows.clear();\n\t_searchRows.clear();\n\t_searchQuery\n\t\t= _normalizedSearchQuery\n\t\t= _mentionHighlight\n\t\t= QString();\n\tif (_controller->hasComplexSearch()) {\n\t\t_controller->search(QString());\n\t}\n}\n\nvoid PeerListContent::convertRowToSearchResult(not_null<PeerListRow*> row) {\n\tif (row->isSearchResult()) {\n\t\treturn;\n\t} else if (!showingSearch() || !_controller->hasComplexSearch()) {\n\t\treturn removeRow(row);\n\t}\n\tauto index = row->absoluteIndex();\n\tAssert(index >= 0 && index < _rows.size());\n\tAssert(_rows[index].get() == row);\n\n\tremoveFromSearchIndex(row);\n\trow->setIsSearchResult(true);\n\trow->setHidden(false);\n\trow->setAbsoluteIndex(_searchRows.size());\n\t_hiddenRows.remove(row);\n\t_searchRows.push_back(std::move(_rows[index]));\n\tremoveRowAtIndex(_rows, index);\n}\n\nint PeerListContent::fullRowsCount() const {\n\treturn _rows.size();\n}\n\nnot_null<PeerListRow*> PeerListContent::rowAt(int index) const {\n\tExpects(index >= 0 && index < _rows.size());\n\n\treturn _rows[index].get();\n}\n\nint PeerListContent::searchRowsCount() const {\n\treturn _searchRows.size();\n}\n\nnot_null<PeerListRow*> PeerListContent::searchRowAt(int index) const {\n\tExpects(index >= 0 && index < _searchRows.size());\n\n\treturn _searchRows[index].get();\n}\n\nvoid PeerListContent::setDescription(object_ptr<Ui::FlatLabel> description) {\n\t_description = std::move(description);\n\tif (_description) {\n\t\t_description->setParent(this);\n\t}\n}\n\nvoid PeerListContent::setSearchLoading(object_ptr<Ui::FlatLabel> loading) {\n\t_searchLoading = std::move(loading);\n\tif (_searchLoading) {\n\t\t_searchLoading->setParent(this);\n\t}\n}\n\nvoid PeerListContent::setSearchNoResults(object_ptr<Ui::FlatLabel> noResults) {\n\t_searchNoResults = std::move(noResults);\n\tif (_searchNoResults) {\n\t\t_searchNoResults->setParent(this);\n\t}\n}\n\nvoid PeerListContent::setAboveWidget(object_ptr<Ui::RpWidget> widget) {\n\t_aboveWidget = std::move(widget);\n\tinitDecorateWidget(_aboveWidget.data());\n}\n\nvoid PeerListContent::setAboveSearchWidget(object_ptr<Ui::RpWidget> widget) {\n\t_aboveSearchWidget = std::move(widget);\n\tinitDecorateWidget(_aboveSearchWidget.data());\n}\n\nvoid PeerListContent::setHideEmpty(bool hide) {\n\t_hideEmpty = hide;\n\tresizeToWidth(width());\n}\n\nvoid PeerListContent::setBelowWidget(object_ptr<Ui::RpWidget> widget) {\n\t_belowWidget = std::move(widget);\n\tinitDecorateWidget(_belowWidget.data());\n}\n\nvoid PeerListContent::initDecorateWidget(Ui::RpWidget *widget) {\n\tif (widget) {\n\t\twidget->setParent(this);\n\t\twidget->events(\n\t\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\t\treturn (e->type() == QEvent::Enter) && widget->isVisible();\n\t\t}) | rpl::on_next([=] {\n\t\t\tmouseLeftGeometry();\n\t\t}, widget->lifetime());\n\t\twidget->heightValue() | rpl::skip(1) | rpl::on_next([=] {\n\t\t\tresizeToWidth(width());\n\t\t}, widget->lifetime());\n\t}\n}\n\nint PeerListContent::labelHeight() const {\n\tif (_hideEmpty && !shownRowsCount()) {\n\t\treturn 0;\n\t}\n\tauto computeLabelHeight = [](auto &label) {\n\t\tif (!label) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn st::membersAboutLimitPadding.top() + label->height() + st::membersAboutLimitPadding.bottom();\n\t};\n\tif (showingSearch()) {\n\t\tif (!_filterResults.empty()) {\n\t\t\treturn 0;\n\t\t}\n\t\tif (_controller->isSearchLoading() && _searchLoading) {\n\t\t\treturn computeLabelHeight(_searchLoading);\n\t\t}\n\t\treturn computeLabelHeight(_searchNoResults);\n\t}\n\treturn computeLabelHeight(_description);\n}\n\nvoid PeerListContent::refreshRows() {\n\tif (!_hiddenRows.empty()) {\n\t\tif (!_ignoreHiddenRowsOnSearch || _normalizedSearchQuery.isEmpty()) {\n\t\t\t_filterResults.clear();\n\t\t\tfor (const auto &row : _rows) {\n\t\t\t\tif (!row->hidden()) {\n\t\t\t\t\t_filterResults.push_back(row.get());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tresizeToWidth(width());\n\tif (_visibleBottom > 0) {\n\t\tcheckScrollForPreload();\n\t}\n\tif (_mouseSelection) {\n\t\tselectByMouse(QCursor::pos());\n\t}\n\tloadProfilePhotos();\n\tupdate();\n}\n\nvoid PeerListContent::setSearchMode(PeerListSearchMode mode) {\n\tif (_searchMode != mode) {\n\t\tif (!addingToSearchIndex()) {\n\t\t\tfor (const auto &row : _rows) {\n\t\t\t\taddToSearchIndex(row.get());\n\t\t\t}\n\t\t}\n\t\t_searchMode = mode;\n\t\tif (_controller->hasComplexSearch()) {\n\t\t\tif (_mode == Mode::Custom) {\n\t\t\t\tif (!_searchLoading) {\n\t\t\t\t\tsetSearchLoading(object_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\tthis,\n\t\t\t\t\t\ttr::lng_contacts_loading(tr::now),\n\t\t\t\t\t\tst::membersAbout));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (!_loadingAnimation) {\n\t\t\t\t\t_loadingAnimation = Ui::CreateLoadingPeerListItemWidget(\n\t\t\t\t\t\tthis,\n\t\t\t\t\t\t_st.item,\n\t\t\t\t\t\t2,\n\t\t\t\t\t\t_controller->computeListSt().bg->c);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tclearSearchRows();\n\t\t}\n\t}\n}\n\nvoid PeerListContent::clearSearchRows() {\n\twhile (!_searchRows.empty()) {\n\t\tremoveRow(_searchRows.back().get());\n\t}\n}\n\nvoid PeerListContent::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tconst auto clip = e->rect();\n\tif (_mode != Mode::Custom) {\n\t\tp.fillRect(clip, _st.item.button.textBg);\n\t}\n\n\tconst auto repaintByStatusAfter = _repaintByStatus.remainingTime();\n\tauto repaintAfterMin = repaintByStatusAfter;\n\n\tconst auto rowsTopCached = rowsTop();\n\tconst auto now = crl::now();\n\tconst auto yFrom = clip.y() - rowsTopCached;\n\tconst auto yTo = clip.y() + clip.height() - rowsTopCached;\n\tp.translate(0, rowsTopCached);\n\tconst auto count = shownRowsCount();\n\tif (count > 0) {\n\t\tconst auto from = floorclamp(yFrom, _rowHeight, 0, count);\n\t\tconst auto to = ceilclamp(yTo, _rowHeight, 0, count);\n\t\tp.translate(0, from * _rowHeight);\n\t\tfor (auto index = from; index != to; ++index) {\n\t\t\tconst auto repaintAfter = paintRow(p, now, RowIndex(index));\n\t\t\tif (repaintAfter > 0\n\t\t\t\t&& (repaintAfterMin < 0\n\t\t\t\t\t|| repaintAfterMin > repaintAfter)) {\n\t\t\t\trepaintAfterMin = repaintAfter;\n\t\t\t}\n\t\t\tp.translate(0, _rowHeight);\n\t\t}\n\t}\n\tif (repaintAfterMin != repaintByStatusAfter) {\n\t\tAssert(repaintAfterMin >= 0);\n\t\t_repaintByStatus.callOnce(repaintAfterMin);\n\t}\n}\n\nint PeerListContent::resizeGetHeight(int newWidth) {\n\tconst auto rowsCount = shownRowsCount();\n\tconst auto hideAll = !rowsCount && _hideEmpty;\n\t_aboveHeight = 0;\n\tif (_aboveWidget) {\n\t\t_aboveWidget->resizeToWidth(newWidth);\n\t\t_aboveWidget->moveToLeft(0, 0, newWidth);\n\t\tif (hideAll || showingSearch()) {\n\t\t\t_aboveWidget->hide();\n\t\t} else {\n\t\t\t_aboveWidget->show();\n\t\t\t_aboveHeight = _aboveWidget->height();\n\t\t}\n\t}\n\tif (_aboveSearchWidget) {\n\t\t_aboveSearchWidget->resizeToWidth(newWidth);\n\t\t_aboveSearchWidget->moveToLeft(0, 0, newWidth);\n\t\tif (hideAll || !showingSearch()) {\n\t\t\t_aboveSearchWidget->hide();\n\t\t} else {\n\t\t\t_aboveSearchWidget->show();\n\t\t\t_aboveHeight = _aboveSearchWidget->height();\n\t\t}\n\t}\n\tconst auto labelTop = rowsTop()\n\t\t+ std::max(\n\t\t\tshownRowsCount() * _rowHeight,\n\t\t\t_controller->descriptionTopSkipMin());\n\tconst auto labelWidth = newWidth - 2 * st::contactsPadding.left();\n\tif (_description) {\n\t\t_description->resizeToWidth(labelWidth);\n\t\t_description->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top(), newWidth);\n\t\t_description->setVisible(!hideAll && !showingSearch());\n\t}\n\tif (_searchNoResults) {\n\t\t_searchNoResults->resizeToWidth(labelWidth);\n\t\t_searchNoResults->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top(), newWidth);\n\t\t_searchNoResults->setVisible(!hideAll && showingSearch() && _filterResults.empty() && !_controller->isSearchLoading());\n\t}\n\tif (_searchLoading) {\n\t\t_searchLoading->resizeToWidth(labelWidth);\n\t\t_searchLoading->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top(), newWidth);\n\t\t_searchLoading->setVisible(!hideAll && showingSearch() && _filterResults.empty() && _controller->isSearchLoading());\n\t}\n\tif (_loadingAnimation) {\n\t\t_loadingAnimation->resizeToWidth(newWidth);\n\t\t_loadingAnimation->moveToLeft(0, rowsTop(), newWidth);\n\t\t_loadingAnimation->setVisible(!hideAll\n\t\t\t&& showingSearch()\n\t\t\t&& _filterResults.empty()\n\t\t\t&& _controller->isSearchLoading());\n\t}\n\tconst auto label = labelHeight();\n\tconst auto belowTop = (label > 0 || rowsCount > 0)\n\t\t? (labelTop + label + _st.padding.bottom())\n\t\t: _aboveHeight;\n\t_belowHeight = 0;\n\tif (_belowWidget) {\n\t\t_belowWidget->resizeToWidth(newWidth);\n\t\t_belowWidget->moveToLeft(0, belowTop, newWidth);\n\t\tif (hideAll || showingSearch()) {\n\t\t\t_belowWidget->hide();\n\t\t} else {\n\t\t\t_belowWidget->show();\n\t\t\t_belowHeight = _belowWidget->height();\n\t\t}\n\t}\n\treturn belowTop + _belowHeight;\n}\n\nvoid PeerListContent::enterEventHook(QEnterEvent *e) {\n\tsetMouseTracking(true);\n}\n\nvoid PeerListContent::leaveEventHook(QEvent *e) {\n\tsetMouseTracking(false);\n\tmouseLeftGeometry();\n}\n\nvoid PeerListContent::mouseMoveEvent(QMouseEvent *e) {\n\thandleMouseMove(e->globalPos());\n}\n\nvoid PeerListContent::handleMouseMove(QPoint globalPosition) {\n\tif (!_lastMousePosition) {\n\t\t_lastMousePosition = globalPosition;\n\t\treturn;\n\t} else if (!_mouseSelection\n\t\t&& *_lastMousePosition == globalPosition) {\n\t\treturn;\n\t}\n\tif (_trackPressStart\n\t\t&& ((*_trackPressStart - globalPosition).manhattanLength()\n\t\t\t> QApplication::startDragDistance())) {\n\t\t_trackPressStart = {};\n\t\t_controller->rowTrackPressCancel();\n\t}\n\tif (!_controller->rowTrackPressSkipMouseSelection()) {\n\t\tselectByMouse(globalPosition);\n\t}\n}\n\nvoid PeerListContent::pressLeftToContextMenu(bool shown) {\n\tif (shown) {\n\t\tsetContexted(_pressed);\n\t\tsetPressed(Selected());\n\t} else {\n\t\tsetContexted(Selected());\n\t}\n}\n\nbool PeerListContent::trackRowPressFromGlobal(QPoint globalPosition) {\n\tselectByMouse(globalPosition);\n\tif (const auto row = getRow(_selected.index)) {\n\t\tif (_controller->rowTrackPress(row)) {\n\t\t\t_trackPressStart = globalPosition;\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid PeerListContent::mousePressEvent(QMouseEvent *e) {\n\t_pressButton = e->button();\n\tselectByMouse(e->globalPos());\n\tsetPressed(_selected);\n\t_trackPressStart = {};\n\tif (const auto row = getRow(_selected.index)) {\n\t\tconst auto updateCallback = [this, row, hint = _selected.index] {\n\t\t\tupdateRow(row, hint);\n\t\t};\n\t\tif (_selected.element) {\n\t\t\tconst auto elementRect = getElementRect(\n\t\t\t\trow,\n\t\t\t\t_selected.index,\n\t\t\t\t_selected.element);\n\t\t\tif (!elementRect.isEmpty()) {\n\t\t\t\trow->elementAddRipple(\n\t\t\t\t\t_selected.element,\n\t\t\t\t\tmapFromGlobal(QCursor::pos()) - elementRect.topLeft(),\n\t\t\t\t\tstd::move(updateCallback));\n\t\t\t}\n\t\t} else {\n\t\t\tauto point = mapFromGlobal(QCursor::pos()) - QPoint(0, getRowTop(_selected.index));\n\t\t\tif (_mode == Mode::Custom) {\n\t\t\t\trow->addRipple(_st.item, _controller->customRowRippleMaskGenerator(), point, std::move(updateCallback));\n\t\t\t} else {\n\t\t\t\tconst auto maskGenerator = [&] {\n\t\t\t\t\treturn Ui::RippleAnimation::RectMask(\n\t\t\t\t\t\tQSize(width(), _rowHeight));\n\t\t\t\t};\n\t\t\t\trow->addRipple(_st.item, maskGenerator, point, std::move(updateCallback));\n\t\t\t}\n\t\t}\n\t\tif (_pressButton == Qt::LeftButton && _controller->rowTrackPress(row)) {\n\t\t\t_trackPressStart = e->globalPos();\n\t\t}\n\t}\n\tif (anim::Disabled() && !_trackPressStart && !_selected.element) {\n\t\tmousePressReleased(e->button());\n\t}\n}\n\nvoid PeerListContent::mouseReleaseEvent(QMouseEvent *e) {\n\tmousePressReleased(e->button());\n}\n\nvoid PeerListContent::mousePressReleased(Qt::MouseButton button) {\n\t_trackPressStart = {};\n\t_controller->rowTrackPressCancel();\n\n\tupdateRow(_pressed.index);\n\tupdateRow(_selected.index);\n\n\tauto pressed = _pressed;\n\tsetPressed(Selected());\n\tif (button == Qt::LeftButton && pressed == _selected) {\n\t\tif (auto row = getRow(pressed.index)) {\n\t\t\tif (pressed.element) {\n\t\t\t\t_controller->rowElementClicked(row, pressed.element);\n\t\t\t} else {\n\t\t\t\t_controller->rowClicked(row);\n\t\t\t}\n\t\t}\n\t} else if (button == Qt::MiddleButton && pressed == _selected) {\n\t\tif (auto row = getRow(pressed.index)) {\n\t\t\t_controller->rowMiddleClicked(row);\n\t\t}\n\t}\n}\n\nvoid PeerListContent::showRowMenu(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool highlightRow,\n\t\tFn<void(not_null<Ui::PopupMenu*>)> destroyed) {\n\tconst auto index = findRowIndex(row);\n\tshowRowMenu(\n\t\tindex,\n\t\trow,\n\t\tQCursor::pos(),\n\t\thighlightRow,\n\t\tstd::move(destroyed));\n}\n\nbool PeerListContent::showRowMenu(\n\t\tRowIndex index,\n\t\tPeerListRow *row,\n\t\tQPoint globalPos,\n\t\tbool highlightRow,\n\t\tFn<void(not_null<Ui::PopupMenu*>)> destroyed) {\n\tif (_contextMenu) {\n\t\t_contextMenu->setDestroyedCallback(nullptr);\n\t\t_contextMenu = nullptr;\n\t}\n\tsetContexted(Selected());\n\tif (_pressButton != Qt::LeftButton) {\n\t\tmousePressReleased(_pressButton);\n\t}\n\n\tif (highlightRow) {\n\t\trow = getRow(index);\n\t}\n\tif (!row) {\n\t\treturn false;\n\t}\n\n\t_contextMenu = _controller->rowContextMenu(this, row);\n\tconst auto raw = _contextMenu.get();\n\tif (!raw) {\n\t\treturn false;\n\t}\n\n\tif (highlightRow) {\n\t\tsetContexted({ index, false });\n\t}\n\traw->setDestroyedCallback(crl::guard(\n\t\tthis,\n\t\t[=] {\n\t\t\tif (highlightRow) {\n\t\t\t\tsetContexted(Selected());\n\t\t\t}\n\t\t\thandleMouseMove(QCursor::pos());\n\t\t\tif (destroyed) {\n\t\t\t\tdestroyed(raw);\n\t\t\t}\n\t\t}));\n\traw->popup(globalPos);\n\treturn true;\n}\n\nvoid PeerListContent::contextMenuEvent(QContextMenuEvent *e) {\n\tif (e->reason() == QContextMenuEvent::Mouse) {\n\t\thandleMouseMove(e->globalPos());\n\t}\n\tif (showRowMenu(_selected.index, nullptr, e->globalPos(), true)) {\n\t\te->accept();\n\t}\n}\n\nvoid PeerListContent::setPressed(Selected pressed) {\n\tif (_pressed == pressed) {\n\t\treturn;\n\t} else if (const auto row = getRow(_pressed.index)) {\n\t\trow->stopLastRipple();\n\t\trow->elementsStopLastRipple();\n\t}\n\t_pressed = pressed;\n}\n\ncrl::time PeerListContent::paintRow(\n\t\tPainter &p,\n\t\tcrl::time now,\n\t\tRowIndex index) {\n\tconst auto row = getRow(index);\n\tAssert(row != nullptr);\n\n\tconst auto &st = row->computeSt(_st.item);\n\n\trow->lazyInitialize(st);\n\tconst auto outerWidth = width();\n\n\tauto refreshStatusAt = row->refreshStatusTime();\n\tif (refreshStatusAt > 0 && now >= refreshStatusAt) {\n\t\trow->refreshStatus();\n\t\trefreshStatusAt = row->refreshStatusTime();\n\t}\n\tconst auto refreshStatusIn = (refreshStatusAt > 0)\n\t\t? std::max(refreshStatusAt - now, crl::time(1))\n\t\t: 0;\n\n\tconst auto peer = row->special() ? nullptr : row->peer().get();\n\tconst auto active = (_contexted.index.value >= 0)\n\t\t? _contexted\n\t\t: (_pressed.index.value >= 0)\n\t\t? _pressed\n\t\t: _selected;\n\tconst auto selected = (active.index == index)\n\t\t&& (!active.element || !row->elementOnlySelect(active.element));\n\n\tif (_mode == Mode::Custom) {\n\t\t_controller->customRowPaint(p, now, row, selected);\n\t\treturn refreshStatusIn;\n\t}\n\n\tconst auto opacity = row->opacity();\n\tconst auto &bg = selected\n\t\t? st.button.textBgOver\n\t\t: st.button.textBg;\n\tif (opacity < 1.) {\n\t\tp.setOpacity(opacity);\n\t}\n\tconst auto guard = gsl::finally([&] {\n\t\tif (opacity < 1.) {\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t});\n\n\tp.fillRect(0, 0, outerWidth, _rowHeight, bg);\n\trow->paintRipple(p, st, 0, 0, outerWidth);\n\trow->paintUserpic(\n\t\tp,\n\t\tst,\n\t\tst.photoPosition.x(),\n\t\tst.photoPosition.y(),\n\t\touterWidth);\n\n\tp.setPen(st::contactsNameFg);\n\n\tconst auto skipRight = st.photoPosition.x();\n\tconst auto rightActionSize = row->rightActionSize();\n\tconst auto rightActionMargins = rightActionSize.isEmpty()\n\t\t? QMargins()\n\t\t: row->rightActionMargins();\n\tconst auto &name = row->name();\n\tconst auto namePosition = st.namePosition;\n\tconst auto namex = namePosition.x();\n\tconst auto namey = namePosition.y();\n\tauto namew = outerWidth - namex - skipRight;\n\tif (!rightActionSize.isEmpty()\n\t\t&& (namey < rightActionMargins.top() + rightActionSize.height())\n\t\t&& (namey + st.nameStyle.font->height\n\t\t\t> rightActionMargins.top())) {\n\t\tnamew -= rightActionMargins.left()\n\t\t\t+ rightActionSize.width()\n\t\t\t+ rightActionMargins.right()\n\t\t\t- skipRight;\n\t}\n\tconst auto statusx = st.statusPosition.x();\n\tconst auto statusy = st.statusPosition.y();\n\tauto statusw = outerWidth - statusx - skipRight;\n\tif (!rightActionSize.isEmpty()\n\t\t&& (statusy < rightActionMargins.top() + rightActionSize.height())\n\t\t&& (statusy + st::contactsStatusFont->height\n\t\t\t> rightActionMargins.top())) {\n\t\tstatusw -= rightActionMargins.left()\n\t\t\t+ rightActionSize.width()\n\t\t\t+ rightActionMargins.right()\n\t\t\t- skipRight;\n\t}\n\tnamew -= row->paintNameIconGetWidth(\n\t\tp,\n\t\t[=] { updateRow(row); },\n\t\tnow,\n\t\tnamex,\n\t\tnamey,\n\t\tname.maxWidth(),\n\t\tnamew,\n\t\twidth(),\n\t\tselected);\n\tauto nameCheckedRatio = row->disabled() ? 0. : row->checkedRatio();\n\tp.setPen(anim::pen(st.nameFg, st.nameFgChecked, nameCheckedRatio));\n\tname.drawLeftElided(p, namex, namey, namew, width());\n\n\tp.setFont(st::contactsStatusFont);\n\tif (row->isSearchResult()\n\t\t&& !_mentionHighlight.isEmpty()\n\t\t&& peer\n\t\t&& peer->username().startsWith(\n\t\t\t_mentionHighlight,\n\t\t\tQt::CaseInsensitive)) {\n\t\tconst auto username = peer->username();\n\t\tconst auto availableWidth = statusw;\n\t\tauto highlightedPart = '@' + username.mid(0, _mentionHighlight.size());\n\t\tauto grayedPart = username.mid(_mentionHighlight.size());\n\t\tconst auto highlightedWidth = st::contactsStatusFont->width(highlightedPart);\n\t\tif (highlightedWidth >= availableWidth || grayedPart.isEmpty()) {\n\t\t\tif (highlightedWidth > availableWidth) {\n\t\t\t\thighlightedPart = st::contactsStatusFont->elided(highlightedPart, availableWidth);\n\t\t\t}\n\t\t\tp.setPen(st.statusFgActive);\n\t\t\tp.drawTextLeft(statusx, statusy, width(), highlightedPart);\n\t\t} else {\n\t\t\tgrayedPart = st::contactsStatusFont->elided(grayedPart, availableWidth - highlightedWidth);\n\t\t\tp.setPen(st.statusFgActive);\n\t\t\tp.drawTextLeft(statusx, statusy, width(), highlightedPart);\n\t\t\tp.setPen(selected ? st.statusFgOver : st.statusFg);\n\t\t\tp.drawTextLeft(statusx + highlightedWidth, statusy, width(), grayedPart);\n\t\t}\n\t} else {\n\t\trow->paintStatusText(p, st, statusx, statusy, statusw, width(), selected);\n\t}\n\n\trow->elementsPaint(\n\t\tp,\n\t\twidth(),\n\t\tselected,\n\t\t(active.index == index) ? active.element : 0);\n\n\treturn refreshStatusIn;\n}\n\nPeerListContent::SkipResult PeerListContent::selectSkip(int direction) {\n\tif (hasPressed()) {\n\t\treturn { _selected.index.value, _selected.index.value };\n\t}\n\t_mouseSelection = false;\n\t_lastMousePosition = std::nullopt;\n\n\tauto newSelectedIndex = _selected.index.value + direction;\n\n\tauto result = SkipResult();\n\tresult.shouldMoveTo = newSelectedIndex;\n\n\tauto rowsCount = shownRowsCount();\n\tauto index = 0;\n\tauto firstEnabled = -1, lastEnabled = -1;\n\tenumerateShownRows([&firstEnabled, &lastEnabled, &index](not_null<PeerListRow*> row) {\n\t\tif (!row->disabled()) {\n\t\t\tif (firstEnabled < 0) {\n\t\t\t\tfirstEnabled = index;\n\t\t\t}\n\t\t\tlastEnabled = index;\n\t\t}\n\t\t++index;\n\t\treturn true;\n\t});\n\tif (firstEnabled < 0) {\n\t\tfirstEnabled = rowsCount;\n\t\tlastEnabled = firstEnabled - 1;\n\t}\n\n\tAssert(lastEnabled < rowsCount);\n\tAssert(firstEnabled - 1 <= lastEnabled);\n\n\t// Always pass through the first enabled item when changing from / to none selected.\n\tif ((_selected.index.value > firstEnabled && newSelectedIndex < firstEnabled)\n\t\t|| (_selected.index.value < firstEnabled && newSelectedIndex > firstEnabled)) {\n\t\tnewSelectedIndex = firstEnabled;\n\t}\n\n\t// Snap the index.\n\tnewSelectedIndex = std::clamp(\n\t\tnewSelectedIndex,\n\t\tfirstEnabled - 1,\n\t\tlastEnabled);\n\n\t// Skip the disabled rows.\n\tif (newSelectedIndex < firstEnabled) {\n\t\tnewSelectedIndex = -1;\n\t} else if (newSelectedIndex > lastEnabled) {\n\t\tnewSelectedIndex = lastEnabled;\n\t} else if (getRow(RowIndex(newSelectedIndex))->disabled()) {\n\t\tauto delta = (direction > 0) ? 1 : -1;\n\t\tfor (newSelectedIndex += delta; ; newSelectedIndex += delta) {\n\t\t\t// We must find an enabled row, firstEnabled <= us <= lastEnabled.\n\t\t\tAssert(newSelectedIndex >= 0 && newSelectedIndex < rowsCount);\n\t\t\tif (!getRow(RowIndex(newSelectedIndex))->disabled()) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (_controller->overrideKeyboardNavigation(\n\t\t\tdirection,\n\t\t\t_selected.index.value,\n\t\t\tnewSelectedIndex)) {\n\t\treturn { _selected.index.value, _selected.index.value };\n\t}\n\n\t_selected.index.value = newSelectedIndex;\n\t_selected.element = 0;\n\tif (newSelectedIndex >= 0) {\n\t\tauto top = (newSelectedIndex > 0) ? getRowTop(RowIndex(newSelectedIndex)) : _aboveHeight;\n\t\tauto bottom = (newSelectedIndex + 1 < rowsCount) ? getRowTop(RowIndex(newSelectedIndex + 1)) : height();\n\t\t_scrollToRequests.fire({ top, bottom });\n\t} else if (!_selected.index.value && direction < 0) {\n\t\tauto top = 0;\n\t\tauto bottom = _aboveHeight;\n\t\t_scrollToRequests.fire({ top, bottom });\n\t}\n\n\tupdate();\n\n\t_selectedIndex = _selected.index.value;\n\tresult.reallyMovedTo = _selected.index.value;\n\treturn result;\n}\n\nvoid PeerListContent::selectSkipPage(int height, int direction) {\n\tauto rowsToSkip = height / _rowHeight;\n\tif (!rowsToSkip) {\n\t\treturn;\n\t}\n\tselectSkip(rowsToSkip * direction);\n}\n\nvoid PeerListContent::selectLast() {\n\tconst auto rowsCount = shownRowsCount();\n\tconst auto newSelectedIndex = rowsCount - 1;\n\t_selected.index.value = newSelectedIndex;\n\t_selected.element = 0;\n\tif (newSelectedIndex >= 0) {\n\t\tauto top = (newSelectedIndex > 0) ? getRowTop(RowIndex(newSelectedIndex)) : 0;\n\t\tauto bottom = (newSelectedIndex + 1 < rowsCount) ? getRowTop(RowIndex(newSelectedIndex + 1)) : height();\n\t\t_scrollToRequests.fire({ top, bottom });\n\t}\n\n\tupdate();\n\n\t_selectedIndex = _selected.index.value;\n}\n\nrpl::producer<int> PeerListContent::selectedIndexValue() const {\n\treturn _selectedIndex.value();\n}\n\nint PeerListContent::selectedIndex() const {\n\treturn _selectedIndex.current();\n}\n\nbool PeerListContent::hasSelection() const {\n\treturn _selected.index.value >= 0;\n}\n\nbool PeerListContent::hasPressed() const {\n\treturn _pressed.index.value >= 0;\n}\n\nvoid PeerListContent::clearSelection() {\n\tsetSelected(Selected());\n}\n\nvoid PeerListContent::mouseLeftGeometry() {\n\tif (_mouseSelection) {\n\t\tsetSelected(Selected());\n\t\t_mouseSelection = false;\n\t\t_lastMousePosition = std::nullopt;\n\t}\n}\n\nvoid PeerListContent::loadProfilePhotos() {\n\tif (_visibleTop >= _visibleBottom) {\n\t\treturn;\n\t}\n\n\tauto yFrom = _visibleTop;\n\tauto yTo = _visibleBottom + (_visibleBottom - _visibleTop) * PreloadHeightsCount;\n\n\tif (yTo < 0) return;\n\tif (yFrom < 0) yFrom = 0;\n\n\tauto rowsCount = shownRowsCount();\n\tif (rowsCount > 0) {\n\t\tauto from = yFrom / _rowHeight;\n\t\tif (from < 0) from = 0;\n\t\tif (from < rowsCount) {\n\t\t\tauto to = (yTo / _rowHeight) + 1;\n\t\t\tif (to > rowsCount) to = rowsCount;\n\n\t\t\tfor (auto index = from; index != to; ++index) {\n\t\t\t\tgetRow(RowIndex(index))->preloadUserpic();\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid PeerListContent::checkScrollForPreload() {\n\tif (_visibleBottom + PreloadHeightsCount * (_visibleBottom - _visibleTop) >= height()) {\n\t\t_controller->loadMoreRows();\n\t}\n}\n\nvoid PeerListContent::searchQueryChanged(QString query) {\n\tconst auto searchWordsList = TextUtilities::PrepareSearchWords(query);\n\tconst auto normalizedQuery = searchWordsList.join(' ');\n\tif (_ignoreHiddenRowsOnSearch && !normalizedQuery.isEmpty()) {\n\t\t_filterResults.clear();\n\t}\n\tif (_normalizedSearchQuery != normalizedQuery) {\n\t\tsetSearchQuery(query, normalizedQuery);\n\t\tif (_controller->searchInLocal() && !searchWordsList.isEmpty()) {\n\t\t\tAssert(_hiddenRows.empty() || _ignoreHiddenRowsOnSearch);\n\n\t\t\tauto minimalList = (const std::vector<not_null<PeerListRow*>>*)nullptr;\n\t\t\tfor (const auto &searchWord : searchWordsList) {\n\t\t\t\tauto searchWordStart = searchWord[0].toLower();\n\t\t\t\tauto it = _searchIndex.find(searchWordStart);\n\t\t\t\tif (it == _searchIndex.cend()) {\n\t\t\t\t\t// Some word can't be found in any row.\n\t\t\t\t\tminimalList = nullptr;\n\t\t\t\t\tbreak;\n\t\t\t\t} else if (!minimalList || minimalList->size() > it->second.size()) {\n\t\t\t\t\tminimalList = &it->second;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (minimalList) {\n\t\t\t\tauto searchWordInNames = [](\n\t\t\t\t\t\tnot_null<PeerListRow*> row,\n\t\t\t\t\t\tconst QString &searchWord) {\n\t\t\t\t\tfor (auto &nameWord : row->generateNameWords()) {\n\t\t\t\t\t\tif (nameWord.startsWith(searchWord)) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn false;\n\t\t\t\t};\n\t\t\t\tauto allSearchWordsInNames = [&](\n\t\t\t\t\t\tnot_null<PeerListRow*> row) {\n\t\t\t\t\tfor (const auto &searchWord : searchWordsList) {\n\t\t\t\t\t\tif (!searchWordInNames(row, searchWord)) {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t};\n\n\t\t\t\t_filterResults.reserve(minimalList->size());\n\t\t\t\tfor (const auto &row : *minimalList) {\n\t\t\t\t\tif (allSearchWordsInNames(row)) {\n\t\t\t\t\t\t_filterResults.push_back(row);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (_controller->hasComplexSearch()) {\n\t\t\t_controller->search(_searchQuery);\n\t\t}\n\t\trefreshRows();\n\t}\n}\n\nstd::unique_ptr<PeerListState> PeerListContent::saveState() const {\n\tExpects(_hiddenRows.empty());\n\n\tauto result = std::make_unique<PeerListState>();\n\tresult->controllerState\n\t\t= std::make_unique<PeerListController::SavedStateBase>();\n\tresult->list.reserve(_rows.size());\n\tfor (const auto &row : _rows) {\n\t\tresult->list.push_back(row->peer());\n\t}\n\tresult->filterResults.reserve(_filterResults.size());\n\tfor (const auto &row : _filterResults) {\n\t\tresult->filterResults.push_back(row->peer());\n\t}\n\tresult->searchQuery = _searchQuery;\n\treturn result;\n}\n\nvoid PeerListContent::restoreState(\n\t\tstd::unique_ptr<PeerListState> state) {\n\tif (!state || !state->controllerState) {\n\t\treturn;\n\t}\n\n\tclearAllContent();\n\n\tfor (auto peer : state->list) {\n\t\tif (auto row = _controller->createRestoredRow(peer)) {\n\t\t\tappendRow(std::move(row));\n\t\t}\n\t}\n\tauto query = state->searchQuery;\n\tauto searchWords = TextUtilities::PrepareSearchWords(query);\n\tsetSearchQuery(query, searchWords.join(' '));\n\tfor (auto peer : state->filterResults) {\n\t\tif (auto existingRow = findRow(peer->id.value)) {\n\t\t\t_filterResults.push_back(existingRow);\n\t\t} else if (auto row = _controller->createSearchRow(peer)) {\n\t\t\tappendSearchRow(std::move(row));\n\t\t}\n\t}\n\trefreshRows();\n}\n\nvoid PeerListContent::setSearchQuery(\n\t\tconst QString &query,\n\t\tconst QString &normalizedQuery) {\n\tsetSelected(Selected());\n\tsetPressed(Selected());\n\tsetContexted(Selected());\n\t_mouseSelection = false;\n\t_lastMousePosition = std::nullopt;\n\t_searchQuery = query;\n\t_normalizedSearchQuery = normalizedQuery;\n\t_mentionHighlight = _searchQuery.startsWith('@')\n\t\t? _searchQuery.mid(1)\n\t\t: _searchQuery;\n\t_filterResults.clear();\n\tclearSearchRows();\n}\n\nbool PeerListContent::submitted() {\n\tif (const auto row = getRow(_selected.index)) {\n\t\t_lastMousePosition = std::nullopt;\n\t\t_controller->rowClicked(row);\n\t\treturn true;\n\t} else if (showingSearch()) {\n\t\tif (const auto row = getRow(RowIndex(0))) {\n\t\t\t_lastMousePosition = std::nullopt;\n\t\t\t_controller->rowClicked(row);\n\t\t\treturn true;\n\t\t}\n\t} else {\n\t\t_noSearchSubmits.fire({});\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nPeerListRowId PeerListContent::updateFromParentDrag(QPoint globalPosition) {\n\tselectByMouse(globalPosition);\n\tconst auto row = getRow(_selected.index);\n\treturn row ? row->id() : 0;\n}\n\nvoid PeerListContent::dragLeft() {\n\tclearSelection();\n}\n\nvoid PeerListContent::setIgnoreHiddenRowsOnSearch(bool value) {\n\t_ignoreHiddenRowsOnSearch = value;\n}\n\nvoid PeerListContent::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\t_visibleTop = visibleTop;\n\t_visibleBottom = visibleBottom;\n\tloadProfilePhotos();\n\tcheckScrollForPreload();\n}\n\nvoid PeerListContent::setSelected(Selected selected) {\n\tupdateRow(_selected.index);\n\tif (_selected == selected) {\n\t\treturn;\n\t}\n\t_selected = selected;\n\tupdateRow(_selected.index);\n\tsetCursor(_selected.element ? style::cur_pointer : style::cur_default);\n\n\t_selectedIndex = _selected.index.value;\n}\n\nvoid PeerListContent::setContexted(Selected contexted) {\n\tupdateRow(_contexted.index);\n\tif (_contexted != contexted) {\n\t\t_contexted = contexted;\n\t\tupdateRow(_contexted.index);\n\t}\n}\n\nvoid PeerListContent::restoreSelection() {\n\tif (_mouseSelection) {\n\t\tselectByMouse(QCursor::pos());\n\t}\n}\n\nauto PeerListContent::saveSelectedData(Selected from)\n-> SelectedSaved {\n\tif (auto row = getRow(from.index)) {\n\t\treturn { row->id(), from };\n\t}\n\treturn { PeerListRowId(0), from };\n}\n\nauto PeerListContent::restoreSelectedData(SelectedSaved from)\n-> Selected {\n\tauto result = from.old;\n\tif (auto row = findRow(from.id)) {\n\t\tresult.index = findRowIndex(row, result.index);\n\t} else {\n\t\tresult.index.value = -1;\n\t}\n\treturn result;\n}\n\nvoid PeerListContent::selectByMouse(QPoint globalPosition) {\n\t_mouseSelection = true;\n\t_lastMousePosition = globalPosition;\n\tconst auto point = mapFromGlobal(globalPosition);\n\tconst auto customMode = (_mode == Mode::Custom);\n\tauto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(globalPosition));\n\tauto selected = Selected();\n\tauto rowsPointY = point.y() - rowsTop();\n\tselected.index.value = (in\n\t\t&& rowsPointY >= 0\n\t\t&& rowsPointY < shownRowsCount() * _rowHeight)\n\t\t? (rowsPointY / _rowHeight)\n\t\t: -1;\n\tif (selected.index.value >= 0) {\n\t\tconst auto row = getRow(selected.index);\n\t\tif (row->disabled()\n\t\t\t|| (customMode\n\t\t\t\t&& !_controller->customRowSelectionPoint(\n\t\t\t\t\trow,\n\t\t\t\t\tpoint.x(),\n\t\t\t\t\trowsPointY - (selected.index.value * _rowHeight)))) {\n\t\t\tselected = Selected();\n\t\t} else if (!customMode) {\n\t\t\tfor (auto i = 0, count = row->elementsCount(); i != count; ++i) {\n\t\t\t\tconst auto rect = getElementRect(row, selected.index, i + 1);\n\t\t\t\tif (rect.contains(point)) {\n\t\t\t\t\tselected.element = i + 1;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tsetSelected(selected);\n}\n\nQRect PeerListContent::getElementRect(\n\t\tnot_null<PeerListRow*> row,\n\t\tRowIndex index,\n\t\tint element) const {\n\tif (row->elementDisabled(element)) {\n\t\treturn QRect();\n\t}\n\tconst auto geometry = row->elementGeometry(element, width());\n\tif (geometry.isEmpty()) {\n\t\treturn QRect();\n\t}\n\treturn geometry.translated(0, getRowTop(index));\n}\n\nint PeerListContent::rowsTop() const {\n\treturn _aboveHeight + _st.padding.top();\n}\n\nint PeerListContent::getRowTop(RowIndex index) const {\n\tif (index.value >= 0) {\n\t\treturn rowsTop() + index.value * _rowHeight;\n\t}\n\treturn -1;\n}\n\nvoid PeerListContent::updateRow(not_null<PeerListRow*> row, RowIndex hint) {\n\tupdateRow(findRowIndex(row, hint));\n}\n\nvoid PeerListContent::updateRow(RowIndex index) {\n\tif (index.value < 0) {\n\t\treturn;\n\t}\n\tif (const auto row = getRow(index); row && row->disabled()) {\n\t\tif (index == _selected.index) {\n\t\t\tsetSelected(Selected());\n\t\t}\n\t\tif (index == _pressed.index) {\n\t\t\tsetPressed(Selected());\n\t\t}\n\t\tif (index == _contexted.index) {\n\t\t\tsetContexted(Selected());\n\t\t}\n\t}\n\tupdate(0, getRowTop(index), width(), _rowHeight);\n}\n\ntemplate <typename Callback>\nbool PeerListContent::enumerateShownRows(Callback callback) {\n\treturn enumerateShownRows(0, shownRowsCount(), std::move(callback));\n}\n\ntemplate <typename Callback>\nbool PeerListContent::enumerateShownRows(int from, int to, Callback callback) {\n\tAssert(0 <= from);\n\tAssert(from <= to);\n\tif (showingSearch()) {\n\t\tAssert(to <= _filterResults.size());\n\t\tfor (auto i = from; i != to; ++i) {\n\t\t\tif (!callback(_filterResults[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tAssert(to <= _rows.size());\n\t\tfor (auto i = from; i != to; ++i) {\n\t\t\tif (!callback(_rows[i].get())) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\treturn true;\n}\n\nPeerListRow *PeerListContent::getRow(RowIndex index) {\n\tif (index.value >= 0) {\n\t\tif (showingSearch()) {\n\t\t\tif (index.value < _filterResults.size()) {\n\t\t\t\treturn _filterResults[index.value];\n\t\t\t}\n\t\t} else if (index.value < _rows.size()) {\n\t\t\treturn _rows[index.value].get();\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nPeerListContent::RowIndex PeerListContent::findRowIndex(\n\t\tnot_null<PeerListRow*> row,\n\t\tRowIndex hint) {\n\tif (!showingSearch()) {\n\t\tAssert(!row->isSearchResult());\n\t\treturn RowIndex(row->absoluteIndex());\n\t}\n\n\tauto result = hint;\n\tif (getRow(result) == row) {\n\t\treturn result;\n\t}\n\n\tauto count = shownRowsCount();\n\tfor (result.value = 0; result.value != count; ++result.value) {\n\t\tif (getRow(result) == row) {\n\t\t\treturn result;\n\t\t}\n\t}\n\tresult.value = -1;\n\treturn result;\n}\n\nvoid PeerListContent::handleNameChanged(not_null<PeerData*> peer) {\n\tauto byPeer = _rowsByPeer.find(peer);\n\tif (byPeer != _rowsByPeer.cend()) {\n\t\tfor (auto row : byPeer->second) {\n\t\t\tif (addingToSearchIndex()) {\n\t\t\t\taddToSearchIndex(row);\n\t\t\t}\n\t\t\trow->refreshName(_st.item);\n\t\t\tupdateRow(row);\n\t\t}\n\t}\n}\n\nPeerListContent::~PeerListContent() {\n\tif (_contextMenu) {\n\t\t_contextMenu->setDestroyedCallback(nullptr);\n\t}\n}\n\nvoid PeerListContentDelegate::peerListShowRowMenu(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool highlightRow,\n\t\tFn<void(not_null<Ui::PopupMenu *>)> destroyed) {\n\t_content->showRowMenu(row, highlightRow, std::move(destroyed));\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peer_list_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/unread_badge.h\"\n#include \"ui/userpic_view.h\"\n#include \"ui/layers/box_content.h\"\n#include \"base/timer.h\"\n\nnamespace style {\nstruct PeerList;\nstruct PeerListItem;\nstruct MultiSelect;\n} // namespace style\n\nnamespace Main {\nclass Session;\nclass SessionShow;\n} // namespace Main\n\nnamespace Ui {\nclass RippleAnimation;\nclass RoundImageCheckbox;\nclass MultiSelect;\ntemplate <typename Widget>\nclass SlideWrap;\nclass FlatLabel;\nstruct ScrollToRequest;\nclass PopupMenu;\nstruct OutlineSegment;\n} // namespace Ui\n\nusing PaintRoundImageCallback = Fn<void(\n\tPainter &p,\n\tint x,\n\tint y,\n\tint outerWidth,\n\tint size)>;\n\n[[nodiscard]] PaintRoundImageCallback PaintUserpicCallback(\n\tnot_null<PeerData*> peer,\n\tbool respectSavedMessagesChat);\n[[nodiscard]] PaintRoundImageCallback ForceRoundUserpicCallback(\n\tnot_null<PeerData*> peer);\n\nusing PeerListRowId = uint64;\n\n[[nodiscard]] PeerListRowId UniqueRowIdFromString(const QString &d);\n\nclass PeerListRow {\npublic:\n\tenum class State {\n\t\tActive,\n\t\tDisabled,\n\t\tDisabledChecked,\n\t};\n\n\texplicit PeerListRow(not_null<PeerData*> peer);\n\tPeerListRow(not_null<PeerData*> peer, PeerListRowId id);\n\n\tvirtual ~PeerListRow();\n\n\tvoid setDisabledState(State state) {\n\t\t_disabledState = state;\n\t}\n\n\t// Checked state is controlled by the box with multiselect,\n\t// not by the row itself, so there is no setChecked() method.\n\t// We can query the checked state from row, but before it is\n\t// added to the box it is always false.\n\t[[nodiscard]] bool checked() const;\n\n\t[[nodiscard]] bool special() const {\n\t\treturn !_peer;\n\t}\n\t[[nodiscard]] not_null<PeerData*> peer() const {\n\t\tExpects(!special());\n\n\t\treturn _peer;\n\t}\n\t[[nodiscard]] PeerListRowId id() const {\n\t\treturn _id;\n\t}\n\n\t[[nodiscard]] Ui::PeerUserpicView &ensureUserpicView();\n\n\t[[nodiscard]] virtual QString generateName();\n\t[[nodiscard]] virtual QString generateShortName();\n\t[[nodiscard]] virtual auto generatePaintUserpicCallback(\n\t\tbool forceRound) -> PaintRoundImageCallback;\n\tvirtual void paintUserpicOverlay(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth) {\n\t}\n\n\t[[nodiscard]] virtual auto generateNameFirstLetters() const\n\t\t-> const base::flat_set<QChar> &;\n\t[[nodiscard]] virtual auto generateNameWords() const\n\t\t-> const base::flat_set<QString> &;\n\t[[nodiscard]] virtual const style::PeerListItem &computeSt(\n\t\tconst style::PeerListItem &st) const;\n\n\tvirtual void preloadUserpic();\n\n\tvoid setCustomStatus(const QString &status, bool active = false);\n\tvoid clearCustomStatus();\n\n\t// Box interface.\n\tvirtual int paintNameIconGetWidth(\n\t\tPainter &p,\n\t\tFn<void()> repaint,\n\t\tcrl::time now,\n\t\tint nameLeft,\n\t\tint nameTop,\n\t\tint nameWidth,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected);\n\n\tvirtual QSize rightActionSize() const {\n\t\treturn QSize();\n\t}\n\tvirtual QMargins rightActionMargins() const {\n\t\treturn QMargins();\n\t}\n\tvirtual bool rightActionDisabled() const {\n\t\treturn false;\n\t}\n\tvirtual void rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\t}\n\tvirtual void rightActionAddRipple(\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) {\n\t}\n\tvirtual void rightActionStopLastRipple() {\n\t}\n\t[[nodiscard]] virtual float64 opacity() {\n\t\treturn 1.;\n\t}\n\n\t// By default elements code falls back to a simple right action code.\n\tvirtual int elementsCount() const;\n\tvirtual QRect elementGeometry(int element, int outerWidth) const;\n\tvirtual bool elementDisabled(int element) const;\n\tvirtual bool elementOnlySelect(int element) const;\n\tvirtual void elementAddRipple(\n\t\tint element,\n\t\tQPoint point,\n\t\tFn<void()> updateCallback);\n\tvirtual void elementsStopLastRipple();\n\tvirtual void elementsPaint(\n\t\tPainter &p,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tint selectedElement);\n\n\tvirtual void refreshName(const style::PeerListItem &st);\n\tconst Ui::Text::String &name() const {\n\t\treturn _name;\n\t}\n\n\tvirtual bool useForumLikeUserpic() const;\n\n\tenum class StatusType {\n\t\tOnline,\n\t\tLastSeen,\n\t\tCustom,\n\t\tCustomActive,\n\t};\n\tvirtual void refreshStatus();\n\tcrl::time refreshStatusTime() const;\n\n\tvoid setAbsoluteIndex(int index) {\n\t\t_absoluteIndex = index;\n\t}\n\tint absoluteIndex() const {\n\t\treturn _absoluteIndex;\n\t}\n\tbool disabled() const {\n\t\treturn (_disabledState != State::Active);\n\t}\n\tbool isSearchResult() const {\n\t\treturn _isSearchResult;\n\t}\n\tvoid setIsSearchResult(bool isSearchResult) {\n\t\t_isSearchResult = isSearchResult;\n\t}\n\tvoid setSavedMessagesChatStatus(QString savedMessagesStatus) {\n\t\t_savedMessagesStatus = savedMessagesStatus;\n\t}\n\tvoid setIsRepliesMessagesChat(bool isRepliesMessagesChat) {\n\t\t_isRepliesMessagesChat = isRepliesMessagesChat;\n\t}\n\tvoid setIsVerifyCodesChat(bool isVerifyCodesChat) {\n\t\t_isVerifyCodesChat = isVerifyCodesChat;\n\t}\n\n\ttemplate <typename UpdateCallback>\n\tvoid setChecked(\n\t\t\tbool checked,\n\t\t\tconst style::RoundImageCheckbox &st,\n\t\t\tanim::type animated,\n\t\t\tUpdateCallback callback) {\n\t\tif (checked && !_checkbox) {\n\t\t\tcreateCheckbox(st, std::move(callback));\n\t\t}\n\t\tsetCheckedInternal(checked, animated);\n\t}\n\tvoid setCustomizedCheckSegments(\n\t\tstd::vector<Ui::OutlineSegment> segments,\n\t\tbool liveBadge);\n\tvoid setHidden(bool hidden) {\n\t\t_hidden = hidden;\n\t}\n\t[[nodiscard]] bool hidden() const {\n\t\treturn _hidden;\n\t}\n\tvoid finishCheckedAnimation();\n\tvoid invalidatePixmapsCache();\n\n\ttemplate <typename MaskGenerator, typename UpdateCallback>\n\tvoid addRipple(\n\t\tconst style::PeerListItem &st,\n\t\tMaskGenerator &&maskGenerator,\n\t\tQPoint point,\n\t\tUpdateCallback &&updateCallback);\n\tvoid stopLastRipple();\n\tvoid paintRipple(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth);\n\tvoid paintUserpic(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth);\n\tfloat64 checkedRatio();\n\n\tvoid setNameFirstLetters(const base::flat_set<QChar> &firstLetters) {\n\t\t_nameFirstLetters = firstLetters;\n\t}\n\tconst base::flat_set<QChar> &nameFirstLetters() const {\n\t\treturn _nameFirstLetters;\n\t}\n\n\tvoid setSkipPeerBadge(bool skip) {\n\t\t_skipPeerBadge = skip;\n\t}\n\n\tvirtual void lazyInitialize(const style::PeerListItem &st);\n\tvirtual void paintStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected);\n\nprotected:\n\tbool isInitialized() const {\n\t\treturn _initialized;\n\t}\n\n\texplicit PeerListRow(PeerListRowId id);\n\nprivate:\n\tvoid createCheckbox(\n\t\tconst style::RoundImageCheckbox &st,\n\t\tFn<void()> updateCallback);\n\tvoid setCheckedInternal(bool checked, anim::type animated);\n\tvoid paintDisabledCheckUserpic(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth) const;\n\tvoid setStatusText(const QString &text);\n\n\tPeerListRowId _id = 0;\n\tPeerData *_peer = nullptr;\n\tmutable Ui::PeerUserpicView _userpic;\n\tstd::unique_ptr<Ui::RippleAnimation> _ripple;\n\tstd::unique_ptr<Ui::RoundImageCheckbox> _checkbox;\n\tUi::Text::String _name;\n\tUi::Text::String _status;\n\tUi::PeerBadge _badge;\n\tStatusType _statusType = StatusType::Online;\n\tcrl::time _statusValidTill = 0;\n\tbase::flat_set<QChar> _nameFirstLetters;\n\tQString _savedMessagesStatus;\n\tint _absoluteIndex = -1;\n\tState _disabledState = State::Active;\n\tbool _hidden : 1 = false;\n\tbool _initialized : 1 = false;\n\tbool _isSearchResult : 1 = false;\n\tbool _isRepliesMessagesChat : 1 = false;\n\tbool _isVerifyCodesChat : 1 = false;\n\tbool _skipPeerBadge : 1 = false;\n\n};\n\nenum class PeerListSearchMode {\n\tDisabled,\n\tEnabled,\n};\n\nstruct PeerListState;\n\nclass PeerListDelegate {\npublic:\n\tvirtual void peerListSetTitle(rpl::producer<QString> title) = 0;\n\tvirtual void peerListSetAdditionalTitle(rpl::producer<QString> title) = 0;\n\tvirtual void peerListSetHideEmpty(bool hide) = 0;\n\tvirtual void peerListSetDescription(object_ptr<Ui::FlatLabel> description) = 0;\n\tvirtual void peerListSetSearchNoResults(object_ptr<Ui::FlatLabel> noResults) = 0;\n\tvirtual void peerListSetAboveWidget(object_ptr<Ui::RpWidget> aboveWidget) = 0;\n\tvirtual void peerListSetAboveSearchWidget(object_ptr<Ui::RpWidget> aboveWidget) = 0;\n\tvirtual void peerListSetBelowWidget(object_ptr<Ui::RpWidget> belowWidget) = 0;\n\tvirtual void peerListMouseLeftGeometry() = 0;\n\tvirtual void peerListSetSearchMode(PeerListSearchMode mode) = 0;\n\tvirtual void peerListAppendRow(std::unique_ptr<PeerListRow> row) = 0;\n\tvirtual void peerListAppendSearchRow(std::unique_ptr<PeerListRow> row) = 0;\n\tvirtual void peerListAppendFoundRow(not_null<PeerListRow*> row) = 0;\n\tvirtual void peerListPrependRow(std::unique_ptr<PeerListRow> row) = 0;\n\tvirtual void peerListPrependRowFromSearchResult(not_null<PeerListRow*> row) = 0;\n\tvirtual void peerListUpdateRow(not_null<PeerListRow*> row) = 0;\n\tvirtual void peerListRemoveRow(not_null<PeerListRow*> row) = 0;\n\tvirtual void peerListConvertRowToSearchResult(not_null<PeerListRow*> row) = 0;\n\tvirtual bool peerListIsRowChecked(not_null<PeerListRow*> row) = 0;\n\tvirtual void peerListSetRowChecked(not_null<PeerListRow*> row, bool checked) = 0;\n\tvirtual void peerListSetRowHidden(not_null<PeerListRow*> row, bool hidden) = 0;\n\tvirtual void peerListSetForeignRowChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked,\n\t\tanim::type animated) = 0;\n\tvirtual not_null<PeerListRow*> peerListRowAt(int index) = 0;\n\tvirtual void peerListRefreshRows() = 0;\n\tvirtual void peerListScrollToTop() = 0;\n\tvirtual int peerListFullRowsCount() = 0;\n\tvirtual PeerListRow *peerListFindRow(PeerListRowId id) = 0;\n\tvirtual int peerListSearchRowsCount() = 0;\n\tvirtual not_null<PeerListRow*> peerListSearchRowAt(int index) = 0;\n\tvirtual std::optional<QPoint> peerListLastRowMousePosition() = 0;\n\tvirtual void peerListSortRows(Fn<bool(const PeerListRow &a, const PeerListRow &b)> compare) = 0;\n\tvirtual int peerListPartitionRows(Fn<bool(const PeerListRow &a)> border) = 0;\n\tvirtual std::shared_ptr<Main::SessionShow> peerListUiShow() = 0;\n\n\tvirtual void peerListSelectSkip(int direction) = 0;\n\n\tvirtual void peerListPressLeftToContextMenu(bool shown) = 0;\n\tvirtual bool peerListTrackRowPressFromGlobal(QPoint globalPosition) = 0;\n\n\ttemplate <typename PeerDataRange>\n\tvoid peerListAddSelectedPeers(PeerDataRange &&range) {\n\t\tfor (const auto &peer : range) {\n\t\t\tpeerListAddSelectedPeerInBunch(peer);\n\t\t}\n\t\tpeerListFinishSelectedRowsBunch();\n\t}\n\n\ttemplate <typename PeerListRowRange>\n\tvoid peerListAddSelectedRows(PeerListRowRange &&range) {\n\t\tfor (const auto row : range) {\n\t\t\tpeerListAddSelectedRowInBunch(row);\n\t\t}\n\t\tpeerListFinishSelectedRowsBunch();\n\t}\n\n\tvirtual void peerListShowRowMenu(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool highlightRow,\n\t\tFn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr) = 0;\n\tvirtual int peerListSelectedRowsCount() = 0;\n\tvirtual std::unique_ptr<PeerListState> peerListSaveState() const = 0;\n\tvirtual void peerListRestoreState(\n\t\tstd::unique_ptr<PeerListState> state) = 0;\n\tvirtual ~PeerListDelegate() = default;\n\nprivate:\n\tvirtual void peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) = 0;\n\tvirtual void peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) = 0;\n\tvirtual void peerListFinishSelectedRowsBunch() = 0;\n\n};\n\nclass PeerListSearchDelegate {\npublic:\n\tvirtual void peerListSearchAddRow(not_null<PeerData*> peer) = 0;\n\tvirtual void peerListSearchAddRow(PeerListRowId id) = 0;\n\tvirtual void peerListSearchRefreshRows() = 0;\n\tvirtual ~PeerListSearchDelegate() = default;\n\n};\n\nclass PeerListSearchController {\npublic:\n\tstruct SavedStateBase {\n\t\tvirtual ~SavedStateBase() = default;\n\t};\n\n\tvirtual void searchQuery(const QString &query) = 0;\n\tvirtual bool isLoading() = 0;\n\tvirtual bool loadMoreRows() = 0;\n\tvirtual ~PeerListSearchController() = default;\n\n\tvoid setDelegate(not_null<PeerListSearchDelegate*> delegate) {\n\t\t_delegate = delegate;\n\t}\n\n\tvirtual std::unique_ptr<SavedStateBase> saveState() const {\n\t\treturn nullptr;\n\t}\n\tvirtual void restoreState(\n\t\tstd::unique_ptr<SavedStateBase> state) {\n\t}\n\n\trpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprotected:\n\tnot_null<PeerListSearchDelegate*> delegate() const {\n\t\treturn _delegate;\n\t}\n\nprivate:\n\tPeerListSearchDelegate *_delegate = nullptr;\n\trpl::lifetime _lifetime;\n\n};\n\nclass PeerListController : public PeerListSearchDelegate {\npublic:\n\tstruct SavedStateBase {\n\t\tvirtual ~SavedStateBase() = default;\n\t};\n\n\t// Search works only with RowId == peer->id.\n\tPeerListController(\n\t\tstd::unique_ptr<PeerListSearchController> searchController = {});\n\n\tvoid setDelegate(not_null<PeerListDelegate*> delegate) {\n\t\t_delegate = delegate;\n\t\tprepare();\n\t}\n\t[[nodiscard]] not_null<PeerListDelegate*> delegate() const {\n\t\treturn _delegate;\n\t}\n\n\tvoid setStyleOverrides(\n\t\t\tconst style::PeerList *listSt,\n\t\t\tconst style::MultiSelect *selectSt = nullptr) {\n\t\t_listSt = listSt;\n\t\t_selectSt = selectSt;\n\t}\n\tconst style::PeerList *listSt() const {\n\t\treturn _listSt;\n\t}\n\tconst style::MultiSelect *selectSt() const {\n\t\treturn _selectSt;\n\t}\n\tconst style::PeerList &computeListSt() const;\n\tconst style::MultiSelect &computeSelectSt() const;\n\n\tvirtual Main::Session &session() const = 0;\n\n\tvirtual void prepare() = 0;\n\n\tvirtual void showFinished();\n\tvoid setShowFinishedCallback(Fn<void()> callback);\n\n\tvirtual void rowClicked(not_null<PeerListRow*> row) = 0;\n\tvirtual void rowMiddleClicked(not_null<PeerListRow*> row) {\n\t}\n\tvirtual void rowRightActionClicked(not_null<PeerListRow*> row) {\n\t}\n\n\t// By default elements code falls back to a simple right action code.\n\tvirtual void rowElementClicked(not_null<PeerListRow*> row, int element) {\n\t\tif (element == 1) {\n\t\t\trowRightActionClicked(row);\n\t\t}\n\t}\n\n\tvirtual bool rowTrackPress(not_null<PeerListRow*> row) {\n\t\treturn false;\n\t}\n\tvirtual void rowTrackPressCancel() {\n\t}\n\tvirtual bool rowTrackPressSkipMouseSelection() {\n\t\treturn false;\n\t}\n\n\tvirtual void loadMoreRows() {\n\t}\n\tvirtual void itemDeselectedHook(not_null<PeerData*> peer) {\n\t}\n\tvirtual bool isForeignRow(PeerListRowId itemId) {\n\t\treturn false;\n\t}\n\tvirtual bool handleDeselectForeignRow(PeerListRowId itemId) {\n\t\treturn false;\n\t}\n\tvirtual base::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row);\n\tbool isSearchLoading() const {\n\t\treturn _searchController ? _searchController->isLoading() : false;\n\t}\n\tvirtual std::unique_ptr<PeerListRow> createSearchRow(\n\t\t\tnot_null<PeerData*> peer) {\n\t\treturn nullptr;\n\t}\n\tvirtual std::unique_ptr<PeerListRow> createSearchRow(PeerListRowId id);\n\tvirtual std::unique_ptr<PeerListRow> createRestoredRow(\n\t\t\tnot_null<PeerData*> peer) {\n\t\treturn nullptr;\n\t}\n\n\tvirtual std::unique_ptr<PeerListState> saveState() const;\n\tvirtual void restoreState(\n\t\tstd::unique_ptr<PeerListState> state);\n\n\t[[nodiscard]] virtual int contentWidth() const;\n\t[[nodiscard]] virtual rpl::producer<int> boxHeightValue() const;\n\t[[nodiscard]] virtual int descriptionTopSkipMin() const;\n\n\t[[nodiscard]] bool isRowSelected(not_null<PeerListRow*> row) {\n\t\treturn delegate()->peerListIsRowChecked(row);\n\t}\n\n\tvirtual bool trackSelectedList() {\n\t\treturn true;\n\t}\n\tvirtual bool searchInLocal() {\n\t\treturn true;\n\t}\n\t[[nodiscard]] bool hasComplexSearch() const;\n\tvoid search(const QString &query);\n\n\tvoid peerListSearchAddRow(not_null<PeerData*> peer) override;\n\tvoid peerListSearchAddRow(PeerListRowId id) override;\n\tvoid peerListSearchRefreshRows() override;\n\n\t[[nodiscard]] virtual QString savedMessagesChatStatus() const {\n\t\treturn QString();\n\t}\n\t[[nodiscard]] virtual int customRowHeight() {\n\t\tUnexpected(\"PeerListController::customRowHeight.\");\n\t}\n\tvirtual void customRowPaint(\n\t\t\tPainter &p,\n\t\t\tcrl::time now,\n\t\t\tnot_null<PeerListRow*> row,\n\t\t\tbool selected) {\n\t\tUnexpected(\"PeerListController::customRowPaint.\");\n\t}\n\t[[nodiscard]] virtual bool customRowSelectionPoint(\n\t\t\tnot_null<PeerListRow*> row,\n\t\t\tint x,\n\t\t\tint y) {\n\t\tUnexpected(\"PeerListController::customRowSelectionPoint.\");\n\t}\n\t[[nodiscard]] virtual Fn<QImage()> customRowRippleMaskGenerator() {\n\t\tUnexpected(\"PeerListController::customRowRippleMaskGenerator.\");\n\t}\n\n\tvirtual bool overrideKeyboardNavigation(\n\t\t\tint direction,\n\t\t\tint fromIndex,\n\t\t\tint toIndex) {\n\t\treturn false;\n\t}\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\n\tvirtual ~PeerListController() = default;\n\nprotected:\n\tPeerListSearchController *searchController() const {\n\t\treturn _searchController.get();\n\t}\n\n\tvoid setDescriptionText(const QString &text);\n\tvoid setSearchNoResultsText(const QString &text);\n\tvoid setDescription(object_ptr<Ui::FlatLabel> description) {\n\t\tdelegate()->peerListSetDescription(std::move(description));\n\t}\n\tvoid setSearchNoResults(object_ptr<Ui::FlatLabel> noResults) {\n\t\tdelegate()->peerListSetSearchNoResults(std::move(noResults));\n\t}\n\n\tvoid sortByName();\n\nprivate:\n\tPeerListDelegate *_delegate = nullptr;\n\tstd::unique_ptr<PeerListSearchController> _searchController = nullptr;\n\n\tconst style::PeerList *_listSt = nullptr;\n\tconst style::MultiSelect *_selectSt = nullptr;\n\n\tFn<void()> _showFinished;\n\n\trpl::lifetime _lifetime;\n\n};\n\nstruct PeerListState {\n\tPeerListState() = default;\n\tPeerListState(PeerListState &&other) = delete;\n\tPeerListState &operator=(PeerListState &&other) = delete;\n\n\tstd::unique_ptr<PeerListController::SavedStateBase> controllerState;\n\tstd::vector<not_null<PeerData*>> list;\n\tstd::vector<not_null<PeerData*>> filterResults;\n\tQString searchQuery;\n};\n\nclass PeerListContent : public Ui::RpWidget {\npublic:\n\tPeerListContent(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListController*> controller);\n\n\tstruct SkipResult {\n\t\tint shouldMoveTo = 0;\n\t\tint reallyMovedTo = 0;\n\t};\n\tSkipResult selectSkip(int direction);\n\tvoid selectSkipPage(int height, int direction);\n\tvoid selectLast();\n\n\tenum class Mode {\n\t\tDefault,\n\t\tCustom,\n\t};\n\tvoid setMode(Mode mode);\n\n\t[[nodiscard]] rpl::producer<int> selectedIndexValue() const;\n\t[[nodiscard]] int selectedIndex() const;\n\t[[nodiscard]] bool hasSelection() const;\n\t[[nodiscard]] bool hasPressed() const;\n\tvoid clearSelection();\n\n\tvoid searchQueryChanged(QString query);\n\tbool submitted();\n\n\tPeerListRowId updateFromParentDrag(QPoint globalPosition);\n\tvoid dragLeft();\n\n\tvoid setIgnoreHiddenRowsOnSearch(bool value);\n\n\t// Interface for the controller.\n\tvoid appendRow(std::unique_ptr<PeerListRow> row);\n\tvoid appendSearchRow(std::unique_ptr<PeerListRow> row);\n\tvoid appendFoundRow(not_null<PeerListRow*> row);\n\tvoid prependRow(std::unique_ptr<PeerListRow> row);\n\tvoid prependRowFromSearchResult(not_null<PeerListRow*> row);\n\tPeerListRow *findRow(PeerListRowId id);\n\tstd::optional<QPoint> lastRowMousePosition() const;\n\tvoid updateRow(not_null<PeerListRow*> row) {\n\t\tupdateRow(row, RowIndex());\n\t}\n\tvoid removeRow(not_null<PeerListRow*> row);\n\tvoid convertRowToSearchResult(not_null<PeerListRow*> row);\n\tint fullRowsCount() const;\n\tnot_null<PeerListRow*> rowAt(int index) const;\n\tint searchRowsCount() const;\n\tnot_null<PeerListRow*> searchRowAt(int index) const;\n\tvoid setDescription(object_ptr<Ui::FlatLabel> description);\n\tvoid setSearchLoading(object_ptr<Ui::FlatLabel> loading);\n\tvoid setSearchNoResults(object_ptr<Ui::FlatLabel> noResults);\n\tvoid setAboveWidget(object_ptr<Ui::RpWidget> widget);\n\tvoid setAboveSearchWidget(object_ptr<Ui::RpWidget> widget);\n\tvoid setBelowWidget(object_ptr<Ui::RpWidget> width);\n\tvoid setHideEmpty(bool hide);\n\tvoid refreshRows();\n\n\tvoid mouseLeftGeometry();\n\tvoid pressLeftToContextMenu(bool shown);\n\tbool trackRowPressFromGlobal(QPoint globalPosition);\n\n\tvoid setSearchMode(PeerListSearchMode mode);\n\tvoid changeCheckState(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked,\n\t\tanim::type animated);\n\tvoid setRowHidden(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool hidden);\n\n\ttemplate <typename ReorderCallback>\n\tvoid reorderRows(ReorderCallback &&callback) {\n\t\tcallback(_rows.begin(), _rows.end());\n\t\tfor (auto &searchEntity : _searchIndex) {\n\t\t\tcallback(searchEntity.second.begin(), searchEntity.second.end());\n\t\t}\n\t\trefreshIndices();\n\t\tif (!_hiddenRows.empty()) {\n\t\t\tcallback(_filterResults.begin(), _filterResults.end());\n\t\t}\n\t\tupdate();\n\t}\n\n\t[[nodiscard]] std::unique_ptr<PeerListState> saveState() const;\n\tvoid restoreState(std::unique_ptr<PeerListState> state);\n\n\tvoid showRowMenu(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool highlightRow,\n\t\tFn<void(not_null<Ui::PopupMenu*>)> destroyed);\n\n\t[[nodiscard]] auto scrollToRequests() const {\n\t\treturn _scrollToRequests.events();\n\t}\n\n\t[[nodiscard]] auto noSearchSubmits() const {\n\t\treturn _noSearchSubmits.events();\n\t}\n\n\t~PeerListContent();\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\nprivate:\n\tvoid refreshIndices();\n\tvoid removeRowAtIndex(std::vector<std::unique_ptr<PeerListRow>> &from, int index);\n\tvoid handleNameChanged(not_null<PeerData*> peer);\n\n\tvoid invalidatePixmapsCache();\n\n\tstruct RowIndex {\n\t\tRowIndex() {\n\t\t}\n\t\texplicit RowIndex(int value) : value(value) {\n\t\t}\n\t\tint value = -1;\n\t};\n\tfriend inline bool operator==(RowIndex a, RowIndex b) {\n\t\treturn (a.value == b.value);\n\t}\n\tfriend inline bool operator!=(RowIndex a, RowIndex b) {\n\t\treturn !(a == b);\n\t}\n\n\tstruct Selected {\n\t\tSelected() {\n\t\t}\n\t\tSelected(RowIndex index, int element)\n\t\t: index(index)\n\t\t, element(element) {\n\t\t}\n\t\tSelected(int index, int element)\n\t\t: index(index)\n\t\t, element(element) {\n\t\t}\n\n\t\tRowIndex index;\n\t\tint element = 0;\n\t};\n\tfriend inline bool operator==(Selected a, Selected b) {\n\t\treturn (a.index == b.index) && (a.element == b.element);\n\t}\n\tfriend inline bool operator!=(Selected a, Selected b) {\n\t\treturn !(a == b);\n\t}\n\tstruct SelectedSaved {\n\t\tSelectedSaved(PeerListRowId id, Selected old)\n\t\t: id(id), old(old) {\n\t\t}\n\t\tPeerListRowId id = 0;\n\t\tSelected old;\n\t};\n\n\tvoid setSelected(Selected selected);\n\tvoid setPressed(Selected pressed);\n\tvoid setContexted(Selected contexted);\n\tvoid restoreSelection();\n\tSelectedSaved saveSelectedData(Selected from);\n\tSelected restoreSelectedData(SelectedSaved from);\n\n\tvoid selectByMouse(QPoint globalPosition);\n\tvoid loadProfilePhotos();\n\tvoid checkScrollForPreload();\n\n\tvoid updateRow(not_null<PeerListRow*> row, RowIndex hint);\n\tvoid updateRow(RowIndex row);\n\tint getRowTop(RowIndex row) const;\n\tPeerListRow *getRow(RowIndex element);\n\tRowIndex findRowIndex(\n\t\tnot_null<PeerListRow*> row,\n\t\tRowIndex hint = RowIndex());\n\tQRect getElementRect(\n\t\tnot_null<PeerListRow*> row,\n\t\tRowIndex index,\n\t\tint element) const;\n\n\tbool showRowMenu(\n\t\tRowIndex index,\n\t\tPeerListRow *row,\n\t\tQPoint globalPos,\n\t\tbool highlightRow,\n\t\tFn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr);\n\n\tcrl::time paintRow(Painter &p, crl::time now, RowIndex index);\n\n\tvoid addRowEntry(not_null<PeerListRow*> row);\n\tvoid addToSearchIndex(not_null<PeerListRow*> row);\n\tbool addingToSearchIndex() const;\n\tvoid removeFromSearchIndex(not_null<PeerListRow*> row);\n\tvoid setSearchQuery(const QString &query, const QString &normalizedQuery);\n\tbool showingSearch() const {\n\t\treturn !_hiddenRows.empty() || !_searchQuery.isEmpty();\n\t}\n\tint shownRowsCount() const {\n\t\treturn showingSearch() ? _filterResults.size() : _rows.size();\n\t}\n\ttemplate <typename Callback>\n\tbool enumerateShownRows(Callback callback);\n\ttemplate <typename Callback>\n\tbool enumerateShownRows(int from, int to, Callback callback);\n\n\tint rowsTop() const;\n\tint labelHeight() const;\n\n\tvoid clearSearchRows();\n\tvoid clearAllContent();\n\tvoid handleMouseMove(QPoint globalPosition);\n\tvoid mousePressReleased(Qt::MouseButton button);\n\tvoid initDecorateWidget(Ui::RpWidget *widget);\n\n\tconst style::PeerList &_st;\n\tnot_null<PeerListController*> _controller;\n\tPeerListSearchMode _searchMode = PeerListSearchMode::Disabled;\n\n\tMode _mode = Mode::Default;\n\tint _rowHeight = 0;\n\tint _visibleTop = 0;\n\tint _visibleBottom = 0;\n\n\tSelected _selected;\n\tSelected _pressed;\n\tSelected _contexted;\n\trpl::variable<int> _selectedIndex = -1;\n\tbool _mouseSelection = false;\n\tstd::optional<QPoint> _lastMousePosition;\n\tQt::MouseButton _pressButton = Qt::LeftButton;\n\tstd::optional<QPoint> _trackPressStart;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\n\tstd::vector<std::unique_ptr<PeerListRow>> _rows;\n\tstd::map<PeerListRowId, not_null<PeerListRow*>> _rowsById;\n\tstd::map<PeerData*, std::vector<not_null<PeerListRow*>>> _rowsByPeer;\n\n\tstd::map<QChar, std::vector<not_null<PeerListRow*>>> _searchIndex;\n\tQString _searchQuery;\n\tQString _normalizedSearchQuery;\n\tQString _mentionHighlight;\n\tstd::vector<not_null<PeerListRow*>> _filterResults;\n\tbase::flat_set<not_null<PeerListRow*>> _hiddenRows;\n\n\tint _aboveHeight = 0;\n\tint _belowHeight = 0;\n\tbool _hideEmpty = false;\n\tbool _ignoreHiddenRowsOnSearch = false;\n\tobject_ptr<Ui::RpWidget> _aboveWidget = { nullptr };\n\tobject_ptr<Ui::RpWidget> _aboveSearchWidget = { nullptr };\n\tobject_ptr<Ui::RpWidget> _belowWidget = { nullptr };\n\tobject_ptr<Ui::FlatLabel> _description = { nullptr };\n\tobject_ptr<Ui::FlatLabel> _searchNoResults = { nullptr };\n\tobject_ptr<Ui::FlatLabel> _searchLoading = { nullptr };\n\tobject_ptr<Ui::RpWidget> _loadingAnimation = { nullptr };\n\n\trpl::event_stream<> _noSearchSubmits;\n\n\tstd::vector<std::unique_ptr<PeerListRow>> _searchRows;\n\tbase::Timer _repaintByStatus;\n\tbase::unique_qptr<Ui::PopupMenu> _contextMenu;\n\n};\n\nclass PeerListContentDelegate : public PeerListDelegate {\npublic:\n\tvoid setContent(PeerListContent *content) {\n\t\t_content = content;\n\t}\n\n\tvoid peerListSetHideEmpty(bool hide) override {\n\t\t_content->setHideEmpty(hide);\n\t}\n\tvoid peerListAppendRow(\n\t\t\tstd::unique_ptr<PeerListRow> row) override {\n\t\t_content->appendRow(std::move(row));\n\t}\n\tvoid peerListAppendSearchRow(\n\t\t\tstd::unique_ptr<PeerListRow> row) override {\n\t\t_content->appendSearchRow(std::move(row));\n\t}\n\tvoid peerListAppendFoundRow(\n\t\t\tnot_null<PeerListRow*> row) override {\n\t\t_content->appendFoundRow(row);\n\t}\n\tvoid peerListPrependRow(\n\t\t\tstd::unique_ptr<PeerListRow> row) override {\n\t\t_content->prependRow(std::move(row));\n\t}\n\tvoid peerListPrependRowFromSearchResult(\n\t\t\tnot_null<PeerListRow*> row) override {\n\t\t_content->prependRowFromSearchResult(row);\n\t}\n\tPeerListRow *peerListFindRow(PeerListRowId id) override {\n\t\treturn _content->findRow(id);\n\t}\n\tstd::optional<QPoint> peerListLastRowMousePosition() override {\n\t\treturn _content->lastRowMousePosition();\n\t}\n\tvoid peerListUpdateRow(not_null<PeerListRow*> row) override {\n\t\t_content->updateRow(row);\n\t}\n\tvoid peerListRemoveRow(not_null<PeerListRow*> row) override {\n\t\t_content->removeRow(row);\n\t}\n\tvoid peerListConvertRowToSearchResult(\n\t\t\tnot_null<PeerListRow*> row) override {\n\t\t_content->convertRowToSearchResult(row);\n\t}\n\tvoid peerListSetRowChecked(\n\t\t\tnot_null<PeerListRow*> row,\n\t\t\tbool checked) override {\n\t\t_content->changeCheckState(row, checked, anim::type::normal);\n\t}\n\tvoid peerListSetRowHidden(\n\t\t\tnot_null<PeerListRow*> row,\n\t\t\tbool hidden) override {\n\t\t_content->setRowHidden(row, hidden);\n\t}\n\tvoid peerListSetForeignRowChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked,\n\t\tanim::type animated) override {\n\t}\n\tint peerListFullRowsCount() override {\n\t\treturn _content->fullRowsCount();\n\t}\n\tnot_null<PeerListRow*> peerListRowAt(int index) override {\n\t\treturn _content->rowAt(index);\n\t}\n\tint peerListSearchRowsCount() override {\n\t\treturn _content->searchRowsCount();\n\t}\n\tnot_null<PeerListRow*> peerListSearchRowAt(int index) override {\n\t\treturn _content->searchRowAt(index);\n\t}\n\tvoid peerListRefreshRows() override {\n\t\t_content->refreshRows();\n\t}\n\tvoid peerListSetDescription(object_ptr<Ui::FlatLabel> description) override {\n\t\t_content->setDescription(std::move(description));\n\t}\n\tvoid peerListSetSearchNoResults(object_ptr<Ui::FlatLabel> noResults) override {\n\t\t_content->setSearchNoResults(std::move(noResults));\n\t}\n\tvoid peerListSetAboveWidget(object_ptr<Ui::RpWidget> aboveWidget) override {\n\t\t_content->setAboveWidget(std::move(aboveWidget));\n\t}\n\tvoid peerListSetAboveSearchWidget(object_ptr<Ui::RpWidget> aboveWidget) override {\n\t\t_content->setAboveSearchWidget(std::move(aboveWidget));\n\t}\n\tvoid peerListSetBelowWidget(object_ptr<Ui::RpWidget> belowWidget) override {\n\t\t_content->setBelowWidget(std::move(belowWidget));\n\t}\n\tvoid peerListSetSearchMode(PeerListSearchMode mode) override {\n\t\t_content->setSearchMode(mode);\n\t}\n\tvoid peerListMouseLeftGeometry() override {\n\t\t_content->mouseLeftGeometry();\n\t}\n\tvoid peerListSortRows(\n\t\t\tFn<bool(const PeerListRow &a, const PeerListRow &b)> compare) override {\n\t\t_content->reorderRows([&](\n\t\t\t\tauto &&begin,\n\t\t\t\tauto &&end) {\n\t\t\tstd::stable_sort(begin, end, [&](auto &&a, auto &&b) {\n\t\t\t\treturn compare(*a, *b);\n\t\t\t});\n\t\t});\n\t}\n\tint peerListPartitionRows(\n\t\t\tFn<bool(const PeerListRow &a)> border) override {\n\t\tauto result = 0;\n\t\t_content->reorderRows([&](\n\t\t\t\tauto &&begin,\n\t\t\t\tauto &&end) {\n\t\t\tauto edge = std::stable_partition(begin, end, [&](\n\t\t\t\t\tauto &&current) {\n\t\t\t\treturn border(*current);\n\t\t\t});\n\t\t\tresult = (edge - begin);\n\t\t});\n\t\treturn result;\n\t}\n\tstd::unique_ptr<PeerListState> peerListSaveState() const override {\n\t\treturn _content->saveState();\n\t}\n\tvoid peerListRestoreState(\n\t\t\tstd::unique_ptr<PeerListState> state) override {\n\t\t_content->restoreState(std::move(state));\n\t}\n\tvoid peerListShowRowMenu(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool highlightRow,\n\t\tFn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr) override;\n\n\tvoid peerListSelectSkip(int direction) override {\n\t\t_content->selectSkip(direction);\n\t}\n\n\tvoid peerListPressLeftToContextMenu(bool shown) override {\n\t\t_content->pressLeftToContextMenu(shown);\n\t}\n\tbool peerListTrackRowPressFromGlobal(QPoint globalPosition) override {\n\t\treturn _content->trackRowPressFromGlobal(globalPosition);\n\t}\n\nprotected:\n\tnot_null<PeerListContent*> content() const {\n\t\treturn _content;\n\t}\n\nprivate:\n\tPeerListContent *_content = nullptr;\n\n};\n\nclass PeerListContentDelegateSimple : public PeerListContentDelegate {\npublic:\n\tvoid peerListSetTitle(rpl::producer<QString> title) override {\n\t}\n\tvoid peerListSetAdditionalTitle(rpl::producer<QString> title) override {\n\t}\n\tbool peerListIsRowChecked(not_null<PeerListRow*> row) override {\n\t\treturn false;\n\t}\n\tint peerListSelectedRowsCount() override {\n\t\treturn 0;\n\t}\n\tvoid peerListScrollToTop() override {\n\t}\n\tvoid peerListAddSelectedPeerInBunch(\n\t\t\tnot_null<PeerData*> peer) override {\n\t\tUnexpected(\"...DelegateSimple::peerListAddSelectedPeerInBunch\");\n\t}\n\tvoid peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) override {\n\t\tUnexpected(\"...DelegateSimple::peerListAddSelectedRowInBunch\");\n\t}\n\tvoid peerListFinishSelectedRowsBunch() override {\n\t\tUnexpected(\"...DelegateSimple::peerListFinishSelectedRowsBunch\");\n\t}\n\tvoid peerListSetDescription(\n\t\t\tobject_ptr<Ui::FlatLabel> description) override {\n\t\tdescription.destroy();\n\t}\n\tstd::shared_ptr<Main::SessionShow> peerListUiShow() override {\n\t\tUnexpected(\"...DelegateSimple::peerListUiShow\");\n\t}\n\n};\n\nclass PeerListContentDelegateShow : public PeerListContentDelegateSimple {\npublic:\n\texplicit PeerListContentDelegateShow(\n\t\tstd::shared_ptr<Main::SessionShow> show);\n\tstd::shared_ptr<Main::SessionShow> peerListUiShow() override;\n\nprivate:\n\tstd::shared_ptr<Main::SessionShow> _show;\n\n};\n\nclass PeerListBox\n\t: public Ui::BoxContent\n\t, public PeerListContentDelegate {\npublic:\n\tPeerListBox(\n\t\tQWidget*,\n\t\tstd::unique_ptr<PeerListController> controller,\n\t\tFn<void(not_null<PeerListBox*>)> init);\n\n\t[[nodiscard]] std::vector<PeerListRowId> collectSelectedIds();\n\t[[nodiscard]] std::vector<not_null<PeerData*>> collectSelectedRows();\n\t[[nodiscard]] rpl::producer<int> multiSelectHeightValue() const;\n\t[[nodiscard]] rpl::producer<> noSearchSubmits() const;\n\n\tvoid peerListSetTitle(rpl::producer<QString> title) override {\n\t\tsetTitle(std::move(title));\n\t}\n\tvoid peerListSetAdditionalTitle(rpl::producer<QString> title) override {\n\t\tsetAdditionalTitle(std::move(title));\n\t}\n\tvoid peerListSetSearchMode(PeerListSearchMode mode) override;\n\tvoid peerListSetRowChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked) override;\n\tvoid peerListSetForeignRowChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked,\n\t\tanim::type animated) override;\n\tbool peerListIsRowChecked(not_null<PeerListRow*> row) override;\n\tint peerListSelectedRowsCount() override;\n\tvoid peerListScrollToTop() override;\n\tstd::shared_ptr<Main::SessionShow> peerListUiShow() override;\n\n\tvoid setAddedTopScrollSkip(int skip, bool aboveSearch = false);\n\n\tvoid showFinished() override;\n\n\tvoid appendQueryChangedCallback(Fn<void(QString)>);\n\nprotected:\n\tvoid prepare() override;\n\tvoid setInnerFocus() override;\n\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid peerListAddSelectedPeerInBunch(\n\t\t\tnot_null<PeerData*> peer) override {\n\t\taddSelectItem(peer, anim::type::instant);\n\t}\n\tvoid peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) override {\n\t\taddSelectItem(row, anim::type::instant);\n\t}\n\tvoid peerListFinishSelectedRowsBunch() override;\n\n\tvoid addSelectItem(\n\t\tnot_null<PeerData*> peer,\n\t\tanim::type animated);\n\tvoid addSelectItem(\n\t\tnot_null<PeerListRow*> row,\n\t\tanim::type animated);\n\tvoid addSelectItem(\n\t\tuint64 itemId,\n\t\tconst QString &text,\n\t\tPaintRoundImageCallback paintUserpic,\n\t\tanim::type animated);\n\tvoid createMultiSelect();\n\t[[nodiscard]] int topScrollSkip() const;\n\t[[nodiscard]] int topSelectSkip() const;\n\tvoid updateScrollSkips();\n\tvoid searchQueryChanged(const QString &query);\n\n\tobject_ptr<Ui::SlideWrap<Ui::MultiSelect>> _select = { nullptr };\n\n\tconst std::shared_ptr<Main::SessionShow> _show;\n\tFn<void(QString)> _customQueryChangedCallback;\n\tstd::unique_ptr<PeerListController> _controller;\n\tFn<void(PeerListBox*)> _init;\n\tbool _scrollBottomFixed = false;\n\tbool _addedTopScrollAboveSearch = false;\n\tint _addedTopScrollSkip = 0;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peer_list_controllers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peer_list_controllers.h\"\n\n#include \"api/api_chat_participants.h\"\n#include \"api/api_premium.h\" // MessageMoneyRestriction.\n#include \"base/random.h\"\n#include \"boxes/filters/edit_filter_chats_list.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/effects/round_checkbox.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"main/main_session.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_changes.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"apiwrap.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"lang/lang_keys.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"dialogs/dialogs_main_list.h\"\n#include \"payments/ui/payments_reaction_box.h\"\n#include \"ui/effects/outline_segments.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"window/window_separate_id.h\"\n#include \"window/window_session_controller.h\" // showAddContact()\n#include \"base/unixtime.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_profile.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace {\n\nconstexpr auto kSortByOnlineThrottle = 3 * crl::time(1000);\nconstexpr auto kSearchPerPage = 50;\n\n} // namespace\n\nobject_ptr<Ui::BoxContent> PrepareContactsBox(\n\t\tnot_null<Window::SessionController*> window) {\n\tusing Mode = ContactsBoxController::SortMode;\n\tclass Controller final : public ContactsBoxController {\n\tpublic:\n\t\tusing ContactsBoxController::ContactsBoxController;\n\n\t\t[[nodiscard]] rpl::producer<not_null<PeerData*>> wheelClicks() const {\n\t\t\treturn _wheelClicks.events();\n\t\t}\n\n\tprotected:\n\t\tstd::unique_ptr<PeerListRow> createRow(\n\t\t\t\tnot_null<UserData*> user) override {\n\t\t\treturn !user->isSelf()\n\t\t\t\t? ContactsBoxController::createRow(user)\n\t\t\t\t: nullptr;\n\t\t}\n\n\t\tvoid rowMiddleClicked(\n\t\t\t\tnot_null<PeerListRow*> row) override {\n\t\t\t_wheelClicks.fire(row->peer());\n\t\t}\n\n\tprivate:\n\t\trpl::event_stream<not_null<PeerData*>> _wheelClicks;\n\n\t};\n\tauto controller = std::make_unique<Controller>(\n\t\t&window->session());\n\tcontroller->setStyleOverrides(&st::contactsWithStories);\n\tcontroller->setStoriesShown(true);\n\tconst auto raw = controller.get();\n\tauto init = [=](not_null<PeerListBox*> box) {\n\t\tstruct State {\n\t\t\tQPointer<::Ui::IconButton> toggleSort;\n\t\t\trpl::variable<Mode> mode = Mode::Online;\n\t\t\t::Ui::Animations::Simple scrollAnimation;\n\t\t};\n\n\t\tconst auto state = box->lifetime().make_state<State>();\n\t\tbox->addButton(tr::lng_close(), [=] { box->closeBox(); });\n\t\tbox->addLeftButton(\n\t\t\ttr::lng_profile_add_contact(),\n\t\t\t[=] { window->showAddContact(); });\n\t\tstate->toggleSort = box->addTopButton(st::contactsSortButton, [=] {\n\t\t\tconst auto online = (state->mode.current() == Mode::Online);\n\t\t\tconst auto mode = online ? Mode::Alphabet : Mode::Online;\n\t\t\tstate->mode = mode;\n\t\t\traw->setSortMode(mode);\n\t\t\tstate->toggleSort->setIconOverride(\n\t\t\t\tonline ? &st::contactsSortOnlineIcon : nullptr,\n\t\t\t\tonline ? &st::contactsSortOnlineIconOver : nullptr);\n\t\t});\n\t\traw->setSortMode(Mode::Online);\n\n\t\traw->wheelClicks() | rpl::on_next([=](not_null<PeerData*> p) {\n\t\t\twindow->showInNewWindow(p);\n\t\t}, box->lifetime());\n\n\t\traw->setShowFinishedCallback([=] {\n\t\t\twindow->checkHighlightControl(\n\t\t\t\tu\"contacts/sort\"_q,\n\t\t\t\tstate->toggleSort,\n\t\t\t\t{ .rippleShape = true });\n\t\t});\n\t};\n\treturn Box<PeerListBox>(std::move(controller), std::move(init));\n}\n\nQBrush PeerListStoriesGradient(const style::PeerList &st) {\n\tconst auto left = st.item.photoPosition.x();\n\tconst auto top = st.item.photoPosition.y();\n\tconst auto size = st.item.photoSize;\n\treturn Ui::UnreadStoryOutlineGradient(QRectF(left, top, size, size));\n}\n\nstd::vector<Ui::OutlineSegment> PeerListStoriesSegments(\n\t\tPeerListStoriesCounts counts,\n\t\tconst QBrush &unreadBrush) {\n\tExpects(counts.unread <= counts.count);\n\tExpects(counts.count > 0);\n\n\tauto result = std::vector<Ui::OutlineSegment>();\n\tconst auto add = [&](bool unread) {\n\t\tresult.push_back({\n\t\t\t.brush = (counts.videoStream\n\t\t\t\t? st::attentionButtonFg->b\n\t\t\t\t: unread\n\t\t\t\t? unreadBrush\n\t\t\t\t: st::dialogsUnreadBgMuted->b),\n\t\t\t.width = (unread\n\t\t\t\t? st::dialogsStoriesFull.lineTwice / 2.\n\t\t\t\t: st::dialogsStoriesFull.lineReadTwice / 2.),\n\t\t});\n\t};\n\tif (counts.videoStream) {\n\t\tadd(true);\n\t} else {\n\t\tconst auto count = counts.count;\n\t\tconst auto unread = counts.unread;\n\t\tresult.reserve(count);\n\t\tfor (auto i = 0, till = count - unread; i != till; ++i) {\n\t\t\tadd(false);\n\t\t}\n\t\tfor (auto i = 0; i != unread; ++i) {\n\t\t\tadd(true);\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid PeerListRowWithLink::setActionLink(const QString &action) {\n\t_action = action;\n\trefreshActionLink();\n}\n\nvoid PeerListRowWithLink::refreshActionLink() {\n\tif (!isInitialized()) return;\n\t_actionWidth = _action.isEmpty() ? 0 : st::normalFont->width(_action);\n}\n\nvoid PeerListRowWithLink::lazyInitialize(const style::PeerListItem &st) {\n\tPeerListRow::lazyInitialize(st);\n\trefreshActionLink();\n}\n\nQSize PeerListRowWithLink::rightActionSize() const {\n\treturn QSize(_actionWidth, st::normalFont->height);\n}\n\nQMargins PeerListRowWithLink::rightActionMargins() const {\n\treturn QMargins(\n\t\tst::contactsCheckPosition.x(),\n\t\t(st::contactsPadding.top() + st::contactsPhotoSize + st::contactsPadding.bottom() - st::normalFont->height) / 2,\n\t\tst::defaultPeerListItem.photoPosition.x() + st::contactsCheckPosition.x(),\n\t\t0);\n}\n\nvoid PeerListRowWithLink::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\tp.setFont(actionSelected ? st::linkFontOver : st::linkFont);\n\tp.setPen(actionSelected ? st::defaultLinkButton.overColor : st::defaultLinkButton.color);\n\tp.drawTextLeft(x, y, outerWidth, _action, _actionWidth);\n}\n\nPeerListGlobalSearchController::PeerListGlobalSearchController(\n\tnot_null<Main::Session*> session)\n: _session(session)\n, _api(&session->mtp()) {\n\t_timer.setCallback([this] { searchOnServer(); });\n}\n\nvoid PeerListGlobalSearchController::searchQuery(const QString &query) {\n\tif (_query != query) {\n\t\t_query = query;\n\t\t_requestId = 0;\n\t\tif (!_query.isEmpty() && !searchInCache()) {\n\t\t\t_timer.callOnce(AutoSearchTimeout);\n\t\t} else {\n\t\t\t_timer.cancel();\n\t\t}\n\t}\n}\n\nbool PeerListGlobalSearchController::searchInCache() {\n\tauto it = _cache.find(_query);\n\tif (it != _cache.cend()) {\n\t\t_requestId = 0;\n\t\tsearchDone(it->second, _requestId);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid PeerListGlobalSearchController::searchOnServer() {\n\t_requestId = _api.request(MTPcontacts_Search(\n\t\tMTP_string(_query),\n\t\tMTP_int(SearchPeopleLimit)\n\t)).done([=](const MTPcontacts_Found &result, mtpRequestId requestId) {\n\t\tsearchDone(result, requestId);\n\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\tif (_requestId == requestId) {\n\t\t\t_requestId = 0;\n\t\t\tdelegate()->peerListSearchRefreshRows();\n\t\t}\n\t}).send();\n\t_queries.emplace(_requestId, _query);\n}\n\nvoid PeerListGlobalSearchController::searchDone(\n\t\tconst MTPcontacts_Found &result,\n\t\tmtpRequestId requestId) {\n\tExpects(result.type() == mtpc_contacts_found);\n\n\tauto &contacts = result.c_contacts_found();\n\tauto query = _query;\n\tif (requestId) {\n\t\t_session->data().processUsers(contacts.vusers());\n\t\t_session->data().processChats(contacts.vchats());\n\t\tauto it = _queries.find(requestId);\n\t\tif (it != _queries.cend()) {\n\t\t\tquery = it->second;\n\t\t\t_cache[query] = result;\n\t\t\t_queries.erase(it);\n\t\t}\n\t}\n\tconst auto feedList = [&](const MTPVector<MTPPeer> &list) {\n\t\tfor (const auto &mtpPeer : list.v) {\n\t\t\tconst auto peer = _session->data().peerLoaded(\n\t\t\t\tpeerFromMTP(mtpPeer));\n\t\t\tif (peer) {\n\t\t\t\tdelegate()->peerListSearchAddRow(peer);\n\t\t\t}\n\t\t}\n\t};\n\tif (_requestId == requestId) {\n\t\t_requestId = 0;\n\t\tfeedList(contacts.vmy_results());\n\t\tfeedList(contacts.vresults());\n\t\tdelegate()->peerListSearchRefreshRows();\n\t}\n}\n\nbool PeerListGlobalSearchController::isLoading() {\n\treturn _timer.isActive() || _requestId;\n}\n\nstruct RecipientRow::Restriction {\n\tApi::MessageMoneyRestriction value;\n\tRestrictionBadgeCache cache;\n};\n\nRecipientRow::RecipientRow(\n\tnot_null<PeerData*> peer,\n\tconst style::PeerListItem *maybeLockedSt,\n\tHistory *maybeHistory)\n: PeerListRow(peer)\n, _maybeHistory(maybeHistory)\n, _maybeLockedSt(maybeLockedSt) {\n\tif (_maybeLockedSt) {\n\t\tsetRestriction(Api::ResolveMessageMoneyRestrictions(\n\t\t\tpeer,\n\t\t\tmaybeHistory));\n\t}\n}\n\nApi::MessageMoneyRestriction RecipientRow::restriction() const {\n\treturn _restriction\n\t\t? _restriction->value\n\t\t: Api::MessageMoneyRestriction();\n}\n\nvoid RecipientRow::setRestriction(Api::MessageMoneyRestriction restriction) {\n\tif (!restriction) {\n\t\t_restriction = nullptr;\n\t\treturn;\n\t} else if (!_restriction) {\n\t\t_restriction = std::make_unique<Restriction>();\n\t}\n\t_restriction->value = restriction;\n}\n\nvoid RecipientRow::paintUserpicOverlay(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth) {\n\tif (const auto &r = _restriction) {\n\t\tPaintRestrictionBadge(\n\t\t\tp,\n\t\t\t_maybeLockedSt,\n\t\t\tr->value.starsPerMessage,\n\t\t\tr->cache,\n\t\t\tx,\n\t\t\ty,\n\t\t\touterWidth,\n\t\t\tst.photoSize);\n\t}\n}\n\nbool RecipientRow::refreshLock(\n\t\tnot_null<const style::PeerListItem*> maybeLockedSt) {\n\tif (const auto user = peer()->asUser()) {\n\t\tusing Restriction = Api::MessageMoneyRestriction;\n\t\tconst auto r = _maybeLockedSt\n\t\t\t? Api::ResolveMessageMoneyRestrictions(\n\t\t\t\tuser,\n\t\t\t\t_maybeHistory)\n\t\t\t: Restriction();\n\t\tif ((_restriction ? _restriction->value : Restriction()) != r) {\n\t\t\tsetRestriction(r);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid RecipientRow::preloadUserpic() {\n\tPeerListRow::preloadUserpic();\n\n\tif (!_maybeLockedSt) {\n\t\treturn;\n\t}\n\tconst auto peer = this->peer();\n\tconst auto known = Api::ResolveMessageMoneyRestrictions(\n\t\tpeer,\n\t\t_maybeHistory).known;\n\tif (known) {\n\t\treturn;\n\t} else if (const auto user = peer->asUser()) {\n\t\tconst auto api = &user->session().api();\n\t\tapi->premium().resolveMessageMoneyRestrictions(user);\n\t} else if (const auto group = peer->asChannel()) {\n\t\tgroup->updateFull();\n\t}\n}\n\nvoid TrackMessageMoneyRestrictionsChanges(\n\t\tnot_null<PeerListController*> controller,\n\t\trpl::lifetime &lifetime) {\n\tconst auto session = &controller->session();\n\trpl::merge(\n\t\tData::AmPremiumValue(session) | rpl::to_empty,\n\t\tsession->api().premium().someMessageMoneyRestrictionsResolved()\n\t) | rpl::on_next([=] {\n\t\tconst auto st = &controller->computeListSt().item;\n\t\tconst auto delegate = controller->delegate();\n\t\tconst auto process = [&](not_null<PeerListRow*> raw) {\n\t\t\tif (static_cast<RecipientRow*>(raw.get())->refreshLock(st)) {\n\t\t\t\tdelegate->peerListUpdateRow(raw);\n\t\t\t}\n\t\t};\n\t\tauto count = delegate->peerListFullRowsCount();\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tprocess(delegate->peerListRowAt(i));\n\t\t}\n\t\tcount = delegate->peerListSearchRowsCount();\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tprocess(delegate->peerListSearchRowAt(i));\n\t\t}\n\t}, lifetime);\n}\n\nChatsListBoxController::Row::Row(\n\tnot_null<History*> history,\n\tconst style::PeerListItem *maybeLockedSt)\n: RecipientRow(history->peer, maybeLockedSt, history) {\n}\n\nChatsListBoxController::ChatsListBoxController(\n\tnot_null<Main::Session*> session)\n: ChatsListBoxController(\n\tstd::make_unique<PeerListGlobalSearchController>(session)) {\n}\n\nChatsListBoxController::ChatsListBoxController(\n\tstd::unique_ptr<PeerListSearchController> searchController)\n: PeerListController(std::move(searchController)) {\n}\n\nvoid ChatsListBoxController::prepare() {\n\tsetSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now));\n\tdelegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);\n\n\tprepareViewHook();\n\n\tif (!session().data().chatsListLoaded()) {\n\t\tsession().data().chatsListLoadedEvents(\n\t\t) | rpl::filter([=](Data::Folder *folder) {\n\t\t\treturn !folder;\n\t\t}) | rpl::on_next([=] {\n\t\t\tcheckForEmptyRows();\n\t\t}, lifetime());\n\t}\n\n\tsession().data().chatsListChanges(\n\t) | rpl::on_next([=] {\n\t\trebuildRows();\n\t}, lifetime());\n\n\tsession().data().contactsLoaded().value(\n\t) | rpl::on_next([=] {\n\t\trebuildRows();\n\t}, lifetime());\n}\n\nvoid ChatsListBoxController::rebuildRows() {\n\tauto wasEmpty = !delegate()->peerListFullRowsCount();\n\tauto appendList = [this](auto chats) {\n\t\tauto count = 0;\n\t\tfor (const auto &row : chats->all()) {\n\t\t\tif (const auto history = row->history()) {\n\t\t\t\tif (appendRow(history)) {\n\t\t\t\t\t++count;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn count;\n\t};\n\tauto added = 0;\n\tif (!savedMessagesChatStatus().isEmpty()) {\n\t\tif (appendRow(session().data().history(session().user()))) {\n\t\t\t++added;\n\t\t}\n\t}\n\tadded += appendList(session().data().chatsList()->indexed());\n\tconst auto id = Data::Folder::kId;\n\tif (const auto folder = session().data().folderLoaded(id)) {\n\t\tadded += appendList(folder->chatsList()->indexed());\n\t}\n\tadded += appendList(session().data().contactsNoChatsList());\n\tif (!wasEmpty && added > 0) {\n\t\t// Place dialogs list before contactsNoDialogs list.\n\t\tdelegate()->peerListPartitionRows([](const PeerListRow &a) {\n\t\t\tconst auto history = static_cast<const Row&>(a).history();\n\t\t\treturn history->inChatList();\n\t\t});\n\t\tif (!savedMessagesChatStatus().isEmpty()) {\n\t\t\tdelegate()->peerListPartitionRows([](const PeerListRow &a) {\n\t\t\t\treturn a.peer()->isSelf();\n\t\t\t});\n\t\t}\n\t}\n\tcheckForEmptyRows();\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid ChatsListBoxController::checkForEmptyRows() {\n\tif (delegate()->peerListFullRowsCount()) {\n\t\tsetDescriptionText(QString());\n\t} else {\n\t\tconst auto loaded = session().data().contactsLoaded().current()\n\t\t\t&& session().data().chatsListLoaded();\n\t\tsetDescriptionText(loaded ? emptyBoxText() : tr::lng_contacts_loading(tr::now));\n\t}\n}\n\nQString ChatsListBoxController::emptyBoxText() const {\n\treturn tr::lng_contacts_not_found(tr::now);\n}\n\nstd::unique_ptr<PeerListRow> ChatsListBoxController::createSearchRow(\n\t\tnot_null<PeerData*> peer) {\n\treturn createRow(peer->owner().history(peer));\n}\n\nbool ChatsListBoxController::appendRow(not_null<History*> history) {\n\tif (auto row = delegate()->peerListFindRow(history->peer->id.value)) {\n\t\tupdateRowHook(static_cast<Row*>(row));\n\t\treturn false;\n\t}\n\tif (auto row = createRow(history)) {\n\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nPeerListStories::PeerListStories(\n\tnot_null<PeerListController*> controller,\n\tnot_null<Main::Session*> session)\n: _controller(controller)\n, _session(session) {\n}\n\nvoid PeerListStories::updateColors() {\n\tfor (auto i = begin(_counts); i != end(_counts); ++i) {\n\t\tif (const auto row = _delegate->peerListFindRow(i->first)) {\n\t\t\tif (i->second.count >= 0 && i->second.unread >= 0) {\n\t\t\t\tapplyForRow(row, i->second, true);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid PeerListStories::updateFor(uint64 id, Counts counts) {\n\tif (const auto row = _delegate->peerListFindRow(id)) {\n\t\tapplyForRow(row, counts);\n\t\t_delegate->peerListUpdateRow(row);\n\t}\n}\n\nvoid PeerListStories::process(not_null<PeerListRow*> row) {\n\tconst auto user = row->peer()->asUser();\n\tif (!user) {\n\t\treturn;\n\t}\n\tconst auto stories = &_session->data().stories();\n\tconst auto source = stories->source(user->id);\n\tconst auto count = source\n\t\t? int(source->ids.size())\n\t\t: user->hasActiveStories()\n\t\t? 1\n\t\t: 0;\n\tconst auto unread = source\n\t\t? int(source->info().unreadCount)\n\t\t: user->hasUnreadStories()\n\t\t? 1\n\t\t: 0;\n\tconst auto videoStream = source\n\t\t? bool(source->info().hasVideoStream)\n\t\t: user->hasActiveVideoStream();\n\tapplyForRow(row, { count, unread, videoStream }, true);\n}\n\nbool PeerListStories::handleClick(not_null<PeerData*> peer) {\n\tconst auto point = _delegate->peerListLastRowMousePosition();\n\tconst auto &st = _controller->computeListSt().item;\n\tif (point && point->x() < st.photoPosition.x() + st.photoSize) {\n\t\tif (const auto window = peer->session().tryResolveWindow()) {\n\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\tif (user->hasActiveStories()) {\n\t\t\t\t\twindow->openPeerStories(peer->id);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid PeerListStories::prepare(not_null<PeerListDelegate*> delegate) {\n\t_delegate = delegate;\n\n\t_unreadBrush = PeerListStoriesGradient(_controller->computeListSt());\n\tstyle::PaletteChanged() | rpl::on_next([=] {\n\t\t_unreadBrush = PeerListStoriesGradient(_controller->computeListSt());\n\t\tupdateColors();\n\t}, _lifetime);\n\n\t_session->changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::StoriesState\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tconst auto id = update.peer->id.value;\n\t\tif (const auto row = _delegate->peerListFindRow(id)) {\n\t\t\tprocess(row);\n\t\t}\n\t}, _lifetime);\n\n\tconst auto stories = &_session->data().stories();\n\tstories->sourceChanged() | rpl::on_next([=](PeerId id) {\n\t\tconst auto source = stories->source(id);\n\t\tconst auto info = source\n\t\t\t? source->info()\n\t\t\t: Data::StoriesSourceInfo();\n\t\tupdateFor(id.value, {\n\t\t\tint(info.count),\n\t\t\tint(info.unreadCount),\n\t\t\tbool(info.hasVideoStream),\n\t\t});\n\t}, _lifetime);\n}\n\nvoid PeerListStories::applyForRow(\n\t\tnot_null<PeerListRow*> row,\n\t\tCounts counts,\n\t\tbool force) {\n\tauto &existing = _counts[row->id()];\n\tif (!force && existing == counts) {\n\t\treturn;\n\t}\n\texisting = counts;\n\t_delegate->peerListSetRowChecked(row, counts.count > 0);\n\tif (counts.count > 0) {\n\t\trow->setCustomizedCheckSegments(\n\t\t\tPeerListStoriesSegments(counts, _unreadBrush),\n\t\t\tcounts.videoStream);\n\t}\n}\n\nContactsBoxController::ContactsBoxController(\n\tnot_null<Main::Session*> session)\n: ContactsBoxController(\n\tsession,\n\tstd::make_unique<PeerListGlobalSearchController>(session)) {\n}\n\nContactsBoxController::ContactsBoxController(\n\tnot_null<Main::Session*> session,\n\tstd::unique_ptr<PeerListSearchController> searchController)\n: PeerListController(std::move(searchController))\n, _session(session)\n, _sortByOnlineTimer([=] { sort(); }) {\n}\n\nMain::Session &ContactsBoxController::session() const {\n\treturn *_session;\n}\n\nvoid ContactsBoxController::prepare() {\n\tsetSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now));\n\tdelegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);\n\tdelegate()->peerListSetTitle(tr::lng_contacts_header());\n\n\tprepareViewHook();\n\n\tif (_stories) {\n\t\t_stories->prepare(delegate());\n\t}\n\n\tsession().data().contactsLoaded().value(\n\t) | rpl::on_next([=] {\n\t\trebuildRows();\n\t}, lifetime());\n}\n\nvoid ContactsBoxController::rebuildRows() {\n\tconst auto appendList = [&](auto chats) {\n\t\tauto count = 0;\n\t\tfor (const auto &row : chats->all()) {\n\t\t\tif (const auto history = row->history()) {\n\t\t\t\tif (const auto user = history->peer->asUser()) {\n\t\t\t\t\tif (appendRow(user)) {\n\t\t\t\t\t\t++count;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn count;\n\t};\n\tappendList(session().data().contactsList());\n\tcheckForEmptyRows();\n\tsort();\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid ContactsBoxController::checkForEmptyRows() {\n\tsetDescriptionText(delegate()->peerListFullRowsCount()\n\t\t? QString()\n\t\t: session().data().contactsLoaded().current()\n\t\t? tr::lng_contacts_not_found(tr::now)\n\t\t: tr::lng_contacts_loading(tr::now));\n}\n\nstd::unique_ptr<PeerListRow> ContactsBoxController::createSearchRow(\n\t\tnot_null<PeerData*> peer) {\n\tif (const auto user = peer->asUser()) {\n\t\treturn createRow(user);\n\t}\n\treturn nullptr;\n}\n\nvoid ContactsBoxController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto peer = row->peer();\n\tif (_stories && _stories->handleClick(peer)) {\n\t\treturn;\n\t} else if (const auto window = peer->session().tryResolveWindow()) {\n\t\twindow->showPeerHistory(peer);\n\t}\n}\n\nvoid ContactsBoxController::setSortMode(SortMode mode) {\n\tif (_sortMode == mode) {\n\t\treturn;\n\t}\n\t_sortMode = mode;\n\tsort();\n\tif (_sortMode == SortMode::Online) {\n\t\tsession().changes().peerUpdates(\n\t\t\tData::PeerUpdate::Flag::OnlineStatus\n\t\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\t\treturn !_sortByOnlineTimer.isActive()\n\t\t\t\t&& delegate()->peerListFindRow(update.peer->id.value);\n\t\t}) | rpl::on_next([=] {\n\t\t\t_sortByOnlineTimer.callOnce(kSortByOnlineThrottle);\n\t\t}, _sortByOnlineLifetime);\n\t} else {\n\t\t_sortByOnlineTimer.cancel();\n\t\t_sortByOnlineLifetime.destroy();\n\t}\n}\n\nvoid ContactsBoxController::setStoriesShown(bool shown) {\n\t_stories = std::make_unique<PeerListStories>(this, _session);\n}\n\nvoid ContactsBoxController::sort() {\n\tswitch (_sortMode) {\n\tcase SortMode::Alphabet: sortByName(); break;\n\tcase SortMode::Online: sortByOnline(); break;\n\tdefault: Unexpected(\"SortMode in ContactsBoxController.\");\n\t}\n}\n\nvoid ContactsBoxController::sortByOnline() {\n\tconst auto now = base::unixtime::now();\n\tconst auto key = [&](const PeerListRow &row) {\n\t\tconst auto user = row.peer()->asUser();\n\t\treturn user\n\t\t\t? (std::min(user->lastseen().onlineTill(), now + 1) + 1)\n\t\t\t: TimeId();\n\t};\n\tconst auto predicate = [&](const PeerListRow &a, const PeerListRow &b) {\n\t\treturn key(a) > key(b);\n\t};\n\tdelegate()->peerListSortRows(predicate);\n}\n\nbool ContactsBoxController::appendRow(not_null<UserData*> user) {\n\tif (auto row = delegate()->peerListFindRow(user->id.value)) {\n\t\tupdateRowHook(row);\n\t\treturn false;\n\t}\n\tif (auto row = createRow(user)) {\n\t\tconst auto raw = row.get();\n\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\tif (_stories) {\n\t\t\t_stories->process(raw);\n\t\t}\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nstd::unique_ptr<PeerListRow> ContactsBoxController::createRow(\n\t\tnot_null<UserData*> user) {\n\treturn std::make_unique<PeerListRow>(user);\n}\n\nRecipientMoneyRestrictionError WriteMoneyRestrictionError(\n\t\tnot_null<UserData*> user) {\n\treturn {\n\t\t.text = tr::lng_send_non_premium_message_toast(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\tTextWithEntities{ user->shortName() },\n\t\t\tlt_link,\n\t\t\ttr::link(\n\t\t\t\ttr::bold(\n\t\t\t\t\ttr::lng_send_non_premium_message_toast_link(\n\t\t\t\t\t\ttr::now))),\n\t\t\ttr::rich),\n\t};\n}\n\nChooseRecipientBoxController::ChooseRecipientBoxController(\n\tnot_null<Main::Session*> session,\n\tFnMut<void(not_null<Data::Thread*>)> callback,\n\tFn<bool(not_null<Data::Thread*>)> filter)\n: ChooseRecipientBoxController({\n\t.session = session,\n\t.callback = std::move(callback),\n\t.filter = std::move(filter),\n}) {\n}\n\nChooseRecipientBoxController::ChooseRecipientBoxController(\n\tChooseRecipientArgs &&args)\n: ChatsListBoxController(args.session)\n, _session(args.session)\n, _callback(std::move(args.callback))\n, _filter(std::move(args.filter))\n, _moneyRestrictionError(std::move(args.moneyRestrictionError)) {\n}\n\nMain::Session &ChooseRecipientBoxController::session() const {\n\treturn *_session;\n}\n\nvoid ChooseRecipientBoxController::prepareViewHook() {\n\tdelegate()->peerListSetTitle(tr::lng_forward_choose());\n\n\tif (_moneyRestrictionError) {\n\t\tTrackMessageMoneyRestrictionsChanges(this, lifetime());\n\t}\n}\n\nbool ChooseRecipientBoxController::showLockedError(\n\t\tnot_null<PeerListRow*> row) {\n\treturn RecipientRow::ShowLockedError(\n\t\tthis,\n\t\trow,\n\t\t_moneyRestrictionError);\n}\n\nvoid ChooseRecipientBoxController::rowClicked(not_null<PeerListRow*> row) {\n\tif (showLockedError(row)) {\n\t\treturn;\n\t}\n\tauto guard = base::make_weak(this);\n\tconst auto peer = row->peer();\n\tif (const auto forum = peer->forum()) {\n\t\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\t\tauto callback = [=](not_null<Data::Thread*> thread) {\n\t\t\tconst auto exists = guard.get();\n\t\t\tif (!exists) {\n\t\t\t\tif (*weak) {\n\t\t\t\t\t(*weak)->closeBox();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto onstack = std::move(_callback);\n\t\t\tonstack(thread);\n\t\t\tif (guard) {\n\t\t\t\t_callback = std::move(onstack);\n\t\t\t} else if (*weak) {\n\t\t\t\t(*weak)->closeBox();\n\t\t\t}\n\t\t};\n\t\tconst auto filter = [=](not_null<Data::Thread*> thread) {\n\t\t\treturn guard && (!_filter || _filter(thread));\n\t\t};\n\t\tauto owned = Box<PeerListBox>(\n\t\t\tstd::make_unique<ChooseTopicBoxController>(\n\t\t\t\tforum,\n\t\t\t\tstd::move(callback),\n\t\t\t\tfilter),\n\t\t\t[=](not_null<PeerListBox*> box) {\n\t\t\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t});\n\n\t\t\t\tforum->destroyed(\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t}, box->lifetime());\n\t\t\t});\n\t\t*weak = owned.data();\n\t\tdelegate()->peerListUiShow()->showBox(std::move(owned));\n\t\treturn;\n\t} else if (const auto monoforum = peer->monoforum()) {\n\t\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\t\tauto callback = [=](not_null<Data::SavedSublist*> sublist) {\n\t\t\tconst auto exists = guard.get();\n\t\t\tif (!exists) {\n\t\t\t\tif (*weak) {\n\t\t\t\t\t(*weak)->closeBox();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto onstack = std::move(_callback);\n\t\t\tonstack(sublist);\n\t\t\tif (guard) {\n\t\t\t\t_callback = std::move(onstack);\n\t\t\t} else if (*weak) {\n\t\t\t\t(*weak)->closeBox();\n\t\t\t}\n\t\t};\n\t\tconst auto filter = [=](not_null<Data::SavedSublist*> sublist) {\n\t\t\treturn guard && (!_filter || _filter(sublist));\n\t\t};\n\t\tauto owned = Box<PeerListBox>(\n\t\t\tstd::make_unique<ChooseSublistBoxController>(\n\t\t\t\tmonoforum,\n\t\t\t\tstd::move(callback),\n\t\t\t\tfilter),\n\t\t\t[=](not_null<PeerListBox*> box) {\n\t\t\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t});\n\n\t\t\t\tmonoforum->destroyed(\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t}, box->lifetime());\n\t\t\t});\n\t\t*weak = owned.data();\n\t\tdelegate()->peerListUiShow()->showBox(std::move(owned));\n\t\treturn;\n\t}\n\tconst auto history = peer->owner().history(peer);\n\tauto callback = std::move(_callback);\n\tcallback(history);\n\tif (guard) {\n\t\t_callback = std::move(callback);\n\t}\n}\n\nbool RecipientRow::ShowLockedError(\n\t\tnot_null<PeerListController*> controller,\n\t\tnot_null<PeerListRow*> row,\n\t\tFn<RecipientMoneyRestrictionError(not_null<UserData*>)> error) {\n\tconst auto recipient = static_cast<RecipientRow*>(row.get());\n\tif (!recipient->restriction().premiumRequired) {\n\t\treturn false;\n\t}\n\t::Settings::ShowPremiumPromoToast(\n\t\tcontroller->delegate()->peerListUiShow(),\n\t\tChatHelpers::ResolveWindowDefault(),\n\t\terror(row->peer()->asUser()).text,\n\t\tu\"require_premium\"_q);\n\treturn true;\n}\n\nQString ChooseRecipientBoxController::savedMessagesChatStatus() const {\n\treturn tr::lng_saved_forward_here(tr::now);\n}\n\nauto ChooseRecipientBoxController::createRow(\n\t\tnot_null<History*> history) -> std::unique_ptr<Row> {\n\tconst auto peer = history->peer;\n\tconst auto skip = _filter\n\t\t? !_filter(history)\n\t\t: ((peer->isBroadcast() && !Data::CanSendAnything(peer))\n\t\t\t|| peer->isRepliesChat()\n\t\t\t|| peer->isVerifyCodes()\n\t\t\t|| (peer->isUser() && (_moneyRestrictionError\n\t\t\t\t? !peer->asUser()->canSendIgnoreMoneyRestrictions()\n\t\t\t\t: !Data::CanSendAnything(peer))));\n\tif (skip) {\n\t\treturn nullptr;\n\t}\n\tauto result = std::make_unique<Row>(\n\t\thistory,\n\t\t_moneyRestrictionError ? &computeListSt().item : nullptr);\n\treturn result;\n}\n\nChooseTopicSearchController::ChooseTopicSearchController(\n\tnot_null<Data::Forum*> forum)\n: _forum(forum)\n, _api(&forum->session().mtp())\n, _timer([=] { searchOnServer(); }) {\n}\n\nvoid ChooseTopicSearchController::searchQuery(const QString &query) {\n\tif (_query != query) {\n\t\t_query = query;\n\t\t_api.request(base::take(_requestId)).cancel();\n\t\t_offsetDate = 0;\n\t\t_offsetId = 0;\n\t\t_offsetTopicId = 0;\n\t\t_allLoaded = false;\n\t\tif (!_query.isEmpty()) {\n\t\t\t_timer.callOnce(AutoSearchTimeout);\n\t\t} else {\n\t\t\t_timer.cancel();\n\t\t}\n\t}\n}\n\nvoid ChooseTopicSearchController::searchOnServer() {\n\t_requestId = _api.request(MTPmessages_GetForumTopics(\n\t\tMTP_flags(MTPmessages_GetForumTopics::Flag::f_q),\n\t\t_forum->peer()->input(),\n\t\tMTP_string(_query),\n\t\tMTP_int(_offsetDate),\n\t\tMTP_int(_offsetId),\n\t\tMTP_int(_offsetTopicId),\n\t\tMTP_int(kSearchPerPage)\n\t)).done([=](const MTPmessages_ForumTopics &result) {\n\t\t_requestId = 0;\n\t\tconst auto savedTopicId = _offsetTopicId;\n\t\tconst auto byCreation = result.data().is_order_by_create_date();\n\t\t_forum->applyReceivedTopics(result, [&](\n\t\t\t\tnot_null<Data::ForumTopic*> topic) {\n\t\t\t_offsetTopicId = topic->rootId();\n\t\t\tif (byCreation) {\n\t\t\t\t_offsetDate = topic->creationDate();\n\t\t\t\tif (const auto last = topic->lastServerMessage()) {\n\t\t\t\t\t_offsetId = last->id;\n\t\t\t\t}\n\t\t\t} else if (const auto last = topic->lastServerMessage()) {\n\t\t\t\t_offsetId = last->id;\n\t\t\t\t_offsetDate = last->date();\n\t\t\t}\n\t\t\tdelegate()->peerListSearchAddRow(topic->rootId().bare);\n\t\t});\n\t\tif (_offsetTopicId == savedTopicId) {\n\t\t\t_allLoaded = true;\n\t\t}\n\t\tdelegate()->peerListSearchRefreshRows();\n\t}).fail([=] {\n\t\t_allLoaded = true;\n\t}).send();\n}\n\nbool ChooseTopicSearchController::isLoading() {\n\treturn _timer.isActive() || _requestId;\n}\n\nbool ChooseTopicSearchController::loadMoreRows() {\n\tif (!isLoading()) {\n\t\tsearchOnServer();\n\t}\n\treturn !_allLoaded;\n}\n\nChooseTopicBoxController::Row::Row(not_null<Data::ForumTopic*> topic)\n: PeerListRow(topic->rootId().bare)\n, _topic(topic) {\n}\n\nQString ChooseTopicBoxController::Row::generateName() {\n\treturn _topic->title();\n}\n\nQString ChooseTopicBoxController::Row::generateShortName() {\n\treturn _topic->title();\n}\n\nauto ChooseTopicBoxController::Row::generatePaintUserpicCallback(\n\tbool forceRound)\n-> PaintRoundImageCallback {\n\treturn [=](\n\t\t\tPainter &p,\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tint outerWidth,\n\t\t\tint size) {\n\t\tconst auto &st = st::forumTopicRow;\n\t\tx -= st.padding.left();\n\t\ty -= st.padding.top();\n\t\tauto view = Ui::PeerUserpicView();\n\t\tp.translate(x, y);\n\t\t_topic->paintUserpic(p, view, {\n\t\t\t.st = &st,\n\t\t\t.currentBg = st::windowBg,\n\t\t\t.now = crl::now(),\n\t\t\t.width = outerWidth,\n\t\t\t.paused = false,\n\t\t});\n\t\tp.translate(-x, -y);\n\t};\n}\n\nauto ChooseTopicBoxController::Row::generateNameFirstLetters() const\n-> const base::flat_set<QChar> & {\n\treturn _topic->chatListFirstLetters();\n}\n\nauto ChooseTopicBoxController::Row::generateNameWords() const\n-> const base::flat_set<QString> & {\n\treturn _topic->chatListNameWords();\n}\n\nQString ChooseTopicBoxController::AllMessagesRow::name() const {\n\treturn _userCreatesTopics\n\t\t? tr::lng_forum_create_new_topic(tr::now)\n\t\t: tr::lng_forum_all_messages(tr::now);\n}\n\nChooseTopicBoxController::AllMessagesRow::AllMessagesRow(bool userCreatesTopics)\n: PeerListRow(PeerListRowId(0))\n, _userCreatesTopics(userCreatesTopics) {\n\tconst auto words = TextUtilities::PrepareSearchWords(name());\n\tfor (const auto &word : words) {\n\t\t_nameWords.emplace(word);\n\t\t_nameFirstLetters.emplace(word[0]);\n\t}\n}\n\nQString ChooseTopicBoxController::AllMessagesRow::generateName() {\n\treturn name();\n}\n\nQString ChooseTopicBoxController::AllMessagesRow::generateShortName() {\n\treturn name();\n}\n\nauto ChooseTopicBoxController::AllMessagesRow::generatePaintUserpicCallback(\n\tbool forceRound)\n-> PaintRoundImageCallback {\n\treturn [userCreatesTopics = _userCreatesTopics](\n\t\t\tPainter &p,\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tint outerWidth,\n\t\t\tint size) {\n\t\tconst auto &icon = userCreatesTopics\n\t\t\t? st::menuIconDiscussion\n\t\t\t: st::menuIconChats;\n\t\ticon.paintInCenter(\n\t\t\tp,\n\t\t\tQRect(x, y - st::lineWidth, size, size));\n\t};\n}\n\nauto ChooseTopicBoxController::AllMessagesRow::generateNameFirstLetters() const\n-> const base::flat_set<QChar> & {\n\treturn _nameFirstLetters;\n}\n\nauto ChooseTopicBoxController::AllMessagesRow::generateNameWords() const\n-> const base::flat_set<QString> & {\n\treturn _nameWords;\n}\n\nChooseTopicBoxController::ChooseTopicBoxController(\n\tnot_null<Data::Forum*> forum,\n\tFnMut<void(not_null<Data::Thread*>)> callback,\n\tFn<bool(not_null<Data::Thread*>)> filter)\n: PeerListController(std::make_unique<ChooseTopicSearchController>(forum))\n, _forum(forum)\n, _callback(std::move(callback))\n, _filter(std::move(filter)) {\n\tsetStyleOverrides(&st::chooseTopicList);\n\n\t_forum->chatsListChanges(\n\t) | rpl::on_next([=] {\n\t\trefreshRows();\n\t}, lifetime());\n\n\t_forum->topicDestroyed(\n\t) | rpl::on_next([=](not_null<Data::ForumTopic*> topic) {\n\t\tconst auto id = PeerListRowId(topic->rootId().bare);\n\t\tif (const auto row = delegate()->peerListFindRow(id)) {\n\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t}, lifetime());\n}\n\nMain::Session &ChooseTopicBoxController::session() const {\n\treturn _forum->session();\n}\n\nvoid ChooseTopicBoxController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto weak = base::make_weak(this);\n\tauto onstack = base::take(_callback);\n\tif (row->id() == PeerListRowId(0)) {\n\t\tonstack(_forum->history());\n\t} else {\n\t\tonstack(static_cast<Row*>(row.get())->topic());\n\t}\n\tif (weak) {\n\t\t_callback = std::move(onstack);\n\t}\n}\n\nvoid ChooseTopicBoxController::prepare() {\n\tdelegate()->peerListSetTitle(tr::lng_forward_choose());\n\tsetSearchNoResultsText(tr::lng_topics_not_found(tr::now));\n\tdelegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);\n\trefreshRows(true);\n\n\tsession().changes().entryUpdates(\n\t\tData::EntryUpdate::Flag::Repaint\n\t) | rpl::on_next([=](const Data::EntryUpdate &update) {\n\t\tif (const auto topic = update.entry->asTopic()) {\n\t\t\tif (topic->forum() == _forum) {\n\t\t\t\tconst auto id = topic->rootId().bare;\n\t\t\t\tif (const auto row = delegate()->peerListFindRow(id)) {\n\t\t\t\t\tdelegate()->peerListUpdateRow(row);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, lifetime());\n}\n\nvoid ChooseTopicBoxController::refreshRows(bool initial) {\n\tauto added = false;\n\tif (_forum->bot()\n\t\t&& !delegate()->peerListFindRow(PeerListRowId(0))\n\t\t&& (!_filter || _filter(_forum->history()))) {\n\t\tconst auto userCreatesTopics = Data::IsBotUserCreatesTopics(\n\t\t\t_forum->peer());\n\t\tdelegate()->peerListAppendRow(\n\t\t\tstd::make_unique<AllMessagesRow>(userCreatesTopics));\n\t\tadded = true;\n\t}\n\tfor (const auto &row : _forum->topicsList()->indexed()->all()) {\n\t\tif (const auto topic = row->topic()) {\n\t\t\tconst auto id = topic->rootId().bare;\n\t\t\tauto already = delegate()->peerListFindRow(id);\n\t\t\tif (initial || !already) {\n\t\t\t\tif (auto created = createRow(topic)) {\n\t\t\t\t\tdelegate()->peerListAppendRow(std::move(created));\n\t\t\t\t\tadded = true;\n\t\t\t\t}\n\t\t\t} else if (already->isSearchResult()) {\n\t\t\t\tdelegate()->peerListAppendFoundRow(already);\n\t\t\t\tadded = true;\n\t\t\t}\n\t\t}\n\t}\n\tif (added) {\n\t\tdelegate()->peerListRefreshRows();\n\t}\n}\n\nvoid ChooseTopicBoxController::loadMoreRows() {\n\t_forum->requestTopics();\n}\n\nstd::unique_ptr<PeerListRow> ChooseTopicBoxController::createSearchRow(\n\t\tPeerListRowId id) {\n\tif (const auto topic = _forum->topicFor(MsgId(id))) {\n\t\treturn std::make_unique<Row>(topic);\n\t}\n\treturn nullptr;\n}\n\nstd::unique_ptr<PeerListRow> ChooseTopicBoxController::MakeRow(\n\t\tnot_null<Data::ForumTopic*> topic) {\n\treturn std::make_unique<Row>(topic);\n}\n\nauto ChooseTopicBoxController::createRow(not_null<Data::ForumTopic*> topic)\n-> std::unique_ptr<Row> {\n\tconst auto skip = _filter && !_filter(topic);\n\treturn skip ? nullptr : std::make_unique<Row>(topic);\n};\n\nChooseSublistBoxController::ChooseSublistBoxController(\n\tnot_null<Data::SavedMessages*> monoforum,\n\tFnMut<void(not_null<Data::SavedSublist*>)> callback,\n\tFn<bool(not_null<Data::SavedSublist*>)> filter)\n: _monoforum(monoforum)\n, _callback(std::move(callback))\n, _filter(std::move(filter)) {\n\tsetStyleOverrides(&st::chooseTopicList);\n\n\t_monoforum->chatsListChanges(\n\t) | rpl::on_next([=] {\n\t\trefreshRows();\n\t}, lifetime());\n\n\t_monoforum->sublistDestroyed(\n\t) | rpl::on_next([=](not_null<Data::SavedSublist*> sublist) {\n\t\tconst auto id = sublist->sublistPeer()->id.value;\n\t\tif (const auto row = delegate()->peerListFindRow(id)) {\n\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t}, lifetime());\n}\n\nMain::Session &ChooseSublistBoxController::session() const {\n\treturn _monoforum->session();\n}\n\nvoid ChooseSublistBoxController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto weak = base::make_weak(this);\n\tauto onstack = base::take(_callback);\n\tonstack(_monoforum->sublist(row->peer()));\n\tif (weak) {\n\t\t_callback = std::move(onstack);\n\t}\n}\n\nvoid ChooseSublistBoxController::prepare() {\n\tdelegate()->peerListSetTitle(tr::lng_forward_choose());\n\tsetSearchNoResultsText(tr::lng_topics_not_found(tr::now));\n\tdelegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);\n\trefreshRows(true);\n\n\tsession().changes().entryUpdates(\n\t\tData::EntryUpdate::Flag::Repaint\n\t) | rpl::on_next([=](const Data::EntryUpdate &update) {\n\t\tif (const auto sublist = update.entry->asSublist()) {\n\t\t\tif (sublist->parent() == _monoforum) {\n\t\t\t\tconst auto id = sublist->sublistPeer()->id.value;\n\t\t\t\tif (const auto row = delegate()->peerListFindRow(id)) {\n\t\t\t\t\tdelegate()->peerListUpdateRow(row);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, lifetime());\n}\n\nvoid ChooseSublistBoxController::refreshRows(bool initial) {\n\tauto added = false;\n\tfor (const auto &row : _monoforum->chatsList()->indexed()->all()) {\n\t\tif (const auto sublist = row->sublist()) {\n\t\t\tconst auto id = sublist->sublistPeer()->id.value;\n\t\t\tauto already = delegate()->peerListFindRow(id);\n\t\t\tif (initial || !already) {\n\t\t\t\tif (auto created = createRow(sublist)) {\n\t\t\t\t\tdelegate()->peerListAppendRow(std::move(created));\n\t\t\t\t\tadded = true;\n\t\t\t\t}\n\t\t\t} else if (already->isSearchResult()) {\n\t\t\t\tdelegate()->peerListAppendFoundRow(already);\n\t\t\t\tadded = true;\n\t\t\t}\n\t\t}\n\t}\n\tif (added) {\n\t\tdelegate()->peerListRefreshRows();\n\t}\n}\n\nvoid ChooseSublistBoxController::loadMoreRows() {\n\t_monoforum->loadMore();\n}\n\nstd::unique_ptr<PeerListRow> ChooseSublistBoxController::createSearchRow(\n\t\tPeerListRowId id) {\n\tconst auto peer = session().data().peer(PeerId(id));\n\tif (const auto sublist = _monoforum->sublistLoaded(peer)) {\n\t\tauto result = std::make_unique<PeerListRow>(sublist->sublistPeer());\n\t\tresult->setCustomStatus(QString());\n\t\treturn result;\n\t}\n\treturn nullptr;\n}\n\nauto ChooseSublistBoxController::createRow(\n\tnot_null<Data::SavedSublist*> sublist)\n-> std::unique_ptr<PeerListRow> {\n\tif (_filter && !_filter(sublist)) {\n\t\treturn nullptr;\n\t}\n\tauto result = std::make_unique<PeerListRow>(sublist->sublistPeer());\n\tresult->setCustomStatus(QString());\n\treturn result;\n};\n\nvoid PaintRestrictionBadge(\n\t\tPainter &p,\n\t\tnot_null<const style::PeerListItem*> st,\n\t\tint stars,\n\t\tRestrictionBadgeCache &cache,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size) {\n\tconst auto paletteVersion = style::PaletteVersion();\n\tconst auto good = !cache.badge.isNull()\n\t\t&& (cache.stars == stars)\n\t\t&& (cache.paletteVersion == paletteVersion);\n\tconst auto &check = st->checkbox.check;\n\tconst auto add = check.width;\n\tif (!good) {\n\t\tcache.stars = stars;\n\t\tcache.paletteVersion = paletteVersion;\n\t\tif (stars) {\n\t\t\tconst auto text = (stars >= 1000)\n\t\t\t\t? (QString::number(stars / 1000) + 'K')\n\t\t\t\t: QString::number(stars);\n\t\t\tcache.badge = Ui::GenerateSmallBadgeImage(\n\t\t\t\ttext,\n\t\t\t\tst::paidReactTopStarIcon,\n\t\t\t\tcheck.bgActive->c,\n\t\t\t\tst::premiumButtonFg->c,\n\t\t\t\t&check);\n\t\t} else {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tconst auto &icon = st::stickersPremiumLock;\n\t\t\tconst auto width = icon.width();\n\t\t\tconst auto height = icon.height();\n\t\t\tconst auto rect = QRect(\n\t\t\t\tQPoint(x + size - width, y + size - height),\n\t\t\t\ticon.size());\n\t\t\tconst auto added = QMargins(add, add, add, add);\n\t\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t\tcache.badge = QImage(\n\t\t\t\t(rect + added).size() * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tcache.badge.setDevicePixelRatio(ratio);\n\t\t\tcache.badge.fill(Qt::transparent);\n\t\t\tconst auto inner = QRect(add, add, rect.width(), rect.height());\n\t\t\tauto q = QPainter(&cache.badge);\n\t\t\tauto pen = check.border->p;\n\t\t\tpen.setWidthF(check.width);\n\t\t\tq.setPen(pen);\n\t\t\tq.setBrush(st::premiumButtonBg2);\n\t\t\tq.drawEllipse(inner);\n\t\t\ticon.paintInCenter(q, inner);\n\t\t}\n\t}\n\tconst auto cached = cache.badge.size() / cache.badge.devicePixelRatio();\n\tconst auto left = x + size + add - cached.width();\n\tconst auto top = stars ? (y - add) : (y + size + add - cached.height());\n\tp.drawImage(left, top, cache.badge);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peer_list_controllers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peer_list_box.h\"\n#include \"base/flat_set.h\"\n#include \"base/weak_ptr.h\"\n#include \"base/timer.h\"\n#include \"mtproto/sender.h\"\n\nclass History;\n\nnamespace style {\nstruct PeerListItem;\n} // namespace style\n\nnamespace Api {\nstruct MessageMoneyRestriction;\n} // namespace Api\n\nnamespace Data {\nclass Thread;\nclass Forum;\nclass ForumTopic;\nclass SavedSublist;\nclass SavedMessages;\n} // namespace Data\n\nnamespace Ui {\nstruct OutlineSegment;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\n[[nodiscard]] object_ptr<Ui::BoxContent> PrepareContactsBox(\n\tnot_null<Window::SessionController*> sessionController);\n[[nodiscard]] QBrush PeerListStoriesGradient(const style::PeerList &st);\n\nstruct PeerListStoriesCounts {\n\tint count = 0;\n\tint unread = 0;\n\tbool videoStream = false;\n\n\tfriend inline bool operator==(\n\t\tconst PeerListStoriesCounts &a,\n\t\tconst PeerListStoriesCounts &b) = default;\n};\n[[nodiscard]] std::vector<Ui::OutlineSegment> PeerListStoriesSegments(\n\tPeerListStoriesCounts counts,\n\tconst QBrush &unreadBrush);\n\nclass PeerListRowWithLink : public PeerListRow {\npublic:\n\tusing PeerListRow::PeerListRow;\n\n\tvoid setActionLink(const QString &action);\n\n\tvoid lazyInitialize(const style::PeerListItem &st) override;\n\nprotected:\n\tQSize rightActionSize() const override;\n\tQMargins rightActionMargins() const override;\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\nprivate:\n\tvoid refreshActionLink();\n\n\tQString _action;\n\tint _actionWidth = 0;\n\n};\n\nclass PeerListGlobalSearchController : public PeerListSearchController {\npublic:\n\texplicit PeerListGlobalSearchController(not_null<Main::Session*> session);\n\n\tvoid searchQuery(const QString &query) override;\n\tbool isLoading() override;\n\tbool loadMoreRows() override {\n\t\treturn false;\n\t}\n\nprivate:\n\tbool searchInCache();\n\tvoid searchOnServer();\n\tvoid searchDone(const MTPcontacts_Found &result, mtpRequestId requestId);\n\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\tbase::Timer _timer;\n\tQString _query;\n\tmtpRequestId _requestId = 0;\n\tstd::map<QString, MTPcontacts_Found> _cache;\n\tstd::map<mtpRequestId, QString> _queries;\n\n};\n\nstruct RecipientMoneyRestrictionError {\n\tTextWithEntities text;\n};\n\n[[nodiscard]] RecipientMoneyRestrictionError WriteMoneyRestrictionError(\n\tnot_null<UserData*> user);\n\nstruct RestrictionBadgeCache {\n\tint paletteVersion = 0;\n\tint stars = 0;\n\tQImage badge;\n};\nvoid PaintRestrictionBadge(\n\tPainter &p,\n\tnot_null<const style::PeerListItem*> st,\n\tint stars,\n\tRestrictionBadgeCache &cache,\n\tint x,\n\tint y,\n\tint outerWidth,\n\tint size);\n\nclass RecipientRow : public PeerListRow {\npublic:\n\texplicit RecipientRow(\n\t\tnot_null<PeerData*> peer,\n\t\tconst style::PeerListItem *maybeLockedSt = nullptr,\n\t\tHistory *maybeHistory = nullptr);\n\n\tbool refreshLock(not_null<const style::PeerListItem*> maybeLockedSt);\n\n\t[[nodiscard]] static bool ShowLockedError(\n\t\tnot_null<PeerListController*> controller,\n\t\tnot_null<PeerListRow*> row,\n\t\tFn<RecipientMoneyRestrictionError(not_null<UserData*>)> error);\n\n\t[[nodiscard]] History *maybeHistory() const {\n\t\treturn _maybeHistory;\n\t}\n\tvoid paintUserpicOverlay(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth) override;\n\n\tvoid preloadUserpic() override;\n\n\t[[nodiscard]] Api::MessageMoneyRestriction restriction() const;\n\tvoid setRestriction(Api::MessageMoneyRestriction restriction);\n\nprivate:\n\tstruct Restriction;\n\n\tHistory *_maybeHistory = nullptr;\n\tconst style::PeerListItem *_maybeLockedSt = nullptr;\n\tstd::shared_ptr<Restriction> _restriction;\n\n};\n\nvoid TrackMessageMoneyRestrictionsChanges(\n\tnot_null<PeerListController*> controller,\n\trpl::lifetime &lifetime);\n\nclass ChatsListBoxController : public PeerListController {\npublic:\n\tclass Row : public RecipientRow {\n\tpublic:\n\t\tRow(\n\t\t\tnot_null<History*> history,\n\t\t\tconst style::PeerListItem *maybeLockedSt = nullptr);\n\n\t\t[[nodiscard]] not_null<History*> history() const {\n\t\t\treturn maybeHistory();\n\t\t}\n\n\t};\n\n\tChatsListBoxController(not_null<Main::Session*> session);\n\tChatsListBoxController(\n\t\tstd::unique_ptr<PeerListSearchController> searchController);\n\n\tvoid prepare() override final;\n\tstd::unique_ptr<PeerListRow> createSearchRow(\n\t\tnot_null<PeerData*> peer) override final;\n\nprotected:\n\tvirtual std::unique_ptr<Row> createRow(not_null<History*> history) = 0;\n\tvirtual void prepareViewHook() = 0;\n\tvirtual void updateRowHook(not_null<Row*> row) {\n\t}\n\tvirtual QString emptyBoxText() const;\n\nprivate:\n\tvoid rebuildRows();\n\tvoid checkForEmptyRows();\n\tbool appendRow(not_null<History*> history);\n\n};\n\nclass PeerListStories final {\npublic:\n\tPeerListStories(\n\t\tnot_null<PeerListController*> controller,\n\t\tnot_null<Main::Session*> session);\n\n\tvoid prepare(not_null<PeerListDelegate*> delegate);\n\n\tvoid process(not_null<PeerListRow*> row);\n\tbool handleClick(not_null<PeerData*> peer);\n\nprivate:\n\tusing Counts = PeerListStoriesCounts;\n\n\tvoid updateColors();\n\tvoid updateFor(uint64 id, Counts counts);\n\tvoid applyForRow(\n\t\tnot_null<PeerListRow*> row,\n\t\tCounts counts,\n\t\tbool force = false);\n\n\tconst not_null<PeerListController*> _controller;\n\tconst not_null<Main::Session*> _session;\n\tPeerListDelegate *_delegate = nullptr;\n\n\tQBrush _unreadBrush;\n\tbase::flat_map<uint64, Counts> _counts;\n\trpl::lifetime _lifetime;\n\n};\n\nclass ContactsBoxController : public PeerListController {\npublic:\n\texplicit ContactsBoxController(not_null<Main::Session*> session);\n\tContactsBoxController(\n\t\tnot_null<Main::Session*> session,\n\t\tstd::unique_ptr<PeerListSearchController> searchController);\n\n\t[[nodiscard]] Main::Session &session() const override;\n\tvoid prepare() override final;\n\t[[nodiscard]] std::unique_ptr<PeerListRow> createSearchRow(\n\t\tnot_null<PeerData*> peer) override final;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tbool trackSelectedList() override {\n\t\treturn !_stories;\n\t}\n\n\tenum class SortMode {\n\t\tAlphabet,\n\t\tOnline,\n\t};\n\tvoid setSortMode(SortMode mode);\n\tvoid setStoriesShown(bool shown);\n\nprotected:\n\tvirtual std::unique_ptr<PeerListRow> createRow(not_null<UserData*> user);\n\tvirtual void prepareViewHook() {\n\t}\n\tvirtual void updateRowHook(not_null<PeerListRow*> row) {\n\t}\n\nprivate:\n\tvoid sort();\n\tvoid sortByOnline();\n\tvoid rebuildRows();\n\tvoid checkForEmptyRows();\n\tbool appendRow(not_null<UserData*> user);\n\n\tconst not_null<Main::Session*> _session;\n\tSortMode _sortMode = SortMode::Alphabet;\n\tbase::Timer _sortByOnlineTimer;\n\trpl::lifetime _sortByOnlineLifetime;\n\n\tstd::unique_ptr<PeerListStories> _stories;\n\n};\n\nstruct ChooseRecipientArgs {\n\tnot_null<Main::Session*> session;\n\tFnMut<void(not_null<Data::Thread*>)> callback;\n\tFn<bool(not_null<Data::Thread*>)> filter;\n\n\tusing MoneyRestrictionError = RecipientMoneyRestrictionError;\n\tFn<MoneyRestrictionError(not_null<UserData*>)> moneyRestrictionError;\n};\n\nclass ChooseRecipientBoxController\n\t: public ChatsListBoxController\n\t, public base::has_weak_ptr {\npublic:\n\tChooseRecipientBoxController(\n\t\tnot_null<Main::Session*> session,\n\t\tFnMut<void(not_null<Data::Thread*>)> callback,\n\t\tFn<bool(not_null<Data::Thread*>)> filter = nullptr);\n\texplicit ChooseRecipientBoxController(ChooseRecipientArgs &&args);\n\n\tMain::Session &session() const override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\n\tQString savedMessagesChatStatus() const override;\n\nprotected:\n\tvoid prepareViewHook() override;\n\tstd::unique_ptr<Row> createRow(not_null<History*> history) override;\n\n\tbool showLockedError(not_null<PeerListRow*> row);\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\tFnMut<void(not_null<Data::Thread*>)> _callback;\n\tFn<bool(not_null<Data::Thread*>)> _filter;\n\tFn<RecipientMoneyRestrictionError(\n\t\tnot_null<UserData*>)> _moneyRestrictionError;\n\n};\n\nclass ChooseTopicSearchController : public PeerListSearchController {\npublic:\n\texplicit ChooseTopicSearchController(not_null<Data::Forum*> forum);\n\n\tvoid searchQuery(const QString &query) override;\n\tbool isLoading() override;\n\tbool loadMoreRows() override;\n\nprivate:\n\tvoid searchOnServer();\n\tvoid searchDone(const MTPcontacts_Found &result, mtpRequestId requestId);\n\n\tconst not_null<Data::Forum*> _forum;\n\tMTP::Sender _api;\n\tbase::Timer _timer;\n\tQString _query;\n\tmtpRequestId _requestId = 0;\n\tTimeId _offsetDate = 0;\n\tMsgId _offsetId = 0;\n\tMsgId _offsetTopicId = 0;\n\tbool _allLoaded = false;\n\n};\n\nclass ChooseTopicBoxController final\n\t: public PeerListController\n\t, public base::has_weak_ptr {\npublic:\n\tChooseTopicBoxController(\n\t\tnot_null<Data::Forum*> forum,\n\t\tFnMut<void(not_null<Data::Thread*>)> callback,\n\t\tFn<bool(not_null<Data::Thread*>)> filter = nullptr);\n\n\tMain::Session &session() const override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\n\tvoid prepare() override;\n\tvoid loadMoreRows() override;\n\tstd::unique_ptr<PeerListRow> createSearchRow(PeerListRowId id) override;\n\n\t[[nodiscard]] static std::unique_ptr<PeerListRow> MakeRow(\n\t\tnot_null<Data::ForumTopic*> topic);\n\nprivate:\n\tclass Row final : public PeerListRow {\n\tpublic:\n\t\texplicit Row(not_null<Data::ForumTopic*> topic);\n\n\t\t[[nodiscard]] not_null<Data::ForumTopic*> topic() const {\n\t\t\treturn _topic;\n\t\t}\n\n\t\tQString generateName() override;\n\t\tQString generateShortName() override;\n\t\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\t\tbool forceRound) override;\n\n\t\tauto generateNameFirstLetters() const\n\t\t\t-> const base::flat_set<QChar> & override;\n\t\tauto generateNameWords() const\n\t\t\t-> const base::flat_set<QString> & override;\n\n\tprivate:\n\t\tconst not_null<Data::ForumTopic*> _topic;\n\n\t};\n\n\tclass AllMessagesRow final : public PeerListRow {\n\tpublic:\n\t\texplicit AllMessagesRow(bool userCreatesTopics);\n\n\t\tQString generateName() override;\n\t\tQString generateShortName() override;\n\t\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\t\tbool forceRound) override;\n\n\t\tauto generateNameFirstLetters() const\n\t\t\t-> const base::flat_set<QChar> & override;\n\t\tauto generateNameWords() const\n\t\t\t-> const base::flat_set<QString> & override;\n\n\tprivate:\n\t\t[[nodiscard]] QString name() const;\n\n\t\tbase::flat_set<QChar> _nameFirstLetters;\n\t\tbase::flat_set<QString> _nameWords;\n\t\tbool _userCreatesTopics = false;\n\n\t};\n\n\tvoid refreshRows(bool initial = false);\n\t[[nodiscard]] std::unique_ptr<Row> createRow(\n\t\tnot_null<Data::ForumTopic*> topic);\n\n\tconst not_null<Data::Forum*> _forum;\n\tFnMut<void(not_null<Data::Thread*>)> _callback;\n\tFn<bool(not_null<Data::Thread*>)> _filter;\n\n};\n\nclass ChooseSublistBoxController final\n\t: public PeerListController\n\t, public base::has_weak_ptr {\npublic:\n\tChooseSublistBoxController(\n\t\tnot_null<Data::SavedMessages*> monoforum,\n\t\tFnMut<void(not_null<Data::SavedSublist*>)> callback,\n\t\tFn<bool(not_null<Data::SavedSublist*>)> filter = nullptr);\n\n\tMain::Session &session() const override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\n\tvoid prepare() override;\n\tvoid loadMoreRows() override;\n\tstd::unique_ptr<PeerListRow> createSearchRow(PeerListRowId id) override;\n\nprivate:\n\tvoid refreshRows(bool initial = false);\n\t[[nodiscard]] std::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<Data::SavedSublist*> sublist);\n\n\tconst not_null<Data::SavedMessages*> _monoforum;\n\tFnMut<void(not_null<Data::SavedSublist*>)> _callback;\n\tFn<bool(not_null<Data::SavedSublist*>)> _filter;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peer_list_widgets.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peer_list_widgets.h\"\n\n#include \"ui/painter.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace {\n\nusing State = std::unique_ptr<PeerListState>;\n\n} // namespace\n\nPeerListWidgets::PeerListWidgets(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<PeerListController*> controller)\n: Ui::RpWidget(parent)\n, _controller(controller)\n, _st(controller->computeListSt()) {\n\t_content = base::make_unique_q<Ui::VerticalLayout>(this);\n\tparent->sizeValue() | rpl::on_next([this](const QSize &size) {\n\t\t_content->resizeToWidth(size.width());\n\t\tresize(size.width(), _content->height());\n\t}, lifetime());\n}\n\ncrl::time PeerListWidgets::paintRow(\n\t\tPainter &p,\n\t\tcrl::time now,\n\t\tbool selected,\n\t\tnot_null<PeerListRow*> row) {\n\tconst auto &st = row->computeSt(_st.item);\n\n\trow->lazyInitialize(st);\n\tconst auto outerWidth = _content->width();\n\tconst auto w = outerWidth;\n\n\tauto refreshStatusAt = row->refreshStatusTime();\n\tif (refreshStatusAt > 0 && now >= refreshStatusAt) {\n\t\trow->refreshStatus();\n\t\trefreshStatusAt = row->refreshStatusTime();\n\t}\n\tconst auto refreshStatusIn = (refreshStatusAt > 0)\n\t\t? std::max(refreshStatusAt - now, crl::time(1))\n\t\t: 0;\n\n\trow->paintUserpic(\n\t\tp,\n\t\tst,\n\t\tst.photoPosition.x(),\n\t\tst.photoPosition.y(),\n\t\touterWidth);\n\n\tp.setPen(st::contactsNameFg);\n\n\tconst auto skipRight = st.photoPosition.x();\n\tconst auto rightActionSize = row->rightActionSize();\n\tconst auto rightActionMargins = rightActionSize.isEmpty()\n\t\t? QMargins()\n\t\t: row->rightActionMargins();\n\tconst auto &name = row->name();\n\tconst auto namePosition = st.namePosition;\n\tconst auto namex = namePosition.x();\n\tconst auto namey = namePosition.y();\n\tauto namew = outerWidth - namex - skipRight;\n\tif (!rightActionSize.isEmpty()\n\t\t&& (namey < rightActionMargins.top() + rightActionSize.height())\n\t\t&& (namey + st.nameStyle.font->height\n\t\t\t> rightActionMargins.top())) {\n\t\tnamew -= rightActionMargins.left()\n\t\t\t+ rightActionSize.width()\n\t\t\t+ rightActionMargins.right()\n\t\t\t- skipRight;\n\t}\n\tconst auto statusx = st.statusPosition.x();\n\tconst auto statusy = st.statusPosition.y();\n\tauto statusw = outerWidth - statusx - skipRight;\n\tif (!rightActionSize.isEmpty()\n\t\t&& (statusy < rightActionMargins.top() + rightActionSize.height())\n\t\t&& (statusy + st::contactsStatusFont->height\n\t\t\t> rightActionMargins.top())) {\n\t\tstatusw -= rightActionMargins.left()\n\t\t\t+ rightActionSize.width()\n\t\t\t+ rightActionMargins.right()\n\t\t\t- skipRight;\n\t}\n\tnamew -= row->paintNameIconGetWidth(\n\t\tp,\n\t\t[=] { updateRow(row); },\n\t\tnow,\n\t\tnamex,\n\t\tnamey,\n\t\tname.maxWidth(),\n\t\tnamew,\n\t\tw,\n\t\tselected);\n\tauto nameCheckedRatio = row->disabled() ? 0. : row->checkedRatio();\n\tp.setPen(anim::pen(st.nameFg, st.nameFgChecked, nameCheckedRatio));\n\tname.drawLeftElided(p, namex, namey, namew, w);\n\n\tp.setFont(st::contactsStatusFont);\n\trow->paintStatusText(p, st, statusx, statusy, statusw, w, selected);\n\n\trow->elementsPaint(p, outerWidth, selected, 0);\n\n\treturn refreshStatusIn;\n}\n\nvoid PeerListWidgets::appendRow(std::unique_ptr<PeerListRow> row) {\n\tExpects(row != nullptr);\n\n\tif (_rowsById.find(row->id()) == _rowsById.cend()) {\n\t\tconst auto raw = row.get();\n\t\tconst auto &st = raw->computeSt(_st.item);\n\t\traw->setAbsoluteIndex(_rows.size());\n\t\t_rows.push_back(std::move(row));\n\n\t\tconst auto widget = _content->add(\n\t\t\tobject_ptr<Ui::AbstractButton>::fromRaw(\n\t\t\t\tUi::CreateSimpleSettingsButton(\n\t\t\t\t\t_content.get(),\n\t\t\t\t\tst.button.ripple,\n\t\t\t\t\tst.button.textBgOver)));\n\t\twidget->resize(widget->width(), st.height);\n\t\twidget->paintRequest() | rpl::on_next([=, this] {\n\t\t\tauto p = Painter(widget);\n\t\t\tconst auto selected = widget->isOver() || widget->isDown();\n\t\t\tpaintRow(p, crl::now(), selected, raw);\n\t\t}, widget->lifetime());\n\n\t\twidget->setClickedCallback([this, raw] {\n\t\t\t_controller->rowClicked(raw);\n\t\t});\n\t}\n}\n\nPeerListRow *PeerListWidgets::findRow(PeerListRowId id) {\n\tconst auto it = _rowsById.find(id);\n\treturn (it == _rowsById.cend()) ? nullptr : it->second.get();\n}\n\nvoid PeerListWidgets::updateRow(not_null<PeerListRow*> row) {\n\tconst auto it = ranges::find_if(\n\t\t_rows,\n\t\t[row](const auto &r) { return r.get() == row; });\n\tif (it != _rows.end()) {\n\t\tconst auto index = std::distance(_rows.begin(), it);\n\t\tif (const auto widget = _content->widgetAt(index)) {\n\t\t\twidget->update();\n\t\t}\n\t}\n}\n\nint PeerListWidgets::fullRowsCount() {\n\treturn _rows.size();\n}\n\n[[nodiscard]] not_null<PeerListRow*> PeerListWidgets::rowAt(int index) {\n\tExpects(index >= 0 && index < _rows.size());\n\n\treturn _rows[index].get();\n}\n\nvoid PeerListWidgets::refreshRows() {\n\t_content->resizeToWidth(width());\n\tresize(width(), _content->height());\n}\n\nvoid PeerListWidgetsDelegate::setContent(PeerListWidgets *content) {\n\t_content = content;\n}\n\nvoid PeerListWidgetsDelegate::setUiShow(\n\t\tstd::shared_ptr<Main::SessionShow> uiShow) {\n\t_uiShow = std::move(uiShow);\n}\n\nvoid PeerListWidgetsDelegate::peerListSetHideEmpty(bool hide) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSetHideEmpty\");\n}\n\nvoid PeerListWidgetsDelegate::peerListAppendRow(\n\t\tstd::unique_ptr<PeerListRow> row) {\n\t_content->appendRow(std::move(row));\n}\n\nvoid PeerListWidgetsDelegate::peerListAppendSearchRow(\n\t\tstd::unique_ptr<PeerListRow> row) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListAppendSearchRow\");\n}\n\nvoid PeerListWidgetsDelegate::peerListAppendFoundRow(\n\t\tnot_null<PeerListRow*> row) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListAppendFoundRow\");\n}\n\nvoid PeerListWidgetsDelegate::peerListPrependRow(\n\t\tstd::unique_ptr<PeerListRow> row) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListPrependRow\");\n}\n\nvoid PeerListWidgetsDelegate::peerListPrependRowFromSearchResult(\n\t\tnot_null<PeerListRow*> row) {\n\tUnexpected(\n\t\t\"...PeerListWidgetsDelegate::peerListPrependRowFromSearchResult\");\n}\n\nPeerListRow* PeerListWidgetsDelegate::peerListFindRow(PeerListRowId id) {\n\treturn _content->findRow(id);\n}\n\nauto PeerListWidgetsDelegate::peerListLastRowMousePosition()\n-> std::optional<QPoint> {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListLastRowMousePosition\");\n}\n\nvoid PeerListWidgetsDelegate::peerListUpdateRow(not_null<PeerListRow*> row) {\n\t_content->updateRow(row);\n}\n\nvoid PeerListWidgetsDelegate::peerListRemoveRow(not_null<PeerListRow*> row) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListRemoveRow\");\n}\n\nvoid PeerListWidgetsDelegate::peerListConvertRowToSearchResult(\n\t\tnot_null<PeerListRow*> row) {\n\tUnexpected(\n\t\t\"...PeerListWidgetsDelegate::peerListConvertRowToSearchResult\");\n}\n\nvoid PeerListWidgetsDelegate::peerListSetRowChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSetRowChecked\");\n}\n\nvoid PeerListWidgetsDelegate::peerListSetRowHidden(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool hidden) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSetRowHidden\");\n}\n\nvoid PeerListWidgetsDelegate::peerListSetForeignRowChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked,\n\t\tanim::type animated) {\n}\n\nint PeerListWidgetsDelegate::peerListFullRowsCount() {\n\treturn _content->fullRowsCount();\n}\n\nnot_null<PeerListRow*> PeerListWidgetsDelegate::peerListRowAt(int index) {\n\treturn _content->rowAt(index);\n}\n\nint PeerListWidgetsDelegate::peerListSearchRowsCount() {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSearchRowsCount\");\n}\n\nnot_null<PeerListRow*> PeerListWidgetsDelegate::peerListSearchRowAt(int) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSearchRowAt\");\n}\n\nvoid PeerListWidgetsDelegate::peerListRefreshRows() {\n\t_content->refreshRows();\n}\n\nvoid PeerListWidgetsDelegate::peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel>) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSetDescription\");\n}\n\nvoid PeerListWidgetsDelegate::peerListSetSearchNoResults(\n\t\tobject_ptr<Ui::FlatLabel>) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSetSearchNoResults\");\n}\n\nvoid PeerListWidgetsDelegate::peerListSetAboveWidget(\n\t\tobject_ptr<Ui::RpWidget>) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSetAboveWidget\");\n}\n\nvoid PeerListWidgetsDelegate::peerListSetAboveSearchWidget(\n\t\tobject_ptr<Ui::RpWidget>) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSetAboveSearchWidget\");\n}\n\nvoid PeerListWidgetsDelegate::peerListSetBelowWidget(\n\t\tobject_ptr<Ui::RpWidget>) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSetBelowWidget\");\n}\n\nvoid PeerListWidgetsDelegate::peerListSetSearchMode(PeerListSearchMode mode) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSetSearchMode\");\n}\n\nvoid PeerListWidgetsDelegate::peerListMouseLeftGeometry() {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListMouseLeftGeometry\");\n}\n\nvoid PeerListWidgetsDelegate::peerListSortRows(\n\t\tFn<bool(const PeerListRow &, const PeerListRow &)>) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSortRows\");\n}\n\nint PeerListWidgetsDelegate::peerListPartitionRows(\n\t\tFn<bool(const PeerListRow &a)> border) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListPartitionRows\");\n}\n\nState PeerListWidgetsDelegate::peerListSaveState() const {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSaveState\");\n\treturn nullptr;\n}\n\nvoid PeerListWidgetsDelegate::peerListRestoreState(\n\t\tstd::unique_ptr<PeerListState> state) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListRestoreState\");\n}\n\nvoid PeerListWidgetsDelegate::peerListShowRowMenu(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool highlightRow,\n\t\tFn<void(not_null<Ui::PopupMenu*>)> destroyed) {\n}\n\nvoid PeerListWidgetsDelegate::peerListSelectSkip(int direction) {\n\t// _content->selectSkip(direction);\n}\n\nvoid PeerListWidgetsDelegate::peerListPressLeftToContextMenu(bool shown) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListPressLeftToContextMenu\");\n}\n\nbool PeerListWidgetsDelegate::peerListTrackRowPressFromGlobal(QPoint) {\n\treturn false;\n}\n\nstd::shared_ptr<Main::SessionShow> PeerListWidgetsDelegate::peerListUiShow() {\n\tExpects(_uiShow != nullptr);\n\treturn _uiShow;\n}\n\nvoid PeerListWidgetsDelegate::peerListAddSelectedPeerInBunch(\n\t\tnot_null<PeerData*> peer) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListAddSelectedPeerInBunch\");\n}\n\nvoid PeerListWidgetsDelegate::peerListAddSelectedRowInBunch(\n\t\tnot_null<PeerListRow*> row) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListAddSelectedRowInBunch\");\n}\n\nvoid PeerListWidgetsDelegate::peerListFinishSelectedRowsBunch() {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListFinishSelectedRowsBunch\");\n}\n\nvoid PeerListWidgetsDelegate::peerListSetTitle(rpl::producer<QString> title) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSetTitle\");\n}\n\nvoid PeerListWidgetsDelegate::peerListSetAdditionalTitle(\n\t\trpl::producer<QString> title) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSetAdditionalTitle\");\n}\n\nbool PeerListWidgetsDelegate::peerListIsRowChecked(\n\t\tnot_null<PeerListRow*> row) {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListIsRowChecked\");\n\treturn false;\n}\n\nvoid PeerListWidgetsDelegate::peerListScrollToTop() {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListScrollToTop\");\n}\n\nint PeerListWidgetsDelegate::peerListSelectedRowsCount() {\n\tUnexpected(\"...PeerListWidgetsDelegate::peerListSelectedRowsCount\");\n\treturn 0;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peer_list_widgets.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peer_list_box.h\"\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nclass PeerListWidgets : public Ui::RpWidget {\npublic:\n\tPeerListWidgets(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<PeerListController*> controller);\n\n\tcrl::time paintRow(\n\t\tPainter &p,\n\t\tcrl::time now,\n\t\tbool selected,\n\t\tnot_null<PeerListRow*> row);\n\tvoid appendRow(std::unique_ptr<PeerListRow> row);\n\tPeerListRow* findRow(PeerListRowId id);\n\tvoid updateRow(not_null<PeerListRow*> row);\n\tint fullRowsCount();\n\t[[nodiscard]] not_null<PeerListRow*> rowAt(int index);\n\tvoid refreshRows();\n\nprivate:\n\tconst not_null<PeerListController*> _controller;\n\tconst style::PeerList &_st;\n\tbase::unique_qptr<Ui::VerticalLayout> _content;\n\n\tstd::vector<std::unique_ptr<PeerListRow>> _rows;\n\tstd::map<PeerListRowId, not_null<PeerListRow*>> _rowsById;\n\tstd::map<PeerData*, std::vector<not_null<PeerListRow*>>> _rowsByPeer;\n};\n\nclass PeerListWidgetsDelegate : public PeerListDelegate {\npublic:\n\tvoid setContent(PeerListWidgets *content);\n\tvoid setUiShow(std::shared_ptr<Main::SessionShow> uiShow);\n\n\tvoid peerListSetHideEmpty(bool hide) override;\n\tvoid peerListAppendRow(std::unique_ptr<PeerListRow> row) override;\n\tvoid peerListAppendSearchRow(std::unique_ptr<PeerListRow> row) override;\n\tvoid peerListAppendFoundRow(not_null<PeerListRow*> row) override;\n\tvoid peerListPrependRow(std::unique_ptr<PeerListRow> row) override;\n\tvoid peerListPrependRowFromSearchResult(\n\t\tnot_null<PeerListRow*> row) override;\n\tPeerListRow *peerListFindRow(PeerListRowId id) override;\n\tstd::optional<QPoint> peerListLastRowMousePosition() override;\n\tvoid peerListUpdateRow(not_null<PeerListRow*> row) override;\n\tvoid peerListRemoveRow(not_null<PeerListRow*> row) override;\n\tvoid peerListConvertRowToSearchResult(\n\t\tnot_null<PeerListRow*> row) override;\n\tvoid peerListSetRowChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked) override;\n\tvoid peerListSetRowHidden(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool hidden) override;\n\tvoid peerListSetForeignRowChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked,\n\t\tanim::type animated) override;\n\tint peerListFullRowsCount() override;\n\tnot_null<PeerListRow*> peerListRowAt(int index) override;\n\tint peerListSearchRowsCount() override;\n\tnot_null<PeerListRow*> peerListSearchRowAt(int index) override;\n\tvoid peerListRefreshRows() override;\n\tvoid peerListSetDescription(object_ptr<Ui::FlatLabel>) override;\n\tvoid peerListSetSearchNoResults(object_ptr<Ui::FlatLabel>) override;\n\tvoid peerListSetAboveWidget(object_ptr<Ui::RpWidget>) override;\n\tvoid peerListSetAboveSearchWidget(object_ptr<Ui::RpWidget>) override;\n\tvoid peerListSetBelowWidget(object_ptr<Ui::RpWidget>) override;\n\tvoid peerListSetSearchMode(PeerListSearchMode mode) override;\n\tvoid peerListMouseLeftGeometry() override;\n\tvoid peerListSortRows(\n\t\tFn<bool(const PeerListRow &, const PeerListRow &)>) override;\n\tint peerListPartitionRows(Fn<bool(const PeerListRow &a)> border) override;\n\tstd::unique_ptr<PeerListState> peerListSaveState() const override;\n\tvoid peerListRestoreState(std::unique_ptr<PeerListState> state) override;\n\tvoid peerListShowRowMenu(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool highlightRow,\n\t\tFn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr) override;\n\tvoid peerListSelectSkip(int direction) override;\n\tvoid peerListPressLeftToContextMenu(bool shown) override;\n\tbool peerListTrackRowPressFromGlobal(QPoint globalPosition) override;\n\tstd::shared_ptr<Main::SessionShow> peerListUiShow() override;\n\tvoid peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) override;\n\tvoid peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) override;\n\tvoid peerListFinishSelectedRowsBunch() override;\n\tvoid peerListSetTitle(rpl::producer<QString> title) override;\n\tvoid peerListSetAdditionalTitle(rpl::producer<QString> title) override;\n\tbool peerListIsRowChecked(not_null<PeerListRow*> row) override;\n\tvoid peerListScrollToTop() override;\n\tint peerListSelectedRowsCount() override;\n\nprivate:\n\tPeerListWidgets *_content = nullptr;\n\tstd::shared_ptr<Main::SessionShow> _uiShow;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peer_lists_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peer_lists_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/widgets/multi_select.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/ui_utility.h\"\n#include \"main/session/session_show.h\"\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"data/data_peer.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n\nPeerListsBox::PeerListsBox(\n\tQWidget*,\n\tstd::vector<std::unique_ptr<PeerListController>> controllers,\n\tFn<void(not_null<PeerListsBox*>)> init)\n: _lists(makeLists(std::move(controllers)))\n, _init(std::move(init)) {\n\tExpects(!_lists.empty());\n}\n\nauto PeerListsBox::collectSelectedRows()\n-> std::vector<not_null<PeerData*>> {\n\tauto result = std::vector<not_null<PeerData*>>();\n\tauto items = _select\n\t\t? _select->entity()->getItems()\n\t\t: QVector<uint64>();\n\tif (!items.empty()) {\n\t\tresult.reserve(items.size());\n\t\tconst auto session = &firstController()->session();\n\t\tfor (const auto itemId : items) {\n\t\t\tconst auto foreign = [&] {\n\t\t\t\tfor (const auto &list : _lists) {\n\t\t\t\t\tif (list.controller->isForeignRow(itemId)) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}();\n\t\t\tif (!foreign) {\n\t\t\t\tresult.push_back(session->data().peer(PeerId(itemId)));\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\n\nPeerListsBox::List PeerListsBox::makeList(\n\t\tstd::unique_ptr<PeerListController> controller) {\n\tauto delegate = std::make_unique<Delegate>(this, controller.get());\n\treturn {\n\t\tstd::move(controller),\n\t\tstd::move(delegate),\n\t};\n}\n\nstd::vector<PeerListsBox::List> PeerListsBox::makeLists(\n\t\tstd::vector<std::unique_ptr<PeerListController>> controllers) {\n\tauto result = std::vector<List>();\n\tresult.reserve(controllers.size());\n\tfor (auto &controller : controllers) {\n\t\tresult.push_back(makeList(std::move(controller)));\n\t}\n\treturn result;\n}\n\nnot_null<PeerListController*> PeerListsBox::firstController() const {\n\treturn _lists.front().controller.get();\n}\n\nvoid PeerListsBox::createMultiSelect() {\n\tExpects(_select == nullptr);\n\n\tauto entity = object_ptr<Ui::MultiSelect>(\n\t\tthis,\n\t\t(firstController()->selectSt()\n\t\t\t? *firstController()->selectSt()\n\t\t\t: st::defaultMultiSelect),\n\t\ttr::lng_participant_filter());\n\t_select.create(this, std::move(entity));\n\t_select->heightValue(\n\t) | rpl::on_next(\n\t\t[this] { updateScrollSkips(); },\n\t\tlifetime());\n\t_select->entity()->setSubmittedCallback([=](Qt::KeyboardModifiers) {\n\t\tfor (const auto &list : _lists) {\n\t\t\tif (list.content->submitted()) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t});\n\t_select->entity()->setQueryChangedCallback([=](const QString &query) {\n\t\tsearchQueryChanged(query);\n\t});\n\t_select->entity()->setItemRemovedCallback([=](uint64 itemId) {\n\t\tfor (const auto &list : _lists) {\n\t\t\tif (list.controller->handleDeselectForeignRow(itemId)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tconst auto session = &firstController()->session();\n\t\tif (const auto peer = session->data().peerLoaded(PeerId(itemId))) {\n\t\t\tconst auto id = peer->id;\n\t\t\tfor (const auto &list : _lists) {\n\t\t\t\tif (const auto row = list.delegate->peerListFindRow(id.value)) {\n\t\t\t\t\tlist.content->changeCheckState(\n\t\t\t\t\t\trow,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\tanim::type::normal);\n\t\t\t\t\tupdate();\n\t\t\t\t}\n\t\t\t\tlist.controller->itemDeselectedHook(peer);\n\t\t\t}\n\t\t}\n\t});\n\t_select->resizeToWidth(firstController()->contentWidth());\n\t_select->moveToLeft(0, 0);\n}\n\nint PeerListsBox::getTopScrollSkip() const {\n\tauto result = 0;\n\tif (_select && !_select->isHidden()) {\n\t\tresult += _select->height();\n\t}\n\treturn result;\n}\n\nvoid PeerListsBox::updateScrollSkips() {\n\t// If we show / hide the search field scroll top is fixed.\n\t// If we resize search field by bubbles scroll bottom is fixed.\n\tsetInnerTopSkip(getTopScrollSkip(), _scrollBottomFixed);\n\tif (!_select->animating()) {\n\t\t_scrollBottomFixed = true;\n\t}\n}\n\nvoid PeerListsBox::prepare() {\n\t_rows = setInnerWidget(\n\t\tobject_ptr<Ui::VerticalLayout>(this),\n\t\tst::boxScroll);\n\tfor (auto &list : _lists) {\n\t\tconst auto content = _rows->add(object_ptr<PeerListContent>(\n\t\t\t_rows,\n\t\t\tlist.controller.get()));\n\t\tlist.content = content;\n\t\tlist.delegate->setContent(content);\n\t\tlist.controller->setDelegate(list.delegate.get());\n\n\t\tcontent->scrollToRequests(\n\t\t) | rpl::on_next([=](Ui::ScrollToRequest request) {\n\t\t\tconst auto skip = content->y();\n\t\t\tscrollToY(\n\t\t\t\tskip + request.ymin,\n\t\t\t\t(request.ymax >= 0) ? (skip + request.ymax) : request.ymax);\n\t\t}, lifetime());\n\n\t\tcontent->selectedIndexValue(\n\t\t) | rpl::filter([=](int index) {\n\t\t\treturn (index >= 0);\n\t\t}) | rpl::on_next([=] {\n\t\t\tfor (const auto &list : _lists) {\n\t\t\t\tif (list.content && list.content != content) {\n\t\t\t\t\tlist.content->clearSelection();\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime());\n\t}\n\t_rows->resizeToWidth(firstController()->contentWidth());\n\n\t{\n\t\tsetDimensions(\n\t\t\tfirstController()->contentWidth(),\n\t\t\tstd::clamp(\n\t\t\t\t_rows->height(),\n\t\t\t\tst::boxMaxListHeight,\n\t\t\t\tst::boxMaxListHeight * 3));\n\t}\n\tif (_select) {\n\t\t_select->finishAnimating();\n\t\tUi::SendPendingMoveResizeEvents(_select);\n\t\t_scrollBottomFixed = true;\n\t\tscrollToY(0);\n\t}\n\n\tif (_init) {\n\t\t_init(this);\n\t}\n}\n\nvoid PeerListsBox::keyPressEvent(QKeyEvent *e) {\n\tconst auto skipRows = [&](int rows) {\n\t\tif (rows == 0) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const auto &list : _lists) {\n\t\t\tif (list.content->hasPressed()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tconst auto from = begin(_lists), till = end(_lists);\n\t\tauto i = from;\n\t\tfor (; i != till; ++i) {\n\t\t\tif (i->content->hasSelection()) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (i == till && rows < 0) {\n\t\t\treturn;\n\t\t}\n\t\tif (rows > 0) {\n\t\t\tif (i == till) {\n\t\t\t\ti = from;\n\t\t\t}\n\t\t\tfor (; i != till; ++i) {\n\t\t\t\tconst auto result = i->content->selectSkip(rows);\n\t\t\t\tif (result.shouldMoveTo - result.reallyMovedTo >= rows) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (result.reallyMovedTo >= result.shouldMoveTo) {\n\t\t\t\t\treturn;\n\t\t\t\t} else {\n\t\t\t\t\trows = result.shouldMoveTo - result.reallyMovedTo;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor (++i; i != from;) {\n\t\t\t\tconst auto result = (--i)->content->selectSkip(rows);\n\t\t\t\tif (result.shouldMoveTo - result.reallyMovedTo <= rows) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (result.reallyMovedTo <= result.shouldMoveTo) {\n\t\t\t\t\treturn;\n\t\t\t\t} else {\n\t\t\t\t\trows = result.shouldMoveTo - result.reallyMovedTo;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\tconst auto rowsInPage = [&] {\n\t\tconst auto rowHeight = firstController()->computeListSt().item.height;\n\t\treturn height() / rowHeight;\n\t};\n\tif (e->key() == Qt::Key_Down) {\n\t\tskipRows(1);\n\t} else if (e->key() == Qt::Key_Up) {\n\t\tskipRows(-1);\n\t} else if (e->key() == Qt::Key_PageDown) {\n\t\tskipRows(rowsInPage());\n\t} else if (e->key() == Qt::Key_PageUp) {\n\t\tskipRows(-rowsInPage());\n\t} else if (e->key() == Qt::Key_Escape && _select && !_select->entity()->getQuery().isEmpty()) {\n\t\t_select->entity()->clearQuery();\n\t} else {\n\t\tBoxContent::keyPressEvent(e);\n\t}\n}\n\nvoid PeerListsBox::searchQueryChanged(const QString &query) {\n\tscrollToY(0);\n\tfor (const auto &list : _lists) {\n\t\tlist.content->searchQueryChanged(query);\n\t}\n}\n\nvoid PeerListsBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\tif (_select) {\n\t\t_select->resizeToWidth(width());\n\t\t_select->moveToLeft(0, 0);\n\n\t\tupdateScrollSkips();\n\t}\n\n\tfor (const auto &list : _lists) {\n\t\tlist.content->resizeToWidth(width());\n\t}\n}\n\nvoid PeerListsBox::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto &bg = firstController()->computeListSt().bg;\n\tfor (const auto &rect : e->region()) {\n\t\tp.fillRect(rect, bg);\n\t}\n}\n\nvoid PeerListsBox::setInnerFocus() {\n\tif (!_select || !_select->toggled()) {\n\t\t_lists.front().content->setFocus();\n\t} else {\n\t\t_select->entity()->setInnerFocus();\n\t}\n}\n\nPeerListsBox::Delegate::Delegate(\n\tnot_null<PeerListsBox*> box,\n\tnot_null<PeerListController*> controller)\n: _box(box)\n, _controller(controller)\n, _show(Main::MakeSessionShow(_box->uiShow(), &_controller->session())) {\n}\n\nvoid PeerListsBox::Delegate::peerListSetTitle(rpl::producer<QString> title) {\n}\n\nvoid PeerListsBox::Delegate::peerListSetAdditionalTitle(\n\trpl::producer<QString> title) {\n}\n\nvoid PeerListsBox::Delegate::peerListSetRowChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked) {\n\tif (checked) {\n\t\t_box->addSelectItem(row, anim::type::normal);\n\t\tPeerListContentDelegate::peerListSetRowChecked(row, checked);\n\t\tpeerListUpdateRow(row);\n\n\t\t// This call deletes row from _searchRows.\n\t\t_box->_select->entity()->clearQuery();\n\t} else {\n\t\t// The itemRemovedCallback will call changeCheckState() here.\n\t\t_box->_select->entity()->removeItem(row->id());\n\t\tpeerListUpdateRow(row);\n\t}\n}\n\nvoid PeerListsBox::Delegate::peerListSetForeignRowChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked,\n\t\tanim::type animated) {\n\tif (checked) {\n\t\t_box->addSelectItem(row, animated);\n\n\t\t// This call deletes row from _searchRows.\n\t\t_box->_select->entity()->clearQuery();\n\t} else {\n\t\t// The itemRemovedCallback will call changeCheckState() here.\n\t\t_box->_select->entity()->removeItem(row->id());\n\t}\n}\n\nnot_null<Ui::RpWidget*> PeerListsBox::addSeparatorBefore(\n\t\tint listIndex,\n\t\tobject_ptr<Ui::RpWidget> widget) {\n\tAssert(!(listIndex < 0 || listIndex >= int(_lists.size()) || !_rows));\n\tconst auto position = listIndex * 2;\n\tauto wrapped = object_ptr<Ui::SlideWrap<>>(\n\t\t_rows,\n\t\tstd::move(widget));\n\t_lists[listIndex].content->heightValue(\n\t) | rpl::on_next([=, separator = wrapped.data()](int h) {\n\t\tseparator->toggle(h > 0, anim::type::instant);\n\t}, wrapped->lifetime());\n\t_lists[listIndex].separator = wrapped.data();\n\t_rows->insert(position, std::move(wrapped));\n\treturn _lists[listIndex].separator;\n}\n\nvoid PeerListsBox::Delegate::peerListScrollToTop() {\n\t_box->scrollToY(0);\n}\n\nvoid PeerListsBox::Delegate::peerListSetSearchMode(PeerListSearchMode mode) {\n\tPeerListContentDelegate::peerListSetSearchMode(mode);\n\t_box->setSearchMode(mode);\n}\n\nvoid PeerListsBox::setSearchMode(PeerListSearchMode mode) {\n\tauto selectVisible = (mode != PeerListSearchMode::Disabled);\n\tif (selectVisible && !_select) {\n\t\tcreateMultiSelect();\n\t\t_select->toggle(!selectVisible, anim::type::instant);\n\t}\n\tif (_select) {\n\t\t_select->toggle(selectVisible, anim::type::normal);\n\t\t_scrollBottomFixed = false;\n\t\tsetInnerFocus();\n\t}\n}\n\nvoid PeerListsBox::Delegate::peerListFinishSelectedRowsBunch() {\n\tExpects(_box->_select != nullptr);\n\n\t_box->_select->entity()->finishItemsBunch();\n}\n\nauto PeerListsBox::Delegate::peerListUiShow()\n-> std::shared_ptr<Main::SessionShow> {\n\treturn _show;\n}\n\nbool PeerListsBox::Delegate::peerListIsRowChecked(\n\t\tnot_null<PeerListRow*> row) {\n\treturn _box->_select\n\t\t? _box->_select->entity()->hasItem(row->id())\n\t\t: false;\n}\n\nint PeerListsBox::Delegate::peerListSelectedRowsCount() {\n\treturn _box->_select ? _box->_select->entity()->getItemsCount() : 0;\n}\n\nvoid PeerListsBox::addSelectItem(\n\t\tnot_null<PeerData*> peer,\n\t\tanim::type animated) {\n\taddSelectItem(\n\t\tpeer->id.value,\n\t\tpeer->shortName(),\n\t\t(peer->isForum()\n\t\t\t? ForceRoundUserpicCallback(peer)\n\t\t\t: PaintUserpicCallback(peer, false)),\n\t\tanimated);\n}\n\nvoid PeerListsBox::addSelectItem(\n\t\tnot_null<PeerListRow*> row,\n\t\tanim::type animated) {\n\taddSelectItem(\n\t\trow->id(),\n\t\trow->generateShortName(),\n\t\trow->generatePaintUserpicCallback(true),\n\t\tanimated);\n}\n\nvoid PeerListsBox::addSelectItem(\n\t\tuint64 itemId,\n\t\tconst QString &text,\n\t\tUi::MultiSelect::PaintRoundImage paintUserpic,\n\t\tanim::type animated) {\n\tif (!_select) {\n\t\tcreateMultiSelect();\n\t\t_select->hide(anim::type::instant);\n\t}\n\tconst auto &activeBg = (firstController()->selectSt()\n\t\t? *firstController()->selectSt()\n\t\t: st::defaultMultiSelect).item.textActiveBg;\n\tif (animated == anim::type::instant) {\n\t\t_select->entity()->addItemInBunch(\n\t\t\titemId,\n\t\t\ttext,\n\t\t\tactiveBg,\n\t\t\tstd::move(paintUserpic));\n\t} else {\n\t\t_select->entity()->addItem(\n\t\t\titemId,\n\t\t\ttext,\n\t\t\tactiveBg,\n\t\t\tstd::move(paintUserpic));\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peer_lists_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peer_list_box.h\"\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nclass PeerListsBox : public Ui::BoxContent {\npublic:\n\tPeerListsBox(\n\t\tQWidget*,\n\t\tstd::vector<std::unique_ptr<PeerListController>> controllers,\n\t\tFn<void(not_null<PeerListsBox*>)> init);\n\n\tnot_null<Ui::RpWidget*> addSeparatorBefore(\n\t\tint listIndex,\n\t\tobject_ptr<Ui::RpWidget> widget);\n\n\t[[nodiscard]] std::vector<not_null<PeerData*>> collectSelectedRows();\n\nprotected:\n\tvoid prepare() override;\n\tvoid setInnerFocus() override;\n\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tclass Delegate final : public PeerListContentDelegate {\n\tpublic:\n\t\tDelegate(\n\t\t\tnot_null<PeerListsBox*> box,\n\t\t\tnot_null<PeerListController*> controller);\n\n\t\tvoid peerListSetTitle(rpl::producer<QString> title) override;\n\t\tvoid peerListSetAdditionalTitle(rpl::producer<QString> title) override;\n\t\tvoid peerListSetSearchMode(PeerListSearchMode mode) override;\n\t\tvoid peerListSetRowChecked(\n\t\t\tnot_null<PeerListRow*> row,\n\t\t\tbool checked) override;\n\t\tvoid peerListSetForeignRowChecked(\n\t\t\tnot_null<PeerListRow*> row,\n\t\t\tbool checked,\n\t\t\tanim::type animated) override;\n\t\tbool peerListIsRowChecked(not_null<PeerListRow*> row) override;\n\t\tint peerListSelectedRowsCount() override;\n\t\tvoid peerListScrollToTop() override;\n\n\t\tvoid peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) override {\n\t\t\t_box->addSelectItem(peer, anim::type::instant);\n\t\t}\n\t\tvoid peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) override {\n\t\t\t_box->addSelectItem(row, anim::type::instant);\n\t\t}\n\t\tvoid peerListFinishSelectedRowsBunch() override;\n\t\tstd::shared_ptr<Main::SessionShow> peerListUiShow() override;\n\n\tprivate:\n\t\tconst not_null<PeerListsBox*> _box;\n\t\tconst not_null<PeerListController*> _controller;\n\t\tconst std::shared_ptr<Main::SessionShow> _show;\n\n\t};\n\tstruct List {\n\t\tstd::unique_ptr<PeerListController> controller;\n\t\tstd::unique_ptr<Delegate> delegate;\n\t\tPeerListContent *content = nullptr;\n\t\tUi::SlideWrap<Ui::RpWidget> *separator = nullptr;\n\t};\n\n\tfriend class Delegate;\n\n\t[[nodiscard]] List makeList(\n\t\tstd::unique_ptr<PeerListController> controller);\n\t[[nodiscard]] std::vector<List> makeLists(\n\t\tstd::vector<std::unique_ptr<PeerListController>> controllers);\n\n\t[[nodiscard]] not_null<PeerListController*> firstController() const;\n\n\tvoid addSelectItem(\n\t\tnot_null<PeerData*> peer,\n\t\tanim::type animated);\n\tvoid addSelectItem(\n\t\tnot_null<PeerListRow*> row,\n\t\tanim::type animated);\n\tvoid addSelectItem(\n\t\tuint64 itemId,\n\t\tconst QString &text,\n\t\tPaintRoundImageCallback paintUserpic,\n\t\tanim::type animated);\n\tvoid setSearchMode(PeerListSearchMode mode);\n\tvoid createMultiSelect();\n\tint getTopScrollSkip() const;\n\tvoid updateScrollSkips();\n\tvoid searchQueryChanged(const QString &query);\n\n\tobject_ptr<Ui::SlideWrap<Ui::MultiSelect>> _select = { nullptr };\n\n\tstd::vector<List> _lists;\n\tFn<void(PeerListsBox*)> _init;\n\tbool _scrollBottomFixed = false;\n\tUi::VerticalLayout *_rows = nullptr;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/add_bot_to_chat_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_histories.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n#include \"boxes/peers/edit_participant_box.h\"\n#include \"boxes/peers/edit_participants_box.h\"\n#include \"boxes/filters/edit_filter_chats_list.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/ui_utility.h\"\n#include \"base/random.h\"\n#include \"base/weak_ptr.h\"\n#include \"api/api_chat_participants.h\"\n#include \"window/window_session_controller.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace {\n\nclass Controller final\n\t: public PeerListController\n\t, public base::has_weak_ptr {\npublic:\n\tController(\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<not_null<PeerData*>> add,\n\t\tFn<void(not_null<PeerData*> chat)> callback);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\nprivate:\n\tvoid addRow(not_null<PeerData*> peer);\n\n\tconst not_null<Main::Session*> _session;\n\tFn<void(not_null<PeerData*> chat)> _callback;\n\tstd::vector<not_null<PeerData*>> _list;\n\tbool _prepared = false;\n\tbool _refreshing = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\nController::Controller(\n\tnot_null<Main::Session*> session,\n\trpl::producer<not_null<PeerData*>> add,\n\tFn<void(not_null<PeerData*> chat)> callback)\n: _session(session)\n, _callback(std::move(callback)) {\n\tstd::move(\n\t\tadd\n\t) | rpl::on_next([=](not_null<PeerData*> peer) {\n\t\tif (_prepared) {\n\t\t\taddRow(peer);\n\t\t} else {\n\t\t\t_list.push_back(peer);\n\t\t}\n\t}, _lifetime);\n}\n\nMain::Session &Controller::session() const {\n\treturn *_session;\n}\n\nvoid Controller::prepare() {\n\t_prepared = true;\n\tfor (const auto &peer : _list) {\n\t\taddRow(peer);\n\t}\n}\n\nvoid Controller::rowClicked(not_null<PeerListRow*> row) {\n\t_callback(row->peer());\n}\n\nvoid Controller::addRow(not_null<PeerData*> peer) {\n\tif (delegate()->peerListFindRow(peer->id.value)) {\n\t\treturn;\n\t}\n\tdelegate()->peerListAppendRow(std::make_unique<PeerListRow>(peer));\n\tif (!_refreshing) {\n\t\t_refreshing = true;\n\t\tUi::PostponeCall(this, [=] {\n\t\t\t_refreshing = false;\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t});\n\t}\n}\n\n} // namespace\n\nvoid AddBotToGroupBoxController::Start(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> bot,\n\t\tScope scope,\n\t\tconst QString &token,\n\t\tChatAdminRights requestedRights) {\n\tif (controller->showFrozenError()) {\n\t\treturn;\n\t}\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tbox->addButton(tr::lng_cancel(), [box] { box->closeBox(); });\n\t};\n\tcontroller->show(Box<PeerListBox>(\n\t\tstd::make_unique<AddBotToGroupBoxController>(\n\t\t\tcontroller,\n\t\t\tbot,\n\t\t\tscope,\n\t\t\ttoken,\n\t\t\trequestedRights),\n\t\tstd::move(initBox)));\n}\n\nAddBotToGroupBoxController::AddBotToGroupBoxController(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<UserData*> bot,\n\tScope scope,\n\tconst QString &token,\n\tChatAdminRights requestedRights)\n: ChatsListBoxController(std::unique_ptr<PeerListSearchController>())\n, _controller(controller)\n, _bot(bot)\n, _scope(scope)\n, _token(token)\n, _requestedRights(requestedRights)\n, _adminToGroup((scope == Scope::GroupAdmin)\n\t|| (scope == Scope::All && _bot->botInfo->groupAdminRights != 0))\n, _adminToChannel((scope == Scope::ChannelAdmin)\n\t|| (scope == Scope::All && _bot->botInfo->channelAdminRights != 0))\n, _memberToGroup(scope == Scope::All) {\n}\n\nMain::Session &AddBotToGroupBoxController::session() const {\n\treturn _bot->session();\n}\n\nvoid AddBotToGroupBoxController::rowClicked(not_null<PeerListRow*> row) {\n\taddBotToGroup(row->peer());\n}\n\nvoid AddBotToGroupBoxController::requestExistingRights(\n\t\tnot_null<ChannelData*> channel) {\n\tif (_existingRightsChannel == channel) {\n\t\treturn;\n\t}\n\t_existingRightsChannel = channel;\n\t_bot->session().api().request(_existingRightsRequestId).cancel();\n\t_existingRightsRequestId = _bot->session().api().request(\n\t\tMTPchannels_GetParticipant(\n\t\t\t_existingRightsChannel->inputChannel(),\n\t\t\t_bot->input())\n\t).done([=](const MTPchannels_ChannelParticipant &result) {\n\t\tresult.match([&](const MTPDchannels_channelParticipant &data) {\n\t\t\tchannel->owner().processUsers(data.vusers());\n\t\t\tconst auto participant = Api::ChatParticipant(\n\t\t\t\tdata.vparticipant(),\n\t\t\t\tchannel);\n\t\t\t_existingRights = participant.rights().flags;\n\t\t\t_existingRank = participant.rank();\n\t\t\t_promotedSince = participant.promotedSince();\n\t\t\t_promotedBy = participant.by();\n\t\t\taddBotToGroup(_existingRightsChannel);\n\t\t});\n\t}).fail([=] {\n\t\t_existingRights = ChatAdminRights();\n\t\t_existingRank = QString();\n\t\t_promotedSince = 0;\n\t\t_promotedBy = 0;\n\t\taddBotToGroup(_existingRightsChannel);\n\t}).send();\n}\n\nvoid AddBotToGroupBoxController::addBotToGroup(not_null<PeerData*> chat) {\n\tif (const auto megagroup = chat->asMegagroup()) {\n\t\tif (!megagroup->canAddMembers()) {\n\t\t\t_controller->show(\n\t\t\t\tUi::MakeInformBox(tr::lng_error_cant_add_member()));\n\t\t\treturn;\n\t\t}\n\t}\n\tif (_existingRightsChannel != chat) {\n\t\t_existingRights = {};\n\t\t_existingRank = QString();\n\t\t_existingRightsChannel = nullptr;\n\t\t_promotedSince = 0;\n\t\t_promotedBy = 0;\n\t\t_bot->session().api().request(_existingRightsRequestId).cancel();\n\t}\n\tconst auto requestedAddAdmin = (_scope == Scope::GroupAdmin)\n\t\t|| (_scope == Scope::ChannelAdmin);\n\tif (chat->isChannel()\n\t\t&& requestedAddAdmin\n\t\t&& !_existingRights.has_value()) {\n\t\trequestExistingRights(chat->asChannel());\n\t\treturn;\n\t}\n\tconst auto bot = _bot;\n\tconst auto controller = _controller;\n\tconst auto close = [=](auto&&...) {\n\t\tusing Way = Window::SectionShow::Way;\n\t\tcontroller->hideLayer();\n\t\tcontroller->showPeerHistory(chat, Way::ClearStack, ShowAtUnreadMsgId);\n\t};\n\tconst auto rights = (requestedAddAdmin && _requestedRights != 0)\n\t\t? _requestedRights\n\t\t: (chat->isBroadcast()\n\t\t\t&& chat->asBroadcast()->canAddAdmins())\n\t\t? bot->botInfo->channelAdminRights\n\t\t: ((chat->isMegagroup() && chat->asMegagroup()->canAddAdmins())\n\t\t\t|| (chat->isChat() && chat->asChat()->canAddAdmins()))\n\t\t? bot->botInfo->groupAdminRights\n\t\t: ChatAdminRights();\n\tconst auto addingAdmin = requestedAddAdmin || (rights != 0);\n\tconst auto show = controller->uiShow();\n\tif (addingAdmin) {\n\t\tconst auto scope = _scope;\n\t\tconst auto token = _token;\n\t\tconst auto done = [=](\n\t\t\t\tChatAdminRightsInfo newRights,\n\t\t\t\tconst std::optional<QString> &rank) {\n\t\t\tif (scope == Scope::GroupAdmin && !token.isEmpty()) {\n\t\t\t\tchat->session().api().sendBotStart(show, bot, chat, token);\n\t\t\t}\n\t\t\tclose();\n\t\t};\n\t\tconst auto saveCallback = SaveAdminCallback(\n\t\t\tshow,\n\t\t\tchat,\n\t\t\tbot,\n\t\t\tdone,\n\t\t\tclose);\n\t\tauto box = Box<EditAdminBox>(\n\t\t\tchat,\n\t\t\tbot,\n\t\t\tChatAdminRightsInfo(rights),\n\t\t\t_existingRank,\n\t\t\t_promotedSince,\n\t\t\t_promotedBy ? chat->owner().user(_promotedBy).get() : nullptr,\n\t\t\tEditAdminBotFields{\n\t\t\t\t_token,\n\t\t\t\t_existingRights.value_or(ChatAdminRights()),\n\t\t\t});\n\t\tbox->setSaveCallback(saveCallback);\n\t\tcontroller->show(std::move(box));\n\t} else {\n\t\tauto callback = crl::guard(this, [=] {\n\t\t\tAddBotToGroup(show, bot, chat, _token);\n\t\t\tcontroller->hideLayer();\n\t\t});\n\t\tcontroller->show(Ui::MakeConfirmBox({\n\t\t\ttr::lng_bot_sure_invite(tr::now, lt_group, chat->name()),\n\t\t\tstd::move(callback),\n\t\t}));\n\t}\n}\n\nauto AddBotToGroupBoxController::createRow(not_null<History*> history)\n-> std::unique_ptr<ChatsListBoxController::Row> {\n\tif (!needToCreateRow(history->peer)) {\n\t\treturn nullptr;\n\t}\n\treturn std::make_unique<Row>(history);\n}\n\nbool AddBotToGroupBoxController::needToCreateRow(\n\t\tnot_null<PeerData*> peer) const {\n\tif (const auto chat = peer->asChat()) {\n\t\tif (onlyAdminToGroup()) {\n\t\t\treturn chat->canAddAdmins();\n\t\t} else if (_adminToGroup && chat->canAddAdmins()) {\n\t\t\t_groups.fire_copy(peer);\n\t\t} else if (!onlyAdminToChannel()) {\n\t\t\treturn chat->canAddMembers();\n\t\t}\n\t} else if (const auto group = peer->asMegagroup()) {\n\t\tif (onlyAdminToGroup()) {\n\t\t\treturn group->canAddAdmins();\n\t\t} else if (_adminToGroup && group->canAddAdmins()) {\n\t\t\t_groups.fire_copy(peer);\n\t\t} else if (!onlyAdminToChannel()) {\n\t\t\treturn group->canAddMembers();\n\t\t}\n\t} else if (const auto channel = peer->asBroadcast()) {\n\t\tif (onlyAdminToChannel()) {\n\t\t\treturn channel->canAddAdmins();\n\t\t} else if (_adminToChannel && channel->canAddAdmins()) {\n\t\t\t_channels.fire_copy(peer);\n\t\t}\n\t}\n\treturn false;\n}\n\nQString AddBotToGroupBoxController::emptyBoxText() const {\n\treturn !session().data().chatsListLoaded()\n\t\t? tr::lng_contacts_loading(tr::now)\n\t\t: _adminToChannel\n\t\t? tr::lng_bot_no_chats(tr::now)\n\t\t: tr::lng_bot_no_groups(tr::now);\n}\n\nQString AddBotToGroupBoxController::noResultsText() const {\n\treturn !session().data().chatsListLoaded()\n\t\t? tr::lng_contacts_loading(tr::now)\n\t\t: _adminToChannel\n\t\t? tr::lng_bot_chats_not_found(tr::now)\n\t\t: tr::lng_bot_groups_not_found(tr::now);\n}\n\nvoid AddBotToGroupBoxController::updateLabels() {\n\tsetSearchNoResultsText(noResultsText());\n}\n\nobject_ptr<Ui::RpWidget> AddBotToGroupBoxController::prepareAdminnedChats() {\n\tauto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tconst auto container = result.data();\n\n\tconst auto callback = [=](not_null<PeerData*> chat) {\n\t\taddBotToGroup(chat);\n\t};\n\n\tconst auto addList = [&](\n\t\t\ttr::phrase<> subtitle,\n\t\t\trpl::event_stream<not_null<PeerData*>> &items) {\n\t\tconst auto wrap = container->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\t\twrap->hide(anim::type::instant);\n\n\t\tconst auto inner = wrap->entity();\n\t\tinner->add(CreatePeerListSectionSubtitle(inner, subtitle()));\n\n\t\tconst auto delegate = inner->lifetime().make_state<\n\t\t\tPeerListContentDelegateSimple\n\t\t>();\n\t\tconst auto controller = inner->lifetime().make_state<Controller>(\n\t\t\t&session(),\n\t\t\titems.events(),\n\t\t\tcallback);\n\t\tconst auto content = inner->add(object_ptr<PeerListContent>(\n\t\t\tcontainer,\n\t\t\tcontroller));\n\t\tdelegate->setContent(content);\n\t\tcontroller->setDelegate(delegate);\n\n\t\titems.events() | rpl::take(1) | rpl::on_next([=] {\n\t\t\twrap->show(anim::type::instant);\n\t\t}, inner->lifetime());\n\t};\n\tif (_adminToChannel) {\n\t\taddList(tr::lng_bot_channels_manage, _channels);\n\t}\n\tif (_adminToGroup) {\n\t\taddList(tr::lng_bot_groups_manage, _groups);\n\t}\n\n\trpl::merge(\n\t\t_groups.events(),\n\t\t_channels.events()\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\tcontainer->add(CreatePeerListSectionSubtitle(\n\t\t\tcontainer,\n\t\t\ttr::lng_bot_groups()));\n\t}, container->lifetime());\n\n\treturn result;\n}\n\nbool AddBotToGroupBoxController::onlyAdminToGroup() const {\n\treturn _adminToGroup && !_memberToGroup && !_adminToChannel;\n}\n\nbool AddBotToGroupBoxController::onlyAdminToChannel() const {\n\treturn _adminToChannel && !_memberToGroup && !_adminToGroup;\n}\n\nvoid AddBotToGroupBoxController::prepareViewHook() {\n\tdelegate()->peerListSetTitle(_adminToChannel\n\t\t? tr::lng_bot_choose_chat()\n\t\t: tr::lng_bot_choose_group());\n\tif ((_adminToGroup && !onlyAdminToGroup())\n\t\t|| (_adminToChannel && !onlyAdminToChannel())) {\n\t\tdelegate()->peerListSetAboveWidget(prepareAdminnedChats());\n\t}\n\n\tupdateLabels();\n\tsession().data().chatsListLoadedEvents(\n\t) | rpl::filter([=](Data::Folder *folder) {\n\t\treturn !folder;\n\t}) | rpl::on_next([=] {\n\t\tupdateLabels();\n\t}, lifetime());\n}\n\nvoid AddBotToGroup(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<UserData*> bot,\n\t\tnot_null<PeerData*> chat,\n\t\tconst QString &startToken) {\n\tif (!startToken.isEmpty()) {\n\t\tchat->session().api().sendBotStart(show, bot, chat, startToken);\n\t} else {\n\t\tchat->session().api().chatParticipants().add(show, chat, { 1, bot });\n\t}\n\tif (const auto window = chat->session().tryResolveWindow()) {\n\t\twindow->showPeerHistory(chat);\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peer_list_controllers.h\"\n#include \"data/data_chat_participant_status.h\"\n\nclass AddBotToGroupBoxController\n\t: public ChatsListBoxController\n\t, public base::has_weak_ptr {\npublic:\n\tenum class Scope {\n\t\tNone,\n\t\tGroupAdmin,\n\t\tChannelAdmin,\n\t\tAll,\n\t};\n\tstatic void Start(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> bot,\n\t\tScope scope = Scope::All,\n\t\tconst QString &token = QString(),\n\t\tChatAdminRights requestedRights = {});\n\n\tAddBotToGroupBoxController(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> bot,\n\t\tScope scope,\n\t\tconst QString &token,\n\t\tChatAdminRights requestedRights);\n\n\tMain::Session &session() const override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\nprotected:\n\tstd::unique_ptr<Row> createRow(not_null<History*> history) override;\n\tvoid prepareViewHook() override;\n\tQString emptyBoxText() const override;\n\nprivate:\n\t[[nodiscard]] object_ptr<Ui::RpWidget> prepareAdminnedChats();\n\n\t[[nodiscard]] bool onlyAdminToGroup() const;\n\t[[nodiscard]] bool onlyAdminToChannel() const;\n\n\tbool needToCreateRow(not_null<PeerData*> peer) const;\n\tQString noResultsText() const;\n\tvoid updateLabels();\n\n\tvoid addBotToGroup(not_null<PeerData*> chat);\n\tvoid requestExistingRights(not_null<ChannelData*> channel);\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<UserData*> _bot;\n\tconst Scope _scope = Scope::None;\n\tconst QString _token;\n\tconst ChatAdminRights _requestedRights;\n\n\tChannelData *_existingRightsChannel = nullptr;\n\tmtpRequestId _existingRightsRequestId = 0;\n\tstd::optional<ChatAdminRights> _existingRights;\n\tQString _existingRank;\n\tTimeId _promotedSince = 0;\n\tUserId _promotedBy = 0;\n\n\trpl::event_stream<not_null<PeerData*>> _groups;\n\trpl::event_stream<not_null<PeerData*>> _channels;\n\n\tbool _adminToGroup = false;\n\tbool _adminToChannel = false;\n\tbool _memberToGroup = false;\n\n};\n\nvoid AddBotToGroup(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<UserData*> bot,\n\tnot_null<PeerData*> chat,\n\tconst QString &startToken);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/add_participants_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/add_participants_box.h\"\n\n#include \"api/api_chat_participants.h\"\n#include \"api/api_invite_links.h\"\n#include \"api/api_premium.h\"\n#include \"boxes/peers/edit_participant_box.h\"\n#include \"boxes/peers/edit_peer_type_box.h\"\n#include \"boxes/peers/replace_boost_box.h\"\n#include \"boxes/max_invite_box.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"lang/lang_keys.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_peer_values.h\"\n#include \"history/history.h\"\n#include \"history/history_item_helpers.h\"\n#include \"dialogs/dialogs_indexed_list.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/show_or_premium_box.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/text/text_utilities.h\" // tr::rich\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/gradient_round_button.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/painter.h\"\n#include \"base/unixtime.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"window/window_session_controller.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_premium.h\"\n\nnamespace {\n\nconstexpr auto kParticipantsFirstPageCount = 16;\nconstexpr auto kParticipantsPerPage = 200;\nconstexpr auto kUserpicsLimit = 3;\n\nclass ForbiddenRow final : public PeerListRow {\npublic:\n\tForbiddenRow(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<const style::PeerListItem*> lockSt,\n\t\tbool locked);\n\n\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\n\tApi::MessageMoneyRestriction restriction() const;\n\tvoid setRestriction(Api::MessageMoneyRestriction restriction);\n\n\tvoid preloadUserpic() override;\n\tvoid paintUserpicOverlay(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth) override;\n\n\tbool refreshLock();\n\nprivate:\n\tstruct Restriction {\n\t\tApi::MessageMoneyRestriction value;\n\t\tRestrictionBadgeCache cache;\n\t};\n\n\tconst bool _locked = false;\n\tconst not_null<const style::PeerListItem*> _lockSt;\n\tQImage _disabledFrame;\n\tInMemoryKey _userpicKey;\n\tint _paletteVersion = 0;\n\tstd::shared_ptr<Restriction> _restriction;\n\n};\n\nclass InviteForbiddenController final : public PeerListController {\npublic:\n\tInviteForbiddenController(\n\t\tnot_null<PeerData*> peer,\n\t\tForbiddenInvites forbidden);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\n\t[[nodiscard]] bool canInvite() const {\n\t\treturn _can;\n\t}\n\t[[nodiscard]] rpl::producer<int> selectedValue() const {\n\t\treturn _selected.value();\n\t}\n\t[[nodiscard]] rpl::producer<int> starsToSend() const {\n\t\treturn _starsToSend.value();\n\t}\n\n\tvoid send(\n\t\tstd::vector<not_null<PeerData*>> list,\n\t\tUi::ShowPtr show,\n\t\tFn<void()> close);\n\nprivate:\n\tvoid appendRow(not_null<UserData*> user);\n\t[[nodiscard]] std::unique_ptr<ForbiddenRow> createRow(\n\t\tnot_null<UserData*> user) const;\n\t[[nodiscard]] bool canInvite(not_null<PeerData*> peer) const;\n\n\tvoid send(\n\t\tstd::vector<not_null<PeerData*>> list,\n\t\tUi::ShowPtr show,\n\t\tFn<void()> close,\n\t\tApi::SendOptions options);\n\n\tvoid setSimpleCover();\n\tvoid setComplexCover();\n\n\tconst not_null<PeerData*> _peer;\n\tconst ForbiddenInvites _forbidden;\n\tconst std::vector<not_null<UserData*>> &_users;\n\tconst bool _can = false;\n\trpl::variable<int> _selected;\n\trpl::variable<int> _starsToSend;\n\tbool _sending = false;\n\n\trpl::lifetime _paymentCheckLifetime;\n\n};\n\nbase::flat_set<not_null<UserData*>> GetAlreadyInFromPeer(PeerData *peer) {\n\tif (!peer) {\n\t\treturn {};\n\t}\n\tif (const auto chat = peer->asChat()) {\n\t\treturn chat->participants;\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tif (channel->isMegagroup() && channel->canViewMembers()) {\n\t\t\tconst auto &participants = channel->mgInfo->lastParticipants;\n\t\t\treturn { participants.cbegin(), participants.cend() };\n\t\t}\n\t}\n\treturn {};\n}\n\nvoid FillUpgradeToPremiumCover(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<PeerData*> peer,\n\t\tconst ForbiddenInvites &forbidden) {\n\tconst auto noneCanSend = (forbidden.premiumAllowsWrite.size()\n\t\t== forbidden.users.size());\n\tconst auto &userpicUsers = (forbidden.premiumAllowsInvite.empty()\n\t\t|| noneCanSend)\n\t\t? forbidden.premiumAllowsWrite\n\t\t: forbidden.premiumAllowsInvite;\n\tAssert(!userpicUsers.empty());\n\n\tauto userpicPeers = userpicUsers | ranges::views::transform([](auto u) {\n\t\treturn not_null<PeerData*>(u);\n\t}) | ranges::to_vector;\n\tcontainer->add(object_ptr<Ui::PaddingWrap<>>(\n\t\tcontainer,\n\t\tCreateUserpicsWithMoreBadge(\n\t\t\tcontainer,\n\t\t\trpl::single(std::move(userpicPeers)),\n\t\t\tst::boostReplaceUserpicsRow,\n\t\t\tkUserpicsLimit),\n\t\tst::inviteForbiddenUserpicsPadding)\n\t)->entity()->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto users = int(userpicUsers.size());\n\tconst auto names = std::min(users, kUserpicsLimit);\n\tconst auto remaining = std::max(users - kUserpicsLimit, 0);\n\tauto text = TextWithEntities();\n\tfor (auto i = 0; i != names; ++i) {\n\t\tconst auto name = userpicUsers[i]->shortName();\n\t\tif (text.empty()) {\n\t\t\ttext = tr::bold(name);\n\t\t} else if (i == names - 1 && !remaining) {\n\t\t\ttext = tr::lng_invite_upgrade_users_few(\n\t\t\t\ttr::now,\n\t\t\t\tlt_users,\n\t\t\t\ttext,\n\t\t\t\tlt_last,\n\t\t\t\ttr::bold(name),\n\t\t\t\ttr::rich);\n\t\t} else {\n\t\t\ttext.append(\", \").append(tr::bold(name));\n\t\t}\n\t}\n\tif (remaining > 0) {\n\t\ttext = tr::lng_invite_upgrade_users_many(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tremaining,\n\t\t\tlt_users,\n\t\t\ttext,\n\t\t\ttr::rich);\n\t}\n\tconst auto inviteOnly = !forbidden.premiumAllowsInvite.empty()\n\t\t&& (forbidden.premiumAllowsWrite.size() != forbidden.users.size());\n\ttext = (peer->isBroadcast()\n\t\t? (inviteOnly\n\t\t\t? tr::lng_invite_upgrade_channel_invite\n\t\t\t: tr::lng_invite_upgrade_channel_write)\n\t\t: (inviteOnly\n\t\t\t? tr::lng_invite_upgrade_group_invite\n\t\t\t: tr::lng_invite_upgrade_group_write))(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tint(userpicUsers.size()),\n\t\t\t\tlt_users,\n\t\t\t\ttext,\n\t\t\t\ttr::rich);\n\tcontainer->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\trpl::single(text),\n\t\t\tst::inviteForbiddenInfo),\n\t\tst::inviteForbiddenInfoPadding,\n\t\tstyle::al_top);\n}\n\nvoid SimpleForbiddenBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<PeerData*> peer,\n\t\tconst ForbiddenInvites &forbidden) {\n\tbox->setTitle(tr::lng_invite_upgrade_title());\n\tbox->setWidth(st::boxWideWidth);\n\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\tbox->closeBox();\n\t});\n\n\tauto sshow = Main::MakeSessionShow(box->uiShow(), &peer->session());\n\tconst auto container = box->verticalLayout();\n\tFillUpgradeToPremiumCover(container, sshow, peer, forbidden);\n\n\tconst auto &stButton = st::premiumGiftBox;\n\tbox->setStyle(stButton);\n\tauto raw = Settings::CreateSubscribeButton(\n\t\tsshow,\n\t\tChatHelpers::ResolveWindowDefault(),\n\t\t{\n\t\t\t.parent = container,\n\t\t\t.computeRef = [] { return u\"invite_privacy\"_q; },\n\t\t\t.text = tr::lng_messages_privacy_premium_button(),\n\t\t\t.showPromo = true,\n\t\t});\n\tauto button = object_ptr<Ui::GradientButton>::fromRaw(raw);\n\tbutton->resizeToWidth(st::boxWideWidth\n\t\t- stButton.buttonPadding.left()\n\t\t- stButton.buttonPadding.right());\n\tbox->setShowFinishedCallback([raw = button.data()] {\n\t\traw->startGlareAnimation();\n\t});\n\tbox->addButton(std::move(button));\n\n\tData::AmPremiumValue(\n\t\t&peer->session()\n\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\tbox->closeBox();\n\t}, box->lifetime());\n}\n\nInviteForbiddenController::InviteForbiddenController(\n\tnot_null<PeerData*> peer,\n\tForbiddenInvites forbidden)\n: _peer(peer)\n, _forbidden(std::move(forbidden))\n, _users(_forbidden.users)\n, _can(peer->isChat()\n\t? peer->asChat()->canHaveInviteLink()\n\t: peer->asChannel()->canHaveInviteLink())\n, _selected(_can\n\t? (int(_users.size()) - int(_forbidden.premiumAllowsWrite.size()))\n\t: 0) {\n}\n\nMain::Session &InviteForbiddenController::session() const {\n\treturn _peer->session();\n}\n\nForbiddenRow::ForbiddenRow(\n\tnot_null<PeerData*> peer,\n\tnot_null<const style::PeerListItem*> lockSt,\n\tbool locked)\n: PeerListRow(peer)\n, _locked(locked)\n, _lockSt(lockSt) {\n\tif (_locked) {\n\t\tsetCustomStatus(tr::lng_invite_status_disabled(tr::now));\n\t} else {\n\t\tsetRestriction(Api::ResolveMessageMoneyRestrictions(peer, nullptr));\n\t}\n}\n\nPaintRoundImageCallback ForbiddenRow::generatePaintUserpicCallback(\n\t\tbool forceRound) {\n\tconst auto peer = this->peer();\n\tconst auto saved = peer->isSelf();\n\tconst auto replies = peer->isRepliesChat();\n\tconst auto verifyCodes = peer->isVerifyCodes();\n\tauto userpic = (saved || replies || verifyCodes)\n\t\t? Ui::PeerUserpicView()\n\t\t: ensureUserpicView();\n\tauto paint = [=](\n\t\t\tPainter &p,\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tint outerWidth,\n\t\t\tint size) mutable {\n\t\tpeer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);\n\t};\n\tif (!_locked) {\n\t\treturn paint;\n\t}\n\treturn [=](\n\t\t\tPainter &p,\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tint outerWidth,\n\t\t\tint size) mutable {\n\t\tconst auto wide = size + style::ConvertScale(3);\n\t\tconst auto full = QSize(wide, wide) * style::DevicePixelRatio();\n\t\tauto repaint = false;\n\t\tif (_disabledFrame.size() != full) {\n\t\t\trepaint = true;\n\t\t\t_disabledFrame = QImage(\n\t\t\t\tfull,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_disabledFrame.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t} else {\n\t\t\trepaint = (_paletteVersion != style::PaletteVersion())\n\t\t\t\t|| (!saved\n\t\t\t\t\t&& !replies\n\t\t\t\t\t&& !verifyCodes\n\t\t\t\t\t&& (_userpicKey != peer->userpicUniqueKey(userpic)));\n\t\t}\n\t\tif (repaint) {\n\t\t\t_paletteVersion = style::PaletteVersion();\n\t\t\t_userpicKey = peer->userpicUniqueKey(userpic);\n\n\t\t\t_disabledFrame.fill(Qt::transparent);\n\t\t\tauto p = Painter(&_disabledFrame);\n\t\t\tpaint(p, 0, 0, wide, size);\n\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setBrush(st::boxBg);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tconst auto lock = st::inviteForbiddenLockIcon.size();\n\t\t\tconst auto stroke = style::ConvertScale(2);\n\t\t\tconst auto inner = QRect(\n\t\t\t\tsize + (stroke / 2) - lock.width(),\n\t\t\t\tsize + (stroke / 2) - lock.height(),\n\t\t\t\tlock.width(),\n\t\t\t\tlock.height());\n\t\t\tconst auto half = stroke / 2.;\n\t\t\tconst auto rect = QRectF(inner).marginsAdded(\n\t\t\t\t{ half, half, half, half });\n\t\t\tauto pen = st::boxBg->p;\n\t\t\tpen.setWidthF(stroke);\n\t\t\tp.setPen(pen);\n\t\t\tp.setBrush(st::inviteForbiddenLockBg);\n\t\t\tp.drawEllipse(rect);\n\n\t\t\tst::inviteForbiddenLockIcon.paintInCenter(p, inner);\n\t\t}\n\t\tp.drawImage(x, y, _disabledFrame);\n\t};\n}\n\n\nApi::MessageMoneyRestriction ForbiddenRow::restriction() const {\n\treturn _restriction\n\t\t? _restriction->value\n\t\t: Api::MessageMoneyRestriction();\n}\n\nvoid ForbiddenRow::setRestriction(Api::MessageMoneyRestriction restriction) {\n\tif (!restriction || !restriction.starsPerMessage) {\n\t\t_restriction = nullptr;\n\t\treturn;\n\t} else if (!_restriction) {\n\t\t_restriction = std::make_unique<Restriction>();\n\t}\n\t_restriction->value = restriction;\n}\n\nvoid ForbiddenRow::paintUserpicOverlay(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth) {\n\tif (const auto &r = _restriction) {\n\t\tPaintRestrictionBadge(\n\t\t\tp,\n\t\t\t_lockSt,\n\t\t\tr->value.starsPerMessage,\n\t\t\tr->cache,\n\t\t\tx,\n\t\t\ty,\n\t\t\touterWidth,\n\t\t\tst.photoSize);\n\t}\n}\n\nbool ForbiddenRow::refreshLock() {\n\tif (_locked) {\n\t\treturn false;\n\t} else if (const auto user = peer()->asUser()) {\n\t\tusing Restriction = Api::MessageMoneyRestriction;\n\t\tauto r = Api::ResolveMessageMoneyRestrictions(user, nullptr);\n\t\tif (!r || !r.starsPerMessage) {\n\t\t\tr = Restriction();\n\t\t}\n\t\tif ((_restriction ? _restriction->value : Restriction()) != r) {\n\t\t\tsetRestriction(r);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid ForbiddenRow::preloadUserpic() {\n\tPeerListRow::preloadUserpic();\n\n\tconst auto peer = this->peer();\n\tconst auto known = Api::ResolveMessageMoneyRestrictions(\n\t\tpeer,\n\t\tnullptr).known;\n\tif (known) {\n\t\treturn;\n\t} else if (const auto user = peer->asUser()) {\n\t\tconst auto api = &user->session().api();\n\t\tapi->premium().resolveMessageMoneyRestrictions(user);\n\t} else if (const auto group = peer->asChannel()) {\n\t\tgroup->updateFull();\n\t}\n}\n\nvoid InviteForbiddenController::setSimpleCover() {\n\tdelegate()->peerListSetTitle(\n\t\t_can ? tr::lng_profile_add_via_link() : tr::lng_via_link_cant());\n\tconst auto broadcast = _peer->isBroadcast();\n\tconst auto count = int(_users.size());\n\tconst auto phraseCounted = !_can\n\t\t? tr::lng_via_link_cant_many\n\t\t: broadcast\n\t\t? tr::lng_via_link_channel_many\n\t\t: tr::lng_via_link_group_many;\n\tconst auto phraseNamed = !_can\n\t\t? tr::lng_via_link_cant_one\n\t\t: broadcast\n\t\t? tr::lng_via_link_channel_one\n\t\t: tr::lng_via_link_group_one;\n\tauto text = (count != 1)\n\t\t? phraseCounted(\n\t\t\tlt_count,\n\t\t\trpl::single<float64>(count),\n\t\t\ttr::rich)\n\t\t: phraseNamed(\n\t\t\tlt_user,\n\t\t\trpl::single(TextWithEntities{ _users.front()->name() }),\n\t\t\ttr::rich);\n\tdelegate()->peerListSetAboveWidget(object_ptr<Ui::PaddingWrap<>>(\n\t\t(QWidget*)nullptr,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t(QWidget*)nullptr,\n\t\t\tstd::move(text),\n\t\t\tst::requestPeerRestriction),\n\t\tst::boxRowPadding));\n}\n\nvoid InviteForbiddenController::setComplexCover() {\n\tdelegate()->peerListSetTitle(tr::lng_invite_upgrade_title());\n\n\tauto cover = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tconst auto container = cover.data();\n\tconst auto show = delegate()->peerListUiShow();\n\tFillUpgradeToPremiumCover(container, show, _peer, _forbidden);\n\n\tcontainer->add(\n\t\tobject_ptr<Ui::GradientButton>::fromRaw(\n\t\t\tSettings::CreateSubscribeButton(\n\t\t\t\tshow,\n\t\t\t\tChatHelpers::ResolveWindowDefault(),\n\t\t\t\t{\n\t\t\t\t\t.parent = container,\n\t\t\t\t\t.computeRef = [] { return u\"invite_privacy\"_q; },\n\t\t\t\t\t.text = tr::lng_messages_privacy_premium_button(),\n\t\t\t\t})),\n\t\t\t\tst::inviteForbiddenSubscribePadding);\n\n\tif (_forbidden.users.size() > _forbidden.premiumAllowsWrite.size()) {\n\t\tif (_can) {\n\t\t\tcontainer->add(\n\t\t\t\tMakeShowOrLabel(container, tr::lng_invite_upgrade_or()),\n\t\t\t\tst::inviteForbiddenOrLabelPadding,\n\t\t\t\tstyle::al_justify);\n\t\t}\n\t\tcontainer->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\t(_can\n\t\t\t\t\t? tr::lng_invite_upgrade_via_title()\n\t\t\t\t\t: tr::lng_via_link_cant()),\n\t\t\t\tst::inviteForbiddenTitle),\n\t\t\tst::inviteForbiddenTitlePadding,\n\t\t\tstyle::al_top);\n\n\t\tconst auto about = _can\n\t\t\t? (_peer->isBroadcast()\n\t\t\t\t? tr::lng_invite_upgrade_via_channel_about\n\t\t\t\t: tr::lng_invite_upgrade_via_group_about)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\ttr::marked)\n\t\t\t: (_forbidden.users.size() == 1\n\t\t\t\t? tr::lng_via_link_cant_one(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tTextWithEntities{ _forbidden.users.front()->shortName() },\n\t\t\t\t\ttr::rich)\n\t\t\t\t: tr::lng_via_link_cant_many(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tint(_forbidden.users.size()),\n\t\t\t\t\ttr::rich));\n\t\tcontainer->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\trpl::single(about),\n\t\t\t\tst::inviteForbiddenInfo),\n\t\t\tst::inviteForbiddenInfoPadding,\n\t\t\tstyle::al_top);\n\t}\n\tdelegate()->peerListSetAboveWidget(std::move(cover));\n}\n\nvoid InviteForbiddenController::prepare() {\n\tsession().api().premium().someMessageMoneyRestrictionsResolved(\n\t) | rpl::on_next([=] {\n\t\tauto stars = 0;\n\t\tconst auto process = [&](not_null<PeerListRow*> raw) {\n\t\t\tconst auto row = static_cast<ForbiddenRow*>(raw.get());\n\t\t\tif (row->refreshLock()) {\n\t\t\t\tdelegate()->peerListUpdateRow(raw);\n\t\t\t}\n\t\t\tif (const auto r = row->restriction()) {\n\t\t\t\tstars += r.starsPerMessage;\n\t\t\t}\n\t\t};\n\t\tauto count = delegate()->peerListFullRowsCount();\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tprocess(delegate()->peerListRowAt(i));\n\t\t}\n\t\t_starsToSend = stars;\n\n\t\tcount = delegate()->peerListSearchRowsCount();\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tprocess(delegate()->peerListSearchRowAt(i));\n\t\t}\n\t}, lifetime());\n\n\tif (session().premium()\n\t\t|| (_forbidden.premiumAllowsInvite.empty()\n\t\t\t&& _forbidden.premiumAllowsWrite.empty())) {\n\t\tsetSimpleCover();\n\t} else {\n\t\tsetComplexCover();\n\t}\n\n\tfor (const auto &user : _users) {\n\t\tappendRow(user);\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nbool InviteForbiddenController::canInvite(not_null<PeerData*> peer) const {\n\tconst auto user = peer->asUser();\n\tAssert(user != nullptr);\n\n\treturn _can\n\t\t&& !ranges::contains(_forbidden.premiumAllowsWrite, not_null(user));\n}\n\nvoid InviteForbiddenController::rowClicked(not_null<PeerListRow*> row) {\n\tif (!canInvite(row->peer())) {\n\t\treturn;\n\t}\n\tconst auto checked = row->checked();\n\tdelegate()->peerListSetRowChecked(row, !checked);\n\t_selected = _selected.current() + (checked ? -1 : 1);\n\tconst auto r = static_cast<ForbiddenRow*>(row.get())->restriction();\n\tif (r.starsPerMessage) {\n\t\t_starsToSend = _starsToSend.current()\n\t\t\t+ (checked ? -r.starsPerMessage : r.starsPerMessage);\n\t}\n}\n\nvoid InviteForbiddenController::appendRow(not_null<UserData*> user) {\n\tif (!delegate()->peerListFindRow(user->id.value)) {\n\t\tauto row = createRow(user);\n\t\tconst auto raw = row.get();\n\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\tif (canInvite(user)) {\n\t\t\tdelegate()->peerListSetRowChecked(raw, true);\n\t\t\tif (const auto r = raw->restriction()) {\n\t\t\t\t_starsToSend = _starsToSend.current() + r.starsPerMessage;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid InviteForbiddenController::send(\n\t\tstd::vector<not_null<PeerData*>> list,\n\t\tUi::ShowPtr show,\n\t\tFn<void()> close) {\n\tsend(list, show, close, {});\n}\n\nvoid InviteForbiddenController::send(\n\t\tstd::vector<not_null<PeerData*>> list,\n\t\tUi::ShowPtr show,\n\t\tFn<void()> close,\n\t\tApi::SendOptions options) {\n\tif (list.empty()) {\n\t\treturn;\n\t}\n\t_paymentCheckLifetime.destroy();\n\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = options;\n\t\tcopy.starsApproved = approved;\n\t\tsend(list, show, close, copy);\n\t};\n\tconst auto messagesCount = 1;\n\tconst auto alreadyApproved = options.starsApproved;\n\tauto paid = std::vector<not_null<PeerData*>>();\n\tauto waiting = base::flat_set<not_null<PeerData*>>();\n\tauto totalStars = 0;\n\tfor (const auto &peer : list) {\n\t\tconst auto details = ComputePaymentDetails(peer, messagesCount);\n\t\tif (!details) {\n\t\t\twaiting.emplace(peer);\n\t\t} else if (details->stars > 0) {\n\t\t\ttotalStars += details->stars;\n\t\t\tpaid.push_back(peer);\n\t\t}\n\t}\n\tif (!waiting.empty()) {\n\t\tsession().changes().peerUpdates(\n\t\t\tData::PeerUpdate::Flag::FullInfo\n\t\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\t\tif (waiting.contains(update.peer)) {\n\t\t\t\twithPaymentApproved(alreadyApproved);\n\t\t\t}\n\t\t}, _paymentCheckLifetime);\n\n\t\tif (!session().credits().loaded()) {\n\t\t\tsession().credits().loadedValue(\n\t\t\t) | rpl::filter(\n\t\t\t\trpl::mappers::_1\n\t\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t\twithPaymentApproved(alreadyApproved);\n\t\t\t}, _paymentCheckLifetime);\n\t\t}\n\t\treturn;\n\t} else if (totalStars > alreadyApproved) {\n\t\tconst auto sessionShow = Main::MakeSessionShow(show, &session());\n\t\tShowSendPaidConfirm(sessionShow, paid, SendPaymentDetails{\n\t\t\t.messages = messagesCount,\n\t\t\t.stars = totalStars,\n\t\t}, [=] { withPaymentApproved(totalStars); });\n\t\treturn;\n\t} else if (_sending) {\n\t\treturn;\n\t}\n\t_sending = true;\n\tconst auto chat = _peer->asChat();\n\tconst auto channel = _peer->asChannel();\n\tconst auto sendLink = [=] {\n\t\tconst auto link = chat ? chat->inviteLink() : channel->inviteLink();\n\t\tif (link.isEmpty()) {\n\t\t\treturn false;\n\t\t}\n\t\tauto full = options;\n\t\tauto &api = _peer->session().api();\n\t\tfor (const auto &to : list) {\n\t\t\tauto copy = full;\n\t\t\tcopy.starsApproved = std::min(\n\t\t\t\tto->starsPerMessageChecked(),\n\t\t\t\tfull.starsApproved);\n\t\t\tfull.starsApproved -= copy.starsApproved;\n\n\t\t\tconst auto history = to->owner().history(to);\n\t\t\tauto message = Api::MessageToSend(\n\t\t\t\tApi::SendAction(history, copy));\n\t\t\tmessage.textWithTags = { link };\n\t\t\tmessage.action.clearDraft = false;\n\t\t\tapi.sendMessage(std::move(message));\n\t\t}\n\t\tauto text = (list.size() == 1)\n\t\t\t? tr::lng_via_link_shared_one(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tTextWithEntities{ list.front()->name() },\n\t\t\t\ttr::rich)\n\t\t\t: tr::lng_via_link_shared_many(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tint(list.size()),\n\t\t\t\ttr::rich);\n\t\tclose();\n\t\tshow->showToast(std::move(text));\n\t\treturn true;\n\t};\n\tconst auto sendForFull = [=] {\n\t\tif (!sendLink()) {\n\t\t\t_peer->session().api().inviteLinks().create({\n\t\t\t\t_peer,\n\t\t\t\t[=](auto) {\n\t\t\t\t\tif (!sendLink()) {\n\t\t\t\t\t\tclose();\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t};\n\tif (_peer->isFullLoaded()) {\n\t\tsendForFull();\n\t} else if (!sendLink()) {\n\t\t_peer->session().api().requestFullPeer(_peer);\n\t\t_peer->session().changes().peerUpdates(\n\t\t\t_peer,\n\t\t\tData::PeerUpdate::Flag::FullInfo\n\t\t) | rpl::on_next([=] {\n\t\t\tsendForFull();\n\t\t}, lifetime());\n\t}\n}\n\nstd::unique_ptr<ForbiddenRow> InviteForbiddenController::createRow(\n\t\tnot_null<UserData*> user) const {\n\tconst auto locked = _can && !canInvite(user);\n\tconst auto lockSt = &computeListSt().item;\n\treturn std::make_unique<ForbiddenRow>(user, lockSt, locked);\n}\n\n} // namespace\n\nAddParticipantsBoxController::AddParticipantsBoxController(\n\tnot_null<Main::Session*> session)\n: ContactsBoxController(session) {\n}\n\nAddParticipantsBoxController::AddParticipantsBoxController(\n\tnot_null<PeerData*> peer)\n: AddParticipantsBoxController(\n\tpeer,\n\tGetAlreadyInFromPeer(peer)) {\n}\n\nAddParticipantsBoxController::AddParticipantsBoxController(\n\tnot_null<PeerData*> peer,\n\tbase::flat_set<not_null<UserData*>> &&alreadyIn)\n: ContactsBoxController(&peer->session())\n, _peer(peer)\n, _alreadyIn(std::move(alreadyIn)) {\n\tif (needsInviteLinkButton()) {\n\t\tsetStyleOverrides(&st::peerListWithInviteViaLink);\n\t}\n\tsubscribeToMigration();\n}\n\nvoid AddParticipantsBoxController::subscribeToMigration() {\n\tExpects(_peer != nullptr);\n\n\tSubscribeToMigration(\n\t\t_peer,\n\t\tlifetime(),\n\t\t[=](not_null<ChannelData*> channel) { _peer = channel; });\n}\n\nvoid AddParticipantsBoxController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto moneyRestrictionError = WriteMoneyRestrictionError;\n\tif (RecipientRow::ShowLockedError(this, row, moneyRestrictionError)) {\n\t\treturn;\n\t}\n\tconst auto &serverConfig = session().serverConfig();\n\tauto count = fullCount();\n\tauto limit = _peer && (_peer->isChat() || _peer->isMegagroup())\n\t\t? serverConfig.megagroupSizeMax\n\t\t: serverConfig.chatSizeMax;\n\tif (count < limit || row->checked()) {\n\t\tdelegate()->peerListSetRowChecked(row, !row->checked());\n\t\tupdateTitle();\n\t} else if (const auto channel = _peer ? _peer->asChannel() : nullptr) {\n\t\tif (!_peer->isMegagroup()) {\n\t\t\tshowBox(Box<MaxInviteBox>(channel));\n\t\t}\n\t} else if (count >= serverConfig.chatSizeMax\n\t\t&& count < serverConfig.megagroupSizeMax) {\n\t\tshowBox(Ui::MakeInformBox(tr::lng_profile_add_more_after_create()));\n\t}\n}\n\nvoid AddParticipantsBoxController::itemDeselectedHook(\n\t\tnot_null<PeerData*> peer) {\n\tupdateTitle();\n}\n\nvoid AddParticipantsBoxController::prepareViewHook() {\n\tupdateTitle();\n\n\tTrackMessageMoneyRestrictionsChanges(this, lifetime());\n}\n\nint AddParticipantsBoxController::alreadyInCount() const {\n\tif (!_peer) {\n\t\treturn 1; // self\n\t}\n\tif (const auto chat = _peer->asChat()) {\n\t\treturn qMax(chat->count, 1);\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\treturn qMax(channel->membersCount(), int(_alreadyIn.size()));\n\t}\n\tUnexpected(\"User in AddParticipantsBoxController::alreadyInCount\");\n}\n\nbool AddParticipantsBoxController::isAlreadyIn(\n\t\tnot_null<UserData*> user) const {\n\tif (!_peer) {\n\t\treturn false;\n\t}\n\tif (const auto chat = _peer->asChat()) {\n\t\treturn _alreadyIn.contains(user)\n\t\t\t|| chat->participants.contains(user);\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\treturn _alreadyIn.contains(user)\n\t\t\t|| (channel->isMegagroup()\n\t\t\t\t&& channel->canViewMembers()\n\t\t\t\t&& base::contains(channel->mgInfo->lastParticipants, user));\n\t}\n\tUnexpected(\"User in AddParticipantsBoxController::isAlreadyIn\");\n}\n\nint AddParticipantsBoxController::fullCount() const {\n\treturn alreadyInCount() + delegate()->peerListSelectedRowsCount();\n}\n\nstd::unique_ptr<PeerListRow> AddParticipantsBoxController::createRow(\n\t\tnot_null<UserData*> user) {\n\tif (user->isSelf()) {\n\t\treturn nullptr;\n\t}\n\tconst auto already = isAlreadyIn(user);\n\tconst auto maybeLockedSt = already ? nullptr : &computeListSt().item;\n\tauto result = std::make_unique<RecipientRow>(user, maybeLockedSt);\n\tif (already) {\n\t\tresult->setDisabledState(PeerListRow::State::DisabledChecked);\n\t}\n\treturn result;\n}\n\nvoid AddParticipantsBoxController::updateTitle() {\n\tconst auto additional = (_peer\n\t\t&& _peer->isChannel()\n\t\t&& !_peer->isMegagroup())\n\t\t? QString()\n\t\t: (u\"%1 / %2\"_q\n\t\t).arg(fullCount()\n\t\t).arg(session().serverConfig().megagroupSizeMax);\n\tdelegate()->peerListSetTitle(tr::lng_profile_add_participant());\n\tdelegate()->peerListSetAdditionalTitle(rpl::single(additional));\n\n\taddInviteLinkButton();\n}\n\nbool AddParticipantsBoxController::needsInviteLinkButton() {\n\tif (!_peer) {\n\t\treturn false;\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\treturn channel->canHaveInviteLink();\n\t}\n\treturn _peer->asChat()->canHaveInviteLink();\n}\n\nbase::weak_qptr<Ui::BoxContent> AddParticipantsBoxController::showBox(\n\t\tobject_ptr<Ui::BoxContent> box) const {\n\tconst auto weak = base::make_weak(box.data());\n\tdelegate()->peerListUiShow()->showBox(std::move(box));\n\treturn weak;\n}\n\nvoid AddParticipantsBoxController::addInviteLinkButton() {\n\tif (!needsInviteLinkButton()) {\n\t\treturn;\n\t}\n\tauto button = object_ptr<Ui::PaddingWrap<Ui::SettingsButton>>(\n\t\tnullptr,\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\tnullptr,\n\t\t\ttr::lng_profile_add_via_link(),\n\t\t\tst::inviteViaLinkButton),\n\t\tstyle::margins(0, st::membersMarginTop, 0, 0));\n\n\tconst auto icon = Ui::CreateChild<Info::Profile::FloatingIcon>(\n\t\tbutton->entity(),\n\t\tst::inviteViaLinkIcon,\n\t\tQPoint());\n\tbutton->entity()->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\ticon->moveToLeft(\n\t\t\tst::inviteViaLinkIconPosition.x(),\n\t\t\t(height - st::inviteViaLinkIcon.height()) / 2);\n\t}, icon->lifetime());\n\n\tbutton->entity()->setClickedCallback([=] {\n\t\tshowBox(Box<EditPeerTypeBox>(_peer));\n\t});\n\tbutton->entity()->events(\n\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\treturn (e->type() == QEvent::Enter);\n\t}) | rpl::on_next([=] {\n\t\tdelegate()->peerListMouseLeftGeometry();\n\t}, button->lifetime());\n\tdelegate()->peerListSetAboveWidget(std::move(button));\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid AddParticipantsBoxController::inviteSelectedUsers(\n\t\tnot_null<PeerListBox*> box,\n\t\tFn<void()> done) const {\n\tExpects(_peer != nullptr);\n\n\tconst auto rows = box->collectSelectedRows();\n\tconst auto users = ranges::views::all(\n\t\trows\n\t) | ranges::views::transform([](not_null<PeerData*> peer) {\n\t\tExpects(peer->isUser());\n\t\tExpects(!peer->isSelf());\n\n\t\treturn not_null<UserData*>(peer->asUser());\n\t}) | ranges::to_vector;\n\tif (users.empty()) {\n\t\treturn;\n\t}\n\tconst auto show = box->uiShow();\n\tconst auto request = [=](bool checked) {\n\t\t_peer->session().api().chatParticipants().add(\n\t\t\tshow,\n\t\t\t_peer,\n\t\t\tusers,\n\t\t\tchecked);\n\t};\n\tif (_peer->isChannel()) {\n\t\trequest(false);\n\t\treturn done();\n\t}\n\tshow->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tauto checkbox = object_ptr<Ui::Checkbox>(\n\t\t\tbox.get(),\n\t\t\ttr::lng_participant_invite_history(),\n\t\t\ttrue,\n\t\t\tst::defaultBoxCheckbox);\n\t\tconst auto weak = base::make_weak(checkbox.data());\n\n\t\tauto text = (users.size() == 1)\n\t\t\t? tr::lng_participant_invite_sure(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\t{ users.front()->name()},\n\t\t\t\tlt_group,\n\t\t\t\t{ _peer->name()},\n\t\t\t\ttr::rich)\n\t\t\t: tr::lng_participant_invite_sure_many(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tint(users.size()),\n\t\t\t\tlt_group,\n\t\t\t\t{ _peer->name() },\n\t\t\t\ttr::rich);\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = std::move(text),\n\t\t\t.confirmed = crl::guard(weak, [=](Fn<void()> &&close) {\n\t\t\t\trequest(weak->checked());\n\t\t\t\tdone();\n\t\t\t\tclose();\n\t\t\t}),\n\t\t\t.confirmText = tr::lng_participant_invite(),\n\t\t});\n\n\t\tauto padding = st::boxPadding;\n\t\tpadding.setTop(padding.bottom());\n\t\tbox->addRow(std::move(checkbox), std::move(padding));\n\t}));\n}\n\nvoid AddParticipantsBoxController::Start(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChatData*> chat) {\n\tauto controller = std::make_unique<AddParticipantsBoxController>(chat);\n\tconst auto weak = controller.get();\n\tconst auto parent = navigation->parentController();\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tbox->addButton(tr::lng_participant_invite(), [=] {\n\t\t\tweak->inviteSelectedUsers(box, [=] {\n\t\t\t\tparent->showPeerHistory(\n\t\t\t\t\tchat,\n\t\t\t\t\tWindow::SectionShow::Way::ClearStack,\n\t\t\t\t\tShowAtTheEndMsgId);\n\t\t\t});\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t};\n\tparent->show(\n\t\tBox<PeerListBox>(std::move(controller), std::move(initBox)));\n}\n\nvoid AddParticipantsBoxController::Start(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChannelData*> channel,\n\t\tbase::flat_set<not_null<UserData*>> &&alreadyIn,\n\t\tbool justCreated) {\n\tauto controller = std::make_unique<AddParticipantsBoxController>(\n\t\tchannel,\n\t\tstd::move(alreadyIn));\n\tconst auto weak = controller.get();\n\tconst auto parent = navigation->parentController();\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tbox->addButton(tr::lng_participant_invite(), [=] {\n\t\t\tweak->inviteSelectedUsers(box, [=] {\n\t\t\t\tif (channel->isMegagroup()) {\n\t\t\t\t\tparent->showPeerHistory(\n\t\t\t\t\t\tchannel,\n\t\t\t\t\t\tWindow::SectionShow::Way::ClearStack,\n\t\t\t\t\t\tShowAtTheEndMsgId);\n\t\t\t\t} else {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t\tbox->addButton(\n\t\t\tjustCreated ? tr::lng_create_group_skip() : tr::lng_cancel(),\n\t\t\t[=] { box->closeBox(); });\n\t\tif (justCreated) {\n\t\t\tconst auto weak = base::make_weak(parent);\n\t\t\tbox->boxClosing() | rpl::on_next([=] {\n\t\t\t\tauto params = Window::SectionShow();\n\t\t\t\tparams.activation = anim::activation::background;\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->showPeerHistory(\n\t\t\t\t\t\tchannel,\n\t\t\t\t\t\tparams,\n\t\t\t\t\t\tShowAtTheEndMsgId);\n\t\t\t\t\tchannel->owner().addRecentJoinChat({\n\t\t\t\t\t\t.fromPeerId = channel->id,\n\t\t\t\t\t\t.joinedPeerId = channel->id,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}, box->lifetime());\n\t\t}\n\t};\n\tparent->show(\n\t\tBox<PeerListBox>(std::move(controller), std::move(initBox)));\n}\n\nvoid AddParticipantsBoxController::Start(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChannelData*> channel,\n\t\tbase::flat_set<not_null<UserData*>> &&alreadyIn) {\n\tStart(navigation, channel, std::move(alreadyIn), false);\n}\n\nvoid AddParticipantsBoxController::Start(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChannelData*> channel) {\n\tStart(navigation, channel, {}, true);\n}\n\nForbiddenInvites CollectForbiddenUsers(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPmessages_InvitedUsers &result) {\n\tconst auto &data = result.data();\n\tconst auto owner = &session->data();\n\tauto forbidden = ForbiddenInvites();\n\tfor (const auto &missing : data.vmissing_invitees().v) {\n\t\tconst auto &data = missing.data();\n\t\tconst auto user = owner->userLoaded(data.vuser_id());\n\t\tif (user) {\n\t\t\tforbidden.users.push_back(user);\n\t\t\tif (data.is_premium_would_allow_invite()) {\n\t\t\t\tforbidden.premiumAllowsInvite.push_back(user);\n\t\t\t}\n\t\t\tif (data.is_premium_required_for_pm()) {\n\t\t\t\tforbidden.premiumAllowsWrite.push_back(user);\n\t\t\t}\n\t\t}\n\t}\n\treturn forbidden;\n}\n\nbool ChatInviteForbidden(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tForbiddenInvites forbidden) {\n\tif (forbidden.empty() || !show || !show->valid()) {\n\t\treturn false;\n\t} else if (forbidden.users.size() <= kUserpicsLimit\n\t\t&& (forbidden.premiumAllowsWrite.size()\n\t\t\t== forbidden.users.size())) {\n\t\tshow->show(Box(SimpleForbiddenBox, peer, forbidden));\n\t\treturn true;\n\t}\n\tauto controller = std::make_unique<InviteForbiddenController>(\n\t\tpeer,\n\t\tstd::move(forbidden));\n\tconst auto weak = controller.get();\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tconst auto can = weak->canInvite();\n\t\tif (!can) {\n\t\t\tbox->addButton(tr::lng_close(), [=] {\n\t\t\t\tbox->closeBox();\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tweak->selectedValue(\n\t\t) | rpl::map(\n\t\t\trpl::mappers::_1 > 0\n\t\t) | rpl::distinct_until_changed(\n\t\t) | rpl::on_next([=](bool has) {\n\t\t\tbox->clearButtons();\n\t\t\tif (has) {\n\t\t\t\tconst auto send = box->addButton(tr::lng_via_link_send(), [=] {\n\t\t\t\t\tweak->send(\n\t\t\t\t\t\tbox->collectSelectedRows(),\n\t\t\t\t\t\tbox->uiShow(),\n\t\t\t\t\t\tcrl::guard(box, [=] { box->closeBox(); }));\n\t\t\t\t});\n\t\t\t\tsend->setText(PaidSendButtonText(\n\t\t\t\t\tweak->starsToSend(),\n\t\t\t\t\ttr::lng_via_link_send()));\n\t\t\t}\n\t\t\tbox->addButton(tr::lng_create_group_skip(), [=] {\n\t\t\t\tbox->closeBox();\n\t\t\t});\n\t\t}, box->lifetime());\n\n\t\tData::AmPremiumValue(\n\t\t\t&peer->session()\n\t\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\t\tbox->closeBox();\n\t\t}, box->lifetime());\n\t};\n\tshow->showBox(\n\t\tBox<PeerListBox>(std::move(controller), std::move(initBox)));\n\treturn true;\n}\n\nAddSpecialBoxController::AddSpecialBoxController(\n\tnot_null<PeerData*> peer,\n\tRole role,\n\tAdminDoneCallback adminDoneCallback,\n\tBannedDoneCallback bannedDoneCallback)\n: PeerListController(std::make_unique<AddSpecialBoxSearchController>(\n\tpeer,\n\t&_additional))\n, _peer(peer)\n, _api(&_peer->session().mtp())\n, _role(role)\n, _additional(peer, Role::Members)\n, _adminDoneCallback(std::move(adminDoneCallback))\n, _bannedDoneCallback(std::move(bannedDoneCallback)) {\n\tsubscribeToMigration();\n}\n\nMain::Session &AddSpecialBoxController::session() const {\n\treturn _peer->session();\n}\n\nvoid AddSpecialBoxController::subscribeToMigration() {\n\tconst auto chat = _peer->asChat();\n\tif (!chat) {\n\t\treturn;\n\t}\n\tSubscribeToMigration(\n\t\tchat,\n\t\tlifetime(),\n\t\t[=](not_null<ChannelData*> channel) { migrate(chat, channel); });\n}\n\nvoid AddSpecialBoxController::migrate(\n\t\tnot_null<ChatData*> chat,\n\t\tnot_null<ChannelData*> channel) {\n\t_peer = channel;\n\t_additional.migrate(chat, channel);\n}\n\nbase::weak_qptr<Ui::BoxContent> AddSpecialBoxController::showBox(\n\t\tobject_ptr<Ui::BoxContent> box) const {\n\tconst auto weak = base::make_weak(box.data());\n\tdelegate()->peerListUiShow()->showBox(std::move(box));\n\treturn weak;\n}\n\nstd::unique_ptr<PeerListRow> AddSpecialBoxController::createSearchRow(\n\t\tnot_null<PeerData*> peer) {\n\tif (_excludeSelf && peer->isSelf()) {\n\t\treturn nullptr;\n\t}\n\tif (const auto user = peer->asUser()) {\n\t\treturn createRow(user);\n\t}\n\treturn nullptr;\n}\n\nvoid AddSpecialBoxController::prepare() {\n\tdelegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);\n\tauto title = [&] {\n\t\tswitch (_role) {\n\t\tcase Role::Members:\n\t\t\treturn tr::lng_profile_participants_section();\n\t\tcase Role::Admins:\n\t\t\treturn tr::lng_channel_add_admin();\n\t\tcase Role::Restricted:\n\t\t\treturn tr::lng_channel_add_exception();\n\t\tcase Role::Kicked:\n\t\t\treturn tr::lng_channel_add_removed();\n\t\t}\n\t\tUnexpected(\"Role in AddSpecialBoxController::prepare()\");\n\t}();\n\tdelegate()->peerListSetTitle(std::move(title));\n\tsetDescriptionText(tr::lng_contacts_loading(tr::now));\n\tsetSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now));\n\n\tif (const auto chat = _peer->asChat()) {\n\t\tprepareChatRows(chat);\n\t} else {\n\t\tloadMoreRows();\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid AddSpecialBoxController::prepareChatRows(not_null<ChatData*> chat) {\n\t_onlineSorter = std::make_unique<ParticipantsOnlineSorter>(\n\t\tchat,\n\t\tdelegate());\n\n\trebuildChatRows(chat);\n\tif (!delegate()->peerListFullRowsCount()) {\n\t\tchat->updateFullForced();\n\t}\n\n\tusing UpdateFlag = Data::PeerUpdate::Flag;\n\tchat->session().changes().peerUpdates(\n\t\tchat,\n\t\tUpdateFlag::Members | UpdateFlag::Admins\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\t_additional.fillFromPeer();\n\t\tif (update.flags & UpdateFlag::Members) {\n\t\t\trebuildChatRows(chat);\n\t\t}\n\t}, lifetime());\n}\n\nvoid AddSpecialBoxController::rebuildChatRows(not_null<ChatData*> chat) {\n\tif (chat->participants.empty()) {\n\t\t// We get such updates often\n\t\t// (when participants list was invalidated).\n\t\t//while (delegate()->peerListFullRowsCount() > 0) {\n\t\t//\tdelegate()->peerListRemoveRow(\n\t\t//\t\tdelegate()->peerListRowAt(0));\n\t\t//}\n\t\treturn;\n\t}\n\n\tauto &participants = chat->participants;\n\tauto count = delegate()->peerListFullRowsCount();\n\tfor (auto i = 0; i != count;) {\n\t\tauto row = delegate()->peerListRowAt(i);\n\t\tAssert(row->peer()->isUser());\n\t\tauto user = row->peer()->asUser();\n\t\tif (participants.contains(user)) {\n\t\t\t++i;\n\t\t} else {\n\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t\t--count;\n\t\t}\n\t}\n\tfor (const auto &user : participants) {\n\t\tif (auto row = createRow(user)) {\n\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t}\n\t}\n\t_onlineSorter->sort();\n\n\tdelegate()->peerListRefreshRows();\n\tsetDescriptionText(QString());\n}\n\nvoid AddSpecialBoxController::loadMoreRows() {\n\tif (searchController() && searchController()->loadMoreRows()) {\n\t\treturn;\n\t} else if (!_peer->isChannel() || _loadRequestId || _allLoaded) {\n\t\treturn;\n\t}\n\n\t// First query is small and fast, next loads a lot of rows.\n\tconst auto perPage = (_offset > 0)\n\t\t? kParticipantsPerPage\n\t\t: kParticipantsFirstPageCount;\n\tconst auto participantsHash = uint64(0);\n\tconst auto channel = _peer->asChannel();\n\n\t_loadRequestId = _api.request(MTPchannels_GetParticipants(\n\t\tchannel->inputChannel(),\n\t\tMTP_channelParticipantsRecent(),\n\t\tMTP_int(_offset),\n\t\tMTP_int(perPage),\n\t\tMTP_long(participantsHash)\n\t)).done([=](const MTPchannels_ChannelParticipants &result) {\n\t\t_loadRequestId = 0;\n\t\tresult.match([&](const MTPDchannels_channelParticipants &data) {\n\t\t\tconst auto &[availableCount, list] = Api::ChatParticipants::Parse(\n\t\t\t\tchannel,\n\t\t\t\tdata);\n\t\t\tfor (const auto &data : list) {\n\t\t\t\tif (const auto participant = _additional.applyParticipant(\n\t\t\t\t\t\tdata)) {\n\t\t\t\t\tappendRow(participant);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (const auto size = list.size()) {\n\t\t\t\t_offset += size;\n\t\t\t} else {\n\t\t\t\t// To be sure - wait for a whole empty result list.\n\t\t\t\t_allLoaded = true;\n\t\t\t}\n\t\t}, [&](const MTPDchannels_channelParticipantsNotModified &) {\n\t\t\tLOG((\"API Error: channels.channelParticipantsNotModified received!\"));\n\t\t});\n\t\tif (delegate()->peerListFullRowsCount() > 0) {\n\t\t\tsetDescriptionText(QString());\n\t\t} else if (_allLoaded) {\n\t\t\tsetDescriptionText(tr::lng_blocked_list_not_found(tr::now));\n\t\t}\n\t\tdelegate()->peerListRefreshRows();\n\t}).fail([this] {\n\t\t_loadRequestId = 0;\n\t}).send();\n}\n\nvoid AddSpecialBoxController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto participant = row->peer();\n\tconst auto user = participant->asUser();\n\tswitch (_role) {\n\tcase Role::Admins:\n\t\tAssert(user != nullptr);\n\t\treturn showAdmin(user);\n\tcase Role::Restricted:\n\t\tAssert(user != nullptr);\n\t\treturn showRestricted(user);\n\tcase Role::Kicked: return kickUser(participant);\n\t}\n\tUnexpected(\"Role in AddSpecialBoxController::rowClicked()\");\n}\n\ntemplate <typename Callback>\nbool AddSpecialBoxController::checkInfoLoaded(\n\t\tnot_null<PeerData*> participant,\n\t\tCallback callback) {\n\tif (_additional.infoLoaded(participant)) {\n\t\treturn true;\n\t}\n\n\t// We don't know what this user status is in the group.\n\tconst auto channel = _peer->asChannel();\n\t_api.request(MTPchannels_GetParticipant(\n\t\tchannel->inputChannel(),\n\t\tparticipant->input()\n\t)).done([=](const MTPchannels_ChannelParticipant &result) {\n\t\tresult.match([&](const MTPDchannels_channelParticipant &data) {\n\t\t\tchannel->owner().processUsers(data.vusers());\n\t\t\t_additional.applyParticipant(\n\t\t\t\tApi::ChatParticipant(data.vparticipant(), channel));\n\t\t});\n\t\tcallback();\n\t}).fail([=] {\n\t\t_additional.setExternal(participant);\n\t\tcallback();\n\t}).send();\n\treturn false;\n}\n\nvoid AddSpecialBoxController::showAdmin(\n\t\tnot_null<UserData*> user,\n\t\tbool sure) {\n\tif (!checkInfoLoaded(user, [=] { showAdmin(user); })) {\n\t\treturn;\n\t}\n\t_editBox = nullptr;\n\tif (_editParticipantBox) {\n\t\t_editParticipantBox->closeBox();\n\t}\n\n\tconst auto chat = _peer->asChat();\n\tconst auto channel = _peer->asChannel();\n\tconst auto showAdminSure = crl::guard(this, [=] {\n\t\tshowAdmin(user, true);\n\t});\n\n\t// Check restrictions.\n\tconst auto canAddMembers = chat\n\t\t? chat->canAddMembers()\n\t\t: channel->canAddMembers();\n\tconst auto canBanMembers = chat\n\t\t? chat->canBanMembers()\n\t\t: channel->canBanMembers();\n\tconst auto adminRights = _additional.adminRights(user);\n\tif (adminRights.has_value()) {\n\t\t// The user is already an admin.\n\t} else if (_additional.isKicked(user)) {\n\t\t// The user is banned.\n\t\tif (canAddMembers) {\n\t\t\tif (canBanMembers) {\n\t\t\t\tif (!sure) {\n\t\t\t\t\t_editBox = showBox(\n\t\t\t\t\t\tUi::MakeConfirmBox({\n\t\t\t\t\t\t\ttr::lng_sure_add_admin_unremove(),\n\t\t\t\t\t\t\tshowAdminSure\n\t\t\t\t\t\t}));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tshowBox(\n\t\t\t\t\tUi::MakeInformBox(tr::lng_error_cant_add_admin_unban()));\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else {\n\t\t\tshowBox(Ui::MakeInformBox(tr::lng_error_cant_add_admin_invite()));\n\t\t\treturn;\n\t\t}\n\t} else if (_additional.restrictedRights(user).has_value()) {\n\t\t// The user is restricted.\n\t\tif (canBanMembers) {\n\t\t\tif (!sure) {\n\t\t\t\t_editBox = showBox(\n\t\t\t\t\tUi::MakeConfirmBox({\n\t\t\t\t\t\ttr::lng_sure_add_admin_unremove(),\n\t\t\t\t\t\tshowAdminSure\n\t\t\t\t\t}));\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else {\n\t\t\tshowBox(Ui::MakeInformBox(tr::lng_error_cant_add_admin_unban()));\n\t\t\treturn;\n\t\t}\n\t} else if (_additional.isExternal(user)) {\n\t\t// The user is not in the group yet.\n\t\tif (canAddMembers) {\n\t\t\tif (!sure) {\n\t\t\t\tauto text = ((_peer->isChat() || _peer->isMegagroup())\n\t\t\t\t\t? tr::lng_sure_add_admin_invite\n\t\t\t\t\t: tr::lng_sure_add_admin_invite_channel)();\n\t\t\t\t_editBox = showBox(\n\t\t\t\t\tUi::MakeConfirmBox({\n\t\t\t\t\t\tstd::move(text),\n\t\t\t\t\t\tshowAdminSure\n\t\t\t\t\t}));\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else {\n\t\t\tshowBox(Ui::MakeInformBox(tr::lng_error_cant_add_admin_invite()));\n\t\t\treturn;\n\t\t}\n\t}\n\n\t// Finally show the admin.\n\tconst auto currentRights = adminRights\n\t\t? *adminRights\n\t\t: ChatAdminRightsInfo();\n\tauto box = Box<EditAdminBox>(\n\t\t_peer,\n\t\tuser,\n\t\tcurrentRights,\n\t\t_additional.memberRank(user),\n\t\t_additional.adminPromotedSince(user),\n\t\t_additional.adminPromotedBy(user));\n\tconst auto show = delegate()->peerListUiShow();\n\tif (_additional.canAddOrEditAdmin(user)) {\n\t\tconst auto done = crl::guard(this, [=](\n\t\t\t\tChatAdminRightsInfo newRights,\n\t\t\t\tconst std::optional<QString> &rank) {\n\t\t\teditAdminDone(user, newRights, rank);\n\t\t});\n\t\tconst auto fail = crl::guard(this, [=] {\n\t\t\tif (_editParticipantBox) {\n\t\t\t\t_editParticipantBox->closeBox();\n\t\t\t}\n\t\t});\n\t\tbox->setSaveCallback(\n\t\t\tSaveAdminCallback(show, _peer, user, done, fail));\n\t}\n\t_editParticipantBox = showBox(std::move(box));\n}\n\nvoid AddSpecialBoxController::editAdminDone(\n\t\tnot_null<UserData*> user,\n\t\tChatAdminRightsInfo rights,\n\t\tconst std::optional<QString> &rank) {\n\tif (_editParticipantBox) {\n\t\t_editParticipantBox->closeBox();\n\t}\n\n\t_additional.applyAdminLocally(\n\t\tuser,\n\t\trights,\n\t\trank.value_or(_additional.memberRank(user)));\n\t// _adminDoneCallback should call changes().chatAdminUpdated.\n\tif (const auto callback = _adminDoneCallback) {\n\t\tcallback(user, rights, rank);\n\t}\n}\n\nvoid AddSpecialBoxController::showRestricted(\n\t\tnot_null<UserData*> user,\n\t\tbool sure) {\n\tif (!checkInfoLoaded(user, [=] { showRestricted(user); })) {\n\t\treturn;\n\t}\n\t_editBox = nullptr;\n\tif (_editParticipantBox) {\n\t\t_editParticipantBox->closeBox();\n\t}\n\n\tconst auto showRestrictedSure = crl::guard(this, [=] {\n\t\tshowRestricted(user, true);\n\t});\n\n\t// Check restrictions.\n\tconst auto restrictedRights = _additional.restrictedRights(user);\n\tif (restrictedRights.has_value()) {\n\t\t// The user is already banned or restricted.\n\t} else if (_additional.adminRights(user).has_value()\n\t\t|| _additional.isCreator(user)) {\n\t\t// The user is an admin or creator.\n\t\tif (!_additional.isCreator(user) && _additional.canEditAdmin(user)) {\n\t\t\tif (!sure) {\n\t\t\t\t_editBox = showBox(\n\t\t\t\t\tUi::MakeConfirmBox({\n\t\t\t\t\t\ttr::lng_sure_ban_admin(),\n\t\t\t\t\t\tshowRestrictedSure\n\t\t\t\t\t}));\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else {\n\t\t\tshowBox(Ui::MakeInformBox(tr::lng_error_cant_ban_admin()));\n\t\t\treturn;\n\t\t}\n\t}\n\n\t// Finally edit the restricted.\n\tconst auto currentRights = restrictedRights\n\t\t? *restrictedRights\n\t\t: ChatRestrictionsInfo();\n\tauto box = Box<EditRestrictedBox>(\n\t\t_peer,\n\t\tuser,\n\t\t_additional.adminRights(user).has_value(),\n\t\tcurrentRights,\n\t\t_additional.memberRank(user),\n\t\t_additional.restrictedBy(user),\n\t\t_additional.restrictedSince(user));\n\tif (_additional.canRestrictParticipant(user)) {\n\t\tconst auto done = crl::guard(this, [=](\n\t\t\t\tChatRestrictionsInfo newRights) {\n\t\t\teditRestrictedDone(user, newRights);\n\t\t});\n\t\tconst auto fail = crl::guard(this, [=] {\n\t\t\tif (_editParticipantBox) {\n\t\t\t\t_editParticipantBox->closeBox();\n\t\t\t}\n\t\t});\n\t\tconst auto show = delegate()->peerListUiShow();\n\t\tbox->setSaveCallback(\n\t\t\tSaveRestrictedCallback(show, _peer, user, done, fail));\n\t}\n\t_editParticipantBox = showBox(std::move(box));\n}\n\nvoid AddSpecialBoxController::editRestrictedDone(\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo rights) {\n\tif (_editParticipantBox) {\n\t\t_editParticipantBox->closeBox();\n\t}\n\n\t_additional.applyBannedLocally(participant, rights);\n\tif (const auto callback = _bannedDoneCallback) {\n\t\tcallback(participant, rights);\n\t}\n}\n\nvoid AddSpecialBoxController::kickUser(\n\t\tnot_null<PeerData*> participant,\n\t\tbool sure) {\n\tif (!checkInfoLoaded(participant, [=] { kickUser(participant); })) {\n\t\treturn;\n\t}\n\n\tconst auto kickUserSure = crl::guard(this, [=] {\n\t\tkickUser(participant, true);\n\t});\n\n\t// Check restrictions.\n\tconst auto user = participant->asUser();\n\tif (user && (_additional.adminRights(user).has_value()\n\t\t|| (_additional.isCreator(user)))) {\n\t\t// The user is an admin or creator.\n\t\tif (!_additional.isCreator(user) && _additional.canEditAdmin(user)) {\n\t\t\tif (!sure) {\n\t\t\t\t_editBox = showBox(\n\t\t\t\t\tUi::MakeConfirmBox({\n\t\t\t\t\t\ttr::lng_sure_ban_admin(),\n\t\t\t\t\t\tkickUserSure\n\t\t\t\t\t}));\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else {\n\t\t\tshowBox(Ui::MakeInformBox(tr::lng_error_cant_ban_admin()));\n\t\t\treturn;\n\t\t}\n\t}\n\n\t// Finally kick him.\n\tif (!sure) {\n\t\tconst auto text = ((_peer->isChat() || _peer->isMegagroup())\n\t\t\t? tr::lng_profile_sure_kick\n\t\t\t: tr::lng_profile_sure_kick_channel)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tparticipant->name());\n\t\t_editBox = showBox(Ui::MakeConfirmBox({ text, kickUserSure }));\n\t\treturn;\n\t}\n\n\tconst auto restrictedRights = _additional.restrictedRights(participant);\n\tconst auto currentRights = restrictedRights\n\t\t? *restrictedRights\n\t\t: ChatRestrictionsInfo();\n\n\tconst auto done = crl::guard(this, [=](\n\t\t\tChatRestrictionsInfo newRights) {\n\t\teditRestrictedDone(participant, newRights);\n\t});\n\tconst auto fail = crl::guard(this, [=] {\n\t\t_editBox = nullptr;\n\t});\n\tconst auto show = delegate()->peerListUiShow();\n\tconst auto callback = SaveRestrictedCallback(\n\t\tshow,\n\t\t_peer,\n\t\tparticipant,\n\t\tdone,\n\t\tfail);\n\tcallback(currentRights, ChannelData::KickedRestrictedRights(participant));\n}\n\nbool AddSpecialBoxController::appendRow(not_null<PeerData*> participant) {\n\tif (delegate()->peerListFindRow(participant->id.value)\n\t\t|| (_excludeSelf && participant->isSelf())) {\n\t\treturn false;\n\t}\n\tdelegate()->peerListAppendRow(createRow(participant));\n\treturn true;\n}\n\nbool AddSpecialBoxController::prependRow(not_null<UserData*> user) {\n\tif (delegate()->peerListFindRow(user->id.value)) {\n\t\treturn false;\n\t}\n\tdelegate()->peerListPrependRow(createRow(user));\n\treturn true;\n}\n\nstd::unique_ptr<PeerListRow> AddSpecialBoxController::createRow(\n\t\tnot_null<PeerData*> participant) const {\n\treturn std::make_unique<PeerListRow>(participant);\n}\n\nAddSpecialBoxSearchController::AddSpecialBoxSearchController(\n\tnot_null<PeerData*> peer,\n\tnot_null<ParticipantsAdditionalData*> additional)\n: _peer(peer)\n, _additional(additional)\n, _api(&_peer->session().mtp())\n, _timer([=] { searchOnServer(); }) {\n\tsubscribeToMigration();\n}\n\nvoid AddSpecialBoxSearchController::subscribeToMigration() {\n\tSubscribeToMigration(\n\t\t_peer,\n\t\tlifetime(),\n\t\t[=](not_null<ChannelData*> channel) { _peer = channel; });\n}\n\nvoid AddSpecialBoxSearchController::searchQuery(const QString &query) {\n\tif (_query != query) {\n\t\t_query = query;\n\t\t_offset = 0;\n\t\t_requestId = 0;\n\t\t_participantsLoaded = false;\n\t\t_chatsContactsAdded = false;\n\t\t_chatMembersAdded = false;\n\t\t_globalLoaded = false;\n\t\tif (!_query.isEmpty() && !searchParticipantsInCache()) {\n\t\t\t_timer.callOnce(AutoSearchTimeout);\n\t\t} else {\n\t\t\t_timer.cancel();\n\t\t}\n\t}\n}\n\nvoid AddSpecialBoxSearchController::searchOnServer() {\n\tExpects(!_query.isEmpty());\n\n\tloadMoreRows();\n}\n\nbool AddSpecialBoxSearchController::isLoading() {\n\treturn _timer.isActive() || _requestId;\n}\n\nbool AddSpecialBoxSearchController::searchParticipantsInCache() {\n\tconst auto i = _participantsCache.find(_query);\n\tif (i != _participantsCache.cend()) {\n\t\t_requestId = 0;\n\t\tsearchParticipantsDone(\n\t\t\t_requestId,\n\t\t\ti->second.result,\n\t\t\ti->second.requestedCount);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool AddSpecialBoxSearchController::searchGlobalInCache() {\n\tauto it = _globalCache.find(_query);\n\tif (it != _globalCache.cend()) {\n\t\t_requestId = 0;\n\t\tsearchGlobalDone(_requestId, it->second);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool AddSpecialBoxSearchController::loadMoreRows() {\n\tif (_query.isEmpty()) {\n\t\treturn false;\n\t}\n\tif (_globalLoaded) {\n\t\treturn true;\n\t}\n\tif (_participantsLoaded || _chatMembersAdded) {\n\t\tif (!_chatsContactsAdded) {\n\t\t\taddChatsContacts();\n\t\t}\n\t\tif (!isLoading() && !searchGlobalInCache()) {\n\t\t\trequestGlobal();\n\t\t}\n\t} else if (const auto chat = _peer->asChat()) {\n\t\tif (!_chatMembersAdded) {\n\t\t\taddChatMembers(chat);\n\t\t}\n\t} else if (!isLoading()) {\n\t\trequestParticipants();\n\t}\n\treturn true;\n}\n\nvoid AddSpecialBoxSearchController::requestParticipants() {\n\tExpects(_peer->isChannel());\n\n\t// For search we request a lot of rows from the first query.\n\t// (because we've waited for search request by timer already,\n\t// so we don't expect it to be fast, but we want to fill cache).\n\tconst auto perPage = kParticipantsPerPage;\n\tconst auto participantsHash = uint64(0);\n\tconst auto channel = _peer->asChannel();\n\n\t_requestId = _api.request(MTPchannels_GetParticipants(\n\t\tchannel->inputChannel(),\n\t\tMTP_channelParticipantsSearch(MTP_string(_query)),\n\t\tMTP_int(_offset),\n\t\tMTP_int(perPage),\n\t\tMTP_long(participantsHash)\n\t)).done([=](\n\t\t\tconst MTPchannels_ChannelParticipants &result,\n\t\t\tmtpRequestId requestId) {\n\t\tsearchParticipantsDone(requestId, result, perPage);\n\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\tif (_requestId == requestId) {\n\t\t\t_requestId = 0;\n\t\t\t_participantsLoaded = true;\n\t\t\tloadMoreRows();\n\t\t\tdelegate()->peerListSearchRefreshRows();\n\t\t}\n\t}).send();\n\n\tauto entry = Query();\n\tentry.text = _query;\n\tentry.offset = _offset;\n\t_participantsQueries.emplace(_requestId, entry);\n}\n\nvoid AddSpecialBoxSearchController::searchParticipantsDone(\n\t\tmtpRequestId requestId,\n\t\tconst MTPchannels_ChannelParticipants &result,\n\t\tint requestedCount) {\n\tExpects(_peer->isChannel());\n\n\tconst auto channel = _peer->asChannel();\n\tauto query = _query;\n\tif (requestId) {\n\t\tconst auto addToCache = [&] {\n\t\t\tauto it = _participantsQueries.find(requestId);\n\t\t\tif (it != _participantsQueries.cend()) {\n\t\t\t\tquery = it->second.text;\n\t\t\t\tif (it->second.offset == 0) {\n\t\t\t\t\tauto &entry = _participantsCache[query];\n\t\t\t\t\tentry.result = result;\n\t\t\t\t\tentry.requestedCount = requestedCount;\n\t\t\t\t}\n\t\t\t\t_participantsQueries.erase(it);\n\t\t\t}\n\t\t};\n\t\tresult.match([&](const MTPDchannels_channelParticipants &data) {\n\t\t\tApi::ChatParticipants::Parse(channel, data);\n\t\t\taddToCache();\n\t\t}, [&](const MTPDchannels_channelParticipantsNotModified &) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"channels.channelParticipantsNotModified received!\"));\n\t\t});\n\t}\n\n\tif (_requestId != requestId) {\n\t\treturn;\n\t}\n\t_requestId = 0;\n\tresult.match([&](const MTPDchannels_channelParticipants &data) {\n\t\tconst auto &list = data.vparticipants().v;\n\t\tif (list.size() < requestedCount) {\n\t\t\t// We want cache to have full information about a query with\n\t\t\t// small results count (that we don't need the second request).\n\t\t\t// So we don't wait for empty list unlike the non-search case.\n\t\t\t_participantsLoaded = true;\n\t\t\tif (list.empty() && _offset == 0) {\n\t\t\t\t// No results, request global search immediately.\n\t\t\t\tloadMoreRows();\n\t\t\t}\n\t\t}\n\t\tfor (const auto &data : list) {\n\t\t\tif (const auto user = _additional->applyParticipant(\n\t\t\t\t\tApi::ChatParticipant(data, channel))) {\n\t\t\t\tdelegate()->peerListSearchAddRow(user);\n\t\t\t}\n\t\t}\n\t\t_offset += list.size();\n\t}, [&](const MTPDchannels_channelParticipantsNotModified &) {\n\t\t_participantsLoaded = true;\n\t});\n\n\tdelegate()->peerListSearchRefreshRows();\n}\n\nvoid AddSpecialBoxSearchController::requestGlobal() {\n\tif (_query.isEmpty()) {\n\t\t_globalLoaded = true;\n\t\treturn;\n\t}\n\n\tauto perPage = SearchPeopleLimit;\n\t_requestId = _api.request(MTPcontacts_Search(\n\t\tMTP_string(_query),\n\t\tMTP_int(perPage)\n\t)).done([=](const MTPcontacts_Found &result, mtpRequestId requestId) {\n\t\tsearchGlobalDone(requestId, result);\n\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\tif (_requestId == requestId) {\n\t\t\t_requestId = 0;\n\t\t\t_globalLoaded = true;\n\t\t\tdelegate()->peerListSearchRefreshRows();\n\t\t}\n\t}).send();\n\t_globalQueries.emplace(_requestId, _query);\n}\n\nvoid AddSpecialBoxSearchController::searchGlobalDone(\n\t\tmtpRequestId requestId,\n\t\tconst MTPcontacts_Found &result) {\n\tExpects(result.type() == mtpc_contacts_found);\n\n\tauto &found = result.c_contacts_found();\n\tauto query = _query;\n\tif (requestId) {\n\t\t_peer->owner().processUsers(found.vusers());\n\t\t_peer->owner().processChats(found.vchats());\n\t\tauto it = _globalQueries.find(requestId);\n\t\tif (it != _globalQueries.cend()) {\n\t\t\tquery = it->second;\n\t\t\t_globalCache[query] = result;\n\t\t\t_globalQueries.erase(it);\n\t\t}\n\t}\n\n\tconst auto feedList = [&](const MTPVector<MTPPeer> &list) {\n\t\tfor (const auto &mtpPeer : list.v) {\n\t\t\tconst auto peerId = peerFromMTP(mtpPeer);\n\t\t\tif (const auto peer = _peer->owner().peerLoaded(peerId)) {\n\t\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\t\t_additional->checkForLoaded(user);\n\t\t\t\t\tdelegate()->peerListSearchAddRow(user);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\tif (_requestId == requestId) {\n\t\t_requestId = 0;\n\t\t_globalLoaded = true;\n\t\tfeedList(found.vmy_results());\n\t\tfeedList(found.vresults());\n\t\tdelegate()->peerListSearchRefreshRows();\n\t}\n}\n\nvoid AddSpecialBoxSearchController::addChatMembers(\n\t\tnot_null<ChatData*> chat) {\n\tif (chat->participants.empty()) {\n\t\treturn;\n\t}\n\n\t_chatMembersAdded = true;\n\tconst auto wordList = TextUtilities::PrepareSearchWords(_query);\n\tif (wordList.empty()) {\n\t\treturn;\n\t}\n\tconst auto allWordsAreFound = [&](\n\t\t\tconst base::flat_set<QString> &nameWords) {\n\t\tconst auto hasNamePartStartingWith = [&](const QString &word) {\n\t\t\tfor (const auto &nameWord : nameWords) {\n\t\t\t\tif (nameWord.startsWith(word)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\n\t\tfor (const auto &word : wordList) {\n\t\t\tif (!hasNamePartStartingWith(word)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t};\n\n\tfor (const auto &user : chat->participants) {\n\t\tif (allWordsAreFound(user->nameWords())) {\n\t\t\tdelegate()->peerListSearchAddRow(user);\n\t\t}\n\t}\n\tdelegate()->peerListSearchRefreshRows();\n}\n\nvoid AddSpecialBoxSearchController::addChatsContacts() {\n\t_chatsContactsAdded = true;\n\tconst auto wordList = TextUtilities::PrepareSearchWords(_query);\n\tif (wordList.empty()) {\n\t\treturn;\n\t}\n\tconst auto allWordsAreFound = [&](\n\t\t\tconst base::flat_set<QString> &nameWords) {\n\t\tconst auto hasNamePartStartingWith = [&](const QString &word) {\n\t\t\tfor (const auto &nameWord : nameWords) {\n\t\t\t\tif (nameWord.startsWith(word)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\n\t\tfor (const auto &word : wordList) {\n\t\t\tif (!hasNamePartStartingWith(word)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t};\n\tconst auto getSmallestIndex = [&](not_null<Dialogs::IndexedList*> list)\n\t-> const Dialogs::List* {\n\t\tif (list->empty()) {\n\t\t\treturn nullptr;\n\t\t}\n\n\t\tauto result = (const Dialogs::List*)nullptr;\n\t\tfor (const auto &word : wordList) {\n\t\t\tconst auto found = list->filtered(word[0]);\n\t\t\tif (!found || found->empty()) {\n\t\t\t\treturn nullptr;\n\t\t\t}\n\t\t\tif (!result || result->size() > found->size()) {\n\t\t\t\tresult = found;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\tconst auto filterAndAppend = [&](not_null<Dialogs::IndexedList*> list) {\n\t\tconst auto index = getSmallestIndex(list);\n\t\tif (!index) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const auto &row : *index) {\n\t\t\tif (const auto history = row->history()) {\n\t\t\t\tif (const auto user = history->peer->asUser()) {\n\t\t\t\t\tif (allWordsAreFound(user->nameWords())) {\n\t\t\t\t\t\tdelegate()->peerListSearchAddRow(user);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\tfilterAndAppend(_peer->owner().chatsList()->indexed());\n\tconst auto id = Data::Folder::kId;\n\tif (const auto folder = _peer->owner().folderLoaded(id)) {\n\t\tfilterAndAppend(folder->chatsList()->indexed());\n\t}\n\tfilterAndAppend(_peer->owner().contactsNoChatsList());\n\tdelegate()->peerListSearchRefreshRows();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/add_participants_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/peers/edit_participants_box.h\"\n\nstruct ChatAdminRightsInfo;\nstruct ChatRestrictionsInfo;\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nclass AddParticipantsBoxController : public ContactsBoxController {\npublic:\n\tstatic void Start(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChatData*> chat);\n\tstatic void Start(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChannelData*> channel);\n\tstatic void Start(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChannelData*> channel,\n\t\tbase::flat_set<not_null<UserData*>> &&alreadyIn);\n\n\texplicit AddParticipantsBoxController(not_null<Main::Session*> session);\n\texplicit AddParticipantsBoxController(not_null<PeerData*> peer);\n\tAddParticipantsBoxController(\n\t\tnot_null<PeerData*> peer,\n\t\tbase::flat_set<not_null<UserData*>> &&alreadyIn);\n\n\t[[nodiscard]] not_null<PeerData*> peer() const {\n\t\treturn _peer;\n\t}\n\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid itemDeselectedHook(not_null<PeerData*> peer) override;\n\nprotected:\n\tvoid prepareViewHook() override;\n\tstd::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<UserData*> user) override;\n\tvirtual bool needsInviteLinkButton();\n\nprivate:\n\tstatic void Start(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChannelData*> channel,\n\t\tbase::flat_set<not_null<UserData*>> &&alreadyIn,\n\t\tbool justCreated);\n\n\tbase::weak_qptr<Ui::BoxContent> showBox(object_ptr<Ui::BoxContent> box) const;\n\n\tvoid addInviteLinkButton();\n\tvoid inviteSelectedUsers(\n\t\tnot_null<PeerListBox*> box,\n\t\tFn<void()> done) const;\n\tvoid subscribeToMigration();\n\tint alreadyInCount() const;\n\tbool isAlreadyIn(not_null<UserData*> user) const;\n\tint fullCount() const;\n\tvoid updateTitle();\n\n\tPeerData *_peer = nullptr;\n\tbase::flat_set<not_null<UserData*>> _alreadyIn;\n\n};\n\nstruct ForbiddenInvites {\n\tstd::vector<not_null<UserData*>> users;\n\tstd::vector<not_null<UserData*>> premiumAllowsInvite;\n\tstd::vector<not_null<UserData*>> premiumAllowsWrite;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn users.empty();\n\t}\n};\n[[nodiscard]] ForbiddenInvites CollectForbiddenUsers(\n\tnot_null<Main::Session*> session,\n\tconst MTPmessages_InvitedUsers &result);\nbool ChatInviteForbidden(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<PeerData*> peer,\n\tForbiddenInvites forbidden);\n\n// Adding an admin, banned or restricted user from channel members\n// with search + contacts search + global search.\nclass AddSpecialBoxController\n\t: public PeerListController\n\t, public base::has_weak_ptr {\npublic:\n\tusing Role = ParticipantsBoxController::Role;\n\n\tusing AdminDoneCallback = Fn<void(\n\t\tnot_null<UserData*> user,\n\t\tChatAdminRightsInfo adminRights,\n\t\tconst std::optional<QString> &rank)>;\n\tusing BannedDoneCallback = Fn<void(\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo bannedRights)>;\n\tAddSpecialBoxController(\n\t\tnot_null<PeerData*> peer,\n\t\tRole role,\n\t\tAdminDoneCallback adminDoneCallback,\n\t\tBannedDoneCallback bannedDoneCallback);\n\n\t[[nodiscard]] not_null<PeerData*> peer() const {\n\t\treturn _peer;\n\t}\n\t[[nodiscard]] Main::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\n\t[[nodiscard]] std::unique_ptr<PeerListRow> createSearchRow(\n\t\tnot_null<PeerData*> peer) override;\n\nprivate:\n\ttemplate <typename Callback>\n\tbool checkInfoLoaded(not_null<PeerData*> participant, Callback callback);\n\n\tvoid prepareChatRows(not_null<ChatData*> chat);\n\tvoid rebuildChatRows(not_null<ChatData*> chat);\n\n\tvoid showAdmin(not_null<UserData*> user, bool sure = false);\n\tvoid editAdminDone(\n\t\tnot_null<UserData*> user,\n\t\tChatAdminRightsInfo rights,\n\t\tconst std::optional<QString> &rank);\n\tvoid showRestricted(not_null<UserData*> user, bool sure = false);\n\tvoid editRestrictedDone(\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo rights);\n\tvoid kickUser(not_null<PeerData*> participant, bool sure = false);\n\tbool appendRow(not_null<PeerData*> participant);\n\tbool prependRow(not_null<UserData*> user);\n\tstd::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<PeerData*> participant) const;\n\n\tvoid subscribeToMigration();\n\tvoid migrate(not_null<ChatData*> chat, not_null<ChannelData*> channel);\n\n\tbase::weak_qptr<Ui::BoxContent> showBox(object_ptr<Ui::BoxContent> box) const;\n\n\tnot_null<PeerData*> _peer;\n\tMTP::Sender _api;\n\tRole _role = Role::Admins;\n\tint _offset = 0;\n\tmtpRequestId _loadRequestId = 0;\n\tbool _allLoaded = false;\n\tParticipantsAdditionalData _additional;\n\tstd::unique_ptr<ParticipantsOnlineSorter> _onlineSorter;\n\tUi::BoxPointer _editBox;\n\tbase::weak_qptr<Ui::BoxContent> _editParticipantBox;\n\tAdminDoneCallback _adminDoneCallback;\n\tBannedDoneCallback _bannedDoneCallback;\n\nprotected:\n\tbool _excludeSelf = true;\n\n};\n\n// Finds chat/channel members, then contacts, then global search results.\nclass AddSpecialBoxSearchController : public PeerListSearchController {\npublic:\n\tusing Role = ParticipantsBoxController::Role;\n\n\tAddSpecialBoxSearchController(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<ParticipantsAdditionalData*> additional);\n\n\tvoid searchQuery(const QString &query) override;\n\tbool isLoading() override;\n\tbool loadMoreRows() override;\n\nprivate:\n\tstruct CacheEntry {\n\t\tMTPchannels_ChannelParticipants result;\n\t\tint requestedCount = 0;\n\t};\n\tstruct Query {\n\t\tQString text;\n\t\tint offset = 0;\n\t};\n\n\tvoid searchOnServer();\n\tbool searchParticipantsInCache();\n\tvoid searchParticipantsDone(\n\t\tmtpRequestId requestId,\n\t\tconst MTPchannels_ChannelParticipants &result,\n\t\tint requestedCount);\n\tbool searchGlobalInCache();\n\tvoid searchGlobalDone(\n\t\tmtpRequestId requestId,\n\t\tconst MTPcontacts_Found &result);\n\tvoid requestParticipants();\n\tvoid addChatMembers(not_null<ChatData*> chat);\n\tvoid addChatsContacts();\n\tvoid requestGlobal();\n\n\tvoid subscribeToMigration();\n\n\tnot_null<PeerData*> _peer;\n\tnot_null<ParticipantsAdditionalData*> _additional;\n\tMTP::Sender _api;\n\n\tbase::Timer _timer;\n\tQString _query;\n\tmtpRequestId _requestId = 0;\n\tint _offset = 0;\n\tbool _participantsLoaded = false;\n\tbool _chatsContactsAdded = false;\n\tbool _chatMembersAdded = false;\n\tbool _globalLoaded = false;\n\tstd::map<QString, CacheEntry> _participantsCache;\n\tstd::map<mtpRequestId, Query> _participantsQueries;\n\tstd::map<QString, MTPcontacts_Found> _globalCache;\n\tstd::map<mtpRequestId, QString> _globalQueries;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/channel_ownership_transfer.h\"\n\n#include \"api/api_cloud_password.h\"\n#include \"apiwrap.h\"\n#include \"boxes/passcode_box.h\"\n#include \"core/core_cloud_password.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/layers/show.h\"\n\nChannelOwnershipTransfer::ChannelOwnershipTransfer(\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> selectedUser,\n\tstd::shared_ptr<Ui::Show> show,\n\tFn<void(std::shared_ptr<Ui::Show>)> onSuccess)\n: _peer(peer)\n, _selectedUser(selectedUser)\n, _show(show)\n, _onSuccess(std::move(onSuccess)) {\n}\n\nbool ChannelOwnershipTransfer::handleTransferPasswordError(\n\t\tconst QString &error) {\n\tconst auto session = &_selectedUser->session();\n\tauto about = (_peer->asChannel() && !_peer->isMegagroup()\n\t\t? tr::lng_rights_transfer_check_about_channel\n\t\t: tr::lng_rights_transfer_check_about)(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\ttr::bold(_selectedUser->shortName()),\n\t\t\ttr::marked);\n\tif (auto box = PrePasswordErrorBox(error, session, std::move(about))) {\n\t\t_show->showBox(std::move(box));\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid ChannelOwnershipTransfer::start() {\n\tconst auto api = &_peer->session().api();\n\tapi->cloudPassword().reload();\n\tapi->request(MTPmessages_EditChatCreator(\n\t\t_peer->input(),\n\t\tMTP_inputUserEmpty(),\n\t\tMTP_inputCheckPasswordEmpty()\n\t)).fail([=](const MTP::Error &error) {\n\t\tconst auto &type = error.type();\n\t\tif (handleTransferPasswordError(type)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto callback = crl::guard(_show->toastParent(), [=](\n\t\t\t\tFn<void()> &&close) {\n\t\t\trequestPassword();\n\t\t\tclose();\n\t\t});\n\t\t_show->showBox(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_rights_transfer_about(\n\t\t\t\ttr::now,\n\t\t\t\tlt_group,\n\t\t\t\ttr::bold(_peer->name()),\n\t\t\t\tlt_user,\n\t\t\t\ttr::bold(_selectedUser->shortName()),\n\t\t\t\ttr::rich),\n\t\t\t.confirmed = callback,\n\t\t\t.confirmText = tr::lng_rights_transfer_sure(),\n\t\t}));\n\t}).send();\n}\n\nvoid ChannelOwnershipTransfer::requestPassword() {\n\t_peer->session().api().cloudPassword().state(\n\t) | rpl::take(\n\t\t1\n\t) | rpl::on_next([=](const Core::CloudPasswordState &state) {\n\t\tauto fields = PasscodeBox::CloudFields::From(state);\n\t\tfields.customTitle = tr::lng_rights_transfer_password_title();\n\t\tfields.customDescription\n\t\t\t= tr::lng_rights_transfer_password_description(tr::now);\n\t\tfields.customSubmitButton = tr::lng_passcode_submit();\n\t\tfields.customCheckCallback = [=](\n\t\t\t\tconst Core::CloudPasswordResult &result,\n\t\t\t\tbase::weak_qptr<PasscodeBox> box) {\n\t\t\tsendRequest(box, result);\n\t\t};\n\t\t_show->showBox(Box<PasscodeBox>(&_peer->session(), fields));\n\t}, _lifetime);\n}\n\nvoid ChannelOwnershipTransfer::sendRequest(\n\t\tbase::weak_qptr<PasscodeBox> box,\n\t\tconst Core::CloudPasswordResult &result) {\n\tconst auto api = &_peer->session().api();\n\tapi->request(MTPmessages_EditChatCreator(\n\t\t_peer->input(),\n\t\t_selectedUser->inputUser(),\n\t\tresult.result\n\t)).done([=](const MTPUpdates &result) {\n\t\tapi->applyUpdates(result);\n\t\tconst auto currentShow = box ? box->uiShow() : _show;\n\t\tcurrentShow->showToast(\n\t\t\t(_peer->isBroadcast()\n\t\t\t\t? tr::lng_rights_transfer_done_channel\n\t\t\t\t: tr::lng_rights_transfer_done_group)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\t_selectedUser->shortName()));\n\t\tif (_onSuccess) {\n\t\t\t_onSuccess(currentShow);\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (box && box->handleCustomCheckError(error)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto &type = error.type();\n\t\tconst auto problem = [&] {\n\t\t\tif (type == u\"CHANNELS_ADMIN_PUBLIC_TOO_MUCH\"_q) {\n\t\t\t\treturn tr::lng_channels_too_much_public_other(tr::now);\n\t\t\t} else if (type == u\"CHANNELS_ADMIN_LOCATED_TOO_MUCH\"_q) {\n\t\t\t\treturn tr::lng_channels_too_much_located_other(tr::now);\n\t\t\t} else if (type == u\"ADMINS_TOO_MUCH\"_q) {\n\t\t\t\treturn (_peer->isBroadcast()\n\t\t\t\t\t? tr::lng_error_admin_limit_channel\n\t\t\t\t\t: tr::lng_error_admin_limit)(tr::now);\n\t\t\t} else if (type == u\"CHANNEL_INVALID\"_q\n\t\t\t\t|| type == u\"CHAT_CREATOR_REQUIRED\"_q\n\t\t\t\t|| type == u\"PARTICIPANT_MISSING\"_q) {\n\t\t\t\treturn (_peer->isBroadcast()\n\t\t\t\t\t? tr::lng_channel_not_accessible\n\t\t\t\t\t: tr::lng_group_not_accessible)(tr::now);\n\t\t\t}\n\t\t\treturn Lang::Hard::ServerError();\n\t\t}();\n\t\t_show->showBox(Ui::MakeInformBox(problem));\n\t\tif (box) {\n\t\t\tbox->closeBox();\n\t\t}\n\t}).handleFloodErrors().send();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass UserData;\n\nnamespace Ui {\nclass Show;\n} // namespace Ui\n\nnamespace Core {\nstruct CloudPasswordResult;\n} // namespace Core\n\nclass PasscodeBox;\n\nclass ChannelOwnershipTransfer {\npublic:\n\tChannelOwnershipTransfer(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> selectedUser,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tFn<void(std::shared_ptr<Ui::Show>)> onSuccess = nullptr);\n\n\tvoid start();\n\nprivate:\n\tbool handleTransferPasswordError(const QString &error);\n\tvoid requestPassword();\n\tvoid sendRequest(\n\t\tbase::weak_qptr<PasscodeBox> box,\n\t\tconst Core::CloudPasswordResult &result);\n\n\tconst not_null<PeerData*> _peer;\n\tconst not_null<UserData*> _selectedUser;\n\tconst std::shared_ptr<Ui::Show> _show;\n\tconst Fn<void(std::shared_ptr<Ui::Show>)> _onSuccess;\n\n\trpl::lifetime _lifetime;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/choose_peer_box.h\"\n\n#include \"apiwrap.h\" // ApiWrap::botCommonGroups / requestBotCommonGroups.\n#include \"boxes/add_contact_box.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"history/history_item_reply_markup.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\" // Session::api().\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n\nnamespace {\n\nclass ChoosePeerBoxController final\n\t: public ChatsListBoxController\n\t, public base::has_weak_ptr {\npublic:\n\tChoosePeerBoxController(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<UserData*> bot,\n\t\tRequestPeerQuery query,\n\t\tFn<void(std::vector<not_null<PeerData*>>)> callback);\n\n\tMain::Session &session() const override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\n\t[[nodiscard]] rpl::producer<int> selectedCountValue() const;\n\tvoid submit();\n\n\tQString savedMessagesChatStatus() const override {\n\t\treturn {};\n\t}\n\nprivate:\n\tvoid prepareViewHook() override;\n\tstd::unique_ptr<Row> createRow(not_null<History*> history) override;\n\tQString emptyBoxText() const override;\n\n\tvoid prepareRestrictions();\n\n\tconst not_null<Main::Session*> _session;\n\tnot_null<UserData*> _bot;\n\tRequestPeerQuery _query;\n\tbase::flat_set<not_null<PeerData*>> _commonGroups;\n\tbase::flat_set<not_null<PeerData*>> _selected;\n\trpl::variable<int> _selectedCount;\n\tFn<void(std::vector<not_null<PeerData*>>)> _callback;\n\n};\n\nusing RightsMap = std::vector<std::pair<ChatAdminRight, tr::phrase<>>>;\n\n[[nodiscard]] RightsMap GroupRights() {\n\tusing Flag = ChatAdminRight;\n\treturn {\n\t\t{ Flag::ChangeInfo, tr::lng_request_group_change_info },\n\t\t{ Flag::DeleteMessages, tr::lng_request_group_delete_messages },\n\t\t{ Flag::BanUsers, tr::lng_request_group_ban_users },\n\t\t{ Flag::InviteByLinkOrAdd, tr::lng_request_group_invite },\n\t\t{ Flag::PinMessages, tr::lng_request_group_pin_messages },\n\t\t{ Flag::ManageTopics, tr::lng_request_group_manage_topics },\n\t\t{ Flag::ManageCall, tr::lng_request_group_manage_video_chats },\n\t\t{ Flag::Anonymous, tr::lng_request_group_anonymous },\n\t\t{ Flag::AddAdmins, tr::lng_request_group_add_admins },\n\t};\n}\n\n[[nodiscard]] RightsMap BroadcastRights() {\n\tusing Flag = ChatAdminRight;\n\treturn {\n\t\t{ Flag::ChangeInfo, tr::lng_request_channel_change_info },\n\t\t{ Flag::PostMessages, tr::lng_request_channel_post_messages },\n\t\t{ Flag::EditMessages, tr::lng_request_channel_edit_messages },\n\t\t{ Flag::DeleteMessages, tr::lng_request_channel_delete_messages },\n\t\t{ Flag::InviteByLinkOrAdd, tr::lng_request_channel_add_subscribers },\n\t\t{ Flag::ManageCall, tr::lng_request_channel_manage_livestreams },\n\t\t{ Flag::ManageDirect, tr::lng_request_channel_manage_direct },\n\t\t{ Flag::AddAdmins, tr::lng_request_channel_add_admins },\n\t\t{ Flag::BanUsers, tr::lng_request_group_ban_users },\n\t};\n}\n\n[[nodiscard]] QString RightsText(\n\t\tChatAdminRights rights,\n\t\tconst RightsMap &phrases) {\n\tauto list = QStringList();\n\tfor (const auto &[flag, phrase] : phrases) {\n\t\tif (rights & flag) {\n\t\t\tlist.push_back(phrase(tr::now));\n\t\t}\n\t}\n\tconst auto count = list.size();\n\tif (!count) {\n\t\treturn QString();\n\t}\n\tconst auto last = list.back();\n\treturn (count > 1)\n\t\t? tr::lng_request_peer_rights_and(\n\t\t\ttr::now,\n\t\t\tlt_rights,\n\t\t\tlist.mid(0, count - 1).join(\", \"),\n\t\t\tlt_last,\n\t\t\tlast)\n\t\t: last;\n}\n\n[[nodiscard]] QString GroupRightsText(ChatAdminRights rights) {\n\treturn RightsText(rights, GroupRights());\n}\n\n[[nodiscard]] QString BroadcastRightsText(ChatAdminRights rights) {\n\treturn RightsText(rights, BroadcastRights());\n}\n\n[[nodiscard]] QStringList RestrictionsList(RequestPeerQuery query) {\n\tusing Type = RequestPeerQuery::Type;\n\tusing Restriction = RequestPeerQuery::Restriction;\n\tauto result = QStringList();\n\tconst auto addRestriction = [&](\n\t\t\tRestriction value,\n\t\t\ttr::phrase<> yes,\n\t\t\ttr::phrase<> no) {\n\t\tif (value == Restriction::Yes) {\n\t\t\tresult.push_back(yes(tr::now));\n\t\t} else if (value == Restriction::No) {\n\t\t\tresult.push_back(no(tr::now));\n\t\t}\n\t};\n\tconst auto addRights = [&](const QString &rights) {\n\t\tif (!rights.isEmpty()) {\n\t\t\tresult.push_back(\n\t\t\t\ttr::lng_request_peer_rights(tr::now, lt_rights, rights));\n\t\t}\n\t};\n\tswitch (query.type) {\n\tcase Type::User:\n\t\tif (query.userIsBot != Restriction::Yes) {\n\t\t\taddRestriction(\n\t\t\t\tquery.userIsPremium,\n\t\t\t\ttr::lng_request_user_premium_yes,\n\t\t\t\ttr::lng_request_user_premium_no);\n\t\t}\n\t\tbreak;\n\tcase Type::Group:\n\t\taddRestriction(\n\t\t\tquery.hasUsername,\n\t\t\ttr::lng_request_group_public_yes,\n\t\t\ttr::lng_request_group_public_no);\n\t\taddRestriction(\n\t\t\tquery.groupIsForum,\n\t\t\ttr::lng_request_group_topics_yes,\n\t\t\ttr::lng_request_group_topics_no);\n\t\tif (query.amCreator) {\n\t\t\tresult.push_back(tr::lng_request_group_am_owner(tr::now));\n\t\t} else {\n\t\t\taddRights(GroupRightsText(query.myRights));\n\t\t}\n\t\tbreak;\n\tcase Type::Broadcast:\n\t\taddRestriction(\n\t\t\tquery.hasUsername,\n\t\t\ttr::lng_request_channel_public_yes,\n\t\t\ttr::lng_request_channel_public_no);\n\t\tif (query.amCreator) {\n\t\t\tresult.push_back(tr::lng_request_channel_am_owner(tr::now));\n\t\t} else {\n\t\t\taddRights(BroadcastRightsText(query.myRights));\n\t\t}\n\t\tbreak;\n\t}\n\treturn result;\n}\n\nobject_ptr<Ui::BoxContent> MakeConfirmBox(\n\t\tnot_null<UserData*> bot,\n\t\tnot_null<PeerData*> peer,\n\t\tRequestPeerQuery query,\n\t\tFn<void()> confirmed) {\n\tconst auto name = peer->name();\n\tconst auto botName = bot->name();\n\tauto text = tr::lng_request_peer_confirm(\n\t\ttr::now,\n\t\tlt_chat,\n\t\ttr::bold(name),\n\t\tlt_bot,\n\t\ttr::bold(botName),\n\t\ttr::marked);\n\tif (!peer->isUser()) {\n\t\tconst auto rights = peer->isBroadcast()\n\t\t\t? BroadcastRightsText(query.botRights)\n\t\t\t: GroupRightsText(query.botRights);\n\t\tif (!rights.isEmpty()) {\n\t\t\ttext.append('\\n').append('\\n').append(\n\t\t\t\ttr::lng_request_peer_confirm_rights(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_bot,\n\t\t\t\t\ttr::bold(botName),\n\t\t\t\t\tlt_chat,\n\t\t\t\t\ttr::bold(name),\n\t\t\t\t\tlt_rights,\n\t\t\t\t\tTextWithEntities{ rights },\n\t\t\t\t\ttr::marked));\n\t\t} else if (!peer->isBroadcast() && query.isBotParticipant) {\n\t\t\tconst auto common = bot->session().api().botCommonGroups(bot);\n\t\t\tif (!common || !ranges::contains(*common, peer)) {\n\t\t\t\ttext.append('\\n').append('\\n').append(\n\t\t\t\t\ttr::lng_request_peer_confirm_add(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_bot,\n\t\t\t\t\t\ttr::bold(botName),\n\t\t\t\t\t\tlt_chat,\n\t\t\t\t\t\ttr::bold(name),\n\t\t\t\t\t\ttr::marked));\n\t\t\t}\n\t\t}\n\t}\n\treturn Ui::MakeConfirmBox({\n\t\t.text = std::move(text),\n\t\t.confirmed = [=](Fn<void()> close) { confirmed(); close(); },\n\t\t.confirmText = tr::lng_request_peer_confirm_send(tr::now),\n\t});\n}\n\nobject_ptr<Ui::BoxContent> CreatePeerByQueryBox(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<UserData*> bot,\n\t\tRequestPeerQuery query,\n\t\tFn<void(std::vector<not_null<PeerData*>>)> done) {\n\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tauto callback = [=](not_null<PeerData*> peer) {\n\t\tdone({ peer });\n\t\tif (const auto strong = weak->get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t};\n\tauto result = Box<GroupInfoBox>(\n\t\tnavigation,\n\t\tbot,\n\t\tquery,\n\t\tstd::move(callback));\n\t*weak = result.data();\n\treturn result;\n}\n\n[[nodiscard]] bool FilterPeerByQuery(\n\t\tnot_null<PeerData*> peer,\n\t\tRequestPeerQuery query,\n\t\tconst base::flat_set<not_null<PeerData*>> &commonGroups) {\n\tusing Type = RequestPeerQuery::Type;\n\tusing Restriction = RequestPeerQuery::Restriction;\n\tconst auto checkRestriction = [](Restriction restriction, bool value) {\n\t\treturn (restriction == Restriction::Any)\n\t\t\t|| ((restriction == Restriction::Yes) == value);\n\t};\n\tconst auto checkRights = [](\n\t\t\tChatAdminRights wanted,\n\t\t\tbool creator,\n\t\t\tChatAdminRights rights) {\n\t\treturn creator || ((rights & wanted) == wanted);\n\t};\n\tswitch (query.type) {\n\tcase Type::User: {\n\t\tconst auto user = peer->asUser();\n\t\treturn user\n\t\t\t&& !user->isInaccessible()\n\t\t\t&& !user->isNotificationsUser()\n\t\t\t&& checkRestriction(query.userIsBot, user->isBot())\n\t\t\t&& checkRestriction(query.userIsPremium, user->isPremium());\n\t}\n\tcase Type::Group: {\n\t\tconst auto chat = peer->asChat();\n\t\tconst auto megagroup = peer->asMegagroup();\n\t\treturn (chat || megagroup)\n\t\t\t&& (!query.amCreator\n\t\t\t\t|| (chat ? chat->amCreator() : megagroup->amCreator()))\n\t\t\t&& checkRestriction(query.groupIsForum, peer->isForum())\n\t\t\t&& checkRestriction(\n\t\t\t\tquery.hasUsername,\n\t\t\t\tmegagroup && megagroup->hasUsername())\n\t\t\t&& checkRights(\n\t\t\t\tquery.myRights,\n\t\t\t\tchat ? chat->amCreator() : megagroup->amCreator(),\n\t\t\t\tchat ? chat->adminRights() : megagroup->adminRights())\n\t\t\t&& (!query.isBotParticipant\n\t\t\t\t|| query.myRights\n\t\t\t\t|| commonGroups.contains(peer)\n\t\t\t\t|| (chat\n\t\t\t\t\t? chat->canAddMembers()\n\t\t\t\t\t: megagroup->canAddMembers()));\n\t}\n\tcase Type::Broadcast: {\n\t\tconst auto broadcast = peer->asBroadcast();\n\t\treturn broadcast\n\t\t\t&& (!query.amCreator || broadcast->amCreator())\n\t\t\t&& checkRestriction(query.hasUsername, broadcast->hasUsername())\n\t\t\t&& checkRights(\n\t\t\t\tquery.myRights,\n\t\t\t\tbroadcast->amCreator(),\n\t\t\t\tbroadcast->adminRights());\n\t}\n\t}\n\tUnexpected(\"Type in FilterPeerByQuery.\");\n}\n\nChoosePeerBoxController::ChoosePeerBoxController(\n\tnot_null<Main::Session*> session,\n\tnot_null<UserData*> bot,\n\tRequestPeerQuery query,\n\tFn<void(std::vector<not_null<PeerData*>>)> callback)\n: ChatsListBoxController(session)\n, _session(session)\n, _bot(bot)\n, _query(query)\n, _callback(std::move(callback)) {\n\tif (const auto list = _bot->session().api().botCommonGroups(_bot)) {\n\t\t_commonGroups = { begin(*list), end(*list) };\n\t}\n}\n\nMain::Session &ChoosePeerBoxController::session() const {\n\treturn *_session;\n}\n\nvoid ChoosePeerBoxController::prepareRestrictions() {\n\tauto above = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tconst auto raw = above.data();\n\tauto rows = RestrictionsList(_query);\n\tif (!rows.empty()) {\n\t\tUi::AddSubsectionTitle(\n\t\t\traw,\n\t\t\ttr::lng_request_peer_requirements(),\n\t\t\t{ 0, st::membersMarginTop, 0, 0 });\n\t\tconst auto skip = st::defaultSubsectionTitlePadding.left();\n\t\tauto separator = '\\n' + Ui::kQBullet + ' ';\n\t\traw->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\traw,\n\t\t\t\tseparator + rows.join(separator),\n\t\t\t\tst::requestPeerRestriction),\n\t\t\t{ skip, 0, skip, st::membersMarginTop });\n\t\tUi::AddDivider(raw);\n\t}\n\tconst auto make = [&](tr::phrase<> text, const style::icon &st) {\n\t\tauto button = raw->add(\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\traw,\n\t\t\t\ttext(),\n\t\t\t\tst::inviteViaLinkButton),\n\t\t\t{ 0, st::membersMarginTop, 0, 0 });\n\t\tconst auto icon = Ui::CreateChild<Info::Profile::FloatingIcon>(\n\t\t\tbutton,\n\t\t\tst,\n\t\t\tQPoint());\n\t\tbutton->heightValue(\n\t\t) | rpl::on_next([=](int height) {\n\t\t\ticon->moveToLeft(\n\t\t\t\tst::choosePeerCreateIconLeft,\n\t\t\t\t(height - st::inviteViaLinkIcon.height()) / 2);\n\t\t}, icon->lifetime());\n\n\t\tbutton->setClickedCallback([=] {\n\t\t\tconst auto controller = ChatHelpers::ResolveWindowDefault()(\n\t\t\t\t_session);\n\t\t\tif (controller) {\n\t\t\t\tdelegate()->peerListUiShow()->showBox(\n\t\t\t\t\tCreatePeerByQueryBox(\n\t\t\t\t\t\tcontroller,\n\t\t\t\t\t\t_bot,\n\t\t\t\t\t\t_query,\n\t\t\t\t\t\t_callback));\n\t\t\t}\n\t\t});\n\n\t\tbutton->events(\n\t\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\t\treturn (e->type() == QEvent::Enter);\n\t\t}) | rpl::on_next([=] {\n\t\t\tdelegate()->peerListMouseLeftGeometry();\n\t\t}, button->lifetime());\n\t\treturn button;\n\t};\n\tif (_query.type == RequestPeerQuery::Type::Group) {\n\t\tmake(tr::lng_request_group_create, st::choosePeerGroupIcon);\n\t} else if (_query.type == RequestPeerQuery::Type::Broadcast) {\n\t\tmake(tr::lng_request_channel_create, st::choosePeerChannelIcon);\n\t}\n\n\tif (raw->count() > 0) {\n\t\tdelegate()->peerListSetAboveWidget(std::move(above));\n\t}\n}\n\nvoid ChoosePeerBoxController::prepareViewHook() {\n\tdelegate()->peerListSetTitle([&] {\n\t\tusing Type = RequestPeerQuery::Type;\n\t\tusing Restriction = RequestPeerQuery::Restriction;\n\t\tswitch (_query.type) {\n\t\tcase Type::User: return (_query.userIsBot == Restriction::Yes)\n\t\t\t? tr::lng_request_bot_title()\n\t\t\t: (_query.maxQuantity > 1)\n\t\t\t? tr::lng_request_users_title()\n\t\t\t: tr::lng_request_user_title();\n\t\tcase Type::Group: return tr::lng_request_group_title();\n\t\tcase Type::Broadcast: return tr::lng_request_channel_title();\n\t\t}\n\t\tUnexpected(\"Type in RequestPeerQuery.\");\n\t}());\n\tprepareRestrictions();\n}\n\nvoid ChoosePeerBoxController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto limit = _query.maxQuantity;\n\tconst auto multiselect = (limit > 1);\n\tconst auto peer = row->peer();\n\tif (multiselect) {\n\t\tif (_selected.contains(peer) || _selected.size() < limit) {\n\t\t\tdelegate()->peerListSetRowChecked(row, !row->checked());\n\t\t\tif (row->checked()) {\n\t\t\t\t_selected.emplace(peer);\n\t\t\t} else {\n\t\t\t\t_selected.remove(peer);\n\t\t\t}\n\t\t\t_selectedCount = int(_selected.size());\n\t\t}\n\t\treturn;\n\t}\n\tconst auto done = [callback = _callback, peer] {\n\t\tconst auto onstack = callback;\n\t\tonstack({ peer });\n\t};\n\tif (peer->isUser()) {\n\t\tdone();\n\t} else {\n\t\tdelegate()->peerListUiShow()->showBox(\n\t\t\tMakeConfirmBox(_bot, peer, _query, done));\n\t}\n}\n\nrpl::producer<int> ChoosePeerBoxController::selectedCountValue() const {\n\treturn _selectedCount.value();\n}\n\nvoid ChoosePeerBoxController::submit() {\n\tconst auto onstack = _callback;\n\tonstack(ranges::to_vector(_selected));\n}\n\nauto ChoosePeerBoxController::createRow(not_null<History*> history)\n-> std::unique_ptr<Row> {\n\treturn FilterPeerByQuery(history->peer, _query, _commonGroups)\n\t\t? std::make_unique<Row>(history)\n\t\t: nullptr;\n}\n\nQString ChoosePeerBoxController::emptyBoxText() const {\n\tusing Type = RequestPeerQuery::Type;\n\tusing Restriction = RequestPeerQuery::Restriction;\n\n\tconst auto result = [](tr::phrase<> title, tr::phrase<> text) {\n\t\treturn title(tr::now) + \"\\n\\n\" + text(tr::now);\n\t};\n\tswitch (_query.type) {\n\tcase Type::User: return (_query.userIsBot == Restriction::Yes)\n\t\t? result(tr::lng_request_bot_no, tr::lng_request_bot_no_about)\n\t\t: result(tr::lng_request_user_no, tr::lng_request_user_no_about);\n\tcase Type::Group:\n\t\treturn result(\n\t\t\ttr::lng_request_group_no,\n\t\t\ttr::lng_request_group_no_about);\n\tcase Type::Broadcast:\n\t\treturn result(\n\t\t\ttr::lng_request_channel_no,\n\t\t\ttr::lng_request_channel_no_about);\n\t}\n\tUnexpected(\"Type in ChoosePeerBoxController::emptyBoxText.\");\n}\n\n} // namespace\n\nvoid ShowChoosePeerBox(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<UserData*> bot,\n\t\tRequestPeerQuery query,\n\t\tFn<void(std::vector<not_null<PeerData*>>)> chosen,\n\t\tFn<void()> cancelled) {\n\tShowChoosePeerBox(\n\t\tnavigation->uiShow(),\n\t\tbot,\n\t\tquery,\n\t\tstd::move(chosen),\n\t\tstd::move(cancelled));\n}\n\nvoid ShowChoosePeerBox(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<UserData*> bot,\n\t\tRequestPeerQuery query,\n\t\tFn<void(std::vector<not_null<PeerData*>>)> chosen,\n\t\tFn<void()> cancelled) {\n\tconst auto session = &show->session();\n\tconst auto needCommonGroups = query.isBotParticipant\n\t\t&& (query.type == RequestPeerQuery::Type::Group)\n\t\t&& !query.myRights;\n\tif (needCommonGroups && !session->api().botCommonGroups(bot)) {\n\t\tconst auto weak = std::weak_ptr(show);\n\t\tsession->api().requestBotCommonGroups(bot, [=] {\n\t\t\tif (const auto strong = weak.lock()) {\n\t\t\t\tShowChoosePeerBox(strong, bot, query, chosen, cancelled);\n\t\t\t}\n\t\t});\n\t\treturn;\n\t}\n\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tconst auto sent = std::make_shared<bool>(false);\n\tauto callback = [=, done = std::move(chosen)](\n\t\t\tstd::vector<not_null<PeerData*>> peers) {\n\t\t*sent = true;\n\t\tdone(std::move(peers));\n\t\tif (const auto strong = weak->get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t};\n\tconst auto limit = query.maxQuantity;\n\tauto controller = std::make_unique<ChoosePeerBoxController>(\n\t\tsession,\n\t\tbot,\n\t\tquery,\n\t\tstd::move(callback));\n\tauto initBox = [=, ptr = controller.get()](not_null<PeerListBox*> box) {\n\t\tptr->selectedCountValue() | rpl::on_next([=](int count) {\n\t\t\tbox->clearButtons();\n\t\t\tif (limit > 1) {\n\t\t\t\tbox->setAdditionalTitle(rpl::single(u\"%1 / %2\"_q.arg(count).arg(limit)));\n\t\t\t}\n\t\t\tif (count > 0) {\n\t\t\t\tbox->addButton(tr::lng_intro_submit(), [=] {\n\t\t\t\t\tptr->submit();\n\t\t\t\t\tif (*weak) {\n\t\t\t\t\t\t(*weak)->closeBox();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t\tbox->addButton(tr::lng_cancel(), [box] {\n\t\t\t\tbox->closeBox();\n\t\t\t});\n\t\t}, box->lifetime());\n\t};\n\t*weak = show->show(Box<PeerListBox>(\n\t\tstd::move(controller),\n\t\tstd::move(initBox)));\n\tif (const auto strong = weak->get()) {\n\t\tstrong->boxClosing() | rpl::on_next([=] {\n\t\t\tif (!*sent && cancelled) {\n\t\t\t\tcancelled();\n\t\t\t}\n\t\t}, strong->lifetime());\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/choose_peer_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nstruct RequestPeerQuery;\n\nnamespace Main {\nclass Session;\nclass SessionShow;\n} // namespace Main\n\nnamespace Ui {\nclass BoxContent;\n} // namespace Ui\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nvoid ShowChoosePeerBox(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<UserData*> bot,\n\tRequestPeerQuery query,\n\tFn<void(std::vector<not_null<PeerData*>>)> chosen,\n\tFn<void()> cancelled = nullptr);\n\nvoid ShowChoosePeerBox(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tnot_null<UserData*> bot,\n\tRequestPeerQuery query,\n\tFn<void(std::vector<not_null<PeerData*>>)> chosen,\n\tFn<void()> cancelled = nullptr);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/create_managed_bot_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/create_managed_bot_box.h\"\n\n#include \"base/timer.h\"\n#include \"boxes/peers/edit_peer_common.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/session/session_show.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/sender.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/fields/special_fields.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/vertical_list.h\"\n\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n\nvoid CreateManagedBotBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tCreateManagedBotDescriptor &&descriptor) {\n\tstruct State {\n\t\tbase::Timer checkTimer;\n\t\tmtpRequestId checkRequestId = 0;\n\t\tmtpRequestId createRequestId = 0;\n\t\tQString checkUsername;\n\t\tQString errorText;\n\t\tQString goodText;\n\t\tbool created = false;\n\t\tbool hadOccupiedError = false;\n\t};\n\tconst auto show = descriptor.show;\n\tconst auto session = &show->session();\n\tconst auto viaDeeplink = descriptor.viaDeeplink;\n\tconst auto done = std::move(descriptor.done);\n\tconst auto cancelled = std::move(descriptor.cancelled);\n\tconst auto manager = descriptor.manager;\n\n\tconst auto api = box->lifetime().make_state<MTP::Sender>(\n\t\t&session->mtp());\n\tconst auto state = box->lifetime().make_state<State>();\n\n\tbox->setStyle(st::createBotBox);\n\tbox->setWidth(st::boxWidth);\n\tbox->setNoContentMargin(true);\n\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\tbox->closeBox();\n\t});\n\n\tauto userpic = object_ptr<Ui::UserpicButton>(\n\t\tbox,\n\t\tmanager,\n\t\tst::defaultUserpicButton);\n\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tbox->addRow(\n\t\tstd::move(userpic),\n\t\tst::boxRowPadding + st::createBotUserpicPadding,\n\t\tstyle::al_top);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_create_bot_title(),\n\t\t\tst::boxTitle),\n\t\tst::boxRowPadding + st::createBotTitlePadding,\n\t\tstyle::al_top);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_create_bot_subtitle(\n\t\t\t\tlt_bot,\n\t\t\t\trpl::single(tr::bold(manager->name())),\n\t\t\t\ttr::rich),\n\t\t\tst::createBotCenteredText),\n\t\tst::boxRowPadding + st::createBotSubtitlePadding,\n\t\tstyle::al_top\n\t)->setTryMakeSimilarLines(true);\n\n\tconst auto name = box->addRow(object_ptr<Ui::InputField>(\n\t\tbox,\n\t\tst::defaultInputField,\n\t\ttr::lng_create_bot_name_placeholder(),\n\t\tdescriptor.suggestedName));\n\tname->setMaxLength(Ui::EditPeer::kMaxGroupChannelTitle);\n\n\tUi::AddSkip(box->verticalLayout(), st::createBotFieldSpacing);\n\n\tconst auto botPrefixText = u\"@\"_q;\n\tconst auto botSuffixText = u\"bot\"_q;\n\n\tauto initialUsername = descriptor.suggestedUsername;\n\twhile (initialUsername.startsWith(botPrefixText)) {\n\t\tinitialUsername.remove(0, botPrefixText.size());\n\t}\n\tif (initialUsername.endsWith(botSuffixText, Qt::CaseInsensitive)) {\n\t\tinitialUsername.chop(botSuffixText.size());\n\t}\n\n\tconst auto fieldSt = box->lifetime().make_state<style::InputField>(\n\t\tst::createBotUsernameField);\n\tfieldSt->textMargins.setLeft(\n\t\tst::defaultFlatLabel.style.font->width(botPrefixText));\n\tfieldSt->textMargins.setRight(\n\t\tst::createBotUsernameSuffix.style.font->width(botSuffixText));\n\tfieldSt->placeholderMargins.setLeft(-fieldSt->textMargins.left());\n\tfieldSt->placeholderMargins.setRight(-fieldSt->textMargins.right());\n\tconst auto usernameWrap = box->addRow(object_ptr<Ui::RpWidget>(box));\n\tconst auto username = Ui::CreateChild<Ui::UsernameInput>(\n\t\tusernameWrap,\n\t\t*fieldSt,\n\t\ttr::lng_create_bot_username_placeholder(),\n\t\tinitialUsername,\n\t\tQString());\n\tusername->setPlaceholderHidden(true);\n\tusername->setMaxLength(\n\t\tUi::EditPeer::kMaxUsernameLength - int(botSuffixText.size()));\n\tusernameWrap->widthValue() | rpl::on_next([=](int width) {\n\t\tusername->resizeToWidth(width);\n\t}, username->lifetime());\n\tusername->heightValue() | rpl::on_next([=](int height) {\n\t\tusernameWrap->resize(usernameWrap->width(), height);\n\t}, username->lifetime());\n\tusername->finishAnimating();\n\n\tconst auto botPrefix = Ui::CreateChild<Ui::FlatLabel>(\n\t\tusername,\n\t\tbotPrefixText,\n\t\tst::createBotUsernamePrefix);\n\tconst auto botSuffix = Ui::CreateChild<Ui::FlatLabel>(\n\t\tusername,\n\t\tbotSuffixText,\n\t\tst::createBotUsernameSuffix);\n\tbotPrefix->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tbotPrefix->show();\n\tbotSuffix->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tbotSuffix->show();\n\n\tconst auto updatePositions = [=] {\n\t\tconst auto &margin = fieldSt->textMargins;\n\t\tconst auto &font = fieldSt->style.font;\n\t\tconst auto text = username->getLastText();\n\t\tconst auto textWidth = font->width(text);\n\t\tconst auto maxX = username->width() - margin.right();\n\t\tconst auto x = std::min(margin.left() + textWidth, maxX);\n\t\tbotPrefix->move(0, margin.top());\n\t\tbotSuffix->move(x, margin.top());\n\t};\n\n\tusername->geometryValue(\n\t) | rpl::on_next(updatePositions, username->lifetime());\n\n\tconst auto statusWrapper = box->addRow(\n\t\tobject_ptr<Ui::RpWidget>(box),\n\t\tst::boxRowPadding + QMargins(0, st::defaultVerticalListSkip, 0, 0));\n\n\tconst auto statusLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\tstatusWrapper,\n\t\tst::createBotStatusLabel);\n\tstatusLabel->move(0, 0);\n\n\tconst auto maxHeight = box->lifetime().make_state<int>(0);\n\tstatusLabel->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tconst auto newMax = std::max({\n\t\t\t*maxHeight,\n\t\t\theight,\n\t\t\tst::createBotStatusLabel.style.font->height,\n\t\t});\n\t\tif (*maxHeight != newMax) {\n\t\t\t*maxHeight = newMax;\n\t\t\tstatusWrapper->resize(statusWrapper->width(), newMax);\n\t\t}\n\t}, statusWrapper->lifetime());\n\n\tstatusWrapper->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tstatusLabel->resizeToWidth(width);\n\t}, statusWrapper->lifetime());\n\n\tbox->setFocusCallback([=] { name->setFocusFast(); });\n\n\tconst auto setError = [=](const QString &text) {\n\t\tstate->errorText = text;\n\t\tstate->goodText = QString();\n\t\tstatusLabel->setText(text);\n\t\tstatusLabel->setTextColorOverride(\n\t\t\ttext.isEmpty()\n\t\t\t\t? std::optional<QColor>()\n\t\t\t\t: st::boxTextFgError->c);\n\t\tstate->checkTimer.cancel();\n\t};\n\n\tconst auto showLinkInfo = [=] {\n\t\tauto raw = username->getLastText().trimmed();\n\t\twhile (raw.startsWith(botPrefixText)) {\n\t\t\traw = raw.mid(botPrefixText.size());\n\t\t}\n\t\tconst auto full = raw + botSuffixText;\n\t\tconst auto text = tr::lng_create_bot_username_link(\n\t\t\ttr::now,\n\t\t\tlt_link,\n\t\t\tu\"t.me/\"_q + full);\n\t\tstate->errorText = QString();\n\t\tstate->goodText = text;\n\t\tstatusLabel->setText(text);\n\t\tstatusLabel->setTextColorOverride(st::usernameDefaultFg->c);\n\t};\n\n\tconst auto checkUsername = [=] {\n\t\tapi->request(base::take(state->checkRequestId)).cancel();\n\n\t\tauto raw = username->getLastText().trimmed();\n\t\twhile (raw.startsWith(botPrefixText)) {\n\t\t\traw = raw.mid(botPrefixText.size());\n\t\t}\n\t\tconst auto value = raw + botSuffixText;\n\t\tif (value.size() < Ui::EditPeer::kMinUsernameLength) {\n\t\t\treturn;\n\t\t}\n\t\tstate->checkUsername = value;\n\t\tstate->checkRequestId = api->request(MTPbots_CheckUsername(\n\t\t\tMTP_string(value)\n\t\t)).done([=](const MTPBool &result) {\n\t\t\tstate->checkRequestId = 0;\n\t\t\tif (mtpIsTrue(result)) {\n\t\t\t\tif (state->hadOccupiedError) {\n\t\t\t\t\tstate->errorText = QString();\n\t\t\t\t\tstate->goodText = tr::lng_create_bot_username_available(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_username,\n\t\t\t\t\t\tstate->checkUsername);\n\t\t\t\t\tstatusLabel->setText(state->goodText);\n\t\t\t\t\tstatusLabel->setTextColorOverride(st::boxTextFgGood->c);\n\t\t\t\t} else {\n\t\t\t\t\tshowLinkInfo();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tstate->hadOccupiedError = true;\n\t\t\t\tsetError(tr::lng_create_bot_username_taken(tr::now));\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tstate->checkRequestId = 0;\n\t\t\tstate->hadOccupiedError = true;\n\t\t\tsetError(tr::lng_create_bot_username_taken(tr::now));\n\t\t}).send();\n\t};\n\n\tstate->checkTimer.setCallback(checkUsername);\n\n\tconst auto usernameChanged = [=] {\n\t\tconst auto value = username->getLastText().trimmed();\n\t\tif (value.isEmpty()) {\n\t\t\tsetError(QString());\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto len = int(value.size());\n\t\tfor (auto i = 0; i < len; ++i) {\n\t\t\tconst auto ch = value.at(i);\n\t\t\tif ((ch < 'A' || ch > 'Z')\n\t\t\t\t&& (ch < 'a' || ch > 'z')\n\t\t\t\t&& (ch < '0' || ch > '9')\n\t\t\t\t&& ch != '_') {\n\t\t\t\tsetError(\n\t\t\t\t\ttr::lng_create_bot_username_bad_symbols(tr::now));\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tif ((value + botSuffixText).size() < Ui::EditPeer::kMinUsernameLength) {\n\t\t\tsetError(tr::lng_create_bot_username_too_short(tr::now));\n\t\t\treturn;\n\t\t}\n\n\t\tshowLinkInfo();\n\t\tstate->checkTimer.callOnce(Ui::EditPeer::kUsernameCheckTimeout);\n\t};\n\n\tconst auto submit = [=] {\n\t\tif (state->createRequestId) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto nameValue = name->getLastText().trimmed();\n\t\tif (nameValue.isEmpty()) {\n\t\t\tname->showError();\n\t\t\treturn;\n\t\t}\n\n\t\tauto rawUsername = username->getLastText().trimmed();\n\t\twhile (rawUsername.startsWith(botPrefixText)) {\n\t\t\trawUsername = rawUsername.mid(botPrefixText.size());\n\t\t}\n\t\tconst auto usernameValue = rawUsername + botSuffixText;\n\t\tif (rawUsername.isEmpty()) {\n\t\t\tusername->showError();\n\t\t\treturn;\n\t\t}\n\n\t\tif (!state->errorText.isEmpty() || state->goodText.isEmpty()) {\n\t\t\tusername->showError();\n\t\t\treturn;\n\t\t}\n\n\t\tusing Flag = MTPbots_CreateBot::Flag;\n\t\tconst auto flags = viaDeeplink ? Flag::f_via_deeplink : Flag(0);\n\t\tconst auto weak = base::make_weak(box);\n\t\tstate->createRequestId = api->request(MTPbots_CreateBot(\n\t\t\tMTP_flags(flags),\n\t\t\tMTP_string(nameValue),\n\t\t\tMTP_string(usernameValue),\n\t\t\tmanager->inputUser()\n\t\t)).done([=](const MTPUser &result) {\n\t\t\tstate->createRequestId = 0;\n\t\t\tconst auto user = session->data().processUser(result);\n\t\t\tstate->created = true;\n\t\t\tif (done) {\n\t\t\t\tdone(user);\n\t\t\t}\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tstate->createRequestId = 0;\n\t\t\tconst auto type = error.type();\n\t\t\tif (type == u\"USERNAME_OCCUPIED\"_q\n\t\t\t\t|| type == u\"USERNAME_INVALID\"_q) {\n\t\t\t\tstate->hadOccupiedError = true;\n\t\t\t\tusername->showError();\n\t\t\t\tsetError(tr::lng_create_bot_username_taken(tr::now));\n\t\t\t} else if (type == u\"BOT_CREATE_LIMIT_EXCEEDED\"_q) {\n\t\t\t\tconst auto limits = Data::PremiumLimits(session);\n\t\t\t\tconst auto premium = session->premium();\n\t\t\t\tconst auto premiumPossible = session->premiumPossible();\n\t\t\t\tconst auto defaultLimit = limits.botsCreateDefault();\n\t\t\t\tconst auto premiumLimit = limits.botsCreatePremium();\n\t\t\t\tconst auto current = premium ? premiumLimit : defaultLimit;\n\t\t\t\tconst auto bot = tr::link(\n\t\t\t\t\tu\"@BotFather\"_q,\n\t\t\t\t\tu\"https://t.me/botfather?start=deletebot\"_q);\n\t\t\t\tif (premium || !premiumPossible) {\n\t\t\t\t\tusing WeakToast = base::weak_ptr<Ui::Toast::Instance>;\n\t\t\t\t\tconst auto toast = std::make_shared<WeakToast>();\n\t\t\t\t\t(*toast) = show->showToast({\n\t\t\t\t\t\t.text = tr::lng_bots_create_limit_final(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\tcurrent,\n\t\t\t\t\t\t\tlt_bot,\n\t\t\t\t\t\t\ttr::bold(bot),\n\t\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t\t.filter = crl::guard(session, [=](\n\t\t\t\t\t\t\t\tconst ClickHandlerPtr &,\n\t\t\t\t\t\t\t\tQt::MouseButton button) {\n\t\t\t\t\t\t\tif (button == Qt::LeftButton) {\n\t\t\t\t\t\t\t\tif (const auto strong = toast->get()) {\n\t\t\t\t\t\t\t\t\tstrong->hideAnimated();\n\t\t\t\t\t\t\t\t\t(*toast) = nullptr;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}),\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tSettings::ShowPremiumPromoToast(\n\t\t\t\t\t\tshow,\n\t\t\t\t\t\tChatHelpers::ResolveWindowDefault(),\n\t\t\t\t\t\ttr::lng_bots_create_limit(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\tcurrent,\n\t\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\t\ttr::bold(\n\t\t\t\t\t\t\t\ttr::lng_bots_create_limit_link(tr::now, tr::link)),\n\t\t\t\t\t\t\tlt_premium_count,\n\t\t\t\t\t\t\ttr::bold(QString::number(premiumLimit)),\n\t\t\t\t\t\t\tlt_bot,\n\t\t\t\t\t\t\ttr::bold(bot),\n\t\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t\tu\"managed_bots\"_q);\n\t\t\t\t}\n\t\t\t} else if (MTP::IsFloodError(error)) {\n\t\t\t\tshow->showToast(tr::lng_flood_error(tr::now));\n\t\t\t} else {\n\t\t\t\tshow->showToast(type);\n\t\t\t}\n\t\t}).handleFloodErrors().send();\n\t};\n\n\tQObject::connect(username, &Ui::UsernameInput::changed, [=] {\n\t\tusernameChanged();\n\t\tupdatePositions();\n\t});\n\n\tname->submits(\n\t) | rpl::on_next([=](auto) {\n\t\tusername->setFocus();\n\t}, name->lifetime());\n\n\tQObject::connect(username, &Ui::UsernameInput::submitted, submit);\n\n\tbox->addButton(tr::lng_create_bot_button(), submit);\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\n\tif (!descriptor.suggestedUsername.isEmpty()) {\n\t\tusernameChanged();\n\t}\n\n\tbox->boxClosing() | rpl::on_next([=] {\n\t\tif (!state->created && cancelled) {\n\t\t\tcancelled();\n\t\t}\n\t}, box->lifetime());\n}\n\nvoid ShowCreateManagedBotBox(CreateManagedBotDescriptor &&descriptor) {\n\tconst auto show = descriptor.show;\n\tshow->showBox(Box(CreateManagedBotBox, std::move(descriptor)));\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/create_managed_bot_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass UserData;\n\nnamespace Main {\nclass SessionShow;\n} // namespace Main\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nstruct CreateManagedBotDescriptor {\n\tstd::shared_ptr<Main::SessionShow> show;\n\tnot_null<UserData*> manager;\n\tQString suggestedName;\n\tQString suggestedUsername;\n\tbool viaDeeplink = false;\n\tFn<void(not_null<UserData*>)> done;\n\tFn<void()> cancelled;\n};\n\nvoid CreateManagedBotBox(\n\tnot_null<Ui::GenericBox*> box,\n\tCreateManagedBotDescriptor &&descriptor);\n\nvoid ShowCreateManagedBotBox(CreateManagedBotDescriptor &&descriptor);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_contact_box.h\"\n\n#include \"api/api_peer_photo.h\"\n#include \"api/api_text_entities.h\"\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"boxes/peers/edit_peer_common.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_document.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"editor/photo_editor_common.h\"\n#include \"editor/photo_editor_layer_widget.h\"\n#include \"history/view/controls/history_view_characters_limit.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"info/userpic/info_userpic_emoji_builder_common.h\"\n#include \"info/userpic/info_userpic_emoji_builder_menu_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_common.h\"\n#include \"lottie/lottie_frame_generator.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/animated_icon.h\"\n#include \"ui/controls/emoji_button_factory.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/format_values.h\" // Ui::FormatPhone\n#include \"ui/text/text_entity.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/painter.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_widgets.h\"\n\n#include <QtGui/QClipboard>\n#include <QtGui/QGuiApplication>\n\nnamespace {\n\nconstexpr auto kAnimationStartFrame = 0;\nconstexpr auto kAnimationEndFrame = 21;\n\nQString UserPhone(not_null<UserData*> user) {\n\tconst auto phone = user->phone();\n\treturn phone.isEmpty()\n\t\t? user->owner().findContactPhone(peerToUser(user->id))\n\t\t: phone;\n}\n\nvoid SendRequest(\n\t\tbase::weak_qptr<Ui::GenericBox> box,\n\t\tnot_null<UserData*> user,\n\t\tbool sharePhone,\n\t\tconst QString &first,\n\t\tconst QString &last,\n\t\tconst QString &phone,\n\t\tconst TextWithEntities &note) {\n\tconst auto wasContact = user->isContact();\n\tusing Flag = MTPcontacts_AddContact::Flag;\n\tuser->session().api().request(MTPcontacts_AddContact(\n\t\tMTP_flags(Flag::f_note\n\t\t\t| (sharePhone ? Flag::f_add_phone_privacy_exception : Flag(0))),\n\t\tuser->inputUser(),\n\t\tMTP_string(first),\n\t\tMTP_string(last),\n\t\tMTP_string(phone),\n\t\tnote.text.isEmpty()\n\t\t\t? MTPTextWithEntities()\n\t\t\t: MTP_textWithEntities(\n\t\t\t\tMTP_string(note.text),\n\t\t\t\tApi::EntitiesToMTP(&user->session(), note.entities))\n\t)).done([=](const MTPUpdates &result) {\n\t\tuser->setName(\n\t\t\tfirst,\n\t\t\tlast,\n\t\t\tuser->nameOrPhone,\n\t\t\tuser->username());\n\t\tuser->session().api().applyUpdates(result);\n\t\tif (const auto settings = user->barSettings()) {\n\t\t\tconst auto flags = PeerBarSetting::AddContact\n\t\t\t\t| PeerBarSetting::BlockContact\n\t\t\t\t| PeerBarSetting::ReportSpam;\n\t\t\tuser->setBarSettings(*settings & ~flags);\n\t\t}\n\t\tif (box) {\n\t\t\tif (!wasContact) {\n\t\t\t\tbox->showToast(\n\t\t\t\t\ttr::lng_new_contact_add_done(tr::now, lt_user, first));\n\t\t\t}\n\t\t\tbox->closeBox();\n\t\t}\n\t}).send();\n}\n\nclass Cover final : public Ui::FixedHeightWidget {\npublic:\n\tCover(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> user,\n\t\trpl::producer<QString> status);\n\nprivate:\n\tvoid setupChildGeometry();\n\tvoid initViewers(rpl::producer<QString> status);\n\tvoid refreshNameGeometry(int newWidth);\n\tvoid refreshStatusGeometry(int newWidth);\n\n\tconst style::InfoProfileCover &_st;\n\tconst not_null<UserData*> _user;\n\n\tobject_ptr<Ui::UserpicButton> _userpic;\n\tobject_ptr<Ui::FlatLabel> _name = { nullptr };\n\tobject_ptr<Ui::FlatLabel> _status = { nullptr };\n\n};\n\nCover::Cover(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<UserData*> user,\n\trpl::producer<QString> status)\n: FixedHeightWidget(parent, st::infoEditContactCover.height)\n, _st(st::infoEditContactCover)\n, _user(user)\n, _userpic(\n\t\tthis,\n\t\tcontroller,\n\t\t_user,\n\t\tUi::UserpicButton::Role::OpenPhoto,\n\t\tUi::UserpicButton::Source::PeerPhoto,\n\t\t_st.photo) {\n\t_userpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t_name = object_ptr<Ui::FlatLabel>(this, _st.name);\n\t_name->setSelectable(true);\n\t_name->setContextCopyText(tr::lng_profile_copy_fullname(tr::now));\n\n\t_status = object_ptr<Ui::FlatLabel>(this, _st.status);\n\t_status->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tinitViewers(std::move(status));\n\tsetupChildGeometry();\n}\n\nvoid Cover::setupChildGeometry() {\n\twidthValue(\n\t) | rpl::on_next([this](int newWidth) {\n\t\t_userpic->moveToLeft(_st.photoLeft, _st.photoTop, newWidth);\n\t\trefreshNameGeometry(newWidth);\n\t\trefreshStatusGeometry(newWidth);\n\t}, lifetime());\n}\n\nvoid Cover::initViewers(rpl::producer<QString> status) {\n\tInfo::Profile::NameValue(\n\t\t_user\n\t) | rpl::on_next([=](const QString &name) {\n\t\t_name->setText(name);\n\t\trefreshNameGeometry(width());\n\t}, lifetime());\n\n\tstd::move(\n\t\tstatus\n\t) | rpl::on_next([=](const QString &status) {\n\t\t_status->setText(status);\n\t\trefreshStatusGeometry(width());\n\t}, lifetime());\n}\n\nvoid Cover::refreshNameGeometry(int newWidth) {\n\tauto nameWidth = newWidth - _st.nameLeft - _st.rightSkip;\n\t_name->resizeToNaturalWidth(nameWidth);\n\t_name->moveToLeft(_st.nameLeft, _st.nameTop, newWidth);\n}\n\nvoid Cover::refreshStatusGeometry(int newWidth) {\n\tauto statusWidth = newWidth - _st.statusLeft - _st.rightSkip;\n\t_status->resizeToNaturalWidth(statusWidth);\n\t_status->moveToLeft(_st.statusLeft, _st.statusTop, newWidth);\n}\n\nclass Controller {\npublic:\n\tController(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<UserData*> user,\n\t\tbool focusOnNotes = false);\n\n\tvoid prepare();\n\nprivate:\n\tvoid setupContent();\n\tvoid setupCover();\n\tvoid setupNameFields();\n\tvoid setupNotesField();\n\tvoid setupPhotoButtons();\n\tvoid setupDeleteContactButton();\n\tvoid setupWarning();\n\tvoid setupSharePhoneNumber();\n\tvoid initNameFields(\n\t\tnot_null<Ui::InputField*> first,\n\t\tnot_null<Ui::InputField*> last,\n\t\tbool inverted);\n\tvoid showPhotoMenu(bool suggest);\n\tvoid choosePhotoFile(bool suggest);\n\tvoid processChosenPhoto(QImage &&image, bool suggest);\n\tvoid processChosenPhotoWithMarkup(\n\t\tUserpicBuilder::Result &&data,\n\t\tbool suggest);\n\tvoid executeWithDelay(\n\t\tFn<void()> callback,\n\t\tbool suggest,\n\t\tbool startAnimation = true);\n\tvoid finishIconAnimation(bool suggest);\n\n\tnot_null<Ui::GenericBox*> _box;\n\tnot_null<Window::SessionController*> _window;\n\tnot_null<UserData*> _user;\n\tbool _focusOnNotes = false;\n\tUi::Checkbox *_sharePhone = nullptr;\n\tUi::InputField *_notesField = nullptr;\n\tUi::InputField *_firstNameField = nullptr;\n\tbase::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;\n\tbase::unique_qptr<Ui::PopupMenu> _photoMenu;\n\tstd::unique_ptr<Ui::AnimatedIcon> _suggestIcon;\n\tstd::unique_ptr<Ui::AnimatedIcon> _cameraIcon;\n\tUi::RpWidget *_suggestIconWidget = nullptr;\n\tUi::RpWidget *_cameraIconWidget = nullptr;\n\tQString _phone;\n\tFn<void()> _focus;\n\tFn<void()> _save;\n\n};\n\nController::Controller(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> window,\n\tnot_null<UserData*> user,\n\tbool focusOnNotes)\n: _box(box)\n, _window(window)\n, _user(user)\n, _focusOnNotes(focusOnNotes)\n, _phone(UserPhone(user)) {\n}\n\nvoid Controller::prepare() {\n\tsetupContent();\n\n\t_box->setTitle(_user->isContact()\n\t\t? tr::lng_edit_contact_title()\n\t\t: tr::lng_enter_contact_data());\n\n\t_box->addButton(tr::lng_box_done(), _save);\n\t_box->addButton(tr::lng_cancel(), [=] { _box->closeBox(); });\n\t_box->setFocusCallback(_focus);\n}\n\nvoid Controller::setupContent() {\n\tsetupCover();\n\tsetupNameFields();\n\tsetupNotesField();\n\tsetupPhotoButtons();\n\tsetupDeleteContactButton();\n\tsetupWarning();\n\tsetupSharePhoneNumber();\n}\n\nvoid Controller::setupCover() {\n\t_box->addRow(\n\t\tobject_ptr<Cover>(\n\t\t\t_box,\n\t\t\t_window,\n\t\t\t_user,\n\t\t\t(_phone.isEmpty()\n\t\t\t\t? tr::lng_contact_mobile_hidden()\n\t\t\t\t: rpl::single(Ui::FormatPhone(_phone)))),\n\t\tstyle::margins());\n}\n\nvoid Controller::setupNameFields() {\n\tconst auto inverted = langFirstNameGoesSecond();\n\t_firstNameField = _box->addRow(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\t_box,\n\t\t\tst::defaultInputField,\n\t\t\ttr::lng_signup_firstname(),\n\t\t\t_user->firstName),\n\t\tst::addContactFieldMargin);\n\tconst auto first = _firstNameField;\n\tauto preparedLast = object_ptr<Ui::InputField>(\n\t\t_box,\n\t\tst::defaultInputField,\n\t\ttr::lng_signup_lastname(),\n\t\t_user->lastName);\n\tconst auto last = inverted\n\t\t? _box->insertRow(\n\t\t\t_box->rowsCount() - 1,\n\t\t\tstd::move(preparedLast),\n\t\t\tst::addContactFieldMargin)\n\t\t: _box->addRow(std::move(preparedLast), st::addContactFieldMargin);\n\n\tinitNameFields(first, last, inverted);\n}\n\nvoid Controller::initNameFields(\n\t\tnot_null<Ui::InputField*> first,\n\t\tnot_null<Ui::InputField*> last,\n\t\tbool inverted) {\n\tconst auto getValue = [](not_null<Ui::InputField*> field) {\n\t\treturn TextUtilities::SingleLine(field->getLastText()).trimmed();\n\t};\n\n\tif (inverted) {\n\t\t_box->setTabOrder(last, first);\n\t}\n\t_focus = [=] {\n\t\tif (_focusOnNotes && _notesField) {\n\t\t\t_notesField->setFocusFast();\n\t\t\t_notesField->setCursorPosition(_notesField->getLastText().size());\n\t\t\treturn;\n\t\t}\n\t\tconst auto firstValue = getValue(first);\n\t\tconst auto lastValue = getValue(last);\n\t\tconst auto empty = firstValue.isEmpty() && lastValue.isEmpty();\n\t\tconst auto focusFirst = (inverted != empty);\n\t\t(focusFirst ? first : last)->setFocusFast();\n\t};\n\t_save = [=] {\n\t\tconst auto firstValue = getValue(first);\n\t\tconst auto lastValue = getValue(last);\n\t\tconst auto empty = firstValue.isEmpty() && lastValue.isEmpty();\n\t\tif (empty) {\n\t\t\t_focus();\n\t\t\t(inverted ? last : first)->showError();\n\t\t\treturn;\n\t\t}\n\n\t\tif (_notesField) {\n\t\t\tconst auto limit = Data::PremiumLimits(\n\t\t\t\t&_user->session()).contactNoteLengthCurrent();\n\t\t\tconst auto remove = Ui::ComputeFieldCharacterCount(_notesField)\n\t\t\t\t- limit;\n\t\t\tif (remove > 0) {\n\t\t\t\t_box->showToast(tr::lng_contact_notes_limit_reached(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tremove));\n\t\t\t\t_notesField->setFocus();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tconst auto noteValue = _notesField\n\t\t\t? [&] {\n\t\t\t\tauto textWithTags = _notesField->getTextWithAppliedMarkdown();\n\t\t\t\treturn TextWithEntities{\n\t\t\t\t\tbase::take(textWithTags.text),\n\t\t\t\t\tTextUtilities::ConvertTextTagsToEntities(\n\t\t\t\t\t\tbase::take(textWithTags.tags)),\n\t\t\t\t};\n\t\t\t}()\n\t\t\t: TextWithEntities();\n\t\tSendRequest(\n\t\t\tbase::make_weak(_box),\n\t\t\t_user,\n\t\t\t_sharePhone && _sharePhone->checked(),\n\t\t\tfirstValue,\n\t\t\tlastValue,\n\t\t\t_phone,\n\t\t\tnoteValue);\n\t};\n\tconst auto submit = [=] {\n\t\tconst auto firstValue = first->getLastText().trimmed();\n\t\tconst auto lastValue = last->getLastText().trimmed();\n\t\tconst auto empty = firstValue.isEmpty() && lastValue.isEmpty();\n\t\tif (inverted ? last->hasFocus() : empty) {\n\t\t\tfirst->setFocus();\n\t\t} else if (inverted ? empty : first->hasFocus()) {\n\t\t\tlast->setFocus();\n\t\t} else {\n\t\t\t_save();\n\t\t}\n\t};\n\tfirst->submits() | rpl::on_next(submit, first->lifetime());\n\tlast->submits() | rpl::on_next(submit, last->lifetime());\n\tfirst->setMaxLength(Ui::EditPeer::kMaxUserFirstLastName);\n\tfirst->setMaxLength(Ui::EditPeer::kMaxUserFirstLastName);\n}\n\nvoid Controller::setupWarning() {\n\tif (_user->isContact() || !_phone.isEmpty()) {\n\t\treturn;\n\t}\n\t_box->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_box,\n\t\t\ttr::lng_contact_phone_after(tr::now, lt_user, _user->shortName()),\n\t\t\tst::changePhoneLabel),\n\t\tst::addContactWarningMargin);\n}\n\nvoid Controller::setupNotesField() {\n\tUi::AddSkip(_box->verticalLayout());\n\tUi::AddDivider(_box->verticalLayout());\n\tUi::AddSkip(_box->verticalLayout());\n\t_notesField = _box->addRow(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\t_box,\n\t\t\tst::notesFieldWithEmoji,\n\t\t\tUi::InputField::Mode::MultiLine,\n\t\t\ttr::lng_contact_add_notes(),\n\t\t\tQString()),\n\t\tst::addContactFieldMargin);\n\t_notesField->setMarkdownSet(Ui::MarkdownSet::Notes);\n\t_notesField->setCustomTextContext(Core::TextContext({\n\t\t.session = &_user->session()\n\t}));\n\t_notesField->setTextWithTags({\n\t\t_user->note().text,\n\t\tTextUtilities::ConvertEntitiesToTextTags(_user->note().entities)\n\t});\n\n\t_notesField->setMarkdownReplacesEnabled(rpl::single(\n\t\tUi::MarkdownEnabledState{\n\t\t\tUi::MarkdownEnabled{\n\t\t\t\t{\n\t\t\t\t\tUi::InputField::kTagBold,\n\t\t\t\t\tUi::InputField::kTagItalic,\n\t\t\t\t\tUi::InputField::kTagUnderline,\n\t\t\t\t\tUi::InputField::kTagStrikeOut,\n\t\t\t\t\tUi::InputField::kTagSpoiler\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t));\n\n\tconst auto container = _box->getDelegate()->outerContainer();\n\tusing Selector = ChatHelpers::TabbedSelector;\n\t_emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(\n\t\tcontainer,\n\t\t_window,\n\t\tobject_ptr<Selector>(\n\t\t\tnullptr,\n\t\t\t_window->uiShow(),\n\t\t\tWindow::GifPauseReason::Layer,\n\t\t\tSelector::Mode::EmojiOnly));\n\t_emojiPanel->setDesiredHeightValues(\n\t\t1.,\n\t\tst::emojiPanMinHeight / 2,\n\t\tst::emojiPanMinHeight);\n\t_emojiPanel->hide();\n\t_emojiPanel->selector()->setCurrentPeer(_window->session().user());\n\t_emojiPanel->selector()->emojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\tUi::InsertEmojiAtCursor(_notesField->textCursor(), data.emoji);\n\t}, _notesField->lifetime());\n\t_emojiPanel->selector()->customEmojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\tconst auto info = data.document->sticker();\n\t\tif (info\n\t\t\t&& info->setType == Data::StickersType::Emoji\n\t\t\t&& !_window->session().premium()) {\n\t\t\tShowPremiumPreviewBox(\n\t\t\t\t_window,\n\t\t\t\tPremiumFeature::AnimatedEmoji);\n\t\t} else {\n\t\t\tData::InsertCustomEmoji(_notesField, data.document);\n\t\t}\n\t}, _notesField->lifetime());\n\n\tconst auto emojiButton = Ui::AddEmojiToggleToField(\n\t\t_notesField,\n\t\t_box,\n\t\t_window,\n\t\t_emojiPanel.get(),\n\t\tst::sendGifWithCaptionEmojiPosition,\n\t\tfalse);\n\temojiButton->show();\n\n\tusing Limit = HistoryView::Controls::CharactersLimitLabel;\n\tstruct LimitState {\n\t\tbase::unique_qptr<Limit> charsLimitation;\n\t};\n\tconst auto limitState = _notesField->lifetime().make_state<LimitState>();\n\n\tconst auto checkCharsLimitation = [=, w = _notesField->window()] {\n\t\tconst auto limit = Data::PremiumLimits(\n\t\t\t&_user->session()).contactNoteLengthCurrent();\n\t\tconst auto remove = Ui::ComputeFieldCharacterCount(_notesField)\n\t\t\t- limit;\n\t\tif (!limitState->charsLimitation) {\n\t\t\tconst auto border = _notesField->st().borderActive;\n\t\t\tlimitState->charsLimitation = base::make_unique_q<Limit>(\n\t\t\t\t_box->verticalLayout(),\n\t\t\t\temojiButton,\n\t\t\t\tstyle::al_top,\n\t\t\t\tQMargins{ 0, -border - _notesField->st().border, 0, 0 });\n\t\t\trpl::combine(\n\t\t\t\tlimitState->charsLimitation->geometryValue(),\n\t\t\t\t_notesField->geometryValue()\n\t\t\t) | rpl::on_next([=](QRect limit, QRect field) {\n\t\t\t\tlimitState->charsLimitation->setVisible(\n\t\t\t\t\t(w->mapToGlobal(limit.bottomLeft()).y() - border)\n\t\t\t\t\t\t< w->mapToGlobal(field.bottomLeft()).y());\n\t\t\t\tlimitState->charsLimitation->raise();\n\t\t\t}, limitState->charsLimitation->lifetime());\n\t\t}\n\t\tlimitState->charsLimitation->setLeft(remove);\n\t};\n\n\t_notesField->changes() | rpl::on_next([=] {\n\t\tcheckCharsLimitation();\n\t}, _notesField->lifetime());\n\n\tUi::AddDividerText(\n\t\t_box->verticalLayout(),\n\t\ttr::lng_contact_add_notes_about());\n}\n\nvoid Controller::setupPhotoButtons() {\n\tif (!_user->isContact()) {\n\t\treturn;\n\t}\n\tconst auto iconPlaceholder = st::restoreUserpicIcon.size * 2;\n\tauto nameValue = _firstNameField\n\t\t? rpl::merge(\n\t\t\trpl::single(_firstNameField->getLastText().trimmed()),\n\t\t\t_firstNameField->changes() | rpl::map([=] {\n\t\t\t\treturn _firstNameField->getLastText().trimmed();\n\t\t})) | rpl::map([=](const QString &text) {\n\t\t\treturn text.isEmpty() ? Ui::kQEllipsis : text;\n\t\t})\n\t\t: rpl::single(_user->shortName()) | rpl::type_erased;\n\tconst auto inner = _box->verticalLayout();\n\tUi::AddSkip(inner);\n\n\tconst auto suggestBirthdayWrap = inner->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tinner,\n\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\n\tconst auto suggestBirthdayButton = Settings::AddButtonWithIcon(\n\t\tsuggestBirthdayWrap->entity(),\n\t\ttr::lng_suggest_birthday(),\n\t\tst::settingsButtonLight,\n\t\t{ &st::editContactSuggestBirthday });\n\tsuggestBirthdayButton->setClickedCallback([=] {\n\t\tCore::App().openInternalUrl(\n\t\t\tu\"internal:edit_birthday:suggest:%1\"_q.arg(\n\t\t\t\tpeerToUser(_user->id).bare),\n\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.sessionWindow = base::make_weak(_window),\n\t\t\t}));\n\t});\n\tsuggestBirthdayWrap->toggleOn(rpl::single(!_user->birthday().valid()\n\t\t&& !_user->starsPerMessageChecked()));\n\n\t_suggestIcon = Ui::MakeAnimatedIcon({\n\t\t.generator = [] {\n\t\t\treturn std::make_unique<Lottie::FrameGenerator>(\n\t\t\t\tLottie::ReadContent(\n\t\t\t\t\tQByteArray(),\n\t\t\t\t\tu\":/animations/photo_suggest_icon.tgs\"_q));\n\t\t},\n\t\t.sizeOverride = iconPlaceholder,\n\t\t.colorized = true,\n\t});\n\n\t_cameraIcon = Ui::MakeAnimatedIcon({\n\t\t.generator = [] {\n\t\t\treturn std::make_unique<Lottie::FrameGenerator>(\n\t\t\t\tLottie::ReadContent(\n\t\t\t\t\tQByteArray(),\n\t\t\t\t\tu\":/animations/camera_outline.tgs\"_q));\n\t\t},\n\t\t.sizeOverride = iconPlaceholder,\n\t\t.colorized = true,\n\t});\n\n\tconst auto suggestButtonWrap = inner->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tinner,\n\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\tsuggestButtonWrap->toggleOn(\n\t\trpl::single(!_user->starsPerMessageChecked()));\n\n\tconst auto suggestButton = Settings::AddButtonWithIcon(\n\t\tsuggestButtonWrap->entity(),\n\t\ttr::lng_suggest_photo_for(lt_user, rpl::duplicate(nameValue)),\n\t\tst::settingsButtonLight,\n\t\t{ nullptr });\n\n\t_suggestIconWidget = Ui::CreateChild<Ui::RpWidget>(suggestButton);\n\t_suggestIconWidget->resize(iconPlaceholder);\n\t_suggestIconWidget->paintRequest() | rpl::on_next([=] {\n\t\tif (_suggestIcon && _suggestIcon->valid()) {\n\t\t\tauto p = QPainter(_suggestIconWidget);\n\t\t\tconst auto frame = _suggestIcon->frame(st::lightButtonFg->c);\n\t\t\tp.drawImage(_suggestIconWidget->rect(), frame);\n\t\t}\n\t}, _suggestIconWidget->lifetime());\n\n\tsuggestButton->sizeValue() | rpl::on_next([=](QSize size) {\n\t\t_suggestIconWidget->move(\n\t\t\tst::settingsButtonLight.iconLeft - iconPlaceholder.width() / 4,\n\t\t\t(size.height() - _suggestIconWidget->height()) / 2);\n\t}, _suggestIconWidget->lifetime());\n\n\tsuggestButton->setClickedCallback([=] {\n\t\tif (_suggestIcon && _suggestIcon->valid()) {\n\t\t\t_suggestIcon->setCustomStartFrame(kAnimationStartFrame);\n\t\t\t_suggestIcon->setCustomEndFrame(kAnimationEndFrame);\n\t\t\t_suggestIcon->jumpToStart([=] { _suggestIconWidget->update(); });\n\t\t\t_suggestIcon->animate([=] { _suggestIconWidget->update(); });\n\t\t}\n\t\tshowPhotoMenu(true);\n\t});\n\n\tconst auto setButton = Settings::AddButtonWithIcon(\n\t\tinner,\n\t\ttr::lng_set_photo_for_user(lt_user, rpl::duplicate(nameValue)),\n\t\tst::settingsButtonLight,\n\t\t{ nullptr });\n\n\t_cameraIconWidget = Ui::CreateChild<Ui::RpWidget>(setButton);\n\t_cameraIconWidget->resize(iconPlaceholder);\n\t_cameraIconWidget->paintRequest() | rpl::on_next([=] {\n\t\tif (_cameraIcon && _cameraIcon->valid()) {\n\t\t\tauto p = QPainter(_cameraIconWidget);\n\t\t\tconst auto frame = _cameraIcon->frame(st::lightButtonFg->c);\n\t\t\tp.drawImage(_cameraIconWidget->rect(), frame);\n\t\t}\n\t}, _cameraIconWidget->lifetime());\n\n\tsetButton->sizeValue() | rpl::on_next([=](QSize size) {\n\t\t_cameraIconWidget->move(\n\t\t\tst::settingsButtonLight.iconLeft - iconPlaceholder.width() / 4,\n\t\t\t(size.height() - _cameraIconWidget->height()) / 2);\n\t}, _cameraIconWidget->lifetime());\n\n\tsetButton->setClickedCallback([=] {\n\t\tif (_cameraIcon && _cameraIcon->valid()) {\n\t\t\t_cameraIcon->setCustomStartFrame(kAnimationStartFrame);\n\t\t\t_cameraIcon->setCustomEndFrame(kAnimationEndFrame);\n\t\t\t_cameraIcon->jumpToStart([=] { _cameraIconWidget->update(); });\n\t\t\t_cameraIcon->animate([=] { _cameraIconWidget->update(); });\n\t\t}\n\t\tshowPhotoMenu(false);\n\t});\n\n\tconst auto resetButtonWrap = inner->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tinner,\n\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\n\tconst auto resetButton = Settings::AddButtonWithIcon(\n\t\tresetButtonWrap->entity(),\n\t\ttr::lng_profile_photo_reset(),\n\t\tst::settingsButtonLight,\n\t\t{ nullptr });\n\n\tconst auto userpicButton = Ui::CreateChild<Ui::UserpicButton>(\n\t\tresetButton,\n\t\t_window,\n\t\t_user,\n\t\tUi::UserpicButton::Role::Custom,\n\t\tUi::UserpicButton::Source::NonPersonalIfHasPersonal,\n\t\tst::restoreUserpicIcon);\n\tuserpicButton->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tresetButton->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tuserpicButton->move(\n\t\t\tst::settingsButtonLight.iconLeft,\n\t\t\t(size.height() - userpicButton->height()) / 2);\n\t}, userpicButton->lifetime());\n\tresetButtonWrap->toggleOn(\n\t\t_user->session().changes().peerFlagsValue(\n\t\t\t_user,\n\t\t\tData::PeerUpdate::Flag::FullInfo | Data::PeerUpdate::Flag::Photo\n\t\t) | rpl::map([=] {\n\t\t\treturn _user->hasPersonalPhoto();\n\t\t}) | rpl::distinct_until_changed());\n\n\tresetButton->setClickedCallback([=] {\n\t\t_window->show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_profile_photo_reset_sure(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\t_user->shortName()),\n\t\t\t.confirmed = [=](Fn<void()> close) {\n\t\t\t\t_window->session().api().peerPhoto().clearPersonal(_user);\n\t\t\t\tclose();\n\t\t\t},\n\t\t\t.confirmText = tr::lng_profile_photo_reset_button(tr::now),\n\t\t}));\n\t});\n\n\tUi::AddSkip(inner);\n\n\tUi::AddDividerText(\n\t\tinner,\n\t\ttr::lng_contact_photo_replace_info(lt_user, std::move(nameValue)));\n\tUi::AddSkip(inner);\n}\n\nvoid Controller::setupDeleteContactButton() {\n\tif (!_user->isContact()) {\n\t\treturn;\n\t}\n\tconst auto inner = _box->verticalLayout();\n\tconst auto deleteButton = Settings::AddButtonWithIcon(\n\t\tinner,\n\t\ttr::lng_info_delete_contact(),\n\t\tst::settingsAttentionButton,\n\t\t{ nullptr });\n\tdeleteButton->setClickedCallback([=] {\n\t\tconst auto text = tr::lng_sure_delete_contact(\n\t\t\ttr::now,\n\t\t\tlt_contact,\n\t\t\t_user->name());\n\t\tconst auto deleteSure = [=](Fn<void()> &&close) {\n\t\t\tclose();\n\t\t\t_user->session().api().request(MTPcontacts_DeleteContacts(\n\t\t\t\tMTP_vector<MTPInputUser>(1, _user->inputUser())\n\t\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t\t_user->session().api().applyUpdates(result);\n\t\t\t\t_box->closeBox();\n\t\t\t}).send();\n\t\t};\n\t\t_window->show(Ui::MakeConfirmBox({\n\t\t\t.text = text,\n\t\t\t.confirmed = deleteSure,\n\t\t\t.confirmText = tr::lng_box_delete(),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t}));\n\t});\n\tUi::AddSkip(inner);\n}\n\nvoid Controller::setupSharePhoneNumber() {\n\tconst auto settings = _user->barSettings();\n\tif (!settings\n\t\t|| !((*settings) & PeerBarSetting::NeedContactsException)) {\n\t\treturn;\n\t}\n\t_sharePhone = _box->addRow(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t_box,\n\t\t\ttr::lng_contact_share_phone(tr::now),\n\t\t\ttrue,\n\t\t\tst::defaultBoxCheckbox),\n\t\tst::addContactWarningMargin);\n\t_box->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_box,\n\t\t\ttr::lng_contact_phone_will_be_shared(tr::now, lt_user, _user->shortName()),\n\t\t\tst::changePhoneLabel),\n\t\tst::addContactWarningMargin);\n\n}\n\nvoid Controller::showPhotoMenu(bool suggest) {\n\t_photoMenu = base::make_unique_q<Ui::PopupMenu>(\n\t\t_box,\n\t\tst::popupMenuWithIcons);\n\n\tQObject::connect(_photoMenu.get(), &QObject::destroyed, [=] {\n\t\tfinishIconAnimation(suggest);\n\t});\n\n\t_photoMenu->addAction(\n\t\ttr::lng_attach_photo(tr::now),\n\t\t[=] { executeWithDelay([=] { choosePhotoFile(suggest); }, suggest); },\n\t\t&st::menuIconPhoto);\n\n\tif (const auto data = QGuiApplication::clipboard()->mimeData()) {\n\t\tif (data->hasImage()) {\n\t\t\tauto callback = [=] {\n\t\t\t\tEditor::PrepareProfilePhoto(\n\t\t\t\t\t_box,\n\t\t\t\t\t&_window->window(),\n\t\t\t\t\tEditor::EditorData{\n\t\t\t\t\t\t.about = (suggest\n\t\t\t\t\t\t\t? tr::lng_profile_suggest_sure(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\t\t\ttr::bold(_user->shortName()),\n\t\t\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t\t\t: tr::lng_profile_set_personal_sure(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\t\t\ttr::bold(_user->shortName()),\n\t\t\t\t\t\t\t\ttr::marked)),\n\t\t\t\t\t\t.confirm = (suggest\n\t\t\t\t\t\t\t? tr::lng_profile_suggest_button(tr::now)\n\t\t\t\t\t\t\t: tr::lng_profile_set_photo_button(tr::now)),\n\t\t\t\t\t\t.cropType = Editor::EditorData::CropType::Ellipse,\n\t\t\t\t\t\t.keepAspectRatio = true,\n\t\t\t\t\t},\n\t\t\t\t\t[=](QImage &&editedImage) {\n\t\t\t\t\t\tprocessChosenPhoto(std::move(editedImage), suggest);\n\t\t\t\t\t},\n\t\t\t\t\tqvariant_cast<QImage>(data->imageData()));\n\t\t\t};\n\t\t\t_photoMenu->addAction(\n\t\t\t\ttr::lng_profile_photo_from_clipboard(tr::now),\n\t\t\t\t[=] { executeWithDelay(callback, suggest); },\n\t\t\t\t&st::menuIconPhoto);\n\t\t}\n\t}\n\n\tUserpicBuilder::AddEmojiBuilderAction(\n\t\t_window,\n\t\t_photoMenu.get(),\n\t\t_window->session().api().peerPhoto().emojiListValue(\n\t\t\tApi::PeerPhoto::EmojiListType::Profile),\n\t\t[=](UserpicBuilder::Result data) {\n\t\t\tprocessChosenPhotoWithMarkup(std::move(data), suggest);\n\t\t},\n\t\tfalse);\n\n\t_photoMenu->popup(QCursor::pos());\n}\n\nvoid Controller::choosePhotoFile(bool suggest) {\n\tEditor::PrepareProfilePhotoFromFile(\n\t\t_box,\n\t\t&_window->window(),\n\t\tEditor::EditorData{\n\t\t\t.about = (suggest\n\t\t\t\t? tr::lng_profile_suggest_sure(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::bold(_user->shortName()),\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_profile_set_personal_sure(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::bold(_user->shortName()),\n\t\t\t\t\ttr::marked)),\n\t\t\t.confirm = (suggest\n\t\t\t\t? tr::lng_profile_suggest_button(tr::now)\n\t\t\t\t: tr::lng_profile_set_photo_button(tr::now)),\n\t\t\t.cropType = Editor::EditorData::CropType::Ellipse,\n\t\t\t.keepAspectRatio = true,\n\t\t},\n\t\t[=](QImage &&image) {\n\t\t\tprocessChosenPhoto(std::move(image), suggest);\n\t\t});\n}\n\nvoid Controller::processChosenPhoto(QImage &&image, bool suggest) {\n\tApi::PeerPhoto::UserPhoto photo{\n\t\t.image = base::duplicate(image),\n\t};\n\tif (suggest) {\n\t\t_window->session().api().peerPhoto().suggest(_user, std::move(photo));\n\t\t_window->showPeerHistory(_user->id);\n\t} else {\n\t\t_window->session().api().peerPhoto().upload(_user, std::move(photo));\n\t}\n}\n\nvoid Controller::processChosenPhotoWithMarkup(\n\t\tUserpicBuilder::Result &&data,\n\t\tbool suggest) {\n\tApi::PeerPhoto::UserPhoto photo{\n\t\t.image = std::move(data.image),\n\t\t.markupDocumentId = data.id,\n\t\t.markupColors = std::move(data.colors),\n\t};\n\tif (suggest) {\n\t\t_window->session().api().peerPhoto().suggest(_user, std::move(photo));\n\t\t_window->showPeerHistory(_user->id);\n\t} else {\n\t\t_window->session().api().peerPhoto().upload(_user, std::move(photo));\n\t}\n}\n\nvoid Controller::finishIconAnimation(bool suggest) {\n\tconst auto icon = suggest ? _suggestIcon.get() : _cameraIcon.get();\n\tconst auto widget = suggest ? _suggestIconWidget : _cameraIconWidget;\n\tif (icon && icon->valid()) {\n\t\ticon->setCustomStartFrame(icon->frameIndex());\n\t\ticon->setCustomEndFrame(-1);\n\t\ticon->animate([=] { widget->update(); });\n\t}\n}\n\nvoid Controller::executeWithDelay(\n\t\tFn<void()> callback,\n\t\tbool suggest,\n\t\tbool startAnimation) {\n\tconst auto icon = suggest ? _suggestIcon.get() : _cameraIcon.get();\n\tconst auto widget = suggest ? _suggestIconWidget : _cameraIconWidget;\n\n\tif (startAnimation && icon && icon->valid()) {\n\t\ticon->setCustomStartFrame(icon->frameIndex());\n\t\ticon->setCustomEndFrame(-1);\n\t\ticon->animate([=] { widget->update(); });\n\t}\n\n\tif (icon && icon->valid() && icon->animating()) {\n\t\tbase::call_delayed(50, [=] {\n\t\t\texecuteWithDelay(callback, suggest, false);\n\t\t});\n\t} else {\n\t\tcallback();\n\t}\n}\n\n} // namespace\n\nvoid EditContactBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<UserData*> user) {\n\tbox->setWidth(st::boxWideWidth);\n\tbox->lifetime().make_state<Controller>(box, window, user)->prepare();\n}\n\nvoid EditContactNoteBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<UserData*> user) {\n\tbox->setWidth(st::boxWideWidth);\n\tbox->lifetime().make_state<Controller>(\n\t\tbox,\n\t\twindow,\n\t\tuser,\n\t\ttrue)->prepare();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_contact_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n\nclass UserData;\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nvoid EditContactBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> window,\n\tnot_null<UserData*> user);\n\nvoid EditContactNoteBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> window,\n\tnot_null<UserData*> user);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_discussion_link_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_discussion_link_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"settings/settings_common.h\" // AddButton.\n#include \"data/data_changes.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/text/text_utilities.h\" // tr::rich\n#include \"boxes/peer_list_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/add_contact_box.h\"\n#include \"apiwrap.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_settings.h\"\n\nnamespace {\n\nconstexpr auto kEnableSearchRowsCount = 10;\n\nclass Controller : public PeerListController, public base::has_weak_ptr {\npublic:\n\tController(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChannelData*> channel,\n\t\tChannelData *chat,\n\t\tconst std::vector<not_null<PeerData*>> &chats,\n\t\tFn<void(ChannelData*)> callback,\n\t\tFn<void(not_null<PeerData*>)> showHistoryCallback);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tint contentWidth() const override;\n\nprivate:\n\tvoid choose(not_null<ChannelData*> chat);\n\tvoid choose(not_null<ChatData*> chat);\n\n\tnot_null<Window::SessionNavigation*> _navigation;\n\tnot_null<ChannelData*> _channel;\n\tChannelData *_chat = nullptr;\n\tstd::vector<not_null<PeerData*>> _chats;\n\tFn<void(ChannelData*)> _callback;\n\tFn<void(not_null<PeerData*>)> _showHistoryCallback;\n\n\tChannelData *_waitForFull = nullptr;\n\n\trpl::event_stream<not_null<PeerData*>> _showHistoryRequest;\n};\n\nController::Controller(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<ChannelData*> channel,\n\tChannelData *chat,\n\tconst std::vector<not_null<PeerData*>> &chats,\n\tFn<void(ChannelData*)> callback,\n\tFn<void(not_null<PeerData*>)> showHistoryCallback)\n: _navigation(navigation)\n, _channel(channel)\n, _chat(chat)\n, _chats(std::move(chats))\n, _callback(std::move(callback))\n, _showHistoryCallback(std::move(showHistoryCallback)) {\n\tchannel->session().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::FullInfo\n\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\treturn (update.peer == _waitForFull);\n\t}) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tchoose(std::exchange(_waitForFull, nullptr));\n\t}, lifetime());\n}\n\nMain::Session &Controller::session() const {\n\treturn _channel->session();\n}\n\nint Controller::contentWidth() const {\n\treturn st::boxWidth;\n}\n\nvoid Controller::prepare() {\n\tconst auto appendRow = [&](not_null<PeerData*> chat) {\n\t\tif (delegate()->peerListFindRow(chat->id.value)) {\n\t\t\treturn;\n\t\t}\n\t\tauto row = std::make_unique<PeerListRow>(chat);\n\t\tconst auto username = chat->username();\n\t\trow->setCustomStatus(!username.isEmpty()\n\t\t\t? ('@' + username)\n\t\t\t: (chat->isChannel() && !chat->isMegagroup())\n\t\t\t? tr::lng_manage_linked_channel_private_status(tr::now)\n\t\t\t: tr::lng_manage_discussion_group_private_status(tr::now));\n\t\tdelegate()->peerListAppendRow(std::move(row));\n\t};\n\tif (_chat) {\n\t\tappendRow(_chat);\n\t} else {\n\t\tfor (const auto &chat : _chats) {\n\t\t\tappendRow(chat);\n\t\t}\n\t\tif (_chats.size() >= kEnableSearchRowsCount) {\n\t\t\tdelegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);\n\t\t}\n\t}\n}\n\nvoid Controller::rowClicked(not_null<PeerListRow*> row) {\n\tif (_chat != nullptr) {\n\t\t_showHistoryCallback(_chat);\n\t\treturn;\n\t}\n\tconst auto peer = row->peer();\n\tif (const auto channel = peer->asChannel()) {\n\t\tif (channel->wasFullUpdated()) {\n\t\t\tchoose(channel);\n\t\t\treturn;\n\t\t}\n\t\t_waitForFull = channel;\n\t\tchannel->updateFull();\n\t} else if (const auto chat = peer->asChat()) {\n\t\tchoose(chat);\n\t}\n}\n\nvoid Controller::choose(not_null<ChannelData*> chat) {\n\tif (chat->isForum()) {\n\t\tShowForumForDiscussionError(_navigation);\n\t\treturn;\n\t}\n\tauto text = tr::lng_manage_discussion_group_sure(\n\t\ttr::now,\n\t\tlt_group,\n\t\ttr::bold(chat->name()),\n\t\tlt_channel,\n\t\ttr::bold(_channel->name()),\n\t\ttr::marked);\n\tif (!_channel->isPublic()) {\n\t\ttext.append(\n\t\t\t\"\\n\\n\" + tr::lng_manage_linked_channel_private(tr::now));\n\t}\n\tif (!chat->isPublic()) {\n\t\ttext.append(\n\t\t\t\"\\n\\n\" + tr::lng_manage_discussion_group_private(tr::now));\n\t\tif (chat->hiddenPreHistory()) {\n\t\t\ttext.append(\"\\n\\n\");\n\t\t\ttext.append(tr::lng_manage_discussion_group_warning(\n\t\t\t\ttr::now,\n\t\t\t\ttr::rich));\n\t\t}\n\t}\n\tconst auto sure = [=](Fn<void()> &&close) {\n\t\tclose();\n\t\tconst auto onstack = _callback;\n\t\tonstack(chat);\n\t};\n\tdelegate()->peerListUiShow()->showBox(Ui::MakeConfirmBox({\n\t\t.text = text,\n\t\t.confirmed = sure,\n\t\t.confirmText = tr::lng_manage_discussion_group_link(tr::now),\n\t}));\n}\n\nvoid Controller::choose(not_null<ChatData*> chat) {\n\tauto text = tr::lng_manage_discussion_group_sure(\n\t\ttr::now,\n\t\tlt_group,\n\t\ttr::bold(chat->name()),\n\t\tlt_channel,\n\t\ttr::bold(_channel->name()),\n\t\ttr::marked);\n\tif (!_channel->isPublic()) {\n\t\ttext.append(\"\\n\\n\" + tr::lng_manage_linked_channel_private(tr::now));\n\t}\n\ttext.append(\"\\n\\n\" + tr::lng_manage_discussion_group_private(tr::now));\n\ttext.append(\"\\n\\n\");\n\ttext.append(tr::lng_manage_discussion_group_warning(\n\t\ttr::now,\n\t\ttr::rich));\n\tconst auto sure = [=](Fn<void()> &&close) {\n\t\tclose();\n\t\tconst auto done = [=](not_null<ChannelData*> chat) {\n\t\t\tconst auto onstack = _callback;\n\t\t\tonstack(chat);\n\t\t};\n\t\tchat->session().api().migrateChat(chat, crl::guard(this, done));\n\t};\n\tdelegate()->peerListUiShow()->showBox(Ui::MakeConfirmBox({\n\t\t.text = text,\n\t\t.confirmed = sure,\n\t\t.confirmText = tr::lng_manage_discussion_group_link(tr::now),\n\t}));\n}\n\n[[nodiscard]] rpl::producer<TextWithEntities> About(\n\t\tnot_null<ChannelData*> channel,\n\t\tChannelData *chat) {\n\tif (!channel->isBroadcast()) {\n\t\treturn tr::lng_manage_linked_channel_about(\n\t\t\tlt_channel,\n\t\t\trpl::single(tr::bold(chat->name())),\n\t\t\ttr::marked);\n\t} else if (chat != nullptr) {\n\t\treturn tr::lng_manage_discussion_group_about_chosen(\n\t\t\tlt_group,\n\t\t\trpl::single(tr::bold(chat->name())),\n\t\t\ttr::marked);\n\t}\n\treturn tr::lng_manage_discussion_group_about(tr::marked);\n}\n\n[[nodiscard]] object_ptr<Ui::BoxContent> EditDiscussionLinkBox(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChannelData*> channel,\n\t\tChannelData *chat,\n\t\tstd::vector<not_null<PeerData*>> &&chats,\n\t\tbool canEdit,\n\t\tFn<void(ChannelData*)> callback) {\n\tExpects((channel->isBroadcast() && canEdit) || (chat != nullptr));\n\n\tclass ListBox final : public PeerListBox {\n\tpublic:\n\t\tListBox(\n\t\t\tQWidget *parent,\n\t\t\tstd::unique_ptr<PeerListController> controller,\n\t\t\tFn<void(not_null<ListBox*>)> init)\n\t\t: PeerListBox(\n\t\t\tparent,\n\t\t\tstd::move(controller),\n\t\t\t[=](not_null<PeerListBox*>) { init(this); }) {\n\t\t}\n\n\t\tvoid showFinished() override {\n\t\t\t_showFinished.fire({});\n\t\t}\n\n\t\trpl::producer<> showFinishes() const {\n\t\t\treturn _showFinished.events();\n\t\t}\n\n\tprivate:\n\t\trpl::event_stream<> _showFinished;\n\n\t};\n\n\tconst auto init = [=](not_null<ListBox*> box) {\n\t\tauto above = object_ptr<Ui::VerticalLayout>(box);\n\t\tSettings::AddDividerTextWithLottie(above, {\n\t\t\t.lottie = u\"discussion\"_q,\n\t\t\t.showFinished = box->showFinishes(),\n\t\t\t.about = About(channel, chat),\n\t\t});\n\t\tif (!chat) {\n\t\t\tAssert(channel->isBroadcast());\n\n\t\t\tUi::AddSkip(above);\n\t\t\tSettings::AddButtonWithIcon(\n\t\t\t\tabove,\n\t\t\t\ttr::lng_manage_discussion_group_create(),\n\t\t\t\tst::infoCreateDiscussionLinkButton,\n\t\t\t\t{ &st::menuBlueIconGroupCreate }\n\t\t\t)->addClickHandler([=, parent = above.data()] {\n\t\t\t\tconst auto guarded = crl::guard(parent, callback);\n\t\t\t\tnavigation->uiShow()->showBox(Box<GroupInfoBox>(\n\t\t\t\t\tnavigation,\n\t\t\t\t\tGroupInfoBox::Type::Megagroup,\n\t\t\t\t\tchannel->name() + \" Chat\",\n\t\t\t\t\tguarded));\n\t\t\t});\n\t\t}\n\t\tbox->peerListSetAboveWidget(std::move(above));\n\n\t\tauto below = object_ptr<Ui::VerticalLayout>(box);\n\t\tif (chat && canEdit) {\n\t\t\tSettings::AddButtonWithIcon(\n\t\t\t\tbelow,\n\t\t\t\t(channel->isBroadcast()\n\t\t\t\t\t? tr::lng_manage_discussion_group_unlink\n\t\t\t\t\t: tr::lng_manage_linked_channel_unlink)(),\n\t\t\t\tst::infoUnlinkDiscussionLinkButton,\n\t\t\t\t{ &st::menuIconRemoveAttention }\n\t\t\t)->addClickHandler([=] { callback(nullptr); });\n\t\t}\n\t\tUi::AddSkip(below);\n\t\tUi::AddDividerText(\n\t\t\tbelow,\n\t\t\t(channel->isBroadcast()\n\t\t\t\t? tr::lng_manage_discussion_group_posted\n\t\t\t\t: tr::lng_manage_linked_channel_posted)());\n\t\tbox->peerListSetBelowWidget(std::move(below));\n\n\t\tbox->setTitle(channel->isBroadcast()\n\t\t\t? tr::lng_manage_discussion_group()\n\t\t\t: tr::lng_manage_linked_channel());\n\t\tbox->addButton(tr::lng_close(), [=] { box->closeBox(); });\n\t};\n\tauto showHistoryCallback = [=](not_null<PeerData*> peer) {\n\t\tnavigation->showPeerHistory(\n\t\t\tpeer,\n\t\t\tWindow::SectionShow::Way::ClearStack,\n\t\t\tShowAtUnreadMsgId);\n\t};\n\tauto controller = std::make_unique<Controller>(\n\t\tnavigation,\n\t\tchannel,\n\t\tchat,\n\t\tstd::move(chats),\n\t\tstd::move(callback),\n\t\tstd::move(showHistoryCallback));\n\treturn Box<ListBox>(std::move(controller), init);\n}\n\n} // namespace\n\nobject_ptr<Ui::BoxContent> EditDiscussionLinkBox(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChannelData*> channel,\n\t\tstd::vector<not_null<PeerData*>> &&chats,\n\t\tFn<void(ChannelData*)> callback) {\n\treturn EditDiscussionLinkBox(\n\t\tnavigation,\n\t\tchannel,\n\t\tnullptr,\n\t\tstd::move(chats),\n\t\ttrue,\n\t\tcallback);\n}\n\nobject_ptr<Ui::BoxContent> EditDiscussionLinkBox(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<ChannelData*> chat,\n\t\tbool canEdit,\n\t\tFn<void(ChannelData*)> callback) {\n\treturn EditDiscussionLinkBox(\n\t\tnavigation,\n\t\tchannel,\n\t\tchat,\n\t\t{},\n\t\tcanEdit,\n\t\tcallback);\n}\n\nvoid ShowForumForDiscussionError(\n\t\tnot_null<Window::SessionNavigation*> navigation) {\n\tnavigation->showToast(\n\t\ttr::lng_forum_topics_no_discussion(\n\t\t\ttr::now,\n\t\t\ttr::rich));\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_discussion_link_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\nclass BoxContent;\n} // namespace Ui\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\n[[nodiscard]] object_ptr<Ui::BoxContent> EditDiscussionLinkBox(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<ChannelData*> channel,\n\tnot_null<ChannelData*> chat,\n\tbool canEdit,\n\tFn<void(ChannelData*)> callback);\n\n[[nodiscard]] object_ptr<Ui::BoxContent> EditDiscussionLinkBox(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<ChannelData*> channel,\n\tstd::vector<not_null<PeerData*>> &&chats,\n\tFn<void(ChannelData*)> callback);\n\nvoid ShowForumForDiscussionError(\n\tnot_null<Window::SessionNavigation*> navigation);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_forum_topic_box.h\"\n\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/effects/emoji_fly_animation.h\"\n#include \"ui/abstract_button.h\"\n#include \"ui/vertical_list.h\"\n#include \"data/data_document.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_icons.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"base/event_filter.h\"\n#include \"base/random.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"chat_helpers/emoji_list_widget.h\"\n#include \"chat_helpers/stickers_list_footer.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"main/main_session.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_chat_section.h\"\n#include \"history/view/history_view_sticker_toast.h\"\n#include \"lang/lang_keys.h\"\n#include \"info/profile/info_profile_emoji_status_panel.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"apiwrap.h\"\n#include \"mainwindow.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace {\n\nconstexpr auto kDefaultIconId = DocumentId(0x7FFF'FFFF'FFFF'FFFFULL);\n\nusing DefaultIcon = Data::TopicIconDescriptor;\n\nclass DefaultIconEmoji final : public Ui::Text::CustomEmoji {\npublic:\n\tDefaultIconEmoji(\n\t\trpl::producer<DefaultIcon> value,\n\t\tFn<void()> repaint,\n\t\tData::CustomEmojiSizeTag tag);\n\n\tint width() override;\n\tQString entityData() override;\n\n\tvoid paint(QPainter &p, const Context &context) override;\n\tvoid unload() override;\n\tbool ready() override;\n\tbool readyInDefaultState() override;\n\nprivate:\n\tDefaultIcon _icon = {};\n\tQImage _image;\n\tData::CustomEmojiSizeTag _tag = {};\n\n\trpl::lifetime _lifetime;\n\n};\n\nDefaultIconEmoji::DefaultIconEmoji(\n\trpl::producer<DefaultIcon> value,\n\tFn<void()> repaint,\n\tData::CustomEmojiSizeTag tag)\n: _tag(tag) {\n\tstd::move(value) | rpl::on_next([=](DefaultIcon value) {\n\t\t_icon = value;\n\t\t_image = QImage();\n\t\tif (repaint) {\n\t\t\trepaint();\n\t\t}\n\t}, _lifetime);\n}\n\nint DefaultIconEmoji::width() {\n\treturn st::emojiSize + 2 * st::emojiPadding;\n}\n\nQString DefaultIconEmoji::entityData() {\n\treturn u\"topic_icon:%1\"_q.arg(_icon.colorId);\n}\n\nvoid DefaultIconEmoji::paint(QPainter &p, const Context &context) {\n\tconst auto &st = (_tag == Data::CustomEmojiSizeTag::Normal)\n\t\t? st::normalForumTopicIcon\n\t\t: st::defaultForumTopicIcon;\n\tconst auto general = Data::IsForumGeneralIconTitle(_icon.title);\n\tif (_image.isNull()) {\n\t\t_image = general\n\t\t\t? Data::ForumTopicGeneralIconFrame(\n\t\t\t\tst.size,\n\t\t\t\tQColor(255, 255, 255))\n\t\t\t: Data::ForumTopicIconFrame(_icon.colorId, _icon.title, st);\n\t}\n\tconst auto full = (_tag == Data::CustomEmojiSizeTag::Normal)\n\t\t? Ui::Emoji::GetSizeNormal()\n\t\t: Ui::Emoji::GetSizeLarge();\n\tconst auto esize = full / style::DevicePixelRatio();\n\tconst auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);\n\tconst auto skip = (customSize - st.size) / 2;\n\tp.drawImage(context.position + QPoint(skip, skip), general\n\t\t? style::colorizeImage(_image, context.textColor)\n\t\t: _image);\n}\n\nvoid DefaultIconEmoji::unload() {\n\t_image = QImage();\n}\n\nbool DefaultIconEmoji::ready() {\n\treturn true;\n}\n\nbool DefaultIconEmoji::readyInDefaultState() {\n\treturn true;\n}\n\n[[nodiscard]] int EditIconSize() {\n\tconst auto tag = Data::CustomEmojiManager::SizeTag::Large;\n\treturn Data::FrameSizeFromTag(tag) / style::DevicePixelRatio();\n}\n\n[[nodiscard]] int32 ChooseNextColorId(\n\t\tint32 currentId,\n\t\tstd::vector<int32> &otherIds) {\n\tif (otherIds.size() == 1 && otherIds.front() == currentId) {\n\t\totherIds = Data::ForumTopicColorIds();\n\t}\n\tconst auto i = ranges::find(otherIds, currentId);\n\tif (i != end(otherIds)) {\n\t\totherIds.erase(i);\n\t}\n\treturn otherIds.empty()\n\t\t? currentId\n\t\t: otherIds[base::RandomIndex(otherIds.size())];\n}\n\n[[nodiscard]] not_null<Ui::AbstractButton*> EditIconButton(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\trpl::producer<DefaultIcon> defaultIcon,\n\t\trpl::producer<DocumentId> iconId,\n\t\tFn<bool(not_null<Ui::RpWidget*>)> paintIconFrame) {\n\tusing namespace Info::Profile;\n\tstruct State {\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji> icon;\n\t\tQImage defaultIcon;\n\t};\n\tconst auto tag = Data::CustomEmojiManager::SizeTag::Large;\n\tconst auto size = EditIconSize();\n\tconst auto result = Ui::CreateChild<Ui::AbstractButton>(parent.get());\n\tresult->show();\n\tconst auto state = result->lifetime().make_state<State>();\n\n\tstd::move(\n\t\ticonId\n\t) | rpl::on_next([=](DocumentId id) {\n\t\tconst auto owner = &controller->session().data();\n\t\tstate->icon = id\n\t\t\t? owner->customEmojiManager().create(\n\t\t\t\tid,\n\t\t\t\t[=] { result->update(); },\n\t\t\t\ttag)\n\t\t\t: nullptr;\n\t\tresult->update();\n\t}, result->lifetime());\n\n\tstd::move(\n\t\tdefaultIcon\n\t) | rpl::on_next([=](DefaultIcon icon) {\n\t\tstate->defaultIcon = Data::ForumTopicIconFrame(\n\t\t\ticon.colorId,\n\t\t\ticon.title,\n\t\t\tst::largeForumTopicIcon);\n\t\tresult->update();\n\t}, result->lifetime());\n\n\tresult->resize(size, size);\n\tresult->paintRequest(\n\t) | rpl::filter([=] {\n\t\treturn !paintIconFrame(result);\n\t}) | rpl::on_next([=](QRect clip) {\n\t\tauto args = Ui::Text::CustomEmoji::Context{\n\t\t\t.textColor = st::windowFg->c,\n\t\t\t.now = crl::now(),\n\t\t\t.paused = controller->isGifPausedAtLeastFor(\n\t\t\t\tWindow::GifPauseReason::Layer),\n\t\t};\n\t\tauto p = QPainter(result);\n\t\tif (state->icon) {\n\t\t\tstate->icon->paint(p, args);\n\t\t} else {\n\t\t\tconst auto skip = (size - st::largeForumTopicIcon.size) / 2;\n\t\t\tp.drawImage(skip, skip, state->defaultIcon);\n\t\t}\n\t}, result->lifetime());\n\n\treturn result;\n}\n\n[[nodiscard]] not_null<Ui::AbstractButton*> GeneralIconPreview(\n\t\tnot_null<QWidget*> parent) {\n\tusing namespace Info::Profile;\n\tstruct State {\n\t\tQImage frame;\n\t};\n\tconst auto size = EditIconSize();\n\tconst auto result = Ui::CreateChild<Ui::AbstractButton>(parent.get());\n\tresult->show();\n\tresult->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tconst auto state = result->lifetime().make_state<State>();\n\n\trpl::single(rpl::empty) | rpl::then(\n\t\tstyle::PaletteChanged()\n\t) | rpl::on_next([=] {\n\t\tstate->frame = Data::ForumTopicGeneralIconFrame(\n\t\t\tst::largeForumTopicIcon.size,\n\t\t\tst::windowSubTextFg->c);\n\t\tresult->update();\n\t}, result->lifetime());\n\n\tresult->resize(size, size);\n\tresult->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(result);\n\t\tconst auto skip = (size - st::largeForumTopicIcon.size) / 2;\n\t\tp.drawImage(skip, skip, state->frame);\n\t}, result->lifetime());\n\n\treturn result;\n}\n\nstruct IconSelector {\n\tFn<bool(not_null<Ui::RpWidget*>)> paintIconFrame;\n\trpl::producer<DocumentId> iconIdValue;\n};\n\n[[nodiscard]] IconSelector AddIconSelector(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Ui::RpWidget*> button,\n\t\tnot_null<Window::SessionController*> controller,\n\t\trpl::producer<DefaultIcon> defaultIcon,\n\t\trpl::producer<int> coverHeight,\n\t\tDocumentId iconId,\n\t\tFn<void(object_ptr<Ui::RpWidget>)> placeFooter) {\n\tusing namespace ChatHelpers;\n\n\tstruct State {\n\t\tstd::unique_ptr<Ui::EmojiFlyAnimation> animation;\n\t\tstd::unique_ptr<HistoryView::StickerToast> toast;\n\t\trpl::variable<DocumentId> iconId;\n\t\tQPointer<QWidget> button;\n\t};\n\tconst auto state = box->lifetime().make_state<State>(State{\n\t\t.iconId = iconId,\n\t\t.button = button.get(),\n\t});\n\n\tconst auto manager = &controller->session().data().customEmojiManager();\n\n\tauto factory = [=](DocumentId id, Fn<void()> repaint)\n\t-> std::unique_ptr<Ui::Text::CustomEmoji> {\n\t\tconst auto tag = Data::CustomEmojiManager::SizeTag::Large;\n\t\tif (id == kDefaultIconId) {\n\t\t\treturn std::make_unique<DefaultIconEmoji>(\n\t\t\t\trpl::duplicate(defaultIcon),\n\t\t\t\tstd::move(repaint),\n\t\t\t\ttag);\n\t\t}\n\t\treturn manager->create(id, std::move(repaint), tag);\n\t};\n\n\tconst auto icons = &controller->session().data().forumIcons();\n\tconst auto body = box->verticalLayout();\n\tconst auto recent = [=] {\n\t\tauto list = icons->list();\n\t\tlist.insert(begin(list), kDefaultIconId);\n\t\treturn list;\n\t};\n\tconst auto selector = body->add(\n\t\tobject_ptr<EmojiListWidget>(body, EmojiListDescriptor{\n\t\t\t.show = controller->uiShow(),\n\t\t\t.mode = EmojiListWidget::Mode::TopicIcon,\n\t\t\t.paused = Window::PausedIn(controller, PauseReason::Layer),\n\t\t\t.customRecentList = DocumentListToRecent(recent()),\n\t\t\t.customRecentFactory = std::move(factory),\n\t\t\t.st = &st::reactPanelEmojiPan,\n\t\t}),\n\t\tst::reactPanelEmojiPan.padding);\n\n\ticons->requestDefaultIfUnknown();\n\ticons->defaultUpdates(\n\t) | rpl::on_next([=] {\n\t\tselector->provideRecent(DocumentListToRecent(recent()));\n\t}, selector->lifetime());\n\n\tplaceFooter(selector->createFooter());\n\n\tconst auto shadow = Ui::CreateChild<Ui::PlainShadow>(box.get());\n\tshadow->show();\n\n\trpl::combine(\n\t\trpl::duplicate(coverHeight),\n\t\tselector->widthValue()\n\t) | rpl::on_next([=](int top, int width) {\n\t\tshadow->setGeometry(0, top, width, st::lineWidth);\n\t}, shadow->lifetime());\n\n\tselector->refreshEmoji();\n\n\tselector->scrollToRequests(\n\t) | rpl::on_next([=](int y) {\n\t\tbox->scrollToY(y);\n\t\tshadow->update();\n\t}, selector->lifetime());\n\n\trpl::combine(\n\t\tbox->heightValue(),\n\t\tstd::move(coverHeight),\n\t\trpl::mappers::_1 - rpl::mappers::_2\n\t) | rpl::on_next([=](int height) {\n\t\tselector->setMinimalHeight(selector->width(), height);\n\t}, body->lifetime());\n\n\tconst auto showToast = [=](not_null<DocumentData*> document) {\n\t\tif (!state->toast) {\n\t\t\tstate->toast = std::make_unique<HistoryView::StickerToast>(\n\t\t\t\tcontroller,\n\t\t\t\tcontroller->widget()->bodyWidget(),\n\t\t\t\t[=] { state->toast = nullptr; });\n\t\t}\n\t\tstate->toast->showFor(\n\t\t\tdocument,\n\t\t\tHistoryView::StickerToast::Section::TopicIcon);\n\t};\n\n\tselector->customChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\tconst auto owner = &controller->session().data();\n\t\tconst auto document = data.document;\n\t\tconst auto id = document->id;\n\t\tconst auto custom = (id != kDefaultIconId);\n\t\tconst auto premium = custom\n\t\t\t&& !ranges::contains(document->owner().forumIcons().list(), id);\n\t\tif (premium && !controller->session().premium()) {\n\t\t\tshowToast(document);\n\t\t\treturn;\n\t\t}\n\t\tconst auto body = controller->window().widget()->bodyWidget();\n\t\tif (state->button && custom) {\n\t\t\tconst auto &from = data.messageSendingFrom;\n\t\t\tauto args = Ui::ReactionFlyAnimationArgs{\n\t\t\t\t.id = { { id } },\n\t\t\t\t.flyIcon = from.frame,\n\t\t\t\t.flyFrom = body->mapFromGlobal(from.globalStartGeometry),\n\t\t\t};\n\t\t\tstate->animation = std::make_unique<Ui::EmojiFlyAnimation>(\n\t\t\t\tbody,\n\t\t\t\t&owner->reactions(),\n\t\t\t\tstd::move(args),\n\t\t\t\t[=] { state->animation->repaint(); },\n\t\t\t\t[] { return st::windowFg->c; },\n\t\t\t\tData::CustomEmojiSizeTag::Large);\n\t\t}\n\t\tstate->iconId = id;\n\t}, selector->lifetime());\n\n\tauto paintIconFrame = [=](not_null<Ui::RpWidget*> button) {\n\t\tif (!state->animation) {\n\t\t\treturn false;\n\t\t} else if (state->animation->paintBadgeFrame(button)) {\n\t\t\treturn true;\n\t\t}\n\t\tInvokeQueued(state->animation->layer(), [=] {\n\t\t\tstate->animation = nullptr;\n\t\t});\n\t\treturn false;\n\t};\n\treturn {\n\t\t.paintIconFrame = std::move(paintIconFrame),\n\t\t.iconIdValue = state->iconId.value(),\n\t};\n}\n\n} // namespace\n\nvoid NewForumTopicBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<History*> forum) {\n\tEditForumTopicBox(box, controller, forum, MsgId(0));\n}\n\nvoid EditForumTopicBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<History*> forum,\n\t\tMsgId rootId) {\n\tconst auto creating = !rootId;\n\tconst auto topic = (!creating && forum->peer->forum())\n\t\t? forum->peer->forum()->topicFor(rootId)\n\t\t: nullptr;\n\tconst auto bot = forum->peer->isBot();\n\tconst auto created = topic && !topic->creating();\n\tbox->setTitle(creating\n\t\t? tr::lng_forum_topic_new()\n\t\t: bot\n\t\t? tr::lng_bot_thread_edit()\n\t\t: tr::lng_forum_topic_edit());\n\n\tbox->setMaxHeight(st::editTopicMaxHeight);\n\n\tstruct State {\n\t\trpl::variable<DefaultIcon> defaultIcon;\n\t\trpl::variable<DocumentId> iconId = 0;\n\t\tstd::vector<int32> otherColorIds;\n\t\tmtpRequestId requestId = 0;\n\t\tFn<bool(not_null<Ui::RpWidget*>)> paintIconFrame;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tconst auto &colors = Data::ForumTopicColorIds();\n\tstate->iconId = topic ? topic->iconId() : 0;\n\tstate->otherColorIds = colors;\n\tstate->defaultIcon = DefaultIcon{\n\t\ttopic ? topic->title() : QString(),\n\t\ttopic ? topic->colorId() : ChooseNextColorId(0, state->otherColorIds)\n\t};\n\n\tconst auto top = box->setPinnedToTopContent(\n\t\tobject_ptr<Ui::VerticalLayout>(box));\n\n\tconst auto title = top->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tbox,\n\t\t\tst::defaultInputField,\n\t\t\t(bot\n\t\t\t\t? tr::lng_bot_thread_title()\n\t\t\t\t: tr::lng_forum_topic_title()),\n\t\t\ttopic ? topic->title() : QString()),\n\t\tst::editTopicTitleMargin);\n\tbox->setFocusCallback([=] {\n\t\ttitle->setFocusFast();\n\t});\n\n\tconst auto paintIconFrame = [=](not_null<Ui::RpWidget*> widget) {\n\t\treturn state->paintIconFrame && state->paintIconFrame(widget);\n\t};\n\tconst auto icon = (topic && topic->isGeneral())\n\t\t? GeneralIconPreview(title->parentWidget())\n\t\t: EditIconButton(\n\t\t\ttitle->parentWidget(),\n\t\t\tcontroller,\n\t\t\tstate->defaultIcon.value(),\n\t\t\tstate->iconId.value(),\n\t\t\tpaintIconFrame);\n\n\ttitle->geometryValue(\n\t) | rpl::on_next([=](QRect geometry) {\n\t\ticon->move(\n\t\t\tst::editTopicIconPosition.x(),\n\t\t\tst::editTopicIconPosition.y());\n\t}, icon->lifetime());\n\n\tstate->iconId.value(\n\t) | rpl::on_next([=](DocumentId iconId) {\n\t\ticon->setAttribute(\n\t\t\tQt::WA_TransparentForMouseEvents,\n\t\t\tcreated || (iconId != 0));\n\t}, box->lifetime());\n\n\ticon->setClickedCallback([=] {\n\t\tconst auto current = state->defaultIcon.current();\n\t\tstate->defaultIcon = DefaultIcon{\n\t\t\tcurrent.title,\n\t\t\tChooseNextColorId(current.colorId, state->otherColorIds),\n\t\t};\n\t});\n\ttitle->changes(\n\t) | rpl::on_next([=] {\n\t\tstate->defaultIcon = DefaultIcon{\n\t\t\ttitle->getLastText().trimmed(),\n\t\t\tstate->defaultIcon.current().colorId,\n\t\t};\n\t}, title->lifetime());\n\ttitle->submits() | rpl::on_next([box] {\n\t\tbox->triggerButton(0);\n\t}, title->lifetime());\n\n\tif (!topic || !topic->isGeneral()) {\n\t\tUi::AddDividerText(top, bot\n\t\t\t? tr::lng_bot_thread_choose_title_and_icon()\n\t\t\t: tr::lng_forum_choose_title_and_icon());\n\n\t\tbox->setScrollStyle(st::reactPanelScroll);\n\n\t\tauto selector = AddIconSelector(\n\t\t\tbox,\n\t\t\ticon,\n\t\t\tcontroller,\n\t\t\tstate->defaultIcon.value(),\n\t\t\ttop->heightValue(),\n\t\t\tstate->iconId.current(),\n\t\t\t[&](object_ptr<Ui::RpWidget> footer) {\n\t\t\t\ttop->add(std::move(footer)); });\n\t\tstate->paintIconFrame = std::move(selector.paintIconFrame);\n\t\tstd::move(\n\t\t\tselector.iconIdValue\n\t\t) | rpl::on_next([=](DocumentId iconId) {\n\t\t\tstate->iconId = (iconId != kDefaultIconId) ? iconId : 0;\n\t\t}, box->lifetime());\n\t}\n\n\tconst auto create = [=] {\n\t\tif (!forum->peer->isForum()) {\n\t\t\tbox->closeBox();\n\t\t\treturn;\n\t\t} else if (title->getLastText().trimmed().isEmpty()) {\n\t\t\ttitle->showError();\n\t\t\treturn;\n\t\t}\n\t\tusing namespace HistoryView;\n\t\tcontroller->showSection(\n\t\t\tstd::make_shared<ChatMemento>(ChatViewId{\n\t\t\t\t.history = forum,\n\t\t\t\t.repliesRootId = forum->peer->forum()->reserveCreatingId(\n\t\t\t\t\ttitle->getLastText().trimmed(),\n\t\t\t\t\tstate->defaultIcon.current().colorId,\n\t\t\t\t\tstate->iconId.current()),\n\t\t\t}),\n\t\t\tWindow::SectionShow::Way::ClearStack);\n\t};\n\n\tconst auto save = [=] {\n\t\tconst auto parent = forum->peer->forum();\n\t\tconst auto topic = parent\n\t\t\t? parent->topicFor(rootId)\n\t\t\t: nullptr;\n\t\tif (!topic) {\n\t\t\tbox->closeBox();\n\t\t\treturn;\n\t\t} else if (state->requestId > 0) {\n\t\t\treturn;\n\t\t} else if (title->getLastText().trimmed().isEmpty()) {\n\t\t\ttitle->showError();\n\t\t\treturn;\n\t\t} else if (parent->creating(rootId)) {\n\t\t\ttopic->applyTitle(title->getLastText().trimmed());\n\t\t\ttopic->applyColorId(state->defaultIcon.current().colorId);\n\t\t\ttopic->applyIconId(state->iconId.current());\n\t\t\tbox->closeBox();\n\t\t} else {\n\t\t\tusing Flag = MTPmessages_EditForumTopic::Flag;\n\t\t\tconst auto api = &forum->session().api();\n\t\t\tconst auto weak = base::make_weak(box);\n\t\t\tstate->requestId = api->request(MTPmessages_EditForumTopic(\n\t\t\t\tMTP_flags(Flag::f_title\n\t\t\t\t\t| (topic->isGeneral() ? Flag() : Flag::f_icon_emoji_id)),\n\t\t\t\ttopic->peer()->input(),\n\t\t\t\tMTP_int(rootId),\n\t\t\t\tMTP_string(title->getLastText().trimmed()),\n\t\t\t\tMTP_long(state->iconId.current()),\n\t\t\t\tMTPBool(), // closed\n\t\t\t\tMTPBool() // hidden\n\t\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t\tapi->applyUpdates(result);\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->closeBox();\n\t\t\t\t}\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tif (error.type() == u\"TOPIC_NOT_MODIFIED\") {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t} else {\n\t\t\t\t\t\tstate->requestId = -1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}).send();\n\t\t}\n\t};\n\n\tif (creating) {\n\t\tbox->addButton(tr::lng_create_group_create(), create);\n\t} else {\n\t\tbox->addButton(tr::lng_settings_save(), save);\n\t}\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nstd::unique_ptr<Ui::Text::CustomEmoji> MakeTopicIconEmoji(\n\t\tData::TopicIconDescriptor descriptor,\n\t\tFn<void()> repaint,\n\t\tData::CustomEmojiSizeTag tag) {\n\treturn std::make_unique<DefaultIconEmoji>(\n\t\trpl::single(descriptor),\n\t\tstd::move(repaint),\n\t\ttag);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n\nclass History;\n\nnamespace Data {\nstruct TopicIconDescriptor;\nenum class CustomEmojiSizeTag : uchar;\n} // namespace Data\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nvoid NewForumTopicBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<History*> forum);\n\nvoid EditForumTopicBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<History*> forum,\n\tMsgId rootId);\n\n[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> MakeTopicIconEmoji(\n\tData::TopicIconDescriptor descriptor,\n\tFn<void()> repaint,\n\tData::CustomEmojiSizeTag tag);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_members_visible.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_members_visible.h\"\n\n#include \"boxes/peers/edit_peer_info_box.h\"\n#include \"data/data_channel.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/vertical_list.h\"\n#include \"settings/settings_common.h\" // IconDescriptor.\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace {\n\n[[nodiscard]] int EnableHideMembersMin(not_null<ChannelData*> channel) {\n\treturn channel->session().appConfig().get<int>(\n\t\tu\"hidden_members_group_size_min\"_q,\n\t\t100);\n}\n\n} // namespace\n\n[[nodiscard]] object_ptr<Ui::RpWidget> CreateMembersVisibleButton(\n\t\tnot_null<ChannelData*> megagroup) {\n\tconst auto min = EnableHideMembersMin(megagroup);\n\tconst auto showHideMembers = megagroup->canBanMembers()\n\t\t&& megagroup->membersCount() >= min;\n\tif (!showHideMembers) {\n\t\treturn { nullptr };\n\t}\n\n\tauto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tconst auto container = result.data();\n\n\tif (showHideMembers) {\n\t\tstruct State {\n\t\t\trpl::event_stream<bool> toggled;\n\t\t};\n\t\tUi::AddSkip(container);\n\t\tconst auto state = container->lifetime().make_state<State>();\n\t\tconst auto button = container->add(\n\t\t\tEditPeerInfoBox::CreateButton(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_profile_hide_participants(),\n\t\t\t\trpl::single(QString()),\n\t\t\t\t[] {},\n\t\t\t\tst::manageGroupNoIconButton,\n\t\t\t\t{}\n\t\t))->toggleOn(rpl::single(\n\t\t\t(megagroup->flags() & ChannelDataFlag::ParticipantsHidden) != 0\n\t\t) | rpl::then(state->toggled.events()));\n\t\tUi::AddSkip(container);\n\t\tUi::AddDividerText(\n\t\t\tcontainer,\n\t\t\ttr::lng_profile_hide_participants_about());\n\n\t\tbutton->toggledValue(\n\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\tmegagroup->session().api().request(\n\t\t\t\tMTPchannels_ToggleParticipantsHidden(\n\t\t\t\t\tmegagroup->inputChannel(),\n\t\t\t\t\tMTP_bool(toggled)\n\t\t\t\t)\n\t\t\t).done([=](const MTPUpdates &result) {\n\t\t\t\tmegagroup->session().api().applyUpdates(result);\n\t\t\t}).send();\n\t\t}, button->lifetime());\n\t}\n\n\treturn result;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_members_visible.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nclass ChannelData;\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\n[[nodiscard]] object_ptr<Ui::RpWidget> CreateMembersVisibleButton(\n\tnot_null<ChannelData*> megagroup);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_participant_box.h\"\n\n#include \"boxes/peers/edit_tag_control.h\"\n#include \"boxes/peers/edit_participants_box.h\"\n#include \"history/view/history_view_message.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/box_content_divider.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/painter.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"settings/sections/settings_privacy_security.h\"\n#include \"ui/boxes/choose_date_time.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/peers/channel_ownership_transfer.h\"\n#include \"boxes/peers/add_bot_to_chat_box.h\"\n#include \"boxes/peers/edit_peer_permissions_box.h\"\n#include \"boxes/peers/edit_peer_info_box.h\"\n#include \"settings/settings_common.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"base/unixtime.h\"\n#include \"apiwrap.h\"\n#include \"main/main_session.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_settings.h\"\n\nnamespace {\n\nconstexpr auto kMaxRestrictDelayDays = 366;\nconstexpr auto kSecondsInDay = 24 * 60 * 60;\nconstexpr auto kSecondsInWeek = 7 * kSecondsInDay;\n\nclass Cover final : public Ui::FixedHeightWidget {\npublic:\n\tCover(QWidget *parent, not_null<UserData*> user, bool hasAdminRights);\n\nprivate:\n\tvoid setupChildGeometry();\n\n\tconst style::InfoProfileCover &_st;\n\tconst not_null<UserData*> _user;\n\tobject_ptr<Ui::UserpicButton> _userpic;\n\tobject_ptr<Ui::FlatLabel> _name = { nullptr };\n\tobject_ptr<Ui::FlatLabel> _status = { nullptr };\n\n};\n\nCover::Cover(\n\tQWidget *parent,\n\tnot_null<UserData*> user,\n\tbool hasAdminRights)\n: FixedHeightWidget(parent, st::infoEditContactCover.height)\n, _st(st::infoEditContactCover)\n, _user(user)\n, _userpic(this, _user, _st.photo) {\n\t_userpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t_name = object_ptr<Ui::FlatLabel>(this, _st.name);\n\t_name->setText(_user->name());\n\n\tconst auto statusText = [&] {\n\t\tif (_user->isBot()) {\n\t\t\tconst auto seesAllMessages = _user->botInfo->readsAllHistory\n\t\t\t\t|| hasAdminRights;\n\t\t\treturn (seesAllMessages\n\t\t\t\t? tr::lng_status_bot_reads_all\n\t\t\t\t: tr::lng_status_bot_not_reads_all)(tr::now);\n\t\t}\n\t\treturn Data::OnlineText(_user->lastseen(), base::unixtime::now());\n\t}();\n\t_status = object_ptr<Ui::FlatLabel>(this, _st.status);\n\t_status->setText(statusText);\n\t_status->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tsetupChildGeometry();\n}\n\nvoid Cover::setupChildGeometry() {\n\twidthValue(\n\t) | rpl::on_next([this](int newWidth) {\n\t\t_userpic->moveToLeft(_st.photoLeft, _st.photoTop, newWidth);\n\t\tauto nameWidth = newWidth - _st.nameLeft - _st.rightSkip;\n\t\t_name->resizeToNaturalWidth(nameWidth);\n\t\t_name->moveToLeft(_st.nameLeft, _st.nameTop, newWidth);\n\t\tauto statusWidth = newWidth - _st.statusLeft - _st.rightSkip;\n\t\t_status->resizeToNaturalWidth(statusWidth);\n\t\t_status->moveToLeft(_st.statusLeft, _st.statusTop, newWidth);\n\t}, lifetime());\n}\n\n} // namespace\n\nclass EditParticipantBox::Inner : public Ui::RpWidget {\npublic:\n\tInner(\n\t\tQWidget *parent,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user,\n\t\tbool hasAdminRights);\n\n\ttemplate <typename Widget>\n\tWidget *addControl(object_ptr<Widget> widget, QMargins margin);\n\n\t[[nodiscard]] not_null<Ui::VerticalLayout*> verticalLayout() const {\n\t\treturn _rows;\n\t}\n\n\tvoid setCoverMode(bool enabled);\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tnot_null<PeerData*> _peer;\n\tnot_null<UserData*> _user;\n\tobject_ptr<Ui::UserpicButton> _userPhoto;\n\tUi::Text::String _userName;\n\tbool _hasAdminRights = false;\n\tbool _coverMode = false;\n\tobject_ptr<Ui::VerticalLayout> _rows;\n\n};\n\nEditParticipantBox::Inner::Inner(\n\tQWidget *parent,\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> user,\n\tbool hasAdminRights)\n: RpWidget(parent)\n, _peer(peer)\n, _user(user)\n, _userPhoto(this, _user, st::rightsPhotoButton)\n, _hasAdminRights(hasAdminRights)\n, _rows(this) {\n\t_rows->heightValue(\n\t) | rpl::on_next([=] {\n\t\tresizeToWidth(width());\n\t}, lifetime());\n\n\t_userPhoto->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_userName.setText(\n\t\tst::rightsNameStyle,\n\t\t_user->name(),\n\t\tUi::NameTextOptions());\n}\n\ntemplate <typename Widget>\nWidget *EditParticipantBox::Inner::addControl(\n\t\tobject_ptr<Widget> widget,\n\t\tQMargins margin) {\n\treturn _rows->add(std::move(widget), margin);\n}\n\nvoid EditParticipantBox::Inner::setCoverMode(bool enabled) {\n\t_coverMode = enabled;\n\tif (enabled) {\n\t\t_userPhoto->hide();\n\t}\n\tresizeToWidth(width());\n}\n\nint EditParticipantBox::Inner::resizeGetHeight(int newWidth) {\n\tif (_coverMode) {\n\t\t_rows->resizeToWidth(newWidth);\n\t\t_rows->moveToLeft(0, 0, newWidth);\n\t\treturn _rows->heightNoMargins();\n\t}\n\t_userPhoto->moveToLeft(\n\t\tst::rightsPhotoMargin.left(),\n\t\tst::rightsPhotoMargin.top());\n\tconst auto rowsTop = st::rightsPhotoMargin.top()\n\t\t+ st::rightsPhotoButton.size.height()\n\t\t+ st::rightsPhotoMargin.bottom();\n\t_rows->resizeToWidth(newWidth);\n\t_rows->moveToLeft(0, rowsTop, newWidth);\n\treturn rowsTop + _rows->heightNoMargins();\n}\n\nvoid EditParticipantBox::Inner::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tp.fillRect(e->rect(), st::boxBg);\n\n\tif (_coverMode) {\n\t\treturn;\n\t}\n\n\tp.setPen(st::contactsNameFg);\n\tauto namex = st::rightsPhotoMargin.left()\n\t\t+ st::rightsPhotoButton.size .width()\n\t\t+ st::rightsPhotoMargin.right();\n\tauto namew = width() - namex - st::rightsPhotoMargin.right();\n\t_userName.drawLeftElided(\n\t\tp,\n\t\tnamex,\n\t\tst::rightsPhotoMargin.top() + st::rightsNameTop,\n\t\tnamew,\n\t\twidth());\n\tconst auto statusText = [&] {\n\t\tif (_user->isBot()) {\n\t\t\tconst auto seesAllMessages = _user->botInfo->readsAllHistory\n\t\t\t\t|| _hasAdminRights;\n\t\t\treturn (seesAllMessages\n\t\t\t\t? tr::lng_status_bot_reads_all\n\t\t\t\t: tr::lng_status_bot_not_reads_all)(tr::now);\n\t\t}\n\t\treturn Data::OnlineText(_user->lastseen(), base::unixtime::now());\n\t}();\n\tp.setFont(st::contactsStatusFont);\n\tp.setPen(st::contactsStatusFg);\n\tp.drawTextLeft(\n\t\tnamex,\n\t\tst::rightsPhotoMargin.top() + st::rightsStatusTop,\n\t\twidth(),\n\t\tstatusText);\n}\n\nEditParticipantBox::EditParticipantBox(\n\tQWidget*,\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> user,\n\tbool hasAdminRights)\n: _peer(peer)\n, _user(user)\n, _hasAdminRights(hasAdminRights) {\n}\n\nnot_null<Ui::VerticalLayout*> EditParticipantBox::verticalLayout() const {\n\treturn _inner->verticalLayout();\n}\n\nvoid EditParticipantBox::prepare() {\n\t_inner = setInnerWidget(object_ptr<Inner>(\n\t\tthis,\n\t\t_peer,\n\t\t_user,\n\t\thasAdminRights()));\n\tsetDimensionsToContent(st::boxWideWidth, _inner);\n}\n\ntemplate <typename Widget>\nWidget *EditParticipantBox::addControl(\n\t\tobject_ptr<Widget> widget,\n\t\tQMargins margin) {\n\tExpects(_inner != nullptr);\n\n\treturn _inner->addControl(std::move(widget), margin);\n}\n\nbool EditParticipantBox::amCreator() const {\n\tif (const auto chat = _peer->asChat()) {\n\t\treturn chat->amCreator();\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\treturn channel->amCreator();\n\t}\n\tUnexpected(\"Peer type in EditParticipantBox::Inner::amCreator.\");\n}\n\nvoid EditParticipantBox::setCoverMode(bool enabled) {\n\tExpects(_inner != nullptr);\n\n\t_inner->setCoverMode(enabled);\n}\n\nEditAdminBox::EditAdminBox(\n\tQWidget*,\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> user,\n\tChatAdminRightsInfo rights,\n\tconst QString &rank,\n\tTimeId promotedSince,\n\tUserData *by,\n\tstd::optional<EditAdminBotFields> addingBot)\n: EditParticipantBox(\n\tnullptr,\n\tpeer,\n\tuser,\n\t(rights.flags != 0))\n, _oldRights(rights)\n, _oldRank(rank)\n, _promotedSince(promotedSince)\n, _by(by)\n, _addingBot(std::move(addingBot)) {\n}\n\nChatAdminRightsInfo EditAdminBox::defaultRights() const {\n\tusing Flag = ChatAdminRight;\n\n\treturn peer()->isChat()\n\t\t? peer()->asChat()->defaultAdminRights(user())\n\t\t: peer()->isMegagroup()\n\t\t? ChatAdminRightsInfo{ (Flag::ChangeInfo\n\t\t\t| Flag::DeleteMessages\n\t\t\t| Flag::PostStories\n\t\t\t| Flag::EditStories\n\t\t\t| Flag::DeleteStories\n\t\t\t| Flag::BanUsers\n\t\t\t| Flag::InviteByLinkOrAdd\n\t\t\t| Flag::ManageTopics\n\t\t\t| Flag::PinMessages\n\t\t\t| Flag::ManageCall\n\t\t\t| Flag::ManageRanks) }\n\t\t: ChatAdminRightsInfo{ (Flag::ChangeInfo\n\t\t\t| Flag::PostMessages\n\t\t\t| Flag::EditMessages\n\t\t\t| Flag::DeleteMessages\n\t\t\t| Flag::PostStories\n\t\t\t| Flag::EditStories\n\t\t\t| Flag::DeleteStories\n\t\t\t| Flag::InviteByLinkOrAdd\n\t\t\t| Flag::ManageCall\n\t\t\t| Flag::ManageDirect\n\t\t\t| Flag::BanUsers) };\n}\n\nvoid EditAdminBox::prepare() {\n\tusing namespace rpl::mappers;\n\tusing Flag = ChatAdminRight;\n\tusing Flags = ChatAdminRights;\n\n\tEditParticipantBox::prepare();\n\n\tsetCoverMode(true);\n\taddControl(\n\t\tobject_ptr<Cover>(this, user(), hasAdminRights()),\n\t\tstyle::margins());\n\n\tsetTitle(_addingBot\n\t\t? (_addingBot->existing\n\t\t\t? tr::lng_rights_edit_admin()\n\t\t\t: tr::lng_bot_add_title())\n\t\t: _oldRights.flags\n\t\t? tr::lng_rights_edit_admin()\n\t\t: tr::lng_channel_add_admin());\n\n\tif (_addingBot\n\t\t&& !_addingBot->existing\n\t\t&& !peer()->isBroadcast()\n\t\t&& _saveCallback) {\n\t\taddControl(\n\t\t\tobject_ptr<Ui::BoxContentDivider>(this),\n\t\t\tst::rightsDividerMargin / 2);\n\t\t_addAsAdmin = addControl(\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tthis,\n\t\t\t\ttr::lng_bot_as_admin_check(tr::now),\n\t\t\t\tst::rightsCheckbox,\n\t\t\t\tstd::make_unique<Ui::ToggleView>(\n\t\t\t\t\tst::rightsToggle,\n\t\t\t\t\ttrue)),\n\t\t\tst::rightsToggleMargin + (st::rightsDividerMargin / 2));\n\t\t_addAsAdmin->checkedChanges(\n\t\t) | rpl::on_next([=](bool checked) {\n\t\t\t_adminControlsWrap->toggle(checked, anim::type::normal);\n\t\t\trefreshButtons();\n\t\t}, _addAsAdmin->lifetime());\n\t}\n\n\t_adminControlsWrap = addControl(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::VerticalLayout>(this)));\n\tconst auto inner = _adminControlsWrap->entity();\n\n\tif (_promotedSince) {\n\t\tconst auto parsed = base::unixtime::parse(_promotedSince);\n\t\tconst auto label = Ui::AddDividerText(\n\t\t\tinner,\n\t\t\ttr::lng_rights_about_by(\n\t\t\t\tlt_user,\n\t\t\t\trpl::single(_by\n\t\t\t\t\t? tr::link(_by->name(), 1)\n\t\t\t\t\t: TextWithEntities{ QString::fromUtf8(\"\\U0001F47B\") }),\n\t\t\t\tlt_date,\n\t\t\t\trpl::single(TextWithEntities{ langDateTimeFull(parsed) }),\n\t\t\t\ttr::marked));\n\t\tif (_by) {\n\t\t\tlabel->setLink(1, _by->createOpenLink());\n\t\t}\n\t\tUi::AddSkip(inner);\n\t} else {\n\t\tUi::AddDivider(inner);\n\t\tUi::AddSkip(inner);\n\t}\n\n\tconst auto chat = peer()->asChat();\n\tconst auto channel = peer()->asChannel();\n\tconst auto prepareRights = _addingBot\n\t\t? ChatAdminRightsInfo(_oldRights.flags | _addingBot->existing)\n\t\t: _oldRights.flags\n\t\t? _oldRights\n\t\t: defaultRights();\n\tconst auto disabledByDefaults = (channel && !channel->isMegagroup())\n\t\t? ChatAdminRights()\n\t\t: DisabledByDefaultRestrictions(peer());\n\tconst auto filterByMyRights = canSave()\n\t\t&& !_oldRights.flags\n\t\t&& channel\n\t\t&& !channel->amCreator();\n\tconst auto prepareFlags = disabledByDefaults\n\t\t| (prepareRights.flags\n\t\t\t& (filterByMyRights ? channel->adminRights() : ~Flag(0)));\n\n\tconst auto disabledMessages = [&] {\n\t\tauto result = base::flat_map<Flags, QString>();\n\t\tif (!canSave()) {\n\t\t\tresult.emplace(\n\t\t\t\t~Flags(0),\n\t\t\t\ttr::lng_rights_about_admin_cant_edit(tr::now));\n\t\t} else {\n\t\t\tresult.emplace(\n\t\t\t\tdisabledByDefaults,\n\t\t\t\ttr::lng_rights_permission_for_all(tr::now));\n\t\t\tif (amCreator() && user()->isSelf()) {\n\t\t\t\tresult.emplace(\n\t\t\t\t\t~Flag::Anonymous,\n\t\t\t\t\ttr::lng_rights_permission_cant_edit(tr::now));\n\t\t\t} else if (const auto channel = peer()->asChannel()) {\n\t\t\t\tif (!channel->amCreator()) {\n\t\t\t\t\tresult.emplace(\n\t\t\t\t\t\t~channel->adminRights(),\n\t\t\t\t\t\ttr::lng_rights_permission_cant_edit(tr::now));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}();\n\n\tconst auto isGroup = chat || channel->isMegagroup();\n\tconst auto anyoneCanAddMembers = chat\n\t\t? chat->anyoneCanAddMembers()\n\t\t: channel->anyoneCanAddMembers();\n\tconst auto options = Data::AdminRightsSetOptions{\n\t\t.isGroup = isGroup,\n\t\t.isForum = peer()->isForum(),\n\t\t.anyoneCanAddMembers = anyoneCanAddMembers,\n\t};\n\tUi::AddSubsectionTitle(inner, tr::lng_rights_edit_admin_header());\n\tauto [checkboxes, getChecked, changes, highlightWidget] = CreateEditAdminRights(\n\t\tinner,\n\t\tprepareFlags,\n\t\tdisabledMessages,\n\t\toptions);\n\tinner->add(std::move(checkboxes), QMargins());\n\n\tauto selectedFlags = rpl::single(\n\t\tgetChecked()\n\t) | rpl::then(std::move(\n\t\tchanges\n\t));\n\n\tconst auto hasRank = canSave() && (chat || channel->isMegagroup());\n\n\t{\n\t\tconst auto aboutAddAdminsInner = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\t\tconst auto emptyAboutAddAdminsInner = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\t\taboutAddAdminsInner->toggle(false, anim::type::instant);\n\t\temptyAboutAddAdminsInner->toggle(false, anim::type::instant);\n\t\tUi::AddSkip(emptyAboutAddAdminsInner->entity());\n\t\tif (hasRank) {\n\t\t\tUi::AddDivider(emptyAboutAddAdminsInner->entity());\n\t\t}\n\t\tUi::AddSkip(aboutAddAdminsInner->entity());\n\t\tUi::AddDividerText(\n\t\t\taboutAddAdminsInner->entity(),\n\t\t\trpl::duplicate(\n\t\t\t\tselectedFlags\n\t\t\t) | rpl::map(\n\t\t\t\t(_1 & Flag::AddAdmins) != 0\n\t\t\t) | rpl::distinct_until_changed(\n\t\t\t) | rpl::map([=](bool canAddAdmins) -> rpl::producer<QString> {\n\t\t\t\tconst auto empty = (amCreator() && user()->isSelf());\n\t\t\t\taboutAddAdminsInner->toggle(!empty, anim::type::instant);\n\t\t\t\temptyAboutAddAdminsInner->toggle(empty, anim::type::instant);\n\t\t\t\tif (empty) {\n\t\t\t\t\treturn rpl::single(QString());\n\t\t\t\t} else if (!canSave()) {\n\t\t\t\t\treturn tr::lng_rights_about_admin_cant_edit();\n\t\t\t\t} else if (canAddAdmins) {\n\t\t\t\t\treturn tr::lng_rights_about_add_admins_yes();\n\t\t\t\t}\n\t\t\t\treturn tr::lng_rights_about_add_admins_no();\n\t\t\t}) | rpl::flatten_latest());\n\t}\n\n\tif (canTransferOwnership()) {\n\t\tconst auto allFlags = AdminRightsForOwnershipTransfer(options);\n\t\tsetupTransferButton(\n\t\t\tinner,\n\t\t\tisGroup\n\t\t)->toggleOn(rpl::duplicate(\n\t\t\tselectedFlags\n\t\t) | rpl::map(\n\t\t\t((_1 & allFlags) == allFlags)\n\t\t))->setDuration(0);\n\t}\n\n\tif (canSave()) {\n\t\tif (hasRank) {\n\t\t\tconst auto role = _oldRights.flags\n\t\t\t\t? LookupBadgeRole(peer(), user())\n\t\t\t\t: HistoryView::BadgeRole::Admin;\n\t\t\t_tagControl = inner->add(\n\t\t\t\tobject_ptr<EditTagControl>(\n\t\t\t\t\tinner,\n\t\t\t\t\t&peer()->session(),\n\t\t\t\t\tuser(),\n\t\t\t\t\t_oldRank,\n\t\t\t\t\trole),\n\t\t\t\tstyle::margins());\n\t\t}\n\t\tif (_tagControl) {\n\t\t\tUi::AddSkip(inner);\n\t\t\tUi::AddDividerText(\n\t\t\t\tinner,\n\t\t\t\tuser()->isSelf()\n\t\t\t\t\t? tr::lng_rights_tag_about_self()\n\t\t\t\t\t: tr::lng_rights_tag_about(\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\trpl::single(user()->shortName())));\n\t\t}\n\t\tif (_oldRights.flags && !_addingBot) {\n\t\t\tconst auto isTargetCreator = (LookupBadgeRole(peer(), user())\n\t\t\t\t== HistoryView::BadgeRole::Creator);\n\t\t\tif (!isTargetCreator) {\n\t\t\t\tif (!_tagControl) {\n\t\t\t\t\tUi::AddSkip(inner);\n\t\t\t\t\tinner->add(\n\t\t\t\t\t\tobject_ptr<Ui::BoxContentDivider>(inner),\n\t\t\t\t\t\t{ 0, st::infoProfileSkip, 0, st::infoProfileSkip });\n\t\t\t\t}\n\t\t\t\tUi::AddSkip(inner);\n\t\t\t\tconst auto dismissButton = Settings::AddButtonWithIcon(\n\t\t\t\t\tinner,\n\t\t\t\t\ttr::lng_rights_dismiss_admin(),\n\t\t\t\t\tst::settingsAttentionButton,\n\t\t\t\t\t{ nullptr });\n\t\t\t\tdismissButton->setClickedCallback([=] {\n\t\t\t\t\tif (const auto callback = _saveCallback) {\n\t\t\t\t\t\tconst auto old = _oldRights;\n\t\t\t\t\t\tconst auto confirmed = [=](Fn<void()> close) {\n\t\t\t\t\t\t\tcallback(old, {}, {});\n\t\t\t\t\t\t\tclose();\n\t\t\t\t\t\t};\n\t\t\t\t\t\tuiShow()->showBox(\n\t\t\t\t\t\t\tUi::MakeConfirmBox({\n\t\t\t\t\t\t\t\t.text = tr::lng_profile_sure_remove_admin(\n\t\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\t\t\t\tuser()->firstName),\n\t\t\t\t\t\t\t\t.confirmed = confirmed,\n\t\t\t\t\t\t\t\t.confirmText = tr::lng_box_remove(),\n\t\t\t\t\t\t\t}));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\t_finishSave = [=, value = getChecked] {\n\t\t\tconst auto newFlags = (value() | ChatAdminRight::Other)\n\t\t\t\t& ((!channel || channel->amCreator())\n\t\t\t\t\t? ~Flags(0)\n\t\t\t\t\t: channel->adminRights());\n\t\t\t_saveCallback(\n\t\t\t\t_oldRights,\n\t\t\t\tChatAdminRightsInfo(newFlags),\n\t\t\t\t_tagControl\n\t\t\t\t\t? std::optional<QString>(_tagControl->currentRank())\n\t\t\t\t\t: std::nullopt);\n\t\t};\n\t\t_save = [=] {\n\t\t\tconst auto show = uiShow();\n\t\t\tif (!_saveCallback) {\n\t\t\t\treturn;\n\t\t\t} else if (_addAsAdmin && !_addAsAdmin->checked()) {\n\t\t\t\tconst auto weak = base::make_weak(this);\n\t\t\t\tAddBotToGroup(show, user(), peer(), _addingBot->token);\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->closeBox();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t} else if (_addingBot && !_addingBot->existing) {\n\t\t\t\tconst auto phrase = peer()->isBroadcast()\n\t\t\t\t\t? tr::lng_bot_sure_add_text_channel\n\t\t\t\t\t: tr::lng_bot_sure_add_text_group;\n\t\t\t\t_confirmBox = getDelegate()->show(Ui::MakeConfirmBox({\n\t\t\t\t\tphrase(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_group,\n\t\t\t\t\t\ttr::bold(peer()->name()),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\tcrl::guard(this, [=] { finishAddAdmin(); })\n\t\t\t\t}));\n\t\t\t} else {\n\t\t\t\t_finishSave();\n\t\t\t}\n\t\t};\n\t}\n\n\trefreshButtons();\n}\n\nvoid EditAdminBox::finishAddAdmin() {\n\t_finishSave();\n\tif (_confirmBox) {\n\t\t_confirmBox->closeBox();\n\t}\n}\n\nvoid EditAdminBox::refreshButtons() {\n\tclearButtons();\n\tif (canSave()) {\n\t\taddButton((!_addingBot || _addingBot->existing)\n\t\t\t? tr::lng_settings_save()\n\t\t\t: _adminControlsWrap->toggled()\n\t\t\t? tr::lng_bot_add_as_admin()\n\t\t\t: tr::lng_bot_add_as_member(), _save);\n\t\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\t} else {\n\t\taddButton(tr::lng_box_ok(), [=] { closeBox(); });\n\t}\n}\n\nbool EditAdminBox::canTransferOwnership() const {\n\tif (user()->isInaccessible() || user()->isBot() || user()->isSelf()) {\n\t\treturn false;\n\t} else if (const auto chat = peer()->asChat()) {\n\t\treturn chat->amCreator();\n\t} else if (const auto channel = peer()->asChannel()) {\n\t\treturn channel->amCreator();\n\t}\n\tUnexpected(\"Chat type in EditAdminBox::canTransferOwnership.\");\n}\n\nnot_null<Ui::SlideWrap<Ui::RpWidget>*> EditAdminBox::setupTransferButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tbool isGroup) {\n\tconst auto wrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\n\tconst auto inner = wrap->entity();\n\n\tinner->add(\n\t\tobject_ptr<Ui::BoxContentDivider>(inner),\n\t\t{ 0, st::infoProfileSkip, 0, st::infoProfileSkip });\n\tinner->add(EditPeerInfoBox::CreateButton(\n\t\tinner,\n\t\t(isGroup\n\t\t\t? tr::lng_rights_transfer_group\n\t\t\t: tr::lng_rights_transfer_channel)(),\n\t\trpl::single(QString()),\n\t\t[=] { transferOwnership(); },\n\t\tst::peerPermissionsButton,\n\t\t{}));\n\n\treturn wrap;\n}\n\nvoid EditAdminBox::transferOwnership() {\n\t_ownershipTransfer = std::make_unique<ChannelOwnershipTransfer>(\n\t\tpeer(),\n\t\tuser(),\n\t\tuiShow(),\n\t\t[](std::shared_ptr<Ui::Show> show) { show->hideLayer(); });\n\t_ownershipTransfer->start();\n}\n\nEditRestrictedBox::EditRestrictedBox(\n\tQWidget*,\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> user,\n\tbool hasAdminRights,\n\tChatRestrictionsInfo rights,\n\tconst QString &rank,\n\tUserData *by,\n\tTimeId since)\n: EditParticipantBox(nullptr, peer, user, hasAdminRights)\n, _oldRights(rights)\n, _oldRank(rank)\n, _by(by)\n, _since(since) {\n}\n\nvoid EditRestrictedBox::prepare() {\n\tusing Flag = ChatRestriction;\n\tusing Flags = ChatRestrictions;\n\n\tEditParticipantBox::prepare();\n\n\tsetCoverMode(true);\n\taddControl(\n\t\tobject_ptr<Cover>(this, user(), hasAdminRights()),\n\t\tstyle::margins());\n\n\tsetTitle(tr::lng_rights_user_restrictions());\n\n\tUi::AddDivider(verticalLayout());\n\tUi::AddSkip(verticalLayout());\n\n\tconst auto chat = peer()->asChat();\n\tconst auto channel = peer()->asChannel();\n\tconst auto defaultRestrictions = chat\n\t\t? chat->defaultRestrictions()\n\t\t: channel->defaultRestrictions();\n\tconst auto prepareRights = _oldRights.flags\n\t\t? _oldRights\n\t\t: defaultRights();\n\tconst auto prepareFlags = FixDependentRestrictions(\n\t\tprepareRights.flags\n\t\t| defaultRestrictions\n\t\t| ((channel && channel->isPublic())\n\t\t\t? (Flag::ChangeInfo | Flag::PinMessages)\n\t\t\t: Flags(0)));\n\tconst auto disabledMessages = [&] {\n\t\tauto result = base::flat_map<Flags, QString>();\n\t\tif (!canSave()) {\n\t\t\tresult.emplace(\n\t\t\t\t~Flags(0),\n\t\t\t\ttr::lng_rights_about_restriction_cant_edit(tr::now));\n\t\t} else {\n\t\t\tconst auto disabled = FixDependentRestrictions(\n\t\t\t\tdefaultRestrictions\n\t\t\t\t| ((channel && channel->isPublic())\n\t\t\t\t\t? (Flag::ChangeInfo | Flag::PinMessages)\n\t\t\t\t\t: Flags(0)));\n\t\t\tresult.emplace(\n\t\t\t\tdisabled,\n\t\t\t\ttr::lng_rights_restriction_for_all(tr::now));\n\t\t}\n\t\treturn result;\n\t}();\n\n\tUi::AddSubsectionTitle(\n\t\tverticalLayout(),\n\t\ttr::lng_rights_user_restrictions_header());\n\tauto [checkboxes, getRestrictions, changes, highlightWidget] = CreateEditRestrictions(\n\t\tthis,\n\t\tprepareFlags,\n\t\tdisabledMessages,\n\t\t{ .isForum = peer()->isForum(), .isUserSpecific = true });\n\taddControl(std::move(checkboxes), QMargins());\n\n\tif (canSave() && peer()->canManageRanks()) {\n\t\tUi::AddSkip(verticalLayout());\n\t\tUi::AddDivider(verticalLayout());\n\t\t_tagControl = addControl(\n\t\t\tobject_ptr<EditTagControl>(\n\t\t\t\tthis,\n\t\t\t\t&peer()->session(),\n\t\t\t\tuser(),\n\t\t\t\t_oldRank,\n\t\t\t\tHistoryView::BadgeRole::User),\n\t\t\tstyle::margins());\n\t\tUi::AddSkip(verticalLayout());\n\t\tUi::AddDividerText(\n\t\t\tverticalLayout(),\n\t\t\tuser()->isSelf()\n\t\t\t\t? tr::lng_rights_tag_about_self()\n\t\t\t\t: tr::lng_rights_tag_about(\n\t\t\t\t\tlt_name,\n\t\t\t\t\trpl::single(user()->shortName())));\n\t}\n\n\t_until = prepareRights.until;\n\tif (!_tagControl) {\n\t\taddControl(\n\t\t\tobject_ptr<Ui::FixedHeightWidget>(this, st::defaultVerticalListSkip));\n\t\tUi::AddDivider(verticalLayout());\n\t}\n\taddControl(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\ttr::lng_rights_chat_banned_until_header(tr::now),\n\t\t\tst::rightsHeaderLabel),\n\t\tst::rightsHeaderMargin);\n\tsetRestrictUntil(_until);\n\n\t//addControl(\n\t//\tobject_ptr<Ui::LinkButton>(\n\t//\t\tthis,\n\t//\t\ttr::lng_rights_chat_banned_block(tr::now),\n\t//\t\tst::boxLinkButton));\n\n\tif (_since) {\n\t\tconst auto parsed = base::unixtime::parse(_since);\n\t\tconst auto inner = addControl(object_ptr<Ui::VerticalLayout>(this));\n\t\tconst auto isBanned = (_oldRights.flags\n\t\t\t& ChatRestriction::ViewMessages);\n\t\tUi::AddSkip(inner);\n\t\tconst auto label = Ui::AddDividerText(\n\t\t\tinner,\n\t\t\t(isBanned\n\t\t\t\t? tr::lng_rights_chat_banned_by\n\t\t\t\t: tr::lng_rights_chat_restricted_by)(\n\t\t\t\t\tlt_user,\n\t\t\t\t\trpl::single(_by\n\t\t\t\t\t\t? tr::link(_by->name(), 1)\n\t\t\t\t\t\t: TextWithEntities{ QString::fromUtf8(\"\\U0001F47B\") }),\n\t\t\t\t\tlt_date,\n\t\t\t\t\trpl::single(TextWithEntities{ langDateTimeFull(parsed) }),\n\t\t\t\t\ttr::marked));\n\t\tif (_by) {\n\t\t\tlabel->setLink(1, _by->createOpenLink());\n\t\t}\n\t}\n\n\tif (canSave()) {\n\t\tUi::AddSkip(verticalLayout());\n\t\tUi::AddDivider(verticalLayout());\n\t\tUi::AddSkip(verticalLayout());\n\n\t\tconst auto canAddAdmins = (chat && chat->canAddAdmins())\n\t\t\t|| (channel && channel->canAddAdmins());\n\t\tif (canAddAdmins) {\n\t\t\tconst auto promoteButton = Settings::AddButtonWithIcon(\n\t\t\t\tverticalLayout(),\n\t\t\t\ttr::lng_rights_promote_member(),\n\t\t\t\tst::settingsButtonNoIcon,\n\t\t\t\t{ nullptr });\n\t\t\tpromoteButton->setClickedCallback([=] {\n\t\t\t\tconst auto rank = _tagControl\n\t\t\t\t\t? _tagControl->currentRank()\n\t\t\t\t\t: _oldRank;\n\t\t\t\tauto adminBox = Box<EditAdminBox>(\n\t\t\t\t\tpeer(),\n\t\t\t\t\tuser(),\n\t\t\t\t\tChatAdminRightsInfo(),\n\t\t\t\t\trank,\n\t\t\t\t\tTimeId(0),\n\t\t\t\t\tnullptr);\n\t\t\t\tconst auto adminBoxWeak = QPointer<Ui::BoxContent>(\n\t\t\t\t\tadminBox.data());\n\t\t\t\tconst auto show = uiShow();\n\t\t\t\tconst auto restrictWeak = QPointer<EditRestrictedBox>(\n\t\t\t\t\tthis);\n\t\t\t\tconst auto closeBoth = [=] {\n\t\t\t\t\tif (adminBoxWeak) {\n\t\t\t\t\t\tadminBoxWeak->closeBox();\n\t\t\t\t\t}\n\t\t\t\t\tif (restrictWeak) {\n\t\t\t\t\t\trestrictWeak->closeBox();\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tconst auto savedUser = user();\n\t\t\t\tconst auto savedPeer = peer();\n\t\t\t\tconst auto done = [=](\n\t\t\t\t\t\tChatAdminRightsInfo newRights,\n\t\t\t\t\t\tconst std::optional<QString> &rank) {\n\t\t\t\t\tcloseBoth();\n\t\t\t\t\tconst auto effectiveRank = rank.value_or([&] {\n\t\t\t\t\t\tconst auto ch = savedPeer->asChannel();\n\t\t\t\t\t\tif (!ch) {\n\t\t\t\t\t\t\treturn QString();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst auto info = ch->mgInfo.get();\n\t\t\t\t\t\tif (!info) {\n\t\t\t\t\t\t\treturn QString();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst auto i = info->memberRanks.find(\n\t\t\t\t\t\t\tpeerToUser(savedUser->id));\n\t\t\t\t\t\treturn (i != end(info->memberRanks))\n\t\t\t\t\t\t\t? i->second\n\t\t\t\t\t\t\t: QString();\n\t\t\t\t\t}());\n\t\t\t\t\tsavedUser->session().changes().chatAdminChanged(\n\t\t\t\t\t\tsavedPeer,\n\t\t\t\t\t\tsavedUser,\n\t\t\t\t\t\tnewRights.flags,\n\t\t\t\t\t\teffectiveRank);\n\t\t\t\t};\n\t\t\t\tconst auto fail = closeBoth;\n\t\t\t\tadminBox->setSaveCallback(\n\t\t\t\t\tSaveAdminCallback(\n\t\t\t\t\t\tshow,\n\t\t\t\t\t\tpeer(),\n\t\t\t\t\t\tuser(),\n\t\t\t\t\t\tdone,\n\t\t\t\t\t\tfail));\n\t\t\t\tshow->showBox(std::move(adminBox));\n\t\t\t});\n\t\t}\n\n\t\tconst auto removeButton = Settings::AddButtonWithIcon(\n\t\t\tverticalLayout(),\n\t\t\ttr::lng_rights_remove_member(),\n\t\t\tst::settingsAttentionButton,\n\t\t\t{ nullptr });\n\t\tremoveButton->setClickedCallback([=] {\n\t\t\tif (const auto callback = _saveCallback) {\n\t\t\t\tconst auto old = _oldRights;\n\t\t\t\tconst auto confirmed = [=](Fn<void()> close) {\n\t\t\t\t\tcallback(\n\t\t\t\t\t\told,\n\t\t\t\t\t\tChannelData::KickedRestrictedRights(user()));\n\t\t\t\t\tclose();\n\t\t\t\t};\n\t\t\t\tuiShow()->show(\n\t\t\t\t\tUi::MakeConfirmBox({\n\t\t\t\t\t\t.text = tr::lng_profile_sure_kick(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\t\tuser()->firstName),\n\t\t\t\t\t\t.confirmed = confirmed,\n\t\t\t\t\t\t.confirmText = tr::lng_box_remove(),\n\t\t\t\t\t}));\n\t\t\t}\n\t\t});\n\n\t\tconst auto save = [=, value = getRestrictions] {\n\t\t\tif (!_saveCallback) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto show = uiShow();\n\t\t\tconst auto rankPeer = peer();\n\t\t\tconst auto rankUser = user();\n\t\t\tconst auto rank = _tagControl\n\t\t\t\t? _tagControl->currentRank()\n\t\t\t\t: _oldRank;\n\t\t\tconst auto saveRank = (rank != _oldRank);\n\n\t\t\t// May destroy the box.\n\t\t\t_saveCallback(\n\t\t\t\t_oldRights,\n\t\t\t\tChatRestrictionsInfo{ value(), getRealUntilValue() });\n\n\t\t\tif (saveRank) {\n\t\t\t\tSaveMemberRank(\n\t\t\t\t\tshow,\n\t\t\t\t\trankPeer,\n\t\t\t\t\trankUser,\n\t\t\t\t\trank,\n\t\t\t\t\tnullptr,\n\t\t\t\t\tnullptr);\n\t\t\t}\n\t\t};\n\t\taddButton(tr::lng_settings_save(), save);\n\t\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\t} else {\n\t\taddButton(tr::lng_box_ok(), [=] { closeBox(); });\n\t}\n}\n\nChatRestrictionsInfo EditRestrictedBox::defaultRights() const {\n\treturn ChatRestrictionsInfo();\n}\n\nvoid EditRestrictedBox::showRestrictUntil() {\n\tuiShow()->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto save = [=](TimeId result) {\n\t\t\tif (!result) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsetRestrictUntil(result);\n\t\t\tbox->closeBox();\n\t\t};\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto time = isUntilForever()\n\t\t\t? (now + kSecondsInDay)\n\t\t\t: getRealUntilValue();\n\t\tChooseDateTimeBox(box, {\n\t\t\t.title = tr::lng_rights_chat_banned_until_header(),\n\t\t\t.submit = tr::lng_settings_save(),\n\t\t\t.done = save,\n\t\t\t.min = [=] { return now; },\n\t\t\t.time = time,\n\t\t\t.max = [=] {\n\t\t\t\treturn now + kSecondsInDay * kMaxRestrictDelayDays;\n\t\t\t},\n\t\t});\n\t}));\n}\n\nvoid EditRestrictedBox::setRestrictUntil(TimeId until) {\n\t_until = until;\n\t_untilVariants.clear();\n\tcreateUntilGroup();\n\tcreateUntilVariants();\n}\n\nbool EditRestrictedBox::isUntilForever() const {\n\treturn ChannelData::IsRestrictedForever(_until);\n}\n\nvoid EditRestrictedBox::createUntilGroup() {\n\t_untilGroup = std::make_shared<Ui::RadiobuttonGroup>(\n\t\tisUntilForever() ? 0 : _until);\n\t_untilGroup->setChangedCallback([this](int value) {\n\t\tif (value == kUntilCustom) {\n\t\t\t_untilGroup->setValue(_until);\n\t\t\tshowRestrictUntil();\n\t\t} else if (_until != value) {\n\t\t\t_until = value;\n\t\t}\n\t});\n}\n\nvoid EditRestrictedBox::createUntilVariants() {\n\tauto addVariant = [&](int value, const QString &text) {\n\t\tif (!canSave() && _untilGroup->current() != value) {\n\t\t\treturn;\n\t\t}\n\t\t_untilVariants.emplace_back(\n\t\t\taddControl(\n\t\t\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\t\t\tthis,\n\t\t\t\t\t_untilGroup,\n\t\t\t\t\tvalue,\n\t\t\t\t\ttext,\n\t\t\t\t\tst::defaultCheckbox),\n\t\t\t\tst::rightsToggleMargin));\n\t\tif (!canSave()) {\n\t\t\t_untilVariants.back()->setDisabled(true);\n\t\t}\n\t};\n\tauto addCustomVariant = [&](TimeId until, TimeId from, TimeId to) {\n\t\tif (!ChannelData::IsRestrictedForever(until)\n\t\t\t&& until > from\n\t\t\t&& until <= to) {\n\t\t\taddVariant(\n\t\t\t\tuntil,\n\t\t\t\ttr::lng_rights_chat_banned_custom_date(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_date,\n\t\t\t\t\tlangDateTime(base::unixtime::parse(until))));\n\t\t}\n\t};\n\tauto addCurrentVariant = [&](TimeId from, TimeId to) {\n\t\tauto oldUntil = _oldRights.until;\n\t\tif (oldUntil < _until) {\n\t\t\taddCustomVariant(oldUntil, from, to);\n\t\t}\n\t\taddCustomVariant(_until, from, to);\n\t\tif (oldUntil > _until) {\n\t\t\taddCustomVariant(oldUntil, from, to);\n\t\t}\n\t};\n\taddVariant(0, tr::lng_rights_chat_banned_forever(tr::now));\n\n\tauto now = base::unixtime::now();\n\tauto nextDay = now + kSecondsInDay;\n\tauto nextWeek = now + kSecondsInWeek;\n\taddCurrentVariant(0, nextDay);\n\taddVariant(kUntilOneDay, tr::lng_rights_chat_banned_day(tr::now, lt_count, 1));\n\taddCurrentVariant(nextDay, nextWeek);\n\taddVariant(kUntilOneWeek, tr::lng_rights_chat_banned_week(tr::now, lt_count, 1));\n\taddCurrentVariant(nextWeek, INT_MAX);\n\taddVariant(kUntilCustom, tr::lng_rights_chat_banned_custom(tr::now));\n}\n\nTimeId EditRestrictedBox::getRealUntilValue() const {\n\tExpects(_until != kUntilCustom);\n\tif (_until == kUntilOneDay) {\n\t\treturn base::unixtime::now() + kSecondsInDay;\n\t} else if (_until == kUntilOneWeek) {\n\t\treturn base::unixtime::now() + kSecondsInWeek;\n\t}\n\tAssert(_until >= 0);\n\treturn _until;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_participant_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n#include \"base/unique_qptr.h\"\n#include \"data/data_chat_participant_status.h\"\n\nnamespace Ui {\nclass FlatLabel;\nclass LinkButton;\nclass Checkbox;\nclass Radiobutton;\nclass RadiobuttonGroup;\nclass VerticalLayout;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nclass ChannelOwnershipTransfer;\nclass EditTagControl;\n\nclass EditParticipantBox : public Ui::BoxContent {\npublic:\n\tEditParticipantBox(\n\t\tQWidget*,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user,\n\t\tbool hasAdminRights);\n\n\t[[nodiscard]] not_null<Ui::VerticalLayout*> verticalLayout() const;\n\nprotected:\n\tvoid prepare() override;\n\n\t[[nodiscard]] not_null<UserData*> user() const {\n\t\treturn _user;\n\t}\n\t[[nodiscard]] not_null<PeerData*> peer() const {\n\t\treturn _peer;\n\t}\n\t[[nodiscard]] bool amCreator() const;\n\n\ttemplate <typename Widget>\n\tWidget *addControl(object_ptr<Widget> widget, QMargins margin = {});\n\n\tvoid setCoverMode(bool enabled);\n\n\tbool hasAdminRights() const {\n\t\treturn _hasAdminRights;\n\t}\n\nprivate:\n\tnot_null<PeerData*> _peer;\n\tnot_null<UserData*> _user;\n\tbool _hasAdminRights = false;\n\n\tclass Inner;\n\tQPointer<Inner> _inner;\n\n};\n\nstruct EditAdminBotFields {\n\tQString token;\n\tChatAdminRights existing;\n};\n\nclass EditAdminBox : public EditParticipantBox {\npublic:\n\tEditAdminBox(\n\t\tQWidget*,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user,\n\t\tChatAdminRightsInfo rights,\n\t\tconst QString &rank,\n\t\tTimeId promotedSince,\n\t\tUserData *by,\n\t\tstd::optional<EditAdminBotFields> addingBot = {});\n\n\tvoid setSaveCallback(\n\t\t\tFn<void(\n\t\t\t\tChatAdminRightsInfo,\n\t\t\t\tChatAdminRightsInfo,\n\t\t\t\tconst std::optional<QString> &rank)> callback) {\n\t\t_saveCallback = std::move(callback);\n\t}\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\t[[nodiscard]] ChatAdminRightsInfo defaultRights() const;\n\n\tvoid transferOwnership();\n\t[[nodiscard]] bool canSave() const {\n\t\treturn _saveCallback != nullptr;\n\t}\n\tvoid finishAddAdmin();\n\tvoid refreshButtons();\n\t[[nodiscard]] bool canTransferOwnership() const;\n\tnot_null<Ui::SlideWrap<Ui::RpWidget>*> setupTransferButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tbool isGroup);\n\n\tconst ChatAdminRightsInfo _oldRights;\n\tconst QString _oldRank;\n\tFn<void(\n\t\tChatAdminRightsInfo,\n\t\tChatAdminRightsInfo,\n\t\tconst std::optional<QString> &rank)> _saveCallback;\n\n\tbase::weak_qptr<Ui::BoxContent> _confirmBox;\n\tUi::Checkbox *_addAsAdmin = nullptr;\n\tUi::SlideWrap<Ui::VerticalLayout> *_adminControlsWrap = nullptr;\n\tEditTagControl *_tagControl = nullptr;\n\n\tFn<void()> _save, _finishSave;\n\n\tTimeId _promotedSince = 0;\n\tUserData *_by = nullptr;\n\tstd::optional<EditAdminBotFields> _addingBot;\n\n\tstd::unique_ptr<ChannelOwnershipTransfer> _ownershipTransfer;\n\n};\n\n// Restricted box works with flags in the opposite way.\n// If some flag is set in the rights then the checkbox is unchecked.\n\nclass EditRestrictedBox : public EditParticipantBox {\npublic:\n\tEditRestrictedBox(\n\t\tQWidget*,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user,\n\t\tbool hasAdminRights,\n\t\tChatRestrictionsInfo rights,\n\t\tconst QString &rank,\n\t\tUserData *by,\n\t\tTimeId since);\n\n\tvoid setSaveCallback(\n\t\t\tFn<void(ChatRestrictionsInfo, ChatRestrictionsInfo)> callback) {\n\t\t_saveCallback = std::move(callback);\n\t}\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\t[[nodiscard]] ChatRestrictionsInfo defaultRights() const;\n\n\tbool canSave() const {\n\t\treturn !!_saveCallback;\n\t}\n\tvoid showRestrictUntil();\n\tvoid setRestrictUntil(TimeId until);\n\tbool isUntilForever() const;\n\tvoid createUntilGroup();\n\tvoid createUntilVariants();\n\tTimeId getRealUntilValue() const;\n\n\tconst ChatRestrictionsInfo _oldRights;\n\tconst QString _oldRank;\n\tEditTagControl *_tagControl = nullptr;\n\tUserData *_by = nullptr;\n\tTimeId _since = 0;\n\tTimeId _until = 0;\n\tFn<void(ChatRestrictionsInfo, ChatRestrictionsInfo)> _saveCallback;\n\n\tstd::shared_ptr<Ui::RadiobuttonGroup> _untilGroup;\n\tstd::vector<base::unique_qptr<Ui::Radiobutton>> _untilVariants;\n\n\tstatic constexpr auto kUntilOneDay = -1;\n\tstatic constexpr auto kUntilOneWeek = -2;\n\tstatic constexpr auto kUntilCustom = -3;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_participants_box.h\"\n\n#include \"api/api_chat_participants.h\"\n#include \"boxes/peers/edit_participant_box.h\"\n#include \"boxes/peers/edit_tag_control.h\"\n#include \"boxes/peers/add_participants_box.h\"\n#include \"boxes/peers/prepare_short_info_box.h\" // PrepareShortInfoBox\n#include \"boxes/peers/edit_members_visible.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/max_invite_box.h\"\n#include \"boxes/add_contact_box.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_antispam_validator.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"apiwrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"dialogs/dialogs_indexed_list.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_changes.h\"\n#include \"base/unixtime.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/outline_segments.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/menu/menu_multiline_action.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"window/window_session_controller.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_message.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace {\n\n// How many messages from chat history server should forward to user,\n// that was added to this chat.\nconstexpr auto kForwardMessagesOnAdd = 100;\n\nconstexpr auto kParticipantsFirstPageCount = 16;\nconstexpr auto kParticipantsPerPage = 200;\nconstexpr auto kSortByOnlineDelay = crl::time(1000);\n\nvoid RemoveAdmin(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<UserData*> user,\n\t\tChatAdminRightsInfo oldRights,\n\t\tFn<void()> onDone,\n\t\tFn<void()> onFail) {\n\tconst auto newRights = MTP_chatAdminRights(MTP_flags(0));\n\tusing Flag = MTPchannels_editAdmin::Flag;\n\tchannel->session().api().request(MTPchannels_EditAdmin(\n\t\tMTP_flags(Flag::f_rank),\n\t\tchannel->inputChannel(),\n\t\tuser->inputUser(),\n\t\tnewRights,\n\t\tMTP_string(QString())\n\t)).done([=](const MTPUpdates &result) {\n\t\tchannel->session().api().applyUpdates(result);\n\t\tchannel->applyEditAdmin(user, oldRights, ChatAdminRightsInfo(), QString());\n\t\tif (onDone) {\n\t\t\tonDone();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (show) {\n\t\t\tshow->showToast(error.type());\n\t\t}\n\t\tif (onFail) {\n\t\t\tonFail();\n\t\t}\n\t}).send();\n}\n\nvoid AddChatParticipant(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<ChatData*> chat,\n\t\tnot_null<UserData*> user,\n\t\tFn<void()> onDone,\n\t\tFn<void()> onFail) {\n\tchat->session().api().request(MTPmessages_AddChatUser(\n\t\tchat->inputChat(),\n\t\tuser->inputUser(),\n\t\tMTP_int(kForwardMessagesOnAdd)\n\t)).done([=](const MTPmessages_InvitedUsers &result) {\n\t\tconst auto &data = result.data();\n\t\tchat->session().api().applyUpdates(data.vupdates());\n\t\tif (onDone) {\n\t\t\tonDone();\n\t\t}\n\t\tChatInviteForbidden(\n\t\t\tshow,\n\t\t\tchat,\n\t\t\tCollectForbiddenUsers(&chat->session(), result));\n\t}).fail([=](const MTP::Error &error) {\n\t\tShowAddParticipantsError(show, error.type(), chat, user);\n\t\tif (onFail) {\n\t\t\tonFail();\n\t\t}\n\t}).send();\n}\n\nvoid SaveChatAdmin(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<ChatData*> chat,\n\t\tnot_null<UserData*> user,\n\t\tbool isAdmin,\n\t\tFn<void()> onDone,\n\t\tFn<void()> onFail,\n\t\tbool retryOnNotParticipant = true) {\n\tchat->session().api().request(MTPmessages_EditChatAdmin(\n\t\tchat->inputChat(),\n\t\tuser->inputUser(),\n\t\tMTP_bool(isAdmin)\n\t)).done([=] {\n\t\tchat->applyEditAdmin(user, isAdmin);\n\t\tif (onDone) {\n\t\t\tonDone();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto &type = error.type();\n\t\tif (retryOnNotParticipant\n\t\t\t&& isAdmin\n\t\t\t&& (type == u\"USER_NOT_PARTICIPANT\"_q)) {\n\t\t\tAddChatParticipant(show, chat, user, [=] {\n\t\t\t\tSaveChatAdmin(\n\t\t\t\t\tshow,\n\t\t\t\t\tchat,\n\t\t\t\t\tuser,\n\t\t\t\t\tisAdmin,\n\t\t\t\t\tonDone,\n\t\t\t\t\tonFail,\n\t\t\t\t\tfalse);\n\t\t\t}, onFail);\n\t\t} else {\n\t\t\tif (show) {\n\t\t\t\tshow->showToast(error.type());\n\t\t\t}\n\t\t\tif (onFail) {\n\t\t\t\tonFail();\n\t\t\t}\n\t\t}\n\t}).send();\n}\n\nvoid SaveChannelAdmin(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<UserData*> user,\n\t\tChatAdminRightsInfo oldRights,\n\t\tChatAdminRightsInfo newRights,\n\t\tconst std::optional<QString> &rank,\n\t\tFn<void()> onDone,\n\t\tFn<void()> onFail) {\n\tusing Flag = MTPchannels_editAdmin::Flag;\n\tconst auto flags = Flag(0)\n\t\t| (rank.has_value() ? Flag::f_rank : Flag(0));\n\tchannel->session().api().request(MTPchannels_EditAdmin(\n\t\tMTP_flags(flags),\n\t\tchannel->inputChannel(),\n\t\tuser->inputUser(),\n\t\tAdminRightsToMTP(newRights),\n\t\trank ? MTP_string(*rank) : MTPstring()\n\t)).done([=](const MTPUpdates &result) {\n\t\tchannel->session().api().applyUpdates(result);\n\t\tconst auto effectiveRank = rank.value_or([&] {\n\t\t\tif (const auto info = channel->mgInfo.get()) {\n\t\t\t\tconst auto i = info->memberRanks.find(\n\t\t\t\t\tpeerToUser(user->id));\n\t\t\t\tif (i != end(info->memberRanks)) {\n\t\t\t\t\treturn i->second;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn QString();\n\t\t}());\n\t\tchannel->applyEditAdmin(user, oldRights, newRights, effectiveRank);\n\t\tif (onDone) {\n\t\t\tonDone();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tShowAddParticipantsError(show, error.type(), channel, user);\n\t\tif (onFail) {\n\t\t\tonFail();\n\t\t}\n\t}).send();\n}\n\nvoid SaveChatParticipantKick(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<ChatData*> chat,\n\t\tnot_null<UserData*> user,\n\t\tFn<void()> onDone,\n\t\tFn<void()> onFail) {\n\tchat->session().api().request(MTPmessages_DeleteChatUser(\n\t\tMTP_flags(0),\n\t\tchat->inputChat(),\n\t\tuser->inputUser()\n\t)).done([=](const MTPUpdates &result) {\n\t\tchat->session().api().applyUpdates(result);\n\t\tif (onDone) {\n\t\t\tonDone();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (show) {\n\t\t\tshow->showToast(error.type());\n\t\t}\n\t\tif (onFail) {\n\t\t\tonFail();\n\t\t}\n\t}).send();\n}\n\n} // namespace\n\nvoid SaveMemberRank(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user,\n\t\tconst QString &rank,\n\t\tFn<void()> onDone,\n\t\tFn<void()> onFail) {\n\tpeer->session().api().request(MTPmessages_EditChatParticipantRank(\n\t\tpeer->input(),\n\t\tuser->input(),\n\t\tMTP_string(rank)\n\t)).done([=](const MTPUpdates &result) {\n\t\tpeer->session().api().applyUpdates(result);\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\tchannel->applyEditMemberRank(user, rank);\n\t\t} else if (const auto chat = peer->asChat()) {\n\t\t\tconst auto userId = peerToUser(user->id);\n\t\t\tif (rank.isEmpty()) {\n\t\t\t\tchat->memberRanks.remove(userId);\n\t\t\t} else {\n\t\t\t\tchat->memberRanks[userId] = rank;\n\t\t\t}\n\t\t\tif (userId != chat->session().userId()) {\n\t\t\t\tif (const auto history = chat->owner().historyLoaded(chat)) {\n\t\t\t\t\tauto changes = base::flat_set<UserId>();\n\t\t\t\t\tchanges.emplace(userId);\n\t\t\t\t\thistory->applyGroupAdminChanges(changes);\n\t\t\t\t}\n\t\t\t}\n\t\t\tchat->session().changes().peerUpdated(\n\t\t\t\tchat,\n\t\t\t\tData::PeerUpdate::Flag::Members);\n\t\t}\n\t\tpeer->session().changes().chatMemberRankChanged(\n\t\t\tpeer,\n\t\t\tuser,\n\t\t\trank);\n\t\tif (onDone) {\n\t\t\tonDone();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (show) {\n\t\t\tshow->showToast(error.type());\n\t\t}\n\t\tif (onFail) {\n\t\t\tonFail();\n\t\t}\n\t}).send();\n}\n\nFn<void(\n\tChatAdminRightsInfo oldRights,\n\tChatAdminRightsInfo newRights,\n\tconst std::optional<QString> &rank)> SaveAdminCallback(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user,\n\t\tFn<void(\n\t\t\tChatAdminRightsInfo newRights,\n\t\t\tconst std::optional<QString> &rank)> onDone,\n\t\tFn<void()> onFail) {\n\treturn [=](\n\t\t\tChatAdminRightsInfo oldRights,\n\t\t\tChatAdminRightsInfo newRights,\n\t\t\tconst std::optional<QString> &rank) {\n\t\tconst auto done = [=] { if (onDone) onDone(newRights, rank); };\n\t\tconst auto saveForChannel = [=](not_null<ChannelData*> channel) {\n\t\t\tSaveChannelAdmin(\n\t\t\t\tshow,\n\t\t\t\tchannel,\n\t\t\t\tuser,\n\t\t\t\toldRights,\n\t\t\t\tnewRights,\n\t\t\t\trank,\n\t\t\t\tdone,\n\t\t\t\tonFail);\n\t\t};\n\t\tif (const auto chat = peer->asChatNotMigrated()) {\n\t\t\tconst auto saveChatAdmin = [&](bool isAdmin) {\n\t\t\t\tSaveChatAdmin(show, chat, user, isAdmin, done, onFail);\n\t\t\t\tif (rank) {\n\t\t\t\t\tSaveMemberRank(show, chat, user, *rank, [] {}, [] {});\n\t\t\t\t}\n\t\t\t};\n\t\t\tif (newRights.flags == chat->defaultAdminRights(user).flags) {\n\t\t\t\tsaveChatAdmin(true);\n\t\t\t} else if (!newRights.flags) {\n\t\t\t\tsaveChatAdmin(false);\n\t\t\t} else {\n\t\t\t\tpeer->session().api().migrateChat(chat, saveForChannel);\n\t\t\t}\n\t\t} else if (const auto channel = peer->asChannelOrMigrated()) {\n\t\t\tsaveForChannel(channel);\n\t\t} else {\n\t\t\tUnexpected(\"Peer in SaveAdminCallback.\");\n\t\t}\n\t};\n}\n\nFn<void(\n\tChatRestrictionsInfo oldRights,\n\tChatRestrictionsInfo newRights)> SaveRestrictedCallback(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<PeerData*> participant,\n\t\tFn<void(ChatRestrictionsInfo newRights)> onDone,\n\t\tFn<void()> onFail) {\n\treturn [=](\n\t\t\tChatRestrictionsInfo oldRights,\n\t\t\tChatRestrictionsInfo newRights) {\n\t\tconst auto done = [=] { if (onDone) onDone(newRights); };\n\t\tconst auto saveForChannel = [=](not_null<ChannelData*> channel) {\n\t\t\tApi::ChatParticipants::Restrict(\n\t\t\t\tchannel,\n\t\t\t\tparticipant,\n\t\t\t\toldRights,\n\t\t\t\tnewRights,\n\t\t\t\tdone,\n\t\t\t\t[=](const QString &errorType) {\n\t\t\t\t\tif (show) {\n\t\t\t\t\t\tshow->showToast(errorType);\n\t\t\t\t\t}\n\t\t\t\t\tif (onFail) {\n\t\t\t\t\t\tonFail();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t};\n\t\tif (const auto chat = peer->asChatNotMigrated()) {\n\t\t\tif (participant->isUser()\n\t\t\t\t&& (newRights.flags & ChatRestriction::ViewMessages)) {\n\t\t\t\tSaveChatParticipantKick(\n\t\t\t\t\tshow,\n\t\t\t\t\tchat,\n\t\t\t\t\tparticipant->asUser(),\n\t\t\t\t\tdone,\n\t\t\t\t\tonFail);\n\t\t\t} else if (!newRights.flags) {\n\t\t\t\tdone();\n\t\t\t} else {\n\t\t\t\tpeer->session().api().migrateChat(chat, saveForChannel);\n\t\t\t}\n\t\t} else if (const auto channel = peer->asChannelOrMigrated()) {\n\t\t\tsaveForChannel(channel);\n\t\t} else {\n\t\t\tUnexpected(\"Peer in SaveAdminCallback.\");\n\t\t}\n\t};\n}\n\nvoid SubscribeToMigration(\n\t\tnot_null<PeerData*> peer,\n\t\trpl::lifetime &lifetime,\n\t\tFn<void(not_null<ChannelData*>)> migrate) {\n\tif (const auto chat = peer->asChat()) {\n\t\tif (const auto channel = peer->migrateTo()) {\n\t\t\tmigrate(channel);\n\t\t} else if (!chat->isDeactivated()) {\n\t\t\tchat->session().changes().peerUpdates(\n\t\t\t\tpeer,\n\t\t\t\tData::PeerUpdate::Flag::Migration\n\t\t\t) | rpl::map([](const Data::PeerUpdate &update) {\n\t\t\t\treturn update.peer->migrateTo();\n\t\t\t}) | rpl::filter([](ChannelData *channel) {\n\t\t\t\treturn (channel != nullptr);\n\t\t\t}) | rpl::take(\n\t\t\t\t1\n\t\t\t) | rpl::on_next([=](not_null<ChannelData*> channel) {\n\t\t\t\tconst auto onstack = base::duplicate(migrate);\n\t\t\t\tonstack(channel);\n\t\t\t}, lifetime);\n\t\t}\n\t}\n}\n\nParticipantsAdditionalData::ParticipantsAdditionalData(\n\tnot_null<PeerData*> peer,\n\tRole role)\n: _peer(peer)\n, _role(role) {\n\tfillFromPeer();\n}\n\nbool ParticipantsAdditionalData::infoLoaded(\n\t\tnot_null<PeerData*> participant) const {\n\treturn _peer->isChat()\n\t\t|| (_infoNotLoaded.find(participant) == end(_infoNotLoaded));\n}\n\nbool ParticipantsAdditionalData::canEditAdmin(\n\t\tnot_null<UserData*> user) const {\n\tif (_creator && _creator->isSelf()) {\n\t\treturn true;\n\t} else if (_creator == user || user->isSelf()) {\n\t\treturn false;\n\t} else if (adminRights(user).has_value()) {\n\t\treturn !_peer->isChat() && _adminCanEdit.contains(user);\n\t}\n\treturn true;\n}\n\nbool ParticipantsAdditionalData::canAddOrEditAdmin(\n\t\tnot_null<UserData*> user) const {\n\tif (!canEditAdmin(user)) {\n\t\treturn false;\n\t} else if (const auto chat = _peer->asChat()) {\n\t\treturn chat->canAddAdmins();\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\treturn channel->canAddAdmins();\n\t}\n\tUnexpected(\"Peer in ParticipantsAdditionalData::canAddOrEditAdmin.\");\n}\n\nbool ParticipantsAdditionalData::canRestrictParticipant(\n\t\tnot_null<PeerData*> participant) const {\n\tconst auto user = participant->asUser();\n\tif (user && (!canEditAdmin(user) || user->isSelf())) {\n\t\treturn false;\n\t} else if (const auto chat = _peer->asChat()) {\n\t\treturn chat->canBanMembers();\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\treturn channel->canBanMembers();\n\t}\n\tUnexpected(\"Peer in ParticipantsAdditionalData::canRestrictParticipant.\");\n}\n\nbool ParticipantsAdditionalData::canRemoveParticipant(\n\t\tnot_null<PeerData*> participant) const {\n\tconst auto user = participant->asUser();\n\tif (canRestrictParticipant(participant)) {\n\t\treturn true;\n\t} else if (const auto chat = _peer->asChat()) {\n\t\treturn user\n\t\t\t&& !user->isSelf()\n\t\t\t&& chat->invitedByMe.contains(user)\n\t\t\t&& (chat->amCreator() || !_admins.contains(user));\n\t}\n\treturn false;\n}\n\nauto ParticipantsAdditionalData::adminRights(\n\tnot_null<UserData*> user) const\n-> std::optional<ChatAdminRightsInfo> {\n\tif (const auto chat = _peer->asChat()) {\n\t\treturn _admins.contains(user)\n\t\t\t? std::make_optional(chat->defaultAdminRights(user))\n\t\t\t: std::nullopt;\n\t}\n\tconst auto i = _adminRights.find(user);\n\treturn (i != end(_adminRights))\n\t\t? std::make_optional(i->second)\n\t\t: std::nullopt;\n}\n\nQString ParticipantsAdditionalData::memberRank(\n\t\tnot_null<UserData*> user) const {\n\tconst auto i = _memberRanks.find(user);\n\treturn (i != end(_memberRanks)) ? i->second : QString();\n}\n\nTimeId ParticipantsAdditionalData::adminPromotedSince(\n\t\tnot_null<UserData*> user) const {\n\tconst auto i = _adminPromotedSince.find(user);\n\treturn (i != end(_adminPromotedSince)) ? i->second : TimeId(0);\n}\n\nTimeId ParticipantsAdditionalData::restrictedSince(\n\t\tnot_null<PeerData*> peer) const {\n\tconst auto i = _restrictedSince.find(peer);\n\treturn (i != end(_restrictedSince)) ? i->second : TimeId(0);\n}\n\nTimeId ParticipantsAdditionalData::memberSince(\n\t\tnot_null<UserData*> user) const {\n\tconst auto i = _memberSince.find(user);\n\treturn (i != end(_memberSince)) ? i->second : TimeId(0);\n}\n\nauto ParticipantsAdditionalData::restrictedRights(\n\tnot_null<PeerData*> participant) const\n-> std::optional<ChatRestrictionsInfo> {\n\tif (_peer->isChat()) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto i = _restrictedRights.find(participant);\n\treturn (i != end(_restrictedRights))\n\t\t? std::make_optional(i->second)\n\t\t: std::nullopt;\n}\n\nbool ParticipantsAdditionalData::isCreator(not_null<UserData*> user) const {\n\treturn (_creator == user);\n}\n\nbool ParticipantsAdditionalData::isExternal(\n\t\tnot_null<PeerData*> participant) const {\n\treturn _peer->isChat()\n\t\t? (participant->isUser()\n\t\t\t&& !_members.contains(participant->asUser()))\n\t\t: _external.find(participant) != end(_external);\n}\n\nbool ParticipantsAdditionalData::isKicked(\n\t\tnot_null<PeerData*> participant) const {\n\treturn !_peer->isChat() && (_kicked.find(participant) != end(_kicked));\n}\n\nUserData *ParticipantsAdditionalData::adminPromotedBy(\n\t\tnot_null<UserData*> user) const {\n\tif (_peer->isChat()) {\n\t\treturn _admins.contains(user) ? _creator : nullptr;\n\t}\n\tconst auto i = _adminPromotedBy.find(user);\n\treturn (i != end(_adminPromotedBy)) ? i->second.get() : nullptr;\n}\n\nUserData *ParticipantsAdditionalData::restrictedBy(\n\t\tnot_null<PeerData*> participant) const {\n\tif (_peer->isChat()) {\n\t\treturn nullptr;\n\t}\n\tconst auto i = _restrictedBy.find(participant);\n\treturn (i != end(_restrictedBy)) ? i->second.get() : nullptr;\n}\n\nvoid ParticipantsAdditionalData::setExternal(\n\t\tnot_null<PeerData*> participant) {\n\tif (const auto user = participant->asUser()) {\n\t\t_adminRights.erase(user);\n\t\t_adminCanEdit.erase(user);\n\t\t_adminPromotedBy.erase(user);\n\t\t_admins.erase(user);\n\t}\n\t_restrictedRights.erase(participant);\n\t_kicked.erase(participant);\n\t_restrictedBy.erase(participant);\n\t_infoNotLoaded.erase(participant);\n\t_external.emplace(participant);\n}\n\nvoid ParticipantsAdditionalData::checkForLoaded(\n\t\tnot_null<PeerData*> participant) {\n\tconst auto contains = [](const auto &map, const auto &value) {\n\t\treturn map.find(value) != map.end();\n\t};\n\tconst auto user = participant->asUser();\n\tif (!(user && _creator == user)\n\t\t&& !(user && contains(_adminRights, user))\n\t\t&& !contains(_restrictedRights, participant)\n\t\t&& !contains(_external, participant)\n\t\t&& !contains(_kicked, participant)) {\n\t\t_infoNotLoaded.emplace(participant);\n\t}\n}\n\nvoid ParticipantsAdditionalData::fillFromPeer() {\n\tif (const auto chat = _peer->asChat()) {\n\t\tfillFromChat(chat);\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\tfillFromChannel(channel);\n\t} else {\n\t\tUnexpected(\"Peer in ParticipantsAdditionalData::fillFromPeer.\");\n\t}\n}\n\nvoid ParticipantsAdditionalData::fillFromChat(not_null<ChatData*> chat) {\n\tif (const auto creator = chat->owner().userLoaded(chat->creator)) {\n\t\t_creator = creator;\n\t}\n\tif (chat->participants.empty()) {\n\t\treturn;\n\t}\n\t_members = chat->participants;\n\t_admins = chat->admins;\n\tfor (const auto &[uid, rank] : chat->memberRanks) {\n\t\t_memberRanks[chat->owner().user(uid)] = rank;\n\t}\n}\n\nvoid ParticipantsAdditionalData::fillFromChannel(\n\t\tnot_null<ChannelData*> channel) {\n\tconst auto information = channel->mgInfo.get();\n\tif (!information || !channel->canViewMembers()) {\n\t\treturn;\n\t}\n\tif (information->creator) {\n\t\t_creator = information->creator;\n\t}\n\tfor (const auto &user : information->lastParticipants) {\n\t\tconst auto admin = information->lastAdmins.find(user);\n\t\tconst auto rank = information->memberRanks.find(peerToUser(user->id));\n\t\tconst auto restricted = information->lastRestricted.find(user);\n\t\tif (admin != information->lastAdmins.cend()) {\n\t\t\t_restrictedRights.erase(user);\n\t\t\t_kicked.erase(user);\n\t\t\t_restrictedBy.erase(user);\n\t\t\tif (admin->second.canEdit) {\n\t\t\t\t_adminCanEdit.emplace(user);\n\t\t\t} else {\n\t\t\t\t_adminCanEdit.erase(user);\n\t\t\t}\n\t\t\t_adminRights.emplace(user, admin->second.rights);\n\t\t\tif (rank != end(information->memberRanks)\n\t\t\t\t&& !rank->second.isEmpty()) {\n\t\t\t\t_memberRanks[user] = rank->second;\n\t\t\t}\n\t\t} else if (restricted != information->lastRestricted.cend()) {\n\t\t\t_adminRights.erase(user);\n\t\t\t_adminCanEdit.erase(user);\n\t\t\t_adminPromotedBy.erase(user);\n\t\t\t_restrictedRights.emplace(user, restricted->second.rights);\n\t\t}\n\t}\n}\n\nvoid ParticipantsAdditionalData::applyAdminLocally(\n\t\tUserData *user,\n\t\tChatAdminRightsInfo rights,\n\t\tconst QString &rank) {\n\tif (isCreator(user) && user->isSelf()) {\n\t\tapplyParticipant(Api::ChatParticipant(\n\t\t\tApi::ChatParticipant::Type::Creator,\n\t\t\tuser->id,\n\t\t\tUserId(),\n\t\t\tChatRestrictionsInfo(),\n\t\t\tstd::move(rights),\n\t\t\ttrue, // As the creator is self.\n\t\t\trank));\n\t} else if (!rights.flags) {\n\t\tapplyParticipant(Api::ChatParticipant(\n\t\t\tApi::ChatParticipant::Type::Member,\n\t\t\tuser->id,\n\t\t\tUserId(),\n\t\t\tChatRestrictionsInfo(),\n\t\t\tChatAdminRightsInfo()));\n\t} else {\n\t\tconst auto alreadyPromotedBy = adminPromotedBy(user);\n\t\tapplyParticipant(Api::ChatParticipant(\n\t\t\tApi::ChatParticipant::Type::Admin,\n\t\t\tuser->id,\n\t\t\talreadyPromotedBy\n\t\t\t\t? peerToUser(alreadyPromotedBy->id)\n\t\t\t\t: user->session().userId(),\n\t\t\tChatRestrictionsInfo(),\n\t\t\tstd::move(rights),\n\t\t\ttrue,\n\t\t\trank));\n\t}\n}\n\nvoid ParticipantsAdditionalData::applyBannedLocally(\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo rights) {\n\tconst auto user = participant->asUser();\n\tif (!rights.flags) {\n\t\tif (user) {\n\t\t\tapplyParticipant(Api::ChatParticipant(\n\t\t\t\tApi::ChatParticipant::Type::Member,\n\t\t\t\tuser->id,\n\t\t\t\tUserId(),\n\t\t\t\tChatRestrictionsInfo(),\n\t\t\t\tChatAdminRightsInfo()));\n\t\t} else {\n\t\t\tsetExternal(participant);\n\t\t}\n\t} else {\n\t\tconst auto kicked = rights.flags & ChatRestriction::ViewMessages;\n\t\tconst auto alreadyRestrictedBy = restrictedBy(participant);\n\t\tapplyParticipant(Api::ChatParticipant(\n\t\t\tkicked\n\t\t\t\t? Api::ChatParticipant::Type::Banned\n\t\t\t\t: Api::ChatParticipant::Type::Restricted,\n\t\t\tparticipant->id,\n\t\t\talreadyRestrictedBy\n\t\t\t\t? peerToUser(alreadyRestrictedBy->id)\n\t\t\t\t: participant->session().userId(),\n\t\t\tstd::move(rights),\n\t\t\tChatAdminRightsInfo()));\n\t}\n}\n\nvoid ParticipantsAdditionalData::applyMemberRankLocally(\n\t\tnot_null<UserData*> user,\n\t\tconst QString &rank) {\n\tif (rank.isEmpty()) {\n\t\t_memberRanks.remove(user);\n\t} else {\n\t\t_memberRanks[user] = rank;\n\t}\n}\n\nPeerData *ParticipantsAdditionalData::applyParticipant(\n\t\tconst Api::ChatParticipant &data) {\n\treturn applyParticipant(data, _role);\n}\n\nPeerData *ParticipantsAdditionalData::applyParticipant(\n\t\tconst Api::ChatParticipant &data,\n\t\tRole overrideRole) {\n\tconst auto logBad = [&]() -> PeerData* {\n\t\tLOG((\"API Error: Bad participant type %1 got \"\n\t\t\t\"while requesting for participants, role: %2\"\n\t\t\t).arg(static_cast<int>(data.type())\n\t\t\t).arg(static_cast<int>(overrideRole)));\n\t\treturn nullptr;\n\t};\n\n\tconst auto result = [&]() -> PeerData* {\n\t\tswitch (data.type()) {\n\t\tcase Api::ChatParticipant::Type::Creator: {\n\t\t\tif (overrideRole != Role::Profile\n\t\t\t\t&& overrideRole != Role::Members\n\t\t\t\t&& overrideRole != Role::Admins) {\n\t\t\t\treturn logBad();\n\t\t\t}\n\t\t\treturn applyCreator(data);\n\t\t}\n\t\tcase Api::ChatParticipant::Type::Admin: {\n\t\t\tif (overrideRole != Role::Profile\n\t\t\t\t&& overrideRole != Role::Members\n\t\t\t\t&& overrideRole != Role::Admins) {\n\t\t\t\treturn logBad();\n\t\t\t}\n\t\t\treturn applyAdmin(data);\n\t\t}\n\t\tcase Api::ChatParticipant::Type::Member: {\n\t\t\tif (overrideRole != Role::Profile\n\t\t\t\t&& overrideRole != Role::Members) {\n\t\t\t\treturn logBad();\n\t\t\t}\n\t\t\treturn applyRegular(data.userId());\n\t\t}\n\t\tcase Api::ChatParticipant::Type::Restricted:\n\t\tcase Api::ChatParticipant::Type::Banned:\n\t\t\tif (overrideRole != Role::Profile\n\t\t\t\t&& overrideRole != Role::Members\n\t\t\t\t&& overrideRole != Role::Restricted\n\t\t\t\t&& overrideRole != Role::Kicked) {\n\t\t\t\treturn logBad();\n\t\t\t}\n\t\t\treturn applyBanned(data);\n\t\tcase Api::ChatParticipant::Type::Left:\n\t\t\treturn logBad();\n\t\t};\n\t\tUnexpected(\"Api::ChatParticipant::type in applyParticipant.\");\n\t}();\n\tif (const auto user = result ? result->asUser() : nullptr) {\n\t\tif (!data.rank().isEmpty()) {\n\t\t\t_memberRanks[user] = data.rank();\n\t\t} else {\n\t\t\t_memberRanks.remove(user);\n\t\t}\n\t}\n\treturn result;\n}\n\nUserData *ParticipantsAdditionalData::applyCreator(\n\t\tconst Api::ChatParticipant &data) {\n\tif (const auto user = applyRegular(data.userId())) {\n\t\t_creator = user;\n\t\t_adminRights[user] = data.rights();\n\t\tif (user->isSelf()) {\n\t\t\t_adminCanEdit.emplace(user);\n\t\t} else {\n\t\t\t_adminCanEdit.erase(user);\n\t\t}\n\t\treturn user;\n\t}\n\treturn nullptr;\n}\n\nUserData *ParticipantsAdditionalData::applyAdmin(\n\t\tconst Api::ChatParticipant &data) {\n\tconst auto user = _peer->owner().userLoaded(data.userId());\n\tif (!user) {\n\t\treturn nullptr;\n\t} else if (_peer->isChat()) {\n\t\t// This can come from saveAdmin callback.\n\t\t_admins.emplace(user);\n\t\treturn user;\n\t}\n\n\t_infoNotLoaded.erase(user);\n\t_restrictedRights.erase(user);\n\t_kicked.erase(user);\n\t_restrictedBy.erase(user);\n\t_adminRights[user] = data.rights();\n\tif (data.canBeEdited()) {\n\t\t_adminCanEdit.emplace(user);\n\t} else {\n\t\t_adminCanEdit.erase(user);\n\t}\n\tif (data.promotedSince()) {\n\t\t_adminPromotedSince[user] = data.promotedSince();\n\t} else {\n\t\t_adminPromotedSince.remove(user);\n\t}\n\tif (const auto by = _peer->owner().userLoaded(data.by())) {\n\t\tconst auto i = _adminPromotedBy.find(user);\n\t\tif (i == _adminPromotedBy.end()) {\n\t\t\t_adminPromotedBy.emplace(user, by);\n\t\t} else {\n\t\t\ti->second = by;\n\t\t}\n\t} else {\n\t\tLOG((\"API Error: No user %1 for admin promoted by.\"\n\t\t\t).arg(data.by().bare));\n\t}\n\treturn user;\n}\n\nUserData *ParticipantsAdditionalData::applyRegular(UserId userId) {\n\tconst auto user = _peer->owner().userLoaded(userId);\n\tif (!user) {\n\t\treturn nullptr;\n\t} else if (_peer->isChat()) {\n\t\t// This can come from saveAdmin or saveRestricted callback.\n\t\t_admins.erase(user);\n\t\treturn user;\n\t}\n\n\t_infoNotLoaded.erase(user);\n\t_adminRights.erase(user);\n\t_adminCanEdit.erase(user);\n\t_adminPromotedBy.erase(user);\n\t_restrictedRights.erase(user);\n\t_kicked.erase(user);\n\t_restrictedBy.erase(user);\n\treturn user;\n}\n\nPeerData *ParticipantsAdditionalData::applyBanned(\n\t\tconst Api::ChatParticipant &data) {\n\tconst auto participant = _peer->owner().peerLoaded(data.id());\n\tif (!participant) {\n\t\treturn nullptr;\n\t}\n\n\t_infoNotLoaded.erase(participant);\n\tif (const auto user = participant->asUser()) {\n\t\t_adminRights.erase(user);\n\t\t_adminCanEdit.erase(user);\n\t\t_adminPromotedBy.erase(user);\n\t}\n\tif (data.isKicked()) {\n\t\t_kicked.emplace(participant);\n\t} else {\n\t\t_kicked.erase(participant);\n\t}\n\tif (data.restrictedSince()) {\n\t\t_restrictedSince[participant] = data.restrictedSince();\n\t} else {\n\t\t_restrictedSince.remove(participant);\n\t}\n\t_restrictedRights[participant] = data.restrictions();\n\tif (const auto by = _peer->owner().userLoaded(data.by())) {\n\t\tconst auto i = _restrictedBy.find(participant);\n\t\tif (i == _restrictedBy.end()) {\n\t\t\t_restrictedBy.emplace(participant, by);\n\t\t} else {\n\t\t\ti->second = by;\n\t\t}\n\t}\n\treturn participant;\n}\n\nvoid ParticipantsAdditionalData::migrate(\n\t\tnot_null<ChatData*> chat,\n\t\tnot_null<ChannelData*> channel) {\n\t_peer = channel;\n\tfillFromChannel(channel);\n\n\tfor (const auto &user : _admins) {\n\t\t_adminRights.emplace(user, chat->defaultAdminRights(user));\n\t\tif (channel->amCreator()) {\n\t\t\t_adminCanEdit.emplace(user);\n\t\t}\n\t\tif (_creator) {\n\t\t\t_adminPromotedBy.emplace(user, _creator);\n\t\t}\n\t}\n}\n\nParticipantsOnlineSorter::ParticipantsOnlineSorter(\n\tnot_null<PeerData*> peer,\n\tnot_null<PeerListDelegate*> delegate)\n: _peer(peer)\n, _delegate(delegate)\n, _sortByOnlineTimer([=] { sort(); }) {\n\tpeer->session().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::OnlineStatus\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tconst auto peerId = update.peer->id;\n\t\tif (const auto row = _delegate->peerListFindRow(peerId.value)) {\n\t\t\trow->refreshStatus();\n\t\t\tsortDelayed();\n\t\t}\n\t}, _lifetime);\n\tsort();\n}\n\nvoid ParticipantsOnlineSorter::sortDelayed() {\n\tif (!_sortByOnlineTimer.isActive()) {\n\t\t_sortByOnlineTimer.callOnce(kSortByOnlineDelay);\n\t}\n}\n\nvoid ParticipantsOnlineSorter::sort() {\n\tconst auto channel = _peer->asChannel();\n\tif (channel\n\t\t&& (!channel->isMegagroup()\n\t\t\t|| (channel->membersCount()\n\t\t\t\t> channel->session().serverConfig().chatSizeMax))) {\n\t\t_onlineCount = 0;\n\t\treturn;\n\t}\n\tconst auto now = base::unixtime::now();\n\t_delegate->peerListSortRows([&](\n\t\t\tconst PeerListRow &a,\n\t\t\tconst PeerListRow &b) {\n\t\treturn Data::SortByOnlineValue(a.peer()->asUser(), now) >\n\t\t\tData::SortByOnlineValue(b.peer()->asUser(), now);\n\t});\n\trefreshOnlineCount();\n}\n\nrpl::producer<int> ParticipantsOnlineSorter::onlineCountValue() const {\n\treturn _onlineCount.value();\n}\n\nvoid ParticipantsOnlineSorter::refreshOnlineCount() {\n\tconst auto now = base::unixtime::now();\n\tauto left = 0, right = _delegate->peerListFullRowsCount();\n\twhile (right > left) {\n\t\tconst auto middle = (left + right) / 2;\n\t\tconst auto row = _delegate->peerListRowAt(middle);\n\t\tif (Data::OnlineTextActive(row->peer()->asUser(), now)) {\n\t\t\tleft = middle + 1;\n\t\t} else {\n\t\t\tright = middle;\n\t\t}\n\t}\n\t_onlineCount = left;\n}\n\nParticipantsBoxController::SavedState::SavedState(\n\tconst ParticipantsAdditionalData &additional)\n: additional(additional) {\n}\n\nParticipantsBoxController::ParticipantsBoxController(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tRole role)\n: ParticipantsBoxController(CreateTag(), navigation, peer, role) {\n}\n\nParticipantsBoxController::~ParticipantsBoxController() = default;\n\nParticipantsBoxController::ParticipantsBoxController(\n\tCreateTag,\n\tWindow::SessionNavigation *navigation,\n\tnot_null<PeerData*> peer,\n\tRole role)\n: PeerListController(CreateSearchController(peer, role, &_additional))\n, _chatStyle(\n\tstd::make_unique<Ui::ChatStyle>(peer->session().colorIndicesValue()))\n, _navigation(navigation)\n, _peer(peer)\n, _api(&_peer->session().mtp())\n, _role(role)\n, _additional(peer, _role) {\n\tsubscribeToMigration();\n\tif (_role == Role::Profile) {\n\t\tsetupListChangeViewers();\n\t}\n\tif (const auto channel = _peer->asChannel()) {\n\t\tsubscribeToCreatorChange(channel);\n\t}\n}\n\nMain::Session &ParticipantsBoxController::session() const {\n\treturn _peer->session();\n}\n\nvoid ParticipantsBoxController::setupListChangeViewers() {\n\tconst auto channel = _peer->asChannel();\n\tif (!channel || !channel->isMegagroup()) {\n\t\treturn;\n\t}\n\n\tchannel->owner().megagroupParticipantAdded(\n\t\tchannel\n\t) | rpl::on_next([=](not_null<UserData*> user) {\n\t\tif (delegate()->peerListFullRowsCount() > 0) {\n\t\t\tif (delegate()->peerListRowAt(0)->peer() == user) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (delegate()->peerListFindRow(user->id.value)) {\n\t\t\tdelegate()->peerListPartitionRows([&](const PeerListRow &row) {\n\t\t\t\treturn (row.peer() == user);\n\t\t\t});\n\t\t} else if (auto row = createRow(user)) {\n\t\t\tconst auto raw = row.get();\n\t\t\tdelegate()->peerListPrependRow(std::move(row));\n\t\t\tif (_stories) {\n\t\t\t\t_stories->process(raw);\n\t\t\t}\n\t\t\trefreshRows();\n\t\t\tif (_onlineSorter) {\n\t\t\t\t_onlineSorter->sort();\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\tchannel->owner().megagroupParticipantRemoved(\n\t\tchannel\n\t) | rpl::on_next([=](not_null<UserData*> user) {\n\t\tif (const auto row = delegate()->peerListFindRow(user->id.value)) {\n\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t}\n\t\trefreshRows();\n\t}, lifetime());\n}\n\nauto ParticipantsBoxController::CreateSearchController(\n\tnot_null<PeerData*> peer,\n\tRole role,\n\tnot_null<ParticipantsAdditionalData*> additional)\n-> std::unique_ptr<PeerListSearchController> {\n\tconst auto channel = peer->asChannel();\n\n\t// In admins box complex search is used for adding new admins.\n\tif (channel && (role != Role::Admins || channel->canAddAdmins())) {\n\t\treturn std::make_unique<ParticipantsBoxSearchController>(\n\t\t\tchannel,\n\t\t\trole,\n\t\t\tadditional);\n\t}\n\treturn nullptr;\n}\n\nvoid ParticipantsBoxController::Start(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tRole role) {\n\tauto controller = std::make_unique<ParticipantsBoxController>(\n\t\tnavigation,\n\t\tpeer,\n\t\trole);\n\tauto initBox = [=, controller = controller.get()](\n\t\t\tnot_null<PeerListBox*> box) {\n\t\tbox->addButton(tr::lng_close(), [=] { box->closeBox(); });\n\n\t\tconst auto chat = peer->asChat();\n\t\tconst auto channel = peer->asChannel();\n\t\tconst auto canAddNewItem = [&] {\n\t\t\tAssert(chat != nullptr || channel != nullptr);\n\n\t\t\tswitch (role) {\n\t\t\tcase Role::Members:\n\t\t\t\treturn chat\n\t\t\t\t\t? chat->canAddMembers()\n\t\t\t\t\t: (channel->canAddMembers()\n\t\t\t\t\t\t&& (channel->isMegagroup()\n\t\t\t\t\t\t\t|| (channel->membersCount()\n\t\t\t\t\t\t\t\t< channel->session().serverConfig().chatSizeMax)));\n\t\t\tcase Role::Admins:\n\t\t\t\treturn chat\n\t\t\t\t\t? chat->canAddAdmins()\n\t\t\t\t\t: channel->canAddAdmins();\n\t\t\tcase Role::Restricted:\n\t\t\tcase Role::Kicked:\n\t\t\t\treturn chat\n\t\t\t\t\t? chat->canBanMembers()\n\t\t\t\t\t: channel->canBanMembers();\n\t\t\t}\n\n\t\t\tUnexpected(\"Role value in ParticipantsBoxController::Start()\");\n\t\t}();\n\t\tauto addNewItemText = [&] {\n\t\t\tswitch (role) {\n\t\t\tcase Role::Members:\n\t\t\t\treturn (chat || channel->isMegagroup())\n\t\t\t\t\t? tr::lng_channel_add_members()\n\t\t\t\t\t: tr::lng_channel_add_users();\n\t\t\tcase Role::Admins:\n\t\t\t\treturn tr::lng_channel_add_admin();\n\t\t\tcase Role::Restricted:\n\t\t\t\treturn tr::lng_channel_add_exception();\n\t\t\tcase Role::Kicked:\n\t\t\t\treturn tr::lng_channel_add_removed();\n\t\t\t}\n\t\t\tUnexpected(\"Role value in ParticipantsBoxController::Start()\");\n\t\t}();\n\t\tif (canAddNewItem) {\n\t\t\tbox->addLeftButton(std::move(addNewItemText), [=] {\n\t\t\t\tcontroller->addNewItem();\n\t\t\t});\n\t\t}\n\t};\n\tnavigation->parentController()->show(\n\t\tBox<PeerListBox>(std::move(controller), initBox));\n}\n\nvoid ParticipantsBoxController::addNewItem() {\n\tExpects(_role != Role::Profile);\n\n\tif (_role == Role::Members) {\n\t\taddNewParticipants();\n\t\treturn;\n\t}\n\tconst auto adminDone = crl::guard(this, [=](\n\t\t\tnot_null<UserData*> user,\n\t\t\tChatAdminRightsInfo rights,\n\t\t\tconst std::optional<QString> &rank) {\n\t\teditAdminDone(user, rights, rank);\n\t});\n\tconst auto restrictedDone = crl::guard(this, [=](\n\t\t\tnot_null<PeerData*> participant,\n\t\t\tChatRestrictionsInfo rights) {\n\t\teditRestrictedDone(participant, rights);\n\t});\n\tconst auto initBox = [](not_null<PeerListBox*> box) {\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t};\n\n\t_addBox = showBox(\n\t\tBox<PeerListBox>(\n\t\t\tstd::make_unique<AddSpecialBoxController>(\n\t\t\t\t_peer,\n\t\t\t\t_role,\n\t\t\t\tadminDone,\n\t\t\t\trestrictedDone),\n\t\t\tinitBox));\n}\n\nvoid ParticipantsBoxController::addNewParticipants() {\n\tExpects(_navigation != nullptr);\n\n\tconst auto chat = _peer->asChat();\n\tconst auto channel = _peer->asChannel();\n\tif (chat) {\n\t\tAddParticipantsBoxController::Start(_navigation, chat);\n\t} else if (channel->isMegagroup()\n\t\t|| (channel->membersCount()\n\t\t\t< channel->session().serverConfig().chatSizeMax)) {\n\t\tconst auto count = delegate()->peerListFullRowsCount();\n\t\tauto already = std::vector<not_null<UserData*>>();\n\t\talready.reserve(count);\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tconst auto participant = delegate()->peerListRowAt(i)->peer();\n\t\t\tif (const auto user = participant->asUser()) {\n\t\t\t\talready.emplace_back(user);\n\t\t\t}\n\t\t}\n\t\tAddParticipantsBoxController::Start(\n\t\t\t_navigation,\n\t\t\tchannel,\n\t\t\t{ already.begin(), already.end() });\n\t} else {\n\t\tshowBox(Box<MaxInviteBox>(channel));\n\t}\n}\n\nvoid ParticipantsBoxController::peerListSearchAddRow(\n\t\tnot_null<PeerData*> peer) {\n\tPeerListController::peerListSearchAddRow(peer);\n\tif (_role == Role::Restricted\n\t\t&& delegate()->peerListFullRowsCount() > 0) {\n\t\tsetDescriptionText(QString());\n\t}\n}\n\nstd::unique_ptr<PeerListRow> ParticipantsBoxController::createSearchRow(\n\t\tnot_null<PeerData*> peer) {\n\tif (_role == Role::Profile\n\t\t|| _role == Role::Members\n\t\t|| _role == Role::Admins) {\n\t\tif (const auto user = peer->asUser()) {\n\t\t\treturn createRow(user);\n\t\t}\n\t\treturn nullptr;\n\t}\n\treturn createRow(peer);\n}\n\nstd::unique_ptr<PeerListRow> ParticipantsBoxController::createRestoredRow(\n\t\tnot_null<PeerData*> peer) {\n\treturn createSearchRow(peer);\n}\n\nauto ParticipantsBoxController::saveState() const\n-> std::unique_ptr<PeerListState> {\n\tExpects(_role == Role::Profile);\n\n\tauto result = PeerListController::saveState();\n\n\tauto my = std::make_unique<SavedState>(_additional);\n\tmy->offset = _offset;\n\tmy->allLoaded = _allLoaded;\n\tmy->wasLoading = (_loadRequestId != 0);\n\tif (const auto search = searchController()) {\n\t\tmy->searchState = search->saveState();\n\t}\n\n\tconst auto weak = result.get();\n\tif (const auto chat = _peer->asChat()) {\n\t\tchat->session().changes().peerUpdates(\n\t\t\tchat,\n\t\t\tData::PeerUpdate::Flag::Members\n\t\t) | rpl::on_next([=] {\n\t\t\tweak->controllerState = nullptr;\n\t\t}, my->lifetime);\n\t} else if (const auto channel = _peer->asMegagroup()) {\n\t\tchannel->owner().megagroupParticipantAdded(\n\t\t\tchannel\n\t\t) | rpl::on_next([=](not_null<UserData*> user) {\n\t\t\tif (!weak->list.empty()) {\n\t\t\t\tif (weak->list[0] == user) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tauto pos = ranges::find(weak->list, user);\n\t\t\tif (pos == weak->list.cend()) {\n\t\t\t\tweak->list.emplace_back(user);\n\t\t\t}\n\t\t\tranges::stable_partition(\n\t\t\t\tweak->list,\n\t\t\t\t[user](auto peer) { return (peer == user); });\n\t\t}, my->lifetime);\n\n\t\tchannel->owner().megagroupParticipantRemoved(\n\t\t\tchannel\n\t\t) | rpl::on_next([=](not_null<UserData*> user) {\n\t\t\tweak->list.erase(std::remove(\n\t\t\t\tweak->list.begin(),\n\t\t\t\tweak->list.end(),\n\t\t\t\tuser), weak->list.end());\n\t\t\tweak->filterResults.erase(std::remove(\n\t\t\t\tweak->filterResults.begin(),\n\t\t\t\tweak->filterResults.end(),\n\t\t\t\tuser), weak->filterResults.end());\n\t\t}, my->lifetime);\n\t}\n\tresult->controllerState = std::move(my);\n\treturn result;\n}\n\nvoid ParticipantsBoxController::restoreState(\n\t\tstd::unique_ptr<PeerListState> state) {\n\tauto typeErasedState = state\n\t\t? state->controllerState.get()\n\t\t: nullptr;\n\tif (const auto my = dynamic_cast<SavedState*>(typeErasedState)) {\n\t\tif (const auto requestId = base::take(_loadRequestId)) {\n\t\t\t_api.request(requestId).cancel();\n\t\t}\n\n\t\t_additional = std::move(my->additional);\n\t\t_offset = my->offset;\n\t\t_allLoaded = my->allLoaded;\n\t\tif (const auto search = searchController()) {\n\t\t\tsearch->restoreState(std::move(my->searchState));\n\t\t}\n\t\tif (my->wasLoading) {\n\t\t\tloadMoreRows();\n\t\t}\n\t\tconst auto was = _fullCountValue.current();\n\t\tPeerListController::restoreState(std::move(state));\n\t\tconst auto now = delegate()->peerListFullRowsCount();\n\t\tif (now > 0 || _allLoaded) {\n\t\t\trefreshDescription();\n\t\t\tif (_stories) {\n\t\t\t\tfor (auto i = 0; i != now; ++i) {\n\t\t\t\t\t_stories->process(delegate()->peerListRowAt(i));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (now != was) {\n\t\t\t\trefreshRows();\n\t\t\t}\n\t\t}\n\t\tif (_onlineSorter) {\n\t\t\t_onlineSorter->sort();\n\t\t}\n\t}\n}\n\nrpl::producer<int> ParticipantsBoxController::onlineCountValue() const {\n\treturn _onlineCountValue.value();\n}\n\nrpl::producer<int> ParticipantsBoxController::fullCountValue() const {\n\treturn _fullCountValue.value();\n}\n\nvoid ParticipantsBoxController::setStoriesShown(bool shown) {\n\t_stories = std::make_unique<PeerListStories>(\n\t\tthis,\n\t\t&_navigation->session());\n}\n\nvoid ParticipantsBoxController::prepare() {\n\tauto title = [&] {\n\t\tswitch (_role) {\n\t\tcase Role::Admins: return tr::lng_channel_admins();\n\t\tcase Role::Profile:\n\t\tcase Role::Members:\n\t\t\treturn ((_peer->isChannel() && !_peer->isMegagroup())\n\t\t\t\t? tr::lng_profile_subscribers_section()\n\t\t\t\t: tr::lng_profile_participants_section());\n\t\tcase Role::Restricted: return tr::lng_exceptions_list_title();\n\t\tcase Role::Kicked: return tr::lng_removed_list_title();\n\t\t}\n\t\tUnexpected(\"Role in ParticipantsBoxController::prepare()\");\n\t}();\n\tif (const auto megagroup = _peer->asMegagroup()) {\n\t\tif (_role == Role::Members) {\n\t\t\tdelegate()->peerListSetAboveWidget(CreateMembersVisibleButton(\n\t\t\t\tmegagroup));\n\t\t} else if ((_role == Role::Admins)\n\t\t\t&& (megagroup->amCreator() || megagroup->hasAdminRights())) {\n\t\t\tconst auto validator = AntiSpamMenu::AntiSpamValidator(\n\t\t\t\t_navigation->parentController(),\n\t\t\t\tmegagroup);\n\t\t\tdelegate()->peerListSetAboveWidget(validator.createButton());\n\t\t}\n\t}\n\tdelegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);\n\tdelegate()->peerListSetTitle(std::move(title));\n\tsetDescriptionText(tr::lng_contacts_loading(tr::now));\n\tsetSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now));\n\n\tif (_stories) {\n\t\t_stories->prepare(delegate());\n\t}\n\n\tif (_role == Role::Profile) {\n\t\tauto visible = _peer->isMegagroup()\n\t\t\t? Info::Profile::CanViewParticipantsValue(_peer->asMegagroup())\n\t\t\t: rpl::single(true);\n\t\tstd::move(visible) | rpl::on_next([=](bool visible) {\n\t\t\tif (!visible) {\n\t\t\t\t_onlineCountValue = 0;\n\t\t\t\t_onlineSorter = nullptr;\n\t\t\t} else if (!_onlineSorter) {\n\t\t\t\t_onlineSorter = std::make_unique<ParticipantsOnlineSorter>(\n\t\t\t\t\t_peer,\n\t\t\t\t\tdelegate());\n\t\t\t\t_onlineCountValue = _onlineSorter->onlineCountValue();\n\t\t\t}\n\t\t\tunload();\n\t\t\trebuild();\n\t\t}, lifetime());\n\t} else {\n\t\trebuild();\n\t}\n\n\t_peer->session().changes().chatAdminChanges(\n\t) | rpl::on_next([=](const Data::ChatAdminChange &update) {\n\t\tif (update.peer != _peer) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto user = update.user;\n\t\tconst auto rights = ChatAdminRightsInfo(update.rights);\n\t\tconst auto rank = update.rank;\n\t\t_additional.applyAdminLocally(user, rights, rank);\n\t\tif (!_additional.isCreator(user) || !user->isSelf()) {\n\t\t\tif (!rights.flags) {\n\t\t\t\tif (_role == Role::Admins) {\n\t\t\t\t\tremoveRow(user);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (_role == Role::Admins) {\n\t\t\t\t\tprependRow(user);\n\t\t\t\t} else if (_role == Role::Kicked\n\t\t\t\t\t|| _role == Role::Restricted) {\n\t\t\t\t\tremoveRow(user);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\trecomputeTypeFor(user);\n\t\trefreshRows();\n\t}, lifetime());\n\n\t_peer->session().changes().chatMemberRankChanges(\n\t) | rpl::on_next([=](const Data::ChatMemberRankChange &update) {\n\t\tif (update.peer != _peer) {\n\t\t\treturn;\n\t\t}\n\t\t_additional.applyMemberRankLocally(update.user, update.rank);\n\t\trecomputeTypeFor(update.user);\n\t\trefreshRows();\n\t}, lifetime());\n\n\tstyle::PaletteChanged() | rpl::on_next([=] {\n\t\t_pillCircleCache.clear();\n\t}, lifetime());\n}\n\nvoid ParticipantsBoxController::unload() {\n\twhile (delegate()->peerListFullRowsCount() > 0) {\n\t\tdelegate()->peerListRemoveRow(\n\t\t\tdelegate()->peerListRowAt(\n\t\t\t\tdelegate()->peerListFullRowsCount() - 1));\n\t}\n\tif (const auto requestId = base::take(_loadRequestId)) {\n\t\t_api.request(requestId).cancel();\n\t}\n\t_allLoaded = false;\n\t_offset = 0;\n}\n\nvoid ParticipantsBoxController::rebuild() {\n\tif (const auto chat = _peer->asChat()) {\n\t\tprepareChatRows(chat);\n\t} else {\n\t\tloadMoreRows();\n\t}\n\trefreshRows();\n}\n\nbase::weak_qptr<Ui::BoxContent> ParticipantsBoxController::showBox(\n\t\tobject_ptr<Ui::BoxContent> box) const {\n\tconst auto weak = base::make_weak(box.data());\n\tdelegate()->peerListUiShow()->showBox(std::move(box));\n\treturn weak;\n}\n\nvoid ParticipantsBoxController::prepareChatRows(not_null<ChatData*> chat) {\n\tif (_role == Role::Profile || _role == Role::Members) {\n\t\t_onlineSorter = std::make_unique<ParticipantsOnlineSorter>(\n\t\t\tchat,\n\t\t\tdelegate());\n\t}\n\n\trebuildChatRows(chat);\n\tif (!delegate()->peerListFullRowsCount()) {\n\t\tchat->updateFullForced();\n\t}\n\n\tusing UpdateFlag = Data::PeerUpdate::Flag;\n\tchat->session().changes().peerUpdates(\n\t\tchat,\n\t\tUpdateFlag::Members | UpdateFlag::Admins\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\t_additional.fillFromPeer();\n\t\tif ((update.flags & UpdateFlag::Members)\n\t\t\t|| (_role == Role::Admins)) {\n\t\t\trebuildChatRows(chat);\n\t\t}\n\t\tif (update.flags & UpdateFlag::Admins) {\n\t\t\trebuildRowTypes();\n\t\t}\n\t}, lifetime());\n}\n\nvoid ParticipantsBoxController::rebuildChatRows(not_null<ChatData*> chat) {\n\tswitch (_role) {\n\tcase Role::Profile:\n\tcase Role::Members: return rebuildChatParticipants(chat);\n\tcase Role::Admins: return rebuildChatAdmins(chat);\n\tcase Role::Restricted:\n\tcase Role::Kicked: return chatListReady();\n\t}\n\tUnexpected(\"Role in ParticipantsBoxController::rebuildChatRows\");\n}\n\nvoid ParticipantsBoxController::rebuildChatParticipants(\n\t\tnot_null<ChatData*> chat) {\n\tif (chat->noParticipantInfo()) {\n\t\tchat->updateFullForced();\n\t\treturn;\n\t}\n\n\tauto &participants = chat->participants;\n\tauto count = delegate()->peerListFullRowsCount();\n\tfor (auto i = 0; i != count;) {\n\t\tauto row = delegate()->peerListRowAt(i);\n\t\tAssert(row->peer()->isUser());\n\t\tauto user = row->peer()->asUser();\n\t\tif (participants.contains(user)) {\n\t\t\t++i;\n\t\t} else {\n\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t\t--count;\n\t\t}\n\t}\n\tfor (const auto &user : participants) {\n\t\tif (!delegate()->peerListFindRow(user->id.value)) {\n\t\t\tif (auto row = createRow(user)) {\n\t\t\t\tconst auto raw = row.get();\n\t\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t\t\tif (_stories) {\n\t\t\t\t\t_stories->process(raw);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t_onlineSorter->sort();\n\n\trefreshRows();\n\tchatListReady();\n}\n\nvoid ParticipantsBoxController::rebuildChatAdmins(\n\t\tnot_null<ChatData*> chat) {\n\tif (chat->participants.empty()) {\n\t\t// We get such updates often\n\t\t// (when participants list was invalidated).\n\t\t//while (delegate()->peerListFullRowsCount() > 0) {\n\t\t//\tdelegate()->peerListRemoveRow(\n\t\t//\t\tdelegate()->peerListRowAt(0));\n\t\t//}\n\t\treturn;\n\t}\n\n\tauto list = ranges::views::all(chat->admins) | ranges::to_vector;\n\tif (const auto creator = chat->owner().userLoaded(chat->creator)) {\n\t\tlist.emplace_back(creator);\n\t}\n\tranges::sort(list, [](not_null<UserData*> a, not_null<UserData*> b) {\n\t\treturn (a->name().compare(b->name(), Qt::CaseInsensitive) < 0);\n\t});\n\n\tconst auto same = [&] {\n\t\tconst auto count = delegate()->peerListFullRowsCount();\n\t\tif (count != list.size()) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tif (list[i] != delegate()->peerListRowAt(i)->peer()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}();\n\tif (same) {\n\t\tif (!_allLoaded && !delegate()->peerListFullRowsCount()) {\n\t\t\tchatListReady();\n\t\t}\n\t\treturn;\n\t}\n\n\twhile (delegate()->peerListFullRowsCount() > 0) {\n\t\tdelegate()->peerListRemoveRow(\n\t\t\tdelegate()->peerListRowAt(0));\n\t}\n\tfor (const auto &user : list) {\n\t\tif (auto row = createRow(user)) {\n\t\t\tconst auto raw = row.get();\n\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t\tif (_stories) {\n\t\t\t\t_stories->process(raw);\n\t\t\t}\n\t\t}\n\t}\n\n\trefreshRows();\n\tchatListReady();\n}\n\nvoid ParticipantsBoxController::chatListReady() {\n\tif (_allLoaded) {\n\t\treturn;\n\t}\n\t_allLoaded = true;\n\trefreshDescription();\n}\n\nvoid ParticipantsBoxController::rebuildRowTypes() {\n\tconst auto count = delegate()->peerListFullRowsCount();\n\tfor (auto i = 0; i != count; ++i) {\n\t\tconst auto row = static_cast<Row*>(\n\t\t\tdelegate()->peerListRowAt(i).get());\n\t\trow->setType(computeType(row->peer()));\n\t}\n\trefreshRows();\n}\n\nvoid ParticipantsBoxController::loadMoreRows() {\n\tif (searchController() && searchController()->loadMoreRows()) {\n\t\treturn;\n\t} else if (!_peer->isChannel() || _loadRequestId || _allLoaded) {\n\t\treturn;\n\t}\n\n\tconst auto channel = _peer->asChannel();\n\tif (feedMegagroupLastParticipants()) {\n\t\treturn;\n\t}\n\n\tconst auto filter = [&] {\n\t\tif (_role == Role::Members || _role == Role::Profile) {\n\t\t\treturn MTP_channelParticipantsRecent();\n\t\t} else if (_role == Role::Admins) {\n\t\t\treturn MTP_channelParticipantsAdmins();\n\t\t} else if (_role == Role::Restricted) {\n\t\t\treturn MTP_channelParticipantsBanned(MTP_string());\n\t\t}\n\t\treturn MTP_channelParticipantsKicked(MTP_string());\n\t}();\n\n\t// First query is small and fast, next loads a lot of rows.\n\tconst auto perPage = (_offset > 0)\n\t\t? kParticipantsPerPage\n\t\t: kParticipantsFirstPageCount;\n\tconst auto participantsHash = uint64(0);\n\n\t_loadRequestId = _api.request(MTPchannels_GetParticipants(\n\t\tchannel->inputChannel(),\n\t\tfilter,\n\t\tMTP_int(_offset),\n\t\tMTP_int(perPage),\n\t\tMTP_long(participantsHash)\n\t)).done([=](const MTPchannels_ChannelParticipants &result) {\n\t\tauto added = false;\n\t\tconst auto firstLoad = !_offset;\n\t\t_loadRequestId = 0;\n\n\t\tauto wasRecentRequest = firstLoad\n\t\t\t&& (_role == Role::Members || _role == Role::Profile)\n\t\t\t&& channel->canViewMembers();\n\n\t\tresult.match([&](const MTPDchannels_channelParticipants &data) {\n\t\t\tconst auto &[availableCount, list] = wasRecentRequest\n\t\t\t\t? Api::ChatParticipants::ParseRecent(channel, data)\n\t\t\t\t: Api::ChatParticipants::Parse(channel, data);\n\t\t\tfor (const auto &data : list) {\n\t\t\t\tif (const auto participant = _additional.applyParticipant(\n\t\t\t\t\t\tdata)) {\n\t\t\t\t\tif (appendRow(participant)) {\n\t\t\t\t\t\tadded = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (const auto size = list.size()) {\n\t\t\t\t_offset += size;\n\t\t\t} else {\n\t\t\t\t// To be sure - wait for a whole empty result list.\n\t\t\t\t_allLoaded = true;\n\t\t\t}\n\t\t}, [](const MTPDchannels_channelParticipantsNotModified &) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"channels.channelParticipantsNotModified received!\"));\n\t\t});\n\t\tif (_offset > 0 && _role == Role::Admins && channel->isMegagroup()) {\n\t\t\tif (channel->mgInfo->admins.empty() && channel->mgInfo->adminsLoaded) {\n\t\t\t\tchannel->mgInfo->adminsLoaded = false;\n\t\t\t}\n\t\t}\n\t\tif (!firstLoad && !added) {\n\t\t\t_allLoaded = true;\n\t\t}\n\t\tif (_allLoaded\n\t\t\t|| (firstLoad && delegate()->peerListFullRowsCount() > 0)) {\n\t\t\trefreshDescription();\n\t\t}\n\t\tif (_onlineSorter) {\n\t\t\t_onlineSorter->sort();\n\t\t}\n\t\trefreshRows();\n\t}).fail([this] {\n\t\t_loadRequestId = 0;\n\t}).send();\n}\n\nvoid ParticipantsBoxController::refreshDescription() {\n\tsetDescriptionText((_role == Role::Kicked)\n\t\t? ((_peer->isChat() || _peer->isMegagroup())\n\t\t\t? tr::lng_group_removed_list_about\n\t\t\t: tr::lng_channel_removed_list_about)(tr::now)\n\t\t: (delegate()->peerListFullRowsCount() > 0)\n\t\t? QString()\n\t\t: tr::lng_blocked_list_not_found(tr::now));\n}\n\nbool ParticipantsBoxController::feedMegagroupLastParticipants() {\n\tif ((_role != Role::Members && _role != Role::Profile)\n\t\t|| delegate()->peerListFullRowsCount() > 0) {\n\t\treturn false;\n\t}\n\tconst auto megagroup = _peer->asMegagroup();\n\tif (!megagroup || !megagroup->canViewMembers()) {\n\t\treturn false;\n\t}\n\tconst auto info = megagroup->mgInfo.get();\n\t//\n\t// channelFull and channels_channelParticipants members count desynced\n\t// so we almost always have LastParticipantsCountOutdated that is set\n\t// inside setMembersCount() and so we almost never use lastParticipants.\n\t//\n\t// => disable this check temporarily.\n\t//\n\t//if (info->lastParticipantsStatus\n\t//\t!= MegagroupInfo::LastParticipantsUpToDate) {\n\t//\t_channel->updateFull();\n\t//\treturn false;\n\t//}\n\tif (info->lastParticipants.empty()) {\n\t\treturn false;\n\t}\n\n\tauto added = false;\n\t_additional.fillFromPeer();\n\tfor (const auto &user : info->lastParticipants) {\n\t\tif (appendRow(user)) {\n\t\t\tadded = true;\n\t\t}\n\n\t\t//\n\t\t// Don't count lastParticipants in _offset, because we don't know\n\t\t// their exact information (admin / creator / restricted), they\n\t\t// could simply be added from the last messages authors.\n\t\t//\n\t\t//++_offset;\n\t}\n\tif (_onlineSorter) {\n\t\t_onlineSorter->sort();\n\t}\n\treturn added;\n}\n\nvoid ParticipantsBoxController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto participant = row->peer();\n\tconst auto user = participant->asUser();\n\n\tif (_stories && _stories->handleClick(participant)) {\n\t\treturn;\n\t}\n\n\tif (_role == Role::Admins) {\n\t\tAssert(user != nullptr);\n\t\tshowAdmin(user);\n\t} else if (_role == Role::Restricted\n\t\t&& (_peer->isChat() || _peer->isMegagroup())\n\t\t&& user) {\n\t\tshowRestricted(user);\n\t} else if (_role == Role::Members\n\t\t&& user\n\t\t&& (_additional.adminRights(user).has_value()\n\t\t\t|| _additional.isCreator(user))\n\t\t&& _additional.canAddOrEditAdmin(user)) {\n\t\tshowAdmin(user);\n\t} else if (_role == Role::Members\n\t\t&& user\n\t\t&& _additional.canRestrictParticipant(participant)) {\n\t\tshowRestricted(user);\n\t} else {\n\t\tAssert(_navigation != nullptr);\n\t\tif (_role != Role::Profile) {\n\t\t\t_navigation->parentController()->show(PrepareShortInfoBox(\n\t\t\t\tparticipant,\n\t\t\t\t_navigation));\n\t\t} else {\n\t\t\t_navigation->showPeerInfo(participant);\n\t\t}\n\t}\n}\n\nvoid ParticipantsBoxController::rowRightActionClicked(\n\t\tnot_null<PeerListRow*> row) {\n\trowElementClicked(row, Row::kRemoveElement);\n}\n\nvoid ParticipantsBoxController::rowElementClicked(\n\t\tnot_null<PeerListRow*> row,\n\t\tint element) {\n\tconst auto participant = row->peer();\n\tconst auto user = participant->asUser();\n\tconst auto memberRow = static_cast<Row*>(row.get());\n\tif (element == Row::kTagElement) {\n\t\tif (!user) {\n\t\t\treturn;\n\t\t}\n\t\tif (memberRow->type().canAddTag || memberRow->type().canEditTag) {\n\t\t\tconst auto show = delegate()->peerListUiShow();\n\t\t\tconst auto peer = _peer;\n\t\t\tconst auto currentRank = _additional.memberRank(user);\n\t\t\tconst auto isSelf = user->isSelf();\n\t\t\tshow->show(Box(\n\t\t\t\tEditCustomRankBox,\n\t\t\t\tshow,\n\t\t\t\tpeer,\n\t\t\t\tuser,\n\t\t\t\tcurrentRank,\n\t\t\t\tisSelf,\n\t\t\t\tcrl::guard(this, [=](const QString &rank) {\n\t\t\t\t\t_additional.applyMemberRankLocally(user, rank);\n\t\t\t\t\trecomputeTypeFor(user);\n\t\t\t\t\trefreshRows();\n\t\t\t\t})));\n\t\t}\n\t} else if (element == Row::kRemoveElement) {\n\t\tif (_role == Role::Members || _role == Role::Profile) {\n\t\t\tkickParticipant(participant);\n\t\t} else if (_role == Role::Admins) {\n\t\t\tAssert(user != nullptr);\n\t\t\tremoveAdmin(user);\n\t\t} else {\n\t\t\tremoveKicked(row, participant);\n\t\t}\n\t}\n}\n\nbase::unique_qptr<Ui::PopupMenu> ParticipantsBoxController::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tconst auto channel = _peer->asChannel();\n\tconst auto participant = row->peer();\n\tconst auto user = participant->asUser();\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\tconst auto addToEnd = gsl::finally([&] {\n\t\tconst auto addInfoAction = [&](\n\t\t\t\tnot_null<PeerData*> by,\n\t\t\t\ttr::phrase<lngtag_user, lngtag_date> phrase,\n\t\t\t\tTimeId since) {\n\t\t\tauto text = phrase(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\ttr::bold(by->name()),\n\t\t\t\tlt_date,\n\t\t\t\ttr::bold(\n\t\t\t\t\tlangDateTimeFull(base::unixtime::parse(since))),\n\t\t\t\ttr::marked);\n\t\t\tauto button = base::make_unique_q<Ui::Menu::MultilineAction>(\n\t\t\t\tresult->menu(),\n\t\t\t\tresult->st().menu,\n\t\t\t\tst::historyHasCustomEmoji,\n\t\t\t\tst::historyHasCustomEmojiPosition,\n\t\t\t\tstd::move(text));\n\t\t\tif (const auto n = _navigation) {\n\t\t\t\tbutton->setActionTriggered([=] {\n\t\t\t\t\tn->parentController()->show(PrepareShortInfoBox(by, n));\n\t\t\t\t});\n\t\t\t}\n\t\t\tresult->addSeparator();\n\t\t\tresult->addAction(std::move(button));\n\t\t};\n\n\t\tif (const auto by = _additional.restrictedBy(participant)) {\n\t\t\tif (const auto since = _additional.restrictedSince(participant)) {\n\t\t\t\taddInfoAction(\n\t\t\t\t\tby,\n\t\t\t\t\t_additional.isKicked(participant)\n\t\t\t\t\t\t? tr::lng_rights_chat_banned_by\n\t\t\t\t\t\t: tr::lng_rights_chat_restricted_by,\n\t\t\t\t\tsince);\n\t\t\t}\n\t\t} else if (user) {\n\t\t\tif (const auto by = _additional.adminPromotedBy(user)) {\n\t\t\t\tif (const auto since = _additional.adminPromotedSince(user)) {\n\t\t\t\t\taddInfoAction(by, tr::lng_rights_about_by, since);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\tif (_navigation) {\n\t\tresult->addAction(\n\t\t\t(participant->isUser()\n\t\t\t\t? tr::lng_context_view_profile\n\t\t\t\t: participant->isBroadcast()\n\t\t\t\t? tr::lng_context_view_channel\n\t\t\t\t: tr::lng_context_view_group)(tr::now),\n\t\t\tcrl::guard(this, [=, this] {\n\t\t\t\t_navigation->parentController()->show(\n\t\t\t\t\tPrepareShortInfoBox(participant, _navigation));\n\t\t\t}),\n\t\t\t(participant->isUser()\n\t\t\t\t? &st::menuIconProfile\n\t\t\t\t: &st::menuIconInfo));\n\t}\n\tif (user) {\n\t\tconst auto isSelf = user->isSelf();\n\t\tconst auto canEditSelf = isSelf\n\t\t\t&& !_peer->amRestricted(ChatRestriction::EditRank);\n\t\tconst auto targetIsAdmin = _additional.adminRights(user).has_value()\n\t\t\t|| _additional.isCreator(user);\n\t\tconst auto canEditTarget = !isSelf\n\t\t\t&& _peer->canManageRanks()\n\t\t\t&& (!targetIsAdmin || _additional.canEditAdmin(user));\n\t\tif (canEditSelf || canEditTarget) {\n\t\t\tconst auto currentRank = _additional.memberRank(user);\n\t\t\tconst auto show = delegate()->peerListUiShow();\n\t\t\tconst auto peer = _peer;\n\t\t\tconst auto actionText = canEditSelf\n\t\t\t\t? (currentRank.isEmpty()\n\t\t\t\t\t? tr::lng_context_add_my_tag(tr::now)\n\t\t\t\t\t: tr::lng_context_edit_my_tag(tr::now))\n\t\t\t\t: (currentRank.isEmpty()\n\t\t\t\t\t? tr::lng_context_add_member_tag(tr::now)\n\t\t\t\t\t: tr::lng_context_edit_member_tag(tr::now));\n\t\t\tconst auto weak = base::make_weak(this);\n\t\t\tresult->addAction(\n\t\t\t\tactionText,\n\t\t\t\t[=] {\n\t\t\t\t\tshow->show(Box(\n\t\t\t\t\t\tEditCustomRankBox,\n\t\t\t\t\t\tshow,\n\t\t\t\t\t\tpeer,\n\t\t\t\t\t\tuser,\n\t\t\t\t\t\tcurrentRank,\n\t\t\t\t\t\tcanEditSelf,\n\t\t\t\t\t\tcrl::guard(weak, [=](const QString &rank) {\n\t\t\t\t\t\t\t_additional.applyMemberRankLocally(user, rank);\n\t\t\t\t\t\t\trecomputeTypeFor(user);\n\t\t\t\t\t\t\trefreshRows();\n\t\t\t\t\t\t})));\n\t\t\t\t},\n\t\t\t\t(currentRank.isEmpty()\n\t\t\t\t\t? &st::menuIconTagAdd\n\t\t\t\t\t: &st::menuIconTagEdit));\n\t\t}\n\t}\n\tif (_role == Role::Kicked) {\n\t\tif (_peer->isMegagroup()\n\t\t\t&& _additional.canRestrictParticipant(participant)) {\n\t\t\tif (user && channel->canAddMembers()) {\n\t\t\t\tresult->addAction(\n\t\t\t\t\ttr::lng_context_add_to_group(tr::now),\n\t\t\t\t\tcrl::guard(this, [=] { unkickParticipant(user); }),\n\t\t\t\t\t&st::menuIconInvite);\n\t\t\t}\n\t\t\tresult->addAction(\n\t\t\t\ttr::lng_profile_delete_removed(tr::now),\n\t\t\t\tcrl::guard(this, [=] { removeKickedWithRow(participant); }),\n\t\t\t\t&st::menuIconDelete);\n\t\t}\n\t\treturn result;\n\t}\n\tif (user && _additional.canAddOrEditAdmin(user)) {\n\t\tconst auto isAdmin = _additional.isCreator(user)\n\t\t\t|| _additional.adminRights(user).has_value();\n\t\tresult->addAction(\n\t\t\t(isAdmin\n\t\t\t\t? tr::lng_context_edit_permissions\n\t\t\t\t: tr::lng_context_promote_admin)(tr::now),\n\t\t\tcrl::guard(this, [=] { showAdmin(user); }),\n\t\t\t(isAdmin\n\t\t\t\t? &st::menuIconAdmin\n\t\t\t\t: &st::menuIconPromote));\n\t}\n\tif (user && _additional.canRestrictParticipant(participant)) {\n\t\tconst auto canRestrictWithoutKick = [&] {\n\t\t\tif (const auto chat = _peer->asChat()) {\n\t\t\t\treturn chat->amCreator();\n\t\t\t}\n\t\t\treturn _peer->isMegagroup() && !_peer->isGigagroup();\n\t\t}();\n\t\tif (canRestrictWithoutKick) {\n\t\t\tresult->addAction(\n\t\t\t\ttr::lng_context_restrict_user(tr::now),\n\t\t\t\tcrl::guard(this, [=] { showRestricted(user); }),\n\t\t\t\t&st::menuIconPermissions);\n\t\t}\n\t}\n\tif (user && _additional.canRemoveParticipant(participant)) {\n\t\tif (!_additional.isKicked(participant)) {\n\t\t\tconst auto isGroup = _peer->isChat() || _peer->isMegagroup();\n\t\t\tresult->addAction(\n\t\t\t\t(isGroup\n\t\t\t\t\t? tr::lng_context_remove_from_group\n\t\t\t\t\t: tr::lng_profile_kick)(tr::now),\n\t\t\t\tcrl::guard(this, [=] { kickParticipant(user); }),\n\t\t\t\t&st::menuIconRemove);\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid ParticipantsBoxController::showAdmin(not_null<UserData*> user) {\n\tconst auto adminRights = _additional.adminRights(user);\n\tconst auto currentRights = adminRights.value_or(ChatAdminRightsInfo());\n\tauto box = Box<EditAdminBox>(\n\t\t_peer,\n\t\tuser,\n\t\tcurrentRights,\n\t\t_additional.memberRank(user),\n\t\t_additional.adminPromotedSince(user),\n\t\t_additional.adminPromotedBy(user));\n\tif (_additional.canAddOrEditAdmin(user)) {\n\t\tconst auto done = crl::guard(this, [=](\n\t\t\t\tChatAdminRightsInfo newRights,\n\t\t\t\tconst std::optional<QString> &rank) {\n\t\t\teditAdminDone(user, newRights, rank);\n\t\t});\n\t\tconst auto fail = crl::guard(this, [=] {\n\t\t\tif (_editParticipantBox) {\n\t\t\t\t_editParticipantBox->closeBox();\n\t\t\t}\n\t\t});\n\t\tconst auto show = delegate()->peerListUiShow();\n\t\tbox->setSaveCallback(\n\t\t\tSaveAdminCallback(show, _peer, user, done, fail));\n\t}\n\t_editParticipantBox = showBox(std::move(box));\n}\n\nvoid ParticipantsBoxController::editAdminDone(\n\t\tnot_null<UserData*> user,\n\t\tChatAdminRightsInfo rights,\n\t\tconst std::optional<QString> &rank) {\n\t_addBox = nullptr;\n\tif (_editParticipantBox) {\n\t\t_editParticipantBox->closeBox();\n\t}\n\n\tconst auto effectiveRank = rank.value_or(\n\t\t_additional.memberRank(user));\n\t_additional.applyAdminLocally(user, rights, effectiveRank);\n\trecomputeTypeFor(user);\n\trefreshRows();\n\n\tconst auto flags = rights.flags;\n\tuser->session().changes().chatAdminChanged(_peer, user, flags, effectiveRank);\n}\n\nvoid ParticipantsBoxController::showRestricted(not_null<UserData*> user) {\n\tconst auto restrictedRights = _additional.restrictedRights(user);\n\tconst auto currentRights = restrictedRights\n\t\t? *restrictedRights\n\t\t: ChatRestrictionsInfo();\n\tconst auto hasAdminRights = _additional.adminRights(user).has_value();\n\tauto box = Box<EditRestrictedBox>(\n\t\t_peer,\n\t\tuser,\n\t\thasAdminRights,\n\t\tcurrentRights,\n\t\t_additional.memberRank(user),\n\t\t_additional.restrictedBy(user),\n\t\t_additional.restrictedSince(user));\n\tif (_additional.canRestrictParticipant(user)) {\n\t\tconst auto done = crl::guard(this, [=](\n\t\t\t\tChatRestrictionsInfo newRights) {\n\t\t\teditRestrictedDone(user, newRights);\n\t\t});\n\t\tconst auto fail = crl::guard(this, [=] {\n\t\t\tif (_editParticipantBox) {\n\t\t\t\t_editParticipantBox->closeBox();\n\t\t\t}\n\t\t});\n\t\tconst auto show = delegate()->peerListUiShow();\n\t\tbox->setSaveCallback(\n\t\t\tSaveRestrictedCallback(show, _peer, user, done, fail));\n\t}\n\t_editParticipantBox = showBox(std::move(box));\n}\n\nvoid ParticipantsBoxController::editRestrictedDone(\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo rights) {\n\t_addBox = nullptr;\n\tif (_editParticipantBox) {\n\t\t_editParticipantBox->closeBox();\n\t}\n\n\t_additional.applyBannedLocally(participant, rights);\n\tif (!rights.flags) {\n\t\tif (_role == Role::Kicked || _role == Role::Restricted) {\n\t\t\tremoveRow(participant);\n\t\t}\n\t} else {\n\t\tconst auto kicked = rights.flags & ChatRestriction::ViewMessages;\n\t\tif (kicked) {\n\t\t\tif (_role == Role::Kicked) {\n\t\t\t\tprependRow(participant);\n\t\t\t} else if (_role == Role::Admins\n\t\t\t\t|| _role == Role::Restricted\n\t\t\t\t|| _role == Role::Members) {\n\t\t\t\tremoveRow(participant);\n\t\t\t}\n\t\t} else {\n\t\t\tif (_role == Role::Restricted) {\n\t\t\t\tprependRow(participant);\n\t\t\t} else if (_role == Role::Kicked\n\t\t\t\t|| _role == Role::Admins) {\n\t\t\t\tremoveRow(participant);\n\t\t\t}\n\t\t}\n\t}\n\trecomputeTypeFor(participant);\n\trefreshRows();\n}\n\nvoid ParticipantsBoxController::kickParticipant(not_null<PeerData*> participant) {\n\tconst auto user = participant->asUser();\n\tconst auto kickFrom = _peer;\n\tconst auto restrictedRights = _additional.restrictedRights(participant);\n\tconst auto removeLocal = crl::guard(this, [=] {\n\t\tconst auto id = participant->id;\n\t\tif (const auto row = delegate()->peerListFindRow(id.value)) {\n\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t\trefreshRows();\n\t\t}\n\t});\n\tconst auto kick = [=] {\n\t\tconst auto currentRights = restrictedRights\n\t\t\t? *restrictedRights\n\t\t\t: ChatRestrictionsInfo();\n\t\tremoveLocal();\n\t\tauto &session = kickFrom->session();\n\t\tif (const auto chat = kickFrom->asChat()) {\n\t\t\tsession.api().chatParticipants().kick(chat, participant);\n\t\t} else if (const auto channel = kickFrom->asChannel()) {\n\t\t\tsession.api().chatParticipants().kick(\n\t\t\t\tchannel,\n\t\t\t\tparticipant,\n\t\t\t\tcurrentRights);\n\t\t}\n\t};\n\n\tif (user && user->isInaccessible()) {\n\t\treturn kick();\n\t}\n\tconst auto text = ((_peer->isChat() || _peer->isMegagroup())\n\t\t? tr::lng_profile_sure_kick\n\t\t: tr::lng_profile_sure_kick_channel)(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\tuser ? user->firstName : participant->name());\n\tshowBox(\n\t\tUi::MakeConfirmBox({\n\t\t\t.text = text,\n\t\t\t.confirmed = [=](Fn<void()> close) { kick(); close(); },\n\t\t\t.confirmText = tr::lng_box_remove(),\n\t\t}));\n}\n\nvoid ParticipantsBoxController::unkickParticipant(not_null<UserData*> user) {\n\t_editBox = nullptr;\n\tif (const auto row = delegate()->peerListFindRow(user->id.value)) {\n\t\tdelegate()->peerListRemoveRow(row);\n\t\trefreshRows();\n\t}\n\tconst auto show = delegate()->peerListUiShow();\n\t_peer->session().api().chatParticipants().add(show, _peer, { 1, user });\n}\n\nvoid ParticipantsBoxController::removeAdmin(not_null<UserData*> user) {\n\t_editBox = showBox(\n\t\tUi::MakeConfirmBox({\n\t\t\t.text = tr::lng_profile_sure_remove_admin(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tuser->firstName),\n\t\t\t.confirmed = crl::guard(this, [=] { removeAdminSure(user); }),\n\t\t\t.confirmText = tr::lng_box_remove(),\n\t\t}));\n}\n\nvoid ParticipantsBoxController::removeAdminSure(not_null<UserData*> user) {\n\t_editBox = nullptr;\n\n\tif (const auto chat = _peer->asChat()) {\n\t\tconst auto show = delegate()->peerListUiShow();\n\t\tSaveChatAdmin(show, chat, user, false, crl::guard(this, [=] {\n\t\t\teditAdminDone(user, {}, {});\n\t\t}), nullptr);\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\tconst auto adminRights = _additional.adminRights(user);\n\t\tif (!adminRights) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto show = delegate()->peerListUiShow();\n\t\tRemoveAdmin(show, channel, user, *adminRights, crl::guard(this, [=] {\n\t\t\teditAdminDone(user, {}, {});\n\t\t}), nullptr);\n\t}\n}\n\nvoid ParticipantsBoxController::removeKickedWithRow(\n\t\tnot_null<PeerData*> participant) {\n\tif (const auto row = delegate()->peerListFindRow(participant->id.value)) {\n\t\tremoveKicked(row, participant);\n\t} else {\n\t\tremoveKicked(participant);\n\t}\n}\nvoid ParticipantsBoxController::removeKicked(\n\t\tnot_null<PeerData*> participant) {\n\tif (const auto channel = _peer->asChannel()) {\n\t\tchannel->session().api().chatParticipants().unblock(\n\t\t\tchannel,\n\t\t\tparticipant);\n\t}\n}\n\nvoid ParticipantsBoxController::removeKicked(\n\t\tnot_null<PeerListRow*> row,\n\t\tnot_null<PeerData*> participant) {\n\tdelegate()->peerListRemoveRow(row);\n\tif (_role != Role::Kicked\n\t\t&& !delegate()->peerListFullRowsCount()) {\n\t\tsetDescriptionText(tr::lng_blocked_list_not_found(tr::now));\n\t}\n\trefreshRows();\n\tremoveKicked(participant);\n}\n\nbool ParticipantsBoxController::appendRow(not_null<PeerData*> participant) {\n\tif (delegate()->peerListFindRow(participant->id.value)) {\n\t\trecomputeTypeFor(participant);\n\t\treturn false;\n\t} else if (auto row = createRow(participant)) {\n\t\tconst auto raw = row.get();\n\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\tif (_stories) {\n\t\t\t_stories->process(raw);\n\t\t}\n\t\tif (_role != Role::Kicked) {\n\t\t\tsetDescriptionText(QString());\n\t\t}\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool ParticipantsBoxController::prependRow(not_null<PeerData*> participant) {\n\tif (const auto row = delegate()->peerListFindRow(participant->id.value)) {\n\t\trecomputeTypeFor(participant);\n\t\trefreshCustomStatus(row);\n\t\tif (_role == Role::Admins) {\n\t\t\t// Perhaps we've added a new admin from search.\n\t\t\tdelegate()->peerListPrependRowFromSearchResult(row);\n\t\t\tif (_stories) {\n\t\t\t\t_stories->process(row);\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t} else if (auto row = createRow(participant)) {\n\t\tconst auto raw = row.get();\n\t\tdelegate()->peerListPrependRow(std::move(row));\n\t\tif (_stories) {\n\t\t\t_stories->process(raw);\n\t\t}\n\t\tif (_role != Role::Kicked) {\n\t\t\tsetDescriptionText(QString());\n\t\t}\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool ParticipantsBoxController::removeRow(not_null<PeerData*> participant) {\n\tif (auto row = delegate()->peerListFindRow(participant->id.value)) {\n\t\tif (_role == Role::Admins) {\n\t\t\t// Perhaps we are removing an admin from search results.\n\t\t\trow->setCustomStatus(tr::lng_channel_admin_status_not_admin(tr::now));\n\t\t\tdelegate()->peerListConvertRowToSearchResult(row);\n\t\t} else {\n\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t}\n\t\tif (_role != Role::Kicked\n\t\t\t&& !delegate()->peerListFullRowsCount()) {\n\t\t\tsetDescriptionText(tr::lng_blocked_list_not_found(tr::now));\n\t\t}\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nstd::unique_ptr<PeerListRow> ParticipantsBoxController::createRow(\n\t\tnot_null<PeerData*> participant) const {\n\tif (_role == Role::Profile) {\n\t\tAssert(participant->asUser() != nullptr);\n\t}\n\tauto row = std::make_unique<Row>(participant, computeType(participant));\n\trefreshCustomStatus(row.get());\n\tif (const auto user = participant->asUser()) {\n\t\tif ((_role == Role::Members || _role == Role::Profile)\n\t\t\t\t&& user->isBot()) {\n\t\t\tconst auto seesAllMessages =\n\t\t\t\t(user->botInfo->readsAllHistory\n\t\t\t\t\t|| _additional.adminRights(user).has_value());\n\t\t\trow->setCustomStatus(seesAllMessages\n\t\t\t\t? tr::lng_status_bot_reads_all(tr::now)\n\t\t\t\t: tr::lng_status_bot_not_reads_all(tr::now));\n\t\t}\n\t}\n\tconst auto raw = row.get();\n\trow->setRefreshCallback(crl::guard(this, [=] {\n\t\tdelegate()->peerListUpdateRow(raw);\n\t}));\n\treturn row;\n}\n\nauto ParticipantsBoxController::computeType(\n\t\tnot_null<PeerData*> participant) const -> Type {\n\tconst auto user = participant->asUser();\n\tauto result = Type{\n\t\t.chatStyle = _chatStyle.get(),\n\t\t.circleCache = &_pillCircleCache,\n\t};\n\tresult.rights = (user && _additional.isCreator(user))\n\t\t? Rights::Creator\n\t\t: (user && _additional.adminRights(user).has_value())\n\t\t? Rights::Admin\n\t\t: Rights::Normal;\n\n\tif (user) {\n\t\tresult.rank = _additional.memberRank(user);\n\t}\n\n\tconst auto chat = _peer->asChat();\n\tconst auto channel = _peer->asChannel();\n\n\tswitch (_role) {\n\tcase Role::Profile: {\n\t\tif (user\n\t\t\t&& (chat\n\t\t\t\t? chat->canBanMembers()\n\t\t\t\t: (channel && channel->canBanMembers()))\n\t\t\t&& !_additional.isCreator(user)\n\t\t\t&& (!_additional.adminRights(user)\n\t\t\t\t|| _additional.canEditAdmin(user))) {\n\t\t\tresult.canRemove = true;\n\t\t\tresult.removeText = tr::lng_profile_kick(tr::now);\n\t\t} else if (_additional.canRemoveParticipant(participant)\n\t\t\t&& user\n\t\t\t&& user->isInaccessible()) {\n\t\t\tresult.canRemove = true;\n\t\t\tresult.rank = QString();\n\t\t\tresult.removeText = tr::lng_profile_delete_removed(tr::now);\n\t\t}\n\t} break;\n\tcase Role::Members: {\n\t\tif (user\n\t\t\t&& (chat\n\t\t\t\t? chat->canBanMembers()\n\t\t\t\t: channel->canBanMembers())\n\t\t\t&& !_additional.isCreator(user)\n\t\t\t&& (!_additional.adminRights(user)\n\t\t\t\t|| _additional.canEditAdmin(user))) {\n\t\t\tresult.canRemove = true;\n\t\t\tresult.removeText = tr::lng_profile_kick(tr::now);\n\t\t}\n\t} break;\n\tcase Role::Admins: {\n\t\tif (user\n\t\t\t&& !_additional.isCreator(user)\n\t\t\t&& _additional.adminRights(user).has_value()\n\t\t\t&& _additional.canEditAdmin(user)) {\n\t\t\tresult.canRemove = true;\n\t\t\tresult.removeText = tr::lng_profile_kick(tr::now);\n\t\t}\n\t} break;\n\tcase Role::Restricted:\n\tcase Role::Kicked: {\n\t\tif (_additional.canRestrictParticipant(participant)) {\n\t\t\tresult.canRemove = true;\n\t\t\tresult.removeText = tr::lng_profile_delete_removed(tr::now);\n\t\t}\n\t} break;\n\t}\n\n\tif (user && !_peer->isBroadcast()) {\n\t\tconst auto isSelf = user->isSelf();\n\t\tconst auto canEditSelf = isSelf\n\t\t\t&& !_peer->amRestricted(ChatRestriction::EditRank);\n\t\tconst auto targetIsAdmin =\n\t\t\t_additional.adminRights(user).has_value()\n\t\t\t|| _additional.isCreator(user);\n\t\tconst auto canEditTarget = !isSelf\n\t\t\t&& _peer->canManageRanks()\n\t\t\t&& (!targetIsAdmin || _additional.canEditAdmin(user));\n\t\tif (canEditSelf || canEditTarget) {\n\t\t\tresult.canEditTag = true;\n\t\t}\n\t\tif (isSelf\n\t\t\t&& result.rank.isEmpty()\n\t\t\t&& !result.canRemove\n\t\t\t&& result.rights == Rights::Normal\n\t\t\t&& canEditSelf) {\n\t\t\tresult.canAddTag = true;\n\t\t}\n\t}\n\n\treturn result;\n}\n\nvoid ParticipantsBoxController::recomputeTypeFor(\n\t\tnot_null<PeerData*> participant) {\n\tconst auto row = delegate()->peerListFindRow(participant->id.value);\n\tif (row) {\n\t\tstatic_cast<Row*>(row)->setType(computeType(participant));\n\t\tdelegate()->peerListUpdateRow(row);\n\t}\n}\n\nvoid ParticipantsBoxController::refreshCustomStatus(\n\t\tnot_null<PeerListRow*> row) const {\n\tconst auto participant = row->peer();\n\tconst auto user = participant->asUser();\n\tif (_role == Role::Admins) {\n\t\tAssert(user != nullptr);\n\t\tif (const auto by = _additional.adminPromotedBy(user)) {\n\t\t\trow->setCustomStatus(tr::lng_channel_admin_status_promoted_by(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tby->name()));\n\t\t} else {\n\t\t\tif (_additional.isCreator(user)) {\n\t\t\t\trow->setCustomStatus(\n\t\t\t\t\ttr::lng_channel_admin_status_creator(tr::now));\n\t\t\t} else {\n\t\t\t\trow->setCustomStatus(\n\t\t\t\t\ttr::lng_channel_admin_status_not_admin(tr::now));\n\t\t\t}\n\t\t}\n\t} else if (_role == Role::Kicked || _role == Role::Restricted) {\n\t\tconst auto by = _additional.restrictedBy(participant);\n\t\trow->setCustomStatus((_role == Role::Kicked\n\t\t\t? tr::lng_channel_banned_status_removed_by\n\t\t\t: tr::lng_channel_banned_status_restricted_by)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tby ? by->name() : \"Unknown\"));\n\t}\n}\n\nvoid ParticipantsBoxController::subscribeToMigration() {\n\tconst auto chat = _peer->asChat();\n\tif (!chat) {\n\t\treturn;\n\t}\n\tSubscribeToMigration(\n\t\tchat,\n\t\tlifetime(),\n\t\t[=](not_null<ChannelData*> channel) { migrate(chat, channel); });\n}\n\nvoid ParticipantsBoxController::migrate(\n\t\tnot_null<ChatData*> chat,\n\t\tnot_null<ChannelData*> channel) {\n\t_peer = channel;\n\t_additional.migrate(chat, channel);\n\tsubscribeToCreatorChange(channel);\n}\n\nvoid ParticipantsBoxController::subscribeToCreatorChange(\n\t\tnot_null<ChannelData*> channel) {\n\tconst auto isCreator = channel->amCreator();\n\tchannel->flagsValue(\n\t) | rpl::filter([](const ChannelData::Flags::Change &change) {\n\t\treturn (change.diff & ChannelDataFlag::Creator);\n\t}) | rpl::filter([=] {\n\t\treturn (isCreator != channel->amCreator());\n\t}) | rpl::on_next([=] {\n\t\tif (channel->isBroadcast()) {\n\t\t\tfullListRefresh();\n\t\t\treturn;\n\t\t}\n\t\tconst auto weak = base::make_weak(this);\n\t\tconst auto api = &channel->session().api();\n\t\tapi->request(MTPchannels_GetParticipants(\n\t\t\tchannel->inputChannel(),\n\t\t\tMTP_channelParticipantsRecent(),\n\t\t\tMTP_int(0), // offset\n\t\t\tMTP_int(channel->session().serverConfig().chatSizeMax),\n\t\t\tMTP_long(0) // hash\n\t\t)).done([=](const MTPchannels_ChannelParticipants &result) {\n\t\t\tif (channel->amCreator()) {\n\t\t\t\tchannel->mgInfo->creator = channel->session().user().get();\n\t\t\t}\n\t\t\tchannel->mgInfo->lastAdmins.clear();\n\t\t\tchannel->mgInfo->lastRestricted.clear();\n\t\t\tchannel->mgInfo->lastParticipants.clear();\n\n\t\t\tresult.match([&](const MTPDchannels_channelParticipants &data) {\n\t\t\t\tApi::ChatParticipants::ParseRecent(channel, data);\n\t\t\t}, [](const MTPDchannels_channelParticipantsNotModified &) {\n\t\t\t});\n\n\t\t\tif (weak) {\n\t\t\t\tfullListRefresh();\n\t\t\t}\n\t\t}).send();\n\t}, lifetime());\n}\n\nvoid ParticipantsBoxController::fullListRefresh() {\n\t_additional = ParticipantsAdditionalData(_peer, _role);\n\n\twhile (const auto count = delegate()->peerListFullRowsCount()) {\n\t\tdelegate()->peerListRemoveRow(\n\t\t\tdelegate()->peerListRowAt(count - 1));\n\t}\n\tloadMoreRows();\n\trefreshRows();\n}\n\nvoid ParticipantsBoxController::refreshRows() {\n\t_fullCountValue = delegate()->peerListFullRowsCount();\n\tdelegate()->peerListRefreshRows();\n}\n\nParticipantsBoxSearchController::ParticipantsBoxSearchController(\n\tnot_null<ChannelData*> channel,\n\tRole role,\n\tnot_null<ParticipantsAdditionalData*> additional)\n: _channel(channel)\n, _role(role)\n, _additional(additional)\n, _api(&_channel->session().mtp()) {\n\t_timer.setCallback([=] { searchOnServer(); });\n}\n\nvoid ParticipantsBoxSearchController::searchQuery(const QString &query) {\n\tif (_query != query) {\n\t\t_query = query;\n\t\t_offset = 0;\n\t\t_requestId = 0;\n\t\t_allLoaded = false;\n\t\tif (!_query.isEmpty() && !searchInCache()) {\n\t\t\t_timer.callOnce(AutoSearchTimeout);\n\t\t} else {\n\t\t\t_timer.cancel();\n\t\t}\n\t}\n}\n\nauto ParticipantsBoxSearchController::saveState() const\n-> std::unique_ptr<SavedStateBase> {\n\tauto result = std::make_unique<SavedState>();\n\tresult->query = _query;\n\tresult->offset = _offset;\n\tresult->allLoaded = _allLoaded;\n\tresult->wasLoading = (_requestId != 0);\n\treturn result;\n}\n\nvoid ParticipantsBoxSearchController::restoreState(\n\t\tstd::unique_ptr<SavedStateBase> state) {\n\tif (auto my = dynamic_cast<SavedState*>(state.get())) {\n\t\tif (auto requestId = base::take(_requestId)) {\n\t\t\t_api.request(requestId).cancel();\n\t\t}\n\t\t_cache.clear();\n\t\t_queries.clear();\n\n\t\t_allLoaded = my->allLoaded;\n\t\t_offset = my->offset;\n\t\t_query = my->query;\n\t\t_timer.cancel();\n\t\t_requestId = 0;\n\t\tif (my->wasLoading) {\n\t\t\tsearchOnServer();\n\t\t}\n\t}\n}\n\nvoid ParticipantsBoxSearchController::searchOnServer() {\n\tExpects(!_query.isEmpty());\n\n\tloadMoreRows();\n}\n\nbool ParticipantsBoxSearchController::isLoading() {\n\treturn _timer.isActive() || _requestId;\n}\n\nbool ParticipantsBoxSearchController::searchInCache() {\n\tconst auto i = _cache.find(_query);\n\tif (i != _cache.cend()) {\n\t\t_requestId = 0;\n\t\tsearchDone(\n\t\t\t_requestId,\n\t\t\ti->second.result,\n\t\t\ti->second.requestedCount);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool ParticipantsBoxSearchController::loadMoreRows() {\n\tif (_query.isEmpty()) {\n\t\treturn false;\n\t}\n\tif (_allLoaded || isLoading()) {\n\t\treturn true;\n\t}\n\tauto filter = [&] {\n\t\tswitch (_role) {\n\t\tcase Role::Admins: // Search for members, appoint as admin on found.\n\t\tcase Role::Profile:\n\t\tcase Role::Members:\n\t\t\treturn MTP_channelParticipantsSearch(MTP_string(_query));\n\t\tcase Role::Restricted:\n\t\t\treturn MTP_channelParticipantsBanned(MTP_string(_query));\n\t\tcase Role::Kicked:\n\t\t\treturn MTP_channelParticipantsKicked(MTP_string(_query));\n\t\t}\n\t\tUnexpected(\"Role in ParticipantsBoxSearchController.\");\n\t}();\n\n\t// For search we request a lot of rows from the first query.\n\t// (because we've waited for search request by timer already,\n\t// so we don't expect it to be fast, but we want to fill cache).\n\tconst auto perPage = kParticipantsPerPage;\n\tconst auto participantsHash = uint64(0);\n\n\t_requestId = _api.request(MTPchannels_GetParticipants(\n\t\t_channel->inputChannel(),\n\t\tfilter,\n\t\tMTP_int(_offset),\n\t\tMTP_int(perPage),\n\t\tMTP_long(participantsHash)\n\t)).done([=](\n\t\t\tconst MTPchannels_ChannelParticipants &result,\n\t\t\tmtpRequestId requestId) {\n\t\tsearchDone(requestId, result, perPage);\n\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\tif (_requestId == requestId) {\n\t\t\t_requestId = 0;\n\t\t\t_allLoaded = true;\n\t\t\tdelegate()->peerListSearchRefreshRows();\n\t\t}\n\t}).send();\n\n\tauto entry = Query();\n\tentry.text = _query;\n\tentry.offset = _offset;\n\t_queries.emplace(_requestId, entry);\n\treturn true;\n}\n\nvoid ParticipantsBoxSearchController::searchDone(\n\t\tmtpRequestId requestId,\n\t\tconst MTPchannels_ChannelParticipants &result,\n\t\tint requestedCount) {\n\tauto query = _query;\n\tif (requestId) {\n\t\tconst auto addToCache = [&](auto&&...) {\n\t\t\tauto it = _queries.find(requestId);\n\t\t\tif (it != _queries.cend()) {\n\t\t\t\tquery = it->second.text;\n\t\t\t\tif (it->second.offset == 0) {\n\t\t\t\t\tauto &entry = _cache[query];\n\t\t\t\t\tentry.result = result;\n\t\t\t\t\tentry.requestedCount = requestedCount;\n\t\t\t\t}\n\t\t\t\t_queries.erase(it);\n\t\t\t}\n\t\t};\n\t\tresult.match([&](const MTPDchannels_channelParticipants &data) {\n\t\t\tApi::ChatParticipants::Parse(_channel, data);\n\t\t\taddToCache();\n\t\t}, [&](const MTPDchannels_channelParticipantsNotModified &) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"channels.channelParticipantsNotModified received!\"));\n\t\t});\n\t}\n\tif (_requestId != requestId) {\n\t\treturn;\n\t}\n\n\t_requestId = 0;\n\tresult.match([&](const MTPDchannels_channelParticipants &data) {\n\t\tconst auto &list = data.vparticipants().v;\n\t\tif (list.size() < requestedCount) {\n\t\t\t// We want cache to have full information about a query with\n\t\t\t// small results count (that we don't need the second request).\n\t\t\t// So we don't wait for empty list unlike the non-search case.\n\t\t\t_allLoaded = true;\n\t\t}\n\t\tconst auto overrideRole = (_role == Role::Admins)\n\t\t\t? Role::Members\n\t\t\t: _role;\n\t\tfor (const auto &data : list) {\n\t\t\tconst auto user = _additional->applyParticipant(\n\t\t\t\tApi::ChatParticipant(data, _channel),\n\t\t\t\toverrideRole);\n\t\t\tif (user) {\n\t\t\t\tdelegate()->peerListSearchAddRow(user);\n\t\t\t}\n\t\t}\n\t\t_offset += list.size();\n\t}, [&](const MTPDchannels_channelParticipantsNotModified &) {\n\t\t_allLoaded = true;\n\t});\n\n\tdelegate()->peerListSearchRefreshRows();\n}\n\nvoid EditCustomRankBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user,\n\t\tconst QString &currentRank,\n\t\tbool isSelf,\n\t\tFn<void(QString rank)> onSaved) {\n\tstruct State {\n\t\tbool saving = false;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\n\tbox->setTitle(tr::lng_rights_edit_tag_title());\n\n\tconst auto role = LookupBadgeRole(peer, user);\n\tconst auto control = box->addRow(\n\t\tobject_ptr<EditTagControl>(\n\t\t\tbox,\n\t\t\t&peer->session(),\n\t\t\tuser,\n\t\t\tcurrentRank,\n\t\t\trole),\n\t\tstyle::margins());\n\tUi::AddSkip(box->verticalLayout());\n\tUi::AddDividerText(\n\t\tbox->verticalLayout(),\n\t\t(isSelf\n\t\t\t? tr::lng_rights_tag_about_self()\n\t\t\t: tr::lng_rights_tag_about(\n\t\t\t\tlt_name,\n\t\t\t\trpl::single(user->shortName()))));\n\tconst auto field = control->field();\n\n\tbox->setFocusCallback([=] { field->setFocusFast(); });\n\n\tconst auto close = crl::guard(box, [=] { box->closeBox(); });\n\tconst auto save = [=] {\n\t\tif (state->saving) {\n\t\t\treturn;\n\t\t}\n\t\tstate->saving = true;\n\t\tconst auto rank = control->currentRank();\n\t\tSaveMemberRank(\n\t\t\tshow,\n\t\t\tpeer,\n\t\t\tuser,\n\t\t\trank,\n\t\t\t[=] {\n\t\t\t\tif (onSaved) {\n\t\t\t\t\tonSaved(rank);\n\t\t\t\t}\n\t\t\t\tclose();\n\t\t\t},\n\t\t\t[=] { state->saving = false; });\n\t};\n\tfield->submits(\n\t) | rpl::on_next([=] { save(); }, field->lifetime());\n\tbox->addButton(tr::lng_settings_save(), save);\n\tbox->addButton(tr::lng_cancel(), close);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_participants_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <rpl/variable.h>\n#include \"mtproto/sender.h\"\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n#include \"info/profile/info_profile_members_controllers.h\"\n\nclass PeerListStories;\nstruct ChatAdminRightsInfo;\nstruct ChatRestrictionsInfo;\n\nnamespace Ui {\nclass Show;\n} // namespace Ui\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nnamespace Api {\nclass ChatParticipant;\n} // namespace Api\n\nFn<void(\n\tChatAdminRightsInfo oldRights,\n\tChatAdminRightsInfo newRights,\n\tconst std::optional<QString> &rank)> SaveAdminCallback(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user,\n\t\tFn<void(\n\t\t\tChatAdminRightsInfo newRights,\n\t\t\tconst std::optional<QString> &rank)> onDone,\n\t\tFn<void()> onFail);\n\nFn<void(\n\tChatRestrictionsInfo oldRights,\n\tChatRestrictionsInfo newRights)> SaveRestrictedCallback(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<PeerData*> participant,\n\t\tFn<void(ChatRestrictionsInfo newRights)> onDone,\n\t\tFn<void()> onFail);\n\nvoid EditCustomRankBox(\n\tnot_null<Ui::GenericBox*> box,\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> user,\n\tconst QString &currentRank,\n\tbool isSelf,\n\tFn<void(QString rank)> onSaved);\n\nvoid SaveMemberRank(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> user,\n\tconst QString &rank,\n\tFn<void()> onDone,\n\tFn<void()> onFail);\n\nvoid SubscribeToMigration(\n\tnot_null<PeerData*> peer,\n\trpl::lifetime &lifetime,\n\tFn<void(not_null<ChannelData*>)> migrate);\n\nenum class ParticipantsRole {\n\tProfile,\n\tMembers,\n\tAdmins,\n\tRestricted,\n\tKicked,\n};\n\nclass ParticipantsOnlineSorter {\npublic:\n\tParticipantsOnlineSorter(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<PeerListDelegate*> delegate);\n\n\tvoid sort();\n\trpl::producer<int> onlineCountValue() const;\n\nprivate:\n\tvoid sortDelayed();\n\tvoid refreshOnlineCount();\n\n\tconst not_null<PeerData*> _peer;\n\tconst not_null<PeerListDelegate*> _delegate;\n\tbase::Timer _sortByOnlineTimer;\n\trpl::variable<int> _onlineCount = 0;\n\trpl::lifetime _lifetime;\n\n};\n\nclass ParticipantsAdditionalData {\npublic:\n\tusing Role = ParticipantsRole;\n\n\tParticipantsAdditionalData(not_null<PeerData*> peer, Role role);\n\n\tPeerData *applyParticipant(const Api::ChatParticipant &data);\n\tPeerData *applyParticipant(\n\t\tconst Api::ChatParticipant &data,\n\t\tRole overrideRole);\n\tvoid setExternal(not_null<PeerData*> participant);\n\tvoid checkForLoaded(not_null<PeerData*> participant);\n\tvoid fillFromPeer();\n\n\t[[nodiscard]] bool infoLoaded(not_null<PeerData*> participant) const;\n\t[[nodiscard]] bool canEditAdmin(not_null<UserData*> user) const;\n\t[[nodiscard]] bool canAddOrEditAdmin(not_null<UserData*> user) const;\n\t[[nodiscard]] bool canRestrictParticipant(\n\t\tnot_null<PeerData*> participant) const;\n\t[[nodiscard]] bool canRemoveParticipant(\n\t\tnot_null<PeerData*> participant) const;\n\t[[nodiscard]] std::optional<ChatAdminRightsInfo> adminRights(\n\t\tnot_null<UserData*> user) const;\n\t[[nodiscard]] QString memberRank(not_null<UserData*> user) const;\n\t[[nodiscard]] std::optional<ChatRestrictionsInfo> restrictedRights(\n\t\tnot_null<PeerData*> participant) const;\n\t[[nodiscard]] bool isCreator(not_null<UserData*> user) const;\n\t[[nodiscard]] bool isExternal(not_null<PeerData*> participant) const;\n\t[[nodiscard]] bool isKicked(not_null<PeerData*> participant) const;\n\t[[nodiscard]] UserData *adminPromotedBy(not_null<UserData*> user) const;\n\t[[nodiscard]] UserData *restrictedBy(\n\t\tnot_null<PeerData*> participant) const;\n\n\t[[nodiscard]] TimeId adminPromotedSince(not_null<UserData*>) const;\n\t[[nodiscard]] TimeId restrictedSince(not_null<PeerData*>) const;\n\t[[nodiscard]] TimeId memberSince(not_null<UserData*>) const;\n\n\tvoid migrate(not_null<ChatData*> chat, not_null<ChannelData*> channel);\n\n\tvoid applyAdminLocally(\n\t\tUserData *user,\n\t\tChatAdminRightsInfo rights,\n\t\tconst QString &rank);\n\tvoid applyBannedLocally(\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo rights);\n\tvoid applyMemberRankLocally(\n\t\tnot_null<UserData*> user,\n\t\tconst QString &rank);\n\nprivate:\n\tUserData *applyCreator(const Api::ChatParticipant &data);\n\tUserData *applyAdmin(const Api::ChatParticipant &data);\n\tUserData *applyRegular(UserId userId);\n\tPeerData *applyBanned(const Api::ChatParticipant &data);\n\tvoid fillFromChat(not_null<ChatData*> chat);\n\tvoid fillFromChannel(not_null<ChannelData*> channel);\n\n\tnot_null<PeerData*> _peer;\n\tRole _role = Role::Members;\n\tUserData *_creator = nullptr;\n\n\t// Data for chats.\n\tbase::flat_set<not_null<UserData*>> _members;\n\tbase::flat_set<not_null<UserData*>> _admins;\n\n\t// Data for channels.\n\tbase::flat_map<not_null<UserData*>, ChatAdminRightsInfo> _adminRights;\n\tbase::flat_map<not_null<UserData*>, QString> _memberRanks;\n\tbase::flat_map<not_null<UserData*>, TimeId> _adminPromotedSince;\n\tbase::flat_map<not_null<PeerData*>, TimeId> _restrictedSince;\n\tbase::flat_map<not_null<UserData*>, TimeId> _memberSince;\n\tbase::flat_set<not_null<UserData*>> _adminCanEdit;\n\tbase::flat_map<not_null<UserData*>, not_null<UserData*>> _adminPromotedBy;\n\tstd::map<not_null<PeerData*>, ChatRestrictionsInfo> _restrictedRights;\n\tstd::set<not_null<PeerData*>> _kicked;\n\tstd::map<not_null<PeerData*>, not_null<UserData*>> _restrictedBy;\n\tstd::set<not_null<PeerData*>> _external;\n\tstd::set<not_null<PeerData*>> _infoNotLoaded;\n\n};\n\n// Viewing admins, banned or restricted users list with search.\nclass ParticipantsBoxController\n\t: public PeerListController\n\t, public base::has_weak_ptr {\npublic:\n\tusing Role = ParticipantsRole;\n\n\tstatic void Start(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tRole role);\n\n\tParticipantsBoxController(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tRole role);\n\t~ParticipantsBoxController();\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override;\n\tvoid rowElementClicked(\n\t\tnot_null<PeerListRow*> row,\n\t\tint element) override;\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\tbool trackSelectedList() override {\n\t\treturn !_stories;\n\t}\n\n\tvoid peerListSearchAddRow(not_null<PeerData*> peer) override;\n\tstd::unique_ptr<PeerListRow> createSearchRow(\n\t\tnot_null<PeerData*> peer) override;\n\tstd::unique_ptr<PeerListRow> createRestoredRow(\n\t\tnot_null<PeerData*> peer) override;\n\n\tstd::unique_ptr<PeerListState> saveState() const override;\n\tvoid restoreState(std::unique_ptr<PeerListState> state) override;\n\n\t[[nodiscard]] rpl::producer<int> onlineCountValue() const;\n\t[[nodiscard]] rpl::producer<int> fullCountValue() const;\n\n\tvoid setStoriesShown(bool shown);\n\nprotected:\n\tusing Row = Info::Profile::MemberListRow;\n\tusing Type = Row::Type;\n\tusing Rights = Row::Rights;\n\n\tstruct CreateTag {\n\t};\n\tParticipantsBoxController(\n\t\tCreateTag,\n\t\tWindow::SessionNavigation *navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tRole role);\n\n\tvirtual std::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<PeerData*> participant) const;\n\n\tstd::unique_ptr<Ui::ChatStyle> _chatStyle;\n\tmutable base::flat_map<QRgb, QImage> _pillCircleCache;\n\nprivate:\n\tstruct SavedState : SavedStateBase {\n\t\texplicit SavedState(const ParticipantsAdditionalData &additional);\n\n\t\tusing SearchStateBase = PeerListSearchController::SavedStateBase;\n\t\tstd::unique_ptr<SearchStateBase> searchState;\n\t\tint offset = 0;\n\t\tbool allLoaded = false;\n\t\tbool wasLoading = false;\n\t\tParticipantsAdditionalData additional;\n\t\trpl::lifetime lifetime;\n\t};\n\n\tstatic std::unique_ptr<PeerListSearchController> CreateSearchController(\n\t\tnot_null<PeerData*> peer,\n\t\tRole role,\n\t\tnot_null<ParticipantsAdditionalData*> additional);\n\n\tbase::weak_qptr<Ui::BoxContent> showBox(object_ptr<Ui::BoxContent> box) const;\n\n\tvoid prepareChatRows(not_null<ChatData*> chat);\n\tvoid rebuildChatRows(not_null<ChatData*> chat);\n\tvoid rebuildChatParticipants(not_null<ChatData*> chat);\n\tvoid rebuildChatAdmins(not_null<ChatData*> chat);\n\tvoid chatListReady();\n\tvoid rebuildRowTypes();\n\tvoid rebuild();\n\tvoid unload();\n\n\tvoid addNewItem();\n\tvoid addNewParticipants();\n\n\tvoid refreshDescription();\n\tvoid setupListChangeViewers();\n\tvoid showAdmin(not_null<UserData*> user);\n\tvoid editAdminDone(\n\t\tnot_null<UserData*> user,\n\t\tChatAdminRightsInfo rights,\n\t\tconst std::optional<QString> &rank);\n\tvoid showRestricted(not_null<UserData*> user);\n\tvoid editRestrictedDone(\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo rights);\n\tvoid removeKicked(\n\t\tnot_null<PeerListRow*> row,\n\t\tnot_null<PeerData*> participant);\n\tvoid removeKickedWithRow(not_null<PeerData*> participant);\n\tvoid removeKicked(not_null<PeerData*> participant);\n\tvoid kickParticipant(not_null<PeerData*> participant);\n\tvoid unkickParticipant(not_null<UserData*> user);\n\tvoid removeAdmin(not_null<UserData*> user);\n\tvoid removeAdminSure(not_null<UserData*> user);\n\tbool appendRow(not_null<PeerData*> participant);\n\tbool prependRow(not_null<PeerData*> participant);\n\tbool removeRow(not_null<PeerData*> participant);\n\tvoid refreshCustomStatus(not_null<PeerListRow*> row) const;\n\tbool feedMegagroupLastParticipants();\n\tType computeType(not_null<PeerData*> participant) const;\n\tvoid recomputeTypeFor(not_null<PeerData*> participant);\n\n\tvoid subscribeToMigration();\n\tvoid migrate(not_null<ChatData*> chat, not_null<ChannelData*> channel);\n\tvoid subscribeToCreatorChange(not_null<ChannelData*> channel);\n\tvoid fullListRefresh();\n\tvoid refreshRows();\n\n\t// It may be nullptr in subclasses of this controller.\n\tWindow::SessionNavigation *_navigation = nullptr;\n\n\tnot_null<PeerData*> _peer;\n\tMTP::Sender _api;\n\tRole _role = Role::Admins;\n\tint _offset = 0;\n\tmtpRequestId _loadRequestId = 0;\n\tbool _allLoaded = false;\n\tParticipantsAdditionalData _additional;\n\tstd::unique_ptr<ParticipantsOnlineSorter> _onlineSorter;\n\trpl::variable<int> _onlineCountValue;\n\trpl::variable<int> _fullCountValue;\n\tUi::BoxPointer _editBox;\n\tUi::BoxPointer _addBox;\n\tbase::weak_qptr<Ui::BoxContent> _editParticipantBox;\n\n\tstd::unique_ptr<PeerListStories> _stories;\n\n};\n\n// Members, banned and restricted users server side search.\nclass ParticipantsBoxSearchController : public PeerListSearchController {\npublic:\n\tusing Role = ParticipantsBoxController::Role;\n\n\tParticipantsBoxSearchController(\n\t\tnot_null<ChannelData*> channel,\n\t\tRole role,\n\t\tnot_null<ParticipantsAdditionalData*> additional);\n\n\tvoid searchQuery(const QString &query) override;\n\tbool isLoading() override;\n\tbool loadMoreRows() override;\n\n\tstd::unique_ptr<SavedStateBase> saveState() const override;\n\tvoid restoreState(std::unique_ptr<SavedStateBase> state) override;\n\nprivate:\n\tstruct SavedState : SavedStateBase {\n\t\tQString query;\n\t\tint offset = 0;\n\t\tbool allLoaded = false;\n\t\tbool wasLoading = false;\n\t};\n\tstruct CacheEntry {\n\t\tMTPchannels_ChannelParticipants result;\n\t\tint requestedCount = 0;\n\t};\n\tstruct Query {\n\t\tQString text;\n\t\tint offset = 0;\n\t};\n\n\tvoid searchOnServer();\n\tbool searchInCache();\n\tvoid searchDone(\n\t\tmtpRequestId requestId,\n\t\tconst MTPchannels_ChannelParticipants &result,\n\t\tint requestedCount);\n\n\tnot_null<ChannelData*> _channel;\n\tRole _role = Role::Restricted;\n\tnot_null<ParticipantsAdditionalData*> _additional;\n\tMTP::Sender _api;\n\n\tbase::Timer _timer;\n\tQString _query;\n\tmtpRequestId _requestId = 0;\n\tint _offset = 0;\n\tbool _allLoaded = false;\n\tstd::map<QString, CacheEntry> _cache;\n\tstd::map<mtpRequestId, Query> _queries;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_peer_color_box.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_peer_colors.h\"\n#include \"api/api_peer_photo.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/peers/replace_boost_box.h\"\n#include \"boxes/background_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"boxes/stickers_box.h\"\n#include \"boxes/transfer_gift_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"core/ui_integration.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_web_page.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"info/channel_statistics/boosts/info_boosts_widget.h\"\n#include \"info/peer_gifts/info_peer_gifts_common.h\"\n#include \"info/profile/info_profile_emoji_status_panel.h\"\n#include \"info/profile/info_profile_top_bar.h\"\n#include \"info/info_controller.h\" // Key\n#include \"info/info_memento.h\"\n#include \"iv/iv_data.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/boxes/boost_box.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/controls/button_labels.h\"\n#include \"ui/controls/sub_tabs.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/new_badges.h\"\n#include \"ui/peer/color_sample.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/pill_tabs.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/color_contrast.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_info.h\" // defaultSubTabs.\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace {\n\nusing namespace Settings;\n\nconstexpr auto kFakeChannelId = ChannelId(0xFFFFFFF000ULL);\nconstexpr auto kFakeWebPageId = WebPageId(0xFFFFFFFF00000000ULL);\nconstexpr auto kUnsetColorIndex = uint8(0xFF);\n\nbase::unique_qptr<Ui::RpWidget> CreateEmptyPlaceholder(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tint width,\n\t\tconst QMargins &padding,\n\t\tFn<void()> switchToNextTab) {\n\tconst auto container = Ui::CreateChild<Ui::RpWidget>(parent);\n\tauto result = base::unique_qptr<Ui::RpWidget>{ container };\n\n\tauto icon = Settings::CreateLottieIcon(\n\t\tcontainer,\n\t\t{\n\t\t\t.name = u\"my_gifts_empty\"_q,\n\t\t\t.sizeOverride = st::normalBoxLottieSize,\n\t\t},\n\t\tst::settingsBlockedListIconPadding);\n\tconst auto iconWidget = icon.widget.data();\n\ticonWidget->show();\n\n\tconst auto emptyLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\tcontainer,\n\t\ttr::lng_gift_stars_tabs_my_empty(),\n\t\tst::giftBoxGiftEmptyLabel);\n\temptyLabel->setTryMakeSimilarLines(true);\n\temptyLabel->resizeToWidth(\n\t\twidth - st::boxRowPadding.left() - st::boxRowPadding.right());\n\temptyLabel->show();\n\n\tconst auto emptyNextLabel = switchToNextTab\n\t\t? Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_gift_stars_tabs_my_empty_next(\n\t\t\t\tlt_emoji,\n\t\t\t\trpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\t\ttr::link),\n\t\t\tst::giftBoxGiftEmptyLabel)\n\t\t: nullptr;\n\tif (emptyNextLabel) {\n\t\temptyNextLabel->resizeToWidth(\n\t\t\twidth - st::boxRowPadding.left() - st::boxRowPadding.right());\n\t\temptyNextLabel->setClickHandlerFilter([=](auto...) {\n\t\t\tswitchToNextTab();\n\t\t\treturn false;\n\t\t});\n\t}\n\n\ticon.animate(anim::repeat::loop);\n\n\tconst auto labelHeight = emptyLabel->height();\n\tconst auto nextLabelHeight = emptyNextLabel\n\t\t? emptyNextLabel->height()\n\t\t: 0;\n\tconst auto totalHeight = iconWidget->height()\n\t\t+ st::normalFont->height + labelHeight + nextLabelHeight\n\t\t+ (nextLabelHeight ? st::normalFont->height : 0)\n\t\t+ padding.top() + padding.bottom();\n\tcontainer->resize(width, totalHeight);\n\n\tcontainer->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tconst auto totalContentHeight = iconWidget->height()\n\t\t\t+ st::normalFont->height + emptyLabel->height()\n\t\t\t+ (emptyNextLabel\n\t\t\t\t? st::normalFont->height + emptyNextLabel->height()\n\t\t\t\t: 0);\n\t\tconst auto iconY = (size.height() - totalContentHeight) / 2;\n\t\ticonWidget->move(\n\t\t\t(size.width() - iconWidget->width()) / 2,\n\t\t\ticonY);\n\t\temptyLabel->move(\n\t\t\t(size.width() - emptyLabel->width()) / 2,\n\t\t\ticonY + iconWidget->height() + st::normalFont->height);\n\t\tif (emptyNextLabel) {\n\t\t\temptyNextLabel->move(\n\t\t\t\t(size.width() - emptyNextLabel->width()) / 2,\n\t\t\t\ticonY + iconWidget->height() + st::normalFont->height\n\t\t\t\t\t+ emptyLabel->height() + st::normalFont->height);\n\t\t}\n\t}, container->lifetime());\n\n\treturn result;\n}\n\nclass PreviewDelegate final : public HistoryView::DefaultElementDelegate {\npublic:\n\tPreviewDelegate(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Ui::ChatStyle*> st,\n\t\tFn<void()> update);\n\n\tbool elementAnimationsPaused() override;\n\tnot_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;\n\tHistoryView::Context elementContext() override;\n\nprivate:\n\tconst not_null<QWidget*> _parent;\n\tconst std::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\n};\n\nclass PreviewWrap final : public Ui::RpWidget {\npublic:\n\tPreviewWrap(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<Ui::ChatStyle> style,\n\t\tstd::shared_ptr<Ui::ChatTheme> theme,\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<uint8> colorIndexValue,\n\t\trpl::producer<DocumentId> backgroundEmojiId,\n\t\trpl::producer<std::optional<Ui::ColorCollectible>> colorCollectible);\n\t~PreviewWrap();\n\nprivate:\n\tusing Element = HistoryView::Element;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid initElements();\n\n\tconst not_null<Ui::GenericBox*> _box;\n\tconst not_null<PeerData*> _peer;\n\tconst not_null<ChannelData*> _fake;\n\tconst not_null<History*> _history;\n\tconst not_null<WebPageData*> _webpage;\n\tconst std::shared_ptr<Ui::ChatTheme> _theme;\n\tconst std::shared_ptr<Ui::ChatStyle> _style;\n\tconst std::unique_ptr<PreviewDelegate> _delegate;\n\tconst not_null<HistoryItem*> _replyToItem;\n\tconst not_null<HistoryItem*> _replyItem;\n\tstd::unique_ptr<Element> _element;\n\tUi::PeerUserpicView _userpic;\n\tQPoint _position;\n\n};\n\nclass LevelBadge final : public Ui::RpWidget {\npublic:\n\tLevelBadge(\n\t\tnot_null<QWidget*> parent,\n\t\tuint32 level,\n\t\tnot_null<Main::Session*> session);\n\n\tvoid setMinimal(bool value);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid updateText();\n\n\tconst uint32 _level;\n\tconst Ui::Text::MarkedContext _context;\n\tUi::Text::String _text;\n\tbool _minimal = false;\n\n};\n\n\n\nPreviewWrap::PreviewWrap(\n\tnot_null<Ui::GenericBox*> box,\n\tstd::shared_ptr<Ui::ChatStyle> style,\n\tstd::shared_ptr<Ui::ChatTheme> theme,\n\tnot_null<PeerData*> peer,\n\trpl::producer<uint8> colorIndexValue,\n\trpl::producer<DocumentId> backgroundEmojiId,\n\trpl::producer<std::optional<Ui::ColorCollectible>> colorCollectible)\n: RpWidget(box)\n, _box(box)\n, _peer(peer)\n, _fake(_peer->owner().channel(kFakeChannelId))\n, _history(_fake->owner().history(_fake))\n, _webpage(_peer->owner().webpage(\n\tkFakeWebPageId,\n\tWebPageType::Article,\n\tu\"internal:peer-color-webpage-preview\"_q,\n\tu\"internal:peer-color-webpage-preview\"_q,\n\ttr::lng_settings_color_link_name(tr::now),\n\ttr::lng_settings_color_link_title(tr::now),\n\t{ tr::lng_settings_color_link_description(tr::now) },\n\tnullptr, // photo\n\tnullptr, // document\n\tWebPageCollage(),\n\tnullptr, // iv\n\tnullptr, // stickerSet\n\tnullptr, // uniqueGift\n\t0, // duration\n\tQString(), // author\n\tfalse, // hasLargeMedia\n\tfalse, // photoIsVideoCover\n\t0)) // pendingTill\n, _theme(theme)\n, _style(style)\n, _delegate(std::make_unique<PreviewDelegate>(box, _style.get(), [=] {\n\tupdate();\n}))\n, _replyToItem(_history->addNewLocalMessage({\n\t.id = _history->nextNonHistoryEntryId(),\n\t.flags = (MessageFlag::FakeHistoryItem\n\t\t| MessageFlag::HasFromId\n\t\t| MessageFlag::Post),\n\t.from = _fake->id,\n\t.date = base::unixtime::now(),\n}, TextWithEntities{ _peer->isSelf()\n\t? tr::lng_settings_color_reply(tr::now)\n\t: tr::lng_settings_color_reply_channel(tr::now),\n}, MTP_messageMediaEmpty()))\n, _replyItem(_history->addNewLocalMessage({\n\t.id = _history->nextNonHistoryEntryId(),\n\t.flags = (MessageFlag::FakeHistoryItem\n\t\t| MessageFlag::HasFromId\n\t\t| MessageFlag::HasReplyInfo\n\t\t| MessageFlag::Post),\n\t.from = _fake->id,\n\t.replyTo = FullReplyTo{.messageId = _replyToItem->fullId() },\n\t.date = base::unixtime::now(),\n}, TextWithEntities{ _peer->isSelf()\n\t? tr::lng_settings_color_text(tr::now)\n\t: tr::lng_settings_color_text_channel(tr::now),\n}, MTP_messageMediaWebPage(\n\tMTP_flags(0),\n\tMTP_webPagePending(\n\t\tMTP_flags(0),\n\t\tMTP_long(_webpage->id),\n\t\tMTPstring(),\n\t\tMTP_int(0)))))\n, _element(_replyItem->createView(_delegate.get()))\n, _position(0, st::msgMargin.bottom()) {\n\t_style->apply(_theme.get());\n\n\t_fake->setName(peer->name(), QString());\n\tstd::move(colorIndexValue) | rpl::on_next([=](uint8 index) {\n\t\tif (index != kUnsetColorIndex) {\n\t\t\t_fake->changeColorIndex(index);\n\t\t\tupdate();\n\t\t}\n\t}, lifetime());\n\tstd::move(backgroundEmojiId) | rpl::on_next([=](DocumentId id) {\n\t\t_fake->changeBackgroundEmojiId(id);\n\t\tupdate();\n\t}, lifetime());\n\tstd::move(colorCollectible) | rpl::on_next([=](\n\t\t\tstd::optional<Ui::ColorCollectible> &&collectible) {\n\t\tif (collectible) {\n\t\t\t_fake->changeColorCollectible(std::move(*collectible));\n\t\t} else {\n\t\t\t_fake->clearColorCollectible();\n\t\t}\n\t\tupdate();\n\t}, lifetime());\n\n\tconst auto session = &_history->session();\n\tsession->data().viewRepaintRequest(\n\t) | rpl::on_next([=](Data::RequestViewRepaint data) {\n\t\tif (data.view == _element.get()) {\n\t\t\tupdate();\n\t\t}\n\t}, lifetime());\n\n\tinitElements();\n}\n\nPreviewWrap::~PreviewWrap() {\n\t_element = nullptr;\n\t_replyItem->destroy();\n\t_replyToItem->destroy();\n}\n\nvoid PreviewWrap::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\tconst auto clip = e->rect();\n\n\tp.setClipRect(clip);\n\tWindow::SectionWidget::PaintBackground(\n\t\tp,\n\t\t_theme.get(),\n\t\tQSize(_box->width(), _box->window()->height()),\n\t\tclip);\n\n\tauto context = _theme->preparePaintContext(\n\t\t_style.get(),\n\t\trect(),\n\t\trect(),\n\t\tclip,\n\t\t!window()->isActiveWindow());\n\n\tp.translate(_position);\n\t_element->draw(p, context);\n\n\tif (_element->displayFromPhoto()) {\n\t\tauto userpicBottom = height()\n\t\t\t- _element->marginBottom()\n\t\t\t- _element->marginTop();\n\t\tconst auto userpicTop = userpicBottom - st::msgPhotoSize;\n\t\t_peer->paintUserpicLeft(\n\t\t\tp,\n\t\t\t_userpic,\n\t\t\tst::historyPhotoLeft,\n\t\t\tuserpicTop,\n\t\t\twidth(),\n\t\t\tst::msgPhotoSize);\n\t}\n}\n\nvoid PreviewWrap::initElements() {\n\t_element->initDimensions();\n\n\twidthValue(\n\t) | rpl::filter([=](int width) {\n\t\treturn width > st::msgMinWidth;\n\t}) | rpl::on_next([=](int width) {\n\t\tconst auto height = _position.y()\n\t\t\t+ _element->resizeGetHeight(width)\n\t\t\t+ st::msgMargin.top();\n\t\tresize(width, height);\n\t}, lifetime());\n}\n\nPreviewDelegate::PreviewDelegate(\n\tnot_null<QWidget*> parent,\n\tnot_null<Ui::ChatStyle*> st,\n\tFn<void()> update)\n: _parent(parent)\n, _pathGradient(HistoryView::MakePathShiftGradient(st, update)) {\n}\n\nbool PreviewDelegate::elementAnimationsPaused() {\n\treturn _parent->window()->isActiveWindow();\n}\n\nauto PreviewDelegate::elementPathShiftGradient()\n-> not_null<Ui::PathShiftGradient*> {\n\treturn _pathGradient.get();\n}\n\nHistoryView::Context PreviewDelegate::elementContext() {\n\treturn HistoryView::Context::AdminLog;\n}\n\nLevelBadge::LevelBadge(\n\tnot_null<QWidget*> parent,\n\tuint32 level,\n\tnot_null<Main::Session*> session)\n: Ui::RpWidget(parent)\n, _level(level) {\n\tupdateText();\n}\n\nvoid LevelBadge::updateText() {\n\tauto text = Ui::Text::IconEmoji(&st::settingsLevelBadgeLock).append(' ');\n\tif (!_minimal) {\n\t\ttext.append(tr::lng_boost_level(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\t_level,\n\t\t\ttr::marked));\n\t} else {\n\t\ttext.append(QString::number(_level));\n\t}\n\tconst auto &st = st::settingsPremiumNewBadge.style;\n\t_text.setMarkedText(\n\t\tst,\n\t\ttext,\n\t\tkMarkupTextOptions,\n\t\tUi::Text::MarkedContext{ .repaint = [=] { update(); } });\n\tconst auto &padding = st::settingsColorSamplePadding;\n\tQWidget::resize(\n\t\t_text.maxWidth() + rect::m::sum::h(padding),\n\t\tst.font->height + rect::m::sum::v(padding));\n}\n\nvoid LevelBadge::setMinimal(bool value) {\n\tif ((value != _minimal) && value) {\n\t\t_minimal = value;\n\t\tupdateText();\n\t\tupdate();\n\t}\n}\n\nvoid LevelBadge::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tconst auto radius = height() / 2;\n\tp.setPen(Qt::NoPen);\n\tauto gradient = QLinearGradient(QPointF(0, 0), QPointF(width(), 0));\n\tgradient.setStops(Ui::Premium::ButtonGradientStops());\n\tp.setBrush(gradient);\n\tp.drawRoundedRect(rect(), radius, radius);\n\n\tp.setPen(st::premiumButtonFg);\n\tp.setBrush(Qt::NoBrush);\n\n\tconst auto context = Ui::Text::PaintContext{\n\t\t.position = rect::m::pos::tl(st::settingsColorSamplePadding),\n\t\t.outerWidth = width(),\n\t\t.availableWidth = width(),\n\t};\n\t_text.draw(p, context);\n}\n\nstruct SetValues {\n\tuint8 colorIndex = 0;\n\tDocumentId backgroundEmojiId = 0;\n\tstd::optional<Ui::ColorCollectible> colorCollectible;\n\tEmojiStatusId statusId;\n\tTimeId statusUntil = 0;\n\tbool statusChanged = false;\n\tbool forProfile = false;\n};\nvoid Set(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tSetValues values,\n\t\tbool showToast = true) {\n\tconst auto wasIndex = values.forProfile\n\t\t? peer->colorProfileIndex().value_or(kUnsetColorIndex)\n\t\t: peer->colorIndex();\n\tconst auto wasEmojiId = values.forProfile\n\t\t? peer->profileBackgroundEmojiId()\n\t\t: peer->backgroundEmojiId();\n\tconst auto &wasColorCollectible = peer->colorCollectible();\n\n\tconst auto setLocal = [=](\n\t\t\tuint8 index,\n\t\t\tDocumentId emojiId,\n\t\t\tstd::optional<Ui::ColorCollectible> colorCollectible) {\n\t\tusing UpdateFlag = Data::PeerUpdate::Flag;\n\t\tif (values.forProfile) {\n\t\t\tif (index == kUnsetColorIndex) {\n\t\t\t\tpeer->clearColorProfileIndex();\n\t\t\t} else {\n\t\t\t\tpeer->changeColorProfileIndex(index);\n\t\t\t}\n\t\t\tpeer->changeProfileBackgroundEmojiId(emojiId);\n\t\t} else {\n\t\t\tif (index == kUnsetColorIndex) {\n\t\t\t\tpeer->clearColorIndex();\n\t\t\t} else {\n\t\t\t\tpeer->changeColorIndex(index);\n\t\t\t}\n\t\t\tif (colorCollectible) {\n\t\t\t\tpeer->changeColorCollectible(*colorCollectible);\n\t\t\t} else {\n\t\t\t\tpeer->clearColorCollectible();\n\t\t\t}\n\t\t\tpeer->changeBackgroundEmojiId(emojiId);\n\t\t}\n\t\tpeer->session().changes().peerUpdated(\n\t\t\tpeer,\n\t\t\t(UpdateFlag::BackgroundEmoji\n\t\t\t\t| (values.forProfile\n\t\t\t\t\t? UpdateFlag::ColorProfile\n\t\t\t\t\t: UpdateFlag::Color)));\n\t};\n\tsetLocal(\n\t\tvalues.colorIndex,\n\t\tvalues.backgroundEmojiId,\n\t\tvalues.colorCollectible);\n\n\tconst auto done = [=] {\n\t\tif (showToast) {\n\t\t\tshow->showToast(peer->isSelf()\n\t\t\t\t? (values.forProfile\n\t\t\t\t\t? tr::lng_settings_color_changed_profile(tr::now)\n\t\t\t\t\t: tr::lng_settings_color_changed(tr::now))\n\t\t\t\t: (values.forProfile\n\t\t\t\t\t? tr::lng_settings_color_changed_profile_channel(tr::now)\n\t\t\t\t\t: tr::lng_settings_color_changed_channel(tr::now)));\n\t\t}\n\t};\n\tconst auto fail = [=](const MTP::Error &error) {\n\t\tconst auto type = error.type();\n\t\tif (type != u\"CHAT_NOT_MODIFIED\"_q) {\n\t\t\tsetLocal(wasIndex, wasEmojiId, wasColorCollectible\n\t\t\t\t? *wasColorCollectible\n\t\t\t\t: std::optional<Ui::ColorCollectible>());\n\t\t\tshow->showToast(type);\n\t\t}\n\t};\n\tconst auto send = [&](auto &&request) {\n\t\tpeer->session().api().request(\n\t\t\tstd::move(request)\n\t\t).done(done).fail(fail).send();\n\t};\n\tif (peer->isSelf()) {\n\t\tusing Flag = MTPaccount_UpdateColor::Flag;\n\t\tusing ColorFlag = MTPDpeerColor::Flag;\n\t\tsend(MTPaccount_UpdateColor(\n\t\t\tMTP_flags((values.forProfile ? Flag::f_for_profile : Flag(0))\n\t\t\t\t| (((!values.forProfile && values.colorCollectible)\n\t\t\t\t\t|| (values.colorIndex != kUnsetColorIndex))\n\t\t\t\t\t? Flag::f_color\n\t\t\t\t\t: Flag(0))),\n\t\t\t((!values.forProfile && values.colorCollectible)\n\t\t\t\t? MTP_inputPeerColorCollectible(\n\t\t\t\t\tMTP_long(values.colorCollectible->collectibleId))\n\t\t\t\t: MTP_peerColor(\n\t\t\t\t\tMTP_flags(ColorFlag()\n\t\t\t\t\t\t| ColorFlag::f_color\n\t\t\t\t\t\t| (values.backgroundEmojiId\n\t\t\t\t\t\t\t? ColorFlag::f_background_emoji_id\n\t\t\t\t\t\t\t: ColorFlag(0))),\n\t\t\t\t\tMTP_int(values.colorIndex),\n\t\t\t\t\tMTP_long(values.backgroundEmojiId)))));\n\t\tif (values.statusChanged\n\t\t\t&& (values.statusId || peer->emojiStatusId())) {\n\t\t\tpeer->owner().emojiStatuses().set(\n\t\t\t\tpeer,\n\t\t\t\tvalues.statusId,\n\t\t\t\tvalues.statusUntil);\n\t\t}\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tif (peer->isBroadcast()) {\n\t\t\tusing Flag = MTPchannels_UpdateColor::Flag;\n\t\t\tsend(MTPchannels_UpdateColor(\n\t\t\t\tMTP_flags((values.colorIndex != kUnsetColorIndex\n\t\t\t\t\t\t? Flag::f_color\n\t\t\t\t\t\t: Flag(0))\n\t\t\t\t\t| Flag::f_background_emoji_id\n\t\t\t\t\t| (values.forProfile ? Flag::f_for_profile : Flag(0))),\n\t\t\t\tchannel->inputChannel(),\n\t\t\t\tMTP_int(values.colorIndex),\n\t\t\t\tMTP_long(values.backgroundEmojiId)));\n\t\t}\n\t\tif (values.statusChanged\n\t\t\t&& (values.statusId || peer->emojiStatusId())) {\n\t\t\tpeer->owner().emojiStatuses().set(\n\t\t\t\tchannel,\n\t\t\t\tvalues.statusId,\n\t\t\t\tvalues.statusUntil);\n\t\t}\n\t} else {\n\t\tUnexpected(\"Invalid peer type in Set(colorIndex).\");\n\t}\n}\n\nbool ShowPremiumPreview(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer) {\n\tif (!peer->isSelf() || show->session().premium()) {\n\t\treturn false;\n\t}\n\tif (const auto controller = show->resolveWindow()) {\n\t\tShowPremiumPreviewBox(controller, PremiumFeature::PeerColors);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Apply(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tSetValues values,\n\t\tFn<void()> close,\n\t\tFn<void()> cancel,\n\t\tbool showToast = true) {\n\tconst auto currentColorIndex = values.forProfile\n\t\t? peer->colorProfileIndex().value_or(kUnsetColorIndex)\n\t\t: peer->colorIndex();\n\tconst auto currentEmojiId = values.forProfile\n\t\t? peer->profileBackgroundEmojiId()\n\t\t: peer->backgroundEmojiId();\n\n\tconst auto colorMatch = (currentColorIndex == values.colorIndex);\n\tconst auto emojiMatch = (currentEmojiId == values.backgroundEmojiId);\n\tconst auto collectibleMatch = values.forProfile\n\t\t|| ((!peer->colorCollectible() == !values.colorCollectible)\n\t\t\t&& (!peer->colorCollectible()\n\t\t\t\t|| (*peer->colorCollectible() == *values.colorCollectible)));\n\n\tif (colorMatch\n\t\t&& emojiMatch\n\t\t&& collectibleMatch\n\t\t&& !values.statusChanged) {\n\t\tclose();\n\t} else if (peer->isSelf()) {\n\t\tSet(show, peer, values, showToast);\n\t\tclose();\n\t} else {\n\t\tCheckBoostLevel(show, peer, [=](int level) {\n\t\t\tconst auto peerColors = &peer->session().api().peerColors();\n\t\t\tconst auto colorRequired = peerColors->requiredLevelFor(\n\t\t\t\tpeer->id,\n\t\t\t\tvalues.colorIndex,\n\t\t\t\tpeer->isMegagroup(),\n\t\t\t\tvalues.forProfile);\n\t\t\tconst auto limits = Data::LevelLimits(&peer->session());\n\t\t\tconst auto iconRequired = values.backgroundEmojiId\n\t\t\t\t? limits.channelBgIconLevelMin()\n\t\t\t\t: 0;\n\t\t\tconst auto statusRequired = (values.statusChanged\n\t\t\t\t&& values.statusId)\n\t\t\t\t? limits.channelEmojiStatusLevelMin()\n\t\t\t\t: 0;\n\t\t\tconst auto required = std::max({\n\t\t\t\tcolorRequired,\n\t\t\t\ticonRequired,\n\t\t\t\tstatusRequired,\n\t\t\t});\n\t\t\tif (level >= required) {\n\t\t\t\tSet(show, peer, values, showToast);\n\t\t\t\tclose();\n\t\t\t\treturn std::optional<Ui::AskBoostReason>();\n\t\t\t}\n\t\t\tconst auto reason = [&]() -> Ui::AskBoostReason {\n\t\t\t\tif (level < statusRequired) {\n\t\t\t\t\treturn { Ui::AskBoostEmojiStatus{\n\t\t\t\t\t\tstatusRequired,\n\t\t\t\t\t\tpeer->isMegagroup()\n\t\t\t\t\t} };\n\t\t\t\t} else if (level < iconRequired) {\n\t\t\t\t\treturn { Ui::AskBoostChannelColor{ iconRequired } };\n\t\t\t\t}\n\t\t\t\treturn { Ui::AskBoostChannelColor{ colorRequired } };\n\t\t\t}();\n\t\t\treturn std::make_optional(reason);\n\t\t}, cancel);\n\t}\n}\n\n\n\n[[nodiscard]] auto ButtonStyleWithAddedPadding(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::SettingsButton &basicSt,\n\t\tQMargins added) {\n\tconst auto st = parent->lifetime().make_state<style::SettingsButton>(\n\t\tbasicSt);\n\tst->padding += added;\n\treturn st;\n}\n\n[[nodiscard]] object_ptr<Ui::SettingsButton> CreateEmojiIconButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tstd::shared_ptr<Ui::ChatStyle> style,\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<uint8> colorIndexValue,\n\t\trpl::producer<DocumentId> emojiIdValue,\n\t\tFn<void(DocumentId)> emojiIdChosen,\n\t\tbool profileIndices) {\n\tconst auto button = ButtonStyleWithRightEmoji(\n\t\tparent,\n\t\ttr::lng_settings_color_emoji_off(tr::now));\n\tauto result = Settings::CreateButtonWithIcon(\n\t\tparent,\n\t\t!profileIndices\n\t\t\t? tr::lng_settings_color_emoji()\n\t\t\t: peer->isChannel()\n\t\t\t? tr::lng_settings_color_profile_emoji_channel()\n\t\t\t: tr::lng_settings_color_profile_emoji(),\n\t\t*button.st,\n\t\t{ &st::menuBlueIconColorNames });\n\tconst auto raw = result.data();\n\n\tconst auto right = Ui::CreateChild<Ui::RpWidget>(raw);\n\tright->show();\n\n\tusing namespace Info::Profile;\n\tstruct State {\n\t\tEmojiStatusPanel panel;\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji> emoji;\n\t\tDocumentId emojiId = 0;\n\t\tuint8 index = 0;\n\t};\n\tconst auto state = right->lifetime().make_state<State>();\n\tstate->panel.someCustomChosen(\n\t) | rpl::on_next([=](EmojiStatusPanel::CustomChosen chosen) {\n\t\temojiIdChosen(chosen.id.documentId);\n\t}, raw->lifetime());\n\n\tstd::move(colorIndexValue) | rpl::on_next([=](uint8 index) {\n\t\tstate->index = index;\n\t\tif (state->emoji) {\n\t\t\tright->update();\n\t\t}\n\t}, right->lifetime());\n\n\tconst auto session = &show->session();\n\tconst auto added = st::lineWidth * 2;\n\tstd::move(emojiIdValue) | rpl::on_next([=](DocumentId emojiId) {\n\t\tstate->emojiId = emojiId;\n\t\tstate->emoji = emojiId\n\t\t\t? session->data().customEmojiManager().create(\n\t\t\t\temojiId,\n\t\t\t\t[=] { right->update(); })\n\t\t\t: nullptr;\n\t\tright->resize(\n\t\t\t(emojiId ? button.emojiWidth : button.noneWidth) + button.added,\n\t\t\tright->height());\n\t\tright->update();\n\t}, right->lifetime());\n\n\trpl::combine(\n\t\traw->sizeValue(),\n\t\tright->widthValue()\n\t) | rpl::on_next([=](QSize outer, int width) {\n\t\tright->resize(width, outer.height());\n\t\tconst auto skip = st::settingsButton.padding.right();\n\t\tright->moveToRight(skip - button.added, 0, outer.width());\n\t}, right->lifetime());\n\n\tright->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tif (state->panel.paintBadgeFrame(right)) {\n\t\t\treturn;\n\t\t}\n\t\tauto p = QPainter(right);\n\t\tconst auto height = right->height();\n\t\tif (state->emoji\n\t\t\t&& (state->index != kUnsetColorIndex || profileIndices)) {\n\t\t\tconst auto profileSet = profileIndices\n\t\t\t\t? peer->session().api().peerColors().colorProfileFor(\n\t\t\t\t\tstate->index)\n\t\t\t\t: std::nullopt;\n\t\t\tconst auto textColor = profileSet && !profileSet->palette.empty()\n\t\t\t\t? profileSet->palette.front()\n\t\t\t\t: profileIndices\n\t\t\t\t? style->windowActiveTextFg()->c\n\t\t\t\t: style->coloredValues(false, state->index).name;\n\t\t\tstate->emoji->paint(p, {\n\t\t\t\t.textColor = textColor,\n\t\t\t\t.position = QPoint(added, (height - button.emojiWidth) / 2),\n\t\t\t\t.internal = {\n\t\t\t\t\t.forceFirstFrame = true,\n\t\t\t\t},\n\t\t\t});\n\t\t} else {\n\t\t\tconst auto &font = st::normalFont;\n\t\t\tp.setFont(font);\n\t\t\tp.setPen(style->windowActiveTextFg());\n\t\t\tp.drawText(\n\t\t\t\tQPoint(added, (height - font->height) / 2 + font->ascent),\n\t\t\t\ttr::lng_settings_color_emoji_off(tr::now));\n\t\t}\n\t}, right->lifetime());\n\n\traw->setClickedCallback([=] {\n\t\tconst auto customTextColor = [=] {\n\t\t\tif (state->index == kUnsetColorIndex) {\n\t\t\t\treturn style->windowActiveTextFg()->c;\n\t\t\t}\n\t\t\tif (profileIndices) {\n\t\t\t\tconst auto colorSet\n\t\t\t\t\t= peer->session().api().peerColors().colorProfileFor(\n\t\t\t\t\t\tstate->index);\n\t\t\t\tif (colorSet && !colorSet->palette.empty()) {\n\t\t\t\t\treturn colorSet->palette.front();\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn style->coloredValues(false, state->index).name;\n\t\t};\n\t\tconst auto controller = show->resolveWindow();\n\t\tif (controller) {\n\t\t\tstate->panel.show({\n\t\t\t\t.controller = controller,\n\t\t\t\t.button = right,\n\t\t\t\t.ensureAddedEmojiId = { state->emojiId },\n\t\t\t\t.customTextColor = customTextColor,\n\t\t\t\t.backgroundEmojiMode = true,\n\t\t\t});\n\t\t}\n\t});\n\n\tif (const auto channel = peer->asChannel()) {\n\t\tconst auto limits = Data::LevelLimits(&channel->session());\n\t\tAddLevelBadge(\n\t\t\tprofileIndices\n\t\t\t\t? limits.channelProfileBgIconLevelMin()\n\t\t\t\t: limits.channelBgIconLevelMin(),\n\t\t\traw,\n\t\t\tright,\n\t\t\tchannel,\n\t\t\tbutton.st->padding,\n\t\t\ttr::lng_settings_color_emoji());\n\t}\n\n\treturn result;\n}\n\n[[nodiscard]] object_ptr<Ui::SettingsButton> CreateEmojiStatusButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<ChannelData*> channel,\n\t\trpl::producer<EmojiStatusId> statusIdValue,\n\t\tFn<void(EmojiStatusId,TimeId)> statusIdChosen,\n\t\tbool group) {\n\tconst auto button = ButtonStyleWithRightEmoji(\n\t\tparent,\n\t\ttr::lng_settings_color_emoji_off(tr::now));\n\tconst auto &phrase = group\n\t\t? tr::lng_edit_channel_status_group\n\t\t: tr::lng_edit_channel_status;\n\tauto result = Settings::CreateButtonWithIcon(\n\t\tparent,\n\t\tphrase(),\n\t\t*button.st,\n\t\t{ &st::menuBlueIconEmojiStatus });\n\tconst auto raw = result.data();\n\n\tconst auto right = Ui::CreateChild<Ui::RpWidget>(raw);\n\tright->show();\n\n\tusing namespace Info::Profile;\n\tstruct State {\n\t\tEmojiStatusPanel panel;\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji> emoji;\n\t\tEmojiStatusId statusId;\n\t};\n\tconst auto state = right->lifetime().make_state<State>();\n\tstate->panel.someCustomChosen(\n\t) | rpl::on_next([=](EmojiStatusPanel::CustomChosen chosen) {\n\t\tstatusIdChosen({ chosen.id }, chosen.until);\n\t}, raw->lifetime());\n\n\tconst auto session = &show->session();\n\tstd::move(statusIdValue) | rpl::on_next([=](EmojiStatusId id) {\n\t\tstate->statusId = id;\n\t\tstate->emoji = id\n\t\t\t? session->data().customEmojiManager().create(\n\t\t\t\tData::EmojiStatusCustomId(id),\n\t\t\t\t[=] { right->update(); })\n\t\t\t: nullptr;\n\t\tright->resize(\n\t\t\t(id ? button.emojiWidth : button.noneWidth) + button.added,\n\t\t\tright->height());\n\t\tright->update();\n\t}, right->lifetime());\n\n\trpl::combine(\n\t\traw->sizeValue(),\n\t\tright->widthValue()\n\t) | rpl::on_next([=](QSize outer, int width) {\n\t\tright->resize(width, outer.height());\n\t\tconst auto skip = st::settingsButton.padding.right();\n\t\tright->moveToRight(skip - button.added, 0, outer.width());\n\t}, right->lifetime());\n\n\tright->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tif (state->panel.paintBadgeFrame(right)) {\n\t\t\treturn;\n\t\t}\n\t\tauto p = QPainter(right);\n\t\tconst auto height = right->height();\n\t\tif (state->emoji) {\n\t\t\tstate->emoji->paint(p, {\n\t\t\t\t.textColor = anim::color(\n\t\t\t\t\tst::stickerPanPremium1,\n\t\t\t\t\tst::stickerPanPremium2,\n\t\t\t\t\t0.5),\n\t\t\t\t.position = QPoint(\n\t\t\t\t\tbutton.added,\n\t\t\t\t\t(height - button.emojiWidth) / 2),\n\t\t\t});\n\t\t} else {\n\t\t\tconst auto &font = st::normalFont;\n\t\t\tp.setFont(font);\n\t\t\tp.setPen(st::windowActiveTextFg);\n\t\t\tp.drawText(\n\t\t\t\tQPoint(\n\t\t\t\t\tbutton.added,\n\t\t\t\t\t(height - font->height) / 2 + font->ascent),\n\t\t\t\ttr::lng_settings_color_emoji_off(tr::now));\n\t\t}\n\t}, right->lifetime());\n\n\traw->setClickedCallback([=] {\n\t\tconst auto controller = show->resolveWindow();\n\t\tif (controller) {\n\t\t\tstate->panel.show({\n\t\t\t\t.controller = controller,\n\t\t\t\t.button = right,\n\t\t\t\t.ensureAddedEmojiId = { state->statusId },\n\t\t\t\t.channelStatusMode = true,\n\t\t\t});\n\t\t}\n\t});\n\n\tconst auto limits = Data::LevelLimits(&channel->session());\n\tAddLevelBadge(\n\t\t(group\n\t\t\t? limits.groupEmojiStatusLevelMin()\n\t\t\t: limits.channelEmojiStatusLevelMin()),\n\t\traw,\n\t\tright,\n\t\tchannel,\n\t\tbutton.st->padding,\n\t\tphrase());\n\n\treturn result;\n}\n\n[[nodiscard]] object_ptr<Ui::SettingsButton> CreateEmojiPackButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<ChannelData*> channel) {\n\tExpects(channel->mgInfo != nullptr);\n\n\tconst auto button = ButtonStyleWithRightEmoji(\n\t\tparent,\n\t\ttr::lng_settings_color_emoji_off(tr::now));\n\tauto result = Settings::CreateButtonWithIcon(\n\t\tparent,\n\t\ttr::lng_group_emoji(),\n\t\t*button.st,\n\t\t{ &st::menuBlueIconEmojiPack });\n\tconst auto raw = result.data();\n\n\tstruct State {\n\t\tDocumentData *icon = nullptr;\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji> custom;\n\t\tQImage cache;\n\t};\n\tconst auto state = parent->lifetime().make_state<State>();\n\n\tconst auto right = Ui::CreateChild<Ui::RpWidget>(raw);\n\tright->show();\n\tright->resize(\n\t\tbutton.emojiWidth + button.added,\n\t\tright->height());\n\n\trpl::combine(\n\t\traw->sizeValue(),\n\t\tright->widthValue()\n\t) | rpl::on_next([=](QSize outer, int width) {\n\t\tright->resize(width, outer.height());\n\t\tconst auto skip = st::settingsButton.padding.right();\n\t\tright->moveToRight(skip - button.added, 0, outer.width());\n\t}, right->lifetime());\n\n\tright->paintRequest(\n\t) | rpl::filter([=] {\n\t\treturn state->icon != nullptr;\n\t}) | rpl::on_next([=] {\n\t\tauto p = QPainter(right);\n\t\tconst auto x = button.added;\n\t\tconst auto y = (right->height() - button.emojiWidth) / 2;\n\t\tconst auto active = right->window()->isActiveWindow();\n\t\tif (const auto emoji = state->icon) {\n\t\t\tif (!state->custom\n\t\t\t\t&& emoji->sticker()\n\t\t\t\t&& emoji->sticker()->setType == Data::StickersType::Emoji) {\n\t\t\t\tauto &manager = emoji->owner().customEmojiManager();\n\t\t\t\tstate->custom = manager.create(\n\t\t\t\t\temoji->id,\n\t\t\t\t\t[=] { right->update(); },\n\t\t\t\t\t{});\n\t\t\t}\n\t\t\tif (state->custom) {\n\t\t\t\tstate->custom->paint(p, Ui::Text::CustomEmoji::Context{\n\t\t\t\t\t.textColor = st::windowFg->c,\n\t\t\t\t\t.now = crl::now(),\n\t\t\t\t\t.position = { x, y },\n\t\t\t\t\t.paused = !active,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}, right->lifetime());\n\n\traw->setClickedCallback([=] {\n\t\tconst auto isEmoji = true;\n\t\tshow->showBox(Box<StickersBox>(show, channel, isEmoji));\n\t});\n\n\tchannel->session().changes().peerFlagsValue(\n\t\tchannel,\n\t\tData::PeerUpdate::Flag::EmojiSet\n\t) | rpl::map([=]() -> rpl::producer<DocumentData*> {\n\t\tconst auto id = channel->mgInfo->emojiSet.id;\n\t\tif (!id) {\n\t\t\treturn rpl::single<DocumentData*>(nullptr);\n\t\t}\n\t\tconst auto sets = &channel->owner().stickers().sets();\n\t\tauto wrapLoaded = [=](Data::StickersSets::const_iterator it) {\n\t\t\treturn it->second->lookupThumbnailDocument();\n\t\t};\n\t\tconst auto it = sets->find(id);\n\t\tif (it != sets->cend()\n\t\t\t&& !(it->second->flags & Data::StickersSetFlag::NotLoaded)) {\n\t\t\treturn rpl::single(wrapLoaded(it));\n\t\t}\n\t\treturn rpl::single<DocumentData*>(\n\t\t\tnullptr\n\t\t) | rpl::then(channel->owner().stickers().updated(\n\t\t\tData::StickersType::Emoji\n\t\t) | rpl::filter([=] {\n\t\t\tconst auto it = sets->find(id);\n\t\t\treturn (it != sets->cend())\n\t\t\t\t&& !(it->second->flags & Data::StickersSetFlag::NotLoaded);\n\t\t}) | rpl::map([=] {\n\t\t\treturn wrapLoaded(sets->find(id));\n\t\t}));\n\t}) | rpl::flatten_latest(\n\t) | rpl::on_next([=](DocumentData *icon) {\n\t\tif (state->icon != icon) {\n\t\t\tstate->icon = icon;\n\t\t\tstate->custom = nullptr;\n\t\t\tright->update();\n\t\t}\n\t}, right->lifetime());\n\n\tAddLevelBadge(\n\t\tData::LevelLimits(&channel->session()).groupEmojiStickersLevelMin(),\n\t\traw,\n\t\tright,\n\t\tchannel,\n\t\tbutton.st->padding,\n\t\ttr::lng_group_emoji());\n\n\treturn result;\n}\n\nstruct ColorGiftTabsResult {\n\tFn<void()> switchToNext;\n\tQPointer<Ui::SubTabs> tabs;\n};\n\nColorGiftTabsResult AddColorGiftTabs(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Main::Session*> session,\n\t\tFn<void(uint64 giftId)> chosen,\n\t\tbool profile) {\n\tusing namespace Info::PeerGifts;\n\n\tstruct State {\n\t\trpl::variable<std::vector<Data::StarGift>> list;\n\t\tUi::SubTabs *tabs = nullptr;\n\t};\n\tconst auto state = container->lifetime().make_state<State>();\n\n\tGiftsStars(\n\t\tsession,\n\t\tsession->user()\n\t) | rpl::on_next([=](const std::vector<GiftTypeStars> &list) {\n\t\tauto filtered = std::vector<Data::StarGift>();\n\t\tfor (const auto &gift : list) {\n\t\t\tif ((profile || gift.info.peerColorAvailable) && gift.resale) {\n\t\t\t\tfiltered.push_back(gift.info);\n\t\t\t}\n\t\t}\n\t\tstate->list = std::move(filtered);\n\t}, container->lifetime());\n\n\tstate->list.value(\n\t) | rpl::on_next([=](const std::vector<Data::StarGift> &list) {\n\t\tauto tabs = std::vector<Ui::SubTabs::Tab>();\n\t\ttabs.push_back({\n\t\t\t.id = u\"my\"_q,\n\t\t\t.text = tr::lng_gift_stars_tabs_my(tr::now, tr::marked),\n\t\t});\n\t\tfor (const auto &gift : list) {\n\t\t\tauto text = TextWithEntities();\n\t\t\ttabs.push_back({\n\t\t\t\t.id = QString::number(gift.id),\n\t\t\t\t.text = Data::SingleCustomEmoji(\n\t\t\t\t\tgift.document).append(' ').append(gift.resellTitle),\n\t\t\t});\n\t\t}\n\t\tconst auto context = Core::TextContext({\n\t\t\t.session = session,\n\t\t});\n\t\tif (!state->tabs) {\n\t\t\tstate->tabs = container->add(\n\t\t\t\tobject_ptr<Ui::SubTabs>(\n\t\t\t\t\tcontainer,\n\t\t\t\t\tst::defaultSubTabs,\n\t\t\t\t\tUi::SubTabs::Options{\n\t\t\t\t\t\t.selected = u\"my\"_q,\n\t\t\t\t\t\t.centered = true,\n\t\t\t\t\t},\n\t\t\t\t\tstd::move(tabs),\n\t\t\t\t\tcontext));\n\n\t\t\tstate->tabs->activated(\n\t\t\t) | rpl::on_next([=](const QString &id) {\n\t\t\t\tstate->tabs->setActiveTab(id);\n\t\t\t\tchosen(id.toULongLong());\n\t\t\t}, state->tabs->lifetime());\n\t\t} else {\n\t\t\tstate->tabs->setTabs(std::move(tabs), context);\n\t\t}\n\t\tcontainer->resizeToWidth(container->width());\n\t}, container->lifetime());\n\n\treturn {\n\t\t.switchToNext = [=]() {\n\t\t\tconst auto &list = state->list.current();\n\t\t\tif (!list.empty()) {\n\t\t\t\tif (state->tabs) {\n\t\t\t\t\tstate->tabs->setActiveTab(QString::number(list.front().id));\n\t\t\t\t}\n\t\t\t\tchosen(list.front().id);\n\t\t\t}\n\t\t},\n\t\t.tabs = state->tabs,\n\t};\n}\n\nvoid AddGiftSelector(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<uint64> showingGiftIdValue,\n\t\tFn<void(std::shared_ptr<Data::UniqueGift> selected)> chosen,\n\t\trpl::producer<uint64> selected,\n\t\tbool profile,\n\t\trpl::producer<uint64> selectedGiftId = rpl::single(uint64(0)),\n\t\tFn<void()> switchToNextTab = nullptr) {\n\tusing namespace Info::PeerGifts;\n\n\tconst auto raw = container->add(\n\t\tobject_ptr<Ui::VisibleRangeWidget>(container));\n\n\tstruct List {\n\t\tstd::vector<GiftTypeStars> list;\n\t\trpl::lifetime loading;\n\t\tQString offset;\n\t\tbool loaded = false;\n\t};\n\tstruct State {\n\t\tstd::optional<Delegate> delegate;\n\t\trpl::variable<uint64> showingGiftId;\n\t\tbase::flat_map<uint64, List> lists;\n\t\tList *current = nullptr;\n\t\tstd::vector<bool> validated;\n\t\tstd::vector<std::unique_ptr<GiftButton>> buttons;\n\t\trpl::variable<Ui::VisibleRange> visibleRange;\n\t\trpl::variable<uint64> selected;\n\t\trpl::variable<uint64> selectedGiftId;\n\t\tint perRow = 1;\n\t\tbase::unique_qptr<Ui::RpWidget> emptyPlaceholder;\n\n\t\tFn<void()> loadMore;\n\t\tFn<void()> resize;\n\t\tFn<void()> rebuild;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>();\n\tstate->delegate.emplace(session, GiftButtonMode::Full);\n\tstate->showingGiftId = std::move(showingGiftIdValue);\n\tstate->selected = std::move(selected);\n\tstate->selectedGiftId = std::move(selectedGiftId);\n\tconst auto shadow = st::defaultDropdownMenu.wrap.shadow;\n\tconst auto extend = shadow.extend;\n\tstate->loadMore = [=] {\n\t\tconst auto selfId = session->userPeerId();\n\t\tconst auto shownGiftId = state->showingGiftId.current();\n\t\tif (state->current->loaded || state->current->loading) {\n\t\t\treturn;\n\t\t} else if (shownGiftId) {\n\t\t\tstate->current->loading = Data::ResaleGiftsSlice(\n\t\t\t\tsession,\n\t\t\t\tshownGiftId,\n\t\t\t\t{},\n\t\t\t\tstate->current->offset\n\t\t\t) | rpl::on_next([=](Data::ResaleGiftsDescriptor slice) {\n\t\t\t\tauto &entry = state->lists[shownGiftId];\n\t\t\t\tentry.loading.destroy();\n\t\t\t\tentry.offset = slice.offset;\n\t\t\t\tentry.loaded = entry.offset.isEmpty();\n\t\t\t\tif (state->showingGiftId.current() != shownGiftId) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tauto &list = state->current->list;\n\t\t\t\tfor (const auto &gift : slice.list) {\n\t\t\t\t\tif (gift.unique && (profile || gift.unique->peerColor)) {\n\t\t\t\t\t\tlist.push_back({\n\t\t\t\t\t\t\t.info = gift,\n\t\t\t\t\t\t\t.resale = true,\n\t\t\t\t\t\t\t.mine = (gift.unique->ownerId == selfId),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstate->resize();\n\t\t\t});\n\t\t} else {\n\t\t\tstate->current->loading = Data::MyUniqueGiftsSlice(\n\t\t\t\tsession,\n\t\t\t\tData::MyUniqueType::OwnedAndHosted,\n\t\t\t\tstate->current->offset\n\t\t\t) | rpl::on_next([=](Data::MyGiftsDescriptor slice) {\n\t\t\t\tauto &entry = state->lists[shownGiftId];\n\t\t\t\tentry.loading.destroy();\n\t\t\t\tentry.offset = slice.offset;\n\t\t\t\tentry.loaded = entry.offset.isEmpty();\n\t\t\t\tif (state->showingGiftId.current() != shownGiftId) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tauto &list = state->current->list;\n\t\t\t\tfor (const auto &gift : slice.list) {\n\t\t\t\t\tif (gift.info.unique\n\t\t\t\t\t\t&& (profile || gift.info.unique->peerColor)) {\n\t\t\t\t\t\tlist.push_back({ .info = gift.info });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstate->resize();\n\t\t\t});\n\t\t}\n\t};\n\tstate->rebuild = [=] {\n\t\tconst auto shownGiftId = state->showingGiftId.current();\n\t\tconst auto width = st::boxWideWidth;\n\t\tconst auto padding = st::giftBoxPadding;\n\t\tconst auto available = width - padding.left() - padding.right();\n\t\tconst auto range = state->visibleRange.current();\n\t\tconst auto count = int(state->current->list.size());\n\n\t\tauto &buttons = state->buttons;\n\t\tif (buttons.size() < count) {\n\t\t\tbuttons.resize(count);\n\t\t}\n\t\tauto &validated = state->validated;\n\t\tvalidated.resize(count);\n\n\t\tauto x = padding.left();\n\t\tauto y = padding.top();\n\t\tconst auto single = state->delegate->buttonSize();\n\t\tconst auto perRow = state->perRow;\n\t\tconst auto singlew = single.width() + st::giftBoxGiftSkip.x();\n\t\tconst auto singleh = single.height() + st::giftBoxGiftSkip.y();\n\t\tconst auto rowFrom = std::max(range.top - y, 0) / singleh;\n\t\tconst auto rowTill = (std::max(range.bottom - y + st::giftBoxGiftSkip.y(), 0) + singleh - 1)\n\t\t\t/ singleh;\n\t\tAssert(rowTill >= rowFrom);\n\t\tconst auto first = rowFrom * perRow;\n\t\tconst auto last = std::min(rowTill * perRow, count);\n\t\tconst auto selectedCollectibleId = state->selected.current();\n\t\tconst auto selectedGiftId = state->selectedGiftId.current();\n\t\tauto checkedFrom = 0;\n\t\tauto checkedTill = int(buttons.size());\n\t\tconst auto ensureButton = [&](int index) {\n\t\t\tauto &button = buttons[index];\n\t\t\tif (!button) {\n\t\t\t\tvalidated[index] = false;\n\t\t\t\tfor (; checkedFrom != first; ++checkedFrom) {\n\t\t\t\t\tif (buttons[checkedFrom]) {\n\t\t\t\t\t\tbutton = std::move(buttons[checkedFrom]);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!button) {\n\t\t\t\tfor (; checkedTill != last; ) {\n\t\t\t\t\t--checkedTill;\n\t\t\t\t\tif (buttons[checkedTill]) {\n\t\t\t\t\t\tbutton = std::move(buttons[checkedTill]);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!button) {\n\t\t\t\tconst auto delegate = &*state->delegate;\n\t\t\t\tbutton = std::make_unique<GiftButton>(raw, delegate);\n\t\t\t}\n\t\t\tconst auto raw = button.get();\n\t\t\tif (validated[index]) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\traw->show();\n\t\t\tvalidated[index] = true;\n\t\t\tconst auto &gift = state->current->list[index];\n\t\t\traw->setDescriptor({ gift }, shownGiftId\n\t\t\t\t? GiftButtonMode::Full\n\t\t\t\t: GiftButtonMode::Minimal);\n\t\t\traw->setClickedCallback([=, unique = gift.info.unique] {\n\t\t\t\tchosen(unique);\n\t\t\t});\n\t\t\traw->setGeometry(QRect(QPoint(x, y), single), extend);\n\t\t\tconst auto isSelected = selectedCollectibleId\n\t\t\t\t? (gift.info.unique->id == selectedCollectibleId)\n\t\t\t\t: (gift.info.unique->id == selectedGiftId);\n\t\t\traw->toggleSelected(\n\t\t\t\tisSelected,\n\t\t\t\tGiftSelectionMode::Inset,\n\t\t\t\tanim::type::instant);\n\t\t};\n\t\ty += rowFrom * singleh;\n\t\tfor (auto row = rowFrom; row != rowTill; ++row) {\n\t\t\tfor (auto col = 0; col != perRow; ++col) {\n\t\t\t\tconst auto index = row * perRow + col;\n\t\t\t\tif (index >= count) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tconst auto last = !((col + 1) % perRow);\n\t\t\t\tif (last) {\n\t\t\t\t\tx = padding.left() + available - single.width();\n\t\t\t\t}\n\t\t\t\tensureButton(index);\n\t\t\t\tif (last) {\n\t\t\t\t\tx = padding.left();\n\t\t\t\t\ty += singleh;\n\t\t\t\t} else {\n\t\t\t\t\tx += singlew;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst auto till = std::min(int(buttons.size()), rowTill * perRow);\n\t\tfor (auto i = count; i < till; ++i) {\n\t\t\tif (const auto button = buttons[i].get()) {\n\t\t\t\tbutton->hide();\n\t\t\t}\n\t\t}\n\n\t\tconst auto find = [=](uint64 id) -> GiftButton* {\n\t\t\tif (!id) {\n\t\t\t\treturn nullptr;\n\t\t\t}\n\t\t\tconst auto count = int(state->current->list.size());\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tconst auto &gift = state->current->list[i];\n\t\t\t\tif (gift.info.unique->id == id) {\n\t\t\t\t\treturn state->buttons[i].get();\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nullptr;\n\t\t};\n\n\t\tstate->selected.value(\n\t\t) | rpl::combine_previous() | rpl::on_next([=](\n\t\t\t\tuint64 wasCollectibleId,\n\t\t\t\tuint64 nowCollectibleId) {\n\t\t\tif (wasCollectibleId) {\n\t\t\t\tif (const auto button = find(wasCollectibleId)) {\n\t\t\t\t\tbutton->toggleSelected(false, GiftSelectionMode::Inset);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (nowCollectibleId) {\n\t\t\t\tif (const auto button = find(nowCollectibleId)) {\n\t\t\t\t\tbutton->toggleSelected(true, GiftSelectionMode::Inset);\n\t\t\t\t}\n\t\t\t}\n\t\t}, raw->lifetime());\n\n\t\tstate->selectedGiftId.value(\n\t\t) | rpl::combine_previous() | rpl::on_next([=](\n\t\t\t\tuint64 wasGiftId,\n\t\t\t\tuint64 nowGiftId) {\n\t\t\tif (wasGiftId) {\n\t\t\t\tif (const auto button = find(wasGiftId)) {\n\t\t\t\t\tbutton->toggleSelected(false, GiftSelectionMode::Inset);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (nowGiftId) {\n\t\t\t\tif (const auto button = find(nowGiftId)) {\n\t\t\t\t\tbutton->toggleSelected(true, GiftSelectionMode::Inset);\n\t\t\t\t}\n\t\t\t}\n\t\t}, raw->lifetime());\n\n\t\tconst auto page = range.bottom - range.top;\n\t\tif (page > 0 && range.bottom + page > raw->height()) {\n\t\t\tstate->loadMore();\n\t\t}\n\t};\n\n\tconst auto width = st::boxWideWidth;\n\tconst auto padding = st::giftBoxPadding;\n\tconst auto available = width - padding.left() - padding.right();\n\tstate->perRow = available / state->delegate->buttonSize().width();\n\n\tstate->resize = [=] {\n\t\tconst auto count = int(state->current->list.size());\n\t\tstate->validated.clear();\n\n\t\tif (count == 0 && state->showingGiftId.current() == 0) {\n\t\t\tif (!state->emptyPlaceholder) {\n\t\t\t\tstate->emptyPlaceholder = CreateEmptyPlaceholder(\n\t\t\t\t\traw,\n\t\t\t\t\twidth,\n\t\t\t\t\tpadding,\n\t\t\t\t\tswitchToNextTab);\n\t\t\t}\n\t\t\tstate->emptyPlaceholder->show();\n\t\t\traw->resize(raw->width(), state->emptyPlaceholder->height());\n\t\t\treturn;\n\t\t} else if (state->emptyPlaceholder) {\n\t\t\tstate->emptyPlaceholder = nullptr;\n\t\t}\n\n\t\tconst auto rows = (count + state->perRow - 1) / state->perRow;\n\t\tconst auto height = padding.top()\n\t\t\t+ (rows * state->delegate->buttonSize().height())\n\t\t\t+ ((rows - 1) * st::giftBoxGiftSkip.y())\n\t\t\t+ padding.bottom();\n\t\traw->resize(raw->width(), height);\n\n\t\tstate->rebuild();\n\t};\n\n\tstate->showingGiftId.value(\n\t) | rpl::on_next([=](uint64 showingId) {\n\t\tstate->current = &state->lists[showingId];\n\t\tstate->buttons.clear();\n\t\tif (state->emptyPlaceholder) {\n\t\t\tstate->emptyPlaceholder = nullptr;\n\t\t}\n\t\tstate->delegate.emplace(session, showingId\n\t\t\t? GiftButtonMode::Full\n\t\t\t: GiftButtonMode::Minimal);\n\t\tstate->resize();\n\t}, raw->lifetime());\n\n\tstate->visibleRange = raw->visibleRange();\n\tstate->visibleRange.value(\n\t) | rpl::on_next(state->rebuild, raw->lifetime());\n}\n\nFn<void(int)> CreateTabsWidget(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst std::vector<QString> &labels,\n\t\tconst std::vector<Fn<void()>> &callbacks) {\n\tconst auto tabs = container->add(\n\t\tobject_ptr<Ui::PillTabs>(\n\t\t\tcontainer,\n\t\t\tlabels,\n\t\t\t0,\n\t\t\tst::giftBoxPillTabs),\n\t\tst::boxRowPadding,\n\t\tstyle::al_top);\n\n\ttabs->activeIndexChanges(\n\t) | rpl::on_next([=](int index) {\n\t\tif (index >= 0 && index < int(callbacks.size()) && callbacks[index]) {\n\t\t\tcallbacks[index]();\n\t\t}\n\t}, tabs->lifetime());\n\n\treturn [=](int index) {\n\t\ttabs->setActiveIndex(index);\n\t\tif (index >= 0 && index < int(callbacks.size()) && callbacks[index]) {\n\t\t\tcallbacks[index]();\n\t\t}\n\t};\n}\n\nnot_null<Info::Profile::TopBar*> CreateProfilePreview(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer) {\n\tconst auto preview = container->add(\n\t\tobject_ptr<Info::Profile::TopBar>(\n\t\t\tcontainer,\n\t\t\tInfo::Profile::TopBar::Descriptor {\n\t\t\t\t.controller = show->resolveWindow(),\n\t\t\t\t.key = Info::Key(peer),\n\t\t\t\t.wrap = rpl::single(Info::Wrap::Side),\n\t\t\t\t.source = Info::Profile::TopBar::Source::Preview,\n\t\t\t\t.peer = peer,\n\t\t\t\t.backToggles = rpl::single(false),\n\t\t\t\t.showFinished = box->showFinishes(),\n\t\t\t}\n\t\t));\n\tpreview->resize(\n\t\tcontainer->width(),\n\t\tst::infoProfileTopBarNoActionsHeightMax);\n\tpreview->setAttribute(Qt::WA_TransparentForMouseEvents);\n\treturn preview;\n}\n\nvoid ProcessButton(not_null<Ui::RoundButton*> button) {\n\t// Raise to be above right emoji from buttons.\n\tcrl::on_main(button, [=] { button->raise(); });\n}\n\nvoid CreateBoostLevelContainer(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tint levelHint,\n\t\trpl::producer<std::optional<QColor>> colorProducer,\n\t\tFn<void()> callback) {\n\tconst auto boostLevelContainer = container->add(\n\t\tobject_ptr<Ui::RpWidget>(container));\n\tboostLevelContainer->resize(\n\t\t0,\n\t\tst::infoProfileTopBarBoostFooter.style.font->height * 1.5);\n\n\tstruct State {\n\t\tbase::unique_qptr<Ui::FlatLabel> label;\n\t\tstd::optional<QColor> currentColor;\n\t};\n\tconst auto state = boostLevelContainer->lifetime().make_state<State>();\n\n\tboostLevelContainer->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(boostLevelContainer);\n\t\tconst auto bg = state->currentColor.value_or(st::boxDividerBg->c);\n\t\tp.fillRect(boostLevelContainer->rect(), bg);\n\t\tp.fillRect(boostLevelContainer->rect(), st::shadowFg);\n\t}, boostLevelContainer->lifetime());\n\n\tstd::move(colorProducer) | rpl::on_next([=](\n\t\t\tstd::optional<QColor> color) {\n\t\tconst auto colorChanged = (state->currentColor != color)\n\t\t\t|| !state->label;\n\t\tstate->currentColor = color;\n\t\tboostLevelContainer->update();\n\n\t\tif (colorChanged) {\n\t\t\tconst auto &style = color\n\t\t\t\t? st::infoProfileTopBarBoostFooterColored\n\t\t\t\t: st::infoProfileTopBarBoostFooter;\n\t\t\tstate->label = base::make_unique_q<Ui::FlatLabel>(\n\t\t\t\tboostLevelContainer,\n\t\t\t\ttr::lng_settings_color_group_boost_footer(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(levelHint) | tr::to_count(),\n\t\t\t\t\tlt_link,\n\t\t\t\t\ttr::lng_settings_color_group_boost_footer_link(\n\t\t\t\t\t) | rpl::map([=](QString t) {\n\t\t\t\t\t\tusing namespace Ui::Text;\n\t\t\t\t\t\treturn Link(std::move(t), u\"internal:\"_q);\n\t\t\t\t\t}),\n\t\t\t\t\ttr::rich),\n\t\t\t\tstyle);\n\t\t\tstate->label->show();\n\t\t\tboostLevelContainer->sizeValue(\n\t\t\t) | rpl::on_next([=](QSize s) {\n\t\t\t\tstate->label->moveToLeft(\n\t\t\t\t\t(s.width() - state->label->width()) / 2,\n\t\t\t\t\t(s.height() - state->label->height()) / 2);\n\t\t\t}, state->label->lifetime());\n\t\t\tstate->label->setClickHandlerFilter([=](auto...) {\n\t\t\t\tcallback();\n\t\t\t\treturn false;\n\t\t\t});\n\t\t}\n\t}, boostLevelContainer->lifetime());\n}\n\n} // namespace\n\nvoid AddLevelBadge(\n\t\tint level,\n\t\tnot_null<Ui::SettingsButton*> button,\n\t\tUi::RpWidget *right,\n\t\tnot_null<ChannelData*> channel,\n\t\tconst QMargins &padding,\n\t\trpl::producer<QString> text) {\n\tif (channel->levelHint() >= level) {\n\t\treturn;\n\t}\n\tconst auto badge = Ui::CreateChild<LevelBadge>(\n\t\tbutton.get(),\n\t\tlevel,\n\t\t&channel->session());\n\tbadge->show();\n\tconst auto sampleLeft = st::settingsColorSamplePadding.left();\n\tconst auto badgeLeft = padding.left() + sampleLeft;\n\trpl::combine(\n\t\tbutton->sizeValue(),\n\t\tstd::move(text)\n\t) | rpl::on_next([=](const QSize &s, const QString &) {\n\t\tif (s.isNull()) {\n\t\t\treturn;\n\t\t}\n\t\tbadge->moveToLeft(\n\t\t\tbutton->fullTextWidth() + badgeLeft,\n\t\t\t(s.height() - badge->height()) / 2);\n\t\tconst auto rightEdge = right ? right->pos().x() : button->width();\n\t\tbadge->setMinimal((rect::right(badge) + sampleLeft) > rightEdge);\n\t\tbadge->setVisible((rect::right(badge) + sampleLeft) < rightEdge);\n\t}, badge->lifetime());\n}\n\nstruct ColorSectionHighlights {\n\tQPointer<Ui::SettingsButton> emojiButton;\n\tQPointer<Ui::SettingsButton> resetButton;\n\tQPointer<Ui::SubTabs> giftTabs;\n};\n\nvoid EditPeerColorSection(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Ui::RoundButton*> button,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<Ui::ChatStyle> style,\n\t\tstd::shared_ptr<Ui::ChatTheme> theme,\n\t\tColorSectionHighlights *highlights) {\n\tProcessButton(button);\n\tconst auto group = peer->isMegagroup();\n\n\tstruct State {\n\t\trpl::variable<uint8> index;\n\t\trpl::variable<DocumentId> emojiId;\n\t\trpl::variable<EmojiStatusId> statusId;\n\t\trpl::variable<std::optional<Ui::ColorCollectible>> collectible;\n\t\trpl::variable<uint64> showingGiftId;\n\t\trpl::variable<uint8> profileIndex;\n\t\trpl::variable<DocumentId> profileEmojiId;\n\t\tstd::shared_ptr<Data::UniqueGift> buyCollectible;\n\t\tInfo::Profile::TopBar *preview = nullptr;\n\t\tTimeId statusUntil = 0;\n\t\tbool statusChanged = false;\n\t\tbool changing = false;\n\t\tbool applying = false;\n\t};\n\tconst auto state = button->lifetime().make_state<State>();\n\tstate->index = peer->colorCollectible()\n\t\t? kUnsetColorIndex\n\t\t: peer->colorIndex();\n\tstate->emojiId = peer->backgroundEmojiId();\n\tstate->statusId = peer->emojiStatusId();\n\tstate->collectible = peer->colorCollectible()\n\t\t? *peer->colorCollectible()\n\t\t: std::optional<Ui::ColorCollectible>();\n\tstate->profileIndex = peer->colorProfileIndex().value_or(\n\t\tkUnsetColorIndex);\n\tstate->profileEmojiId = peer->profileBackgroundEmojiId();\n\n\tconst auto appendProfileSettings = [=](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<ChannelData*> channel) {\n\t\tstate->preview = CreateProfilePreview(box, container, show, peer);\n\t\tif (state->profileIndex.current() != kUnsetColorIndex) {\n\t\t\tstate->preview->setColorProfileIndex(\n\t\t\t\tstate->profileIndex.current());\n\t\t}\n\t\tif (state->profileEmojiId.current()) {\n\t\t\tstate->preview->setPatternEmojiId(\n\t\t\t\tstate->profileEmojiId.current());\n\t\t}\n\t\tstate->statusId.value() | rpl::on_next([=](EmojiStatusId id) {\n\t\t\tstate->preview->setLocalEmojiStatusId(std::move(id));\n\t\t}, state->preview->lifetime());\n\t\tconst auto peerColors = &peer->session().api().peerColors();\n\t\tconst auto profileIndices = peerColors->profileColorIndices();\n\n\t\tif (group) {\n\t\t\tauto colorProducer = state->profileIndex.value(\n\t\t\t) | rpl::map([=, colors = &peer->session().api().peerColors()](\n\t\t\t\t\tuint8 index) {\n\t\t\t\tconst auto colorSet = colors->colorProfileFor(index);\n\t\t\t\treturn (colorSet && !colorSet->bg.empty())\n\t\t\t\t\t? std::make_optional(colorSet->bg.front())\n\t\t\t\t\t: std::optional<QColor>();\n\t\t\t});\n\t\t\tCreateBoostLevelContainer(\n\t\t\t\tcontainer,\n\t\t\t\tchannel->levelHint(),\n\t\t\t\tstd::move(colorProducer),\n\t\t\t\t[=] {\n\t\t\t\t\tif (const auto strong = show->resolveWindow()) {\n\t\t\t\t\t\tstrong->resolveBoostState(channel);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t}\n\n\t\tconst auto profileMargin = st::settingsColorRadioMargin;\n\t\tconst auto profileSkip = st::settingsColorRadioSkip;\n\t\tconst auto selector = container->add(\n\t\t\tobject_ptr<Ui::ColorSelector>(\n\t\t\t\tcontainer,\n\t\t\t\tprofileIndices,\n\t\t\t\tstate->profileIndex.current(),\n\t\t\t\t[=](uint8 index) {\n\t\t\t\t\tstate->profileIndex = index;\n\t\t\t\t\tif (state->preview) {\n\t\t\t\t\t\tstate->preview->setColorProfileIndex(\n\t\t\t\t\t\t\tindex == kUnsetColorIndex\n\t\t\t\t\t\t\t\t? std::nullopt\n\t\t\t\t\t\t\t\t: std::make_optional(index));\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t[=](uint8 index) {\n\t\t\t\t\treturn peerColors->colorProfileFor(index).value_or(\n\t\t\t\t\t\tData::ColorProfileSet{});\n\t\t\t\t}),\n\t\t\t{ profileMargin, profileSkip, profileMargin, profileSkip });\n\n\t\tcontainer->add(CreateEmojiIconButton(\n\t\t\tcontainer,\n\t\t\tshow,\n\t\t\tstyle,\n\t\t\tpeer,\n\t\t\tstate->profileIndex.value(),\n\t\t\tstate->profileEmojiId.value(),\n\t\t\t[=](DocumentId id) {\n\t\t\t\tstate->profileEmojiId = id;\n\t\t\t\tif (state->preview) {\n\t\t\t\t\tstate->preview->setPatternEmojiId(id);\n\t\t\t\t}\n\t\t\t},\n\t\t\ttrue));\n\n\t\tstate->profileIndex.value(\n\t\t) | rpl::on_next([=](uint8 index) {\n\t\t\tselector->updateSelection(index);\n\t\t}, selector->lifetime());\n\n\t\tUi::AddSkip(container, st::settingsColorSampleSkip);\n\n\t\tconst auto resetWrap = container->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\t\tconst auto resetInner = resetWrap->entity();\n\t\tconst auto resetButton = resetInner->add(\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tresetInner,\n\t\t\t\ttr::lng_settings_color_reset(),\n\t\t\t\tst::settingsButtonLightNoIcon));\n\t\tUi::AddSkip(resetInner, st::settingsColorSampleSkip);\n\t\tresetButton->setClickedCallback([=] {\n\t\t\tstate->profileIndex = kUnsetColorIndex;\n\t\t\tstate->profileEmojiId = 0;\n\t\t\tif (state->preview) {\n\t\t\t\tstate->preview->setColorProfileIndex(std::nullopt);\n\t\t\t\tstate->preview->setPatternEmojiId(0);\n\t\t\t}\n\t\t\tresetWrap->toggle(false, anim::type::normal);\n\t\t});\n\n\t\tresetWrap->toggleOn(state->profileIndex.value(\n\t\t) | rpl::map(rpl::mappers::_1 != kUnsetColorIndex));\n\t\tresetWrap->finishAnimating();\n\n\t\tUi::AddDividerText(\n\t\t\tcontainer,\n\t\t\tgroup\n\t\t\t\t? tr::lng_settings_color_choose_group()\n\t\t\t\t: tr::lng_settings_color_choose_channel());\n\t\tUi::AddSkip(container, st::settingsColorSampleSkip);\n\n\t\tcontainer->add(CreateEmojiStatusButton(\n\t\t\tcontainer,\n\t\t\tshow,\n\t\t\tchannel,\n\t\t\tstate->statusId.value(),\n\t\t\t[=](EmojiStatusId id, TimeId until) {\n\t\t\t\tstate->statusId = id;\n\t\t\t\tstate->statusUntil = until;\n\t\t\t\tstate->statusChanged = true;\n\t\t\t},\n\t\t\tgroup));\n\t\tUi::AddSkip(container, st::settingsColorSampleSkip);\n\t\tUi::AddDividerText(\n\t\t\tcontainer,\n\t\t\t(group\n\t\t\t\t? tr::lng_edit_channel_status_about_group()\n\t\t\t\t: tr::lng_edit_channel_status_about()),\n\t\t\tst::peerAppearanceDividerTextMargin);\n\t};\n\n\tif (group) {\n\t\tappendProfileSettings(container, peer->asChannel());\n\t} else {\n\t\tcontainer->add(object_ptr<PreviewWrap>(\n\t\t\tbox,\n\t\t\tstyle,\n\t\t\ttheme,\n\t\t\tpeer,\n\t\t\tstate->index.value(),\n\t\t\tstate->emojiId.value(),\n\t\t\tstate->collectible.value()));\n\n\t\tauto indices = peer->session().api().peerColors().suggestedValue();\n\t\tconst auto margin = st::settingsColorRadioMargin;\n\t\tconst auto skip = st::settingsColorRadioSkip;\n\t\tcontainer->add(\n\t\t\tobject_ptr<Ui::ColorSelector>(\n\t\t\t\tcontainer,\n\t\t\t\tstyle,\n\t\t\t\tstd::move(indices),\n\t\t\t\tstate->index.value(),\n\t\t\t\t[=](uint8 index) {\n\t\t\t\t\tif (state->collectible.current()) {\n\t\t\t\t\t\tstate->buyCollectible = nullptr;\n\t\t\t\t\t\tstate->collectible = std::nullopt;\n\t\t\t\t\t\tstate->emojiId = 0;\n\t\t\t\t\t}\n\t\t\t\t\tstate->index = index;\n\t\t\t\t}),\n\t\t\t{ margin, skip, margin, skip });\n\n\t\tUi::AddDividerText(\n\t\t\tcontainer,\n\t\t\t(peer->isSelf()\n\t\t\t\t? tr::lng_settings_color_about()\n\t\t\t\t: tr::lng_settings_color_about_channel()),\n\t\t\tst::peerAppearanceDividerTextMargin);\n\n\t\tconst auto iconWrap = container->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\t\tconst auto iconInner = iconWrap->entity();\n\n\t\tUi::AddSkip(iconInner, st::settingsColorSampleSkip);\n\t\tconst auto emojiButton = iconInner->add(CreateEmojiIconButton(\n\t\t\ticonInner,\n\t\t\tshow,\n\t\t\tstyle,\n\t\t\tpeer,\n\t\t\tstate->index.value(),\n\t\t\tstate->emojiId.value(),\n\t\t\t[=](DocumentId id) { state->emojiId = id; },\n\t\t\tfalse));\n\t\tif (highlights) {\n\t\t\thighlights->emojiButton = emojiButton;\n\t\t}\n\n\t\tUi::AddSkip(iconInner, st::settingsColorSampleSkip);\n\t\tUi::AddDividerText(\n\t\t\ticonInner,\n\t\t\t(peer->isSelf()\n\t\t\t\t? tr::lng_settings_color_emoji_about()\n\t\t\t\t: tr::lng_settings_color_emoji_about_channel()),\n\t\t\tst::peerAppearanceDividerTextMargin);\n\n\t\ticonWrap->toggleOn(state->collectible.value(\n\t\t) | rpl::map([](const std::optional<Ui::ColorCollectible> &value) {\n\t\t\treturn !value.has_value();\n\t\t}));\n\t\ticonWrap->finishAnimating();\n\t}\n\n\tif (const auto channel = peer->asChannel()) {\n\t\tUi::AddSkip(container, st::settingsColorSampleSkip);\n\t\tconst auto &phrase = group\n\t\t\t? tr::lng_edit_channel_wallpaper_group\n\t\t\t: tr::lng_edit_channel_wallpaper;\n\t\tconst auto button = Settings::AddButtonWithIcon(\n\t\t\tcontainer,\n\t\t\tphrase(),\n\t\t\tst::peerAppearanceButton,\n\t\t\t{ &st::menuBlueIconWallpaper }\n\t\t);\n\t\tbutton->setClickedCallback([=] {\n\t\t\tif (const auto strong = show->resolveWindow()) {\n\t\t\t\tshow->show(Box<BackgroundBox>(strong, channel));\n\t\t\t}\n\t\t});\n\n\t\t{\n\t\t\tconst auto limits = Data::LevelLimits(&channel->session());\n\t\t\tAddLevelBadge(\n\t\t\t\tgroup\n\t\t\t\t\t? limits.groupCustomWallpaperLevelMin()\n\t\t\t\t\t: limits.channelCustomWallpaperLevelMin(),\n\t\t\t\tbutton,\n\t\t\t\tnullptr,\n\t\t\t\tchannel,\n\t\t\t\tst::peerAppearanceButton.padding,\n\t\t\t\tphrase());\n\t\t}\n\n\t\tUi::AddSkip(container, st::settingsColorSampleSkip);\n\t\tUi::AddDividerText(\n\t\t\tcontainer,\n\t\t\t(group\n\t\t\t\t? tr::lng_edit_channel_wallpaper_about_group()\n\t\t\t\t: tr::lng_edit_channel_wallpaper_about()),\n\t\t\tst::peerAppearanceDividerTextMargin);\n\n\t\tif (group) {\n\t\t\tUi::AddSkip(container, st::settingsColorSampleSkip);\n\n\t\t\tcontainer->add(CreateEmojiPackButton(\n\t\t\t\tcontainer,\n\t\t\t\tshow,\n\t\t\t\tchannel));\n\n\t\t\tUi::AddSkip(container, st::settingsColorSampleSkip);\n\t\t\tUi::AddDividerText(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_group_emoji_description(),\n\t\t\t\tst::peerAppearanceDividerTextMargin);\n\t\t}\n\n\t\t// Preload exceptions list.\n\t\tconst auto peerPhoto = &channel->session().api().peerPhoto();\n\t\t[[maybe_unused]] auto list = peerPhoto->emojiListValue(\n\t\t\tApi::PeerPhoto::EmojiListType::NoChannelStatus\n\t\t);\n\n\t\tconst auto statuses = &channel->owner().emojiStatuses();\n\t\tstatuses->refreshChannelDefault();\n\t\tstatuses->refreshChannelColored();\n\n\t\tif (!state->preview) {\n\t\t\tUi::AddSkip(container);\n\t\t\tappendProfileSettings(container, channel);\n\t\t}\n\t} else if (peer->isSelf()) {\n\t\tUi::AddSkip(container, st::settingsColorSampleSkip);\n\n\t\tconst auto session = &peer->session();\n\t\tconst auto giftTabs = AddColorGiftTabs(\n\t\t\tcontainer,\n\t\t\tsession,\n\t\t\t[=](uint64 giftId) { state->showingGiftId = giftId; },\n\t\t\tfalse);\n\t\tif (highlights) {\n\t\t\thighlights->giftTabs = giftTabs.tabs;\n\t\t}\n\n\t\tauto showingGiftId = state->showingGiftId.value();\n\t\tAddGiftSelector(\n\t\t\tcontainer,\n\t\t\tsession,\n\t\t\tstd::move(showingGiftId),\n\t\t\t[=](std::shared_ptr<Data::UniqueGift> selected) {\n\t\t\t\tstate->index = selected->peerColor ? kUnsetColorIndex : 0;\n\t\t\t\tstate->emojiId = selected->peerColor\n\t\t\t\t\t? selected->peerColor->backgroundEmojiId\n\t\t\t\t\t: 0;\n\t\t\t\tstate->buyCollectible = (selected->peerColor\n\t\t\t\t\t&& (selected->ownerId != session->userPeerId())\n\t\t\t\t\t&& selected->starsForResale > 0)\n\t\t\t\t\t? selected\n\t\t\t\t\t: nullptr;\n\t\t\t\tstate->collectible = selected->peerColor\n\t\t\t\t\t? *selected->peerColor\n\t\t\t\t\t: std::optional<Ui::ColorCollectible>();\n\t\t\t},\n\t\t\tstate->collectible.value() | rpl::map([](\n\t\t\t\t\tconst std::optional<Ui::ColorCollectible> &value) {\n\t\t\t\treturn value ? value->collectibleId : 0;\n\t\t\t}),\n\t\t\tfalse,\n\t\t\trpl::single(uint64(0)),\n\t\t\tgiftTabs.switchToNext);\n\t}\n\n\tbutton->setClickedCallback([=] {\n\t\tif (state->applying) {\n\t\t\treturn;\n\t\t} else if (ShowPremiumPreview(show, peer)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto values = SetValues{\n\t\t\tstate->index.current(),\n\t\t\tstate->emojiId.current(),\n\t\t\tstate->collectible.current(),\n\t\t\tstate->statusId.current(),\n\t\t\tstate->statusUntil,\n\t\t\tstate->statusChanged,\n\t\t};\n\t\tconst auto profileValues = SetValues{\n\t\t\t.colorIndex = state->profileIndex.current(),\n\t\t\t.backgroundEmojiId = state->profileEmojiId.current(),\n\t\t\t.colorCollectible = state->collectible.current(),\n\t\t\t.statusId = {},\n\t\t\t.statusUntil = 0,\n\t\t\t.statusChanged = false,\n\t\t\t.forProfile = true,\n\t\t};\n\t\tif (const auto buy = state->buyCollectible) {\n\t\t\tconst auto done = [=, weak = base::make_weak(box)](bool ok) {\n\t\t\t\tif (ok) {\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t\tApply(show, peer, values, [] {}, [] {});\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto to = peer->session().user();\n\t\t\tShowBuyResaleGiftBox(show, buy, false, to, done);\n\t\t\treturn;\n\t\t}\n\t\tstate->applying = true;\n\t\tif (peer->isChannel()) {\n\t\t\t// First request: regular color data (without toast)\n\t\t\tApply(show, peer, values, [=] {\n\t\t\t\t// Second request: profile color data (with toast)\n\t\t\t\tApply(show, peer, profileValues, crl::guard(box, [=] {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t}), crl::guard(box, [=] {\n\t\t\t\t\tstate->applying = false;\n\t\t\t\t}), true);\n\t\t\t}, crl::guard(box, [=] {\n\t\t\t\tstate->applying = false;\n\t\t\t}), false);\n\t\t\treturn;\n\t\t}\n\t\tApply(show, peer, values, crl::guard(box, [=] {\n\t\t\tbox->closeBox();\n\t\t}), crl::guard(box, [=] {\n\t\t\tstate->applying = false;\n\t\t}));\n\t});\n\tstate->collectible.value(\n\t) | rpl::on_next([=] {\n\t\tconst auto buy = state->buyCollectible.get();\n\t\twhile (!button->children().isEmpty()) {\n\t\t\tdelete button->children().first();\n\t\t}\n\t\tif (!buy) {\n\t\t\tbutton->setText(rpl::combine(\n\t\t\t\ttr::lng_settings_color_apply(),\n\t\t\t\tData::AmPremiumValue(&peer->session())\n\t\t\t) | rpl::map([=](const QString &text, bool premium) {\n\t\t\t\tauto result = TextWithEntities();\n\t\t\t\tif (!premium && peer->isSelf()) {\n\t\t\t\t\tresult.append(Ui::Text::IconEmoji(&st::giftBoxLock));\n\t\t\t\t}\n\t\t\t\tresult.append(text);\n\t\t\t\treturn result;\n\t\t\t}));\n\t\t} else if (buy->onlyAcceptTon) {\n\t\t\tbutton->setText(rpl::single(QString()));\n\t\t\tUi::SetButtonTwoLabels(\n\t\t\t\tbutton,\n\t\t\t\ttr::lng_gift_buy_resale_button(\n\t\t\t\t\tlt_cost,\n\t\t\t\t\trpl::single(Data::FormatGiftResaleTon(*buy)),\n\t\t\t\t\ttr::marked),\n\t\t\t\ttr::lng_gift_buy_resale_equals(\n\t\t\t\t\tlt_cost,\n\t\t\t\t\trpl::single(Ui::Text::IconEmoji(\n\t\t\t\t\t\t&st::starIconEmojiSmall\n\t\t\t\t\t).append(Lang::FormatCountDecimal(buy->starsForResale))),\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::resaleButtonTitle,\n\t\t\t\tst::resaleButtonSubtitle);\n\t\t} else {\n\t\t\tbutton->setText(tr::lng_gift_buy_resale_button(\n\t\t\t\tlt_cost,\n\t\t\t\trpl::single(Ui::Text::IconEmoji(&st::starIconEmoji).append(\n\t\t\t\t\tLang::FormatCountDecimal(buy->starsForResale))),\n\t\t\t\ttr::marked));\n\t\t}\n\t}, button->lifetime());\n}\n\nvoid EditPeerProfileColorSection(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Ui::RoundButton*> button,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<Ui::ChatStyle> style,\n\t\tstd::shared_ptr<Ui::ChatTheme> theme,\n\t\tFn<void()> aboutCallback,\n\t\tColorSectionHighlights *highlights) {\n\tExpects(peer->isSelf());\n\n\tProcessButton(button);\n\n\tconst auto preview = CreateProfilePreview(box, container, show, peer);\n\n\tconst auto peerColors = &peer->session().api().peerColors();\n\tconst auto indices = peerColors->profileColorIndices();\n\n\tstruct State {\n\t\trpl::variable<uint8> index = kUnsetColorIndex;\n\t\trpl::variable<DocumentId> patternEmojiId;\n\t\trpl::variable<EmojiStatusId> wearable;\n\t\trpl::variable<uint64> showingGiftId;\n\t\trpl::variable<uint64> selectedGiftId;\n\t\tstd::shared_ptr<Data::UniqueGift> buyCollectible;\n\t\tUi::ColorSelector *selector = nullptr;\n\t};\n\tconst auto state = button->lifetime().make_state<State>();\n\tstate->patternEmojiId = peer->profileBackgroundEmojiId();\n\tstate->wearable = peer->emojiStatusId();\n\n\tconst auto resetUnique = [=] {\n\t\tpreview->setLocalEmojiStatusId({});\n\t\tstate->buyCollectible = nullptr;\n\t\tstate->wearable = {};\n\t};\n\n\tconst auto setIndex = [=](uint8 index) {\n\t\tstate->index = index;\n\t\tif (index != kUnsetColorIndex) {\n\t\t\tresetUnique();\n\t\t}\n\t\tpreview->setColorProfileIndex(index == kUnsetColorIndex\n\t\t\t? std::nullopt\n\t\t\t: std::make_optional(index));\n\t\tpreview->setPatternEmojiId(index == kUnsetColorIndex\n\t\t\t? std::nullopt\n\t\t\t: std::make_optional(state->patternEmojiId.current()));\n\t};\n\tsetIndex(peer->emojiStatusId().collectible\n\t\t? kUnsetColorIndex\n\t\t: peer->colorProfileIndex().value_or(kUnsetColorIndex));\n\n\tconst auto margin = st::settingsColorRadioMargin;\n\tconst auto skip = st::settingsColorRadioSkip;\n\tstate->selector = container->add(\n\t\tobject_ptr<Ui::ColorSelector>(\n\t\t\tbox,\n\t\t\tindices,\n\t\t\tstate->index.current(),\n\t\t\tsetIndex,\n\t\t\t[=](uint8 index) {\n\t\t\t\treturn peerColors->colorProfileFor(index).value_or(\n\t\t\t\t\tData::ColorProfileSet{});\n\t\t\t}),\n\t\t{ margin, skip, margin, skip });\n\n\tUi::AddSkip(container, st::settingsColorSampleSkip);\n\tconst auto emojiButton = container->add(CreateEmojiIconButton(\n\t\tcontainer,\n\t\tshow,\n\t\tstyle,\n\t\tpeer,\n\t\tstate->index.value(),\n\t\tstate->patternEmojiId.value(),\n\t\t[=](DocumentId id) {\n\t\t\tstate->patternEmojiId = id;\n\t\t\tpreview->setPatternEmojiId(id);\n\t\t\tresetUnique();\n\t\t},\n\t\ttrue));\n\tif (highlights) {\n\t\thighlights->emojiButton = emojiButton;\n\t}\n\n\tconst auto resetWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto resetInner = resetWrap->entity();\n\n\tUi::AddSkip(resetInner, st::settingsColorSampleSkip);\n\tconst auto resetButton = resetInner->add(\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\tresetInner,\n\t\t\ttr::lng_settings_color_reset(),\n\t\t\tst::settingsButtonLightNoIcon));\n\tif (highlights) {\n\t\thighlights->resetButton = resetButton;\n\t}\n\tresetButton->setClickedCallback([=] {\n\t\tstate->index = kUnsetColorIndex;\n\t\tstate->patternEmojiId = 0;\n\t\tpreview->setColorProfileIndex(std::nullopt);\n\t\tpreview->setPatternEmojiId(0);\n\t\tresetUnique();\n\t\tresetWrap->toggle(false, anim::type::normal);\n\t});\n\n\tresetWrap->toggleOn(state->index.value(\n\t) | rpl::map([](uint8 index) { return index != kUnsetColorIndex; }));\n\tresetWrap->finishAnimating();\n\n\tUi::AddSkip(container, st::settingsColorSampleSkip);\n\tconst auto about = Ui::AddDividerText(\n\t\tcontainer,\n\t\ttr::lng_settings_color_profile_about(\n\t\t\tlt_link,\n\t\t\ttr::lng_settings_color_profile_about_link(\n\t\t\t\tlt_emoji,\n\t\t\t\trpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\t\ttr::rich\n\t\t\t) | rpl::map([=](TextWithEntities t) {\n\t\t\t\treturn tr::link(std::move(t), u\"internal:\"_q);\n\t\t\t}),\n\t\t\ttr::rich));\n\tUi::AddSkip(container, st::settingsColorSampleSkip);\n\tabout->setClickHandlerFilter([=](auto...) {\n\t\taboutCallback();\n\t\treturn false;\n\t});\n\n\tstate->index.value(\n\t) | rpl::on_next([=](uint8 index) {\n\t\tif (state->selector) {\n\t\t\tstate->selector->updateSelection(index);\n\t\t}\n\t}, button->lifetime());\n\n\tif (peer->isSelf()) {\n\t\tUi::AddSkip(container, st::settingsColorSampleSkip);\n\n\t\tconst auto session = &peer->session();\n\t\tconst auto giftTabs = AddColorGiftTabs(\n\t\t\tcontainer,\n\t\t\tsession,\n\t\t\t[=](uint64 giftId) { state->showingGiftId = giftId; },\n\t\t\ttrue);\n\t\tif (highlights) {\n\t\t\thighlights->giftTabs = giftTabs.tabs;\n\t\t}\n\n\t\tauto showingGiftId = state->showingGiftId.value();\n\t\tAddGiftSelector(\n\t\t\tcontainer,\n\t\t\tsession,\n\t\t\tstd::move(showingGiftId),\n\t\t\t[=](std::shared_ptr<Data::UniqueGift> selected) {\n\t\t\t\tstate->selectedGiftId = selected->id;\n\t\t\t\tstate->index = kUnsetColorIndex;\n\t\t\t\tstate->patternEmojiId = 0;\n\t\t\t\tstate->buyCollectible = (selected->peerColor\n\t\t\t\t\t&& (selected->ownerId != session->userPeerId())\n\t\t\t\t\t&& selected->starsForResale > 0)\n\t\t\t\t\t? selected\n\t\t\t\t\t: nullptr;\n\t\t\t\tconst auto statuses = &peer->owner().emojiStatuses();\n\t\t\t\tstate->wearable = statuses->fromUniqueGift(*selected);\n\t\t\t\tpreview->setColorProfileIndex(std::nullopt);\n\t\t\t\tpreview->setPatternEmojiId(selected->pattern.document->id);\n\t\t\t\tpreview->setLocalEmojiStatusId(state->wearable.current());\n\t\t\t\tresetWrap->toggle(true, anim::type::normal);\n\t\t\t},\n\t\t\tstate->wearable.value() | rpl::map([=](const EmojiStatusId &value) {\n\t\t\t\treturn value.collectible ? value.collectible->id : 0;\n\t\t\t}),\n\t\t\ttrue,\n\t\t\tstate->selectedGiftId.value(),\n\t\t\tgiftTabs.switchToNext);\n\t}\n\n\tstruct ProfileState {\n\t\tbool applying = false;\n\t};\n\tconst auto profileState = button->lifetime().make_state<ProfileState>();\n\n\tbutton->setClickedCallback([=] {\n\t\tif (profileState->applying) {\n\t\t\treturn;\n\t\t} else if (ShowPremiumPreview(show, peer)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto statusId = peer->emojiStatusId();\n\t\tconst auto wearable = state->wearable.current();\n\t\tconst auto statusChanged = wearable.collectible\n\t\t\t? (!statusId.collectible\n\t\t\t\t|| statusId.collectible->id != wearable.collectible->id)\n\t\t\t: (statusId.collectible != nullptr);\n\t\tconst auto values = SetValues{\n\t\t\t.colorIndex = state->index.current(),\n\t\t\t.backgroundEmojiId = state->patternEmojiId.current(),\n\t\t\t.colorCollectible = std::nullopt,\n\t\t\t.statusId = state->wearable.current(),\n\t\t\t.statusUntil = 0,\n\t\t\t.statusChanged = statusChanged,\n\t\t\t.forProfile = true,\n\t\t};\n\t\tif (const auto buy = state->buyCollectible) {\n\t\t\tconst auto done = [=, weak = base::make_weak(box)](bool ok) {\n\t\t\t\tif (ok) {\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t\tApply(show, peer, values, [] {}, [] {});\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto to = peer->session().user();\n\t\t\tShowBuyResaleGiftBox(show, buy, false, to, done);\n\t\t\treturn;\n\t\t}\n\t\tprofileState->applying = true;\n\t\tApply(show, peer, values, crl::guard(box, [=] {\n\t\t\tbox->closeBox();\n\t\t}), crl::guard(box, [=] {\n\t\t\tprofileState->applying = false;\n\t\t}));\n\t});\n\tstate->wearable.value(\n\t) | rpl::on_next([=](EmojiStatusId id) {\n\t\tconst auto buy = state->buyCollectible.get();\n\t\twhile (!button->children().isEmpty()) {\n\t\t\tdelete button->children().first();\n\t\t}\n\t\tif (!buy) {\n\t\t\tbutton->setText(rpl::combine(\n\t\t\t\t(id.collectible\n\t\t\t\t\t? tr::lng_settings_color_wear()\n\t\t\t\t\t: tr::lng_settings_color_apply()),\n\t\t\t\tData::AmPremiumValue(&peer->session())\n\t\t\t) | rpl::map([=](const QString &text, bool premium) {\n\t\t\t\tauto result = TextWithEntities();\n\t\t\t\tif (!premium && peer->isSelf()) {\n\t\t\t\t\tresult.append(Ui::Text::IconEmoji(&st::giftBoxLock));\n\t\t\t\t}\n\t\t\t\tresult.append(text);\n\t\t\t\treturn result;\n\t\t\t}));\n\t\t} else if (buy->onlyAcceptTon) {\n\t\t\tbutton->setText(rpl::single(QString()));\n\t\t\tUi::SetButtonTwoLabels(\n\t\t\t\tbutton,\n\t\t\t\ttr::lng_gift_buy_resale_button(\n\t\t\t\t\tlt_cost,\n\t\t\t\t\trpl::single(Data::FormatGiftResaleTon(*buy)),\n\t\t\t\t\ttr::marked),\n\t\t\t\ttr::lng_gift_buy_resale_equals(\n\t\t\t\t\tlt_cost,\n\t\t\t\t\trpl::single(Ui::Text::IconEmoji(\n\t\t\t\t\t\t&st::starIconEmojiSmall\n\t\t\t\t\t).append(Lang::FormatCountDecimal(buy->starsForResale))),\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::resaleButtonTitle,\n\t\t\t\tst::resaleButtonSubtitle);\n\t\t} else {\n\t\t\tbutton->setText(tr::lng_gift_buy_resale_button(\n\t\t\t\tlt_cost,\n\t\t\t\trpl::single(Ui::Text::IconEmoji(&st::starIconEmoji).append(\n\t\t\t\t\tLang::FormatCountDecimal(buy->starsForResale))),\n\t\t\t\ttr::marked));\n\t\t}\n\t}, button->lifetime());\n}\n\nvoid EditPeerColorBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<Ui::ChatStyle> style,\n\t\tstd::shared_ptr<Ui::ChatTheme> theme,\n\t\tPeerColorTab initialTab) {\n\tconst auto show = controller->uiShow();\n\tif (!style) {\n\t\tstyle = std::make_shared<Ui::ChatStyle>(\n\t\t\tpeer->session().colorIndicesValue());\n\t}\n\tif (!theme) {\n\t\ttheme = std::shared_ptr<Ui::ChatTheme>(\n\t\t\tWindow::Theme::DefaultChatThemeOn(box->lifetime()));\n\t\tstyle->apply(theme.get());\n\t}\n\tbox->setTitle(peer->isSelf()\n\t\t? tr::lng_settings_color_title()\n\t\t: tr::lng_edit_channel_color());\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setStyle(st::giftBox);\n\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\tbox->closeBox();\n\t});\n\tif (peer->isChannel()) {\n\t\tconst auto button = box->addButton(\n\t\t\ttr::lng_settings_color_apply(),\n\t\t\t[] {});\n\t\tEditPeerColorSection(box, box->verticalLayout(), button, show, peer, style, theme, nullptr);\n\t\treturn;\n\t}\n\tconst auto buttonContainer = box->addButton(\n\t\trpl::single(QString()),\n\t\t[] {});\n\tconst auto content = box->verticalLayout();\n\n\tconst auto profileButton = Ui::CreateChild<Ui::RoundButton>(\n\t\tbuttonContainer,\n\t\ttr::lng_settings_color_apply(),\n\t\tbox->getDelegate()->style().button);\n\tprofileButton->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\tconst auto nameButton = Ui::CreateChild<Ui::RoundButton>(\n\t\tbuttonContainer,\n\t\ttr::lng_settings_color_apply(),\n\t\tbox->getDelegate()->style().button);\n\tnameButton->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\trpl::combine(\n\t\tbuttonContainer->widthValue(),\n\t\tprofileButton->sizeValue(),\n\t\tnameButton->sizeValue()\n\t) | rpl::on_next([=](int w, QSize, QSize) {\n\t\tprofileButton->resizeToWidth(w);\n\t\tnameButton->resizeToWidth(w);\n\t}, buttonContainer->lifetime());\n\n\tauto nameOwned = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\tcontent,\n\t\tobject_ptr<Ui::VerticalLayout>(content));\n\tauto profileOwned = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\tcontent,\n\t\tobject_ptr<Ui::VerticalLayout>(content));\n\tconst auto nameWrap = nameOwned.get();\n\tconst auto profileWrap = profileOwned.get();\n\tconst auto name = nameWrap->entity();\n\tconst auto profile = profileWrap->entity();\n\n\tconst auto showName = [=] {\n\t\tnameWrap->toggle(true, anim::type::instant);\n\t\tprofileWrap->toggle(false, anim::type::instant);\n\t\tprofileButton->hide();\n\t\tnameButton->show();\n\t};\n\n\tconst auto switchTab = CreateTabsWidget(\n\t\tcontent,\n\t\t{\n\t\t\ttr::lng_settings_color_tab_profile(tr::now),\n\t\t\ttr::lng_settings_color_tab_name(tr::now),\n\t\t},\n\t\t{\n\t\t\t[=] {\n\t\t\t\tnameWrap->toggle(false, anim::type::instant);\n\t\t\t\tprofileWrap->toggle(true, anim::type::instant);\n\t\t\t\tnameButton->hide();\n\t\t\t\tprofileButton->show();\n\t\t\t},\n\t\t\tshowName,\n\t\t});\n\n\tUi::AddSkip(content);\n\tnameWrap->toggle(false, anim::type::instant);\n\tprofileWrap->toggle(true, anim::type::instant);\n\tnameButton->hide();\n\tcontent->add(std::move(profileOwned));\n\tcontent->add(std::move(nameOwned));\n\n\tstruct HighlightState {\n\t\tColorSectionHighlights profile;\n\t\tColorSectionHighlights name;\n\t};\n\tconst auto highlightState = box->lifetime().make_state<HighlightState>();\n\n\tEditPeerProfileColorSection(\n\t\tbox,\n\t\tprofile,\n\t\tprofileButton,\n\t\tshow,\n\t\tpeer,\n\t\tstyle,\n\t\ttheme,\n\t\t[=] { switchTab(1); },\n\t\t&highlightState->profile);\n\n\tEditPeerColorSection(\n\t\tbox,\n\t\tname,\n\t\tnameButton,\n\t\tshow,\n\t\tpeer,\n\t\tstyle,\n\t\ttheme,\n\t\t&highlightState->name);\n\n\tif (initialTab == PeerColorTab::Name) {\n\t\tswitchTab(1);\n\t}\n\n\tbox->setShowFinishedCallback([=] {\n\t\tconst auto isProfileTab = (initialTab == PeerColorTab::Profile);\n\t\tconst auto &highlights = isProfileTab\n\t\t\t? highlightState->profile\n\t\t\t: highlightState->name;\n\t\tcontroller->checkHighlightControl(\n\t\t\tu\"profile-color/add-icons\"_q,\n\t\t\thighlights.emojiButton.data());\n\t\tcontroller->checkHighlightControl(\n\t\t\tu\"profile-color/use-gift\"_q,\n\t\t\thighlights.giftTabs.data());\n\t\tcontroller->checkHighlightControl(\n\t\t\tu\"profile-color/reset\"_q,\n\t\t\thighlights.resetButton.data());\n\t});\n}\n\nvoid SetupPeerColorSample(\n\t\tnot_null<Button*> button,\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<QString> label,\n\t\tstd::shared_ptr<Ui::ChatStyle> style) {\n\tauto colorIndexValue = peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::Color\n\t) | rpl::map([=] {\n\t\treturn peer->colorIndex();\n\t});\n\tauto colorCollectibleValue = peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::Color\n\t) | rpl::map([=] {\n\t\treturn peer->colorCollectible();\n\t});\n\tauto colorProfileIndexValue = peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::ColorProfile\n\t) | rpl::map([=] {\n\t\treturn peer->colorProfileIndex();\n\t});\n\tauto emojiStatusIdValue = peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::EmojiStatus\n\t) | rpl::map([=] {\n\t\treturn peer->emojiStatusId();\n\t});\n\tauto name = peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::Name\n\t) | rpl::map([=] { return peer->shortName(); });\n\n\tconst auto sampleSize = st::settingsColorSampleSize;\n\n\tconst auto sample = Ui::CreateChild<Ui::ColorSample>(\n\t\tbutton.get(),\n\t\t[=] { return Core::TextContext({ .session = &peer->session() }); },\n\t\t[=](auto id) { return Data::SingleCustomEmoji(id); },\n\t\tstyle,\n\t\trpl::duplicate(colorIndexValue),\n\t\trpl::duplicate(colorCollectibleValue),\n\t\trpl::duplicate(name));\n\tsample->show();\n\n\tstruct ProfileSampleState {\n\t\tData::ColorProfileSet colorSet;\n\t};\n\tconst auto profileState\n\t\t= button->lifetime().make_state<ProfileSampleState>();\n\n\tconst auto profileSample = Ui::CreateChild<Ui::ColorSample>(\n\t\tbutton.get(),\n\t\t[=](uint8 index) { return profileState->colorSet; },\n\t\t0,\n\t\tfalse);\n\tprofileSample->hide();\n\tprofileSample->resize(sampleSize, sampleSize);\n\n\tconst auto emojiStatusWidget = Ui::CreateChild<Ui::RpWidget>(\n\t\tbutton.get());\n\temojiStatusWidget->hide();\n\temojiStatusWidget->resize(sampleSize, sampleSize);\n\tbutton->lifetime().make_state<std::unique_ptr<Ui::Text::CustomEmoji>>();\n\n\tstruct EmojiStatusState {\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji> emoji;\n\t};\n\tconst auto emojiState = button->lifetime().make_state<EmojiStatusState>();\n\n\trpl::combine(\n\t\tbutton->widthValue(),\n\t\trpl::duplicate(label),\n\t\trpl::duplicate(colorIndexValue),\n\t\trpl::duplicate(colorProfileIndexValue),\n\t\trpl::duplicate(emojiStatusIdValue),\n\t\trpl::duplicate(name)\n\t) | rpl::on_next([=](\n\t\t\tint width,\n\t\t\tconst QString &buttonText,\n\t\t\tint colorIndex,\n\t\t\tstd::optional<uint8> profileIndex,\n\t\t\tEmojiStatusId emojiStatusId,\n\t\t\tconst QString &name) {\n\t\tconst auto available = width\n\t\t\t- st::settingsButton.padding.left()\n\t\t\t- (st::settingsColorButton.padding.right() - sampleSize)\n\t\t\t- st::settingsButton.style.font->width(buttonText)\n\t\t\t- st::settingsButtonRightSkip;\n\n\t\tconst auto hasEmojiStatus = emojiStatusId\n\t\t\t&& emojiStatusId.collectible;\n\t\tconst auto hasProfile = profileIndex.has_value() || hasEmojiStatus;\n\n\t\tif (hasEmojiStatus && emojiStatusId.collectible) {\n\t\t\tconst auto color = emojiStatusId.collectible->centerColor;\n\t\t\tprofileState->colorSet.palette = { color };\n\t\t\tprofileState->colorSet.bg = { color };\n\t\t\tprofileState->colorSet.story = { color };\n\t\t} else if (hasProfile) {\n\t\t\tconst auto peerColors = &peer->session().api().peerColors();\n\t\t\tprofileState->colorSet\n\t\t\t\t= peerColors->colorProfileFor(peer).value_or(\n\t\t\t\t\tData::ColorProfileSet{});\n\t\t}\n\n\t\tprofileSample->setVisible(hasProfile);\n\t\temojiStatusWidget->setVisible(hasEmojiStatus);\n\n\t\tif (hasEmojiStatus && !emojiState->emoji) {\n\t\t\temojiState->emoji\n\t\t\t\t= peer->session().data().customEmojiManager().create(\n\t\t\t\t\tData::EmojiStatusCustomId(emojiStatusId),\n\t\t\t\t\t[raw = emojiStatusWidget] { raw->update(); },\n\t\t\t\t\tData::CustomEmojiSizeTag::Normal);\n\t\t} else if (!hasEmojiStatus) {\n\t\t\temojiState->emoji = nullptr;\n\t\t}\n\n\t\tsample->setForceCircle(hasProfile);\n\t\tif (style->colorPatternIndex(colorIndex) || hasProfile) {\n\t\t\tsample->resize(sampleSize, sampleSize);\n\t\t} else {\n\t\t\tconst auto padding = st::settingsColorSamplePadding;\n\t\t\tconst auto wantedHeight = padding.top()\n\t\t\t\t+ st::semiboldFont->height\n\t\t\t\t+ padding.bottom();\n\t\t\tconst auto wantedWidth = sample->naturalWidth();\n\t\t\tsample->resize(std::min(wantedWidth, available), wantedHeight);\n\t\t}\n\t\tsample->update();\n\t\tsample->setCutoutPadding(hasProfile\n\t\t\t? st::settingsColorSampleCutout\n\t\t\t: 0);\n\t\tprofileSample->update();\n\t\temojiStatusWidget->update();\n\t}, sample->lifetime());\n\n\trpl::combine(\n\t\tbutton->sizeValue(),\n\t\tsample->sizeValue(),\n\t\trpl::duplicate(colorIndexValue),\n\t\trpl::duplicate(colorProfileIndexValue),\n\t\trpl::duplicate(emojiStatusIdValue)\n\t) | rpl::on_next([=](\n\t\t\tQSize outer,\n\t\t\tQSize inner,\n\t\t\tint colorIndex,\n\t\t\tstd::optional<uint8> profileIndex,\n\t\t\tEmojiStatusId emojiStatusId) {\n\t\tconst auto hasColor = (colorIndex != 0);\n\n\t\tconst auto right = st::settingsColorButton.padding.right()\n\t\t\t- st::settingsColorSampleSkip\n\t\t\t- st::settingsColorSampleSize\n\t\t\t- (style->colorPatternIndex(colorIndex)\n\t\t\t\t? 0\n\t\t\t\t: st::settingsColorSamplePadding.right());\n\t\tsample->move(\n\t\t\touter.width() - right - inner.width(),\n\t\t\t(outer.height() - inner.height()) / 2);\n\t\tconst auto profilePos = sample->pos()\n\t\t\t+ (hasColor\n\t\t\t\t? QPoint(st::settingsColorProfileSampleShift\n\t\t\t\t\t- st::settingsColorSampleSize\n\t\t\t\t\t- st::lineWidth, 0)\n\t\t\t\t: QPoint());\n\t\tprofileSample->move(profilePos);\n\t\temojiStatusWidget->move(profilePos);\n\t}, sample->lifetime());\n\n\tconstexpr auto kScale = 0.7;\n\temojiStatusWidget->paintOn([=](QPainter &p) {\n\t\tif (!emojiState->emoji) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto size = emojiStatusWidget->size();\n\t\tconst auto offset = (size * (1.0 - kScale)) / 2.0;\n\t\tp.translate(offset.width(), offset.height());\n\t\tp.scale(kScale, kScale);\n\t\temojiState->emoji->paint(p, {\n\t\t\t.textColor = st::windowFg->c,\n\t\t\t.now = crl::now(),\n\t\t});\n\t});\n\n\tsample->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tprofileSample->setAttribute(Qt::WA_TransparentForMouseEvents);\n\temojiStatusWidget->setAttribute(Qt::WA_TransparentForMouseEvents);\n}\n\nnot_null<Ui::SettingsButton*> AddPeerColorButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tconst style::SettingsButton &st) {\n\tauto label = peer->isSelf()\n\t\t? tr::lng_settings_theme_name_color()\n\t\t: tr::lng_edit_channel_color();\n\tconst auto button = AddButtonWithIcon(\n\t\tcontainer,\n\t\trpl::duplicate(label),\n\t\tst,\n\t\t{ &st::menuIconChangeColors });\n\n\tconst auto style = std::make_shared<Ui::ChatStyle>(\n\t\tpeer->session().colorIndicesValue());\n\tconst auto theme = std::shared_ptr<Ui::ChatTheme>(\n\t\tWindow::Theme::DefaultChatThemeOn(button->lifetime()));\n\tstyle->apply(theme.get());\n\n\tif (!peer->isMegagroup()) {\n\t\tSetupPeerColorSample(button, peer, rpl::duplicate(label), style);\n\t}\n\n\t{\n\t\tconst auto badge = Ui::NewBadge::CreateNewBadge(\n\t\t\tbutton,\n\t\t\ttr::lng_premium_summary_new_badge()).get();\n\t\trpl::combine(\n\t\t\trpl::duplicate(label),\n\t\t\tbutton->widthValue()\n\t\t) | rpl::on_next([=](\n\t\t\t\tconst QString &text,\n\t\t\t\tint width) {\n\t\t\tconst auto space = st.style.font->spacew;\n\t\t\tconst auto left = st.padding.left()\n\t\t\t\t+ st.style.font->width(text)\n\t\t\t\t+ space;\n\t\t\tconst auto available = width - left - st.padding.right();\n\t\t\tbadge->setVisible(available >= badge->width());\n\t\t\tif (!badge->isHidden()) {\n\t\t\t\tconst auto top = st.padding.top()\n\t\t\t\t\t+ st.style.font->ascent\n\t\t\t\t\t- st::settingsPremiumNewBadge.style.font->ascent\n\t\t\t\t\t- st::settingsPremiumNewBadgePadding.top();\n\t\t\t\tbadge->moveToLeft(left, top, width);\n\t\t\t}\n\t\t}, badge->lifetime());\n\t}\n\n\tbutton->setClickedCallback([=] {\n\t\tif (const auto controller = show->resolveWindow()) {\n\t\t\tcontroller->show(Box(\n\t\t\t\tEditPeerColorBox,\n\t\t\t\tcontroller,\n\t\t\t\tpeer,\n\t\t\t\tstyle,\n\t\t\t\ttheme,\n\t\t\t\tPeerColorTab::Profile));\n\t\t}\n\t});\n\treturn button;\n}\n\nvoid CheckBoostLevel(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tFn<std::optional<Ui::AskBoostReason>(int level)> askMore,\n\t\tFn<void()> cancel) {\n\tpeer->session().api().request(MTPpremium_GetBoostsStatus(\n\t\tpeer->input()\n\t)).done([=](const MTPpremium_BoostsStatus &result) {\n\t\tconst auto &data = result.data();\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\tchannel->updateLevelHint(data.vlevel().v);\n\t\t}\n\t\tconst auto reason = askMore(data.vlevel().v);\n\t\tif (!reason) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto openStatistics = [=] {\n\t\t\tif (const auto controller = show->resolveWindow()) {\n\t\t\t\tcontroller->showSection(Info::Boosts::Make(peer));\n\t\t\t}\n\t\t};\n\t\tauto counters = ParseBoostCounters(result);\n\t\tcounters.mine = 0; // Don't show current level as just-reached.\n\t\tshow->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{\n\t\t\t.link = qs(data.vboost_url()),\n\t\t\t.boost = counters,\n\t\t\t.features = (peer->isChannel()\n\t\t\t\t? LookupBoostFeatures(peer->asChannel())\n\t\t\t\t: Ui::BoostFeatures()),\n\t\t\t.reason = *reason,\n\t\t\t.group = !peer->isBroadcast(),\n\t\t}, openStatistics, nullptr));\n\t\tcancel();\n\t}).fail([=](const MTP::Error &error) {\n\t\tshow->showToast(error.type());\n\t\tcancel();\n\t}).send();\n}\n\nButtonWithEmoji ButtonStyleWithRightEmoji(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst QString &noneString,\n\t\tconst style::SettingsButton &parentSt) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto emojiWidth = Data::FrameSizeFromTag({}) / ratio;\n\n\tconst auto noneWidth = st::normalFont->width(noneString);\n\n\tconst auto added = st::normalFont->spacew;\n\tconst auto rightAdded = std::max(noneWidth, emojiWidth);\n\treturn {\n\t\t.st = ButtonStyleWithAddedPadding(\n\t\t\tparent,\n\t\t\tparentSt,\n\t\t\tQMargins(0, 0, added + rightAdded, 0)),\n\t\t.emojiWidth = emojiWidth,\n\t\t.noneWidth = noneWidth,\n\t\t.added = added,\n\t};\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace style {\nstruct SettingsButton;\n} // namespace style\n\nnamespace st {\nextern const style::SettingsButton &peerAppearanceButton;\n} // namespace st\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass RpWidget;\nclass GenericBox;\nclass ChatStyle;\nclass ChatTheme;\nclass VerticalLayout;\nstruct AskBoostReason;\nclass RpWidget;\nclass SettingsButton;\n} // namespace Ui\n\nvoid AddLevelBadge(\n\tint level,\n\tnot_null<Ui::SettingsButton*> button,\n\tUi::RpWidget *right,\n\tnot_null<ChannelData*> channel,\n\tconst QMargins &padding,\n\trpl::producer<QString> text);\n\nenum class PeerColorTab {\n\tProfile,\n\tName,\n};\n\nvoid EditPeerColorBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\tstd::shared_ptr<Ui::ChatStyle> style = nullptr,\n\tstd::shared_ptr<Ui::ChatTheme> theme = nullptr,\n\tPeerColorTab initialTab = PeerColorTab::Profile);\n\nnot_null<Ui::SettingsButton*> AddPeerColorButton(\n\tnot_null<Ui::VerticalLayout*> container,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<PeerData*> peer,\n\tconst style::SettingsButton &st);\n\nvoid CheckBoostLevel(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<PeerData*> peer,\n\tFn<std::optional<Ui::AskBoostReason>(int level)> askMore,\n\tFn<void()> cancel);\n\nstruct ButtonWithEmoji {\n\tnot_null<const style::SettingsButton*> st;\n\tint emojiWidth = 0;\n\tint noneWidth = 0;\n\tint added = 0;\n};\n[[nodiscard]] ButtonWithEmoji ButtonStyleWithRightEmoji(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst QString &noneString,\n\tconst style::SettingsButton &parentSt = st::peerAppearanceButton);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui::EditPeer {\n\nconstexpr auto kMaxGroupChannelTitle = 128;\nconstexpr auto kMaxUserFirstLastName = 64;\nconstexpr auto kMaxChannelDescription = 255;\nconstexpr auto kMinUsernameLength = 5;\nconstexpr auto kMaxUsernameLength = 32;\nconstexpr auto kUsernameCheckTimeout = crl::time(200);\n\n} // namespace Ui::EditPeer\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_history_visibility_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_peer_history_visibility_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_info.h\"\n\nvoid EditPeerHistoryVisibilityBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tbool isLegacy,\n\t\tFn<void(HistoryVisibility)> savedCallback,\n\t\tHistoryVisibility historyVisibilitySavedValue) {\n\tconst auto historyVisibility = std::make_shared<\n\t\tUi::RadioenumGroup<HistoryVisibility>\n\t>(historyVisibilitySavedValue);\n\n\tconst auto addButton = [=](\n\t\t\tnot_null<Ui::RpWidget*> inner,\n\t\t\tHistoryVisibility v) {\n\t\tconst auto button = Ui::CreateChild<Ui::AbstractButton>(inner.get());\n\t\tinner->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\tbutton->resize(s);\n\t\t}, button->lifetime());\n\t\tbutton->setClickedCallback([=] { historyVisibility->setValue(v); });\n\t};\n\n\tbox->setTitle(tr::lng_manage_history_visibility_title());\n\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\tsavedCallback(historyVisibility->current());\n\t\tbox->closeBox();\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\n\tbox->addSkip(st::editPeerHistoryVisibilityTopSkip);\n\tconst auto visible = box->addRow(object_ptr<Ui::VerticalLayout>(box));\n\tvisible->add(object_ptr<Ui::Radioenum<HistoryVisibility>>(\n\t\tbox,\n\t\thistoryVisibility,\n\t\tHistoryVisibility::Visible,\n\t\ttr::lng_manage_history_visibility_shown(tr::now),\n\t\tst::defaultBoxCheckbox));\n\tvisible->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_manage_history_visibility_shown_about(),\n\t\t\tst::editPeerPrivacyLabel),\n\t\tst::editPeerPreHistoryLabelMargins);\n\taddButton(visible, HistoryVisibility::Visible);\n\n\tbox->addSkip(st::editPeerHistoryVisibilityTopSkip);\n\tconst auto hidden = box->addRow(object_ptr<Ui::VerticalLayout>(box));\n\thidden->add(object_ptr<Ui::Radioenum<HistoryVisibility>>(\n\t\tbox,\n\t\thistoryVisibility,\n\t\tHistoryVisibility::Hidden,\n\t\ttr::lng_manage_history_visibility_hidden(tr::now),\n\t\tst::defaultBoxCheckbox));\n\thidden->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\t(isLegacy\n\t\t\t\t? tr::lng_manage_history_visibility_hidden_legacy\n\t\t\t\t: tr::lng_manage_history_visibility_hidden_about)(),\n\t\t\tst::editPeerPrivacyLabel),\n\t\tst::editPeerPreHistoryLabelMargins);\n\taddButton(hidden, HistoryVisibility::Hidden);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_history_visibility_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nenum class HistoryVisibility {\n\tVisible,\n\tHidden,\n};\n\nvoid EditPeerHistoryVisibilityBox(\n\tnot_null<Ui::GenericBox*> box,\n\tbool isLegacy,\n\tFn<void(HistoryVisibility)> savedCallback,\n\tHistoryVisibility historyVisibilitySavedValue);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_peer_info_box.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_credits.h\"\n#include \"api/api_peer_photo.h\"\n#include \"api/api_statistics.h\"\n#include \"api/api_user_names.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"base/event_filter.h\"\n#include \"boxes/peers/edit_participants_box.h\"\n#include \"boxes/peers/edit_peer_color_box.h\"\n#include \"boxes/peers/edit_peer_common.h\"\n#include \"boxes/peers/edit_peer_type_box.h\"\n#include \"boxes/peers/edit_peer_history_visibility_box.h\"\n#include \"boxes/peers/edit_peer_permissions_box.h\"\n#include \"boxes/peers/edit_peer_invite_links.h\"\n#include \"boxes/peers/edit_discussion_link_box.h\"\n#include \"boxes/peers/edit_peer_requests_box.h\"\n#include \"boxes/peers/edit_peer_reactions.h\"\n#include \"boxes/peers/replace_boost_box.h\"\n#include \"boxes/peers/toggle_topics_box.h\"\n#include \"boxes/peers/verify_peers_box.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/edit_privacy_box.h\" // EditDirectMessagesPriceBox\n#include \"boxes/stickers_box.h\"\n#include \"boxes/username_box.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/components/credits.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_user.h\"\n#include \"history/admin_log/history_admin_log_section.h\"\n#include \"info/bot/earn/info_bot_earn_widget.h\"\n#include \"info/bot/starref/info_bot_starref_join_widget.h\"\n#include \"info/bot/starref/info_bot_starref_setup_widget.h\"\n#include \"info/channel_statistics/boosts/info_boosts_widget.h\"\n#include \"info/channel_statistics/earn/earn_format.h\"\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"info/channel_statistics/earn/info_channel_earn_widget.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"info/info_memento.h\"\n#include \"lang/lang_keys.h\"\n#include \"mtproto/sender.h\"\n#include \"main/main_app_config.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/boxes/boost_box.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/new_badges.h\"\n#include \"ui/rect.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_session_controller.h\"\n#include \"api/api_invite_links.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_info.h\"\n\n#include <QtSvg/QSvgRenderer>\n\nnamespace {\n\nconstexpr auto kBotManagerUsername = \"BotFather\"_cs;\n\n[[nodiscard]] auto ToPositiveNumberString() {\n\treturn rpl::map([](int count) {\n\t\treturn count ? QString::number(count) : QString();\n\t});\n}\n\n[[nodiscard]] int EnableForumMinMembers(not_null<PeerData*> peer) {\n\treturn peer->session().appConfig().get<int>(\n\t\tu\"forum_upgrade_participants_min\"_q,\n\t\t200);\n}\n\nvoid AddSkip(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tint top = st::editPeerTopButtonsLayoutSkip,\n\t\tint bottom = st::editPeerTopButtonsLayoutSkipToBottom) {\n\tUi::AddSkip(container, top);\n\tUi::AddDivider(container);\n\tUi::AddSkip(container, bottom);\n}\n\nvoid AddButtonWithCount(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\trpl::producer<QString> &&text,\n\t\trpl::producer<QString> &&count,\n\t\tFn<void()> callback,\n\t\tSettings::IconDescriptor &&descriptor) {\n\tparent->add(EditPeerInfoBox::CreateButton(\n\t\tparent,\n\t\tstd::move(text),\n\t\tstd::move(count),\n\t\tstd::move(callback),\n\t\tst::manageGroupButton,\n\t\tstd::move(descriptor)));\n}\n\nnot_null<Ui::SettingsButton*> AddButtonWithText(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\trpl::producer<QString> &&text,\n\t\trpl::producer<TextWithEntities> &&label,\n\t\tFn<void()> callback,\n\t\tSettings::IconDescriptor &&descriptor) {\n\treturn parent->add(EditPeerInfoBox::CreateButton(\n\t\tparent,\n\t\tstd::move(text),\n\t\tstd::move(label),\n\t\tstd::move(callback),\n\t\tst::manageGroupTopButtonWithText,\n\t\tstd::move(descriptor)));\n}\n\nnot_null<Ui::SettingsButton*> AddButtonWithText(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\trpl::producer<QString> &&text,\n\t\trpl::producer<QString> &&label,\n\t\tFn<void()> callback,\n\t\tSettings::IconDescriptor &&descriptor) {\n\treturn AddButtonWithText(\n\t\tparent,\n\t\tstd::move(text),\n\t\tstd::move(label) | rpl::map(tr::marked),\n\t\tstd::move(callback),\n\t\tstd::move(descriptor));\n}\n\nvoid AddButtonDelete(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\trpl::producer<QString> &&text,\n\t\tFn<void()> callback) {\n\tparent->add(EditPeerInfoBox::CreateButton(\n\t\tparent,\n\t\tstd::move(text),\n\t\trpl::single(QString()),\n\t\tstd::move(callback),\n\t\tst::manageDeleteGroupButton,\n\t\t{}));\n}\n\nvoid SaveDefaultRestrictions(\n\t\tnot_null<PeerData*> peer,\n\t\tChatRestrictions rights,\n\t\tFn<void()> done) {\n\tconst auto api = &peer->session().api();\n\tconst auto key = Api::RequestKey(\"default_restrictions\", peer->id);\n\n\tconst auto requestId = api->request(\n\t\tMTPmessages_EditChatDefaultBannedRights(\n\t\t\tpeer->input(),\n\t\t\tRestrictionsToMTP({ rights, 0 }))\n\t).done([=](const MTPUpdates &result) {\n\t\tapi->clearModifyRequest(key);\n\t\tapi->applyUpdates(result);\n\t\tdone();\n\t}).fail([=](const MTP::Error &error) {\n\t\tapi->clearModifyRequest(key);\n\t\tif (error.type() != u\"CHAT_NOT_MODIFIED\"_q) {\n\t\t\treturn;\n\t\t}\n\t\tif (const auto chat = peer->asChat()) {\n\t\t\tchat->setDefaultRestrictions(rights);\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\tchannel->setDefaultRestrictions(rights);\n\t\t} else {\n\t\t\tUnexpected(\"Peer in ApiWrap::saveDefaultRestrictions.\");\n\t\t}\n\t\tdone();\n\t}).send();\n\n\tapi->registerModifyRequest(key, requestId);\n}\n\nvoid SaveSlowmodeSeconds(\n\t\tnot_null<ChannelData*> channel,\n\t\tint seconds,\n\t\tFn<void()> done) {\n\tconst auto api = &channel->session().api();\n\tconst auto key = Api::RequestKey(\"slowmode_seconds\", channel->id);\n\n\tconst auto requestId = api->request(MTPchannels_ToggleSlowMode(\n\t\tchannel->inputChannel(),\n\t\tMTP_int(seconds)\n\t)).done([=](const MTPUpdates &result) {\n\t\tapi->clearModifyRequest(key);\n\t\tapi->applyUpdates(result);\n\t\tchannel->setSlowmodeSeconds(seconds);\n\t\tdone();\n\t}).fail([=](const MTP::Error &error) {\n\t\tapi->clearModifyRequest(key);\n\t\tif (error.type() != u\"CHAT_NOT_MODIFIED\"_q) {\n\t\t\treturn;\n\t\t}\n\t\tchannel->setSlowmodeSeconds(seconds);\n\t\tdone();\n\t}).send();\n\n\tapi->registerModifyRequest(key, requestId);\n}\n\nvoid SaveStarsPerMessage(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<ChannelData*> channel,\n\t\tint starsPerMessage,\n\t\tFn<void(bool)> done) {\n\tconst auto api = &channel->session().api();\n\tconst auto key = Api::RequestKey(\"stars_per_message\", channel->id);\n\n\tconst auto broadcast = channel->isBroadcast();\n\n\tusing Flag = MTPchannels_UpdatePaidMessagesPrice::Flag;\n\tconst auto broadcastAllowed = broadcast && (starsPerMessage >= 0);\n\tconst auto requestId = api->request(MTPchannels_UpdatePaidMessagesPrice(\n\t\tMTP_flags(broadcastAllowed\n\t\t\t? Flag::f_broadcast_messages_allowed\n\t\t\t: Flag(0)),\n\t\tchannel->inputChannel(),\n\t\tMTP_long(starsPerMessage)\n\t)).done([=](const MTPUpdates &result) {\n\t\tapi->clearModifyRequest(key);\n\t\tapi->applyUpdates(result);\n\t\tif (!broadcast) {\n\t\t\tchannel->owner().editStarsPerMessage(channel, starsPerMessage);\n\t\t}\n\t\tdone(true);\n\t}).fail([=](const MTP::Error &error) {\n\t\tapi->clearModifyRequest(key);\n\t\tif (error.type() != u\"CHAT_NOT_MODIFIED\"_q) {\n\t\t\tshow->showToast(error.type());\n\t\t\tdone(false);\n\t\t} else {\n\t\t\tif (!broadcast) {\n\t\t\t\tchannel->owner().editStarsPerMessage(\n\t\t\t\t\tchannel,\n\t\t\t\t\tstarsPerMessage);\n\t\t\t}\n\t\t\tdone(true);\n\t\t}\n\t}).send();\n\n\tapi->registerModifyRequest(key, requestId);\n}\n\nvoid SaveBoostsUnrestrict(\n\t\tnot_null<ChannelData*> channel,\n\t\tint boostsUnrestrict,\n\t\tFn<void()> done) {\n\tconst auto api = &channel->session().api();\n\tconst auto key = Api::RequestKey(\"boosts_unrestrict\", channel->id);\n\tconst auto requestId = api->request(\n\t\tMTPchannels_SetBoostsToUnblockRestrictions(\n\t\t\tchannel->inputChannel(),\n\t\t\tMTP_int(boostsUnrestrict))\n\t).done([=](const MTPUpdates &result) {\n\t\tapi->clearModifyRequest(key);\n\t\tapi->applyUpdates(result);\n\t\tchannel->setBoostsUnrestrict(\n\t\t\tchannel->boostsApplied(),\n\t\t\tboostsUnrestrict);\n\t\tdone();\n\t}).fail([=](const MTP::Error &error) {\n\t\tapi->clearModifyRequest(key);\n\t\tif (error.type() != u\"CHAT_NOT_MODIFIED\"_q) {\n\t\t\treturn;\n\t\t}\n\t\tchannel->setBoostsUnrestrict(\n\t\t\tchannel->boostsApplied(),\n\t\t\tboostsUnrestrict);\n\t\tdone();\n\t}).send();\n\n\tapi->registerModifyRequest(key, requestId);\n}\n\nvoid ShowEditPermissions(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer) {\n\tconst auto show = navigation->uiShow();\n\tauto createBox = [=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto saving = box->lifetime().make_state<int>(0);\n\t\tconst auto save = [=](\n\t\t\t\tnot_null<PeerData*> peer,\n\t\t\t\tEditPeerPermissionsBoxResult result) {\n\t\t\tExpects(result.slowmodeSeconds == 0 || peer->isChannel());\n\n\t\t\tconst auto close = crl::guard(box, [=] { box->closeBox(); });\n\t\t\tSaveDefaultRestrictions(\n\t\t\t\tpeer,\n\t\t\t\tresult.rights,\n\t\t\t\tclose);\n\t\t\tif (const auto channel = peer->asChannel()) {\n\t\t\t\tSaveSlowmodeSeconds(channel, result.slowmodeSeconds, close);\n\t\t\t\tSaveBoostsUnrestrict(\n\t\t\t\t\tchannel,\n\t\t\t\t\tresult.boostsUnrestrict,\n\t\t\t\t\tclose);\n\t\t\t\tconst auto price = result.starsPerMessage;\n\t\t\t\tSaveStarsPerMessage(show, channel, price, [=](bool ok) {\n\t\t\t\t\tclose();\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\t\tauto done = [=](EditPeerPermissionsBoxResult result) {\n\t\t\tif (*saving) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t*saving = true;\n\n\t\t\tconst auto saveFor = peer->migrateToOrMe();\n\t\t\tconst auto chat = saveFor->asChat();\n\t\t\tif (!chat\n\t\t\t\t|| (!result.slowmodeSeconds\n\t\t\t\t\t&& !result.boostsUnrestrict\n\t\t\t\t\t&& !result.starsPerMessage)) {\n\t\t\t\tsave(saveFor, result);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto api = &peer->session().api();\n\t\t\tapi->migrateChat(chat, [=](not_null<ChannelData*> channel) {\n\t\t\t\tsave(channel, result);\n\t\t\t}, [=](const QString &) {\n\t\t\t\t*saving = false;\n\t\t\t});\n\t\t};\n\t\tShowEditPeerPermissionsBox(box, navigation, peer, std::move(done));\n\t};\n\tnavigation->parentController()->show(Box(std::move(createBox)));\n}\n\n[[nodiscard]] int CurrentPricePerDirectMessage(\n\t\tnot_null<ChannelData*> broadcast) {\n\tconst auto monoforumLink = broadcast->monoforumLink();\n\treturn (monoforumLink && !monoforumLink->monoforumDisabled())\n\t\t? monoforumLink->commonStarsPerMessage()\n\t\t: -1;\n}\n\nclass Controller : public base::has_weak_ptr {\npublic:\n\tController(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<Ui::BoxContent*> box,\n\t\tnot_null<PeerData*> peer);\n\t~Controller();\n\n\t[[nodiscard]] object_ptr<Ui::VerticalLayout> createContent();\n\tvoid setFocus();\n\nprivate:\n\tstruct Controls {\n\t\tUi::InputField *title = nullptr;\n\t\tUi::InputField *description = nullptr;\n\t\tUi::UserpicButton *photo = nullptr;\n\t\trpl::lifetime initialPhotoImageWaiting;\n\t\tUi::VerticalLayout *buttonsLayout = nullptr;\n\t\tUi::SettingsButton *forumToggle = nullptr;\n\t\tbool forumToggleLocked = false;\n\t\tbool levelRequested = false;\n\t\tUi::SlideWrap<> *historyVisibilityWrap = nullptr;\n\t};\n\tstruct Saving {\n\t\tstd::optional<QString> username;\n\t\tstd::optional<std::vector<QString>> usernamesOrder;\n\t\tstd::optional<QString> title;\n\t\tstd::optional<QString> description;\n\t\tstd::optional<bool> hiddenPreHistory;\n\t\tstd::optional<bool> forum;\n\t\tstd::optional<bool> forumTabs;\n\t\tstd::optional<bool> autotranslate;\n\t\tstd::optional<bool> signatures;\n\t\tstd::optional<bool> signatureProfiles;\n\t\tstd::optional<bool> noForwards;\n\t\tstd::optional<bool> joinToWrite;\n\t\tstd::optional<bool> requestToJoin;\n\t\tstd::optional<ChannelData*> discussionLink;\n\t\tstd::optional<int> starsPerDirectMessage;\n\t};\n\n\t[[nodiscard]] object_ptr<Ui::RpWidget> createPhotoAndTitleEdit();\n\t[[nodiscard]] object_ptr<Ui::RpWidget> createTitleEdit();\n\t[[nodiscard]] object_ptr<Ui::RpWidget> createPhotoEdit();\n\t[[nodiscard]] object_ptr<Ui::RpWidget> createDescriptionEdit();\n\t[[nodiscard]] object_ptr<Ui::RpWidget> createManageGroupButtons();\n\t[[nodiscard]] object_ptr<Ui::RpWidget> createStickersEdit();\n\n\t[[nodiscard]] bool canEditInformation() const;\n\t[[nodiscard]] bool canEditReactions() const;\n\tvoid refreshHistoryVisibility();\n\tvoid refreshForumToggleLocked();\n\tvoid showEditPeerTypeBox(\n\t\tstd::optional<rpl::producer<QString>> error = {});\n\tvoid showEditDiscussionLinkBox();\n\tvoid showEditDirectMessagesBox();\n\tvoid fillPrivacyTypeButton();\n\tvoid fillDiscussionLinkButton();\n\tvoid fillDirectMessagesButton();\n\t//void fillInviteLinkButton();\n\tvoid fillForumButton();\n\tvoid fillColorIndexButton();\n\tvoid fillAutoTranslateButton();\n\tvoid fillSignaturesButton();\n\tvoid fillHistoryVisibilityButton();\n\tvoid fillManageSection();\n\tvoid fillPendingRequestsButton();\n\n\tvoid fillBotUsernamesButton();\n\tvoid fillBotCurrencyButton();\n\tvoid fillBotCreditsButton();\n\tvoid fillBotAffiliateProgram();\n\tvoid fillBotEditIntroButton();\n\tvoid fillBotEditCommandsButton();\n\tvoid fillBotEditSettingsButton();\n\tvoid fillBotVerifyAccounts();\n\n\tvoid submitTitle();\n\tvoid submitDescription();\n\tvoid deleteWithConfirmation();\n\tvoid deleteChannel();\n\tvoid editReactions();\n\n\t[[nodiscard]] std::optional<Saving> validate() const;\n\t[[nodiscard]] bool validateUsernamesOrder(Saving &to) const;\n\t[[nodiscard]] bool validateUsername(Saving &to) const;\n\t[[nodiscard]] bool validateDiscussionLink(Saving &to) const;\n\t[[nodiscard]] bool validateDirectMessagesPrice(Saving &to) const;\n\t[[nodiscard]] bool validateTitle(Saving &to) const;\n\t[[nodiscard]] bool validateDescription(Saving &to) const;\n\t[[nodiscard]] bool validateHistoryVisibility(Saving &to) const;\n\t[[nodiscard]] bool validateForum(Saving &to) const;\n\t[[nodiscard]] bool validateAutotranslate(Saving &to) const;\n\t[[nodiscard]] bool validateSignatures(Saving &to) const;\n\t[[nodiscard]] bool validateForwards(Saving &to) const;\n\t[[nodiscard]] bool validateJoinToWrite(Saving &to) const;\n\t[[nodiscard]] bool validateRequestToJoin(Saving &to) const;\n\n\tvoid save();\n\tvoid saveUsernamesOrder();\n\tvoid saveUsername();\n\tvoid saveDiscussionLink();\n\tvoid saveDirectMessagesPrice();\n\tvoid saveTitle();\n\tvoid saveDescription();\n\tvoid saveHistoryVisibility();\n\tvoid saveForum();\n\tvoid saveAutotranslate();\n\tvoid saveSignatures();\n\tvoid saveForwards();\n\tvoid saveJoinToWrite();\n\tvoid saveRequestToJoin();\n\tvoid savePhoto();\n\tvoid pushSaveStage(FnMut<void()> &&lambda);\n\tvoid continueSave();\n\tvoid cancelSave();\n\n\tvoid toggleBotManager(const QString &command);\n\n\tvoid togglePreHistoryHidden(\n\t\tnot_null<ChannelData*> channel,\n\t\tbool hidden,\n\t\tFn<void()> done,\n\t\tFn<void()> fail);\n\n\tvoid subscribeToMigration();\n\tvoid migrate(not_null<ChannelData*> channel);\n\n\tstd::optional<ChannelData*> _discussionLinkSavedValue;\n\tChannelData *_discussionLinkOriginalValue = nullptr;\n\tbool _channelHasLocationOriginalValue = false;\n\tstd::optional<rpl::variable<int>> _starsPerDirectMessageSavedValue;\n\tstd::optional<HistoryVisibility> _historyVisibilitySavedValue;\n\tstd::optional<EditPeerTypeData> _typeDataSavedValue;\n\tstd::optional<bool> _forumSavedValue;\n\tstd::optional<bool> _forumTabsSavedValue;\n\tstd::optional<bool> _autotranslateSavedValue;\n\tstd::optional<bool> _signaturesSavedValue;\n\tstd::optional<bool> _signatureProfilesSavedValue;\n\n\tconst not_null<Window::SessionNavigation*> _navigation;\n\tconst not_null<Ui::BoxContent*> _box;\n\tnot_null<PeerData*> _peer;\n\tMTP::Sender _api;\n\tconst bool _isGroup = false;\n\tconst bool _isBot = false;\n\n\tbase::unique_qptr<Ui::VerticalLayout> _wrap;\n\tControls _controls;\n\n\tstd::deque<FnMut<void()>> _saveStagesQueue;\n\tSaving _savingData;\n\n\tstruct PrivacyAndForwards {\n\t\tPrivacy privacy;\n\t\tbool noForwards = false;\n\t};\n\n\tconst rpl::event_stream<PrivacyAndForwards> _privacyTypeUpdates;\n\tconst rpl::event_stream<ChannelData*> _discussionLinkUpdates;\n\tmtpRequestId _discussionLinksRequestId = 0;\n\n\trpl::lifetime _lifetime;\n\n};\n\nController::Controller(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<Ui::BoxContent*> box,\n\tnot_null<PeerData*> peer)\n: _navigation(navigation)\n, _box(box)\n, _peer(peer)\n, _api(&_peer->session().mtp())\n, _isGroup(_peer->isChat() || _peer->isMegagroup())\n, _isBot(_peer->isUser() && _peer->asUser()->botInfo) {\n\t_box->setTitle(_isBot\n\t\t? tr::lng_edit_bot_title()\n\t\t: _isGroup\n\t\t? tr::lng_edit_group()\n\t\t: tr::lng_edit_channel_title());\n\t_box->addButton(tr::lng_settings_save(), [=] {\n\t\tsave();\n\t});\n\t_box->addButton(tr::lng_cancel(), [=] {\n\t\t_box->closeBox();\n\t});\n\tsubscribeToMigration();\n\t_peer->updateFull();\n}\n\nController::~Controller() = default;\n\nvoid Controller::subscribeToMigration() {\n\tSubscribeToMigration(\n\t\t_peer,\n\t\t_lifetime,\n\t\t[=](not_null<ChannelData*> channel) { migrate(channel); });\n}\n\nvoid Controller::migrate(not_null<ChannelData*> channel) {\n\t_peer = channel;\n\t_peer->updateFull();\n}\n\nobject_ptr<Ui::VerticalLayout> Controller::createContent() {\n\tauto result = object_ptr<Ui::VerticalLayout>(_box);\n\t_wrap.reset(result.data());\n\t_controls = Controls();\n\n\t_wrap->add(createPhotoAndTitleEdit());\n\t_wrap->add(createDescriptionEdit());\n\t_wrap->add(createManageGroupButtons());\n\n\treturn result;\n}\n\nvoid Controller::setFocus() {\n\tif (_controls.title) {\n\t\t_controls.title->setFocusFast();\n\t}\n}\n\nobject_ptr<Ui::RpWidget> Controller::createPhotoAndTitleEdit() {\n\tExpects(_wrap != nullptr);\n\n\tif (!canEditInformation()) {\n\t\treturn nullptr;\n\t}\n\n\tauto result = object_ptr<Ui::RpWidget>(_wrap);\n\tconst auto container = result.data();\n\n\tconst auto photoWrap = Ui::AttachParentChild(\n\t\tcontainer,\n\t\tcreatePhotoEdit());\n\tconst auto titleEdit = Ui::AttachParentChild(\n\t\tcontainer,\n\t\tcreateTitleEdit());\n\tphotoWrap->heightValue(\n\t) | rpl::on_next([container](int height) {\n\t\tcontainer->resize(container->width(), height);\n\t}, photoWrap->lifetime());\n\tcontainer->widthValue(\n\t) | rpl::on_next([titleEdit](int width) {\n\t\tconst auto left = st::editPeerPhotoMargins.left()\n\t\t\t+ st::defaultUserpicButton.size.width();\n\t\ttitleEdit->resizeToWidth(width - left);\n\t\ttitleEdit->moveToLeft(left, 0, width);\n\t}, titleEdit->lifetime());\n\n\treturn result;\n}\n\nobject_ptr<Ui::RpWidget> Controller::createPhotoEdit() {\n\tExpects(_wrap != nullptr);\n\n\tusing PhotoWrap = Ui::PaddingWrap<Ui::UserpicButton>;\n\tauto photoWrap = object_ptr<PhotoWrap>(\n\t\t_wrap,\n\t\tobject_ptr<Ui::UserpicButton>(\n\t\t\t_wrap,\n\t\t\t_navigation->parentController(),\n\t\t\t_peer,\n\t\t\tUi::UserpicButton::Role::ChangePhoto,\n\t\t\tUi::UserpicButton::Source::PeerPhoto,\n\t\t\tst::defaultUserpicButton),\n\t\tst::editPeerPhotoMargins);\n\t_controls.photo = photoWrap->entity();\n\t_controls.photo->showCustomOnChosen();\n\n\treturn photoWrap;\n}\n\nobject_ptr<Ui::RpWidget> Controller::createTitleEdit() {\n\tExpects(_wrap != nullptr);\n\n\tauto result = object_ptr<Ui::PaddingWrap<Ui::InputField>>(\n\t\t_wrap,\n\t\tobject_ptr<Ui::InputField>(\n\t\t\t_wrap,\n\t\t\tst::editPeerTitleField,\n\t\t\t(_isBot\n\t\t\t\t? tr::lng_dlg_new_bot_name\n\t\t\t\t: _isGroup\n\t\t\t\t? tr::lng_dlg_new_group_name\n\t\t\t\t: tr::lng_dlg_new_channel_name)(),\n\t\t\t_peer->name()),\n\t\tst::editPeerTitleMargins);\n\tresult->entity()->setMaxLength(Ui::EditPeer::kMaxGroupChannelTitle);\n\tresult->entity()->setInstantReplaces(Ui::InstantReplaces::Default());\n\tresult->entity()->setInstantReplacesEnabled(\n\t\tCore::App().settings().replaceEmojiValue(),\n\t\tCore::App().settings().systemTextReplaceValue());\n\tUi::Emoji::SuggestionsController::Init(\n\t\t_wrap->window(),\n\t\tresult->entity(),\n\t\t&_peer->session());\n\n\tresult->entity()->submits(\n\t) | rpl::on_next([=] {\n\t\tsubmitTitle();\n\t}, result->entity()->lifetime());\n\n\t{\n\t\tconst auto field = result->entity();\n\t\tconst auto container = _box->getDelegate()->outerContainer();\n\t\tusing Selector = ChatHelpers::TabbedSelector;\n\t\tusing PanelPtr = base::unique_qptr<ChatHelpers::TabbedPanel>;\n\t\tconst auto emojiPanelPtr = field->lifetime().make_state<PanelPtr>(\n\t\t\tbase::make_unique_q<ChatHelpers::TabbedPanel>(\n\t\t\t\tcontainer,\n\t\t\t\tChatHelpers::TabbedPanelDescriptor{\n\t\t\t\t\t.ownedSelector = object_ptr<Selector>(\n\t\t\t\t\t\tnullptr,\n\t\t\t\t\t\tChatHelpers::TabbedSelectorDescriptor{\n\t\t\t\t\t\t\t.show = _navigation->uiShow(),\n\t\t\t\t\t\t\t.st = st::defaultComposeControls.tabbed,\n\t\t\t\t\t\t\t.level = Window::GifPauseReason::Layer,\n\t\t\t\t\t\t\t.mode = Selector::Mode::PeerTitle,\n\t\t\t\t\t\t}),\n\t\t\t\t}));\n\t\tconst auto emojiPanel = emojiPanelPtr->get();\n\t\temojiPanel->setDesiredHeightValues(\n\t\t\t1.,\n\t\t\tst::emojiPanMinHeight / 2,\n\t\t\tst::emojiPanMinHeight);\n\t\temojiPanel->hide();\n\t\temojiPanel->selector()->setCurrentPeer(_peer);\n\t\temojiPanel->selector()->emojiChosen(\n\t\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\t\tUi::InsertEmojiAtCursor(field->textCursor(), data.emoji);\n\t\t\tfield->setFocus();\n\t\t}, field->lifetime());\n\t\temojiPanel->setDropDown(true);\n\n\t\tconst auto emojiToggle = Ui::CreateChild<Ui::EmojiButton>(\n\t\t\tfield,\n\t\t\tst::defaultComposeControls.files.emoji);\n\t\temojiToggle->show();\n\t\temojiToggle->installEventFilter(emojiPanel);\n\t\temojiToggle->addClickHandler([=] { emojiPanel->toggleAnimated(); });\n\n\t\tconst auto updateEmojiPanelGeometry = [=] {\n\t\t\tconst auto parent = emojiPanel->parentWidget();\n\t\t\tconst auto global = emojiToggle->mapToGlobal({ 0, 0 });\n\t\t\tconst auto local = parent->mapFromGlobal(global);\n\t\t\temojiPanel->moveTopRight(\n\t\t\t\tlocal.y() + emojiToggle->height(),\n\t\t\t\tlocal.x() + emojiToggle->width() * 3);\n\t\t};\n\n\n\t\tfield->lifetime().make_state<base::unique_qptr<QObject>>([&] {\n\t\t\treturn base::install_event_filter(container, [=](\n\t\t\t\t\tnot_null<QEvent*> event) {\n\t\t\t\tconst auto type = event->type();\n\t\t\t\tif (type == QEvent::Move || type == QEvent::Resize) {\n\t\t\t\t\tcrl::on_main(field, [=] { updateEmojiPanelGeometry(); });\n\t\t\t\t}\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t});\n\t\t}());\n\n\t\tfield->widthValue() | rpl::on_next([=](int width) {\n\t\t\tconst auto &p = st::editPeerTitleEmojiPosition;\n\t\t\temojiToggle->moveToRight(p.x(), p.y(), width);\n\t\t\tupdateEmojiPanelGeometry();\n\t\t}, emojiToggle->lifetime());\n\n\t\tbase::install_event_filter(emojiToggle, [=](not_null<QEvent*> event) {\n\t\t\tif (event->type() == QEvent::Enter) {\n\t\t\t\tupdateEmojiPanelGeometry();\n\t\t\t}\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t});\n\t}\n\n\t_controls.title = result->entity();\n\treturn result;\n}\n\nobject_ptr<Ui::RpWidget> Controller::createDescriptionEdit() {\n\tExpects(_wrap != nullptr);\n\n\tif (!canEditInformation()) {\n\t\treturn nullptr;\n\t}\n\n\tauto result = object_ptr<Ui::PaddingWrap<Ui::InputField>>(\n\t\t_wrap,\n\t\tobject_ptr<Ui::InputField>(\n\t\t\t_wrap,\n\t\t\tst::editPeerDescription,\n\t\t\tUi::InputField::Mode::MultiLine,\n\t\t\ttr::lng_create_group_description(),\n\t\t\t_peer->about()),\n\t\tst::editPeerDescriptionMargins);\n\tresult->entity()->setMaxLength(Ui::EditPeer::kMaxChannelDescription);\n\tresult->entity()->setInstantReplaces(Ui::InstantReplaces::Default());\n\tresult->entity()->setInstantReplacesEnabled(\n\t\tCore::App().settings().replaceEmojiValue(),\n\t\tCore::App().settings().systemTextReplaceValue());\n\tresult->entity()->setSubmitSettings(\n\t\tCore::App().settings().sendSubmitWay());\n\tUi::Emoji::SuggestionsController::Init(\n\t\t_wrap->window(),\n\t\tresult->entity(),\n\t\t&_peer->session());\n\n\tresult->entity()->submits(\n\t) | rpl::on_next([=] {\n\t\tsubmitDescription();\n\t}, result->entity()->lifetime());\n\n\t_controls.description = result->entity();\n\treturn result;\n}\n\nobject_ptr<Ui::RpWidget> Controller::createManageGroupButtons() {\n\tExpects(_wrap != nullptr);\n\n\tauto result = object_ptr<Ui::PaddingWrap<Ui::VerticalLayout>>(\n\t\t_wrap,\n\t\tobject_ptr<Ui::VerticalLayout>(_wrap),\n\t\tst::editPeerBottomButtonsLayoutMargins);\n\t_controls.buttonsLayout = result->entity();\n\n\tfillManageSection();\n\n\treturn result;\n}\n\nobject_ptr<Ui::RpWidget> Controller::createStickersEdit() {\n\tExpects(_wrap != nullptr);\n\n\tconst auto channel = _peer->asChannel();\n\tconst auto bottomSkip = st::editPeerTopButtonsLayoutSkipCustomBottom;\n\n\tauto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t_wrap,\n\t\tobject_ptr<Ui::VerticalLayout>(_wrap));\n\tconst auto container = result->entity();\n\n\tUi::AddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_group_stickers(),\n\t\t{ 0, st::defaultSubsectionTitlePadding.top() - bottomSkip, 0, 0 });\n\n\tAddButtonWithCount(\n\t\tcontainer,\n\t\ttr::lng_group_stickers_add(),\n\t\trpl::single(QString()), //Empty count.\n\t\t[=, controller = _navigation->parentController()] {\n\t\t\tconst auto isEmoji = false;\n\t\t\tcontroller->show(\n\t\t\t\tBox<StickersBox>(controller->uiShow(), channel, isEmoji));\n\t\t},\n\t\t{ &st::menuIconStickers });\n\n\tUi::AddSkip(container, bottomSkip);\n\n\tUi::AddDividerText(\n\t\tcontainer,\n\t\ttr::lng_group_stickers_description());\n\n\tUi::AddSkip(container, bottomSkip);\n\n\treturn result;\n}\n\nbool Controller::canEditInformation() const {\n\tif (_isBot) {\n\t\treturn _peer->asUser()->botInfo->canEditInformation;\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\treturn channel->canEditInformation();\n\t} else if (const auto chat = _peer->asChat()) {\n\t\treturn chat->canEditInformation();\n\t}\n\treturn false;\n}\n\nbool Controller::canEditReactions() const {\n\tif (const auto channel = _peer->asChannel()) {\n\t\treturn channel->amCreator()\n\t\t\t|| (channel->adminRights() & ChatAdminRight::ChangeInfo);\n\t} else if (const auto chat = _peer->asChat()) {\n\t\treturn chat->amCreator()\n\t\t\t|| (chat->adminRights() & ChatAdminRight::ChangeInfo);\n\t}\n\treturn false;\n}\n\nvoid Controller::refreshHistoryVisibility() {\n\tif (!_controls.historyVisibilityWrap) {\n\t\treturn;\n\t}\n\tconst auto withUsername = _typeDataSavedValue\n\t\t&& (_typeDataSavedValue->privacy == Privacy::HasUsername);\n\t_controls.historyVisibilityWrap->toggle(\n\t\t(!withUsername\n\t\t\t&& !_channelHasLocationOriginalValue\n\t\t\t&& (!_discussionLinkSavedValue || !*_discussionLinkSavedValue)\n\t\t\t&& (!_forumSavedValue || !*_forumSavedValue)),\n\t\tanim::type::instant);\n}\n\nvoid Controller::showEditPeerTypeBox(\n\t\tstd::optional<rpl::producer<QString>> error) {\n\tconst auto boxCallback = crl::guard(this, [=](EditPeerTypeData data) {\n\t\t_privacyTypeUpdates.fire({ data.privacy, data.noForwards });\n\t\t_typeDataSavedValue = data;\n\t\trefreshHistoryVisibility();\n\t});\n\t_typeDataSavedValue->hasDiscussionLink\n\t\t= (_discussionLinkSavedValue.value_or(nullptr) != nullptr);\n\tconst auto box = _navigation->parentController()->show(\n\t\tBox<EditPeerTypeBox>(\n\t\t\t_navigation,\n\t\t\t_peer,\n\t\t\t_channelHasLocationOriginalValue,\n\t\t\tboxCallback,\n\t\t\t_typeDataSavedValue,\n\t\t\terror));\n\tbox->boxClosing(\n\t) | rpl::on_next([peer = _peer] {\n\t\tpeer->session().api().usernames().requestToCache(peer);\n\t}, box->lifetime());\n}\n\nvoid Controller::showEditDiscussionLinkBox() {\n\tExpects(_peer->isChannel());\n\n\tif (_forumSavedValue && *_forumSavedValue) {\n\t\tShowForumForDiscussionError(_navigation);\n\t\treturn;\n\t}\n\n\tconst auto box = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tconst auto channel = _peer->asChannel();\n\tconst auto callback = [=](ChannelData *result) {\n\t\tif (*box) {\n\t\t\t(*box)->closeBox();\n\t\t}\n\t\t*_discussionLinkSavedValue = result;\n\t\t_discussionLinkUpdates.fire_copy(result);\n\t\trefreshHistoryVisibility();\n\t\trefreshForumToggleLocked();\n\t};\n\tconst auto canEdit = channel->isBroadcast()\n\t\t? channel->canEditInformation()\n\t\t: (channel->canPinMessages()\n\t\t\t&& (channel->amCreator() || channel->adminRights() != 0)\n\t\t\t&& (!channel->hiddenPreHistory()\n\t\t\t\t|| channel->canEditPreHistoryHidden()));\n\n\tif (const auto chat = *_discussionLinkSavedValue) {\n\t\t*box = _navigation->parentController()->show(EditDiscussionLinkBox(\n\t\t\t_navigation,\n\t\t\tchannel,\n\t\t\tchat,\n\t\t\tcanEdit,\n\t\t\tcallback));\n\t\treturn;\n\t} else if (!canEdit || _discussionLinksRequestId) {\n\t\treturn;\n\t} else if (channel->isMegagroup()) {\n\t\tif (_forumSavedValue\n\t\t\t&& *_forumSavedValue\n\t\t\t&& _discussionLinkOriginalValue) {\n\t\t\tShowForumForDiscussionError(_navigation);\n\t\t} else {\n\t\t\t// Restore original discussion link.\n\t\t\tcallback(_discussionLinkOriginalValue);\n\t\t}\n\t\treturn;\n\t}\n\t_discussionLinksRequestId = _api.request(\n\t\tMTPchannels_GetGroupsForDiscussion()\n\t).done([=](const MTPmessages_Chats &result) {\n\t\t_discussionLinksRequestId = 0;\n\t\tconst auto list = result.match([&](const auto &data) {\n\t\t\treturn data.vchats().v;\n\t\t});\n\t\tauto chats = std::vector<not_null<PeerData*>>();\n\t\tchats.reserve(list.size());\n\t\tfor (const auto &item : list) {\n\t\t\tchats.emplace_back(_peer->owner().processChat(item));\n\t\t}\n\t\t*box = _navigation->parentController()->show(EditDiscussionLinkBox(\n\t\t\t_navigation,\n\t\t\tchannel,\n\t\t\tstd::move(chats),\n\t\t\tcallback));\n\t}).fail([=] {\n\t\t_discussionLinksRequestId = 0;\n\t}).send();\n}\n\nvoid Controller::showEditDirectMessagesBox() {\n\tExpects(_peer->isBroadcast());\n\tExpects(_starsPerDirectMessageSavedValue.has_value());\n\n\tconst auto stars = _starsPerDirectMessageSavedValue->current();\n\t_navigation->parentController()->show(Box(\n\t\tEditDirectMessagesPriceBox,\n\t\t_peer->asChannel(),\n\t\t(stars >= 0) ? stars : std::optional<int>(),\n\t\t[=](std::optional<int> value) {\n\t\t\t*_starsPerDirectMessageSavedValue = value.value_or(-1);\n\t\t}));\n}\n\nvoid Controller::fillPrivacyTypeButton() {\n\tExpects(_controls.buttonsLayout != nullptr);\n\n\t// Create Privacy Button.\n\tconst auto hasLocation = _peer->isChannel()\n\t\t&& _peer->asChannel()->hasLocation();\n\t_typeDataSavedValue = EditPeerTypeData{\n\t\t.privacy = ((_peer->isChannel()\n\t\t\t&& _peer->asChannel()->hasUsername())\n\t\t\t? Privacy::HasUsername\n\t\t\t: Privacy::NoUsername),\n\t\t.username = (_peer->isChannel()\n\t\t\t? _peer->asChannel()->editableUsername()\n\t\t\t: QString()),\n\t\t.usernamesOrder = (_peer->isChannel()\n\t\t\t? _peer->asChannel()->usernames()\n\t\t\t: std::vector<QString>()),\n\t\t.noForwards = !_peer->allowsForwarding(),\n\t\t.joinToWrite = (_peer->isMegagroup()\n\t\t\t&& _peer->asChannel()->joinToWrite()),\n\t\t.requestToJoin = (_peer->isMegagroup()\n\t\t\t&& _peer->asChannel()->requestToJoin()),\n\t};\n\tconst auto isGroup = (_peer->isChat() || _peer->isMegagroup());\n\tAddButtonWithText(\n\t\t_controls.buttonsLayout,\n\t\t(hasLocation\n\t\t\t? tr::lng_manage_peer_link_type\n\t\t\t: isGroup\n\t\t\t? tr::lng_manage_peer_group_type\n\t\t\t: tr::lng_manage_peer_channel_type)(),\n\t\t_privacyTypeUpdates.events(\n\t\t) | rpl::map([=](PrivacyAndForwards data) {\n\t\t\tconst auto flag = data.privacy;\n\t\t\tif (flag == Privacy::HasUsername) {\n\t\t\t\t_peer->session().api().usernames().requestToCache(_peer);\n\t\t\t}\n\t\t\treturn (flag == Privacy::HasUsername)\n\t\t\t\t? (hasLocation\n\t\t\t\t\t? tr::lng_manage_peer_link_permanent\n\t\t\t\t\t: isGroup\n\t\t\t\t\t? tr::lng_manage_public_group_title\n\t\t\t\t\t: tr::lng_manage_public_peer_title)()\n\t\t\t\t: (hasLocation\n\t\t\t\t\t? tr::lng_manage_peer_link_invite\n\t\t\t\t\t: ((!data.noForwards) && isGroup)\n\t\t\t\t\t? tr::lng_manage_private_group_title\n\t\t\t\t\t: ((!data.noForwards) && !isGroup)\n\t\t\t\t\t? tr::lng_manage_private_peer_title\n\t\t\t\t\t: isGroup\n\t\t\t\t\t? tr::lng_manage_private_group_noforwards_title\n\t\t\t\t\t: tr::lng_manage_private_peer_noforwards_title)();\n\t\t}) | rpl::flatten_latest(),\n\t\t[=] { showEditPeerTypeBox(); },\n\t\t{ &st::menuIconCustomize });\n\n\t_privacyTypeUpdates.fire_copy({\n\t\t_typeDataSavedValue->privacy,\n\t\t_typeDataSavedValue->noForwards,\n\t});\n}\n\nvoid Controller::fillDiscussionLinkButton() {\n\tExpects(_controls.buttonsLayout != nullptr);\n\n\t_discussionLinkSavedValue\n\t\t= _discussionLinkOriginalValue\n\t\t= (_peer->isChannel()\n\t\t\t? _peer->asChannel()->discussionLink()\n\t\t\t: nullptr);\n\n\tconst auto isGroup = (_peer->isChat() || _peer->isMegagroup());\n\tauto text = !isGroup\n\t\t? tr::lng_manage_discussion_group()\n\t\t: rpl::combine(\n\t\t\ttr::lng_manage_linked_channel(),\n\t\t\ttr::lng_manage_linked_channel_restore(),\n\t\t\t_discussionLinkUpdates.events()\n\t\t) | rpl::map([=](\n\t\t\t\tconst QString &edit,\n\t\t\t\tconst QString &restore,\n\t\t\t\tChannelData *chat) {\n\t\t\treturn chat ? edit : restore;\n\t\t});\n\tauto label = isGroup\n\t\t? _discussionLinkUpdates.events(\n\t\t) | rpl::map([](ChannelData *chat) {\n\t\t\treturn chat ? chat->name() : QString();\n\t\t}) | rpl::type_erased\n\t\t: rpl::combine(\n\t\t\ttr::lng_manage_discussion_group_add(),\n\t\t\t_discussionLinkUpdates.events()\n\t\t) | rpl::map([=](const QString &add, ChannelData *chat) {\n\t\t\treturn chat ? chat->name() : add;\n\t\t}) | rpl::type_erased;\n\tAddButtonWithText(\n\t\t_controls.buttonsLayout,\n\t\tstd::move(text),\n\t\tstd::move(label),\n\t\t[=] { showEditDiscussionLinkBox(); },\n\t\t{ isGroup ? &st::menuIconChannel : &st::menuIconGroups });\n\t_discussionLinkUpdates.fire_copy(*_discussionLinkSavedValue);\n}\n\nvoid Controller::fillDirectMessagesButton() {\n\tExpects(_controls.buttonsLayout != nullptr);\n\n\tif (!_peer->isBroadcast() || !_peer->asChannel()->canEditInformation()) {\n\t\treturn;\n\t}\n\n\tconst auto perMessage = CurrentPricePerDirectMessage(_peer->asChannel());\n\t_starsPerDirectMessageSavedValue = rpl::variable<int>(perMessage);\n\n\tauto label = _starsPerDirectMessageSavedValue->value(\n\t) | rpl::map([](int starsPerMessage) {\n\t\treturn (starsPerMessage < 0)\n\t\t\t? tr::lng_manage_monoforum_off(tr::marked)\n\t\t\t: !starsPerMessage\n\t\t\t? tr::lng_manage_monoforum_free(tr::marked)\n\t\t\t: rpl::single(Ui::Text::IconEmoji(\n\t\t\t\t&st::starIconEmojiColored\n\t\t\t).append(' ').append(\n\t\t\t\tLang::FormatCreditsAmountDecimal(\n\t\t\t\t\tCreditsAmount{ starsPerMessage })));\n\t}) | rpl::flatten_latest();\n\tAddButtonWithText(\n\t\t_controls.buttonsLayout,\n\t\ttr::lng_manage_monoforum(),\n\t\tstd::move(label),\n\t\t[=] { showEditDirectMessagesBox(); },\n\t\t{ .icon = &st::menuIconChats, .newBadge = true });\n}\n//\n//void Controller::fillInviteLinkButton() {\n//\tExpects(_controls.buttonsLayout != nullptr);\n//\n//\tconst auto buttonCallback = [=] {\n//\t\tUi::show(Box<EditPeerTypeBox>(_peer), Ui::LayerOption::KeepOther);\n//\t};\n//\n//\tAddButtonWithText(\n//\t\t_controls.buttonsLayout,\n//\t\ttr::lng_profile_invite_link_section(),\n//\t\trpl::single(QString()), //Empty text.\n//\t\tbuttonCallback);\n//}\n\nvoid Controller::fillForumButton() {\n\tExpects(_controls.buttonsLayout != nullptr);\n\n\t_forumSavedValue = _peer->isForum();\n\t_forumTabsSavedValue = !_peer->isChannel()\n\t\t|| !_peer->isForum()\n\t\t|| _peer->useSubsectionTabs();\n\n\tconst auto changes = std::make_shared<rpl::event_stream<>>();\n\tconst auto label = [=] {\n\t\treturn !*_forumSavedValue\n\t\t\t? tr::lng_manage_monoforum_off(tr::now)\n\t\t\t: *_forumTabsSavedValue\n\t\t\t? tr::lng_edit_topics_tabs(tr::now)\n\t\t\t: tr::lng_edit_topics_list(tr::now);\n\t};\n\tconst auto button = _controls.forumToggle = _controls.buttonsLayout->add(\n\t\tEditPeerInfoBox::CreateButton(\n\t\t\t_controls.buttonsLayout,\n\t\t\ttr::lng_forum_topics_switch(),\n\t\t\tchanges->events_starting_with({}) | rpl::map(label),\n\t\t\t[] {},\n\t\t\tst::manageGroupTopicsButton,\n\t\t\t{ .icon = &st::menuIconTopics, .newBadge = true }));\n\n\tbutton->setClickedCallback(crl::guard(this, [=] {\n\t\tif (!*_forumSavedValue && _controls.forumToggleLocked) {\n\t\t\tif (_discussionLinkSavedValue && *_discussionLinkSavedValue) {\n\t\t\t\tShowForumForDiscussionError(_navigation);\n\t\t\t} else {\n\t\t\t\t_navigation->showToast(\n\t\t\t\t\ttr::lng_forum_topics_not_enough(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tEnableForumMinMembers(_peer),\n\t\t\t\t\t\ttr::rich));\n\t\t\t}\n\t\t} else {\n\t\t\t_navigation->uiShow()->show(Box(\n\t\t\t\tUi::ToggleTopicsBox,\n\t\t\t\t*_forumSavedValue,\n\t\t\t\t*_forumTabsSavedValue,\n\t\t\t\tcrl::guard(this, [=](bool topics, bool topicsTabs) {\n\t\t\t\t\t_forumSavedValue = topics;\n\t\t\t\t\t_forumTabsSavedValue = !topics || topicsTabs;\n\t\t\t\t\tif (topics) {\n\t\t\t\t\t\t_savingData.hiddenPreHistory = false;\n\t\t\t\t\t}\n\t\t\t\t\tchanges->fire({});\n\t\t\t\t\trefreshHistoryVisibility();\n\t\t\t\t})));\n\t\t}\n\t}));\n\trefreshForumToggleLocked();\n}\n\nvoid Controller::refreshForumToggleLocked() {\n\tif (!_controls.forumToggle) {\n\t\treturn;\n\t}\n\tconst auto limit = EnableForumMinMembers(_peer);\n\tconst auto chat = _peer->asChat();\n\tconst auto channel = _peer->asChannel();\n\tconst auto notenough = !_peer->isForum()\n\t\t&& ((chat ? chat->count : channel->membersCount()) < limit);\n\tconst auto linked = _discussionLinkSavedValue\n\t\t&& *_discussionLinkSavedValue;\n\tconst auto locked = _controls.forumToggleLocked = notenough || linked;\n\t_controls.forumToggle->setToggleLocked(locked);\n}\n\nvoid Controller::fillColorIndexButton() {\n\tExpects(_controls.buttonsLayout != nullptr);\n\n\tAddPeerColorButton(\n\t\t_controls.buttonsLayout,\n\t\t_navigation->uiShow(),\n\t\t_peer,\n\t\tst::managePeerColorsButton);\n}\n\nvoid Controller::fillAutoTranslateButton() {\n\tExpects(_controls.buttonsLayout != nullptr);\n\n\tconst auto channel = _peer->asBroadcast();\n\tif (!channel) {\n\t\treturn;\n\t}\n\n\tconst auto requiredLevel = Data::LevelLimits(&channel->session())\n\t\t.channelAutoTranslateLevelMin();\n\tconst auto autotranslate = _controls.buttonsLayout->add(\n\t\tEditPeerInfoBox::CreateButton(\n\t\t\t_controls.buttonsLayout,\n\t\t\ttr::lng_edit_autotranslate(),\n\t\t\trpl::single(QString()),\n\t\t\t[] {},\n\t\t\tst::manageGroupTopicsButton,\n\t\t\t{ &st::menuIconTranslate }));\n\tstruct State {\n\t\trpl::event_stream<bool> toggled;\n\t\trpl::variable<bool> isLocked = false;\n\t};\n\tconst auto state = autotranslate->lifetime().make_state<State>();\n\tautotranslate->toggleOn(rpl::single(\n\t\tchannel->autoTranslation()\n\t) | rpl::then(state->toggled.events()));\n\tstate->isLocked = (channel->levelHint() < requiredLevel);\n\tconst auto reason = Ui::AskBoostReason{\n\t\t.data = Ui::AskBoostAutotranslate{ .requiredLevel = requiredLevel },\n\t};\n\n\tstate->isLocked.value() | rpl::on_next([=](bool locked) {\n\t\tautotranslate->setToggleLocked(locked);\n\t}, autotranslate->lifetime());\n\n\tautotranslate->toggledChanges(\n\t) | rpl::on_next([=](bool value) {\n\t\tif (!state->isLocked.current()) {\n\t\t\t_autotranslateSavedValue = value;\n\t\t} else if (value) {\n\t\t\tstate->toggled.fire(false);\n\t\t\tauto weak = base::make_weak(autotranslate);\n\t\t\tCheckBoostLevel(\n\t\t\t\t_navigation->uiShow(),\n\t\t\t\t_peer,\n\t\t\t\t[=](int level) {\n\t\t\t\t\tif (weak.get()) {\n\t\t\t\t\t\tstate->isLocked = (level < requiredLevel);\n\t\t\t\t\t}\n\t\t\t\t\treturn (level < requiredLevel)\n\t\t\t\t\t\t? std::make_optional(reason)\n\t\t\t\t\t\t: std::nullopt;\n\t\t\t\t},\n\t\t\t\t[] {});\n\t\t}\n\t}, autotranslate->lifetime());\n\n\tautotranslate->toggledValue(\n\t) | rpl::on_next([=](bool toggled) {\n\t\t_autotranslateSavedValue = toggled;\n\t}, _controls.buttonsLayout->lifetime());\n}\n\nvoid Controller::fillSignaturesButton() {\n\tExpects(_controls.buttonsLayout != nullptr);\n\n\tconst auto channel = _peer->asChannel();\n\tif (!channel) {\n\t\treturn;\n\t}\n\n\tconst auto signs = AddButtonWithText(\n\t\t_controls.buttonsLayout,\n\t\ttr::lng_edit_sign_messages(),\n\t\trpl::single(QString()),\n\t\t[] {},\n\t\t{ &st::menuIconSigned }\n\t)->toggleOn(rpl::single(channel->addsSignature()));\n\n\tconst auto profiles = _controls.buttonsLayout->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\t_controls.buttonsLayout,\n\t\t\tEditPeerInfoBox::CreateButton(\n\t\t\t\t_controls.buttonsLayout,\n\t\t\t\ttr::lng_edit_sign_profiles(),\n\t\t\t\trpl::single(QString()),\n\t\t\t\t[] {},\n\t\t\t\tst::manageGroupTopButtonWithText,\n\t\t\t\t{ &st::menuIconProfile })));\n\tprofiles->toggleOn(signs->toggledValue());\n\tprofiles->finishAnimating();\n\n\tprofiles->entity()->toggleOn(rpl::single(\n\t\tchannel->addsSignature() && channel->signatureProfiles()\n\t))->toggledValue(\n\t) | rpl::on_next([=](bool toggled) {\n\t\t_signatureProfilesSavedValue = toggled;\n\t}, profiles->entity()->lifetime());\n\n\tsigns->toggledValue(\n\t) | rpl::on_next([=](bool toggled) {\n\t\t_signaturesSavedValue = toggled;\n\t\tif (!toggled) {\n\t\t\t_signatureProfilesSavedValue = false;\n\t\t}\n\t}, _controls.buttonsLayout->lifetime());\n\n\tUi::AddSkip(_controls.buttonsLayout);\n\tUi::AddDividerText(\n\t\t_controls.buttonsLayout,\n\t\trpl::conditional(\n\t\t\tsigns->toggledValue(),\n\t\t\ttr::lng_edit_sign_profiles_about(tr::marked),\n\t\t\ttr::lng_edit_sign_messages_about(tr::marked)));\n\tUi::AddSkip(_controls.buttonsLayout);\n}\n\nvoid Controller::fillHistoryVisibilityButton() {\n\tExpects(_controls.buttonsLayout != nullptr);\n\n\tconst auto wrapLayout = _controls.buttonsLayout->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_controls.buttonsLayout,\n\t\t\tobject_ptr<Ui::VerticalLayout>(_controls.buttonsLayout),\n\t\t\tst::boxOptionListPadding)); // Empty margins.\n\t_controls.historyVisibilityWrap = wrapLayout;\n\n\tconst auto channel = _peer->asChannel();\n\tconst auto container = wrapLayout->entity();\n\n\t_historyVisibilitySavedValue = (!channel || channel->hiddenPreHistory())\n\t\t? HistoryVisibility::Hidden\n\t\t: HistoryVisibility::Visible;\n\t_channelHasLocationOriginalValue = channel && channel->hasLocation();\n\n\tconst auto updateHistoryVisibility\n\t\t= std::make_shared<rpl::event_stream<HistoryVisibility>>();\n\n\tconst auto boxCallback = crl::guard(this, [=](HistoryVisibility checked) {\n\t\tupdateHistoryVisibility->fire(std::move(checked));\n\t\t_historyVisibilitySavedValue = checked;\n\t});\n\tconst auto buttonCallback = [=] {\n\t\t_peer->updateFull();\n\t\tconst auto canEdit = [&] {\n\t\t\tif (const auto chat = _peer->asChat()) {\n\t\t\t\treturn chat->canEditPreHistoryHidden();\n\t\t\t} else if (const auto channel = _peer->asChannel()) {\n\t\t\t\treturn channel->canEditPreHistoryHidden();\n\t\t\t}\n\t\t\tUnexpected(\"User in HistoryVisibilityEdit.\");\n\t\t}();\n\t\tif (!canEdit) {\n\t\t\treturn;\n\t\t}\n\t\t_navigation->parentController()->show(Box(\n\t\t\tEditPeerHistoryVisibilityBox,\n\t\t\t_peer->isChat(),\n\t\t\tboxCallback,\n\t\t\t*_historyVisibilitySavedValue));\n\t};\n\tAddButtonWithText(\n\t\tcontainer,\n\t\ttr::lng_manage_history_visibility_title(),\n\t\tupdateHistoryVisibility->events(\n\t\t) | rpl::map([](HistoryVisibility flag) {\n\t\t\treturn (HistoryVisibility::Visible == flag\n\t\t\t\t? tr::lng_manage_history_visibility_shown\n\t\t\t\t: tr::lng_manage_history_visibility_hidden)();\n\t\t}) | rpl::flatten_latest(),\n\t\tbuttonCallback,\n\t\t{ &st::menuIconChatBubble });\n\n\tupdateHistoryVisibility->fire_copy(*_historyVisibilitySavedValue);\n\n\trefreshHistoryVisibility();\n}\n\nvoid Controller::fillManageSection() {\n\tExpects(_controls.buttonsLayout != nullptr);\n\n\tif (_isBot) {\n\t\tconst auto &container = _controls.buttonsLayout;\n\n\t\t::AddSkip(container, 0);\n\t\tfillBotUsernamesButton();\n\t\tfillBotCurrencyButton();\n\t\tfillBotCreditsButton();\n\t\tfillBotAffiliateProgram();\n\t\tfillBotEditIntroButton();\n\t\tfillBotEditCommandsButton();\n\t\tfillBotEditSettingsButton();\n\t\tUi::AddSkip(\n\t\t\tcontainer,\n\t\t\tst::editPeerTopButtonsLayoutSkipCustomBottom);\n\t\tcontainer->add(object_ptr<Ui::DividerLabel>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_manage_peer_bot_about(\n\t\t\t\t\tlt_bot,\n\t\t\t\t\trpl::single(tr::link(\n\t\t\t\t\t\t'@' + kBotManagerUsername.utf16(),\n\t\t\t\t\t\t_peer->session().createInternalLinkFull(\n\t\t\t\t\t\t\tkBotManagerUsername.utf16()))),\n\t\t\t\t\ttr::rich),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::defaultBoxDividerLabelPadding));\n\t\tfillBotVerifyAccounts();\n\t\treturn;\n\t}\n\n\tconst auto chat = _peer->asChat();\n\tconst auto channel = _peer->asChannel();\n\tconst auto isChannel = (!chat);\n\tif (!chat && !channel) {\n\t\treturn;\n\t}\n\n\tconst auto canEditType = isChannel\n\t\t? channel->amCreator()\n\t\t: chat->amCreator();\n\tconst auto canEditSignatures = isChannel\n\t\t&& channel->canEditSignatures()\n\t\t&& !channel->isMegagroup();\n\tconst auto canEditAutoTranslate = isChannel\n\t\t&& channel->canEditAutoTranslate();\n\tconst auto canEditPreHistoryHidden = isChannel\n\t\t? channel->canEditPreHistoryHidden()\n\t\t: chat->canEditPreHistoryHidden();\n\tconst auto canEditForum = isChannel\n\t\t? (channel->isMegagroup() && channel->amCreator())\n\t\t: chat->amCreator();\n\tconst auto canEditPermissions = isChannel\n\t\t? channel->canEditPermissions()\n\t\t: chat->canEditPermissions();\n\tconst auto canEditInviteLinks = isChannel\n\t\t? channel->canHaveInviteLink()\n\t\t: chat->canHaveInviteLink();\n\tconst auto canViewAdmins = isChannel\n\t\t? channel->canViewAdmins()\n\t\t: chat->amIn();\n\tconst auto canViewMembers = isChannel\n\t\t? channel->canViewMembers()\n\t\t: chat->amIn();\n\tconst auto canViewKicked = isChannel\n\t\t&& (channel->isMegagroup()\n\t\t\t? (channel->isBroadcast() || channel->isGigagroup())\n\t\t\t: true);\n\tconst auto hasRecentActions = isChannel\n\t\t&& (channel->hasAdminRights() || channel->amCreator());\n\tconst auto hasStarRef = Info::BotStarRef::Join::Allowed(_peer)\n\t\t&& isChannel\n\t\t&& channel->canPostMessages();\n\tconst auto canEditStickers = isChannel && channel->canEditStickers();\n\tconst auto canDeleteChannel = isChannel && channel->canDelete();\n\tconst auto canEditColorIndex = isChannel && channel->canEditEmoji();\n\tconst auto canViewOrEditDiscussionLink = isChannel\n\t\t&& (channel->discussionLink()\n\t\t\t|| (channel->isBroadcast() && channel->canEditInformation()));\n\tconst auto canEditDirectMessages = isChannel\n\t\t&& (channel->isBroadcast() && channel->canEditInformation());\n\n\t::AddSkip(_controls.buttonsLayout, 0);\n\n\tif (canEditType) {\n\t\tfillPrivacyTypeButton();\n\t//} else if (canEditInviteLinks) {\n\t//\tfillInviteLinkButton();\n\t}\n\tif (canViewOrEditDiscussionLink) {\n\t\tfillDiscussionLinkButton();\n\t}\n\tif (canEditDirectMessages) {\n\t\tfillDirectMessagesButton();\n\t}\n\tif (canEditPreHistoryHidden) {\n\t\tfillHistoryVisibilityButton();\n\t}\n\tif (canEditForum) {\n\t\tfillForumButton();\n\t}\n\tif (canEditColorIndex) {\n\t\tfillColorIndexButton();\n\t}\n\tif (canEditAutoTranslate) {\n\t\tfillAutoTranslateButton();\n\t}\n\tif (canEditSignatures) {\n\t\tfillSignaturesButton();\n\t} else if (canEditPreHistoryHidden\n\t\t|| canEditForum\n\t\t|| canEditColorIndex\n\t\t//|| canEditInviteLinks\n\t\t|| canViewOrEditDiscussionLink\n\t\t|| canEditType) {\n\t\t::AddSkip(_controls.buttonsLayout);\n\t}\n\n\tif (canEditReactions()) {\n\t\tauto allowedReactions = Info::Profile::MigratedOrMeValue(\n\t\t\t_peer\n\t\t) | rpl::map([=](not_null<PeerData*> peer) {\n\t\t\treturn peer->session().changes().peerFlagsValue(\n\t\t\t\tpeer,\n\t\t\t\tData::PeerUpdate::Flag::Reactions\n\t\t\t) | rpl::map([=] {\n\t\t\t\treturn Data::PeerAllowedReactions(peer);\n\t\t\t});\n\t\t}) | rpl::flatten_latest();\n\t\tauto label = std::move(\n\t\t\tallowedReactions\n\t\t) | rpl::map([=](const Data::AllowedReactions &allowed) {\n\t\t\tconst auto some = int(allowed.some.size());\n\t\t\treturn (allowed.type != Data::AllowedReactionsType::Some)\n\t\t\t\t? tr::lng_manage_peer_reactions_on(tr::now)\n\t\t\t\t: some\n\t\t\t\t? QString::number(some)\n\t\t\t\t: allowed.paidEnabled\n\t\t\t\t? QString::number(1)\n\t\t\t\t: tr::lng_manage_peer_reactions_off(tr::now);\n\t\t});\n\t\tAddButtonWithCount(\n\t\t\t_controls.buttonsLayout,\n\t\t\ttr::lng_manage_peer_reactions(),\n\t\t\tstd::move(label),\n\t\t\t[=] { editReactions(); },\n\t\t\t{ &st::menuIconGroupReactions });\n\t}\n\tif (canEditPermissions) {\n\t\tAddButtonWithCount(\n\t\t\t_controls.buttonsLayout,\n\t\t\ttr::lng_manage_peer_permissions(),\n\t\t\tInfo::Profile::MigratedOrMeValue(\n\t\t\t\t_peer\n\t\t\t) | rpl::map([=](not_null<PeerData*> peer) {\n\t\t\t\treturn Info::Profile::RestrictionsCountValue(\n\t\t\t\t\tpeer\n\t\t\t\t) | rpl::map([=](int count) {\n\t\t\t\t\treturn QString::number(count)\n\t\t\t\t\t\t+ QString(\"/\")\n\t\t\t\t\t\t+ QString::number(int(Data::ListOfRestrictions(\n\t\t\t\t\t\t\t{ .isForum = peer->isForum() }).size()));\n\t\t\t\t});\n\t\t\t}) | rpl::flatten_latest(),\n\t\t\t[=] { ShowEditPermissions(_navigation, _peer); },\n\t\t\t{ &st::menuIconPermissions });\n\t}\n\tif (canEditInviteLinks) {\n\t\tauto count = Info::Profile::MigratedOrMeValue(\n\t\t\t_peer\n\t\t) | rpl::map([=](not_null<PeerData*> peer) {\n\t\t\tpeer->session().api().inviteLinks().requestMyLinks(peer);\n\t\t\treturn peer->session().changes().peerUpdates(\n\t\t\t\tpeer,\n\t\t\t\tData::PeerUpdate::Flag::InviteLinks\n\t\t\t) | rpl::map([=] {\n\t\t\t\treturn peer->session().api().inviteLinks().myLinks(\n\t\t\t\t\tpeer).count;\n\t\t\t});\n\t\t}) | rpl::flatten_latest(\n\t\t) | rpl::start_spawning(_controls.buttonsLayout->lifetime());\n\n\t\tconst auto wrap = _controls.buttonsLayout->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\t_controls.buttonsLayout,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(\n\t\t\t\t\t_controls.buttonsLayout)));\n\t\tAddButtonWithCount(\n\t\t\twrap->entity(),\n\t\t\ttr::lng_manage_peer_invite_links(),\n\t\t\trpl::duplicate(count) | ToPositiveNumberString(),\n\t\t\t[=] {\n\t\t\t\t_navigation->parentController()->show(Box(\n\t\t\t\t\tManageInviteLinksBox,\n\t\t\t\t\t_peer,\n\t\t\t\t\t_peer->session().user(),\n\t\t\t\t\t0,\n\t\t\t\t\t0));\n\t\t\t},\n\t\t\t{ &st::menuIconLinks });\n\t\twrap->toggle(true, anim::type::instant);\n\t}\n\tif (canViewAdmins) {\n\t\tAddButtonWithCount(\n\t\t\t_controls.buttonsLayout,\n\t\t\ttr::lng_manage_peer_administrators(),\n\t\t\tInfo::Profile::MigratedOrMeValue(\n\t\t\t\t_peer\n\t\t\t) | rpl::map(\n\t\t\t\tInfo::Profile::AdminsCountValue\n\t\t\t) | rpl::flatten_latest(\n\t\t\t) | ToPositiveNumberString(),\n\t\t\t[=] {\n\t\t\t\tParticipantsBoxController::Start(\n\t\t\t\t\t_navigation,\n\t\t\t\t\t_peer,\n\t\t\t\t\tParticipantsBoxController::Role::Admins);\n\t\t\t},\n\t\t\t{ &st::menuIconAdmin });\n\t}\n\tif (canViewMembers) {\n\t\tAddButtonWithCount(\n\t\t\t_controls.buttonsLayout,\n\t\t\t(_isGroup\n\t\t\t\t? tr::lng_manage_peer_members()\n\t\t\t\t: tr::lng_manage_peer_subscribers()),\n\t\t\tInfo::Profile::MigratedOrMeValue(\n\t\t\t\t_peer\n\t\t\t) | rpl::map(\n\t\t\t\tInfo::Profile::MembersCountValue\n\t\t\t) | rpl::flatten_latest(\n\t\t\t) | ToPositiveNumberString(),\n\t\t\t[=] {\n\t\t\t\tParticipantsBoxController::Start(\n\t\t\t\t\t_navigation,\n\t\t\t\t\t_peer,\n\t\t\t\t\tParticipantsBoxController::Role::Members);\n\t\t\t},\n\t\t\t{ &st::menuIconGroups });\n\t}\n\n\tfillPendingRequestsButton();\n\n\tif (canViewKicked) {\n\t\tAddButtonWithCount(\n\t\t\t_controls.buttonsLayout,\n\t\t\ttr::lng_manage_peer_removed_users(),\n\t\t\tInfo::Profile::KickedCountValue(channel)\n\t\t\t| ToPositiveNumberString(),\n\t\t\t[=] {\n\t\t\t\tParticipantsBoxController::Start(\n\t\t\t\t\t_navigation,\n\t\t\t\t\t_peer,\n\t\t\t\t\tParticipantsBoxController::Role::Kicked);\n\t\t\t},\n\t\t\t{ &st::menuIconRemove });\n\t}\n\tif (hasRecentActions) {\n\t\tauto callback = [=] {\n\t\t\t_navigation->showSection(\n\t\t\t\tstd::make_shared<AdminLog::SectionMemento>(channel));\n\t\t};\n\t\tAddButtonWithCount(\n\t\t\t_controls.buttonsLayout,\n\t\t\ttr::lng_manage_peer_recent_actions(),\n\t\t\trpl::single(QString()), // Empty count.\n\t\t\tstd::move(callback),\n\t\t\t{ &st::menuIconGroupLog });\n\t}\n\tif (hasStarRef) {\n\t\tauto callback = [=] {\n\t\t\t_navigation->showSection(Info::BotStarRef::Join::Make(_peer));\n\t\t};\n\t\tAddButtonWithCount(\n\t\t\t_controls.buttonsLayout,\n\t\t\ttr::lng_manage_peer_star_ref(),\n\t\t\trpl::single(QString()), // Empty count.\n\t\t\tstd::move(callback),\n\t\t\t{ .icon = &st::menuIconStarRefShare, .newBadge = true });\n\t}\n\n\tif (canEditStickers || canDeleteChannel) {\n\t\t::AddSkip(_controls.buttonsLayout);\n\t}\n\n\tif (canEditStickers) {\n\t\t_controls.buttonsLayout->add(createStickersEdit());\n\t}\n\n\tif (canDeleteChannel) {\n\t\tAddButtonDelete(\n\t\t\t_controls.buttonsLayout,\n\t\t\t(_isGroup\n\t\t\t\t? tr::lng_profile_delete_group\n\t\t\t\t: tr::lng_profile_delete_channel)(),\n\t\t\t[=]{ deleteWithConfirmation(); }\n\t\t);\n\t}\n\n\tif (canEditStickers || canDeleteChannel) {\n\t\t::AddSkip(_controls.buttonsLayout);\n\t}\n}\n\nvoid Controller::editReactions() {\n\tconst auto done = [=](const Data::AllowedReactions &chosen) {\n\t\tSaveAllowedReactions(_peer, chosen);\n\t};\n\tif (!_peer->isBroadcast()) {\n\t\t_navigation->uiShow()->show(Box(\n\t\t\tEditAllowedReactionsBox,\n\t\t\tEditAllowedReactionsArgs{\n\t\t\t\t.navigation = _navigation,\n\t\t\t\t.isGroup = true,\n\t\t\t\t.list = _navigation->session().data().reactions().list(\n\t\t\t\t\tData::Reactions::Type::Active),\n\t\t\t\t.allowed = Data::PeerAllowedReactions(_peer),\n\t\t\t\t.save = done,\n\t\t\t}));\n\t\treturn;\n\t}\n\tif (_controls.levelRequested) {\n\t\treturn;\n\t}\n\t_controls.levelRequested = true;\n\t_api.request(MTPpremium_GetBoostsStatus(\n\t\t_peer->input()\n\t)).done([=](const MTPpremium_BoostsStatus &result) {\n\t\t_controls.levelRequested = false;\n\t\tif (const auto channel = _peer->asChannel()) {\n\t\t\tchannel->updateLevelHint(result.data().vlevel().v);\n\t\t}\n\t\tconst auto link = qs(result.data().vboost_url());\n\t\tconst auto weak = base::make_weak(_navigation->parentController());\n\t\tauto counters = ParseBoostCounters(result);\n\t\tcounters.mine = 0; // Don't show current level as just-reached.\n\t\tconst auto askForBoosts = [=](int required) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tconst auto openStatistics = [=, peer = _peer] {\n\t\t\t\t\tstrong->showSection(Info::Boosts::Make(peer));\n\t\t\t\t};\n\t\t\t\tstrong->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{\n\t\t\t\t\t.link = link,\n\t\t\t\t\t.boost = counters,\n\t\t\t\t\t.features = (_peer->isChannel()\n\t\t\t\t\t\t? LookupBoostFeatures(_peer->asChannel())\n\t\t\t\t\t\t: Ui::BoostFeatures()),\n\t\t\t\t\t.reason = { Ui::AskBoostCustomReactions{ required } },\n\t\t\t\t\t.group = !_peer->isBroadcast(),\n\t\t\t\t}, openStatistics, nullptr));\n\t\t\t}\n\t\t};\n\t\t_navigation->uiShow()->show(Box(\n\t\t\tEditAllowedReactionsBox,\n\t\t\tEditAllowedReactionsArgs{\n\t\t\t\t.navigation = _navigation,\n\t\t\t\t.allowedCustomReactions = counters.level,\n\t\t\t\t.customReactionsHardLimit = Data::PremiumLimits(\n\t\t\t\t\t&_peer->session()).maxBoostLevel(),\n\t\t\t\t.list = _navigation->session().data().reactions().list(\n\t\t\t\t\tData::Reactions::Type::Active),\n\t\t\t\t.allowed = Data::PeerAllowedReactions(_peer),\n\t\t\t\t.askForBoosts = askForBoosts,\n\t\t\t\t.save = done,\n\t\t\t}));\n\t}).send();\n}\n\nvoid Controller::fillPendingRequestsButton() {\n\tauto pendingRequestsCount = Info::Profile::MigratedOrMeValue(\n\t\t_peer\n\t) | rpl::map(\n\t\tInfo::Profile::PendingRequestsCountValue\n\t) | rpl::flatten_latest(\n\t) | rpl::start_spawning(_controls.buttonsLayout->lifetime());\n\tconst auto wrap = _controls.buttonsLayout->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_controls.buttonsLayout,\n\t\t\tobject_ptr<Ui::VerticalLayout>(\n\t\t\t\t_controls.buttonsLayout)));\n\tAddButtonWithCount(\n\t\twrap->entity(),\n\t\t(_isGroup\n\t\t\t? tr::lng_manage_peer_requests()\n\t\t\t: tr::lng_manage_peer_requests_channel()),\n\t\trpl::duplicate(pendingRequestsCount) | ToPositiveNumberString(),\n\t\t[=] { RequestsBoxController::Start(_navigation, _peer); },\n\t\t{ &st::menuIconInvite });\n\tstd::move(\n\t\tpendingRequestsCount\n\t) | rpl::on_next([=](int count) {\n\t\twrap->toggle(count > 0, anim::type::instant);\n\t}, wrap->lifetime());\n}\n\nvoid Controller::fillBotUsernamesButton() {\n\tExpects(_isBot);\n\n\tconst auto user = _peer->asUser();\n\n\tauto localUsernames = rpl::single(\n\t\tuser->usernames()\n\t) | rpl::map([](const std::vector<QString> &usernames) {\n\t\treturn ranges::views::all(\n\t\t\tusernames\n\t\t) | ranges::views::transform([](const QString &u) {\n\t\t\treturn Data::Username{ u };\n\t\t}) | ranges::to_vector;\n\t});\n\tauto usernamesValue = std::move(\n\t\tlocalUsernames\n\t) | rpl::then(\n\t\t_peer->session().api().usernames().loadUsernames(_peer)\n\t);\n\tauto rightLabel = rpl::duplicate(\n\t\tusernamesValue\n\t) | rpl::map([=](const Data::Usernames &usernames) {\n\t\tif (usernames.size() <= 1) {\n\t\t\treturn user->session().createInternalLink(user->username());\n\t\t} else {\n\t\t\tconst auto active = ranges::count_if(\n\t\t\t\tusernames,\n\t\t\t\t[](const Data::Username &u) { return u.active; });\n\t\t\treturn u\"%1/%2\"_q.arg(active).arg(usernames.size());\n\t\t}\n\t});\n\tauto leftLabel = std::move(\n\t\tusernamesValue\n\t) | rpl::map([=](const Data::Usernames &usernames) {\n\t\treturn (usernames.size() <= 1)\n\t\t\t? tr::lng_manage_peer_bot_public_link()\n\t\t\t: tr::lng_manage_peer_bot_public_links();\n\t}) | rpl::flatten_latest();\n\n\t_controls.buttonsLayout->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_controls.buttonsLayout,\n\t\t\tobject_ptr<Ui::VerticalLayout>(\n\t\t\t\t_controls.buttonsLayout)));\n\tAddButtonWithCount(\n\t\t_controls.buttonsLayout,\n\t\tstd::move(leftLabel),\n\t\tstd::move(rightLabel),\n\t\t[=] {\n\t\t\t_navigation->uiShow()->showBox(Box(UsernamesBox, user));\n\t\t},\n\t\t{ &st::menuIconLinks });\n}\n\nvoid Controller::fillBotCurrencyButton() {\n\tExpects(_isBot);\n\n\tstruct State final {\n\t\trpl::variable<QString> balance;\n\t};\n\n\tauto &lifetime = _controls.buttonsLayout->lifetime();\n\tconst auto state = lifetime.make_state<State>();\n\tconst auto format = [=](const CreditsAmount &balance) {\n\t\treturn Lang::FormatCreditsAmountDecimal(balance);\n\t};\n\tconst auto was = _peer->session().credits().balanceCurrency(\n\t\t_peer->id);\n\tif (was) {\n\t\tstate->balance = format(was);\n\t}\n\n\tconst auto wrap = _controls.buttonsLayout->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\t_controls.buttonsLayout,\n\t\t\tEditPeerInfoBox::CreateButton(\n\t\t\t\t_controls.buttonsLayout,\n\t\t\t\ttr::lng_manage_peer_bot_balance_currency(),\n\t\t\t\tstate->balance.value(),\n\t\t\t\t[controller = _navigation->parentController(), peer = _peer] {\n\t\t\t\t\tcontroller->showSection(Info::ChannelEarn::Make(peer));\n\t\t\t\t},\n\t\t\t\tst::manageGroupButton,\n\t\t\t\t{})));\n\twrap->toggle(!state->balance.current().isEmpty(), anim::type::instant);\n\n\tconst auto button = wrap->entity();\n\t{\n\t\tconst auto currencyLoad\n\t\t\t= button->lifetime().make_state<Api::EarnStatistics>(_peer);\n\t\tcurrencyLoad->request(\n\t\t) | rpl::on_error_done([=](const QString &error) {\n\t\t}, [=] {\n\t\t\tconst auto balance = currencyLoad->data().currentBalance;\n\t\t\tif (balance) {\n\t\t\t\twrap->toggle(true, anim::type::normal);\n\t\t\t}\n\t\t\tstate->balance = format(balance);\n\t\t}, button->lifetime());\n\t}\n\t{\n\t\tconst auto icon = Ui::CreateChild<Ui::RpWidget>(button);\n\t\ticon->resize(st::menuIconLinks.size());\n\t\tconst auto image = Ui::Earn::MenuIconCurrency(icon->size());\n\t\ticon->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(icon);\n\t\t\tp.drawImage(0, 0, image);\n\t\t}, icon->lifetime());\n\n\t\tbutton->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &size) {\n\t\t\ticon->moveToLeft(\n\t\t\t\tbutton->st().iconLeft,\n\t\t\t\t(size.height() - icon->height()) / 2);\n\t\t}, icon->lifetime());\n\t}\n}\n\nvoid Controller::fillBotCreditsButton() {\n\tExpects(_isBot);\n\n\tstruct State final {\n\t\trpl::variable<QString> balance;\n\t};\n\n\tauto &lifetime = _controls.buttonsLayout->lifetime();\n\tconst auto state = lifetime.make_state<State>();\n\tif (const auto balance = _peer->session().credits().balance(_peer->id)) {\n\t\tstate->balance = Lang::FormatCreditsAmountDecimal(balance);\n\t}\n\n\tconst auto wrap = _controls.buttonsLayout->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\t_controls.buttonsLayout,\n\t\t\tEditPeerInfoBox::CreateButton(\n\t\t\t\t_controls.buttonsLayout,\n\t\t\t\ttr::lng_manage_peer_bot_balance_credits(),\n\t\t\t\tstate->balance.value(),\n\t\t\t\t[controller = _navigation->parentController(), peer = _peer] {\n\t\t\t\t\tcontroller->showSection(Info::BotEarn::Make(peer));\n\t\t\t\t},\n\t\t\t\tst::manageGroupButton,\n\t\t\t\t{})));\n\twrap->toggle(!state->balance.current().isEmpty(), anim::type::instant);\n\n\tconst auto button = wrap->entity();\n\t{\n\t\tconst auto api = button->lifetime().make_state<Api::CreditsStatus>(\n\t\t\t_peer);\n\t\tapi->request({}, [=](Data::CreditsStatusSlice data) {\n\t\t\tif (data.balance) {\n\t\t\t\twrap->toggle(true, anim::type::normal);\n\t\t\t}\n\t\t\tstate->balance = Lang::FormatCreditsAmountDecimal(data.balance);\n\t\t});\n\t}\n\t{\n\t\tconst auto icon = Ui::CreateChild<Ui::RpWidget>(button);\n\t\tconst auto image = Ui::Earn::MenuIconCredits();\n\t\ticon->resize(image.size() / style::DevicePixelRatio());\n\t\ticon->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(icon);\n\t\t\tp.drawImage(0, 0, image);\n\t\t}, icon->lifetime());\n\n\t\tbutton->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &size) {\n\t\t\ticon->moveToLeft(\n\t\t\t\tbutton->st().iconLeft,\n\t\t\t\t(size.height() - icon->height()) / 2);\n\t\t}, icon->lifetime());\n\t}\n\n}\n\nvoid Controller::fillBotAffiliateProgram() {\n\tExpects(_isBot);\n\n\tif (!Info::BotStarRef::Setup::Allowed(_peer)) {\n\t\treturn;\n\t}\n\n\tconst auto user = _peer->asUser();\n\tauto label = user->session().changes().peerFlagsValue(\n\t\tuser,\n\t\tData::PeerUpdate::Flag::StarRefProgram\n\t) | rpl::map([=] {\n\t\tconst auto commission = user->botInfo\n\t\t\t? user->botInfo->starRefProgram.commission\n\t\t\t: 0;\n\t\treturn commission\n\t\t\t? Info::BotStarRef::FormatCommission(commission)\n\t\t\t: tr::lng_manage_peer_bot_star_ref_off(tr::now);\n\t});\n\tAddButtonWithCount(\n\t\t_controls.buttonsLayout,\n\t\ttr::lng_manage_peer_bot_star_ref(),\n\t\tstd::move(label),\n\t\t[controller = _navigation->parentController(), user] {\n\t\t\tcontroller->showSection(Info::BotStarRef::Setup::Make(user));\n\t\t},\n\t\t{ .icon = &st::menuIconSharing, .newBadge = true });\n}\n\nvoid Controller::fillBotEditIntroButton() {\n\tExpects(_isBot);\n\n\tconst auto user = _peer->asUser();\n\tAddButtonWithCount(\n\t\t_controls.buttonsLayout,\n\t\ttr::lng_manage_peer_bot_edit_intro(),\n\t\trpl::never<QString>(),\n\t\t[=] { toggleBotManager(u\"%1-intro\"_q.arg(user->username())); },\n\t\t{ &st::menuIconEdit });\n}\n\nvoid Controller::fillBotEditCommandsButton() {\n\tExpects(_isBot);\n\n\tconst auto user = _peer->asUser();\n\tAddButtonWithCount(\n\t\t_controls.buttonsLayout,\n\t\ttr::lng_manage_peer_bot_edit_commands(),\n\t\trpl::never<QString>(),\n\t\t[=] { toggleBotManager(u\"%1-commands\"_q.arg(user->username())); },\n\t\t{ &st::menuIconBotCommands });\n}\n\nvoid Controller::fillBotEditSettingsButton() {\n\tExpects(_isBot);\n\n\tconst auto user = _peer->asUser();\n\tAddButtonWithCount(\n\t\t_controls.buttonsLayout,\n\t\ttr::lng_manage_peer_bot_edit_settings(),\n\t\trpl::never<QString>(),\n\t\t[=] { toggleBotManager(user->username()); },\n\t\t{ &st::menuIconSettings });\n}\n\nvoid Controller::fillBotVerifyAccounts() {\n\tExpects(_isBot);\n\n\tconst auto user = _peer->asUser();\n\tconst auto wrap = _controls.buttonsLayout->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_controls.buttonsLayout,\n\t\t\tobject_ptr<Ui::VerticalLayout>(\n\t\t\t\t_controls.buttonsLayout)));\n\twrap->toggleOn(rpl::single(\n\t\trpl::empty\n\t) | rpl::then(user->owner().botCommandsChanges(\n\t) | rpl::filter(\n\t\trpl::mappers::_1 == _peer\n\t) | rpl::to_empty) | rpl::map([=] {\n\t\tconst auto info = user->botInfo.get();\n\t\treturn info && info->verifierSettings;\n\t}));\n\n\tconst auto inner = wrap->entity();\n\tUi::AddSkip(inner);\n\tAddButtonWithCount(\n\t\tinner,\n\t\ttr::lng_manage_peer_bot_verify(),\n\t\trpl::never<QString>(),\n\t\t[controller = _navigation->parentController(), user] {\n\t\t\tcontroller->show(MakeVerifyPeersBox(controller, user));\n\t\t},\n\t\t{ &st::menuIconFactcheck });\n\tUi::AddSkip(inner);\n\tUi::AddDivider(inner);\n}\n\nvoid Controller::submitTitle() {\n\tExpects(_controls.title != nullptr);\n\n\tif (_controls.title->getLastText().isEmpty()) {\n\t\t_controls.title->showError();\n\t\t_box->scrollToWidget(_controls.title);\n\t} else if (_controls.description) {\n\t\t_controls.description->setFocus();\n\t\t_box->scrollToWidget(_controls.description);\n\t}\n}\n\nvoid Controller::submitDescription() {\n\tExpects(_controls.title != nullptr);\n\tExpects(_controls.description != nullptr);\n\n\tif (_controls.title->getLastText().isEmpty()) {\n\t\t_controls.title->showError();\n\t\t_box->scrollToWidget(_controls.title);\n\t} else {\n\t\tsave();\n\t}\n}\n\nstd::optional<Controller::Saving> Controller::validate() const {\n\tauto result = Saving();\n\tif (validateUsernamesOrder(result)\n\t\t&& validateUsername(result)\n\t\t&& validateDiscussionLink(result)\n\t\t&& validateDirectMessagesPrice(result)\n\t\t&& validateTitle(result)\n\t\t&& validateDescription(result)\n\t\t&& validateHistoryVisibility(result)\n\t\t&& validateForum(result)\n\t\t&& validateAutotranslate(result)\n\t\t&& validateSignatures(result)\n\t\t&& validateForwards(result)\n\t\t&& validateJoinToWrite(result)\n\t\t&& validateRequestToJoin(result)) {\n\t\treturn result;\n\t}\n\treturn {};\n}\n\nbool Controller::validateUsernamesOrder(Saving &to) const {\n\tif (!_typeDataSavedValue) {\n\t\treturn true;\n\t} else if (_typeDataSavedValue->privacy != Privacy::HasUsername) {\n\t\tto.usernamesOrder = std::vector<QString>();\n\t\treturn true;\n\t}\n\tto.usernamesOrder = _typeDataSavedValue->usernamesOrder;\n\treturn true;\n}\n\nbool Controller::validateUsername(Saving &to) const {\n\tif (!_typeDataSavedValue) {\n\t\treturn true;\n\t} else if (_typeDataSavedValue->privacy != Privacy::HasUsername) {\n\t\tto.username = QString();\n\t\treturn true;\n\t}\n\tconst auto username = _typeDataSavedValue->username;\n\tif (username.isEmpty()) {\n\t\tto.username = QString();\n\t\treturn true;\n\t}\n\tto.username = username;\n\treturn true;\n}\n\nbool Controller::validateDiscussionLink(Saving &to) const {\n\tif (!_discussionLinkSavedValue) {\n\t\treturn true;\n\t}\n\tto.discussionLink = *_discussionLinkSavedValue;\n\treturn true;\n}\n\nbool Controller::validateDirectMessagesPrice(Saving &to) const {\n\tif (!_starsPerDirectMessageSavedValue) {\n\t\treturn true;\n\t}\n\tto.starsPerDirectMessage = _starsPerDirectMessageSavedValue->current();\n\treturn true;\n}\n\nbool Controller::validateTitle(Saving &to) const {\n\tif (!_controls.title) {\n\t\treturn true;\n\t}\n\tconst auto title = _controls.title->getLastText().trimmed();\n\tif (title.isEmpty()) {\n\t\t_controls.title->showError();\n\t\t_box->scrollToWidget(_controls.title);\n\t\treturn false;\n\t}\n\tto.title = title;\n\treturn true;\n}\n\nbool Controller::validateDescription(Saving &to) const {\n\tif (!_controls.description) {\n\t\treturn true;\n\t}\n\tto.description = _controls.description->getLastText().trimmed();\n\treturn true;\n}\n\nbool Controller::validateHistoryVisibility(Saving &to) const {\n\tif (!_controls.historyVisibilityWrap\n\t\t|| !_controls.historyVisibilityWrap->toggled()\n\t\t|| _channelHasLocationOriginalValue\n\t\t|| (_typeDataSavedValue\n\t\t\t&& _typeDataSavedValue->privacy == Privacy::HasUsername)) {\n\t\treturn true;\n\t}\n\tto.hiddenPreHistory\n\t\t= (_historyVisibilitySavedValue == HistoryVisibility::Hidden);\n\treturn true;\n}\n\nbool Controller::validateForum(Saving &to) const {\n\tif (!_forumSavedValue.has_value()) {\n\t\treturn true;\n\t}\n\tto.forum = _forumSavedValue;\n\tto.forumTabs = _forumTabsSavedValue;\n\treturn true;\n}\n\nbool Controller::validateAutotranslate(Saving &to) const {\n\tif (!_autotranslateSavedValue.has_value()) {\n\t\treturn true;\n\t}\n\tto.autotranslate = _autotranslateSavedValue;\n\treturn true;\n}\n\nbool Controller::validateSignatures(Saving &to) const {\n\tExpects(_signaturesSavedValue.has_value()\n\t\t== _signatureProfilesSavedValue.has_value());\n\n\tif (!_signaturesSavedValue.has_value()) {\n\t\treturn true;\n\t}\n\tto.signatures = _signaturesSavedValue;\n\tto.signatureProfiles = _signatureProfilesSavedValue;\n\treturn true;\n}\n\nbool Controller::validateForwards(Saving &to) const {\n\tif (!_typeDataSavedValue) {\n\t\treturn true;\n\t}\n\tto.noForwards = _typeDataSavedValue->noForwards;\n\treturn true;\n}\n\nbool Controller::validateJoinToWrite(Saving &to) const {\n\tif (!_typeDataSavedValue) {\n\t\treturn true;\n\t}\n\tto.joinToWrite = _typeDataSavedValue->joinToWrite;\n\treturn true;\n}\n\nbool Controller::validateRequestToJoin(Saving &to) const {\n\tif (!_typeDataSavedValue) {\n\t\treturn true;\n\t}\n\tto.requestToJoin = _typeDataSavedValue->requestToJoin;\n\treturn true;\n}\n\nvoid Controller::save() {\n\tExpects(_wrap != nullptr);\n\n\tif (!_saveStagesQueue.empty()) {\n\t\treturn;\n\t}\n\tif (const auto saving = validate()) {\n\t\t_savingData = *saving;\n\t\tpushSaveStage([=] { saveUsernamesOrder(); });\n\t\tpushSaveStage([=] { saveUsername(); });\n\t\tpushSaveStage([=] { saveDiscussionLink(); });\n\t\tpushSaveStage([=] { saveDirectMessagesPrice(); });\n\t\tpushSaveStage([=] { saveTitle(); });\n\t\tpushSaveStage([=] { saveDescription(); });\n\t\tpushSaveStage([=] { saveHistoryVisibility(); });\n\t\tpushSaveStage([=] { saveForum(); });\n\t\tpushSaveStage([=] { saveAutotranslate(); });\n\t\tpushSaveStage([=] { saveSignatures(); });\n\t\tpushSaveStage([=] { saveForwards(); });\n\t\tpushSaveStage([=] { saveJoinToWrite(); });\n\t\tpushSaveStage([=] { saveRequestToJoin(); });\n\t\tpushSaveStage([=] { savePhoto(); });\n\t\tcontinueSave();\n\t}\n}\n\nvoid Controller::pushSaveStage(FnMut<void()> &&lambda) {\n\t_saveStagesQueue.push_back(std::move(lambda));\n}\n\nvoid Controller::continueSave() {\n\tif (!_saveStagesQueue.empty()) {\n\t\tauto next = std::move(_saveStagesQueue.front());\n\t\t_saveStagesQueue.pop_front();\n\t\tnext();\n\t}\n}\n\nvoid Controller::cancelSave() {\n\t_saveStagesQueue.clear();\n}\n\nvoid Controller::saveUsernamesOrder() {\n\tconst auto channel = _peer->asChannel();\n\tif (!_savingData.usernamesOrder || !channel) {\n\t\treturn continueSave();\n\t}\n\tif (_savingData.usernamesOrder->empty()) {\n\t\t_api.request(MTPchannels_DeactivateAllUsernames(\n\t\t\tchannel->inputChannel()\n\t\t)).done([=] {\n\t\t\tif (channel->editableUsername().isEmpty()) {\n\t\t\t\tchannel->setUsernames({});\n\t\t\t} else {\n\t\t\t\tchannel->setUsernames({\n\t\t\t\t\t{ channel->editableUsername(), true, true }\n\t\t\t\t});\n\t\t\t}\n\t\t\tcontinueSave();\n\t\t}).send();\n\t} else {\n\t\tconst auto weakContinue = crl::guard(this, [=] {\n\t\t\tcontinueSave();\n\t\t});\n\t\tconst auto lifetime = std::make_shared<rpl::lifetime>();\n\t\tconst auto newUsernames = (*_savingData.usernamesOrder);\n\t\t_peer->session().api().usernames().reorder(\n\t\t\t_peer,\n\t\t\tnewUsernames\n\t\t) | rpl::on_done([=] {\n\t\t\tchannel->setUsernames(ranges::views::all(\n\t\t\t\tnewUsernames\n\t\t\t) | ranges::views::transform([&](QString username) {\n\t\t\t\tconst auto editable\n\t\t\t\t\t= (channel->editableUsername() == username);\n\t\t\t\treturn Data::Username{\n\t\t\t\t\t.username = std::move(username),\n\t\t\t\t\t.active = true,\n\t\t\t\t\t.editable = editable,\n\t\t\t\t};\n\t\t\t}) | ranges::to_vector);\n\t\t\tweakContinue();\n\t\t\tlifetime->destroy();\n\t\t}, *lifetime);\n\t}\n}\n\nvoid Controller::saveUsername() {\n\tconst auto channel = _peer->asChannel();\n\tconst auto username = (channel ? channel->editableUsername() : QString());\n\tif (!_savingData.username || *_savingData.username == username) {\n\t\treturn continueSave();\n\t} else if (!channel) {\n\t\tconst auto saveForChannel = [=](not_null<ChannelData*> channel) {\n\t\t\tif (_peer->asChannel() == channel) {\n\t\t\t\tsaveUsername();\n\t\t\t} else {\n\t\t\t\tcancelSave();\n\t\t\t}\n\t\t};\n\t\t_peer->session().api().migrateChat(\n\t\t\t_peer->asChat(),\n\t\t\tcrl::guard(this, saveForChannel));\n\t\treturn;\n\t}\n\n\tconst auto newUsername = (*_savingData.username);\n\t_api.request(MTPchannels_UpdateUsername(\n\t\tchannel->inputChannel(),\n\t\tMTP_string(newUsername)\n\t)).done([=] {\n\t\tchannel->setName(\n\t\t\tTextUtilities::SingleLine(channel->name()),\n\t\t\tnewUsername);\n\t\tcontinueSave();\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto &type = error.type();\n\t\tif (type == u\"USERNAME_NOT_MODIFIED\"_q) {\n\t\t\tchannel->setName(\n\t\t\t\tTextUtilities::SingleLine(channel->name()),\n\t\t\t\tTextUtilities::SingleLine(*_savingData.username));\n\t\t\tcontinueSave();\n\t\t\treturn;\n\t\t}\n\n\t\t// Very rare case.\n\t\tshowEditPeerTypeBox([&] {\n\t\t\tif (type == u\"USERNAME_INVALID\"_q) {\n\t\t\t\treturn tr::lng_create_channel_link_invalid();\n\t\t\t} else if (type == u\"USERNAME_OCCUPIED\"_q\n\t\t\t\t|| type == u\"USERNAMES_UNAVAILABLE\"_q) {\n\t\t\t\treturn tr::lng_create_channel_link_occupied();\n\t\t\t}\n\t\t\treturn tr::lng_create_channel_link_invalid();\n\t\t}());\n\t\tcancelSave();\n\t}).send();\n}\n\nvoid Controller::saveDiscussionLink() {\n\tconst auto channel = _peer->asChannel();\n\tif (!channel) {\n\t\treturn continueSave();\n\t}\n\tif (!_savingData.discussionLink\n\t\t|| *_savingData.discussionLink == channel->discussionLink()) {\n\t\treturn continueSave();\n\t}\n\n\tconst auto chat = *_savingData.discussionLink;\n\tif (channel->isBroadcast() && chat && chat->hiddenPreHistory()) {\n\t\ttogglePreHistoryHidden(\n\t\t\tchat,\n\t\t\tfalse,\n\t\t\t[=] { saveDiscussionLink(); },\n\t\t\t[=] { cancelSave(); });\n\t\treturn;\n\t}\n\n\tconst auto input = *_savingData.discussionLink\n\t\t? (*_savingData.discussionLink)->inputChannel()\n\t\t: MTP_inputChannelEmpty();\n\t_api.request(MTPchannels_SetDiscussionGroup(\n\t\t(channel->isBroadcast() ? channel->inputChannel() : input),\n\t\t(channel->isBroadcast() ? input : channel->inputChannel())\n\t)).done([=] {\n\t\tchannel->setDiscussionLink(*_savingData.discussionLink);\n\t\tcontinueSave();\n\t}).fail([=](const MTP::Error &error) {\n\t\t_navigation->showToast(error.type());\n\t\tcancelSave();\n\t}).send();\n}\n\nvoid Controller::saveDirectMessagesPrice() {\n\tconst auto channel = _peer->asChannel();\n\tif (!channel) {\n\t\treturn continueSave();\n\t}\n\tconst auto current = CurrentPricePerDirectMessage(channel);\n\tconst auto desired = _savingData.starsPerDirectMessage\n\t\t? *_savingData.starsPerDirectMessage\n\t\t: current;\n\tif (desired == current) {\n\t\treturn continueSave();\n\t}\n\tconst auto show = _navigation->uiShow();\n\tconst auto done = [=](bool ok) {\n\t\tif (ok) {\n\t\t\tcontinueSave();\n\t\t} else {\n\t\t\tcancelSave();\n\t\t}\n\t};\n\tSaveStarsPerMessage(show, channel, desired, crl::guard(this, done));\n}\n\nvoid Controller::saveTitle() {\n\tif (!_savingData.title || *_savingData.title == _peer->name()) {\n\t\treturn continueSave();\n\t}\n\n\tconst auto onDone = [=](const MTPUpdates &result) {\n\t\t_peer->session().api().applyUpdates(result);\n\t\tcontinueSave();\n\t};\n\tconst auto onFail = [=](const MTP::Error &error) {\n\t\tconst auto &type = error.type();\n\t\tif (type == u\"CHAT_NOT_MODIFIED\"_q\n\t\t\t|| type == u\"CHAT_TITLE_NOT_MODIFIED\"_q) {\n\t\t\tif (const auto channel = _peer->asChannel()) {\n\t\t\t\tchannel->setName(\n\t\t\t\t\t*_savingData.title,\n\t\t\t\t\tchannel->editableUsername());\n\t\t\t} else if (const auto chat = _peer->asChat()) {\n\t\t\t\tchat->setName(*_savingData.title);\n\t\t\t}\n\t\t\tcontinueSave();\n\t\t\treturn;\n\t\t}\n\t\t_controls.title->showError();\n\t\tif (type == u\"NO_CHAT_TITLE\"_q) {\n\t\t\t_box->scrollToWidget(_controls.title);\n\t\t} else {\n\t\t\t_navigation->showToast(type);\n\t\t}\n\t\tcancelSave();\n\t};\n\n\tif (const auto channel = _peer->asChannel()) {\n\t\t_api.request(MTPchannels_EditTitle(\n\t\t\tchannel->inputChannel(),\n\t\t\tMTP_string(*_savingData.title)\n\t\t)).done(std::move(onDone)\n\t\t).fail(std::move(onFail)\n\t\t).send();\n\t} else if (const auto chat = _peer->asChat()) {\n\t\t_api.request(MTPmessages_EditChatTitle(\n\t\t\tchat->inputChat(),\n\t\t\tMTP_string(*_savingData.title)\n\t\t)).done(std::move(onDone)\n\t\t).fail(std::move(onFail)\n\t\t).send();\n\t} else if (_isBot) {\n\t\t_api.request(MTPbots_GetBotInfo(\n\t\t\tMTP_flags(MTPbots_GetBotInfo::Flag::f_bot),\n\t\t\t_peer->asUser()->inputUser(),\n\t\t\tMTPstring() // Lang code.\n\t\t)).done([=](const MTPbots_BotInfo &result) {\n\t\t\tconst auto was = qs(result.data().vname());\n\t\t\tconst auto now = *_savingData.title;\n\t\t\tif (was == now) {\n\t\t\t\treturn continueSave();\n\t\t\t}\n\t\t\tusing Flag = MTPbots_SetBotInfo::Flag;\n\t\t\t_api.request(MTPbots_SetBotInfo(\n\t\t\t\tMTP_flags(Flag::f_bot | Flag::f_name),\n\t\t\t\t_peer->asUser()->inputUser(),\n\t\t\t\tMTPstring(), // Lang code.\n\t\t\t\tMTP_string(now), // Name.\n\t\t\t\tMTPstring(), // About.\n\t\t\t\tMTPstring() // Description.\n\t\t\t)).done([=] {\n\t\t\t\tcontinueSave();\n\t\t\t}).fail(std::move(onFail)\n\t\t\t).send();\n\t\t}).fail(std::move(onFail)\n\t\t).send();\n\t} else {\n\t\tcontinueSave();\n\t}\n}\n\nvoid Controller::saveDescription() {\n\tif (!_savingData.description\n\t\t|| *_savingData.description == _peer->about()) {\n\t\treturn continueSave();\n\t}\n\tconst auto successCallback = [=] {\n\t\t_peer->setAbout(*_savingData.description);\n\t\tcontinueSave();\n\t};\n\tif (_isBot) {\n\t\t_api.request(MTPbots_GetBotInfo(\n\t\t\tMTP_flags(MTPbots_GetBotInfo::Flag::f_bot),\n\t\t\t_peer->asUser()->inputUser(),\n\t\t\tMTPstring() // Lang code.\n\t\t)).done([=](const MTPbots_BotInfo &result) {\n\t\t\tconst auto was = qs(result.data().vabout());\n\t\t\tconst auto now = *_savingData.description;\n\t\t\tif (was == now) {\n\t\t\t\treturn continueSave();\n\t\t\t}\n\t\t\tusing Flag = MTPbots_SetBotInfo::Flag;\n\t\t\t_api.request(MTPbots_SetBotInfo(\n\t\t\t\tMTP_flags(Flag::f_bot | Flag::f_about),\n\t\t\t\t_peer->asUser()->inputUser(),\n\t\t\t\tMTPstring(), // Lang code.\n\t\t\t\tMTPstring(), // Name.\n\t\t\t\tMTP_string(now), // About.\n\t\t\t\tMTPstring() // Description.\n\t\t\t)).done([=] {\n\t\t\t\tsuccessCallback();\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\t_controls.description->showError();\n\t\t\t\t_navigation->showToast(error.type());\n\t\t\t\tcancelSave();\n\t\t\t}).send();\n\t\t}).fail([=] {\n\t\t\tcontinueSave();\n\t\t}).send();\n\t\treturn;\n\t}\n\t_api.request(MTPmessages_EditChatAbout(\n\t\t_peer->input(),\n\t\tMTP_string(*_savingData.description)\n\t)).done([=] {\n\t\tsuccessCallback();\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto &type = error.type();\n\t\tif (type == u\"CHAT_ABOUT_NOT_MODIFIED\"_q) {\n\t\t\tsuccessCallback();\n\t\t\treturn;\n\t\t}\n\t\t_controls.description->showError();\n\t\t_navigation->showToast(type);\n\t\tcancelSave();\n\t}).send();\n}\n\nvoid Controller::saveHistoryVisibility() {\n\tconst auto channel = _peer->asChannel();\n\tconst auto hidden = channel ? channel->hiddenPreHistory() : true;\n\tif (!_savingData.hiddenPreHistory\n\t\t|| *_savingData.hiddenPreHistory == hidden) {\n\t\treturn continueSave();\n\t} else if (!channel) {\n\t\tconst auto saveForChannel = [=](not_null<ChannelData*> channel) {\n\t\t\tif (_peer->asChannel() == channel) {\n\t\t\t\tsaveHistoryVisibility();\n\t\t\t} else {\n\t\t\t\tcancelSave();\n\t\t\t}\n\t\t};\n\t\t_peer->session().api().migrateChat(\n\t\t\t_peer->asChat(),\n\t\t\tcrl::guard(this, saveForChannel));\n\t\treturn;\n\t}\n\ttogglePreHistoryHidden(\n\t\tchannel,\n\t\t*_savingData.hiddenPreHistory,\n\t\t[=] { continueSave(); },\n\t\t[=] { cancelSave(); });\n}\n\nvoid Controller::toggleBotManager(const QString &command) {\n\tconst auto controller = _navigation->parentController();\n\t_api.request(MTPcontacts_ResolveUsername(\n\t\tMTP_flags(0),\n\t\tMTP_string(kBotManagerUsername.utf16()),\n\t\tMTP_string()\n\t)).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\t_peer->owner().processUsers(result.data().vusers());\n\t\t_peer->owner().processChats(result.data().vchats());\n\t\tconst auto botPeer = _peer->owner().peerLoaded(\n\t\t\tpeerFromMTP(result.data().vpeer()));\n\t\tif (const auto bot = botPeer ? botPeer->asUser() : nullptr) {\n\t\t\tconst auto show = controller->uiShow();\n\t\t\t_peer->session().api().sendBotStart(show, bot, bot, command);\n\t\t\tcontroller->showPeerHistory(bot);\n\t\t}\n\t}).send();\n}\n\nvoid Controller::togglePreHistoryHidden(\n\t\tnot_null<ChannelData*> channel,\n\t\tbool hidden,\n\t\tFn<void()> done,\n\t\tFn<void()> fail) {\n\tconst auto apply = [=] {\n\t\t// Update in the result doesn't contain the\n\t\t// channelFull:flags field which holds this value.\n\t\t// So after saving we need to update it manually.\n\t\tconst auto flags = channel->flags();\n\t\tconst auto flag = ChannelDataFlag::PreHistoryHidden;\n\t\tchannel->setFlags(hidden ? (flags | flag) : (flags & ~flag));\n\n\t\tdone();\n\t};\n\t_api.request(MTPchannels_TogglePreHistoryHidden(\n\t\tchannel->inputChannel(),\n\t\tMTP_bool(hidden)\n\t)).done([=](const MTPUpdates &result) {\n\t\tchannel->session().api().applyUpdates(result);\n\t\tapply();\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto type = error.type();\n\t\tif (type == u\"CHAT_NOT_MODIFIED\"_q) {\n\t\t\tapply();\n\t\t} else {\n\t\t\t_navigation->showToast(type);\n\t\t\tfail();\n\t\t}\n\t}).send();\n}\n\nvoid Controller::saveForum() {\n\tconst auto channel = _peer->asChannel();\n\tconst auto nowForum = _peer->isForum();\n\tconst auto nowForumTabs = !channel\n\t\t|| !nowForum\n\t\t|| channel->useSubsectionTabs();\n\tif (!_savingData.forum\n\t\t|| (*_savingData.forum == nowForum\n\t\t\t&& *_savingData.forumTabs == nowForumTabs)) {\n\t\treturn continueSave();\n\t} else if (!channel) {\n\t\tconst auto saveForChannel = [=](not_null<ChannelData*> channel) {\n\t\t\tif (_peer->asChannel() == channel) {\n\t\t\t\tsaveForum();\n\t\t\t} else {\n\t\t\t\tcancelSave();\n\t\t\t}\n\t\t};\n\t\t_peer->session().api().migrateChat(\n\t\t\t_peer->asChat(),\n\t\t\tcrl::guard(this, saveForChannel));\n\t\treturn;\n\t}\n\t_api.request(MTPchannels_ToggleForum(\n\t\tchannel->inputChannel(),\n\t\tMTP_bool(*_savingData.forum),\n\t\tMTP_bool(*_savingData.forum && *_savingData.forumTabs)\n\t)).done([=](const MTPUpdates &result) {\n\t\tconst auto weak = base::make_weak(this);\n\t\tchannel->session().api().applyUpdates(result);\n\t\tif (weak) { // todo better to be able to save in closed already box.\n\t\t\tcontinueSave();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (error.type() == u\"CHAT_NOT_MODIFIED\"_q) {\n\t\t\tcontinueSave();\n\t\t} else {\n\t\t\t_navigation->showToast(error.type());\n\t\t\tcancelSave();\n\t\t}\n\t}).send();\n}\n\nvoid Controller::saveAutotranslate() {\n\tconst auto channel = _peer->asBroadcast();\n\tif (!_savingData.autotranslate\n\t\t|| !channel\n\t\t|| (*_savingData.autotranslate == channel->autoTranslation())) {\n\t\treturn continueSave();\n\t}\n\t_api.request(MTPchannels_ToggleAutotranslation(\n\t\tchannel->inputChannel(),\n\t\tMTP_bool(*_savingData.autotranslate)\n\t)).done([=](const MTPUpdates &result) {\n\t\tchannel->session().api().applyUpdates(result);\n\t\tcontinueSave();\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (error.type() == u\"CHAT_NOT_MODIFIED\"_q) {\n\t\t\tcontinueSave();\n\t\t} else {\n\t\t\t_navigation->showToast(error.type());\n\t\t\tcancelSave();\n\t\t}\n\t}).send();\n}\n\nvoid Controller::saveSignatures() {\n\tExpects(_savingData.signatures.has_value()\n\t\t== _savingData.signatureProfiles.has_value());\n\n\tconst auto channel = _peer->asChannel();\n\tif (!_savingData.signatures\n\t\t|| !channel\n\t\t|| ((*_savingData.signatures == channel->addsSignature())\n\t\t\t&& (*_savingData.signatureProfiles\n\t\t\t\t== channel->signatureProfiles()))) {\n\t\treturn continueSave();\n\t}\n\tusing Flag = MTPchannels_ToggleSignatures::Flag;\n\t_api.request(MTPchannels_ToggleSignatures(\n\t\tMTP_flags(Flag()\n\t\t\t| (*_savingData.signatures\n\t\t\t\t? Flag::f_signatures_enabled\n\t\t\t\t: Flag())\n\t\t\t| (*_savingData.signatureProfiles\n\t\t\t\t? Flag::f_profiles_enabled\n\t\t\t\t: Flag())),\n\t\tchannel->inputChannel()\n\t)).done([=](const MTPUpdates &result) {\n\t\tchannel->session().api().applyUpdates(result);\n\t\tcontinueSave();\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (error.type() == u\"CHAT_NOT_MODIFIED\"_q) {\n\t\t\tcontinueSave();\n\t\t} else {\n\t\t\t_navigation->showToast(error.type());\n\t\t\tcancelSave();\n\t\t}\n\t}).send();\n}\n\nvoid Controller::saveForwards() {\n\tif (!_savingData.noForwards\n\t\t|| *_savingData.noForwards != _peer->allowsForwarding()) {\n\t\treturn continueSave();\n\t}\n\tusing Flag = MTPmessages_ToggleNoForwards::Flag;\n\t_api.request(MTPmessages_ToggleNoForwards(\n\t\tMTP_flags(Flag()),\n\t\t_peer->input(),\n\t\tMTP_bool(*_savingData.noForwards),\n\t\tMTPint()\n\t)).done([=](const MTPUpdates &result) {\n\t\t_peer->session().api().applyUpdates(result);\n\t\tcontinueSave();\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (error.type() == u\"CHAT_NOT_MODIFIED\"_q) {\n\t\t\tcontinueSave();\n\t\t} else {\n\t\t\t_navigation->showToast(error.type());\n\t\t\tcancelSave();\n\t\t}\n\t}).send();\n}\n\nvoid Controller::saveJoinToWrite() {\n\tconst auto joinToWrite = _peer->isMegagroup()\n\t\t&& _peer->asChannel()->joinToWrite();\n\tif (!_savingData.joinToWrite\n\t\t|| *_savingData.joinToWrite == joinToWrite) {\n\t\treturn continueSave();\n\t}\n\t_api.request(MTPchannels_ToggleJoinToSend(\n\t\t_peer->asChannel()->inputChannel(),\n\t\tMTP_bool(*_savingData.joinToWrite)\n\t)).done([=](const MTPUpdates &result) {\n\t\t_peer->session().api().applyUpdates(result);\n\t\tcontinueSave();\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (error.type() == u\"CHAT_NOT_MODIFIED\"_q) {\n\t\t\tcontinueSave();\n\t\t} else {\n\t\t\t_navigation->showToast(error.type());\n\t\t\tcancelSave();\n\t\t}\n\t}).send();\n}\n\nvoid Controller::saveRequestToJoin() {\n\tconst auto requestToJoin = _peer->isMegagroup()\n\t\t&& _peer->asChannel()->requestToJoin();\n\tif (!_savingData.requestToJoin\n\t\t|| *_savingData.requestToJoin == requestToJoin) {\n\t\treturn continueSave();\n\t}\n\t_api.request(MTPchannels_ToggleJoinRequest(\n\t\t_peer->asChannel()->inputChannel(),\n\t\tMTP_bool(*_savingData.requestToJoin)\n\t)).done([=](const MTPUpdates &result) {\n\t\t_peer->session().api().applyUpdates(result);\n\t\tcontinueSave();\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (error.type() == u\"CHAT_NOT_MODIFIED\"_q) {\n\t\t\tcontinueSave();\n\t\t} else {\n\t\t\t_navigation->showToast(error.type());\n\t\t\tcancelSave();\n\t\t}\n\t}).send();\n}\n\nvoid Controller::savePhoto() {\n\tauto image = _controls.photo\n\t\t? _controls.photo->takeResultImage()\n\t\t: QImage();\n\tif (!image.isNull()) {\n\t\t_peer->session().api().peerPhoto().upload(\n\t\t\t_peer,\n\t\t\t{ std::move(image) });\n\t}\n\t_box->closeBox();\n}\n\nvoid Controller::deleteWithConfirmation() {\n\tconst auto channel = _peer->asChannel();\n\tAssert(channel != nullptr);\n\n\tconst auto text = (_isGroup\n\t\t? tr::lng_sure_delete_group\n\t\t: tr::lng_sure_delete_channel)(tr::now);\n\tconst auto deleteCallback = crl::guard(this, [=] {\n\t\tdeleteChannel();\n\t});\n\t_navigation->parentController()->show(\n\t\tUi::MakeConfirmBox({\n\t\t\t.text = text,\n\t\t\t.confirmed = deleteCallback,\n\t\t\t.confirmText = tr::lng_box_delete(),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t}));\n}\n\nvoid Controller::deleteChannel() {\n\tExpects(_peer->isChannel());\n\n\tconst auto channel = _peer->asChannel();\n\tconst auto chat = channel->migrateFrom();\n\n\tconst auto session = &_peer->session();\n\n\t_navigation->parentController()->hideLayer();\n\tCore::App().closeChatFromWindows(channel);\n\tif (chat) {\n\t\tsession->api().deleteConversation(chat, false);\n\t}\n\tsession->api().request(MTPchannels_DeleteChannel(\n\t\tchannel->inputChannel()\n\t)).done([=](const MTPUpdates &result) {\n\t\tsession->api().applyUpdates(result);\n\t//}).fail([=](const MTP::Error &error) {\n\t//\tif (error.type() == u\"CHANNEL_TOO_LARGE\"_q) {\n\t//\t\tUi::show(Box<Ui::InformBox>(tr::lng_cant_delete_channel(tr::now)));\n\t//\t}\n\t}).send();\n}\n\n} // namespace\n\n\nEditPeerInfoBox::EditPeerInfoBox(\n\tQWidget*,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer)\n: _navigation(navigation)\n, _peer(peer->migrateToOrMe()) {\n}\n\nvoid EditPeerInfoBox::prepare() {\n\tconst auto controller = Ui::CreateChild<Controller>(\n\t\tthis,\n\t\t_navigation,\n\t\tthis,\n\t\t_peer);\n\t_focusRequests.events(\n\t) | rpl::on_next(\n\t\t[=] { controller->setFocus(); },\n\t\tlifetime());\n\tauto content = controller->createContent();\n\tsetDimensionsToContent(st::boxWideWidth, content);\n\tsetInnerWidget(object_ptr<Ui::OverrideMargins>(\n\t\tthis,\n\t\tstd::move(content)));\n}\n\nobject_ptr<Ui::SettingsButton> EditPeerInfoBox::CreateButton(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<QString> &&text,\n\t\trpl::producer<QString> &&count,\n\t\tFn<void()> callback,\n\t\tconst style::SettingsCountButton &st,\n\t\tSettings::IconDescriptor &&descriptor) {\n\treturn CreateButton(\n\t\tparent,\n\t\tstd::move(text),\n\t\tstd::move(count) | rpl::map(tr::marked),\n\t\tstd::move(callback),\n\t\tst,\n\t\tstd::move(descriptor));\n}\n\nobject_ptr<Ui::SettingsButton> EditPeerInfoBox::CreateButton(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<QString> &&text,\n\t\trpl::producer<TextWithEntities> &&labelText,\n\t\tFn<void()> callback,\n\t\tconst style::SettingsCountButton &st,\n\t\tSettings::IconDescriptor &&descriptor) {\n\tauto result = object_ptr<Ui::SettingsButton>(\n\t\tparent,\n\t\trpl::duplicate(text),\n\t\tst.button);\n\tconst auto button = result.data();\n\tbutton->addClickHandler(callback);\n\n\tconst auto badge = descriptor.newBadge\n\t\t? Ui::NewBadge::CreateNewBadge(\n\t\t\tbutton,\n\t\t\ttr::lng_premium_summary_new_badge()).get()\n\t\t: nullptr;\n\n\tif (descriptor) {\n\t\tAddButtonIcon(\n\t\t\tbutton,\n\t\t\tst.button,\n\t\t\tstd::move(descriptor));\n\t}\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton,\n\t\trpl::duplicate(labelText),\n\t\tst.label);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tlabel->show();\n\n\trpl::combine(\n\t\trpl::duplicate(text),\n\t\tstd::move(labelText),\n\t\tbutton->widthValue()\n\t) | rpl::on_next([&st, label](\n\t\t\tconst QString &text,\n\t\t\tconst TextWithEntities &labelText,\n\t\t\tint width) {\n\t\tconst auto available = width\n\t\t\t- st.button.padding.left()\n\t\t\t- (st.button.style.font->spacew * 2)\n\t\t\t- st.button.style.font->width(text)\n\t\t\t- st.labelPosition.x();\n\t\tconst auto required = label->textMaxWidth();\n\t\tlabel->resizeToWidth(std::min(required, available));\n\t\tlabel->moveToRight(\n\t\t\tst.labelPosition.x(),\n\t\t\tst.labelPosition.y(),\n\t\t\twidth);\n\t}, label->lifetime());\n\n\tif (badge) {\n\t\trpl::combine(\n\t\t\tstd::move(text),\n\t\t\tlabel->widthValue(),\n\t\t\tbutton->widthValue()\n\t\t) | rpl::on_next([=](\n\t\t\t\tconst QString &text,\n\t\t\t\tint labelWidth,\n\t\t\t\tint width) {\n\t\t\tconst auto space = st.button.style.font->spacew;\n\t\t\tconst auto left = st.button.padding.left()\n\t\t\t\t+ st.button.style.font->width(text)\n\t\t\t\t+ space;\n\t\t\tconst auto right = st.labelPosition.x()\n\t\t\t\t+ labelWidth\n\t\t\t\t+ (space * 2);\n\t\t\tconst auto available = width - left - right;\n\t\t\tbadge->setVisible(available >= badge->width());\n\t\t\tif (!badge->isHidden()) {\n\t\t\t\tconst auto top = st.button.padding.top()\n\t\t\t\t\t+ st.button.style.font->ascent\n\t\t\t\t\t- st::settingsPremiumNewBadge.style.font->ascent\n\t\t\t\t\t- st::settingsPremiumNewBadgePadding.top();\n\t\t\t\tbadge->moveToLeft(left, top, width);\n\t\t\t}\n\t\t}, badge->lifetime());\n\t}\n\n\treturn result;\n}\n\nbool EditPeerInfoBox::Available(not_null<PeerData*> peer) {\n\tif (const auto bot = peer->asUser()) {\n\t\treturn bot->botInfo && bot->botInfo->canEditInformation;\n\t} else if (const auto chat = peer->asChat()) {\n\t\treturn false\n\t\t\t|| chat->canEditInformation()\n\t\t\t|| chat->canEditPermissions();\n\t} else if (const auto channel = peer->asChannel()) {\n\t\t// canViewMembers() is removed, because in supergroups you\n\t\t// see them in profile and in channels only admins can see them.\n\n\t\t// canViewAdmins() is removed, because in supergroups it is\n\t\t// always true and in channels it is equal to canViewBanned().\n\t\tif (channel->isMonoforum()) {\n\t\t\treturn false;\n\t\t}\n\t\treturn false\n\t\t\t//|| channel->canViewMembers()\n\t\t\t//|| channel->canViewAdmins()\n\t\t\t|| channel->canViewBanned()\n\t\t\t|| channel->canEditInformation()\n\t\t\t|| channel->canEditPermissions()\n\t\t\t|| channel->hasAdminRights()\n\t\t\t|| channel->amCreator();\n\t} else {\n\t\treturn false;\n\t}\n}\n\nvoid ShowEditChatPermissions(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer) {\n\tShowEditPermissions(navigation, peer);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n\nnamespace Settings {\nstruct IconDescriptor;\n} // namespace Settings\n\nnamespace style {\nstruct SettingsCountButton;\n} // namespace style\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nnamespace Ui {\nclass VerticalLayout;\nclass SettingsButton;\n} // namespace Ui\n\nclass EditPeerInfoBox : public Ui::BoxContent {\npublic:\n\tEditPeerInfoBox(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer);\n\n\tvoid setInnerFocus() override {\n\t\t_focusRequests.fire({});\n\t}\n\n\tstatic bool Available(not_null<PeerData*> peer);\n\n\t[[nodiscard]] static object_ptr<Ui::SettingsButton> CreateButton(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<QString> &&text,\n\t\trpl::producer<QString> &&count,\n\t\tFn<void()> callback,\n\t\tconst style::SettingsCountButton &st,\n\t\tSettings::IconDescriptor &&descriptor);\n\t[[nodiscard]] static object_ptr<Ui::SettingsButton> CreateButton(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<QString> &&text,\n\t\trpl::producer<TextWithEntities> &&labelText,\n\t\tFn<void()> callback,\n\t\tconst style::SettingsCountButton &st,\n\t\tSettings::IconDescriptor &&descriptor);\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\trpl::event_stream<> _focusRequests;\n\tnot_null<Window::SessionNavigation*> _navigation;\n\tnot_null<PeerData*> _peer;\n\n};\n\nvoid ShowEditChatPermissions(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_peer_invite_link.h\"\n\n#include \"api/api_invite_links.h\"\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/gift_premium_box.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/share_box.h\"\n#include \"core/application.h\"\n#include \"core/ui_integration.h\" // TextContext\n#include \"data/components/credits.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"history/history.h\"\n#include \"history/history_item_helpers.h\" // GetErrorForSending.\n#include \"history/view/history_view_group_call_bar.h\" // GenerateUserpics...\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"qr/qr_generate.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/edit_invite_link.h\"\n#include \"ui/boxes/edit_invite_link_session.h\"\n#include \"ui/boxes/peer_qr_box.h\"\n#include \"ui/controls/invite_link_buttons.h\"\n#include \"ui/controls/invite_link_label.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\" // st::boxDividerLabel.\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_premium.h\"\n\n#include <QtCore/QMimeData>\n#include <QtGui/QGuiApplication>\n#include <QtSvg/QSvgRenderer>\n\nnamespace {\n\nconstexpr auto kFirstPage = 20;\nconstexpr auto kPerPage = 100;\n// constexpr auto kShareQrSize = 768;\n// constexpr auto kShareQrPadding = 16;\nconstexpr auto kMaxShownJoined = 3;\n\nusing LinkData = Api::InviteLink;\n\nvoid ShowPeerInfoSync(not_null<PeerData*> peer) {\n\t// While a peer info is demanded by the left click\n\t// we can safely use activeWindow.\n\tif (const auto window = Core::App().activeWindow()) {\n\t\tif (const auto controller = window->sessionController()) {\n\t\t\tif (&controller->session() == &peer->session()) {\n\t\t\t\tcontroller->showPeerInfo(peer);\n\t\t\t}\n\t\t}\n\t}\n}\n\nclass SubscriptionRow final : public PeerListRow {\npublic:\n\tSubscriptionRow(\n\t\tnot_null<PeerData*> peer,\n\t\tTimeId date,\n\t\tData::PeerSubscription subscription);\n\n\tQSize rightActionSize() const override;\n\tQMargins rightActionMargins() const override;\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\nprivate:\n\tstd::optional<Settings::SubscriptionRightLabel> _rightLabel;\n\n};\n\nSubscriptionRow::SubscriptionRow(\n\tnot_null<PeerData*> peer,\n\tTimeId date,\n\tData::PeerSubscription subscription)\n: PeerListRow(peer) {\n\tif (subscription) {\n\t\t_rightLabel = Settings::PaintSubscriptionRightLabelCallback(\n\t\t\t&peer->session(),\n\t\t\tst::peerListBoxItem,\n\t\t\tsubscription.credits);\n\t}\n\tsetCustomStatus(\n\t\ttr::lng_group_invite_joined_status(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\tlangDayOfMonthFull(base::unixtime::parse(date).date())));\n}\n\nQSize SubscriptionRow::rightActionSize() const {\n\treturn _rightLabel ? _rightLabel->size : QSize();\n}\n\nQMargins SubscriptionRow::rightActionMargins() const {\n\treturn QMargins(0, 0, st::boxRowPadding.right(), 0);\n}\n\nvoid SubscriptionRow::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\tif (_rightLabel) {\n\t\treturn _rightLabel->draw(p, x, y, st::peerListBoxItem.height);\n\t}\n}\n\nclass RequestedRow final : public PeerListRow {\npublic:\n\tRequestedRow(not_null<PeerData*> peer, TimeId date);\n\n\tQSize rightActionSize() const override;\n\tQMargins rightActionMargins() const override;\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\n};\n\nRequestedRow::RequestedRow(not_null<PeerData*> peer, TimeId date)\n: PeerListRow(peer) {\n\tsetCustomStatus(PrepareRequestedRowStatus(date));\n}\n\nQSize RequestedRow::rightActionSize() const {\n\treturn QSize(\n\t\tst::inviteLinkThreeDotsIcon.width(),\n\t\tst::inviteLinkThreeDotsIcon.height());\n}\n\nQMargins RequestedRow::rightActionMargins() const {\n\treturn QMargins(\n\t\t0,\n\t\t(st::peerListBoxItem.height - rightActionSize().height()) / 2,\n\t\tst::inviteLinkThreeDotsSkip,\n\t\t0);\n}\n\nvoid RequestedRow::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\t(actionSelected\n\t\t? st::inviteLinkThreeDotsIconOver\n\t\t: st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth);\n}\n\nclass Controller final\n\t: public PeerListController\n\t, public base::has_weak_ptr {\npublic:\n\tenum class Role {\n\t\tRequested,\n\t\tJoined,\n\t};\n\tController(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\trpl::producer<LinkData> data,\n\t\tRole role);\n\n\tvoid prepare() override;\n\tvoid loadMoreRows() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override;\n\tMain::Session &session() const override;\n\n\trpl::producer<int> boxHeightValue() const override;\n\tint descriptionTopSkipMin() const override;\n\n\tstruct Processed {\n\t\tnot_null<UserData*> user;\n\t\tbool approved = false;\n\t};\n\t[[nodiscard]] rpl::producer<Processed> processed() const {\n\t\treturn _processed.events();\n\t}\n\nprivate:\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\n\tvoid setupAboveJoinedWidget();\n\tvoid appendSlice(const Api::JoinedByLinkSlice &slice);\n\tvoid addHeaderBlock(not_null<Ui::VerticalLayout*> container);\n\tnot_null<Ui::SlideWrap<>*> addRequestedListBlock(\n\t\tnot_null<Ui::VerticalLayout*> container);\n\tvoid updateWithProcessed(Processed processed);\n\n\t[[nodiscard]] rpl::producer<LinkData> dataValue() const;\n\n\t[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row);\n\tvoid processRequest(not_null<UserData*> user, bool approved);\n\n\tconst not_null<PeerData*> _peer;\n\tconst Role _role = Role::Joined;\n\trpl::variable<LinkData> _data;\n\n\tUi::Text::CustomEmojiHelper _emojiHelper;\n\tTextWithEntities _creditsEmoji;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\trpl::event_stream<Processed> _processed;\n\n\tQString _link;\n\tbool _revoked = false;\n\n\tmtpRequestId _requestId = 0;\n\tstd::optional<Api::JoinedByLinkUser> _lastUser;\n\tbool _allLoaded = false;\n\n\tUi::RpWidget *_headerWidget = nullptr;\n\trpl::variable<int> _addedHeight;\n\n\tMTP::Sender _api;\n\trpl::lifetime _lifetime;\n\n};\n\nclass SingleRowController final : public PeerListController {\npublic:\n\tSingleRowController(\n\t\tnot_null<Data::Thread*> thread,\n\t\trpl::producer<QString> status,\n\t\tFn<void()> clicked);\n\n\tvoid prepare() override;\n\tvoid loadMoreRows() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tMain::Session &session() const override;\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\tconst base::weak_ptr<Data::Thread> _thread;\n\trpl::producer<QString> _status;\n\tFn<void()> _clicked;\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] bool ClosingLinkBox(const LinkData &updated, bool revoked) {\n\treturn updated.link.isEmpty() || (!revoked && updated.revoked);\n}\n\n#if 0\n\nQImage QrExact(const Qr::Data &data, int pixel, QColor color) {\n\tconst auto image = [](int size) {\n\t\tauto result = QImage(\n\t\t\tsize,\n\t\t\tsize,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tresult.fill(Qt::transparent);\n\t\t{\n\t\t\tQPainter p(&result);\n\t\t\tconst auto skip = size / 12;\n\t\t\tconst auto logoSize = size - 2 * skip;\n\t\t\tp.drawImage(\n\t\t\t\tskip,\n\t\t\t\tskip,\n\t\t\t\tWindow::LogoNoMargin().scaled(\n\t\t\t\t\tlogoSize,\n\t\t\t\t\tlogoSize,\n\t\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\t\tQt::SmoothTransformation));\n\t\t}\n\t\treturn result;\n\t};\n\treturn Qr::ReplaceCenter(\n\t\tQr::Generate(data, pixel, color),\n\t\timage(Qr::ReplaceSize(data, pixel)));\n}\n\nQImage Qr(const Qr::Data &data, int pixel, int max = 0) {\n\tExpects(data.size > 0);\n\n\tif (max > 0 && data.size * pixel > max) {\n\t\tpixel = std::max(max / data.size, 1);\n\t}\n\treturn QrExact(data, pixel * style::DevicePixelRatio(), st::windowFg->c);\n}\n\nQImage Qr(const QString &text, int pixel, int max) {\n\treturn Qr(Qr::Encode(text), pixel, max);\n}\n\nQImage QrForShare(const QString &text) {\n\tconst auto data = Qr::Encode(text);\n\tconst auto size = (kShareQrSize - 2 * kShareQrPadding);\n\tconst auto image = QrExact(data, size / data.size, Qt::black);\n\tauto result = QImage(\n\t\tkShareQrPadding * 2 + image.width(),\n\t\tkShareQrPadding * 2 + image.height(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::white);\n\t{\n\t\tauto p = QPainter(&result);\n\t\tp.drawImage(kShareQrPadding, kShareQrPadding, image);\n\t}\n\treturn result;\n}\n\nvoid QrBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst QString &link,\n\t\trpl::producer<QString> title,\n\t\trpl::producer<QString> about,\n\t\tFn<void(QImage, std::shared_ptr<Ui::Show>)> share) {\n\tbox->setTitle(std::move(title));\n\n\tbox->addButton(tr::lng_about_done(), [=] { box->closeBox(); });\n\n\tconst auto copyCallback = [=, show = box->uiShow()] {\n\t\tshare(QrForShare(link), show);\n\t};\n\n\tconst auto qr = Qr(\n\t\tlink,\n\t\tst::inviteLinkQrPixel,\n\t\tst::boxWidth - st::boxRowPadding.left() - st::boxRowPadding.right());\n\tconst auto size = qr.width() / style::DevicePixelRatio();\n\tconst auto height = st::inviteLinkQrSkip * 2 + size;\n\tconst auto container = box->addRow(\n\t\tobject_ptr<Ui::BoxContentDivider>(box, height),\n\t\tst::inviteLinkQrMargin);\n\tconst auto button = Ui::CreateChild<Ui::AbstractButton>(container);\n\tbutton->resize(size, size);\n\tbutton->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tQPainter(button).drawImage(QRect(0, 0, size, size), qr);\n\t}, button->lifetime());\n\tcontainer->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tbutton->move((width - size) / 2, st::inviteLinkQrSkip);\n\t}, button->lifetime());\n\tbutton->setClickedCallback(copyCallback);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(about),\n\t\t\tst::boxLabel),\n\t\tst::inviteLinkQrValuePadding);\n\n\tbox->addLeftButton(tr::lng_group_invite_context_copy(), copyCallback);\n}\n\n#endif\n\nController::Controller(\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> admin,\n\trpl::producer<LinkData> data,\n\tRole role)\n: _peer(peer)\n, _role(role)\n, _data(LinkData{ .admin = admin })\n, _api(&session().api().instance()) {\n\t_data = std::move(data);\n\tconst auto current = _data.current();\n\t_link = current.link;\n\t_revoked = current.revoked;\n\t_creditsEmoji = _emojiHelper.paletteDependent(\n\t\tUi::Earn::IconCreditsEmoji());\n}\n\nrpl::producer<LinkData> Controller::dataValue() const {\n\treturn _data.value(\n\t) | rpl::filter([=](const LinkData &data) {\n\t\treturn !ClosingLinkBox(data, _revoked);\n\t});\n}\n\nvoid Controller::addHeaderBlock(not_null<Ui::VerticalLayout*> container) {\n\tusing namespace Settings;\n\n\tconst auto current = _data.current();\n\tconst auto revoked = current.revoked;\n\tconst auto link = current.link;\n\tconst auto admin = current.admin;\n\tconst auto weak = base::make_weak(container);\n\tconst auto copyLink = crl::guard(weak, [=] {\n\t\tCopyInviteLink(delegate()->peerListUiShow(), link);\n\t});\n\tconst auto shareLink = crl::guard(weak, [=, peer = _peer] {\n\t\tdelegate()->peerListUiShow()->showBox(ShareInviteLinkBox(peer, link));\n\t});\n\tconst auto getLinkQr = crl::guard(weak, [=] {\n\t\tdelegate()->peerListUiShow()->showBox(InviteLinkQrBox(\n\t\t\t_peer,\n\t\t\tlink,\n\t\t\ttr::lng_group_invite_qr_title(),\n\t\t\ttr::lng_group_invite_qr_about()));\n\t});\n\tconst auto revokeLink = crl::guard(weak, [=] {\n\t\tdelegate()->peerListUiShow()->showBox(\n\t\t\tRevokeLinkBox(_peer, admin, link));\n\t});\n\tconst auto editLink = crl::guard(weak, [=] {\n\t\tdelegate()->peerListUiShow()->showBox(\n\t\t\tEditLinkBox(_peer, _data.current()));\n\t});\n\tconst auto deleteLink = crl::guard(weak, [=] {\n\t\tdelegate()->peerListUiShow()->showBox(\n\t\t\tDeleteLinkBox(_peer, admin, link));\n\t});\n\n\tconst auto createMenu = [=] {\n\t\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tcontainer,\n\t\t\tst::popupMenuWithIcons);\n\t\tif (revoked) {\n\t\t\tresult->addAction(\n\t\t\t\ttr::lng_group_invite_context_delete(tr::now),\n\t\t\t\tdeleteLink,\n\t\t\t\t&st::menuIconDelete);\n\t\t} else {\n\t\t\tresult->addAction(\n\t\t\t\ttr::lng_group_invite_context_copy(tr::now),\n\t\t\t\tcopyLink,\n\t\t\t\t&st::menuIconCopy);\n\t\t\tresult->addAction(\n\t\t\t\ttr::lng_group_invite_context_share(tr::now),\n\t\t\t\tshareLink,\n\t\t\t\t&st::menuIconShare);\n\t\t\tresult->addAction(\n\t\t\t\ttr::lng_group_invite_context_qr(tr::now),\n\t\t\t\tgetLinkQr,\n\t\t\t\t&st::menuIconQrCode);\n\t\t\tif (!admin->isBot()) {\n\t\t\t\tresult->addAction(\n\t\t\t\t\ttr::lng_group_invite_context_edit(tr::now),\n\t\t\t\t\teditLink,\n\t\t\t\t\t&st::menuIconEdit);\n\t\t\t\tresult->addAction(\n\t\t\t\t\ttr::lng_group_invite_context_revoke(tr::now),\n\t\t\t\t\trevokeLink,\n\t\t\t\t\t&st::menuIconRemove);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\n\tconst auto prefix = u\"https://\"_q;\n\tconst auto label = container->lifetime().make_state<Ui::InviteLinkLabel>(\n\t\tcontainer,\n\t\trpl::single(link.startsWith(prefix)\n\t\t\t? link.mid(prefix.size())\n\t\t\t: link),\n\t\tcreateMenu);\n\tcontainer->add(\n\t\tlabel->take(),\n\t\tst::inviteLinkFieldPadding);\n\n\tlabel->clicks(\n\t) | rpl::on_next(copyLink, label->lifetime());\n\n\tconst auto reactivateWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(\n\t\t\t\tcontainer)));\n\tconst auto copyShareWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(\n\t\t\t\tcontainer)));\n\n\tAddReactivateLinkButton(reactivateWrap->entity(), editLink);\n\tAddCopyShareLinkButtons(copyShareWrap->entity(), copyLink, shareLink);\n\tif (revoked) {\n\t\tAddDeleteLinkButton(container, deleteLink);\n\t}\n\n\tUi::AddSkip(container, st::inviteLinkJoinedRowPadding.bottom() * 2);\n\n\tauto grayLabelText = dataValue(\n\t) | rpl::map([=](const LinkData &data) {\n\t\tconst auto usageExpired = (data.usageLimit > 0)\n\t\t\t&& (data.usageLimit <= data.usage);\n\t\treturn usageExpired\n\t\t\t? tr::lng_group_invite_used_about()\n\t\t\t: tr::lng_group_invite_expires_at(\n\t\t\t\tlt_when,\n\t\t\t\trpl::single(langDateTime(\n\t\t\t\t\tbase::unixtime::parse(data.expireDate))));\n\t}) | rpl::flatten_latest();\n\n\tconst auto redLabelWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::DividerLabel>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tcontainer,\n\t\t\t\t\ttr::lng_group_invite_expired_about(),\n\t\t\t\t\tst::boxAttentionDividerLabel),\n\t\t\t\tst::defaultBoxDividerLabelPadding)));\n\tconst auto grayLabelWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::DividerLabel>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tcontainer,\n\t\t\t\t\tstd::move(grayLabelText),\n\t\t\t\t\tst::boxDividerLabel),\n\t\t\t\tst::defaultBoxDividerLabelPadding)));\n\tconst auto justDividerWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::BoxContentDivider>(container)));\n\tUi::AddSkip(container);\n\n\tdataValue(\n\t) | rpl::on_next([=](const LinkData &data) {\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto expired = IsExpiredLink(data, now);\n\t\treactivateWrap->toggle(\n\t\t\t!revoked && expired && !admin->isBot(),\n\t\t\tanim::type::instant);\n\t\tcopyShareWrap->toggle(!revoked && !expired, anim::type::instant);\n\n\t\tconst auto timeExpired = (data.expireDate > 0)\n\t\t\t&& (data.expireDate <= now);\n\t\tconst auto usageExpired = (data.usageLimit > 0)\n\t\t\t&& (data.usageLimit <= data.usage);\n\t\tredLabelWrap->toggle(!revoked && timeExpired, anim::type::instant);\n\t\tgrayLabelWrap->toggle(\n\t\t\t!revoked && !timeExpired && (data.expireDate > 0 || usageExpired),\n\t\t\tanim::type::instant);\n\t\tjustDividerWrap->toggle(\n\t\t\trevoked || (!data.expireDate && !expired),\n\t\t\tanim::type::instant);\n\t}, lifetime());\n}\n\nnot_null<Ui::SlideWrap<>*> Controller::addRequestedListBlock(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tusing namespace Settings;\n\n\tauto result = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(\n\t\t\t\tcontainer)));\n\tconst auto wrap = result->entity();\n\t// Make this container occupy full width.\n\twrap->add(object_ptr<Ui::RpWidget>(wrap));\n\tUi::AddDivider(wrap);\n\tUi::AddSkip(wrap);\n\tauto requestedCount = dataValue(\n\t) | rpl::filter([](const LinkData &data) {\n\t\treturn data.requested > 0;\n\t}) | rpl::map([=](const LinkData &data) {\n\t\treturn float64(data.requested);\n\t});\n\tUi::AddSubsectionTitle(\n\t\twrap,\n\t\ttr::lng_group_invite_requested_full(\n\t\t\tlt_count_decimal,\n\t\t\tstd::move(requestedCount)));\n\n\tclass Delegate final : public PeerListContentDelegateSimple {\n\tpublic:\n\t\texplicit Delegate(std::shared_ptr<Main::SessionShow> show)\n\t\t: _show(std::move(show)) {\n\t\t}\n\n\t\tstd::shared_ptr<Main::SessionShow> peerListUiShow() override {\n\t\t\treturn _show;\n\t\t}\n\n\tprivate:\n\t\tconst std::shared_ptr<Main::SessionShow> _show;\n\n\t};\n\tconst auto delegate = container->lifetime().make_state<Delegate>(\n\t\tthis->delegate()->peerListUiShow());\n\n\tconst auto controller = container->lifetime().make_state<\n\t\tController\n\t>(_peer, _data.current().admin, _data.value(), Role::Requested);\n\tconst auto content = container->add(object_ptr<PeerListContent>(\n\t\tcontainer,\n\t\tcontroller));\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\n\tcontroller->processed(\n\t) | rpl::on_next([=](Processed processed) {\n\t\tupdateWithProcessed(processed);\n\t}, lifetime());\n\n\treturn result;\n}\n\nvoid Controller::prepare() {\n\tif (_role == Role::Joined) {\n\t\tsetupAboveJoinedWidget();\n\n\t\t_allLoaded = (_data.current().usage == 0);\n\n\t\tconst auto &inviteLinks = session().api().inviteLinks();\n\t\tconst auto slice = inviteLinks.joinedFirstSliceLoaded(_peer, _link);\n\t\tif (slice) {\n\t\t\tappendSlice(*slice);\n\t\t}\n\t} else {\n\t\t_allLoaded = (_data.current().requested == 0);\n\t}\n\tloadMoreRows();\n}\n\nvoid Controller::updateWithProcessed(Processed processed) {\n\tconst auto user = processed.user;\n\tauto updated = _data.current();\n\tif (processed.approved) {\n\t\t++updated.usage;\n\t\tif (!delegate()->peerListFindRow(user->id.value)) {\n\t\t\tdelegate()->peerListPrependRow(\n\t\t\t\tstd::make_unique<PeerListRow>(user));\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t}\n\tif (updated.requested > 0) {\n\t\t--updated.requested;\n\t}\n\tsession().api().inviteLinks().applyExternalUpdate(_peer, updated);\n}\n\nvoid Controller::setupAboveJoinedWidget() {\n\tusing namespace Settings;\n\n\tauto header = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tconst auto container = header.data();\n\n\tconst auto current = _data.current();\n\tconst auto revoked = current.revoked;\n\tif (revoked || !current.permanent) {\n\t\taddHeaderBlock(container);\n\t}\n\tif (current.subscription) {\n\t\tconst auto &st = st::peerListSingleRow.item;\n\t\tUi::AddSubsectionTitle(\n\t\t\tcontainer,\n\t\t\ttr::lng_group_invite_subscription_info_subtitle());\n\t\tconst auto widget = container->add(\n\t\t\tCreateSkipWidget(container, st.height));\n\t\tconst auto name = widget->lifetime().make_state<Ui::Text::String>();\n\t\tauto userpic = QImage(\n\t\t\tSize(st.photoSize) * style::DevicePixelRatio(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t{\n\t\t\tconstexpr auto kGreenIndex = 3;\n\t\t\tconst auto colors = Ui::EmptyUserpic::UserpicColor(kGreenIndex);\n\t\t\tauto emptyUserpic = Ui::EmptyUserpic(colors, {});\n\n\t\t\tuserpic.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\tuserpic.fill(Qt::transparent);\n\n\t\t\tauto p = QPainter(&userpic);\n\t\t\temptyUserpic.paintCircle(p, 0, 0, st.photoSize, st.photoSize);\n\n\t\t\tauto svg = QSvgRenderer(u\":/gui/links_subscription.svg\"_q);\n\t\t\tconst auto size = st.photoSize / 4. * 3.;\n\t\t\tconst auto r = QRectF(\n\t\t\t\t(st.photoSize - size) / 2.,\n\t\t\t\t(st.photoSize - size) / 2.,\n\t\t\t\tsize,\n\t\t\t\tsize);\n\t\t\tp.setPen(st::historyPeerUserpicFg);\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\tsvg.render(&p, r);\n\t\t}\n\t\tname->setMarkedText(\n\t\t\tst.nameStyle,\n\t\t\tcurrent.usage\n\t\t\t\t? tr::lng_group_invite_subscription_info_title(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\t_creditsEmoji,\n\t\t\t\t\tlt_price,\n\t\t\t\t\t{ QString::number(current.subscription.credits) },\n\t\t\t\t\tlt_multiplier,\n\t\t\t\t\tTextWithEntities{ .text = QString(QChar(0x00D7)) },\n\t\t\t\t\tlt_total,\n\t\t\t\t\t{ QString::number(current.usage) },\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_group_invite_subscription_info_title_none(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\t_creditsEmoji,\n\t\t\t\t\tlt_price,\n\t\t\t\t\t{ QString::number(current.subscription.credits) },\n\t\t\t\t\ttr::marked),\n\t\t\tkMarkupTextOptions,\n\t\t\t_emojiHelper.context([=] { widget->update(); }));\n\t\tauto &lifetime = widget->lifetime();\n\t\tconst auto rateValue = lifetime.make_state<rpl::variable<float64>>(\n\t\t\tsession().credits().rateValue(_peer));\n\t\tconst auto currency = u\"USD\"_q;\n\t\tconst auto allCredits = current.subscription.credits * current.usage;\n\t\twidget->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = Painter(widget);\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\tp.setPen(st.nameFg);\n\t\t\tname->draw(p, {\n\t\t\t\t.position = st.namePosition,\n\t\t\t\t.outerWidth = widget->width() - name->maxWidth(),\n\t\t\t\t.availableWidth = widget->width() - name->maxWidth(),\n\t\t\t});\n\n\t\t\tp.drawImage(st.photoPosition, userpic);\n\n\t\t\tconst auto rate = rateValue->current();\n\t\t\tconst auto status = (allCredits <= 0)\n\t\t\t\t? tr::lng_group_invite_no_joined(tr::now)\n\t\t\t\t: (rate > 0)\n\t\t\t\t? tr::lng_group_invite_subscription_info_about(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_total,\n\t\t\t\t\tUi::FillAmountAndCurrency(allCredits * rate, currency))\n\t\t\t\t: QString();\n\t\t\tp.setPen(st.statusFg);\n\t\t\tp.setFont(st::contactsStatusFont);\n\t\t\tp.drawTextLeft(\n\t\t\t\tst.statusPosition.x(),\n\t\t\t\tst.statusPosition.y(),\n\t\t\t\twidget->width() - st.statusPosition.x(),\n\t\t\t\tstatus);\n\t\t}, widget->lifetime());\n\t}\n\tUi::AddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_group_invite_created_by());\n\tAddSinglePeerRow(\n\t\tcontainer,\n\t\tcurrent.admin,\n\t\trpl::single(langDateTime(base::unixtime::parse(current.date))));\n\tUi::AddSkip(container, st::membersMarginBottom);\n\n\tauto requestedWrap = addRequestedListBlock(container);\n\n\tconst auto listHeaderWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(\n\t\t\t\tcontainer)));\n\tconst auto listHeader = listHeaderWrap->entity();\n\n\t// Make this container occupy full width.\n\tlistHeader->add(object_ptr<Ui::RpWidget>(listHeader));\n\n\tUi::AddDivider(listHeader);\n\tUi::AddSkip(listHeader);\n\n\tauto listHeaderText = dataValue(\n\t) | rpl::map([=](const LinkData &data) {\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto timeExpired = (data.expireDate > 0)\n\t\t\t&& (data.expireDate <= now);\n\t\tif (!revoked && !data.usage && data.usageLimit > 0 && !timeExpired) {\n\t\t\tauto description = object_ptr<Ui::FlatLabel>(\n\t\t\t\tnullptr,\n\t\t\t\ttr::lng_group_invite_can_join_via_link(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tdata.usageLimit),\n\t\t\t\tcomputeListSt().about);\n\t\t\tif (!delegate()->peerListFullRowsCount()) {\n\t\t\t\tusing namespace rpl::mappers;\n\t\t\t\t_addedHeight = description->heightValue(\n\t\t\t\t) | rpl::map(_1\n\t\t\t\t\t+ st::membersAboutLimitPadding.top()\n\t\t\t\t\t+ st::membersAboutLimitPadding.bottom());\n\t\t\t}\n\t\t\tdelegate()->peerListSetDescription(std::move(description));\n\t\t} else {\n\t\t\t_addedHeight = std::max(\n\t\t\t\tdata.usage,\n\t\t\t\tdelegate()->peerListFullRowsCount()\n\t\t\t) * computeListSt().item.height;\n\t\t\tdelegate()->peerListSetDescription(nullptr);\n\t\t}\n\t\tlistHeaderWrap->toggle(\n\t\t\t!revoked && (data.usage || (data.usageLimit > 0 && !timeExpired)),\n\t\t\tanim::type::instant);\n\t\tdelegate()->peerListRefreshRows();\n\t\treturn data.usage\n\t\t\t? tr::lng_group_invite_joined(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(data.usage)))\n\t\t\t: tr::lng_group_invite_no_joined();\n\t}) | rpl::flatten_latest();\n\tconst auto listTitle = AddSubsectionTitle(\n\t\tlistHeader,\n\t\tstd::move(listHeaderText));\n\tauto remainingText = dataValue(\n\t) | rpl::map([=](const LinkData &data) {\n\t\treturn !data.usageLimit\n\t\t\t? QString()\n\t\t\t: tr::lng_group_invite_remaining(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\tstd::max(data.usageLimit - data.usage, 0));\n\t});\n\tconst auto remaining = Ui::CreateChild<Ui::FlatLabel>(\n\t\tlistHeader,\n\t\tstd::move(remainingText),\n\t\tst::inviteLinkTitleRight);\n\tdataValue(\n\t) | rpl::on_next([=](const LinkData &data) {\n\t\tremaining->setTextColorOverride(\n\t\t\t(data.usageLimit && (data.usageLimit <= data.usage)\n\t\t\t\t? std::make_optional(st::boxTextFgError->c)\n\t\t\t\t: std::nullopt));\n\t\tif (revoked || (!data.usage && data.usageLimit > 0)) {\n\t\t\tremaining->hide();\n\t\t} else {\n\t\t\tremaining->show();\n\t\t}\n\n\t\trequestedWrap->toggle(data.requested > 0, anim::type::instant);\n\t}, remaining->lifetime());\n\n\trpl::combine(\n\t\tlistTitle->positionValue(),\n\t\tremaining->widthValue(),\n\t\tlistHeader->widthValue()\n\t) | rpl::on_next([=](\n\t\t\tQPoint position,\n\t\t\tint width,\n\t\t\tint outerWidth) {\n\t\tremaining->moveToRight(position.x(), position.y(), outerWidth);\n\t}, remaining->lifetime());\n\n\t_headerWidget = header.data();\n\n\tdelegate()->peerListSetAboveWidget(std::move(header));\n}\n\nvoid Controller::loadMoreRows() {\n\tif (_requestId || _allLoaded) {\n\t\treturn;\n\t}\n\tusing Flag = MTPmessages_GetChatInviteImporters::Flag;\n\t_requestId = _api.request(MTPmessages_GetChatInviteImporters(\n\t\tMTP_flags(Flag::f_link\n\t\t\t| (_role == Role::Requested ? Flag::f_requested : Flag(0))),\n\t\t_peer->input(),\n\t\tMTP_string(_link),\n\t\tMTPstring(), // q\n\t\tMTP_int(_lastUser ? _lastUser->date : 0),\n\t\t_lastUser ? _lastUser->user->inputUser() : MTP_inputUserEmpty(),\n\t\tMTP_int(_lastUser ? kPerPage : kFirstPage)\n\t)).done([=](const MTPmessages_ChatInviteImporters &result) {\n\t\t_requestId = 0;\n\t\tauto slice = Api::ParseJoinedByLinkSlice(_peer, result);\n\t\t_allLoaded = slice.users.empty();\n\t\tappendSlice(slice);\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t\t_allLoaded = true;\n\t}).send();\n}\n\nvoid Controller::appendSlice(const Api::JoinedByLinkSlice &slice) {\n\tfor (const auto &user : slice.users) {\n\t\t_lastUser = user;\n\t\tauto row = (_role == Role::Requested)\n\t\t\t? std::make_unique<RequestedRow>(user.user, user.date)\n\t\t\t: (_data.current().subscription)\n\t\t\t? std::make_unique<SubscriptionRow>(\n\t\t\t\tuser.user,\n\t\t\t\tuser.date,\n\t\t\t\t_data.current().subscription)\n\t\t\t: std::make_unique<PeerListRow>(user.user);\n\t\tif (_role != Role::Requested && user.viaFilterLink) {\n\t\t\trow->setCustomStatus(\n\t\t\t\ttr::lng_group_invite_joined_via_filter(tr::now));\n\t\t}\n\t\tdelegate()->peerListAppendRow(std::move(row));\n\t}\n\tdelegate()->peerListRefreshRows();\n\tif (delegate()->peerListFullRowsCount() > 0) {\n\t\t_addedHeight = std::max(\n\t\t\t_data.current().usage,\n\t\t\tdelegate()->peerListFullRowsCount()\n\t\t) * computeListSt().item.height;\n\t}\n}\n\nvoid Controller::rowClicked(not_null<PeerListRow*> row) {\n\tif (!_data.current().subscription) {\n\t\treturn ShowPeerInfoSync(row->peer());\n\t}\n\tconst auto channel = _peer;\n\tconst auto data = _data.current();\n\tconst auto show = delegate()->peerListUiShow();\n\tshow->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto w = Core::App().findWindow(box);\n\t\tconst auto controller = w ? w->sessionController() : nullptr;\n\t\tif (!controller) {\n\t\t\treturn;\n\t\t}\n\n\t\tbox->setStyle(st::giveawayGiftCodeBox);\n\t\tbox->setNoContentMargin(true);\n\n\t\tconst auto content = box->verticalLayout();\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\n\t\tconst auto photoSize = st::boostReplaceUserpic.photoSize;\n\t\tconst auto session = &row->peer()->session();\n\t\tcontent->add(\n\t\t\tSettings::SubscriptionUserpic(content, channel, photoSize),\n\t\t\tstyle::al_top);\n\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_credits_box_subscription_title(),\n\t\t\t\tst::creditsBoxAboutTitle),\n\t\t\tstyle::al_top);\n\n\t\tUi::AddSkip(content);\n\n\t\tconst auto subtitle1 = box->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\tst::creditsTopupPrice),\n\t\t\tstyle::al_top);\n\t\tsubtitle1->setMarkedText(\n\t\t\ttr::lng_credits_subscription_subtitle(\n\t\t\t\ttr::now,\n\t\t\t\tlt_emoji,\n\t\t\t\t_creditsEmoji,\n\t\t\t\tlt_cost,\n\t\t\t\t{ QString::number(data.subscription.credits) },\n\t\t\t\ttr::marked),\n\t\t\t_emojiHelper.context());\n\t\tconst auto subtitle2 = box->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\tst::creditsTopupPrice),\n\t\t\tstyle::al_top);\n\t\tsession->credits().rateValue(\n\t\t\tchannel\n\t\t) | rpl::on_next([=, currency = u\"USD\"_q](float64 rate) {\n\t\t\tsubtitle2->setText(\n\t\t\t\ttr::lng_credits_subscriber_subtitle(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_total,\n\t\t\t\t\tUi::FillAmountAndCurrency(\n\t\t\t\t\t\tdata.subscription.credits * rate,\n\t\t\t\t\t\tcurrency)));\n\t\t}, subtitle2->lifetime());\n\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\n\t\tconst auto show = controller->uiShow();\n\t\tAddSubscriberEntryTable(show, content, {}, row->peer(), data.date);\n\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_credits_box_out_about(\n\t\t\t\t\tlt_link,\n\t\t\t\t\ttr::lng_payments_terms_link(tr::url(\n\t\t\t\t\t\ttr::lng_credits_box_out_about_link(tr::now))),\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::creditsBoxAboutDivider),\n\t\t\tstyle::al_top);\n\n\t\tbox->addButton(tr::lng_box_ok(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t}));\n}\n\nvoid Controller::rowRightActionClicked(not_null<PeerListRow*> row) {\n\tif (_role != Role::Requested || _data.current().subscription) {\n\t\treturn;\n\t}\n\tdelegate()->peerListShowRowMenu(row, true);\n}\n\nbase::unique_qptr<Ui::PopupMenu> Controller::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tauto result = createRowContextMenu(parent, row);\n\n\tif (result) {\n\t\t// First clear _menu value, so that we don't check row positions yet.\n\t\tbase::take(_menu);\n\n\t\t// Here unique_qptr is used like a shared pointer, where\n\t\t// not the last destroyed pointer destroys the object, but the first.\n\t\t_menu = base::unique_qptr<Ui::PopupMenu>(result.get());\n\t}\n\n\treturn result;\n}\n\nbase::unique_qptr<Ui::PopupMenu> Controller::createRowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tconst auto user = row->peer()->asUser();\n\tAssert(user != nullptr);\n\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\tconst auto add = _peer->isBroadcast()\n\t\t? tr::lng_group_requests_add_channel(tr::now)\n\t\t: tr::lng_group_requests_add(tr::now);\n\tresult->addAction(add, [=] {\n\t\tprocessRequest(user, true);\n\t}, &st::menuIconInvite);\n\tresult->addAction(tr::lng_group_requests_dismiss(tr::now), [=] {\n\t\tprocessRequest(user, false);\n\t}, &st::menuIconRemove);\n\treturn result;\n}\n\nvoid Controller::processRequest(\n\t\tnot_null<UserData*> user,\n\t\tbool approved) {\n\tconst auto done = crl::guard(this, [=] {\n\t\t_processed.fire({ user, approved });\n\t\tif (const auto row = delegate()->peerListFindRow(user->id.value)) {\n\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t\tif (approved) {\n\t\t\tdelegate()->peerListUiShow()->showToast((_peer->isBroadcast()\n\t\t\t\t? tr::lng_group_requests_was_added_channel\n\t\t\t\t: tr::lng_group_requests_was_added)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::bold(user->name()),\n\t\t\t\t\ttr::marked));\n\t\t}\n\t});\n\tconst auto fail = crl::guard(this, [=] {\n\t\t_processed.fire({ user, false });\n\t});\n\tsession().api().inviteLinks().processRequest(\n\t\t_peer,\n\t\t_data.current().link,\n\t\tuser,\n\t\tapproved,\n\t\tdone,\n\t\tfail);\n}\n\nMain::Session &Controller::session() const {\n\treturn _peer->session();\n}\n\nrpl::producer<int> Controller::boxHeightValue() const {\n\tExpects(_headerWidget != nullptr);\n\n\treturn rpl::combine(\n\t\t_headerWidget->heightValue(),\n\t\t_addedHeight.value()\n\t) | rpl::map([=](int header, int description) {\n\t\tconst auto wrapped = description\n\t\t\t? (computeListSt().padding.top()\n\t\t\t\t+ description\n\t\t\t\t+ computeListSt().padding.bottom())\n\t\t\t: 0;\n\t\treturn std::min(header + wrapped, st::boxMaxListHeight);\n\t});\n}\n\nint Controller::descriptionTopSkipMin() const {\n\treturn 0;\n}\n\nSingleRowController::SingleRowController(\n\tnot_null<Data::Thread*> thread,\n\trpl::producer<QString> status,\n\tFn<void()> clicked)\n: _session(&thread->session())\n, _thread(thread)\n, _status(std::move(status))\n, _clicked(std::move(clicked)) {\n}\n\nvoid SingleRowController::prepare() {\n\tconst auto strong = _thread.get();\n\tif (!strong) {\n\t\treturn;\n\t}\n\tconst auto topic = strong->asTopic();\n\tconst auto sublist = strong->asSublist();\n\tauto row = topic\n\t\t? ChooseTopicBoxController::MakeRow(topic)\n\t\t: sublist\n\t\t? std::make_unique<PeerListRow>(sublist->sublistPeer())\n\t\t: std::make_unique<PeerListRow>(strong->peer());\n\tconst auto raw = row.get();\n\tif (_status) {\n\t\tstd::move(\n\t\t\t_status\n\t\t) | rpl::on_next([=](const QString &status) {\n\t\t\traw->setCustomStatus(status);\n\t\t\tdelegate()->peerListUpdateRow(raw);\n\t\t}, _lifetime);\n\t}\n\tdelegate()->peerListAppendRow(std::move(row));\n\tdelegate()->peerListRefreshRows();\n\n\tif (topic) {\n\t\ttopic->destroyed() | rpl::on_next([=] {\n\t\t\twhile (delegate()->peerListFullRowsCount()) {\n\t\t\t\tdelegate()->peerListRemoveRow(delegate()->peerListRowAt(0));\n\t\t\t}\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}, _lifetime);\n\t}\n}\n\nvoid SingleRowController::loadMoreRows() {\n}\n\nvoid SingleRowController::rowClicked(not_null<PeerListRow*> row) {\n\tif (const auto onstack = _clicked) {\n\t\tonstack();\n\t} else {\n\t\tShowPeerInfoSync(row->peer());\n\t}\n}\n\nMain::Session &SingleRowController::session() const {\n\treturn *_session;\n}\n\n} // namespace\n\nbool IsExpiredLink(const Api::InviteLink &data, TimeId now) {\n\treturn (data.expireDate > 0 && data.expireDate <= now)\n\t\t|| (data.usageLimit > 0 && data.usageLimit <= data.usage);\n}\n\nvoid AddSinglePeerRow(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<QString> status,\n\t\tFn<void()> clicked) {\n\tAddSinglePeerRow(\n\t\tcontainer,\n\t\tpeer->owner().history(peer),\n\t\tstd::move(status),\n\t\tstd::move(clicked));\n}\n\nvoid AddSinglePeerRow(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Data::Thread*> thread,\n\t\trpl::producer<QString> status,\n\t\tFn<void()> clicked) {\n\tconst auto delegate = container->lifetime().make_state<\n\t\tPeerListContentDelegateSimple\n\t>();\n\tconst auto controller = container->lifetime().make_state<\n\t\tSingleRowController\n\t>(thread, std::move(status), std::move(clicked));\n\tcontroller->setStyleOverrides(thread->asTopic()\n\t\t? &st::chooseTopicList\n\t\t: &st::peerListSingleRow);\n\tconst auto content = container->add(object_ptr<PeerListContent>(\n\t\tcontainer,\n\t\tcontroller));\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n}\n\nvoid AddPermanentLinkBlock(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\trpl::producer<Api::InviteLink> fromList) {\n\tstruct LinkData {\n\t\tQString link;\n\t\tint usage = 0;\n\t};\n\tconst auto value = container->lifetime().make_state<\n\t\trpl::variable<LinkData>\n\t>();\n\tconst auto currentLinkFields = container->lifetime().make_state<\n\t\tApi::InviteLink\n\t>(Api::InviteLink{ .admin = admin });\n\tif (admin->isSelf()) {\n\t\t*value = peer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tData::PeerUpdate::Flag::InviteLinks\n\t\t) | rpl::map([=] {\n\t\t\tconst auto &links = peer->session().api().inviteLinks().myLinks(\n\t\t\t\tpeer).links;\n\t\t\tconst auto link = links.empty() ? nullptr : &links.front();\n\t\t\tif (link && link->permanent && !link->revoked) {\n\t\t\t\t*currentLinkFields = *link;\n\t\t\t\treturn LinkData{ link->link, link->usage };\n\t\t\t}\n\t\t\treturn LinkData();\n\t\t});\n\t} else {\n\t\trpl::duplicate(\n\t\t\tfromList\n\t\t) | rpl::on_next([=](const Api::InviteLink &link) {\n\t\t\t*currentLinkFields = link;\n\t\t}, container->lifetime());\n\n\t\t*value = std::move(\n\t\t\tfromList\n\t\t) | rpl::map([](const Api::InviteLink &link) {\n\t\t\treturn LinkData{ link.link, link.usage };\n\t\t});\n\t}\n\tconst auto weak = base::make_weak(container);\n\tconst auto copyLink = crl::guard(weak, [=] {\n\t\tif (const auto current = value->current(); !current.link.isEmpty()) {\n\t\t\tCopyInviteLink(show, current.link);\n\t\t}\n\t});\n\tconst auto shareLink = crl::guard(weak, [=] {\n\t\tif (const auto current = value->current(); !current.link.isEmpty()) {\n\t\t\tshow->showBox(ShareInviteLinkBox(peer, current.link));\n\t\t}\n\t});\n\tconst auto getLinkQr = crl::guard(weak, [=] {\n\t\tif (const auto current = value->current(); !current.link.isEmpty()) {\n\t\t\tshow->showBox(InviteLinkQrBox(\n\t\t\t\tpeer,\n\t\t\t\tcurrent.link,\n\t\t\t\ttr::lng_group_invite_qr_title(),\n\t\t\t\ttr::lng_group_invite_qr_about()));\n\t\t}\n\t});\n\tconst auto revokeLink = crl::guard(weak, [=] {\n\t\tif (const auto current = value->current(); !current.link.isEmpty()) {\n\t\t\tshow->showBox(RevokeLinkBox(peer, admin, current.link, true));\n\t\t}\n\t});\n\n\tauto link = value->value(\n\t) | rpl::map([=](const LinkData &data) {\n\t\tconst auto prefix = u\"https://\"_q;\n\t\treturn data.link.startsWith(prefix)\n\t\t\t? data.link.mid(prefix.size())\n\t\t\t: data.link;\n\t});\n\tconst auto createMenu = [=] {\n\t\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tcontainer,\n\t\t\tst::popupMenuWithIcons);\n\t\tresult->addAction(\n\t\t\ttr::lng_group_invite_context_copy(tr::now),\n\t\t\tcopyLink,\n\t\t\t&st::menuIconCopy);\n\t\tresult->addAction(\n\t\t\ttr::lng_group_invite_context_share(tr::now),\n\t\t\tshareLink,\n\t\t\t&st::menuIconShare);\n\t\tresult->addAction(\n\t\t\ttr::lng_group_invite_context_qr(tr::now),\n\t\t\tgetLinkQr,\n\t\t\t&st::menuIconQrCode);\n\t\tif (!admin->isBot()) {\n\t\t\tresult->addAction(\n\t\t\t\ttr::lng_group_invite_context_revoke(tr::now),\n\t\t\t\trevokeLink,\n\t\t\t\t&st::menuIconRemove);\n\t\t}\n\t\treturn result;\n\t};\n\tconst auto label = container->lifetime().make_state<Ui::InviteLinkLabel>(\n\t\tcontainer,\n\t\tstd::move(link),\n\t\tcreateMenu);\n\tcontainer->add(\n\t\tlabel->take(),\n\t\tst::inviteLinkFieldPadding);\n\n\tlabel->clicks(\n\t) | rpl::on_next(copyLink, label->lifetime());\n\n\tAddCopyShareLinkButtons(container, copyLink, shareLink);\n\n\tstruct JoinedState {\n\t\tQImage cachedUserpics;\n\t\tstd::vector<HistoryView::UserpicInRow> list;\n\t\tint count = 0;\n\t\tbool allUserpicsLoaded = false;\n\t\trpl::variable<Ui::JoinedCountContent> content;\n\t\trpl::lifetime lifetime;\n\t};\n\tconst auto state = container->lifetime().make_state<JoinedState>();\n\tconst auto push = [=] {\n\t\tHistoryView::GenerateUserpicsInRow(\n\t\t\tstate->cachedUserpics,\n\t\t\tstate->list,\n\t\t\tst::inviteLinkUserpics,\n\t\t\t0);\n\t\tstate->allUserpicsLoaded = ranges::all_of(\n\t\t\tstate->list,\n\t\t\t[](const HistoryView::UserpicInRow &element) {\n\t\t\t\treturn !element.peer->hasUserpic()\n\t\t\t\t\t|| !Ui::PeerUserpicLoading(element.view);\n\t\t\t});\n\t\tstate->content = Ui::JoinedCountContent{\n\t\t\t.count = state->count,\n\t\t\t.userpics = state->cachedUserpics,\n\t\t};\n\t};\n\tvalue->value(\n\t) | rpl::map([=](const LinkData &data) {\n\t\treturn peer->session().api().inviteLinks().joinedFirstSliceValue(\n\t\t\tpeer,\n\t\t\tdata.link,\n\t\t\tdata.usage);\n\t}) | rpl::flatten_latest(\n\t) | rpl::on_next([=](const Api::JoinedByLinkSlice &slice) {\n\t\tauto list = std::vector<HistoryView::UserpicInRow>();\n\t\tconst auto take = std::min(int(slice.users.size()), kMaxShownJoined);\n\t\tlist.reserve(take);\n\t\tfor (const auto &item : slice.users) {\n\t\t\tconst auto i = ranges::find(\n\t\t\t\tstate->list,\n\t\t\t\titem.user,\n\t\t\t\t&HistoryView::UserpicInRow::peer);\n\t\t\tif (i != end(state->list)) {\n\t\t\t\tlist.push_back(std::move(*i));\n\t\t\t} else {\n\t\t\t\tlist.push_back({ item.user });\n\t\t\t}\n\t\t\tif (list.size() == take) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tstate->count = slice.count;\n\t\tstate->list = std::move(list);\n\t\tpush();\n\t}, state->lifetime);\n\n\tpeer->session().downloaderTaskFinished(\n\t) | rpl::filter([=] {\n\t\treturn !state->allUserpicsLoaded;\n\t}) | rpl::on_next([=] {\n\t\tauto pushing = false;\n\t\tstate->allUserpicsLoaded = true;\n\t\tfor (const auto &element : state->list) {\n\t\t\tif (!element.peer->hasUserpic()) {\n\t\t\t\tcontinue;\n\t\t\t} else if (element.peer->userpicUniqueKey(element.view)\n\t\t\t\t!= element.uniqueKey) {\n\t\t\t\tpushing = true;\n\t\t\t} else if (Ui::PeerUserpicLoading(element.view)) {\n\t\t\t\tstate->allUserpicsLoaded = false;\n\t\t\t}\n\t\t}\n\t\tif (pushing) {\n\t\t\tpush();\n\t\t}\n\t}, state->lifetime);\n\n\tUi::AddJoinedCountButton(\n\t\tcontainer,\n\t\tstate->content.value(),\n\t\tst::inviteLinkJoinedRowPadding\n\t)->setClickedCallback([=] {\n\t\tif (!currentLinkFields->link.isEmpty()) {\n\t\t\tshow->showBox(ShowInviteLinkBox(peer, *currentLinkFields));\n\t\t}\n\t});\n\n\tcontainer->add(object_ptr<Ui::SlideWrap<Ui::FixedHeightWidget>>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::FixedHeightWidget>(\n\t\t\tcontainer,\n\t\t\tst::inviteLinkJoinedRowPadding.bottom()))\n\t)->setDuration(0)->toggleOn(state->content.value(\n\t) | rpl::map([=](const Ui::JoinedCountContent &content) {\n\t\treturn (content.count <= 0);\n\t}));\n}\n\nvoid CopyInviteLink(std::shared_ptr<Ui::Show> show, const QString &link) {\n\tQGuiApplication::clipboard()->setText(link);\n\tshow->showToast(tr::lng_group_invite_copied(tr::now));\n}\n\nobject_ptr<Ui::BoxContent> ShareInviteLinkBox(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &link,\n\t\tconst QString &copied) {\n\treturn ShareInviteLinkBox(&peer->session(), link, copied);\n}\n\nobject_ptr<Ui::BoxContent> ShareInviteLinkBox(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &link,\n\t\tconst QString &copied) {\n\tconst auto sending = std::make_shared<bool>();\n\tconst auto box = std::make_shared<base::weak_qptr<ShareBox>>();\n\n\tconst auto showToast = [=](const QString &text) {\n\t\tif (*box) {\n\t\t\t(*box)->showToast(text);\n\t\t}\n\t};\n\n\tauto copyCallback = [=] {\n\t\tQGuiApplication::clipboard()->setText(link);\n\t\tshowToast(copied.isEmpty()\n\t\t\t? tr::lng_group_invite_copied(tr::now)\n\t\t\t: copied);\n\t};\n\tauto countMessagesCallback = [=](const TextWithTags &comment) {\n\t\treturn 1;\n\t};\n\tauto submitCallback = [=](\n\t\t\tstd::vector<not_null<Data::Thread*>> &&result,\n\t\t\tFn<bool()> checkPaid,\n\t\t\tTextWithTags &&comment,\n\t\t\tApi::SendOptions options,\n\t\t\tData::ForwardOptions) {\n\t\tif (*sending || result.empty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto errorWithThread = GetErrorForSending(\n\t\t\tresult,\n\t\t\t{ .text = &comment });\n\t\tif (errorWithThread.error) {\n\t\t\tif (*box) {\n\t\t\t\t(*box)->uiShow()->showBox(MakeSendErrorBox(\n\t\t\t\t\terrorWithThread,\n\t\t\t\t\tresult.size() > 1));\n\t\t\t}\n\t\t\treturn;\n\t\t} else if (!checkPaid()) {\n\t\t\treturn;\n\t\t}\n\n\t\t*sending = true;\n\t\tif (!comment.text.isEmpty()) {\n\t\t\tcomment.text = link + \"\\n\" + comment.text;\n\t\t\tconst auto add = link.size() + 1;\n\t\t\tfor (auto &tag : comment.tags) {\n\t\t\t\ttag.offset += add;\n\t\t\t}\n\t\t} else {\n\t\t\tcomment.text = link;\n\t\t}\n\t\tauto &api = session->api();\n\t\tfor (const auto &thread : result) {\n\t\t\tauto message = Api::MessageToSend(\n\t\t\t\tApi::SendAction(thread, options));\n\t\t\tmessage.textWithTags = comment;\n\t\t\tmessage.action.clearDraft = false;\n\t\t\tapi.sendMessage(std::move(message));\n\t\t}\n\t\tif (*box) {\n\t\t\tshowToast(tr::lng_share_done(tr::now));\n\t\t\t(*box)->closeBox();\n\t\t}\n\t};\n\tauto filterCallback = [](not_null<Data::Thread*> thread) {\n\t\tif (const auto user = thread->peer()->asUser()) {\n\t\t\tif (user->canSendIgnoreMoneyRestrictions()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn Data::CanSendTexts(thread);\n\t};\n\tauto object = Box<ShareBox>(ShareBox::Descriptor{\n\t\t.session = session,\n\t\t.copyCallback = std::move(copyCallback),\n\t\t.countMessagesCallback = std::move(countMessagesCallback),\n\t\t.submitCallback = std::move(submitCallback),\n\t\t.filterCallback = std::move(filterCallback),\n\t\t.asCopyCallback = nullptr,\n\t\t.moneyRestrictionError = ShareMessageMoneyRestrictionError(),\n\t});\n\t*box = base::make_weak(object.data());\n\treturn object;\n}\n\nobject_ptr<Ui::BoxContent> InviteLinkQrBox(\n\t\tPeerData *peer,\n\t\tconst QString &link,\n\t\trpl::producer<QString> title,\n\t\trpl::producer<QString> about) {\n\treturn Box([=, t = std::move(title), a = std::move(about)](\n\t\t\tnot_null<Ui::GenericBox*> box) {\n\t\tUi::FillPeerQrBox(box, peer, link, std::move(a));\n\t\tbox->setTitle(std::move(t));\n\t});\n}\n\nobject_ptr<Ui::BoxContent> EditLinkBox(\n\t\tnot_null<PeerData*> peer,\n\t\tconst Api::InviteLink &data) {\n\tconstexpr auto kPeriod = 3600 * 24 * 30;\n\tconstexpr auto kTestModePeriod = 300;\n\tconst auto creating = data.link.isEmpty();\n\tconst auto box = std::make_shared<base::weak_qptr<Ui::GenericBox>>();\n\tusing Fields = Ui::InviteLinkFields;\n\tconst auto done = [=](Fields result) {\n\t\tconst auto finish = [=](Api::InviteLink finished) {\n\t\t\tif (*box) {\n\t\t\t\tif (creating) {\n\t\t\t\t\t(*box)->getDelegate()->show(\n\t\t\t\t\t\tShowInviteLinkBox(peer, finished));\n\t\t\t\t}\n\t\t\t\t(*box)->closeBox();\n\t\t\t}\n\t\t};\n\t\tif (creating) {\n\t\t\tAssert(data.admin->isSelf());\n\t\t\tconst auto period = peer->session().isTestMode()\n\t\t\t\t? kTestModePeriod\n\t\t\t\t: kPeriod;\n\t\t\tpeer->session().api().inviteLinks().create({\n\t\t\t\tpeer,\n\t\t\t\tfinish,\n\t\t\t\tresult.label,\n\t\t\t\tresult.expireDate,\n\t\t\t\tresult.usageLimit,\n\t\t\t\tresult.requestApproval,\n\t\t\t\t{ uint64(result.subscriptionCredits), period },\n\t\t\t});\n\t\t} else if (result.subscriptionCredits) {\n\t\t\tpeer->session().api().inviteLinks().editTitle(\n\t\t\t\tpeer,\n\t\t\t\tdata.admin,\n\t\t\t\tresult.link,\n\t\t\t\tresult.label,\n\t\t\t\tfinish);\n\t\t} else {\n\t\t\tpeer->session().api().inviteLinks().edit(\n\t\t\t\tpeer,\n\t\t\t\tdata.admin,\n\t\t\t\tresult.link,\n\t\t\t\tresult.label,\n\t\t\t\tresult.expireDate,\n\t\t\t\tresult.usageLimit,\n\t\t\t\tresult.requestApproval,\n\t\t\t\tfinish);\n\t\t}\n\t};\n\tconst auto isGroup = !peer->isBroadcast();\n\tconst auto isPublic = peer->isChannel() && peer->asChannel()->isPublic();\n\tauto object = Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto fill = isGroup\n\t\t\t? Fn<Ui::InviteLinkSubscriptionToggle()>(nullptr)\n\t\t\t: [=] {\n\t\t\t\treturn Ui::FillCreateInviteLinkSubscriptionToggle(box, peer);\n\t\t\t};\n\t\tif (creating) {\n\t\t\tUi::CreateInviteLinkBox(box, fill, isGroup, isPublic, done);\n\t\t} else {\n\t\t\tUi::EditInviteLinkBox(\n\t\t\t\tbox,\n\t\t\t\tfill,\n\t\t\t\tFields{\n\t\t\t\t\t.link = data.link,\n\t\t\t\t\t.label = data.label,\n\t\t\t\t\t.expireDate = data.expireDate,\n\t\t\t\t\t.usageLimit = data.usageLimit,\n\t\t\t\t\t.subscriptionCredits = int(data.subscription.credits),\n\t\t\t\t\t.requestApproval = data.requestApproval,\n\t\t\t\t\t.isGroup = isGroup,\n\t\t\t\t\t.isPublic = isPublic,\n\t\t\t\t},\n\t\t\t\tdone);\n\t\t}\n\t});\n\t*box = base::make_weak(object.data());\n\treturn object;\n}\n\nobject_ptr<Ui::BoxContent> RevokeLinkBox(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst QString &link,\n\t\tbool permanent) {\n\tconst auto revoke = [=](Fn<void()> &&close) {\n\t\tauto &l = peer->session().api().inviteLinks();\n\t\tif (permanent) {\n\t\t\tl.revokePermanent(peer, admin, link, std::move(close));\n\t\t} else {\n\t\t\tauto done = [c = std::move(close)](const LinkData &) { c(); };\n\t\t\tl.revoke(peer, admin, link, std::move(done));\n\t\t}\n\t};\n\treturn Ui::MakeConfirmBox({\n\t\tpermanent\n\t\t\t? tr::lng_group_invite_about_new()\n\t\t\t: tr::lng_group_invite_revoke_about(),\n\t\trevoke\n\t});\n}\n\nobject_ptr<Ui::BoxContent> DeleteLinkBox(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tconst QString &link) {\n\tconst auto sure = [=](Fn<void()> &&close) {\n\t\tpeer->session().api().inviteLinks().destroy(\n\t\t\tpeer,\n\t\t\tadmin,\n\t\t\tlink,\n\t\t\tstd::move(close));\n\t};\n\treturn Ui::MakeConfirmBox({ tr::lng_group_invite_delete_sure(), sure });\n}\n\nobject_ptr<Ui::BoxContent> ShowInviteLinkBox(\n\t\tnot_null<PeerData*> peer,\n\t\tconst Api::InviteLink &link) {\n\tconst auto admin = link.admin;\n\tconst auto linkText = link.link;\n\tconst auto revoked = link.revoked;\n\n\tauto updates = peer->session().api().inviteLinks().updates(\n\t\tpeer,\n\t\tadmin\n\t) | rpl::filter([=](const Api::InviteLinkUpdate &update) {\n\t\treturn (update.was == linkText);\n\t}) | rpl::map([=](const Api::InviteLinkUpdate &update) {\n\t\treturn update.now ? *update.now : LinkData{ .admin = admin };\n\t});\n\tauto data = rpl::single(link) | rpl::then(std::move(updates));\n\n\tauto initBox = [=, data = rpl::duplicate(data)](\n\t\tnot_null<Ui::BoxContent*> box) {\n\t\trpl::duplicate(\n\t\t\tdata\n\t\t) | rpl::on_next([=](const LinkData &link) {\n\t\t\tif (ClosingLinkBox(link, revoked)) {\n\t\t\t\tbox->closeBox();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto now = base::unixtime::now();\n\t\t\tbox->setTitle(!link.label.isEmpty()\n\t\t\t\t? rpl::single(link.label)\n\t\t\t\t: link.revoked\n\t\t\t\t? tr::lng_manage_peer_link_invite()\n\t\t\t\t: IsExpiredLink(link, now)\n\t\t\t\t? tr::lng_manage_peer_link_expired()\n\t\t\t\t: link.permanent\n\t\t\t\t? tr::lng_manage_peer_link_permanent()\n\t\t\t\t: tr::lng_manage_peer_link_invite());\n\t\t}, box->lifetime());\n\n\t\tbox->addButton(tr::lng_about_done(), [=] { box->closeBox(); });\n\t};\n\treturn Box<PeerListBox>(\n\t\tstd::make_unique<Controller>(\n\t\t\tpeer,\n\t\t\tlink.admin,\n\t\t\tstd::move(data),\n\t\t\tController::Role::Joined),\n\t\tstd::move(initBox));\n}\n\nQString PrepareRequestedRowStatus(TimeId date) {\n\tconst auto now = QDateTime::currentDateTime();\n\tconst auto parsed = base::unixtime::parse(date);\n\tconst auto parsedDate = parsed.date();\n\tconst auto time = QLocale().toString(parsed.time(), QLocale::ShortFormat);\n\tconst auto generic = [&] {\n\t\treturn tr::lng_group_requests_status_date_time(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\tlangDayOfMonth(parsedDate),\n\t\t\tlt_time,\n\t\t\ttime);\n\t};\n\treturn (parsedDate.addDays(1) < now.date())\n\t\t? generic()\n\t\t: (parsedDate.addDays(1) == now.date())\n\t\t? tr::lng_group_requests_status_yesterday(tr::now, lt_time, time)\n\t\t: (now.date() == parsedDate)\n\t\t? tr::lng_group_requests_status_today(tr::now, lt_time, time)\n\t\t: generic();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\ntemplate <typename Object>\nclass object_ptr;\n\nclass PeerData;\n\nnamespace Api {\nstruct InviteLink;\n} // namespace Api\n\nnamespace Data {\nclass Thread;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass VerticalLayout;\nclass Show;\nclass BoxContent;\n} // namespace Ui\n\n[[nodiscard]] bool IsExpiredLink(const Api::InviteLink &data, TimeId now);\n\nvoid AddSinglePeerRow(\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<PeerData*> peer,\n\trpl::producer<QString> status,\n\tFn<void()> clicked = nullptr);\n\nvoid AddSinglePeerRow(\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<Data::Thread*> thread,\n\trpl::producer<QString> status,\n\tFn<void()> clicked = nullptr);\n\nvoid AddPermanentLinkBlock(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> admin,\n\trpl::producer<Api::InviteLink> fromList);\n\nvoid CopyInviteLink(std::shared_ptr<Ui::Show> show, const QString &link);\n[[nodiscard]] object_ptr<Ui::BoxContent> ShareInviteLinkBox(\n\tnot_null<PeerData*> peer,\n\tconst QString &link,\n\tconst QString &copied = {});\n[[nodiscard]] object_ptr<Ui::BoxContent> ShareInviteLinkBox(\n\tnot_null<Main::Session*> session,\n\tconst QString &link,\n\tconst QString &copied = {});\n[[nodiscard]] object_ptr<Ui::BoxContent> InviteLinkQrBox(\n\tPeerData *peer,\n\tconst QString &link,\n\trpl::producer<QString> title,\n\trpl::producer<QString> about);\n[[nodiscard]] object_ptr<Ui::BoxContent> RevokeLinkBox(\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> admin,\n\tconst QString &link,\n\tbool permanent = false);\n[[nodiscard]] object_ptr<Ui::BoxContent> EditLinkBox(\n\tnot_null<PeerData*> peer,\n\tconst Api::InviteLink &data);\n[[nodiscard]] object_ptr<Ui::BoxContent> DeleteLinkBox(\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> admin,\n\tconst QString &link);\n\n[[nodiscard]] object_ptr<Ui::BoxContent> ShowInviteLinkBox(\n\tnot_null<PeerData*> peer,\n\tconst Api::InviteLink &link);\n\n[[nodiscard]] QString PrepareRequestedRowStatus(TimeId date);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_peer_invite_links.h\"\n\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"main/session/session_show.h\"\n#include \"main/main_session.h\"\n#include \"api/api_invite_links.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/vertical_list.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/peers/edit_peer_invite_link.h\"\n#include \"apiwrap.h\"\n#include \"base/weak_ptr.h\"\n#include \"base/unixtime.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\" // st::boxDividerLabel\n#include \"styles/style_menu_icons.h\"\n\n#include <QtSvg/QSvgRenderer>\n\nnamespace {\n\nenum class Color {\n\tPermanent,\n\tExpiring,\n\tExpireSoon,\n\tExpired,\n\tRevoked,\n\tSubscription,\n\n\tCount,\n};\n\nusing InviteLinkData = Api::InviteLink;\nusing InviteLinksSlice = Api::PeerInviteLinks;\n\nstruct InviteLinkAction {\n\tenum class Type {\n\t\tCopy,\n\t\tShare,\n\t\tEdit,\n\t\tRevoke,\n\t\tDelete,\n\t};\n\tQString link;\n\tType type = Type::Copy;\n};\n\nclass Row;\n\nusing SubscriptionRightLabel = Settings::SubscriptionRightLabel;\n\nclass RowDelegate {\npublic:\n\tvirtual std::optional<SubscriptionRightLabel> rightLabel(\n\t\tint credits) const = 0;\n\tvirtual void rowUpdateRow(not_null<Row*> row) = 0;\n\tvirtual void rowPaintIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tfloat64 progress,\n\t\tColor color) = 0;\n};\n\nclass Row final : public PeerListRow {\npublic:\n\tRow(\n\t\tnot_null<RowDelegate*> delegate,\n\t\tconst InviteLinkData &data,\n\t\tTimeId now);\n\n\tvoid update(const InviteLinkData &data, TimeId now);\n\tvoid updateExpireProgress(TimeId now);\n\n\t[[nodiscard]] InviteLinkData data() const;\n\t[[nodiscard]] crl::time updateExpireIn() const;\n\n\tQString generateName() override;\n\tQString generateShortName() override;\n\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\n\tQSize rightActionSize() const override;\n\tbool rightActionDisabled() const override;\n\tQMargins rightActionMargins() const override;\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\nprivate:\n\tconst not_null<RowDelegate*> _delegate;\n\tstd::optional<SubscriptionRightLabel> _rightLabel;\n\tInviteLinkData _data;\n\tQString _status;\n\tfloat64 _progressTillExpire = 0.;\n\tColor _color = Color::Permanent;\n\n};\n\n[[nodiscard]] uint64 ComputeRowId(const InviteLinkData &data) {\n\treturn UniqueRowIdFromString(data.link);\n}\n\n[[nodiscard]] float64 ComputeProgress(\n\t\tconst InviteLinkData &link,\n\t\tTimeId now) {\n\tconst auto startDate = link.startDate ? link.startDate : link.date;\n\tif (link.expireDate <= startDate && link.usageLimit <= 0) {\n\t\treturn -1;\n\t}\n\tconst auto expireProgress = (link.expireDate <= startDate\n\t\t|| now <= startDate)\n\t\t? 0.\n\t\t: (link.expireDate <= now)\n\t\t? 1.\n\t\t: (now - startDate) / float64(link.expireDate - startDate);\n\tconst auto usageProgress = (link.usageLimit <= 0 || link.usage <= 0)\n\t\t? 0.\n\t\t: (link.usageLimit <= link.usage)\n\t\t? 1.\n\t\t: link.usage / float64(link.usageLimit);\n\treturn std::max(expireProgress, usageProgress);\n}\n\n[[nodiscard]] Color ComputeColor(\n\t\tconst InviteLinkData &link,\n\t\tfloat64 progress) {\n\treturn link.subscription\n\t\t? Color::Subscription\n\t\t: link.revoked\n\t\t? Color::Revoked\n\t\t: (progress >= 1.)\n\t\t? Color::Expired\n\t\t: (progress >= 3 / 4.)\n\t\t? Color::ExpireSoon\n\t\t: (progress >= 0.)\n\t\t? Color::Expiring\n\t\t: Color::Permanent;\n}\n\n[[nodiscard]] QString ComputeStatus(const InviteLinkData &link, TimeId now) {\n\tconst auto expired = IsExpiredLink(link, now);\n\tconst auto revoked = link.revoked;\n\tauto result = link.usage\n\t\t? tr::lng_group_invite_joined(tr::now, lt_count_decimal, link.usage)\n\t\t: (!expired && !revoked && link.usageLimit > 0)\n\t\t? tr::lng_group_invite_can_join(\n\t\t\ttr::now,\n\t\t\tlt_count_decimal,\n\t\t\tlink.usageLimit)\n\t\t: tr::lng_group_invite_no_joined(tr::now);\n\tconst auto add = [&](const QString &text) {\n\t\tresult += ' ' + Ui::kQBullet + ' ' + text;\n\t};\n\tif (revoked) {\n\t\treturn result;\n\t} else if (expired) {\n\t\tadd(tr::lng_group_invite_link_expired(tr::now));\n\t\treturn result;\n\t}\n\tif (link.usage > 0 && link.usageLimit > link.usage) {\n\t\tresult += \", \" + tr::lng_group_invite_remaining(\n\t\t\ttr::now,\n\t\t\tlt_count_decimal,\n\t\t\tlink.usageLimit - link.usage);\n\t} else if (link.usage > 0 && link.requested > 0) {\n\t\tresult += \", \" + tr::lng_group_invite_requested(\n\t\t\ttr::now,\n\t\t\tlt_count_decimal,\n\t\t\tlink.requested);\n\t} else if (link.requested > 0) {\n\t\tresult = tr::lng_group_invite_requested_full(\n\t\t\ttr::now,\n\t\t\tlt_count_decimal,\n\t\t\tlink.requested);\n\t}\n\tif (link.expireDate > now) {\n\t\tconst auto left = (link.expireDate - now);\n\t\tif (left >= 86400) {\n\t\t\tadd(tr::lng_group_invite_days_left(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tleft / 86400));\n\t\t} else {\n\t\t\tconst auto time = base::unixtime::parse(link.expireDate).time();\n\t\t\tadd(QLocale().toString(time, QLocale::LongFormat));\n\t\t}\n\t}\n\treturn result;\n}\n\nobject_ptr<Ui::BoxContent> DeleteAllRevokedBox(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin) {\n\tconst auto sure = [=](Fn<void()> &&close) {\n\t\tpeer->session().api().inviteLinks().destroyAllRevoked(\n\t\t\tpeer,\n\t\t\tadmin,\n\t\t\tstd::move(close));\n\t};\n\treturn Ui::MakeConfirmBox({\n\t\ttr::lng_group_invite_delete_all_sure(),\n\t\tsure\n\t});\n}\n\n[[nodiscard]] not_null<Ui::SettingsButton*> AddCreateLinkButton(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\treturn container->add(\n\t\tMakeCreateLinkButton(container, tr::lng_group_invite_add()),\n\t\tstyle::margins(0, st::inviteLinkCreateSkip, 0, 0));\n}\n\nRow::Row(\n\tnot_null<RowDelegate*> delegate,\n\tconst InviteLinkData &data,\n\tTimeId now)\n: PeerListRow(ComputeRowId(data))\n, _delegate(delegate)\n, _data(data)\n, _progressTillExpire(ComputeProgress(data, now))\n, _color(ComputeColor(data, _progressTillExpire)) {\n\t_rightLabel = _delegate->rightLabel(_data.subscription.credits);\n\tsetCustomStatus(ComputeStatus(data, now));\n}\n\nvoid Row::update(const InviteLinkData &data, TimeId now) {\n\t_data = data;\n\t_rightLabel = _delegate->rightLabel(_data.subscription.credits);\n\t_progressTillExpire = ComputeProgress(data, now);\n\t_color = ComputeColor(data, _progressTillExpire);\n\tsetCustomStatus(ComputeStatus(data, now));\n\trefreshName(st::inviteLinkList.item);\n\t_delegate->rowUpdateRow(this);\n}\n\nvoid Row::updateExpireProgress(TimeId now) {\n\tconst auto updated = ComputeProgress(_data, now);\n\tif (base::SafeRound(_progressTillExpire * 360)\n\t\t!= base::SafeRound(updated * 360)) {\n\t\t_progressTillExpire = updated;\n\t\tconst auto color = ComputeColor(_data, _progressTillExpire);\n\t\tif (_color != color) {\n\t\t\t_color = color;\n\t\t\tsetCustomStatus(ComputeStatus(_data, now));\n\t\t}\n\t\t_delegate->rowUpdateRow(this);\n\t}\n}\n\nInviteLinkData Row::data() const {\n\treturn _data;\n}\n\ncrl::time Row::updateExpireIn() const {\n\tif (_color != Color::Expiring && _color != Color::ExpireSoon) {\n\t\treturn 0;\n\t}\n\tconst auto start = _data.startDate ? _data.startDate : _data.date;\n\tif (_data.expireDate <= start) {\n\t\treturn 0;\n\t}\n\treturn base::SafeRound(\n\t\t(_data.expireDate - start) * crl::time(1000) / 720.);\n}\n\nQString Row::generateName() {\n\tif (!_data.label.isEmpty()) {\n\t\treturn _data.label;\n\t}\n\tauto result = _data.link;\n\treturn result.replace(\n\t\tu\"https://\"_q,\n\t\tQString()\n\t).replace(\n\t\tu\"t.me/+\"_q,\n\t\tQString()\n\t).replace(\n\t\tu\"t.me/joinchat/\"_q,\n\t\tQString()\n\t);\n}\n\nQString Row::generateShortName() {\n\treturn generateName();\n}\n\nPaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) {\n\treturn [=](\n\t\t\tQPainter &p,\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tint outerWidth,\n\t\t\tint size) {\n\t\t_delegate->rowPaintIcon(p, x, y, size, _progressTillExpire, _color);\n\t};\n}\n\nQSize Row::rightActionSize() const {\n\tif (_rightLabel) {\n\t\treturn _rightLabel->size;\n\t}\n\treturn QSize(\n\t\tst::inviteLinkThreeDotsIcon.width(),\n\t\tst::inviteLinkThreeDotsIcon.height());\n}\n\nbool Row::rightActionDisabled() const {\n\treturn _rightLabel.has_value();\n}\n\nQMargins Row::rightActionMargins() const {\n\tif (_rightLabel) {\n\t\treturn QMargins(0, 0, st::boxRowPadding.right(), 0);\n\t}\n\treturn QMargins(\n\t\t0,\n\t\t(st::inviteLinkList.item.height - rightActionSize().height()) / 2,\n\t\tst::inviteLinkThreeDotsSkip,\n\t\t0);\n}\n\nvoid Row::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\tif (_rightLabel) {\n\t\treturn _rightLabel->draw(p, x, y, st::inviteLinkList.item.height);\n\t}\n\t(actionSelected\n\t\t? st::inviteLinkThreeDotsIconOver\n\t\t: st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth);\n}\n\nclass LinksController final\n\t: public PeerListController\n\t, public RowDelegate\n\t, public base::has_weak_ptr {\npublic:\n\tLinksController(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tint count,\n\t\tbool revoked);\n\n\t[[nodiscard]] rpl::producer<int> fullCountValue() const {\n\t\treturn _count.value();\n\t}\n\n\tvoid prepare() override;\n\tvoid loadMoreRows() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override;\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\tMain::Session &session() const override;\n\n\tstd::optional<SubscriptionRightLabel> rightLabel(int) const override;\n\tvoid rowUpdateRow(not_null<Row*> row) override;\n\tvoid rowPaintIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tfloat64 progress,\n\t\tColor color) override;\n\n\t[[nodiscard]] rpl::producer<InviteLinkData> permanentFound() const {\n\t\treturn _permanentFound.events();\n\t}\n\nprivate:\n\tvoid appendRow(const InviteLinkData &data, TimeId now);\n\tvoid prependRow(const InviteLinkData &data, TimeId now);\n\tvoid updateRow(const InviteLinkData &data, TimeId now);\n\tbool removeRow(const QString &link);\n\n\tvoid appendSlice(const InviteLinksSlice &slice);\n\tvoid checkExpiringTimer(not_null<Row*> row);\n\tvoid expiringProgressTimer();\n\n\t[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row);\n\n\tconst not_null<PeerData*> _peer;\n\tconst not_null<UserData*> _admin;\n\tconst bool _revoked = false;\n\trpl::variable<int> _count;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tQString _offsetLink;\n\tTimeId _offsetDate = 0;\n\tbool _requesting = false;\n\tbool _allLoaded = false;\n\n\trpl::event_stream<InviteLinkData> _permanentFound;\n\tbase::flat_set<not_null<Row*>> _expiringRows;\n\tbase::Timer _updateExpiringTimer;\n\n\tstd::array<QImage, int(Color::Count)> _icons;\n\trpl::lifetime _lifetime;\n\n};\n\nLinksController::LinksController(\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> admin,\n\tint count,\n\tbool revoked)\n: _peer(peer)\n, _admin(admin)\n, _revoked(revoked)\n, _count(count)\n, _updateExpiringTimer([=] { expiringProgressTimer(); }) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tfor (auto &image : _icons) {\n\t\t\timage = QImage();\n\t\t}\n\t}, _lifetime);\n\n\tpeer->session().api().inviteLinks().updates(\n\t\tpeer,\n\t\tadmin\n\t) | rpl::on_next([=](const Api::InviteLinkUpdate &update) {\n\t\tconst auto now = base::unixtime::now();\n\t\tif (!update.now || update.now->revoked != _revoked) {\n\t\t\tif (removeRow(update.was)) {\n\t\t\t\tdelegate()->peerListRefreshRows();\n\t\t\t}\n\t\t} else if (update.was.isEmpty()) {\n\t\t\tif (update.now->permanent && !update.now->revoked) {\n\t\t\t\t_permanentFound.fire_copy(*update.now);\n\t\t\t} else {\n\t\t\t\tprependRow(*update.now, now);\n\t\t\t\tdelegate()->peerListRefreshRows();\n\t\t\t}\n\t\t} else {\n\t\t\tupdateRow(*update.now, now);\n\t\t}\n\t}, _lifetime);\n\n\tif (_revoked) {\n\t\tpeer->session().api().inviteLinks().allRevokedDestroyed(\n\t\t\tpeer,\n\t\t\tadmin\n\t\t) | rpl::on_next([=] {\n\t\t\t_requesting = false;\n\t\t\t_allLoaded = true;\n\t\t\twhile (delegate()->peerListFullRowsCount()) {\n\t\t\t\tdelegate()->peerListRemoveRow(delegate()->peerListRowAt(0));\n\t\t\t}\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}, _lifetime);\n\t}\n}\n\nvoid LinksController::prepare() {\n\tif (!_revoked && _admin->isSelf()) {\n\t\tappendSlice(_peer->session().api().inviteLinks().myLinks(_peer));\n\t}\n\tif (!delegate()->peerListFullRowsCount()) {\n\t\tloadMoreRows();\n\t}\n}\n\nvoid LinksController::loadMoreRows() {\n\tif (_requesting || _allLoaded) {\n\t\treturn;\n\t}\n\t_requesting = true;\n\tconst auto done = [=](const InviteLinksSlice &slice) {\n\t\tif (!_requesting) {\n\t\t\treturn;\n\t\t}\n\t\t_requesting = false;\n\t\tif (slice.links.empty()) {\n\t\t\t_allLoaded = true;\n\t\t\treturn;\n\t\t}\n\t\tappendSlice(slice);\n\t};\n\t_peer->session().api().inviteLinks().requestMoreLinks(\n\t\t_peer,\n\t\t_admin,\n\t\t_offsetDate,\n\t\t_offsetLink,\n\t\t_revoked,\n\t\tcrl::guard(this, done));\n}\n\nvoid LinksController::appendSlice(const InviteLinksSlice &slice) {\n\tconst auto now = base::unixtime::now();\n\tfor (const auto &link : slice.links) {\n\t\tif (link.permanent && !link.revoked) {\n\t\t\t_permanentFound.fire_copy(link);\n\t\t} else {\n\t\t\tappendRow(link, now);\n\t\t}\n\t\t_offsetLink = link.link;\n\t\t_offsetDate = link.date;\n\t}\n\tif (slice.links.size() >= slice.count) {\n\t\t_allLoaded = true;\n\t}\n\tconst auto rowsCount = delegate()->peerListFullRowsCount();\n\tconst auto minimalCount = _revoked ? rowsCount : (rowsCount + 1);\n\t_count = _allLoaded ? minimalCount : std::max(slice.count, minimalCount);\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid LinksController::rowClicked(not_null<PeerListRow*> row) {\n\tdelegate()->peerListUiShow()->showBox(\n\t\tShowInviteLinkBox(_peer, static_cast<Row*>(row.get())->data()));\n}\n\nvoid LinksController::rowRightActionClicked(not_null<PeerListRow*> row) {\n\tdelegate()->peerListShowRowMenu(row, true);\n}\n\nbase::unique_qptr<Ui::PopupMenu> LinksController::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tauto result = createRowContextMenu(parent, row);\n\n\tif (result) {\n\t\t// First clear _menu value, so that we don't check row positions yet.\n\t\tbase::take(_menu);\n\n\t\t// Here unique_qptr is used like a shared pointer, where\n\t\t// not the last destroyed pointer destroys the object, but the first.\n\t\t_menu = base::unique_qptr<Ui::PopupMenu>(result.get());\n\t}\n\n\treturn result;\n}\n\nbase::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tconst auto real = static_cast<Row*>(row.get());\n\tconst auto data = real->data();\n\tconst auto link = data.link;\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\tif (data.revoked) {\n\t\tresult->addAction(tr::lng_group_invite_context_delete(tr::now), [=] {\n\t\t\tdelegate()->peerListUiShow()->showBox(\n\t\t\t\tDeleteLinkBox(_peer, _admin, link));\n\t\t}, &st::menuIconDelete);\n\t} else {\n\t\tresult->addAction(tr::lng_group_invite_context_copy(tr::now), [=] {\n\t\t\tCopyInviteLink(delegate()->peerListUiShow(), link);\n\t\t}, &st::menuIconCopy);\n\t\tresult->addAction(tr::lng_group_invite_context_share(tr::now), [=] {\n\t\t\tdelegate()->peerListUiShow()->showBox(\n\t\t\t\tShareInviteLinkBox(_peer, link));\n\t\t}, &st::menuIconShare);\n\t\tresult->addAction(tr::lng_group_invite_context_qr(tr::now), [=] {\n\t\t\tdelegate()->peerListUiShow()->showBox(InviteLinkQrBox(\n\t\t\t\tnullptr,\n\t\t\t\tlink,\n\t\t\t\ttr::lng_group_invite_qr_title(),\n\t\t\t\ttr::lng_group_invite_qr_about()));\n\t\t}, &st::menuIconQrCode);\n\t\tresult->addAction(tr::lng_group_invite_context_edit(tr::now), [=] {\n\t\t\tdelegate()->peerListUiShow()->showBox(EditLinkBox(_peer, data));\n\t\t}, &st::menuIconEdit);\n\t\tresult->addAction(tr::lng_group_invite_context_revoke(tr::now), [=] {\n\t\t\tdelegate()->peerListUiShow()->showBox(\n\t\t\t\tRevokeLinkBox(_peer, _admin, link));\n\t\t}, &st::menuIconRemove);\n\t}\n\treturn result;\n}\n\nMain::Session &LinksController::session() const {\n\treturn _peer->session();\n}\n\nvoid LinksController::appendRow(const InviteLinkData &data, TimeId now) {\n\tdelegate()->peerListAppendRow(std::make_unique<Row>(this, data, now));\n}\n\nvoid LinksController::prependRow(const InviteLinkData &data, TimeId now) {\n\tdelegate()->peerListPrependRow(std::make_unique<Row>(this, data, now));\n}\n\nvoid LinksController::updateRow(const InviteLinkData &data, TimeId now) {\n\tif (const auto row = delegate()->peerListFindRow(ComputeRowId(data))) {\n\t\tconst auto real = static_cast<Row*>(row);\n\t\treal->update(data, now);\n\t\tcheckExpiringTimer(real);\n\t\tdelegate()->peerListUpdateRow(row);\n\t} else if (_revoked) {\n\t\tprependRow(data, now);\n\t\tdelegate()->peerListRefreshRows();\n\t}\n}\n\nbool LinksController::removeRow(const QString &link) {\n\tconst auto id = UniqueRowIdFromString(link);\n\tif (const auto row = delegate()->peerListFindRow(id)) {\n\t\tdelegate()->peerListRemoveRow(row);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid LinksController::checkExpiringTimer(not_null<Row*> row) {\n\tconst auto updateIn = row->updateExpireIn();\n\tif (updateIn > 0) {\n\t\t_expiringRows.emplace(row);\n\t\tif (!_updateExpiringTimer.isActive()\n\t\t\t|| updateIn < _updateExpiringTimer.remainingTime()) {\n\t\t\t_updateExpiringTimer.callOnce(updateIn);\n\t\t}\n\t} else {\n\t\t_expiringRows.remove(row);\n\t}\n}\n\nvoid LinksController::expiringProgressTimer() {\n\tconst auto now = base::unixtime::now();\n\tauto minimalIn = 0;\n\tfor (auto i = begin(_expiringRows); i != end(_expiringRows);) {\n\t\t(*i)->updateExpireProgress(now);\n\t\tconst auto updateIn = (*i)->updateExpireIn();\n\t\tif (!updateIn) {\n\t\t\ti = _expiringRows.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t\tif (!minimalIn || minimalIn > updateIn) {\n\t\t\t\tminimalIn = updateIn;\n\t\t\t}\n\t\t}\n\t}\n\tif (minimalIn) {\n\t\t_updateExpiringTimer.callOnce(minimalIn);\n\t}\n}\n\nstd::optional<SubscriptionRightLabel> LinksController::rightLabel(\n\t\tint credits) const {\n\tif (credits > 0) {\n\t\treturn Settings::PaintSubscriptionRightLabelCallback(\n\t\t\t&session(),\n\t\t\tst::inviteLinkList.item,\n\t\t\tcredits);\n\t}\n\treturn std::nullopt;\n}\n\nvoid LinksController::rowUpdateRow(not_null<Row*> row) {\n\tdelegate()->peerListUpdateRow(row);\n}\n\nvoid LinksController::rowPaintIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tfloat64 progress,\n\t\tColor color) {\n\tconst auto skip = st::inviteLinkIconSkip;\n\tconst auto inner = size - 2 * skip;\n\tconst auto bg = [&] {\n\t\tswitch (color) {\n\t\tcase Color::Permanent: return &st::msgFile1Bg;\n\t\tcase Color::Expiring: return &st::msgFile2Bg;\n\t\tcase Color::ExpireSoon: return &st::msgFile4Bg;\n\t\tcase Color::Expired: return &st::msgFile3Bg;\n\t\tcase Color::Revoked: return &st::windowSubTextFg;\n\t\tcase Color::Subscription: return &st::msgFile2Bg;\n\t\t}\n\t\tUnexpected(\"Color in LinksController::rowPaintIcon.\");\n\t}();\n\tconst auto stroke = st::inviteLinkIconStroke;\n\tauto &icon = _icons[int(color)];\n\tif (icon.isNull()) {\n\t\ticon = QImage(\n\t\t\tQSize(inner, inner) * style::DevicePixelRatio(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\ticon.fill(Qt::transparent);\n\t\ticon.setDevicePixelRatio(style::DevicePixelRatio());\n\n\t\tauto p = QPainter(&icon);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(*bg);\n\t\t{\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tconst auto rect = QRect(0, 0, inner, inner)\n\t\t\t\t- ((color == Color::Expiring || color == Color::ExpireSoon)\n\t\t\t\t\t? Margins(stroke)\n\t\t\t\t\t: Margins(0));\n\t\t\tp.drawEllipse(rect);\n\t\t}\n\t\tif (color == Color::Subscription) {\n\t\t\tauto svg = QSvgRenderer(u\":/gui/links_subscription.svg\"_q);\n\t\t\tconst auto r = QRect(\n\t\t\t\t(inner - st::inviteLinkSubscriptionSize) / 2,\n\t\t\t\t(inner - st::inviteLinkSubscriptionSize) / 2,\n\t\t\t\tst::inviteLinkSubscriptionSize,\n\t\t\t\tst::inviteLinkSubscriptionSize);\n\t\t\tsvg.render(&p, r);\n\t\t} else {\n\t\t\t(color == Color::Revoked\n\t\t\t\t? st::inviteLinkRevokedIcon\n\t\t\t\t: st::inviteLinkIcon).paintInCenter(p, Rect(Size(inner)));\n\t\t}\n\t}\n\tp.drawImage(x + skip, y + skip, icon);\n\tif (progress >= 0. && progress < 1.) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tauto pen = QPen((*bg)->c);\n\t\tpen.setWidth(stroke);\n\t\tpen.setCapStyle(Qt::RoundCap);\n\t\tp.setPen(pen);\n\t\tp.setBrush(Qt::NoBrush);\n\n\t\tconst auto margins = .5 * stroke;\n\t\tp.drawArc(QRectF(x + skip, y + skip, inner, inner).marginsAdded({\n\t\t\tmargins,\n\t\t\tmargins,\n\t\t\tmargins,\n\t\t\tmargins,\n\t\t}), arc::kQuarterLength, arc::kFullLength * (1. - progress));\n\t}\n}\n\nclass AdminsController final\n\t: public PeerListController\n\t, public base::has_weak_ptr {\npublic:\n\tAdminsController(not_null<PeerData*> peer, not_null<UserData*> admin);\n\t~AdminsController();\n\n\tvoid prepare() override;\n\tvoid loadMoreRows() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tMain::Session &session() const override;\n\nprivate:\n\tvoid appendRow(not_null<UserData*> user, int count);\n\n\tconst not_null<PeerData*> _peer;\n\tconst not_null<UserData*> _admin;\n\tmtpRequestId _requestId = 0;\n\n};\n\nAdminsController::AdminsController(\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> admin)\n: _peer(peer)\n, _admin(admin) {\n}\n\nAdminsController::~AdminsController() {\n\tsession().api().request(base::take(_requestId)).cancel();\n}\n\nvoid AdminsController::prepare() {\n\tif (const auto chat = _peer->asChat()) {\n\t\tif (!chat->amCreator()) {\n\t\t\treturn;\n\t\t}\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\tif (!channel->amCreator()) {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (!_admin->isSelf()) {\n\t\treturn;\n\t}\n\t_requestId = session().api().request(MTPmessages_GetAdminsWithInvites(\n\t\t_peer->input()\n\t)).done([=](const MTPmessages_ChatAdminsWithInvites &result) {\n\t\tresult.match([&](const MTPDmessages_chatAdminsWithInvites &data) {\n\t\t\tauto &owner = _peer->owner();\n\t\t\towner.processUsers(data.vusers());\n\t\t\tfor (const auto &admin : data.vadmins().v) {\n\t\t\t\tadmin.match([&](const MTPDchatAdminWithInvites &data) {\n\t\t\t\t\tconst auto adminId = data.vadmin_id();\n\t\t\t\t\tif (const auto user = owner.userLoaded(adminId)) {\n\t\t\t\t\t\tif (!user->isSelf()) {\n\t\t\t\t\t\t\tappendRow(user, data.vinvites_count().v);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t});\n\t}).send();\n}\n\nvoid AdminsController::loadMoreRows() {\n}\n\nvoid AdminsController::rowClicked(not_null<PeerListRow*> row) {\n\tdelegate()->peerListUiShow()->showBox(\n\t\tBox(ManageInviteLinksBox, _peer, row->peer()->asUser(), 0, 0));\n}\n\nMain::Session &AdminsController::session() const {\n\treturn _peer->session();\n}\n\nvoid AdminsController::appendRow(not_null<UserData*> user, int count) {\n\tauto row = std::make_unique<PeerListRow>(user);\n\trow->setCustomStatus(\n\t\ttr::lng_group_invite_other_count(tr::now, lt_count, count));\n\tdelegate()->peerListAppendRow(std::move(row));\n}\n\n} // namespace\n\nstruct LinksList {\n\tnot_null<Ui::RpWidget*> widget;\n\tnot_null<LinksController*> controller;\n};\n\nLinksList AddLinksList(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tint count,\n\t\tbool revoked) {\n\tauto &lifetime = container->lifetime();\n\tconst auto delegate = lifetime.make_state<PeerListContentDelegateShow>(\n\t\tshow);\n\tconst auto controller = lifetime.make_state<LinksController>(\n\t\tpeer,\n\t\tadmin,\n\t\tcount,\n\t\trevoked);\n\tcontroller->setStyleOverrides(&st::inviteLinkList);\n\tconst auto content = container->add(object_ptr<PeerListContent>(\n\t\tcontainer,\n\t\tcontroller));\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\n\treturn { content, controller };\n}\n\nnot_null<Ui::RpWidget*> AddAdminsList(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin) {\n\tauto &lifetime = container->lifetime();\n\tconst auto delegate = lifetime.make_state<PeerListContentDelegateShow>(\n\t\tshow);\n\tconst auto controller = lifetime.make_state<AdminsController>(\n\t\tpeer,\n\t\tadmin);\n\tcontroller->setStyleOverrides(&st::inviteLinkAdminsList);\n\tconst auto content = container->add(object_ptr<PeerListContent>(\n\t\tcontainer,\n\t\tcontroller));\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\n\treturn content;\n}\n\nvoid ManageInviteLinksBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> admin,\n\t\tint count,\n\t\tint revokedCount) {\n\tconst auto show = Main::MakeSessionShow(\n\t\tbox->uiShow(),\n\t\t&peer->session());\n\n\tbox->setTitle(tr::lng_group_invite_title());\n\tbox->setWidth(st::boxWideWidth);\n\n\tconst auto container = box->verticalLayout();\n\tconst auto permanentFromList = box->lifetime().make_state<\n\t\trpl::event_stream<InviteLinkData>\n\t>();\n\tconst auto countValue = box->lifetime().make_state<rpl::variable<int>>(\n\t\tcount);\n\n\tif (!admin->isSelf()) {\n\t\tauto status = tr::lng_group_invite_links_count(\n\t\t\tlt_count,\n\t\t\tcountValue->value() | tr::to_count());\n\t\tAddSinglePeerRow(\n\t\t\tcontainer,\n\t\t\tadmin,\n\t\t\tstd::move(status));\n\t}\n\n\tUi::AddSubsectionTitle(container, tr::lng_create_permanent_link_title());\n\tAddPermanentLinkBlock(\n\t\tshow,\n\t\tcontainer,\n\t\tpeer,\n\t\tadmin,\n\t\tpermanentFromList->events());\n\tUi::AddDivider(container);\n\n\tauto otherHeader = (Ui::SlideWrap<>*)nullptr;\n\tif (admin->isSelf()) {\n\t\tconst auto add = AddCreateLinkButton(container);\n\t\tadd->setClickedCallback([=] {\n\t\t\tshow->showBox(\n\t\t\t\tEditLinkBox(peer, InviteLinkData{ .admin = admin }));\n\t\t});\n\t} else {\n\t\totherHeader = container->add(object_ptr<Ui::SlideWrap<>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_group_invite_other_list(),\n\t\t\t\tst::defaultSubsectionTitle),\n\t\t\tst::inviteLinkRevokedTitlePadding));\n\t}\n\n\tauto [list, controller] = AddLinksList(\n\t\tshow,\n\t\tcontainer,\n\t\tpeer,\n\t\tadmin,\n\t\tcount,\n\t\tfalse);\n\t*countValue = controller->fullCountValue();\n\n\tcontroller->permanentFound(\n\t) | rpl::on_next([=](InviteLinkData &&data) {\n\t\tpermanentFromList->fire(std::move(data));\n\t}, container->lifetime());\n\n\tconst auto dividerAbout = container->add(object_ptr<Ui::SlideWrap<>>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_group_invite_add_about(),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::defaultBoxDividerLabelPadding)),\n\t\tstyle::margins(0, st::inviteLinkCreateSkip, 0, 0));\n\n\tconst auto adminsDivider = container->add(object_ptr<Ui::SlideWrap<>>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::BoxContentDivider>(container)));\n\tconst auto adminsHeader = container->add(object_ptr<Ui::SlideWrap<>>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_group_invite_other_title(),\n\t\t\tst::defaultSubsectionTitle),\n\t\tst::inviteLinkRevokedTitlePadding));\n\tconst auto admins = AddAdminsList(show, container, peer, admin);\n\n\tconst auto revokedDivider = container->add(object_ptr<Ui::SlideWrap<>>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::BoxContentDivider>(container)));\n\tconst auto revokedHeader = container->add(object_ptr<Ui::SlideWrap<>>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_group_invite_revoked_title(),\n\t\t\tst::defaultSubsectionTitle),\n\t\tst::inviteLinkRevokedTitlePadding));\n\tconst auto revoked = AddLinksList(\n\t\tshow,\n\t\tcontainer,\n\t\tpeer,\n\t\tadmin,\n\t\trevokedCount,\n\t\ttrue).widget;\n\n\tconst auto deleteAll = Ui::CreateChild<Ui::LinkButton>(\n\t\tcontainer.get(),\n\t\ttr::lng_group_invite_context_delete_all(tr::now),\n\t\tst::defaultLinkButton);\n\trpl::combine(\n\t\trevokedHeader->topValue(),\n\t\tcontainer->widthValue()\n\t) | rpl::on_next([=](int top, int outerWidth) {\n\t\tdeleteAll->moveToRight(\n\t\t\tst::inviteLinkRevokedTitlePadding.left(),\n\t\t\ttop + st::inviteLinkRevokedTitlePadding.top(),\n\t\t\touterWidth);\n\t}, deleteAll->lifetime());\n\tdeleteAll->setClickedCallback([=, show = box->uiShow()] {\n\t\tshow->showBox(DeleteAllRevokedBox(peer, admin));\n\t});\n\n\trpl::combine(\n\t\tlist->heightValue(),\n\t\tadmins->heightValue(),\n\t\trevoked->heightValue()\n\t) | rpl::on_next([=](int list, int admins, int revoked) {\n\t\tif (otherHeader) {\n\t\t\totherHeader->toggle(list > 0, anim::type::instant);\n\t\t}\n\t\tdividerAbout->toggle(!list && !otherHeader, anim::type::instant);\n\t\tadminsDivider->toggle(admins > 0 && list > 0, anim::type::instant);\n\t\tadminsHeader->toggle(admins > 0, anim::type::instant);\n\t\trevokedDivider->toggle(revoked > 0 && (list > 0 || admins > 0), anim::type::instant);\n\t\trevokedHeader->toggle(revoked > 0, anim::type::instant);\n\t\tdeleteAll->setVisible(revoked > 0);\n\t}, revokedHeader->lifetime());\n\n\tbox->addButton(tr::lng_about_done(), [=] { box->closeBox(); });\n}\n\nobject_ptr<Ui::SettingsButton> MakeCreateLinkButton(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<QString> text) {\n\tauto result = object_ptr<Ui::SettingsButton>(\n\t\tparent,\n\t\tstd::move(text),\n\t\tst::inviteLinkCreate);\n\tconst auto raw = result.data();\n\n\tconst auto icon = Ui::CreateChild<Ui::RpWidget>(raw);\n\ticon->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto size = st::inviteLinkCreateIconSize;\n\ticon->resize(size, size);\n\n\traw->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tconst auto &st = st::inviteLinkList.item;\n\t\ticon->move(\n\t\t\tst.photoPosition.x() + (st.photoSize - size) / 2,\n\t\t\t(height - size) / 2);\n\t}, icon->lifetime());\n\n\ticon->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(icon);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::windowBgActive);\n\t\tconst auto rect = icon->rect();\n\t\t{\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.drawEllipse(rect);\n\t\t}\n\t\tst::inviteLinkCreateIcon.paintInCenter(p, rect);\n\t}, icon->lifetime());\n\n\treturn result;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n\nclass PeerData;\n\nnamespace Ui {\nclass SettingsButton;\n} // namespace Ui\n\nvoid ManageInviteLinksBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> admin,\n\tint count,\n\tint revokedCount);\n\n[[nodiscard]] object_ptr<Ui::SettingsButton> MakeCreateLinkButton(\n\tnot_null<QWidget*> parent,\n\trpl::producer<QString> text);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_peer_permissions_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"history/admin_log/history_admin_log_filter.h\"\n#include \"core/ui_integration.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/business/data_business_chatbots.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_session.h\"\n#include \"ui/effects/toggle_arrow.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/box_content_divider.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"boxes/peers/edit_participants_box.h\"\n#include \"boxes/peers/edit_peer_info_box.h\"\n#include \"boxes/edit_privacy_box.h\"\n#include \"settings/settings_power_saving.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/mtproto_config.h\" // megagroupSizeMax\n#include \"apiwrap.h\"\n#include \"settings/settings_common.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_settings.h\"\n\nnamespace {\n\nconstexpr auto kSlowmodeValues = 8;\nconstexpr auto kBoostsUnrestrictValues = 5;\nconstexpr auto kForceDisableTooltipDuration = 3 * crl::time(1000);\nconstexpr auto kDefaultChargeStars = 10;\n\n[[nodiscard]] auto Dependencies(PowerSaving::Flags)\n-> std::vector<std::pair<PowerSaving::Flag, PowerSaving::Flag>> {\n\treturn {};\n}\n\n[[nodiscard]] auto Dependencies(AdminLog::FilterValue::Flags) {\n\tusing Flag = AdminLog::FilterValue::Flag;\n\treturn std::vector<std::pair<Flag, Flag>>{};\n}\n\n[[nodiscard]] auto Dependencies(Data::ChatbotsPermissions) {\n\tusing Flag = Data::ChatbotsPermission;\n\treturn std::vector<std::pair<Flag, Flag>>{};\n}\n\n[[nodiscard]] auto NestedRestrictionLabelsList(\n\tData::RestrictionsSetOptions options)\n-> std::vector<NestedEditFlagsLabels<ChatRestrictions>> {\n\tusing Flag = ChatRestriction;\n\n\tauto first = std::vector<RestrictionLabel>{\n\t\t{ Flag::SendOther, tr::lng_rights_chat_send_text(tr::now) },\n\t};\n\tauto media = std::vector<RestrictionLabel>{\n\t\t{ Flag::SendPhotos, tr::lng_rights_chat_photos(tr::now) },\n\t\t{ Flag::SendVideos, tr::lng_rights_chat_videos(tr::now) },\n\t\t{ Flag::SendVideoMessages, tr::lng_rights_chat_video_messages(tr::now) },\n\t\t{ Flag::SendMusic, tr::lng_rights_chat_music(tr::now) },\n\t\t{ Flag::SendVoiceMessages, tr::lng_rights_chat_voice_messages(tr::now) },\n\t\t{ Flag::SendFiles, tr::lng_rights_chat_files(tr::now) },\n\t\t{ Flag::SendStickers\n\t\t\t| Flag::SendGifs\n\t\t\t| Flag::SendGames\n\t\t\t| Flag::SendInline, tr::lng_rights_chat_stickers(tr::now) },\n\t\t{ Flag::EmbedLinks, tr::lng_rights_chat_send_links(tr::now) },\n\t\t{ Flag::SendPolls, tr::lng_rights_chat_send_polls(tr::now) },\n\t};\n\tauto second = std::vector<RestrictionLabel>{\n\t\t{ Flag::AddParticipants, tr::lng_rights_chat_add_members(tr::now) },\n\t\t{ Flag::CreateTopics, tr::lng_rights_group_add_topics(tr::now) },\n\t\t{ Flag::PinMessages, tr::lng_rights_group_pin(tr::now) },\n\t\t{ Flag::EditRank, (options.isUserSpecific\n\t\t\t? tr::lng_rights_group_edit_rank_single\n\t\t\t: tr::lng_rights_group_edit_rank)(tr::now) },\n\t\t{ Flag::ChangeInfo, tr::lng_rights_group_info(tr::now) },\n\t};\n\tif (!options.isForum) {\n\t\tsecond.erase(\n\t\t\tranges::remove(\n\t\t\t\tsecond,\n\t\t\t\tFlag::CreateTopics | Flag(),\n\t\t\t\t&RestrictionLabel::flags),\n\t\t\tend(second));\n\t}\n\treturn {\n\t\t{ std::nullopt, std::move(first) },\n\t\t{ tr::lng_rights_chat_send_media(), std::move(media) },\n\t\t{ std::nullopt, std::move(second) },\n\t};\n}\n\n[[nodiscard]] auto NestedAdminRightLabels(\n\tData::AdminRightsSetOptions options)\n-> std::vector<NestedEditFlagsLabels<ChatAdminRights>> {\n\tusing Flag = ChatAdminRight;\n\n\tif (options.isGroup) {\n\t\tauto first = std::vector<AdminRightLabel>{\n\t\t\t{ Flag::ChangeInfo, tr::lng_rights_group_info(tr::now) },\n\t\t\t{ Flag::DeleteMessages, tr::lng_rights_group_delete(tr::now) },\n\t\t\t{ Flag::BanUsers, tr::lng_rights_group_ban(tr::now) },\n\t\t\t{ Flag::InviteByLinkOrAdd, options.anyoneCanAddMembers\n\t\t\t\t? tr::lng_rights_group_invite_link(tr::now)\n\t\t\t\t: tr::lng_rights_group_invite(tr::now) },\n\t\t\t{ Flag::ManageTopics, tr::lng_rights_group_topics(tr::now) },\n\t\t\t{ Flag::PinMessages, tr::lng_rights_group_pin(tr::now) },\n\t\t};\n\t\tauto stories = std::vector<AdminRightLabel>{\n\t\t\t{ Flag::PostStories, tr::lng_rights_channel_post_stories(tr::now) },\n\t\t\t{ Flag::EditStories, tr::lng_rights_channel_edit_stories(tr::now) },\n\t\t\t{ Flag::DeleteStories, tr::lng_rights_channel_delete_stories(tr::now) },\n\t\t};\n\t\tauto second = std::vector<AdminRightLabel>{\n\t\t\t{ Flag::ManageCall, tr::lng_rights_group_manage_calls(tr::now) },\n\t\t\t{ Flag::ManageRanks, tr::lng_rights_group_manage_ranks(tr::now) },\n\t\t\t{ Flag::Anonymous, tr::lng_rights_group_anonymous(tr::now) },\n\t\t\t{ Flag::AddAdmins, tr::lng_rights_add_admins(tr::now) },\n\t\t};\n\t\tif (!options.isForum) {\n\t\t\tfirst.erase(\n\t\t\t\tranges::remove(\n\t\t\t\t\tfirst,\n\t\t\t\t\tFlag::ManageTopics | Flag(),\n\t\t\t\t\t&AdminRightLabel::flags),\n\t\t\t\tend(first));\n\t\t}\n\t\treturn {\n\t\t\t{ std::nullopt, std::move(first) },\n\t\t\t{ tr::lng_rights_channel_manage_stories(), std::move(stories) },\n\t\t\t{ std::nullopt, std::move(second) },\n\t\t};\n\t}\n\tauto first = std::vector<AdminRightLabel>{\n\t\t{ Flag::ChangeInfo, tr::lng_rights_channel_info(tr::now) },\n\t};\n\tauto messages = std::vector<AdminRightLabel>{\n\t\t{ Flag::PostMessages, tr::lng_rights_channel_post(tr::now) },\n\t\t{ Flag::EditMessages, tr::lng_rights_channel_edit(tr::now) },\n\t\t{ Flag::DeleteMessages, tr::lng_rights_channel_delete(tr::now) },\n\t};\n\tauto stories = std::vector<AdminRightLabel>{\n\t\t{ Flag::PostStories, tr::lng_rights_channel_post_stories(tr::now) },\n\t\t{ Flag::EditStories, tr::lng_rights_channel_edit_stories(tr::now) },\n\t\t{\n\t\t\tFlag::DeleteStories,\n\t\t\ttr::lng_rights_channel_delete_stories(tr::now),\n\t\t},\n\t};\n\tauto second = std::vector<AdminRightLabel>{\n\t\t{ Flag::InviteByLinkOrAdd, tr::lng_rights_group_invite(tr::now) },\n\t\t{ Flag::ManageCall, tr::lng_rights_channel_manage_calls(tr::now) },\n\t\t{ Flag::ManageDirect, tr::lng_rights_channel_manage_direct(tr::now) },\n\t\t{ Flag::AddAdmins, tr::lng_rights_add_admins(tr::now) },\n\t\t{ Flag::BanUsers, tr::lng_rights_group_ban(tr::now) },\n\t};\n\treturn {\n\t\t{ std::nullopt, std::move(first) },\n\t\t{ tr::lng_rights_channel_manage(), std::move(messages) },\n\t\t{ tr::lng_rights_channel_manage_stories(), std::move(stories) },\n\t\t{ std::nullopt, std::move(second) },\n\t};\n}\n\nint SlowmodeDelayByIndex(int index) {\n\tExpects(index >= 0 && index < kSlowmodeValues);\n\n\tswitch (index) {\n\tcase 0: return 0;\n\tcase 1: return 5;\n\tcase 2: return 10;\n\tcase 3: return 30;\n\tcase 4: return 60;\n\tcase 5: return 5 * 60;\n\tcase 6: return 15 * 60;\n\tcase 7: return 60 * 60;\n\t}\n\tUnexpected(\"Index in SlowmodeDelayByIndex.\");\n}\n\n[[nodiscard]] int BoostsUnrestrictByIndex(int index) {\n\treturn index + 1;\n}\n\ntemplate <typename CheckboxesMap, typename DependenciesMap>\nvoid ApplyDependencies(\n\t\tconst CheckboxesMap &checkboxes,\n\t\tconst DependenciesMap &dependencies,\n\t\tUi::AbstractCheckView *changed) {\n\tconst auto checkAndApply = [&](\n\t\t\tauto &&current,\n\t\t\tauto dependency,\n\t\t\tbool isChecked) {\n\t\tfor (auto &&checkbox : checkboxes) {\n\t\t\tif ((checkbox.first & dependency)\n\t\t\t\t&& (checkbox.second->checked() == isChecked)) {\n\t\t\t\tcurrent->setChecked(isChecked, anim::type::normal);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\tconst auto applySomeDependency = [&] {\n\t\tauto result = false;\n\t\tfor (auto &&entry : checkboxes) {\n\t\t\tif (entry.second == changed) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tauto isChecked = entry.second->checked();\n\t\t\tfor (auto &&dependency : dependencies) {\n\t\t\t\tconst auto check = isChecked\n\t\t\t\t\t? dependency.first\n\t\t\t\t\t: dependency.second;\n\t\t\t\tif (entry.first & check) {\n\t\t\t\t\tif (checkAndApply(\n\t\t\t\t\t\t\tentry.second,\n\t\t\t\t\t\t\t(isChecked\n\t\t\t\t\t\t\t\t? dependency.second\n\t\t\t\t\t\t\t\t: dependency.first),\n\t\t\t\t\t\t\t!isChecked)) {\n\t\t\t\t\t\tresult = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\n\tconst auto maxFixesCount = int(checkboxes.size());\n\tfor (auto i = 0; i != maxFixesCount; ++i) {\n\t\tif (!applySomeDependency()) {\n\t\t\tbreak;\n\t\t}\n\t};\n}\n\nauto Dependencies(ChatRestrictions)\n-> std::vector<std::pair<ChatRestriction, ChatRestriction>> {\n\tusing Flag = ChatRestriction;\n\n\treturn {\n\t\t// stickers <-> gifs\n\t\t{ Flag::SendGifs, Flag::SendStickers },\n\t\t{ Flag::SendStickers, Flag::SendGifs },\n\n\t\t// stickers <-> games\n\t\t{ Flag::SendGames, Flag::SendStickers },\n\t\t{ Flag::SendStickers, Flag::SendGames },\n\n\t\t// stickers <-> inline\n\t\t{ Flag::SendInline, Flag::SendStickers },\n\t\t{ Flag::SendStickers, Flag::SendInline },\n\n\t\t// embed_links -> send_plain\n\t\t{ Flag::EmbedLinks, Flag::SendOther },\n\n\t\t// send_* -> view_messages\n\t\t{ Flag::SendStickers, Flag::ViewMessages },\n\t\t{ Flag::SendGifs, Flag::ViewMessages },\n\t\t{ Flag::SendGames, Flag::ViewMessages },\n\t\t{ Flag::SendInline, Flag::ViewMessages },\n\t\t{ Flag::SendPolls, Flag::ViewMessages },\n\t\t{ Flag::SendPhotos, Flag::ViewMessages },\n\t\t{ Flag::SendVideos, Flag::ViewMessages },\n\t\t{ Flag::SendVideoMessages, Flag::ViewMessages },\n\t\t{ Flag::SendMusic, Flag::ViewMessages },\n\t\t{ Flag::SendVoiceMessages, Flag::ViewMessages },\n\t\t{ Flag::SendFiles, Flag::ViewMessages },\n\t\t{ Flag::SendOther, Flag::ViewMessages },\n\t};\n}\n\nChatRestrictions NegateRestrictions(ChatRestrictions value) {\n\tusing Flag = ChatRestriction;\n\n\treturn (~value) & (Flag(0)\n\t\t// view_messages is always allowed, so it is never in restrictions.\n\t\t//| Flag::ViewMessages\n\t\t| Flag::ChangeInfo\n\t\t| Flag::EmbedLinks\n\t\t| Flag::AddParticipants\n\t\t| Flag::CreateTopics\n\t\t| Flag::PinMessages\n\t\t| Flag::SendGames\n\t\t| Flag::SendGifs\n\t\t| Flag::SendInline\n\t\t| Flag::SendPolls\n\t\t| Flag::SendStickers\n\t\t| Flag::SendPhotos\n\t\t| Flag::SendVideos\n\t\t| Flag::SendVideoMessages\n\t\t| Flag::SendMusic\n\t\t| Flag::SendVoiceMessages\n\t\t| Flag::SendFiles\n\t\t| Flag::SendOther\n\t\t| Flag::EditRank);\n}\n\nauto Dependencies(ChatAdminRights)\n-> std::vector<std::pair<ChatAdminRight, ChatAdminRight>> {\n\treturn {};\n}\n\nauto ToPositiveNumberString() {\n\treturn rpl::map([](int count) {\n\t\treturn count ? QString::number(count) : QString();\n\t});\n}\n\nChatRestrictions DisabledByAdminRights(not_null<PeerData*> peer) {\n\tusing Flag = ChatRestriction;\n\tusing Admin = ChatAdminRight;\n\tusing Admins = ChatAdminRights;\n\n\tconst auto adminRights = [&] {\n\t\tconst auto full = ~Admins(0);\n\t\tif (const auto chat = peer->asChat()) {\n\t\t\treturn chat->amCreator() ? full : chat->adminRights();\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\treturn channel->amCreator() ? full : channel->adminRights();\n\t\t}\n\t\tUnexpected(\"User in DisabledByAdminRights.\");\n\t}();\n\treturn Flag(0)\n\t\t| ((adminRights & Admin::ManageTopics)\n\t\t\t? Flag(0)\n\t\t\t: Flag::CreateTopics)\n\t\t| ((adminRights & Admin::PinMessages)\n\t\t\t? Flag(0)\n\t\t\t: Flag::PinMessages)\n\t\t| ((adminRights & Admin::InviteByLinkOrAdd)\n\t\t\t? Flag(0)\n\t\t\t: Flag::AddParticipants)\n\t\t| ((adminRights & Admin::ChangeInfo)\n\t\t\t? Flag(0)\n\t\t\t: Flag::ChangeInfo);\n}\n\nnot_null<Ui::RpWidget*> AddInnerToggle(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst style::SettingsButton &st,\n\t\tstd::vector<not_null<Ui::AbstractCheckView*>> innerCheckViews,\n\t\tnot_null<Ui::SlideWrap<>*> wrap,\n\t\trpl::producer<QString> buttonLabel,\n\t\tstd::optional<QString> locked,\n\t\tSettings::IconDescriptor &&icon) {\n\tconst auto button = container->add(object_ptr<Ui::SettingsButton>(\n\t\tcontainer,\n\t\tnullptr,\n\t\tst));\n\tif (icon) {\n\t\tSettings::AddButtonIcon(button, st, std::move(icon));\n\t}\n\n\tconst auto toggleButton = Ui::CreateChild<Ui::SettingsButton>(\n\t\tcontainer.get(),\n\t\tnullptr,\n\t\tst);\n\n\tstruct State final {\n\t\tState(const style::Toggle &st, Fn<void()> c)\n\t\t: checkView(st, false, c) {\n\t\t}\n\t\tUi::ToggleView checkView;\n\t\tUi::Animations::Simple animation;\n\t\trpl::event_stream<> anyChanges;\n\t\tstd::vector<not_null<Ui::AbstractCheckView*>> innerChecks;\n\t};\n\tconst auto state = button->lifetime().make_state<State>(\n\t\tst.toggle,\n\t\t[=] { toggleButton->update(); });\n\tstate->innerChecks = std::move(innerCheckViews);\n\tconst auto countChecked = [=] {\n\t\treturn ranges::count_if(\n\t\t\tstate->innerChecks,\n\t\t\t[](const auto &v) { return v->checked(); });\n\t};\n\tfor (const auto &innerCheck : state->innerChecks) {\n\t\tinnerCheck->checkedChanges(\n\t\t) | rpl::to_empty | rpl::start_to_stream(\n\t\t\tstate->anyChanges,\n\t\t\tbutton->lifetime());\n\t}\n\tconst auto checkView = &state->checkView;\n\t{\n\t\tconst auto separator = Ui::CreateChild<Ui::RpWidget>(container.get());\n\t\tseparator->paintRequest(\n\t\t) | rpl::on_next([=, bg = st.textBgOver] {\n\t\t\tauto p = QPainter(separator);\n\t\t\tp.fillRect(separator->rect(), bg);\n\t\t}, separator->lifetime());\n\t\tconst auto separatorHeight = 2 * st.toggle.border\n\t\t\t+ st.toggle.diameter;\n\t\tbutton->geometryValue(\n\t\t) | rpl::on_next([=](const QRect &r) {\n\t\t\tconst auto w = st::rightsButtonToggleWidth;\n\t\t\ttoggleButton->setGeometry(\n\t\t\t\tr.x() + r.width() - w,\n\t\t\t\tr.y(),\n\t\t\t\tw,\n\t\t\t\tr.height());\n\t\t\tseparator->setGeometry(\n\t\t\t\ttoggleButton->x() - st::lineWidth,\n\t\t\t\tr.y() + (r.height() - separatorHeight) / 2,\n\t\t\t\tst::lineWidth,\n\t\t\t\tseparatorHeight);\n\t\t}, toggleButton->lifetime());\n\n\t\tconst auto checkWidget = Ui::CreateChild<Ui::RpWidget>(toggleButton);\n\t\tcheckWidget->resize(checkView->getSize());\n\t\tcheckWidget->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = QPainter(checkWidget);\n\t\t\tcheckView->paint(p, 0, 0, checkWidget->width());\n\t\t}, checkWidget->lifetime());\n\t\ttoggleButton->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\tcheckWidget->moveToRight(\n\t\t\t\tst.toggleSkip,\n\t\t\t\t(s.height() - checkWidget->height()) / 2);\n\t\t}, toggleButton->lifetime());\n\t}\n\tstate->anyChanges.events_starting_with(\n\t\trpl::empty_value()\n\t) | rpl::map(countChecked) | rpl::on_next([=](int count) {\n\t\tcheckView->setChecked(count > 0, anim::type::normal);\n\t}, toggleButton->lifetime());\n\tcheckView->setLocked(locked.has_value());\n\tcheckView->finishAnimating();\n\n\tconst auto totalInnerChecks = state->innerChecks.size();\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton,\n\t\trpl::combine(\n\t\t\tstd::move(buttonLabel),\n\t\t\tstate->anyChanges.events_starting_with(\n\t\t\t\trpl::empty_value()\n\t\t\t) | rpl::map(countChecked)\n\t\t) | rpl::map([=](const QString &t, int checked) {\n\t\t\tauto count = tr::bold(\"  \"\n\t\t\t\t+ QString::number(checked)\n\t\t\t\t+ '/'\n\t\t\t\t+ QString::number(totalInnerChecks));\n\t\t\treturn TextWithEntities::Simple(t).append(std::move(count));\n\t\t}));\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tconst auto arrow = Ui::CreateChild<Ui::RpWidget>(button);\n\t{\n\t\tconst auto &icon = st::permissionsExpandIcon;\n\t\tarrow->resize(icon.size());\n\t\tarrow->paintRequest(\n\t\t) | rpl::on_next([=, &icon] {\n\t\t\tauto p = QPainter(arrow);\n\t\t\tconst auto center = QPointF(\n\t\t\t\ticon.width() / 2.,\n\t\t\t\ticon.height() / 2.);\n\t\t\tconst auto progress = state->animation.value(\n\t\t\t\twrap->toggled() ? 1. : 0.);\n\t\t\tauto hq = std::optional<PainterHighQualityEnabler>();\n\t\t\tif (progress > 0.) {\n\t\t\t\thq.emplace(p);\n\t\t\t\tp.translate(center);\n\t\t\t\tp.rotate(progress * 180.);\n\t\t\t\tp.translate(-center);\n\t\t\t}\n\t\t\ticon.paint(p, 0, 0, arrow->width());\n\t\t}, arrow->lifetime());\n\t}\n\tbutton->sizeValue(\n\t) | rpl::on_next([=, &st](const QSize &s) {\n\t\tconst auto labelLeft = st.padding.left();\n\t\tconst auto labelRight = s.width() - toggleButton->width();\n\n\t\tlabel->resizeToWidth(labelRight - labelLeft - arrow->width());\n\t\tlabel->moveToLeft(\n\t\t\tlabelLeft,\n\t\t\t(s.height() - label->height()) / 2);\n\t\tarrow->moveToLeft(\n\t\t\tstd::min(\n\t\t\t\tlabelLeft + label->textMaxWidth(),\n\t\t\t\tlabelRight - arrow->width()),\n\t\t\t(s.height() - arrow->height()) / 2);\n\t}, button->lifetime());\n\twrap->toggledValue(\n\t) | rpl::skip(1) | rpl::on_next([=](bool toggled) {\n\t\tstate->animation.start(\n\t\t\t[=] { arrow->update(); },\n\t\t\ttoggled ? 0. : 1.,\n\t\t\ttoggled ? 1. : 0.,\n\t\t\tst::slideWrapDuration);\n\t}, button->lifetime());\n\n\tconst auto handleLocked = [=] {\n\t\tif (locked.has_value()) {\n\t\t\tUi::Toast::Show(container, *locked);\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\n\tbutton->clicks(\n\t) | rpl::on_next([=] {\n\t\tif (!handleLocked()) {\n\t\t\twrap->toggle(!wrap->toggled(), anim::type::normal);\n\t\t}\n\t}, button->lifetime());\n\n\ttoggleButton->clicks(\n\t) | rpl::on_next([=] {\n\t\tif (!handleLocked()) {\n\t\t\tconst auto checked = !checkView->checked();\n\t\t\tfor (const auto &innerCheck : state->innerChecks) {\n\t\t\t\tinnerCheck->setChecked(checked, anim::type::normal);\n\t\t\t}\n\t\t}\n\t}, toggleButton->lifetime());\n\n\treturn button;\n}\n\ntemplate <typename Flags>\n[[nodiscard]] EditFlagsControl<Flags> CreateEditFlags(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tFlags checked,\n\t\tEditFlagsDescriptor<Flags> &&descriptor) {\n\tstruct State final {\n\t\tstd::map<Flags, not_null<Ui::AbstractCheckView*>> checkViews;\n\t\trpl::event_stream<> anyChanges;\n\t\trpl::variable<QString> forceDisabledMessage;\n\t\trpl::variable<bool> forceDisabled;\n\t\tbase::flat_map<Flags, bool> realCheckedValues;\n\t\tbase::weak_ptr<Ui::Toast::Instance> toast;\n\t};\n\tconst auto state = container->lifetime().make_state<State>();\n\tif (descriptor.forceDisabledMessage) {\n\t\tstate->forceDisabledMessage = std::move(\n\t\t\tdescriptor.forceDisabledMessage);\n\t\tstate->forceDisabled = state->forceDisabledMessage.value(\n\t\t) | rpl::map([=](const QString &message) {\n\t\t\treturn !message.isEmpty();\n\t\t});\n\n\t\tstate->forceDisabled.value(\n\t\t) | rpl::on_next([=](bool disabled) {\n\t\t\tif (disabled) {\n\t\t\t\tfor (const auto &[flags, checkView] : state->checkViews) {\n\t\t\t\t\tcheckView->setChecked(false, anim::type::normal);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor (const auto &[flags, checkView] : state->checkViews) {\n\t\t\t\t\tif (const auto i = state->realCheckedValues.find(flags)\n\t\t\t\t\t\t; i != state->realCheckedValues.end()) {\n\t\t\t\t\t\tcheckView->setChecked(\n\t\t\t\t\t\t\ti->second,\n\t\t\t\t\t\t\tanim::type::normal);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}, container->lifetime());\n\t}\n\n\tconst auto &st = descriptor.st ? *descriptor.st : st::rightsButton;\n\tconst auto value = [=] {\n\t\tauto result = Flags(0);\n\t\tfor (const auto &[flags, checkView] : state->checkViews) {\n\t\t\tif (checkView->checked()) {\n\t\t\t\tresult |= flags;\n\t\t\t} else {\n\t\t\t\tresult &= ~flags;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\tconst auto applyDependencies = [=](Ui::AbstractCheckView *view) {\n\t\tstatic const auto dependencies = Dependencies(Flags());\n\t\tApplyDependencies(state->checkViews, dependencies, view);\n\t};\n\n\tconst auto addCheckbox = [&](\n\t\t\tnot_null<Ui::VerticalLayout*> verticalLayout,\n\t\t\tbool isInner,\n\t\t\tconst EditFlagsLabel<Flags> &entry) {\n\t\tconst auto flags = entry.flags;\n\t\tconst auto lockedIt = ranges::find_if(\n\t\t\tdescriptor.disabledMessages,\n\t\t\t[&](const auto &pair) { return (pair.first & flags) != 0; });\n\t\tconst auto locked = (lockedIt != end(descriptor.disabledMessages))\n\t\t\t? std::make_optional(lockedIt->second)\n\t\t\t: std::nullopt;\n\t\tconst auto realChecked = (checked & flags) != 0;\n\t\tstate->realCheckedValues.emplace(flags, realChecked);\n\t\tconst auto toggled = realChecked && !state->forceDisabled.current();\n\n\t\tconst auto checkView = [&]() -> not_null<Ui::AbstractCheckView*> {\n\t\t\tif (isInner) {\n\t\t\t\tconst auto checkbox = verticalLayout->add(\n\t\t\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\t\t\tverticalLayout,\n\t\t\t\t\t\tentry.label,\n\t\t\t\t\t\ttoggled,\n\t\t\t\t\t\tst::settingsCheckbox),\n\t\t\t\t\tst.padding);\n\t\t\t\tconst auto button = Ui::CreateChild<Ui::RippleButton>(\n\t\t\t\t\tverticalLayout.get(),\n\t\t\t\t\tst::defaultRippleAnimation);\n\t\t\t\tbutton->stackUnder(checkbox);\n\t\t\t\trpl::combine(\n\t\t\t\t\tverticalLayout->widthValue(),\n\t\t\t\t\tcheckbox->geometryValue()\n\t\t\t\t) | rpl::on_next([=](int w, const QRect &r) {\n\t\t\t\t\tbutton->setGeometry(0, r.y(), w, r.height());\n\t\t\t\t}, button->lifetime());\n\t\t\t\tcheckbox->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\t\tconst auto checkView = checkbox->checkView();\n\t\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\t\tcheckView->setChecked(\n\t\t\t\t\t\t!checkView->checked(),\n\t\t\t\t\t\tanim::type::normal);\n\t\t\t\t});\n\n\t\t\t\treturn checkView;\n\t\t\t} else {\n\t\t\t\tconst auto button = Settings::AddButtonWithIcon(\n\t\t\t\t\tverticalLayout,\n\t\t\t\t\trpl::single(entry.label),\n\t\t\t\t\tst,\n\t\t\t\t\t{ entry.icon });\n\t\t\t\tconst auto toggle = Ui::CreateChild<Ui::RpWidget>(\n\t\t\t\t\tbutton.get());\n\n\t\t\t\t// Looks like a bug in Clang, fails to compile with 'auto&' below.\n\t\t\t\trpl::lifetime &lifetime = toggle->lifetime();\n\n\t\t\t\tconst auto checkView = lifetime.make_state<Ui::ToggleView>(\n\t\t\t\t\tst.toggle,\n\t\t\t\t\ttoggled,\n\t\t\t\t\t[=] { toggle->update(); });\n\t\t\t\ttoggle->resize(checkView->getSize());\n\t\t\t\ttoggle->paintRequest(\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\tauto p = QPainter(toggle);\n\t\t\t\t\tcheckView->paint(p, 0, 0, toggle->width());\n\t\t\t\t}, toggle->lifetime());\n\t\t\t\tbutton->sizeValue(\n\t\t\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\t\t\ttoggle->moveToRight(\n\t\t\t\t\t\tst.toggleSkip,\n\t\t\t\t\t\t(s.height() - toggle->height()) / 2);\n\t\t\t\t}, toggle->lifetime());\n\t\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\t\tcheckView->setChecked(\n\t\t\t\t\t\t!checkView->checked(),\n\t\t\t\t\t\tanim::type::normal);\n\t\t\t\t});\n\t\t\t\tcheckView->setLocked(locked.has_value());\n\t\t\t\treturn checkView;\n\t\t\t}\n\t\t}();\n\t\tstate->checkViews.emplace(flags, checkView);\n\t\tcheckView->checkedChanges(\n\t\t) | rpl::on_next([=](bool checked) {\n\t\t\tif (checked && state->forceDisabled.current()) {\n\t\t\t\tif (!state->toast) {\n\t\t\t\t\tstate->toast = Ui::Toast::Show(container, {\n\t\t\t\t\t\t.text = { state->forceDisabledMessage.current() },\n\t\t\t\t\t\t.duration = kForceDisableTooltipDuration,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tcheckView->setChecked(false, anim::type::instant);\n\t\t\t} else if (locked.has_value()) {\n\t\t\t\tif (checked != toggled) {\n\t\t\t\t\tif (!state->toast && !locked->isEmpty()) {\n\t\t\t\t\t\tstate->toast = Ui::Toast::Show(container, {\n\t\t\t\t\t\t\t.text = { *locked },\n\t\t\t\t\t\t\t.duration = kForceDisableTooltipDuration,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tcheckView->setChecked(toggled, anim::type::instant);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (!state->forceDisabled.current()) {\n\t\t\t\t\tstate->realCheckedValues[flags] = checked;\n\t\t\t\t}\n\t\t\t\tInvokeQueued(container, [=] {\n\t\t\t\t\tapplyDependencies(checkView);\n\t\t\t\t\tstate->anyChanges.fire({});\n\t\t\t\t});\n\t\t\t}\n\t\t}, verticalLayout->lifetime());\n\n\t\treturn checkView;\n\t};\n\tauto highlightWidget = QPointer<Ui::RpWidget>();\n\tconst auto highlightFlags = descriptor.highlightFlags;\n\tfor (const auto &nestedWithLabel : descriptor.labels) {\n\t\tAssert(!nestedWithLabel.nested.empty());\n\n\t\tconst auto isInner = nestedWithLabel.nestingLabel.has_value();\n\t\tauto wrap = isInner\n\t\t\t? object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(container))\n\t\t\t: object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>{ nullptr };\n\t\tconst auto verticalLayout = wrap ? wrap->entity() : container.get();\n\t\tauto innerChecks = std::vector<not_null<Ui::AbstractCheckView*>>();\n\t\tauto sectionFlags = Flags();\n\t\tfor (const auto &entry : nestedWithLabel.nested) {\n\t\t\tconst auto c = addCheckbox(verticalLayout, isInner, entry);\n\t\t\tif (isInner) {\n\t\t\t\tinnerChecks.push_back(c);\n\t\t\t\tsectionFlags |= entry.flags;\n\t\t\t}\n\t\t}\n\t\tif (wrap) {\n\t\t\tconst auto raw = wrap.data();\n\t\t\traw->hide(anim::type::instant);\n\t\t\tconst auto toggle = AddInnerToggle(\n\t\t\t\tcontainer,\n\t\t\t\tst,\n\t\t\t\tinnerChecks,\n\t\t\t\traw,\n\t\t\t\t*nestedWithLabel.nestingLabel,\n\t\t\t\tstd::nullopt,\n\t\t\t\t{ nestedWithLabel.nested.front().icon });\n\t\t\tif (highlightFlags && (sectionFlags & highlightFlags)) {\n\t\t\t\thighlightWidget = toggle;\n\t\t\t}\n\t\t\tcontainer->add(std::move(wrap));\n\t\t\tcontainer->widthValue(\n\t\t\t) | rpl::on_next([=](int w) {\n\t\t\t\traw->resizeToWidth(w);\n\t\t\t}, raw->lifetime());\n\t\t}\n\t}\n\n\tapplyDependencies(nullptr);\n\tfor (const auto &[flags, checkView] : state->checkViews) {\n\t\tcheckView->finishAnimating();\n\t}\n\n\treturn {\n\t\t.widget = nullptr,\n\t\t.value = value,\n\t\t.changes = state->anyChanges.events() | rpl::map(value),\n\t\t.highlightWidget = highlightWidget,\n\t};\n}\n\nvoid AddSlowmodeLabels(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tconst auto labels = container->add(\n\t\tobject_ptr<Ui::FixedHeightWidget>(container, st::normalFont->height),\n\t\tst::slowmodeLabelsMargin);\n\tfor (auto i = 0; i != kSlowmodeValues; ++i) {\n\t\tconst auto seconds = SlowmodeDelayByIndex(i);\n\t\tconst auto label = Ui::CreateChild<Ui::LabelSimple>(\n\t\t\tlabels,\n\t\t\tst::slowmodeLabel,\n\t\t\t(!seconds\n\t\t\t\t? tr::lng_rights_slowmode_off(tr::now)\n\t\t\t\t: (seconds < 60)\n\t\t\t\t? tr::lng_seconds_tiny(tr::now, lt_count, seconds)\n\t\t\t\t: (seconds < 3600)\n\t\t\t\t? tr::lng_minutes_tiny(tr::now, lt_count, seconds / 60)\n\t\t\t\t: tr::lng_hours_tiny(tr::now, lt_count,seconds / 3600)));\n\t\trpl::combine(\n\t\t\tlabels->widthValue(),\n\t\t\tlabel->widthValue()\n\t\t) | rpl::on_next([=](int outer, int inner) {\n\t\t\tconst auto skip = st::localStorageLimitMargin;\n\t\t\tconst auto size = st::localStorageLimitSlider.seekSize;\n\t\t\tconst auto available = outer\n\t\t\t\t- skip.left()\n\t\t\t\t- skip.right()\n\t\t\t\t- size.width();\n\t\t\tconst auto shift = (i == 0)\n\t\t\t\t? -(size.width() / 2)\n\t\t\t\t: (i + 1 == kSlowmodeValues)\n\t\t\t\t? (size.width() - (size.width() / 2) - inner)\n\t\t\t\t: (-inner / 2);\n\t\t\tconst auto left = skip.left()\n\t\t\t\t+ (size.width() / 2)\n\t\t\t\t+ (i * available) / (kSlowmodeValues - 1)\n\t\t\t\t+ shift;\n\t\t\tlabel->moveToLeft(left, 0, outer);\n\t\t}, label->lifetime());\n\t}\n}\n\nrpl::producer<int> AddSlowmodeSlider(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<PeerData*> peer) {\n\tusing namespace rpl::mappers;\n\n\tif (const auto chat = peer->asChat()) {\n\t\tif (!chat->amCreator()) {\n\t\t\treturn rpl::single(0);\n\t\t}\n\t}\n\tconst auto channel = peer->asChannel();\n\tauto &lifetime = container->lifetime();\n\tconst auto secondsCount = lifetime.make_state<rpl::variable<int>>(\n\t\tchannel ? channel->slowmodeSeconds() : 0);\n\n\tcontainer->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_rights_slowmode_header(),\n\t\t\tst::rightsHeaderLabel),\n\t\tst::rightsHeaderMargin);\n\n\tAddSlowmodeLabels(container);\n\n\tconst auto slider = container->add(\n\t\tobject_ptr<Ui::MediaSlider>(container, st::localStorageLimitSlider),\n\t\tst::localStorageLimitMargin);\n\tslider->resize(st::localStorageLimitSlider.seekSize);\n\tslider->setPseudoDiscrete(\n\t\tkSlowmodeValues,\n\t\tSlowmodeDelayByIndex,\n\t\tsecondsCount->current(),\n\t\t[=](int seconds) {\n\t\t\t(*secondsCount) = seconds;\n\t\t});\n\n\tauto hasSlowMode = secondsCount->value(\n\t) | rpl::map(\n\t\t_1 != 0\n\t) | rpl::distinct_until_changed();\n\n\tauto useSeconds = secondsCount->value(\n\t) | rpl::map(\n\t\t_1 < 60\n\t) | rpl::distinct_until_changed();\n\n\tauto interval = rpl::combine(\n\t\tstd::move(useSeconds),\n\t\ttr::lng_rights_slowmode_interval_seconds(\n\t\t\tlt_count,\n\t\t\tsecondsCount->value() | tr::to_count()),\n\t\ttr::lng_rights_slowmode_interval_minutes(\n\t\t\tlt_count,\n\t\t\tsecondsCount->value() | rpl::map(_1 / 60.))\n\t) | rpl::map([](\n\t\t\tbool use,\n\t\t\tconst QString &seconds,\n\t\t\tconst QString &minutes) {\n\t\treturn use ? seconds : minutes;\n\t});\n\n\tauto aboutText = rpl::combine(\n\t\tstd::move(hasSlowMode),\n\t\ttr::lng_rights_slowmode_about(),\n\t\ttr::lng_rights_slowmode_about_interval(\n\t\t\tlt_interval,\n\t\t\tstd::move(interval))\n\t) | rpl::map([](\n\t\t\tbool has,\n\t\t\tconst QString &about,\n\t\t\tconst QString &aboutInterval) {\n\t\treturn has ? aboutInterval : about;\n\t});\n\n\tcontainer->add(\n\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\tstd::move(aboutText),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::proxyAboutPadding),\n\t\tstyle::margins(0, st::infoProfileSkip, 0, st::infoProfileSkip));\n\n\treturn secondsCount->value();\n}\n\nvoid AddBoostsUnrestrictLabels(not_null<Ui::VerticalLayout*> container) {\n\tconst auto labels = container->add(\n\t\tobject_ptr<Ui::FixedHeightWidget>(container, st::normalFont->height),\n\t\tst::slowmodeLabelsMargin);\n\tconst auto one = Ui::Text::IconEmoji(&st::boostMessageIcon);\n\tconst auto many = Ui::Text::IconEmoji(&st::boostsMessageIcon);\n\tfor (auto i = 0; i != kBoostsUnrestrictValues; ++i) {\n\t\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tlabels,\n\t\t\tst::boostsUnrestrictLabel);\n\t\tlabel->setMarkedText(\n\t\t\tTextWithEntities(i ? many : one).append(QString::number(i + 1)));\n\t\trpl::combine(\n\t\t\tlabels->widthValue(),\n\t\t\tlabel->widthValue()\n\t\t) | rpl::on_next([=](int outer, int inner) {\n\t\t\tconst auto skip = st::localStorageLimitMargin;\n\t\t\tconst auto size = st::localStorageLimitSlider.seekSize;\n\t\t\tconst auto available = outer\n\t\t\t\t- skip.left()\n\t\t\t\t- skip.right()\n\t\t\t\t- size.width();\n\t\t\tconst auto shift = (i == 0)\n\t\t\t\t? -(size.width() / 2)\n\t\t\t\t: (i + 1 == kBoostsUnrestrictValues)\n\t\t\t\t? (size.width() - (size.width() / 2) - inner)\n\t\t\t\t: (-inner / 2);\n\t\t\tconst auto left = skip.left()\n\t\t\t\t+ (size.width() / 2)\n\t\t\t\t+ (i * available) / (kBoostsUnrestrictValues - 1)\n\t\t\t\t+ shift;\n\t\t\tlabel->moveToLeft(left, 0, outer);\n\t\t}, label->lifetime());\n\t}\n}\n\nrpl::producer<int> AddBoostsUnrestrictSlider(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<PeerData*> peer) {\n\tusing namespace rpl::mappers;\n\n\tif (const auto chat = peer->asChat()) {\n\t\tif (!chat->amCreator()) {\n\t\t\treturn rpl::single(0);\n\t\t}\n\t}\n\tconst auto channel = peer->asChannel();\n\tauto &lifetime = container->lifetime();\n\tconst auto boostsUnrestrict = lifetime.make_state<rpl::variable<int>>(\n\t\tchannel ? channel->boostsUnrestrict() : 0);\n\n\tUi::AddSkip(container);\n\n\tauto enabled = boostsUnrestrict->value(\n\t) | rpl::map(_1 > 0);\n\tcontainer->add(object_ptr<Ui::SettingsButton>(\n\t\tcontainer,\n\t\ttr::lng_rights_boosts_no_restrict(),\n\t\tst::defaultSettingsButton\n\t))->toggleOn(rpl::duplicate(enabled))->toggledValue(\n\t) | rpl::on_next([=](bool toggled) {\n\t\tif (toggled && !boostsUnrestrict->current()) {\n\t\t\t*boostsUnrestrict = 1;\n\t\t} else if (!toggled && boostsUnrestrict->current()) {\n\t\t\t*boostsUnrestrict = 0;\n\t\t}\n\t}, container->lifetime());\n\n\tconst auto outer = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\touter->toggleOn(rpl::duplicate(enabled), anim::type::normal);\n\touter->finishAnimating();\n\n\tconst auto inner = outer->entity();\n\n\tAddBoostsUnrestrictLabels(inner);\n\n\tconst auto slider = inner->add(\n\t\tobject_ptr<Ui::MediaSlider>(inner, st::localStorageLimitSlider),\n\t\tst::localStorageLimitMargin);\n\tslider->resize(st::localStorageLimitSlider.seekSize);\n\tslider->setPseudoDiscrete(\n\t\tkBoostsUnrestrictValues,\n\t\tBoostsUnrestrictByIndex,\n\t\tboostsUnrestrict->current(),\n\t\t[=](int boosts) {\n\t\t\t(*boostsUnrestrict) = boosts;\n\t\t});\n\n\tinner->add(\n\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\tinner,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tinner,\n\t\t\t\trpl::conditional(\n\t\t\t\t\tboostsUnrestrict->value() | rpl::map(_1 > 0),\n\t\t\t\t\ttr::lng_rights_boosts_about_on(),\n\t\t\t\t\ttr::lng_rights_boosts_about()),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::proxyAboutPadding),\n\t\tstyle::margins(0, st::infoProfileSkip, 0, 0));\n\n\treturn boostsUnrestrict->value();\n}\n\nrpl::producer<int> AddBoostsUnrestrictWrapped(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<bool> shown) {\n\tconst auto wrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\twrap->toggleOn(std::move(shown), anim::type::normal);\n\twrap->finishAnimating();\n\n\tconst auto inner = wrap->entity();\n\n\tauto result = AddBoostsUnrestrictSlider(inner, peer);\n\n\tconst auto skip = st::defaultVerticalListSkip;\n\tconst auto divider = inner->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>(\n\t\t\tinner,\n\t\t\tobject_ptr<Ui::BoxContentDivider>(inner),\n\t\t\tQMargins{ 0, skip, 0, skip }));\n\tdivider->toggleOn(rpl::duplicate(result) | rpl::map(!rpl::mappers::_1));\n\tdivider->finishAnimating();\n\n\treturn result;\n}\n\nvoid AddSuggestGigagroup(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tFn<void()> callback) {\n\tcontainer->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_rights_gigagroup_title(),\n\t\t\tst::rightsHeaderLabel),\n\t\tst::rightsHeaderMargin);\n\tcontainer->add(EditPeerInfoBox::CreateButton(\n\t\tcontainer,\n\t\ttr::lng_rights_gigagroup_convert(),\n\t\trpl::single(QString()),\n\t\tstd::move(callback),\n\t\tst::manageGroupTopicsButton,\n\t\t{ &st::menuIconChatDiscuss }));\n\n\tcontainer->add(\n\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_rights_gigagroup_about(),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::proxyAboutPadding),\n\t\tstyle::margins(0, st::infoProfileSkip, 0, st::infoProfileSkip));\n}\n\nvoid AddBannedButtons(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer) {\n\tif (const auto chat = peer->asChat()) {\n\t\tif (!chat->amCreator()) {\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto channel = peer->asChannel();\n\tcontainer->add(EditPeerInfoBox::CreateButton(\n\t\tcontainer,\n\t\ttr::lng_manage_peer_exceptions(),\n\t\t(channel\n\t\t\t? Info::Profile::RestrictedCountValue(channel)\n\t\t\t: rpl::single(0)) | ToPositiveNumberString(),\n\t\t[=] {\n\t\t\tParticipantsBoxController::Start(\n\t\t\t\tnavigation,\n\t\t\t\tpeer,\n\t\t\t\tParticipantsBoxController::Role::Restricted);\n\t\t},\n\t\tst::manageGroupTopicsButton,\n\t\t{ &st::menuIconPermissions }));\n\tif (channel) {\n\t\tcontainer->add(EditPeerInfoBox::CreateButton(\n\t\t\tcontainer,\n\t\t\ttr::lng_manage_peer_removed_users(),\n\t\t\tInfo::Profile::KickedCountValue(channel)\n\t\t\t| ToPositiveNumberString(),\n\t\t\t[=] {\n\t\t\t\tParticipantsBoxController::Start(\n\t\t\t\t\tnavigation,\n\t\t\t\t\tpeer,\n\t\t\t\t\tParticipantsBoxController::Role::Kicked);\n\t\t\t},\n\t\t\tst::manageGroupTopicsButton,\n\t\t\t{ &st::menuIconRemove }));\n\t}\n}\n\n} // namespace\n\nvoid ShowEditPeerPermissionsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> channelOrGroup,\n\t\tFn<void(EditPeerPermissionsBoxResult)> done) {\n\tconst auto peer = channelOrGroup->migrateToOrMe();\n\n\tbox->setTitle(tr::lng_manage_peer_permissions());\n\n\tconst auto inner = box->verticalLayout();\n\n\tusing Flag = ChatRestriction;\n\tusing Flags = ChatRestrictions;\n\n\tconst auto disabledByAdminRights = DisabledByAdminRights(peer);\n\tconst auto restrictions = FixDependentRestrictions([&] {\n\t\tif (const auto chat = peer->asChat()) {\n\t\t\treturn chat->defaultRestrictions()\n\t\t\t\t| disabledByAdminRights;\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\treturn channel->defaultRestrictions()\n\t\t\t\t| (channel->isPublic()\n\t\t\t\t\t? (Flag::ChangeInfo | Flag::PinMessages)\n\t\t\t\t\t: Flags(0))\n\t\t\t\t| disabledByAdminRights;\n\t\t}\n\t\tUnexpected(\"User in EditPeerPermissionsBox.\");\n\t}());\n\tconst auto disabledMessages = [&] {\n\t\tauto result = base::flat_map<Flags, QString>();\n\t\tresult.emplace(\n\t\t\tdisabledByAdminRights,\n\t\t\ttr::lng_rights_permission_cant_edit(tr::now));\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\tif (channel->isPublic()) {\n\t\t\t\tresult.emplace(\n\t\t\t\t\tFlag::ChangeInfo | Flag::PinMessages,\n\t\t\t\t\ttr::lng_rights_permission_unavailable(tr::now));\n\t\t\t} else if (channel->isMegagroup() && channel->discussionLink()) {\n\t\t\t\tresult.emplace(\n\t\t\t\t\tFlag::ChangeInfo | Flag::PinMessages,\n\t\t\t\t\ttr::lng_rights_permission_in_discuss(tr::now));\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}();\n\n\tUi::AddSubsectionTitle(\n\t\tinner,\n\t\ttr::lng_rights_default_restrictions_header());\n\tauto [checkboxes, getRestrictions, changes, highlightWidget] = CreateEditRestrictions(\n\t\tinner,\n\t\trestrictions,\n\t\tdisabledMessages,\n\t\t{ .isForum = peer->isForum() });\n\n\tinner->add(std::move(checkboxes));\n\n\tstruct State {\n\t\trpl::variable<int> slowmodeSeconds;\n\t\trpl::variable<int> boostsUnrestrict;\n\t\trpl::variable<bool> hasSendRestrictions;\n\t\trpl::variable<int> starsPerMessage;\n\t};\n\tconst auto state = inner->lifetime().make_state<State>();\n\tconst auto channel = peer->asChannel();\n\tconst auto available = channel && channel->paidMessagesAvailable();\n\n\tUi::AddSkip(inner);\n\tUi::AddDivider(inner);\n\tauto charging = (Ui::SettingsButton*)nullptr;\n\tif (available) {\n\t\tUi::AddSkip(inner);\n\t\tconst auto starsPerMessage = peer->isChannel()\n\t\t\t? peer->asChannel()->commonStarsPerMessage()\n\t\t\t: 0;\n\t\tcharging = inner->add(object_ptr<Ui::SettingsButton>(\n\t\t\tinner,\n\t\t\ttr::lng_rights_charge_stars(),\n\t\t\tst::settingsButtonNoIcon));\n\t\tcharging->toggleOn(rpl::single(starsPerMessage > 0));\n\t\tUi::AddSkip(inner);\n\t\tUi::AddDividerText(inner, tr::lng_rights_charge_stars_about());\n\n\t\tconst auto chargeWrap = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\t\tchargeWrap->toggleOn(charging->toggledValue());\n\t\tchargeWrap->finishAnimating();\n\t\tconst auto chargeInner = chargeWrap->entity();\n\n\t\tUi::AddSkip(chargeInner);\n\t\tstate->starsPerMessage = SetupChargeSlider(\n\t\t\tchargeInner,\n\t\t\tpeer,\n\t\t\t(starsPerMessage > 0) ? starsPerMessage : std::optional<int>(),\n\t\t\tkDefaultChargeStars);\n\t}\n\n\tstatic constexpr auto kSendRestrictions = Flag::EmbedLinks\n\t\t| Flag::SendGames\n\t\t| Flag::SendGifs\n\t\t| Flag::SendInline\n\t\t| Flag::SendPolls\n\t\t| Flag::SendStickers\n\t\t| Flag::SendPhotos\n\t\t| Flag::SendVideos\n\t\t| Flag::SendVideoMessages\n\t\t| Flag::SendMusic\n\t\t| Flag::SendVoiceMessages\n\t\t| Flag::SendFiles\n\t\t| Flag::SendOther;\n\tstate->hasSendRestrictions = ((restrictions & kSendRestrictions) != 0)\n\t\t|| (peer->isChannel() && peer->asChannel()->slowmodeSeconds() > 0);\n\tstate->boostsUnrestrict = AddBoostsUnrestrictWrapped(\n\t\tinner,\n\t\tpeer,\n\t\tstate->hasSendRestrictions.value());\n\tstate->slowmodeSeconds = AddSlowmodeSlider(inner, peer);\n\tstate->hasSendRestrictions = rpl::combine(\n\t\trpl::single(\n\t\t\trestrictions\n\t\t) | rpl::then(std::move(changes)),\n\t\tstate->slowmodeSeconds.value()\n\t) | rpl::map([](ChatRestrictions restrictions, int slowmodeSeconds) {\n\t\treturn ((restrictions & kSendRestrictions) != 0) || slowmodeSeconds;\n\t});\n\n\tif (const auto channel = peer->asChannel()) {\n\t\tconstexpr auto kThresholdOffset = int(1000);\n\t\tconst auto threshold =  -kThresholdOffset\n\t\t\t+ channel->session().serverConfig().megagroupSizeMax;\n\t\tif (channel->amCreator()\n\t\t\t&& channel->membersCount() >= threshold) {\n\t\t\tAddSuggestGigagroup(\n\t\t\t\tinner,\n\t\t\t\tAboutGigagroupCallback(\n\t\t\t\t\tpeer->asChannel(),\n\t\t\t\t\tnavigation->parentController()));\n\t\t}\n\t}\n\n\tAddBannedButtons(inner, navigation, peer);\n\n\tbox->addButton(tr::lng_settings_save(), [=, rights = getRestrictions] {\n\t\tconst auto restrictions = rights();\n\t\tconst auto slowmodeSeconds = state->slowmodeSeconds.current();\n\t\tconst auto hasRestrictions = (slowmodeSeconds > 0)\n\t\t\t|| ((restrictions & kSendRestrictions) != 0);\n\t\tconst auto boostsUnrestrict = hasRestrictions\n\t\t\t? state->boostsUnrestrict.current()\n\t\t\t: 0;\n\t\tconst auto starsPerMessage = (charging && charging->toggled())\n\t\t\t? state->starsPerMessage.current()\n\t\t\t: 0;\n\t\tdone({\n\t\t\trestrictions,\n\t\t\tslowmodeSeconds,\n\t\t\tboostsUnrestrict,\n\t\t\tstarsPerMessage,\n\t\t});\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\n\tbox->setWidth(st::boxWideWidth);\n}\n\nFn<void()> AboutGigagroupCallback(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto weak = base::make_weak(controller);\n\n\tconst auto converting = std::make_shared<bool>();\n\tconst auto convertSure = [=] {\n\t\tif (*converting) {\n\t\t\treturn;\n\t\t}\n\t\t*converting = true;\n\t\tchannel->session().api().request(MTPchannels_ConvertToGigagroup(\n\t\t\tchannel->inputChannel()\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\tchannel->session().api().applyUpdates(result);\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->window().hideSettingsAndLayer();\n\t\t\t\tstrong->showToast(tr::lng_gigagroup_done(tr::now));\n\t\t\t}\n\t\t}).fail([=] {\n\t\t\t*converting = false;\n\t\t}).send();\n\t};\n\tconst auto convertWarn = [=] {\n\t\tconst auto strong = weak.get();\n\t\tif (*converting || !strong) {\n\t\t\treturn;\n\t\t}\n\t\tstrong->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tbox->setTitle(tr::lng_gigagroup_warning_title());\n\t\t\tbox->addRow(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tbox,\n\t\t\t\t\ttr::lng_gigagroup_warning(\n\t\t\t\t\t) | rpl::map(tr::rich),\n\t\t\t\t\tst::infoAboutGigagroup));\n\t\t\tbox->addButton(tr::lng_gigagroup_convert_sure(), convertSure);\n\t\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t\t}));\n\t};\n\treturn [=] {\n\t\tconst auto strong = weak.get();\n\t\tif (*converting || !strong) {\n\t\t\treturn;\n\t\t}\n\t\tstrong->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tbox->setTitle(tr::lng_gigagroup_convert_title());\n\t\t\tconst auto addFeature = [&](rpl::producer<QString> text) {\n\t\t\t\tusing namespace rpl::mappers;\n\t\t\t\tconst auto prefix = Ui::kQBullet + ' ';\n\t\t\t\tbox->addRow(\n\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\tbox,\n\t\t\t\t\t\tstd::move(text) | rpl::map(prefix + _1),\n\t\t\t\t\t\tst::infoAboutGigagroup),\n\t\t\t\t\tstyle::margins(\n\t\t\t\t\t\tst::boxRowPadding.left(),\n\t\t\t\t\t\tst::boxLittleSkip,\n\t\t\t\t\t\tst::boxRowPadding.right(),\n\t\t\t\t\t\tst::boxLittleSkip));\n\t\t\t};\n\t\t\taddFeature(tr::lng_gigagroup_convert_feature1());\n\t\t\taddFeature(tr::lng_gigagroup_convert_feature2());\n\t\t\taddFeature(tr::lng_gigagroup_convert_feature3());\n\t\t\tbox->addButton(tr::lng_gigagroup_convert_sure(), convertWarn);\n\t\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t\t}));\n\t};\n}\n\nstd::vector<RestrictionLabel> RestrictionLabels(\n\t\tData::RestrictionsSetOptions options) {\n\tauto result = std::vector<RestrictionLabel>();\n\tfor (const auto &[_, r] : NestedRestrictionLabelsList(options)) {\n\t\tresult.insert(result.end(), r.begin(), r.end());\n\t}\n\treturn result;\n}\n\nstd::vector<AdminRightLabel> AdminRightLabels(\n\t\tData::AdminRightsSetOptions options) {\n\tauto result = std::vector<AdminRightLabel>();\n\tfor (const auto &[_, r] : NestedAdminRightLabels(options)) {\n\t\tresult.insert(result.end(), r.begin(), r.end());\n\t}\n\treturn result;\n}\n\nEditFlagsControl<ChatRestrictions> CreateEditRestrictions(\n\t\tQWidget *parent,\n\t\tChatRestrictions restrictions,\n\t\tbase::flat_map<ChatRestrictions, QString> disabledMessages,\n\t\tData::RestrictionsSetOptions options) {\n\tauto widget = object_ptr<Ui::VerticalLayout>(parent);\n\tauto result = CreateEditFlags(\n\t\twidget.data(),\n\t\tNegateRestrictions(restrictions),\n\t\t{\n\t\t\t.labels = NestedRestrictionLabelsList(options),\n\t\t\t.disabledMessages = std::move(disabledMessages),\n\t\t});\n\tresult.widget = std::move(widget);\n\tresult.value = [original = std::move(result.value)]{\n\t\treturn NegateRestrictions(original());\n\t};\n\tresult.changes = std::move(\n\t\tresult.changes\n\t) | rpl::map(NegateRestrictions);\n\n\treturn result;\n}\n\nEditFlagsControl<ChatAdminRights> CreateEditAdminRights(\n\t\tQWidget *parent,\n\t\tChatAdminRights rights,\n\t\tbase::flat_map<ChatAdminRights, QString> disabledMessages,\n\t\tData::AdminRightsSetOptions options) {\n\tauto widget = object_ptr<Ui::VerticalLayout>(parent);\n\tauto result = CreateEditFlags(\n\t\twidget.data(),\n\t\trights,\n\t\t{\n\t\t\t.labels = NestedAdminRightLabels(options),\n\t\t\t.disabledMessages = std::move(disabledMessages),\n\t\t});\n\tresult.widget = std::move(widget);\n\n\treturn result;\n}\n\nChatAdminRights DisabledByDefaultRestrictions(not_null<PeerData*> peer) {\n\tusing Flag = ChatAdminRight;\n\tusing Restriction = ChatRestriction;\n\n\tconst auto restrictions = FixDependentRestrictions([&] {\n\t\tif (const auto chat = peer->asChat()) {\n\t\t\treturn chat->defaultRestrictions();\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\treturn channel->defaultRestrictions();\n\t\t}\n\t\tUnexpected(\"User in DisabledByDefaultRestrictions.\");\n\t}());\n\treturn Flag(0)\n\t\t| ((restrictions & Restriction::PinMessages)\n\t\t\t? Flag(0)\n\t\t\t: Flag::PinMessages)\n\t\t//\n\t\t// We allow to edit 'invite_users' admin right no matter what\n\t\t// is chosen in default permissions for 'invite_users', because\n\t\t// if everyone can 'invite_users' it handles invite link for admins.\n\t\t//\n\t\t//| ((restrictions & Restriction::AddParticipants)\n\t\t//\t? Flag(0)\n\t\t//\t: Flag::InviteByLinkOrAdd)\n\t\t//\n\t\t| ((restrictions & Restriction::ChangeInfo)\n\t\t\t? Flag(0)\n\t\t\t: Flag::ChangeInfo);\n}\n\nChatRestrictions FixDependentRestrictions(ChatRestrictions restrictions) {\n\tconst auto &dependencies = Dependencies(restrictions);\n\n\t// Fix iOS bug of saving send_inline like embed_links.\n\t// We copy send_stickers to send_inline.\n\tif (restrictions & ChatRestriction::SendStickers) {\n\t\trestrictions |= ChatRestriction::SendInline;\n\t} else {\n\t\trestrictions &= ~ChatRestriction::SendInline;\n\t}\n\n\t// Apply the strictest.\n\tconst auto fixOne = [&] {\n\t\tfor (const auto &[first, second] : dependencies) {\n\t\t\tif ((restrictions & second) && !(restrictions & first)) {\n\t\t\t\trestrictions |= first;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\twhile (fixOne()) {\n\t}\n\treturn restrictions;\n}\n\nChatAdminRights AdminRightsForOwnershipTransfer(\n\t\tData::AdminRightsSetOptions options) {\n\tauto result = ChatAdminRights();\n\tfor (const auto &entry : AdminRightLabels(options)) {\n\t\tif (!(entry.flags & ChatAdminRight::Anonymous)) {\n\t\t\tresult |= entry.flags;\n\t\t}\n\t}\n\treturn result;\n}\n\nEditFlagsControl<PowerSaving::Flags> CreateEditPowerSaving(\n\t\tQWidget *parent,\n\t\tPowerSaving::Flags flags,\n\t\trpl::producer<QString> forceDisabledMessage,\n\t\tPowerSaving::Flags highlightFlags) {\n\tauto widget = object_ptr<Ui::VerticalLayout>(parent);\n\tauto descriptor = Settings::PowerSavingLabels();\n\tdescriptor.forceDisabledMessage = std::move(forceDisabledMessage);\n\tdescriptor.highlightFlags = highlightFlags;\n\tauto result = CreateEditFlags(\n\t\twidget.data(),\n\t\tflags,\n\t\tstd::move(descriptor));\n\tresult.widget = std::move(widget);\n\n\treturn result;\n}\n\nEditFlagsControl<AdminLog::FilterValue::Flags> CreateEditAdminLogFilter(\n\t\tQWidget *parent,\n\t\tAdminLog::FilterValue::Flags flags,\n\t\tbool isChannel) {\n\tauto widget = object_ptr<Ui::VerticalLayout>(parent);\n\tauto descriptor = AdminLog::FilterValueLabels(isChannel);\n\tauto result = CreateEditFlags(\n\t\twidget.data(),\n\t\tflags,\n\t\tstd::move(descriptor));\n\tresult.widget = std::move(widget);\n\n\treturn result;\n}\n\nEditFlagsControl<Data::ChatbotsPermissions> CreateEditChatbotPermissions(\n\t\tQWidget *parent,\n\t\tData::ChatbotsPermissions flags) {\n\tauto widget = object_ptr<Ui::VerticalLayout>(parent);\n\tauto descriptor = Data::ChatbotsPermissionsLabels();\n\tdescriptor.disabledMessages.emplace(\n\t\tData::ChatbotsPermission::ViewMessages,\n\t\tQString());\n\tauto result = CreateEditFlags(\n\t\twidget.data(),\n\t\tflags | Data::ChatbotsPermission::ViewMessages,\n\t\tstd::move(descriptor));\n\tresult.widget = std::move(widget);\n\n\treturn result;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"history/admin_log/history_admin_log_filter_value.h\"\n\nnamespace style {\nstruct SettingsButton;\n} // namespace style\n\nnamespace Ui {\nclass GenericBox;\nclass RoundButton;\nclass RpWidget;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace PowerSaving {\nenum Flag : uint32;\nusing Flags = base::flags<Flag>;\n} // namespace PowerSaving\n\nnamespace Data {\nenum class ChatbotsPermission;\nusing ChatbotsPermissions = base::flags<ChatbotsPermission>;\n} // namespace Data\n\ntemplate <typename Object>\nclass object_ptr;\n\nnamespace Window {\nclass SessionController;\nclass SessionNavigation;\n} // namespace Window\n\nstruct EditPeerPermissionsBoxResult final {\n\tChatRestrictions rights;\n\tint slowmodeSeconds = 0;\n\tint boostsUnrestrict = 0;\n\tint starsPerMessage = 0;\n};\n\nvoid ShowEditPeerPermissionsBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> channelOrGroup,\n\tFn<void(EditPeerPermissionsBoxResult)> done);\n\n[[nodiscard]] Fn<void()> AboutGigagroupCallback(\n\tnot_null<ChannelData*> channel,\n\tnot_null<Window::SessionController*> controller);\n\ntemplate <typename Flags>\nstruct EditFlagsLabel {\n\tFlags flags;\n\tQString label;\n\tconst style::icon *icon = nullptr;\n};\n\ntemplate <typename Flags>\nstruct EditFlagsControl {\n\tobject_ptr<Ui::RpWidget> widget;\n\tFn<Flags()> value;\n\trpl::producer<Flags> changes;\n\tQPointer<Ui::RpWidget> highlightWidget;\n};\n\ntemplate <typename Flags>\nstruct NestedEditFlagsLabels {\n\tstd::optional<rpl::producer<QString>> nestingLabel;\n\tstd::vector<EditFlagsLabel<Flags>> nested;\n};\n\ntemplate <typename Flags>\nstruct EditFlagsDescriptor {\n\tstd::vector<NestedEditFlagsLabels<Flags>> labels;\n\tbase::flat_map<Flags, QString> disabledMessages;\n\tconst style::SettingsButton *st = nullptr;\n\trpl::producer<QString> forceDisabledMessage;\n\tFlags highlightFlags = Flags();\n};\n\nusing RestrictionLabel = EditFlagsLabel<ChatRestrictions>;\n[[nodiscard]] std::vector<RestrictionLabel> RestrictionLabels(\n\tData::RestrictionsSetOptions options);\n\nusing AdminRightLabel = EditFlagsLabel<ChatAdminRights>;\n[[nodiscard]] std::vector<AdminRightLabel> AdminRightLabels(\n\tData::AdminRightsSetOptions options);\n\n[[nodiscard]] auto CreateEditRestrictions(\n\tQWidget *parent,\n\tChatRestrictions restrictions,\n\tbase::flat_map<ChatRestrictions, QString> disabledMessages,\n\tData::RestrictionsSetOptions options)\n-> EditFlagsControl<ChatRestrictions>;\n\n[[nodiscard]] auto CreateEditAdminRights(\n\tQWidget *parent,\n\tChatAdminRights rights,\n\tbase::flat_map<ChatAdminRights, QString> disabledMessages,\n\tData::AdminRightsSetOptions options)\n-> EditFlagsControl<ChatAdminRights>;\n\n[[nodiscard]] ChatAdminRights DisabledByDefaultRestrictions(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] ChatRestrictions FixDependentRestrictions(\n\tChatRestrictions restrictions);\n[[nodiscard]] ChatAdminRights AdminRightsForOwnershipTransfer(\n\tData::AdminRightsSetOptions options);\n\n[[nodiscard]] auto CreateEditPowerSaving(\n\tQWidget *parent,\n\tPowerSaving::Flags flags,\n\trpl::producer<QString> forceDisabledMessage,\n\tPowerSaving::Flags highlightFlags = PowerSaving::Flags()\n) -> EditFlagsControl<PowerSaving::Flags>;\n\n[[nodiscard]] auto CreateEditAdminLogFilter(\n\tQWidget *parent,\n\tAdminLog::FilterValue::Flags flags,\n\tbool isChannel\n) -> EditFlagsControl<AdminLog::FilterValue::Flags>;\n\n[[nodiscard]] auto CreateEditChatbotPermissions(\n\tQWidget *parent,\n\tData::ChatbotsPermissions flags\n) -> EditFlagsControl<Data::ChatbotsPermissions>;\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_peer_reactions.h\"\n\n#include \"apiwrap.h\"\n#include \"base/event_filter.h\"\n#include \"chat_helpers/emoji_list_widget.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_document.h\"\n#include \"data/data_peer_values.h\" // UniqueReactionsLimit.\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/view/reactions/history_view_reactions_selector.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/boost_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_session_controller_link_info.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\n#include <QtWidgets/QTextEdit>\n#include <QtGui/QTextBlock>\n#include <QtGui/QTextDocumentFragment>\n\nnamespace {\n\nconstexpr auto kDisabledEmojiOpacity = 0.4;\n\nstruct UniqueCustomEmojiContext {\n\tstd::vector<DocumentId> ids;\n\tFn<bool(DocumentId)> applyHardLimit;\n\tint hardLimit = 0;\n\tint hardLimitChecked = 0;\n\tbool hardLimitHit = false;\n};\n\nclass MaybeDisabledEmoji final : public Ui::Text::CustomEmoji {\npublic:\n\tMaybeDisabledEmoji(\n\t\tstd::unique_ptr<CustomEmoji> wrapped,\n\t\tFn<bool()> enabled);\n\n\tint width() override;\n\tQString entityData() override;\n\tvoid paint(QPainter &p, const Context &context) override;\n\tvoid unload() override;\n\tbool ready() override;\n\tbool readyInDefaultState() override;\n\nprivate:\n\tconst std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;\n\tconst Fn<bool()> _enabled;\n\n};\n\nMaybeDisabledEmoji::MaybeDisabledEmoji(\n\tstd::unique_ptr<CustomEmoji> wrapped,\n\tFn<bool()> enabled)\n: _wrapped(std::move(wrapped))\n, _enabled(std::move(enabled)) {\n}\n\nint MaybeDisabledEmoji::width() {\n\treturn _wrapped->width();\n}\n\nQString MaybeDisabledEmoji::entityData() {\n\treturn _wrapped->entityData();\n}\n\nvoid MaybeDisabledEmoji::paint(QPainter &p, const Context &context) {\n\tconst auto disabled = !_enabled();\n\tconst auto was = disabled ? p.opacity() : 1.;\n\tif (disabled) {\n\t\tp.setOpacity(kDisabledEmojiOpacity);\n\t}\n\t_wrapped->paint(p, context);\n\tif (disabled) {\n\t\tp.setOpacity(was);\n\t}\n}\n\nvoid MaybeDisabledEmoji::unload() {\n\t_wrapped->unload();\n}\n\nbool MaybeDisabledEmoji::ready() {\n\treturn _wrapped->ready();\n}\n\nbool MaybeDisabledEmoji::readyInDefaultState() {\n\treturn _wrapped->readyInDefaultState();\n}\n\n[[nodiscard]] QString AllowOnlyCustomEmojiProcessor(QStringView mimeTag) {\n\tauto all = TextUtilities::SplitTags(mimeTag);\n\tfor (auto i = all.begin(); i != all.end();) {\n\t\tif (Ui::InputField::IsCustomEmojiLink(*i)) {\n\t\t\t++i;\n\t\t} else {\n\t\t\ti = all.erase(i);\n\t\t}\n\t}\n\treturn TextUtilities::JoinTag(all);\n}\n\n[[nodiscard]] bool AllowOnlyCustomEmojiMimeDataHook(\n\t\tnot_null<const QMimeData*> data,\n\t\tUi::InputField::MimeAction action) {\n\tif (action == Ui::InputField::MimeAction::Check) {\n\t\tconst auto textMime = TextUtilities::TagsTextMimeType();\n\t\tconst auto tagsMime = TextUtilities::TagsMimeType();\n\t\tif (!data->hasFormat(textMime) || !data->hasFormat(tagsMime)) {\n\t\t\treturn false;\n\t\t}\n\t\tauto text = QString::fromUtf8(data->data(textMime));\n\t\tauto tags = TextUtilities::DeserializeTags(\n\t\t\tdata->data(tagsMime),\n\t\t\ttext.size());\n\t\tauto checkedTill = 0;\n\t\tranges::sort(tags, ranges::less(), &TextWithTags::Tag::offset);\n\t\tfor (const auto &tag : tags) {\n\t\t\tif (tag.offset != checkedTill\n\t\t\t\t|| AllowOnlyCustomEmojiProcessor(tag.id) != tag.id) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tcheckedTill += tag.length;\n\t\t}\n\t\treturn true;\n\t} else if (action == Ui::InputField::MimeAction::Insert) {\n\t\treturn false;\n\t}\n\tUnexpected(\"Action in MimeData hook.\");\n}\n\n[[nodiscard]] std::vector<Data::ReactionId> DefaultSelected() {\n\tconst auto like = QString::fromUtf8(\"\\xf0\\x9f\\x91\\x8d\");\n\tconst auto dislike = QString::fromUtf8(\"\\xf0\\x9f\\x91\\x8e\");\n\treturn { Data::ReactionId{ like }, Data::ReactionId{ dislike } };\n}\n\n[[nodiscard]] std::vector<Data::ReactionId> CollectAvailableReactions(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto &all = session->data().reactions().list(\n\t\tData::Reactions::Type::Active);\n\tif (all.empty()) {\n\t\treturn DefaultSelected();\n\t}\n\tauto result = std::vector<Data::ReactionId>();\n\tresult.reserve(all.size());\n\tfor (const auto &reaction : all) {\n\t\tif (!reaction.id.paid()) {\n\t\t\tresult.push_back(reaction.id);\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] bool RemoveNonCustomEmojiFragment(\n\t\tnot_null<QTextDocument*> document,\n\t\tUniqueCustomEmojiContext &context) {\n\tcontext.ids.clear();\n\tcontext.hardLimitChecked = 0;\n\tauto removeFrom = 0;\n\tauto removeTill = 0;\n\tauto block = document->begin();\n\tfor (auto j = block.begin(); !j.atEnd(); ++j) {\n\t\tconst auto fragment = j.fragment();\n\t\tAssert(fragment.isValid());\n\n\t\tremoveTill = removeFrom = fragment.position();\n\t\tconst auto format = fragment.charFormat();\n\t\tif (format.objectType() != Ui::InputField::kCustomEmojiFormat) {\n\t\t\tremoveTill += fragment.length();\n\t\t\tbreak;\n\t\t}\n\t\tconst auto id = format.property(Ui::InputField::kCustomEmojiId);\n\t\tconst auto documentId = id.toULongLong();\n\t\tconst auto applyHardLimit = context.applyHardLimit(documentId);\n\t\tif (ranges::contains(context.ids, documentId)) {\n\t\t\tremoveTill += fragment.length();\n\t\t\tbreak;\n\t\t} else if (applyHardLimit\n\t\t\t&& context.hardLimitChecked >= context.hardLimit) {\n\t\t\tcontext.hardLimitHit = true;\n\t\t\tremoveTill += fragment.length();\n\t\t\tbreak;\n\t\t}\n\t\tcontext.ids.push_back(documentId);\n\t\tif (applyHardLimit) {\n\t\t\t++context.hardLimitChecked;\n\t\t}\n\t}\n\twhile (removeTill == removeFrom) {\n\t\tblock = block.next();\n\t\tif (block == document->end()) {\n\t\t\treturn false;\n\t\t}\n\t\tremoveTill = block.position();\n\t}\n\tUi::PrepareFormattingOptimization(document);\n\tauto cursor = QTextCursor(document);\n\tcursor.setPosition(removeFrom);\n\tcursor.setPosition(removeTill, QTextCursor::KeepAnchor);\n\tcursor.removeSelectedText();\n\treturn true;\n}\n\nbool RemoveNonCustomEmoji(\n\t\tnot_null<QTextDocument*> document,\n\t\tUniqueCustomEmojiContext &context) {\n\tif (!RemoveNonCustomEmojiFragment(document, context)) {\n\t\treturn false;\n\t}\n\twhile (RemoveNonCustomEmojiFragment(document, context)) {\n\t}\n\treturn true;\n}\n\nvoid SetupOnlyCustomEmojiField(\n\t\tnot_null<Ui::InputField*> field,\n\t\tFn<void(std::vector<DocumentId>, bool)> callback,\n\t\tFn<bool(DocumentId)> applyHardLimit,\n\t\tint customHardLimit) {\n\tfield->setTagMimeProcessor(AllowOnlyCustomEmojiProcessor);\n\tfield->setMimeDataHook(AllowOnlyCustomEmojiMimeDataHook);\n\n\tstruct State {\n\t\tbool processing = false;\n\t\tbool pending = false;\n\t};\n\tconst auto state = field->lifetime().make_state<State>();\n\n\tfield->changes(\n\t) | rpl::on_next([=] {\n\t\tstate->pending = true;\n\t\tif (state->processing) {\n\t\t\treturn;\n\t\t}\n\t\tauto context = UniqueCustomEmojiContext{\n\t\t\t.applyHardLimit = applyHardLimit,\n\t\t\t.hardLimit = customHardLimit,\n\t\t};\n\t\tauto changed = false;\n\t\tstate->processing = true;\n\t\twhile (state->pending) {\n\t\t\tstate->pending = false;\n\t\t\tconst auto document = field->rawTextEdit()->document();\n\t\t\tconst auto pageSize = document->pageSize();\n\t\t\tQTextCursor(document).joinPreviousEditBlock();\n\t\t\tif (RemoveNonCustomEmoji(document, context)) {\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t\tstate->processing = false;\n\t\t\tQTextCursor(document).endEditBlock();\n\t\t\tif (document->pageSize() != pageSize) {\n\t\t\t\tdocument->setPageSize(pageSize);\n\t\t\t}\n\t\t}\n\t\tcallback(context.ids, context.hardLimitHit);\n\t\tif (changed) {\n\t\t\tfield->forceProcessContentsChanges();\n\t\t}\n\t}, field->lifetime());\n}\n\n[[nodiscard]] TextWithTags ComposeEmojiList(\n\t\tnot_null<Data::Reactions*> reactions,\n\t\tconst std::vector<Data::ReactionId> &list) {\n\tauto result = TextWithTags();\n\tconst auto size = [&] {\n\t\treturn int(result.text.size());\n\t};\n\tauto added = base::flat_set<Data::ReactionId>();\n\tconst auto &all = reactions->list(Data::Reactions::Type::All);\n\tconst auto add = [&](Data::ReactionId id) {\n\t\tif (!added.emplace(id).second) {\n\t\t\treturn;\n\t\t}\n\t\tauto unifiedId = id.custom();\n\t\tconst auto offset = size();\n\t\tif (unifiedId) {\n\t\t\tresult.text.append('@');\n\t\t} else if (id.paid()) {\n\t\t\tresult.text.append(QChar(0x2B50));\n\t\t\tunifiedId = reactions->lookupPaid()->selectAnimation->id;\n\t\t} else {\n\t\t\tresult.text.append(id.emoji());\n\t\t\tconst auto i = ranges::find(all, id, &Data::Reaction::id);\n\t\t\tif (i == end(all)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tunifiedId = i->selectAnimation->id;\n\t\t}\n\t\tconst auto data = Data::SerializeCustomEmojiId(unifiedId);\n\t\tconst auto tag = Ui::InputField::CustomEmojiLink(data);\n\t\tresult.tags.append({ offset, size() - offset, tag });\n\t};\n\tfor (const auto &id : list) {\n\t\tadd(id);\n\t}\n\treturn result;\n}\n\nenum class ReactionsSelectorState {\n\tActive,\n\tDisabled,\n\tHidden,\n};\n\nstruct ReactionsSelectorArgs {\n\tnot_null<QWidget*> outer;\n\tnot_null<Window::SessionController*> controller;\n\trpl::producer<QString> title;\n\tstd::vector<Data::Reaction> list;\n\tstd::vector<Data::ReactionId> selected;\n\trpl::producer<bool> paid;\n\tFn<void(std::vector<Data::ReactionId>, bool)> callback;\n\trpl::producer<ReactionsSelectorState> stateValue;\n\tint customAllowed = 0;\n\tint customHardLimit = 0;\n\tbool all = false;\n\tbool isGroup = false;\n};\n\nobject_ptr<Ui::RpWidget> AddReactionsSelector(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tReactionsSelectorArgs &&args) {\n\tusing namespace ChatHelpers;\n\tusing HistoryView::Reactions::UnifiedFactoryOwner;\n\n\tauto result = object_ptr<Ui::InputField>(\n\t\tparent,\n\t\tst::manageGroupReactionsField,\n\t\tUi::InputField::Mode::MultiLine,\n\t\tstd::move(args.title));\n\tconst auto raw = result.data();\n\tconst auto session = &args.controller->session();\n\tconst auto owner = &session->data();\n\tconst auto reactions = &owner->reactions();\n\tconst auto customAllowed = args.customAllowed;\n\n\tstruct State {\n\t\tstd::unique_ptr<Ui::RpWidget> overlay;\n\t\tstd::unique_ptr<UnifiedFactoryOwner> unifiedFactoryOwner;\n\t\tUnifiedFactoryOwner::RecentFactory factory;\n\t\tbase::flat_set<DocumentId> allowed;\n\t\tstd::vector<Data::ReactionId> reactions;\n\t\trpl::lifetime focusLifetime;\n\t};\n\tconst auto paid = reactions->lookupPaid();\n\tauto normal = reactions->list(Data::Reactions::Type::Active);\n\tnormal.push_back(*paid);\n\tconst auto state = raw->lifetime().make_state<State>();\n\tstate->unifiedFactoryOwner = std::make_unique<UnifiedFactoryOwner>(\n\t\tsession,\n\t\tnormal);\n\tstate->factory = state->unifiedFactoryOwner->factory();\n\tstate->reactions = std::move(args.selected);\n\n\tconst auto customEmojiPaused = [controller = args.controller] {\n\t\treturn controller->isGifPausedAtLeastFor(PauseReason::Layer);\n\t};\n\tauto simpleContext = Core::TextContext({\n\t\t.session = session,\n\t\t.repaint = [=] { raw->update(); },\n\t});\n\tauto context = simpleContext;\n\tcontext.customEmojiFactory = [=](\n\t\tQStringView data,\n\t\tconst Ui::Text::MarkedContext &context\n\t) -> std::unique_ptr<Ui::Text::CustomEmoji> {\n\t\tconst auto id = Data::ParseCustomEmojiData(data);\n\t\tauto result = Ui::Text::MakeCustomEmoji(data, simpleContext);\n\t\tif (state->unifiedFactoryOwner->lookupReactionId(id).custom()) {\n\t\t\treturn std::make_unique<MaybeDisabledEmoji>(\n\t\t\t\tstd::move(result),\n\t\t\t\t[=] { return state->allowed.contains(id); });\n\t\t}\n\t\tusing namespace Ui::Text;\n\t\treturn std::make_unique<FirstFrameEmoji>(std::move(result));\n\t};\n\traw->setCustomTextContext(\n\t\tstd::move(context),\n\t\tcustomEmojiPaused,\n\t\tcustomEmojiPaused);\n\n\tconst auto callback = args.callback;\n\tconst auto isCustom = [=](DocumentId id) {\n\t\treturn state->unifiedFactoryOwner->lookupReactionId(id).custom();\n\t};\n\tSetupOnlyCustomEmojiField(raw, [=](\n\t\t\tstd::vector<DocumentId> ids,\n\t\t\tbool hardLimitHit) {\n\t\tauto allowed = base::flat_set<DocumentId>();\n\t\tauto reactions = std::vector<Data::ReactionId>();\n\t\treactions.reserve(ids.size());\n\t\tallowed.reserve(std::min(customAllowed, int(ids.size())));\n\t\tconst auto owner = state->unifiedFactoryOwner.get();\n\t\tfor (const auto id : ids) {\n\t\t\tconst auto reactionId = owner->lookupReactionId(id);\n\t\t\tif (reactionId.custom() && allowed.size() < customAllowed) {\n\t\t\t\tallowed.emplace(id);\n\t\t\t}\n\t\t\treactions.push_back(reactionId);\n\t\t}\n\t\tif (state->allowed != allowed) {\n\t\t\tstate->allowed = std::move(allowed);\n\t\t\traw->rawTextEdit()->update();\n\t\t}\n\t\tstate->reactions = reactions;\n\t\tcallback(std::move(reactions), hardLimitHit);\n\t}, isCustom, args.customHardLimit);\n\tconst auto applyFromState = [=] {\n\t\traw->setTextWithTags(ComposeEmojiList(reactions, state->reactions));\n\t};\n\n\tapplyFromState();\n\tstd::move(\n\t\targs.paid\n\t) | rpl::on_next([=](bool paid) {\n\t\tconst auto id = Data::ReactionId::Paid();\n\t\tif (paid && !ranges::contains(state->reactions, id)) {\n\t\t\tstate->reactions.insert(begin(state->reactions), id);\n\t\t\tapplyFromState();\n\t\t} else if (!paid && ranges::contains(state->reactions, id)) {\n\t\t\tstate->reactions.erase(\n\t\t\t\tranges::remove(state->reactions, id),\n\t\t\t\tend(state->reactions));\n\t\t\tapplyFromState();\n\t\t}\n\t}, raw->lifetime());\n\n\tconst auto toggle = Ui::CreateChild<Ui::IconButton>(\n\t\tparent.get(),\n\t\tst::manageGroupReactions);\n\n\tusing SelectorState = ReactionsSelectorState;\n\tstd::move(\n\t\targs.stateValue\n\t) | rpl::on_next([=, all = args.all](SelectorState value) {\n\t\tswitch (value) {\n\t\tcase SelectorState::Active:\n\t\t\tstate->overlay = nullptr;\n\t\t\tstate->focusLifetime.destroy();\n\t\t\tif (raw->empty()) {\n\t\t\t\traw->setTextWithTags(\n\t\t\t\t\tComposeEmojiList(\n\t\t\t\t\t\treactions,\n\t\t\t\t\t\tall\n\t\t\t\t\t\t\t? CollectAvailableReactions(\n\t\t\t\t\t\t\t\t&args.controller->session())\n\t\t\t\t\t\t\t: DefaultSelected()));\n\t\t\t}\n\t\t\traw->setDisabled(false);\n\t\t\traw->setFocusFast();\n\t\t\tbreak;\n\t\tcase SelectorState::Disabled:\n\t\t\tstate->overlay = std::make_unique<Ui::RpWidget>(parent);\n\t\t\tstate->overlay->show();\n\t\t\traw->geometryValue() | rpl::on_next([=](QRect rect) {\n\t\t\t\tstate->overlay->setGeometry(rect);\n\t\t\t}, state->overlay->lifetime());\n\t\t\tstate->overlay->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\t\t\tauto color = st::boxBg->c;\n\t\t\t\tcolor.setAlphaF(0.5);\n\t\t\t\tQPainter(state->overlay.get()).fillRect(\n\t\t\t\t\tclip,\n\t\t\t\t\tcolor);\n\t\t\t}, state->overlay->lifetime());\n\t\t\t[[fallthrough]];\n\t\tcase SelectorState::Hidden:\n\t\t\tif (Ui::InFocusChain(raw)) {\n\t\t\t\traw->parentWidget()->setFocus();\n\t\t\t}\n\t\t\traw->setDisabled(true);\n\t\t\traw->focusedChanges(\n\t\t\t) | rpl::on_next([=](bool focused) {\n\t\t\t\tif (focused) {\n\t\t\t\t\traw->parentWidget()->setFocus();\n\t\t\t\t}\n\t\t\t}, state->focusLifetime);\n\t\t\tbreak;\n\t\t}\n\t}, raw->lifetime());\n\n\tconst auto panel = Ui::CreateChild<TabbedPanel>(\n\t\targs.outer.get(),\n\t\targs.controller,\n\t\tobject_ptr<TabbedSelector>(\n\t\t\tnullptr,\n\t\t\targs.controller->uiShow(),\n\t\t\tWindow::GifPauseReason::Layer,\n\t\t\t(args.all\n\t\t\t\t? TabbedSelector::Mode::FullReactions\n\t\t\t\t: TabbedSelector::Mode::RecentReactions)));\n\tauto panelList = state->unifiedFactoryOwner->unifiedIdsList();\n\tpanelList.erase(\n\t\tranges::remove(panelList, paid->selectAnimation->id),\n\t\tend(panelList));\n\tpanel->selector()->provideRecentEmoji(\n\t\tChatHelpers::DocumentListToRecent(panelList));\n\tpanel->setDesiredHeightValues(\n\t\t1.,\n\t\tst::emojiPanMinHeight / 2,\n\t\tst::emojiPanMinHeight);\n\tpanel->hide();\n\tpanel->selector()->customEmojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\tData::InsertCustomEmoji(raw, data.document);\n\t}, panel->lifetime());\n\n\tconst auto updateEmojiPanelGeometry = [=] {\n\t\tconst auto parent = panel->parentWidget();\n\t\tconst auto global = toggle->mapToGlobal({ 0, 0 });\n\t\tconst auto local = parent->mapFromGlobal(global);\n\t\tpanel->moveBottomRight(\n\t\t\tlocal.y(),\n\t\t\tlocal.x() + toggle->width() * 3);\n\t};\n\tconst auto scheduleUpdateEmojiPanelGeometry = [=] {\n\t\t// updateEmojiPanelGeometry uses not only container geometry, but\n\t\t// also container children geometries that will be updated later.\n\t\tcrl::on_main(raw, updateEmojiPanelGeometry);\n\t};\n\tconst auto filterCallback = [=](not_null<QEvent*> event) {\n\t\tconst auto type = event->type();\n\t\tif (type == QEvent::Move || type == QEvent::Resize) {\n\t\t\tscheduleUpdateEmojiPanelGeometry();\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t};\n\tfor (auto widget = (QWidget*)raw\n\t\t; widget && widget != args.outer\n\t\t; widget = widget->parentWidget()) {\n\t\tbase::install_event_filter(raw, widget, filterCallback);\n\t}\n\tbase::install_event_filter(raw, args.outer, filterCallback);\n\tscheduleUpdateEmojiPanelGeometry();\n\n\ttoggle->installEventFilter(panel);\n\ttoggle->addClickHandler([=] {\n\t\tpanel->toggleAnimated();\n\t});\n\n\traw->geometryValue() | rpl::on_next([=](QRect geometry) {\n\t\ttoggle->move(\n\t\t\tgeometry.x() + geometry.width() - toggle->width(),\n\t\t\tgeometry.y() + geometry.height() - toggle->height());\n\t\tupdateEmojiPanelGeometry();\n\t}, toggle->lifetime());\n\n\treturn result;\n}\n\nvoid AddReactionsText(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tint allowedCustomReactions,\n\t\trpl::producer<int> customCountValue,\n\t\tFn<void(int required)> askForBoosts) {\n\tauto ownedInner = object_ptr<Ui::VerticalLayout>(container);\n\tconst auto inner = ownedInner.data();\n\tconst auto count = inner->lifetime().make_state<rpl::variable<int>>(\n\t\tstd::move(customCountValue));\n\n\tcontainer->add(\n\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\tcontainer,\n\t\t\tstd::move(ownedInner),\n\t\t\tst::defaultBoxDividerLabelPadding),\n\t\tQMargins(0, st::manageGroupReactionsTextSkip, 0, 0));\n\tconst auto label = inner->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tinner,\n\t\t\ttr::lng_manage_peer_reactions_own(\n\t\t\t\tlt_link,\n\t\t\t\ttr::lng_manage_peer_reactions_own_link(tr::link),\n\t\t\t\ttr::marked),\n\t\t\tst::boxDividerLabel));\n\tconst auto weak = base::make_weak(navigation);\n\tlabel->setClickHandlerFilter([=](const auto &...) {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->showPeerByLink(Window::PeerByLinkInfo{\n\t\t\t\t.usernameOrId = u\"stickers\"_q,\n\t\t\t\t.resolveType = Window::ResolveType::Mention,\n\t\t\t});\n\t\t}\n\t\treturn false;\n\t});\n\tauto countString = count->value() | rpl::map([](int count) {\n\t\treturn TextWithEntities{ QString::number(count) };\n\t});\n\tauto needs = rpl::combine(\n\t\ttr::lng_manage_peer_reactions_level(\n\t\t\tlt_count,\n\t\t\tcount->value() | tr::to_count(),\n\t\t\tlt_same_count,\n\t\t\tstd::move(countString),\n\t\t\ttr::rich),\n\t\ttr::lng_manage_peer_reactions_boost(\n\t\t\tlt_link,\n\t\t\ttr::lng_manage_peer_reactions_boost_link(tr::link),\n\t\t\ttr::rich)\n\t) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {\n\t\ta.append(' ').append(std::move(b));\n\t\treturn std::move(a);\n\t});\n\tconst auto wrap = inner->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::FlatLabel>>(\n\t\t\tinner,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tinner,\n\t\t\t\tstd::move(needs),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tQMargins{ 0, st::normalFont->height, 0, 0 }));\n\twrap->toggleOn(count->value() | rpl::map(\n\t\trpl::mappers::_1 > allowedCustomReactions\n\t));\n\twrap->finishAnimating();\n\n\twrap->entity()->setClickHandlerFilter([=](const auto &...) {\n\t\taskForBoosts(count->current());\n\t\treturn false;\n\t});\n}\n\n} // namespace\n\nvoid EditAllowedReactionsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tEditAllowedReactionsArgs &&args) {\n\tusing namespace Data;\n\tusing namespace rpl::mappers;\n\n\tbox->setTitle(tr::lng_manage_peer_reactions());\n\tbox->setWidth(st::boxWideWidth);\n\n\tenum class Option {\n\t\tAll,\n\t\tSome,\n\t\tNone,\n\t};\n\tusing SelectorState = ReactionsSelectorState;\n\tstruct State {\n\t\trpl::variable<Option> option; // For groups.\n\t\trpl::variable<SelectorState> selectorState;\n\t\tstd::vector<Data::ReactionId> selected;\n\t\trpl::variable<int> customCount;\n\t\trpl::variable<bool> paidEnabled;\n\t};\n\tconst auto allowed = args.allowed;\n\tconst auto optionInitial = (allowed.type != AllowedReactionsType::Some)\n\t\t? Option::All\n\t\t: (allowed.some.empty() && !allowed.paidEnabled)\n\t\t? Option::None\n\t\t: Option::Some;\n\tconst auto state = box->lifetime().make_state<State>(State{\n\t\t.option = optionInitial,\n\t\t.paidEnabled = allowed.paidEnabled,\n\t});\n\n\tconst auto container = box->verticalLayout();\n\tconst auto isGroup = args.isGroup;\n\tconst auto enabled = isGroup\n\t\t? nullptr\n\t\t: container->add(object_ptr<Ui::SettingsButton>(\n\t\t\tcontainer.get(),\n\t\t\ttr::lng_manage_peer_reactions_enable(),\n\t\t\tst::manageGroupNoIconButton.button));\n\tif (enabled) {\n\t\tenabled->toggleOn(rpl::single(optionInitial != Option::None));\n\t\tenabled->toggledValue(\n\t\t) | rpl::on_next([=](bool value) {\n\t\t\tstate->selectorState = value\n\t\t\t\t? SelectorState::Active\n\t\t\t\t: SelectorState::Disabled;\n\t\t}, enabled->lifetime());\n\t}\n\tconst auto group = std::make_shared<Ui::RadioenumGroup<Option>>(\n\t\tstate->option.current());\n\tgroup->setChangedCallback([=](Option value) {\n\t\tstate->option = value;\n\t});\n\tconst auto addOption = [&](Option option, const QString &text) {\n\t\tif (!isGroup) {\n\t\t\treturn;\n\t\t}\n\t\tcontainer->add(\n\t\t\tobject_ptr<Ui::Radioenum<Option>>(\n\t\t\t\tcontainer,\n\t\t\t\tgroup,\n\t\t\t\toption,\n\t\t\t\ttext,\n\t\t\t\tst::settingsSendType),\n\t\t\tst::settingsSendTypePadding);\n\t};\n\taddOption(Option::All, tr::lng_manage_peer_reactions_all(tr::now));\n\taddOption(Option::Some, tr::lng_manage_peer_reactions_some(tr::now));\n\taddOption(Option::None, tr::lng_manage_peer_reactions_none(tr::now));\n\n\tconst auto about = [](Option option) {\n\t\tswitch (option) {\n\t\tcase Option::All: return tr::lng_manage_peer_reactions_all_about();\n\t\tcase Option::Some: return tr::lng_manage_peer_reactions_some_about();\n\t\tcase Option::None: return tr::lng_manage_peer_reactions_none_about();\n\t\t}\n\t\tUnexpected(\"Option value in EditAllowedReactionsBox.\");\n\t};\n\tUi::AddSkip(container);\n\tUi::AddDividerText(\n\t\tcontainer,\n\t\t(isGroup\n\t\t\t? (state->option.value()\n\t\t\t\t| rpl::map(about)\n\t\t\t\t| rpl::flatten_latest())\n\t\t\t: tr::lng_manage_peer_reactions_about_channel()));\n\n\tconst auto wrap = enabled ? nullptr : container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tif (wrap) {\n\t\twrap->toggleOn(state->option.value(\n\t\t) | rpl::map(_1 == Option::Some) | rpl::before_next([=](bool some) {\n\t\t\tif (!some) {\n\t\t\t\tstate->selectorState = SelectorState::Hidden;\n\t\t\t}\n\t\t}) | rpl::after_next([=](bool some) {\n\t\t\tif (some) {\n\t\t\t\tstate->selectorState = SelectorState::Active;\n\t\t\t}\n\t\t}));\n\t\twrap->finishAnimating();\n\t}\n\tconst auto reactions = wrap ? wrap->entity() : container.get();\n\n\tUi::AddSkip(reactions);\n\n\tconst auto all = args.list;\n\tauto selected = (allowed.type != AllowedReactionsType::Some)\n\t\t? std::vector<Data::ReactionId>()\n\t\t: allowed.some;\n\tif (allowed.paidEnabled) {\n\t\tselected.insert(begin(selected), Data::ReactionId::Paid());\n\t}\n\tconst auto changed = [=](\n\t\t\tstd::vector<Data::ReactionId> chosen,\n\t\t\tbool hardLimitHit) {\n\t\tstate->selected = std::move(chosen);\n\t\tstate->customCount = ranges::count_if(\n\t\t\tstate->selected,\n\t\t\t&Data::ReactionId::custom);\n\t\tstate->paidEnabled = ranges::contains(\n\t\t\tstate->selected,\n\t\t\tData::ReactionId::Paid());\n\t\tif (hardLimitHit) {\n\t\t\tbox->uiShow()->showToast(\n\t\t\t\ttr::lng_manage_peer_reactions_limit(tr::now));\n\t\t}\n\t};\n\tchanged(\n\t\t!selected.empty()\n\t\t\t? std::move(selected)\n\t\t\t: !isGroup\n\t\t\t? CollectAvailableReactions(\n\t\t\t\t&args.navigation->parentController()->session())\n\t\t\t: DefaultSelected(),\n\t\t{});\n\tUi::AddSubsectionTitle(\n\t\treactions,\n\t\tenabled\n\t\t\t? tr::lng_manage_peer_reactions_available()\n\t\t\t: tr::lng_manage_peer_reactions_some_title(),\n\t\tst::manageGroupReactionsFieldPadding);\n\treactions->add(AddReactionsSelector(reactions, {\n\t\t.outer = box->getDelegate()->outerContainer(),\n\t\t.controller = args.navigation->parentController(),\n\t\t.title = tr::lng_manage_peer_reactions_available_ph(),\n\t\t.list = all,\n\t\t.selected = state->selected,\n\t\t.paid = state->paidEnabled.value(),\n\t\t.callback = changed,\n\t\t.stateValue = state->selectorState.value(),\n\t\t.customAllowed = args.allowedCustomReactions,\n\t\t.customHardLimit = args.customReactionsHardLimit,\n\t\t.all = !args.isGroup,\n\t}), st::boxRowPadding);\n\n\tbox->setFocusCallback([=] {\n\t\tif (state->option.current() == Option::Some) {\n\t\t\tstate->selectorState.force_assign(SelectorState::Active);\n\t\t}\n\t});\n\n\tconst auto reactionsLimit = container->lifetime().make_state<int>(0);\n\tif (!isGroup) {\n\t\tAddReactionsText(\n\t\t\tcontainer,\n\t\t\targs.navigation,\n\t\t\targs.allowedCustomReactions,\n\t\t\tstate->customCount.value(),\n\t\t\targs.askForBoosts);\n\n\t\tconst auto session = &args.navigation->parentController()->session();\n\n\t\tconst auto wrap = container->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\t\tconst auto max = Data::UniqueReactionsLimit(session->user());\n\t\tconst auto inactiveColor = std::make_optional(st::windowSubTextFg->c);\n\t\tconst auto activeColor = std::make_optional(\n\t\t\tst::windowActiveTextFg->c);\n\t\tconst auto inner = wrap->entity();\n\t\tUi::AddSkip(inner);\n\t\tUi::AddSubsectionTitle(\n\t\t\tinner,\n\t\t\ttr::lng_manage_peer_reactions_max_title(),\n\t\t\tst::manageGroupReactionsMaxSubtitlePadding);\n\t\tUi::AddSkip(inner);\n\t\tconst auto line = inner->add(\n\t\t\tobject_ptr<Ui::RpWidget>(inner),\n\t\t\tst::boxRowPadding);\n\t\tUi::AddSkip(inner);\n\t\tUi::AddSkip(inner);\n\t\tconst auto left = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tline,\n\t\t\tQString::number(1),\n\t\t\tst::defaultFlatLabel);\n\t\tconst auto center = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tline,\n\t\t\tst::defaultFlatLabel);\n\t\tconst auto right = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tline,\n\t\t\tQString::number(max),\n\t\t\tst::defaultFlatLabel);\n\t\tconst auto slider = Ui::CreateChild<Ui::MediaSlider>(\n\t\t\tline,\n\t\t\tst::settingsScale);\n\t\trpl::combine(\n\t\t\tline->sizeValue(),\n\t\t\tleft->sizeValue(),\n\t\t\tcenter->sizeValue(),\n\t\t\tright->sizeValue()\n\t\t) | rpl::on_next([=](\n\t\t\t\tconst QSize &s,\n\t\t\t\tconst QSize &leftSize,\n\t\t\t\tconst QSize &centerSize,\n\t\t\t\tconst QSize &rightSize) {\n\t\t\tconst auto sliderHeight = st::settingsScale.seekSize.height();\n\t\t\tline->resize(\n\t\t\t\tline->width(),\n\t\t\t\tleftSize.height() + sliderHeight * 2);\n\t\t\t{\n\t\t\t\tconst auto r = line->rect();\n\t\t\t\tslider->setGeometry(\n\t\t\t\t\t0,\n\t\t\t\t\tr.height() - sliderHeight * 1.5,\n\t\t\t\t\tr.width(),\n\t\t\t\t\tsliderHeight);\n\t\t\t}\n\t\t\tleft->moveToLeft(0, 0);\n\t\t\tright->moveToRight(0, 0);\n\t\t\tcenter->moveToLeft((s.width() - centerSize.width()) / 2, 0);\n\t\t}, line->lifetime());\n\n\t\tconst auto updateLabels = [=](int limit) {\n\t\t\tleft->setTextColorOverride((limit <= 1)\n\t\t\t\t? activeColor\n\t\t\t\t: inactiveColor);\n\n\t\t\tcenter->setText(tr::lng_manage_peer_reactions_max_slider(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tlimit));\n\t\t\tcenter->setTextColorOverride(activeColor);\n\n\t\t\tright->setTextColorOverride((limit >= max)\n\t\t\t\t? activeColor\n\t\t\t\t: inactiveColor);\n\n\t\t\t(*reactionsLimit) = limit;\n\t\t};\n\t\tconst auto current = args.allowed.maxCount\n\t\t\t? std::clamp(1, args.allowed.maxCount, max)\n\t\t\t: max / 2;\n\t\tslider->setPseudoDiscrete(\n\t\t\tmax,\n\t\t\t[=](int index) { return index + 1; },\n\t\t\tcurrent,\n\t\t\tupdateLabels,\n\t\t\tupdateLabels);\n\t\tupdateLabels(current);\n\n\t\twrap->toggleOn(rpl::single(\n\t\t\toptionInitial != Option::None\n\t\t) | rpl::then(\n\t\t\tstate->selectorState.value(\n\t\t\t) | rpl::map(rpl::mappers::_1 == SelectorState::Active)));\n\n\t\tUi::AddDividerText(inner, tr::lng_manage_peer_reactions_max_about());\n\n\t\tUi::AddSkip(inner);\n\t\tconst auto paid = inner->add(object_ptr<Ui::SettingsButton>(\n\t\t\tinner,\n\t\t\ttr::lng_manage_peer_reactions_paid(),\n\t\t\tst::manageGroupNoIconButton.button));\n\t\tpaid->toggleOn(state->paidEnabled.value());\n\t\tpaid->toggledValue(\n\t\t) | rpl::on_next([=](bool value) {\n\t\t\tstate->paidEnabled = value;\n\t\t}, paid->lifetime());\n\t\tUi::AddSkip(inner);\n\n\t\tUi::AddDividerText(\n\t\t\tinner,\n\t\t\ttr::lng_manage_peer_reactions_paid_about(\n\t\t\t\tlt_link,\n\t\t\t\ttr::lng_manage_peer_reactions_paid_link([=](QString text) {\n\t\t\t\t\treturn tr::link(\n\t\t\t\t\t\ttext,\n\t\t\t\t\t\tu\"https://telegram.org/tos/stars\"_q);\n\t\t\t\t}),\n\t\t\t\ttr::marked));\n\t}\n\tconst auto collect = [=] {\n\t\tauto result = AllowedReactions();\n\t\tresult.maxCount = (*reactionsLimit);\n\t\tif (isGroup\n\t\t\t? (state->option.current() == Option::Some)\n\t\t\t: (enabled->toggled())) {\n\t\t\tresult.some = state->selected;\n\t\t}\n\t\tif (!isGroup && enabled->toggled())\t{\n\t\t\tresult.paidEnabled = state->paidEnabled.current();\n\t\t}\n\t\tauto some = result.some;\n\t\tauto simple = all | ranges::views::transform(\n\t\t\t&Data::Reaction::id\n\t\t) | ranges::to_vector;\n\t\tranges::sort(some);\n\t\tranges::sort(simple);\n\t\tresult.type = isGroup\n\t\t\t? (state->option.current() != Option::All\n\t\t\t\t? AllowedReactionsType::Some\n\t\t\t\t: AllowedReactionsType::All)\n\t\t\t: (some == simple)\n\t\t\t? AllowedReactionsType::Default\n\t\t\t: AllowedReactionsType::Some;\n\t\treturn result;\n\t};\n\n\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\tconst auto result = collect();\n\t\tif (!isGroup) {\n\t\t\tconst auto custom = ranges::count_if(\n\t\t\t\tresult.some,\n\t\t\t\t&Data::ReactionId::custom);\n\t\t\tif (custom > args.allowedCustomReactions) {\n\t\t\t\targs.askForBoosts(custom);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tbox->closeBox();\n\t\targs.save(result);\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nvoid SaveAllowedReactions(\n\t\tnot_null<PeerData*> peer,\n\t\tconst Data::AllowedReactions &allowed) {\n\tauto ids = allowed.some | ranges::views::transform(\n\t\tData::ReactionToMTP\n\t) | ranges::to<QVector<MTPReaction>>;\n\n\tusing Flag = MTPmessages_SetChatAvailableReactions::Flag;\n\tusing Type = Data::AllowedReactionsType;\n\tconst auto updated = (allowed.type != Type::Some)\n\t\t? MTP_chatReactionsAll(MTP_flags((allowed.type == Type::Default)\n\t\t\t? MTPDchatReactionsAll::Flag(0)\n\t\t\t: MTPDchatReactionsAll::Flag::f_allow_custom))\n\t\t: allowed.some.empty()\n\t\t? MTP_chatReactionsNone()\n\t\t: MTP_chatReactionsSome(MTP_vector<MTPReaction>(ids));\n\tconst auto editPaidEnabled = peer->isBroadcast();\n\tconst auto paidEnabled = editPaidEnabled && allowed.paidEnabled;\n\tconst auto maxCount = allowed.maxCount;\n\tpeer->session().api().request(MTPmessages_SetChatAvailableReactions(\n\t\tMTP_flags(Flag()\n\t\t\t| (maxCount ? Flag::f_reactions_limit : Flag())\n\t\t\t| (editPaidEnabled ? Flag::f_paid_enabled : Flag())),\n\t\tpeer->input(),\n\t\tupdated,\n\t\tMTP_int(maxCount),\n\t\tMTP_bool(paidEnabled)\n\t)).done([=](const MTPUpdates &result) {\n\t\tpeer->session().api().applyUpdates(result);\n\t\tauto parsed = Data::Parse(updated, maxCount, paidEnabled);\n\t\tif (const auto chat = peer->asChat()) {\n\t\t\tchat->setAllowedReactions(parsed);\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\tchannel->setAllowedReactions(parsed);\n\t\t} else {\n\t\t\tUnexpected(\"Invalid peer type in SaveAllowedReactions.\");\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (error.type() == u\"REACTION_INVALID\"_q) {\n\t\t\tpeer->updateFullForced();\n\t\t\tpeer->owner().reactions().refreshDefault();\n\t\t}\n\t}).send();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_reactions.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_peer.h\"\n\nnamespace Data {\nstruct Reaction;\nstruct AllowedReactions;\n} // namespace Data\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nstruct EditAllowedReactionsArgs {\n\tnot_null<Window::SessionNavigation*> navigation;\n\tint allowedCustomReactions = 0;\n\tint customReactionsHardLimit = 0;\n\tbool isGroup = false;\n\tstd::vector<Data::Reaction> list;\n\tData::AllowedReactions allowed;\n\tFn<void(int required)> askForBoosts;\n\tFn<void(const Data::AllowedReactions &)> save;\n};\n\nvoid EditAllowedReactionsBox(\n\tnot_null<Ui::GenericBox*> box,\n\tEditAllowedReactionsArgs &&args);\n\nvoid SaveAllowedReactions(\n\tnot_null<PeerData*> peer,\n\tconst Data::AllowedReactions &allowed);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_peer_requests_box.h\"\n\n#include \"api/api_invite_links.h\"\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/peers/edit_participants_box.h\" // SubscribeToMigration\n#include \"boxes/peers/edit_peer_invite_link.h\" // PrepareRequestedRowStatus\n#include \"boxes/peers/edit_peer_requests_box.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/view/history_view_requests_bar.h\" // kRecentRequestsLimit\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"info/requests_list/info_requests_list_widget.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/sender.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/round_rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace {\n\nconstexpr auto kFirstPageCount = 16;\nconstexpr auto kPerPage = 200;\nconstexpr auto kServerSearchDelay = crl::time(1000);\nconstexpr auto kAcceptButton = 1;\nconstexpr auto kRejectButton = 2;\n\nclass RowDelegate {\npublic:\n\t[[nodiscard]] virtual QSize rowAcceptButtonSize() = 0;\n\t[[nodiscard]] virtual QSize rowRejectButtonSize() = 0;\n\tvirtual void rowPaintAccept(\n\t\tPainter &p,\n\t\tQRect geometry,\n\t\tstd::unique_ptr<Ui::RippleAnimation> &ripple,\n\t\tint outerWidth,\n\t\tbool over) = 0;\n\tvirtual void rowPaintReject(\n\t\tPainter &p,\n\t\tQRect geometry,\n\t\tstd::unique_ptr<Ui::RippleAnimation> &ripple,\n\t\tint outerWidth,\n\t\tbool over) = 0;\n};\n\nclass Row final : public PeerListRow {\npublic:\n\tRow(\n\t\tnot_null<RowDelegate*> delegate,\n\t\tnot_null<UserData*> user,\n\t\tTimeId date);\n\n\tint elementsCount() const override;\n\tQRect elementGeometry(int element, int outerWidth) const override;\n\tbool elementDisabled(int element) const override;\n\tbool elementOnlySelect(int element) const override;\n\tvoid elementAddRipple(\n\t\tint element,\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) override;\n\tvoid elementsStopLastRipple() override;\n\tvoid elementsPaint(\n\t\tPainter &p,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tint selectedElement) override;\n\nprivate:\n\tconst not_null<RowDelegate*> _delegate;\n\tstd::unique_ptr<Ui::RippleAnimation> _acceptRipple;\n\tstd::unique_ptr<Ui::RippleAnimation> _rejectRipple;\n\n};\n\nRow::Row(\n\tnot_null<RowDelegate*> delegate,\n\tnot_null<UserData*> user,\n\tTimeId date)\n: PeerListRow(user)\n, _delegate(delegate) {\n\tsetCustomStatus(PrepareRequestedRowStatus(date));\n}\n\nint Row::elementsCount() const {\n\treturn 2;\n}\n\nQRect Row::elementGeometry(int element, int outerWidth) const {\n\tswitch (element) {\n\tcase kAcceptButton: {\n\t\tconst auto size = _delegate->rowAcceptButtonSize();\n\t\treturn QRect(st::requestAcceptPosition, size);\n\t} break;\n\tcase kRejectButton: {\n\t\tconst auto accept = _delegate->rowAcceptButtonSize();\n\t\tconst auto size = _delegate->rowRejectButtonSize();\n\t\treturn QRect(\n\t\t\t(st::requestAcceptPosition\n\t\t\t\t+ QPoint(accept.width() + st::requestButtonsSkip, 0)),\n\t\t\tsize);\n\t} break;\n\t}\n\treturn QRect();\n}\n\nbool Row::elementDisabled(int element) const {\n\treturn false;\n}\n\nbool Row::elementOnlySelect(int element) const {\n\treturn true;\n}\n\nvoid Row::elementAddRipple(\n\t\tint element,\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) {\n\tconst auto pointer = (element == kAcceptButton)\n\t\t? &_acceptRipple\n\t\t: (element == kRejectButton)\n\t\t? &_rejectRipple\n\t\t: nullptr;\n\tif (!pointer) {\n\t\treturn;\n\t}\n\tauto &ripple = *pointer;\n\tif (!ripple) {\n\t\tauto mask = Ui::RippleAnimation::RoundRectMask(\n\t\t\t(element == kAcceptButton\n\t\t\t\t? _delegate->rowAcceptButtonSize()\n\t\t\t\t: _delegate->rowRejectButtonSize()),\n\t\t\tst::buttonRadius);\n\t\tripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t(element == kAcceptButton\n\t\t\t\t? st::requestsAcceptButton.ripple\n\t\t\t\t: st::requestsRejectButton.ripple),\n\t\t\tstd::move(mask),\n\t\t\tstd::move(updateCallback));\n\t}\n\tripple->add(point);\n}\n\nvoid Row::elementsStopLastRipple() {\n\tif (_acceptRipple) {\n\t\t_acceptRipple->lastStop();\n\t}\n\tif (_rejectRipple) {\n\t\t_rejectRipple->lastStop();\n\t}\n}\n\nvoid Row::elementsPaint(\n\t\tPainter &p,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tint selectedElement) {\n\tconst auto accept = elementGeometry(kAcceptButton, outerWidth);\n\tconst auto reject = elementGeometry(kRejectButton, outerWidth);\n\n\tconst auto over = [&](int element) {\n\t\treturn (selectedElement == element);\n\t};\n\t_delegate->rowPaintAccept(\n\t\tp,\n\t\taccept,\n\t\t_acceptRipple,\n\t\touterWidth,\n\t\tover(kAcceptButton));\n\t_delegate->rowPaintReject(\n\t\tp,\n\t\treject,\n\t\t_rejectRipple,\n\t\touterWidth,\n\t\tover(kRejectButton));\n}\n\n} // namespace\n\nclass RequestsBoxController::RowHelper final : public RowDelegate {\npublic:\n\texplicit RowHelper(bool isGroup);\n\n\t[[nodiscard]] QSize rowAcceptButtonSize() override;\n\t[[nodiscard]] QSize rowRejectButtonSize() override;\n\tvoid rowPaintAccept(\n\t\tPainter &p,\n\t\tQRect geometry,\n\t\tstd::unique_ptr<Ui::RippleAnimation> &ripple,\n\t\tint outerWidth,\n\t\tbool over) override;\n\tvoid rowPaintReject(\n\t\tPainter &p,\n\t\tQRect geometry,\n\t\tstd::unique_ptr<Ui::RippleAnimation> &ripple,\n\t\tint outerWidth,\n\t\tbool over) override;\n\nprivate:\n\tvoid paintButton(\n\t\tPainter &p,\n\t\tQRect geometry,\n\t\tconst style::RoundButton &st,\n\t\tconst Ui::RoundRect &rect,\n\t\tconst Ui::RoundRect &rectOver,\n\t\tstd::unique_ptr<Ui::RippleAnimation> &ripple,\n\t\tconst QString &text,\n\t\tint textWidth,\n\t\tint outerWidth,\n\t\tbool over);\n\n\tUi::RoundRect _acceptRect;\n\tUi::RoundRect _acceptRectOver;\n\tUi::RoundRect _rejectRect;\n\tUi::RoundRect _rejectRectOver;\n\tQString _acceptText;\n\tQString _rejectText;\n\tint _acceptTextWidth = 0;\n\tint _rejectTextWidth = 0;\n\n};\n\nRequestsBoxController::RowHelper::RowHelper(bool isGroup)\n: _acceptRect(st::buttonRadius, st::requestsAcceptButton.textBg)\n, _acceptRectOver(st::buttonRadius, st::requestsAcceptButton.textBgOver)\n, _rejectRect(st::buttonRadius, st::requestsRejectButton.textBg)\n, _rejectRectOver(st::buttonRadius, st::requestsRejectButton.textBgOver)\n, _acceptText(isGroup\n\t? tr::lng_group_requests_add(tr::now)\n\t: tr::lng_group_requests_add_channel(tr::now))\n, _rejectText(tr::lng_group_requests_dismiss(tr::now))\n, _acceptTextWidth(st::requestsAcceptButton.style.font->width(_acceptText))\n, _rejectTextWidth(st::requestsRejectButton.style.font->width(_rejectText)) {\n}\n\nRequestsBoxController::RequestsBoxController(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer)\n: PeerListController(CreateSearchController(peer))\n, _navigation(navigation)\n, _helper(std::make_unique<RowHelper>(!peer->isBroadcast()))\n, _peer(peer)\n, _api(&_peer->session().mtp()) {\n\tsetStyleOverrides(&st::requestsBoxList);\n\tsubscribeToMigration();\n}\n\nRequestsBoxController::~RequestsBoxController() = default;\n\nvoid RequestsBoxController::Start(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer) {\n\tnavigation->showSection(\n\t\tstd::make_shared<Info::Memento>(\n\t\t\tpeer->migrateToOrMe(),\n\t\t\tInfo::Section::Type::RequestsList));\n}\n\nMain::Session &RequestsBoxController::session() const {\n\treturn _peer->session();\n}\n\nauto RequestsBoxController::CreateSearchController(not_null<PeerData*> peer)\n-> std::unique_ptr<PeerListSearchController> {\n\treturn std::make_unique<RequestsBoxSearchController>(peer);\n}\n\nstd::unique_ptr<PeerListRow> RequestsBoxController::createSearchRow(\n\t\tnot_null<PeerData*> peer) {\n\tif (const auto user = peer->asUser()) {\n\t\treturn createRow(user);\n\t}\n\treturn nullptr;\n}\n\nstd::unique_ptr<PeerListRow> RequestsBoxController::createRestoredRow(\n\t\tnot_null<PeerData*> peer) {\n\tif (const auto user = peer->asUser()) {\n\t\treturn createRow(user, _dates[user]);\n\t}\n\treturn nullptr;\n}\n\nauto RequestsBoxController::saveState() const\n-> std::unique_ptr<PeerListState> {\n\tauto result = PeerListController::saveState();\n\n\tauto my = std::make_unique<SavedState>();\n\tmy->dates = _dates;\n\tmy->offsetDate = _offsetDate;\n\tmy->offsetUser = _offsetUser;\n\tmy->allLoaded = _allLoaded;\n\tmy->wasLoading = (_loadRequestId != 0);\n\tif (const auto search = searchController()) {\n\t\tmy->searchState = search->saveState();\n\t}\n\tresult->controllerState = std::move(my);\n\treturn result;\n}\n\nvoid RequestsBoxController::restoreState(\n\t\tstd::unique_ptr<PeerListState> state) {\n\tauto typeErasedState = state\n\t\t? state->controllerState.get()\n\t\t: nullptr;\n\tif (const auto my = dynamic_cast<SavedState*>(typeErasedState)) {\n\t\tif (const auto requestId = base::take(_loadRequestId)) {\n\t\t\t_api.request(requestId).cancel();\n\t\t}\n\t\t_dates = std::move(my->dates);\n\t\t_offsetDate = my->offsetDate;\n\t\t_offsetUser = my->offsetUser;\n\t\t_allLoaded = my->allLoaded;\n\t\tif (const auto search = searchController()) {\n\t\t\tsearch->restoreState(std::move(my->searchState));\n\t\t}\n\t\tif (my->wasLoading) {\n\t\t\tloadMoreRows();\n\t\t}\n\t\tPeerListController::restoreState(std::move(state));\n\t\tif (delegate()->peerListFullRowsCount() || _allLoaded) {\n\t\t\trefreshDescription();\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t}\n}\n\nvoid RequestsBoxController::prepare() {\n\tdelegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);\n\tdelegate()->peerListSetTitle(_peer->isBroadcast()\n\t\t? tr::lng_manage_peer_requests_channel()\n\t\t: tr::lng_manage_peer_requests());\n\tsetDescriptionText(tr::lng_contacts_loading(tr::now));\n\tsetSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now));\n\tloadMoreRows();\n}\n\nvoid RequestsBoxController::loadMoreRows() {\n\tif (searchController() && searchController()->loadMoreRows()) {\n\t\treturn;\n\t} else if (_loadRequestId || _allLoaded) {\n\t\treturn;\n\t}\n\n\t// First query is small and fast, next loads a lot of rows.\n\tconst auto limit = _offsetDate ? kPerPage : kFirstPageCount;\n\tusing Flag = MTPmessages_GetChatInviteImporters::Flag;\n\t_loadRequestId = _api.request(MTPmessages_GetChatInviteImporters(\n\t\tMTP_flags(Flag::f_requested),\n\t\t_peer->input(),\n\t\tMTPstring(), // link\n\t\tMTPstring(), // q\n\t\tMTP_int(_offsetDate),\n\t\t_offsetUser ? _offsetUser->inputUser() : MTP_inputUserEmpty(),\n\t\tMTP_int(limit)\n\t)).done([=](const MTPmessages_ChatInviteImporters &result) {\n\t\tconst auto firstLoad = !_offsetDate;\n\t\t_loadRequestId = 0;\n\n\t\tresult.match([&](const MTPDmessages_chatInviteImporters &data) {\n\t\t\tsession().data().processUsers(data.vusers());\n\t\t\tconst auto &importers = data.vimporters().v;\n\t\t\tauto &owner = _peer->owner();\n\t\t\tfor (const auto &importer : importers) {\n\t\t\t\timporter.match([&](const MTPDchatInviteImporter &data) {\n\t\t\t\t\t_offsetDate = data.vdate().v;\n\t\t\t\t\t_offsetUser = owner.user(data.vuser_id());\n\t\t\t\t\tappendRow(_offsetUser, _offsetDate);\n\t\t\t\t});\n\t\t\t}\n\t\t\t// To be sure - wait for a whole empty result list.\n\t\t\t_allLoaded = importers.isEmpty();\n\t\t});\n\n\t\tif (_allLoaded\n\t\t\t|| (firstLoad && delegate()->peerListFullRowsCount() > 0)) {\n\t\t\trefreshDescription();\n\t\t}\n\t\tdelegate()->peerListRefreshRows();\n\t}).fail([=] {\n\t\t_loadRequestId = 0;\n\t\t_allLoaded = true;\n\t}).send();\n}\n\nvoid RequestsBoxController::refreshDescription() {\n\tsetDescriptionText((delegate()->peerListFullRowsCount() > 0)\n\t\t? QString()\n\t\t: _peer->isBroadcast()\n\t\t? tr::lng_group_requests_none_channel(tr::now)\n\t\t: tr::lng_group_requests_none(tr::now));\n}\n\nvoid RequestsBoxController::rowClicked(not_null<PeerListRow*> row) {\n\t_navigation->showPeerInfo(row->peer());\n}\n\nvoid RequestsBoxController::rowElementClicked(\n\t\tnot_null<PeerListRow*> row,\n\t\tint element) {\n\tprocessRequest(row->peer()->asUser(), (element == kAcceptButton));\n}\n\nvoid RequestsBoxController::processRequest(\n\t\tnot_null<UserData*> user,\n\t\tbool approved) {\n\tconst auto remove = [=] {\n\t\tif (const auto row = delegate()->peerListFindRow(user->id.value)) {\n\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t\trefreshDescription();\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t\tstatic_cast<RequestsBoxSearchController*>(\n\t\t\tsearchController())->removeFromCache(user);\n\t};\n\tconst auto done = crl::guard(this, [=] {\n\t\tremove();\n\t\tif (approved) {\n\t\t\tdelegate()->peerListUiShow()->showToast((_peer->isBroadcast()\n\t\t\t\t? tr::lng_group_requests_was_added_channel\n\t\t\t\t: tr::lng_group_requests_was_added)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::bold(user->name()),\n\t\t\t\t\ttr::marked));\n\t\t}\n\t});\n\tconst auto fail = crl::guard(this, remove);\n\tsession().api().inviteLinks().processRequest(\n\t\t_peer,\n\t\tQString(), // link\n\t\tuser,\n\t\tapproved,\n\t\tdone,\n\t\tfail);\n}\n\nvoid RequestsBoxController::appendRow(\n\t\tnot_null<UserData*> user,\n\t\tTimeId date) {\n\tif (!delegate()->peerListFindRow(user->id.value)) {\n\t\t_dates.emplace(user, date);\n\t\tif (auto row = createRow(user, date)) {\n\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t\tsetDescriptionText(QString());\n\t\t}\n\t}\n}\n\nQSize RequestsBoxController::RowHelper::rowAcceptButtonSize() {\n\tconst auto &st = st::requestsAcceptButton;\n\treturn {\n\t\t (st.width <= 0) ? (_acceptTextWidth - st.width) : st.width,\n\t\t st.height,\n\t};\n}\n\nQSize RequestsBoxController::RowHelper::rowRejectButtonSize() {\n\tconst auto &st = st::requestsRejectButton;\n\treturn {\n\t\t (st.width <= 0) ? (_rejectTextWidth - st.width) : st.width,\n\t\t st.height,\n\t};\n}\n\nvoid RequestsBoxController::RowHelper::rowPaintAccept(\n\t\tPainter &p,\n\t\tQRect geometry,\n\t\tstd::unique_ptr<Ui::RippleAnimation> &ripple,\n\t\tint outerWidth,\n\t\tbool over) {\n\tpaintButton(\n\t\tp,\n\t\tgeometry,\n\t\tst::requestsAcceptButton,\n\t\t_acceptRect,\n\t\t_acceptRectOver,\n\t\tripple,\n\t\t_acceptText,\n\t\t_acceptTextWidth,\n\t\touterWidth,\n\t\tover);\n}\n\nvoid RequestsBoxController::RowHelper::rowPaintReject(\n\t\tPainter &p,\n\t\tQRect geometry,\n\t\tstd::unique_ptr<Ui::RippleAnimation> &ripple,\n\t\tint outerWidth,\n\t\tbool over) {\n\tpaintButton(\n\t\tp,\n\t\tgeometry,\n\t\tst::requestsRejectButton,\n\t\t_rejectRect,\n\t\t_rejectRectOver,\n\t\tripple,\n\t\t_rejectText,\n\t\t_rejectTextWidth,\n\t\touterWidth,\n\t\tover);\n}\n\nvoid RequestsBoxController::RowHelper::paintButton(\n\t\tPainter &p,\n\t\tQRect geometry,\n\t\tconst style::RoundButton &st,\n\t\tconst Ui::RoundRect &rect,\n\t\tconst Ui::RoundRect &rectOver,\n\t\tstd::unique_ptr<Ui::RippleAnimation> &ripple,\n\t\tconst QString &text,\n\t\tint textWidth,\n\t\tint outerWidth,\n\t\tbool over) {\n\trect.paint(p, geometry);\n\tif (over) {\n\t\trectOver.paint(p, geometry);\n\t}\n\tif (ripple) {\n\t\tripple->paint(p, geometry.x(), geometry.y(), outerWidth);\n\t\tif (ripple->empty()) {\n\t\t\tripple = nullptr;\n\t\t}\n\t}\n\n\tconst auto textLeft = geometry.x()\n\t\t+ ((geometry.width() - textWidth) / 2);\n\tconst auto textTop = geometry.y() + st.textTop;\n\tp.setFont(st.style.font);\n\tp.setPen(over ? st.textFgOver : st.textFg);\n\tp.drawTextLeft(textLeft, textTop, outerWidth, text);\n}\n\nstd::unique_ptr<PeerListRow> RequestsBoxController::createRow(\n\t\tnot_null<UserData*> user,\n\t\tTimeId date) {\n\tif (!date) {\n\t\tconst auto search = static_cast<RequestsBoxSearchController*>(\n\t\t\tsearchController());\n\t\tdate = search->dateForUser(user);\n\t\t_dates.emplace(user, date);\n\t}\n\treturn std::make_unique<Row>(_helper.get(), user, date);\n}\n\nvoid RequestsBoxController::subscribeToMigration() {\n\tconst auto chat = _peer->asChat();\n\tif (!chat) {\n\t\treturn;\n\t}\n\tSubscribeToMigration(\n\t\tchat,\n\t\tlifetime(),\n\t\t[=](not_null<ChannelData*> channel) { migrate(chat, channel); });\n}\n\nvoid RequestsBoxController::migrate(\n\t\tnot_null<ChatData*> chat,\n\t\tnot_null<ChannelData*> channel) {\n\t_peer = channel;\n}\n\nRequestsBoxSearchController::RequestsBoxSearchController(\n\tnot_null<PeerData*> peer)\n: _peer(peer)\n, _api(&_peer->session().mtp()) {\n\t_timer.setCallback([=] { searchOnServer(); });\n}\n\nvoid RequestsBoxSearchController::searchQuery(const QString &query) {\n\tif (_query != query) {\n\t\t_query = query;\n\t\t_offsetDate = 0;\n\t\t_offsetUser = nullptr;\n\t\t_requestId = 0;\n\t\t_allLoaded = false;\n\t\tif (!_query.isEmpty() && !searchInCache()) {\n\t\t\t_timer.callOnce(kServerSearchDelay);\n\t\t} else {\n\t\t\t_timer.cancel();\n\t\t}\n\t}\n}\n\nvoid RequestsBoxSearchController::searchOnServer() {\n\tExpects(!_query.isEmpty());\n\n\tloadMoreRows();\n}\n\nbool RequestsBoxSearchController::isLoading() {\n\treturn _timer.isActive() || _requestId;\n}\n\nvoid RequestsBoxSearchController::removeFromCache(not_null<UserData*> user) {\n\tfor (auto &entry : _cache) {\n\t\tauto &items = entry.second.items;\n\t\tconst auto j = ranges::remove(items, user, &Item::user);\n\t\tif (j != end(items)) {\n\t\t\tentry.second.requestedCount -= (end(items) - j);\n\t\t\titems.erase(j, end(items));\n\t\t}\n\t}\n}\n\nTimeId RequestsBoxSearchController::dateForUser(not_null<UserData*> user) {\n\tif (const auto i = _dates.find(user); i != end(_dates)) {\n\t\treturn i->second;\n\t}\n\treturn {};\n}\n\nauto RequestsBoxSearchController::saveState() const\n-> std::unique_ptr<SavedStateBase> {\n\tauto result = std::make_unique<SavedState>();\n\tresult->query = _query;\n\tresult->offsetDate = _offsetDate;\n\tresult->offsetUser = _offsetUser;\n\tresult->allLoaded = _allLoaded;\n\tresult->wasLoading = (_requestId != 0);\n\treturn result;\n}\n\nvoid RequestsBoxSearchController::restoreState(\n\t\tstd::unique_ptr<SavedStateBase> state) {\n\tif (auto my = dynamic_cast<SavedState*>(state.get())) {\n\t\tif (auto requestId = base::take(_requestId)) {\n\t\t\t_api.request(requestId).cancel();\n\t\t}\n\t\t_cache.clear();\n\t\t_queries.clear();\n\n\t\t_allLoaded = my->allLoaded;\n\t\t_offsetDate = my->offsetDate;\n\t\t_offsetUser = my->offsetUser;\n\t\t_query = my->query;\n\t\tif (my->wasLoading) {\n\t\t\tsearchOnServer();\n\t\t}\n\t}\n}\n\nbool RequestsBoxSearchController::searchInCache() {\n\tconst auto i = _cache.find(_query);\n\tif (i != _cache.cend()) {\n\t\t_requestId = 0;\n\t\tsearchDone(\n\t\t\t_requestId,\n\t\t\ti->second.items,\n\t\t\ti->second.requestedCount);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool RequestsBoxSearchController::loadMoreRows() {\n\tif (_query.isEmpty()) {\n\t\treturn false;\n\t} else if (_allLoaded || isLoading()) {\n\t\treturn true;\n\t}\n\t// For search we request a lot of rows from the first query.\n\t// (because we've waited for search request by timer already,\n\t// so we don't expect it to be fast, but we want to fill cache).\n\tconst auto limit = kPerPage;\n\tusing Flag = MTPmessages_GetChatInviteImporters::Flag;\n\t_requestId = _api.request(MTPmessages_GetChatInviteImporters(\n\t\tMTP_flags(Flag::f_requested | Flag::f_q),\n\t\t_peer->input(),\n\t\tMTPstring(), // link\n\t\tMTP_string(_query),\n\t\tMTP_int(_offsetDate),\n\t\t_offsetUser ? _offsetUser->inputUser() : MTP_inputUserEmpty(),\n\t\tMTP_int(limit)\n\t)).done([=](\n\t\t\tconst MTPmessages_ChatInviteImporters &result,\n\t\t\tmtpRequestId requestId) {\n\t\tauto items = std::vector<Item>();\n\t\tresult.match([&](const MTPDmessages_chatInviteImporters &data) {\n\t\t\tconst auto &importers = data.vimporters().v;\n\t\t\tauto &owner = _peer->owner();\n\t\t\towner.processUsers(data.vusers());\n\t\t\titems.reserve(importers.size());\n\t\t\tfor (const auto &importer : importers) {\n\t\t\t\timporter.match([&](const MTPDchatInviteImporter &data) {\n\t\t\t\t\titems.push_back({\n\t\t\t\t\t\towner.user(data.vuser_id()),\n\t\t\t\t\t\tdata.vdate().v,\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t\tsearchDone(requestId, items, limit);\n\n\t\tauto it = _queries.find(requestId);\n\t\tif (it != _queries.cend()) {\n\t\t\tconst auto &query = it->second.text;\n\t\t\tif (it->second.offsetDate == 0) {\n\t\t\t\tauto &entry = _cache[query];\n\t\t\t\tentry.items = std::move(items);\n\t\t\t\tentry.requestedCount = limit;\n\t\t\t}\n\t\t\t_queries.erase(it);\n\t\t}\n\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\tif (_requestId == requestId) {\n\t\t\t_requestId = 0;\n\t\t\t_allLoaded = true;\n\t\t\tdelegate()->peerListSearchRefreshRows();\n\t\t}\n\t}).send();\n\n\tauto entry = Query();\n\tentry.text = _query;\n\tentry.offsetDate = _offsetDate;\n\t_queries.emplace(_requestId, entry);\n\treturn true;\n}\n\nvoid RequestsBoxSearchController::searchDone(\n\t\tmtpRequestId requestId,\n\t\tconst std::vector<Item> &items,\n\t\tint requestedCount) {\n\tif (_requestId != requestId) {\n\t\treturn;\n\t}\n\n\t_requestId = 0;\n\tif (!_offsetDate) {\n\t\t_dates.clear();\n\t}\n\tfor (const auto &[user, date] : items) {\n\t\t_offsetDate = date;\n\t\t_offsetUser = user;\n\t\t_dates.emplace(user, date);\n\t\tdelegate()->peerListSearchAddRow(user);\n\t}\n\tif (items.size() < requestedCount) {\n\t\t// We want cache to have full information about a query with\n\t\t// small results count (that we don't need the second request).\n\t\t// So we don't wait for empty list unlike the non-search case.\n\t\t_allLoaded = true;\n\t}\n\tdelegate()->peerListSearchRefreshRows();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peer_list_box.h\"\n#include \"base/weak_ptr.h\"\n#include \"mtproto/sender.h\"\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nnamespace Ui {\nclass RippleAnimation;\n} // namespace Ui\n\nclass RequestsBoxController final\n\t: public PeerListController\n\t, public base::has_weak_ptr {\npublic:\n\tRequestsBoxController(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer);\n\t~RequestsBoxController();\n\n\tstatic void Start(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowElementClicked(\n\t\tnot_null<PeerListRow*> row,\n\t\tint element) override;\n\tvoid loadMoreRows() override;\n\n\tstd::unique_ptr<PeerListRow> createSearchRow(\n\t\tnot_null<PeerData*> peer) override;\n\tstd::unique_ptr<PeerListRow> createRestoredRow(\n\t\tnot_null<PeerData*> peer) override;\n\n\tstd::unique_ptr<PeerListState> saveState() const override;\n\tvoid restoreState(std::unique_ptr<PeerListState> state) override;\n\nprivate:\n\tclass RowHelper;\n\n\tstruct SavedState : SavedStateBase {\n\t\tusing SearchStateBase = PeerListSearchController::SavedStateBase;\n\t\tstd::unique_ptr<SearchStateBase> searchState;\n\t\tbase::flat_map<not_null<UserData*>, TimeId> dates;\n\t\tTimeId offsetDate = 0;\n\t\tUserData *offsetUser = nullptr;\n\t\tbool allLoaded = false;\n\t\tbool wasLoading = false;\n\t};\n\n\tstatic std::unique_ptr<PeerListSearchController> CreateSearchController(\n\t\tnot_null<PeerData*> peer);\n\n\t[[nodiscard]] std::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<UserData*> user,\n\t\tTimeId date = 0);\n\n\tvoid appendRow(not_null<UserData*> user, TimeId date);\n\tvoid refreshDescription();\n\tvoid processRequest(not_null<UserData*> user, bool approved);\n\n\tvoid subscribeToMigration();\n\tvoid migrate(not_null<ChatData*> chat, not_null<ChannelData*> channel);\n\n\tconst not_null<Window::SessionNavigation*> _navigation;\n\tconst std::unique_ptr<RowHelper> _helper;\n\tnot_null<PeerData*> _peer;\n\tMTP::Sender _api;\n\n\tbase::flat_map<not_null<UserData*>, TimeId> _dates;\n\n\tTimeId _offsetDate = 0;\n\tUserData *_offsetUser = nullptr;\n\tmtpRequestId _loadRequestId = 0;\n\tbool _allLoaded = false;\n\n};\n\n// Members, banned and restricted users server side search.\nclass RequestsBoxSearchController final : public PeerListSearchController {\npublic:\n\tRequestsBoxSearchController(not_null<PeerData*> peer);\n\n\tvoid searchQuery(const QString &query) override;\n\tbool isLoading() override;\n\tbool loadMoreRows() override;\n\n\tvoid removeFromCache(not_null<UserData*> user);\n\t[[nodiscard]] TimeId dateForUser(not_null<UserData*> user);\n\n\tstd::unique_ptr<SavedStateBase> saveState() const override;\n\tvoid restoreState(std::unique_ptr<SavedStateBase> state) override;\n\nprivate:\n\tstruct SavedState : SavedStateBase {\n\t\tQString query;\n\t\tTimeId offsetDate = 0;\n\t\tUserData *offsetUser = nullptr;\n\t\tbool allLoaded = false;\n\t\tbool wasLoading = false;\n\t};\n\tstruct Item {\n\t\tnot_null<UserData*> user;\n\t\tTimeId date = 0;\n\t};\n\tstruct CacheEntry {\n\t\tstd::vector<Item> items;\n\t\tint requestedCount = 0;\n\t};\n\tstruct Query {\n\t\tQString text;\n\t\tTimeId offsetDate = 0;\n\t\tUserData *offsetUser = nullptr;\n\t};\n\n\tvoid searchOnServer();\n\tbool searchInCache();\n\tvoid searchDone(\n\t\tmtpRequestId requestId,\n\t\tconst std::vector<Item> &items,\n\t\tint requestedCount);\n\n\tnot_null<PeerData*> _peer;\n\tMTP::Sender _api;\n\n\tbase::Timer _timer;\n\tQString _query;\n\tmtpRequestId _requestId = 0;\n\tTimeId _offsetDate = 0;\n\tUserData *_offsetUser = nullptr;\n\tbool _allLoaded = false;\n\tbase::flat_map<QString, CacheEntry> _cache;\n\tbase::flat_map<mtpRequestId, Query> _queries;\n\tbase::flat_map<not_null<UserData*>, TimeId> _dates;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_peer_type_box.h\"\n\n#include \"main/main_session.h\"\n#include \"boxes/add_contact_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/peers/edit_participants_box.h\"\n#include \"boxes/peers/edit_peer_common.h\"\n#include \"boxes/peers/edit_peer_info_box.h\" // CreateButton.\n#include \"boxes/peers/edit_peer_invite_link.h\"\n#include \"boxes/peers/edit_peer_invite_links.h\"\n#include \"boxes/peers/edit_peer_usernames_list.h\"\n#include \"boxes/username_box.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"mtproto/sender.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/box_content_divider.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/widgets/fields/special_fields.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_session_controller.h\"\n#include \"settings/settings_common.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_info.h\"\n\nnamespace {\n\nclass Controller : public base::has_weak_ptr {\npublic:\n\tController(\n\t\tWindow::SessionNavigation *navigation,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<PeerData*> peer,\n\t\tbool useLocationPhrases,\n\t\tstd::optional<EditPeerTypeData> dataSavedValue);\n\n\tvoid createContent();\n\t[[nodiscard]] QString getUsernameInput() const;\n\t[[nodiscard]] std::vector<QString> usernamesOrder() const;\n\tvoid setFocusUsername();\n\n\t[[nodiscard]] rpl::producer<QString> getTitle() const {\n\t\treturn !_dataSavedValue\n\t\t\t? tr::lng_create_invite_link_title()\n\t\t\t: _isGroup\n\t\t\t? tr::lng_manage_peer_group_type()\n\t\t\t: tr::lng_manage_peer_channel_type();\n\t}\n\n\t[[nodiscard]] bool goodUsername() const {\n\t\treturn _goodUsername;\n\t}\n\n\t[[nodiscard]] Privacy getPrivacy() const {\n\t\treturn _controls.privacy->current();\n\t}\n\n\t[[nodiscard]] bool noForwards() const {\n\t\treturn _controls.noForwards->toggled();\n\t}\n\t[[nodiscard]] bool joinToWrite() const {\n\t\treturn _controls.joinToWrite && _controls.joinToWrite->toggled();\n\t}\n\t[[nodiscard]] bool requestToJoin() const {\n\t\treturn _controls.requestToJoin && _controls.requestToJoin->toggled();\n\t}\n\n\t[[nodiscard]] rpl::producer<int> scrollToRequests() const {\n\t\treturn _scrollToRequests.events();\n\t}\n\n\tvoid showError(rpl::producer<QString> text) {\n\t\t_controls.usernameInput->showError();\n\t\tshowUsernameError(std::move(text));\n\t}\n\nprivate:\n\tstruct Controls {\n\t\tstd::shared_ptr<Ui::RadioenumGroup<Privacy>> privacy;\n\t\tUi::SlideWrap<Ui::RpWidget> *usernameWrap = nullptr;\n\t\tUi::UsernameInput *usernameInput = nullptr;\n\t\tUsernamesList *usernamesList = nullptr;\n\t\tbase::unique_qptr<Ui::FlatLabel> usernameCheckResult;\n\n\t\tUi::SlideWrap<> *inviteLinkWrap = nullptr;\n\t\tUi::FlatLabel *inviteLink = nullptr;\n\n\t\tUi::SlideWrap<Ui::VerticalLayout> *whoSendWrap = nullptr;\n\t\tUi::SettingsButton *noForwards = nullptr;\n\t\tUi::SettingsButton *joinToWrite = nullptr;\n\t\tUi::SettingsButton *requestToJoin = nullptr;\n\t};\n\n\tControls _controls;\n\n\tobject_ptr<Ui::RpWidget> createUsernameEdit();\n\tobject_ptr<Ui::RpWidget> createInviteLinkBlock();\n\n\tvoid privacyChanged(Privacy value);\n\n\tvoid checkUsernameAvailability();\n\tvoid askUsernameRevoke();\n\tvoid usernameChanged();\n\tvoid showUsernameError(rpl::producer<QString> &&error);\n\tvoid showUsernameGood();\n\tvoid showUsernamePending();\n\tvoid showUsernameEmpty();\n\n\tvoid fillPrivaciesButtons(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tstd::optional<Privacy> savedValue);\n\tvoid addRoundButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tPrivacy value,\n\t\tconst QString &text,\n\t\trpl::producer<QString> about);\n\n\tWindow::SessionNavigation *_navigation = nullptr;\n\tstd::shared_ptr<Ui::Show> _show;\n\n\tnot_null<PeerData*> _peer;\n\tbool _linkOnly = false;\n\n\tMTP::Sender _api;\n\tstd::optional<EditPeerTypeData> _dataSavedValue;\n\n\tbool _useLocationPhrases = false;\n\tbool _isGroup = false;\n\tbool _goodUsername = false;\n\n\tbase::unique_qptr<Ui::VerticalLayout> _wrap;\n\tbase::Timer _checkUsernameTimer;\n\tmtpRequestId _checkUsernameRequestId = 0;\n\tUsernameState _usernameState = UsernameState::Normal;\n\n\trpl::event_stream<UsernameCheckInfo> _usernameCheckInfo;\n\trpl::lifetime _usernameCheckInfoLifetime;\n\n\trpl::event_stream<int> _scrollToRequests;\n\n\trpl::lifetime _lifetime;\n\n};\n\nController::Controller(\n\tWindow::SessionNavigation *navigation,\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<PeerData*> peer,\n\tbool useLocationPhrases,\n\tstd::optional<EditPeerTypeData> dataSavedValue)\n: _navigation(navigation)\n, _show(show)\n, _peer(peer)\n, _linkOnly(!dataSavedValue.has_value())\n, _api(&_peer->session().mtp())\n, _dataSavedValue(dataSavedValue)\n, _useLocationPhrases(useLocationPhrases)\n, _isGroup(_peer->isChat() || _peer->isMegagroup())\n, _goodUsername(_dataSavedValue\n\t? !_dataSavedValue->username.isEmpty()\n\t: (_peer->isChannel() && !_peer->asChannel()->editableUsername().isEmpty()))\n, _wrap(container)\n, _checkUsernameTimer([=] { checkUsernameAvailability(); }) {\n\t_peer->updateFull();\n}\n\nvoid Controller::createContent() {\n\t_controls = Controls();\n\n\tfillPrivaciesButtons(\n\t\t_wrap,\n\t\t(_dataSavedValue\n\t\t\t? _dataSavedValue->privacy\n\t\t\t: std::optional<Privacy>()));\n\n\t// Skip.\n\tif (!_linkOnly) {\n\t\t_wrap->add(object_ptr<Ui::BoxContentDivider>(_wrap));\n\t}\n\t//\n\t_wrap->add(createInviteLinkBlock());\n\tif (!_linkOnly) {\n\t\t_wrap->add(createUsernameEdit());\n\t}\n\n\tusing namespace Settings;\n\n\tif (!_linkOnly) {\n\t\tif (_peer->isMegagroup()) {\n\t\t\t_controls.whoSendWrap = _wrap->add(\n\t\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\t\t_wrap.get(),\n\t\t\t\t\tobject_ptr<Ui::VerticalLayout>(_wrap.get())));\n\t\t\tconst auto wrap = _controls.whoSendWrap->entity();\n\n\t\t\tUi::AddSkip(wrap);\n\t\t\tif (_dataSavedValue->hasDiscussionLink) {\n\t\t\t\tUi::AddSubsectionTitle(wrap, tr::lng_manage_peer_send_title());\n\n\t\t\t\t_controls.joinToWrite = wrap->add(EditPeerInfoBox::CreateButton(\n\t\t\t\t\twrap,\n\t\t\t\t\ttr::lng_manage_peer_send_only_members(),\n\t\t\t\t\trpl::single(QString()),\n\t\t\t\t\t[=] {},\n\t\t\t\t\tst::peerPermissionsButton,\n\t\t\t\t\t{}\n\t\t\t\t));\n\t\t\t\t_controls.joinToWrite->toggleOn(\n\t\t\t\t\trpl::single(_dataSavedValue->joinToWrite)\n\t\t\t\t)->toggledValue(\n\t\t\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\t\t\t_dataSavedValue->joinToWrite = toggled;\n\t\t\t\t}, wrap->lifetime());\n\t\t\t} else {\n\t\t\t\t_controls.whoSendWrap->toggle(\n\t\t\t\t\t(_controls.privacy->current() == Privacy::HasUsername),\n\t\t\t\t\tanim::type::instant);\n\t\t\t}\n\t\t\tauto joinToWrite = _controls.joinToWrite\n\t\t\t\t? _controls.joinToWrite->toggledValue()\n\t\t\t\t: rpl::single(true);\n\n\t\t\tconst auto requestToJoinWrap = wrap->add(\n\t\t\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\t\t\twrap,\n\t\t\t\t\tEditPeerInfoBox::CreateButton(\n\t\t\t\t\t\twrap,\n\t\t\t\t\t\ttr::lng_manage_peer_send_approve_members(),\n\t\t\t\t\t\trpl::single(QString()),\n\t\t\t\t\t\t[=] {},\n\t\t\t\t\t\tst::peerPermissionsButton,\n\t\t\t\t\t\t{})))->setDuration(0);\n\t\t\trequestToJoinWrap->toggleOn(rpl::duplicate(joinToWrite));\n\t\t\t_controls.requestToJoin = requestToJoinWrap->entity();\n\t\t\t_controls.requestToJoin->toggleOn(\n\t\t\t\trpl::single(_dataSavedValue->requestToJoin)\n\t\t\t)->toggledValue(\n\t\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\t\t_dataSavedValue->requestToJoin = toggled;\n\t\t\t}, wrap->lifetime());\n\n\t\t\tUi::AddSkip(wrap);\n\t\t\tUi::AddDividerText(\n\t\t\t\twrap,\n\t\t\t\trpl::conditional(\n\t\t\t\t\tstd::move(joinToWrite),\n\t\t\t\t\ttr::lng_manage_peer_send_approve_members_about(),\n\t\t\t\t\ttr::lng_manage_peer_send_only_members_about()));\n\t\t}\n\t\tUi::AddSkip(_wrap.get());\n\t\tUi::AddSubsectionTitle(\n\t\t\t_wrap.get(),\n\t\t\ttr::lng_manage_peer_no_forwards_title());\n\t\t_controls.noForwards = _wrap->add(EditPeerInfoBox::CreateButton(\n\t\t\t_wrap.get(),\n\t\t\ttr::lng_manage_peer_no_forwards(),\n\t\t\trpl::single(QString()),\n\t\t\t[] {},\n\t\t\tst::peerPermissionsButton,\n\t\t\t{}));\n\t\t_controls.noForwards->toggleOn(\n\t\t\trpl::single(_dataSavedValue->noForwards)\n\t\t)->toggledValue(\n\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\t_dataSavedValue->noForwards = toggled;\n\t\t}, _wrap->lifetime());\n\t\tUi::AddSkip(_wrap.get());\n\t\tUi::AddDividerText(\n\t\t\t_wrap.get(),\n\t\t\t(_isGroup\n\t\t\t\t? tr::lng_manage_peer_no_forwards_about\n\t\t\t\t: tr::lng_manage_peer_no_forwards_about_channel)());\n\n\t}\n\tif (_linkOnly) {\n\t\t_controls.inviteLinkWrap->show(anim::type::instant);\n\t} else {\n\t\tif (_controls.privacy->current() == Privacy::NoUsername) {\n\t\t\tcheckUsernameAvailability();\n\t\t}\n\t\tconst auto forShowing = _dataSavedValue\n\t\t\t? _dataSavedValue->privacy\n\t\t\t: Privacy::NoUsername;\n\t\t_controls.inviteLinkWrap->toggle(\n\t\t\t(forShowing != Privacy::HasUsername),\n\t\t\tanim::type::instant);\n\t\t_controls.usernameWrap->toggle(\n\t\t\t(forShowing == Privacy::HasUsername),\n\t\t\tanim::type::instant);\n\t}\n}\n\nvoid Controller::addRoundButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tPrivacy value,\n\t\tconst QString &text,\n\t\trpl::producer<QString> about) {\n\tcontainer->add(object_ptr<Ui::Radioenum<Privacy>>(\n\t\tcontainer,\n\t\t_controls.privacy,\n\t\tvalue,\n\t\ttext,\n\t\tst::editPeerPrivacyBoxCheckbox));\n\tcontainer->add(object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\tstd::move(about),\n\t\t\tst::editPeerPrivacyLabel),\n\t\tst::editPeerPrivacyLabelMargins));\n\tcontainer->add(object_ptr<Ui::FixedHeightWidget>(\n\t\t\tcontainer,\n\t\t\tst::editPeerPrivacyBottomSkip));\n};\n\nvoid Controller::fillPrivaciesButtons(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tstd::optional<Privacy> savedValue) {\n\tif (_linkOnly) {\n\t\treturn;\n\t}\n\n\tconst auto result = parent->add(\n\t\t\tobject_ptr<Ui::PaddingWrap<Ui::VerticalLayout>>(\n\t\t\t\tparent,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(parent),\n\t\t\t\tst::editPeerPrivaciesMargins));\n\tconst auto container = result->entity();\n\n\tconst auto isPublic = _peer->isChannel()\n\t\t&& _peer->asChannel()->hasUsername();\n\t_controls.privacy = std::make_shared<Ui::RadioenumGroup<Privacy>>(\n\t\tsavedValue.value_or(\n\t\t\tisPublic ? Privacy::HasUsername : Privacy::NoUsername));\n\n\taddRoundButton(\n\t\tcontainer,\n\t\tPrivacy::HasUsername,\n\t\t(_useLocationPhrases\n\t\t\t? tr::lng_create_permanent_link_title\n\t\t\t: _isGroup\n\t\t\t? tr::lng_create_public_group_title\n\t\t\t: tr::lng_create_public_channel_title)(tr::now),\n\t\t(_isGroup\n\t\t\t? tr::lng_create_public_group_about\n\t\t\t: tr::lng_create_public_channel_about)());\n\taddRoundButton(\n\t\tcontainer,\n\t\tPrivacy::NoUsername,\n\t\t(_useLocationPhrases\n\t\t\t? tr::lng_create_invite_link_title\n\t\t\t: _isGroup\n\t\t\t? tr::lng_create_private_group_title\n\t\t\t: tr::lng_create_private_channel_title)(tr::now),\n\t\t(_useLocationPhrases\n\t\t\t? tr::lng_create_invite_link_about\n\t\t\t: _isGroup\n\t\t\t? tr::lng_create_private_group_about\n\t\t\t: tr::lng_create_private_channel_about)());\n\n\t_controls.privacy->setChangedCallback([=](Privacy value) {\n\t\tprivacyChanged(value);\n\t});\n}\n\nvoid Controller::setFocusUsername() {\n\tif (_controls.usernameInput) {\n\t\t_controls.usernameInput->setFocus();\n\t}\n}\n\nQString Controller::getUsernameInput() const {\n\treturn _controls.usernameInput->getLastText().trimmed();\n}\n\nstd::vector<QString> Controller::usernamesOrder() const {\n\treturn _controls.usernamesList\n\t\t? _controls.usernamesList->order()\n\t\t: std::vector<QString>();\n}\n\nobject_ptr<Ui::RpWidget> Controller::createUsernameEdit() {\n\tExpects(_wrap != nullptr);\n\n\tconst auto channel = _peer->asChannel();\n\tconst auto username = (!_dataSavedValue || !channel)\n\t\t? QString()\n\t\t: channel->editableUsername();\n\n\tauto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t_wrap,\n\t\tobject_ptr<Ui::VerticalLayout>(_wrap));\n\t_controls.usernameWrap = result.data();\n\n\tconst auto container = result->entity();\n\n\tusing namespace Settings;\n\tUi::AddSkip(container);\n\tcontainer->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_create_group_link(),\n\t\t\tst::defaultSubsectionTitle),\n\t\tst::defaultSubsectionTitlePadding);\n\n\tconst auto placeholder = container->add(\n\t\tobject_ptr<Ui::RpWidget>(container),\n\t\tst::editPeerUsernameFieldMargins);\n\tplaceholder->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_controls.usernameInput = Ui::AttachParentChild(\n\t\tcontainer,\n\t\tobject_ptr<Ui::UsernameInput>(\n\t\t\tcontainer,\n\t\t\tst::setupChannelLink,\n\t\t\tnullptr,\n\t\t\tusername,\n\t\t\t_peer->session().createInternalLink(QString())));\n\t_controls.usernameInput->heightValue(\n\t) | rpl::on_next([placeholder](int height) {\n\t\tplaceholder->resize(placeholder->width(), height);\n\t}, placeholder->lifetime());\n\tplaceholder->widthValue(\n\t) | rpl::on_next([this](int width) {\n\t\t_controls.usernameInput->resize(\n\t\t\twidth,\n\t\t\t_controls.usernameInput->height());\n\t}, placeholder->lifetime());\n\t_controls.usernameInput->move(placeholder->pos());\n\n\tAddUsernameCheckLabel(container, _usernameCheckInfo.events());\n\n\tUi::AddDividerText(\n\t\tcontainer,\n\t\ttr::lng_create_channel_link_about());\n\n\tif (channel) {\n\t\tconst auto focusCallback = [=] {\n\t\t\t_scrollToRequests.fire(container->y());\n\t\t\t_controls.usernameInput->setFocusFast();\n\t\t};\n\t\t_controls.usernamesList = container->add(object_ptr<UsernamesList>(\n\t\t\tcontainer,\n\t\t\tchannel,\n\t\t\t_show,\n\t\t\tfocusCallback));\n\t}\n\n\tQObject::connect(\n\t\t_controls.usernameInput,\n\t\t&Ui::UsernameInput::changed,\n\t\t[this] { usernameChanged(); });\n\n\tconst auto shown = (_controls.privacy->current() == Privacy::HasUsername);\n\tresult->toggle(shown, anim::type::instant);\n\n\treturn result;\n}\n\nvoid Controller::privacyChanged(Privacy value) {\n\tconst auto toggleInviteLink = [&] {\n\t\t_controls.inviteLinkWrap->toggle(\n\t\t\t(value != Privacy::HasUsername),\n\t\t\tanim::type::instant);\n\t};\n\tconst auto toggleEditUsername = [&] {\n\t\t_controls.usernameWrap->toggle(\n\t\t\t(value == Privacy::HasUsername),\n\t\t\tanim::type::instant);\n\t};\n\tconst auto toggleWhoSendWrap = [&] {\n\t\tif (!_controls.whoSendWrap) {\n\t\t\treturn;\n\t\t}\n\t\t_controls.whoSendWrap->toggle(\n\t\t\t(value == Privacy::HasUsername\n\t\t\t\t|| (_dataSavedValue && _dataSavedValue->hasDiscussionLink)),\n\t\t\tanim::type::instant);\n\t};\n\tconst auto refreshVisibilities = [&] {\n\t\t// Now first we need to hide that was shown.\n\t\t// Otherwise box will change own Y position.\n\n\t\tif (value == Privacy::HasUsername) {\n\t\t\ttoggleInviteLink();\n\t\t\ttoggleEditUsername();\n\t\t\ttoggleWhoSendWrap();\n\n\t\t\tshowUsernameEmpty();\n\t\t\tcheckUsernameAvailability();\n\t\t} else {\n\t\t\ttoggleWhoSendWrap();\n\t\t\ttoggleEditUsername();\n\t\t\ttoggleInviteLink();\n\t\t}\n\t};\n\tif (value == Privacy::HasUsername) {\n\t\tif (_usernameState == UsernameState::TooMany) {\n\t\t\taskUsernameRevoke();\n\t\t\treturn;\n\t\t} else if (_usernameState == UsernameState::NotAvailable) {\n\t\t\t_controls.privacy->setValue(Privacy::NoUsername);\n\t\t\treturn;\n\t\t}\n\t\trefreshVisibilities();\n\t\t_controls.usernameInput->setDisplayFocused(true);\n\t} else {\n\t\t_api.request(base::take(_checkUsernameRequestId)).cancel();\n\t\t_checkUsernameTimer.cancel();\n\t\trefreshVisibilities();\n\t}\n\tsetFocusUsername();\n}\n\nvoid Controller::checkUsernameAvailability() {\n\tif (!_controls.usernameInput) {\n\t\treturn;\n\t}\n\tconst auto initial = (_controls.privacy->current() != Privacy::HasUsername);\n\tconst auto checking = initial\n\t\t? u\".bad.\"_q\n\t\t: getUsernameInput();\n\tif (checking.size() < Ui::EditPeer::kMinUsernameLength) {\n\t\treturn;\n\t}\n\tif (_checkUsernameRequestId) {\n\t\t_api.request(_checkUsernameRequestId).cancel();\n\t}\n\tconst auto channel = _peer->migrateToOrMe()->asChannel();\n\tconst auto username = channel ? channel->editableUsername() : QString();\n\t_checkUsernameRequestId = _api.request(MTPchannels_CheckUsername(\n\t\tchannel ? channel->inputChannel() : MTP_inputChannelEmpty(),\n\t\tMTP_string(checking)\n\t)).done([=](const MTPBool &result) {\n\t\t_checkUsernameRequestId = 0;\n\t\tif (initial) {\n\t\t\treturn;\n\t\t}\n\t\tif (!mtpIsTrue(result) && checking != username) {\n\t\t\tshowUsernameError(tr::lng_create_channel_link_occupied());\n\t\t} else {\n\t\t\tshowUsernameGood();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\t_checkUsernameRequestId = 0;\n\t\tconst auto &type = error.type();\n\t\t_usernameState = UsernameState::Normal;\n\t\tif (type == u\"CHANNEL_PUBLIC_GROUP_NA\"_q) {\n\t\t\t_usernameState = UsernameState::NotAvailable;\n\t\t\t_controls.privacy->setValue(Privacy::NoUsername);\n\t\t} else if (type == u\"CHANNELS_ADMIN_PUBLIC_TOO_MUCH\"_q) {\n\t\t\t_usernameState = UsernameState::TooMany;\n\t\t\tif (_controls.privacy->current() == Privacy::HasUsername) {\n\t\t\t\taskUsernameRevoke();\n\t\t\t}\n\t\t} else if (initial) {\n\t\t\tif (_controls.privacy->current() == Privacy::HasUsername) {\n\t\t\t\tshowUsernameEmpty();\n\t\t\t\tsetFocusUsername();\n\t\t\t}\n\t\t} else if (type == u\"USERNAME_INVALID\"_q) {\n\t\t\tshowUsernameError(tr::lng_create_channel_link_invalid());\n\t\t} else if (type == u\"USERNAME_PURCHASE_AVAILABLE\"_q) {\n\t\t\t_goodUsername = false;\n\t\t\t_usernameCheckInfo.fire(\n\t\t\t\tUsernameCheckInfo::PurchaseAvailable(checking, _peer));\n\t\t} else if (type == u\"USERNAME_OCCUPIED\"_q && checking != username) {\n\t\t\tshowUsernameError(tr::lng_create_channel_link_occupied());\n\t\t}\n\t}).send();\n}\n\nvoid Controller::askUsernameRevoke() {\n\t_controls.privacy->setValue(Privacy::NoUsername);\n\tconst auto revokeCallback = crl::guard(this, [this] {\n\t\t_usernameState = UsernameState::Normal;\n\t\t_controls.privacy->setValue(Privacy::HasUsername);\n\t\tcheckUsernameAvailability();\n\t});\n\t_show->showBox(Box(PublicLinksLimitBox, _navigation, revokeCallback));\n}\n\nvoid Controller::usernameChanged() {\n\t_goodUsername = false;\n\tconst auto username = getUsernameInput();\n\tif (username.isEmpty()) {\n\t\tshowUsernameEmpty();\n\t\t_checkUsernameTimer.cancel();\n\t\treturn;\n\t}\n\tconst auto bad = ranges::any_of(username, [](QChar ch) {\n\t\treturn (ch < 'A' || ch > 'Z')\n\t\t\t&& (ch < 'a' || ch > 'z')\n\t\t\t&& (ch < '0' || ch > '9')\n\t\t\t&& (ch != '_');\n\t});\n\tif (bad) {\n\t\tshowUsernameError(tr::lng_create_channel_link_bad_symbols());\n\t} else if (username.size() < Ui::EditPeer::kMinUsernameLength) {\n\t\tshowUsernameError(tr::lng_create_channel_link_too_short());\n\t} else {\n\t\tshowUsernamePending();\n\t\t_checkUsernameTimer.callOnce(Ui::EditPeer::kUsernameCheckTimeout);\n\t}\n}\n\nvoid Controller::showUsernameError(rpl::producer<QString> &&error) {\n\t_goodUsername = false;\n\t_usernameCheckInfoLifetime.destroy();\n\tstd::move(\n\t\terror\n\t) | rpl::map([](QString s) {\n\t\treturn UsernameCheckInfo{\n\t\t\t.type = UsernameCheckInfo::Type::Error,\n\t\t\t.text = { std::move(s) },\n\t\t};\n\t}) | rpl::start_to_stream(_usernameCheckInfo, _usernameCheckInfoLifetime);\n}\n\nvoid Controller::showUsernameGood() {\n\t_goodUsername = true;\n\t_usernameCheckInfoLifetime.destroy();\n\t_usernameCheckInfo.fire({\n\t\t.type = UsernameCheckInfo::Type::Good,\n\t\t.text = { tr::lng_create_channel_link_available(tr::now) },\n\t});\n}\n\nvoid Controller::showUsernamePending() {\n\t_usernameCheckInfoLifetime.destroy();\n\t_usernameCheckInfo.fire({\n\t\t.type = UsernameCheckInfo::Type::Default,\n\t\t.text = { .text = tr::lng_create_channel_link_pending(tr::now) },\n\t});\n}\n\nvoid Controller::showUsernameEmpty() {\n\t_usernameCheckInfoLifetime.destroy();\n\t_usernameCheckInfo.fire({ .type = UsernameCheckInfo::Type::Default });\n}\n\nobject_ptr<Ui::RpWidget> Controller::createInviteLinkBlock() {\n\tExpects(_wrap != nullptr);\n\n\tauto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t_wrap,\n\t\tobject_ptr<Ui::VerticalLayout>(_wrap));\n\t_controls.inviteLinkWrap = result.data();\n\n\tconst auto container = result->entity();\n\n\tusing namespace Settings;\n\tif (_dataSavedValue) {\n\t\tUi::AddSkip(container);\n\n\t\tAddSubsectionTitle(container, tr::lng_create_permanent_link_title());\n\t}\n\tAddPermanentLinkBlock(\n\t\t_show,\n\t\tcontainer,\n\t\t_peer,\n\t\t_peer->session().user(),\n\t\tnullptr);\n\n\tUi::AddSkip(container);\n\n\tUi::AddDividerText(\n\t\tcontainer,\n\t\t((_peer->isMegagroup() || _peer->asChat())\n\t\t\t? tr::lng_group_invite_about_permanent_group()\n\t\t\t: tr::lng_group_invite_about_permanent_channel()));\n\n\treturn result;\n}\n\n} // namespace\n\nEditPeerTypeBox::EditPeerTypeBox(\n\tQWidget*,\n\tWindow::SessionNavigation *navigation,\n\tnot_null<PeerData*> peer,\n\tbool useLocationPhrases,\n\tstd::optional<FnMut<void(EditPeerTypeData)>> savedCallback,\n\tstd::optional<EditPeerTypeData> dataSaved,\n\tstd::optional<rpl::producer<QString>> usernameError)\n: _navigation(navigation)\n, _peer(peer)\n, _useLocationPhrases(useLocationPhrases)\n, _savedCallback(std::move(savedCallback))\n, _dataSavedValue(dataSaved)\n, _usernameError(usernameError) {\n}\n\nEditPeerTypeBox::EditPeerTypeBox(\n\tQWidget*,\n\tnot_null<PeerData*> peer)\n: EditPeerTypeBox(nullptr, nullptr, peer, {}, {}, {}) {\n}\n\nvoid EditPeerTypeBox::setInnerFocus() {\n\t_focusRequests.fire({});\n}\n\nvoid EditPeerTypeBox::prepare() {\n\t_peer->updateFull();\n\n\tauto content = object_ptr<Ui::VerticalLayout>(this);\n\n\tconst auto controller = Ui::CreateChild<Controller>(\n\t\tthis,\n\t\t_navigation,\n\t\tuiShow(),\n\t\tcontent.data(),\n\t\t_peer,\n\t\t_useLocationPhrases,\n\t\t_dataSavedValue);\n\tcontroller->scrollToRequests(\n\t) | rpl::on_next([=, raw = content.data()](int y) {\n\t\tscrollToY(raw->y() + y);\n\t}, lifetime());\n\t_focusRequests.events(\n\t) | rpl::on_next(\n\t\t[=] {\n\t\t\tcontroller->setFocusUsername();\n\t\t\tif (_usernameError.has_value()) {\n\t\t\t\tcontroller->showError(std::move(*_usernameError));\n\t\t\t\t_usernameError = std::nullopt;\n\t\t\t}\n\t\t},\n\t\tlifetime());\n\tcontroller->createContent();\n\n\tsetTitle(controller->getTitle());\n\n\tif (_savedCallback.has_value()) {\n\t\taddButton(tr::lng_settings_save(), [=] {\n\t\t\tconst auto v = controller->getPrivacy();\n\t\t\tif ((v == Privacy::HasUsername) && !controller->goodUsername()) {\n\t\t\t\tif (!controller->getUsernameInput().isEmpty()\n\t\t\t\t\t|| controller->usernamesOrder().empty()) {\n\t\t\t\t\tcontroller->setFocusUsername();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tauto local = std::move(*_savedCallback);\n\t\t\tlocal(EditPeerTypeData{\n\t\t\t\t.privacy = v,\n\t\t\t\t.username = (v == Privacy::HasUsername\n\t\t\t\t\t? controller->getUsernameInput()\n\t\t\t\t\t: QString()),\n\t\t\t\t.usernamesOrder = (v == Privacy::HasUsername\n\t\t\t\t\t? controller->usernamesOrder()\n\t\t\t\t\t: std::vector<QString>()),\n\t\t\t\t.noForwards = controller->noForwards(),\n\t\t\t\t.joinToWrite = controller->joinToWrite(),\n\t\t\t\t.requestToJoin = controller->requestToJoin(),\n\t\t\t}); // We don't need username with private type.\n\t\t\tcloseBox();\n\t\t});\n\t\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\t} else {\n\t\taddButton(tr::lng_close(), [=] { closeBox(); });\n\t}\n\n\tsetDimensionsToContent(st::boxWideWidth, content.data());\n\tsetInnerWidget(std::move(content));\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n\nnamespace style {\nstruct SettingsCountButton;\n} // namespace style\n\nnamespace Ui {\nclass VerticalLayout;\nclass SettingsButton;\n} // namespace Ui\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nenum class Privacy {\n\tHasUsername,\n\tNoUsername,\n};\n\nenum class UsernameState {\n\tNormal,\n\tTooMany,\n\tNotAvailable,\n};\n\nstruct EditPeerTypeData {\n\tPrivacy privacy = Privacy::NoUsername;\n\tQString username;\n\tstd::vector<QString> usernamesOrder;\n\tbool hasDiscussionLink = false;\n\tbool noForwards = false;\n\tbool joinToWrite = false;\n\tbool requestToJoin = false;\n};\n\nclass EditPeerTypeBox : public Ui::BoxContent {\npublic:\n\tEditPeerTypeBox(\n\t\tQWidget*,\n\t\tWindow::SessionNavigation *navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tbool useLocationPhrases,\n\t\tstd::optional<FnMut<void(EditPeerTypeData)>> savedCallback,\n\t\tstd::optional<EditPeerTypeData> dataSaved,\n\t\tstd::optional<rpl::producer<QString>> usernameError = {});\n\n\t// For invite link only.\n\tEditPeerTypeBox(\n\t\tQWidget*,\n\t\tnot_null<PeerData*> peer);\n\nprotected:\n\tvoid prepare() override;\n\tvoid setInnerFocus() override;\n\nprivate:\n\tWindow::SessionNavigation *_navigation = nullptr;\n\tconst not_null<PeerData*> _peer;\n\tbool _useLocationPhrases = false;\n\tstd::optional<FnMut<void(EditPeerTypeData)>> _savedCallback;\n\n\tstd::optional<EditPeerTypeData> _dataSavedValue;\n\tstd::optional<rpl::producer<QString>> _usernameError;\n\n\trpl::event_stream<> _focusRequests;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_usernames_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_peer_usernames_list.h\"\n\n#include \"api/api_filter_updates.h\"\n#include \"api/api_user_names.h\"\n#include \"apiwrap.h\"\n#include \"base/event_filter.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/layers/show.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/text_utilities.h\" // tr::rich.\n#include \"ui/toast/toast.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/vertical_layout_reorder.h\"\n#include \"styles/style_boxes.h\" // contactsStatusFont.\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtGui/QGuiApplication>\n\nnamespace {\n\nclass RightAction final : public Ui::RpWidget {\npublic:\n\tRightAction(not_null<Ui::RpWidget*> parent);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\n};\n\nRightAction::RightAction(not_null<Ui::RpWidget*> parent)\n: RpWidget(parent) {\n\tsetCursor(style::cur_sizeall);\n\tconst auto &st = st::inviteLinkThreeDots;\n\tresize(st.width, st.height);\n}\n\nvoid RightAction::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\tst::usernamesReorderIcon.paintInCenter(p, rect());\n}\n\nvoid RightAction::mousePressEvent(QMouseEvent *e) {\n}\n\n} // namespace\n\nclass UsernamesList::Row final : public Ui::SettingsButton {\npublic:\n\tRow(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst Data::Username &data,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tQString status,\n\t\tQString link);\n\n\t[[nodiscard]] const Data::Username &username() const;\n\t[[nodiscard]] not_null<Ui::RpWidget*> rightAction() const;\n\n\tint resizeGetHeight(int newWidth) override;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tconst style::PeerListItem &_st;\n\tconst Data::Username _data;\n\tconst QString _status;\n\tconst not_null<Ui::RpWidget*> _rightAction;\n\tconst QRect _iconRect;\n\tstd::shared_ptr<Ui::Show> _show;\n\tUi::Text::String _title;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n};\n\nUsernamesList::Row::Row(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst Data::Username &data,\n\tstd::shared_ptr<Ui::Show> show,\n\tQString status,\n\tQString link)\n: Ui::SettingsButton(parent, rpl::never<QString>())\n, _st(st::inviteLinkListItem)\n, _data(data)\n, _status(std::move(status))\n, _rightAction(Ui::CreateChild<RightAction>(this))\n, _iconRect(\n\t_st.photoPosition.x() + st::inviteLinkIconSkip,\n\t_st.photoPosition.y() + st::inviteLinkIconSkip,\n\t_st.photoSize - st::inviteLinkIconSkip * 2,\n\t_st.photoSize - st::inviteLinkIconSkip * 2)\n, _show(show)\n, _title(_st.nameStyle, '@' + data.username) {\n\tbase::install_event_filter(this, [=](not_null<QEvent*> e) {\n\t\tif (e->type() != QEvent::ContextMenu) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\t\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tthis,\n\t\t\tst::popupMenuWithIcons);\n\t\t_menu->addAction(\n\t\t\ttr::lng_group_invite_context_copy(tr::now),\n\t\t\t[=] {\n\t\t\t\tQGuiApplication::clipboard()->setText(link);\n\t\t\t\tshow->showToast(\n\t\t\t\t\ttr::lng_create_channel_link_copied(tr::now));\n\t\t\t},\n\t\t\t&st::menuIconCopy);\n\t\t_menu->popup(QCursor::pos());\n\t\treturn base::EventFilterResult::Cancel;\n\t});\n\n\t_rightAction->setVisible(data.active);\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\t_rightAction->moveToLeft(\n\t\t\ts.width() - _rightAction->width() - st::inviteLinkThreeDotsSkip,\n\t\t\t(s.height() - _rightAction->height()) / 2);\n\t}, _rightAction->lifetime());\n}\n\nconst Data::Username &UsernamesList::Row::username() const {\n\treturn _data;\n}\n\nnot_null<Ui::RpWidget*> UsernamesList::Row::rightAction() const {\n\treturn _rightAction;\n}\n\nint UsernamesList::Row::resizeGetHeight(int newWidth) {\n\treturn _st.height;\n}\n\nvoid UsernamesList::Row::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\n\tconst auto paintOver = (isOver() || isDown()) && !isDisabled();\n\tUi::SettingsButton::paintBg(p, e->rect(), paintOver);\n\tUi::SettingsButton::paintRipple(p, 0, 0);\n\n\tconst auto active = _data.active;\n\n\tconst auto &color = active ? st::msgFile1Bg : st::windowSubTextFg;\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(color);\n\t{\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.drawEllipse(_iconRect);\n\t}\n\t(!active\n\t\t? st::inviteLinkRevokedIcon\n\t\t: st::inviteLinkIcon).paintInCenter(p, _iconRect);\n\n\tp.setPen(_st.nameFg);\n\t_title.drawLeft(\n\t\tp,\n\t\t_st.namePosition.x(),\n\t\t_st.namePosition.y(),\n\t\twidth(),\n\t\twidth() - _st.namePosition.x());\n\n\tp.setPen(active\n\t\t? _st.statusFgActive\n\t\t: paintOver\n\t\t? _st.statusFgOver\n\t\t: _st.statusFg);\n\tp.setFont(st::contactsStatusFont);\n\tp.drawTextLeft(\n\t\t_st.statusPosition.x(),\n\t\t_st.statusPosition.y(),\n\t\twidth() - _st.statusPosition.x(),\n\t\t_status);\n}\n\nUsernamesList::UsernamesList(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<PeerData*> peer,\n\tstd::shared_ptr<Ui::Show> show,\n\tFn<void()> focusCallback)\n: RpWidget(parent)\n, _show(show)\n, _peer(peer)\n, _isBot(peer->isUser()\n\t&& peer->asUser()->botInfo\n\t&& peer->asUser()->botInfo->canEditInformation)\n, _focusCallback(std::move(focusCallback)) {\n\t{\n\t\tauto &api = _peer->session().api();\n\t\tconst auto usernames = api.usernames().cacheFor(_peer->id);\n\t\tif (!usernames.empty()) {\n\t\t\trebuild(usernames);\n\t\t}\n\t}\n\tload();\n\n\trpl::merge(\n\t\tpeer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tData::PeerUpdate::Flag::Username),\n\t\tpeer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tData::PeerUpdate::Flag::Usernames)\n\t) | rpl::on_next([=] {\n\t\tload();\n\t}, lifetime());\n}\n\nvoid UsernamesList::load() {\n\t_loadLifetime = _peer->session().api().usernames().loadUsernames(\n\t\t_peer\n\t) | rpl::on_next([=](const Data::Usernames &usernames) {\n\t\tif (usernames.empty()) {\n\t\t\t_container = nullptr;\n\t\t\tresize(0, 0);\n\t\t} else {\n\t\t\trebuild(usernames);\n\t\t}\n\t});\n}\n\nvoid UsernamesList::rebuild(const Data::Usernames &usernames) {\n\tif (_reorder) {\n\t\t_reorder->cancel();\n\t}\n\t_rows.clear();\n\t_rows.reserve(usernames.size());\n\t_container = base::make_unique_q<Ui::VerticalLayout>(this);\n\n\t{\n\t\tUi::AddSkip(_container);\n\t\t_container->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t_container,\n\t\t\t\t_peer->isSelf()\n\t\t\t\t\t? tr::lng_usernames_subtitle()\n\t\t\t\t\t: tr::lng_channel_usernames_subtitle(),\n\t\t\t\tst::defaultSubsectionTitle),\n\t\t\tst::defaultSubsectionTitlePadding);\n\t}\n\n\tconst auto content = _container->add(\n\t\tobject_ptr<Ui::VerticalLayout>(_container));\n\tfor (const auto &username : usernames) {\n\t\tconst auto link = _peer->session().createInternalLinkFull(\n\t\t\tusername.username);\n\t\tconst auto status = (username.editable && _focusCallback)\n\t\t\t? tr::lng_usernames_edit(tr::now)\n\t\t\t: (username.editable && !username.active)\n\t\t\t? tr::lng_usernames_non_active(tr::now)\n\t\t\t: username.active\n\t\t\t? tr::lng_usernames_active(tr::now)\n\t\t\t: tr::lng_usernames_non_active(tr::now);\n\t\tconst auto row = content->add(\n\t\t\tobject_ptr<Row>(content, username, _show, status, link));\n\t\t_rows.push_back(row);\n\t\trow->addClickHandler([=] {\n\t\t\tif (_reordering\n\t\t\t\t|| (!_peer->isSelf() && !_peer->isChannel() && !_isBot)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (username.editable) {\n\t\t\t\tif (_focusCallback) {\n\t\t\t\t\t_focusCallback();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (_isBot) {\n\t\t\t\t\tconst auto hasActiveAuction = ranges::any_of(\n\t\t\t\t\t\tusernames,\n\t\t\t\t\t\t[](const Data::Username &u) {\n\t\t\t\t\t\t\treturn !u.editable && u.active;\n\t\t\t\t\t\t});\n\t\t\t\t\tif (!hasActiveAuction && username.active) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tauto text = _peer->isSelf()\n\t\t\t\t? (username.active\n\t\t\t\t\t? tr::lng_usernames_deactivate_description()\n\t\t\t\t\t: tr::lng_usernames_activate_description())\n\t\t\t\t: _isBot\n\t\t\t\t? (username.active\n\t\t\t\t\t? tr::lng_bot_usernames_deactivate_description()\n\t\t\t\t\t: tr::lng_bot_usernames_activate_description())\n\t\t\t\t: (username.active\n\t\t\t\t\t? tr::lng_channel_usernames_deactivate_description()\n\t\t\t\t\t: tr::lng_channel_usernames_activate_description());\n\n\t\t\tauto confirmText = username.active\n\t\t\t\t? tr::lng_usernames_deactivate_confirm()\n\t\t\t\t: tr::lng_usernames_activate_confirm();\n\n\t\t\tauto args = Ui::ConfirmBoxArgs{\n\t\t\t\t.text = std::move(text),\n\t\t\t\t.confirmed = crl::guard(this, [=](Fn<void()> close) {\n\t\t\t\t\tauto &api = _peer->session().api();\n\t\t\t\t\t_toggleLifetime = api.usernames().reorder(\n\t\t\t\t\t\t_peer,\n\t\t\t\t\t\torder()\n\t\t\t\t\t) | rpl::on_done([=] {\n\t\t\t\t\t\tauto &api = _peer->session().api();\n\t\t\t\t\t\t_toggleLifetime = api.usernames().toggle(\n\t\t\t\t\t\t\t_peer,\n\t\t\t\t\t\t\tusername.username,\n\t\t\t\t\t\t\t!username.active\n\t\t\t\t\t\t) | rpl::on_error_done([=](\n\t\t\t\t\t\t\t\tApi::Usernames::Error error) {\n\t\t\t\t\t\t\tif (error == Api::Usernames::Error::TooMuch) {\n\t\t\t\t\t\t\t\tconstexpr auto kMaxUsernames = 10.;\n\t\t\t\t\t\t\t\t_show->showBox(\n\t\t\t\t\t\t\t\t\tUi::MakeInformBox(\n\t\t\t\t\t\t\t\t\t\ttr::lng_usernames_activate_error(\n\t\t\t\t\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\t\t\t\t\trpl::single(kMaxUsernames),\n\t\t\t\t\t\t\t\t\t\t\ttr::rich)));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (error == Api::Usernames::Error::Flood) {\n\t\t\t\t\t\t\t\t_show->showToast(\n\t\t\t\t\t\t\t\t\ttr::lng_flood_error(tr::now));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tload();\n\t\t\t\t\t\t\t_toggleLifetime.destroy();\n\t\t\t\t\t\t}, [=] {\n\t\t\t\t\t\t\t_toggleLifetime.destroy();\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t\tclose();\n\t\t\t\t}),\n\t\t\t\t.confirmText = std::move(confirmText),\n\t\t\t};\n\t\t\t_show->showBox(Ui::MakeConfirmBox(std::move(args)));\n\t\t});\n\t}\n\n\t_reorder = std::make_unique<Ui::VerticalLayoutReorder>(content);\n\t_reorder->setMouseEventProxy([=](int i) {\n\t\treturn _rows[i]->rightAction();\n\t});\n\n\t{\n\t\tconst auto it = ranges::find_if(usernames, [&](\n\t\t\t\tconst Data::Username username) {\n\t\t\treturn !username.active;\n\t\t});\n\t\tif (it != end(usernames)) {\n\t\t\tconst auto from = std::distance(begin(usernames), it);\n\t\t\tconst auto length = std::distance(it, end(usernames));\n\t\t\t_reorder->addPinnedInterval(from, length);\n\t\t\tif (from == 1) {\n\t\t\t\t// Can't be reordered.\n\t\t\t\t_rows[0]->rightAction()->hide();\n\t\t\t}\n\t\t}\n\t}\n\t_reorder->start();\n\n\t_reorder->updates(\n\t) | rpl::on_next([=](Ui::VerticalLayoutReorder::Single data) {\n\t\tusing State = Ui::VerticalLayoutReorder::State;\n\t\tif (data.state == State::Started) {\n\t\t\t++_reordering;\n\t\t} else {\n\t\t\tUi::PostponeCall(content, [=] {\n\t\t\t\t--_reordering;\n\t\t\t});\n\t\t\tif (data.state == State::Applied) {\n\t\t\t\tbase::reorder(\n\t\t\t\t\t_rows,\n\t\t\t\t\tdata.oldPosition,\n\t\t\t\t\tdata.newPosition);\n\t\t\t}\n\t\t}\n\t}, content->lifetime());\n\n\t{\n\t\tUi::AddSkip(_container);\n\t\tUi::AddDividerText(\n\t\t\t_container,\n\t\t\t_peer->isSelf()\n\t\t\t\t? tr::lng_usernames_description()\n\t\t\t\t: _isBot\n\t\t\t\t? tr::lng_bot_usernames_description()\n\t\t\t\t: tr::lng_channel_usernames_description());\n\t}\n\n\tUi::ResizeFitChild(this, _container.get());\n\tcontent->show();\n\t_container->show();\n}\n\nstd::vector<QString> UsernamesList::order() const {\n\treturn ranges::views::all(\n\t\t_rows\n\t) | ranges::views::filter([](not_null<Row*> row) {\n\t\treturn row->username().active;\n\t}) | ranges::views::transform([](not_null<Row*> row) {\n\t\treturn row->username().username;\n\t}) | ranges::to_vector;\n}\n\nrpl::producer<> UsernamesList::save() {\n\treturn _peer->session().api().usernames().reorder(_peer, order());\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_peer_usernames_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n#include \"ui/rp_widget.h\"\n\nclass PeerData;\n\nnamespace Ui {\nclass VerticalLayout;\nclass VerticalLayoutReorder;\nclass Show;\n} // namespace Ui\n\nnamespace Data {\nstruct Username;\n} // namespace Data\n\nclass UsernamesList final : public Ui::RpWidget {\npublic:\n\tUsernamesList(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tFn<void()> focusCallback);\n\n\t[[nodiscard]] rpl::producer<> save();\n\t[[nodiscard]] std::vector<QString> order() const;\n\nprivate:\n\tvoid rebuild(const std::vector<Data::Username> &usernames);\n\tvoid load();\n\n\tclass Row;\n\n\tconst std::shared_ptr<Ui::Show> _show;\n\tconst not_null<PeerData*> _peer;\n\tconst bool _isBot = false;\n\tFn<void()> _focusCallback;\n\n\tbase::unique_qptr<Ui::VerticalLayout> _container;\n\tstd::unique_ptr<Ui::VerticalLayoutReorder> _reorder;\n\tstd::vector<Row*> _rows;\n\n\tint _reordering = 0;\n\n\trpl::lifetime _loadLifetime;\n\trpl::lifetime _toggleLifetime;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_tag_control.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/edit_tag_control.h\"\n\n#include \"base/unixtime.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/admin_log/history_admin_log_item.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_message.h\"\n#include \"history/view/media/history_view_media_generic.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"window/section_widget.h\"\n#include \"window/themes/window_theme.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n\nnamespace {\n\nconstexpr auto kRankLimit = 16;\nconstexpr auto kTextLinesAlpha = 0.1;\n\nusing namespace HistoryView;\n\nclass TextLinesPart final : public MediaGenericPart {\npublic:\n\tTextLinesPart(QMargins margins);\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const override;\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\nprivate:\n\tQMargins _margins;\n\n};\n\nTextLinesPart::TextLinesPart(QMargins margins)\n: _margins(margins) {\n}\n\nQSize TextLinesPart::countOptimalSize() {\n\tconst auto h = _margins.top()\n\t\t+ 4 * st::tagPreviewLineHeight\n\t\t+ 3 * st::tagPreviewLineSpacing\n\t\t+ _margins.bottom();\n\treturn { st::msgMinWidth, h };\n}\n\nQSize TextLinesPart::countCurrentSize(int newWidth) {\n\treturn { newWidth, minHeight() };\n}\n\nvoid TextLinesPart::draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const {\n\tconst auto &stm = context.messageStyle();\n\tauto color = stm->historyTextFg->c;\n\tcolor.setAlphaF(color.alphaF() * kTextLinesAlpha);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(color);\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tconst auto available = outerWidth - _margins.left() - _margins.right();\n\tconst auto lineHeight = st::tagPreviewLineHeight;\n\tconst auto radius = lineHeight / 2.0;\n\tconst auto fractions = { 1.0, 0.85, 0.65, 0.4 };\n\tauto y = double(_margins.top());\n\tfor (const auto fraction : fractions) {\n\t\tconst auto w = available * fraction;\n\t\tp.drawRoundedRect(\n\t\t\tQRectF(_margins.left(), y, w, lineHeight),\n\t\t\tradius,\n\t\t\tradius);\n\t\ty += lineHeight + st::tagPreviewLineSpacing;\n\t}\n}\n\nclass TagPreviewDelegate final : public DefaultElementDelegate {\npublic:\n\tTagPreviewDelegate(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Ui::ChatStyle*> st,\n\t\tFn<void()> update);\n\n\tvoid setTagText(const QString &text);\n\t[[nodiscard]] const QString &tagText() const;\n\n\tbool elementAnimationsPaused() override;\n\tnot_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;\n\tContext elementContext() override;\n\tQString elementAuthorRank(not_null<const Element*> view) override;\n\nprivate:\n\tconst not_null<QWidget*> _parent;\n\tconst std::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\tQString _tagText;\n\n};\n\nTagPreviewDelegate::TagPreviewDelegate(\n\tnot_null<QWidget*> parent,\n\tnot_null<Ui::ChatStyle*> st,\n\tFn<void()> update)\n: _parent(parent)\n, _pathGradient(MakePathShiftGradient(st, std::move(update))) {\n}\n\nvoid TagPreviewDelegate::setTagText(const QString &text) {\n\t_tagText = text;\n}\n\nconst QString &TagPreviewDelegate::tagText() const {\n\treturn _tagText;\n}\n\nbool TagPreviewDelegate::elementAnimationsPaused() {\n\treturn _parent->window()->isActiveWindow();\n}\n\nauto TagPreviewDelegate::elementPathShiftGradient()\n-> not_null<Ui::PathShiftGradient*> {\n\treturn _pathGradient.get();\n}\n\nContext TagPreviewDelegate::elementContext() {\n\treturn Context::AdminLog;\n}\n\nQString TagPreviewDelegate::elementAuthorRank(\n\t\tnot_null<const Element*> view) {\n\treturn _tagText;\n}\n\n} // namespace\n\nHistoryView::BadgeRole LookupBadgeRole(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user) {\n\tif (const auto channel = peer->asMegagroup()) {\n\t\tconst auto info = channel->mgInfo.get();\n\t\tif (info && info->creator == user) {\n\t\t\treturn HistoryView::BadgeRole::Creator;\n\t\t}\n\t\tconst auto userId = peerToUser(user->id);\n\t\tif (info && info->admins.contains(userId)) {\n\t\t\treturn HistoryView::BadgeRole::Admin;\n\t\t}\n\t} else if (const auto chat = peer->asChat()) {\n\t\tif (peerToUser(user->id) == chat->creator) {\n\t\t\treturn HistoryView::BadgeRole::Creator;\n\t\t} else if (chat->admins.contains(user)) {\n\t\t\treturn HistoryView::BadgeRole::Admin;\n\t\t}\n\t}\n\treturn HistoryView::BadgeRole::User;\n}\n\nclass EditTagControl::PreviewWidget final : public Ui::RpWidget {\npublic:\n\tPreviewWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<UserData*> user,\n\t\tconst QString &initialText,\n\t\tHistoryView::BadgeRole role);\n\t~PreviewWidget();\n\n\tvoid setTagText(const QString &text);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid createItem();\n\tvoid applyBadge(const QString &text);\n\n\tconst not_null<History*> _history;\n\tconst not_null<UserData*> _user;\n\tconst HistoryView::BadgeRole _role;\n\tconst std::unique_ptr<Ui::ChatTheme> _theme;\n\tconst std::unique_ptr<Ui::ChatStyle> _style;\n\tconst std::unique_ptr<TagPreviewDelegate> _delegate;\n\tAdminLog::OwnedItem _item;\n\tint _topSkip = 0;\n\tint _bottomSkip = 0;\n\tUi::PeerUserpicView _userpic;\n\n};\n\nEditTagControl::PreviewWidget::PreviewWidget(\n\tQWidget *parent,\n\tnot_null<Main::Session*> session,\n\tnot_null<UserData*> user,\n\tconst QString &initialText,\n\tHistoryView::BadgeRole role)\n: RpWidget(parent)\n, _history(session->data().history(PeerData::kServiceNotificationsId))\n, _user(user)\n, _role(role)\n, _theme(Window::Theme::DefaultChatThemeOn(lifetime()))\n, _style(std::make_unique<Ui::ChatStyle>(\n\tsession->colorIndicesValue()))\n, _delegate(std::make_unique<TagPreviewDelegate>(\n\tthis,\n\t_style.get(),\n\t[=] { update(); }))\n, _topSkip(st::msgMargin.bottom() * 2)\n, _bottomSkip(st::msgMargin.bottom() + st::msgMargin.top()) {\n\t_style->apply(_theme.get());\n\t_delegate->setTagText(initialText);\n\n\t_history->owner().viewRepaintRequest(\n\t) | rpl::on_next([=](Data::RequestViewRepaint data) {\n\t\tif (data.view == _item.get()) {\n\t\t\tupdate();\n\t\t}\n\t}, lifetime());\n\n\tcreateItem();\n\n\twidthValue(\n\t) | rpl::filter([=](int w) {\n\t\treturn w >= st::msgMinWidth;\n\t}) | rpl::on_next([=](int w) {\n\t\tconst auto h = _topSkip\n\t\t\t+ _item->resizeGetHeight(w)\n\t\t\t+ _bottomSkip;\n\t\tresize(w, h);\n\t}, lifetime());\n\n\t_history->owner().itemResizeRequest(\n\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\tif (_item && item == _item->data() && width() >= st::msgMinWidth) {\n\t\t\tconst auto h = _topSkip\n\t\t\t\t+ _item->resizeGetHeight(width())\n\t\t\t\t+ _bottomSkip;\n\t\t\tresize(width(), h);\n\t\t}\n\t}, lifetime());\n}\n\nEditTagControl::PreviewWidget::~PreviewWidget() {\n\t_item = {};\n}\n\nvoid EditTagControl::PreviewWidget::createItem() {\n\tconst auto item = _history->addNewLocalMessage({\n\t\t.id = _history->nextNonHistoryEntryId(),\n\t\t.flags = (MessageFlag::FakeHistoryItem\n\t\t\t| MessageFlag::HasFromId),\n\t\t.from = _user->id,\n\t\t.date = base::unixtime::now(),\n\t}, TextWithEntities(), MTP_messageMediaEmpty());\n\n\tauto owned = AdminLog::OwnedItem(_delegate.get(), item);\n\towned->overrideMedia(std::make_unique<HistoryView::MediaGeneric>(\n\t\towned.get(),\n\t\t[](not_null<HistoryView::MediaGeneric*>,\n\t\t\t\tFn<void(std::unique_ptr<HistoryView::MediaGenericPart>)> push) {\n\t\t\tpush(std::make_unique<TextLinesPart>(st::msgPadding));\n\t\t}));\n\t_item = std::move(owned);\n\tapplyBadge(_delegate->tagText());\n\tif (width() >= st::msgMinWidth) {\n\t\tconst auto h = _topSkip\n\t\t\t+ _item->resizeGetHeight(width())\n\t\t\t+ _bottomSkip;\n\t\tresize(width(), h);\n\t}\n}\n\nvoid EditTagControl::PreviewWidget::applyBadge(const QString &text) {\n\tif (!_item) {\n\t\treturn;\n\t}\n\tauto badgeText = text;\n\tif (badgeText.isEmpty()) {\n\t\tif (_role == HistoryView::BadgeRole::Admin) {\n\t\t\tbadgeText = tr::lng_admin_badge(tr::now);\n\t\t} else if (_role == HistoryView::BadgeRole::Creator) {\n\t\t\tbadgeText = tr::lng_owner_badge(tr::now);\n\t\t}\n\t}\n\t_item->overrideRightBadge(badgeText, _role);\n}\n\nvoid EditTagControl::PreviewWidget::setTagText(const QString &text) {\n\t_delegate->setTagText(text);\n\tapplyBadge(text);\n\tupdate();\n}\n\nvoid EditTagControl::PreviewWidget::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\n\tconst auto clip = e->rect();\n\tif (!clip.isEmpty()) {\n\t\tp.setClipRect(clip);\n\t\tWindow::SectionWidget::PaintBackground(\n\t\t\tp,\n\t\t\t_theme.get(),\n\t\t\tQSize(width(), window()->height()),\n\t\t\tclip);\n\t}\n\n\tauto context = _theme->preparePaintContext(\n\t\t_style.get(),\n\t\trect(),\n\t\trect(),\n\t\te->rect(),\n\t\t!window()->isActiveWindow());\n\tp.translate(0, _topSkip);\n\t_item->draw(p, context);\n\n\tif (_item->displayFromPhoto()) {\n\t\tauto userpicBottom = height()\n\t\t\t- _bottomSkip\n\t\t\t- _item->marginBottom()\n\t\t\t- _item->marginTop();\n\t\tconst auto userpicTop = userpicBottom - st::msgPhotoSize;\n\t\t_user->paintUserpicLeft(\n\t\t\tp,\n\t\t\t_userpic,\n\t\t\tst::historyPhotoLeft,\n\t\t\tuserpicTop,\n\t\t\twidth(),\n\t\t\tst::msgPhotoSize);\n\t}\n}\n\nEditTagControl::EditTagControl(\n\tQWidget *parent,\n\tnot_null<Main::Session*> session,\n\tnot_null<UserData*> user,\n\tconst QString &currentRank,\n\tHistoryView::BadgeRole role)\n: RpWidget(parent)\n, _preview(Ui::CreateChild<PreviewWidget>(\n\tthis,\n\tsession,\n\tuser,\n\tTextUtilities::RemoveEmoji(currentRank),\n\trole))\n, _field(Ui::CreateChild<Ui::InputField>(\n\tthis,\n\tst::customBadgeField,\n\t(role == HistoryView::BadgeRole::Admin\n\t\t? tr::lng_admin_badge()\n\t\t: role == HistoryView::BadgeRole::Creator\n\t\t? tr::lng_owner_badge()\n\t\t: tr::lng_rights_edit_admin_rank_name()),\n\tTextUtilities::RemoveEmoji(currentRank))) {\n\t_field->setMaxLength(kRankLimit);\n\t_field->setInstantReplaces(Ui::InstantReplaces::TextOnly());\n\n\t_field->changes(\n\t) | rpl::on_next([=] {\n\t\tconst auto text = _field->getLastText();\n\t\tconst auto removed = TextUtilities::RemoveEmoji(text);\n\t\tif (removed != text) {\n\t\t\t_field->setText(removed);\n\t\t}\n\t\t_preview->setTagText(_field->getLastText());\n\t}, _field->lifetime());\n\n\twidthValue(\n\t) | rpl::on_next([=](int w) {\n\t\t_preview->resizeToWidth(w);\n\t\tconst auto inputMargins = st::boxRowPadding;\n\t\t_field->resizeToWidth(w - inputMargins.left() - inputMargins.right());\n\t\t_field->moveToLeft(\n\t\t\tinputMargins.left(),\n\t\t\t_preview->height() + st::tagPreviewInputSkip);\n\t\tresize(w, _field->y() + _field->height());\n\t}, lifetime());\n\n\t_preview->heightValue(\n\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\t_field->moveToLeft(\n\t\t\tst::boxRowPadding.left(),\n\t\t\t_preview->height() + st::tagPreviewInputSkip);\n\t\tresize(width(), _field->y() + _field->height());\n\t}, lifetime());\n}\n\nEditTagControl::~EditTagControl() = default;\n\nQString EditTagControl::currentRank() const {\n\treturn TextUtilities::RemoveEmoji(\n\t\tTextUtilities::SingleLine(_field->getLastText().trimmed()));\n}\n\nnot_null<Ui::InputField*> EditTagControl::field() const {\n\treturn _field;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/edit_tag_control.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\nclass InputField;\n} // namespace Ui\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nclass PeerData;\nclass UserData;\n\nnamespace HistoryView {\nenum class BadgeRole : uchar;\n} // namespace HistoryView\n\n[[nodiscard]] HistoryView::BadgeRole LookupBadgeRole(\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> user);\n\nclass EditTagControl final : public Ui::RpWidget {\npublic:\n\tEditTagControl(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<UserData*> user,\n\t\tconst QString &currentRank,\n\t\tHistoryView::BadgeRole role);\n\t~EditTagControl();\n\n\t[[nodiscard]] QString currentRank() const;\n\t[[nodiscard]] not_null<Ui::InputField*> field() const;\n\nprivate:\n\tclass PreviewWidget;\n\n\tnot_null<PreviewWidget*> _preview;\n\tnot_null<Ui::InputField*> _field;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/peer_short_info_box.h\"\n\n#include \"base/event_filter.h\"\n#include \"core/application.h\"\n#include \"info/profile/info_profile_text.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"media/streaming/media_streaming_instance.h\"\n#include \"media/streaming/media_streaming_player.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/wrap.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace {\n\nusing MenuCallback = Ui::Menu::MenuCallback;\n\nconstexpr auto kShadowMaxAlpha = 80;\nconstexpr auto kInactiveBarOpacity = 0.5;\n\n} // namespace\n\nstruct PeerShortInfoCover::CustomLabelStyle {\n\texplicit CustomLabelStyle(const style::FlatLabel &original);\n\n\tstyle::complex_color textFg;\n\tstyle::FlatLabel st;\n\tfloat64 opacity = 1.;\n};\n\nstruct PeerShortInfoCover::Radial {\n\texplicit Radial(Fn<void()> &&callback);\n\n\tvoid toggle(bool visible);\n\n\tUi::RadialAnimation radial;\n\tUi::Animations::Simple shownAnimation;\n\tFn<void()> callback;\n\tbase::Timer showTimer;\n\tbool shown = false;\n};\n\nPeerShortInfoCover::Radial::Radial(Fn<void()> &&callback)\n: radial(callback)\n, callback(callback)\n, showTimer([=] { toggle(true); }) {\n}\n\nvoid PeerShortInfoCover::Radial::toggle(bool visible) {\n\tif (shown == visible) {\n\t\treturn;\n\t}\n\tshown = visible;\n\tshownAnimation.start(\n\t\tcallback,\n\t\tshown ? 0. : 1.,\n\t\tshown ? 1. : 0.,\n\t\tst::fadeWrapDuration);\n}\n\nPeerShortInfoCover::CustomLabelStyle::CustomLabelStyle(\n\tconst style::FlatLabel &original)\n: textFg([=, c = original.textFg]{\n\tauto result = c->c;\n\tresult.setAlphaF(result.alphaF() * opacity);\n\treturn result;\n})\n, st(original) {\n\tst.textFg = textFg.color();\n}\n\nPeerShortInfoCover::PeerShortInfoCover(\n\tnot_null<QWidget*> parent,\n\tconst style::ShortInfoCover &st,\n\trpl::producer<QString> name,\n\trpl::producer<QString> status,\n\trpl::producer<PeerShortInfoUserpic> userpic,\n\tFn<bool()> videoPaused)\n: _st(st)\n, _owned(parent.get())\n, _widget(_owned.data())\n, _nameStyle(std::make_unique<CustomLabelStyle>(_st.name))\n, _name(_widget.get(), std::move(name), _nameStyle->st)\n, _statusStyle(std::make_unique<CustomLabelStyle>(_st.status))\n, _status(_widget.get(), std::move(status), _statusStyle->st)\n, _roundMask(Images::CornersMask(_st.radius))\n, _roundMaskRetina(\n\tImages::CornersMask(_st.radius / style::DevicePixelRatio()))\n, _videoPaused(std::move(videoPaused)) {\n\t_widget->setCursor(_cursor);\n\n\t_widget->resize(_st.size, _st.size);\n\n\tstd::move(\n\t\tuserpic\n\t) | rpl::on_next([=](PeerShortInfoUserpic &&value) {\n\t\tapplyUserpic(std::move(value));\n\t\tapplyAdditionalStatus(value.additionalStatus);\n\t}, lifetime());\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\trefreshBarImages();\n\t}, lifetime());\n\n\t_widget->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(_widget.get());\n\t\tpaint(p);\n\t}, lifetime());\n\n\tbase::install_event_filter(_widget.get(), [=](not_null<QEvent*> e) {\n\t\tif (e->type() != QEvent::MouseButtonPress\n\t\t\t&& e->type() != QEvent::MouseButtonDblClick) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\t\tconst auto mouse = static_cast<QMouseEvent*>(e.get());\n\t\tconst auto x = mouse->pos().x();\n\t\tif (mouse->button() != Qt::LeftButton) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t} else if (/*_index > 0 && */x < _st.size / 3) {\n\t\t\t_moveRequests.fire(-1);\n\t\t} else if (/*_index + 1 < _count && */x >= _st.size / 3) {\n\t\t\t_moveRequests.fire(1);\n\t\t}\n\t\te->accept();\n\t\treturn base::EventFilterResult::Cancel;\n\t});\n\n\trefreshLabelsGeometry();\n\n\t_roundedTopImage = QImage(\n\t\tQSize(_st.size, _st.radius) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_roundedTopImage.setDevicePixelRatio(style::DevicePixelRatio());\n\t_roundedTopImage.fill(Qt::transparent);\n}\n\nPeerShortInfoCover::~PeerShortInfoCover() = default;\n\nnot_null<Ui::RpWidget*> PeerShortInfoCover::widget() const {\n\treturn _widget;\n}\n\nobject_ptr<Ui::RpWidget> PeerShortInfoCover::takeOwned() {\n\treturn std::move(_owned);\n}\n\ngsl::span<const QImage, 4> PeerShortInfoCover::roundMask() const {\n\treturn _roundMask;\n}\n\nvoid PeerShortInfoCover::setScrollTop(int scrollTop) {\n\t_scrollTop = scrollTop;\n\t_widget->update();\n}\n\nrpl::producer<int> PeerShortInfoCover::moveRequests() const {\n\treturn _moveRequests.events();\n}\n\nrpl::lifetime &PeerShortInfoCover::lifetime() {\n\treturn _widget->lifetime();\n}\n\nvoid PeerShortInfoCover::paint(QPainter &p) {\n\tcheckStreamedIsStarted();\n\tauto frame = currentVideoFrame();\n\tauto paused = _videoPaused && _videoPaused();\n\tif (!frame.isNull()) {\n\t\tframe = Images::Round(\n\t\t\tstd::move(frame),\n\t\t\t_roundMaskRetina,\n\t\t\tRectPart::TopLeft | RectPart::TopRight);\n\t} else if (_userpicImage.isNull()) {\n\t\tauto image = QImage(\n\t\t\t_widget->size() * style::DevicePixelRatio(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\timage.fill(Qt::black);\n\t\t_userpicImage = Images::Round(\n\t\t\tstd::move(image),\n\t\t\t_roundMask,\n\t\t\tRectPart::TopLeft | RectPart::TopRight);\n\t}\n\n\tpaintCoverImage(p, frame.isNull() ? _userpicImage : frame);\n\tpaintBars(p);\n\tpaintShadow(p);\n\tpaintRadial(p);\n\tif (_videoInstance && _videoInstance->ready() && !paused) {\n\t\t_videoInstance->markFrameShown();\n\t}\n}\n\nvoid PeerShortInfoCover::paintCoverImage(QPainter &p, const QImage &image) {\n\tconst auto roundedWidth = _st.size;\n\tconst auto roundedHeight = _st.radius;\n\tconst auto covered = (_st.size - _scrollTop);\n\tif (covered <= 0) {\n\t\treturn;\n\t} else if (!_scrollTop) {\n\t\tp.drawImage(_widget->rect(), image);\n\t\treturn;\n\t}\n\tconst auto fill = covered - roundedHeight;\n\tconst auto top = _widget->height() - fill;\n\tconst auto factor = style::DevicePixelRatio();\n\tif (fill > 0) {\n\t\tconst auto t = roundedHeight + _scrollTop;\n\t\tp.drawImage(\n\t\t\tQRect(0, t, roundedWidth, roundedWidth - t),\n\t\t\timage,\n\t\t\tQRect(\n\t\t\t\t0,\n\t\t\t\tt * factor,\n\t\t\t\troundedWidth * factor,\n\t\t\t\t(roundedWidth - t) * factor));\n\t}\n\tif (covered <= 0) {\n\t\treturn;\n\t}\n\tconst auto rounded = std::min(covered, roundedHeight);\n\tconst auto from = top - rounded;\n\tauto q = QPainter(&_roundedTopImage);\n\tq.drawImage(\n\t\tQRect(0, 0, roundedWidth, rounded),\n\t\timage,\n\t\tQRect(0, _scrollTop * factor, roundedWidth * factor, rounded * factor));\n\tq.end();\n\t_roundedTopImage = Images::Round(\n\t\tstd::move(_roundedTopImage),\n\t\t_roundMask,\n\t\tRectPart::TopLeft | RectPart::TopRight);\n\tp.drawImage(\n\t\tQRect(0, from, roundedWidth, rounded),\n\t\t_roundedTopImage,\n\t\tQRect(0, 0, roundedWidth * factor, rounded * factor));\n}\n\nvoid PeerShortInfoCover::paintBars(QPainter &p) {\n\tconst auto height = _st.linePadding * 2 + _st.line;\n\tconst auto factor = style::DevicePixelRatio();\n\tif (_shadowTop.isNull()) {\n\t\t_shadowTop = Images::GenerateShadow(height, kShadowMaxAlpha, 0);\n\t\t_shadowTop = Images::Round(\n\t\t\t_shadowTop.scaled(QSize(_st.size, height) * factor),\n\t\t\t_roundMask,\n\t\t\tRectPart::TopLeft | RectPart::TopRight);\n\t}\n\tconst auto shadowRect = QRect(0, _scrollTop, _st.size, height);\n\tp.drawImage(\n\t\tshadowRect,\n\t\t_shadowTop,\n\t\tQRect(0, 0, _shadowTop.width(), height * factor));\n\tconst auto hiddenAt = _st.size - _st.namePosition.y();\n\tif (!_smallWidth || _scrollTop >= hiddenAt) {\n\t\treturn;\n\t}\n\tconst auto start = _st.linePadding;\n\tconst auto y = _scrollTop + start;\n\tconst auto skip = _st.lineSkip;\n\tconst auto full = (_st.size - 2 * start - (_count - 1) * skip);\n\tconst auto single = full / float64(_count);\n\tconst auto masterOpacity = 1. - (_scrollTop / float64(hiddenAt));\n\tconst auto inactiveOpacity = masterOpacity * kInactiveBarOpacity;\n\tfor (auto i = 0; i != _count; ++i) {\n\t\tconst auto left = start + i * (single + skip);\n\t\tconst auto right = left + single;\n\t\tconst auto x = qRound(left);\n\t\tconst auto small = (qRound(right) == qRound(left) + _smallWidth);\n\t\tconst auto width = small ? _smallWidth : _largeWidth;\n\t\tconst auto &image = small ? _barSmall : _barLarge;\n\t\tconst auto min = 2 * ((_st.line + 1) / 2);\n\t\tconst auto minProgress = min / float64(width);\n\t\tconst auto videoProgress = (_videoInstance && _videoDuration > 0);\n\t\tconst auto progress = (i != _index)\n\t\t\t? 0.\n\t\t\t: videoProgress\n\t\t\t? std::max(_videoPosition / float64(_videoDuration), minProgress)\n\t\t\t: (_videoInstance ? 0. : 1.);\n\t\tif (progress == 1. && !videoProgress) {\n\t\t\tp.setOpacity(masterOpacity);\n\t\t\tp.drawImage(x, y, image);\n\t\t} else {\n\t\t\tp.setOpacity(inactiveOpacity);\n\t\t\tp.drawImage(x, y, image);\n\t\t\tif (progress > 0.) {\n\t\t\t\tconst auto paint = qRound(progress * width);\n\t\t\t\tconst auto right = paint / 2;\n\t\t\t\tconst auto left = paint - right;\n\t\t\t\tp.setOpacity(masterOpacity);\n\t\t\t\tp.drawImage(\n\t\t\t\t\tQRect(x, y, left, _st.line),\n\t\t\t\t\timage,\n\t\t\t\t\tQRect(0, 0, left * factor, image.height()));\n\t\t\t\tp.drawImage(\n\t\t\t\t\tQRect(x + left, y, right, _st.line),\n\t\t\t\t\timage,\n\t\t\t\t\tQRect(left * factor, 0, right * factor, image.height()));\n\t\t\t}\n\t\t}\n\t}\n\tp.setOpacity(1.);\n}\n\nvoid PeerShortInfoCover::paintShadow(QPainter &p) {\n\tif (_shadowBottom.isNull()) {\n\t\t_shadowBottom = Images::GenerateShadow(\n\t\t\t_st.shadowHeight,\n\t\t\t0,\n\t\t\tkShadowMaxAlpha);\n\t}\n\tconst auto shadowTop = _st.size - _st.shadowHeight;\n\tif (_scrollTop >= shadowTop) {\n\t\t_name->hide();\n\t\t_status->hide();\n\t\treturn;\n\t}\n\tconst auto opacity = 1. - (_scrollTop / float64(shadowTop));\n\t_nameStyle->opacity = opacity;\n\t_nameStyle->textFg.refresh();\n\t_name->show();\n\t_statusStyle->opacity = opacity;\n\t_statusStyle->textFg.refresh();\n\t_status->show();\n\tp.setOpacity(opacity);\n\tconst auto shadowRect = QRect(\n\t\t0,\n\t\tshadowTop,\n\t\t_st.size,\n\t\t_st.shadowHeight);\n\tconst auto factor = style::DevicePixelRatio();\n\tp.drawImage(\n\t\tshadowRect,\n\t\t_shadowBottom,\n\t\tQRect(\n\t\t\t0,\n\t\t\t0,\n\t\t\t_shadowBottom.width(),\n\t\t\t_st.shadowHeight * factor));\n\tp.setOpacity(1.);\n}\n\nvoid PeerShortInfoCover::paintRadial(QPainter &p) {\n\tconst auto infinite = _videoInstance && _videoInstance->waitingShown();\n\tif (!_radial && !infinite) {\n\t\treturn;\n\t}\n\tconst auto radial = radialRect();\n\tconst auto line = _st.radialAnimation.thickness;\n\tconst auto arc = radial.marginsRemoved(\n\t\t{ line, line, line, line });\n\tconst auto infiniteOpacity = _videoInstance\n\t\t? _videoInstance->waitingOpacity()\n\t\t: 0.;\n\tconst auto radialState = _radial\n\t\t? _radial->radial.computeState()\n\t\t: Ui::RadialState();\n\tif (_radial) {\n\t\tupdateRadialState();\n\t}\n\tconst auto radialOpacity = _radial\n\t\t? (_radial->shownAnimation.value(_radial->shown ? 1. : 0.)\n\t\t\t* radialState.shown)\n\t\t: 0.;\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setOpacity(std::max(infiniteOpacity, radialOpacity));\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(st::radialBg);\n\tp.drawEllipse(radial);\n\tif (radialOpacity > 0.) {\n\t\tp.setOpacity(radialOpacity);\n\t\tauto pen = _st.radialAnimation.color->p;\n\t\tpen.setWidth(line);\n\t\tpen.setCapStyle(Qt::RoundCap);\n\t\tp.setPen(pen);\n\t\tp.drawArc(arc, radialState.arcFrom, radialState.arcLength);\n\t}\n\tif (infinite) {\n\t\tp.setOpacity(1.);\n\t\tUi::InfiniteRadialAnimation::Draw(\n\t\t\tp,\n\t\t\t_videoInstance->waitingState(),\n\t\t\tarc.topLeft(),\n\t\t\tarc.size(),\n\t\t\t_st.size,\n\t\t\t_st.radialAnimation.color,\n\t\t\tline);\n\t}\n}\n\nQImage PeerShortInfoCover::currentVideoFrame() const {\n\tconst auto size = QSize(_st.size, _st.size);\n\tconst auto request = Media::Streaming::FrameRequest{\n\t\t.resize = size,\n\t\t.outer = size,\n\t};\n\treturn (_videoInstance\n\t\t&& _videoInstance->player().ready()\n\t\t&& !_videoInstance->player().videoSize().isEmpty())\n\t\t? _videoInstance->frame(request)\n\t\t: QImage();\n}\n\nvoid PeerShortInfoCover::applyAdditionalStatus(const QString &status) {\n\tif (status.isEmpty()) {\n\t\tif (_additionalStatus) {\n\t\t\t_additionalStatus.destroy();\n\t\t\trefreshLabelsGeometry();\n\t\t}\n\t\treturn;\n\t}\n\tif (_additionalStatus) {\n\t\t_additionalStatus->setText(status);\n\t} else {\n\t\t_additionalStatus.create(_widget.get(), status, _statusStyle->st);\n\t\t_additionalStatus->show();\n\t\trefreshLabelsGeometry();\n\t}\n}\n\nvoid PeerShortInfoCover::applyUserpic(PeerShortInfoUserpic &&value) {\n\tif (_index != value.index) {\n\t\t_index = value.index;\n\t\t_widget->update();\n\t}\n\tif (_count != value.count) {\n\t\t_count = value.count;\n\t\trefreshCoverCursor();\n\t\trefreshBarImages();\n\t\t_widget->update();\n\t}\n\tif (value.photo.isNull()) {\n\t\tconst auto videoChanged = _videoInstance\n\t\t\t? (_videoInstance->shared() != value.videoDocument)\n\t\t\t: (value.videoDocument != nullptr);\n\t\tauto frame = videoChanged ? currentVideoFrame() : QImage();\n\t\tif (!frame.isNull()) {\n\t\t\t_userpicImage = Images::Round(\n\t\t\t\tstd::move(frame),\n\t\t\t\t_roundMask,\n\t\t\t\tRectPart::TopLeft | RectPart::TopRight);\n\t\t}\n\t} else if (_userpicImage.cacheKey() != value.photo.cacheKey()) {\n\t\t_userpicImage = std::move(value.photo);\n\t\t_widget->update();\n\t}\n\tif (!value.videoDocument) {\n\t\tclearVideo();\n\t} else if (!_videoInstance\n\t\t|| _videoInstance->shared() != value.videoDocument) {\n\t\tusing namespace Media::Streaming;\n\t\t_videoInstance = std::make_unique<Instance>(\n\t\t\tstd::move(value.videoDocument),\n\t\t\t[=] { videoWaiting(); });\n\t\t_videoStartPosition = value.videoStartPosition;\n\t\t_videoInstance->lockPlayer();\n\t\t_videoInstance->player().updates(\n\t\t) | rpl::on_next_error([=](Update &&update) {\n\t\t\thandleStreamingUpdate(std::move(update));\n\t\t}, [=](Error &&error) {\n\t\t\thandleStreamingError(std::move(error));\n\t\t}, _videoInstance->lifetime());\n\t\tif (_videoInstance->ready()) {\n\t\t\tstreamingReady(base::duplicate(_videoInstance->info()));\n\t\t}\n\t\tif (!_videoInstance->valid()) {\n\t\t\tclearVideo();\n\t\t}\n\t}\n\t_photoLoadingProgress = value.photoLoadingProgress;\n\tupdateRadialState();\n}\n\nvoid PeerShortInfoCover::updateRadialState() {\n\tconst auto progress = _videoInstance ? 1. : _photoLoadingProgress;\n\tif (_radial) {\n\t\t_radial->radial.update(progress, (progress == 1.), crl::now());\n\t}\n\t_widget->update(radialRect());\n\n\tif (progress == 1.) {\n\t\tif (!_radial) {\n\t\t\treturn;\n\t\t}\n\t\t_radial->showTimer.cancel();\n\t\t_radial->toggle(false);\n\t\tif (!_radial->shownAnimation.animating()) {\n\t\t\t_radial = nullptr;\n\t\t}\n\t\treturn;\n\t} else if (!_radial) {\n\t\t_radial = std::make_unique<Radial>([=] { updateRadialState(); });\n\t\t_radial->radial.update(progress, false, crl::now());\n\t\t_radial->showTimer.callOnce(st::fadeWrapDuration);\n\t\treturn;\n\t} else if (!_radial->showTimer.isActive()) {\n\t\t_radial->toggle(true);\n\t}\n}\n\nvoid PeerShortInfoCover::clearVideo() {\n\t_videoInstance = nullptr;\n\t_videoStartPosition = _videoPosition = _videoDuration = 0;\n}\n\nvoid PeerShortInfoCover::checkStreamedIsStarted() {\n\tif (!_videoInstance) {\n\t\treturn;\n\t} else if (_videoInstance->paused()) {\n\t\t_videoInstance->resume();\n\t}\n\tif (!_videoInstance\n\t\t|| _videoInstance->active()\n\t\t|| _videoInstance->failed()) {\n\t\treturn;\n\t}\n\tauto options = Media::Streaming::PlaybackOptions();\n\toptions.position = _videoStartPosition;\n\toptions.mode = Media::Streaming::Mode::Video;\n\toptions.loop = true;\n\t_videoInstance->play(options);\n}\n\nvoid PeerShortInfoCover::handleStreamingUpdate(\n\t\tMedia::Streaming::Update &&update) {\n\tusing namespace Media::Streaming;\n\n\tv::match(update.data, [&](Information &update) {\n\t\tstreamingReady(std::move(update));\n\t}, [](PreloadedVideo) {\n\t}, [&](UpdateVideo update) {\n\t\t_videoPosition = update.position;\n\t\t_widget->update();\n\t}, [](PreloadedAudio) {\n\t}, [](UpdateAudio) {\n\t}, [](WaitingForData) {\n\t}, [](SpeedEstimate) {\n\t}, [](MutedByOther) {\n\t}, [](Finished) {\n\t});\n}\n\nvoid PeerShortInfoCover::handleStreamingError(\n\t\tMedia::Streaming::Error &&error) {\n\t//_streamedPhoto->setVideoPlaybackFailed();\n\t//_streamedPhoto = nullptr;\n\tclearVideo();\n}\n\nvoid PeerShortInfoCover::streamingReady(Media::Streaming::Information &&info) {\n\t_videoPosition = info.video.state.position;\n\t_videoDuration = info.video.state.duration;\n\t_widget->update();\n}\n\nvoid PeerShortInfoCover::refreshCoverCursor() {\n\tconst auto cursor = (_count > 1)\n\t\t? style::cur_pointer\n\t\t: style::cur_default;\n\tif (_cursor != cursor) {\n\t\t_cursor = cursor;\n\t\t_widget->setCursor(_cursor);\n\t}\n}\n\nvoid PeerShortInfoCover::refreshBarImages() {\n\tif (_count < 2) {\n\t\t_smallWidth = _largeWidth = 0;\n\t\t_barSmall = _barLarge = QImage();\n\t\treturn;\n\t}\n\tconst auto width = _st.size - 2 * _st.linePadding;\n\t_smallWidth = (width - (_count - 1) * _st.lineSkip) / _count;\n\tif (_smallWidth < _st.line) {\n\t\t_smallWidth = _largeWidth = 0;\n\t\t_barSmall = _barLarge = QImage();\n\t\treturn;\n\t}\n\t_largeWidth = _smallWidth + 1;\n\tconst auto makeBar = [&](int size) {\n\t\tconst auto radius = _st.line / 2.;\n\t\tauto result = QImage(\n\t\t\tQSize(size, _st.line) * style::DevicePixelRatio(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\t\tresult.fill(Qt::transparent);\n\t\tauto p = QPainter(&result);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::groupCallVideoTextFg);\n\t\tp.drawRoundedRect(0, 0, size, _st.line, radius, radius);\n\t\tp.end();\n\n\t\treturn result;\n\t};\n\t_barSmall = makeBar(_smallWidth);\n\t_barLarge = makeBar(_largeWidth);\n}\n\nvoid PeerShortInfoCover::refreshLabelsGeometry() {\n\tconst auto statusTop = _st.size\n\t\t- _st.statusPosition.y()\n\t\t- _status->height();\n\tconst auto diff = _st.namePosition.y()\n\t\t- _name->height()\n\t\t- _st.statusPosition.y();\n\tif (_additionalStatus) {\n\t\t_additionalStatus->moveToLeft(\n\t\t\t_status->x(),\n\t\t\tstatusTop - diff - _additionalStatus->height());\n\t}\n\t_name->moveToLeft(\n\t\t_st.namePosition.x(),\n\t\t_st.size\n\t\t\t- _st.namePosition.y()\n\t\t\t- _name->height()\n\t\t\t- (_additionalStatus ? (diff + _additionalStatus->height()) : 0),\n\t\t_st.size);\n\t_status->moveToLeft(_st.statusPosition.x(), statusTop, _st.size);\n}\n\nQRect PeerShortInfoCover::radialRect() const {\n\tconst auto cover = _widget->rect();\n\tconst auto size = st::boxLoadingSize;\n\treturn QRect(\n\t\tcover.x() + (cover.width() - size) / 2,\n\t\tcover.y() + (cover.height() - size) / 2,\n\t\tsize,\n\t\tsize);\n}\n\nvoid PeerShortInfoCover::videoWaiting() {\n\tif (!anim::Disabled()) {\n\t\t_widget->update(radialRect());\n\t}\n}\n\nPeerShortInfoBox::PeerShortInfoBox(\n\tQWidget*,\n\tPeerShortInfoType type,\n\trpl::producer<PeerShortInfoFields> fields,\n\trpl::producer<QString> status,\n\trpl::producer<PeerShortInfoUserpic> userpic,\n\tFn<bool()> videoPaused,\n\tconst style::ShortInfoBox *stOverride)\n: _st(stOverride ? *stOverride : st::shortInfoBox)\n, _type(type)\n, _fields(std::move(fields))\n, _topRoundBackground(this)\n, _scroll(this, st::shortInfoScroll)\n, _rows(\n\t_scroll->setOwnedWidget(\n\t\tobject_ptr<Ui::VerticalLayout>(\n\t\t\t_scroll.data())))\n, _cover(\n\t\t_rows.get(),\n\t\tst::shortInfoCover,\n\t\tnameValue(),\n\t\tstd::move(status),\n\t\tstd::move(userpic),\n\t\tstd::move(videoPaused)) {\n\t_rows->add(_cover.takeOwned());\n\n\t_scroll->scrolls(\n\t) | rpl::on_next([=] {\n\t\t_cover.setScrollTop(_scroll->scrollTop());\n\t}, _cover.lifetime());\n}\n\nPeerShortInfoBox::~PeerShortInfoBox() = default;\n\nrpl::producer<> PeerShortInfoBox::openRequests() const {\n\treturn _openRequests.events();\n}\n\nrpl::producer<int> PeerShortInfoBox::moveRequests() const {\n\treturn _cover.moveRequests();\n}\n\nvoid PeerShortInfoBox::prepare() {\n\taddButton(tr::lng_close(), [=] { closeBox(); });\n\n\tif (_type != PeerShortInfoType::Self) {\n\t\t// Perhaps a new lang key should be added for opening a group.\n\t\taddLeftButton(\n\t\t\t(_type == PeerShortInfoType::User)\n\t\t\t\t? tr::lng_profile_send_message()\n\t\t\t\t: (_type == PeerShortInfoType::Group)\n\t\t\t\t? tr::lng_view_button_group()\n\t\t\t\t: tr::lng_profile_view_channel(),\n\t\t\t[=] { _openRequests.fire({}); });\n\t}\n\n\tprepareRows();\n\n\tsetNoContentMargin(true);\n\n\t_topRoundBackground->resize(st::shortInfoWidth, st::boxRadius);\n\t_topRoundBackground->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tif (const auto use = fillRoundedTopHeight()) {\n\t\t\tconst auto width = _topRoundBackground->width();\n\t\t\tconst auto top = _topRoundBackground->height() - use;\n\t\t\tconst auto factor = style::DevicePixelRatio();\n\t\t\tQPainter(_topRoundBackground.data()).drawImage(\n\t\t\t\tQRect(0, top, width, use),\n\t\t\t\t_roundedTop,\n\t\t\t\tQRect(0, top * factor, width * factor, use * factor));\n\t\t}\n\t}, _topRoundBackground->lifetime());\n\n\t_roundedTop = QImage(\n\t\t_topRoundBackground->size() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_roundedTop.setDevicePixelRatio(style::DevicePixelRatio());\n\trefreshRoundedTopImage(getDelegate()->style().bg->c);\n\n\tsetCustomCornersFilling(RectPart::FullTop);\n\tsetDimensionsToContent(st::shortInfoWidth, _rows);\n}\n\nvoid PeerShortInfoBox::prepareRows() {\n\tusing namespace Info::Profile;\n\n\tauto addInfoLineGeneric = [&](\n\t\t\trpl::producer<QString> &&label,\n\t\t\trpl::producer<TextWithEntities> &&text,\n\t\t\tconst style::FlatLabel &textSt) {\n\t\tauto line = CreateTextWithLabel(\n\t\t\t_rows,\n\t\t\trpl::duplicate(label) | rpl::map(tr::marked),\n\t\t\trpl::duplicate(text),\n\t\t\t_st.label,\n\t\t\ttextSt,\n\t\t\tst::shortInfoLabeledPadding);\n\t\t_rows->add(object_ptr<Ui::OverrideMargins>(\n\t\t\t_rows.get(),\n\t\t\tstd::move(line.wrap)));\n\n\t\trpl::combine(\n\t\t\tstd::move(label),\n\t\t\tstd::move(text)\n\t\t) | rpl::on_next([=] {\n\t\t\t_rows->resizeToWidth(st::shortInfoWidth);\n\t\t}, _rows->lifetime());\n\n\t\t//line.text->setClickHandlerFilter(infoClickFilter);\n\t\treturn line.text;\n\t};\n\tauto addInfoLine = [&](\n\t\t\trpl::producer<QString> &&label,\n\t\t\trpl::producer<TextWithEntities> &&text,\n\t\t\tconst style::FlatLabel &textSt) {\n\t\treturn addInfoLineGeneric(\n\t\t\tstd::move(label),\n\t\t\tstd::move(text),\n\t\t\ttextSt);\n\t};\n\tauto addInfoOneLine = [&](\n\t\t\trpl::producer<QString> &&label,\n\t\t\trpl::producer<TextWithEntities> &&text,\n\t\t\tconst QString &contextCopyText) {\n\t\tauto result = addInfoLine(\n\t\t\tstd::move(label),\n\t\t\tstd::move(text),\n\t\t\t_st.labeledOneLine);\n\t\tresult->setDoubleClickSelectsParagraph(true);\n\t\tresult->setContextCopyText(contextCopyText);\n\t\treturn result;\n\t};\n\taddInfoOneLine(\n\t\ttr::lng_settings_channel_label(),\n\t\tchannelValue(),\n\t\ttr::lng_context_copy_link(tr::now));\n\taddInfoOneLine(\n\t\ttr::lng_info_link_label(),\n\t\tlinkValue(),\n\t\ttr::lng_context_copy_link(tr::now));\n\taddInfoOneLine(\n\t\ttr::lng_info_mobile_label(),\n\t\tphoneValue() | rpl::map(tr::marked),\n\t\ttr::lng_profile_copy_phone(tr::now));\n\tauto label = _fields.current().isBio\n\t\t? tr::lng_info_bio_label()\n\t\t: tr::lng_info_about_label();\n\taddInfoLine(std::move(label), aboutValue(), _st.labeled);\n\taddInfoOneLine(\n\t\ttr::lng_info_username_label(),\n\t\tusernameValue() | rpl::map(tr::marked),\n\t\ttr::lng_context_copy_mention(tr::now));\n\taddInfoOneLine(\n\t\tbirthdayLabel(),\n\t\tbirthdayValue() | rpl::map(tr::marked),\n\t\ttr::lng_mediaview_copy(tr::now));\n\taddInfoLine(\n\t\ttr::lng_info_notes_label(),\n\t\tnoteValue(),\n\t\t_st.labeled);\n}\n\nvoid PeerShortInfoBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\t_rows->resizeToWidth(st::shortInfoWidth);\n\t_scroll->resize(st::shortInfoWidth, height());\n\t_scroll->move(0, 0);\n\t_topRoundBackground->move(0, 0);\n}\n\nint PeerShortInfoBox::fillRoundedTopHeight() {\n\tconst auto roundedHeight = _topRoundBackground->height();\n\tconst auto scrollTop = _scroll->scrollTop();\n\tconst auto covered = (st::shortInfoWidth - scrollTop);\n\tif (covered >= roundedHeight) {\n\t\treturn 0;\n\t}\n\tconst auto &color = getDelegate()->style().bg->c;\n\tif (_roundedTopColor != color) {\n\t\trefreshRoundedTopImage(color);\n\t}\n\treturn roundedHeight - covered;\n}\n\nvoid PeerShortInfoBox::refreshRoundedTopImage(const QColor &color) {\n\t_roundedTopColor = color;\n\t_roundedTop.fill(color);\n\t_roundedTop = Images::Round(\n\t\tstd::move(_roundedTop),\n\t\t_cover.roundMask(),\n\t\tRectPart::TopLeft | RectPart::TopRight);\n}\n\nrpl::producer<MenuCallback> PeerShortInfoBox::fillMenuRequests() const {\n\treturn _fillMenuRequests.events();\n}\n\nvoid PeerShortInfoBox::contextMenuEvent(QContextMenuEvent *e) {\n\t_menuHolder = nullptr;\n\tconst auto menu = Ui::CreateChild<Ui::PopupMenu>(\n\t\tthis,\n\t\tst::popupMenuWithIcons);\n\t_fillMenuRequests.fire(Ui::Menu::CreateAddActionCallback(menu));\n\t_menuHolder.reset(menu);\n\tif (menu->empty()) {\n\t\t_menuHolder = nullptr;\n\t\treturn;\n\t}\n\tmenu->popup(e->globalPos());\n}\n\nrpl::producer<QString> PeerShortInfoBox::nameValue() const {\n\treturn _fields.value(\n\t) | rpl::map([](const PeerShortInfoFields &fields) {\n\t\treturn fields.name;\n\t}) | rpl::distinct_until_changed();\n}\n\nrpl::producer<TextWithEntities> PeerShortInfoBox::channelValue() const {\n\treturn _fields.value(\n\t) | rpl::map([](const PeerShortInfoFields &fields) {\n\t\treturn tr::link(fields.channelName, fields.channelLink);\n\t}) | rpl::distinct_until_changed();\n}\n\nrpl::producer<TextWithEntities> PeerShortInfoBox::linkValue() const {\n\treturn _fields.value(\n\t) | rpl::map([](const PeerShortInfoFields &fields) {\n\t\treturn tr::link(fields.link, fields.link);\n\t}) | rpl::distinct_until_changed();\n}\n\nrpl::producer<QString> PeerShortInfoBox::phoneValue() const {\n\treturn _fields.value(\n\t) | rpl::map([](const PeerShortInfoFields &fields) {\n\t\treturn fields.phone;\n\t}) | rpl::distinct_until_changed();\n}\n\nrpl::producer<QString> PeerShortInfoBox::usernameValue() const {\n\treturn _fields.value(\n\t) | rpl::map([](const PeerShortInfoFields &fields) {\n\t\treturn fields.username;\n\t}) | rpl::distinct_until_changed();\n}\n\nrpl::producer<QString> PeerShortInfoBox::birthdayLabel() const {\n\treturn Info::Profile::BirthdayLabelText(_fields.value(\n\t) | rpl::map([](const PeerShortInfoFields &fields) {\n\t\treturn fields.birthday;\n\t}) | rpl::distinct_until_changed());\n}\n\nrpl::producer<QString> PeerShortInfoBox::birthdayValue() const {\n\treturn Info::Profile::BirthdayValueText(_fields.value(\n\t) | rpl::map([](const PeerShortInfoFields &fields) {\n\t\treturn fields.birthday;\n\t}) | rpl::distinct_until_changed());\n}\n\nrpl::producer<TextWithEntities> PeerShortInfoBox::aboutValue() const {\n\treturn _fields.value() | rpl::map([](const PeerShortInfoFields &fields) {\n\t\treturn fields.about;\n\t}) | rpl::distinct_until_changed();\n}\n\nrpl::producer<TextWithEntities> PeerShortInfoBox::noteValue() const {\n\treturn _fields.value() | rpl::map([](const PeerShortInfoFields &fields) {\n\t\treturn fields.note;\n\t}) | rpl::distinct_until_changed();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/peer_short_info_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_birthday.h\"\n#include \"ui/layers/box_content.h\"\n\nnamespace style {\nstruct ShortInfoCover;\nstruct ShortInfoBox;\n} // namespace style\n\nnamespace Ui::Menu {\nstruct MenuCallback;\n} // namespace Ui::Menu\n\nnamespace Media::Streaming {\nclass Document;\nclass Instance;\nstruct Update;\nenum class Error;\nstruct Information;\n} // namespace Media::Streaming\n\nnamespace Ui {\nclass VerticalLayout;\nclass RpWidget;\n} // namespace Ui\n\nenum class PeerShortInfoType {\n\tSelf,\n\tUser,\n\tGroup,\n\tChannel,\n};\n\nstruct PeerShortInfoFields {\n\tQString name;\n\tQString channelName;\n\tQString channelLink;\n\tQString phone;\n\tQString link;\n\tTextWithEntities about;\n\tQString username;\n\tData::Birthday birthday;\n\tTextWithEntities note;\n\tbool isBio = false;\n};\n\nstruct PeerShortInfoUserpic {\n\tint index = 0;\n\tint count = 0;\n\n\tQImage photo;\n\tfloat64 photoLoadingProgress = 0.;\n\tstd::shared_ptr<Media::Streaming::Document> videoDocument;\n\tcrl::time videoStartPosition = 0;\n\tQString additionalStatus;\n};\n\nclass PeerShortInfoCover final {\npublic:\n\tPeerShortInfoCover(\n\t\tnot_null<QWidget*> parent,\n\t\tconst style::ShortInfoCover &st,\n\t\trpl::producer<QString> name,\n\t\trpl::producer<QString> status,\n\t\trpl::producer<PeerShortInfoUserpic> userpic,\n\t\tFn<bool()> videoPaused);\n\t~PeerShortInfoCover();\n\n\t[[nodiscard]] not_null<Ui::RpWidget*> widget() const;\n\t[[nodiscard]] object_ptr<Ui::RpWidget> takeOwned();\n\n\t[[nodiscard]] gsl::span<const QImage, 4> roundMask() const;\n\n\tvoid setScrollTop(int scrollTop);\n\n\t[[nodiscard]] rpl::producer<int> moveRequests() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tstruct CustomLabelStyle;\n\tstruct Radial;\n\n\tvoid paint(QPainter &p);\n\tvoid paintCoverImage(QPainter &p, const QImage &image);\n\tvoid paintBars(QPainter &p);\n\tvoid paintShadow(QPainter &p);\n\tvoid paintRadial(QPainter &p);\n\n\t[[nodiscard]] QImage currentVideoFrame() const;\n\n\tvoid applyUserpic(PeerShortInfoUserpic &&value);\n\tvoid applyAdditionalStatus(const QString &status);\n\t[[nodiscard]] QRect radialRect() const;\n\n\tvoid videoWaiting();\n\tvoid checkStreamedIsStarted();\n\tvoid handleStreamingUpdate(Media::Streaming::Update &&update);\n\tvoid handleStreamingError(Media::Streaming::Error &&error);\n\tvoid streamingReady(Media::Streaming::Information &&info);\n\tvoid clearVideo();\n\n\tvoid updateRadialState();\n\tvoid refreshCoverCursor();\n\tvoid refreshBarImages();\n\tvoid refreshLabelsGeometry();\n\n\tconst style::ShortInfoCover &_st;\n\n\tobject_ptr<Ui::RpWidget> _owned;\n\tconst not_null<Ui::RpWidget*> _widget;\n\tstd::unique_ptr<CustomLabelStyle> _nameStyle;\n\tobject_ptr<Ui::FlatLabel> _name;\n\tstd::unique_ptr<CustomLabelStyle> _statusStyle;\n\tobject_ptr<Ui::FlatLabel> _status;\n\tobject_ptr<Ui::FlatLabel> _additionalStatus = { nullptr };\n\n\tstd::array<QImage, 4> _roundMask;\n\tstd::array<QImage, 4> _roundMaskRetina;\n\tQImage _userpicImage;\n\tQImage _roundedTopImage;\n\tQImage _barSmall;\n\tQImage _barLarge;\n\tQImage _shadowTop;\n\tint _scrollTop = 0;\n\tint _smallWidth = 0;\n\tint _largeWidth = 0;\n\tint _index = 0;\n\tint _count = 0;\n\n\tstyle::cursor _cursor = style::cur_default;\n\n\tstd::unique_ptr<Media::Streaming::Instance> _videoInstance;\n\tcrl::time _videoStartPosition = 0;\n\tcrl::time _videoPosition = 0;\n\tcrl::time _videoDuration = 0;\n\tFn<bool()> _videoPaused;\n\tQImage _shadowBottom;\n\n\tstd::unique_ptr<Radial> _radial;\n\tfloat64 _photoLoadingProgress = 0.;\n\n\trpl::event_stream<int> _moveRequests;\n\n};\n\nclass PeerShortInfoBox final : public Ui::BoxContent {\npublic:\n\tPeerShortInfoBox(\n\t\tQWidget*,\n\t\tPeerShortInfoType type,\n\t\trpl::producer<PeerShortInfoFields> fields,\n\t\trpl::producer<QString> status,\n\t\trpl::producer<PeerShortInfoUserpic> userpic,\n\t\tFn<bool()> videoPaused,\n\t\tconst style::ShortInfoBox *stOverride);\n\t~PeerShortInfoBox();\n\n\t[[nodiscard]] rpl::producer<> openRequests() const;\n\t[[nodiscard]] rpl::producer<int> moveRequests() const;\n\t[[nodiscard]] auto fillMenuRequests() const\n\t-> rpl::producer<Ui::Menu::MenuCallback>;\n\nprotected:\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\nprivate:\n\tvoid prepare() override;\n\tvoid prepareRows();\n\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\tvoid refreshRoundedTopImage(const QColor &color);\n\tint fillRoundedTopHeight();\n\n\t[[nodiscard]] rpl::producer<QString> nameValue() const;\n\t[[nodiscard]] rpl::producer<TextWithEntities> channelValue() const;\n\t[[nodiscard]] rpl::producer<TextWithEntities> linkValue() const;\n\t[[nodiscard]] rpl::producer<QString> phoneValue() const;\n\t[[nodiscard]] rpl::producer<QString> usernameValue() const;\n\t[[nodiscard]] rpl::producer<QString> birthdayLabel() const;\n\t[[nodiscard]] rpl::producer<QString> birthdayValue() const;\n\t[[nodiscard]] rpl::producer<TextWithEntities> aboutValue() const;\n\t[[nodiscard]] rpl::producer<TextWithEntities> noteValue() const;\n\n\tconst style::ShortInfoBox &_st;\n\tconst PeerShortInfoType _type = PeerShortInfoType::User;\n\n\trpl::variable<PeerShortInfoFields> _fields;\n\n\tQColor _roundedTopColor;\n\tQImage _roundedTop;\n\n\tobject_ptr<Ui::RpWidget> _topRoundBackground;\n\tobject_ptr<Ui::ScrollArea> _scroll;\n\tnot_null<Ui::VerticalLayout*> _rows;\n\tPeerShortInfoCover _cover;\n\n\tbase::unique_qptr<Ui::RpWidget> _menuHolder;\n\trpl::event_stream<Ui::Menu::MenuCallback> _fillMenuRequests;\n\n\trpl::event_stream<> _openRequests;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/prepare_short_info_box.h\"\n\n#include \"base/unixtime.h\"\n#include \"boxes/peers/peer_short_info_box.h\"\n#include \"core/application.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_streaming.h\"\n#include \"data/data_user.h\"\n#include \"data/data_user_photos.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/delayed_activation.h\" // PreventDelayedActivation\n#include \"ui/text/format_values.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace {\n\nconstexpr auto kOverviewLimit = 48;\n\nstruct UserpicState {\n\tPeerShortInfoUserpic current;\n\tstd::optional<UserPhotosSlice> userSlice;\n\tPhotoId userpicPhotoId = PeerData::kUnknownPhotoId;\n\tUi::PeerUserpicView userpicView;\n\tstd::shared_ptr<Data::PhotoMedia> photoView;\n\tstd::vector<std::shared_ptr<Data::PhotoMedia>> photoPreloads;\n\tInMemoryKey userpicKey;\n\tPhotoId photoId = PeerData::kUnknownPhotoId;\n\tstd::array<QImage, 4> roundMask;\n\tint size = 0;\n\tbool waitingFull = false;\n\tbool waitingLoad = false;\n};\n\nvoid GenerateImage(\n\t\tnot_null<UserpicState*> state,\n\t\tQImage image,\n\t\tbool blurred = false) {\n\tusing namespace Images;\n\tconst auto size = state->size;\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto options = blurred ? Option::Blur : Option();\n\tstate->current.photo = Images::Round(\n\t\tImages::Prepare(\n\t\t\tstd::move(image),\n\t\t\tQSize(size, size) * ratio,\n\t\t\t{ .options = options, .outer = { size, size } }),\n\t\tstate->roundMask,\n\t\tRectPart::TopLeft | RectPart::TopRight);\n}\n\nvoid GenerateImage(\n\t\tnot_null<UserpicState*> state,\n\t\tnot_null<Image*> image,\n\t\tbool blurred = false) {\n\tGenerateImage(state, image->original(), blurred);\n}\n\nvoid ProcessUserpic(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserpicState*> state) {\n\tstate->current.videoDocument = nullptr;\n\tstate->userpicKey = peer->userpicUniqueKey(state->userpicView);\n\tif (!state->userpicView.cloud) {\n\t\tGenerateImage(\n\t\t\tstate,\n\t\t\tPeerData::GenerateUserpicImage(\n\t\t\t\tpeer,\n\t\t\t\tstate->userpicView,\n\t\t\t\tst::shortInfoWidth * style::DevicePixelRatio(),\n\t\t\t\t0),\n\t\t\tfalse);\n\t\tstate->current.photoLoadingProgress = 1.;\n\t\tstate->photoView = nullptr;\n\t\treturn;\n\t}\n\tpeer->loadUserpic();\n\tif (Ui::PeerUserpicLoading(state->userpicView)) {\n\t\tstate->current.photoLoadingProgress = 0.;\n\t\tstate->current.photo = QImage();\n\t\tstate->waitingLoad = true;\n\t\treturn;\n\t}\n\tGenerateImage(state, *state->userpicView.cloud, true);\n\tstate->current.photoLoadingProgress = peer->userpicPhotoId() ? 0. : 1.;\n\tstate->photoView = nullptr;\n}\n\nvoid Preload(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserpicState*> state) {\n\tauto taken = base::take(state->photoPreloads);\n\tif (state->userSlice && state->userSlice->size() > 0) {\n\t\tconst auto preload = [&](int index) {\n\t\t\tconst auto photo = peer->owner().photo(\n\t\t\t\t(*state->userSlice)[index]);\n\t\t\tconst auto current = (peer->userpicPhotoId() == photo->id);\n\t\t\tconst auto origin = current\n\t\t\t\t? peer->userpicPhotoOrigin()\n\t\t\t\t: Data::FileOriginUserPhoto(peerToUser(peer->id), photo->id);\n\t\t\tstate->photoPreloads.push_back(photo->createMediaView());\n\t\t\tif (photo->hasVideo()) {\n\t\t\t\tstate->photoPreloads.back()->videoWanted(\n\t\t\t\t\tData::PhotoSize::Large,\n\t\t\t\t\torigin);\n\t\t\t} else {\n\t\t\t\tstate->photoPreloads.back()->wanted(\n\t\t\t\t\tData::PhotoSize::Large,\n\t\t\t\t\torigin);\n\t\t\t}\n\t\t};\n\t\tconst auto skip = (state->userSlice->size() == state->current.count)\n\t\t\t? 0\n\t\t\t: 1;\n\t\tif (state->current.index - skip > 0) {\n\t\t\tpreload(state->current.index - skip - 1);\n\t\t} else if (!state->current.index && state->current.count > 1) {\n\t\t\tpreload(state->userSlice->size() - 1);\n\t\t}\n\t\tif (state->current.index - skip + 1 < state->userSlice->size()) {\n\t\t\tpreload(state->current.index - skip + 1);\n\t\t} else if (!skip && state->current.index > 0) {\n\t\t\tpreload(0);\n\t\t}\n\t}\n}\n\nvoid ProcessFullPhoto(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserpicState*> state,\n\t\tnot_null<PhotoData*> photo) {\n\tusing PhotoSize = Data::PhotoSize;\n\tconst auto current = (peer->userpicPhotoId() == photo->id);\n\tconst auto video = photo->hasVideo();\n\tconst auto originCurrent = peer->userpicPhotoOrigin();\n\tconst auto originOther = peer->isUser()\n\t\t? Data::FileOriginUserPhoto(peerToUser(peer->id), photo->id)\n\t\t: originCurrent;\n\tconst auto origin = current ? originCurrent : originOther;\n\tconst auto was = base::take(state->current.videoDocument);\n\tconst auto view = photo->createMediaView();\n\tif (!video) {\n\t\tview->wanted(PhotoSize::Large, origin);\n\t}\n\tif (const auto image = view->image(PhotoSize::Large)) {\n\t\tGenerateImage(state, image);\n\t\tPreload(peer, state);\n\t\tstate->photoView = nullptr;\n\t\tstate->current.photoLoadingProgress = 1.;\n\t} else {\n\t\tif (const auto thumbnail = view->image(PhotoSize::Thumbnail)) {\n\t\t\tGenerateImage(state, thumbnail, true);\n\t\t} else if (const auto small = view->image(PhotoSize::Small)) {\n\t\t\tGenerateImage(state, small, true);\n\t\t} else {\n\t\t\tif (current) {\n\t\t\t\tProcessUserpic(peer, state);\n\t\t\t}\n\t\t\tif (!current || state->current.photo.isNull()) {\n\t\t\t\tif (const auto blurred = view->thumbnailInline()) {\n\t\t\t\t\tGenerateImage(state, blurred, true);\n\t\t\t\t} else {\n\t\t\t\t\tstate->current.photo = QImage();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tstate->waitingLoad = !video;\n\t\tstate->photoView = view;\n\t\tstate->current.photoLoadingProgress = photo->progress();\n\t}\n\tif (!video) {\n\t\treturn;\n\t}\n\tstate->current.videoDocument = peer->owner().streaming().sharedDocument(\n\t\tphoto,\n\t\torigin);\n\tstate->current.videoStartPosition = photo->videoStartPosition();\n\tstate->photoView = nullptr;\n\tstate->current.photoLoadingProgress = 1.;\n}\n\n} // namespace\n\n[[nodiscard]] rpl::producer<PeerShortInfoFields> FieldsValue(\n\t\tnot_null<PeerData*> peer) {\n\tusing UpdateFlag = Data::PeerUpdate::Flag;\n\treturn peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\t(UpdateFlag::Name\n\t\t\t| UpdateFlag::PersonalChannel\n\t\t\t| UpdateFlag::PhoneNumber\n\t\t\t| UpdateFlag::Username\n\t\t\t| UpdateFlag::About\n\t\t\t| UpdateFlag::Birthday\n\t\t\t| UpdateFlag::ContactNote)\n\t) | rpl::map([=] {\n\t\tconst auto user = peer->asUser();\n\t\tconst auto username = peer->username();\n\t\tconst auto channelId = user ? user->personalChannelId() : 0;\n\t\tconst auto channel = channelId\n\t\t\t? user->owner().channel(channelId).get()\n\t\t\t: nullptr;\n\t\tconst auto channelUsername = channel\n\t\t\t? channel->username()\n\t\t\t: QString();\n\t\tconst auto hasChannel = !channelUsername.isEmpty();\n\t\treturn PeerShortInfoFields{\n\t\t\t.name = peer->name(),\n\t\t\t.channelName = hasChannel ? channel->name() : QString(),\n\t\t\t.channelLink = (hasChannel\n\t\t\t\t? channel->session().createInternalLinkFull(channelUsername)\n\t\t\t\t: QString()),\n\t\t\t.phone = user ? Ui::FormatPhone(user->phone()) : QString(),\n\t\t\t.link = ((user || username.isEmpty())\n\t\t\t\t? QString()\n\t\t\t\t: peer->session().createInternalLinkFull(username)),\n\t\t\t.about = Info::Profile::AboutWithEntities(peer, peer->about()),\n\t\t\t.username = ((user && !username.isEmpty())\n\t\t\t\t? ('@' + username)\n\t\t\t\t: QString()),\n\t\t\t.birthday = user ? user->birthday() : Data::Birthday(),\n\t\t\t.note = user ? user->note() : TextWithEntities(),\n\t\t\t.isBio = (user && !user->isBot()),\n\t\t};\n\t});\n}\n\n[[nodiscard]] rpl::producer<QString> StatusValue(not_null<PeerData*> peer) {\n\tif (const auto user = peer->asUser()) {\n\t\tconst auto now = base::unixtime::now();\n\t\treturn [=](auto consumer) {\n\t\t\tauto lifetime = rpl::lifetime();\n\t\t\tconst auto timer = lifetime.make_state<base::Timer>();\n\t\t\tconst auto push = [=] {\n\t\t\t\tconsumer.put_next(Data::OnlineText(user, now));\n\t\t\t\ttimer->callOnce(Data::OnlineChangeTimeout(user, now));\n\t\t\t};\n\t\t\ttimer->setCallback(push);\n\t\t\tpush();\n\t\t\treturn lifetime;\n\t\t};\n\t}\n\treturn peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::Members\n\t) | rpl::map([=] {\n\t\tconst auto chat = peer->asChat();\n\t\tconst auto channel = peer->asChannel();\n\t\tconst auto count = std::max({\n\t\t\tchat ? chat->count : channel->membersCount(),\n\t\t\tchat ? int(chat->participants.size()) : 0,\n\t\t\t0,\n\t\t});\n\t\treturn (chat && !chat->amIn())\n\t\t\t? tr::lng_chat_status_unaccessible(tr::now)\n\t\t\t: (count > 0)\n\t\t\t? ((channel && channel->isBroadcast())\n\t\t\t\t? tr::lng_chat_status_subscribers(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\tcount)\n\t\t\t\t: tr::lng_chat_status_members(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\tcount))\n\t\t\t: ((channel && channel->isBroadcast())\n\t\t\t\t? tr::lng_channel_status(tr::now)\n\t\t\t\t: tr::lng_group_status(tr::now));\n\t});\n}\n\nvoid ValidatePhotoId(\n\t\tnot_null<UserpicState*> state,\n\t\tPhotoId oldUserpicPhotoId) {\n\tif (state->userSlice) {\n\t\tconst auto count = state->userSlice->size();\n\t\tconst auto hasOld = state->userSlice->indexOf(\n\t\t\toldUserpicPhotoId).has_value();\n\t\tconst auto hasNew = state->userSlice->indexOf(\n\t\t\tstate->userpicPhotoId).has_value();\n\t\tconst auto shift = (hasNew ? 0 : 1);\n\t\tconst auto fullCount = count + shift;\n\t\tstate->current.count = fullCount;\n\t\tif (hasOld && !hasNew && state->current.index + 1 < fullCount) {\n\t\t\t++state->current.index;\n\t\t} else if (!hasOld && hasNew && state->current.index > 0) {\n\t\t\t--state->current.index;\n\t\t}\n\t\tconst auto index = state->current.index;\n\t\tif (!index || index >= fullCount) {\n\t\t\tstate->current.index = 0;\n\t\t\tstate->photoId = state->userpicPhotoId;\n\t\t} else {\n\t\t\tstate->photoId = (*state->userSlice)[index - shift];\n\t\t}\n\t} else {\n\t\tstate->photoId = state->userpicPhotoId;\n\t}\n}\n\nbool ProcessCurrent(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserpicState*> state) {\n\tconst auto userpicPhotoId = peer->userpicPhotoId();\n\tconst auto userpicPhoto = (userpicPhotoId\n\t\t&& (userpicPhotoId != PeerData::kUnknownPhotoId)\n\t\t&& (state->userpicPhotoId != userpicPhotoId))\n\t\t? peer->owner().photo(userpicPhotoId).get()\n\t\t: (state->photoId == userpicPhotoId && state->photoView)\n\t\t? state->photoView->owner().get()\n\t\t: nullptr;\n\tstate->waitingFull = (state->userpicPhotoId != userpicPhotoId)\n\t\t&& ((userpicPhotoId == PeerData::kUnknownPhotoId)\n\t\t\t|| (userpicPhotoId && userpicPhoto->isNull()));\n\tif (state->waitingFull) {\n\t\tpeer->updateFullForced();\n\t}\n\tconst auto oldUserpicPhotoId = state->waitingFull\n\t\t? state->userpicPhotoId\n\t\t: std::exchange(state->userpicPhotoId, userpicPhotoId);\n\tconst auto changedUserpic = (state->userpicKey\n\t\t!= peer->userpicUniqueKey(state->userpicView));\n\n\tconst auto wasIndex = state->current.index;\n\tconst auto wasCount = state->current.count;\n\tconst auto wasPhotoId = state->photoId;\n\tValidatePhotoId(state, oldUserpicPhotoId);\n\tconst auto changedInSlice = (state->current.index != wasIndex)\n\t\t|| (state->current.count != wasCount);\n\tconst auto changedPhotoId = (state->photoId != wasPhotoId);\n\tconst auto photo = (state->photoId == state->userpicPhotoId\n\t\t&& userpicPhoto)\n\t\t? userpicPhoto\n\t\t: (state->photoId\n\t\t\t&& (state->photoId != PeerData::kUnknownPhotoId)\n\t\t\t&& changedPhotoId)\n\t\t? peer->owner().photo(state->photoId).get()\n\t\t: state->photoView\n\t\t? state->photoView->owner().get()\n\t\t: nullptr;\n\tstate->current.additionalStatus = (!peer->isUser())\n\t\t? QString()\n\t\t: ((state->photoId == userpicPhotoId)\n\t\t\t&& peer->asUser()->hasPersonalPhoto())\n\t\t? tr::lng_profile_photo_by_you(tr::now)\n\t\t: ((state->current.index == (state->current.count - 1))\n\t\t\t&& SyncUserFallbackPhotoViewer(peer->asUser()) == state->photoId)\n\t\t? tr::lng_profile_public_photo(tr::now)\n\t\t: QString();\n\tstate->waitingLoad = false;\n\tif (!changedPhotoId\n\t\t&& (state->current.index > 0 || !changedUserpic)\n\t\t&& !state->photoView\n\t\t&& (!state->current.photo.isNull()\n\t\t\t|| state->current.videoDocument)) {\n\t\treturn changedInSlice;\n\t} else if (photo && !photo->isNull()) {\n\t\tProcessFullPhoto(peer, state, photo);\n\t} else if (state->current.index > 0) {\n\t\treturn changedInSlice;\n\t} else {\n\t\tProcessUserpic(peer, state);\n\t}\n\treturn true;\n}\n\n[[nodiscard]] PreparedShortInfoUserpic UserpicValue(\n\t\tnot_null<PeerData*> peer,\n\t\tconst style::ShortInfoCover &st,\n\t\trpl::producer<UserPhotosSlice> slices,\n\t\tFn<bool(not_null<UserpicState*>)> customProcess) {\n\tconst auto moveRequests = std::make_shared<rpl::event_stream<int>>();\n\tauto move = [=](int shift) {\n\t\tmoveRequests->fire_copy(shift);\n\t};\n\tconst auto size = st.size;\n\tconst auto radius = st.radius;\n\tauto value = [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tconst auto state = lifetime.make_state<UserpicState>();\n\t\tstate->size = size;\n\t\tstate->roundMask = Images::CornersMask(radius);\n\t\tconst auto push = [=](bool force = false) {\n\t\t\tif (customProcess(state) || force) {\n\t\t\t\tconsumer.put_next_copy(state->current);\n\t\t\t}\n\t\t};\n\t\tusing UpdateFlag = Data::PeerUpdate::Flag;\n\t\tpeer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tUpdateFlag::Photo | UpdateFlag::FullInfo\n\t\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\t\treturn (update.flags & UpdateFlag::Photo) || state->waitingFull;\n\t\t}) | rpl::on_next([=] {\n\t\t\tpush();\n\t\t}, lifetime);\n\n\t\trpl::duplicate(\n\t\t\tslices\n\t\t) | rpl::on_next([=](UserPhotosSlice &&slice) {\n\t\t\tstate->userSlice = std::move(slice);\n\t\t\tpush();\n\t\t}, lifetime);\n\n\t\tmoveRequests->events(\n\t\t) | rpl::filter([=] {\n\t\t\treturn (state->current.count > 1);\n\t\t}) | rpl::on_next([=](int shift) {\n\t\t\tstate->current.index = std::clamp(\n\t\t\t\t((state->current.index + shift + state->current.count)\n\t\t\t\t\t% state->current.count),\n\t\t\t\t0,\n\t\t\t\tstate->current.count - 1);\n\t\t\tpush(true);\n\t\t}, lifetime);\n\n\t\tpeer->session().downloaderTaskFinished(\n\t\t) | rpl::filter([=] {\n\t\t\treturn state->waitingLoad\n\t\t\t\t&& (state->photoView\n\t\t\t\t\t? (!!state->photoView->image(Data::PhotoSize::Large))\n\t\t\t\t\t: (!Ui::PeerUserpicLoading(state->userpicView)));\n\t\t}) | rpl::on_next([=] {\n\t\t\tpush();\n\t\t}, lifetime);\n\n\t\treturn lifetime;\n\t};\n\treturn { .value = std::move(value), .move = std::move(move) };\n}\n\nobject_ptr<Ui::BoxContent> PrepareShortInfoBox(\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> open,\n\t\tFn<bool()> videoPaused,\n\t\tFn<void(Ui::Menu::MenuCallback)> menuFiller,\n\t\tconst style::ShortInfoBox *stOverride) {\n\tconst auto type = peer->isSelf()\n\t\t? PeerShortInfoType::Self\n\t\t: peer->isUser()\n\t\t? PeerShortInfoType::User\n\t\t: peer->isBroadcast()\n\t\t? PeerShortInfoType::Channel\n\t\t: PeerShortInfoType::Group;\n\tauto userpic = PrepareShortInfoUserpic(peer, st::shortInfoCover);\n\tauto result = Box<PeerShortInfoBox>(\n\t\ttype,\n\t\tFieldsValue(peer),\n\t\tStatusValue(peer),\n\t\tstd::move(userpic.value),\n\t\tstd::move(videoPaused),\n\t\tstOverride);\n\n\tif (menuFiller) {\n\t\tresult->fillMenuRequests(\n\t\t) | rpl::on_next([=](Ui::Menu::MenuCallback callback) {\n\t\t\tmenuFiller(std::move(callback));\n\t\t}, result->lifetime());\n\t}\n\n\tresult->openRequests(\n\t) | rpl::on_next(open, result->lifetime());\n\n\tresult->moveRequests(\n\t) | rpl::on_next(userpic.move, result->lifetime());\n\n\treturn result;\n}\n\nobject_ptr<Ui::BoxContent> PrepareShortInfoBox(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst style::ShortInfoBox *stOverride) {\n\tconst auto open = [=] {\n\t\tif (const auto window = show->resolveWindow()) {\n\t\t\twindow->showPeerHistory(peer);\n\t\t}\n\t};\n\tconst auto videoIsPaused = [=] {\n\t\treturn show->paused(Window::GifPauseReason::Layer);\n\t};\n\tauto menuFiller = [=](Ui::Menu::MenuCallback addAction) {\n\t\tconst auto peerSeparateId = Window::SeparateId(peer);\n\t\tconst auto window = show->resolveWindow();\n\t\tif (window && window->windowId() != peerSeparateId) {\n\t\t\taddAction(tr::lng_context_new_window(tr::now), [=] {\n\t\t\t\tUi::PreventDelayedActivation();\n\t\t\t\twindow->showInNewWindow(peer);\n\t\t\t}, &st::menuIconNewWindow);\n\t\t}\n\t};\n\treturn PrepareShortInfoBox(\n\t\tpeer,\n\t\topen,\n\t\tvideoIsPaused,\n\t\tstd::move(menuFiller),\n\t\tstOverride);\n}\n\nobject_ptr<Ui::BoxContent> PrepareShortInfoBox(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tconst style::ShortInfoBox *stOverride) {\n\treturn PrepareShortInfoBox(peer, navigation->uiShow(), stOverride);\n}\n\nrpl::producer<QString> PrepareShortInfoStatus(not_null<PeerData*> peer) {\n\treturn StatusValue(peer);\n}\n\nPreparedShortInfoUserpic PrepareShortInfoUserpic(\n\t\tnot_null<PeerData*> peer,\n\t\tconst style::ShortInfoCover &st) {\n\tauto slices = peer->isUser()\n\t\t? UserPhotosReversedViewer(\n\t\t\t&peer->session(),\n\t\t\tUserPhotosSlice::Key(peerToUser(peer->asUser()->id), PhotoId()),\n\t\t\tkOverviewLimit,\n\t\t\tkOverviewLimit)\n\t\t: rpl::never<UserPhotosSlice>();\n\tauto process = [=](not_null<UserpicState*> state) {\n\t\treturn ProcessCurrent(peer, state);\n\t};\n\treturn UserpicValue(peer, st, std::move(slices), std::move(process));\n}\n\nPreparedShortInfoUserpic PrepareShortInfoFallbackUserpic(\n\t\tnot_null<PeerData*> peer,\n\t\tconst style::ShortInfoCover &st) {\n\tExpects(peer->isUser());\n\n\tconst auto photoId = SyncUserFallbackPhotoViewer(peer->asUser());\n\tauto slices = photoId\n\t\t? rpl::single<UserPhotosSlice>(UserPhotosSlice(\n\t\t\tStorage::UserPhotosKey(peerToUser(peer->id), *photoId),\n\t\t\tstd::deque<PhotoId>({ *photoId }),\n\t\t\t1,\n\t\t\t1,\n\t\t\t1))\n\t\t: (rpl::never<UserPhotosSlice>() | rpl::type_erased);\n\tauto process = [=](not_null<UserpicState*> state) {\n\t\tif (photoId) {\n\t\t\tProcessFullPhoto(peer, state, peer->owner().photo(*photoId));\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\treturn UserpicValue(peer, st, std::move(slices), std::move(process));\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nclass PeerData;\n\nnamespace style {\nstruct ShortInfoCover;\nstruct ShortInfoBox;\n} // namespace style\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Ui::Menu {\nstruct MenuCallback;\n} // namespace Ui::Menu\n\nnamespace Ui {\nclass BoxContent;\n} // namespace Ui\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nstruct PeerShortInfoUserpic;\n\nstruct PreparedShortInfoUserpic {\n\trpl::producer<PeerShortInfoUserpic> value;\n\tFn<void(int)> move;\n};\n\n[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShortInfoBox(\n\tnot_null<PeerData*> peer,\n\tFn<void()> open,\n\tFn<bool()> videoPaused,\n\tFn<void(Ui::Menu::MenuCallback)> menuFiller,\n\tconst style::ShortInfoBox *stOverride = nullptr);\n\n[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShortInfoBox(\n\tnot_null<PeerData*> peer,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst style::ShortInfoBox *stOverride = nullptr);\n\n[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShortInfoBox(\n\tnot_null<PeerData*> peer,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tconst style::ShortInfoBox *stOverride = nullptr);\n\n[[nodiscard]] rpl::producer<QString> PrepareShortInfoStatus(\n\tnot_null<PeerData*> peer);\n\n[[nodiscard]] PreparedShortInfoUserpic PrepareShortInfoUserpic(\n\tnot_null<PeerData*> peer,\n\tconst style::ShortInfoCover &st);\n\n[[nodiscard]] PreparedShortInfoUserpic PrepareShortInfoFallbackUserpic(\n\tnot_null<PeerData*> peer,\n\tconst style::ShortInfoCover &st);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/replace_boost_box.h\"\n\n#include \"api/api_peer_colors.h\"\n#include \"apiwrap.h\"\n#include \"base/event_filter.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"data/data_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"main/session/session_show.h\"\n#include \"ui/boxes/boost_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/painter.h\"\n#include \"ui/top_background_gradient.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_premium.h\"\n\nnamespace {\n\nconstexpr auto kWaitingOpacity = 0.5;\n\nclass Row final : public PeerListRow {\npublic:\n\tRow(\n\t\tnot_null<Main::Session*> session,\n\t\tTakenBoostSlot slot,\n\t\tTimeId unixtimeNow,\n\t\tcrl::time preciseNow);\n\n\tvoid updateStatus(TimeId unixtimeNow, crl::time preciseNow);\n\t[[nodiscard]] TakenBoostSlot data() const {\n\t\treturn _data;\n\t}\n\t[[nodiscard]] bool waiting() const {\n\t\treturn _waiting;\n\t}\n\n\tQString generateName() override;\n\tQString generateShortName() override;\n\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\tfloat64 opacity() override;\n\nprivate:\n\t[[nodiscard]]PaintRoundImageCallback peerPaintUserpicCallback();\n\n\tTakenBoostSlot _data;\n\tPeerData *_peer = nullptr;\n\tstd::shared_ptr<Ui::EmptyUserpic> _empty;\n\tUi::PeerUserpicView _userpic;\n\tcrl::time _startPreciseTime = 0;\n\tTimeId _startUnixtime = 0;\n\tbool _waiting = false;\n\n};\n\nclass Controller final : public PeerListController {\npublic:\n\tController(not_null<ChannelData*> to, std::vector<TakenBoostSlot> from);\n\n\t[[nodiscard]] rpl::producer<std::vector<int>> selectedValue() const {\n\t\treturn _selected.value();\n\t}\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tbool trackSelectedList() override {\n\t\treturn false;\n\t}\n\nprivate:\n\tvoid updateWaitingState();\n\n\tnot_null<ChannelData*> _to;\n\tstd::vector<TakenBoostSlot> _from;\n\trpl::variable<std::vector<int>> _selected;\n\trpl::variable<std::vector<not_null<PeerData*>>> _selectedPeers;\n\tbase::Timer _waitingTimer;\n\tbool _hasWaitingRows = false;\n\n};\n\nRow::Row(\n\tnot_null<Main::Session*> session,\n\tTakenBoostSlot slot,\n\tTimeId unixtimeNow,\n\tcrl::time preciseNow)\n: PeerListRow(PeerListRowId(slot.id))\n, _data(slot)\n, _peer(session->data().peerLoaded(_data.peerId))\n, _startPreciseTime(preciseNow)\n, _startUnixtime(unixtimeNow) {\n\tupdateStatus(unixtimeNow, preciseNow);\n}\n\nvoid Row::updateStatus(TimeId unixtimeNow, crl::time preciseNow) {\n\t_waiting = (_data.cooldown > unixtimeNow);\n\tif (_waiting) {\n\t\tconst auto initial = crl::time(_data.cooldown - _startUnixtime);\n\t\tconst auto elapsed = (preciseNow + 500 - _startPreciseTime) / 1000;\n\t\tconst auto seconds = initial\n\t\t\t- std::clamp(elapsed, crl::time(), initial);\n\t\tconst auto hours = seconds / 3600;\n\t\tconst auto minutes = seconds / 60;\n\t\tconst auto duration = (hours > 0)\n\t\t\t? u\"%1:%2:%3\"_q.arg(\n\t\t\t\thours\n\t\t\t).arg(minutes % 60, 2, 10, QChar('0')\n\t\t\t).arg(seconds % 60, 2, 10, QChar('0'))\n\t\t\t: u\"%1:%2\"_q.arg(\n\t\t\t\tminutes\n\t\t\t).arg(seconds % 60, 2, 10, QChar('0'));\n\t\tsetCustomStatus(\n\t\t\ttr::lng_boost_available_in(tr::now, lt_duration, duration));\n\t} else {\n\t\tconst auto date = base::unixtime::parse(_data.expires);\n\t\tsetCustomStatus(tr::lng_boosts_list_status(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\tlangDayOfMonth(date.date())));\n\t}\n}\n\nQString Row::generateName() {\n\treturn _peer ? _peer->name() : u\" \"_q;\n}\n\nQString Row::generateShortName() {\n\treturn _peer ? _peer->shortName() : generateName();\n}\n\nPaintRoundImageCallback Row::generatePaintUserpicCallback(\n\t\tbool forceRound) {\n\tif (_peer) {\n\t\treturn (forceRound && _peer->isForum())\n\t\t\t? ForceRoundUserpicCallback(_peer)\n\t\t\t: peerPaintUserpicCallback();\n\t} else if (!_empty) {\n\t\tconst auto colorIndex = _data.id % Ui::kColorIndexCount;\n\t\t_empty = std::make_shared<Ui::EmptyUserpic>(\n\t\t\tUi::EmptyUserpic::UserpicColor(colorIndex),\n\t\t\tu\" \"_q);\n\t}\n\tconst auto empty = _empty;\n\treturn [=](Painter &p, int x, int y, int outerWidth, int size) {\n\t\tempty->paintCircle(p, x, y, outerWidth, size);\n\t};\n}\n\nfloat64 Row::opacity() {\n\treturn _waiting ? kWaitingOpacity : 1.;\n}\n\nPaintRoundImageCallback Row::peerPaintUserpicCallback() {\n\tconst auto peer = _peer;\n\tif (!_userpic.cloud && peer->hasUserpic()) {\n\t\t_userpic = peer->createUserpicView();\n\t}\n\tauto userpic = _userpic;\n\treturn [=](Painter &p, int x, int y, int outerWidth, int size) mutable {\n\t\tpeer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);\n\t};\n}\n\nController::Controller(\n\tnot_null<ChannelData*> to,\n\tstd::vector<TakenBoostSlot> from)\n: _to(to)\n, _from(std::move(from))\n, _waitingTimer([=] { updateWaitingState(); }) {\n}\n\nMain::Session &Controller::session() const {\n\treturn _to->session();\n}\n\nvoid Controller::prepare() {\n\tdelegate()->peerListSetTitle(tr::lng_boost_reassign_title());\n\n\tconst auto session = &_to->session();\n\tauto above = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tabove->add(\n\t\tCreateUserpicsTransfer(\n\t\t\tabove.data(),\n\t\t\t_selectedPeers.value(),\n\t\t\t_to,\n\t\t\tUserpicsTransferType::BoostReplace),\n\t\tst::boxRowPadding + st::boostReplaceUserpicsPadding);\n\tabove->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tabove.data(),\n\t\t\ttr::lng_boost_reassign_text(\n\t\t\t\tlt_channel,\n\t\t\t\trpl::single(tr::bold(_to->name())),\n\t\t\t\tlt_gift,\n\t\t\t\ttr::lng_boost_reassign_gift(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(1. * BoostsForGift(session)),\n\t\t\t\t\ttr::rich),\n\t\t\t\ttr::rich),\n\t\t\tst::boostReassignText),\n\t\tst::boxRowPadding,\n\t\tstyle::al_top);\n\tdelegate()->peerListSetAboveWidget(std::move(above));\n\n\tconst auto now = base::unixtime::now();\n\tconst auto precise = crl::now();\n\tranges::stable_sort(_from, ranges::less(), [&](TakenBoostSlot slot) {\n\t\treturn (slot.cooldown > now) ? slot.cooldown : -slot.cooldown;\n\t});\n\tfor (const auto &slot : _from) {\n\t\tauto row = std::make_unique<Row>(session, slot, now, precise);\n\t\tif (row->waiting()) {\n\t\t\t_hasWaitingRows = true;\n\t\t}\n\t\tdelegate()->peerListAppendRow(std::move(row));\n\t}\n\n\tif (_hasWaitingRows) {\n\t\t_waitingTimer.callEach(1000);\n\t}\n\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid Controller::updateWaitingState() {\n\t_hasWaitingRows = false;\n\tconst auto now = base::unixtime::now();\n\tconst auto precise = crl::now();\n\tconst auto count = delegate()->peerListFullRowsCount();\n\tfor (auto i = 0; i != count; ++i) {\n\t\tconst auto bare = delegate()->peerListRowAt(i);\n\t\tconst auto row = static_cast<Row*>(bare.get());\n\t\tif (row->waiting()) {\n\t\t\trow->updateStatus(now, precise);\n\t\t\tdelegate()->peerListUpdateRow(row);\n\t\t\tif (row->waiting()) {\n\t\t\t\t_hasWaitingRows = true;\n\t\t\t}\n\t\t}\n\t}\n\tif (!_hasWaitingRows) {\n\t\t_waitingTimer.cancel();\n\t}\n}\n\nvoid Controller::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto slot = static_cast<Row*>(row.get())->data();\n\tif (slot.cooldown > base::unixtime::now()) {\n\t\tdelegate()->peerListUiShow()->showToast({\n\t\t\t.text = tr::lng_boost_available_in_toast(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tBoostsForGift(&session()),\n\t\t\t\ttr::rich),\n\t\t\t.adaptive = true,\n\t\t});\n\t\treturn;\n\t}\n\tauto now = _selected.current();\n\tconst auto id = slot.id;\n\tconst auto checked = !row->checked();\n\tdelegate()->peerListSetRowChecked(row, checked);\n\tconst auto peer = slot.peerId\n\t\t? _to->owner().peerLoaded(slot.peerId)\n\t\t: nullptr;\n\tauto peerRemoved = false;\n\tif (checked) {\n\t\tnow.push_back(id);\n\t} else {\n\t\tnow.erase(ranges::remove(now, id), end(now));\n\n\t\tpeerRemoved = true;\n\t\tfor (const auto left : now) {\n\t\t\tconst auto i = ranges::find(_from, left, &TakenBoostSlot::id);\n\t\t\tAssert(i != end(_from));\n\t\t\tif (i->peerId == slot.peerId) {\n\t\t\t\tpeerRemoved = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\t_selected = std::move(now);\n\n\tif (peer) {\n\t\tauto selectedPeers = _selectedPeers.current();\n\t\tconst auto i = ranges::find(selectedPeers, not_null(peer));\n\t\tif (peerRemoved) {\n\t\t\tAssert(i != end(selectedPeers));\n\t\t\tselectedPeers.erase(i);\n\t\t\t_selectedPeers = std::move(selectedPeers);\n\t\t} else if (i == end(selectedPeers) && checked) {\n\t\t\tselectedPeers.insert(begin(selectedPeers), peer);\n\t\t\t_selectedPeers = std::move(selectedPeers);\n\t\t}\n\t}\n}\n\nobject_ptr<Ui::BoxContent> ReassignBoostFloodBox(int seconds, bool group) {\n\tconst auto days = seconds / 86400;\n\tconst auto hours = seconds / 3600;\n\tconst auto minutes = seconds / 60;\n\treturn Ui::MakeInformBox({\n\t\t.text = (group\n\t\t\t? tr::lng_boost_error_flood_text_group\n\t\t\t: tr::lng_boost_error_flood_text)(\n\t\t\t\tlt_left,\n\t\t\t\trpl::single(tr::bold((days > 1)\n\t\t\t\t\t? tr::lng_days(tr::now, lt_count, days)\n\t\t\t\t\t: (hours > 1)\n\t\t\t\t\t? tr::lng_hours(tr::now, lt_count, hours)\n\t\t\t\t\t: (minutes > 1)\n\t\t\t\t\t? tr::lng_minutes(tr::now, lt_count, minutes)\n\t\t\t\t\t: tr::lng_seconds(tr::now, lt_count, seconds))),\n\t\t\t\ttr::rich),\n\t\t.title = tr::lng_boost_error_flood_title(),\n\t});\n}\n\nobject_ptr<Ui::BoxContent> ReassignBoostSingleBox(\n\t\tnot_null<ChannelData*> to,\n\t\tTakenBoostSlot from,\n\t\tFn<void(std::vector<int> slots, int groups, int channels)> reassign,\n\t\tFn<void()> cancel) {\n\tconst auto reassigned = std::make_shared<bool>();\n\tconst auto slot = from.id;\n\tconst auto peer = to->owner().peer(from.peerId);\n\tconst auto group = peer->isMegagroup();\n\tconst auto confirmed = [=](Fn<void()> close) {\n\t\t*reassigned = true;\n\t\treassign({ slot }, group ? 1 : 0, group ? 0 : 1);\n\t\tclose();\n\t};\n\n\tauto result = Box([=](not_null<Ui::GenericBox*> box) {\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = tr::lng_boost_now_instead(\n\t\t\t\tlt_channel,\n\t\t\t\trpl::single(tr::bold(peer->name())),\n\t\t\t\tlt_other,\n\t\t\t\trpl::single(tr::bold(to->name())),\n\t\t\t\ttr::marked),\n\t\t\t.confirmed = confirmed,\n\t\t\t.confirmText = tr::lng_boost_now_replace(),\n\t\t\t.labelPadding = st::boxRowPadding,\n\t\t});\n\t\tbox->verticalLayout()->insert(\n\t\t\t0,\n\t\t\tCreateUserpicsTransfer(\n\t\t\t\tbox,\n\t\t\t\trpl::single(std::vector{ peer }),\n\t\t\t\tto,\n\t\t\t\tUserpicsTransferType::BoostReplace),\n\t\t\tst::boxRowPadding + st::boostReplaceUserpicsPadding);\n\t});\n\n\tresult->boxClosing() | rpl::filter([=] {\n\t\treturn !*reassigned;\n\t}) | rpl::on_next(cancel, result->lifetime());\n\n\treturn result;\n}\n\n} // namespace\n\nForChannelBoostSlots ParseForChannelBoostSlots(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst QVector<MTPMyBoost> &boosts) {\n\tauto result = ForChannelBoostSlots();\n\tconst auto now = base::unixtime::now();\n\tfor (const auto &my : boosts) {\n\t\tconst auto &data = my.data();\n\t\tconst auto id = data.vslot().v;\n\t\tconst auto cooldown = data.vcooldown_until_date().value_or(0);\n\t\tconst auto peerId = data.vpeer()\n\t\t\t? peerFromMTP(*data.vpeer())\n\t\t\t: PeerId();\n\t\tif (!peerId && cooldown <= now) {\n\t\t\tresult.free.push_back(id);\n\t\t} else if (peerId == channel->id) {\n\t\t\tresult.already.push_back(id);\n\t\t} else {\n\t\t\tresult.other.push_back({\n\t\t\t\t.id = id,\n\t\t\t\t.expires = data.vexpires().v,\n\t\t\t\t.peerId = peerId,\n\t\t\t\t.cooldown = cooldown,\n\t\t\t});\n\t\t}\n\t}\n\treturn result;\n}\n\nUi::BoostCounters ParseBoostCounters(\n\t\tconst MTPpremium_BoostsStatus &status) {\n\tconst auto &data = status.data();\n\tconst auto slots = data.vmy_boost_slots();\n\treturn {\n\t\t.level = data.vlevel().v,\n\t\t.boosts = data.vboosts().v,\n\t\t.thisLevelBoosts = data.vcurrent_level_boosts().v,\n\t\t.nextLevelBoosts = data.vnext_level_boosts().value_or_empty(),\n\t\t.mine = slots ? int(slots->v.size()) : 0,\n\t};\n}\n\nUi::BoostFeatures LookupBoostFeatures(not_null<ChannelData*> channel) {\n\tauto nameColorsByLevel = base::flat_map<int, int>();\n\tauto linkStylesByLevel = base::flat_map<int, int>();\n\tauto profileColorsByLevel = base::flat_map<int, int>();\n\tconst auto group = channel->isMegagroup();\n\tconst auto peerColors = &channel->session().api().peerColors();\n\tconst auto &list = group\n\t\t? peerColors->requiredLevelsGroup()\n\t\t: peerColors->requiredLevelsChannel();\n\tconst auto indices = peerColors->indicesCurrent();\n\tfor (const auto &[index, level] : list) {\n\t\tif (!Ui::ColorPatternIndex(indices, index, false)) {\n\t\t\t++nameColorsByLevel[level];\n\t\t}\n\t\t++linkStylesByLevel[level];\n\t}\n\t{\n\t\tconst auto profileIndices = peerColors->profileColorIndices();\n\t\tauto lowestNonZeroLevel = std::numeric_limits<int>::max();\n\t\tauto levels = std::vector<int>();\n\t\tlevels.reserve(profileIndices.size());\n\n\t\tfor (const auto index : profileIndices) {\n\t\t\tconst auto level = peerColors->requiredLevelFor(\n\t\t\t\tchannel->id,\n\t\t\t\tindex,\n\t\t\t\tgroup,\n\t\t\t\ttrue);\n\t\t\tlevels.push_back(level);\n\t\t\tif (level) {\n\t\t\t\tlowestNonZeroLevel = std::min(lowestNonZeroLevel, level);\n\t\t\t}\n\t\t}\n\n\t\tfor (const auto level : levels) {\n\t\t\t++profileColorsByLevel[std::max(level, lowestNonZeroLevel)];\n\t\t}\n\t}\n\n\tconst auto &themes = channel->owner().cloudThemes().chatThemes();\n\tif (themes.empty()) {\n\t\tchannel->owner().cloudThemes().refreshChatThemes();\n\t}\n\tconst auto levelLimits = Data::LevelLimits(&channel->session());\n\treturn Ui::BoostFeatures{\n\t\t.nameColorsByLevel = std::move(nameColorsByLevel),\n\t\t.linkStylesByLevel = std::move(linkStylesByLevel),\n\t\t.profileColorsByLevel = std::move(profileColorsByLevel),\n\t\t.linkLogoLevel = group ? 0 : levelLimits.channelBgIconLevelMin(),\n\t\t.profileIconLevel = group\n\t\t\t? levelLimits.groupProfileBgIconLevelMin()\n\t\t\t: levelLimits.channelProfileBgIconLevelMin(),\n\t\t.autotranslateLevel = group ? 0 : levelLimits.channelAutoTranslateLevelMin(),\n\t\t.transcribeLevel = group ? levelLimits.groupTranscribeLevelMin() : 0,\n\t\t.emojiPackLevel = group ? levelLimits.groupEmojiStickersLevelMin() : 0,\n\t\t.emojiStatusLevel = group\n\t\t\t? levelLimits.groupEmojiStatusLevelMin()\n\t\t\t: levelLimits.channelEmojiStatusLevelMin(),\n\t\t.wallpaperLevel = group\n\t\t\t? levelLimits.groupWallpaperLevelMin()\n\t\t\t: levelLimits.channelWallpaperLevelMin(),\n\t\t.wallpapersCount = themes.empty() ? 8 : int(themes.size()),\n\t\t.customWallpaperLevel = group\n\t\t\t? levelLimits.groupCustomWallpaperLevelMin()\n\t\t\t: levelLimits.channelCustomWallpaperLevelMin(),\n\t\t.sponsoredLevel = levelLimits.channelRestrictSponsoredLevelMin(),\n\t};\n}\n\nint BoostsForGift(not_null<Main::Session*> session) {\n\treturn session->appConfig().get<int>(u\"boosts_per_sent_gift\"_q, 0);\n}\n\nstruct Sources {\n\tint groups = 0;\n\tint channels = 0;\n};\n[[nodiscard]] Sources SourcesCount(\n\t\tnot_null<ChannelData*> to,\n\t\tconst std::vector<TakenBoostSlot> &from,\n\t\tconst std::vector<int> &slots) {\n\tauto groups = base::flat_set<PeerId>();\n\tgroups.reserve(slots.size());\n\tauto channels = base::flat_set<PeerId>();\n\tchannels.reserve(slots.size());\n\tconst auto owner = &to->owner();\n\tfor (const auto slot : slots) {\n\t\tconst auto i = ranges::find(from, slot, &TakenBoostSlot::id);\n\t\tAssert(i != end(from));\n\t\tconst auto id = i->peerId;\n\t\tif (!groups.contains(id) && !channels.contains(id)) {\n\t\t\t(owner->peer(id)->isMegagroup() ? groups : channels).insert(id);\n\t\t}\n\t}\n\treturn {\n\t\t.groups = int(groups.size()),\n\t\t.channels = int(channels.size()),\n\t};\n}\n\nobject_ptr<Ui::BoxContent> ReassignBoostsBox(\n\t\tnot_null<ChannelData*> to,\n\t\tstd::vector<TakenBoostSlot> from,\n\t\tFn<void(std::vector<int> slots, int groups, int channels)> reassign,\n\t\tFn<void()> cancel) {\n\tExpects(!from.empty());\n\n\tconst auto now = base::unixtime::now();\n\tif (from.size() == 1 && from.front().cooldown > now) {\n\t\tcancel();\n\t\treturn ReassignBoostFloodBox(\n\t\t\tfrom.front().cooldown - now,\n\t\t\tto->owner().peer(from.front().peerId)->isMegagroup());\n\t} else if (from.size() == 1 && from.front().peerId) {\n\t\treturn ReassignBoostSingleBox(to, from.front(), reassign, cancel);\n\t}\n\tconst auto reassigned = std::make_shared<bool>();\n\tauto controller = std::make_unique<Controller>(to, from);\n\tconst auto raw = controller.get();\n\tauto initBox = [=](not_null<Ui::BoxContent*> box) {\n\t\traw->selectedValue(\n\t\t) | rpl::on_next([=](std::vector<int> slots) {\n\t\t\tbox->clearButtons();\n\t\t\tif (!slots.empty()) {\n\t\t\t\tconst auto sources = SourcesCount(to, from, slots);\n\t\t\t\tbox->addButton(tr::lng_boost_reassign_button(), [=] {\n\t\t\t\t\t*reassigned = true;\n\t\t\t\t\treassign(slots, sources.groups, sources.channels);\n\t\t\t\t});\n\t\t\t}\n\t\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\t\tbox->closeBox();\n\t\t\t});\n\t\t}, box->lifetime());\n\n\t\tbox->boxClosing() | rpl::filter([=] {\n\t\t\treturn !*reassigned;\n\t\t}) | rpl::on_next(cancel, box->lifetime());\n\t};\n\treturn Box<PeerListBox>(std::move(controller), std::move(initBox));\n}\n\nobject_ptr<Ui::RpWidget> CreateUserpicsTransfer(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<std::vector<not_null<PeerData*>>> from,\n\t\tnot_null<PeerData*> to,\n\t\tUserpicsTransferType type) {\n\tusing Type = UserpicsTransferType;\n\tstruct State {\n\t\tstd::vector<not_null<PeerData*>> from;\n\t\tstd::vector<std::unique_ptr<Ui::UserpicButton>> buttons;\n\t\tQImage layer;\n\t\trpl::variable<int> count = 0;\n\t\tbool painting = false;\n\t};\n\tconst auto st = &st::boostReplaceUserpicsRow;\n\tconst auto full = st->button.size.height()\n\t\t+ st::boostReplaceIconAdd.y()\n\t\t+ st::lineWidth;\n\tauto result = object_ptr<Ui::FixedHeightWidget>(parent, full);\n\tconst auto raw = result.data();\n\tconst auto right = CreateChild<Ui::UserpicButton>(raw, to, st->button);\n\tconst auto overlay = CreateChild<Ui::RpWidget>(raw);\n\tconst auto drawCornerPeer = (type == Type::ChannelFutureOwner)\n\t\t? [&]() -> PaintRoundImageCallback {\n\t\t\tusing Peers = std::vector<not_null<PeerData*>>;\n\t\t\tconst auto snapshot = rpl::variable<Peers>(\n\t\t\t\trpl::duplicate(from)).current();\n\t\t\tif (snapshot.size() == 2) {\n\t\t\t\treturn ForceRoundUserpicCallback(snapshot[1].get());\n\t\t\t}\n\t\t\treturn nullptr;\n\t\t}()\n\t\t: (PaintRoundImageCallback)(nullptr);\n\n\tconst auto state = raw->lifetime().make_state<State>();\n\t((type == Type::ChannelFutureOwner)\n\t\t? std::move(from) | rpl::map([=](\n\t\t\t\tconst std::vector<not_null<PeerData*>> &list) {\n\t\t\treturn std::vector<not_null<PeerData*>>{ list.front() };\n\t\t})\n\t\t: std::move(from)\n\t) | rpl::on_next([=](\n\t\t\tconst std::vector<not_null<PeerData*>> &list) {\n\t\tauto was = base::take(state->from);\n\t\tauto buttons = base::take(state->buttons);\n\t\tstate->from.reserve(list.size());\n\t\tstate->buttons.reserve(list.size());\n\t\tfor (const auto &peer : list) {\n\t\t\tstate->from.push_back(peer);\n\t\t\tconst auto i = ranges::find(was, peer);\n\t\t\tif (i != end(was)) {\n\t\t\t\tconst auto index = int(i - begin(was));\n\t\t\t\tAssert(buttons[index] != nullptr);\n\t\t\t\tstate->buttons.push_back(std::move(buttons[index]));\n\t\t\t} else {\n\t\t\t\tstate->buttons.push_back(\n\t\t\t\t\tstd::make_unique<Ui::UserpicButton>(raw, peer, st->button));\n\t\t\t\tconst auto raw = state->buttons.back().get();\n\t\t\t\tbase::install_event_filter(raw, [=](not_null<QEvent*> e) {\n\t\t\t\t\treturn (e->type() == QEvent::Paint && !state->painting)\n\t\t\t\t\t\t? base::EventFilterResult::Cancel\n\t\t\t\t\t\t: base::EventFilterResult::Continue;\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tstate->count.force_assign(int(list.size()));\n\t\toverlay->update();\n\t}, raw->lifetime());\n\n\trpl::combine(\n\t\traw->widthValue(),\n\t\tstate->count.value()\n\t) | rpl::on_next([=](int width, int count) {\n\t\tconst auto skip = st::boostReplaceUserpicsSkip;\n\t\tconst auto left = width - 2 * right->width() - skip;\n\t\tconst auto shift = std::min(\n\t\t\tst->shift,\n\t\t\t(count > 1 ? (left / (count - 1)) : width));\n\t\tconst auto total = right->width()\n\t\t\t+ (count ? (skip + right->width() + (count - 1) * shift) : 0);\n\t\tauto x = (width - total) / 2;\n\t\tfor (const auto &single : state->buttons) {\n\t\t\tsingle->moveToLeft(x, 0);\n\t\t\tx += shift;\n\t\t}\n\t\tif (count) {\n\t\t\tx += right->width() - shift + skip;\n\t\t}\n\t\tright->moveToLeft(x, 0);\n\t\toverlay->setGeometry(QRect(0, 0, width, raw->height()));\n\t}, raw->lifetime());\n\n\toverlay->paintRequest(\n\t) | rpl::filter([=] {\n\t\treturn !state->buttons.empty();\n\t}) | rpl::on_next([=] {\n\t\tconst auto outerw = overlay->width();\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tif (state->layer.size() != QSize(outerw, full) * ratio) {\n\t\t\tstate->layer = QImage(\n\t\t\t\tQSize(outerw, full) * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tstate->layer.setDevicePixelRatio(ratio);\n\t\t}\n\t\tstate->layer.fill(Qt::transparent);\n\n\t\tauto q = Painter(&state->layer);\n\t\tauto hq = PainterHighQualityEnabler(q);\n\t\tconst auto stroke = st->stroke;\n\t\tconst auto half = stroke / 2.;\n\t\tauto pen = st::windowBg->p;\n\t\tpen.setWidthF(stroke * 2.);\n\t\tstate->painting = true;\n\t\tfor (const auto &button : state->buttons) {\n\t\t\tq.setPen(pen);\n\t\t\tq.setBrush(Qt::NoBrush);\n\t\t\tq.drawEllipse(button->geometry());\n\t\t\tconst auto position = button->pos();\n\t\t\tbutton->render(&q, position, QRegion(), QWidget::DrawChildren);\n\t\t}\n\t\tstate->painting = false;\n\t\tconst auto last = state->buttons.back().get();\n\t\tif (type != Type::AuctionRecipient) {\n\t\t\tconst auto boosting = (type == Type::BoostReplace);\n\t\t\tconst auto back = boosting ? last : right;\n\t\t\tconst auto add = st::boostReplaceIconAdd;\n\t\t\tconst auto &icon = boosting\n\t\t\t\t? st::boostReplaceIcon\n\t\t\t\t: st::starrefJoinIcon;\n\t\t\tconst auto skip = boosting ? st::boostReplaceIconSkip : 0;\n\t\t\tconst auto w = icon.width() + 2 * skip;\n\t\t\tconst auto h = icon.height() + 2 * skip;\n\t\t\tconst auto x = back->x() + back->width() - w + add.x();\n\t\t\tconst auto y = back->y() + back->height() - h + add.y();\n\n\t\t\tpen.setWidthF(drawCornerPeer ? stroke * 2 : stroke);\n\t\t\tq.setPen(pen);\n\t\t\tq.drawEllipse(x - half, y - half, w + stroke, h + stroke);\n\t\t\tif (drawCornerPeer) {\n\t\t\t\tdrawCornerPeer(\n\t\t\t\t\tq,\n\t\t\t\t\tx - half,\n\t\t\t\t\ty - half,\n\t\t\t\t\tw + stroke,\n\t\t\t\t\tw + stroke);\n\t\t\t} else {\n\t\t\t\tauto brush = QLinearGradient(\n\t\t\t\t\tQPointF(x + w, y + h),\n\t\t\t\t\tQPointF(x, y));\n\t\t\t\tbrush.setStops(Ui::Premium::ButtonGradientStops());\n\t\t\t\tq.setBrush(brush);\n\t\t\t\ticon.paint(q, x + skip, y + skip, outerw);\n\t\t\t}\n\t\t}\n\t\tconst auto size = st::boostReplaceArrow.size();\n\t\tst::boostReplaceArrow.paint(\n\t\t\tq,\n\t\t\t(last->x()\n\t\t\t\t+ last->width()\n\t\t\t\t+ (st::boostReplaceUserpicsSkip - size.width()) / 2),\n\t\t\t(last->height() - size.height()) / 2,\n\t\t\touterw);\n\t\tq.end();\n\n\t\tauto p = QPainter(overlay);\n\t\tp.drawImage(0, 0, state->layer);\n\t}, overlay->lifetime());\n\treturn result;\n}\n\nobject_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<std::vector<not_null<PeerData*>>> peers,\n\t\tconst style::UserpicsRow &st,\n\t\tint limit) {\n\tstruct State {\n\t\tstd::vector<not_null<PeerData*>> from;\n\t\tstd::vector<std::unique_ptr<Ui::UserpicButton>> buttons;\n\t\tQImage layer;\n\t\tQImage badge;\n\t\trpl::variable<int> count = 0;\n\t\tbool painting = false;\n\t};\n\tconst auto full = st.button.size.height()\n\t\t+ (st.complex ? (st::boostReplaceIconAdd.y() + st::lineWidth) : 0);\n\tauto result = object_ptr<Ui::FixedHeightWidget>(parent, full);\n\tconst auto raw = result.data();\n\tconst auto overlay = CreateChild<Ui::RpWidget>(raw);\n\n\tconst auto state = raw->lifetime().make_state<State>();\n\tstd::move(\n\t\tpeers\n\t) | rpl::on_next([=, &st](\n\t\t\tconst std::vector<not_null<PeerData*>> &list) {\n\t\tauto was = base::take(state->from);\n\t\tauto buttons = base::take(state->buttons);\n\t\tstate->from.reserve(list.size());\n\t\tstate->buttons.reserve(list.size());\n\t\tfor (const auto &peer : list | ranges::views::take(limit)) {\n\t\t\tstate->from.push_back(peer);\n\t\t\tconst auto i = ranges::find(was, peer);\n\t\t\tif (i != end(was)) {\n\t\t\t\tconst auto index = int(i - begin(was));\n\t\t\t\tAssert(buttons[index] != nullptr);\n\t\t\t\tstate->buttons.push_back(std::move(buttons[index]));\n\t\t\t} else {\n\t\t\t\tstate->buttons.push_back(\n\t\t\t\t\tstd::make_unique<Ui::UserpicButton>(raw, peer, st.button));\n\t\t\t\tconst auto raw = state->buttons.back().get();\n\t\t\t\tbase::install_event_filter(raw, [=](not_null<QEvent*> e) {\n\t\t\t\t\treturn (e->type() == QEvent::Paint && !state->painting)\n\t\t\t\t\t\t? base::EventFilterResult::Cancel\n\t\t\t\t\t\t: base::EventFilterResult::Continue;\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tstate->count.force_assign(int(list.size()));\n\t\toverlay->update();\n\t}, raw->lifetime());\n\n\tif (const auto count = state->count.current()) {\n\t\tconst auto single = st.button.size.width();\n\t\tconst auto used = std::min(count, int(state->buttons.size()));\n\t\tconst auto shift = st.shift;\n\t\traw->resize(used ? (single + (used - 1) * shift) : 0, raw->height());\n\t}\n\trpl::combine(\n\t\traw->widthValue(),\n\t\tstate->count.value()\n\t) | rpl::on_next([=, &st](int width, int count) {\n\t\tconst auto single = st.button.size.width();\n\t\tconst auto left = width - single;\n\t\tconst auto used = std::min(count, int(state->buttons.size()));\n\t\tconst auto shift = std::min(\n\t\t\tst.shift,\n\t\t\t(used > 1 ? (left / (used - 1)) : width));\n\t\tconst auto total = used ? (single + (used - 1) * shift) : 0;\n\t\tauto x = (width - total) / 2;\n\t\tfor (const auto &single : state->buttons) {\n\t\t\tsingle->moveToLeft(x, 0);\n\t\t\tx += shift;\n\t\t}\n\t\toverlay->setGeometry(QRect(0, 0, width, raw->height()));\n\t}, raw->lifetime());\n\n\toverlay->paintRequest(\n\t) | rpl::filter([=] {\n\t\treturn !state->buttons.empty();\n\t}) | rpl::on_next([=, &st] {\n\t\tconst auto outerw = overlay->width();\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tif (state->layer.size() != QSize(outerw, full) * ratio) {\n\t\t\tstate->layer = QImage(\n\t\t\t\tQSize(outerw, full) * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tstate->layer.setDevicePixelRatio(ratio);\n\t\t}\n\t\tstate->layer.fill(Qt::transparent);\n\n\t\tauto q = QPainter(&state->layer);\n\t\tauto hq = PainterHighQualityEnabler(q);\n\t\tconst auto stroke = st.stroke;\n\t\tconst auto half = stroke / 2.;\n\t\tauto pen = st.bg->p;\n\t\tpen.setWidthF(stroke * 2.);\n\t\tstate->painting = true;\n\t\tconst auto paintOne = [&](not_null<Ui::UserpicButton*> button) {\n\t\t\tq.setPen(pen);\n\t\t\tq.setBrush(Qt::NoBrush);\n\t\t\tq.drawEllipse(button->geometry());\n\t\t\tconst auto position = button->pos();\n\t\t\tbutton->render(&q, position, QRegion(), QWidget::DrawChildren);\n\t\t};\n\t\tif (st.invert) {\n\t\t\tfor (const auto &button : ranges::views::reverse(state->buttons)) {\n\t\t\t\tpaintOne(button.get());\n\t\t\t}\n\t\t} else {\n\t\t\tfor (const auto &button : state->buttons) {\n\t\t\t\tpaintOne(button.get());\n\t\t\t}\n\t\t}\n\t\tstate->painting = false;\n\n\t\tconst auto text = (state->count.current() > limit)\n\t\t\t? ('+' + QString::number(state->count.current() - limit))\n\t\t\t: QString();\n\t\tif (st.complex && !text.isEmpty()) {\n\t\t\tconst auto last = state->buttons.back().get();\n\t\t\tconst auto add = st::boostReplaceIconAdd;\n\t\t\tconst auto skip = st::boostReplaceIconSkip;\n\t\t\tconst auto w = st::boostReplaceIcon.width() + 2 * skip;\n\t\t\tconst auto h = st::boostReplaceIcon.height() + 2 * skip;\n\t\t\tconst auto x = last->x() + last->width() - w + add.x();\n\t\t\tconst auto y = last->y() + last->height() - h + add.y();\n\t\t\tconst auto &font = st::semiboldFont;\n\t\t\tconst auto width = font->width(text);\n\t\t\tconst auto padded = std::max(w, width + 2 * font->spacew);\n\t\t\tconst auto rect = QRect(x - (padded - w) / 2, y, padded, h);\n\t\t\tauto brush = QLinearGradient(rect.bottomRight(), rect.topLeft());\n\t\t\tbrush.setStops(Ui::Premium::ButtonGradientStops());\n\t\t\tq.setBrush(brush);\n\t\t\tpen.setWidthF(stroke);\n\t\t\tq.setPen(pen);\n\t\t\tconst auto rectf = QRectF(rect);\n\t\t\tconst auto radius = std::min(rect.width(), rect.height()) / 2.;\n\t\t\tq.drawRoundedRect(\n\t\t\t\trectf.marginsAdded(QMarginsF{ half, half, half, half }),\n\t\t\t\tradius,\n\t\t\t\tradius);\n\t\t\tq.setFont(font);\n\t\t\tq.setPen(st::premiumButtonFg);\n\t\t\tq.drawText(rect, Qt::AlignCenter, text);\n\t\t}\n\t\tq.end();\n\n\t\tauto p = QPainter(overlay);\n\t\tp.drawImage(0, 0, state->layer);\n\t}, overlay->lifetime());\n\treturn result;\n}\n\nclass UniqueGiftBackground final : public Ui::DynamicImage {\npublic:\n\tUniqueGiftBackground(\n\t\tnot_null<Main::Session*> session,\n\t\tstd::shared_ptr<Data::UniqueGift> unique)\n\t: _session(session)\n\t, _unique(std::move(unique)) {\n\t}\n\n\tstd::shared_ptr<Ui::DynamicImage> clone() override {\n\t\treturn std::make_shared<UniqueGiftBackground>(_session, _unique);\n\t}\n\n\tvoid subscribeToUpdates(Fn<void()> callback) override {\n\t\t_repaint = std::move(callback);\n\t\tif (!_repaint) {\n\t\t\t_patternEmoji = nullptr;\n\t\t}\n\t}\n\n\tQImage image(int size) override {\n\t\tif (!_patternEmoji) {\n\t\t\t_patternEmoji = _session->data().customEmojiManager().create(\n\t\t\t\t_unique->pattern.document,\n\t\t\t\t[=] { ready(); },\n\t\t\t\tData::CustomEmojiSizeTag::Large);\n\t\t\t[[maybe_unused]] const auto preload = _patternEmoji->ready();\n\t\t}\n\t\tconst auto inner = QRect(0, 0, size, size);\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tif (_backgroundCache.size() != inner.size() * ratio) {\n\t\t\t_backgroundCache = QImage(\n\t\t\t\tinner.size() * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_backgroundCache.fill(Qt::transparent);\n\t\t\t_backgroundCache.setDevicePixelRatio(ratio);\n\n\t\t\tconst auto radius = st::giftBoxGiftRadius;\n\t\t\tauto p = QPainter(&_backgroundCache);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tauto gradient = QRadialGradient(\n\t\t\t\tinner.center(),\n\t\t\t\tinner.width() / 2);\n\t\t\tgradient.setStops({\n\t\t\t\t{ 0., _unique->backdrop.centerColor },\n\t\t\t\t{ 1., _unique->backdrop.edgeColor },\n\t\t\t});\n\t\t\tp.setBrush(gradient);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.drawRoundedRect(inner, radius, radius);\n\t\t\t_backroundPatterned = false;\n\t\t}\n\t\tif (!_backroundPatterned && _patternEmoji->ready()) {\n\t\t\t_backroundPatterned = true;\n\t\t\tauto p = QPainter(&_backgroundCache);\n\t\t\tp.setClipRect(inner);\n\t\t\tconst auto skip = inner.width() / 3;\n\t\t\tUi::PaintBgPoints(\n\t\t\t\tp,\n\t\t\t\tUi::PatternBgPointsSmall(),\n\t\t\t\t_patternCache,\n\t\t\t\t_patternEmoji.get(),\n\t\t\t\t*_unique,\n\t\t\t\tQRect(-skip, 0, inner.width() + 2 * skip, inner.height()));\n\t\t}\n\t\treturn _backgroundCache;\n\t}\n\nprivate:\n\tvoid ready() {\n\t\tif (!_backroundPatterned && _repaint) {\n\t\t\t_repaint();\n\t\t}\n\t}\n\n\tconst not_null<Main::Session*> _session;\n\tconst std::shared_ptr<Data::UniqueGift> _unique;\n\tFn<void()> _repaint;\n\tstd::unique_ptr<Ui::Text::CustomEmoji> _patternEmoji;\n\tQImage _backgroundCache;\n\tbase::flat_map<float64, QImage> _patternCache;\n\tbool _backroundPatterned = false;\n\n};\n\n[[nodiscard]] PaintRoundImageCallback GenerateGiftUniqueUserpicCallback(\n\t\tnot_null<Main::Session*> session,\n\t\tstd::shared_ptr<Data::UniqueGift> unique,\n\t\tFn<void()> update) {\n\tstruct State {\n\t\tQImage layer;\n\t\tstd::shared_ptr<UniqueGiftBackground> bg;\n\t\tstd::shared_ptr<Ui::Text::CustomEmoji> sticker;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tconst auto repaint = [=] {\n\t\tif (update) {\n\t\t\tupdate();\n\t\t}\n\t};\n\tstate->bg = std::make_shared<UniqueGiftBackground>(session, unique);\n\tstate->bg->subscribeToUpdates(repaint);\n\tconst auto tag = Data::CustomEmojiSizeTag::Isolated;\n\tstate->sticker = session->data().customEmojiManager().create(\n\t\tunique->model.document,\n\t\trepaint,\n\t\ttag);\n\n\treturn [=](QPainter &p, int x, int y, int outerw, int size) {\n\t\tconst auto ideal = st::boostReplaceUserpic.photoSize;\n\t\tconst auto scale = size / float64(ideal);\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tif (state->layer.size() != QSize(ideal, ideal) * ratio) {\n\t\t\tstate->layer = QImage(\n\t\t\t\tQSize(ideal, ideal) * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tstate->layer.setDevicePixelRatio(ratio);\n\t\t}\n\t\tstate->layer.fill(Qt::transparent);\n\n\t\tauto q = QPainter(&state->layer);\n\t\tauto hq = PainterHighQualityEnabler(q);\n\t\tconst auto esize = Data::FrameSizeFromTag(tag) / ratio;\n\t\tq.drawImage(QRect(0, 0, ideal, ideal), state->bg->image(ideal));\n\t\tstate->sticker->paint(q, {\n\t\t\t.textColor = st::windowFg->c,\n\t\t\t.now = crl::now(),\n\t\t\t.position = QPoint((ideal - esize) / 2, (ideal - esize) / 2),\n\t\t});\n\t\tq.end();\n\n\t\tif (scale != 1.) {\n\t\t\tp.save();\n\t\t\tp.translate(x, y);\n\t\t\tp.scale(scale, scale);\n\t\t\tp.drawImage(0, 0, state->layer);\n\t\t\tp.restore();\n\t\t} else {\n\t\t\tp.drawImage(x, y, state->layer);\n\t\t}\n\t};\n}\n\nobject_ptr<Ui::RpWidget> CreateGiftTransfer(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<Data::UniqueGift> unique,\n\t\tnot_null<PeerData*> to) {\n\tstruct State {\n\t\tQImage layer;\n\t\tQPoint giftPosition;\n\t\tPaintRoundImageCallback paintGift;\n\t};\n\tconst auto st = &st::boostReplaceUserpicsRow;\n\tconst auto full = st->button.size.height()\n\t\t+ st::boostReplaceIconAdd.y()\n\t\t+ st::lineWidth;\n\tauto result = object_ptr<Ui::FixedHeightWidget>(parent, full);\n\tconst auto raw = result.data();\n\tconst auto right = CreateChild<Ui::UserpicButton>(raw, to, st->button);\n\tconst auto overlay = CreateChild<Ui::RpWidget>(raw);\n\n\tconst auto state = raw->lifetime().make_state<State>();\n\tstate->paintGift = GenerateGiftUniqueUserpicCallback(\n\t\t&to->session(),\n\t\tunique,\n\t\t[=] { raw->update(); });\n\n\traw->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto skip = st::boostReplaceUserpicsSkip;\n\t\tconst auto total = right->width() + skip + right->width();\n\t\tauto x = (width - total) / 2;\n\t\tstate->giftPosition = QPoint(x, 0);\n\t\tx += right->width() + skip;\n\t\tright->moveToLeft(x, 0);\n\t\toverlay->setGeometry(QRect(0, 0, width, raw->height()));\n\t}, raw->lifetime());\n\n\toverlay->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tconst auto outerw = overlay->width();\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tif (state->layer.size() != QSize(outerw, full) * ratio) {\n\t\t\tstate->layer = QImage(\n\t\t\t\tQSize(outerw, full) * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tstate->layer.setDevicePixelRatio(ratio);\n\t\t}\n\t\tstate->layer.fill(Qt::transparent);\n\n\t\tauto q = Painter(&state->layer);\n\t\tauto hq = PainterHighQualityEnabler(q);\n\t\tstate->paintGift(\n\t\t\tq,\n\t\t\tstate->giftPosition.x(),\n\t\t\tstate->giftPosition.y(),\n\t\t\touterw,\n\t\t\tright->width());\n\n\t\tconst auto size = st::boostReplaceArrow.size();\n\t\tst::boostReplaceArrow.paint(\n\t\t\tq,\n\t\t\t(state->giftPosition.x()\n\t\t\t\t+ right->width()\n\t\t\t\t+ (st::boostReplaceUserpicsSkip - size.width()) / 2),\n\t\t\t(right->height() - size.height()) / 2,\n\t\t\touterw);\n\n\t\tq.end();\n\n\t\tauto p = QPainter(overlay);\n\t\tp.drawImage(0, 0, state->layer);\n\t}, overlay->lifetime());\n\treturn result;\n}\n\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/replace_boost_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nnamespace style {\nstruct UserpicsRow;\n} // namespace style\n\nclass ChannelData;\n\nnamespace Data {\nstruct UniqueGift;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nstruct BoostCounters;\nstruct BoostFeatures;\nclass BoxContent;\nclass RpWidget;\n} // namespace Ui\n\nstruct TakenBoostSlot {\n\tint id = 0;\n\tTimeId expires = 0;\n\tPeerId peerId = 0;\n\tTimeId cooldown = 0;\n};\n\nstruct ForChannelBoostSlots {\n\tstd::vector<int> free;\n\tstd::vector<int> already;\n\tstd::vector<TakenBoostSlot> other;\n};\n\n[[nodiscard]] ForChannelBoostSlots ParseForChannelBoostSlots(\n\tnot_null<ChannelData*> channel,\n\tconst QVector<MTPMyBoost> &boosts);\n\n[[nodiscard]] Ui::BoostCounters ParseBoostCounters(\n\tconst MTPpremium_BoostsStatus &status);\n\n[[nodiscard]] Ui::BoostFeatures LookupBoostFeatures(\n\tnot_null<ChannelData*> channel);\n\n[[nodiscard]] int BoostsForGift(not_null<Main::Session*> session);\n\nobject_ptr<Ui::BoxContent> ReassignBoostsBox(\n\tnot_null<ChannelData*> to,\n\tstd::vector<TakenBoostSlot> from,\n\tFn<void(std::vector<int> slots, int groups, int channels)> reassign,\n\tFn<void()> cancel);\n\nenum class UserpicsTransferType {\n\tBoostReplace,\n\tStarRefJoin,\n\tAuctionRecipient,\n\tChannelFutureOwner,\n};\n[[nodiscard]] object_ptr<Ui::RpWidget> CreateUserpicsTransfer(\n\tnot_null<Ui::RpWidget*> parent,\n\trpl::producer<std::vector<not_null<PeerData*>>> from,\n\tnot_null<PeerData*> to,\n\tUserpicsTransferType type);\n\n[[nodiscard]] object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(\n\tnot_null<Ui::RpWidget*> parent,\n\trpl::producer<std::vector<not_null<PeerData*>>> peers,\n\tconst style::UserpicsRow &st,\n\tint limit);\n\n[[nodiscard]] object_ptr<Ui::RpWidget> CreateGiftTransfer(\n\tnot_null<Ui::RpWidget*> parent,\n\tstd::shared_ptr<Data::UniqueGift> unique,\n\tnot_null<PeerData*> to);\n\nusing PaintRoundImageCallback = Fn<void(\n\tPainter &p,\n\tint x,\n\tint y,\n\tint outerWidth,\n\tint size)>;\n\n[[nodiscard]] PaintRoundImageCallback GenerateGiftUniqueUserpicCallback(\n\tnot_null<Main::Session*> session,\n\tstd::shared_ptr<Data::UniqueGift> unique,\n\tFn<void()> update);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/tag_info_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/tag_info_box.h\"\n\n#include \"boxes/peers/edit_participants_box.h\"\n#include \"boxes/peers/edit_tag_control.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"history/view/history_view_message.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"window/section_widget.h\"\n#include \"window/themes/window_theme.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace {\n\nusing HistoryView::BadgeRole;\n\nconstexpr auto kTextLinesAlpha = 0.1;\n\n[[nodiscard]] QColor RoleColor(BadgeRole role) {\n\treturn (role == BadgeRole::Creator)\n\t\t? st::rankOwnerFg->c\n\t\t: (role == BadgeRole::Admin)\n\t\t? st::rankAdminFg->c\n\t\t: st::rankUserFg->c;\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeRoundColoredLogo(\n\t\tnot_null<QWidget*> parent,\n\t\tBadgeRole role) {\n\tconst auto &icon = st::tagInfoIcon;\n\tconst auto &padding = st::tagInfoIconPadding;\n\tconst auto logoSize = icon.size();\n\tconst auto logoOuter = logoSize.grownBy(padding);\n\tauto result = object_ptr<Ui::RpWidget>(parent);\n\tconst auto logo = result.data();\n\tlogo->resize(logo->width(), logoOuter.height());\n\tlogo->paintRequest() | rpl::on_next([=, &icon] {\n\t\tif (logo->width() < logoOuter.width()) {\n\t\t\treturn;\n\t\t}\n\t\tauto p = QPainter(logo);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto x = (logo->width() - logoOuter.width()) / 2;\n\t\tconst auto outer = QRect(QPoint(x, 0), logoOuter);\n\t\tp.setBrush(RoleColor(role));\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawEllipse(outer);\n\t\ticon.paintInCenter(p, outer);\n\t}, logo->lifetime());\n\treturn result;\n}\n\n[[nodiscard]] Ui::Text::PaletteDependentEmoji MakeTagPillEmoji(\n\t\tconst QString &text,\n\t\tBadgeRole role) {\n\treturn { .factory = [=] {\n\t\tconst auto color = RoleColor(role);\n\t\tconst auto &padding = st::msgTagBadgePadding;\n\t\tauto string = Ui::Text::String(st::defaultTextStyle, text);\n\t\tconst auto textWidth = string.maxWidth();\n\t\tconst auto isUser = (role == BadgeRole::User);\n\t\tconst auto contentWidth = padding.left()\n\t\t\t+ textWidth\n\t\t\t+ padding.right();\n\t\tconst auto pillHeight = padding.top()\n\t\t\t+ st::msgFont->height\n\t\t\t+ padding.bottom();\n\t\tconst auto imgWidth = isUser\n\t\t\t? textWidth\n\t\t\t: std::max(contentWidth, pillHeight);\n\t\tconst auto imgHeight = isUser\n\t\t\t? st::msgFont->height\n\t\t\t: pillHeight;\n\t\tconst auto ratio = style::DevicePixelRatio();\n\n\t\tauto result = QImage(\n\t\t\tQSize(imgWidth, imgHeight) * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tresult.setDevicePixelRatio(ratio);\n\t\tresult.fill(Qt::transparent);\n\n\t\tauto p = QPainter(&result);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tif (!isUser) {\n\t\t\tauto bgColor = color;\n\t\t\tbgColor.setAlphaF(0.15);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(bgColor);\n\t\t\tp.drawRoundedRect(\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\timgWidth,\n\t\t\t\timgHeight,\n\t\t\t\timgHeight / 2.,\n\t\t\t\timgHeight / 2.);\n\t\t}\n\t\tp.setPen(color);\n\t\tstring.draw(p, {\n\t\t\t.position = QPoint(\n\t\t\t\tisUser ? 0 : ((imgWidth - textWidth) / 2),\n\t\t\t\tisUser ? 0 : padding.top()),\n\t\t\t.availableWidth = textWidth,\n\t\t});\n\t\tp.end();\n\t\treturn result;\n\t}, .margin = st::customEmojiTextBadgeMargin };\n}\n\nclass TagPreviewsWidget final : public Ui::RpWidget {\npublic:\n\tTagPreviewsWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Session*> session,\n\t\tBadgeRole role);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid paintPreview(\n\t\tQPainter &p,\n\t\tQRect rect,\n\t\tBadgeRole previewRole) const;\n\tvoid paintBubbleToImage(\n\t\tQRect rect,\n\t\tBadgeRole previewRole) const;\n\tvoid invalidateCache();\n\n\tconst std::unique_ptr<Ui::ChatTheme> _theme;\n\tstd::unique_ptr<Ui::ChatStyle> _style;\n\tBadgeRole _role = BadgeRole::User;\n\tmutable QImage _leftCache;\n\tmutable QImage _rightCache;\n\n};\n\nTagPreviewsWidget::TagPreviewsWidget(\n\tQWidget *parent,\n\tnot_null<Main::Session*> session,\n\tBadgeRole role)\n: RpWidget(parent)\n, _theme(Window::Theme::DefaultChatThemeOn(lifetime()))\n, _style(std::make_unique<Ui::ChatStyle>(\n\tsession->colorIndicesValue()))\n, _role(role) {\n\t_style->apply(_theme.get());\n\tresize(width(), st::tagInfoPreviewHeight);\n\n\t_theme->repaintBackgroundRequests(\n\t) | rpl::on_next([=] {\n\t\tinvalidateCache();\n\t\tupdate();\n\t}, lifetime());\n\n\tstyle::PaletteChanged() | rpl::on_next([=] {\n\t\tinvalidateCache();\n\t\tupdate();\n\t}, lifetime());\n\n\tsizeValue() | rpl::skip(1) | rpl::on_next([=] {\n\t\tinvalidateCache();\n\t}, lifetime());\n}\n\nvoid TagPreviewsWidget::invalidateCache() {\n\t_leftCache = QImage();\n\t_rightCache = QImage();\n}\n\nvoid TagPreviewsWidget::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto gap = st::tagInfoPreviewGap;\n\tconst auto previewWidth = (width() - gap) / 2;\n\tif (previewWidth <= 0) {\n\t\treturn;\n\t}\n\n\tconst auto leftRect = QRect(0, 0, previewWidth, height());\n\tconst auto rightRect = QRect(\n\t\tpreviewWidth + gap,\n\t\t0,\n\t\twidth() - previewWidth - gap,\n\t\theight());\n\n\tpaintPreview(p, leftRect, BadgeRole::User);\n\n\tconst auto rightRole = (_role == BadgeRole::User)\n\t\t? BadgeRole::Admin\n\t\t: _role;\n\tpaintPreview(p, rightRect, rightRole);\n}\n\nvoid TagPreviewsWidget::paintPreview(\n\t\tQPainter &p,\n\t\tQRect rect,\n\t\tBadgeRole previewRole) const {\n\tconst auto previewRadius = st::tagInfoPreviewRadius;\n\n\tp.save();\n\tp.translate(rect.topLeft());\n\n\tconst auto local = QRect(0, 0, rect.width(), rect.height());\n\tauto clipPath = QPainterPath();\n\tclipPath.addRoundedRect(local, previewRadius, previewRadius);\n\tp.setClipPath(clipPath);\n\n\tWindow::SectionWidget::PaintBackground(\n\t\tp,\n\t\t_theme.get(),\n\t\tQSize(rect.width(), window()->height()),\n\t\tlocal);\n\n\tauto &cache = (previewRole == BadgeRole::User)\n\t\t? _leftCache\n\t\t: _rightCache;\n\tif (cache.isNull()) {\n\t\tpaintBubbleToImage(rect, previewRole);\n\t}\n\tp.drawImage(0, 0, cache);\n\n\tp.restore();\n}\n\nvoid TagPreviewsWidget::paintBubbleToImage(\n\t\tQRect rect,\n\t\tBadgeRole previewRole) const {\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto &cache = (previewRole == BadgeRole::User)\n\t\t? _leftCache\n\t\t: _rightCache;\n\tcache = QImage(\n\t\trect.size() * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tcache.setDevicePixelRatio(ratio);\n\tcache.fill(Qt::transparent);\n\n\tconst auto &stm = _style->messageStyle(false, false);\n\tconst auto &padding = st::tagInfoPreviewBubblePadding;\n\tconst auto radius = st::tagInfoPreviewBubbleRadius;\n\tconst auto rightMargin = st::tagInfoPreviewBubbleRightMargin;\n\tconst auto bubbleTop = padding.top();\n\tconst auto bubbleHeight = rect.height()\n\t\t- padding.top()\n\t\t- padding.bottom();\n\tconst auto bubbleRight = rect.width() - rightMargin;\n\tconst auto bubbleRect = QRect(\n\t\t-radius,\n\t\tbubbleTop,\n\t\tbubbleRight + radius,\n\t\tbubbleHeight);\n\n\tauto p = QPainter(&cache);\n\t{\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(stm.msgBg);\n\t\tp.drawRoundedRect(bubbleRect, radius, radius);\n\t}\n\n\tconst auto innerLeft = padding.left();\n\tconst auto innerRight = bubbleRight - padding.right();\n\tconst auto available = innerRight - innerLeft;\n\n\tconst auto badgeColor = RoleColor(previewRole);\n\tconst auto badgeText = (previewRole == BadgeRole::Creator)\n\t\t? tr::lng_tag_info_preview_owner(tr::now)\n\t\t: (previewRole == BadgeRole::Admin)\n\t\t? tr::lng_tag_info_preview_admin(tr::now)\n\t\t: tr::lng_tag_info_preview_member(tr::now);\n\tauto badgeString = Ui::Text::String(st::defaultTextStyle, badgeText);\n\tconst auto badgeTextWidth = badgeString.maxWidth();\n\tconst auto &badgePadding = st::msgTagBadgePadding;\n\tconst auto badgeContentWidth = badgePadding.left()\n\t\t+ badgeTextWidth\n\t\t+ badgePadding.right();\n\tconst auto pillHeight = badgePadding.top()\n\t\t+ st::msgFont->height\n\t\t+ badgePadding.bottom();\n\tconst auto pillWidth = std::max(badgeContentWidth, pillHeight);\n\n\tconst auto badgeRight = innerRight;\n\tconst auto badgeLeft = badgeRight - pillWidth;\n\tconst auto badgeTop = bubbleTop + st::tagInfoPreviewBadgeTop;\n\n\tif (previewRole != BadgeRole::User) {\n\t\tauto bgColor = badgeColor;\n\t\tbgColor.setAlphaF(0.15);\n\t\tconst auto pillRect = QRect(\n\t\t\tbadgeLeft,\n\t\t\tbadgeTop,\n\t\t\tpillWidth,\n\t\t\tpillHeight);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(bgColor);\n\t\tp.drawRoundedRect(pillRect, pillHeight / 2., pillHeight / 2.);\n\t\tp.setPen(badgeColor);\n\t\tbadgeString.draw(p, {\n\t\t\t.position = QPoint(\n\t\t\t\tbadgeLeft + (pillWidth - badgeTextWidth) / 2,\n\t\t\t\tbadgeTop + badgePadding.top()),\n\t\t\t.availableWidth = badgeTextWidth,\n\t\t});\n\t} else if (badgeTextWidth > 0) {\n\t\tp.setPen(st::rankUserFg);\n\t\tbadgeString.draw(p, {\n\t\t\t.position = QPoint(innerRight - badgeTextWidth, badgeTop),\n\t\t\t.availableWidth = badgeTextWidth,\n\t\t});\n\t}\n\n\tp.setFont(st::msgDateFont);\n\tconst auto timeText = u\"12:00\"_q;\n\tconst auto timeWidth = st::msgDateFont->width(timeText);\n\tconst auto timeX = innerRight - timeWidth;\n\tconst auto timeY = bubbleTop\n\t\t+ bubbleHeight\n\t\t- padding.bottom()\n\t\t+ st::msgDateFont->ascent\n\t\t- st::msgDateFont->height;\n\tp.setPen(stm.msgDateFg);\n\tp.drawText(timeX, timeY, timeText);\n\n\t{\n\t\tauto color = stm.historyTextFg->c;\n\t\tcolor.setAlphaF(color.alphaF() * kTextLinesAlpha);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(color);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tconst auto lineHeight = st::tagInfoPreviewLineHeight;\n\t\tconst auto lineSpacing = st::tagInfoPreviewLineSpacing;\n\t\tconst auto linesTop = badgeTop\n\t\t\t+ pillHeight\n\t\t\t+ lineSpacing;\n\t\tconst auto lineRadius = lineHeight / 2.0;\n\t\tconst auto timeAreaLeft = timeX - padding.right();\n\t\tconst auto fractions = { 1.0, 0.65, 0.65 };\n\t\tauto y = double(linesTop);\n\t\tauto lineIndex = 0;\n\t\tfor (const auto fraction : fractions) {\n\t\t\tauto w = available * fraction;\n\t\t\tconst auto lineBottom = y + lineHeight;\n\t\t\tif (lineIndex >= 1 && lineBottom > (timeY - lineSpacing)) {\n\t\t\t\tw = std::min(w, double(timeAreaLeft - innerLeft));\n\t\t\t}\n\t\t\tp.drawRoundedRect(\n\t\t\t\tQRectF(innerLeft, y, w, lineHeight),\n\t\t\t\tlineRadius,\n\t\t\t\tlineRadius);\n\t\t\ty += lineHeight + lineSpacing;\n\t\t\t++lineIndex;\n\t\t}\n\t}\n\n\tconst auto fadeWidth = st::tagInfoPreviewFadeWidth;\n\tp.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\tauto gradient = QLinearGradient(0, 0, fadeWidth, 0);\n\tgradient.setStops({\n\t\t{ 0., QColor(255, 255, 255, 0) },\n\t\t{ 1., QColor(255, 255, 255, 255) },\n\t});\n\tp.fillRect(0, 0, fadeWidth, rect.height(), gradient);\n\tp.end();\n}\n\n[[nodiscard]] QString LookupCurrentRank(not_null<PeerData*> peer) {\n\tconst auto selfId = peerToUser(peer->session().user()->id);\n\tif (const auto channel = peer->asMegagroup()) {\n\t\tif (const auto info = channel->mgInfo.get()) {\n\t\t\tconst auto it = info->memberRanks.find(selfId);\n\t\t\tif (it != info->memberRanks.end()) {\n\t\t\t\treturn it->second;\n\t\t\t}\n\t\t}\n\t} else if (const auto chat = peer->asChat()) {\n\t\tconst auto it = chat->memberRanks.find(selfId);\n\t\tif (it != chat->memberRanks.end()) {\n\t\t\treturn it->second;\n\t\t}\n\t}\n\treturn QString();\n}\n\n} // namespace\n\nvoid TagInfoBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<PeerData*> author,\n\t\tconst QString &tagText,\n\t\tHistoryView::BadgeRole role) {\n\tbox->setStyle(st::confcallJoinBox);\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setNoContentMargin(true);\n\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\tbox->closeBox();\n\t});\n\n\tbox->addRow(\n\t\tMakeRoundColoredLogo(box, role),\n\t\tst::boxRowPadding + st::confcallLinkHeaderIconPadding);\n\n\tauto title = (role == BadgeRole::Creator)\n\t\t? tr::lng_tag_info_title_owner()\n\t\t: (role == BadgeRole::Admin)\n\t\t? tr::lng_tag_info_title_admin()\n\t\t: tr::lng_tag_info_title_user();\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(box, std::move(title), st::boxTitle),\n\t\tst::boxRowPadding + st::confcallLinkTitlePadding,\n\t\tstyle::al_top);\n\n\tauto helper = Ui::Text::CustomEmojiHelper();\n\tconst auto tagPill = helper.paletteDependent(\n\t\tMakeTagPillEmoji(tagText, role));\n\tconst auto authorName = author->shortName();\n\tconst auto groupName = peer->name();\n\tconst auto descText = (role == BadgeRole::Creator)\n\t\t? tr::lng_tag_info_text_owner(\n\t\t\ttr::now,\n\t\t\tlt_emoji,\n\t\t\ttagPill,\n\t\t\tlt_author,\n\t\t\ttr::bold(authorName),\n\t\t\tlt_group,\n\t\t\ttr::bold(groupName),\n\t\t\ttr::rich)\n\t\t: (role == BadgeRole::Admin)\n\t\t? tr::lng_tag_info_text_admin(\n\t\t\ttr::now,\n\t\t\tlt_emoji,\n\t\t\ttagPill,\n\t\t\tlt_author,\n\t\t\ttr::bold(authorName),\n\t\t\tlt_group,\n\t\t\ttr::bold(groupName),\n\t\t\ttr::rich)\n\t\t: tr::lng_tag_info_text_user(\n\t\t\ttr::now,\n\t\t\tlt_emoji,\n\t\t\ttagPill,\n\t\t\tlt_author,\n\t\t\ttr::bold(authorName),\n\t\t\tlt_group,\n\t\t\ttr::bold(groupName),\n\t\t\ttr::rich);\n\tconst auto context = helper.context();\n\tconst auto desc = box->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\trpl::single(descText),\n\t\t\tst::confcallLinkCenteredText,\n\t\t\tst::defaultPopupMenu,\n\t\t\tcontext),\n\t\tst::boxRowPadding,\n\t\tstyle::al_top);\n\tdesc->setTryMakeSimilarLines(true);\n\n\tbox->addRow(\n\t\tobject_ptr<TagPreviewsWidget>(\n\t\t\tbox,\n\t\t\t&peer->session(),\n\t\t\trole),\n\t\tst::boxRowPadding + st::tagInfoPreviewPadding);\n\n\tconst auto selfUser = peer->session().user();\n\tconst auto selfRole = LookupBadgeRole(peer, selfUser);\n\tconst auto isAdmin = (selfRole != BadgeRole::User);\n\tconst auto canEditSelf = isAdmin\n\t\t|| !peer->amRestricted(ChatRestriction::EditRank);\n\tconst auto channel = peer->asChannel();\n\tconst auto inGroup = peer->isChat() || (channel && channel->amIn());\n\tif (canEditSelf && inGroup) {\n\t\tconst auto currentRank = LookupCurrentRank(peer);\n\t\tauto buttonText = currentRank.isEmpty()\n\t\t\t? tr::lng_tag_info_add_my_tag()\n\t\t\t: tr::lng_tag_info_edit_my_tag();\n\t\tbox->addButton(std::move(buttonText), [=] {\n\t\t\tbox->closeBox();\n\t\t\tshow->show(Box(\n\t\t\t\tEditCustomRankBox,\n\t\t\t\tshow,\n\t\t\t\tpeer,\n\t\t\t\tselfUser,\n\t\t\t\tcurrentRank,\n\t\t\t\ttrue,\n\t\t\t\tFn<void(QString)>(nullptr)));\n\t\t});\n\t} else {\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_tag_info_admins_only(),\n\t\t\t\tst::tagInfoAdminsOnlyLabel),\n\t\t\tst::boxRowPadding + st::tagInfoAdminsOnlyPadding,\n\t\t\tstyle::al_top);\n\t\tbox->addButton(\n\t\t\trpl::single(QString()),\n\t\t\t[=] { box->closeBox(); }\n\t\t)->setText(rpl::single(Ui::Text::IconEmoji(\n\t\t\t&st::infoStarsUnderstood\n\t\t).append(' ').append(\n\t\t\ttr::lng_stars_rating_understood(tr::now))));\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/tag_info_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass GenericBox;\nclass Show;\n} // namespace Ui\n\nnamespace HistoryView {\nenum class BadgeRole : uchar;\n} // namespace HistoryView\n\nclass PeerData;\n\nvoid TagInfoBox(\n\tnot_null<Ui::GenericBox*> box,\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<PeerData*> peer,\n\tnot_null<PeerData*> author,\n\tconst QString &tagText,\n\tHistoryView::BadgeRole role);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/toggle_topics_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/toggle_topics_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Ui {\nnamespace {\n\nenum class LayoutType {\n\tTabs,\n\tList\n};\n\nclass LayoutButton final : public Ui::RippleButton {\npublic:\n\tLayoutButton(\n\t\tQWidget *parent,\n\t\tLayoutType type,\n\t\tstd::shared_ptr<Ui::RadioenumGroup<LayoutType>> group);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tQImage prepareRippleMask() const override;\n\n\tUi::FlatLabel _text;\n\tUi::Animations::Simple _activeAnimation;\n\tbool _active = false;\n\n};\n\nLayoutButton::LayoutButton(\n\tQWidget *parent,\n\tLayoutType type,\n\tstd::shared_ptr<Ui::RadioenumGroup<LayoutType>> group)\n: RippleButton(parent, st::defaultRippleAnimationBgOver)\n, _text(this, st::topicsLayoutButtonLabel)\n, _active(group->current() == type) {\n\t_text.setText(type == LayoutType::Tabs\n\t\t? tr::lng_edit_topics_tabs(tr::now)\n\t\t: tr::lng_edit_topics_list(tr::now));\n\tconst auto iconColorOverride = [=] {\n\t\treturn anim::color(\n\t\t\tst::windowSubTextFg,\n\t\t\tst::windowActiveTextFg,\n\t\t\t_activeAnimation.value(_active ? 1. : 0.));\n\t};\n\tconst auto iconSize = st::topicsLayoutButtonIconSize;\n\tauto [iconWidget, iconAnimate] = Settings::CreateLottieIcon(\n\t\tthis,\n\t\t{\n\t\t\t.name = (type == LayoutType::Tabs\n\t\t\t\t? u\"topics_tabs\"_q\n\t\t\t\t: u\"topics_list\"_q),\n\t\t\t.color = &st::windowSubTextFg,\n\t\t\t.sizeOverride = { iconSize, iconSize },\n\t\t\t.colorizeUsingAlpha = true,\n\t\t},\n\t\tst::topicsLayoutButtonIconPadding,\n\t\ticonColorOverride);\n\tconst auto icon = iconWidget.release();\n\tsetClickedCallback([=] {\n\t\tgroup->setValue(type);\n\t\ticonAnimate(anim::repeat::once);\n\t});\n\tgroup->value() | rpl::on_next([=](LayoutType value) {\n\t\tconst auto active = (value == type);\n\t\t_text.setTextColorOverride(active\n\t\t\t? st::windowFgActive->c\n\t\t\t: std::optional<QColor>());\n\n\t\tif (_active == active) {\n\t\t\treturn;\n\t\t}\n\t\t_active = active;\n\t\t_text.update();\n\t\t_activeAnimation.start([=] {\n\t\t\ticon->update();\n\t\t}, _active ? 0. : 1., _active ? 0. : 1., st::fadeWrapDuration);\n\t}, lifetime());\n\n\t_text.paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tif (_active) {\n\t\t\tauto p = QPainter(&_text);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tconst auto radius = _text.height() / 2.;\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(st::windowBgActive);\n\t\t\tp.drawRoundedRect(_text.rect(), radius, radius);\n\t\t}\n\t}, _text.lifetime());\n\n\tconst auto padding = st::topicsLayoutButtonPadding;\n\tconst auto skip = st::topicsLayoutButtonSkip;\n\tconst auto text = _text.height();\n\n\tresize(\n\t\tpadding.left() + icon->width() + padding.right(),\n\t\tpadding.top() + icon->height() + skip + text + padding.bottom());\n\ticon->move(padding.left(), padding.top());\n\t_text.move(\n\t\t(width() - _text.width()) / 2,\n\t\tpadding.top() + icon->height() + skip);\n}\n\nvoid LayoutButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tconst auto rippleBg = anim::color(\n\t\tst::windowBgOver,\n\t\tst::lightButtonBgOver,\n\t\t_activeAnimation.value(_active ? 1. : 0.));\n\tpaintRipple(p, QPoint(), &rippleBg);\n}\n\nQImage LayoutButton::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::RoundRectMask(size(), st::boxRadius);\n}\n\n} // namespace\n\nvoid ToggleTopicsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tbool enabled,\n\t\tbool tabs,\n\t\tFn<void(bool enabled, bool tabs)> callback) {\n\tbox->setTitle(tr::lng_forum_topics_switch());\n\tbox->setWidth(st::boxWideWidth);\n\n\tconst auto container = box->verticalLayout();\n\n\tSettings::AddDividerTextWithLottie(container, {\n\t\t.lottie = u\"topics\"_q,\n\t\t.lottieSize = st::settingsFilterIconSize,\n\t\t.lottieMargins = st::settingsFilterIconPadding,\n\t\t.showFinished = box->showFinishes(),\n\t\t.about = tr::lng_edit_topics_about(\n\t\t\ttr::rich\n\t\t),\n\t\t.aboutMargins = st::settingsFilterDividerLabelPadding,\n\t});\n\n\tUi::AddSkip(container);\n\n\tconst auto toggle = container->add(\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\tcontainer,\n\t\t\ttr::lng_edit_topics_enable(),\n\t\t\tst::settingsButtonNoIcon));\n\ttoggle->toggleOn(rpl::single(enabled));\n\n\tUi::AddSkip(container);\n\tUi::AddDivider(container);\n\tUi::AddSkip(container);\n\n\tconst auto group = std::make_shared<Ui::RadioenumGroup<LayoutType>>(tabs\n\t\t? LayoutType::Tabs\n\t\t: LayoutType::List);\n\n\tconst auto layoutWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto layout = layoutWrap->entity();\n\n\tUi::AddSubsectionTitle(layout, tr::lng_edit_topics_layout());\n\tconst auto buttons = layout->add(\n\t\tobject_ptr<Ui::RpWidget>(layout),\n\t\tQMargins(0, 0, 0, st::defaultVerticalListSkip * 2));\n\n\tconst auto tabsButton = Ui::CreateChild<LayoutButton>(\n\t\tbuttons,\n\t\tLayoutType::Tabs,\n\t\tgroup);\n\tconst auto listButton = Ui::CreateChild<LayoutButton>(\n\t\tbuttons,\n\t\tLayoutType::List,\n\t\tgroup);\n\n\tbuttons->resize(container->width(), tabsButton->height());\n\tbuttons->widthValue() | rpl::on_next([=](int outer) {\n\t\tconst auto skip = st::boxRowPadding.left() - st::boxRadius;\n\t\ttabsButton->moveToLeft(skip, 0, outer);\n\t\tlistButton->moveToRight(skip, 0, outer);\n\t}, buttons->lifetime());\n\n\tUi::AddDividerText(\n\t\tlayout,\n\t\ttr::lng_edit_topics_layout_about(tr::rich));\n\n\tlayoutWrap->toggle(enabled, anim::type::instant);\n\ttoggle->toggledChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tlayoutWrap->toggle(checked, anim::type::normal);\n\t}, layoutWrap->lifetime());\n\n\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\tconst auto enabledValue = toggle->toggled();\n\t\tconst auto tabsValue = (group->current() == LayoutType::Tabs);\n\t\tcallback(enabledValue, tabsValue);\n\t\tbox->closeBox();\n\t});\n\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/toggle_topics_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n\nnamespace Ui {\n\nvoid ToggleTopicsBox(\n    not_null<Ui::GenericBox*> box,\n    bool enabled,\n    bool tabs,\n\tFn<void(bool enabled, bool tabs)> callback);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/verify_peers_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/peers/verify_peers_box.h\"\n\n#include \"apiwrap.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_polls.h\"\n\nnamespace {\n\nconstexpr auto kSetupVerificationToastDuration = 4 * crl::time(1000);\n\nclass Controller final : public ChatsListBoxController {\npublic:\n\tController(not_null<Main::Session*> session, not_null<UserData*> bot)\n\t: ChatsListBoxController(session)\n\t, _bot(bot) {\n\t}\n\n\tMain::Session &session() const override;\n\n\tvoid rowClicked(gsl::not_null<PeerListRow*> row) override;\n\nprivate:\n\tstd::unique_ptr<Row> createRow(not_null<History*> history) override;\n\tvoid prepareViewHook() override;\n\n\tvoid confirmAdd(not_null<PeerData*> peer);\n\tvoid confirmRemove(not_null<PeerData*> peer);\n\n\tconst not_null<UserData*> _bot;\n\n};\n\nvoid Setup(\n\t\tnot_null<UserData*> bot,\n\t\tnot_null<PeerData*> peer,\n\t\tQString description,\n\t\tFn<void(QString)> done) {\n\tusing Flag = MTPbots_SetCustomVerification::Flag;\n\tbot->session().api().request(MTPbots_SetCustomVerification(\n\t\tMTP_flags(Flag::f_bot\n\t\t\t| Flag::f_enabled\n\t\t\t| (description.isEmpty() ? Flag() : Flag::f_custom_description)),\n\t\tbot->inputUser(),\n\t\tpeer->input(),\n\t\tMTP_string(description)\n\t)).done([=] {\n\t\tdone(QString());\n\t}).fail([=](const MTP::Error &error) {\n\t\tdone(error.type());\n\t}).send();\n}\n\nvoid Remove(\n\t\tnot_null<UserData*> bot,\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void(QString)> done) {\n\tbot->session().api().request(MTPbots_SetCustomVerification(\n\t\tMTP_flags(MTPbots_SetCustomVerification::Flag::f_bot),\n\t\tbot->inputUser(),\n\t\tpeer->input(),\n\t\tMTPstring()\n\t)).done([=] {\n\t\tdone(QString());\n\t}).fail([=](const MTP::Error &error) {\n\t\tdone(error.type());\n\t}).send();\n}\n\nMain::Session &Controller::session() const {\n\treturn _bot->session();\n}\n\nvoid Controller::rowClicked(gsl::not_null<PeerListRow*> row) {\n\tconst auto peer = row->peer();\n\tconst auto details = peer->botVerifyDetails();\n\tconst auto already = details && (details->botId == peerToUser(_bot->id));\n\tif (already) {\n\t\tconfirmRemove(peer);\n\t} else {\n\t\tconfirmAdd(peer);\n\t}\n}\n\nvoid Controller::confirmAdd(not_null<PeerData*> peer) {\n\tconst auto bot = _bot;\n\tconst auto show = delegate()->peerListUiShow();\n\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tstruct State {\n\t\t\tUi::InputField *field = nullptr;\n\t\t\tQString description;\n\t\t\tbool sent = false;\n\t\t};\n\t\tconst auto settings = bot->botInfo\n\t\t\t? bot->botInfo->verifierSettings.get()\n\t\t\t: nullptr;\n\t\tconst auto modify = settings && settings->canModifyDescription;\n\t\tconst auto state = std::make_shared<State>(State{\n\t\t\t.description = settings ? settings->customDescription : QString()\n\t\t});\n\n\t\tconst auto limit = session().appConfig().get<int>(\n\t\t\tu\"bot_verification_description_length_limit\"_q,\n\t\t\t70);\n\t\tconst auto send = [=] {\n\t\t\tif (modify && state->description.size() > limit) {\n\t\t\t\tstate->field->showError();\n\t\t\t\treturn;\n\t\t\t} else if (state->sent) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->sent = true;\n\t\t\tconst auto weak = base::make_weak(box);\n\t\t\tconst auto description = modify ? state->description : QString();\n\t\t\tSetup(bot, peer, description, [=](QString error) {\n\t\t\t\tif (error.isEmpty()) {\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t\tshow->showToast({\n\t\t\t\t\t\t.text = PeerVerifyPhrases(peer).sent(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\t\ttr::bold(peer->shortName()),\n\t\t\t\t\t\t\ttr::marked),\n\t\t\t\t\t\t.duration = kSetupVerificationToastDuration,\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tstate->sent = false;\n\t\t\t\t\tshow->showToast(error);\n\t\t\t\t}\n\t\t\t});\n\t\t};\n\n\t\tconst auto phrases = PeerVerifyPhrases(peer);\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = phrases.text(\n\t\t\t\tlt_name,\n\t\t\t\trpl::single(tr::bold(peer->shortName())),\n\t\t\t\ttr::marked),\n\t\t\t.confirmed = send,\n\t\t\t.confirmText = phrases.submit(),\n\t\t\t.title = phrases.title(),\n\t\t});\n\t\tif (!modify) {\n\t\t\treturn;\n\t\t}\n\n\t\tUi::AddSubsectionTitle(\n\t\t\tbox->verticalLayout(),\n\t\t\ttr::lng_bot_verify_description_label(),\n\t\t\tQMargins(0, 0, 0, -st::defaultSubsectionTitlePadding.bottom()));\n\n\t\tconst auto field = box->addRow(object_ptr<Ui::InputField>(\n\t\t\tbox,\n\t\t\tst::createPollField,\n\t\t\tUi::InputField::Mode::NoNewlines,\n\t\t\trpl::single(state->description),\n\t\t\tstate->description\n\t\t), st::createPollFieldPadding);\n\t\tstate->field = field;\n\n\t\tbox->setFocusCallback([=] {\n\t\t\tfield->setFocusFast();\n\t\t});\n\n\t\tUi::AddSkip(box->verticalLayout());\n\n\t\tfield->changes() | rpl::on_next([=] {\n\t\t\tstate->description = field->getLastText();\n\t\t}, field->lifetime());\n\n\t\tfield->setMaxLength(limit * 2);\n\t\tUi::AddLengthLimitLabel(field, limit);\n\n\t\tUi::AddDividerText(box->verticalLayout(), phrases.about());\n\t}));\n}\n\nvoid Controller::confirmRemove(not_null<PeerData*> peer) {\n\tconst auto bot = _bot;\n\tconst auto show = delegate()->peerListUiShow();\n\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto sent = std::make_shared<bool>();\n\t\tconst auto send = [=] {\n\t\t\tif (*sent) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t*sent = true;\n\t\t\tconst auto weak = base::make_weak(box);\n\t\t\tRemove(bot, peer, [=](QString error) {\n\t\t\t\tif (error.isEmpty()) {\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t\tshow->showToast(tr::lng_bot_verify_remove_done(tr::now));\n\t\t\t\t} else {\n\t\t\t\t\t*sent = false;\n\t\t\t\t\tshow->showToast(error);\n\t\t\t\t}\n\t\t\t});\n\t\t};\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = PeerVerifyPhrases(peer).remove(),\n\t\t\t.confirmed = send,\n\t\t\t.confirmText = tr::lng_bot_verify_remove_submit(),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t.title = tr::lng_bot_verify_remove_title(),\n\t\t});\n\t}));\n}\n\nauto Controller::createRow(not_null<History*> history)\n-> std::unique_ptr<Row> {\n\tconst auto peer = history->peer;\n\tconst auto may = peer->isUser() || peer->isChannel();\n\treturn may ? std::make_unique<Row>(history) : nullptr;\n}\n\nvoid Controller::prepareViewHook() {\n}\n\n} // namespace\n\nobject_ptr<Ui::BoxContent> MakeVerifyPeersBox(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<UserData*> bot) {\n\tconst auto session = &window->session();\n\tauto controller = std::make_unique<Controller>(session, bot);\n\tauto init = [=](not_null<PeerListBox*> box) {\n\t\tbox->setTitle(tr::lng_bot_verify_title());\n\t\tbox->addButton(tr::lng_box_done(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t};\n\treturn Box<PeerListBox>(std::move(controller), std::move(init));\n}\n\nBotVerifyPhrases PeerVerifyPhrases(not_null<PeerData*> peer) {\n\tif (const auto user = peer->asUser()) {\n\t\tif (user->isBot()) {\n\t\t\treturn {\n\t\t\t\t.title = tr::lng_bot_verify_bot_title,\n\t\t\t\t.text = tr::lng_bot_verify_bot_text,\n\t\t\t\t.about = tr::lng_bot_verify_bot_about,\n\t\t\t\t.submit = tr::lng_bot_verify_bot_submit,\n\t\t\t\t.sent = tr::lng_bot_verify_bot_sent,\n\t\t\t\t.remove = tr::lng_bot_verify_bot_remove,\n\t\t\t};\n\t\t} else {\n\t\t\treturn {\n\t\t\t\t.title = tr::lng_bot_verify_user_title,\n\t\t\t\t.text = tr::lng_bot_verify_user_text,\n\t\t\t\t.about = tr::lng_bot_verify_user_about,\n\t\t\t\t.submit = tr::lng_bot_verify_user_submit,\n\t\t\t\t.sent = tr::lng_bot_verify_user_sent,\n\t\t\t\t.remove = tr::lng_bot_verify_user_remove,\n\t\t\t};\n\t\t}\n\t} else if (peer->isBroadcast()) {\n\t\treturn {\n\t\t\t.title = tr::lng_bot_verify_channel_title,\n\t\t\t.text = tr::lng_bot_verify_channel_text,\n\t\t\t.about = tr::lng_bot_verify_channel_about,\n\t\t\t.submit = tr::lng_bot_verify_channel_submit,\n\t\t\t.sent = tr::lng_bot_verify_channel_sent,\n\t\t\t.remove = tr::lng_bot_verify_channel_remove,\n\t\t};\n\t}\n\treturn {\n\t\t.title = tr::lng_bot_verify_group_title,\n\t\t.text = tr::lng_bot_verify_group_text,\n\t\t.about = tr::lng_bot_verify_group_about,\n\t\t.submit = tr::lng_bot_verify_group_submit,\n\t\t.sent = tr::lng_bot_verify_group_sent,\n\t\t.remove = tr::lng_bot_verify_group_remove,\n\t};\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/peers/verify_peers_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"lang/lang_keys.h\"\n\nclass PeerData;\nclass UserData;\n\nnamespace Ui {\nclass BoxContent;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\n[[nodiscard]] object_ptr<Ui::BoxContent> MakeVerifyPeersBox(\n\tnot_null<Window::SessionController*> window,\n\tnot_null<UserData*> bot);\n\nstruct BotVerifyPhrases {\n\ttr::phrase<> title;\n\ttr::phrase<lngtag_name> text;\n\ttr::phrase<> about;\n\ttr::phrase<> submit;\n\ttr::phrase<lngtag_name> sent;\n\ttr::phrase<> remove;\n};\n[[nodiscard]] BotVerifyPhrases PeerVerifyPhrases(not_null<PeerData*> peer);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/phone_banned_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/phone_banned_box.h\"\n\n#include \"ui/boxes/confirm_box.h\"\n#include \"core/click_handler_types.h\" // UrlClickHandler\n#include \"base/qthelp_url.h\" // qthelp::url_encode\n#include \"base/platform/base_platform_info.h\"\n#include \"window/window_controller.h\"\n#include \"lang/lang_keys.h\"\n\nnamespace Ui {\n\nnamespace {\n\nvoid SendToBannedHelp(const QString &phone) {\n\tconst auto version = QString::fromLatin1(AppVersionStr)\n\t\t+ (cAlphaVersion()\n\t\t\t? qsl(\" alpha %1\").arg(cAlphaVersion())\n\t\t\t: (AppBetaVersion ? \" beta\" : \"\"));\n\n\tconst auto subject = qsl(\"Banned phone number: \") + phone;\n\n\tconst auto body = qsl(\"\\\nI'm trying to use my mobile phone number: \") + phone + qsl(\"\\n\\\nBut Telegram says it's banned. Please help.\\n\\\n\\n\\\nApp version: \") + version + qsl(\"\\n\\\nOS version: \") + ::Platform::SystemVersionPretty() + qsl(\"\\n\\\nLocale: \") + ::Platform::SystemLanguage();\n\n\tconst auto url = \"mailto:?to=\"\n\t\t+ qthelp::url_encode(\"login@stel.com\")\n\t\t+ \"&subject=\"\n\t\t+ qthelp::url_encode(subject)\n\t\t+ \"&body=\"\n\t\t+ qthelp::url_encode(body);\n\n\tUrlClickHandler::Open(url);\n}\n\n} // namespace\n\nvoid ShowPhoneBannedError(\n\t\tnot_null<Window::Controller*> controller,\n\t\tconst QString &phone) {\n\tconst auto box = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tconst auto close = [=] {\n\t\tif (*box) {\n\t\t\t(*box)->closeBox();\n\t\t}\n\t};\n\t*box = controller->show(\n\t\tUi::MakeConfirmBox({\n\t\t\t.text = tr::lng_signin_banned_text(),\n\t\t\t.cancelled = [=](Fn<void()> &&close) {\n\t\t\t\tSendToBannedHelp(phone);\n\t\t\t\tclose();\n\t\t\t},\n\t\t\t.confirmText = tr::lng_box_ok(),\n\t\t\t.cancelText = tr::lng_signin_banned_help(),\n\t\t\t.strictCancel = true,\n\t\t}),\n\t\tUi::LayerOption::CloseOther);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/phone_banned_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Window {\nclass Controller;\n} // namespace Window\n\nnamespace Ui {\n\nvoid ShowPhoneBannedError(\n\tnot_null<Window::Controller*> controller,\n\tconst QString &phone);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/pin_messages_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/pin_messages_box.h\"\n\n#include \"apiwrap.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace {\n\n[[nodiscard]] bool IsOldForPin(\n\t\tMsgId id,\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId) {\n\tconst auto normal = peer->migrateToOrMe();\n\tconst auto migrated = normal->migrateFrom();\n\tconst auto top = Data::ResolveTopPinnedId(\n\t\tnormal,\n\t\ttopicRootId,\n\t\tmonoforumPeerId,\n\t\tmigrated);\n\tif (!top) {\n\t\treturn false;\n\t} else if (peer == migrated) {\n\t\treturn peerIsChannel(top.peer) || (id < top.msg);\n\t} else if (migrated) {\n\t\treturn peerIsChannel(top.peer) && (id < top.msg);\n\t} else {\n\t\treturn (id < top.msg);\n\t}\n}\n\n} // namespace\n\nvoid PinMessageBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<HistoryItem*> item) {\n\tstruct State {\n\t\tbase::weak_qptr<Ui::Checkbox> pinForPeer;\n\t\tbase::weak_qptr<Ui::Checkbox> notify;\n\t\tmtpRequestId requestId = 0;\n\t};\n\n\tconst auto peer = item->history()->peer;\n\tconst auto msgId = item->id;\n\tconst auto topicRootId = item->topic() ? item->topicRootId() : MsgId();\n\tconst auto monoforumPeerId = item->history()->peer->amMonoforumAdmin()\n\t\t? item->sublistPeerId()\n\t\t: PeerId();\n\tconst auto pinningOld = IsOldForPin(\n\t\tmsgId,\n\t\tpeer,\n\t\ttopicRootId,\n\t\tmonoforumPeerId);\n\tconst auto state = box->lifetime().make_state<State>();\n\tconst auto api = box->lifetime().make_state<MTP::Sender>(\n\t\t&peer->session().mtp());\n\n\tauto checkbox = [&]() -> object_ptr<Ui::Checkbox> {\n\t\tif (peer->isUser() && !peer->isSelf()) {\n\t\t\tauto object = object_ptr<Ui::Checkbox>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_pinned_also_for_other(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tpeer->shortName()),\n\t\t\t\tfalse,\n\t\t\t\tst::urlAuthCheckbox);\n\t\t\tobject->setAllowTextLines();\n\t\t\tstate->pinForPeer = base::make_weak(object.data());\n\t\t\treturn object;\n\t\t} else if (!pinningOld\n\t\t\t&& (peer->isChat() || peer->isMegagroup())\n\t\t\t&& !peer->isMonoforum()) {\n\t\t\tauto object = object_ptr<Ui::Checkbox>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_pinned_notify(tr::now),\n\t\t\t\ttrue,\n\t\t\t\tst::urlAuthCheckbox);\n\t\t\tobject->setAllowTextLines();\n\t\t\tstate->notify = base::make_weak(object.data());\n\t\t\treturn object;\n\t\t}\n\t\treturn { nullptr };\n\t}();\n\n\tauto pinMessage = [=] {\n\t\tif (state->requestId) {\n\t\t\treturn;\n\t\t}\n\n\t\tauto flags = MTPmessages_UpdatePinnedMessage::Flags(0);\n\t\tif (state->notify && !state->notify->checked()) {\n\t\t\tflags |= MTPmessages_UpdatePinnedMessage::Flag::f_silent;\n\t\t}\n\t\tif (state->pinForPeer && !state->pinForPeer->checked()) {\n\t\t\tflags |= MTPmessages_UpdatePinnedMessage::Flag::f_pm_oneside;\n\t\t}\n\t\tstate->requestId = api->request(MTPmessages_UpdatePinnedMessage(\n\t\t\tMTP_flags(flags),\n\t\t\tpeer->input(),\n\t\t\tMTP_int(msgId)\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\tpeer->session().api().applyUpdates(result);\n\t\t\tbox->closeBox();\n\t\t}).fail([=] {\n\t\t\tbox->closeBox();\n\t\t}).send();\n\t};\n\n\tUi::ConfirmBox(box, {\n\t\t.text = (pinningOld\n\t\t\t? tr::lng_pinned_pin_old_sure()\n\t\t\t: (peer->isChat() || peer->isMegagroup())\n\t\t\t? tr::lng_pinned_pin_sure_group()\n\t\t\t: tr::lng_pinned_pin_sure()),\n\t\t.confirmed = std::move(pinMessage),\n\t\t.confirmText = tr::lng_pinned_pin(),\n\t});\n\n\tif (checkbox) {\n\t\tauto padding = st::boxPadding;\n\t\tpadding.setTop(padding.bottom());\n\t\tbox->addRow(std::move(checkbox), std::move(padding));\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/pin_messages_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nvoid PinMessageBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<HistoryItem*> item);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/polls.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\n\nusing \"ui/widgets/widgets.style\";\nusing \"boxes/boxes.style\";\nusing \"chat_helpers/chat_helpers.style\";\n\nhistoryPollQuestionFont: semiboldFont;\nhistoryPollQuestionStyle: TextStyle(defaultTextStyle) {\n\tfont: historyPollQuestionFont;\n}\nhistoryPollAnswerStyle: defaultTextStyle;\nhistoryPollQuestionTop: 7px;\nhistoryPollMediaHeight: 180px;\nhistoryPollMediaTopSkip: 1px;\nhistoryPollMediaSideSkip: 1px;\nhistoryPollMediaSkip: 10px;\nhistoryPollDescriptionSkip: 8px;\nhistoryPollSubtitleSkip: 4px;\nhistoryPollAnswerPadding: margins(32px, 6px, 0px, 6px);\nhistoryPollAnswerPaddingNoMedia: margins(32px, 10px, 0px, 10px);\nhistoryPollAnswersSkip: 2px;\nhistoryPollPercentFont: semiboldFont;\nhistoryPollPercentSkip: 5px;\nhistoryPollPercentTop: 0px;\nhistoryPollTotalVotesSkip: 5px;\nhistoryPollFillingMin: 4px;\nhistoryPollFillingHeight: 4px;\nhistoryPollFillingRadius: 2px;\nhistoryPollFillingBgOpacity: 0.15;\nhistoryPollFillingBottom: 2px;\nhistoryPollFillingTop: 6px;\nhistoryPollFillingRight: 4px;\nhistoryPollRadio: Radio(defaultRadio) {\n\tbg: transparent;\n\tuntoggledFg: checkboxFg;\n\ttoggledFg: windowBgActive;\n\tdiameter: 18px;\n\tthickness: 2px;\n\tskip: 65px; // * 0.1\n\tduration: universalDuration;\n\trippleAreaPadding: 8px;\n}\nhistoryPollCheckboxRadius: 3px;\nhistoryPollReplyIcon: icon {{ \"dialogs/dialogs_chatlist_poll-17x17\", windowFg }};\nhistoryPollReplyIconSkip: 3px;\nhistoryPollRadioOpacity: 0.7;\nhistoryPollRadioOpacityOver: 1.;\nhistoryPollDuration: 300;\nhistoryPollRadialAnimation: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {\n\tthickness: 2px;\n\tsize: size(18px, 18px);\n}\nhistoryPollRippleOpacity: 0.3;\nhistoryPollRecentVotersSkip: 4px;\nhistoryPollRecentVoterSize: 18px;\nhistoryPollRecentVoterSkip: 13px;\nhistoryPollAnswerUserpics: GroupCallUserpics {\n\tsize: 16px;\n\tshift: 5px;\n\tstroke: 1px;\n\talign: align(left);\n}\nhistoryPollAnswerUserpicsSkip: 4px;\nhistoryPollBottomButtonSkip: 15px;\nhistoryPollBottomButtonTop: 4px;\nhistoryPollExplanationSkip: 8px;\nhistoryPollExplanationTitleSkip: 4px;\nhistoryPollExplanationCloseSize: 20px;\nhistoryPollExplanationCloseIconSize: 8px;\nhistoryPollExplanationCloseStroke: 2px;\nhistoryPollExplanationMediaMaxHeight: 300px;\nhistoryPollExplanationMediaSkip: 6px;\nhistoryPollChoiceRight: icon {{ \"poll_choice_right\", activeButtonFg }};\nhistoryPollChoiceWrong: icon {{ \"poll_choice_wrong\", activeButtonFg }};\nhistoryPollOutChoiceRight: icon {{ \"poll_choice_right\", historyFileOutIconFg }};\nhistoryPollOutChoiceRightSelected: icon {{ \"poll_choice_right\", historyFileOutIconFgSelected }};\nhistoryPollInChoiceRight: icon {{ \"poll_choice_right\", historyFileInIconFg }};\nhistoryPollInChoiceRightSelected: icon {{ \"poll_choice_right\", historyFileInIconFgSelected }};\nhistoryPollOutChosen: icon {{ \"poll_select_check\", historyFileOutIconFg }};\nhistoryPollOutChosenSelected: icon {{ \"poll_select_check\", historyFileOutIconFgSelected }};\nhistoryPollInChosen: icon {{ \"poll_select_check\", historyFileInIconFg }};\nhistoryPollInChosenSelected: icon {{ \"poll_select_check\", historyFileInIconFgSelected }};\n\nhistoryPollAddOptionTop: 3px;\nhistoryPollAddOptionHeight: 32px;\nhistoryPollAddOptionButtonSize: 26px;\nhistoryPollAddOptionEmojiLeft: -4px;\nhistoryPollAddOptionField: InputField(defaultInputField) {\n\ttextBg: transparent;\n\ttextFg: windowFg;\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\ttextMargins: margins(32px, 7px, 28px, 4px);\n\theightMin: 32px;\n\theightMax: 32px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(13px);\n\t}\n\tplaceholderMargins: margins(0px, 1px, 0px, 0px);\n\tplaceholderFont: font(12px);\n\tplaceholderScale: 0.;\n\tborder: 0px;\n\tborderActive: 0px;\n}\nhistoryPollAddOptionEmoji: IconButton(defaultIconButton) {\n\twidth: 26px;\n\theight: 26px;\n\ticon: icon {{ \"poll/general/outline_poll_emoji-22x22\", historyComposeIconFg }};\n\ticonOver: icon {{ \"poll/general/outline_poll_emoji-22x22\", historyComposeIconFgOver }};\n\trippleAreaSize: 0px;\n}\nhistoryPollAddOptionAttach: IconButton(defaultIconButton) {\n\twidth: 26px;\n\theight: 26px;\n\ticon: icon {{ \"poll/general/outline_poll_attach-22x22\", historyComposeIconFg }};\n\ticonOver: icon {{ \"poll/general/outline_poll_attach-22x22\", historyComposeIconFgOver }};\n\trippleAreaPosition: point(0px, 0px);\n\trippleAreaSize: 26px;\n\tripple: defaultRippleAnimationBgOver;\n}\n\npollBoxOutlinePollEmojiIcon: icon{{ \"poll/general/outline_poll_emoji\", activeButtonFg }};\npollBoxOutlinePollAddIcon: icon{{ \"poll/general/outline_poll_add-18x18\", activeButtonFg }};\npollBoxOutlinePollAttachIcon: icon{{ \"poll/general/outline_poll_attach\", activeButtonFg }};\npollBoxMenuPollOrderIcon: icon{{ \"poll/general/menu_poll_order-24x24\", historyComposeIconFg }};\npollBoxFilledPollDeadlineIcon: icon{{ \"poll/filled/filled_poll_deadline\", activeButtonFg }};\npollBoxFilledPollViewIcon: icon{{ \"poll/filled/filled_poll_view\", activeButtonFg }};\npollBoxFilledPollAddIcon: icon{{ \"poll/filled/filled_poll_add\", activeButtonFg }};\npollBoxFilledPollCorrectIcon: icon{{ \"poll/filled/filled_poll_correct\", activeButtonFg }};\npollBoxFilledPollRevoteIcon: icon{{ \"poll/filled/filled_poll_revote\", activeButtonFg }};\npollBoxFilledPollShuffleIcon: icon{{ \"poll/filled/filled_poll_shuffle\", activeButtonFg }};\npollBoxFilledPollMultipleIcon: icon{{ \"poll/filled/filled_poll_multiple\", activeButtonFg }};\n\npollAttachTextSkip: 28px;\npollAttachProgressMargin: 4px;\npollAttachCancel: icon {{ \"history_audio_cancel\", historyFileThumbIconFg }};\npollAttachView: icon {{ \"mediaview/views\", historyFileThumbIconFg }};\npollAttachShift: point(-11px, -2px);\n\ncreatePollField: InputField(defaultInputField) {\n\ttextMargins: margins(0px, 4px, 0px, 4px);\n\ttextAlign: align(left);\n\theightMin: 36px;\n\theightMax: 86px;\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(2px, 0px, 2px, 0px);\n\tplaceholderAlign: align(topleft);\n\tplaceholderScale: 0.;\n\tplaceholderFont: boxTextFont;\n\tplaceholderShift: -50px;\n\tborder: 0px;\n\tborderActive: 0px;\n\tduration: 100;\n}\ncreatePollFieldPadding: margins(22px, 5px, 22px, 5px);\ncreatePollOptionField: InputField(createPollField) {\n\ttextMargins: margins(22px, 11px, 40px, 11px);\n\tplaceholderMargins: margins(2px, 0px, 2px, 0px);\n\theightMax: 68px;\n}\ncreatePollOptionFieldPremium: InputField(createPollOptionField) {\n\ttextMargins: margins(22px, 11px, 72px, 11px);\n}\ncreatePollOptionFieldPremiumEmojiPosition: point(13px, -1px);\ncreatePollSolutionField: InputField(createPollField) {\n\ttextMargins: margins(0px, 4px, 0px, 4px);\n\tborder: 1px;\n\tborderActive: 2px;\n}\ncreatePollLimitPadding: margins(22px, 10px, 22px, 16px);\ncreatePollOptionRemove: CrossButton {\n\twidth: 22px;\n\theight: 22px;\n\n\tcross: CrossAnimation {\n\t\tsize: 22px;\n\t\tskip: 6px;\n\t\tstroke: 1.5;\n\t\tminScale: 0.3;\n\t}\n\tcrossFg: boxTitleCloseFg;\n\tcrossFgOver: boxTitleCloseFgOver;\n\tcrossPosition: point(0px, 0px);\n\n\tduration: 150;\n\tloadingPeriod: 1000;\n\tripple: defaultRippleAnimationBgOver;\n}\ncreatePollAttachPosition: point(0px, 0px);\ncreatePollOptionRemovePosition: point(11px, 9px);\ncreatePollOptionEmojiPositionSkip: 6px;\ncreatePollWarning: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n\tpalette: TextPalette(defaultTextPalette) {\n\t\tlinkFg: boxTextFgError;\n\t}\n}\ncreatePollWarningPosition: point(16px, 6px);\ncreatePollCheckboxMargin: margins(22px, 10px, 22px, 10px);\ncreatePollFieldTitlePadding: margins(22px, 7px, 10px, 6px);\n\ncreateTodoOptionField: InputField(createPollOptionField) {\n\ttextMargins: margins(22px, 11px, 68px, 11px);\n}\ncreateTodoOptionFieldEmojiPosition: point(13px, -1px);\n\npollResultsQuestion: FlatLabel(defaultFlatLabel) {\n\tminWidth: 320px;\n\ttextFg: windowBoldFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(16px semibold);\n\t}\n}\npollResultsVotesCount: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n}\npollResultsHeaderPadding: margins(22px, 22px, 22px, 8px);\npollResultsShowMore: SettingsButton(defaultSettingsButton) {\n\ttextFg: lightButtonFg;\n\ttextFgOver: lightButtonFgOver;\n\ttextBg: windowBg;\n\ttextBgOver: windowBgOver;\n\n\tstyle: semiboldTextStyle;\n\n\theight: 20px;\n\tpadding: margins(71px, 10px, 8px, 8px);\n\n\tripple: defaultRippleAnimation;\n}\npollResultsVoteTimeFont: font(fsize);\npollResultsVoteTimeDateFont: font(fsize);\npollResultsVoteTimeRightSkip: 16px;\npollResultsVoteTimeLeftSkip: 8px;\npollResultsVoteTimeGap: 2px;\n\npollAttach: IconButton(defaultIconButton) {\n\twidth: pollAttachTextSkip;\n\theight: pollAttachTextSkip;\n\n\ticon: icon {{ \"poll/general/outline_poll_attach-26x26\", historyComposeIconFg }};\n\ticonOver: icon {{ \"poll/general/outline_poll_attach-26x26\", historyComposeIconFgOver }};\n\n\trippleAreaPosition: point(0px, 0px);\n\trippleAreaSize: pollAttachTextSkip;\n\tripple: defaultRippleAnimationBgOver;\n}\n\ncreatePollDurationIconSize: 20px;\ncreatePollDurationIconPosition: point(1px, 5px);\ncreatePollDurationIconMargins: margins(2px, 2px, 2px, 2px);\n\npollDescriptionFieldPadding: margins(22px, 5px, 11px, 5px);\npollDescriptionField: InputField(createPollField) {\n\ttextMargins: margins(0px, 4px, pollAttachTextSkip + pollAttachTextSkip + 1px, 4px);\n\tborder: 0px;\n\tborderActive: 0px;\n}\n\npollMediaField: InputField(createPollSolutionField) {\n\ttextMargins: margins(0px, 4px, pollAttachTextSkip, 4px);\n\tborder: 0px;\n\tborderActive: 0px;\n}\n\ninfoPollsNewButtonBottom: 19px;\n\npollToastUploadingIconSize: size(30px, 30px);\npollToastIconSize: size(22px, 22px);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/premium_limits_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/premium_limits_box.h\"\n\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/peer_list_dummy.h\"\n#include \"ui/effects/premium_bubble.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"main/main_session.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/peers/prepare_short_info_box.h\" // PrepareShortInfoBox\n#include \"window/window_session_controller.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_user.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_session.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_premium_limits.h\"\n#include \"lang/lang_keys.h\"\n#include \"settings/sections/settings_premium.h\" // ShowPremium.\n#include \"base/unixtime.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_settings.h\"\n\nnamespace {\n\nstruct InfographicDescriptor {\n\tfloat64 defaultLimit = 0;\n\tfloat64 current = 0;\n\tfloat64 premiumLimit = 0;\n\tconst style::icon *icon;\n\tstd::optional<tr::phrase<lngtag_count>> phrase;\n\tbool complexRatio = false;\n};\n\nvoid AddSubtitle(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> text) {\n\tconst auto &subtitlePadding = st::settingsButton.padding;\n\tUi::AddSubsectionTitle(\n\t\tcontainer,\n\t\tstd::move(text),\n\t\t{ 0, subtitlePadding.top(), 0, -subtitlePadding.bottom() });\n}\n\nclass InactiveController final : public PeerListController {\npublic:\n\texplicit InactiveController(not_null<Main::Session*> session);\n\t~InactiveController();\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\n\t[[nodiscard]] rpl::producer<int> countValue() const;\n\nprivate:\n\tvoid appendRow(not_null<PeerData*> peer, TimeId date);\n\t[[nodiscard]] std::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<PeerData*> peer,\n\t\tTimeId date) const;\n\n\tconst not_null<Main::Session*> _session;\n\trpl::variable<int> _count;\n\tmtpRequestId _requestId = 0;\n\n};\n\nclass PublicsController final : public PeerListController {\npublic:\n\tPublicsController(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tFn<void()> closeBox);\n\t~PublicsController();\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override;\n\n\t[[nodiscard]] rpl::producer<int> countValue() const;\n\nprivate:\n\tvoid appendRow(not_null<PeerData*> peer);\n\t[[nodiscard]] std::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<PeerData*> peer) const;\n\n\tconst not_null<Window::SessionNavigation*> _navigation;\n\trpl::variable<int> _count;\n\tFn<void()> _closeBox;\n\tmtpRequestId _requestId = 0;\n\n};\n\nclass InactiveDelegate final : public PeerListContentDelegate {\npublic:\n\tvoid peerListSetTitle(rpl::producer<QString> title) override;\n\tvoid peerListSetAdditionalTitle(rpl::producer<QString> title) override;\n\tbool peerListIsRowChecked(not_null<PeerListRow*> row) override;\n\tint peerListSelectedRowsCount() override;\n\tvoid peerListScrollToTop() override;\n\tvoid peerListAddSelectedPeerInBunch(\n\t\tnot_null<PeerData*> peer) override;\n\tvoid peerListAddSelectedRowInBunch(\n\t\tnot_null<PeerListRow*> row) override;\n\tvoid peerListFinishSelectedRowsBunch() override;\n\tvoid peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) override;\n\tstd::shared_ptr<Main::SessionShow> peerListUiShow() override;\n\tvoid peerListSetRowChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked) override;\n\n\t[[nodiscard]] rpl::producer<int> selectedCountChanges() const;\n\t[[nodiscard]] const base::flat_set<PeerListRowId> &selected() const;\n\nprivate:\n\tbase::flat_set<PeerListRowId> _selectedIds;\n\trpl::event_stream<int> _selectedCountChanges;\n\n};\n\n[[nodiscard]] Ui::Premium::BubbleType ChooseBubbleType(bool premium) {\n\treturn premium\n\t\t? Ui::Premium::BubbleType::Premium\n\t\t: Ui::Premium::BubbleType::NoPremium;\n}\n\nvoid InactiveDelegate::peerListSetTitle(rpl::producer<QString> title) {\n}\n\nvoid InactiveDelegate::peerListSetAdditionalTitle(\n\trpl::producer<QString> title) {\n}\n\nbool InactiveDelegate::peerListIsRowChecked(not_null<PeerListRow*> row) {\n\treturn _selectedIds.contains(row->id());\n}\n\nint InactiveDelegate::peerListSelectedRowsCount() {\n\treturn int(_selectedIds.size());\n}\n\nvoid InactiveDelegate::peerListScrollToTop() {\n}\n\nvoid InactiveDelegate::peerListAddSelectedPeerInBunch(\n\t\tnot_null<PeerData*> peer) {\n\t_selectedIds.emplace(PeerListRowId(peer->id.value));\n\t_selectedCountChanges.fire(int(_selectedIds.size()));\n}\n\nvoid InactiveDelegate::peerListAddSelectedRowInBunch(\n\t\tnot_null<PeerListRow*> row) {\n\t_selectedIds.emplace(row->id());\n\t_selectedCountChanges.fire(int(_selectedIds.size()));\n}\n\nvoid InactiveDelegate::peerListSetRowChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool checked) {\n\tif (checked) {\n\t\t_selectedIds.emplace(row->id());\n\t} else {\n\t\t_selectedIds.remove(row->id());\n\t}\n\t_selectedCountChanges.fire(int(_selectedIds.size()));\n\tPeerListContentDelegate::peerListSetRowChecked(row, checked);\n}\n\nvoid InactiveDelegate::peerListFinishSelectedRowsBunch() {\n}\n\nvoid InactiveDelegate::peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) {\n\tdescription.destroy();\n}\n\nstd::shared_ptr<Main::SessionShow> InactiveDelegate::peerListUiShow() {\n\tUnexpected(\"...InactiveDelegate::peerListUiShow\");\n}\n\nrpl::producer<int> InactiveDelegate::selectedCountChanges() const {\n\treturn _selectedCountChanges.events();\n}\n\nconst base::flat_set<PeerListRowId> &InactiveDelegate::selected() const {\n\treturn _selectedIds;\n}\n\nInactiveController::InactiveController(not_null<Main::Session*> session)\n: _session(session) {\n}\n\nInactiveController::~InactiveController() {\n\tif (_requestId) {\n\t\t_session->api().request(_requestId).cancel();\n\t}\n}\n\nMain::Session &InactiveController::session() const {\n\treturn *_session;\n}\n\nvoid InactiveController::prepare() {\n\t_requestId = _session->api().request(MTPchannels_GetInactiveChannels(\n\t)).done([=](const MTPmessages_InactiveChats &result) {\n\t\t_requestId = 0;\n\t\tconst auto &data = result.data();\n\t\t_session->data().processUsers(data.vusers());\n\t\tconst auto &list = data.vchats().v;\n\t\tconst auto &dates = data.vdates().v;\n\t\tfor (auto i = 0, count = int(list.size()); i != count; ++i) {\n\t\t\tconst auto peer = _session->data().processChat(list[i]);\n\t\t\tconst auto date = (i < dates.size()) ? dates[i].v : TimeId();\n\t\t\tappendRow(peer, date);\n\t\t}\n\t\tdelegate()->peerListRefreshRows();\n\t\t_count = delegate()->peerListFullRowsCount();\n\t}).send();\n}\n\nvoid InactiveController::rowClicked(not_null<PeerListRow*> row) {\n\tdelegate()->peerListSetRowChecked(row, !row->checked());\n}\n\nrpl::producer<int> InactiveController::countValue() const {\n\treturn _count.value();\n}\n\nvoid InactiveController::appendRow(\n\t\tnot_null<PeerData*> participant,\n\t\tTimeId date) {\n\tif (!delegate()->peerListFindRow(participant->id.value)) {\n\t\tdelegate()->peerListAppendRow(createRow(participant, date));\n\t}\n}\n\nstd::unique_ptr<PeerListRow> InactiveController::createRow(\n\t\tnot_null<PeerData*> peer,\n\t\tTimeId date) const {\n\tauto result = std::make_unique<PeerListRow>(peer);\n\tconst auto active = base::unixtime::parse(date).date();\n\tconst auto now = QDate::currentDate();\n\tconst auto time = [&] {\n\t\tconst auto days = active.daysTo(now);\n\t\tif (now < active) {\n\t\t\treturn QString();\n\t\t} else if (active == now) {\n\t\t\tconst auto unixtime = base::unixtime::now();\n\t\t\tconst auto delta = int64(unixtime) - int64(date);\n\t\t\tif (delta <= 0) {\n\t\t\t\treturn QString();\n\t\t\t} else if (delta >= 3600) {\n\t\t\t\treturn tr::lng_hours(tr::now, lt_count, delta / 3600);\n\t\t\t} else if (delta >= 60) {\n\t\t\t\treturn tr::lng_minutes(tr::now, lt_count, delta / 60);\n\t\t\t} else {\n\t\t\t\treturn tr::lng_seconds(tr::now, lt_count, delta);\n\t\t\t}\n\t\t} else if (days >= 365) {\n\t\t\treturn tr::lng_years(tr::now, lt_count, days / 365);\n\t\t} else if (days >= 31) {\n\t\t\treturn tr::lng_months(tr::now, lt_count, days / 31);\n\t\t} else if (days >= 7) {\n\t\t\treturn tr::lng_weeks(tr::now, lt_count, days / 7);\n\t\t} else {\n\t\t\treturn tr::lng_days(tr::now, lt_count, days);\n\t\t}\n\t}();\n\tresult->setCustomStatus(tr::lng_channels_leave_status(\n\t\ttr::now,\n\t\tlt_type,\n\t\t(peer->isBroadcast()\n\t\t\t? tr::lng_channel_status(tr::now)\n\t\t\t: tr::lng_group_status(tr::now)),\n\t\tlt_time,\n\t\ttime));\n\treturn result;\n}\n\nPublicsController::PublicsController(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tFn<void()> closeBox)\n: _navigation(navigation)\n, _closeBox(std::move(closeBox)) {\n}\n\nPublicsController::~PublicsController() {\n\tif (_requestId) {\n\t\t_navigation->session().api().request(_requestId).cancel();\n\t}\n}\n\nMain::Session &PublicsController::session() const {\n\treturn _navigation->session();\n}\n\nrpl::producer<int> PublicsController::countValue() const {\n\treturn _count.value();\n}\n\nvoid PublicsController::prepare() {\n\t_requestId = _navigation->session().api().request(\n\t\tMTPchannels_GetAdminedPublicChannels(MTP_flags(0))\n\t).done([=](const MTPmessages_Chats &result) {\n\t\t_requestId = 0;\n\n\t\tconst auto &chats = result.match([](const auto &data) {\n\t\t\treturn data.vchats().v;\n\t\t});\n\t\tauto &owner = _navigation->session().data();\n\t\tfor (const auto &chat : chats) {\n\t\t\tif (const auto peer = owner.processChat(chat)) {\n\t\t\t\tif (!peer->isChannel() || peer->username().isEmpty()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tappendRow(peer);\n\t\t\t}\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t\t_count = delegate()->peerListFullRowsCount();\n\t}).send();\n}\n\nvoid PublicsController::rowClicked(not_null<PeerListRow*> row) {\n\t_navigation->parentController()->show(\n\t\tPrepareShortInfoBox(row->peer(), _navigation));\n}\n\nvoid PublicsController::rowRightActionClicked(not_null<PeerListRow*> row) {\n\tconst auto peer = row->peer();\n\tconst auto textMethod = peer->isMegagroup()\n\t\t? tr::lng_channels_too_much_public_revoke_confirm_group\n\t\t: tr::lng_channels_too_much_public_revoke_confirm_channel;\n\tconst auto text = textMethod(\n\t\ttr::now,\n\t\tlt_link,\n\t\tpeer->session().createInternalLink(peer->username()),\n\t\tlt_group,\n\t\tpeer->name());\n\tconst auto confirmText = tr::lng_channels_too_much_public_revoke(\n\t\ttr::now);\n\tconst auto closeBox = _closeBox;\n\tconst auto once = std::make_shared<bool>(false);\n\tauto callback = crl::guard(_navigation, [=](Fn<void()> close) {\n\t\tif (*once) {\n\t\t\treturn;\n\t\t}\n\t\t*once = true;\n\t\tpeer->session().api().request(MTPchannels_UpdateUsername(\n\t\t\tpeer->asChannel()->inputChannel(),\n\t\t\tMTP_string()\n\t\t)).done([=] {\n\t\t\tpeer->session().api().request(MTPchannels_DeactivateAllUsernames(\n\t\t\t\tpeer->asChannel()->inputChannel()\n\t\t\t)).done([=] {\n\t\t\t\tcloseBox();\n\t\t\t\tclose();\n\t\t\t}).send();\n\t\t}).send();\n\t});\n\t_navigation->parentController()->show(\n\t\tUi::MakeConfirmBox({\n\t\t\t.text = text,\n\t\t\t.confirmed = std::move(callback),\n\t\t\t.confirmText = confirmText,\n\t\t}));\n}\n\nvoid PublicsController::appendRow(not_null<PeerData*> participant) {\n\tif (!delegate()->peerListFindRow(participant->id.value)) {\n\t\tdelegate()->peerListAppendRow(createRow(participant));\n\t}\n}\n\nstd::unique_ptr<PeerListRow> PublicsController::createRow(\n\t\tnot_null<PeerData*> peer) const {\n\tauto result = std::make_unique<PeerListRowWithLink>(peer);\n\tresult->setActionLink(tr::lng_channels_too_much_public_revoke(tr::now));\n\tresult->setCustomStatus(\n\t\t_navigation->session().createInternalLink(peer->username()));\n\treturn result;\n}\n\nvoid SimpleLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst style::PremiumLimits *stOverride,\n\t\tnot_null<Main::Session*> session,\n\t\tbool premiumPossible,\n\t\trpl::producer<QString> title,\n\t\trpl::producer<TextWithEntities> text,\n\t\tconst QString &refAddition,\n\t\tconst InfographicDescriptor &descriptor,\n\t\tbool fixed = false) {\n\tconst auto &st = stOverride ? *stOverride : st::defaultPremiumLimits;\n\n\tbox->setWidth(st::boxWideWidth);\n\n\tconst auto top = fixed\n\t\t? box->setPinnedToTopContent(object_ptr<Ui::VerticalLayout>(box))\n\t\t: box->verticalLayout();\n\n\tUi::AddSkip(top, st::premiumInfographicPadding.top());\n\tUi::Premium::AddBubbleRow(\n\t\ttop,\n\t\tst::defaultPremiumBubble,\n\t\tBoxShowFinishes(box),\n\t\t0,\n\t\tdescriptor.current,\n\t\t(descriptor.complexRatio\n\t\t\t? descriptor.premiumLimit\n\t\t\t: 2 * descriptor.current),\n\t\tChooseBubbleType(premiumPossible),\n\t\tdescriptor.phrase,\n\t\tdescriptor.icon);\n\tUi::AddSkip(top, st::premiumLineTextSkip);\n\tif (premiumPossible) {\n\t\tUi::Premium::AddLimitRow(\n\t\t\ttop,\n\t\t\tst,\n\t\t\tdescriptor.premiumLimit,\n\t\t\tdescriptor.phrase,\n\t\t\t0,\n\t\t\t(descriptor.complexRatio\n\t\t\t\t? (float64(descriptor.current) / descriptor.premiumLimit)\n\t\t\t\t: Ui::Premium::kLimitRowRatio));\n\t\tUi::AddSkip(top, st::premiumInfographicPadding.bottom());\n\t}\n\n\tbox->setTitle(std::move(title));\n\n\tauto padding = st::boxPadding;\n\tpadding.setTop(padding.bottom());\n\ttop->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(text),\n\t\t\tst::aboutRevokePublicLabel),\n\t\tpadding);\n\n\tif (session->premium() || !premiumPossible) {\n\t\tbox->addButton(tr::lng_box_ok(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t} else {\n\t\tbox->addButton(tr::lng_limits_increase(), [=] {\n\t\t\tSettings::ShowPremium(session, LimitsPremiumRef(refAddition));\n\t\t});\n\n\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t}\n\n\tif (fixed) {\n\t\tUi::AddSkip(top, st::settingsButton.padding.bottom());\n\t\tUi::AddDivider(top);\n\t}\n}\n\nvoid SimpleLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst style::PremiumLimits *stOverride,\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<QString> title,\n\t\trpl::producer<TextWithEntities> text,\n\t\tconst QString &refAddition,\n\t\tconst InfographicDescriptor &descriptor,\n\t\tbool fixed = false) {\n\tSimpleLimitBox(\n\t\tbox,\n\t\tstOverride,\n\t\tsession,\n\t\tsession->premiumPossible(),\n\t\tstd::move(title),\n\t\tstd::move(text),\n\t\trefAddition,\n\t\tdescriptor,\n\t\tfixed);\n}\n\n[[nodiscard]] int PinsCount(not_null<Dialogs::MainList*> list) {\n\treturn list->pinned()->order().size();\n}\n\nvoid SimplePinsLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &refAddition,\n\t\tfloat64 defaultLimit,\n\t\tfloat64 premiumLimit,\n\t\tfloat64 currentCount) {\n\tconst auto premium = session->premium();\n\tconst auto premiumPossible = session->premiumPossible();\n\n\tconst auto current = std::clamp(currentCount, defaultLimit, premiumLimit);\n\n\tauto text = rpl::combine(\n\t\ttr::lng_filter_pin_limit1(\n\t\t\tlt_count,\n\t\t\trpl::single(premium ? premiumLimit : defaultLimit),\n\t\t\ttr::rich),\n\t\t((premium || !premiumPossible)\n\t\t\t? rpl::single(TextWithEntities())\n\t\t\t: tr::lng_filter_pin_limit2(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(premiumLimit),\n\t\t\t\ttr::rich))\n\t) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {\n\t\treturn b.text.isEmpty()\n\t\t\t? a\n\t\t\t: a.append(QChar(' ')).append(std::move(b));\n\t});\n\tSimpleLimitBox(\n\t\tbox,\n\t\tnullptr,\n\t\tsession,\n\t\ttr::lng_filter_pin_limit_title(),\n\t\tstd::move(text),\n\t\trefAddition,\n\t\t{ defaultLimit, current, premiumLimit, &st::premiumIconPins });\n}\n\n} // namespace\n\nvoid ChannelsLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session) {\n\tconst auto premium = session->premium();\n\tconst auto premiumPossible = session->premiumPossible();\n\n\tconst auto limits = Data::PremiumLimits(session);\n\tconst auto defaultLimit = float64(limits.channelsDefault());\n\tconst auto premiumLimit = float64(limits.channelsPremium());\n\tconst auto current = (premium ? premiumLimit : defaultLimit);\n\n\tauto text = rpl::combine(\n\t\ttr::lng_channels_limit1(\n\t\t\tlt_count,\n\t\t\trpl::single(current),\n\t\t\ttr::rich),\n\t\t((premium || !premiumPossible)\n\t\t\t? tr::lng_channels_limit2_final(tr::rich)\n\t\t\t: tr::lng_channels_limit2(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(premiumLimit),\n\t\t\t\ttr::rich))\n\t) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {\n\t\treturn a.append(QChar(' ')).append(std::move(b));\n\t});\n\n\tSimpleLimitBox(\n\t\tbox,\n\t\tnullptr,\n\t\tsession,\n\t\ttr::lng_channels_limit_title(),\n\t\tstd::move(text),\n\t\t\"channels\",\n\t\t{ defaultLimit, current, premiumLimit, &st::premiumIconGroups },\n\t\ttrue);\n\n\tAddSubtitle(box->verticalLayout(), tr::lng_channels_leave_title());\n\n\tconst auto delegate = box->lifetime().make_state<InactiveDelegate>();\n\tconst auto controller = box->lifetime().make_state<InactiveController>(\n\t\tsession);\n\n\tconst auto content = box->addRow(\n\t\tobject_ptr<PeerListContent>(box, controller),\n\t\tstyle::margins());\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\n\tconst auto count = 100;\n\tconst auto placeholder = box->addRow(\n\t\tobject_ptr<PeerListDummy>(box, count, st::defaultPeerList),\n\t\tstyle::margins());\n\n\tusing namespace rpl::mappers;\n\tcontroller->countValue(\n\t) | rpl::filter(_1 > 0) | rpl::on_next([=] {\n\t\tdelete placeholder;\n\t}, placeholder->lifetime());\n\n\tdelegate->selectedCountChanges(\n\t) | rpl::on_next([=](int count) {\n\t\tconst auto leave = [=](const base::flat_set<PeerListRowId> &ids) {\n\t\t\tfor (const auto rowId : ids) {\n\t\t\t\tconst auto id = peerToChannel(PeerId(rowId));\n\t\t\t\tif (const auto channel = session->data().channelLoaded(id)) {\n\t\t\t\t\tsession->api().leaveChannel(channel);\n\t\t\t\t}\n\t\t\t}\n\t\t\tbox->showToast(tr::lng_channels_leave_done(tr::now));\n\t\t\tbox->closeBox();\n\t\t};\n\t\tbox->clearButtons();\n\t\tif (count) {\n\t\t\tbox->addButton(\n\t\t\t\ttr::lng_channels_leave(lt_count, rpl::single(count * 1.)),\n\t\t\t\t[=] { leave(delegate->selected()); });\n\t\t} else if (premium) {\n\t\t\tbox->addButton(tr::lng_box_ok(), [=] {\n\t\t\t\tbox->closeBox();\n\t\t\t});\n\t\t} else {\n\t\t\tbox->addButton(tr::lng_limits_increase(), [=] {\n\t\t\t\tSettings::ShowPremium(session, LimitsPremiumRef(\"channels\"));\n\t\t\t});\n\t\t}\n\t}, box->lifetime());\n}\n\nvoid PublicLinksLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tFn<void()> retry) {\n\tconst auto session = &navigation->session();\n\tconst auto premium = session->premium();\n\tconst auto premiumPossible = session->premiumPossible();\n\n\tconst auto limits = Data::PremiumLimits(session);\n\tconst auto defaultLimit = float64(limits.channelsPublicDefault());\n\tconst auto premiumLimit = float64(limits.channelsPublicPremium());\n\tconst auto current = (premium ? premiumLimit : defaultLimit);\n\n\tauto text = rpl::combine(\n\t\ttr::lng_links_limit1(\n\t\t\tlt_count,\n\t\t\trpl::single(current),\n\t\t\ttr::rich),\n\t\t((premium || !premiumPossible)\n\t\t\t? tr::lng_links_limit2_final(tr::rich)\n\t\t\t: tr::lng_links_limit2(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(premiumLimit),\n\t\t\t\ttr::rich))\n\t) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {\n\t\treturn a.append(QChar(' ')).append(std::move(b));\n\t});\n\n\tSimpleLimitBox(\n\t\tbox,\n\t\tnullptr,\n\t\tsession,\n\t\ttr::lng_links_limit_title(),\n\t\tstd::move(text),\n\t\t\"channels_public\",\n\t\t{ defaultLimit, current, premiumLimit, &st::premiumIconLinks },\n\t\ttrue);\n\n\tAddSubtitle(box->verticalLayout(), tr::lng_links_revoke_title());\n\n\tconst auto delegate = box->lifetime().make_state<InactiveDelegate>();\n\tconst auto controller = box->lifetime().make_state<PublicsController>(\n\t\tnavigation,\n\t\tcrl::guard(box, [=] { box->closeBox(); retry(); }));\n\n\tconst auto content = box->addRow(\n\t\tobject_ptr<PeerListContent>(box, controller),\n\t\tstyle::margins());\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\n\tconst auto count = defaultLimit;\n\tconst auto placeholder = box->addRow(\n\t\tobject_ptr<PeerListDummy>(box, count, st::defaultPeerList),\n\t\tstyle::margins());\n\n\tusing namespace rpl::mappers;\n\tcontroller->countValue(\n\t) | rpl::filter(_1 > 0) | rpl::on_next([=] {\n\t\tdelete placeholder;\n\t}, placeholder->lifetime());\n}\n\nvoid FilterChatsLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tint currentCount,\n\t\tbool include) {\n\tconst auto premium = session->premium();\n\tconst auto premiumPossible = session->premiumPossible();\n\n\tconst auto limits = Data::PremiumLimits(session);\n\tconst auto defaultLimit = float64(limits.dialogFiltersChatsDefault());\n\tconst auto premiumLimit = float64(limits.dialogFiltersChatsPremium());\n\tconst auto current = std::clamp(\n\t\tfloat64(currentCount),\n\t\tdefaultLimit,\n\t\tpremiumLimit);\n\n\tauto text = rpl::combine(\n\t\t(include\n\t\t\t? tr::lng_filter_chats_limit1\n\t\t\t: tr::lng_filter_chats_exlude_limit1)(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(premium ? premiumLimit : defaultLimit),\n\t\t\t\ttr::rich),\n\t\t((premium || !premiumPossible)\n\t\t\t? rpl::single(TextWithEntities())\n\t\t\t: tr::lng_filter_chats_limit2(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(premiumLimit),\n\t\t\t\ttr::rich))\n\t) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {\n\t\treturn b.text.isEmpty()\n\t\t\t? a\n\t\t\t: a.append(QChar(' ')).append(std::move(b));\n\t});\n\n\tSimpleLimitBox(\n\t\tbox,\n\t\tnullptr,\n\t\tsession,\n\t\ttr::lng_filter_chats_limit_title(),\n\t\tstd::move(text),\n\t\t\"dialog_filters_chats\",\n\t\t{ defaultLimit, current, premiumLimit, &st::premiumIconChats });\n}\n\nvoid FilterLinksLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session) {\n\tconst auto premium = session->premium();\n\tconst auto premiumPossible = session->premiumPossible();\n\n\tconst auto limits = Data::PremiumLimits(session);\n\tconst auto defaultLimit = float64(limits.dialogFiltersLinksDefault());\n\tconst auto premiumLimit = float64(limits.dialogFiltersLinksPremium());\n\tconst auto current = (premium ? premiumLimit : defaultLimit);\n\n\tauto text = rpl::combine(\n\t\ttr::lng_filter_links_limit1(\n\t\t\tlt_count,\n\t\t\trpl::single(premium ? premiumLimit : defaultLimit),\n\t\t\ttr::rich),\n\t\t((premium || !premiumPossible)\n\t\t\t? rpl::single(TextWithEntities())\n\t\t\t: tr::lng_filter_links_limit2(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(premiumLimit),\n\t\t\t\ttr::rich))\n\t) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {\n\t\treturn b.text.isEmpty()\n\t\t\t? a\n\t\t\t: a.append(QChar(' ')).append(std::move(b));\n\t});\n\n\tSimpleLimitBox(\n\t\tbox,\n\t\tnullptr,\n\t\tsession,\n\t\ttr::lng_filter_links_limit_title(),\n\t\tstd::move(text),\n\t\t\"chatlist_invites\",\n\t\t{\n\t\t\tdefaultLimit,\n\t\t\tcurrent,\n\t\t\tpremiumLimit,\n\t\t\t&st::premiumIconChats,\n\t\t\tstd::nullopt,\n\t\t\t/*true */}); // Don't use real ratio, \"Free\" doesn't fit.\n}\n\n\nvoid FiltersLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tstd::optional<int> filtersCountOverride) {\n\tconst auto premium = session->premium();\n\tconst auto premiumPossible = session->premiumPossible();\n\n\tconst auto limits = Data::PremiumLimits(session);\n\tconst auto defaultLimit = float64(limits.dialogFiltersDefault());\n\tconst auto premiumLimit = float64(limits.dialogFiltersPremium());\n\tconst auto cloud = int(ranges::count_if(\n\t\tsession->data().chatsFilters().list(),\n\t\t[](const Data::ChatFilter &f) { return f.id() != FilterId(); }));\n\tconst auto current = float64(filtersCountOverride.value_or(cloud));\n\n\tauto text = rpl::combine(\n\t\ttr::lng_filters_limit1(\n\t\t\tlt_count,\n\t\t\trpl::single(premium ? premiumLimit : defaultLimit),\n\t\t\ttr::rich),\n\t\t((premium || !premiumPossible)\n\t\t\t? rpl::single(TextWithEntities())\n\t\t\t: tr::lng_filters_limit2(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(premiumLimit),\n\t\t\t\ttr::rich))\n\t) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {\n\t\treturn b.text.isEmpty()\n\t\t\t? a\n\t\t\t: a.append(QChar(' ')).append(std::move(b));\n\t});\n\tSimpleLimitBox(\n\t\tbox,\n\t\tnullptr,\n\t\tsession,\n\t\ttr::lng_filters_limit_title(),\n\t\tstd::move(text),\n\t\t\"dialog_filters\",\n\t\t{ defaultLimit, current, premiumLimit, &st::premiumIconFolders });\n}\n\nvoid ShareableFiltersLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session) {\n\tconst auto premium = session->premium();\n\tconst auto premiumPossible = session->premiumPossible();\n\n\tconst auto limits = Data::PremiumLimits(session);\n\tconst auto defaultLimit = float64(limits.dialogShareableFiltersDefault());\n\tconst auto premiumLimit = float64(limits.dialogShareableFiltersPremium());\n\tconst auto current = float64(ranges::count_if(\n\t\tsession->data().chatsFilters().list(),\n\t\t[](const Data::ChatFilter &f) { return f.chatlist(); }));\n\n\tauto text = rpl::combine(\n\t\ttr::lng_filter_shared_limit1(\n\t\t\tlt_count,\n\t\t\trpl::single(premium ? premiumLimit : defaultLimit),\n\t\t\ttr::rich),\n\t\t((premium || !premiumPossible)\n\t\t\t? rpl::single(TextWithEntities())\n\t\t\t: tr::lng_filter_shared_limit2(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(premiumLimit),\n\t\t\t\ttr::rich))\n\t) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {\n\t\treturn b.text.isEmpty()\n\t\t\t? a\n\t\t\t: a.append(QChar(' ')).append(std::move(b));\n\t});\n\tSimpleLimitBox(\n\t\tbox,\n\t\tnullptr,\n\t\tsession,\n\t\ttr::lng_filter_shared_limit_title(),\n\t\tstd::move(text),\n\t\t\"chatlists_joined\",\n\t\t{\n\t\t\tdefaultLimit,\n\t\t\tcurrent,\n\t\t\tpremiumLimit,\n\t\t\t&st::premiumIconFolders,\n\t\t\tstd::nullopt,\n\t\t\t/*true*/ }); // Don't use real ratio, \"Free\" doesn't fit.\n}\n\nvoid FilterPinsLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tFilterId filterId) {\n\tconst auto limits = Data::PremiumLimits(session);\n\tSimplePinsLimitBox(\n\t\tbox,\n\t\tsession,\n\t\t\"dialog_filters_pinned\",\n\t\tlimits.dialogFiltersChatsDefault(),\n\t\tlimits.dialogFiltersChatsPremium(),\n\t\tPinsCount(session->data().chatsFilters().chatsList(filterId)));\n}\n\nvoid FolderPinsLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session) {\n\tconst auto limits = Data::PremiumLimits(session);\n\tSimplePinsLimitBox(\n\t\tbox,\n\t\tsession,\n\t\t\"dialogs_folder_pinned\",\n\t\tlimits.dialogsFolderPinnedDefault(),\n\t\tlimits.dialogsFolderPinnedPremium(),\n\t\tPinsCount(session->data().folder(Data::Folder::kId)->chatsList()));\n}\n\nvoid PinsLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session) {\n\tconst auto limits = Data::PremiumLimits(session);\n\tSimplePinsLimitBox(\n\t\tbox,\n\t\tsession,\n\t\t\"dialog_pinned\",\n\t\tlimits.dialogsPinnedDefault(),\n\t\tlimits.dialogsPinnedPremium(),\n\t\tPinsCount(session->data().chatsList()));\n}\n\nvoid SublistsPinsLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session) {\n\tconst auto limits = Data::PremiumLimits(session);\n\tSimplePinsLimitBox(\n\t\tbox,\n\t\tsession,\n\t\t\"saved_dialog_pinned\",\n\t\tlimits.savedSublistsPinnedDefault(),\n\t\tlimits.savedSublistsPinnedPremium(),\n\t\tPinsCount(session->data().savedMessages().chatsList()));\n}\n\nvoid ForumPinsLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Data::Forum*> forum) {\n\tconst auto current = forum->owner().pinnedChatsLimit(forum) * 1.;\n\n\tauto text = tr::lng_forum_pin_limit(\n\t\tlt_count,\n\t\trpl::single(current),\n\t\ttr::rich);\n\tSimpleLimitBox(\n\t\tbox,\n\t\tnullptr,\n\t\t&forum->session(),\n\t\tfalse,\n\t\ttr::lng_filter_pin_limit_title(),\n\t\tstd::move(text),\n\t\tQString(),\n\t\t{ current, current, current * 2, &st::premiumIconPins });\n}\n\nvoid CaptionLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tint remove,\n\t\tconst style::PremiumLimits *stOverride) {\n\tconst auto premium = session->premium();\n\tconst auto premiumPossible = session->premiumPossible();\n\n\tconst auto limits = Data::PremiumLimits(session);\n\tconst auto defaultLimit = float64(limits.captionLengthDefault());\n\tconst auto premiumLimit = float64(limits.captionLengthPremium());\n\tconst auto currentLimit = premium ? premiumLimit : defaultLimit;\n\tconst auto current = std::clamp(\n\t\tremove + currentLimit,\n\t\tdefaultLimit,\n\t\tpremiumLimit);\n\n\tauto text = rpl::combine(\n\t\ttr::lng_caption_limit1(\n\t\t\tlt_count,\n\t\t\trpl::single(currentLimit),\n\t\t\ttr::rich),\n\t\t(!premiumPossible\n\t\t\t? rpl::single(TextWithEntities())\n\t\t\t: tr::lng_caption_limit2(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(premiumLimit),\n\t\t\t\ttr::rich))\n\t) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {\n\t\treturn b.text.isEmpty()\n\t\t\t? a\n\t\t\t: a.append(QChar(' ')).append(std::move(b));\n\t});\n\n\tSimpleLimitBox(\n\t\tbox,\n\t\tstOverride,\n\t\tsession,\n\t\ttr::lng_caption_limit_title(),\n\t\tstd::move(text),\n\t\t\"caption_length\",\n\t\t{ defaultLimit, current, premiumLimit, &st::premiumIconChats });\n}\n\nvoid CaptionLimitReachedBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tint remove,\n\t\tconst style::PremiumLimits *stOverride) {\n\tUi::ConfirmBox(box, Ui::ConfirmBoxArgs{\n\t\t.text = tr::lng_caption_limit_reached(tr::now, lt_count, remove),\n\t\t.labelStyle = stOverride ? &stOverride->boxLabel : nullptr,\n\t\t.inform = true,\n\t});\n\tif (!session->premium()) {\n\t\tbox->addLeftButton(tr::lng_limits_increase(), [=] {\n\t\t\tbox->getDelegate()->showBox(\n\t\t\t\tBox(CaptionLimitBox, session, remove, stOverride),\n\t\t\t\tUi::LayerOption::KeepOther,\n\t\t\t\tanim::type::normal);\n\t\t\tbox->closeBox();\n\t\t});\n\t}\n}\n\nvoid FileSizeLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tuint64 fileSizeBytes,\n\t\tconst style::PremiumLimits *stOverride) {\n\tconst auto limits = Data::PremiumLimits(session);\n\tconst auto defaultLimit = float64(limits.uploadMaxDefault());\n\tconst auto premiumLimit = float64(limits.uploadMaxPremium());\n\n\tconst auto defaultGb = float64(int(defaultLimit + 999) / 2000);\n\tconst auto premiumGb = float64(int(premiumLimit + 999) / 2000);\n\n\tconst auto tooLarge = (fileSizeBytes > premiumLimit * 512ULL * 1024);\n\tconst auto showLimit = tooLarge ? premiumGb : defaultGb;\n\tconst auto premiumPossible = !tooLarge && session->premiumPossible();\n\n\tconst auto current = (fileSizeBytes && premiumPossible)\n\t\t? std::clamp(\n\t\t\tfloat64(((fileSizeBytes / uint64(1024 * 1024)) + 499) / 1000),\n\t\t\tdefaultGb,\n\t\t\tpremiumGb)\n\t\t: showLimit;\n\tconst auto gb = [](int count) {\n\t\treturn tr::lng_file_size_limit(tr::now, lt_count, count);\n\t};\n\n\tauto text = rpl::combine(\n\t\ttr::lng_file_size_limit1(\n\t\t\tlt_size,\n\t\t\trpl::single(tr::bold(gb(showLimit))),\n\t\t\ttr::rich),\n\t\t(!premiumPossible\n\t\t\t? rpl::single(TextWithEntities())\n\t\t\t: tr::lng_file_size_limit2(\n\t\t\t\tlt_size,\n\t\t\t\trpl::single(tr::bold(gb(premiumGb))),\n\t\t\t\ttr::rich))\n\t) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {\n\t\treturn a.append(QChar(' ')).append(std::move(b));\n\t});\n\n\tSimpleLimitBox(\n\t\tbox,\n\t\tstOverride,\n\t\tsession,\n\t\tpremiumPossible,\n\t\ttr::lng_file_size_limit_title(),\n\t\tstd::move(text),\n\t\t\"upload_max_fileparts\",\n\t\t{\n\t\t\tdefaultGb,\n\t\t\tcurrent,\n\t\t\t(tooLarge ? showLimit * 2 : premiumGb),\n\t\t\t&st::premiumIconFiles,\n\t\t\ttr::lng_file_size_limit\n\t\t});\n}\n\nvoid AccountsLimitBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session) {\n\tconst auto defaultLimit = Main::Domain::kMaxAccounts;\n\tconst auto premiumLimit = Main::Domain::kPremiumMaxAccounts;\n\n\tusing Args = Ui::Premium::AccountsRowArgs;\n\tconst auto accounts = session->domain().orderedAccounts();\n\tauto promotePossible = ranges::views::all(\n\t\taccounts\n\t) | ranges::views::filter([&](not_null<Main::Account*> account) {\n\t\treturn account->sessionExists()\n\t\t\t&& !account->session().premium()\n\t\t\t&& account->session().premiumPossible();\n\t}) | ranges::views::transform([&](not_null<Main::Account*> account) {\n\t\tconst auto user = account->session().user();\n\t\treturn Args::Entry{ user->name(), PaintUserpicCallback(user, false)};\n\t}) | ranges::views::take(defaultLimit) | ranges::to_vector;\n\n\tconst auto premiumPossible = !promotePossible.empty();\n\tconst auto current = int(accounts.size());\n\n\tauto text = rpl::combine(\n\t\ttr::lng_accounts_limit1(\n\t\t\tlt_count,\n\t\t\trpl::single<float64>(current),\n\t\t\ttr::rich),\n\t\t((!premiumPossible || current > premiumLimit)\n\t\t\t? rpl::single(TextWithEntities())\n\t\t\t: tr::lng_accounts_limit2(tr::rich))\n\t) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {\n\t\treturn b.text.isEmpty()\n\t\t\t? a\n\t\t\t: a.append(QChar(' ')).append(std::move(b));\n\t});\n\n\tbox->setWidth(st::boxWideWidth);\n\n\tconst auto top = box->verticalLayout();\n\tconst auto group = std::make_shared<Ui::RadiobuttonGroup>(0);\n\n\tUi::AddSkip(top, st::premiumInfographicPadding.top());\n\tUi::Premium::AddBubbleRow(\n\t\ttop,\n\t\tst::defaultPremiumBubble,\n\t\tBoxShowFinishes(box),\n\t\t0,\n\t\tcurrent,\n\t\t(!premiumPossible\n\t\t\t? (current * 2)\n\t\t\t: (current > defaultLimit)\n\t\t\t? (current + 1)\n\t\t\t: (defaultLimit * 2)),\n\t\tChooseBubbleType(premiumPossible),\n\t\tstd::nullopt,\n\t\t&st::premiumIconAccounts);\n\tUi::AddSkip(top, st::premiumLineTextSkip);\n\tif (premiumPossible) {\n\t\tUi::Premium::AddLimitRow(\n\t\t\ttop,\n\t\t\tst::defaultPremiumLimits,\n\t\t\t(QString::number(std::max(current, defaultLimit) + 1)\n\t\t\t\t+ ((current + 1 == premiumLimit) ? \"\" : \"+\")),\n\t\t\tQString::number(defaultLimit));\n\t\tUi::AddSkip(top, st::premiumInfographicPadding.bottom());\n\t}\n\tbox->setTitle(tr::lng_accounts_limit_title());\n\n\tauto padding = st::boxPadding;\n\tpadding.setTop(padding.bottom());\n\ttop->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(text),\n\t\t\tst::aboutRevokePublicLabel),\n\t\tpadding);\n\n\tif (!premiumPossible || current > premiumLimit) {\n\t\tbox->addButton(tr::lng_box_ok(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t\treturn;\n\t}\n\tauto switchingLifetime = std::make_shared<rpl::lifetime>();\n\tbox->addButton(tr::lng_continue(), [=]() mutable {\n\t\tconst auto ref = QString();\n\n\t\tconst auto wasAccount = &session->account();\n\t\tconst auto nowAccount = accounts[group->current()];\n\t\tif (wasAccount == nowAccount) {\n\t\t\tSettings::ShowPremium(session, ref);\n\t\t\treturn;\n\t\t}\n\n\t\tif (*switchingLifetime) {\n\t\t\treturn;\n\t\t}\n\t\t*switchingLifetime = session->domain().activeSessionChanges(\n\t\t) | rpl::on_next([=](Main::Session *session) mutable {\n\t\t\tif (session) {\n\t\t\t\tSettings::ShowPremium(session, ref);\n\t\t\t}\n\t\t\tif (switchingLifetime) {\n\t\t\t\tbase::take(switchingLifetime)->destroy();\n\t\t\t}\n\t\t});\n\t\tsession->domain().activate(nowAccount);\n\t});\n\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n\n\tauto args = Args{\n\t\t.group = group,\n\t\t.st = st::premiumAccountsCheckbox,\n\t\t.stName = st::shareBoxListItem.nameStyle,\n\t\t.stNameFg = st::shareBoxListItem.nameFg,\n\t\t.entries = std::move(promotePossible),\n\t};\n\tif (!args.entries.empty()) {\n\t\tbox->addSkip(st::premiumAccountsPadding.top());\n\t\tUi::Premium::AddAccountsRow(box->verticalLayout(), std::move(args));\n\t\tbox->addSkip(st::premiumAccountsPadding.bottom());\n\t}\n}\n\nQString LimitsPremiumRef(const QString &addition) {\n\treturn \"double_limits__\" + addition;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/premium_limits_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n\nnamespace style {\nstruct PremiumLimits;\n} // namespace style\n\nnamespace Data {\nclass Forum;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nvoid ChannelsLimitBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session);\nvoid PublicLinksLimitBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tFn<void()> retry);\nvoid FilterChatsLimitBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session,\n\tint currentCount,\n\tbool include);\nvoid FilterLinksLimitBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session);\nvoid FiltersLimitBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session,\n\tstd::optional<int> filtersCountOverride);\nvoid ShareableFiltersLimitBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session);\nvoid FilterPinsLimitBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session,\n\tFilterId filterId);\nvoid FolderPinsLimitBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session);\nvoid PinsLimitBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session);\nvoid ForumPinsLimitBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Data::Forum*> forum);\nvoid SublistsPinsLimitBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session);\nvoid CaptionLimitBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session,\n\tint remove,\n\tconst style::PremiumLimits *stOverride = nullptr);\nvoid CaptionLimitReachedBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session,\n\tint remove,\n\tconst style::PremiumLimits *stOverride = nullptr);\nvoid FileSizeLimitBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session,\n\tuint64 fileSizeBytes,\n\tconst style::PremiumLimits *stOverride = nullptr);\nvoid AccountsLimitBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session);\n\n[[nodiscard]] QString LimitsPremiumRef(const QString &addition);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/premium_preview_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/premium_preview_box.h\"\n\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_streaming.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_premium_limits.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"main/main_domain.h\" // kMaxAccounts\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/controls/feature_list.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/effects/gradient.h\"\n#include \"ui/text/text.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/gradient_round_button.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"settings/sections/settings_business.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"history/view/history_view_element.h\"\n#include \"media/streaming/media_streaming_instance.h\"\n#include \"media/streaming/media_streaming_player.h\"\n#include \"window/window_session_controller.h\"\n#include \"api/api_premium.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_credits.h\" // upgradeGiftSubtext\n#include \"styles/style_info.h\" // infoStarsUnderstood\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n\n#include <QSvgRenderer>\n\nnamespace {\n\nconstexpr auto kPremiumShift = 21. / 240;\nconstexpr auto kToggleStickerTimeout = 2 * crl::time(1000);\nconstexpr auto kStarOpacityOff = 0.1;\nconstexpr auto kStarOpacityOn = 1.;\nconstexpr auto kStarPeriod = 3 * crl::time(1000);\n\nusing Data::ReactionId;\n\nstruct Descriptor {\n\tPremiumFeature section = PremiumFeature::Stickers;\n\tDocumentData *requestedSticker = nullptr;\n\tbool fromSettings = false;\n\tFn<void()> hiddenCallback;\n\tFn<void(not_null<Ui::BoxContent*>)> shownCallback;\n\tbool hideSubscriptionButton = false;\n};\n\nbool operator==(const Descriptor &a, const Descriptor &b) {\n\treturn (a.section == b.section)\n\t\t&& (a.requestedSticker == b.requestedSticker)\n\t\t&& (a.fromSettings == b.fromSettings);\n}\n\nstruct Preload {\n\tDescriptor descriptor;\n\tstd::shared_ptr<Data::DocumentMedia> media;\n\tstd::weak_ptr<Main::SessionShow> show;\n};\n\n[[nodiscard]] std::vector<Preload> &Preloads() {\n\tstatic auto result = std::vector<Preload>();\n\treturn result;\n}\n\nvoid PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {\n\tconst auto origin = media->owner()->stickerSetOrigin();\n\tmedia->automaticLoad(origin, nullptr);\n\tmedia->videoThumbnailWanted(origin);\n}\n\n[[nodiscard]] rpl::producer<QString> SectionTitle(PremiumFeature section) {\n\tswitch (section) {\n\tcase PremiumFeature::Wallpapers:\n\t\treturn tr::lng_premium_summary_subtitle_wallpapers();\n\tcase PremiumFeature::Stories:\n\t\treturn tr::lng_premium_summary_subtitle_stories();\n\tcase PremiumFeature::DoubleLimits:\n\t\treturn tr::lng_premium_summary_subtitle_double_limits();\n\tcase PremiumFeature::MoreUpload:\n\t\treturn tr::lng_premium_summary_subtitle_more_upload();\n\tcase PremiumFeature::FasterDownload:\n\t\treturn tr::lng_premium_summary_subtitle_faster_download();\n\tcase PremiumFeature::VoiceToText:\n\t\treturn tr::lng_premium_summary_subtitle_voice_to_text();\n\tcase PremiumFeature::NoAds:\n\t\treturn tr::lng_premium_summary_subtitle_no_ads();\n\tcase PremiumFeature::EmojiStatus:\n\t\treturn tr::lng_premium_summary_subtitle_emoji_status();\n\tcase PremiumFeature::InfiniteReactions:\n\t\treturn tr::lng_premium_summary_subtitle_infinite_reactions();\n\tcase PremiumFeature::TagsForMessages:\n\t\treturn tr::lng_premium_summary_subtitle_tags_for_messages();\n\tcase PremiumFeature::LastSeen:\n\t\treturn tr::lng_premium_summary_subtitle_last_seen();\n\tcase PremiumFeature::MessagePrivacy:\n\t\treturn tr::lng_premium_summary_subtitle_message_privacy();\n\tcase PremiumFeature::Stickers:\n\t\treturn tr::lng_premium_summary_subtitle_premium_stickers();\n\tcase PremiumFeature::AnimatedEmoji:\n\t\treturn tr::lng_premium_summary_subtitle_animated_emoji();\n\tcase PremiumFeature::AdvancedChatManagement:\n\t\treturn tr::lng_premium_summary_subtitle_advanced_chat_management();\n\tcase PremiumFeature::ProfileBadge:\n\t\treturn tr::lng_premium_summary_subtitle_profile_badge();\n\tcase PremiumFeature::AnimatedUserpics:\n\t\treturn tr::lng_premium_summary_subtitle_animated_userpics();\n\tcase PremiumFeature::RealTimeTranslation:\n\t\treturn tr::lng_premium_summary_subtitle_translation();\n\tcase PremiumFeature::Business:\n\t\treturn tr::lng_premium_summary_subtitle_business();\n\tcase PremiumFeature::Effects:\n\t\treturn tr::lng_premium_summary_subtitle_effects();\n\tcase PremiumFeature::TodoLists:\n\t\treturn tr::lng_premium_summary_subtitle_todo_lists();\n\tcase PremiumFeature::PeerColors:\n\t\treturn tr::lng_premium_summary_subtitle_peer_colors();\n\tcase PremiumFeature::Gifts:\n\t\treturn tr::lng_premium_summary_subtitle_gifts();\n\tcase PremiumFeature::NoForwards:\n\t\treturn tr::lng_premium_summary_subtitle_no_forwards();\n\tcase PremiumFeature::AiCompose:\n\t\treturn tr::lng_premium_summary_subtitle_ai_compose();\n\n\tcase PremiumFeature::BusinessLocation:\n\t\treturn tr::lng_business_subtitle_location();\n\tcase PremiumFeature::BusinessHours:\n\t\treturn tr::lng_business_subtitle_opening_hours();\n\tcase PremiumFeature::QuickReplies:\n\t\treturn tr::lng_business_subtitle_quick_replies();\n\tcase PremiumFeature::GreetingMessage:\n\t\treturn tr::lng_business_subtitle_greeting_messages();\n\tcase PremiumFeature::AwayMessage:\n\t\treturn tr::lng_business_subtitle_away_messages();\n\tcase PremiumFeature::BusinessBots:\n\t\treturn tr::lng_business_subtitle_chatbots();\n\tcase PremiumFeature::ChatIntro:\n\t\treturn tr::lng_business_subtitle_chat_intro();\n\tcase PremiumFeature::ChatLinks:\n\t\treturn tr::lng_business_subtitle_chat_links();\n\tcase PremiumFeature::FilterTags:\n\t\treturn tr::lng_premium_summary_subtitle_filter_tags();\n\t}\n\tUnexpected(\"PremiumFeature in SectionTitle.\");\n}\n\n[[nodiscard]] rpl::producer<QString> SectionAbout(PremiumFeature section) {\n\tswitch (section) {\n\tcase PremiumFeature::Wallpapers:\n\t\treturn tr::lng_premium_summary_about_wallpapers();\n\tcase PremiumFeature::Stories:\n\t\treturn tr::lng_premium_summary_about_stories();\n\tcase PremiumFeature::DoubleLimits:\n\t\treturn tr::lng_premium_summary_about_double_limits();\n\tcase PremiumFeature::MoreUpload:\n\t\treturn tr::lng_premium_summary_about_more_upload();\n\tcase PremiumFeature::FasterDownload:\n\t\treturn tr::lng_premium_summary_about_faster_download();\n\tcase PremiumFeature::VoiceToText:\n\t\treturn tr::lng_premium_summary_about_voice_to_text();\n\tcase PremiumFeature::NoAds:\n\t\treturn tr::lng_premium_summary_about_no_ads();\n\tcase PremiumFeature::EmojiStatus:\n\t\treturn tr::lng_premium_summary_about_emoji_status();\n\tcase PremiumFeature::InfiniteReactions:\n\t\treturn tr::lng_premium_summary_about_infinite_reactions();\n\tcase PremiumFeature::TagsForMessages:\n\t\treturn tr::lng_premium_summary_about_tags_for_messages();\n\tcase PremiumFeature::LastSeen:\n\t\treturn tr::lng_premium_summary_about_last_seen();\n\tcase PremiumFeature::MessagePrivacy:\n\t\treturn tr::lng_premium_summary_about_message_privacy();\n\tcase PremiumFeature::Stickers:\n\t\treturn tr::lng_premium_summary_about_premium_stickers();\n\tcase PremiumFeature::AnimatedEmoji:\n\t\treturn tr::lng_premium_summary_about_animated_emoji();\n\tcase PremiumFeature::AdvancedChatManagement:\n\t\treturn tr::lng_premium_summary_about_advanced_chat_management();\n\tcase PremiumFeature::ProfileBadge:\n\t\treturn tr::lng_premium_summary_about_profile_badge();\n\tcase PremiumFeature::AnimatedUserpics:\n\t\treturn tr::lng_premium_summary_about_animated_userpics();\n\tcase PremiumFeature::RealTimeTranslation:\n\t\treturn tr::lng_premium_summary_about_translation();\n\tcase PremiumFeature::Business:\n\t\treturn tr::lng_premium_summary_about_business();\n\tcase PremiumFeature::Effects:\n\t\treturn tr::lng_premium_summary_about_effects();\n\tcase PremiumFeature::TodoLists:\n\t\treturn tr::lng_premium_summary_about_todo_lists();\n\tcase PremiumFeature::PeerColors:\n\t\treturn tr::lng_premium_summary_about_peer_colors();\n\tcase PremiumFeature::Gifts:\n\t\treturn tr::lng_premium_summary_about_gifts();\n\tcase PremiumFeature::NoForwards:\n\t\treturn tr::lng_premium_summary_about_no_forwards();\n\tcase PremiumFeature::AiCompose:\n\t\treturn tr::lng_premium_summary_about_ai_compose();\n\n\tcase PremiumFeature::BusinessLocation:\n\t\treturn tr::lng_business_about_location();\n\tcase PremiumFeature::BusinessHours:\n\t\treturn tr::lng_business_about_opening_hours();\n\tcase PremiumFeature::QuickReplies:\n\t\treturn tr::lng_business_about_quick_replies();\n\tcase PremiumFeature::GreetingMessage:\n\t\treturn tr::lng_business_about_greeting_messages();\n\tcase PremiumFeature::AwayMessage:\n\t\treturn tr::lng_business_about_away_messages();\n\tcase PremiumFeature::BusinessBots:\n\t\treturn tr::lng_business_about_chatbots();\n\tcase PremiumFeature::ChatIntro:\n\t\treturn tr::lng_business_about_chat_intro();\n\tcase PremiumFeature::ChatLinks:\n\t\treturn tr::lng_business_about_chat_links();\n\tcase PremiumFeature::FilterTags:\n\t\treturn tr::lng_premium_summary_about_filter_tags();\n\t}\n\tUnexpected(\"PremiumFeature in SectionTitle.\");\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> ChatBackPreview(\n\t\tQWidget *parent,\n\t\tint height,\n\t\tconst QImage &back) {\n\tauto result = object_ptr<Ui::FixedHeightWidget>(parent, height);\n\tconst auto raw = result.data();\n\n\traw->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\tp.drawImage(0, 0, back);\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\n[[nodiscard]] not_null<Ui::RpWidget*> StickerPreview(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst std::shared_ptr<Data::DocumentMedia> &media,\n\t\tFn<void()> readyCallback = nullptr) {\n\tusing namespace HistoryView;\n\n\tPreloadSticker(media);\n\n\tconst auto document = media->owner();\n\tconst auto lottieSize = Sticker::Size(document);\n\tconst auto effectSize = Sticker::PremiumEffectSize(document);\n\tconst auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());\n\tresult->show();\n\n\tparent->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tresult->setGeometry(QRect(\n\t\t\tQPoint(\n\t\t\t\t(size.width() - effectSize.width()) / 2,\n\t\t\t\t(size.height() - effectSize.height()) / 2),\n\t\t\teffectSize));\n\t}, result->lifetime());\n\tauto &lifetime = result->lifetime();\n\n\tstruct State {\n\t\tstd::unique_ptr<Lottie::SinglePlayer> lottie;\n\t\tstd::unique_ptr<Lottie::SinglePlayer> effect;\n\t\tstyle::owned_color pathFg = style::owned_color(\n\t\t\tQColor(255, 255, 255, 64));\n\t\tstd::unique_ptr<Ui::PathShiftGradient> pathGradient;\n\t\tbool readyInvoked = false;\n\t};\n\tconst auto state = lifetime.make_state<State>();\n\tconst auto createLottieIfReady = [=] {\n\t\tif (state->lottie) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto document = media->owner();\n\t\tconst auto sticker = document->sticker();\n\t\tif (!sticker || !sticker->isLottie() || !media->loaded()) {\n\t\t\treturn;\n\t\t} else if (media->videoThumbnailContent().isEmpty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tstate->lottie = ChatHelpers::LottiePlayerFromDocument(\n\t\t\tmedia.get(),\n\t\t\tnullptr,\n\t\t\tChatHelpers::StickerLottieSize::MessageHistory,\n\t\t\tlottieSize * factor,\n\t\t\tLottie::Quality::High);\n\t\tstate->effect = document->session().emojiStickersPack().effectPlayer(\n\t\t\tdocument,\n\t\t\tmedia->videoThumbnailContent(),\n\t\t\tQString(),\n\t\t\tStickers::EffectType::PremiumSticker);\n\n\t\tconst auto update = [=] {\n\t\t\tif (!state->readyInvoked\n\t\t\t\t&& readyCallback\n\t\t\t\t&& state->lottie->ready()\n\t\t\t\t&& state->effect->ready()) {\n\t\t\t\tstate->readyInvoked = true;\n\t\t\t\treadyCallback();\n\t\t\t}\n\t\t\tresult->update();\n\t\t};\n\t\tauto &lifetime = result->lifetime();\n\t\tstate->lottie->updates() | rpl::on_next(update, lifetime);\n\t\tstate->effect->updates() | rpl::on_next(update, lifetime);\n\t};\n\tcreateLottieIfReady();\n\tif (!state->lottie || !state->effect) {\n\t\tshow->session().downloaderTaskFinished(\n\t\t) | rpl::take_while([=] {\n\t\t\tcreateLottieIfReady();\n\t\t\treturn !state->lottie || !state->effect;\n\t\t}) | rpl::start(result->lifetime());\n\t}\n\tstate->pathGradient = std::make_unique<Ui::PathShiftGradient>(\n\t\tst::shadowFg,\n\t\tstate->pathFg.color(),\n\t\t[=] { result->update(); },\n\t\trpl::never<>());\n\n\tresult->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tcreateLottieIfReady();\n\n\t\tauto p = QPainter(result);\n\n\t\tconst auto left = effectSize.width()\n\t\t\t- int(lottieSize.width() * (1. + kPremiumShift));\n\t\tconst auto top = (effectSize.height() - lottieSize.height()) / 2;\n\t\tconst auto r = QRect(QPoint(left, top), lottieSize);\n\t\tif (!state->lottie\n\t\t\t|| !state->lottie->ready()\n\t\t\t|| !state->effect->ready()) {\n\t\t\tp.setBrush(st::shadowFg);\n\t\t\tChatHelpers::PaintStickerThumbnailPath(\n\t\t\t\tp,\n\t\t\t\tmedia.get(),\n\t\t\t\tr,\n\t\t\t\tstate->pathGradient.get());\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tconst auto frame = state->lottie->frameInfo({ lottieSize * factor });\n\t\tconst auto effect = state->effect->frameInfo(\n\t\t\t{ effectSize * factor });\n\t\t//const auto framesCount = !frame.image.isNull()\n\t\t//\t? state->lottie->framesCount()\n\t\t//\t: 1;\n\t\t//const auto effectsCount = !effect.image.isNull()\n\t\t//\t? state->effect->framesCount()\n\t\t//\t: 1;\n\n\t\tp.drawImage(r, frame.image);\n\t\tp.drawImage(\n\t\t\tQRect(QPoint(), effect.image.size() / factor),\n\t\t\teffect.image);\n\n\t\tif (!frame.image.isNull()/*\n\t\t\t&& ((frame.index % effectsCount) <= effect.index)*/) {\n\t\t\tstate->lottie->markFrameShown();\n\t\t}\n\t\tif (!effect.image.isNull()/*\n\t\t\t&& ((effect.index % framesCount) <= frame.index)*/) {\n\t\t\tstate->effect->markFrameShown();\n\t\t}\n\t}, lifetime);\n\n\treturn result;\n}\n\n[[nodiscard]] not_null<Ui::RpWidget*> StickersPreview(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tFn<void()> readyCallback) {\n\tconst auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());\n\tresult->show();\n\n\tparent->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tresult->setGeometry(QRect(QPoint(), size));\n\t}, result->lifetime());\n\tauto &lifetime = result->lifetime();\n\n\tstruct State {\n\t\tstd::vector<std::shared_ptr<Data::DocumentMedia>> medias;\n\t\tUi::RpWidget *previous = nullptr;\n\t\tUi::RpWidget *current = nullptr;\n\t\tUi::RpWidget *next = nullptr;\n\t\tUi::Animations::Simple slide;\n\t\tbase::Timer toggleTimer;\n\t\tbool toggleTimerPending = false;\n\t\tFn<void()> singleReadyCallback;\n\t\tbool readyInvoked = false;\n\t\tbool timerFired = false;\n\t\tbool nextReady = false;\n\t\tint index = 0;\n\t};\n\tconst auto premium = &show->session().api().premium();\n\tconst auto state = lifetime.make_state<State>();\n\tconst auto create = [=](std::shared_ptr<Data::DocumentMedia> media) {\n\t\tconst auto outer = Ui::CreateChild<Ui::RpWidget>(result);\n\t\touter->show();\n\n\t\tresult->sizeValue(\n\t\t) | rpl::on_next([=](QSize size) {\n\t\t\touter->resize(size);\n\t\t}, outer->lifetime());\n\n\t\t[[maybe_unused]] const auto sticker = StickerPreview(\n\t\t\touter,\n\t\t\tshow,\n\t\t\tmedia,\n\t\t\tstate->singleReadyCallback);\n\n\t\treturn outer;\n\t};\n\tconst auto createNext = [=] {\n\t\tstate->nextReady = false;\n\t\tstate->next = create(state->medias[state->index]);\n\t\tstate->next->move(0, state->current->height());\n\t};\n\tconst auto check = [=] {\n\t\tif (!state->timerFired || !state->nextReady) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto animationCallback = [=] {\n\t\t\tconst auto top = int(base::SafeRound(state->slide.value(0.)));\n\t\t\tstate->previous->move(0, top - state->current->height());\n\t\t\tstate->current->move(0, top);\n\t\t\tif (!state->slide.animating()) {\n\t\t\t\tdelete base::take(state->previous);\n\t\t\t\tstate->timerFired = false;\n\t\t\t\tstate->toggleTimer.callOnce(kToggleStickerTimeout);\n\t\t\t}\n\t\t};\n\t\tstate->timerFired = false;\n\t\t++state->index;\n\t\tstate->index %= state->medias.size();\n\t\tdelete std::exchange(state->previous, state->current);\n\t\tstate->current = state->next;\n\t\tcreateNext();\n\t\tstate->slide.stop();\n\t\tstate->slide.start(\n\t\t\tanimationCallback,\n\t\t\tstate->current->height(),\n\t\t\t0,\n\t\t\tst::premiumSlideDuration,\n\t\t\tanim::sineInOut);\n\t};\n\tstate->toggleTimer.setCallback([=] {\n\t\tstate->timerFired = true;\n\t\tcheck();\n\t});\n\tstate->singleReadyCallback = [=] {\n\t\tif (!state->readyInvoked && readyCallback) {\n\t\t\tstate->readyInvoked = true;\n\t\t\treadyCallback();\n\t\t}\n\t\tif (!state->next) {\n\t\t\tcreateNext();\n\t\t\tif (result->isHidden()) {\n\t\t\t\tstate->toggleTimerPending = true;\n\t\t\t} else {\n\t\t\t\tstate->toggleTimer.callOnce(kToggleStickerTimeout);\n\t\t\t}\n\t\t} else {\n\t\t\tstate->nextReady = true;\n\t\t\tcheck();\n\t\t}\n\t};\n\n\tresult->shownValue(\n\t) | rpl::filter([=](bool shown) {\n\t\treturn shown && state->toggleTimerPending;\n\t}) | rpl::on_next([=] {\n\t\tstate->toggleTimerPending = false;\n\t\tstate->toggleTimer.callOnce(kToggleStickerTimeout);\n\t}, result->lifetime());\n\n\tconst auto fill = [=] {\n\t\tconst auto &list = premium->stickers();\n\t\tfor (const auto &document : list) {\n\t\t\tstate->medias.push_back(document->createMediaView());\n\t\t}\n\t\tif (!state->medias.empty()) {\n\t\t\tstate->current = create(state->medias.front());\n\t\t\tstate->index = 1 % state->medias.size();\n\t\t\tstate->current->move(0, 0);\n\t\t}\n\t};\n\n\tfill();\n\tif (state->medias.empty()) {\n\t\tpremium->stickersUpdated(\n\t\t) | rpl::take(1) | rpl::on_next(fill, lifetime);\n\t}\n\n\treturn result;\n}\n\nstruct VideoPreviewDocument {\n\tDocumentData *document = nullptr;\n\tRectPart align = RectPart::Bottom;\n};\n\n[[nodiscard]] bool VideoAlignToTop(PremiumFeature section) {\n\treturn (section == PremiumFeature::MoreUpload)\n\t\t|| (section == PremiumFeature::NoAds)\n\t\t|| (section == PremiumFeature::AnimatedEmoji);\n}\n\n[[nodiscard]] DocumentData *LookupVideo(\n\t\tnot_null<Main::Session*> session,\n\t\tPremiumFeature section) {\n\tconst auto name = [&] {\n\t\tswitch (section) {\n\t\tcase PremiumFeature::MoreUpload: return \"more_upload\";\n\t\tcase PremiumFeature::FasterDownload: return \"faster_download\";\n\t\tcase PremiumFeature::VoiceToText: return \"voice_to_text\";\n\t\tcase PremiumFeature::NoAds: return \"no_ads\";\n\t\tcase PremiumFeature::AnimatedEmoji: return \"animated_emoji\";\n\t\tcase PremiumFeature::AdvancedChatManagement:\n\t\t\treturn \"advanced_chat_management\";\n\t\tcase PremiumFeature::EmojiStatus: return \"emoji_status\";\n\t\tcase PremiumFeature::InfiniteReactions: return \"infinite_reactions\";\n\t\tcase PremiumFeature::TagsForMessages: return \"saved_tags\";\n\t\tcase PremiumFeature::ProfileBadge: return \"profile_badge\";\n\t\tcase PremiumFeature::AnimatedUserpics: return \"animated_userpics\";\n\t\tcase PremiumFeature::RealTimeTranslation: return \"translations\";\n\t\tcase PremiumFeature::Wallpapers: return \"wallpapers\";\n\t\tcase PremiumFeature::LastSeen: return \"last_seen\";\n\t\tcase PremiumFeature::MessagePrivacy: return \"message_privacy\";\n\t\tcase PremiumFeature::Effects: return \"effects\";\n\t\tcase PremiumFeature::TodoLists: return \"todo\";\n\t\tcase PremiumFeature::PeerColors: return \"peer_colors\";\n\t\tcase PremiumFeature::Gifts: return \"gifts\";\n\t\tcase PremiumFeature::NoForwards: return \"no_forwards\";\n\t\tcase PremiumFeature::AiCompose: return \"ai_compose\";\n\n\t\tcase PremiumFeature::BusinessLocation: return \"business_location\";\n\t\tcase PremiumFeature::BusinessHours: return \"business_hours\";\n\t\tcase PremiumFeature::QuickReplies: return \"quick_replies\";\n\t\tcase PremiumFeature::GreetingMessage: return \"greeting_message\";\n\t\tcase PremiumFeature::AwayMessage: return \"away_message\";\n\t\tcase PremiumFeature::BusinessBots: return \"business_bots\";\n\t\tcase PremiumFeature::ChatIntro: return \"business_intro\";\n\t\tcase PremiumFeature::ChatLinks: return \"business_links\";\n\t\tcase PremiumFeature::FilterTags: return \"folder_tags\";\n\t\t}\n\t\treturn \"\";\n\t}();\n\tconst auto &videos = session->api().premium().videos();\n\tconst auto i = videos.find(name);\n\treturn (i != end(videos)) ? i->second.get() : nullptr;\n}\n\n[[nodiscard]] QPainterPath GenerateFrame(\n\t\tint left,\n\t\tint top,\n\t\tint width,\n\t\tint height,\n\t\tbool alignToBottom) {\n\tconst auto radius = style::ConvertScaleExact(20.);\n\tconst auto thickness = style::ConvertScaleExact(6.);\n\tconst auto skip = thickness / 2.;\n\tauto path = QPainterPath();\n\tif (alignToBottom) {\n\t\tpath.moveTo(left - skip, top + height);\n\t\tpath.lineTo(left - skip, top - skip + radius);\n\t\tpath.arcTo(\n\t\t\tleft - skip,\n\t\t\ttop - skip,\n\t\t\tradius * 2,\n\t\t\tradius * 2,\n\t\t\t180,\n\t\t\t-90);\n\t\tpath.lineTo(left + width + skip - radius, top - skip);\n\t\tpath.arcTo(\n\t\t\tleft + width + skip - 2 * radius,\n\t\t\ttop - skip,\n\t\t\tradius * 2,\n\t\t\tradius * 2,\n\t\t\t90,\n\t\t\t-90);\n\t\tpath.lineTo(left + width + skip, top + height);\n\t} else {\n\t\tpath.moveTo(left - skip, top);\n\t\tpath.lineTo(left - skip, top + height + skip - radius);\n\t\tpath.arcTo(\n\t\t\tleft - skip,\n\t\t\ttop + height + skip - 2 * radius,\n\t\t\tradius * 2,\n\t\t\tradius * 2,\n\t\t\t180,\n\t\t\t90);\n\t\tpath.lineTo(left + width + skip - radius, top + height + skip);\n\t\tpath.arcTo(\n\t\t\tleft + width + skip - 2 * radius,\n\t\t\ttop + height + skip - 2 * radius,\n\t\t\tradius * 2,\n\t\t\tradius * 2,\n\t\t\t270,\n\t\t\t90);\n\t\tpath.lineTo(left + width + skip, top);\n\t}\n\treturn path;\n}\n\n[[nodiscard]] not_null<Ui::RpWidget*> VideoPreview(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document,\n\t\tbool alignToBottom,\n\t\tFn<void()> readyCallback) {\n\tconst auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());\n\tresult->show();\n\n\tparent->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tresult->setGeometry(parent->rect());\n\t}, result->lifetime());\n\tauto &lifetime = result->lifetime();\n\n\tauto shared = document->owner().streaming().sharedDocument(\n\t\tdocument,\n\t\tData::FileOriginPremiumPreviews());\n\tif (!shared) {\n\t\treturn result;\n\t}\n\n\tstruct State {\n\t\tState(\n\t\t\tstd::shared_ptr<Media::Streaming::Document> shared,\n\t\t\tFn<void()> waitingCallback)\n\t\t: instance(shared, std::move(waitingCallback))\n\t\t, star(u\":/gui/icons/settings/star.svg\"_q) {\n\t\t}\n\t\tQImage blurred;\n\t\tMedia::Streaming::Instance instance;\n\t\tstd::shared_ptr<Data::DocumentMedia> media;\n\t\tUi::Animations::Basic loading;\n\t\tQPainterPath frame;\n\t\tQSvgRenderer star;\n\t\tbool readyInvoked = false;\n\t};\n\tconst auto state = lifetime.make_state<State>(std::move(shared), [] {});\n\tstate->media = document->createMediaView();\n\tif (const auto image = state->media->thumbnailInline()) {\n\t\tif (image->width() > 0) {\n\t\t\tconst auto width = st::premiumVideoWidth;\n\t\t\tconst auto height = std::max(\n\t\t\t\tint(base::SafeRound(\n\t\t\t\t\tfloat64(width) * image->height() / image->width())),\n\t\t\t\t1);\n\t\t\tusing Option = Images::Option;\n\t\t\tconst auto corners = alignToBottom\n\t\t\t\t? (Option::RoundSkipBottomLeft\n\t\t\t\t\t| Option::RoundSkipBottomRight)\n\t\t\t\t: (Option::RoundSkipTopLeft\n\t\t\t\t\t| Option::RoundSkipTopRight);\n\t\t\tstate->blurred = Images::Prepare(\n\t\t\t\timage->original(),\n\t\t\t\tQSize(width, height) * style::DevicePixelRatio(),\n\t\t\t\t{ .options = (Option::Blur | Option::RoundLarge | corners) });\n\t\t}\n\t}\n\tconst auto width = st::premiumVideoWidth;\n\tconst auto height = state->blurred.height()\n\t\t? (state->blurred.height() / state->blurred.devicePixelRatio())\n\t\t: width;\n\tconst auto left = (st::boxWideWidth - width) / 2;\n\tconst auto top = alignToBottom ? (st::premiumPreviewHeight - height) : 0;\n\tstate->frame = GenerateFrame(left, top, width, height, alignToBottom);\n\tconst auto check = [=] {\n\t\tif (state->instance.playerLocked()) {\n\t\t\treturn;\n\t\t} else if (state->instance.paused()) {\n\t\t\tstate->instance.resume();\n\t\t}\n\t\tif (!state->instance.active() && !state->instance.failed()) {\n\t\t\tauto options = Media::Streaming::PlaybackOptions();\n\t\t\toptions.waitForMarkAsShown = true;\n\t\t\toptions.mode = ::Media::Streaming::Mode::Video;\n\t\t\toptions.loop = true;\n\t\t\tstate->instance.play(options);\n\t\t}\n\t};\n\tstate->instance.player().updates(\n\t) | rpl::on_next_error([=](Media::Streaming::Update &&update) {\n\t\tif (v::is<Media::Streaming::Information>(update.data)\n\t\t\t|| v::is<Media::Streaming::UpdateVideo>(update.data)) {\n\t\t\tif (!state->readyInvoked && readyCallback) {\n\t\t\t\tstate->readyInvoked = true;\n\t\t\t\treadyCallback();\n\t\t\t}\n\t\t\tresult->update();\n\t\t}\n\t}, [=](::Media::Streaming::Error &&error) {\n\t\tresult->update();\n\t}, state->instance.lifetime());\n\n\tstate->loading.init([=] {\n\t\tif (!anim::Disabled()) {\n\t\t\tresult->update();\n\t\t}\n\t});\n\n\tresult->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(result);\n\t\tconst auto paintFrame = [&](QColor color, float64 thickness) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tauto pen = QPen(color);\n\t\t\tpen.setWidthF(style::ConvertScaleExact(thickness));\n\t\t\tp.setPen(pen);\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\tp.drawPath(state->frame);\n\t\t};\n\n\t\tcheck();\n\t\tconst auto ready = state->instance.player().ready()\n\t\t\t&& !state->instance.player().videoSize().isEmpty();\n\t\tconst auto size = QSize(width, height) * style::DevicePixelRatio();\n\n\t\tusing namespace Images;\n\t\tauto rounding = CornersMaskRef(\n\t\t\tImages::CornersMask(ImageRoundRadius::Large));\n\t\tif (alignToBottom) {\n\t\t\trounding.p[kBottomLeft] = rounding.p[kBottomRight] = nullptr;\n\t\t} else {\n\t\t\trounding.p[kTopLeft] = rounding.p[kTopRight] = nullptr;\n\t\t}\n\t\tconst auto frame = !ready\n\t\t\t? state->blurred\n\t\t\t: state->instance.frame({\n\t\t\t\t.resize = size,\n\t\t\t\t.outer = size,\n\t\t\t\t.rounding = rounding,\n\t\t\t});\n\t\tpaintFrame(QColor(0, 0, 0, 128), 12.);\n\t\tp.drawImage(QRect(left, top, width, height), frame);\n\t\tpaintFrame(Qt::black, 6.6);\n\t\tif (ready) {\n\t\t\tstate->loading.stop();\n\t\t\tstate->instance.markFrameShown();\n\t\t} else {\n\t\t\tif (!state->loading.animating()) {\n\t\t\t\tstate->loading.start();\n\t\t\t}\n\t\t\tconst auto progress = anim::Disabled()\n\t\t\t\t? 1.\n\t\t\t\t: ((crl::now() % kStarPeriod) / float64(kStarPeriod));\n\t\t\tconst auto ratio = anim::Disabled()\n\t\t\t\t? 1.\n\t\t\t\t: (1. + cos(progress * 2 * M_PI)) / 2.;\n\t\t\tconst auto opacity = kStarOpacityOff\n\t\t\t\t+ (kStarOpacityOn - kStarOpacityOff) * ratio;\n\t\t\tp.setOpacity(opacity);\n\n\t\t\tconst auto starSize = st::premiumVideoStarSize;\n\t\t\tstate->star.render(&p, QRectF(\n\t\t\t\tQPointF(\n\t\t\t\t\tleft + (width - starSize.width()) / 2.,\n\t\t\t\t\ttop + (height - starSize.height()) / 2.),\n\t\t\t\tstarSize));\n\t\t}\n\t}, lifetime);\n\n\treturn result;\n}\n\n[[nodiscard]] not_null<Ui::RpWidget*> GenericPreview(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tPremiumFeature section,\n\t\tFn<void()> readyCallback) {\n\tconst auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());\n\tresult->show();\n\n\tparent->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tresult->setGeometry(QRect(QPoint(), size));\n\t}, result->lifetime());\n\tauto &lifetime = result->lifetime();\n\n\tstruct State {\n\t\tstd::vector<std::shared_ptr<Data::DocumentMedia>> medias;\n\t\tUi::RpWidget *single = nullptr;\n\t};\n\tconst auto session = &show->session();\n\tconst auto state = lifetime.make_state<State>();\n\tconst auto create = [=] {\n\t\tconst auto document = LookupVideo(session, section);\n\t\tif (!document) {\n\t\t\treturn;\n\t\t}\n\t\tstate->single = VideoPreview(\n\t\t\tresult,\n\t\t\tshow,\n\t\t\tdocument,\n\t\t\t!VideoAlignToTop(section),\n\t\t\treadyCallback);\n\t};\n\tcreate();\n\tif (!state->single) {\n\t\tsession->api().premium().videosUpdated(\n\t\t) | rpl::take(1) | rpl::on_next(create, lifetime);\n\t}\n\n\treturn result;\n}\n\n[[nodiscard]] not_null<Ui::RpWidget*> GenerateDefaultPreview(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tPremiumFeature section,\n\t\tFn<void()> readyCallback) {\n\tswitch (section) {\n\tcase PremiumFeature::Stickers:\n\t\treturn StickersPreview(parent, std::move(show), readyCallback);\n\tdefault:\n\t\treturn GenericPreview(\n\t\t\tparent,\n\t\t\tstd::move(show),\n\t\t\tsection,\n\t\t\treadyCallback);\n\t}\n}\n\n[[nodiscard]] object_ptr<Ui::GradientButton> CreateGradientButton(\n\t\tQWidget *parent,\n\t\tQGradientStops stops) {\n\treturn object_ptr<Ui::GradientButton>(parent, std::move(stops));\n}\n\n[[nodiscard]] object_ptr<Ui::GradientButton> CreatePremiumButton(\n\t\tQWidget *parent) {\n\treturn CreateGradientButton(parent, Ui::Premium::ButtonGradientStops());\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> CreateSwitch(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<rpl::variable<PremiumFeature>*> selected,\n\t\tstd::vector<PremiumFeature> order) {\n\tconst auto padding = st::premiumDotPadding;\n\tconst auto width = padding.left() + st::premiumDot + padding.right();\n\tconst auto height = padding.top() + st::premiumDot + padding.bottom();\n\tconst auto stops = Ui::Premium::ButtonGradientStops();\n\tauto result = object_ptr<Ui::FixedHeightWidget>(parent.get(), height);\n\tconst auto raw = result.data();\n\tconst auto count = order.size();\n\tfor (auto i = 0; i != count; ++i) {\n\t\tconst auto section = order[i];\n\t\tconst auto button = Ui::CreateChild<Ui::AbstractButton>(raw);\n\t\tparent->widthValue(\n\t\t) | rpl::on_next([=](int outer) {\n\t\t\tconst auto full = width * count;\n\t\t\tconst auto left = (outer - full) / 2 + (i * width);\n\t\t\tbutton->setGeometry(left, 0, width, height);\n\t\t}, button->lifetime());\n\t\tbutton->setClickedCallback([=] {\n\t\t\t*selected = section;\n\t\t});\n\t\tbutton->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = QPainter(button);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setBrush((selected->current() == section)\n\t\t\t\t? anim::gradient_color_at(\n\t\t\t\t\tstops,\n\t\t\t\t\tfloat64(i) / (count - 1))\n\t\t\t\t: st::windowBgRipple->c);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.drawEllipse(\n\t\t\t\tbutton->rect().marginsRemoved(st::premiumDotPadding));\n\t\t}, button->lifetime());\n\t\tselected->changes(\n\t\t) | rpl::on_next([=] {\n\t\t\tbutton->update();\n\t\t}, button->lifetime());\n\t}\n\treturn result;\n}\n\nvoid AddGiftsInfoRows(not_null<Ui::VerticalLayout*> container) {\n\tconst auto features = std::vector<Ui::FeatureListEntry>{\n\t\t{\n\t\t\tst::menuIconUnique,\n\t\t\ttr::lng_gift_upgrade_unique_title(tr::now),\n\t\t\ttr::lng_gift_upgrade_unique_about(tr::now, tr::marked),\n\t\t},\n\t\t{\n\t\t\tst::menuIconTradable,\n\t\t\ttr::lng_gift_upgrade_tradable_title(tr::now),\n\t\t\ttr::lng_gift_upgrade_tradable_about(tr::now, tr::marked),\n\t\t},\n\t\t{\n\t\t\tst::menuIconNftWear,\n\t\t\ttr::lng_gift_upgrade_wearable_title(tr::now),\n\t\t\ttr::lng_gift_upgrade_wearable_about(tr::now, tr::marked),\n\t\t},\n\t};\n\tfor (const auto &feature : features) {\n\t\tcontainer->add(\n\t\t\tUi::MakeFeatureListEntry(container, feature),\n\t\t\tst::boxRowPadding);\n\t}\n}\n\nvoid PreviewBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Descriptor &descriptor,\n\t\tconst std::shared_ptr<Data::DocumentMedia> &media,\n\t\tconst QImage &back) {\n\tconst auto single = st::boxWideWidth;\n\tconst auto size = QSize(single, st::premiumPreviewHeight);\n\tbox->setWidth(size.width());\n\tbox->setNoContentMargin(true);\n\n\tconst auto outer = box->addRow(\n\t\tChatBackPreview(box, size.height(), back),\n\t\tstyle::margins());\n\n\tstruct Hiding {\n\t\tnot_null<Ui::RpWidget*> widget;\n\t\tint leftFrom = 0;\n\t\tint leftTill = 0;\n\t};\n\tstruct State {\n\t\tint leftFrom = 0;\n\t\tUi::RpWidget *content = nullptr;\n\t\tUi::RpWidget *stickersPreload = nullptr;\n\t\tbool stickersPreloadReady = false;\n\t\tbool preloadScheduled = false;\n\t\tbool showFinished = false;\n\t\tUi::Animations::Simple animation;\n\t\tFn<void()> preload;\n\t\tstd::vector<Hiding> hiding;\n\t\trpl::variable<PremiumFeature> selected;\n\t\tstd::vector<PremiumFeature> order;\n\t};\n\tconst auto state = outer->lifetime().make_state<State>();\n\tstate->selected = descriptor.section;\n\tauto premiumOrder = Settings::PremiumFeaturesOrder(&show->session());\n\tauto businessOrder = Settings::BusinessFeaturesOrder(&show->session());\n\tstate->order = ranges::contains(businessOrder, descriptor.section)\n\t\t? std::move(businessOrder)\n\t\t: ranges::contains(premiumOrder, descriptor.section)\n\t\t? std::move(premiumOrder)\n\t\t: std::vector{ descriptor.section };\n\n\tconst auto index = [=](PremiumFeature section) {\n\t\tconst auto it = ranges::find(state->order, section);\n\t\treturn (it == end(state->order))\n\t\t\t? 0\n\t\t\t: std::distance(begin(state->order), it);\n\t};\n\n\tconst auto move = [=](int delta) {\n\t\tconst auto count = int(state->order.size());\n\t\tconst auto now = state->selected.current();\n\t\tstate->selected = state->order[(index(now) + count + delta) % count];\n\t};\n\n\tconst auto buttonsParent = box->verticalLayout().get();\n\tconst auto close = Ui::CreateChild<Ui::IconButton>(\n\t\tbuttonsParent,\n\t\tst::settingsPremiumTopBarClose);\n\tclose->setClickedCallback([=] { box->closeBox(); });\n\n\tconst auto gifts = (state->selected.current() == PremiumFeature::Gifts);\n\n\tconst auto left = gifts ? nullptr : Ui::CreateChild<Ui::IconButton>(\n\t\tbuttonsParent,\n\t\tst::settingsPremiumMoveLeft);\n\tif (left) {\n\t\tleft->setClickedCallback([=] { move(-1); });\n\t}\n\n\tconst auto right = gifts ? nullptr : Ui::CreateChild<Ui::IconButton>(\n\t\tbuttonsParent,\n\t\tst::settingsPremiumMoveRight);\n\tif (right) {\n\t\tright->setClickedCallback([=] { move(1); });\n\t}\n\n\tbuttonsParent->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto outerHeight = st::premiumPreviewHeight;\n\t\tclose->moveToRight(0, 0, width);\n\t\tif (left) {\n\t\t\tleft->moveToLeft(0, (outerHeight - left->height()) / 2, width);\n\t\t}\n\t\tif (right) {\n\t\t\tright->moveToRight(0, (outerHeight - right->height()) / 2, width);\n\t\t}\n\t}, close->lifetime());\n\n\tstate->preload = [=] {\n\t\tif (!state->showFinished) {\n\t\t\tstate->preloadScheduled = true;\n\t\t\treturn;\n\t\t}\n\t\tconst auto now = state->selected.current();\n\t\tif (now != PremiumFeature::Stickers && !state->stickersPreload) {\n\t\t\tconst auto ready = [=] {\n\t\t\t\tif (state->stickersPreload) {\n\t\t\t\t\tstate->stickersPreloadReady = true;\n\t\t\t\t} else {\n\t\t\t\t\tstate->preload();\n\t\t\t\t}\n\t\t\t};\n\t\t\tstate->stickersPreload = GenerateDefaultPreview(\n\t\t\t\touter,\n\t\t\t\tshow,\n\t\t\t\tPremiumFeature::Stickers,\n\t\t\t\tready);\n\t\t\tstate->stickersPreload->hide();\n\t\t}\n\t};\n\n\tswitch (descriptor.section) {\n\tcase PremiumFeature::Stickers:\n\t\tstate->content = media\n\t\t\t? StickerPreview(outer, show, media, state->preload)\n\t\t\t: StickersPreview(outer, show, state->preload);\n\t\tbreak;\n\tdefault:\n\t\tstate->content = GenericPreview(\n\t\t\touter,\n\t\t\tshow,\n\t\t\tdescriptor.section,\n\t\t\tstate->preload);\n\t\tbreak;\n\t}\n\n\tstate->selected.value(\n\t) | rpl::combine_previous(\n\t) | rpl::on_next([=](PremiumFeature was, PremiumFeature now) {\n\t\tconst auto animationCallback = [=] {\n\t\t\tif (!state->animation.animating()) {\n\t\t\t\tfor (const auto &hiding : base::take(state->hiding)) {\n\t\t\t\t\tdelete hiding.widget;\n\t\t\t\t}\n\t\t\t\tstate->leftFrom = 0;\n\t\t\t\tstate->content->move(0, 0);\n\t\t\t} else {\n\t\t\t\tconst auto progress = state->animation.value(1.);\n\t\t\t\tstate->content->move(\n\t\t\t\t\tanim::interpolate(state->leftFrom, 0, progress),\n\t\t\t\t\t0);\n\t\t\t\tfor (const auto &hiding : state->hiding) {\n\t\t\t\t\thiding.widget->move(anim::interpolate(\n\t\t\t\t\t\thiding.leftFrom,\n\t\t\t\t\t\thiding.leftTill,\n\t\t\t\t\t\tprogress), 0);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tanimationCallback();\n\t\tconst auto toLeft = index(now) > index(was);\n\t\tauto start = state->content->x() + (toLeft ? single : -single);\n\t\tfor (const auto &hiding : state->hiding) {\n\t\t\tconst auto left = hiding.widget->x();\n\t\t\tif (toLeft && left + single > start) {\n\t\t\t\tstart = left + single;\n\t\t\t} else if (!toLeft && left - single < start) {\n\t\t\t\tstart = left - single;\n\t\t\t}\n\t\t}\n\t\tfor (auto &hiding : state->hiding) {\n\t\t\thiding.leftFrom = hiding.widget->x();\n\t\t\thiding.leftTill = hiding.leftFrom - start;\n\t\t}\n\t\tstate->hiding.push_back({\n\t\t\t.widget = state->content,\n\t\t\t.leftFrom = state->content->x(),\n\t\t\t.leftTill = state->content->x() - start,\n\t\t});\n\t\tstate->leftFrom = start;\n\t\tif (now == PremiumFeature::Stickers && state->stickersPreload) {\n\t\t\tstate->content = base::take(state->stickersPreload);\n\t\t\tstate->content->show();\n\t\t\tif (base::take(state->stickersPreloadReady)) {\n\t\t\t\tstate->preload();\n\t\t\t}\n\t\t} else {\n\t\t\tstate->content = GenerateDefaultPreview(\n\t\t\t\touter,\n\t\t\t\tshow,\n\t\t\t\tnow,\n\t\t\t\tstate->preload);\n\t\t}\n\t\tstate->animation.stop();\n\t\tstate->animation.start(\n\t\t\tanimationCallback,\n\t\t\t0.,\n\t\t\t1.,\n\t\t\tst::premiumSlideDuration,\n\t\t\tanim::sineInOut);\n\t}, outer->lifetime());\n\n\tauto title = state->selected.value(\n\t) | rpl::map(SectionTitle) | rpl::flatten_latest();\n\n\tauto text = state->selected.value(\n\t) | rpl::map(SectionAbout) | rpl::flatten_latest();\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(title),\n\t\t\tst::premiumPreviewAboutTitle),\n\t\tst::premiumPreviewAboutTitlePadding,\n\t\tstyle::al_top);\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(text),\n\t\t\tst::premiumPreviewAbout),\n\t\tst::premiumPreviewAboutPadding,\n\t\tstyle::al_top\n\t)->setTryMakeSimilarLines(true);\n\n\tif (gifts) {\n\t\tbox->setStyle(st::giftBox);\n\t\tAddGiftsInfoRows(box->verticalLayout());\n\t} else {\n\t\tbox->addRow(\n\t\t\tCreateSwitch(box->verticalLayout(), &state->selected, state->order),\n\t\t\tst::premiumDotsMargin);\n\t}\n\tconst auto showFinished = [=] {\n\t\tstate->showFinished = true;\n\t\tif (base::take(state->preloadScheduled)) {\n\t\t\tstate->preload();\n\t\t}\n\t};\n\tif (gifts) {\n\t\tbox->setShowFinishedCallback(showFinished);\n\t\tbox->addButton(\n\t\t\trpl::single(QString()),\n\t\t\t[=] { box->closeBox(); }\n\t\t)->setText(rpl::single(Ui::Text::IconEmoji(\n\t\t\t&st::infoStarsUnderstood\n\t\t).append(' ').append(tr::lng_auction_about_understood(tr::now))));\n\t} else if ((descriptor.fromSettings && show->session().premium())\n\t\t|| descriptor.hideSubscriptionButton) {\n\t\tbox->setShowFinishedCallback(showFinished);\n\t\tbox->addButton(tr::lng_close(), [=] { box->closeBox(); });\n\t} else {\n\t\tbox->setStyle(st::premiumPreviewBox);\n\t\tconst auto buttonPadding = st::premiumPreviewBox.buttonPadding;\n\t\tconst auto width = size.width()\n\t\t\t- buttonPadding.left()\n\t\t\t- buttonPadding.right();\n\t\tconst auto computeRef = [=] {\n\t\t\treturn Settings::LookupPremiumRef(state->selected.current());\n\t\t};\n\t\tauto unlock = state->selected.value(\n\t\t) | rpl::map([=](PremiumFeature section) {\n\t\t\treturn (section == PremiumFeature::InfiniteReactions)\n\t\t\t\t? tr::lng_premium_unlock_reactions()\n\t\t\t\t: (section == PremiumFeature::Stickers)\n\t\t\t\t? tr::lng_premium_unlock_stickers()\n\t\t\t\t: (section == PremiumFeature::AnimatedEmoji)\n\t\t\t\t? tr::lng_premium_unlock_emoji()\n\t\t\t\t: (section == PremiumFeature::EmojiStatus)\n\t\t\t\t? tr::lng_premium_unlock_status()\n\t\t\t\t: tr::lng_premium_more_about();\n\t\t}) | rpl::flatten_latest();\n\t\tauto button = descriptor.fromSettings\n\t\t\t? object_ptr<Ui::GradientButton>::fromRaw(\n\t\t\t\tSettings::CreateSubscribeButton({\n\t\t\t\t\t.parent = box,\n\t\t\t\t\t.computeRef = computeRef,\n\t\t\t\t\t.show = show,\n\t\t\t\t}))\n\t\t\t: CreateUnlockButton(box, std::move(unlock));\n\t\tbutton->resizeToWidth(width);\n\t\tif (!descriptor.fromSettings) {\n\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\tconst auto window = show->resolveWindow();\n\t\t\t\tif (!window) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tSettings::ShowPremium(\n\t\t\t\t\twindow,\n\t\t\t\t\tSettings::LookupPremiumRef(state->selected.current()));\n\t\t\t});\n\t\t}\n\t\tbox->setShowFinishedCallback([=, raw = button.data()]{\n\t\t\tshowFinished();\n\t\t\traw->startGlareAnimation();\n\t\t});\n\t\tbox->addButton(std::move(button));\n\t}\n\n\tif (descriptor.fromSettings) {\n\t\tData::AmPremiumValue(\n\t\t\t&show->session()\n\t\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\t\tbox->closeBox();\n\t\t}, box->lifetime());\n\t}\n\n\tbox->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::KeyPress) {\n\t\t\tconst auto key = static_cast<QKeyEvent*>(e.get())->key();\n\t\t\tif (key == Qt::Key_Left) {\n\t\t\t\tmove(-1);\n\t\t\t} else if (key == Qt::Key_Right) {\n\t\t\t\tmove(1);\n\t\t\t}\n\t\t}\n\t}, box->lifetime());\n\n\tif (const auto &hidden = descriptor.hiddenCallback) {\n\t\tbox->boxClosing() | rpl::on_next(hidden, box->lifetime());\n\t}\n}\n\nvoid Show(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Descriptor &descriptor,\n\t\tconst std::shared_ptr<Data::DocumentMedia> &media,\n\t\tQImage back) {\n\tauto box = Box(PreviewBox, show, descriptor, media, back);\n\tconst auto raw = box.data();\n\tshow->showBox(std::move(box));\n\tif (descriptor.shownCallback) {\n\t\tdescriptor.shownCallback(raw);\n\t}\n}\n\nvoid Show(std::shared_ptr<ChatHelpers::Show> show, QImage back) {\n\tauto &list = Preloads();\n\tfor (auto i = begin(list); i != end(list);) {\n\t\tconst auto already = i->show.lock();\n\t\tif (!already) {\n\t\t\ti = list.erase(i);\n\t\t} else if (already == show) {\n\t\t\tShow(std::move(show), i->descriptor, i->media, back);\n\t\t\ti = list.erase(i);\n\t\t\treturn;\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n}\n\nvoid DecorateListPromoBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Descriptor &descriptor) {\n\tconst auto session = &show->session();\n\n\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\tbox->closeBox();\n\t});\n\n\tif (!descriptor.hideSubscriptionButton) {\n\t\tData::AmPremiumValue(\n\t\t\tsession\n\t\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\t\tbox->closeBox();\n\t\t}, box->lifetime());\n\t}\n\n\tif (const auto &hidden = descriptor.hiddenCallback) {\n\t\tbox->boxClosing() | rpl::on_next(hidden, box->lifetime());\n\t}\n\n\tif (session->premium() || descriptor.hideSubscriptionButton) {\n\t\tbox->addButton(tr::lng_close(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t} else {\n\t\tconst auto button = Settings::CreateSubscribeButton({\n\t\t\t.parent = box,\n\t\t\t.computeRef = [] { return u\"double_limits\"_q; },\n\t\t\t.show = show,\n\t\t});\n\n\t\tbox->setShowFinishedCallback([=] {\n\t\t\tbutton->startGlareAnimation();\n\t\t});\n\n\t\tbox->setStyle(st::premiumPreviewDoubledLimitsBox);\n\t\tbox->widthValue(\n\t\t) | rpl::on_next([=](int width) {\n\t\t\tconst auto &padding\n\t\t\t\t= st::premiumPreviewDoubledLimitsBox.buttonPadding;\n\t\t\tbutton->resizeToWidth(width\n\t\t\t\t- padding.left()\n\t\t\t\t- padding.right());\n\t\t\tbutton->moveToLeft(padding.left(), padding.top());\n\t\t}, button->lifetime());\n\t\tbox->addButton(\n\t\t\tobject_ptr<Ui::AbstractButton>::fromRaw(button));\n\t}\n}\n\nvoid Show(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tDescriptor &&descriptor) {\n\tif (!show->session().premiumPossible()) {\n\t\tauto box = Box(PremiumUnavailableBox);\n\t\tconst auto raw = box.data();\n\t\tshow->showBox(std::move(box));\n\t\tif (descriptor.shownCallback) {\n\t\t\tdescriptor.shownCallback(raw);\n\t\t}\n\t\treturn;\n\t} else if (descriptor.section == PremiumFeature::DoubleLimits) {\n\t\tshow->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tDoubledLimitsPreviewBox(box, &show->session());\n\t\t\tDecorateListPromoBox(box, show, descriptor);\n\t\t}));\n\t\treturn;\n\t} else if (descriptor.section == PremiumFeature::Stories) {\n\t\tshow->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tUpgradedStoriesPreviewBox(box, &show->session());\n\t\t\tDecorateListPromoBox(box, show, descriptor);\n\t\t}));\n\t\treturn;\n\t} else if (descriptor.section == PremiumFeature::Business) {\n\t\tshow->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tTelegramBusinessPreviewBox(box, &show->session());\n\t\t\tDecorateListPromoBox(box, show, descriptor);\n\t\t}));\n\t\treturn;\n\t}\n\tauto &list = Preloads();\n\tfor (auto i = begin(list); i != end(list);) {\n\t\tconst auto already = i->show.lock();\n\t\tif (!already) {\n\t\t\ti = list.erase(i);\n\t\t} else if (already == show) {\n\t\t\tif (i->descriptor == descriptor) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ti->descriptor = descriptor;\n\t\t\ti->media = descriptor.requestedSticker\n\t\t\t\t? descriptor.requestedSticker->createMediaView()\n\t\t\t\t: nullptr;\n\t\t\tif (const auto &media = i->media) {\n\t\t\t\tPreloadSticker(media);\n\t\t\t}\n\t\t\treturn;\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\n\tconst auto weak = std::weak_ptr(show);\n\tlist.push_back({\n\t\t.descriptor = descriptor,\n\t\t.media = (descriptor.requestedSticker\n\t\t\t? descriptor.requestedSticker->createMediaView()\n\t\t\t: nullptr),\n\t\t.show = weak,\n\t});\n\tif (const auto &media = list.back().media) {\n\t\tPreloadSticker(media);\n\t}\n\n\tconst auto fill = QSize(st::boxWideWidth, st::boxWideWidth);\n\tconst auto stops = Ui::Premium::LimitGradientStops();\n\tcrl::async([=] {\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tauto cropped = QImage(\n\t\t\tfill * factor,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tcropped.setDevicePixelRatio(factor);\n\t\tauto p = QPainter(&cropped);\n\t\tauto gradient = QLinearGradient(0, fill.height(), fill.width(), 0);\n\t\tgradient.setStops(stops);\n\t\tp.fillRect(QRect(QPoint(), fill), gradient);\n\t\tp.end();\n\n\t\tconst auto result = Images::Round(\n\t\t\tstd::move(cropped),\n\t\t\tImages::CornersMask(st::boxRadius),\n\t\t\tRectPart::TopLeft | RectPart::TopRight);\n\t\tcrl::on_main([=] {\n\t\t\tif (auto strong = weak.lock()) {\n\t\t\t\tShow(std::move(strong), result);\n\t\t\t}\n\t\t});\n\t});\n}\n\n} // namespace\n\nvoid ShowStickerPreviewBox(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document) {\n\tShow(std::move(show), Descriptor{\n\t\t.section = PremiumFeature::Stickers,\n\t\t.requestedSticker = document,\n\t});\n}\n\nvoid ShowPremiumPreviewBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tPremiumFeature section,\n\t\tFn<void(not_null<Ui::BoxContent*>)> shown) {\n\tShowPremiumPreviewBox(controller->uiShow(), section, std::move(shown));\n}\n\nvoid ShowPremiumPreviewBox(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tPremiumFeature section,\n\t\tFn<void(not_null<Ui::BoxContent*>)> shown,\n\t\tbool hideSubscriptionButton) {\n\tShow(std::move(show), Descriptor{\n\t\t.section = section,\n\t\t.shownCallback = std::move(shown),\n\t\t.hideSubscriptionButton = hideSubscriptionButton,\n\t});\n}\n\nvoid ShowPremiumPreviewToBuy(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tPremiumFeature section,\n\t\tFn<void()> hiddenCallback) {\n\tShow(controller->uiShow(), Descriptor{\n\t\t.section = section,\n\t\t.fromSettings = true,\n\t\t.hiddenCallback = std::move(hiddenCallback),\n\t});\n}\n\nvoid PremiumUnavailableBox(not_null<Ui::GenericBox*> box) {\n\tUi::ConfirmBox(box, {\n\t\t.text = tr::lng_premium_unavailable(\n\t\t\ttr::now,\n\t\t\ttr::rich),\n\t\t.inform = true,\n\t});\n}\n\nvoid DoubledLimitsPreviewBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session) {\n\tbox->setTitle(tr::lng_premium_summary_subtitle_double_limits());\n\n\tconst auto limits = Data::PremiumLimits(session);\n\tauto entries = std::vector<Ui::Premium::ListEntry>();\n\t{\n\t\tconst auto premium = limits.channelsPremium();\n\t\tentries.push_back({\n\t\t\ttr::lng_premium_double_limits_subtitle_channels(),\n\t\t\ttr::lng_premium_double_limits_about_channels(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(premium)),\n\t\t\t\ttr::rich),\n\t\t\tlimits.channelsDefault(),\n\t\t\tpremium,\n\t\t});\n\t}\n\t{\n\t\tconst auto premium = limits.dialogsPinnedPremium();\n\t\tentries.push_back({\n\t\t\ttr::lng_premium_double_limits_subtitle_pins(),\n\t\t\ttr::lng_premium_double_limits_about_pins(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(premium)),\n\t\t\t\ttr::rich),\n\t\t\tlimits.dialogsPinnedDefault(),\n\t\t\tpremium,\n\t\t});\n\t}\n\t{\n\t\tconst auto premium = limits.channelsPublicPremium();\n\t\tentries.push_back({\n\t\t\ttr::lng_premium_double_limits_subtitle_links(),\n\t\t\ttr::lng_premium_double_limits_about_links(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(premium)),\n\t\t\t\ttr::rich),\n\t\t\tlimits.channelsPublicDefault(),\n\t\t\tpremium,\n\t\t});\n\t}\n\t{\n\t\tconst auto premium = limits.gifsPremium();\n\t\tentries.push_back({\n\t\t\ttr::lng_premium_double_limits_subtitle_gifs(),\n\t\t\ttr::lng_premium_double_limits_about_gifs(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(premium)),\n\t\t\t\ttr::rich),\n\t\t\tlimits.gifsDefault(),\n\t\t\tpremium,\n\t\t});\n\t}\n\t{\n\t\tconst auto premium = limits.stickersFavedPremium();\n\t\tentries.push_back({\n\t\t\ttr::lng_premium_double_limits_subtitle_stickers(),\n\t\t\ttr::lng_premium_double_limits_about_stickers(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(premium)),\n\t\t\t\ttr::rich),\n\t\t\tlimits.stickersFavedDefault(),\n\t\t\tpremium,\n\t\t});\n\t}\n\t{\n\t\tconst auto premium = limits.aboutLengthPremium();\n\t\tentries.push_back({\n\t\t\ttr::lng_premium_double_limits_subtitle_bio(),\n\t\t\ttr::lng_premium_double_limits_about_bio(\n\t\t\t\ttr::rich),\n\t\t\tlimits.aboutLengthDefault(),\n\t\t\tpremium,\n\t\t});\n\t}\n\t{\n\t\tconst auto premium = limits.captionLengthPremium();\n\t\tentries.push_back({\n\t\t\ttr::lng_premium_double_limits_subtitle_captions(),\n\t\t\ttr::lng_premium_double_limits_about_captions(\n\t\t\t\ttr::rich),\n\t\t\tlimits.captionLengthDefault(),\n\t\t\tpremium,\n\t\t});\n\t}\n\t{\n\t\tconst auto premium = limits.dialogFiltersPremium();\n\t\tentries.push_back({\n\t\t\ttr::lng_premium_double_limits_subtitle_folders(),\n\t\t\ttr::lng_premium_double_limits_about_folders(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(premium)),\n\t\t\t\ttr::rich),\n\t\t\tlimits.dialogFiltersDefault(),\n\t\t\tpremium,\n\t\t});\n\t}\n\t{\n\t\tconst auto premium = limits.dialogFiltersChatsPremium();\n\t\tentries.push_back({\n\t\t\ttr::lng_premium_double_limits_subtitle_folder_chats(),\n\t\t\ttr::lng_premium_double_limits_about_folder_chats(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(premium)),\n\t\t\t\ttr::rich),\n\t\t\tlimits.dialogFiltersChatsDefault(),\n\t\t\tpremium,\n\t\t});\n\t}\n\tconst auto nextMax = session->domain().maxAccounts() + 1;\n\tconst auto till = (nextMax >= Main::Domain::kPremiumMaxAccounts)\n\t\t? QString::number(Main::Domain::kPremiumMaxAccounts)\n\t\t: (QString::number(nextMax) + QChar('+'));\n\tentries.push_back({\n\t\ttr::lng_premium_double_limits_subtitle_accounts(),\n\t\ttr::lng_premium_double_limits_about_accounts(\n\t\t\tlt_count,\n\t\t\trpl::single(float64(Main::Domain::kPremiumMaxAccounts)),\n\t\t\ttr::rich),\n\t\tMain::Domain::kMaxAccounts,\n\t\tMain::Domain::kPremiumMaxAccounts,\n\t\ttill,\n\t});\n\t{\n\t\tconst auto premium = limits.similarChannelsPremium();\n\t\tentries.push_back({\n\t\t\ttr::lng_premium_double_limits_subtitle_similar_channels(),\n\t\t\ttr::lng_premium_double_limits_about_similar_channels(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(premium)),\n\t\t\t\ttr::rich),\n\t\t\tlimits.similarChannelsDefault(),\n\t\t\tpremium,\n\t\t});\n\t}\n\tUi::Premium::ShowListBox(\n\t\tbox,\n\t\tst::defaultPremiumLimits,\n\t\tstd::move(entries));\n}\n\nvoid UpgradedStoriesPreviewBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session) {\n\tusing namespace Ui::Text;\n\n\tbox->setTitle(tr::lng_premium_summary_subtitle_stories());\n\n\tauto entries = std::vector<Ui::Premium::ListEntry>();\n\tentries.push_back({\n\t\t.title = tr::lng_premium_stories_subtitle_order(),\n\t\t.about = tr::lng_premium_stories_about_order(WithEntities),\n\t\t.icon = &st::settingsStoriesIconOrder,\n\t});\n\tentries.push_back({\n\t\t.title = tr::lng_premium_stories_subtitle_stealth(),\n\t\t.about = tr::lng_premium_stories_about_stealth(WithEntities),\n\t\t.icon = &st::settingsStoriesIconStealth,\n\t});\n\tentries.push_back({\n\t\t.title = tr::lng_premium_stories_subtitle_views(),\n\t\t.about = tr::lng_premium_stories_about_views(WithEntities),\n\t\t.icon = &st::settingsStoriesIconViews,\n\t});\n\tentries.push_back({\n\t\t.title = tr::lng_premium_stories_subtitle_expiration(),\n\t\t.about = tr::lng_premium_stories_about_expiration(WithEntities),\n\t\t.icon = &st::settingsStoriesIconExpiration,\n\t});\n\tentries.push_back({\n\t\t.title = tr::lng_premium_stories_subtitle_download(),\n\t\t.about = tr::lng_premium_stories_about_download(WithEntities),\n\t\t.icon = &st::settingsStoriesIconDownload,\n\t});\n\tentries.push_back({\n\t\t.title = tr::lng_premium_stories_subtitle_caption(),\n\t\t.about = tr::lng_premium_stories_about_caption(WithEntities),\n\t\t.icon = &st::settingsStoriesIconCaption,\n\t});\n\tentries.push_back({\n\t\t.title = tr::lng_premium_stories_subtitle_links(),\n\t\t.about = tr::lng_premium_stories_about_links(WithEntities),\n\t\t.icon = &st::settingsStoriesIconLinks,\n\t});\n\n\tUi::Premium::ShowListBox(\n\t\tbox,\n\t\tst::defaultPremiumLimits,\n\t\tstd::move(entries));\n\n\tUi::AddDividerText(\n\t\tbox->verticalLayout(),\n\t\ttr::lng_premium_stories_about_mobile());\n}\n\nvoid TelegramBusinessPreviewBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session) {\n\tusing namespace Ui::Text;\n\n\tbox->setTitle(tr::lng_business_title());\n\n\tauto entries = std::vector<Ui::Premium::ListEntry>();\n\tconst auto push = [&](\n\t\t\ttr::phrase<> title,\n\t\t\ttr::phrase<> description,\n\t\t\tconst style::icon &icon) {\n\t\tentries.push_back({\n\t\t\t.title = title(),\n\t\t\t.about = description(WithEntities),\n\t\t\t.icon = &icon,\n\t\t});\n\t};\n\tfor (const auto feature : Settings::BusinessFeaturesOrder(session)) {\n\t\tswitch (feature) {\n\t\tcase PremiumFeature::GreetingMessage: push(\n\t\t\ttr::lng_business_subtitle_greeting_messages,\n\t\t\ttr::lng_business_about_greeting_messages,\n\t\t\tst::settingsBusinessPromoGreeting);\n\t\t\tbreak;\n\t\tcase PremiumFeature::AwayMessage: push(\n\t\t\ttr::lng_business_subtitle_away_messages,\n\t\t\ttr::lng_business_about_away_messages,\n\t\t\tst::settingsBusinessPromoAway);\n\t\t\tbreak;\n\t\tcase PremiumFeature::QuickReplies: push(\n\t\t\ttr::lng_business_subtitle_quick_replies,\n\t\t\ttr::lng_business_about_quick_replies,\n\t\t\tst::settingsBusinessPromoReplies);\n\t\t\tbreak;\n\t\tcase PremiumFeature::BusinessHours: push(\n\t\t\ttr::lng_business_subtitle_opening_hours,\n\t\t\ttr::lng_business_about_opening_hours,\n\t\t\tst::settingsBusinessPromoHours);\n\t\t\tbreak;\n\t\tcase PremiumFeature::BusinessLocation: push(\n\t\t\ttr::lng_business_subtitle_location,\n\t\t\ttr::lng_business_about_location,\n\t\t\tst::settingsBusinessPromoLocation);\n\t\t\tbreak;\n\t\tcase PremiumFeature::BusinessBots: push(\n\t\t\ttr::lng_business_subtitle_chatbots,\n\t\t\ttr::lng_business_about_chatbots,\n\t\t\tst::settingsBusinessPromoChatbots);\n\t\t\tbreak;\n\t\tcase PremiumFeature::ChatIntro: push(\n\t\t\ttr::lng_business_subtitle_chat_intro,\n\t\t\ttr::lng_business_about_chat_intro,\n\t\t\tst::settingsBusinessPromoChatIntro);\n\t\t\tbreak;\n\t\tcase PremiumFeature::ChatLinks: push(\n\t\t\ttr::lng_business_subtitle_chat_links,\n\t\t\ttr::lng_business_about_chat_links,\n\t\t\tst::settingsBusinessPromoChatLinks);\n\t\t\tbreak;\n\t\tcase PremiumFeature::FilterTags: push(\n\t\t\ttr::lng_premium_summary_subtitle_filter_tags,\n\t\t\ttr::lng_premium_summary_about_filter_tags,\n\t\t\tst::settingsPremiumIconTags);\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tUi::Premium::ShowListBox(\n\t\tbox,\n\t\tst::defaultPremiumLimits,\n\t\tstd::move(entries));\n}\n\nobject_ptr<Ui::GradientButton> CreateUnlockButton(\n\t\tQWidget *parent,\n\t\trpl::producer<QString> text) {\n\tauto result = CreatePremiumButton(parent);\n\tconst auto &st = st::premiumPreviewBox.button;\n\tresult->resize(result->width(), st.height);\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tresult.data(),\n\t\tstd::move(text),\n\t\tst::premiumPreviewButtonLabel);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\trpl::combine(\n\t\tresult->widthValue(),\n\t\tlabel->widthValue()\n\t) | rpl::on_next([=](int outer, int width) {\n\t\tlabel->moveToLeft(\n\t\t\t(outer - width) / 2,\n\t\t\tst::premiumPreviewBox.button.textTop,\n\t\t\touter);\n\t}, label->lifetime());\n\n\treturn result;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/premium_preview_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nclass DocumentData;\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nstruct ReactionId;\n} // namespace Data\n\nnamespace Ui {\nclass BoxContent;\nclass GenericBox;\nclass GradientButton;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nvoid ShowStickerPreviewBox(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<DocumentData*> document);\n\nvoid DoubledLimitsPreviewBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session);\n\nvoid UpgradedStoriesPreviewBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session);\n\nvoid TelegramBusinessPreviewBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session);\n\nenum class PremiumFeature {\n\t// Premium features.\n\tStories,\n\tDoubleLimits,\n\tMoreUpload,\n\tFasterDownload,\n\tVoiceToText,\n\tNoAds,\n\tEmojiStatus,\n\tInfiniteReactions,\n\tStickers,\n\tAnimatedEmoji,\n\tAdvancedChatManagement,\n\tProfileBadge,\n\tAnimatedUserpics,\n\tRealTimeTranslation,\n\tWallpapers,\n\tTagsForMessages,\n\tLastSeen,\n\tMessagePrivacy,\n\tBusiness,\n\tEffects,\n\tFilterTags,\n\tTodoLists,\n\tPeerColors,\n\tGifts,\n\tNoForwards,\n\tAiCompose,\n\n\t// Business features.\n\tBusinessLocation,\n\tBusinessHours,\n\tQuickReplies,\n\tGreetingMessage,\n\tAwayMessage,\n\tBusinessBots,\n\tChatIntro,\n\tChatLinks,\n\n\tkCount,\n};\n\nvoid ShowPremiumPreviewBox(\n\tnot_null<Window::SessionController*> controller,\n\tPremiumFeature section,\n\tFn<void(not_null<Ui::BoxContent*>)> shown = nullptr);\n\nvoid ShowPremiumPreviewBox(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tPremiumFeature section,\n\tFn<void(not_null<Ui::BoxContent*>)> shown = nullptr,\n\tbool hideSubscriptionButton = false);\n\nvoid ShowPremiumPreviewToBuy(\n\tnot_null<Window::SessionController*> controller,\n\tPremiumFeature section,\n\tFn<void()> hiddenCallback = nullptr);\n\nvoid PremiumUnavailableBox(not_null<Ui::GenericBox*> box);\n\n[[nodiscard]] object_ptr<Ui::GradientButton> CreateUnlockButton(\n\tQWidget *parent,\n\trpl::producer<QString> text);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/reactions_settings_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/reactions_settings_box.h\"\n\n#include \"base/unixtime.h\"\n#include \"data/data_user.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_session.h\"\n#include \"history/admin_log/history_admin_log_item.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/reactions/history_view_reactions_strip.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_fake_items.h\"\n#include \"lang/lang_keys.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"chat_helpers/emoji_list_widget.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/effects/scroll_content_shadow.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/animated_icon.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/section_widget.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_media_player.h\" // mediaPlayerMenuCheck\n#include \"styles/style_settings.h\"\n\nnamespace {\n\nvoid AddMessage(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\trpl::producer<Data::ReactionId> &&idValue,\n\t\tint width) {\n\n\tconst auto widget = container->add(\n\t\tobject_ptr<Ui::RpWidget>(container),\n\t\tstyle::margins(\n\t\t\t0,\n\t\t\tst::defaultVerticalListSkip,\n\t\t\t0,\n\t\t\tst::settingsPrivacySkipTop));\n\n\tclass Delegate final : public HistoryView::SimpleElementDelegate {\n\tpublic:\n\t\tusing HistoryView::SimpleElementDelegate::SimpleElementDelegate;\n\tprivate:\n\t\tHistoryView::Context elementContext() override {\n\t\t\treturn HistoryView::Context::ContactPreview;\n\t\t}\n\t};\n\n\tstruct State {\n\t\tAdminLog::OwnedItem reply;\n\t\tAdminLog::OwnedItem item;\n\t\tstd::unique_ptr<Delegate> delegate;\n\t\tstd::unique_ptr<Ui::ChatStyle> style;\n\n\t\tstruct {\n\t\t\tstd::vector<rpl::lifetime> lifetimes;\n\t\t\tbool flag = false;\n\t\t} icons;\n\t};\n\tconst auto state = container->lifetime().make_state<State>();\n\tstate->delegate = std::make_unique<Delegate>(\n\t\tcontroller,\n\t\tcrl::guard(widget, [=] { widget->update(); }));\n\tstate->style = std::make_unique<Ui::ChatStyle>(\n\t\tcontroller->session().colorIndicesValue());\n\tstate->style->apply(controller->defaultChatTheme().get());\n\tstate->icons.lifetimes = std::vector<rpl::lifetime>(2);\n\n\tconst auto history = controller->session().data().history(\n\t\tPeerData::kServiceNotificationsId);\n\tstate->reply = HistoryView::GenerateItem(\n\t\tstate->delegate.get(),\n\t\thistory,\n\t\tHistoryView::GenerateUser(\n\t\t\thistory,\n\t\t\ttr::lng_settings_chat_message_reply_from(tr::now)),\n\t\tFullMsgId(),\n\t\ttr::lng_settings_chat_message_reply(tr::now));\n\tauto message = HistoryView::GenerateItem(\n\t\tstate->delegate.get(),\n\t\thistory,\n\t\thistory->peer->id,\n\t\tstate->reply->data()->fullId(),\n\t\ttr::lng_settings_chat_message(tr::now));\n\tconst auto view = message.get();\n\tstate->item = std::move(message);\n\n\tconst auto padding = st::settingsForwardPrivacyPadding;\n\n\tconst auto updateWidgetSize = [=](int width) {\n\t\tconst auto height = view->resizeGetHeight(width);\n\t\tconst auto top = view->marginTop();\n\t\tconst auto bottom = view->marginBottom();\n\t\tconst auto full = padding + top + height + bottom + padding;\n\t\twidget->resize(width, full);\n\t};\n\twidget->widthValue(\n\t) | rpl::filter(\n\t\trpl::mappers::_1 >= (st::historyMinimalWidth / 2)\n\t) | rpl::on_next(updateWidgetSize, widget->lifetime());\n\tupdateWidgetSize(width);\n\n\tconst auto rightSize = st::settingsReactionCornerSize;\n\tconst auto rightRect = [=] {\n\t\tconst auto viewInner = view->innerGeometry();\n\t\treturn QRect(\n\t\t\tviewInner.x() + viewInner.width(),\n\t\t\tpadding\n\t\t\t\t+ view->marginTop()\n\t\t\t\t+ view->resizeGetHeight(widget->width())\n\t\t\t\t- rightSize.height(),\n\t\t\trightSize.width(),\n\t\t\trightSize.height()).translated(st::settingsReactionCornerSkip);\n\t};\n\n\tusing ThemePtr = std::unique_ptr<Ui::ChatTheme>;\n\tconst auto theme = widget->lifetime().make_state<ThemePtr>(\n\t\tWindow::Theme::DefaultChatThemeOn(widget->lifetime()));\n\twidget->paintRequest(\n\t) | rpl::on_next([=](const QRect &rect) {\n\t\tPainter p(widget);\n\t\tp.setClipRect(rect);\n\t\tWindow::SectionWidget::PaintBackground(\n\t\t\tp,\n\t\t\ttheme->get(),\n\t\t\tQSize(widget->width(), widget->window()->height()),\n\t\t\trect);\n\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto theme = controller->defaultChatTheme().get();\n\t\tauto context = theme->preparePaintContext(\n\t\t\tstate->style.get(),\n\t\t\twidget->rect(),\n\t\t\twidget->rect(),\n\t\t\twidget->rect(),\n\t\t\tcontroller->isGifPausedAtLeastFor(\n\t\t\t\tWindow::GifPauseReason::Layer));\n\t\tcontext.outbg = view->hasOutLayout();\n\n\t\t{\n\t\t\tconst auto radius = rightSize.height() / 2;\n\t\t\tconst auto r = rightRect();\n\t\t\tconst auto &st = context.st->messageStyle(\n\t\t\t\tcontext.outbg,\n\t\t\t\tcontext.selected());\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(st.msgShadow);\n\t\t\tp.drawRoundedRect(r.translated(0, st::msgShadow), radius, radius);\n\t\t\tp.setBrush(st.msgBg);\n\t\t\tp.drawRoundedRect(r, radius, radius);\n\t\t}\n\n\t\tp.translate(padding / 2, padding + view->marginBottom());\n\t\tview->draw(p, context);\n\t}, widget->lifetime());\n\n\tauto selectedId = rpl::duplicate(idValue);\n\tstd::move(\n\t\tselectedId\n\t) | rpl::on_next([\n\t\t=,\n\t\tidValue = std::move(idValue),\n\t\ticonSize = st::settingsReactionMessageSize\n\t](const Data::ReactionId &id) {\n\t\tconst auto index = state->icons.flag ? 1 : 0;\n\t\tstate->icons.flag = !state->icons.flag;\n\t\tstate->icons.lifetimes[index] = rpl::lifetime();\n\t\tconst auto &reactions = controller->session().data().reactions();\n\t\tauto iconPositionValue = widget->geometryValue(\n\t\t) | rpl::map([=](const QRect &r) {\n\t\t\treturn widget->pos()\n\t\t\t\t+ rightRect().topLeft()\n\t\t\t\t+ QPoint(\n\t\t\t\t\t(rightSize.width() - iconSize) / 2,\n\t\t\t\t\t(rightSize.height() - iconSize) / 2);\n\t\t});\n\t\tauto destroys = rpl::duplicate(\n\t\t\tidValue\n\t\t) | rpl::skip(1) | rpl::to_empty;\n\t\tif (const auto customId = id.custom()) {\n\t\t\tAddReactionCustomIcon(\n\t\t\t\tcontainer,\n\t\t\t\tstd::move(iconPositionValue),\n\t\t\t\ticonSize,\n\t\t\t\tcontroller,\n\t\t\t\tcustomId,\n\t\t\t\tstd::move(destroys),\n\t\t\t\t&state->icons.lifetimes[index]);\n\t\t\treturn;\n\t\t}\n\t\tfor (const auto &r : reactions.list(Data::Reactions::Type::Active)) {\n\t\t\tif (r.id != id) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tAddReactionAnimatedIcon(\n\t\t\t\tcontainer,\n\t\t\t\tstd::move(iconPositionValue),\n\t\t\t\ticonSize,\n\t\t\t\tr,\n\t\t\t\trpl::never<>(),\n\t\t\t\tstd::move(destroys),\n\t\t\t\t&state->icons.lifetimes[index]);\n\t\t\treturn;\n\t\t}\n\t}, widget->lifetime());\n}\n\nnot_null<Ui::RpWidget*> AddReactionIconWrap(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<QPoint> iconPositionValue,\n\t\tint iconSize,\n\t\tFn<void(not_null<QWidget*>, QPainter&)> paintCallback,\n\t\trpl::producer<> &&destroys,\n\t\tnot_null<rpl::lifetime*> stateLifetime) {\n\tstruct State {\n\t\tbase::unique_qptr<Ui::RpWidget> widget;\n\t\tUi::Animations::Simple finalAnimation;\n\t};\n\n\tconst auto state = stateLifetime->make_state<State>();\n\tstate->widget = base::make_unique_q<Ui::RpWidget>(parent);\n\n\tconst auto widget = state->widget.get();\n\twidget->resize(iconSize, iconSize);\n\twidget->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tstd::move(\n\t\ticonPositionValue\n\t) | rpl::on_next([=](const QPoint &point) {\n\t\twidget->moveToLeft(point.x(), point.y());\n\t}, widget->lifetime());\n\n\tconst auto update = crl::guard(widget, [=] { widget->update(); });\n\n\twidget->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(widget);\n\n\t\tif (state->finalAnimation.animating()) {\n\t\t\tconst auto progress = 1. - state->finalAnimation.value(0.);\n\t\t\tconst auto size = widget->size();\n\t\t\tconst auto scaledSize = size * progress;\n\t\t\tconst auto scaledCenter = QPoint(\n\t\t\t\t(size.width() - scaledSize.width()) / 2.,\n\t\t\t\t(size.height() - scaledSize.height()) / 2.);\n\t\t\tp.setOpacity(progress);\n\t\t\tp.translate(scaledCenter);\n\t\t\tp.scale(progress, progress);\n\t\t}\n\n\t\tpaintCallback(widget, p);\n\t}, widget->lifetime());\n\n\tstd::move(\n\t\tdestroys\n\t) | rpl::take(1) | rpl::on_next([=, from = 0., to = 1.] {\n\t\tstate->finalAnimation.start(\n\t\t\t[=](float64 value) {\n\t\t\t\tupdate();\n\t\t\t\tif (value == to) {\n\t\t\t\t\tstateLifetime->destroy();\n\t\t\t\t}\n\t\t\t},\n\t\t\tfrom,\n\t\t\tto,\n\t\t\tst::defaultPopupMenu.showDuration);\n\t}, widget->lifetime());\n\n\twidget->raise();\n\twidget->show();\n\n\treturn widget;\n}\n\n} // namespace\n\nvoid AddReactionAnimatedIcon(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<QPoint> iconPositionValue,\n\t\tint iconSize,\n\t\tconst Data::Reaction &reaction,\n\t\trpl::producer<> &&selects,\n\t\trpl::producer<> &&destroys,\n\t\tnot_null<rpl::lifetime*> stateLifetime) {\n\tstruct State {\n\t\tstruct Entry {\n\t\t\tstd::shared_ptr<Data::DocumentMedia> media;\n\t\t\tstd::shared_ptr<Ui::AnimatedIcon> icon;\n\t\t};\n\t\tEntry appear;\n\t\tEntry select;\n\t\tbool appearAnimated = false;\n\t\trpl::lifetime loadingLifetime;\n\t};\n\tconst auto state = stateLifetime->make_state<State>();\n\n\tstate->appear.media = reaction.appearAnimation->createMediaView();\n\tstate->select.media = reaction.selectAnimation->createMediaView();\n\tstate->appear.media->checkStickerLarge();\n\tstate->select.media->checkStickerLarge();\n\trpl::single() | rpl::then(\n\t\treaction.appearAnimation->session().downloaderTaskFinished()\n\t) | rpl::on_next([=] {\n\t\tconst auto check = [&](State::Entry &entry) {\n\t\t\tif (!entry.media) {\n\t\t\t\treturn true;\n\t\t\t} else if (!entry.media->loaded()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tentry.icon = HistoryView::Reactions::DefaultIconFactory(\n\t\t\t\tentry.media.get(),\n\t\t\t\ticonSize);\n\t\t\tentry.media = nullptr;\n\t\t\treturn true;\n\t\t};\n\t\tif (check(state->select) && check(state->appear)) {\n\t\t\tstate->select.icon->setCustomEndFrame(1);\n\t\t\tstate->select.icon->animate([] {});\n\t\t\tstate->loadingLifetime.destroy();\n\t\t}\n\t}, state->loadingLifetime);\n\n\tconst auto paintCallback = [=](not_null<QWidget*> widget, QPainter &p) {\n\t\tconst auto paintFrame = [&](not_null<Ui::AnimatedIcon*> animation) {\n\t\t\tconst auto frame = animation->frame(st::windowFg->c);\n\t\t\tp.drawImage(\n\t\t\t\tQRect(\n\t\t\t\t\t(widget->width() - iconSize) / 2,\n\t\t\t\t\t(widget->height() - iconSize) / 2,\n\t\t\t\t\ticonSize,\n\t\t\t\t\ticonSize),\n\t\t\t\tframe);\n\t\t};\n\n\t\tconst auto appear = state->appear.icon.get();\n\t\tif (appear && !state->appearAnimated) {\n\t\t\tstate->appearAnimated = true;\n\t\t\tappear->animate(crl::guard(widget, [=] { widget->update(); }));\n\t\t}\n\t\tif (appear && appear->animating()) {\n\t\t\tpaintFrame(appear);\n\t\t\tif (appear->frameIndex() == appear->framesCount() - 1) {\n\t\t\t\tif (const auto select = state->select.icon.get()) {\n\t\t\t\t\tselect->setCustomEndFrame(select->framesCount() - 1);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (const auto select = state->select.icon.get()) {\n\t\t\tpaintFrame(select);\n\t\t}\n\t};\n\tconst auto widget = AddReactionIconWrap(\n\t\tparent,\n\t\tstd::move(iconPositionValue),\n\t\ticonSize,\n\t\tpaintCallback,\n\t\tstd::move(destroys),\n\t\tstateLifetime);\n\n\tstd::move(\n\t\tselects\n\t) | rpl::on_next([=] {\n\t\tconst auto select = state->select.icon.get();\n\t\tif (select && !select->animating()) {\n\t\t\tselect->animate(crl::guard(widget, [=] { widget->update(); }));\n\t\t}\n\t}, widget->lifetime());\n}\n\nvoid AddReactionCustomIcon(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<QPoint> iconPositionValue,\n\t\tint iconSize,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tDocumentId customId,\n\t\trpl::producer<> &&destroys,\n\t\tnot_null<rpl::lifetime*> stateLifetime) {\n\tstruct State {\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji> custom;\n\t\tFn<void()> repaint;\n\t};\n\tconst auto state = stateLifetime->make_state<State>();\n\tstatic constexpr auto tag = Data::CustomEmojiManager::SizeTag::Normal;\n\tstate->custom = controller->session().data().customEmojiManager().create(\n\t\tcustomId,\n\t\t[=] { state->repaint(); },\n\t\ttag);\n\n\tconst auto paintCallback = [=](not_null<QWidget*> widget, QPainter &p) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto size = Data::FrameSizeFromTag(tag) / ratio;\n\t\tstate->custom->paint(p, {\n\t\t\t.textColor = st::windowFg->c,\n\t\t\t.now = crl::now(),\n\t\t\t.position = QPoint(\n\t\t\t\t(widget->width() - size) / 2,\n\t\t\t\t(widget->height() - size) / 2),\n\t\t\t.paused = controller->isGifPausedAtLeastFor(\n\t\t\t\tWindow::GifPauseReason::Layer),\n\t\t});\n\t};\n\tconst auto widget = AddReactionIconWrap(\n\t\tparent,\n\t\tstd::move(iconPositionValue),\n\t\ticonSize,\n\t\tpaintCallback,\n\t\tstd::move(destroys),\n\t\tstateLifetime);\n\tstate->repaint = crl::guard(widget, [=] { widget->update(); });\n}\n\nvoid ReactionsSettingsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller) {\n\n\tstruct State {\n\t\trpl::variable<Data::ReactionId> selectedId;\n\t};\n\n\tconst auto &reactions = controller->session().data().reactions();\n\tconst auto state = box->lifetime().make_state<State>();\n\tstate->selectedId = reactions.favoriteId();\n\n\tconst auto pinnedToTop = box->setPinnedToTopContent(\n\t\tobject_ptr<Ui::VerticalLayout>(box));\n\n\tauto idValue = state->selectedId.value();\n\tAddMessage(pinnedToTop, controller, std::move(idValue), box->width());\n\n\tUi::AddSubsectionTitle(\n\t\tpinnedToTop,\n\t\ttr::lng_settings_chat_reactions_subtitle());\n\n\tconst auto container = box->verticalLayout();\n\n\tbox->setTitle(tr::lng_settings_chat_reactions_title());\n\tbox->setWidth(st::boxWideWidth);\n\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\tconst auto &data = controller->session().data();\n\t\tconst auto selectedId = state->selectedId.current();\n\t\tif (data.reactions().favoriteId() != selectedId) {\n\t\t\tdata.reactions().setFavorite(selectedId);\n\t\t}\n\t\tbox->closeBox();\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\n\tif (controller->session().premium()) {\n\t\tusing Selector = ChatHelpers::TabbedSelector;\n\t\tconst auto selector = container->add(\n\t\t\tobject_ptr<Selector>(\n\t\t\t\tcontainer,\n\t\t\t\tcontroller->uiShow(),\n\t\t\t\tWindow::GifPauseReason::Layer,\n\t\t\t\tSelector::Mode::FullReactions),\n\t\t\tstyle::margins());\n\t\tselector->setAllowEmojiWithoutPremium(false);\n\t\tselector->setRoundRadius(0);\n\t\tselector->resize(st::boxWideWidth, st::emojiPanMinHeight);\n\n\t\tconst auto docToReaction = box->lifetime().make_state<\n\t\t\tbase::flat_map<DocumentId, Data::ReactionId>>();\n\t\tauto recentIds = std::vector<DocumentId>();\n\t\tfor (const auto &r\n\t\t\t: reactions.list(Data::Reactions::Type::Active)) {\n\t\t\tconst auto docId = r.selectAnimation->id;\n\t\t\trecentIds.push_back(docId);\n\t\t\tdocToReaction->emplace(docId, r.id);\n\t\t}\n\t\tselector->provideRecentEmoji(\n\t\t\tChatHelpers::DocumentListToRecent(recentIds));\n\n\t\tselector->customEmojiChosen(\n\t\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\t\tconst auto docId = data.document->id;\n\t\t\tconst auto it = docToReaction->find(docId);\n\t\t\tif (it != docToReaction->end()) {\n\t\t\t\tstate->selectedId = it->second;\n\t\t\t} else {\n\t\t\t\tstate->selectedId = Data::ReactionId{ docId };\n\t\t\t}\n\t\t}, selector->lifetime());\n\t\treturn;\n\t}\n\n\tconst auto check = Ui::CreateChild<Ui::RpWidget>(container.get());\n\tcheck->resize(st::settingsReactionCornerSize);\n\tcheck->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tcheck->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(check);\n\t\tst::mediaPlayerMenuCheck.paintInCenter(p, check->rect());\n\t}, check->lifetime());\n\tconst auto checkButton = [=](not_null<const Ui::RpWidget*> button) {\n\t\tcheck->moveToRight(\n\t\t\tst::settingsButtonRightSkip,\n\t\t\tbutton->y() + (button->height() - check->height()) / 2);\n\t};\n\n\tauto firstCheckedButton = (Ui::RpWidget*)(nullptr);\n\tauto list = reactions.list(Data::Reactions::Type::Active);\n\tif (const auto favorite = reactions.favorite()) {\n\t\tif (favorite->id.custom()) {\n\t\t\tlist.insert(begin(list), *favorite);\n\t\t}\n\t}\n\tfor (const auto &r : list) {\n\t\tconst auto button = container->add(object_ptr<Ui::SettingsButton>(\n\t\t\tcontainer,\n\t\t\trpl::single<QString>(base::duplicate(r.title)),\n\t\t\tst::settingsButton));\n\n\t\tconst auto iconSize = st::settingsReactionSize;\n\t\tconst auto left = button->st().iconLeft;\n\t\tauto iconPositionValue = button->sizeValue(\n\t\t) | rpl::map([=](const QSize &s) {\n\t\t\treturn QPoint(\n\t\t\t\tleft + st::settingsReactionRightSkip,\n\t\t\t\t(s.height() - iconSize) / 2);\n\t\t});\n\t\tif (const auto customId = r.id.custom()) {\n\t\t\tAddReactionCustomIcon(\n\t\t\t\tbutton,\n\t\t\t\tstd::move(iconPositionValue),\n\t\t\t\ticonSize,\n\t\t\t\tcontroller,\n\t\t\t\tcustomId,\n\t\t\t\trpl::never<>(),\n\t\t\t\t&button->lifetime());\n\t\t} else {\n\t\t\tAddReactionAnimatedIcon(\n\t\t\t\tbutton,\n\t\t\t\tstd::move(iconPositionValue),\n\t\t\t\ticonSize,\n\t\t\t\tr,\n\t\t\t\tbutton->events(\n\t\t\t\t) | rpl::filter([=](not_null<QEvent*> event) {\n\t\t\t\t\treturn event->type() == QEvent::Enter;\n\t\t\t\t}) | rpl::to_empty,\n\t\t\t\trpl::never<>(),\n\t\t\t\t&button->lifetime());\n\t\t}\n\t\tbutton->setClickedCallback([=, id = r.id] {\n\t\t\tcheckButton(button);\n\t\t\tstate->selectedId = id;\n\t\t});\n\t\tif (r.id == state->selectedId.current()) {\n\t\t\tfirstCheckedButton = button;\n\t\t}\n\t}\n\tif (firstCheckedButton) {\n\t\tfirstCheckedButton->geometryValue(\n\t\t) | rpl::filter([=](const QRect &r) {\n\t\t\treturn r.isValid();\n\t\t}) | rpl::take(1) | rpl::on_next([=] {\n\t\t\tcheckButton(firstCheckedButton);\n\t\t}, firstCheckedButton->lifetime());\n\t}\n\tcheck->raise();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/reactions_settings_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass GenericBox;\nclass RpWidget;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Data {\nstruct Reaction;\n} // namespace Data\n\nvoid AddReactionAnimatedIcon(\n\tnot_null<Ui::RpWidget*> parent,\n\trpl::producer<QPoint> iconPositionValue,\n\tint iconSize,\n\tconst Data::Reaction &reaction,\n\trpl::producer<> &&selects,\n\trpl::producer<> &&destroys,\n\tnot_null<rpl::lifetime*> stateLifetime);\nvoid AddReactionCustomIcon(\n\tnot_null<Ui::RpWidget*> parent,\n\trpl::producer<QPoint> iconPositionValue,\n\tint iconSize,\n\tnot_null<Window::SessionController*> controller,\n\tDocumentId customId,\n\trpl::producer<> &&destroys,\n\tnot_null<rpl::lifetime*> stateLifetime);\n\nvoid ReactionsSettingsBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> controller);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/report_messages_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/report_messages_box.h\"\n\n#include \"api/api_report.h\"\n#include \"core/application.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_photo.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/boxes/report_box_graphics.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/rect.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\nnamespace {\n\n[[nodiscard]] object_ptr<Ui::BoxContent> ReportPhoto(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<PhotoData*> photo,\n\t\tconst style::ReportBox *stOverride) {\n\tconst auto source = [&] {\n\t\treturn peer->isUser()\n\t\t\t? (photo->hasVideo()\n\t\t\t\t? Ui::ReportSource::ProfileVideo\n\t\t\t\t: Ui::ReportSource::ProfilePhoto)\n\t\t\t: (peer->isChat() || (peer->isChannel() && peer->isMegagroup()))\n\t\t\t? (photo->hasVideo()\n\t\t\t\t? Ui::ReportSource::GroupVideo\n\t\t\t\t: Ui::ReportSource::GroupPhoto)\n\t\t\t: (photo->hasVideo()\n\t\t\t\t? Ui::ReportSource::ChannelVideo\n\t\t\t\t: Ui::ReportSource::ChannelPhoto);\n\t}();\n\tconst auto st = stOverride ? stOverride : &st::defaultReportBox;\n\treturn Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto show = box->uiShow();\n\t\tUi::ReportReasonBox(box, *st, source, [=](Ui::ReportReason reason) {\n\t\t\tshow->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\tUi::ReportDetailsBox(box, *st, [=](const QString &text) {\n\t\t\t\t\tApi::SendPhotoReport(show, peer, reason, text, photo);\n\t\t\t\t\tshow->hideLayer();\n\t\t\t\t});\n\t\t\t}));\n\t\t});\n\t});\n}\n\n} // namespace\n\nobject_ptr<Ui::BoxContent> ReportProfilePhotoBox(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<PhotoData*> photo) {\n\treturn ReportPhoto(peer, photo, nullptr);\n}\n\nvoid ShowReportMessageBox(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tconst std::vector<MsgId> &ids,\n\t\tconst std::vector<StoryId> &stories,\n\t\tconst style::ReportBox *stOverride) {\n\tconst auto report = Api::CreateReportMessagesOrStoriesCallback(\n\t\tshow,\n\t\tpeer);\n\n\tauto performRequest = [=](\n\t\t\tconst auto &repeatRequest,\n\t\t\tData::ReportInput reportInput) -> void {\n\t\treport(reportInput, [=](const Api::ReportResult &result) {\n\t\t\tif (!result.error.isEmpty()) {\n\t\t\t\tif (result.error == u\"MESSAGE_ID_REQUIRED\"_q) {\n\t\t\t\t\tconst auto widget = show->toastParent();\n\t\t\t\t\tconst auto window = Core::App().findWindow(widget);\n\t\t\t\t\tconst auto controller = window\n\t\t\t\t\t\t? window->sessionController()\n\t\t\t\t\t\t: nullptr;\n\t\t\t\t\tif (controller) {\n\t\t\t\t\t\tconst auto callback = [=](std::vector<MsgId> ids) {\n\t\t\t\t\t\t\tauto copy = reportInput;\n\t\t\t\t\t\t\tcopy.ids = std::move(ids);\n\t\t\t\t\t\t\trepeatRequest(repeatRequest, std::move(copy));\n\t\t\t\t\t\t};\n\t\t\t\t\t\tcontroller->showChooseReportMessages(\n\t\t\t\t\t\t\tpeer,\n\t\t\t\t\t\t\treportInput,\n\t\t\t\t\t\t\tstd::move(callback));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tshow->showToast(result.error);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!result.options.empty() || result.commentOption) {\n\t\t\t\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\t\tbox->setTitle(result.title.isEmpty()\n\t\t\t\t\t\t? reportInput.optionText\n\t\t\t\t\t\t: result.title);\n\n\t\t\t\t\tfor (const auto &option : result.options) {\n\t\t\t\t\t\tconst auto button = Ui::AddReportOptionButton(\n\t\t\t\t\t\t\tbox->verticalLayout(),\n\t\t\t\t\t\t\toption.text,\n\t\t\t\t\t\t\tstOverride);\n\t\t\t\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\t\t\t\tauto copy = reportInput;\n\t\t\t\t\t\t\tcopy.optionId = option.id;\n\t\t\t\t\t\t\tcopy.optionText = option.text;\n\t\t\t\t\t\t\trepeatRequest(repeatRequest, std::move(copy));\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (const auto commentOption = result.commentOption) {\n\t\t\t\t\t\tconstexpr auto kReportReasonLengthMax = 512;\n\t\t\t\t\t\tconst auto &st = stOverride\n\t\t\t\t\t\t\t? stOverride\n\t\t\t\t\t\t\t: &st::defaultReportBox;\n\t\t\t\t\t\tUi::AddReportDetailsIconButton(box);\n\t\t\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\t\t\tconst auto details = box->addRow(\n\t\t\t\t\t\t\tobject_ptr<Ui::InputField>(\n\t\t\t\t\t\t\t\tbox,\n\t\t\t\t\t\t\t\tst->field,\n\t\t\t\t\t\t\t\tUi::InputField::Mode::MultiLine,\n\t\t\t\t\t\t\t\tcommentOption->optional\n\t\t\t\t\t\t\t\t\t? tr::lng_report_details_optional()\n\t\t\t\t\t\t\t\t\t: tr::lng_report_details_non_optional(),\n\t\t\t\t\t\t\t\tQString()));\n\t\t\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tconst auto container = box->verticalLayout();\n\t\t\t\t\t\t\tauto label = object_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\t\t\tcontainer,\n\t\t\t\t\t\t\t\ttr::lng_report_details_message_about(),\n\t\t\t\t\t\t\t\tst->divider.label);\n\t\t\t\t\t\t\tusing namespace Ui;\n\t\t\t\t\t\t\tconst auto widget = container->add(\n\t\t\t\t\t\t\t\tobject_ptr<PaddingWrap<>>(\n\t\t\t\t\t\t\t\t\tcontainer,\n\t\t\t\t\t\t\t\t\tstd::move(label),\n\t\t\t\t\t\t\t\t\tst::defaultBoxDividerLabelPadding));\n\t\t\t\t\t\t\tconst auto background\n\t\t\t\t\t\t\t\t= CreateChild<BoxContentDivider>(\n\t\t\t\t\t\t\t\t\twidget,\n\t\t\t\t\t\t\t\t\tst::boxDividerHeight,\n\t\t\t\t\t\t\t\t\tst->divider.bar,\n\t\t\t\t\t\t\t\t\tRectPart::Top | RectPart::Bottom);\n\t\t\t\t\t\t\tbackground->lower();\n\t\t\t\t\t\t\twidget->sizeValue(\n\t\t\t\t\t\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\t\t\t\t\t\tbackground->resize(s);\n\t\t\t\t\t\t\t}, background->lifetime());\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdetails->setMaxLength(kReportReasonLengthMax);\n\t\t\t\t\t\tbox->setFocusCallback([=] {\n\t\t\t\t\t\t\tdetails->setFocusFast();\n\t\t\t\t\t\t});\n\t\t\t\t\t\tconst auto submit = [=] {\n\t\t\t\t\t\t\tif (!commentOption->optional\n\t\t\t\t\t\t\t\t&& details->empty()) {\n\t\t\t\t\t\t\t\tdetails->showError();\n\t\t\t\t\t\t\t\tdetails->setFocus();\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tauto copy = reportInput;\n\t\t\t\t\t\t\tcopy.optionId = commentOption->id;\n\t\t\t\t\t\t\tcopy.comment = details->getLastText();\n\t\t\t\t\t\t\trepeatRequest(repeatRequest, std::move(copy));\n\t\t\t\t\t\t};\n\t\t\t\t\t\tdetails->submits(\n\t\t\t\t\t\t) | rpl::on_next(submit, details->lifetime());\n\t\t\t\t\t\tbox->addButton(tr::lng_report_button(), submit);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbox->addButton(\n\t\t\t\t\t\t\ttr::lng_close(),\n\t\t\t\t\t\t\t[=] { show->hideLayer(); });\n\t\t\t\t\t}\n\t\t\t\t\tif (!reportInput.optionId.isNull()) {\n\t\t\t\t\t\tbox->addLeftButton(\n\t\t\t\t\t\t\ttr::lng_create_group_back(),\n\t\t\t\t\t\t\t[=] { box->closeBox(); });\n\t\t\t\t\t}\n\t\t\t\t}));\n\t\t\t} else if (result.successful) {\n\t\t\t\tconstexpr auto kToastDuration = crl::time(4000);\n\t\t\t\tshow->showToast(\n\t\t\t\t\ttr::lng_report_thanks(tr::now),\n\t\t\t\t\tkToastDuration);\n\t\t\t\tshow->hideLayer();\n\t\t\t}\n\t\t});\n\t};\n\tperformRequest(performRequest, { .ids = ids, .stories = stories });\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/report_messages_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\ntemplate <typename Object>\nclass object_ptr;\n\nnamespace Ui {\nclass BoxContent;\nclass Show;\n} // namespace Ui\n\nnamespace style {\nstruct ReportBox;\n} // namespace style\n\nclass PeerData;\n\n[[nodiscard]] object_ptr<Ui::BoxContent> ReportProfilePhotoBox(\n\tnot_null<PeerData*> peer,\n\tnot_null<PhotoData*> photo);\n\nvoid ShowReportMessageBox(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<PeerData*> peer,\n\tconst std::vector<MsgId> &ids,\n\tconst std::vector<StoryId> &stories,\n\tconst style::ReportBox *stOverride = nullptr);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/ringtones_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/ringtones_box.h\"\n\n#include \"data/notify/data_peer_notify_volume.h\"\n#include \"data/notify/data_peer_notify_settings.h\"\n#include \"api/api_ringtones.h\"\n#include \"apiwrap.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"base/call_delayed.h\"\n#include \"base/event_filter.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/file_utilities.h\"\n#include \"core/mime_type.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_document_resolver.h\"\n#include \"data/data_thread.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"media/audio/media_audio.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"platform/platform_notifications_manager.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/vertical_list.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\nnamespace {\n\nconstexpr auto kDefaultValue = -1;\nconstexpr auto kNoSoundValue = -2;\nconstexpr auto kNoDetachTimeout = crl::time(250);\n\nclass AudioCreator final {\npublic:\n\tAudioCreator();\n\tAudioCreator(AudioCreator &&other);\n\t~AudioCreator();\n\nprivate:\n\trpl::lifetime _lifetime;\n\tbool _attached = false;\n\n};\n\nAudioCreator::AudioCreator()\n: _attached(true) {\n\tcrl::async([] {\n\t\tQMutexLocker lock(Media::Player::internal::audioPlayerMutex());\n\t\tMedia::Audio::AttachToDevice();\n\t});\n\tbase::timer_each(\n\t\tkNoDetachTimeout\n\t) | rpl::on_next([=] {\n\t\tMedia::Audio::StopDetachIfNotUsedSafe();\n\t}, _lifetime);\n}\n\nAudioCreator::AudioCreator(AudioCreator &&other)\n: _lifetime(base::take(other._lifetime))\n, _attached(base::take(other._attached)) {\n}\n\nAudioCreator::~AudioCreator() {\n\tif (_attached) {\n\t\tMedia::Audio::ScheduleDetachIfNotUsedSafe();\n\t}\n}\n\n} // namespace\n\nQString ExtractRingtoneName(not_null<DocumentData*> document) {\n\tif (document->isNull()) {\n\t\treturn QString();\n\t}\n\tconst auto name = document->filename();\n\tif (!name.isEmpty()) {\n\t\tconst auto extension = Core::FileExtension(name);\n\t\tif (extension.isEmpty()) {\n\t\t\treturn name;\n\t\t} else if (name.size() > extension.size() + 1) {\n\t\t\treturn name.mid(0, name.size() - extension.size() - 1);\n\t\t}\n\t}\n\tconst auto date = langDateTime(\n\t\tbase::unixtime::parse(document->date));\n\tconst auto base = document->isVoiceMessage()\n\t\t? (tr::lng_in_dlg_audio(tr::now) + ' ')\n\t\t: document->isAudioFile()\n\t\t? (tr::lng_in_dlg_audio_file(tr::now) + ' ')\n\t\t: QString();\n\treturn base + date;\n}\n\nvoid RingtonesBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tData::NotifySound selected,\n\t\tFn<void(Data::NotifySound)> save,\n\t\tData::VolumeController volumeController) {\n\tbox->setTitle(tr::lng_ringtones_box_title());\n\n\tconst auto container = box->verticalLayout();\n\n\tauto padding = st::boxPadding;\n\tpadding.setTop(padding.bottom());\n\n\tstruct State {\n\t\tAudioCreator creator;\n\t\tstd::shared_ptr<Ui::RadiobuttonGroup> group;\n\t\tstd::vector<std::shared_ptr<Data::DocumentMedia>> medias;\n\t\tData::NotifySound chosen;\n\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t\tQPointer<Ui::Radiobutton> defaultButton;\n\t\tQPointer<Ui::Radiobutton> chosenButton;\n\t\tstd::vector<QPointer<Ui::Radiobutton>> buttons;\n\t\tushort presavedVolume = 0;\n\t};\n\tconst auto state = container->lifetime().make_state<State>(State{\n\t\t.group = std::make_shared<Ui::RadiobuttonGroup>(),\n\t\t.chosen = selected,\n\t});\n\n\tconst auto volumeOverride = [volume = volumeController.volume] {\n\t\treturn volume ? (0.01 * volume()) : -1;\n\t};\n\n\tconst auto addToGroup = [=](\n\t\t\tnot_null<Ui::VerticalLayout*> verticalLayout,\n\t\t\tint value,\n\t\t\tconst QString &text,\n\t\t\tbool chosen) {\n\t\tif (chosen) {\n\t\t\tstate->group->setValue(value);\n\t\t}\n\t\tconst auto button = verticalLayout->add(\n\t\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\t\tverticalLayout,\n\t\t\t\tstate->group,\n\t\t\t\tvalue,\n\t\t\t\ttext,\n\t\t\t\tst::defaultCheckbox),\n\t\t\tpadding);\n\t\tif (chosen) {\n\t\t\tstate->chosenButton = button;\n\t\t}\n\t\tif (value == kDefaultValue) {\n\t\t\tstate->defaultButton = button;\n\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\tCore::App().notifications().playSound(\n\t\t\t\t\tsession,\n\t\t\t\t\t0,\n\t\t\t\t\tvolumeOverride());\n\t\t\t});\n\t\t}\n\t\tif (value < 0) {\n\t\t\treturn;\n\t\t}\n\t\twhile (state->buttons.size() <= value) {\n\t\t\tstate->buttons.push_back(nullptr);\n\t\t}\n\t\tbutton->setClickedCallback([=] {\n\t\t\tconst auto media = state->medias[value].get();\n\t\t\tif (media->loaded()) {\n\t\t\t\tCore::App().notifications().playSound(\n\t\t\t\t\tsession,\n\t\t\t\t\tmedia->owner()->id,\n\t\t\t\t\tvolumeOverride());\n\t\t\t}\n\t\t});\n\t\tbase::install_event_filter(button, [=](not_null<QEvent*> e) {\n\t\t\tif (e->type() != QEvent::ContextMenu || state->menu) {\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t}\n\t\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\tbutton,\n\t\t\t\tst::popupMenuWithIcons);\n\t\t\tauto callback = [=] {\n\t\t\t\tconst auto id = state->medias[value]->owner()->id;\n\t\t\t\tsession->api().ringtones().remove(id);\n\t\t\t};\n\t\t\tstate->menu->addAction(\n\t\t\t\ttr::lng_box_delete(tr::now),\n\t\t\t\tstd::move(callback),\n\t\t\t\t&st::menuIconDelete);\n\t\t\tstate->menu->popup(QCursor::pos());\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t});\n\t};\n\n\tsession->api().ringtones().uploadFails(\n\t) | rpl::on_next([=](const QString &error) {\n\t\tif ((error == u\"RINGTONE_DURATION_TOO_LONG\"_q)) {\n\t\t\tbox->getDelegate()->show(Ui::MakeInformBox(\n\t\t\t\ttr::lng_ringtones_error_max_duration(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_duration,\n\t\t\t\t\tUi::FormatMuteFor(\n\t\t\t\t\t\tsession->api().ringtones().maxDuration()))));\n\t\t} else if ((error == u\"RINGTONE_SIZE_TOO_BIG\"_q)) {\n\t\t\tbox->getDelegate()->show(Ui::MakeInformBox(\n\t\t\t\ttr::lng_ringtones_error_max_size(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_size,\n\t\t\t\t\tUi::FormatSizeText(\n\t\t\t\t\t\tsession->api().ringtones().maxSize()))));\n\t\t} else if (error == u\"RINGTONE_MIME_INVALID\"_q) {\n\t\t\tbox->getDelegate()->show(\n\t\t\t\tUi::MakeInformBox(tr::lng_edit_media_invalid_file()));\n\t\t}\n\t}, box->lifetime());\n\n\tUi::AddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_ringtones_box_cloud_subtitle());\n\n\tconst auto noSound = selected.none;\n\taddToGroup(\n\t\tcontainer,\n\t\tkDefaultValue,\n\t\ttr::lng_ringtones_box_default(tr::now),\n\t\tfalse);\n\taddToGroup(\n\t\tcontainer,\n\t\tkNoSoundValue,\n\t\ttr::lng_ringtones_box_no_sound(tr::now),\n\t\tnoSound);\n\n\tconst auto custom = container->add(\n\t\tobject_ptr<Ui::VerticalLayout>(container));\n\n\tconst auto rebuild = [=] {\n\t\tconst auto old = base::take(state->medias);\n\t\tauto value = 0;\n\t\twhile (custom->count()) {\n\t\t\tdelete custom->widgetAt(0);\n\t\t}\n\n\t\tfor (const auto &id : session->api().ringtones().list()) {\n\t\t\tconst auto chosen = (state->chosen.id && state->chosen.id == id);\n\t\t\tconst auto document = session->data().document(id);\n\t\t\tconst auto text = ExtractRingtoneName(document);\n\t\t\taddToGroup(custom, value++, text, chosen);\n\t\t\tstate->medias.push_back(document->createMediaView());\n\t\t\tdocument->owner().notifySettings().cacheSound(document);\n\t\t}\n\n\t\tcustom->resizeToWidth(container->width());\n\t\tif (!state->chosenButton) {\n\t\t\tstate->group->setValue(kDefaultValue);\n\t\t\tstate->defaultButton->finishAnimating();\n\t\t}\n\t};\n\n\tsession->api().ringtones().listUpdates(\n\t) | rpl::on_next(rebuild, container->lifetime());\n\n\tsession->api().ringtones().uploadDones(\n\t) | rpl::on_next([=](DocumentId id) {\n\t\tstate->chosen = Data::NotifySound{ .id = id };\n\t\trebuild();\n\t}, container->lifetime());\n\n\tsession->api().ringtones().requestList();\n\trebuild();\n\n\tconst auto upload = box->addRow(\n\t\tSettings::CreateButtonWithIcon(\n\t\t\tcontainer,\n\t\t\ttr::lng_ringtones_box_upload_button(),\n\t\t\tst::ringtonesBoxButton,\n\t\t\t{\n\t\t\t\t&st::settingsIconAdd,\n\t\t\t\tSettings::IconType::Round,\n\t\t\t\t&st::windowBgActive\n\t\t\t}),\n\t\tstyle::margins());\n\tupload->addClickHandler([=] {\n\t\tconst auto delay = st::ringtonesBoxButton.ripple.hideDuration;\n\t\tbase::call_delayed(delay, crl::guard(box, [=] {\n\t\t\tconst auto callback = [=](const FileDialog::OpenResult &result) {\n\t\t\t\tauto mime = QString();\n\t\t\t\tauto name = QString();\n\t\t\t\tauto content = result.remoteContent;\n\t\t\t\tif (!result.paths.isEmpty()) {\n\t\t\t\t\tauto info = QFileInfo(result.paths.front());\n\t\t\t\t\tmime = Core::MimeTypeForFile(info).name();\n\t\t\t\t\tname = info.fileName();\n\t\t\t\t\tauto f = QFile(result.paths.front());\n\t\t\t\t\tif (f.open(QIODevice::ReadOnly)) {\n\t\t\t\t\t\tcontent = f.readAll();\n\t\t\t\t\t\tf.close();\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tmime = Core::MimeTypeForData(content).name();\n\t\t\t\t\tname = \"audio\";\n\t\t\t\t}\n\t\t\t\tconst auto &ringtones = session->api().ringtones();\n\t\t\t\tif (int(content.size()) > ringtones.maxSize()) {\n\t\t\t\t\tbox->getDelegate()->show(Ui::MakeInformBox(\n\t\t\t\t\t\ttr::lng_ringtones_error_max_size(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_size,\n\t\t\t\t\t\t\tUi::FormatSizeText(ringtones.maxSize()))));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tsession->api().ringtones().upload(name, mime, content);\n\t\t\t};\n\t\t\tFileDialog::GetOpenPath(\n\t\t\t\tbox.get(),\n\t\t\t\ttr::lng_ringtones_box_upload_choose(tr::now),\n\t\t\t\t\"Audio files (*.mp3)\",\n\t\t\t\tcrl::guard(box, callback));\n\t\t}));\n\t});\n\n\tif (volumeController.volume && volumeController.saveVolume) {\n\t\tauto saveAndTestVolume = [=](ushort currentVolume) {\n\t\t\tstate->presavedVolume = currentVolume;\n\t\t\tconst auto value = state->group->current();\n\t\t\tif (value != kNoSoundValue) {\n\t\t\t\tCore::App().notifications().playSound(\n\t\t\t\t\tsession,\n\t\t\t\t\t(value == kDefaultValue)\n\t\t\t\t\t\t? 0\n\t\t\t\t\t\t: state->medias[value]->owner()->id,\n\t\t\t\t\t0.01 * currentVolume);\n\t\t\t}\n\t\t};\n\t\tUi::AddRingtonesVolumeSlider(\n\t\t\tcontainer,\n\t\t\tstate->group->value() | rpl::map([=](int value) {\n\t\t\t\treturn value != kNoSoundValue;\n\t\t\t}),\n\t\t\ttr::lng_ringtones_box_volume(),\n\t\t\tData::VolumeController{\n\t\t\t\tbase::take(volumeController.volume),\n\t\t\t\tstd::move(saveAndTestVolume),\n\t\t\t});\n\t}\n\n\tbox->addSkip(st::ringtonesBoxSkip);\n\tUi::AddDividerText(container, tr::lng_ringtones_box_about());\n\n\tbox->addSkip(st::ringtonesBoxSkip);\n\n\tbox->setWidth(st::boxWideWidth);\n\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\tconst auto value = state->group->current();\n\t\tauto sound = (value == kDefaultValue)\n\t\t\t? Data::NotifySound()\n\t\t\t: (value == kNoSoundValue)\n\t\t\t? Data::NotifySound{ .none = true }\n\t\t\t: Data::NotifySound{ .id = state->medias[value]->owner()->id };\n\t\tif (state->presavedVolume) {\n\t\t\tvolumeController.saveVolume(state->presavedVolume);\n\t\t}\n\t\tsave(sound);\n\t\tbox->closeBox();\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\nvoid ThreadRingtonesBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Data::Thread*> thread) {\n\tconst auto now = thread->owner().notifySettings().sound(thread);\n\tRingtonesBox(box, &thread->session(), now, [=](Data::NotifySound sound) {\n\t\tthread->owner().notifySettings().update(thread, {}, {}, sound);\n\t}, Data::ThreadRingtonesVolumeController(thread));\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/ringtones_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n\nnamespace Data {\nstruct NotifySound;\nclass Thread;\nenum class DefaultNotify : uint8_t;\nstruct VolumeController;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\n[[nodiscard]] QString ExtractRingtoneName(not_null<DocumentData*> document);\n\nvoid RingtonesBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session,\n\tData::NotifySound selected,\n\tFn<void(Data::NotifySound)> save,\n\tData::VolumeController volumeController);\n\nvoid ThreadRingtonesBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Data::Thread*> thread);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/select_future_owner_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/select_future_owner_box.h\"\n\n#include \"api/api_chat_participants.h\"\n#include \"api/api_cloud_password.h\"\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/filters/edit_filter_chats_list.h\"\n#include \"boxes/passcode_box.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/peer_lists_box.h\"\n#include \"boxes/peers/replace_boost_box.h\" // CreateUserpicsTransfer.\n#include \"core/application.h\"\n#include \"core/core_cloud_password.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"dialogs/ui/chat_search_empty.h\"\n#include \"boxes/peers/channel_ownership_transfer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n\nnamespace {\n\nenum class ParticipantType {\n\tAdmins,\n\tMembers\n};\n\nclass FutureOwnerController : public PeerListController {\npublic:\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid itemDeselectedHook(not_null<PeerData*> peer) override;\n\n\tvoid setOnRowClicked(Fn<void()> callback);\n\trpl::producer<> itemDeselected() const;\n\nprivate:\n\tFn<void()> _onRowClicked;\n\trpl::event_stream<> _itemDeselected;\n\n};\n\nvoid FutureOwnerController::setOnRowClicked(Fn<void()> callback) {\n\t_onRowClicked = callback;\n}\n\nvoid FutureOwnerController::rowClicked(not_null<PeerListRow*> row) {\n\tdelegate()->peerListSetRowChecked(\n\t\trow,\n\t\t!delegate()->peerListIsRowChecked(row));\n\tfor (auto i = 0; i < delegate()->peerListFullRowsCount(); ++i) {\n\t\tauto r = delegate()->peerListRowAt(i);\n\t\tif (r != row) {\n\t\t\tdelegate()->peerListSetRowChecked(r, false);\n\t\t}\n\t}\n\tif (_onRowClicked) {\n\t\t_onRowClicked();\n\t}\n}\n\nvoid FutureOwnerController::itemDeselectedHook(not_null<PeerData*> peer) {\n\t_itemDeselected.fire({});\n}\n\nrpl::producer<> FutureOwnerController::itemDeselected() const {\n\treturn _itemDeselected.events();\n}\n\nclass ParticipantsController : public FutureOwnerController {\npublic:\n\tParticipantsController(\n\t\tnot_null<ChannelData*> channel,\n\t\tParticipantType type);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid loadMoreRows() override;\n\nprivate:\n\tconst not_null<ChannelData*> _channel;\n\tconst ParticipantType _type;\n\tMTP::Sender _api;\n\n\tmtpRequestId _loadRequestId = 0;\n\tint _offset = 0;\n\tbool _allLoaded = false;\n\n};\n\nParticipantsController::ParticipantsController(\n\tnot_null<ChannelData*> channel,\n\tParticipantType type)\n: _channel(channel)\n, _type(type)\n, _api(&channel->session().mtp()) {\n}\n\nMain::Session &ParticipantsController::session() const {\n\treturn _channel->session();\n}\n\nvoid ParticipantsController::prepare() {\n\tloadMoreRows();\n}\n\nvoid ParticipantsController::loadMoreRows() {\n\tif (_loadRequestId || _allLoaded) {\n\t\treturn;\n\t}\n\n\tconst auto perPage = (_offset > 0) ? 200 : 50;\n\tconst auto participantsHash = uint64(0);\n\tconst auto filter = (_type == ParticipantType::Admins)\n\t\t? MTP_channelParticipantsAdmins()\n\t\t: MTP_channelParticipantsRecent();\n\n\t_loadRequestId = _api.request(MTPchannels_GetParticipants(\n\t\t_channel->inputChannel(),\n\t\tfilter,\n\t\tMTP_int(_offset),\n\t\tMTP_int(perPage),\n\t\tMTP_long(participantsHash)\n\t)).done([=](const MTPchannels_ChannelParticipants &result) {\n\t\t_loadRequestId = 0;\n\t\tauto added = false;\n\n\t\tresult.match([&](const MTPDchannels_channelParticipants &data) {\n\t\t\tconst auto &[availableCount, list] = Api::ChatParticipants::Parse(\n\t\t\t\t_channel,\n\t\t\t\tdata);\n\t\t\tfor (const auto &participant : list) {\n\t\t\t\tif (const auto user = _channel->owner().userLoaded(\n\t\t\t\t\t\tparticipant.userId())) {\n\t\t\t\t\tif (delegate()->peerListFindRow(user->id.value)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (user->isBot()) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tusing Type = Api::ChatParticipant::Type;\n\t\t\t\t\tif ((participant.type() == Type::Creator)\n\t\t\t\t\t\t|| (_type == ParticipantType::Members\n\t\t\t\t\t\t\t&& (participant.type() == Type::Admin))) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tauto row = std::make_unique<PeerListRow>(user);\n\t\t\t\t\tconst auto promotedSince = participant.promotedSince();\n\t\t\t\t\trow->setCustomStatus(\n\t\t\t\t\t\t(promotedSince\n\t\t\t\t\t\t\t? tr::lng_select_next_owner_box_status_promoted\n\t\t\t\t\t\t\t: tr::lng_select_next_owner_box_status_joined)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_date,\n\t\t\t\t\t\t\tUi::FormatDateTime(\n\t\t\t\t\t\t\t\tbase::unixtime::parse(promotedSince\n\t\t\t\t\t\t\t\t\t? promotedSince\n\t\t\t\t\t\t\t\t\t: participant.memberSince()))));\n\t\t\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t\t\t\tadded = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (const auto size = list.size()) {\n\t\t\t\t_offset += size;\n\t\t\t} else {\n\t\t\t\t_allLoaded = true;\n\t\t\t}\n\t\t}, [](const MTPDchannels_channelParticipantsNotModified &) {\n\t\t});\n\n\t\tif (!added && _offset > 0) {\n\t\t\t_allLoaded = true;\n\t\t}\n\t\tdelegate()->peerListRefreshRows();\n\t}).fail([=] {\n\t\t_loadRequestId = 0;\n\t}).send();\n}\n\nclass LegacyParticipantsController : public FutureOwnerController {\npublic:\n\tLegacyParticipantsController(\n\t\tnot_null<ChatData*> chat,\n\t\tParticipantType type);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid loadMoreRows() override;\n\nprivate:\n\tconst not_null<ChatData*> _chat;\n\tconst ParticipantType _type;\n\n};\n\nLegacyParticipantsController::LegacyParticipantsController(\n\tnot_null<ChatData*> chat,\n\tParticipantType type)\n: _chat(chat)\n, _type(type) {\n}\n\nMain::Session &LegacyParticipantsController::session() const {\n\treturn _chat->session();\n}\n\nvoid LegacyParticipantsController::prepare() {\n\tif (_chat->noParticipantInfo()) {\n\t\t_chat->updateFullForced();\n\t}\n\tconst auto &source = (_type == ParticipantType::Admins)\n\t\t? _chat->admins\n\t\t: _chat->participants;\n\tfor (const auto &user : source) {\n\t\tif (user->isBot()) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (user->id == peerFromUser(_chat->creator)) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (_type == ParticipantType::Members\n\t\t\t&& _chat->admins.contains(user)) {\n\t\t\tcontinue;\n\t\t}\n\t\tdelegate()->peerListAppendRow(\n\t\t\tstd::make_unique<PeerListRow>(user));\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid LegacyParticipantsController::loadMoreRows() {\n}\n\n} // namespace\n\nvoid SelectFutureOwnerbox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user) {\n\tconst auto content = box->verticalLayout();\n\tconst auto channel = peer->asChannel();\n\tconst auto chat = peer->asChat();\n\tconst auto isGroup = peer->isMegagroup() || peer->isChat();\n\tconst auto isLegacy = (chat != nullptr);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tcontent->add(\n\t\tCreateUserpicsTransfer(\n\t\t\tcontent,\n\t\t\trpl::single(std::vector<not_null<PeerData*>>{\n\t\t\t\tuser->session().user(),\n\t\t\t\tpeer,\n\t\t\t}),\n\t\t\tuser,\n\t\t\tUserpicsTransferType::ChannelFutureOwner),\n\t\tst::boxRowPadding);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\tisGroup\n\t\t\t\t? tr::lng_leave_next_owner_box_title_group()\n\t\t\t\t: tr::lng_leave_next_owner_box_title(),\n\t\t\tbox->getDelegate()->style().title),\n\t\tst::boxRowPadding);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tconst auto adminsCount = [&] {\n\t\tif (channel) {\n\t\t\treturn channel->adminsCount();\n\t\t} else if (chat) {\n\t\t\treturn int(chat->admins.size()) + 1;\n\t\t}\n\t\treturn 0;\n\t}();\n\tconst auto adminsAreEqual = (adminsCount <= 1);\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\t(isLegacy\n\t\t\t\t? (adminsAreEqual\n\t\t\t\t\t? tr::lng_leave_next_owner_box_about_legacy\n\t\t\t\t\t: tr::lng_leave_next_owner_box_about_admin_legacy)\n\t\t\t\t: (adminsAreEqual\n\t\t\t\t\t? tr::lng_leave_next_owner_box_about\n\t\t\t\t\t: tr::lng_leave_next_owner_box_about_admin))(\n\t\t\t\t\tlt_user,\n\t\t\t\t\tInfo::Profile::NameValue(user) | rpl::map(tr::marked),\n\t\t\t\t\tlt_chat,\n\t\t\t\t\tInfo::Profile::NameValue(peer) | rpl::map(tr::marked),\n\t\t\t\t\ttr::rich),\n\t\t\tst::boxLabel),\n\t\tst::boxRowPadding);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\n\tconst auto select = content->add(\n\t\tobject_ptr<Ui::RoundButton>(\n\t\t\tcontent,\n\t\t\t!adminsAreEqual\n\t\t\t\t? tr::lng_select_next_owner_box()\n\t\t\t\t: tr::lng_select_next_owner_box_admin(),\n\t\t\tst::defaultLightButton),\n\t\tst::boxRowPadding,\n\t\tstyle::al_justify);\n\tUi::AddSkip(content);\n\tconst auto cancel = content->add(\n\t\tobject_ptr<Ui::RoundButton>(\n\t\t\tcontent,\n\t\t\ttr::lng_cancel(),\n\t\t\tst::defaultLightButton),\n\t\tst::boxRowPadding,\n\t\tstyle::al_justify);\n\tcancel->setClickedCallback([=] {\n\t\tbox->closeBox();\n\t});\n\tUi::AddSkip(content);\n\tconst auto leave = content->add(\n\t\tobject_ptr<Ui::RoundButton>(\n\t\t\tcontent,\n\t\t\tisGroup\n\t\t\t\t? tr::lng_profile_leave_group()\n\t\t\t\t: tr::lng_profile_leave_channel(),\n\t\t\tst::attentionBoxButton),\n\t\tst::boxRowPadding,\n\t\tstyle::al_justify);\n\tleave->setClickedCallback([=, revoke = false] {\n\t\tpeer->session().api().deleteConversation(peer, revoke);\n\t\tbox->closeBox();\n\t});\n\tselect->setClickedCallback([=] {\n\t\tconst auto window = Core::App().findWindow(box);\n\t\tconst auto sessionController = window\n\t\t\t? window->sessionController()\n\t\t\t: nullptr;\n\t\tif (!sessionController) {\n\t\t\treturn;\n\t\t}\n\n\t\tusing Pair = std::pair<\n\t\t\tstd::unique_ptr<FutureOwnerController>,\n\t\t\tstd::unique_ptr<FutureOwnerController>>;\n\t\tauto makeControllers = [&]() -> Pair {\n\t\t\tif (channel) {\n\t\t\t\treturn {\n\t\t\t\t\tstd::make_unique<ParticipantsController>(\n\t\t\t\t\t\tchannel,\n\t\t\t\t\t\tParticipantType::Admins),\n\t\t\t\t\tstd::make_unique<ParticipantsController>(\n\t\t\t\t\t\tchannel,\n\t\t\t\t\t\tParticipantType::Members),\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\treturn {\n\t\t\t\t\tstd::make_unique<LegacyParticipantsController>(\n\t\t\t\t\t\tchat,\n\t\t\t\t\t\tParticipantType::Admins),\n\t\t\t\t\tstd::make_unique<LegacyParticipantsController>(\n\t\t\t\t\t\tchat,\n\t\t\t\t\t\tParticipantType::Members),\n\t\t\t\t};\n\t\t\t}\n\t\t};\n\t\tauto [adminsOwned, membersOwned] = makeControllers();\n\t\tconst auto admins = adminsOwned.get();\n\t\tconst auto members = membersOwned.get();\n\n\t\tauto initBox = [=](not_null<PeerListsBox*> selectBox) {\n\t\t\tstruct State {\n\t\t\t\tbase::unique_qptr<Dialogs::SearchEmpty> noLists;\n\t\t\t\trpl::event_stream<> selectionChanges;\n\t\t\t};\n\t\t\tconst auto state = selectBox->lifetime().make_state<State>();\n\t\t\tconst auto uncheckOtherList = [=](\n\t\t\t\t\tnot_null<PeerListController*> otherController) {\n\t\t\t\tauto delegate = otherController->delegate();\n\t\t\t\tconst auto full = delegate->peerListFullRowsCount();\n\t\t\t\tfor (auto i = 0; i < full; ++i) {\n\t\t\t\t\tdelegate->peerListSetRowChecked(\n\t\t\t\t\t\tdelegate->peerListRowAt(i),\n\t\t\t\t\t\tfalse);\n\t\t\t\t}\n\t\t\t\tstate->selectionChanges.fire({});\n\t\t\t};\n\t\t\tadmins->setOnRowClicked([=] { uncheckOtherList(members); });\n\t\t\tmembers->setOnRowClicked([=] { uncheckOtherList(admins); });\n\t\t\tselectBox->setTitle(!adminsAreEqual\n\t\t\t\t? tr::lng_select_next_owner_box_title()\n\t\t\t\t: tr::lng_select_next_owner_box_title_admin());\n\t\t\tconst auto searchEnabled = PeerListSearchMode::Enabled;\n\t\t\tadmins->delegate()->peerListSetSearchMode(searchEnabled);\n\t\t\tmembers->delegate()->peerListSetSearchMode(searchEnabled);\n\t\t\trpl::merge(\n\t\t\t\tadmins->itemDeselected(),\n\t\t\t\tmembers->itemDeselected()\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tstate->selectionChanges.fire({});\n\t\t\t}, selectBox->lifetime());\n\t\t\tconst auto separatorAdmins = selectBox->addSeparatorBefore(\n\t\t\t\t0,\n\t\t\t\tCreatePeerListSectionSubtitle(\n\t\t\t\t\tselectBox,\n\t\t\t\t\t!isGroup\n\t\t\t\t\t\t? tr::lng_select_next_owner_box_sub_admins()\n\t\t\t\t\t\t: tr::lng_select_next_owner_box_sub_admins_group()));\n\t\t\tconst auto separatorMembers = selectBox->addSeparatorBefore(\n\t\t\t\t1,\n\t\t\t\tCreatePeerListSectionSubtitle(\n\t\t\t\t\tselectBox,\n\t\t\t\t\t!isGroup\n\t\t\t\t\t\t? tr::lng_select_next_owner_box_sub_members()\n\t\t\t\t\t\t: tr::lng_select_next_owner_box_sub_members_group()));\n\t\t\trpl::combine(\n\t\t\t\tseparatorAdmins->heightValue(),\n\t\t\t\tseparatorMembers->heightValue()\n\t\t\t) | rpl::map(\n\t\t\t\t(rpl::mappers::_1 + rpl::mappers::_2) > 0\n\t\t\t) | rpl::distinct_until_changed() | rpl::on_next([=](bool has) {\n\t\t\t\tqDebug() << \"has\" << has;\n\t\t\t\tif (has) {\n\t\t\t\t\tstate->noLists = nullptr;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tusing namespace Dialogs;\n\t\t\t\tstate->noLists = base::make_unique_q<SearchEmpty>(\n\t\t\t\t\tselectBox,\n\t\t\t\t\tSearchEmpty::Icon::NoResults,\n\t\t\t\t\t(adminsAreEqual\n\t\t\t\t\t\t? tr::lng_select_next_owner_box_empty_list\n\t\t\t\t\t\t: tr::lng_select_next_owner_box_empty_list_admin)(\n\t\t\t\t\t\t\ttr::rich));\n\t\t\t\tstate->noLists->show();\n\t\t\t\tstate->noLists->raise();\n\t\t\t\tselectBox->sizeValue(\n\t\t\t\t) | rpl::filter_size() | rpl::on_next([=](QSize s) {\n\t\t\t\t\tstate->noLists->setMinimalHeight(s.height() / 3);\n\t\t\t\t\tstate->noLists->resizeToWidth(s.width() / 3 * 2);\n\t\t\t\t\tstate->noLists->moveToLeft(\n\t\t\t\t\t\t(s.width() - state->noLists->width()) / 2,\n\t\t\t\t\t\t(s.height() - state->noLists->height()) / 2);\n\t\t\t\t}, state->noLists->lifetime());\n\t\t\t\tcrl::on_main(state->noLists.get(), [=] {\n\t\t\t\t\tstate->noLists->animate();\n\t\t\t\t});\n\t\t\t}, selectBox->lifetime());\n\t\t\t{\n\t\t\t\tconst auto &st = st::futureOwnerBoxSelect;\n\t\t\t\tselectBox->setStyle(st);\n\t\t\t\tauto button = object_ptr<Ui::RoundButton>(\n\t\t\t\t\tselectBox,\n\t\t\t\t\trpl::conditional(\n\t\t\t\t\t\tstate->selectionChanges.events(\n\t\t\t\t\t\t) | rpl::map([=] {\n\t\t\t\t\t\t\treturn !selectBox->collectSelectedRows().empty();\n\t\t\t\t\t\t}),\n\t\t\t\t\t\ttr::lng_select_next_owner_box_confirm(),\n\t\t\t\t\t\ttr::lng_close()),\n\t\t\t\t\tst::defaultActiveButton);\n\t\t\t\tconst auto raw = button.data();\n\t\t\t\trpl::combine(\n\t\t\t\t\tstate->selectionChanges.events() | rpl::map_to(0),\n\t\t\t\t\tselectBox->widthValue()\n\t\t\t\t) | rpl::on_next([=](int, int width) {\n\t\t\t\t\traw->resizeToWidth(width\n\t\t\t\t\t\t- st.buttonPadding.left()\n\t\t\t\t\t\t- st.buttonPadding.right());\n\t\t\t\t}, selectBox->lifetime());\n\t\t\t\tbutton->setFullRadius(true);\n\t\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\t\tconst auto selected = selectBox->collectSelectedRows();\n\t\t\t\t\tif (selected.empty()) {\n\t\t\t\t\t\treturn selectBox->closeBox();\n\t\t\t\t\t}\n\t\t\t\t\tif (const auto user = selected.front()->asUser()) {\n\t\t\t\t\t\tauto &lifetime = selectBox->lifetime();\n\t\t\t\t\t\tlifetime.make_state<ChannelOwnershipTransfer>(\n\t\t\t\t\t\t\tpeer,\n\t\t\t\t\t\t\tuser,\n\t\t\t\t\t\t\tselectBox->uiShow(),\n\t\t\t\t\t\t\t[=](std::shared_ptr<Ui::Show> show) {\n\t\t\t\t\t\t\t\tconst auto revoke = false;\n\t\t\t\t\t\t\t\tpeer->session().api().deleteConversation(\n\t\t\t\t\t\t\t\t\tpeer,\n\t\t\t\t\t\t\t\t\trevoke);\n\t\t\t\t\t\t\t\tshow->hideLayer();\n\t\t\t\t\t\t\t})->start();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tselectBox->addButton(std::move(button));\n\t\t\t\tstate->selectionChanges.fire({});\n\t\t\t}\n\t\t};\n\n\t\tauto controllers = std::vector<std::unique_ptr<PeerListController>>();\n\t\tcontrollers.reserve(2);\n\t\tcontrollers.push_back(std::move(adminsOwned));\n\t\tcontrollers.push_back(std::move(membersOwned));\n\t\tbox->uiShow()->showBox(\n\t\t\tBox<PeerListsBox>(std::move(controllers), initBox));\n\t});\n\tfor (const auto &b : { select, cancel, leave }) {\n\t\tb->setFullRadius(true);\n\t}\n\tbox->setStyle(st::futureOwnerBox);\n}"
  },
  {
    "path": "Telegram/SourceFiles/boxes/select_future_owner_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\nclass UserData;\n\nnamespace Ui {\nclass GenericBox;\nclass Show;\n} // namespace Ui\n\nvoid SelectFutureOwnerbox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> user);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/self_destruction_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/self_destruction_box.h\"\n\n#include \"api/api_authorizations.h\"\n#include \"api/api_cloud_password.h\"\n#include \"api/api_self_destruct.h\"\n#include \"apiwrap.h\"\n#include \"boxes/passcode_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/rect.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace {\n\nusing Type = SelfDestructionBox::Type;\n\nvoid AddDeleteAccount(\n\t\tnot_null<Ui::BoxContent*> box,\n\t\tnot_null<Main::Session*> session) {\n\tif (!session->isTestMode()) {\n\t\treturn;\n\t}\n\tconst auto maybeState = session->api().cloudPassword().stateCurrent();\n\tif (!maybeState || !maybeState->hasPassword) {\n\t\treturn;\n\t}\n\tconst auto top = box->addTopButton(st::infoTopBarMenu);\n\tconst auto menu\n\t\t= top->lifetime().make_state<base::unique_qptr<Ui::PopupMenu>>();\n\tconst auto handler = [=] {\n\t\tsession->api().cloudPassword().state(\n\t\t) | rpl::take(\n\t\t\t1\n\t\t) | rpl::on_next([=](const Core::CloudPasswordState &state) {\n\t\t\tauto fields = PasscodeBox::CloudFields::From(state);\n\t\t\tfields.customTitle = tr::lng_settings_destroy_title();\n\t\t\tfields.customDescription = tr::lng_context_mark_read_all_sure_2(\n\t\t\t\ttr::now,\n\t\t\t\ttr::rich).text;\n\t\t\tfields.customSubmitButton = tr::lng_theme_delete();\n\t\t\tfields.customCheckCallback = [=](\n\t\t\t\t\tconst Core::CloudPasswordResult &result,\n\t\t\t\t\tbase::weak_qptr<PasscodeBox> box) {\n\t\t\t\tsession->api().request(MTPaccount_DeleteAccount(\n\t\t\t\t\tMTP_flags(MTPaccount_DeleteAccount::Flag::f_password),\n\t\t\t\t\tMTP_string(\"Manual\"),\n\t\t\t\t\tresult.result\n\t\t\t\t)).done([=] {\n\t\t\t\t\tif (box) {\n\t\t\t\t\t\tbox->uiShow()->hideLayer();\n\t\t\t\t\t}\n\t\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\t\tif (box) {\n\t\t\t\t\t\tbox->handleCustomCheckError(error.type());\n\t\t\t\t\t}\n\t\t\t\t}).send();\n\t\t\t};\n\t\t\tbox->uiShow()->showBox(Box<PasscodeBox>(session, fields));\n\t\t}, top->lifetime());\n\t};\n\ttop->setClickedCallback([=] {\n\t\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\ttop,\n\t\t\tst::popupMenuWithIcons);\n\n\t\tconst auto addAction = Ui::Menu::CreateAddActionCallback(menu->get());\n\t\taddAction({\n\t\t\t.text = tr::lng_settings_destroy_title(tr::now),\n\t\t\t.handler = handler,\n\t\t\t.icon = &st::menuIconDeleteAttention,\n\t\t\t.isAttention = true,\n\t\t});\n\t\t(*menu)->popup(QCursor::pos());\n\t});\n}\n\n[[nodiscard]] std::vector<int> Values(Type type) {\n\tswitch (type) {\n\tcase Type::Account: return { 30, 90, 180, 365, 548, 720 };\n\tcase Type::Sessions: return { 7, 30, 90, 180, 365 };\n\t}\n\tUnexpected(\"SelfDestructionBox::Type in Values.\");\n}\n\n} // namespace\n\nSelfDestructionBox::SelfDestructionBox(\n\tQWidget*,\n\tnot_null<Main::Session*> session,\n\tType type,\n\trpl::producer<int> preloaded)\n: _type(type)\n, _session(session)\n, _ttlValues(Values(type))\n, _loading(\n\t\tthis,\n\t\ttr::lng_contacts_loading(tr::now),\n\t\tst::membersAbout) {\n\tstd::move(\n\t\tpreloaded\n\t) | rpl::take(\n\t\t1\n\t) | rpl::on_next([=](int days) {\n\t\tgotCurrent(days);\n\t}, lifetime());\n}\n\nvoid SelfDestructionBox::gotCurrent(int days) {\n\tExpects(!_ttlValues.empty());\n\n\t_loading.destroy();\n\n\tauto daysAdjusted = _ttlValues[0];\n\tfor (const auto value : _ttlValues) {\n\t\tif (qAbs(days - value) < qAbs(days - daysAdjusted)) {\n\t\t\tdaysAdjusted = value;\n\t\t}\n\t}\n\t_ttlGroup = std::make_shared<Ui::RadiobuttonGroup>(daysAdjusted);\n\n\tif (_prepared) {\n\t\tshowContent();\n\t}\n}\n\nvoid SelfDestructionBox::showContent() {\n\tauto y = st::boxOptionListPadding.top();\n\t_description.create(\n\t\tthis,\n\t\t(_type == Type::Account\n\t\t\t? tr::lng_self_destruct_description(tr::now)\n\t\t\t: tr::lng_self_destruct_sessions_description(tr::now)),\n\t\tst::boxLabel);\n\t_description->moveToLeft(st::boxPadding.left(), y);\n\t_description->resizeToWidth(st::boxWidth\n\t\t- rect::m::sum::h(st::boxPadding));\n\ty += _description->height() + st::boxMediumSkip;\n\n\tfor (const auto value : _ttlValues) {\n\t\tconst auto button = Ui::CreateChild<Ui::Radiobutton>(\n\t\t\tthis,\n\t\t\t_ttlGroup,\n\t\t\tvalue,\n\t\t\tDaysLabel(value),\n\t\t\tst::autolockButton);\n\t\tbutton->moveToLeft(st::boxPadding.left(), y);\n\t\ty += button->heightNoMargins() + st::boxOptionListSkip;\n\t}\n\tshowChildren();\n\n\tclearButtons();\n\taddButton(tr::lng_settings_save(), [=] {\n\t\tconst auto value = _ttlGroup->current();\n\t\tswitch (_type) {\n\t\tcase Type::Account:\n\t\t\t_session->api().selfDestruct().updateAccountTTL(value);\n\t\t\tbreak;\n\t\tcase Type::Sessions:\n\t\t\t_session->api().authorizations().updateTTL(value);\n\t\t\tbreak;\n\t\t}\n\n\t\tcloseBox();\n\t});\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n}\n\nQString SelfDestructionBox::DaysLabel(int days) {\n\treturn !days\n\t\t? QString()\n\t\t//: (days > 364)\n\t\t//? tr::lng_years(tr::now, lt_count, days / 365)\n\t\t: (days > 25)\n\t\t? tr::lng_months(tr::now, lt_count, std::max(days / 30, 1))\n\t\t: tr::lng_weeks(tr::now, lt_count, std::max(days / 7, 1));\n}\n\nvoid SelfDestructionBox::prepare() {\n\tsetTitle((_type == Type::Account\n\t\t? tr::lng_self_destruct_title()\n\t\t: tr::lng_self_destruct_sessions_title()));\n\n\tauto fake = object_ptr<Ui::FlatLabel>(\n\t\tthis,\n\t\t(_type == Type::Account\n\t\t\t? tr::lng_self_destruct_description(tr::now)\n\t\t\t: tr::lng_self_destruct_sessions_description(tr::now)),\n\t\tst::boxLabel);\n\tfake->resizeToWidth(st::boxWidth\n\t\t- rect::m::sum::h(st::boxPadding));\n\tconst auto boxHeight = st::boxOptionListPadding.top()\n\t\t+ fake->height() + st::boxMediumSkip\n\t\t+ (_ttlValues.size()\n\t\t\t* (st::defaultRadio.diameter + st::boxOptionListSkip))\n\t\t- st::boxOptionListSkip\n\t\t+ st::boxOptionListPadding.bottom() + st::boxPadding.bottom();\n\tfake.destroy();\n\n\tsetDimensions(st::boxWidth, boxHeight);\n\n\taddButton(tr::lng_cancel(), [this] { closeBox(); });\n\n\tif (_loading) {\n\t\t_loading->moveToLeft(\n\t\t\t(st::boxWidth - _loading->width()) / 2,\n\t\t\tboxHeight / 3);\n\t\t_prepared = true;\n\t} else {\n\t\tshowContent();\n\t}\n\n\tAddDeleteAccount(this, _session);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/self_destruction_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n#include \"mtproto/sender.h\"\n\nnamespace Ui {\nclass RadiobuttonGroup;\nclass Radiobutton;\nclass FlatLabel;\n} // namespace Ui\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nclass SelfDestructionBox : public Ui::BoxContent {\npublic:\n\tenum class Type {\n\t\tAccount,\n\t\tSessions,\n\t};\n\tSelfDestructionBox(\n\t\tQWidget*,\n\t\tnot_null<Main::Session*> session,\n\t\tType type,\n\t\trpl::producer<int> preloaded);\n\n\tstatic QString DaysLabel(int days);\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\tvoid gotCurrent(int days);\n\tvoid showContent();\n\n\tconst Type _type;\n\tconst not_null<Main::Session*> _session;\n\tbool _prepared = false;\n\tstd::vector<int> _ttlValues;\n\tobject_ptr<Ui::FlatLabel> _description = { nullptr };\n\tobject_ptr<Ui::FlatLabel> _loading;\n\tstd::shared_ptr<Ui::RadiobuttonGroup> _ttlGroup;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/send_credits_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/send_credits_box.h\"\n\n#include \"api/api_credits.h\"\n#include \"apiwrap.h\"\n#include \"core/ui_integration.h\" // TextContext\n#include \"data/components/credits.h\"\n#include \"data/data_credits.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"info/channel_statistics/boosts/giveaway/boost_badge.h\" // InfiniteRadialAnimationWidget.\n#include \"lang/lang_keys.h\"\n#include \"main/session/session_show.h\"\n#include \"main/main_session.h\"\n#include \"payments/payments_checkout_process.h\"\n#include \"payments/payments_form.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/effects/premium_top_bar.h\" // Ui::Premium::ColorizedSvg.\n#include \"ui/image/image_prepare.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/peer_bubble.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_info.h\" // inviteLinkSubscribeBoxTerms\n#include \"styles/style_layers.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Ui {\nnamespace {\n\nstruct PaidMediaData {\n\tconst Data::Invoice *invoice = nullptr;\n\tHistoryItem *item = nullptr;\n\tPeerData *peer = nullptr;\n\tint photos = 0;\n\tint videos = 0;\n\n\texplicit operator bool() const {\n\t\treturn invoice && item && peer && (photos || videos);\n\t}\n};\n\n[[nodiscard]] PaidMediaData LookupPaidMediaData(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<Payments::CreditsFormData*> form) {\n\tusing namespace Payments;\n\tconst auto message = std::get_if<InvoiceMessage>(&form->id.value);\n\tconst auto item = message\n\t\t? session->data().message(message->peer, message->itemId)\n\t\t: nullptr;\n\tconst auto media = item ? item->media() : nullptr;\n\tconst auto invoice = media ? media->invoice() : nullptr;\n\tif (!invoice || !invoice->isPaidMedia) {\n\t\treturn {};\n\t}\n\n\tauto photos = 0;\n\tauto videos = 0;\n\tfor (const auto &media : invoice->extendedMedia) {\n\t\tconst auto photo = media->photo();\n\t\tif (photo && !photo->extendedMediaVideoDuration().has_value()) {\n\t\t\t++photos;\n\t\t} else {\n\t\t\t++videos;\n\t\t}\n\t}\n\n\tconst auto bot = item->viaBot();\n\tconst auto sender = item->originalSender();\n\treturn {\n\t\t.invoice = invoice,\n\t\t.item = item,\n\t\t.peer = (bot ? bot : sender ? sender : message->peer.get()),\n\t\t.photos = photos,\n\t\t.videos = videos,\n\t};\n}\n\nvoid AddTerms(\n\t\tnot_null<Ui::BoxContent*> box,\n\t\tnot_null<Ui::RpWidget*> button,\n\t\tconst style::Box &stBox) {\n\tconst auto terms = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton->parentWidget(),\n\t\ttr::lng_channel_invite_subscription_terms(\n\t\t\tlt_link,\n\t\t\trpl::combine(\n\t\t\t\ttr::lng_paid_react_agree_link(),\n\t\t\t\ttr::lng_group_invite_subscription_about_url()\n\t\t\t) | rpl::map([](const QString &text, const QString &url) {\n\t\t\t\treturn tr::link(text, url);\n\t\t\t}),\n\t\t\ttr::rich),\n\t\tst::inviteLinkSubscribeBoxTerms);\n\tconst auto &buttonPadding = stBox.buttonPadding;\n\tconst auto style = box->lifetime().make_state<style::Box>(style::Box{\n\t\t.buttonPadding = buttonPadding + QMargins(0, 0, 0, terms->height()),\n\t\t.buttonHeight = stBox.buttonHeight,\n\t\t.button = stBox.button,\n\t\t.margin = stBox.margin,\n\t\t.title = stBox.title,\n\t\t.bg = stBox.bg,\n\t\t.titleAdditionalFg = stBox.titleAdditionalFg,\n\t\t.shadowIgnoreTopSkip = stBox.shadowIgnoreTopSkip,\n\t\t.shadowIgnoreBottomSkip = stBox.shadowIgnoreBottomSkip,\n\t});\n\tbutton->geometryValue() | rpl::on_next([=](const QRect &rect) {\n\t\tterms->resizeToWidth(box->width()\n\t\t\t- rect::m::sum::h(st::boxRowPadding));\n\t\tterms->moveToLeft(\n\t\t\trect.x() + (rect.width() - terms->width()) / 2,\n\t\t\trect::bottom(rect) + buttonPadding.bottom() / 2);\n\t}, terms->lifetime());\n\tbox->setStyle(*style);\n}\n\n[[nodiscard]] rpl::producer<TextWithEntities> SendCreditsConfirmText(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<Payments::CreditsFormData*> form) {\n\tif (const auto data = LookupPaidMediaData(session, form)) {\n\t\tauto photos = 0;\n\t\tauto videos = 0;\n\t\tfor (const auto &media : data.invoice->extendedMedia) {\n\t\t\tconst auto photo = media->photo();\n\t\t\tif (photo && !photo->extendedMediaVideoDuration().has_value()) {\n\t\t\t\t++photos;\n\t\t\t} else {\n\t\t\t\t++videos;\n\t\t\t}\n\t\t}\n\n\t\tauto photosBold = tr::lng_credits_box_out_photos(\n\t\t\tlt_count,\n\t\t\trpl::single(photos) | tr::to_count(),\n\t\t\ttr::bold);\n\t\tauto videosBold = tr::lng_credits_box_out_videos(\n\t\t\tlt_count,\n\t\t\trpl::single(videos) | tr::to_count(),\n\t\t\ttr::bold);\n\t\tauto media = (!videos)\n\t\t\t\t? ((photos > 1)\n\t\t\t\t\t? std::move(photosBold)\n\t\t\t\t\t: tr::lng_credits_box_out_photo(tr::marked))\n\t\t\t\t: (!photos)\n\t\t\t\t? ((videos > 1)\n\t\t\t\t\t? std::move(videosBold)\n\t\t\t\t\t: tr::lng_credits_box_out_video(tr::marked))\n\t\t\t\t: tr::lng_credits_box_out_both(\n\t\t\t\t\tlt_photo,\n\t\t\t\t\tstd::move(photosBold),\n\t\t\t\t\tlt_video,\n\t\t\t\t\tstd::move(videosBold),\n\t\t\t\t\ttr::marked);\n\t\tif (const auto user = data.peer->asUser()) {\n\t\t\treturn tr::lng_credits_box_out_media_user(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(form->invoice.amount) | tr::to_count(),\n\t\t\t\tlt_media,\n\t\t\t\tstd::move(media),\n\t\t\t\tlt_user,\n\t\t\t\trpl::single(tr::bold(user->shortName())),\n\t\t\t\ttr::rich);\n\t\t}\n\t\treturn tr::lng_credits_box_out_media(\n\t\t\tlt_count,\n\t\t\trpl::single(form->invoice.amount) | tr::to_count(),\n\t\t\tlt_media,\n\t\t\tstd::move(media),\n\t\t\tlt_chat,\n\t\t\trpl::single(tr::bold(data.peer->name())),\n\t\t\ttr::rich);\n\t}\n\n\tconst auto bot = session->data().user(form->botId);\n\tif (form->invoice.subscriptionPeriod) {\n\t\treturn (bot->botInfo\n\t\t\t\t? tr::lng_credits_box_out_subscription_bot\n\t\t\t\t: tr::lng_credits_box_out_subscription_business)(\n\t\t\tlt_count,\n\t\t\trpl::single(form->invoice.amount) | tr::to_count(),\n\t\t\tlt_title,\n\t\t\trpl::single(TextWithEntities{ form->title }),\n\t\t\tlt_recipient,\n\t\t\trpl::single(TextWithEntities{ bot->name() }),\n\t\t\ttr::rich);\n\t}\n\treturn tr::lng_credits_box_out_sure(\n\t\tlt_count,\n\t\trpl::single(form->invoice.amount) | tr::to_count(),\n\t\tlt_text,\n\t\trpl::single(TextWithEntities{ form->title }),\n\t\tlt_bot,\n\t\trpl::single(TextWithEntities{ bot->name() }),\n\t\ttr::rich);\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> SendCreditsThumbnail(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<Payments::CreditsFormData*> form,\n\t\tint photoSize) {\n\tif (const auto data = LookupPaidMediaData(session, form)) {\n\t\tconst auto first = data.invoice->extendedMedia[0]->photo();\n\t\tconst auto second = (data.photos > 1)\n\t\t\t? data.invoice->extendedMedia[1]->photo()\n\t\t\t: nullptr;\n\t\tconst auto totalCount = int(data.invoice->extendedMedia.size());\n\t\tif (first && first->extendedMediaPreview()) {\n\t\t\treturn Settings::PaidMediaThumbnail(\n\t\t\t\tparent,\n\t\t\t\tfirst,\n\t\t\t\tsecond,\n\t\t\t\ttotalCount,\n\t\t\t\tphotoSize);\n\t\t}\n\t}\n\tif (form->photo) {\n\t\treturn Settings::HistoryEntryPhoto(parent, form->photo, photoSize);\n\t}\n\tconst auto bot = session->data().user(form->botId);\n\treturn object_ptr<Ui::UserpicButton>(\n\t\tparent,\n\t\tbot,\n\t\tst::defaultUserpicButton);\n}\n\n[[nodiscard]] not_null<Ui::RpWidget*> SendCreditsBadge(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tint credits) {\n\tconst auto widget = Ui::CreateChild<Ui::RpWidget>(parent);\n\tconst auto &font = st::chatGiveawayBadgeFont;\n\tconst auto text = QString::number(credits);\n\tconst auto iconHeight = font->ascent - font->descent;\n\tconst auto iconWidth = iconHeight + st::lineWidth;\n\tconst auto width = font->width(text) + iconWidth + st::lineWidth;\n\tconst auto inner = QRect(0, 0, width, font->height);\n\tconst auto rect = inner + st::subscriptionCreditsBadgePadding;\n\tconst auto size = rect.size();\n\tconst auto svg = widget->lifetime().make_state<QSvgRenderer>(\n\t\tUi::Premium::Svg());\n\tconst auto half = st::chatGiveawayBadgeStroke / 2.;\n\tconst auto left = st::subscriptionCreditsBadgePadding.left();\n\tconst auto smaller = QRectF(rect.translated(-rect.topLeft()))\n\t\t- Margins(half);\n\tconst auto radius = smaller.height() / 2.;\n\twidget->resize(size);\n\n\twidget->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(widget);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(QPen(st::premiumButtonFg, st::chatGiveawayBadgeStroke * 1.));\n\t\tp.setBrush(st::creditsBg3);\n\t\tp.drawRoundedRect(smaller, radius, radius);\n\n\t\tp.translate(0, font->descent / 2);\n\n\t\tp.setPen(st::premiumButtonFg);\n\t\tp.setBrush(st::premiumButtonFg);\n\t\tsvg->render(\n\t\t\t&p,\n\t\t\tQRect(\n\t\t\t\tleft,\n\t\t\t\thalf + (inner.height() - iconHeight) / 2,\n\t\t\t\ticonHeight,\n\t\t\t\ticonHeight));\n\n\t\tp.setFont(font);\n\t\tp.drawText(\n\t\t\tleft + iconWidth,\n\t\t\tst::subscriptionCreditsBadgePadding.top() + font->ascent,\n\t\t\ttext);\n\n\t}, widget->lifetime());\n\n\treturn widget;\n}\n\n} // namespace\n\nvoid SendCreditsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<Payments::CreditsFormData> form,\n\t\tFn<void(Settings::SmallBalanceResult)> sent) {\n\tif (!form) {\n\t\treturn;\n\t}\n\tstruct State {\n\t\trpl::variable<bool> confirmButtonBusy = false;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tconst auto &stBox = st::giveawayGiftCodeBox;\n\tbox->setStyle(stBox);\n\tbox->setNoContentMargin(true);\n\n\tconst auto session = form->invoice.session;\n\n\tconst auto photoSize = st::defaultUserpicButton.photoSize;\n\n\tconst auto content = box->verticalLayout();\n\tUi::AddSkip(content, photoSize / 2);\n\n\t{\n\t\tconst auto ministarsContainer = Ui::CreateChild<Ui::RpWidget>(box);\n\t\tconst auto fullHeight = photoSize * 2;\n\t\tusing MiniStars = Ui::Premium::ColoredMiniStars;\n\t\tconst auto ministars = box->lifetime().make_state<MiniStars>(\n\t\t\tministarsContainer,\n\t\t\tfalse,\n\t\t\tUi::Premium::MiniStarsType::BiStars);\n\t\tministars->setColorOverride(Ui::Premium::CreditsIconGradientStops());\n\n\t\tministarsContainer->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = QPainter(ministarsContainer);\n\t\t\tministars->paint(p);\n\t\t}, ministarsContainer->lifetime());\n\n\t\tbox->widthValue(\n\t\t) | rpl::on_next([=](int width) {\n\t\t\tministarsContainer->resize(width, fullHeight);\n\t\t\tconst auto w = fullHeight / 3 * 2;\n\t\t\tministars->setCenter(QRect(\n\t\t\t\t(width - w) / 2,\n\t\t\t\t(fullHeight - w) / 2,\n\t\t\t\tw,\n\t\t\t\tw));\n\t\t}, ministarsContainer->lifetime());\n\t}\n\n\tconst auto thumb = box->addRow(\n\t\tSendCreditsThumbnail(content, session, form.get(), photoSize),\n\t\tstyle::al_top);\n\tthumb->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tif (form->invoice.subscriptionPeriod) {\n\t\tconst auto badge = SendCreditsBadge(content, form->invoice.amount);\n\t\tthumb->geometryValue() | rpl::on_next([=](const QRect &r) {\n\t\t\tbadge->moveToLeft(\n\t\t\t\tr.x() + (r.width() - badge->width()) / 2,\n\t\t\t\trect::bottom(r) - badge->height() / 2);\n\t\t}, badge->lifetime());\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\t}\n\n\tUi::AddSkip(content);\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\tform->invoice.subscriptionPeriod\n\t\t\t\t? rpl::single(form->title)\n\t\t\t\t: tr::lng_credits_box_out_title(),\n\t\t\tst::settingsPremiumUserTitle),\n\t\tstyle::al_top);\n\tif (form->invoice.subscriptionPeriod && form->botId && form->photo) {\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\t\tconst auto bot = session->data().user(form->botId);\n\t\tbox->addRow(\n\t\t\tUi::CreatePeerBubble(box, bot),\n\t\t\tstyle::al_top);\n\t\tUi::AddSkip(content);\n\t}\n\tUi::AddSkip(content);\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\tSendCreditsConfirmText(session, form.get()),\n\t\t\tst::creditsBoxAbout),\n\t\tstyle::al_top);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\n\tconst auto sendStars = [=] {\n\t\tif (state->confirmButtonBusy.current()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto show = box->uiShow();\n\t\tconst auto weak = base::make_weak(box.get());\n\t\tstate->confirmButtonBusy = true;\n\t\tsession->api().request(\n\t\t\tMTPpayments_SendStarsForm(\n\t\t\t\tMTP_long(form->formId),\n\t\t\t\tform->inputInvoice)\n\t\t).done([=](const MTPpayments_PaymentResult &result) {\n\t\t\tresult.match([&](const MTPDpayments_paymentResult &data) {\n\t\t\t\tsession->api().applyUpdates(data.vupdates());\n\t\t\t}, [](const MTPDpayments_paymentVerificationNeeded &data) {\n\t\t\t});\n\t\t\tif (weak) {\n\t\t\t\tstate->confirmButtonBusy = false;\n\t\t\t\tbox->closeBox();\n\t\t\t}\n\t\t\tsent(Settings::SmallBalanceResult::Success);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tif (weak) {\n\t\t\t\tstate->confirmButtonBusy = false;\n\t\t\t}\n\t\t\tconst auto id = error.type();\n\t\t\tif (id == u\"BOT_PRECHECKOUT_FAILED\"_q) {\n\t\t\t\tauto error = ::Ui::MakeInformBox(\n\t\t\t\t\ttr::lng_payments_precheckout_stars_failed(tr::now));\n\t\t\t\terror->boxClosing() | rpl::on_next([=] {\n\t\t\t\t\tif (const auto paybox = weak.get()) {\n\t\t\t\t\t\tpaybox->closeBox();\n\t\t\t\t\t}\n\t\t\t\t}, error->lifetime());\n\t\t\t\tshow->showBox(std::move(error));\n\t\t\t} else if (id == u\"BOT_PRECHECKOUT_TIMEOUT\"_q) {\n\t\t\t\tshow->showToast(\n\t\t\t\t\ttr::lng_payments_precheckout_stars_timeout(tr::now));\n\t\t\t} else {\n\t\t\t\tshow->showToast(id);\n\t\t\t}\n\t\t}).send();\n\t};\n\n\tconst auto button = box->addButton(rpl::single(QString()), [=] {\n\t\tSettings::MaybeRequestBalanceIncrease(\n\t\t\tMain::MakeSessionShow(box->uiShow(), session),\n\t\t\tform->invoice.credits,\n\t\t\tSmallBalanceSourceFromForm(form),\n\t\t\t[=](Settings::SmallBalanceResult result) {\n\t\t\t\tif (result == Settings::SmallBalanceResult::Cancelled) {\n\t\t\t\t} else if (result == Settings::SmallBalanceResult::Success\n\t\t\t\t\t|| result == Settings::SmallBalanceResult::Already) {\n\t\t\t\t\tsendStars();\n\t\t\t\t} else {\n\t\t\t\t\tsent(result);\n\t\t\t\t}\n\t\t\t});\n\t});\n\tif (form->invoice.subscriptionPeriod) {\n\t\tAddTerms(box, button, stBox);\n\t}\n\t{\n\t\tusing namespace Info::Statistics;\n\t\tconst auto loadingAnimation = InfiniteRadialAnimationWidget(\n\t\t\tbutton,\n\t\t\tst::giveawayGiftCodeStartButton.height / 2);\n\t\tAddChildToWidgetCenter(button.data(), loadingAnimation);\n\t\tloadingAnimation->showOn(state->confirmButtonBusy.value());\n\t}\n\tSetButtonMarkedLabel(\n\t\tbutton,\n\t\trpl::combine(\n\t\t\t(form->invoice.subscriptionPeriod\n\t\t\t\t? tr::lng_credits_box_out_subscription_confirm\n\t\t\t\t: tr::lng_credits_box_out_confirm)(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(form->invoice.amount) | tr::to_count(),\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\trpl::single(CreditsEmojiSmall()),\n\t\t\t\t\ttr::rich),\n\t\t\tstate->confirmButtonBusy.value()\n\t\t) | rpl::map([](TextWithEntities &&text, bool busy) {\n\t\t\treturn busy ? TextWithEntities() : std::move(text);\n\t\t}),\n\t\tsession,\n\t\tst::creditsBoxButtonLabel,\n\t\t&box->getDelegate()->style().button.textFg);\n\n\t{\n\t\tconst auto close = Ui::CreateChild<Ui::IconButton>(\n\t\t\tcontent,\n\t\t\tst::boxTitleClose);\n\t\tclose->setClickedCallback([=] { box->closeBox(); });\n\t\tcontent->widthValue() | rpl::on_next([=](int) {\n\t\t\tclose->moveToRight(0, 0);\n\t\t}, close->lifetime());\n\t}\n\n\t{\n\t\tsession->credits().load(true);\n\t\tconst auto balance = Settings::AddBalanceWidget(\n\t\t\tcontent,\n\t\t\tsession,\n\t\t\tsession->credits().balanceValue(),\n\t\t\tfalse);\n\t\trpl::combine(\n\t\t\tbalance->sizeValue(),\n\t\t\tcontent->sizeValue()\n\t\t) | rpl::on_next([=](const QSize &, const QSize &) {\n\t\t\tbalance->moveToLeft(\n\t\t\t\tst::creditsHistoryRightSkip * 2,\n\t\t\t\tst::creditsHistoryRightSkip);\n\t\t\tbalance->update();\n\t\t}, balance->lifetime());\n\t}\n}\n\nTextWithEntities CreditsEmoji() {\n\treturn Ui::Text::IconEmoji(\n\t\t&st::starIconEmojiLarge,\n\t\tQString(QChar(0x2B50)));\n}\n\nTextWithEntities CreditsEmojiSmall() {\n\treturn Ui::Text::IconEmoji(\n\t\t&st::starIconEmoji,\n\t\tQString(QChar(0x2B50)));\n}\n\nnot_null<FlatLabel*> SetButtonMarkedLabel(\n\t\tnot_null<RpWidget*> button,\n\t\trpl::producer<TextWithEntities> text,\n\t\tText::MarkedContext context,\n\t\tconst style::FlatLabel &st,\n\t\tconst style::color *textFg) {\n\tconst auto buttonLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton,\n\t\trpl::single(QString()),\n\t\tst);\n\tcontext.repaint = [=] { buttonLabel->update(); };\n\trpl::duplicate(\n\t\ttext\n\t) | rpl::filter([=](const TextWithEntities &text) {\n\t\treturn !text.text.isEmpty();\n\t}) | rpl::on_next([=](const TextWithEntities &text) {\n\t\tbuttonLabel->setMarkedText(text, context);\n\t}, buttonLabel->lifetime());\n\tif (textFg) {\n\t\tbuttonLabel->setTextColorOverride((*textFg)->c);\n\t\tstyle::PaletteChanged() | rpl::on_next([=] {\n\t\t\tbuttonLabel->setTextColorOverride((*textFg)->c);\n\t\t}, buttonLabel->lifetime());\n\t}\n\tbutton->sizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\tbuttonLabel->moveToLeft(\n\t\t\t(size.width() - buttonLabel->width()) / 2,\n\t\t\t(size.height() - buttonLabel->height()) / 2);\n\t}, buttonLabel->lifetime());\n\tbuttonLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tbuttonLabel->showOn(std::move(\n\t\ttext\n\t) | rpl::map([=](const TextWithEntities &text) {\n\t\treturn !text.text.isEmpty();\n\t}));\n\treturn buttonLabel;\n}\n\nnot_null<FlatLabel*> SetButtonMarkedLabel(\n\t\tnot_null<RpWidget*> button,\n\t\trpl::producer<TextWithEntities> text,\n\t\tnot_null<Main::Session*> session,\n\t\tconst style::FlatLabel &st,\n\t\tconst style::color *textFg) {\n\treturn SetButtonMarkedLabel(button, text, Core::TextContext({\n\t\t.session = session,\n\t}), st, textFg);\n}\n\nvoid SendStarsForm(\n\t\tnot_null<Main::Session*> session,\n\t\tstd::shared_ptr<Payments::CreditsFormData> data,\n\t\tFn<void(std::optional<QString>)> done) {\n\tsession->api().request(MTPpayments_SendStarsForm(\n\t\tMTP_long(data->formId),\n\t\tdata->inputInvoice\n\t)).done([=](const MTPpayments_PaymentResult &result) {\n\t\tresult.match([&](const MTPDpayments_paymentResult &data) {\n\t\t\tsession->api().applyUpdates(data.vupdates());\n\t\t}, [](const MTPDpayments_paymentVerificationNeeded &data) {\n\t\t});\n\t\tdone(std::nullopt);\n\t}).fail([=](const MTP::Error &error) {\n\t\tdone(error.type());\n\t}).send();\n}\n\nSettings::SmallBalanceSource SmallBalanceSourceFromForm(\n\t\tstd::shared_ptr<Payments::CreditsFormData> form) {\n\tusing namespace Payments;\n\tusing namespace Settings;\n\tconst auto starGift = std::get_if<InvoiceStarGift>(&form->id.value);\n\treturn !starGift\n\t\t? SmallBalanceSource(SmallBalanceBot{ .botId = form->botId })\n\t\t: SmallBalanceSource(SmallBalanceStarGift{\n\t\t\t.recipientId = starGift->recipient->id,\n\t\t});\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/send_credits_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass HistoryItem;\n\nnamespace style {\nstruct FlatLabel;\n} // namespace style\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Payments {\nstruct CreditsFormData;\n} // namespace Payments\n\nnamespace Settings {\nenum class SmallBalanceResult;\nstruct SmallBalanceSource;\n} // namespace Settings\n\nnamespace Ui {\n\nclass RpWidget;\nclass GenericBox;\nclass FlatLabel;\n\nvoid SendCreditsBox(\n\tnot_null<Ui::GenericBox*> box,\n\tstd::shared_ptr<Payments::CreditsFormData> data,\n\tFn<void(Settings::SmallBalanceResult)> sent);\n\n[[nodiscard]] TextWithEntities CreditsEmoji();\n\n[[nodiscard]] TextWithEntities CreditsEmojiSmall();\n\nnot_null<FlatLabel*> SetButtonMarkedLabel(\n\tnot_null<RpWidget*> button,\n\trpl::producer<TextWithEntities> text,\n\tText::MarkedContext context,\n\tconst style::FlatLabel &st,\n\tconst style::color *textFg = nullptr);\n\nnot_null<FlatLabel*> SetButtonMarkedLabel(\n\tnot_null<RpWidget*> button,\n\trpl::producer<TextWithEntities> text,\n\tnot_null<Main::Session*> session,\n\tconst style::FlatLabel &st,\n\tconst style::color *textFg = nullptr);\n\nvoid SendStarsForm(\n\tnot_null<Main::Session*> session,\n\tstd::shared_ptr<Payments::CreditsFormData> data,\n\tFn<void(std::optional<QString>)> done);\n\n[[nodiscard]] Settings::SmallBalanceSource SmallBalanceSourceFromForm(\n\tstd::shared_ptr<Payments::CreditsFormData> form);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/send_files_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/send_files_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"storage/localimageloader.h\"\n#include \"storage/localstorage.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"iv/iv_instance.h\"\n#include \"mainwidget.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"menu/menu_checked_action.h\"\n#include \"menu/menu_send.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"chat_helpers/field_autocomplete.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"editor/photo_editor_layer_widget.h\"\n#include \"history/history_drag_area.h\"\n#include \"history/view/controls/history_view_characters_limit.h\"\n#include \"history/view/controls/history_view_compose_ai_button.h\"\n#include \"history/view/history_view_schedule_box.h\"\n#include \"core/mime_type.h\"\n#include \"core/ui_integration.h\"\n#include \"base/event_filter.h\"\n#include \"base/call_delayed.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"boxes/send_gif_with_caption_box.h\"\n#include \"boxes/send_credits_box.h\"\n#include \"ui/effects/scroll_content_shadow.h\"\n#include \"ui/widgets/fields/number_input.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/chat/attach/attach_album_preview.h\"\n#include \"ui/chat/attach/attach_single_file_preview.h\"\n#include \"ui/chat/attach/attach_single_media_preview.h\"\n#include \"ui/grouped_layout.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/controls/compose_ai_button_factory.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/ui_utility.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_user.h\"\n#include \"data/data_peer_values.h\" // Data::AmPremiumValue.\n#include \"data/data_premium_limits.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtCore/QMimeData>\n\nnamespace {\n\nconstexpr auto kMaxMessageLength = 4096;\nconstexpr auto kMaxDisplayNameLength = 64;\n\nusing Ui::SendFilesWay;\n\n[[nodiscard]] inline bool CanAddUrls(const QList<QUrl> &urls) {\n\treturn !urls.isEmpty() && ranges::all_of(urls, &QUrl::isLocalFile);\n}\n\n[[nodiscard]] bool CanAddFiles(not_null<const QMimeData*> data) {\n\treturn data->hasImage() || CanAddUrls(Core::ReadMimeUrls(data));\n}\n\nvoid FileDialogCallback(\n\t\tFileDialog::OpenResult &&result,\n\t\tFn<bool(const Ui::PreparedList&)> checkResult,\n\t\tFn<void(Ui::PreparedList)> callback,\n\t\tbool premium,\n\t\tstd::shared_ptr<Ui::Show> show) {\n\tauto showError = [=](tr::phrase<> text) {\n\t\tshow->showToast(text(tr::now));\n\t};\n\n\tauto list = Storage::PreparedFileFromFilesDialog(\n\t\tstd::move(result),\n\t\tcheckResult,\n\t\tshowError,\n\t\tst::sendMediaPreviewSize,\n\t\tpremium);\n\n\tif (!list) {\n\t\treturn;\n\t}\n\n\tcallback(std::move(*list));\n}\n\nvoid RenameFileBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst QString &currentName,\n\t\tFn<void(QString)> apply) {\n\tbox->setTitle(tr::lng_rename_file());\n\tconst auto field = box->addRow(object_ptr<Ui::InputField>(\n\t\tbox,\n\t\tst::settingsDeviceName,\n\t\trpl::single(QString()),\n\t\tcurrentName));\n\tconst auto extension = [&] {\n\t\tif (currentName.isEmpty()) {\n\t\t\treturn u\".png\"_q;\n\t\t}\n\t\tconst auto dot = currentName.lastIndexOf('.');\n\t\treturn (dot >= 0) ? currentName.mid(dot) : QString();\n\t}();\n\tconst auto nameWithoutExt = extension.isEmpty()\n\t\t? currentName\n\t\t: currentName.left(currentName.size() - extension.size());\n\tconst auto maxNameLength = kMaxDisplayNameLength - extension.size();\n\tfield->setMaxLength((maxNameLength > 0) ? maxNameLength : 0);\n\tfield->setText(nameWithoutExt);\n\tfield->selectAll();\n\tbox->setFocusCallback([=] {\n\t\tfield->setFocusFast();\n\t});\n\tconst auto save = [=] {\n\t\tconst auto newName = field->getLastText().trimmed();\n\t\tif (newName.isEmpty()) {\n\t\t\tfield->showError();\n\t\t\treturn;\n\t\t}\n\t\tif ((newName.size() + extension.size()) > kMaxDisplayNameLength) {\n\t\t\tfield->showError();\n\t\t\treturn;\n\t\t}\n\t\tconst auto weak = base::make_weak(box);\n\t\tapply(newName + extension);\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t};\n\tfield->submits() | rpl::on_next([=] {\n\t\tsave();\n\t}, box->lifetime());\n\tbox->addButton(tr::lng_settings_save(), save);\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nvoid EditFileCaptionBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst style::ComposeControls &st,\n\t\tPeerData *captionToPeer,\n\t\tTextWithTags currentCaption,\n\t\tFn<bool(TextWithTags)> apply) {\n\tbox->setTitle(tr::lng_context_upload_edit_caption());\n\tconst auto wrap = box->addRow(\n\t\tobject_ptr<Ui::RpWidget>(box),\n\t\tst::boxRowPadding);\n\tconst auto field = Ui::CreateChild<Ui::InputField>(\n\t\twrap,\n\t\tst.files.caption,\n\t\tUi::InputField::Mode::MultiLine,\n\t\ttr::lng_photo_caption());\n\tfield->setMaxLength(kMaxMessageLength);\n\tfield->setSubmitSettings(Core::App().settings().sendSubmitWay());\n\tUi::ResizeFitChild(wrap, field);\n\tif (const auto window = Core::App().findWindow(box)) {\n\t\tconst auto controller = window->sessionController();\n\t\tconst auto allow = [=](not_null<DocumentData*> emoji) {\n\t\t\treturn captionToPeer\n\t\t\t\t&& Data::AllowEmojiWithoutPremium(captionToPeer, emoji);\n\t\t};\n\t\tUi::SetupCaptionFieldInBox(\n\t\t\tbox,\n\t\t\tcontroller,\n\t\t\tfield,\n\t\t\tcaptionToPeer,\n\t\t\tallow,\n\t\t\tPremiumFeature::EmojiStatus);\n\t\tif (controller) {\n\t\t\tconst auto chatStyle = InitMessageFieldHandlers({\n\t\t\t\t.session = &controller->session(),\n\t\t\t\t.show = controller->uiShow(),\n\t\t\t\t.field = field,\n\t\t\t\t.customEmojiPaused = [=] {\n\t\t\t\t\treturn controller->isGifPausedAtLeastFor(\n\t\t\t\t\t\tWindow::GifPauseReason::Layer);\n\t\t\t\t},\n\t\t\t\t.allowPremiumEmoji = allow,\n\t\t\t\t.fieldStyle = &st.files.caption,\n\t\t\t});\n\t\t\tconst auto aiButton = Ui::SetupCaptionAiButton({\n\t\t\t\t.parent = field->parentWidget(),\n\t\t\t\t.field = field,\n\t\t\t\t.session = &controller->session(),\n\t\t\t\t.show = controller->uiShow(),\n\t\t\t\t.chatStyle = chatStyle,\n\t\t\t});\n\t\t\trpl::combine(\n\t\t\t\tbox->sizeValue(),\n\t\t\t\tfield->geometryValue()\n\t\t\t) | rpl::on_next([=](QSize, QRect) {\n\t\t\t\tUi::UpdateCaptionAiButtonGeometry(aiButton, field);\n\t\t\t\taiButton->raise();\n\t\t\t}, aiButton->lifetime());\n\t\t}\n\t}\n\tfield->setTextWithTags(std::move(currentCaption));\n\n\tbox->setFocusCallback([=] {\n\t\tfield->setFocusFast();\n\t});\n\tconst auto save = [=] {\n\t\tconst auto text = field->getTextWithAppliedMarkdown();\n\t\tif (!apply(text)) {\n\t\t\treturn;\n\t\t}\n\t\tbox->closeBox();\n\t};\n\tfield->submits() | rpl::on_next([=] {\n\t\tsave();\n\t}, box->lifetime());\n\tbox->addButton(tr::lng_settings_save(), save);\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nvoid EditPriceBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tuint64 price,\n\t\tFn<void(uint64)> apply) {\n\tbox->setTitle(tr::lng_paid_title());\n\tAddSubsectionTitle(\n\t\tbox->verticalLayout(),\n\t\ttr::lng_paid_enter_cost(),\n\t\t(st::boxRowPadding - QMargins(\n\t\t\tst::defaultSubsectionTitlePadding.left(),\n\t\t\t0,\n\t\t\tst::defaultSubsectionTitlePadding.right(),\n\t\t\t0)));\n\tconst auto limit = session->appConfig().get<int>(\n\t\tu\"stars_paid_post_amount_max\"_q,\n\t\t10'000);\n\tconst auto wrap = box->addRow(object_ptr<Ui::FixedHeightWidget>(\n\t\tbox,\n\t\tst::editTagField.heightMin));\n\tauto owned = object_ptr<Ui::NumberInput>(\n\t\twrap,\n\t\tst::editTagField,\n\t\ttr::lng_paid_cost_placeholder(),\n\t\tprice ? QString::number(price) : QString(),\n\t\tlimit);\n\tconst auto field = owned.data();\n\twrap->widthValue() | rpl::on_next([=](int width) {\n\t\tfield->move(0, 0);\n\t\tfield->resize(width, field->height());\n\t\twrap->resize(width, field->height());\n\t}, wrap->lifetime());\n\tfield->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(field);\n\t\tst::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width());\n\t}, field->lifetime());\n\tfield->selectAll();\n\tbox->setFocusCallback([=] {\n\t\tfield->setFocusFast();\n\t});\n\tconst auto about = box->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_paid_about(\n\t\t\t\tlt_link,\n\t\t\t\ttr::lng_paid_about_link(tr::link),\n\t\t\t\ttr::marked),\n\t\t\tst::paidAmountAbout),\n\t\tst::boxRowPadding + QMargins(0, st::sendMediaRowSkip, 0, 0));\n\tabout->setClickHandlerFilter([=](const auto &...) {\n\t\tCore::App().iv().openWithIvPreferred(\n\t\t\tsession,\n\t\t\ttr::lng_paid_about_link_url(tr::now));\n\t\treturn false;\n\t});\n\n\tconst auto save = [=] {\n\t\tconst auto now = field->getLastText().toULongLong();\n\t\tif (now > limit) {\n\t\t\tfield->showError();\n\t\t\treturn;\n\t\t}\n\t\tconst auto weak = base::make_weak(box);\n\t\tapply(now);\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t};\n\n\tQObject::connect(field, &Ui::NumberInput::submitted, box, save);\n\n\tbox->addButton(tr::lng_settings_save(), save);\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\n[[nodiscard]] bool SkipCaption(\n\t\tconst Ui::PreparedFile &file,\n\t\tconst Ui::SendFilesWay &way) {\n\treturn way.sendImagesAsPhotos()\n\t\t? (file.type == Ui::PreparedFile::Type::Photo\n\t\t\t|| file.type == Ui::PreparedFile::Type::Video)\n\t\t: file.isSticker();\n}\n\n} // namespace\n\nSendFilesLimits DefaultLimitsForPeer(not_null<PeerData*> peer) {\n\tusing Flag = SendFilesAllow;\n\tusing Restriction = ChatRestriction;\n\tconst auto allowByRestriction = [&](Restriction check, Flag allow) {\n\t\treturn Data::RestrictionError(peer, check) ? Flag() : allow;\n\t};\n\treturn Flag()\n\t\t| (peer->slowmodeApplied() ? Flag::OnlyOne : Flag())\n\t\t| (Data::AllowEmojiWithoutPremium(peer)\n\t\t\t? Flag::EmojiWithoutPremium\n\t\t\t: Flag())\n\t\t| allowByRestriction(Restriction::SendPhotos, Flag::Photos)\n\t\t| allowByRestriction(Restriction::SendVideos, Flag::Videos)\n\t\t| allowByRestriction(Restriction::SendMusic, Flag::Music)\n\t\t| allowByRestriction(Restriction::SendFiles, Flag::Files)\n\t\t| allowByRestriction(Restriction::SendStickers, Flag::Stickers)\n\t\t| allowByRestriction(Restriction::SendGifs, Flag::Gifs)\n\t\t| allowByRestriction(Restriction::SendOther, Flag::Texts);\n}\n\nSendFilesCheck DefaultCheckForPeer(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer) {\n\treturn DefaultCheckForPeer(controller->uiShow(), peer);\n}\n\nSendFilesCheck DefaultCheckForPeer(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer) {\n\treturn [=](\n\t\t\tconst Ui::PreparedFile &file,\n\t\t\tbool compress,\n\t\t\tbool silent) {\n\t\tconst auto error = Data::FileRestrictionError(peer, file, compress);\n\t\tif (error && !silent) {\n\t\t\tData::ShowSendErrorToast(show, peer, error);\n\t\t}\n\t\treturn !error.has_value();\n\t};\n}\n\nSendFilesBox::Block::Block(\n\tnot_null<QWidget*> parent,\n\tconst style::ComposeControls &st,\n\tnot_null<std::vector<Ui::PreparedFile>*> items,\n\tint from,\n\tint till,\n\tconst Ui::Text::MarkedContext &captionContext,\n\tFn<bool()> gifPaused,\n\tSendFilesWay way)\n: _items(items)\n, _from(from)\n, _till(till) {\n\tExpects(from >= 0);\n\tExpects(till > from);\n\tExpects(till <= items->size());\n\n\tconst auto count = till - from;\n\tconst auto my = gsl::make_span(*items).subspan(from, count);\n\tconst auto &first = my.front();\n\t_isAlbum = (my.size() > 1);\n\tif (_isAlbum) {\n\t\tconst auto preview = Ui::CreateChild<Ui::AlbumPreview>(\n\t\t\tparent.get(),\n\t\t\tst,\n\t\t\tmy,\n\t\t\tcaptionContext,\n\t\t\tway);\n\t\t_preview.reset(preview);\n\t} else {\n\t\tconst auto media = way.sendImagesAsPhotos()\n\t\t\t? Ui::SingleMediaPreview::Create(\n\t\t\t\tparent,\n\t\t\t\tst,\n\t\t\t\tgifPaused,\n\t\t\t\tfirst)\n\t\t\t: nullptr;\n\t\tif (media) {\n\t\t\t_isSingleMedia = true;\n\t\t\tmedia->setSendWay(way);\n\t\t\tmedia->setCanShowHighQualityBadge(first.canUseHighQualityPhoto());\n\t\t\t_preview.reset(media);\n\t\t} else {\n\t\t\t_preview.reset(Ui::CreateChild<Ui::SingleFilePreview>(\n\t\t\t\tparent.get(),\n\t\t\t\tst,\n\t\t\t\tfirst,\n\t\t\t\tcaptionContext));\n\t\t}\n\t}\n\t_preview->show();\n}\n\nint SendFilesBox::Block::fromIndex() const {\n\treturn _from;\n}\n\nint SendFilesBox::Block::tillIndex() const {\n\treturn _till;\n}\n\nobject_ptr<Ui::RpWidget> SendFilesBox::Block::takeWidget() {\n\treturn object_ptr<Ui::RpWidget>::fromRaw(_preview.get());\n}\n\nrpl::producer<int> SendFilesBox::Block::itemDeleteRequest() const {\n\tusing namespace rpl::mappers;\n\n\tconst auto preview = _preview.get();\n\tconst auto from = _from;\n\tif (_isAlbum) {\n\t\tconst auto album = static_cast<Ui::AlbumPreview*>(_preview.get());\n\t\treturn album->thumbDeleted() | rpl::map(_1 + from);\n\t} else if (_isSingleMedia) {\n\t\tconst auto media = static_cast<Ui::SingleMediaPreview*>(preview);\n\t\treturn media->deleteRequests() | rpl::map([from] { return from; });\n\t} else {\n\t\tconst auto single = static_cast<Ui::SingleFilePreview*>(preview);\n\t\treturn single->deleteRequests() | rpl::map([from] { return from; });\n\t}\n}\n\nrpl::producer<int> SendFilesBox::Block::itemReplaceRequest() const {\n\tusing namespace rpl::mappers;\n\n\tconst auto preview = _preview.get();\n\tconst auto from = _from;\n\tif (_isAlbum) {\n\t\tconst auto album = static_cast<Ui::AlbumPreview*>(preview);\n\t\treturn album->thumbChanged() | rpl::map(_1 + from);\n\t} else if (_isSingleMedia) {\n\t\tconst auto media = static_cast<Ui::SingleMediaPreview*>(preview);\n\t\treturn media->editRequests() | rpl::map([from] { return from; });\n\t} else {\n\t\tconst auto single = static_cast<Ui::SingleFilePreview*>(preview);\n\t\treturn single->editRequests() | rpl::map([from] { return from; });\n\t}\n}\n\nrpl::producer<int> SendFilesBox::Block::itemModifyRequest() const {\n\tusing namespace rpl::mappers;\n\n\tconst auto preview = _preview.get();\n\tconst auto from = _from;\n\tif (_isAlbum) {\n\t\tconst auto album = static_cast<Ui::AlbumPreview*>(preview);\n\t\treturn album->thumbModified() | rpl::map(_1 + from);\n\t} else if (_isSingleMedia) {\n\t\tconst auto media = static_cast<Ui::SingleMediaPreview*>(preview);\n\t\treturn media->modifyRequests() | rpl::map_to(from);\n\t} else {\n\t\treturn rpl::never<int>();\n\t}\n}\n\nrpl::producer<> SendFilesBox::Block::orderUpdated() const {\n\tif (_isAlbum) {\n\t\tconst auto album = static_cast<Ui::AlbumPreview*>(_preview.get());\n\t\treturn album->orderUpdated();\n\t}\n\treturn rpl::never<>();\n}\n\nvoid SendFilesBox::Block::setSendWay(Ui::SendFilesWay way) {\n\tif (!_isAlbum) {\n\t\tif (_isSingleMedia) {\n\t\t\tconst auto media = static_cast<Ui::SingleMediaPreview*>(\n\t\t\t\t_preview.get());\n\t\t\tmedia->setSendWay(way);\n\t\t}\n\t\treturn;\n\t}\n\tapplyChanges();\n\tconst auto album = static_cast<Ui::AlbumPreview*>(_preview.get());\n\talbum->setSendWay(way);\n}\n\nvoid SendFilesBox::Block::toggleSpoilers(bool enabled) {\n\tif (_isAlbum) {\n\t\tconst auto album = static_cast<Ui::AlbumPreview*>(_preview.get());\n\t\talbum->toggleSpoilers(enabled);\n\t} else if (_isSingleMedia) {\n\t\tconst auto media = static_cast<Ui::SingleMediaPreview*>(\n\t\t\t_preview.get());\n\t\tmedia->setSpoiler(enabled);\n\t}\n}\n\nvoid SendFilesBox::Block::applyChanges() {\n\tif (!_isAlbum) {\n\t\tif (_isSingleMedia) {\n\t\t\tconst auto media = static_cast<Ui::SingleMediaPreview*>(\n\t\t\t\t_preview.get());\n\t\t\tif (media->canHaveSpoiler()) {\n\t\t\t\t(*_items)[_from].spoiler = media->hasSpoiler();\n\t\t\t}\n\t\t}\n\t\treturn;\n\t}\n\tconst auto album = static_cast<Ui::AlbumPreview*>(_preview.get());\n\tconst auto order = album->takeOrder();\n\tconst auto guard = gsl::finally([&] {\n\t\tconst auto spoilered = album->collectSpoileredIndices();\n\t\tfor (auto i = 0, count = int(order.size()); i != count; ++i) {\n\t\t\tif (album->canHaveSpoiler(i)) {\n\t\t\t\t(*_items)[_from + i].spoiler = spoilered.contains(i);\n\t\t\t}\n\t\t}\n\t});\n\tconst auto isIdentity = [&] {\n\t\tfor (auto i = 0, count = int(order.size()); i != count; ++i) {\n\t\t\tif (order[i] != i) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}();\n\tif (isIdentity) {\n\t\treturn;\n\t}\n\n\tauto elements = std::vector<Ui::PreparedFile>();\n\telements.reserve(order.size());\n\tfor (const auto index : order) {\n\t\telements.push_back(std::move((*_items)[_from + index]));\n\t}\n\tfor (auto i = 0, count = int(order.size()); i != count; ++i) {\n\t\t(*_items)[_from + i] = std::move(elements[i]);\n\t}\n}\n\nQImage SendFilesBox::Block::generatePriceTagBackground() const {\n\tconst auto preview = _preview.get();\n\tif (_isAlbum) {\n\t\tconst auto album = static_cast<Ui::AlbumPreview*>(preview);\n\t\treturn album->generatePriceTagBackground();\n\t} else if (_isSingleMedia) {\n\t\tconst auto media = static_cast<Ui::SingleMediaPreview*>(preview);\n\t\treturn media->generatePriceTagBackground();\n\t}\n\treturn QImage();\n}\n\nbool SendFilesBox::Block::setSingleFileDisplayName(\n\t\tconst QString &displayName) {\n\tif (_isAlbum || _isSingleMedia) {\n\t\treturn false;\n\t}\n\tconst auto single = static_cast<Ui::SingleFilePreview*>(_preview.get());\n\tsingle->setDisplayName(displayName);\n\treturn true;\n}\n\nbool SendFilesBox::Block::setSingleFileCaption(\n\t\tint index,\n\t\tconst TextWithTags &caption) {\n\tif (_isSingleMedia || index < _from || index >= _till) {\n\t\treturn false;\n\t}\n\tif (_isAlbum) {\n\t\tconst auto album = static_cast<Ui::AlbumPreview*>(_preview.get());\n\t\talbum->setCaption(index - _from, caption);\n\t\treturn true;\n\t}\n\tconst auto single = static_cast<Ui::SingleFilePreview*>(_preview.get());\n\tsingle->setCaption(caption);\n\treturn true;\n}\n\nSendFilesBox::SendFilesBox(\n\tQWidget*,\n\tnot_null<Window::SessionController*> controller,\n\tUi::PreparedList &&list,\n\tconst TextWithTags &caption,\n\tnot_null<PeerData*> toPeer,\n\tApi::SendType sendType,\n\tSendMenu::Details sendMenuDetails)\n: SendFilesBox(nullptr, {\n\t.show = controller->uiShow(),\n\t.list = std::move(list),\n\t.caption = caption,\n\t.toPeer = toPeer,\n\t.limits = DefaultLimitsForPeer(toPeer),\n\t.check = DefaultCheckForPeer(controller, toPeer),\n\t.sendType = sendType,\n\t.sendMenuDetails = [=] { return sendMenuDetails; },\n}) {\n}\n\nSendFilesBox::SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor)\n: _show(std::move(descriptor.show))\n, _st(descriptor.stOverride\n\t? *descriptor.stOverride\n\t: st::defaultComposeControls)\n, _sendType(descriptor.sendType)\n, _titleHeight(st::boxTitleHeight)\n, _list(std::move(descriptor.list))\n, _limits(descriptor.limits)\n, _sendMenuDetails(prepareSendMenuDetails(descriptor))\n, _sendMenuCallback(prepareSendMenuCallback())\n, _toPeer(descriptor.toPeer)\n, _check(std::move(descriptor.check))\n, _confirmedCallback(std::move(descriptor.confirmed))\n, _cancelledCallback(std::move(descriptor.cancelled))\n, _caption(\n\tthis,\n\t_st.files.caption,\n\tUi::InputField::Mode::MultiLine,\n\ttr::lng_photo_caption())\n, _prefilledCaptionText(std::move(descriptor.caption))\n, _scroll(this, st::boxScroll)\n, _inner(\n\t_scroll->setOwnedWidget(\n\t\tobject_ptr<Ui::VerticalLayout>(_scroll.data()))) {\n\tenqueueNextPrepare();\n}\n\nFn<SendMenu::Details()> SendFilesBox::prepareSendMenuDetails(\n\t\tconst SendFilesBoxDescriptor &descriptor) {\n\tauto initial = descriptor.sendMenuDetails;\n\treturn crl::guard(this, [=] {\n\t\tauto result = initial ? initial() : SendMenu::Details();\n\t\tresult.spoiler = !hasSpoilerMenu()\n\t\t\t? SendMenu::SpoilerState::None\n\t\t\t: allWithSpoilers()\n\t\t\t? SendMenu::SpoilerState::Enabled\n\t\t\t: SendMenu::SpoilerState::Possible;\n\t\tconst auto way = _sendWay.current();\n\t\tconst auto canMoveCaption = _list.canMoveCaption(\n\t\t\tway.groupFiles() && way.sendImagesAsPhotos(),\n\t\t\tway.sendImagesAsPhotos()\n\t\t) && HasSendText(_caption);\n\t\tresult.caption = !canMoveCaption\n\t\t\t? SendMenu::CaptionState::None\n\t\t\t: _invertCaption\n\t\t\t? SendMenu::CaptionState::Above\n\t\t\t: SendMenu::CaptionState::Below;\n\t\tresult.photoQuality = !hasSendLargePhotosOption()\n\t\t\t? SendMenu::PhotoQualityState::None\n\t\t\t: way.sendLargePhotos()\n\t\t\t? SendMenu::PhotoQualityState::High\n\t\t\t: SendMenu::PhotoQualityState::Standard;\n\t\tresult.price = canChangePrice()\n\t\t\t? _price.current()\n\t\t\t: std::optional<uint64>();\n\t\treturn result;\n\t});\n}\n\nauto SendFilesBox::prepareSendMenuCallback()\n-> Fn<void(MenuAction, MenuDetails)> {\n\treturn crl::guard(this, [=](MenuAction action, MenuDetails details) {\n\t\tusing Type = SendMenu::ActionType;\n\t\tswitch (action.type) {\n\t\tcase Type::CaptionDown: _invertCaption = false; break;\n\t\tcase Type::CaptionUp: _invertCaption = true; break;\n\t\tcase Type::PhotoQualityOn: setSendLargePhotos(true); break;\n\t\tcase Type::PhotoQualityOff: setSendLargePhotos(false); break;\n\t\tcase Type::SpoilerOn: toggleSpoilers(true); break;\n\t\tcase Type::SpoilerOff: toggleSpoilers(false); break;\n\t\tcase Type::ChangePrice: changePrice(); break;\n\t\tdefault:\n\t\t\tSendMenu::DefaultCallback(\n\t\t\t\t_show,\n\t\t\t\tsendCallback())(\n\t\t\t\t\taction,\n\t\t\t\t\tdetails);\n\t\t\tbreak;\n\t\t}\n\t});\n}\n\nvoid SendFilesBox::initPreview() {\n\tusing namespace rpl::mappers;\n\n\trefreshControls(true);\n\n\tupdateBoxSize();\n\n\t_dimensionsLifetime.destroy();\n\t_inner->resizeToWidth(st::boxWideWidth);\n\n\trpl::combine(\n\t\t_inner->heightValue(),\n\t\t_footerHeight.value(),\n\t\t_titleHeight.value(),\n\t\t_1 + _2 + _3\n\t) | rpl::on_next([=](int height) {\n\t\tsetDimensions(\n\t\t\tst::boxWideWidth,\n\t\t\tstd::min(st::sendMediaPreviewHeightMax, height),\n\t\t\ttrue);\n\t}, _dimensionsLifetime);\n}\n\nvoid SendFilesBox::enqueueNextPrepare() {\n\tif (_preparing) {\n\t\treturn;\n\t}\n\twhile (!_list.filesToProcess.empty()\n\t\t&& _list.filesToProcess.front().information) {\n\t\tauto file = std::move(_list.filesToProcess.front());\n\t\t_list.filesToProcess.pop_front();\n\t\taddFile(std::move(file));\n\t}\n\tif (_list.filesToProcess.empty()) {\n\t\treturn;\n\t}\n\tauto file = std::move(_list.filesToProcess.front());\n\t_list.filesToProcess.pop_front();\n\tconst auto weak = base::make_weak(this);\n\t_preparing = true;\n\tconst auto sideLimit = PhotoSideLimit(_sendWay.current().sendLargePhotos());\n\tcrl::async([weak, sideLimit, file = std::move(file)]() mutable {\n\t\tStorage::PrepareDetails(file, st::sendMediaPreviewSize, sideLimit);\n\t\tcrl::on_main([weak, file = std::move(file)]() mutable {\n\t\t\tif (weak) {\n\t\t\t\tweak->addPreparedAsyncFile(std::move(file));\n\t\t\t}\n\t\t});\n\t});\n}\n\nvoid SendFilesBox::prepare() {\n\tinitSendWay();\n\tsetupCaption();\n\tsetupSendWayControls();\n\tpreparePreview();\n\tinitPreview();\n\tSetupShadowsToScrollContent(this, _scroll, _inner->heightValue());\n\tsetCloseByOutsideClick(false);\n\n\tboxClosing() | rpl::on_next([=] {\n\t\tif (!_confirmed && _cancelledCallback) {\n\t\t\t_cancelledCallback();\n\t\t}\n\t}, lifetime());\n\n\tsetupDragArea();\n}\n\nvoid SendFilesBox::setupDragArea() {\n\t// Avoid both drag areas appearing at one time.\n\tauto computeState = [=](const QMimeData *data) {\n\t\tusing DragState = Storage::MimeDataState;\n\t\tconst auto state = Storage::ComputeMimeDataState(data);\n\t\treturn (state == DragState::PhotoFiles\n\t\t\t|| state == DragState::Image\n\t\t\t|| state == DragState::MediaFiles)\n\t\t\t? (_sendWay.current().sendImagesAsPhotos()\n\t\t\t\t? DragState::Image\n\t\t\t\t: DragState::Files)\n\t\t\t: state;\n\t};\n\tconst auto areas = DragArea::SetupDragAreaToContainer(\n\t\tthis,\n\t\tCanAddFiles,\n\t\t[=](bool f) { _caption->setAcceptDrops(f); },\n\t\t[=] { updateControlsGeometry(); },\n\t\tstd::move(computeState));\n\n\tconst auto droppedCallback = [=](bool compress) {\n\t\treturn [=](const QMimeData *data) {\n\t\t\taddFiles(data);\n\t\t\t_show->activate();\n\t\t};\n\t};\n\tareas.document->setDroppedCallback(droppedCallback(false));\n\tareas.photo->setDroppedCallback(droppedCallback(true));\n}\n\nvoid SendFilesBox::refreshAllAfterChanges(int fromItem, Fn<void()> perform) {\n\tauto fromBlock = 0;\n\tfor (auto count = int(_blocks.size()); fromBlock != count; ++fromBlock) {\n\t\tif (_blocks[fromBlock].tillIndex() >= fromItem) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tfor (auto index = fromBlock; index < _blocks.size(); ++index) {\n\t\t_blocks[index].applyChanges();\n\t}\n\tif (perform) {\n\t\tperform();\n\t}\n\tgeneratePreviewFrom(fromBlock);\n\t{\n\t\tauto sendWay = _sendWay.current();\n\t\tsendWay.setHasCompressedStickers(_list.hasSticker());\n\t\tif (_limits & SendFilesAllow::OnlyOne) {\n\t\t\tif (_list.files.size() > 1) {\n\t\t\t\tsendWay.setGroupFiles(true);\n\t\t\t}\n\t\t}\n\t\t_sendWay = sendWay;\n\t}\n\t_inner->resizeToWidth(st::boxWideWidth);\n\trefreshControls();\n\tcaptionResized();\n}\n\nbool SendFilesBox::setDisplayNameInSingleFilePreview(\n\t\tint fileIndex,\n\t\tconst QString &displayName) {\n\tfor (auto &block : _blocks) {\n\t\tif (fileIndex < block.fromIndex() || fileIndex >= block.tillIndex()) {\n\t\t\tcontinue;\n\t\t}\n\t\treturn block.setSingleFileDisplayName(displayName);\n\t}\n\treturn false;\n}\n\nbool SendFilesBox::setCaptionInSingleFilePreview(\n\t\tint fileIndex,\n\t\tconst TextWithTags &caption) {\n\tfor (auto &block : _blocks) {\n\t\tif (fileIndex < block.fromIndex() || fileIndex >= block.tillIndex()) {\n\t\t\tcontinue;\n\t\t}\n\t\treturn block.setSingleFileCaption(fileIndex, caption);\n\t}\n\treturn false;\n}\n\nvoid SendFilesBox::openDialogToAddFileToAlbum() {\n\tconst auto show = uiShow();\n\tconst auto checkResult = [=](const Ui::PreparedList &list) {\n\t\tif (!(_limits & SendFilesAllow::OnlyOne)) {\n\t\t\treturn true;\n\t\t} else if (!_list.canBeSentInSlowmodeWith(list)) {\n\t\t\tshowToast(tr::lng_slowmode_no_many(tr::now));\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t};\n\tconst auto callback = [=](FileDialog::OpenResult &&result) {\n\t\tconst auto premium = _show->session().premium();\n\t\tFileDialogCallback(\n\t\t\tstd::move(result),\n\t\t\tcheckResult,\n\t\t\t[=](Ui::PreparedList list) { addFiles(std::move(list)); },\n\t\t\tpremium,\n\t\t\tshow);\n\t};\n\n\tFileDialog::GetOpenPaths(\n\t\tthis,\n\t\ttr::lng_choose_file(tr::now),\n\t\tFileDialog::AllOrImagesFilter(),\n\t\tcrl::guard(this, callback));\n}\n\nvoid SendFilesBox::refreshMessagesCount() {\n\t_messagesCount = _list.files.size();\n}\n\nvoid SendFilesBox::refreshButtons() {\n\tclearButtons();\n\n\t_send = addButton(\n\t\t(_sendType == Api::SendType::Normal\n\t\t\t? tr::lng_send_button()\n\t\t\t: tr::lng_create_group_next()),\n\t\t[=] { send({}); });\n\trefreshMessagesCount();\n\n\tconst auto perMessage = _toPeer->starsPerMessageChecked();\n\tif (perMessage > 0) {\n\t\t_send->setText(PaidSendButtonText(_messagesCount.value(\n\t\t) | rpl::map(rpl::mappers::_1 * perMessage)));\n\t}\n\tif (_sendType == Api::SendType::Normal) {\n\t\tSendMenu::SetupMenuAndShortcuts(\n\t\t\t_send,\n\t\t\t_show,\n\t\t\t_sendMenuDetails,\n\t\t\t_sendMenuCallback,\n\t\t\t&_st.tabbed.menu,\n\t\t\t&_st.tabbed.icons);\n\t}\n\taddButton(tr::lng_cancel(), [=] {\n\t\trequestToTakeTextWithTags();\n\t\tcloseBox();\n\t});\n\t_addFile = addLeftButton(\n\t\ttr::lng_stickers_featured_add(),\n\t\tbase::fn_delayed(st::historyAttach.ripple.hideDuration, this, [=] {\n\t\t\topenDialogToAddFileToAlbum();\n\t\t}));\n\n\taddMenuButton();\n}\n\nbool SendFilesBox::hasSendMenu(const MenuDetails &details) const {\n\treturn (details.type != SendMenu::Type::Disabled)\n\t\t|| (details.spoiler != SendMenu::SpoilerState::None)\n\t\t|| (details.caption != SendMenu::CaptionState::None)\n\t\t|| (details.photoQuality != SendMenu::PhotoQualityState::None)\n\t\t|| details.price.has_value();\n}\n\nbool SendFilesBox::hasSpoilerMenu() const {\n\treturn !hasPrice()\n\t\t&& _list.hasSpoilerMenu(_sendWay.current().sendImagesAsPhotos());\n}\n\nbool SendFilesBox::hasSendLargePhotosOption() const {\n\treturn _list.hasSendLargePhotosOption(\n\t\t_sendWay.current().sendImagesAsPhotos());\n}\n\nbool SendFilesBox::canChangePrice() const {\n\tconst auto way = _sendWay.current();\n\tconst auto broadcast = _toPeer->asBroadcast();\n\treturn broadcast\n\t\t&& broadcast->canPostPaidMedia()\n\t\t&& _list.canChangePrice(\n\t\t\tway.groupFiles() && way.sendImagesAsPhotos(),\n\t\t\tway.sendImagesAsPhotos());\n}\n\nvoid SendFilesBox::applyBlockChanges() {\n\tfor (auto &block : _blocks) {\n\t\tblock.applyChanges();\n\t}\n}\n\nbool SendFilesBox::allWithSpoilers() {\n\tapplyBlockChanges();\n\treturn ranges::all_of(_list.files, &Ui::PreparedFile::spoiler);\n}\n\nvoid SendFilesBox::toggleSpoilers(bool enabled) {\n\tfor (auto &file : _list.files) {\n\t\tfile.spoiler = enabled;\n\t}\n\tfor (auto &block : _blocks) {\n\t\tblock.toggleSpoilers(enabled);\n\t}\n}\n\nvoid SendFilesBox::setSendLargePhotos(bool enabled) {\n\tauto way = _sendWay.current();\n\tif (way.sendLargePhotos() == enabled) {\n\t\treturn;\n\t}\n\tway.setSendLargePhotos(enabled);\n\t_sendWay = way;\n}\n\nvoid SendFilesBox::changePrice() {\n\tconst auto weak = base::make_weak(this);\n\tconst auto session = &_show->session();\n\tconst auto now = _price.current();\n\t_show->show(Box(EditPriceBox, session, now, [=](uint64 price) {\n\t\tif (weak && price != now) {\n\t\t\t_price = price;\n\t\t\trefreshPriceTag();\n\t\t}\n\t}));\n}\n\nbool SendFilesBox::hasPrice() const {\n\treturn canChangePrice() && _price.current() > 0;\n}\n\nvoid SendFilesBox::refreshPriceTag() {\n\tconst auto resetSpoilers = hasPrice() || _priceTag;\n\tif (resetSpoilers) {\n\t\tfor (auto &file : _list.files) {\n\t\t\tfile.spoiler = false;\n\t\t}\n\t\tfor (auto &block : _blocks) {\n\t\t\tblock.toggleSpoilers(hasPrice());\n\t\t}\n\t}\n\tif (!hasPrice()) {\n\t\t_priceTag = nullptr;\n\t\t_priceTagBg = QImage();\n\t} else if (!_priceTag) {\n\t\t_priceTag = std::make_unique<Ui::RpWidget>(_inner.data());\n\t\tconst auto raw = _priceTag.get();\n\n\t\traw->show();\n\t\traw->paintRequest() | rpl::on_next([=] {\n\t\t\tif (_priceTagBg.isNull()) {\n\t\t\t\t_priceTagBg = preparePriceTagBg(raw->size());\n\t\t\t}\n\t\t\tQPainter(raw).drawImage(0, 0, _priceTagBg);\n\t\t}, raw->lifetime());\n\n\t\tauto price = _price.value() | rpl::map([=](uint64 amount) {\n\t\t\tauto result = Ui::Text::Colorized(Ui::CreditsEmoji());\n\t\t\tresult.append(Lang::FormatCountDecimal(amount));\n\t\t\treturn result;\n\t\t});\n\t\tauto text = tr::lng_paid_price(\n\t\t\tlt_price,\n\t\t\tstd::move(price),\n\t\t\ttr::marked);\n\t\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\traw,\n\t\t\tQString(),\n\t\t\tst::paidTagLabel);\n\t\tstd::move(\n\t\t\ttext\n\t\t) | rpl::on_next([=](const TextWithEntities &text) {\n\t\t\tlabel->setMarkedText(text);\n\t\t}, label->lifetime());\n\t\tlabel->show();\n\t\tlabel->sizeValue() | rpl::on_next([=](QSize size) {\n\t\t\tconst auto inner = QRect(QPoint(), size);\n\t\t\tconst auto rect = inner.marginsAdded(st::paidTagPadding);\n\t\t\traw->resize(rect.size());\n\t\t\tlabel->move(-rect.topLeft());\n\t\t}, label->lifetime());\n\t\t_inner->sizeValue() | rpl::on_next([=](QSize size) {\n\t\t\traw->move(\n\t\t\t\t(size.width() - raw->width()) / 2,\n\t\t\t\t(size.height() - raw->height()) / 2);\n\t\t}, raw->lifetime());\n\t} else {\n\t\t_priceTag->raise();\n\t\t_priceTag->update();\n\t\t_priceTagBg = QImage();\n\t}\n}\n\nQImage SendFilesBox::preparePriceTagBg(QSize size) const {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto outer = _blocks.empty()\n\t\t? size\n\t\t: _inner->widgetAt(0)->geometry().size();\n\tauto bg = _blocks.empty()\n\t\t? QImage()\n\t\t: _blocks.front().generatePriceTagBackground();\n\tif (bg.isNull()) {\n\t\tbg = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied);\n\t\tbg.fill(Qt::black);\n\t}\n\n\tauto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\tresult.fill(Qt::black);\n\tauto p = QPainter(&result);\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.drawImage(\n\t\tQRect(\n\t\t\t(size.width() - outer.width()) / 2,\n\t\t\t(size.height() - outer.height()) / 2,\n\t\t\touter.width(),\n\t\t\touter.height()),\n\t\tbg);\n\tp.fillRect(QRect(QPoint(), size), st::msgDateImgBg);\n\tp.end();\n\n\tconst auto radius = std::min(size.width(), size.height()) / 2;\n\treturn Images::Round(std::move(result), Images::CornersMask(radius));\n}\n\nvoid SendFilesBox::addMenuButton() {\n\tconst auto details = _sendMenuDetails();\n\tif (!hasSendMenu(details)) {\n\t\treturn;\n\t}\n\n\tconst auto top = addTopButton(_st.files.menu);\n\ttop->setClickedCallback([=] {\n\t\tconst auto &tabbed = _st.tabbed;\n\t\t_menu = base::make_unique_q<Ui::PopupMenu>(top, tabbed.menu);\n\t\t_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);\n\t\tconst auto position = QCursor::pos();\n\t\tSendMenu::FillSendMenu(\n\t\t\t_menu.get(),\n\t\t\t_show,\n\t\t\t_sendMenuDetails(),\n\t\t\t_sendMenuCallback,\n\t\t\t&_st.tabbed.icons,\n\t\t\tposition);\n\t\t_menu->popup(position);\n\t\treturn true;\n\t});\n}\n\nvoid SendFilesBox::initSendWay() {\n\t_sendWay = [&] {\n\t\tauto result = Core::App().settings().sendFilesWay();\n\t\tresult.setHasCompressedStickers(_list.hasSticker());\n\t\tif ((_limits & SendFilesAllow::OnlyOne)\n\t\t\t&& (_list.files.size() > 1)) {\n\t\t\tresult.setGroupFiles(true);\n\t\t}\n\t\tif (_list.overrideSendImagesAsPhotos == false) {\n\t\t\tif (!(_limits & SendFilesAllow::OnlyOne)\n\t\t\t\t|| !_list.hasSticker()) {\n\t\t\t\tresult.setSendImagesAsPhotos(false);\n\t\t\t}\n\t\t\treturn result;\n\t\t} else if (_list.overrideSendImagesAsPhotos == true) {\n\t\t\tresult.setSendImagesAsPhotos(true);\n\t\t\tconst auto silent = true;\n\t\t\tif (!checkWithWay(result, silent)) {\n\t\t\t\tresult.setSendImagesAsPhotos(false);\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\t\tconst auto silent = true;\n\t\tif (!checkWithWay(result, silent)) {\n\t\t\tresult.setSendImagesAsPhotos(!result.sendImagesAsPhotos());\n\t\t}\n\t\treturn result;\n\t}();\n\t_sendWay.changes(\n\t) | rpl::on_next([=](SendFilesWay value) {\n\t\tconst auto hidden = [&] {\n\t\t\treturn _caption->isHidden();\n\t\t};\n\t\tconst auto was = hidden();\n\t\tupdateCaptionVisibility();\n\t\tupdateEmojiPanelGeometry();\n\t\tapplyBlockChanges();\n\t\tgeneratePreviewFrom(0);\n\t\t_inner->resizeToWidth(st::boxWideWidth);\n\t\trefreshControls();\n\t\tcaptionResized();\n\t\tif (was != hidden()) {\n\t\t\tupdateBoxSize();\n\t\t\tupdateControlsGeometry();\n\t\t}\n\t\tsetInnerFocus();\n\t}, lifetime());\n}\n\nvoid SendFilesBox::updateCaptionVisibility() {\n\tconst auto way = _sendWay.current();\n\tconst auto can = _list.canAddCaption(way.sendImagesAsPhotos());\n\t_caption->setVisible(can);\n\tif (_emojiToggle) {\n\t\t_emojiToggle->setVisible(can);\n\t}\n\tif (_aiButton) {\n\t\t_aiButton->setVisible(can\n\t\t\t&& Ui::HasEnoughLinesForAi(&_show->session(), _caption.data()));\n\t}\n}\n\nvoid SendFilesBox::preparePreview() {\n\tgeneratePreviewFrom(0);\n}\n\nvoid SendFilesBox::generatePreviewFrom(int fromBlock) {\n\tExpects(fromBlock <= _blocks.size());\n\n\tusing Type = Ui::PreparedFile::Type;\n\n\t_blocks.erase(_blocks.begin() + fromBlock, _blocks.end());\n\n\tconst auto fromItem = _blocks.empty() ? 0 : _blocks.back().tillIndex();\n\tAssert(fromItem <= _list.files.size());\n\n\tauto albumStart = -1;\n\tfor (auto i = fromItem, till = int(_list.files.size()); i != till; ++i) {\n\t\tconst auto type = _list.files[i].type;\n\t\tif (albumStart >= 0) {\n\t\t\tconst auto albumCount = (i - albumStart);\n\t\t\tif ((type == Type::File)\n\t\t\t\t|| (type == Type::None)\n\t\t\t\t|| (type == Type::Music)\n\t\t\t\t|| (albumCount == Ui::MaxAlbumItems())) {\n\t\t\t\tpushBlock(std::exchange(albumStart, -1), i);\n\t\t\t} else {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (type != Type::File\n\t\t\t&& type != Type::Music\n\t\t\t&& type != Type::None) {\n\t\t\tif (albumStart < 0) {\n\t\t\t\talbumStart = i;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tpushBlock(i, i + 1);\n\t}\n\tif (albumStart >= 0) {\n\t\tpushBlock(albumStart, _list.files.size());\n\t}\n}\n\nvoid SendFilesBox::pushBlock(int from, int till) {\n\tconst auto gifPaused = [show = _show] {\n\t\treturn show->paused(Window::GifPauseReason::Layer);\n\t};\n\tconst auto captionContext = Core::TextContext({\n\t\t.session = &_show->session(),\n\t});\n\t_blocks.emplace_back(\n\t\t_inner.data(),\n\t\t_st,\n\t\t&_list.files,\n\t\tfrom,\n\t\ttill,\n\t\tcaptionContext,\n\t\tgifPaused,\n\t\t_sendWay.current());\n\tauto &block = _blocks.back();\n\tconst auto widget = _inner->add(\n\t\tblock.takeWidget(),\n\t\tQMargins(0, _inner->count() ? st::sendMediaRowSkip : 0, 0, 0));\n\tstruct State {\n\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t};\n\tconst auto state = widget->lifetime().make_state<State>();\n\tconst auto openedOnce = widget->lifetime().make_state<bool>(false);\n\tconst auto openInPhotoEditor = [=, show = _show](int index) {\n\t\tapplyBlockChanges();\n\n\t\tif (!(*openedOnce)) {\n\t\t\tshow->session().settings().incrementPhotoEditorHintShown();\n\t\t\tshow->session().saveSettings();\n\t\t}\n\t\t*openedOnce = true;\n\t\tEditor::OpenWithPreparedFile(\n\t\t\tthis,\n\t\t\tshow,\n\t\t\t&_list.files[index],\n\t\t\tst::sendMediaPreviewSize,\n\t\t\t[=](bool ok) {\n\t\t\t\tif (ok) {\n\t\t\t\t\trefreshAllAfterChanges(from);\n\t\t\t\t}\n\t\t\t},\n\t\t\tPhotoSideLimit(true));\n\t};\n\tconst auto replaceAttachment = [=, show = _show](int index) {\n\t\tapplyBlockChanges();\n\n\t\tconst auto replace = [=](Ui::PreparedList list) {\n\t\t\tif (list.files.empty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\trefreshAllAfterChanges(from, [&] {\n\t\t\t\t_list.files[index] = std::move(list.files.front());\n\t\t\t});\n\t\t};\n\t\tconst auto checkSlowmode = [=](const Ui::PreparedList &list) {\n\t\t\tif (list.files.empty() || !(_limits & SendFilesAllow::OnlyOne)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tauto removing = std::move(_list.files[index]);\n\t\t\tstd::swap(_list.files[index], _list.files.back());\n\t\t\t_list.files.pop_back();\n\t\t\tconst auto result = _list.canBeSentInSlowmodeWith(list);\n\t\t\t_list.files.push_back(std::move(removing));\n\t\t\tstd::swap(_list.files[index], _list.files.back());\n\t\t\tif (!result) {\n\t\t\t\tshow->showToast(tr::lng_slowmode_no_many(tr::now));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\t\tconst auto checkRights = [=](const Ui::PreparedList &list) {\n\t\t\tif (list.files.empty()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tauto removing = std::move(_list.files[index]);\n\t\t\tstd::swap(_list.files[index], _list.files.back());\n\t\t\t_list.files.pop_back();\n\t\t\tauto way = _sendWay.current();\n\t\t\tconst auto has = _list.hasSticker()\n\t\t\t\t|| list.files.front().isSticker();\n\t\t\tway.setHasCompressedStickers(has);\n\t\t\tif (_limits & SendFilesAllow::OnlyOne) {\n\t\t\t\tway.setGroupFiles(true);\n\t\t\t}\n\t\t\tconst auto silent = true;\n\t\t\tif (!checkWith(list, way, silent)\n\t\t\t\t&& (!(_limits & SendFilesAllow::OnlyOne) || !has)) {\n\t\t\t\tway.setSendImagesAsPhotos(!way.sendImagesAsPhotos());\n\t\t\t}\n\t\t\tconst auto result = checkWith(list, way);\n\t\t\t_list.files.push_back(std::move(removing));\n\t\t\tstd::swap(_list.files[index], _list.files.back());\n\t\t\tif (!result) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t_sendWay = way;\n\t\t\treturn true;\n\t\t};\n\t\tconst auto checkResult = [=](const Ui::PreparedList &list) {\n\t\t\treturn checkSlowmode(list) && checkRights(list);\n\t\t};\n\t\tconst auto callback = [=](FileDialog::OpenResult &&result) {\n\t\t\tconst auto premium = _show->session().premium();\n\t\t\tFileDialogCallback(\n\t\t\t\tstd::move(result),\n\t\t\t\tcheckResult,\n\t\t\t\treplace,\n\t\t\t\tpremium,\n\t\t\t\tshow);\n\t\t};\n\n\t\tFileDialog::GetOpenPath(\n\t\t\tthis,\n\t\t\ttr::lng_choose_file(tr::now),\n\t\t\tFileDialog::AllOrImagesFilter(),\n\t\t\tcrl::guard(this, callback));\n\t};\n\tconst auto editCover = [=, show = _show](int index) {\n\t\tapplyBlockChanges();\n\n\t\tconst auto replace = [=](Ui::PreparedList list) {\n\t\t\tif (list.files.empty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto &entry = _list.files[index];\n\t\t\tconst auto video = entry.information\n\t\t\t\t? std::get_if<Ui::PreparedFileInformation::Video>(\n\t\t\t\t\t&entry.information->media)\n\t\t\t\t: nullptr;\n\t\t\tif (!video) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto old = std::shared_ptr<Ui::PreparedFile>(\n\t\t\t\tstd::move(entry.videoCover));\n\t\t\tentry.videoCover = std::make_unique<Ui::PreparedFile>(\n\t\t\t\tstd::move(list.files.front()));\n\t\t\tEditor::OpenWithPreparedFile(\n\t\t\t\tthis,\n\t\t\t\tshow,\n\t\t\t\tentry.videoCover.get(),\n\t\t\t\tst::sendMediaPreviewSize,\n\t\t\t\tcrl::guard(this, [=](bool ok) {\n\t\t\t\t\tif (!ok) {\n\t\t\t\t\t\t_list.files[index].videoCover = old\n\t\t\t\t\t\t\t? std::make_unique<Ui::PreparedFile>(\n\t\t\t\t\t\t\t\tstd::move(*old))\n\t\t\t\t\t\t\t: nullptr;\n\t\t\t\t\t}\n\t\t\t\t\trefreshAllAfterChanges(from);\n\t\t\t\t}),\n\t\t\t\tPhotoSideLimit(true),\n\t\t\t\tvideo->thumbnail.size());\n\t\t};\n\t\tconst auto checkResult = [=](const Ui::PreparedList &list) {\n\t\t\tif (list.files.empty()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (list.files.front().type != Ui::PreparedFile::Type::Photo) {\n\t\t\t\tshow->showToast(tr::lng_choose_cover_bad(tr::now));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\t\tconst auto callback = [=](FileDialog::OpenResult &&result) {\n\t\t\tconst auto premium = _show->session().premium();\n\t\t\tFileDialogCallback(\n\t\t\t\tstd::move(result),\n\t\t\t\tcheckResult,\n\t\t\t\treplace,\n\t\t\t\tpremium,\n\t\t\t\tshow);\n\t\t};\n\n\t\tFileDialog::GetOpenPath(\n\t\t\tthis,\n\t\t\ttr::lng_choose_cover(tr::now),\n\t\t\tFileDialog::ImagesFilter(),\n\t\t\tcrl::guard(this, callback));\n\t};\n\tconst auto clearCover = [=](int index) {\n\t\tapplyBlockChanges();\n\t\trefreshAllAfterChanges(from, [&] {\n\t\t\tauto &entry = _list.files[index];\n\t\t\tentry.videoCover = nullptr;\n\t\t});\n\t};\n\tconst auto showContextMenu = [=](\n\t\t\tint fileIndex,\n\t\t\tQPoint globalPosition,\n\t\t\tbool forceToLeft = false) {\n\t\tif (from >= till\n\t\t\t|| fileIndex < from\n\t\t\t|| fileIndex >= till\n\t\t\t|| fileIndex >= _list.files.size()) {\n\t\t\treturn false;\n\t\t}\n\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\twidget,\n\t\t\t_st.tabbed.menu);\n\t\tif (forceToLeft) {\n\t\t\tusing Origin = Ui::PanelAnimation::Origin;\n\t\t\tstate->menu->setForcedOrigin(Origin::TopRight);\n\t\t}\n\t\tconst auto &file = _list.files[fileIndex];\n\t\tstate->menu->addAction(tr::lng_attach_replace(tr::now), [=] {\n\t\t\treplaceAttachment(fileIndex);\n\t\t}, &st::menuIconReplace);\n\t\tconst auto canOpenPhotoEditor = true\n\t\t\t&& _sendWay.current().sendImagesAsPhotos()\n\t\t\t&& (file.type == Ui::PreparedFile::Type::Photo);\n\t\tif (canOpenPhotoEditor) {\n\t\t\tstate->menu->addAction(tr::lng_context_draw(tr::now), [=] {\n\t\t\t\topenInPhotoEditor(fileIndex);\n\t\t\t}, &st::menuIconDraw);\n\t\t}\n\t\tconst auto canEditFileData = !SkipCaption(\n\t\t\tfile,\n\t\t\t_sendWay.current());\n\t\tif (canEditFileData) {\n\t\t\tstate->menu->addAction(tr::lng_rename_file(tr::now), [=] {\n\t\t\t\tauto &file = _list.files[fileIndex];\n\t\t\t\t_show->show(Box(RenameFileBox, file.displayName, [=](\n\t\t\t\t\t\tQString newName) {\n\t\t\t\t\tconst auto displayName = std::move(newName);\n\t\t\t\t\t_list.files[fileIndex].displayName = displayName;\n\t\t\t\t\tif (!setDisplayNameInSingleFilePreview(\n\t\t\t\t\t\t\tfileIndex,\n\t\t\t\t\t\t\tdisplayName)) {\n\t\t\t\t\t\trefreshAllAfterChanges(from);\n\t\t\t\t\t}\n\t\t\t\t}));\n\t\t\t}, &st::menuIconEdit);\n\t\t\tstate->menu->addAction(\n\t\t\t\ttr::lng_context_upload_edit_caption(tr::now),\n\t\t\t\t[=] {\n\t\t\t\t\tauto &file = _list.files[fileIndex];\n\t\t\t\t\tconst auto count = int(_list.files.size());\n\t\t\t\t\tconst auto sync = (fileIndex + 1 == count);\n\t\t\t\t\t_show->show(Box(\n\t\t\t\t\t\tEditFileCaptionBox,\n\t\t\t\t\t\t_st,\n\t\t\t\t\t\t_toPeer,\n\t\t\t\t\t\tsync ? fieldText() : file.caption,\n\t\t\t\t\t\t[=](TextWithTags text) {\n\t\t\t\t\t\t\tif (!validateLength(text.text)) {\n\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (sync) {\n\t\t\t\t\t\t\t\t_caption->setTextWithTags(\n\t\t\t\t\t\t\t\t\tbase::take(text));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t_list.files[fileIndex].caption = text;\n\t\t\t\t\t\t\tif (!setCaptionInSingleFilePreview(\n\t\t\t\t\t\t\t\t\tfileIndex,\n\t\t\t\t\t\t\t\t\ttext)) {\n\t\t\t\t\t\t\t\trefreshAllAfterChanges(from);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}));\n\t\t\t\t},\n\t\t\t\t\t&st::menuIconCaptionShow);\n\t\t}\n\t\tconst auto canToggleSpoiler = !hasPrice()\n\t\t\t&& _sendWay.current().sendImagesAsPhotos()\n\t\t\t&& (file.type == Ui::PreparedFile::Type::Photo\n\t\t\t\t|| file.type == Ui::PreparedFile::Type::Video);\n\t\tif (canToggleSpoiler) {\n\t\t\tconst auto spoilered = file.spoiler;\n\t\t\tconst auto &icons = _st.tabbed.icons;\n\t\t\tMenu::AddCheckedAction(\n\t\t\t\tstate->menu.get(),\n\t\t\t\ttr::lng_context_spoiler_effect(tr::now),\n\t\t\t\t[=] {\n\t\t\t\t\tapplyBlockChanges();\n\t\t\t\t\trefreshAllAfterChanges(from, [&] {\n\t\t\t\t\t\tauto &entry = _list.files[fileIndex];\n\t\t\t\t\t\tentry.spoiler = !spoilered;\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\t&icons.menuSpoiler,\n\t\t\t\tspoilered);\n\t\t}\n\t\tconst auto canEditCover = file.isVideoFile()\n\t\t\t&& (_toPeer->isBroadcast() || _toPeer->isSelf());\n\t\tif (canEditCover) {\n\t\t\tstate->menu->addAction(tr::lng_context_edit_cover(tr::now), [=] {\n\t\t\t\teditCover(fileIndex);\n\t\t\t}, &st::menuIconEdit);\n\n\t\t\tif (file.videoCover != nullptr) {\n\t\t\t\tstate->menu->addAction(\n\t\t\t\t\ttr::lng_context_clear_cover(tr::now),\n\t\t\t\t\t[=] { clearCover(fileIndex); },\n\t\t\t\t\t&st::menuIconCancel);\n\t\t\t}\n\t\t}\n\t\tif (state->menu->empty()) {\n\t\t\tstate->menu = nullptr;\n\t\t\treturn false;\n\t\t}\n\t\tstate->menu->popup(globalPosition);\n\t\treturn true;\n\t};\n\n\tblock.itemDeleteRequest(\n\t) | rpl::filter([=] {\n\t\treturn !_removingIndex;\n\t}) | rpl::on_next([=](int index) {\n\t\tapplyBlockChanges();\n\n\t\t_removingIndex = index;\n\t\tcrl::on_main(this, [=] {\n\t\t\tconst auto index = base::take(_removingIndex).value_or(-1);\n\t\t\tif (index < 0 || index >= _list.files.size()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Just close the box if it is the only one.\n\t\t\tif (_list.files.size() == 1) {\n\t\t\t\trequestToTakeTextWithTags();\n\t\t\t\tcloseBox();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\trefreshAllAfterChanges(index, [&] {\n\t\t\t\t_list.files.erase(_list.files.begin() + index);\n\t\t\t\tif (index == _list.files.size()) {\n\t\t\t\t\tauto &last = _list.files.back();\n\t\t\t\t\tconst auto was = base::take(last.caption);\n\t\t\t\t\tif (fieldText().empty() && !last.isSticker()) {\n\t\t\t\t\t\t_caption->setTextWithTags(was);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}, widget->lifetime());\n\n\tblock.itemReplaceRequest(\n\t) | rpl::on_next([=](int index) {\n\t\tshowContextMenu(index, QCursor::pos(), true);\n\t}, widget->lifetime());\n\n\tblock.itemModifyRequest(\n\t) | rpl::on_next([=](int index) {\n\t\topenInPhotoEditor(index);\n\t}, widget->lifetime());\n\n\tblock.orderUpdated() | rpl::on_next([=]{\n\t\tif (_priceTag) {\n\t\t\t_priceTagBg = QImage();\n\t\t\t_priceTag->update();\n\t\t}\n\t}, widget->lifetime());\n\n\tbase::install_event_filter(widget, [=](\n\t\t\tnot_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::ContextMenu) {\n\t\t\tconst auto mouse = static_cast<QContextMenuEvent*>(e.get());\n\t\t\tif (from >= till || from >= _list.files.size()) {\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t}\n\t\t\tauto fileIndex = from;\n\t\t\tif (const auto album = dynamic_cast<Ui::AlbumPreview*>(widget)) {\n\t\t\t\tconst auto indexInBlock = album->indexFromPoint(mouse->pos());\n\t\t\t\tif (indexInBlock < 0) {\n\t\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t\t}\n\t\t\t\tfileIndex += indexInBlock;\n\t\t\t}\n\t\t\tif (fileIndex >= till || fileIndex >= _list.files.size()) {\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t}\n\t\t\tif (showContextMenu(fileIndex, mouse->globalPos())) {\n\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t}\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t}, widget->lifetime());\n}\n\nvoid SendFilesBox::refreshControls(bool initial) {\n\trefreshButtons();\n\trefreshPriceTag();\n\trefreshTitleText();\n\tupdateSendWayControls();\n\tupdateCaptionVisibility();\n}\n\nvoid SendFilesBox::setupSendWayControls() {\n\tconst auto groupFilesFirst = _sendWay.current().groupFiles();\n\tconst auto asPhotosFirst = _sendWay.current().sendImagesAsPhotos();\n\n\tif ((_list.files.size() == 1)\n\t\t&& _list.files.front().path.endsWith(\".ogg\")) {\n\t\t_sendAsVoice.create(\n\t\t\tthis,\n\t\t\tu\"Send first as Voice\"_q,\n\t\t\tfalse,\n\t\t\t_st.files.checkbox,\n\t\t\t_st.files.check);\n\t}\n\t_groupFiles.create(\n\t\tthis,\n\t\ttr::lng_send_grouped(tr::now),\n\t\tgroupFilesFirst,\n\t\t_st.files.checkbox,\n\t\t_st.files.check);\n\t_sendImagesAsPhotos.create(\n\t\tthis,\n\t\ttr::lng_send_as_documents(tr::now),\n\t\t!_sendWay.current().sendImagesAsPhotos(),\n\t\t_st.files.checkbox,\n\t\t_st.files.check);\n\n\t_sendWay.changes(\n\t) | rpl::on_next([=](SendFilesWay value) {\n\t\t_groupFiles->setChecked(value.groupFiles());\n\t\t_sendImagesAsPhotos->setChecked(!value.sendImagesAsPhotos());\n\t}, lifetime());\n\n\t_groupFiles->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tauto sendWay = _sendWay.current();\n\t\tif (sendWay.groupFiles() == checked) {\n\t\t\treturn;\n\t\t}\n\t\tsendWay.setGroupFiles(checked);\n\t\tif (checkWithWay(sendWay)) {\n\t\t\t_sendWay = sendWay;\n\t\t} else {\n\t\t\tUi::PostponeCall(_groupFiles.data(), [=] {\n\t\t\t\t_groupFiles->setChecked(!checked);\n\t\t\t});\n\t\t}\n\t}, lifetime());\n\n\t_sendImagesAsPhotos->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tauto sendWay = _sendWay.current();\n\t\tif (sendWay.sendImagesAsPhotos() == !checked) {\n\t\t\treturn;\n\t\t}\n\t\tsendWay.setSendImagesAsPhotos(!checked);\n\t\tif (checkWithWay(sendWay)) {\n\t\t\t_sendWay = sendWay;\n\t\t} else {\n\t\t\tUi::PostponeCall(_sendImagesAsPhotos.data(), [=] {\n\t\t\t\t_sendImagesAsPhotos->setChecked(!checked);\n\t\t\t});\n\t\t}\n\t}, lifetime());\n\n\t_wayRemember.create(\n\t\tthis,\n\t\ttr::lng_remember(tr::now),\n\t\tfalse,\n\t\t_st.files.checkbox,\n\t\t_st.files.check);\n\t_wayRemember->hide();\n\trpl::combine(\n\t\t_groupFiles->checkedValue(),\n\t\t_sendImagesAsPhotos->checkedValue()\n\t) | rpl::on_next([=](bool groupFiles, bool asDocuments) {\n\t\t_wayRemember->setVisible(\n\t\t\t(groupFiles != groupFilesFirst)\n\t\t\t\t|| ((!asDocuments) != asPhotosFirst));\n\t\tcaptionResized();\n\t}, lifetime());\n\n\t_hintLabel.create(\n\t\tthis,\n\t\ttr::lng_edit_photo_editor_hint(tr::now),\n\t\tst::editMediaHintLabel);\n}\n\nbool SendFilesBox::checkWithWay(Ui::SendFilesWay way, bool silent) const {\n\treturn checkWith({}, way, silent);\n}\n\nbool SendFilesBox::checkWith(\n\t\tconst Ui::PreparedList &added,\n\t\tUi::SendFilesWay way,\n\t\tbool silent) const {\n\tif (!_check) {\n\t\treturn true;\n\t}\n\tconst auto compress = way.sendImagesAsPhotos();\n\tauto &already = _list.files;\n\tfor (const auto &file : ranges::views::concat(already, added.files)) {\n\t\tif (!_check(file, compress, silent)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid SendFilesBox::updateSendWayControls() {\n\tconst auto onlyOne = (_limits & SendFilesAllow::OnlyOne);\n\t_groupFiles->setVisible(_list.hasGroupOption(onlyOne));\n\t_sendImagesAsPhotos->setVisible(\n\t\t_list.hasSendImagesAsPhotosOption(onlyOne));\n\t_sendImagesAsPhotos->setText((_list.files.size() > 1)\n\t\t? tr::lng_send_as_documents(tr::now)\n\t\t: tr::lng_send_as_documents_one(tr::now));\n\n\t_hintLabel->setVisible(\n\t\t_show->session().settings().photoEditorHintShown()\n\t\t\t? _list.canHaveEditorHintLabel()\n\t\t\t: false);\n}\n\nvoid SendFilesBox::setupCaption() {\n\tconst auto allow = [=](not_null<DocumentData*> emoji) {\n\t\treturn Data::AllowEmojiWithoutPremium(_toPeer, emoji);\n\t};\n\tconst auto show = _show;\n\tconst auto chatStyle = InitMessageFieldHandlers({\n\t\t.session = &show->session(),\n\t\t.show = show,\n\t\t.field = _caption.data(),\n\t\t.customEmojiPaused = [=] {\n\t\t\treturn show->paused(Window::GifPauseReason::Layer);\n\t\t},\n\t\t.allowPremiumEmoji = allow,\n\t\t.fieldStyle = &_st.files.caption,\n\t});\n\tsetupCaptionAutocomplete();\n\tUi::Emoji::SuggestionsController::Init(\n\t\tgetDelegate()->outerContainer(),\n\t\t_caption,\n\t\t&_show->session(),\n\t\t{\n\t\t\t.suggestCustomEmoji = true,\n\t\t\t.allowCustomWithoutPremium = allow,\n\t\t\t.st = &_st.suggestions,\n\t\t});\n\n\tif (!_prefilledCaptionText.text.isEmpty()) {\n\t\t_caption->setTextWithTags(\n\t\t\t_prefilledCaptionText,\n\t\t\tUi::InputField::HistoryAction::Clear);\n\n\t\tauto cursor = _caption->textCursor();\n\t\tcursor.movePosition(QTextCursor::End);\n\t\t_caption->setTextCursor(cursor);\n\t}\n\t_caption->setSubmitSettings(\n\t\tCore::App().settings().sendSubmitWay());\n\t_caption->setMaxLength(kMaxMessageLength);\n\n\t_caption->heightChanges(\n\t) | rpl::on_next([=] {\n\t\tcaptionResized();\n\t}, _caption->lifetime());\n\t_caption->submits(\n\t) | rpl::on_next([=](Qt::KeyboardModifiers modifiers) {\n\t\tconst auto ctrlShiftEnter = modifiers.testFlag(Qt::ShiftModifier)\n\t\t\t&& (modifiers.testFlag(Qt::ControlModifier)\n\t\t\t\t|| modifiers.testFlag(Qt::MetaModifier));\n\t\tsend({}, ctrlShiftEnter);\n\t}, _caption->lifetime());\n\t_caption->cancelled(\n\t) | rpl::on_next([=] {\n\t\trequestToTakeTextWithTags();\n\t\tcloseBox();\n\t}, _caption->lifetime());\n\t_caption->setMimeDataHook([=](\n\t\t\tnot_null<const QMimeData*> data,\n\t\t\tUi::InputField::MimeAction action) {\n\t\tif (action == Ui::InputField::MimeAction::Check) {\n\t\t\treturn CanAddFiles(data);\n\t\t} else if (action == Ui::InputField::MimeAction::Insert) {\n\t\t\treturn addFiles(data);\n\t\t}\n\t\tUnexpected(\"action in MimeData hook.\");\n\t});\n\n\tupdateCaptionVisibility();\n\tsetupEmojiPanel();\n\n\trpl::single(rpl::empty_value()) | rpl::then(\n\t\t_caption->changes()\n\t) | rpl::on_next([=] {\n\t\tcheckCharsLimitation();\n\t\trefreshMessagesCount();\n\t}, _caption->lifetime());\n\n\t_aiButton = Ui::SetupCaptionAiButton({\n\t\t.parent = this,\n\t\t.field = _caption.data(),\n\t\t.session = &_show->session(),\n\t\t.show = _show,\n\t\t.chatStyle = chatStyle,\n\t});\n}\n\nvoid SendFilesBox::setupCaptionAutocomplete() {\n\tconst auto parent = getDelegate()->outerContainer();\n\tChatHelpers::InitFieldAutocomplete(_autocomplete, {\n\t\t.parent = parent,\n\t\t.show = _show,\n\t\t.field = _caption.data(),\n\t\t.peer = _toPeer,\n\t\t.features = [=] {\n\t\t\tauto result = ChatHelpers::ComposeFeatures();\n\t\t\tresult.autocompleteCommands = false;\n\t\t\tresult.suggestStickersByEmoji = false;\n\t\t\treturn result;\n\t\t},\n\t\t.sendMenuDetails = _sendMenuDetails,\n\t});\n\tconst auto raw = _autocomplete.get();\n\tconst auto scheduled = std::make_shared<bool>();\n\tconst auto recountPostponed = [=] {\n\t\tif (*scheduled) {\n\t\t\treturn;\n\t\t}\n\t\t*scheduled = true;\n\t\tUi::PostponeCall(raw, [=] {\n\t\t\t*scheduled = false;\n\n\t\t\tauto field = Ui::MapFrom(parent, this, _caption->geometry());\n\t\t\t_autocomplete->setBoundings(QRect(\n\t\t\t\tfield.x() - _caption->x(),\n\t\t\t\tst::defaultBox.margin.top(),\n\t\t\t\twidth(),\n\t\t\t\t(field.y()\n\t\t\t\t\t+ _st.files.caption.textMargins.top()\n\t\t\t\t\t+ _st.files.caption.placeholderShift\n\t\t\t\t\t+ _st.files.caption.placeholderFont->height\n\t\t\t\t\t- st::defaultBox.margin.top())));\n\t\t});\n\t};\n\tfor (auto w = (QWidget*)_caption.data(); w; w = w->parentWidget()) {\n\t\tbase::install_event_filter(raw, w, [=](not_null<QEvent*> e) {\n\t\t\tif (e->type() == QEvent::Move || e->type() == QEvent::Resize) {\n\t\t\t\trecountPostponed();\n\t\t\t}\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t});\n\t\tif (w == parent) {\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nTextWithTags SendFilesBox::fieldText() const {\n\treturn !_caption->isHidden()\n\t\t? _caption->getTextWithAppliedMarkdown()\n\t\t: TextWithTags();\n}\n\nvoid SendFilesBox::checkCharsLimitation() {\n\tconst auto limits = Data::PremiumLimits(&_show->session());\n\tconst auto caption = fieldText();\n\tconst auto remove = caption.text.size() - limits.captionLengthCurrent();\n\tif ((remove > 0) && _emojiToggle) {\n\t\tif (!_charsLimitation) {\n\t\t\t_charsLimitation = base::make_unique_q<CharactersLimitLabel>(\n\t\t\t\tthis,\n\t\t\t\t_emojiToggle.data(),\n\t\t\t\tstyle::al_top);\n\t\t\t_charsLimitation->show();\n\t\t\tData::AmPremiumValue(\n\t\t\t\t&_show->session()\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tcheckCharsLimitation();\n\t\t\t}, _charsLimitation->lifetime());\n\t\t}\n\t\t_charsLimitation->setLeft(remove);\n\t} else {\n\t\tif (_charsLimitation) {\n\t\t\t_charsLimitation = nullptr;\n\t\t}\n\t}\n}\n\nvoid SendFilesBox::setupEmojiPanel() {\n\tconst auto container = getDelegate()->outerContainer();\n\tusing Selector = ChatHelpers::TabbedSelector;\n\t_emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(\n\t\tcontainer,\n\t\tChatHelpers::TabbedPanelDescriptor{\n\t\t\t.ownedSelector = object_ptr<Selector>(\n\t\t\t\tnullptr,\n\t\t\t\tChatHelpers::TabbedSelectorDescriptor{\n\t\t\t\t\t.show = _show,\n\t\t\t\t\t.st = _st.tabbed,\n\t\t\t\t\t.level = Window::GifPauseReason::Layer,\n\t\t\t\t\t.mode = ChatHelpers::TabbedSelector::Mode::EmojiOnly,\n\t\t\t\t\t.features = {\n\t\t\t\t\t\t.stickersSettings = false,\n\t\t\t\t\t\t.openStickerSets = false,\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t});\n\t_emojiPanel->setDesiredHeightValues(\n\t\t1.,\n\t\tst::emojiPanMinHeight / 2,\n\t\tst::emojiPanMinHeight);\n\t_emojiPanel->hide();\n\t_emojiPanel->selector()->setCurrentPeer(_toPeer);\n\t_emojiPanel->selector()->setAllowEmojiWithoutPremium(\n\t\t_limits & SendFilesAllow::EmojiWithoutPremium);\n\t_emojiPanel->selector()->emojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\tUi::InsertEmojiAtCursor(_caption->textCursor(), data.emoji);\n\t}, lifetime());\n\t_emojiPanel->selector()->customEmojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\tconst auto info = data.document->sticker();\n\t\tif (info\n\t\t\t&& info->setType == Data::StickersType::Emoji\n\t\t\t&& !_show->session().premium()\n\t\t\t&& !Data::AllowEmojiWithoutPremium(_toPeer, data.document)) {\n\t\t\tShowPremiumPreviewBox(_show, PremiumFeature::AnimatedEmoji);\n\t\t} else {\n\t\t\tData::InsertCustomEmoji(_caption.data(), data.document);\n\t\t}\n\t}, lifetime());\n\n\tconst auto filterCallback = [=](not_null<QEvent*> event) {\n\t\temojiFilterForGeometry(event);\n\t\treturn base::EventFilterResult::Continue;\n\t};\n\t_emojiFilter.reset(base::install_event_filter(container, filterCallback));\n\n\t_emojiToggle.create(this, _st.files.emoji);\n\t_emojiToggle->setVisible(!_caption->isHidden());\n\tif (!Core::App().settings().fork().emojiPopupOnClick()) {\n\t_emojiToggle->installEventFilter(_emojiPanel);\n\t}\n\t_emojiToggle->addClickHandler([=] {\n\t\t_emojiPanel->toggleAnimated();\n\t});\n}\n\nvoid SendFilesBox::emojiFilterForGeometry(not_null<QEvent*> event) {\n\tconst auto type = event->type();\n\tif (type == QEvent::Move || type == QEvent::Resize) {\n\t\t// updateEmojiPanelGeometry uses not only container geometry, but\n\t\t// also container children geometries that will be updated later.\n\t\tcrl::on_main(this, [=] { updateEmojiPanelGeometry(); });\n\t}\n}\n\nvoid SendFilesBox::updateEmojiPanelGeometry() {\n\tconst auto parent = _emojiPanel->parentWidget();\n\tconst auto global = _emojiToggle->mapToGlobal({ 0, 0 });\n\tconst auto local = parent->mapFromGlobal(global);\n\t_emojiPanel->moveBottomRight(\n\t\tlocal.y(),\n\t\tlocal.x() + _emojiToggle->width() * 3);\n}\n\nvoid SendFilesBox::captionResized() {\n\tupdateBoxSize();\n\tupdateControlsGeometry();\n\tupdateEmojiPanelGeometry();\n\tupdate();\n}\n\nbool SendFilesBox::addFiles(not_null<const QMimeData*> data) {\n\tconst auto premium = _show->session().premium();\n\tauto list = [&] {\n\t\tconst auto urls = Core::ReadMimeUrls(data);\n\t\tauto result = CanAddUrls(urls)\n\t\t\t? Storage::PrepareMediaList(\n\t\t\t\turls,\n\t\t\t\tst::sendMediaPreviewSize,\n\t\t\t\tpremium)\n\t\t\t: Ui::PreparedList(\n\t\t\t\tUi::PreparedList::Error::EmptyFile,\n\t\t\t\tQString());\n\t\tif (result.error == Ui::PreparedList::Error::None) {\n\t\t\treturn result;\n\t\t} else if (auto read = Core::ReadMimeImage(data)) {\n\t\t\treturn Storage::PrepareMediaFromImage(\n\t\t\t\tstd::move(read.image),\n\t\t\t\tstd::move(read.content),\n\t\t\t\tst::sendMediaPreviewSize);\n\t\t}\n\t\treturn result;\n\t}();\n\treturn addFiles(std::move(list));\n}\n\nbool SendFilesBox::addFiles(Ui::PreparedList list) {\n\tif (list.error != Ui::PreparedList::Error::None) {\n\t\treturn false;\n\t}\n\tconst auto count = int(_list.files.size());\n\t_list.filesToProcess.insert(\n\t\t_list.filesToProcess.end(),\n\t\tstd::make_move_iterator(list.files.begin()),\n\t\tstd::make_move_iterator(list.files.end()));\n\t_list.filesToProcess.insert(\n\t\t_list.filesToProcess.end(),\n\t\tstd::make_move_iterator(list.filesToProcess.begin()),\n\t\tstd::make_move_iterator(list.filesToProcess.end()));\n\tenqueueNextPrepare();\n\tif (_list.files.size() > count) {\n\t\trefreshAllAfterChanges(count);\n\t}\n\treturn true;\n}\n\nvoid SendFilesBox::addPreparedAsyncFile(Ui::PreparedFile &&file) {\n\tExpects(file.information != nullptr);\n\n\t_preparing = false;\n\tconst auto count = int(_list.files.size());\n\taddFile(std::move(file));\n\tenqueueNextPrepare();\n\tif (_list.files.size() > count) {\n\t\trefreshAllAfterChanges(count);\n\t}\n\tif (!_preparing && _whenReadySend) {\n\t\t_whenReadySend();\n\t}\n}\n\nvoid SendFilesBox::addFile(Ui::PreparedFile &&file) {\n\t// canBeSentInSlowmode checks for non empty filesToProcess.\n\tauto saved = base::take(_list.filesToProcess);\n\t_list.files.push_back(std::move(file));\n\tconst auto lastOk = [&] {\n\t\tauto way = _sendWay.current();\n\t\tif (_limits & SendFilesAllow::OnlyOne) {\n\t\t\tway.setGroupFiles(true);\n\t\t\tif (!_list.canBeSentInSlowmode()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else if (!checkWithWay(way)) {\n\t\t\treturn false;\n\t\t}\n\t\t_sendWay = way;\n\t\treturn true;\n\t}();\n\tif (!lastOk) {\n\t\t_list.files.pop_back();\n\t}\n\t_list.filesToProcess = std::move(saved);\n}\n\nvoid SendFilesBox::refreshTitleText() {\n\tusing Type = Ui::PreparedFile::Type;\n\tconst auto count = int(_list.files.size());\n\tif (count > 1) {\n\t\tconst auto imagesCount = ranges::count(\n\t\t\t_list.files,\n\t\t\tType::Photo,\n\t\t\t&Ui::PreparedFile::type);\n\t\t_titleText = (imagesCount == count)\n\t\t\t? tr::lng_send_images_selected(tr::now, lt_count, count)\n\t\t\t: tr::lng_send_files_selected(tr::now, lt_count, count);\n\t} else {\n\t\tconst auto type = _list.files.empty()\n\t\t\t? Type::None\n\t\t\t: _list.files.front().type;\n\t\t_titleText = (type == Type::Photo)\n\t\t\t? tr::lng_send_image(tr::now)\n\t\t\t: (type == Type::Video)\n\t\t\t? tr::lng_send_video(tr::now)\n\t\t\t: tr::lng_send_file(tr::now);\n\t}\n\t_titleHeight = st::boxTitleHeight;\n}\n\nvoid SendFilesBox::updateBoxSize() {\n\tauto footerHeight = 0;\n\tif (!_caption->isHidden()) {\n\t\tfooterHeight += st::boxPhotoCaptionSkip + _caption->height();\n\t}\n\tconst auto pairs = std::array<std::pair<RpWidget*, int>, 5>{ {\n\t\t{ _groupFiles.data(), st::boxPhotoCompressedSkip },\n\t\t{ _sendImagesAsPhotos.data(), st::boxPhotoCompressedSkip },\n\t\t{ _wayRemember.data(), st::boxPhotoCompressedSkip },\n\t\t{ _hintLabel.data(), st::editMediaLabelMargins.top() },\n\t\t{ _sendAsVoice.data(), st::boxPhotoCompressedSkip },\n\t} };\n\tfor (const auto &pair : pairs) {\n\t\tconst auto pointer = pair.first;\n\t\tif (pointer && !pointer->isHidden()) {\n\t\t\tfooterHeight += pair.second + pointer->heightNoMargins();\n\t\t}\n\t}\n\t_footerHeight = footerHeight;\n}\n\nvoid SendFilesBox::keyPressEvent(QKeyEvent *e) {\n\tif (e->matches(QKeySequence::Open)) {\n\t\topenDialogToAddFileToAlbum();\n\t} else if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {\n\t\tconst auto modifiers = e->modifiers();\n\t\tconst auto ctrl = modifiers.testFlag(Qt::ControlModifier)\n\t\t\t|| modifiers.testFlag(Qt::MetaModifier);\n\t\tconst auto shift = modifiers.testFlag(Qt::ShiftModifier);\n\t\tsend({}, ctrl && shift);\n\t} else {\n\t\tBoxContent::keyPressEvent(e);\n\t}\n}\n\nvoid SendFilesBox::paintEvent(QPaintEvent *e) {\n\tBoxContent::paintEvent(e);\n\n\tif (!_titleText.isEmpty()) {\n\t\tPainter p(this);\n\n\t\tp.setFont(st::boxTitleFont);\n\t\tp.setPen(getDelegate()->style().title.textFg);\n\t\tp.drawTextLeft(\n\t\t\tst::boxPhotoTitlePosition.x(),\n\t\t\tst::boxTitlePosition.y() - st::boxTopMargin,\n\t\t\twidth(),\n\t\t\t_titleText);\n\t}\n}\n\nvoid SendFilesBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\tupdateControlsGeometry();\n}\n\nvoid SendFilesBox::updateControlsGeometry() {\n\tauto bottom = height();\n\tif (!_caption->isHidden()) {\n\t\t_caption->resize(st::sendMediaPreviewSize, _caption->height());\n\t\t_caption->moveToLeft(\n\t\t\tst::boxPhotoPadding.left(),\n\t\t\tbottom - _caption->height());\n\t\tbottom -= st::boxPhotoCaptionSkip + _caption->height();\n\n\t\tif (_emojiToggle) {\n\t\t\t_emojiToggle->moveToLeft(\n\t\t\t\t(st::boxPhotoPadding.left()\n\t\t\t\t\t+ st::sendMediaPreviewSize\n\t\t\t\t\t- _emojiToggle->width()),\n\t\t\t\t_caption->y() + st::boxAttachEmojiTop);\n\t\t\t_emojiToggle->update();\n\t\t}\n\t\tif (_aiButton) {\n\t\t\tUi::UpdateCaptionAiButtonGeometry(_aiButton, _caption.data());\n\t\t\t_aiButton->raise();\n\t\t}\n\t}\n\tconst auto pairs = std::array<std::pair<RpWidget*, int>, 5>{ {\n\t\t{ _hintLabel.data(), st::editMediaLabelMargins.top() },\n\t\t{ _groupFiles.data(), st::boxPhotoCompressedSkip },\n\t\t{ _sendImagesAsPhotos.data(), st::boxPhotoCompressedSkip },\n\t\t{ _wayRemember.data(), st::boxPhotoCompressedSkip },\n\t\t{ _sendAsVoice.data(), st::boxPhotoCompressedSkip },\n\t} };\n\tfor (const auto &pair : ranges::views::reverse(pairs)) {\n\t\tconst auto pointer = pair.first;\n\t\tif (pointer && !pointer->isHidden()) {\n\t\t\tpointer->moveToLeft(\n\t\t\t\tst::boxPhotoPadding.left(),\n\t\t\t\tbottom - pointer->heightNoMargins());\n\t\t\tbottom -= pair.second + pointer->heightNoMargins();\n\t\t}\n\t}\n\t_scroll->resize(width(), bottom - _titleHeight.current());\n\t_scroll->move(0, _titleHeight.current());\n}\n\nvoid SendFilesBox::showFinished() {\n\tif (const auto raw = _autocomplete.get()) {\n\t\tInvokeQueued(raw, [=] {\n\t\t\traw->raise();\n\t\t});\n\t}\n}\n\nrpl::producer<TextWithTags> SendFilesBox::takeTextWithTagsRequests() const {\n\treturn _textWithTagsRequests.events();\n}\n\nvoid SendFilesBox::requestToTakeTextWithTags() const {\n\tif (!_caption->isHidden()) {\n\t\t_textWithTagsRequests.fire_copy(_caption->getTextWithTags());\n\t}\n}\n\nvoid SendFilesBox::setInnerFocus() {\n\tif (!_caption->isHidden()) {\n\t\t_caption->setFocusFast();\n\t} else {\n\t\tBoxContent::setInnerFocus();\n\t}\n}\n\nvoid SendFilesBox::saveSendWaySettings(bool rememberAll) {\n\tauto way = _sendWay.current();\n\tauto oldWay = Core::App().settings().sendFilesWay();\n\tif (!rememberAll) {\n\t\tway.setGroupFiles(oldWay.groupFiles());\n\t\tway.setSendImagesAsPhotos(oldWay.sendImagesAsPhotos());\n\t} else if (_groupFiles->isHidden()) {\n\t\tway.setGroupFiles(oldWay.groupFiles());\n\t}\n\tif (rememberAll\n\t\t&& (_list.overrideSendImagesAsPhotos == way.sendImagesAsPhotos()\n\t\t\t|| _sendImagesAsPhotos->isHidden())) {\n\t\tway.setSendImagesAsPhotos(oldWay.sendImagesAsPhotos());\n\t}\n\tif (way != oldWay) {\n\t\tCore::App().settings().setSendFilesWay(way);\n\t\tCore::App().saveSettingsDelayed();\n\t}\n}\n\nbool SendFilesBox::validateLength(const QString &text) const {\n\tconst auto session = &_show->session();\n\tconst auto limit = Data::PremiumLimits(session).captionLengthCurrent();\n\tconst auto remove = int(text.size()) - limit;\n\tif (remove <= 0) {\n\t\treturn true;\n\t}\n\t_show->showBox(\n\t\tBox(CaptionLimitReachedBox, session, remove, &_st.premium));\n\treturn false;\n}\n\nvoid SendFilesBox::send(\n\t\tApi::SendOptions options,\n\t\tbool ctrlShiftEnter) {\n\tif ((_sendType == Api::SendType::Scheduled\n\t\t|| _sendType == Api::SendType::ScheduledToUser)\n\t\t&& !options.scheduled) {\n\t\tauto child = _sendMenuDetails();\n\t\tchild.spoiler = SendMenu::SpoilerState::None;\n\t\tchild.caption = SendMenu::CaptionState::None;\n\t\tchild.photoQuality = SendMenu::PhotoQualityState::None;\n\t\tchild.price = std::nullopt;\n\t\treturn SendMenu::DefaultCallback(_show, sendCallback())(\n\t\t\t{ .type = SendMenu::ActionType::Schedule },\n\t\t\tchild);\n\t}\n\tif (_preparing) {\n\t\t_whenReadySend = [=] {\n\t\t\tsend(options, ctrlShiftEnter);\n\t\t};\n\t\treturn;\n\t}\n\n\tauto way = _sendWay.current();\n\n\tfor (auto &item : _list.files) {\n\t\titem.spoiler = false;\n\t}\n\tapplyBlockChanges();\n\tfor (auto &item : _list.files) {\n\t\titem.sendLargePhotos = way.sendLargePhotos();\n\t}\n\n\tStorage::ApplyModifications(_list);\n\n\t_confirmed = true;\n\tif (_confirmedCallback) {\n\t\tauto caption = fieldText();\n\t\tif (!validateLength(caption.text)) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const auto &file : _list.files) {\n\t\t\tif (!SkipCaption(file, way)\n\t\t\t\t&& !validateLength(file.caption.text)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tsaveSendWaySettings(_wayRemember && _wayRemember->checked());\n\t\toptions.invertCaption = _invertCaption;\n\t\toptions.price = hasPrice() ? _price.current() : 0;\n\t\tif (options.price > 0) {\n\t\t\tfor (auto &file : _list.files) {\n\t\t\t\tfile.spoiler = false;\n\t\t\t}\n\t\t}\n\t\tfor (auto &file : _list.files) {\n\t\t\tif (SkipCaption(file, way)) {\n\t\t\t\tfile.caption = {};\n\t\t\t}\n\t\t}\n\n\t\tAssert(_list.filesToProcess.empty());\n\n\t\tif (_sendAsVoice && _sendAsVoice->checked()) {\n\t\t\tway.asVoice = true;\n\t\t}\n\t\tauto groups = DivideByGroups(\n\t\t\tstd::move(_list),\n\t\t\tway,\n\t\t\t(_limits & SendFilesAllow::OnlyOne));\n\t\tauto bundle = PrepareFilesBundle(\n\t\t\tstd::move(groups),\n\t\t\tway,\n\t\t\tctrlShiftEnter);\n\n\t\tif (!bundle->groups.empty()) {\n\t\t\tauto &group = bundle->groups.back();\n\t\t\tauto &files = group.list.files;\n\t\t\tif (!files.empty()) {\n\t\t\t\tauto &captioned = (group.type == Ui::AlbumType::PhotoVideo)\n\t\t\t\t\t? files.front()\n\t\t\t\t\t: files.back();\n\t\t\t\tif (!captioned.isSticker() || way.sendImagesAsPhotos()) {\n\t\t\t\t\tcaptioned.caption = std::move(caption);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t_confirmedCallback(std::move(bundle), options);\n\t}\n\tcloseBox();\n}\n\nFn<void(Api::SendOptions)> SendFilesBox::sendCallback() {\n\treturn crl::guard(this, [=](Api::SendOptions options) {\n\t\tsend(options, false);\n\t});\n}\n\nSendFilesBox::~SendFilesBox() = default;\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/send_files_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n#include \"ui/layers/box_content.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/chat/attach/attach_send_files_way.h\"\n\nnamespace style {\nstruct ComposeControls;\n} // namespace style\n\nnamespace Ui::Text {\nstruct MarkedContext;\n} // namespace Ui::Text\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Api {\nstruct SendOptions;\nenum class SendType;\n} // namespace Api\n\nnamespace ChatHelpers {\nclass TabbedPanel;\nclass Show;\nclass FieldAutocomplete;\n} // namespace ChatHelpers\n\nnamespace Ui {\nclass Checkbox;\nclass RoundButton;\nclass InputField;\nstruct GroupMediaLayout;\nstruct PreparedBundle;\nclass EmojiButton;\nclass AlbumPreview;\nclass VerticalLayout;\nclass FlatLabel;\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace SendMenu {\nstruct Details;\nstruct Action;\n} // namespace SendMenu\n\nnamespace HistoryView::Controls {\nclass CharactersLimitLabel;\nclass ComposeAiButton;\n} // namespace HistoryView::Controls\n\nenum class SendFilesAllow {\n\tOnlyOne = (1 << 0),\n\tPhotos = (1 << 1),\n\tVideos = (1 << 2),\n\tMusic = (1 << 3),\n\tFiles = (1 << 4),\n\tStickers = (1 << 5),\n\tGifs = (1 << 6),\n\tEmojiWithoutPremium = (1 << 7),\n\tTexts = (1 << 8),\n};\ninline constexpr bool is_flag_type(SendFilesAllow) { return true; }\nusing SendFilesLimits = base::flags<SendFilesAllow>;\n\nusing SendFilesCheck = Fn<bool(\n\tconst Ui::PreparedFile &file,\n\tbool compress,\n\tbool silent)>;\n\n[[nodiscard]] SendFilesLimits DefaultLimitsForPeer(not_null<PeerData*> peer);\n[[nodiscard]] SendFilesCheck DefaultCheckForPeer(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer);\n[[nodiscard]] SendFilesCheck DefaultCheckForPeer(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<PeerData*> peer);\n\nusing SendFilesConfirmed = Fn<void(\n\tstd::shared_ptr<Ui::PreparedBundle>,\n\tApi::SendOptions)>;\n\nstruct SendFilesBoxDescriptor {\n\tstd::shared_ptr<ChatHelpers::Show> show;\n\tUi::PreparedList list;\n\tTextWithTags caption;\n\tnot_null<PeerData*> toPeer;\n\tSendFilesLimits limits = {};\n\tSendFilesCheck check;\n\tApi::SendType sendType = {};\n\tFn<SendMenu::Details()> sendMenuDetails = nullptr;\n\tconst style::ComposeControls *stOverride = nullptr;\n\tSendFilesConfirmed confirmed;\n\tFn<void()> cancelled;\n};\n\nclass SendFilesBox : public Ui::BoxContent {\npublic:\n\tenum class SendLimit {\n\t\tOne,\n\t\tMany\n\t};\n\tSendFilesBox(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tUi::PreparedList &&list,\n\t\tconst TextWithTags &caption,\n\t\tnot_null<PeerData*> toPeer,\n\t\tApi::SendType sendType,\n\t\tSendMenu::Details sendMenuDetails);\n\tSendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor);\n\n\tvoid setConfirmedCallback(SendFilesConfirmed callback) {\n\t\t_confirmedCallback = std::move(callback);\n\t}\n\tvoid setCancelledCallback(Fn<void()> callback) {\n\t\t_cancelledCallback = std::move(callback);\n\t}\n\n\t[[nodiscard]] rpl::producer<TextWithTags> takeTextWithTagsRequests() const;\n\n\tvoid showFinished() override;\n\n\t~SendFilesBox();\n\nprotected:\n\tvoid prepare() override;\n\tvoid setInnerFocus() override;\n\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tusing MenuAction = SendMenu::Action;\n\tusing MenuDetails = SendMenu::Details;\n\n\tclass Block final {\n\tpublic:\n\t\tBlock(\n\t\t\tnot_null<QWidget*> parent,\n\t\t\tconst style::ComposeControls &st,\n\t\t\tnot_null<std::vector<Ui::PreparedFile>*> items,\n\t\t\tint from,\n\t\t\tint till,\n\t\t\tconst Ui::Text::MarkedContext &captionContext,\n\t\t\tFn<bool()> gifPaused,\n\t\t\tUi::SendFilesWay way);\n\t\tBlock(Block &&other) = default;\n\t\tBlock &operator=(Block &&other) = default;\n\n\t\t[[nodiscard]] int fromIndex() const;\n\t\t[[nodiscard]] int tillIndex() const;\n\t\t[[nodiscard]] object_ptr<Ui::RpWidget> takeWidget();\n\n\t\t[[nodiscard]] rpl::producer<int> itemDeleteRequest() const;\n\t\t[[nodiscard]] rpl::producer<int> itemReplaceRequest() const;\n\t\t[[nodiscard]] rpl::producer<int> itemModifyRequest() const;\n\t\t[[nodiscard]] rpl::producer<> orderUpdated() const;\n\n\t\tvoid setSendWay(Ui::SendFilesWay way);\n\t\tvoid toggleSpoilers(bool enabled);\n\t\tvoid applyChanges();\n\n\t\t[[nodiscard]] QImage generatePriceTagBackground() const;\n\t\t[[nodiscard]] bool setSingleFileDisplayName(\n\t\t\tconst QString &displayName);\n\t\t[[nodiscard]] bool setSingleFileCaption(\n\t\t\tint index,\n\t\t\tconst TextWithTags &caption);\n\n\tprivate:\n\t\tbase::unique_qptr<Ui::RpWidget> _preview;\n\t\tnot_null<std::vector<Ui::PreparedFile>*> _items;\n\t\tint _from = 0;\n\t\tint _till = 0;\n\t\tbool _isAlbum = false;\n\t\tbool _isSingleMedia = false;\n\n\t};\n\n\tvoid initSendWay();\n\tvoid initPreview();\n\t[[nodiscard]] bool hasSendMenu(const MenuDetails &details) const;\n\t[[nodiscard]] bool hasSpoilerMenu() const;\n\t[[nodiscard]] bool allWithSpoilers();\n\t[[nodiscard]] bool checkWithWay(\n\t\tUi::SendFilesWay way,\n\t\tbool silent = false) const;\n\t[[nodiscard]] bool checkWith(\n\t\tconst Ui::PreparedList &added,\n\t\tUi::SendFilesWay way,\n\t\tbool silent = false) const;\n\tvoid addMenuButton();\n\tvoid applyBlockChanges();\n\tvoid toggleSpoilers(bool enabled);\n\tvoid setSendLargePhotos(bool enabled);\n\tvoid changePrice();\n\n\t[[nodiscard]] bool canChangePrice() const;\n\t[[nodiscard]] bool hasPrice() const;\n\t[[nodiscard]] bool hasSendLargePhotosOption() const;\n\tvoid refreshPriceTag();\n\t[[nodiscard]] QImage preparePriceTagBg(QSize size) const;\n\n\tvoid refreshButtons();\n\tvoid refreshControls(bool initial = false);\n\tvoid setupSendWayControls();\n\tvoid setupCaption();\n\tvoid setupCaptionAutocomplete();\n\n\tvoid setupEmojiPanel();\n\tvoid updateSendWayControls();\n\tvoid updateEmojiPanelGeometry();\n\tvoid emojiFilterForGeometry(not_null<QEvent*> event);\n\n\tvoid preparePreview();\n\tvoid generatePreviewFrom(int fromBlock);\n\n\tvoid send(Api::SendOptions options, bool ctrlShiftEnter = false);\n\t[[nodiscard]] Fn<void(Api::SendOptions)> sendCallback();\n\tvoid captionResized();\n\tvoid saveSendWaySettings(bool rememberAll);\n\n\tvoid setupDragArea();\n\tvoid refreshTitleText();\n\tvoid updateBoxSize();\n\tvoid updateControlsGeometry();\n\tvoid updateCaptionVisibility();\n\n\tbool addFiles(not_null<const QMimeData*> data);\n\tbool addFiles(Ui::PreparedList list);\n\tvoid addFile(Ui::PreparedFile &&file);\n\tvoid pushBlock(int from, int till);\n\n\tvoid openDialogToAddFileToAlbum();\n\tvoid refreshAllAfterChanges(int fromItem, Fn<void()> perform = nullptr);\n\t[[nodiscard]] bool setDisplayNameInSingleFilePreview(\n\t\tint fileIndex,\n\t\tconst QString &displayName);\n\t[[nodiscard]] bool setCaptionInSingleFilePreview(\n\t\tint fileIndex,\n\t\tconst TextWithTags &caption);\n\n\tvoid enqueueNextPrepare();\n\tvoid addPreparedAsyncFile(Ui::PreparedFile &&file);\n\n\tvoid checkCharsLimitation();\n\tvoid refreshMessagesCount();\n\n\tvoid requestToTakeTextWithTags() const;\n\tbool validateLength(const QString &text) const;\n\n\t[[nodiscard]] Fn<MenuDetails()> prepareSendMenuDetails(\n\t\tconst SendFilesBoxDescriptor &descriptor);\n\t[[nodiscard]] auto prepareSendMenuCallback()\n\t\t-> Fn<void(MenuAction, MenuDetails)>;\n\n\t[[nodiscard]] TextWithTags fieldText() const;\n\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst style::ComposeControls &_st;\n\tconst Api::SendType _sendType = Api::SendType();\n\n\tQString _titleText;\n\trpl::variable<int> _titleHeight = 0;\n\n\tUi::PreparedList _list;\n\tstd::optional<int> _removingIndex;\n\trpl::variable<int> _messagesCount;\n\n\tSendFilesLimits _limits = {};\n\tFn<MenuDetails()> _sendMenuDetails;\n\tFn<void(MenuAction, MenuDetails)> _sendMenuCallback;\n\n\tnot_null<PeerData*> _toPeer;\n\tSendFilesCheck _check;\n\tSendFilesConfirmed _confirmedCallback;\n\tFn<void()> _cancelledCallback;\n\trpl::variable<uint64> _price = 0;\n\tstd::unique_ptr<Ui::RpWidget> _priceTag;\n\tQImage _priceTagBg;\n\tbool _confirmed = false;\n\tbool _invertCaption = false;\n\n\tconst object_ptr<Ui::InputField> _caption;\n\tstd::unique_ptr<ChatHelpers::FieldAutocomplete> _autocomplete;\n\tTextWithTags _prefilledCaptionText;\n\tobject_ptr<Ui::EmojiButton> _emojiToggle = { nullptr };\n\tHistoryView::Controls::ComposeAiButton *_aiButton = nullptr;\n\tbase::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;\n\tbase::unique_qptr<QObject> _emojiFilter;\n\tusing CharactersLimitLabel = HistoryView::Controls::CharactersLimitLabel;\n\tbase::unique_qptr<CharactersLimitLabel> _charsLimitation;\n\n\tobject_ptr<Ui::Checkbox> _groupFiles = { nullptr };\n\tobject_ptr<Ui::Checkbox> _sendImagesAsPhotos = { nullptr };\n\tobject_ptr<Ui::Checkbox> _wayRemember = { nullptr };\n\tobject_ptr<Ui::Checkbox> _sendAsVoice = { nullptr };\n\tobject_ptr<Ui::FlatLabel> _hintLabel = { nullptr };\n\trpl::variable<Ui::SendFilesWay> _sendWay = Ui::SendFilesWay();\n\n\trpl::variable<int> _footerHeight = 0;\n\trpl::lifetime _dimensionsLifetime;\n\n\tobject_ptr<Ui::ScrollArea> _scroll;\n\tQPointer<Ui::VerticalLayout> _inner;\n\tstd::deque<Block> _blocks;\n\tFn<void()> _whenReadySend;\n\tbool _preparing = false;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tQPointer<Ui::RoundButton> _send;\n\tQPointer<Ui::RoundButton> _addFile;\n\n\trpl::event_stream<TextWithTags> _textWithTagsRequests;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/send_gif_with_caption_box.h\"\n\n#include \"api/api_editing.h\"\n#include \"base/event_filter.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"chat_helpers/field_autocomplete.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_groups.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/controls/history_view_characters_limit.h\"\n#include \"history/view/controls/history_view_compose_ai_button.h\"\n#include \"history/view/history_view_message.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"media/clip/media_clip_reader.h\"\n#include \"menu/menu_checked_action.h\"\n#include \"menu/menu_send.h\"\n#include \"ui/controls/compose_ai_button_factory.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/controls/emoji_button_factory.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_entity.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Ui {\n\nvoid SetupCaptionFieldInBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::InputField*> field,\n\t\tPeerData *panelPeer,\n\t\tFn<bool(not_null<DocumentData*>)> allowWithoutPremium,\n\t\tPremiumFeature premiumFeature) {\n\tusing Limit = HistoryView::Controls::CharactersLimitLabel;\n\tstruct State final {\n\t\tbase::unique_qptr<ChatHelpers::TabbedPanel> emojiPanel;\n\t\tbase::unique_qptr<Limit> charsLimitation;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tconst auto container = box->getDelegate()->outerContainer();\n\tusing Selector = ChatHelpers::TabbedSelector;\n\tstate->emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(\n\t\tcontainer,\n\t\tcontroller,\n\t\tobject_ptr<Selector>(\n\t\t\tnullptr,\n\t\t\tcontroller->uiShow(),\n\t\t\tWindow::GifPauseReason::Layer,\n\t\t\tSelector::Mode::EmojiOnly));\n\tconst auto emojiPanel = state->emojiPanel.get();\n\temojiPanel->setDesiredHeightValues(\n\t\t1.,\n\t\tst::emojiPanMinHeight / 2,\n\t\tst::emojiPanMinHeight);\n\temojiPanel->hide();\n\temojiPanel->selector()->setCurrentPeer(\n\t\tpanelPeer ? panelPeer : controller->session().user());\n\temojiPanel->selector()->emojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\tUi::InsertEmojiAtCursor(field->textCursor(), data.emoji);\n\t}, field->lifetime());\n\temojiPanel->selector()->customEmojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\tconst auto info = data.document->sticker();\n\t\tif (info\n\t\t\t&& info->setType == Data::StickersType::Emoji\n\t\t\t&& !allowWithoutPremium(data.document)\n\t\t\t&& !controller->session().premium()) {\n\t\t\tShowPremiumPreviewBox(controller, premiumFeature);\n\t\t} else {\n\t\t\tData::InsertCustomEmoji(field, data.document);\n\t\t}\n\t}, field->lifetime());\n\n\tconst auto emojiButton = Ui::AddEmojiToggleToField(\n\t\tfield,\n\t\tbox,\n\t\tcontroller,\n\t\temojiPanel,\n\t\tst::sendGifWithCaptionEmojiPosition);\n\temojiButton->show();\n\n\tconst auto session = &controller->session();\n\tconst auto checkCharsLimitation = [=](auto repeat) -> void {\n\t\tconst auto remove = Ui::ComputeFieldCharacterCount(field)\n\t\t\t- Data::PremiumLimits(session).captionLengthCurrent();\n\t\tif (remove > 0) {\n\t\t\tif (!state->charsLimitation) {\n\t\t\t\tstate->charsLimitation = base::make_unique_q<Limit>(\n\t\t\t\t\tfield,\n\t\t\t\t\temojiButton,\n\t\t\t\t\tstyle::al_top);\n\t\t\t\tstate->charsLimitation->show();\n\t\t\t\tData::AmPremiumValue(session) | rpl::on_next([=] {\n\t\t\t\t\trepeat(repeat);\n\t\t\t\t}, state->charsLimitation->lifetime());\n\t\t\t}\n\t\t\tstate->charsLimitation->setLeft(remove);\n\t\t\tstate->charsLimitation->show();\n\t\t} else {\n\t\t\tstate->charsLimitation = nullptr;\n\t\t}\n\t};\n\tfield->changes() | rpl::on_next([=] {\n\t\tcheckCharsLimitation(checkCharsLimitation);\n\t}, field->lifetime());\n}\n\nnamespace {\n\nstruct State final {\n\tstd::shared_ptr<Data::DocumentMedia> mediaView;\n\t::Media::Clip::ReaderPointer gif;\n\tstd::unique_ptr<Ui::SpoilerAnimation> spoiler;\n\tQImage firstFrame;\n\tQImage blurredFrame;\n\tbool hasSpoiler = false;\n\trpl::lifetime loadingLifetime;\n};\n\n[[nodiscard]] not_null<State*> AddGifWidget(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<DocumentData*> document,\n\t\tint width) {\n\tconst auto state = container->lifetime().make_state<State>();\n\tstate->mediaView = document->createMediaView();\n\tstate->mediaView->automaticLoad(Data::FileOriginSavedGifs(), nullptr);\n\tstate->mediaView->thumbnailWanted(Data::FileOriginSavedGifs());\n\tstate->mediaView->videoThumbnailWanted(Data::FileOriginSavedGifs());\n\n\tconst auto widget = container->add(\n\t\tUi::CreateSkipWidget(\n\t\t\tcontainer,\n\t\t\tdocument->dimensions.scaled(\n\t\t\t\twidth - rect::m::sum::h(st::boxRowPadding),\n\t\t\t\tstd::numeric_limits<int>::max(),\n\t\t\t\tQt::KeepAspectRatio).height()),\n\t\tst::boxRowPadding);\n\twidget->paintOn([=](QPainter &p) {\n\t\tif (state->hasSpoiler) {\n\t\t\tif (state->firstFrame.isNull()\n\t\t\t\t\t&& state->gif\n\t\t\t\t\t&& state->gif->ready()) {\n\t\t\t\tstate->firstFrame = state->gif->current(\n\t\t\t\t\t{ .frame = widget->size() },\n\t\t\t\t\tcrl::now());\n\t\t\t\tstate->blurredFrame = Images::BlurLargeImage(\n\t\t\t\t\tbase::duplicate(state->firstFrame),\n\t\t\t\t\t24);\n\t\t\t}\n\t\t\tif (!state->blurredFrame.isNull()) {\n\t\t\t\tp.drawImage(0, 0, state->blurredFrame);\n\t\t\t} else if (const auto thumb = state->mediaView->thumbnail()) {\n\t\t\t\tp.drawImage(\n\t\t\t\t\twidget->rect(),\n\t\t\t\t\tthumb->pixNoCache(\n\t\t\t\t\t\twidget->size() * style::DevicePixelRatio(),\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t.options = Images::Option::Blur,\n\t\t\t\t\t\t\t.outer = widget->size(),\n\t\t\t\t\t\t}).toImage());\n\t\t\t}\n\t\t\tif (!state->spoiler) {\n\t\t\t\tstate->spoiler = std::make_unique<Ui::SpoilerAnimation>(\n\t\t\t\t\t[=] { widget->update(); });\n\t\t\t}\n\t\t\tconst auto now = crl::now();\n\t\t\tconst auto index = state->spoiler->index(now, false);\n\t\t\tconst auto frame = Ui::DefaultImageSpoiler().frame(index);\n\t\t\tUi::FillSpoilerRect(p, widget->rect(), frame);\n\t\t\treturn;\n\t\t}\n\t\tif (state->gif && state->gif->started()) {\n\t\t\tp.drawImage(\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\tstate->gif->current({ .frame = widget->size() }, crl::now()));\n\t\t} else if (const auto thumb = state->mediaView->thumbnail()) {\n\t\t\tp.drawImage(\n\t\t\t\twidget->rect(),\n\t\t\t\tthumb->pixNoCache(\n\t\t\t\t\twidget->size() * style::DevicePixelRatio(),\n\t\t\t\t\t{ .outer = widget->size() }).toImage());\n\t\t} else if (const auto thumb = state->mediaView->thumbnailInline()) {\n\t\t\tp.drawImage(\n\t\t\t\twidget->rect(),\n\t\t\t\tthumb->pixNoCache(\n\t\t\t\t\twidget->size() * style::DevicePixelRatio(),\n\t\t\t\t\t{\n\t\t\t\t\t\t.options = Images::Option::Blur,\n\t\t\t\t\t\t.outer = widget->size(),\n\t\t\t\t\t}).toImage());\n\t\t}\n\t});\n\n\tconst auto updateThumbnail = [=] {\n\t\tif (document->dimensions.isEmpty()) {\n\t\t\treturn false;\n\t\t}\n\t\tif (!state->mediaView->loaded()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto callback = [=](::Media::Clip::Notification) {\n\t\t\tif (state->gif && state->gif->ready() && !state->gif->started()) {\n\t\t\t\tstate->gif->start({ .frame = widget->size() });\n\t\t\t}\n\t\t\twidget->update();\n\t\t};\n\t\tstate->gif = ::Media::Clip::MakeReader(\n\t\t\tstate->mediaView->owner()->location(),\n\t\t\tstate->mediaView->bytes(),\n\t\t\tcallback);\n\t\treturn true;\n\t};\n\tif (!updateThumbnail()) {\n\t\tdocument->session().downloaderTaskFinished(\n\t\t) | rpl::on_next([=] {\n\t\t\tif (updateThumbnail()) {\n\t\t\t\tstate->loadingLifetime.destroy();\n\t\t\t\twidget->update();\n\t\t\t}\n\t\t}, state->loadingLifetime);\n\t}\n\n\tbase::install_event_filter(widget, [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::ContextMenu) {\n\t\t\tconst auto menu = Ui::CreateChild<Ui::PopupMenu>(\n\t\t\t\twidget,\n\t\t\t\tst::popupMenuWithIcons);\n\t\t\t::Menu::AddCheckedAction(\n\t\t\t\tmenu,\n\t\t\t\ttr::lng_context_spoiler_effect(tr::now),\n\t\t\t\t[=] {\n\t\t\t\t\tstate->hasSpoiler = !state->hasSpoiler;\n\t\t\t\t\tif (!state->hasSpoiler) {\n\t\t\t\t\t\tstate->spoiler = nullptr;\n\t\t\t\t\t\tstate->firstFrame = QImage();\n\t\t\t\t\t\tstate->blurredFrame = QImage();\n\t\t\t\t\t\tif (state->gif && state->gif->ready()) {\n\t\t\t\t\t\t\tstate->gif->start({ .frame = widget->size() });\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\twidget->update();\n\t\t\t\t},\n\t\t\t\t&st::menuIconSpoiler,\n\t\t\t\tstate->hasSpoiler);\n\t\t\tmenu->popup(QCursor::pos());\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\treturn state;\n}\n\n[[nodiscard]] not_null<Ui::InputField*> AddInputField(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto bottomContainer = box->setPinnedToBottomContent(\n\t\tobject_ptr<Ui::VerticalLayout>(box));\n\tconst auto wrap = bottomContainer->add(\n\t\tobject_ptr<Ui::RpWidget>(box),\n\t\tst::boxRowPadding);\n\tconst auto input = Ui::CreateChild<Ui::InputField>(\n\t\twrap,\n\t\tst::defaultComposeFiles.caption,\n\t\tUi::InputField::Mode::MultiLine,\n\t\ttr::lng_photo_caption());\n\tUi::ResizeFitChild(wrap, input);\n\tSetupCaptionFieldInBox(\n\t\tbox,\n\t\tcontroller,\n\t\tinput,\n\t\tcontroller->session().user(),\n\t\t[](not_null<DocumentData*>) {\n\t\t\treturn false;\n\t\t},\n\t\tPremiumFeature::AnimatedEmoji);\n\n\treturn input;\n}\n\nvoid CaptionBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\trpl::producer<QString> confirmText,\n\t\tTextWithTags initialText,\n\t\tnot_null<PeerData*> peer,\n\t\tconst SendMenu::Details &details,\n\t\tFn<void(Api::SendOptions, TextWithTags)> done) {\n\tconst auto window = Core::App().findWindow(box);\n\tconst auto controller = window ? window->sessionController() : nullptr;\n\tif (!controller) {\n\t\treturn;\n\t}\n\tbox->setWidth(st::boxWidth);\n\tbox->getDelegate()->setStyle(st::sendGifBox);\n\n\tconst auto input = AddInputField(box, controller);\n\tbox->setFocusCallback([=] {\n\t\tinput->setFocus();\n\t});\n\n\tinput->setTextWithTags(std::move(initialText));\n\tinput->setSubmitSettings(Core::App().settings().sendSubmitWay());\n\tconst auto chatStyle = InitMessageField(\n\t\tcontroller,\n\t\tinput,\n\t\t[=](not_null<DocumentData*>) { return true; });\n\n\tconst auto aiButton = Ui::SetupCaptionAiButton({\n\t\t.parent = input->parentWidget(),\n\t\t.field = input,\n\t\t.session = &controller->session(),\n\t\t.show = controller->uiShow(),\n\t\t.chatStyle = chatStyle,\n\t});\n\trpl::combine(\n\t\tbox->sizeValue(),\n\t\tinput->geometryValue()\n\t) | rpl::on_next([=](QSize, QRect) {\n\t\tUi::UpdateCaptionAiButtonGeometry(aiButton, input);\n\t\taiButton->raise();\n\t}, aiButton->lifetime());\n\n\tconst auto sendMenuDetails = [=] { return details; };\n\tstruct Autocomplete {\n\t\tstd::unique_ptr<ChatHelpers::FieldAutocomplete> dropdown;\n\t\tbool geometryUpdateScheduled = false;\n\t};\n\tconst auto autocomplete = box->lifetime().make_state<Autocomplete>();\n\tconst auto outer = box->getDelegate()->outerContainer();\n\tChatHelpers::InitFieldAutocomplete(autocomplete->dropdown, {\n\t\t.parent = outer,\n\t\t.show = controller->uiShow(),\n\t\t.field = input,\n\t\t.peer = peer,\n\t\t.features = [=] {\n\t\t\tauto result = ChatHelpers::ComposeFeatures();\n\t\t\tresult.autocompleteCommands = false;\n\t\t\tresult.suggestStickersByEmoji = false;\n\t\t\treturn result;\n\t\t},\n\t\t.sendMenuDetails = sendMenuDetails,\n\t});\n\tconst auto raw = autocomplete->dropdown.get();\n\tconst auto recountPostponed = [=] {\n\t\tif (autocomplete->geometryUpdateScheduled) {\n\t\t\treturn;\n\t\t}\n\t\tautocomplete->geometryUpdateScheduled = true;\n\t\tUi::PostponeCall(raw, [=] {\n\t\t\tautocomplete->geometryUpdateScheduled = false;\n\n\t\t\tconst auto from = input->parentWidget();\n\t\t\tauto field = Ui::MapFrom(outer, from, input->geometry());\n\t\t\tconst auto &st = st::defaultComposeFiles;\n\t\t\tautocomplete->dropdown->setBoundings(QRect(\n\t\t\t\tfield.x() - input->x(),\n\t\t\t\tst::defaultBox.margin.top(),\n\t\t\t\tinput->width(),\n\t\t\t\t(field.y()\n\t\t\t\t\t+ st.caption.textMargins.top()\n\t\t\t\t\t+ st.caption.placeholderShift\n\t\t\t\t\t+ st.caption.placeholderFont->height\n\t\t\t\t\t- st::defaultBox.margin.top())));\n\t\t});\n\t};\n\tfor (auto w = (QWidget*)input; w; w = w->parentWidget()) {\n\t\tbase::install_event_filter(raw, w, [=](not_null<QEvent*> e) {\n\t\t\tif (e->type() == QEvent::Move || e->type() == QEvent::Resize) {\n\t\t\t\trecountPostponed();\n\t\t\t}\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t});\n\t\tif (w == outer) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst auto send = [=, show = controller->uiShow()](\n\t\t\tApi::SendOptions options) {\n\t\tconst auto textWithTags = input->getTextWithTags();\n\t\tconst auto remove = Ui::ComputeFieldCharacterCount(input)\n\t\t\t- Data::PremiumLimits(&show->session()).captionLengthCurrent();\n\t\tif (remove > 0) {\n\t\t\tshow->showToast(\n\t\t\t\ttr::lng_edit_limit_reached(tr::now, lt_count, remove));\n\t\t\treturn;\n\t\t}\n\t\tdone(std::move(options), textWithTags);\n\t};\n\tconst auto confirm = box->addButton(\n\t\tstd::move(confirmText),\n\t\t[=] { send({}); });\n\tSendMenu::SetupMenuAndShortcuts(\n\t\tconfirm,\n\t\tcontroller->uiShow(),\n\t\tsendMenuDetails,\n\t\tSendMenu::DefaultCallback(controller->uiShow(), send));\n\tbox->setShowFinishedCallback([=] {\n\t\tif (const auto raw = autocomplete->dropdown.get()) {\n\t\t\tInvokeQueued(raw, [=] {\n\t\t\t\traw->raise();\n\t\t\t});\n\t\t}\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n\tinput->submits(\n\t) | rpl::on_next([=] { send({}); }, input->lifetime());\n}\n\n} // namespace\n\nvoid SendGifWithCaptionBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<DocumentData*> document,\n\t\tnot_null<PeerData*> peer,\n\t\tconst SendMenu::Details &details,\n\t\tFn<void(Api::SendOptions, TextWithTags)> c) {\n\tbox->setTitle(tr::lng_send_gif_with_caption());\n\tconst auto state = AddGifWidget(\n\t\tbox->verticalLayout(),\n\t\tdocument,\n\t\tst::boxWidth);\n\tUi::AddSkip(box->verticalLayout());\n\tconst auto d = [=](Api::SendOptions o, TextWithTags t) {\n\t\to.mediaSpoiler = state->hasSpoiler;\n\t\tdocument->owner().stickers().notifyGifWithCaptionSent();\n\t\tc(std::move(o), std::move(t));\n\t};\n\tCaptionBox(box, tr::lng_send_button(), {}, peer, details, std::move(d));\n}\n\nvoid EditCaptionBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<HistoryView::Element*> view) {\n\tusing namespace TextUtilities;\n\n\tbox->setTitle(tr::lng_context_upload_edit_caption());\n\n\tconst auto data = &view->data()->history()->peer->owner();\n\n\tstruct State {\n\t\tFullMsgId fullId;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tstate->fullId = view->data()->fullId();\n\n\tdata->itemIdChanged(\n\t) | rpl::on_next([=](Data::Session::IdChange event) {\n\t\tif (event.oldId == state->fullId.msg) {\n\t\t\tstate->fullId = event.newId;\n\t\t}\n\t}, box->lifetime());\n\n\tauto done = [=, show = box->uiShow()](\n\t\t\tApi::SendOptions,\n\t\t\tTextWithTags textWithTags) {\n\t\tconst auto item = data->message(state->fullId);\n\t\tif (!item) {\n\t\t\tshow->showToast(tr::lng_message_not_found(tr::now));\n\t\t\treturn;\n\t\t}\n\t\tif (!(item->media() && item->media()->allowsEditCaption())) {\n\t\t\tshow->showToast(tr::lng_edit_error(tr::now));\n\t\t\treturn;\n\t\t}\n\t\tconst auto textLength = textWithTags.text.size();\n\t\tconst auto limit = Data::PremiumLimits(\n\t\t\t&item->history()->session()).captionLengthCurrent();\n\t\tif (textLength > limit) {\n\t\t\tshow->showToast(tr::lng_edit_limit_reached(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\ttextLength - limit));\n\t\t\treturn;\n\t\t}\n\t\tauto text = TextWithEntities{\n\t\t\tstd::move(textWithTags.text),\n\t\t\tConvertTextTagsToEntities(std::move(textWithTags.tags)),\n\t\t};\n\t\tif (item->isUploading()) {\n\t\t\titem->setText(std::move(text));\n\t\t\tdata->requestViewResize(view);\n\t\t\tif (item->groupId()) {\n\t\t\t\tdata->groups().refreshMessage(item, true);\n\t\t\t}\n\t\t\tbox->closeBox();\n\t\t} else {\n\t\t\tApi::EditCaption(\n\t\t\t\titem,\n\t\t\t\tstd::move(text),\n\t\t\t\t{ .invertCaption = item->invertMedia() },\n\t\t\t\t[=] { box->closeBox(); },\n\t\t\t\t[=](const QString &e) { box->uiShow()->showToast(e); });\n\t\t}\n\t};\n\n\tconst auto item = view->data();\n\tCaptionBox(\n\t\tbox,\n\t\ttr::lng_settings_save(),\n\t\tTextWithTags{\n\t\t\t.text = item->originalText().text,\n\t\t\t.tags = ConvertEntitiesToTextTags(item->originalText().entities),\n\t\t},\n\t\titem->history()->peer,\n\t\t{},\n\t\tstd::move(done));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/send_gif_with_caption_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\nclass DocumentData;\nenum class PremiumFeature;\n\nnamespace Api {\nstruct SendOptions;\n} // namespace Api\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nnamespace HistoryView {\nclass Element;\n} // namespace HistoryView\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\n\nclass GenericBox;\nclass InputField;\n\nvoid SetupCaptionFieldInBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::InputField*> field,\n\tPeerData *panelPeer,\n\tFn<bool(not_null<DocumentData*>)> allowWithoutPremium,\n\tPremiumFeature premiumFeature);\n\nvoid EditCaptionBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<HistoryView::Element*> view);\n\nvoid SendGifWithCaptionBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<DocumentData*> document,\n\tnot_null<PeerData*> peer,\n\tconst SendMenu::Details &details,\n\tFn<void(Api::SendOptions, TextWithTags)> done);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/share_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/share_box.h\"\n\n#include \"api/api_premium.h\"\n#include \"api/api_sending.h\"\n#include \"api/api_as_copy.h\"\n#include \"base/random.h\"\n#include \"lang/lang_keys.h\"\n#include \"base/qthelp_url.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"storage/storage_account.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"apiwrap.h\"\n#include \"ui/widgets/chat_filters_tabs_strip.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/multi_select.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"menu/menu_check_item.h\"\n#include \"menu/menu_send.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_context_menu.h\" // CopyPostLink.\n#include \"settings/sections/settings_premium.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"chat_helpers/share_message_phrase_factory.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_game.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_user.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_changes.h\"\n#include \"main/main_session.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_chat_helpers.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QClipboard>\n\nclass ShareBox::Inner final : public Ui::RpWidget {\npublic:\n\tInner(\n\t\tQWidget *parent,\n\t\tconst Descriptor &descriptor,\n\t\tstd::shared_ptr<Ui::Show> show);\n\n\tvoid setPeerSelectedChangedCallback(\n\t\tFn<void(not_null<Data::Thread*> thread, bool selected)> callback);\n\tvoid peerUnselected(not_null<PeerData*> peer);\n\n\t[[nodiscard]] std::vector<not_null<Data::Thread*>> selected() const;\n\t[[nodiscard]] bool hasSelected() const;\n\n\tvoid peopleReceived(\n\t\tconst QString &query,\n\t\tconst QVector<MTPPeer> &my,\n\t\tconst QVector<MTPPeer> &people);\n\n\tvoid activateSkipRow(int direction);\n\tvoid activateSkipColumn(int direction);\n\tvoid activateSkipPage(int pageHeight, int direction);\n\tvoid updateFilter(QString filter = QString());\n\t[[nodiscard]] bool isFilterEmpty() const;\n\tvoid selectActive();\n\n\trpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\trpl::producer<> searchRequests() const;\n\n\tvoid applyChatFilter(FilterId id);\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tstruct Chat {\n\t\tChat(\n\t\t\tnot_null<History*> history,\n\t\t\tconst style::PeerListItem &st,\n\t\t\tFn<void()> updateCallback);\n\n\t\tnot_null<History*> history;\n\t\tnot_null<PeerData*> peer;\n\t\tData::ForumTopic *topic = nullptr;\n\t\tData::SavedSublist *sublist = nullptr;\n\t\trpl::lifetime topicLifetime;\n\t\trpl::lifetime sublistLifetime;\n\t\tUi::RoundImageCheckbox checkbox;\n\t\tUi::Text::String name;\n\t\tUi::Animations::Simple nameActive;\n\t\tApi::MessageMoneyRestriction restriction;\n\t\tRestrictionBadgeCache badgeCache;\n\t};\n\n\tvoid invalidateCache();\n\tbool showLockedError(not_null<Chat*> chat);\n\tvoid refreshRestrictedRows();\n\n\t[[nodiscard]] int displayedChatsCount() const;\n\t[[nodiscard]] not_null<Data::Thread*> chatThread(\n\t\tnot_null<Chat*> chat) const;\n\n\tvoid paintChat(Painter &p, not_null<Chat*> chat, int index);\n\tvoid updateChat(not_null<PeerData*> peer);\n\tvoid updateChatName(not_null<Chat*> chat);\n\tvoid initChatRestriction(not_null<Chat*> chat);\n\tvoid repaintChat(not_null<PeerData*> peer);\n\tint chatIndex(not_null<PeerData*> peer) const;\n\tvoid repaintChatAtIndex(int index);\n\tChat *getChatAtIndex(int index);\n\n\tvoid loadProfilePhotos();\n\tvoid preloadUserpic(not_null<Dialogs::Entry*> entry);\n\tvoid changeCheckState(Chat *chat);\n\tvoid chooseForumTopic(not_null<Data::Forum*> forum);\n\tvoid chooseMonoforumSublist(not_null<Data::SavedMessages*> monoforum);\n\tenum class ChangeStateWay {\n\t\tDefault,\n\t\tSkipCallback,\n\t};\n\tvoid changePeerCheckState(\n\t\tnot_null<Chat*> chat,\n\t\tbool checked,\n\t\tChangeStateWay useCallback = ChangeStateWay::Default);\n\n\tnot_null<Chat*> getChat(not_null<Dialogs::Row*> row);\n\tvoid setActive(int active);\n\tvoid updateUpon(const QPoint &pos);\n\n\tvoid refresh();\n\n\tconst Descriptor &_descriptor;\n\tconst std::shared_ptr<Ui::Show> _show;\n\tconst style::PeerList &_st;\n\n\tfloat64 _columnSkip = 0.;\n\tfloat64 _rowWidthReal = 0.;\n\tint _rowsLeft = 0;\n\tint _rowsTop = 0;\n\tint _rowWidth = 0;\n\tint _rowHeight = 0;\n\tint _columnCount = 4;\n\tint _active = -1;\n\tint _upon = -1;\n\tint _visibleTop = 0;\n\n\tstd::unique_ptr<Dialogs::IndexedList> _defaultChatsIndexed;\n\tstd::unique_ptr<Dialogs::IndexedList> _customChatsIndexed;\n\tnot_null<Dialogs::IndexedList*> _chatsIndexed;\n\tQString _filter;\n\tstd::vector<not_null<Dialogs::Row*>> _filtered;\n\n\tstd::map<not_null<PeerData*>, std::unique_ptr<Chat>> _dataMap;\n\tbase::flat_set<not_null<Data::Thread*>> _selected;\n\n\tFn<void(not_null<Data::Thread*>, bool)> _peerSelectedChangedCallback;\n\n\tbool _searching = false;\n\tQString _lastQuery;\n\tstd::vector<PeerData*> _byUsernameFiltered;\n\tstd::vector<std::unique_ptr<Chat>> d_byUsernameFiltered;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\trpl::event_stream<> _searchRequests;\n\n};\n\nShareBox::ShareBox(QWidget*, Descriptor &&descriptor)\n: _descriptor(std::move(descriptor))\n, _api(&_descriptor.session->mtp())\n, _select(\n\tthis,\n\t(_descriptor.st.multiSelect\n\t\t? *_descriptor.st.multiSelect\n\t\t: st::defaultMultiSelect),\n\ttr::lng_participant_filter())\n, _comment(\n\tthis,\n\tobject_ptr<Ui::InputField>(\n\t\tthis,\n\t\t(_descriptor.st.comment\n\t\t\t? *_descriptor.st.comment\n\t\t\t: st::shareComment),\n\t\tUi::InputField::Mode::MultiLine,\n\t\ttr::lng_photos_comment()),\n\tst::shareCommentPadding)\n, _bottomWidget(std::move(_descriptor.bottomWidget))\n, _copyLinkText(_descriptor.copyLinkText\n\t? std::move(_descriptor.copyLinkText)\n\t: tr::lng_share_copy_link())\n, _searchTimer([=] { searchByUsername(); }) {\n\tif (_bottomWidget) {\n\t\t_bottomWidget->setParent(this);\n\t\t_bottomWidget->resizeToWidth(st::boxWideWidth);\n\t\t_bottomWidget->show();\n\t}\n}\n\nvoid ShareBox::prepareCommentField() {\n\t_comment->hide(anim::type::instant);\n\n\trpl::combine(\n\t\theightValue(),\n\t\t_comment->heightValue(),\n\t\t(_bottomWidget\n\t\t\t? _bottomWidget->heightValue()\n\t\t\t: (rpl::single(0) | rpl::type_erased))\n\t) | rpl::on_next([=](int height, int comment, int bottom) {\n\t\t_comment->moveToLeft(0, height - bottom - comment);\n\t\tif (_bottomWidget) {\n\t\t\t_bottomWidget->moveToLeft(0, height - bottom);\n\t\t}\n\t}, _comment->lifetime());\n\n\tconst auto field = _comment->entity();\n\n\tfield->submits(\n\t) | rpl::on_next([=] {\n\t\tsubmit({});\n\t}, field->lifetime());\n\n\tif (const auto show = uiShow(); show->valid()) {\n\t\tInitMessageFieldHandlers({\n\t\t\t.session = _descriptor.session,\n\t\t\t.show = Main::MakeSessionShow(show, _descriptor.session),\n\t\t\t.field = field,\n\t\t\t.fieldStyle = _descriptor.st.label,\n\t\t});\n\t}\n\tfield->setSubmitSettings(Core::App().settings().sendSubmitWay());\n\n\tfield->changes() | rpl::on_next([=] {\n\t\tif (!field->getLastText().isEmpty()) {\n\t\t\tsetCloseByOutsideClick(false);\n\t\t} else if (_inner->selected().empty()) {\n\t\t\tsetCloseByOutsideClick(true);\n\t\t}\n\t}, field->lifetime());\n\n\tUi::SendPendingMoveResizeEvents(_comment);\n\tif (_bottomWidget) {\n\t\tUi::SendPendingMoveResizeEvents(_bottomWidget);\n\t}\n}\n\nvoid ShareBox::prepare() {\n\tprepareCommentField();\n\n\t_select->resizeToWidth(st::boxWideWidth);\n\tUi::SendPendingMoveResizeEvents(_select);\n\n\tsetTitle(_descriptor.titleOverride\n\t\t? std::move(_descriptor.titleOverride)\n\t\t: tr::lng_share_title());\n\n\t_inner = setInnerWidget(\n\t\tobject_ptr<Inner>(this, _descriptor, uiShow()),\n\t\tgetTopScrollSkip(),\n\t\tgetBottomScrollSkip());\n\n\tcreateButtons();\n\n\tsetDimensions(st::boxWideWidth, st::boxMaxListHeight);\n\n\t_select->setQueryChangedCallback([=](const QString &query) {\n\t\tapplyFilterUpdate(query);\n\t\tif (_chatsFilters) {\n\t\t\tupdateScrollSkips();\n\t\t\tscrollToY(0);\n\t\t}\n\t});\n\t_select->setItemRemovedCallback([=](uint64 itemId) {\n\t\tif (const auto peer = _descriptor.session->data().peerLoaded(PeerId(itemId))) {\n\t\t\t_inner->peerUnselected(peer);\n\t\t\tselectedChanged();\n\t\t\tupdate();\n\t\t}\n\t});\n\t_select->setResizedCallback([=] { updateScrollSkips(); });\n\t_select->setSubmittedCallback([=](Qt::KeyboardModifiers modifiers) {\n\t\tif (modifiers.testFlag(Qt::ControlModifier)\n\t\t\t|| modifiers.testFlag(Qt::MetaModifier)) {\n\t\t\tsubmit({});\n\t\t} else {\n\t\t\t_inner->selectActive();\n\t\t}\n\t});\n\trpl::combine(\n\t\t_comment->heightValue(),\n\t\t(_bottomWidget\n\t\t\t? _bottomWidget->heightValue()\n\t\t\t: rpl::single(0) | rpl::type_erased)\n\t) | rpl::on_next([=] {\n\t\tupdateScrollSkips();\n\t}, _comment->lifetime());\n\n\t_inner->searchRequests(\n\t) | rpl::on_next([=] {\n\t\tneedSearchByUsername();\n\t}, _inner->lifetime());\n\n\t_inner->scrollToRequests(\n\t) | rpl::on_next([=](const Ui::ScrollToRequest &request) {\n\t\tscrollTo(request);\n\t}, _inner->lifetime());\n\n\t_inner->setPeerSelectedChangedCallback([=](\n\t\t\tnot_null<Data::Thread*> thread,\n\t\t\tbool checked) {\n\t\tinnerSelectedChanged(thread, checked);\n\t\tif (checked) {\n\t\t\tsetCloseByOutsideClick(false);\n\t\t} else if (_inner->selected().empty()\n\t\t\t&& _comment->entity()->getLastText().isEmpty()) {\n\t\t\tsetCloseByOutsideClick(true);\n\t\t}\n\t});\n\n\tUi::Emoji::SuggestionsController::Init(\n\t\tgetDelegate()->outerContainer(),\n\t\t_comment->entity(),\n\t\t_descriptor.session,\n\t\t{ .suggestCustomEmoji = true });\n\n\t_select->raise();\n\n\t{\n\t\tconst auto chatsFilters = AddChatFiltersTabsStrip(\n\t\t\tthis,\n\t\t\t_descriptor.session,\n\t\t\t[this](FilterId id) {\n\t\t\t\t_inner->applyChatFilter(id);\n\t\t\t\tscrollToY(0);\n\t\t\t},\n\t\t\tWindow::GifPauseReason::Layer);\n\t\tchatsFilters->lower();\n\t\tchatsFilters->heightValue() | rpl::on_next([this](int h) {\n\t\t\tupdateScrollSkips();\n\t\t\tscrollToY(0);\n\t\t}, lifetime());\n\t\t_select->heightValue() | rpl::on_next([=](int h) {\n\t\t\tchatsFilters->moveToLeft(0, h);\n\t\t}, chatsFilters->lifetime());\n\t\t_chatsFilters = chatsFilters;\n\t}\n}\n\nint ShareBox::getTopScrollSkip() const {\n\treturn (_select->isHidden() ? 0 : _select->height())\n\t\t+ ((_chatsFilters && _inner && _inner->isFilterEmpty())\n\t\t\t? _chatsFilters->height()\n\t\t\t: 0);\n}\n\nint ShareBox::getBottomScrollSkip() const {\n\treturn (_comment->isHidden() ? 0 : _comment->height())\n\t\t+ (_bottomWidget ? _bottomWidget->height() : 0);\n}\n\nint ShareBox::contentHeight() const {\n\treturn height() - getTopScrollSkip() - getBottomScrollSkip();\n}\n\nvoid ShareBox::updateScrollSkips() {\n\tsetInnerTopSkip(getTopScrollSkip(), true);\n\tsetInnerBottomSkip(getBottomScrollSkip());\n}\n\nbool ShareBox::searchByUsername(bool searchCache) {\n\tauto query = _select->getQuery();\n\tif (query.isEmpty()) {\n\t\tif (_peopleRequest) {\n\t\t\t_peopleRequest = 0;\n\t\t}\n\t\treturn true;\n\t}\n\tif (!query.isEmpty()) {\n\t\tif (searchCache) {\n\t\t\tauto i = _peopleCache.constFind(query);\n\t\t\tif (i != _peopleCache.cend()) {\n\t\t\t\t_peopleQuery = query;\n\t\t\t\t_peopleRequest = 0;\n\t\t\t\tpeopleDone(i.value(), 0);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (_peopleQuery != query) {\n\t\t\t_peopleQuery = query;\n\t\t\t_peopleFull = false;\n\t\t\t_peopleRequest = _api.request(MTPcontacts_Search(\n\t\t\t\tMTP_string(_peopleQuery),\n\t\t\t\tMTP_int(SearchPeopleLimit)\n\t\t\t)).done([=](const MTPcontacts_Found &result, mtpRequestId requestId) {\n\t\t\t\tpeopleDone(result, requestId);\n\t\t\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\t\t\tpeopleFail(error, requestId);\n\t\t\t}).send();\n\t\t\t_peopleQueries.insert(_peopleRequest, _peopleQuery);\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid ShareBox::needSearchByUsername() {\n\tif (!searchByUsername(true)) {\n\t\t_searchTimer.callOnce(AutoSearchTimeout);\n\t}\n}\n\nvoid ShareBox::peopleDone(\n\t\tconst MTPcontacts_Found &result,\n\t\tmtpRequestId requestId) {\n\tExpects(result.type() == mtpc_contacts_found);\n\n\tauto query = _peopleQuery;\n\n\tauto i = _peopleQueries.find(requestId);\n\tif (i != _peopleQueries.cend()) {\n\t\tquery = i.value();\n\t\t_peopleCache[query] = result;\n\t\t_peopleQueries.erase(i);\n\t}\n\n\tif (_peopleRequest == requestId) {\n\t\tswitch (result.type()) {\n\t\tcase mtpc_contacts_found: {\n\t\t\tauto &found = result.c_contacts_found();\n\t\t\t_descriptor.session->data().processUsers(found.vusers());\n\t\t\t_descriptor.session->data().processChats(found.vchats());\n\t\t\t_inner->peopleReceived(\n\t\t\t\tquery,\n\t\t\t\tfound.vmy_results().v,\n\t\t\t\tfound.vresults().v);\n\t\t} break;\n\t\t}\n\n\t\t_peopleRequest = 0;\n\t}\n}\n\nvoid ShareBox::peopleFail(const MTP::Error &error, mtpRequestId requestId) {\n\tif (_peopleRequest == requestId) {\n\t\t_peopleRequest = 0;\n\t\t_peopleFull = true;\n\t}\n}\n\nvoid ShareBox::setInnerFocus() {\n\tif (_comment->isHidden()) {\n\t\t_select->setInnerFocus();\n\t} else {\n\t\t_comment->entity()->setFocusFast();\n\t}\n}\n\nvoid ShareBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\t_select->resizeToWidth(width());\n\t_select->moveToLeft(0, 0);\n\n\tupdateScrollSkips();\n\n\t_inner->resizeToWidth(width());\n}\n\nvoid ShareBox::keyPressEvent(QKeyEvent *e) {\n\tauto focused = focusWidget();\n\tif (_select == focused || _select->isAncestorOf(focusWidget())) {\n\t\tif (e->key() == Qt::Key_Up) {\n\t\t\t_inner->activateSkipColumn(-1);\n\t\t} else if (e->key() == Qt::Key_Down) {\n\t\t\t_inner->activateSkipColumn(1);\n\t\t} else if (e->key() == Qt::Key_PageUp) {\n\t\t\t_inner->activateSkipPage(contentHeight(), -1);\n\t\t} else if (e->key() == Qt::Key_PageDown) {\n\t\t\t_inner->activateSkipPage(contentHeight(), 1);\n\t\t} else {\n\t\t\tBoxContent::keyPressEvent(e);\n\t\t}\n\t} else {\n\t\tBoxContent::keyPressEvent(e);\n\t}\n}\n\nSendMenu::Details ShareBox::sendMenuDetails() const {\n\tconst auto selected = _inner->selected();\n\tconst auto hasPaid = [&] {\n\t\tfor (const auto &thread : selected) {\n\t\t\tif (thread->peer()->starsPerMessageChecked()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}();\n\tconst auto type = hasPaid\n\t\t? SendMenu::Type::SilentOnly\n\t\t: ranges::all_of(\n\t\t\tselected | ranges::views::transform(&Data::Thread::peer),\n\t\t\tHistoryView::CanScheduleUntilOnline)\n\t\t? SendMenu::Type::ScheduledToUser\n\t\t: (selected.size() == 1 && selected.front()->peer()->isSelf())\n\t\t? SendMenu::Type::Reminder\n\t\t: SendMenu::Type::Scheduled;\n\n\t// We can't support effect here because we don't have ChatHelpers::Show.\n\treturn { .type = type, .effectAllowed = false };\n}\n\nvoid ShareBox::showMenu(not_null<Ui::RpWidget*> parent) {\n\tif (_menu) {\n\t\t_menu = nullptr;\n\t\treturn;\n\t}\n\t_menu.emplace(parent, st::popupMenuWithIcons);\n\n\tif (_descriptor.forwardOptions.show) {\n\t\tauto createView = [&](rpl::producer<QString> &&text, bool checked) {\n\t\t\tauto item = base::make_unique_q<Menu::ItemWithCheck>(\n\t\t\t\t_menu->menu(),\n\t\t\t\tst::popupMenuWithIcons.menu,\n\t\t\t\tUi::CreateChild<QAction>(_menu->menu().get()),\n\t\t\t\tnullptr,\n\t\t\t\tnullptr);\n\t\t\tstd::move(\n\t\t\t\ttext\n\t\t\t) | rpl::on_next([action = item->action()](QString text) {\n\t\t\t\taction->setText(text);\n\t\t\t}, item->lifetime());\n\t\t\titem->init(checked);\n\t\t\tconst auto view = item->checkView();\n\t\t\t_menu->addAction(std::move(item));\n\t\t\treturn view;\n\t\t};\n\t\tUi::FillForwardOptions(\n\t\t\tstd::move(createView),\n\t\t\t_forwardOptions,\n\t\t\t[=](Ui::ForwardOptions value) { _forwardOptions = value; },\n\t\t\t_menu->lifetime());\n\n\t\t_menu->addSeparator();\n\t}\n\n\tusing namespace SendMenu;\n\tconst auto sendAction = crl::guard(this, [=](Action action, Details) {\n\t\tif (action.type == ActionType::Send) {\n\t\t\tsubmit(action.options);\n\t\t\treturn;\n\t\t}\n\t\tconst auto st = _descriptor.st.scheduleBox\n\t\t\t? *_descriptor.st.scheduleBox\n\t\t\t: HistoryView::ScheduleBoxStyleArgs();\n\t\tuiShow()->showBox(\n\t\t\tHistoryView::PrepareScheduleBox(\n\t\t\t\tthis,\n\t\t\t\t_descriptor.session,\n\t\t\t\tsendMenuDetails(),\n\t\t\t\t[=](Api::SendOptions options) { submit(options); },\n\t\t\t\taction.options,\n\t\t\t\tHistoryView::DefaultScheduleTime(),\n\t\t\t\tst));\n\t});\n\t_menu->setForcedVerticalOrigin(Ui::PopupMenu::VerticalOrigin::Bottom);\n\tconst auto result = FillSendMenu(\n\t\t_menu.get(),\n\t\tnullptr, // showForEffect.\n\t\tsendMenuDetails(),\n\t\tsendAction);\n\tif (result == SendMenu::FillMenuResult::Prepared) {\n\t\t_menu->popupPrepared();\n\t} else if (_descriptor.forwardOptions.show\n\t\t&& result != SendMenu::FillMenuResult::Failed) {\n\t\t_menu->popup(QCursor::pos());\n\t}\n}\n\nvoid ShareBox::createButtons() {\n\tconst auto asCopyShare = [=](bool emptyText, TimeId scheduled) {\n\t\tif (!_descriptor.asCopyCallback) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto selected = _inner->selected();\n\t\t_descriptor.asCopyCallback(\n\t\t\tranges::views::all(\n\t\t\t\tselected\n\t\t\t) | ranges::views::transform(\n\t\t\t\t&Data::Thread::peer\n\t\t\t) | ranges::to_vector,\n\t\t\t_comment->entity()->getTextWithAppliedMarkdown(),\n\t\t\temptyText,\n\t\t\tscheduled);\n\t\tcloseBox();\n\t};\n\tclearButtons();\n\tif (_hasSelected) {\n\t\tconst auto send = addButton(tr::lng_share_confirm(), [=] {\n\t\t\tsubmit({});\n\t\t});\n\t\t_forwardOptions.sendersCount\n\t\t\t= _descriptor.forwardOptions.sendersCount;\n\t\t_forwardOptions.captionsCount\n\t\t\t= _descriptor.forwardOptions.captionsCount;\n\n\t\tsend->setAcceptBoth();\n\t\tsend->clicks(\n\t\t) | rpl::on_next([=](Qt::MouseButton button) {\n\t\t\tif (button == Qt::RightButton) {\n\t\t\t\tshowMenu(send);\n\t\t\t}\n\t\t}, send->lifetime());\n\t\tsend->setText(PaidSendButtonText(\n\t\t\t_starsToSend.value(),\n\t\t\ttr::lng_share_confirm()));\n\t\taddButton(tr::lng_share_as_copy(), [=] { asCopyShare(false, 0); });\n\t\taddButton(tr::lng_share_as_copy_no_text(), [=] { asCopyShare(true, 0); });\n\t\taddTopButton(st::historyScheduledToggle, [=] {\n\t\t\tconst auto window = Core::App().findWindow(this);\n\t\t\tconst auto controller = window ? window->sessionController() : nullptr;\n\t\t\tif (!controller) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tuiShow()->show(\n\t\t\t\tHistoryView::PrepareScheduleBox(\n\t\t\t\t\tthis,\n\t\t\t\t\tcontroller->uiShow(),\n\t\t\t\t\tSendMenu::Details(),\n\t\t\t\t\t[=](Api::SendOptions options) { asCopyShare(false, options.scheduled); }));\n\t\t});\n\t} else if (_descriptor.copyCallback) {\n\t\taddButton(_copyLinkText.value(), [=] { copyLink(); });\n\t}\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n}\n\nvoid ShareBox::applyFilterUpdate(const QString &query) {\n\tscrollToY(0);\n\t_inner->updateFilter(query);\n}\n\nvoid ShareBox::addPeerToMultiSelect(not_null<Data::Thread*> thread) {\n\tauto addItemWay = Ui::MultiSelect::AddItemWay::Default;\n\tconst auto peer = thread->peer();\n\tconst auto topic = thread->asTopic();\n\tconst auto sublist = thread->asSublist();\n\t_select->addItem(\n\t\tpeer->id.value,\n\t\t(topic\n\t\t\t? topic->title()\n\t\t\t: sublist\n\t\t\t? sublist->sublistPeer()->shortName()\n\t\t\t: peer->isSelf()\n\t\t\t? tr::lng_saved_short(tr::now)\n\t\t\t: peer->shortName()),\n\t\tst::activeButtonBg,\n\t\t((topic || sublist)\n\t\t\t? ForceRoundUserpicCallback(peer)\n\t\t\t: PaintUserpicCallback(peer, true)),\n\t\taddItemWay);\n}\n\nvoid ShareBox::innerSelectedChanged(\n\t\tnot_null<Data::Thread*> thread,\n\t\tbool checked) {\n\tif (checked) {\n\t\taddPeerToMultiSelect(thread);\n\t\t_select->clearQuery();\n\t} else {\n\t\t_select->removeItem(thread->peer()->id.value);\n\t}\n\tselectedChanged();\n\tupdate();\n}\n\nvoid ShareBox::submit(Api::SendOptions options) {\n\t_submitLifetime.destroy();\n\n\tauto threads = _inner->selected();\n\tconst auto weak = base::make_weak(this);\n\tconst auto field = _comment->entity();\n\tauto comment = field->getTextWithAppliedMarkdown();\n\tconst auto checkPaid = [=] {\n\t\tif (!_descriptor.countMessagesCallback) {\n\t\t\treturn true;\n\t\t}\n\t\tconst auto withPaymentApproved = crl::guard(weak, [=](int approved) {\n\t\t\tauto copy = options;\n\t\t\tcopy.starsApproved = approved;\n\t\t\tsubmit(copy);\n\t\t});\n\t\tconst auto messagesCount = _descriptor.countMessagesCallback(\n\t\t\tcomment);\n\t\tconst auto alreadyApproved = options.starsApproved;\n\t\tauto paid = std::vector<not_null<PeerData*>>();\n\t\tauto waiting = base::flat_set<not_null<PeerData*>>();\n\t\tauto totalStars = 0;\n\t\tfor (const auto &thread : threads) {\n\t\t\tconst auto peer = thread->peer();\n\t\t\tconst auto details = ComputePaymentDetails(peer, messagesCount);\n\t\t\tif (!details) {\n\t\t\t\twaiting.emplace(peer);\n\t\t\t} else if (details->stars > 0) {\n\t\t\t\ttotalStars += details->stars;\n\t\t\t\tpaid.push_back(peer);\n\t\t\t}\n\t\t}\n\t\tif (!waiting.empty()) {\n\t\t\t_descriptor.session->changes().peerUpdates(\n\t\t\t\tData::PeerUpdate::Flag::FullInfo\n\t\t\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\t\t\tif (waiting.contains(update.peer)) {\n\t\t\t\t\twithPaymentApproved(alreadyApproved);\n\t\t\t\t}\n\t\t\t}, _submitLifetime);\n\n\t\t\tif (!_descriptor.session->credits().loaded()) {\n\t\t\t\t_descriptor.session->credits().loadedValue(\n\t\t\t\t) | rpl::filter(\n\t\t\t\t\trpl::mappers::_1\n\t\t\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t\t\twithPaymentApproved(alreadyApproved);\n\t\t\t\t}, _submitLifetime);\n\t\t\t}\n\t\t\treturn false;\n\t\t} else if (totalStars > alreadyApproved) {\n\t\t\tconst auto show = uiShow();\n\t\t\tconst auto session = _descriptor.session;\n\t\t\tconst auto sessionShow = Main::MakeSessionShow(show, session);\n\t\t\tconst auto scheduleBoxSt = _descriptor.st.scheduleBox.get();\n\t\t\tShowSendPaidConfirm(sessionShow, paid, SendPaymentDetails{\n\t\t\t\t.messages = messagesCount,\n\t\t\t\t.stars = totalStars,\n\t\t\t}, [=] { withPaymentApproved(totalStars); }, PaidConfirmStyles{\n\t\t\t\t.label = (scheduleBoxSt\n\t\t\t\t\t? scheduleBoxSt->chooseDateTimeArgs.labelStyle\n\t\t\t\t\t: nullptr),\n\t\t\t\t.checkbox = _descriptor.st.checkbox,\n\t\t\t});\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t};\n\tif (const auto onstack = _descriptor.submitCallback) {\n\t\tconst auto forwardOptions = (_forwardOptions.captionsCount\n\t\t\t&& _forwardOptions.dropCaptions)\n\t\t\t? Data::ForwardOptions::NoNamesAndCaptions\n\t\t\t: _forwardOptions.dropNames\n\t\t\t? Data::ForwardOptions::NoSenderNames\n\t\t\t: Data::ForwardOptions::PreserveInfo;\n\t\tonstack(\n\t\t\tstd::move(threads),\n\t\t\tcheckPaid,\n\t\t\tstd::move(comment),\n\t\t\toptions,\n\t\t\tforwardOptions);\n\t}\n}\n\nvoid ShareBox::copyLink() const {\n\tif (const auto onstack = _descriptor.copyCallback) {\n\t\tonstack();\n\t}\n}\n\nvoid ShareBox::selectedChanged() {\n\tauto hasSelected = _inner->hasSelected();\n\tif (_hasSelected != hasSelected) {\n\t\t_hasSelected = hasSelected;\n\t\tcreateButtons();\n\t\t_comment->toggle(_hasSelected, anim::type::normal);\n\t\t_comment->resizeToWidth(st::boxWideWidth);\n\t}\n\tcomputeStarsCount();\n\tupdate();\n}\n\nvoid ShareBox::computeStarsCount() {\n\tauto perMessage = 0;\n\tfor (const auto &thread : _inner->selected()) {\n\t\tperMessage += thread->peer()->starsPerMessageChecked();\n\t}\n\tconst auto messagesCount = _descriptor.countMessagesCallback\n\t\t? _descriptor.countMessagesCallback(_comment\n\t\t\t? _comment->entity()->getTextWithTags()\n\t\t\t: TextWithTags())\n\t\t: 0;\n\t_starsToSend = perMessage * messagesCount;\n}\n\nvoid ShareBox::scrollTo(Ui::ScrollToRequest request) {\n\tscrollToY(request.ymin, request.ymax);\n\t//auto scrollTop = scrollArea()->scrollTop(), scrollBottom = scrollTop + scrollArea()->height();\n\t//auto from = scrollTop, to = scrollTop;\n\t//if (scrollTop > top) {\n\t//\tto = top;\n\t//} else if (scrollBottom < bottom) {\n\t//\tto = bottom - (scrollBottom - scrollTop);\n\t//}\n\t//if (from != to) {\n\t//\t_scrollAnimation.start([this]() { scrollAnimationCallback(); }, from, to, st::shareScrollDuration, anim::sineInOut);\n\t//}\n}\n\nvoid ShareBox::scrollAnimationCallback() {\n\t//auto scrollTop = qRound(_scrollAnimation.current(scrollArea()->scrollTop()));\n\t//scrollArea()->scrollToY(scrollTop);\n}\n\nShareBox::Inner::Inner(\n\tQWidget *parent,\n\tconst Descriptor &descriptor,\n\tstd::shared_ptr<Ui::Show> show)\n: RpWidget(parent)\n, _descriptor(descriptor)\n, _show(std::move(show))\n, _st(_descriptor.st.peerList\n\t? *_descriptor.st.peerList\n\t: st::shareBoxList)\n, _defaultChatsIndexed(\n\tstd::make_unique<Dialogs::IndexedList>(\n\t\tDialogs::SortMode::Add))\n, _chatsIndexed(_defaultChatsIndexed.get()) {\n\t_rowsTop = st::shareRowsTop;\n\t_rowHeight = st::shareRowHeight;\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\n\tif (_descriptor.moneyRestrictionError) {\n\t\tconst auto session = _descriptor.session;\n\t\trpl::merge(\n\t\t\tData::AmPremiumValue(session) | rpl::to_empty,\n\t\t\tsession->api().premium().someMessageMoneyRestrictionsResolved()\n\t\t) | rpl::on_next([=] {\n\t\t\trefreshRestrictedRows();\n\t\t}, lifetime());\n\t}\n\n\tconst auto self = _descriptor.session->user();\n\tconst auto selfHistory = self->owner().history(self);\n\tif (_descriptor.filterCallback(selfHistory)) {\n\t\t_defaultChatsIndexed->addToEnd(selfHistory);\n\t}\n\tconst auto addList = [&](not_null<Dialogs::IndexedList*> list) {\n\t\tfor (const auto &row : list->all()) {\n\t\t\tif (const auto history = row->history()) {\n\t\t\t\tif (!history->peer->isSelf()\n\t\t\t\t\t&& (history->asForum()\n\t\t\t\t\t\t|| _descriptor.filterCallback(history))) {\n\t\t\t\t\t_defaultChatsIndexed->addToEnd(history);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\taddList(_descriptor.session->data().chatsList()->indexed());\n\tconst auto id = Data::Folder::kId;\n\tif (const auto folder = _descriptor.session->data().folderLoaded(id)) {\n\t\taddList(folder->chatsList()->indexed());\n\t}\n\taddList(_descriptor.session->data().contactsNoChatsList());\n\n\t_filter = u\"a\"_q;\n\tupdateFilter();\n\n\t_descriptor.session->changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::Photo\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tupdateChat(update.peer);\n\t}, lifetime());\n\n\t_descriptor.session->changes().realtimeNameUpdates(\n\t) | rpl::on_next([=](const Data::NameUpdate &update) {\n\t\t_defaultChatsIndexed->peerNameChanged(\n\t\t\tupdate.peer,\n\t\t\tupdate.oldFirstLetters);\n\t}, lifetime());\n\n\t_descriptor.session->downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tinvalidateCache();\n\t}, lifetime());\n}\n\nvoid ShareBox::Inner::invalidateCache() {\n\tfor (const auto &[peer, data] : _dataMap) {\n\t\tdata->checkbox.invalidateCache();\n\t}\n}\n\nbool ShareBox::Inner::showLockedError(not_null<Chat*> chat) {\n\tif (!chat->restriction.premiumRequired) {\n\t\treturn false;\n\t}\n\t::Settings::ShowPremiumPromoToast(\n\t\tMain::MakeSessionShow(_show, _descriptor.session),\n\t\tChatHelpers::ResolveWindowDefault(),\n\t\t_descriptor.moneyRestrictionError(chat->peer->asUser()).text,\n\t\tu\"require_premium\"_q);\n\treturn true;\n}\n\nvoid ShareBox::Inner::refreshRestrictedRows() {\n\tauto changed = false;\n\tfor (const auto &[peer, data] : _dataMap) {\n\t\tconst auto history = data->history;\n\t\tconst auto restriction = Api::ResolveMessageMoneyRestrictions(\n\t\t\thistory->peer,\n\t\t\thistory);\n\t\tif (data->restriction != restriction) {\n\t\t\tdata->restriction = restriction;\n\t\t\tchanged = true;\n\t\t}\n\t}\n\tfor (const auto &data : d_byUsernameFiltered) {\n\t\tconst auto history = data->history;\n\t\tconst auto restriction = Api::ResolveMessageMoneyRestrictions(\n\t\t\thistory->peer,\n\t\t\thistory);\n\t\tif (data->restriction != restriction) {\n\t\t\tdata->restriction = restriction;\n\t\t\tchanged = true;\n\t\t}\n\t}\n\tif (changed) {\n\t\tupdate();\n\t}\n}\n\nvoid ShareBox::Inner::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\t_visibleTop = visibleTop;\n\tloadProfilePhotos();\n}\n\nvoid ShareBox::Inner::activateSkipRow(int direction) {\n\tactivateSkipColumn(direction * _columnCount);\n}\n\nint ShareBox::Inner::displayedChatsCount() const {\n\treturn _filter.isEmpty() ? _chatsIndexed->size() : (_filtered.size() + d_byUsernameFiltered.size());\n}\n\nvoid ShareBox::Inner::activateSkipColumn(int direction) {\n\tif (_active < 0) {\n\t\tif (direction > 0) {\n\t\t\tsetActive(0);\n\t\t}\n\t\treturn;\n\t}\n\tauto count = displayedChatsCount();\n\tauto active = _active + direction;\n\tif (active < 0) {\n\t\tactive = (_active > 0) ? 0 : -1;\n\t}\n\tif (active >= count) {\n\t\tactive = count - 1;\n\t}\n\tsetActive(active);\n}\n\nvoid ShareBox::Inner::activateSkipPage(int pageHeight, int direction) {\n\tactivateSkipRow(direction * (pageHeight / _rowHeight));\n}\n\nvoid ShareBox::Inner::updateChat(not_null<PeerData*> peer) {\n\tif (const auto i = _dataMap.find(peer); i != end(_dataMap)) {\n\t\tupdateChatName(i->second.get());\n\t\trepaintChat(peer);\n\t}\n}\n\nvoid ShareBox::Inner::updateChatName(not_null<Chat*> chat) {\n\tconst auto peer = chat->peer;\n\tconst auto text = chat->topic\n\t\t? chat->topic->title()\n\t\t: chat->sublist\n\t\t? chat->sublist->sublistPeer()->name()\n\t\t: peer->isSelf()\n\t\t? tr::lng_saved_messages(tr::now)\n\t\t: peer->isRepliesChat()\n\t\t? tr::lng_replies_messages(tr::now)\n\t\t: peer->isVerifyCodes()\n\t\t? tr::lng_verification_codes(tr::now)\n\t\t: peer->name();\n\tchat->name.setText(_st.item.nameStyle, text, Ui::NameTextOptions());\n}\n\nvoid ShareBox::Inner::initChatRestriction(not_null<Chat*> chat) {\n\tif (_descriptor.moneyRestrictionError) {\n\t\tconst auto history = chat->history;\n\t\tconst auto restriction = Api::ResolveMessageMoneyRestrictions(\n\t\t\thistory->peer,\n\t\t\thistory);\n\t\tif (restriction || restriction.known) {\n\t\t\tchat->restriction = restriction;\n\t\t}\n\t}\n}\n\nvoid ShareBox::Inner::repaintChatAtIndex(int index) {\n\tif (index < 0) return;\n\n\tauto row = index / _columnCount;\n\tauto column = index % _columnCount;\n\tupdate(style::rtlrect(_rowsLeft + qFloor(column * _rowWidthReal), row * _rowHeight, _rowWidth, _rowHeight, width()));\n}\n\nShareBox::Inner::Chat *ShareBox::Inner::getChatAtIndex(int index) {\n\tif (index < 0) {\n\t\treturn nullptr;\n\t}\n\tconst auto row = [=] {\n\t\tif (_filter.isEmpty()) {\n\t\t\treturn (index < _chatsIndexed->size())\n\t\t\t\t? (_chatsIndexed->begin() + index)->get()\n\t\t\t\t: nullptr;\n\t\t}\n\t\treturn (index < _filtered.size())\n\t\t\t? _filtered[index].get()\n\t\t\t: nullptr;\n\t}();\n\tif (row) {\n\t\treturn static_cast<Chat*>(row->attached);\n\t}\n\n\tif (!_filter.isEmpty()) {\n\t\tindex -= _filtered.size();\n\t\tif (index >= 0 && index < d_byUsernameFiltered.size()) {\n\t\t\treturn d_byUsernameFiltered[index].get();\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid ShareBox::Inner::repaintChat(not_null<PeerData*> peer) {\n\trepaintChatAtIndex(chatIndex(peer));\n}\n\nint ShareBox::Inner::chatIndex(not_null<PeerData*> peer) const {\n\tint index = 0;\n\tif (_filter.isEmpty()) {\n\t\tfor (const auto &row : _chatsIndexed->all()) {\n\t\t\tif (const auto history = row->history()) {\n\t\t\t\tif (history->peer == peer) {\n\t\t\t\t\treturn index;\n\t\t\t\t}\n\t\t\t}\n\t\t\t++index;\n\t\t}\n\t} else {\n\t\tfor (const auto &row : _filtered) {\n\t\t\tif (const auto history = row->history()) {\n\t\t\t\tif (history->peer == peer) {\n\t\t\t\t\treturn index;\n\t\t\t\t}\n\t\t\t}\n\t\t\t++index;\n\t\t}\n\t\tfor (const auto &row : d_byUsernameFiltered) {\n\t\t\tif (row->peer == peer) {\n\t\t\t\treturn index;\n\t\t\t}\n\t\t\t++index;\n\t\t}\n\t}\n\treturn -1;\n}\n\nvoid ShareBox::Inner::loadProfilePhotos() {\n\tif (!parentWidget()) {\n\t\treturn;\n\t}\n\tauto yFrom = std::max(_visibleTop, 0);\n\tif (auto part = (yFrom % _rowHeight)) {\n\t\tyFrom -= part;\n\t}\n\tint yTo = yFrom + parentWidget()->height() * 5 * _columnCount;\n\tif (!yTo) {\n\t\treturn;\n\t}\n\tyFrom *= _columnCount;\n\tyTo *= _columnCount;\n\n\tif (_filter.isEmpty()) {\n\t\tif (!_chatsIndexed->empty()) {\n\t\t\tconst auto index = yFrom / _rowHeight;\n\t\t\tauto i = _chatsIndexed->begin()\n\t\t\t\t+ std::min(index, _chatsIndexed->size());\n\t\t\tfor (auto end = _chatsIndexed->cend(); i != end; ++i) {\n\t\t\t\tif (((*i)->index() * _rowHeight) >= yTo) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tpreloadUserpic((*i)->entry());\n\t\t\t}\n\t\t}\n\t} else {\n\t\tconst auto from = std::max(yFrom / _rowHeight, 0);\n\t\tconst auto to = std::max((yTo / _rowHeight) + 1, from);\n\n\t\tconst auto fto = std::min(to, int(_filtered.size()));\n\t\tconst auto ffrom = std::min(from, fto);\n\t\tfor (auto i = ffrom; i != fto; ++i) {\n\t\t\tpreloadUserpic(_filtered[i]->entry());\n\t\t}\n\n\t\tconst auto uto = std::min(\n\t\t\tto - int(_filtered.size()),\n\t\t\tint(d_byUsernameFiltered.size()));\n\t\tconst auto ufrom = std::min(\n\t\t\tstd::max(from - int(_filtered.size()), 0),\n\t\t\tuto);\n\t\tfor (auto i = ufrom; i != uto; ++i) {\n\t\t\tpreloadUserpic(d_byUsernameFiltered[i]->history);\n\t\t}\n\t}\n}\n\nvoid ShareBox::Inner::preloadUserpic(not_null<Dialogs::Entry*> entry) {\n\tentry->chatListPreloadData();\n\tconst auto history = entry->asHistory();\n\tif (!_descriptor.moneyRestrictionError || !history) {\n\t\treturn;\n\t} else if (!Api::ResolveMessageMoneyRestrictions(\n\t\t\thistory->peer,\n\t\t\thistory).known) {\n\t\tif (const auto user = history->peer->asUser()) {\n\t\t\tconst auto api = &_descriptor.session->api();\n\t\t\tapi->premium().resolveMessageMoneyRestrictions(user);\n\t\t}\n\t}\n}\n\nauto ShareBox::Inner::getChat(not_null<Dialogs::Row*> row)\n-> not_null<Chat*> {\n\tExpects(row->history() != nullptr);\n\n\tif (const auto data = static_cast<Chat*>(row->attached)) {\n\t\treturn data;\n\t}\n\tconst auto history = row->history();\n\tconst auto peer = history->peer;\n\tif (const auto i = _dataMap.find(peer); i != end(_dataMap)) {\n\t\trow->attached = i->second.get();\n\t\treturn i->second.get();\n\t}\n\tconst auto &[i, ok] = _dataMap.emplace(\n\t\tpeer,\n\t\tstd::make_unique<Chat>(history, _st.item, [=] {\n\t\t\trepaintChat(peer);\n\t\t}));\n\tupdateChatName(i->second.get());\n\tinitChatRestriction(i->second.get());\n\trow->attached = i->second.get();\n\treturn i->second.get();\n}\n\nvoid ShareBox::Inner::setActive(int active) {\n\tif (active != _active) {\n\t\tauto changeNameFg = [this](int index, float64 from, float64 to) {\n\t\t\tif (auto chat = getChatAtIndex(index)) {\n\t\t\t\tchat->nameActive.start([this, peer = chat->peer] {\n\t\t\t\t\trepaintChat(peer);\n\t\t\t\t}, from, to, st::shareActivateDuration);\n\t\t\t}\n\t\t};\n\t\tchangeNameFg(_active, 1., 0.);\n\t\t_active = active;\n\t\tchangeNameFg(_active, 0., 1.);\n\t}\n\tauto y = (_active < _columnCount) ? 0 : (_rowsTop + ((_active / _columnCount) * _rowHeight));\n\t_scrollToRequests.fire({ y, y + _rowHeight });\n}\n\nvoid ShareBox::Inner::paintChat(\n\t\tPainter &p,\n\t\tnot_null<Chat*> chat,\n\t\tint index) {\n\tauto x = _rowsLeft + qFloor((index % _columnCount) * _rowWidthReal);\n\tauto y = _rowsTop + (index / _columnCount) * _rowHeight;\n\n\tauto outerWidth = width();\n\tauto photoLeft = (_rowWidth - (_st.item.checkbox.imageRadius * 2)) / 2;\n\tauto photoTop = st::sharePhotoTop;\n\tchat->checkbox.paint(p, x + photoLeft, y + photoTop, outerWidth);\n\n\tif (chat->restriction) {\n\t\tPaintRestrictionBadge(\n\t\t\tp,\n\t\t\t&_st.item,\n\t\t\tchat->restriction.starsPerMessage,\n\t\t\tchat->badgeCache,\n\t\t\tx + photoLeft,\n\t\t\ty + photoTop,\n\t\t\touterWidth,\n\t\t\t_st.item.checkbox.imageRadius * 2);\n\t}\n\n\tauto nameActive = chat->nameActive.value((index == _active) ? 1. : 0.);\n\tp.setPen(anim::pen(_st.item.nameFg, _st.item.nameFgChecked, nameActive));\n\n\tauto nameWidth = (_rowWidth - st::shareColumnSkip);\n\tauto nameLeft = st::shareColumnSkip / 2;\n\tauto nameTop = photoTop + _st.item.checkbox.imageRadius * 2 + st::shareNameTop;\n\tchat->name.drawLeftElided(p, x + nameLeft, y + nameTop, nameWidth, outerWidth, 2, style::al_top, 0, -1, 0, true);\n}\n\nShareBox::Inner::Chat::Chat(\n\tnot_null<History*> history,\n\tconst style::PeerListItem &st,\n\tFn<void()> updateCallback)\n: history(history)\n, peer(history->peer)\n, checkbox(\n\tst.checkbox,\n\tupdateCallback,\n\tPaintUserpicCallback(peer, true),\n\t[=](int size) { return (peer->isForum() || peer->isMonoforum())\n\t\t? int(size * Ui::ForumUserpicRadiusMultiplier())\n\t\t: std::optional<int>(); })\n, name(st.checkbox.imageRadius * 2) {\n}\n\nvoid ShareBox::Inner::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tauto r = e->rect();\n\tp.setClipRect(r);\n\tp.fillRect(r, _st.bg);\n\tauto yFrom = r.y(), yTo = r.y() + r.height();\n\tauto rowFrom = yFrom / _rowHeight;\n\tauto rowTo = (yTo + _rowHeight - 1) / _rowHeight;\n\tauto indexFrom = rowFrom * _columnCount;\n\tauto indexTo = rowTo * _columnCount;\n\tif (_filter.isEmpty()) {\n\t\tif (!_chatsIndexed->empty()) {\n\t\t\tauto i = _chatsIndexed->begin()\n\t\t\t\t+ std::min(indexFrom, _chatsIndexed->size());\n\t\t\tfor (auto end = _chatsIndexed->cend(); i != end; ++i) {\n\t\t\t\tif (indexFrom >= indexTo) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tpaintChat(p, getChat(*i), indexFrom);\n\t\t\t\t++indexFrom;\n\t\t\t}\n\t\t} else {\n\t\t\tp.setFont(st::noContactsFont);\n\t\t\tp.setPen(_st.about.textFg);\n\t\t\tp.drawText(\n\t\t\t\trect().marginsRemoved(st::boxPadding),\n\t\t\t\ttr::lng_bot_no_chats(tr::now),\n\t\t\t\tstyle::al_center);\n\t\t}\n\t} else {\n\t\tif (_filtered.empty()\n\t\t\t&& _byUsernameFiltered.empty()\n\t\t\t&& !_searching) {\n\t\t\tp.setFont(st::noContactsFont);\n\t\t\tp.setPen(_st.about.textFg);\n\t\t\tp.drawText(\n\t\t\t\trect().marginsRemoved(st::boxPadding),\n\t\t\t\ttr::lng_bot_chats_not_found(tr::now),\n\t\t\t\tstyle::al_center);\n\t\t} else {\n\t\t\tauto filteredSize = _filtered.size();\n\t\t\tif (filteredSize) {\n\t\t\t\tif (indexFrom < 0) indexFrom = 0;\n\t\t\t\twhile (indexFrom < indexTo) {\n\t\t\t\t\tif (indexFrom >= _filtered.size()) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tpaintChat(p, getChat(_filtered[indexFrom]), indexFrom);\n\t\t\t\t\t++indexFrom;\n\t\t\t\t}\n\t\t\t\tindexFrom -= filteredSize;\n\t\t\t\tindexTo -= filteredSize;\n\t\t\t}\n\t\t\tif (!_byUsernameFiltered.empty()) {\n\t\t\t\tif (indexFrom < 0) indexFrom = 0;\n\t\t\t\twhile (indexFrom < indexTo) {\n\t\t\t\t\tif (indexFrom >= d_byUsernameFiltered.size()) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tpaintChat(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\td_byUsernameFiltered[indexFrom].get(),\n\t\t\t\t\t\tfilteredSize + indexFrom);\n\t\t\t\t\t++indexFrom;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ShareBox::Inner::enterEventHook(QEnterEvent *e) {\n\tsetMouseTracking(true);\n}\n\nvoid ShareBox::Inner::leaveEventHook(QEvent *e) {\n\tsetMouseTracking(false);\n}\n\nvoid ShareBox::Inner::mouseMoveEvent(QMouseEvent *e) {\n\tupdateUpon(e->pos());\n\tsetCursor((_upon >= 0) ? style::cur_pointer : style::cur_default);\n}\n\nvoid ShareBox::Inner::updateUpon(const QPoint &pos) {\n\tauto x = pos.x(), y = pos.y();\n\tauto row = (y - _rowsTop) / _rowHeight;\n\tauto column = qFloor((x - _rowsLeft) / _rowWidthReal);\n\n\tif (column < 0 || column >= _columnCount) {\n\t\t_upon = -1;\n\t\treturn;\n\t}\n\n\tauto left = _rowsLeft + qFloor(column * _rowWidthReal) + st::shareColumnSkip / 2;\n\tauto top = _rowsTop + row * _rowHeight + st::sharePhotoTop;\n\tauto xupon = (x >= left) && (x < left + (_rowWidth - st::shareColumnSkip));\n\tauto yupon = (y >= top) && (y < top + _st.item.checkbox.imageRadius * 2 + st::shareNameTop + _st.item.nameStyle.font->height * 2);\n\tauto upon = (xupon && yupon) ? (row * _columnCount + column) : -1;\n\tif (upon >= displayedChatsCount()) {\n\t\tupon = -1;\n\t}\n\t_upon = upon;\n}\n\nvoid ShareBox::Inner::mousePressEvent(QMouseEvent *e) {\n\tif (e->button() == Qt::LeftButton) {\n\t\tupdateUpon(e->pos());\n\t\tchangeCheckState(getChatAtIndex(_upon));\n\t}\n}\n\nvoid ShareBox::Inner::selectActive() {\n\tchangeCheckState(getChatAtIndex(_active > 0 ? _active : 0));\n}\n\nvoid ShareBox::Inner::resizeEvent(QResizeEvent *e) {\n\t_columnSkip = (width() - _columnCount * _st.item.checkbox.imageRadius * 2) / float64(_columnCount + 1);\n\t_rowWidthReal = _st.item.checkbox.imageRadius * 2 + _columnSkip;\n\t_rowsLeft = qFloor(_columnSkip / 2);\n\t_rowWidth = qFloor(_rowWidthReal);\n\tupdate();\n}\n\nvoid ShareBox::Inner::changeCheckState(Chat *chat) {\n\tif (!chat || showLockedError(chat)) {\n\t\treturn;\n\t} else if (!_filter.isEmpty()) {\n\t\tconst auto history = chat->peer->owner().history(chat->peer);\n\t\tauto row = _chatsIndexed->getRow(history);\n\t\tif (!row) {\n\t\t\trow = _chatsIndexed->addToEnd(history).main;\n\t\t}\n\t\tchat = getChat(row);\n\t\tif (!chat->checkbox.checked()) {\n\t\t\t_chatsIndexed->moveToTop(history);\n\t\t}\n\t}\n\n\tconst auto checked = chat->checkbox.checked();\n\tconst auto forum = chat->peer->forum();\n\tconst auto monoforum = chat->peer->monoforum();\n\tif (checked || (!forum && !monoforum)) {\n\t\tchangePeerCheckState(chat, !checked);\n\t} else if (forum) {\n\t\tchooseForumTopic(forum);\n\t} else if (monoforum) {\n\t\tchooseMonoforumSublist(monoforum);\n\t}\n}\n\nvoid ShareBox::Inner::chooseForumTopic(not_null<Data::Forum*> forum) {\n\tconst auto guard = base::make_weak(this);\n\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tauto chosen = [=](not_null<Data::Thread*> thread) {\n\t\tif (const auto strong = *weak) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t\tif (!guard) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto row = _chatsIndexed->getRow(thread->owningHistory());\n\t\tif (!row) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto chat = getChat(row);\n\t\tif (const auto topic = thread->asTopic()) {\n\t\t\tAssert(!chat->topic);\n\t\t\tchat->topic = topic;\n\t\t\tchat->topic->destroyed(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tchangePeerCheckState(chat, false);\n\t\t\t}, chat->topicLifetime);\n\t\t}\n\t\tupdateChatName(chat);\n\t\tchangePeerCheckState(chat, true);\n\t};\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\n\t\tforum->destroyed(\n\t\t) | rpl::on_next([=] {\n\t\t\tbox->closeBox();\n\t\t}, box->lifetime());\n\t};\n\tauto filter = [=](not_null<Data::Thread*> thread) {\n\t\treturn guard && _descriptor.filterCallback(thread);\n\t};\n\tauto box = Box<PeerListBox>(\n\t\tstd::make_unique<ChooseTopicBoxController>(\n\t\t\tforum,\n\t\t\tstd::move(chosen),\n\t\t\tstd::move(filter)),\n\t\tstd::move(initBox));\n\t*weak = box.data();\n\t_show->showBox(std::move(box));\n}\n\nvoid ShareBox::Inner::chooseMonoforumSublist(\n\t\tnot_null<Data::SavedMessages*> monoforum) {\n\tconst auto guard = base::make_weak(this);\n\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tauto chosen = [=](not_null<Data::SavedSublist*> sublist) {\n\t\tif (const auto strong = *weak) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t\tif (!guard) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto row = _chatsIndexed->getRow(sublist->owningHistory());\n\t\tif (!row) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto chat = getChat(row);\n\t\tAssert(!chat->sublist);\n\t\tchat->sublist = sublist;\n\t\tchat->sublist->destroyed(\n\t\t) | rpl::on_next([=] {\n\t\t\tchangePeerCheckState(chat, false);\n\t\t}, chat->sublistLifetime);\n\t\tupdateChatName(chat);\n\t\tchangePeerCheckState(chat, true);\n\t};\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\n\t\tmonoforum->destroyed(\n\t\t) | rpl::on_next([=] {\n\t\t\tbox->closeBox();\n\t\t}, box->lifetime());\n\t};\n\tauto filter = [=](not_null<Data::SavedSublist*> sublist) {\n\t\treturn guard && _descriptor.filterCallback(sublist);\n\t};\n\tauto box = Box<PeerListBox>(\n\t\tstd::make_unique<ChooseSublistBoxController>(\n\t\t\tmonoforum,\n\t\t\tstd::move(chosen),\n\t\t\tstd::move(filter)),\n\t\tstd::move(initBox));\n\t*weak = box.data();\n\t_show->showBox(std::move(box));\n}\n\nvoid ShareBox::Inner::peerUnselected(not_null<PeerData*> peer) {\n\tif (const auto i = _dataMap.find(peer); i != end(_dataMap)) {\n\t\tchangePeerCheckState(\n\t\t\ti->second.get(),\n\t\t\tfalse,\n\t\t\tChangeStateWay::SkipCallback);\n\t}\n}\n\nvoid ShareBox::Inner::setPeerSelectedChangedCallback(\n\t\tFn<void(not_null<Data::Thread*> thread, bool selected)> callback) {\n\t_peerSelectedChangedCallback = std::move(callback);\n}\n\nvoid ShareBox::Inner::changePeerCheckState(\n\t\tnot_null<Chat*> chat,\n\t\tbool checked,\n\t\tChangeStateWay useCallback) {\n\tchat->checkbox.setChecked(checked);\n\tconst auto thread = chatThread(chat);\n\tif (checked) {\n\t\t_selected.emplace(thread);\n\t\tsetActive(chatIndex(chat->peer));\n\t} else {\n\t\t_selected.remove(thread);\n\t\tif (chat->topic) {\n\t\t\tchat->topicLifetime.destroy();\n\t\t\tchat->topic = nullptr;\n\t\t\tupdateChatName(chat);\n\t\t}\n\t\tif (chat->sublist) {\n\t\t\tchat->sublistLifetime.destroy();\n\t\t\tchat->sublist = nullptr;\n\t\t\tupdateChatName(chat);\n\t\t}\n\t}\n\tif (useCallback != ChangeStateWay::SkipCallback\n\t\t&& _peerSelectedChangedCallback) {\n\t\t_peerSelectedChangedCallback(thread, checked);\n\t}\n}\n\nbool ShareBox::Inner::hasSelected() const {\n\treturn _selected.size();\n}\n\nvoid ShareBox::Inner::updateFilter(QString filter) {\n\t_lastQuery = filter.toLower().trimmed();\n\n\tauto words = TextUtilities::PrepareSearchWords(_lastQuery);\n\tfilter = words.isEmpty() ? QString() : words.join(' ');\n\tif (_filter != filter) {\n\t\t_filter = filter;\n\n\t\t_byUsernameFiltered.clear();\n\t\td_byUsernameFiltered.clear();\n\n\t\tif (_filter.isEmpty()) {\n\t\t\trefresh();\n\t\t} else {\n\t\t\t_filtered = _chatsIndexed->filtered(words);\n\t\t\trefresh();\n\n\t\t\t_searching = true;\n\t\t\t_searchRequests.fire({});\n\t\t}\n\t\tsetActive(-1);\n\t\tloadProfilePhotos();\n\t\tupdate();\n\t}\n}\n\nbool ShareBox::Inner::isFilterEmpty() const {\n\treturn _filter.isEmpty();\n}\n\nrpl::producer<Ui::ScrollToRequest> ShareBox::Inner::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nrpl::producer<> ShareBox::Inner::searchRequests() const {\n\treturn _searchRequests.events();\n}\n\nvoid ShareBox::Inner::applyChatFilter(FilterId id) {\n\tif (!id) {\n\t\t_chatsIndexed = _defaultChatsIndexed.get();\n\t} else {\n\t\t_customChatsIndexed = std::make_unique<Dialogs::IndexedList>(\n\t\t\tDialogs::SortMode::Add);\n\t\t_chatsIndexed = _customChatsIndexed.get();\n\n\t\tconst auto addList = [&](not_null<Dialogs::IndexedList*> list) {\n\t\t\tfor (const auto &row : list->all()) {\n\t\t\t\tif (const auto history = row->history()) {\n\t\t\t\t\tif (history->asForum()\n\t\t\t\t\t\t\t|| _descriptor.filterCallback(history)) {\n\t\t\t\t\t\t_customChatsIndexed->addToEnd(history);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tconst auto &data = _descriptor.session->data();\n\t\taddList(data.chatsFilters().chatsList(id)->indexed());\n\t}\n\tupdate();\n}\n\nvoid ShareBox::Inner::peopleReceived(\n\t\tconst QString &query,\n\t\tconst QVector<MTPPeer> &my,\n\t\tconst QVector<MTPPeer> &people) {\n\t_lastQuery = query.toLower().trimmed();\n\tif (_lastQuery.at(0) == '@') {\n\t\t_lastQuery = _lastQuery.mid(1);\n\t}\n\tint32 already = _byUsernameFiltered.size();\n\t_byUsernameFiltered.reserve(already + my.size() + people.size());\n\td_byUsernameFiltered.reserve(already + my.size() + people.size());\n\tconst auto feedList = [&](const QVector<MTPPeer> &list) {\n\t\tfor (const auto &data : list) {\n\t\t\tif (const auto peer = _descriptor.session->data().peerLoaded(\n\t\t\t\t\tpeerFromMTP(data))) {\n\t\t\t\tconst auto history = _descriptor.session->data().history(\n\t\t\t\t\tpeer);\n\t\t\t\tif (!history->asForum()\n\t\t\t\t\t&& !_descriptor.filterCallback(history)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (history && _chatsIndexed->getRow(history)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (base::contains(_byUsernameFiltered, peer)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t_byUsernameFiltered.push_back(peer);\n\t\t\t\td_byUsernameFiltered.push_back(std::make_unique<Chat>(\n\t\t\t\t\thistory,\n\t\t\t\t\t_st.item,\n\t\t\t\t\t[=] { repaintChat(peer); }));\n\t\t\t\tupdateChatName(d_byUsernameFiltered.back().get());\n\t\t\t\tinitChatRestriction(d_byUsernameFiltered.back().get());\n\t\t\t}\n\t\t}\n\t};\n\tfeedList(my);\n\tfeedList(people);\n\n\t_searching = false;\n\trefresh();\n}\n\nvoid ShareBox::Inner::refresh() {\n\tauto count = displayedChatsCount();\n\tif (count) {\n\t\tauto rows = (count / _columnCount) + (count % _columnCount ? 1 : 0);\n\t\tresize(width(), _rowsTop + rows * _rowHeight);\n\t} else {\n\t\tresize(width(), st::noContactsHeight);\n\t}\n\tloadProfilePhotos();\n\tupdate();\n}\n\nnot_null<Data::Thread*> ShareBox::Inner::chatThread(\n\t\tnot_null<Chat*> chat) const {\n\treturn chat->topic\n\t\t? (Data::Thread*)chat->topic\n\t\t: chat->sublist\n\t\t? (Data::Thread*)chat->sublist\n\t\t: chat->peer->owner().history(chat->peer).get();\n}\n\nstd::vector<not_null<Data::Thread*>> ShareBox::Inner::selected() const {\n\tauto result = std::vector<not_null<Data::Thread*>>();\n\tresult.reserve(_dataMap.size());\n\tfor (const auto &[peer, chat] : _dataMap) {\n\t\tif (chat->checkbox.checked()) {\n\t\t\tresult.push_back(chatThread(chat.get()));\n\t\t}\n\t}\n\treturn result;\n}\n\nChatHelpers::ForwardedMessagePhraseArgs CreateForwardedMessagePhraseArgs(\n\t\tconst std::vector<not_null<Data::Thread*>> &result,\n\t\tconst MessageIdsList &msgIds) {\n\tconst auto toCount = result.size();\n\treturn {\n\t\t.toCount = result.size(),\n\t\t.singleMessage = (msgIds.size() <= 1),\n\t\t.to1 = (toCount > 0) ? result.front()->peer().get() : nullptr,\n\t\t.to2 = (toCount > 1) ? result[1]->peer().get() : nullptr,\n\t};\n}\n\nShareBox::CountMessagesCallback ShareBox::DefaultForwardCountMessages(\n\t\tnot_null<History*> history,\n\t\tMessageIdsList msgIds) {\n\treturn [=](const TextWithTags &comment) {\n\t\tconst auto items = history->owner().idsToItems(msgIds);\n\t\treturn int(items.size()) + (comment.empty() ? 0 : 1);\n\t};\n}\n\nShareBox::SubmitCallback ShareBox::DefaultForwardCallback(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<History*> history,\n\t\tMessageIdsList msgIds,\n\t\tstd::optional<TimeId> videoTimestamp) {\n\tstruct State final {\n\t\tbase::flat_set<mtpRequestId> requests;\n\t\tmtpRequestId nextRequestKey = 0;\n\t};\n\tconst auto state = std::make_shared<State>();\n\treturn [=](\n\t\t\tstd::vector<not_null<Data::Thread*>> &&result,\n\t\t\tFn<bool()> checkPaid,\n\t\t\tTextWithTags comment,\n\t\t\tApi::SendOptions options,\n\t\t\tData::ForwardOptions forwardOptions) {\n\t\tif (!state->requests.empty()) {\n\t\t\treturn; // Share clicked already.\n\t\t}\n\n\t\tconst auto items = history->owner().idsToItems(msgIds);\n\t\tconst auto existingIds = history->owner().itemsToIds(items);\n\t\tif (existingIds.empty() || result.empty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto error = GetErrorForSending(\n\t\t\tresult,\n\t\t\t{ .forward = &items, .text = &comment });\n\t\tif (error.error) {\n\t\t\tshow->showBox(MakeSendErrorBox(error, result.size() > 1));\n\t\t\treturn;\n\t\t} else if (!checkPaid()) {\n\t\t\treturn;\n\t\t}\n\n\t\tusing Flag = MTPmessages_ForwardMessages::Flag;\n\t\tconst auto commonSendFlags = Flag(0)\n\t\t\t| Flag::f_with_my_score\n\t\t\t| (options.scheduled ? Flag::f_schedule_date : Flag(0))\n\t\t\t| ((options.scheduled && options.scheduleRepeatPeriod)\n\t\t\t\t? Flag::f_schedule_repeat_period\n\t\t\t\t: Flag(0))\n\t\t\t| ((forwardOptions != Data::ForwardOptions::PreserveInfo)\n\t\t\t\t? Flag::f_drop_author\n\t\t\t\t: Flag(0))\n\t\t\t| ((forwardOptions == Data::ForwardOptions::NoNamesAndCaptions)\n\t\t\t\t? Flag::f_drop_media_captions\n\t\t\t\t: Flag(0))\n\t\t\t| (videoTimestamp.has_value()\n\t\t\t\t? Flag::f_video_timestamp\n\t\t\t\t: Flag(0));\n\t\tauto mtpMsgIds = QVector<MTPint>();\n\t\tmtpMsgIds.reserve(existingIds.size());\n\t\tfor (const auto &fullId : existingIds) {\n\t\t\tmtpMsgIds.push_back(MTP_int(fullId.msg));\n\t\t}\n\t\tauto &api = history->session().api();\n\t\tauto &histories = history->owner().histories();\n\t\tconst auto donePhraseArgs = CreateForwardedMessagePhraseArgs(\n\t\t\tresult,\n\t\t\tmsgIds);\n\t\tconst auto showRecentForwardsToSelf = result.size() == 1\n\t\t\t&& result.front()->peer()->isSelf()\n\t\t\t&& history->session().premium();\n\t\tfor (const auto &thread : result) {\n\t\t\tconst auto peer = thread->peer();\n\t\t\tconst auto threadHistory = thread->owningHistory();\n\t\t\tconst auto forum = threadHistory->asForum();\n\t\t\tconst auto needNewTopic = forum\n\t\t\t\t&& forum->bot()\n\t\t\t\t&& Data::IsBotUserCreatesTopics(peer)\n\t\t\t\t&& !thread->asTopic();\n\t\t\tconst auto effectiveThread = [&]() -> not_null<Data::Thread*> {\n\t\t\t\tif (needNewTopic) {\n\t\t\t\t\tconst auto topic = forum->reserveNewBotTopic();\n\t\t\t\t\tAssert(topic != nullptr);\n\t\t\t\t\treturn topic;\n\t\t\t\t}\n\t\t\t\treturn thread;\n\t\t\t}();\n\n\t\t\tif (!comment.text.isEmpty()) {\n\t\t\t\tauto message = Api::MessageToSend(\n\t\t\t\t\tApi::SendAction(effectiveThread, options));\n\t\t\t\tmessage.textWithTags = comment;\n\t\t\t\tmessage.action.clearDraft = false;\n\t\t\t\tapi.sendMessage(std::move(message));\n\t\t\t}\n\n\t\t\tconst auto topicRootId = effectiveThread->topicRootId();\n\t\t\tconst auto sublistPeer = needNewTopic\n\t\t\t\t? nullptr\n\t\t\t\t: thread->maybeSublistPeer();\n\t\t\tconst auto fromPeer = history->peer;\n\t\t\tconst auto msgCount = int(existingIds.size());\n\t\t\tconst auto starsPaid = std::min(\n\t\t\t\tpeer->starsPerMessageChecked(),\n\t\t\t\toptions.starsApproved);\n\t\t\tif (starsPaid) {\n\t\t\t\toptions.starsApproved -= starsPaid;\n\t\t\t}\n\t\t\tconst auto sendFlags = commonSendFlags\n\t\t\t\t| (ShouldSendSilent(peer, options)\n\t\t\t\t\t? Flag::f_silent\n\t\t\t\t\t: Flag(0))\n\t\t\t\t| (options.shortcutId\n\t\t\t\t\t? Flag::f_quick_reply_shortcut\n\t\t\t\t\t: Flag(0))\n\t\t\t\t| (starsPaid ? Flag::f_allow_paid_stars : Flag())\n\t\t\t\t| (sublistPeer ? Flag::f_reply_to : Flag())\n\t\t\t\t| (options.suggest ? Flag::f_suggested_post : Flag())\n\t\t\t\t| (options.effectId ? Flag::f_effect : Flag());\n\t\t\tauto buildMessage = [=](\n\t\t\t\t\tnot_null<History*> history,\n\t\t\t\t\tFullReplyTo replyTo)\n\t\t\t\t-> Data::Histories::PreparedMessage {\n\t\t\t\tconst auto kGeneralId\n\t\t\t\t\t= Data::ForumTopic::kGeneralId;\n\t\t\t\tconst auto realTopMsgId\n\t\t\t\t\t= (replyTo.topicRootId == kGeneralId)\n\t\t\t\t\t? MsgId(0)\n\t\t\t\t\t: replyTo.topicRootId;\n\t\t\t\tauto flags = sendFlags;\n\t\t\t\tif (realTopMsgId) {\n\t\t\t\t\tflags |= Flag::f_top_msg_id;\n\t\t\t\t} else {\n\t\t\t\t\tflags &= ~Flag::f_top_msg_id;\n\t\t\t\t}\n\t\t\t\tauto randoms = QVector<MTPlong>(msgCount);\n\t\t\t\tfor (auto &value : randoms) {\n\t\t\t\t\tvalue = base::RandomValue<MTPlong>();\n\t\t\t\t}\n\t\t\t\treturn MTPmessages_ForwardMessages(\n\t\t\t\t\tMTP_flags(flags),\n\t\t\t\t\tfromPeer->input(),\n\t\t\t\t\tMTP_vector<MTPint>(mtpMsgIds),\n\t\t\t\t\tMTP_vector<MTPlong>(randoms),\n\t\t\t\t\thistory->peer->input(),\n\t\t\t\t\tMTP_int(realTopMsgId),\n\t\t\t\t\t(sublistPeer\n\t\t\t\t\t\t? MTP_inputReplyToMonoForum(\n\t\t\t\t\t\t\tsublistPeer->input())\n\t\t\t\t\t\t: MTPInputReplyTo()),\n\t\t\t\t\tMTP_int(options.scheduled),\n\t\t\t\t\tMTP_int(options.scheduleRepeatPeriod),\n\t\t\t\t\tMTP_inputPeerEmpty(),\n\t\t\t\t\tData::ShortcutIdToMTP(\n\t\t\t\t\t\t&history->session(),\n\t\t\t\t\t\toptions.shortcutId),\n\t\t\t\t\tMTP_long(options.effectId),\n\t\t\t\t\tMTP_int(videoTimestamp.value_or(0)),\n\t\t\t\t\tMTP_long(starsPaid),\n\t\t\t\t\tApi::SuggestToMTP(options.suggest));\n\t\t\t};\n\t\t\tconst auto requestDone = [=](\n\t\t\t\t\tconst MTPUpdates &updates,\n\t\t\t\t\tmtpRequestId requestKey) {\n\t\t\t\tif (showRecentForwardsToSelf) {\n\t\t\t\t\tApiWrap::ProcessRecentSelfForwards(\n\t\t\t\t\t\t&threadHistory->session(),\n\t\t\t\t\t\tupdates,\n\t\t\t\t\t\tpeer->id,\n\t\t\t\t\t\thistory->peer->id);\n\t\t\t\t}\n\t\t\t\tstate->requests.remove(requestKey);\n\t\t\t\tif (state->requests.empty()) {\n\t\t\t\t\tif (show->valid()) {\n\t\t\t\t\t\tauto phrase = rpl::variable<\n\t\t\t\t\t\t\tTextWithEntities>(\n\t\t\t\t\t\t\tChatHelpers::ForwardedMessagePhrase(\n\t\t\t\t\t\t\t\tdonePhraseArgs)).current();\n\t\t\t\t\t\tif (!phrase.empty()) {\n\t\t\t\t\t\t\tshow->showToast(std::move(phrase));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tshow->hideLayer();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto requestFail = [=](\n\t\t\t\t\tconst MTP::Error &error,\n\t\t\t\t\tmtpRequestId requestKey) {\n\t\t\t\tconst auto type = error.type();\n\t\t\t\tif (type.startsWith(\n\t\t\t\t\t\tu\"ALLOW_PAYMENT_REQUIRED_\"_q)) {\n\t\t\t\t\tshow->showToast(\n\t\t\t\t\t\tu\"Payment requirements changed. \"\n\t\t\t\t\t\t\"Please, try again.\"_q);\n\t\t\t\t} else if (type\n\t\t\t\t\t== u\"VOICE_MESSAGES_FORBIDDEN\"_q) {\n\t\t\t\t\tshow->showToast(\n\t\t\t\t\t\ttr::lng_restricted_send_voice_messages(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\t\tpeer->name()));\n\t\t\t\t}\n\t\t\t\tstate->requests.remove(requestKey);\n\t\t\t\tif (state->requests.empty()) {\n\t\t\t\t\tif (show->valid()) {\n\t\t\t\t\t\tshow->hideLayer();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto requestKey = ++state->nextRequestKey;\n\t\t\tstate->requests.insert(requestKey);\n\t\t\thistories.sendPreparedMessage(\n\t\t\t\tthreadHistory,\n\t\t\t\tFullReplyTo{ .topicRootId = topicRootId },\n\t\t\t\tuint64(0),\n\t\t\t\tstd::move(buildMessage),\n\t\t\t\t[=](const MTPUpdates &updates,\n\t\t\t\t\t\tconst MTP::Response &) {\n\t\t\t\t\trequestDone(updates, requestKey);\n\t\t\t\t},\n\t\t\t\t[=](const MTP::Error &error,\n\t\t\t\t\t\tconst MTP::Response &) {\n\t\t\t\t\trequestFail(error, requestKey);\n\t\t\t\t});\n\t\t}\n\t\tif (state->requests.empty()) {\n\t\t\tif (show->valid()) {\n\t\t\t\tauto phrase = rpl::variable<TextWithEntities>(\n\t\t\t\t\tChatHelpers::ForwardedMessagePhrase(\n\t\t\t\t\t\tdonePhraseArgs)).current();\n\t\t\t\tif (!phrase.empty()) {\n\t\t\t\t\tshow->showToast(std::move(phrase));\n\t\t\t\t}\n\t\t\t\tshow->hideLayer();\n\t\t\t}\n\t\t}\n\t};\n}\n\nShareBoxStyleOverrides DarkShareBoxStyle() {\n\tusing namespace HistoryView;\n\n\tconst auto schedule = [&] {\n\t\tauto date = Ui::ChooseDateTimeStyleArgs();\n\t\tdate.labelStyle = &st::groupCallBoxLabel;\n\t\tdate.dateFieldStyle = &st::groupCallScheduleDateField;\n\t\tdate.timeFieldStyle = &st::groupCallScheduleTimeField;\n\t\tdate.separatorStyle = &st::callMuteButtonLabel;\n\t\tdate.atStyle = &st::callMuteButtonLabel;\n\t\tdate.calendarStyle = &st::groupCallCalendarColors;\n\n\t\tauto st = ScheduleBoxStyleArgs();\n\t\tst.topButtonStyle = &st::groupCallMenuToggle;\n\t\tst.popupMenuStyle = &st::groupCallPopupMenu;\n\t\tst.chooseDateTimeArgs = std::move(date);\n\t\treturn st;\n\t};\n\treturn {\n\t\t.multiSelect = &st::groupCallMultiSelect,\n\t\t.comment = &st::groupCallShareBoxComment,\n\t\t.peerList = &st::groupCallShareBoxList,\n\t\t.label = &st::groupCallField,\n\t\t.checkbox = &st::groupCallCheckbox,\n\t\t.scheduleBox = std::make_shared<ScheduleBoxStyleArgs>(schedule()),\n\t};\n}\n\nvoid FastShareMessage(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<HistoryItem*> item,\n\t\tShareBoxStyleOverrides st) {\n\tconst auto history = item->history();\n\tconst auto owner = &history->owner();\n\tconst auto session = &history->session();\n\tconst auto msgIds = owner->itemOrItsGroup(item);\n\tconst auto isGame = item->getMessageBot()\n\t\t&& item->media()\n\t\t&& (item->media()->game() != nullptr);\n\tconst auto canCopyLink = item->hasDirectLink() || isGame;\n\n\tconst auto items = owner->idsToItems(msgIds);\n\tconst auto hasCaptions = ranges::any_of(items, [](auto item) {\n\t\treturn item->media()\n\t\t\t&& !item->originalText().text.isEmpty()\n\t\t\t&& item->media()->allowsEditCaption();\n\t});\n\tconst auto hasOnlyForcedForwardedInfo = hasCaptions\n\t\t? false\n\t\t: ranges::all_of(items, [](auto item) {\n\t\t\treturn item->media() && item->media()->forceForwardedInfo();\n\t\t});\n\n\tauto copyCallback = [=] {\n\t\tconst auto item = owner->message(msgIds[0]);\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\t\tif (item->hasDirectLink()) {\n\t\t\tusing namespace HistoryView;\n\t\t\tCopyPostLink(show, item->fullId(), Context::History);\n\t\t} else if (const auto bot = item->getMessageBot()) {\n\t\t\tif (const auto media = item->media()) {\n\t\t\t\tif (const auto game = media->game()) {\n\t\t\t\t\tconst auto link = session->createInternalLinkFull(\n\t\t\t\t\t\tbot->username() + u\"?game=\"_q + game->shortName);\n\n\t\t\t\t\tQGuiApplication::clipboard()->setText(link);\n\n\t\t\t\t\tshow->showToast(\n\t\t\t\t\t\ttr::lng_share_game_link_copied(tr::now));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\tauto asCopyCallback = [=, msgIds = owner->itemOrItsGroup(item)](\n\t\t\tstd::vector<not_null<PeerData*>> &&result,\n\t\t\tTextWithTags &&comment,\n\t\t\tbool emptyText,\n\t\t\tTimeId scheduled) {\n\t\tauto toSend = Api::AsCopy::ToSend{\n\t\t\t.peers = std::move(result),\n\t\t\t.comment = std::move(comment),\n\t\t\t.emptyText = emptyText,\n\t\t\t.silent = QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier),\n\t\t\t.scheduled = scheduled,\n\t\t};\n\t\tconst auto toast = (toSend.silent && toSend.scheduled)\n\t\t\t? u\"Silently scheduled.\"_q\n\t\t\t: toSend.silent\n\t\t\t? u\"Silently.\"_q\n\t\t\t: toSend.scheduled\n\t\t\t? u\"Scheduled.\"_q\n\t\t\t: QString();\n\t\tif (!toast.isEmpty()) {\n\t\t\tshow->showToast(toast);\n\t\t}\n\t\tif (item->groupId()) {\n\t\t\tApi::AsCopy::GuardedSendExistingAlbumFromItem(item, std::move(toSend));\n\t\t} else if (const auto i = history->owner().message(msgIds[0])) {\n\t\t\tApi::AsCopy::UpdateFileRef(\n\t\t\t\t{ i },\n\t\t\t\t[=, toSend = std::move(toSend)] {\n\t\t\t\t\tApi::AsCopy::SendExistingMediaFromItem(i, base::duplicate(toSend));\n\t\t\t\t},\n\t\t\t\t[=](QString a) { show->showToast(a); });\n\t\t}\n\t};\n\n\tconst auto requiredRight = item->requiredSendRight();\n\tconst auto requiresInline = item->requiresSendInlineRight();\n\tauto filterCallback = [=](not_null<Data::Thread*> thread) {\n\t\tif (const auto user = thread->peer()->asUser()) {\n\t\t\tif (user->canSendIgnoreMoneyRestrictions()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn Data::CanSend(thread, requiredRight)\n\t\t\t&& (!requiresInline\n\t\t\t\t|| Data::CanSend(thread, ChatRestriction::SendInline))\n\t\t\t&& (!isGame || !thread->peer()->isBroadcast());\n\t};\n\tauto copyLinkCallback = canCopyLink\n\t\t? Fn<void()>(std::move(copyCallback))\n\t\t: Fn<void()>();\n\tshow->show(Box<ShareBox>(ShareBox::Descriptor{\n\t\t.session = session,\n\t\t.copyCallback = std::move(copyLinkCallback),\n\t\t.countMessagesCallback = ShareBox::DefaultForwardCountMessages(\n\t\t\thistory,\n\t\t\tmsgIds),\n\t\t.submitCallback = ShareBox::DefaultForwardCallback(\n\t\t\tshow,\n\t\t\thistory,\n\t\t\tmsgIds),\n\t\t.filterCallback = std::move(filterCallback),\n\t\t.asCopyCallback = std::move(asCopyCallback),\n\t\t.st = st,\n\t\t.forwardOptions = {\n\t\t\t.sendersCount = ItemsForwardSendersCount(items),\n\t\t\t.captionsCount = ItemsForwardCaptionsCount(items),\n\t\t\t.show = !hasOnlyForcedForwardedInfo,\n\t\t},\n\t\t.moneyRestrictionError = ShareMessageMoneyRestrictionError(),\n\t}), Ui::LayerOption::CloseOther);\n}\n\nvoid FastShareMessageToSelf(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto self = show->session().user();\n\tauto &owner = self->owner();\n\tconst auto items = owner.idsToItems(owner.itemOrItsGroup(item));\n\tconst auto donePhraseArgs = ChatHelpers::ForwardedMessagePhraseArgs{\n\t\t.toCount = 1,\n\t\t.singleMessage = (items.size() == 1),\n\t\t.to1 = self,\n\t\t.to2 = nullptr,\n\t};\n\tauto sendAction = Api::SendAction(owner.history(self));\n\tsendAction.clearDraft = false;\n\tshow->session().api().forwardMessages(\n\t\tData::ResolvedForwardDraft{ .items = items },\n\t\tstd::move(sendAction),\n\t\t[=] {\n\t\t\tauto phrase = rpl::variable<TextWithEntities>(\n\t\t\t\tChatHelpers::ForwardedMessagePhrase(\n\t\t\t\t\tdonePhraseArgs)).current();\n\t\t\tif (!phrase.empty()) {\n\t\t\t\tshow->showToast(std::move(phrase));\n\t\t\t}\n\t\t});\n}\n\nvoid FastShareMessage(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item,\n\t\tShareBoxStyleOverrides st) {\n\tFastShareMessage(controller->uiShow(), item, st);\n}\n\nvoid FastShareLink(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &url,\n\t\tShareBoxStyleOverrides st) {\n\tFastShareLink(controller->uiShow(), url, st);\n}\n\nvoid FastShareLink(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tconst QString &url,\n\t\tShareBoxStyleOverrides st) {\n\tconst auto box = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tconst auto sending = std::make_shared<bool>();\n\tauto copyCallback = [=] {\n\t\tQGuiApplication::clipboard()->setText(url);\n\t\tshow->showToast(tr::lng_background_link_copied(tr::now));\n\t};\n\tauto countMessagesCallback = [=](const TextWithTags &comment) {\n\t\treturn 1;\n\t};\n\tauto submitCallback = [=](\n\t\t\tstd::vector<not_null<::Data::Thread*>> &&result,\n\t\t\tFn<bool()> checkPaid,\n\t\t\tTextWithTags &&comment,\n\t\t\tApi::SendOptions options,\n\t\t\t::Data::ForwardOptions) {\n\t\tif (*sending || result.empty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto error = GetErrorForSending(\n\t\t\tresult,\n\t\t\t{ .text = &comment });\n\t\tif (error.error) {\n\t\t\tif (const auto weak = *box) {\n\t\t\t\tweak->getDelegate()->show(\n\t\t\t\t\tMakeSendErrorBox(error, result.size() > 1));\n\t\t\t}\n\t\t\treturn;\n\t\t} else if (!checkPaid()) {\n\t\t\treturn;\n\t\t}\n\n\t\t*sending = true;\n\t\tif (!comment.text.isEmpty()) {\n\t\t\tcomment.text = url + \"\\n\" + comment.text;\n\t\t\tconst auto add = url.size() + 1;\n\t\t\tfor (auto &tag : comment.tags) {\n\t\t\t\ttag.offset += add;\n\t\t\t}\n\t\t} else {\n\t\t\tcomment.text = url;\n\t\t}\n\t\tauto &api = show->session().api();\n\t\tfor (const auto &thread : result) {\n\t\t\tauto message = Api::MessageToSend(\n\t\t\t\tApi::SendAction(thread, options));\n\t\t\tmessage.textWithTags = comment;\n\t\t\tmessage.action.clearDraft = false;\n\t\t\tapi.sendMessage(std::move(message));\n\t\t}\n\t\tif (*box) {\n\t\t\t(*box)->closeBox();\n\t\t}\n\t\tshow->showToast(tr::lng_share_done(tr::now));\n\t};\n\tauto filterCallback = [](not_null<::Data::Thread*> thread) {\n\t\tif (const auto user = thread->peer()->asUser()) {\n\t\t\tif (user->canSendIgnoreMoneyRestrictions()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn ::Data::CanSend(thread, ChatRestriction::SendOther);\n\t};\n\t*box = show->show(\n\t\tBox<ShareBox>(ShareBox::Descriptor{\n\t\t\t.session = &show->session(),\n\t\t\t.copyCallback = std::move(copyCallback),\n\t\t\t.countMessagesCallback = std::move(countMessagesCallback),\n\t\t\t.submitCallback = std::move(submitCallback),\n\t\t\t.filterCallback = std::move(filterCallback),\n\t\t\t.st = st,\n\t\t\t.moneyRestrictionError = ShareMessageMoneyRestrictionError(),\n\t\t}),\n\t\tUi::LayerOption::KeepOther,\n\t\tanim::type::normal);\n}\n\nauto ShareMessageMoneyRestrictionError()\n-> Fn<RecipientMoneyRestrictionError(not_null<UserData*>)> {\n\treturn WriteMoneyRestrictionError;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/share_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n#include \"base/timer.h\"\n#include \"history/view/history_view_schedule_box.h\"\n#include \"ui/chat/forward_options_box.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/round_checkbox.h\"\n#include \"mtproto/sender.h\"\n\nclass History;\n\nnamespace style {\nstruct MultiSelect;\nstruct InputField;\nstruct PeerList;\n} // namespace style\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Api {\nstruct SendOptions;\n} // namespace Api\n\nnamespace Main {\nclass Session;\nclass SessionShow;\n} // namespace Main\n\nnamespace Dialogs {\nclass Row;\nclass IndexedList;\n} // namespace Dialogs\n\nnamespace Data {\nenum class ForwardOptions;\nclass Thread;\n} // namespace Data\n\nnamespace Ui {\nclass MultiSelect;\nclass InputField;\nstruct ScrollToRequest;\ntemplate <typename Widget>\nclass SlideWrap;\nclass PopupMenu;\n} // namespace Ui\n\nclass ShareBox;\n\nstruct ShareBoxStyleOverrides {\n\tconst style::MultiSelect *multiSelect = nullptr;\n\tconst style::InputField *comment = nullptr;\n\tconst style::PeerList *peerList = nullptr;\n\tconst style::InputField *label = nullptr;\n\tconst style::Checkbox *checkbox = nullptr;\n\tstd::shared_ptr<HistoryView::ScheduleBoxStyleArgs> scheduleBox;\n};\n[[nodiscard]] ShareBoxStyleOverrides DarkShareBoxStyle();\n\nvoid FastShareMessageToSelf(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tnot_null<HistoryItem*> item);\nvoid FastShareMessage(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tnot_null<HistoryItem*> item,\n\tShareBoxStyleOverrides st = {});\nvoid FastShareMessage(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<HistoryItem*> item,\n\tShareBoxStyleOverrides st = {});\nvoid FastShareLink(\n\tnot_null<Window::SessionController*> controller,\n\tconst QString &url,\n\tShareBoxStyleOverrides st = {});\nvoid FastShareLink(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tconst QString &url,\n\tShareBoxStyleOverrides st = {});\n\nstruct RecipientMoneyRestrictionError;\n[[nodiscard]] auto ShareMessageMoneyRestrictionError()\n-> Fn<RecipientMoneyRestrictionError(not_null<UserData*>)>;\n\nclass ShareBox final : public Ui::BoxContent {\npublic:\n\tusing CopyCallback = Fn<void()>;\n\tusing CountMessagesCallback = Fn<int(const TextWithTags&)>;\n\tusing SubmitCallback = Fn<void(\n\t\tstd::vector<not_null<Data::Thread*>>&&,\n\t\tFn<bool()> checkPaid,\n\t\tTextWithTags&&,\n\t\tApi::SendOptions,\n\t\tData::ForwardOptions)>;\n\tusing AsCopyCallback = Fn<void(\n\t\tstd::vector<not_null<PeerData*>>&&,\n\t\tTextWithTags&&,\n\t\tbool emptyText,\n\t\tTimeId scheduled)>;\n\tusing FilterCallback = Fn<bool(not_null<Data::Thread*>)>;\n\n\t[[nodiscard]] static auto DefaultForwardCountMessages(\n\t\tnot_null<History*> history,\n\t\tMessageIdsList msgIds) -> CountMessagesCallback;\n\t[[nodiscard]] static SubmitCallback DefaultForwardCallback(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<History*> history,\n\t\tMessageIdsList msgIds,\n\t\tstd::optional<TimeId> videoTimestamp = {});\n\n\tstruct Descriptor {\n\t\tnot_null<Main::Session*> session;\n\t\tCopyCallback copyCallback;\n\t\tCountMessagesCallback countMessagesCallback;\n\t\tSubmitCallback submitCallback;\n\t\tFilterCallback filterCallback;\n\t\tAsCopyCallback asCopyCallback;\n\t\tobject_ptr<Ui::RpWidget> bottomWidget = { nullptr };\n\t\trpl::producer<QString> copyLinkText;\n\t\trpl::producer<QString> titleOverride;\n\t\tShareBoxStyleOverrides st;\n\t\tstd::optional<TimeId> videoTimestamp;\n\t\tstruct {\n\t\t\tint sendersCount = 0;\n\t\t\tint captionsCount = 0;\n\t\t\tbool show = false;\n\t\t} forwardOptions;\n\n\t\tusing MoneyRestrictionError = RecipientMoneyRestrictionError;\n\t\tFn<MoneyRestrictionError(\n\t\t\tnot_null<UserData*>)> moneyRestrictionError;\n\t};\n\tShareBox(QWidget*, Descriptor &&descriptor);\n\nprotected:\n\tvoid prepare() override;\n\tvoid setInnerFocus() override;\n\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tvoid prepareCommentField();\n\tvoid scrollAnimationCallback();\n\n\tvoid submit(Api::SendOptions options);\n\tvoid copyLink() const;\n\tbool searchByUsername(bool useCache = false);\n\n\t[[nodiscard]] SendMenu::Details sendMenuDetails() const;\n\n\tvoid scrollTo(Ui::ScrollToRequest request);\n\tvoid needSearchByUsername();\n\tvoid applyFilterUpdate(const QString &query);\n\tvoid selectedChanged();\n\tvoid computeStarsCount();\n\tvoid createButtons();\n\tint getTopScrollSkip() const;\n\tint getBottomScrollSkip() const;\n\tint contentHeight() const;\n\tvoid updateScrollSkips();\n\n\tvoid addPeerToMultiSelect(not_null<Data::Thread*> thread);\n\tvoid innerSelectedChanged(not_null<Data::Thread*> thread, bool checked);\n\n\tvoid peopleDone(\n\t\tconst MTPcontacts_Found &result,\n\t\tmtpRequestId requestId);\n\tvoid peopleFail(const MTP::Error &error, mtpRequestId requestId);\n\n\tvoid showMenu(not_null<Ui::RpWidget*> parent);\n\n\tDescriptor _descriptor;\n\tMTP::Sender _api;\n\n\tobject_ptr<Ui::MultiSelect> _select;\n\tobject_ptr<Ui::SlideWrap<Ui::InputField>> _comment;\n\tobject_ptr<Ui::RpWidget> _bottomWidget;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\tUi::ForwardOptions _forwardOptions;\n\n\tclass Inner;\n\tQPointer<Inner> _inner;\n\n\tbool _hasSelected = false;\n\trpl::variable<QString> _copyLinkText;\n\trpl::variable<int> _starsToSend;\n\n\tbase::Timer _searchTimer;\n\tQString _peopleQuery;\n\tbool _peopleFull = false;\n\tmtpRequestId _peopleRequest = 0;\n\n\tRpWidget *_chatsFilters = nullptr;\n\n\tusing PeopleCache = QMap<QString, MTPcontacts_Found>;\n\tPeopleCache _peopleCache;\n\n\tusing PeopleQueries = QMap<mtpRequestId, QString>;\n\tPeopleQueries _peopleQueries;\n\n\tUi::Animations::Simple _scrollAnimation;\n\trpl::lifetime _submitLifetime;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/star_gift_auction_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/star_gift_auction_box.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"base/random.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/peers/replace_boost_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"boxes/send_credits_box.h\" // CreditsEmojiSmall\n#include \"boxes/share_box.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"boxes/star_gift_preview_box.h\"\n#include \"boxes/star_gift_resale_box.h\"\n#include \"calls/group/calls_group_common.h\"\n#include \"core/application.h\"\n#include \"core/credits_amount.h\"\n#include \"core/ui_integration.h\"\n#include \"data/components/credits.h\"\n#include \"data/components/gift_auctions.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_document.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/view/controls/history_view_suggest_options.h\"\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"info/peer_gifts/info_peer_gifts_common.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"payments/ui/payments_reaction_box.h\"\n#include \"payments/payments_checkout_process.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"storage/storage_account.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/emoji_stake_box.h\" // AddStarsInputField\n#include \"ui/controls/button_labels.h\"\n#include \"ui/controls/feature_list.h\"\n#include \"ui/controls/table_rows.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/premium_bubble.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/custom_emoji_text_badge.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/fields/number_input.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/table_layout.h\"\n#include \"ui/color_int_conversion.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_widgets.h\"\n\n#include <QtWidgets/QApplication>\n#include <QtGui/QClipboard>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kAuctionAboutShownPref = \"gift_auction_about_shown\"_cs;\nconstexpr auto kBidPlacedToastDuration = 5 * crl::time(1000);\nconstexpr auto kSwitchPreviewCoverInterval = 3 * crl::time(1000);\nconstexpr auto kMaxShownBid = 30'000;\nconstexpr auto kShowTopPlaces = 3;\n\nenum class BidType {\n\tSetting,\n\tWinning,\n\tLoosing,\n};\nstruct BidRowData {\n\tUserData *user = nullptr;\n\tint stars = 0;\n\tint position = 0;\n\tint winners = 0;\n\tQString place;\n\tBidType type = BidType::Setting;\n\n\tfriend inline bool operator==(\n\t\tconst BidRowData &,\n\t\tconst BidRowData &) = default;\n};\n\nstruct BidSliderValues {\n\tint min = 0;\n\tint explicitlyAllowed = 0;\n\tint max = 0;\n\n\tfriend inline bool operator==(\n\t\tconst BidSliderValues &,\n\t\tconst BidSliderValues &) = default;\n};\n\n[[nodiscard]] std::optional<QColor> BidColorOverride(int position, int per) {\n\treturn (position <= per)\n\t\t? st::boxTextFgGood->c\n\t\t: st::attentionButtonFg->c;\n\t//switch (type) {\n\t//case BidType::Setting: return {};\n\t//case BidType::Winning: return st::boxTextFgGood->c;\n\t//case BidType::Loosing: return st::attentionButtonFg->c;\n\t//}\n\t//Unexpected(\"Type in BidType.\");\n}\n\n[[nodiscard]] rpl::producer<int> MinutesLeftTillValue(TimeId endDate) {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tconst auto now = base::unixtime::now();\n\t\tif (endDate <= now) {\n\t\t\tconsumer.put_next(0);\n\t\t\tconsumer.put_done();\n\t\t\treturn lifetime;\n\t\t}\n\n\t\tconst auto timer = lifetime.make_state<base::Timer>();\n\t\tconst auto callback = [=] {\n\t\t\tconst auto now = base::unixtime::now();\n\t\t\tconst auto left = (endDate > now) ? endDate - now : 0;\n\t\t\tconst auto minutes = (left + 59) / 60;\n\t\t\tconsumer.put_next_copy(minutes);\n\t\t\tif (minutes) {\n\t\t\t\tconst auto next = left % 60;\n\t\t\t\tconst auto wait = next ? next : 60;\n\t\t\t\ttimer->callOnce(wait * crl::time(1000));\n\t\t\t} else {\n\t\t\t\tconsumer.put_done();\n\t\t\t}\n\t\t};\n\t\ttimer->setCallback(callback);\n\t\tcallback();\n\n\t\treturn lifetime;\n\t};\n}\n\n[[nodiscard]] rpl::producer<int> SecondsLeftTillValue(TimeId endDate) {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto left = (endDate > now) ? endDate - now : 0;\n\t\tif (!left) {\n\t\t\tconsumer.put_next(0);\n\t\t\tconsumer.put_done();\n\t\t\treturn lifetime;\n\t\t}\n\n\t\tconst auto starts = crl::now();\n\t\tconst auto ends = starts + left * crl::time(1000);\n\t\tconst auto timer = lifetime.make_state<base::Timer>();\n\t\tconst auto callback = [=] {\n\t\t\tconst auto now = crl::now();\n\t\t\tconst auto left = (ends > now) ? ends - now : 0;\n\t\t\tconst auto seconds = (left + 999) / 1000;\n\t\t\tconsumer.put_next_copy(seconds);\n\t\t\tif (seconds) {\n\t\t\t\tconst auto next = left % 1000;\n\t\t\t\tconst auto wait = next ? next : 1000;\n\t\t\t\ttimer->callOnce(wait);\n\t\t\t} else {\n\t\t\t\tconsumer.put_done();\n\t\t\t}\n\t\t};\n\t\ttimer->setCallback(callback);\n\t\tcallback();\n\n\t\treturn lifetime;\n\t};\n}\n\n[[nodiscard]] QString NiceCountdownText(int seconds) {\n\tconst auto minutes = seconds / 60;\n\tconst auto hours = minutes / 60;\n\treturn hours\n\t\t? u\"%1:%2:%3\"_q\n\t\t.arg(hours)\n\t\t.arg((minutes % 60), 2, 10, QChar('0'))\n\t\t.arg((seconds % 60), 2, 10, QChar('0'))\n\t\t: u\"%1:%2\"_q.arg(minutes).arg((seconds % 60), 2, 10, QChar('0'));\n}\n\n[[nodiscard]] object_ptr<RpWidget> MakeBidRow(\n\t\tnot_null<RpWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\trpl::producer<BidRowData> data) {\n\tauto result = object_ptr<RpWidget>(parent.get());\n\tconst auto raw = result.data();\n\n\traw->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tstruct State {\n\t\tstd::unique_ptr<FlatLabel> place;\n\t\tstd::unique_ptr<UserpicButton> userpic;\n\t\tstd::unique_ptr<FlatLabel> name;\n\t\tstd::unique_ptr<FlatLabel> stars;\n\t\tUserData *user = nullptr;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>();\n\tstate->place = std::make_unique<FlatLabel>(\n\t\traw,\n\t\trpl::duplicate(data) | rpl::map(&BidRowData::place),\n\t\tst::auctionBidPlace);\n\n\tauto name = rpl::duplicate(data) | rpl::map([](const BidRowData &bid) {\n\t\treturn bid.user ? bid.user->name() : QString();\n\t});\n\tstate->name = std::make_unique<FlatLabel>(\n\t\traw,\n\t\tstd::move(name),\n\t\tst::auctionBidName);\n\n\tauto helper = Text::CustomEmojiHelper(Core::TextContext({\n\t\t.session = &show->session(),\n\t}));\n\tconst auto star = helper.paletteDependent(Ui::Earn::IconCreditsEmoji());\n\tauto stars = rpl::duplicate(data) | rpl::map([=](const BidRowData &bid) {\n\t\treturn tr::marked(star).append(' ').append(\n\t\t\tLang::FormatCountDecimal(bid.stars));\n\t});\n\tstate->stars = std::make_unique<FlatLabel>(\n\t\traw,\n\t\tstd::move(stars),\n\t\tst::auctionBidStars,\n\t\tst::defaultPopupMenu,\n\t\thelper.context());\n\n\tconst auto kHuge = u\"99999\"_q;\n\tconst auto userpicLeft = st::auctionBidPlace.style.font->width(kHuge);\n\n\tstd::move(data) | rpl::on_next([=](BidRowData bid) {\n\t\tstate->place->setTextColorOverride(\n\t\t\tBidColorOverride(bid.position, bid.winners));\n\t\tif (state->user != bid.user) {\n\t\t\tstate->user = bid.user;\n\t\t\tif (state->user) {\n\t\t\t\tif (auto was = state->userpic.release()) {\n\t\t\t\t\twas->hide();\n\t\t\t\t\tcrl::on_main(was, [=] { delete was; });\n\t\t\t\t}\n\t\t\t\tstate->userpic = std::make_unique<UserpicButton>(\n\t\t\t\t\traw,\n\t\t\t\t\tstate->user,\n\t\t\t\t\tst::auctionBidUserpic);\n\t\t\t\tstate->userpic->show();\n\t\t\t\tstate->userpic->moveToLeft(userpicLeft, 0);\n\t\t\t\traw->resize(raw->width(), state->userpic->height());\n\t\t\t} else {\n\t\t\t\traw->resize(raw->width(), 0);\n\t\t\t}\n\t\t}\n\t}, raw->lifetime());\n\n\trpl::combine(\n\t\traw->widthValue(),\n\t\tstate->stars->widthValue()\n\t) | rpl::on_next([=](int outer, int stars) {\n\t\tconst auto userpicSize = st::auctionBidUserpic.size;\n\t\tconst auto top = (userpicSize.height() - st::normalFont->height) / 2;\n\t\tstate->place->moveToLeft(0, top, outer);\n\t\tif (state->userpic) {\n\t\t\tstate->userpic->moveToLeft(userpicLeft, 0, outer);\n\t\t}\n\t\tstate->stars->moveToRight(0, top, outer);\n\n\t\tconst auto userpicRight = userpicLeft + userpicSize.width();\n\t\tconst auto nameLeft = userpicRight + st::auctionBidSkip;\n\t\tconst auto nameRight = stars + st::auctionBidSkip;\n\t\tstate->name->resizeToWidth(outer - nameLeft - nameRight);\n\t\tstate->name->moveToLeft(nameLeft, top, outer);\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\nFn<void(not_null<Ui::PopupMenu*>)> MakeAuctionFillMenuCallback(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::GiftAuctionState &state) {\n\tconst auto url = show->session().createInternalLinkFull(\n\t\tu\"auction/\"_q + state.gift->auctionSlug);\n\tconst auto rounds = state.totalRounds;\n\tconst auto perRound = state.gift->auctionGiftsPerRound;;\n\treturn [=](not_null<Ui::PopupMenu*> menu) {\n\t\tmenu->addAction(tr::lng_auction_menu_about(tr::now), [=] {\n\t\t\tshow->show(Box(AuctionAboutBox, rounds, perRound, nullptr));\n\t\t}, &st::menuIconInfo);\n\n\t\tmenu->addAction(tr::lng_auction_menu_copy_link(tr::now), [=] {\n\t\t\tQApplication::clipboard()->setText(url);\n\t\t\tshow->showToast(tr::lng_username_copied(tr::now));\n\t\t}, &st::menuIconLink);\n\n\t\tmenu->addAction(tr::lng_auction_menu_share(tr::now), [=] {\n\t\t\tFastShareLink(show, url);\n\t\t}, &st::menuIconShare);\n\t};\n}\n\nFn<void()> MakeAuctionMenuCallback(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::GiftAuctionState &state) {\n\tconst auto menu = std::make_shared<base::unique_qptr<PopupMenu>>();\n\treturn [=, fill = MakeAuctionFillMenuCallback(show, state)] {\n\t\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tparent,\n\t\t\tst::popupMenuWithIcons);\n\t\tfill(menu->get());\n\t\t(*menu)->popup(QCursor::pos());\n\t};\n}\n\nvoid PlaceAuctionBid(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> to,\n\t\tint64 amount,\n\t\tconst Data::GiftAuctionState &state,\n\t\tstd::unique_ptr<Info::PeerGifts::GiftSendDetails> details,\n\t\tFn<void(Payments::CheckoutResult)> done) {\n\tauto paymentDone = [=](\n\t\t\tPayments::CheckoutResult result,\n\t\t\tconst MTPUpdates *updates) {\n\t\tdone(result);\n\t};\n\tconst auto passDetails = (details || !state.my.bid);\n\tconst auto hideName = passDetails && details && details->anonymous;\n\tconst auto text = details ? details->text : TextWithEntities();\n\n\tusing Flag = MTPDinputInvoiceStarGiftAuctionBid::Flag;\n\tconst auto invoice = MTP_inputInvoiceStarGiftAuctionBid(\n\t\tMTP_flags((state.my.bid ? Flag::f_update_bid : Flag())\n\t\t\t| (passDetails ? Flag::f_peer : Flag())\n\t\t\t| (passDetails ? Flag::f_message : Flag())\n\t\t\t| (hideName ? Flag::f_hide_name : Flag())),\n\t\tpassDetails ? to->input() : MTP_inputPeerEmpty(),\n\t\tMTP_long(state.gift->id),\n\t\tMTP_long(amount),\n\t\tMTP_textWithEntities(\n\t\t\tMTP_string(text.text),\n\t\t\tApi::EntitiesToMTP(&to->session(), text.entities)));\n\tRequestOurForm(show, invoice, [=](\n\t\t\tuint64 formId,\n\t\t\tCreditsAmount price,\n\t\t\tstd::optional<Payments::CheckoutResult> failure) {\n\t\tif (failure) {\n\t\t\tpaymentDone(*failure, nullptr);\n\t\t} else {\n\t\t\tSubmitStarsForm(\n\t\t\t\tshow,\n\t\t\t\tinvoice,\n\t\t\t\tformId,\n\t\t\t\tprice.whole(),\n\t\t\t\tpaymentDone);\n\t\t}\n\t});\n}\n\nobject_ptr<RpWidget> MakeAuctionInfoBlocks(\n\t\tnot_null<RpWidget*> box,\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<Data::GiftAuctionState> stateValue,\n\t\tFn<void()> setMinimal) {\n\tauto helper = Text::CustomEmojiHelper(Core::TextContext({\n\t\t.session = session,\n\t}));\n\tconst auto star = helper.paletteDependent(Ui::Earn::IconCreditsEmoji());\n\n\tauto bidTitle = rpl::duplicate(\n\t\tstateValue\n\t) | rpl::map([=](const Data::GiftAuctionState &state) {\n\t\tconst auto count = int(state.my.minBidAmount\n\t\t\t? state.my.minBidAmount\n\t\t\t: state.minBidAmount);\n\t\tconst auto text = (count >= 10'000'000)\n\t\t\t? Lang::FormatCountToShort(count).string\n\t\t\t: (count >= 1000'000)\n\t\t\t? Lang::FormatCountToShort(count, true).string\n\t\t\t: Lang::FormatCountDecimal(count);\n\t\treturn tr::marked(star).append(' ').append(text);\n\t});\n\tauto minimal = rpl::duplicate(\n\t\tstateValue\n\t) | rpl::map([=](const Data::GiftAuctionState &state) {\n\t\treturn state.my.minBidAmount\n\t\t\t? state.my.minBidAmount\n\t\t\t: state.minBidAmount;\n\t}) | tr::to_count();\n\tauto untilTitle = rpl::duplicate(\n\t\tstateValue\n\t) | rpl::map([=](const Data::GiftAuctionState &state) {\n\t\treturn SecondsLeftTillValue(state.startDate) | rpl::then(\n\t\t\tSecondsLeftTillValue(state.nextRoundAt\n\t\t\t\t? state.nextRoundAt\n\t\t\t\t: state.endDate));\n\t}) | rpl::flatten_latest(\n\t) | rpl::map(NiceCountdownText) | rpl::map(tr::marked);\n\tauto untilSubtext = rpl::duplicate(\n\t\tstateValue\n\t) | rpl::map([=](const Data::GiftAuctionState &state) {\n\t\tauto preview = SecondsLeftTillValue(\n\t\t\tstate.startDate\n\t\t) | rpl::map(rpl::mappers::_1 > 0) | rpl::distinct_until_changed();\n\t\treturn rpl::conditional(\n\t\t\tstd::move(preview),\n\t\t\ttr::lng_auction_bid_before_start(),\n\t\t\ttr::lng_auction_bid_until());\n\t}) | rpl::flatten_latest();\n\tauto leftTitle = rpl::duplicate(\n\t\tstateValue\n\t) | rpl::map([=](const Data::GiftAuctionState &state) {\n\t\treturn Data::SingleCustomEmoji(\n\t\t\tstate.gift->document\n\t\t).append(' ').append(Lang::FormatCountDecimal(state.giftsLeft));\n\t});\n\tauto left = rpl::duplicate(\n\t\tstateValue\n\t) | rpl::map([=](const Data::GiftAuctionState &state) {\n\t\treturn state.giftsLeft;\n\t}) | tr::to_count();\n\treturn MakeStarSelectInfoBlocks(box, {\n\t\t{\n\t\t\t.title = std::move(bidTitle),\n\t\t\t.subtext = tr::lng_auction_bid_minimal(\n\t\t\t\tlt_count,\n\t\t\t\tstd::move(minimal)),\n\t\t\t.click = setMinimal,\n\t\t},\n\t\t{\n\t\t\t.title = std::move(untilTitle),\n\t\t\t.subtext = std::move(untilSubtext),\n\t\t},\n\t\t{\n\t\t\t.title = std::move(leftTitle),\n\t\t\t.subtext = tr::lng_auction_bid_left(lt_count, std::move(left))\n\t\t},\n\t}, helper.context());\n}\n\nvoid AddBidPlaces(\n\t\tnot_null<GenericBox*> box,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\trpl::producer<Data::GiftAuctionState> value,\n\t\trpl::producer<int> chosen) {\n\tstruct My {\n\t\tBidType type;\n\t\tint position = 0;\n\n\t\tinline bool operator==(const My &) const = default;\n\t};\n\tstruct State {\n\t\trpl::variable<My> my;\n\t\trpl::variable<bool> started;\n\t\trpl::variable<std::vector<BidRowData>> top;\n\t\tstd::vector<Ui::PeerUserpicView> cache;\n\t\tint winners = 0;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\n\trpl::duplicate(\n\t\tvalue\n\t) | rpl::on_next([=](const Data::GiftAuctionState &value) {\n\t\tauto cache = std::vector<Ui::PeerUserpicView>();\n\t\tcache.reserve(value.topBidders.size());\n\t\tfor (const auto &user : value.topBidders) {\n\t\t\tcache.push_back(user->createUserpicView());\n\t\t}\n\t\tstate->winners = value.gift->auctionGiftsPerRound;\n\t\tstate->cache = std::move(cache);\n\t\tstate->started = SecondsLeftTillValue(\n\t\t\tvalue.startDate\n\t\t) | rpl::map(!rpl::mappers::_1);\n\t}, box->lifetime());\n\n\tstate->my = rpl::combine(\n\t\trpl::duplicate(value),\n\t\trpl::duplicate(chosen)\n\t) | rpl::map([=](const Data::GiftAuctionState &value, int chosen) {\n\t\tconst auto my = value.my.bid;\n\t\tconst auto &levels = value.bidLevels;\n\n\t\tauto top = std::vector<BidRowData>();\n\t\ttop.reserve(kShowTopPlaces);\n\t\tconst auto pushTop = [&](auto i) {\n\t\t\tconst auto index = int(i - begin(levels));\n\t\t\tif (top.size() >= kShowTopPlaces\n\t\t\t\t|| index >= value.topBidders.size()) {\n\t\t\t\treturn false;\n\t\t\t} else if (!value.topBidders[index]->isSelf()) {\n\t\t\t\ttop.push_back({ value.topBidders[index], int(i->amount) });\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\n\t\tconst auto setting = (chosen > my);\n\t\tconst auto finishWith = [&](int position) {\n\t\t\tstate->top = std::move(top);\n\t\t\tconst auto type = setting\n\t\t\t\t? BidType::Setting\n\t\t\t\t: (position <= value.gift->auctionGiftsPerRound)\n\t\t\t\t? BidType::Winning\n\t\t\t\t: BidType::Loosing;\n\t\t\treturn My{ type, position };\n\t\t};\n\n\t\tfor (auto i = begin(levels), e = end(levels); i != e; ++i) {\n\t\t\tif (i->amount < chosen\n\t\t\t\t|| (!setting\n\t\t\t\t\t&& i->amount == chosen\n\t\t\t\t\t&& i->date >= value.my.date)) {\n\t\t\t\ttop.push_back({ show->session().user(), chosen });\n\t\t\t\tfor (auto j = i; j != e; ++j) {\n\t\t\t\t\tif (!pushTop(j)) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn finishWith(i->position);\n\t\t\t}\n\t\t\tpushTop(i);\n\t\t}\n\t\ttop.push_back({ show->session().user(), chosen });\n\t\treturn finishWith((levels.empty() ? 0 : levels.back().position) + 1);\n\t});\n\tauto myLabelText = rpl::combine(\n\t\tstate->my.value(),\n\t\tstate->started.value()\n\t) | rpl::map([](My my, bool started) {\n\t\tif (!started) {\n\t\t\treturn tr::lng_auction_bid_your_title();\n\t\t}\n\t\tswitch (my.type) {\n\t\tcase BidType::Setting: return tr::lng_auction_bid_your_title();\n\t\tcase BidType::Winning: return tr::lng_auction_bid_your_winning();\n\t\tcase BidType::Loosing: return tr::lng_auction_bid_your_outbid();\n\t\t}\n\t\tUnexpected(\"Type in BidType.\");\n\t}) | rpl::flatten_latest();\n\tconst auto myLabel = AddSubsectionTitle(\n\t\tbox->verticalLayout(),\n\t\tstd::move(myLabelText));\n\tstate->my.value() | rpl::on_next([=](My my) {\n\t\tmyLabel->setTextColorOverride(\n\t\t\tBidColorOverride(my.position, state->winners));\n\t}, myLabel->lifetime());\n\n\tauto bid = rpl::combine(\n\t\tstate->my.value(),\n\t\trpl::duplicate(chosen)\n\t) | rpl::map([=, user = show->session().user()](My my, int stars) {\n\t\tconst auto position = my.position;\n\t\tconst auto winners = state->winners;\n\t\tconst auto place = QString::number(position);\n\t\treturn BidRowData{ user, stars, position, winners, place, my.type };\n\t});\n\tbox->addRow(MakeBidRow(box, show, std::move(bid)));\n\n\tAddSubsectionTitle(\n\t\tbox->verticalLayout(),\n\t\ttr::lng_auction_bid_winners_title(),\n\t\t{ 0, st::paidReactTitleSkip / 2, 0, 0 });\n\tfor (auto i = 0; i != kShowTopPlaces; ++i) {\n\t\tauto icon = QString::fromUtf8(\"\\xf0\\x9f\\xa5\\x87\");\n\t\ticon.back().unicode() += i;\n\n\t\tauto bid = state->top.value(\n\t\t) | rpl::map([=](const std::vector<BidRowData> &top) {\n\t\t\tauto result = (i < top.size()) ? top[i] : BidRowData();\n\t\t\tresult.place = icon;\n\t\t\treturn result;\n\t\t});\n\t\tbox->addRow(MakeBidRow(box, show, std::move(bid)));\n\t}\n}\n\nvoid EditCustomBid(\n\t\tnot_null<GenericBox*> box,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tFn<void(int)> save,\n\t\trpl::producer<int> minBid,\n\t\tint current) {\n\tbox->setTitle(tr::lng_auction_bid_custom_title());\n\n\tconst auto container = box->verticalLayout();\n\n\tbox->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });\n\n\tconst auto starsField = AddStarsInputField(container, {\n\t\t.value = current,\n\t});\n\n\tconst auto min = box->lifetime().make_state<rpl::variable<int>>(\n\t\tstd::move(minBid));\n\n\tbox->setFocusCallback([=] {\n\t\tstarsField->setFocusFast();\n\t});\n\n\tconst auto submit = [=] {\n\t\tconst auto value = starsField->getLastText().toLongLong();\n\t\tif (value <= min->current() || value > 1'000'000'000) {\n\t\t\tstarsField->showError();\n\t\t\treturn;\n\t\t}\n\t\tsave(value);\n\t\tbox->closeBox();\n\t};\n\tQObject::connect(starsField, &Ui::NumberInput::submitted, submit);\n\n\tbox->addButton(tr::lng_settings_save(), submit);\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nvoid AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {\n\tusing namespace Info::PeerGifts;\n\tstruct State {\n\t\tState(rpl::producer<Data::GiftAuctionState> value)\n\t\t: value(std::move(value)) {\n\t\t}\n\n\t\trpl::variable<Data::GiftAuctionState> value;\n\t\trpl::variable<BidSliderValues> sliderValues;\n\t\trpl::variable<int> chosen;\n\t\trpl::variable<QString> subtext;\n\t\trpl::variable<bool> started;\n\t\tbool placing = false;\n\t};\n\tconst auto state = box->lifetime().make_state<State>(\n\t\tstd::move(args.state));\n\tstate->started = state->value.value(\n\t) | rpl::map([=](const Data::GiftAuctionState &value) {\n\t\treturn value.startDate;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::map([=](TimeId startTime) {\n\t\treturn SecondsLeftTillValue(\n\t\t\tstartTime\n\t\t) | rpl::map([=](int seconds) {\n\t\t\treturn !seconds;\n\t\t});\n\t}) | rpl::flatten_latest();\n\tstate->sliderValues = state->value.value(\n\t) | rpl::map([=](const Data::GiftAuctionState &value) {\n\t\tconst auto mine = int(value.my.bid);\n\t\tconst auto min = std::max(1, int(value.my.minBidAmount\n\t\t\t? value.my.minBidAmount\n\t\t\t: value.minBidAmount));\n\t\tconst auto last = value.bidLevels.empty()\n\t\t\t? 0\n\t\t\t: value.bidLevels.front().amount;\n\t\tauto max = std::max({\n\t\t\tmin + 1,\n\t\t\tkMaxShownBid,\n\t\t\tint(base::SafeRound(mine * 1.2)),\n\t\t});\n\t\tif (max < last * 1.05) {\n\t\t\tmax = int(base::SafeRound(last * 1.2));\n\t\t}\n\t\treturn BidSliderValues{\n\t\t\t.min = min,\n\t\t\t.explicitlyAllowed = mine,\n\t\t\t.max = max,\n\t\t};\n\t});\n\n\tconst auto show = args.show;\n\tconst auto giftId = state->value.current().gift->id;\n\tconst auto &sliderValues = state->sliderValues.current();\n\tstate->chosen = sliderValues.explicitlyAllowed\n\t\t? sliderValues.explicitlyAllowed\n\t\t: sliderValues.min;\n\n\tstate->subtext = rpl::combine(\n\t\tstate->value.value(),\n\t\tstate->chosen.value()\n\t) | rpl::map([=](\n\t\t\tconst Data::GiftAuctionState &value,\n\t\t\tint chosen) {\n\t\tif (value.my.bid == chosen) {\n\t\t\treturn tr::lng_auction_bid_your(tr::now);\n\t\t} else if (chosen == state->sliderValues.current().max) {\n\t\t\treturn tr::lng_auction_bid_custom(tr::now);\n\t\t} else if (value.my.bid && chosen > value.my.bid) {\n\t\t\tconst auto delta = chosen - value.my.bid;\n\t\t\treturn '+' + Lang::FormatCountDecimal(delta);\n\t\t}\n\t\treturn QString();\n\t});\n\n\targs.peer->owner().giftAuctionGots(\n\t) | rpl::on_next([=](const Data::GiftAuctionGot &update) {\n\t\tif (update.giftId == giftId) {\n\t\t\tbox->closeBox();\n\n\t\t\tif (const auto window = show->resolveWindow()) {\n\t\t\t\twindow->showPeerHistory(\n\t\t\t\t\tupdate.to,\n\t\t\t\t\tWindow::SectionShow::Way::ClearStack,\n\t\t\t\t\tShowAtTheEndMsgId);\n\t\t\t}\n\t\t}\n\t}, box->lifetime());\n\n\tconst auto details = args.details\n\t\t? *args.details\n\t\t: std::optional<GiftSendDetails>();\n\tconst auto colorings = show->session().appConfig().groupCallColorings();\n\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setStyle(st::paidReactBox);\n\tbox->setNoContentMargin(true);\n\n\tconst auto content = box->verticalLayout();\n\tAddSkip(content, st::boxTitleClose.height + st::paidReactBubbleTop);\n\n\tconst auto activeFgOverride = [=](int count) {\n\t\tconst auto coloring = Calls::Group::Ui::StarsColoringForCount(\n\t\t\tcolorings,\n\t\t\tcount);\n\t\treturn ColorFromSerialized(coloring.bgLight);\n\t};\n\tconst auto sliderWrap = content->add(\n\t\tobject_ptr<VerticalLayout>(content));\n\tstate->sliderValues.value(\n\t) | rpl::on_next([=](const BidSliderValues &values) {\n\t\tconst auto initial = !sliderWrap->count();\n\t\tif (!initial) {\n\t\t\twhile (sliderWrap->count()) {\n\t\t\t\tdelete sliderWrap->widgetAt(0);\n\t\t\t}\n\t\t\twhile (!sliderWrap->children().isEmpty()) {\n\t\t\t\tdelete sliderWrap->children().front();\n\t\t\t}\n\t\t}\n\n\t\tconst auto setCustom = [=] {\n\t\t\tauto min = state->value.value(\n\t\t\t) | rpl::map([=](const Data::GiftAuctionState &state) {\n\t\t\t\treturn std::max(1, int(state.my.minBidAmount\n\t\t\t\t\t? state.my.minBidAmount\n\t\t\t\t\t: state.minBidAmount));\n\t\t\t});\n\t\t\tshow->show(Box(EditCustomBid, show, crl::guard(box, [=](int v) {\n\t\t\t\tstate->chosen = v;\n\t\t\t}), std::move(min), state->chosen.current()));\n\t\t};\n\n\t\tconst auto bubble = AddStarSelectBubble(\n\t\t\tsliderWrap,\n\t\t\tinitial ? BoxShowFinishes(box) : nullptr,\n\t\t\tstate->chosen.value(),\n\t\t\tvalues.max,\n\t\t\tactiveFgOverride);\n\t\tbubble->setAttribute(Qt::WA_TransparentForMouseEvents, false);\n\t\tbubble->setClickedCallback(setCustom);\n\t\tstate->subtext.value() | rpl::on_next([=](QString &&text) {\n\t\t\tbubble->setSubtext(std::move(text));\n\t\t}, bubble->lifetime());\n\n\t\tPaidReactionSlider(\n\t\t\tsliderWrap,\n\t\t\tst::paidReactSlider,\n\t\t\tvalues.min,\n\t\t\tvalues.explicitlyAllowed,\n\t\t\tstate->chosen.value(),\n\t\t\tvalues.max,\n\t\t\t[=](int count) { state->chosen = count; },\n\t\t\tactiveFgOverride);\n\n\t\tsliderWrap->resizeToWidth(st::boxWideWidth);\n\n\t\tconst auto custom = CreateChild<AbstractButton>(sliderWrap);\n\t\tstate->chosen.changes() | rpl::on_next([=] {\n\t\t\tcustom->update();\n\t\t}, custom->lifetime());\n\t\tcustom->show();\n\t\tcustom->setClickedCallback(setCustom);\n\t\tcustom->resize(st::paidReactSlider.width, st::paidReactSlider.width);\n\t\tcustom->paintOn([=](QPainter &p) {\n\t\t\tconst auto rem = st::paidReactSlider.borderWidth * 2;\n\t\t\tconst auto inner = custom->width() - 2 * rem;\n\t\t\tconst auto sub = (inner - 1) / 2;\n\t\t\tconst auto stroke = inner - (2 * sub);\n\t\t\tconst auto color = activeFgOverride(state->chosen.current());\n\t\t\tp.fillRect(rem + sub, rem, stroke, sub, color);\n\t\t\tp.fillRect(rem, rem + sub, inner, stroke, color);\n\t\t\tp.fillRect(rem + sub, rem + inner - sub, stroke, sub, color);\n\t\t});\n\t\tsliderWrap->sizeValue() | rpl::on_next([=](QSize size) {\n\t\t\tcustom->move(\n\t\t\t\tsize.width() - st::boxRowPadding.right() - custom->width(),\n\t\t\t\tsize.height() - custom->height());\n\t\t}, custom->lifetime());\n\t}, sliderWrap->lifetime());\n\n\tbox->addTopButton(\n\t\tst::boxTitleClose,\n\t\t[=] { box->closeBox(); });\n\tif (const auto now = state->value.current(); !now.finished()) {\n\t\tbox->addTopButton(\n\t\t\tst::boxTitleMenu,\n\t\t\tMakeAuctionMenuCallback(box, show, now));\n\t}\n\n\tconst auto skip = st::paidReactTitleSkip;\n\tbox->addRow(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tbox,\n\t\t\trpl::conditional(\n\t\t\t\tstate->started.value(),\n\t\t\t\ttr::lng_auction_bid_title(),\n\t\t\t\ttr::lng_auction_bid_title_early()),\n\t\t\tst::boostCenteredTitle),\n\t\tst::boxRowPadding + QMargins(0, skip / 2, 0, 0),\n\t\tstyle::al_top);\n\n\tauto subtitle = tr::lng_auction_bid_subtitle(\n\t\tlt_count,\n\t\tstate->value.value(\n\t\t) | rpl::map([=](const Data::GiftAuctionState &state) {\n\t\t\treturn state.gift->auctionGiftsPerRound * 1.;\n\t\t}));\n\tbox->addRow(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(subtitle),\n\t\t\tst::auctionCenteredSubtitle),\n\t\tstyle::al_top);\n\n\tconst auto setMinimal = [=] {\n\t\tconst auto &now = state->value.current();\n\t\tstate->chosen = int(now.my.minBidAmount\n\t\t\t? now.my.minBidAmount\n\t\t\t: now.minBidAmount);\n\t};\n\tbox->addRow(\n\t\tMakeAuctionInfoBlocks(\n\t\t\tbox,\n\t\t\t&show->session(),\n\t\t\tstate->value.value(),\n\t\t\tsetMinimal),\n\t\tst::boxRowPadding + QMargins(0, skip / 2, 0, skip));\n\n\tAddBidPlaces(box, show, state->value.value(), state->chosen.value());\n\n\tAddSkip(content);\n\tAddSkip(content);\n\n\tconst auto peer = args.peer;\n\tconst auto button = box->addButton(rpl::single(QString()), [=] {\n\t\tconst auto &current = state->value.current();\n\t\tconst auto amount = state->chosen.current();\n\t\tif (amount <= current.my.bid) {\n\t\t\tbox->closeBox();\n\t\t\treturn;\n\t\t} else if (state->placing) {\n\t\t\treturn;\n\t\t}\n\t\tstate->placing = true;\n\t\tconst auto was = (current.my.bid > 0);\n\t\tconst auto perRound = current.gift->auctionGiftsPerRound;\n\t\tconst auto done = [=](Payments::CheckoutResult result) {\n\t\t\tstate->placing = false;\n\t\t\tif (result == Payments::CheckoutResult::Paid) {\n\t\t\t\tshow->showToast({\n\t\t\t\t\t.title = (was\n\t\t\t\t\t\t? tr::lng_auction_bid_increased_title\n\t\t\t\t\t\t: tr::lng_auction_bid_placed_title)(\n\t\t\t\t\t\t\ttr::now),\n\t\t\t\t\t.text = tr::lng_auction_bid_done_text(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tperRound,\n\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t.icon = &st::auctionBidToastIcon,\n\t\t\t\t\t.iconPadding = st::auctionBidToast.padding,\n\t\t\t\t\t.st = &st::auctionBidToast,\n\t\t\t\t\t.attach = RectPart::Top,\n\t\t\t\t\t.duration = kBidPlacedToastDuration,\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\t\tauto owned = details\n\t\t\t? std::make_unique<GiftSendDetails>(*details)\n\t\t\t: nullptr;\n\t\tPlaceAuctionBid(show, peer, amount, current, std::move(owned), done);\n\t});\n\n\tbutton->setText(rpl::combine(\n\t\tstate->value.value(),\n\t\tstate->chosen.value()\n\t) | rpl::map([=](const Data::GiftAuctionState &state, int count) {\n\t\treturn !state.my.bid\n\t\t\t? tr::lng_auction_bid_place(\n\t\t\t\tlt_stars,\n\t\t\t\trpl::single(CreditsEmojiSmall().append(\n\t\t\t\t\tLang::FormatCountDecimal(count))),\n\t\t\t\ttr::marked)\n\t\t\t: (count <= state.my.bid)\n\t\t\t? tr::lng_box_ok(tr::marked)\n\t\t\t: tr::lng_auction_bid_increase(\n\t\t\t\tlt_stars,\n\t\t\t\trpl::single(CreditsEmojiSmall().append(\n\t\t\t\t\tLang::FormatCountDecimal(count - state.my.bid))),\n\t\t\t\ttr::marked);\n\t}) | rpl::flatten_latest());\n\n\tshow->session().credits().load(true);\n\tAddStarSelectBalance(\n\t\tbox,\n\t\t&show->session(),\n\t\tshow->session().credits().balanceValue());\n}\n\n[[nodiscard]] object_ptr<RpWidget> MakeAveragePriceValue(\n\t\tnot_null<TableLayout*> table,\n\t\tstd::shared_ptr<TableRowTooltipData> tooltip,\n\t\tconst QString &name,\n\t\tint64 amount) {\n\tauto helper = Text::CustomEmojiHelper();\n\tconst auto price = helper.paletteDependent(Earn::IconCreditsEmoji(\n\t)).append(' ').append(\n\t\tLang::FormatCreditsAmountDecimal(CreditsAmount{ amount }));\n\treturn MakeTableValueWithTooltip(\n\t\ttable,\n\t\tstd::move(tooltip),\n\t\tprice,\n\t\ttr::lng_auction_average_tooltip(\n\t\t\ttr::now,\n\t\t\tlt_amount,\n\t\t\ttr::bold(\n\t\t\t\tText::IconEmoji(&st::starIconEmojiInline).append(\n\t\t\t\t\tLang::FormatCountDecimal(amount))),\n\t\t\tlt_gift,\n\t\t\ttr::bold(name),\n\t\t\ttr::marked),\n\t\thelper.context()).widget;\n}\n\n[[nodiscard]] object_ptr<TableLayout> AuctionInfoTable(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<VerticalLayout*> container,\n\t\trpl::producer<Data::GiftAuctionState> value) {\n\tauto result = object_ptr<TableLayout>(parent.get(), st::defaultTable);\n\tconst auto raw = result.data();\n\n\tstruct State {\n\t\trpl::variable<Data::GiftAuctionState> value;\n\t\trpl::variable<bool> finished;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>();\n\tstate->value = std::move(value);\n\n\tconst auto &now = state->value.current();\n\tconst auto preview = (now.startDate > base::unixtime::now());\n\tconst auto name = now.gift->resellTitle;\n\tstate->finished = now.finished()\n\t\t? (rpl::single(true) | rpl::type_erased)\n\t\t: (MinutesLeftTillValue(now.endDate) | rpl::map(!rpl::mappers::_1));\n\n\tconst auto date = [&](TimeId time) {\n\t\treturn rpl::single(\n\t\t\ttr::marked(langDateTime(base::unixtime::parse(time))));\n\t};\n\tAddTableRow(\n\t\traw,\n\t\t(preview\n\t\t\t? tr::lng_auction_starts_label()\n\t\t\t: rpl::conditional(\n\t\t\t\tstate->finished.value(),\n\t\t\t\ttr::lng_gift_link_label_first_sale(),\n\t\t\t\ttr::lng_auction_start_label())),\n\t\tdate(now.startDate));\n\tAddTableRow(\n\t\traw,\n\t\trpl::conditional(\n\t\t\tstate->finished.value(),\n\t\t\ttr::lng_gift_link_label_last_sale(),\n\t\t\ttr::lng_auction_end_label()),\n\t\tdate(now.endDate));\n\tif (preview) {\n\t\tAddTableRow(\n\t\t\traw,\n\t\t\ttr::lng_gift_unique_availability_label(),\n\t\t\trpl::single(tr::marked(\n\t\t\t\tLang::FormatCountDecimal(now.gift->limitedCount))));\n\t\tAddTableRow(\n\t\t\traw,\n\t\t\ttr::lng_auction_rounds_label(),\n\t\t\trpl::single(tr::marked(\n\t\t\t\tLang::FormatCountDecimal(now.totalRounds))));\n\t\tconst auto formatDuration = [&](TimeId value, bool exact) {\n\t\t\treturn (!(value % 3600))\n\t\t\t\t? (exact ? tr::lng_hours : tr::lng_auction_rounds_hours)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tvalue / 3600)\n\t\t\t\t: (!(value % 60))\n\t\t\t\t? (exact ? tr::lng_minutes : tr::lng_auction_rounds_minutes)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tvalue / 60)\n\t\t\t\t: (exact ? tr::lng_seconds : tr::lng_auction_rounds_seconds)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tvalue);\n\t\t};\n\t\tfor (auto i = 0, n = int(now.roundParameters.size()); i != n; ++i) {\n\t\t\tconst auto &that = now.roundParameters[i];\n\t\t\tconst auto next = (i + 1 < n)\n\t\t\t\t? now.roundParameters[i + 1]\n\t\t\t\t: Data::GiftAuctionRound{ now.totalRounds + 1 };\n\t\t\tconst auto exact = (next.number == that.number + 1);\n\t\t\tconst auto extended = that.extendTop && that.extendDuration;\n\t\t\tconst auto duration = formatDuration(that.duration, exact);\n\t\t\tconst auto value = extended\n\t\t\t\t? tr::lng_auction_rounds_extended(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_duration,\n\t\t\t\t\tduration,\n\t\t\t\t\tlt_increase,\n\t\t\t\t\tformatDuration(that.extendDuration, true),\n\t\t\t\t\tlt_n,\n\t\t\t\t\tQString::number(that.extendTop))\n\t\t\t\t: duration;\n\t\t\tAddTableRow(\n\t\t\t\traw,\n\t\t\t\t(exact\n\t\t\t\t\t? tr::lng_auction_rounds_exact(\n\t\t\t\t\t\tlt_n,\n\t\t\t\t\t\trpl::single(QString::number(that.number)))\n\t\t\t\t\t: tr::lng_auction_rounds_range(\n\t\t\t\t\t\tlt_n,\n\t\t\t\t\t\trpl::single(QString::number(that.number)),\n\t\t\t\t\t\tlt_last,\n\t\t\t\t\t\trpl::single(QString::number(next.number - 1)))),\n\t\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\t\traw,\n\t\t\t\t\tvalue,\n\t\t\t\t\tst::auctionInfoValueMultiline));\n\t\t}\n\t} else {\n\t\tauto roundText = state->value.value(\n\t\t) | rpl::map([](const Data::GiftAuctionState &state) {\n\t\t\tconst auto wrapped = [](int count) {\n\t\t\t\treturn rpl::single(tr::marked(Lang::FormatCountDecimal(count)));\n\t\t\t};\n\t\t\treturn tr::lng_auction_round_value(\n\t\t\t\tlt_n,\n\t\t\t\twrapped(state.currentRound),\n\t\t\t\tlt_amount,\n\t\t\t\twrapped(state.totalRounds),\n\t\t\t\ttr::marked);\n\t\t}) | rpl::flatten_latest();\n\t\tconst auto round = AddTableRow(\n\t\t\traw,\n\t\t\ttr::lng_auction_round_label(),\n\t\t\tstd::move(roundText));\n\n\t\tauto availabilityText = state->value.value(\n\t\t) | rpl::map([](const Data::GiftAuctionState &state) {\n\t\t\tconst auto wrapped = [](int count) {\n\t\t\t\treturn rpl::single(tr::marked(Lang::FormatCountDecimal(count)));\n\t\t\t};\n\t\t\treturn tr::lng_auction_availability_value(\n\t\t\t\tlt_n,\n\t\t\t\twrapped(state.giftsLeft),\n\t\t\t\tlt_amount,\n\t\t\t\twrapped(state.gift->limitedCount),\n\t\t\t\ttr::marked);\n\t\t}) | rpl::flatten_latest();\n\t\tAddTableRow(\n\t\t\traw,\n\t\t\ttr::lng_auction_availability_label(),\n\t\t\tstd::move(availabilityText));\n\n\t\tconst auto tooltip = std::make_shared<TableRowTooltipData>(\n\t\t\tTableRowTooltipData{ .parent = container });\n\t\tstate->value.value(\n\t\t) | rpl::map([](const Data::GiftAuctionState &state) {\n\t\t\treturn state.averagePrice;\n\t\t}) | rpl::filter(\n\t\t\trpl::mappers::_1 != 0\n\t\t) | rpl::take(\n\t\t\t1\n\t\t) | rpl::on_next([=](int64 price) {\n\t\t\tdelete round;\n\n\t\t\traw->insertRow(\n\t\t\t\t2,\n\t\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\t\traw,\n\t\t\t\t\ttr::lng_auction_average_label(),\n\t\t\t\t\traw->st().defaultLabel),\n\t\t\t\tMakeAveragePriceValue(raw, tooltip, name, price),\n\t\t\t\tst::giveawayGiftCodeLabelMargin,\n\t\t\t\tst::giveawayGiftCodeValueMargin);\n\t\t\traw->resizeToWidth(raw->widthNoMargins());\n\t\t}, raw->lifetime());\n\t}\n\treturn result;\n}\n\nvoid AuctionGotGiftsBox(\n\t\tnot_null<GenericBox*> box,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::StarGift &gift,\n\t\tstd::vector<Data::GiftAcquired> list) {\n\tExpects(!list.empty());\n\n\tconst auto count = int(list.size());\n\tbox->setTitle(\n\t\ttr::lng_auction_bought_title(lt_count, rpl::single(count * 1.)));\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setMaxHeight(st::boxWideWidth * 2);\n\tbox->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });\n\tbox->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });\n\n\tauto helper = Text::CustomEmojiHelper(Core::TextContext({\n\t\t.session = &show->session(),\n\t}));\n\tconst auto emoji = Data::SingleCustomEmoji(gift.document);\n\tconst auto container = box->verticalLayout();\n\tranges::sort(list, ranges::less(), &Data::GiftAcquired::round);\n\tfor (const auto &entry : list) {\n\t\tconst auto table = container->add(\n\t\t\tobject_ptr<Ui::TableLayout>(\n\t\t\t\tcontainer,\n\t\t\t\tst::giveawayGiftCodeTable),\n\t\t\tst::giveawayGiftCodeTableMargin);\n\t\tconst auto addFullWidth = [&](rpl::producer<TextWithEntities> text) {\n\t\t\ttable->addRow(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\ttable,\n\t\t\t\t\tstd::move(text),\n\t\t\t\t\tst::giveawayGiftMessage,\n\t\t\t\t\tst::defaultPopupMenu,\n\t\t\t\t\thelper.context()),\n\t\t\t\tnullptr,\n\t\t\t\tst::giveawayGiftCodeLabelMargin,\n\t\t\t\tst::giveawayGiftCodeValueMargin);\n\t\t};\n\n\t\t// Title \"Gift #number in round #n\"\n\t\taddFullWidth(tr::lng_auction_bought_in_round(\n\t\t\tlt_name,\n\t\t\trpl::single(tr::marked(\n\t\t\t\temoji\n\t\t\t).append(' ').append(\n\t\t\t\tData::UniqueGiftName(gift.resellTitle, entry.number)\n\t\t\t)),\n\t\t\tlt_n,\n\t\t\trpl::single(tr::marked(QString::number(entry.round))),\n\t\t\ttr::bold));\n\n\t\t// Recipient\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_credits_box_history_entry_peer(),\n\t\t\tshow,\n\t\t\tentry.to->id);\n\n\t\t// Date\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_auction_bought_date(),\n\t\t\trpl::single(tr::marked(\n\t\t\t\tlangDateTime(base::unixtime::parse(entry.date)))));\n\n\t\t// Accepted Bid\n\t\tauto accepted = helper.paletteDependent(\n\t\t\tUi::Earn::IconCreditsEmoji()\n\t\t).append(\n\t\t\tLang::FormatCountDecimal(entry.bidAmount)\n\t\t).append(' ').append(\n\t\t\thelper.paletteDependent(\n\t\t\t\tText::CustomEmojiTextBadge(\n\t\t\t\t\t'#' + Lang::FormatCountDecimal(entry.position),\n\t\t\t\t\tst::defaultTableSmallButton)));\n\t\tAddTableRow(\n\t\t\ttable,\n\t\t\ttr::lng_auction_bought_bid(),\n\t\t\trpl::single(accepted),\n\t\t\thelper.context());\n\n\t\t// Message\n\t\tif (!entry.message.empty()) {\n\t\t\taddFullWidth(rpl::single(entry.message));\n\t\t}\n\t}\n}\n\n[[nodiscard]] rpl::producer<UniqueGiftCover> MakePreviewAuctionStream(\n\t\tconst Data::StarGift &info,\n\t\trpl::producer<Data::UniqueGiftAttributes> attributes) {\n\tExpects(attributes);\n\n\tconst auto cover = [](Data::UniqueGift gift) {\n\t\treturn UniqueGiftCover{ std::move(gift) };\n\t};\n\tauto initial = Data::UniqueGift{\n\t\t.title = info.resellTitle,\n\t\t.model = Data::UniqueGiftModel{\n\t\t\t.document = info.document,\n\t\t},\n\t\t.pattern = Data::UniqueGiftPattern{\n\t\t\t.document = info.document,\n\t\t},\n\t\t.backdrop = (info.background\n\t\t\t? info.background->backdrop()\n\t\t\t: Data::UniqueGiftBackdrop()),\n\t};\n\treturn rpl::single(cover(initial)) | rpl::then(std::move(\n\t\tattributes\n\t) | rpl::map([=](const Data::UniqueGiftAttributes &values)\n\t-> rpl::producer<UniqueGiftCover> {\n\t\tif (values.backdrops.empty()\n\t\t\t|| values.models.empty()\n\t\t\t|| values.patterns.empty()) {\n\t\t\treturn rpl::never<UniqueGiftCover>();\n\t\t}\n\t\treturn [=](auto consumer) {\n\t\t\tauto lifetime = rpl::lifetime();\n\n\t\t\tstruct State {\n\t\t\t\tData::UniqueGiftAttributes data;\n\t\t\t\tstd::vector<int> modelIndices;\n\t\t\t\tstd::vector<int> patternIndices;\n\t\t\t\tstd::vector<int> backdropIndices;\n\t\t\t};\n\t\t\tconst auto state = lifetime.make_state<State>(State{\n\t\t\t\t.data = values,\n\t\t\t});\n\n\t\t\tconst auto put = [=] {\n\t\t\t\tconst auto index = [](\n\t\t\t\t\t\tstd::vector<int> &indices,\n\t\t\t\t\t\tconst auto &v) {\n\t\t\t\t\tconst auto fill = [&] {\n\t\t\t\t\t\tif (!indices.empty()) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tindices = ranges::views::ints(\n\t\t\t\t\t\t\t0\n\t\t\t\t\t\t) | ranges::views::take(\n\t\t\t\t\t\t\tv.size()\n\t\t\t\t\t\t) | ranges::to_vector;\n\t\t\t\t\t\tranges::shuffle(indices);\n\t\t\t\t\t};\n\t\t\t\t\tfill();\n\t\t\t\t\tconst auto result = indices.back();\n\t\t\t\t\tindices.pop_back();\n\t\t\t\t\tfill();\n\t\t\t\t\tif (indices.back() == result) {\n\t\t\t\t\t\tstd::swap(indices.front(), indices.back());\n\t\t\t\t\t}\n\t\t\t\t\treturn result;\n\t\t\t\t};\n\t\t\t\tauto &models = state->data.models;\n\t\t\t\tauto &patterns = state->data.patterns;\n\t\t\t\tauto &backdrops = state->data.backdrops;\n\t\t\t\tconsumer.put_next(cover({\n\t\t\t\t\t.title = info.resellTitle,\n\t\t\t\t\t.model = models[index(state->modelIndices, models)],\n\t\t\t\t\t.pattern = patterns[index(state->patternIndices, patterns)],\n\t\t\t\t\t.backdrop = backdrops[index(state->backdropIndices, backdrops)],\n\t\t\t\t}));\n\t\t\t};\n\n\t\t\tput();\n\t\t\tbase::timer_each(\n\t\t\t\tkSwitchPreviewCoverInterval / 3\n\t\t\t) | rpl::on_next(put, lifetime);\n\n\t\t\treturn lifetime;\n\t\t};\n\t}) | rpl::flatten_latest());\n}\n\nvoid AuctionInfoBox(\n\t\tnot_null<GenericBox*> box,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<Data::GiftAuctionState> value) {\n\tusing namespace Info::PeerGifts;\n\n\tstruct State {\n\t\texplicit State(not_null<Main::Session*> session)\n\t\t: delegate(session, GiftButtonMode::Minimal) {\n\t\t}\n\n\t\tDelegate delegate;\n\t\trpl::variable<Data::GiftAuctionState> value;\n\t\trpl::variable<int> minutesTillEnd;\n\t\trpl::variable<int> secondsTillStart;\n\t\trpl::variable<Data::UniqueGiftAttributes> attributes;\n\n\t\tstd::vector<Data::GiftAcquired> acquired;\n\t\tbool acquiredRequested = false;\n\n\t\tbase::unique_qptr<PopupMenu> menu;\n\n\t\trpl::lifetime previewLifetime;\n\t\tbool previewRequested = false;\n\t};\n\tconst auto show = window->uiShow();\n\tconst auto state = box->lifetime().make_state<State>(&show->session());\n\tstate->value = std::move(value);\n\tconst auto &now = state->value.current();\n\tconst auto auctions = &show->session().giftAuctions();\n\tconst auto giftId = now.gift->id;\n\tif (auto attributes = auctions->attributes(giftId)) {\n\t\tstate->attributes = std::move(*attributes);\n\t}  else {\n\t\tauctions->requestAttributes(giftId, crl::guard(box, [=] {\n\t\t\tstate->attributes.force_assign(*auctions->attributes(giftId));\n\t\t}));\n\t}\n\tstate->minutesTillEnd = MinutesLeftTillValue(now.endDate);\n\tstate->secondsTillStart = SecondsLeftTillValue(now.startDate);\n\tconst auto started = !state->secondsTillStart.current();\n\n\tbox->setStyle(st::giftBox);\n\tbox->setNoContentMargin(true);\n\n\tconst auto container = box->verticalLayout();\n\tauto gift = MakePreviewAuctionStream(\n\t\t*now.gift,\n\t\tstate->attributes.value());\n\tAddUniqueGiftCover(container, std::move(gift), {\n\t\t.pretitle = started ? nullptr : tr::lng_auction_preview_name(),\n\t\t.subtitle = tr::lng_auction_preview_learn_gifts(\n\t\t\tlt_arrow,\n\t\t\trpl::single(Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\ttr::link),\n\t\t.subtitleClick = [=] {\n\t\t\tShowPremiumPreviewBox(window, PremiumFeature::Gifts);\n\t\t},\n\t\t.subtitleLinkColored = true,\n\t});\n\tAddSkip(container, st::defaultVerticalListSkip * 2);\n\n\tSettings::AddUniqueCloseMoreButton(\n\t\tbox,\n\t\t{},\n\t\tnow.finished() ? nullptr : MakeAuctionFillMenuCallback(show, now));\n\n\tbox->addRow(\n\t\tAuctionInfoTable(box, box->verticalLayout(), state->value.value()),\n\t\tst::boxRowPadding + st::auctionInfoTableMargin);\n\n\tif (const auto got = now.my.gotCount) {\n\t\tbox->addRow(\n\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_auction_bought(\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\trpl::single(1. * got),\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\trpl::single(Data::SingleCustomEmoji(\n\t\t\t\t\t\tstate->value.current().gift->document)),\n\t\t\t\t\tlt_arrow,\n\t\t\t\t\trpl::single(Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\t\t\ttr::link),\n\t\t\t\tst::uniqueGiftValueAvailableLink,\n\t\t\t\tst::defaultPopupMenu,\n\t\t\t\tCore::TextContext({ .session = &show->session() })),\n\t\t\tst::boxRowPadding + st::uniqueGiftValueAvailableMargin,\n\t\t\tstyle::al_top\n\t\t)->setClickHandlerFilter([=](const auto &...) {\n\t\t\tconst auto &value = state->value.current();\n\t\t\tconst auto &gift = *value.gift;\n\t\t\tif (!value.my.gotCount) {\n\t\t\t\treturn false;\n\t\t\t} else if (state->acquired.size() == value.my.gotCount) {\n\t\t\t\tshow->show(Box(\n\t\t\t\t\tAuctionGotGiftsBox,\n\t\t\t\t\tshow,\n\t\t\t\t\tgift,\n\t\t\t\t\tstate->acquired));\n\t\t\t} else if (!state->acquiredRequested) {\n\t\t\t\tstate->acquiredRequested = true;\n\t\t\t\tauctions->requestAcquired(\n\t\t\t\t\tvalue.gift->id,\n\t\t\t\t\tcrl::guard(box, [=](\n\t\t\t\t\t\t\tstd::vector<Data::GiftAcquired> result) {\n\t\t\t\t\t\tstate->acquiredRequested = false;\n\t\t\t\t\t\tstate->acquired = std::move(result);\n\t\t\t\t\t\tif (!state->acquired.empty()) {\n\t\t\t\t\t\t\tshow->show(Box(\n\t\t\t\t\t\t\t\tAuctionGotGiftsBox,\n\t\t\t\t\t\t\t\tshow,\n\t\t\t\t\t\t\t\tgift,\n\t\t\t\t\t\t\t\tstate->acquired));\n\t\t\t\t\t\t}\n\t\t\t\t\t}));\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t} else if (const auto variants = now.gift->upgradeVariants) {\n\t\tusing namespace Data;\n\t\tstate->attributes.value(\n\t\t) | rpl::filter([](const UniqueGiftAttributes &list) {\n\t\t\treturn !list.models.empty();\n\t\t}) | rpl::take(\n\t\t\t1\n\t\t) | rpl::on_next([=](const UniqueGiftAttributes &list) {\n\t\t\tauto emoji = tr::marked();\n\t\t\tconst auto indices = RandomIndicesSubset(list.models.size(), 3);\n\t\t\tfor (const auto index : indices) {\n\t\t\t\temoji.append(Data::SingleCustomEmoji(\n\t\t\t\t\tlist.models[index].document));\n\t\t\t}\n\t\t\tbox->addRow(\n\t\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\t\tbox,\n\t\t\t\t\ttr::lng_auction_preview_variants(\n\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\trpl::single(1. * variants),\n\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\trpl::single(emoji),\n\t\t\t\t\t\tlt_arrow,\n\t\t\t\t\t\trpl::single(Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\t\t\t\ttr::link),\n\t\t\t\t\tst::uniqueGiftValueAvailableLink,\n\t\t\t\t\tst::defaultPopupMenu,\n\t\t\t\t\tCore::TextContext({ .session = &show->session() })),\n\t\t\t\tst::boxRowPadding + st::uniqueGiftValueAvailableMargin,\n\t\t\t\tstyle::al_top\n\t\t\t)->setClickHandlerFilter([=](const auto &...) {\n\t\t\t\tconst auto title = now.gift->resellTitle;\n\t\t\t\tconst auto type = Data::GiftAttributeIdType::Model;\n\t\t\t\tconst auto null = nullptr;\n\t\t\t\tshow->show(Box(StarGiftPreviewBox, title, list, type, null));\n\t\t\t\treturn false;\n\t\t\t});\n\t\t}, box->lifetime());\n\t}\n\tconst auto button = box->addButton(rpl::single(QString()), [=] {\n\t\tif (state->value.current().finished()\n\t\t\t|| !state->minutesTillEnd.current()) {\n\t\t\tbox->closeBox();\n\t\t\treturn;\n\t\t}\n\t\tconst auto sendBox = show->show(Box(\n\t\t\tSendGiftBox,\n\t\t\twindow,\n\t\t\tpeer,\n\t\t\tnullptr,\n\t\t\tGiftTypeStars{ .info = *state->value.current().gift },\n\t\t\tstate->value.value()));\n\t\tsendBox->boxClosing(\n\t\t) | rpl::on_next([=] {\n\t\t\tbox->closeBox();\n\t\t}, box->lifetime());\n\t});\n\n\tSetAuctionButtonCountdownText(\n\t\tbutton,\n\t\tAuctionButtonCountdownType::Join,\n\t\tstate->value.value());\n}\n\nbase::weak_qptr<BoxContent> ChooseAndShowAuctionBox(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<rpl::variable<Data::GiftAuctionState>> state,\n\t\tFn<void()> boxClosed) {\n\tconst auto local = &peer->session().local();\n\tconst auto &current = state->current();\n\tconst auto now = base::unixtime::now();\n\tconst auto started = (current.startDate <= now);\n\tconst auto finished = current.finished() || (current.endDate <= now);\n\tconst auto showBidBox = current.my.bid\n\t\t&& !finished\n\t\t&& (!current.my.to || current.my.to == peer);\n\tconst auto showChangeRecipient = !showBidBox\n\t\t&& current.my.bid\n\t\t&& !finished;\n\tconst auto showInfoBox = !showBidBox\n\t\t&& !showChangeRecipient\n\t\t&& (!started\n\t\t\t|| finished\n\t\t\t|| local->readPref<bool>(kAuctionAboutShownPref));\n\tauto box = base::weak_qptr<BoxContent>();\n\tif (showBidBox) {\n\t\tbox = window->show(MakeAuctionBidBox({\n\t\t\t.peer = peer,\n\t\t\t.show = window->uiShow(),\n\t\t\t.state = state->value(),\n\t\t}));\n\t} else if (showChangeRecipient) {\n\t\tconst auto change = [=](Fn<void()> close) {\n\t\t\tconst auto sendBox = window->show(Box(\n\t\t\t\tSendGiftBox,\n\t\t\t\twindow,\n\t\t\t\tpeer,\n\t\t\t\tnullptr,\n\t\t\t\tInfo::PeerGifts::GiftTypeStars{\n\t\t\t\t\t.info = *current.gift,\n\t\t\t\t},\n\t\t\t\tstate->value()));\n\t\t\tsendBox->boxClosing(\n\t\t\t) | rpl::on_next(close, sendBox->lifetime());\n\t\t};\n\t\tconst auto from = current.my.to;\n\t\tconst auto text = (from->isSelf()\n\t\t\t? tr::lng_auction_change_already_me(tr::now, tr::rich)\n\t\t\t: tr::lng_auction_change_already(\n\t\t\t\ttr::now,\n\t\t\t\tlt_name,\n\t\t\t\ttr::bold(from->name()),\n\t\t\t\ttr::rich)).append(' ').append(peer->isSelf()\n\t\t\t\t\t? tr::lng_auction_change_to_me(tr::now, tr::rich)\n\t\t\t\t\t: tr::lng_auction_change_to(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\ttr::bold(peer->name()),\n\t\t\t\t\t\ttr::rich));\n\t\tbox = window->show(Box([=](not_null<GenericBox*> box) {\n\t\t\tbox->addRow(\n\t\t\t\tCreateUserpicsTransfer(\n\t\t\t\t\tbox,\n\t\t\t\t\trpl::single(std::vector{ not_null<PeerData*>(from) }),\n\t\t\t\t\tpeer,\n\t\t\t\t\tUserpicsTransferType::AuctionRecipient),\n\t\t\t\tst::boxRowPadding + st::auctionChangeRecipientPadding\n\t\t\t)->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t\t\tConfirmBox(box, {\n\t\t\t\t.text = text,\n\t\t\t\t.confirmed = change,\n\t\t\t\t.confirmText = tr::lng_auction_change_button(),\n\t\t\t\t.title = tr::lng_auction_change_title(),\n\t\t\t});\n\t\t}));\n\t} else if (showInfoBox) {\n\t\tbox = window->show(Box(\n\t\t\tAuctionInfoBox,\n\t\t\twindow,\n\t\t\tpeer,\n\t\t\tstate->value()));\n\t} else {\n\t\tlocal->writePref<bool>(kAuctionAboutShownPref, true);\n\t\tconst auto understood = [=](Fn<void()> close) {\n\t\t\tChooseAndShowAuctionBox(window, peer, state, close);\n\t\t};\n\t\tbox = window->show(Box(\n\t\t\tAuctionAboutBox,\n\t\t\tcurrent.totalRounds,\n\t\t\tcurrent.gift->auctionGiftsPerRound,\n\t\t\tunderstood));\n\t}\n\tif (const auto strong = box.get()) {\n\t\tstrong->boxClosing(\n\t\t) | rpl::on_next(boxClosed, strong->lifetime());\n\t} else {\n\t\tboxClosed();\n\t}\n\treturn box;\n}\n\n} // namespace\n\nrpl::lifetime ShowStarGiftAuction(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tPeerData *peer,\n\t\tuint64 giftId,\n\t\tFn<void()> finishRequesting,\n\t\tFn<void()> boxClosed) {\n\tconst auto weak = base::make_weak(controller);\n\tconst auto session = &controller->session();\n\tstruct State {\n\t\trpl::variable<Data::GiftAuctionState> value;\n\t\tbase::weak_qptr<BoxContent> box;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tauto result = session->giftAuctions().state(\n\t\tgiftId\n\t) | rpl::on_next([=](Data::GiftAuctionState &&value) {\n\t\tif (const auto onstack = finishRequesting) {\n\t\t\tonstack();\n\t\t}\n\t\tconst auto initial = !state->value.current().gift.has_value();\n\t\tconst auto already = value.my.to;\n\t\tstate->value = std::move(value);\n\t\tif (initial) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tconst auto to = peer\n\t\t\t\t\t? peer\n\t\t\t\t\t: already\n\t\t\t\t\t? already\n\t\t\t\t\t: strong->session().user();\n\t\t\t\tstate->box = ChooseAndShowAuctionBox(\n\t\t\t\t\tstrong,\n\t\t\t\t\tto,\n\t\t\t\t\tstd::shared_ptr<rpl::variable<Data::GiftAuctionState>>(\n\t\t\t\t\t\tstate,\n\t\t\t\t\t\t&state->value),\n\t\t\t\t\tboxClosed);\n\t\t\t} else {\n\t\t\t\tboxClosed();\n\t\t\t}\n\t\t}\n\t});\n\tresult.add([=] {\n\t\tif (const auto strong = state->box.get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t});\n\treturn result;\n}\n\nobject_ptr<BoxContent> MakeAuctionBidBox(AuctionBidBoxArgs &&args) {\n\treturn Box(AuctionBidBox, std::move(args));\n}\n\nvoid SetAuctionButtonCountdownText(\n\t\tnot_null<RoundButton*> button,\n\t\tAuctionButtonCountdownType type,\n\t\trpl::producer<Data::GiftAuctionState> value) {\n\tstruct State {\n\t\trpl::variable<Data::GiftAuctionState> value;\n\t\trpl::variable<int> minutesTillEnd;\n\t\trpl::variable<int> secondsTillStart;\n\t};\n\tconst auto state = button->lifetime().make_state<State>();\n\tstate->value = std::move(value);\n\n\tconst auto &now = state->value.current();\n\tconst auto preview = (now.startDate > base::unixtime::now());\n\tif (preview) {\n\t\tstate->secondsTillStart = SecondsLeftTillValue(now.startDate);\n\t} else {\n\t\tstate->minutesTillEnd = MinutesLeftTillValue(now.endDate);\n\t}\n\n\tauto buttonTitle = rpl::combine(\n\t\tstate->value.value(),\n\t\t(preview\n\t\t\t? state->secondsTillStart.value()\n\t\t\t: state->minutesTillEnd.value())\n\t) | rpl::map([=](const Data::GiftAuctionState &state, int leftTill) {\n\t\treturn (state.finished() || (!preview && leftTill <= 0))\n\t\t\t? tr::lng_box_ok(tr::marked)\n\t\t\t: preview\n\t\t\t? tr::lng_auction_join_early_bid(tr::marked)\n\t\t\t: (type != AuctionButtonCountdownType::Place)\n\t\t\t? tr::lng_auction_join_button(tr::marked)\n\t\t\t: tr::lng_auction_join_bid(tr::marked);\n\t}) | rpl::flatten_latest();\n\n\tauto buttonSubtitle = rpl::combine(\n\t\tstate->value.value(),\n\t\t(preview\n\t\t\t? state->secondsTillStart.value()\n\t\t\t: state->minutesTillEnd.value())\n\t) | rpl::map([=](\n\t\tconst Data::GiftAuctionState &state,\n\t\tint leftTill\n\t) -> rpl::producer<TextWithEntities> {\n\t\tif (state.finished() || leftTill <= 0) {\n\t\t\treturn rpl::single(TextWithEntities());\n\t\t} else if (preview) {\n\t\t\tconst auto hours = (leftTill / 3600);\n\t\t\tconst auto minutes = (leftTill % 3600) / 60;\n\t\t\tconst auto seconds = (leftTill % 60);\n\t\t\tconst auto time = hours\n\t\t\t\t? u\"%1:%2:%3\"_q\n\t\t\t\t.arg(hours).arg(minutes, 2, 10, QChar('0'))\n\t\t\t\t.arg(seconds, 2, 10, QChar('0'))\n\t\t\t\t: u\"%1:%2\"_q.arg(minutes).arg(seconds, 2, 10, QChar('0'));\n\t\t\treturn tr::lng_auction_join_starts_in(\n\t\t\t\tlt_time,\n\t\t\t\trpl::single(tr::marked(time)),\n\t\t\t\ttr::marked);\n\t\t}\n\t\tconst auto hours = (leftTill / 60);\n\t\tconst auto minutes = leftTill % 60;\n\n\t\tauto value = [](int count) {\n\t\t\treturn rpl::single(tr::marked(QString::number(count)));\n\t\t};\n\t\treturn tr::lng_auction_join_time_left(\n\t\t\tlt_time,\n\t\t\t(hours\n\t\t\t\t? tr::lng_auction_join_time_medium(\n\t\t\t\t\tlt_hours,\n\t\t\t\t\tvalue(hours),\n\t\t\t\t\tlt_minutes,\n\t\t\t\t\tvalue(minutes),\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_auction_join_time_small(\n\t\t\t\t\tlt_minutes,\n\t\t\t\t\tvalue(minutes),\n\t\t\t\t\ttr::marked)),\n\t\t\ttr::marked);\n\t}) | rpl::flatten_latest();\n\n\tSetButtonTwoLabels(\n\t\tbutton,\n\t\tstd::move(buttonTitle),\n\t\tstd::move(buttonSubtitle),\n\t\tst::resaleButtonTitle,\n\t\tst::resaleButtonSubtitle);\n}\n\nvoid AuctionAboutBox(\n\t\tnot_null<GenericBox*> box,\n\t\tint rounds,\n\t\tint giftsPerRound,\n\t\tFn<void(Fn<void()> close)> understood) {\n\tbox->setStyle(st::confcallJoinBox);\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setNoContentMargin(true);\n\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\tbox->closeBox();\n\t});\n\tbox->addRow(\n\t\tCalls::Group::MakeRoundActiveLogo(\n\t\t\tbox,\n\t\t\tst::auctionAboutLogo,\n\t\t\tst::auctionAboutLogoPadding),\n\t\tst::boxRowPadding + st::confcallLinkHeaderIconPadding);\n\n\tbox->addRow(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_auction_about_title(),\n\t\t\tst::boxTitle),\n\t\tst::boxRowPadding,\n\t\tstyle::al_top);\n\tbox->addRow(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_auction_about_subtitle(tr::rich),\n\t\t\tst::confcallLinkCenteredText),\n\t\tst::boxRowPadding + st::auctionAboutTextPadding,\n\t\tstyle::al_top\n\t)->setTryMakeSimilarLines(true);\n\n\tconst auto features = std::vector<FeatureListEntry>{\n\t\t{\n\t\t\tst::menuIconAuctionDrop,\n\t\t\ttr::lng_auction_about_top_title(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tgiftsPerRound),\n\t\t\ttr::lng_auction_about_top_about(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tgiftsPerRound,\n\t\t\t\tlt_rounds,\n\t\t\t\ttr::lng_auction_about_top_rounds(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\trounds,\n\t\t\t\t\ttr::rich),\n\t\t\t\tlt_bidders,\n\t\t\t\ttr::lng_auction_about_top_bidders(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tgiftsPerRound,\n\t\t\t\t\ttr::rich),\n\t\t\t\ttr::rich),\n\t\t},\n\t\t{\n\t\t\tst::menuIconStarsCarryover,\n\t\t\ttr::lng_auction_about_bid_title(tr::now),\n\t\t\ttr::lng_auction_about_bid_about(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tgiftsPerRound,\n\t\t\t\ttr::rich),\n\t\t},\n\t\t{\n\t\t\tst::menuIconStarsRefund,\n\t\t\ttr::lng_auction_about_missed_title(tr::now),\n\t\t\ttr::lng_auction_about_missed_about(tr::now, tr::rich),\n\t\t},\n\t};\n\tfor (const auto &feature : features) {\n\t\tbox->addRow(MakeFeatureListEntry(box, feature));\n\t}\n\n\tconst auto close = Fn<void()>([weak = base::make_weak(box)] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t});\n\tbox->addButton(\n\t\trpl::single(QString()),\n\t\tunderstood ? [=] { understood(close); } : close\n\t)->setText(rpl::single(Text::IconEmoji(\n\t\t&st::infoStarsUnderstood\n\t).append(' ').append(tr::lng_auction_about_understood(tr::now))));\n}\n\nTextWithEntities ActiveAuctionsTitle(const Data::ActiveAuctions &auctions) {\n\tconst auto &list = auctions.list;\n\tif (list.size() == 1) {\n\t\tconst auto auction = list.front();\n\t\treturn Data::SingleCustomEmoji(\n\t\t\tauction->gift->document\n\t\t).append(' ').append(tr::lng_auction_bar_active(tr::now));\n\t}\n\tauto result = tr::marked();\n\tfor (const auto &auction : list | ranges::views::take(3)) {\n\t\tresult.append(Data::SingleCustomEmoji(auction->gift->document));\n\t}\n\treturn result.append(' ').append(\n\t\ttr::lng_auction_bar_active_many(tr::now, lt_count, list.size()));\n}\n\nManyAuctionsState ActiveAuctionsState(const Data::ActiveAuctions &auctions) {\n\tconst auto &list = auctions.list;\n\tconst auto winning = [](not_null<Data::GiftAuctionState*> auction) {\n\t\tconst auto position = MyAuctionPosition(*auction);\n\t\treturn (position <= auction->gift->auctionGiftsPerRound)\n\t\t\t? position\n\t\t\t: 0;\n\t};\n\tif (list.size() == 1) {\n\t\tconst auto auction = list.front();\n\t\tconst auto position = winning(auction);\n\t\tauto text = position\n\t\t\t? tr::lng_auction_bar_winning(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tposition,\n\t\t\t\ttr::marked)\n\t\t\t: tr::lng_auction_bar_outbid(tr::now, tr::marked);\n\t\treturn { std::move(text), !position };\n\t}\n\tauto outbid = 0;\n\tfor (const auto &auction : list) {\n\t\tif (!winning(auction)) {\n\t\t\t++outbid;\n\t\t}\n\t}\n\tauto text = (outbid == list.size())\n\t\t? tr::lng_auction_bar_outbid_all(tr::now, tr::marked)\n\t\t: outbid\n\t\t? tr::lng_auction_bar_outbid_some(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\toutbid,\n\t\t\ttr::marked)\n\t\t: tr::lng_auction_bar_winning_all(tr::now, tr::marked);\n\treturn { std::move(text), outbid != 0 };\n}\n\nrpl::producer<TextWithEntities> ActiveAuctionsButton(\n\t\tconst Data::ActiveAuctions &auctions) {\n\tconst auto &list = auctions.list;\n\tconst auto withIcon = [](const QString &text) {\n\t\tusing namespace Ui::Text;\n\t\treturn IconEmoji(&st::auctionBidEmoji).append(' ').append(text);\n\t};\n\tif (list.size() == 1) {\n\t\tconst auto auction = auctions.list.front();\n\t\tconst auto end = auction->nextRoundAt\n\t\t\t? auction->nextRoundAt\n\t\t\t: auction->endDate;\n\t\treturn SecondsLeftTillValue(end)\n\t\t\t| rpl::map(NiceCountdownText)\n\t\t\t| rpl::map(withIcon);\n\t}\n\treturn tr::lng_auction_bar_view() | rpl::map(withIcon);\n}\n\nstruct Single {\n\tuint64 giftId = 0;\n\tnot_null<DocumentData*> document;\n\tint round = 0;\n\tint total = 0;\n\tint bid = 0;\n\tint position = 0;\n\tint winning = 0;\n\tTimeId ends = 0;\n};\n\nobject_ptr<Ui::RpWidget> MakeActiveAuctionRow(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<DocumentData*> document,\n\t\tuint64 giftId,\n\t\trpl::producer<Single> value) {\n\tauto result = object_ptr<Ui::VerticalLayout>(parent);\n\tconst auto raw = result.data();\n\n\traw->add(object_ptr<Ui::RpWidget>(raw));\n\n\tauto title = rpl::duplicate(value) | rpl::map([=](const Single &fields) {\n\t\treturn tr::lng_auction_bar_round(\n\t\t\ttr::now,\n\t\t\tlt_n,\n\t\t\tQString::number(fields.round + 1),\n\t\t\tlt_amount,\n\t\t\tQString::number(fields.total));\n\t});\n\traw->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\traw,\n\t\t\tstd::move(title),\n\t\t\tst::auctionListTitle),\n\t\tst::auctionListTitlePadding);\n\n\tconst auto tag = Data::CustomEmojiSizeTag::Isolated;\n\tconst auto sticker = std::shared_ptr<Ui::Text::CustomEmoji>(\n\t\tdocument->owner().customEmojiManager().create(\n\t\t\tdocument,\n\t\t\t[=] { raw->update(); },\n\t\t\ttag));\n\n\traw->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto q = QPainter(raw);\n\t\tsticker->paint(q, {\n\t\t\t.textColor = st::windowFg->c,\n\t\t\t.now = crl::now(),\n\t\t\t.position = QPoint(),\n\t\t});\n\t}, raw->lifetime());\n\n\tauto helper = Ui::Text::CustomEmojiHelper();\n\tconst auto star = helper.paletteDependent(Ui::Earn::IconCreditsEmoji());\n\tauto text = rpl::duplicate(value) | rpl::map([=](const Single &fields) {\n\t\tconst auto stars = tr::marked(star).append(' ').append(\n\t\t\tLang::FormatCountDecimal(fields.bid));\n\t\tconst auto outbid = (fields.position > fields.winning);\n\t\treturn outbid\n\t\t\t? tr::lng_auction_bar_bid_outbid(\n\t\t\t\ttr::now,\n\t\t\t\tlt_stars,\n\t\t\t\tstars,\n\t\t\t\ttr::rich)\n\t\t\t: tr::lng_auction_bar_bid_ranked(\n\t\t\t\ttr::now,\n\t\t\t\tlt_stars,\n\t\t\t\tstars,\n\t\t\t\tlt_n,\n\t\t\t\ttr::marked(QString::number(fields.position)),\n\t\t\t\ttr::rich);\n\t});\n\tconst auto subtitle = raw->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\traw,\n\t\t\tstd::move(text),\n\t\t\tst::auctionListText,\n\t\t\tst::defaultPopupMenu,\n\t\t\thelper.context()),\n\t\tst::auctionListTextPadding);\n\trpl::duplicate(value) | rpl::on_next([=](const Single &fields) {\n\t\tconst auto outbid = (fields.position > fields.winning);\n\t\tsubtitle->setTextColorOverride(outbid\n\t\t\t? st::attentionButtonFg->c\n\t\t\t: std::optional<QColor>());\n\t}, subtitle->lifetime());\n\n\tconst auto button = raw->add(\n\t\tobject_ptr<Ui::RoundButton>(\n\t\t\traw,\n\t\t\trpl::single(QString()),\n\t\t\tst::auctionListRaise),\n\t\tst::auctionListRaisePadding);\n\n\tauto secondsLeft = rpl::duplicate(\n\t\tvalue\n\t) | rpl::map([=](const Single &fields) {\n\t\treturn SecondsLeftTillValue(fields.ends);\n\t}) | rpl::flatten_latest();\n\tbutton->setText(rpl::combine(\n\t\tstd::move(secondsLeft),\n\t\ttr::lng_auction_bar_raise_bid()\n\t) | rpl::map([=](int seconds, const QString &text) {\n\t\treturn Ui::Text::IconEmoji(\n\t\t\t&st::auctionBidEmoji\n\t\t).append(' ').append(text).append(' ').append(\n\t\t\tUi::Text::Colorized(NiceCountdownText(seconds)));\n\t}));\n\tbutton->setClickedCallback([=] {\n\t\twindow->showStarGiftAuction(giftId);\n\t});\n\tbutton->setFullRadius(true);\n\traw->widthValue() | rpl::on_next([=](int width) {\n\t\tbutton->setFullWidth(width);\n\t}, button->lifetime());\n\n\treturn result;\n}\n\nFn<void()> ActiveAuctionsCallback(\n\t\tnot_null<Window::SessionController*> window,\n\t\tconst Data::ActiveAuctions &auctions) {\n\tconst auto &list = auctions.list;\n\tconst auto count = int(list.size());\n\tif (count == 1) {\n\t\tconst auto giftId = list.front()->gift->id;\n\t\treturn [=] {\n\t\t\twindow->showStarGiftAuction(giftId);\n\t\t};\n\t}\n\tstruct Auctions {\n\t\tstd::vector<rpl::variable<Single>> list;\n\t};\n\tconst auto state = std::make_shared<Auctions>();\n\tconst auto singleFrom = [](const Data::GiftAuctionState &state) {\n\t\treturn Single{\n\t\t\t.giftId = state.gift->id,\n\t\t\t.document = state.gift->document,\n\t\t\t.round = state.currentRound,\n\t\t\t.total = state.totalRounds,\n\t\t\t.bid = int(state.my.bid),\n\t\t\t.position = MyAuctionPosition(state),\n\t\t\t.winning = state.gift->auctionGiftsPerRound,\n\t\t\t.ends = state.nextRoundAt ? state.nextRoundAt : state.endDate,\n\t\t};\n\t};\n\tfor (const auto &auction : list) {\n\t\tstate->list.push_back(singleFrom(*auction));\n\t}\n\treturn [=] {\n\t\twindow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tconst auto rows = box->lifetime().make_state<\n\t\t\t\trpl::variable<int>\n\t\t\t>(count);\n\n\t\t\tbox->setWidth(st::boxWideWidth);\n\t\t\tbox->setTitle(tr::lng_auction_bar_active_many(\n\t\t\t\tlt_count,\n\t\t\t\trows->value() | tr::to_count()));\n\n\t\t\tconst auto auctions = &window->session().giftAuctions();\n\t\t\tfor (auto &entry : state->list) {\n\t\t\t\tusing Data::GiftAuctionState;\n\n\t\t\t\tconst auto &now = entry.current();\n\t\t\t\tentry = auctions->state(\n\t\t\t\t\tnow.giftId\n\t\t\t\t) | rpl::filter([=](const GiftAuctionState &state) {\n\t\t\t\t\treturn state.my.bid != 0;\n\t\t\t\t}) | rpl::map(singleFrom);\n\n\t\t\t\tconst auto skip = st::auctionListEntrySkip;\n\t\t\t\tconst auto row = box->addRow(\n\t\t\t\t\tMakeActiveAuctionRow(\n\t\t\t\t\t\tbox,\n\t\t\t\t\t\twindow,\n\t\t\t\t\t\tnow.document,\n\t\t\t\t\t\tnow.giftId,\n\t\t\t\t\t\tentry.value()),\n\t\t\t\t\tst::boxRowPadding + QMargins(0, skip, 0, skip));\n\n\t\t\t\tauctions->state(\n\t\t\t\t\tnow.giftId\n\t\t\t\t) | rpl::on_next([=](const GiftAuctionState &state) {\n\t\t\t\t\tif (!state.my.bid) {\n\t\t\t\t\t\tdelete row;\n\t\t\t\t\t\tif (const auto now = rows->current(); now > 1) {\n\t\t\t\t\t\t\t*rows = (now - 1);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tbox->closeBox();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}, row->lifetime());\n\t\t\t}\n\n\t\t\tbox->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });\n\t\t\tbox->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });\n\t\t}));\n\t};\n}\n\nstd::vector<int> RandomIndicesSubset(int total, int subset) {\n\tconst auto take = std::min(total, subset);\n\tif (!take) {\n\t\treturn {};\n\t}\n\tauto result = std::vector<int>();\n\tauto taken = base::flat_set<int>();\n\tresult.reserve(take);\n\ttaken.reserve(take);\n\tfor (auto i = 0; i < take; ++i) {\n\t\tauto index = base::RandomIndex(total - i);\n\t\tfor (const auto already : taken) {\n\t\t\tif (index >= already) {\n\t\t\t\t++index;\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\ttaken.emplace(index);\n\t\tresult.push_back(index);\n\t}\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/star_gift_auction_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nstruct GiftAuctionState;\nstruct ActiveAuctions;\nstruct StarGift;\n} // namespace Data\n\nnamespace Info::PeerGifts {\nstruct GiftSendDetails;\n} // namespace Info::PeerGifts\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\n\nclass BoxContent;\nclass RoundButton;\nclass GenericBox;\n\n[[nodiscard]] rpl::lifetime ShowStarGiftAuction(\n\tnot_null<Window::SessionController*> controller,\n\tPeerData *peer,\n\tuint64 giftId,\n\tFn<void()> finishRequesting,\n\tFn<void()> boxClosed);\n\nstruct AuctionBidBoxArgs {\n\tnot_null<PeerData*> peer;\n\tstd::shared_ptr<ChatHelpers::Show> show;\n\trpl::producer<Data::GiftAuctionState> state;\n\tstd::unique_ptr<Info::PeerGifts::GiftSendDetails> details;\n};\n[[nodiscard]] object_ptr<BoxContent> MakeAuctionBidBox(\n\tAuctionBidBoxArgs &&args);\n\nenum class AuctionButtonCountdownType {\n\tJoin,\n\tPlace,\n\tPreview,\n};\nvoid SetAuctionButtonCountdownText(\n\tnot_null<RoundButton*> button,\n\tAuctionButtonCountdownType type,\n\trpl::producer<Data::GiftAuctionState> value);\n\nvoid AuctionAboutBox(\n\tnot_null<GenericBox*> box,\n\tint rounds,\n\tint giftsPerRound,\n\tFn<void(Fn<void()> close)> understood);\n\n[[nodiscard]] TextWithEntities ActiveAuctionsTitle(\n\tconst Data::ActiveAuctions &auctions);\nstruct ManyAuctionsState {\n\tTextWithEntities text;\n\tbool someOutbid = false;\n};\n[[nodiscard]] ManyAuctionsState ActiveAuctionsState(\n\tconst Data::ActiveAuctions &auctions);\n[[nodiscard]] rpl::producer<TextWithEntities> ActiveAuctionsButton(\n\tconst Data::ActiveAuctions &auctions);\n[[nodiscard]] Fn<void()> ActiveAuctionsCallback(\n\tnot_null<Window::SessionController*> window,\n\tconst Data::ActiveAuctions &auctions);\n\n[[nodiscard]] std::vector<int> RandomIndicesSubset(int total, int subset);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/star_gift_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/star_gift_box.h\"\n\n#include \"boxes/star_gift_cover_box.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_credits.h\"\n#include \"api/api_global_privacy.h\"\n#include \"api/api_premium.h\"\n#include \"api/api_text_entities.h\"\n#include \"base/event_filter.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"base/random.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/filters/edit_filter_chats_list.h\"\n#include \"boxes/peers/edit_peer_color_box.h\"\n#include \"boxes/peers/prepare_short_info_box.h\"\n#include \"boxes/gift_premium_box.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"boxes/send_credits_box.h\"\n#include \"boxes/star_gift_auction_box.h\"\n#include \"boxes/star_gift_preview_box.h\"\n#include \"boxes/star_gift_resale_box.h\"\n#include \"boxes/transfer_gift_box.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"chat_helpers/stickers_gift_box_pack.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\"\n#include \"data/components/gift_auctions.h\"\n#include \"data/components/promo_suggestions.h\"\n#include \"data/data_birthday.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_credits.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"history/admin_log/history_admin_log_item.h\"\n#include \"history/view/controls/history_view_suggest_options.h\"\n#include \"history/view/media/history_view_media_generic.h\"\n#include \"history/view/media/history_view_unique_gift.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"info/channel_statistics/boosts/giveaway/boost_badge.h\" // InfiniteRadialAnimationWidget.\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"info/peer_gifts/info_peer_gifts_common.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_common.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"menu/gift_resale_filter.h\"\n#include \"payments/payments_form.h\"\n#include \"payments/payments_checkout_process.h\"\n#include \"payments/payments_non_panel_process.h\"\n#include \"settings/sections/settings_credits.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/boxes/about_cocoon_box.h\" // Ui::AddUniqueCloseButton\n#include \"ui/boxes/boost_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/emoji_stake_box.h\" // InsufficientTonBox\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/controls/button_labels.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/controls/feature_list.h\"\n#include \"ui/controls/ton_common.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/effects/premium_bubble.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/effects/premium_stars_colored.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/new_badges.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/top_background_gradient.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/fields/number_input.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/table_layout.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_widgets.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kPriceTabAll = 0;\nconstexpr auto kPriceTabMy = -1;\nconstexpr auto kPriceTabCollectibles = -2;\nconstexpr auto kGiftMessageLimit = 255;\nconstexpr auto kSentToastDuration = 3 * crl::time(1000);\nconstexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000);\nconstexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000);\nconstexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000);\nconstexpr auto kResellPriceCacheLifetime = 60 * crl::time(1000);\n\nusing namespace HistoryView;\nusing namespace Info::PeerGifts;\n\nusing Data::GiftAttributeId;\nusing Data::GiftAttributeIdType;\nusing Data::MyGiftsDescriptor;\n\nenum class PickType {\n\tActivate,\n\tSendMessage,\n\tOpenProfile,\n};\nusing PickCallback = Fn<void(not_null<PeerData*>, PickType)>;\n\nstruct PremiumGiftsDescriptor {\n\tstd::vector<GiftTypePremium> list;\n\tstd::shared_ptr<Api::PremiumGiftCodeOptions> api;\n};\n\nstruct SessionResalePrices {\n\texplicit SessionResalePrices(not_null<Main::Session*> session)\n\t: api(std::make_unique<Api::PremiumGiftCodeOptions>(session->user())) {\n\t}\n\n\tstd::unique_ptr<Api::PremiumGiftCodeOptions> api;\n\tbase::flat_map<QString, int> prices;\n\tstd::vector<Fn<void()>> waiting;\n\trpl::lifetime requestLifetime;\n\tcrl::time lastReceived = 0;\n};\n\n[[nodiscard]] not_null<SessionResalePrices*> ResalePrices(\n\t\tnot_null<Main::Session*> session) {\n\tstatic auto result = base::flat_map<\n\t\tnot_null<Main::Session*>,\n\t\tstd::unique_ptr<SessionResalePrices>>();\n\tif (const auto i = result.find(session); i != end(result)) {\n\t\treturn i->second.get();\n\t}\n\tconst auto i = result.emplace(\n\t\tsession,\n\t\tstd::make_unique<SessionResalePrices>(session)).first;\n\tsession->lifetime().add([session] { result.remove(session); });\n\treturn i->second.get();\n}\n\nclass PeerRow final : public PeerListRow {\npublic:\n\tusing PeerListRow::PeerListRow;\n\n\tQSize rightActionSize() const override;\n\tQMargins rightActionMargins() const override;\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\n\tvoid rightActionAddRipple(\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) override;\n\tvoid rightActionStopLastRipple() override;\n\nprivate:\n\tstd::unique_ptr<Ui::RippleAnimation> _actionRipple;\n\n};\n\nQSize PeerRow::rightActionSize() const {\n\treturn QSize(\n\t\tst::inviteLinkThreeDotsIcon.width(),\n\t\tst::inviteLinkThreeDotsIcon.height());\n}\n\nQMargins PeerRow::rightActionMargins() const {\n\treturn QMargins(\n\t\t0,\n\t\t(st::inviteLinkList.item.height - rightActionSize().height()) / 2,\n\t\tst::inviteLinkThreeDotsSkip,\n\t\t0);\n}\n\nvoid PeerRow::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\tif (_actionRipple) {\n\t\t_actionRipple->paint(p, x, y, outerWidth);\n\t\tif (_actionRipple->empty()) {\n\t\t\t_actionRipple.reset();\n\t\t}\n\t}\n\t(actionSelected\n\t\t? st::inviteLinkThreeDotsIconOver\n\t\t: st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth);\n}\n\nvoid PeerRow::rightActionAddRipple(QPoint point, Fn<void()> updateCallback) {\n\tif (!_actionRipple) {\n\t\tauto mask = Ui::RippleAnimation::EllipseMask(\n\t\t\tSize(st::inviteLinkThreeDotsIcon.height()));\n\t\t_actionRipple = std::make_unique<Ui::RippleAnimation>(\n\t\t\tst::defaultRippleAnimation,\n\t\t\tstd::move(mask),\n\t\t\tstd::move(updateCallback));\n\t}\n\t_actionRipple->add(point);\n}\n\nvoid PeerRow::rightActionStopLastRipple() {\n\tif (_actionRipple) {\n\t\t_actionRipple->lastStop();\n\t}\n}\n\nclass PreviewDelegate final : public DefaultElementDelegate {\npublic:\n\tPreviewDelegate(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<ChatStyle*> st,\n\t\tFn<void()> update);\n\n\tbool elementAnimationsPaused() override;\n\tnot_null<PathShiftGradient*> elementPathShiftGradient() override;\n\tContext elementContext() override;\n\nprivate:\n\tconst not_null<QWidget*> _parent;\n\tconst std::unique_ptr<PathShiftGradient> _pathGradient;\n\n};\n\nclass PreviewWrap final : public RpWidget {\npublic:\n\tPreviewWrap(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<PeerData*> recipient,\n\t\trpl::producer<GiftSendDetails> details);\n\t~PreviewWrap();\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid resizeTo(int width);\n\tvoid prepare(rpl::producer<GiftSendDetails> details);\n\n\tconst not_null<History*> _history;\n\tconst not_null<PeerData*> _recipient;\n\tconst std::unique_ptr<ChatTheme> _theme;\n\tconst std::unique_ptr<ChatStyle> _style;\n\tconst std::unique_ptr<PreviewDelegate> _delegate;\n\tAdminLog::OwnedItem _item;\n\tQPoint _position;\n\n};\n\nclass TextBubblePart final : public MediaGenericTextPart {\npublic:\n\tTextBubblePart(\n\t\tTextWithEntities text,\n\t\tQMargins margins,\n\t\tconst style::TextStyle &st = st::defaultTextStyle,\n\t\tconst base::flat_map<uint16, ClickHandlerPtr> &links = {},\n\t\tconst Ui::Text::MarkedContext &context = {},\n\t\tstyle::align align = style::al_top);\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const override;\n\nprivate:\n\tvoid setupPen(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context) const override;\n\tint elisionLines() const override;\n\n};\n\nTextBubblePart::TextBubblePart(\n\tTextWithEntities text,\n\tQMargins margins,\n\tconst style::TextStyle &st,\n\tconst base::flat_map<uint16, ClickHandlerPtr> &links,\n\tconst Ui::Text::MarkedContext &context,\n\tstyle::align align)\n: MediaGenericTextPart(std::move(text), margins, st, links, context, align) {\n}\n\nvoid TextBubblePart::draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const {\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(context.st->msgServiceBg());\n\tconst auto radius = height() / 2.;\n\tconst auto left = (outerWidth - width()) / 2;\n\tconst auto r = QRect(left, 0, width(), height());\n\tp.drawRoundedRect(r, radius, radius);\n\n\tMediaGenericTextPart::draw(p, owner, context, outerWidth);\n}\n\nvoid TextBubblePart::setupPen(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context) const {\n\tauto pen = context.st->msgServiceFg()->c;\n\tpen.setAlphaF(pen.alphaF() * 0.65);\n\tp.setPen(pen);\n}\n\nint TextBubblePart::elisionLines() const {\n\treturn 1;\n}\n\n[[nodiscard]] bool SortForBirthday(not_null<PeerData*> peer) {\n\tconst auto user = peer->asUser();\n\tif (!user) {\n\t\treturn false;\n\t}\n\tconst auto birthday = user->birthday();\n\tif (!birthday) {\n\t\treturn false;\n\t}\n\tconst auto is = [&](const QDate &date) {\n\t\treturn (date.day() == birthday.day())\n\t\t\t&& (date.month() == birthday.month());\n\t};\n\tconst auto now = QDate::currentDate();\n\treturn is(now) || is(now.addDays(1)) || is(now.addDays(-1));\n}\n\n[[nodiscard]] bool IsSoldOut(const Data::StarGift &info) {\n\treturn info.limitedCount && !info.limitedLeft;\n}\n\nstruct UpgradePrice {\n\tTimeId date = 0;\n\tint stars = 0;\n};\n\n[[nodiscard]] std::vector<UpgradePrice> ParsePrices(\n\t\tconst MTPVector<MTPStarGiftUpgradePrice> &list) {\n\treturn ranges::views::all(\n\t\tlist.v\n\t) | ranges::views::transform([](const MTPStarGiftUpgradePrice &price) {\n\t\tconst auto &data = price.data();\n\t\treturn UpgradePrice{\n\t\t\t.date = data.vdate().v,\n\t\t\t.stars = int(data.vupgrade_stars().v),\n\t\t};\n\t}) | ranges::to_vector;\n}\n\nPreviewDelegate::PreviewDelegate(\n\tnot_null<QWidget*> parent,\n\tnot_null<ChatStyle*> st,\n\tFn<void()> update)\n: _parent(parent)\n, _pathGradient(MakePathShiftGradient(st, update)) {\n}\n\nbool PreviewDelegate::elementAnimationsPaused() {\n\treturn _parent->window()->isActiveWindow();\n}\n\nauto PreviewDelegate::elementPathShiftGradient()\n-> not_null<PathShiftGradient*> {\n\treturn _pathGradient.get();\n}\n\nContext PreviewDelegate::elementContext() {\n\treturn Context::History;\n}\n\nauto GenerateGiftMedia(\n\tnot_null<Element*> parent,\n\tElement *replacing,\n\tnot_null<PeerData*> recipient,\n\tconst GiftSendDetails &data)\n-> Fn<void(\n\t\tnot_null<MediaGeneric*>,\n\t\tFn<void(std::unique_ptr<MediaGenericPart>)>)> {\n\treturn [=](\n\t\t\tnot_null<MediaGeneric*> media,\n\t\t\tFn<void(std::unique_ptr<MediaGenericPart>)> push) {\n\t\tconst auto &descriptor = data.descriptor;\n\t\tauto pushText = [&](\n\t\t\t\tTextWithEntities text,\n\t\t\t\tQMargins margins = {},\n\t\t\t\tconst base::flat_map<uint16, ClickHandlerPtr> &links = {},\n\t\t\t\tUi::Text::MarkedContext context = {}) {\n\t\t\tif (text.empty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpush(std::make_unique<MediaGenericTextPart>(\n\t\t\t\tstd::move(text),\n\t\t\t\tmargins,\n\t\t\t\tst::defaultTextStyle,\n\t\t\t\tlinks,\n\t\t\t\tstd::move(context)));\n\t\t};\n\n\t\tconst auto sticker = [=] {\n\t\t\tusing Tag = ChatHelpers::StickerLottieSize;\n\t\t\tconst auto session = &parent->history()->session();\n\t\t\tconst auto sticker = LookupGiftSticker(session, descriptor);\n\t\t\treturn StickerInBubblePart::Data{\n\t\t\t\t.sticker = sticker,\n\t\t\t\t.size = st::chatIntroStickerSize,\n\t\t\t\t.cacheTag = Tag::ChatIntroHelloSticker,\n\t\t\t\t.stopOnLastFrame = v::is<GiftTypePremium>(descriptor),\n\t\t\t};\n\t\t};\n\t\tpush(std::make_unique<StickerInBubblePart>(\n\t\t\tparent,\n\t\t\treplacing,\n\t\t\tsticker,\n\t\t\tst::giftBoxPreviewStickerPadding));\n\t\tauto title = v::match(descriptor, [&](GiftTypePremium gift) {\n\t\t\treturn tr::lng_action_gift_premium_months(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tgift.months,\n\t\t\t\ttr::bold);\n\t\t}, [&](const GiftTypeStars &gift) {\n\t\t\treturn recipient->isSelf()\n\t\t\t\t? ((gift.info.unique && gift.info.unique->crafted)\n\t\t\t\t\t? tr::lng_action_gift_crafted_subtitle(tr::now, tr::bold)\n\t\t\t\t\t: tr::lng_action_gift_self_subtitle(tr::now, tr::bold))\n\t\t\t\t: tr::lng_action_gift_got_subtitle(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tTextWithEntities()\n\t\t\t\t\t\t.append(Text::SingleCustomEmoji(\n\t\t\t\t\t\t\trecipient->owner().customEmojiManager(\n\t\t\t\t\t\t\t\t).peerUserpicEmojiData(\n\t\t\t\t\t\t\t\t\trecipient->session().user())))\n\t\t\t\t\t\t.append(' ')\n\t\t\t\t\t\t.append(recipient->session().user()->shortName()),\n\t\t\t\t\ttr::bold);\n\t\t});\n\t\tauto textFallback = v::match(descriptor, [&](GiftTypePremium gift) {\n\t\t\treturn tr::lng_action_gift_premium_about(\n\t\t\t\ttr::now,\n\t\t\t\ttr::rich);\n\t\t}, [&](const GiftTypeStars &gift) {\n\t\t\treturn data.upgraded\n\t\t\t\t? tr::lng_action_gift_got_upgradable_text(tr::now, tr::rich)\n\t\t\t\t: (recipient->isSelf() && gift.info.starsToUpgrade)\n\t\t\t\t? tr::lng_action_gift_self_about_unique(tr::now, tr::rich)\n\t\t\t\t: (recipient->isBroadcast() && gift.info.starsToUpgrade)\n\t\t\t\t? tr::lng_action_gift_channel_about_unique(tr::now, tr::rich)\n\t\t\t\t: gift.info.auction()\n\t\t\t\t? (recipient->isBroadcast()\n\t\t\t\t\t? tr::lng_action_gift_got_gift_channel(tr::now, tr::rich)\n\t\t\t\t\t: tr::lng_action_gift_got_gift_text(tr::now, tr::rich))\n\t\t\t\t: (recipient->isSelf()\n\t\t\t\t\t? tr::lng_action_gift_self_about\n\t\t\t\t\t: recipient->isBroadcast()\n\t\t\t\t\t? tr::lng_action_gift_channel_about\n\t\t\t\t\t: tr::lng_action_gift_got_stars_text)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tgift.info.starsConverted,\n\t\t\t\t\t\ttr::rich);\n\t\t});\n\t\tauto description = data.text.empty()\n\t\t\t? std::move(textFallback)\n\t\t\t: data.text;\n\t\tconst auto context = Core::TextContext({\n\t\t\t.session = &parent->history()->session(),\n\t\t\t.repaint = [parent] { parent->repaint(); },\n\t\t});\n\t\tpushText(\n\t\t\tstd::move(title),\n\t\t\tst::giftBoxPreviewTitlePadding,\n\t\t\t{},\n\t\t\tcontext);\n\n\t\tif (v::is<GiftTypeStars>(descriptor)) {\n\t\t\tconst auto &stars = v::get<GiftTypeStars>(descriptor);\n\t\t\tif (const auto by = stars.info.releasedBy) {\n\t\t\t\tpush(std::make_unique<TextBubblePart>(\n\t\t\t\t\ttr::lng_gift_released_by(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\ttr::link('@' + by->username()),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\tst::giftBoxReleasedByMargin,\n\t\t\t\t\tst::uniqueGiftReleasedBy.style));\n\t\t\t}\n\t\t}\n\n\t\tpushText(\n\t\t\tstd::move(description),\n\t\t\tst::giftBoxPreviewTextPadding,\n\t\t\t{},\n\t\t\tcontext);\n\n\t\tpush(HistoryView::MakeGenericButtonPart(\n\t\t\t(data.upgraded\n\t\t\t\t? tr::lng_gift_view_unpack(tr::now)\n\t\t\t\t: tr::lng_sticker_premium_view(tr::now)),\n\t\t\tst::giftBoxButtonMargin,\n\t\t\t[parent] { parent->repaint(); },\n\t\t\tnullptr));\n\t};\n}\n\nPreviewWrap::PreviewWrap(\n\tnot_null<QWidget*> parent,\n\tnot_null<PeerData*> recipient,\n\trpl::producer<GiftSendDetails> details)\n: RpWidget(parent)\n, _history(recipient->owner().history(recipient->session().userPeerId()))\n, _recipient(recipient)\n, _theme(Window::Theme::DefaultChatThemeOn(lifetime()))\n, _style(std::make_unique<ChatStyle>(\n\t_history->session().colorIndicesValue()))\n, _delegate(std::make_unique<PreviewDelegate>(\n\tparent,\n\t_style.get(),\n\t[=] { update(); }))\n, _position(0, st::msgMargin.bottom()) {\n\t_style->apply(_theme.get());\n\n\tusing namespace HistoryView;\n\t_history->owner().viewRepaintRequest(\n\t) | rpl::on_next([=](Data::RequestViewRepaint data) {\n\t\tif (data.view == _item.get()) {\n\t\t\tupdate();\n\t\t}\n\t}, lifetime());\n\n\t_history->session().downloaderTaskFinished() | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\tprepare(std::move(details));\n}\n\nvoid ShowSentToast(\n\t\tnot_null<Window::SessionController*> window,\n\t\tconst GiftDescriptor &descriptor,\n\t\tconst GiftSendDetails &details) {\n\tconst auto &st = st::historyPremiumToast;\n\tconst auto skip = st.padding.top();\n\tconst auto size = st.style.font->height * 2;\n\tconst auto document = LookupGiftSticker(&window->session(), descriptor);\n\tconst auto leftSkip = document\n\t\t? (skip + size + skip - st.padding.left())\n\t\t: 0;\n\tauto text = v::match(descriptor, [&](const GiftTypePremium &gift) {\n\t\treturn tr::lng_action_gift_premium_about(\n\t\t\ttr::now,\n\t\t\ttr::rich);\n\t}, [&](const GiftTypeStars &gift) {\n\t\tif (gift.info.perUserTotal && gift.info.perUserRemains < 2) {\n\t\t\treturn tr::lng_gift_sent_finished(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tgift.info.perUserTotal,\n\t\t\t\ttr::rich);\n\t\t} else if (gift.info.perUserTotal) {\n\t\t\treturn tr::lng_gift_sent_remains(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tgift.info.perUserRemains - 1,\n\t\t\t\ttr::rich);\n\t\t}\n\t\tconst auto amount = gift.info.stars\n\t\t\t+ (details.upgraded ? gift.info.starsToUpgrade : 0);\n\t\treturn tr::lng_gift_sent_about(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tamount,\n\t\t\ttr::rich);\n\t});\n\tconst auto strong = window->showToast({\n\t\t.title = tr::lng_gift_sent_title(tr::now),\n\t\t.text = std::move(text),\n\t\t.padding = rpl::single(QMargins(leftSkip, 0, 0, 0)),\n\t\t.st = &st,\n\t\t.attach = RectPart::Top,\n\t\t.duration = kSentToastDuration,\n\t}).get();\n\tif (!strong || !document) {\n\t\treturn;\n\t}\n\tconst auto widget = strong->widget();\n\tconst auto preview = CreateChild<RpWidget>(widget.get());\n\tpreview->moveToLeft(skip, skip);\n\tpreview->resize(size, size);\n\tpreview->show();\n\n\tconst auto bytes = document->createMediaView()->bytes();\n\tconst auto filepath = document->filepath();\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto player = preview->lifetime().make_state<Lottie::SinglePlayer>(\n\t\tLottie::ReadContent(bytes, filepath),\n\t\tLottie::FrameRequest{ QSize(size, size) * ratio },\n\t\tLottie::Quality::Default);\n\n\tpreview->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tif (!player->ready()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto image = player->frame();\n\t\tQPainter(preview).drawImage(\n\t\t\tQRect(QPoint(), image.size() / ratio),\n\t\t\timage);\n\t\tif (player->frameIndex() + 1 != player->framesCount()) {\n\t\t\tplayer->markFrameShown();\n\t\t}\n\t}, preview->lifetime());\n\n\tplayer->updates(\n\t) | rpl::on_next([=] {\n\t\tpreview->update();\n\t}, preview->lifetime());\n}\n\nPreviewWrap::~PreviewWrap() {\n\t_item = {};\n}\n\nvoid PreviewWrap::prepare(rpl::producer<GiftSendDetails> details) {\n\tstd::move(details) | rpl::on_next([=](GiftSendDetails details) {\n\t\tconst auto &descriptor = details.descriptor;\n\t\tconst auto cost = v::match(descriptor, [&](GiftTypePremium data) {\n\t\t\tconst auto stars = (details.byStars && data.stars)\n\t\t\t\t? data.stars\n\t\t\t\t: (data.currency == kCreditsCurrency)\n\t\t\t\t? data.cost\n\t\t\t\t: 0;\n\t\t\treturn stars\n\t\t\t\t? tr::lng_gift_stars_title(tr::now, lt_count, stars)\n\t\t\t\t: FillAmountAndCurrency(data.cost, data.currency, true);\n\t\t}, [&](GiftTypeStars data) {\n\t\t\tconst auto stars = data.info.stars\n\t\t\t\t+ (details.upgraded ? data.info.starsToUpgrade : 0);\n\t\t\treturn stars\n\t\t\t\t? tr::lng_gift_stars_title(tr::now, lt_count, stars)\n\t\t\t\t: QString();\n\t\t});\n\t\tconst auto name = _history->session().user()->shortName();\n\t\tconst auto text = cost.isEmpty()\n\t\t\t? tr::lng_action_gift_unique_received(tr::now, lt_user, name)\n\t\t\t: _recipient->isSelf()\n\t\t\t? tr::lng_action_gift_self_bought(tr::now, lt_cost, cost)\n\t\t\t: _recipient->isBroadcast()\n\t\t\t? tr::lng_action_gift_sent_channel(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tname,\n\t\t\t\tlt_name,\n\t\t\t\t_recipient->name(),\n\t\t\t\tlt_cost,\n\t\t\t\tcost)\n\t\t\t: tr::lng_action_gift_received(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tname,\n\t\t\t\tlt_cost,\n\t\t\t\tcost);\n\t\tconst auto item = _history->makeMessage({\n\t\t\t.id = _history->nextNonHistoryEntryId(),\n\t\t\t.flags = (MessageFlag::FakeAboutView\n\t\t\t\t| MessageFlag::FakeHistoryItem\n\t\t\t\t| MessageFlag::Local),\n\t\t\t.from = _history->peer->id,\n\t\t}, PreparedServiceText{ { text } });\n\n\t\tauto owned = AdminLog::OwnedItem(_delegate.get(), item);\n\t\towned->overrideMedia(std::make_unique<MediaGeneric>(\n\t\t\towned.get(),\n\t\t\tGenerateGiftMedia(owned.get(), _item.get(), _recipient, details),\n\t\t\tMediaGenericDescriptor{\n\t\t\t\t.maxWidth = st::chatGiftPreviewWidth,\n\t\t\t\t.service = true,\n\t\t\t}));\n\t\t_item = std::move(owned);\n\t\tif (width() >= st::msgMinWidth) {\n\t\t\tresizeTo(width());\n\t\t}\n\t\tupdate();\n\t}, lifetime());\n\n\twidthValue(\n\t) | rpl::filter([=](int width) {\n\t\treturn width >= st::msgMinWidth;\n\t}) | rpl::on_next([=](int width) {\n\t\tresizeTo(width);\n\t}, lifetime());\n\n\t_history->owner().itemResizeRequest(\n\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\tif (_item && item == _item->data() && width() >= st::msgMinWidth) {\n\t\t\tresizeTo(width());\n\t\t}\n\t}, lifetime());\n}\n\nvoid PreviewWrap::resizeTo(int width) {\n\tconst auto height = _position.y()\n\t\t+ _item->resizeGetHeight(width)\n\t\t+ _position.y()\n\t\t+ st::msgServiceMargin.top()\n\t\t+ st::msgServiceGiftBoxTopSkip\n\t\t- st::msgServiceMargin.bottom();\n\tresize(width, height);\n}\n\nvoid PreviewWrap::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\n\tconst auto clip = e->rect();\n\tif (!clip.isEmpty()) {\n\t\tp.setClipRect(clip);\n\t\tWindow::SectionWidget::PaintBackground(\n\t\t\tp,\n\t\t\t_theme.get(),\n\t\t\tQSize(width(), window()->height()),\n\t\t\tclip);\n\t}\n\n\tauto context = _theme->preparePaintContext(\n\t\t_style.get(),\n\t\trect(),\n\t\trect(),\n\t\te->rect(),\n\t\t!window()->isActiveWindow());\n\tp.translate(_position);\n\t_item->draw(p, context);\n}\n\n[[nodiscard]] rpl::producer<PremiumGiftsDescriptor> GiftsPremium(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<PeerData*> peer) {\n\tstruct Session {\n\t\tPremiumGiftsDescriptor last;\n\t};\n\tstatic auto Map = base::flat_map<not_null<Main::Session*>, Session>();\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tauto i = Map.find(session);\n\t\tif (i == end(Map)) {\n\t\t\ti = Map.emplace(session, Session()).first;\n\t\t\tsession->lifetime().add([=] { Map.remove(session); });\n\t\t}\n\t\tif (!i->second.last.list.empty()) {\n\t\t\tconsumer.put_next_copy(i->second.last);\n\t\t}\n\n\t\tusing namespace Api;\n\t\tconst auto api = std::make_shared<PremiumGiftCodeOptions>(peer);\n\t\tapi->request() | rpl::on_error_done([=](QString error) {\n\t\t\tconsumer.put_next({});\n\t\t}, [=] {\n\t\t\tconst auto &options = api->optionsForPeer();\n\t\t\tauto list = std::vector<GiftTypePremium>();\n\t\t\tlist.reserve(options.size());\n\t\t\tauto minMonthsGift = GiftTypePremium();\n\t\t\tfor (const auto &option : options) {\n\t\t\t\tif (option.currency != kCreditsCurrency) {\n\t\t\t\t\tlist.push_back({\n\t\t\t\t\t\t.cost = option.cost,\n\t\t\t\t\t\t.currency = option.currency,\n\t\t\t\t\t\t.months = option.months,\n\t\t\t\t\t});\n\t\t\t\t\tif (!minMonthsGift.months\n\t\t\t\t\t\t|| option.months < minMonthsGift.months) {\n\t\t\t\t\t\tminMonthsGift = list.back();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const auto &option : options) {\n\t\t\t\tif (option.currency == kCreditsCurrency) {\n\t\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\t\tlist,\n\t\t\t\t\t\toption.months,\n\t\t\t\t\t\t&GiftTypePremium::months);\n\t\t\t\t\tif (i != end(list)) {\n\t\t\t\t\t\ti->stars = option.cost;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (auto &gift : list) {\n\t\t\t\tif (gift.months > minMonthsGift.months\n\t\t\t\t\t&& gift.currency == minMonthsGift.currency) {\n\t\t\t\t\tconst auto costPerMonth = gift.cost / (1. * gift.months);\n\t\t\t\t\tconst auto maxCostPerMonth = minMonthsGift.cost\n\t\t\t\t\t\t/ (1. * minMonthsGift.months);\n\t\t\t\t\tconst auto costRatio = costPerMonth / maxCostPerMonth;\n\t\t\t\t\tconst auto discount = 1. - costRatio;\n\t\t\t\t\tconst auto discountPercent = 100 * discount;\n\t\t\t\t\tconst auto value = int(base::SafeRound(discountPercent));\n\t\t\t\t\tif (value > 0 && value < 100) {\n\t\t\t\t\t\tgift.discountPercent = value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tranges::sort(list, ranges::less(), &GiftTypePremium::months);\n\t\t\tauto &map = Map[session];\n\t\t\tif (map.last.list != list || list.empty()) {\n\t\t\t\tmap.last = PremiumGiftsDescriptor{\n\t\t\t\t\tstd::move(list),\n\t\t\t\t\tapi,\n\t\t\t\t};\n\t\t\t\tconsumer.put_next_copy(map.last);\n\t\t\t}\n\t\t}, lifetime);\n\n\t\treturn lifetime;\n\t};\n}\n\n[[nodiscard]] Text::String TabTextForPrice(int price) {\n\tconst auto simple = [](const QString &text) {\n\t\treturn Text::String(st::semiboldTextStyle, text);\n\t};\n\tif (price == kPriceTabAll) {\n\t\treturn simple(tr::lng_gift_stars_tabs_all(tr::now));\n\t} else if (price == kPriceTabMy) {\n\t\treturn simple(tr::lng_gift_stars_tabs_my(tr::now));\n\t} else if (price == kPriceTabCollectibles) {\n\t\treturn simple(tr::lng_gift_stars_tabs_collectibles(tr::now));\n\t}\n\treturn {};\n}\n\nstruct GiftPriceTabs {\n\trpl::producer<int> priceTab;\n\tobject_ptr<RpWidget> widget;\n};\n[[nodiscard]] GiftPriceTabs MakeGiftsPriceTabs(\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<std::vector<GiftTypeStars>> gifts,\n\t\tbool hasMyUnique) {\n\tauto widget = object_ptr<RpWidget>((QWidget*)nullptr);\n\tconst auto raw = widget.data();\n\n\tstruct Button {\n\t\tQRect geometry;\n\t\tText::String text;\n\t\tint price = 0;\n\t\tbool active = false;\n\t};\n\tstruct State {\n\t\trpl::variable<std::vector<int>> prices;\n\t\trpl::variable<int> priceTab = kPriceTabAll;\n\t\trpl::variable<int> fullWidth;\n\t\tstd::vector<Button> buttons;\n\t\tint dragx = 0;\n\t\tint pressx = 0;\n\t\tfloat64 dragscroll = 0.;\n\t\tfloat64 scroll = 0.;\n\t\tint scrollMax = 0;\n\t\tint tabsShift = 0;\n\t\tint selected = -1;\n\t\tint pressed = -1;\n\t\tint active = -1;\n\t};\n\tconst auto user = peer->asUser();\n\tconst auto disallowed = user\n\t\t? user->disallowedGiftTypes()\n\t\t: Api::DisallowedGiftType();\n\tif (disallowed & Api::DisallowedGiftType::Unique) {\n\t\thasMyUnique = false;\n\t}\n\tconst auto state = raw->lifetime().make_state<State>();\n\tconst auto scroll = [=] {\n\t\treturn QPoint(int(base::SafeRound(state->scroll)), 0)\n\t\t\t- QPoint(state->tabsShift, 0);\n\t};\n\n\tstate->prices = std::move(\n\t\tgifts\n\t) | rpl::map([=](const std::vector<GiftTypeStars> &gifts) {\n\t\tauto result = std::vector<int>();\n\t\tresult.push_back(kPriceTabAll);\n\t\tauto hasCollectibles = false;\n\t\tif (!(disallowed & Api::DisallowedGiftType::Unique)) {\n\t\t\tfor (const auto &gift : gifts) {\n\t\t\t\tif (gift.resale\n\t\t\t\t\t|| (gift.info.limitedCount && gift.info.upgradable)) {\n\t\t\t\t\thasCollectibles = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (hasMyUnique && !gifts.empty()) {\n\t\t\tresult.push_back(kPriceTabMy);\n\t\t}\n\t\tif (hasCollectibles) {\n\t\t\tresult.push_back(kPriceTabCollectibles);\n\t\t}\n\t\treturn result;\n\t});\n\n\tconst auto setSelected = [=](int index) {\n\t\tconst auto was = (state->selected >= 0);\n\t\tconst auto now = (index >= 0);\n\t\tstate->selected = index;\n\t\tif (was != now) {\n\t\t\traw->setCursor(now ? style::cur_pointer : style::cur_default);\n\t\t}\n\t};\n\tconst auto setActive = [=](int index) {\n\t\tconst auto was = state->active;\n\t\tif (was == index) {\n\t\t\treturn;\n\t\t}\n\t\tif (was >= 0 && was < state->buttons.size()) {\n\t\t\tstate->buttons[was].active = false;\n\t\t}\n\t\tstate->active = index;\n\t\tstate->buttons[index].active = true;\n\t\traw->update();\n\n\t\tstate->priceTab = state->buttons[index].price;\n\t};\n\n\tstate->prices.value(\n\t) | rpl::on_next([=](const std::vector<int> &prices) {\n\t\tauto x = st::giftBoxTabsMargin.left();\n\t\tauto y = st::giftBoxTabsMargin.top();\n\n\t\tsetSelected(-1);\n\t\tstate->buttons.resize(prices.size());\n\t\tconst auto padding = st::giftBoxTabPadding;\n\t\tauto currentPrice = state->priceTab.current();\n\t\tif (!ranges::contains(prices, currentPrice)) {\n\t\t\tcurrentPrice = kPriceTabAll;\n\t\t}\n\t\tstate->active = -1;\n\t\tfor (auto i = 0, count = int(prices.size()); i != count; ++i) {\n\t\t\tconst auto price = prices[i];\n\t\t\tauto &button = state->buttons[i];\n\t\t\tif (button.text.isEmpty() || button.price != price) {\n\t\t\t\tbutton.price = price;\n\t\t\t\tbutton.text = TabTextForPrice(price);\n\t\t\t}\n\t\t\tbutton.active = (price == currentPrice);\n\t\t\tif (button.active) {\n\t\t\t\tstate->active = i;\n\t\t\t}\n\t\t\tconst auto width = button.text.maxWidth();\n\t\t\tconst auto height = st::giftBoxTabStyle.font->height;\n\t\t\tconst auto r = QRect(0, 0, width, height).marginsAdded(padding);\n\t\t\tbutton.geometry = QRect(QPoint(x, y), r.size());\n\t\t\tx += r.width() + st::giftBoxTabSkip;\n\t\t}\n\t\tstate->fullWidth = x\n\t\t\t- st::giftBoxTabSkip\n\t\t\t+ st::giftBoxTabsMargin.right();\n\t\tconst auto height = state->buttons.empty()\n\t\t\t? 0\n\t\t\t: (y\n\t\t\t\t+ state->buttons.back().geometry.height()\n\t\t\t\t+ st::giftBoxTabsMargin.bottom());\n\t\traw->resize(raw->width(), height);\n\t\traw->update();\n\t}, raw->lifetime());\n\n\trpl::combine(\n\t\traw->widthValue(),\n\t\tstate->fullWidth.value()\n\t) | rpl::on_next([=](int outer, int inner) {\n\t\tstate->scrollMax = std::max(0, inner - outer);\n\t\tstate->tabsShift = (outer - inner) / 2;\n\t}, raw->lifetime());\n\n\traw->setMouseTracking(true);\n\traw->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tswitch (type) {\n\t\tcase QEvent::Leave: setSelected(-1); break;\n\t\tcase QEvent::MouseMove: {\n\t\t\tconst auto me = static_cast<QMouseEvent*>(e.get());\n\t\t\tconst auto mousex = me->pos().x() - state->tabsShift;\n\t\t\tconst auto drag = QApplication::startDragDistance();\n\t\t\tif (state->dragx > 0) {\n\t\t\t\tstate->scroll = std::clamp(\n\t\t\t\t\tstate->dragscroll + state->dragx - mousex,\n\t\t\t\t\t0.,\n\t\t\t\t\tstate->scrollMax * 1.);\n\t\t\t\traw->update();\n\t\t\t\tbreak;\n\t\t\t} else if (state->pressx > 0\n\t\t\t\t&& std::abs(state->pressx - mousex) > drag) {\n\t\t\t\tstate->dragx = state->pressx;\n\t\t\t\tstate->dragscroll = state->scroll;\n\t\t\t}\n\t\t\tconst auto position = me->pos() + scroll();\n\t\t\tfor (auto i = 0, c = int(state->buttons.size()); i != c; ++i) {\n\t\t\t\tif (state->buttons[i].geometry.contains(position)) {\n\t\t\t\t\tsetSelected(i);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} break;\n\t\tcase QEvent::Wheel: {\n\t\t\tconst auto me = static_cast<QWheelEvent*>(e.get());\n\t\t\tstate->scroll = std::clamp(\n\t\t\t\tstate->scroll - ScrollDeltaF(me).x(),\n\t\t\t\t0.,\n\t\t\t\tstate->scrollMax * 1.);\n\t\t\traw->update();\n\t\t} break;\n\t\tcase QEvent::MouseButtonPress: {\n\t\t\tconst auto me = static_cast<QMouseEvent*>(e.get());\n\t\t\tif (me->button() != Qt::LeftButton) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tstate->pressed = state->selected;\n\t\t\tstate->pressx = me->pos().x() - state->tabsShift;\n\t\t} break;\n\t\tcase QEvent::MouseButtonRelease: {\n\t\t\tconst auto me = static_cast<QMouseEvent*>(e.get());\n\t\t\tif (me->button() != Qt::LeftButton) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst auto dragx = std::exchange(state->dragx, 0);\n\t\t\tconst auto pressed = std::exchange(state->pressed, -1);\n\t\t\tstate->pressx = 0;\n\t\t\tif (!dragx && pressed >= 0 && state->selected == pressed) {\n\t\t\t\tsetActive(pressed);\n\t\t\t}\n\t\t} break;\n\t\t}\n\t}, raw->lifetime());\n\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto padding = st::giftBoxTabPadding;\n\t\tconst auto shift = -scroll();\n\t\tfor (const auto &button : state->buttons) {\n\t\t\tconst auto geometry = button.geometry.translated(shift);\n\t\t\tif (button.active) {\n\t\t\t\tp.setBrush(st::giftBoxTabBgActive);\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tconst auto radius = geometry.height() / 2.;\n\t\t\t\tp.drawRoundedRect(geometry, radius, radius);\n\t\t\t\tp.setPen(st::giftBoxTabFgActive);\n\t\t\t} else {\n\t\t\t\tp.setPen(st::giftBoxTabFg);\n\t\t\t}\n\t\t\tbutton.text.draw(p, {\n\t\t\t\t.position = geometry.marginsRemoved(padding).topLeft(),\n\t\t\t\t.availableWidth = button.text.maxWidth(),\n\t\t\t});\n\t\t}\n\t\t{\n\t\t\tconst auto &icon = st::defaultEmojiSuggestions;\n\t\t\tconst auto w = icon.fadeRight.width();\n\t\t\tconst auto &c = st::boxDividerBg->c;\n\t\t\tconst auto r = QRect(0, 0, w, raw->height());\n\t\t\tconst auto s = std::abs(float64(shift.x()));\n\t\t\tconstexpr auto kF = 0.5;\n\t\t\tconst auto opacityRight = (state->scrollMax - s)\n\t\t\t\t/ (icon.fadeRight.width() * kF);\n\t\t\tp.setOpacity(std::clamp(std::abs(opacityRight), 0., 1.));\n\t\t\ticon.fadeRight.fill(p, r.translated(raw->width() -  w, 0), c);\n\n\t\t\tconst auto opacityLeft = s / (icon.fadeLeft.width() * kF);\n\t\t\tp.setOpacity(std::clamp(std::abs(opacityLeft), 0., 1.));\n\t\t\ticon.fadeLeft.fill(p, r, c);\n\t\t}\n\t}, raw->lifetime());\n\n\treturn {\n\t\t.priceTab = state->priceTab.value(),\n\t\t.widget = std::move(widget),\n\t};\n}\n\n[[nodiscard]] int StarGiftMessageLimit(not_null<Main::Session*> session) {\n\treturn session->appConfig().get<int>(\n\t\tu\"stargifts_message_length_max\"_q,\n\t\t255);\n}\n\n[[nodiscard]] not_null<InputField*> AddPartInput(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<VerticalLayout*> container,\n\t\tnot_null<QWidget*> outer,\n\t\trpl::producer<QString> placeholder,\n\t\tQString current,\n\t\tint limit) {\n\tconst auto field = container->add(\n\t\tobject_ptr<InputField>(\n\t\t\tcontainer,\n\t\t\tst::giftBoxTextField,\n\t\t\tInputField::Mode::NoNewlines,\n\t\t\tstd::move(placeholder),\n\t\t\tcurrent),\n\t\tst::giftBoxTextPadding);\n\tfield->setMaxLength(limit);\n\tAddLengthLimitLabel(field, limit, {\n\t\t.limitLabelTop = st::giftBoxLimitTop,\n\t});\n\n\tconst auto toggle = CreateChild<EmojiButton>(\n\t\tcontainer,\n\t\tst::defaultComposeFiles.emoji);\n\ttoggle->show();\n\tfield->geometryValue() | rpl::on_next([=](QRect r) {\n\t\ttoggle->move(\n\t\t\tr.x() + r.width() - toggle->width(),\n\t\t\tr.y() - st::giftBoxEmojiToggleTop);\n\t}, toggle->lifetime());\n\n\tusing namespace ChatHelpers;\n\tconst auto panel = field->lifetime().make_state<TabbedPanel>(\n\t\touter,\n\t\tcontroller,\n\t\tobject_ptr<TabbedSelector>(\n\t\t\tnullptr,\n\t\t\tcontroller->uiShow(),\n\t\t\tWindow::GifPauseReason::Layer,\n\t\t\tTabbedSelector::Mode::EmojiOnly));\n\tpanel->setDesiredHeightValues(\n\t\t1.,\n\t\tst::emojiPanMinHeight / 2,\n\t\tst::emojiPanMinHeight);\n\tpanel->hide();\n\tpanel->selector()->setAllowEmojiWithoutPremium(true);\n\tpanel->selector()->emojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\tInsertEmojiAtCursor(field->textCursor(), data.emoji);\n\t}, field->lifetime());\n\tpanel->selector()->customEmojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\tData::InsertCustomEmoji(field, data.document);\n\t}, field->lifetime());\n\n\tconst auto updateEmojiPanelGeometry = [=] {\n\t\tconst auto parent = panel->parentWidget();\n\t\tconst auto global = toggle->mapToGlobal({ 0, 0 });\n\t\tconst auto local = parent->mapFromGlobal(global);\n\t\tpanel->moveBottomRight(\n\t\t\tlocal.y(),\n\t\t\tlocal.x() + toggle->width() * 3);\n\t};\n\n\tconst auto filterCallback = [=](not_null<QEvent*> event) {\n\t\tconst auto type = event->type();\n\t\tif (type == QEvent::Move || type == QEvent::Resize) {\n\t\t\t// updateEmojiPanelGeometry uses not only container geometry, but\n\t\t\t// also container children geometries that will be updated later.\n\t\t\tcrl::on_main(field, updateEmojiPanelGeometry);\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t};\n\tfor (auto widget = (QWidget*)field, end = (QWidget*)outer->parentWidget()\n\t\t; widget && widget != end\n\t\t; widget = widget->parentWidget()) {\n\t\tbase::install_event_filter(field, widget, filterCallback);\n\t}\n\n\ttoggle->installEventFilter(panel);\n\ttoggle->addClickHandler([=] {\n\t\tpanel->toggleAnimated();\n\t});\n\n\treturn field;\n}\n\nvoid SendGift(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<Api::PremiumGiftCodeOptions> api,\n\t\tconst GiftSendDetails &details,\n\t\tFn<void(Payments::CheckoutResult)> done) {\n\tconst auto processNonPanelPaymentFormFactory\n\t\t= Payments::ProcessNonPanelPaymentFormFactory(window, done);\n\tv::match(details.descriptor, [&](const GiftTypePremium &gift) {\n\t\tif (details.byStars && gift.stars) {\n\t\t\tauto invoice = Payments::InvoicePremiumGiftCode{\n\t\t\t\t.purpose = Payments::InvoicePremiumGiftCodeUsers{\n\t\t\t\t\t.users = { peer->asUser() },\n\t\t\t\t\t.message = details.text,\n\t\t\t\t},\n\t\t\t\t.currency = Ui::kCreditsCurrency,\n\t\t\t\t.randomId = details.randomId,\n\t\t\t\t.amount = uint64(gift.stars),\n\t\t\t\t.storeQuantity = 1,\n\t\t\t\t.users = 1,\n\t\t\t\t.months = gift.months,\n\t\t\t};\n\t\t\tPayments::CheckoutProcess::Start(\n\t\t\t\tstd::move(invoice),\n\t\t\t\tdone,\n\t\t\t\tprocessNonPanelPaymentFormFactory);\n\t\t} else {\n\t\t\tauto invoice = api->invoice(1, gift.months);\n\t\t\tinvoice.purpose = Payments::InvoicePremiumGiftCodeUsers{\n\t\t\t\t.users = { peer->asUser() },\n\t\t\t\t.message = details.text,\n\t\t\t};\n\t\t\tPayments::CheckoutProcess::Start(std::move(invoice), done);\n\t\t}\n\t}, [&](const GiftTypeStars &gift) {\n\t\tPayments::CheckoutProcess::Start(Payments::InvoiceStarGift{\n\t\t\t.giftId = gift.info.id,\n\t\t\t.randomId = details.randomId,\n\t\t\t.message = details.text,\n\t\t\t.recipient = peer,\n\t\t\t.limitedCount = gift.info.limitedCount,\n\t\t\t.perUserLimit = gift.info.perUserTotal,\n\t\t\t.anonymous = details.anonymous,\n\t\t\t.upgraded = details.upgraded,\n\t\t}, done, processNonPanelPaymentFormFactory);\n\t});\n}\n\nvoid ShowGiftUpgradedToast(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<Main::Session*> session,\n\t\tstd::shared_ptr<Data::UniqueGift> gift) {\n\tExpects(gift != nullptr);\n\n\twindow->showToast({\n\t\t.title = tr::lng_gift_upgraded_title(tr::now),\n\t\t.text = tr::lng_gift_upgraded_about(\n\t\t\ttr::now,\n\t\t\tlt_name,\n\t\t\ttr::bold(Data::UniqueGiftName(*gift)),\n\t\t\ttr::marked),\n\t\t.duration = kUpgradeDoneToastDuration,\n\t});\n}\n\nvoid ShowUpgradeGiftedToast(\n\t\tbase::weak_ptr<Window::SessionController> weak,\n\t\tnot_null<PeerData*> peer) {\n\tif (const auto strong = weak.get()) {\n\t\tstrong->showToast({\n\t\t\t.title = tr::lng_gift_upgrade_gifted_title(tr::now),\n\t\t\t.text = { (peer->isBroadcast()\n\t\t\t\t? tr::lng_gift_upgrade_gifted_about_channel\n\t\t\t\t: tr::lng_gift_upgrade_gifted_about)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_name,\n\t\t\t\t\tpeer->shortName()) },\n\t\t\t.duration = kUpgradeDoneToastDuration,\n\t\t});\n\t}\n}\n\nvoid SendStarsFormRequest(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tSettings::SmallBalanceResult result,\n\t\tuint64 formId,\n\t\tMTPInputInvoice invoice,\n\t\tFn<void(Payments::CheckoutResult, const MTPUpdates *)> done) {\n\tusing BalanceResult = Settings::SmallBalanceResult;\n\tconst auto session = &show->session();\n\tif (result == BalanceResult::Success\n\t\t|| result == BalanceResult::Already) {\n\t\tsession->api().request(MTPpayments_SendStarsForm(\n\t\t\tMTP_long(formId),\n\t\t\tinvoice\n\t\t)).done([=](const MTPpayments_PaymentResult &result) {\n\t\t\tresult.match([&](const MTPDpayments_paymentResult &data) {\n\t\t\t\tsession->api().applyUpdates(data.vupdates());\n\t\t\t\tsession->credits().tonLoad(true);\n\t\t\t\tsession->credits().load(true);\n\t\t\t\tdone(Payments::CheckoutResult::Paid, &data.vupdates());\n\t\t\t}, [&](const MTPDpayments_paymentVerificationNeeded &data) {\n\t\t\t\tdone(Payments::CheckoutResult::Failed, nullptr);\n\t\t\t});\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tif (!ShowGiftErrorToast(show, error)) {\n\t\t\t\tshow->showToast(error.type());\n\t\t\t}\n\t\t\tdone(Payments::CheckoutResult::Failed, nullptr);\n\t\t}).send();\n\t} else if (result == BalanceResult::Cancelled) {\n\t\tdone(Payments::CheckoutResult::Cancelled, nullptr);\n\t} else {\n\t\tdone(Payments::CheckoutResult::Failed, nullptr);\n\t}\n}\n\nvoid UpgradeGift(\n\t\tnot_null<Window::SessionController*> window,\n\t\tData::SavedStarGiftId savedId,\n\t\tbool keepDetails,\n\t\tint stars,\n\t\tFn<void(bool, std::shared_ptr<Data::GiftUpgradeResult>)> done) {\n\t//AssertIsDebug(); return;\n\tconst auto session = &window->session();\n\tconst auto weak = base::make_weak(window);\n\tauto formDone = [=](\n\t\t\tPayments::CheckoutResult result,\n\t\t\tconst MTPUpdates *updates) {\n\t\tif (result != Payments::CheckoutResult::Paid) {\n\t\t\tdone(false, nullptr);\n\t\t\treturn;\n\t\t}\n\t\tconst auto owner = savedId.isUser()\n\t\t\t? session->user().get()\n\t\t\t: savedId.chat();\n\t\tif (owner) {\n\t\t\towner->owner().nextForUpgradeGiftInvalidate(owner);\n\t\t}\n\t\tconst auto gift = updates\n\t\t\t? FindUniqueGift(session, *updates)\n\t\t\t: nullptr;\n\t\tdone(true, gift);\n\t};\n\tif (stars <= 0) {\n\t\tusing Flag = MTPpayments_UpgradeStarGift::Flag;\n\t\tsession->api().request(MTPpayments_UpgradeStarGift(\n\t\t\tMTP_flags(keepDetails ? Flag::f_keep_original_details : Flag()),\n\t\t\tApi::InputSavedStarGiftId(savedId)\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\tsession->api().applyUpdates(result);\n\t\t\tformDone(Payments::CheckoutResult::Paid, &result);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tif (!ShowGiftErrorToast(strong->uiShow(), error)) {\n\t\t\t\t\tstrong->showToast(error.type());\n\t\t\t\t}\n\t\t\t}\n\t\t\tformDone(Payments::CheckoutResult::Failed, nullptr);\n\t\t}).send();\n\t\treturn;\n\t}\n\tusing Flag = MTPDinputInvoiceStarGiftUpgrade::Flag;\n\tRequestStarsFormAndSubmit(\n\t\twindow->uiShow(),\n\t\tMTP_inputInvoiceStarGiftUpgrade(\n\t\t\tMTP_flags(keepDetails ? Flag::f_keep_original_details : Flag()),\n\t\t\tApi::InputSavedStarGiftId(savedId)),\n\t\tstd::move(formDone));\n}\n\nvoid GiftUpgrade(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerData*> peer,\n\t\tQString giftPrepayUpgradeHash,\n\t\tint stars,\n\t\tFn<void(bool)> done) {\n\tconst auto weak = base::make_weak(window);\n\tauto formDone = [=](\n\t\t\tPayments::CheckoutResult result,\n\t\t\tconst MTPUpdates *updates) {\n\t\tconst auto success = (result == Payments::CheckoutResult::Paid);\n\t\tif (success) {\n\t\t\tShowUpgradeGiftedToast(weak, peer);\n\t\t}\n\t\tdone(success);\n\t};\n\tRequestStarsFormAndSubmit(\n\t\twindow->uiShow(),\n\t\tMTP_inputInvoiceStarGiftPrepaidUpgrade(\n\t\t\tpeer->input(),\n\t\t\tMTP_string(giftPrepayUpgradeHash)),\n\t\tstd::move(formDone));\n}\n\nvoid SoldOutBox(\n\t\tnot_null<GenericBox*> box,\n\t\tnot_null<Window::SessionController*> window,\n\t\tconst GiftTypeStars &gift) {\n\tSettings::ReceiptCreditsBox(\n\t\tbox,\n\t\twindow,\n\t\tData::CreditsHistoryEntry{\n\t\t\t.firstSaleDate = base::unixtime::parse(gift.info.firstSaleDate),\n\t\t\t.lastSaleDate = base::unixtime::parse(gift.info.lastSaleDate),\n\t\t\t.credits = CreditsAmount(gift.info.stars),\n\t\t\t.bareGiftStickerId = gift.info.document->id,\n\t\t\t.peerType = Data::CreditsHistoryEntry::PeerType::Peer,\n\t\t\t.limitedCount = gift.info.limitedCount,\n\t\t\t.limitedLeft = gift.info.limitedLeft,\n\t\t\t.soldOutInfo = true,\n\t\t\t.gift = true,\n\t\t},\n\t\tData::SubscriptionEntry());\n}\n\nvoid AddUpgradeButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tint cost,\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void(bool)> toggled,\n\t\tFn<void()> preview) {\n\tconst auto button = container->add(\n\t\tobject_ptr<SettingsButton>(\n\t\t\tcontainer,\n\t\t\trpl::single(QString()),\n\t\t\tst::settingsButtonNoIcon));\n\tbutton->toggleOn(rpl::single(false))->toggledValue(\n\t) | rpl::on_next(toggled, button->lifetime());\n\n\tauto helper = Ui::Text::CustomEmojiHelper();\n\tauto star = helper.paletteDependent(Ui::Earn::IconCreditsEmoji());\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton,\n\t\ttr::lng_gift_send_unique(\n\t\t\tlt_price,\n\t\t\trpl::single(star.append(' '\n\t\t\t\t+ Lang::FormatCreditsAmountDecimal(\n\t\t\t\t\tCreditsAmount{ cost }))),\n\t\t\ttr::marked),\n\t\tst::boxLabel,\n\t\tst::defaultPopupMenu,\n\t\thelper.context());\n\tlabel->show();\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tbutton->widthValue() | rpl::on_next([=](int outer) {\n\t\tconst auto padding = st::settingsButtonNoIcon.padding;\n\t\tconst auto inner = outer\n\t\t\t- padding.left()\n\t\t\t- padding.right()\n\t\t\t- st::settingsButtonNoIcon.toggleSkip\n\t\t\t- 2 * st::settingsButtonNoIcon.toggle.border\n\t\t\t- 2 * st::settingsButtonNoIcon.toggle.diameter\n\t\t\t- 2 * st::settingsButtonNoIcon.toggle.width;\n\t\tlabel->resizeToWidth(inner);\n\t\tlabel->moveToLeft(padding.left(), padding.top(), outer);\n\t}, label->lifetime());\n\n\tAddSkip(container);\n\tconst auto about = AddDividerText(\n\t\tcontainer,\n\t\t(peer->isBroadcast()\n\t\t\t? tr::lng_gift_send_unique_about_channel(\n\t\t\t\tlt_name,\n\t\t\t\trpl::single(TextWithEntities{ peer->name() }),\n\t\t\t\tlt_link,\n\t\t\t\ttr::lng_gift_send_unique_link(tr::link),\n\t\t\t\ttr::marked)\n\t\t\t: tr::lng_gift_send_unique_about(\n\t\t\t\tlt_user,\n\t\t\t\trpl::single(TextWithEntities{ peer->shortName() }),\n\t\t\t\tlt_link,\n\t\t\t\ttr::lng_gift_send_unique_link(tr::link),\n\t\t\t\ttr::marked)));\n\tabout->setClickHandlerFilter([=](const auto &...) {\n\t\tpreview();\n\t\treturn false;\n\t});\n}\n\nvoid AddSoldLeftSlider(\n\t\tnot_null<RpWidget*> above,\n\t\tconst GiftTypeStars &gift,\n\t\tQMargins added = {}) {\n\tconst auto still = gift.info.limitedLeft;\n\tconst auto total = gift.info.limitedCount;\n\tconst auto slider = CreateChild<RpWidget>(above->parentWidget());\n\tstruct State {\n\t\tText::String still;\n\t\tText::String sold;\n\t\tint height = 0;\n\t};\n\tconst auto state = slider->lifetime().make_state<State>();\n\tconst auto sold = total - still;\n\tstate->still.setText(\n\t\tst::semiboldTextStyle,\n\t\ttr::lng_gift_send_limited_left(tr::now, lt_count_decimal, still));\n\tstate->sold.setText(\n\t\tst::semiboldTextStyle,\n\t\ttr::lng_gift_send_limited_sold(tr::now, lt_count_decimal, sold));\n\tstate->height = st::giftLimitedPadding.top()\n\t\t+ st::semiboldFont->height\n\t\t+ st::giftLimitedPadding.bottom();\n\tabove->geometryValue() | rpl::on_next([=](QRect geometry) {\n\t\tconst auto space = st::giftLimitedBox.buttonPadding.top();\n\t\tconst auto skip = (space - state->height) / 2;\n\t\tslider->setGeometry(\n\t\t\tgeometry.x() + added.left(),\n\t\t\tgeometry.y() - skip - state->height,\n\t\t\tgeometry.width() - added.left() - added.right(),\n\t\t\tstate->height);\n\t}, slider->lifetime());\n\tslider->paintRequest() | rpl::on_next([=] {\n\t\tconst auto &padding = st::giftLimitedPadding;\n\t\tconst auto left = (padding.left() * 2) + state->still.maxWidth();\n\t\tconst auto right = (padding.right() * 2) + state->sold.maxWidth();\n\t\tconst auto space = slider->width() - left - right;\n\t\tif (space <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto edge = left + ((space * still) / total);\n\n\t\tconst auto radius = st::buttonRadius;\n\t\tauto p = QPainter(slider);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::windowBgOver);\n\t\tp.drawRoundedRect(\n\t\t\tedge - (radius * 3),\n\t\t\t0,\n\t\t\tslider->width() - (edge - (radius * 3)),\n\t\t\tstate->height,\n\t\t\tradius,\n\t\t\tradius);\n\t\tp.setBrush(st::windowBgActive);\n\t\tp.drawRoundedRect(0, 0, edge, state->height, radius, radius);\n\n\t\tp.setPen(st::windowFgActive);\n\t\tstate->still.draw(p, {\n\t\t\t.position = { padding.left(), padding.top() },\n\t\t\t.availableWidth = left,\n\t\t});\n\t\tp.setPen(st::windowSubTextFg);\n\t\tstate->sold.draw(p, {\n\t\t\t.position = { left + space + padding.right(), padding.top() },\n\t\t\t.availableWidth = right,\n\t\t});\n\t}, slider->lifetime());\n}\n\nvoid CheckMaybeGiftLocked(\n\t\tnot_null<Window::SessionController*> window,\n\t\tuint64 giftId,\n\t\tFn<void()> send) {\n\tconst auto session = &window->session();\n\tsession->api().request(MTPpayments_CheckCanSendGift(\n\t\tMTP_long(giftId)\n\t)).done(crl::guard(window, [=](\n\t\t\tconst MTPpayments_CheckCanSendGiftResult &result) {\n\t\tresult.match([&](const MTPDpayments_checkCanSendGiftResultOk &) {\n\t\t\tsend();\n\t\t}, [&](const MTPDpayments_checkCanSendGiftResultFail &data) {\n\t\t\twindow->show(Ui::MakeInformBox({\n\t\t\t\t.text = Api::ParseTextWithEntities(session, data.vreason()),\n\t\t\t\t.title = tr::lng_gift_locked_title(),\n\t\t\t}));\n\t\t});\n\t})).fail(crl::guard(window, [=] {\n\t})).send();\n}\n\nvoid FillBg(not_null<RpWidget*> box) {\n\tbox->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(box);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tconst auto radius = st::boxRadius;\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::boxDividerBg);\n\t\tp.drawRoundedRect(\n\t\t\tbox->rect().marginsAdded({ 0, 0, 0, 2 * radius }),\n\t\t\tradius,\n\t\t\tradius);\n\t}, box->lifetime());\n}\n\nstruct AddBlockArgs {\n\trpl::producer<QString> subtitle;\n\trpl::producer<TextWithEntities> about;\n\tFn<bool(const ClickHandlerPtr&, Qt::MouseButton)> aboutFilter;\n\tobject_ptr<RpWidget> content;\n};\n\nvoid AddBlock(\n\t\tnot_null<VerticalLayout*> content,\n\t\tnot_null<Window::SessionController*> window,\n\t\tAddBlockArgs &&args) {\n\tcontent->add(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tcontent,\n\t\t\tstd::move(args.subtitle),\n\t\t\tst::giftBoxSubtitle),\n\t\tst::giftBoxSubtitleMargin,\n\t\tstyle::al_top);\n\tconst auto about = content->add(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tcontent,\n\t\t\tstd::move(args.about),\n\t\t\tst::giftBoxAbout),\n\t\tst::giftBoxAboutMargin,\n\t\tstyle::al_top);\n\tabout->setClickHandlerFilter(std::move(args.aboutFilter));\n\tcontent->add(std::move(args.content));\n}\n\n[[nodiscard]] object_ptr<RpWidget> MakePremiumGifts(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerData*> peer) {\n\tstruct State {\n\t\trpl::variable<PremiumGiftsDescriptor> gifts;\n\t};\n\tauto state = std::make_unique<State>();\n\n\tstate->gifts = GiftsPremium(&window->session(), peer);\n\n\tauto gifts = state->gifts.value(\n\t) | rpl::map([=](const PremiumGiftsDescriptor &gifts) {\n\t\treturn GiftsDescriptor{\n\t\t\tgifts.list | ranges::to<std::vector<GiftDescriptor>>,\n\t\t\tgifts.api,\n\t\t};\n\t});\n\tauto result = MakeGiftsList({\n\t\t.window = window,\n\t\t.peer = peer,\n\t\t.gifts = std::move(gifts),\n\t});\n\tresult->lifetime().add([state = std::move(state)] {});\n\treturn result;\n}\n\n[[nodiscard]] object_ptr<RpWidget> MakeStarsGifts(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerData*> peer,\n\t\tMyGiftsDescriptor my,\n\t\tFn<void(int)> tabSelected) {\n\tauto result = object_ptr<VerticalLayout>((QWidget*)nullptr);\n\n\tstruct State {\n\t\trpl::variable<std::vector<GiftTypeStars>> gifts;\n\t\trpl::variable<int> priceTab = kPriceTabAll;\n\t\trpl::event_stream<> myUpdated;\n\t\tMyGiftsDescriptor my;\n\t\trpl::lifetime myLoading;\n\t};\n\tconst auto state = result->lifetime().make_state<State>();\n\tstate->my = std::move(my);\n\n\tstate->gifts = GiftsStars(&window->session(), peer);\n\n\tauto tabs = MakeGiftsPriceTabs(\n\t\tpeer,\n\t\tstate->gifts.value(),\n\t\t!state->my.list.empty() && !peer->isSelf());\n\tstate->priceTab = std::move(tabs.priceTab);\n\tstate->priceTab.changes() | rpl::on_next([=](int tab) {\n\t\ttabSelected(tab);\n\t}, tabs.widget->lifetime());\n\tresult->add(std::move(tabs.widget));\n\n\tauto gifts = rpl::combine(\n\t\tstate->gifts.value(),\n\t\tstate->priceTab.value(),\n\t\trpl::single(rpl::empty) | rpl::then(state->myUpdated.events())\n\t) | rpl::map([=](std::vector<GiftTypeStars> &&gifts, int price, auto) {\n\t\tif (price == kPriceTabMy) {\n\t\t\tgifts.clear();\n\t\t\tfor (const auto &gift : state->my.list) {\n\t\t\t\tgifts.push_back({\n\t\t\t\t\t.transferId = gift.manageId,\n\t\t\t\t\t.info = gift.info,\n\t\t\t\t\t.mine = true,\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\t// First, gather information about which gifts are available on resale\n\t\t\tbase::flat_set<uint64> resaleGiftIds;\n\t\t\tif (price != kPriceTabCollectibles) {\n\t\t\t\t// Only need this info when not viewing the resale tab\n\t\t\t\tfor (const auto &gift : gifts) {\n\t\t\t\t\tif (gift.resale) {\n\t\t\t\t\t\tresaleGiftIds.insert(gift.info.id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst auto pred = [&](const GiftTypeStars &gift) {\n\t\t\t\t// Skip sold out gifts if they're available on resale\n\t\t\t\t// (unless we're specifically viewing resale gifts)\n\t\t\t\tif (price != kPriceTabCollectibles\n\t\t\t\t\t&& IsSoldOut(gift.info)\n\t\t\t\t\t&& !gift.resale\n\t\t\t\t\t&& resaleGiftIds.contains(gift.info.id)) {\n\t\t\t\t\treturn true; // Remove this gift\n\t\t\t\t} else if ((price != kPriceTabCollectibles) || gift.resale) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\treturn !gift.info.limitedCount\n\t\t\t\t\t|| !gift.info.upgradable\n\t\t\t\t\t|| IsSoldOut(gift.info);\n\t\t\t};\n\t\t\tgifts.erase(ranges::remove_if(gifts, pred), end(gifts));\n\t\t}\n\t\treturn GiftsDescriptor{\n\t\t\tgifts | ranges::to<std::vector<GiftDescriptor>>(),\n\t\t};\n\t});\n\tconst auto loadMore = [=] {\n\t\tif (state->priceTab.current() == kPriceTabMy\n\t\t\t&& !state->my.offset.isEmpty()\n\t\t\t&& !state->myLoading) {\n\t\t\tstate->myLoading = Data::MyUniqueGiftsSlice(\n\t\t\t\t&peer->session(),\n\t\t\t\tData::MyUniqueType::OnlyOwned,\n\t\t\t\tstate->my.offset\n\t\t\t) | rpl::on_next([=](MyGiftsDescriptor &&descriptor) {\n\t\t\t\tstate->myLoading.destroy();\n\t\t\t\tstate->my.offset = descriptor.list.empty()\n\t\t\t\t\t? QString()\n\t\t\t\t\t: descriptor.offset;\n\t\t\t\tstate->my.list.insert(\n\t\t\t\t\tend(state->my.list),\n\t\t\t\t\tstd::make_move_iterator(begin(descriptor.list)),\n\t\t\t\t\tstd::make_move_iterator(end(descriptor.list)));\n\t\t\t\tstate->myUpdated.fire({});\n\t\t\t});\n\t\t}\n\t};\n\tresult->add(MakeGiftsList({\n\t\t.window = window,\n\t\t.peer = peer,\n\t\t.gifts = std::move(gifts),\n\t\t.loadMore = loadMore,\n\t}));\n\n\treturn result;\n}\n\nvoid GiftBox(\n\t\tnot_null<GenericBox*> box,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerData*> peer,\n\t\tMyGiftsDescriptor my) {\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setStyle(st::creditsGiftBox);\n\tbox->setNoContentMargin(true);\n\tbox->setCustomCornersFilling(RectPart::FullTop);\n\tbox->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); });\n\n\twindow->session().credits().load();\n\n\tFillBg(box);\n\n\tconst auto &stUser = st::premiumGiftsUserpicButton;\n\tconst auto content = box->verticalLayout();\n\n\tAddSkip(content, st::defaultVerticalListSkip * 5);\n\n\t// Check disallowed gift types\n\tconst auto user = peer->asUser();\n\tusing Type = Api::DisallowedGiftType;\n\tconst auto disallowedTypes = user\n\t\t? user->disallowedGiftTypes()\n\t\t: Type::Premium;\n\tconst auto premiumDisallowed = peer->isSelf()\n\t\t|| (disallowedTypes & Type::Premium);\n\tconst auto limitedDisallowed = !peer->isSelf()\n\t\t&& (disallowedTypes & Type::Limited);\n\tconst auto unlimitedDisallowed = !peer->isSelf()\n\t\t&& (disallowedTypes & Type::Unlimited);\n\tconst auto uniqueDisallowed = !peer->isSelf()\n\t\t&& (disallowedTypes & Type::Unique);\n\tconst auto allStarsDisallowed = limitedDisallowed\n\t\t&& unlimitedDisallowed\n\t\t&& uniqueDisallowed;\n\n\tcontent->add(\n\t\tobject_ptr<UserpicButton>(content, peer, stUser),\n\t\tstyle::al_top\n\t)->setClickedCallback([=] { window->showPeerInfo(peer); });\n\tAddSkip(content);\n\tAddSkip(content);\n\n\tSettings::AddMiniStars(\n\t\tcontent,\n\t\tCreateChild<RpWidget>(content),\n\t\tstUser.photoSize,\n\t\tbox->width(),\n\t\t2.);\n\tAddSkip(content);\n\tAddSkip(box->verticalLayout());\n\n\tconst auto starsClickHandlerFilter = [=](const auto &...) {\n\t\twindow->showSettings(Settings::CreditsId());\n\t\treturn false;\n\t};\n\tif (peer->isUser() && !peer->isSelf() && !premiumDisallowed) {\n\t\tconst auto premiumClickHandlerFilter = [=](const auto &...) {\n\t\t\tSettings::ShowPremium(window, u\"gift_send\"_q);\n\t\t\treturn false;\n\t\t};\n\n\t\tAddBlock(content, window, {\n\t\t\t.subtitle = tr::lng_gift_premium_subtitle(),\n\t\t\t.about = tr::lng_gift_premium_about(\n\t\t\t\tlt_name,\n\t\t\t\trpl::single(tr::bold(peer->shortName())),\n\t\t\t\tlt_features,\n\t\t\t\ttr::lng_gift_premium_features(tr::link),\n\t\t\t\ttr::marked),\n\t\t\t.aboutFilter = premiumClickHandlerFilter,\n\t\t\t.content = MakePremiumGifts(window, peer),\n\t\t});\n\t}\n\n\t// Only add star gifts if at least one type is allowed\n\tif (!allStarsDisallowed) {\n\t\tconst auto collectibles = content->lifetime().make_state<\n\t\t\trpl::variable<bool>\n\t\t>();\n\t\tauto tabSelected = [=](int tab) {\n\t\t\t*collectibles = (tab == kPriceTabCollectibles);\n\t\t};\n\t\tAddBlock(content, window, {\n\t\t\t.subtitle = (peer->isSelf()\n\t\t\t\t? tr::lng_gift_self_title()\n\t\t\t\t: peer->isBroadcast()\n\t\t\t\t? tr::lng_gift_channel_title()\n\t\t\t\t: tr::lng_gift_stars_subtitle()),\n\t\t\t.about = (peer->isSelf()\n\t\t\t\t? tr::lng_gift_self_about(tr::marked)\n\t\t\t\t: peer->isBroadcast()\n\t\t\t\t? tr::lng_gift_channel_about(\n\t\t\t\t\tlt_name,\n\t\t\t\t\trpl::single(tr::bold(peer->name())),\n\t\t\t\t\ttr::marked)\n\t\t\t\t: rpl::conditional(\n\t\t\t\t\tcollectibles->value(),\n\t\t\t\t\ttr::lng_gift_stars_about_collectibles(\n\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\ttr::lng_gift_stars_link(tr::link),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\ttr::lng_gift_stars_about(\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\trpl::single(tr::bold(peer->shortName())),\n\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\ttr::lng_gift_stars_link(tr::link),\n\t\t\t\t\t\ttr::marked))),\n\t\t\t.aboutFilter = starsClickHandlerFilter,\n\t\t\t.content = MakeStarsGifts(\n\t\t\t\twindow,\n\t\t\t\tpeer,\n\t\t\t\tstd::move(my),\n\t\t\t\tstd::move(tabSelected)),\n\t\t});\n\t}\n}\n\n[[nodiscard]] base::unique_qptr<Ui::PopupMenu> CreateRowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerData*> peer,\n\t\tPickCallback pick) {\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\tresult->addAction(\n\t\ttr::lng_context_send_message(tr::now),\n\t\t[=] { pick(peer, PickType::SendMessage); },\n\t\t&st::menuIconChatBubble);\n\tresult->addAction(\n\t\ttr::lng_context_view_profile(tr::now),\n\t\t[=] { pick(peer, PickType::OpenProfile); },\n\t\t&st::menuIconProfile);\n\treturn result;\n}\n\nstruct CustomList {\n\tobject_ptr<Ui::RpWidget> content = { nullptr };\n\tFn<bool(int, int, int)> overrideKey;\n\tFn<void()> activate;\n\tFn<bool()> hasSelection;\n};\n\nclass Controller final : public ContactsBoxController {\npublic:\n\tController(not_null<Main::Session*> session, PickCallback pick);\n\n\tvoid noSearchSubmit();\n\n\tbool overrideKeyboardNavigation(\n\t\tint direction,\n\t\tint fromIndex,\n\t\tint toIndex) override final;\n\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override final;\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override final;\n\nprivate:\n\tstd::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<UserData*> user) override;\n\n\tvoid prepareViewHook() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\n\tconst PickCallback _pick;\n\tconst std::vector<UserId> _contactBirthdays;\n\tCustomList _selfOption;\n\tCustomList _birthdayOptions;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tbool _skipUpDirectionSelect = false;\n\n};\n\n[[nodiscard]] CustomList MakeCustomList(\n\t\tnot_null<Main::Session*> session,\n\t\tFn<void(not_null<PeerListController*>)> fill,\n\t\tPickCallback pick,\n\t\trpl::producer<QString> below) {\n\tclass CustomController final : public PeerListController {\n\tpublic:\n\t\tCustomController(\n\t\t\tnot_null<Main::Session*> session,\n\t\t\tFn<void(not_null<PeerListController*>)> fill,\n\t\t\tPickCallback pick)\n\t\t: _session(session)\n\t\t, _pick(std::move(pick))\n\t\t, _fill(std::move(fill)) {\n\t\t}\n\n\t\tvoid prepare() override {\n\t\t\tif (_fill) {\n\t\t\t\t_fill(this);\n\t\t\t}\n\t\t}\n\t\tvoid loadMoreRows() override {\n\t\t}\n\t\tvoid rowClicked(not_null<PeerListRow*> row) override {\n\t\t\t_pick(row->peer(), PickType::Activate);\n\t\t}\n\t\tMain::Session &session() const override {\n\t\t\treturn *_session;\n\t\t}\n\n\t\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override {\n\t\t\tdelegate()->peerListShowRowMenu(row, true);\n\t\t}\n\n\t\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\t\t\tQWidget *parent,\n\t\t\t\tnot_null<PeerListRow*> row) override {\n\t\t\tauto result = CreateRowContextMenu(parent, row->peer(), _pick);\n\n\t\t\tif (result) {\n\t\t\t\tbase::take(_menu);\n\n\t\t\t\t_menu = base::unique_qptr<Ui::PopupMenu>(result.get());\n\t\t\t}\n\n\t\t\treturn result;\n\t\t}\n\n\tprivate:\n\t\tconst not_null<Main::Session*> _session;\n\t\tPickCallback _pick;\n\t\tFn<void(not_null<PeerListController*>)> _fill;\n\n\t\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\t};\n\n\tauto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tconst auto container = result.data();\n\n\tUi::AddSkip(container);\n\n\tconst auto delegate = container->lifetime().make_state<\n\t\tPeerListContentDelegateSimple\n\t>();\n\tconst auto controller\n\t\t= container->lifetime().make_state<CustomController>(\n\t\t\tsession,\n\t\t\tfill,\n\t\t\tpick);\n\n\tcontroller->setStyleOverrides(&st::peerListSingleRow);\n\tconst auto content = container->add(object_ptr<PeerListContent>(\n\t\tcontainer,\n\t\tcontroller));\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\n\tif (below) {\n\t\tUi::AddSkip(container);\n\t\tcontainer->add(CreatePeerListSectionSubtitle(\n\t\t\tcontainer,\n\t\t\tstd::move(below)));\n\t}\n\n\tconst auto overrideKey = [=](int direction, int from, int to) {\n\t\tif (!content->isVisible()) {\n\t\t\treturn false;\n\t\t} else if (direction > 0 && from < 0 && to >= 0) {\n\t\t\tif (content->hasSelection()) {\n\t\t\t\tconst auto was = content->selectedIndex();\n\t\t\t\tconst auto now = content->selectSkip(1).reallyMovedTo;\n\t\t\t\tif (was != now) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tcontent->clearSelection();\n\t\t\t} else {\n\t\t\t\tcontent->selectSkip(1);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (direction < 0 && to < 0) {\n\t\t\tif (!content->hasSelection()) {\n\t\t\t\tcontent->selectLast();\n\t\t\t} else if (from >= 0 || content->hasSelection()) {\n\t\t\t\tcontent->selectSkip(-1);\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\tconst auto hasSelection = [=] {\n\t\treturn content->isVisible() && content->hasSelection();\n\t};\n\n\treturn {\n\t\t.content = std::move(result),\n\t\t.overrideKey = overrideKey,\n\t\t.activate = [=] {\n\t\t\tif (content->hasSelection()) {\n\t\t\t\tpick(\n\t\t\t\t\tcontent->rowAt(content->selectedIndex())->peer(),\n\t\t\t\t\tPickType::Activate);\n\t\t\t}\n\t\t},\n\t\t.hasSelection = hasSelection,\n\t};\n}\n\nController::Controller(not_null<Main::Session*> session, PickCallback pick)\n: ContactsBoxController(session)\n, _pick(std::move(pick))\n, _contactBirthdays(\n\tsession->promoSuggestions().knownContactBirthdays().value_or(\n\t\tstd::vector<UserId>{}))\n, _selfOption(\n\tMakeCustomList(\n\t\tsession,\n\t\t[=](not_null<PeerListController*> controller) {\n\t\t\tauto row = std::make_unique<PeerListRow>(session->user());\n\t\t\trow->setCustomStatus(tr::lng_gift_self_status(tr::now));\n\t\t\tcontroller->delegate()->peerListAppendRow(std::move(row));\n\t\t\tcontroller->delegate()->peerListRefreshRows();\n\t\t},\n\t\t_pick,\n\t\t_contactBirthdays.empty()\n\t\t\t? tr::lng_contacts_header()\n\t\t\t: tr::lng_gift_subtitle_birthdays()))\n, _birthdayOptions(\n\tMakeCustomList(\n\t\tsession,\n\t\t[=](not_null<PeerListController*> controller) {\n\t\t\tconst auto status = [=](const Data::Birthday &date) {\n\t\t\t\tif (Data::IsBirthdayToday(date)) {\n\t\t\t\t\treturn tr::lng_gift_list_birthday_status_today(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\tData::BirthdayCake());\n\t\t\t\t}\n\t\t\t\tconst auto yesterday = QDate::currentDate().addDays(-1);\n\t\t\t\tconst auto tomorrow = QDate::currentDate().addDays(1);\n\t\t\t\tif (date.day() == yesterday.day()\n\t\t\t\t\t\t&& date.month() == yesterday.month()) {\n\t\t\t\t\treturn tr::lng_gift_list_birthday_status_yesterday(\n\t\t\t\t\t\ttr::now);\n\t\t\t\t} else if (date.day() == tomorrow.day()\n\t\t\t\t\t\t&& date.month() == tomorrow.month()) {\n\t\t\t\t\treturn tr::lng_gift_list_birthday_status_tomorrow(\n\t\t\t\t\t\ttr::now);\n\t\t\t\t}\n\t\t\t\treturn QString();\n\t\t\t};\n\n\t\t\tauto usersWithBirthdays = ranges::views::all(\n\t\t\t\t_contactBirthdays\n\t\t\t) | ranges::views::transform([&](UserId userId) {\n\t\t\t\treturn session->data().user(userId);\n\t\t\t}) | ranges::to_vector;\n\n\t\t\tranges::sort(usersWithBirthdays, [](UserData *a, UserData *b) {\n\t\t\t\tconst auto aBirthday = a->birthday();\n\t\t\t\tconst auto bBirthday = b->birthday();\n\t\t\t\tconst auto aIsToday = Data::IsBirthdayToday(aBirthday);\n\t\t\t\tconst auto bIsToday = Data::IsBirthdayToday(bBirthday);\n\t\t\t\tif (aIsToday != bIsToday) {\n\t\t\t\t\treturn aIsToday > bIsToday;\n\t\t\t\t}\n\t\t\t\tif (aBirthday.month() != bBirthday.month()) {\n\t\t\t\t\treturn aBirthday.month() < bBirthday.month();\n\t\t\t\t}\n\t\t\t\treturn aBirthday.day() < bBirthday.day();\n\t\t\t});\n\n\t\t\tfor (const auto &user : usersWithBirthdays) {\n\t\t\t\tauto row = std::make_unique<PeerRow>(user);\n\t\t\t\tif (auto s = status(user->birthday()); !s.isEmpty()) {\n\t\t\t\t\trow->setCustomStatus(std::move(s));\n\t\t\t\t}\n\t\t\t\tcontroller->delegate()->peerListAppendRow(std::move(row));\n\t\t\t}\n\n\t\t\tcontroller->delegate()->peerListRefreshRows();\n\t\t},\n\t\t_pick,\n\t\t_contactBirthdays.empty()\n\t\t\t? rpl::producer<QString>(nullptr)\n\t\t\t: tr::lng_contacts_header())) {\n\tsetStyleOverrides(&st::peerListSmallSkips);\n}\n\nvoid Controller::rowRightActionClicked(not_null<PeerListRow*> row) {\n\tdelegate()->peerListShowRowMenu(row, true);\n}\n\nbase::unique_qptr<Ui::PopupMenu> Controller::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tauto result = CreateRowContextMenu(parent, row->peer(), _pick);\n\n\tif (result) {\n\t\t// First clear _menu value, so that we don't check row positions yet.\n\t\tbase::take(_menu);\n\n\t\t// Here unique_qptr is used like a shared pointer, where\n\t\t// not the last destroyed pointer destroys the object, but the first.\n\t\t_menu = base::unique_qptr<Ui::PopupMenu>(result.get());\n\t}\n\n\treturn result;\n}\n\nvoid Controller::noSearchSubmit() {\n\tif (const auto onstack = _selfOption.activate) {\n\t\tonstack();\n\t}\n\tif (const auto onstack = _birthdayOptions.activate) {\n\t\tonstack();\n\t}\n}\n\nbool Controller::overrideKeyboardNavigation(\n\t\tint direction,\n\t\tint from,\n\t\tint to) {\n\tif (direction == -1 && from == -1 && to == -1 && _skipUpDirectionSelect) {\n\t\treturn true;\n\t}\n\t_skipUpDirectionSelect = false;\n\tif (direction > 0) {\n\t\tif (!_selfOption.hasSelection() && !_birthdayOptions.hasSelection()) {\n\t\t\treturn _selfOption.overrideKey(direction, from, to);\n\t\t}\n\t\tif (_selfOption.hasSelection() && !_birthdayOptions.hasSelection()) {\n\t\t\tif (_selfOption.overrideKey(direction, from, to)) {\n\t\t\t\treturn true;\n\t\t\t} else {\n\t\t\t\treturn _birthdayOptions.overrideKey(direction, from, to);\n\t\t\t}\n\t\t}\n\t\tif (!_selfOption.hasSelection() && _birthdayOptions.hasSelection()) {\n\t\t\tif (_birthdayOptions.overrideKey(direction, from, to)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t} else if (direction < 0) {\n\t\tif (!_selfOption.hasSelection() && !_birthdayOptions.hasSelection()) {\n\t\t\treturn _birthdayOptions.overrideKey(direction, from, to);\n\t\t}\n\t\tif (!_selfOption.hasSelection() && _birthdayOptions.hasSelection()) {\n\t\t\tif (_birthdayOptions.overrideKey(direction, from, to)) {\n\t\t\t\treturn true;\n\t\t\t} else if (!_birthdayOptions.hasSelection()) {\n\t\t\t\tconst auto res = _selfOption.overrideKey(direction, from, to);\n\t\t\t\t_skipUpDirectionSelect = _selfOption.hasSelection();\n\t\t\t\treturn res;\n\t\t\t}\n\t\t}\n\t\tif (_selfOption.hasSelection() && !_birthdayOptions.hasSelection()) {\n\t\t\tif (_selfOption.overrideKey(direction, from, to)) {\n\t\t\t\t_skipUpDirectionSelect = _selfOption.hasSelection();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nstd::unique_ptr<PeerListRow> Controller::createRow(\n\t\tnot_null<UserData*> user) {\n\tif (const auto birthday\n\t\t\t= user->session().promoSuggestions().knownContactBirthdays()) {\n\t\tif (ranges::contains(*birthday, peerToUser(user->id))) {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\tif (user->isSelf()\n\t\t|| user->isBot()\n\t\t|| user->isServiceUser()\n\t\t|| user->isInaccessible()) {\n\t\treturn nullptr;\n\t}\n\treturn std::make_unique<PeerRow>(user);\n}\n\nvoid Controller::prepareViewHook() {\n\tauto list = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tlist->add(std::move(_selfOption.content));\n\tlist->add(std::move(_birthdayOptions.content));\n\tdelegate()->peerListSetAboveWidget(std::move(list));\n}\n\nvoid Controller::rowClicked(not_null<PeerListRow*> row) {\n\t_pick(row->peer(), PickType::Activate);\n}\n\n} // namespace\n\nvoid ChooseStarGiftRecipient(\n\t\tnot_null<Window::SessionController*> window) {\n\tconst auto session = &window->session();\n\tsession->promoSuggestions().requestContactBirthdays([=] {\n\t\tauto controller = std::make_unique<Controller>(\n\t\t\tsession,\n\t\t\t[=](not_null<PeerData*> peer, PickType type) {\n\t\t\t\tif (type == PickType::Activate) {\n\t\t\t\t\tShowStarGiftBox(window, peer);\n\t\t\t\t} else if (type == PickType::SendMessage) {\n\t\t\t\t\tusing Way = Window::SectionShow::Way;\n\t\t\t\t\twindow->showPeerHistory(peer, Way::Forward);\n\t\t\t\t} else if (type == PickType::OpenProfile) {\n\t\t\t\t\twindow->show(PrepareShortInfoBox(peer, window));\n\t\t\t\t}\n\t\t\t});\n\t\tconst auto controllerRaw = controller.get();\n\t\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\t\tbox->setTitle(tr::lng_gift_premium_or_stars());\n\t\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\n\t\t\tbox->noSearchSubmits() | rpl::on_next([=] {\n\t\t\t\tcontrollerRaw->noSearchSubmit();\n\t\t\t}, box->lifetime());\n\t\t};\n\t\twindow->show(\n\t\t\tBox<PeerListBox>(std::move(controller), std::move(initBox)),\n\t\t\tLayerOption::KeepOther);\n\t});\n}\n\nvoid ShowStarGiftBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer) {\n\tif (controller->showFrozenError()) {\n\t\treturn;\n\t}\n\n\tstruct Session {\n\t\tPeerData *peer = nullptr;\n\t\tMyGiftsDescriptor my;\n\t\tbool premiumGiftsReady = false;\n\t\tbool starsGiftsReady = false;\n\t\tbool fullReady = false;\n\t\tbool myReady = false;\n\n\t\tbool hasPremium = false;\n\t\tbool hasUpgradable = false;\n\t\tbool hasLimited = false;\n\t\tbool hasUnlimited = false;\n\n\t\trpl::lifetime lifetime;\n\n\t\t[[nodiscard]] bool ready() const {\n\t\t\treturn premiumGiftsReady\n\t\t\t\t&& starsGiftsReady\n\t\t\t\t&& fullReady\n\t\t\t\t&& myReady;\n\t\t}\n\t};\n\tstatic auto Map = base::flat_map<not_null<Main::Session*>, Session>();\n\n\tconst auto session = &controller->session();\n\tauto i = Map.find(session);\n\tif (i == end(Map)) {\n\t\ti = Map.emplace(session).first;\n\t\tsession->lifetime().add([=] { Map.remove(session); });\n\t} else if (i->second.peer == peer) {\n\t\treturn;\n\t}\n\ti->second = Session{ .peer = peer };\n\n\tconst auto weak = base::make_weak(controller);\n\tconst auto checkReady = [=] {\n\t\tauto &entry = Map[session];\n\t\tif (!entry.ready()) {\n\t\t\treturn;\n\t\t}\n\t\tauto was = std::move(entry);\n\t\tentry = Session();\n\t\tif (const auto strong = weak.get()) {\n\t\t\tif (const auto user = peer->asUser(); user && !user->isSelf()) {\n\t\t\t\tusing Type = Api::DisallowedGiftType;\n\t\t\t\tconst auto disallowedTypes = user->disallowedGiftTypes();\n\t\t\t\tconst auto premium = (disallowedTypes & Type::Premium)\n\t\t\t\t\t|| peer->isSelf();\n\t\t\t\tconst auto limited = (disallowedTypes & Type::Limited);\n\t\t\t\tconst auto unlimited = (disallowedTypes & Type::Unlimited);\n\t\t\t\tconst auto unique = (disallowedTypes & Type::Unique);\n\t\t\t\tif ((unique || (!was.hasUpgradable && was.my.list.empty()))\n\t\t\t\t\t&& (premium || !was.hasPremium)\n\t\t\t\t\t&& (limited || !was.hasLimited)\n\t\t\t\t\t&& (unlimited || !was.hasUnlimited)) {\n\t\t\t\t\tstrong->showToast(\n\t\t\t\t\t\ttr::lng_edit_privacy_gifts_restricted(tr::now));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tstrong->show(Box(GiftBox, strong, peer, std::move(was.my)));\n\t\t}\n\t};\n\n\tconst auto user = peer->asUser();\n\tif (user && !user->isSelf()) {\n\t\tGiftsPremium(\n\t\t\tsession,\n\t\t\tpeer\n\t\t) | rpl::on_next([=](PremiumGiftsDescriptor &&gifts) {\n\t\t\tauto &entry = Map[session];\n\t\t\tentry.premiumGiftsReady = true;\n\t\t\tentry.hasPremium = !gifts.list.empty();\n\t\t\tcheckReady();\n\t\t}, i->second.lifetime);\n\t} else {\n\t\ti->second.hasPremium = false;\n\t\ti->second.premiumGiftsReady = true;\n\t}\n\n\tif (peer->isFullLoaded()) {\n\t\ti->second.fullReady = true;\n\t} else {\n\t\tpeer->updateFull();\n\t\tpeer->session().changes().peerUpdates(\n\t\t\tpeer,\n\t\t\tData::PeerUpdate::Flag::FullInfo\n\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\tauto &entry = Map[session];\n\t\t\tentry.fullReady = true;\n\t\t\tcheckReady();\n\t\t}, i->second.lifetime);\n\t}\n\n\tGiftsStars(\n\t\tsession,\n\t\tpeer\n\t) | rpl::on_next([=](std::vector<GiftTypeStars> &&gifts) {\n\t\tauto &entry = Map[session];\n\t\tentry.starsGiftsReady = true;\n\t\tfor (const auto &gift : gifts) {\n\t\t\tif (gift.info.limitedCount) {\n\t\t\t\tentry.hasLimited = true;\n\t\t\t\tif (gift.info.starsToUpgrade) {\n\t\t\t\t\tentry.hasUpgradable = true;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tentry.hasUnlimited = true;\n\t\t\t}\n\t\t}\n\t\tcheckReady();\n\t}, i->second.lifetime);\n\n\tData::MyUniqueGiftsSlice(\n\t\tsession,\n\t\tData::MyUniqueType::OnlyOwned\n\t) | rpl::on_next([=](MyGiftsDescriptor &&gifts) {\n\t\tauto &entry = Map[session];\n\t\tentry.my = std::move(gifts);\n\t\tentry.myReady = true;\n\t\tcheckReady();\n\t}, i->second.lifetime);\n}\n\nvoid AddWearGiftCover(\n\t\tnot_null<VerticalLayout*> container,\n\t\tconst Data::UniqueGift &data,\n\t\tnot_null<PeerData*> peer) {\n\tconst auto cover = container->add(object_ptr<RpWidget>(container));\n\n\tconst auto title = CreateChild<FlatLabel>(\n\t\tcover,\n\t\trpl::single(peer->name()),\n\t\tst::uniqueGiftTitle);\n\ttitle->setTextColorOverride(QColor(255, 255, 255));\n\tconst auto subtitle = CreateChild<FlatLabel>(\n\t\tcover,\n\t\t(peer->isChannel()\n\t\t\t? tr::lng_chat_status_subscribers(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(peer->asChannel()->membersCount() * 1.))\n\t\t\t: tr::lng_status_online()),\n\t\tst::uniqueGiftSubtitle);\n\tsubtitle->setTextColorOverride(data.backdrop.textColor);\n\n\tstruct State {\n\t\tQImage gradient;\n\t\tData::UniqueGift gift;\n\t\tUi::PeerUserpicView view;\n\t\tstd::unique_ptr<Text::CustomEmoji> emoji;\n\t\tbase::flat_map<float64, QImage> emojis;\n\t\trpl::lifetime lifetime;\n\t};\n\tconst auto state = cover->lifetime().make_state<State>(State{\n\t\t.gift = data,\n\t});\n\tstate->emoji = peer->owner().customEmojiManager().create(\n\t\tstate->gift.pattern.document,\n\t\t[=] { cover->update(); },\n\t\tData::CustomEmojiSizeTag::Large);\n\n\tcover->widthValue() | rpl::on_next([=](int width) {\n\t\tconst auto skip = st::uniqueGiftBottom;\n\t\tif (width <= 3 * skip) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto available = width - 2 * skip;\n\t\ttitle->resizeToWidth(available);\n\t\ttitle->moveToLeft(skip, st::uniqueGiftTitleTop);\n\n\t\tsubtitle->resizeToWidth(available);\n\t\tsubtitle->moveToLeft(skip, st::uniqueGiftSubtitleTop);\n\n\t\tcover->resize(width, subtitle->y() + subtitle->height() + skip);\n\t}, cover->lifetime());\n\n\tcover->paintRequest() | rpl::on_next([=] {\n\t\tauto p = Painter(cover);\n\n\t\tconst auto width = cover->width();\n\t\tconst auto pointsHeight = st::uniqueGiftSubtitleTop;\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tif (state->gradient.size() != cover->size() * ratio) {\n\t\t\tstate->gradient = Ui::CreateTopBgGradient(\n\t\t\t\tcover->size(),\n\t\t\t\tstate->gift);\n\t\t}\n\t\tp.drawImage(0, 0, state->gradient);\n\n\t\tUi::PaintBgPoints(\n\t\t\tp,\n\t\t\tUi::PatternBgPoints(),\n\t\t\tstate->emojis,\n\t\t\tstate->emoji.get(),\n\t\t\tstate->gift,\n\t\t\tQRect(0, 0, width, pointsHeight),\n\t\t\t1.);\n\n\t\tpeer->paintUserpic(\n\t\t\tp,\n\t\t\tstate->view,\n\t\t\t(width - st::uniqueGiftUserpicSize) / 2,\n\t\t\tst::uniqueGiftUserpicTop,\n\t\t\tst::uniqueGiftUserpicSize);\n\t}, cover->lifetime());\n}\n\nvoid AttachGiftSenderBadge(\n\t\tnot_null<GenericBox*> box,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> from,\n\t\tconst QDateTime &date,\n\t\tbool crafted) {\n\tconst auto parent = box->getDelegate()->outerContainer();\n\n\tconst auto dateText = tr::bold(langDayOfMonth(date.date()));\n\tconst auto badge = CreateChild<FlatLabel>(\n\t\tparent,\n\t\t(from->isSelf()\n\t\t\t? (crafted\n\t\t\t\t? tr::lng_gift_unique_crafter_you\n\t\t\t\t: tr::lng_gift_unique_sender_you)(\n\t\t\t\t\tlt_date,\n\t\t\t\t\trpl::single(dateText),\n\t\t\t\t\ttr::marked)\n\t\t\t: tr::lng_gift_unique_sender(\n\t\t\t\tlt_from,\n\t\t\t\trpl::single(tr::link(tr::bold(from->shortName()), 1)),\n\t\t\t\tlt_date,\n\t\t\t\trpl::single(dateText),\n\t\t\t\ttr::marked)),\n\t\tst::uniqueGiftSenderBadge);\n\n\tbadge->paintOn([=](QPainter &p) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setBrush(st::radialBg);\n\t\tp.setPen(Qt::NoPen);\n\t\tconst auto radius = badge->height() / 2.;\n\t\tp.drawRoundedRect(badge->rect(), radius, radius);\n\t});\n\tbadge->setLink(1, std::make_shared<LambdaClickHandler>([=] {\n\t\tif (const auto window = show->resolveWindow()) {\n\t\t\twindow->showPeerHistory(from);\n\t\t}\n\t}));\n\n\tauto widget = static_cast<QWidget*>(box);\n\twhile (widget->parentWidget() && widget->parentWidget() != parent) {\n\t\twidget = widget->parentWidget();\n\t}\n\n\tconst auto updateGeometry = [=] {\n\t\tconst auto outer = parent->rect();\n\t\tconst auto margin = st::msgServiceMargin;\n\t\tconst auto padding = st::msgServicePadding;\n\t\tconst auto available = outer.marginsRemoved(margin).width();\n\t\tbadge->resizeToWidth(std::min(\n\t\t\tavailable - padding.left() - padding.right(),\n\t\t\tbadge->textMaxWidth()));\n\t\tconst auto inner = Ui::MapFrom(parent, box, box->rect());\n\t\tconst auto top = std::max(\n\t\t\tinner.y() - badge->height() - margin.top(),\n\t\t\touter.y() + (margin.top() * 2));\n\t\tconst auto left = margin.left() + (available - badge->width()) / 2;\n\t\tbadge->move(left, top);\n\t};\n\tbadge->naturalWidthValue(\n\t) | rpl::on_next(updateGeometry, badge->lifetime());\n\tfor (auto w = static_cast<QWidget*>(box)\n\t\t; w != widget\n\t\t; w = w->parentWidget()) {\n\t\tbase::install_event_filter(w, [=](not_null<QEvent*> e) {\n\t\t\tconst auto type = e->type();\n\t\t\tif (type == QEvent::Move || type == QEvent::Resize) {\n\t\t\t\tPostponeCall(badge, updateGeometry);\n\t\t\t}\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}, badge->lifetime());\n\t}\n\n\tbase::install_event_filter(widget, [=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::Show) {\n\t\t\tbadge->show();\n\t\t} else if (type == QEvent::Hide) {\n\t\t\tbadge->hide();\n\t\t} else if (type == QEvent::Move || type == QEvent::Resize) {\n\t\t\tPostponeCall(badge, updateGeometry);\n\t\t} else if (type == QEvent::ZOrderChange) {\n\t\t\tPostponeCall(badge, [=] { badge->raise(); });\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t}, badge->lifetime());\n\tbadge->setVisible(!widget->isHidden());\n\n\tbase::install_event_filter(parent, [=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::ChildAdded) {\n\t\t\tPostponeCall(badge, [=] { badge->raise(); });\n\t\t} else if (e->type() == QEvent::Resize) {\n\t\t\tPostponeCall(badge, updateGeometry);\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t}, badge->lifetime());\n\tbadge->raise();\n\n\tbox->boxClosing() | rpl::on_next([=] {\n\t\tdelete badge;\n\t}, badge->lifetime());\n}\n\nvoid ShowUniqueGiftWearBox(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tconst Data::UniqueGift &gift,\n\t\tSettings::GiftWearBoxStyleOverride st) {\n\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tbox->setNoContentMargin(true);\n\n\t\tbox->setWidth((st::boxWidth + st::boxWideWidth) / 2); // =)\n\t\tbox->setStyle(st.box ? *st.box : st::upgradeGiftBox);\n\n\t\tconst auto channel = peer->isChannel();\n\t\tconst auto content = box->verticalLayout();\n\t\tAddWearGiftCover(content, gift, peer);\n\n\t\tAddSkip(content, st::defaultVerticalListSkip * 2);\n\n\t\tconst auto infoRow = [&](\n\t\t\t\trpl::producer<QString> title,\n\t\t\t\trpl::producer<QString> text,\n\t\t\t\tnot_null<const style::icon*> icon) {\n\t\t\tauto raw = content->add(\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(content));\n\t\t\traw->add(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\traw,\n\t\t\t\t\tstd::move(title) | rpl::map(tr::bold),\n\t\t\t\t\tst.infoTitle ? *st.infoTitle : st::defaultFlatLabel),\n\t\t\t\tst::settingsPremiumRowTitlePadding);\n\t\t\traw->add(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\traw,\n\t\t\t\t\tstd::move(text),\n\t\t\t\t\tst.infoAbout ? *st.infoAbout : st::upgradeGiftSubtext),\n\t\t\t\tst::settingsPremiumRowAboutPadding);\n\t\t\tobject_ptr<Info::Profile::FloatingIcon>(\n\t\t\t\traw,\n\t\t\t\t*icon,\n\t\t\t\tst::starrefInfoIconPosition);\n\t\t};\n\n\t\tcontent->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_gift_wear_title(\n\t\t\t\t\tlt_name,\n\t\t\t\t\trpl::single(UniqueGiftName(gift))),\n\t\t\t\tst.title ? *st.title : st::uniqueGiftTitle),\n\t\t\tst::settingsPremiumRowTitlePadding,\n\t\t\tstyle::al_top);\n\t\tcontent->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_gift_wear_about(),\n\t\t\t\tst.subtitle ? *st.subtitle : st::uniqueGiftSubtitle),\n\t\t\tst::settingsPremiumRowAboutPadding,\n\t\t\tstyle::al_top);\n\t\tinfoRow(\n\t\t\ttr::lng_gift_wear_badge_title(),\n\t\t\t(channel\n\t\t\t\t? tr::lng_gift_wear_badge_about_channel()\n\t\t\t\t: tr::lng_gift_wear_badge_about()),\n\t\t\tst.radiantIcon ? st.radiantIcon : &st::menuIconUnique);\n\t\tinfoRow(\n\t\t\ttr::lng_gift_wear_design_title(),\n\t\t\t(channel\n\t\t\t\t? tr::lng_gift_wear_design_about_channel()\n\t\t\t\t: tr::lng_gift_wear_design_about()),\n\t\t\tst.profileIcon ? st.profileIcon : &st::menuIconUniqueProfile);\n\t\tinfoRow(\n\t\t\ttr::lng_gift_wear_proof_title(),\n\t\t\t(channel\n\t\t\t\t? tr::lng_gift_wear_proof_about_channel()\n\t\t\t\t: tr::lng_gift_wear_proof_about()),\n\t\t\tst.proofIcon ? st.proofIcon : &st::menuIconFactcheck);\n\n\t\tconst auto session = &show->session();\n\t\tconst auto checking = std::make_shared<bool>();\n\t\tconst auto button = box->addButton(rpl::single(QString()), [=] {\n\t\t\tconst auto emojiStatuses = &session->data().emojiStatuses();\n\t\t\tconst auto id = emojiStatuses->fromUniqueGift(gift);\n\t\t\tif (!peer->isSelf()) {\n\t\t\t\tif (*checking) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t*checking = true;\n\t\t\t\tconst auto weak = base::make_weak(box);\n\t\t\t\tCheckBoostLevel(show, peer, [=](int level) {\n\t\t\t\t\tconst auto limits = Data::LevelLimits(&peer->session());\n\t\t\t\t\tconst auto wanted = limits.channelEmojiStatusLevelMin();\n\t\t\t\t\tif (level >= wanted) {\n\t\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t\t}\n\t\t\t\t\t\temojiStatuses->set(peer, id);\n\t\t\t\t\t\treturn std::optional<Ui::AskBoostReason>();\n\t\t\t\t\t}\n\t\t\t\t\tconst auto reason = [&]() -> Ui::AskBoostReason {\n\t\t\t\t\t\treturn { Ui::AskBoostWearCollectible{\n\t\t\t\t\t\t\twanted\n\t\t\t\t\t\t} };\n\t\t\t\t\t}();\n\t\t\t\t\treturn std::make_optional(reason);\n\t\t\t\t}, [=] { *checking = false; });\n\t\t\t} else if (session->premium()) {\n\t\t\t\tbox->closeBox();\n\t\t\t\temojiStatuses->set(peer, id);\n\t\t\t} else {\n\t\t\t\tconst auto link = tr::bold(\n\t\t\t\t\ttr::lng_send_as_premium_required_link(tr::now));\n\t\t\t\tSettings::ShowPremiumPromoToast(\n\t\t\t\t\tshow,\n\t\t\t\t\ttr::lng_gift_wear_subscribe(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\ttr::link(link),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\tu\"wear_collectibles\"_q);\n\t\t\t}\n\t\t});\n\t\tconst auto lock = Ui::Text::IconEmoji(&st::giftBoxLock);\n\t\tauto label = rpl::combine(\n\t\t\ttr::lng_gift_wear_start(),\n\t\t\tData::AmPremiumValue(&show->session())\n\t\t) | rpl::map([=](const QString &text, bool premium) {\n\t\t\tauto result = TextWithEntities();\n\t\t\tif (!premium && peer->isSelf()) {\n\t\t\t\tresult.append(lock);\n\t\t\t}\n\t\t\tresult.append(text);\n\t\t\treturn result;\n\t\t});\n\t\tSetButtonMarkedLabel(\n\t\t\tbutton,\n\t\t\tstd::move(label),\n\t\t\tsession,\n\t\t\tst::creditsBoxButtonLabel,\n\t\t\t&st::giftBox.button.textFg);\n\t\tUi::AddUniqueCloseButton(box);\n\t}));\n}\n\nvoid PreloadUniqueGiftResellPrices(not_null<Main::Session*> session) {\n\tconst auto entry = ResalePrices(session);\n\tconst auto now = crl::now();\n\tconst auto makeRequest = entry->prices.empty()\n\t\t|| (now - entry->lastReceived >= kResellPriceCacheLifetime);\n\tif (!makeRequest || entry->requestLifetime) {\n\t\treturn;\n\t}\n\tconst auto finish = [=] {\n\t\tentry->requestLifetime.destroy();\n\t\tentry->lastReceived = crl::now();\n\t\tfor (const auto &callback : base::take(entry->waiting)) {\n\t\t\tcallback();\n\t\t}\n\t};\n\tentry->requestLifetime = entry->api->requestStarGifts(\n\t) | rpl::on_error_done(finish, [=] {\n\t\tconst auto &gifts = entry->api->starGifts();\n\t\tentry->prices.reserve(gifts.size());\n\t\tfor (auto &gift : gifts) {\n\t\t\tif (!gift.resellTitle.isEmpty() && gift.starsResellMin > 0) {\n\t\t\t\tentry->prices[gift.resellTitle] = gift.starsResellMin;\n\t\t\t}\n\t\t}\n\t\tfinish();\n\t});\n}\n\nvoid InvokeWithUniqueGiftResellPrice(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &title,\n\t\tFn<void(int)> callback) {\n\tPreloadUniqueGiftResellPrices(session);\n\n\tconst auto finish = [=] {\n\t\tconst auto entry = ResalePrices(session);\n\t\tAssert(entry->lastReceived != 0);\n\n\t\tconst auto i = entry->prices.find(title);\n\t\tcallback((i != end(entry->prices)) ? i->second : 0);\n\t};\n\tconst auto entry = ResalePrices(session);\n\tif (entry->lastReceived) {\n\t\tfinish();\n\t} else {\n\t\tentry->waiting.push_back(finish);\n\t}\n}\n\nvoid UpdateGiftSellPrice(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tstd::shared_ptr<Data::UniqueGift> unique,\n\t\tData::SavedStarGiftId savedId,\n\t\tCreditsAmount price) {\n\tconst auto wasOnResale = (unique->starsForResale > 0);\n\tconst auto session = &show->session();\n\tsession->api().request(MTPpayments_UpdateStarGiftPrice(\n\t\tApi::InputSavedStarGiftId(savedId, unique),\n\t\t(price\n\t\t\t? StarsAmountToTL(price)\n\t\t\t: MTP_starsAmount(MTP_long(0), MTP_int(0)))\n\t)).done([=](const MTPUpdates &result) {\n\t\tsession->api().applyUpdates(result);\n\t\tshow->showToast((!price\n\t\t\t? tr::lng_gift_sell_removed\n\t\t\t: wasOnResale\n\t\t\t? tr::lng_gift_sell_updated\n\t\t\t: tr::lng_gift_sell_toast)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_name,\n\t\t\t\tData::UniqueGiftName(*unique)));\n\t\tconst auto setStars = [&](CreditsAmount amount) {\n\t\t\tunique->starsForResale = amount.whole();\n\t\t};\n\t\tconst auto setTon = [&](CreditsAmount amount) {\n\t\t\tunique->nanoTonForResale = amount.whole() * Ui::kNanosInOne\n\t\t\t\t+ amount.nano();\n\t\t};\n\t\tif (!price) {\n\t\t\tsetStars({});\n\t\t\tsetTon({});\n\t\t\tunique->onlyAcceptTon = false;\n\t\t} else if (price.ton()) {\n\t\t\tsetStars(StarsFromTon(session, price));\n\t\t\tsetTon(price);\n\t\t\tunique->onlyAcceptTon = true;\n\t\t} else {\n\t\t\tsetStars(price);\n\t\t\tsetTon(TonFromStars(session, price));\n\t\t\tunique->onlyAcceptTon = false;\n\t\t}\n\t\tsession->data().notifyGiftUpdate({\n\t\t\t.id = savedId,\n\t\t\t.slug = unique->slug,\n\t\t\t.action = Data::GiftUpdate::Action::ResaleChange,\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto earlyPrefix = u\"STARGIFT_RESELL_TOO_EARLY_\"_q;\n\t\tconst auto type = error.type();\n\t\tif (type.startsWith(earlyPrefix)) {\n\t\t\tconst auto seconds = type.mid(earlyPrefix.size()).toInt();\n\t\t\tconst auto newAvailableAt = base::unixtime::now() + seconds;\n\t\t\tunique->canResellAt = newAvailableAt;\n\t\t\tShowResaleGiftLater(show, unique);\n\t\t} else if (!ShowGiftErrorToast(show, error)) {\n\t\t\tshow->showToast(type);\n\t\t}\n\t}).send();\n}\n\nvoid UniqueGiftSellBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tstd::shared_ptr<Data::UniqueGift> unique,\n\t\tData::SavedStarGiftId savedId,\n\t\tint price,\n\t\tSettings::GiftWearBoxStyleOverride st) {\n\tconst auto session = &show->session();\n\tconst auto &appConfig = session->appConfig();\n\tconst auto starsMin = appConfig.giftResaleStarsMin();\n\tconst auto nanoTonMin = appConfig.giftResaleNanoTonMin();\n\tconst auto starsThousandths = appConfig.giftResaleStarsThousandths();\n\tconst auto nanoTonThousandths = appConfig.giftResaleNanoTonThousandths();\n\n\tstruct State {\n\t\trpl::variable<bool> onlyTon;\n\t\trpl::variable<CreditsAmount> price;\n\t\tFn<std::optional<CreditsAmount>()> computePrice;\n\t\trpl::event_stream<> errors;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tstate->onlyTon = unique->onlyAcceptTon;\n\tconst auto priceNow = Data::UniqueGiftResaleAsked(*unique);\n\tstate->price = priceNow\n\t\t? priceNow\n\t\t: price\n\t\t? CreditsAmount(price)\n\t\t: CreditsAmount(starsMin);\n\n\tbox->setTitle(rpl::conditional(\n\t\tstate->onlyTon.value(),\n\t\ttr::lng_gift_sell_title_ton(),\n\t\ttr::lng_gift_sell_title()));\n\tbox->setStyle(st.box ? *st.box : st::upgradeGiftBox);\n\tbox->setWidth(st::boxWideWidth);\n\n\tbox->addTopButton(st.close ? *st.close : st::boxTitleClose, [=] {\n\t\tbox->closeBox();\n\t});\n\tconst auto name = Data::UniqueGiftName(*unique);\n\tconst auto slug = unique->slug;\n\n\tconst auto container = box->verticalLayout();\n\tauto priceInput = HistoryView::AddStarsTonPriceInput(container, {\n\t\t.session = session,\n\t\t.showTon = state->onlyTon.value(),\n\t\t.price = state->price.current(),\n\t\t.starsMin = starsMin,\n\t\t.starsMax = appConfig.giftResaleStarsMax(),\n\t\t.nanoTonMin = nanoTonMin,\n\t\t.nanoTonMax = appConfig.giftResaleNanoTonMax(),\n\t\t.allowEmpty = true,\n\t});\n\tstate->price = std::move(priceInput.result);\n\tstate->computePrice = std::move(priceInput.computeResult);\n\tbox->setFocusCallback(std::move(priceInput.focusCallback));\n\n\tauto goods = rpl::merge(\n\t\trpl::single(rpl::empty) | rpl::map_to(true),\n\t\tstd::move(priceInput.updates) | rpl::map_to(true),\n\t\tstate->errors.events() | rpl::map_to(false)\n\t) | rpl::start_spawning(box->lifetime());\n\tauto text = rpl::duplicate(goods) | rpl::map([=](bool good) {\n\t\tconst auto value = state->computePrice();\n\t\tconst auto amount = value ? value->value() : 0.;\n\t\tconst auto tonMin = nanoTonMin / float64(Ui::kNanosInOne);\n\t\tconst auto enough = value\n\t\t\t&& (amount >= (value->ton() ? tonMin : starsMin));\n\t\tconst auto receive = !value\n\t\t\t? 0\n\t\t\t: value->ton()\n\t\t\t? ((amount * nanoTonThousandths) / 1000.)\n\t\t\t: ((int64(amount) * starsThousandths) / 1000);\n\t\tconst auto thousandths = state->onlyTon.current()\n\t\t\t? nanoTonThousandths\n\t\t\t: starsThousandths;\n\t\treturn (!good || !value)\n\t\t\t? (state->onlyTon.current()\n\t\t\t\t? tr::lng_gift_sell_min_price_ton(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tnanoTonMin / float64(Ui::kNanosInOne),\n\t\t\t\t\ttr::rich)\n\t\t\t\t: tr::lng_gift_sell_min_price(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tstarsMin,\n\t\t\t\t\ttr::rich))\n\t\t\t: enough\n\t\t\t? (value->ton()\n\t\t\t\t? tr::lng_gift_sell_amount_ton(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\treceive,\n\t\t\t\t\ttr::rich)\n\t\t\t\t: tr::lng_gift_sell_amount(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\treceive,\n\t\t\t\t\ttr::rich))\n\t\t\t: tr::lng_gift_sell_about(\n\t\t\t\ttr::now,\n\t\t\t\tlt_percent,\n\t\t\t\tTextWithEntities{ u\"%1%\"_q.arg(thousandths / 10.) },\n\t\t\t\ttr::rich);\n\t});\n\tconst auto details = box->addRow(object_ptr<Ui::FlatLabel>(\n\t\tbox,\n\t\tstd::move(text) | rpl::after_next([=] {\n\t\t\tbox->verticalLayout()->resizeToWidth(box->width());\n\t\t}),\n\t\tst::boxLabel));\n\n\tUi::AddSkip(container);\n\tUi::AddSkip(container);\n\tbox->addRow(object_ptr<Ui::PlainShadow>(box));\n\tUi::AddSkip(container);\n\tUi::AddSkip(container);\n\n\tconst auto onlyTon = box->addRow(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tbox,\n\t\t\ttr::lng_gift_sell_only_ton(tr::now),\n\t\t\tstate->onlyTon.current(),\n\t\t\tst::defaultCheckbox));\n\tstate->onlyTon = onlyTon->checkedValue();\n\n\tUi::AddSkip(container);\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_gift_sell_only_ton_about(tr::rich),\n\t\t\tst::boxDividerLabel));\n\tUi::AddSkip(container);\n\n\trpl::duplicate(goods) | rpl::on_next([=](bool good) {\n\t\tdetails->setTextColorOverride(\n\t\t\tgood ? st::windowSubTextFg->c : st::boxTextFgError->c);\n\t}, details->lifetime());\n\n\tconst auto submit = [=] {\n\t\tconst auto value = state->computePrice();\n\t\tif (!value) {\n\t\t\tstate->errors.fire({});\n\t\t\treturn;\n\t\t}\n\t\tbox->closeBox();\n\t\tUpdateGiftSellPrice(show, unique, savedId, *value);\n\t};\n\tstd::move(\n\t\tpriceInput.submits\n\t) | rpl::on_next(submit, details->lifetime());\n\tauto submitText = priceNow\n\t\t? tr::lng_gift_sell_update()\n\t\t: tr::lng_gift_sell_put();\n\tbox->addButton(std::move(submitText), submit);\n}\n\nvoid ShowUniqueGiftSellBox(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tstd::shared_ptr<Data::UniqueGift> unique,\n\t\tData::SavedStarGiftId savedId,\n\t\tSettings::GiftWearBoxStyleOverride st) {\n\tif (ShowResaleGiftLater(show, unique)) {\n\t\treturn;\n\t}\n\tconst auto session = &show->session();\n\tconst auto &title = unique->title;\n\tInvokeWithUniqueGiftResellPrice(session, title, [=](int price) {\n\t\tshow->show(Box(UniqueGiftSellBox, show, unique, savedId, price, st));\n\t});\n}\n\nvoid SendOfferBuyGift(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tstd::shared_ptr<Data::UniqueGift> unique,\n\t\tSuggestOptions options,\n\t\tint starsPerMessage,\n\t\tFn<void(bool)> done) {\n\tconst auto randomId = base::RandomValue<uint64>();\n\tconst auto owner = show->session().data().peer(unique->ownerId);\n\n\tusing Flag = MTPpayments_SendStarGiftOffer::Flag;\n\tshow->session().api().request(MTPpayments_SendStarGiftOffer(\n\t\tMTP_flags(starsPerMessage ? Flag::f_allow_paid_stars : Flag()),\n\t\towner->input(),\n\t\tMTP_string(unique->slug),\n\t\tStarsAmountToTL(options.price()),\n\t\tMTP_int(options.offerDuration),\n\t\tMTP_long(randomId),\n\t\tMTP_long(starsPerMessage)\n\t)).done([=](const MTPUpdates &result) {\n\t\tshow->session().api().applyUpdates(result);\n\t\tdone(true);\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto type = error.type();\n\t\tif (type == u\"\"_q) {\n\t\t} else if (!ShowGiftErrorToast(show, error)) {\n\t\t\tshow->showToast(type);\n\t\t}\n\t\tdone(false);\n\t}).send();\n}\n\nvoid ConfirmOfferBuyGift(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tstd::shared_ptr<Data::UniqueGift> unique,\n\t\tSuggestOptions options,\n\t\tFn<void()> done) {\n\tconst auto owner = show->session().data().peer(unique->ownerId);\n\tconst auto fee = owner->starsPerMessageChecked();\n\tconst auto price = options.price();\n\tconst auto sent = std::make_shared<bool>();\n\tconst auto send = [=](Fn<void()> close) {\n\t\tif (*sent) {\n\t\t\treturn;\n\t\t}\n\t\t*sent = true;\n\t\tSendOfferBuyGift(show, unique, options, fee, [=](bool ok) {\n\t\t\t*sent = false;\n\t\t\tif (ok) {\n\t\t\t\tif (const auto window = show->resolveWindow()) {\n\t\t\t\t\twindow->showPeerHistory(owner->id);\n\t\t\t\t}\n\t\t\t\tdone();\n\t\t\t\tclose();\n\t\t\t}\n\t\t});\n\t};\n\n\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = tr::lng_gift_offer_confirm_text(\n\t\t\t\ttr::now,\n\t\t\t\tlt_cost,\n\t\t\t\ttr::bold(PrepareCreditsAmountText(options.price())),\n\t\t\t\tlt_user,\n\t\t\t\ttr::bold(owner->shortName()),\n\t\t\t\tlt_name,\n\t\t\t\ttr::bold(Data::UniqueGiftName(*unique)),\n\t\t\t\ttr::marked),\n\t\t\t.confirmed = send,\n\t\t\t.confirmText = tr::lng_payments_pay_amount(\n\t\t\t\ttr::now,\n\t\t\t\tlt_amount,\n\t\t\t\tUi::Text::IconEmoji(price.ton()\n\t\t\t\t\t? &st::buttonTonIconEmoji\n\t\t\t\t\t: &st::buttonStarIconEmoji\n\t\t\t\t).append(Lang::FormatCreditsAmountDecimal(price.ton()\n\t\t\t\t\t? price\n\t\t\t\t\t: CreditsAmount(price.whole() + fee))),\n\t\t\t\ttr::marked),\n\t\t\t.title = tr::lng_gift_offer_confirm_title(),\n\t\t});\n\n\t\tauto helper = Ui::Text::CustomEmojiHelper();\n\t\tconst auto starIcon = helper.paletteDependent(\n\t\t\tUi::Earn::IconCreditsEmoji());\n\t\tconst auto tonIcon = helper.paletteDependent(\n\t\t\tUi::Earn::IconCurrencyEmoji());\n\t\tconst auto context = helper.context();\n\t\tconst auto table = box->addRow(\n\t\t\tobject_ptr<Ui::TableLayout>(box, st::defaultTable),\n\t\t\tst::boxPadding);\n\t\tconst auto add = [&](tr::phrase<> label, TextWithEntities value) {\n\t\t\ttable->addRow(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\ttable,\n\t\t\t\t\tlabel(),\n\t\t\t\t\tst::defaultTable.defaultLabel),\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\ttable,\n\t\t\t\t\trpl::single(value),\n\t\t\t\t\tst::defaultTable.defaultValue,\n\t\t\t\t\tst::defaultPopupMenu,\n\t\t\t\t\tcontext),\n\t\t\t\tst::giveawayGiftCodeLabelMargin,\n\t\t\t\tst::giveawayGiftCodeValueMargin);\n\t\t};\n\t\tadd(tr::lng_gift_offer_table_offer, tr::marked(price.ton()\n\t\t\t? tonIcon\n\t\t\t: starIcon).append(Lang::FormatCreditsAmountDecimal(price)));\n\t\tif (fee) {\n\t\t\tadd(tr::lng_gift_offer_table_fee, tr::marked(starIcon).append(\n\t\t\t\tLang::FormatCreditsAmountDecimal(CreditsAmount(fee))));\n\t\t}\n\t\tconst auto hours = options.offerDuration / 3600;\n\t\tconst auto duration = hours\n\t\t\t? tr::lng_hours(tr::now, lt_count, hours)\n\t\t\t: tr::lng_minutes(tr::now, lt_count, options.offerDuration / 60);\n\t\tadd(tr::lng_gift_offer_table_duration, tr::marked(duration));\n\t}));\n}\n\nvoid ShowOfferBuyBox(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tstd::shared_ptr<Data::UniqueGift> unique) {\n\tExpects(unique->starsMinOffer >= 0);\n\n\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tconst auto done = [=](SuggestOptions result) {\n\t\tConfirmOfferBuyGift(show, unique, result, [=] {\n\t\t\tif (const auto strong = weak->get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t});\n\t};\n\tusing namespace HistoryView;\n\tconst auto options = SuggestOptions{\n\t\t.exists = 1,\n\t\t.priceWhole = uint32(unique->starsMinOffer),\n\t};\n\tauto priceBox = Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{\n\t\t.peer = show->session().data().peer(unique->ownerId),\n\t\t.done = done,\n\t\t.value = options,\n\t\t.mode = SuggestMode::Gift,\n\t\t.giftName = UniqueGiftName(*unique),\n\t});\n\t*weak = priceBox.data();\n\tshow->show(std::move(priceBox));\n}\n\nstruct UpgradeArgs : StarGiftUpgradeArgs {\n\tstd::vector<Data::UniqueGiftModel> models;\n\tstd::vector<Data::UniqueGiftPattern> patterns;\n\tstd::vector<Data::UniqueGiftBackdrop> backdrops;\n\tstd::vector<UpgradePrice> prices;\n\tstd::vector<UpgradePrice> nextPrices;\n\tData::UniqueGiftAttributes all;\n};\n\n[[nodiscard]] rpl::producer<UniqueGiftCover> MakeUpgradeGiftStream(\n\t\tconst UpgradeArgs &args) {\n\tif (args.models.empty()\n\t\t|| args.patterns.empty()\n\t\t|| args.backdrops.empty()) {\n\t\treturn rpl::never<UniqueGiftCover>();\n\t}\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tstruct State {\n\t\t\tUpgradeArgs data;\n\t\t\tstd::vector<int> modelIndices;\n\t\t\tstd::vector<int> patternIndices;\n\t\t\tstd::vector<int> backdropIndices;\n\t\t};\n\t\tconst auto state = lifetime.make_state<State>(State{\n\t\t\t.data = args,\n\t\t});\n\n\t\tconst auto put = [=] {\n\t\t\tconst auto index = [](std::vector<int> &indices, const auto &v) {\n\t\t\t\tconst auto fill = [&] {\n\t\t\t\t\tif (!indices.empty()) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tindices = ranges::views::ints(0) | ranges::views::take(\n\t\t\t\t\t\tv.size()\n\t\t\t\t\t) | ranges::to_vector;\n\t\t\t\t\tranges::shuffle(indices);\n\t\t\t\t};\n\t\t\t\tfill();\n\t\t\t\tconst auto result = indices.back();\n\t\t\t\tindices.pop_back();\n\t\t\t\tfill();\n\t\t\t\tif (indices.back() == result) {\n\t\t\t\t\tstd::swap(indices.front(), indices.back());\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t};\n\t\t\tauto &models = state->data.models;\n\t\t\tauto &patterns = state->data.patterns;\n\t\t\tauto &backdrops = state->data.backdrops;\n\t\t\tconsumer.put_next(UniqueGiftCover{ Data::UniqueGift{\n\t\t\t\t.title = (state->data.savedId\n\t\t\t\t\t? tr::lng_gift_upgrade_title(tr::now)\n\t\t\t\t\t: tr::lng_gift_upgrade_preview_title(tr::now)),\n\t\t\t\t.model = models[index(state->modelIndices, models)],\n\t\t\t\t.pattern = patterns[index(state->patternIndices, patterns)],\n\t\t\t\t.backdrop = backdrops[index(state->backdropIndices, backdrops)],\n\t\t\t} });\n\t\t};\n\n\t\tput();\n\t\tbase::timer_each(\n\t\t\tkSwitchUpgradeCoverInterval / 3\n\t\t) | rpl::on_next(put, lifetime);\n\n\t\treturn lifetime;\n\t};\n}\n\nvoid AddUpgradeGiftCover(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<VerticalLayout*> container,\n\t\tconst UpgradeArgs &args,\n\t\tstd::shared_ptr<Data::GiftUpgradeSpinner> upgradeSpinner,\n\t\trpl::producer<std::shared_ptr<Data::GiftUpgradeResult>> upgraded) {\n\tstruct State {\n\t\trpl::variable<std::shared_ptr<Data::GiftUpgradeResult>> upgraded;\n\t};\n\tconst auto state = container->lifetime().make_state<State>();\n\tstate->upgraded = std::move(upgraded);\n\n\tauto resalePrice = rpl::single(\n\t\tCreditsAmount()\n\t) | rpl::then(state->upgraded.changes(\n\t) | rpl::map([=](std::shared_ptr<Data::GiftUpgradeResult> gift) {\n\t\treturn Settings::UniqueGiftResalePrice(gift->info.unique);\n\t}) | rpl::flatten_latest());\n\tauto resaleClick = [=] {\n\t\tif (const auto upgraded = state->upgraded.current()) {\n\t\t\tShowUniqueGiftSellBox(\n\t\t\t\tshow,\n\t\t\t\tupgraded->info.unique,\n\t\t\t\tupgraded->manageId,\n\t\t\t\t{});\n\t\t}\n\t};\n\tauto gifts = state->upgraded.value(\n\t) | rpl::map([=](std::shared_ptr<Data::GiftUpgradeResult> upgraded) {\n\t\treturn upgraded\n\t\t\t? rpl::single(Ui::UniqueGiftCover{\n\t\t\t\t.values = *upgraded->info.unique,\n\t\t\t\t.spinner = true,\n\t\t\t})\n\t\t\t: MakeUpgradeGiftStream(args);\n\t}) | rpl::flatten_latest();\n\n\tauto emoji = tr::marked();\n\tif (const auto &models = args.all.models; !models.empty()) {\n\t\tconst auto indices = RandomIndicesSubset(models.size(), 3);\n\t\tfor (const auto index : indices) {\n\t\t\tauto single = Data::SingleCustomEmoji(models[index].document);\n\t\t\tsingle.entities.front() = EntityInText(\n\t\t\t\tsingle.entities.front().type(),\n\t\t\t\tsingle.entities.front().offset(),\n\t\t\t\tsingle.entities.front().length(),\n\t\t\t\tu\"scaled-custom:\"_q + single.entities.front().data());\n\t\t\temoji.append(single).append(' ');\n\t\t}\n\t}\n\n\tauto subtitle = state->upgraded.value(\n\t) | rpl::map([=](std::shared_ptr<Data::GiftUpgradeResult> upgraded) {\n\t\treturn upgraded\n\t\t\t? rpl::single(tr::marked())\n\t\t\t: args.savedId\n\t\t\t? (emoji.empty()\n\t\t\t\t? tr::lng_gift_upgrade_about(tr::marked)\n\t\t\t\t: tr::lng_gift_upgrade_view_all(\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\trpl::single(emoji),\n\t\t\t\t\tlt_arrow,\n\t\t\t\t\trpl::single(Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\t\t\ttr::link))\n\t\t\t: (args.peer->isBroadcast()\n\t\t\t\t? tr::lng_gift_upgrade_preview_about_channel\n\t\t\t\t: tr::lng_gift_upgrade_preview_about)(\n\t\t\t\t\tlt_name,\n\t\t\t\t\trpl::single(tr::marked(args.peer->shortName())),\n\t\t\t\t\ttr::marked);\n\t}) | rpl::flatten_latest();\n\tconst auto hasAll = !args.all.models.empty();\n\tconst auto title = args.stargift.resellTitle;\n\tconst auto showAll = [=, list = args.all] {\n\t\tconst auto type = Data::GiftAttributeIdType::Model;\n\t\tconst auto null = nullptr;\n\t\tshow->show(Box(StarGiftPreviewBox, title, list, type, null));\n\t};\n\tauto numberText = state->upgraded.value(\n\t) | rpl::map([](const std::shared_ptr<Data::GiftUpgradeResult> &v) {\n\t\treturn (v && v->info.unique && v->info.unique->number > 0)\n\t\t\t? u\"#\"_q + Lang::FormatCountDecimal(v->info.unique->number)\n\t\t\t: QString();\n\t});\n\tAddUniqueGiftCover(container, std::move(gifts), {\n\t\t.numberText = std::move(numberText),\n\t\t.subtitle = std::move(subtitle),\n\t\t.subtitleClick = hasAll ? showAll : Fn<void()>(),\n\t\t.subtitleLinkColored = hasAll,\n\t\t.subtitleOutlined = hasAll,\n\t\t.resalePrice = std::move(resalePrice),\n\t\t.resaleClick = std::move(resaleClick),\n\t\t.upgradeSpinner = upgradeSpinner,\n\t});\n}\n\nData::CreditsHistoryEntry EntryForUpgradedGift(\n\t\tconst std::shared_ptr<Data::GiftUpgradeResult> &gift,\n\t\tuint64 nextToUpgradeStickerId,\n\t\tFn<void()> nextToUpgradeShow,\n\t\tFn<void()> craftAnother) {\n\tExpects(gift != nullptr);\n\n\tconst auto unique = gift->info.unique;\n\tconst auto chatGiftPeer = gift->manageId.chat();\n\tconst auto selfId = gift->info.document->session().userPeerId();\n\treturn {\n\t\t//.description = data.message,\n\t\t.date = base::unixtime::parse(gift->date),\n\t\t.credits = CreditsAmount(gift->info.stars),\n\t\t.bareMsgId = uint64(gift->manageId.userMessageId().bare),\n\t\t//.barePeerId = data.fromId.value,\n\t\t.bareGiftStickerId = gift->info.document->id,\n\t\t.bareGiftOwnerId = unique->ownerId.value,\n\t\t.bareGiftHostId = unique->hostId.value,\n\t\t//.bareActorId = data.fromId.value,\n\t\t.bareEntryOwnerId = chatGiftPeer ? chatGiftPeer->id.value : 0,\n\t\t.giftChannelSavedId = gift->manageId.chatSavedId(),\n\t\t.stargiftId = gift->info.id,\n\t\t.giftTitle = gift->info.resellTitle,\n\t\t.uniqueGift = unique,\n\t\t.nextToUpgradeStickerId = nextToUpgradeStickerId,\n\t\t.nextToUpgradeShow = nextToUpgradeShow,\n\t\t.craftAnotherCallback = craftAnother,\n\t\t.peerType = Data::CreditsHistoryEntry::PeerType::Peer,\n\t\t.limitedCount = gift->info.limitedCount,\n\t\t.limitedLeft = gift->info.limitedLeft,\n\t\t.starsToUpgrade = int(gift->info.starsToUpgrade),\n\t\t.starsForDetailsRemove = int(gift->starsForDetailsRemove),\n\t\t.giftNumber = unique->number,\n\t\t.converted = false,\n\t\t.stargift = true,\n\t\t.savedToProfile = gift->saved,\n\t\t.in = (unique->ownerId == selfId),\n\t\t.gift = true,\n\t};\n}\n\nvoid SwitchToUpgradedAnimation(\n\t\tnot_null<Window::SessionController*> window,\n\t\tstd::shared_ptr<Data::GiftUpgradeResult> gift,\n\t\tstd::shared_ptr<Data::GiftUpgradeSpinner> spinner,\n\t\tnot_null<GenericBox*> box,\n\t\tint contentStartIndex,\n\t\tstd::optional<Data::SavedStarGift> upgradeNext) {\n\tExpects(gift != nullptr);\n\tExpects(gift->info.unique != nullptr);\n\n\tauto oldContent = QImage();\n\tconst auto content = box->verticalLayout();\n\tconst auto oldContentTill = content->height();\n\tconst auto oldContentFrom = (content->count() > contentStartIndex)\n\t\t? content->widgetAt(contentStartIndex)->y()\n\t\t: oldContentTill;\n\tconst auto oldContentHeight = oldContentTill - oldContentFrom;\n\tif (oldContentHeight > 0) {\n\t\toldContent = GrabWidgetToImage(\n\t\t\tcontent,\n\t\t\tQRect(0, oldContentFrom, content->width(), oldContentHeight),\n\t\t\tst::giveawayGiftCodeBox.bg->c);\n\t}\n\n\tbox->setStyle(st::giveawayGiftCodeBox);\n\tbox->clearButtons();\n\tbox->addButton(tr::lng_create_group_skip(), [=] {\n\t\tbox->closeBox();\n\t});\n\n\twhile (content->count() > contentStartIndex) {\n\t\tdelete content->widgetAt(contentStartIndex);\n\t}\n\n\tconst auto show = window->uiShow();\n\tconst auto unique = gift->info.unique;\n\tconst auto nextToUpgradeStickerId = upgradeNext\n\t\t? upgradeNext->info.document->id\n\t\t: uint64();\n\tconst auto nextToUpgradeShow = upgradeNext\n\t\t? [=] {\n\t\t\tSettings::ShowSavedStarGiftBox(\n\t\t\t\twindow,\n\t\t\t\twindow->session().data().peer(unique->ownerId),\n\t\t\t\t*upgradeNext);\n\t\t}\n\t\t: Fn<void()>();\n\tconst auto entry = EntryForUpgradedGift(\n\t\tgift,\n\t\tnextToUpgradeStickerId,\n\t\tnextToUpgradeShow);\n\tSettings::GenericCreditsEntryBody(box, show, entry, {}, spinner);\n\n\tcontent->resizeToWidth(st::boxWideWidth);\n\n\tconst auto nowContentTill = content->height();\n\tconst auto nowContentFrom = (content->count() > contentStartIndex)\n\t\t? content->widgetAt(contentStartIndex)->y()\n\t\t: nowContentTill;\n\tif (const auto height = nowContentTill - nowContentFrom; height > 0) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto fadeOutSize = QSize(content->width(), height);\n\t\tauto fadeOutImage = QImage(\n\t\t\tfadeOutSize * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tfadeOutImage.fill(st::giveawayGiftCodeBox.bg->c);\n\t\tif (oldContentHeight > 0) {\n\t\t\tQPainter p(&fadeOutImage);\n\t\t\tp.drawImage(0, 0, oldContent);\n\t\t}\n\t\tstruct FadeOut {\n\t\t\tRpWidget *widget = nullptr;\n\t\t\tAnimations::Simple animation;\n\t\t};\n\t\tconst auto fadeOut = content->lifetime().make_state<FadeOut>();\n\t\tfadeOut->widget = CreateChild<RpWidget>(content);\n\t\tfadeOut->widget->show();\n\t\tfadeOut->widget->setGeometry(\n\t\t\t{ 0, nowContentFrom, content->width(), height });\n\t\tfadeOut->widget->paintOn([=](QPainter &p) {\n\t\t\tif (!fadeOut->animation.animating()) {\n\t\t\t\tcrl::on_main(fadeOut->widget, [widget = fadeOut->widget] {\n\t\t\t\t\tdelete widget;\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto progress = fadeOut->animation.value(1.);\n\t\t\tp.setOpacity(1. - progress);\n\n\t\t\tconst auto shift = int(base::SafeRound(\n\t\t\t\tst::defaultVerticalListSkip * progress));\n\t\t\tif (shift > 0) {\n\t\t\t\tp.fillRect(\n\t\t\t\t\tQRect{ 0, 0, content->width(), shift },\n\t\t\t\t\tst::giveawayGiftCodeBox.bg);\n\t\t\t}\n\t\t\tp.drawImage(0, shift, fadeOutImage);\n\t\t});\n\t\tfadeOut->animation.start([=] {\n\t\t\tfadeOut->widget->update();\n\t\t}, 0., 1., st::slideWrapDuration);\n\t}\n}\n\nclass UpgradePriceValue final {\npublic:\n\tUpgradePriceValue(\n\t\tnot_null<Main::Session*> session,\n\t\tuint64 stargiftId,\n\t\tint cost,\n\t\tstd::vector<UpgradePrice> all,\n\t\tstd::vector<UpgradePrice> next);\n\n\t[[nodiscard]] int cost() const;\n\t[[nodiscard]] rpl::producer<int> costValue() const;\n\t[[nodiscard]] rpl::producer<TimeId> tillNextValue() const;\n\nprivate:\n\tvoid update();\n\tvoid refresh();\n\n\tMTP::Sender _api;\n\tuint64 _stargiftId = 0;\n\trpl::variable<int> _cost;\n\trpl::variable<TimeId> _tillNext;\n\tstd::vector<UpgradePrice> _all;\n\tstd::vector<UpgradePrice> _next;\n\tbase::Timer _timer;\n\tbool _refreshing = false;\n\tbool _finished = false;\n\n};\n\nUpgradePriceValue::UpgradePriceValue(\n\tnot_null<Main::Session*> session,\n\tuint64 stargiftId,\n\tint cost,\n\tstd::vector<UpgradePrice> all,\n\tstd::vector<UpgradePrice> next)\n: _api(&session->mtp())\n, _stargiftId(stargiftId)\n, _cost(cost)\n, _all(std::move(all))\n, _next(std::move(next))\n, _timer([=] { update(); })\n, _finished(_next.size() < 2) {\n\tupdate();\n\t_timer.callEach(1000);\n}\n\nint UpgradePriceValue::cost() const {\n\treturn _cost.current();\n}\n\nrpl::producer<int> UpgradePriceValue::costValue() const {\n\treturn _cost.value();\n}\n\nrpl::producer<TimeId> UpgradePriceValue::tillNextValue() const {\n\treturn _tillNext.value();\n}\n\nvoid UpgradePriceValue::update() {\n\tif (_all.empty() || _next.empty()) {\n\t\t_timer.cancel();\n\t\treturn;\n\t}\n\tconst auto now = base::unixtime::now();\n\tconst auto i = ranges::upper_bound(\n\t\t_all,\n\t\tnow,\n\t\tranges::less(),\n\t\t&UpgradePrice::date);\n\tconst auto j = ranges::upper_bound(\n\t\t_next,\n\t\tnow,\n\t\tranges::less(),\n\t\t&UpgradePrice::date);\n\tconst auto full = (i == begin(_all)) ? i->stars : (i - 1)->stars;\n\tconst auto part = (j == begin(_next)) ? j->stars : (j - 1)->stars;\n\tconst auto fullDate = (i != end(_all)) ? i->date : TimeId();\n\tconst auto partDate = (j != end(_next)) ? j->date : TimeId();\n\t_cost = std::min({ _cost.current(), part, full});\n\tif (int(end(_next) - j) < 3) {\n\t\trefresh();\n\t}\n\n\tconst auto next = std::min({\n\t\tpartDate ? partDate : TimeId(INT_MAX),\n\t\tfullDate ? fullDate : TimeId(INT_MAX),\n\t});\n\tif (next != TimeId(INT_MAX)) {\n\t\t_tillNext = next - now;\n\t} else {\n\t\t_tillNext = 0;\n\t\t_timer.cancel();\n\t}\n}\n\nvoid UpgradePriceValue::refresh() {\n\tif (_refreshing || _finished) {\n\t\treturn;\n\t}\n\t_api.request(MTPpayments_GetStarGiftUpgradePreview(\n\t\tMTP_long(_stargiftId)\n\t)).done([=](const MTPpayments_StarGiftUpgradePreview &result) {\n\t\tconst auto &data = result.data();\n\t\t_all = ParsePrices(data.vprices());\n\t\t_next = ParsePrices(data.vnext_prices());\n\t}).fail([=] {\n\t\t_finished = true;\n\t}).send();\n}\n\nvoid PricesBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst std::vector<UpgradePrice> &list,\n\t\trpl::producer<int> cost) {\n\tconst auto top = box->setPinnedToTopContent(\n\t\tobject_ptr<Ui::VerticalLayout>(box));\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setStyle(st::boostBox);\n\n\tstruct State {\n\t\trpl::variable<int> cost;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tstate->cost = std::move(cost);\n\tconst auto min = list.back().stars;\n\tconst auto max = std::max(min + 2, list.front().stars);\n\tconst auto from = 0;\n\tconst auto till = max - min;\n\n\tconst auto addSkip = [&](int skip) {\n\t\ttop->add(object_ptr<Ui::FixedHeightWidget>(top, skip));\n\t};\n\n\tconst auto ratio = [=](int current) {\n\t\tcurrent = std::clamp(current, from, till);\n\t\tconst auto count = (till - from);\n\t\tconst auto index = (current - from);\n\t\tif (count <= 2) {\n\t\t\treturn 0.5;\n\t\t}\n\t\tconst auto available = st::boxWideWidth\n\t\t\t- st::boxPadding.left()\n\t\t\t- st::boxPadding.right();\n\t\tconst auto average = available / float64(count);\n\t\tconst auto levelWidth = [&](int stars) {\n\t\t\treturn st::normalFont->width(Lang::FormatCountDecimal(stars));\n\t\t};\n\t\tconst auto paddings = 2 * st::premiumLineTextSkip;\n\t\tconst auto labelLeftWidth = paddings + levelWidth(min);\n\t\tconst auto labelRightWidth = paddings + levelWidth(max);\n\t\tconst auto first = std::max(average, labelLeftWidth * 1.);\n\t\tconst auto last = std::max(average, labelRightWidth * 1.);\n\t\tconst auto other = (available - first - last) / (count - 2);\n\t\treturn (first + (index - 1) * other) / available;\n\t};\n\n\tauto bubbleRowState = state->cost.value(\n\t) | rpl::map([=](int value) {\n\t\treturn Premium::BubbleRowState{\n\t\t\t.counter = max - value,\n\t\t\t.ratio = ratio(max - value),\n\t\t\t.dynamic = true,\n\t\t};\n\t});\n\tPremium::AddBubbleRow(\n\t\ttop,\n\t\tst::starRatingBubble,\n\t\tbox->showFinishes(),\n\t\trpl::duplicate(bubbleRowState),\n\t\tUi::Premium::BubbleType::StarRating,\n\t\t[=](int value) {\n\t\t\treturn Premium::BubbleText{\n\t\t\t\t.counter = Lang::FormatCountDecimal(max - value),\n\t\t\t};\n\t\t},\n\t\t&st::paidReactBubbleIcon,\n\t\tst::boxRowPadding);\n\taddSkip(st::premiumLineTextSkip);\n\n\tauto limitState = std::move(\n\t\tbubbleRowState\n\t) | rpl::map([=](const Premium::BubbleRowState &state) {\n\t\treturn Premium::LimitRowState{\n\t\t\t.ratio = state.ratio,\n\t\t\t.dynamic = state.dynamic\n\t\t};\n\t});\n\tauto left = rpl::single(Lang::FormatCountDecimal(max));\n\tauto right = rpl::single(Lang::FormatCountDecimal(min));\n\tPremium::AddLimitRow(\n\t\ttop,\n\t\tst::upgradePriceLimits,\n\t\tPremium::LimitRowLabels{\n\t\t\t.leftLabel = std::move(left),\n\t\t\t.rightLabel = std::move(right),\n\t\t\t.activeLineBg = [=] { return st::windowBgActive->b; },\n\t\t},\n\t\tstd::move(limitState),\n\t\tst::boxRowPadding);\n\n\ttop->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\ttop,\n\t\t\ttr::lng_gift_upgrade_prices_title(),\n\t\t\tst::infoStarsTitle),\n\t\tst::boxRowPadding + QMargins(0, st::boostTitleSkip / 2, 0, 0),\n\t\tstyle::al_top);\n\ttop->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\ttop,\n\t\t\ttr::lng_gift_upgrade_prices_subtitle(),\n\t\t\tst::boostText),\n\t\t(st::boxRowPadding\n\t\t\t+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)),\n\t\tstyle::al_top\n\t)->setTryMakeSimilarLines(true);\n\n\tauto helper = Ui::Text::CustomEmojiHelper();\n\tconst auto creditsIcon = helper.paletteDependent(\n\t\tUi::Earn::IconCreditsEmoji());\n\tconst auto context = helper.context();\n\tconst auto table = box->addRow(\n\t\tobject_ptr<Ui::TableLayout>(box, st::defaultTable),\n\t\tst::boxPadding);\n\tfor (const auto &price : list) {\n\t\tconst auto parsed = base::unixtime::parse(price.date);\n\t\tconst auto time = QLocale().toString(\n\t\t\tparsed.time(),\n\t\t\tQLocale::ShortFormat);\n\t\tconst auto date = tr::lng_month_day(\n\t\t\ttr::now,\n\t\t\tlt_month,\n\t\t\tLang::MonthSmall(parsed.date().month())(tr::now),\n\t\t\tlt_day,\n\t\t\tQString::number(parsed.date().day()));\n\t\ttable->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\ttable,\n\t\t\t\ttime + u\", \"_q + date,\n\t\t\t\tst::defaultTable.defaultLabel),\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\ttable,\n\t\t\t\trpl::single(\n\t\t\t\t\tbase::duplicate(creditsIcon).append(' ').append(\n\t\t\t\t\t\tLang::FormatCountDecimal(price.stars))),\n\t\t\t\tst::defaultTable.defaultValue,\n\t\t\t\tst::defaultPopupMenu,\n\t\t\t\tcontext),\n\t\t\tst::giveawayGiftCodeLabelMargin,\n\t\t\tst::giveawayGiftCodeValueMargin);\n\t}\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_gift_upgrade_prices_about(),\n\t\t\tst::resalePriceAbout),\n\t\tstyle::al_top);\n\tbox->addSkip(st::boxPadding.top());\n\n\tbox->setMaxHeight(st::boxWideWidth);\n\n\tbox->addButton(rpl::single(QString()), [=] {\n\t\tbox->closeBox();\n\t})->setText(rpl::single(Ui::Text::IconEmoji(\n\t\t&st::infoStarsUnderstood\n\t).append(' ').append(tr::lng_stars_rating_understood(tr::now))));\n}\n\nvoid UpgradeBox(\n\t\tnot_null<GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tUpgradeArgs &&args) {\n\tstruct State {\n\t\tbase::Timer timer;\n\t\tstd::unique_ptr<UpgradePriceValue> cost;\n\t\trpl::variable<bool> upgrading = false;\n\t\trpl::event_stream<std::shared_ptr<Data::GiftUpgradeResult>> upgraded;\n\t\tUi::FlatLabel *pricesLink = nullptr;\n\t\tbool preserveDetails = false;\n\t\tbool sent = false;\n\t};\n\tconst auto state = std::make_shared<State>();\n\n\tbox->setNoContentMargin(true);\n\tbox->setWidth(st::boxWideWidth);\n\n\tconst auto show = controller->uiShow();\n\tconst auto container = box->verticalLayout();\n\tconst auto spinner = std::make_shared<Data::GiftUpgradeSpinner>();\n\tspinner->attributes = Data::UniqueGiftAttributes{\n\t\t.models = args.models,\n\t\t.backdrops = args.backdrops,\n\t\t.patterns = args.patterns,\n\t};\n\n\tAddUpgradeGiftCover(\n\t\tshow,\n\t\tcontainer,\n\t\targs,\n\t\tspinner,\n\t\tstate->upgraded.events());\n\tconst auto contentIndex = container->count();\n\n\tAddSkip(container, st::defaultVerticalListSkip * 2);\n\n\tconst auto features = std::vector<Ui::FeatureListEntry>{\n\t\t{\n\t\t\tst::menuIconUnique,\n\t\t\ttr::lng_gift_upgrade_unique_title(tr::now),\n\t\t\t(args.savedId\n\t\t\t\t? tr::lng_gift_upgrade_unique_about(tr::now, tr::marked)\n\t\t\t\t: (args.peer->isBroadcast()\n\t\t\t\t\t? tr::lng_gift_upgrade_unique_about_channel\n\t\t\t\t\t: tr::lng_gift_upgrade_unique_about_user)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\ttr::marked(args.peer->shortName()),\n\t\t\t\t\t\ttr::marked)),\n\t\t},\n\t\t{\n\t\t\tst::menuIconReplace,\n\t\t\ttr::lng_gift_upgrade_transferable_title(tr::now),\n\t\t\t(args.savedId\n\t\t\t\t? tr::lng_gift_upgrade_transferable_about(\n\t\t\t\t\ttr::now,\n\t\t\t\t\ttr::marked)\n\t\t\t\t: (args.peer->isBroadcast()\n\t\t\t\t\t? tr::lng_gift_upgrade_transferable_about_channel\n\t\t\t\t\t: tr::lng_gift_upgrade_transferable_about_user)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\ttr::marked(args.peer->shortName()),\n\t\t\t\t\t\ttr::marked)),\n\t\t},\n\t\t{\n\t\t\tst::menuIconTradable,\n\t\t\ttr::lng_gift_upgrade_tradable_title(tr::now),\n\t\t\t(args.savedId\n\t\t\t\t? tr::lng_gift_upgrade_tradable_about(tr::now, tr::marked)\n\t\t\t\t: (args.peer->isBroadcast()\n\t\t\t\t\t? tr::lng_gift_upgrade_tradable_about_channel\n\t\t\t\t\t: tr::lng_gift_upgrade_tradable_about_user)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\ttr::marked(args.peer->shortName()),\n\t\t\t\t\t\ttr::marked)),\n\t\t},\n\t};\n\tfor (const auto &feature : features) {\n\t\tcontainer->add(\n\t\t\tUi::MakeFeatureListEntry(container, feature),\n\t\t\tst::boxRowPadding);\n\t}\n\n\tconst auto gifting = !args.savedId\n\t\t&& !args.giftPrepayUpgradeHash.isEmpty();\n\tconst auto preview = !args.savedId && !gifting;\n\tconst auto showPrices = !preview\n\t\t&& (args.cost > 0)\n\t\t&& (args.prices.size() > 1)\n\t\t&& (args.nextPrices.size() > 1);\n\tauto prices = args.prices;\n\tstate->cost = std::make_unique<UpgradePriceValue>(\n\t\t&controller->session(),\n\t\targs.stargift.id,\n\t\targs.cost,\n\t\tstd::move(args.prices),\n\t\tstd::move(args.nextPrices));\n\tif (!preview && !gifting) {\n\t\tconst auto skip = st::defaultVerticalListSkip;\n\t\tcontainer->add(\n\t\t\tobject_ptr<PlainShadow>(container),\n\t\t\tst::boxRowPadding + QMargins(0, skip, 0, skip));\n\t\tconst auto checkbox = container->add(\n\t\t\tobject_ptr<Checkbox>(\n\t\t\t\tcontainer,\n\t\t\t\t(args.canAddComment\n\t\t\t\t\t? tr::lng_gift_upgrade_add_comment(tr::now)\n\t\t\t\t\t: args.canAddSender\n\t\t\t\t\t? tr::lng_gift_upgrade_add_sender(tr::now)\n\t\t\t\t\t: args.canAddMyComment\n\t\t\t\t\t? tr::lng_gift_upgrade_add_my_comment(tr::now)\n\t\t\t\t\t: tr::lng_gift_upgrade_add_my(tr::now)),\n\t\t\t\targs.addDetailsDefault),\n\t\t\tst::defaultCheckbox.margin,\n\t\t\tstyle::al_top);\n\t\tcheckbox->checkedValue() | rpl::on_next([=](bool checked) {\n\t\t\tstate->preserveDetails = checked;\n\t\t}, checkbox->lifetime());\n\t}\n\n\tbox->setStyle(preview\n\t\t? st::giftBox\n\t\t: showPrices\n\t\t? st::upgradeGiftWithPricesBox\n\t\t: st::upgradeGiftBox);\n\tif (gifting) {\n\t\tbox->setWidth(st::boxWideWidth);\n\t}\n\tauto buttonText = preview ? tr::lng_box_ok() : rpl::single(QString());\n\tconst auto button = box->addButton(std::move(buttonText), [=] {\n\t\tif (preview) {\n\t\t\tbox->closeBox();\n\t\t\treturn;\n\t\t} else if (state->sent) {\n\t\t\treturn;\n\t\t}\n\t\tstate->sent = true;\n\t\tconst auto weakBox = base::make_weak(box);\n\t\tconst auto session = &controller->session();\n\t\tconst auto weakWindow = base::make_weak(controller);\n\t\tconst auto cost = state->cost->cost();\n\t\tconst auto keepDetails = state->preserveDetails;\n\t\tif (gifting) {\n\t\t\tconst auto done = [=](bool success) {\n\t\t\t\tstate->upgrading = false;\n\t\t\t\tif (!success) {\n\t\t\t\t\tstate->sent = false;\n\t\t\t\t} else {\n\t\t\t\t\tif (const auto window = weakWindow.get()) {\n\t\t\t\t\t\twindow->showPeerHistory(args.peer);\n\t\t\t\t\t}\n\t\t\t\t\tif (const auto strong = weakBox.get()) {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\tGiftUpgrade(\n\t\t\t\tcontroller,\n\t\t\t\targs.peer,\n\t\t\t\targs.giftPrepayUpgradeHash,\n\t\t\t\tcost,\n\t\t\t\tdone);\n\t\t} else {\n\t\t\tconst auto done = [=](\n\t\t\t\t\tbool success,\n\t\t\t\t\tstd::shared_ptr<Data::GiftUpgradeResult> gift) {\n\t\t\t\tconst auto ownerId = (gift && gift->info.unique)\n\t\t\t\t\t? gift->info.unique->ownerId\n\t\t\t\t\t: PeerId();\n\t\t\t\tconst auto owner = ownerId\n\t\t\t\t\t? session->data().peer(ownerId).get()\n\t\t\t\t\t: nullptr;\n\t\t\t\tif (!success) {\n\t\t\t\t\tif (weakBox) {\n\t\t\t\t\t\tstate->upgrading = false;\n\t\t\t\t\t\tstate->sent = false;\n\t\t\t\t\t}\n\t\t\t\t} else if (owner) {\n\t\t\t\t\tusing State = Data::GiftUpgradeSpinner::State;\n\t\t\t\t\tif (const auto onstack = args.upgraded) {\n\t\t\t\t\t\tonstack();\n\t\t\t\t\t}\n\t\t\t\t\tif (weakBox.get()) {\n\t\t\t\t\t\tspinner->target = gift->info.unique;\n\t\t\t\t\t\tspinner->state = State::Preparing;\n\t\t\t\t\t}\n\t\t\t\t\tif (weakWindow.get()) {\n\t\t\t\t\t\tsession->data().nextForUpgradeGiftRequest(\n\t\t\t\t\t\t\towner,\n\t\t\t\t\t\t\t[=](std::optional<Data::SavedStarGift> next) {\n\t\t\t\t\t\t\t\tif (const auto box = weakBox.get()) {\n\t\t\t\t\t\t\t\t\tspinner->state.value() | rpl::filter(\n\t\t\t\t\t\t\t\t\t\trpl::mappers::_1 == State::Prepared\n\t\t\t\t\t\t\t\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t\t\t\t\t\t\t\tstate->upgrading = false;\n\t\t\t\t\t\t\t\t\t\tstate->upgraded.fire_copy(gift);\n\n\t\t\t\t\t\t\t\t\t\tdelete base::take(state->pricesLink);\n\t\t\t\t\t\t\t\t\t\tSwitchToUpgradedAnimation(\n\t\t\t\t\t\t\t\t\t\t\tweakWindow.get(),\n\t\t\t\t\t\t\t\t\t\t\tgift,\n\t\t\t\t\t\t\t\t\t\t\tspinner,\n\t\t\t\t\t\t\t\t\t\t\tbox,\n\t\t\t\t\t\t\t\t\t\t\tcontentIndex,\n\t\t\t\t\t\t\t\t\t\t\tnext);\n\t\t\t\t\t\t\t\t\t}, box->lifetime());\n\t\t\t\t\t\t\t\t} else if (const auto w = weakWindow.get()) {\n\t\t\t\t\t\t\t\t\tShowGiftUpgradedToast(\n\t\t\t\t\t\t\t\t\t\tw,\n\t\t\t\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\t\t\t\tgift->info.unique);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif (const auto window = weakWindow.get()) {\n\t\t\t\t\t\twindow->showPeerHistory(args.peer);\n\t\t\t\t\t}\n\t\t\t\t\tif (const auto strong = weakBox.get()) {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\tstate->upgrading = true;\n\t\t\tUpgradeGift(controller, args.savedId, keepDetails, cost, done);\n\n\t\t\t//AssertIsDebug();\n\t\t\t//const auto modelIndex = base::RandomIndex(args.models.size());\n\t\t\t//const auto patternIndex = base::RandomIndex(args.patterns.size());\n\t\t\t//const auto backdropIndex = base::RandomIndex(args.backdrops.size());\n\t\t\t//const auto result = std::make_shared<Data::GiftUpgradeResult>(Data::GiftUpgradeResult{\n\t\t\t//\t.info = args.stargift,\n\t\t\t//\t.manageId = Data::SavedStarGiftId::User(123),\n\t\t\t//\t.date = base::unixtime::now(),\n\t\t\t//\t.starsForDetailsRemove = 119,\n\t\t\t//\t.saved = true,\n\t\t\t//});\n\t\t\t//result->info.unique = std::make_shared<Data::UniqueGift>(Data::UniqueGift{\n\t\t\t//\t.id = 1,\n\t\t\t//\t.initialGiftId = args.stargift.id,\n\t\t\t//\t.slug = u\"hello_world\"_q,\n\t\t\t//\t.title = u\"Shard Test\"_q,\n\t\t\t//\t.ownerId = session->userPeerId(),\n\t\t\t//\t.releasedBy = show->session().user(),\n\t\t\t//\t.model = args.models[modelIndex],\n\t\t\t//\t.pattern = args.patterns[patternIndex],\n\t\t\t//\t.backdrop = args.backdrops[backdropIndex],\n\t\t\t//});\n\t\t\t//base::call_delayed(3000, [=] {\n\t\t\t//\tdone(true, result);\n\t\t\t//});\n\t\t}\n\t});\n\tif (!preview) {\n\t\tusing namespace Info::Statistics;\n\t\tconst auto loadingAnimation = InfiniteRadialAnimationWidget(\n\t\t\tbutton,\n\t\t\tst::giveawayGiftCodeStartButton.height / 2);\n\t\tAddChildToWidgetCenter(button.data(), loadingAnimation);\n\t\tloadingAnimation->showOn(state->upgrading.value());\n\n\t\tauto costText = [=] {\n\t\t\treturn state->cost->costValue(\n\t\t\t) | rpl::map([](int cost) {\n\t\t\t\tif (!cost) {\n\t\t\t\t\treturn tr::lng_gift_upgrade_confirm(\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t}\n\t\t\t\treturn tr::lng_gift_upgrade_button(\n\t\t\t\t\tlt_price,\n\t\t\t\t\trpl::single(Ui::Text::IconEmoji(\n\t\t\t\t\t\t&st::starIconEmoji\n\t\t\t\t\t).append(Lang::FormatCreditsAmountDecimal(\n\t\t\t\t\t\tCreditsAmount{ cost }))),\n\t\t\t\t\ttr::marked);\n\t\t\t}) | rpl::flatten_latest();\n\t\t};\n\t\tauto subtext = state->cost->tillNextValue(\n\t\t) | rpl::map([](TimeId left) {\n\t\t\tif (!left) {\n\t\t\t\treturn QString();\n\t\t\t}\n\t\t\tconst auto hours = left / 3600;\n\t\t\tconst auto minutes = (left % 3600) / 60;\n\t\t\tconst auto seconds = left % 60;\n\t\t\tconst auto padded = [](int value) {\n\t\t\t\treturn u\"%1\"_q.arg(value, 2, 10, QChar('0'));\n\t\t\t};\n\t\t\tconst auto text = hours\n\t\t\t\t? u\"%1:%2:%3\"_q\n\t\t\t\t.arg(hours).arg(padded(minutes)).arg(padded(seconds))\n\t\t\t\t: u\"%2:%3\"_q.arg(padded(minutes)).arg(padded(seconds));\n\t\t\treturn tr::lng_gift_upgrade_decreases(tr::now, lt_time, text);\n\t\t}) | rpl::map(tr::marked);\n\t\tUi::SetButtonTwoLabels(\n\t\t\tbutton,\n\t\t\trpl::conditional(\n\t\t\t\tstate->upgrading.value(),\n\t\t\t\trpl::single(tr::marked()),\n\t\t\t\tcostText()),\n\t\t\trpl::conditional(\n\t\t\t\t(showPrices\n\t\t\t\t\t? state->upgrading.value()\n\t\t\t\t\t: (rpl::single(true) | rpl::type_erased)),\n\t\t\t\trpl::single(tr::marked()),\n\t\t\t\tstd::move(subtext)),\n\t\t\tst::resaleButtonTitle,\n\t\t\tst::resaleButtonSubtitle);\n\t}\n\tif (showPrices) {\n\t\tconst auto link = state->pricesLink = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tbutton->parentWidget(),\n\t\t\ttr::lng_gift_upgrade_see_table(\n\t\t\t\tlt_arrow,\n\t\t\t\trpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\t\t[](QString text) { return tr::link(text); }),\n\t\t\tst::resalePriceTableLink);\n\t\tlink->setTryMakeSimilarLines(true);\n\t\tbutton->geometryValue() | rpl::on_next([=](QRect geometry) {\n\t\t\tconst auto outer = button->parentWidget()->height();\n\t\t\tconst auto top = geometry.y() + geometry.height();\n\t\t\tconst auto available = outer - top - st::boxRadius;\n\t\t\tlink->resizeToWidth(geometry.width());\n\t\t\tlink->move(geometry.x(), top + (available - link->height()) / 2);\n\t\t}, link->lifetime());\n\t\tlink->setClickHandlerFilter([=, list = prices](const auto &...) {\n\t\t\tcontroller->show(Box(PricesBox, list, state->cost->costValue()));\n\t\t\treturn false;\n\t\t});\n\t}\n\n\tUi::AddUniqueCloseButton(box);\n\n\tspinner->state.value() | rpl::filter(\n\t\trpl::mappers::_1 == Data::GiftUpgradeSpinner::State::Finished\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\tUi::StartFireworks(box->parentWidget());\n\t}, box->lifetime());\n}\n\nvoid GetVariantsAndShowUpgradeBox(UpgradeArgs &&args) {\n\tconst auto weak = base::make_weak(args.controller);\n\tconst auto session = &args.peer->session();\n\tconst auto auctions = &session->giftAuctions();\n\tconst auto guard = base::make_weak(args.controller);\n\tconst auto done = crl::guard(args.controller, [](UpgradeArgs &&args) {\n\t\tconst auto onstack = args.ready;\n\t\tconst auto window = args.controller;\n\t\twindow->show(Box(UpgradeBox, window, std::move(args)));\n\t\tif (onstack) {\n\t\t\tonstack(true);\n\t\t}\n\t});\n\tconst auto giftId = args.stargift.id;\n\tif (auto attributes = auctions->attributes(args.stargift.id)) {\n\t\targs.all = std::move(*attributes);\n\t\tdone(std::move(args));\n\t} else {\n\t\tauctions->requestAttributes(giftId, crl::guard(guard, [=] {\n\t\t\tauto copy = args;\n\t\t\tcopy.all = std::move(*auctions->attributes(giftId));\n\t\t\tdone(std::move(copy));\n\t\t}));\n\t}\n}\n\nvoid ShowStarGiftUpgradeBox(StarGiftUpgradeArgs &&args) {\n\tconst auto weak = base::make_weak(args.controller);\n\tconst auto session = &args.peer->session();\n\tsession->api().request(MTPpayments_GetStarGiftUpgradePreview(\n\t\tMTP_long(args.stargift.id)\n\t)).done([=](const MTPpayments_StarGiftUpgradePreview &result) {\n\t\tconst auto strong = weak.get();\n\t\tif (!strong) {\n\t\t\tif (const auto onstack = args.ready) {\n\t\t\t\tonstack(false);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst auto &data = result.data();\n\t\tauto upgrade = UpgradeArgs{ args };\n\t\tupgrade.prices = ParsePrices(data.vprices());\n\t\tupgrade.nextPrices = ParsePrices(data.vnext_prices());\n\t\tfor (const auto &attribute : data.vsample_attributes().v) {\n\t\t\tattribute.match([&](const MTPDstarGiftAttributeModel &data) {\n\t\t\t\tupgrade.models.push_back(Api::FromTL(session, data));\n\t\t\t}, [&](const MTPDstarGiftAttributePattern &data) {\n\t\t\t\tupgrade.patterns.push_back(Api::FromTL(session, data));\n\t\t\t}, [&](const MTPDstarGiftAttributeBackdrop &data) {\n\t\t\t\tupgrade.backdrops.push_back(Api::FromTL(data));\n\t\t\t}, [](const auto &) {});\n\t\t}\n\t\tGetVariantsAndShowUpgradeBox(std::move(upgrade));\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->showToast(error.type());\n\t\t}\n\t\tif (const auto onstack = args.ready) {\n\t\t\tonstack(false);\n\t\t}\n\t}).send();\n}\n\nvoid SubmitStarsForm(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tMTPInputInvoice invoice,\n\t\tuint64 formId,\n\t\tuint64 price,\n\t\tFn<void(Payments::CheckoutResult, const MTPUpdates *)> done) {\n\tconst auto ready = [=](Settings::SmallBalanceResult result) {\n\t\tSendStarsFormRequest(show, result, formId, invoice, done);\n\t};\n\tSettings::MaybeRequestBalanceIncrease(\n\t\tshow,\n\t\tprice,\n\t\tSettings::SmallBalanceDeepLink{},\n\t\tready);\n}\n\nvoid SubmitTonForm(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tMTPInputInvoice invoice,\n\t\tuint64 formId,\n\t\tCreditsAmount ton,\n\t\tFn<void(Payments::CheckoutResult, const MTPUpdates *)> done) {\n\tconst auto ready = [=] {\n\t\tSendStarsFormRequest(\n\t\t\tshow,\n\t\t\tSettings::SmallBalanceResult::Already,\n\t\t\tformId,\n\t\t\tinvoice,\n\t\t\tdone);\n\t};\n\tstruct State {\n\t\trpl::lifetime lifetime;\n\t\tbool success = false;\n\t};\n\tconst auto state = std::make_shared<State>();\n\n\tconst auto session = &show->session();\n\tsession->credits().tonLoad();\n\tsession->credits().tonLoadedValue(\n\t) | rpl::filter(rpl::mappers::_1) | rpl::on_next([=] {\n\t\tstate->lifetime.destroy();\n\n\t\tif (session->credits().tonBalance() < ton) {\n\t\t\tshow->show(Box(Ui::InsufficientTonBox, session, ton));\n\t\t} else {\n\t\t\tready();\n\t\t}\n\t}, state->lifetime);\n}\n\nvoid RequestOurForm(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tMTPInputInvoice invoice,\n\tFn<void(\n\t\tuint64 formId,\n\t\tCreditsAmount price,\n\t\tstd::optional<Payments::CheckoutResult> failure)> done) {\n\tconst auto fail = [=](Payments::CheckoutResult failure) {\n\t\tdone(0, {}, failure);\n\t};\n\tshow->session().api().request(MTPpayments_GetPaymentForm(\n\t\tMTP_flags(0),\n\t\tinvoice,\n\t\tMTPDataJSON() // theme_params\n\t)).done([=](const MTPpayments_PaymentForm &result) {\n\t\tresult.match([&](const MTPDpayments_paymentFormStarGift &data) {\n\t\t\tconst auto &invoice = data.vinvoice().data();\n\t\t\tconst auto prices = invoice.vprices().v;\n\t\t\tif (show->valid() && !prices.isEmpty()) {\n\t\t\t\tconst auto price = prices.front().data().vamount().v;\n\t\t\t\tconst auto currency = qs(invoice.vcurrency());\n\t\t\t\tconst auto amount = (currency == Ui::kCreditsCurrency)\n\t\t\t\t\t? CreditsAmount(price)\n\t\t\t\t\t: (currency == u\"TON\"_q)\n\t\t\t\t\t? CreditsAmount(\n\t\t\t\t\t\tprice / Ui::kNanosInOne,\n\t\t\t\t\t\tprice % Ui::kNanosInOne,\n\t\t\t\t\t\tCreditsType::Ton)\n\t\t\t\t\t: std::optional<CreditsAmount>();\n\t\t\t\tif (amount) {\n\t\t\t\t\tdone(data.vform_id().v, *amount, std::nullopt);\n\t\t\t\t} else {\n\t\t\t\t\tfail(Payments::CheckoutResult::Failed);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfail(Payments::CheckoutResult::Failed);\n\t\t\t}\n\t\t}, [&](const MTPDpayments_paymentFormStars &data) {\n\t\t\tshow->session().data().processUsers(data.vusers());\n\t\t\tconst auto currency = qs(data.vinvoice().data().vcurrency());\n\t\t\tconst auto &prices = data.vinvoice().data().vprices().v;\n\t\t\tif (!prices.empty()) {\n\t\t\t\tconst auto price = prices.front().data().vamount().v;\n\t\t\t\tconst auto amount = (currency == Ui::kCreditsCurrency)\n\t\t\t\t\t? CreditsAmount(price)\n\t\t\t\t\t: (currency == u\"TON\"_q)\n\t\t\t\t\t? CreditsAmount(\n\t\t\t\t\t\tprice / Ui::kNanosInOne,\n\t\t\t\t\t\tprice % Ui::kNanosInOne,\n\t\t\t\t\t\tCreditsType::Ton)\n\t\t\t\t\t: std::optional<CreditsAmount>();\n\t\t\t\tif (amount) {\n\t\t\t\t\tdone(data.vform_id().v, *amount, std::nullopt);\n\t\t\t\t} else {\n\t\t\t\t\tfail(Payments::CheckoutResult::Failed);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfail(Payments::CheckoutResult::Failed);\n\t\t\t}\n\t\t}, [&](const auto &) {\n\t\t\tfail(Payments::CheckoutResult::Failed);\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto type = error.type();\n\t\tif (type == u\"STARGIFT_EXPORT_IN_PROGRESS\"_q) {\n\t\t\tfail(Payments::CheckoutResult::Cancelled);\n\t\t} else if (type == u\"NO_PAYMENT_NEEDED\"_q) {\n\t\t\tfail(Payments::CheckoutResult::Free);\n\t\t} else if (type == u\"USER_DISALLOWED_STARGIFTS\"_q) {\n\t\t\tshow->showToast(tr::lng_edit_privacy_gifts_restricted(tr::now));\n\t\t\tfail(Payments::CheckoutResult::Cancelled);\n\t\t} else {\n\t\t\tif (!ShowGiftErrorToast(show, error)) {\n\t\t\t\tshow->showToast(type);\n\t\t\t}\n\t\t\tfail(Payments::CheckoutResult::Failed);\n\t\t}\n\t}).send();\n}\n\nvoid RequestStarsFormAndSubmit(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tMTPInputInvoice invoice,\n\t\tFn<void(Payments::CheckoutResult, const MTPUpdates *)> done) {\n\tRequestOurForm(show, invoice, [=](\n\t\t\tuint64 formId,\n\t\t\tCreditsAmount price,\n\t\t\tstd::optional<Payments::CheckoutResult> failure) {\n\t\tif (failure) {\n\t\t\tdone(*failure, nullptr);\n\t\t} else if (!price.stars()) {\n\t\t\tdone(Payments::CheckoutResult::Failed, nullptr);\n\t\t} else {\n\t\t\tSubmitStarsForm(show, invoice, formId, price.whole(), done);\n\t\t}\n\t});\n}\n\nvoid ShowGiftTransferredToast(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<PeerData*> to,\n\t\tconst Data::UniqueGift &gift) {\n\tshow->showToast({\n\t\t.title = tr::lng_gift_transferred_title(tr::now),\n\t\t.text = tr::lng_gift_transferred_about(\n\t\t\ttr::now,\n\t\t\t\tlt_name,\n\t\t\t\ttr::bold(Data::UniqueGiftName(gift)),\n\t\t\t\tlt_recipient,\n\t\t\t\ttr::bold(to->shortName()),\n\t\t\t\ttr::marked),\n\t\t.duration = kUpgradeDoneToastDuration,\n\t});\n}\n\nbool ShowGiftErrorToast(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tconst MTP::Error &error) {\n\tif (error.type() == u\"STARGIFT_ALREADY_BURNED\"_q) {\n\t\tshow->showToast(tr::lng_gift_burned_message(tr::now));\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nCreditsAmount StarsFromTon(\n\t\tnot_null<Main::Session*> session,\n\t\tCreditsAmount ton) {\n\tconst auto appConfig = &session->appConfig();\n\tconst auto starsRate = appConfig->starsSellRate() / 100.;\n\tconst auto tonRate = appConfig->currencySellRate();\n\tif (!starsRate) {\n\t\treturn {};\n\t}\n\tconst auto count = (ton.value() * tonRate) / starsRate;\n\treturn CreditsAmount(int(base::SafeRound(count)));\n}\n\nCreditsAmount TonFromStars(\n\t\tnot_null<Main::Session*> session,\n\t\tCreditsAmount stars) {\n\tconst auto appConfig = &session->appConfig();\n\tconst auto starsRate = appConfig->starsSellRate() / 100.;\n\tconst auto tonRate = appConfig->currencySellRate();\n\tif (!tonRate) {\n\t\treturn {};\n\t}\n\tconst auto count = (stars.value() * starsRate) / tonRate;\n\tconst auto whole = int(std::floor(count));\n\tconst auto cents = int(base::SafeRound((count - whole) * 100));\n\treturn CreditsAmount(\n\t\twhole,\n\t\tcents * (Ui::kNanosInOne / 100),\n\t\tCreditsType::Ton);\n}\n\nstruct DefaultGiftHandlerState {\n\tnot_null<Window::SessionController*> window;\n\tnot_null<PeerData*> peer;\n\tstd::shared_ptr<Api::PremiumGiftCodeOptions> api;\n\tstd::shared_ptr<Data::UniqueGift> transferRequested;\n\tuint64 resaleRequestingId = 0;\n\trpl::lifetime resaleLifetime;\n\n\tbase::has_weak_ptr guard;\n};\n\nvoid DefaultGiftHandler(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<DefaultGiftHandlerState*> state,\n\t\tInfo::PeerGifts::GiftDescriptor descriptor) {\n\tconst auto star = std::get_if<GiftTypeStars>(&descriptor);\n\tconst auto send = crl::guard(&state->guard, [=] {\n\t\twindow->show(Box(\n\t\t\tSendGiftBox,\n\t\t\twindow,\n\t\t\tstate->peer,\n\t\t\tstate->api,\n\t\t\tdescriptor,\n\t\t\tnullptr));\n\t});\n\tconst auto peer = state->peer;\n\tconst auto unique = star ? star->info.unique : nullptr;\n\tconst auto premiumNeeded = star && star->info.requirePremium;\n\tif (unique && star->resale) {\n\t\twindow->show(Box(\n\t\t\tSettings::GlobalStarGiftBox,\n\t\t\twindow->uiShow(),\n\t\t\tstar->info,\n\t\t\tSettings::StarGiftResaleInfo{\n\t\t\t\t.recipientId = peer->id,\n\t\t\t\t.forceTon = star->forceTon,\n\t\t\t},\n\t\t\tSettings::CreditsEntryBoxStyleOverrides()));\n\t} else if (unique && star->mine && !peer->isSelf()) {\n\t\tif (ShowTransferGiftLater(window->uiShow(), unique)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto done = [=] {\n\t\t\twindow->session().credits().load(true);\n\t\t\twindow->showPeerHistory(peer);\n\t\t};\n\t\tif (state->transferRequested == unique) {\n\t\t\treturn;\n\t\t}\n\t\tstate->transferRequested = unique;\n\t\tconst auto savedId = star->transferId;\n\t\tusing Payments::CheckoutResult;\n\t\tconst auto formReady = [=](\n\t\t\t\tuint64 formId,\n\t\t\t\tCreditsAmount price,\n\t\t\t\tstd::optional<CheckoutResult> failure) {\n\t\t\tstate->transferRequested = nullptr;\n\t\t\tif (!failure && !price.stars()) {\n\t\t\t\tLOG((\"API Error: Bad transfer invoice currenct.\"));\n\t\t\t} else if (!failure\n\t\t\t\t|| *failure == CheckoutResult::Free) {\n\t\t\t\tunique->starsForTransfer = failure\n\t\t\t\t\t? 0\n\t\t\t\t\t: price.whole();\n\t\t\t\tShowTransferToBox(\n\t\t\t\t\twindow,\n\t\t\t\t\tpeer,\n\t\t\t\t\tunique,\n\t\t\t\t\tsavedId,\n\t\t\t\t\tdone);\n\t\t\t} else if (*failure == CheckoutResult::Cancelled) {\n\t\t\t\tdone();\n\t\t\t}\n\t\t};\n\t\tRequestOurForm(\n\t\t\twindow->uiShow(),\n\t\t\tMTP_inputInvoiceStarGiftTransfer(\n\t\t\t\tApi::InputSavedStarGiftId(savedId, unique),\n\t\t\t\tpeer->input()),\n\t\t\tformReady);\n\t} else if (star && star->resale) {\n\t\tconst auto id = star->info.id;\n\t\tif (state->resaleRequestingId == id) {\n\t\t\treturn;\n\t\t}\n\t\tstate->resaleRequestingId = id;\n\t\tstate->resaleLifetime = ShowStarGiftResale(\n\t\t\twindow,\n\t\t\tpeer,\n\t\t\tid,\n\t\t\tstar->info.resellTitle,\n\t\t\t[=] { state->resaleRequestingId = 0; });\n\t} else if (star && star->info.auction()) {\n\t\tif (!IsSoldOut(star->info)\n\t\t\t&& premiumNeeded\n\t\t\t&& !peer->session().premium()) {\n\t\t\tSettings::ShowPremiumGiftPremium(window, star->info);\n\t\t} else {\n\t\t\tconst auto id = star->info.id;\n\t\t\tif (state->resaleRequestingId == id) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->resaleRequestingId = id;\n\t\t\tstate->resaleLifetime = ShowStarGiftAuction(\n\t\t\t\twindow,\n\t\t\t\tpeer,\n\t\t\t\tid,\n\t\t\t\t[=] { state->resaleRequestingId = 0; },\n\t\t\t\tcrl::guard(&state->guard, [=] {\n\t\t\t\t\tstate->resaleLifetime.destroy();\n\t\t\t\t}));\n\t\t}\n\t} else if (star && IsSoldOut(star->info)) {\n\t\twindow->show(Box(SoldOutBox, window, *star));\n\t} else if (premiumNeeded && !peer->session().premium()) {\n\t\tSettings::ShowPremiumGiftPremium(window, star->info);\n\t} else if (star\n\t\t&& star->info.lockedUntilDate\n\t\t&& star->info.lockedUntilDate > base::unixtime::now()) {\n\t\tconst auto ready = crl::guard(window, [=] {\n\t\t\tif (premiumNeeded && !peer->session().premium()) {\n\t\t\t\tSettings::ShowPremiumGiftPremium(\n\t\t\t\t\twindow,\n\t\t\t\t\tv::get<GiftTypeStars>(descriptor).info);\n\t\t\t} else {\n\t\t\t\tsend();\n\t\t\t}\n\t\t});\n\t\tCheckMaybeGiftLocked(window, star->info.id, ready);\n\t} else if (star\n\t\t\t&& star->info.perUserTotal\n\t\t\t&& !star->info.perUserRemains) {\n\t\twindow->showToast({\n\t\t\t.text = tr::lng_gift_sent_finished(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tstar->info.perUserTotal,\n\t\t\t\ttr::rich),\n\t\t});\n\t} else {\n\t\tsend();\n\t}\n}\n\nobject_ptr<RpWidget> MakeGiftsList(GiftsListArgs &&args) {\n\tusing namespace Info::PeerGifts;\n\n\tauto result = object_ptr<VisibleRangeWidget>((QWidget*)nullptr);\n\tconst auto raw = result.data();\n\n\tconst auto mode = args.mode;\n\tconst auto peer = args.peer;\n\tconst auto window = args.window;\n\tconst auto session = &window->session();\n\n\tData::AmPremiumValue(session) | rpl::on_next([=] {\n\t\traw->update();\n\t}, raw->lifetime());\n\n\tstruct State {\n\t\tDelegate delegate;\n\t\tDefaultGiftHandlerState handlerState;\n\t\tstd::vector<int> order;\n\t\tstd::vector<bool> validated;\n\t\tstd::vector<GiftDescriptor> list;\n\t\tstd::vector<std::unique_ptr<GiftButton>> buttons;\n\t\trpl::variable<VisibleRange> visibleRange;\n\t\tbool sending = false;\n\t\tint perRow = 1;\n\t};\n\tconst auto buttonMode = (mode == GiftsListMode::Craft)\n\t\t? GiftButtonMode::Craft\n\t\t: (mode == GiftsListMode::CraftResale)\n\t\t? GiftButtonMode::CraftResale\n\t\t: GiftButtonMode::Full;\n\tconst auto state = raw->lifetime().make_state<State>(State{\n\t\t.delegate = Delegate(session, buttonMode),\n\t\t.handlerState = {\n\t\t\t.window = window,\n\t\t\t.peer = peer,\n\t\t},\n\t});\n\tconst auto single = state->delegate.buttonSize();\n\tconst auto shadow = st::defaultDropdownMenu.wrap.shadow;\n\tconst auto extend = shadow.extend;\n\n\tconst auto handler = args.handler\n\t\t? args.handler\n\t\t: crl::guard(raw, [=](const GiftDescriptor &descriptor) {\n\t\t\tDefaultGiftHandler(window, &state->handlerState, descriptor);\n\t\t});\n\n\tauto &packs = session->giftBoxStickersPacks();\n\tpacks.updated() | rpl::on_next([=] {\n\t\tfor (const auto &button : state->buttons) {\n\t\t\tif (const auto raw = button.get()) {\n\t\t\t\traw->update();\n\t\t\t}\n\t\t}\n\t}, raw->lifetime());\n\n\tconst auto loadMore = args.loadMore;\n\tconst auto alreadySelected = args.selected;\n\tconst auto rebuild = [=] {\n\t\tconst auto width = st::boxWideWidth;\n\t\tconst auto padding = st::giftBoxPadding;\n\t\tconst auto available = width - padding.left() - padding.right();\n\t\tconst auto range = state->visibleRange.current();\n\t\tconst auto count = int(state->list.size());\n\n\t\tauto &buttons = state->buttons;\n\t\tif (buttons.size() < count) {\n\t\t\tbuttons.resize(count);\n\t\t}\n\t\tauto &validated = state->validated;\n\t\tvalidated.resize(count);\n\n\t\tauto x = padding.left();\n\t\tauto y = padding.top();\n\t\tconst auto perRow = state->perRow;\n\t\tconst auto singlew = single.width() + st::giftBoxGiftSkip.x();\n\t\tconst auto singleh = single.height() + st::giftBoxGiftSkip.y();\n\t\tconst auto rangeFrom = range.top - y;\n\t\tconst auto rowFrom = std::max(rangeFrom, 0) / singleh;\n\t\tconst auto rangeTill = range.bottom - y + st::giftBoxGiftSkip.y();\n\t\tconst auto rowTill = (std::max(rangeTill, 0) + singleh - 1)\n\t\t\t/ singleh;\n\t\tAssert(rowTill >= rowFrom);\n\t\tconst auto first = rowFrom * perRow;\n\t\tconst auto last = std::min(rowTill * perRow, count);\n\t\tauto checkedFrom = 0;\n\t\tauto checkedTill = int(buttons.size());\n\t\tconst auto ensureButton = [&](int index) {\n\t\t\tauto &button = buttons[index];\n\t\t\tif (!button) {\n\t\t\t\tvalidated[index] = false;\n\t\t\t\tfor (; checkedFrom != first; ++checkedFrom) {\n\t\t\t\t\tif (buttons[checkedFrom]) {\n\t\t\t\t\t\tbutton = std::move(buttons[checkedFrom]);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!button) {\n\t\t\t\tfor (; checkedTill != last; ) {\n\t\t\t\t\t--checkedTill;\n\t\t\t\t\tif (buttons[checkedTill]) {\n\t\t\t\t\t\tbutton = std::move(buttons[checkedTill]);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!button) {\n\t\t\t\tbutton = std::make_unique<GiftButton>(raw, &state->delegate);\n\t\t\t}\n\t\t\tconst auto raw = button.get();\n\t\t\tif (validated[index]) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\traw->show();\n\t\t\tvalidated[index] = true;\n\t\t\tconst auto &descriptor = state->list[state->order[index]];\n\t\t\tif (mode == GiftsListMode::Craft) {\n\t\t\t\tconst auto already = ranges::contains(\n\t\t\t\t\talreadySelected,\n\t\t\t\t\tv::get<GiftTypeStars>(descriptor).info.unique->slug,\n\t\t\t\t\t&Data::UniqueGift::slug);\n\t\t\t\traw->toggleSelected(\n\t\t\t\t\talready,\n\t\t\t\t\tGiftSelectionMode::Inset,\n\t\t\t\t\tanim::type::instant);\n\t\t\t\traw->setAttribute(Qt::WA_TransparentForMouseEvents, already);\n\t\t\t}\n\t\t\traw->setDescriptor(descriptor, buttonMode);\n\t\t\traw->setClickedCallback([=] {\n\t\t\t\thandler(descriptor);\n\t\t\t});\n\t\t\traw->setGeometry(QRect(QPoint(x, y), single), extend);\n\t\t};\n\t\ty += rowFrom * singleh;\n\t\tfor (auto row = rowFrom; row != rowTill; ++row) {\n\t\t\tfor (auto col = 0; col != perRow; ++col) {\n\t\t\t\tconst auto index = row * perRow + col;\n\t\t\t\tif (index >= count) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tconst auto last = !((col + 1) % perRow);\n\t\t\t\tif (last) {\n\t\t\t\t\tx = padding.left() + available - single.width();\n\t\t\t\t}\n\t\t\t\tensureButton(index);\n\t\t\t\tif (last) {\n\t\t\t\t\tx = padding.left();\n\t\t\t\t\ty += singleh;\n\t\t\t\t} else {\n\t\t\t\t\tx += singlew;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst auto till = std::min(int(buttons.size()), rowTill * perRow);\n\t\tfor (auto i = count; i < till; ++i) {\n\t\t\tif (const auto button = buttons[i].get()) {\n\t\t\t\tbutton->hide();\n\t\t\t}\n\t\t}\n\n\t\tconst auto page = range.bottom - range.top;\n\t\tif (loadMore && page > 0 && range.bottom + page > raw->height()) {\n\t\t\tloadMore();\n\t\t}\n\t};\n\n\tstate->visibleRange = raw->visibleRange();\n\tstate->visibleRange.value(\n\t) | rpl::on_next(rebuild, raw->lifetime());\n\n\tstd::move(\n\t\targs.gifts\n\t) | rpl::on_next([=](const GiftsDescriptor &gifts) {\n\t\tconst auto width = st::boxWideWidth;\n\t\tconst auto padding = st::giftBoxPadding;\n\t\tconst auto available = width - padding.left() - padding.right();\n\t\tstate->perRow = available / single.width();\n\t\tstate->list = std::move(gifts.list);\n\t\tstate->handlerState.api = gifts.api;\n\n\t\tconst auto count = int(state->list.size());\n\t\tstate->order = ranges::views::ints\n\t\t\t| ranges::views::take(count)\n\t\t\t| ranges::to_vector;\n\t\tstate->validated.clear();\n\n\t\tif (SortForBirthday(peer)) {\n\t\t\tranges::stable_partition(state->order, [&](int i) {\n\t\t\t\tconst auto &gift = state->list[i];\n\t\t\t\tconst auto stars = std::get_if<GiftTypeStars>(&gift);\n\t\t\t\treturn stars && stars->info.birthday && !stars->info.unique;\n\t\t\t});\n\t\t}\n\n\t\tconst auto rows = (count + state->perRow - 1) / state->perRow;\n\t\tconst auto height = padding.top()\n\t\t\t+ (rows * single.height())\n\t\t\t+ ((rows - 1) * st::giftBoxGiftSkip.y())\n\t\t\t+ padding.bottom();\n\t\traw->resize(raw->width(), height);\n\t\trebuild();\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\nvoid SendGiftBox(\n\t\tnot_null<GenericBox*> box,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<Api::PremiumGiftCodeOptions> api,\n\t\tconst GiftDescriptor &descriptor,\n\t\trpl::producer<Data::GiftAuctionState> auctionState) {\n\tconst auto stars = std::get_if<GiftTypeStars>(&descriptor);\n\tconst auto auction = !!auctionState;\n\tconst auto limited = stars\n\t\t&& (stars->info.limitedCount > stars->info.limitedLeft)\n\t\t&& (stars->info.limitedLeft > 0);\n\tconst auto costToUpgrade = stars ? stars->info.starsToUpgrade : 0;\n\tconst auto user = peer->asUser();\n\tconst auto disallowed = user\n\t\t? user->disallowedGiftTypes()\n\t\t: Api::DisallowedGiftTypes();\n\tconst auto disallowLimited = !peer->isSelf()\n\t\t&& (disallowed & Api::DisallowedGiftType::Limited);\n\tconst auto disallowUnique = !peer->isSelf()\n\t\t&& (disallowed & Api::DisallowedGiftType::Unique);\n\tbox->setStyle((limited && !auction) ? st::giftLimitedBox : st::giftBox);\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setTitle(tr::lng_gift_send_title());\n\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\tbox->closeBox();\n\t});\n\n\tconst auto session = &window->session();\n\n\tstruct State {\n\t\trpl::variable<GiftSendDetails> details;\n\t\trpl::variable<bool> messageAllowed;\n\t\tstd::shared_ptr<Data::DocumentMedia> media;\n\t\trpl::variable<Data::GiftAuctionState> auction;\n\t\tbool submitting = false;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tif (auctionState) {\n\t\tstate->auction = std::move(auctionState);\n\t}\n\tstate->details = GiftSendDetails{\n\t\t.descriptor = descriptor,\n\t\t.randomId = base::RandomValue<uint64>(),\n\t\t.upgraded = disallowLimited && (costToUpgrade > 0) && !disallowUnique,\n\t};\n\tpeer->updateFull();\n\tstate->messageAllowed = peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::StarsPerMessage\n\t) | rpl::map([=] {\n\t\treturn peer->starsPerMessageChecked() == 0;\n\t});\n\n\tauto cost = state->details.value(\n\t) | rpl::map([](const GiftSendDetails &details) {\n\t\treturn v::match(details.descriptor, [&](const GiftTypePremium &data) {\n\t\t\tconst auto stars = (details.byStars && data.stars)\n\t\t\t\t? data.stars\n\t\t\t\t: (data.currency == kCreditsCurrency)\n\t\t\t\t? data.cost\n\t\t\t\t: 0;\n\t\t\tif (stars) {\n\t\t\t\treturn CreditsEmojiSmall().append(\n\t\t\t\t\tLang::FormatCountDecimal(std::abs(stars)));\n\t\t\t}\n\t\t\treturn TextWithEntities{\n\t\t\t\tFillAmountAndCurrency(data.cost, data.currency),\n\t\t\t};\n\t\t}, [&](const GiftTypeStars &data) {\n\t\t\tconst auto amount = std::abs(data.info.stars)\n\t\t\t\t+ (details.upgraded ? data.info.starsToUpgrade : 0);\n\t\t\treturn CreditsEmojiSmall().append(\n\t\t\t\tLang::FormatCountDecimal(amount));\n\t\t});\n\t});\n\n\tconst auto document = LookupGiftSticker(session, descriptor);\n\tif ((state->media = document ? document->createMediaView() : nullptr)) {\n\t\tstate->media->checkStickerLarge();\n\t}\n\n\tconst auto container = box->verticalLayout();\n\tcontainer->add(object_ptr<PreviewWrap>(\n\t\tcontainer,\n\t\tpeer,\n\t\tstate->details.value()));\n\n\tconst auto messageWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tmessageWrap->toggleOn(state->messageAllowed.value());\n\tmessageWrap->finishAnimating();\n\tconst auto messageInner = messageWrap->entity();\n\tconst auto limit = StarGiftMessageLimit(session);\n\tconst auto text = AddPartInput(\n\t\twindow,\n\t\tmessageInner,\n\t\tbox->getDelegate()->outerContainer(),\n\t\ttr::lng_gift_send_message(),\n\t\tQString(),\n\t\tlimit);\n\ttext->changes() | rpl::on_next([=] {\n\t\tauto now = state->details.current();\n\t\tauto textWithTags = text->getTextWithAppliedMarkdown();\n\t\tnow.text = TextWithEntities{\n\t\t\tstd::move(textWithTags.text),\n\t\t\tTextUtilities::ConvertTextTagsToEntities(textWithTags.tags)\n\t\t};\n\t\tstate->details = std::move(now);\n\t}, text->lifetime());\n\n\tbox->setFocusCallback([=] {\n\t\ttext->setFocusFast();\n\t});\n\n\tconst auto allow = [=](not_null<DocumentData*> emoji) {\n\t\treturn true;\n\t};\n\tInitMessageFieldHandlers({\n\t\t.session = session,\n\t\t.show = window->uiShow(),\n\t\t.field = text,\n\t\t.customEmojiPaused = [=] {\n\t\t\tusing namespace Window;\n\t\t\treturn window->isGifPausedAtLeastFor(GifPauseReason::Layer);\n\t\t},\n\t\t.allowPremiumEmoji = allow,\n\t\t.allowMarkdownTags = {\n\t\t\tInputField::kTagBold,\n\t\t\tInputField::kTagItalic,\n\t\t\tInputField::kTagUnderline,\n\t\t\tInputField::kTagStrikeOut,\n\t\t\tInputField::kTagSpoiler,\n\t\t}\n\t});\n\tEmoji::SuggestionsController::Init(\n\t\tbox->getDelegate()->outerContainer(),\n\t\ttext,\n\t\tsession,\n\t\t{ .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow });\n\tif (stars) {\n\t\tif (costToUpgrade > 0 && !peer->isSelf() && !disallowLimited && !disallowUnique) {\n\t\t\tconst auto stargiftInfo = stars->info;\n\t\t\tconst auto showing = std::make_shared<bool>();\n\t\t\tAddDivider(container);\n\t\t\tAddSkip(container);\n\t\t\tAddUpgradeButton(container, costToUpgrade, peer, [=](bool on) {\n\t\t\t\tauto now = state->details.current();\n\t\t\t\tnow.upgraded = on;\n\t\t\t\tstate->details = std::move(now);\n\t\t\t}, [=] {\n\t\t\t\tif (*showing) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t*showing = true;\n\t\t\t\tShowStarGiftUpgradeBox({\n\t\t\t\t\t.controller = window,\n\t\t\t\t\t.stargift = stargiftInfo,\n\t\t\t\t\t.ready = [=](bool) { *showing = false; },\n\t\t\t\t\t.peer = peer,\n\t\t\t\t\t.cost = int(costToUpgrade),\n\t\t\t\t});\n\t\t\t});\n\t\t} else {\n\t\t\tAddDivider(container);\n\t\t}\n\t\tAddSkip(container);\n\t\tcontainer->add(\n\t\t\tobject_ptr<SettingsButton>(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_gift_send_anonymous(),\n\t\t\t\tst::settingsButtonNoIcon)\n\t\t)->toggleOn(rpl::single(peer->isSelf()))->toggledValue(\n\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\tauto now = state->details.current();\n\t\t\tnow.anonymous = toggled;\n\t\t\tstate->details = std::move(now);\n\t\t}, container->lifetime());\n\t\tAddSkip(container);\n\t}\n\tv::match(descriptor, [&](const GiftTypePremium &data) {\n\t\tAddDividerText(messageInner, tr::lng_gift_send_premium_about(\n\t\t\tlt_user,\n\t\t\trpl::single(peer->shortName())));\n\n\t\tif (const auto byStars = data.stars) {\n\t\t\tconst auto star = Ui::Text::IconEmoji(&st::starIconEmojiColored);\n\t\t\tAddSkip(container);\n\t\t\tcontainer->add(\n\t\t\t\tobject_ptr<SettingsButton>(\n\t\t\t\t\tcontainer,\n\t\t\t\t\ttr::lng_gift_send_pay_with_stars(\n\t\t\t\t\t\tlt_amount,\n\t\t\t\t\t\trpl::single(base::duplicate(star).append(Lang::FormatCountDecimal(byStars))),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\t\tst::settingsButtonNoIcon)\n\t\t\t)->toggleOn(rpl::single(false))->toggledValue(\n\t\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\t\tauto now = state->details.current();\n\t\t\t\tnow.byStars = toggled;\n\t\t\t\tstate->details = std::move(now);\n\t\t\t}, container->lifetime());\n\t\t\tAddSkip(container);\n\n\t\t\tconst auto balance = AddDividerText(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_gift_send_stars_balance(\n\t\t\t\t\tlt_amount,\n\t\t\t\t\tpeer->session().credits().balanceValue(\n\t\t\t\t\t) | rpl::map([=](CreditsAmount amount) {\n\t\t\t\t\t\treturn base::duplicate(star).append(\n\t\t\t\t\t\t\tLang::FormatCreditsAmountDecimal(amount));\n\t\t\t\t\t}),\n\t\t\t\t\tlt_link,\n\t\t\t\t\ttr::lng_gift_send_stars_balance_link(tr::link),\n\t\t\t\t\ttr::marked));\n\t\t\tstruct State {\n\t\t\t\tSettings::BuyStarsHandler buyStars;\n\t\t\t\trpl::variable<bool> loading;\n\t\t\t};\n\t\t\tconst auto state = balance->lifetime().make_state<State>();\n\t\t\tstate->loading = state->buyStars.loadingValue();\n\t\t\tbalance->setClickHandlerFilter([=](const auto &...) {\n\t\t\t\tif (!state->loading.current()) {\n\t\t\t\t\tstate->buyStars.handler(window->uiShow())();\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t});\n\t\t}\n\t}, [&](const GiftTypeStars &) {\n\t\tAddDividerText(container, peer->isSelf()\n\t\t\t? tr::lng_gift_send_anonymous_self()\n\t\t\t: peer->isBroadcast()\n\t\t\t? tr::lng_gift_send_anonymous_about_channel()\n\t\t\t: rpl::conditional(\n\t\t\t\tstate->messageAllowed.value(),\n\t\t\t\ttr::lng_gift_send_anonymous_about(\n\t\t\t\t\tlt_user,\n\t\t\t\t\trpl::single(peer->shortName()),\n\t\t\t\t\tlt_recipient,\n\t\t\t\t\trpl::single(peer->shortName())),\n\t\t\t\ttr::lng_gift_send_anonymous_about_paid(\n\t\t\t\t\tlt_user,\n\t\t\t\t\trpl::single(peer->shortName()),\n\t\t\t\t\tlt_recipient,\n\t\t\t\t\trpl::single(peer->shortName()))));\n\t});\n\n\tconst auto button = box->addButton(rpl::single(QString()), [=] {\n\t\tif (state->submitting) {\n\t\t\treturn;\n\t\t}\n\t\tstate->submitting = true;\n\t\tauto details = state->details.current();\n\t\tif (!state->messageAllowed.current()) {\n\t\t\tdetails.text = {};\n\t\t}\n\t\tconst auto stars = std::get_if<GiftTypeStars>(&details.descriptor);\n\t\tif (stars && stars->info.auction()) {\n\t\t\tconst auto bidBox = window->show(MakeAuctionBidBox({\n\t\t\t\t.peer = peer,\n\t\t\t\t.show = window->uiShow(),\n\t\t\t\t.state = state->auction.value(),\n\t\t\t\t.details = std::make_unique<GiftSendDetails>(details),\n\t\t\t}));\n\t\t\tbidBox->boxClosing(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tbox->closeBox();\n\t\t\t}, box->lifetime());\n\t\t\treturn;\n\t\t}\n\t\tconst auto copy = state->media; // Let media outlive the box.\n\t\tconst auto weak = base::make_weak(box);\n\t\tconst auto done = [=](Payments::CheckoutResult result) {\n\t\t\tif (result == Payments::CheckoutResult::Paid) {\n\t\t\t\tif (details.byStars\n\t\t\t\t\t|| v::is<GiftTypeStars>(details.descriptor)) {\n\t\t\t\t\twindow->session().credits().load(true);\n\t\t\t\t}\n\t\t\t\tconst auto another = copy; // Let media outlive the box.\n\t\t\t\twindow->showPeerHistory(peer);\n\t\t\t\tShowSentToast(window, details.descriptor, details);\n\t\t\t}\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t};\n\t\tSendGift(window, peer, api, details, done);\n\t});\n\tif (limited) {\n\t\tif (auction) {\n\t\t\tconst auto &now = state->auction.current();\n\t\t\tconst auto rounds = now.totalRounds;\n\t\t\tconst auto perRound = now.gift->auctionGiftsPerRound;\n\t\t\tauto owned = object_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\trpl::single(tr::lng_auction_about_top_short(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tperRound,\n\t\t\t\t\tlt_bidders,\n\t\t\t\t\ttr::lng_auction_about_top_bidders(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tperRound,\n\t\t\t\t\t\ttr::rich),\n\t\t\t\t\tlt_link,\n\t\t\t\t\ttr::lng_auction_text_link(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_arrow,\n\t\t\t\t\t\tText::IconEmoji(&st::textMoreIconEmoji),\n\t\t\t\t\t\ttr::link),\n\t\t\t\t\ttr::rich)),\n\t\t\t\tst::defaultDividerLabel.label);\n\t\t\tconst auto label = owned.data();\n\t\t\tconst auto about = container->add(\n\t\t\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\t\t\tcontainer,\n\t\t\t\t\tstd::move(owned),\n\t\t\t\t\tst::defaultBoxDividerLabelPadding),\n\t\t\t\t{ 0, st::giftLimitedBox.buttonPadding.top(), 0, 0 });\n\t\t\tAddSoldLeftSlider(about, *stars, st::boxRowPadding);\n\n\t\t\tconst auto show = window->uiShow();\n\t\t\tlabel->setClickHandlerFilter([=](const auto &...) {\n\t\t\t\tshow->show(Box(AuctionAboutBox, rounds, perRound, nullptr));\n\t\t\t\treturn false;\n\t\t\t});\n\t\t} else {\n\t\t\tAddSoldLeftSlider(button, *stars);\n\t\t}\n\t}\n\tif (stars && stars->info.auction()) {\n\t\tSetAuctionButtonCountdownText(\n\t\t\tbutton,\n\t\t\tAuctionButtonCountdownType::Place,\n\t\t\tstate->auction.value());\n\t} else {\n\t\tSetButtonMarkedLabel(\n\t\t\tbutton,\n\t\t\t(peer->isSelf()\n\t\t\t\t? tr::lng_gift_send_button_self\n\t\t\t\t: tr::lng_gift_send_button)(\n\t\t\t\t\tlt_cost,\n\t\t\t\t\tstd::move(cost),\n\t\t\t\t\ttr::marked),\n\t\t\tsession,\n\t\t\tst::creditsBoxButtonLabel,\n\t\t\t&st::giftBox.button.textFg);\n\t}\n}\n\nstd::shared_ptr<Data::GiftUpgradeResult> FindUniqueGift(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPUpdates &updates) {\n\tauto result = std::shared_ptr<Data::GiftUpgradeResult>();\n\tconst auto checkAction = [&](const MTPDmessageService &message) {\n\t\tconst auto &action = message.vaction();\n\t\taction.match([&](const MTPDmessageActionStarGiftUnique &data) {\n\t\t\tif (const auto gift = Api::FromTL(session, data.vgift())) {\n\t\t\t\tconst auto to = data.vpeer()\n\t\t\t\t\t? peerFromMTP(*data.vpeer())\n\t\t\t\t\t: PeerId();\n\t\t\t\tconst auto service = data.vfrom_id()\n\t\t\t\t\t&& session->data().peer(\n\t\t\t\t\t\tpeerFromMTP(*data.vfrom_id()))->isServiceUser();\n\t\t\t\tconst auto channel = (service && peerIsChannel(to))\n\t\t\t\t\t? session->data().channel(peerToChannel(to)).get()\n\t\t\t\t\t: nullptr;\n\t\t\t\tconst auto channelSavedId = channel\n\t\t\t\t\t? data.vsaved_id().value_or_empty()\n\t\t\t\t\t: uint64();\n\t\t\t\tconst auto realGiftMsgId = (peerIsUser(to) && data.vsaved_id())\n\t\t\t\t\t? MsgId(data.vsaved_id().value_or_empty())\n\t\t\t\t\t: MsgId(message.vid().v);\n\n\t\t\t\tresult = std::make_shared<Data::GiftUpgradeResult>(\n\t\t\t\t\tData::GiftUpgradeResult{\n\t\t\t\t\t\t.info = *gift,\n\t\t\t\t\t\t.manageId = (channel && channelSavedId)\n\t\t\t\t\t\t\t? Data::SavedStarGiftId::Chat(\n\t\t\t\t\t\t\t\tchannel,\n\t\t\t\t\t\t\t\tchannelSavedId)\n\t\t\t\t\t\t\t: Data::SavedStarGiftId::User(realGiftMsgId),\n\t\t\t\t\t\t.date = message.vdate().v,\n\t\t\t\t\t\t.starsForDetailsRemove = int(\n\t\t\t\t\t\t\tdata.vdrop_original_details_stars(\n\t\t\t\t\t\t\t).value_or_empty()),\n\t\t\t\t\t\t.saved = data.is_saved(),\n\t\t\t\t\t});\n\t\t\t}\n\t\t}, [](const auto &) {});\n\t};\n\tupdates.match([&](const MTPDupdates &data) {\n\t\tfor (const auto &update : data.vupdates().v) {\n\t\t\tupdate.match([&](const MTPDupdateNewMessage &data) {\n\t\t\t\tdata.vmessage().match([&](const MTPDmessageService &data) {\n\t\t\t\t\tcheckAction(data);\n\t\t\t\t}, [](const auto &) {});\n\t\t\t}, [](const auto &) {});\n\t\t}\n\t}, [](const auto &) {});\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/star_gift_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/star_gift_cover_box.h\"\n#include \"data/data_star_gift.h\"\n\nnamespace Api {\nclass PremiumGiftCodeOptions;\n} // namespace Api\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nstruct UniqueGift;\nstruct GiftCode;\nstruct CreditsHistoryEntry;\nclass SavedStarGiftId;\nstruct GiftAuctionState;\n} // namespace Data\n\nnamespace Info::PeerGifts {\nstruct GiftDescriptor;\n} // namespace Info::PeerGifts\n\nnamespace Main {\nclass Session;\nclass SessionShow;\n} // namespace Main\n\nnamespace Payments {\nenum class CheckoutResult;\n} // namespace Payments\n\nnamespace MTP {\nclass Error;\n} // namespace MTP\n\nnamespace Settings {\nstruct GiftWearBoxStyleOverride;\nstruct CreditsEntryBoxStyleOverrides;\n} // namespace Settings\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nnamespace Ui {\n\nclass RpWidget;\nclass PopupMenu;\nclass GenericBox;\nclass Show;\nclass VerticalLayout;\n\nvoid ChooseStarGiftRecipient(\n\tnot_null<Window::SessionController*> controller);\n\nvoid ShowStarGiftBox(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer);\n\nvoid AddWearGiftCover(\n\tnot_null<VerticalLayout*> container,\n\tconst Data::UniqueGift &data,\n\tnot_null<PeerData*> peer);\n\nvoid AttachGiftSenderBadge(\n\tnot_null<GenericBox*> box,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<PeerData*> from,\n\tconst QDateTime &date,\n\tbool crafted);\n\nvoid ShowUniqueGiftWearBox(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<PeerData*> peer,\n\tconst Data::UniqueGift &gift,\n\tSettings::GiftWearBoxStyleOverride st);\n\nvoid PreloadUniqueGiftResellPrices(not_null<Main::Session*> session);\n\nvoid UpdateGiftSellPrice(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tstd::shared_ptr<Data::UniqueGift> unique,\n\tData::SavedStarGiftId savedId,\n\tCreditsAmount price);\nvoid ShowUniqueGiftSellBox(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tstd::shared_ptr<Data::UniqueGift> unique,\n\tData::SavedStarGiftId savedId,\n\tSettings::GiftWearBoxStyleOverride st);\n\nvoid ShowOfferBuyBox(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tstd::shared_ptr<Data::UniqueGift> unique);\n\nstruct StarGiftUpgradeArgs {\n\tnot_null<Window::SessionController*> controller;\n\tData::StarGift stargift;\n\tFn<void(bool)> ready;\n\tFn<void()> upgraded;\n\tnot_null<PeerData*> peer;\n\tData::SavedStarGiftId savedId;\n\tQString giftPrepayUpgradeHash;\n\tint cost = 0;\n\tbool canAddSender = false;\n\tbool canAddComment = false;\n\tbool canAddMyComment = false;\n\tbool addDetailsDefault = false;\n};\nvoid ShowStarGiftUpgradeBox(StarGiftUpgradeArgs &&args);\n\nvoid SubmitStarsForm(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tMTPInputInvoice invoice,\n\tuint64 formId,\n\tuint64 price,\n\tFn<void(Payments::CheckoutResult, const MTPUpdates *)> done);\nvoid SubmitTonForm(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tMTPInputInvoice invoice,\n\tuint64 formId,\n\tCreditsAmount ton,\n\tFn<void(Payments::CheckoutResult, const MTPUpdates *)> done);\nvoid RequestOurForm(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tMTPInputInvoice invoice,\n\tFn<void(\n\t\tuint64 formId,\n\t\tCreditsAmount price,\n\t\tstd::optional<Payments::CheckoutResult> failure)> done);\nvoid RequestStarsFormAndSubmit(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tMTPInputInvoice invoice,\n\tFn<void(Payments::CheckoutResult, const MTPUpdates *)> done);\n\nvoid ShowGiftTransferredToast(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tnot_null<PeerData*> to,\n\tconst Data::UniqueGift &gift);\n\n[[nodiscard]] bool ShowGiftErrorToast(\n\tstd::shared_ptr<Ui::Show> show,\n\tconst MTP::Error &error);\n\n[[nodiscard]] CreditsAmount StarsFromTon(\n\tnot_null<Main::Session*> session,\n\tCreditsAmount ton);\n[[nodiscard]] CreditsAmount TonFromStars(\n\tnot_null<Main::Session*> session,\n\tCreditsAmount stars);\n\nstruct GiftsDescriptor {\n\tstd::vector<Info::PeerGifts::GiftDescriptor> list;\n\tstd::shared_ptr<Api::PremiumGiftCodeOptions> api;\n};\nenum class GiftsListMode {\n\tSend,\n\tCraft,\n\tCraftResale,\n};\nstruct GiftsListArgs {\n\tnot_null<Window::SessionController*> window;\n\tGiftsListMode mode = GiftsListMode::Send;\n\tnot_null<PeerData*> peer;\n\trpl::producer<GiftsDescriptor> gifts;\n\tstd::vector<std::shared_ptr<Data::UniqueGift>> selected;\n\tFn<void()> loadMore;\n\tFn<void(Info::PeerGifts::GiftDescriptor)> handler;\n};\n[[nodiscard]] object_ptr<RpWidget> MakeGiftsList(GiftsListArgs &&args);\n\nvoid SendGiftBox(\n\tnot_null<GenericBox*> box,\n\tnot_null<Window::SessionController*> window,\n\tnot_null<PeerData*> peer,\n\tstd::shared_ptr<Api::PremiumGiftCodeOptions> api,\n\tconst Info::PeerGifts::GiftDescriptor &descriptor,\n\trpl::producer<Data::GiftAuctionState> auctionState);\n\n[[nodiscard]] Data::CreditsHistoryEntry EntryForUpgradedGift(\n\tconst std::shared_ptr<Data::GiftUpgradeResult> &gift,\n\tuint64 nextToUpgradeStickerId = 0,\n\tFn<void()> nextToUpgradeShow = nullptr,\n\tFn<void()> craftAnother = nullptr);\n\n[[nodiscard]] std::shared_ptr<Data::GiftUpgradeResult> FindUniqueGift(\n\tnot_null<Main::Session*> session,\n\tconst MTPUpdates &updates);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/star_gift_cover_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/star_gift_cover_box.h\"\n\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"core/application.h\"\n#include \"core/ui_integration.h\"\n#include \"main/main_session.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_common.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"ui/effects/premium_stars_colored.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/top_background_gradient.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/widgets/labels.h\"\n#include \"info/peer_gifts/info_peer_gifts_common.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kCrossfadeDuration = crl::time(400);\nconstexpr auto kGradientButtonBgOpacity = 0.6;\n\nconstexpr auto kSpinnerBackdrops = 6;\nconstexpr auto kSpinnerPatterns = 6;\nconstexpr auto kSpinnerModels = 6;\n\nconstexpr auto kBackdropSpinDuration = crl::time(300);\nconstexpr auto kBackdropStopsAt = crl::time(2.5 * 1000);\nconstexpr auto kPatternSpinDuration = crl::time(600);\nconstexpr auto kPatternStopsAt = crl::time(4 * 1000);\nconstexpr auto kModelSpinDuration = crl::time(160);\nconstexpr auto kModelStopsAt = crl::time(5.5 * 1000);\nconstexpr auto kModelScaleFrom = 0.7;\n\nstruct AttributeSpin {\n\tAttributeSpin(crl::time duration) : duration(duration) {\n\t}\n\n\tAnimations::Simple animation;\n\tcrl::time duration = 0;\n\tint wasIndex = -1;\n\tint nowIndex = -1;\n\tint willIndex = -1;\n\n\t[[nodiscard]] float64 progress() const {\n\t\treturn animation.value(1.);\n\t}\n\tvoid startWithin(int count, Fn<void()> update) {\n\t\tExpects(count > 0);\n\n\t\twasIndex = nowIndex;\n\t\tnowIndex = willIndex;\n\t\twillIndex = (willIndex < 0 ? 1 : (willIndex + 1)) % count;\n\t\tanimation.start(update, 0., 1., duration);\n\t}\n\tvoid startToTarget(Fn<void()> update, int slowdown = 1) {\n\t\tif (willIndex != 0) {\n\t\t\twasIndex = nowIndex;\n\t\t\tnowIndex = willIndex;\n\t\t\twillIndex = 0;\n\t\t\tanimation.start(\n\t\t\t\tupdate,\n\t\t\t\t0.,\n\t\t\t\t1.,\n\t\t\t\tduration * 3 * slowdown,\n\t\t\t\tanim::easeOutCubic);\n\t\t}\n\t}\n};\n\nstruct Released {\n\tReleased() : link(QColor(255, 255, 255)) {\n\t}\n\n\trpl::variable<TextWithEntities> subtitleText;\n\tstd::optional<Premium::ColoredMiniStars> stars;\n\tstyle::owned_color link;\n\tstyle::FlatLabel st;\n\trpl::variable<PeerData*> by;\n\tbase::unique_qptr<FlatLabel> subtitle;\n\tbase::unique_qptr<AbstractButton> subtitleButton;\n\trpl::variable<int> subtitleHeight;\n\trpl::variable<bool> subtitleCustom;\n\tbool outlined = false;\n\tQColor bg;\n\tQColor fg;\n};\n\n} // namespace\n\nstruct UniqueGiftCoverWidget::BackdropView {\n\tData::UniqueGiftBackdrop colors;\n\tQImage gradient;\n};\n\nstruct UniqueGiftCoverWidget::PatternView {\n\tDocumentData *document = nullptr;\n\tstd::unique_ptr<Text::CustomEmoji> emoji;\n\tbase::flat_map<int, base::flat_map<float64, QImage>> emojis;\n};\n\nstruct UniqueGiftCoverWidget::ModelView {\n\tstd::shared_ptr<Data::DocumentMedia> media;\n\tstd::unique_ptr<Lottie::SinglePlayer> lottie;\n\trpl::lifetime lifetime;\n};\n\nstruct UniqueGiftCoverWidget::GiftView {\n\tstd::optional<Data::UniqueGift> gift;\n\tBackdropView backdrop;\n\tPatternView pattern;\n\tModelView model;\n\tbool forced = false;\n};\n\nstruct UniqueGiftCoverWidget::State {\n\tstd::shared_ptr<Data::GiftUpgradeSpinner> spinner;\n\tFn<void()> checkSpinnerStart;\n\tGiftView now;\n\tGiftView next;\n\tAnimations::Simple crossfade;\n\tAnimations::Simple heightAnimation;\n\tstd::vector<BackdropView> spinnerBackdrops;\n\tstd::vector<PatternView> spinnerPatterns;\n\tstd::vector<ModelView> spinnerModels;\n\tAttributeSpin backdropSpin = kBackdropSpinDuration;\n\tAttributeSpin patternSpin = kPatternSpinDuration;\n\tAttributeSpin modelSpin = kModelSpinDuration;\n\tcrl::time spinStarted = 0;\n\tint heightFinal = 0;\n\tbool crossfading = false;\n\tbool updateAttributesPending = false;\n\n\tReleased released;\n\n\tFlatLabel *pretitle = nullptr;\n\tFlatLabel *title = nullptr;\n\tRpWidget *attrs = nullptr;\n\n\tFn<void(const Data::UniqueGift &)> updateAttrs;\n\tFn<void(float64)> updateColors;\n\tFn<void(const BackdropView &, const BackdropView &, float64)>\n\t\tupdateColorsFromBackdrops;\n\tFn<void(ModelView &, const Data::UniqueGiftModel &)> setupModel;\n\tFn<void(PatternView &, const Data::UniqueGiftPattern &)> setupPattern;\n\n\tFlatLabel *number = nullptr;\n\trpl::variable<int> numberTextWidth;\n\tQImage craftedBadge;\n\tInfo::PeerGifts::GiftBadge craftedBadgeKey;\n\trpl::variable<CreditsAmount> resaleAmount;\n\tFn<void()> resaleClick;\n\n};\n\nUniqueGiftCoverWidget::UniqueGiftCoverWidget(\n\tQWidget *parent,\n\trpl::producer<UniqueGiftCover> data,\n\tUniqueGiftCoverArgs &&args)\n: RpWidget(parent)\n, _state(std::make_unique<State>()) {\n\tusing SpinnerState = Data::GiftUpgradeSpinner::State;\n\n\t_state->spinner = std::move(args.upgradeSpinner);\n\n\t_state->setupModel = [this](\n\t\t\tModelView &to,\n\t\t\tconst Data::UniqueGiftModel &model) {\n\t\tto.lifetime.destroy();\n\n\t\tconst auto document = model.document;\n\t\tto.media = document->createMediaView();\n\t\tto.media->automaticLoad(document->stickerSetOrigin(), nullptr);\n\t\trpl::single() | rpl::then(\n\t\t\tdocument->session().downloaderTaskFinished()\n\t\t) | rpl::filter([&to] {\n\t\t\treturn to.media->loaded();\n\t\t}) | rpl::on_next([this, &to] {\n\t\t\tconst auto lottieSize = st::creditsHistoryEntryStarGiftSize;\n\t\t\tto.lottie = ChatHelpers::LottiePlayerFromDocument(\n\t\t\t\tto.media.get(),\n\t\t\t\tChatHelpers::StickerLottieSize::MessageHistory,\n\t\t\t\tQSize(lottieSize, lottieSize),\n\t\t\t\tLottie::Quality::High);\n\n\t\t\tto.lifetime.destroy();\n\t\t\tconst auto lottie = to.lottie.get();\n\t\t\tlottie->updates() | rpl::on_next([this, lottie] {\n\t\t\t\tif (_state->now.model.lottie.get() == lottie\n\t\t\t\t\t|| _state->crossfade.animating()) {\n\t\t\t\t\tupdate();\n\t\t\t\t}\n\t\t\t\tif (const auto onstack = _state->checkSpinnerStart) {\n\t\t\t\t\tonstack();\n\t\t\t\t}\n\t\t\t}, to.lifetime);\n\t\t}, to.lifetime);\n\t};\n\n\t_state->setupPattern = [this](\n\t\t\tPatternView &to,\n\t\t\tconst Data::UniqueGiftPattern &pattern) {\n\t\tconst auto document = pattern.document;\n\t\tconst auto callback = [this, document] {\n\t\t\tif (_state->now.pattern.document == document\n\t\t\t\t|| _state->crossfade.animating()) {\n\t\t\t\tupdate();\n\t\t\t}\n\t\t\tif (const auto onstack = _state->checkSpinnerStart) {\n\t\t\t\tonstack();\n\t\t\t}\n\t\t};\n\t\tto.document = document;\n\t\tto.emoji = document->owner().customEmojiManager().create(\n\t\t\tdocument,\n\t\t\tcallback,\n\t\t\tData::CustomEmojiSizeTag::Large);\n\t\t[[maybe_unused]] const auto preload = to.emoji->ready();\n\t};\n\n\tif (const auto spinner = _state->spinner.get()) {\n\t\tconst auto fillBackdrops = [this, spinner] {\n\t\t\t_state->spinnerBackdrops.clear();\n\t\t\t_state->spinnerBackdrops.reserve(kSpinnerBackdrops);\n\t\t\tconst auto push = [this](const Data::UniqueGiftBackdrop &backdrop) {\n\t\t\t\tif (_state->spinnerBackdrops.size() >= kSpinnerBackdrops) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst auto already = ranges::contains(\n\t\t\t\t\t_state->spinnerBackdrops,\n\t\t\t\t\tbackdrop,\n\t\t\t\t\t&BackdropView::colors);\n\t\t\t\tif (!already) {\n\t\t\t\t\t_state->spinnerBackdrops.push_back({ backdrop });\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t};\n\t\t\tpush(spinner->target->backdrop);\n\t\t\tfor (const auto &backdrop : spinner->attributes.backdrops) {\n\t\t\t\tif (!push(backdrop)) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tconst auto fillPatterns = [this, spinner] {\n\t\t\t_state->spinnerPatterns.clear();\n\t\t\t_state->spinnerPatterns.reserve(kSpinnerPatterns);\n\t\t\tconst auto push = [this](const Data::UniqueGiftPattern &pattern) {\n\t\t\t\tif (_state->spinnerPatterns.size() >= kSpinnerPatterns) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst auto already = ranges::contains(\n\t\t\t\t\t_state->spinnerPatterns,\n\t\t\t\t\tpattern.document.get(),\n\t\t\t\t\t&PatternView::document);\n\t\t\t\tif (!already) {\n\t\t\t\t\t_state->setupPattern(\n\t\t\t\t\t\t_state->spinnerPatterns.emplace_back(),\n\t\t\t\t\t\tpattern);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t};\n\t\t\tpush(spinner->target->pattern);\n\t\t\tfor (const auto &pattern : spinner->attributes.patterns) {\n\t\t\t\tif (!push(pattern)) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tconst auto fillModels = [this, spinner] {\n\t\t\t_state->spinnerModels.clear();\n\t\t\t_state->spinnerModels.reserve(kSpinnerModels);\n\t\t\tconst auto push = [this](const Data::UniqueGiftModel &model) {\n\t\t\t\tif (_state->spinnerModels.size() >= kSpinnerModels) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst auto already = ranges::contains(\n\t\t\t\t\t_state->spinnerModels,\n\t\t\t\t\tmodel.document,\n\t\t\t\t\t[](const ModelView &view) {\n\t\t\t\t\t\treturn view.media->owner();\n\t\t\t\t\t});\n\t\t\t\tif (!already) {\n\t\t\t\t\t_state->setupModel(\n\t\t\t\t\t\t_state->spinnerModels.emplace_back(),\n\t\t\t\t\t\tmodel);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t};\n\t\t\tpush(spinner->target->model);\n\t\t\tfor (const auto &model : spinner->attributes.models) {\n\t\t\t\tif (!push(model)) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\t_state->checkSpinnerStart = [this, spinner] {\n\t\t\tif (spinner->state.current() != SpinnerState::Loading\n\t\t\t\t|| _state->crossfading) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfor (const auto &pattern : _state->spinnerPatterns) {\n\t\t\t\tif (!pattern.emoji->ready()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const auto &model : _state->spinnerModels) {\n\t\t\t\tif (!model.lottie || !model.lottie->ready()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tspinner->state = SpinnerState::Prepared;\n\t\t\t_state->checkSpinnerStart = nullptr;\n\t\t};\n\t\tspinner->state.value() | rpl::on_next([=](SpinnerState now) {\n\t\t\tif (now == SpinnerState::Preparing) {\n\t\t\t\tfillBackdrops();\n\t\t\t\tfillPatterns();\n\t\t\t\tfillModels();\n\t\t\t\tspinner->state = SpinnerState::Loading;\n\t\t\t\tif (!_state->crossfading) {\n\t\t\t\t\t_state->next = {};\n\t\t\t\t}\n\t\t\t\t_state->checkSpinnerStart();\n\t\t\t} else if (now == SpinnerState::Started) {\n\t\t\t\tconst auto repaint = [this] { update(); };\n\t\t\t\t_state->backdropSpin.startWithin(\n\t\t\t\t\t_state->spinnerBackdrops.size(),\n\t\t\t\t\trepaint);\n\t\t\t\t_state->patternSpin.startWithin(\n\t\t\t\t\t_state->spinnerPatterns.size(),\n\t\t\t\t\trepaint);\n\t\t\t\t_state->modelSpin.startWithin(\n\t\t\t\t\t_state->spinnerModels.size(),\n\t\t\t\t\trepaint);\n\t\t\t\t_state->spinStarted = crl::now();\n\t\t\t}\n\t\t}, lifetime());\n\t}\n\n\trpl::duplicate(\n\t\tdata\n\t) | rpl::on_next([this](const UniqueGiftCover &now) {\n\t\tconst auto setup = [&](GiftView &to) {\n\t\t\tExpects(!now.spinner);\n\n\t\t\tto = {};\n\t\t\tto.gift = now.values;\n\t\t\tto.forced = now.force;\n\t\t\tto.backdrop.colors = now.values.backdrop;\n\t\t\t_state->setupModel(to.model, now.values.model);\n\t\t\t_state->setupPattern(to.pattern, now.values.pattern);\n\t\t};\n\n\t\tconst auto spinner = _state->spinner.get();\n\t\tif (!_state->now.gift) {\n\t\t\tsetup(_state->now);\n\t\t\tupdate();\n\t\t} else if (now.spinner) {\n\t\t\tAssert(spinner != nullptr);\n\t\t\tAssert(spinner->state.current() == SpinnerState::Prepared);\n\n\t\t\tspinner->state = SpinnerState::Started;\n\t\t} else if (!_state->next.gift || now.force) {\n\t\t\tconst auto spinnerState = spinner\n\t\t\t\t? spinner->state.current()\n\t\t\t\t: SpinnerState::Initial;\n\t\t\tif (spinnerState == SpinnerState::Initial) {\n\t\t\t\tsetup(_state->next);\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\tauto subtitleCustomText = args.subtitle\n\t\t? std::move(args.subtitle)\n\t\t: rpl::single(tr::marked());\n\t_state->released.subtitleCustom = rpl::duplicate(\n\t\tsubtitleCustomText\n\t) | rpl::map([](const TextWithEntities &custom) {\n\t\treturn !custom.empty();\n\t});\n\n\tconst auto repaintedHook = args.repaintedHook;\n\tconst auto updateLinkFg = args.subtitleLinkColored;\n\t_state->updateColorsFromBackdrops = [this, updateLinkFg](\n\t\t\tconst BackdropView &from,\n\t\t\tconst BackdropView &to,\n\t\t\tfloat64 progress) {\n\t\t_state->released.bg = (progress == 0.)\n\t\t\t? from.colors.patternColor\n\t\t\t: (progress == 1.)\n\t\t\t? to.colors.patternColor\n\t\t\t: anim::color(\n\t\t\t\tfrom.colors.patternColor,\n\t\t\t\tto.colors.patternColor,\n\t\t\t\tprogress);\n\t\tconst auto color = (progress == 0.)\n\t\t\t? from.colors.textColor\n\t\t\t: (progress == 1.)\n\t\t\t? to.colors.textColor\n\t\t\t: anim::color(\n\t\t\t\tfrom.colors.textColor,\n\t\t\t\tto.colors.textColor,\n\t\t\t\tprogress);\n\t\tif (updateLinkFg) {\n\t\t\tconst auto &subtitleCustom = _state->released.subtitleCustom;\n\t\t\t_state->released.link.update(subtitleCustom.current()\n\t\t\t\t? color\n\t\t\t\t: QColor(255, 255, 255));\n\t\t}\n\t\t_state->released.fg = color;\n\t\tconst auto onSale = !_state->released.subtitleCustom.current()\n\t\t\t&& _state->resaleAmount.current()\n\t\t\t&& _state->resaleAmount.current().value() > 0;\n\t\t_state->released.subtitle->setTextColorOverride(\n\t\t\tonSale ? QColor(255, 255, 255) : color);\n\t\tif (_state->number) {\n\t\t\t_state->number->setTextColorOverride(color);\n\t\t}\n\t};\n\t_state->updateColors = [this, repaintedHook](float64 progress) {\n\t\tif (repaintedHook) {\n\t\t\trepaintedHook(_state->now.gift, _state->next.gift, progress);\n\t\t}\n\t\t_state->updateColorsFromBackdrops(\n\t\t\t_state->now.backdrop,\n\t\t\t_state->next.backdrop,\n\t\t\tprogress);\n\t};\n\n\tif (args.resalePrice) {\n\t\tstd::move(args.resalePrice) | rpl::on_next([this](CreditsAmount value) {\n\t\t\t_state->resaleAmount = value;\n\t\t}, lifetime());\n\t}\n\t_state->resaleClick = std::move(args.resaleClick);\n\n\t_state->pretitle = args.pretitle\n\t\t? CreateChild<FlatLabel>(\n\t\t\tthis,\n\t\t\tstd::move(args.pretitle),\n\t\t\tst::uniqueGiftPretitle)\n\t\t: nullptr;\n\tif (_state->pretitle) {\n\t\t_state->released.stars.emplace(\n\t\t\tthis,\n\t\t\ttrue,\n\t\t\tPremium::MiniStarsType::SlowStars);\n\t\tconst auto white = QColor(255, 255, 255);\n\t\t_state->released.stars->setColorOverride(QGradientStops{\n\t\t\t{ 0., anim::with_alpha(white, .3) },\n\t\t\t{ 1., white },\n\t\t});\n\t\t_state->pretitle->geometryValue() | rpl::on_next([this](QRect rect) {\n\t\t\tconst auto half = rect.height() / 2;\n\t\t\t_state->released.stars->setCenter(rect - QMargins(half, 0, half, 0));\n\t\t}, _state->pretitle->lifetime());\n\t\t_state->pretitle->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t_state->pretitle->setTextColorOverride(QColor(255, 255, 255));\n\t\t_state->pretitle->paintOn([this](QPainter &p) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tconst auto radius = _state->pretitle->height() / 2.;\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tauto bg = _state->released.bg;\n\t\t\tbg.setAlphaF(kGradientButtonBgOpacity * bg.alphaF());\n\t\t\tp.setBrush(bg);\n\t\t\tp.drawRoundedRect(_state->pretitle->rect(), radius, radius);\n\t\t\tp.translate(-_state->pretitle->pos());\n\t\t\t_state->released.stars->paint(p);\n\t\t});\n\t}\n\n\t_state->title = CreateChild<FlatLabel>(\n\t\tthis,\n\t\trpl::duplicate(\n\t\t\tdata\n\t\t) | rpl::map([](const UniqueGiftCover &now) {\n\t\t\treturn now.values.title;\n\t\t}),\n\t\tst::uniqueGiftTitle);\n\t_state->title->setTextColorOverride(QColor(255, 255, 255));\n\n\tauto numberTextDup = args.numberText\n\t\t? rpl::duplicate(args.numberText)\n\t\t: rpl::producer<QString>();\n\t_state->number = args.numberText\n\t\t? CreateChild<FlatLabel>(\n\t\t\tthis,\n\t\t\tstd::move(args.numberText),\n\t\t\tst::uniqueGiftNumber)\n\t\t: nullptr;\n\tif (_state->number) {\n\t\t_state->number->setTextColorOverride(QColor(255, 255, 255));\n\t\tstd::move(\n\t\t\tnumberTextDup\n\t\t) | rpl::on_next([this](const QString &text) {\n\t\t\t_state->numberTextWidth = text.isEmpty() ? 0 : 1;\n\t\t}, lifetime());\n\t}\n\n\t_state->released.by = rpl::duplicate(\n\t\tdata\n\t) | rpl::map([](const UniqueGiftCover &cover) {\n\t\treturn cover.values.releasedBy;\n\t});\n\t_state->released.subtitleText = rpl::combine(\n\t\tstd::move(subtitleCustomText),\n\t\t_state->resaleAmount.value(),\n\t\trpl::duplicate(data)\n\t) | rpl::map([](\n\t\t\tTextWithEntities custom,\n\t\t\tCreditsAmount resalePrice,\n\t\t\tconst UniqueGiftCover &cover) {\n\t\tif (!custom.empty()) {\n\t\t\treturn custom;\n\t\t}\n\t\tconst auto &gift = cover.values;\n\t\tif (resalePrice && resalePrice.value() > 0) {\n\t\t\tauto priceText = resalePrice.ton()\n\t\t\t\t? Text::IconEmoji(&st::tonIconEmojiInSmall).append(\n\t\t\t\t\tLang::FormatCreditsAmountDecimal(resalePrice))\n\t\t\t\t: Text::IconEmoji(&st::starIconEmojiSmall).append(\n\t\t\t\t\tLang::FormatCountDecimal(resalePrice.whole()));\n\t\t\treturn tr::lng_gift_on_sale_for(\n\t\t\t\ttr::now,\n\t\t\t\tlt_price,\n\t\t\t\tpriceText,\n\t\t\t\ttr::marked);\n\t\t}\n\t\tif (gift.releasedBy) {\n\t\t\treturn tr::lng_gift_released_by(\n\t\t\t\ttr::now,\n\t\t\t\tlt_name,\n\t\t\t\ttr::link('@' + gift.releasedBy->username()),\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn tr::marked(gift.model.name);\n\t});\n\n\tconst auto subtitleOutlined = args.subtitleOutlined;\n\tconst auto subtitleClick = args.subtitleClick;\n\trpl::combine(\n\t\t_state->released.by.value(),\n\t\t_state->released.subtitleCustom.value(),\n\t\t_state->resaleAmount.value()\n\t) | rpl::on_next([=](PeerData *by, bool subtitleCustom, CreditsAmount resale) {\n\t\tconst auto hasResale = resale && resale.value() > 0;\n\t\t_state->released.outlined = (subtitleCustom && subtitleOutlined)\n\t\t\t|| (!subtitleCustom && (hasResale || by));\n\t\t_state->released.st = !_state->released.outlined\n\t\t\t? st::uniqueGiftSubtitle\n\t\t\t: (!subtitleCustom && hasResale)\n\t\t\t? st::uniqueGiftOnSale\n\t\t\t: st::uniqueGiftReleasedBy;\n\t\t_state->released.st.palette.linkFg = _state->released.link.color();\n\n\t\tconst auto session = &_state->now.gift->model.document->session();\n\t\t_state->released.subtitle = base::make_unique_q<FlatLabel>(\n\t\t\tthis,\n\t\t\t_state->released.subtitleText.value(),\n\t\t\t_state->released.st,\n\t\t\tst::defaultPopupMenu,\n\t\t\tCore::TextContext({ .session = session }));\n\t\tconst auto subtitle = _state->released.subtitle.get();\n\t\tsubtitle->show();\n\n\t\twidthValue(\n\t\t) | rpl::on_next([=](int width) {\n\t\t\tconst auto skip = st::uniqueGiftBottom;\n\t\t\tif (width <= 3 * skip) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto available = width - 2 * skip;\n\t\t\tsubtitle->resizeToWidth(available);\n\t\t}, subtitle->lifetime());\n\t\t_state->released.subtitleHeight.force_assign(subtitle->height());\n\t\t_state->released.subtitleHeight = subtitle->heightValue();\n\n\t\tconst auto handler = subtitleCustom\n\t\t\t? (subtitleClick ? subtitleClick : Fn<void()>())\n\t\t\t: hasResale\n\t\t\t? _state->resaleClick\n\t\t\t: by\n\t\t\t? Fn<void()>([=] { GiftReleasedByHandler(by); })\n\t\t\t: Fn<void()>();\n\n\t\tif (!_state->released.outlined && handler) {\n\t\t\tsubtitle->setClickHandlerFilter([=](const auto &...) {\n\t\t\t\thandler();\n\t\t\t\treturn false;\n\t\t\t});\n\t\t} else if (_state->released.outlined) {\n\t\t\t_state->released.subtitleButton = base::make_unique_q<AbstractButton>(\n\t\t\t\tthis);\n\t\t\tconst auto button = _state->released.subtitleButton.get();\n\n\t\t\tbutton->show();\n\t\t\tsubtitle->raise();\n\t\t\tsubtitle->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t\t\tif (handler) {\n\t\t\t\tbutton->setClickedCallback(handler);\n\t\t\t} else {\n\t\t\t\tbutton->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\t}\n\t\t\tsubtitle->geometryValue(\n\t\t\t) | rpl::on_next([=](QRect geometry) {\n\t\t\t\tbutton->setGeometry(\n\t\t\t\t\tgeometry.marginsAdded(st::giftBoxReleasedByMargin));\n\t\t\t}, button->lifetime());\n\t\t\tbutton->paintRequest() | rpl::on_next([=] {\n\t\t\t\tauto p = QPainter(button);\n\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\tconst auto use = subtitle->textMaxWidth();\n\t\t\t\tconst auto add = button->width() - subtitle->width();\n\t\t\t\tconst auto full = use + add;\n\t\t\t\tconst auto left = (button->width() - full) / 2;\n\t\t\t\tconst auto height = button->height();\n\t\t\t\tconst auto radius = height / 2.;\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(_state->released.bg);\n\t\t\t\tp.setOpacity(0.5);\n\t\t\t\tp.drawRoundedRect(left, 0, full, height, radius, radius);\n\t\t\t}, button->lifetime());\n\t\t} else {\n\t\t\t_state->released.subtitleButton = nullptr;\n\t\t}\n\t\t_state->updateColors(_state->crossfade.value(\n\t\t\t_state->crossfading ? 1. : 0.));\n\t}, _state->title->lifetime());\n\n\t_state->attrs = args.attributesInfo\n\t\t? CreateChild<RpWidget>(this)\n\t\t: nullptr;\n\t_state->updateAttrs = [](const Data::UniqueGift &) {};\n\tif (_state->attrs) {\n\t\tstruct AttributeState {\n\t\t\tText::String name;\n\t\t\tText::String type;\n\t\t\tText::String percent;\n\t\t};\n\t\tstruct AttributesState {\n\t\t\tAttributeState model;\n\t\t\tAttributeState pattern;\n\t\t\tAttributeState backdrop;\n\t\t};\n\t\tconst auto astate = lifetime().make_state<AttributesState>();\n\t\tconst auto setType = [&](AttributeState &state, tr::phrase<> text) {\n\t\t\tstate.type = Text::String(\n\t\t\t\tst::uniqueAttributeType,\n\t\t\t\ttext(tr::now));\n\t\t};\n\t\tsetType(astate->model, tr::lng_auction_preview_model);\n\t\tsetType(astate->pattern, tr::lng_auction_preview_symbol);\n\t\tsetType(astate->backdrop, tr::lng_auction_preview_backdrop);\n\n\t\t_state->updateAttrs = [this, astate](const Data::UniqueGift &gift) {\n\t\t\tconst auto set = [&](\n\t\t\t\t\tAttributeState &state,\n\t\t\t\t\tconst Data::UniqueGiftAttribute &value) {\n\t\t\t\tstate.name = Text::String(\n\t\t\t\t\tst::uniqueAttributeName,\n\t\t\t\t\tvalue.name);\n\t\t\t\tstate.percent = Text::String(\n\t\t\t\t\tst::uniqueAttributePercent,\n\t\t\t\t\tData::UniqueGiftAttributeText(value));\n\t\t\t};\n\t\t\tset(astate->model, gift.model);\n\t\t\tset(astate->pattern, gift.pattern);\n\t\t\tset(astate->backdrop, gift.backdrop);\n\t\t\t_state->attrs->update();\n\t\t};\n\t\tconst auto attrsHeight = st::uniqueAttributeTop\n\t\t\t+ st::uniqueAttributePadding.top()\n\t\t\t+ st::uniqueAttributeName.font->height\n\t\t\t+ st::uniqueAttributeType.font->height\n\t\t\t+ st::uniqueAttributePadding.bottom();\n\t\t_state->attrs->resize(_state->attrs->width(), attrsHeight);\n\t\t_state->attrs->paintOn([this, astate, attrsHeight](QPainter &p) {\n\t\t\tconst auto boxPadding = st::giftBoxPadding;\n\t\t\tconst auto skip = st::giftBoxGiftSkip.x();\n\t\t\tconst auto available = _state->attrs->width()\n\t\t\t\t- boxPadding.left()\n\t\t\t\t- boxPadding.right()\n\t\t\t\t- 2 * skip;\n\t\t\tconst auto single = available / 3;\n\t\t\tif (single <= 0) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tauto bg = _state->released.bg;\n\t\t\tbg.setAlphaF(kGradientButtonBgOpacity * bg.alphaF());\n\t\t\tconst auto innert = st::uniqueAttributeTop;\n\t\t\tconst auto innerh = attrsHeight - innert;\n\t\t\tconst auto radius = innerh / 3.;\n\t\t\tconst auto paint = [&](int x, const AttributeState &state) {\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(bg);\n\t\t\t\tp.drawRoundedRect(x, innert, single, innerh, radius, radius);\n\t\t\t\tp.setPen(QColor(255, 255, 255));\n\t\t\t\tconst auto padding = st::uniqueAttributePadding;\n\t\t\t\tconst auto inner = single - padding.left() - padding.right();\n\t\t\t\tconst auto namew = std::min(inner, state.name.maxWidth());\n\t\t\t\tstate.name.draw(p, {\n\t\t\t\t\t.position = QPoint(\n\t\t\t\t\t\tx + (single - namew) / 2,\n\t\t\t\t\t\tinnert + padding.top()),\n\t\t\t\t\t.availableWidth = namew,\n\t\t\t\t\t.elisionLines = 1,\n\t\t\t\t});\n\t\t\t\tp.setPen(_state->released.fg);\n\t\t\t\tconst auto typew = std::min(inner, state.type.maxWidth());\n\t\t\t\tstate.type.draw(p, {\n\t\t\t\t\t.position = QPoint(\n\t\t\t\t\t\tx + (single - typew) / 2,\n\t\t\t\t\t\tinnert + padding.top() + state.name.minHeight()),\n\t\t\t\t\t.availableWidth = typew,\n\t\t\t\t});\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(anim::color(_state->released.bg, _state->released.fg, 0.3));\n\t\t\t\tconst auto r = st::uniqueAttributePercent.font->height / 2.;\n\t\t\t\tconst auto left = x + single - state.percent.maxWidth();\n\t\t\t\tconst auto top = st::uniqueAttributePercentPadding.top();\n\t\t\t\tconst auto percent = QRect(\n\t\t\t\t\tleft,\n\t\t\t\t\ttop,\n\t\t\t\t\tstate.percent.maxWidth(),\n\t\t\t\t\tst::uniqueAttributeType.font->height);\n\t\t\t\tp.drawRoundedRect(\n\t\t\t\t\tpercent.marginsAdded(st::uniqueAttributePercentPadding),\n\t\t\t\t\tr,\n\t\t\t\t\tr);\n\t\t\t\tp.setPen(QColor(255, 255, 255));\n\t\t\t\tstate.percent.draw(p, {\n\t\t\t\t\t.position = percent.topLeft(),\n\t\t\t\t});\n\t\t\t};\n\t\t\tauto left = boxPadding.left();\n\t\t\tpaint(left, astate->model);\n\t\t\tpaint(left + single + skip, astate->backdrop);\n\t\t\tpaint(_state->attrs->width() - single - boxPadding.right(), astate->pattern);\n\t\t});\n\t}\n\t_state->updateAttrs(*_state->now.gift);\n\n\trpl::combine(\n\t\twidthValue(),\n\t\t_state->released.subtitleHeight.value(),\n\t\t_state->numberTextWidth.value()\n\t) | rpl::on_next([this](int width, int subtitleHeight, int) {\n\t\tconst auto skip = st::uniqueGiftBottom;\n\t\tif (width <= 3 * skip) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto available = width - 2 * skip;\n\t\tauto top = st::uniqueGiftTitleTop;\n\t\tif (_state->pretitle) {\n\t\t\t_state->title->resizeToWidth(available);\n\t\t\t_state->pretitle->move((width - _state->pretitle->width()) / 2, top);\n\t\t\ttop += _state->pretitle->height()\n\t\t\t\t+ (st::uniqueGiftSubtitleTop - st::uniqueGiftTitleTop)\n\t\t\t\t- _state->title->height();\n\t\t}\n\n\t\tif (_state->number && _state->number->textMaxWidth() > 0) {\n\t\t\tconst auto titleWidth = _state->title->textMaxWidth();\n\t\t\t_state->title->resizeToWidth(titleWidth);\n\t\t\tconst auto numberWidth = _state->number->textMaxWidth();\n\t\t\t_state->number->resizeToWidth(numberWidth);\n\t\t\tconst auto gap = st::normalFont->spacew;\n\t\t\tconst auto totalWidth = titleWidth + gap + numberWidth;\n\t\t\tconst auto groupLeft = (width - totalWidth) / 2;\n\t\t\t_state->title->moveToLeft(groupLeft, top);\n\t\t\tconst auto &stTitle = _state->title->st();\n\t\t\tconst auto &stNumber = _state->number->st();\n\t\t\t_state->number->moveToLeft(\n\t\t\t\tgroupLeft + titleWidth + gap,\n\t\t\t\t(top\n\t\t\t\t\t+ stTitle.style.font->ascent\n\t\t\t\t\t- stNumber.style.font->ascent));\n\t\t} else {\n\t\t\t_state->title->resizeToWidth(available);\n\t\t\t_state->title->moveToLeft(skip, top);\n\t\t}\n\t\tif (_state->pretitle) {\n\t\t\ttop += _state->title->height() + st::defaultVerticalListSkip;\n\t\t} else {\n\t\t\ttop += st::uniqueGiftSubtitleTop - st::uniqueGiftTitleTop;\n\t\t}\n\n\t\t_state->released.subtitle->moveToLeft(skip, top);\n\t\ttop += subtitleHeight + (skip / 2);\n\n\t\tif (_state->attrs) {\n\t\t\t_state->attrs->resizeToWidth(width);\n\t\t\t_state->attrs->moveToLeft(0, top);\n\t\t\ttop += _state->attrs->height() + (skip / 2);\n\t\t} else {\n\t\t\ttop += (skip / 2);\n\t\t}\n\t\tif (!height() || height() == top) {\n\t\t\tresize(width, top);\n\t\t} else {\n\t\t\t_state->heightFinal = top;\n\t\t\t_state->heightAnimation.start([this, width, top] {\n\t\t\t\tresize(\n\t\t\t\t\twidth,\n\t\t\t\t\tint(base::SafeRound(_state->heightAnimation.value(top))));\n\t\t\t}, height(), top, st::slideWrapDuration);\n\t\t}\n\t}, lifetime());\n}\n\nvoid UniqueGiftCoverWidget::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tauto progress = _state->crossfade.value(_state->crossfading ? 1. : 0.);\n\tif (_state->updateAttributesPending && progress >= 0.5) {\n\t\t_state->updateAttributesPending = false;\n\t\t_state->updateAttrs(*_state->next.gift);\n\t} else if (_state->updateAttributesPending\n\t\t&& !_state->crossfade.animating()) {\n\t\t_state->updateAttributesPending = false;\n\t\t_state->updateAttrs(*_state->now.gift);\n\t}\n\tif (_state->crossfading) {\n\t\t_state->updateColors(progress);\n\t}\n\tif (progress == 1.) {\n\t\t_state->crossfading = false;\n\t\t_state->now = base::take(_state->next);\n\t\tprogress = 0.;\n\t}\n\n\tconst auto context = PaintContext{\n\t\t.width = width(),\n\t\t.patternAreaHeight = st::uniqueGiftSubtitleTop,\n\t};\n\tif (_state->spinStarted) {\n\t\tpaintSpinnerAnimation(p, context);\n\t} else {\n\t\tpaintNormalAnimation(p, context, progress);\n\t}\n}\n\nUniqueGiftCoverWidget::~UniqueGiftCoverWidget() = default;\n\nQImage UniqueGiftCoverWidget::prepareBackdrop(\n\t\tBackdropView &backdrop,\n\t\tconst PaintContext &context) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto gradientSize = QSize(\n\t\tcontext.width,\n\t\tstd::max(height(), _state->heightFinal));\n\tif (backdrop.gradient.size() != gradientSize * ratio) {\n\t\tbackdrop.gradient = CreateTopBgGradient(\n\t\t\tgradientSize,\n\t\t\tbackdrop.colors);\n\t}\n\treturn backdrop.gradient;\n}\n\nvoid UniqueGiftCoverWidget::paintBackdrop(\n\t\tQPainter &p,\n\t\tBackdropView &backdrop,\n\t\tconst PaintContext &context) {\n\tp.drawImage(0, 0, prepareBackdrop(backdrop, context));\n}\n\nvoid UniqueGiftCoverWidget::paintPattern(\n\t\tQPainter &p,\n\t\tPatternView &pattern,\n\t\tconst BackdropView &backdrop,\n\t\tconst PaintContext &context,\n\t\tfloat64 shown) {\n\tconst auto color = backdrop.colors.patternColor;\n\tconst auto key = (color.red() << 16)\n\t\t| (color.green() << 8)\n\t\t| color.blue();\n\tPaintBgPoints(\n\t\tp,\n\t\tPatternBgPoints(),\n\t\tpattern.emojis[key],\n\t\tpattern.emoji.get(),\n\t\tcolor,\n\t\tQRect(0, 0, context.width, context.patternAreaHeight),\n\t\tshown);\n}\n\nbool UniqueGiftCoverWidget::paintModel(\n\t\tQPainter &p,\n\t\tModelView &model,\n\t\tconst PaintContext &context,\n\t\tfloat64 scale,\n\t\tbool paused) {\n\tconst auto lottieSize = st::creditsHistoryEntryStarGiftSize;\n\tconst auto lottie = model.lottie.get();\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto request = Lottie::FrameRequest{\n\t\t.box = QSize(lottieSize, lottieSize) * factor,\n\t};\n\tconst auto frame = (lottie && lottie->ready())\n\t\t? lottie->frameInfo(request)\n\t\t: Lottie::Animation::FrameInfo();\n\tif (frame.image.isNull()) {\n\t\treturn false;\n\t}\n\tconst auto size = frame.image.size() / factor;\n\tconst auto rect = QRect(\n\t\tQPoint((context.width - size.width()) / 2, st::uniqueGiftModelTop),\n\t\tsize);\n\tif (scale < 1.) {\n\t\tconst auto origin = rect.center();\n\t\tp.translate(origin);\n\t\tp.scale(scale, scale);\n\t\tp.translate(-origin);\n\t}\n\tp.drawImage(rect, frame.image);\n\tconst auto count = lottie->framesCount();\n\tconst auto finished = lottie->frameIndex() == (count - 1);\n\tif (!paused) {\n\t\tlottie->markFrameShown();\n\t}\n\treturn finished;\n}\n\nQRect UniqueGiftCoverWidget::prepareCraftFrame(\n\t\tQImage &canvas,\n\t\tconst CraftContext &context) {\n\tconst auto full = this->size();\n\tconst auto ratio = style::DevicePixelRatio();\n\n\tif (canvas.size() != full * ratio) {\n\t\tcanvas = QImage(full * ratio, QImage::Format_ARGB32_Premultiplied);\n\t\tcanvas.setDevicePixelRatio(ratio);\n\t}\n\n\tLOG((\"FULL: %1x%2\").arg(full.width()).arg(full.height()));\n\n\tconst auto expand = context.expandProgress;\n\tconst auto size = QSize(\n\t\tanim::interpolate(context.initial.width(), full.width(), expand),\n\t\tanim::interpolate(context.initial.height(), full.height(), expand));\n\tconst auto rect = QRect(QPoint(), size);\n\tauto p = QPainter(&canvas);\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto radius = st::boxRadius * expand;\n\tconst auto bgProgress = context.fadeInProgress;\n\n\tauto gradient = QRadialGradient(\n\t\trect.center(),\n\t\tsize.height() / 2);\n\tconst auto center = anim::color(\n\t\tcontext.initialBg,\n\t\t_state->now.backdrop.colors.centerColor,\n\t\tbgProgress);\n\tconst auto edge = anim::color(\n\t\tcontext.initialBg,\n\t\t_state->now.backdrop.colors.edgeColor,\n\t\tbgProgress);\n\tgradient.setStops({ { 0., center }, { 1., edge } });\n\tp.setPen(Qt::NoPen);\n\tif (radius > 0.) {\n\t\tconst auto more = qCeil(radius * 2);\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tp.fillRect(0, 0, more, more, Qt::transparent);\n\t\tp.fillRect(rect.width() - more, 0, more, more, Qt::transparent);\n\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\tp.setBrush(gradient);\n\t\tp.drawRoundedRect(\n\t\t\trect.marginsAdded({ 0, 0, 0, more }),\n\t\t\tradius,\n\t\t\tradius);\n\t} else {\n\t\tp.fillRect(rect, gradient);\n\t}\n\n\tconst auto lottieSize = st::creditsHistoryEntryStarGiftSize;\n\tconst auto paused = (expand == 0.);\n\tconst auto modelScale = expand\n\t\t+ ((st::giftBoxStickerTiny.width() / float64(lottieSize))\n\t\t\t* (1. - expand));\n\tconst auto finalShift = (size.height() - lottieSize) / 2;\n\tconst auto shift = (1. - expand) * (finalShift - st::uniqueGiftModelTop);\n\tp.translate(0, shift);\n\tpaintModel(p, _state->now.model, {\n\t\t.width = size.width(),\n\t}, modelScale, paused);\n\tp.translate(0, -shift);\n\n\tconst auto skip = anim::interpolate(size.width() / 3, 0, expand);\n\tconst auto pointsHeight = anim::interpolate(\n\t\tcontext.initial.height(),\n\t\tst::uniqueGiftSubtitleTop,\n\t\texpand);\n\tp.translate(-skip, 0);\n\tpaintPattern(p, _state->now.pattern, _state->now.backdrop, {\n\t\t.width = size.width() + 2 * skip,\n\t\t.patternAreaHeight = pointsHeight,\n\t}, bgProgress);\n\n\treturn QRect(QPoint(), size * ratio);\n}\n\nbool UniqueGiftCoverWidget::paintGift(\n\t\tQPainter &p,\n\t\tGiftView &gift,\n\t\tconst PaintContext &context,\n\t\tfloat64 shown) {\n\tExpects(gift.gift.has_value());\n\n\tpaintBackdrop(p, gift.backdrop, context);\n\tif (gift.gift->pattern.document != gift.gift->model.document) {\n\t\tpaintPattern(p, gift.pattern, gift.backdrop, context, shown);\n\t}\n\tconst auto finished = paintModel(p, gift.model, context);\n\tif (gift.gift->crafted) {\n\t\tconst auto padding = st::chatUniqueGiftBadgePadding;\n\t\tauto badge = Info::PeerGifts::GiftBadge{\n\t\t\t.text = tr::lng_gift_crafted_tag(tr::now),\n\t\t\t.bg1 = gift.gift->backdrop.edgeColor,\n\t\t\t.bg2 = gift.gift->backdrop.patternColor,\n\t\t\t.fg = gift.gift->backdrop.textColor,\n\t\t};\n\t\tif (_state->craftedBadge.isNull()\n\t\t\t|| _state->craftedBadgeKey != badge) {\n\t\t\t_state->craftedBadgeKey = badge;\n\t\t\t_state->craftedBadge = Info::PeerGifts::ValidateRotatedBadge(\n\t\t\t\tbadge, padding, true);\n\t\t}\n\t\tp.drawImage(0, 0, _state->craftedBadge);\n\t}\n\treturn finished;\n}\n\nQColor UniqueGiftCoverWidget::backgroundColor() const {\n\treturn _state->released.bg;\n}\n\nQColor UniqueGiftCoverWidget::foregroundColor() const {\n\treturn _state->released.fg;\n}\n\nvoid UniqueGiftCoverWidget::paintSpinnerAnimation(\n\t\tQPainter &p,\n\t\tconst PaintContext &context) {\n\tusing SpinnerState = Data::GiftUpgradeSpinner::State;\n\n\tconst auto select = [&](auto &&list, int index, auto &fallback)\n\t-> decltype(auto) {\n\t\treturn (index >= 0) ? list[index] : fallback;\n\t};\n\n\tconst auto now = crl::now();\n\tconst auto elapsed = now - _state->spinStarted;\n\tauto current = _state->spinner->state.current();\n\tconst auto stateTo = [&](SpinnerState to) {\n\t\tif (int(current) < int(to)) {\n\t\t\t_state->spinner->state = current = to;\n\t\t}\n\t};\n\tconst auto switchTo = [&](SpinnerState to, crl::time at) {\n\t\tif (elapsed >= at) {\n\t\t\tstateTo(to);\n\t\t}\n\t};\n\tconst auto actualize = [&](\n\t\t\tAttributeSpin &spin,\n\t\t\tauto &&list,\n\t\t\tSpinnerState checkState,\n\t\t\tSpinnerState finishState,\n\t\t\tint slowdown = 1) {\n\t\tif (spin.progress() < 1.) {\n\t\t\treturn;\n\t\t} else if (current >= checkState) {\n\t\t\tspin.startToTarget([this] { update(); }, slowdown);\n\t\t\tstateTo(finishState);\n\t\t} else {\n\t\t\tspin.startWithin(list.size(), [this] { update(); });\n\t\t}\n\t};\n\tif (anim::Disabled() && current < SpinnerState::FinishedModel) {\n\t\tstateTo(SpinnerState::FinishedModel);\n\t\t_state->backdropSpin.startToTarget([this] { update(); });\n\t\t_state->patternSpin.startToTarget([this] { update(); });\n\t\t_state->modelSpin.startToTarget([this] { update(); });\n\t}\n\tif (_state->backdropSpin.willIndex != 0) {\n\t\tswitchTo(SpinnerState::FinishBackdrop, kBackdropStopsAt);\n\t}\n\tactualize(\n\t\t_state->backdropSpin,\n\t\t_state->spinnerBackdrops,\n\t\tSpinnerState::FinishBackdrop,\n\t\tSpinnerState::FinishedBackdrop);\n\n\tif (current == SpinnerState::FinishedBackdrop\n\t\t&& _state->patternSpin.willIndex != 0) {\n\t\tswitchTo(SpinnerState::FinishPattern, kPatternStopsAt);\n\t}\n\tactualize(\n\t\t_state->patternSpin,\n\t\t_state->spinnerPatterns,\n\t\tSpinnerState::FinishPattern,\n\t\tSpinnerState::FinishedPattern);\n\n\tif (current == SpinnerState::FinishedPattern\n\t\t&& _state->modelSpin.willIndex != 0) {\n\t\tswitchTo(SpinnerState::FinishModel, kModelStopsAt);\n\t}\n\tactualize(\n\t\t_state->modelSpin,\n\t\t_state->spinnerModels,\n\t\tSpinnerState::FinishModel,\n\t\tSpinnerState::FinishedModel,\n\t\t2);\n\n\tauto &backdropNow = select(\n\t\t_state->spinnerBackdrops,\n\t\t_state->backdropSpin.nowIndex,\n\t\t_state->now.backdrop);\n\tauto &backdropWill = select(\n\t\t_state->spinnerBackdrops,\n\t\t_state->backdropSpin.willIndex,\n\t\t_state->now.backdrop);\n\tconst auto backdropProgress = _state->backdropSpin.progress();\n\t_state->updateColorsFromBackdrops(\n\t\tbackdropNow,\n\t\tbackdropWill,\n\t\tbackdropProgress);\n\n\tauto &patternNow = select(\n\t\t_state->spinnerPatterns,\n\t\t_state->patternSpin.nowIndex,\n\t\t_state->now.pattern);\n\tauto &patternWill = select(\n\t\t_state->spinnerPatterns,\n\t\t_state->patternSpin.willIndex,\n\t\t_state->now.pattern);\n\tconst auto patternProgress = _state->patternSpin.progress();\n\tconst auto paintPatterns = [&](\n\t\t\tQPainter &p,\n\t\t\tconst BackdropView &backdrop) {\n\t\tif (patternProgress >= 1.) {\n\t\t\tpaintPattern(p, patternWill, backdrop, context, 1.);\n\t\t} else {\n\t\t\tpaintPattern(p, patternNow, backdrop, context, 1. - patternProgress);\n\t\t\tif (patternProgress > 0.) {\n\t\t\t\tpaintPattern(p, patternWill, backdrop, context, patternProgress);\n\t\t\t}\n\t\t}\n\t};\n\n\tif (backdropProgress >= 1.) {\n\t\tp.drawImage(0, 0, prepareBackdrop(backdropWill, context));\n\t\tpaintPatterns(p, backdropWill);\n\t} else {\n\t\tp.drawImage(0, 0, prepareBackdrop(backdropNow, context));\n\t\tpaintPatterns(p, backdropNow);\n\n\t\tconst auto fade = context.width / 2;\n\t\tconst auto from = anim::interpolate(\n\t\t\t-fade,\n\t\t\tcontext.width,\n\t\t\tbackdropProgress);\n\t\tif (const auto till = from + fade; till > 0) {\n\t\t\tauto faded = prepareBackdrop(backdropWill, context);\n\t\t\tauto q = QPainter(&faded);\n\t\t\tpaintPatterns(q, backdropWill);\n\n\t\t\tq.setCompositionMode(\n\t\t\t\tQPainter::CompositionMode_DestinationIn);\n\t\t\tauto brush = QLinearGradient(from, 0, till, 0);\n\t\t\tbrush.setStops({\n\t\t\t\t{ 0., QColor(255, 255, 255, 255) },\n\t\t\t\t{ 1., QColor(255, 255, 255, 0) },\n\t\t\t});\n\t\t\tconst auto ratio = int(faded.devicePixelRatio());\n\t\t\tconst auto imgHeight = faded.height() / ratio;\n\t\t\tq.fillRect(from, 0, fade, imgHeight, brush);\n\t\t\tq.end();\n\n\t\t\tp.drawImage(\n\t\t\t\tQRect(0, 0, till, imgHeight),\n\t\t\t\tfaded,\n\t\t\t\tQRect(0, 0, till * ratio, faded.height()));\n\t\t}\n\t}\n\n\tauto &modelWas = select(\n\t\t_state->spinnerModels,\n\t\t_state->modelSpin.wasIndex,\n\t\t_state->now.model);\n\tauto &modelNow = select(\n\t\t_state->spinnerModels,\n\t\t_state->modelSpin.nowIndex,\n\t\t_state->now.model);\n\tauto &modelWill = select(\n\t\t_state->spinnerModels,\n\t\t_state->modelSpin.willIndex,\n\t\t_state->now.model);\n\tconst auto modelProgress = _state->modelSpin.progress();\n\tconst auto paintOneModel = [&](ModelView &view, float64 progress) {\n\t\tif (progress >= 1. || progress <= -1.) {\n\t\t\treturn;\n\t\t}\n\t\tauto scale = 1.;\n\t\tif (progress != 0.) {\n\t\t\tconst auto shift = progress * context.width / 2.;\n\t\t\tconst auto shown = 1. - std::abs(progress);\n\t\t\tscale = kModelScaleFrom + shown * (1. - kModelScaleFrom);\n\t\t\tp.save();\n\t\t\tp.setOpacity(shown);\n\t\t\tp.translate(int(base::SafeRound(shift)), 0);\n\t\t}\n\t\tpaintModel(p, view, context, scale, true);\n\t\tif (progress != 0.) {\n\t\t\tp.restore();\n\t\t}\n\t};\n\tconst auto initial = (_state->modelSpin.nowIndex < 0);\n\tconst auto ending = (current == SpinnerState::FinishedModel)\n\t\t&& !_state->modelSpin.willIndex;\n\tconst auto willProgress = ending\n\t\t? (modelProgress - 1.)\n\t\t: (-1. + modelProgress * 2 / 3.);\n\tconst auto nowProgress = ending\n\t\t? (modelProgress * 4 / 3. - 1 / 3.)\n\t\t: initial\n\t\t? (modelProgress * 1 / 3.)\n\t\t: (willProgress + 2 / 3.);\n\tconst auto wasProgress = ending\n\t\t? std::min(nowProgress + 2 / 3., 1.)\n\t\t: 1. + (modelProgress - 1.) * 2 / 3.;\n\tif (!initial) {\n\t\tpaintOneModel(modelWas, wasProgress);\n\t}\n\tpaintOneModel(modelNow, nowProgress);\n\tpaintOneModel(modelWill, willProgress);\n\n\tif (anim::Disabled()\n\t\t|| (ending\n\t\t\t&& modelProgress >= 1.\n\t\t\t&& backdropProgress >= 1.\n\t\t\t&& patternProgress >= 1.)) {\n\t\tconst auto take = [&](auto &&list) {\n\t\t\tauto result = std::move(list.front());\n\t\t\tlist.clear();\n\t\t\treturn result;\n\t\t};\n\t\t_state->now.gift = *_state->spinner->target;\n\t\t_state->now.backdrop = take(_state->spinnerBackdrops);\n\t\t_state->now.pattern = take(_state->spinnerPatterns);\n\t\t_state->now.model = take(_state->spinnerModels);\n\t\t_state->now.forced = true;\n\t\t_state->spinStarted = 0;\n\t\t_state->spinner->state = SpinnerState::Finished;\n\t}\n}\n\nvoid UniqueGiftCoverWidget::paintNormalAnimation(\n\t\tQPainter &p,\n\t\tconst PaintContext &context,\n\t\tfloat64 progress) {\n\tif (progress < 1.) {\n\t\tconst auto finished = paintGift(p, _state->now, context, 1. - progress)\n\t\t\t|| (_state->next.forced\n\t\t\t\t&& (!_state->crossfading\n\t\t\t\t\t|| !_state->crossfade.animating()));\n\t\tconst auto next = finished\n\t\t\t? _state->next.model.lottie.get()\n\t\t\t: nullptr;\n\t\tif (next && next->ready()) {\n\t\t\t_state->crossfading = true;\n\t\t\t_state->updateAttributesPending = true;\n\t\t\t_state->crossfade.start([this] {\n\t\t\t\tupdate();\n\t\t\t}, 0., 1., kCrossfadeDuration);\n\t\t} else if (finished) {\n\t\t\tif (const auto onstack = _state->checkSpinnerStart) {\n\t\t\t\tonstack();\n\t\t\t}\n\t\t}\n\t}\n\tif (progress > 0.) {\n\t\tp.setOpacity(progress);\n\t\tpaintGift(p, _state->next, context, progress);\n\t}\n}\n\nvoid GiftReleasedByHandler(not_null<PeerData*> peer) {\n\tconst auto session = &peer->session();\n\tconst auto window = session->tryResolveWindow(peer);\n\tif (window) {\n\t\twindow->showPeerHistory(peer);\n\t\treturn;\n\t}\n\tconst auto account = not_null(&session->account());\n\tif (const auto window = Core::App().windowFor(account)) {\n\t\twindow->invokeForSessionController(\n\t\t\t&session->account(),\n\t\t\tpeer,\n\t\t\t[=](not_null<Window::SessionController*> window) {\n\t\t\t\twindow->showPeerHistory(peer);\n\t\t\t});\n\t}\n}\n\nobject_ptr<UniqueGiftCoverWidget> MakeUniqueGiftCover(\n\t\tQWidget *parent,\n\t\trpl::producer<UniqueGiftCover> data,\n\t\tUniqueGiftCoverArgs &&args) {\n\treturn object_ptr<UniqueGiftCoverWidget>(\n\t\tparent,\n\t\tstd::move(data),\n\t\tstd::move(args));\n}\n\nvoid AddUniqueGiftCover(\n\t\tnot_null<VerticalLayout*> container,\n\t\trpl::producer<UniqueGiftCover> data,\n\t\tUniqueGiftCoverArgs &&args) {\n\tcontainer->add(\n\t\tMakeUniqueGiftCover(container, std::move(data), std::move(args)));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/star_gift_cover_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_star_gift.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Data {\nstruct UniqueGift;\nstruct GiftUpgradeSpinner;\n} // namespace Data\n\nnamespace Ui {\n\nclass RpWidget;\nclass VerticalLayout;\n\nstruct UniqueGiftCoverArgs {\n\trpl::producer<QString> pretitle;\n\trpl::producer<QString> numberText;\n\trpl::producer<TextWithEntities> subtitle;\n\tFn<void()> subtitleClick;\n\tbool subtitleLinkColored = false;\n\tbool subtitleOutlined = false;\n\trpl::producer<CreditsAmount> resalePrice;\n\tFn<void()> resaleClick;\n\tbool attributesInfo = false;\n\tFn<void(\n\t\tstd::optional<Data::UniqueGift> now,\n\t\tstd::optional<Data::UniqueGift> next,\n\t\tfloat64 progress)> repaintedHook;\n\tstd::shared_ptr<Data::GiftUpgradeSpinner> upgradeSpinner;\n};\n\nstruct UniqueGiftCover {\n\tData::UniqueGift values;\n\tbool spinner = false;\n\tbool force = false;\n};\n\nvoid GiftReleasedByHandler(not_null<PeerData*> peer);\n\nclass UniqueGiftCoverWidget final : public RpWidget {\npublic:\n\tUniqueGiftCoverWidget(\n\t\tQWidget *parent,\n\t\trpl::producer<UniqueGiftCover> data,\n\t\tUniqueGiftCoverArgs &&args);\n\t~UniqueGiftCoverWidget();\n\n\tstruct PaintContext {\n\t\tint width = 0;\n\t\tint patternAreaHeight = 0;\n\t};\n\n\tstruct CraftContext {\n\t\tQSize initial;\n\t\tQColor initialBg;\n\t\tfloat64 fadeInProgress = 0.;\n\t\tfloat64 expandProgress = 0.;\n\t};\n\t[[nodiscard]] QRect prepareCraftFrame(\n\t\tQImage &canvas,\n\t\tconst CraftContext &context);\n\n\t[[nodiscard]] QColor backgroundColor() const;\n\t[[nodiscard]] QColor foregroundColor() const;\n\nprivate:\n\tstruct BackdropView;\n\tstruct PatternView;\n\tstruct ModelView;\n\tstruct GiftView;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\t[[nodiscard]] QImage prepareBackdrop(\n\t\tBackdropView &backdrop,\n\t\tconst PaintContext &context);\n\tvoid paintBackdrop(\n\t\tQPainter &p,\n\t\tBackdropView &backdrop,\n\t\tconst PaintContext &context);\n\tvoid paintPattern(\n\t\tQPainter &p,\n\t\tPatternView &pattern,\n\t\tconst BackdropView &backdrop,\n\t\tconst PaintContext &context,\n\t\tfloat64 shown);\n\tbool paintModel(\n\t\tQPainter &p,\n\t\tModelView &model,\n\t\tconst PaintContext &context,\n\t\tfloat64 scale = 1.,\n\t\tbool paused = false);\n\tbool paintGift(\n\t\tQPainter &p,\n\t\tGiftView &gift,\n\t\tconst PaintContext &context,\n\t\tfloat64 shown);\n\n\tvoid paintSpinnerAnimation(QPainter &p, const PaintContext &context);\n\tvoid paintNormalAnimation(\n\t\tQPainter &p,\n\t\tconst PaintContext &context,\n\t\tfloat64 progress);\n\n\tstruct State;\n\tconst std::unique_ptr<State> _state;\n\n};\n\n[[nodiscard]] object_ptr<UniqueGiftCoverWidget> MakeUniqueGiftCover(\n\tQWidget *parent,\n\trpl::producer<UniqueGiftCover> data,\n\tUniqueGiftCoverArgs &&args);\n\nvoid AddUniqueGiftCover(\n\tnot_null<VerticalLayout*> container,\n\trpl::producer<UniqueGiftCover> data,\n\tUniqueGiftCoverArgs &&args);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/star_gift_craft_animation.h\"\n\n#include \"base/call_delayed.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"boxes/star_gift_cover_box.h\"\n#include \"boxes/star_gift_craft_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"data/data_credits.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_session.h\"\n#include \"history/view/media/history_view_sticker_player.h\"\n#include \"info/peer_gifts/info_peer_gifts_common.h\"\n#include \"lang/lang_keys.h\"\n#include \"lang/lang_tag.h\"\n#include \"lottie/lottie_common.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"ui/boxes/boost_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/gradient_round_button.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/top_background_gradient.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Ui {\nnamespace {\n\nusing namespace Info::PeerGifts;\n\nconstexpr auto kFrameDuration = 1000. / 60.;\nconstexpr auto kFlightDuration = crl::time(400);\nconstexpr auto kLoadingFadeInDuration = crl::time(150);\nconstexpr auto kLoadingFadeOutDuration = crl::time(300);\nconstexpr auto kLoadingMinDuration = crl::time(600);\nconstexpr auto kFailureFadeDuration = crl::time(400);\nconstexpr auto kSuccessFadeInDuration = crl::time(300);\nconstexpr auto kSuccessExpandDuration = crl::time(400);\nconstexpr auto kSuccessExpandStart = crl::time(100);\nconstexpr auto kProgressFadeInDuration = crl::time(300);\nconstexpr auto kFailureFadeInDuration = crl::time(300);\n\n[[nodiscard]] QString FormatPercent(int permille) {\n\tconst auto rounded = (permille + 5) / 10;\n\treturn QString::number(rounded) + '%';\n}\n\nstruct Rotation {\n\tfloat64 x = 0.;\n\tfloat64 y = 0.;\n\n\tRotation operator+(Rotation other) const {\n\t\treturn { x + other.x, y + other.y };\n\t}\n\tRotation operator*(float64 scale) const {\n\t\treturn { x * scale, y * scale };\n\t}\n};\n\nusing RotationFn = Fn<Rotation(Rotation initial, crl::time t)>;\n\n[[nodiscard]] int Slowing() {\n\treturn anim::SlowMultiplier();\n}\n\n[[nodiscard]] RotationFn DecayingRotation(Rotation impulse, float64 decay) {\n\tconst auto lambda = -std::log(decay) / kFrameDuration;\n\treturn [=](Rotation initial, crl::time t) {\n\t\tconst auto factor = (1. - std::exp(-lambda * t)) / lambda / 1000.;\n\t\treturn initial + impulse * factor;\n\t};\n}\n\n[[nodiscard]] RotationFn DecayingEndRotation(\n\t\tRotation impulse,\n\t\tfloat64 decay,\n\t\tRotation target,\n\t\tcrl::time duration) {\n\tconst auto lambda = -std::log(decay) / kFrameDuration;\n\ttarget = target * M_PI;\n\treturn [=](Rotation initial, crl::time t) {\n\t\tconst auto factor = (1. - std::exp(-lambda * t)) / lambda / 1000.;\n\t\tconst auto result = initial + impulse * factor;\n\t\tif (t <= duration - 200) {\n\t\t\treturn result;\n\t\t} else if (t >= duration) {\n\t\t\treturn target;\n\t\t}\n\t\tconst auto progress = (duration - t) / 200.;\n\t\treturn result * progress + target * (1. - progress);\n\t};\n}\n\nstruct GiftAnimationConfig {\n\tRotationFn rotation;\n\tcrl::time duration = 0;\n\tcrl::time revealTime = 0;\n\tint targetFaceIndex = 0;\n\tint targetFaceRotation = 0;\n};\n\nconst auto kGiftAnimations = std::array<GiftAnimationConfig, 7>{{ {\n\t.rotation = DecayingEndRotation({ 4.1, -8.2 }, 0.98, { 1, -2 }, 2200),\n\t.duration = 2200,\n\t.revealTime = 600,\n\t.targetFaceIndex = 1,\n\t.targetFaceRotation = 180,\n}, {\n\t.rotation = DecayingRotation({ 3.2, -5.9 }, 0.97),\n\t.duration = 1200,\n\t.revealTime = 0,\n\t.targetFaceIndex = 4,\n\t.targetFaceRotation = 180,\n}, {\n\t.rotation = DecayingEndRotation({ 6.2, 7.8 }, 0.98, { 2, 1 }, 2200),\n\t.duration = 2200,\n\t.revealTime = 1000,\n\t.targetFaceIndex = 1,\n\t.targetFaceRotation = 0,\n}, {\n\t.rotation = DecayingRotation({ 7.0, 4.5 }, 0.97),\n\t.duration = 1200,\n\t.revealTime = 200,\n\t.targetFaceIndex = 5,\n\t.targetFaceRotation = 0,\n}, {\n\t.rotation = DecayingEndRotation({ -6.5, 5.0 }, 0.98, { 0, 1 }, 2200),\n\t.duration = 2200,\n\t.revealTime = 800,\n\t.targetFaceIndex = 1,\n\t.targetFaceRotation = 0,\n}, {\n\t.rotation = DecayingRotation({ -2.5, 3.5 }, 0.98),\n\t.duration = 1200,\n\t.revealTime = 200,\n\t.targetFaceIndex = 2,\n\t.targetFaceRotation = 180,\n}, {\n\t.rotation = DecayingEndRotation({ -4.4, -8.2 }, 0.98, { 0, -1.5 }, 2200),\n\t.duration = 2200,\n\t.revealTime = 600,\n\t.targetFaceIndex = 3,\n\t.targetFaceRotation = 0,\n} }};\n\n[[nodiscard]] QImage CreateBgGradient(\n\t\tQSize size,\n\t\tconst Data::UniqueGiftBackdrop &backdrop) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\n\tauto p = QPainter(&result);\n\tauto hq = PainterHighQualityEnabler(p);\n\tauto gradient = QRadialGradient(\n\t\tQPoint(size.width() / 2, size.height() / 2),\n\t\tsize.height() / 2);\n\tgradient.setStops({\n\t\t{ 0., backdrop.centerColor },\n\t\t{ 1., backdrop.edgeColor },\n\t});\n\tp.setBrush(gradient);\n\tp.setPen(Qt::NoPen);\n\tp.drawRect(0, 0, size.width(), size.height());\n\tp.end();\n\n\tconst auto mask = Images::CornersMask(st::boxRadius);\n\treturn Images::Round(std::move(result), mask);\n}\n\n[[nodiscard]] std::optional<std::array<QPointF, 4>> ComputeCubeFaceCorners(\n\t\tQPointF center,\n\t\tfloat64 size,\n\t\tfloat64 rotationX,\n\t\tfloat64 rotationY,\n\t\tint faceIndex) {\n\tstruct Vec3 {\n\t\tfloat64 x, y, z;\n\n\t\tVec3 operator+(const Vec3 &o) const {\n\t\t\treturn { x + o.x, y + o.y, z + o.z };\n\t\t}\n\t\tVec3 operator-(const Vec3 &o) const {\n\t\t\treturn { x - o.x, y - o.y, z - o.z };\n\t\t}\n\t\tVec3 operator*(float64 s) const {\n\t\t\treturn { x * s, y * s, z * s };\n\t\t}\n\t\t[[nodiscard]] Vec3 cross(const Vec3 &o) const {\n\t\t\treturn {\n\t\t\t\ty * o.z - z * o.y,\n\t\t\t\tz * o.x - x * o.z,\n\t\t\t\tx * o.y - y * o.x\n\t\t\t};\n\t\t}\n\t\t[[nodiscard]] float64 dot(const Vec3 &o) const {\n\t\t\treturn x * o.x + y * o.y + z * o.z;\n\t\t}\n\t\t[[nodiscard]] Vec3 normalized() const {\n\t\t\tconst auto len = std::sqrt(x * x + y * y + z * z);\n\t\t\treturn (len > 0.) ? Vec3{ x / len, y / len, z / len } : *this;\n\t\t}\n\t};\n\n\tconst auto rotateY = [](Vec3 v, float64 angle) {\n\t\tconst auto c = std::cos(angle);\n\t\tconst auto s = std::sin(angle);\n\t\treturn Vec3{ v.x * c + v.z * s, v.y, -v.x * s + v.z * c };\n\t};\n\tconst auto rotateX = [](Vec3 v, float64 angle) {\n\t\tconst auto c = std::cos(angle);\n\t\tconst auto s = std::sin(angle);\n\t\treturn Vec3{ v.x, v.y * c - v.z * s, v.y * s + v.z * c };\n\t};\n\n\tconst auto half = size / 2.;\n\tconstexpr auto kFocalLength = 800.;\n\n\tstruct FaceDefinition {\n\t\tstd::array<Vec3, 4> corners;\n\t\tVec3 normal;\n\t};\n\n\tconst auto faces = std::array<FaceDefinition, 6>{{\n\t\t{{{{ -half, -half, -half },\n\t\t   {  half, -half, -half },\n\t\t   {  half,  half, -half },\n\t\t   { -half,  half, -half }}},\n\t\t { 0., 0., -1. }},\n\n\t\t{{{{  half, -half,  half },\n\t\t   { -half, -half,  half },\n\t\t   { -half,  half,  half },\n\t\t   {  half,  half,  half }}},\n\t\t { 0., 0., 1. }},\n\n\t\t{{{{ -half, -half,  half },\n\t\t   { -half, -half, -half },\n\t\t   { -half,  half, -half },\n\t\t   { -half,  half,  half }}},\n\t\t { -1., 0., 0. }},\n\n\t\t{{{{  half, -half, -half },\n\t\t   {  half, -half,  half },\n\t\t   {  half,  half,  half },\n\t\t   {  half,  half, -half }}},\n\t\t { 1., 0., 0. }},\n\n\t\t{{{{ -half, -half,  half },\n\t\t   {  half, -half,  half },\n\t\t   {  half, -half, -half },\n\t\t   { -half, -half, -half }}},\n\t\t { 0., -1., 0. }},\n\n\t\t{{{{ -half,  half, -half },\n\t\t   {  half,  half, -half },\n\t\t   {  half,  half,  half },\n\t\t   { -half,  half,  half }}},\n\t\t { 0., 1., 0. }},\n\t}};\n\n\tif (faceIndex < 0 || faceIndex >= 6) {\n\t\treturn std::nullopt;\n\t}\n\n\tconst auto &face = faces[faceIndex];\n\n\tconst auto transformedNormal = rotateX(\n\t\trotateY(face.normal, rotationY),\n\t\trotationX);\n\tif (transformedNormal.z >= 0.) {\n\t\treturn std::nullopt;\n\t}\n\n\tauto result = std::array<QPointF, 4>();\n\tfor (auto i = 0; i != 4; ++i) {\n\t\tconst auto p = rotateX(\n\t\t\trotateY(face.corners[i], rotationY),\n\t\t\trotationX);\n\n\t\tconst auto viewZ = p.z + half + kFocalLength;\n\t\tif (viewZ <= 0.) {\n\t\t\treturn std::nullopt;\n\t\t}\n\n\t\tconst auto scale = kFocalLength / viewZ;\n\t\tresult[i] = QPointF(\n\t\t\tcenter.x() + p.x * scale,\n\t\t\tcenter.y() + p.y * scale);\n\t}\n\n\treturn result;\n}\n\nvoid PaintCubeFace(\n\t\tQPainter &p,\n\t\tconst std::array<QPointF, 4> &corners,\n\t\tconst QImage &face,\n\t\tQSize faceSize = QSize(),\n\t\tconst QRect &source = QRect(),\n\t\tint rotation = 0) {\n\tif (face.isNull()) {\n\t\treturn;\n\t}\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto origin = source.isEmpty() ? face.size() : source.size();\n\tif (faceSize.isEmpty()) {\n\t\tfaceSize = origin / ratio;\n\t}\n\tconst auto w = faceSize.width();\n\tconst auto h = faceSize.height();\n\n\tconst auto srcRect = QPolygonF({\n\t\tQPointF(0, 0),\n\t\tQPointF(w, 0),\n\t\tQPointF(w, h),\n\t\tQPointF(0, h)\n\t});\n\n\tconst auto rotationSteps = (rotation / 90) % 4;\n\tauto rotatedCorners = corners;\n\tfor (auto i = 0; i < rotationSteps; ++i) {\n\t\trotatedCorners = {\n\t\t\trotatedCorners[1],\n\t\t\trotatedCorners[2],\n\t\t\trotatedCorners[3],\n\t\t\trotatedCorners[0],\n\t\t};\n\t}\n\n\tconst auto dstRect = QPolygonF({\n\t\trotatedCorners[0],\n\t\trotatedCorners[1],\n\t\trotatedCorners[2],\n\t\trotatedCorners[3]\n\t});\n\n\tauto transform = QTransform();\n\tif (!QTransform::quadToQuad(srcRect, dstRect, transform)) {\n\t\treturn;\n\t}\n\n\tPainterHighQualityEnabler hq(p);\n\tp.save();\n\tp.setTransform(transform, true);\n\tconst auto delta = source.isEmpty()\n\t\t? QPointF()\n\t\t: QPointF(\n\t\t\t((origin.width() / ratio) - faceSize.width()) / 2.,\n\t\t\t((origin.height() / ratio) - faceSize.height()) / 2.);\n\tp.drawImage(QRectF(-delta, QSizeF(origin) / ratio), face, source);\n\tp.restore();\n}\n\n[[nodiscard]] std::vector<int> GetVisibleCubeFaces(\n\t\tfloat64 rotationX,\n\t\tfloat64 rotationY) {\n\tstruct Vec3 {\n\t\tfloat64 x, y, z;\n\n\t\tVec3 operator*(float64 s) const {\n\t\t\treturn { x * s, y * s, z * s };\n\t\t}\n\t};\n\n\tconst auto rotateY = [](Vec3 v, float64 angle) {\n\t\tconst auto c = std::cos(angle);\n\t\tconst auto s = std::sin(angle);\n\t\treturn Vec3{ v.x * c + v.z * s, v.y, -v.x * s + v.z * c };\n\t};\n\tconst auto rotateX = [](Vec3 v, float64 angle) {\n\t\tconst auto c = std::cos(angle);\n\t\tconst auto s = std::sin(angle);\n\t\treturn Vec3{ v.x, v.y * c - v.z * s, v.y * s + v.z * c };\n\t};\n\n\tconstexpr auto normals = std::array<Vec3, 6>{{\n\t\t{ 0., 0., -1. },\n\t\t{ 0., 0., 1. },\n\t\t{ -1., 0., 0. },\n\t\t{ 1., 0., 0. },\n\t\t{ 0., -1., 0. },\n\t\t{ 0., 1., 0. },\n\t}};\n\n\tstruct FaceDepth {\n\t\tint index;\n\t\tfloat64 z;\n\t};\n\tauto visible = std::vector<FaceDepth>();\n\tvisible.reserve(3);\n\n\tfor (auto i = 0; i != 6; ++i) {\n\t\tconst auto transformed = rotateX(\n\t\t\trotateY(normals[i], rotationY),\n\t\t\trotationX);\n\t\tif (transformed.z < 0.) {\n\t\t\tvisible.push_back({ i, transformed.z });\n\t\t}\n\t}\n\n\tranges::sort(visible, ranges::greater(), &FaceDepth::z);\n\n\tauto result = std::vector<int>();\n\tresult.reserve(visible.size());\n\tfor (const auto &face : visible) {\n\t\tresult.push_back(face.index);\n\t}\n\treturn result;\n}\n\nvoid PaintCubeFirstFlight(\n\t\tQPainter &p,\n\t\tnot_null<const CraftAnimationState*> state,\n\t\tfloat64 progress,\n\t\tbool skipForgeIcon = false) {\n\tconst auto shared = state->shared.get();\n\n\tconst auto overlayBg = shared->forgeBgOverlay;\n\tauto sideBg = shared->forgeBg1;\n\tsideBg.setAlphaF(progress);\n\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto forge = shared->forgeRect;\n\tconst auto radius = (1. - progress) * st::boxRadius;\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(overlayBg);\n\tp.drawRoundedRect(forge, radius, radius);\n\tp.setBrush(sideBg);\n\tp.drawRoundedRect(forge, radius, radius);\n\n\tif (!skipForgeIcon) {\n\t\tst::craftForge.paintInCenter(p, forge, st::white->c);\n\t}\n}\n\nstd::unique_ptr<CraftFailAnimation> SetupFailureAnimation(\n\t\tnot_null<RpWidget*> canvas,\n\t\tnot_null<CraftAnimationState*> state) {\n\tauto result = std::make_unique<CraftFailAnimation>();\n\tresult->lottie = Lottie::MakeIcon({\n\t\t.name = u\"craft_failed\"_q,\n\t\t.sizeOverride = st::craftFailLottieSize,\n\t});\n\treturn result;\n}\n\nvoid FailureAnimationCheck(\n\t\tnot_null<CraftAnimationState*> state,\n\t\tnot_null<RpWidget*> canvas,\n\t\tcrl::time now) {\n\tconst auto animation = state->failureAnimation.get();\n\tif (!animation || animation->started) {\n\t\treturn;\n\t}\n\tconst auto elapsed = (now - state->animationStartTime);\n\tconst auto left = (state->animationDuration * Slowing()) - elapsed;\n\tif (left <= 0 || left > kFailureFadeDuration * Slowing()) {\n\t\treturn;\n\t}\n\tanimation->started = true;\n\tauto &covers = state->shared->covers;\n\tfor (auto i = 0; i != 5; ++i) {\n\t\tauto &cover = covers[i];\n\t\tif (!cover.shown) {\n\t\t\tanimation->finalCoverIndex = i;\n\t\t\tif (i != 4) {\n\t\t\t\tauto &last = covers.back();\n\t\t\t\tcover.backdrop = last.backdrop;\n\t\t\t\tcover.pattern = std::move(last.pattern);\n\t\t\t\tcover.button1 = last.button1;\n\t\t\t\tcover.button2 = last.button2;\n\t\t\t}\n\t\t\tcover.shown = true;\n\t\t\tcover.shownAnimation.start([=] {\n\t\t\t\tcanvas->update();\n\t\t\t}, 0., 1., left);\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst auto lottie = animation->lottie.get();\n\tif (lottie->framesCount() > 1) {\n\t\tlottie->animate([=] {\n\t\t\tcanvas->update();\n\t\t}, 0, lottie->framesCount() - 1);\n\t}\n\n\tstate->failureShown = true;\n\tstate->progressShown = false;\n}\n\nvoid FailureAnimationPrepareFrame(\n\t\tnot_null<CraftAnimationState*> state,\n\t\tnot_null<RpWidget*> canvas,\n\t\tint faceIndex) {\n\tif (!state->failureAnimation) {\n\t\tstate->failureAnimation = SetupFailureAnimation(\n\t\t\tcanvas,\n\t\t\tstate);\n\t}\n\tconst auto shared = state->shared.get();\n\tconst auto animation = state->failureAnimation.get();\n\tif (animation->frame.isNull()) {\n\t\tanimation->frame = shared->forgeSides[faceIndex].frame;\n\t}\n\tconst auto lastIndex = animation->finalCoverIndex;\n\tconst auto progress = (lastIndex >= 0)\n\t\t? shared->covers[lastIndex].shownAnimation.value(1.)\n\t\t: 0.;\n\tconst auto bg = anim::color(\n\t\tshared->forgeSides[faceIndex].bg,\n\t\tshared->forgeFail,\n\t\tprogress);\n\tconst auto radius = progress * st::boxRadius;\n\tconst auto rounded = (radius > 0.);\n\tif (rounded) {\n\t\tanimation->frame.fill(Qt::transparent);\n\t}\n\n\tauto p = QPainter(&animation->frame);\n\tconst auto size = shared->forgeRect.size();\n\tconst auto rect = QRect(QPoint(), size);\n\tif (rounded) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setBrush(bg);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawRoundedRect(rect, radius, radius);\n\t} else {\n\t\tp.fillRect(rect, bg);\n\t}\n\tanimation->lottie->paintInCenter(p, rect);\n\tp.setOpacity(progress);\n\tp.drawImage(\n\t\tst::craftForgePadding,\n\t\tst::craftForgePadding,\n\t\tstate->shared->lostRadial);\n}\n\n[[nodiscard]] QRect PrepareCraftFrame(\n\t\tnot_null<CraftAnimationState*> state,\n\t\tint faceIndex,\n\t\tcrl::time now) {\n\tExpects(state->successAnimation != nullptr);\n\n\tconst auto &shared = state->shared.get();\n\tconst auto success = state->successAnimation.get();\n\tconst auto elapsed = now - state->animationStartTime;\n\tconst auto end = state->animationDuration * Slowing();\n\tconst auto left = end - elapsed;\n\tconst auto fadeDuration = kSuccessFadeInDuration * Slowing();\n\tconst auto fadeInProgress = std::clamp(\n\t\t(1. - (left / float64(fadeDuration))),\n\t\t0.,\n\t\t1.);\n\tconst auto expandStart = kSuccessExpandStart * Slowing();\n\tconst auto expanding = kSuccessExpandDuration * Slowing();\n\tconst auto expandProgress = std::clamp(\n\t\t(expandStart - left) / float64(expanding),\n\t\t0.,\n\t\t1.);\n\tif (expandProgress == 1. && !success->finished) {\n\t\tsuccess->finished = true;\n\t}\n\tconst auto expanded = anim::easeOutCubic(1., expandProgress);\n\tsuccess->expanded = expanded;\n\treturn success->widget->prepareCraftFrame(success->frame, {\n\t\t.initial = shared->forgeRect.size(),\n\t\t.initialBg = shared->forgeSides[faceIndex].bg,\n\t\t.fadeInProgress = fadeInProgress,\n\t\t.expandProgress = expanded,\n\t});\n}\n\nvoid PaintCube(\n\t\tQPainter &p,\n\t\tnot_null<CraftAnimationState*> state,\n\t\tnot_null<RpWidget*> canvas,\n\t\tQPointF center,\n\t\tfloat64 size) {\n\tconst auto shared = state->shared.get();\n\n\tconst auto now = crl::now();\n\tFailureAnimationCheck(state, canvas, now);\n\n\tconst auto success = state->successAnimation.get();\n\tconst auto failure = state->failureAnimation.get();\n\n\tconst auto finalCoverIndex = failure ? failure->finalCoverIndex : -1;\n\tconst auto finishingFailure = (finalCoverIndex >= 0)\n\t\t? shared->covers[finalCoverIndex].shownAnimation.value(1.)\n\t\t: 0.;\n\n\tif (!state->nextFaceRevealed\n\t\t&& state->currentPhaseIndex >= 0) {\n\t\tconst auto &config = kGiftAnimations[state->currentPhaseIndex];\n\t\tconst auto elapsed = (crl::now() - state->animationStartTime);\n\t\tif (elapsed >= config.revealTime * Slowing()) {\n\t\t\tstate->nextFaceRevealed = true;\n\t\t}\n\t}\n\n\tconst auto faces = GetVisibleCubeFaces(\n\t\tstate->rotationX,\n\t\tstate->rotationY);\n\n\tauto paintFailure = Fn<void()>();\n\tfor (const auto faceIndex : faces) {\n\t\tconst auto corners = ComputeCubeFaceCorners(\n\t\t\tcenter,\n\t\t\tsize,\n\t\t\tstate->rotationX,\n\t\t\tstate->rotationY,\n\t\t\tfaceIndex);\n\t\tif (!corners) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto thisFaceRevealed = state->nextFaceRevealed\n\t\t\t&& (faceIndex == state->nextFaceIndex);\n\t\tauto faceSize = shared->forgeRect.size();\n\t\tauto faceImage = shared->forgeSides[faceIndex].frame;\n\t\tauto faceSource = QRect();\n\t\tauto faceRotation = thisFaceRevealed\n\t\t\t? state->nextFaceRotation\n\t\t\t: 0;\n\n\t\tfor (auto i = 0; i < 4; ++i) {\n\t\t\tif (i != state->currentlyFlying\n\t\t\t\t&& state->giftToSide[i].face == faceIndex\n\t\t\t\t&& shared->corners[i].giftButton) {\n\t\t\t\tfaceSize = QSize(); // Take from the frame.\n\t\t\t\tfaceImage = shared->corners[i].gift(1.);\n\t\t\t\tfaceRotation = state->giftToSide[i].rotation;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (thisFaceRevealed && state->allGiftsLanded) {\n\t\t\tconst auto cover = success ? success->widget : nullptr;\n\t\t\tif (cover) {\n\t\t\t\tfaceSource = PrepareCraftFrame(state, faceIndex, now);\n\t\t\t\tfaceImage = success->frame;\n\t\t\t} else {\n\t\t\t\tFailureAnimationPrepareFrame(\n\t\t\t\t\tstate,\n\t\t\t\t\tcanvas,\n\t\t\t\t\tfaceIndex);\n\t\t\t\tfaceImage = state->failureAnimation->frame;\n\t\t\t}\n\t\t}\n\t\tif (finishingFailure > 0.) {\n\t\t\tp.setOpacity((faceIndex != state->nextFaceIndex)\n\t\t\t\t? (1. - finishingFailure)\n\t\t\t\t: 1.);\n\t\t}\n\t\tPaintCubeFace(\n\t\t\tp,\n\t\t\t*corners,\n\t\t\tfaceImage,\n\t\t\tfaceSize,\n\t\t\tfaceSource,\n\t\t\tfaceRotation);\n\t}\n\tif (finishingFailure > 0.) {\n\t\tp.setOpacity(1.);\n\t}\n}\n\nstruct GiftFlightPosition {\n\tQPointF origin;\n\tfloat64 scale;\n};\n\n[[nodiscard]] GiftFlightPosition ComputeGiftFlightPosition(\n\t\tQRect originalRect,\n\t\tQPointF targetCenter,\n\t\tfloat64 targetSize,\n\t\tfloat64 progress,\n\t\tfloat64 startOffsetY) {\n\tconst auto eased = progress;\n\n\tconst auto startOrigin = QPointF(originalRect.topLeft())\n\t\t+ QPointF(0, startOffsetY);\n\tconst auto targetOrigin = targetCenter\n\t\t- QPointF(targetSize, targetSize) / 2.;\n\tconst auto currentOrigin = startOrigin\n\t\t+ (targetOrigin - startOrigin) * eased;\n\n\tconst auto originalSize = std::max(\n\t\toriginalRect.width(),\n\t\toriginalRect.height());\n\tconst auto endScale = (originalSize > 0.)\n\t\t? (targetSize / float64(originalSize))\n\t\t: 1.;\n\tconst auto currentScale = 1. + (endScale - 1.) * eased;\n\n\treturn {\n\t\t.origin = currentOrigin,\n\t\t.scale = currentScale,\n\t};\n}\n\n[[nodiscard]] FacePlacement GetCameraFacingFreeFace(\n\t\tnot_null<const CraftAnimationState*> state) {\n\tstruct Vec3 {\n\t\tfloat64 x, y, z;\n\t};\n\n\tconst auto rotateY = [](Vec3 v, float64 angle) {\n\t\tconst auto c = std::cos(angle);\n\t\tconst auto s = std::sin(angle);\n\t\treturn Vec3{ v.x * c + v.z * s, v.y, -v.x * s + v.z * c };\n\t};\n\tconst auto rotateX = [](Vec3 v, float64 angle) {\n\t\tconst auto c = std::cos(angle);\n\t\tconst auto s = std::sin(angle);\n\t\treturn Vec3{ v.x, v.y * c - v.z * s, v.y * s + v.z * c };\n\t};\n\n\tconstexpr auto normals = std::array<Vec3, 6>{{\n\t\t{ 0., 0., -1. },\n\t\t{ 0., 0., 1. },\n\t\t{ -1., 0., 0. },\n\t\t{ 1., 0., 0. },\n\t\t{ 0., -1., 0. },\n\t\t{ 0., 1., 0. },\n\t}};\n\n\tconstexpr auto faceUpVectors = std::array<Vec3, 6>{{\n\t\t{ 0., -1., 0. },\n\t\t{ 0., -1., 0. },\n\t\t{ 0., -1., 0. },\n\t\t{ 0., -1., 0. },\n\t\t{ 0., 0., -1. },\n\t\t{ 0., 0., 1. },\n\t}};\n\n\tconst auto isOccupied = [&](int faceIndex) {\n\t\tfor (const auto &placement : state->giftToSide) {\n\t\t\tif (placement.face == faceIndex) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\n\tauto bestFace = -1;\n\tauto bestZ = 0.;\n\n\tfor (auto i = 0; i != 6; ++i) {\n\t\tif (isOccupied(i)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto transformed = rotateX(\n\t\t\trotateY(normals[i], state->rotationY),\n\t\t\tstate->rotationX);\n\t\tif (bestFace < 0 || transformed.z < bestZ) {\n\t\t\tbestFace = i;\n\t\t\tbestZ = transformed.z;\n\t\t}\n\t}\n\n\tif (bestFace < 0) {\n\t\treturn {};\n\t}\n\n\tconst auto faceUp = rotateX(\n\t\trotateY(faceUpVectors[bestFace], state->rotationY),\n\t\tstate->rotationX);\n\n\tconst auto screenUpY = faceUp.y;\n\tconst auto screenUpX = faceUp.x;\n\n\tauto rotation = 0;\n\tif (std::abs(screenUpY) >= std::abs(screenUpX)) {\n\t\trotation = (screenUpY < 0.) ? 0 : 180;\n\t} else {\n\t\trotation = (screenUpX < 0.) ? 90 : 270;\n\t}\n\n\treturn { .face = bestFace, .rotation = rotation };\n}\n\n[[nodiscard]] std::array<QPointF, 4> InterpolateQuadCorners(\n\t\tconst std::array<QPointF, 4> &from,\n\t\tconst std::array<QPointF, 4> &to,\n\t\tfloat64 progress) {\n\treturn {\n\t\tfrom[0] + (to[0] - from[0]) * progress,\n\t\tfrom[1] + (to[1] - from[1]) * progress,\n\t\tfrom[2] + (to[2] - from[2]) * progress,\n\t\tfrom[3] + (to[3] - from[3]) * progress,\n\t};\n}\n\n[[nodiscard]] std::array<QPointF, 4> RectToCorners(QRectF rect) {\n\treturn {\n\t\trect.topLeft(),\n\t\trect.topRight(),\n\t\trect.bottomRight(),\n\t\trect.bottomLeft(),\n\t};\n}\n\nvoid PaintFlyingGiftWithQuad(\n\t\tQPainter &p,\n\t\tconst CraftState::CornerSnapshot &corner,\n\t\tconst std::array<QPointF, 4> &corners,\n\t\tfloat64 progress) {\n\tif (!corner.giftButton) {\n\t\treturn;\n\t}\n\n\tconst auto frame = corner.gift(progress);\n\tPaintCubeFace(p, corners, frame);\n}\n\nvoid PaintFlyingGift(\n\t\tQPainter &p,\n\t\tconst CraftState::CornerSnapshot &corner,\n\t\tconst GiftFlightPosition &position,\n\t\tfloat64 progress) {\n\tif (!corner.giftButton) {\n\t\treturn;\n\t}\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto frame = corner.gift(progress);\n\tconst auto giftSize = QSizeF(frame.size()) / ratio;\n\tconst auto scaledSize = giftSize * position.scale;\n\tconst auto giftTopLeft = position.origin;\n\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.drawImage(QRectF(giftTopLeft, scaledSize), frame);\n}\n\nvoid PaintSlideOutCorner(\n\t\tQPainter &p,\n\t\tconst CraftState::CornerSnapshot &corner,\n\t\tfloat64 progress) {\n\tif (corner.originalRect.isEmpty()) {\n\t\treturn;\n\t}\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto currentTopLeft = QPointF(corner.originalRect.topLeft());\n\tif (corner.giftButton) {\n\t\tconst auto frame = corner.gift(0.);\n\t\tconst auto giftSize = QSizeF(frame.size()) / ratio;\n\t\tconst auto giftTopLeft = currentTopLeft;\n\n\t\tconst auto &extend = st::defaultDropdownMenu.wrap.shadow.extend;\n\t\tconst auto innerTopLeft = giftTopLeft\n\t\t\t+ QPointF(extend.left(), extend.top());\n\t\tconst auto innerWidth = giftSize.width()\n\t\t\t- extend.left()\n\t\t\t- extend.right();\n\n\t\tp.drawImage(giftTopLeft, frame);\n\n\t\tconst auto badgeFade = std::clamp(1. - progress / 0.7, 0., 1.);\n\t\tif (!corner.percentBadge.isNull() && badgeFade > 0.) {\n\t\t\tconst auto badgeSize = QSizeF(corner.percentBadge.size())\n\t\t\t\t/ ratio;\n\t\t\tconst auto badgePos = innerTopLeft\n\t\t\t\t- QPointF(badgeSize.width() / 3., badgeSize.height() / 3.);\n\t\t\tp.setOpacity(badgeFade);\n\t\t\tp.drawImage(badgePos, corner.percentBadge);\n\t\t\tp.setOpacity(1.);\n\t\t}\n\n\t\tif (!corner.removeButton.isNull() && badgeFade > 0.) {\n\t\t\tconst auto removeSize = QSizeF(corner.removeButton.size())\n\t\t\t\t/ ratio;\n\t\t\tconst auto removePos = innerTopLeft\n\t\t\t\t+ QPointF(\n\t\t\t\t\t(innerWidth\n\t\t\t\t\t\t- removeSize.width()\n\t\t\t\t\t\t+ removeSize.width() / 3.),\n\t\t\t\t\t-removeSize.height() / 3.);\n\t\t\tp.setOpacity(badgeFade);\n\t\t\tp.drawImage(removePos, corner.removeButton);\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t} else if (!corner.addButton.isNull()) {\n\t\tconst auto fadeProgress = std::clamp(progress * 1.5, 0., 1.);\n\t\tconst auto addTopLeft = currentTopLeft;\n\t\tp.setOpacity(1. - fadeProgress);\n\t\tp.drawImage(addTopLeft, corner.addButton);\n\t\tp.setOpacity(1.);\n\t}\n}\n\nvoid PaintSlideOutPhase(\n\t\tQPainter &p,\n\t\tnot_null<CraftState*> shared,\n\t\tQSize canvasSize,\n\t\tfloat64 progress) {\n\tconst auto ratio = style::DevicePixelRatio();\n\n\tif (!shared->bottomPart.isNull()) {\n\t\tconst auto slideDistance = QSizeF(shared->bottomPart.size()).height()\n\t\t\t/ ratio;\n\t\tconst auto offsetY = slideDistance * progress;\n\t\tconst auto opacity = 1. - progress;\n\n\t\tp.setOpacity(opacity);\n\t\tp.drawImage(\n\t\t\tQPointF(0, shared->bottomPartY + offsetY),\n\t\t\tshared->bottomPart);\n\t\tp.setOpacity(1.);\n\t}\n\n\tfor (const auto &corner : shared->corners) {\n\t\tPaintSlideOutCorner(p, corner, progress);\n\t}\n\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto forge = shared->forgeRect;\n\tconst auto radius = st::boxRadius;\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(shared->forgeBgOverlay);\n\tp.drawRoundedRect(forge, radius, radius);\n\tst::craftForge.paintInCenter(p, forge, st::white->c);\n\tp.setOpacity(1. - progress);\n\tp.drawImage(\n\t\tforge.x() + st::craftForgePadding,\n\t\tforge.y() + st::craftForgePadding,\n\t\tshared->forgePercent);\n}\n\nvoid PaintFailureThumbnails(\n\t\tQPainter &p,\n\t\tnot_null<const CraftState*> shared,\n\t\tQSize canvasSize,\n\t\tfloat64 fadeProgress) {\n\tif (fadeProgress <= 0. || shared->lostGifts.empty()) {\n\t\treturn;\n\t}\n\n\tp.setOpacity(fadeProgress);\n\n\tconst auto width = canvasSize.width();\n\tconst auto giftsCount = int(shared->lostGifts.size());\n\tconst auto &firstCorner = shared->corners[\n\t\tshared->lostGifts.front().cornerIndex];\n\tif (!firstCorner.giftButton) {\n\t\tp.setOpacity(1.);\n\t\treturn;\n\t}\n\tconst auto thumbSize = firstCorner.giftButton->size();\n\tconst auto &extend = st::defaultDropdownMenu.wrap.shadow.extend;\n\tconst auto thumbSpacing = st::boxRowPadding.left() / 2;\n\tconst auto totalThumbWidth = giftsCount * thumbSize.width()\n\t\t+ (giftsCount - 1) * thumbSpacing;\n\tconst auto available = width - extend.left() - extend.right();\n\tconst auto skip = (totalThumbWidth > available)\n\t\t? (available - giftsCount * thumbSize.width()) / (giftsCount - 1)\n\t\t: thumbSpacing;\n\tconst auto full = giftsCount * thumbSize.width()\n\t\t+ (giftsCount - 1) * skip;\n\tauto x = (width - full) / 2;\n\tconst auto y = shared->forgeRect.bottom() + st::craftFailureThumbsTop;\n\tconst auto rubberOut = st::lineWidth;\n\n\tfor (const auto &gift : shared->lostGifts) {\n\t\tif (gift.cornerIndex < 0) {\n\t\t\tx += thumbSize.width() + skip;\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto &corner = shared->corners[gift.cornerIndex];\n\t\tconst auto giftFrame = corner.gift(0.);\n\t\tif (!giftFrame.isNull()) {\n\t\t\tconst auto targetRect = QRect(QPoint(x, y), thumbSize);\n\t\t\tp.drawImage(targetRect, giftFrame);\n\n\t\t\tif (!gift.number.isEmpty()) {\n\t\t\t\tif (gift.badgeCache.isNull()) {\n\t\t\t\t\tconst auto burnedBg = BurnedBadgeBg();\n\t\t\t\t\tgift.badgeCache = ValidateRotatedBadge(GiftBadge{\n\t\t\t\t\t\t.text = gift.number,\n\t\t\t\t\t\t.bg1 = burnedBg,\n\t\t\t\t\t\t.bg2 = burnedBg,\n\t\t\t\t\t\t.fg = st::white->c,\n\t\t\t\t\t\t.small = true,\n\t\t\t\t\t}, QMargins());\n\t\t\t\t}\n\t\t\t\tconst auto inner = targetRect.marginsRemoved(extend);\n\t\t\t\tp.save();\n\t\t\t\tp.setClipRect(inner.marginsAdded(\n\t\t\t\t\t{ rubberOut, rubberOut, rubberOut, rubberOut }));\n\t\t\t\tconst auto badgeW = gift.badgeCache.width()\n\t\t\t\t\t/ gift.badgeCache.devicePixelRatio();\n\t\t\t\tp.drawImage(\n\t\t\t\t\tinner.x() + inner.width() + rubberOut - badgeW,\n\t\t\t\t\tinner.y() - rubberOut,\n\t\t\t\t\tgift.badgeCache);\n\t\t\t\tp.restore();\n\t\t\t}\n\t\t}\n\t\tx += thumbSize.width() + skip;\n\t}\n\n\tp.setOpacity(1.);\n}\n\n[[nodiscard]] std::array<QPointF, 4> RotateCornersForFace(\n\t\tstd::array<QPointF, 4> corners,\n\t\tint rotation) {\n\tconst auto steps = (rotation / 90) % 4;\n\tfor (auto i = 0; i < steps; ++i) {\n\t\tcorners = {\n\t\t\tcorners[1],\n\t\t\tcorners[2],\n\t\t\tcorners[3],\n\t\t\tcorners[0],\n\t\t};\n\t}\n\treturn corners;\n}\n\nvoid StartGiftFlight(\n\t\tnot_null<CraftAnimationState*> state,\n\t\tnot_null<RpWidget*> canvas,\n\t\tint startIndex) {\n\tconst auto &corners = state->shared->corners;\n\n\tauto nextGift = -1;\n\tfor (auto i = startIndex; i < 4; ++i) {\n\t\tif (corners[i].giftButton) {\n\t\t\tnextGift = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tif (nextGift < 0) {\n\t\treturn;\n\t}\n\n\tstate->currentlyFlying = nextGift;\n\tstate->giftToSide[nextGift] = GetCameraFacingFreeFace(state);\n\tstate->flightTargetCorners = std::nullopt;\n\n\tstate->flightAnimation.start(\n\t\t[=] { canvas->update(); },\n\t\t0.,\n\t\t1.,\n\t\tkFlightDuration,\n\t\tanim::easeInCubic);\n\n\tif (!state->continuousAnimation.animating()) {\n\t\tstate->continuousAnimation.start();\n\t}\n}\n\nvoid StartGiftFlightToFace(\n\t\tnot_null<CraftAnimationState*> state,\n\t\tnot_null<RpWidget*> canvas,\n\t\tint targetFace) {\n\tconst auto shared = state->shared.get();\n\tconst auto &corners = shared->corners;\n\n\tauto nextCorner = -1;\n\tfor (auto i = 0; i < 4; ++i) {\n\t\tif (corners[i].giftButton && state->giftToSide[i].face < 0) {\n\t\t\tnextCorner = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tif (nextCorner < 0) {\n\t\treturn;\n\t}\n\n\tstate->currentlyFlying = nextCorner;\n\n\tconst auto faceRotation = state->nextFaceRotation;\n\tstate->giftToSide[nextCorner] = FacePlacement{\n\t\t.face = targetFace,\n\t\t.rotation = faceRotation,\n\t};\n\n\tif (state->currentPhaseIndex >= 0) {\n\t\tconst auto &config = kGiftAnimations[state->currentPhaseIndex];\n\t\tconst auto initial = Rotation{\n\t\t\tstate->initialRotationX,\n\t\t\tstate->initialRotationY,\n\t\t};\n\t\tconst auto endRotation = config.rotation(initial, config.duration);\n\n\t\tconst auto cubeSize = float64(shared->forgeRect.width());\n\t\tconst auto cubeCenter = QPointF(shared->forgeRect.topLeft())\n\t\t\t+ QPointF(cubeSize, cubeSize) / 2.;\n\n\t\tconst auto targetCorners = ComputeCubeFaceCorners(\n\t\t\tcubeCenter,\n\t\t\tcubeSize,\n\t\t\tendRotation.x,\n\t\t\tendRotation.y,\n\t\t\ttargetFace);\n\t\tif (targetCorners) {\n\t\t\tstate->flightTargetCorners = RotateCornersForFace(\n\t\t\t\t*targetCorners,\n\t\t\t\tfaceRotation);\n\t\t} else {\n\t\t\tstate->flightTargetCorners = std::nullopt;\n\t\t}\n\t} else {\n\t\tstate->flightTargetCorners = std::nullopt;\n\t}\n\n\tstate->flightAnimation.start([=] {\n\t\tcanvas->update();\n\t}, 0., 1., kFlightDuration, anim::easeInCubic);\n\n\tif (!state->continuousAnimation.animating()) {\n\t\tstate->continuousAnimation.start();\n\t}\n}\n\nvoid LandCurrentGift(not_null<CraftAnimationState*> state, crl::time now) {\n\tif (state->currentlyFlying < 0) {\n\t\treturn;\n\t}\n\n\tconst auto giftNumber = ++state->giftsLanded;\n\tconst auto isLastGift\n\t\t= state->allGiftsLanded\n\t\t= (giftNumber >= state->totalGifts);\n\n\tconst auto configIndex = (giftNumber - 1) * 2 + (isLastGift ? 0 : 1);\n\tAssert(configIndex < 7);\n\n\tconst auto &config = kGiftAnimations[configIndex];\n\n\tstate->currentlyFlying = -1;\n\n\tstate->initialRotationX = state->rotationX;\n\tstate->initialRotationY = state->rotationY;\n\tstate->currentPhaseIndex = configIndex;\n\tstate->currentPhaseFinished = false;\n\tstate->animationStartTime = now;\n\tstate->animationDuration = config.duration;\n\tstate->nextFaceIndex = config.targetFaceIndex;\n\tstate->nextFaceRotation = config.targetFaceRotation;\n\tstate->nextFaceRevealed = false;\n}\n\nvoid PaintLoadingAnimation(\n\t\tQPainter &p,\n\t\tnot_null<CraftAnimationState*> state) {\n\tconst auto &loading = state->loadingAnimation;\n\tif (!loading || !state->loadingStartedTime) {\n\t\treturn;\n\t}\n\n\tconst auto loadingShown = state->loadingShownAnimation.value(\n\t\tstate->loadingFadingOut ? 0. : 1.);\n\tif (loadingShown <= 0.) {\n\t\treturn;\n\t}\n\n\tconst auto shared = state->shared.get();\n\tconst auto forge = shared->forgeRect;\n\tconst auto inner = forge.marginsRemoved({\n\t\tst::craftForgePadding,\n\t\tst::craftForgePadding,\n\t\tst::craftForgePadding,\n\t\tst::craftForgePadding,\n\t});\n\n\tconst auto radial = loading->computeState();\n\tconst auto adjustedState = RadialState{\n\t\t.shown = radial.shown * loadingShown,\n\t\t.arcFrom = radial.arcFrom,\n\t\t.arcLength = radial.arcLength,\n\t};\n\n\tauto pen = QPen(st::white->c);\n\tpen.setCapStyle(Qt::RoundCap);\n\tInfiniteRadialAnimation::Draw(\n\t\tp,\n\t\tadjustedState,\n\t\tinner.topLeft(),\n\t\tinner.size(),\n\t\tinner.width(),\n\t\tpen,\n\t\tst::craftForgeLoading.thickness);\n}\n\n} // namespace\n\nQImage CraftState::CornerSnapshot::gift(float64 progress) const {\n\tif (!giftButton) {\n\t\treturn QImage();\n\t} else if (progress == 1. && giftFrameFinal) {\n\t\treturn giftFrame;\n\t} else if (progress < 1.) {\n\t\tgiftFrameFinal = false;\n\t}\n\tif (giftButton->makeCraftFrameIsFinal(giftFrame, progress)) {\n\t\tgiftFrameFinal = true;\n\t}\n\treturn giftFrame;\n}\n\nvoid CraftState::paint(\n\t\tQPainter &p,\n\t\tQSize size,\n\t\tint craftingHeight,\n\t\tfloat64 slideProgress) {\n\tconst auto width = size.width();\n\tconst auto getBackdrop = [&](BackdropView &backdrop) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto gradientSize = size;\n\t\tauto &gradient = backdrop.gradient;\n\t\tif (gradient.size() != gradientSize * ratio) {\n\t\t\tgradient = CreateBgGradient(gradientSize, backdrop.colors);\n\t\t}\n\t\treturn gradient;\n\t};\n\tauto patternOffsetY = 0.;\n\tconst auto paintPattern = [&](\n\t\t\tQPainter &q,\n\t\t\tPatternView &pattern,\n\t\t\tconst BackdropView &backdrop,\n\t\t\tfloat64 shown) {\n\t\tconst auto color = backdrop.colors.patternColor;\n\t\tconst auto key = (color.red() << 16)\n\t\t\t| (color.green() << 8)\n\t\t\t| color.blue();\n\t\tif (patternOffsetY != 0.) {\n\t\t\tq.translate(0, patternOffsetY);\n\t\t}\n\t\tPaintBgPoints(\n\t\t\tq,\n\t\t\tPatternBgPoints(),\n\t\t\tpattern.emojis[key],\n\t\t\tpattern.emoji.get(),\n\t\t\tcolor,\n\t\t\tQRect(0, 0, width, st::boxTitleHeight + craftingHeight),\n\t\t\tshown);\n\t\tif (patternOffsetY != 0.) {\n\t\t\tq.translate(0, -patternOffsetY);\n\t\t}\n\t};\n\tauto animating = false;\n\tauto newEdgeColor = std::optional<QColor>();\n\tauto newButton1 = QColor();\n\tauto newButton2 = QColor();\n\tfor (auto i = 0; i != 5; ++i) {\n\t\tauto &cover = covers[i];\n\t\tif (cover.shownAnimation.animating()) {\n\t\t\tanimating = true;\n\t\t}\n\t\tconst auto finalValue = cover.shown ? 1. : 0.;\n\t\tconst auto shown = cover.shownAnimation.value(finalValue);\n\t\tif (shown <= 0.) {\n\t\t\tbreak;\n\t\t} else if (shown == 1.) {\n\t\t\tconst auto next = i + 1;\n\t\t\tif (next < 4\n\t\t\t\t&& covers[next].shown\n\t\t\t\t&& !covers[next].shownAnimation.animating()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tp.setOpacity(shown);\n\t\tp.drawImage(0, 0, getBackdrop(cover.backdrop));\n\t\tpaintPattern(p, cover.pattern, cover.backdrop, 1.);\n\t\tif (coversAnimate) {\n\t\t\tconst auto edge = cover.backdrop.colors.edgeColor;\n\t\t\tif (!newEdgeColor) {\n\t\t\t\tnewEdgeColor = edge;\n\t\t\t\tnewButton1 = cover.button1;\n\t\t\t\tnewButton2 = cover.button2;\n\t\t\t} else {\n\t\t\t\tnewEdgeColor = anim::color(*newEdgeColor, edge, shown);\n\t\t\t\tnewButton1 = anim::color(newButton1, cover.button1, shown);\n\t\t\t\tnewButton2 = anim::color(newButton2, cover.button2, shown);\n\t\t\t}\n\t\t}\n\t}\n\tif (newEdgeColor) {\n\t\tedgeColor = *newEdgeColor;\n\t\tbutton1 = newButton1;\n\t\tbutton2 = newButton2;\n\t}\n\tif (animating) {\n\t\tp.setOpacity(1.);\n\t} else {\n\t\tcoversAnimate = false;\n\t}\n}\n\nvoid CraftState::updateForGiftCount(int count, Fn<void()> repaint) {\n\tfor (auto i = 5; i != 0;) {\n\t\tauto &cover = covers[--i];\n\t\tconst auto shown = (i < count);\n\t\tif (cover.shown != shown) {\n\t\t\tcover.shown = shown;\n\t\t\tconst auto from = shown ? 0. : 1.;\n\t\t\tconst auto to = shown ? 1. : 0.;\n\t\t\tcover.shownAnimation.start(\n\t\t\t\trepaint,\n\t\t\t\tfrom,\n\t\t\t\tto,\n\t\t\t\tst::fadeWrapDuration);\n\t\t\tcoversAnimate = true;\n\t\t}\n\t}\n}\n\nCraftState::EmptySide CraftState::prepareEmptySide(int index) const {\n\tconst auto size = forgeRect.size();\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\n\tconst auto bg = anim::color(forgeBg1, forgeBg2, index / 5.);\n\tresult.fill(bg);\n\n\tauto p = QPainter(&result);\n\tst::craftForge.paintInCenter(p, QRect(QPoint(), size), st::white->c);\n\n//#if _DEBUG\n//\tp.setFont(st::semiboldFont);\n//\tp.setPen(st::white);\n//\tp.drawText(\n//\t\tsize.width() / 10,\n//\t\tsize.width() / 10 + st::semiboldFont->ascent,\n//\t\tQString::number(index));\n//#endif // _DEBUG\n\n\tp.end();\n\n\treturn { .bg = bg, .frame = result };\n}\n\nvoid SetupCraftProgressTitle(\n\t\tnot_null<VerticalLayout*> container,\n\t\tnot_null<rpl::variable<float64>*> opacity) {\n\tconst auto row = container->add(\n\t\tobject_ptr<RpWidget>(container),\n\t\tst::boxRowPadding + st::craftProgressTitleMargin,\n\t\tstyle::al_top);\n\n\tconst auto label = CreateChild<FlatLabel>(\n\t\trow,\n\t\ttr::lng_gift_craft_progress(),\n\t\tst::uniqueGiftTitle);\n\tlabel->setTextColorOverride(st::white->c);\n\n\tconst auto iconSize = st::craftProgressIconSize;\n\tconst auto iconWidget = CreateChild<RpWidget>(row);\n\ticonWidget->resize(iconSize);\n\n\tauto owned = Lottie::MakeIcon({\n\t\t.name = u\"craft_progress\"_q,\n\t\t.sizeOverride = iconSize,\n\t\t.limitFps = true,\n\t});\n\tconst auto icon = owned.get();\n\ticonWidget->lifetime().add([kept = std::move(owned)] {});\n\n\tconst auto startAnimation = [=] {\n\t\ticon->animate(\n\t\t\t[=] { iconWidget->update(); },\n\t\t\t0,\n\t\t\ticon->framesCount() - 1);\n\t};\n\tstartAnimation();\n\n\ticonWidget->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(iconWidget);\n\t\tp.setOpacity(opacity->current());\n\t\ticon->paint(p, 0, 0);\n\t\tif (!icon->animating() && icon->frameIndex() > 0) {\n\t\t\tstartAnimation();\n\t\t}\n\t}, iconWidget->lifetime());\n\n\trpl::combine(\n\t\tlabel->sizeValue(),\n\t\trow->sizeValue()\n\t) | rpl::on_next([=](QSize labelSize, QSize rowSize) {\n\t\tconst auto skip = st::craftProgressIconSkip;\n\t\tconst auto totalWidth = iconSize.width() + skip + labelSize.width();\n\t\tconst auto rowHeight = std::max(iconSize.height(), labelSize.height());\n\t\tif (rowSize.height() != rowHeight) {\n\t\t\trow->resize(rowSize.width(), rowHeight);\n\t\t\treturn;\n\t\t}\n\t\tconst auto left = (rowSize.width() - totalWidth) / 2;\n\t\ticonWidget->move(left, (rowHeight - iconSize.height()) / 2);\n\t\tlabel->move(\n\t\t\tleft + iconSize.width() + skip,\n\t\t\t(rowHeight - labelSize.height()) / 2);\n\t}, row->lifetime());\n\n\topacity->value(\n\t) | rpl::on_next([=](float64 value) {\n\t\tlabel->setOpacity(value);\n\t\ticonWidget->update();\n\t}, row->lifetime());\n\n}\n\nvoid SetupProgressControls(\n\t\tnot_null<CraftAnimationState*> state,\n\t\tnot_null<RpWidget*> canvas) {\n\tconst auto add = [&](not_null<FlatLabel*> label) {\n\t\tstate->progressOpacity.value() | rpl::on_next([=](float64 opacity) {\n\t\t\tlabel->setOpacity(opacity);\n\t\t}, label->lifetime());\n\t};\n\n\tconst auto controls = CreateChild<VerticalLayout>(canvas);\n\tcontrols->resizeToWidth(canvas->width());\n\tcontrols->move(0, state->shared->craftingBottom);\n\n\tSetupCraftProgressTitle(controls, &state->progressOpacity);\n\n\tconst auto subColor = QColor(255, 255, 255, 178);\n\tconst auto about = controls->add(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tcontrols,\n\t\t\ttr::lng_gift_craft_progress_about(\n\t\t\t\tlt_gift,\n\t\t\t\trpl::single(tr::marked(state->shared->giftName)),\n\t\t\t\ttr::rich),\n\t\t\tst::uniqueGiftSubtitle),\n\t\tst::boxRowPadding + st::craftProgressAboutMargin,\n\t\tstyle::al_top);\n\tadd(about);\n\tabout->setTextColorOverride(subColor);\n\n\tconst auto warning = controls->add(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tcontrols,\n\t\t\ttr::lng_gift_craft_progress_fails(),\n\t\t\tst::craftAbout),\n\t\tst::boxRowPadding + st::craftProgressWarningMargin,\n\t\tstyle::al_top);\n\tadd(warning);\n\twarning->setTextColorOverride(subColor);\n\n\tconst auto chance = controls->add(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tcontrols,\n\t\t\ttr::lng_gift_craft_progress_chance(\n\t\t\t\ttr::now,\n\t\t\t\tlt_percent,\n\t\t\t\tFormatPercent(state->shared->successPermille)),\n\t\t\tst::craftChance),\n\t\tst::boxRowPadding + st::craftProgressChanceMargin,\n\t\tstyle::al_top);\n\tadd(chance);\n\tchance->setTextColorOverride(st::white->c);\n\tchance->paintOn([=](QPainter &p) {\n\t\tif (const auto opacity = state->progressOpacity.current()) {\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setOpacity(opacity);\n\t\t\tp.setBrush(state->shared->forgeBgOverlay);\n\t\t\tconst auto radius = chance->height() / 2.;\n\t\t\tp.drawRoundedRect(chance->rect(), radius, radius);\n\t\t}\n\t});\n\n\tcontrols->showOn(state->progressOpacity.value(\n\t) | rpl::map(\n\t\trpl::mappers::_1 > 0.\n\t) | rpl::distinct_until_changed());\n}\n\nvoid SetupFailureControls(\n\t\tnot_null<CraftAnimationState*> state,\n\t\tnot_null<RpWidget*> canvas) {\n\tconst auto add = [&](not_null<FlatLabel*> label) {\n\t\tstate->failureOpacity.value() | rpl::on_next([=](float64 opacity) {\n\t\t\tlabel->setOpacity(opacity);\n\t\t}, label->lifetime());\n\t};\n\n\tconst auto controls = CreateChild<VerticalLayout>(canvas);\n\tcontrols->resizeToWidth(canvas->width());\n\tcontrols->move(0, state->shared->craftingBottom);\n\n\tconst auto title = controls->add(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tcontrols,\n\t\t\ttr::lng_gift_craft_failed_title(),\n\t\t\tst::uniqueGiftTitle),\n\t\tst::boxRowPadding + st::craftFailureTitleMargin,\n\t\tstyle::al_top);\n\tadd(title);\n\ttitle->setTextColorOverride(QColor(0xF8, 0x4A, 0x4A));\n\n\tconst auto about = controls->add(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tcontrols,\n\t\t\ttr::lng_gift_craft_failed_about(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(state->shared->lostGifts.size() * 1.),\n\t\t\t\ttr::rich),\n\t\t\tst::uniqueGiftSubtitle),\n\t\tst::boxRowPadding + st::craftFailureAboutMargin,\n\t\tstyle::al_top);\n\tadd(about);\n\tabout->setTextColorOverride(QColor(0xFF, 0xBC, 0x9B));\n\n\tcontrols->showOn(state->failureOpacity.value(\n\t) | rpl::map(\n\t\trpl::mappers::_1 > 0.\n\t) | rpl::distinct_until_changed());\n}\n\nvoid SetupCraftNewButton(\n\t\tnot_null<CraftAnimationState*> state,\n\t\tnot_null<RpWidget*> canvas) {\n\tconst auto button = CreateChild<GradientButton>(\n\t\tcanvas,\n\t\tQGradientStops{\n\t\t\t{ 0., QColor(0xE2, 0x75, 0x19) },\n\t\t\t{ 1., QColor(0xDD, 0x48, 0x19) },\n\t\t});\n\tbutton->setFullRadius(true);\n\tbutton->setClickedCallback([=] {\n\t\tstate->retryWithNewGift();\n\t});\n\n\tconst auto buttonLabel = CreateChild<FlatLabel>(\n\t\tbutton,\n\t\ttr::lng_gift_craft_new_button(),\n\t\tst::creditsBoxButtonLabel);\n\tbuttonLabel->setTextColorOverride(st::white->c);\n\tbuttonLabel->setAttribute(\n\t\tQt::WA_TransparentForMouseEvents);\n\tbutton->sizeValue() | rpl::on_next([=](QSize size) {\n\t\tbuttonLabel->moveToLeft(\n\t\t\t(size.width() - buttonLabel->width()) / 2,\n\t\t\tst::giftBox.button.textTop);\n\t}, buttonLabel->lifetime());\n\n\tconst auto buttonWidth = canvas->width()\n\t\t- st::craftBoxButtonPadding.left()\n\t\t- st::craftBoxButtonPadding.right();\n\tconst auto buttonHeight = st::giftBox.button.height;\n\tbutton->setNaturalWidth(buttonWidth);\n\tbutton->setGeometry(\n\t\tst::craftBoxButtonPadding.left(),\n\t\tstate->shared->originalButtonY,\n\t\tbuttonWidth,\n\t\tbuttonHeight);\n\tbutton->show();\n\n\tbutton->shownValue() | rpl::filter(\n\t\trpl::mappers::_1\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\tbutton->startGlareAnimation();\n\t}, button->lifetime());\n}\n\nvoid StartCraftAnimation(\n\t\tnot_null<GenericBox*> box,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tstd::shared_ptr<CraftState> shared,\n\t\tFn<void(Fn<void(CraftResult)> callback)> startRequest,\n\t\tFn<void(Fn<void()> closeCurrent)> retryWithNewGift) {\n\tconst auto container = box->verticalLayout();\n\twhile (container->count() > 0) {\n\t\tdelete container->widgetAt(0);\n\t}\n\n\tauto canvas = object_ptr<RpWidget>(container);\n\tconst auto raw = canvas.data();\n\tconst auto state = raw->lifetime().make_state<CraftAnimationState>();\n\n\tstate->retryWithNewGift = [=] {\n\t\tretryWithNewGift(crl::guard(box, [=] { box->closeBox(); }));\n\t};\n\n\tconst auto title = CreateChild<FlatLabel>(\n\t\traw,\n\t\ttr::lng_gift_craft_title(),\n\t\tst::uniqueGiftTitle);\n\ttitle->naturalWidthValue() | rpl::on_next([=](int titleWidth) {\n\t\tconst auto width = st::boxWideWidth;\n\t\ttitle->moveToLeft(\n\t\t\t(width - titleWidth) / 2,\n\t\t\tst::craftTitleMargin.top(),\n\t\t\twidth);\n\t}, title->lifetime());\n\ttitle->setTextColorOverride(st::white->c);\n\n\tconst auto height = shared->containerHeight;\n\tconst auto craftingHeight = shared->craftingBottom - shared->craftingTop;\n\tstate->shared = std::move(shared);\n\tstate->shared->craftingStarted = true;\n\n\tconst auto craftingSize = QSize(container->width(), height);\n\traw->resize(craftingSize);\n\n\tfor (auto &corner : state->shared->corners) {\n\t\tif (corner.giftButton) {\n\t\t\t++state->totalGifts;\n\t\t}\n\t}\n\n\tstate->loadingAnimation = std::make_unique<InfiniteRadialAnimation>(\n\t\t[=] { raw->update(); },\n\t\tst::craftForgeLoading);\n\n\tSetupProgressControls(state, raw);\n\tSetupFailureControls(state, raw);\n\n\tstate->progressShown.value(\n\t) | rpl::combine_previous(\n\t) | rpl::on_next([=](bool wasShown, bool nowShown) {\n\t\tif (wasShown == nowShown) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto from = wasShown ? 1. : 0.;\n\t\tconst auto to = nowShown ? 1. : 0.;\n\t\tstate->progressFadeIn.start([=] {\n\t\t\traw->update();\n\t\t}, from, to, kProgressFadeInDuration, anim::easeOutCubic);\n\t}, raw->lifetime());\n\n\tstate->failureShown.value(\n\t) | rpl::combine_previous(\n\t) | rpl::on_next([=](bool wasShown, bool nowShown) {\n\t\tif (wasShown == nowShown) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto from = wasShown ? 1. : 0.;\n\t\tconst auto to = nowShown ? 1. : 0.;\n\t\tstate->failureFadeIn.start([=] {\n\t\t\traw->update();\n\t\t\tif (!state->failureFadeIn.animating()) {\n\t\t\t\tSetupCraftNewButton(state, raw);\n\t\t\t}\n\t\t}, from, to, kFailureFadeDuration, anim::easeOutCubic);\n\t}, raw->lifetime());\n\n\traw->paintOn([=](QPainter &p) {\n\t\tconst auto shared = state->shared.get();\n\t\tconst auto success = state->successAnimation.get();\n\t\tconst auto failure = state->failureAnimation.get();\n\t\tconst auto slideProgress = state->slideOutAnimation.value(1.);\n\t\tif (slideProgress < 1. || (failure && failure->started)) {\n\t\t\tshared->paint(p, craftingSize, craftingHeight, slideProgress);\n\t\t} else {\n\t\t\tif (shared->craftBg.isNull()) {\n\t\t\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t\t\tshared->craftBg = QImage(\n\t\t\t\t\tcraftingSize * ratio,\n\t\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t\tshared->craftBg.setDevicePixelRatio(ratio);\n\t\t\t\tshared->craftBg.fill(Qt::transparent);\n\n\t\t\t\tauto q = QPainter(&shared->craftBg);\n\t\t\t\tshared->paint(q, craftingSize, craftingHeight, 1.);\n\t\t\t}\n\t\t\tif (success && success->hiding) {\n\t\t\t\tp.setOpacity(1. - success->heightAnimation.value(1.));\n\t\t\t}\n\t\t\tp.drawImage(0, 0, shared->craftBg);\n\t\t\tif (success && success->hiding) {\n\t\t\t\tp.setOpacity(1.);\n\t\t\t}\n\t\t}\n\n\t\tif (slideProgress < 1.) {\n\t\t\tPaintSlideOutPhase(p, shared, craftingSize, slideProgress);\n\t\t} else {\n\t\t\tconst auto craftingOffsetY = success\n\t\t\t\t? (-success->coverShift * success->expanded)\n\t\t\t\t: 0.;\n\t\t\tif (success && success->expanded > 0.) {\n\t\t\t\tconst auto width = st::boxWideWidth;\n\t\t\t\tconst auto top = st::craftTitleMargin.top();\n\t\t\t\ttitle->moveToLeft(\n\t\t\t\t\t(width - title->naturalWidth()) / 2,\n\t\t\t\t\ttop - (success->expanded * (top + title->height())),\n\t\t\t\t\twidth);\n\t\t\t}\n\t\t\tconst auto cubeSize = float64(shared->forgeRect.width());\n\t\t\tconst auto cubeCenter = QPointF(shared->forgeRect.topLeft())\n\t\t\t\t+ QPointF(cubeSize, cubeSize) / 2.\n\t\t\t\t+ QPointF(0, craftingOffsetY);\n\n\t\t\tfor (auto i = 0; i < 4; ++i) {\n\t\t\t\tconst auto &corner = shared->corners[i];\n\t\t\t\tif (corner.giftButton && state->giftToSide[i].face < 0) {\n\t\t\t\t\tconst auto giftTopLeft = QPointF()\n\t\t\t\t\t\t+ QPointF(corner.originalRect.topLeft())\n\t\t\t\t\t\t+ QPointF(0, craftingOffsetY);\n\t\t\t\t\tp.drawImage(giftTopLeft, corner.gift(0));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst auto flying = state->currentlyFlying;\n\t\t\tconst auto firstFlyProgress = (flying == 0)\n\t\t\t\t? state->flightAnimation.value(1.)\n\t\t\t\t: state->giftsLanded\n\t\t\t\t? 1.\n\t\t\t\t: 0.;\n\t\t\tconst auto loadingShown = state->loadingStartedTime\n\t\t\t\t&& !state->loadingFadingOut;\n\t\t\tif (firstFlyProgress < 1.) {\n\t\t\t\tconst auto skipForgeIcon = loadingShown && anim::Disabled();\n\t\t\t\tPaintCubeFirstFlight(\n\t\t\t\t\tp,\n\t\t\t\t\tstate,\n\t\t\t\t\tfirstFlyProgress,\n\t\t\t\t\tskipForgeIcon);\n\t\t\t} else {\n\t\t\t\tPaintCube(p, state, raw, cubeCenter, cubeSize);\n\t\t\t}\n\n\t\t\tif (flying >= 0) {\n\t\t\t\tconst auto &corner = shared->corners[flying];\n\t\t\t\tconst auto progress = (flying > 0)\n\t\t\t\t\t? state->flightAnimation.value(1.)\n\t\t\t\t\t: firstFlyProgress;\n\n\t\t\t\tif (state->flightTargetCorners) {\n\t\t\t\t\tconst auto sourceRect = QRectF(corner.originalRect)\n\t\t\t\t\t\t.translated(0, craftingOffsetY);\n\t\t\t\t\tconst auto sourceCorners = RectToCorners(sourceRect);\n\t\t\t\t\tconst auto interpolatedCorners = InterpolateQuadCorners(\n\t\t\t\t\t\tsourceCorners,\n\t\t\t\t\t\t*state->flightTargetCorners,\n\t\t\t\t\t\tprogress);\n\t\t\t\t\tPaintFlyingGiftWithQuad(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tcorner,\n\t\t\t\t\t\tinterpolatedCorners,\n\t\t\t\t\t\tprogress);\n\t\t\t\t} else {\n\t\t\t\t\tconst auto position = ComputeGiftFlightPosition(\n\t\t\t\t\t\tcorner.originalRect,\n\t\t\t\t\t\tcubeCenter,\n\t\t\t\t\t\tcubeSize,\n\t\t\t\t\t\tprogress,\n\t\t\t\t\t\tcraftingOffsetY);\n\t\t\t\t\tPaintFlyingGift(p, corner, position, progress);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tPaintLoadingAnimation(p, state);\n\n\t\t\tif (const auto opacity = state->failureOpacity.current()) {\n\t\t\t\tPaintFailureThumbnails(\n\t\t\t\t\tp,\n\t\t\t\t\tshared,\n\t\t\t\t\tcraftingSize,\n\t\t\t\t\topacity);\n\t\t\t}\n\t\t}\n\n\t\tstate->progressOpacity = state->progressFadeIn.value(\n\t\t\tstate->progressShown.current() ? 1. : 0.);\n\t\tstate->failureOpacity = state->failureFadeIn.value(\n\t\t\tstate->failureShown.current() ? 1. : 0.);\n\t});\n\n\tconst auto startFlying = [=] {\n\t\tif (state->loadingStartedTime > 0) {\n\t\t\tstate->loadingFadingOut = true;\n\t\t\tstate->loadingShownAnimation.start(\n\t\t\t\t[=] { raw->update(); },\n\t\t\t\t1.,\n\t\t\t\t0.,\n\t\t\t\tkLoadingFadeOutDuration);\n\t\t}\n\t\tStartGiftFlight(state, raw, 0);\n\t};\n\n\tconst auto tryStartFlying = [=] {\n\t\tconst auto slideFinished = !state->slideOutAnimation.animating();\n\t\tconst auto resultArrived = state->craftResult.has_value();\n\t\tconst auto notYetFlying = (state->currentlyFlying < 0)\n\t\t\t&& (state->giftsLanded == 0);\n\t\tif (!slideFinished || !notYetFlying) {\n\t\t\treturn;\n\t\t}\n\t\tif (!resultArrived) {\n\t\t\tif (!state->loadingStartedTime) {\n\t\t\t\tstate->loadingStartedTime = crl::now();\n\t\t\t\tstate->loadingAnimation->start();\n\t\t\t\tstate->loadingShownAnimation.start(\n\t\t\t\t\t[=] { raw->update(); },\n\t\t\t\t\t0.,\n\t\t\t\t\t1.,\n\t\t\t\t\tkLoadingFadeInDuration);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (!state->loadingStartedTime) {\n\t\t\tstartFlying();\n\t\t\treturn;\n\t\t}\n\t\tconst auto elapsed = crl::now() - state->loadingStartedTime;\n\t\tif (elapsed >= kLoadingMinDuration) {\n\t\t\tstartFlying();\n\t\t} else {\n\t\t\tbase::call_delayed(\n\t\t\t\tkLoadingMinDuration - elapsed,\n\t\t\t\traw,\n\t\t\t\tstartFlying);\n\t\t}\n\t};\n\n\tstate->progressShown = true;\n\n\tstate->slideOutAnimation.start([=] {\n\t\traw->update();\n\n\t\tconst auto progress = state->slideOutAnimation.value(1.);\n\t\tif (progress >= 1.) {\n\t\t\ttryStartFlying();\n\t\t}\n\t}, 0., 1., crl::time(300), anim::easeOutCubic);\n\n\tstate->continuousAnimation.init([=](crl::time now) {\n\t\tif (state->currentPhaseIndex >= 0) {\n\t\t\tconst auto &config = kGiftAnimations[state->currentPhaseIndex];\n\t\t\tconst auto elapsed = state->currentPhaseFinished\n\t\t\t\t? (config.duration * Slowing())\n\t\t\t\t: (now - state->animationStartTime);\n\t\t\tconst auto initial = Rotation{\n\t\t\t\tstate->initialRotationX,\n\t\t\t\tstate->initialRotationY,\n\t\t\t};\n\t\t\tconst auto r = config.rotation(initial, elapsed / Slowing());\n\t\t\tstate->rotationX = r.x;\n\t\t\tstate->rotationY = r.y;\n\t\t}\n\n\t\traw->update();\n\n\t\tif (state->currentlyFlying >= 0\n\t\t\t&& !state->flightAnimation.animating()) {\n\t\t\tLandCurrentGift(state, now);\n\t\t}\n\n\t\tif (!state->flightAnimation.animating()\n\t\t\t&& state->currentlyFlying < 0\n\t\t\t&& state->giftsLanded > 0\n\t\t\t&& state->giftsLanded < state->totalGifts\n\t\t\t&& state->currentPhaseIndex >= 0) {\n\t\t\tconst auto elapsed = now - state->animationStartTime;\n\t\t\tconst auto start = state->animationDuration - kFlightDuration;\n\t\t\tif (elapsed >= start * Slowing()) {\n\t\t\t\tStartGiftFlightToFace(state, raw, state->nextFaceIndex);\n\t\t\t}\n\t\t}\n\n\t\tconst auto success = state->successAnimation.get();\n\t\tif (!state->currentPhaseFinished\n\t\t\t&& !state->flightAnimation.animating()\n\t\t\t&& state->currentlyFlying < 0\n\t\t\t&& state->giftsLanded >= state->totalGifts\n\t\t\t&& state->totalGifts > 0\n\t\t\t&& state->currentPhaseIndex >= 0) {\n\t\t\tconst auto elapsed = now - state->animationStartTime;\n\t\t\tif (elapsed >= state->animationDuration * Slowing()) {\n\t\t\t\tstate->currentPhaseFinished = true;\n\t\t\t\tif (success) {\n\t\t\t\t\tconst auto wasContent = box->height();\n\t\t\t\t\tconst auto wasTotal = box->parentWidget()->height();\n\t\t\t\t\tconst auto wasButtons = wasTotal - wasContent;\n\n\t\t\t\t\tcontainer->clear();\n\t\t\t\t\tcontainer->add(std::move(success->owned));\n\n\t\t\t\t\tbox->setStyle(st::giftBox);\n\n\t\t\t\t\tauto craftAnotherCallback = crl::guard(box, [=] {\n\t\t\t\t\t\tretryWithNewGift([=] { box->closeBox(); });\n\t\t\t\t\t});\n\n\t\t\t\t\tconst auto entry = EntryForUpgradedGift(\n\t\t\t\t\t\t*state->craftResult,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tnullptr,\n\t\t\t\t\t\tcraftAnotherCallback);\n\t\t\t\t\tSettings::GenericCreditsEntryBody(box, show, entry);\n\t\t\t\t\tcontainer->resizeToWidth(st::boxWideWidth);\n\n\t\t\t\t\tconst auto nowContent = box->height();\n\t\t\t\t\tconst auto nowTotal = box->parentWidget()->height();\n\t\t\t\t\tconst auto nowButtons = nowTotal - nowContent;\n\n\t\t\t\t\tbox->animateHeightFrom(\n\t\t\t\t\t\twasContent + wasButtons - nowButtons);\n\t\t\t\t\tsuccess->hiding = true;\n\t\t\t\t\tconst auto duration = kSuccessExpandDuration\n\t\t\t\t\t\t- kSuccessExpandStart;\n\t\t\t\t\tsuccess->heightAnimation.start([=] {\n\t\t\t\t\t\tconst auto height = anim::interpolate(\n\t\t\t\t\t\t\tcraftingSize.height(),\n\t\t\t\t\t\t\tsuccess->coverHeight,\n\t\t\t\t\t\t\tsuccess->heightAnimation.value(1.));\n\t\t\t\t\t\traw->resize(craftingSize.width(), height);\n\t\t\t\t\t}, 0., 1., duration, anim::easeOutCubic);\n\n\t\t\t\t\traw->setParent(box->parentWidget());\n\t\t\t\t\traw->show();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (success && success->finished) {\n\t\t\tstate->continuousAnimation.stop();\n\t\t\traw->hide();\n\n\t\t\tStartFireworks(box->parentWidget());\n\t\t}\n\t\treturn true;\n\t});\n\n\tcontainer->add(std::move(canvas));\n\n\tif (startRequest) {\n\t\tconst auto weak = base::make_weak(raw);\n\t\tstartRequest([=](CraftResult result) {\n\t\t\tif (!weak) {\n\t\t\t\treturn;\n\t\t\t} else if (v::is<CraftResultError>(result)) {\n\t\t\t\tbox->uiShow()->show(MakeInformBox({\n\t\t\t\t\t.text = v::get<CraftResultError>(result).type,\n\t\t\t\t}));\n\t\t\t\tbox->closeBox();\n\t\t\t\treturn;\n\t\t\t} else if (v::is<CraftResultWait>(result)) {\n\t\t\t\tconst auto when = base::unixtime::now()\n\t\t\t\t\t+ v::get<CraftResultWait>(result).seconds;\n\t\t\t\tShowCraftLaterError(box->uiShow(), when);\n\t\t\t\tbox->closeBox();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tusing Result = std::shared_ptr<Data::GiftUpgradeResult>;\n\t\t\tstate->craftResult = v::get<Result>(result);\n\t\t\tif (const auto result = state->craftResult->get()) {\n\t\t\t\tauto numberText = (result->info.unique && result->info.unique->number > 0)\n\t\t\t\t\t? rpl::single(u\"#\"_q + Lang::FormatCountDecimal(result->info.unique->number))\n\t\t\t\t\t: rpl::producer<QString>();\n\n\t\t\t\tstate->successAnimation = std::make_unique<\n\t\t\t\t\tCraftDoneAnimation\n\t\t\t\t>(CraftDoneAnimation{\n\t\t\t\t\t.owned = MakeUniqueGiftCover(\n\t\t\t\t\t\tcontainer,\n\t\t\t\t\t\trpl::single(UniqueGiftCover{ *result->info.unique }),\n\t\t\t\t\t\t{ .numberText = std::move(numberText) }),\n\t\t\t\t});\n\t\t\t\tconst auto success = state->successAnimation.get();\n\n\t\t\t\tconst auto raw = success->owned.get();\n\t\t\t\tsuccess->widget = raw;\n\t\t\t\traw->hide();\n\t\t\t\traw->resizeToWidth(st::boxWideWidth);\n\t\t\t\tSendPendingMoveResizeEvents(raw);\n\n\t\t\t\tsuccess->coverHeight = raw->height();\n\t\t\t\tsuccess->coverShift = state->shared->forgeRect.y()\n\t\t\t\t\t+ (state->shared->forgeRect.height() / 2)\n\t\t\t\t\t- (raw->height() / 2);\n\t\t\t}\n\t\t\ttryStartFlying();\n\t\t});\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/star_gift_craft_animation.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"base/unique_qptr.h\"\n#include \"data/data_star_gift.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"ui/text/text_custom_emoji.h\"\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace HistoryView {\nclass StickerPlayer;\n} // namespace HistoryView\n\nnamespace Info::PeerGifts {\nclass GiftButton;\n} // namespace Info::PeerGifts\n\nnamespace Ui {\n\nclass FlatLabel;\nclass GenericBox;\nclass RpWidget;\nclass VerticalLayout;\nclass UniqueGiftCoverWidget;\n\nstruct CraftResultError {\n\tQString type;\n};\n\nstruct CraftResultWait {\n\tTimeId seconds = 0;\n};\n\nusing CraftResult = std::variant<\n\tstd::shared_ptr<Data::GiftUpgradeResult>,\n\tCraftResultError,\n\tCraftResultWait>;\n\nstruct BackdropView {\n\tData::UniqueGiftBackdrop colors;\n\tQImage gradient;\n};\n\nstruct PatternView {\n\tstd::unique_ptr<Text::CustomEmoji> emoji;\n\tbase::flat_map<int, base::flat_map<float64, QImage>> emojis;\n};\n\nstruct CraftState {\n\tstruct Cover {\n\t\tBackdropView backdrop;\n\t\tPatternView pattern;\n\t\tQColor button1;\n\t\tQColor button2;\n\t\tAnimations::Simple shownAnimation;\n\t\tbool shown = false;\n\t};\n\n\tstd::array<Cover, 5> covers; // Last one for failed background.\n\trpl::variable<QColor> edgeColor;\n\tQColor button1;\n\tQColor button2;\n\tbool coversAnimate = false;\n\tbool craftingStarted = false;\n\tQImage craftBg;\n\n\tQImage topPart;\n\tQRect topPartRect;\n\tQImage bottomPart;\n\tint bottomPartY = 0;\n\n\tstruct CornerSnapshot {\n\t\tbase::unique_qptr<Info::PeerGifts::GiftButton> giftButton;\n\t\tmutable QImage giftFrame;\n\t\tQImage percentBadge;\n\t\tQImage removeButton;\n\t\tQImage addButton;\n\t\tQRect originalRect;\n\t\tmutable bool giftFrameFinal = false;\n\n\t\t[[nodiscard]] QImage gift(float64 progress) const;\n\t};\n\tstd::array<CornerSnapshot, 4> corners;\n\n\tQRect forgeRect;\n\n\tQColor forgeBgOverlay;\n\tQColor forgeBg1;\n\tQColor forgeBg2;\n\tQColor forgeFail;\n\tQImage forgePercent;\n\n\tstruct EmptySide {\n\t\tQColor bg;\n\t\tQImage frame;\n\t};\n\tstd::array<EmptySide, 6> forgeSides;\n\n\tMain::Session *session = nullptr;\n\n\tint containerHeight = 0;\n\tint craftingTop = 0;\n\tint craftingBottom = 0;\n\tint craftingAreaCenterY = 0;\n\tint originalButtonY = 0;\n\n\tQString giftName;\n\tint successPermille = 0;\n\tstruct LostGift {\n\t\tint cornerIndex = -1;\n\t\tQString number;\n\t\tmutable QImage badgeCache;\n\t};\n\tstd::vector<LostGift> lostGifts;\n\tQImage lostRadial;\n\n\tvoid paint(\n\t\tQPainter &p,\n\t\tQSize size,\n\t\tint craftingHeight,\n\t\tfloat64 slideProgress = 0.);\n\tvoid updateForGiftCount(int count, Fn<void()> repaint);\n\t[[nodiscard]] EmptySide prepareEmptySide(int index) const;\n};\n\nstruct FacePlacement {\n\tint face = -1;\n\tint rotation = 0;\n};\n\nstruct CraftDoneAnimation {\n\tobject_ptr<UniqueGiftCoverWidget> owned;\n\tUniqueGiftCoverWidget *widget = nullptr;\n\tAnimations::Simple shownAnimation;\n\tAnimations::Simple heightAnimation;\n\tQImage frame;\n\tint coverHeight = 0;\n\tint coverShift = 0;\n\tfloat64 expanded = 0.;\n\tbool finished = false;\n\tbool hiding = false;\n};\n\nstruct CraftFailAnimation {\n\tQImage frame;\n\tstd::unique_ptr<Lottie::Icon> lottie;\n\tbool started = false;\n\tint finalCoverIndex = -1;\n\trpl::lifetime lifetime;\n};\n\nstruct CraftAnimationState {\n\tstd::shared_ptr<CraftState> shared;\n\n\tAnimations::Simple slideOutAnimation;\n\tAnimations::Basic continuousAnimation;\n\n\tfloat64 rotationX = 0.;\n\tfloat64 rotationY = 0.;\n\n\tint currentlyFlying = -1;\n\tint giftsLanded = 0;\n\tint totalGifts = 0;\n\tbool allGiftsLanded = false;\n\tbool currentPhaseFinished = false;\n\tstd::array<FacePlacement, 4> giftToSide;\n\tAnimations::Simple flightAnimation;\n\n\tint currentPhaseIndex = -1;\n\tcrl::time animationStartTime = 0;\n\tcrl::time animationDuration = 0;\n\tfloat64 initialRotationX = 0.;\n\tfloat64 initialRotationY = 0.;\n\tint nextFaceIndex = 0;\n\tint nextFaceRotation = 0;\n\tbool nextFaceRevealed = false;\n\n\tstd::optional<std::array<QPointF, 4>> flightTargetCorners;\n\n\tstd::optional<std::shared_ptr<Data::GiftUpgradeResult>> craftResult;\n\tstd::unique_ptr<InfiniteRadialAnimation> loadingAnimation;\n\tAnimations::Simple loadingShownAnimation;\n\tcrl::time loadingStartedTime = 0;\n\tbool loadingFadingOut = false;\n\n\tstd::unique_ptr<CraftDoneAnimation> successAnimation;\n\tstd::unique_ptr<CraftFailAnimation> failureAnimation;\n\n\trpl::variable<float64> progressOpacity;\n\tAnimations::Simple progressFadeIn;\n\trpl::variable<bool> progressShown;\n\n\trpl::variable<float64> failureOpacity;\n\tAnimations::Simple failureFadeIn;\n\trpl::variable<bool> failureShown;\n\n\tFn<void()> retryWithNewGift;\n};\n\nvoid StartCraftAnimation(\n\tnot_null<GenericBox*> box,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tstd::shared_ptr<CraftState> state,\n\tFn<void(Fn<void(CraftResult)> callback)> startRequest,\n\tFn<void(Fn<void()> closeCurrent)> retryWithNewGift);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/star_gift_craft_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/star_gift_craft_box.h\"\n\n#include \"base/call_delayed.h\"\n#include \"base/event_filter.h\"\n#include \"base/random.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/star_gift_auction_box.h\"\n#include \"boxes/star_gift_craft_animation.h\"\n#include \"boxes/star_gift_preview_box.h\"\n#include \"boxes/star_gift_resale_box.h\"\n#include \"apiwrap.h\"\n#include \"api/api_credits.h\"\n#include \"api/api_premium.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"data/components/gift_auctions.h\"\n#include \"core/ui_integration.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_star_gift.h\"\n#include \"data/data_user.h\"\n#include \"info/peer_gifts/info_peer_gifts_common.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/about_cocoon_box.h\" // AddUniqueCloseButton.\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/button_labels.h\"\n#include \"ui/controls/feature_list.h\"\n#include \"ui/effects/numbers_animation.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/gradient_round_button.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/painter.h\"\n#include \"ui/top_background_gradient.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\" // stickerPanDeleteIconFg\n#include \"styles/style_credits.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Ui {\nnamespace {\n\nusing namespace Info::PeerGifts;\n\nstruct ColorScheme {\n\tData::UniqueGiftBackdrop backdrop;\n\tQColor button1;\n\tQColor button2;\n};\n\n[[nodiscard]] QColor ForgeBgOverlay() {\n\treturn QColor(0xBA, 0xDF, 0xFF, 32);\n}\n\n[[nodiscard]] QColor ForgeFailOverlay() {\n\treturn QColor(0xE1, 0x79, 0x23, 41);\n}\n\n[[nodiscard]] std::array<ColorScheme, 5> CraftBackdrops() {\n\tstruct Colors {\n\t\tint center = 0;\n\t\tint edge = 0;\n\t\tint pattern = 0;\n\t\tint button1 = 0;\n\t\tint button2 = 0;\n\t};\n\tconst auto hardcoded = [](Colors colors) {\n\t\tauto result = ColorScheme();\n\t\tconst auto color = [](int value) {\n\t\t\treturn QColor(\n\t\t\t\t(uint32(value) >> 16) & 0xFF,\n\t\t\t\t(uint32(value) >> 8) & 0xFF,\n\t\t\t\t(uint32(value)) & 0xFF);\n\t\t};\n\t\tresult.backdrop.centerColor = color(colors.center);\n\t\tresult.backdrop.edgeColor = color(colors.edge);\n\t\tresult.backdrop.patternColor = color(colors.pattern);\n\t\tresult.button1 = color(colors.button1);\n\t\tresult.button2 = color(colors.button2);\n\t\treturn result;\n\t};\n\treturn {\n\t\thardcoded({ 0x2C4359, 0x232E3F, 0x040C1A, 0x10A5DF, 0x2091E9 }),\n\t\thardcoded({ 0x2C4359, 0x232E3F, 0x040C1A, 0x10A5DF, 0x2091E9 }),\n\t\thardcoded({ 0x2C4359, 0x232E3F, 0x040C1A, 0x10A5DF, 0x2091E9 }),\n\t\thardcoded({ 0x1C4843, 0x1A2E37, 0x040C1A, 0x3ACA49, 0x007D9E }),\n\t\thardcoded({ 0x5D2E16, 0x371B1A, 0x040C1A, 0xE27519, 0xDD4819 }),\n\t};\n}\n\nstruct GiftForCraft {\n\tstd::shared_ptr<Data::UniqueGift> unique;\n\tData::SavedStarGiftId manageId;\n\n\t[[nodiscard]] QString slugId() const {\n\t\treturn unique ? unique->slug : QString();\n\t}\n\n\texplicit operator bool() const {\n\t\treturn unique != nullptr;\n\t}\n\tfriend inline bool operator==(\n\t\tconst GiftForCraft &,\n\t\tconst GiftForCraft &) = default;\n};\n\nstruct CraftingView {\n\tobject_ptr<RpWidget> widget;\n\trpl::producer<int> editRequests;\n\trpl::producer<int> removeRequests;\n\tFn<void(std::shared_ptr<CraftState>)> grabForAnimation;\n};\n\nvoid ShowGiftCraftBox(\n\tnot_null<Window::SessionController*> controller,\n\tstd::vector<GiftForCraft> gifts,\n\tbool autoStartCraft);\n\n[[nodiscard]] QString FormatPercent(int permille) {\n\tconst auto rounded = (permille + 5) / 10;\n\treturn QString::number(rounded) + '%';\n}\n\n[[nodiscard]] not_null<RpWidget*> MakeRadialPercent(\n\t\tnot_null<RpWidget*> parent,\n\t\tconst style::CraftRadialPercent &st,\n\t\trpl::producer<int> permille,\n\t\tFn<QString(int)> format = FormatPercent) {\n\tauto raw = CreateChild<RpWidget>(parent);\n\n\tstruct State {\n\t\tState(const style::CraftRadialPercent &st, Fn<void()> callback)\n\t\t: numbers(st.font, std::move(callback)) {\n\t\t\tnumbers.setDisabledMonospace(true);\n\t\t}\n\n\t\tAnimations::Simple animation;\n\t\tNumbersAnimation numbers;\n\t\tint permille = -1;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>(\n\t\tst,\n\t\t[=] { raw->update(); });\n\n\tstd::move(permille) | rpl::on_next([=](int value) {\n\t\tif (state->permille == value) {\n\t\t\treturn;\n\t\t}\n\t\tstate->animation.start([=] {\n\t\t\traw->update();\n\t\t}, state->permille, value, st::slideWrapDuration);\n\t\tstate->permille = value;\n\t\tstate->numbers.setText(format(value), value);\n\t}, raw->lifetime());\n\tstate->animation.stop();\n\tstate->numbers.finishAnimating();\n\n\traw->show();\n\traw->setAttribute(Qt::WA_TransparentForMouseEvents);\n\traw->paintOn([=, &st](QPainter &p) {\n\t\tstatic constexpr auto kArcSkip = arc::kFullLength / 4;\n\t\tstatic constexpr auto kArcStart = -(arc::kHalfLength - kArcSkip) / 2;\n\t\tstatic constexpr auto kArcLength = arc::kFullLength - kArcSkip;\n\n\t\tconst auto paint = [&](QColor color, float64 permille) {\n\t\t\tp.setPen(QPen(color, st.stroke, Qt::SolidLine, Qt::RoundCap));\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\tconst auto part = kArcLength * (permille / 1000.);\n\t\t\tconst auto length = int(base::SafeRound(part));\n\t\t\tconst auto inner = raw->rect().marginsRemoved(\n\t\t\t\t{ st.stroke, st.stroke, st.stroke, st.stroke });\n\t\t\tp.drawArc(inner, kArcStart + kArcLength - length, length);\n\t\t};\n\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tauto inactive = QColor(255, 255, 255, 64);\n\t\tpaint(inactive, 1000.);\n\t\tpaint(st::white->c, state->animation.value(state->permille));\n\n\t\tstate->numbers.paint(\n\t\t\tp,\n\t\t\t(raw->width() - state->numbers.countWidth()) / 2,\n\t\t\traw->height() - st.font->height,\n\t\t\traw->width());\n\t});\n\n\treturn raw;\n}\n\nAbstractButton *MakeCornerButton(\n\t\tnot_null<RpWidget*> parent,\n\t\tnot_null<GiftButton*> button,\n\t\tobject_ptr<RpWidget> content,\n\t\tstyle::align align,\n\t\tconst GiftForCraft &gift,\n\t\trpl::producer<QColor> edgeColor) {\n\tExpects(content != nullptr);\n\n\tconst auto result = CreateChild<AbstractButton>(parent);\n\tresult->show();\n\n\tconst auto inner = content.release();\n\tinner->setParent(result);\n\tinner->show();\n\tinner->sizeValue() | rpl::on_next([=](QSize size) {\n\t\tresult->resize(size);\n\t}, result->lifetime());\n\tinner->move(0, 0);\n\n\trpl::combine(\n\t\tbutton->geometryValue(),\n\t\tresult->sizeValue()\n\t) | rpl::on_next([=](QRect geometry, QSize size) {\n\t\tconst auto extend = st::defaultDropdownMenu.wrap.shadow.extend;\n\t\tgeometry = geometry.marginsRemoved(extend);\n\t\tconst auto out = QPoint(size.width(), size.height()) / 3;\n\t\tconst auto left = (align == style::al_left)\n\t\t\t? (geometry.x() - out.x())\n\t\t\t: (geometry.x() + geometry.width() - size.width() + out.x());\n\t\tconst auto top = geometry.y() - out.y();\n\t\tresult->move(left, top);\n\t}, result->lifetime());\n\n\tstruct State {\n\t\trpl::variable<QColor> edgeColor;\n\t\tQColor buttonEdgeColor;\n\t};\n\tconst auto state = result->lifetime().make_state<State>();\n\tstate->edgeColor = std::move(edgeColor);\n\tstate->buttonEdgeColor = gift.unique->backdrop.edgeColor;\n\tresult->paintOn([=](QPainter &p) {\n\t\tconst auto right = result->width();\n\t\tconst auto bottom = result->height();\n\t\tconst auto add = QPoint(right, bottom) / 3;\n\t\tconst auto radius = bottom / 2.;\n\t\tauto gradient = QLinearGradient(\n\t\t\t(align == style::al_left) ? -add.x() : (right + add.x()),\n\t\t\t-add.y(),\n\t\t\t(align == style::al_left) ? (right + add.x()) : -add.x(),\n\t\t\tbottom + add.y());\n\t\tgradient.setColorAt(0, state->edgeColor.current());\n\t\tgradient.setColorAt(1, state->buttonEdgeColor);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(gradient);\n\t\tp.drawRoundedRect(result->rect(), radius, radius);\n\t});\n\n\treturn result;\n}\n\nAbstractButton *MakePercentButton(\n\t\tnot_null<RpWidget*> parent,\n\t\tnot_null<GiftButton*> button,\n\t\tconst GiftForCraft &gift,\n\t\trpl::producer<QColor> edgeColor) {\n\tauto label = object_ptr<FlatLabel>(\n\t\tparent,\n\t\tFormatPercent(gift.unique->craftChancePermille),\n\t\tst::craftPercentLabel);\n\tlabel->setTextColorOverride(st::white->c);\n\tconst auto result = MakeCornerButton(\n\t\tparent,\n\t\tbutton,\n\t\tstd::move(label),\n\t\tstyle::al_left,\n\t\tgift,\n\t\tstd::move(edgeColor));\n\tresult->setAttribute(Qt::WA_TransparentForMouseEvents);\n\treturn result;\n}\n\nAbstractButton *MakeRemoveButton(\n\t\tnot_null<RpWidget*> parent,\n\t\tnot_null<GiftButton*> button,\n\t\tint size,\n\t\tconst GiftForCraft &gift,\n\t\tFn<void()> onClick,\n\t\trpl::producer<QColor> edgeColor,\n\t\tconst style::icon &icon) {\n\tauto remove = object_ptr<RpWidget>(parent);\n\tconst auto add = (size - icon.width()) / 2;\n\tremove->resize(icon.size() + QSize(add, add) * 2);\n\tremove->paintOn([=, &icon](QPainter &p) {\n\t\ticon.paint(p, add, add, add * 2 + icon.width(), st::white->c);\n\t});\n\tremove->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tconst auto result = MakeCornerButton(\n\t\tparent,\n\t\tbutton,\n\t\tstd::move(remove),\n\t\tstyle::al_right,\n\t\tgift,\n\t\tstd::move(edgeColor));\n\tresult->setClickedCallback(std::move(onClick));\n\treturn result;\n}\n\n[[nodiscard]] CraftingView MakeCraftingView(\n\t\tnot_null<RpWidget*> parent,\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<std::vector<GiftForCraft>> chosen,\n\t\trpl::producer<QColor> edgeColor) {\n\tconst auto width = st::boxWideWidth;\n\n\tconst auto buttonPadding = st::craftPreviewPadding;\n\tconst auto buttonSize = st::giftBoxGiftTiny;\n\tconst auto height = 2\n\t\t* (buttonPadding.top() + buttonSize + buttonPadding.bottom());\n\n\tauto widget = object_ptr<FixedHeightWidget>(parent, height);\n\tconst auto raw = widget.data();\n\n\tstruct Entry {\n\t\tGiftForCraft gift;\n\t\tGiftButton *button = nullptr;\n\t\tAbstractButton *add = nullptr;\n\t\tAbstractButton *percent = nullptr;\n\t\tAbstractButton *remove = nullptr;\n\t};\n\tstruct State {\n\t\texplicit State(not_null<Main::Session*> session)\n\t\t: delegate(session, GiftButtonMode::CraftPreview) {\n\t\t}\n\n\t\tDelegate delegate;\n\t\tstd::array<Entry, 4> entries;\n\t\tFn<void(int)> refreshButton;\n\t\trpl::event_stream<int> editRequests;\n\t\trpl::event_stream<int> removeRequests;\n\t\trpl::variable<int> chancePermille;\n\t\trpl::variable<QColor> edgeColor;\n\t\tRpWidget *forgeRadial = nullptr;\n\t};\n\tconst auto state = parent->lifetime().make_state<State>(session);\n\tstate->edgeColor = std::move(edgeColor);\n\n\tstate->refreshButton = [=](int index) {\n\t\tExpects(index >= 0 && index < state->entries.size());\n\n\t\tauto &entry = state->entries[index];\n\t\tconst auto single = state->delegate.buttonSize();\n\t\tconst auto geometry = QRect(\n\t\t\t((index % 2)\n\t\t\t\t? (width - buttonPadding.left() - single.width())\n\t\t\t\t: buttonPadding.left()),\n\t\t\t((index < 2)\n\t\t\t\t? buttonPadding.top()\n\t\t\t\t: (height - buttonPadding.top() - single.height())),\n\t\t\tsingle.width(),\n\t\t\tsingle.height());\n\t\tdelete base::take(entry.add);\n\t\tdelete base::take(entry.button);\n\t\tdelete base::take(entry.percent);\n\t\tdelete base::take(entry.remove);\n\n\t\tif (entry.gift) {\n\t\t\tentry.button = CreateChild<GiftButton>(raw, &state->delegate);\n\t\t\tentry.button->setDescriptor(GiftTypeStars{\n\t\t\t\t.info = {\n\t\t\t\t\t.id = entry.gift.unique->initialGiftId,\n\t\t\t\t\t.unique = entry.gift.unique,\n\t\t\t\t\t.document = entry.gift.unique->model.document,\n\t\t\t\t},\n\t\t\t}, GiftButton::Mode::CraftPreview);\n\t\t\tentry.button->show();\n\t\t\tentry.button->setClickedCallback([=] {\n\t\t\t\tstate->editRequests.fire_copy(index);\n\t\t\t});\n\t\t\tentry.button->setGeometry(\n\t\t\t\tgeometry,\n\t\t\t\tstate->delegate.buttonExtend());\n\n\t\t\tentry.percent = MakePercentButton(\n\t\t\t\traw,\n\t\t\t\tentry.button,\n\t\t\t\tentry.gift,\n\t\t\t\tstate->edgeColor.value());\n\t\t} else {\n\t\t\tentry.add = CreateChild<AbstractButton>(raw);\n\t\t\tentry.add->show();\n\t\t\tentry.add->paintOn([=](QPainter &p) {\n\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\tconst auto radius = st::boxRadius;\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(ForgeBgOverlay());\n\n\t\t\t\tconst auto rect = QRect(QPoint(), geometry.size());\n\t\t\t\tp.drawRoundedRect(rect, radius, radius);\n\n\t\t\t\tconst auto &icon = st::craftAddIcon;\n\t\t\t\ticon.paintInCenter(p, rect, st::white->c);\n\t\t\t});\n\t\t\tentry.add->setClickedCallback([=] {\n\t\t\t\tstate->editRequests.fire_copy(index);\n\t\t\t});\n\t\t\tentry.add->setGeometry(geometry);\n\t\t}\n\n\t\tconst auto count = 4 - ranges::count(\n\t\t\tstate->entries,\n\t\t\tnullptr,\n\t\t\t&Entry::button);\n\t\tconst auto canRemove = (count > 1);\n\t\tconst auto secondHasAddress = state->entries[1].gift\n\t\t\t&& !state->entries[1].gift.unique->giftAddress.isEmpty();\n\t\tfor (auto i = 0; i != 4; ++i) {\n\t\t\tauto &entry = state->entries[i];\n\t\t\tif (entry.button) {\n\t\t\t\tdelete base::take(entry.remove);\n\t\t\t\tif (canRemove) {\n\t\t\t\t\tconst auto needReplace = (i == 0) && secondHasAddress;\n\t\t\t\t\tconst auto callback = [=] {\n\t\t\t\t\t\tif (needReplace) {\n\t\t\t\t\t\t\tstate->editRequests.fire_copy(0);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tstate->removeRequests.fire_copy(i);\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\tconst auto &icon = needReplace\n\t\t\t\t\t\t? st::craftReplaceIcon\n\t\t\t\t\t\t: st::stickerPanDeleteIconFg;\n\t\t\t\t\tentry.remove = MakeRemoveButton(\n\t\t\t\t\t\traw,\n\t\t\t\t\t\tentry.button,\n\t\t\t\t\t\tentry.percent->height(),\n\t\t\t\t\t\tentry.gift,\n\t\t\t\t\t\tcallback,\n\t\t\t\t\t\tstate->edgeColor.value(),\n\t\t\t\t\t\ticon);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\tstd::move(\n\t\tchosen\n\t) | rpl::on_next([=](const std::vector<GiftForCraft> &gifts) {\n\t\tauto chance = 0;\n\t\tfor (auto i = 0; i != 4; ++i) {\n\t\t\tauto &entry = state->entries[i];\n\t\t\tconst auto gift = (i < gifts.size()) ? gifts[i] : GiftForCraft();\n\t\t\tchance += gift.unique ? gift.unique->craftChancePermille : 0;\n\t\t\tif (entry.gift == gift && (entry.button || entry.add)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tentry.gift = gift;\n\t\t\tstate->refreshButton(i);\n\t\t}\n\t\tstate->chancePermille = chance;\n\t}, raw->lifetime());\n\n\tconst auto center = [&] {\n\t\tconst auto buttonPadding = st::craftPreviewPadding;\n\t\tconst auto buttonSize = st::giftBoxGiftTiny;\n\t\tconst auto left = buttonPadding.left()\n\t\t\t+ buttonSize\n\t\t\t+ buttonPadding.right();\n\t\tconst auto center = (width - 2 * left);\n\t\tconst auto top = (height - center) / 2;\n\t\treturn QRect(left, top, center, center);\n\t}();\n\traw->paintOn([=](QPainter &p) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tconst auto radius = st::boxRadius;\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(ForgeBgOverlay());\n\n\t\tp.drawRoundedRect(center, radius, radius);\n\n\t\tst::craftForge.paintInCenter(p, center, st::white->c);\n\t});\n\n\tstate->forgeRadial = MakeRadialPercent(\n\t\traw,\n\t\tst::craftForgePercent,\n\t\tstate->chancePermille.value());\n\tstate->forgeRadial->setGeometry(center.marginsRemoved({\n\t\tst::craftForgePadding,\n\t\tst::craftForgePadding,\n\t\tst::craftForgePadding,\n\t\tst::craftForgePadding,\n\t}));\n\n\tauto grabForAnimation = [=](std::shared_ptr<CraftState> craftState) {\n\t\tcraftState->forgeRect = center;\n\t\tcraftState->forgePercent = GrabWidgetToImage(state->forgeRadial);\n\n\t\tauto giftsTotal = 0;\n\t\tauto lostIndex = 0;\n\t\tfor (auto i = 0; i != 4; ++i) {\n\t\t\tauto &entry = state->entries[i];\n\t\t\tauto &corner = craftState->corners[i];\n\n\t\t\tif (entry.button) {\n\t\t\t\tcorner.originalRect = entry.button->geometry();\n\t\t\t\tif (entry.percent) {\n\t\t\t\t\tcorner.percentBadge = GrabWidgetToImage(entry.percent);\n\t\t\t\t}\n\t\t\t\tif (entry.remove) {\n\t\t\t\t\tcorner.removeButton = GrabWidgetToImage(entry.remove);\n\t\t\t\t}\n\t\t\t\tif (lostIndex < craftState->lostGifts.size()) {\n\t\t\t\t\tcraftState->lostGifts[lostIndex].cornerIndex = i;\n\t\t\t\t\t++lostIndex;\n\t\t\t\t}\n\t\t\t\tcorner.giftButton.reset(entry.button);\n\t\t\t\tentry.button->setParent(parent);\n\t\t\t\tbase::take(entry.button)->hide();\n\n\t\t\t\t++giftsTotal;\n\t\t\t} else if (entry.add) {\n\t\t\t\tcorner.addButton = GrabWidgetToImage(entry.add);\n\t\t\t\tcorner.originalRect = entry.add->geometry();\n\t\t\t}\n\t\t}\n\n\t\tconst auto failedCount = MakeRadialPercent(\n\t\t\traw,\n\t\t\tst::craftForgePercent,\n\t\t\trpl::single(0),\n\t\t\t[=](int) { return QString::number(giftsTotal); });\n\t\tfailedCount->setGeometry(state->forgeRadial->geometry());\n\t\tcraftState->lostRadial = GrabWidgetToImage(failedCount);\n\t\tdelete failedCount;\n\n\t\tconst auto overlayBg = craftState->forgeBgOverlay = ForgeBgOverlay();\n\t\tconst auto backdrop = CraftBackdrops()[giftsTotal - 1].backdrop;\n\t\tcraftState->forgeBg1 = anim::color(\n\t\t\tbackdrop.centerColor,\n\t\t\tQColor(overlayBg.red(), overlayBg.green(), overlayBg.blue()),\n\t\t\toverlayBg.alphaF());\n\t\tcraftState->forgeBg2 = anim::color(\n\t\t\tbackdrop.edgeColor,\n\t\t\tQColor(overlayBg.red(), overlayBg.green(), overlayBg.blue()),\n\t\t\toverlayBg.alphaF());\n\t\tconst auto overlayFail = ForgeFailOverlay();\n\t\tcraftState->forgeFail = anim::color(\n\t\t\tCraftBackdrops().back().backdrop.centerColor,\n\t\t\tQColor(overlayFail.red(), overlayFail.green(), overlayFail.blue()),\n\t\t\toverlayFail.alphaF());\n\t\tfor (auto i = 0; i != 6; ++i) {\n\t\t\tcraftState->forgeSides[i] = craftState->prepareEmptySide(i);\n\t\t}\n\t};\n\n\treturn {\n\t\t.widget = std::move(widget),\n\t\t.editRequests = state->editRequests.events(),\n\t\t.removeRequests = state->removeRequests.events(),\n\t\t.grabForAnimation = std::move(grabForAnimation),\n\t};\n}\n\nvoid AddCraftGiftsList(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<VerticalLayout*> container,\n\t\tData::CraftGiftsDescriptor descriptor,\n\t\tconst std::vector<GiftForCraft> &selected,\n\t\tFn<void(std::shared_ptr<Data::UniqueGift>)> chosen) {\n\tstruct State {\n\t\trpl::event_stream<> updated;\n\t\tData::CraftGiftsDescriptor data;\n\t\trpl::variable<bool> empty = true;\n\t\trpl::lifetime loading;\n\t};\n\tconst auto state = container->lifetime().make_state<State>();\n\tstate->data = std::move(descriptor);\n\n\tusing Descriptor = Info::PeerGifts::GiftDescriptor;\n\tusing StarGift = Info::PeerGifts::GiftTypeStars;\n\tauto handler = crl::guard(container, [=](Descriptor descriptor) {\n\t\tExpects(v::is<StarGift>(descriptor));\n\n\t\tconst auto unique = v::get<StarGift>(descriptor).info.unique;\n\t\tchosen(unique);\n\t});\n\n\tauto gifts = rpl::single(\n\t\trpl::empty\n\t) | rpl::then(state->updated.events()) | rpl::map([=] {\n\t\tauto result = GiftsDescriptor();\n\t\tconst auto selfId = window->session().userPeerId();\n\t\tfor (const auto &gift : state->data.list) {\n\t\t\tresult.list.push_back(Info::PeerGifts::GiftTypeStars{\n\t\t\t\t.info = gift.info,\n\t\t\t\t.resale = true,\n\t\t\t\t.mine = (gift.info.unique->ownerId == selfId),\n\t\t\t\t});\n\t\t}\n\t\tstate->empty = result.list.empty();\n\t\treturn result;\n\t});\n\tconst auto peer = window->session().user();\n\tconst auto loadMore = [=] {\n\t\tif (!state->data.offset.isEmpty() && !state->loading) {\n\t\t\tstate->loading = Data::CraftGiftsSlice(\n\t\t\t\t&peer->session(),\n\t\t\t\tstate->data.giftId,\n\t\t\t\tstate->data.offset\n\t\t\t) | rpl::on_next([=](Data::CraftGiftsDescriptor &&slice) {\n\t\t\t\tstate->loading.destroy();\n\t\t\t\tstate->data.offset = slice.list.empty()\n\t\t\t\t\t? QString()\n\t\t\t\t\t: slice.offset;\n\t\t\t\tstate->data.list.insert(\n\t\t\t\t\tend(state->data.list),\n\t\t\t\t\tstd::make_move_iterator(begin(slice.list)),\n\t\t\t\t\tstd::make_move_iterator(end(slice.list)));\n\t\t\t\tstate->updated.fire({});\n\t\t\t});\n\t\t}\n\t};\n\tcontainer->add(MakeGiftsList({\n\t\t.window = window,\n\t\t.mode = GiftsListMode::Craft,\n\t\t.peer = peer,\n\t\t.gifts = std::move(gifts),\n\t\t.selected = (selected\n\t\t\t| ranges::views::transform(&GiftForCraft::unique)\n\t\t\t| ranges::to_vector),\n\t\t.loadMore = loadMore,\n\t\t.handler = handler,\n\t}));\n\n\tconst auto skip = st::defaultSubsectionTitlePadding.top();\n\tconst auto wrap = container->add(\n\t\tobject_ptr<SlideWrap<FlatLabel>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_gift_craft_select_none(),\n\t\t\t\tst::craftYourListEmpty),\n\t\t\t(st::boxRowPadding + QMargins(0, 0, 0, skip))),\n\t\tstyle::al_top);\n\tstate->empty.value() | rpl::on_next([=](bool empty) {\n\t\t// Scroll doesn't jump up if we show before rows are cleared,\n\t\t// and we hide after rows are added.\n\t\tif (empty) {\n\t\t\twrap->show(anim::type::instant);\n\t\t} else {\n\t\t\tcrl::on_main(wrap, [=] {\n\t\t\t\tif (!state->empty.current()) {\n\t\t\t\t\twrap->hide(anim::type::instant);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}, wrap->lifetime());\n\twrap->entity()->setTryMakeSimilarLines(true);\n}\n\nvoid ShowSelectGiftBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tuint64 giftId,\n\t\tFn<void(GiftForCraft)> chosen,\n\t\tstd::vector<GiftForCraft> selected,\n\t\tFn<void()> boxClosed,\n\t\tbool firstSlot) {\n\tstruct Entry {\n\t\tData::SavedStarGift gift;\n\t\tGiftButton *button = nullptr;\n\t};\n\tstruct State {\n\t\tstd::vector<Entry> entries;\n\n\t\tData::CraftGiftsDescriptor craft;\n\t\tData::ResaleGiftsDescriptor resale;\n\n\t\trpl::lifetime craftLifetime;\n\t\trpl::lifetime resaleLifetime;\n\t};\n\n\tconst auto session = &controller->session();\n\tconst auto state = std::make_shared<State>();\n\n\tconst auto make = [=](not_null<GenericBox*> box) {\n\t\tbox->setTitle(tr::lng_gift_craft_select_title());\n\t\tbox->setWidth(st::boxWideWidth);\n\n\t\tbox->boxClosing() | rpl::on_next(boxClosed, box->lifetime());\n\n\t\tAddSubsectionTitle(\n\t\t\tbox->verticalLayout(),\n\t\t\ttr::lng_gift_craft_select_your());\n\n\t\tconst auto got = crl::guard(box, [=](\n\t\t\t\tstd::shared_ptr<Data::UniqueGift> gift) {\n\t\t\tif (ShowCraftLaterError(box->uiShow(), gift)\n\t\t\t\t|| (firstSlot\n\t\t\t\t\t&& ShowCraftAddressError(box->uiShow(), gift))) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tchosen(GiftForCraft{ .unique = gift });\n\t\t\tbox->closeBox();\n\t\t});\n\n\t\tAddCraftGiftsList(\n\t\t\tcontroller,\n\t\t\tbox->verticalLayout(),\n\t\t\tstate->craft,\n\t\t\tselected,\n\t\t\tgot);\n\n\t\tif (const auto count = state->resale.count) {\n\t\t\tAddSubsectionTitle(\n\t\t\t\tbox->verticalLayout(),\n\t\t\t\ttr::lng_gift_craft_select_market(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(count * 1.)));\n\n\t\t\tAddResaleGiftsList(\n\t\t\t\tcontroller,\n\t\t\t\tsession->user(),\n\t\t\t\tbox->verticalLayout(),\n\t\t\t\tstate->resale,\n\t\t\t\tnullptr,\n\t\t\t\tgot,\n\t\t\t\ttrue);\n\t\t}\n\n\t\tbox->addButton(tr::lng_box_ok(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t\tbox->setMaxHeight(st::boxWideWidth);\n\t};\n\tconst auto show = crl::guard(controller, [=] {\n\t\tcontroller->show(Box(make));\n\t});\n\n\tstate->craftLifetime = Data::CraftGiftsSlice(\n\t\tsession,\n\t\tgiftId\n\t) | rpl::on_next([=](Data::CraftGiftsDescriptor &&info) {\n\t\tstate->craftLifetime.destroy();\n\n\t\tstate->craft = std::move(info);\n\t\tif (state->resale.giftId) {\n\t\t\tshow();\n\t\t}\n\t});\n\n\tstate->resaleLifetime = Data::ResaleGiftsSlice(\n\t\tsession,\n\t\tgiftId,\n\t\t{ .forCraft = true }\n\t) | rpl::on_next([=](Data::ResaleGiftsDescriptor &&info) {\n\t\tstate->resaleLifetime.destroy();\n\n\t\tstate->resale = std::move(info);\n\t\tif (state->craft.giftId) {\n\t\t\tshow();\n\t\t}\n\t});\n}\n\n[[nodiscard]] object_ptr<RpWidget> MakeRarityExpectancyPreview(\n\t\tnot_null<RpWidget*> parent,\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<std::vector<GiftForCraft>> gifts) {\n\tauto result = object_ptr<RpWidget>(parent);\n\tconst auto raw = result.data();\n\n\tstruct AttributeState {\n\t\trpl::variable<int> permille;\n\t\tbool isBackdrop = false;\n\t\tQString name;\n\n\t\tAnimations::Simple backdropAnimation;\n\t\tData::UniqueGiftBackdrop wasBackdrop;\n\t\tData::UniqueGiftBackdrop nowBackdrop;\n\t\tQImage wasFrame;\n\t\tQImage nowFrame;\n\n\t\tAnimations::Simple patternAnimation;\n\t\tDocumentData *wasPattern = nullptr;\n\t\tDocumentData *nowPattern = nullptr;\n\t\tstd::unique_ptr<Text::CustomEmoji> wasEmoji;\n\t\tstd::unique_ptr<Text::CustomEmoji> nowEmoji;\n\n\t\tAbstractButton *button = nullptr;\n\t\tRpWidget *radial = nullptr;\n\t};\n\n\tstruct State {\n\t\tstd::array<AttributeState, 8> attrs;\n\t\tint count = 0;\n\t\tImportantTooltip *tooltip = nullptr;\n\t};\n\n\tconst auto state = raw->lifetime().make_state<State>();\n\n\tconst auto single = st::craftAttributeSize;\n\tconst auto skip = st::craftAttributeSkip;\n\n\tfor (auto i = 0; i != 8; ++i) {\n\t\tauto &attr = state->attrs[i];\n\t\tconst auto btn = CreateChild<AbstractButton>(raw);\n\t\tattr.button = btn;\n\t\tbtn->resize(single, single);\n\t\tbtn->hide();\n\n\t\tconst auto idx = i;\n\t\tbtn->paintOn([=](QPainter &p) {\n\t\t\tauto &a = state->attrs[idx];\n\t\t\tconst auto sub = st::craftAttributePadding;\n\t\t\tconst auto inner = QRect(0, 0, single, single).marginsRemoved(\n\t\t\t\t{ sub, sub, sub, sub });\n\t\t\tif (a.isBackdrop) {\n\t\t\t\tconst auto progress = a.backdropAnimation.value(1.);\n\t\t\t\tif (progress < 1.) {\n\t\t\t\t\tp.drawImage(inner.topLeft(), a.wasFrame);\n\t\t\t\t\tp.setOpacity(progress);\n\t\t\t\t}\n\t\t\t\tif (a.nowFrame.isNull()) {\n\t\t\t\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t\t\t\ta.nowFrame = QImage(\n\t\t\t\t\t\tinner.size() * ratio,\n\t\t\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t\t\ta.nowFrame.fill(Qt::transparent);\n\t\t\t\t\ta.nowFrame.setDevicePixelRatio(ratio);\n\t\t\t\t\tauto q = QPainter(&a.nowFrame);\n\t\t\t\t\tauto hq = PainterHighQualityEnabler(q);\n\t\t\t\t\tauto gradient = QLinearGradient(\n\t\t\t\t\t\tQPointF(inner.width(), 0),\n\t\t\t\t\t\tQPointF(0, inner.height()));\n\t\t\t\t\tgradient.setColorAt(0., a.nowBackdrop.centerColor);\n\t\t\t\t\tgradient.setColorAt(1., a.nowBackdrop.edgeColor);\n\t\t\t\t\tq.setPen(Qt::NoPen);\n\t\t\t\t\tq.setBrush(gradient);\n\t\t\t\t\tq.drawEllipse(\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tinner.width(),\n\t\t\t\t\t\tinner.height());\n\n\t\t\t\t\tq.setCompositionMode(\n\t\t\t\t\t\tQPainter::CompositionMode_Source);\n\t\t\t\t\tq.setBrush(Qt::transparent);\n\t\t\t\t\tconst auto max = u\"100%\"_q;\n\t\t\t\t\tconst auto &font = st::craftAttributePercent.font;\n\t\t\t\t\tconst auto tw = font->width(max);\n\t\t\t\t\tq.drawRoundedRect(\n\t\t\t\t\t\t(inner.width() - tw) / 2,\n\t\t\t\t\t\tinner.height() + sub - font->height,\n\t\t\t\t\t\ttw,\n\t\t\t\t\t\tfont->height,\n\t\t\t\t\t\tfont->height / 2.,\n\t\t\t\t\t\tfont->height / 2.);\n\t\t\t\t}\n\t\t\t\tp.drawImage(inner.topLeft(), a.nowFrame);\n\t\t\t\tp.setOpacity(1.);\n\t\t\t} else {\n\t\t\t\tconst auto center = QRect(0, 0, single, single).center();\n\t\t\t\tconst auto emojiShift\n\t\t\t\t\t= (single - Emoji::GetCustomSizeNormal()) / 2;\n\t\t\t\tconst auto pos = QPoint(emojiShift, emojiShift);\n\t\t\t\tconst auto progress = a.patternAnimation.value(1.);\n\t\t\t\tif (progress < 1.) {\n\t\t\t\t\tp.translate(center);\n\t\t\t\t\tp.save();\n\t\t\t\t\tp.setOpacity(1. - progress);\n\t\t\t\t\tp.scale(1. - progress, 1. - progress);\n\t\t\t\t\tp.translate(-center);\n\t\t\t\t\ta.wasEmoji->paint(p, {\n\t\t\t\t\t\t.textColor = st::white->c,\n\t\t\t\t\t\t.position = pos,\n\t\t\t\t\t});\n\t\t\t\t\tp.restore();\n\t\t\t\t\tp.scale(progress, progress);\n\t\t\t\t\tp.setOpacity(progress);\n\t\t\t\t\tp.translate(-center);\n\t\t\t\t}\n\t\t\t\tif (!a.nowEmoji) {\n\t\t\t\t\ta.nowEmoji = session->data().customEmojiManager().create(\n\t\t\t\t\t\ta.nowPattern,\n\t\t\t\t\t\t[=] { btn->update(); });\n\t\t\t\t}\n\t\t\t\ta.nowEmoji->paint(p, {\n\t\t\t\t\t.textColor = st::white->c,\n\t\t\t\t\t.position = pos,\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\n\t\tattr.radial = MakeRadialPercent(\n\t\t\tbtn,\n\t\t\tst::craftAttributePercent,\n\t\t\tattr.permille.value());\n\t\tattr.radial->setGeometry(0, 0, single, single);\n\n\t\tbtn->setClickedCallback([=] {\n\t\t\tauto &a = state->attrs[idx];\n\t\t\tif (state->tooltip) {\n\t\t\t\tstate->tooltip->toggleAnimated(false);\n\t\t\t}\n\n\t\t\tauto text = a.isBackdrop\n\t\t\t\t? tr::lng_gift_craft_chance_backdrop(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_percent,\n\t\t\t\t\tTextWithEntities{ FormatPercent(a.permille.current()) },\n\t\t\t\t\tlt_name,\n\t\t\t\t\tTextWithEntities{ a.name },\n\t\t\t\t\tUi::Text::RichLangValue)\n\t\t\t\t: tr::lng_gift_craft_chance_symbol(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_percent,\n\t\t\t\t\tTextWithEntities{ FormatPercent(a.permille.current()) },\n\t\t\t\t\tlt_name,\n\t\t\t\t\tTextWithEntities{ a.name },\n\t\t\t\t\tUi::Text::RichLangValue);\n\t\t\tconst auto tooltip = CreateChild<ImportantTooltip>(\n\t\t\t\tparent,\n\t\t\t\tMakeNiceTooltipLabel(\n\t\t\t\t\tparent,\n\t\t\t\t\trpl::single(std::move(text)),\n\t\t\t\t\tst::boxWideWidth / 2,\n\t\t\t\t\tst::defaultImportantTooltipLabel,\n\t\t\t\t\tst::defaultPopupMenu),\n\t\t\t\tst::defaultImportantTooltip);\n\t\t\ttooltip->toggleFast(false);\n\n\t\t\tbase::install_event_filter(tooltip, qApp, [=](not_null<QEvent*> e) {\n\t\t\t\tif (e->type() == QEvent::MouseButtonPress) {\n\t\t\t\t\ttooltip->toggleAnimated(false);\n\t\t\t\t}\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t});\n\n\t\t\tconst auto geometry = MapFrom(parent, btn, btn->rect());\n\t\t\tconst auto countPosition = [=](QSize size) {\n\t\t\t\tconst auto left = geometry.x()\n\t\t\t\t\t+ (geometry.width() - size.width()) / 2;\n\t\t\t\tconst auto right = parent->width()\n\t\t\t\t\t- st::normalFont->spacew;\n\t\t\t\treturn QPoint(\n\t\t\t\t\tstd::max(std::min(left, right - size.width()), 0),\n\t\t\t\t\tgeometry.y()\n\t\t\t\t\t\t- size.height()\n\t\t\t\t\t\t- st::normalFont->descent);\n\t\t\t};\n\t\t\ttooltip->pointAt(geometry, RectPart::Top, countPosition);\n\t\t\ttooltip->toggleAnimated(true);\n\n\t\t\tstate->tooltip = tooltip;\n\t\t\ttooltip->shownValue() | rpl::filter(\n\t\t\t\t!rpl::mappers::_1\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tcrl::on_main(tooltip, [=] {\n\t\t\t\t\tif (tooltip->isHidden()) {\n\t\t\t\t\t\tif (state->tooltip == tooltip) {\n\t\t\t\t\t\t\tstate->tooltip = nullptr;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdelete tooltip;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}, tooltip->lifetime());\n\n\t\t\tbase::timer_once(\n\t\t\t\t3000\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\ttooltip->toggleAnimated(false);\n\t\t\t}, tooltip->lifetime());\n\t\t});\n\t}\n\n\tconst auto relayout = [=](int count) {\n\t\tconst auto twoRows = (count > 5);\n\t\tconst auto row1 = twoRows ? ((count + 1) / 2) : count;\n\t\tconst auto row2 = twoRows ? (count - row1) : 0;\n\t\tconst auto rowWidth = [&](int n) {\n\t\t\treturn n * single + (n - 1) * skip;\n\t\t};\n\t\tconst auto w1 = rowWidth(row1);\n\t\tconst auto w2 = row2 ? rowWidth(row2) : 0;\n\t\tconst auto naturalWidth = std::max(w1, w2);\n\t\tconst auto totalHeight = twoRows\n\t\t\t? (2 * single + st::craftAttributeRowSkip)\n\t\t\t: single;\n\n\t\tfor (auto i = 0; i != 8; ++i) {\n\t\t\tauto &attr = state->attrs[i];\n\t\t\tif (i < count) {\n\t\t\t\tconst auto inRow1 = (i < row1);\n\t\t\t\tconst auto rowItems = inRow1 ? row1 : row2;\n\t\t\t\tconst auto rowW = rowWidth(rowItems);\n\t\t\t\tconst auto indexInRow = inRow1 ? i : (i - row1);\n\t\t\t\tconst auto x = (naturalWidth - rowW) / 2\n\t\t\t\t\t+ indexInRow * (single + skip);\n\t\t\t\tconst auto y = inRow1\n\t\t\t\t\t? 0\n\t\t\t\t\t: (single + st::craftAttributeRowSkip);\n\t\t\t\tattr.button->setGeometry(x, y, single, single);\n\t\t\t\tattr.button->show();\n\t\t\t} else {\n\t\t\t\tattr.button->hide();\n\t\t\t}\n\t\t}\n\n\t\traw->setNaturalWidth(naturalWidth);\n\t\traw->resize(naturalWidth, totalHeight);\n\t};\n\n\tconst auto permilles = session->appConfig().craftAttributePermilles();\n\tconst auto computePermille = [=](int total, int count) {\n\t\tExpects(total > 0);\n\t\tExpects(count > 0);\n\n\t\tconst auto &list = (total <= permilles.size())\n\t\t\t? permilles[total - 1]\n\t\t\t: std::vector<int>();\n\t\treturn (count <= list.size()) ? list[count - 1] : 1000;\n\t};\n\n\tstd::move(\n\t\tgifts\n\t) | rpl::on_next([=](const std::vector<GiftForCraft> &list) {\n\t\tstruct BackdropEntry {\n\t\t\tData::UniqueGiftBackdrop fields;\n\t\t\tQString name;\n\t\t\tint count = 0;\n\t\t};\n\t\tstruct PatternEntry {\n\t\t\tnot_null<DocumentData*> document;\n\t\t\tQString name;\n\t\t\tint count = 0;\n\t\t};\n\t\tauto backdrops = std::vector<BackdropEntry>();\n\t\tauto patterns = std::vector<PatternEntry>();\n\t\tfor (const auto &gift : list) {\n\t\t\tconst auto &backdrop = gift.unique->backdrop;\n\t\t\tconst auto &pattern = gift.unique->pattern;\n\t\t\tconst auto proj1 = &BackdropEntry::fields;\n\t\t\tconst auto i = ranges::find(backdrops, backdrop, proj1);\n\t\t\tif (i != end(backdrops)) {\n\t\t\t\t++i->count;\n\t\t\t} else {\n\t\t\t\tbackdrops.push_back({ backdrop, backdrop.name, 1 });\n\t\t\t}\n\t\t\tconst auto proj2 = &PatternEntry::document;\n\t\t\tconst auto j = ranges::find(\n\t\t\t\tpatterns,\n\t\t\t\tpattern.document,\n\t\t\t\tproj2);\n\t\t\tif (j != end(patterns)) {\n\t\t\t\t++j->count;\n\t\t\t} else {\n\t\t\t\tpatterns.push_back({ pattern.document, pattern.name, 1 });\n\t\t\t}\n\t\t}\n\t\tranges::sort(backdrops, ranges::greater(), &BackdropEntry::count);\n\t\tranges::sort(patterns, ranges::greater(), &PatternEntry::count);\n\n\t\tconst auto total = int(list.size());\n\t\tauto slotIndex = 0;\n\t\tfor (const auto &b : backdrops) {\n\t\t\tif (slotIndex >= 8) break;\n\t\t\tauto &a = state->attrs[slotIndex];\n\t\t\ta.isBackdrop = true;\n\t\t\ta.name = b.name;\n\t\t\ta.permille = computePermille(total, b.count);\n\t\t\tif (a.nowBackdrop != b.fields) {\n\t\t\t\tif (!a.nowFrame.isNull()) {\n\t\t\t\t\ta.wasBackdrop = a.nowBackdrop;\n\t\t\t\t\ta.wasFrame = base::take(a.nowFrame);\n\t\t\t\t\ta.backdropAnimation.stop();\n\t\t\t\t\ta.backdropAnimation.start([=, btn = a.button] {\n\t\t\t\t\t\tbtn->update();\n\t\t\t\t\t}, 0., 1., st::fadeWrapDuration);\n\t\t\t\t}\n\t\t\t\ta.nowBackdrop = b.fields;\n\t\t\t\ta.nowFrame = QImage();\n\t\t\t}\n\t\t\t++slotIndex;\n\t\t}\n\t\tfor (const auto &pt : patterns) {\n\t\t\tif (slotIndex >= 8) break;\n\t\t\tauto &a = state->attrs[slotIndex];\n\t\t\ta.isBackdrop = false;\n\t\t\ta.name = pt.name;\n\t\t\ta.permille = computePermille(total, pt.count);\n\t\t\tif (a.nowPattern != pt.document) {\n\t\t\t\tif (a.nowEmoji) {\n\t\t\t\t\ta.wasPattern = a.nowPattern;\n\t\t\t\t\ta.wasEmoji = base::take(a.nowEmoji);\n\t\t\t\t\ta.patternAnimation.stop();\n\t\t\t\t\ta.patternAnimation.start([=, btn = a.button] {\n\t\t\t\t\t\tbtn->update();\n\t\t\t\t\t}, 0., 1., st::fadeWrapDuration);\n\t\t\t\t}\n\t\t\t\ta.nowPattern = pt.document;\n\t\t\t\ta.nowEmoji = nullptr;\n\t\t\t}\n\t\t\t++slotIndex;\n\t\t}\n\n\t\tconst auto newCount = slotIndex;\n\t\tif (state->count != newCount) {\n\t\t\tstate->count = newCount;\n\t\t\trelayout(newCount);\n\t\t}\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\nvoid Craft(\n\t\tnot_null<GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tstd::shared_ptr<CraftState> state,\n\t\tconst std::vector<GiftForCraft> &gifts) {\n\tauto show = controller->uiShow();\n\tauto startRequest = [=](Fn<void(CraftResult)> done) {\n#if 0\n\t\tconstexpr auto kDelays = std::array<crl::time, 7>{\n\t\t\t100, 200, 300, 400, 500, 1000, 2000\n\t\t};\n\t\tconst auto delay = kDelays[base::RandomIndex(kDelays.size())];\n\t\tconst auto giftsCopy = gifts;\n\n\t\tbase::call_delayed(delay, box, [=] {\n\t\t\t//static auto testing = 0;\n\t\t\tconst auto shouldSucceed = false;// (((++testing) / 4) % 2 == 0);\n\t\t\tconst auto count = int(giftsCopy.size());\n\t\t\tif (shouldSucceed && count > 0) {\n\t\t\t\tconst auto &chosen = giftsCopy[base::RandomIndex(count)];\n\t\t\t\tauto info = Data::StarGift{\n\t\t\t\t\t.id = chosen.unique->initialGiftId,\n\t\t\t\t\t.unique = chosen.unique,\n\t\t\t\t\t.document = chosen.unique->model.document,\n\t\t\t\t};\n\t\t\t\tauto result = std::make_shared<Data::GiftUpgradeResult>(\n\t\t\t\t\tData::GiftUpgradeResult{\n\t\t\t\t\t\t.info = std::move(info),\n\t\t\t\t\t\t.manageId = chosen.manageId,\n\t\t\t\t\t});\n\t\t\t\tdone(std::move(result));\n\t\t\t} else {\n\t\t\t\tdone(nullptr);\n\t\t\t}\n\t\t});\n#endif\n\n\t\tauto inputs = QVector<MTPInputSavedStarGift>();\n\t\tfor (const auto &gift : gifts) {\n\t\t\tinputs.push_back(\n\t\t\t\tApi::InputSavedStarGiftId(gift.manageId, gift.unique));\n\t\t}\n\t\tconst auto weak = base::make_weak(controller);\n\t\tconst auto session = &controller->session();\n\t\tsession->api().request(MTPpayments_CraftStarGift(\n\t\t\tMTP_vector<MTPInputSavedStarGift>(inputs)\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\tsession->api().applyUpdates(result);\n\t\t\tsession->data().nextForUpgradeGiftInvalidate(session->user());\n\t\t\tconst auto gift = FindUniqueGift(session, result);\n\t\t\tconst auto slug = gift ? gift->info.unique->slug : QString();\n\t\t\tfor (const auto &input : gifts) {\n\t\t\t\tconst auto action = (slug == input.unique->slug)\n\t\t\t\t\t? Data::GiftUpdate::Action::Upgraded\n\t\t\t\t\t: Data::GiftUpdate::Action::Delete;\n\t\t\t\tcontroller->session().data().notifyGiftUpdate({\n\t\t\t\t\t.id = input.manageId,\n\t\t\t\t\t.slug = input.unique->slug,\n\t\t\t\t\t.action = action,\n\t\t\t\t});\n\t\t\t}\n\t\t\tdone(gift);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconst auto type = error.type();\n\t\t\tconst auto waitPrefix = u\"STARGIFT_CRAFT_TOO_EARLY_\"_q;\n\t\t\tif (type.startsWith(waitPrefix)) {\n\t\t\t\tdone(CraftResultWait{\n\t\t\t\t\t.seconds = type.mid(waitPrefix.size()).toInt(),\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tdone(CraftResultError{ type });\n\t\t\t}\n\t\t}).send();\n\t};\n\tconst auto requested = std::make_shared<bool>();\n\tconst auto giftId = gifts.front().unique->initialGiftId;\n\tauto retryWithNewGift = [=](Fn<void()> closeCurrent) {\n\t\tif (*requested) {\n\t\t\treturn;\n\t\t}\n\t\t*requested = true;\n\t\tShowSelectGiftBox(controller, giftId, [=](GiftForCraft chosen) {\n\t\t\tShowGiftCraftBox(controller, { chosen }, false);\n\t\t\tcloseCurrent();\n\t\t}, {}, [=] { *requested = false; }, true);\n\t};\n\tStartCraftAnimation(\n\t\tbox,\n\t\tstd::move(show),\n\t\tstd::move(state),\n\t\tstd::move(startRequest),\n\t\tstd::move(retryWithNewGift));\n}\n\nvoid AddPreviewNewModels(\n\t\tnot_null<VerticalLayout*> container,\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tconst QString &giftName,\n\t\tData::UniqueGiftAttributes attributes,\n\t\trpl::producer<bool> visible) {\n\tauto exceptional = std::vector<Data::UniqueGiftModel>();\n\tfor (const auto &model : attributes.models) {\n\t\tif (Data::UniqueGiftAttributeHasSpecialRarity(model)) {\n\t\t\texceptional.push_back(model);\n\t\t}\n\t}\n\tattributes.models = std::move(exceptional);\n\n\tauto emoji = TextWithEntities();\n\tconst auto indices = RandomIndicesSubset(\n\t\tint(attributes.models.size()),\n\t\tstd::min(3, int(attributes.models.size())));\n\tfor (const auto index : indices) {\n\t\temoji.append(Data::SingleCustomEmoji(\n\t\t\tattributes.models[index].document));\n\t}\n\n\tauto badge = object_ptr<AbstractButton>(container);\n\tconst auto badgeRaw = badge.data();\n\n\tconst auto label = CreateChild<FlatLabel>(\n\t\tbadgeRaw,\n\t\ttr::lng_gift_craft_view_all(\n\t\t\tlt_emoji,\n\t\t\trpl::single(emoji),\n\t\t\tlt_arrow,\n\t\t\trpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\tText::WithEntities),\n\t\tst::uniqueGiftResalePrice,\n\t\tst::defaultPopupMenu,\n\t\tCore::TextContext({ .session = &show->session() }));\n\tlabel->setTextColorOverride(st::white->c);\n\n\tlabel->sizeValue() | rpl::on_next([=](QSize size) {\n\t\tconst auto padding = st::craftPreviewAllPadding;\n\t\tbadgeRaw->setNaturalWidth(\n\t\t\tpadding.left() + size.width() + padding.right());\n\t\tbadgeRaw->resize(\n\t\t\tbadgeRaw->naturalWidth(),\n\t\t\tpadding.top() + size.height() + padding.bottom());\n\t}, label->lifetime());\n\n\tbadgeRaw->widthValue() | rpl::on_next([=](int width) {\n\t\tconst auto padding = st::craftPreviewAllPadding;\n\t\tlabel->move(padding.left(), padding.top());\n\t}, badgeRaw->lifetime());\n\n\tbadgeRaw->paintOn([=](QPainter &p) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto rect = badgeRaw->rect();\n\t\tconst auto radius = rect.height() / 2.;\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(ForgeBgOverlay());\n\t\tp.drawRoundedRect(rect, radius, radius);\n\t});\n\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tbadgeRaw->setClickedCallback([=] {\n\t\tauto previewAttrs = attributes;\n\t\tpreviewAttrs.models.erase(\n\t\t\tranges::remove_if(previewAttrs.models, [](const auto &m) {\n\t\t\t\treturn !Data::UniqueGiftAttributeHasSpecialRarity(m);\n\t\t\t}),\n\t\t\tend(previewAttrs.models));\n\t\tshow->show(Box(\n\t\t\tStarGiftPreviewBox,\n\t\t\tgiftName,\n\t\t\tpreviewAttrs,\n\t\t\tData::GiftAttributeIdType::Model,\n\t\t\tnullptr));\n\t});\n\n\tconst auto wrap = container->add(\n\t\tobject_ptr<SlideWrap<AbstractButton>>(\n\t\t\tcontainer,\n\t\t\tstd::move(badge),\n\t\t\tst::craftPreviewAllMargin),\n\t\tstyle::al_top);\n\tstd::move(visible) | rpl::on_next([=](bool shown) {\n\t\twrap->toggle(shown, anim::type::instant);\n\t}, wrap->lifetime());\n}\n\nvoid MakeCraftContent(\n\t\tnot_null<GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tstd::vector<GiftForCraft> gifts,\n\t\tbool autoStartCraft) {\n\tExpects(!gifts.empty());\n\n\tstruct State {\n\t\tstd::shared_ptr<CraftState> craftState;\n\t\tGradientButton *button = nullptr;\n\n\t\tFlatLabel *title = nullptr;\n\t\tFlatLabel *about = nullptr;\n\t\tRpWidget *attributes = nullptr;\n\t\tRpWidget *craftingView = nullptr;\n\t\tFn<void(std::shared_ptr<CraftState>)> grabCraftingView;\n\n\t\trpl::variable<std::vector<GiftForCraft>> chosen;\n\t\trpl::variable<QString> name;\n\t\trpl::variable<int> successPercentPermille;\n\t\trpl::variable<QString> successPercentText;\n\n\t\tint requestingIndex = -1;\n\t\tbool crafting = false;\n\t};\n\tconst auto session = &controller->session();\n\tconst auto giftId = gifts.front().unique->initialGiftId;\n\tconst auto state = box->lifetime().make_state<State>();\n\n\tconst auto auctions = &controller->session().giftAuctions();\n\tauto attributes = auctions->attributes(giftId).value_or(\n\t\tData::UniqueGiftAttributes());\n\n\tstate->craftState = std::make_shared<CraftState>();\n\tstate->craftState->session = session;\n\tstate->craftState->coversAnimate = true;\n\n\t{\n\t\tauto backdrops = CraftBackdrops();\n\t\tconst auto emoji = Text::IconEmoji(&st::craftPattern);\n\t\tconst auto data = emoji.entities.front().data();\n\t\tfor (auto i = 0; i != backdrops.size(); ++i) {\n\t\t\tauto &cover = state->craftState->covers[i];\n\t\t\tcover.backdrop.colors = backdrops[i].backdrop;\n\t\t\tcover.pattern.emoji = Text::TryMakeSimpleEmoji(data);\n\t\t\tcover.button1 = backdrops[i].button1;\n\t\t\tcover.button2 = backdrops[i].button2;\n\t\t}\n\t}\n\n\tconst auto giftName = gifts.front().unique->title;\n\tstate->chosen = std::move(gifts);\n\tfor (auto i = 0; i != int(state->chosen.current().size()); ++i) {\n\t\tstate->craftState->covers[i].shown = true;\n\t}\n\n\tstate->name = state->chosen.value(\n\t) | rpl::map([=](const std::vector<GiftForCraft> &gifts) {\n\t\treturn Data::UniqueGiftName(*gifts.front().unique);\n\t});\n\tstate->successPercentPermille = state->chosen.value(\n\t) | rpl::map([=](const std::vector<GiftForCraft> &gifts) {\n\t\tauto result = 0;\n\t\tfor (const auto &entry : gifts) {\n\t\t\tif (const auto gift = entry.unique.get()) {\n\t\t\t\tresult += gift->craftChancePermille;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t});\n\tstate->successPercentText = state->successPercentPermille.value(\n\t) | rpl::map(FormatPercent);\n\n\tconst auto raw = box->verticalLayout();\n\n\tstate->chosen.value(\n\t) | rpl::on_next([=](const std::vector<GiftForCraft> &gifts) {\n\t\tstate->craftState->updateForGiftCount(int(gifts.size()), [=] {\n\t\t\traw->update();\n\t\t});\n\t}, box->lifetime());\n\n\tconst auto title = state->title = raw->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_gift_craft_title(),\n\t\t\tst::uniqueGiftTitle),\n\t\tst::craftTitleMargin,\n\t\tstyle::al_top);\n\ttitle->setTextColorOverride(QColor(255, 255, 255));\n\n\tauto crafting = MakeCraftingView(\n\t\traw,\n\t\tsession,\n\t\tstate->chosen.value(),\n\t\tstate->craftState->edgeColor.value());\n\tconst auto craftingHeight = crafting.widget->height();\n\tstate->craftingView = crafting.widget.data();\n\tstate->grabCraftingView = std::move(crafting.grabForAnimation);\n\traw->add(std::move(crafting.widget));\n\tstd::move(crafting.removeRequests) | rpl::on_next([=](int index) {\n\t\tauto chosen = state->chosen.current();\n\t\tif (index < chosen.size()) {\n\t\t\tchosen.erase(begin(chosen) + index);\n\t\t\tstate->chosen = std::move(chosen);\n\t\t}\n\t}, raw->lifetime());\n\n\tstd::move(\n\t\tcrafting.editRequests\n\t) | rpl::on_next([=](int index) {\n\t\tconst auto guard = base::make_weak(raw);\n\t\tif (state->requestingIndex >= 0) {\n\t\t\tstate->requestingIndex = index;\n\t\t\treturn;\n\t\t}\n\t\tstate->requestingIndex = index;\n\t\tconst auto callback = [=](GiftForCraft chosen) {\n\t\t\tauto copy = state->chosen.current();\n\t\t\tif (state->requestingIndex < copy.size()) {\n\t\t\t\tcopy[state->requestingIndex] = chosen;\n\t\t\t} else {\n\t\t\t\tcopy.push_back(chosen);\n\t\t\t}\n\t\t\tstate->chosen = std::move(copy);\n\t\t};\n\t\tconst auto first = (state->requestingIndex == 0);\n\t\tShowSelectGiftBox(\n\t\t\tcontroller,\n\t\t\tgiftId,\n\t\t\tcrl::guard(raw, callback),\n\t\t\tstate->chosen.current(),\n\t\t\tcrl::guard(raw, [=] { state->requestingIndex = -1; }),\n\t\t\tfirst);\n\t}, raw->lifetime());\n\n\tauto fullName = state->chosen.value(\n\t) | rpl::map([=](const std::vector<GiftForCraft> &list) {\n\t\tconst auto unique = list.front().unique.get();\n\t\treturn Data::SingleCustomEmoji(\n\t\t\tunique->model.document\n\t\t).append(' ').append(tr::bold(Data::UniqueGiftName(*unique)));\n\t});\n\tauto aboutText = rpl::combine(\n\t\ttr::lng_gift_craft_about1(lt_gift, fullName, tr::rich),\n\t\ttr::lng_gift_craft_about2(tr::rich)\n\t) | rpl::map([=](TextWithEntities &&a, TextWithEntities &&b) {\n\t\treturn a.append('\\n').append('\\n').append(b);\n\t});\n\tconst auto about = state->about = raw->add(\n\t\tobject_ptr<FlatLabel>(\n\t\t\traw,\n\t\t\tstd::move(aboutText),\n\t\t\tst::craftAbout,\n\t\t\tst::defaultPopupMenu,\n\t\t\tCore::TextContext({ .session = session })),\n\t\tst::craftAboutMargin,\n\t\tstyle::al_top);\n\tabout->setTextColorOverride(st::white->c);\n\tabout->setTryMakeSimilarLines(true);\n\n\tstate->attributes = raw->add(\n\t\tMakeRarityExpectancyPreview(raw, session, state->chosen.value()),\n\t\tst::craftAttributesMargin,\n\t\tstyle::al_top);\n\n\tauto viewAllVisible = state->chosen.value(\n\t) | rpl::map([](const std::vector<GiftForCraft> &list) {\n\t\tauto backdropIds = base::flat_set<int>();\n\t\tauto patternPtrs = base::flat_set<DocumentData*>();\n\t\tfor (const auto &gift : list) {\n\t\t\tbackdropIds.emplace(gift.unique->backdrop.id);\n\t\t\tpatternPtrs.emplace(gift.unique->pattern.document.get());\n\t\t}\n\t\treturn int(backdropIds.size() + patternPtrs.size()) <= 5;\n\t});\n\n\tAddPreviewNewModels(\n\t\traw,\n\t\tcontroller->uiShow(),\n\t\tgiftName,\n\t\tstd::move(attributes),\n\t\tstd::move(viewAllVisible));\n\n\traw->paintOn([=](QPainter &p) {\n\t\tconst auto &cs = state->craftState;\n\t\tif (cs->craftingStarted) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto wasButton1 = cs->button1;\n\t\tconst auto wasButton2 = cs->button2;\n\t\tcs->paint(p, raw->size(), craftingHeight);\n\t\tif (cs->button1 != wasButton1 || cs->button2 != wasButton2) {\n\t\t\tstate->button->setStops({\n\t\t\t\t{ 0., cs->button1 },\n\t\t\t\t{ 1., cs->button2 },\n\t\t\t});\n\t\t}\n\t});\n\n\tconst auto button = state->button = raw->add(\n\t\tobject_ptr<GradientButton>(raw, QGradientStops()),\n\t\tst::craftBoxButtonPadding);\n\tbutton->setFullRadius(true);\n\tbutton->startGlareAnimation();\n\n\tconst auto startCrafting = [=] {\n\t\tif (state->crafting) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto &gifts = state->chosen.current();\n\t\tif (!gifts.empty()\n\t\t\t&& ShowCraftAddressError(box->uiShow(), gifts.front().unique)) {\n\t\t\treturn;\n\t\t}\n\t\tstate->crafting = true;\n\n\t\tconst auto &cs = state->craftState;\n\t\tcs->giftName = state->name.current();\n\t\tcs->successPermille = state->successPercentPermille.current();\n\n\t\tfor (const auto &gift : gifts) {\n\t\t\tif (gift.unique) {\n\t\t\t\tcs->lostGifts.push_back({\n\t\t\t\t\t.number = u\"#\"_q + Lang::FormatCountDecimal(gift.unique->number),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tif (state->grabCraftingView) {\n\t\t\tstate->grabCraftingView(cs);\n\t\t}\n\n\t\tconst auto craftingPos = state->craftingView->pos();\n\t\tcs->craftingTop = craftingPos.y();\n\t\tcs->craftingBottom = craftingPos.y() + state->craftingView->height();\n\n\t\tfor (auto &corner : cs->corners) {\n\t\t\tcorner.originalRect.translate(craftingPos);\n\t\t}\n\t\tcs->forgeRect.translate(craftingPos);\n\t\tcs->craftingAreaCenterY = cs->forgeRect.center().y();\n\n\t\tconst auto aboutPos = state->about->pos();\n\t\tconst auto buttonY = state->button->pos().y();\n\t\tconst auto buttonBottom = buttonY + state->button->height();\n\t\tcs->originalButtonY = buttonY;\n\t\tconst auto bottomRect = QRect(\n\t\t\t0,\n\t\t\taboutPos.y(),\n\t\t\traw->width(),\n\t\t\tbuttonBottom - aboutPos.y());\n\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tauto bottomPart = QImage(\n\t\t\tbottomRect.size() * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tbottomPart.fill(Qt::transparent);\n\t\tbottomPart.setDevicePixelRatio(ratio);\n\n\t\tconst auto renderWidget = [&](QWidget *widget) {\n\t\t\tconst auto pos = widget->pos() - bottomRect.topLeft();\n\t\t\twidget->render(&bottomPart, pos, {}, QWidget::DrawChildren);\n\t\t};\n\t\trenderWidget(state->about);\n\t\trenderWidget(state->attributes);\n\t\trenderWidget(state->button);\n\n\t\tcs->bottomPart = std::move(bottomPart);\n\t\tcs->bottomPartY = aboutPos.y();\n\t\tcs->containerHeight = raw->height();\n\n\t\tCraft(\n\t\t\tbox,\n\t\t\tcontroller,\n\t\t\tstate->craftState,\n\t\t\tstate->chosen.current());\n\t};\n\tbutton->setClickedCallback(startCrafting);\n\n\tSetButtonTwoLabels(\n\t\tbutton,\n\t\tst::giftBox.button.textTop,\n\t\ttr::lng_gift_craft_button(\n\t\t\tlt_gift,\n\t\t\tstate->name.value() | rpl::map(tr::marked),\n\t\t\ttr::marked),\n\t\ttr::lng_gift_craft_button_chance(\n\t\t\tlt_percent,\n\t\t\tstate->successPercentText.value() | rpl::map(tr::marked),\n\t\t\ttr::marked),\n\t\tst::resaleButtonTitle,\n\t\tst::resaleButtonSubtitle);\n\n\traw->widthValue() | rpl::on_next([=](int width) {\n\t\tconst auto padding = st::craftBoxButtonPadding;\n\t\tbutton->setNaturalWidth(width - padding.left() - padding.right());\n\t\tbutton->resize(button->naturalWidth(), st::giftBox.button.height);\n\t}, raw->lifetime());\n\n\tif (autoStartCraft) {\n//\t\tbase::call_delayed(crl::time(1000), raw, startCrafting);\n\t}\n}\n\nvoid ShowGiftCraftBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tstd::vector<GiftForCraft> gifts,\n\t\tbool autoStartCraft) {\n\tcontroller->show(Box([=](not_null<GenericBox*> box) {\n\t\tbox->setStyle(st::giftCraftBox);\n\t\tbox->setWidth(st::boxWideWidth);\n\t\tbox->setNoContentMargin(true);\n\t\tMakeCraftContent(\n\t\t\tbox,\n\t\t\tcontroller,\n\t\t\tgifts,\n\t\t\tautoStartCraft);\n\t\tAddUniqueCloseButton(box);\n\n#if _DEBUG\n\t\tif (autoStartCraft) {\n\t\t\tstatic const auto full = gifts;\n\t\t\tbox->boxClosing() | rpl::on_next([=] {\n\t\t\t\tbase::call_delayed(1000, controller, [=] {\n\t\t\t\t\tauto copy = gifts;\n\t\t\t\t\tif (copy.size() > 1) {\n\t\t\t\t\t\tcopy.pop_back();\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcopy = full;\n\t\t\t\t\t}\n\t\t\t\t\tShowGiftCraftBox(controller, copy, true);\n\t\t\t\t});\n\t\t\t}, box->lifetime());\n\t\t}\n#endif\n\t}));\n}\n\n} // namespace\n\nvoid ShowGiftCraftInfoBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tstd::shared_ptr<Data::UniqueGift> gift,\n\t\tData::SavedStarGiftId savedId) {\n\tcontroller->show(Box([=](not_null<GenericBox*> box) {\n\t\tconst auto container = box->verticalLayout();\n\t\tauto cover = tr::lng_gift_craft_info_title(\n\t\t) | rpl::map([=](const QString &title) {\n\t\t\tauto result = UniqueGiftCover{ *gift };\n\t\t\tresult.values.title = title;\n\t\t\treturn result;\n\t\t});\n\t\tAddUniqueGiftCover(container, std::move(cover), {\n\t\t\t.subtitle = tr::lng_gift_craft_info_about(tr::marked),\n\t\t});\n\t\tAddSkip(container);\n\n\t\tAddUniqueCloseButton(box);\n\n\t\tconst auto features = std::vector<FeatureListEntry>{\n\t\t\t{\n\t\t\t\tst::menuIconUnique,\n\t\t\t\ttr::lng_gift_craft_rare_title(tr::now),\n\t\t\t\ttr::lng_gift_craft_rare_about(tr::now, tr::rich),\n\t\t\t},\n\t\t\t{\n\t\t\t\tst::menuIconCraftTraits,\n\t\t\t\ttr::lng_gift_craft_chance_title(tr::now),\n\t\t\t\ttr::lng_gift_craft_chance_about(tr::now, tr::marked),\n\t\t\t},\n\t\t\t{\n\t\t\t\tst::menuIconCraftChance,\n\t\t\t\ttr::lng_gift_craft_affect_title(tr::now),\n\t\t\t\ttr::lng_gift_craft_affect_about(tr::now, tr::marked),\n\t\t\t},\n\t\t};\n\t\tfor (const auto &feature : features) {\n\t\t\tcontainer->add(\n\t\t\t\tMakeFeatureListEntry(container, feature),\n\t\t\t\tst::boxRowPadding);\n\t\t}\n\n\t\tbox->setStyle(st::giftBox);\n\t\tbox->setWidth(st::boxWideWidth);\n\t\tbox->setNoContentMargin(true);\n\n\t\tbox->addButton(tr::lng_gift_craft_start_button(), [=] {\n\t\t\tconst auto auctions = &controller->session().giftAuctions();\n\t\t\tconst auto show = crl::guard(box, [=] {\n\t\t\t\tShowGiftCraftBox(\n\t\t\t\t\tcontroller,\n\t\t\t\t\t{ GiftForCraft{ gift, savedId } },\n\t\t\t\t\tfalse);\n\t\t\t\tbox->closeBox();\n\t\t\t});\n\t\t\tif (auctions->attributes(gift->initialGiftId)) {\n\t\t\t\tshow();\n\t\t\t} else {\n\t\t\t\tauctions->requestAttributes(gift->initialGiftId, show);\n\t\t\t}\n\t\t});\n\t}));\n}\n\nvoid ShowTestGiftCraftBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tstd::vector<GiftForCraftEntry> gifts) {\n\tauto converted = std::vector<GiftForCraft>();\n\tconverted.reserve(gifts.size());\n\tfor (auto &gift : gifts) {\n\t\tauto entry = GiftForCraft();\n\t\tentry.unique = std::move(gift.unique);\n\t\tentry.manageId = std::move(gift.manageId);\n\t\tconverted.push_back(std::move(entry));\n\t}\n\tShowGiftCraftBox(controller, std::move(converted), true);\n}\n\nbool ShowCraftLaterError(\n\t\tstd::shared_ptr<Show> show,\n\t\tstd::shared_ptr<Data::UniqueGift> gift) {\n\tconst auto now = base::unixtime::now();\n\tif (gift->canCraftAt <= now) {\n\t\treturn false;\n\t}\n\tShowCraftLaterError(show, gift->canCraftAt);\n\treturn true;\n}\n\nvoid ShowCraftLaterError(\n\t\tstd::shared_ptr<Show> show,\n\t\tTimeId when) {\n\tconst auto data = base::unixtime::parse(when);\n\tconst auto time = QLocale().toString(data.time(), QLocale::ShortFormat);\n\tconst auto date = langDayOfMonthShort(data.date());\n\n\tshow->show(MakeInformBox({\n\t\t.text = tr::lng_gift_craft_when(\n\t\t\tlt_date,\n\t\t\trpl::single(tr::bold(date)),\n\t\t\tlt_time,\n\t\t\trpl::single(tr::bold(time)),\n\t\t\ttr::marked),\n\t\t.title = tr::lng_gift_craft_unavailable(),\n\t}));\n}\n\nbool ShowCraftAddressError(\n\t\tstd::shared_ptr<Show> show,\n\t\tstd::shared_ptr<Data::UniqueGift> gift) {\n\tif (gift->giftAddress.isEmpty()) {\n\t\treturn false;\n\t}\n\tshow->show(MakeInformBox({\n\t\t.text = tr::lng_gift_craft_address_error(tr::marked),\n\t\t.title = tr::lng_gift_craft_unavailable(),\n\t}));\n\treturn true;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/star_gift_craft_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_star_gift.h\"\n\nnamespace Data {\nstruct UniqueGift;\n} // namespace Data\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\n\nclass Show;\n\nstruct GiftForCraftEntry {\n\tstd::shared_ptr<Data::UniqueGift> unique;\n\tData::SavedStarGiftId manageId;\n};\n\nvoid ShowGiftCraftInfoBox(\n\tnot_null<Window::SessionController*> controller,\n\tstd::shared_ptr<Data::UniqueGift> gift,\n\tData::SavedStarGiftId savedId);\n\nvoid ShowTestGiftCraftBox(\n\tnot_null<Window::SessionController*> controller,\n\tstd::vector<GiftForCraftEntry> gifts);\n\n[[nodiscard]] bool ShowCraftLaterError(\n\tstd::shared_ptr<Show> show,\n\tstd::shared_ptr<Data::UniqueGift> gift);\n\nvoid ShowCraftLaterError(\n\tstd::shared_ptr<Show> show,\n\tTimeId when);\n\n[[nodiscard]] bool ShowCraftAddressError(\n\tstd::shared_ptr<Show> show,\n\tstd::shared_ptr<Data::UniqueGift> gift);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/star_gift_preview_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/star_gift_preview_box.h\"\n\n#include \"base/random.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_star_gift.h\"\n#include \"history/view/media/history_view_sticker_player.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/about_cocoon_box.h\" // Ui::AddUniqueCloseButton\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/painter.h\"\n#include \"ui/top_background_gradient.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Ui {\nnamespace {\n\nusing Tab = Data::GiftAttributeIdType;\nusing GiftModel = Data::UniqueGiftModel;\nusing GiftPattern = Data::UniqueGiftPattern;\nusing GiftBackdrop = Data::UniqueGiftBackdrop;\nconstexpr auto kSwitchTimeout = 3 * crl::time(1000);\nconstexpr auto kGiftsPerRow = 3;\n\nstruct AttributeDescriptor\n\t: std::variant<GiftModel, GiftPattern, GiftBackdrop> {\n\n\tfriend inline bool operator==(\n\t\tconst AttributeDescriptor &,\n\t\tconst AttributeDescriptor &) = default;\n};\n\n[[nodiscard]] DocumentData *Sticker(const AttributeDescriptor &value) {\n\tusing namespace Data;\n\tif (const auto pattern = std::get_if<UniqueGiftPattern>(&value)) {\n\t\treturn pattern->document;\n\t} else if (const auto model = std::get_if<UniqueGiftModel>(&value)) {\n\t\treturn model->document;\n\t}\n\treturn nullptr;\n}\n\nstruct BackdropPlayers {\n\tHistoryView::StickerPlayer *now = nullptr;\n\tHistoryView::StickerPlayer *next = nullptr;\n\tfloat64 progress = 0.;\n};\n\nstruct PatternEmoji {\n\tstd::shared_ptr<Text::CustomEmoji> now;\n\tstd::shared_ptr<Text::CustomEmoji> next;\n\tfloat64 progress = 0.;\n};\n\nclass AttributeDelegate {\npublic:\n\t[[nodiscard]] virtual QSize buttonSize() = 0;\n\t[[nodiscard]] virtual QMargins buttonExtend() const = 0;\n\t[[nodiscard]] virtual QImage background() = 0;\n\t[[nodiscard]] virtual QImage cachePatternBackground(\n\t\tint width,\n\t\tint height,\n\t\tQMargins extend) = 0;\n\t[[nodiscard]] virtual QColor patternColor() = 0;\n\t[[nodiscard]] virtual BackdropPlayers backdropPlayers() = 0;\n\t[[nodiscard]] virtual PatternEmoji patternEmoji() = 0;\n};\n\nclass AttributeButton final : public Ui::AbstractButton {\npublic:\n\tAttributeButton(\n\t\tQWidget *parent,\n\t\tnot_null<AttributeDelegate*> delegate,\n\t\tconst AttributeDescriptor &descriptor);\n\n\tvoid toggleSelected(bool selected, anim::type animated);\n\n\tvoid setDescriptor(const AttributeDescriptor &descriptor);\n\tvoid setGeometry(QRect inner, QMargins extend);\n\nprivate:\n\tstruct PatternCache {\n\t\tbase::flat_map<float64, QImage> map;\n\t\tstd::shared_ptr<Text::CustomEmoji> emoji;\n\t};\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid paintBackground(QPainter &p, const QImage &background);\n\n\tvoid setup();\n\tvoid validatePatternCache();\n\tvoid setDocument(not_null<DocumentData*> document);\n\n\tconst not_null<AttributeDelegate*> _delegate;\n\tQImage _hiddenBgCache;\n\tAttributeDescriptor _descriptor;\n\tUi::Text::String _name;\n\tUi::Text::String _percent;\n\tQImage _backgroundCache;\n\tPatternCache _patternNow;\n\tPatternCache _patternNext;\n\tQImage _patternFrame;\n\tQColor _patternColor;\n\tUi::Animations::Simple _selectedAnimation;\n\tbool _selected : 1 = false;\n\n\tQMargins _extend;\n\n\tDocumentData *_document = nullptr;\n\tDocumentData *_playerDocument = nullptr;\n\tstd::unique_ptr<HistoryView::StickerPlayer> _player;\n\trpl::lifetime _mediaLifetime;\n\n};\n\nclass Delegate final : public AttributeDelegate {\npublic:\n\texplicit Delegate(Fn<void()> fullUpdate);\n\n\tDelegate(Delegate &&other) = default;\n\t~Delegate() = default;\n\n\tvoid update(\n\t\tconst std::optional<Data::UniqueGift> &now,\n\t\tconst std::optional<Data::UniqueGift> &next,\n\t\tfloat64 progress);\n\n\t[[nodiscard]] QSize buttonSize() override;\n\t[[nodiscard]] QMargins buttonExtend() const override;\n\t[[nodiscard]] QImage background() override;\n\t[[nodiscard]] QImage cachePatternBackground(\n\t\tint width,\n\t\tint height,\n\t\tQMargins extend) override;\n\t[[nodiscard]] QColor patternColor() override;\n\t[[nodiscard]] BackdropPlayers backdropPlayers() override;\n\t[[nodiscard]] PatternEmoji patternEmoji() override;\n\nprivate:\n\tstruct Sticker {\n\t\tDocumentData *document = nullptr;\n\t\tDocumentData *playerDocument = nullptr;\n\t\tstd::unique_ptr<HistoryView::StickerPlayer> player;\n\t\trpl::lifetime mediaLifetime;\n\t};\n\tstruct Emoji {\n\t\tDocumentData *document;\n\t\tstd::shared_ptr<Text::CustomEmoji> custom;\n\t};\n\n\tFn<void()> _fullUpdate;\n\tQSize _single;\n\tQImage _bg;\n\n\tGiftBackdrop _nowBackdrop;\n\tGiftBackdrop _nextBackdrop;\n\n\tSticker _nowModel;\n\tSticker _nextModel;\n\n\tEmoji _nowPatternEmoji;\n\tEmoji _nextPatternEmoji;\n\n\tfloat64 _progress = 0.;\n\n\tGiftBackdrop _backdrop;\n\tQImage _backdropCache;\n\tbool _backdropDirty = false;\n\n};\n\nstruct Selection {\n\tint model = -1;\n\tint pattern = -1;\n\tint backdrop = -1;\n\n\tfriend inline bool operator==(Selection, Selection) = default;\n};\n\nclass AttributesList final : public Ui::BoxContentDivider {\npublic:\n\tAttributesList(\n\t\tQWidget *parent,\n\t\tnot_null<Delegate*> delegate,\n\t\tnot_null<const Data::UniqueGiftAttributes*> attributes,\n\t\tstd::vector<Data::UniqueGiftModel> otherModels,\n\t\trpl::producer<Tab> tab,\n\t\tSelection initialSelection);\n\n\t[[nodiscard]] rpl::producer<Selection> selected() const;\n\t[[nodiscard]] auto modelsToggled() const\n\t\t-> rpl::producer<std::vector<Data::UniqueGiftModel>>;\n\nprivate:\n\tstruct Entry {\n\t\tAttributeDescriptor descriptor;\n\t\tSelection value;\n\t};\n\tstruct Entries {\n\t\tstd::vector<Entry> list;\n\t};\n\tstruct View {\n\t\tstd::unique_ptr<AttributeButton> button;\n\t\tDocumentData *document = nullptr;\n\t\tint index = 0;\n\t};\n\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid fill();\n\tvoid refreshAbout();\n\tvoid refreshButtons();\n\tvoid validateButtons();\n\n\tint resizeGetHeight(int width) override;\n\tvoid clicked(int index);\n\tvoid toggleModels();\n\n\tconst not_null<Delegate*> _delegate;\n\tconst not_null<const Data::UniqueGiftAttributes*> _attributes;\n\trpl::variable<Tab> _tab;\n\trpl::variable<Selection> _selected;\n\n\tstd::unique_ptr<Ui::RpWidget> _about;\n\n\tEntries _models;\n\tEntries _patterns;\n\tEntries _backdrops;\n\tnot_null<Entries*> _entries;\n\tnot_null<std::vector<Entry>*> _list;\n\n\tstd::vector<View> _views;\n\tint _viewsForWidth = 0;\n\tint _viewsFromRow = 0;\n\tint _viewsTillRow = 0;\n\n\tQSize _singleMin;\n\tQSize _single;\n\tint _perRow = 0;\n\tint _visibleFrom = 0;\n\tint _visibleTill = 0;\n\n\tbool _craftedModels = false;\n\tstd::vector<Data::UniqueGiftModel> _activeModels;\n\tstd::vector<Data::UniqueGiftModel> _otherModels;\n\tstd::unique_ptr<Ui::FlatLabel> _toggleLink;\n\trpl::event_stream<std::vector<Data::UniqueGiftModel>> _modelsToggled;\n\n};\n\n[[nodiscard]] QColor MakeOpaque(QColor color, QColor bg) {\n\treturn (color.alpha() == 255)\n\t\t? color\n\t\t: anim::color(\n\t\t\tbg,\n\t\t\tQColor(color.red(), color.green(), color.blue(), 255),\n\t\t\tcolor.alphaF());\n}\n\nvoid CacheBackdropBackground(\n\t\tnot_null<GiftBackdrop*> backdrop,\n\t\tint width,\n\t\tint height,\n\t\tQMargins extend,\n\t\tQImage &cache,\n\t\tbool force = false) {\n\tconst auto outer = QRect(0, 0, width, height);\n\tconst auto inner = outer.marginsRemoved(\n\t\textend\n\t).translated(-extend.left(), -extend.top());\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (cache.size() != inner.size() * ratio) {\n\t\tcache = QImage(\n\t\t\tinner.size() * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tcache.setDevicePixelRatio(ratio);\n\t\tforce = true;\n\t}\n\tif (force) {\n\t\tcache.fill(Qt::transparent);\n\n\t\tconst auto radius = st::giftBoxGiftRadius;\n\t\tauto p = QPainter(&cache);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tauto gradient = QRadialGradient(inner.center(), inner.width() / 2);\n\t\tgradient.setStops({\n\t\t\t{ 0., backdrop->centerColor },\n\t\t\t{ 1., backdrop->edgeColor },\n\t\t});\n\t\tp.setBrush(gradient);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawRoundedRect(inner, radius, radius);\n\t}\n}\n\nAttributeButton::AttributeButton(\n\tQWidget *parent,\n\tnot_null<AttributeDelegate*> delegate,\n\tconst AttributeDescriptor &descriptor)\n: AbstractButton(parent)\n, _delegate(delegate)\n, _descriptor(descriptor) {\n\tsetup();\n}\n\nvoid AttributeButton::setup() {\n\tv::match(_descriptor, [&](const auto &data) {\n\t\t_name.setText(st::uniqueAttributeName, data.name);\n\t\t_percent.setText(\n\t\t\tst::uniqueAttributePercent,\n\t\t\tData::UniqueGiftAttributeText(data));\n\t});\n\n\tv::match(_descriptor, [&](const GiftBackdrop &data) {\n\t\tconst auto destroyed = base::take(_player);\n\t\t_document = nullptr;\n\t\t_playerDocument = nullptr;\n\t\t_mediaLifetime.destroy();\n\t\t_patternNow = {};\n\t\t_patternNext = {};\n\t\t_patternFrame = QImage();\n\t}, [&](const GiftModel &data) {\n\t\tsetDocument(data.document);\n\t\t_patternNow = {};\n\t\t_patternNext = {};\n\t\t_patternFrame = QImage();\n\t}, [&](const GiftPattern &data) {\n\t\tconst auto document = data.document;\n\t\tif (_document != document) {\n\t\t\tsetDocument(document);\n\t\t\tconst auto owner = &document->owner();\n\t\t\t_patternNow.emoji = owner->customEmojiManager().create(\n\t\t\t\tdocument,\n\t\t\t\t[=] { update(); },\n\t\t\t\tData::CustomEmojiSizeTag::Large);\n\t\t\t_patternNow.map.clear();\n\t\t\t_patternColor = QColor();\n\t\t}\n\t});\n}\n\nvoid AttributeButton::setDescriptor(const AttributeDescriptor &descriptor) {\n\tif (_descriptor == descriptor) {\n\t\treturn;\n\t}\n\t_descriptor = descriptor;\n\t_backgroundCache = QImage();\n\tsetup();\n\tupdate();\n}\n\nvoid AttributeButton::setDocument(not_null<DocumentData*> document) {\n\tif (_document == document) {\n\t\treturn;\n\t}\n\t_document = document;\n\n\tconst auto media = document->createMediaView();\n\tmedia->checkStickerLarge();\n\tmedia->goodThumbnailWanted();\n\n\tconst auto destroyed = base::take(_player);\n\t_playerDocument = nullptr;\n\t_mediaLifetime = rpl::single() | rpl::then(\n\t\tdocument->session().downloaderTaskFinished()\n\t) | rpl::filter([=] {\n\t\treturn media->loaded();\n\t}) | rpl::on_next([=] {\n\t\t_mediaLifetime.destroy();\n\n\t\tauto result = std::unique_ptr<HistoryView::StickerPlayer>();\n\t\tconst auto sticker = document->sticker();\n\t\tif (sticker->isLottie()) {\n\t\t\tresult = std::make_unique<HistoryView::LottiePlayer>(\n\t\t\t\tChatHelpers::LottiePlayerFromDocument(\n\t\t\t\t\tmedia.get(),\n\t\t\t\t\tChatHelpers::StickerLottieSize::InlineResults,\n\t\t\t\t\tst::uniqueAttributeStickerSize,\n\t\t\t\t\tLottie::Quality::High));\n\t\t} else if (sticker->isWebm()) {\n\t\t\tresult = std::make_unique<HistoryView::WebmPlayer>(\n\t\t\t\tmedia->owner()->location(),\n\t\t\t\tmedia->bytes(),\n\t\t\t\tst::uniqueAttributeStickerSize);\n\t\t} else {\n\t\t\tresult = std::make_unique<HistoryView::StaticStickerPlayer>(\n\t\t\t\tmedia->owner()->location(),\n\t\t\t\tmedia->bytes(),\n\t\t\t\tst::uniqueAttributeStickerSize);\n\t\t}\n\t\tresult->setRepaintCallback([=] { update(); });\n\t\t_playerDocument = media->owner();\n\t\t_player = std::move(result);\n\t\tupdate();\n\t});\n\tif (_playerDocument) {\n\t\t_mediaLifetime.destroy();\n\t}\n}\n\nvoid AttributeButton::setGeometry(QRect inner, QMargins extend) {\n\t_extend = extend;\n\tAbstractButton::setGeometry(inner.marginsAdded(extend));\n}\n\nvoid AttributeButton::toggleSelected(bool selected, anim::type animated) {\n\tif (_selected == selected) {\n\t\tif (animated == anim::type::instant) {\n\t\t\t_selectedAnimation.stop();\n\t\t}\n\t\treturn;\n\t}\n\n\tconst auto duration = st::defaultRoundCheckbox.duration;\n\t_selected = selected;\n\tif (animated == anim::type::instant) {\n\t\t_selectedAnimation.stop();\n\t\treturn;\n\t}\n\t_selectedAnimation.start([=] {\n\t\tupdate();\n\t}, selected ? 0. : 1., selected ? 1. : 0., duration, anim::easeOutCirc);\n}\n\nvoid AttributeButton::paintBackground(\n\t\tQPainter &p,\n\t\tconst QImage &background) {\n\tconst auto removed = QMargins();\n\tconst auto x = removed.left();\n\tconst auto y = removed.top();\n\tconst auto width = this->width() - x - removed.right();\n\tconst auto height = this->height() - y - removed.bottom();\n\tconst auto dpr = int(background.devicePixelRatio());\n\tconst auto bwidth = background.width() / dpr;\n\tconst auto bheight = background.height() / dpr;\n\tconst auto fillRow = [&](int yfrom, int ytill, int bfrom) {\n\t\tconst auto fill = [&](int xto, int wto, int xfrom, int wfrom = 0) {\n\t\t\tconst auto fheight = ytill - yfrom;\n\t\t\tp.drawImage(\n\t\t\t\tQRect(x + xto, y + yfrom, wto, fheight),\n\t\t\t\tbackground,\n\t\t\t\tQRect(\n\t\t\t\t\tQPoint(xfrom, bfrom) * dpr,\n\t\t\t\t\tQSize((wfrom ? wfrom : wto), fheight) * dpr));\n\t\t};\n\t\tif (width < bwidth) {\n\t\t\tconst auto xhalf = width / 2;\n\t\t\tfill(0, xhalf, 0);\n\t\t\tfill(xhalf, width - xhalf, bwidth - (width - xhalf));\n\t\t} else if (width == bwidth) {\n\t\t\tfill(0, width, 0);\n\t\t} else {\n\t\t\tconst auto half = bwidth / (2 * dpr);\n\t\t\tfill(0, half, 0);\n\t\t\tfill(width - half, half, bwidth - half);\n\t\t\tfill(half, width - 2 * half, half, 1);\n\t\t}\n\t};\n\tif (height < bheight) {\n\t\tfillRow(0, height / 2, 0);\n\t\tfillRow(height / 2, height, bheight - (height - (height / 2)));\n\t} else {\n\t\tfillRow(0, height, 0);\n\t}\n\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto progress = _selectedAnimation.value(_selected ? 1. : 0.);\n\tif (progress < 0.01) {\n\t\treturn;\n\t}\n\tconst auto pwidth = progress * st::defaultRoundCheckbox.width;\n\tconst auto model = std::get_if<GiftModel>(&_descriptor);\n\tconst auto color = (!model\n\t\t|| (model->rarityType() == Data::UniqueGiftRarity::Default))\n\t\t? st::defaultRoundCheckbox.bgActive->c\n\t\t: Data::UniqueGiftRarityBadgeColors(model->rarityType()).fg;\n\tp.setPen(QPen(color, pwidth));\n\tp.setBrush(Qt::NoBrush);\n\tconst auto rounded = rect().marginsRemoved(_extend);\n\tconst auto phalf = pwidth / 2.;\n\tconst auto extended = QRectF(rounded).marginsRemoved(\n\t\t{ phalf, phalf, phalf, phalf });\n\tconst auto xradius = removed.left() + st::giftBoxGiftRadius - phalf;\n\tconst auto yradius = removed.top() + st::giftBoxGiftRadius - phalf;\n\tp.drawRoundedRect(extended, xradius, yradius);\n}\n\nvoid AttributeButton::validatePatternCache() {\n\tif (_patternNow.emoji && _patternNow.emoji->ready()) {\n\t\tconst auto inner = QRect(0, 0,\n\t\t\tthis->width() - _extend.left() - _extend.right(),\n\t\t\tthis->height() - _extend.top() - _extend.bottom());\n\t\tconst auto size = inner.size();\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tif (_patternFrame.size() != size * ratio) {\n\t\t\t_patternFrame = QImage(\n\t\t\t\tsize * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_patternFrame.setDevicePixelRatio(ratio);\n\t\t\t_patternColor = QColor();\n\t\t}\n\t\tconst auto color = _delegate->patternColor();\n\t\tif (_patternColor != color) {\n\t\t\t_patternColor = color;\n\t\t\t_patternNow.map.clear();\n\t\t\t_patternFrame.fill(Qt::transparent);\n\n\t\t\tauto p = QPainter(&_patternFrame);\n\t\t\tconst auto skip = inner.width() / 3;\n\t\t\tUi::PaintBgPoints(\n\t\t\t\tp,\n\t\t\t\tUi::PatternBgPointsSmall(),\n\t\t\t\t_patternNow.map,\n\t\t\t\t_patternNow.emoji.get(),\n\t\t\t\tcolor,\n\t\t\t\tQRect(-skip, 0, inner.width() + 2 * skip, inner.height()));\n\t\t}\n\t} else if (!_patternFrame.isNull()) {\n\t\t_patternFrame = QImage();\n\t}\n}\n\nvoid AttributeButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tconst auto model = std::get_if<GiftModel>(&_descriptor);\n\tconst auto pattern = std::get_if<GiftPattern>(&_descriptor);\n\tconst auto backdrop = std::get_if<GiftBackdrop>(&_descriptor);\n\tconst auto position = QPoint(_extend.left(), _extend.top());\n\tconst auto background = _delegate->background();\n\tconst auto width = this->width();\n\tconst auto dpr = int(background.devicePixelRatio());\n\tpaintBackground(p, background);\n\tif (backdrop) {\n\t\tCacheBackdropBackground(\n\t\t\tbackdrop,\n\t\t\twidth,\n\t\t\tbackground.height() / dpr,\n\t\t\t_extend,\n\t\t\t_backgroundCache);\n\t\tp.drawImage(_extend.left(), _extend.top(), _backgroundCache);\n\n\t\tconst auto inner = QRect(\n\t\t\t_extend.left(),\n\t\t\t_extend.top(),\n\t\t\tthis->width() - _extend.left() - _extend.right(),\n\t\t\tthis->height() - _extend.top() - _extend.bottom());\n\t\tconst auto skip = inner.width() / 3;\n\t\tconst auto emoji = _delegate->patternEmoji();\n\t\tconst auto paintSymbols = [&](\n\t\t\t\tPatternCache &cache,\n\t\t\t\tstd::shared_ptr<Text::CustomEmoji> emoji,\n\t\t\t\tfloat64 shown) {\n\t\t\tif (shown == 0. || !emoji) {\n\t\t\t\treturn;\n\t\t\t} else if (cache.emoji != emoji) {\n\t\t\t\tcache.map.clear();\n\t\t\t\tcache.emoji = emoji;\n\t\t\t}\n\t\t\tUi::PaintBgPoints(\n\t\t\t\tp,\n\t\t\t\tUi::PatternBgPointsSmall(),\n\t\t\t\tcache.map,\n\t\t\t\temoji.get(),\n\t\t\t\tbackdrop->patternColor,\n\t\t\t\tQRect(-skip, 0, inner.width() + 2 * skip, inner.height()),\n\t\t\t\tshown);\n\t\t};\n\t\tp.setClipRect(inner);\n\t\tpaintSymbols(_patternNow, emoji.now, 1. - emoji.progress);\n\t\tpaintSymbols(_patternNext, emoji.next, emoji.progress);\n\t\tp.setClipping(false);\n\t} else if (pattern) {\n\t\tp.drawImage(\n\t\t\t_extend.left(),\n\t\t\t_extend.top(),\n\t\t\t_delegate->cachePatternBackground(\n\t\t\t\twidth,\n\t\t\t\tbackground.height() / dpr,\n\t\t\t\t_extend));\n\t\tvalidatePatternCache();\n\t\tif (!_patternFrame.isNull()) {\n\t\t\tp.drawImage(_extend.left(), _extend.top(), _patternFrame);\n\t\t}\n\t}\n\n\tconst auto progress = _selectedAnimation.value(_selected ? 1. : 0.);\n\tif (progress > 0) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tauto pen = st::boxBg->p;\n\t\tconst auto thickness = style::ConvertScaleExact(2.);\n\t\tpen.setWidthF(progress * thickness);\n\t\tp.setPen(pen);\n\t\tp.setBrush(Qt::NoBrush);\n\t\tconst auto height = background.height() / dpr;\n\t\tconst auto outer = QRectF(0, 0, width, height);\n\t\tconst auto shift = progress * thickness * 2;\n\t\tconst auto extend = QMarginsF(_extend)\n\t\t\t+ QMarginsF(shift, shift, shift, shift);\n\t\tconst auto radius = st::giftBoxGiftRadius - shift;\n\t\tp.drawRoundedRect(outer.marginsRemoved(extend), radius, radius);\n\t}\n\n\tconst auto paused = !isOver();\n\tconst auto now = crl::now();\n\tconst auto colored = v::is<GiftPattern>(_descriptor)\n\t\t? QColor(255, 255, 255)\n\t\t: QColor(0, 0, 0, 0);\n\tconst auto frameSize = st::uniqueAttributeStickerSize;\n\tconst auto paintFrame = [&](HistoryView::StickerPlayer *player) {\n\t\tif (!player || !player->ready()) {\n\t\t\treturn;\n\t\t}\n\t\tauto info = player->frame(frameSize, colored, false, now, paused);\n\t\tconst auto finished = (info.index + 1 == player->framesCount());\n\t\tif (!finished || !paused) {\n\t\t\tplayer->markFrameShown();\n\t\t}\n\t\tconst auto size = info.image.size() / style::DevicePixelRatio();\n\t\tp.drawImage(\n\t\t\tQRect(\n\t\t\t\t(width - size.width()) / 2,\n\t\t\t\tst::giftBoxSmallStickerTop,\n\t\t\t\tsize.width(),\n\t\t\t\tsize.height()),\n\t\t\tinfo.image);\n\t};\n\tif (backdrop) {\n\t\tconst auto backdropPlayers = _delegate->backdropPlayers();\n\t\tif (backdropPlayers.progress == 0.) {\n\t\t\tpaintFrame(backdropPlayers.now);\n\t\t} else if (backdropPlayers.progress == 1.) {\n\t\t\tpaintFrame(backdropPlayers.next);\n\t\t} else {\n\t\t\tp.setOpacity(1. - backdropPlayers.progress);\n\t\t\tpaintFrame(backdropPlayers.now);\n\t\t\tp.setOpacity(backdropPlayers.progress);\n\t\t\tpaintFrame(backdropPlayers.next);\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t} else {\n\t\tpaintFrame(_player.get());\n\t}\n\n\tconst auto singlew = width - _extend.left() - _extend.right();\n\tif (model) {\n\t\tp.setPen(st::windowBoldFg);\n\t} else {\n\t\tp.setPen(QColor(255, 255, 255));\n\t}\n\t_name.draw(p, {\n\t\t.position = (position + QPoint(0, st::giftBoxPremiumTextTop)),\n\t\t.availableWidth = singlew,\n\t\t.align = style::al_top,\n\t});\n\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(backdrop\n\t\t? backdrop->patternColor\n\t\t: !model\n\t\t? _delegate->patternColor()\n\t\t: (model->rarityType() == Data::UniqueGiftRarity::Default)\n\t\t? anim::color(st::windowBgOver, st::windowBgActive, progress)\n\t\t: MakeOpaque(\n\t\t\tData::UniqueGiftRarityBadgeColors(model->rarityType()).bg,\n\t\t\tst::boxBg->c));\n\tconst auto ppadding = st::uniqueAttributePercentPadding;\n\tconst auto add = int(std::ceil(style::ConvertScaleExact(6.)));\n\tconst auto pradius = std::max(st::giftBoxGiftRadius - add, 1);\n\tconst auto left = position.x()\n\t\t+ singlew\n\t\t- add\n\t\t- ppadding.right()\n\t\t- _percent.maxWidth();\n\tconst auto top = position.y() + add + ppadding.top();\n\tconst auto percent = QRect(\n\t\tleft,\n\t\ttop,\n\t\t_percent.maxWidth(),\n\t\tst::uniqueAttributeType.font->height);\n\tp.drawRoundedRect(percent.marginsAdded(ppadding), pradius, pradius);\n\tp.setPen(!model\n\t\t? QColor(255, 255, 255)\n\t\t: (model->rarityType() == Data::UniqueGiftRarity::Default)\n\t\t? anim::color(st::windowSubTextFg, st::windowFgActive, progress)\n\t\t: Data::UniqueGiftRarityBadgeColors(model->rarityType()).fg);\n\t_percent.draw(p, {\n\t\t.position = percent.topLeft(),\n\t});\n}\n\nDelegate::Delegate(Fn<void()> fullUpdate)\n: _fullUpdate(std::move(fullUpdate)) {\n}\n\nvoid Delegate::update(\n\t\tconst std::optional<Data::UniqueGift> &now,\n\t\tconst std::optional<Data::UniqueGift> &next,\n\t\tfloat64 progress) {\n\tconst auto wasDirty = _backdropDirty;\n\tconst auto badSticker = [&](\n\t\t\tSticker &model,\n\t\t\tconst std::optional<Data::UniqueGift> &gift) {\n\t\treturn gift && (model.document != gift->model.document);\n\t};\n\tconst auto enforceSticker = [&](\n\t\t\tSticker &model,\n\t\t\tconst std::optional<Data::UniqueGift> &gift) {\n\t\tExpects(gift.has_value());\n\n\t\tconst auto document = model.document = gift->model.document;\n\t\tconst auto media = document->createMediaView();\n\t\tmedia->checkStickerLarge();\n\t\tmedia->goodThumbnailWanted();\n\n\t\tconst auto destroyed = base::take(model.player);\n\t\tmodel.playerDocument = nullptr;\n\t\tmodel.mediaLifetime = rpl::single() | rpl::then(\n\t\t\tdocument->session().downloaderTaskFinished()\n\t\t) | rpl::filter([=] {\n\t\t\treturn media->loaded();\n\t\t}) | rpl::on_next([=, &model] {\n\t\t\tmodel.mediaLifetime.destroy();\n\n\t\t\tauto result = std::unique_ptr<HistoryView::StickerPlayer>();\n\t\t\tconst auto sticker = document->sticker();\n\t\t\tif (sticker->isLottie()) {\n\t\t\t\tresult = std::make_unique<HistoryView::LottiePlayer>(\n\t\t\t\t\tChatHelpers::LottiePlayerFromDocument(\n\t\t\t\t\t\tmedia.get(),\n\t\t\t\t\t\tChatHelpers::StickerLottieSize::InlineResults,\n\t\t\t\t\t\tst::uniqueAttributeStickerSize,\n\t\t\t\t\t\tLottie::Quality::High));\n\t\t\t} else if (sticker->isWebm()) {\n\t\t\t\tresult = std::make_unique<HistoryView::WebmPlayer>(\n\t\t\t\t\tmedia->owner()->location(),\n\t\t\t\t\tmedia->bytes(),\n\t\t\t\t\tst::uniqueAttributeStickerSize);\n\t\t\t} else {\n\t\t\t\tresult = std::make_unique<HistoryView::StaticStickerPlayer>(\n\t\t\t\t\tmedia->owner()->location(),\n\t\t\t\t\tmedia->bytes(),\n\t\t\t\t\tst::uniqueAttributeStickerSize);\n\t\t\t}\n\t\t\tresult->setRepaintCallback(_fullUpdate);\n\t\t\tmodel.playerDocument = media->owner();\n\t\t\tmodel.player = std::move(result);\n\t\t\t_fullUpdate();\n\t\t});\n\t\tif (model.playerDocument) {\n\t\t\tmodel.mediaLifetime.destroy();\n\t\t}\n\t};\n\tconst auto validateSticker = [&](\n\t\t\tSticker &model,\n\t\t\tconst std::optional<Data::UniqueGift> &gift) {\n\t\tif (badSticker(model, gift)) {\n\t\t\tenforceSticker(model, gift);\n\t\t}\n\t};\n\tconst auto validatePatternEmoji = [&](\n\t\t\tEmoji &emoji,\n\t\t\tconst std::optional<Data::UniqueGift> &gift) {\n\t\tif (!gift) {\n\t\t\temoji = {};\n\t\t\treturn;\n\t\t}\n\t\tconst auto document = gift->pattern.document;\n\t\tif (emoji.document != document) {\n\t\t\temoji.document = document;\n\t\t\temoji.custom = document->owner().customEmojiManager().create(\n\t\t\t\tdocument,\n\t\t\t\t_fullUpdate,\n\t\t\t\tData::CustomEmojiSizeTag::Large);\n\t\t}\n\t};\n\n\tif (progress == 1.) {\n\t\tAssert(next.has_value());\n\t\tif (_nextBackdrop != next->backdrop) {\n\t\t\t_nextBackdrop = next->backdrop;\n\t\t\t_backdropDirty = true;\n\t\t}\n\t\tvalidateSticker(_nextModel, next);\n\t\tvalidatePatternEmoji(_nextPatternEmoji, next);\n\t\t_nowModel = {};\n\t\t_nowPatternEmoji = {};\n\t} else {\n\t\tAssert(now.has_value());\n\t\tif (_nowBackdrop != now->backdrop) {\n\t\t\tif (_nextBackdrop == now->backdrop) {\n\t\t\t\t_nowBackdrop = base::take(_nextBackdrop);\n\t\t\t} else {\n\t\t\t\t_nowBackdrop = now->backdrop;\n\t\t\t\t_backdropDirty = true;\n\t\t\t}\n\t\t}\n\t\tif (badSticker(_nowModel, now)) {\n\t\t\tif (_nextModel.document == now->model.document\n\t\t\t\t&& !_nextModel.mediaLifetime) {\n\t\t\t\t_nowModel = base::take(_nextModel);\n\t\t\t} else {\n\t\t\t\tenforceSticker(_nowModel, now);\n\t\t\t}\n\t\t}\n\t\tvalidatePatternEmoji(_nowPatternEmoji, now);\n\t\tif (progress > 0.) {\n\t\t\tAssert(next.has_value());\n\t\t\tif (_nextBackdrop != next->backdrop) {\n\t\t\t\t_nextBackdrop = next->backdrop;\n\t\t\t\t_backdropDirty = true;\n\t\t\t}\n\t\t\tvalidateSticker(_nextModel, next);\n\t\t\tvalidatePatternEmoji(_nextPatternEmoji, next);\n\t\t} else {\n\t\t\tif (_nextModel.document) {\n\t\t\t\t_nextModel = Sticker();\n\t\t\t}\n\t\t\tif (_nextPatternEmoji.document) {\n\t\t\t\t_nextPatternEmoji = Emoji();\n\t\t\t}\n\t\t}\n\t}\n\tif (_progress != progress) {\n\t\t_progress = progress;\n\t\t_backdropDirty = true;\n\t}\n\tif (!wasDirty && _backdropDirty) {\n\t\t_fullUpdate();\n\t}\n}\n\nQSize Delegate::buttonSize() {\n\tif (!_single.isEmpty()) {\n\t\treturn _single;\n\t}\n\tconst auto width = st::boxWideWidth;\n\tconst auto padding = st::giftBoxPadding;\n\tconst auto available = width - padding.left() - padding.right();\n\tconst auto singlew = (available - 2 * st::giftBoxGiftSkip.x())\n\t\t/ kGiftsPerRow;\n\t_single = QSize(singlew, st::giftBoxGiftSmall);\n\treturn _single;\n}\n\nQMargins Delegate::buttonExtend() const {\n\treturn st::defaultDropdownMenu.wrap.shadow.extend;\n}\n\nQImage Delegate::background() {\n\tif (!_bg.isNull()) {\n\t\treturn _bg;\n\t}\n\tconst auto single = buttonSize();\n\tconst auto extend = buttonExtend();\n\tconst auto bgSize = single.grownBy(extend);\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto bg = QImage(\n\t\tbgSize * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tbg.setDevicePixelRatio(ratio);\n\tbg.fill(Qt::transparent);\n\n\tconst auto radius = st::giftBoxGiftRadius;\n\tconst auto rect = QRect(QPoint(), bgSize).marginsRemoved(extend);\n\n\t{\n\t\tauto p = QPainter(&bg);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setOpacity(0.3);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::windowShadowFg);\n\t\tp.drawRoundedRect(\n\t\t\tQRectF(rect).translated(0, radius / 12.),\n\t\t\tradius,\n\t\t\tradius);\n\t}\n\tbg = bg.scaled(\n\t\t(bgSize * ratio) / 2,\n\t\tQt::IgnoreAspectRatio,\n\t\tQt::SmoothTransformation);\n\tbg = Images::Blur(std::move(bg), true);\n\tbg = bg.scaled(\n\t\tbgSize * ratio,\n\t\tQt::IgnoreAspectRatio,\n\t\tQt::SmoothTransformation);\n\t{\n\t\tauto p = QPainter(&bg);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::windowBg);\n\t\tp.drawRoundedRect(rect, radius, radius);\n\t}\n\n\t_bg = std::move(bg);\n\treturn _bg;\n}\n\nQImage Delegate::cachePatternBackground(\n\t\tint width,\n\t\tint height,\n\t\tQMargins extend) {\n\tif (_backdropDirty) {\n\t\t_backdrop = GiftBackdrop();\n\t\tif (_progress == 0.) {\n\t\t\t_backdrop = _nowBackdrop;\n\t\t} else if (_progress == 1.) {\n\t\t\t_backdrop = _nextBackdrop;\n\t\t} else {\n\t\t\t_backdrop.centerColor = anim::color(\n\t\t\t\t_nowBackdrop.centerColor,\n\t\t\t\t_nextBackdrop.centerColor,\n\t\t\t\t_progress);\n\t\t\t_backdrop.edgeColor = anim::color(\n\t\t\t\t_nowBackdrop.edgeColor,\n\t\t\t\t_nextBackdrop.edgeColor,\n\t\t\t\t_progress);\n\t\t\t_backdrop.patternColor = anim::color(\n\t\t\t\t_nowBackdrop.patternColor,\n\t\t\t\t_nextBackdrop.patternColor,\n\t\t\t\t_progress);\n\t\t}\n\t}\n\tCacheBackdropBackground(\n\t\t&_backdrop,\n\t\twidth,\n\t\theight,\n\t\textend,\n\t\t_backdropCache,\n\t\t_backdropDirty);\n\n\t_backdropDirty = false;\n\treturn _backdropCache;\n}\n\nQColor Delegate::patternColor() {\n\tExpects(!_backdropDirty);\n\n\treturn _backdrop.patternColor;\n}\n\nBackdropPlayers Delegate::backdropPlayers() {\n\treturn {\n\t\t.now = _nowModel.player.get(),\n\t\t.next = _nextModel.player.get(),\n\t\t.progress = _progress,\n\t};\n}\n\nPatternEmoji Delegate::patternEmoji() {\n\treturn {\n\t\t.now = _nowPatternEmoji.custom,\n\t\t.next = _nextPatternEmoji.custom,\n\t\t.progress = _progress,\n\t};\n}\n\nAttributesList::AttributesList(\n\tQWidget *parent,\n\tnot_null<Delegate*> delegate,\n\tnot_null<const Data::UniqueGiftAttributes*> attributes,\n\tstd::vector<Data::UniqueGiftModel> otherModels,\n\trpl::producer<Tab> tab,\n\tSelection initialSelection)\n: BoxContentDivider(parent)\n, _delegate(delegate)\n, _attributes(attributes)\n, _tab(std::move(tab))\n, _selected(initialSelection)\n, _entries(&_models)\n, _list(&_entries->list) {\n\t_singleMin = _delegate->buttonSize();\n\t_activeModels.assign(\n\t\tattributes->models.begin(),\n\t\tattributes->models.end());\n\t_otherModels = std::move(otherModels);\n\n\t_craftedModels = !_activeModels.empty()\n\t\t&& (_activeModels.front().rarityType()\n\t\t\t!= Data::UniqueGiftRarity::Default);\n\n\tfill();\n\n\t_tab.value(\n\t) | rpl::on_next([=](Tab tab) {\n\t\t_entries = [&] {\n\t\t\tswitch (tab) {\n\t\t\tcase Tab::Model: return &_models;\n\t\t\tcase Tab::Pattern: return &_patterns;\n\t\t\tcase Tab::Backdrop: return &_backdrops;\n\t\t\t}\n\t\t\tUnexpected(\"Tab in AttributesList.\");\n\t\t}();\n\t\t_list = &_entries->list;\n\t\trefreshAbout();\n\t\trefreshButtons();\n\t}, lifetime());\n\n\t_selected.value() | rpl::combine_previous() | rpl::on_next([=](\n\t\t\tSelection was,\n\t\t\tSelection now) {\n\t\tconst auto tab = _tab.current();\n\t\tconst auto button = [&](int index) {\n\t\t\tconst auto i = ranges::find(_views, index, &View::index);\n\t\t\treturn (i != end(_views)) ? i->button.get() : nullptr;\n\t\t};\n\t\tconst auto update = [&](int was, int now) {\n\t\t\tif (was != now) {\n\t\t\t\tif (const auto deselect = button(was)) {\n\t\t\t\t\tdeselect->toggleSelected(false, anim::type::normal);\n\t\t\t\t}\n\t\t\t\tif (const auto select = button(now)) {\n\t\t\t\t\tselect->toggleSelected(true, anim::type::normal);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tif (tab == Tab::Model) {\n\t\t\tupdate(was.model, now.model);\n\t\t} else if (tab == Tab::Pattern) {\n\t\t\tupdate(was.pattern, now.pattern);\n\t\t} else {\n\t\t\tupdate(was.backdrop, now.backdrop);\n\t\t}\n\t}, lifetime());\n}\n\nauto AttributesList::selected() const -> rpl::producer<Selection> {\n\treturn _selected.value();\n}\n\nauto AttributesList::modelsToggled() const\n\t-> rpl::producer<std::vector<Data::UniqueGiftModel>> {\n\treturn _modelsToggled.events();\n}\n\nvoid AttributesList::fill() {\n\tconst auto addEntries = [&](\n\t\t\tconst auto &source,\n\t\t\tEntries &target,\n\t\t\tconst auto field) {\n\t\tauto value = Selection();\n\t\ttarget.list.clear();\n\t\ttarget.list.reserve(source.size());\n\t\tfor (const auto &item : source) {\n\t\t\t++(value.*field);\n\t\t\ttarget.list.emplace_back(Entry{ { item }, { value } });\n\t\t}\n\t};\n\taddEntries(_activeModels, _models, &Selection::model);\n\taddEntries(_attributes->patterns, _patterns, &Selection::pattern);\n\taddEntries(_attributes->backdrops, _backdrops, &Selection::backdrop);\n}\n\nvoid AttributesList::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\t_visibleFrom = visibleTop;\n\t_visibleTill = visibleBottom;\n\tvalidateButtons();\n}\n\nvoid AttributesList::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tp.fillRect(rect(), st::boxDividerBg->c);\n\tpaintTop(p);\n\tpaintBottom(p);\n}\n\nvoid AttributesList::refreshButtons() {\n\t_viewsForWidth = 0;\n\t_viewsFromRow = 0;\n\t_viewsTillRow = 0;\n\tresizeToWidth(width());\n\tvalidateButtons();\n}\n\nvoid AttributesList::validateButtons() {\n\tif (!_perRow || !_about) {\n\t\treturn;\n\t}\n\tconst auto padding = st::giftBoxPadding;\n\tconst auto vskip = st::giftListAboutMargin.top()\n\t\t+ _about->height()\n\t\t+ st::giftListAboutMargin.bottom();\n\tconst auto row = _single.height() + st::giftBoxGiftSkip.y();\n\tconst auto fromRow = std::max(_visibleFrom - vskip, 0) / row;\n\tconst auto tillRow = (_visibleTill - vskip + row - 1) / row;\n\tAssert(tillRow >= fromRow);\n\tif (_viewsFromRow == fromRow\n\t\t&& _viewsTillRow == tillRow\n\t\t&& _viewsForWidth == width()) {\n\t\treturn;\n\t}\n\t_viewsFromRow = fromRow;\n\t_viewsTillRow = tillRow;\n\t_viewsForWidth = width();\n\n\tconst auto available = _viewsForWidth - padding.left() - padding.right();\n\tconst auto skipw = st::giftBoxGiftSkip.x();\n\tconst auto fullw = _perRow * (_single.width() + skipw) - skipw;\n\tconst auto left = padding.left() + (available - fullw) / 2;\n\tconst auto oneh = _single.height() + st::giftBoxGiftSkip.y();\n\tauto x = left;\n\tauto y = vskip + fromRow * oneh;\n\tauto views = std::vector<View>();\n\tviews.reserve((tillRow - fromRow) * _perRow);\n\tconst auto idUsed = [&](DocumentData *document, int column, int row) {\n\t\tif (!document) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (auto j = row; j != tillRow; ++j) {\n\t\t\tfor (auto i = column; i != _perRow; ++i) {\n\t\t\t\tconst auto index = j * _perRow + i;\n\t\t\t\tif (index >= _list->size()) {\n\t\t\t\t\treturn false;\n\t\t\t\t} else if (Sticker((*_list)[index].descriptor) == document) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tcolumn = 0;\n\t\t}\n\t\treturn false;\n\t};\n\tconst auto selected = [&] {\n\t\tconst auto values = _selected.current();\n\t\tswitch (_tab.current()) {\n\t\tcase Tab::Model: return values.model;\n\t\tcase Tab::Pattern: return values.pattern;\n\t\tcase Tab::Backdrop: return values.backdrop;\n\t\t}\n\t\tUnexpected(\"Tab in AttributesList::validateButtons.\");\n\t}();\n\tconst auto add = [&](int column, int row) {\n\t\tconst auto index = row * _perRow + column;\n\t\tif (index >= _list->size()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto &entry = (*_list)[index];\n\t\tconst auto &descriptor = entry.descriptor;\n\t\tconst auto document = Sticker(descriptor);\n\t\tconst auto already = ranges::find_if(_views, [&](const View &view) {\n\t\t\treturn view.button && view.document == document;\n\t\t});\n\t\tif (already != end(_views)) {\n\t\t\tviews.push_back(base::take(*already));\n\t\t\tviews.back().button->setDescriptor(descriptor);\n\t\t} else {\n\t\t\tconst auto unused = ranges::find_if(_views, [&](const View &v) {\n\t\t\t\treturn v.button && !idUsed(v.document, column, row);\n\t\t\t});\n\t\t\tif (unused != end(_views)) {\n\t\t\t\tviews.push_back(base::take(*unused));\n\t\t\t\tviews.back().document = document;\n\t\t\t\tviews.back().button->setDescriptor(descriptor);\n\t\t\t} else {\n\t\t\t\tviews.push_back({\n\t\t\t\t\t.button = std::make_unique<AttributeButton>(\n\t\t\t\t\t\tthis,\n\t\t\t\t\t\t_delegate,\n\t\t\t\t\t\tdescriptor),\n\t\t\t\t\t.document = document,\n\t\t\t\t});\n\t\t\t\tviews.back().button->show();\n\t\t\t}\n\t\t}\n\t\tauto &view = views.back();\n\t\tview.index = index;\n\t\tview.button->toggleSelected(index == selected, anim::type::instant);\n\t\tview.button->setClickedCallback([=] {\n\t\t\tclicked(index);\n\t\t});\n\t\treturn true;\n\t};\n\tfor (auto j = fromRow; j != tillRow; ++j) {\n\t\tfor (auto i = 0; i != _perRow; ++i) {\n\t\t\tif (!add(i, j)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst auto &view = views.back();\n\t\t\tauto pos = QPoint(x, y);\n\n\t\t\tview.button->setGeometry(\n\t\t\t\tQRect(pos, _single),\n\t\t\t\t_delegate->buttonExtend());\n\t\t\tx += _single.width() + skipw;\n\t\t}\n\t\tx = left;\n\t\ty += oneh;\n\t}\n\n\tstd::swap(_views, views);\n}\n\nvoid AttributesList::clicked(int index) {\n\tExpects(index >= 0 && index < _list->size());\n\n\tauto now = _selected.current();\n\tconst auto value = (*_list)[index].value;\n\tconst auto check = [&](const auto field) {\n\t\tif (value.*field >= 0) {\n\t\t\tnow.*field = (now.*field == value.*field) ? -1 : value.*field;\n\t\t}\n\t};\n\tcheck(&Selection::model);\n\tcheck(&Selection::pattern);\n\tcheck(&Selection::backdrop);\n\t_selected = now;\n}\n\nvoid AttributesList::refreshAbout() {\n\tauto text = [&] {\n\t\tswitch (_tab.current()) {\n\t\tcase Tab::Model: return _craftedModels\n\t\t\t? tr::lng_auction_preview_crafted\n\t\t\t: tr::lng_auction_preview_models;\n\t\tcase Tab::Pattern: return tr::lng_auction_preview_symbols;\n\t\tcase Tab::Backdrop: return tr::lng_auction_preview_backdrops;\n\t\t}\n\t\tUnexpected(\"Tab in AttributesList::refreshAbout.\");\n\t}()(lt_count_decimal, rpl::single(_list->size() * 1.), tr::rich);\n\tauto about = std::make_unique<Ui::FlatLabel>(\n\t\tthis,\n\t\tstd::move(text),\n\t\tst::giftListAbout);\n\tabout->show();\n\t_about = std::move(about);\n\n\tif (_tab.current() == Tab::Model && !_otherModels.empty()) {\n\t\tauto linkText = (_craftedModels\n\t\t\t? tr::lng_auction_preview_view_primary\n\t\t\t: tr::lng_auction_preview_view_craftable)(\n\t\t\tlt_arrow,\n\t\t\trpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\ttr::link);\n\t\tauto link = std::make_unique<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\tstd::move(linkText),\n\t\t\tst::giftListAboutToggle);\n\t\tlink->overrideLinkClickHandler([=] { toggleModels(); });\n\t\tlink->show();\n\t\t_toggleLink = std::move(link);\n\t} else {\n\t\t_toggleLink = nullptr;\n\t}\n\n\tresizeToWidth(width());\n}\n\nvoid AttributesList::toggleModels() {\n\tstd::swap(_activeModels, _otherModels);\n\t_craftedModels = !_craftedModels;\n\tfill();\n\t_entries = &_models;\n\t_list = &_entries->list;\n\t_modelsToggled.fire_copy(_activeModels);\n\tauto sel = _selected.current();\n\tsel.model = -1;\n\t_selected.force_assign(sel);\n\trefreshAbout();\n\trefreshButtons();\n}\n\nint AttributesList::resizeGetHeight(int width) {\n\tconst auto padding = st::giftBoxPadding;\n\tconst auto count = int(_list->size());\n\tconst auto available = width - padding.left() - padding.right();\n\tconst auto skipw = st::giftBoxGiftSkip.x();\n\t_perRow = std::min(\n\t\t(available + skipw) / (_singleMin.width() + skipw),\n\t\tstd::max(count, 1));\n\tif (!_perRow || !_about) {\n\t\treturn 0;\n\t}\n\tauto result = 0;\n\n\tconst auto margin = st::giftListAboutMargin;\n\tconst auto contentWidth = width - margin.left() - margin.right();\n\t_about->resizeToWidth(contentWidth);\n\tif (_toggleLink) {\n\t\t_toggleLink->resizeToWidth(contentWidth);\n\t}\n\tconst auto addedSpace = _toggleLink\n\t\t? (st::giftListAboutToggleSkip + _toggleLink->height())\n\t\t: 0;\n\tconst auto marginTop = margin.top() - addedSpace / 2;\n\tconst auto marginBottom = margin.bottom()\n\t\t- (addedSpace - addedSpace / 2);\n\t_about->moveToLeft(margin.left(), result + marginTop);\n\tresult += marginTop + _about->height();\n\tif (_toggleLink) {\n\t\tresult += st::giftListAboutToggleSkip;\n\t\t_toggleLink->moveToLeft(margin.left(), result);\n\t\tresult += _toggleLink->height();\n\t}\n\tresult += marginBottom;\n\n\tconst auto singlew = std::min(\n\t\t((available + skipw) / _perRow) - skipw,\n\t\t2 * _singleMin.width());\n\tAssert(singlew >= _singleMin.width());\n\tconst auto singleh = _singleMin.height();\n\n\t_single = QSize(singlew, singleh);\n\tconst auto rows = (count + _perRow - 1) / _perRow;\n\tconst auto skiph = st::giftBoxGiftSkip.y();\n\n\tresult += rows\n\t\t? (padding.bottom() + rows * (singleh + skiph) - skiph)\n\t\t: 0;\n\n\treturn result;\n}\n\n} // namespace\n\nvoid StarGiftPreviewBox(\n\t\tnot_null<GenericBox*> box,\n\t\tconst QString &title,\n\t\tData::UniqueGiftAttributes attributes,\n\t\tData::GiftAttributeIdType tab,\n\t\tstd::shared_ptr<Data::UniqueGift> selected) {\n\tExpects(!attributes.models.empty());\n\tExpects(!attributes.patterns.empty());\n\tExpects(!attributes.backdrops.empty());\n\n\tconst auto filter = [&](auto &list, auto attribute) {\n\t\tconst auto rarityProj = &Data::UniqueGiftAttribute::rarityType;\n\t\tconst auto simple = Data::UniqueGiftRarity::Default;\n\t\tif (!selected || ((*selected).*attribute).rarityType() == simple) {\n\t\t\tif (ranges::contains(list, simple, rarityProj)) {\n\t\t\t\tlist.erase(ranges::remove_if(list, [&](const auto &attribute) {\n\t\t\t\t\treturn attribute.rarityType() != simple;\n\t\t\t\t}), end(list));\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto rare = ((*selected).*attribute).rarityType();\n\t\t\tif (ranges::contains(list, rare, rarityProj)) {\n\t\t\t\tlist.erase(ranges::remove_if(list, [&](const auto &attribute) {\n\t\t\t\t\treturn attribute.rarityType() == simple;\n\t\t\t\t}), end(list));\n\t\t\t}\n\t\t}\n\t\tAssert(!list.empty());\n\t};\n\tfilter(attributes.patterns, &Data::UniqueGift::pattern);\n\tfilter(attributes.backdrops, &Data::UniqueGift::backdrop);\n\n\tauto otherModels = std::vector<Data::UniqueGiftModel>();\n\t{\n\t\tconst auto simple = Data::UniqueGiftRarity::Default;\n\t\tconst auto showCrafted = selected\n\t\t\t&& (selected->model.rarityType() != simple);\n\t\tauto defaultModels = std::vector<Data::UniqueGiftModel>();\n\t\tauto craftedModels = std::vector<Data::UniqueGiftModel>();\n\t\tfor (auto &model : attributes.models) {\n\t\t\tif (model.rarityType() == simple) {\n\t\t\t\tdefaultModels.push_back(std::move(model));\n\t\t\t} else {\n\t\t\t\tcraftedModels.push_back(std::move(model));\n\t\t\t}\n\t\t}\n\t\tif (!defaultModels.empty() && !craftedModels.empty()) {\n\t\t\tif (showCrafted) {\n\t\t\t\tattributes.models = std::move(craftedModels);\n\t\t\t\totherModels = std::move(defaultModels);\n\t\t\t} else {\n\t\t\t\tattributes.models = std::move(defaultModels);\n\t\t\t\totherModels = std::move(craftedModels);\n\t\t\t}\n\t\t} else if (!defaultModels.empty()) {\n\t\t\tattributes.models = std::move(defaultModels);\n\t\t} else {\n\t\t\tattributes.models = std::move(craftedModels);\n\t\t}\n\t}\n\n\tbox->setStyle(st::uniqueAttributesBox);\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setNoContentMargin(true);\n\n\tstruct State {\n\t\tState(\n\t\t\tQString title,\n\t\t\tconst Data::UniqueGiftAttributes &attributes,\n\t\t\tData::GiftAttributeIdType tab,\n\t\t\tstd::shared_ptr<Data::UniqueGift> selected)\n\t\t: title(title)\n\t\t, delegate([=] {\n\t\t\tif (this->tab.current() != Tab::Model && list) {\n\t\t\t\tlist->update();\n\t\t\t}\n\t\t})\n\t\t, attributes(attributes)\n\t\t, tab(tab)\n\t\t, gift(make())\n\t\t, pushNextTimer([=] { push(); }) {\n\t\t\tapply(selected);\n\t\t}\n\t\tvoid apply(std::shared_ptr<Data::UniqueGift> selected) {\n\t\t\tif (!selected) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto up = [&](auto &list, const auto &attribute) {\n\t\t\t\tranges::stable_partition(list, [&](const auto &existing) {\n\t\t\t\t\treturn IdFor(attribute) == IdFor(existing);\n\t\t\t\t});\n\t\t\t};\n\t\t\tup(attributes.models, selected->model);\n\t\t\tup(attributes.patterns, selected->pattern);\n\t\t\tup(attributes.backdrops, selected->backdrop);\n\t\t\tfixed = { 0, 0, 0 };\n\t\t\tpaused = true;\n\t\t\tgift = make();\n\t\t}\n\n\t\tvoid randomize() {\n\t\t\tconst auto choose = [](const auto &list, auto &indices) {\n\t\t\t\tif (indices.empty()) {\n\t\t\t\t\tranges::copy(\n\t\t\t\t\t\tranges::views::ints(0, int(list.size())),\n\t\t\t\t\t\tstd::back_inserter(indices));\n\t\t\t\t}\n\t\t\t\tconst auto which = base::RandomIndex(indices.size());\n\t\t\t\tconst auto index = indices[which];\n\t\t\t\tindices.erase(begin(indices) + which);\n\t\t\t\treturn index;\n\t\t\t};\n\t\t\tindex = {\n\t\t\t\t.model = choose(attributes.models, models),\n\t\t\t\t.pattern = choose(attributes.patterns, patterns),\n\t\t\t\t.backdrop = choose(attributes.backdrops, backdrops),\n\t\t\t};\n\t\t}\n\t\t[[nodiscard]] UniqueGiftCover make() {\n\t\t\trandomize();\n\n\t\t\tconst auto choose = [](const auto &list, int index, int fixed) {\n\t\t\t\treturn list[(fixed >= 0) ? fixed : index];\n\t\t\t};\n\t\t\treturn {\n\t\t\t\t.values = {\n\t\t\t\t\t.title = title,\n\t\t\t\t\t.model = choose(\n\t\t\t\t\t\tattributes.models,\n\t\t\t\t\t\tindex.model,\n\t\t\t\t\t\tfixed.model),\n\t\t\t\t\t.pattern = choose(\n\t\t\t\t\t\tattributes.patterns,\n\t\t\t\t\t\tindex.pattern,\n\t\t\t\t\t\tfixed.pattern),\n\t\t\t\t\t.backdrop = choose(\n\t\t\t\t\t\tattributes.backdrops,\n\t\t\t\t\t\tindex.backdrop,\n\t\t\t\t\t\tfixed.backdrop),\n\t\t\t\t},\n\t\t\t\t.force = paused.current(),\n\t\t\t};\n\t\t}\n\t\tvoid push() {\n\t\t\tgift = make();\n\t\t}\n\n\t\tQString title;\n\t\tDelegate delegate;\n\t\tData::UniqueGiftAttributes attributes;\n\t\tstd::vector<int> models;\n\t\tstd::vector<int> patterns;\n\t\tstd::vector<int> backdrops;\n\t\trpl::variable<Tab> tab = Tab::Model;\n\t\tSelection index;\n\t\tSelection fixed;\n\t\trpl::variable<bool> paused;\n\n\t\trpl::variable<UniqueGiftCover> gift;\n\n\t\tAttributesList *list = nullptr;\n\n\t\tbase::Timer pushNextTimer;\n\t};\n\n\tconst auto state = box->lifetime().make_state<State>(\n\t\ttitle,\n\t\tattributes,\n\t\ttab,\n\t\tselected);\n\n\tconst auto repaintedHook = [=](\n\t\t\tstd::optional<Data::UniqueGift> now,\n\t\t\tstd::optional<Data::UniqueGift> next,\n\t\t\tfloat64 progress) {\n\t\tstate->delegate.update(now, next, progress);\n\t};\n\n\tconst auto top = box->setPinnedToTopContent(\n\t\tobject_ptr<VerticalLayout>(box));\n\tAddUniqueGiftCover(top, state->gift.value(), {\n\t\t.subtitle = rpl::conditional(\n\t\t\tstate->paused.value(),\n\t\t\ttr::lng_auction_preview_selected(tr::marked),\n\t\t\ttr::lng_auction_preview_random(tr::marked)),\n\t\t.attributesInfo = true,\n\t\t.repaintedHook = repaintedHook,\n\t});\n\n\tUi::AddUniqueCloseButton(box);\n\n\tconst auto container = box->verticalLayout();\n\tstate->list = container->add(object_ptr<AttributesList>(\n\t\tbox,\n\t\t&state->delegate,\n\t\t&state->attributes,\n\t\tstd::move(otherModels),\n\t\tstate->tab.value(),\n\t\tstate->fixed));\n\tstate->list->selected(\n\t) | rpl::on_next([=](Selection value) {\n\t\tstate->fixed = value;\n\t\tstate->paused = (value.model >= 0)\n\t\t\t|| (value.pattern >= 0)\n\t\t\t|| (value.backdrop >= 0);\n\t\tstate->push();\n\t}, box->lifetime());\n\tstate->list->modelsToggled(\n\t) | rpl::on_next([=](std::vector<Data::UniqueGiftModel> models) {\n\t\tstate->attributes.models = std::move(models);\n\t\tstate->models.clear();\n\t}, box->lifetime());\n\n\tconst auto button = box->addButton(rpl::single(u\"\"_q), [] {});\n\tconst auto buttonsParent = button->parentWidget();\n\tbox->clearButtons();\n\n\tconst auto add = [&](\n\t\t\ttr::phrase<> text,\n\t\t\tconst style::RoundButton &st,\n\t\t\tconst style::icon &active,\n\t\t\tTab tab) {\n\t\tauto owned = object_ptr<RoundButton>(\n\t\t\tbuttonsParent,\n\t\t\ttext(),\n\t\t\tst);\n\t\tconst auto raw = owned.data();\n\n\t\traw->setClickedCallback([=] {\n\t\t\tstate->tab = tab;\n\t\t});\n\t\tconst auto icon = &active;\n\t\tstate->tab.value() | rpl::on_next([=](Tab now) {\n\t\t\traw->setTextFgOverride((now == tab)\n\t\t\t\t? st::defaultActiveButton.textFg->c\n\t\t\t\t: std::optional<QColor>());\n\t\t\traw->setBrushOverride((now == tab)\n\t\t\t\t? st::defaultActiveButton.textBg->c\n\t\t\t\t: std::optional<QColor>());\n\t\t\traw->setIconOverride((now == tab) ? icon : nullptr);\n\t\t}, raw->lifetime());\n\t\tbox->addButton(std::move(owned));\n\t};\n\tadd(\n\t\ttr::lng_auction_preview_symbols_button,\n\t\tst::uniqueAttributeSymbol,\n\t\tst::uniqueAttributeSymbolActive,\n\t\tTab::Pattern);\n\tadd(\n\t\ttr::lng_auction_preview_backdrops_button,\n\t\tst::uniqueAttributeBackdrop,\n\t\tst::uniqueAttributeBackdropActive,\n\t\tTab::Backdrop);\n\tadd(\n\t\ttr::lng_auction_preview_models_button,\n\t\tst::uniqueAttributeModel,\n\t\tst::uniqueAttributeModelActive,\n\t\tTab::Model);\n\n\tstate->paused.value() | rpl::on_next([=](bool paused) {\n\t\tif (paused) {\n\t\t\tstate->pushNextTimer.cancel();\n\t\t} else {\n\t\t\tstate->pushNextTimer.callEach(kSwitchTimeout);\n\t\t}\n\t}, box->lifetime());\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/star_gift_preview_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nstruct StarGift;\nstruct UniqueGift;\nstruct UniqueGiftAttributes;\nenum class GiftAttributeIdType;\n} // namespace Data\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\n\nclass GenericBox;\n\nvoid StarGiftPreviewBox(\n\tnot_null<GenericBox*> box,\n\tconst QString &title,\n\tData::UniqueGiftAttributes attributes,\n\tData::GiftAttributeIdType tab,\n\tstd::shared_ptr<Data::UniqueGift> selected);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/star_gift_resale_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/star_gift_resale_box.h\"\n\n#include \"boxes/star_gift_box.h\"\n#include \"boxes/transfer_gift_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"core/ui_integration.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_star_gift.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"info/peer_gifts/info_peer_gifts_common.h\"\n#include \"main/main_session.h\"\n#include \"menu/gift_resale_filter.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kFiltersCount = 4;\nconstexpr auto kResaleBoughtToastDuration = 4 * crl::time(1000);\n\nusing Data::GiftAttributeId;\nusing Data::GiftAttributeIdType;\nusing Data::ResaleGiftsSort;\nusing Data::ResaleGiftsFilter;\nusing Data::ResaleGiftsDescriptor;\n//using Data::MyGiftsDescriptor;\n\n[[nodiscard]] Text::String ResaleTabText(QString text) {\n\tauto result = Text::String();\n\tresult.setMarkedText(\n\t\tst::semiboldTextStyle,\n\t\tTextWithEntities{ text }.append(st::giftBoxResaleTabsDropdown),\n\t\tkMarkupTextOptions);\n\treturn result;\n}\n\n[[nodiscard]] Text::String SortModeText(ResaleGiftsSort mode) {\n\tauto text = [&] {\n\t\tif (mode == ResaleGiftsSort::Number) {\n\t\t\treturn Ui::Text::IconEmoji(&st::giftBoxResaleMiniNumber).append(\n\t\t\t\ttr::lng_gift_resale_number(tr::now));\n\t\t} else if (mode == ResaleGiftsSort::Price) {\n\t\t\treturn Ui::Text::IconEmoji(&st::giftBoxResaleMiniPrice).append(\n\t\t\t\ttr::lng_gift_resale_price(tr::now));\n\t\t}\n\t\treturn Ui::Text::IconEmoji(&st::giftBoxResaleMiniDate).append(\n\t\t\ttr::lng_gift_resale_date(tr::now));\n\t}();\n\tauto result = Text::String();\n\tresult.setMarkedText(\n\t\tst::semiboldTextStyle,\n\t\ttext,\n\t\tkMarkupTextOptions);\n\treturn result;\n}\n\nstruct ResaleTabs {\n\trpl::producer<ResaleGiftsFilter> filter;\n\tobject_ptr<RpWidget> widget;\n};\n[[nodiscard]] ResaleTabs MakeResaleTabs(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst ResaleGiftsDescriptor &info,\n\t\trpl::producer<ResaleGiftsFilter> filter) {\n\tauto widget = object_ptr<RpWidget>((QWidget*)nullptr);\n\tconst auto raw = widget.data();\n\n\tstruct Button {\n\t\tQRect geometry;\n\t\tText::String text;\n\t};\n\tstruct State {\n\t\trpl::variable<ResaleGiftsFilter> filter;\n\t\trpl::variable<int> fullWidth;\n\t\tstd::vector<Button> buttons;\n\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t\tResaleGiftsDescriptor lists;\n\t\tint dragx = 0;\n\t\tint pressx = 0;\n\t\tfloat64 dragscroll = 0.;\n\t\tfloat64 scroll = 0.;\n\t\tint scrollMax = 0;\n\t\tint selected = -1;\n\t\tint pressed = -1;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>();\n\tstate->filter = std::move(filter);\n\n\tconst auto forCraft = state->filter.current().forCraft;\n\tconst auto wrapName = [=](QString name, int count) {\n\t\tauto result = tr::marked(name);\n\t\tif (!forCraft) {\n\t\t\tresult.append(' ').append(tr::bold(\n\t\t\t\tLang::FormatCountDecimal(count)));\n\t\t}\n\t\treturn result;\n\t};\n\n\tstate->lists.backdrops = info.backdrops;\n\tstate->lists.models = info.models;\n\tstate->lists.patterns = info.patterns;\n\tif (forCraft) {\n\t\tusing namespace Data;\n\t\tconst auto isRare = [](const UniqueGiftModelCount &entry) {\n\t\t\treturn entry.model.rarityType() != UniqueGiftRarity::Default;\n\t\t};\n\t\tstate->lists.models.erase(\n\t\t\tranges::remove_if(state->lists.models, isRare),\n\t\t\tend(state->lists.models));\n\t}\n\n\tconst auto scroll = [=] {\n\t\treturn QPoint(int(base::SafeRound(state->scroll)), 0);\n\t};\n\n\tstatic constexpr auto IndexToType = [](int index) {\n\t\tExpects(index > 0 && index < 4);\n\n\t\treturn (index == 1)\n\t\t\t? GiftAttributeIdType::Model\n\t\t\t: (index == 2)\n\t\t\t? GiftAttributeIdType::Backdrop\n\t\t\t: GiftAttributeIdType::Pattern;\n\t};\n\n\tconst auto setSelected = [=](int index) {\n\t\tconst auto was = (state->selected >= 0);\n\t\tconst auto now = (index >= 0);\n\t\tstate->selected = index;\n\t\tif (was != now) {\n\t\t\traw->setCursor(now ? style::cur_pointer : style::cur_default);\n\t\t}\n\t};\n\tconst auto showMenu = [=](int index) {\n\t\tif (state->menu) {\n\t\t\treturn;\n\t\t}\n\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\traw,\n\t\t\tst::giftBoxResaleFilter);\n\t\tconst auto menu = state->menu.get();\n\t\tconst auto modify = [=](Fn<void(ResaleGiftsFilter&)> modifier) {\n\t\t\tauto now = state->filter.current();\n\t\t\tmodifier(now);\n\t\t\tstate->filter = now;\n\t\t};\n\t\tconst auto actionWithIcon = [=](\n\t\t\t\tQString text,\n\t\t\t\tFn<void()> callback,\n\t\t\t\tnot_null<const style::icon*> icon,\n\t\t\t\tbool checked = false) {\n\t\t\tauto action = base::make_unique_q<Ui::GiftResaleFilterAction>(\n\t\t\t\tmenu->menu(),\n\t\t\t\tmenu->st().menu,\n\t\t\t\tTextWithEntities{ text },\n\t\t\t\tUi::Text::MarkedContext(),\n\t\t\t\tQString(),\n\t\t\t\ticon);\n\t\t\taction->setChecked(checked);\n\t\t\taction->setActionTriggered(std::move(callback));\n\t\t\tmenu->addAction(std::move(action));\n\t\t};\n\t\tauto context = Core::TextContext({ .session = &show->session() });\n\t\tcontext.customEmojiFactory = [original = context.customEmojiFactory](\n\t\t\t\tQStringView data,\n\t\t\t\tconst Ui::Text::MarkedContext &context) {\n\t\t\treturn Ui::GiftResaleColorEmoji::Owns(data)\n\t\t\t\t? std::make_unique<Ui::GiftResaleColorEmoji>(data)\n\t\t\t\t: original(data, context);\n\t\t};\n\t\tconst auto actionWithEmoji = [=](\n\t\t\t\tTextWithEntities text,\n\t\t\t\tFn<void()> callback,\n\t\t\t\tQString data,\n\t\t\t\tbool checked) {\n\t\t\tauto action = base::make_unique_q<Ui::GiftResaleFilterAction>(\n\t\t\t\tmenu->menu(),\n\t\t\t\tmenu->st().menu,\n\t\t\t\tstd::move(text),\n\t\t\t\tcontext,\n\t\t\t\tdata,\n\t\t\t\tnullptr);\n\t\t\taction->setChecked(checked);\n\t\t\taction->setActionTriggered(std::move(callback));\n\t\t\tmenu->addAction(std::move(action));\n\t\t};\n\t\tconst auto actionWithDocument = [=](\n\t\t\t\tTextWithEntities text,\n\t\t\t\tFn<void()> callback,\n\t\t\t\tDocumentId id,\n\t\t\t\tbool checked) {\n\t\t\tactionWithEmoji(\n\t\t\t\tstd::move(text),\n\t\t\t\tstd::move(callback),\n\t\t\t\tData::SerializeCustomEmojiId(id),\n\t\t\t\tchecked);\n\t\t};\n\t\tconst auto actionWithColor = [=](\n\t\t\t\tTextWithEntities text,\n\t\t\t\tFn<void()> callback,\n\t\t\t\tconst QColor &color,\n\t\t\t\tbool checked) {\n\t\t\tactionWithEmoji(\n\t\t\t\tstd::move(text),\n\t\t\t\tstd::move(callback),\n\t\t\t\tUi::GiftResaleColorEmoji::DataFor(color),\n\t\t\t\tchecked);\n\t\t};\n\t\tif (!index) {\n\t\t\tconst auto sort = [=](ResaleGiftsSort value) {\n\t\t\t\tmodify([&](ResaleGiftsFilter &filter) {\n\t\t\t\t\tfilter.sort = value;\n\t\t\t\t});\n\t\t\t};\n\t\t\tconst auto is = [&](ResaleGiftsSort value) {\n\t\t\t\treturn state->filter.current().sort == value;\n\t\t\t};\n\t\t\tactionWithIcon(tr::lng_gift_resale_sort_price(tr::now), [=] {\n\t\t\t\tsort(ResaleGiftsSort::Price);\n\t\t\t}, &st::menuIconOrderPrice, is(ResaleGiftsSort::Price));\n\t\t\tactionWithIcon(tr::lng_gift_resale_sort_date(tr::now), [=] {\n\t\t\t\tsort(ResaleGiftsSort::Date);\n\t\t\t}, &st::menuIconOrderDate, is(ResaleGiftsSort::Date));\n\t\t\tactionWithIcon(tr::lng_gift_resale_sort_number(tr::now), [=] {\n\t\t\t\tsort(ResaleGiftsSort::Number);\n\t\t\t}, &st::menuIconOrderNumber, is(ResaleGiftsSort::Number));\n\t\t\tmenu->addSeparator();\n\t\t\tconst auto starsOnly = state->filter.current().starsOnly;\n\t\t\tactionWithIcon(tr::lng_gift_resale_all_listings(tr::now), [=] {\n\t\t\t\tmodify([&](ResaleGiftsFilter &filter) {\n\t\t\t\t\tfilter.starsOnly = false;\n\t\t\t\t});\n\t\t\t}, &st::menuIconTopics, !starsOnly);\n\t\t\tactionWithIcon(tr::lng_gift_resale_stars_only(tr::now), [=] {\n\t\t\t\tmodify([&](ResaleGiftsFilter &filter) {\n\t\t\t\t\tfilter.starsOnly = true;\n\t\t\t\t});\n\t\t\t}, &st::menuIconStar, starsOnly);\n\t\t} else {\n\t\t\tconst auto now = state->filter.current().attributes;\n\t\t\tconst auto type = IndexToType(index);\n\t\t\tconst auto has = ranges::contains(\n\t\t\t\tnow,\n\t\t\t\ttype,\n\t\t\t\t&GiftAttributeId::type);\n\t\t\tif (has) {\n\t\t\t\tactionWithIcon(tr::lng_gift_resale_filter_all(tr::now), [=] {\n\t\t\t\t\tmodify([&](ResaleGiftsFilter &filter) {\n\t\t\t\t\t\tauto &list = filter.attributes;\n\t\t\t\t\t\tfor (auto i = begin(list); i != end(list);) {\n\t\t\t\t\t\t\tif (i->type == type) {\n\t\t\t\t\t\t\t\ti = list.erase(i);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t++i;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}, &st::menuIconSelect);\n\t\t\t}\n\t\t\tconst auto toggle = [=](GiftAttributeId id) {\n\t\t\t\tmodify([&](ResaleGiftsFilter &filter) {\n\t\t\t\t\tauto &list = filter.attributes;\n\t\t\t\t\tif (ranges::contains(list, id)) {\n\t\t\t\t\t\tlist.remove(id);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlist.emplace(id);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t};\n\t\t\tconst auto checked = [=](GiftAttributeId id) {\n\t\t\t\treturn !has || ranges::contains(now, id);\n\t\t\t};\n\t\t\tif (type == GiftAttributeIdType::Model) {\n\t\t\t\tfor (auto &entry : state->lists.models) {\n\t\t\t\t\tconst auto id = IdFor(entry.model);\n\t\t\t\t\tconst auto text = wrapName(\n\t\t\t\t\t\tentry.model.name,\n\t\t\t\t\t\tentry.count);\n\t\t\t\t\tactionWithDocument(text, [=] {\n\t\t\t\t\t\ttoggle(id);\n\t\t\t\t\t}, id.value, checked(id));\n\t\t\t\t}\n\t\t\t} else if (type == GiftAttributeIdType::Backdrop) {\n\t\t\t\tfor (auto &entry : state->lists.backdrops) {\n\t\t\t\t\tconst auto id = IdFor(entry.backdrop);\n\t\t\t\t\tconst auto text = wrapName(\n\t\t\t\t\t\tentry.backdrop.name,\n\t\t\t\t\t\tentry.count);\n\t\t\t\t\tactionWithColor(text, [=] {\n\t\t\t\t\t\ttoggle(id);\n\t\t\t\t\t}, entry.backdrop.centerColor, checked(id));\n\t\t\t\t}\n\t\t\t} else if (type == GiftAttributeIdType::Pattern) {\n\t\t\t\tfor (auto &entry : state->lists.patterns) {\n\t\t\t\t\tconst auto id = IdFor(entry.pattern);\n\t\t\t\t\tconst auto text = wrapName(\n\t\t\t\t\t\tentry.pattern.name,\n\t\t\t\t\t\tentry.count);\n\t\t\t\t\tactionWithDocument(text, [=] {\n\t\t\t\t\t\ttoggle(id);\n\t\t\t\t\t}, id.value, checked(id));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tmenu->popup(QCursor::pos());\n\t};\n\n\tstate->filter.value(\n\t) | rpl::on_next([=](const ResaleGiftsFilter &fields) {\n\t\tauto x = st::giftBoxResaleTabsMargin.left();\n\t\tauto y = st::giftBoxResaleTabsMargin.top();\n\n\t\tsetSelected(-1);\n\t\tstate->buttons.resize(kFiltersCount);\n\t\tconst auto &list = fields.attributes;\n\t\tconst auto setForIndex = [&](int i, auto many, auto one) {\n\t\t\tconst auto type = IndexToType(i);\n\t\t\tconst auto count = ranges::count(\n\t\t\t\tlist,\n\t\t\t\ttype,\n\t\t\t\t&GiftAttributeId::type);\n\t\t\tstate->buttons[i].text = ResaleTabText((count > 0)\n\t\t\t\t? many(tr::now, lt_count, count)\n\t\t\t\t: one(tr::now));\n\t\t};\n\t\tstate->buttons[0].text = SortModeText(fields.sort);\n\t\tsetForIndex(\n\t\t\t1,\n\t\t\ttr::lng_gift_resale_models,\n\t\t\ttr::lng_gift_resale_model);\n\t\tsetForIndex(\n\t\t\t2,\n\t\t\ttr::lng_gift_resale_backdrops,\n\t\t\ttr::lng_gift_resale_backdrop);\n\t\tsetForIndex(\n\t\t\t3,\n\t\t\ttr::lng_gift_resale_symbols,\n\t\t\ttr::lng_gift_resale_symbol);\n\n\t\tconst auto padding = st::giftBoxTabPadding;\n\t\tfor (auto &button : state->buttons) {\n\t\t\tconst auto width = button.text.maxWidth();\n\t\t\tconst auto height = st::giftBoxTabStyle.font->height;\n\t\t\tconst auto r = QRect(0, 0, width, height).marginsAdded(padding);\n\t\t\tbutton.geometry = QRect(QPoint(x, y), r.size());\n\t\t\tx += r.width() + st::giftBoxResaleTabSkip;\n\t\t}\n\t\tstate->fullWidth = x\n\t\t\t- st::giftBoxTabSkip\n\t\t\t+ st::giftBoxTabsMargin.right();\n\t\tconst auto height = state->buttons.empty()\n\t\t\t? 0\n\t\t\t: (y\n\t\t\t\t+ state->buttons.back().geometry.height()\n\t\t\t\t+ st::giftBoxTabsMargin.bottom());\n\t\traw->resize(raw->width(), height);\n\t\traw->update();\n\t}, raw->lifetime());\n\n\trpl::combine(\n\t\traw->widthValue(),\n\t\tstate->fullWidth.value()\n\t) | rpl::on_next([=](int outer, int inner) {\n\t\tstate->scrollMax = std::max(0, inner - outer);\n\t}, raw->lifetime());\n\n\traw->setMouseTracking(true);\n\traw->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tswitch (type) {\n\t\tcase QEvent::Leave: setSelected(-1); break;\n\t\tcase QEvent::MouseMove: {\n\t\t\tconst auto me = static_cast<QMouseEvent*>(e.get());\n\t\t\tconst auto mousex = me->pos().x();\n\t\t\tconst auto drag = QApplication::startDragDistance();\n\t\t\tif (state->dragx > 0) {\n\t\t\t\tstate->scroll = std::clamp(\n\t\t\t\t\tstate->dragscroll + state->dragx - mousex,\n\t\t\t\t\t0.,\n\t\t\t\t\tstate->scrollMax * 1.);\n\t\t\t\traw->update();\n\t\t\t\tbreak;\n\t\t\t} else if (state->pressx > 0\n\t\t\t\t&& std::abs(state->pressx - mousex) > drag) {\n\t\t\t\tstate->dragx = state->pressx;\n\t\t\t\tstate->dragscroll = state->scroll;\n\t\t\t}\n\t\t\tconst auto position = me->pos() + scroll();\n\t\t\tfor (auto i = 0, c = int(state->buttons.size()); i != c; ++i) {\n\t\t\t\tif (state->buttons[i].geometry.contains(position)) {\n\t\t\t\t\tsetSelected(i);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} break;\n\t\tcase QEvent::Wheel: {\n\t\t\tconst auto me = static_cast<QWheelEvent*>(e.get());\n\t\t\tstate->scroll = std::clamp(\n\t\t\t\tstate->scroll - ScrollDeltaF(me).x(),\n\t\t\t\t0.,\n\t\t\t\tstate->scrollMax * 1.);\n\t\t\traw->update();\n\t\t} break;\n\t\tcase QEvent::MouseButtonPress: {\n\t\t\tconst auto me = static_cast<QMouseEvent*>(e.get());\n\t\t\tif (me->button() != Qt::LeftButton) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tstate->pressed = state->selected;\n\t\t\tstate->pressx = me->pos().x();\n\t\t} break;\n\t\tcase QEvent::MouseButtonRelease: {\n\t\t\tconst auto me = static_cast<QMouseEvent*>(e.get());\n\t\t\tif (me->button() != Qt::LeftButton) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst auto dragx = std::exchange(state->dragx, 0);\n\t\t\tconst auto pressed = std::exchange(state->pressed, -1);\n\t\t\tstate->pressx = 0;\n\t\t\tif (!dragx && pressed >= 0 && state->selected == pressed) {\n\t\t\t\tshowMenu(pressed);\n\t\t\t}\n\t\t} break;\n\t\t}\n\t}, raw->lifetime());\n\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto padding = st::giftBoxTabPadding;\n\t\tconst auto shift = -scroll();\n\t\tfor (const auto &button : state->buttons) {\n\t\t\tconst auto geometry = button.geometry.translated(shift);\n\n\t\t\tp.setBrush(st::giftBoxTabBgActive);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tconst auto radius = geometry.height() / 2.;\n\t\t\tp.drawRoundedRect(geometry, radius, radius);\n\t\t\tp.setPen(st::giftBoxTabFgActive);\n\n\t\t\tbutton.text.draw(p, {\n\t\t\t\t.position = geometry.marginsRemoved(padding).topLeft(),\n\t\t\t\t.availableWidth = button.text.maxWidth(),\n\t\t\t});\n\t\t}\n\t\t{\n\t\t\tconst auto &icon = st::defaultEmojiSuggestions;\n\t\t\tconst auto w = icon.fadeRight.width();\n\t\t\tconst auto &c = st::boxDividerBg->c;\n\t\t\tconst auto r = QRect(0, 0, w, raw->height());\n\t\t\tconst auto s = std::abs(float64(shift.x()));\n\t\t\tconstexpr auto kF = 0.5;\n\t\t\tconst auto opacityRight = (state->scrollMax - s)\n\t\t\t\t/ (icon.fadeRight.width() * kF);\n\t\t\tp.setOpacity(std::clamp(std::abs(opacityRight), 0., 1.));\n\t\t\ticon.fadeRight.fill(p, r.translated(raw->width() -  w, 0), c);\n\n\t\t\tconst auto opacityLeft = s / (icon.fadeLeft.width() * kF);\n\t\t\tp.setOpacity(std::clamp(std::abs(opacityLeft), 0., 1.));\n\t\t\ticon.fadeLeft.fill(p, r, c);\n\t\t}\n\t}, raw->lifetime());\n\n\treturn {\n\t\t.filter = state->filter.value(),\n\t\t.widget = std::move(widget),\n\t};\n}\n\nvoid GiftResaleBox(\n\t\tnot_null<GenericBox*> box,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerData*> peer,\n\t\tResaleGiftsDescriptor descriptor) {\n\tbox->setWidth(st::boxWideWidth);\n\n\tconst auto titleWrap = box->setPinnedToTopContent(\n\t\tobject_ptr<Ui::VerticalLayout>(box.get()));\n\n\ttitleWrap->add(object_ptr<Ui::FixedHeightWidget>(\n\t\ttitleWrap,\n\t\tst::defaultVerticalListSkip));\n\n\ttitleWrap->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\ttitleWrap,\n\t\t\trpl::single(descriptor.title),\n\t\t\tst::boxTitle),\n\t\tQMargins(st::boxRowPadding.left(), 0, st::boxRowPadding.right(), 0));\n\n\tconst auto countLabel = titleWrap->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\ttitleWrap,\n\t\t\t(descriptor.count\n\t\t\t\t? tr::lng_gift_resale_count(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tdescriptor.count)\n\t\t\t\t: tr::lng_gift_resale_count_none(tr::now)),\n\t\t\tst::defaultFlatLabel),\n\t\tQMargins(\n\t\t\tst::boxRowPadding.left(),\n\t\t\t0,\n\t\t\tst::boxRowPadding.right(),\n\t\t\tst::defaultVerticalListSkip));\n\tcountLabel->setTextColorOverride(st::windowSubTextFg->c);\n\n\tconst auto content = box->verticalLayout();\n\tcontent->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tQPainter(content).fillRect(clip, st::boxDividerBg);\n\t}, content->lifetime());\n\n\tstruct State {\n\t\trpl::variable<bool> starsOnly;\n\t\tint lastMinHeight = 0;\n\t};\n\tconst auto state = content->lifetime().make_state<State>();\n\n\tbox->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); });\n\n\tconst auto filter = box->addLeftButton(rpl::single(QString()), [=] {\n\t\tstate->starsOnly = !state->starsOnly.current();\n\t});\n\tfilter->setText(rpl::conditional(\n\t\tstate->starsOnly.value(),\n\t\ttr::lng_gift_resale_all_listings(),\n\t\ttr::lng_gift_resale_stars_only()));\n\n\tbox->heightValue() | rpl::on_next([=](int height) {\n\t\tif (height > state->lastMinHeight) {\n\t\t\tstate->lastMinHeight = height;\n\t\t\tbox->setMinHeight(height);\n\t\t}\n\t}, content->lifetime());\n\n\tAddResaleGiftsList(\n\t\twindow,\n\t\tpeer,\n\t\tcontent,\n\t\tstd::move(descriptor),\n\t\t&state->starsOnly,\n\t\tnullptr,\n\t\tfalse,\n\t\t[=](int count) {\n\t\t\tcountLabel->setText(count\n\t\t\t\t? tr::lng_gift_resale_count(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tcount)\n\t\t\t\t: tr::lng_gift_resale_count_none(tr::now));\n\t\t});\n}\n\n} // namespace\n\nvoid AddResaleGiftsList(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<VerticalLayout*> container,\n\t\tData::ResaleGiftsDescriptor descriptor,\n\t\trpl::variable<bool> *starsOnly,\n\t\tFn<void(std::shared_ptr<Data::UniqueGift>)> bought,\n\t\tbool forCraft,\n\t\tFn<void(int)> countChanged) {\n\tstruct State {\n\t\trpl::event_stream<> updated;\n\t\tResaleGiftsDescriptor data;\n\t\trpl::variable<ResaleGiftsFilter> filter;\n\t\trpl::variable<bool> empty = true;\n\t\trpl::lifetime loading;\n\t\tint lastMinHeight = 0;\n\t};\n\tconst auto state = container->lifetime().make_state<State>();\n\tstate->filter = ResaleGiftsFilter{ .forCraft = forCraft };\n\tstate->data = std::move(descriptor);\n\n\tauto tabs = MakeResaleTabs(\n\t\twindow->uiShow(),\n\t\tstate->data,\n\t\tstate->filter.value());\n\tstate->filter = std::move(tabs.filter);\n\tif (starsOnly) {\n\t\tstarsOnly->changes(\n\t\t) | rpl::on_next([=](bool value) {\n\t\t\tauto f = state->filter.current();\n\t\t\tif (f.starsOnly != value) {\n\t\t\t\tf.starsOnly = value;\n\t\t\t\tstate->filter = f;\n\t\t\t}\n\t\t}, container->lifetime());\n\n\t\tstate->filter.changes(\n\t\t) | rpl::on_next([=](const ResaleGiftsFilter &f) {\n\t\t\tif (starsOnly->current() != f.starsOnly) {\n\t\t\t\t*starsOnly = f.starsOnly;\n\t\t\t}\n\t\t}, container->lifetime());\n\n\t\tif (starsOnly->current()) {\n\t\t\tauto f = state->filter.current();\n\t\t\tf.starsOnly = true;\n\t\t\tstate->filter = f;\n\t\t}\n\t}\n\tif (forCraft) {\n\t\tconst auto skip = st::giftBoxResaleTabsMargin.top()\n\t\t\t- st::giftBoxTabsMargin.bottom();\n\t\tconst auto wrap = container->add(object_ptr<PaddingWrap<>>(\n\t\t\tcontainer,\n\t\t\tstd::move(tabs.widget),\n\t\t\tQMargins(0, 0, 0, skip)));\n\t\twrap->paintOn([=](QPainter &p) {\n\t\t\tp.fillRect(wrap->rect(), st::windowBgOver);\n\t\t});\n\t} else {\n\t\tcontainer->add(std::move(tabs.widget));\n\t}\n\n\tconst auto session = &window->session();\n\tstate->filter.changes() | rpl::on_next([=](ResaleGiftsFilter value) {\n\t\tstate->data.offset = QString();\n\t\tstate->loading = ResaleGiftsSlice(\n\t\t\tsession,\n\t\t\tstate->data.giftId,\n\t\t\tvalue,\n\t\t\tQString()\n\t\t) | rpl::on_next([=](ResaleGiftsDescriptor &&slice) {\n\t\t\tstate->loading.destroy();\n\t\t\tstate->data.offset = slice.list.empty()\n\t\t\t\t? QString()\n\t\t\t\t: slice.offset;\n\t\t\tstate->data.count = slice.count;\n\t\t\tstate->data.list = std::move(slice.list);\n\t\t\tif (countChanged) {\n\t\t\t\tcountChanged(state->data.count);\n\t\t\t}\n\t\t\tstate->updated.fire({});\n\t\t});\n\t}, container->lifetime());\n\n\tsession->data().giftUpdates(\n\t) | rpl::on_next([=](const Data::GiftUpdate &update) {\n\t\tusing Action = Data::GiftUpdate::Action;\n\t\tconst auto action = update.action;\n\t\tif (action != Action::Transfer && action != Action::ResaleChange) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto i = ranges::find(\n\t\t\tstate->data.list,\n\t\t\tupdate.slug,\n\t\t\t[](const Data::StarGift &gift) {\n\t\t\t\treturn gift.unique ? gift.unique->slug : QString();\n\t\t\t});\n\t\tif (i == end(state->data.list)) {\n\t\t\treturn;\n\t\t} else if (action == Action::Transfer\n\t\t\t|| !i->unique->starsForResale) {\n\t\t\tstate->data.list.erase(i);\n\t\t}\n\t\tstate->updated.fire({});\n\t}, container->lifetime());\n\n\tusing Descriptor = Info::PeerGifts::GiftDescriptor;\n\tauto customHandler = Fn<void(Descriptor)>();\n\tif (bought) {\n\t\tusing StarGift = Info::PeerGifts::GiftTypeStars;\n\t\tcustomHandler = crl::guard(container, [=](Descriptor descriptor) {\n\t\t\tExpects(v::is<StarGift>(descriptor));\n\n\t\t\tconst auto unique = v::get<StarGift>(descriptor).info.unique;\n\t\t\tconst auto done = crl::guard(container, [=](bool ok) {\n\t\t\t\tif (ok) {\n\t\t\t\t\tbought(unique);\n\t\t\t\t}\n\t\t\t});\n\t\t\tconst auto to = peer->session().user();\n\t\t\tShowBuyResaleGiftBox(window->uiShow(), unique, false, to, done);\n\t\t});\n\t}\n\n\tauto gifts = rpl::single(\n\t\trpl::empty\n\t) | rpl::then(\n\t\tstate->updated.events() | rpl::type_erased\n\t) | rpl::map([=] {\n\t\tauto result = GiftsDescriptor();\n\t\tconst auto selfId = window->session().userPeerId();\n\t\tfor (const auto &gift : state->data.list) {\n\t\t\tconst auto mine = (gift.unique->ownerId == selfId);\n\t\t\tif (mine && forCraft) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tresult.list.push_back(Info::PeerGifts::GiftTypeStars{\n\t\t\t\t.info = gift,\n\t\t\t\t.resale = true,\n\t\t\t\t.mine = mine,\n\t\t\t\t});\n\t\t}\n\t\tstate->empty = result.list.empty();\n\t\treturn result;\n\t});\n\tconst auto loadMore = [=] {\n\t\tif (!state->data.offset.isEmpty()\n\t\t\t&& !state->loading) {\n\t\t\tstate->loading = ResaleGiftsSlice(\n\t\t\t\t&peer->session(),\n\t\t\t\tstate->data.giftId,\n\t\t\t\tstate->filter.current(),\n\t\t\t\tstate->data.offset\n\t\t\t) | rpl::on_next([=](ResaleGiftsDescriptor &&slice) {\n\t\t\t\tstate->loading.destroy();\n\t\t\t\tstate->data.offset = slice.list.empty()\n\t\t\t\t\t? QString()\n\t\t\t\t\t: slice.offset;\n\t\t\t\tstate->data.list.insert(\n\t\t\t\t\tend(state->data.list),\n\t\t\t\t\tstd::make_move_iterator(begin(slice.list)),\n\t\t\t\t\tstd::make_move_iterator(end(slice.list)));\n\t\t\t\tstate->updated.fire({});\n\t\t\t});\n\t\t}\n\t};\n\tcontainer->add(MakeGiftsList({\n\t\t.window = window,\n\t\t.mode = forCraft ? GiftsListMode::CraftResale : GiftsListMode::Send,\n\t\t.peer = peer,\n\t\t.gifts = std::move(gifts),\n\t\t.loadMore = loadMore,\n\t\t.handler = customHandler,\n\t}));\n\n\tconst auto skip = st::defaultSubsectionTitlePadding.top();\n\tconst auto wrap = container->add(\n\t\tobject_ptr<SlideWrap<FlatLabel>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\t(forCraft\n\t\t\t\t\t? tr::lng_gift_craft_search_none()\n\t\t\t\t\t: tr::lng_gift_resale_search_none()),\n\t\t\t\tst::craftYourListEmpty),\n\t\t\t(st::boxRowPadding + QMargins(0, 0, 0, skip))),\n\t\tstyle::al_top);\n\tstate->empty.value() | rpl::on_next([=](bool empty) {\n\t\t// Scroll doesn't jump up if we show before rows are cleared,\n\t\t// and we hide after rows are added.\n\t\tif (empty) {\n\t\t\twrap->show(anim::type::instant);\n\t\t} else {\n\t\t\tcrl::on_main(wrap, [=] {\n\t\t\t\tif (!state->empty.current()) {\n\t\t\t\t\twrap->hide(anim::type::instant);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}, wrap->lifetime());\n\twrap->entity()->setTryMakeSimilarLines(true);\n}\n\nvoid ShowResaleGiftBoughtToast(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<PeerData*> to,\n\t\tconst Data::UniqueGift &gift) {\n\tshow->showToast({\n\t\t.title = to->isSelf() ? QString() : tr::lng_gift_sent_title(tr::now),\n\t\t.text = TextWithEntities{ (to->isSelf()\n\t\t\t? tr::lng_gift_sent_resale_done_self(\n\t\t\t\ttr::now,\n\t\t\t\tlt_gift,\n\t\t\t\tData::UniqueGiftName(gift))\n\t\t\t: tr::lng_gift_sent_resale_done(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tto->shortName())),\n\t\t},\n\t\t.duration = kResaleBoughtToastDuration,\n\t});\n}\n\nrpl::lifetime ShowStarGiftResale(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tuint64 giftId,\n\t\tQString title,\n\t\tFn<void()> finishRequesting) {\n\tconst auto weak = base::make_weak(controller);\n\tconst auto session = &controller->session();\n\treturn Data::ResaleGiftsSlice(\n\t\tsession,\n\t\tgiftId\n\t) | rpl::on_next([=](ResaleGiftsDescriptor &&info) {\n\t\tif (const auto onstack = finishRequesting) {\n\t\t\tonstack();\n\t\t}\n\t\tif (!info.giftId || !info.count) {\n\t\t\treturn;\n\t\t}\n\t\tinfo.title = title;\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->show(Box(GiftResaleBox, strong, peer, std::move(info)));\n\t\t}\n\t});\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/star_gift_resale_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nstruct UniqueGift;\nstruct ResaleGiftsDescriptor;\n} // namespace Data\n\nnamespace Info::PeerGifts {\nstruct GiftTypeStars;\n} // namespace Info::PeerGifts\n\nnamespace Main {\nclass Session;\nclass SessionShow;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\n\nclass VerticalLayout;\n\nvoid ShowResaleGiftBoughtToast(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tnot_null<PeerData*> to,\n\tconst Data::UniqueGift &gift);\n\n[[nodiscard]] rpl::lifetime ShowStarGiftResale(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\tuint64 giftId,\n\tQString title,\n\tFn<void()> finishRequesting);\n\nvoid AddResaleGiftsList(\n\tnot_null<Window::SessionController*> window,\n\tnot_null<PeerData*> peer,\n\tnot_null<VerticalLayout*> container,\n\tData::ResaleGiftsDescriptor descriptor,\n\trpl::variable<bool> *starsOnly = nullptr,\n\tFn<void(std::shared_ptr<Data::UniqueGift>)> bought = nullptr,\n\tbool forCraft = false,\n\tFn<void(int)> countChanged = nullptr);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/sticker_set_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/sticker_set_box.h\"\n\n#include \"api/api_common.h\"\n#include \"api/api_toggling_media.h\"\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"chat_helpers/stickers_list_widget.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"core/application.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"info/channel_statistics/boosts/giveaway/boost_badge.h\" // InfiniteRadialAnimationWidget.\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_animation.h\"\n#include \"lottie/lottie_multi_player.h\"\n#include \"main/main_session.h\"\n#include \"mainwindow.h\"\n#include \"media/clip/media_clip_reader.h\"\n#include \"menu/menu_send.h\"\n#include \"mtproto/sender.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"storage/storage_account.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/effects/animation_value_f.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/image/image.h\"\n#include \"ui/image/image_location_factory.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/custom_emoji_instance.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/gradient_round_button.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_premium.h\"\n\n#include <QtWidgets/QApplication>\n#include <QtGui/QClipboard>\n#include <QtSvg/QSvgRenderer>\n\nnamespace {\n\nconstexpr auto kStickersPerRow = 5;\nconstexpr auto kEmojiPerRow = 8;\nconstexpr auto kMinRepaintDelay = crl::time(33);\nconstexpr auto kMinAfterScrollDelay = crl::time(33);\nconstexpr auto kGrayLockOpacity = 0.3;\nconstexpr auto kStickerMoveDuration = crl::time(200);\n\nusing Data::StickersSet;\nusing Data::StickersPack;\nusing SetFlag = Data::StickersSetFlag;\nusing TLStickerSet = MTPmessages_StickerSet;\n\n[[nodiscard]] std::optional<QColor> ComputeImageColor(\n\t\tconst style::icon &lockIcon,\n\t\tconst QImage &frame,\n\t\tRectPart part) {\n\tif (frame.isNull()\n\t\t|| frame.format() != QImage::Format_ARGB32_Premultiplied) {\n\t\treturn {};\n\t}\n\tauto sr = int64();\n\tauto sg = int64();\n\tauto sb = int64();\n\tauto sa = int64();\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto size = lockIcon.size() * factor;\n\tconst auto width = std::min(frame.width(), size.width());\n\tconst auto height = std::min(frame.height(), size.height());\n\tconst auto radius = st::roundRadiusSmall;\n\tconst auto skipx = (part == RectPart::TopLeft\n\t\t|| part == RectPart::Left\n\t\t|| part == RectPart::BottomLeft)\n\t\t? 0\n\t\t: (part == RectPart::Top\n\t\t\t|| part == RectPart::Center\n\t\t\t|| part == RectPart::Bottom)\n\t\t? (frame.width() - width) / 2\n\t\t: std::max(frame.width() - width - radius, 0);\n\tconst auto skipy = (part == RectPart::TopLeft\n\t\t|| part == RectPart::Top\n\t\t|| part == RectPart::TopRight)\n\t\t? 0\n\t\t: (part == RectPart::Left\n\t\t\t|| part == RectPart::Center\n\t\t\t|| part == RectPart::Right)\n\t\t? (frame.height() - height) / 2\n\t\t: std::max(frame.height() - height - radius, 0);\n\tconst auto perline = frame.bytesPerLine();\n\tconst auto addperline = perline - (width * 4);\n\tauto bits = static_cast<const uchar*>(frame.bits())\n\t\t+ perline * skipy\n\t\t+ sizeof(uint32) * skipx;\n\tfor (auto y = 0; y != height; ++y) {\n\t\tfor (auto x = 0; x != width; ++x) {\n\t\t\tsb += int(*bits++);\n\t\t\tsg += int(*bits++);\n\t\t\tsr += int(*bits++);\n\t\t\tsa += int(*bits++);\n\t\t}\n\t\tbits += addperline;\n\t}\n\tif (!sa) {\n\t\treturn {};\n\t}\n\treturn QColor(sr * 255 / sa, sg * 255 / sa, sb * 255 / sa, 255);\n\n}\n\n[[nodiscard]] QColor ComputeLockColor(\n\t\tconst style::icon &lockIcon,\n\t\tconst QImage &frame,\n\t\tRectPart part) {\n\treturn ComputeImageColor(\n\t\tlockIcon,\n\t\tframe,\n\t\tpart\n\t).value_or(st::windowSubTextFg->c);\n}\n\nvoid ValidatePremiumLockBg(\n\t\tconst style::icon &lockIcon,\n\t\tQImage &image,\n\t\tconst QImage &frame,\n\t\tRectPart part) {\n\tif (!image.isNull()) {\n\t\treturn;\n\t}\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto size = lockIcon.size();\n\timage = QImage(\n\t\tsize * factor,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(factor);\n\tauto p = QPainter(&image);\n\tconst auto color = ComputeLockColor(lockIcon, frame, part);\n\tp.fillRect(\n\t\tQRect(QPoint(), size),\n\t\tanim::color(color, st::windowSubTextFg, kGrayLockOpacity));\n\tp.end();\n\n\timage = Images::Circle(std::move(image));\n}\n\nvoid ValidatePremiumStarFg(const style::icon &lockIcon, QImage &image) {\n\tif (!image.isNull()) {\n\t\treturn;\n\t}\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto size = lockIcon.size();\n\timage = QImage(\n\t\tsize * factor,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(factor);\n\timage.fill(Qt::transparent);\n\tauto p = QPainter(&image);\n\tauto star = QSvgRenderer(u\":/gui/icons/settings/star.svg\"_q);\n\tconst auto skip = size.width() / 5.;\n\tconst auto outer = QRectF(QPointF(), size).marginsRemoved(\n\t\t{ skip, skip, skip, skip });\n\tp.setBrush(st::premiumButtonFg);\n\tp.setPen(Qt::NoPen);\n\tstar.render(&p, outer);\n}\n\n[[nodiscard]] TextForMimeData PrepareTextFromEmoji(\n\t\tnot_null<DocumentData*> document) {\n\tconst auto info = document->sticker();\n\tconst auto text = info ? info->alt : QString();\n\treturn {\n\t\t.expanded = text,\n\t\t.rich = {\n\t\t\ttext,\n\t\t\t{\n\t\t\t\tEntityInText(\n\t\t\t\t\tEntityType::CustomEmoji,\n\t\t\t\t\t0,\n\t\t\t\t\ttext.size(),\n\t\t\t\t\tData::SerializeCustomEmojiId(document))\n\t\t\t},\n\t\t},\n\t};\n}\n\n} // namespace\n\nStickerPremiumMark::StickerPremiumMark(\n\tnot_null<Main::Session*> session,\n\tconst style::icon &lockIcon,\n\tRectPart part)\n: _lockIcon(lockIcon)\n, _part(part) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_lockGray = QImage();\n\t\t_star = QImage();\n\t}, _lifetime);\n\n\tData::AmPremiumValue(\n\t\tsession\n\t) | rpl::on_next([=](bool premium) {\n\t\t_premium = premium;\n\t}, _lifetime);\n}\n\nvoid StickerPremiumMark::paint(\n\t\tQPainter &p,\n\t\tconst QImage &frame,\n\t\tQImage &backCache,\n\t\tQPoint position,\n\t\tQSize singleSize,\n\t\tint outerWidth) {\n\tvalidateLock(frame, backCache);\n\tconst auto &bg = frame.isNull() ? _lockGray : backCache;\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto radius = st::roundRadiusSmall;\n\tconst auto shiftx = (_part == RectPart::Center)\n\t\t? (singleSize.width() - (bg.width() / factor)) / 2\n\t\t: (singleSize.width() - (bg.width() / factor) - radius);\n\tconst auto shifty = (_part == RectPart::Center)\n\t\t? (singleSize.height() - (bg.height() / factor)) / 2\n\t\t: (singleSize.height() - (bg.height() / factor) - radius);\n\tconst auto point = position + QPoint(shiftx, shifty);\n\tp.drawImage(point, bg);\n\tif (_premium && _part != RectPart::Center) {\n\t\tvalidateStar();\n\t\tp.drawImage(point, _star);\n\t} else {\n\t\t_lockIcon.paint(p, point, outerWidth);\n\t}\n}\n\nvoid StickerPremiumMark::validateLock(\n\t\tconst QImage &frame,\n\t\tQImage &backCache) {\n\tauto &image = frame.isNull() ? _lockGray : backCache;\n\tValidatePremiumLockBg(_lockIcon, image, frame, _part);\n}\n\nvoid StickerPremiumMark::validateStar() {\n\tValidatePremiumStarFg(_lockIcon, _star);\n}\n\nclass StickerSetBox::Inner final : public Ui::RpWidget {\npublic:\n\tInner(\n\t\tQWidget *parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst StickerSetIdentifier &set,\n\t\tData::StickersType type);\n\n\t[[nodiscard]] bool loaded() const;\n\t[[nodiscard]] bool notInstalled() const;\n\t[[nodiscard]] bool premiumEmojiSet() const;\n\t[[nodiscard]] bool official() const;\n\t[[nodiscard]] rpl::producer<TextWithEntities> title() const;\n\t[[nodiscard]] QString shortName() const;\n\t[[nodiscard]] bool isEmojiSet() const;\n\t[[nodiscard]] uint64 setId() const;\n\n\tvoid install();\n\tvoid showPreviewForDocument(DocumentId documentId);\n\t[[nodiscard]] rpl::producer<uint64> setInstalled() const;\n\t[[nodiscard]] rpl::producer<uint64> setArchived() const;\n\t[[nodiscard]] rpl::producer<> updateControls() const;\n\n\tvoid setReorderState(bool enabled) {\n\t\t_dragging.enabled = enabled;\n\t\tif (enabled) {\n\t\t\t_shakeAnimation.init([=] { update(); });\n\t\t\t_shakeAnimation.start();\n\t\t} else {\n\t\t\t_shakeAnimation.stop();\n\t\t\tupdate();\n\t\t}\n\t}\n\t[[nodiscard]] bool reorderState() const {\n\t\treturn _dragging.enabled;\n\t}\n\n\t[[nodiscard]] rpl::producer<Error> errors() const;\n\n\tvoid archiveStickers();\n\n\t[[nodiscard]] Data::StickersType setType() const {\n\t\treturn (_setFlags & SetFlag::Emoji)\n\t\t\t? Data::StickersType::Emoji\n\t\t\t: (_setFlags & SetFlag::Masks)\n\t\t\t? Data::StickersType::Masks\n\t\t\t: Data::StickersType::Stickers;\n\t}\n\n\t[[nodiscard]] bool amSetCreator() const {\n\t\treturn _amSetCreator;\n\t}\n\n\tvoid applySet(const TLStickerSet &set);\n\n\t~Inner();\n\nprotected:\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\nprivate:\n\tstruct Element {\n\t\tnot_null<DocumentData*> document;\n\t\tstd::shared_ptr<Data::DocumentMedia> documentMedia;\n\t\tLottie::Animation *lottie = nullptr;\n\t\tMedia::Clip::ReaderPointer webm;\n\t\tUi::Text::CustomEmoji *emoji = nullptr;\n\t\tUi::Animations::Simple overAnimation;\n\n\t\tmutable QImage premiumLock;\n\t};\n\n\tvoid visibleTopBottomUpdated(int visibleTop, int visibleBottom) override;\n\n\t[[nodiscard]] Ui::MessageSendingAnimationFrom messageSentAnimationInfo(\n\t\tint index,\n\t\tnot_null<DocumentData*> document) const;\n\t[[nodiscard]] QSize boundingBoxSize() const;\n\n\tvoid paintSticker(\n\t\tPainter &p,\n\t\tint index,\n\t\tQPoint position,\n\t\tbool paused,\n\t\tcrl::time now) const;\n\tvoid shakeTransform(\n\t\tQPainter &p,\n\t\tint index,\n\t\tQPoint position,\n\t\tcrl::time now) const;\n\tvoid setupLottie(int index);\n\tvoid setupWebm(int index);\n\tvoid clipCallback(\n\t\tMedia::Clip::Notification notification,\n\t\tnot_null<DocumentData*> document,\n\t\tint index);\n\tvoid setupEmoji(int index);\n\t[[nodiscard]] not_null<Ui::Text::CustomEmoji*> resolveCustomEmoji(\n\t\tnot_null<DocumentData*> document);\n\tvoid customEmojiRepaint();\n\n\tvoid updateSelected();\n\tvoid setSelected(int selected);\n\tvoid startOverAnimation(int index, float64 from, float64 to);\n\tint stickerFromGlobalPos(const QPoint &p) const;\n\n\tvoid installDone(const MTPmessages_StickerSetInstallResult &result);\n\n\tvoid requestReorder(not_null<DocumentData*> document, int index);\n\tvoid fillDeleteStickerBox(not_null<Ui::GenericBox*> box, int index);\n\n\tvoid chosen(\n\t\tint index,\n\t\tnot_null<DocumentData*> sticker,\n\t\tApi::SendOptions options);\n\n\t[[nodiscard]] QPoint posFromIndex(int index) const;\n\t[[nodiscard]] bool isDraggedAnimating() const;\n\n\tnot_null<Lottie::MultiPlayer*> getLottiePlayer();\n\n\tvoid showPreview();\n\tvoid showPreviewAt(QPoint globalPos);\n\n\tvoid updateItems();\n\tvoid repaintItems(crl::time now = 0);\n\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst not_null<Main::Session*> _session;\n\n\tMTP::Sender _api;\n\tstd::vector<Element> _elements;\n\tstd::unique_ptr<Lottie::MultiPlayer> _lottiePlayer;\n\n\tbase::flat_map<\n\t\tnot_null<DocumentData*>,\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji>> _customEmoji;\n\tbool _repaintScheduled = false;\n\n\tStickersPack _pack;\n\tbase::flat_map<EmojiPtr, StickersPack> _emoji;\n\tbool _loaded = false;\n\tuint64 _setId = 0;\n\tuint64 _setAccessHash = 0;\n\tuint64 _setHash = 0;\n\tDocumentId _setThumbnailDocumentId = 0;\n\tQString _setTitle, _setShortName;\n\tint _setCount = 0;\n\tData::StickersSetFlags _setFlags;\n\tint _rowsCount = 0;\n\tint _perRow = 0;\n\tQSize _singleSize;\n\tTimeId _setInstallDate = TimeId(0);\n\tStickerType _setThumbnailType = StickerType::Webp;\n\tImageWithLocation _setThumbnail;\n\tbool _amSetCreator = false;\n\n\tstruct {\n\t\tbool enabled = false;\n\t\tint index = -1;\n\t\tint lastSelected = -1;\n\t\tQPoint point;\n\t} _dragging;\n\tUi::Animations::Basic _shakeAnimation;\n\tstd::deque<Fn<void()>> _reorderRequests;\n\tstd::optional<MTP::Sender> _apiReorder;\n\n\tstruct ShiftAnimation final {\n\t\tUi::Animations::Simple animation;\n\t\tUi::Animations::Simple yAnimation;\n\t\tint shift = 0;\n\t};\n\tbase::flat_map<int, ShiftAnimation> _shiftAnimations;\n\n\tconst std::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\tmutable StickerPremiumMark _premiumMark;\n\n\tint _visibleTop = 0;\n\tint _visibleBottom = 0;\n\tcrl::time _lastScrolledAt = 0;\n\tcrl::time _lastUpdatedAt = 0;\n\tbase::Timer _updateItemsTimer;\n\n\tStickerSetIdentifier _input;\n\tQMargins _padding;\n\n\tmtpRequestId _installRequest = 0;\n\n\tint _selected = -1;\n\n\tbase::Timer _previewTimer;\n\tint _previewShown = -1;\n\tDocumentId _previewDocumentId = 0;\n\tbool _previewLocked = false;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\trpl::event_stream<uint64> _setInstalled;\n\trpl::event_stream<uint64> _setArchived;\n\trpl::event_stream<> _updateControls;\n\trpl::event_stream<Error> _errors;\n\n};\n\nStickerSetBox::StickerSetBox(\n\tQWidget *parent,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst StickerSetIdentifier &set,\n\tData::StickersType type,\n\tDocumentId previewDocumentId)\n: _show(std::move(show))\n, _session(&_show->session())\n, _set(set)\n, _type(type)\n, _previewDocumentId(previewDocumentId) {\n}\n\nStickerSetBox::StickerSetBox(\n\tQWidget *parent,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<Data::StickersSet*> set)\n: StickerSetBox(parent, std::move(show), set->identifier(), set->type()) {\n}\n\nbase::weak_qptr<Ui::BoxContent> StickerSetBox::Show(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document,\n\t\tDocumentId previewDocumentId) {\n\tif (const auto sticker = document->sticker()) {\n\t\tif (sticker->set) {\n\t\t\tauto box = Box<StickerSetBox>(\n\t\t\t\tshow,\n\t\t\t\tsticker->set,\n\t\t\t\tsticker->setType,\n\t\t\t\tpreviewDocumentId);\n\t\t\tconst auto result = base::make_weak(box.data());\n\t\t\tshow->showBox(std::move(box));\n\t\t\treturn result;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid StickerSetBox::prepare() {\n\tsetTitle(tr::lng_contacts_loading());\n\n\t_inner = setInnerWidget(\n\t\tobject_ptr<Inner>(this, _show, _set, _type),\n\t\tst::stickersScroll);\n\tif (const auto previewId = base::take(_previewDocumentId)) {\n\t\t_inner->showPreviewForDocument(previewId);\n\t}\n\t_session->data().stickers().updated(\n\t\t_type\n\t) | rpl::on_next([=] {\n\t\tupdateButtons();\n\t}, lifetime());\n\n\tsetDimensions(\n\t\tst::boxWideWidth,\n\t\t(_type == Data::StickersType::Emoji\n\t\t\t? st::emojiSetMaxHeight\n\t\t\t: st::stickersMaxHeight));\n\n\tupdateTitleAndButtons();\n\n\t_inner->updateControls(\n\t) | rpl::on_next([=] {\n\t\tupdateTitleAndButtons();\n\t}, lifetime());\n\n\t_inner->setInstalled(\n\t) | rpl::on_next([=](uint64 setId) {\n\t\tif (_inner->setType() == Data::StickersType::Masks) {\n\t\t\tshowToast(tr::lng_masks_installed(tr::now));\n\t\t} else if (_inner->setType() == Data::StickersType::Emoji) {\n\t\t\tauto &stickers = _session->data().stickers();\n\t\t\tstickers.notifyEmojiSetInstalled(setId);\n\t\t} else if (_inner->setType() == Data::StickersType::Stickers) {\n\t\t\tauto &stickers = _session->data().stickers();\n\t\t\tstickers.notifyStickerSetInstalled(setId);\n\t\t}\n\t\tcloseBox();\n\t}, lifetime());\n\n\t_inner->errors(\n\t) | rpl::on_next([=](Error error) {\n\t\thandleError(error);\n\t}, lifetime());\n\n\t_inner->setArchived(\n\t) | rpl::on_next([=](uint64 setId) {\n\t\tconst auto type = _inner->setType();\n\t\tif (type == Data::StickersType::Emoji) {\n\t\t\treturn;\n\t\t}\n\n\t\tshowToast((type == Data::StickersType::Masks)\n\t\t\t\t? tr::lng_masks_has_been_archived(tr::now)\n\t\t\t\t: tr::lng_stickers_has_been_archived(tr::now));\n\n\t\tauto &order = (type == Data::StickersType::Masks)\n\t\t\t? _session->data().stickers().maskSetsOrderRef()\n\t\t\t: _session->data().stickers().setsOrderRef();\n\t\tconst auto index = order.indexOf(setId);\n\t\tif (index != -1) {\n\t\t\torder.removeAt(index);\n\n\t\t\tauto &local = _session->local();\n\t\t\tif (type == Data::StickersType::Masks) {\n\t\t\t\tlocal.writeInstalledMasks();\n\t\t\t\tlocal.writeArchivedMasks();\n\t\t\t} else {\n\t\t\t\tlocal.writeInstalledStickers();\n\t\t\t\tlocal.writeArchivedStickers();\n\t\t\t}\n\t\t}\n\n\t\t_session->data().stickers().notifyUpdated(type);\n\n\t\tcloseBox();\n\t}, lifetime());\n\n\tboxClosing(\n\t) | rpl::on_next([show = _show] {\n\t\tif (const auto window = show->resolveWindow()) {\n\t\t\twindow->widget()->hideMediaPreview();\n\t\t}\n\t}, lifetime());\n}\n\nvoid StickerSetBox::addStickers() {\n\t_inner->install();\n}\n\nvoid StickerSetBox::copyStickersLink() {\n\tconst auto part = _inner->isEmojiSet() ? u\"addemoji\"_q : \"addstickers\";\n\tconst auto url = _session->createInternalLinkFull(\n\t\tpart + '/' + _inner->shortName());\n\tQGuiApplication::clipboard()->setText(url);\n}\n\nvoid StickerSetBox::handleError(Error error) {\n\tconst auto guard = gsl::finally(crl::guard(this, [=] {\n\t\tcloseBox();\n\t}));\n\n\tswitch (error) {\n\tcase Error::NotFound:\n\t\t_show->showBox(\n\t\t\tUi::MakeInformBox(tr::lng_stickers_not_found(tr::now)));\n\t\tbreak;\n\tdefault: Unexpected(\"Error in StickerSetBox::handleError.\");\n\t}\n}\n\nvoid StickerSetBox::updateTitleAndButtons() {\n\tsetTitle(_inner->title());\n\tupdateButtons();\n}\n\nvoid ChangeSetNameBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Data::Session*> data,\n\t\tconst StickerSetIdentifier &input,\n\t\tFn<void(TLStickerSet)> done) {\n\tstruct State final {\n\t\trpl::variable<mtpRequestId> requestId = 0;\n\t\tUi::RpWidget* saveButton = nullptr;\n\t};\n\tbox->setTitle(tr::lng_stickers_box_edit_name_title());\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_stickers_box_edit_name_about(),\n\t\t\tst::boxLabel));\n\tconst auto state = box->lifetime().make_state<State>();\n\n\tconst auto wasName = [&] {\n\t\tconst auto &sets = data->stickers().sets();\n\t\tconst auto it = sets.find(input.id);\n\t\treturn (it == sets.end()) ? QString() : it->second->title;\n\t}();\n\tconst auto field = box->addRow(object_ptr<Ui::InputField>(\n\t\tbox,\n\t\tst::editStickerSetNameField,\n\t\ttr::lng_stickers_context_edit_name(),\n\t\twasName));\n\tfield->selectAll();\n\tconstexpr auto kMaxSetNameLength = 50;\n\tfield->setMaxLength(kMaxSetNameLength);\n\tUi::AddLengthLimitLabel(field, kMaxSetNameLength, {\n\t\t.customThreshold = kMaxSetNameLength + 1,\n\t});\n\tbox->setFocusCallback([=] { field->setFocusFast(); });\n\tconst auto close = crl::guard(box, [=] { box->closeBox(); });\n\tconst auto save = [=, show = box->uiShow()] {\n\t\tif (state->requestId.current()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto text = field->getLastText().trimmed();\n\t\tif ((Ui::ComputeRealUnicodeCharactersCount(text) > kMaxSetNameLength)\n\t\t\t|| text.isEmpty()) {\n\t\t\tfield->showError();\n\t\t\treturn;\n\t\t}\n\t\tconst auto buttonWidth = state->saveButton\n\t\t\t? state->saveButton->width()\n\t\t\t: 0;\n\t\tstate->requestId = data->session().api().request(\n\t\t\tMTPstickers_RenameStickerSet(\n\t\t\t\tData::InputStickerSet(input),\n\t\t\t\tMTP_string(text))\n\t\t).done([=](const TLStickerSet &result) {\n\t\t\tresult.match([&](const MTPDmessages_stickerSet &d) {\n\t\t\t\tdata->stickers().feedSetFull(d);\n\t\t\t\tdata->stickers().notifyUpdated(Data::StickersType::Stickers);\n\t\t\t}, [](const auto &) {\n\t\t\t});\n\t\t\tdone(result);\n\t\t\tclose();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tshow->showToast(error.type());\n\t\t\tclose();\n\t\t}).send();\n\t\tif (state->saveButton) {\n\t\t\tstate->saveButton->resizeToWidth(buttonWidth);\n\t\t}\n\t};\n\n\tstate->saveButton = box->addButton(\n\t\trpl::conditional(\n\t\t\tstate->requestId.value() | rpl::map(rpl::mappers::_1 > 0),\n\t\t\trpl::single(QString()),\n\t\t\ttr::lng_box_done()),\n\t\tsave);\n\tif (const auto saveButton = state->saveButton) {\n\t\tusing namespace Info::Statistics;\n\t\tconst auto loadingAnimation = InfiniteRadialAnimationWidget(\n\t\t\tsaveButton,\n\t\t\tsaveButton->height() / 2,\n\t\t\t&st::editStickerSetNameLoading);\n\t\tAddChildToWidgetCenter(saveButton, loadingAnimation);\n\t\tloadingAnimation->showOn(\n\t\t\tstate->requestId.value() | rpl::map(rpl::mappers::_1 > 0));\n\t}\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tdata->session().api().request(state->requestId.current()).cancel();\n\t\tclose();\n\t});\n}\n\nvoid StickerSetBox::updateButtons() {\n\tclearButtons();\n\tif (_inner->reorderState()) {\n\t\taddButton(tr::lng_box_done(), [=] {\n\t\t\t_inner->setReorderState(false);\n\t\t\tupdateButtons();\n\t\t});\n\t} else if (_inner->loaded()) {\n\t\tconst auto type = _inner->setType();\n\t\tconst auto share = [=] {\n\t\t\tcopyStickersLink();\n\t\t\tshowToast(type == Data::StickersType::Emoji\n\t\t\t\t\t? tr::lng_stickers_copied_emoji(tr::now)\n\t\t\t\t\t: tr::lng_stickers_copied(tr::now));\n\t\t};\n\t\tconst auto fillSetCreatorMenu = [&] {\n\t\t\tusing Filler = Fn<void(not_null<Ui::PopupMenu*>)>;\n\t\t\tif (!_inner->amSetCreator()) {\n\t\t\t\treturn Filler(nullptr);\n\t\t\t}\n\t\t\tconst auto data = &_session->data();\n\t\t\treturn Filler([=, show = _show, set = _set](\n\t\t\t\t\tnot_null<Ui::PopupMenu*> menu) {\n\t\t\t\tconst auto done = [inner = _inner](const TLStickerSet &set) {\n\t\t\t\t\tif (const auto raw = inner.data()) {\n\t\t\t\t\t\traw->applySet(set);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tmenu->addAction(\n\t\t\t\t\ttr::lng_stickers_context_edit_name(tr::now),\n\t\t\t\t\t[=] {\n\t\t\t\t\t\tshow->showBox(Box(ChangeSetNameBox, data, set, done));\n\t\t\t\t\t},\n\t\t\t\t\t&st::menuIconEdit);\n\t\t\t\tmenu->addAction(\n\t\t\t\t\ttr::lng_stickers_context_reorder(tr::now),\n\t\t\t\t\t[=] {\n\t\t\t\t\t\t_inner->setReorderState(true);\n\t\t\t\t\t\tupdateButtons();\n\t\t\t\t\t},\n\t\t\t\t\t&st::menuIconReorder);\n\t\t\t});\n\t\t}();\n\t\tif (_inner->notInstalled()) {\n\t\t\tif (!_session->premium()\n\t\t\t\t&& _session->premiumPossible()\n\t\t\t\t&& _inner->premiumEmojiSet()) {\n\t\t\t\tconst auto &st = st::premiumPreviewDoubledLimitsBox;\n\t\t\t\tsetStyle(st);\n\t\t\t\tauto button = CreateUnlockButton(\n\t\t\t\t\tthis,\n\t\t\t\t\ttr::lng_premium_unlock_emoji());\n\t\t\t\tbutton->resizeToWidth(st::boxWideWidth\n\t\t\t\t\t- st.buttonPadding.left()\n\t\t\t\t\t- st.buttonPadding.left());\n\t\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\t\tif (const auto window = _show->resolveWindow()) {\n\t\t\t\t\t\tSettings::ShowPremium(window, u\"animated_emoji\"_q);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\taddButton(std::move(button));\n\t\t\t} else {\n\t\t\t\tauto addText = (type == Data::StickersType::Emoji)\n\t\t\t\t\t? tr::lng_stickers_add_emoji()\n\t\t\t\t\t: (type == Data::StickersType::Masks)\n\t\t\t\t\t? tr::lng_stickers_add_masks()\n\t\t\t\t\t: tr::lng_stickers_add_pack();\n\t\t\t\taddButton(std::move(addText), [=] { addStickers(); });\n\t\t\t\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\t\t\t}\n\n\t\t\tif (!_inner->shortName().isEmpty()) {\n\t\t\t\tconst auto top = addTopButton(st::infoTopBarMenu);\n\t\t\t\tconst auto menu\n\t\t\t\t\t= std::make_shared<base::unique_qptr<Ui::PopupMenu>>();\n\t\t\t\ttop->setClickedCallback([=] {\n\t\t\t\t\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\t\t\ttop,\n\t\t\t\t\t\tst::popupMenuWithIcons);\n\t\t\t\t\tif (fillSetCreatorMenu) {\n\t\t\t\t\t\tfillSetCreatorMenu(*menu);\n\t\t\t\t\t}\n\t\t\t\t\t(*menu)->addAction(\n\t\t\t\t\t\t((type == Data::StickersType::Emoji)\n\t\t\t\t\t\t\t? tr::lng_stickers_share_emoji\n\t\t\t\t\t\t\t: (type == Data::StickersType::Masks)\n\t\t\t\t\t\t\t? tr::lng_stickers_share_masks\n\t\t\t\t\t\t\t: tr::lng_stickers_share_pack)(tr::now),\n\t\t\t\t\t\t[=] { share(); closeBox(); },\n\t\t\t\t\t\t&st::menuIconShare);\n\t\t\t\t\t(*menu)->popup(QCursor::pos());\n\t\t\t\t\treturn true;\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (_inner->official()) {\n\t\t\taddButton(tr::lng_about_done(), [=] { closeBox(); });\n\t\t} else {\n\t\t\tauto shareText = (type == Data::StickersType::Emoji)\n\t\t\t\t? tr::lng_stickers_share_emoji()\n\t\t\t\t: (type == Data::StickersType::Masks)\n\t\t\t\t? tr::lng_stickers_share_masks()\n\t\t\t\t: tr::lng_stickers_share_pack();\n\t\t\taddButton(std::move(shareText), std::move(share));\n\t\t\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\n\t\t\tif (!_inner->shortName().isEmpty()) {\n\t\t\t\tconst auto top = addTopButton(st::infoTopBarMenu);\n\t\t\t\tconst auto archive = [=] {\n\t\t\t\t\t_inner->archiveStickers();\n\t\t\t\t};\n\t\t\t\tconst auto remove = [=] {\n\t\t\t\t\tconst auto session = &_show->session();\n\t\t\t\t\tauto box = ChatHelpers::MakeConfirmRemoveSetBox(\n\t\t\t\t\t\tsession,\n\t\t\t\t\t\tst::boxLabel,\n\t\t\t\t\t\t_inner->setId());\n\t\t\t\t\tif (box) {\n\t\t\t\t\t\t_show->showBox(std::move(box));\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tconst auto menu\n\t\t\t\t\t= std::make_shared<base::unique_qptr<Ui::PopupMenu>>();\n\t\t\t\ttop->setClickedCallback([=] {\n\t\t\t\t\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\t\t\ttop,\n\t\t\t\t\t\tst::popupMenuWithIcons);\n\t\t\t\t\tif (type == Data::StickersType::Emoji) {\n\t\t\t\t\t\t(*menu)->addAction(\n\t\t\t\t\t\t\ttr::lng_custom_emoji_remove_pack_button(tr::now),\n\t\t\t\t\t\t\tremove,\n\t\t\t\t\t\t\t&st::menuIconRemove);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (fillSetCreatorMenu) {\n\t\t\t\t\t\t\tfillSetCreatorMenu(*menu);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t(*menu)->addAction(\n\t\t\t\t\t\t\t(type == Data::StickersType::Masks\n\t\t\t\t\t\t\t\t? tr::lng_masks_archive_pack(tr::now)\n\t\t\t\t\t\t\t\t: tr::lng_stickers_archive_pack(tr::now)),\n\t\t\t\t\t\t\tarchive,\n\t\t\t\t\t\t\t&st::menuIconArchive);\n\t\t\t\t\t}\n\t\t\t\t\t(*menu)->popup(QCursor::pos());\n\t\t\t\t\treturn true;\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t} else {\n\t\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\t}\n\tupdate();\n}\n\nvoid StickerSetBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\t_inner->resize(width(), _inner->height());\n}\n\nStickerSetBox::Inner::Inner(\n\tQWidget *parent,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst StickerSetIdentifier &set,\n\tData::StickersType type)\n: RpWidget(parent)\n, _show(std::move(show))\n, _session(&_show->session())\n, _api(&_session->mtp())\n, _setId(set.id)\n, _setAccessHash(set.accessHash)\n, _setShortName(set.shortName)\n, _pathGradient(std::make_unique<Ui::PathShiftGradient>(\n\tst::windowBgRipple,\n\tst::windowBgOver,\n\t[=] { repaintItems(); }))\n, _premiumMark(_session, st::stickersPremiumLock)\n, _updateItemsTimer([=] { updateItems(); })\n, _input(set)\n, _padding((type == Data::StickersType::Emoji)\n\t? st::emojiSetPadding\n\t: st::stickersPadding)\n, _previewTimer([=] { showPreview(); }) {\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\n\t_api.request(MTPmessages_GetStickerSet(\n\t\tData::InputStickerSet(_input),\n\t\tMTP_int(0) // hash\n\t)).done([=](const TLStickerSet &result) {\n\t\tapplySet(result);\n\t}).fail([=] {\n\t\t_loaded = true;\n\t\t_errors.fire(Error::NotFound);\n\t}).send();\n\n\t_session->api().updateStickers();\n\n\t_session->downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdateItems();\n\t}, lifetime());\n\n\tsetMouseTracking(true);\n}\n\nvoid StickerSetBox::Inner::applySet(const TLStickerSet &set) {\n\t_pack.clear();\n\t_emoji.clear();\n\t_elements.clear();\n\t_selected = -1;\n\tsetCursor(style::cur_default);\n\tconst auto owner = &_session->data();\n\tconst auto premiumPossible = _session->premiumPossible();\n\tset.match([&](const MTPDmessages_stickerSet &data) {\n\t\tconst auto &v = data.vdocuments().v;\n\t\t_pack.reserve(v.size());\n\t\t_elements.reserve(v.size());\n\t\tfor (const auto &item : v) {\n\t\t\tconst auto document = owner->processDocument(item);\n\t\t\tconst auto sticker = document->sticker();\n\t\t\tif (!sticker) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t_pack.push_back(document);\n\t\t\tif (!document->isPremiumSticker() || premiumPossible) {\n\t\t\t\t_elements.push_back({\n\t\t\t\t\tdocument,\n\t\t\t\t\tdocument->createMediaView(),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tfor (const auto &pack : data.vpacks().v) {\n\t\t\tpack.match([&](const MTPDstickerPack &pack) {\n\t\t\t\tif (const auto emoji = Ui::Emoji::Find(qs(pack.vemoticon()))) {\n\t\t\t\t\tconst auto original = emoji->original();\n\t\t\t\t\tauto &stickers = pack.vdocuments().v;\n\n\t\t\t\t\tauto p = StickersPack();\n\t\t\t\t\tp.reserve(stickers.size());\n\t\t\t\t\tfor (auto j = 0, c = int(stickers.size()); j != c; ++j) {\n\t\t\t\t\t\tauto doc = _session->data().document(stickers[j].v);\n\t\t\t\t\t\tif (!doc || !doc->sticker()) continue;\n\n\t\t\t\t\t\tp.push_back(doc);\n\t\t\t\t\t}\n\t\t\t\t\t_emoji[original] = std::move(p);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\t{\n\t\t\tconst auto &set = data.vset().data();\n\t\t\t_setTitle = _session->data().stickers().getSetTitle(\n\t\t\t\tset);\n\t\t\t_setShortName = qs(set.vshort_name());\n\t\t\t_setId = set.vid().v;\n\t\t\t_setAccessHash = set.vaccess_hash().v;\n\t\t\t_setHash = set.vhash().v;\n\t\t\t_setCount = set.vcount().v;\n\t\t\t_setFlags = Data::ParseStickersSetFlags(set);\n\t\t\t_setInstallDate = set.vinstalled_date().value_or(0);\n\t\t\t_setThumbnailDocumentId = set.vthumb_document_id().value_or_empty();\n\t\t\t_amSetCreator = set.is_creator();\n\t\t\t_setThumbnail = [&] {\n\t\t\t\tif (const auto thumbs = set.vthumbs()) {\n\t\t\t\t\tfor (const auto &thumb : thumbs->v) {\n\t\t\t\t\t\tconst auto result = Images::FromPhotoSize(\n\t\t\t\t\t\t\t_session,\n\t\t\t\t\t\t\tset,\n\t\t\t\t\t\t\tthumb);\n\t\t\t\t\t\tif (result.location.valid()) {\n\t\t\t\t\t\t\t_setThumbnailType\n\t\t\t\t\t\t\t\t= Data::ThumbnailTypeFromPhotoSize(thumb);\n\t\t\t\t\t\t\treturn result;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn ImageWithLocation();\n\t\t\t}();\n\t\t\tconst auto &sets = _session->data().stickers().sets();\n\t\t\tconst auto it = sets.find(_setId);\n\t\t\tif (it != sets.cend()) {\n\t\t\t\tconst auto set = it->second.get();\n\t\t\t\tconst auto clientFlags = set->flags\n\t\t\t\t\t& (SetFlag::Featured\n\t\t\t\t\t\t| SetFlag::NotLoaded\n\t\t\t\t\t\t| SetFlag::Unread\n\t\t\t\t\t\t| SetFlag::Special);\n\t\t\t\t_setFlags |= clientFlags;\n\t\t\t\tset->flags = _setFlags;\n\t\t\t\tset->installDate = _setInstallDate;\n\t\t\t\tset->stickers = _pack;\n\t\t\t\tset->emoji = _emoji;\n\t\t\t\tset->setThumbnail(_setThumbnail, _setThumbnailType);\n\t\t\t}\n\t\t};\n\t}, [&](const MTPDmessages_stickerSetNotModified &data) {\n\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t});\n\n\tif (_pack.isEmpty()) {\n\t\t_errors.fire(Error::NotFound);\n\t\treturn;\n\t}\n\t_perRow = isEmojiSet() ? kEmojiPerRow : kStickersPerRow;\n\t_rowsCount = (_pack.size() + _perRow - 1) / _perRow;\n\t_singleSize = isEmojiSet() ? st::emojiSetSize : st::stickersSize;\n\n\tresize(\n\t\t_padding.left() + _perRow * _singleSize.width(),\n\t\t_padding.top() + _rowsCount * _singleSize.height() + _padding.bottom());\n\n\t_loaded = true;\n\tif (const auto previewId = base::take(_previewDocumentId)) {\n\t\tshowPreviewForDocument(previewId);\n\t}\n\tupdateSelected();\n\t_updateControls.fire({});\n}\n\nrpl::producer<uint64> StickerSetBox::Inner::setInstalled() const {\n\treturn _setInstalled.events();\n}\n\nrpl::producer<uint64> StickerSetBox::Inner::setArchived() const {\n\treturn _setArchived.events();\n}\n\nrpl::producer<> StickerSetBox::Inner::updateControls() const {\n\treturn _updateControls.events();\n}\n\nrpl::producer<StickerSetBox::Error> StickerSetBox::Inner::errors() const {\n\treturn _errors.events();\n}\n\nvoid StickerSetBox::Inner::installDone(\n\t\tconst MTPmessages_StickerSetInstallResult &result) {\n\tauto &stickers = _session->data().stickers();\n\tauto &sets = stickers.setsRef();\n\tconst auto type = setType();\n\n\tconst bool wasArchived = (_setFlags & SetFlag::Archived);\n\tif (wasArchived && type != Data::StickersType::Emoji) {\n\t\tconst auto index = ((type == Data::StickersType::Masks)\n\t\t\t? stickers.archivedMaskSetsOrderRef()\n\t\t\t: stickers.archivedSetsOrderRef()).indexOf(_setId);\n\t\tif (index >= 0) {\n\t\t\t((type == Data::StickersType::Masks)\n\t\t\t\t? stickers.archivedMaskSetsOrderRef()\n\t\t\t\t: stickers.archivedSetsOrderRef()).removeAt(index);\n\t\t}\n\t}\n\t_setInstallDate = base::unixtime::now();\n\t_setFlags &= ~SetFlag::Archived;\n\t_setFlags |= SetFlag::Installed;\n\tauto it = sets.find(_setId);\n\tif (it == sets.cend()) {\n\t\tit = sets.emplace(\n\t\t\t_setId,\n\t\t\tstd::make_unique<StickersSet>(\n\t\t\t\t&_session->data(),\n\t\t\t\t_setId,\n\t\t\t\t_setAccessHash,\n\t\t\t\t_setHash,\n\t\t\t\t_setTitle,\n\t\t\t\t_setShortName,\n\t\t\t\t_setCount,\n\t\t\t\t_setFlags,\n\t\t\t\t_setInstallDate)).first;\n\t} else {\n\t\tit->second->flags = _setFlags;\n\t\tit->second->installDate = _setInstallDate;\n\t}\n\tconst auto set = it->second.get();\n\tset->thumbnailDocumentId = _setThumbnailDocumentId;\n\tset->setThumbnail(_setThumbnail, _setThumbnailType);\n\tset->stickers = _pack;\n\tset->emoji = _emoji;\n\n\tauto &order = (type == Data::StickersType::Emoji)\n\t\t? stickers.emojiSetsOrderRef()\n\t\t: (type == Data::StickersType::Masks)\n\t\t? stickers.maskSetsOrderRef()\n\t\t: stickers.setsOrderRef();\n\tconst auto insertAtIndex = 0, currentIndex = int(order.indexOf(_setId));\n\tif (currentIndex != insertAtIndex) {\n\t\tif (currentIndex > 0) {\n\t\t\torder.removeAt(currentIndex);\n\t\t}\n\t\torder.insert(insertAtIndex, _setId);\n\t}\n\n\tconst auto customIt = sets.find(Data::Stickers::CustomSetId);\n\tif (customIt != sets.cend()) {\n\t\tconst auto custom = customIt->second.get();\n\t\tfor (const auto sticker : std::as_const(_pack)) {\n\t\t\tconst int removeIndex = custom->stickers.indexOf(sticker);\n\t\t\tif (removeIndex >= 0) {\n\t\t\t\tcustom->stickers.removeAt(removeIndex);\n\t\t\t}\n\t\t}\n\t\tif (custom->stickers.isEmpty()) {\n\t\t\tsets.erase(customIt);\n\t\t}\n\t}\n\n\tif (result.type() == mtpc_messages_stickerSetInstallResultArchive) {\n\t\tstickers.applyArchivedResult(\n\t\t\tresult.c_messages_stickerSetInstallResultArchive());\n\t} else {\n\t\tauto &storage = _session->local();\n\t\tif (wasArchived && type != Data::StickersType::Emoji) {\n\t\t\tif (type == Data::StickersType::Masks) {\n\t\t\t\tstorage.writeArchivedMasks();\n\t\t\t} else {\n\t\t\t\tstorage.writeArchivedStickers();\n\t\t\t}\n\t\t}\n\t\tif (type == Data::StickersType::Emoji) {\n\t\t\tstorage.writeInstalledCustomEmoji();\n\t\t} else if (type == Data::StickersType::Masks) {\n\t\t\tstorage.writeInstalledMasks();\n\t\t} else {\n\t\t\tstorage.writeInstalledStickers();\n\t\t}\n\t\tstickers.notifyUpdated(type);\n\t}\n\t_setInstalled.fire_copy(_setId);\n}\n\nvoid StickerSetBox::Inner::mousePressEvent(QMouseEvent *e) {\n\tif (_previewLocked) {\n\t\t_previewLocked = false;\n\t\t_previewShown = -1;\n\t\tif (const auto window = _show->resolveWindow()) {\n\t\t\twindow->widget()->hideMediaPreview();\n\t\t}\n\t\treturn;\n\t}\n\tif (e->button() != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tconst auto index = stickerFromGlobalPos(e->globalPos());\n\tif (index < 0 || index >= _pack.size()) {\n\t\treturn;\n\t}\n\tif (_dragging.enabled) {\n\t\t_previewTimer.cancel();\n\t\tif (isDraggedAnimating()) {\n\t\t\treturn;\n\t\t}\n\t\t_dragging.index = index;\n\t\t_dragging.point = mapFromGlobal(QCursor::pos()) - posFromIndex(index);\n\t\treturn;\n\t}\n\t_previewTimer.callOnce(QApplication::startDragTime());\n}\n\nvoid StickerSetBox::Inner::mouseMoveEvent(QMouseEvent *e) {\n\tupdateSelected();\n\tconst auto draggedAnimating = isDraggedAnimating();\n\tif (_selected >= 0 && !draggedAnimating) {\n\t\t_dragging.lastSelected = _selected;\n\t}\n\tif (_dragging.index >= 0\n\t\t&& _dragging.index < _pack.size()\n\t\t&& _dragging.lastSelected >= 0\n\t\t&& !draggedAnimating) {\n\t\tfor (auto i = 0; i < _pack.size(); i++) {\n\t\t\tif (i == _dragging.index) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tauto &entry = _shiftAnimations[i];\n\t\t\tconst auto wasShift = entry.shift;\n\t\t\tif ((i >= _dragging.index) && (i <= _dragging.lastSelected)) {\n\t\t\t\tif (entry.shift == 0) {\n\t\t\t\t\tentry.shift = -1;\n\t\t\t\t} else if (entry.shift == 1) {\n\t\t\t\t\tentry.shift = 0;\n\t\t\t\t}\n\t\t\t} else if ((i < _dragging.index)\n\t\t\t\t\t&& (i >= _dragging.lastSelected)) {\n\t\t\t\tif (entry.shift == 0) {\n\t\t\t\t\tentry.shift = 1;\n\t\t\t\t} else if (entry.shift == -1) {\n\t\t\t\t\tentry.shift = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ((i < std::min(_dragging.index, _dragging.lastSelected))\n\t\t\t\t|| (i > std::max(_dragging.index, _dragging.lastSelected))) {\n\t\t\t\tentry.shift = 0;\n\t\t\t}\n\t\t\tif (wasShift != entry.shift) {\n\t\t\t\tconst auto fromPoint = posFromIndex(i + wasShift);\n\t\t\t\tconst auto toPoint = posFromIndex(i + entry.shift);\n\t\t\t\tconst auto toX = float64(toPoint.x());\n\t\t\t\tconst auto toY = float64(toPoint.y());\n\t\t\t\tconst auto ratio = [&] {\n\t\t\t\t\tconst auto fromX = entry.animation.value(toX);\n\t\t\t\t\tconst auto ratioX = std::min(toX, fromX)\n\t\t\t\t\t\t/ std::max(toX, fromX);\n\t\t\t\t\tconst auto fromY = entry.yAnimation.value(toY);\n\t\t\t\t\tconst auto ratioY = std::min(toY, fromY)\n\t\t\t\t\t\t/ std::max(toY, fromY);\n\t\t\t\t\treturn (ratioX == 1.)\n\t\t\t\t\t\t? ratioY\n\t\t\t\t\t\t: (ratioY == 1.)\n\t\t\t\t\t\t? ratioX\n\t\t\t\t\t\t: std::max(ratioX, ratioY);\n\t\t\t\t}();\n\t\t\t\tif (!entry.animation.animating()) {\n\t\t\t\t\tentry.animation.stop();\n\t\t\t\t\tentry.animation.start(\n\t\t\t\t\t\t[=] { update(); },\n\t\t\t\t\t\tfromPoint.x(),\n\t\t\t\t\t\ttoX,\n\t\t\t\t\t\tkStickerMoveDuration);\n\t\t\t\t} else {\n\t\t\t\t\tentry.animation.change(\n\t\t\t\t\t\ttoX,\n\t\t\t\t\t\tkStickerMoveDuration * (1. - ratio),\n\t\t\t\t\t\tanim::linear);\n\t\t\t\t}\n\t\t\t\tif (!entry.yAnimation.animating()) {\n\t\t\t\t\tentry.yAnimation.stop();\n\t\t\t\t\tentry.yAnimation.start(\n\t\t\t\t\t\t[=] { update(); },\n\t\t\t\t\t\tfromPoint.y(),\n\t\t\t\t\t\ttoY,\n\t\t\t\t\t\tkStickerMoveDuration);\n\t\t\t\t} else {\n\t\t\t\t\tentry.yAnimation.change(\n\t\t\t\t\t\ttoY,\n\t\t\t\t\t\tkStickerMoveDuration * (1. - ratio),\n\t\t\t\t\t\tanim::linear);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tupdate();\n\t}\n\tif (_previewShown >= 0 && !_previewLocked) {\n\t\tshowPreviewAt(e->globalPos());\n\t}\n}\n\nvoid StickerSetBox::Inner::showPreviewAt(QPoint globalPos) {\n\tconst auto index = stickerFromGlobalPos(globalPos);\n\tif (index >= 0\n\t\t&& index < _pack.size()\n\t\t&& index != _previewShown) {\n\t\t_previewShown = index;\n\t\t_show->showMediaPreview(\n\t\t\tData::FileOriginStickerSet(_setId, _setAccessHash),\n\t\t\t_pack[_previewShown]);\n\t}\n}\n\nvoid StickerSetBox::Inner::showPreviewForDocument(DocumentId documentId) {\n\tif (!_loaded) {\n\t\t_previewDocumentId = documentId;\n\t\treturn;\n\t}\n\tconst auto it = ranges::find(\n\t\t_pack,\n\t\tdocumentId,\n\t\t&DocumentData::id);\n\tif (it != _pack.end()) {\n\t\tconst auto index = int(it - _pack.begin());\n\t\tif (index != _previewShown) {\n\t\t\t_previewShown = index;\n\t\t\t_previewLocked = true;\n\t\t\t_show->showMediaPreview(\n\t\t\t\tData::FileOriginStickerSet(_setId, _setAccessHash),\n\t\t\t\t_pack[index]);\n\t\t}\n\t}\n}\n\nvoid StickerSetBox::Inner::leaveEventHook(QEvent *e) {\n\tsetSelected(-1);\n}\n\nvoid StickerSetBox::Inner::requestReorder(\n\t\tnot_null<DocumentData*> document,\n\t\tint index) {\n\tif (!_apiReorder) {\n\t\t_apiReorder.emplace(&_session->mtp());\n\t}\n\t_reorderRequests.emplace_back([document, index, this] {\n\t\t_apiReorder->request(\n\t\t\tMTPstickers_ChangeStickerPosition(\n\t\t\t\tdocument->mtpInput(),\n\t\t\t\tMTP_int(index))\n\t\t\t).done([this, document](const TLStickerSet &result) {\n\t\t\t\tresult.match([&](const MTPDmessages_stickerSet &d) {\n\t\t\t\t\tdocument->owner().stickers().feedSetFull(d);\n\t\t\t\t\tdocument->owner().stickers().notifyUpdated(\n\t\t\t\t\t\tData::StickersType::Stickers);\n\t\t\t\t}, [](const auto &) {\n\t\t\t\t});\n\t\t\t\tif (!_reorderRequests.empty()) {\n\t\t\t\t\t_reorderRequests.pop_front();\n\t\t\t\t}\n\t\t\t\tif (_reorderRequests.empty()) {\n\t\t\t\t\t// applySet(result); // Causes stickers blink.\n\t\t\t\t} else {\n\t\t\t\t\t_reorderRequests.front()();\n\t\t\t\t}\n\t\t\t}).fail([show = _show](const MTP::Error &error) {\n\t\t\t\tshow->showToast(error.type());\n\t\t\t}).send();\n\t});\n\tif (_reorderRequests.size() == 1) {\n\t\t_reorderRequests.front()();\n\t}\n}\n\nvoid StickerSetBox::Inner::mouseReleaseEvent(QMouseEvent *e) {\n\tif (_dragging.index >= 0 && !isDraggedAnimating()) {\n\t\tconst auto fromPos = mapFromGlobal(e->globalPos()) - _dragging.point;\n\t\tconst auto toPos = posFromIndex(_dragging.lastSelected);\n\t\tconst auto document = _pack[_dragging.index];\n\t\tconst auto wasPosition = _dragging.index;\n\t\tconst auto nowPosition = _dragging.lastSelected;\n\t\tconst auto finish = [=, this] {\n\t\t\trequestReorder(document, nowPosition);\n\t\t\tbase::reorder(_pack, wasPosition, nowPosition);\n\t\t\tbase::reorder(_elements, wasPosition, nowPosition);\n\t\t\t_dragging = {};\n\t\t\t_dragging.enabled = true;\n\t\t\t_shiftAnimations.clear();\n\t\t};\n\t\tauto &entry = _shiftAnimations[_dragging.index];\n\t\tentry.animation.stop();\n\t\tentry.yAnimation.stop();\n\t\tentry.animation.start(\n\t\t\t[finish, toPos, this](float64 value) {\n\t\t\t\tconst auto index = _dragging.index;\n\t\t\t\tif (value >= toPos.x()\n\t\t\t\t\t&& index >= 0\n\t\t\t\t\t&& !_shiftAnimations[index].yAnimation.animating()) {\n\t\t\t\t\tfinish();\n\t\t\t\t}\n\t\t\t\tupdate();\n\t\t\t},\n\t\t\tfromPos.x(),\n\t\t\ttoPos.x(),\n\t\t\tkStickerMoveDuration);\n\t\tentry.yAnimation.start(\n\t\t\t[finish, toPos, this](float64 value) {\n\t\t\t\tconst auto index = _dragging.index;\n\t\t\t\tif (value >= toPos.y()\n\t\t\t\t\t&& index >= 0\n\t\t\t\t\t&& !_shiftAnimations[index].animation.animating()) {\n\t\t\t\t\tfinish();\n\t\t\t\t}\n\t\t\t\tupdate();\n\t\t\t},\n\t\t\tfromPos.y(),\n\t\t\ttoPos.y(),\n\t\t\tkStickerMoveDuration);\n\t}\n\tif (_previewShown >= 0) {\n\t\tif (_previewLocked) {\n\t\t\t_previewLocked = false;\n\t\t\tif (const auto window = _show->resolveWindow()) {\n\t\t\t\twindow->widget()->hideMediaPreview();\n\t\t\t}\n\t\t}\n\t\t_previewShown = -1;\n\t\treturn;\n\t}\n\tif (!_previewTimer.isActive()) {\n\t\treturn;\n\t}\n\t_previewTimer.cancel();\n\tconst auto index = stickerFromGlobalPos(e->globalPos());\n\tif (index < 0 || index >= _pack.size()) {\n\t\treturn;\n\t}\n\tchosen(index, _pack[index], {});\n}\n\nvoid StickerSetBox::Inner::chosen(\n\t\tint index,\n\t\tnot_null<DocumentData*> sticker,\n\t\tApi::SendOptions options) {\n\tconst auto animation = options.scheduled\n\t\t? Ui::MessageSendingAnimationFrom()\n\t\t: messageSentAnimationInfo(index, sticker);\n\t_show->processChosenSticker({\n\t\t.document = sticker,\n\t\t.options = options,\n\t\t.messageSendingFrom = animation,\n\t});\n}\n\nauto StickerSetBox::Inner::messageSentAnimationInfo(\n\tint index,\n\tnot_null<DocumentData*> document) const\n-> Ui::MessageSendingAnimationFrom {\n\tif (index < 0 || index >= _pack.size() || _pack[index] != document) {\n\t\treturn {};\n\t}\n\tconst auto row = index / _perRow;\n\tconst auto column = index % _perRow;\n\tconst auto left = _padding.left() + column * _singleSize.width();\n\tconst auto top = _padding.top() + row * _singleSize.height();\n\tconst auto rect = QRect(QPoint(left, top), _singleSize);\n\tconst auto size = ChatHelpers::ComputeStickerSize(\n\t\tdocument,\n\t\tboundingBoxSize());\n\tconst auto innerPos = QPoint(\n\t\t(rect.width() - size.width()) / 2,\n\t\t(rect.height() - size.height()) / 2);\n\treturn {\n\t\t.type = Ui::MessageSendingAnimationFrom::Type::Sticker,\n\t\t.localId = _session->data().nextLocalMessageId(),\n\t\t.globalStartGeometry = mapToGlobal(\n\t\t\tQRect(rect.topLeft() + innerPos, size)),\n\t};\n}\n\nvoid StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {\n\tconst auto index = stickerFromGlobalPos(e->globalPos());\n\tif (index < 0\n\t\t|| index >= _pack.size()\n\t\t|| setType() == Data::StickersType::Masks) {\n\t\treturn;\n\t}\n\t_previewTimer.cancel();\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\tst::popupMenuWithIcons);\n\tconst auto details = _show->sendMenuDetails();\n\tif (setType() == Data::StickersType::Emoji) {\n\t\tif (const auto t = PrepareTextFromEmoji(_pack[index]); !t.empty()) {\n\t\t\t_menu->addAction(tr::lng_mediaview_copy(tr::now), [=] {\n\t\t\t\tif (auto data = TextUtilities::MimeDataFromText(t)) {\n\t\t\t\t\tQGuiApplication::clipboard()->setMimeData(data.release());\n\t\t\t\t}\n\t\t\t}, &st::menuIconCopy);\n\t\t}\n\t} else if (details.type != SendMenu::Type::Disabled) {\n\t\tconst auto document = _pack[index];\n\t\tconst auto send = crl::guard(this, [=](Api::SendOptions options) {\n\t\t\tchosen(index, document, options);\n\t\t});\n\n\t\t// In case we're adding items after FillSendMenu we have\n\t\t// to pass nullptr for showForEffect and attach selector later.\n\t\t// Otherwise added items widths won't be respected in menu geometry.\n\t\tSendMenu::FillSendMenu(\n\t\t\t_menu.get(),\n\t\t\tnullptr, // showForEffect\n\t\t\tdetails,\n\t\t\tSendMenu::DefaultCallback(_show, send));\n\n\t\tconst auto show = _show;\n\t\tconst auto toggleFavedSticker = [=] {\n\t\t\tApi::ToggleFavedSticker(\n\t\t\t\tshow,\n\t\t\t\tdocument,\n\t\t\t\tData::FileOriginStickerSet(Data::Stickers::FavedSetId, 0));\n\t\t};\n\t\tconst auto isFaved = document->owner().stickers().isFaved(document);\n\t\t_menu->addAction(\n\t\t\t(isFaved\n\t\t\t\t? tr::lng_faved_stickers_remove\n\t\t\t\t: tr::lng_faved_stickers_add)(tr::now),\n\t\t\ttoggleFavedSticker,\n\t\t\t(isFaved\n\t\t\t\t? &st::menuIconUnfave\n\t\t\t\t: &st::menuIconFave));\n\t\tif (amSetCreator()) {\n\t\t\tconst auto addAction = Ui::Menu::CreateAddActionCallback(\n\t\t\t\t_menu.get());\n\t\t\taddAction({\n\t\t\t\t.text = tr::lng_stickers_context_delete(tr::now),\n\t\t\t\t.handler = [index, this, show = _show] {\n\t\t\t\t\tshow->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\t\t\tfillDeleteStickerBox(box, index);\n\t\t\t\t\t}));\n\t\t\t\t},\n\t\t\t\t.icon = &st::menuIconDeleteAttention,\n\t\t\t\t.isAttention = true,\n\t\t\t});\n\t\t}\n\n\t\tSendMenu::AttachSendMenuEffect(\n\t\t\t_menu.get(),\n\t\t\t_show,\n\t\t\tdetails,\n\t\t\tSendMenu::DefaultCallback(_show, send));\n\t}\n\tif (_menu->empty()) {\n\t\t_menu = nullptr;\n\t} else {\n\t\t_menu->popup(QCursor::pos());\n\t}\n}\n\nvoid StickerSetBox::Inner::fillDeleteStickerBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tint index) {\n\tExpects(index >= 0 || index < _pack.size());\n\tconst auto document = _pack[index];\n\tconst auto weak = base::make_weak(this);\n\tconst auto show = _show;\n\n\tconst auto container = box->verticalLayout();\n\tUi::AddSkip(container);\n\tUi::AddSkip(container);\n\tconst auto line = container->add(object_ptr<Ui::RpWidget>(container));\n\tline->resize(line->width(), _singleSize.height());\n\n\tconst auto sticker = Ui::CreateChild<Ui::RpWidget>(line);\n\tauto &lifetime = sticker->lifetime();\n\tstruct State final {\n\t\trpl::variable<mtpRequestId> requestId = 0;\n\t\tUi::RpWidget* saveButton = nullptr;\n\t};\n\tconst auto state = lifetime.make_state<State>();\n\tsticker->resize(_singleSize);\n\t{\n\t\tconst auto animation = lifetime.make_state<Ui::Animations::Basic>();\n\t\tanimation->init([=] { sticker->update(); });\n\t\tanimation->start();\n\t}\n\tsticker->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = Painter(sticker);\n\t\tif ([[maybe_unused]] const auto strong = weak.get()) {\n\t\t\tconst auto paused = On(PowerSaving::kStickersPanel)\n\t\t\t\t|| show->paused(ChatHelpers::PauseReason::Layer);\n\t\t\tpaintSticker(p, index, QPoint(), paused, crl::now());\n\t\t\tif (_lottiePlayer && !paused) {\n\t\t\t\t_lottiePlayer->markFrameShown();\n\t\t\t}\n\t\t}\n\t}, sticker->lifetime());\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tline,\n\t\ttr::lng_stickers_context_delete(),\n\t\tbox->getDelegate()->style().title);\n\tline->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tsticker->moveToLeft(st::boxRowPadding.left(), 0);\n\t\tconst auto skip = st::defaultBoxCheckbox.textPosition.x();\n\t\tlabel->resizeToWidth(width\n\t\t\t- rect::right(sticker)\n\t\t\t- skip\n\t\t\t- st::boxRowPadding.right());\n\t\tlabel->moveToLeft(\n\t\t\trect::right(sticker) + skip,\n\t\t\t((sticker->height() - label->height()) / 2));\n\t}, label->lifetime());\n\n\tsticker->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tUi::AddSkip(container);\n\tUi::AddSkip(container);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_stickers_context_delete_sure(),\n\t\t\tst::boxLabel));\n\tconst auto save = [=] {\n\t\tif (state->requestId.current()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto weakBox = base::make_weak(box);\n\t\tconst auto buttonWidth = state->saveButton\n\t\t\t? state->saveButton->width()\n\t\t\t: 0;\n\t\tstate->requestId = document->session().api().request(\n\t\t\tMTPstickers_RemoveStickerFromSet(document->mtpInput()\n\t\t)).done([=](const TLStickerSet &result) {\n\t\t\tresult.match([&](const MTPDmessages_stickerSet &d) {\n\t\t\t\tdocument->owner().stickers().feedSetFull(d);\n\t\t\t\tdocument->owner().stickers().notifyUpdated(\n\t\t\t\t\tData::StickersType::Stickers);\n\t\t\t}, [](const auto &) {\n\t\t\t});\n\t\t\tif ([[maybe_unused]] const auto strong = weak.get()) {\n\t\t\t\tapplySet(result);\n\t\t\t}\n\t\t\tif (const auto strongBox = weakBox.get()) {\n\t\t\t\tstrongBox->closeBox();\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tif (const auto strongBox = weakBox.get()) {\n\t\t\t\tstrongBox->uiShow()->showToast(error.type());\n\t\t\t}\n\t\t}).send();\n\t\tif (state->saveButton) {\n\t\t\tstate->saveButton->resizeToWidth(buttonWidth);\n\t\t}\n\t};\n\tstate->saveButton = box->addButton(\n\t\trpl::conditional(\n\t\t\tstate->requestId.value() | rpl::map(rpl::mappers::_1 > 0),\n\t\t\trpl::single(QString()),\n\t\t\ttr::lng_selected_delete()),\n\t\tsave,\n\t\tst::attentionBoxButton);\n\tif (const auto saveButton = state->saveButton) {\n\t\tusing namespace Info::Statistics;\n\t\tconst auto loadingAnimation = InfiniteRadialAnimationWidget(\n\t\t\tsaveButton,\n\t\t\tsaveButton->height() / 2,\n\t\t\t&st::editStickerSetNameLoading);\n\t\tAddChildToWidgetCenter(saveButton, loadingAnimation);\n\t\tloadingAnimation->showOn(\n\t\t\tstate->requestId.value() | rpl::map(rpl::mappers::_1 > 0));\n\t}\n\tbox->addButton(tr::lng_close(), [=] {\n\t\tdocument->session().api().request(\n\t\t\tstate->requestId.current()).cancel();\n\t\tbox->closeBox();\n\t});\n}\n\nvoid StickerSetBox::Inner::updateSelected() {\n\tauto selected = stickerFromGlobalPos(QCursor::pos());\n\tsetSelected(setType() == Data::StickersType::Masks ? -1 : selected);\n}\n\nvoid StickerSetBox::Inner::setSelected(int selected) {\n\tif (_selected != selected) {\n\t\tstartOverAnimation(_selected, 1., 0.);\n\t\t_selected = selected;\n\t\tstartOverAnimation(_selected, 0., 1.);\n\t\tsetCursor((_selected < 0)\n\t\t\t? style::cur_default\n\t\t\t: _dragging.enabled\n\t\t\t? style::cur_sizeall\n\t\t\t: style::cur_pointer);\n\t}\n}\n\nvoid StickerSetBox::Inner::startOverAnimation(int index, float64 from, float64 to) {\n\tif (index < 0 || index >= _elements.size()) {\n\t\treturn;\n\t}\n\t_elements[index].overAnimation.start([=] {\n\t\tconst auto row = index / _perRow;\n\t\tconst auto column = index % _perRow;\n\t\tconst auto left = _padding.left() + column * _singleSize.width();\n\t\tconst auto top = _padding.top() + row * _singleSize.height();\n\t\trtlupdate(left, top, _singleSize.width(), _singleSize.height());\n\t}, from, to, st::emojiPanDuration);\n}\n\nvoid StickerSetBox::Inner::showPreview() {\n\t_previewShown = -1;\n\tshowPreviewAt(QCursor::pos());\n}\n\nQPoint StickerSetBox::Inner::posFromIndex(int index) const {\n\treturn {\n\t\t_padding.left() + (index % _perRow) * _singleSize.width(),\n\t\t_padding.top() + (index / _perRow) * _singleSize.height(),\n\t};\n}\n\nbool StickerSetBox::Inner::isDraggedAnimating() const {\n\tif (_dragging.index < 0) {\n\t\treturn false;\n\t}\n\tconst auto it = _shiftAnimations.find(_dragging.index);\n\treturn (it == _shiftAnimations.end())\n\t\t? false\n\t\t: (it->second.animation.animating()\n\t\t\t|| it->second.yAnimation.animating());\n}\n\nnot_null<Lottie::MultiPlayer*> StickerSetBox::Inner::getLottiePlayer() {\n\tif (!_lottiePlayer) {\n\t\t_lottiePlayer = std::make_unique<Lottie::MultiPlayer>(\n\t\t\tLottie::Quality::Default,\n\t\t\tLottie::MakeFrameRenderer());\n\t\t_lottiePlayer->updates(\n\t\t) | rpl::on_next([=] {\n\t\t\tupdateItems();\n\t\t}, lifetime());\n\t}\n\treturn _lottiePlayer.get();\n}\n\nint32 StickerSetBox::Inner::stickerFromGlobalPos(const QPoint &p) const {\n\tQPoint l(mapFromGlobal(p));\n\tif (rtl()) l.setX(width() - l.x());\n\tint32 row = (l.y() >= _padding.top()) ? qFloor((l.y() - _padding.top()) / _singleSize.height()) : -1;\n\tint32 col = (l.x() >= _padding.left()) ? qFloor((l.x() - _padding.left()) / _singleSize.width()) : -1;\n\tif (row >= 0 && col >= 0 && col < _perRow) {\n\t\tint32 result = row * _perRow + col;\n\t\treturn (result < _pack.size()) ? result : -1;\n\t}\n\treturn -1;\n}\n\nvoid StickerSetBox::Inner::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\t_repaintScheduled = false;\n\n\tp.fillRect(e->rect(), st::boxBg);\n\tif (_elements.empty()) {\n\t\treturn;\n\t}\n\n\tint32 from = qFloor(e->rect().top() / _singleSize.height()), to = qFloor(e->rect().bottom() / _singleSize.height()) + 1;\n\n\t_pathGradient->startFrame(0, width(), width() / 2);\n\n\tconst auto indexUnderCursor = (_dragging.index >= 0\n\t\t\t&& _dragging.index < _elements.size())\n\t\t? stickerFromGlobalPos(QCursor::pos())\n\t\t: -2;\n\tconst auto lastIndex = indexUnderCursor >= 0\n\t\t? indexUnderCursor\n\t\t: _dragging.lastSelected;\n\n\tconst auto now = crl::now();\n\tconst auto paused = On(PowerSaving::kStickersPanel)\n\t\t|| _show->paused(ChatHelpers::PauseReason::Layer);\n\tfor (int32 i = from; i < to; ++i) {\n\t\tfor (int32 j = 0; j < _perRow; ++j) {\n\t\t\tint32 index = i * _perRow + j;\n\n\t\t\tif (lastIndex >= 0) {\n\t\t\t\tif (_dragging.index == index) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto it = _shiftAnimations.find(index);\n\t\t\t\tif (it != _shiftAnimations.end()) {\n\t\t\t\t\tconst auto &entry = it->second;\n\t\t\t\t\tconst auto toPos = posFromIndex(index + entry.shift);\n\t\t\t\t\tconst auto pos = QPoint(\n\t\t\t\t\t\tentry.animation.value(toPos.x()),\n\t\t\t\t\t\tentry.yAnimation.value(toPos.y()));\n\t\t\t\t\tpaintSticker(p, index, pos, paused, now);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (index >= _elements.size()) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst auto pos = QPoint(\n\t\t\t\t_padding.left() + j * _singleSize.width(),\n\t\t\t\t_padding.top() + i * _singleSize.height());\n\t\t\tpaintSticker(p, index, pos, paused, now);\n\t\t}\n\t}\n\tif (_dragging.index >= 0 && _dragging.index < _elements.size()) {\n\t\tconst auto pos = isDraggedAnimating()\n\t\t\t? QPoint(\n\t\t\t\t_shiftAnimations[_dragging.index].animation.value(0),\n\t\t\t\t_shiftAnimations[_dragging.index].yAnimation.value(0))\n\t\t\t: (mapFromGlobal(QCursor::pos()) - _dragging.point);\n\t\tpaintSticker(p, _dragging.index, pos, paused, now);\n\t}\n\n\tif (_lottiePlayer && !paused) {\n\t\t_lottiePlayer->markFrameShown();\n\t}\n}\n\nbool StickerSetBox::Inner::isEmojiSet() const {\n\treturn (_setFlags & Data::StickersSetFlag::Emoji);\n}\n\nuint64 StickerSetBox::Inner::setId() const {\n\treturn _setId;\n}\n\nQSize StickerSetBox::Inner::boundingBoxSize() const {\n\tif (isEmojiSet()) {\n\t\tusing namespace Data;\n\t\tconst auto size = FrameSizeFromTag(CustomEmojiSizeTag::Large)\n\t\t\t/ style::DevicePixelRatio();\n\t\treturn { size, size };\n\t}\n\treturn QSize(\n\t\t_singleSize.width() - st::roundRadiusSmall * 2,\n\t\t_singleSize.height() - st::roundRadiusSmall * 2);\n}\n\nvoid StickerSetBox::Inner::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tif (_visibleTop != visibleTop || _visibleBottom != visibleBottom) {\n\t\t_visibleTop = visibleTop;\n\t\t_visibleBottom = visibleBottom;\n\t\t_lastScrolledAt = crl::now();\n\t\tupdate();\n\t}\n\tconst auto pauseInRows = [&](int fromRow, int tillRow) {\n\t\tExpects(fromRow <= tillRow);\n\n\t\tfor (auto i = fromRow; i != tillRow; ++i) {\n\t\t\tfor (auto j = 0; j != _perRow; ++j) {\n\t\t\t\tconst auto index = i * _perRow + j;\n\t\t\t\tif (index >= _elements.size()) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (const auto lottie = _elements[index].lottie) {\n\t\t\t\t\t_lottiePlayer->pause(lottie);\n\t\t\t\t} else if (auto &webm = _elements[index].webm) {\n\t\t\t\t\twebm = nullptr;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\tconst auto rowsTop = _padding.top();\n\tconst auto singleHeight = _singleSize.height();\n\tconst auto rowsBottom = rowsTop + _rowsCount * singleHeight;\n\tif (visibleTop >= rowsTop + singleHeight && visibleTop < rowsBottom) {\n\t\tconst auto pauseHeight = (visibleTop - rowsTop);\n\t\tconst auto pauseRows = std::min(\n\t\t\tpauseHeight / singleHeight,\n\t\t\t_rowsCount);\n\t\tpauseInRows(0, pauseRows);\n\t}\n\tif (visibleBottom > rowsTop\n\t\t&& visibleBottom + singleHeight <= rowsBottom) {\n\t\tconst auto pauseHeight = (rowsBottom - visibleBottom);\n\t\tconst auto pauseRows = std::min(\n\t\t\tpauseHeight / singleHeight,\n\t\t\t_rowsCount);\n\t\tpauseInRows(_rowsCount - pauseRows, _rowsCount);\n\t}\n}\n\nvoid StickerSetBox::Inner::setupLottie(int index) {\n\tauto &element = _elements[index];\n\n\telement.lottie = ChatHelpers::LottieAnimationFromDocument(\n\t\tgetLottiePlayer(),\n\t\telement.documentMedia.get(),\n\t\tChatHelpers::StickerLottieSize::StickerSet,\n\t\tboundingBoxSize() * style::DevicePixelRatio());\n}\n\nvoid StickerSetBox::Inner::setupWebm(int index) {\n\tauto &element = _elements[index];\n\n\tconst auto document = element.document;\n\tauto callback = [=](Media::Clip::Notification notification) {\n\t\tclipCallback(notification, document, index);\n\t};\n\telement.webm = Media::Clip::MakeReader(\n\t\telement.documentMedia->owner()->location(),\n\t\telement.documentMedia->bytes(),\n\t\tstd::move(callback));\n}\n\nvoid StickerSetBox::Inner::clipCallback(\n\t\tMedia::Clip::Notification notification,\n\t\tnot_null<DocumentData*> document,\n\t\tint index) {\n\tconst auto i = (index < _elements.size()\n\t\t&& _elements[index].document == document)\n\t\t? (_elements.begin() + index)\n\t\t: ranges::find(_elements, document, &Element::document);\n\tif (i == end(_elements)) {\n\t\treturn;\n\t}\n\tusing namespace Media::Clip;\n\tswitch (notification) {\n\tcase Notification::Reinit: {\n\t\tauto &webm = i->webm;\n\t\tif (webm->state() == State::Error) {\n\t\t\twebm.setBad();\n\t\t} else if (webm->ready() && !webm->started()) {\n\t\t\tconst auto size = ChatHelpers::ComputeStickerSize(\n\t\t\t\ti->document,\n\t\t\t\tboundingBoxSize());\n\t\t\twebm->start({ .frame = size, .keepAlpha = true });\n\t\t}\n\t} break;\n\n\tcase Notification::Repaint: break;\n\t}\n\n\tupdateItems();\n}\n\nvoid StickerSetBox::Inner::setupEmoji(int index) {\n\tauto &element = _elements[index];\n\telement.emoji = resolveCustomEmoji(element.document);\n}\n\nnot_null<Ui::Text::CustomEmoji*> StickerSetBox::Inner::resolveCustomEmoji(\n\t\tnot_null<DocumentData*> document) {\n\tconst auto i = _customEmoji.find(document);\n\tif (i != end(_customEmoji)) {\n\t\treturn i->second.get();\n\t}\n\tauto emoji = document->session().data().customEmojiManager().create(\n\t\tdocument,\n\t\t[=] { customEmojiRepaint(); },\n\t\tData::CustomEmojiManager::SizeTag::Large);\n\treturn _customEmoji.emplace(\n\t\tdocument,\n\t\tstd::move(emoji)\n\t).first->second.get();\n}\n\nvoid StickerSetBox::Inner::customEmojiRepaint() {\n\tif (_repaintScheduled) {\n\t\treturn;\n\t}\n\t_repaintScheduled = true;\n\tupdate();\n}\n\nvoid StickerSetBox::Inner::shakeTransform(\n\t\tQPainter &p,\n\t\tint index,\n\t\tQPoint position,\n\t\tcrl::time now) const {\n\tconstexpr auto kShakeADuration = crl::time(400);\n\tconstexpr auto kShakeXDuration = crl::time(kShakeADuration * 1.2);\n\tconstexpr auto kShakeYDuration = kShakeADuration;\n\tconst auto diff = ((index % 2) ? 0 : kShakeYDuration / 2)\n\t\t+ (now - _shakeAnimation.started());\n\tconst auto pX = (diff % kShakeXDuration)\n\t\t/ float64(kShakeXDuration);\n\tconst auto pY = (diff % kShakeYDuration)\n\t\t/ float64(kShakeYDuration);\n\tconst auto pA = (diff % kShakeADuration)\n\t\t/ float64(kShakeADuration);\n\n\tconstexpr auto kMaxA = 2.;\n\tconstexpr auto kMaxTranslation = .5;\n\tconstexpr auto kAStep = 1. / 5;\n\tconstexpr auto kXStep = 1. / 5;\n\tconstexpr auto kYStep = 1. / 4;\n\n\t// 0, -kMaxA, 0, kMaxA, 0.\n\tconst auto angle = (pA < kAStep)\n\t\t? anim::interpolateF(0., -kMaxA, pA / kAStep)\n\t\t: (pA < kAStep * 2.)\n\t\t? anim::interpolateF(-kMaxA, 0, (pA - kAStep) / kAStep)\n\t\t: (pA < kAStep * 3.)\n\t\t? anim::interpolateF(0, kMaxA, (pA - kAStep * 2.) / kAStep)\n\t\t: (pA < kAStep * 4.)\n\t\t? anim::interpolateF(kMaxA, 0, (pA - kAStep * 3.) / kAStep)\n\t\t: anim::interpolateF(0, 0., (pA - kAStep * 4.) / kAStep);\n\n\t// 0, kMaxTranslation, 0, -kMaxTranslation, 0.\n\tconst auto x = (pX < kXStep)\n\t\t? anim::interpolateF(0., kMaxTranslation, pX / kXStep)\n\t\t: (pX < kXStep * 2.)\n\t\t? anim::interpolateF(kMaxTranslation, 0, (pX - kXStep) / kXStep)\n\t\t: (pX < kXStep * 3.)\n\t\t? anim::interpolateF(0, -kMaxTranslation, (pX - kXStep * 2.) / kXStep)\n\t\t: (pX < kXStep * 4.)\n\t\t? anim::interpolateF(-kMaxTranslation, 0, (pX - kXStep * 3.) / kXStep)\n\t\t: anim::interpolateF(0, 0., (pX - kXStep * 4.) / kXStep);\n\n\t// 0, kMaxTranslation, -kMaxTranslation, 0.\n\tconst auto y = (pY < kYStep)\n\t\t? anim::interpolateF(0., kMaxTranslation, pY / kYStep)\n\t\t: (pY < kYStep * 2.)\n\t\t? anim::interpolateF(kMaxTranslation, 0, (pY - kYStep) / kYStep)\n\t\t: (pY < kYStep * 3.)\n\t\t? anim::interpolateF(0, -kMaxTranslation, (pY - kYStep * 2.) / kYStep)\n\t\t: anim::interpolateF(-kMaxTranslation, 0, (pY - kYStep * 3) / kYStep);\n\n\tconst auto center = position + QPoint(\n\t\t_singleSize.width() / 2,\n\t\t_singleSize.height() / 2);\n\n\tp.translate(center);\n\tp.rotate(angle);\n\tp.translate(-center);\n\tp.translate(x, y);\n}\n\nvoid StickerSetBox::Inner::paintSticker(\n\t\tPainter &p,\n\t\tint index,\n\t\tQPoint position,\n\t\tbool paused,\n\t\tcrl::time now) const {\n\tif (_dragging.index != index) {\n\t\tconst auto over = _elements[index].overAnimation.value(\n\t\t\t(index == _selected) ? 1. : 0.);\n\t\tif (over) {\n\t\t\tp.setOpacity(over);\n\t\t\tUi::FillRoundRect(\n\t\t\t\tp,\n\t\t\t\tQRect(\n\t\t\t\t\trtl()\n\t\t\t\t\t\t? QPoint(\n\t\t\t\t\t\t\twidth() - position.x() - _singleSize.width(),\n\t\t\t\t\t\t\tposition.y())\n\t\t\t\t\t\t: position,\n\t\t\t\t\t_singleSize),\n\t\t\t\tst::emojiPanHover,\n\t\t\t\tUi::StickerHoverCorners);\n\t\t\tp.setOpacity(1);\n\t\t}\n\t}\n\n\tconst auto hasShake = _shakeAnimation.animating();\n\tif (hasShake) {\n\t\tshakeTransform(p, index, position, now);\n\t}\n\n\tconst auto &element = _elements[index];\n\tconst auto document = element.document;\n\tconst auto &media = element.documentMedia;\n\tconst auto sticker = document->sticker();\n\tmedia->checkStickerSmall();\n\n\tif (sticker->setType == Data::StickersType::Emoji) {\n\t\tconst_cast<Inner*>(this)->setupEmoji(index);\n\t} else if (media->loaded()) {\n\t\tif (sticker->isLottie() && !element.lottie) {\n\t\t\tconst_cast<Inner*>(this)->setupLottie(index);\n\t\t} else if (sticker->isWebm() && !element.webm) {\n\t\t\tconst_cast<Inner*>(this)->setupWebm(index);\n\t\t}\n\t}\n\n\tconst auto premium = document->isPremiumSticker();\n\tconst auto size = ChatHelpers::ComputeStickerSize(\n\t\tdocument,\n\t\tboundingBoxSize());\n\tconst auto ppos = position + QPoint(\n\t\t(_singleSize.width() - size.width()) / 2,\n\t\t(_singleSize.height() - size.height()) / 2);\n\tauto lottieFrame = QImage();\n\tif (element.emoji) {\n\t\telement.emoji->paint(p, {\n\t\t\t.textColor = st::windowFg->c,\n\t\t\t.now = now,\n\t\t\t.position = ppos,\n\t\t\t.paused = paused,\n\t\t});\n\t} else if (element.lottie && element.lottie->ready()) {\n\t\tlottieFrame = element.lottie->frame();\n\t\tp.drawImage(\n\t\t\tQRect(ppos, lottieFrame.size() / style::DevicePixelRatio()),\n\t\t\tlottieFrame);\n\n\t\t_lottiePlayer->unpause(element.lottie);\n\t} else if (element.webm && element.webm->started()) {\n\t\tp.drawImage(ppos, element.webm->current({\n\t\t\t.frame = size,\n\t\t\t.keepAlpha = true,\n\t\t}, paused ? 0 : now));\n\t} else if (const auto image = media->getStickerSmall()) {\n\t\tconst auto pixmap = image->pix(size);\n\t\tp.drawPixmapLeft(ppos, width(), pixmap);\n\t\tif (premium) {\n\t\t\tlottieFrame = pixmap.toImage().convertToFormat(\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t}\n\t} else {\n\t\tChatHelpers::PaintStickerThumbnailPath(\n\t\t\tp,\n\t\t\tmedia.get(),\n\t\t\tQRect(ppos, size),\n\t\t\t_pathGradient.get());\n\t}\n\tif (premium) {\n\t\t_premiumMark.paint(\n\t\t\tp,\n\t\t\tlottieFrame,\n\t\t\telement.premiumLock,\n\t\t\tposition,\n\t\t\t_singleSize,\n\t\t\twidth());\n\t}\n\tif (hasShake) {\n\t\tp.resetTransform();\n\t}\n}\n\nbool StickerSetBox::Inner::loaded() const {\n\treturn _loaded && !_pack.isEmpty();\n}\n\nbool StickerSetBox::Inner::premiumEmojiSet() const {\n\treturn (_setFlags & SetFlag::Emoji)\n\t\t&& !_pack.empty()\n\t\t&& _pack.front()->isPremiumEmoji();\n}\n\nbool StickerSetBox::Inner::notInstalled() const {\n\tif (!_loaded) {\n\t\treturn false;\n\t}\n\tconst auto &sets = _session->data().stickers().sets();\n\tconst auto it = sets.find(_setId);\n\tif ((it == sets.cend())\n\t\t|| !(it->second->flags & SetFlag::Installed)\n\t\t|| (it->second->flags & SetFlag::Archived)) {\n\t\treturn !_pack.empty();\n\t}\n\treturn false;\n}\n\nbool StickerSetBox::Inner::official() const {\n\treturn _loaded && _setShortName.isEmpty();\n}\n\nrpl::producer<TextWithEntities> StickerSetBox::Inner::title() const {\n\tif (!_loaded) {\n\t\treturn tr::lng_contacts_loading(tr::marked);\n\t} else if (_pack.isEmpty()) {\n\t\treturn tr::lng_attach_failed(tr::marked);\n\t}\n\tauto text = TextWithEntities{ _setTitle };\n\tTextUtilities::ParseEntities(text, TextParseMentions);\n\treturn rpl::single(text);\n}\n\nQString StickerSetBox::Inner::shortName() const {\n\treturn _setShortName;\n}\n\nvoid StickerSetBox::Inner::install() {\n\tif (_installRequest) {\n\t\treturn;\n\t}\n\t_installRequest = _api.request(MTPmessages_InstallStickerSet(\n\t\tData::InputStickerSet(_input),\n\t\tMTP_bool(false)\n\t)).done([=](const MTPmessages_StickerSetInstallResult &result) {\n\t\tinstallDone(result);\n\t}).fail([=] {\n\t\t_errors.fire(Error::NotFound);\n\t}).send();\n}\n\nvoid StickerSetBox::Inner::archiveStickers() {\n\t_api.request(MTPmessages_InstallStickerSet(\n\t\tData::InputStickerSet(_input),\n\t\tMTP_boolTrue()\n\t)).done([=](const MTPmessages_StickerSetInstallResult &result) {\n\t\tif (result.type() == mtpc_messages_stickerSetInstallResultSuccess) {\n\t\t\t_setArchived.fire_copy(_setId);\n\t\t}\n\t}).fail([=] {\n\t\t_show->showToast(Lang::Hard::ServerError());\n\t}).send();\n}\n\nvoid StickerSetBox::Inner::updateItems() {\n\tconst auto now = crl::now();\n\n\tconst auto delay = std::max(\n\t\t_lastScrolledAt + kMinAfterScrollDelay - now,\n\t\t_lastUpdatedAt + kMinRepaintDelay - now);\n\tif (delay <= 0) {\n\t\trepaintItems(now);\n\t} else if (!_updateItemsTimer.isActive()\n\t\t|| _updateItemsTimer.remainingTime() > kMinRepaintDelay) {\n\t\t_updateItemsTimer.callOnce(std::max(delay, kMinRepaintDelay));\n\t}\n}\n\nvoid StickerSetBox::Inner::repaintItems(crl::time now) {\n\t_lastUpdatedAt = now ? now : crl::now();\n\tupdate();\n}\n\nStickerSetBox::Inner::~Inner() = default;\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/sticker_set_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n#include \"base/timer.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"ui/rect_part.h\"\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass PlainShadow;\n} // namespace Ui\n\nnamespace Data {\nclass StickersSet;\n} // namespace Data\n\nnamespace ChatHelpers {\nstruct FileChosen;\nclass Show;\n} // namespace ChatHelpers\n\nclass StickerPremiumMark final {\npublic:\n\tStickerPremiumMark(\n\t\tnot_null<Main::Session*> session,\n\t\tconst style::icon &lockIcon,\n\t\tRectPart part = RectPart::Bottom);\n\n\tvoid paint(\n\t\tQPainter &p,\n\t\tconst QImage &frame,\n\t\tQImage &backCache,\n\t\tQPoint position,\n\t\tQSize singleSize,\n\t\tint outerWidth);\n\nprivate:\n\tvoid validateLock(const QImage &frame, QImage &backCache);\n\tvoid validateStar();\n\n\tconst style::icon &_lockIcon;\n\tQImage _lockGray;\n\tQImage _star;\n\tRectPart _part = RectPart::Bottom;\n\tbool _premium = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\nclass StickerSetBox final : public Ui::BoxContent {\npublic:\n\tStickerSetBox(\n\t\tQWidget*,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst StickerSetIdentifier &set,\n\t\tData::StickersType type,\n\t\tDocumentId previewDocumentId = 0);\n\tStickerSetBox(\n\t\tQWidget*,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<Data::StickersSet*> set);\n\n\tstatic base::weak_qptr<Ui::BoxContent> Show(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document,\n\t\tDocumentId previewDocumentId = 0);\n\nprotected:\n\tvoid prepare() override;\n\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tenum class Error {\n\t\tNotFound,\n\t};\n\n\tvoid updateTitleAndButtons();\n\tvoid updateButtons();\n\tvoid addStickers();\n\tvoid copyStickersLink();\n\tvoid handleError(Error error);\n\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst not_null<Main::Session*> _session;\n\tconst StickerSetIdentifier _set;\n\tconst Data::StickersType _type;\n\n\tclass Inner;\n\tQPointer<Inner> _inner;\n\tDocumentId _previewDocumentId = 0;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/stickers_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"stickers_box.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"core/application.h\"\n#include \"lang/lang_keys.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"ui/boxes/boost_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/peers/edit_peer_color_box.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"apiwrap.h\"\n#include \"storage/storage_account.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/slide_animation.h\"\n#include \"ui/widgets/discrete_sliders.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/fields/special_fields.h\"\n#include \"ui/image/image.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/painter.h\"\n#include \"ui/unread_badge_paint.h\"\n#include \"media/clip/media_clip_reader.h\"\n#include \"main/main_session.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace {\n\nusing Data::StickersSet;\nusing Data::StickersSetsOrder;\nusing Data::StickersSetThumbnailView;\nusing SetFlag = Data::StickersSetFlag;\n\nconstexpr auto kArchivedLimitFirstRequest = 10;\nconstexpr auto kArchivedLimitPerPage = 30;\nconstexpr auto kHandleMegagroupSetAddressChangeTimeout = crl::time(1000);\n\n[[nodiscard]] QString FillSetTitle(\n\t\tnot_null<StickersSet*> set,\n\t\tint maxNameWidth,\n\t\tint *outTitleWidth) {\n\tauto result = set->title;\n\tauto titleWidth = st::contactsNameStyle.font->width(result);\n\tif (titleWidth > maxNameWidth) {\n\t\tresult = st::contactsNameStyle.font->elided(result, maxNameWidth);\n\t\ttitleWidth = st::contactsNameStyle.font->width(result);\n\t}\n\tif (outTitleWidth) {\n\t\t*outTitleWidth = titleWidth;\n\t}\n\treturn result;\n}\n\n} // namespace\n\nclass StickersBox::CounterWidget : public Ui::RpWidget {\npublic:\n\tCounterWidget(QWidget *parent, rpl::producer<int> count);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid setCounter(int counter);\n\n\tQString _text;\n\tUi::UnreadBadgeStyle _st;\n\n};\n\n// This class is hold in header because it requires Qt preprocessing.\nclass StickersBox::Inner : public Ui::RpWidget {\npublic:\n\tusing Section = StickersBox::Section;\n\n\tInner(\n\t\tQWidget *parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tSection section);\n\tInner(\n\t\tQWidget *parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<ChannelData*> megagroup,\n\t\tbool isEmoji);\n\n\t[[nodiscard]] Main::Session &session() const;\n\n\trpl::producer<int> scrollsToY() const {\n\t\treturn _scrollsToY.events();\n\t}\n\tvoid setInnerFocus();\n\n\tvoid saveGroupSet(Fn<void()> done);\n\n\tvoid rebuild(bool masks);\n\tvoid updateSize(int newWidth = 0);\n\tvoid updateRows(); // refresh only pack cover stickers\n\tbool appendSet(not_null<StickersSet*> set);\n\n\tStickersSetsOrder order() const;\n\tStickersSetsOrder fullOrder() const;\n\tStickersSetsOrder removedSets() const;\n\n\tvoid setFullOrder(const StickersSetsOrder &order);\n\tvoid setRemovedSets(const StickersSetsOrder &removed);\n\n\tvoid setRowRemovedBySetId(uint64 setId, bool removed);\n\n\tvoid setInstallSetCallback(Fn<void(uint64 setId)> callback) {\n\t\t_installSetCallback = std::move(callback);\n\t}\n\tvoid setRemoveSetCallback(Fn<void(uint64 setId)> callback) {\n\t\t_removeSetCallback = std::move(callback);\n\t}\n\tvoid setLoadMoreCallback(Fn<void()> callback) {\n\t\t_loadMoreCallback = std::move(callback);\n\t}\n\n\tint getVisibleTop() const {\n\t\treturn _visibleTop;\n\t}\n\n\t[[nodiscard]] rpl::producer<int> draggingScrollDelta() const {\n\t\treturn _draggingScrollDelta.events();\n\t}\n\n\t~Inner();\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid leaveToChildEvent(QEvent *e, QWidget *child) override;\n\nprivate:\n\tstruct Row {\n\t\tRow(\n\t\t\tnot_null<StickersSet*> set,\n\t\t\tDocumentData *sticker,\n\t\t\tint32 count,\n\t\t\tconst QString &title,\n\t\t\tint titleWidth,\n\t\t\tData::StickersSetFlags flagsOverride,\n\t\t\tbool removed,\n\t\t\tint32 pixw,\n\t\t\tint32 pixh);\n\t\t~Row();\n\n\t\t[[nodiscard]] bool isRecentSet() const;\n\t\t[[nodiscard]] bool isMasksSet() const;\n\t\t[[nodiscard]] bool isEmojiSet() const;\n\t\t[[nodiscard]] bool isInstalled() const;\n\t\t[[nodiscard]] bool isUnread() const;\n\t\t[[nodiscard]] bool isArchived() const;\n\n\t\tconst not_null<StickersSet*> set;\n\t\tDocumentData *sticker = nullptr;\n\t\tstd::shared_ptr<Data::DocumentMedia> stickerMedia;\n\t\tstd::shared_ptr<StickersSetThumbnailView> thumbnailMedia;\n\t\tint32 count = 0;\n\t\tQString title;\n\t\tint titleWidth = 0;\n\t\tData::StickersSetFlags flagsOverride;\n\t\tbool removed = false;\n\t\tint32 pixw = 0;\n\t\tint32 pixh = 0;\n\t\tanim::value yadd;\n\t\tstd::unique_ptr<Ui::RippleAnimation> ripple;\n\t\tstd::unique_ptr<Lottie::SinglePlayer> lottie;\n\t\tMedia::Clip::ReaderPointer webm;\n\t};\n\tstruct MegagroupSet {\n\t\tinline bool operator==(const MegagroupSet &other) const {\n\t\t\treturn true;\n\t\t}\n\t\tinline bool operator!=(const MegagroupSet &other) const {\n\t\t\treturn false;\n\t\t}\n\t};\n\tusing SelectedRow = std::variant<v::null_t, MegagroupSet, int>;\n\tclass AddressField : public Ui::UsernameInput {\n\tpublic:\n\t\tusing UsernameInput::UsernameInput;\n\n\tprotected:\n\t\tvoid correctValue(\n\t\t\tconst QString &was,\n\t\t\tint wasCursor,\n\t\t\tQString &now,\n\t\t\tint &nowCursor) override;\n\n\t};\n\n\ttemplate <typename Check>\n\tStickersSetsOrder collectSets(Check check) const;\n\n\tvoid updateSelected();\n\tvoid checkGroupLevel(Fn<void()> done);\n\n\tvoid checkLoadMore();\n\tvoid updateScrollbarWidth();\n\tint getRowIndex(uint64 setId) const;\n\tvoid setRowRemoved(int index, bool removed);\n\n\tvoid setSelected(SelectedRow selected);\n\tvoid setActionDown(int newActionDown);\n\tvoid setPressed(SelectedRow pressed);\n\tvoid setup();\n\tQRect relativeButtonRect(bool removeButton, bool installedSet) const;\n\tvoid ensureRipple(\n\t\tconst style::RippleAnimation &st,\n\t\tQImage mask,\n\t\tbool removeButton,\n\t\tbool installedSet);\n\n\tbool shiftingAnimationCallback(crl::time now);\n\tvoid paintRow(Painter &p, not_null<Row*> row, int index);\n\tvoid paintRowThumbnail(Painter &p, not_null<Row*> row, int left);\n\tvoid paintFakeButton(Painter &p, not_null<Row*> row, int index);\n\tvoid clear();\n\tvoid updateCursor();\n\tvoid setActionSel(int32 actionSel);\n\tfloat64 aboveShadowOpacity() const;\n\tvoid validateLottieAnimation(not_null<Row*> row);\n\tvoid validateWebmAnimation(not_null<Row*> row);\n\tvoid validateAnimation(not_null<Row*> row);\n\tvoid updateRowThumbnail(not_null<Row*> row);\n\n\tvoid clipCallback(\n\t\tnot_null<Row*> row,\n\t\tMedia::Clip::Notification notification);\n\n\tvoid readVisibleSets();\n\n\tvoid updateControlsGeometry();\n\tvoid rebuildAppendSet(not_null<StickersSet*> set);\n\tvoid fillSetCover(not_null<StickersSet*> set, DocumentData **outSticker, int *outWidth, int *outHeight) const;\n\tint fillSetCount(not_null<StickersSet*> set) const;\n\t[[nodiscard]] Data::StickersSetFlags fillSetFlags(\n\t\tnot_null<StickersSet*> set) const;\n\tvoid rebuildMegagroupSet();\n\tvoid handleMegagroupSetAddressChange();\n\tvoid setMegagroupSelectedSet(const StickerSetIdentifier &set);\n\n\tint countMaxNameWidth(bool installedSet) const;\n\t[[nodiscard]] bool skipPremium() const;\n\n\tconst style::PeerListItem &_st;\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\n\tconst Section _section;\n\tconst bool _isInstalledTab;\n\n\tUi::RoundRect _buttonBgOver, _buttonBg, _inactiveButtonBg;\n\n\tint32 _rowHeight = 0;\n\n\tstd::vector<std::unique_ptr<Row>> _rows;\n\tstd::vector<std::unique_ptr<Row>> _oldRows;\n\tstd::vector<crl::time> _shiftingStartTimes;\n\tcrl::time _aboveShadowFadeStart = 0;\n\tanim::value _aboveShadowFadeOpacity;\n\tUi::Animations::Basic _shiftingAnimation;\n\n\tFn<void(uint64 setId)> _installSetCallback;\n\tFn<void(uint64 setId)> _removeSetCallback;\n\tFn<void()> _loadMoreCallback;\n\n\tint _visibleTop = 0;\n\tint _visibleBottom = 0;\n\tint _itemsTop = 0;\n\n\tint _actionSel = -1;\n\tint _actionDown = -1;\n\n\tQString _addText;\n\tint _addWidth = 0;\n\tQString _undoText;\n\tint _undoWidth = 0;\n\tQString _installedText;\n\tint _installedWidth = 0;\n\n\tQPoint _mouse;\n\tbool _inDragArea = false;\n\tSelectedRow _selected;\n\tSelectedRow _pressed;\n\tQPoint _dragStart;\n\tint _started = -1;\n\tint _dragging = -1;\n\tint _above = -1;\n\trpl::event_stream<int> _draggingScrollDelta;\n\n\trpl::event_stream<int> _scrollsToY;\n\n\tint _minHeight = 0;\n\n\tint _scrollbar = 0;\n\tChannelData *_megagroupSet = nullptr;\n\tbool _megagroupSetEmoji = false;\n\tbool _checkingGroupLevel = false;\n\tStickerSetIdentifier _megagroupSetInput;\n\tstd::unique_ptr<Row> _megagroupSelectedSet;\n\tobject_ptr<AddressField> _megagroupSetField = { nullptr };\n\tobject_ptr<Ui::PlainShadow> _megagroupSelectedShadow = { nullptr };\n\tobject_ptr<Ui::CrossButton> _megagroupSelectedRemove = { nullptr };\n\tobject_ptr<Ui::BoxContentDivider> _megagroupDivider = { nullptr };\n\tobject_ptr<Ui::FlatLabel> _megagroupSubTitle = { nullptr };\n\tbase::Timer _megagroupSetAddressChangedTimer;\n\tmtpRequestId _megagroupSetRequestId = 0;\n\n};\n\nStickersBox::CounterWidget::CounterWidget(\n\tQWidget *parent,\n\trpl::producer<int> count)\n: RpWidget(parent) {\n\tsetAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t_st.sizeId = Dialogs::Ui::UnreadBadgeSize::StickersBox;\n\t_st.textTop = st::stickersFeaturedBadgeTextTop;\n\t_st.size = st::stickersFeaturedBadgeSize;\n\t_st.padding = st::stickersFeaturedBadgePadding;\n\t_st.font = st::stickersFeaturedBadgeFont;\n\n\tstd::move(\n\t\tcount\n\t) | rpl::on_next([=](int count) {\n\t\tsetCounter(count);\n\t\tupdate();\n\t}, lifetime());\n}\n\nvoid StickersBox::CounterWidget::setCounter(int counter) {\n\t_text = (counter > 0) ? QString::number(counter) : QString();\n\tauto dummy = QImage(1, 1, QImage::Format_ARGB32_Premultiplied);\n\tauto p = QPainter(&dummy);\n\n\tconst auto badge = Ui::PaintUnreadBadge(p, _text, 0, 0, _st);\n\n\tresize(badge.width(), st::stickersFeaturedBadgeSize);\n}\n\nvoid StickersBox::CounterWidget::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tif (!_text.isEmpty()) {\n\t\tconst auto unreadRight = rtl() ? 0 : width();\n\t\tconst auto unreadTop = 0;\n\t\tUi::PaintUnreadBadge(p, _text, unreadRight, unreadTop, _st);\n\t}\n}\n\ntemplate <typename ...Args>\nStickersBox::Tab::Tab(int index, Args&&... args)\n: _index(index)\n, _widget(std::forward<Args>(args)...)\n, _weak(_widget) {\n}\n\nobject_ptr<StickersBox::Inner> StickersBox::Tab::takeWidget() {\n\treturn std::move(_widget);\n}\n\nvoid StickersBox::Tab::returnWidget(object_ptr<Inner> widget) {\n\t_widget = std::move(widget);\n\tAssert(_widget == _weak);\n}\n\nStickersBox::Inner *StickersBox::Tab::widget() const {\n\treturn _weak;\n}\n\nint StickersBox::Tab::index() const {\n\treturn _index;\n}\n\nvoid StickersBox::Tab::saveScrollTop() {\n\t_scrollTop = widget()->getVisibleTop();\n}\n\nStickersBox::StickersBox(\n\tQWidget*,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tSection section,\n\tbool masks)\n: _show(std::move(show))\n, _session(&_show->session())\n, _api(&_session->mtp())\n, _tabs(this, st::stickersTabs)\n, _unreadBadge(\n\tthis,\n\t_session->data().stickers().featuredSetsUnreadCountValue())\n, _section(section)\n, _isMasks(masks)\n, _isEmoji(false)\n, _installed(_isMasks ? Tab() : Tab(0, this, _show, Section::Installed))\n, _masks(_isMasks ? Tab(0, this, _show, Section::Masks) : Tab())\n, _featured(_isMasks ? Tab() : Tab(1, this, _show, Section::Featured))\n, _archived((_isMasks ? 1 : 2), this, _show, Section::Archived) {\n\t_tabs->setRippleTopRoundRadius(st::boxRadius);\n}\n\nStickersBox::StickersBox(\n\tQWidget*,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<ChannelData*> megagroup,\n\tbool isEmoji)\n: _show(std::move(show))\n, _session(&_show->session())\n, _api(&_session->mtp())\n, _section(Section::Installed)\n, _isMasks(false)\n, _isEmoji(isEmoji)\n, _installed(0, this, _show, megagroup, isEmoji)\n, _megagroupSet(megagroup) {\n\t_installed.widget()->scrollsToY(\n\t) | rpl::on_next([=](int y) {\n\t\tscrollToY(y);\n\t}, lifetime());\n}\n\nStickersBox::StickersBox(\n\tQWidget*,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst QVector<MTPStickerSetCovered> &attachedSets)\n: _show(std::move(show))\n, _session(&_show->session())\n, _api(&_session->mtp())\n, _section(Section::Attached)\n, _isMasks(false)\n, _isEmoji(false)\n, _attached(0, this, _show, Section::Attached)\n, _attachedType(Data::StickersType::Stickers)\n, _attachedSets(attachedSets) {\n}\n\nStickersBox::StickersBox(\n\tQWidget*,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst std::vector<StickerSetIdentifier> &emojiSets)\n: _show(std::move(show))\n, _session(&_show->session())\n, _api(&_session->mtp())\n, _section(Section::Attached)\n, _isMasks(false)\n, _isEmoji(true)\n, _attached(0, this, _show, Section::Attached)\n, _attachedType(Data::StickersType::Emoji)\n, _emojiSets(emojiSets) {\n}\n\nMain::Session &StickersBox::session() const {\n\treturn *_session;\n}\n\nvoid StickersBox::showAttachedStickers() {\n\tconst auto stickers = &session().data().stickers();\n\n\tauto addedSet = false;\n\tconst auto add = [&](not_null<StickersSet*> set) {\n\t\tif (_attached.widget()->appendSet(set)) {\n\t\t\taddedSet = true;\n\t\t\tif (set->stickers.isEmpty()\n\t\t\t\t|| (set->flags & SetFlag::NotLoaded)) {\n\t\t\t\tsession().api().scheduleStickerSetRequest(\n\t\t\t\t\tset->id,\n\t\t\t\t\tset->accessHash);\n\t\t\t}\n\t\t}\n\t};\n\tfor (const auto &set : _attachedSets) {\n\t\tadd(stickers->feedSet(set));\n\t}\n\tfor (const auto &setId : _emojiSets) {\n\t\tconst auto i = stickers->sets().find(setId.id);\n\t\tif (i != end(stickers->sets())) {\n\t\t\tadd(i->second.get());\n\t\t}\n\t}\n\tif (addedSet) {\n\t\t_attached.widget()->updateSize();\n\t}\n\n\tif (_section == Section::Attached && addedSet) {\n\t\tsession().api().requestStickerSets();\n\t}\n}\n\nvoid StickersBox::getArchivedDone(\n\t\tconst MTPmessages_ArchivedStickers &result,\n\t\tuint64 offsetId) {\n\t_archivedRequestId = 0;\n\t_archivedLoaded = true;\n\tif (result.type() != mtpc_messages_archivedStickers) {\n\t\treturn;\n\t}\n\n\tauto &stickers = result.c_messages_archivedStickers();\n\tauto &archived = archivedSetsOrderRef();\n\tif (offsetId) {\n\t\tauto index = archived.indexOf(offsetId);\n\t\tif (index >= 0) {\n\t\t\tarchived = archived.mid(0, index + 1);\n\t\t}\n\t} else {\n\t\tarchived.clear();\n\t}\n\n\tauto addedSet = false;\n\tauto changedSets = false;\n\tfor (const auto &data : stickers.vsets().v) {\n\t\tconst auto set = session().data().stickers().feedSet(data);\n\t\tconst auto index = archived.indexOf(set->id);\n\t\tif (archived.isEmpty() || index != archived.size() - 1) {\n\t\t\tchangedSets = true;\n\t\t\tif (index >= 0 && index < archived.size() - 1) {\n\t\t\t\tarchived.removeAt(index);\n\t\t\t}\n\t\t\tarchived.push_back(set->id);\n\t\t}\n\t\tif (_archived.widget()->appendSet(set)) {\n\t\t\taddedSet = true;\n\t\t\tif (set->flags & SetFlag::NotLoaded) {\n\t\t\t\tsession().api().scheduleStickerSetRequest(\n\t\t\t\t\tset->id,\n\t\t\t\t\tset->accessHash);\n\t\t\t}\n\t\t}\n\t}\n\tif (addedSet) {\n\t\t_archived.widget()->updateSize();\n\t} else {\n\t\t_allArchivedLoaded = stickers.vsets().v.isEmpty()\n\t\t\t|| (!changedSets && offsetId != 0);\n\t\tif (changedSets) {\n\t\t\tloadMoreArchived();\n\t\t}\n\t}\n\n\trefreshTabs();\n\t_someArchivedLoaded = true;\n\tif (_section == Section::Archived && addedSet) {\n\t\tsession().api().requestStickerSets();\n\t}\n}\n\nvoid StickersBox::prepare() {\n\tif (_section == Section::Installed) {\n\t\tif (_tabs) {\n\t\t\tif (_isMasks) {\n\t\t\t\tsession().local().readArchivedMasks();\n\t\t\t} else {\n\t\t\t\tsession().local().readArchivedStickers();\n\t\t\t}\n\t\t} else {\n\t\t\tsetTitle(_isEmoji\n\t\t\t\t? tr::lng_emoji_group_set()\n\t\t\t\t: tr::lng_stickers_group_set());\n\t\t}\n\t} else if (_section == Section::Archived) {\n\t\trequestArchivedSets();\n\t} else if (_section == Section::Attached) {\n\t\tsetTitle(_attachedType == Data::StickersType::Emoji\n\t\t\t? tr::lng_custom_emoji_used_sets()\n\t\t\t: tr::lng_stickers_attached_sets());\n\t}\n\tif (_tabs) {\n\t\tif (archivedSetsOrder().isEmpty()) {\n\t\t\tpreloadArchivedSets();\n\t\t}\n\t\tsetNoContentMargin(true);\n\t\t_tabs->sectionActivated(\n\t\t) | rpl::filter([=] {\n\t\t\treturn !_ignoreTabActivation;\n\t\t}) | rpl::on_next(\n\t\t\t[this] { switchTab(); },\n\t\t\tlifetime());\n\t\trefreshTabs();\n\t}\n\tif (_installed.widget() && _section != Section::Installed) {\n\t\t_installed.widget()->hide();\n\t}\n\tif (_masks.widget() && _section != Section::Masks) {\n\t\t_masks.widget()->hide();\n\t}\n\tif (_featured.widget() && _section != Section::Featured) {\n\t\t_featured.widget()->hide();\n\t}\n\tif (_archived.widget() && _section != Section::Archived) {\n\t\t_archived.widget()->hide();\n\t}\n\tif (_attached.widget() && _section != Section::Attached) {\n\t\t_attached.widget()->hide();\n\t}\n\n\t{\n\t\tconst auto installCallback = [=](uint64 setId) { installSet(setId); };\n\t\tconst auto markAsInstalledCallback = [=](uint64 setId) {\n\t\t\tif (_installed.widget()) {\n\t\t\t\t_installed.widget()->setRowRemovedBySetId(setId, false);\n\t\t\t}\n\t\t\tif (_featured.widget()) {\n\t\t\t\t_featured.widget()->setRowRemovedBySetId(setId, false);\n\t\t\t}\n\t\t};\n\t\tconst auto markAsRemovedCallback = [=](uint64 setId) {\n\t\t\tif (_installed.widget()) {\n\t\t\t\t_installed.widget()->setRowRemovedBySetId(setId, true);\n\t\t\t}\n\t\t\tif (_featured.widget()) {\n\t\t\t\t_featured.widget()->setRowRemovedBySetId(setId, true);\n\t\t\t}\n\t\t};\n\t\tif (const auto installed = _installed.widget()) {\n\t\t\tinstalled->setInstallSetCallback(markAsInstalledCallback);\n\t\t\tinstalled->setRemoveSetCallback(markAsRemovedCallback);\n\t\t}\n\t\tif (const auto featured = _featured.widget()) {\n\t\t\tfeatured->setInstallSetCallback([=](uint64 setId) {\n\t\t\t\tinstallCallback(setId);\n\t\t\t\tmarkAsInstalledCallback(setId);\n\t\t\t});\n\t\t\tfeatured->setRemoveSetCallback(markAsRemovedCallback);\n\t\t}\n\t\tif (const auto archived = _archived.widget()) {\n\t\t\tarchived->setInstallSetCallback(installCallback);\n\t\t\tarchived->setLoadMoreCallback([=] { loadMoreArchived(); });\n\t\t}\n\t\tif (const auto attached = _attached.widget()) {\n\t\t\tattached->setInstallSetCallback(installCallback);\n\t\t\tattached->setLoadMoreCallback([=] { showAttachedStickers(); });\n\t\t}\n\t}\n\n\tif (_megagroupSet) {\n\t\taddButton(tr::lng_settings_save(), [=] {\n\t\t\t_installed.widget()->saveGroupSet(crl::guard(this, [=] {\n\t\t\t\tcloseBox();\n\t\t\t}));\n\t\t});\n\t\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\t} else {\n\t\tconst auto close = _section == Section::Attached;\n\t\taddButton(\n\t\t\tclose ? tr::lng_close() : tr::lng_about_done(),\n\t\t\t[=] { closeBox(); });\n\t}\n\n\tif (_section == Section::Installed) {\n\t\t_tab = &_installed;\n\t} else if (_section == Section::Masks) {\n\t\t_tab = &_masks;\n\t} else if (_section == Section::Archived) {\n\t\t_tab = &_archived;\n\t} else if (_section == Section::Attached) {\n\t\t_tab = &_attached;\n\t} else { // _section == Section::Featured\n\t\t_tab = &_featured;\n\t}\n\tsetInnerWidget(_tab->takeWidget(), topSkip());\n\tsetDimensions(st::boxWideWidth, st::boxMaxListHeight);\n\n\tsession().data().stickers().updated(_isEmoji\n\t\t? Data::StickersType::Emoji\n\t\t: _isMasks\n\t\t? Data::StickersType::Masks\n\t\t: Data::StickersType::Stickers\n\t) | rpl::on_next([=] {\n\t\thandleStickersUpdated();\n\t}, lifetime());\n\n\tif (_isEmoji) {\n\t\tsession().api().updateCustomEmoji();\n\t} else if (_isMasks) {\n\t\tsession().api().updateMasks();\n\t} else {\n\t\tsession().api().updateStickers();\n\t}\n\n\tfor (const auto &widget : { _installed.widget(), _masks.widget() }) {\n\t\tif (widget) {\n\t\t\twidget->draggingScrollDelta(\n\t\t\t) | rpl::on_next([=](int delta) {\n\t\t\t\tscrollByDraggingDelta(delta);\n\t\t\t}, widget->lifetime());\n\t\t}\n\t}\n\tif (!_megagroupSet) {\n\t\tboxClosing() | rpl::on_next([=] {\n\t\t\tsaveChanges();\n\t\t}, lifetime());\n\t}\n\n\tif (_tabs) {\n\t\t_tabs->raise();\n\t\t_unreadBadge->raise();\n\t}\n\trebuildList();\n}\n\nvoid StickersBox::refreshTabs() {\n\tif (!_tabs) {\n\t\treturn;\n\t}\n\n\tauto &stickers = session().data().stickers();\n\n\t_tabIndices.clear();\n\tauto sections = std::vector<QString>();\n\tif (_installed.widget()) {\n\t\tsections.push_back(tr::lng_stickers_installed_tab(tr::now));\n\t\t_tabIndices.push_back(Section::Installed);\n\t}\n\tif (_masks.widget()) {\n\t\tsections.push_back(tr::lng_stickers_masks_tab(tr::now));\n\t\t_tabIndices.push_back(Section::Masks);\n\t}\n\tconst auto showFeatured = _featured.widget()\n\t\t&& (!stickers.featuredSetsOrder().isEmpty()\n\t\t\t|| _section == Section::Featured);\n\tif (showFeatured) {\n\t\tsections.push_back(tr::lng_stickers_featured_tab(tr::now));\n\t\t_tabIndices.push_back(Section::Featured);\n\t}\n\tconst auto showArchived = _archived.widget()\n\t\t&& (!archivedSetsOrder().isEmpty()\n\t\t\t|| _section == Section::Archived);\n\tif (showArchived) {\n\t\tsections.push_back(tr::lng_stickers_archived_tab(tr::now));\n\t\t_tabIndices.push_back(Section::Archived);\n\t}\n\t_tabs->setSections(sections);\n\tif ((_section == Section::Archived && !_tabIndices.contains(Section::Archived))\n\t\t|| (_section == Section::Featured && !_tabIndices.contains(Section::Featured))\n\t\t|| (_section == Section::Masks && !_tabIndices.contains(Section::Masks))) {\n\t\tswitchTab();\n\t} else {\n\t\t_ignoreTabActivation = true;\n\t\t_tabs->setActiveSectionFast(_tabIndices.indexOf(_section));\n\t\t_ignoreTabActivation = false;\n\t}\n\tupdateTabsGeometry();\n}\n\nvoid StickersBox::loadMoreArchived() {\n\tif (_section != Section::Archived\n\t\t|| _allArchivedLoaded\n\t\t|| _archivedRequestId) {\n\t\treturn;\n\t}\n\n\tuint64 lastId = 0;\n\tconst auto &order = archivedSetsOrder();\n\tconst auto &sets = session().data().stickers().sets();\n\tfor (auto setIt = order.cend(), e = order.cbegin(); setIt != e;) {\n\t\t--setIt;\n\t\tauto it = sets.find(*setIt);\n\t\tif (it != sets.cend()) {\n\t\t\tif (it->second->flags & SetFlag::Archived) {\n\t\t\t\tlastId = it->second->id;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto flags = _isMasks\n\t\t? MTPmessages_GetArchivedStickers::Flag::f_masks\n\t\t: MTPmessages_GetArchivedStickers::Flags(0);\n\t_archivedRequestId = _api.request(MTPmessages_GetArchivedStickers(\n\t\tMTP_flags(flags),\n\t\tMTP_long(lastId),\n\t\tMTP_int(kArchivedLimitPerPage)\n\t)).done([=](const MTPmessages_ArchivedStickers &result) {\n\t\tgetArchivedDone(result, lastId);\n\t}).send();\n}\n\nvoid StickersBox::paintEvent(QPaintEvent *e) {\n\tBoxContent::paintEvent(e);\n\n\tPainter p(this);\n\n\tif (_slideAnimation) {\n\t\t_slideAnimation->paintFrame(p, 0, topSkip(), width());\n\t\tif (!_slideAnimation->animating()) {\n\t\t\t_slideAnimation.reset();\n\t\t\tsetInnerVisible(true);\n\t\t\tupdate();\n\t\t}\n\t}\n}\n\nvoid StickersBox::updateTabsGeometry() {\n\tif (!_tabs) return;\n\n\tconst auto maxTabs = _isMasks ? 2 : 3;\n\n\t_tabs->resizeToWidth(_tabIndices.size() * width() / maxTabs);\n\t_unreadBadge->setVisible(_tabIndices.contains(Section::Featured));\n\n\tsetInnerTopSkip(topSkip());\n\n\tauto featuredLeft = width() / maxTabs;\n\tauto featuredRight = 2 * width() / maxTabs;\n\tauto featuredTextWidth = st::stickersTabs.labelStyle.font->width(tr::lng_stickers_featured_tab(tr::now));\n\tauto featuredTextRight = featuredLeft + (featuredRight - featuredLeft - featuredTextWidth) / 2 + featuredTextWidth;\n\tauto unreadBadgeLeft = featuredTextRight - st::stickersFeaturedBadgeSkip;\n\tauto unreadBadgeTop = st::stickersFeaturedBadgeTop;\n\tif (unreadBadgeLeft + _unreadBadge->width() > featuredRight) {\n\t\tunreadBadgeLeft = featuredRight - _unreadBadge->width();\n\t}\n\t_unreadBadge->moveToLeft(unreadBadgeLeft, unreadBadgeTop);\n\n\t_tabs->moveToLeft(0, 0);\n}\n\nint StickersBox::topSkip() const {\n\treturn _tabs ? (_tabs->height() - st::lineWidth) : 0;\n}\n\nvoid StickersBox::switchTab() {\n\tif (!_tabs) return;\n\n\tauto tab = _tabs->activeSection();\n\tAssert(tab >= 0 && tab < _tabIndices.size());\n\tauto newSection = _tabIndices[tab];\n\n\tauto newTab = _tab;\n\tif (newSection == Section::Installed) {\n\t\tnewTab = &_installed;\n\t} else if (newSection == Section::Featured) {\n\t\tnewTab = &_featured;\n\t} else if (newSection == Section::Archived) {\n\t\tnewTab = &_archived;\n\t\trequestArchivedSets();\n\t} else if (newSection == Section::Masks) {\n\t\tnewTab = &_masks;\n\t\tsession().api().updateMasks();\n\t}\n\tif (_tab == newTab) {\n\t\tscrollToY(0);\n\t\treturn;\n\t}\n\n\tif (_tab == &_installed) {\n\t\t_localOrder = _tab->widget()->fullOrder();\n\t\t_localRemoved = _tab->widget()->removedSets();\n\t}\n\tauto wasCache = grabContentCache();\n\tauto wasIndex = _tab->index();\n\t_tab->saveScrollTop();\n\tauto widget = takeInnerWidget<Inner>();\n\twidget->setParent(this);\n\twidget->hide();\n\t_tab->returnWidget(std::move(widget));\n\t_tab = newTab;\n\t_section = newSection;\n\tsetInnerWidget(_tab->takeWidget(), topSkip());\n\t_tabs->raise();\n\t_unreadBadge->raise();\n\t_tab->widget()->show();\n\trebuildList();\n\tscrollToY(_tab->scrollTop());\n\tsetInnerVisible(true);\n\tauto nowCache = grabContentCache();\n\tauto nowIndex = _tab->index();\n\n\t_slideAnimation = std::make_unique<Ui::SlideAnimation>();\n\t_slideAnimation->setSnapshots(std::move(wasCache), std::move(nowCache));\n\tauto slideLeft = wasIndex > nowIndex;\n\t_slideAnimation->start(slideLeft, [=] { update(); }, st::slideDuration);\n\tsetInnerVisible(false);\n\n\tsetFocus();\n\tupdate();\n}\n\nQPixmap StickersBox::grabContentCache() {\n\t_tabs->hide();\n\tauto result = grabInnerCache();\n\t_tabs->show();\n\treturn result;\n}\n\nstd::array<StickersBox::Inner*, 5> StickersBox::widgets() const {\n\treturn {\n\t\t_installed.widget(),\n\t\t_featured.widget(),\n\t\t_archived.widget(),\n\t\t_attached.widget(),\n\t\t_masks.widget()\n\t};\n}\n\nvoid StickersBox::installSet(uint64 setId) {\n\tconst auto &sets = session().data().stickers().sets();\n\tconst auto it = sets.find(setId);\n\tif (it == sets.cend()) {\n\t\trebuildList();\n\t\treturn;\n\t}\n\n\tconst auto set = it->second.get();\n\tif (_localRemoved.contains(setId)) {\n\t\t_localRemoved.removeOne(setId);\n\t\tfor (const auto &widget : widgets()) {\n\t\t\tif (widget) {\n\t\t\t\twidget->setRemovedSets(_localRemoved);\n\t\t\t}\n\t\t}\n\t}\n\tif (!(set->flags & SetFlag::Installed)\n\t\t|| (set->flags & SetFlag::Archived)) {\n\t\t_api.request(MTPmessages_InstallStickerSet(\n\t\t\tset->mtpInput(),\n\t\t\tMTP_boolFalse()\n\t\t)).done([=](const MTPmessages_StickerSetInstallResult &result) {\n\t\t\tinstallDone(result);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tinstallFail(error, setId);\n\t\t}).send();\n\n\t\tsession().data().stickers().installLocally(setId);\n\t}\n}\n\nvoid StickersBox::installDone(\n\t\tconst MTPmessages_StickerSetInstallResult &result) const {\n\tif (result.type() == mtpc_messages_stickerSetInstallResultArchive) {\n\t\tsession().data().stickers().applyArchivedResult(\n\t\t\tresult.c_messages_stickerSetInstallResultArchive());\n\t}\n}\n\nvoid StickersBox::installFail(const MTP::Error &error, uint64 setId) {\n\tconst auto &sets = session().data().stickers().sets();\n\tconst auto it = sets.find(setId);\n\tif (it == sets.cend()) {\n\t\trebuildList();\n\t} else {\n\t\tsession().data().stickers().undoInstallLocally(setId);\n\t}\n}\n\nvoid StickersBox::preloadArchivedSets() {\n\tif (!_tabs) {\n\t\treturn;\n\t}\n\tif (!_archivedRequestId) {\n\t\tconst auto flags = _isMasks\n\t\t\t? MTPmessages_GetArchivedStickers::Flag::f_masks\n\t\t\t: MTPmessages_GetArchivedStickers::Flags(0);\n\t\t_archivedRequestId = _api.request(MTPmessages_GetArchivedStickers(\n\t\t\tMTP_flags(flags),\n\t\t\tMTP_long(0),\n\t\t\tMTP_int(kArchivedLimitFirstRequest)\n\t\t)).done([=](const MTPmessages_ArchivedStickers &result) {\n\t\t\tgetArchivedDone(result, 0);\n\t\t}).send();\n\t}\n}\n\nvoid StickersBox::requestArchivedSets() {\n\t// Reload the archived list.\n\tif (!_archivedLoaded) {\n\t\tpreloadArchivedSets();\n\t}\n\n\tconst auto &sets = session().data().stickers().sets();\n\tconst auto &order = archivedSetsOrder();\n\tfor (const auto setId : order) {\n\t\tauto it = sets.find(setId);\n\t\tif (it != sets.cend()) {\n\t\t\tconst auto set = it->second.get();\n\t\t\tif (set->stickers.isEmpty()\n\t\t\t\t&& (set->flags & SetFlag::NotLoaded)) {\n\t\t\t\tsession().api().scheduleStickerSetRequest(\n\t\t\t\t\tsetId,\n\t\t\t\t\tset->accessHash);\n\t\t\t}\n\t\t}\n\t}\n\tsession().api().requestStickerSets();\n}\n\nvoid StickersBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\tif (_tabs) {\n\t\tupdateTabsGeometry();\n\t}\n\tif (_titleShadow) {\n\t\t_titleShadow->setGeometry(0, 0, width(), st::lineWidth);\n\t}\n\tfor (const auto &widget : widgets()) {\n\t\tif (widget) {\n\t\t\twidget->resize(width(), widget->height());\n\t\t}\n\t}\n}\n\nvoid StickersBox::handleStickersUpdated() {\n\tif (_section == Section::Installed\n\t\t|| _section == Section::Featured\n\t\t|| _section == Section::Masks) {\n\t\trebuildList();\n\t} else {\n\t\t_tab->widget()->updateRows();\n\t}\n\tif (archivedSetsOrder().isEmpty()) {\n\t\tpreloadArchivedSets();\n\t} else {\n\t\trefreshTabs();\n\t}\n}\n\nvoid StickersBox::rebuildList(Tab *tab) {\n\tif (_section == Section::Attached) {\n\t\treturn;\n\t}\n\tif (!tab) {\n\t\ttab = _tab;\n\t}\n\n\tif ((tab == &_installed) || (tab == &_masks) || (_tab == &_featured)) {\n\t\t_localOrder = tab->widget()->fullOrder();\n\t\t_localRemoved = tab->widget()->removedSets();\n\t}\n\ttab->widget()->rebuild(_isMasks);\n\tif ((tab == &_installed) || (tab == &_masks) || (_tab == &_featured)) {\n\t\ttab->widget()->setFullOrder(_localOrder);\n\t}\n\ttab->widget()->setRemovedSets(_localRemoved);\n}\n\nvoid StickersBox::saveChanges() {\n\tconst auto installed = _installed.widget();\n\tconst auto masks = _masks.widget();\n\n\t// Make sure that our changes in other tabs are applied in the Installed tab.\n\tif (installed) {\n\t\trebuildList(&_installed);\n\t}\n\tif (masks) {\n\t\trebuildList(&_masks);\n\t}\n\n\tif (_someArchivedLoaded) {\n\t\tif (_isMasks) {\n\t\t\tsession().local().writeArchivedMasks();\n\t\t} else {\n\t\t\tsession().local().writeArchivedStickers();\n\t\t}\n\t}\n\tif (installed) {\n\t\tsession().api().saveStickerSets(\n\t\t\tinstalled->order(),\n\t\t\tinstalled->removedSets(),\n\t\t\tData::StickersType::Stickers);\n\t}\n\tif (masks) {\n\t\tsession().api().saveStickerSets(\n\t\t\tmasks->order(),\n\t\t\tmasks->removedSets(),\n\t\t\tData::StickersType::Masks);\n\t}\n}\n\nvoid StickersBox::setInnerFocus() {\n\tif (_megagroupSet) {\n\t\t_installed.widget()->setInnerFocus();\n\t} else {\n\t\tBoxContent::setInnerFocus();\n\t}\n}\n\nconst Data::StickersSetsOrder &StickersBox::archivedSetsOrder() const {\n\treturn !_isMasks\n\t\t? session().data().stickers().archivedSetsOrder()\n\t\t: session().data().stickers().archivedMaskSetsOrder();\n}\n\nData::StickersSetsOrder &StickersBox::archivedSetsOrderRef() const {\n\treturn !_isMasks\n\t\t? session().data().stickers().archivedSetsOrderRef()\n\t\t: session().data().stickers().archivedMaskSetsOrderRef();\n}\n\nStickersBox::~StickersBox() = default;\n\nStickersBox::Inner::Row::Row(\n\tnot_null<StickersSet*> set,\n\tDocumentData *sticker,\n\tint32 count,\n\tconst QString &title,\n\tint titleWidth,\n\tData::StickersSetFlags flagsOverride,\n\tbool removed,\n\tint32 pixw,\n\tint32 pixh)\n: set(set)\n, sticker(sticker)\n, count(count)\n, title(title)\n, titleWidth(titleWidth)\n, flagsOverride(flagsOverride)\n, removed(removed)\n, pixw(pixw)\n, pixh(pixh) {\n\t++set->locked;\n}\n\nStickersBox::Inner::Row::~Row() {\n\tif (!--set->locked) {\n\t\tconst auto installed = !!(set->flags & SetFlag::Installed);\n\t\tconst auto featured = !!(set->flags & SetFlag::Featured);\n\t\tconst auto special = !!(set->flags & SetFlag::Special);\n\t\tconst auto archived = !!(set->flags & SetFlag::Archived);\n\t\tconst auto emoji = !!(set->flags & SetFlag::Emoji);\n\t\tif (!installed && !featured && !special && !archived && !emoji) {\n\t\t\tauto &sets = set->owner().stickers().setsRef();\n\t\t\tif (const auto i = sets.find(set->id); i != end(sets)) {\n\t\t\t\tsets.erase(i);\n\t\t\t}\n\t\t}\n\t}\n}\n\nbool StickersBox::Inner::Row::isRecentSet() const {\n\treturn (set->id == Data::Stickers::CloudRecentSetId)\n\t\t|| (set->id == Data::Stickers::CloudRecentAttachedSetId);\n}\n\nbool StickersBox::Inner::Row::isMasksSet() const {\n\treturn (set->type() == Data::StickersType::Masks);\n}\n\nbool StickersBox::Inner::Row::isEmojiSet() const {\n\treturn (set->type() == Data::StickersType::Emoji);\n}\n\nbool StickersBox::Inner::Row::isInstalled() const {\n\treturn (flagsOverride & SetFlag::Installed);\n}\n\nbool StickersBox::Inner::Row::isUnread() const {\n\treturn (flagsOverride & SetFlag::Unread);\n}\n\nbool StickersBox::Inner::Row::isArchived() const {\n\treturn (flagsOverride & SetFlag::Archived);\n}\n\nStickersBox::Inner::Inner(\n\tQWidget *parent,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tStickersBox::Section section)\n: RpWidget(parent)\n, _st(st::stickersRowItem)\n, _show(std::move(show))\n, _session(&_show->session())\n, _api(&_session->mtp())\n, _section(section)\n, _isInstalledTab(_section == Section::Installed\n\t|| _section == Section::Masks)\n, _buttonBgOver(\n\tImageRoundRadius::Large,\n\t(_isInstalledTab\n\t\t? st::stickersUndoRemove\n\t\t: st::stickersTrendingAdd).textBgOver)\n, _buttonBg(\n\tImageRoundRadius::Large,\n\t(_isInstalledTab\n\t\t? st::stickersUndoRemove\n\t\t: st::stickersTrendingAdd).textBg)\n, _inactiveButtonBg(\n\tImageRoundRadius::Large,\n\tst::stickersTrendingInstalled.textBg)\n, _rowHeight(_st.height)\n, _shiftingAnimation([=](crl::time now) {\n\treturn shiftingAnimationCallback(now);\n})\n, _itemsTop(st::lineWidth)\n, _addText(tr::lng_stickers_featured_add(tr::now))\n, _addWidth(st::stickersTrendingAdd.style.font->width(_addText))\n, _undoText(tr::lng_stickers_return(tr::now))\n, _undoWidth(st::stickersUndoRemove.style.font->width(_undoText))\n, _installedText(tr::lng_stickers_featured_installed(tr::now))\n, _installedWidth(st::stickersTrendingInstalled.style.font->width(\n\t\t_installedText)) {\n\tsetup();\n}\n\nStickersBox::Inner::Inner(\n\tQWidget *parent,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<ChannelData*> megagroup,\n\tbool isEmoji)\n: RpWidget(parent)\n, _st(st::stickersRowItem)\n, _show(std::move(show))\n, _session(&_show->session())\n, _api(&_session->mtp())\n, _section(StickersBox::Section::Installed)\n, _isInstalledTab(_section == Section::Installed\n\t|| _section == Section::Masks)\n, _buttonBgOver(\n\tImageRoundRadius::Large,\n\t(_isInstalledTab\n\t\t? st::stickersUndoRemove\n\t\t: st::stickersTrendingAdd).textBgOver)\n, _buttonBg(\n\tImageRoundRadius::Large,\n\t(_isInstalledTab\n\t\t? st::stickersUndoRemove\n\t\t: st::stickersTrendingAdd).textBg)\n, _inactiveButtonBg(\n\tImageRoundRadius::Large,\n\tst::stickersTrendingInstalled.textBg)\n, _rowHeight(_st.height)\n, _shiftingAnimation([=](crl::time now) {\n\treturn shiftingAnimationCallback(now);\n})\n, _itemsTop(st::lineWidth)\n, _megagroupSet(megagroup)\n, _megagroupSetEmoji(isEmoji)\n, _megagroupSetInput(isEmoji\n\t? _megagroupSet->mgInfo->emojiSet\n\t: _megagroupSet->mgInfo->stickerSet)\n, _megagroupSetField(\n\tthis,\n\tst::groupStickersField,\n\trpl::single(isEmoji ? u\"emojipack\"_q : u\"stickerset\"_q),\n\tQString(),\n\t_session->createInternalLink(QString()))\n, _megagroupDivider(this)\n, _megagroupSubTitle(\n\t\tthis,\n\t\t(isEmoji\n\t\t\t? tr::lng_emoji_group_from_your\n\t\t\t: tr::lng_stickers_group_from_your)(tr::now),\n\t\tst::boxTitle) {\n\t_megagroupSetField->setLinkPlaceholder(\n\t\t_session->createInternalLink(\n\t\t\tisEmoji ? u\"addemoji/\"_q : u\"addstickers/\"_q));\n\t_megagroupSetField->setPlaceholderHidden(false);\n\t_megagroupSetAddressChangedTimer.setCallback([this] {\n\t\thandleMegagroupSetAddressChange();\n\t});\n\tconnect(\n\t\t_megagroupSetField,\n\t\t&Ui::MaskedInputField::changed,\n\t\t[=] {\n\t\t\t_megagroupSetAddressChangedTimer.callOnce(\n\t\t\t\tkHandleMegagroupSetAddressChangeTimeout);\n\t\t});\n\tconnect(\n\t\t_megagroupSetField,\n\t\t&Ui::MaskedInputField::submitted,\n\t\t[=] {\n\t\t\t_megagroupSetAddressChangedTimer.cancel();\n\t\t\thandleMegagroupSetAddressChange();\n\t\t});\n\n\tsetup();\n}\n\nMain::Session &StickersBox::Inner::session() const {\n\treturn *_session;\n}\n\nvoid StickersBox::Inner::setup() {\n\tsession().downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t\treadVisibleSets();\n\t}, lifetime());\n\n\tsetMouseTracking(true);\n}\n\nvoid StickersBox::Inner::setInnerFocus() {\n\tif (_megagroupSetField) {\n\t\t_megagroupSetField->setFocusFast();\n\t}\n}\n\nvoid StickersBox::Inner::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tauto clip = e->rect();\n\tp.fillRect(clip, st::boxBg);\n\tp.setClipRect(clip);\n\n\tif (_megagroupSelectedSet) {\n\t\tauto setTop = _megagroupDivider->y() - _rowHeight;\n\t\tp.translate(0, setTop);\n\t\tpaintRow(p, _megagroupSelectedSet.get(), -1);\n\t\tp.translate(0, -setTop);\n\t}\n\n\tauto y = _itemsTop;\n\tif (_rows.empty()) {\n\t\tp.setFont(st::noContactsFont);\n\t\tp.setPen(st::noContactsColor);\n\t\tp.drawText(QRect(0, y, width(), st::noContactsHeight), tr::lng_contacts_loading(tr::now), style::al_center);\n\t} else {\n\t\tp.translate(0, _itemsTop);\n\n\t\tint32 yFrom = clip.y() - _itemsTop, yTo = clip.y() + clip.height() - _itemsTop;\n\t\tint32 from = floorclamp(yFrom - _rowHeight, _rowHeight, 0, _rows.size());\n\t\tint32 to = ceilclamp(yTo + _rowHeight, _rowHeight, 0, _rows.size());\n\t\tp.translate(0, from * _rowHeight);\n\t\tfor (int32 i = from; i < to; ++i) {\n\t\t\tif (i != _above) {\n\t\t\t\tpaintRow(p, _rows[i].get(), i);\n\t\t\t}\n\t\t\tp.translate(0, _rowHeight);\n\t\t}\n\t\tif (from <= _above && _above < to) {\n\t\t\tp.translate(0, (_above - to) * _rowHeight);\n\t\t\tpaintRow(p, _rows[_above].get(), _above);\n\t\t}\n\t}\n}\n\nvoid StickersBox::Inner::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid StickersBox::Inner::updateControlsGeometry() {\n\tif (_megagroupSet) {\n\t\tauto top = st::groupStickersFieldPadding.top();\n\t\tauto fieldLeft = st::boxTitlePosition.x();\n\t\t_megagroupSetField->setGeometryToLeft(fieldLeft, top, width() - fieldLeft - st::groupStickersFieldPadding.right(), _megagroupSetField->height());\n\t\ttop += _megagroupSetField->height() + st::groupStickersFieldPadding.bottom();\n\t\tif (_megagroupSelectedRemove) {\n\t\t\t_megagroupSelectedShadow->setGeometryToLeft(0, top, width(), st::lineWidth);\n\t\t\ttop += st::lineWidth;\n\t\t\t_megagroupSelectedRemove->moveToRight(st::groupStickersRemovePosition.x(), top + st::groupStickersRemovePosition.y());\n\t\t\ttop += _rowHeight;\n\t\t}\n\t\t_megagroupDivider->setGeometryToLeft(0, top, width(), _megagroupDivider->height());\n\t\ttop += _megagroupDivider->height();\n\t\t_megagroupSubTitle->resizeToNaturalWidth(width() - 2 * st::boxTitlePosition.x());\n\t\t_megagroupSubTitle->moveToLeft(st::boxTitlePosition.x(), top + st::boxTitlePosition.y());\n\t}\n}\n\nQRect StickersBox::Inner::relativeButtonRect(\n\t\tbool removeButton,\n\t\tbool installedSet) const {\n\tauto buttonw = st::stickersRemove.width;\n\tauto buttonh = st::stickersRemove.height;\n\tauto buttonshift = st::stickersRemoveSkip;\n\tif (!removeButton) {\n\t\tconst auto &st = installedSet\n\t\t\t? st::stickersTrendingInstalled\n\t\t\t: _isInstalledTab\n\t\t\t? st::stickersUndoRemove\n\t\t\t: st::stickersTrendingAdd;\n\t\tconst auto textWidth = installedSet\n\t\t\t? _installedWidth\n\t\t\t: _isInstalledTab\n\t\t\t? _undoWidth\n\t\t\t: _addWidth;\n\t\tbuttonw = textWidth - st.width;\n\t\tbuttonh = st.height;\n\t\tbuttonshift = 0;\n\t}\n\tauto buttonx = width() - st::contactsPadding.right() - buttonw + buttonshift;\n\tauto buttony = (_st.height - buttonh) / 2;\n\treturn QRect(buttonx, buttony, buttonw, buttonh);\n}\n\nvoid StickersBox::Inner::paintRow(Painter &p, not_null<Row*> row, int index) {\n\tauto xadd = 0, yadd = qRound(row->yadd.current());\n\tif (xadd || yadd) p.translate(xadd, yadd);\n\n\tif (_megagroupSet) {\n\t\tauto selectedIndex = [&] {\n\t\t\tif (auto index = std::get_if<int>(&_selected)) {\n\t\t\t\treturn *index;\n\t\t\t}\n\t\t\treturn -1;\n\t\t}();\n\t\tif (index >= 0 && index == selectedIndex) {\n\t\t\tp.fillRect(0, 0, width(), _rowHeight, _st.button.textBgOver);\n\t\t\tif (row->ripple) {\n\t\t\t\trow->ripple->paint(p, 0, 0, width());\n\t\t\t}\n\t\t}\n\t}\n\n\tif (_isInstalledTab) {\n\t\tif (index >= 0 && index == _above) {\n\t\t\tauto current = _aboveShadowFadeOpacity.current();\n\t\t\tif (_started >= 0) {\n\t\t\t\tauto reachedOpacity = aboveShadowOpacity();\n\t\t\t\tif (reachedOpacity > current) {\n\t\t\t\t\t_aboveShadowFadeOpacity = anim::value(reachedOpacity, reachedOpacity);\n\t\t\t\t\tcurrent = reachedOpacity;\n\t\t\t\t}\n\t\t\t}\n\t\t\tauto rect = myrtlrect(_st.photoPosition.x() / 2, _st.photoPosition.y() / 2, width() - _st.photoPosition.x() - _scrollbar, _rowHeight - _st.photoPosition.y());\n\t\t\tp.setOpacity(current);\n\t\t\tUi::Shadow::paint(p, rect, width(), st::boxRoundShadow);\n\t\t\tp.setOpacity(1);\n\n\t\t\tUi::FillRoundRect(p, rect, st::boxBg, Ui::BoxCorners);\n\n\t\t\tp.setOpacity(1. - current);\n\t\t\tpaintFakeButton(p, row, index);\n\t\t\tp.setOpacity(1.);\n\t\t} else if (!_megagroupSet) {\n\t\t\tpaintFakeButton(p, row, index);\n\t\t}\n\t} else if (!_megagroupSet) {\n\t\tpaintFakeButton(p, row, index);\n\t}\n\n\tif (row->removed && _isInstalledTab) {\n\t\tp.setOpacity(st::stickersRowDisabledOpacity);\n\t}\n\n\tauto stickerskip = 0;\n\n\tif (!_megagroupSet && _isInstalledTab) {\n\t\tstickerskip += st::stickersReorderIcon.width() + st::stickersReorderSkip;\n\t\tif (!row->isRecentSet()) {\n\t\t\tst::stickersReorderIcon.paint(p, _st.photoPosition.x(), (_rowHeight - st::stickersReorderIcon.height()) / 2, width());\n\t\t}\n\t}\n\n\tif (row->sticker) {\n\t\tpaintRowThumbnail(p, row, stickerskip + _st.photoPosition.x());\n\t}\n\n\tint namex = stickerskip + _st.namePosition.x();\n\tint namey = _st.namePosition.y();\n\n\tint statusx = stickerskip + _st.statusPosition.x();\n\tint statusy = _st.statusPosition.y();\n\n\tp.setFont(st::contactsNameStyle.font);\n\tp.setPen(_st.nameFg);\n\tp.drawTextLeft(namex, namey, width(), row->title, row->titleWidth);\n\n\tif (row->isUnread()) {\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::stickersFeaturedUnreadBg);\n\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(style::rtlrect(namex + row->titleWidth + st::stickersFeaturedUnreadSkip, namey + st::stickersFeaturedUnreadTop, st::stickersFeaturedUnreadSize, st::stickersFeaturedUnreadSize, width()));\n\t\t}\n\t}\n\n\tconst auto statusText = (row->count == 0)\n\t\t? tr::lng_contacts_loading(tr::now)\n\t\t: row->isEmojiSet()\n\t\t? tr::lng_custom_emoji_count(tr::now, lt_count, row->count)\n\t\t: row->isMasksSet()\n\t\t? tr::lng_masks_count(tr::now, lt_count, row->count)\n\t\t: tr::lng_stickers_count(tr::now, lt_count, row->count);\n\n\tp.setFont(st::contactsStatusFont);\n\tp.setPen(_st.statusFg);\n\tp.drawTextLeft(statusx, statusy, width(), statusText);\n\n\tp.setOpacity(1);\n\tif (xadd || yadd) p.translate(-xadd, -yadd);\n}\n\nvoid StickersBox::Inner::paintRowThumbnail(\n\t\tPainter &p,\n\t\tnot_null<Row*> row,\n\t\tint left) {\n\tconst auto origin = Data::FileOriginStickerSet(\n\t\trow->set->id,\n\t\trow->set->accessHash);\n\tif (row->set->hasThumbnail()) {\n\t\tif (!row->thumbnailMedia) {\n\t\t\trow->thumbnailMedia = row->set->createThumbnailView();\n\t\t\trow->set->loadThumbnail();\n\t\t}\n\t} else if (row->sticker) {\n\t\tif (!row->stickerMedia) {\n\t\t\trow->stickerMedia = row->sticker->createMediaView();\n\t\t\trow->stickerMedia->thumbnailWanted(origin);\n\t\t}\n\t}\n\tvalidateAnimation(row);\n\tconst auto thumb = row->thumbnailMedia\n\t\t? row->thumbnailMedia->image()\n\t\t: row->stickerMedia\n\t\t? row->stickerMedia->thumbnail()\n\t\t: nullptr;\n\tconst auto paused = _show->paused(ChatHelpers::PauseReason::Layer);\n\tconst auto x = left + (_st.photoSize - row->pixw) / 2;\n\tconst auto y = _st.photoPosition.y() + (_st.photoSize - row->pixh) / 2;\n\tif (row->lottie && row->lottie->ready()) {\n\t\tconst auto frame = row->lottie->frame();\n\t\tconst auto size = frame.size() / style::DevicePixelRatio();\n\t\tp.drawImage(\n\t\t\tQRect(\n\t\t\t\tleft + (_st.photoSize - size.width()) / 2,\n\t\t\t\t_st.photoPosition.y() + (_st.photoSize - size.height()) / 2,\n\t\t\t\tsize.width(),\n\t\t\t\tsize.height()),\n\t\t\tframe);\n\t\tif (!paused) {\n\t\t\trow->lottie->markFrameShown();\n\t\t}\n\t} else if (row->webm && row->webm->started()) {\n\t\tp.drawImage(\n\t\t\tx,\n\t\t\ty,\n\t\t\trow->webm->current(\n\t\t\t\t{ .frame = { row->pixw, row->pixh }, .keepAlpha = true },\n\t\t\t\tpaused ? 0 : crl::now()));\n\t} else if (thumb) {\n\t\tp.drawPixmapLeft(\n\t\t\tx,\n\t\t\ty,\n\t\t\twidth(),\n\t\t\tthumb->pix(row->pixw, row->pixh));\n\t}\n}\n\nvoid StickersBox::Inner::validateLottieAnimation(not_null<Row*> row) {\n\tif (row->lottie\n\t\t|| !ChatHelpers::HasLottieThumbnail(\n\t\t\trow->set->thumbnailType(),\n\t\t\trow->thumbnailMedia.get(),\n\t\t\trow->stickerMedia.get())) {\n\t\treturn;\n\t}\n\tauto player = ChatHelpers::LottieThumbnail(\n\t\trow->thumbnailMedia.get(),\n\t\trow->stickerMedia.get(),\n\t\tChatHelpers::StickerLottieSize::SetsListThumbnail,\n\t\tQSize(_st.photoSize, _st.photoSize) * style::DevicePixelRatio());\n\tif (!player) {\n\t\treturn;\n\t}\n\trow->lottie = std::move(player);\n\trow->lottie->updates(\n\t) | rpl::on_next([=] {\n\t\tupdateRowThumbnail(row);\n\t}, lifetime());\n}\n\nvoid StickersBox::Inner::validateWebmAnimation(not_null<Row*> row) {\n\tif (row->webm\n\t\t|| !ChatHelpers::HasWebmThumbnail(\n\t\t\trow->set->thumbnailType(),\n\t\t\trow->thumbnailMedia.get(),\n\t\t\trow->stickerMedia.get())) {\n\t\treturn;\n\t}\n\tauto callback = [=](Media::Clip::Notification notification) {\n\t\tclipCallback(row, notification);\n\t};\n\trow->webm = ChatHelpers::WebmThumbnail(\n\t\trow->thumbnailMedia.get(),\n\t\trow->stickerMedia.get(),\n\t\tstd::move(callback));\n}\n\nvoid StickersBox::Inner::clipCallback(\n\t\tnot_null<Row*> row,\n\t\tMedia::Clip::Notification notification) {\n\tusing namespace Media::Clip;\n\tswitch (notification) {\n\tcase Notification::Reinit: {\n\t\tif (!row->webm) {\n\t\t\treturn;\n\t\t} else if (row->webm->state() == State::Error) {\n\t\t\trow->webm.setBad();\n\t\t} else if (row->webm->ready() && !row->webm->started()) {\n\t\t\trow->webm->start({\n\t\t\t\t.frame = { row->pixw, row->pixh },\n\t\t\t\t.keepAlpha = true,\n\t\t\t});\n\t\t}\n\t} break;\n\n\tcase Notification::Repaint: break;\n\t}\n\tupdateRowThumbnail(row);\n}\n\nvoid StickersBox::Inner::validateAnimation(not_null<Row*> row) {\n\tvalidateWebmAnimation(row);\n\tvalidateLottieAnimation(row);\n}\n\nvoid StickersBox::Inner::updateRowThumbnail(not_null<Row*> row) {\n\tconst auto rowTop = [&] {\n\t\tif (row == _megagroupSelectedSet.get()) {\n\t\t\treturn _megagroupDivider->y() - _rowHeight;\n\t\t}\n\t\tauto top = _itemsTop;\n\t\tfor (const auto &entry : _rows) {\n\t\t\tif (entry.get() == row) {\n\t\t\t\treturn top + qRound(row->yadd.current());\n\t\t\t}\n\t\t\ttop += _rowHeight;\n\t\t}\n\t\tUnexpected(\"StickersBox::Inner::updateRowThumbnail: row not found\");\n\t}();\n\tconst auto left = _st.photoPosition.x()\n\t\t+ ((!_megagroupSet && _isInstalledTab)\n\t\t\t? st::stickersReorderIcon.width() + st::stickersReorderSkip\n\t\t\t: 0);\n\tconst auto top = rowTop + _st.photoPosition.y();\n\tupdate(left, top, _st.photoSize, _st.photoSize);\n}\n\nvoid StickersBox::Inner::paintFakeButton(Painter &p, not_null<Row*> row, int index) {\n\tconst auto removeButton = (_isInstalledTab && !row->removed);\n\tif (!_isInstalledTab && row->isInstalled() && !row->isArchived() && !row->removed) {\n\t\t// Round button \"Added\" after installed from Trending or Archived.\n\t\tconst auto rect = relativeButtonRect(removeButton, true);\n\t\tconst auto &st = st::stickersTrendingInstalled;\n\t\tconst auto textWidth = _installedWidth;\n\t\tconst auto &text = _installedText;\n\t\t_inactiveButtonBg.paint(p, myrtlrect(rect));\n\t\tif (row->ripple) {\n\t\t\trow->ripple->paint(p, rect.x(), rect.y(), width());\n\t\t\tif (row->ripple->empty()) {\n\t\t\t\trow->ripple.reset();\n\t\t\t}\n\t\t}\n\t\tp.setFont(st.style.font);\n\t\tp.setPen(st.textFg);\n\t\tp.drawTextLeft(rect.x() - (st.width / 2), rect.y() + st.textTop, width(), text, textWidth);\n\t} else {\n\t\tconst auto rect = relativeButtonRect(removeButton, false);\n\t\tauto selected = (index == _actionSel && _actionDown < 0) || (index == _actionDown);\n\t\tif (removeButton) {\n\t\t\t// Trash icon button when not disabled in Installed.\n\t\t\tif (row->ripple) {\n\t\t\t\trow->ripple->paint(p, rect.x(), rect.y(), width());\n\t\t\t\tif (row->ripple->empty()) {\n\t\t\t\t\trow->ripple.reset();\n\t\t\t\t}\n\t\t\t}\n\t\t\tauto &icon = selected ? st::stickersRemove.iconOver : st::stickersRemove.icon;\n\t\t\tauto position = st::stickersRemove.iconPosition;\n\t\t\tif (position.x() < 0) position.setX((rect.width() - icon.width()) / 2);\n\t\t\tif (position.y() < 0) position.setY((rect.height() - icon.height()) / 2);\n\t\t\ticon.paint(p, rect.topLeft() + position, width());\n\t\t} else {\n\t\t\t// Round button ADD when not installed from Trending or Archived.\n\t\t\t// Or round button UNDO after disabled from Installed.\n\t\t\tconst auto &st = _isInstalledTab\n\t\t\t\t? st::stickersUndoRemove\n\t\t\t\t: st::stickersTrendingAdd;\n\t\t\tconst auto textWidth = _isInstalledTab ? _undoWidth : _addWidth;\n\t\t\tconst auto &text = _isInstalledTab ? _undoText : _addText;\n\t\t\t(selected ? _buttonBgOver : _buttonBg).paint(p, myrtlrect(rect));\n\t\t\tif (row->ripple) {\n\t\t\t\trow->ripple->paint(p, rect.x(), rect.y(), width());\n\t\t\t\tif (row->ripple->empty()) {\n\t\t\t\t\trow->ripple.reset();\n\t\t\t\t}\n\t\t\t}\n\t\t\tp.setFont(st.style.font);\n\t\t\tp.setPen(selected ? st.textFgOver : st.textFg);\n\t\t\tp.drawTextLeft(rect.x() - (st.width / 2), rect.y() + st.textTop, width(), text, textWidth);\n\t\t}\n\t}\n}\n\nvoid StickersBox::Inner::mousePressEvent(QMouseEvent *e) {\n\tif (_dragging >= 0) {\n\t\tmouseReleaseEvent(e);\n\t}\n\t_mouse = e->globalPos();\n\tupdateSelected();\n\n\tsetPressed(_selected);\n\tif (_actionSel >= 0) {\n\t\tsetActionDown(_actionSel);\n\t\tupdate(0, _itemsTop + _actionSel * _rowHeight, width(), _rowHeight);\n\t} else if (auto selectedIndex = std::get_if<int>(&_selected)) {\n\t\tif (_isInstalledTab && !_rows[*selectedIndex]->isRecentSet() && _inDragArea) {\n\t\t\t_above = _dragging = _started = *selectedIndex;\n\t\t\t_dragStart = mapFromGlobal(_mouse);\n\t\t}\n\t}\n}\n\nvoid StickersBox::Inner::setActionDown(int newActionDown) {\n\tif (_actionDown == newActionDown) {\n\t\treturn;\n\t}\n\tif (_actionDown >= 0 && _actionDown < _rows.size()) {\n\t\tupdate(0, _itemsTop + _actionDown * _rowHeight, width(), _rowHeight);\n\t\tconst auto row = _rows[_actionDown].get();\n\t\tif (row->ripple) {\n\t\t\trow->ripple->lastStop();\n\t\t}\n\t}\n\t_actionDown = newActionDown;\n\tif (_actionDown >= 0 && _actionDown < _rows.size()) {\n\t\tupdate(0, _itemsTop + _actionDown * _rowHeight, width(), _rowHeight);\n\t\tconst auto row = _rows[_actionDown].get();\n\t\tauto removeButton = (_isInstalledTab && !row->removed);\n\t\tif (!row->ripple) {\n\t\t\tif (_isInstalledTab) {\n\t\t\t\tif (row->removed) {\n\t\t\t\t\tauto rippleSize = QSize(_undoWidth - st::stickersUndoRemove.width, st::stickersUndoRemove.height);\n\t\t\t\t\tauto rippleMask = Ui::RippleAnimation::RoundRectMask(rippleSize, st::roundRadiusLarge);\n\t\t\t\t\tensureRipple(st::stickersUndoRemove.ripple, std::move(rippleMask), removeButton, false);\n\t\t\t\t} else {\n\t\t\t\t\tauto rippleSize = st::stickersRemove.rippleAreaSize;\n\t\t\t\t\tauto rippleMask = Ui::RippleAnimation::EllipseMask(QSize(rippleSize, rippleSize));\n\t\t\t\t\tensureRipple(st::stickersRemove.ripple, std::move(rippleMask), removeButton, false);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst auto installedSet = row->isInstalled()\n\t\t\t\t\t&& !row->isArchived()\n\t\t\t\t\t&& !row->removed;\n\t\t\t\tconst auto &st = installedSet\n\t\t\t\t\t? st::stickersTrendingInstalled\n\t\t\t\t\t: st::stickersTrendingAdd;\n\t\t\t\tconst auto buttonTextWidth = installedSet\n\t\t\t\t\t? _installedWidth\n\t\t\t\t\t: _addWidth;\n\t\t\t\tauto rippleMask = Ui::RippleAnimation::RoundRectMask(\n\t\t\t\t\tQSize(buttonTextWidth - st.width, st.height),\n\t\t\t\t\tst::roundRadiusLarge);\n\t\t\t\tensureRipple(\n\t\t\t\t\tst.ripple,\n\t\t\t\t\tstd::move(rippleMask),\n\t\t\t\t\tremoveButton,\n\t\t\t\t\tinstalledSet);\n\t\t\t}\n\t\t}\n\t\tif (row->ripple) {\n\t\t\tauto rect = relativeButtonRect(removeButton, false);\n\t\t\trow->ripple->add(mapFromGlobal(QCursor::pos()) - QPoint(myrtlrect(rect).x(), _itemsTop + _actionDown * _rowHeight + rect.y()));\n\t\t}\n\t}\n}\n\nvoid StickersBox::Inner::setSelected(SelectedRow selected) {\n\tif (_selected == selected) {\n\t\treturn;\n\t}\n\tauto countSelectedIndex = [&] {\n\t\tif (auto index = std::get_if<int>(&_selected)) {\n\t\t\treturn *index;\n\t\t}\n\t\treturn -1;\n\t};\n\tauto selectedIndex = countSelectedIndex();\n\tif (_megagroupSet && selectedIndex >= 0 && selectedIndex < _rows.size()) {\n\t\tupdate(0, _itemsTop + selectedIndex * _rowHeight, width(), _rowHeight);\n\t}\n\t_selected = selected;\n\tupdateCursor();\n\tselectedIndex = countSelectedIndex();\n\tif (_megagroupSet && selectedIndex >= 0 && selectedIndex < _rows.size()) {\n\t\tupdate(0, _itemsTop + selectedIndex * _rowHeight, width(), _rowHeight);\n\t}\n}\n\nvoid StickersBox::Inner::setPressed(SelectedRow pressed) {\n\tif (_pressed == pressed) {\n\t\treturn;\n\t}\n\tauto countPressedIndex = [&] {\n\t\tif (auto index = std::get_if<int>(&_pressed)) {\n\t\t\treturn *index;\n\t\t}\n\t\treturn -1;\n\t};\n\tauto pressedIndex = countPressedIndex();\n\tif (_megagroupSet && pressedIndex >= 0 && pressedIndex < _rows.size()) {\n\t\tupdate(0, _itemsTop + pressedIndex * _rowHeight, width(), _rowHeight);\n\t\tconst auto row = _rows[pressedIndex].get();\n\t\tif (row->ripple) {\n\t\t\trow->ripple->lastStop();\n\t\t}\n\t}\n\t_pressed = pressed;\n\tpressedIndex = countPressedIndex();\n\tif (_megagroupSet && pressedIndex >= 0 && pressedIndex < _rows.size()) {\n\t\tupdate(0, _itemsTop + pressedIndex * _rowHeight, width(), _rowHeight);\n\t\tauto &set = _rows[pressedIndex];\n\t\tif (!set->ripple) {\n\t\t\tauto rippleMask = Ui::RippleAnimation::RectMask(QSize(width(), _rowHeight));\n\t\t\tset->ripple = std::make_unique<Ui::RippleAnimation>(st::defaultRippleAnimation, std::move(rippleMask), [this, pressedIndex] {\n\t\t\t\tupdate(0, _itemsTop + pressedIndex * _rowHeight, width(), _rowHeight);\n\t\t\t});\n\t\t}\n\t\tset->ripple->add(mapFromGlobal(QCursor::pos()) - QPoint(0, _itemsTop + pressedIndex * _rowHeight));\n\t}\n}\n\nvoid StickersBox::Inner::ensureRipple(\n\t\tconst style::RippleAnimation &st,\n\t\tQImage mask,\n\t\tbool removeButton,\n\t\tbool installedSet) {\n\tconst auto dy = _itemsTop + _actionDown * _rowHeight;\n\t_rows[_actionDown]->ripple = std::make_unique<Ui::RippleAnimation>(st, std::move(mask), [=] {\n\t\tupdate(myrtlrect(relativeButtonRect(removeButton, installedSet).translated(0, dy)));\n\t});\n}\n\nvoid StickersBox::Inner::mouseMoveEvent(QMouseEvent *e) {\n\t_mouse = e->globalPos();\n\tupdateSelected();\n}\n\nvoid StickersBox::Inner::updateSelected() {\n\tauto local = mapFromGlobal(_mouse);\n\tif (_dragging >= 0) {\n\t\tauto shift = 0;\n\t\tauto now = crl::now();\n\t\tint firstSetIndex = 0;\n\t\tif (_rows.at(firstSetIndex)->isRecentSet()) {\n\t\t\t++firstSetIndex;\n\t\t}\n\t\tif (_dragStart.y() > local.y() && _dragging > 0) {\n\t\t\tshift = -floorclamp(_dragStart.y() - local.y() + (_rowHeight / 2), _rowHeight, 0, _dragging - firstSetIndex);\n\t\t\tfor (int32 from = _dragging, to = _dragging + shift; from > to; --from) {\n\t\t\t\tqSwap(_rows[from], _rows[from - 1]);\n\t\t\t\t_rows[from]->yadd = anim::value(_rows[from]->yadd.current() - _rowHeight, 0);\n\t\t\t\t_shiftingStartTimes[from] = now;\n\t\t\t}\n\t\t} else if (_dragStart.y() < local.y() && _dragging + 1 < _rows.size()) {\n\t\t\tshift = floorclamp(local.y() - _dragStart.y() + (_rowHeight / 2), _rowHeight, 0, _rows.size() - _dragging - 1);\n\t\t\tfor (int32 from = _dragging, to = _dragging + shift; from < to; ++from) {\n\t\t\t\tqSwap(_rows[from], _rows[from + 1]);\n\t\t\t\t_rows[from]->yadd = anim::value(_rows[from]->yadd.current() + _rowHeight, 0);\n\t\t\t\t_shiftingStartTimes[from] = now;\n\t\t\t}\n\t\t}\n\t\tif (shift) {\n\t\t\t_dragging += shift;\n\t\t\t_above = _dragging;\n\t\t\t_dragStart.setY(_dragStart.y() + shift * _rowHeight);\n\t\t\tif (!_shiftingAnimation.animating()) {\n\t\t\t\t_shiftingAnimation.start();\n\t\t\t}\n\t\t}\n\t\t_rows[_dragging]->yadd = anim::value(local.y() - _dragStart.y(), local.y() - _dragStart.y());\n\t\t_shiftingStartTimes[_dragging] = 0;\n\t\tshiftingAnimationCallback(now);\n\n\t\t_draggingScrollDelta.fire_copy([&] {\n\t\t\tif (local.y() < _visibleTop) {\n\t\t\t\treturn local.y() - _visibleTop;\n\t\t\t} else if (local.y() >= _visibleBottom) {\n\t\t\t\treturn local.y() + 1 - _visibleBottom;\n\t\t\t}\n\t\t\treturn 0;\n\t\t}());\n\t} else {\n\t\tbool in = rect().marginsRemoved(QMargins(0, _itemsTop, 0, st::membersMarginBottom)).contains(local);\n\t\tauto selected = SelectedRow();\n\t\tauto actionSel = -1;\n\t\tauto inDragArea = false;\n\t\tif (in && !_rows.empty()) {\n\t\t\tauto selectedIndex = floorclamp(local.y() - _itemsTop, _rowHeight, 0, _rows.size() - 1);\n\t\t\tselected = selectedIndex;\n\t\t\tlocal.setY(local.y() - _itemsTop - selectedIndex * _rowHeight);\n\t\t\tconst auto row = _rows[selectedIndex].get();\n\t\t\tif (!_megagroupSet\n\t\t\t\t&& (_isInstalledTab\n\t\t\t\t\t|| (_section == Section::Featured)\n\t\t\t\t\t|| !row->isInstalled()\n\t\t\t\t\t|| row->isArchived()\n\t\t\t\t\t|| row->removed)) {\n\t\t\t\tauto removeButton = (_isInstalledTab && !row->removed);\n\n\t\t\t\tconst auto installedSetButton = !_isInstalledTab\n\t\t\t\t\t&& row->isInstalled()\n\t\t\t\t\t&& !row->isArchived()\n\t\t\t\t\t&& !row->removed;\n\t\t\t\tauto rect = myrtlrect(relativeButtonRect(removeButton, installedSetButton));\n\t\t\t\tactionSel = rect.contains(local) ? selectedIndex : -1;\n\t\t\t} else {\n\t\t\t\tactionSel = -1;\n\t\t\t}\n\t\t\tif (!_megagroupSet && _isInstalledTab && !row->isRecentSet()) {\n\t\t\t\tauto dragAreaWidth = _st.photoPosition.x() + st::stickersReorderIcon.width() + st::stickersReorderSkip;\n\t\t\t\tauto dragArea = myrtlrect(0, 0, dragAreaWidth, _rowHeight);\n\t\t\t\tinDragArea = dragArea.contains(local);\n\t\t\t}\n\t\t} else if (_megagroupSelectedSet) {\n\t\t\tauto setTop = _megagroupDivider->y() - _rowHeight;\n\t\t\tif (QRect(0, setTop, width(), _rowHeight).contains(local)) {\n\t\t\t\tselected = MegagroupSet();\n\t\t\t}\n\t\t}\n\t\tsetSelected(selected);\n\t\tif (_inDragArea != inDragArea) {\n\t\t\t_inDragArea = inDragArea;\n\t\t\tupdateCursor();\n\t\t}\n\t\tsetActionSel(actionSel);\n\t\t_draggingScrollDelta.fire(0);\n\t}\n}\n\nvoid StickersBox::Inner::updateCursor() {\n\tsetCursor(_inDragArea\n\t\t? style::cur_sizeall\n\t\t: (!_megagroupSet && _isInstalledTab)\n\t\t? ((_actionSel >= 0 && (_actionDown < 0 || _actionDown == _actionSel))\n\t\t\t? style::cur_pointer\n\t\t\t: style::cur_default)\n\t\t: (!v::is_null(_selected) || !v::is_null(_pressed))\n\t\t? style::cur_pointer\n\t\t: style::cur_default);\n}\n\nfloat64 StickersBox::Inner::aboveShadowOpacity() const {\n\tif (_above < 0) return 0;\n\n\tauto dx = 0;\n\tauto dy = qAbs(_above * _rowHeight + qRound(_rows[_above]->yadd.current()) - _started * _rowHeight);\n\treturn qMin((dx + dy) * 2. / _rowHeight, 1.);\n}\n\nvoid StickersBox::Inner::mouseReleaseEvent(QMouseEvent *e) {\n\tauto pressed = std::exchange(_pressed, SelectedRow());\n\n\tupdateCursor();\n\n\t_mouse = e->globalPos();\n\tupdateSelected();\n\tconst auto down = _actionDown;\n\tsetActionDown(-1);\n\tif (down == _actionSel && _actionSel >= 0) {\n\t\tconst auto row = _rows[down].get();\n\t\tconst auto installedSet = row->isInstalled()\n\t\t\t&& !row->isArchived()\n\t\t\t&& !row->removed;\n\t\tconst auto callback = installedSet\n\t\t\t? _removeSetCallback\n\t\t\t: _installSetCallback;\n\t\tif (callback) {\n\t\t\trow->ripple.reset();\n\t\t\tcallback(row->set->id);\n\t\t}\n\t} else if (_dragging >= 0) {\n\t\t_rows[_dragging]->yadd.start(0.);\n\t\t_aboveShadowFadeStart = _shiftingStartTimes[_dragging] = crl::now();\n\t\t_aboveShadowFadeOpacity = anim::value(aboveShadowOpacity(), 0);\n\t\tif (!_shiftingAnimation.animating()) {\n\t\t\t_shiftingAnimation.start();\n\t\t}\n\n\t\t_dragging = _started = -1;\n\t} else if (pressed == _selected && _actionSel < 0 && down < 0) {\n\t\tconst auto selectedIndex = [&] {\n\t\t\tif (auto index = std::get_if<int>(&_selected)) {\n\t\t\t\treturn *index;\n\t\t\t}\n\t\t\treturn -1;\n\t\t}();\n\t\tconst auto showSetByRow = [&](const Row &row) {\n\t\t\tsetSelected(SelectedRow());\n\t\t\t_show->showBox(Box<StickerSetBox>(_show, row.set));\n\t\t};\n\t\tif (selectedIndex >= 0 && !_inDragArea) {\n\t\t\tconst auto row = _rows[selectedIndex].get();\n\t\t\tif (!row->isRecentSet()) {\n\t\t\t\tif (_megagroupSet) {\n\t\t\t\t\tsetMegagroupSelectedSet(row->set->identifier());\n\t\t\t\t} else {\n\t\t\t\t\tshowSetByRow(*row);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (_megagroupSelectedSet && v::is<MegagroupSet>(_selected)) {\n\t\t\tshowSetByRow(*_megagroupSelectedSet);\n\t\t}\n\t}\n}\n\nvoid StickersBox::Inner::saveGroupSet(Fn<void()> done) {\n\tExpects(_megagroupSet != nullptr);\n\n\tauto oldId = _megagroupSetEmoji\n\t\t? _megagroupSet->mgInfo->emojiSet.id\n\t\t: _megagroupSet->mgInfo->stickerSet.id;\n\tauto newId = _megagroupSetInput.id;\n\tif (newId == oldId) {\n\t\tdone();\n\t} else if (_megagroupSetEmoji) {\n\t\tcheckGroupLevel(done);\n\t} else {\n\t\tsession().api().setGroupStickerSet(_megagroupSet, _megagroupSetInput);\n\t\tsession().data().stickers().notifyStickerSetInstalled(\n\t\t\tData::Stickers::MegagroupSetId);\n\t}\n}\n\nvoid StickersBox::Inner::checkGroupLevel(Fn<void()> done) {\n\tExpects(_megagroupSet != nullptr);\n\tExpects(_megagroupSetEmoji);\n\n\tconst auto peer = _megagroupSet;\n\tconst auto save = [=] {\n\t\tsession().api().setGroupEmojiSet(peer, _megagroupSetInput);\n\t\tsession().data().stickers().notifyEmojiSetInstalled(\n\t\t\tData::Stickers::MegagroupSetId);\n\t\tdone();\n\t};\n\n\tif (!_megagroupSetInput) {\n\t\tsave();\n\t\treturn;\n\t} else if (_checkingGroupLevel) {\n\t\treturn;\n\t}\n\t_checkingGroupLevel = true;\n\n\tconst auto weak = base::make_weak(this);\n\tCheckBoostLevel(_show, peer, [=](int level) {\n\t\tif (!weak) {\n\t\t\treturn std::optional<Ui::AskBoostReason>();\n\t\t}\n\t\t_checkingGroupLevel = false;\n\t\tconst auto required = Data::LevelLimits(\n\t\t\t&peer->session()).groupEmojiStickersLevelMin();\n\t\tif (level >= required) {\n\t\t\tsave();\n\t\t\treturn std::optional<Ui::AskBoostReason>();\n\t\t}\n\t\treturn std::make_optional(Ui::AskBoostReason{\n\t\t\tUi::AskBoostEmojiPack{ required }\n\t\t});\n\t}, [=] { _checkingGroupLevel = false; });\n}\n\nvoid StickersBox::Inner::setRowRemovedBySetId(uint64 setId, bool removed) {\n\tconst auto index = getRowIndex(setId);\n\tif (index >= 0) {\n\t\tsetRowRemoved(index, removed);\n\t}\n}\n\nvoid StickersBox::Inner::setRowRemoved(int index, bool removed) {\n\tauto &row = _rows[index];\n\tif (row->removed != removed) {\n\t\trow->removed = removed;\n\t\trow->ripple.reset();\n\t\tupdate(0, _itemsTop + index * _rowHeight, width(), _rowHeight);\n\t\tupdateSelected();\n\t}\n}\n\nvoid StickersBox::Inner::leaveEventHook(QEvent *e) {\n\t_mouse = QPoint(-1, -1);\n\tupdateSelected();\n}\n\nvoid StickersBox::Inner::leaveToChildEvent(QEvent *e, QWidget *child) {\n\t_mouse = QPoint(-1, -1);\n\tupdateSelected();\n}\n\nbool StickersBox::Inner::shiftingAnimationCallback(crl::time now) {\n\tif (anim::Disabled()) {\n\t\tnow += st::stickersRowDuration;\n\t}\n\tauto animating = false;\n\tauto updateMin = -1;\n\tauto updateMax = 0;\n\tfor (auto i = 0, count = int(_shiftingStartTimes.size()); i != count; ++i) {\n\t\tconst auto start = _shiftingStartTimes[i];\n\t\tif (start) {\n\t\t\tif (updateMin < 0) {\n\t\t\t\tupdateMin = i;\n\t\t\t}\n\t\t\tupdateMax = i;\n\t\t\tif (start + st::stickersRowDuration > now && now >= start) {\n\t\t\t\t_rows[i]->yadd.update(float64(now - start) / st::stickersRowDuration, anim::sineInOut);\n\t\t\t\tanimating = true;\n\t\t\t} else {\n\t\t\t\t_rows[i]->yadd.finish();\n\t\t\t\t_shiftingStartTimes[i] = 0;\n\t\t\t}\n\t\t}\n\t}\n\tif (_aboveShadowFadeStart) {\n\t\tif (updateMin < 0 || updateMin > _above) updateMin = _above;\n\t\tif (updateMax < _above) updateMin = _above;\n\t\tif (_aboveShadowFadeStart + st::stickersRowDuration > now && now > _aboveShadowFadeStart) {\n\t\t\t_aboveShadowFadeOpacity.update(float64(now - _aboveShadowFadeStart) / st::stickersRowDuration, anim::sineInOut);\n\t\t\tanimating = true;\n\t\t} else {\n\t\t\t_aboveShadowFadeOpacity.finish();\n\t\t\t_aboveShadowFadeStart = 0;\n\t\t}\n\t}\n\tif (_dragging >= 0) {\n\t\tif (updateMin < 0 || updateMin > _dragging) {\n\t\t\tupdateMin = _dragging;\n\t\t}\n\t\tif (updateMax < _dragging) updateMax = _dragging;\n\t}\n\tif (updateMin == 1 && _rows[0]->isRecentSet()) {\n\t\tupdateMin = 0; // Repaint from the very top of the content.\n\t}\n\tif (updateMin >= 0) {\n\t\tupdate(0, _itemsTop + _rowHeight * (updateMin - 1), width(), _rowHeight * (updateMax - updateMin + 3));\n\t}\n\tif (!animating) {\n\t\t_above = _dragging;\n\t}\n\treturn animating;\n}\n\nvoid StickersBox::Inner::clear() {\n\t_rows.clear();\n\t_shiftingStartTimes.clear();\n\t_aboveShadowFadeStart = 0;\n\t_aboveShadowFadeOpacity = anim::value();\n\t_shiftingAnimation.stop();\n\t_above = _dragging = _started = -1;\n\tsetSelected(SelectedRow());\n\tsetPressed(SelectedRow());\n\tsetActionSel(-1);\n\tsetActionDown(-1);\n\tupdate();\n}\n\nvoid StickersBox::Inner::setActionSel(int32 actionSel) {\n\tif (actionSel != _actionSel) {\n\t\tif (_actionSel >= 0) update(0, _itemsTop + _actionSel * _rowHeight, width(), _rowHeight);\n\t\t_actionSel = actionSel;\n\t\tif (_actionSel >= 0) update(0, _itemsTop + _actionSel * _rowHeight, width(), _rowHeight);\n\t\tupdateCursor();\n\t}\n}\n\nvoid StickersBox::Inner::AddressField::correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) {\n\tauto newText = now;\n\tauto newCursor = nowCursor;\n\tauto removeFromBeginning = {\n\t\tu\"http://\"_q,\n\t\tu\"https://\"_q,\n\t\tu\"www.t.me/\"_q,\n\t\tu\"www.telegram.me/\"_q,\n\t\tu\"www.telegram.dog/\"_q,\n\t\tu\"t.me/\"_q,\n\t\tu\"telegram.me/\"_q,\n\t\tu\"telegram.dog/\"_q,\n\t\tu\"addstickers/\"_q,\n\t};\n\tfor (auto &removePhrase : removeFromBeginning) {\n\t\tif (newText.startsWith(removePhrase)) {\n\t\t\tnewText = newText.mid(removePhrase.size());\n\t\t\tnewCursor = newText.size();\n\t\t}\n\t}\n\tsetCorrectedText(now, nowCursor, newText, newCursor);\n}\n\nvoid StickersBox::Inner::handleMegagroupSetAddressChange() {\n\tauto text = _megagroupSetField->getLastText().trimmed();\n\tif (text.isEmpty()) {\n\t\tif (_megagroupSelectedSet) {\n\t\t\tconst auto &sets = session().data().stickers().sets();\n\t\t\tconst auto it = sets.find(_megagroupSelectedSet->set->id);\n\t\t\tif (it != sets.cend() && !it->second->shortName.isEmpty()) {\n\t\t\t\tsetMegagroupSelectedSet({});\n\t\t\t}\n\t\t}\n\t} else if (!_megagroupSetRequestId) {\n\t\t_megagroupSetRequestId = _api.request(MTPmessages_GetStickerSet(\n\t\t\tMTP_inputStickerSetShortName(MTP_string(text)),\n\t\t\tMTP_int(0) // hash\n\t\t)).done([=](const MTPmessages_StickerSet &result) {\n\t\t\t_megagroupSetRequestId = 0;\n\t\t\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\t\tconst auto set = session().data().stickers().feedSetFull(data);\n\t\t\t\tsetMegagroupSelectedSet(set->identifier());\n\t\t\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\t\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t\t\t});\n\t\t}).fail([=] {\n\t\t\t_megagroupSetRequestId = 0;\n\t\t\tsetMegagroupSelectedSet({});\n\t\t}).send();\n\t} else {\n\t\t_megagroupSetAddressChangedTimer.callOnce(kHandleMegagroupSetAddressChangeTimeout);\n\t}\n}\n\nvoid StickersBox::Inner::rebuildMegagroupSet() {\n\tExpects(_megagroupSet != nullptr);\n\n\tconst auto clearCurrent = [&] {\n\t\tif (_megagroupSelectedSet) {\n\t\t\t_megagroupSetField->setText(QString());\n\t\t\t_megagroupSetField->finishAnimating();\n\t\t}\n\t\t_megagroupSelectedSet = nullptr;\n\t\t_megagroupSelectedRemove.destroy();\n\t\t_megagroupSelectedShadow.destroy();\n\t};\n\tif (!_megagroupSetInput.id) {\n\t\tclearCurrent();\n\t\treturn;\n\t}\n\tauto setId = _megagroupSetInput.id;\n\tconst auto &sets = session().data().stickers().sets();\n\tauto it = sets.find(setId);\n\tif (it == sets.cend()\n\t\t|| (it->second->flags & SetFlag::NotLoaded)) {\n\t\t// It may have been in sets and stored in _megagroupSelectedSet\n\t\t// already, but then removed from sets. We need to clear the stored\n\t\t// pointer, otherwise we may crash in paint event while loading.\n\t\tclearCurrent();\n\t\tsession().api().scheduleStickerSetRequest(\n\t\t\t_megagroupSetInput.id,\n\t\t\t_megagroupSetInput.accessHash);\n\t\treturn;\n\t}\n\n\tconst auto set = it->second.get();\n\tauto count = fillSetCount(set);\n\tauto sticker = (DocumentData*)nullptr;\n\tauto pixw = 0, pixh = 0;\n\tfillSetCover(set, &sticker, &pixw, &pixh);\n\tauto flagsOverride = SetFlag::Installed;\n\tauto removed = false;\n\tauto maxNameWidth = countMaxNameWidth(!_isInstalledTab);\n\tauto titleWidth = 0;\n\tauto title = FillSetTitle(set, maxNameWidth, &titleWidth);\n\tif (!_megagroupSelectedSet\n\t\t|| _megagroupSelectedSet->set->id != set->id) {\n\t\t_megagroupSetField->setText(set->shortName);\n\t\t_megagroupSetField->finishAnimating();\n\t}\n\t_megagroupSelectedSet = std::make_unique<Row>(\n\t\tset,\n\t\tsticker,\n\t\tcount,\n\t\ttitle,\n\t\ttitleWidth,\n\t\tflagsOverride,\n\t\tremoved,\n\t\tpixw,\n\t\tpixh);\n\t_itemsTop += st::lineWidth + _rowHeight;\n\n\tif (!_megagroupSelectedRemove) {\n\t\t_megagroupSelectedRemove.create(this, st::groupStickersRemove);\n\t\t_megagroupSelectedRemove->show(anim::type::instant);\n\t\t_megagroupSelectedRemove->setClickedCallback([this] {\n\t\t\tsetMegagroupSelectedSet({});\n\t\t});\n\t\t_megagroupSelectedShadow.create(this);\n\t\tupdateControlsGeometry();\n\t}\n}\n\nvoid StickersBox::Inner::rebuild(bool masks) {\n\t_itemsTop = st::lineWidth;\n\n\tif (_megagroupSet) {\n\t\t_itemsTop += st::groupStickersFieldPadding.top() + _megagroupSetField->height() + st::groupStickersFieldPadding.bottom();\n\t\t_itemsTop += _megagroupDivider->height() + st::groupStickersSubTitleHeight;\n\t\trebuildMegagroupSet();\n\t}\n\n\t_oldRows = std::move(_rows);\n\tclear();\n\tconst auto &order = ([&]() -> const StickersSetsOrder & {\n\t\tif (_section == Section::Installed) {\n\t\t\tauto &result = _megagroupSetEmoji\n\t\t\t\t? session().data().stickers().emojiSetsOrder()\n\t\t\t\t: session().data().stickers().setsOrder();\n\t\t\tif (_megagroupSet && result.empty()) {\n\t\t\t\treturn _megagroupSetEmoji\n\t\t\t\t\t? session().data().stickers().featuredEmojiSetsOrder()\n\t\t\t\t\t: session().data().stickers().featuredSetsOrder();\n\t\t\t}\n\t\t\treturn result;\n\t\t} else if (_section == Section::Masks) {\n\t\t\treturn session().data().stickers().maskSetsOrder();\n\t\t} else if (_section == Section::Featured) {\n\t\t\treturn session().data().stickers().featuredSetsOrder();\n\t\t}\n\t\treturn masks\n\t\t\t? session().data().stickers().archivedMaskSetsOrder()\n\t\t\t: session().data().stickers().archivedSetsOrder();\n\t})();\n\t_rows.reserve(order.size() + 1);\n\t_shiftingStartTimes.reserve(order.size() + 1);\n\n\tconst auto &sets = session().data().stickers().sets();\n\tif (_megagroupSet) {\n\t\tauto usingFeatured = _megagroupSetEmoji\n\t\t\t? session().data().stickers().emojiSetsOrder().empty()\n\t\t\t: session().data().stickers().setsOrder().empty();\n\t\t_megagroupSubTitle->setText(usingFeatured\n\t\t\t? (_megagroupSetEmoji\n\t\t\t\t? tr::lng_stickers_group_from_featured(tr::now)\n\t\t\t\t: tr::lng_emoji_group_from_featured(tr::now))\n\t\t\t: _megagroupSetEmoji\n\t\t\t? tr::lng_emoji_group_from_your(tr::now)\n\t\t\t: tr::lng_stickers_group_from_your(tr::now));\n\t\tupdateControlsGeometry();\n\t} else if (_isInstalledTab) {\n\t\tconst auto cloudIt = sets.find((_section == Section::Masks)\n\t\t\t? Data::Stickers::CloudRecentAttachedSetId\n\t\t\t: Data::Stickers::CloudRecentSetId); // Section::Installed.\n\t\tif (cloudIt != sets.cend() && !cloudIt->second->stickers.isEmpty()) {\n\t\t\trebuildAppendSet(cloudIt->second.get());\n\t\t}\n\t}\n\tfor (const auto setId : order) {\n\t\tauto it = sets.find(setId);\n\t\tif (it == sets.cend()) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto set = it->second.get();\n\t\trebuildAppendSet(set);\n\n\t\tif (set->stickers.isEmpty()\n\t\t\t|| (set->flags & SetFlag::NotLoaded)) {\n\t\t\tsession().api().scheduleStickerSetRequest(\n\t\t\t\tset->id,\n\t\t\t\tset->accessHash);\n\t\t}\n\t}\n\t_oldRows.clear();\n\tsession().api().requestStickerSets();\n\tupdateSize();\n}\n\nvoid StickersBox::Inner::setMegagroupSelectedSet(\n\t\tconst StickerSetIdentifier &set) {\n\t_megagroupSetInput = set;\n\trebuild(false);\n\t_scrollsToY.fire(0);\n\tupdateSelected();\n}\n\nvoid StickersBox::Inner::updateSize(int newWidth) {\n\tauto naturalHeight = _itemsTop + int(_rows.size()) * _rowHeight + st::membersMarginBottom;\n\tresize(newWidth ? newWidth : width(), qMax(_minHeight, naturalHeight));\n\tupdateControlsGeometry();\n\tcheckLoadMore();\n}\n\nvoid StickersBox::Inner::updateRows() {\n\tconst auto maxNameWidth = countMaxNameWidth(false);\n\tconst auto maxNameWidthInstalled = countMaxNameWidth(true);\n\tconst auto &sets = session().data().stickers().sets();\n\tfor (const auto &row : _rows) {\n\t\tconst auto it = sets.find(row->set->id);\n\t\tif (it == sets.cend()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto set = it->second.get();\n\t\tif (!row->sticker) {\n\t\t\tauto sticker = (DocumentData*)nullptr;\n\t\t\tauto pixw = 0, pixh = 0;\n\t\t\tfillSetCover(set, &sticker, &pixw, &pixh);\n\t\t\tif (sticker) {\n\t\t\t\tif (row->sticker != sticker && !row->thumbnailMedia) {\n\t\t\t\t\trow->lottie = nullptr;\n\t\t\t\t\trow->stickerMedia = nullptr;\n\t\t\t\t}\n\t\t\t\trow->sticker = sticker;\n\t\t\t\trow->pixw = pixw;\n\t\t\t\trow->pixh = pixh;\n\t\t\t}\n\t\t}\n\t\tif (!row->isRecentSet()) {\n\t\t\tauto wasInstalled = row->isInstalled();\n\t\t\tauto wasArchived = row->isArchived();\n\t\t\trow->flagsOverride = fillSetFlags(set);\n\t\t\tif (_isInstalledTab) {\n\t\t\t\trow->flagsOverride &= ~SetFlag::Archived;\n\t\t\t}\n\t\t\tif (row->isInstalled() != wasInstalled\n\t\t\t\t|| row->isArchived() != wasArchived) {\n\t\t\t\trow->ripple.reset();\n\t\t\t}\n\t\t}\n\t\tconst auto installedSet = (!_isInstalledTab\n\t\t\t&& row->isInstalled()\n\t\t\t&& !row->isArchived()\n\t\t\t&& !row->removed);\n\t\trow->title = FillSetTitle(\n\t\t\tset,\n\t\t\tinstalledSet ? maxNameWidthInstalled : maxNameWidth,\n\t\t\t&row->titleWidth);\n\t\trow->count = fillSetCount(set);\n\t}\n\tupdate();\n}\n\nbool StickersBox::Inner::appendSet(not_null<StickersSet*> set) {\n\tfor (const auto &row : _rows) {\n\t\tif (row->set == set) {\n\t\t\treturn false;\n\t\t}\n\t}\n\trebuildAppendSet(set);\n\treturn true;\n}\n\nbool StickersBox::Inner::skipPremium() const {\n\treturn !_session->premiumPossible();\n}\n\nint StickersBox::Inner::countMaxNameWidth(bool installedSet) const {\n\tint namex = _st.namePosition.x();\n\tif (!_megagroupSet && _isInstalledTab) {\n\t\tnamex += st::stickersReorderIcon.width() + st::stickersReorderSkip;\n\t}\n\tint namew = st::boxWideWidth - namex - st::contactsPadding.right();\n\tif (_isInstalledTab) {\n\t\tif (!_megagroupSet) {\n\t\t\tnamew -= _undoWidth - st::stickersUndoRemove.width;\n\t\t}\n\t} else {\n\t\tnamew -= installedSet\n\t\t\t? (_installedWidth - st::stickersTrendingInstalled.width)\n\t\t\t: (_addWidth - st::stickersTrendingAdd.width);\n\t\tif (_section == Section::Featured) {\n\t\t\tnamew -= st::stickersFeaturedUnreadSize + st::stickersFeaturedUnreadSkip;\n\t\t}\n\t}\n\treturn namew;\n}\n\nvoid StickersBox::Inner::rebuildAppendSet(not_null<StickersSet*> set) {\n\tauto flagsOverride = (set->id != Data::Stickers::CloudRecentSetId)\n\t\t? fillSetFlags(set)\n\t\t: SetFlag::Installed;\n\tauto removed = false;\n\tif (_isInstalledTab && (flagsOverride & SetFlag::Archived)) {\n\t\treturn;\n\t}\n\n\tDocumentData *sticker = nullptr;\n\tint pixw = 0, pixh = 0;\n\tfillSetCover(set, &sticker, &pixw, &pixh);\n\n\tconst auto maxNameWidth = countMaxNameWidth(!_isInstalledTab\n\t\t&& (flagsOverride & SetFlag::Installed)\n\t\t&& !(flagsOverride & SetFlag::Archived)\n\t\t&& !removed);\n\tint titleWidth = 0;\n\tQString title = FillSetTitle(set, maxNameWidth, &titleWidth);\n\tint count = fillSetCount(set);\n\n\tconst auto existing = [&]{\n\t\tconst auto now = int(_rows.size());\n\t\tconst auto setProj = [](const std::unique_ptr<Row> &row) {\n\t\t\treturn row ? row->set.get() : nullptr;\n\t\t};\n\t\tif (_oldRows.size() > now\n\t\t\t&& setProj(_oldRows[now]) == set.get()) {\n\t\t\treturn _oldRows.begin() + now;\n\t\t}\n\t\treturn ranges::find(_oldRows, set.get(), setProj);\n\t}();\n\tif (existing != end(_oldRows)) {\n\t\tconst auto raw = existing->get();\n\t\traw->sticker = sticker;\n\t\traw->count = count;\n\t\traw->title = title;\n\t\traw->titleWidth = titleWidth;\n\t\traw->flagsOverride = flagsOverride;\n\t\traw->removed = removed;\n\t\traw->pixw = pixw;\n\t\traw->pixh = pixh;\n\t\traw->yadd = {};\n\t\tauto oldStickerMedia = std::move(raw->stickerMedia);\n\t\tauto oldThumbnailMedia = std::move(raw->thumbnailMedia);\n\t\traw->stickerMedia = sticker ? sticker->activeMediaView() : nullptr;\n\t\traw->thumbnailMedia = set->activeThumbnailView();\n\t\tif (raw->thumbnailMedia != oldThumbnailMedia\n\t\t\t|| (!raw->thumbnailMedia && raw->stickerMedia != oldStickerMedia)) {\n\t\t\traw->lottie = nullptr;\n\t\t}\n\t\t_rows.push_back(std::move(*existing));\n\t} else {\n\t\t_rows.push_back(std::make_unique<Row>(\n\t\t\tset,\n\t\t\tsticker,\n\t\t\tcount,\n\t\t\ttitle,\n\t\t\ttitleWidth,\n\t\t\tflagsOverride,\n\t\t\tremoved,\n\t\t\tpixw,\n\t\t\tpixh));\n\t}\n\t_shiftingStartTimes.push_back(0);\n}\n\nvoid StickersBox::Inner::fillSetCover(\n\t\tnot_null<StickersSet*> set,\n\t\tDocumentData **outSticker,\n\t\tint *outWidth,\n\t\tint *outHeight) const {\n\tif (set->stickers.isEmpty()) {\n\t\t*outSticker = nullptr;\n\t\t*outWidth = *outHeight = 0;\n\t\treturn;\n\t}\n\tauto sticker = *outSticker = set->stickers.front();\n\n\tconst auto size = set->hasThumbnail()\n\t\t? QSize(\n\t\t\tset->thumbnailLocation().width(),\n\t\t\tset->thumbnailLocation().height())\n\t\t: sticker->hasThumbnail()\n\t\t? QSize(\n\t\t\tsticker->thumbnailLocation().width(),\n\t\t\tsticker->thumbnailLocation().height())\n\t\t: QSize(1, 1);\n\tauto pixw = size.width();\n\tauto pixh = size.height();\n\tif (pixw > _st.photoSize) {\n\t\tif (pixw > pixh) {\n\t\t\tpixh = (pixh * _st.photoSize) / pixw;\n\t\t\tpixw = _st.photoSize;\n\t\t} else {\n\t\t\tpixw = (pixw * _st.photoSize) / pixh;\n\t\t\tpixh = _st.photoSize;\n\t\t}\n\t} else if (pixh > _st.photoSize) {\n\t\tpixw = (pixw * _st.photoSize) / pixh;\n\t\tpixh = _st.photoSize;\n\t}\n\t*outWidth = pixw;\n\t*outHeight = pixh;\n}\n\nint StickersBox::Inner::fillSetCount(not_null<StickersSet*> set) const {\n\tconst auto skipPremium = this->skipPremium();\n\tint result = set->stickers.isEmpty()\n\t\t? set->count\n\t\t: set->stickers.size();\n\tif (skipPremium && !set->stickers.isEmpty()) {\n\t\tresult -= ranges::count(\n\t\t\tset->stickers,\n\t\t\ttrue,\n\t\t\t&DocumentData::isPremiumSticker);\n\t}\n\tauto added = 0;\n\tif (set->id == Data::Stickers::CloudRecentSetId) {\n\t\tconst auto &sets = session().data().stickers().sets();\n\t\tconst auto &recent = session().data().stickers().getRecentPack();\n\t\tauto customIt = sets.find(Data::Stickers::CustomSetId);\n\t\tif (customIt != sets.cend()) {\n\t\t\tauto &custom = customIt->second->stickers;\n\t\t\tadded = custom.size();\n\t\t\tif (skipPremium) {\n\t\t\t\tadded -= ranges::count(\n\t\t\t\t\tcustom,\n\t\t\t\t\ttrue,\n\t\t\t\t\t&DocumentData::isPremiumSticker);\n\t\t\t}\n\t\t\tfor (const auto &sticker : recent) {\n\t\t\t\tif (skipPremium && sticker.first->isPremiumSticker()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (customIt->second->stickers.indexOf(sticker.first) < 0) {\n\t\t\t\t\t++added;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tadded = recent.size();\n\t\t}\n\t}\n\treturn result + added;\n}\n\nData::StickersSetFlags StickersBox::Inner::fillSetFlags(\n\t\tnot_null<StickersSet*> set) const {\n\tconst auto result = set->flags;\n\treturn (_section == Section::Featured)\n\t\t? result\n\t\t: (result & ~SetFlag::Unread);\n}\n\ntemplate <typename Check>\nStickersSetsOrder StickersBox::Inner::collectSets(Check check) const {\n\tStickersSetsOrder result;\n\tresult.reserve(_rows.size());\n\tfor (const auto &row : _rows) {\n\t\tif (check(row.get())) {\n\t\t\tresult.push_back(row->set->id);\n\t\t}\n\t}\n\treturn result;\n}\n\nStickersSetsOrder StickersBox::Inner::order() const {\n\treturn collectSets([](Row *row) {\n\t\treturn !row->isArchived() && !row->removed && !row->isRecentSet();\n\t});\n}\n\nStickersSetsOrder StickersBox::Inner::fullOrder() const {\n\treturn collectSets([](Row *row) {\n\t\treturn !row->isRecentSet();\n\t});\n}\n\nStickersSetsOrder StickersBox::Inner::removedSets() const {\n\treturn collectSets([](Row *row) {\n\t\treturn row->removed;\n\t});\n}\n\nint StickersBox::Inner::getRowIndex(uint64 setId) const {\n\tfor (auto i = 0, count = int(_rows.size()); i != count; ++i) {\n\t\tauto &row = _rows[i];\n\t\tif (row->set->id == setId) {\n\t\t\treturn i;\n\t\t}\n\t}\n\treturn -1;\n}\n\nvoid StickersBox::Inner::setFullOrder(const StickersSetsOrder &order) {\n\tfor (const auto setId : order) {\n\t\tauto index = getRowIndex(setId);\n\t\tif (index >= 0) {\n\t\t\tauto row = std::move(_rows[index]);\n\t\t\tauto count = _rows.size();\n\t\t\tfor (auto i = index + 1; i != count; ++i) {\n\t\t\t\t_rows[i - 1] = std::move(_rows[i]);\n\t\t\t}\n\t\t\t_rows[count - 1] = std::move(row);\n\t\t}\n\t}\n}\n\nvoid StickersBox::Inner::setRemovedSets(const StickersSetsOrder &removed) {\n\tfor (auto i = 0, count = int(_rows.size()); i != count; ++i) {\n\t\tsetRowRemoved(i, removed.contains(_rows[i]->set->id));\n\t}\n}\n\nvoid StickersBox::Inner::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\t_visibleTop = visibleTop;\n\t_visibleBottom = visibleBottom;\n\tupdateScrollbarWidth();\n\tif (_section == Section::Featured) {\n\t\treadVisibleSets();\n\t}\n\tcheckLoadMore();\n}\n\nvoid StickersBox::Inner::checkLoadMore() {\n\tif (_loadMoreCallback) {\n\t\tauto scrollHeight = (_visibleBottom - _visibleTop);\n\t\tint scrollTop = _visibleTop, scrollTopMax = height() - scrollHeight;\n\t\tif (scrollTop + PreloadHeightsCount * scrollHeight >= scrollTopMax) {\n\t\t\t_loadMoreCallback();\n\t\t}\n\t}\n}\n\nvoid StickersBox::Inner::readVisibleSets() {\n\tauto itemsVisibleTop = _visibleTop - _itemsTop;\n\tauto itemsVisibleBottom = _visibleBottom - _itemsTop;\n\tint rowFrom = floorclamp(itemsVisibleTop, _rowHeight, 0, _rows.size());\n\tint rowTo = ceilclamp(itemsVisibleBottom, _rowHeight, 0, _rows.size());\n\tfor (int i = rowFrom; i < rowTo; ++i) {\n\t\tconst auto row = _rows[i].get();\n\t\tif (!row->isUnread()) {\n\t\t\tcontinue;\n\t\t}\n\t\tif ((i * _rowHeight < itemsVisibleTop)\n\t\t\t|| ((i + 1) * _rowHeight > itemsVisibleBottom)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto thumbnailLoading = row->set->hasThumbnail()\n\t\t\t? row->set->thumbnailLoading()\n\t\t\t: row->sticker\n\t\t\t? row->sticker->thumbnailLoading()\n\t\t\t: false;\n\t\tconst auto thumbnailLoaded = row->set->hasThumbnail()\n\t\t\t? (row->thumbnailMedia\n\t\t\t\t&& (row->thumbnailMedia->image()\n\t\t\t\t\t|| !row->thumbnailMedia->content().isEmpty()))\n\t\t\t: row->sticker\n\t\t\t? (row->stickerMedia && row->stickerMedia->loaded())\n\t\t\t: true;\n\t\tif (!thumbnailLoading || thumbnailLoaded) {\n\t\t\tsession().api().readFeaturedSetDelayed(row->set->id);\n\t\t}\n\t}\n}\n\nvoid StickersBox::Inner::updateScrollbarWidth() {\n\tauto width = (_visibleBottom - _visibleTop < height()) ? (st::boxScroll.width - st::boxScroll.deltax) : 0;\n\tif (_scrollbar != width) {\n\t\t_scrollbar = width;\n\t\tupdate();\n\t}\n}\n\nStickersBox::Inner::~Inner() {\n\tclear();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/stickers_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n#include \"base/timer.h\"\n#include \"mtproto/sender.h\"\n#include \"data/stickers/data_stickers_set.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace style {\nstruct RippleAnimation;\nstruct PeerListItem;\n} // namespace style\n\nnamespace Ui {\nclass PlainShadow;\nclass RippleAnimation;\nclass SettingsSlider;\nclass SlideAnimation;\nclass CrossButton;\nclass BoxContentDivider;\n} // namespace Ui\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\nclass DocumentMedia;\nenum class StickersType : uchar;\n} // namespace Data\n\nnamespace Lottie {\nclass SinglePlayer;\n} // namespace Lottie\n\nnamespace Stickers {\nclass Set;\n} // namespace Stickers\n\nclass StickersBox final : public Ui::BoxContent {\npublic:\n\tenum class Section {\n\t\tInstalled,\n\t\tFeatured,\n\t\tArchived,\n\t\tAttached,\n\t\tMasks,\n\t};\n\n\tStickersBox(\n\t\tQWidget*,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tSection section,\n\t\tbool masks = false);\n\tStickersBox(\n\t\tQWidget*,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<ChannelData*> megagroup,\n\t\tbool isEmoji);\n\tStickersBox(\n\t\tQWidget*,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst QVector<MTPStickerSetCovered> &attachedSets);\n\tStickersBox(\n\t\tQWidget*,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst std::vector<StickerSetIdentifier> &emojiSets);\n\t~StickersBox();\n\n\t[[nodiscard]] Main::Session &session() const;\n\n\tvoid setInnerFocus() override;\n\nprotected:\n\tvoid prepare() override;\n\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tclass Inner;\n\tclass Tab {\n\tpublic:\n\t\tTab() = default;\n\n\t\ttemplate <typename ...Args>\n\t\tTab(int index, Args&&... args);\n\n\t\tobject_ptr<Inner> takeWidget();\n\t\tvoid returnWidget(object_ptr<Inner> widget);\n\n\t\t[[nodiscard]] Inner *widget() const;\n\t\t[[nodiscard]] int index() const;\n\n\t\tvoid saveScrollTop();\n\t\tint scrollTop() const {\n\t\t\treturn _scrollTop;\n\t\t}\n\n\tprivate:\n\t\tconst int _index = 0;\n\t\tobject_ptr<Inner> _widget = { nullptr };\n\t\tQPointer<Inner> _weak;\n\t\tint _scrollTop = 0;\n\n\t};\n\n\tvoid handleStickersUpdated();\n\tvoid refreshTabs();\n\tvoid rebuildList(Tab *tab = nullptr);\n\tvoid updateTabsGeometry();\n\tvoid switchTab();\n\tvoid installSet(uint64 setId);\n\tint topSkip() const;\n\tvoid saveChanges();\n\n\tQPixmap grabContentCache();\n\n\tvoid installDone(const MTPmessages_StickerSetInstallResult &result) const;\n\tvoid installFail(const MTP::Error &error, uint64 setId);\n\n\tvoid preloadArchivedSets();\n\tvoid requestArchivedSets();\n\tvoid loadMoreArchived();\n\tvoid getArchivedDone(\n\t\tconst MTPmessages_ArchivedStickers &result,\n\t\tuint64 offsetId);\n\tvoid showAttachedStickers();\n\n\tconst Data::StickersSetsOrder &archivedSetsOrder() const;\n\tData::StickersSetsOrder &archivedSetsOrderRef() const;\n\n\tstd::array<Inner*, 5> widgets() const;\n\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\n\tobject_ptr<Ui::SettingsSlider> _tabs = { nullptr };\n\tQList<Section> _tabIndices;\n\tbool _ignoreTabActivation = false;\n\n\tclass CounterWidget;\n\tobject_ptr<CounterWidget> _unreadBadge = { nullptr };\n\n\tSection _section;\n\tconst bool _isMasks;\n\tconst bool _isEmoji;\n\n\tTab _installed;\n\tTab _masks;\n\tTab _featured;\n\tTab _archived;\n\tTab _attached;\n\tTab *_tab = nullptr;\n\n\tconst Data::StickersType _attachedType = {};\n\tconst QVector<MTPStickerSetCovered> _attachedSets;\n\tconst std::vector<StickerSetIdentifier> _emojiSets;\n\n\tChannelData *_megagroupSet = nullptr;\n\n\tstd::unique_ptr<Ui::SlideAnimation> _slideAnimation;\n\tobject_ptr<Ui::PlainShadow> _titleShadow = { nullptr };\n\n\tmtpRequestId _archivedRequestId = 0;\n\tbool _archivedLoaded = false;\n\tbool _allArchivedLoaded = false;\n\tbool _someArchivedLoaded = false;\n\n\tData::StickersSetsOrder _localOrder;\n\tData::StickersSetsOrder _localRemoved;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/transfer_gift_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/transfer_gift_box.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_credits.h\"\n#include \"api/api_cloud_password.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/filters/edit_filter_chats_list.h\" // CreatePe...tionSubtitle.\n#include \"boxes/peers/replace_boost_box.h\"\n#include \"boxes/gift_premium_box.h\"\n#include \"boxes/passcode_box.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"boxes/star_gift_resale_box.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"data/data_session.h\"\n#include \"data/data_star_gift.h\"\n#include \"data/data_thread.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"payments/payments_checkout_process.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/sub_tabs.h\"\n#include \"ui/controls/ton_common.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\" // peerListSingleRow.\n#include \"styles/style_credits.h\" // starIconEmoji.\n#include \"styles/style_dialogs.h\" // recentPeersSpecialName.\n#include \"styles/style_info.h\" // defaultSubTabs.\n#include \"styles/style_layers.h\" // boxLabel.\n\nnamespace {\n\nstruct ExportOption {\n\tobject_ptr<Ui::RpWidget> content = { nullptr };\n\tFn<bool(int, int, int)> overrideKey;\n\tFn<void()> activate;\n};\n\nclass Controller final : public ContactsBoxController {\npublic:\n\tController(\n\t\tnot_null<Window::SessionController*> window,\n\t\tstd::shared_ptr<Data::UniqueGift> gift,\n\t\tData::SavedStarGiftId savedId,\n\t\tFn<void(not_null<PeerData*>, Fn<void()>)> choose);\n\n\tvoid init(not_null<PeerListBox*> box);\n\n\tvoid noSearchSubmit();\n\nprivate:\n\tvoid prepareViewHook() override;\n\n\tbool overrideKeyboardNavigation(\n\t\tint direction,\n\t\tint fromIndex,\n\t\tint toIndex) override;\n\n\tstd::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<UserData*> user) override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\n\tconst not_null<Window::SessionController*> _window;\n\tconst std::shared_ptr<Data::UniqueGift> _gift;\n\tconst Data::SavedStarGiftId _giftId;\n\tconst Fn<void(not_null<PeerData*>, Fn<void()>)> _choose;\n\tExportOption _exportOption;\n\tQPointer<PeerListBox> _box;\n\n};\n\nvoid ConfirmExportBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<Data::UniqueGift> gift,\n\t\tFn<void(Fn<void()> close)> confirmed) {\n\tbox->setTitle(tr::lng_gift_transfer_confirm_title());\n\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\tbox,\n\t\ttr::lng_gift_transfer_confirm_text(\n\t\t\tlt_name,\n\t\t\trpl::single(tr::bold(UniqueGiftName(*gift))),\n\t\t\ttr::marked),\n\t\tst::boxLabel));\n\tbox->addButton(tr::lng_gift_transfer_confirm_button(), [=] {\n\t\tconfirmed([weak = base::make_weak(box)] {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t});\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nvoid ExportOnBlockchain(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<Data::UniqueGift> gift,\n\t\tData::SavedStarGiftId giftId,\n\t\tFn<void()> boxShown,\n\t\tFn<void()> wentToUrl) {\n\tstruct State {\n\t\tbool loading = false;\n\t\trpl::lifetime lifetime;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tconst auto session = &window->session();\n\tconst auto show = window->uiShow();\n\tsession->api().cloudPassword().reload();\n\tsession->api().request(\n\t\tMTPpayments_GetStarGiftWithdrawalUrl(\n\t\t\tApi::InputSavedStarGiftId(giftId),\n\t\t\tMTP_inputCheckPasswordEmpty())\n\t).fail([=](const MTP::Error &error) {\n\t\tauto box = PrePasswordErrorBox(\n\t\t\terror.type(),\n\t\t\tsession,\n\t\t\tTextWithEntities{\n\t\t\t\ttr::lng_gift_transfer_password_about(tr::now),\n\t\t\t});\n\t\tif (box) {\n\t\t\tshow->show(std::move(box));\n\t\t\tboxShown();\n\t\t\treturn;\n\t\t}\n\t\tstate->lifetime = session->api().cloudPassword().state(\n\t\t) | rpl::take(\n\t\t\t1\n\t\t) | rpl::on_next([=](const Core::CloudPasswordState &pass) {\n\t\t\tstate->lifetime.destroy();\n\n\t\t\tauto fields = PasscodeBox::CloudFields::From(pass);\n\t\t\tfields.customTitle = tr::lng_gift_transfer_password_title();\n\t\t\tfields.customDescription\n\t\t\t\t= tr::lng_gift_transfer_password_description(tr::now);\n\t\t\tfields.customSubmitButton = tr::lng_passcode_submit();\n\t\t\tfields.customCheckCallback = crl::guard(parent, [=](\n\t\t\t\t\tconst Core::CloudPasswordResult &result,\n\t\t\t\t\tbase::weak_qptr<PasscodeBox> box) {\n\t\t\t\tusing ExportUrl = MTPpayments_StarGiftWithdrawalUrl;\n\t\t\t\tsession->api().request(\n\t\t\t\t\tMTPpayments_GetStarGiftWithdrawalUrl(\n\t\t\t\t\t\tApi::InputSavedStarGiftId(giftId),\n\t\t\t\t\t\tresult.result)\n\t\t\t\t).done([=](const ExportUrl &result) {\n\t\t\t\t\tUrlClickHandler::Open(qs(result.data().vurl()));\n\t\t\t\t\twentToUrl();\n\t\t\t\t\tif (box) {\n\t\t\t\t\t\tbox->closeBox();\n\t\t\t\t\t}\n\t\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\t\tconst auto message = error.type();\n\t\t\t\t\tif (box && !box->handleCustomCheckError(message)) {\n\t\t\t\t\t\tshow->showToast(message);\n\t\t\t\t\t}\n\t\t\t\t}).send();\n\t\t\t});\n\t\t\tshow->show(Box<PasscodeBox>(session, fields));\n\t\t\tboxShown();\n\t\t});\n\t}).send();\n}\n\n[[nodiscard]] ExportOption MakeExportOption(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerListBox*> box,\n\t\tstd::shared_ptr<Data::UniqueGift> gift,\n\t\tData::SavedStarGiftId giftId,\n\t\tTimeId when) {\n\tstruct State {\n\t\tbool exporting = false;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tconst auto activate = [=] {\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto weak = base::make_weak(box);\n\t\tconst auto left = (when > now) ? (when - now) : 0;\n\t\tconst auto hours = left ? std::max((left + 1800) / 3600, 1) : 0;\n\t\tif (!hours) {\n\t\t\twindow->show(Box(ConfirmExportBox, gift, [=](Fn<void()> close) {\n\t\t\t\tif (state->exporting) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tstate->exporting = true;\n\t\t\t\tExportOnBlockchain(window, box, gift, giftId, [=] {\n\t\t\t\t\tstate->exporting = false;\n\t\t\t\t\tclose();\n\t\t\t\t}, [=] {\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t\tclose();\n\t\t\t\t});\n\t\t\t}));\n\t\t\treturn;\n\t\t}\n\t\twindow->show(Ui::MakeInformBox({\n\t\t\t.text = tr::lng_gift_transfer_unlocks_about(\n\t\t\t\tlt_when,\n\t\t\t\t((hours >= 24)\n\t\t\t\t\t? tr::lng_gift_transfer_unlocks_when_days(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single((hours / 24) * 1.))\n\t\t\t\t\t: tr::lng_gift_transfer_unlocks_when_hours(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single(hours * 1.)))),\n\t\t\t.title = tr::lng_gift_transfer_unlocks_title(),\n\t\t}));\n\t};\n\n\tclass ExportRow final : public PeerListRow {\n\tpublic:\n\t\texplicit ExportRow(TimeId when)\n\t\t: PeerListRow(Data::FakePeerIdForJustName(\"ton-export\").value) {\n\t\t\tconst auto now = base::unixtime::now();\n\t\t\t_available = (when <= now);\n\t\t\tif (const auto left = when - now; left > 0) {\n\t\t\t\tconst auto hours = std::max((left + 1800) / 3600, 1);\n\t\t\t\tconst auto days = hours / 24;\n\t\t\t\tsetCustomStatus(days\n\t\t\t\t\t? tr::lng_gift_transfer_unlocks_days(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tdays)\n\t\t\t\t\t: tr::lng_gift_transfer_unlocks_hours(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\thours));\n\t\t\t}\n\t\t}\n\n\t\tQString generateName() override {\n\t\t\treturn tr::lng_gift_transfer_via_blockchain(tr::now);\n\t\t}\n\t\tQString generateShortName() override {\n\t\t\treturn generateName();\n\t\t}\n\t\tauto generatePaintUserpicCallback(bool forceRound)\n\t\t-> PaintRoundImageCallback override {\n\t\t\treturn [=](\n\t\t\t\t\tPainter &p,\n\t\t\t\t\tint x,\n\t\t\t\t\tint y,\n\t\t\t\t\tint outerWidth,\n\t\t\t\t\tint size) mutable {\n\t\t\t\tUi::EmptyUserpic::PaintCurrency(p, x, y, outerWidth, size);\n\t\t\t};\n\t\t}\n\n\t\tconst style::PeerListItem &computeSt(\n\t\t\t\tconst style::PeerListItem &st) const override {\n\t\t\t_st = st;\n\t\t\t_st.namePosition.setY(\n\t\t\t\tst::recentPeersSpecialName.namePosition.y());\n\t\t\treturn _available ? _st : st;\n\t\t}\n\n\tprivate:\n\t\tmutable style::PeerListItem _st;\n\t\tbool _available = false;\n\n\t};\n\n\tclass ExportController final : public PeerListController {\n\tpublic:\n\t\tExportController(\n\t\t\tnot_null<Main::Session*> session,\n\t\t\tTimeId when,\n\t\t\tFn<void()> activate)\n\t\t: _session(session)\n\t\t, _when(when)\n\t\t, _activate(std::move(activate)) {\n\t\t}\n\n\t\tvoid prepare() override {\n\t\t\tdelegate()->peerListAppendRow(\n\t\t\t\tstd::make_unique<ExportRow>(_when));\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t\tvoid loadMoreRows() override {\n\t\t}\n\t\tvoid rowClicked(not_null<PeerListRow*> row) override {\n\t\t\t_activate();\n\t\t}\n\t\tMain::Session &session() const override {\n\t\t\treturn *_session;\n\t\t}\n\n\tprivate:\n\t\tconst not_null<Main::Session*> _session;\n\t\tTimeId _when = 0;\n\t\tFn<void()> _activate;\n\n\t};\n\n\tauto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tconst auto container = result.data();\n\n\tUi::AddSkip(container);\n\n\tconst auto delegate = container->lifetime().make_state<\n\t\tPeerListContentDelegateSimple\n\t>();\n\tconst auto controller = container->lifetime().make_state<\n\t\tExportController\n\t>(&window->session(), when, activate);\n\tcontroller->setStyleOverrides(&st::peerListSingleRow);\n\tconst auto content = container->add(object_ptr<PeerListContent>(\n\t\tcontainer,\n\t\tcontroller));\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\n\tUi::AddSkip(container);\n\tcontainer->add(CreatePeerListSectionSubtitle(\n\t\tcontainer,\n\t\ttr::lng_contacts_header()));\n\n\tconst auto overrideKey = [=](int direction, int from, int to) {\n\t\tif (!content->isVisible()) {\n\t\t\treturn false;\n\t\t} else if (direction > 0 && from < 0 && to >= 0) {\n\t\t\tif (content->hasSelection()) {\n\t\t\t\tconst auto was = content->selectedIndex();\n\t\t\t\tconst auto now = content->selectSkip(1).reallyMovedTo;\n\t\t\t\tif (was != now) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tcontent->clearSelection();\n\t\t\t} else {\n\t\t\t\tcontent->selectSkip(1);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (direction < 0 && to < 0) {\n\t\t\tif (!content->hasSelection()) {\n\t\t\t\tcontent->selectLast();\n\t\t\t} else if (from >= 0 || content->hasSelection()) {\n\t\t\t\tcontent->selectSkip(-1);\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\n\treturn {\n\t\t.content = std::move(result),\n\t\t.overrideKey = overrideKey,\n\t\t.activate = activate,\n\t};\n}\n\nController::Controller(\n\tnot_null<Window::SessionController*> window,\n\tstd::shared_ptr<Data::UniqueGift> gift,\n\tData::SavedStarGiftId giftId,\n\tFn<void(not_null<PeerData*>, Fn<void()>)> choose)\n: ContactsBoxController(&window->session())\n, _window(window)\n, _gift(std::move(gift))\n, _giftId(giftId)\n, _choose(std::move(choose)) {\n\tif (_gift->exportAt) {\n\t\tsetStyleOverrides(&st::peerListSmallSkips);\n\t}\n}\n\nvoid Controller::init(not_null<PeerListBox*> box) {\n\t_box = box;\n\tif (const auto when = _gift->exportAt) {\n\t\t_exportOption = MakeExportOption(_window, box, _gift, _giftId, when);\n\t\tdelegate()->peerListSetAboveWidget(std::move(_exportOption.content));\n\t\tdelegate()->peerListRefreshRows();\n\t}\n}\n\nvoid Controller::noSearchSubmit() {\n\tif (const auto onstack = _exportOption.activate) {\n\t\tonstack();\n\t}\n}\n\nbool Controller::overrideKeyboardNavigation(\n\t\tint direction,\n\t\tint fromIndex,\n\t\tint toIndex) {\n\treturn _exportOption.overrideKey\n\t\t&& _exportOption.overrideKey(direction, fromIndex, toIndex);\n}\n\nvoid Controller::prepareViewHook() {\n\tdelegate()->peerListSetTitle(tr::lng_gift_transfer_title(\n\t\tlt_name,\n\t\trpl::single(UniqueGiftName(*_gift))));\n}\n\nstd::unique_ptr<PeerListRow> Controller::createRow(\n\tnot_null<UserData*> user) {\n\tif (user->isSelf()\n\t\t|| user->isBot()\n\t\t|| user->isServiceUser()\n\t\t|| user->isInaccessible()) {\n\t\treturn nullptr;\n\t}\n\treturn ContactsBoxController::createRow(user);\n}\n\nvoid Controller::rowClicked(not_null<PeerListRow*> row) {\n\t_choose(row->peer(), [parentBox = _box] {\n\t\tif (const auto strong = parentBox.data()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t});\n}\n\nvoid TransferGift(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerData*> to,\n\t\tstd::shared_ptr<Data::UniqueGift> gift,\n\t\tData::SavedStarGiftId savedId,\n\t\tFn<void(Payments::CheckoutResult)> done,\n\t\tbool skipPaymentForm = false) {\n\tconst auto session = &window->session();\n\tconst auto weak = base::make_weak(window);\n\tauto formDone = [=](\n\t\t\tPayments::CheckoutResult result,\n\t\t\tconst MTPUpdates *updates) {\n\t\tif (result == Payments::CheckoutResult::Free) {\n\t\t\tAssert(!skipPaymentForm);\n\t\t\tTransferGift(window, to, gift, savedId, done, true);\n\t\t\treturn;\n\t\t}\n\t\tdone(result);\n\t\tif (result == Payments::CheckoutResult::Paid) {\n\t\t\tsession->data().notifyGiftUpdate({\n\t\t\t\t.id = savedId,\n\t\t\t\t.action = Data::GiftUpdate::Action::Transfer,\n\t\t\t});\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tUi::ShowGiftTransferredToast(strong->uiShow(), to, *gift);\n\t\t\t}\n\t\t}\n\t};\n\tif (skipPaymentForm) {\n\t\t// We can't check (gift->starsForTransfer <= 0) here.\n\t\t//\n\t\t// Sometimes we don't know the price for transfer.\n\t\t// Like when we transfer a gift from Resale tab.\n\t\tsession->api().request(MTPpayments_TransferStarGift(\n\t\t\tApi::InputSavedStarGiftId(savedId, gift),\n\t\t\tto->input()\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\tsession->api().applyUpdates(result);\n\t\t\tformDone(Payments::CheckoutResult::Paid, &result);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tformDone(Payments::CheckoutResult::Failed, nullptr);\n\t\t\tconst auto earlyPrefix = u\"STARGIFT_TRANSFER_TOO_EARLY_\"_q;\n\t\t\tconst auto type = error.type();\n\t\t\tif (type.startsWith(earlyPrefix)) {\n\t\t\t\tconst auto seconds = type.mid(earlyPrefix.size()).toInt();\n\t\t\t\tconst auto newAvailableAt = base::unixtime::now() + seconds;\n\t\t\t\tgift->canTransferAt = newAvailableAt;\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tShowTransferGiftLater(strong->uiShow(), gift);\n\t\t\t\t}\n\t\t\t} else if (const auto strong = weak.get()) {\n\t\t\t\tif (!Ui::ShowGiftErrorToast(strong->uiShow(), error)) {\n\t\t\t\t\tstrong->showToast(type);\n\t\t\t\t}\n\t\t\t}\n\t\t}).send();\n\t} else {\n\t\tUi::RequestStarsFormAndSubmit(\n\t\t\twindow->uiShow(),\n\t\t\tMTP_inputInvoiceStarGiftTransfer(\n\t\t\t\tApi::InputSavedStarGiftId(savedId, gift),\n\t\t\t\tto->input()),\n\t\t\tstd::move(formDone));\n\t}\n}\n\nvoid ResolveGiftSaleOffer(\n\t\tnot_null<Window::SessionController*> window,\n\t\tMsgId id,\n\t\tbool accept,\n\t\tFn<void(bool)> done) {\n\tusing Flag = MTPpayments_ResolveStarGiftOffer::Flag;\n\tconst auto session = &window->session();\n\tconst auto show = window->uiShow();\n\tsession->api().request(MTPpayments_ResolveStarGiftOffer(\n\t\tMTP_flags(accept ? Flag() : Flag::f_decline),\n\t\tMTP_int(id.bare)\n\t)).done([=](const MTPUpdates &result) {\n\t\tsession->api().applyUpdates(result);\n\t\tdone(true);\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (!Ui::ShowGiftErrorToast(show, error)) {\n\t\t\tshow->showToast(error.type());\n\t\t}\n\t\tdone(false);\n\t}).send();\n}\n\nvoid BuyResaleGift(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> to,\n\t\tstd::shared_ptr<Data::UniqueGift> gift,\n\t\tCreditsType type,\n\t\tFn<void(Payments::CheckoutResult)> done) {\n\tauto paymentDone = [=](\n\t\t\tPayments::CheckoutResult result,\n\t\t\tconst MTPUpdates *updates) {\n\t\tif (result == Payments::CheckoutResult::Paid) {\n\t\t\tgift->starsForResale = 0;\n\t\t}\n\t\tdone(result);\n\t\tif (result == Payments::CheckoutResult::Paid) {\n\t\t\tto->owner().notifyGiftUpdate({\n\t\t\t\t.slug = gift->slug,\n\t\t\t\t.action = Data::GiftUpdate::Action::ResaleChange,\n\t\t\t});\n\t\t\tUi::ShowResaleGiftBoughtToast(show, to, *gift);\n\t\t}\n\t};\n\n\tusing Flag = MTPDinputInvoiceStarGiftResale::Flag;\n\tconst auto invoice = MTP_inputInvoiceStarGiftResale(\n\t\tMTP_flags((type == CreditsType::Ton) ? Flag::f_ton : Flag()),\n\t\tMTP_string(gift->slug),\n\t\tto->input());\n\n\tUi::RequestOurForm(show, invoice, [=](\n\t\t\tuint64 formId,\n\t\t\tCreditsAmount price,\n\t\t\tstd::optional<Payments::CheckoutResult> failure) {\n\t\tif ((type == CreditsType::Ton && price.stars())\n\t\t\t|| (type == CreditsType::Stars && price.ton())) {\n\t\t\tpaymentDone(Payments::CheckoutResult::Failed, nullptr);\n\t\t\treturn;\n\t\t}\n\t\tconst auto submit = [=] {\n\t\t\tif (price.stars()) {\n\t\t\t\tSubmitStarsForm(\n\t\t\t\t\tshow,\n\t\t\t\t\tinvoice,\n\t\t\t\t\tformId,\n\t\t\t\t\tprice.whole(),\n\t\t\t\t\tpaymentDone);\n\t\t\t} else {\n\t\t\t\tSubmitTonForm(show, invoice, formId, price, paymentDone);\n\t\t\t}\n\t\t};\n\t\tconst auto was = (type == CreditsType::Ton)\n\t\t\t? Data::UniqueGiftResaleTon(*gift)\n\t\t\t: Data::UniqueGiftResaleStars(*gift);\n\t\tif (failure) {\n\t\t\tpaymentDone(*failure, nullptr);\n\t\t} else if (price != was) {\n\t\t\tconst auto cost = price.ton()\n\t\t\t\t? Ui::Text::IconEmoji(&st::tonIconEmoji).append(\n\t\t\t\t\tLang::FormatCreditsAmountDecimal(price))\n\t\t\t\t: Ui::Text::IconEmoji(&st::starIconEmoji).append(\n\t\t\t\t\tLang::FormatCountDecimal(price.whole()));\n\t\t\tconst auto cancelled = [=](Fn<void()> close) {\n\t\t\t\tpaymentDone(Payments::CheckoutResult::Cancelled, nullptr);\n\t\t\t\tclose();\n\t\t\t};\n\t\t\tshow->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_gift_buy_price_change_text(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_price,\n\t\t\t\t\tUi::Text::Wrapped(cost, EntityType::Bold),\n\t\t\t\t\ttr::marked),\n\t\t\t\t.confirmed = [=](Fn<void()> close) { close(); submit(); },\n\t\t\t\t.cancelled = cancelled,\n\t\t\t\t.confirmText = tr::lng_gift_buy_resale_button(\n\t\t\t\t\tlt_cost,\n\t\t\t\t\trpl::single(cost),\n\t\t\t\t\ttr::marked),\n\t\t\t\t.title = tr::lng_gift_buy_price_change_title(),\n\t\t\t}));\n\t\t} else {\n\t\t\tsubmit();\n\t\t}\n\t});\n}\n\n} // namespace\n\nvoid ShowTransferToBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<Data::UniqueGift> gift,\n\t\tData::SavedStarGiftId savedId,\n\t\tFn<void()> closeParentBox) {\n\tconst auto stars = gift->starsForTransfer;\n\tcontroller->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tauto transfer = (stars > 0)\n\t\t\t? tr::lng_gift_transfer_button_for(\n\t\t\t\tlt_price,\n\t\t\t\trpl::single(Ui::Text::IconEmoji(\n\t\t\t\t\t&st::starIconEmoji\n\t\t\t\t).append(Lang::FormatCreditsAmountDecimal(\n\t\t\t\t\tCreditsAmount(stars)\n\t\t\t\t))),\n\t\t\t\ttr::marked)\n\t\t\t: tr::lng_gift_transfer_button(tr::marked);\n\n\t\tstruct State {\n\t\t\tbool sent = false;\n\t\t};\n\t\tconst auto state = std::make_shared<State>();\n\t\tauto callback = [=] {\n\t\t\tif (state->sent) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->sent = true;\n\t\t\tconst auto weak = base::make_weak(box);\n\t\t\tconst auto done = [=](Payments::CheckoutResult result) {\n\t\t\t\tif (result == Payments::CheckoutResult::Cancelled) {\n\t\t\t\t\tcloseParentBox();\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t} else if (result != Payments::CheckoutResult::Paid) {\n\t\t\t\t\tstate->sent = false;\n\t\t\t\t} else {\n\t\t\t\t\tif (savedId.isUser()) {\n\t\t\t\t\t\tcontroller->showPeerHistory(peer);\n\t\t\t\t\t}\n\t\t\t\t\tcloseParentBox();\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\tTransferGift(controller, peer, gift, savedId, done);\n\t\t};\n\n\t\tbox->addRow(\n\t\t\tCreateGiftTransfer(box->verticalLayout(), gift, peer),\n\t\t\tQMargins(0, st::boxPadding.top(), 0, 0));\n\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = (stars > 0)\n\t\t\t\t? tr::lng_gift_transfer_sure_for(\n\t\t\t\t\tlt_name,\n\t\t\t\t\trpl::single(tr::bold(UniqueGiftName(*gift))),\n\t\t\t\t\tlt_recipient,\n\t\t\t\t\trpl::single(tr::bold(peer->shortName())),\n\t\t\t\t\tlt_price,\n\t\t\t\t\ttr::lng_action_gift_for_stars(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single(stars * 1.),\n\t\t\t\t\t\ttr::bold),\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_gift_transfer_sure(\n\t\t\t\t\tlt_name,\n\t\t\t\t\trpl::single(tr::bold(UniqueGiftName(*gift))),\n\t\t\t\t\tlt_recipient,\n\t\t\t\t\trpl::single(tr::bold(peer->shortName())),\n\t\t\t\t\ttr::marked),\n\t\t\t.confirmed = std::move(callback),\n\t\t\t.confirmText = std::move(transfer),\n\t\t});\n\n\t\tconst auto show = controller->uiShow();\n\t\tAddTransferGiftTable(show, box->verticalLayout(), gift);\n\t}));\n}\n\nvoid ShowTransferGiftBox(\n\t\tnot_null<Window::SessionController*> window,\n\t\tstd::shared_ptr<Data::UniqueGift> gift,\n\t\tData::SavedStarGiftId savedId) {\n\tif (ShowTransferGiftLater(window->uiShow(), gift)) {\n\t\treturn;\n\t}\n\tauto controller = std::make_unique<Controller>(\n\t\twindow,\n\t\tgift,\n\t\tsavedId,\n\t\t[=](not_null<PeerData*> peer, Fn<void()> done) {\n\t\t\tShowTransferToBox(window, peer, gift, savedId, done);\n\t\t});\n\tconst auto controllerRaw = controller.get();\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tcontrollerRaw->init(box);\n\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\n\t\tbox->noSearchSubmits() | rpl::on_next([=] {\n\t\t\tcontrollerRaw->noSearchSubmit();\n\t\t}, box->lifetime());\n\t};\n\twindow->show(\n\t\tBox<PeerListBox>(std::move(controller), std::move(initBox)),\n\t\tUi::LayerOption::KeepOther);\n}\n\nvoid ShowGiftSaleAcceptBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<HistoryMessageSuggestion*> suggestion) {\n\tconst auto id = item->id;\n\tconst auto peer = item->history()->peer;\n\tconst auto gift = suggestion->gift;\n\tconst auto price = suggestion->price;\n\n\tconst auto &appConfig = controller->session().appConfig();\n\tconst auto starsThousandths = appConfig.giftResaleStarsThousandths();\n\tconst auto nanoTonThousandths = appConfig.giftResaleNanoTonThousandths();\n\n\tcontroller->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tstruct State {\n\t\t\tbool sent = false;\n\t\t};\n\t\tconst auto state = std::make_shared<State>();\n\t\tauto callback = [=] {\n\t\t\tif (state->sent) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->sent = true;\n\t\t\tconst auto weak = base::make_weak(controller);\n\t\t\tconst auto weakBox = base::make_weak(box);\n\t\t\tResolveGiftSaleOffer(controller, id, true, [=](bool ok) {\n\t\t\t\tstate->sent = false;\n\t\t\t\tif (ok) {\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->showPeerHistory(peer->id);\n\t\t\t\t\t}\n\t\t\t\t\tif (const auto strong = weakBox.get()) {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t};\n\n\t\tconst auto receive = price.ton()\n\t\t\t? ((price.value() * nanoTonThousandths) / 1000.)\n\t\t\t: ((int64(price.value()) * starsThousandths) / 1000);\n\n\t\tauto button = tr::lng_gift_offer_sell_for(\n\t\t\tlt_price,\n\t\t\trpl::single(Ui::Text::IconEmoji(price.ton()\n\t\t\t\t? &st::buttonTonIconEmoji\n\t\t\t\t: &st::buttonStarIconEmoji\n\t\t\t).append(Lang::FormatExactCountDecimal(receive))),\n\t\t\ttr::marked);\n\n\t\tbox->addRow(\n\t\t\tCreateGiftTransfer(box->verticalLayout(), gift, peer),\n\t\t\tQMargins(0, st::boxPadding.top(), 0, 0));\n\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = tr::lng_gift_offer_confirm_accept(\n\t\t\t\ttr::now,\n\t\t\t\tlt_name,\n\t\t\t\ttr::bold(UniqueGiftName(*gift)),\n\t\t\t\tlt_user,\n\t\t\t\ttr::bold(peer->shortName()),\n\t\t\t\tlt_cost,\n\t\t\t\ttr::bold(PrepareCreditsAmountText(price)),\n\t\t\t\ttr::marked\n\t\t\t).append(u\"\\n\\n\"_q).append(tr::lng_gift_offer_you_get(\n\t\t\t\ttr::now,\n\t\t\t\tlt_cost,\n\t\t\t\ttr::bold(price.stars()\n\t\t\t\t\t? tr::lng_action_gift_for_stars(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\treceive)\n\t\t\t\t\t: tr::lng_action_gift_for_ton(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\treceive)),\n\t\t\t\ttr::marked)),\n\t\t\t.confirmed = std::move(callback),\n\t\t\t.confirmText = std::move(button),\n\t\t});\n\n\t\tconst auto show = controller->uiShow();\n\t\tauto taken = base::take(gift->value);\n\t\tAddTransferGiftTable(show, box->verticalLayout(), gift);\n\t\tgift->value = std::move(taken);\n\n\t\tif (gift->value.get()) {\n\t\t\tconst auto appConfig = &show->session().appConfig();\n\t\t\tconst auto rule = Ui::LookupCurrencyRule(u\"USD\"_q);\n\t\t\tconst auto value = (gift->value->valuePriceUsd > 0 ? 1 : -1)\n\t\t\t\t* std::abs(gift->value->valuePriceUsd)\n\t\t\t\t/ std::pow(10., rule.exponent);\n\t\t\tif (std::abs(value) >= 0.01) {\n\t\t\t\tconst auto rate = price.ton()\n\t\t\t\t\t? appConfig->currencySellRate()\n\t\t\t\t\t: (appConfig->starsSellRate() / 100.);\n\t\t\t\tconst auto offered = receive * rate;\n\t\t\t\tconst auto diff = offered - value;\n\t\t\t\tconst auto percent = std::abs(diff / value * 100.);\n\t\t\t\tif (percent >= 1) {\n\t\t\t\t\tconst auto higher = (diff > 0.);\n\t\t\t\t\tconst auto good = higher || (percent < 10);\n\t\t\t\t\tconst auto number = int(base::SafeRound(percent));\n\t\t\t\t\tconst auto percentText = QString::number(number) + '%';\n\t\t\t\t\tbox->addRow(\n\t\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\t\tbox,\n\t\t\t\t\t\t\t(higher\n\t\t\t\t\t\t\t\t? tr::lng_gift_offer_higher\n\t\t\t\t\t\t\t\t: tr::lng_gift_offer_lower)(\n\t\t\t\t\t\t\t\t\tlt_percent,\n\t\t\t\t\t\t\t\t\trpl::single(tr::bold(percentText)),\n\t\t\t\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\t\t\t\trpl::single(tr::marked(gift->title)),\n\t\t\t\t\t\t\t\t\ttr::marked),\n\t\t\t\t\t\t\t(good ? st::offerValueGood : st::offerValueBad)),\n\t\t\t\t\t\tst::boxRowPadding + st::offerValuePadding\n\t\t\t\t\t)->setTryMakeSimilarLines(true);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}));\n}\n\nvoid ShowGiftSaleRejectBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<HistoryMessageSuggestion*> suggestion) {\n\tstruct State {\n\t\tbool sent = false;\n\t};\n\tconst auto id = item->id;\n\tconst auto state = std::make_shared<State>();\n\tauto callback = [=](Fn<void()> close) {\n\t\tif (state->sent) {\n\t\t\treturn;\n\t\t}\n\t\tstate->sent = true;\n\t\tconst auto weak = base::make_weak(controller);\n\t\tResolveGiftSaleOffer(controller, id, false, [=](bool ok) {\n\t\t\tstate->sent = false;\n\t\t\tif (ok) {\n\t\t\t\tclose();\n\t\t\t}\n\t\t});\n\t};\n\tcontroller->show(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_gift_offer_confirm_reject(\n\t\t\tlt_user,\n\t\t\trpl::single(tr::bold(item->history()->peer->shortName())),\n\t\t\ttr::marked),\n\t\t.confirmed = std::move(callback),\n\t\t.confirmText = tr::lng_action_gift_offer_decline(),\n\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t.title = tr::lng_gift_offer_reject_title(),\n\t}));\n}\n\nvoid SetThemeFromUniqueGift(\n\t\tnot_null<Window::SessionController*> window,\n\t\tstd::shared_ptr<Data::UniqueGift> unique) {\n\tclass Controller final : public ChooseRecipientBoxController {\n\tpublic:\n\t\tController(\n\t\t\tnot_null<Window::SessionController*> window,\n\t\t\tstd::shared_ptr<Data::UniqueGift> unique)\n\t\t: ChooseRecipientBoxController({\n\t\t\t.session = &window->session(),\n\t\t\t.callback = [=](not_null<Data::Thread*> thread) {\n\t\t\t\tconst auto weak = base::make_weak(window);\n\t\t\t\tconst auto peer = thread->peer();\n\t\t\t\tSendPeerThemeChangeRequest(window, peer, QString(), unique);\n\t\t\t\tif (weak) window->showPeerHistory(peer);\n\t\t\t\tif (weak) window->hideLayer(anim::type::normal);\n\t\t\t},\n\t\t\t.filter = [=](not_null<Data::Thread*> thread) {\n\t\t\t\treturn thread->peer()->isUser();\n\t\t\t},\n\t\t\t.moneyRestrictionError = WriteMoneyRestrictionError,\n\t\t}) {\n\t\t}\n\n\tprivate:\n\t\tvoid prepareViewHook() override {\n\t\t\tChooseRecipientBoxController::prepareViewHook();\n\t\t\tdelegate()->peerListSetTitle(tr::lng_gift_transfer_choose());\n\t\t}\n\n\t};\n\n\twindow->show(\n\t\tBox<PeerListBox>(\n\t\t\tstd::make_unique<Controller>(window, std::move(unique)),\n\t\t\t[](not_null<PeerListBox*> box) {\n\t\t\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t});\n\t\t\t}));\n}\n\nvoid SendPeerThemeChangeRequest(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &token,\n\t\tconst std::shared_ptr<Data::UniqueGift> &unique,\n\t\tbool locallySet) {\n\tconst auto api = &peer->session().api();\n\n\tapi->request(MTPmessages_SetChatWallPaper(\n\t\tMTP_flags(0),\n\t\tpeer->input(),\n\t\tMTPInputWallPaper(),\n\t\tMTPWallPaperSettings(),\n\t\tMTPint()\n\t)).afterDelay(10).done([=](const MTPUpdates &result) {\n\t\tapi->applyUpdates(result);\n\t}).send();\n\n\tapi->request(MTPmessages_SetChatTheme(\n\t\tpeer->input(),\n\t\t(unique\n\t\t\t? MTP_inputChatThemeUniqueGift(MTP_string(unique->slug))\n\t\t\t: MTP_inputChatTheme(MTP_string(token)))\n\t)).done([=](const MTPUpdates &result) {\n\t\tapi->applyUpdates(result);\n\t\tif (!locallySet) {\n\t\t\tpeer->updateFullForced();\n\t\t}\n\t}).send();\n}\n\nvoid SetPeerTheme(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &token,\n\t\tconst std::shared_ptr<Ui::ChatTheme> &theme) {\n\tconst auto giftTheme = token.startsWith(u\"gift:\"_q)\n\t\t? peer->owner().cloudThemes().themeForToken(token)\n\t\t: std::optional<Data::CloudTheme>();\n\n\tpeer->setThemeToken(token);\n\tconst auto dropWallPaper = (peer->wallPaper() != nullptr);\n\tif (dropWallPaper) {\n\t\tpeer->setWallPaper({});\n\t}\n\n\tif (theme) {\n\t\t// Remember while changes propagate through event loop.\n\t\tcontroller->pushLastUsedChatTheme(theme);\n\t}\n\n\tSendPeerThemeChangeRequest(\n\t\tcontroller,\n\t\tpeer,\n\t\ttoken,\n\t\tgiftTheme ? giftTheme->unique : nullptr,\n\t\ttrue);\n}\n\nvoid ShowBuyResaleGiftBox(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tstd::shared_ptr<Data::UniqueGift> gift,\n\t\tbool forceTon,\n\t\tnot_null<PeerData*> to,\n\t\tFn<void(bool ok)> closeParentBox) {\n\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tstruct State {\n\t\t\trpl::variable<bool> ton;\n\t\t\tbool sent = false;\n\t\t};\n\t\tconst auto state = std::make_shared<State>();\n\t\tstate->ton = gift->onlyAcceptTon || forceTon;\n\n\t\tif (gift->onlyAcceptTon) {\n\t\t\tbox->addRow(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tbox,\n\t\t\t\t\ttr::lng_gift_buy_resale_only_ton(\n\t\t\t\t\t\ttr::rich),\n\t\t\t\t\tst::resaleConfirmTonOnly),\n\t\t\t\tst::boxRowPadding + st::resaleConfirmTonOnlyMargin);\n\t\t} else {\n\t\t\tconst auto tabs = box->addRow(\n\t\t\t\tobject_ptr<Ui::SubTabs>(\n\t\t\t\t\tbox,\n\t\t\t\t\tst::defaultSubTabs,\n\t\t\t\t\tUi::SubTabsOptions{\n\t\t\t\t\t\t.selected = (state->ton.current()\n\t\t\t\t\t\t\t? u\"ton\"_q\n\t\t\t\t\t\t\t: u\"stars\"_q),\n\t\t\t\t\t\t.centered = true,\n\t\t\t\t\t},\n\t\t\t\t\tstd::vector<Ui::SubTabsTab>{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tu\"stars\"_q,\n\t\t\t\t\t\t\ttr::lng_gift_buy_resale_pay_stars(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\ttr::marked),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tu\"ton\"_q,\n\t\t\t\t\t\t\ttr::lng_gift_buy_resale_pay_ton(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\ttr::marked),\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\tst::boxRowPadding + st::resaleConfirmTonOnlyMargin);\n\t\t\ttabs->activated() | rpl::on_next([=](QString id) {\n\t\t\t\ttabs->setActiveTab(id);\n\t\t\t\tstate->ton = (id == u\"ton\"_q);\n\t\t\t}, tabs->lifetime());\n\t\t}\n\n\t\tauto transfer = state->ton.value() | rpl::map([=](bool ton) {\n\t\t\treturn tr::lng_gift_buy_resale_button(\n\t\t\t\tlt_cost,\n\t\t\t\trpl::single(ton\n\t\t\t\t\t? Data::FormatGiftResaleTon(*gift)\n\t\t\t\t\t: Data::FormatGiftResaleStars(*gift)),\n\t\t\t\ttr::marked);\n\t\t}) | rpl::flatten_latest();\n\n\t\tauto callback = [=](Fn<void()> close) {\n\t\t\tif (state->sent) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->sent = true;\n\t\t\tconst auto weak = base::make_weak(box);\n\t\t\tconst auto done = [=](Payments::CheckoutResult result) {\n\t\t\t\tif (result == Payments::CheckoutResult::Cancelled) {\n\t\t\t\t\tcloseParentBox(false);\n\t\t\t\t\tclose();\n\t\t\t\t} else if (result != Payments::CheckoutResult::Paid) {\n\t\t\t\t\tstate->sent = false;\n\t\t\t\t} else {\n\t\t\t\t\tcloseParentBox(true);\n\t\t\t\t\tclose();\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto type = state->ton.current()\n\t\t\t\t? CreditsType::Ton\n\t\t\t\t: CreditsType::Stars;\n\t\t\tBuyResaleGift(show, to, gift, type, done);\n\t\t};\n\n\t\tauto price = state->ton.value() | rpl::map([=](bool ton) {\n\t\t\treturn ton\n\t\t\t\t? tr::lng_action_gift_for_ton(\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\trpl::single(gift->nanoTonForResale\n\t\t\t\t\t\t/ float64(Ui::kNanosInOne)),\n\t\t\t\t\ttr::bold)\n\t\t\t\t: tr::lng_action_gift_for_stars(\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\trpl::single(gift->starsForResale * 1.),\n\t\t\t\t\ttr::bold);\n\t\t}) | rpl::flatten_latest();\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = to->isSelf()\n\t\t\t\t? tr::lng_gift_buy_resale_confirm_self(\n\t\t\t\t\tlt_name,\n\t\t\t\t\trpl::single(tr::bold(UniqueGiftName(*gift))),\n\t\t\t\t\tlt_price,\n\t\t\t\t\tstd::move(price),\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_gift_buy_resale_confirm(\n\t\t\t\t\tlt_name,\n\t\t\t\t\trpl::single(tr::bold(UniqueGiftName(*gift))),\n\t\t\t\t\tlt_price,\n\t\t\t\t\tstd::move(price),\n\t\t\t\t\tlt_user,\n\t\t\t\t\trpl::single(tr::bold(to->shortName())),\n\t\t\t\t\ttr::marked),\n\t\t\t.confirmed = std::move(callback),\n\t\t\t.confirmText = std::move(transfer),\n\t\t});\n\t}));\n}\n\nbool ShowResaleGiftLater(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tstd::shared_ptr<Data::UniqueGift> gift) {\n\tconst auto now = base::unixtime::now();\n\tif (gift->canResellAt <= now) {\n\t\treturn false;\n\t}\n\tconst auto seconds = gift->canResellAt - now;\n\tconst auto days = seconds / 86400;\n\tconst auto hours = seconds / 3600;\n\tconst auto minutes = std::max(seconds / 60, 1);\n\tshow->showToast({\n\t\t.title = tr::lng_gift_resale_transfer_early_title(tr::now),\n\t\t.text = { tr::lng_gift_resale_early(tr::now, lt_duration, days\n\t\t\t? tr::lng_days(tr::now, lt_count, days)\n\t\t\t: hours\n\t\t\t? tr::lng_hours(tr::now, lt_count, hours)\n\t\t\t: tr::lng_minutes(tr::now, lt_count, minutes)) },\n\t});\n\treturn true;\n}\n\nbool ShowTransferGiftLater(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tstd::shared_ptr<Data::UniqueGift> gift) {\n\tconst auto seconds = gift->canTransferAt - base::unixtime::now();\n\tif (seconds <= 0) {\n\t\treturn false;\n\t}\n\tconst auto days = seconds / 86400;\n\tconst auto hours = seconds / 3600;\n\tconst auto minutes = std::max(seconds / 60, 1);\n\tshow->showToast({\n\t\t.title = tr::lng_gift_resale_transfer_early_title(tr::now),\n\t\t.text = { tr::lng_gift_transfer_early(tr::now, lt_duration, days\n\t\t\t? tr::lng_days(tr::now, lt_count, days)\n\t\t\t: hours\n\t\t\t? tr::lng_hours(tr::now, lt_count, hours)\n\t\t\t: tr::lng_minutes(tr::now, lt_count, minutes)) },\n\t});\n\treturn true;\n}\n\nvoid ShowActionLocked(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst QString &slug) {\n\tconst auto open = [=] {\n\t\tUrlClickHandler::Open(u\"https://fragment.com/gift/\"_q\n\t\t\t+ slug\n\t\t\t+ u\"?collection=my\"_q);\n\t};\n\tshow->show(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_gift_transfer_locked_text(),\n\t\t.confirmed = [=](Fn<void()> close) { open(); close(); },\n\t\t.confirmText = tr::lng_gift_transfer_confirm_button(),\n\t\t.title = tr::lng_gift_transfer_locked_title(),\n\t}));\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/transfer_gift_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nstruct HistoryMessageSuggestion;\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nstruct UniqueGift;\nclass SavedStarGiftId;\n} // namespace Data\n\nnamespace Ui {\nclass ChatTheme;\n} // namespace Ui\n\nvoid ShowTransferToBox(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\tstd::shared_ptr<Data::UniqueGift> gift,\n\tData::SavedStarGiftId savedId,\n\tFn<void()> closeParentBox);\n\nvoid ShowTransferGiftBox(\n\tnot_null<Window::SessionController*> window,\n\tstd::shared_ptr<Data::UniqueGift> gift,\n\tData::SavedStarGiftId savedId);\n\nvoid ShowGiftSaleAcceptBox(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<HistoryItem*> item,\n\tnot_null<HistoryMessageSuggestion*> suggestion);\nvoid ShowGiftSaleRejectBox(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<HistoryItem*> item,\n\tnot_null<HistoryMessageSuggestion*> suggestion);\n\nvoid ShowBuyResaleGiftBox(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tstd::shared_ptr<Data::UniqueGift> gift,\n\tbool forceTon,\n\tnot_null<PeerData*> to,\n\tFn<void(bool ok)> closeParentBox);\n\nbool ShowResaleGiftLater(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tstd::shared_ptr<Data::UniqueGift> gift);\nbool ShowTransferGiftLater(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tstd::shared_ptr<Data::UniqueGift> gift);\n\nvoid SetThemeFromUniqueGift(\n\tnot_null<Window::SessionController*> window,\n\tstd::shared_ptr<Data::UniqueGift> unique);\nvoid SendPeerThemeChangeRequest(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\tconst QString &token,\n\tconst std::shared_ptr<Data::UniqueGift> &unique,\n\tbool locallySet = false);\nvoid SetPeerTheme(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\tconst QString &token,\n\tconst std::shared_ptr<Ui::ChatTheme> &theme);\n\nvoid ShowActionLocked(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst QString &slug);\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/translate_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/translate_box.h\"\n#include \"boxes/translate_box_content.h\"\n#include \"lang/translate_provider.h\"\n\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"lang/lang_instance.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"spellcheck/platform/platform_language.h\"\n#include \"ui/boxes/choose_language_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/multi_select.h\"\n#include \"ui/text/text_utilities.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kSkipAtLeastOneDuration = 3 * crl::time(1000);\n\n} // namespace\n\nvoid TranslateBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId msgId,\n\t\tTextWithEntities text,\n\t\tbool hasCopyRestriction) {\n\tstruct State {\n\t\tState(not_null<Main::Session*> session)\n\t\t: provider(CreateTranslateProvider(session)) {\n\t\t}\n\n\t\tstd::unique_ptr<TranslateProvider> provider;\n\t\trpl::variable<LanguageId> to;\n\t};\n\tconst auto state = box->lifetime().make_state<State>(&peer->session());\n\tstate->to = ChooseTranslateTo(peer->owner().history(peer));\n\tconst auto request = std::make_shared<TranslateProviderRequest>(\n\t\tPrepareTranslateProviderRequest(\n\t\t\tstate->provider.get(),\n\t\t\tpeer,\n\t\t\tmsgId,\n\t\t\tstd::move(text)));\n\n\tTranslateBoxContent(box, {\n\t\t.text = request->text,\n\t\t.hasCopyRestriction = hasCopyRestriction,\n\t\t.textContext = Core::TextContext({ .session = &peer->session() }),\n\t\t.to = state->to.value(),\n\t\t.chooseTo = [=] {\n\t\t\tbox->uiShow()->showBox(ChooseTranslateToBox(\n\t\t\t\tstate->to.current(),\n\t\t\t\tcrl::guard(box, [=](LanguageId id) { state->to = id; })));\n\t\t},\n\t\t.request = [=](\n\t\t\t\tLanguageId to,\n\t\t\t\tFn<void(TranslateBoxContentResult)> done) {\n\t\t\tstate->provider->request(\n\t\t\t\t*request,\n\t\t\t\tto,\n\t\t\t\t[done = std::move(done)](TranslateProviderResult result) {\n\t\t\t\t\tusing ProviderError = TranslateProviderError;\n\t\t\t\t\tusing UiError = TranslateBoxContentError;\n\t\t\t\t\tdone(TranslateBoxContentResult{\n\t\t\t\t\t\t.text = std::move(result.text),\n\t\t\t\t\t\t.error = (result.error\n\t\t\t\t\t\t\t\t== ProviderError::LocalLanguagePackMissing)\n\t\t\t\t\t\t\t? UiError::LocalLanguagePackMissing\n\t\t\t\t\t\t\t: (result.error == ProviderError::None)\n\t\t\t\t\t\t\t? UiError::None\n\t\t\t\t\t\t\t: UiError::Unknown,\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t},\n\t});\n}\n\nbool SkipTranslate(TextWithEntities textWithEntities) {\n\tconst auto &text = textWithEntities.text;\n\tif (text.isEmpty()) {\n\t\treturn true;\n\t}\n\tif (!Core::App().settings().translateButtonEnabled()) {\n\t\treturn true;\n\t}\n\tconstexpr auto kFirstChunk = size_t(100);\n\tauto hasLetters = (text.size() >= kFirstChunk);\n\tfor (auto i = 0; i < kFirstChunk; i++) {\n\t\tif (i >= text.size()) {\n\t\t\tbreak;\n\t\t}\n\t\tif (text.at(i).isLetter()) {\n\t\t\thasLetters = true;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (!hasLetters) {\n\t\treturn true;\n\t}\n#ifndef TDESKTOP_DISABLE_SPELLCHECK\n\tconst auto result = Platform::Language::Recognize(text);\n\tconst auto skip = Core::App().settings().skipTranslationLanguages();\n\treturn result.known() && ranges::contains(skip, result);\n#else\n\treturn false;\n#endif\n}\n\nobject_ptr<BoxContent> EditSkipTranslationLanguages() {\n\tauto title = tr::lng_translate_settings_choose();\n\tconst auto selected = std::make_shared<std::vector<LanguageId>>(\n\t\tCore::App().settings().skipTranslationLanguages());\n\tconst auto weak = std::make_shared<base::weak_qptr<BoxContent>>();\n\tconst auto check = [=](LanguageId id) {\n\t\tconst auto already = ranges::contains(*selected, id);\n\t\tif (already) {\n\t\t\tselected->erase(ranges::remove(*selected, id), selected->end());\n\t\t} else {\n\t\t\tselected->push_back(id);\n\t\t}\n\t\tif (already && selected->empty()) {\n\t\t\tif (const auto strong = weak->get()) {\n\t\t\t\tstrong->showToast(\n\t\t\t\t\ttr::lng_translate_settings_one(tr::now),\n\t\t\t\t\tkSkipAtLeastOneDuration);\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t};\n\tauto result = Box(ChooseLanguageBox, std::move(title), [=](\n\t\t\tstd::vector<LanguageId> &&list) {\n\t\tCore::App().settings().setSkipTranslationLanguages(\n\t\t\tstd::move(list));\n\t\tCore::App().saveSettingsDelayed();\n\t}, *selected, true, check);\n\t*weak = result.data();\n\treturn result;\n}\n\nobject_ptr<BoxContent> ChooseTranslateToBox(\n\t\tLanguageId bringUp,\n\t\tFn<void(LanguageId)> callback) {\n\tauto &settings = Core::App().settings();\n\tauto selected = std::vector<LanguageId>{\n\t\tsettings.translateTo(),\n\t};\n\tfor (const auto &id : settings.skipTranslationLanguages()) {\n\t\tif (id != selected.front()) {\n\t\t\tselected.push_back(id);\n\t\t}\n\t}\n\tif (bringUp && ranges::contains(selected, bringUp)) {\n\t\tselected.push_back(bringUp);\n\t}\n\treturn Box(ChooseLanguageBox, tr::lng_languages(), [=](\n\t\t\tconst std::vector<LanguageId> &ids) {\n\t\tExpects(!ids.empty());\n\n\t\tconst auto id = ids.front();\n\t\tCore::App().settings().setTranslateTo(id);\n\t\tCore::App().saveSettingsDelayed();\n\t\tcallback(id);\n\t}, selected, false, nullptr);\n}\n\nLanguageId ChooseTranslateTo(not_null<History*> history) {\n\treturn ChooseTranslateTo(history->translateOfferedFrom());\n}\n\nLanguageId ChooseTranslateTo(LanguageId offeredFrom) {\n\tauto &settings = Core::App().settings();\n\treturn ChooseTranslateTo(\n\t\tofferedFrom,\n\t\tsettings.translateTo(),\n\t\tsettings.skipTranslationLanguages());\n}\n\nLanguageId ChooseTranslateTo(\n\t\tnot_null<History*> history,\n\t\tLanguageId savedTo,\n\t\tconst std::vector<LanguageId> &skip) {\n\treturn ChooseTranslateTo(history->translateOfferedFrom(), savedTo, skip);\n}\n\nLanguageId ChooseTranslateTo(\n\t\tLanguageId offeredFrom,\n\t\tLanguageId savedTo,\n\t\tconst std::vector<LanguageId> &skip) {\n\treturn (offeredFrom != savedTo) ? savedTo : skip.front();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/translate_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nclass History;\nclass PeerData;\nstruct LanguageId;\n\nnamespace Ui {\n\nclass BoxContent;\nclass GenericBox;\n\nvoid TranslateBox(\n\tnot_null<GenericBox*> box,\n\tnot_null<PeerData*> peer,\n\tMsgId msgId,\n\tTextWithEntities text,\n\tbool hasCopyRestriction);\n\n[[nodiscard]] bool SkipTranslate(TextWithEntities textWithEntities);\n\n[[nodiscard]] object_ptr<BoxContent> EditSkipTranslationLanguages();\n[[nodiscard]] object_ptr<BoxContent> ChooseTranslateToBox(\n\tLanguageId bringUp,\n\tFn<void(LanguageId)> callback);\n\n[[nodiscard]] LanguageId ChooseTranslateTo(not_null<History*> history);\n[[nodiscard]] LanguageId ChooseTranslateTo(LanguageId offeredFrom);\n[[nodiscard]] LanguageId ChooseTranslateTo(\n\tnot_null<History*> history,\n\tLanguageId savedTo,\n\tconst std::vector<LanguageId> &skip);\n[[nodiscard]] LanguageId ChooseTranslateTo(\n\tLanguageId offeredFrom,\n\tLanguageId savedTo,\n\tconst std::vector<LanguageId> &skip);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/translate_box_content.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/translate_box_content.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/boxes/choose_language_box.h\"\n#include \"ui/effects/loading_element.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Ui {\nnamespace {\n\nclass ShowButton final : public RpWidget {\npublic:\n\tShowButton(not_null<Ui::RpWidget*> parent);\n\n\t[[nodiscard]] rpl::producer<Qt::MouseButton> clicks() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tLinkButton _button;\n\n};\n\nShowButton::ShowButton(not_null<Ui::RpWidget*> parent)\n: RpWidget(parent)\n, _button(this, tr::lng_usernames_activate_confirm(tr::now)) {\n\t_button.sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tresize(\n\t\t\ts.width() + st::defaultEmojiSuggestions.fadeRight.width(),\n\t\t\ts.height());\n\t\t_button.moveToRight(0, 0);\n\t}, lifetime());\n\t_button.show();\n}\n\nvoid ShowButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tconst auto clip = e->rect();\n\n\tconst auto &icon = st::defaultEmojiSuggestions.fadeRight;\n\tconst auto fade = QRect(0, 0, icon.width(), height());\n\tif (fade.intersects(clip)) {\n\t\ticon.fill(p, fade);\n\t}\n\tconst auto fill = clip.intersected(\n\t\t{ icon.width(), 0, width() - icon.width(), height() });\n\tif (!fill.isEmpty()) {\n\t\tp.fillRect(fill, st::boxBg);\n\t}\n}\n\nrpl::producer<Qt::MouseButton> ShowButton::clicks() const {\n\treturn _button.clicks();\n}\n\n} // namespace\n\nvoid TranslateBoxContent(\n\t\tnot_null<GenericBox*> box,\n\t\tTranslateBoxContentArgs &&args) {\n\tbox->setWidth(st::boxWideWidth);\n\tbox->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });\n\tconst auto container = box->verticalLayout();\n\n\tconst auto text = std::move(args.text);\n\tconst auto hasCopyRestriction = args.hasCopyRestriction;\n\tconst auto textContext = std::move(args.textContext);\n\tconst auto chooseTo = std::make_shared<Fn<void()>>(\n\t\tstd::move(args.chooseTo));\n\tconst auto request = std::make_shared<\n\t\tFn<void(LanguageId, Fn<void(TranslateBoxContentResult)>)>>(\n\t\t\tstd::move(args.request));\n\n\tauto to = std::move(args.to) | rpl::start_spawning(box->lifetime());\n\tconst auto toTitle = rpl::duplicate(to) | rpl::map(LanguageName);\n\tconst auto toDirection = rpl::duplicate(to) | rpl::map([=](\n\t\t\tLanguageId id) {\n\t\treturn id.locale().textDirection() == Qt::RightToLeft;\n\t});\n\n\tconst auto &stLabel = st::aboutLabel;\n\tconst auto lineHeight = stLabel.style.lineHeight;\n\n\tUi::AddSkip(container);\n\n\tconst auto animationsPaused = [] {\n\t\tusing Which = FlatLabel::WhichAnimationsPaused;\n\t\tconst auto emoji = On(PowerSaving::kEmojiChat);\n\t\tconst auto spoiler = On(PowerSaving::kChatSpoiler);\n\t\treturn emoji\n\t\t\t? (spoiler ? Which::All : Which::CustomEmoji)\n\t\t\t: (spoiler ? Which::Spoiler : Which::None);\n\t};\n\tconst auto original = box->addRow(object_ptr<SlideWrap<FlatLabel>>(\n\t\tbox,\n\t\tobject_ptr<FlatLabel>(box, stLabel)));\n\t{\n\t\tif (hasCopyRestriction) {\n\t\t\toriginal->entity()->setContextMenuHook([](auto&&) {\n\t\t\t});\n\t\t}\n\t\toriginal->entity()->setAnimationsPausedCallback(animationsPaused);\n\t\toriginal->entity()->setMarkedText(text, textContext);\n\t\toriginal->setMinimalHeight(lineHeight);\n\t\toriginal->hide(anim::type::instant);\n\n\t\tconst auto show = Ui::CreateChild<FadeWrap<ShowButton>>(\n\t\t\tcontainer.get(),\n\t\t\tobject_ptr<ShowButton>(container));\n\t\tshow->hide(anim::type::instant);\n\t\trpl::combine(\n\t\t\tcontainer->widthValue(),\n\t\t\toriginal->geometryValue()\n\t\t) | rpl::on_next([=](int width, const QRect &rect) {\n\t\t\tshow->moveToLeft(\n\t\t\t\twidth - show->width() - st::boxRowPadding.right(),\n\t\t\t\trect.y() + std::abs(lineHeight - show->height()) / 2);\n\t\t}, show->lifetime());\n\t\tcontainer->widthValue(\n\t\t) | rpl::filter([](int width) {\n\t\t\treturn width > 0;\n\t\t}) | rpl::take(1) | rpl::on_next([=](int width) {\n\t\t\tif (original->entity()->textMaxWidth()\n\t\t\t\t> (width - rect::m::sum::h(st::boxRowPadding))) {\n\t\t\t\tshow->show(anim::type::instant);\n\t\t\t}\n\t\t}, show->lifetime());\n\t\tshow->toggleOn(show->entity()->clicks() | rpl::map_to(false));\n\t\toriginal->toggleOn(show->entity()->clicks() | rpl::map_to(true));\n\t}\n\tUi::AddSkip(container);\n\tUi::AddSkip(container);\n\tUi::AddDivider(container);\n\tUi::AddSkip(container);\n\n\t{\n\t\tconst auto padding = st::defaultSubsectionTitlePadding;\n\t\tconst auto subtitle = Ui::AddSubsectionTitle(container, std::move(toTitle));\n\n\t\trpl::duplicate(to) | rpl::on_next([=] {\n\t\t\tsubtitle->resizeToWidth(container->width()\n\t\t\t\t- padding.left()\n\t\t\t\t- padding.right());\n\t\t}, subtitle->lifetime());\n\t}\n\n\tconst auto translated = box->addRow(object_ptr<SlideWrap<FlatLabel>>(\n\t\tbox,\n\t\tobject_ptr<FlatLabel>(box, stLabel)));\n\ttranslated->entity()->setSelectable(!hasCopyRestriction);\n\ttranslated->entity()->setAnimationsPausedCallback(animationsPaused);\n\n\tconstexpr auto kMaxLines = 3;\n\tcontainer->resizeToWidth(box->width());\n\tconst auto loading = box->addRow(object_ptr<SlideWrap<RpWidget>>(\n\t\tbox,\n\t\tCreateLoadingTextWidget(\n\t\t\tbox,\n\t\t\tst::aboutLabel.style,\n\t\t\tstd::min(original->entity()->height() / lineHeight, kMaxLines),\n\t\t\tstd::move(toDirection))));\n\n\tstruct State {\n\t\tint requestId = 0;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\n\tconst auto showText = [=](TranslateBoxContentResult result) {\n\t\tusing UiError = TranslateBoxContentError;\n\t\tauto value = result.text.value_or(\n\t\t\ttr::italic(((result.error == UiError::LocalLanguagePackMissing)\n\t\t\t\t? tr::lng_translate_box_error_language_pack_not_installed\n\t\t\t\t: tr::lng_translate_box_error)(tr::now)));\n\t\ttranslated->entity()->setMarkedText(value, textContext);\n\t\ttranslated->show(anim::type::instant);\n\t\tloading->hide(anim::type::instant);\n\t};\n\tconst auto send = [=](LanguageId id) {\n\t\tconst auto requestId = ++state->requestId;\n\t\tloading->show(anim::type::instant);\n\t\ttranslated->hide(anim::type::instant);\n\t\t(*request)(id, [=](TranslateBoxContentResult result) {\n\t\t\tif (state->requestId != requestId) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tshowText(std::move(result));\n\t\t});\n\t};\n\tstd::move(to) | rpl::on_next(send, box->lifetime());\n\n\tbox->addLeftButton(tr::lng_settings_language(), [=] {\n\t\tif (loading->toggled()) {\n\t\t\treturn;\n\t\t}\n\t\t(*chooseTo)();\n\t});\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/translate_box_content.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"spellcheck/platform/platform_language.h\"\n\nnamespace Ui::Text {\nstruct MarkedContext;\n} // namespace Ui::Text\n\nnamespace Ui {\n\nenum class TranslateBoxContentError {\n\tNone = 0,\n\tUnknown,\n\tLocalLanguagePackMissing,\n};\n\nstruct TranslateBoxContentResult {\n\tstd::optional<TextWithEntities> text;\n\tTranslateBoxContentError error = TranslateBoxContentError::None;\n};\n\nclass GenericBox;\n\nstruct TranslateBoxContentArgs {\n\tTextWithEntities text;\n\tbool hasCopyRestriction = false;\n\tText::MarkedContext textContext;\n\trpl::producer<LanguageId> to;\n\tFn<void()> chooseTo;\n\tFn<void(LanguageId, Fn<void(TranslateBoxContentResult)>)> request;\n};\n\nvoid TranslateBoxContent(\n\tnot_null<GenericBox*> box,\n\tTranslateBoxContentArgs &&args);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/url_auth_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/url_auth_box.h\"\n\n#include \"apiwrap.h\"\n#include \"boxes/url_auth_box_content.h\"\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_user.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_window.h\"\n\nnamespace UrlAuthBox {\nnamespace {\n\nusing AnotherSessionFactory = Fn<not_null<Main::Session*>()>;\nusing OnUserChangedCallback = Fn<void(Fn<void()>)>;\n\nstruct SwitchAccountResult {\n\tUi::RpWidget *widget = nullptr;\n\tAnotherSessionFactory anotherSession;\n\tOnUserChangedCallback setOnUserChanged;\n\tFn<void(UserId)> updateUserIdHint;\n};\n\n[[nodiscard]] std::shared_ptr<Ui::DynamicImage> MakeMatchCodeImage(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &code) {\n\tauto emojiLength = 0;\n\tconst auto emoji = Ui::Emoji::Find(code, &emojiLength);\n\tif (!emoji || emojiLength != code.size()) {\n\t\treturn nullptr;\n\t}\n\tconst auto makeFor = [&](not_null<Main::Session*> source) {\n\t\tif (const auto sticker = source->emojiStickersPack()\n\t\t\t\t.stickerForEmoji(emoji)) {\n\t\t\treturn Ui::MakeEmojiThumbnail(\n\t\t\t\t&source->data(),\n\t\t\t\tData::SerializeCustomEmojiId(sticker.document),\n\t\t\t\tnullptr,\n\t\t\t\tnullptr,\n\t\t\t\t1);\n\t\t}\n\t\treturn std::shared_ptr<Ui::DynamicImage>();\n\t};\n\tif (session->isTestMode()) {\n\t\tfor (const auto &account : Core::App().domain().orderedAccounts()) {\n\t\t\tif (!account->sessionExists()\n\t\t\t\t|| account->session().isTestMode()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (const auto image = makeFor(&account->session())) {\n\t\t\t\treturn image;\n\t\t\t}\n\t\t}\n\t}\n\treturn makeFor(session);\n}\n\n[[nodiscard]] SwitchAccountResult AddAccountsMenu(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tUserId userIdHint = UserId()) {\n\tconst auto session = &Core::App().domain().active().session();\n\tconst auto widget = Ui::CreateChild<SwitchableUserpicButton>(\n\t\tparent,\n\t\tst::restoreUserpicIcon.photoSize + st::lineWidth * 8);\n\tstruct State {\n\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t\tUserData *currentUser = nullptr;\n\t\tFn<void()> onUserChanged;\n\t};\n\tconst auto state = widget->lifetime().make_state<State>();\n\n\tconst auto isCurrentTest = session->isTestMode();\n\tconst auto findHintedUser = [&]() -> UserData* {\n\t\tif (!userIdHint) {\n\t\t\treturn session->user().get();\n\t\t}\n\t\tfor (const auto &account : Core::App().domain().orderedAccounts()) {\n\t\t\tif (!account->sessionExists()\n\t\t\t\t|| (account->session().isTestMode() != isCurrentTest)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (account->session().userId() == userIdHint) {\n\t\t\t\treturn account->session().user();\n\t\t\t}\n\t\t}\n\t\treturn session->user().get();\n\t};\n\n\tstate->currentUser = findHintedUser();\n\tconst auto userpic = Ui::CreateChild<Ui::UserpicButton>(\n\t\tparent,\n\t\tstate->currentUser,\n\t\tst::restoreUserpicIcon);\n\twidget->setUserpic(userpic);\n\tconst auto filtered = [=] {\n\t\tauto result = std::vector<not_null<Main::Session*>>();\n\t\tfor (const auto &account : Core::App().domain().orderedAccounts()) {\n\t\t\tif (!account->sessionExists()\n\t\t\t\t|| (account->session().user() == state->currentUser)\n\t\t\t\t|| (account->session().isTestMode() != isCurrentTest)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tresult.push_back(&account->session());\n\t\t}\n\t\treturn result;\n\t};\n\tconst auto isSingle = filtered().empty();\n\twidget->setExpanded(!isSingle);\n\twidget->setAttribute(Qt::WA_TransparentForMouseEvents, isSingle);\n\twidget->setClickedCallback([=] {\n\t\tconst auto &st = st::popupMenuWithIcons;\n\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(widget, st);\n\t\tfor (const auto &anotherSession : filtered()) {\n\t\t\tconst auto user = anotherSession->user();\n\t\t\tconst auto action = new QAction(user->name(), state->menu);\n\t\t\tQObject::connect(action, &QAction::triggered, [=] {\n\t\t\t\tstate->currentUser = user;\n\t\t\t\tconst auto newUserpic = Ui::CreateChild<Ui::UserpicButton>(\n\t\t\t\t\tparent,\n\t\t\t\t\tuser,\n\t\t\t\t\tst::restoreUserpicIcon);\n\t\t\t\twidget->setUserpic(newUserpic);\n\t\t\t\tif (state->onUserChanged) {\n\t\t\t\t\tstate->onUserChanged();\n\t\t\t\t}\n\t\t\t});\n\t\t\tauto owned = base::make_unique_q<Ui::Menu::Action>(\n\t\t\t\tstate->menu->menu(),\n\t\t\t\tstate->menu->menu()->st(),\n\t\t\t\taction,\n\t\t\t\tnullptr,\n\t\t\t\tnullptr);\n\t\t\tconst auto menuUserpic = Ui::CreateChild<Ui::UserpicButton>(\n\t\t\t\towned.get(),\n\t\t\t\tuser,\n\t\t\t\tst::lockSetupEmailUserpicSmall);\n\t\t\tmenuUserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\tmenuUserpic->move(st.menu.itemIconPosition);\n\t\t\tstate->menu->addAction(std::move(owned));\n\t\t}\n\n\t\tstate->menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);\n\t\tstate->menu->popup(\n\t\t\twidget->mapToGlobal(\n\t\t\t\tQPoint(\n\t\t\t\t\twidget->width()\n\t\t\t\t\t\t+ Ui::BoxShadow::ExtendFor(st.shadow).right(),\n\t\t\t\t\twidget->height())));\n\t});\n\treturn {\n\t\twidget,\n\t\t[=] { return &state->currentUser->session(); },\n\t\t[=](Fn<void()> callback) { state->onUserChanged = callback; },\n\t\t[=](UserId newUserIdHint) {\n\t\t\tconst auto isCurrentTest = session->isTestMode();\n\t\t\tfor (const auto &acc : Core::App().domain().orderedAccounts()) {\n\t\t\t\tif (!acc->sessionExists()\n\t\t\t\t\t|| (acc->session().isTestMode() != isCurrentTest)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (acc->session().userId() == newUserIdHint) {\n\t\t\t\t\tstate->currentUser = acc->session().user();\n\t\t\t\t\tconst auto next = Ui::CreateChild<Ui::UserpicButton>(\n\t\t\t\t\t\tparent,\n\t\t\t\t\t\tstate->currentUser,\n\t\t\t\t\t\tst::restoreUserpicIcon);\n\t\t\t\t\twidget->setUserpic(next);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t};\n}\n\n} // namespace\n\nvoid RequestButton(\n\tstd::shared_ptr<Ui::Show> show,\n\tconst MTPDurlAuthResultRequest &request,\n\tnot_null<const HistoryItem*> message,\n\tint row,\n\tint column);\nvoid RequestUrl(\n\tstd::shared_ptr<Ui::Show> show,\n\tconst MTPDurlAuthResultRequest &request,\n\tnot_null<Main::Session*> session,\n\tconst QString &url,\n\tQVariant context);\n\nvoid ActivateButton(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<const HistoryItem*> message,\n\t\tint row,\n\t\tint column) {\n\tconst auto itemId = message->fullId();\n\tconst auto button = HistoryMessageMarkupButton::Get(\n\t\t&message->history()->owner(),\n\t\titemId,\n\t\trow,\n\t\tcolumn);\n\tif (button->requestId || !message->isRegular()) {\n\t\treturn;\n\t}\n\tconst auto session = &message->history()->session();\n\tconst auto inputPeer = message->history()->peer->input();\n\tconst auto buttonId = button->buttonId;\n\tconst auto url = QString::fromUtf8(button->data);\n\n\tusing Flag = MTPmessages_RequestUrlAuth::Flag;\n\tbutton->requestId = session->api().request(MTPmessages_RequestUrlAuth(\n\t\tMTP_flags(Flag::f_peer | Flag::f_msg_id | Flag::f_button_id),\n\t\tinputPeer,\n\t\tMTP_int(itemId.msg),\n\t\tMTP_int(buttonId),\n\t\tMTPstring(), // #TODO auth url\n\t\tMTPstring() // in_app_origin\n\t)).done([=](const MTPUrlAuthResult &result) {\n\t\tconst auto button = HistoryMessageMarkupButton::Get(\n\t\t\t&session->data(),\n\t\t\titemId,\n\t\t\trow,\n\t\t\tcolumn);\n\t\tif (!button) {\n\t\t\treturn;\n\t\t}\n\n\t\tbutton->requestId = 0;\n\t\tresult.match([&](const MTPDurlAuthResultAccepted &data) {\n\t\t\tif (const auto url = data.vurl()) {\n\t\t\t\tUrlClickHandler::Open(qs(url->v));\n\t\t\t}\n\t\t}, [&](const MTPDurlAuthResultDefault &data) {\n\t\t\tHiddenUrlClickHandler::Open(url);\n\t\t}, [&](const MTPDurlAuthResultRequest &data) {\n\t\t\tif (const auto item = session->data().message(itemId)) {\n\t\t\t\tRequestButton(show, data, item, row, column);\n\t\t\t}\n\t\t});\n\t}).fail([=] {\n\t\tconst auto button = HistoryMessageMarkupButton::Get(\n\t\t\t&session->data(),\n\t\t\titemId,\n\t\t\trow,\n\t\t\tcolumn);\n\t\tif (!button) {\n\t\t\treturn;\n\t\t}\n\n\t\tbutton->requestId = 0;\n\t\tHiddenUrlClickHandler::Open(url);\n\t}).send();\n}\n\nvoid ActivateUrl(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &url,\n\t\tQVariant context) {\n\tcontext = QVariant::fromValue([&] {\n\t\tauto result = context.value<ClickHandlerContext>();\n\t\tresult.skipBotAutoLogin = true;\n\t\treturn result;\n\t}());\n\n\tusing Flag = MTPmessages_RequestUrlAuth::Flag;\n\tsession->api().request(MTPmessages_RequestUrlAuth(\n\t\tMTP_flags(Flag::f_url),\n\t\tMTPInputPeer(),\n\t\tMTPint(), // msg_id\n\t\tMTPint(), // button_id\n\t\tMTP_string(url),\n\t\tMTPstring() // in_app_origin\n\t)).done([=](const MTPUrlAuthResult &result) {\n\t\tresult.match([&](const MTPDurlAuthResultAccepted &data) {\n\t\t\tUrlClickHandler::Open(qs(data.vurl().value_or_empty()), context);\n\t\t}, [&](const MTPDurlAuthResultDefault &data) {\n\t\t\tHiddenUrlClickHandler::Open(url, context);\n\t\t}, [&](const MTPDurlAuthResultRequest &data) {\n\t\t\tRequestUrl(show, data, session, url, context);\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (error.type() == u\"URL_EXPIRED\"_q) {\n\t\t\tshow->showToast(\n\t\t\t\ttr::lng_url_auth_phone_toast_bad_expired(tr::now));\n\t\t} else {\n\t\t\tHiddenUrlClickHandler::Open(url, context);\n\t\t}\n\t}).send();\n}\n\nvoid RequestButton(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tconst MTPDurlAuthResultRequest &request,\n\t\tnot_null<const HistoryItem*> message,\n\t\tint row,\n\t\tint column) {\n\tconst auto itemId = message->fullId();\n\tconst auto button = HistoryMessageMarkupButton::Get(\n\t\t&message->history()->owner(),\n\t\titemId,\n\t\trow,\n\t\tcolumn);\n\tif (!button || button->requestId || !message->isRegular()) {\n\t\treturn;\n\t}\n\tconst auto session = &message->history()->session();\n\tconst auto inputPeer = message->history()->peer->input();\n\tconst auto buttonId = button->buttonId;\n\tconst auto url = QString::fromUtf8(button->data);\n\n\tconst auto bot = request.is_request_write_access()\n\t\t? session->data().processUser(request.vbot()).get()\n\t\t: nullptr;\n\tconst auto box = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tconst auto finishWithUrl = [=](const QString &url, bool accepted) {\n\t\tif (*box) {\n\t\t\t(*box)->closeBox();\n\t\t}\n\t\tif (url.isEmpty() && accepted) {\n\t\t\tshow->showToast(tr::lng_passport_success(tr::now));\n\t\t} else {\n\t\t\tUrlClickHandler::Open(url);\n\t\t}\n\t};\n\tconst auto callback = [=](Result result) {\n\t\tif (!result.auth) {\n\t\t\tsession->api().request(MTPmessages_DeclineUrlAuth(\n\t\t\t\tMTP_string(url)\n\t\t\t)).send();\n\t\t\tfinishWithUrl(url, false);\n\t\t} else if (session->data().message(itemId)) {\n\t\t\tusing Flag = MTPmessages_AcceptUrlAuth::Flag;\n\t\t\tconst auto flags = Flag(0)\n\t\t\t\t| (result.allowWrite ? Flag::f_write_allowed : Flag(0))\n\t\t\t\t| (result.sharePhone ? Flag::f_share_phone_number : Flag(0))\n\t\t\t\t| (result.matchCode.isEmpty() ? Flag(0) : Flag::f_match_code)\n\t\t\t\t| (Flag::f_peer | Flag::f_msg_id | Flag::f_button_id);\n\t\t\tsession->api().request(MTPmessages_AcceptUrlAuth(\n\t\t\t\tMTP_flags(flags),\n\t\t\t\tinputPeer,\n\t\t\t\tMTP_int(itemId.msg),\n\t\t\t\tMTP_int(buttonId),\n\t\t\t\tMTPstring(),\n\t\t\t\tresult.matchCode.isEmpty()\n\t\t\t\t\t? MTPstring()\n\t\t\t\t\t: MTP_string(result.matchCode)\n\t\t\t)).done([=](const MTPUrlAuthResult &result) {\n\t\t\t\tconst auto accepted = result.match(\n\t\t\t\t[](const MTPDurlAuthResultAccepted &data) {\n\t\t\t\t\treturn true;\n\t\t\t\t}, [](const auto &) {\n\t\t\t\t\treturn false;\n\t\t\t\t});\n\t\t\t\tconst auto to = result.match(\n\t\t\t\t[&](const MTPDurlAuthResultAccepted &data) {\n\t\t\t\t\treturn qs(data.vurl().value_or_empty());\n\t\t\t\t}, [&](const MTPDurlAuthResultDefault &data) {\n\t\t\t\t\treturn url;\n\t\t\t\t}, [&](const MTPDurlAuthResultRequest &data) {\n\t\t\t\t\tLOG((\"API Error: \"\n\t\t\t\t\t\t\"got urlAuthResultRequest after acceptUrlAuth.\"));\n\t\t\t\t\treturn url;\n\t\t\t\t});\n\t\t\t\tfinishWithUrl(to, accepted);\n\t\t\t}).fail([=] {\n\t\t\t\tfinishWithUrl(url, false);\n\t\t\t}).send();\n\t\t}\n\t};\n\tconst auto displayName = request.is_is_app()\n\t\t? (request.vverified_app_name()\n\t\t\t? qs(*request.vverified_app_name())\n\t\t\t: tr::lng_url_auth_unverified_app(tr::now))\n\t\t: qs(request.vdomain());\n\t*box = show->show(\n\t\tBox(\n\t\t\tShow,\n\t\t\turl,\n\t\t\tdisplayName,\n\t\t\tsession->user()->name(),\n\t\t\tbot ? bot->firstName : QString(),\n\t\t\tcallback),\n\t\tUi::LayerOption::KeepOther);\n}\n\nvoid RequestUrl(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tconst MTPDurlAuthResultRequest &request,\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &url,\n\t\tQVariant context) {\n\tstruct State {\n\t\tbase::weak_qptr<Ui::BoxContent> box;\n\t\tAnotherSessionFactory anotherSession = nullptr;\n\t\tQString firstMatchCode;\n\t\trpl::lifetime boxDeclineLifetime;\n\t\trpl::lifetime matchCodesBoxDeclineLifetime;\n\t};\n\tconst auto bot = request.is_request_write_access()\n\t\t? session->data().processUser(request.vbot()).get()\n\t\t: nullptr;\n\tconst auto requestPhone = request.is_request_phone_number();\n\tconst auto matchCodesFirst = request.is_match_codes_first();\n\tconst auto isApp = request.is_is_app();\n\tconst auto domain = isApp\n\t\t? (request.vverified_app_name()\n\t\t\t? qs(*request.vverified_app_name())\n\t\t\t: tr::lng_url_auth_unverified_app(tr::now))\n\t\t: qs(request.vdomain());\n\tconst auto userIdHint = request.vuser_id_hint()\n\t\t? peerToUser(peerFromUser(*request.vuser_id_hint()))\n\t\t: UserId();\n\tconst auto matchCodes = [&] {\n\t\tauto result = QStringList();\n\t\tif (const auto codes = request.vmatch_codes()) {\n\t\t\tfor (const auto &code : codes->v) {\n\t\t\t\tresult.push_back(qs(code));\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}();\n\tconst auto state = std::make_shared<State>();\n\tconst auto finishWithUrl = [=](const QString &to, bool accepted) {\n\t\tif (state->box) {\n\t\t\tstate->box->closeBox();\n\t\t}\n\n\t\tif ((to.isEmpty() && accepted) || (to == url)) {\n\t\t} else {\n\t\t\tUrlClickHandler::Open(to, context);\n\t\t}\n\t};\n\tconst auto resolveSession = [=] {\n\t\treturn state->anotherSession ? state->anotherSession() : session;\n\t};\n\tconst auto requestDecline = [=] {\n\t\tresolveSession()->api().request(MTPmessages_DeclineUrlAuth(\n\t\t\tMTP_string(url)\n\t\t)).send();\n\t};\n\tconst auto sendRequest = [=](Result result) {\n\t\tif (!result.auth) {\n\t\t\trequestDecline();\n\t\t\tfinishWithUrl(url, false);\n\t\t} else {\n\t\t\tconst auto sharePhone = result.sharePhone;\n\t\t\tusing Flag = MTPmessages_AcceptUrlAuth::Flag;\n\t\t\tconst auto flags = Flag::f_url\n\t\t\t\t| (result.allowWrite ? Flag::f_write_allowed : Flag(0))\n\t\t\t\t| (sharePhone ? Flag::f_share_phone_number : Flag(0))\n\t\t\t\t| (result.matchCode.isEmpty() ? Flag(0) : Flag::f_match_code);\n\t\t\tresolveSession()->api().request(MTPmessages_AcceptUrlAuth(\n\t\t\t\tMTP_flags(flags),\n\t\t\t\tMTPInputPeer(),\n\t\t\t\tMTPint(), // msg_id\n\t\t\t\tMTPint(), // button_id\n\t\t\t\tMTP_string(url),\n\t\t\t\tresult.matchCode.isEmpty()\n\t\t\t\t\t? MTPstring()\n\t\t\t\t\t: MTP_string(result.matchCode)\n\t\t\t)).done([=](const MTPUrlAuthResult &result) {\n\t\t\t\tconst auto accepted = result.match(\n\t\t\t\t[](const MTPDurlAuthResultAccepted &data) {\n\t\t\t\t\treturn true;\n\t\t\t\t}, [](const auto &) {\n\t\t\t\t\treturn false;\n\t\t\t\t});\n\t\t\t\tconst auto to = result.match(\n\t\t\t\t[&](const MTPDurlAuthResultAccepted &data) {\n\t\t\t\t\treturn qs(data.vurl().value_or_empty());\n\t\t\t\t}, [&](const MTPDurlAuthResultDefault &data) {\n\t\t\t\t\treturn url;\n\t\t\t\t}, [&](const MTPDurlAuthResultRequest &data) {\n\t\t\t\t\tLOG((\"API Error: \"\n\t\t\t\t\t\t\"got urlAuthResultRequest after acceptUrlAuth.\"));\n\t\t\t\t\treturn url;\n\t\t\t\t});\n\t\t\t\tfinishWithUrl(to, accepted);\n\t\t\t\tconst auto domainWrapped = isApp\n\t\t\t\t\t? tr::bold(domain)\n\t\t\t\t\t: tr::link(domain);\n\t\t\t\tshow->showToast(Ui::Toast::Config{\n\t\t\t\t\t.title = tr::lng_url_auth_phone_toast_good_title(tr::now),\n\t\t\t\t\t.text = ((requestPhone && !sharePhone)\n\t\t\t\t\t\t? tr::lng_url_auth_phone_toast_good_no_phone\n\t\t\t\t\t\t: tr::lng_url_auth_phone_toast_good)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_domain,\n\t\t\t\t\t\t\tdomainWrapped,\n\t\t\t\t\t\t\ttr::marked),\n\t\t\t\t\t.duration = crl::time(4000),\n\t\t\t\t});\n\t\t\t}).fail([=] {\n\t\t\t\tconst auto domainWrapped = isApp\n\t\t\t\t\t? tr::bold(domain)\n\t\t\t\t\t: tr::link(domain);\n\t\t\t\tshow->showToast(Ui::Toast::Config{\n\t\t\t\t\t.title = tr::lng_url_auth_phone_toast_bad_title(tr::now),\n\t\t\t\t\t.text = tr::lng_url_auth_phone_toast_bad(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_domain,\n\t\t\t\t\t\tdomainWrapped,\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\t.duration = crl::time(4000),\n\t\t\t\t});\n\t\t\t\tfinishWithUrl(url, false);\n\t\t\t}).send();\n\t\t}\n\t};\n\tconst auto browser = qs(request.vbrowser().value_or(\"Unknown browser\"));\n\tconst auto device = qs(request.vplatform().value_or(\"Unknown platform\"));\n\tconst auto ip = qs(request.vip().value_or(\"Unknown IP\"));\n\tconst auto region = qs(request.vregion().value_or(\"Unknown region\"));\n\tconst auto showAuthBox = [=] {\n\t\tstate->box = show->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tconst auto accountResult = box->lifetime().make_state<\n\t\t\t\tSwitchAccountResult>(nullptr);\n\t\t\tconst auto matchCodesShared = box->lifetime().make_state<\n\t\t\t\trpl::variable<QStringList>>(matchCodes);\n\t\t\tconst auto reloadRequest = [=] {\n\t\t\t\tusing Flag = MTPmessages_RequestUrlAuth::Flag;\n\t\t\t\tconst auto currentSession = resolveSession();\n\t\t\t\tcurrentSession->api().request(MTPmessages_RequestUrlAuth(\n\t\t\t\t\tMTP_flags(Flag::f_url),\n\t\t\t\t\tMTPInputPeer(),\n\t\t\t\t\tMTPint(), // msg_id\n\t\t\t\t\tMTPint(), // button_id\n\t\t\t\t\tMTP_string(url),\n\t\t\t\t\tMTPstring() // in_app_origin\n\t\t\t\t)).done(crl::guard(box, [=](const MTPUrlAuthResult &result) {\n\t\t\t\t\tresult.match([&](const MTPDurlAuthResultRequest &data) {\n\t\t\t\t\t\tconst auto newUserId = data.vuser_id_hint()\n\t\t\t\t\t\t\t? peerToUser(peerFromUser(*data.vuser_id_hint()))\n\t\t\t\t\t\t\t: UserId();\n\t\t\t\t\t\taccountResult->updateUserIdHint(newUserId);\n\t\t\t\t\t\tauto newCodes = QStringList();\n\t\t\t\t\t\tif (const auto codes = data.vmatch_codes()) {\n\t\t\t\t\t\t\tfor (const auto &code : codes->v) {\n\t\t\t\t\t\t\t\tnewCodes.push_back(qs(code));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t*matchCodesShared = newCodes;\n\t\t\t\t\t}, [](const auto &) {});\n\t\t\t\t})).send();\n\t\t\t};\n\t\t\tconst auto callback = [=](Result result) {\n\t\t\t\tstate->boxDeclineLifetime.destroy();\n\t\t\t\tif (result.matchCode.isEmpty()\n\t\t\t\t\t&& !state->firstMatchCode.isEmpty()) {\n\t\t\t\t\tresult.matchCode = state->firstMatchCode;\n\t\t\t\t}\n\t\t\t\tif (!requestPhone) {\n\t\t\t\t\treturn sendRequest(result);\n\t\t\t\t}\n\t\t\t\tbox->uiShow()->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\t\tbox->setTitle(tr::lng_url_auth_phone_sure_title());\n\t\t\t\t\tconst auto confirm = [=](bool confirmed) {\n\t\t\t\t\t\treturn [=](Fn<void()> close) {\n\t\t\t\t\t\t\tauto copy = result;\n\t\t\t\t\t\t\tcopy.sharePhone = confirmed;\n\t\t\t\t\t\t\tsendRequest(copy);\n\t\t\t\t\t\t\tclose();\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t\tconst auto currentSession = resolveSession();\n\t\t\t\t\tconst auto capitalized = [=](const QString &v) {\n\t\t\t\t\t\treturn v.left(1).toUpper() + v.mid(1).toLower();\n\t\t\t\t\t};\n\t\t\t\t\tusing namespace Info::Profile;\n\t\t\t\t\tUi::ConfirmBox(\n\t\t\t\t\t\tbox,\n\t\t\t\t\t\tUi::ConfirmBoxArgs{\n\t\t\t\t\t\t\t.text = tr::lng_url_auth_phone_sure_text(\n\t\t\t\t\t\t\t\tlt_domain,\n\t\t\t\t\t\t\t\trpl::single(\n\t\t\t\t\t\t\t\t\ttr::bold(isApp\n\t\t\t\t\t\t\t\t\t\t? domain\n\t\t\t\t\t\t\t\t\t\t: capitalized(domain))),\n\t\t\t\t\t\t\t\tlt_phone,\n\t\t\t\t\t\t\t\tPhoneValue(currentSession->user()),\n\t\t\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t\t\t.confirmed = confirm(true),\n\t\t\t\t\t\t\t.cancelled = confirm(false),\n\t\t\t\t\t\t\t.confirmText = tr::lng_allow_bot(),\n\t\t\t\t\t\t\t.cancelText = tr::lng_url_auth_phone_sure_deny(),\n\t\t\t\t\t\t});\n\t\t\t\t}));\n\t\t\t};\n\t\t\tShowDetails(\n\t\t\t\tbox,\n\t\t\t\turl,\n\t\t\t\tdomain,\n\t\t\t\t[=](QString code) -> std::shared_ptr<Ui::DynamicImage> {\n\t\t\t\t\treturn MakeMatchCodeImage(resolveSession(), code);\n\t\t\t\t},\n\t\t\t\tcallback,\n\t\t\t\t(bot\n\t\t\t\t\t? object_ptr<Ui::UserpicButton>(\n\t\t\t\t\t\tbox->verticalLayout(),\n\t\t\t\t\t\tbot,\n\t\t\t\t\t\tst::defaultUserpicButton,\n\t\t\t\t\t\tUi::PeerUserpicShape::Forum)\n\t\t\t\t\t: nullptr),\n\t\t\t\tbot ? Info::Profile::NameValue(bot) : nullptr,\n\t\t\t\tbrowser,\n\t\t\t\tdevice,\n\t\t\t\tip,\n\t\t\t\tregion,\n\t\t\t\t(matchCodesFirst\n\t\t\t\t\t? (rpl::single(QStringList()) | rpl::type_erased)\n\t\t\t\t\t: matchCodesShared->value()),\n\t\t\t\tisApp);\n\n\t\t\t*accountResult = AddAccountsMenu(\n\t\t\t\tbox->verticalLayout(),\n\t\t\t\tuserIdHint);\n\t\t\tbox->verticalLayout()->widthValue(\n\t\t\t) | rpl::on_next([=, w = (*accountResult).widget] {\n\t\t\t\tw->moveToRight(st::lineWidth * 4, 0);\n\t\t\t}, (*accountResult).widget->lifetime());\n\t\t\tstate->anotherSession = (*accountResult).anotherSession;\n\t\t\t(*accountResult).setOnUserChanged(reloadRequest);\n\t\t}));\n\t\tstate->box->boxClosing() | rpl::on_next([=] {\n\t\t\trequestDecline();\n\t\t}, state->boxDeclineLifetime);\n\t};\n\tif (!matchCodesFirst || matchCodes.isEmpty()) {\n\t\tshowAuthBox();\n\t\treturn;\n\t}\n\tauto matchCodesBox = base::weak_qptr<Ui::BoxContent>();\n\tmatchCodesBox = show->show(\n\t\tBox([=](not_null<Ui::GenericBox*> matchBox) {\n\t\t\tShowMatchCodesBox(\n\t\t\t\tmatchBox,\n\t\t\t\t[=](QString code) -> std::shared_ptr<Ui::DynamicImage> {\n\t\t\t\t\treturn MakeMatchCodeImage(resolveSession(), code);\n\t\t\t\t},\n\t\t\t\tdomain,\n\t\t\t\tmatchCodes,\n\t\t\t\t[=](QString matchCode) {\n\t\t\t\t\tstate->matchCodesBoxDeclineLifetime.destroy();\n\t\t\t\t\tresolveSession()->api().request(\n\t\t\t\t\t\tMTPmessages_CheckUrlAuthMatchCode(\n\t\t\t\t\t\t\tMTP_string(url),\n\t\t\t\t\t\t\tMTP_string(matchCode))\n\t\t\t\t\t).done([=](const MTPBool &result) {\n\t\t\t\t\t\tif (!mtpIsTrue(result)) {\n\t\t\t\t\t\t\tshow->showToast(\n\t\t\t\t\t\t\t\ttr::lng_url_auth_phone_toast_bad_expired(\n\t\t\t\t\t\t\t\t\ttr::now));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstate->firstMatchCode = std::move(matchCode);\n\t\t\t\t\t\tshowAuthBox();\n\t\t\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\t\t\tshow->showToast((error.type() == u\"URL_EXPIRED\"_q)\n\t\t\t\t\t\t\t? tr::lng_url_auth_phone_toast_bad_expired(\n\t\t\t\t\t\t\t\ttr::now)\n\t\t\t\t\t\t\t: error.type());\n\t\t\t\t\t}).send();\n\t\t\t\t},\n\t\t\t\tisApp);\n\t\t}),\n\t\tUi::LayerOption::KeepOther);\n\tmatchCodesBox->boxClosing() | rpl::on_next([=] {\n\t\trequestDecline();\n\t}, state->matchCodesBoxDeclineLifetime);\n}\n\n} // namespace UrlAuthBox\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/url_auth_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass HistoryItem;\nstruct HistoryMessageMarkupButton;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass GenericBox;\nclass Show;\n} // namespace Ui\n\nnamespace UrlAuthBox {\n\nvoid ActivateButton(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<const HistoryItem*> message,\n\tint row,\n\tint column);\nvoid ActivateUrl(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<Main::Session*> session,\n\tconst QString &url,\n\tQVariant context);\n\n} // namespace UrlAuthBox\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/url_auth_box_content.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/url_auth_box_content.h\"\n\n#include \"base/qthelp_url.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/horizontal_fit_container.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\nnamespace UrlAuthBox {\nnamespace {\n\nconstexpr auto kEmojiAnimationActiveFor = crl::time(250);\n\nvoid PrepareFullWidthRoundButton(\n\t\tnot_null<Ui::RoundButton*> button,\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tconst style::margins &padding) {\n\tbutton->setFullRadius(true);\n\tconst auto paddingHorizontal = padding.left() + padding.right();\n\tcontent->widthValue() | rpl::on_next([=](int w) {\n\t\tbutton->resize(w - paddingHorizontal, button->height());\n\t}, button->lifetime());\n}\n\n} // namespace\n\nvoid ShowMatchCodesBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tFn<std::shared_ptr<Ui::DynamicImage>(QString)> emojiImageFactory,\n\t\tconst QString &domain,\n\t\tconst QStringList &codes,\n\t\tFn<void(QString)> callback,\n\t\tbool isApp) {\n\tbox->setWidth(st::boxWidth);\n\tbox->setStyle(st::urlAuthBox);\n\n\tconst auto content = box->verticalLayout();\n\n\tUi::AddSkip(content);\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_url_auth_match_code_title(),\n\t\t\tst::urlAuthCodesTitle),\n\t\tst::boxRowPadding,\n\t\tstyle::al_top);\n\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\n\tconst auto buttons = content->add(\n\t\tobject_ptr<Ui::HorizontalFitContainer>(\n\t\t\tcontent,\n\t\t\tst::boxRowPadding.left() * 2),\n\t\tst::boxRowPadding);\n\tbuttons->resize(0, st::urlAuthCodesButton.height);\n\n\tfor (const auto &code : codes) {\n\t\tauto emojiLength = 0;\n\t\tconst auto emoji = Ui::Emoji::Find(code, &emojiLength);\n\t\tconst auto emojiCode = (emoji && (emojiLength == code.size()));\n\t\tconst auto button = Ui::CreateChild<Ui::RoundButton>(\n\t\t\tbuttons,\n\t\t\trpl::single(emojiCode ? QString() : code),\n\t\t\tst::urlAuthCodesButton);\n\t\tif (emojiCode) {\n\t\t\tbutton->setTextFgOverride(QColor(Qt::transparent));\n\t\t\tconst auto overlay = Ui::CreateChild<Ui::RpWidget>(button);\n\t\t\toverlay->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\toverlay->show();\n\t\t\tstruct State {\n\t\t\t\tstd::shared_ptr<Ui::DynamicImage> image;\n\t\t\t\tbool hovered = false;\n\t\t\t\tcrl::time lastFrameUpdate = 0;\n\t\t\t};\n\t\t\tconst auto state = overlay->lifetime().make_state<State>();\n\t\t\tconst auto animationActive = [=] {\n\t\t\t\treturn state->lastFrameUpdate\n\t\t\t\t\t&& (crl::now() - state->lastFrameUpdate\n\t\t\t\t\t\t<= kEmojiAnimationActiveFor);\n\t\t\t};\n\t\t\tconst auto refreshImage = [=] {\n\t\t\t\tif (state->image) {\n\t\t\t\t\tstate->image->subscribeToUpdates(nullptr);\n\t\t\t\t}\n\t\t\t\tstate->image = emojiImageFactory\n\t\t\t\t\t? emojiImageFactory(code)\n\t\t\t\t\t: nullptr;\n\t\t\t\tif (state->image) {\n\t\t\t\t\tstate->image->subscribeToUpdates([=] {\n\t\t\t\t\t\tstate->lastFrameUpdate = crl::now();\n\t\t\t\t\t\toverlay->update();\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\toverlay->update();\n\t\t\t};\n\t\t\trefreshImage();\n\t\t\toverlay->lifetime().add([=] {\n\t\t\t\tif (state->image) {\n\t\t\t\t\tstate->image->subscribeToUpdates(nullptr);\n\t\t\t\t}\n\t\t\t});\n\t\t\tbutton->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\t\t\tswitch (e->type()) {\n\t\t\t\tcase QEvent::Enter:\n\t\t\t\t\tif (!state->hovered) {\n\t\t\t\t\t\tstate->hovered = true;\n\t\t\t\t\t\tif (!animationActive()) {\n\t\t\t\t\t\t\trefreshImage();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tcase QEvent::Leave:\n\t\t\t\t\tstate->hovered = false;\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}, overlay->lifetime());\n\t\t\tbutton->sizeValue() | rpl::on_next([=](QSize size) {\n\t\t\t\toverlay->resize(size);\n\t\t\t}, overlay->lifetime());\n\t\t\toverlay->paintOn([=](QPainter &p) {\n\t\t\t\tif (state->image) {\n\t\t\t\t\tconst auto side = st::urlAuthCodesButton.height;\n\t\t\t\t\tconst auto frame = state->image->image(side);\n\t\t\t\t\tconst auto visible = frame.isNull()\n\t\t\t\t\t\t? 0\n\t\t\t\t\t\t: (frame.width() / frame.devicePixelRatio());\n\t\t\t\t\tp.drawImage(\n\t\t\t\t\t\tQPoint(\n\t\t\t\t\t\t\t(overlay->width() - visible) / 2,\n\t\t\t\t\t\t\t(overlay->height() - visible) / 2),\n\t\t\t\t\t\tframe);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto size = Ui::Emoji::GetSizeLarge();\n\t\t\t\tconst auto visible = size / style::DevicePixelRatio();\n\t\t\t\tUi::Emoji::Draw(\n\t\t\t\t\tp,\n\t\t\t\t\temoji,\n\t\t\t\t\tsize,\n\t\t\t\t\t(overlay->width() - visible) / 2,\n\t\t\t\t\t(overlay->height() - visible) / 2);\n\t\t\t});\n\t\t}\n\t\tbutton->setFullRadius(true);\n\t\tbutton->setClickedCallback([=] {\n\t\t\tcallback(code);\n\t\t\tbox->closeBox();\n\t\t});\n\t\tbuttons->add(button);\n\t}\n\n\tUi::AddSkip(content);\n\n\tconst auto domainUrl = isApp ? QString() : qthelp::validate_url(domain);\n\tif (!domainUrl.isEmpty() || isApp) {\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\t\tcontent->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_url_auth_login_title(\n\t\t\t\t\tlt_domain,\n\t\t\t\t\trpl::single(isApp\n\t\t\t\t\t\t? tr::bold(domain)\n\t\t\t\t\t\t: Ui::Text::Link(domain, domainUrl)),\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::urlAuthCheckboxAbout),\n\t\t\tst::boxRowPadding,\n\t\t\tstyle::al_top);\n\t}\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\t{\n\t\tconst auto &padding = st::boxRowPadding;\n\t\tconst auto button = content->add(\n\t\t\tobject_ptr<Ui::RoundButton>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_cancel(),\n\t\t\t\tst::attentionBoxButton),\n\t\t\tpadding);\n\t\tPrepareFullWidthRoundButton(button, content, padding);\n\t\tbutton->setClickedCallback([=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t}\n}\n\nSwitchableUserpicButton::SwitchableUserpicButton(\n\tnot_null<Ui::RpWidget*> parent,\n\tint size)\n: RippleButton(parent, st::defaultRippleAnimation)\n, _size(size)\n, _userpicSize(st::restoreUserpicIcon.photoSize)\n, _skip((_size - _userpicSize) / 2) {\n\tresize(_size, _size);\n}\n\nvoid SwitchableUserpicButton::setUserpic(not_null<Ui::RpWidget*> userpic) {\n\t_userpic = userpic;\n\t_userpic->setParent(this);\n\t_userpic->moveToRight(_skip, _skip);\n\t_userpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_userpic->show();\n\tupdate();\n}\n\nvoid SwitchableUserpicButton::setExpanded(bool expanded) {\n\tif (_expanded == expanded) {\n\t\treturn;\n\t}\n\t_expanded = expanded;\n\tconst auto w = _expanded\n\t\t? (_size * 2.5 - _userpicSize)\n\t\t: _size;\n\tresize(w, _size);\n\tif (_userpic) {\n\t\t_userpic->moveToRight(_skip, _skip);\n\t}\n\tupdate();\n}\n\nvoid SwitchableUserpicButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tpaintRipple(p, 0, 0);\n\n\tif (!_expanded) {\n\t\treturn;\n\t}\n\n\tconst auto arrowSize = st::lineWidth * 10;\n\tconst auto center = QPoint(_size / 2, height() / 2 + st::lineWidth * 3);\n\n\tauto pen = QPen(st::windowSubTextFg);\n\tpen.setWidthF(st::lineWidth * 1.5);\n\tp.setPen(pen);\n\tp.setRenderHint(QPainter::Antialiasing);\n\n\tp.drawLine(center, center + QPoint(-arrowSize / 2, -arrowSize / 2));\n\tp.drawLine(center, center + QPoint(arrowSize / 2, -arrowSize / 2));\n}\n\nQImage SwitchableUserpicButton::prepareRippleMask() const {\n\treturn _expanded\n\t\t? Ui::RippleAnimation::RoundRectMask(size(), height() / 2)\n\t\t: Ui::RippleAnimation::EllipseMask(size());\n}\n\nQPoint SwitchableUserpicButton::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos());\n}\n\nvoid AddAuthInfoRow(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst QString &topText,\n\t\tconst QString &bottomText,\n\t\tconst QString &leftText,\n\t\tconst style::icon &icon) {\n\tconst auto row = container->add(\n\t\tobject_ptr<Ui::RpWidget>(container),\n\t\tst::boxRowPadding);\n\n\tconst auto topLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\trow,\n\t\ttopText,\n\t\tst::urlAuthBoxRowTopLabel);\n\ttopLabel->setSelectable(true);\n\tUi::InstallTooltip(topLabel, [=] {\n\t\treturn (topLabel->textMaxWidth() > topLabel->width())\n\t\t\t? topText\n\t\t\t: QString();\n\t});\n\tconst auto bottomLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\trow,\n\t\tbottomText,\n\t\tst::urlAuthBoxRowBottomLabel);\n\tbottomLabel->setSelectable(true);\n\tUi::InstallTooltip(bottomLabel, [=] {\n\t\treturn (bottomLabel->textMaxWidth() > bottomLabel->width())\n\t\t\t? bottomText\n\t\t\t: QString();\n\t});\n\tconst auto leftLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\trow,\n\t\tleftText,\n\t\tst::boxLabel);\n\n\trpl::combine(\n\t\trow->widthValue(),\n\t\ttopLabel->sizeValue(),\n\t\tbottomLabel->sizeValue()\n\t) | rpl::on_next([=](int rowWidth, QSize topSize, QSize bottomSize) {\n\t\tconst auto totalHeight = topSize.height() + bottomSize.height();\n\t\trow->resize(rowWidth, totalHeight);\n\n\t\tconst auto left = st::sessionValuePadding.left();\n\t\tconst auto availableWidth = rowWidth\n\t\t\t- leftLabel->width()\n\t\t\t- left\n\t\t\t- st::defaultVerticalListSkip;\n\n\t\ttopLabel->resizeToNaturalWidth(availableWidth);\n\t\ttopLabel->moveToRight(0, 0);\n\t\tbottomLabel->resizeToNaturalWidth(availableWidth);\n\t\tbottomLabel->moveToRight(0, topSize.height());\n\n\t\tleftLabel->moveToLeft(left, (totalHeight - leftLabel->height()) / 2);\n\t}, row->lifetime());\n\n\t{\n\t\tconst auto widget = Ui::CreateChild<Ui::RpWidget>(row);\n\t\twidget->resize(icon.size());\n\n\t\trpl::combine(\n\t\t\trow->widthValue(),\n\t\t\ttopLabel->sizeValue(),\n\t\t\tbottomLabel->sizeValue()\n\t\t) | rpl::on_next([=](int rowWidth, QSize topSize, QSize bottomSize) {\n\t\t\tconst auto totalHeight = topSize.height() + bottomSize.height();\n\t\t\twidget->moveToLeft(0, (totalHeight - leftLabel->height()) / 2);\n\t\t}, row->lifetime());\n\n\t\twidget->paintRequest() | rpl::on_next([=, &icon] {\n\t\t\tauto p = QPainter(widget);\n\t\t\ticon.paintInCenter(p, widget->rect());\n\t\t}, widget->lifetime());\n\t}\n}\n\nvoid Show(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst QString &url,\n\t\tconst QString &domain,\n\t\tconst QString &selfName,\n\t\tconst QString &botName,\n\t\tFn<void(Result)> callback) {\n\tbox->setWidth(st::boxWidth);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_url_auth_open_confirm(tr::now, lt_link, url),\n\t\t\tst::boxLabel),\n\t\tst::boxPadding);\n\n\tconst auto addCheckbox = [&](const TextWithEntities &text) {\n\t\tconst auto checkbox = box->addRow(\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tbox,\n\t\t\t\ttext,\n\t\t\t\ttrue,\n\t\t\t\tst::urlAuthCheckbox),\n\t\t\tstyle::margins(\n\t\t\t\tst::boxPadding.left(),\n\t\t\t\tst::boxPadding.bottom(),\n\t\t\t\tst::boxPadding.right(),\n\t\t\t\tst::boxPadding.bottom()));\n\t\tcheckbox->setAllowTextLines();\n\t\treturn checkbox;\n\t};\n\n\tconst auto auth = addCheckbox(\n\t\ttr::lng_url_auth_login_option(\n\t\t\ttr::now,\n\t\t\tlt_domain,\n\t\t\ttr::bold(domain),\n\t\t\tlt_user,\n\t\t\ttr::bold(selfName),\n\t\t\ttr::marked));\n\n\tconst auto allow = !botName.isEmpty()\n\t\t? addCheckbox(tr::lng_url_auth_allow_messages(\n\t\t\ttr::now,\n\t\t\tlt_bot,\n\t\t\ttr::bold(botName),\n\t\t\ttr::marked))\n\t\t: nullptr;\n\n\tif (allow) {\n\t\trpl::single(\n\t\t\tauth->checked()\n\t\t) | rpl::then(\n\t\t\tauth->checkedChanges()\n\t\t) | rpl::on_next([=](bool checked) {\n\t\t\tif (!checked) {\n\t\t\t\tallow->setChecked(false);\n\t\t\t}\n\t\t\tallow->setDisabled(!checked);\n\t\t}, auth->lifetime());\n\t}\n\n\tbox->addButton(tr::lng_open_link(), [=] {\n\t\tconst auto authed = auth->checked();\n\t\tconst auto allowed = (authed && allow && allow->checked());\n\t\tcallback({\n\t\t\t.auth = authed,\n\t\t\t.allowWrite = allowed,\n\t\t});\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\nvoid ShowDetails(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst QString &url,\n\t\tconst QString &domain,\n\t\tFn<std::shared_ptr<Ui::DynamicImage>(QString)> emojiImageFactory,\n\t\tFn<void(Result)> callback,\n\t\tobject_ptr<Ui::RpWidget> userpicOwned,\n\t\trpl::producer<QString> botName,\n\t\tconst QString &browser,\n\t\tconst QString &platform,\n\t\tconst QString &ip,\n\t\tconst QString &region,\n\t\trpl::producer<QStringList> matchCodes,\n\t\tbool isApp) {\n\tbox->setWidth(st::boxWidth);\n\tbox->setStyle(st::urlAuthBox);\n\n\tconst auto content = box->verticalLayout();\n\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tif (userpicOwned) {\n\t\tconst auto userpic = content->add(\n\t\t\tstd::move(userpicOwned),\n\t\t\tst::boxRowPadding,\n\t\t\tstyle::al_top);\n\t\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\t}\n\n\tconst auto domainUrl = isApp ? QString() : qthelp::validate_url(domain);\n\tconst auto userpicButtonWidth = st::restoreUserpicIcon.photoSize;\n\tconst auto titlePadding = style::margins(\n\t\tst::boxRowPadding.left(),\n\t\tst::boxRowPadding.top(),\n\t\tst::boxRowPadding.right() + userpicButtonWidth,\n\t\tst::boxRowPadding.bottom());\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\t(isApp\n\t\t\t\t? tr::lng_url_auth_login_title(\n\t\t\t\t\tlt_domain,\n\t\t\t\t\trpl::single(tr::bold(domain)),\n\t\t\t\t\ttr::marked)\n\t\t\t\t: domainUrl.isEmpty()\n\t\t\t\t\t? tr::lng_url_auth_login_button(tr::marked)\n\t\t\t\t\t: tr::lng_url_auth_login_title(\n\t\t\t\t\t\tlt_domain,\n\t\t\t\t\t\trpl::single(Ui::Text::Link(domain, domainUrl)),\n\t\t\t\t\t\ttr::marked)),\n\t\t\tst::boxTitle),\n\t\ttitlePadding,\n\t\tstyle::al_top);\n\tUi::AddSkip(content);\n\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\t(isApp\n\t\t\t\t? tr::lng_url_auth_app_access(tr::rich)\n\t\t\t\t: tr::lng_url_auth_site_access(tr::rich)),\n\t\t\tst::urlAuthCheckboxAbout),\n\t\tst::boxRowPadding);\n\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tif (!platform.isEmpty() || !browser.isEmpty()) {\n\t\tAddAuthInfoRow(\n\t\t\tcontent,\n\t\t\tplatform,\n\t\t\tbrowser,\n\t\t\ttr::lng_url_auth_device_label(tr::now),\n\t\t\tst::menuIconDevices);\n\t}\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\n\tif (!ip.isEmpty() || !region.isEmpty()) {\n\t\tAddAuthInfoRow(\n\t\t\tcontent,\n\t\t\tip,\n\t\t\tregion,\n\t\t\ttr::lng_url_auth_ip_label(tr::now),\n\t\t\tst::menuIconAddress);\n\t}\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\n\tUi::AddDividerText(\n\t\tcontent,\n\t\trpl::single(tr::lng_url_auth_login_attempt(tr::now)));\n\tUi::AddSkip(content);\n\n\tauto allowMessages = (Ui::SettingsButton*)(nullptr);\n\tif (botName) {\n\t\tallowMessages = content->add(\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_url_auth_allow_messages_label()));\n\t\tallowMessages->toggleOn(rpl::single(false));\n\t\tUi::AddSkip(content);\n\t\tUi::AddDividerText(\n\t\t\tcontent,\n\t\t\ttr::lng_url_auth_allow_messages_about(\n\t\t\t\tlt_bot,\n\t\t\t\tstd::move(botName)));\n\t\tUi::AddSkip(content);\n\t}\n\n\tstruct State {\n\t\tQStringList matchCodes;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tstd::move(matchCodes) | rpl::on_next([=](const QStringList &codes) {\n\t\tstate->matchCodes = codes;\n\t}, box->lifetime());\n\n\tUi::AddSkip(content);\n\t{\n\t\tconst auto &padding = st::boxRowPadding;\n\t\tconst auto button = content->add(\n\t\t\tobject_ptr<Ui::RoundButton>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_url_auth_login_button(),\n\t\t\t\tst::defaultLightButton),\n\t\t\tpadding);\n\t\tPrepareFullWidthRoundButton(button, content, padding);\n\t\tbutton->setClickedCallback([=] {\n\t\t\tif (state->matchCodes.isEmpty()) {\n\t\t\t\tcallback({\n\t\t\t\t\t.auth = true,\n\t\t\t\t\t.allowWrite = (allowMessages && allowMessages->toggled()),\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tbox->uiShow()->show(Box([=](\n\t\t\t\t\tnot_null<Ui::GenericBox*> matchCodesBox) {\n\t\t\t\tShowMatchCodesBox(\n\t\t\t\t\tmatchCodesBox,\n\t\t\t\t\temojiImageFactory,\n\t\t\t\t\tdomain,\n\t\t\t\t\tstate->matchCodes,\n\t\t\t\t\t[=](QString matchCode) {\n\t\t\t\t\t\tcallback({\n\t\t\t\t\t\t\t.auth = true,\n\t\t\t\t\t\t\t.allowWrite = (allowMessages\n\t\t\t\t\t\t\t\t&& allowMessages->toggled()),\n\t\t\t\t\t\t\t.matchCode = std::move(matchCode),\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tisApp);\n\t\t\t}));\n\t\t});\n\t}\n\tUi::AddSkip(content);\n\t{\n\t\tconst auto &padding = st::boxRowPadding;\n\t\tconst auto button = content->add(\n\t\t\tobject_ptr<Ui::RoundButton>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_suggest_action_decline(),\n\t\t\t\tst::attentionBoxButton),\n\t\t\tpadding);\n\t\tPrepareFullWidthRoundButton(button, content, padding);\n\t\tbutton->setClickedCallback([=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t}\n}\n\n} // namespace UrlAuthBox\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/url_auth_box_content.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/buttons.h\"\n\nnamespace Ui {\nclass DynamicImage;\nclass GenericBox;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace UrlAuthBox {\n\nstruct Result {\n\tbool auth : 1 = false;\n\tbool allowWrite : 1 = false;\n\tbool sharePhone : 1 = false;\n\tQString matchCode;\n};\n\nclass SwitchableUserpicButton final : public Ui::RippleButton {\npublic:\n\tSwitchableUserpicButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tint size);\n\n\tvoid setExpanded(bool expanded);\n\tvoid setUserpic(not_null<Ui::RpWidget*>);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\n\tconst int _size;\n\tconst int _userpicSize;\n\tconst int _skip;\n\tbool _expanded = false;\n\tUi::RpWidget *_userpic = nullptr;\n\n};\n\nvoid AddAuthInfoRow(\n\tnot_null<Ui::VerticalLayout*> container,\n\tconst QString &topText,\n\tconst QString &bottomText,\n\tconst QString &leftText,\n\tconst style::icon &icon);\n\nvoid ShowMatchCodesBox(\n\tnot_null<Ui::GenericBox*> box,\n\tFn<std::shared_ptr<Ui::DynamicImage>(QString)> emojiImageFactory,\n\tconst QString &domain,\n\tconst QStringList &codes,\n\tFn<void(QString)> callback,\n\tbool isApp = false);\n\nvoid Show(\n\tnot_null<Ui::GenericBox*> box,\n\tconst QString &url,\n\tconst QString &domain,\n\tconst QString &selfName,\n\tconst QString &botName,\n\tFn<void(Result)> callback);\n\nvoid ShowDetails(\n\tnot_null<Ui::GenericBox*> box,\n\tconst QString &url,\n\tconst QString &domain,\n\tFn<std::shared_ptr<Ui::DynamicImage>(QString)> emojiImageFactory,\n\tFn<void(Result)> callback,\n\tobject_ptr<Ui::RpWidget> userpicOwned,\n\trpl::producer<QString> botName,\n\tconst QString &browser,\n\tconst QString &platform,\n\tconst QString &ip,\n\tconst QString &region,\n\trpl::producer<QStringList> matchCodes = rpl::single(QStringList()),\n\tbool isApp = false);\n\n} // namespace UrlAuthBox\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/username_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"boxes/username_box.h\"\n\n#include \"boxes/peers/edit_peer_usernames_list.h\"\n#include \"base/timer.h\"\n#include \"boxes/peers/edit_peer_common.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config_values.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/sender.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/text/text_variant.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/special_fields.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/follow_slide_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace {\n\nclass UsernameEditor final : public Ui::RpWidget {\npublic:\n\tUsernameEditor(not_null<Ui::RpWidget*>, not_null<PeerData*> peer);\n\n\tvoid setInnerFocus();\n\tvoid setEnabled(bool value);\n\t[[nodiscard]] rpl::producer<> submitted() const;\n\t[[nodiscard]] rpl::producer<> save();\n\t[[nodiscard]] rpl::producer<UsernameCheckInfo> checkInfoChanged() const;\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid updateFail(const QString &error);\n\tvoid checkFail(const QString &error);\n\n\tvoid checkInfoPurchaseAvailable();\n\n\tvoid check();\n\tvoid changed();\n\n\tvoid checkInfoChange();\n\n\t[[nodiscard]] QString editableUsername() const;\n\n\tQString getName() const;\n\n\tconst not_null<PeerData*> _peer;\n\tconst not_null<Main::Session*> _session;\n\tconst style::margins &_padding;\n\tMTP::Sender _api;\n\n\tobject_ptr<Ui::UsernameInput> _username;\n\n\tmtpRequestId _saveRequestId = 0;\n\tmtpRequestId _checkRequestId = 0;\n\tQString _sentUsername, _checkUsername, _errorText, _goodText;\n\n\tbase::Timer _checkTimer;\n\n\trpl::event_stream<> _saved;\n\trpl::event_stream<UsernameCheckInfo> _checkInfoChanged;\n\n};\n\nUsernameEditor::UsernameEditor(\n\tnot_null<Ui::RpWidget*>,\n\tnot_null<PeerData*> peer)\n: _peer(peer)\n, _session(&peer->session())\n, _padding(st::usernamePadding)\n, _api(&_session->mtp())\n, _username(\n\tthis,\n\tst::defaultInputField,\n\trpl::single(u\"@username\"_q),\n\teditableUsername(),\n\tQString())\n, _checkTimer([=] { check(); }) {\n\t_goodText = editableUsername().isEmpty()\n\t\t? QString()\n\t\t: tr::lng_username_available(tr::now);\n\n\tconnect(_username, &Ui::MaskedInputField::changed, [=] { changed(); });\n\n\tresize(width(), (_padding.top() + _username->height()));\n}\n\nrpl::producer<> UsernameEditor::submitted() const {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tQObject::connect(\n\t\t\t_username,\n\t\t\t&Ui::MaskedInputField::submitted,\n\t\t\t[=] { consumer.put_next({}); });\n\t\treturn lifetime;\n\t};\n}\n\nvoid UsernameEditor::setInnerFocus() {\n\tif (_username->isEnabled()) {\n\t\t_username->setFocusFast();\n\t}\n}\n\nvoid UsernameEditor::setEnabled(bool value) {\n\t_username->setEnabled(value);\n\t_username->setDisplayFocused(value);\n}\n\nvoid UsernameEditor::resizeEvent(QResizeEvent *e) {\n\t_username->resize(\n\t\twidth() - _padding.left() - _padding.right(),\n\t\t_username->height());\n\t_username->moveToLeft(_padding.left(), _padding.top());\n}\n\nrpl::producer<> UsernameEditor::save() {\n\tif (_saveRequestId) {\n\t\treturn _saved.events();\n\t}\n\n\t_sentUsername = getName();\n\t_saveRequestId = _api.request(MTPaccount_UpdateUsername(\n\t\tMTP_string(_sentUsername)\n\t)).done([=](const MTPUser &result) {\n\t\t_saveRequestId = 0;\n\t\t_session->data().processUser(result);\n\t\t_saved.fire_done();\n\t}).fail([=](const MTP::Error &error) {\n\t\t_saveRequestId = 0;\n\t\tupdateFail(error.type());\n\t}).send();\n\treturn _saved.events();\n}\n\nQString UsernameEditor::editableUsername() const {\n\tif (const auto user = _peer->asUser()) {\n\t\treturn user->editableUsername();\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\treturn channel->editableUsername();\n\t} else {\n\t\treturn QString();\n\t}\n}\n\nrpl::producer<UsernameCheckInfo> UsernameEditor::checkInfoChanged() const {\n\treturn _checkInfoChanged.events();\n}\n\nvoid UsernameEditor::check() {\n\t_api.request(base::take(_checkRequestId)).cancel();\n\n\tconst auto name = getName();\n\tif (name.size() < Ui::EditPeer::kMinUsernameLength) {\n\t\treturn;\n\t}\n\t_checkUsername = name;\n\t_checkRequestId = _api.request(MTPaccount_CheckUsername(\n\t\tMTP_string(name)\n\t)).done([=](const MTPBool &result) {\n\t\t_checkRequestId = 0;\n\n\t\t_errorText = (mtpIsTrue(result)\n\t\t\t\t|| (_checkUsername == editableUsername()))\n\t\t\t? QString()\n\t\t\t: tr::lng_username_occupied(tr::now);\n\t\t_goodText = _errorText.isEmpty()\n\t\t\t? tr::lng_username_available(tr::now)\n\t\t\t: QString();\n\n\t\tcheckInfoChange();\n\t}).fail([=](const MTP::Error &error) {\n\t\t_checkRequestId = 0;\n\t\tcheckFail(error.type());\n\t}).send();\n}\n\nvoid UsernameEditor::changed() {\n\tconst auto name = getName();\n\tif (name.isEmpty()) {\n\t\tif (!_errorText.isEmpty() || !_goodText.isEmpty()) {\n\t\t\t_errorText = _goodText = QString();\n\t\t\t_checkInfoChanged.fire({ UsernameCheckInfo::Type::Default });\n\t\t}\n\t\t_checkTimer.cancel();\n\t} else {\n\t\tconst auto len = int(name.size());\n\t\tfor (auto i = 0; i < len; ++i) {\n\t\t\tconst auto ch = name.at(i);\n\t\t\tif ((ch < 'A' || ch > 'Z')\n\t\t\t\t&& (ch < 'a' || ch > 'z')\n\t\t\t\t&& (ch < '0' || ch > '9')\n\t\t\t\t&& ch != '_'\n\t\t\t\t&& (ch != '@' || i > 0)) {\n\t\t\t\tif (_errorText != tr::lng_username_bad_symbols(tr::now)) {\n\t\t\t\t\t_errorText = tr::lng_username_bad_symbols(tr::now);\n\t\t\t\t\tcheckInfoChange();\n\t\t\t\t}\n\t\t\t\t_checkTimer.cancel();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (name.size() < Ui::EditPeer::kMinUsernameLength) {\n\t\t\tif (_errorText != tr::lng_username_too_short(tr::now)) {\n\t\t\t\t_errorText = tr::lng_username_too_short(tr::now);\n\t\t\t\tcheckInfoChange();\n\t\t\t}\n\t\t\t_checkTimer.cancel();\n\t\t} else {\n\t\t\tif (!_errorText.isEmpty() || !_goodText.isEmpty()) {\n\t\t\t\t_errorText = _goodText = QString();\n\t\t\t\tcheckInfoChange();\n\t\t\t}\n\t\t\t_checkTimer.callOnce(Ui::EditPeer::kUsernameCheckTimeout);\n\t\t}\n\t}\n}\n\nvoid UsernameEditor::checkInfoChange() {\n\tif (!_errorText.isEmpty()) {\n\t\t_checkInfoChanged.fire({\n\t\t\t.type = UsernameCheckInfo::Type::Error,\n\t\t\t.text = { _errorText },\n\t\t});\n\t} else if (!_goodText.isEmpty()) {\n\t\t_checkInfoChanged.fire({\n\t\t\t.type = UsernameCheckInfo::Type::Good,\n\t\t\t.text = { _goodText },\n\t\t});\n\t} else {\n\t\t_checkInfoChanged.fire({\n\t\t\t.type = UsernameCheckInfo::Type::Default,\n\t\t\t.text = { tr::lng_username_choose(tr::now) },\n\t\t});\n\t}\n}\n\nvoid UsernameEditor::checkInfoPurchaseAvailable() {\n\t_username->setFocus();\n\t_username->showError();\n\t_errorText = u\".bad.\"_q;\n\n\t_checkInfoChanged.fire(\n\t\tUsernameCheckInfo::PurchaseAvailable(_checkUsername, _peer));\n}\n\nvoid UsernameEditor::updateFail(const QString &error) {\n\tif ((error == u\"USERNAME_NOT_MODIFIED\"_q)\n\t\t|| (_sentUsername == editableUsername())) {\n\t\tif (const auto user = _peer->asUser()) {\n\t\t\tuser->setName(\n\t\t\t\tTextUtilities::SingleLine(user->firstName),\n\t\t\t\tTextUtilities::SingleLine(user->lastName),\n\t\t\t\tTextUtilities::SingleLine(user->nameOrPhone),\n\t\t\t\tTextUtilities::SingleLine(_sentUsername));\n\t\t}\n\t\t_saved.fire_done();\n\t} else if (error == u\"USERNAME_INVALID\"_q) {\n\t\t_username->setFocus();\n\t\t_username->showError();\n\t\t_errorText = tr::lng_username_invalid(tr::now);\n\t\tcheckInfoChange();\n\t} else if ((error == u\"USERNAME_OCCUPIED\"_q)\n\t\t|| (error == u\"USERNAMES_UNAVAILABLE\"_q)) {\n\t\t_username->setFocus();\n\t\t_username->showError();\n\t\t_errorText = tr::lng_username_occupied(tr::now);\n\t\tcheckInfoChange();\n\t} else if (error == u\"USERNAME_PURCHASE_AVAILABLE\"_q) {\n\t\tcheckInfoPurchaseAvailable();\n\t} else {\n\t\t_username->setFocus();\n\t}\n}\n\nvoid UsernameEditor::checkFail(const QString &error) {\n\tif (error == u\"USERNAME_INVALID\"_q) {\n\t\t_errorText = tr::lng_username_invalid(tr::now);\n\t\tcheckInfoChange();\n\t} else if ((error == u\"USERNAME_OCCUPIED\"_q)\n\t\t&& (_checkUsername != editableUsername())) {\n\t\t_errorText = tr::lng_username_occupied(tr::now);\n\t\tcheckInfoChange();\n\t} else if (error == u\"USERNAME_PURCHASE_AVAILABLE\"_q) {\n\t\tcheckInfoPurchaseAvailable();\n\t} else {\n\t\t_goodText = QString();\n\t\t_username->setFocus();\n\t}\n}\n\nQString UsernameEditor::getName() const {\n\treturn _username->text().replace('@', QString()).trimmed();\n}\n\n} // namespace\n\nvoid FillUsernamesBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> onSaved) {\n\tconst auto isBot = peer && peer->isUser() && peer->asUser()->isBot();\n\tbox->setTitle(isBot\n\t\t? tr::lng_bot_username_title()\n\t\t: tr::lng_username_title());\n\n\tconst auto container = box->verticalLayout();\n\n\tconst auto editor = box->addRow(\n\t\tobject_ptr<UsernameEditor>(box, peer),\n\t\tstyle::margins());\n\teditor->setEnabled(!isBot);\n\tbox->setFocusCallback([=] { editor->setInnerFocus(); });\n\n\tAddUsernameCheckLabel(container, editor->checkInfoChanged());\n\n\tauto description = [&]() -> rpl::producer<TextWithEntities> {\n\t\tif (!isBot) {\n\t\t\treturn rpl::combine(\n\t\t\t\ttr::lng_username_description1(tr::rich),\n\t\t\t\ttr::lng_username_description2(tr::rich)\n\t\t\t) | rpl::map([](TextWithEntities d1, TextWithEntities d2) {\n\t\t\t\treturn d1.append(\"\\n\\n\").append(std::move(d2));\n\t\t\t});\n\t\t}\n\t\tif (const auto url = AppConfig::FragmentLink(&peer->session())) {\n\t\t\tconst auto link = tr::link(\n\t\t\t\ttr::lng_bot_username_description1_link(tr::now),\n\t\t\t\t*url);\n\t\t\treturn tr::lng_bot_username_description1(\n\t\t\t\tlt_link,\n\t\t\t\trpl::single(link),\n\t\t\t\ttr::rich);\n\t\t}\n\t\treturn rpl::single<TextWithEntities>({});\n\t}();\n\tcontainer->add(object_ptr<Ui::DividerLabel>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\tstd::move(description),\n\t\t\tst::boxDividerLabel),\n\t\tst::defaultBoxDividerLabelPadding));\n\n\tconst auto list = box->addRow(\n\t\tobject_ptr<UsernamesList>(\n\t\t\tbox,\n\t\t\tpeer,\n\t\t\tbox->uiShow(),\n\t\t\t!isBot\n\t\t\t\t? [=] { box->scrollToY(0); editor->setInnerFocus(); }\n\t\t\t\t: Fn<void()>(nullptr)),\n\t\tstyle::margins());\n\n\tconst auto finish = [=] {\n\t\tlist->save(\n\t\t) | rpl::on_done([=] {\n\t\t\teditor->save(\n\t\t\t) | rpl::on_done([=] {\n\t\t\t\tif (onSaved) {\n\t\t\t\t\tonSaved();\n\t\t\t\t}\n\t\t\t\tbox->closeBox();\n\t\t\t}, box->lifetime());\n\t\t}, box->lifetime());\n\t};\n\teditor->submitted(\n\t) | rpl::on_next(finish, editor->lifetime());\n\n\tif (isBot) {\n\t\tbox->addButton(tr::lng_close(), [=] { box->closeBox(); });\n\t} else {\n\t\tbox->addButton(tr::lng_settings_save(), finish);\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t}\n}\n\nvoid UsernamesBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<PeerData*> peer) {\n\tFillUsernamesBox(box, peer, nullptr);\n}\n\nvoid UsernamesBoxWithCallback(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> onSaved) {\n\tFillUsernamesBox(box, peer, std::move(onSaved));\n}\n\nvoid AddUsernameCheckLabel(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<UsernameCheckInfo> checkInfo) {\n\tconst auto padding = st::boxRowPadding;\n\tconst auto &st = st::aboutRevokePublicLabel;\n\tconst auto skip = (st::usernameSkip - st.style.font->height) / 4;\n\n\tauto wrapped = object_ptr<Ui::VerticalLayout>(container);\n\tUi::AddSkip(wrapped, skip);\n\tconst auto label = wrapped->add(object_ptr<Ui::FlatLabel>(wrapped, st));\n\tUi::AddSkip(wrapped, skip);\n\n\tUi::AddSkip(container, skip);\n\tcontainer->add(\n\t\tobject_ptr<Ui::FollowSlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tstd::move(wrapped)),\n\t\tpadding);\n\n\trpl::combine(\n\t\tstd::move(checkInfo),\n\t\tcontainer->widthValue()\n\t) | rpl::on_next([=](const UsernameCheckInfo &info, int w) {\n\t\tusing Type = UsernameCheckInfo::Type;\n\t\tlabel->setMarkedText(info.text);\n\t\tconst auto &color = (info.type == Type::Good)\n\t\t\t? st::boxTextFgGood\n\t\t\t: (info.type == Type::Error)\n\t\t\t? st::boxTextFgError\n\t\t\t: st::usernameDefaultFg;\n\t\tlabel->setTextColorOverride(color->c);\n\t\tlabel->resizeToWidth(w - padding.left() - padding.right());\n\t}, label->lifetime());\n\tUi::AddSkip(container, skip);\n}\n\nUsernameCheckInfo UsernameCheckInfo::PurchaseAvailable(\n\t\tconst QString &username,\n\t\tnot_null<PeerData*> peer) {\n\tif (const auto fragmentLink = AppConfig::FragmentLink(&peer->session())) {\n\t\treturn {\n\t\t\t.type = UsernameCheckInfo::Type::Default,\n\t\t\t.text = tr::lng_username_purchase_available(\n\t\t\t\ttr::now,\n\t\t\t\tlt_link,\n\t\t\t\ttr::link(\n\t\t\t\t\ttr::lng_username_purchase_available_link(tr::now),\n\t\t\t\t\t(*fragmentLink) + u\"/username/\"_q + username),\n\t\t\t\ttr::rich),\n\t\t};\n\t} else {\n\t\treturn {\n\t\t\t.type = UsernameCheckInfo::Type::Error,\n\t\t\t.text = { u\"INTERNAL_SERVER_ERROR\"_q },\n\t\t};\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/boxes/username_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass GenericBox;\nclass VerticalLayout;\n} // namespace Ui\n\nclass PeerData;\n\nvoid UsernamesBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<PeerData*> peer);\n\nvoid UsernamesBoxWithCallback(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<PeerData*> peer,\n\tFn<void()> onSaved);\n\nstruct UsernameCheckInfo final {\n\t[[nodiscard]] static UsernameCheckInfo PurchaseAvailable(\n\t\tconst QString &username,\n\t\tnot_null<PeerData*> peer);\n\n\tenum class Type {\n\t\tGood,\n\t\tError,\n\t\tDefault,\n\t};\n\tType type;\n\tTextWithEntities text;\n};\n\nvoid AddUsernameCheckLabel(\n\tnot_null<Ui::VerticalLayout*> container,\n\trpl::producer<UsernameCheckInfo> checkInfo);\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\n\nusing \"ui/widgets/widgets.style\";\nusing \"ui/layers/layers.style\";\nusing \"ui/chat/chat.style\"; // GroupCallUserpics\nusing \"info/info.style\"; // ShortInfoCover\nusing \"window/window.style\";\nusing \"settings/settings.style\"; // defaultSubsectionTitle\n\nCallSignalBars {\n\twidth: pixels;\n\tradius: pixels;\n\tskip: pixels;\n\tmin: pixels;\n\tmax: pixels;\n\tcolor: color;\n\tinactiveOpacity: double;\n}\n\ncallWidthMin: 380px;\ncallHeightMin: 520px;\ncallWidth: 720px;\ncallHeight: 540px;\n\ncallBottomControlsHeight: 87px;\n\nCallBodyLayout {\n\theight: pixels;\n\tphotoTop: pixels;\n\tphotoSize: pixels;\n\tnameTop: pixels;\n\tstatusTop: pixels;\n\tparticipantsTop: pixels;\n\tmuteStroke: pixels;\n\tmuteSize: pixels;\n\tmutePosition: point;\n}\n\ncallBodyLayout: CallBodyLayout {\n\theight: 284px;\n\tphotoTop: 21px;\n\tphotoSize: 160px;\n\tnameTop: 221px;\n\tstatusTop: 254px;\n\tparticipantsTop: 294px;\n\tmuteStroke: 3px;\n\tmuteSize: 36px;\n\tmutePosition: point(142px, 135px);\n}\ncallBodyWithPreview: CallBodyLayout {\n\theight: 185px;\n\tphotoTop: 21px;\n\tphotoSize: 100px;\n\tnameTop: 132px;\n\tstatusTop: 163px;\n\tparticipantsTop: 193px;\n\tmuteStroke: 3px;\n\tmuteSize: 0px;\n\tmutePosition: point(90px, 84px);\n}\ncallMutedPeerIcon: icon {{ \"calls/calls_mute_userpic\", callIconFg }};\n\ncallOutgoingPreviewMin: size(360px, 120px);\ncallOutgoingPreview: size(540px, 180px); // default, for height == callHeight.\ncallOutgoingPreviewMax: size(1620px, 540px);\ncallOutgoingDefaultSize: size(160px, 110px);\n\ncallInnerPadding: 12px;\n\ncallFingerprintPadding: margins(10px, 4px, 8px, 5px);\ncallFingerprintSkip: 4px;\ncallFingerprintSignalBarsSkip: 2px;\ncallSignalBarsPadding: margins(8px, 9px, 11px, 5px);\n\ncallFingerprintTop: 8px;\ncallFingerprintBottom: -16px;\n\ncallTooltipMutedIcon: icon{{ \"calls/calls_mute_tooltip\", videoPlayIconFg }};\ncallTooltipMutedIconPosition: point(10px, 5px);\ncallTooltipPadding: margins(41px, 7px, 15px, 8px);\n\ncallButton: IconButton {\n\twidth: 68px;\n\theight: 79px;\n\n\ticonPosition: point(-1px, 16px);\n\n\trippleAreaPosition: point(12px, 12px);\n\trippleAreaSize: 44px;\n\tripple: defaultRippleAnimation;\n}\ncallButtonLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: callNameFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(11px);\n\t}\n\tmaxHeight: 16px;\n}\n\ncallAnswer: CallButton {\n\tbutton: IconButton(callButton) {\n\t\ticon: icon {{ \"calls/call_answer\", callIconFg }};\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: callAnswerRipple;\n\t\t}\n\t}\n\tbg: callAnswerBg;\n\tbgSize: 44px;\n\tbgPosition: point(12px, 12px);\n\tangle: 135.;\n\touterRadius: 12px;\n\touterBg: callAnswerBgOuter;\n\tlabel: callButtonLabel;\n}\ncallStartVideo: CallButton(callAnswer) {\n\tbutton: IconButton(callButton) {\n\t\ticon: icon {{ \"calls/call_camera_active\", callIconFg }};\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: callAnswerRipple;\n\t\t}\n\t}\n}\ncallHangup: CallButton(callAnswer) {\n\tbutton: IconButton(callButton) {\n\t\ticon: icon {{ \"calls/call_discard\", callIconFg }};\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: callHangupRipple;\n\t\t}\n\t}\n\tbg: callHangupBg;\n\touterBg: callHangupBg;\n}\ncallCancel: CallButton(callAnswer) {\n\tbutton: IconButton(callButton) {\n\t\ticon: icon {{ \"calls/call_cancel\", callIconFgActive }};\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: callIconActiveRipple;\n\t\t}\n\t}\n\tbg: callIconBgActive;\n\touterBg: callIconBgActive;\n}\ncallMicrophoneMute: CallButton(callAnswer) {\n\tbutton: IconButton(callButton) {\n\t\ticon: icon {{ \"calls/call_record_active\", callIconFg }};\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: callMuteRipple;\n\t\t}\n\t}\n\tbg: callIconBg;\n\touterBg: callMuteRipple;\n\tcornerButtonPosition: point(40px, 4px);\n\tcornerButtonBorder: 2px;\n}\ncallMicrophoneUnmute: CallButton(callMicrophoneMute) {\n\tbutton: IconButton(callButton) {\n\t\ticon: icon {{ \"calls/call_record_muted\", callIconFgActive }};\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: callIconActiveRipple;\n\t\t}\n\t}\n\tbg: callIconBgActive;\n}\ncallCameraMute: CallButton(callMicrophoneMute) {\n\tbutton: IconButton(callButton) {\n\t\ticon: icon {{ \"calls/call_camera_active\", callIconFg }};\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: callMuteRipple;\n\t\t}\n\t}\n}\ncallCameraUnmute: CallButton(callMicrophoneUnmute) {\n\tbutton: IconButton(callButton) {\n\t\ticon: icon {{ \"calls/call_camera_muted\", callIconFgActive }};\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: callIconActiveRipple;\n\t\t}\n\t}\n}\ncallAddPeople: CallButton(callAnswer) {\n\tbutton: IconButton(callButton) {\n\t\ticon: icon {{ \"calls/calls_add_people\", callIconFg }};\n\t\ticonPosition: point(-1px, 22px);\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: callMuteRipple;\n\t\t}\n\t}\n\tbg: callIconBg;\n\touterBg: callMuteRipple;\n}\ncallCornerButtonInner: IconButton {\n\twidth: 20px;\n\theight: 20px;\n\n\ticonPosition: point(-1px, -1px);\n\n\trippleAreaPosition: point(0px, 0px);\n\trippleAreaSize: 20px;\n\tripple: defaultRippleAnimation;\n}\ncallCornerButton: CallButton(callMicrophoneMute) {\n\tbutton: IconButton(callCornerButtonInner) {\n\t\ticon: icon {{ \"calls/mini_calls_arrow\", callIconFg }};\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: callMuteRipple;\n\t\t}\n\t}\n\tbgSize: 20px;\n\tbgPosition: point(0px, 0px);\n}\ncallCornerButtonInactive: CallButton(callMicrophoneUnmute, callCornerButton) {\n\tbutton: IconButton(callCornerButtonInner) {\n\t\ticon: icon {{ \"calls/mini_calls_arrow\", callIconFgActive }};\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: callIconActiveRipple;\n\t\t}\n\t}\n}\ncallScreencastOn: CallButton(callMicrophoneMute) {\n\tbutton: IconButton(callButton) {\n\t\ticon: icon {{ \"calls/calls_present\", callIconFg }};\n\t\ticonPosition: point(-1px, 22px);\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: callMuteRipple;\n\t\t}\n\t}\n}\ncallScreencastOff: CallButton(callMicrophoneUnmute) {\n\tbutton: IconButton(callButton) {\n\t\ticon: icon {{ \"calls/calls_present\", callIconFgActive }};\n\t\ticonPosition: point(-1px, 22px);\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: callIconActiveRipple;\n\t\t}\n\t}\n}\ncallBottomShadowSize: 124px;\n\nCallMuteButton {\n\tactive: CallButton;\n\tmuted: CallButton;\n\tlabelAdditional: pixels;\n\tsublabel: FlatLabel;\n\tlabelsSkip: pixels;\n\tsublabelSkip: pixels;\n\tlottieSize: size;\n\tlottieTop: pixels;\n}\n\ncallMuteButtonLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: groupCallMembersFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px);\n\t}\n}\ncallMuteButtonActiveInner: IconButton {\n//\twidth: 112px;\n//\theight: 138px;\n\twidth: 68px;\n\theight: 79px;\n}\ncallMuteButtonSmallActiveInner: IconButton {\n\twidth: 68px;\n\theight: 68px;\n}\ncallMuteButtonActive: CallButton {\n\tbutton: callMuteButtonActiveInner;\n\tbg: groupCallLive1;\n//\tbgSize: 77px;\n//\tbgPosition: point(18px, 18px);\n//\touterRadius: 18px;\n\tbgSize: 42px;\n\tbgPosition: point(13px, 13px);\n\touterRadius: 13px;\n\touterBg: callAnswerBgOuter;\n\t//label: callMuteButtonLabel;\n\tlabel: callButtonLabel;\n}\ncallMuteButton: CallMuteButton {\n\tactive: callMuteButtonActive;\n\tmuted: CallButton(callMuteButtonActive) {\n\t\tbg: groupCallMuted1;\n\t\tlabel: callMuteButtonLabel;\n\t}\n\tlabelAdditional: 5px;\n\tsublabel: FlatLabel(defaultFlatLabel) {\n\t\ttextFg: groupCallMemberNotJoinedStatus;\n\t}\n\tlabelsSkip: 0px;\n\tsublabelSkip: 0px;\n//\tlabelsSkip: 8px;\n//\tsublabelSkip: 14px;\n\tlottieSize: size(36px, 36px);\n\tlottieTop: 17px;\n//\tlottieSize: size(54px, 54px);\n//\tlottieTop: 31px;\n}\ncallMuteButtonSmallActive: CallButton(callMuteButtonActive) {\n\tbutton: callMuteButtonSmallActiveInner;\n\tbgSize: 42px;\n\tbgPosition: point(13px, 13px);\n\touterRadius: 13px;\n\tlabel: callButtonLabel;\n}\ncallMuteButtonSmall: CallMuteButton(callMuteButton) {\n\tactive: callMuteButtonSmallActive;\n\tmuted: CallButton(callMuteButtonSmallActive) {\n\t\tbg: groupCallMuted1;\n\t\tlabel: callButtonLabel;\n\t}\n\tlabelsSkip: 0px;\n\tsublabelSkip: 0px;\n\tlottieSize: size(36px, 36px);\n\tlottieTop: 17px;\n}\n\ncallMuteMinorBlobMinRadius: 64px;\ncallMuteMinorBlobMaxRadius: 74px;\ncallMuteMajorBlobMinRadius: 67px;\ncallMuteMajorBlobMaxRadius: 77px;\ncallMuteBlobRadiusForDiameter: 100px;\n\ncallMuteToFullScreen: icon {{ \"player/player_fullscreen\", groupCallIconFg }};\ncallMuteFromFullScreen: icon {{ \"player/player_minimize\", groupCallIconFg }};\n\ncallConnectingRadial: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {\n\tcolor: lightButtonFg;\n\tthickness: 4px;\n}\n\ncallName: FlatLabel(defaultFlatLabel) {\n\tminWidth: 260px;\n\tmaxHeight: 30px;\n\ttextFg: callNameFg;\n\talign: align(top);\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(21px semibold);\n\t}\n}\ncallStatus: FlatLabel(defaultFlatLabel) {\n\tminWidth: 260px;\n\tmaxHeight: 60px;\n\ttextFg: callStatusFg;\n\talign: align(top);\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px);\n\t}\n}\ncallRemoteAudioMute: FlatLabel(callStatus) {\n\tminWidth: 0px;\n\ttextFg: videoPlayIconFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px);\n\t}\n}\ncallRemoteAudioMuteSkip: 12px;\n\ncallBarHeight: 38px;\ncallBarMuteToggle: IconButton {\n\twidth: 41px;\n\theight: 38px;\n\n\ticon: icon {{ \"calls/call_record_active\", callBarFg }};\n\ticonPosition: point(3px, 2px);\n\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: callBarMuteRipple;\n\t}\n\trippleAreaPosition: point(5px, 3px);\n\trippleAreaSize: 32px;\n}\ncallBarRightSkip: 12px;\ncallBarSkip: 10px;\ncallBarHangup: IconButton(callBarMuteToggle) {\n\ticon: icon {{ \"calls/call_discard\", callBarFg }};\n\ticonPosition: point(3px, 1px);\n}\ncallBarLabel: LabelSimple(defaultLabelSimple) {\n\tfont: semiboldFont;\n\ttextFg: callBarFg;\n}\ncallBarInfoLabel: FlatLabel(defaultFlatLabel) {\n\tmaxHeight: 28px;\n\talign: align(top);\n\ttextFg: callBarFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: semiboldFont;\n\t}\n}\ncallBarLabelTop: 10px;\n\ncallArrowPosition: point(-2px, 1px);\ncallArrowIn: icon {{ \"calls/call_arrow_in\", callArrowFg }};\ncallArrowOut: icon {{ \"calls/call_arrow_out\", callArrowFg }};\ncallArrowMissed: icon {{ \"calls/call_arrow_in\", callArrowMissedFg }};\ncallArrowSkip: 4px;\ncallReDial: IconButton {\n\twidth: 40px;\n\theight: 56px;\n\n\ticon: icon {{ \"calls/call_answer\", menuIconFg }};\n\ticonOver: icon {{ \"calls/call_answer\", menuIconFgOver }};\n\ticonPosition: point(-1px, -1px);\n\n\tripple: defaultRippleAnimation;\n\trippleAreaPosition: point(0px, 8px);\n\trippleAreaSize: 40px;\n}\n\ncallCameraReDial: IconButton(callReDial) {\n\ticon: icon {{ \"calls/call_camera_active\", menuIconFg }};\n\ticonOver: icon {{ \"calls/call_camera_active\", menuIconFgOver }};\n}\ncallGroupCall: IconButton(callCameraReDial) {\n\ticon: icon {{ \"top_bar_group_call\", menuIconFg }};\n\ticonOver: icon {{ \"top_bar_group_call\", menuIconFgOver }};\n}\n\ncallRatingPadding: margins(24px, 12px, 24px, 0px);\ncallRatingStar: IconButton {\n\twidth: 36px;\n\theight: 36px;\n\n\ticon: icon {{ \"calls/call_rating\", windowSubTextFg }};\n\ticonPosition: point(-1px, -1px);\n\n\tripple: defaultRippleAnimationBgOver;\n\trippleAreaPosition: point(0px, 0px);\n\trippleAreaSize: 36px;\n}\ncallRatingStarFilled: icon {{ \"calls/call_rating_filled\", lightButtonFg }};\ncallRatingStarTop: 4px;\ncallRatingComment: InputField(defaultInputField) {\n\ttextMargins: margins(1px, 26px, 1px, 4px);\n\theightMax: 135px;\n}\ncallRatingCommentTop: 8px;\n\ncallDebugPadding: margins(24px, 0px, 24px, 0px);\ncallDebugLabel: FlatLabel(defaultFlatLabel) {\n\tmargin: callDebugPadding;\n}\ncallPanelDuration: 150;\n\ncallPanelSignalBars: CallSignalBars {\n\twidth: 2px;\n\tradius: 1px;\n\tskip: 2px;\n\tmin: 4px;\n\tmax: 10px;\n\tcolor: callNameFg;\n\tinactiveOpacity: 0.5;\n}\ncallBarSignalBars: CallSignalBars(callPanelSignalBars) {\n\twidth: 3px;\n\tskip: 1px;\n\tmin: 3px;\n\tmax: 12px;\n\tcolor: callBarFg;\n}\n\ncallTitleButton: windowTitleButton;\ncallTitleMinimizeIcon: icon {\n\t{ \"title_shadow_minimize\", windowShadowFg },\n\t{ \"title_button_minimize\", callNameFg },\n};\ncallTitleMinimizeIconOver: icon {\n\t{ windowTitleButtonSize, callBgButton },\n\t{ windowTitleButtonSize, callMuteRipple },\n\t{ \"title_shadow_minimize\", windowShadowFg },\n\t{ \"title_button_minimize\", callNameFg },\n};\ncallTitleMaximizeIcon: icon {\n\t{ \"title_shadow_maximize\", windowShadowFg },\n\t{ \"title_button_maximize\", callNameFg },\n};\ncallTitleMaximizeIconOver: icon {\n\t{ windowTitleButtonSize, callBgButton },\n\t{ windowTitleButtonSize, callMuteRipple },\n\t{ \"title_shadow_maximize\", windowShadowFg },\n\t{ \"title_button_maximize\", callNameFg },\n};\ncallTitleRestoreIcon: icon {\n\t{ \"title_shadow_restore\", windowShadowFg },\n\t{ \"title_button_restore\", callNameFg },\n};\ncallTitleRestoreIconOver: icon {\n\t{ windowTitleButtonSize, callBgButton },\n\t{ windowTitleButtonSize, callMuteRipple },\n\t{ \"title_shadow_restore\", windowShadowFg },\n\t{ \"title_button_restore\", callNameFg },\n};\ncallTitleCloseIcon: icon {\n\t{ \"title_shadow_close\", windowShadowFg },\n\t{ \"title_button_close\", callNameFg },\n};\ncallTitleCloseIconOver: icon {\n\t{ windowTitleButtonSize, titleButtonCloseBgOver },\n\t{ \"title_shadow_close\", windowShadowFg },\n\t{ \"title_button_close\", titleButtonCloseFgOver },\n};\ncallTitle: WindowTitle(defaultWindowTitle) {\n\theight: 0px;\n\tbg: callBgOpaque;\n\tbgActive: callBgOpaque;\n\tfg: transparent;\n\tfgActive: transparent;\n\tminimize: IconButton(callTitleButton) {\n\t\ticon: callTitleMinimizeIcon;\n\t\ticonOver: callTitleMinimizeIconOver;\n\t}\n\tminimizeIconActive: callTitleMinimizeIcon;\n\tminimizeIconActiveOver: callTitleMinimizeIconOver;\n\tmaximize: IconButton(callTitleButton) {\n\t\ticon: callTitleMaximizeIcon;\n\t\ticonOver: callTitleMaximizeIconOver;\n\t}\n\tmaximizeIconActive: callTitleMaximizeIcon;\n\tmaximizeIconActiveOver: callTitleMaximizeIconOver;\n\trestoreIcon: callTitleRestoreIcon;\n\trestoreIconOver: callTitleRestoreIconOver;\n\trestoreIconActive: callTitleRestoreIcon;\n\trestoreIconActiveOver: callTitleRestoreIconOver;\n\tclose: IconButton(callTitleButton) {\n\t\ticon: callTitleCloseIcon;\n\t\ticonOver: callTitleCloseIconOver;\n\t}\n\tcloseIconActive: callTitleCloseIcon;\n\tcloseIconActiveOver: callTitleCloseIconOver;\n\n\t// Dummy values.\n\ttop: IconButton(callTitleButton) {\n\t}\n\ttopIconActive: callTitleMinimizeIcon;\n\ttopIconActiveOver: callTitleMinimizeIcon;\n\ttop2Icon: callTitleMinimizeIcon;\n\ttop2IconOver: callTitleMinimizeIcon;\n\ttop2IconActive: callTitleMinimizeIcon;\n\ttop2IconActiveOver: callTitleMinimizeIcon;\n}\ncallTitleShadowRight: icon {{ \"calls/calls_shadow_controls\", windowShadowFg }};\ncallTitleShadowLeft: icon {{ \"calls/calls_shadow_controls-flip_horizontal\", windowShadowFg }};\n\ncallErrorToast: Toast(defaultToast) {\n\tminWidth: 240px;\n}\n\ngroupCallWidth: 380px;\ngroupCallHeight: 520px;\ngroupCallWidthRtmp: 720px;\ngroupCallWidthRtmpMin: 240px;\ngroupCallHeightRtmp: 580px;\ngroupCallHeightRtmpMin: 160px;\n\ngroupCallRipple: RippleAnimation(defaultRippleAnimation) {\n\tcolor: groupCallMembersBgRipple;\n}\n\ngroupCallMenu: Menu(defaultMenu) {\n\titemBg: groupCallMenuBg;\n\titemBgOver: groupCallMenuBgOver;\n\titemFg: groupCallMembersFg;\n\titemFgOver: groupCallMembersFg;\n\titemFgDisabled: groupCallMemberNotJoinedStatus;\n\titemFgShortcut: groupCallMemberNotJoinedStatus;\n\titemFgShortcutOver: groupCallMemberNotJoinedStatus;\n\titemFgShortcutDisabled: groupCallMemberNotJoinedStatus;\n\n\tseparator: MenuSeparator(defaultMenuSeparator) {\n\t\tpadding: margins(0px, 4px, 0px, 4px);\n\t\tfg: groupCallMenuBgOver;\n\t}\n\tarrow: icon {{ \"menu/submenu_arrow\", groupCallMemberNotJoinedStatus }};\n\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: groupCallMenuBgRipple;\n\t}\n}\ngroupCallFinishMenu: Menu(groupCallMenu) {\n\titemFg: groupCallMemberMutedIcon;\n\titemFgOver: groupCallMemberMutedIcon;\n}\ngroupCallMenuShadow: Shadow(defaultEmptyShadow) {\n\tfallback: groupCallMenuBg;\n}\ngroupCallPanelAnimation: PanelAnimation(defaultPanelAnimation) {\n\tfadeBg: groupCallMenuBg;\n}\ngroupCallPopupMenu: PopupMenu(defaultPopupMenu) {\n\tshadowFallback: groupCallMenuBg;\n\tmenu: groupCallMenu;\n\tanimation: groupCallPanelAnimation;\n}\ngroupCallPopupMenuWithIcons: PopupMenu(popupMenuWithIcons) {\n\tshadowFallback: groupCallMenuBg;\n\tmenu: Menu(groupCallMenu, menuWithIcons) {\n\t\tarrow: icon {{ \"menu/submenu_arrow\", groupCallMemberNotJoinedStatus }};\n\t}\n\tanimation: groupCallPanelAnimation;\n}\ngroupCallPopupMenuWithVolume: PopupMenu(groupCallPopupMenu) {\n\tscrollPadding: margins(0px, 3px, 0px, 8px);\n\tmenu: Menu(groupCallMenu) {\n\t\twidthMin: 210px;\n\t}\n}\ngroupCallPopupVolumeMenu: Menu(groupCallMenu) {\n\twidthMin: 210px;\n\titemBgOver: groupCallMenuBg;\n}\ngroupCallMenuCoverSize: 240px;\ngroupCallPopupCoverMenu: Menu(groupCallMenu) {\n\twidthMin: groupCallMenuCoverSize;\n\twidthMax: groupCallMenuCoverSize;\n\titemBgOver: groupCallMenuBg;\n}\ngroupCallPopupMenuWithCover: PopupMenu(groupCallPopupMenu) {\n\tscrollPadding: margins(0px, 0px, 0px, 8px);\n\tmenu: Menu(groupCallMenu) {\n\t\twidthMin: groupCallMenuCoverSize;\n\t\twidthMax: groupCallMenuCoverSize;\n\t}\n}\ngroupCallMenuCover: ShortInfoCover(shortInfoCover) {\n\tradius: roundRadiusSmall;\n\tsize: groupCallMenuCoverSize;\n\tnamePosition: point(17px, 28px);\n\tstatusPosition: point(17px, 8px);\n}\ngroupCallTextPalette: TextPalette(defaultTextPalette) {\n\tlinkFg: groupCallActiveFg;\n}\ngroupCallMenuAbout: FlatLabel(defaultFlatLabel) {\n\ttextFg: groupCallMemberNotJoinedStatus;\n\tpalette: groupCallTextPalette;\n\tminWidth: 200px;\n\tmaxHeight: 92px;\n}\ncallDeviceSelectionLabel: FlatLabel(defaultSubsectionTitle) {\n\ttextFg: groupCallActiveFg;\n\tminWidth: 200px;\n\tmaxHeight: 20px;\n}\ncallDeviceSelectionMenu: PopupMenu(groupCallPopupMenu) {\n\tscrollPadding: margins(0px, 3px, 0px, 8px);\n\tmenu: Menu(groupCallMenu) {\n\t\twidthMin: 240px;\n\t\titemPadding: margins(17px, 8px, 17px, 7px);\n\t}\n}\n\ncreateCallListButton: OutlineButton(defaultPeerListButton) {\n\tfont: normalFont;\n\tpadding: margins(11px, 5px, 11px, 5px);\n}\ncreateCallListItem: PeerListItem(defaultPeerListItem) {\n\tbutton: createCallListButton;\n\theight: 52px;\n\tphotoPosition: point(12px, 6px);\n\tnamePosition: point(63px, 7px);\n\tstatusPosition: point(63px, 26px);\n\tphotoSize: 40px;\n}\ncreateCallList: PeerList(defaultPeerList) {\n\titem: createCallListItem;\n\tpadding: margins(0px, 6px, 0px, 6px);\n}\n\ngroupCallRecordingTimerPadding: margins(0px, 4px, 0px, 4px);\ngroupCallRecordingTimerFont: font(12px);\n\ngroupCallInnerDropdown: InnerDropdown(defaultInnerDropdown) {\n\tshadow: groupCallMenuShadow;\n\tanimation: groupCallPanelAnimation;\n\tbg: groupCallMenuBg;\n\tscroll: defaultSolidScroll;\n\tscrollPadding: margins(0px, 7px, 0px, 7px);\n}\ngroupCallDropdownMenu: DropdownMenu(defaultDropdownMenu) {\n\twrap: groupCallInnerDropdown;\n\tmenu: groupCallMenu;\n}\ngroupCallMembersListCheck: RoundCheckbox(defaultPeerListCheck) {\n\tborder: groupCallMembersBg;\n\tbgActive: groupCallActiveFg;\n\tcheck: icon {{ \"default_checkbox_check\", groupCallMembersFg, point(3px, 6px) }};\n}\ngroupCallMembersListCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) {\n\tselectFg: groupCallActiveFg;\n\tcheck: groupCallMembersListCheck;\n}\ngroupCallMembersListItem: PeerListItem(createCallListItem) {\n\tbutton: OutlineButton(createCallListButton) {\n\t\ttextBg: groupCallMembersBg;\n\t\ttextBgOver: groupCallMembersBgOver;\n\n\t\ttextFg: groupCallMemberInactiveStatus;\n\t\ttextFgOver: groupCallMemberInactiveStatus;\n\n\t\tripple: groupCallRipple;\n\t}\n\tdisabledCheckFg: groupCallMemberNotJoinedStatus;\n\tcheckbox: groupCallMembersListCheckbox;\n\tnameFg: groupCallMembersFg;\n\tnameFgChecked: groupCallMembersFg;\n\tstatusFg: groupCallMemberInactiveStatus;\n\tstatusFgOver: groupCallMemberInactiveStatus;\n\tstatusFgActive: groupCallMemberActiveStatus;\n}\ngroupCallNarrowMembersListItem: PeerListItem(groupCallMembersListItem) {\n\tstatusFg: groupCallMemberNotJoinedStatus;\n\tstatusFgOver: groupCallMemberNotJoinedStatus;\n\tstatusFgActive: groupCallMemberActiveStatus;\n}\ngroupCallMembersList: PeerList(defaultPeerList) {\n\tbg: groupCallMembersBg;\n\tabout: FlatLabel(defaultPeerListAbout) {\n\t\ttextFg: groupCallMemberNotJoinedStatus;\n\t}\n\titem: groupCallMembersListItem;\n}\ngroupCallInviteDividerPadding: margins(17px, 7px, 17px, 7px);\ngroupCallInviteMembersListItem: PeerListItem(groupCallMembersListItem) {\n\tstatusFg: groupCallMemberNotJoinedStatus;\n\tstatusFgOver: groupCallMemberNotJoinedStatus;\n\tstatusFgActive: groupCallMemberInactiveStatus;\n}\ngroupCallInviteMembersList: PeerList(groupCallMembersList) {\n\tpadding: margins(0px, 10px, 0px, 10px);\n\titem: groupCallInviteMembersListItem;\n}\ngroupCallJoinAsList: PeerList(groupCallInviteMembersList) {\n\titem: PeerListItem(groupCallInviteMembersListItem) {\n\t\theight: 56px;\n\t\tcheckbox: RoundImageCheckbox(groupCallMembersListCheckbox) {\n\t\t\tcheck: RoundCheckbox(groupCallMembersListCheck) {\n\t\t\t\tsize: 0px;\n\t\t\t}\n\t\t\timageRadius: 19px;\n\t\t\timageSmallRadius: 15px;\n\t\t\tselectFg: groupCallMemberInactiveStatus;\n\t\t}\n\t\tphotoSize: 38px;\n\t\tphotoPosition: point(24px, 9px);\n\t\tnamePosition: point(73px, 9px);\n\t\tstatusPosition: point(73px, 28px);\n\t}\n}\ngroupCallMultiSelect: MultiSelect(defaultMultiSelect) {\n\tbg: groupCallMembersBg;\n\titem: MultiSelectItem(defaultMultiSelectItem) {\n\t\ttextBg: groupCallMembersBgRipple;\n\t\ttextFg: groupCallMembersFg;\n\t\ttextActiveBg: groupCallActiveFg;\n\t\ttextActiveFg: groupCallMembersFg;\n\t\tdeleteFg: groupCallMembersFg;\n\t}\n\tfield: InputField(defaultMultiSelectSearchField) {\n\t\ttextFg: groupCallMembersFg;\n\t\tplaceholderFg: groupCallMemberNotJoinedStatus;\n\t\tplaceholderFgActive: groupCallMemberNotJoinedStatus;\n\t\tplaceholderFgError: groupCallMemberNotJoinedStatus;\n\t\tmenu: groupCallPopupMenu;\n\t}\n\tfieldIcon: icon {{ \"box_search\", groupCallMemberNotJoinedStatus, point(10px, 9px) }};\n\tfieldCancel: CrossButton(defaultMultiSelectSearchCancel) {\n\t\tcrossFg: groupCallMemberNotJoinedStatus;\n\t\tcrossFgOver: groupCallMemberNotJoinedStatus;\n\t\tripple: groupCallRipple;\n\t}\n}\ngroupCallField: InputField(defaultInputField) {\n\ttextMargins: margins(2px, 7px, 2px, 0px);\n\n\ttextBg: transparent;\n\ttextFg: groupCallMembersFg;\n\n\tplaceholderFg: groupCallMemberNotJoinedStatus;\n\tplaceholderFgActive: groupCallMemberNotJoinedStatus;\n\tplaceholderFgError: groupCallMemberNotJoinedStatus;\n\tplaceholderMargins: margins(0px, 0px, 0px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n\theightMin: 32px;\n\n\tborderFg: inputBorderFg;\n\tborderFgActive: groupCallMemberInactiveStatus;\n\tborderFgError: activeLineFgError;\n\n\tmenu: groupCallPopupMenu;\n}\ngroupCallShareBoxComment: InputField(groupCallField) {\n\ttextMargins: margins(8px, 8px, 8px, 6px);\n\theightMin: 36px;\n\theightMax: 72px;\n\tplaceholderMargins: margins(2px, 0px, 2px, 0px);\n\tborder: 0px;\n\tborderActive: 0px;\n}\ngroupCallShareBoxList: PeerList(groupCallMembersList) {\n\titem: PeerListItem(groupCallMembersListItem) {\n\t\tnameStyle: TextStyle(defaultTextStyle) {\n\t\t\tfont: font(11px);\n\t\t}\n\t\tcheckbox: RoundImageCheckbox(groupCallMembersListCheckbox) {\n\t\t\timageRadius: 28px;\n\t\t\timageSmallRadius: 24px;\n\t\t}\n\t}\n}\n\ngroupCallMembersTop: 51px;\ngroupCallTitleTop: 8px;\ngroupCallSubtitleTop: 26px;\ngroupCallWideVideoTop: 24px;\n\ngroupCallAddMember: SettingsButton(defaultSettingsButton) {\n\ttextFg: groupCallMemberNotJoinedStatus;\n\ttextFgOver: groupCallMemberNotJoinedStatus;\n\ttextBg: groupCallMembersBg;\n\ttextBgOver: groupCallMembersBgOver;\n\n\tstyle: semiboldTextStyle;\n\n\theight: 22px;\n\tpadding: margins(63px, 17px, 22px, 11px);\n\ticonLeft: 16px;\n\n\tripple: groupCallRipple;\n}\ngroupCallAddMemberIcon: icon {{ \"info/info_add_member\", groupCallMemberInactiveIcon, point(0px, 3px) }};\ngroupCallShareLinkIcon: icon {{ \"menu/links_profile\", groupCallMemberInactiveIcon, point(4px, 3px) }};\ngroupCallSubtitleLabel: FlatLabel(defaultFlatLabel) {\n\tmaxHeight: 18px;\n\ttextFg: groupCallMemberNotJoinedStatus;\n}\ngroupCallTitleLabel: FlatLabel(groupCallSubtitleLabel) {\n\ttextFg: groupCallMembersFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(semibold 14px);\n\t}\n}\ngroupCallTitleSeparator: 4px;\ngroupCallVideoLimitLabel: FlatLabel(defaultFlatLabel) {\n\talign: align(top);\n\ttextFg: groupCallMemberNotJoinedStatus;\n\tstyle: semiboldTextStyle;\n\tminWidth: 96px;\n}\ngroupCallMembersWidthMax: 480px;\ngroupCallRecordingMark: 6px;\ngroupCallRecordingMarkSkip: 4px;\ngroupCallRecordingMarkTop: 8px;\n\ngroupCallMenuTogglePosition: point(13px, 8px);\ngroupCallMenuToggle: IconButton {\n\twidth: 36px;\n\theight: 36px;\n\n\ticon: icon {{ \"info/edit/dotsmini\", groupCallMemberInactiveIcon }};\n\ticonOver: icon {{ \"info/edit/dotsmini\", groupCallMemberInactiveIcon }};\n\ticonPosition: point(2px, 2px);\n\n\trippleAreaPosition: point(3px, 3px);\n\trippleAreaSize: 30px;\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: groupCallMembersBg;\n\t}\n}\ngroupCallJoinAsToggle: UserpicButton(defaultUserpicButton) {\n\tsize: size(36px, 36px);\n\tphotoSize: 30px;\n\tphotoPosition: point(3px, 3px);\n}\ngroupCallMenuPosition: point(-1px, 29px);\ngroupCallWideMenuPosition: point(-2px, 28px);\n\ngroupCallActiveButton: IconButton {\n\twidth: 36px;\n\theight: 52px;\n\n\ticon: icon {{ \"calls/group_calls_unmuted\", groupCallMemberInactiveIcon }};\n\ticonOver: icon {{ \"calls/group_calls_unmuted\", groupCallMemberInactiveIcon }};\n\ticonPosition: point(-1px, -1px);\n\n\tripple: groupCallRipple;\n\trippleAreaPosition: point(0px, 8px);\n\trippleAreaSize: 36px;\n}\ngroupCallMemberButtonSkip: 10px;\n\ngroupCallMemberInactiveCrossLine: CrossLineAnimation {\n\tfg: groupCallMemberInactiveIcon;\n\ticon: icon {{ \"calls/group_calls_unmuted\", groupCallMemberInactiveIcon }};\n\tstartPosition: point(5px, 2px);\n\tendPosition: point(20px, 17px);\n\tstroke: 2px;\n}\ngroupCallMemberColoredCrossLine: CrossLineAnimation(groupCallMemberInactiveCrossLine) {\n\tfg: groupCallMemberMutedIcon;\n\ticon: icon {{ \"calls/group_calls_unmuted\", groupCallMemberActiveIcon }};\n}\ngroupCallMemberCalling: icon {{ \"calls/call_answer\", groupCallMemberInactiveIcon }};\ngroupCallMemberCallingPosition: point(0px, 8px);\ngroupCallMemberInvited: icon {{ \"calls/group_calls_invited\", groupCallMemberInactiveIcon }};\ngroupCallMemberInvitedPosition: point(2px, 12px);\ngroupCallMemberRaisedHand: icon {{ \"calls/group_calls_raised_hand\", groupCallMemberInactiveStatus }};\n\ngroupCallSettingsInner: IconButton(callButton) {\n\ticonPosition: point(-1px, 22px);\n\ticon: icon {{ \"calls/calls_settings\", groupCallIconFg }};\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: callMuteRipple;\n\t}\n}\ngroupCallShareInner: IconButton(groupCallSettingsInner) {\n\ticon: icon {{ \"calls/group_calls_share\", groupCallIconFg }};\n}\ngroupCallVideoInner: IconButton(groupCallSettingsInner) {\n\ticon: icon {{ \"calls/call_camera_muted\", groupCallIconFg }};\n\ticonPosition: point(-1px, 16px);\n}\ngroupCallHangupInner: IconButton(callButton) {\n\ticon: icon {{ \"calls/call_discard\", groupCallIconFg }};\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: groupCallLeaveBgRipple;\n\t}\n}\ngroupCallMessageInner: IconButton(groupCallSettingsInner) {\n\ticon: icon {{ \"calls/call_message\", groupCallIconFg }};\n\ticonPosition: point(-1px, 16px);\n}\ngroupCallSettings: CallButton(callMicrophoneMute) {\n\tbutton: groupCallSettingsInner;\n}\ngroupCallShare: CallButton(groupCallSettings) {\n\tbutton: groupCallShareInner;\n}\ngroupCallVideo: CallButton(groupCallSettings) {\n\tbutton: groupCallVideoInner;\n}\ngroupCallVideoInnerActive: IconButton(groupCallVideoInner) {\n\ticon: icon {{ \"calls/call_camera_active\", groupCallIconFg }};\n}\ngroupCallVideoActive: CallButton(groupCallVideo) {\n\tbutton: groupCallVideoInnerActive;\n}\ngroupCallMessage: CallButton(groupCallSettings) {\n\tbutton: groupCallMessageInner;\n}\ngroupCallMessageInnerActive: IconButton(groupCallMessageInner) {\n\ticon: icon {{ \"calls/call_message\", groupCallIconFg }};\n}\ngroupCallMessageActive: CallButton(groupCallMessage) {\n\tbutton: groupCallMessageInnerActive;\n}\ngroupCallHangup: CallButton(callHangup) {\n\tbutton: groupCallHangupInner;\n\tbg: groupCallLeaveBg;\n\touterBg: groupCallLeaveBg;\n}\ngroupCallSettingsSmall: CallButton(groupCallSettings) {\n\tbutton: IconButton(groupCallSettingsInner) {\n\t\twidth: 60px;\n\t\theight: 68px;\n\t\trippleAreaPosition: point(8px, 12px);\n\t}\n\tbgPosition: point(8px, 12px);\n}\ngroupCallHangupSmall: CallButton(groupCallHangup) {\n\tbutton: IconButton(groupCallHangupInner) {\n\t\twidth: 60px;\n\t\theight: 68px;\n\t\trippleAreaPosition: point(8px, 12px);\n\t}\n\tbgPosition: point(8px, 12px);\n}\ngroupCallVideoSmall: CallButton(groupCallSettingsSmall) {\n\tbutton: IconButton(groupCallVideoInner) {\n\t\twidth: 60px;\n\t\theight: 68px;\n\t\trippleAreaPosition: point(8px, 12px);\n\t}\n}\ngroupCallVideoActiveSmall: CallButton(groupCallVideoSmall) {\n\tbutton: IconButton(groupCallVideoInnerActive) {\n\t\twidth: 60px;\n\t\theight: 68px;\n\t\trippleAreaPosition: point(8px, 12px);\n\t}\n}\ngroupCallMessageSmall: CallButton(groupCallSettingsSmall) {\n\tbutton: IconButton(groupCallMessageInner) {\n\t\twidth: 60px;\n\t\theight: 68px;\n\t\trippleAreaPosition: point(8px, 12px);\n\t}\n}\ngroupCallMessageActiveSmall: CallButton(groupCallMessageSmall) {\n\tbutton: IconButton(groupCallMessageInnerActive) {\n\t\twidth: 60px;\n\t\theight: 68px;\n\t\trippleAreaPosition: point(8px, 12px);\n\t}\n}\ngroupCallScreenShareSmall: CallButton(groupCallSettingsSmall) {\n\tbutton: IconButton(groupCallSettingsInner) {\n\t\ticon: icon {{ \"calls/calls_present\", groupCallIconFg }};\n\t\twidth: 60px;\n\t\theight: 68px;\n\t\trippleAreaPosition: point(8px, 12px);\n\t}\n}\ngroupCallMenuToggleSmall: CallButton(groupCallSettingsSmall) {\n\tbutton: IconButton(groupCallSettingsInner) {\n\t\ticon: icon {{ \"calls/calls_more\", groupCallIconFg }};\n\t\twidth: 60px;\n\t\theight: 68px;\n\t\trippleAreaPosition: point(8px, 12px);\n\t}\n}\ngroupCallButtonSkip: 40px;\ngroupCallButtonSkipSmall: 5px;\ngroupCallButtonBottomSkip: 113px;\ngroupCallButtonBottomSkipWide: 108px;\ngroupCallControlsBackMargin: margins(10px, 0px, 10px, 0px);\ngroupCallControlsBackRadius: 12px;\ngroupCallMuteBottomSkip: 116px;\n\ngroupCallMembersMargin: margins(16px, 16px, 16px, 60px);\ngroupCallMembersTopSkip: 6px;\ngroupCallMembersBottomSkip: 80px;\ngroupCallMembersShadowHeight: 160px;\ngroupCallMembersFadeSkip: 10px;\ngroupCallMembersFadeHeight: 100px;\n\ngroupCallTopBarUserpics: GroupCallUserpics {\n\tsize: 28px;\n\tshift: 8px;\n\tstroke: 2px;\n\talign: align(left);\n}\ngroupCallTopBarJoin: RoundButton(defaultActiveButton) {\n\twidth: -26px;\n\theight: 26px;\n\ttextTop: 4px;\n}\ngroupCallTopBarOpen: RoundButton(groupCallTopBarJoin) {\n\tripple: universalRippleAnimation;\n}\ngroupCallBoxButton: RoundButton(defaultBoxButton) {\n\ttextFg: groupCallActiveFg;\n\ttextFgOver: groupCallActiveFg;\n\tnumbersTextFg: groupCallActiveFg;\n\tnumbersTextFgOver: groupCallActiveFg;\n\ttextBg: groupCallMembersBg;\n\ttextBgOver: groupCallMembersBgOver;\n\n\tripple: groupCallRipple;\n}\ngroupCallBox: Box(defaultBox) {\n\tbutton: groupCallBoxButton;\n\tmargin: margins(0px, 56px, 0px, 10px);\n\tbg: groupCallMembersBg;\n\ttitle: FlatLabel(boxTitle) {\n\t\ttextFg: groupCallMembersFg;\n\t}\n\ttitleAdditionalFg: groupCallMemberNotJoinedStatus;\n}\ngroupCallLayerBox: Box(groupCallBox) {\n\tbuttonPadding: margins(8px, 8px, 8px, 8px);\n}\ngroupCallDividerBar: DividerBar {\n\tbg: groupCallBg;\n\ttop: icon {{ \"box_divider_top\", groupCallMemberNotJoinedStatus }};\n\tbottom: icon {{ \"box_divider_bottom\", groupCallMemberNotJoinedStatus }};\n}\ngroupCallDividerLabel: DividerLabel {\n\tbar: groupCallDividerBar;\n\tlabel: FlatLabel(boxDividerLabel) {\n\t\ttextFg: groupCallVideoSubTextFg;\n\t\tpalette: groupCallTextPalette;\n\t}\n}\n\ngroupCallLevelMeter: LevelMeter(defaultLevelMeter) {\n\theight: 18px;\n\tlineWidth: 3px;\n\tlineSpacing: 5px;\n\tlineCount: 44;\n\tactiveFg: groupCallActiveFg;\n\tinactiveFg: groupCallMembersBgRipple;\n}\ngroupCallCheckboxIcon: icon {{ \"default_checkbox_check\", groupCallMembersFg, point(4px, 7px) }};\ngroupCallCheck: Check(defaultCheck) {\n\tuntoggledFg: groupCallMemberNotJoinedStatus;\n\ttoggledFg: groupCallActiveFg;\n\ticon: groupCallCheckboxIcon;\n}\ngroupCallRadio: Radio(defaultRadio) {\n\tuntoggledFg: groupCallMemberNotJoinedStatus;\n\ttoggledFg: groupCallActiveFg;\n}\ngroupCallCheckbox: Checkbox(defaultBoxCheckbox) {\n\ttextFg: groupCallMembersFg;\n\ttextFgActive: groupCallMembersFg;\n\trippleBg: groupCallMembersBgRipple;\n\trippleBgActive: groupCallMembersBgRipple;\n}\n\ngroupCallSettingsToggle: Toggle(defaultToggle) {\n\ttoggledBg: groupCallMembersBg;\n\ttoggledFg: groupCallActiveFg;\n\tuntoggledBg: groupCallMembersBg;\n\tuntoggledFg: groupCallMemberNotJoinedStatus;\n}\ngroupCallSettingsButton: SettingsButton(defaultSettingsButton) {\n\tpadding: margins(24px, 10px, 24px, 8px);\n\ttextFg: groupCallMembersFg;\n\ttextFgOver: groupCallMembersFg;\n\ttextBg: groupCallMembersBg;\n\ttextBgOver: groupCallMembersBgOver;\n\trightLabel: FlatLabel(defaultSettingsRightLabel) {\n\t\ttextFg: groupCallActiveFg;\n\t}\n\ttoggle: groupCallSettingsToggle;\n\ttoggleOver: groupCallSettingsToggle;\n\tripple: groupCallRipple;\n}\ngroupCallSettingsAttentionButton: SettingsButton(groupCallSettingsButton) {\n\ttextFg: attentionButtonFg;\n\ttextFgOver: attentionButtonFgOver;\n}\ngroupCallBoxLabel: FlatLabel(boxLabel) {\n\tminWidth: 200px;\n\ttextFg: groupCallMembersFg;\n}\ngroupCallJoinAsLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 272px;\n\ttextFg: groupCallMembersFg;\n}\ngroupCallJoinAsWidth: 330px;\ngroupCallJoinAsTextTop: 4px;\ngroupCallJoinAsNameTop: 23px;\ngroupCallJoinAsPadding: margins(12px, 8px, 12px, 7px);\ngroupCallJoinAsPhotoSize: 30px;\n\ngroupCallRowBlobMinRadius: 27px;\ngroupCallRowBlobMaxRadius: 29px;\n\ngroupCallDelayLabel: LabelSimple(defaultLabelSimple) {\n\ttextFg: groupCallMembersFg;\n\tfont: boxTextFont;\n}\ngroupCallDelayLabelMargin: margins(22px, 10px, 20px, 5px);\ngroupCallDelaySlider: MediaSlider(defaultContinuousSlider) {\n\tseekSize: size(15px, 15px);\n\tactiveFg: groupCallActiveFg;\n\tinactiveFg: groupCallMemberNotJoinedStatus;\n\tactiveFgOver: groupCallActiveFg;\n\tinactiveFgOver: groupCallMemberNotJoinedStatus;\n\tactiveFgDisabled: groupCallActiveFg;\n\tinactiveFgDisabled: groupCallMemberNotJoinedStatus;\n\treceivedTillFg: groupCallMemberNotJoinedStatus;\n}\ngroupCallDelayMargin: margins(22px, 5px, 20px, 10px);\n\ngroupCallTitleButton: IconButton {\n\twidth: windowTitleButtonWidth;\n\theight: windowTitleHeight;\n\ticonPosition: point(0px, 0px);\n}\ngroupCallTitleMinimizeIcon: icon {\n\t{ \"title_button_minimize\", groupCallMemberNotJoinedStatus },\n};\ngroupCallTitleMinimizeIconOver: icon {\n\t{ windowTitleButtonSize, groupCallMembersBgOver },\n\t{ \"title_button_minimize\", groupCallMembersFg },\n};\ngroupCallTitleMaximizeIcon: icon {\n\t{ \"title_button_maximize\", groupCallMemberNotJoinedStatus },\n};\ngroupCallTitleMaximizeIconOver: icon {\n\t{ windowTitleButtonSize, groupCallMembersBgOver },\n\t{ \"title_button_maximize\", groupCallMembersFg },\n};\ngroupCallTitleRestoreIcon: icon {\n\t{ \"title_button_restore\", groupCallMemberNotJoinedStatus },\n};\ngroupCallTitleRestoreIconOver: icon {\n\t{ windowTitleButtonSize, groupCallMembersBgOver },\n\t{ \"title_button_restore\", groupCallMembersFg },\n};\ngroupCallTitleCloseIcon: icon {\n\t{ \"title_button_close\", groupCallMemberNotJoinedStatus },\n};\ngroupCallTitleCloseIconOver: icon {\n\t{ windowTitleButtonSize, titleButtonCloseBgOver },\n\t{ \"title_button_close\", titleButtonCloseFgOver },\n};\ngroupCallTitle: WindowTitle(defaultWindowTitle) {\n\theight: 0px;\n\tbg: groupCallBg;\n\tbgActive: groupCallBg;\n\tfg: transparent;\n\tfgActive: transparent;\n\toneSideControls: true;\n\tminimize: IconButton(groupCallTitleButton) {\n\t\ticon: groupCallTitleMinimizeIcon;\n\t\ticonOver: groupCallTitleMinimizeIconOver;\n\t}\n\tminimizeIconActive: groupCallTitleMinimizeIcon;\n\tminimizeIconActiveOver: groupCallTitleMinimizeIconOver;\n\tmaximize: IconButton(groupCallTitleButton) {\n\t\ticon: groupCallTitleMaximizeIcon;\n\t\ticonOver: groupCallTitleMaximizeIconOver;\n\t}\n\tmaximizeIconActive: groupCallTitleMaximizeIcon;\n\tmaximizeIconActiveOver: groupCallTitleMaximizeIconOver;\n\trestoreIcon: groupCallTitleRestoreIcon;\n\trestoreIconOver: groupCallTitleRestoreIconOver;\n\trestoreIconActive: groupCallTitleRestoreIcon;\n\trestoreIconActiveOver: groupCallTitleRestoreIconOver;\n\tclose: IconButton(groupCallTitleButton) {\n\t\ticon: groupCallTitleCloseIcon;\n\t\ticonOver: groupCallTitleCloseIconOver;\n\t}\n\tcloseIconActive: groupCallTitleCloseIcon;\n\tcloseIconActiveOver: groupCallTitleCloseIconOver;\n\n\t// Dummy values.\n\ttop: IconButton(callTitleButton) {\n\t}\n\ttopIconActive: callTitleMinimizeIcon;\n\ttopIconActiveOver: callTitleMinimizeIcon;\n\ttop2Icon: callTitleMinimizeIcon;\n\ttop2IconOver: callTitleMinimizeIcon;\n\ttop2IconActive: callTitleMinimizeIcon;\n\ttop2IconActiveOver: callTitleMinimizeIcon;\n}\ngroupCallPinOnTop: IconButton(groupCallTitleButton) {\n\ticon: icon{{ \"calls/navbar_pin\", groupCallMemberNotJoinedStatus }};\n\ticonOver: icon{{ \"calls/navbar_pin\", groupCallMembersFg }};\n\ticonPosition: point(0px, 2px);\n}\ngroupCallPinnedOnTop: icon{{ \"calls/navbar_pin_filled\", groupCallMembersFg }};\n\ngroupCallMajorBlobMaxRadius: 4px;\n\ngroupCallMinorBlobIdleRadius: 3px;\ngroupCallMinorBlobMaxRadius: 12px;\n\ngroupCallMuteCrossLine: CrossLineAnimation {\n\tfg: groupCallIconFg;\n\ticon: icon {{ \"calls/volume/speaker\", groupCallIconFg }};\n\tstartPosition: point(2px, 5px);\n\tendPosition: point(16px, 19px);\n\tstroke: 2px;\n}\n\ngroupCallMenuSpeakerArcsSkip: 1px;\ngroupCallMenuVolumePadding: margins(17px, 6px, 17px, 5px);\ngroupCallMenuVolumeMargin: margins(55px, 0px, 15px, 0px);\ngroupCallMenuVolumeSlider: MediaSlider(defaultContinuousSlider) {\n\tactiveFg: groupCallMembersFg;\n\tinactiveFg: groupCallMembersBgOver;\n\tactiveFgOver: groupCallMembersFg;\n\tinactiveFgOver: groupCallMembersBgOver;\n\tactiveFgDisabled: groupCallMembersBgOver;\n\treceivedTillFg: groupCallMembersBgOver;\n\twidth: 7px;\n\tseekSize: size(7px, 7px);\n}\n\ngroupCallSpeakerArcsAnimation: ArcsAnimation {\n\tfg: groupCallIconFg;\n\tstroke: 2px;\n\tspace: 4px;\n\tduration: 200;\n\tdeltaAngle: 60;\n\tdeltaHeight: 6px;\n\tdeltaWidth: 7px;\n\tstartHeight: 3px;\n\tstartWidth: 0px;\n}\n\ngroupCallStatusSpeakerIcon: icon {{ \"calls/volume/speaker_small\", groupCallIconFg }};\ngroupCallStatusSpeakerArcsSkip: 3px;\ngroupCallStatusSpeakerArcsAnimation: ArcsAnimation(groupCallSpeakerArcsAnimation) {\n\tdeltaAngle: 68;\n\tspace: 3px;\n\tdeltaHeight: 5px;\n\tdeltaWidth: 4px;\n\tstartHeight: 1px;\n}\n\ngroupCallShareMutedMargin: margins(16px, 16px, 16px, 8px);\n\ncallTopBarMuteCrossLine: CrossLineAnimation {\n\tfg: callBarFg;\n\ticon: icon {{ \"calls/call_record_active\", callBarFg }};\n\tstartPosition: point(11px, 8px);\n\tendPosition: point(26px, 23px);\n\tstroke: 2px;\n}\n\ngroupCallStartsIn: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(20px semibold);\n\t}\n\ttextFg: groupCallMembersFg;\n}\ngroupCallScheduledBodyHeight: 200px;\ngroupCallStartsWhen: groupCallStartsIn;\ngroupCallStartsInTop: 10px;\ngroupCallStartsWhenTop: 160px;\ngroupCallCountdownFont: font(64px semibold);\ngroupCallCountdownTop: 52px;\n\ndesktopCaptureMargins: margins(12px, 8px, 12px, 6px);\ndesktopCaptureSourceSize: size(235px, 165px);\ndesktopCaptureSourceSkips: size(2px, 10px);\ndesktopCaptureSourceTitle: WindowTitle(groupCallTitle) {\n\tbg: groupCallMembersBgOver;\n\tbgActive: groupCallMembersBgOver;\n\theight: windowTitleHeight;\n}\ndesktopCapturePadding: margins(7px, 7px, 7px, 33px);\ndesktopCaptureLabelBottom: 7px;\ndesktopCaptureLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 200px;\n\tmaxHeight: 20px;\n\ttextFg: groupCallMembersFg;\n\tstyle: semiboldTextStyle;\n}\ndesktopCaptureCancel: RoundButton(defaultBoxButton) {\n\ttextFg: groupCallActiveFg;\n\ttextFgOver: groupCallActiveFg;\n\tnumbersTextFg: groupCallActiveFg;\n\tnumbersTextFgOver: groupCallActiveFg;\n\ttextBg: groupCallMembersBg;\n\ttextBgOver: groupCallMembersBgOver;\n\n\tripple: groupCallRipple;\n}\ndesktopCaptureFinish: RoundButton(desktopCaptureCancel) {\n\ttextFg: groupCallMemberMutedIcon;\n\ttextFgOver: groupCallMemberMutedIcon;\n}\ndesktopCaptureSubmit: RoundButton(desktopCaptureCancel) {\n\ttextFg: groupCallIconFg;\n\ttextFgOver: groupCallIconFg;\n\tnumbersTextFg: groupCallIconFg;\n\tnumbersTextFgOver: groupCallIconFg;\n\ttextBg: groupCallMuted1;\n\ttextBgOver: groupCallMuted1;\n\n\tripple: universalRippleAnimation;\n}\ndesktopCaptureWithAudio: Checkbox(defaultCheckbox) {\n\ttextFg: groupCallMembersFg;\n\ttextFgActive: groupCallMembersFg;\n\trippleBg: groupCallMembersBgRipple;\n\trippleBgActive: groupCallMembersBgRipple;\n\tstyle: semiboldTextStyle;\n}\n\ngroupCallNarrowSkip: 9px;\ngroupCallNarrowMembersWidth: 204px;\ngroupCallNarrowVideoHeight: 120px;\ngroupCallWideModeWidthMin: 600px;\ngroupCallWideModeSize: size(960px, 580px);\ngroupCallNarrowInactiveCrossLine: CrossLineAnimation {\n\tfg: groupCallMemberNotJoinedStatus;\n\ticon: icon {{ \"calls/video_mini_mute\", groupCallMemberNotJoinedStatus }};\n\tstartPosition: point(3px, 0px);\n\tendPosition: point(13px, 12px);\n\tstroke: 3px;\n\tstrokeDenominator: 2;\n}\ngroupCallNarrowColoredCrossLine: CrossLineAnimation(groupCallNarrowInactiveCrossLine) {\n\tfg: groupCallMemberNotJoinedStatus;\n\ticon: icon {{ \"calls/video_mini_mute\", groupCallMemberActiveStatus }};\n}\ngroupCallNarrowRaisedHand: icon {{ \"calls/video_mini_speak\", groupCallMemberInactiveStatus }};\ngroupCallNarrowCameraIcon: icon {{ \"calls/video_mini_video\", groupCallMemberNotJoinedStatus }};\ngroupCallNarrowScreenIcon: icon {{ \"calls/video_mini_screencast\", groupCallMemberNotJoinedStatus }};\ngroupCallNarrowInvitedIcon: icon {{ \"calls/video_mini_invited\", groupCallMemberNotJoinedStatus }};\ngroupCallNarrowCallingIcon: icon {{ \"calls/video_mini_invited\", groupCallMemberNotJoinedStatus }};\ngroupCallNarrowIconPosition: point(-4px, 2px);\ngroupCallNarrowIconSkip: 15px;\ngroupCallOutline: 2px;\ngroupCallVideoCrossLine: CrossLineAnimation(groupCallMemberColoredCrossLine) {\n\tfg: groupCallVideoTextFg;\n\ticon: icon {{ \"calls/video_over_mute\", groupCallVideoTextFg }};\n}\n\nGroupCallVideoTile {\n\tshadowHeight: pixels;\n\tnamePosition: point;\n\tpin: CrossLineAnimation;\n\tpinPosition: point;\n\tpinPadding: margins;\n\tpinTextPosition: point;\n\tback: icon;\n\ticonPosition: point;\n}\n\ngroupCallVideoTile: GroupCallVideoTile {\n\tshadowHeight: 40px;\n\tnamePosition: point(15px, 8px);\n\tpin: CrossLineAnimation {\n\t\tfg: groupCallVideoTextFg;\n\t\ticon: icon {{ \"calls/video_over_pin\", groupCallVideoTextFg }};\n\t\tstartPosition: point(7px, 4px);\n\t\tendPosition: point(17px, 14px);\n\t\tstroke: 3px;\n\t\tstrokeDenominator: 2;\n\t}\n\tpinPosition: point(18px, 18px);\n\tpinPadding: margins(6px, 2px, 12px, 1px);\n\tpinTextPosition: point(1px, 3px);\n\tback: icon {{ \"calls/video_back\", groupCallVideoTextFg }};\n\ticonPosition: point(10px, 5px);\n}\n\ngroupCallVideoSmallSkip: 4px;\ngroupCallVideoLargeSkip: 6px;\ngroupCallVideoPlaceholderHeight: 212px;\ngroupCallVideoPlaceholderIconTop: 50px;\ngroupCallVideoPlaceholderTextTop: 120px;\n\ngroupCallTooltip: Tooltip(defaultTooltip) {\n\ttextBg: groupCallMembersBg;\n\ttextFg: groupCallMembersFg;\n\ttextBorder: groupCallMembersBgOver;\n}\ngroupCallNiceTooltip: ImportantTooltip(defaultImportantTooltip) {\n\tbg: importantTooltipBg;\n\tpadding: margins(10px, 3px, 10px, 5px);\n\tradius: 4px;\n\tarrow: 4px;\n}\ngroupCallNiceTooltipLabel: defaultImportantTooltipLabel;\ngroupCallStickedTooltip: ImportantTooltip(groupCallNiceTooltip) {\n\tpadding: margins(10px, 1px, 6px, 3px);\n}\ngroupCallStickedTooltipClose: IconButton(defaultIconButton) {\n\twidth: 20px;\n\theight: 20px;\n\ticonPosition: point(4px, 3px);\n\ticon: icon {{ \"calls/video_tooltip\", importantTooltipFg }};\n\ticonOver: icon {{ \"calls/video_tooltip\", importantTooltipFg }};\n\tripple: emptyRippleAnimation;\n}\ngroupCallNiceTooltipTop: 4px;\ngroupCallPaused: icon {{ \"calls/video_large_paused\", groupCallVideoTextFg }};\n\ngroupCallRecordingSubLabel: FlatLabel(boxDividerLabel) {\n\tmargin: margins(0px, 0px, 0px, 0px);\n\ttextFg: groupCallMemberNotJoinedStatus;\n\talign: align(top);\n}\ngroupCallRecordingInfoMargins: margins(0px, 22px, 0px, 22px);\ngroupCallRecordingSubLabelMargins: margins(8px, 22px, 8px, 22px);\ngroupCallRecordingAudioSkip: 23px;\ngroupCallRecordingSelectWidth: 2px;\ngroupCallRecordingInfoHeight: 204px;\n\ngroupCallRtmpCopyButton: RoundButton(defaultActiveButton) {\n\theight: 32px;\n\twidth: -26px;\n\n\ttextTop: 7px;\n}\ngroupCallRtmpCopyButtonTopSkip: 12px;\ngroupCallRtmpCopyButtonBottomSkip: 15px;\n\ngroupCallRtmpShowButton: IconButton(defaultIconButton) {\n\twidth: 32px;\n\theight: 32px;\n\n\ticon: icon {{ \"menu/show_in_chat\", menuIconFg }};\n\ticonOver: icon {{ \"menu/show_in_chat\", menuIconFgOver }};\n\ticonPosition: point(4px, 4px);\n\n\trippleAreaPosition: point(0px, 0px);\n\trippleAreaSize: 32px;\n\tripple: defaultRippleAnimationBgOver;\n}\ngroupCallSettingsRtmpShowButton: IconButton(groupCallRtmpShowButton) {\n\tripple: groupCallRipple;\n}\ngroupCallSubsectionTitle: FlatLabel(defaultSubsectionTitle) {\n\ttextFg: groupCallActiveFg;\n}\ngroupCallAttentionBoxButton: RoundButton(groupCallBoxButton) {\n\ttextFg: attentionButtonFg;\n\ttextFgOver: attentionButtonFgOver;\n}\n\ngroupCallRtmpUrlSkip: 1px;\ngroupCallRtmpKeySubsectionTitleSkip: 8px;\ngroupCallRtmpSubsectionTitleAddPadding: margins(0px, -1px, 0px, -4px);\ngroupCallRtmpShowButtonPosition: point(21px, -5px);\ngroupCallDividerBg: groupCallMembersBgRipple;\n\ngroupCallScheduleDateField: InputField(groupCallField) {\n\ttextMargins: margins(2px, 0px, 2px, 0px);\n\tplaceholderScale: 0.;\n\theightMin: 30px;\n\ttextAlign: align(top);\n}\ngroupCallScheduleTimeField: InputField(groupCallScheduleDateField) {\n\ttextBg: groupCallMembersBg;\n\tborder: 0px;\n\tborderActive: 0px;\n\theightMin: 28px;\n\tplaceholderFont: font(14px);\n}\n\ngroupCallVolumeSettings: Menu(groupCallPopupVolumeMenu) {\n\twidthMin: 210px;\n\titemBg: groupCallMembersBg;\n\titemBgOver: groupCallMembersBgOver;\n}\ngroupCallVolumeSettingsPadding: margins(24px, 8px, 24px, 6px);\ngroupCallVolumeSettingsSlider: MediaSlider(groupCallMenuVolumeSlider) {\n\tactiveFg: groupCallMenuBg;\n\tinactiveFg: groupCallMenuBg;\n\tactiveFgOver: groupCallMenuBg;\n\tinactiveFgOver: groupCallMenuBg;\n}\n\n//\ngroupCallCalendarPreviousDisabled: icon {{ \"calendar_down-flip_vertical\", groupCallMemberNotJoinedStatus }};\ngroupCallCalendarNextDisabled: icon {{ \"calendar_down\", groupCallMemberNotJoinedStatus }};\ngroupCallCalendarPrevious: IconButton(calendarPrevious) {\n\ticon: icon {{ \"calendar_down-flip_vertical\", groupCallMembersFg }};\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: groupCallMembersBgRipple;\n\t}\n}\ngroupCallCalendarNext: IconButton(groupCallCalendarPrevious) {\n\ticon: icon {{ \"calendar_down\", groupCallMembersFg }};\n}\ngroupCallCalendarColors: CalendarColors {\n\tdayTextColor: groupCallMembersFg;\n\tdayTextGrayedOutColor: groupCallMemberNotJoinedStatus;\n\n\ticonButtonNext: groupCallCalendarNext;\n\ticonButtonNextDisabled: groupCallCalendarNextDisabled;\n\ticonButtonPrevious: groupCallCalendarPrevious;\n\ticonButtonPreviousDisabled: groupCallCalendarPreviousDisabled;\n\n\ticonButtonRippleColorDisabled: groupCallMembersBgRipple;\n\n\trippleColor: groupCallMembersBgRipple;\n\trippleColorHighlighted: groupCallMembersBgRipple;\n\trippleGrayedOutColor: groupCallMembersBgRipple;\n\n\ttitleTextColor: groupCallMembersFg;\n}\n\ncreateCallInviteLink: SettingsButton(defaultSettingsButton) {\n\ttextFg: windowActiveTextFg;\n\ttextFgOver: windowActiveTextFg;\n\ttextBg: windowBg;\n\ttextBgOver: windowBgOver;\n\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px semibold);\n\t}\n\n\theight: 20px;\n\tpadding: margins(74px, 8px, 8px, 9px);\n}\ncreateCallInviteLinkIcon: icon {{ \"info/edit/group_manage_links\", windowActiveTextFg }};\ncreateCallInviteLinkIconPosition: point(23px, 2px);\ncreateCallVideo: IconButton {\n\twidth: 36px;\n\theight: 52px;\n\n\ticon: icon {{ \"info/info_media_video\", menuIconFg }};\n\ticonOver: icon {{ \"info/info_media_video\", menuIconFgOver }};\n\ticonPosition: point(-1px, -1px);\n\n\tripple: defaultRippleAnimation;\n\trippleAreaPosition: point(0px, 8px);\n\trippleAreaSize: 36px;\n}\ncreateCallVideoActive: icon {{ \"info/info_media_video\", windowActiveTextFg }};\ncreateCallVideoMargins: margins(0px, 0px, 10px, 0px);\ncreateCallAudio: IconButton(createCallVideo) {\n\ticon: icon {{ \"menu/phone\", menuIconFg }};\n\ticonOver: icon {{ \"menu/phone\", menuIconFgOver }};\n}\ncreateCallAudioActive: icon {{ \"menu/phone\", windowActiveTextFg }};\ncreateCallAudioMargins: margins(0px, 0px, 4px, 0px);\n\nconfcallLinkButton: RoundButton(defaultActiveButton) {\n\theight: 42px;\n\ttextTop: 12px;\n\tstyle: semiboldTextStyle;\n}\nconfcallLinkBoxInitial: Box(defaultBox) {\n\tbuttonPadding: margins(12px, 11px, 24px, 96px);\n\tbuttonHeight: 42px;\n\tbuttonWide: true;\n\tbutton: confcallLinkButton;\n\tshadowIgnoreTopSkip: true;\n}\nconfcallLinkBox: Box(confcallLinkBoxInitial) {\n\tbuttonPadding: margins(12px, 11px, 24px, 24px);\n}\nconfcallLinkCopyButton: RoundButton(confcallLinkButton) {\n\ticon: icon {{ \"info/edit/links_copy\", activeButtonFg }};\n\ticonOver: icon {{ \"info/edit/links_copy\", activeButtonFgOver }};\n\ticonPosition: point(-1px, 5px);\n}\nconfcallLinkShareButton: RoundButton(confcallLinkCopyButton) {\n\ticon: icon {{ \"info/edit/links_share\", activeButtonFg }};\n\ticonOver: icon {{ \"info/edit/links_share\", activeButtonFgOver }};\n}\nconfcallLinkHeaderIconPadding: margins(0px, 32px, 0px, 10px);\nconfcallLinkTitlePadding: margins(0px, 0px, 0px, 12px);\nconfcallLinkCenteredText: FlatLabel(defaultFlatLabel) {\n\talign: align(top);\n\tminWidth: 40px;\n}\nconfcallLinkFooterOr: FlatLabel(confcallLinkCenteredText) {\n\ttextFg: windowSubTextFg;\n}\nconfcallLinkFooterOrTop: 12px;\nconfcallLinkFooterOrSkip: 8px;\nconfcallLinkFooterOrLineTop: 9px;\nconfcallJoinBox: Box(confcallLinkBox) {\n\tbuttonPadding: margins(24px, 24px, 24px, 24px);\n}\nconfcallJoinLogo: icon {{ \"calls/group_call_logo\", windowFgActive }};\nconfcallJoinLogoPadding: margins(8px, 8px, 8px, 8px);\nconfcallJoinSepPadding: margins(0px, 8px, 0px, 8px);\nconfcallJoinUserpics: UserpicsRow {\n\tbutton: UserpicButton(defaultUserpicButton) {\n\t\tsize: size(36px, 36px);\n\t\tphotoSize: 36px;\n\t}\n\tbg: boxBg;\n\tshift: 16px;\n\tstroke: 2px;\n\tinvert: true;\n}\nconfcallJoinUserpicsPadding: margins(0px, 0px, 0px, 16px);\nconfcallInviteUserpicsBg: groupCallMembersBg;\nconfcallInviteUserpics: UserpicsRow {\n\tbutton: UserpicButton(defaultUserpicButton) {\n\t\tsize: size(24px, 24px);\n\t\tphotoSize: 24px;\n\t}\n\tbg: confcallInviteUserpicsBg;\n\tshift: 10px;\n\tstroke: 2px;\n\tinvert: true;\n}\nconfcallInviteParticipants: FlatLabel(defaultFlatLabel) {\n\ttextFg: callNameFg;\n}\nconfcallInviteParticipantsPadding: margins(8px, 3px, 12px, 2px);\nconfcallInviteVideo: IconButton(createCallVideo) {\n\ticon: icon {{ \"info/info_media_video\", groupCallMemberInactiveIcon }};\n\ticonOver: icon {{ \"info/info_media_video\", groupCallMemberInactiveIcon }};\n\tripple: groupCallRipple;\n}\nconfcallInviteVideoActive: icon {{ \"info/info_media_video\", groupCallActiveFg }};\nconfcallInviteAudio: IconButton(confcallInviteVideo) {\n\ticon: icon {{ \"menu/phone\", groupCallMemberInactiveIcon }};\n\ticonOver: icon {{ \"menu/phone\", groupCallMemberInactiveIcon }};\n}\nconfcallInviteAudioActive: icon {{ \"menu/phone\", groupCallActiveFg }};\n\ngroupCallLinkBox: Box(confcallLinkBox) {\n\tbg: groupCallMembersBg;\n\ttitle: FlatLabel(boxTitle) {\n\t\ttextFg: groupCallMembersFg;\n\t}\n\ttitleAdditionalFg: groupCallMemberNotJoinedStatus;\n}\ngroupCallLinkCenteredText: FlatLabel(confcallLinkCenteredText) {\n\ttextFg: groupCallMembersFg;\n}\ngroupCallLinkPreview: InputField(defaultInputField) {\n\ttextBg: groupCallMembersBgOver;\n\ttextFg: groupCallMembersFg;\n\ttextMargins: margins(12px, 8px, 30px, 5px);\n\tstyle: defaultTextStyle;\n\theightMin: 35px;\n}\ngroupCallInviteLink: SettingsButton(createCallInviteLink) {\n\ttextFg: mediaviewTextLinkFg;\n\ttextFgOver: mediaviewTextLinkFg;\n\ttextBg: groupCallMembersBg;\n\ttextBgOver: groupCallMembersBgOver;\n\n\tpadding: margins(63px, 8px, 8px, 9px);\n\n\tripple: groupCallRipple;\n}\ngroupCallInviteLinkIcon: icon {{ \"info/edit/group_manage_links\", mediaviewTextLinkFg }};\n\ngroupCallMessagesScroll: ScrollArea(defaultScrollArea) {\n\tbarHidden: true;\n}\ngroupCallMessagePadding: margins(8px, 3px, 8px, 2px);\ngroupCallPricePadding: margins(5px, 0px, 5px, 0px);\ngroupCallMessageSkip: 8px;\ngroupCallMessagePalette: TextPalette(defaultTextPalette) {\n\tlinkFg: radialFg;\n\tmonoFg: radialFg;\n\tspoilerFg: radialFg;\n}\ngroupCallMessageBadge: RoundButton(defaultActiveButton) {\n\ttextBg: attentionButtonFg;\n\ttextBgOver: attentionButtonFg;\n\twidth: -6px;\n\theight: 11px;\n\tradius: 5px;\n\ttextTop: 0px;\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(8px semibold);\n\t}\n}\ngroupCallMessageBadgeMargin: margins(0px, 4px, 0px, 0px);\ngroupCallUserpic: 20px;\ngroupCallUserpicPadding: margins(2px, 2px, 4px, 2px);\ngroupCallPinnedPadding: margins(10px, 4px, 10px, 2px);\ngroupCallPinnedMaxWidth: 96px;\ngroupCallPinnedUserpic: 22px;\ngroupCallEffectPadding: margins(3px, 1px, 3px, 1px);\ngroupCallEffectUserpicPadding: margins(1px, 1px, 3px, 1px);\n\ngroupCallTopReactorBadge: RoundCheckbox(defaultRoundCheckbox) {\n\twidth: 1px;\n\tborder: groupCallMembersBg;\n}\n\ngroupCallLinkMenu: IconButton(boxTitleMenu) {\n\ticon: icon {{ \"title_menu_dots\", groupCallMemberInactiveIcon }};\n\ticonOver: icon {{ \"title_menu_dots\", groupCallMemberInactiveIcon }};\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: groupCallMembersBgOver;\n\t}\n}\n\nconfcallFingerprintBottomSkip: 8px;\nconfcallFingerprintMargins: margins(8px, 5px, 8px, 5px);\nconfcallFingerprintTextMargins: margins(3px, 3px, 3px, 0px);\nconfcallFingerprintText: FlatLabel(defaultFlatLabel) {\n\ttextFg: groupCallMembersFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(10px semibold);\n\t}\n}\nconfcallFingerprintSkip: 2px;\nconfcallFingerprintTooltipLabel: defaultImportantTooltipLabel;\nconfcallFingerprintTooltip: defaultImportantTooltip;\nconfcallFingerprintTooltipSkip: 12px;\nconfcallFingerprintTooltipMaxWidth: 220px;\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_box_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/calls_box_controller.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"core/application.h\"\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/group/calls_group_invite_controller.h\"\n#include \"calls/calls_instance.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"mainwidget.h\"\n#include \"window/window_session_controller.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_user.h\"\n#include \"data/data_peer_values.h\" // Data::ChannelHasActiveCall.\n#include \"data/data_group_call.h\"\n#include \"data/data_channel.h\"\n#include \"boxes/delete_messages_box.h\"\n#include \"base/unixtime.h\"\n#include \"api/api_updates.h\"\n#include \"apiwrap.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"settings/sections/settings_calls.h\"\n#include \"settings/settings_common.h\"\n#include \"styles/style_info.h\" // infoTopBarMenu\n#include \"styles/style_layers.h\" // st::boxLabel.\n#include \"styles/style_calls.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Calls {\nnamespace {\n\nconstexpr auto kFirstPageCount = 20;\nconstexpr auto kPerPageCount = 100;\n\nclass GroupCallRow final : public PeerListRow {\npublic:\n\tGroupCallRow(not_null<PeerData*> peer);\n\n\tvoid rightActionAddRipple(\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) override;\n\tvoid rightActionStopLastRipple() override;\n\n\tint paintNameIconGetWidth(\n\t\t\tPainter &p,\n\t\t\tFn<void()> repaint,\n\t\t\tcrl::time now,\n\t\t\tint nameLeft,\n\t\t\tint nameTop,\n\t\t\tint nameWidth,\n\t\t\tint availableWidth,\n\t\t\tint outerWidth,\n\t\t\tbool selected) override {\n\t\treturn 0;\n\t}\n\tQSize rightActionSize() const override {\n\t\treturn peer()->isChannel() ? QSize(_st.width, _st.height) : QSize();\n\t}\n\tQMargins rightActionMargins() const override {\n\t\treturn QMargins(\n\t\t\t0,\n\t\t\t0,\n\t\t\tst::defaultPeerListItem.photoPosition.x(),\n\t\t\t0);\n\t}\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\nprivate:\n\tconst style::IconButton &_st;\n\tstd::unique_ptr<Ui::RippleAnimation> _actionRipple;\n\n};\n\nGroupCallRow::GroupCallRow(not_null<PeerData*> peer)\n: PeerListRow(peer)\n, _st(st::callGroupCall) {\n\tif (const auto channel = peer->asChannel()) {\n\t\tconst auto status = (!channel->isMegagroup()\n\t\t\t? (channel->isPublic()\n\t\t\t\t? tr::lng_create_public_channel_title\n\t\t\t\t: tr::lng_create_private_channel_title)\n\t\t\t: (channel->isPublic()\n\t\t\t\t? tr::lng_create_public_group_title\n\t\t\t\t: tr::lng_create_private_group_title))(tr::now);\n\t\tsetCustomStatus(status.toLower());\n\t}\n}\n\nvoid GroupCallRow::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\tauto size = rightActionSize();\n\tif (_actionRipple) {\n\t\t_actionRipple->paint(\n\t\t\tp,\n\t\t\tx + _st.rippleAreaPosition.x(),\n\t\t\ty + _st.rippleAreaPosition.y(),\n\t\t\touterWidth);\n\t\tif (_actionRipple->empty()) {\n\t\t\t_actionRipple.reset();\n\t\t}\n\t}\n\t_st.icon.paintInCenter(\n\t\tp,\n\t\tstyle::rtlrect(x, y, size.width(), size.height(), outerWidth));\n}\n\nvoid GroupCallRow::rightActionAddRipple(\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) {\n\tif (!_actionRipple) {\n\t\tauto mask = Ui::RippleAnimation::EllipseMask(\n\t\t\tQSize(_st.rippleAreaSize, _st.rippleAreaSize));\n\t\t_actionRipple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t_st.ripple,\n\t\t\tstd::move(mask),\n\t\t\tstd::move(updateCallback));\n\t}\n\t_actionRipple->add(point - _st.rippleAreaPosition);\n}\n\nvoid GroupCallRow::rightActionStopLastRipple() {\n\tif (_actionRipple) {\n\t\t_actionRipple->lastStop();\n\t}\n}\n\n} // namespace\n\nnamespace GroupCalls {\n\nListController::ListController(not_null<::Window::SessionController*> window)\n: _window(window) {\n\tsetStyleOverrides(&st::peerListSingleRow);\n}\n\nMain::Session &ListController::session() const {\n\treturn _window->session();\n}\n\nvoid ListController::prepare() {\n\tconst auto removeRow = [=](not_null<PeerData*> peer) {\n\t\tconst auto it = _groupCalls.find(peer->id);\n\t\tif (it != end(_groupCalls)) {\n\t\t\tconst auto &row = it->second;\n\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t\t_groupCalls.erase(it);\n\t\t}\n\t};\n\tconst auto createRow = [=](not_null<PeerData*> peer) {\n\t\tconst auto it = _groupCalls.find(peer->id);\n\t\tif (it == end(_groupCalls)) {\n\t\t\tauto row = std::make_unique<GroupCallRow>(peer);\n\t\t\t_groupCalls.emplace(peer->id, row.get());\n\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t}\n\t};\n\n\tconst auto processPeer = [=](PeerData *peer) {\n\t\tif (!peer) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto channel = peer->asChannel();\n\t\tif (channel && Data::ChannelHasActiveCall(channel)) {\n\t\t\tcreateRow(peer);\n\t\t} else {\n\t\t\tremoveRow(peer);\n\t\t}\n\t};\n\tconst auto finishProcess = [=] {\n\t\tdelegate()->peerListRefreshRows();\n\t\t_fullCount = delegate()->peerListFullRowsCount();\n\t};\n\n\tsession().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::GroupCall\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tprocessPeer(update.peer);\n\t\tfinishProcess();\n\t}, lifetime());\n\n\t{\n\t\tauto count = 0;\n\t\tconst auto list = session().data().chatsList(nullptr);\n\t\tfor (const auto &key : list->pinned()->order()) {\n\t\t\tprocessPeer(key.peer());\n\t\t}\n\t\tfor (const auto &key : list->indexed()->all()) {\n\t\t\tif (count > kFirstPageCount) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tprocessPeer(key->key().peer());\n\t\t\tcount++;\n\t\t}\n\t\tfinishProcess();\n\t}\n}\n\nrpl::producer<bool> ListController::shownValue() const {\n\treturn _fullCount.value(\n\t) | rpl::map(rpl::mappers::_1 > 0) | rpl::distinct_until_changed();\n}\n\nvoid ListController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto window = _window;\n\tcrl::on_main(window, [=, peer = row->peer()] {\n\t\twindow->showPeerHistory(\n\t\t\tpeer,\n\t\t\t::Window::SectionShow::Way::ClearStack);\n\t});\n}\n\nvoid ListController::rowRightActionClicked(not_null<PeerListRow*> row) {\n\t_window->startOrJoinGroupCall(row->peer());\n}\n\n} // namespace GroupCalls\n\nclass BoxController::Row : public PeerListRow {\npublic:\n\tRow(not_null<HistoryItem*> item);\n\n\tenum class Type {\n\t\tOut,\n\t\tIn,\n\t\tMissed,\n\t};\n\n\tenum class CallType {\n\t\tVoice,\n\t\tVideo,\n\t};\n\n\tbool canAddItem(not_null<const HistoryItem*> item) const {\n\t\treturn (ComputeType(item) == _type)\n\t\t\t&& (!hasItems() || _items.front()->history() == item->history())\n\t\t\t&& (ItemDateTime(item).date() == _date);\n\t}\n\tvoid addItem(not_null<HistoryItem*> item) {\n\t\tExpects(canAddItem(item));\n\n\t\t_items.push_back(item);\n\t\tranges::sort(_items, [](not_null<HistoryItem*> a, auto b) {\n\t\t\treturn (a->id > b->id);\n\t\t});\n\t\trefreshStatus();\n\t}\n\tvoid itemRemoved(not_null<const HistoryItem*> item) {\n\t\tif (hasItems() && item->id >= minItemId() && item->id <= maxItemId()) {\n\t\t\t_items.erase(std::remove(_items.begin(), _items.end(), item), _items.end());\n\t\t\trefreshStatus();\n\t\t}\n\t}\n\t[[nodiscard]] bool hasItems() const {\n\t\treturn !_items.empty();\n\t}\n\n\t[[nodiscard]] MsgId minItemId() const {\n\t\tExpects(hasItems());\n\n\t\treturn _items.back()->id;\n\t}\n\n\t[[nodiscard]] MsgId maxItemId() const {\n\t\tExpects(hasItems());\n\n\t\treturn _items.front()->id;\n\t}\n\n\t[[nodiscard]] const std::vector<not_null<HistoryItem*>> &items() const {\n\t\treturn _items;\n\t}\n\n\tvoid paintStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) override;\n\tvoid rightActionAddRipple(\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) override;\n\tvoid rightActionStopLastRipple() override;\n\n\tint paintNameIconGetWidth(\n\t\t\tPainter &p,\n\t\t\tFn<void()> repaint,\n\t\t\tcrl::time now,\n\t\t\tint nameLeft,\n\t\t\tint nameTop,\n\t\t\tint nameWidth,\n\t\t\tint availableWidth,\n\t\t\tint outerWidth,\n\t\t\tbool selected) override {\n\t\treturn 0;\n\t}\n\tQSize rightActionSize() const override {\n\t\treturn peer()->isUser() ? QSize(_st->width, _st->height) : QSize();\n\t}\n\tQMargins rightActionMargins() const override {\n\t\treturn QMargins(\n\t\t\t0,\n\t\t\t0,\n\t\t\tst::defaultPeerListItem.photoPosition.x(),\n\t\t\t0);\n\t}\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\nprivate:\n\tvoid refreshStatus() override;\n\tstatic Type ComputeType(not_null<const HistoryItem*> item);\n\tstatic CallType ComputeCallType(not_null<const HistoryItem*> item);\n\n\tstd::vector<not_null<HistoryItem*>> _items;\n\tQDate _date;\n\tType _type;\n\tnot_null<const style::IconButton*> _st;\n\n\tstd::unique_ptr<Ui::RippleAnimation> _actionRipple;\n\n};\n\nBoxController::Row::Row(not_null<HistoryItem*> item)\n: PeerListRow(item->history()->peer, item->id.bare)\n, _items(1, item)\n, _date(ItemDateTime(item).date())\n, _type(ComputeType(item))\n, _st(ComputeCallType(item) == CallType::Voice\n\t\t? &st::callReDial\n\t\t: &st::callCameraReDial) {\n\trefreshStatus();\n}\n\nvoid BoxController::Row::paintStatusText(Painter &p, const style::PeerListItem &st, int x, int y, int availableWidth, int outerWidth, bool selected) {\n\tauto icon = ([this] {\n\t\tswitch (_type) {\n\t\tcase Type::In: return &st::callArrowIn;\n\t\tcase Type::Out: return &st::callArrowOut;\n\t\tcase Type::Missed: return &st::callArrowMissed;\n\t\t}\n\t\tUnexpected(\"_type in Calls::BoxController::Row::paintStatusText().\");\n\t})();\n\ticon->paint(p, x + st::callArrowPosition.x(), y + st::callArrowPosition.y(), outerWidth);\n\tauto shift = st::callArrowPosition.x() + icon->width() + st::callArrowSkip;\n\tx += shift;\n\tavailableWidth -= shift;\n\n\tPeerListRow::paintStatusText(p, st, x, y, availableWidth, outerWidth, selected);\n}\n\nvoid BoxController::Row::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\tauto size = rightActionSize();\n\tif (_actionRipple) {\n\t\t_actionRipple->paint(\n\t\t\tp,\n\t\t\tx + _st->rippleAreaPosition.x(),\n\t\t\ty + _st->rippleAreaPosition.y(),\n\t\t\touterWidth);\n\t\tif (_actionRipple->empty()) {\n\t\t\t_actionRipple.reset();\n\t\t}\n\t}\n\t_st->icon.paintInCenter(\n\t\tp,\n\t\tstyle::rtlrect(x, y, size.width(), size.height(), outerWidth));\n}\n\nvoid BoxController::Row::refreshStatus() {\n\tif (!hasItems()) {\n\t\treturn;\n\t}\n\tauto text = [this] {\n\t\tauto time = QLocale().toString(ItemDateTime(_items.front()).time(), QLocale::ShortFormat);\n\t\tauto today = QDateTime::currentDateTime().date();\n\t\tif (_date == today) {\n\t\t\treturn tr::lng_call_box_status_today(tr::now, lt_time, time);\n\t\t} else if (_date.addDays(1) == today) {\n\t\t\treturn tr::lng_call_box_status_yesterday(tr::now, lt_time, time);\n\t\t}\n\t\treturn tr::lng_call_box_status_date(tr::now, lt_date, langDayOfMonthFull(_date), lt_time, time);\n\t};\n\tsetCustomStatus((_items.size() > 1)\n\t\t? tr::lng_call_box_status_group(\n\t\t\ttr::now,\n\t\t\tlt_amount,\n\t\t\tQString::number(_items.size()),\n\t\t\tlt_status,\n\t\t\ttext())\n\t\t: text());\n}\n\nBoxController::Row::Type BoxController::Row::ComputeType(\n\t\tnot_null<const HistoryItem*> item) {\n\tif (item->out()) {\n\t\treturn Type::Out;\n\t} else if (auto media = item->media()) {\n\t\tif (const auto call = media->call()) {\n\t\t\tusing State = Data::CallState;\n\t\t\tconst auto state = call->state;\n\t\t\tif (state == State::Busy || state == State::Missed) {\n\t\t\t\treturn Type::Missed;\n\t\t\t}\n\t\t}\n\t}\n\treturn Type::In;\n}\n\nBoxController::Row::CallType BoxController::Row::ComputeCallType(\n\t\tnot_null<const HistoryItem*> item) {\n\tif (auto media = item->media()) {\n\t\tif (const auto call = media->call()) {\n\t\t\tif (call->video) {\n\t\t\t\treturn CallType::Video;\n\t\t\t}\n\t\t}\n\t}\n\treturn CallType::Voice;\n}\n\nvoid BoxController::Row::rightActionAddRipple(QPoint point, Fn<void()> updateCallback) {\n\tif (!_actionRipple) {\n\t\tauto mask = Ui::RippleAnimation::EllipseMask(\n\t\t\tQSize(_st->rippleAreaSize, _st->rippleAreaSize));\n\t\t_actionRipple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t_st->ripple,\n\t\t\tstd::move(mask),\n\t\t\tstd::move(updateCallback));\n\t}\n\t_actionRipple->add(point - _st->rippleAreaPosition);\n}\n\nvoid BoxController::Row::rightActionStopLastRipple() {\n\tif (_actionRipple) {\n\t\t_actionRipple->lastStop();\n\t}\n}\n\nBoxController::BoxController(not_null<::Window::SessionController*> window)\n: _window(window)\n, _api(&_window->session().mtp()) {\n}\n\nMain::Session &BoxController::session() const {\n\treturn _window->session();\n}\n\nvoid BoxController::prepare() {\n\tsession().data().itemRemoved(\n\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\tif (const auto row = rowForItem(item)) {\n\t\t\trow->itemRemoved(item);\n\t\t\tif (!row->hasItems()) {\n\t\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t\t\tif (!delegate()->peerListFullRowsCount()) {\n\t\t\t\t\trefreshAbout();\n\t\t\t\t}\n\t\t\t}\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t}, lifetime());\n\n\tsession().changes().messageUpdates(\n\t\tData::MessageUpdate::Flag::NewAdded\n\t) | rpl::filter([=](const Data::MessageUpdate &update) {\n\t\tconst auto media = update.item->media();\n\t\treturn (media != nullptr) && (media->call() != nullptr);\n\t}) | rpl::on_next([=](const Data::MessageUpdate &update) {\n\t\tinsertRow(update.item, InsertWay::Prepend);\n\t}, lifetime());\n\n\tdelegate()->peerListSetTitle(tr::lng_call_box_title());\n\tsetDescriptionText(tr::lng_contacts_loading(tr::now));\n\tdelegate()->peerListRefreshRows();\n\n\tloadMoreRows();\n}\n\nvoid BoxController::loadMoreRows() {\n\tif (_loadRequestId || _allLoaded) {\n\t\treturn;\n\t}\n\n\t_loadRequestId = _api.request(MTPmessages_Search(\n\t\tMTP_flags(0),\n\t\tMTP_inputPeerEmpty(),\n\t\tMTP_string(), // q\n\t\tMTP_inputPeerEmpty(),\n\t\tMTPInputPeer(), // saved_peer_id\n\t\tMTPVector<MTPReaction>(), // saved_reaction\n\t\tMTPint(), // top_msg_id\n\t\tMTP_inputMessagesFilterPhoneCalls(MTP_flags(0)),\n\t\tMTP_int(0), // min_date\n\t\tMTP_int(0), // max_date\n\t\tMTP_int(_offsetId),\n\t\tMTP_int(0), // add_offset\n\t\tMTP_int(_offsetId ? kFirstPageCount : kPerPageCount),\n\t\tMTP_int(0), // max_id\n\t\tMTP_int(0), // min_id\n\t\tMTP_long(0) // hash\n\t)).done([this](const MTPmessages_Messages &result) {\n\t\t_loadRequestId = 0;\n\n\t\tauto handleResult = [&](auto &data) {\n\t\t\tsession().data().processUsers(data.vusers());\n\t\t\tsession().data().processChats(data.vchats());\n\t\t\treceivedCalls(data.vmessages().v);\n\t\t};\n\n\t\tswitch (result.type()) {\n\t\tcase mtpc_messages_messages: handleResult(result.c_messages_messages()); _allLoaded = true; break;\n\t\tcase mtpc_messages_messagesSlice: handleResult(result.c_messages_messagesSlice()); break;\n\t\tcase mtpc_messages_channelMessages: {\n\t\t\tLOG((\"API Error: received messages.channelMessages! (Calls::BoxController::preloadRows)\"));\n\t\t\thandleResult(result.c_messages_channelMessages());\n\t\t} break;\n\t\tcase mtpc_messages_messagesNotModified: {\n\t\t\tLOG((\"API Error: received messages.messagesNotModified! (Calls::BoxController::preloadRows)\"));\n\t\t} break;\n\t\tdefault: Unexpected(\"Type of messages.Messages (Calls::BoxController::preloadRows)\");\n\t\t}\n\t}).fail([this] {\n\t\t_loadRequestId = 0;\n\t}).send();\n}\n\nbase::unique_qptr<Ui::PopupMenu> BoxController::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tconst auto &items = static_cast<Row*>(row.get())->items();\n\tconst auto session = &this->session();\n\tconst auto ids = session->data().itemsToIds(items);\n\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\tresult->addAction(tr::lng_context_delete_selected(tr::now), [=] {\n\t\t_window->show(\n\t\t\tBox<DeleteMessagesBox>(session, base::duplicate(ids)));\n\t}, &st::menuIconDelete);\n\tresult->addAction(tr::lng_context_to_msg(tr::now), [=, window = _window] {\n\t\tif (const auto item = session->data().message(ids.front())) {\n\t\t\twindow->showMessage(item);\n\t\t}\n\t}, &st::menuIconShowInChat);\n\treturn result;\n}\n\nvoid BoxController::refreshAbout() {\n\tsetDescriptionText(delegate()->peerListFullRowsCount() ? QString() : tr::lng_call_box_about(tr::now));\n}\n\nvoid BoxController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto itemsRow = static_cast<Row*>(row.get());\n\tconst auto itemId = itemsRow->maxItemId();\n\tconst auto window = _window;\n\tcrl::on_main(window, [=, peer = row->peer()] {\n\t\twindow->showPeerHistory(\n\t\t\tpeer,\n\t\t\t::Window::SectionShow::Way::ClearStack,\n\t\t\titemId);\n\t});\n}\n\nvoid BoxController::rowRightActionClicked(not_null<PeerListRow*> row) {\n\tauto user = row->peer()->asUser();\n\tAssert(user != nullptr);\n\n\tCore::App().calls().startOutgoingCall(user, {});\n}\n\nvoid BoxController::receivedCalls(const QVector<MTPMessage> &result) {\n\tif (result.empty()) {\n\t\t_allLoaded = true;\n\t}\n\n\tfor (const auto &message : result) {\n\t\tconst auto msgId = IdFromMessage(message);\n\t\tconst auto peerId = PeerFromMessage(message);\n\t\tif (session().data().peerLoaded(peerId)) {\n\t\t\tconst auto item = session().data().addNewMessage(\n\t\t\t\tmessage,\n\t\t\t\tMessageFlags(),\n\t\t\t\tNewMessageType::Existing);\n\t\t\tinsertRow(item, InsertWay::Append);\n\t\t} else {\n\t\t\tLOG((\"API Error: a search results with not loaded peer %1\"\n\t\t\t\t).arg(peerId.value));\n\t\t}\n\t\t_offsetId = msgId;\n\t}\n\n\trefreshAbout();\n\tdelegate()->peerListRefreshRows();\n}\n\nbool BoxController::insertRow(\n\t\tnot_null<HistoryItem*> item,\n\t\tInsertWay way) {\n\tif (auto row = rowForItem(item)) {\n\t\tif (row->canAddItem(item)) {\n\t\t\trow->addItem(item);\n\t\t\treturn false;\n\t\t}\n\t}\n\t(way == InsertWay::Append)\n\t\t? delegate()->peerListAppendRow(createRow(item))\n\t\t: delegate()->peerListPrependRow(createRow(item));\n\tdelegate()->peerListSortRows([](\n\t\t\tconst PeerListRow &a,\n\t\t\tconst PeerListRow &b) {\n\t\treturn static_cast<const Row&>(a).maxItemId()\n\t\t\t> static_cast<const Row&>(b).maxItemId();\n\t});\n\treturn true;\n}\n\nBoxController::Row *BoxController::rowForItem(not_null<const HistoryItem*> item) {\n\tauto v = delegate();\n\tif (auto fullRowsCount = v->peerListFullRowsCount()) {\n\t\tauto itemId = item->id;\n\t\tauto lastRow = static_cast<Row*>(v->peerListRowAt(fullRowsCount - 1).get());\n\t\tif (itemId < lastRow->minItemId()) {\n\t\t\treturn lastRow;\n\t\t}\n\t\tauto firstRow = static_cast<Row*>(v->peerListRowAt(0).get());\n\t\tif (itemId > firstRow->maxItemId()) {\n\t\t\treturn firstRow;\n\t\t}\n\n\t\t// Binary search. Invariant:\n\t\t// 1. rowAt(left)->maxItemId() >= itemId.\n\t\t// 2. (left + 1 == fullRowsCount) OR rowAt(left + 1)->maxItemId() < itemId.\n\t\tauto left = 0;\n\t\tauto right = fullRowsCount;\n\t\twhile (left + 1 < right) {\n\t\t\tauto middle = (right + left) / 2;\n\t\t\tauto middleRow = static_cast<Row*>(v->peerListRowAt(middle).get());\n\t\t\tif (middleRow->maxItemId() >= itemId) {\n\t\t\t\tleft = middle;\n\t\t\t} else {\n\t\t\t\tright = middle;\n\t\t\t}\n\t\t}\n\t\tauto result = static_cast<Row*>(v->peerListRowAt(left).get());\n\t\t// Check for rowAt(left)->minItemId > itemId > rowAt(left + 1)->maxItemId.\n\t\t// In that case we sometimes need to return rowAt(left + 1), not rowAt(left).\n\t\tif (result->minItemId() > itemId && left + 1 < fullRowsCount) {\n\t\t\tauto possibleResult = static_cast<Row*>(v->peerListRowAt(left + 1).get());\n\t\t\tAssert(possibleResult->maxItemId() < itemId);\n\t\t\tif (possibleResult->canAddItem(item)) {\n\t\t\t\treturn possibleResult;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\treturn nullptr;\n}\n\nstd::unique_ptr<PeerListRow> BoxController::createRow(\n\t\tnot_null<HistoryItem*> item) const {\n\treturn std::make_unique<Row>(item);\n}\n\nvoid ClearCallsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<::Window::SessionController*> window) {\n\tconst auto weak = base::make_weak(box);\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_call_box_clear_sure(),\n\t\t\tst::boxLabel),\n\t\tst::boxPadding);\n\tconst auto revokeCheckbox = box->addRow(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tbox,\n\t\t\ttr::lng_delete_for_everyone_check(tr::now),\n\t\t\tfalse,\n\t\t\tst::defaultBoxCheckbox),\n\t\tstyle::margins(\n\t\t\tst::boxPadding.left(),\n\t\t\tst::boxPadding.bottom(),\n\t\t\tst::boxPadding.right(),\n\t\t\tst::boxPadding.bottom()));\n\n\tconst auto api = &window->session().api();\n\tconst auto sendRequest = [=](bool revoke, auto self) -> void {\n\t\tusing Flag = MTPmessages_DeletePhoneCallHistory::Flag;\n\t\tapi->request(MTPmessages_DeletePhoneCallHistory(\n\t\t\tMTP_flags(revoke ? Flag::f_revoke : Flag(0))\n\t\t)).done([=](const MTPmessages_AffectedFoundMessages &result) {\n\t\t\tresult.match([&](\n\t\t\t\t\tconst MTPDmessages_affectedFoundMessages &data) {\n\t\t\t\tapi->applyUpdates(MTP_updates(\n\t\t\t\t\tMTP_vector<MTPUpdate>(\n\t\t\t\t\t\t1,\n\t\t\t\t\t\tMTP_updateDeleteMessages(\n\t\t\t\t\t\t\tdata.vmessages(),\n\t\t\t\t\t\t\tdata.vpts(),\n\t\t\t\t\t\t\tdata.vpts_count())),\n\t\t\t\t\tMTP_vector<MTPUser>(),\n\t\t\t\t\tMTP_vector<MTPChat>(),\n\t\t\t\t\tMTP_int(base::unixtime::now()),\n\t\t\t\t\tMTP_int(0)));\n\t\t\t\tconst auto offset = data.voffset().v;\n\t\t\t\tif (offset > 0) {\n\t\t\t\t\tself(revoke, self);\n\t\t\t\t} else {\n\t\t\t\t\tapi->session().data().destroyAllCallItems();\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}).send();\n\t};\n\n\tbox->addButton(tr::lng_call_box_clear_button(), [=] {\n\t\tsendRequest(revokeCheckbox->checked(), sendRequest);\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\n[[nodiscard]] not_null<Ui::SettingsButton*> AddCreateCallButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<::Window::SessionController*> controller,\n\t\tFn<void()> done) {\n\tconst auto result = container->add(object_ptr<Ui::SettingsButton>(\n\t\tcontainer,\n\t\ttr::lng_confcall_create_call(),\n\t\tst::inviteViaLinkButton), QMargins());\n\tUi::AddSkip(container);\n\tUi::AddDividerText(\n\t\tcontainer,\n\t\ttr::lng_confcall_create_call_description(\n\t\t\tlt_count,\n\t\t\trpl::single(controller->session().appConfig().confcallSizeLimit()\n\t\t\t\t* 1.),\n\t\t\ttr::marked));\n\n\tconst auto icon = Ui::CreateChild<Info::Profile::FloatingIcon>(\n\t\tresult,\n\t\tst::inviteViaLinkIcon,\n\t\tQPoint());\n\tresult->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\ticon->moveToLeft(\n\t\t\tst::inviteViaLinkIconPosition.x(),\n\t\t\t(height - st::inviteViaLinkIcon.height()) / 2);\n\t}, icon->lifetime());\n\n\tresult->setClickedCallback([=] {\n\t\tcontroller->show(Group::PrepareCreateCallBox(controller, done));\n\t});\n\n\treturn result;\n}\n\nvoid ShowCallsBox(\n\t\tnot_null<::Window::SessionController*> window,\n\t\tbool highlightStartCall) {\n\tstruct State {\n\t\tState(not_null<::Window::SessionController*> window)\n\t\t: callsController(window)\n\t\t, groupCallsController(window) {\n\t\t}\n\t\tCalls::BoxController callsController;\n\t\tPeerListContentDelegateSimple callsDelegate;\n\n\t\tCalls::GroupCalls::ListController groupCallsController;\n\t\tPeerListContentDelegateSimple groupCallsDelegate;\n\n\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t};\n\n\twindow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto state = box->lifetime().make_state<State>(window);\n\n\t\tconst auto groupCalls = box->addRow(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tbox,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(box)),\n\t\t\tstyle::margins());\n\t\tgroupCalls->hide(anim::type::instant);\n\t\tgroupCalls->toggleOn(state->groupCallsController.shownValue());\n\n\t\tUi::AddSubsectionTitle(\n\t\t\tgroupCalls->entity(),\n\t\t\ttr::lng_call_box_groupcalls_subtitle());\n\t\tstate->groupCallsDelegate.setContent(groupCalls->entity()->add(\n\t\t\tobject_ptr<PeerListContent>(box, &state->groupCallsController)));\n\t\tstate->groupCallsController.setDelegate(&state->groupCallsDelegate);\n\t\tUi::AddSkip(groupCalls->entity());\n\t\tUi::AddDivider(groupCalls->entity());\n\t\tUi::AddSkip(groupCalls->entity());\n\n\t\tconst auto button = AddCreateCallButton(\n\t\t\tbox->verticalLayout(),\n\t\t\twindow,\n\t\t\tcrl::guard(box, [=] { box->closeBox(); }));\n\t\tbutton->events(\n\t\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\t\treturn (e->type() == QEvent::Enter);\n\t\t}) | rpl::on_next([=] {\n\t\t\tstate->callsDelegate.peerListMouseLeftGeometry();\n\t\t}, button->lifetime());\n\n\t\tconst auto content = box->addRow(\n\t\t\tobject_ptr<PeerListContent>(box, &state->callsController),\n\t\t\tstyle::margins());\n\t\tstate->callsDelegate.setContent(content);\n\t\tstate->callsController.setDelegate(&state->callsDelegate);\n\n\t\tbox->setWidth(state->callsController.contentWidth());\n\t\tstate->callsController.boxHeightValue(\n\t\t) | rpl::on_next([=](int height) {\n\t\t\tbox->setMinHeight(height);\n\t\t}, box->lifetime());\n\t\tbox->setTitle(tr::lng_call_box_title());\n\t\tbox->addButton(tr::lng_close(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t\tconst auto menuButton = box->addTopButton(st::infoTopBarMenu);\n\t\tmenuButton->setClickedCallback([=] {\n\t\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\tmenuButton,\n\t\t\t\tst::popupMenuWithIcons);\n\t\t\tconst auto showSettings = [=] {\n\t\t\t\twindow->showSettings(\n\t\t\t\t\tSettings::CallsId(),\n\t\t\t\t\t::Window::SectionShow(anim::type::instant));\n\t\t\t};\n\t\t\tconst auto clearAll = crl::guard(box, [=] {\n\t\t\t\tbox->uiShow()->showBox(Box(Calls::ClearCallsBox, window));\n\t\t\t});\n\t\t\tstate->menu->addAction(\n\t\t\t\ttr::lng_settings_section_call_settings(tr::now),\n\t\t\t\tshowSettings,\n\t\t\t\t&st::menuIconSettings);\n\t\t\tif (state->callsDelegate.peerListFullRowsCount() > 0) {\n\t\t\t\tUi::Menu::CreateAddActionCallback(state->menu)({\n\t\t\t\t\t.text = tr::lng_call_box_clear_all(tr::now),\n\t\t\t\t\t.handler = clearAll,\n\t\t\t\t\t.icon = &st::menuIconDeleteAttention,\n\t\t\t\t\t.isAttention = true,\n\t\t\t\t});\n\t\t\t}\n\t\t\tstate->menu->popup(QCursor::pos());\n\t\t\treturn true;\n\t\t});\n\n\t\tif (highlightStartCall) {\n\t\t\tbox->showFinishes(\n\t\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t\tSettings::HighlightWidget(button);\n\t\t\t}, box->lifetime());\n\t\t}\n\t}));\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_box_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peer_list_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"mtproto/sender.h\"\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Calls {\nnamespace GroupCalls {\n\nclass ListController : public PeerListController {\npublic:\n\texplicit ListController(not_null<::Window::SessionController*> window);\n\n\t[[nodiscard]] rpl::producer<bool> shownValue() const;\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override;\n\nprivate:\n\tconst not_null<::Window::SessionController*> _window;\n\tbase::flat_map<PeerId, not_null<PeerListRow*>> _groupCalls;\n\trpl::variable<int> _fullCount;\n\n};\n\n} // namespace GroupCalls\n\nclass BoxController : public PeerListController {\npublic:\n\texplicit BoxController(not_null<::Window::SessionController*> window);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\nprivate:\n\tvoid receivedCalls(const QVector<MTPMessage> &result);\n\tvoid refreshAbout();\n\n\tclass GroupCallRow;\n\tclass Row;\n\tRow *rowForItem(not_null<const HistoryItem*> item);\n\n\tenum class InsertWay {\n\t\tAppend,\n\t\tPrepend,\n\t};\n\tbool insertRow(not_null<HistoryItem*> item, InsertWay way);\n\tstd::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<HistoryItem*> item) const;\n\n\tconst not_null<::Window::SessionController*> _window;\n\tMTP::Sender _api;\n\n\tMsgId _offsetId = 0;\n\tint _loadRequestId = 0; // Not a real mtpRequestId.\n\tbool _allLoaded = false;\n\n};\n\nvoid ClearCallsBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<::Window::SessionController*> window);\n\nvoid ShowCallsBox(\n\tnot_null<::Window::SessionController*> window,\n\tbool highlightStartCall = false);\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_call.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/calls_call.h\"\n\n#include \"apiwrap.h\"\n#include \"base/openssl_help.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/random.h\"\n#include \"boxes/abstract_box.h\"\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/calls_instance.h\"\n#include \"calls/calls_panel.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"media/audio/media_audio_track.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"mtproto/mtproto_dh_utils.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/rate_call_box.h\"\n#include \"webrtc/webrtc_create_adm.h\"\n#include \"webrtc/webrtc_environment.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"window/window_controller.h\"\n\n#include <tgcalls/Instance.h>\n#include <tgcalls/VideoCaptureInterface.h>\n#include <tgcalls/StaticThreads.h>\n\nnamespace tgcalls {\nclass InstanceImpl;\nclass InstanceV2Impl;\nclass InstanceV2ReferenceImpl;\n} // namespace tgcalls\n\nnamespace Calls {\nnamespace {\n\nconstexpr auto kMinLayer = 65;\nconstexpr auto kHangupTimeoutMs = 5000;\nconstexpr auto kSha256Size = 32;\nconstexpr auto kAuthKeySize = 256;\nconst auto kDefaultVersion = \"2.4.4\"_q;\n\nconst auto Register = tgcalls::Register<tgcalls::InstanceImpl>();\nconst auto RegisterV2 = tgcalls::Register<tgcalls::InstanceV2Impl>();\nconst auto RegV2Ref = tgcalls::Register<tgcalls::InstanceV2ReferenceImpl>();\n\n[[nodiscard]] base::flat_set<int64> CollectEndpointIds(\n\t\tconst QVector<MTPPhoneConnection> &list) {\n\tauto result = base::flat_set<int64>();\n\tresult.reserve(list.size());\n\tfor (const auto &connection : list) {\n\t\tconnection.match([&](const MTPDphoneConnection &data) {\n\t\t\tresult.emplace(int64(data.vid().v));\n\t\t}, [](const MTPDphoneConnectionWebrtc &) {\n\t\t});\n\t}\n\treturn result;\n}\n\nvoid AppendEndpoint(\n\t\tstd::vector<tgcalls::Endpoint> &list,\n\t\tconst MTPPhoneConnection &connection) {\n\tconnection.match([&](const MTPDphoneConnection &data) {\n\t\tif (data.vpeer_tag().v.length() != 16 || data.is_tcp()) {\n\t\t\treturn;\n\t\t}\n\t\ttgcalls::Endpoint endpoint = {\n\t\t\t.endpointId = (int64_t)data.vid().v,\n\t\t\t.host = tgcalls::EndpointHost{\n\t\t\t\t.ipv4 = data.vip().v.toStdString(),\n\t\t\t\t.ipv6 = data.vipv6().v.toStdString() },\n\t\t\t.port = (uint16_t)data.vport().v,\n\t\t\t.type = tgcalls::EndpointType::UdpRelay,\n\t\t};\n\t\tconst auto tag = data.vpeer_tag().v;\n\t\tif (tag.size() >= 16) {\n\t\t\tmemcpy(endpoint.peerTag, tag.data(), 16);\n\t\t}\n\t\tlist.push_back(std::move(endpoint));\n\t}, [&](const MTPDphoneConnectionWebrtc &data) {\n\t});\n}\n\nvoid AppendServer(\n\t\tstd::vector<tgcalls::RtcServer> &list,\n\t\tconst MTPPhoneConnection &connection,\n\t\tconst base::flat_set<int64> &ids) {\n\tconnection.match([&](const MTPDphoneConnection &data) {\n\t\tconst auto hex = [](const QByteArray &value) {\n\t\t\tconst auto digit = [](uchar c) {\n\t\t\t\treturn char((c < 10) ? ('0' + c) : ('a' + c - 10));\n\t\t\t};\n\t\t\tauto result = std::string();\n\t\t\tresult.reserve(value.size() * 2);\n\t\t\tfor (const auto ch : value) {\n\t\t\t\tresult += digit(uchar(ch) / 16);\n\t\t\t\tresult += digit(uchar(ch) % 16);\n\t\t\t}\n\t\t\treturn result;\n\t\t};\n\t\tconst auto host = data.vip().v;\n\t\tconst auto hostv6 = data.vipv6().v;\n\t\tconst auto port = uint16_t(data.vport().v);\n\t\tconst auto username = std::string(\"reflector\");\n\t\tconst auto password = hex(data.vpeer_tag().v);\n\t\tconst auto i = ids.find(int64(data.vid().v));\n\t\tAssert(i != end(ids));\n\t\tconst auto id = uint8_t((i - begin(ids)) + 1);\n\t\tconst auto pushTurn = [&](const QString &host) {\n\t\t\tlist.push_back(tgcalls::RtcServer{\n\t\t\t\t.id = id,\n\t\t\t\t.host = host.toStdString(),\n\t\t\t\t.port = port,\n\t\t\t\t.login = username,\n\t\t\t\t.password = password,\n\t\t\t\t.isTurn = true,\n\t\t\t\t.isTcp = data.is_tcp(),\n\t\t\t});\n\t\t};\n\t\tpushTurn(host);\n\t\tpushTurn(hostv6);\n\t}, [&](const MTPDphoneConnectionWebrtc &data) {\n\t\tconst auto host = qs(data.vip());\n\t\tconst auto hostv6 = qs(data.vipv6());\n\t\tconst auto port = uint16_t(data.vport().v);\n\t\tif (data.is_stun()) {\n\t\t\tconst auto pushStun = [&](const QString &host) {\n\t\t\t\tif (host.isEmpty()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlist.push_back(tgcalls::RtcServer{\n\t\t\t\t\t.host = host.toStdString(),\n\t\t\t\t\t.port = port,\n\t\t\t\t\t.isTurn = false\n\t\t\t\t});\n\t\t\t};\n\t\t\tpushStun(host);\n\t\t\tpushStun(hostv6);\n\t\t}\n\t\tconst auto username = qs(data.vusername());\n\t\tconst auto password = qs(data.vpassword());\n\t\tif (data.is_turn() && !username.isEmpty() && !password.isEmpty()) {\n\t\t\tconst auto pushTurn = [&](const QString &host) {\n\t\t\t\tlist.push_back(tgcalls::RtcServer{\n\t\t\t\t\t.host = host.toStdString(),\n\t\t\t\t\t.port = port,\n\t\t\t\t\t.login = username.toStdString(),\n\t\t\t\t\t.password = password.toStdString(),\n\t\t\t\t\t.isTurn = true,\n\t\t\t\t});\n\t\t\t};\n\t\t\tpushTurn(host);\n\t\t\tpushTurn(hostv6);\n\t\t}\n\t});\n}\n\nconstexpr auto kFingerprintDataSize = 256;\nuint64 ComputeFingerprint(bytes::const_span authKey) {\n\tExpects(authKey.size() == kFingerprintDataSize);\n\n\tauto hash = openssl::Sha1(authKey);\n\treturn (gsl::to_integer<uint64>(hash[19]) << 56)\n\t\t| (gsl::to_integer<uint64>(hash[18]) << 48)\n\t\t| (gsl::to_integer<uint64>(hash[17]) << 40)\n\t\t| (gsl::to_integer<uint64>(hash[16]) << 32)\n\t\t| (gsl::to_integer<uint64>(hash[15]) << 24)\n\t\t| (gsl::to_integer<uint64>(hash[14]) << 16)\n\t\t| (gsl::to_integer<uint64>(hash[13]) << 8)\n\t\t| (gsl::to_integer<uint64>(hash[12]));\n}\n\n[[nodiscard]] QVector<MTPstring> WrapVersions(\n\t\tconst std::vector<std::string> &data) {\n\treturn ranges::views::all(\n\t\tdata\n\t) | ranges::views::transform([=](const std::string &string) {\n\t\treturn MTP_string(string);\n\t}) | ranges::to<QVector<MTPstring>>;\n}\n\n[[nodiscard]] QVector<MTPstring> CollectVersionsForApi() {\n\treturn WrapVersions(tgcalls::Meta::Versions() | ranges::actions::reverse);\n}\n\n[[nodiscard]] Webrtc::VideoState StartVideoState(bool enabled) {\n\tusing State = Webrtc::VideoState;\n\treturn enabled ? State::Active : State::Inactive;\n}\n\n} // namespace\n\nCall::Call(\n\tnot_null<Delegate*> delegate,\n\tnot_null<UserData*> user,\n\tType type,\n\tbool video)\n: _delegate(delegate)\n, _user(user)\n, _api(&_user->session().mtp())\n, _type(type)\n, _discardByTimeoutTimer([=] { hangup(); })\n, _playbackDeviceId(\n\t&Core::App().mediaDevices(),\n\tWebrtc::DeviceType::Playback,\n\tWebrtc::DeviceIdValueWithFallback(\n\t\tCore::App().settings().callPlaybackDeviceIdValue(),\n\t\tCore::App().settings().playbackDeviceIdValue()))\n, _captureDeviceId(\n\t&Core::App().mediaDevices(),\n\tWebrtc::DeviceType::Capture,\n\tWebrtc::DeviceIdValueWithFallback(\n\t\tCore::App().settings().callCaptureDeviceIdValue(),\n\t\tCore::App().settings().captureDeviceIdValue()))\n, _cameraDeviceId(\n\t&Core::App().mediaDevices(),\n\tWebrtc::DeviceType::Camera,\n\tCore::App().settings().cameraDeviceIdValue())\n, _videoIncoming(\n\tstd::make_unique<Webrtc::VideoTrack>(\n\t\tStartVideoState(video)))\n, _videoOutgoing(\n\tstd::make_unique<Webrtc::VideoTrack>(\n\t\tStartVideoState(video))) {\n\tif (_type == Type::Outgoing) {\n\t\tsetState(State::WaitingUserConfirmation);\n\t} else {\n\t\tconst auto &config = _user->session().serverConfig();\n\t\t_discardByTimeoutTimer.callOnce(config.callRingTimeoutMs);\n\t\tstartWaitingTrack();\n\t}\n\tsetupMediaDevices();\n\tsetupOutgoingVideo();\n}\n\nCall::Call(\n\tnot_null<Delegate*> delegate,\n\tnot_null<UserData*> user,\n\tCallId conferenceId,\n\tMsgId conferenceInviteMsgId,\n\tstd::vector<not_null<PeerData*>> conferenceParticipants,\n\tbool video)\n: _delegate(delegate)\n, _user(user)\n, _api(&_user->session().mtp())\n, _type(Type::Incoming)\n, _state(State::WaitingIncoming)\n, _discardByTimeoutTimer([=] { hangup(); })\n, _playbackDeviceId(\n\t&Core::App().mediaDevices(),\n\tWebrtc::DeviceType::Playback,\n\tWebrtc::DeviceIdValueWithFallback(\n\t\tCore::App().settings().callPlaybackDeviceIdValue(),\n\t\tCore::App().settings().playbackDeviceIdValue()))\n, _captureDeviceId(\n\t&Core::App().mediaDevices(),\n\tWebrtc::DeviceType::Capture,\n\tWebrtc::DeviceIdValueWithFallback(\n\t\tCore::App().settings().callCaptureDeviceIdValue(),\n\t\tCore::App().settings().captureDeviceIdValue()))\n, _cameraDeviceId(\n\t&Core::App().mediaDevices(),\n\tWebrtc::DeviceType::Camera,\n\tCore::App().settings().cameraDeviceIdValue())\n, _id(base::RandomValue<CallId>())\n, _conferenceId(conferenceId)\n, _conferenceInviteMsgId(conferenceInviteMsgId)\n, _conferenceParticipants(std::move(conferenceParticipants))\n, _videoIncoming(\n\tstd::make_unique<Webrtc::VideoTrack>(\n\t\tStartVideoState(video)))\n, _videoOutgoing(\n\tstd::make_unique<Webrtc::VideoTrack>(\n\t\tStartVideoState(video))) {\n\tstartWaitingTrack();\n\tsetupOutgoingVideo();\n}\n\nvoid Call::generateModExpFirst(bytes::const_span randomSeed) {\n\tExpects(!conferenceInvite());\n\n\tauto first = MTP::CreateModExp(_dhConfig.g, _dhConfig.p, randomSeed);\n\tif (first.modexp.empty()) {\n\t\tLOG((\"Call Error: Could not compute mod-exp first.\"));\n\t\tfinish(FinishType::Failed);\n\t\treturn;\n\t}\n\n\t_randomPower = std::move(first.randomPower);\n\tif (_type == Type::Incoming) {\n\t\t_gb = std::move(first.modexp);\n\t} else {\n\t\t_ga = std::move(first.modexp);\n\t\t_gaHash = openssl::Sha256(_ga);\n\t}\n}\n\nbool Call::isIncomingWaiting() const {\n\tif (type() != Call::Type::Incoming) {\n\t\treturn false;\n\t}\n\treturn (state() == State::Starting)\n\t\t|| (state() == State::WaitingIncoming);\n}\n\nvoid Call::start(bytes::const_span random) {\n\tExpects(!conferenceInvite());\n\n\t// Save config here, because it is possible that it changes between\n\t// different usages inside the same call.\n\t_dhConfig = _delegate->getDhConfig();\n\tAssert(_dhConfig.g != 0);\n\tAssert(!_dhConfig.p.empty());\n\n\tgenerateModExpFirst(random);\n\tconst auto state = _state.current();\n\tif (state == State::Starting || state == State::Requesting) {\n\t\tif (_type == Type::Outgoing) {\n\t\t\tstartOutgoing();\n\t\t} else {\n\t\t\tstartIncoming();\n\t\t}\n\t} else if (state == State::ExchangingKeys\n\t\t&& _answerAfterDhConfigReceived) {\n\t\tanswer();\n\t}\n}\n\nvoid Call::startOutgoing() {\n\tExpects(_type == Type::Outgoing);\n\tExpects(_state.current() == State::Requesting);\n\tExpects(_gaHash.size() == kSha256Size);\n\tExpects(!conferenceInvite());\n\n\tconst auto flags = _videoCapture\n\t\t? MTPphone_RequestCall::Flag::f_video\n\t\t: MTPphone_RequestCall::Flag(0);\n\t_api.request(MTPphone_RequestCall(\n\t\tMTP_flags(flags),\n\t\t_user->inputUser(),\n\t\tMTP_int(base::RandomValue<int32>()),\n\t\tMTP_bytes(_gaHash),\n\t\tMTP_phoneCallProtocol(\n\t\t\tMTP_flags(MTPDphoneCallProtocol::Flag::f_udp_p2p\n\t\t\t\t| MTPDphoneCallProtocol::Flag::f_udp_reflector),\n\t\t\tMTP_int(kMinLayer),\n\t\t\tMTP_int(tgcalls::Meta::MaxLayer()),\n\t\t\tMTP_vector(CollectVersionsForApi()))\n\t)).done([=](const MTPphone_PhoneCall &result) {\n\t\tExpects(result.type() == mtpc_phone_phoneCall);\n\n\t\tsetState(State::Waiting);\n\n\t\tconst auto &call = result.c_phone_phoneCall();\n\t\t_user->session().data().processUsers(call.vusers());\n\t\tif (call.vphone_call().type() != mtpc_phoneCallWaiting) {\n\t\t\tLOG((\"Call Error: Expected phoneCallWaiting in response to \"\n\t\t\t\t\"phone.requestCall()\"));\n\t\t\tfinish(FinishType::Failed);\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto &phoneCall = call.vphone_call();\n\t\tconst auto &waitingCall = phoneCall.c_phoneCallWaiting();\n\t\t_id = waitingCall.vid().v;\n\t\t_accessHash = waitingCall.vaccess_hash().v;\n\t\tif (_finishAfterRequestingCall != FinishType::None) {\n\t\t\tif (_finishAfterRequestingCall == FinishType::Failed) {\n\t\t\t\tfinish(_finishAfterRequestingCall);\n\t\t\t} else {\n\t\t\t\thangup();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto &config = _user->session().serverConfig();\n\t\t_discardByTimeoutTimer.callOnce(config.callReceiveTimeoutMs);\n\t\thandleUpdate(phoneCall);\n\t}).fail([this](const MTP::Error &error) {\n\t\thandleRequestError(error.type());\n\t}).send();\n}\n\nvoid Call::startIncoming() {\n\tExpects(_type == Type::Incoming);\n\tExpects(_state.current() == State::Starting);\n\tExpects(!conferenceInvite());\n\n\t_api.request(MTPphone_ReceivedCall(\n\t\tMTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash))\n\t)).done([=] {\n\t\tif (_state.current() == State::Starting) {\n\t\t\tsetState(State::WaitingIncoming);\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\thandleRequestError(error.type());\n\t}).send();\n}\n\nvoid Call::applyUserConfirmation() {\n\tExpects(!conferenceInvite());\n\n\tif (_state.current() == State::WaitingUserConfirmation) {\n\t\tsetState(State::Requesting);\n\t}\n}\n\nvoid Call::answer() {\n\tconst auto video = isSharingVideo();\n\t_delegate->callRequestPermissionsOrFail(crl::guard(this, [=] {\n\t\tactuallyAnswer();\n\t}), video);\n}\n\nStartConferenceInfo Call::migrateConferenceInfo(StartConferenceInfo extend) {\n\textend.migrating = true;\n\textend.muted = muted();\n\textend.videoCapture = isSharingVideo() ? _videoCapture : nullptr;\n\textend.videoCaptureScreenId = screenSharingDeviceId();\n\treturn extend;\n}\n\nvoid Call::acceptConferenceInvite() {\n\tExpects(conferenceInvite());\n\n\tif (_state.current() != State::WaitingIncoming) {\n\t\treturn;\n\t}\n\tsetState(State::ExchangingKeys);\n\tconst auto limit = 5;\n\tconst auto messageId = _conferenceInviteMsgId;\n\t_api.request(MTPphone_GetGroupCall(\n\t\tMTP_inputGroupCallInviteMessage(MTP_int(messageId.bare)),\n\t\tMTP_int(limit)\n\t)).done([=](const MTPphone_GroupCall &result) {\n\t\tresult.data().vcall().match([&](const auto &data) {\n\t\t\tauto call = _user->owner().sharedConferenceCall(\n\t\t\t\tdata.vid().v,\n\t\t\t\tdata.vaccess_hash().v);\n\t\t\tcall->processFullCall(result);\n\t\t\tCore::App().calls().startOrJoinConferenceCall(\n\t\t\t\tmigrateConferenceInfo({\n\t\t\t\t\t.call = std::move(call),\n\t\t\t\t\t.joinMessageId = messageId,\n\t\t\t\t}));\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\thandleRequestError(error.type());\n\t}).send();\n}\n\nvoid Call::actuallyAnswer() {\n\tExpects(_type == Type::Incoming);\n\n\tif (conferenceInvite()) {\n\t\tacceptConferenceInvite();\n\t\treturn;\n\t}\n\n\tconst auto state = _state.current();\n\tif (state != State::Starting && state != State::WaitingIncoming) {\n\t\tif (state != State::ExchangingKeys\n\t\t\t|| !_answerAfterDhConfigReceived) {\n\t\t\treturn;\n\t\t}\n\t}\n\tsetState(State::ExchangingKeys);\n\tif (_gb.empty()) {\n\t\t_answerAfterDhConfigReceived = true;\n\t\treturn;\n\t} else {\n\t\t_answerAfterDhConfigReceived = false;\n\t}\n\t_api.request(MTPphone_AcceptCall(\n\t\tMTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash)),\n\t\tMTP_bytes(_gb),\n\t\tMTP_phoneCallProtocol(\n\t\t\tMTP_flags(MTPDphoneCallProtocol::Flag::f_udp_p2p\n\t\t\t\t| MTPDphoneCallProtocol::Flag::f_udp_reflector),\n\t\t\tMTP_int(kMinLayer),\n\t\t\tMTP_int(tgcalls::Meta::MaxLayer()),\n\t\t\tMTP_vector(CollectVersionsForApi()))\n\t)).done([=](const MTPphone_PhoneCall &result) {\n\t\tExpects(result.type() == mtpc_phone_phoneCall);\n\n\t\tconst auto &call = result.c_phone_phoneCall();\n\t\t_user->session().data().processUsers(call.vusers());\n\t\tif (call.vphone_call().type() != mtpc_phoneCallWaiting) {\n\t\t\tLOG((\"Call Error: \"\n\t\t\t\t\"Not phoneCallWaiting in response to phone.acceptCall.\"));\n\t\t\tfinish(FinishType::Failed);\n\t\t\treturn;\n\t\t}\n\n\t\thandleUpdate(call.vphone_call());\n\t}).fail([=](const MTP::Error &error) {\n\t\thandleRequestError(error.type());\n\t}).send();\n}\n\nvoid Call::captureMuteChanged(bool mute) {\n\tsetMuted(mute);\n}\n\nrpl::producer<Webrtc::DeviceResolvedId> Call::captureMuteDeviceId() {\n\treturn _captureDeviceId.value();\n}\n\nvoid Call::setMuted(bool mute) {\n\t_muted = mute;\n\tif (_instance) {\n\t\t_instance->setMuteMicrophone(mute);\n\t}\n}\n\nvoid Call::setupMediaDevices() {\n\tExpects(!conferenceInvite());\n\n\t_playbackDeviceId.changes() | rpl::filter([=] {\n\t\treturn _instance && _setDeviceIdCallback;\n\t}) | rpl::on_next([=](const Webrtc::DeviceResolvedId &deviceId) {\n\t\t_setDeviceIdCallback(deviceId);\n\n\t\t// Value doesn't matter here, just trigger reading of the new value.\n\t\t_instance->setAudioOutputDevice(deviceId.value.toStdString());\n\t}, _lifetime);\n\n\t_captureDeviceId.changes() | rpl::filter([=] {\n\t\treturn _instance && _setDeviceIdCallback;\n\t}) | rpl::on_next([=](const Webrtc::DeviceResolvedId &deviceId) {\n\t\t_setDeviceIdCallback(deviceId);\n\n\t\t// Value doesn't matter here, just trigger reading of the new value.\n\t\t_instance->setAudioInputDevice(deviceId.value.toStdString());\n\t}, _lifetime);\n}\n\nvoid Call::setupOutgoingVideo() {\n\tconst auto cameraId = [] {\n\t\treturn Core::App().mediaDevices().defaultId(\n\t\t\tWebrtc::DeviceType::Camera);\n\t};\n\tconst auto started = _videoOutgoing->state();\n\tif (cameraId().isEmpty()) {\n\t\t_videoOutgoing->setState(Webrtc::VideoState::Inactive);\n\t}\n\t_videoOutgoing->stateValue(\n\t) | rpl::on_next([=](Webrtc::VideoState state) {\n\t\tif (state != Webrtc::VideoState::Inactive\n\t\t\t&& cameraId().isEmpty()\n\t\t\t&& !_videoCaptureIsScreencast) {\n\t\t\t_errors.fire({ ErrorType::NoCamera });\n\t\t\t_videoOutgoing->setState(Webrtc::VideoState::Inactive);\n\t\t} else if (_state.current() != State::Established\n\t\t\t&& (state != Webrtc::VideoState::Inactive)\n\t\t\t&& (started == Webrtc::VideoState::Inactive)\n\t\t\t&& !conferenceInvite()) {\n\t\t\t_errors.fire({ ErrorType::NotStartedCall });\n\t\t\t_videoOutgoing->setState(Webrtc::VideoState::Inactive);\n\t\t} else if (state != Webrtc::VideoState::Inactive\n\t\t\t&& _instance\n\t\t\t&& !_instance->supportsVideo()) {\n\t\t\t_errors.fire({ ErrorType::NotVideoCall });\n\t\t\t_videoOutgoing->setState(Webrtc::VideoState::Inactive);\n\t\t} else if (state != Webrtc::VideoState::Inactive) {\n\t\t\t// Paused not supported right now.\n\t\t\tAssert(state == Webrtc::VideoState::Active);\n\t\t\tif (!_videoCapture) {\n\t\t\t\t_videoCapture = _delegate->callGetVideoCapture(\n\t\t\t\t\t_videoCaptureDeviceId,\n\t\t\t\t\t_videoCaptureIsScreencast);\n\t\t\t\t_videoCapture->setOutput(_videoOutgoing->sink());\n\t\t\t}\n\t\t\t_videoCapture->setState(tgcalls::VideoState::Active);\n\t\t\tif (_instance) {\n\t\t\t\t_instance->setVideoCapture(_videoCapture);\n\t\t\t}\n\t\t} else if (_videoCapture) {\n\t\t\t_videoCapture->setState(tgcalls::VideoState::Inactive);\n\t\t\tif (_instance) {\n\t\t\t\t_instance->setVideoCapture(nullptr);\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n\n\t_cameraDeviceId.changes(\n\t) | rpl::filter([=] {\n\t\treturn !_videoCaptureIsScreencast;\n\t}) | rpl::on_next([=](Webrtc::DeviceResolvedId deviceId) {\n\t\tconst auto &id = deviceId.value;\n\t\t_videoCaptureDeviceId = id;\n\t\tif (_videoCapture) {\n\t\t\t_videoCapture->switchToDevice(id.toStdString(), false);\n\t\t\tif (_instance) {\n\t\t\t\t_instance->sendVideoDeviceUpdated();\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n}\n\nnot_null<Webrtc::VideoTrack*> Call::videoIncoming() const {\n\treturn _videoIncoming.get();\n}\n\nnot_null<Webrtc::VideoTrack*> Call::videoOutgoing() const {\n\treturn _videoOutgoing.get();\n}\n\ncrl::time Call::getDurationMs() const {\n\treturn _startTime ? (crl::now() - _startTime) : 0;\n}\n\nvoid Call::hangup(Data::GroupCall *migrateCall, const QString &migrateSlug) {\n\tconst auto state = _state.current();\n\tif (state == State::Busy\n\t\t|| state == State::MigrationHangingUp) {\n\t\t_delegate->callFinished(this);\n\t} else {\n\t\tconst auto missed = (state == State::Ringing\n\t\t\t|| (state == State::Waiting && _type == Type::Outgoing));\n\t\tconst auto declined = isIncomingWaiting();\n\t\tconst auto reason = !migrateSlug.isEmpty()\n\t\t\t? MTP_phoneCallDiscardReasonMigrateConferenceCall(\n\t\t\t\tMTP_string(migrateSlug))\n\t\t\t: missed\n\t\t\t? MTP_phoneCallDiscardReasonMissed()\n\t\t\t: declined\n\t\t\t? MTP_phoneCallDiscardReasonBusy()\n\t\t\t: MTP_phoneCallDiscardReasonHangup();\n\t\tfinish(FinishType::Ended, reason, migrateCall);\n\t}\n}\n\nvoid Call::redial() {\n\tExpects(!conferenceInvite());\n\n\tif (_state.current() != State::Busy) {\n\t\treturn;\n\t}\n\tAssert(_instance == nullptr);\n\t_type = Type::Outgoing;\n\tsetState(State::Requesting);\n\t_answerAfterDhConfigReceived = false;\n\tstartWaitingTrack();\n\t_delegate->callRedial(this);\n}\n\nQString Call::getDebugLog() const {\n\treturn _instance\n\t\t? QString::fromStdString(_instance->getDebugInfo())\n\t\t: QString();\n}\n\nvoid Call::startWaitingTrack() {\n\t_waitingTrack = Media::Audio::Current().createTrack();\n\tconst auto trackFileName = Core::App().settings().getSoundPath(\n\t\t(_type == Type::Outgoing)\n\t\t? u\"call_outgoing\"_q\n\t\t: u\"call_incoming\"_q);\n\t_waitingTrack->samplePeakEach(kSoundSampleMs);\n\t_waitingTrack->fillFromFile(trackFileName);\n\t_waitingTrack->playInLoop();\n}\n\nvoid Call::sendSignalingData(const QByteArray &data) {\n\tExpects(!conferenceInvite());\n\n\t_api.request(MTPphone_SendSignalingData(\n\t\tMTP_inputPhoneCall(\n\t\t\tMTP_long(_id),\n\t\t\tMTP_long(_accessHash)),\n\t\tMTP_bytes(data)\n\t)).done([=](const MTPBool &result) {\n\t\tif (!mtpIsTrue(result)) {\n\t\t\tfinish(FinishType::Failed);\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\thandleRequestError(error.type());\n\t}).send();\n}\n\nfloat64 Call::getWaitingSoundPeakValue() const {\n\tif (_waitingTrack) {\n\t\tconst auto when = crl::now() + kSoundSampleMs / 4;\n\t\treturn _waitingTrack->getPeakValue(when);\n\t}\n\treturn 0.;\n}\n\nbool Call::isKeyShaForFingerprintReady() const {\n\treturn (_keyFingerprint != 0);\n}\n\nbytes::vector Call::getKeyShaForFingerprint() const {\n\tExpects(isKeyShaForFingerprintReady());\n\tExpects(!_ga.empty());\n\n\tauto encryptedChatAuthKey = bytes::vector(\n\t\t_authKey.size() + _ga.size(),\n\t\tgsl::byte{});\n\tbytes::copy(\n\t\tgsl::make_span(encryptedChatAuthKey).subspan(0, _authKey.size()),\n\t\t_authKey);\n\tbytes::copy(\n\t\tgsl::make_span(encryptedChatAuthKey).subspan(\n\t\t\t_authKey.size(),\n\t\t\t_ga.size()),\n\t\t_ga);\n\treturn openssl::Sha256(encryptedChatAuthKey);\n}\n\nbool Call::handleUpdate(const MTPPhoneCall &call) {\n\tswitch (call.type()) {\n\tcase mtpc_phoneCallRequested: {\n\t\tconst auto &data = call.c_phoneCallRequested();\n\t\tif (_type != Type::Incoming\n\t\t\t|| _id != 0\n\t\t\t|| peerToUser(_user->id) != UserId(data.vadmin_id())) {\n\t\t\tUnexpected(\"phoneCallRequested call inside an existing call \"\n\t\t\t\t\"handleUpdate()\");\n\t\t}\n\t\tif (_user->session().userId() != UserId(data.vparticipant_id())) {\n\t\t\tLOG((\"Call Error: Wrong call participant_id %1, expected %2.\"\n\t\t\t\t).arg(data.vparticipant_id().v\n\t\t\t\t).arg(_user->session().userId().bare));\n\t\t\tfinish(FinishType::Failed);\n\t\t\treturn true;\n\t\t}\n\n\t\t_id = data.vid().v;\n\t\t_accessHash = data.vaccess_hash().v;\n\t\tconst auto gaHashBytes = bytes::make_span(data.vg_a_hash().v);\n\t\tif (gaHashBytes.size() != kSha256Size) {\n\t\t\tLOG((\"Call Error: Wrong g_a_hash size %1, expected %2.\"\n\t\t\t\t).arg(gaHashBytes.size()\n\t\t\t\t).arg(kSha256Size));\n\t\t\tfinish(FinishType::Failed);\n\t\t\treturn true;\n\t\t}\n\t\t_gaHash = bytes::make_vector(gaHashBytes);\n\t} return true;\n\n\tcase mtpc_phoneCallEmpty: {\n\t\tconst auto &data = call.c_phoneCallEmpty();\n\t\tif (data.vid().v != _id) {\n\t\t\treturn false;\n\t\t}\n\t\tLOG((\"Call Error: phoneCallEmpty received.\"));\n\t\tfinish(FinishType::Failed);\n\t} return true;\n\n\tcase mtpc_phoneCallWaiting: {\n\t\tconst auto &data = call.c_phoneCallWaiting();\n\t\tif (data.vid().v != _id) {\n\t\t\treturn false;\n\t\t}\n\t\tif (_type == Type::Outgoing\n\t\t\t&& _state.current() == State::Waiting\n\t\t\t&& data.vreceive_date().value_or_empty() != 0) {\n\t\t\tconst auto &config = _user->session().serverConfig();\n\t\t\t_discardByTimeoutTimer.callOnce(config.callRingTimeoutMs);\n\t\t\tsetState(State::Ringing);\n\t\t\tstartWaitingTrack();\n\t\t}\n\t} return true;\n\n\tcase mtpc_phoneCall: {\n\t\tconst auto &data = call.c_phoneCall();\n\t\tif (data.vid().v != _id) {\n\t\t\treturn false;\n\t\t}\n\t\tif (_type == Type::Incoming\n\t\t\t&& _state.current() == State::ExchangingKeys\n\t\t\t&& !_instance) {\n\t\t\tstartConfirmedCall(data);\n\t\t}\n\t} return true;\n\n\tcase mtpc_phoneCallDiscarded: {\n\t\tconst auto &data = call.c_phoneCallDiscarded();\n\t\tif (data.vid().v != _id) {\n\t\t\treturn false;\n\t\t}\n\t\tif (data.is_need_debug()) {\n\t\t\tconst auto debugLog = _instance\n\t\t\t\t? _instance->getDebugInfo()\n\t\t\t\t: std::string();\n\t\t\tif (!debugLog.empty()) {\n\t\t\t\tuser()->session().api().request(MTPphone_SaveCallDebug(\n\t\t\t\t\tMTP_inputPhoneCall(\n\t\t\t\t\t\tMTP_long(_id),\n\t\t\t\t\t\tMTP_long(_accessHash)),\n\t\t\t\t\tMTP_dataJSON(MTP_string(debugLog))\n\t\t\t\t)).send();\n\t\t\t}\n\t\t}\n\t\tif (data.is_need_rating() && _id && _accessHash) {\n\t\t\tconst auto window = Core::App().windowFor(\n\t\t\t\t::Window::SeparateId(_user));\n\t\t\tconst auto session = &_user->session();\n\t\t\tconst auto callId = _id;\n\t\t\tconst auto callAccessHash = _accessHash;\n\t\t\tauto owned = Box<Ui::RateCallBox>(\n\t\t\t\tCore::App().settings().sendSubmitWay());\n\t\t\tconst auto box = window\n\t\t\t\t? window->show(std::move(owned))\n\t\t\t\t: Ui::show(std::move(owned));\n\t\t\tconst auto sender = box->lifetime().make_state<MTP::Sender>(\n\t\t\t\t&session->mtp());\n\t\t\tbox->sends(\n\t\t\t) | rpl::take(\n\t\t\t\t1 // Instead of keeping requestId.\n\t\t\t) | rpl::on_next([=](const Ui::RateCallBox::Result &r) {\n\t\t\t\tsender->request(MTPphone_SetCallRating(\n\t\t\t\t\tMTP_flags(0),\n\t\t\t\t\tMTP_inputPhoneCall(\n\t\t\t\t\t\tMTP_long(callId),\n\t\t\t\t\t\tMTP_long(callAccessHash)),\n\t\t\t\t\tMTP_int(r.rating),\n\t\t\t\t\tMTP_string(r.comment)\n\t\t\t\t)).done([=](const MTPUpdates &updates) {\n\t\t\t\t\tsession->api().applyUpdates(updates);\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t}).fail([=] {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t}).send();\n\t\t\t}, box->lifetime());\n\t\t}\n\t\tconst auto reason = data.vreason();\n\t\tif (reason\n\t\t\t&& reason->type() == mtpc_phoneCallDiscardReasonDisconnect) {\n\t\t\tLOG((\"Call Info: Discarded with DISCONNECT reason.\"));\n\t\t}\n\t\tif (reason && reason->type() == mtpc_phoneCallDiscardReasonMigrateConferenceCall) {\n\t\t\tconst auto slug = qs(reason->c_phoneCallDiscardReasonMigrateConferenceCall().vslug());\n\t\t\tfinishByMigration(slug);\n\t\t} else if (reason && reason->type() == mtpc_phoneCallDiscardReasonBusy) {\n\t\t\tsetState(State::Busy);\n\t\t} else if (_type == Type::Outgoing\n\t\t\t|| _state.current() == State::HangingUp) {\n\t\t\tsetState(State::Ended);\n\t\t} else {\n\t\t\tsetState(State::EndedByOtherDevice);\n\t\t}\n\t} return true;\n\n\tcase mtpc_phoneCallAccepted: {\n\t\tconst auto &data = call.c_phoneCallAccepted();\n\t\tif (data.vid().v != _id) {\n\t\t\treturn false;\n\t\t}\n\t\tif (_type != Type::Outgoing) {\n\t\t\tLOG((\"Call Error: \"\n\t\t\t\t\"Unexpected phoneCallAccepted for an incoming call.\"));\n\t\t\tfinish(FinishType::Failed);\n\t\t} else if (checkCallFields(data)) {\n\t\t\tconfirmAcceptedCall(data);\n\t\t}\n\t} return true;\n\t}\n\n\tUnexpected(\"phoneCall type inside an existing call handleUpdate()\");\n}\n\nvoid Call::finishByMigration(const QString &slug) {\n\tExpects(!conferenceInvite());\n\n\tif (_state.current() == State::MigrationHangingUp) {\n\t\treturn;\n\t}\n\tsetState(State::MigrationHangingUp);\n\tconst auto limit = 5;\n\tconst auto session = &_user->session();\n\tsession->api().request(MTPphone_GetGroupCall(\n\t\tMTP_inputGroupCallSlug(MTP_string(slug)),\n\t\tMTP_int(limit)\n\t)).done([=](const MTPphone_GroupCall &result) {\n\t\tresult.data().vcall().match([&](const auto &data) {\n\t\t\tconst auto call = session->data().sharedConferenceCall(\n\t\t\t\tdata.vid().v,\n\t\t\t\tdata.vaccess_hash().v);\n\t\t\tcall->processFullCall(result);\n\t\t\tCore::App().calls().startOrJoinConferenceCall(\n\t\t\t\tmigrateConferenceInfo({\n\t\t\t\t\t.call = call,\n\t\t\t\t\t.linkSlug = slug,\n\t\t\t\t}));\n\t\t});\n\t}).fail(crl::guard(this, [=] {\n\t\tsetState(State::Failed);\n\t})).send();\n}\n\nvoid Call::updateRemoteMediaState(\n\t\ttgcalls::AudioState audio,\n\t\ttgcalls::VideoState video) {\n\t_remoteAudioState = [&] {\n\t\tusing From = tgcalls::AudioState;\n\t\tusing To = RemoteAudioState;\n\t\tswitch (audio) {\n\t\tcase From::Active: return To::Active;\n\t\tcase From::Muted: return To::Muted;\n\t\t}\n\t\tUnexpected(\"Audio state in remoteMediaStateUpdated.\");\n\t}();\n\t_videoIncoming->setState([&] {\n\t\tusing From = tgcalls::VideoState;\n\t\tusing To = Webrtc::VideoState;\n\t\tswitch (video) {\n\t\tcase From::Inactive: return To::Inactive;\n\t\tcase From::Paused: return To::Paused;\n\t\tcase From::Active: return To::Active;\n\t\t}\n\t\tUnexpected(\"Video state in remoteMediaStateUpdated.\");\n\t}());\n}\n\nbool Call::handleSignalingData(\n\t\tconst MTPDupdatePhoneCallSignalingData &data) {\n\tif (data.vphone_call_id().v != _id || !_instance) {\n\t\treturn false;\n\t}\n\tauto prepared = ranges::views::all(\n\t\tdata.vdata().v\n\t) | ranges::views::transform([](char byte) {\n\t\treturn static_cast<uint8_t>(byte);\n\t}) | ranges::to_vector;\n\t_instance->receiveSignalingData(std::move(prepared));\n\treturn true;\n}\n\nvoid Call::confirmAcceptedCall(const MTPDphoneCallAccepted &call) {\n\tExpects(_type == Type::Outgoing);\n\tExpects(!conferenceInvite());\n\n\tif (_state.current() == State::ExchangingKeys\n\t\t|| _instance) {\n\t\tLOG((\"Call Warning: Unexpected confirmAcceptedCall.\"));\n\t\treturn;\n\t}\n\n\tconst auto firstBytes = bytes::make_span(call.vg_b().v);\n\tconst auto computedAuthKey = MTP::CreateAuthKey(\n\t\tfirstBytes,\n\t\t_randomPower,\n\t\t_dhConfig.p);\n\tif (computedAuthKey.empty()) {\n\t\tLOG((\"Call Error: Could not compute mod-exp final.\"));\n\t\tfinish(FinishType::Failed);\n\t\treturn;\n\t}\n\n\tMTP::AuthKey::FillData(_authKey, computedAuthKey);\n\t_keyFingerprint = ComputeFingerprint(_authKey);\n\n\tsetState(State::ExchangingKeys);\n\t_api.request(MTPphone_ConfirmCall(\n\t\tMTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash)),\n\t\tMTP_bytes(_ga),\n\t\tMTP_long(_keyFingerprint),\n\t\tMTP_phoneCallProtocol(\n\t\t\tMTP_flags(MTPDphoneCallProtocol::Flag::f_udp_p2p\n\t\t\t\t| MTPDphoneCallProtocol::Flag::f_udp_reflector),\n\t\t\tMTP_int(kMinLayer),\n\t\t\tMTP_int(tgcalls::Meta::MaxLayer()),\n\t\t\tMTP_vector(CollectVersionsForApi()))\n\t)).done([=](const MTPphone_PhoneCall &result) {\n\t\tExpects(result.type() == mtpc_phone_phoneCall);\n\n\t\tconst auto &call = result.c_phone_phoneCall();\n\t\t_user->session().data().processUsers(call.vusers());\n\t\tif (call.vphone_call().type() != mtpc_phoneCall) {\n\t\t\tLOG((\"Call Error: Expected phoneCall in response to \"\n\t\t\t\t\"phone.confirmCall()\"));\n\t\t\tfinish(FinishType::Failed);\n\t\t\treturn;\n\t\t}\n\n\t\tcreateAndStartController(call.vphone_call().c_phoneCall());\n\t}).fail([=](const MTP::Error &error) {\n\t\thandleRequestError(error.type());\n\t}).send();\n}\n\nvoid Call::startConfirmedCall(const MTPDphoneCall &call) {\n\tExpects(_type == Type::Incoming);\n\tExpects(!conferenceInvite());\n\n\tconst auto firstBytes = bytes::make_span(call.vg_a_or_b().v);\n\tif (_gaHash != openssl::Sha256(firstBytes)) {\n\t\tLOG((\"Call Error: Wrong g_a hash received.\"));\n\t\tfinish(FinishType::Failed);\n\t\treturn;\n\t}\n\t_ga = bytes::vector(firstBytes.begin(), firstBytes.end());\n\n\tconst auto computedAuthKey = MTP::CreateAuthKey(\n\t\tfirstBytes,\n\t\t_randomPower,\n\t\t_dhConfig.p);\n\tif (computedAuthKey.empty()) {\n\t\tLOG((\"Call Error: Could not compute mod-exp final.\"));\n\t\tfinish(FinishType::Failed);\n\t\treturn;\n\t}\n\n\tMTP::AuthKey::FillData(_authKey, computedAuthKey);\n\t_keyFingerprint = ComputeFingerprint(_authKey);\n\n\tcreateAndStartController(call);\n}\n\nvoid Call::createAndStartController(const MTPDphoneCall &call) {\n\tExpects(!conferenceInvite());\n\n\t_discardByTimeoutTimer.cancel();\n\tif (!checkCallFields(call) || _authKey.size() != kAuthKeySize) {\n\t\treturn;\n\t}\n\n\t_conferenceSupported = call.is_conference_supported();\n\n\tconst auto &protocol = call.vprotocol().c_phoneCallProtocol();\n\tconst auto &serverConfig = _user->session().serverConfig();\n\n\tauto encryptionKeyValue = std::make_shared<std::array<\n\t\tuint8_t,\n\t\tkAuthKeySize>>();\n\tmemcpy(encryptionKeyValue->data(), _authKey.data(), kAuthKeySize);\n\n\tconst auto version = call.vprotocol().match([&](\n\t\t\tconst MTPDphoneCallProtocol &data) {\n\t\treturn data.vlibrary_versions().v;\n\t}).value(0, MTP_bytes(kDefaultVersion)).v;\n\n\tLOG((\"Call Info: Creating instance with version '%1', allowP2P: %2\").arg(\n\t\tQString::fromUtf8(version),\n\t\tLogs::b(call.is_p2p_allowed())));\n\n\tconst auto versionString = version.toStdString();\n\tconst auto &settings = Core::App().settings();\n\tconst auto weak = base::make_weak(this);\n\n\t_setDeviceIdCallback = nullptr;\n\tconst auto playbackDeviceIdInitial = _playbackDeviceId.current();\n\tconst auto captureDeviceIdInitial = _captureDeviceId.current();\n\tconst auto saveSetDeviceIdCallback = [=](\n\t\t\tFn<void(Webrtc::DeviceResolvedId)> setDeviceIdCallback) {\n\t\tsetDeviceIdCallback(playbackDeviceIdInitial);\n\t\tsetDeviceIdCallback(captureDeviceIdInitial);\n\t\tcrl::on_main(weak, [=] {\n\t\t\t_setDeviceIdCallback = std::move(setDeviceIdCallback);\n\t\t\tconst auto playback = _playbackDeviceId.current();\n\t\t\tif (_instance && playback != playbackDeviceIdInitial) {\n\t\t\t\t_setDeviceIdCallback(playback);\n\n\t\t\t\t// Value doesn't matter here, just trigger reading of the...\n\t\t\t\t_instance->setAudioOutputDevice(\n\t\t\t\t\tplayback.value.toStdString());\n\t\t\t}\n\t\t\tconst auto capture = _captureDeviceId.current();\n\t\t\tif (_instance && capture != captureDeviceIdInitial) {\n\t\t\t\t_setDeviceIdCallback(capture);\n\n\t\t\t\t// Value doesn't matter here, just trigger reading of the...\n\t\t\t\t_instance->setAudioInputDevice(capture.value.toStdString());\n\t\t\t}\n\t\t});\n\t};\n\n\ttgcalls::Descriptor descriptor = {\n\t\t.version = versionString,\n\t\t.config = tgcalls::Config{\n\t\t\t.initializationTimeout\n\t\t\t\t= serverConfig.callConnectTimeoutMs / 1000.,\n\t\t\t.receiveTimeout = serverConfig.callPacketTimeoutMs / 1000.,\n\t\t\t.dataSaving = tgcalls::DataSaving::Never,\n\t\t\t.enableP2P = call.is_p2p_allowed(),\n\t\t\t.enableAEC = false,\n\t\t\t.enableNS = true,\n\t\t\t.enableAGC = true,\n\t\t\t.enableVolumeControl = true,\n\t\t\t.maxApiLayer = protocol.vmax_layer().v,\n\t\t},\n\t\t.encryptionKey = tgcalls::EncryptionKey(\n\t\t\tstd::move(encryptionKeyValue),\n\t\t\t(_type == Type::Outgoing)),\n\t\t.mediaDevicesConfig = tgcalls::MediaDevicesConfig{\n\t\t\t.audioInputId = captureDeviceIdInitial.value.toStdString(),\n\t\t\t.audioOutputId = playbackDeviceIdInitial.value.toStdString(),\n\t\t\t.inputVolume = 1.f,//settings.callInputVolume() / 100.f,\n\t\t\t.outputVolume = 1.f,//settings.callOutputVolume() / 100.f,\n\t\t},\n\t\t.videoCapture = _videoCapture,\n\t\t.stateUpdated = [=](tgcalls::State state) {\n\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\thandleControllerStateChange(state);\n\t\t\t});\n\t\t},\n\t\t.signalBarsUpdated = [=](int count) {\n\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\thandleControllerBarCountChange(count);\n\t\t\t});\n\t\t},\n\t\t.remoteBatteryLevelIsLowUpdated = [=](bool isLow) {\n#ifdef _DEBUG\n//\t\t\tisLow = true;\n#endif\n\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\t_remoteBatteryState = isLow\n\t\t\t\t\t? RemoteBatteryState::Low\n\t\t\t\t\t: RemoteBatteryState::Normal;\n\t\t\t});\n\t\t},\n\t\t.remoteMediaStateUpdated = [=](\n\t\t\t\ttgcalls::AudioState audio,\n\t\t\t\ttgcalls::VideoState video) {\n\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\tupdateRemoteMediaState(audio, video);\n\t\t\t});\n\t\t},\n\t\t.signalingDataEmitted = [=](const std::vector<uint8_t> &data) {\n\t\t\tconst auto bytes = QByteArray(\n\t\t\t\treinterpret_cast<const char*>(data.data()),\n\t\t\t\tdata.size());\n\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\tsendSignalingData(bytes);\n\t\t\t});\n\t\t},\n\t\t.createAudioDeviceModule = Webrtc::AudioDeviceModuleCreator(\n\t\t\tsaveSetDeviceIdCallback),\n\t};\n\tif (Logs::DebugEnabled()) {\n\t\tconst auto callLogFolder = cWorkingDir() + u\"DebugLogs\"_q;\n\t\tconst auto callLogPath = callLogFolder + u\"/last_call_log.txt\"_q;\n\t\tconst auto callLogNative = QDir::toNativeSeparators(callLogPath);\n#ifdef Q_OS_WIN\n\t\tdescriptor.config.logPath.data = callLogNative.toStdWString();\n#else // Q_OS_WIN\n\t\tconst auto callLogUtf = QFile::encodeName(callLogNative);\n\t\tdescriptor.config.logPath.data.resize(callLogUtf.size());\n\t\tranges::copy(callLogUtf, descriptor.config.logPath.data.begin());\n#endif // Q_OS_WIN\n\t\tQFile(callLogPath).remove();\n\t\tQDir().mkpath(callLogFolder);\n\t}\n\n\tconst auto ids = CollectEndpointIds(call.vconnections().v);\n\tfor (const auto &connection : call.vconnections().v) {\n\t\tAppendEndpoint(descriptor.endpoints, connection);\n\t}\n\tfor (const auto &connection : call.vconnections().v) {\n\t\tAppendServer(descriptor.rtcServers, connection, ids);\n\t}\n\n\t{\n\t\tconst auto &settingsProxy = Core::App().settings().proxy();\n\t\tusing ProxyData = MTP::ProxyData;\n\t\tif (settingsProxy.useProxyForCalls() && settingsProxy.isEnabled()) {\n\t\t\tconst auto &selected = settingsProxy.selected();\n\t\t\tif (selected.supportsCalls() && !selected.host.isEmpty()) {\n\t\t\t\tAssert(selected.type == ProxyData::Type::Socks5);\n\t\t\t\tdescriptor.proxy = std::make_unique<tgcalls::Proxy>();\n\t\t\t\tdescriptor.proxy->host = selected.host.toStdString();\n\t\t\t\tdescriptor.proxy->port = selected.port;\n\t\t\t\tdescriptor.proxy->login = selected.user.toStdString();\n\t\t\t\tdescriptor.proxy->password = selected.password.toStdString();\n\t\t\t}\n\t\t}\n\t}\n\t_instance = tgcalls::Meta::Create(versionString, std::move(descriptor));\n\tif (!_instance) {\n\t\tLOG((\"Call Error: Wrong library version: %1.\"\n\t\t\t).arg(QString::fromUtf8(version)));\n\t\tfinish(FinishType::Failed);\n\t\treturn;\n\t}\n\n\tconst auto raw = _instance.get();\n\tif (_muted.current()) {\n\t\traw->setMuteMicrophone(_muted.current());\n\t}\n\n\traw->setIncomingVideoOutput(_videoIncoming->sink());\n\traw->setAudioOutputDuckingEnabled(settings.callAudioDuckingEnabled());\n\n\t_state.value() | rpl::on_next([=](State state) {\n\t\tconst auto track = (state != State::FailedHangingUp)\n\t\t\t&& (state != State::Failed)\n\t\t\t&& (state != State::HangingUp)\n\t\t\t&& (state != State::MigrationHangingUp)\n\t\t\t&& (state != State::Ended)\n\t\t\t&& (state != State::EndedByOtherDevice)\n\t\t\t&& (state != State::Busy);\n\t\tCore::App().mediaDevices().setCaptureMuteTracker(this, track);\n\t}, _instanceLifetime);\n\n\t_muted.value() | rpl::on_next([=](bool muted) {\n\t\tCore::App().mediaDevices().setCaptureMuted(muted);\n\t}, _instanceLifetime);\n\n#if 0\n\tCore::App().batterySaving().value(\n\t) | rpl::on_next([=](bool isSaving) {\n\t\tcrl::on_main(weak, [=] {\n\t\t\tif (_instance) {\n\t\t\t\t_instance->setIsLowBatteryLevel(isSaving);\n\t\t\t}\n\t\t});\n\t}, _instanceLifetime);\n#endif\n}\n\nvoid Call::handleControllerStateChange(tgcalls::State state) {\n\tExpects(!conferenceInvite());\n\n\tswitch (state) {\n\tcase tgcalls::State::WaitInit: {\n\t\tDEBUG_LOG((\"Call Info: State changed to WaitingInit.\"));\n\t\tsetState(State::WaitingInit);\n\t} break;\n\n\tcase tgcalls::State::WaitInitAck: {\n\t\tDEBUG_LOG((\"Call Info: State changed to WaitingInitAck.\"));\n\t\tsetState(State::WaitingInitAck);\n\t} break;\n\n\tcase tgcalls::State::Established: {\n\t\tDEBUG_LOG((\"Call Info: State changed to Established.\"));\n\t\tsetState(State::Established);\n\t} break;\n\n\tcase tgcalls::State::Failed: {\n\t\tconst auto error = _instance\n\t\t\t? QString::fromStdString(_instance->getLastError())\n\t\t\t: QString();\n\t\tLOG((\"Call Info: State changed to Failed, error: %1.\").arg(error));\n\t\thandleControllerError(error);\n\t} break;\n\n\tdefault: LOG((\"Call Error: Unexpected state in handleStateChange: %1\"\n\t\t).arg(int(state)));\n\t}\n}\n\nvoid Call::handleControllerBarCountChange(int count) {\n\tsetSignalBarCount(count);\n}\n\nvoid Call::setSignalBarCount(int count) {\n\t_signalBarCount = count;\n}\n\ntemplate <typename T>\nbool Call::checkCallCommonFields(const T &call) {\n\tconst auto checkFailed = [this] {\n\t\tfinish(FinishType::Failed);\n\t\treturn false;\n\t};\n\tif (call.vaccess_hash().v != _accessHash) {\n\t\tLOG((\"Call Error: Wrong call access_hash.\"));\n\t\treturn checkFailed();\n\t}\n\tconst auto adminId = (_type == Type::Outgoing)\n\t\t? _user->session().userId()\n\t\t: peerToUser(_user->id);\n\tconst auto participantId = (_type == Type::Outgoing)\n\t\t? peerToUser(_user->id)\n\t\t: _user->session().userId();\n\tif (UserId(call.vadmin_id()) != adminId) {\n\t\tLOG((\"Call Error: Wrong call admin_id %1, expected %2.\")\n\t\t\t.arg(call.vadmin_id().v)\n\t\t\t.arg(adminId.bare));\n\t\treturn checkFailed();\n\t}\n\tif (UserId(call.vparticipant_id()) != participantId) {\n\t\tLOG((\"Call Error: Wrong call participant_id %1, expected %2.\")\n\t\t\t.arg(call.vparticipant_id().v)\n\t\t\t.arg(participantId.bare));\n\t\treturn checkFailed();\n\t}\n\treturn true;\n}\n\nbool Call::checkCallFields(const MTPDphoneCall &call) {\n\tif (!checkCallCommonFields(call)) {\n\t\treturn false;\n\t}\n\tif (call.vkey_fingerprint().v != _keyFingerprint) {\n\t\tLOG((\"Call Error: Wrong call fingerprint.\"));\n\t\tfinish(FinishType::Failed);\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nbool Call::checkCallFields(const MTPDphoneCallAccepted &call) {\n\treturn checkCallCommonFields(call);\n}\n\nvoid Call::setState(State state) {\n\tconst auto was = _state.current();\n\tif (was == State::Failed) {\n\t\treturn;\n\t}\n\tif (was == State::FailedHangingUp\n\t\t&& state != State::Failed) {\n\t\treturn;\n\t}\n\tif (was == State::MigrationHangingUp\n\t\t&& state != State::Ended\n\t\t&& state != State::Failed) {\n\t\treturn;\n\t}\n\tif (was != state) {\n\t\t_state = state;\n\n\t\tif (true\n\t\t\t&& state != State::Starting\n\t\t\t&& state != State::Requesting\n\t\t\t&& state != State::Waiting\n\t\t\t&& state != State::WaitingIncoming\n\t\t\t&& state != State::Ringing) {\n\t\t\t_waitingTrack.reset();\n\t\t}\n\t\tif (false\n\t\t\t|| state == State::Ended\n\t\t\t|| state == State::EndedByOtherDevice\n\t\t\t|| state == State::Failed\n\t\t\t|| state == State::Busy) {\n\t\t\t// Destroy controller before destroying Call Panel,\n\t\t\t// so that the panel hide animation is smooth.\n\t\t\tdestroyController();\n\t\t}\n\t\tswitch (state) {\n\t\tcase State::Established:\n\t\t\t_startTime = crl::now();\n\t\t\tbreak;\n\t\tcase State::ExchangingKeys:\n\t\t\t_delegate->callPlaySound(Delegate::CallSound::Connecting);\n\t\t\tbreak;\n\t\tcase State::Ended:\n\t\t\tif (was != State::WaitingUserConfirmation) {\n\t\t\t\t_delegate->callPlaySound(Delegate::CallSound::Ended);\n\t\t\t}\n\t\t\t[[fallthrough]];\n\t\tcase State::EndedByOtherDevice:\n\t\t\t_delegate->callFinished(this);\n\t\t\tbreak;\n\t\tcase State::Failed:\n\t\t\t_delegate->callPlaySound(Delegate::CallSound::Ended);\n\t\t\t_delegate->callFailed(this);\n\t\t\tbreak;\n\t\tcase State::Busy:\n\t\t\t_delegate->callPlaySound(Delegate::CallSound::Busy);\n\t\t\t_discardByTimeoutTimer.cancel();\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n//void Call::setAudioVolume(bool input, float level) {\n//\tif (_instance) {\n//\t\tif (input) {\n//\t\t\t_instance->setInputVolume(level);\n//\t\t} else {\n//\t\t\t_instance->setOutputVolume(level);\n//\t\t}\n//\t}\n//}\n\nvoid Call::setAudioDuckingEnabled(bool enabled) {\n\tif (_instance) {\n\t\t_instance->setAudioOutputDuckingEnabled(enabled);\n\t}\n}\n\nbool Call::isSharingVideo() const {\n\treturn (_videoOutgoing->state() != Webrtc::VideoState::Inactive);\n}\n\nbool Call::isSharingCamera() const {\n\treturn !_videoCaptureIsScreencast && isSharingVideo();\n}\n\nbool Call::isSharingScreen() const {\n\treturn _videoCaptureIsScreencast && isSharingVideo();\n}\n\nQString Call::cameraSharingDeviceId() const {\n\treturn isSharingCamera() ? _videoCaptureDeviceId : QString();\n}\n\nQString Call::screenSharingDeviceId() const {\n\treturn isSharingScreen() ? _videoCaptureDeviceId : QString();\n}\n\nvoid Call::toggleCameraSharing(bool enabled) {\n\tif (isSharingCamera() == enabled) {\n\t\treturn;\n\t} else if (!enabled) {\n\t\tif (_videoCapture) {\n\t\t\t_videoCapture->setState(tgcalls::VideoState::Inactive);\n\t\t}\n\t\t_videoOutgoing->setState(Webrtc::VideoState::Inactive);\n\t\t_videoCaptureDeviceId = QString();\n\t\treturn;\n\t}\n\t_delegate->callRequestPermissionsOrFail(crl::guard(this, [=] {\n\t\ttoggleScreenSharing(std::nullopt);\n\t\t_videoCaptureDeviceId = _cameraDeviceId.current().value;\n\t\tif (_videoCapture) {\n\t\t\t_videoCapture->switchToDevice(\n\t\t\t\t_videoCaptureDeviceId.toStdString(),\n\t\t\t\tfalse);\n\t\t\tif (_instance) {\n\t\t\t\t_instance->sendVideoDeviceUpdated();\n\t\t\t}\n\t\t}\n\t\t_videoOutgoing->setState(Webrtc::VideoState::Active);\n\t}), true);\n}\n\nvoid Call::toggleScreenSharing(\n\t\tstd::optional<QString> uniqueId,\n\t\tbool withAudio) {\n\tif (!uniqueId) {\n\t\tif (isSharingScreen()) {\n\t\t\tif (_videoCapture) {\n\t\t\t\t_videoCapture->setState(tgcalls::VideoState::Inactive);\n\t\t\t}\n\t\t\t_videoOutgoing->setState(Webrtc::VideoState::Inactive);\n\t\t}\n\t\t_videoCaptureDeviceId = QString();\n\t\t_videoCaptureIsScreencast = false;\n\t\t_screenWithAudio = false;\n\t\tif (_systemAudioCapture) {\n\t\t\t_systemAudioCapture->stop();\n\t\t\t_systemAudioCapture = nullptr;\n\t\t}\n\t\treturn;\n\t} else if (screenSharingDeviceId() == *uniqueId\n\t\t&& _screenWithAudio == withAudio) {\n\t\treturn;\n\t}\n\ttoggleCameraSharing(false);\n\t_videoCaptureIsScreencast = true;\n\t_videoCaptureDeviceId = *uniqueId;\n\t_screenWithAudio = withAudio;\n\tif (_videoCapture) {\n\t\t_videoCapture->switchToDevice(uniqueId->toStdString(), true);\n\t\tif (_instance) {\n\t\t\t_instance->sendVideoDeviceUpdated();\n\t\t}\n\t}\n\t_videoOutgoing->setState(Webrtc::VideoState::Active);\n\n\tif (_systemAudioCapture) {\n\t\t_systemAudioCapture->stop();\n\t\t_systemAudioCapture = nullptr;\n\t}\n\tif (withAudio && Webrtc::SystemAudioCaptureSupported()) {\n\t\t_systemAudioCapture = Webrtc::CreateSystemAudioCapture(\n\t\t\t[weak = base::make_weak(this)](std::vector<uint8_t> &&samples) {\n\t\t\t\tcrl::on_main(\n\t\t\t\t\tweak,\n\t\t\t\t\t[weak, samples = std::move(samples)]() mutable {\n\t\t\t\t\t\tif (const auto strong = weak.get(); strong\n\t\t\t\t\t\t\t&& strong->_instance\n\t\t\t\t\t\t\t&& strong->_screenWithAudio) {\n\t\t\t\t\t\t\tstrong->_instance->addExternalAudioSamples(\n\t\t\t\t\t\t\t\tstd::move(samples));\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t});\n\t\tif (_systemAudioCapture) {\n\t\t\t_systemAudioCapture->start();\n\t\t}\n\t}\n}\n\nauto Call::peekVideoCapture() const\n-> std::shared_ptr<tgcalls::VideoCaptureInterface> {\n\treturn _videoCapture;\n}\n\nauto Call::playbackDeviceIdValue() const\n-> rpl::producer<Webrtc::DeviceResolvedId> {\n\treturn _playbackDeviceId.value();\n}\n\nrpl::producer<Webrtc::DeviceResolvedId> Call::captureDeviceIdValue() const {\n\treturn _captureDeviceId.value();\n}\n\nrpl::producer<Webrtc::DeviceResolvedId> Call::cameraDeviceIdValue() const {\n\treturn _cameraDeviceId.value();\n}\n\nvoid Call::finish(\n\t\tFinishType type,\n\t\tconst MTPPhoneCallDiscardReason &reason,\n\t\tData::GroupCall *migrateCall) {\n\tExpects(type != FinishType::None);\n\n\tsetSignalBarCount(kSignalBarFinished);\n\n\tconst auto finalState = (type == FinishType::Ended)\n\t\t? State::Ended\n\t\t: State::Failed;\n\tconst auto hangupState = (type == FinishType::Ended)\n\t\t? State::HangingUp\n\t\t: State::FailedHangingUp;\n\tconst auto state = _state.current();\n\tif (state == State::Requesting) {\n\t\t_finishByTimeoutTimer.call(kHangupTimeoutMs, [this, finalState] {\n\t\t\tsetState(finalState);\n\t\t});\n\t\t_finishAfterRequestingCall = type;\n\t\treturn;\n\t}\n\tif (state == State::HangingUp\n\t\t|| state == State::FailedHangingUp\n\t\t|| state == State::EndedByOtherDevice\n\t\t|| state == State::Ended\n\t\t|| state == State::Failed) {\n\t\treturn;\n\t} else if (conferenceInvite()) {\n\t\tif (migrateCall) {\n\t\t\t_delegate->callFinished(this);\n\t\t} else {\n\t\t\tCore::App().calls().declineIncomingConferenceInvites(_conferenceId);\n\t\t\tsetState(finalState);\n\t\t}\n\t\treturn;\n\t} else if (!_id) {\n\t\tsetState(finalState);\n\t\treturn;\n\t}\n\n\tsetState(hangupState);\n\tconst auto duration = getDurationMs() / 1000;\n\tconst auto connectionId = _instance\n\t\t? _instance->getPreferredRelayId()\n\t\t: 0;\n\t_finishByTimeoutTimer.call(kHangupTimeoutMs, [this, finalState] {\n\t\tsetState(finalState);\n\t});\n\n\tusing Video = Webrtc::VideoState;\n\tconst auto flags = ((_videoIncoming->state() != Video::Inactive)\n\t\t|| (_videoOutgoing->state() != Video::Inactive))\n\t\t? MTPphone_DiscardCall::Flag::f_video\n\t\t: MTPphone_DiscardCall::Flag(0);\n\n\t// We want to discard request still being sent and processed even if\n\t// the call is already destroyed.\n\tif (migrateCall) {\n\t\t_user->owner().registerInvitedToCallUser(\n\t\t\tmigrateCall->id(),\n\t\t\tmigrateCall,\n\t\t\t_user,\n\t\t\ttrue);\n\t}\n\tconst auto session = &_user->session();\n\tconst auto weak = base::make_weak(this);\n\tsession->api().request(MTPphone_DiscardCall( // We send 'discard' here.\n\t\tMTP_flags(flags),\n\t\tMTP_inputPhoneCall(\n\t\t\tMTP_long(_id),\n\t\t\tMTP_long(_accessHash)),\n\t\tMTP_int(duration),\n\t\treason,\n\t\tMTP_long(connectionId)\n\t)).done([=](const MTPUpdates &result) {\n\t\t// Here 'this' could be destroyed by updates, so we set Ended after\n\t\t// updates being handled, but in a guarded way.\n\t\tcrl::on_main(weak, [=] { setState(finalState); });\n\t\tsession->api().applyUpdates(result);\n\t}).fail(crl::guard(weak, [this, finalState] {\n\t\tsetState(finalState);\n\t})).send();\n}\n\nvoid Call::setStateQueued(State state) {\n\tcrl::on_main(this, [=] {\n\t\tsetState(state);\n\t});\n}\n\nvoid Call::setFailedQueued(const QString &error) {\n\tcrl::on_main(this, [=] {\n\t\thandleControllerError(error);\n\t});\n}\n\nvoid Call::handleRequestError(const QString &error) {\n\tconst auto inform = (error == u\"USER_PRIVACY_RESTRICTED\"_q)\n\t\t? tr::lng_call_error_not_available(tr::now, lt_user, _user->name())\n\t\t: (error == u\"PARTICIPANT_VERSION_OUTDATED\"_q)\n\t\t? tr::lng_call_error_outdated(tr::now, lt_user, _user->name())\n\t\t: (error == u\"CALL_PROTOCOL_LAYER_INVALID\"_q)\n\t\t? Lang::Hard::CallErrorIncompatible().replace(\n\t\t\t\"{user}\",\n\t\t\t_user->name())\n\t\t: error;\n\tif (!inform.isEmpty()) {\n\t\tif (const auto window = Core::App().windowFor(\n\t\t\t\t::Window::SeparateId(_user))) {\n\t\t\twindow->show(Ui::MakeInformBox(inform));\n\t\t} else {\n\t\t\tUi::show(Ui::MakeInformBox(inform));\n\t\t}\n\t}\n\tfinish(FinishType::Failed);\n}\n\nvoid Call::handleControllerError(const QString &error) {\n\tconst auto inform = (error == u\"ERROR_INCOMPATIBLE\"_q)\n\t\t? Lang::Hard::CallErrorIncompatible().replace(\n\t\t\t\"{user}\",\n\t\t\t_user->name())\n\t\t: (error == u\"ERROR_AUDIO_IO\"_q)\n\t\t? tr::lng_call_error_audio_io(tr::now)\n\t\t: QString();\n\tif (!inform.isEmpty()) {\n\t\tif (const auto window = Core::App().windowFor(\n\t\t\t\t::Window::SeparateId(_user))) {\n\t\t\twindow->show(Ui::MakeInformBox(inform));\n\t\t} else {\n\t\t\tUi::show(Ui::MakeInformBox(inform));\n\t\t}\n\t}\n\tfinish(FinishType::Failed);\n}\n\nvoid Call::destroyController() {\n\t_instanceLifetime.destroy();\n\tCore::App().mediaDevices().setCaptureMuteTracker(this, false);\n\tif (_systemAudioCapture) {\n\t\t_systemAudioCapture->stop();\n\t\t_systemAudioCapture = nullptr;\n\t}\n\n\tif (_instance) {\n\t\t_instance->stop([](tgcalls::FinalState) {\n\t\t});\n\n\t\tDEBUG_LOG((\"Call Info: Destroying call controller..\"));\n\t\t_instance.reset();\n\t\tDEBUG_LOG((\"Call Info: Call controller destroyed.\"));\n\t}\n\tsetSignalBarCount(kSignalBarFinished);\n}\n\nCall::~Call() {\n\tdestroyController();\n}\n\nvoid UpdateConfig(const std::string &data) {\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_call.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"base/timer.h\"\n#include \"base/bytes.h\"\n#include \"mtproto/sender.h\"\n#include \"mtproto/mtproto_auth_key.h\"\n#include \"webrtc/webrtc_device_resolver.h\"\n#include \"webrtc/webrtc_system_audio_capture.h\"\n\nnamespace Data {\nclass GroupCall;\n} // namespace Data\n\nnamespace Media {\nnamespace Audio {\nclass Track;\n} // namespace Audio\n} // namespace Media\n\nnamespace tgcalls {\nclass Instance;\nclass VideoCaptureInterface;\nenum class State;\nenum class VideoState;\nenum class AudioState;\n} // namespace tgcalls\n\nnamespace Webrtc {\nenum class VideoState;\nclass VideoTrack;\nstruct DeviceResolvedId;\n} // namespace Webrtc\n\nnamespace Calls {\n\nstruct StartConferenceInfo;\n\nstruct DhConfig {\n\tint32 version = 0;\n\tint32 g = 0;\n\tbytes::vector p;\n};\n\nenum class ErrorType {\n\tNoCamera,\n\tNoMicrophone,\n\tNotStartedCall,\n\tNotVideoCall,\n\tUnknown,\n};\n\nstruct Error {\n\tErrorType type = ErrorType::Unknown;\n\tQString details;\n};\n\nenum class CallType {\n\tIncoming,\n\tOutgoing,\n};\n\nclass Call final\n\t: public base::has_weak_ptr\n\t, private Webrtc::CaptureMuteTracker {\npublic:\n\tclass Delegate {\n\tpublic:\n\t\tvirtual DhConfig getDhConfig() const = 0;\n\t\tvirtual void callFinished(not_null<Call*> call) = 0;\n\t\tvirtual void callFailed(not_null<Call*> call) = 0;\n\t\tvirtual void callRedial(not_null<Call*> call) = 0;\n\n\t\tenum class CallSound {\n\t\t\tConnecting,\n\t\t\tBusy,\n\t\t\tEnded,\n\t\t};\n\t\tvirtual void callPlaySound(CallSound sound) = 0;\n\t\tvirtual void callRequestPermissionsOrFail(\n\t\t\tFn<void()> onSuccess,\n\t\t\tbool video) = 0;\n\n\t\tvirtual auto callGetVideoCapture(\n\t\t\tconst QString &deviceId,\n\t\t\tbool isScreenCapture)\n\t\t-> std::shared_ptr<tgcalls::VideoCaptureInterface> = 0;\n\n\t\tvirtual ~Delegate() = default;\n\n\t};\n\n\tstatic constexpr auto kSoundSampleMs = 100;\n\n\tusing Type = CallType;\n\tCall(\n\t\tnot_null<Delegate*> delegate,\n\t\tnot_null<UserData*> user,\n\t\tType type,\n\t\tbool video);\n\tCall(\n\t\tnot_null<Delegate*> delegate,\n\t\tnot_null<UserData*> user,\n\t\tCallId conferenceId,\n\t\tMsgId conferenceInviteMsgId,\n\t\tstd::vector<not_null<PeerData*>> conferenceParticipants,\n\t\tbool video);\n\n\t[[nodiscard]] Type type() const {\n\t\treturn _type;\n\t}\n\t[[nodiscard]] not_null<UserData*> user() const {\n\t\treturn _user;\n\t}\n\t[[nodiscard]] CallId id() const {\n\t\treturn _id;\n\t}\n\t[[nodiscard]] bool conferenceInvite() const {\n\t\treturn _conferenceId != 0;\n\t}\n\t[[nodiscard]] CallId conferenceId() const {\n\t\treturn _conferenceId;\n\t}\n\t[[nodiscard]] MsgId conferenceInviteMsgId() const {\n\t\treturn _conferenceInviteMsgId;\n\t}\n\t[[nodiscard]] auto conferenceParticipants() const\n\t-> const std::vector<not_null<PeerData*>> & {\n\t\treturn _conferenceParticipants;\n\t}\n\t[[nodiscard]] bool isIncomingWaiting() const;\n\n\tvoid start(bytes::const_span random);\n\tbool handleUpdate(const MTPPhoneCall &call);\n\tbool handleSignalingData(const MTPDupdatePhoneCallSignalingData &data);\n\n\tenum State {\n\t\tStarting,\n\t\tWaitingInit,\n\t\tWaitingInitAck,\n\t\tEstablished,\n\t\tFailedHangingUp,\n\t\tFailed,\n\t\tHangingUp,\n\t\tMigrationHangingUp,\n\t\tEnded,\n\t\tEndedByOtherDevice,\n\t\tExchangingKeys,\n\t\tWaiting,\n\t\tRequesting,\n\t\tWaitingIncoming,\n\t\tRinging,\n\t\tBusy,\n\t\tWaitingUserConfirmation,\n\t};\n\t[[nodiscard]] State state() const {\n\t\treturn _state.current();\n\t}\n\t[[nodiscard]] rpl::producer<State> stateValue() const {\n\t\treturn _state.value();\n\t}\n\n\t[[nodiscard]] rpl::producer<Error> errors() const {\n\t\treturn _errors.events();\n\t}\n\n\t[[nodiscard]] rpl::producer<bool> confereceSupportedValue() const {\n\t\treturn _conferenceSupported.value();\n\t}\n\n\tenum class RemoteAudioState {\n\t\tMuted,\n\t\tActive,\n\t};\n\t[[nodiscard]] RemoteAudioState remoteAudioState() const {\n\t\treturn _remoteAudioState.current();\n\t}\n\t[[nodiscard]] auto remoteAudioStateValue() const\n\t-> rpl::producer<RemoteAudioState> {\n\t\treturn _remoteAudioState.value();\n\t}\n\n\t[[nodiscard]] Webrtc::VideoState remoteVideoState() const {\n\t\treturn _remoteVideoState.current();\n\t}\n\t[[nodiscard]] auto remoteVideoStateValue() const\n\t-> rpl::producer<Webrtc::VideoState> {\n\t\treturn _remoteVideoState.value();\n\t}\n\n\tenum class RemoteBatteryState {\n\t\tLow,\n\t\tNormal,\n\t};\n\t[[nodiscard]] RemoteBatteryState remoteBatteryState() const {\n\t\treturn _remoteBatteryState.current();\n\t}\n\t[[nodiscard]] auto remoteBatteryStateValue() const\n\t-> rpl::producer<RemoteBatteryState> {\n\t\treturn _remoteBatteryState.value();\n\t}\n\n\tstatic constexpr auto kSignalBarStarting = -1;\n\tstatic constexpr auto kSignalBarFinished = -2;\n\tstatic constexpr auto kSignalBarCount = 4;\n\t[[nodiscard]] rpl::producer<int> signalBarCountValue() const {\n\t\treturn _signalBarCount.value();\n\t}\n\n\tvoid setMuted(bool mute);\n\t[[nodiscard]] bool muted() const {\n\t\treturn _muted.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> mutedValue() const {\n\t\treturn _muted.value();\n\t}\n\n\t[[nodiscard]] not_null<Webrtc::VideoTrack*> videoIncoming() const;\n\t[[nodiscard]] not_null<Webrtc::VideoTrack*> videoOutgoing() const;\n\n\tcrl::time getDurationMs() const;\n\tfloat64 getWaitingSoundPeakValue() const;\n\n\tvoid applyUserConfirmation();\n\tvoid answer();\n\tvoid hangup(\n\t\tData::GroupCall *migrateCall = nullptr,\n\t\tconst QString &migrateSlug = QString());\n\tvoid redial();\n\n\tbool isKeyShaForFingerprintReady() const;\n\tbytes::vector getKeyShaForFingerprint() const;\n\n\tQString getDebugLog() const;\n\n\t//void setAudioVolume(bool input, float level);\n\tvoid setAudioDuckingEnabled(bool enabled);\n\n\t[[nodiscard]] QString videoDeviceId() const {\n\t\treturn _videoCaptureDeviceId;\n\t}\n\n\t[[nodiscard]] bool isSharingVideo() const;\n\t[[nodiscard]] bool isSharingCamera() const;\n\t[[nodiscard]] bool isSharingScreen() const;\n\t[[nodiscard]] QString cameraSharingDeviceId() const;\n\t[[nodiscard]] QString screenSharingDeviceId() const;\n\tvoid toggleCameraSharing(bool enabled);\n\tvoid toggleScreenSharing(\n\t\tstd::optional<QString> uniqueId,\n\t\tbool withAudio = false);\n\t[[nodiscard]] bool screenSharingWithAudio() const {\n\t\treturn _screenWithAudio;\n\t}\n\t[[nodiscard]] auto peekVideoCapture() const\n\t\t-> std::shared_ptr<tgcalls::VideoCaptureInterface>;\n\n\t[[nodiscard]] auto playbackDeviceIdValue() const\n\t\t-> rpl::producer<Webrtc::DeviceResolvedId>;\n\t[[nodiscard]] auto captureDeviceIdValue() const\n\t\t-> rpl::producer<Webrtc::DeviceResolvedId>;\n\t[[nodiscard]] auto cameraDeviceIdValue() const\n\t\t-> rpl::producer<Webrtc::DeviceResolvedId>;\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\n\t~Call();\n\nprivate:\n\tenum class FinishType {\n\t\tNone,\n\t\tEnded,\n\t\tFailed,\n\t};\n\n\tvoid handleRequestError(const QString &error);\n\tvoid handleControllerError(const QString &error);\n\tvoid finish(\n\t\tFinishType type,\n\t\tconst MTPPhoneCallDiscardReason &reason\n\t\t\t= MTP_phoneCallDiscardReasonDisconnect(),\n\t\tData::GroupCall *migrateCall = nullptr);\n\tvoid finishByMigration(const QString &slug);\n\tvoid startOutgoing();\n\tvoid startIncoming();\n\tvoid startWaitingTrack();\n\tvoid sendSignalingData(const QByteArray &data);\n\n\tvoid generateModExpFirst(bytes::const_span randomSeed);\n\tvoid handleControllerStateChange(tgcalls::State state);\n\tvoid handleControllerBarCountChange(int count);\n\tvoid createAndStartController(const MTPDphoneCall &call);\n\n\ttemplate <typename T>\n\tbool checkCallCommonFields(const T &call);\n\tbool checkCallFields(const MTPDphoneCall &call);\n\tbool checkCallFields(const MTPDphoneCallAccepted &call);\n\n\tvoid actuallyAnswer();\n\tvoid acceptConferenceInvite();\n\tvoid confirmAcceptedCall(const MTPDphoneCallAccepted &call);\n\tvoid startConfirmedCall(const MTPDphoneCall &call);\n\tvoid setState(State state);\n\tvoid setStateQueued(State state);\n\tvoid setFailedQueued(const QString &error);\n\tvoid setSignalBarCount(int count);\n\tvoid destroyController();\n\n\tvoid captureMuteChanged(bool mute) override;\n\trpl::producer<Webrtc::DeviceResolvedId> captureMuteDeviceId() override;\n\n\tvoid setupMediaDevices();\n\tvoid setupOutgoingVideo();\n\tvoid updateRemoteMediaState(\n\t\ttgcalls::AudioState audio,\n\t\ttgcalls::VideoState video);\n\n\t[[nodiscard]] StartConferenceInfo migrateConferenceInfo(\n\t\tStartConferenceInfo extend);\n\n\tconst not_null<Delegate*> _delegate;\n\tconst not_null<UserData*> _user;\n\tMTP::Sender _api;\n\tType _type = Type::Outgoing;\n\trpl::variable<State> _state = State::Starting;\n\trpl::variable<bool> _conferenceSupported = false;\n\trpl::variable<RemoteAudioState> _remoteAudioState\n\t\t= RemoteAudioState::Active;\n\trpl::variable<Webrtc::VideoState> _remoteVideoState;\n\trpl::variable<RemoteBatteryState> _remoteBatteryState\n\t\t= RemoteBatteryState::Normal;\n\trpl::event_stream<Error> _errors;\n\tFinishType _finishAfterRequestingCall = FinishType::None;\n\tbool _answerAfterDhConfigReceived = false;\n\trpl::variable<int> _signalBarCount = kSignalBarStarting;\n\tcrl::time _startTime = 0;\n\tbase::DelayedCallTimer _finishByTimeoutTimer;\n\tbase::Timer _discardByTimeoutTimer;\n\n\tFn<void(Webrtc::DeviceResolvedId)> _setDeviceIdCallback;\n\tWebrtc::DeviceResolver _playbackDeviceId;\n\tWebrtc::DeviceResolver _captureDeviceId;\n\tWebrtc::DeviceResolver _cameraDeviceId;\n\n\trpl::variable<bool> _muted = false;\n\n\tDhConfig _dhConfig;\n\tbytes::vector _ga;\n\tbytes::vector _gb;\n\tbytes::vector _gaHash;\n\tbytes::vector _randomPower;\n\tMTP::AuthKey::Data _authKey;\n\n\tCallId _id = 0;\n\tuint64 _accessHash = 0;\n\tuint64 _keyFingerprint = 0;\n\n\tCallId _conferenceId = 0;\n\tMsgId _conferenceInviteMsgId = 0;\n\tstd::vector<not_null<PeerData*>> _conferenceParticipants;\n\n\tstd::unique_ptr<tgcalls::Instance> _instance;\n\tstd::shared_ptr<tgcalls::VideoCaptureInterface> _videoCapture;\n\tQString _videoCaptureDeviceId;\n\tbool _videoCaptureIsScreencast = false;\n\tbool _screenWithAudio = false;\n\tstd::unique_ptr<Webrtc::SystemAudioCapture> _systemAudioCapture;\n\tconst std::unique_ptr<Webrtc::VideoTrack> _videoIncoming;\n\tconst std::unique_ptr<Webrtc::VideoTrack> _videoOutgoing;\n\n\tstd::unique_ptr<Media::Audio::Track> _waitingTrack;\n\n\trpl::lifetime _instanceLifetime;\n\trpl::lifetime _lifetime;\n\n};\n\nvoid UpdateConfig(const std::string &data);\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/calls_controller.h\"\n\n#include \"calls/calls_controller_tgvoip.h\"\n#include \"calls/calls_controller_webrtc.h\"\n\nnamespace Calls {\n\n[[nodiscard]] std::unique_ptr<Controller> MakeController(\n\t\tconst std::string &version,\n\t\tconst TgVoipConfig &config,\n\t\tconst TgVoipPersistentState &persistentState,\n\t\tconst std::vector<TgVoipEndpoint> &endpoints,\n\t\tconst TgVoipProxy *proxy,\n\t\tTgVoipNetworkType initialNetworkType,\n\t\tconst TgVoipEncryptionKey &encryptionKey,\n\t\tFn<void(QByteArray)> sendSignalingData,\n\t\tFn<void(QImage)> displayNextFrame) {\n\tif (version == WebrtcController::Version()) {\n\t\treturn std::make_unique<WebrtcController>(\n\t\t\tconfig,\n\t\t\tpersistentState,\n\t\t\tendpoints,\n\t\t\tproxy,\n\t\t\tinitialNetworkType,\n\t\t\tencryptionKey,\n\t\t\tstd::move(sendSignalingData),\n\t\t\tstd::move(displayNextFrame));\n\t}\n\treturn std::make_unique<TgVoipController>(\n\t\tconfig,\n\t\tpersistentState,\n\t\tendpoints,\n\t\tproxy,\n\t\tinitialNetworkType,\n\t\tencryptionKey);\n}\n\nstd::vector<std::string> CollectControllerVersions() {\n\treturn { WebrtcController::Version(), TgVoipController::Version() };\n}\n\nint ControllerMaxLayer() {\n\treturn TgVoip::getConnectionMaxLayer();\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <TgVoip.h>\n\nnamespace Calls {\n\nclass Controller {\npublic:\n\tvirtual ~Controller() = default;\n\n\t[[nodiscard]] virtual std::string version() = 0;\n\n\tvirtual void setNetworkType(TgVoipNetworkType networkType) = 0;\n\tvirtual void setMuteMicrophone(bool muteMicrophone) = 0;\n\tvirtual void setAudioOutputGainControlEnabled(bool enabled) = 0;\n\tvirtual void setEchoCancellationStrength(int strength) = 0;\n\tvirtual void setAudioInputDevice(std::string id) = 0;\n\tvirtual void setAudioOutputDevice(std::string id) = 0;\n\tvirtual void setInputVolume(float level) = 0;\n\tvirtual void setOutputVolume(float level) = 0;\n\tvirtual void setAudioOutputDuckingEnabled(bool enabled) = 0;\n\tvirtual bool receiveSignalingData(const QByteArray &data) = 0;\n\n\tvirtual std::string getLastError() = 0;\n\tvirtual std::string getDebugInfo() = 0;\n\tvirtual int64_t getPreferredRelayId() = 0;\n\tvirtual TgVoipTrafficStats getTrafficStats() = 0;\n\tvirtual TgVoipPersistentState getPersistentState() = 0;\n\n\tvirtual void setOnStateUpdated(Fn<void(TgVoipState)> onStateUpdated) = 0;\n\tvirtual void setOnSignalBarsUpdated(\n\t\tFn<void(int)> onSignalBarsUpdated) = 0;\n\n\tvirtual TgVoipFinalState stop() = 0;\n\n};\n\n[[nodiscard]] std::unique_ptr<Controller> MakeController(\n\tconst std::string &version,\n\tconst TgVoipConfig &config,\n\tconst TgVoipPersistentState &persistentState,\n\tconst std::vector<TgVoipEndpoint> &endpoints,\n\tconst TgVoipProxy *proxy,\n\tTgVoipNetworkType initialNetworkType,\n\tconst TgVoipEncryptionKey &encryptionKey,\n\tFn<void(QByteArray)> sendSignalingData,\n\tFn<void(QImage)> displayNextFrame);\n\n[[nodiscard]] std::vector<std::string> CollectControllerVersions();\n[[nodiscard]] int ControllerMaxLayer();\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_controller_tgvoip.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"calls/calls_controller.h\"\n\nnamespace Calls {\n\nclass TgVoipController final : public Controller {\npublic:\n\tTgVoipController(\n\t\tconst TgVoipConfig &config,\n\t\tconst TgVoipPersistentState &persistentState,\n\t\tconst std::vector<TgVoipEndpoint> &endpoints,\n\t\tconst TgVoipProxy *proxy,\n\t\tTgVoipNetworkType initialNetworkType,\n\t\tconst TgVoipEncryptionKey &encryptionKey)\n\t: _impl(TgVoip::makeInstance(\n\t\tconfig,\n\t\tpersistentState,\n\t\tendpoints,\n\t\tproxy,\n\t\tinitialNetworkType,\n\t\tencryptionKey)) {\n\t}\n\n\t[[nodiscard]] static std::string Version() {\n\t\treturn TgVoip::getVersion();\n\t}\n\n\tstd::string version() override {\n\t\treturn Version();\n\t}\n\tvoid setNetworkType(TgVoipNetworkType networkType) override {\n\t\t_impl->setNetworkType(networkType);\n\t}\n\tvoid setMuteMicrophone(bool muteMicrophone) override {\n\t\t_impl->setMuteMicrophone(muteMicrophone);\n\t}\n\tvoid setAudioOutputGainControlEnabled(bool enabled) override {\n\t\t_impl->setAudioOutputGainControlEnabled(enabled);\n\t}\n\tvoid setEchoCancellationStrength(int strength) override {\n\t\t_impl->setEchoCancellationStrength(strength);\n\t}\n\tvoid setAudioInputDevice(std::string id) override {\n\t\t_impl->setAudioInputDevice(id);\n\t}\n\tvoid setAudioOutputDevice(std::string id) override {\n\t\t_impl->setAudioOutputDevice(id);\n\t}\n\tvoid setInputVolume(float level) override {\n\t\t_impl->setInputVolume(level);\n\t}\n\tvoid setOutputVolume(float level) override {\n\t\t_impl->setOutputVolume(level);\n\t}\n\tvoid setAudioOutputDuckingEnabled(bool enabled) override {\n\t\t_impl->setAudioOutputDuckingEnabled(enabled);\n\t}\n\tbool receiveSignalingData(const QByteArray &data) override {\n\t\treturn false;\n\t}\n\tstd::string getLastError() override {\n\t\treturn _impl->getLastError();\n\t}\n\tstd::string getDebugInfo() override {\n\t\treturn _impl->getDebugInfo();\n\t}\n\tint64_t getPreferredRelayId() override {\n\t\treturn _impl->getPreferredRelayId();\n\t}\n\tTgVoipTrafficStats getTrafficStats() override {\n\t\treturn _impl->getTrafficStats();\n\t}\n\tTgVoipPersistentState getPersistentState() override {\n\t\treturn _impl->getPersistentState();\n\t}\n\tvoid setOnStateUpdated(Fn<void(TgVoipState)> onStateUpdated) override {\n\t\t_impl->setOnStateUpdated(std::move(onStateUpdated));\n\t}\n\tvoid setOnSignalBarsUpdated(Fn<void(int)> onSignalBarsUpdated) override {\n\t\t_impl->setOnSignalBarsUpdated(std::move(onSignalBarsUpdated));\n\t}\n\tTgVoipFinalState stop() override {\n\t\treturn _impl->stop();\n\t}\n\nprivate:\n\tconst std::unique_ptr<TgVoip> _impl;\n\n};\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_controller_webrtc.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/calls_controller_webrtc.h\"\n\n#include \"webrtc/webrtc_call_context.h\"\n\nnamespace Calls {\nnamespace {\n\nusing namespace Webrtc;\n\n[[nodiscard]] CallConnectionDescription ConvertEndpoint(const TgVoipEndpoint &data) {\n\treturn CallConnectionDescription{\n\t\t.ip = QString::fromStdString(data.host.ipv4),\n\t\t.ipv6 = QString::fromStdString(data.host.ipv6),\n\t\t.peerTag = QByteArray(\n\t\t\treinterpret_cast<const char*>(data.peerTag),\n\t\t\tbase::array_size(data.peerTag)),\n\t\t.connectionId = data.endpointId,\n\t\t.port = data.port,\n\t};\n}\n\n[[nodiscard]] CallContext::Config MakeContextConfig(\n\t\tconst TgVoipConfig &config,\n\t\tconst TgVoipPersistentState &persistentState,\n\t\tconst std::vector<TgVoipEndpoint> &endpoints,\n\t\tconst TgVoipProxy *proxy,\n\t\tTgVoipNetworkType initialNetworkType,\n\t\tconst TgVoipEncryptionKey &encryptionKey,\n\t\tFn<void(QByteArray)> sendSignalingData,\n\t\tFn<void(QImage)> displayNextFrame) {\n\tExpects(!endpoints.empty());\n\n\tauto result = CallContext::Config{\n\t\t.proxy = (proxy\n\t\t\t? ProxyServer{\n\t\t\t\t.host = QString::fromStdString(proxy->host),\n\t\t\t\t.username = QString::fromStdString(proxy->login),\n\t\t\t\t.password = QString::fromStdString(proxy->password),\n\t\t\t\t.port = proxy->port }\n\t\t\t: ProxyServer()),\n\t\t.dataSaving = (config.dataSaving != TgVoipDataSaving::Never),\n\t\t.key = QByteArray(\n\t\t\treinterpret_cast<const char*>(encryptionKey.value.data()),\n\t\t\tencryptionKey.value.size()),\n\t\t.outgoing = encryptionKey.isOutgoing,\n\t\t.primary = ConvertEndpoint(endpoints.front()),\n\t\t.alternatives = endpoints | ranges::views::drop(\n\t\t\t1\n\t\t) | ranges::views::transform(ConvertEndpoint) | ranges::to_vector,\n\t\t.maxLayer = config.maxApiLayer,\n\t\t.allowP2P = config.enableP2P,\n\t\t.sendSignalingData = std::move(sendSignalingData),\n\t\t.displayNextFrame = std::move(displayNextFrame),\n\t};\n\treturn result;\n}\n\n} // namespace\n\nWebrtcController::WebrtcController(\n\tconst TgVoipConfig &config,\n\tconst TgVoipPersistentState &persistentState,\n\tconst std::vector<TgVoipEndpoint> &endpoints,\n\tconst TgVoipProxy *proxy,\n\tTgVoipNetworkType initialNetworkType,\n\tconst TgVoipEncryptionKey &encryptionKey,\n\tFn<void(QByteArray)> sendSignalingData,\n\tFn<void(QImage)> displayNextFrame)\n: _impl(std::make_unique<CallContext>(MakeContextConfig(\n\t\tconfig,\n\t\tpersistentState,\n\t\tendpoints,\n\t\tproxy,\n\t\tinitialNetworkType,\n\t\tencryptionKey,\n\t\tstd::move(sendSignalingData),\n\t\tstd::move(displayNextFrame)))) {\n}\n\nWebrtcController::~WebrtcController() = default;\n\nstd::string WebrtcController::Version() {\n\treturn CallContext::Version().toStdString();\n}\n\nstd::string WebrtcController::version() {\n\treturn Version();\n}\n\nvoid WebrtcController::setNetworkType(TgVoipNetworkType networkType) {\n}\n\nvoid WebrtcController::setMuteMicrophone(bool muteMicrophone) {\n\t_impl->setIsMuted(muteMicrophone);\n}\n\nvoid WebrtcController::setAudioOutputGainControlEnabled(bool enabled) {\n}\n\nvoid WebrtcController::setEchoCancellationStrength(int strength) {\n}\n\nvoid WebrtcController::setAudioInputDevice(std::string id) {\n}\n\nvoid WebrtcController::setAudioOutputDevice(std::string id) {\n}\n\nvoid WebrtcController::setInputVolume(float level) {\n}\n\nvoid WebrtcController::setOutputVolume(float level) {\n}\n\nvoid WebrtcController::setAudioOutputDuckingEnabled(bool enabled) {\n}\n\nbool WebrtcController::receiveSignalingData(const QByteArray &data) {\n\treturn _impl->receiveSignalingData(data);\n}\n\nstd::string WebrtcController::getLastError() {\n\treturn {};\n}\n\nstd::string WebrtcController::getDebugInfo() {\n\treturn _impl->getDebugInfo().toStdString();\n}\n\nint64_t WebrtcController::getPreferredRelayId() {\n\treturn 0;\n}\n\nTgVoipTrafficStats WebrtcController::getTrafficStats() {\n\treturn {};\n}\n\nTgVoipPersistentState WebrtcController::getPersistentState() {\n\treturn TgVoipPersistentState{};\n}\n\nvoid WebrtcController::setOnStateUpdated(\n\t\tFn<void(TgVoipState)> onStateUpdated) {\n\t_stateUpdatedLifetime.destroy();\n\t_impl->state().changes(\n\t) | rpl::on_next([=](CallState state) {\n\t\tonStateUpdated([&] {\n\t\t\tswitch (state) {\n\t\t\tcase CallState::Initializing: return TgVoipState::WaitInit;\n\t\t\tcase CallState::Reconnecting: return TgVoipState::Reconnecting;\n\t\t\tcase CallState::Connected: return TgVoipState::Established;\n\t\t\tcase CallState::Failed: return TgVoipState::Failed;\n\t\t\t}\n\t\t\tUnexpected(\"State value in Webrtc::CallContext::state.\");\n\t\t}());\n\t}, _stateUpdatedLifetime);\n}\n\nvoid WebrtcController::setOnSignalBarsUpdated(\n\tFn<void(int)> onSignalBarsUpdated) {\n}\n\nTgVoipFinalState WebrtcController::stop() {\n\t_impl->stop();\n\treturn TgVoipFinalState();\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_controller_webrtc.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"calls/calls_controller.h\"\n\nnamespace Webrtc {\nclass CallContext;\n} // namespace Webrtc\n\nnamespace Calls {\n\nclass WebrtcController final : public Controller {\npublic:\n\tWebrtcController(\n\t\tconst TgVoipConfig &config,\n\t\tconst TgVoipPersistentState &persistentState,\n\t\tconst std::vector<TgVoipEndpoint> &endpoints,\n\t\tconst TgVoipProxy *proxy,\n\t\tTgVoipNetworkType initialNetworkType,\n\t\tconst TgVoipEncryptionKey &encryptionKey,\n\t\tFn<void(QByteArray)> sendSignalingData,\n\t\tFn<void(QImage)> displayNextFrame);\n\t~WebrtcController();\n\n\t[[nodiscard]] static std::string Version();\n\n\tstd::string version() override;\n\tvoid setNetworkType(TgVoipNetworkType networkType) override;\n\tvoid setMuteMicrophone(bool muteMicrophone) override;\n\tvoid setAudioOutputGainControlEnabled(bool enabled) override;\n\tvoid setEchoCancellationStrength(int strength) override;\n\tvoid setAudioInputDevice(std::string id) override;\n\tvoid setAudioOutputDevice(std::string id) override;\n\tvoid setInputVolume(float level) override;\n\tvoid setOutputVolume(float level) override;\n\tvoid setAudioOutputDuckingEnabled(bool enabled) override;\n\tbool receiveSignalingData(const QByteArray &data) override;\n\tstd::string getLastError() override;\n\tstd::string getDebugInfo() override;\n\tint64_t getPreferredRelayId() override;\n\tTgVoipTrafficStats getTrafficStats() override;\n\tTgVoipPersistentState getPersistentState() override;\n\tvoid setOnStateUpdated(Fn<void(TgVoipState)> onStateUpdated) override;\n\tvoid setOnSignalBarsUpdated(Fn<void(int)> onSignalBarsUpdated) override;\n\tTgVoipFinalState stop() override;\n\nprivate:\n\tconst std::unique_ptr<Webrtc::CallContext> _impl;\n\n\trpl::lifetime _stateUpdatedLifetime;\n\n};\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/calls_emoji_fingerprint.h\"\n\n#include \"base/random.h\"\n#include \"calls/calls_call.h\"\n#include \"calls/calls_signal_bars.h\"\n#include \"lang/lang_keys.h\"\n#include \"data/data_user.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/abstract_button.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/painter.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_calls.h\"\n\nnamespace Calls {\nnamespace {\n\nconstexpr auto kTooltipShowTimeoutMs = crl::time(1000);\nconstexpr auto kCarouselOneDuration = crl::time(100);\nconstexpr auto kStartTimeShift = crl::time(50);\nconstexpr auto kEmojiInFingerprint = 4;\nconstexpr auto kEmojiInCarousel = 10;\n\nconst ushort Data[] = {\n0xd83d, 0xde09, 0xd83d, 0xde0d, 0xd83d, 0xde1b, 0xd83d, 0xde2d, 0xd83d, 0xde31, 0xd83d, 0xde21,\n0xd83d, 0xde0e, 0xd83d, 0xde34, 0xd83d, 0xde35, 0xd83d, 0xde08, 0xd83d, 0xde2c, 0xd83d, 0xde07,\n0xd83d, 0xde0f, 0xd83d, 0xdc6e, 0xd83d, 0xdc77, 0xd83d, 0xdc82, 0xd83d, 0xdc76, 0xd83d, 0xdc68,\n0xd83d, 0xdc69, 0xd83d, 0xdc74, 0xd83d, 0xdc75, 0xd83d, 0xde3b, 0xd83d, 0xde3d, 0xd83d, 0xde40,\n0xd83d, 0xdc7a, 0xd83d, 0xde48, 0xd83d, 0xde49, 0xd83d, 0xde4a, 0xd83d, 0xdc80, 0xd83d, 0xdc7d,\n0xd83d, 0xdca9, 0xd83d, 0xdd25, 0xd83d, 0xdca5, 0xd83d, 0xdca4, 0xd83d, 0xdc42, 0xd83d, 0xdc40,\n0xd83d, 0xdc43, 0xd83d, 0xdc45, 0xd83d, 0xdc44, 0xd83d, 0xdc4d, 0xd83d, 0xdc4e, 0xd83d, 0xdc4c,\n0xd83d, 0xdc4a, 0x270c, 0x270b, 0xd83d, 0xdc50, 0xd83d, 0xdc46, 0xd83d, 0xdc47, 0xd83d, 0xdc49,\n0xd83d, 0xdc48, 0xd83d, 0xde4f, 0xd83d, 0xdc4f, 0xd83d, 0xdcaa, 0xd83d, 0xdeb6, 0xd83c, 0xdfc3,\n0xd83d, 0xdc83, 0xd83d, 0xdc6b, 0xd83d, 0xdc6a, 0xd83d, 0xdc6c, 0xd83d, 0xdc6d, 0xd83d, 0xdc85,\n0xd83c, 0xdfa9, 0xd83d, 0xdc51, 0xd83d, 0xdc52, 0xd83d, 0xdc5f, 0xd83d, 0xdc5e, 0xd83d, 0xdc60,\n0xd83d, 0xdc55, 0xd83d, 0xdc57, 0xd83d, 0xdc56, 0xd83d, 0xdc59, 0xd83d, 0xdc5c, 0xd83d, 0xdc53,\n0xd83c, 0xdf80, 0xd83d, 0xdc84, 0xd83d, 0xdc9b, 0xd83d, 0xdc99, 0xd83d, 0xdc9c, 0xd83d, 0xdc9a,\n0xd83d, 0xdc8d, 0xd83d, 0xdc8e, 0xd83d, 0xdc36, 0xd83d, 0xdc3a, 0xd83d, 0xdc31, 0xd83d, 0xdc2d,\n0xd83d, 0xdc39, 0xd83d, 0xdc30, 0xd83d, 0xdc38, 0xd83d, 0xdc2f, 0xd83d, 0xdc28, 0xd83d, 0xdc3b,\n0xd83d, 0xdc37, 0xd83d, 0xdc2e, 0xd83d, 0xdc17, 0xd83d, 0xdc34, 0xd83d, 0xdc11, 0xd83d, 0xdc18,\n0xd83d, 0xdc3c, 0xd83d, 0xdc27, 0xd83d, 0xdc25, 0xd83d, 0xdc14, 0xd83d, 0xdc0d, 0xd83d, 0xdc22,\n0xd83d, 0xdc1b, 0xd83d, 0xdc1d, 0xd83d, 0xdc1c, 0xd83d, 0xdc1e, 0xd83d, 0xdc0c, 0xd83d, 0xdc19,\n0xd83d, 0xdc1a, 0xd83d, 0xdc1f, 0xd83d, 0xdc2c, 0xd83d, 0xdc0b, 0xd83d, 0xdc10, 0xd83d, 0xdc0a,\n0xd83d, 0xdc2b, 0xd83c, 0xdf40, 0xd83c, 0xdf39, 0xd83c, 0xdf3b, 0xd83c, 0xdf41, 0xd83c, 0xdf3e,\n0xd83c, 0xdf44, 0xd83c, 0xdf35, 0xd83c, 0xdf34, 0xd83c, 0xdf33, 0xd83c, 0xdf1e, 0xd83c, 0xdf1a,\n0xd83c, 0xdf19, 0xd83c, 0xdf0e, 0xd83c, 0xdf0b, 0x26a1, 0x2614, 0x2744, 0x26c4, 0xd83c, 0xdf00,\n0xd83c, 0xdf08, 0xd83c, 0xdf0a, 0xd83c, 0xdf93, 0xd83c, 0xdf86, 0xd83c, 0xdf83, 0xd83d, 0xdc7b,\n0xd83c, 0xdf85, 0xd83c, 0xdf84, 0xd83c, 0xdf81, 0xd83c, 0xdf88, 0xd83d, 0xdd2e, 0xd83c, 0xdfa5,\n0xd83d, 0xdcf7, 0xd83d, 0xdcbf, 0xd83d, 0xdcbb, 0x260e, 0xd83d, 0xdce1, 0xd83d, 0xdcfa, 0xd83d,\n0xdcfb, 0xd83d, 0xdd09, 0xd83d, 0xdd14, 0x23f3, 0x23f0, 0x231a, 0xd83d, 0xdd12, 0xd83d, 0xdd11,\n0xd83d, 0xdd0e, 0xd83d, 0xdca1, 0xd83d, 0xdd26, 0xd83d, 0xdd0c, 0xd83d, 0xdd0b, 0xd83d, 0xdebf,\n0xd83d, 0xdebd, 0xd83d, 0xdd27, 0xd83d, 0xdd28, 0xd83d, 0xdeaa, 0xd83d, 0xdeac, 0xd83d, 0xdca3,\n0xd83d, 0xdd2b, 0xd83d, 0xdd2a, 0xd83d, 0xdc8a, 0xd83d, 0xdc89, 0xd83d, 0xdcb0, 0xd83d, 0xdcb5,\n0xd83d, 0xdcb3, 0x2709, 0xd83d, 0xdceb, 0xd83d, 0xdce6, 0xd83d, 0xdcc5, 0xd83d, 0xdcc1, 0x2702,\n0xd83d, 0xdccc, 0xd83d, 0xdcce, 0x2712, 0x270f, 0xd83d, 0xdcd0, 0xd83d, 0xdcda, 0xd83d, 0xdd2c,\n0xd83d, 0xdd2d, 0xd83c, 0xdfa8, 0xd83c, 0xdfac, 0xd83c, 0xdfa4, 0xd83c, 0xdfa7, 0xd83c, 0xdfb5,\n0xd83c, 0xdfb9, 0xd83c, 0xdfbb, 0xd83c, 0xdfba, 0xd83c, 0xdfb8, 0xd83d, 0xdc7e, 0xd83c, 0xdfae,\n0xd83c, 0xdccf, 0xd83c, 0xdfb2, 0xd83c, 0xdfaf, 0xd83c, 0xdfc8, 0xd83c, 0xdfc0, 0x26bd, 0x26be,\n0xd83c, 0xdfbe, 0xd83c, 0xdfb1, 0xd83c, 0xdfc9, 0xd83c, 0xdfb3, 0xd83c, 0xdfc1, 0xd83c, 0xdfc7,\n0xd83c, 0xdfc6, 0xd83c, 0xdfca, 0xd83c, 0xdfc4, 0x2615, 0xd83c, 0xdf7c, 0xd83c, 0xdf7a, 0xd83c,\n0xdf77, 0xd83c, 0xdf74, 0xd83c, 0xdf55, 0xd83c, 0xdf54, 0xd83c, 0xdf5f, 0xd83c, 0xdf57, 0xd83c,\n0xdf71, 0xd83c, 0xdf5a, 0xd83c, 0xdf5c, 0xd83c, 0xdf61, 0xd83c, 0xdf73, 0xd83c, 0xdf5e, 0xd83c,\n0xdf69, 0xd83c, 0xdf66, 0xd83c, 0xdf82, 0xd83c, 0xdf70, 0xd83c, 0xdf6a, 0xd83c, 0xdf6b, 0xd83c,\n0xdf6d, 0xd83c, 0xdf6f, 0xd83c, 0xdf4e, 0xd83c, 0xdf4f, 0xd83c, 0xdf4a, 0xd83c, 0xdf4b, 0xd83c,\n0xdf52, 0xd83c, 0xdf47, 0xd83c, 0xdf49, 0xd83c, 0xdf53, 0xd83c, 0xdf51, 0xd83c, 0xdf4c, 0xd83c,\n0xdf50, 0xd83c, 0xdf4d, 0xd83c, 0xdf46, 0xd83c, 0xdf45, 0xd83c, 0xdf3d, 0xd83c, 0xdfe1, 0xd83c,\n0xdfe5, 0xd83c, 0xdfe6, 0x26ea, 0xd83c, 0xdff0, 0x26fa, 0xd83c, 0xdfed, 0xd83d, 0xddfb, 0xd83d,\n0xddfd, 0xd83c, 0xdfa0, 0xd83c, 0xdfa1, 0x26f2, 0xd83c, 0xdfa2, 0xd83d, 0xdea2, 0xd83d, 0xdea4,\n0x2693, 0xd83d, 0xde80, 0x2708, 0xd83d, 0xde81, 0xd83d, 0xde82, 0xd83d, 0xde8b, 0xd83d, 0xde8e,\n0xd83d, 0xde8c, 0xd83d, 0xde99, 0xd83d, 0xde97, 0xd83d, 0xde95, 0xd83d, 0xde9b, 0xd83d, 0xdea8,\n0xd83d, 0xde94, 0xd83d, 0xde92, 0xd83d, 0xde91, 0xd83d, 0xdeb2, 0xd83d, 0xdea0, 0xd83d, 0xde9c,\n0xd83d, 0xdea6, 0x26a0, 0xd83d, 0xdea7, 0x26fd, 0xd83c, 0xdfb0, 0xd83d, 0xddff, 0xd83c, 0xdfaa,\n0xd83c, 0xdfad, 0xd83c, 0xddef, 0xd83c, 0xddf5, 0xd83c, 0xddf0, 0xd83c, 0xddf7, 0xd83c, 0xdde9,\n0xd83c, 0xddea, 0xd83c, 0xdde8, 0xd83c, 0xddf3, 0xd83c, 0xddfa, 0xd83c, 0xddf8, 0xd83c, 0xddeb,\n0xd83c, 0xddf7, 0xd83c, 0xddea, 0xd83c, 0xddf8, 0xd83c, 0xddee, 0xd83c, 0xddf9, 0xd83c, 0xddf7,\n0xd83c, 0xddfa, 0xd83c, 0xddec, 0xd83c, 0xdde7, 0x0031, 0x20e3, 0x0032, 0x20e3, 0x0033, 0x20e3,\n0x0034, 0x20e3, 0x0035, 0x20e3, 0x0036, 0x20e3, 0x0037, 0x20e3, 0x0038, 0x20e3, 0x0039, 0x20e3,\n0x0030, 0x20e3, 0xd83d, 0xdd1f, 0x2757, 0x2753, 0x2665, 0x2666, 0xd83d, 0xdcaf, 0xd83d, 0xdd17,\n0xd83d, 0xdd31, 0xd83d, 0xdd34, 0xd83d, 0xdd35, 0xd83d, 0xdd36, 0xd83d, 0xdd37 };\n\nconst ushort Offsets[] = {\n0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22,\n24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46,\n48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70,\n72, 74, 76, 78, 80, 82, 84, 86, 87, 88, 90, 92,\n94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116,\n118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140,\n142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164,\n166, 168, 170, 172, 174, 176, 178, 180, 182, 184, 186, 188,\n190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212,\n214, 216, 218, 220, 222, 224, 226, 228, 230, 232, 234, 236,\n238, 240, 242, 244, 246, 248, 250, 252, 254, 256, 258, 259,\n260, 261, 262, 264, 266, 268, 270, 272, 274, 276, 278, 280,\n282, 284, 286, 288, 290, 292, 294, 295, 297, 299, 301, 303,\n305, 306, 307, 308, 310, 312, 314, 316, 318, 320, 322, 324,\n326, 328, 330, 332, 334, 336, 338, 340, 342, 344, 346, 348,\n350, 351, 353, 355, 357, 359, 360, 362, 364, 365, 366, 368,\n370, 372, 374, 376, 378, 380, 382, 384, 386, 388, 390, 392,\n394, 396, 398, 400, 402, 404, 406, 407, 408, 410, 412, 414,\n416, 418, 420, 422, 424, 426, 427, 429, 431, 433, 435, 437,\n439, 441, 443, 445, 447, 449, 451, 453, 455, 457, 459, 461,\n463, 465, 467, 469, 471, 473, 475, 477, 479, 481, 483, 485,\n487, 489, 491, 493, 495, 497, 499, 501, 503, 505, 507, 508,\n510, 511, 513, 515, 517, 519, 521, 522, 524, 526, 528, 529,\n531, 532, 534, 536, 538, 540, 542, 544, 546, 548, 550, 552,\n554, 556, 558, 560, 562, 564, 566, 567, 569, 570, 572, 574,\n576, 578, 582, 586, 590, 594, 598, 602, 606, 610, 614, 618,\n620, 622, 624, 626, 628, 630, 632, 634, 636, 638, 640, 641,\n642, 643, 644, 646, 648, 650, 652, 654, 656, 658 };\n\nconstexpr auto kEmojiCount = (base::array_size(Offsets) - 1);\n\nuint64 ComputeEmojiIndex(bytes::const_span bytes) {\n\tExpects(bytes.size() == 8);\n\n\treturn ((gsl::to_integer<uint64>(bytes[0]) & 0x7F) << 56)\n\t\t| (gsl::to_integer<uint64>(bytes[1]) << 48)\n\t\t| (gsl::to_integer<uint64>(bytes[2]) << 40)\n\t\t| (gsl::to_integer<uint64>(bytes[3]) << 32)\n\t\t| (gsl::to_integer<uint64>(bytes[4]) << 24)\n\t\t| (gsl::to_integer<uint64>(bytes[5]) << 16)\n\t\t| (gsl::to_integer<uint64>(bytes[6]) << 8)\n\t\t| (gsl::to_integer<uint64>(bytes[7]));\n}\n\n[[nodiscard]] EmojiPtr EmojiByIndex(int index) {\n\tExpects(index >= 0 && index < kEmojiCount);\n\n\tconst auto offset = Offsets[index];\n\tconst auto size = Offsets[index + 1] - offset;\n\tconst auto string = QString::fromRawData(\n\t\treinterpret_cast<const QChar*>(Data + offset),\n\t\tsize);\n\treturn Ui::Emoji::Find(string);\n}\n\n} // namespace\n\nstd::vector<EmojiPtr> ComputeEmojiFingerprint(not_null<Call*> call) {\n\tif (!call->isKeyShaForFingerprintReady()) {\n\t\treturn {};\n\t}\n\treturn ComputeEmojiFingerprint(call->getKeyShaForFingerprint());\n}\n\nstd::vector<EmojiPtr> ComputeEmojiFingerprint(\n\t\tbytes::const_span fingerprint) {\n\tauto result = std::vector<EmojiPtr>();\n\tconstexpr auto kPartSize = 8;\n\tfor (auto partOffset = 0\n\t\t; partOffset != fingerprint.size()\n\t\t; partOffset += kPartSize) {\n\t\tconst auto value = ComputeEmojiIndex(\n\t\t\tfingerprint.subspan(partOffset, kPartSize));\n\t\tresult.push_back(EmojiByIndex(value % kEmojiCount));\n\t}\n\treturn result;\n}\n\nbase::unique_qptr<Ui::RpWidget> CreateFingerprintAndSignalBars(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Call*> call) {\n\tclass EmojiTooltipShower final : public Ui::AbstractTooltipShower {\n\tpublic:\n\t\tEmojiTooltipShower(not_null<QWidget*> window, const QString &text)\n\t\t: _window(window)\n\t\t, _text(text) {\n\t\t}\n\n\t\tQString tooltipText() const override {\n\t\t\treturn _text;\n\t\t}\n\t\tQPoint tooltipPos() const override {\n\t\t\treturn QCursor::pos();\n\t\t}\n\t\tbool tooltipWindowActive() const override {\n\t\t\treturn _window->isActiveWindow();\n\t\t}\n\n\tprivate:\n\t\tconst not_null<QWidget*> _window;\n\t\tconst QString _text;\n\n\t};\n\n\tauto result = base::make_unique_q<Ui::RpWidget>(parent);\n\tconst auto raw = result.get();\n\n\t// Emoji tooltip.\n\tconst auto shower = raw->lifetime().make_state<EmojiTooltipShower>(\n\t\tparent->window(),\n\t\ttr::lng_call_fingerprint_tooltip(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\tcall->user()->name()));\n\traw->setMouseTracking(true);\n\traw->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::MouseMove) {\n\t\t\tUi::Tooltip::Show(kTooltipShowTimeoutMs, shower);\n\t\t} else if (e->type() == QEvent::Leave) {\n\t\t\tUi::Tooltip::Hide();\n\t\t}\n\t}, raw->lifetime());\n\n\t// Signal bars.\n\tconst auto bars = Ui::CreateChild<SignalBars>(\n\t\traw,\n\t\tcall,\n\t\tst::callPanelSignalBars);\n\tbars->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t// Geometry.\n\tconst auto print = ComputeEmojiFingerprint(call);\n\tauto realSize = Ui::Emoji::GetSizeNormal();\n\tauto size = realSize / style::DevicePixelRatio();\n\tauto count = print.size();\n\tconst auto printSize = QSize(\n\t\tcount * size + (count - 1) * st::callFingerprintSkip,\n\t\tsize);\n\tconst auto fullPrintSize = QRect(\n\t\tQPoint(),\n\t\tprintSize\n\t).marginsAdded(st::callFingerprintPadding).size();\n\tconst auto fullBarsSize = bars->rect().marginsAdded(\n\t\tst::callSignalBarsPadding\n\t).size();\n\tconst auto fullSize = QSize(\n\t\t(fullPrintSize.width()\n\t\t\t+ st::callFingerprintSignalBarsSkip\n\t\t\t+ fullBarsSize.width()),\n\t\tfullPrintSize.height());\n\traw->resize(fullSize);\n\tbars->moveToRight(\n\t\tst::callSignalBarsPadding.right(),\n\t\tst::callSignalBarsPadding.top());\n\n\t// Paint.\n\tconst auto background = raw->lifetime().make_state<QImage>(\n\t\tfullSize * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tbackground->setDevicePixelRatio(style::DevicePixelRatio());\n\trpl::merge(\n\t\trpl::single(rpl::empty),\n\t\tUi::Emoji::Updated(),\n\t\tstyle::PaletteChanged()\n\t) | rpl::on_next([=] {\n\t\tbackground->fill(Qt::transparent);\n\n\t\t// Prepare.\n\t\tauto p = QPainter(background);\n\t\tconst auto height = fullSize.height();\n\t\tconst auto fullPrintRect = QRect(QPoint(), fullPrintSize);\n\t\tconst auto fullBarsRect = QRect(\n\t\t\tfullSize.width() - fullBarsSize.width(),\n\t\t\t0,\n\t\t\tfullBarsSize.width(),\n\t\t\theight);\n\t\tconst auto bigRadius = height / 2;\n\t\tconst auto smallRadius = st::roundRadiusSmall;\n\t\tconst auto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::callBgButton);\n\n\t\t// Fingerprint part.\n\t\tp.setClipRect(0, 0, fullPrintSize.width() / 2, height);\n\t\tp.drawRoundedRect(fullPrintRect, bigRadius, bigRadius);\n\t\tp.setClipRect(fullPrintSize.width() / 2, 0, fullSize.width(), height);\n\t\tp.drawRoundedRect(fullPrintRect, smallRadius, smallRadius);\n\n\t\t// Signal bars part.\n\t\tconst auto middle = fullBarsRect.center().x();\n\t\tp.setClipRect(0, 0, middle, height);\n\t\tp.drawRoundedRect(fullBarsRect, smallRadius, smallRadius);\n\t\tp.setClipRect(middle, 0, fullBarsRect.width(), height);\n\t\tp.drawRoundedRect(fullBarsRect, bigRadius, bigRadius);\n\n\t\t// Emoji.\n\t\tconst auto realSize = Ui::Emoji::GetSizeNormal();\n\t\tconst auto size = realSize / style::DevicePixelRatio();\n\t\tauto left = st::callFingerprintPadding.left();\n\t\tconst auto top = st::callFingerprintPadding.top();\n\t\tp.setClipping(false);\n\t\tfor (const auto emoji : print) {\n\t\t\tUi::Emoji::Draw(p, emoji, realSize, left, top);\n\t\t\tleft += st::callFingerprintSkip + size;\n\t\t}\n\n\t\traw->update();\n\t}, raw->lifetime());\n\n\traw->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tQPainter(raw).drawImage(raw->rect(), *background);\n\t}, raw->lifetime());\n\n\traw->show();\n\treturn result;\n}\n\nFingerprintBadge SetupFingerprintBadge(\n\t\trpl::lifetime &on,\n\t\trpl::producer<QByteArray> fingerprint) {\n\tstruct State {\n\t\tFingerprintBadgeState data;\n\t\tUi::Animations::Basic animation;\n\t\tFn<void(crl::time)> update;\n\t\trpl::event_stream<> repaints;\n\t};\n\tconst auto state = on.make_state<State>();\n\n\tstate->data.speed = 1. / kCarouselOneDuration;\n\tstate->update = [=](crl::time now) {\n\t\t// speed-up-duration = 2 * one / speed.\n\t\tconst auto one = 1.;\n\t\tconst auto speedUpDuration = 2 * kCarouselOneDuration;\n\t\tconst auto speed0 = one / kCarouselOneDuration;\n\n\t\tauto updated = false;\n\t\tauto animating = false;\n\t\tfor (auto &entry : state->data.entries) {\n\t\t\tif (!entry.time) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tanimating = true;\n\t\t\tif (entry.time >= now) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tupdated = true;\n\t\t\tconst auto elapsed = (now - entry.time) * 1.;\n\t\t\tentry.time = now;\n\n\t\t\tAssert(!entry.emoji || entry.sliding.size() > 1);\n\t\t\tconst auto slideCount = entry.emoji\n\t\t\t\t? (int(entry.sliding.size()) - 1) * one\n\t\t\t\t: (kEmojiInCarousel + (elapsed / kCarouselOneDuration));\n\t\t\tconst auto finalPosition = slideCount * one;\n\t\t\tconst auto distance = finalPosition - entry.position;\n\n\t\t\tconst auto accelerate0 = speed0 - entry.speed;\n\t\t\tconst auto decelerate0 = speed0;\n\t\t\tconst auto acceleration0 = speed0 / speedUpDuration;\n\t\t\tconst auto taccelerate0 = accelerate0 / acceleration0;\n\t\t\tconst auto tdecelerate0 = decelerate0 / acceleration0;\n\t\t\tconst auto paccelerate0 = entry.speed * taccelerate0\n\t\t\t\t+ acceleration0 * taccelerate0 * taccelerate0 / 2.;\n\t\t\tconst auto pdecelerate0 = 0\n\t\t\t\t+ acceleration0 * tdecelerate0 * tdecelerate0 / 2.;\n\t\t\tconst auto ttozero = entry.speed / acceleration0;\n\t\t\tif (paccelerate0 + pdecelerate0 <= distance) {\n\t\t\t\t// We have time to accelerate to speed0,\n\t\t\t\t// maybe go some time on speed0 and then decelerate to 0.\n\t\t\t\tconst auto uaccelerate0 = std::min(taccelerate0, elapsed);\n\t\t\t\tconst auto left = distance - paccelerate0 - pdecelerate0;\n\t\t\t\tconst auto tconstant = left / speed0;\n\t\t\t\tconst auto uconstant = std::min(\n\t\t\t\t\ttconstant,\n\t\t\t\t\telapsed - uaccelerate0);\n\t\t\t\tconst auto udecelerate0 = std::min(\n\t\t\t\t\ttdecelerate0,\n\t\t\t\t\telapsed - uaccelerate0 - uconstant);\n\t\t\t\tif (udecelerate0 >= tdecelerate0) {\n\t\t\t\t\tAssert(entry.emoji != nullptr);\n\t\t\t\t\tentry = { .emoji = entry.emoji };\n\t\t\t\t} else {\n\t\t\t\t\tentry.position += entry.speed * uaccelerate0\n\t\t\t\t\t\t+ acceleration0 * uaccelerate0 * uaccelerate0 / 2.\n\t\t\t\t\t\t+ speed0 * uconstant\n\t\t\t\t\t\t+ speed0 * udecelerate0\n\t\t\t\t\t\t- acceleration0 * udecelerate0 * udecelerate0 / 2.;\n\t\t\t\t\tentry.speed += acceleration0\n\t\t\t\t\t\t* (uaccelerate0 - udecelerate0);\n\t\t\t\t}\n\t\t\t} else if (acceleration0 * ttozero * ttozero / 2 <= distance) {\n\t\t\t\t// We have time to accelerate at least for some time >= 0,\n\t\t\t\t// and then decelerate to 0 to make it to final position.\n\t\t\t\t//\n\t\t\t\t// peak = entry.speed + acceleration0 * t\n\t\t\t\t// tdecelerate = peak / acceleration0\n\t\t\t\t// distance = entry.speed * t\n\t\t\t\t//     + acceleration0 * t * t / 2\n\t\t\t\t//     + acceleration0 * tdecelerate * tdecelerate / 2\n\t\t\t\tconst auto det = entry.speed * entry.speed / 2\n\t\t\t\t\t+ distance * acceleration0;\n\t\t\t\tconst auto t = std::max(\n\t\t\t\t\t(sqrt(det) - entry.speed) / acceleration0,\n\t\t\t\t\t0.);\n\n\t\t\t\tconst auto taccelerate = t;\n\t\t\t\tconst auto uaccelerate = std::min(taccelerate, elapsed);\n\t\t\t\tconst auto tdecelerate = t + (entry.speed / acceleration0);\n\t\t\t\tconst auto udecelerate = std::min(\n\t\t\t\t\ttdecelerate,\n\t\t\t\t\telapsed - uaccelerate);\n\t\t\t\tif (udecelerate >= tdecelerate) {\n\t\t\t\t\tAssert(entry.emoji != nullptr);\n\t\t\t\t\tentry = { .emoji = entry.emoji };\n\t\t\t\t} else {\n\t\t\t\t\tconst auto topspeed = entry.speed\n\t\t\t\t\t\t+ acceleration0 * taccelerate;\n\t\t\t\t\tentry.position += entry.speed * uaccelerate\n\t\t\t\t\t\t+ acceleration0 * uaccelerate * uaccelerate / 2.\n\t\t\t\t\t\t+ topspeed * udecelerate\n\t\t\t\t\t\t- acceleration0 * udecelerate * udecelerate / 2.;\n\t\t\t\t\tentry.speed += acceleration0\n\t\t\t\t\t\t* (uaccelerate - udecelerate);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// We just need to decelerate to 0,\n\t\t\t\t// faster than acceleration0.\n\t\t\t\tAssert(entry.speed > 0);\n\t\t\t\tconst auto tdecelerate = 2 * distance / entry.speed;\n\t\t\t\tconst auto udecelerate = std::min(tdecelerate, elapsed);\n\t\t\t\tif (udecelerate >= tdecelerate) {\n\t\t\t\t\tAssert(entry.emoji != nullptr);\n\t\t\t\t\tentry = { .emoji = entry.emoji };\n\t\t\t\t} else {\n\t\t\t\t\tconst auto a = entry.speed / tdecelerate;\n\t\t\t\t\tentry.position += entry.speed * udecelerate\n\t\t\t\t\t\t- a * udecelerate * udecelerate / 2;\n\t\t\t\t\tentry.speed -= a * udecelerate;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (entry.position >= kEmojiInCarousel) {\n\t\t\t\tentry.position -= qFloor(entry.position / kEmojiInCarousel)\n\t\t\t\t\t* kEmojiInCarousel;\n\t\t\t}\n\t\t\twhile (entry.position >= 1.) {\n\t\t\t\tAssert(!entry.sliding.empty());\n\t\t\t\tentry.position -= 1.;\n\t\t\t\tentry.sliding.erase(begin(entry.sliding));\n\t\t\t\tif (entry.emoji && entry.sliding.size() < 2) {\n\t\t\t\t\tentry = { .emoji = entry.emoji };\n\t\t\t\t\tbreak;\n\t\t\t\t} else if (entry.sliding.empty()) {\n\t\t\t\t\tconst auto index = (entry.added++) % kEmojiInCarousel;\n\t\t\t\t\tentry.sliding.push_back(entry.carousel[index]);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!entry.emoji\n\t\t\t\t&& entry.position > 0.\n\t\t\t\t&& entry.sliding.size() < 2) {\n\t\t\t\tconst auto index = (entry.added++) % kEmojiInCarousel;\n\t\t\t\tentry.sliding.push_back(entry.carousel[index]);\n\t\t\t}\n\t\t}\n\t\tif (!animating) {\n\t\t\tstate->animation.stop();\n\t\t} else if (updated) {\n\t\t\tstate->repaints.fire({});\n\t\t}\n\t};\n\tstate->animation.init(state->update);\n\tstate->data.entries.resize(kEmojiInFingerprint);\n\n\tconst auto fillCarousel = [=](\n\t\t\tint index,\n\t\t\tbase::BufferedRandom<uint32> &buffered) {\n\t\tauto &entry = state->data.entries[index];\n\t\tauto indices = std::vector<int>();\n\t\tindices.reserve(kEmojiInCarousel);\n\t\tauto count = kEmojiCount;\n\t\tfor (auto i = 0; i != kEmojiInCarousel; ++i, --count) {\n\t\t\tauto index = base::RandomIndex(count, buffered);\n\t\t\tfor (const auto &already : indices) {\n\t\t\t\tif (index >= already) {\n\t\t\t\t\t++index;\n\t\t\t\t}\n\t\t\t}\n\t\t\tindices.push_back(index);\n\t\t}\n\n\t\tentry.carousel.clear();\n\t\tentry.carousel.reserve(kEmojiInCarousel);\n\t\tfor (const auto index : indices) {\n\t\t\tentry.carousel.push_back(EmojiByIndex(index));\n\t\t}\n\t};\n\n\tconst auto startTo = [=](\n\t\t\tint index,\n\t\t\tEmojiPtr emoji,\n\t\t\tcrl::time now,\n\t\t\tbase::BufferedRandom<uint32> &buffered) {\n\t\tauto &entry = state->data.entries[index];\n\t\tif ((entry.emoji == emoji) && (emoji || entry.time)) {\n\t\t\treturn;\n\t\t} else if (!entry.time) {\n\t\t\tAssert(entry.sliding.empty());\n\n\t\t\tif (entry.emoji) {\n\t\t\t\tentry.sliding.push_back(entry.emoji);\n\t\t\t} else if (emoji) {\n\t\t\t\t// Just initialize if we get emoji right from the start.\n\t\t\t\tentry.emoji = emoji;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tentry.time = now + index * kStartTimeShift;\n\n\t\t\tfillCarousel(index, buffered);\n\t\t}\n\t\tentry.emoji = emoji;\n\t\tif (entry.emoji) {\n\t\t\tentry.sliding.push_back(entry.emoji);\n\t\t} else {\n\t\t\tconst auto index = (entry.added++) % kEmojiInCarousel;\n\t\t\tentry.sliding.push_back(entry.carousel[index]);\n\t\t}\n\t};\n\n\tstd::move(\n\t\tfingerprint\n\t) | rpl::on_next([=](const QByteArray &fingerprint) {\n\t\tauto buffered = base::BufferedRandom<uint32>(\n\t\t\tkEmojiInCarousel * kEmojiInFingerprint);\n\t\tconst auto now = crl::now();\n\t\tconst auto emoji = (fingerprint.size() >= 32)\n\t\t\t? ComputeEmojiFingerprint(\n\t\t\t\tbytes::make_span(fingerprint).subspan(0, 32))\n\t\t\t: std::vector<EmojiPtr>();\n\t\tstate->update(now);\n\n\t\tif (emoji.size() == kEmojiInFingerprint) {\n\t\t\tfor (auto i = 0; i != kEmojiInFingerprint; ++i) {\n\t\t\t\tstartTo(i, emoji[i], now, buffered);\n\t\t\t}\n\t\t} else {\n\t\t\tfor (auto i = 0; i != kEmojiInFingerprint; ++i) {\n\t\t\t\tstartTo(i, nullptr, now, buffered);\n\t\t\t}\n\t\t}\n\t\tif (!state->animation.animating()) {\n\t\t\tstate->animation.start();\n\t\t}\n\t}, on);\n\n\treturn { .state = &state->data, .repaints = state->repaints.events() };\n}\n\nvoid SetupFingerprintTooltip(not_null<Ui::RpWidget*> widget) {\n\tstruct State {\n\t\tstd::unique_ptr<Ui::ImportantTooltip> tooltip;\n\t\tFn<void()> updateGeometry;\n\t\tFn<void(bool)> toggleTooltip;\n\t\tbool tooltipShown = false;\n\t};\n\tconst auto state = widget->lifetime().make_state<State>();\n\tstate->updateGeometry = [=] {\n\t\tif (!state->tooltip.get()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto geometry = Ui::MapFrom(\n\t\t\twidget->window(),\n\t\t\twidget,\n\t\t\twidget->rect());\n\t\tif (geometry.isEmpty()) {\n\t\t\tstate->toggleTooltip(false);\n\t\t\treturn;\n\t\t}\n\t\tconst auto weak = QPointer<QWidget>(state->tooltip.get());\n\t\tconst auto countPosition = [=](QSize size) {\n\t\t\tconst auto result = geometry.bottomLeft()\n\t\t\t\t+ QPoint(\n\t\t\t\t\tgeometry.width() / 2,\n\t\t\t\t\tst::confcallFingerprintTooltipSkip)\n\t\t\t\t- QPoint(size.width() / 2, 0);\n\t\t\treturn result;\n\t\t};\n\t\tstate->tooltip.get()->pointAt(\n\t\t\tgeometry,\n\t\t\tRectPart::Bottom,\n\t\t\tcountPosition);\n\t};\n\tstate->toggleTooltip = [=](bool show) {\n\t\tif (const auto was = state->tooltip.release()) {\n\t\t\twas->toggleAnimated(false);\n\t\t}\n\t\tif (!show) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto text = tr::lng_confcall_e2e_about(\n\t\t\ttr::now,\n\t\t\ttr::marked);\n\t\tif (text.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tstate->tooltip = std::make_unique<Ui::ImportantTooltip>(\n\t\t\twidget->window(),\n\t\t\tUi::MakeNiceTooltipLabel(\n\t\t\t\twidget,\n\t\t\t\trpl::single(text),\n\t\t\t\tst::confcallFingerprintTooltipMaxWidth,\n\t\t\t\tst::confcallFingerprintTooltipLabel),\n\t\t\tst::confcallFingerprintTooltip);\n\t\tconst auto raw = state->tooltip.get();\n\t\tconst auto weak = base::make_weak(raw);\n\t\tconst auto destroy = [=] {\n\t\t\tdelete weak.get();\n\t\t};\n\t\traw->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\traw->setHiddenCallback(destroy);\n\t\tstate->updateGeometry();\n\t\traw->toggleAnimated(true);\n\t};\n\n\twidget->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::Enter) {\n\t\t\t// Enter events may come from widget destructors,\n\t\t\t// in that case sync-showing tooltip (calling Grab)\n\t\t\t// crashes the whole thing.\n\t\t\tstate->tooltipShown = true;\n\t\t\tcrl::on_main(widget, [=] {\n\t\t\t\tif (state->tooltipShown) {\n\t\t\t\t\tstate->toggleTooltip(true);\n\t\t\t\t}\n\t\t\t});\n\t\t} else if (type == QEvent::Leave) {\n\t\t\tstate->tooltipShown = false;\n\t\t\tcrl::on_main(widget, [=] {\n\t\t\t\tif (!state->tooltipShown) {\n\t\t\t\t\tstate->toggleTooltip(false);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}, widget->lifetime());\n}\n\nQImage MakeVerticalShadow(int height) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto result = QImage(\n\t\tQSize(1, height) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\tauto p = QPainter(&result);\n\tauto g = QLinearGradient(0, 0, 0, height);\n\tauto color = st::groupCallMembersBg->c;\n\tauto trans = color;\n\ttrans.setAlpha(0);\n\tg.setStops({\n\t\t{ 0.0, color },\n\t\t{ 0.4, trans },\n\t\t{ 0.6, trans },\n\t\t{ 1.0, color },\n\t});\n\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\tp.fillRect(0, 0, 1, height, g);\n\tp.end();\n\n\treturn result;\n}\n\nvoid SetupFingerprintBadgeWidget(\n\t\tnot_null<Ui::RpWidget*> widget,\n\t\tnot_null<const FingerprintBadgeState*> state,\n\t\trpl::producer<> repaints) {\n\tauto &lifetime = widget->lifetime();\n\n\tconst auto button = Ui::CreateChild<Ui::RpWidget>(widget);\n\tbutton->show();\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton,\n\t\tQString(),\n\t\tst::confcallFingerprintText);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tlabel->show();\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto esize = Ui::Emoji::GetSizeNormal();\n\tconst auto size = esize / ratio;\n\twidget->widthValue() | rpl::on_next([=](int width) {\n\t\tstatic_assert(!(kEmojiInFingerprint % 2));\n\n\t\tconst auto available = width\n\t\t\t- st::confcallFingerprintMargins.left()\n\t\t\t- st::confcallFingerprintMargins.right()\n\t\t\t- (kEmojiInFingerprint * size)\n\t\t\t- (kEmojiInFingerprint - 2) * st::confcallFingerprintSkip\n\t\t\t- st::confcallFingerprintTextMargins.left()\n\t\t\t- st::confcallFingerprintTextMargins.right();\n\t\tif (available <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tlabel->setText(tr::lng_confcall_e2e_badge(tr::now));\n\t\tif (label->textMaxWidth() > available) {\n\t\t\tlabel->setText(tr::lng_confcall_e2e_badge_small(tr::now));\n\t\t}\n\t\tconst auto use = std::min(available, label->textMaxWidth());\n\t\tlabel->resizeToWidth(use);\n\n\t\tconst auto ontheleft = kEmojiInFingerprint / 2;\n\t\tconst auto ontheside = ontheleft * size\n\t\t\t+ (ontheleft - 1) * st::confcallFingerprintSkip;\n\t\tconst auto text = QRect(\n\t\t\t(width - use) / 2,\n\t\t\t(st::confcallFingerprintMargins.top()\n\t\t\t\t+ st::confcallFingerprintTextMargins.top()),\n\t\t\tuse,\n\t\t\tlabel->height());\n\t\tconst auto textOuter = text.marginsAdded(\n\t\t\tst::confcallFingerprintTextMargins);\n\t\tconst auto withEmoji = QRect(\n\t\t\ttextOuter.x() - ontheside,\n\t\t\ttextOuter.y(),\n\t\t\ttextOuter.width() + ontheside * 2,\n\t\t\tsize);\n\t\tconst auto outer = withEmoji.marginsAdded(\n\t\t\tst::confcallFingerprintMargins);\n\n\t\tbutton->setGeometry(outer);\n\t\tlabel->moveToLeft(text.x() - outer.x(), text.y() - outer.y(), width);\n\n\t\twidget->resize(\n\t\t\twidth,\n\t\t\tbutton->height() + st::confcallFingerprintBottomSkip);\n\t}, lifetime);\n\n\tconst auto cache = lifetime.make_state<FingerprintBadgeCache>();\n\tbutton->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(button);\n\n\t\tconst auto outer = button->rect();\n\t\tconst auto radius = outer.height() / 2.;\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::groupCallMembersBg);\n\t\tp.drawRoundedRect(outer, radius, radius);\n\t\tp.setClipRect(outer);\n\n\t\tconst auto withEmoji = outer.marginsRemoved(\n\t\t\tst::confcallFingerprintMargins);\n\t\tp.translate(withEmoji.topLeft());\n\n\t\tconst auto text = label->geometry();\n\t\tconst auto textOuter = text.marginsAdded(\n\t\t\tst::confcallFingerprintTextMargins);\n\t\tconst auto count = int(state->entries.size());\n\t\tcache->entries.resize(count);\n\t\tcache->shadow = MakeVerticalShadow(outer.height());\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tconst auto &entry = state->entries[i];\n\t\t\tauto &cached = cache->entries[i];\n\t\t\tconst auto shadowed = entry.speed / state->speed;\n\t\t\tPaintFingerprintEntry(p, entry, cached, esize);\n\t\t\tif (shadowed > 0.) {\n\t\t\t\tp.setOpacity(shadowed);\n\t\t\t\tp.drawImage(\n\t\t\t\t\tQRect(0, -st::confcallFingerprintMargins.top(), size, outer.height()),\n\t\t\t\t\tcache->shadow);\n\t\t\t\tp.setOpacity(1.);\n\t\t\t}\n\t\t\tif (i + 1 == count / 2) {\n\t\t\t\tp.translate(size + textOuter.width(), 0);\n\t\t\t} else {\n\t\t\t\tp.translate(size + st::confcallFingerprintSkip, 0);\n\t\t\t}\n\t\t}\n\t}, lifetime);\n\n\tstd::move(repaints) | rpl::on_next([=] {\n\t\tbutton->update();\n\t}, lifetime);\n\n\tSetupFingerprintTooltip(button);\n}\n\nvoid PaintFingerprintEntry(\n\t\tQPainter &p,\n\t\tconst FingerprintBadgeState::Entry &entry,\n\t\tFingerprintBadgeCache::Entry &cache,\n\t\tint esize) {\n\tconst auto stationary = !entry.time;\n\tif (stationary) {\n\t\tUi::Emoji::Draw(p, entry.emoji, esize, 0, 0);\n\t\treturn;\n\t}\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto size = esize / ratio;\n\tconst auto add = 4;\n\tconst auto height = size + 2 * add;\n\tconst auto validateCache = [&](int index, EmojiPtr e) {\n\t\tif (cache.emoji.size() <= index) {\n\t\t\tcache.emoji.reserve(entry.carousel.size() + 2);\n\t\t\tcache.emoji.resize(index + 1);\n\t\t}\n\t\tauto &emoji = cache.emoji[index];\n\t\tif (emoji.ptr != e) {\n\t\t\temoji.ptr = e;\n\t\t\temoji.image = QImage(\n\t\t\t\tQSize(size, height) * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\temoji.image.setDevicePixelRatio(ratio);\n\t\t\temoji.image.fill(Qt::transparent);\n\t\t\tauto q = QPainter(&emoji.image);\n\t\t\tUi::Emoji::Draw(q, e, esize, 0, add);\n\t\t\tq.end();\n\n\t\t\t//emoji.image = Images::Blur(\n\t\t\t//\tstd::move(emoji.image),\n\t\t\t//\tfalse,\n\t\t\t//\tQt::Vertical);\n\t\t}\n\t\treturn &emoji;\n\t};\n\tauto shift = entry.position * height - add;\n\tp.translate(0, shift);\n\tfor (const auto &e : entry.sliding) {\n\t\tconst auto index = [&] {\n\t\t\tconst auto i = ranges::find(entry.carousel, e);\n\t\t\tif (i != end(entry.carousel)) {\n\t\t\t\treturn int(i - begin(entry.carousel));\n\t\t\t}\n\t\t\treturn int(entry.carousel.size())\n\t\t\t\t+ ((e == entry.sliding.back()) ? 1 : 0);\n\t\t}();\n\t\tconst auto entry = validateCache(index, e);\n\t\tp.drawImage(0, 0, entry->image);\n\t\tp.translate(0, -height);\n\t\tshift -= height;\n\t}\n\tp.translate(0, -shift);\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_emoji_fingerprint.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Calls {\n\nclass Call;\n\n[[nodiscard]] std::vector<EmojiPtr> ComputeEmojiFingerprint(\n\tnot_null<Call*> call);\n[[nodiscard]] std::vector<EmojiPtr> ComputeEmojiFingerprint(\n\tbytes::const_span fingerprint);\n\n[[nodiscard]] base::unique_qptr<Ui::RpWidget> CreateFingerprintAndSignalBars(\n\tnot_null<QWidget*> parent,\n\tnot_null<Call*> call);\n\nstruct FingerprintBadgeState {\n\tstruct Entry {\n\t\tEmojiPtr emoji = nullptr;\n\t\tstd::vector<EmojiPtr> sliding;\n\t\tstd::vector<EmojiPtr> carousel;\n\t\tcrl::time time = 0;\n\t\tfloat64 speed = 0.;\n\t\tfloat64 position = 0.;\n\t\tint added = 0;\n\t};\n\tstd::vector<Entry> entries;\n\tfloat64 speed = 1.;\n};\nstruct FingerprintBadge {\n\tnot_null<const FingerprintBadgeState*> state;\n\trpl::producer<> repaints;\n};\nFingerprintBadge SetupFingerprintBadge(\n\trpl::lifetime &on,\n\trpl::producer<QByteArray> fingerprint);\n\nvoid SetupFingerprintBadgeWidget(\n\tnot_null<Ui::RpWidget*> widget,\n\tnot_null<const FingerprintBadgeState*> state,\n\trpl::producer<> repaints);\n\nstruct FingerprintBadgeCache {\n\tstruct Emoji {\n\t\tEmojiPtr ptr = nullptr;\n\t\tQImage image;\n\t};\n\tstruct Entry {\n\t\tstd::vector<Emoji> emoji;\n\t};\n\tstd::vector<Entry> entries;\n\tQImage shadow;\n};\nvoid PaintFingerprintEntry(\n\tQPainter &p,\n\tconst FingerprintBadgeState::Entry &entry,\n\tFingerprintBadgeCache::Entry &cache,\n\tint esize);\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_instance.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/calls_instance.h\"\n\n#include \"calls/calls_call.h\"\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/group/calls_choose_join_as.h\"\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_rtmp.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"mtproto/mtproto_dh_utils.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"main/session/session_show.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"main/main_account.h\"\n#include \"apiwrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_panel.h\"\n#include \"calls/calls_call.h\"\n#include \"calls/calls_panel.h\"\n#include \"data/data_user.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_session.h\"\n#include \"media/audio/media_audio_track.h\"\n#include \"platform/platform_specific.h\"\n#include \"ui/toast/toast.h\"\n#include \"base/unixtime.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"boxes/abstract_box.h\" // Ui::show().\n\n#include <tgcalls/VideoCaptureInterface.h>\n#include <tgcalls/StaticThreads.h>\n\nnamespace Calls {\nnamespace {\n\nconstexpr auto kServerConfigUpdateTimeoutMs = 24 * 3600 * crl::time(1000);\n\nusing CallSound = Call::Delegate::CallSound;\nusing GroupCallSound = GroupCall::Delegate::GroupCallSound;\n\n} // namespace\n\nclass Instance::Delegate final\n\t: public Call::Delegate\n\t, public GroupCall::Delegate {\npublic:\n\texplicit Delegate(not_null<Instance*> instance);\n\n\tDhConfig getDhConfig() const override;\n\n\tvoid callFinished(not_null<Call*> call) override;\n\tvoid callFailed(not_null<Call*> call) override;\n\tvoid callRedial(not_null<Call*> call) override;\n\tvoid callRequestPermissionsOrFail(\n\t\tFn<void()> onSuccess,\n\t\tbool video) override;\n\tvoid callPlaySound(CallSound sound) override;\n\tauto callGetVideoCapture(\n\t\tconst QString &deviceId,\n\t\tbool isScreenCapture)\n\t-> std::shared_ptr<tgcalls::VideoCaptureInterface> override;\n\n\tvoid groupCallFinished(not_null<GroupCall*> call) override;\n\tvoid groupCallFailed(not_null<GroupCall*> call) override;\n\tvoid groupCallRequestPermissionsOrFail(Fn<void()> onSuccess) override;\n\tvoid groupCallPlaySound(GroupCallSound sound) override;\n\tauto groupCallGetVideoCapture(const QString &deviceId)\n\t\t-> std::shared_ptr<tgcalls::VideoCaptureInterface> override;\n\tFnMut<void()> groupCallAddAsyncWaiter() override;\n\nprivate:\n\tconst not_null<Instance*> _instance;\n\n};\n\nInstance::Delegate::Delegate(not_null<Instance*> instance)\n: _instance(instance) {\n}\n\nDhConfig Instance::Delegate::getDhConfig() const {\n\treturn *_instance->_cachedDhConfig;\n}\n\nvoid Instance::Delegate::callFinished(not_null<Call*> call) {\n\tcrl::on_main(call, [=] {\n\t\t_instance->destroyCall(call);\n\t});\n}\n\nvoid Instance::Delegate::callFailed(not_null<Call*> call) {\n\tcrl::on_main(call, [=] {\n\t\t_instance->destroyCall(call);\n\t});\n}\n\nvoid Instance::Delegate::callRedial(not_null<Call*> call) {\n\tExpects(!call->conferenceInvite());\n\n\tif (_instance->_currentCall.get() == call) {\n\t\t_instance->refreshDhConfig();\n\t}\n}\n\nvoid Instance::Delegate::callRequestPermissionsOrFail(\n\t\tFn<void()> onSuccess,\n\t\tbool video) {\n\t_instance->requestPermissionsOrFail(std::move(onSuccess), video);\n}\n\nvoid Instance::Delegate::callPlaySound(CallSound sound) {\n\t_instance->playSoundOnce([&] {\n\t\tswitch (sound) {\n\t\tcase CallSound::Busy: return \"call_busy\";\n\t\tcase CallSound::Ended: return \"call_end\";\n\t\tcase CallSound::Connecting: return \"call_connect\";\n\t\t}\n\t\tUnexpected(\"CallSound in Instance::callPlaySound.\");\n\t}());\n}\n\nauto Instance::Delegate::callGetVideoCapture(\n\tconst QString &deviceId,\n\tbool isScreenCapture)\n-> std::shared_ptr<tgcalls::VideoCaptureInterface> {\n\treturn _instance->getVideoCapture(deviceId, isScreenCapture);\n}\n\nvoid Instance::Delegate::groupCallFinished(not_null<GroupCall*> call) {\n\tcrl::on_main(call, [=] {\n\t\t_instance->destroyGroupCall(call);\n\t});\n}\n\nvoid Instance::Delegate::groupCallFailed(not_null<GroupCall*> call) {\n\tcrl::on_main(call, [=] {\n\t\t_instance->destroyGroupCall(call);\n\t});\n}\n\nvoid Instance::Delegate::groupCallRequestPermissionsOrFail(\n\t\tFn<void()> onSuccess) {\n\t_instance->requestPermissionsOrFail(std::move(onSuccess), false);\n}\n\nvoid Instance::Delegate::groupCallPlaySound(GroupCallSound sound) {\n\t_instance->playSoundOnce([&] {\n\t\tswitch (sound) {\n\t\tcase GroupCallSound::Started: return \"group_call_start\";\n\t\tcase GroupCallSound::Ended: return \"group_call_end\";\n\t\tcase GroupCallSound::AllowedToSpeak: return \"group_call_allowed\";\n\t\tcase GroupCallSound::Connecting: return \"group_call_connect\";\n\t\tcase GroupCallSound::RecordingStarted:\n\t\t\treturn \"group_call_recording_start\";\n\t\t}\n\t\tUnexpected(\"GroupCallSound in Instance::groupCallPlaySound.\");\n\t}());\n}\n\nauto Instance::Delegate::groupCallGetVideoCapture(const QString &deviceId)\n-> std::shared_ptr<tgcalls::VideoCaptureInterface> {\n\treturn _instance->getVideoCapture(deviceId, false);\n}\n\nFnMut<void()> Instance::Delegate::groupCallAddAsyncWaiter() {\n\treturn _instance->addAsyncWaiter();\n}\n\nInstance::Instance()\n: _delegate(std::make_unique<Delegate>(this))\n, _cachedDhConfig(std::make_unique<DhConfig>())\n, _chooseJoinAs(std::make_unique<Group::ChooseJoinAsProcess>())\n, _startWithRtmp(std::make_unique<Group::StartRtmpProcess>()) {\n}\n\nInstance::~Instance() {\n\tdestroyCurrentCall();\n\n\twhile (!_asyncWaiters.empty()) {\n\t\t_asyncWaiters.front()->acquire();\n\t\t_asyncWaiters.erase(_asyncWaiters.begin());\n\t}\n}\n\nvoid Instance::startOutgoingCall(\n\t\tnot_null<UserData*> user,\n\t\tStartOutgoingCallArgs args) {\n\tif (activateCurrentCall()) {\n\t\treturn;\n\t}\n\tif (user->callsStatus() == UserData::CallsStatus::Private) {\n\t\t// Request full user once more to refresh the setting in case it was changed.\n\t\tuser->session().api().requestFullPeer(user);\n\t\tUi::show(Ui::MakeInformBox(tr::lng_call_error_not_available(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\tuser->name())));\n\t\treturn;\n\t}\n\trequestPermissionsOrFail(crl::guard(this, [=] {\n\t\tcreateCall(user, Call::Type::Outgoing, args);\n\t}), args.video);\n}\n\nvoid Instance::startOrJoinGroupCall(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tStartGroupCallArgs args) {\n\tconfirmLeaveCurrent(show, peer, args, [=](StartGroupCallArgs args) {\n\t\tusing JoinConfirm = Calls::StartGroupCallArgs::JoinConfirm;\n\t\tconst auto context = (args.confirm == JoinConfirm::Always)\n\t\t\t? Group::ChooseJoinAsProcess::Context::JoinWithConfirm\n\t\t\t: peer->groupCall()\n\t\t\t? Group::ChooseJoinAsProcess::Context::Join\n\t\t\t: args.scheduleNeeded\n\t\t\t? Group::ChooseJoinAsProcess::Context::CreateScheduled\n\t\t\t: Group::ChooseJoinAsProcess::Context::Create;\n\t\t_chooseJoinAs->start(peer, context, show, [=](Group::JoinInfo info) {\n\t\t\tconst auto call = info.peer->groupCall();\n\t\t\tinfo.joinHash = args.joinHash;\n\t\t\tif (call) {\n\t\t\t\tinfo.rtmp = call->rtmp();\n\t\t\t}\n\t\t\tcreateGroupCall(\n\t\t\t\tstd::move(info),\n\t\t\t\tcall ? call->input() : MTP_inputGroupCall({}, {}));\n\t\t});\n\t});\n}\n\nvoid Instance::startOrJoinConferenceCall(StartConferenceInfo args) {\n\tExpects(args.call || args.show);\n\n\tconst auto migrationInfo = (args.migrating\n\t\t&& args.call\n\t\t&& _currentCallPanel)\n\t\t? _currentCallPanel->migrationInfo()\n\t\t: ConferencePanelMigration();\n\tif (!args.migrating) {\n\t\tdestroyCurrentCall();\n\t}\n\n\tconst auto session = args.show\n\t\t? &args.show->session()\n\t\t: &args.call->session();\n\tauto call = std::make_unique<GroupCall>(_delegate.get(), args);\n\tconst auto raw = call.get();\n\n\tsession->account().sessionChanges(\n\t) | rpl::on_next([=] {\n\t\tdestroyGroupCall(raw);\n\t}, raw->lifetime());\n\n\tif (args.call) {\n\t\t_currentGroupCallPanel = std::make_unique<Group::Panel>(\n\t\t\traw,\n\t\t\tmigrationInfo);\n\t\t_currentGroupCall = std::move(call);\n\t\t_currentGroupCallChanges.fire_copy(raw);\n\t\tfinishConferenceInvitations(args);\n\t\tif (args.migrating) {\n\t\t\tdestroyCurrentCall(args.call.get(), args.linkSlug);\n\t\t}\n\t} else {\n\t\tif (const auto was = base::take(_startingGroupCall)) {\n\t\t\tdestroyGroupCall(was.get());\n\t\t}\n\t\t_startingGroupCall = std::move(call);\n\t}\n}\n\nvoid Instance::startedConferenceReady(\n\t\tnot_null<GroupCall*> call,\n\t\tStartConferenceInfo args) {\n\tif (_startingGroupCall.get() != call) {\n\t\treturn;\n\t}\n\tconst auto migrationInfo = _currentCallPanel\n\t\t? _currentCallPanel->migrationInfo()\n\t\t: ConferencePanelMigration();\n\t_currentGroupCallPanel = std::make_unique<Group::Panel>(\n\t\tcall,\n\t\tmigrationInfo);\n\t_currentGroupCall = std::move(_startingGroupCall);\n\t_currentGroupCallChanges.fire_copy(call);\n\tconst auto real = call->sharedCall().get();\n\tconst auto link = real->conferenceInviteLink();\n\tconst auto slug = Group::ExtractConferenceSlug(link);\n\tfinishConferenceInvitations(args);\n\tdestroyCurrentCall(real, slug);\n}\n\nvoid Instance::finishConferenceInvitations(const StartConferenceInfo &args) {\n\tExpects(_currentGroupCallPanel != nullptr);\n\n\tif (!args.invite.empty()) {\n\t\t_currentGroupCallPanel->migrationInviteUsers(std::move(args.invite));\n\t} else if (args.sharingLink) {\n\t\t_currentGroupCallPanel->migrationShowShareLink();\n\t}\n}\n\nvoid Instance::confirmLeaveCurrent(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tStartGroupCallArgs args,\n\t\tFn<void(StartGroupCallArgs)> confirmed) {\n\tusing JoinConfirm = Calls::StartGroupCallArgs::JoinConfirm;\n\n\tauto confirmedArgs = args;\n\tconfirmedArgs.confirm = JoinConfirm::None;\n\n\tconst auto askConfirmation = [&](QString text, QString button) {\n\t\tshow->showBox(Ui::MakeConfirmBox({\n\t\t\t.text = text,\n\t\t\t.confirmed = [=] {\n\t\t\t\tshow->hideLayer();\n\t\t\t\tconfirmed(confirmedArgs);\n\t\t\t},\n\t\t\t.confirmText = button,\n\t\t}));\n\t};\n\tif (args.confirm != JoinConfirm::None && inCall()) {\n\t\t// Do you want to leave your active voice chat\n\t\t// to join a voice chat in this group?\n\t\taskConfirmation(\n\t\t\t(peer->isBroadcast()\n\t\t\t\t? tr::lng_call_leave_to_other_sure_channel\n\t\t\t\t: tr::lng_call_leave_to_other_sure)(tr::now),\n\t\t\ttr::lng_call_bar_hangup(tr::now));\n\t} else if (args.confirm != JoinConfirm::None && inGroupCall()) {\n\t\tconst auto now = currentGroupCall()->peer();\n\t\tif (now == peer) {\n\t\t\tactivateCurrentCall(args.joinHash);\n\t\t} else if (currentGroupCall()->scheduleDate()) {\n\t\t\tconfirmed(confirmedArgs);\n\t\t} else {\n\t\t\taskConfirmation(\n\t\t\t\t((peer->isBroadcast() && now->isBroadcast())\n\t\t\t\t\t? tr::lng_group_call_leave_channel_to_other_sure_channel\n\t\t\t\t\t: now->isBroadcast()\n\t\t\t\t\t? tr::lng_group_call_leave_channel_to_other_sure\n\t\t\t\t\t: peer->isBroadcast()\n\t\t\t\t\t? tr::lng_group_call_leave_to_other_sure_channel\n\t\t\t\t\t: tr::lng_group_call_leave_to_other_sure)(tr::now),\n\t\t\t\ttr::lng_group_call_leave(tr::now));\n\t\t}\n\t} else {\n\t\tconfirmed(args);\n\t}\n}\n\nvoid Instance::showStartWithRtmp(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer) {\n\t_startWithRtmp->start(peer, show, [=](Group::JoinInfo info) {\n\t\tconfirmLeaveCurrent(show, peer, {}, [=](auto) {\n\t\t\t_startWithRtmp->close();\n\t\t\tcreateGroupCall(std::move(info), MTP_inputGroupCall({}, {}));\n\t\t});\n\t});\n}\n\nnot_null<Media::Audio::Track*> Instance::ensureSoundLoaded(\n\t\tconst QString &key) {\n\tconst auto i = _tracks.find(key);\n\tif (i != end(_tracks)) {\n\t\treturn i->second.get();\n\t}\n\tconst auto result = _tracks.emplace(\n\t\tkey,\n\t\tMedia::Audio::Current().createTrack()).first->second.get();\n\tresult->fillFromFile(Core::App().settings().getSoundPath(key));\n\treturn result;\n}\n\nvoid Instance::playSoundOnce(const QString &key) {\n\tensureSoundLoaded(key)->playOnce();\n}\n\nvoid Instance::destroyCall(not_null<Call*> call) {\n\tif (_currentCall.get() == call) {\n\t\tconst auto groupCallWindow = _currentGroupCallPanel\n\t\t\t? _currentGroupCallPanel->window().get()\n\t\t\t: nullptr;\n\t\tconst auto reused = (_currentCallPanel->window() == groupCallWindow);\n\t\t_currentCallPanel->closeBeforeDestroy(reused);\n\t\t_currentCallPanel = nullptr;\n\n\t\tauto taken = base::take(_currentCall);\n\t\t_currentCallChanges.fire(nullptr);\n\t\ttaken.reset();\n\n\t\tif (Core::Quitting()) {\n\t\t\tLOG((\"Calls::Instance doesn't prevent quit any more.\"));\n\t\t}\n\t\tCore::App().quitPreventFinished();\n\t}\n}\n\nvoid Instance::createCall(\n\t\tnot_null<UserData*> user,\n\t\tCallType type,\n\t\tStartOutgoingCallArgs args) {\n\tstruct Performer final {\n\t\texplicit Performer(Fn<void(bool, bool, const Performer &)> callback)\n\t\t: callback(std::move(callback)) {\n\t\t}\n\t\tFn<void(bool, bool, const Performer &)> callback;\n\t};\n\tconst auto performer = Performer([=](\n\t\t\tbool video,\n\t\t\tbool isConfirmed,\n\t\t\tconst Performer &repeater) {\n\t\tconst auto delegate = _delegate.get();\n\t\tauto call = std::make_unique<Call>(delegate, user, type, video);\n\t\tif (isConfirmed) {\n\t\t\tcall->applyUserConfirmation();\n\t\t}\n\t\tconst auto raw = call.get();\n\n\t\tuser->session().account().sessionChanges(\n\t\t) | rpl::on_next([=] {\n\t\t\tdestroyCall(raw);\n\t\t}, raw->lifetime());\n\n\t\tif (_currentCall) {\n\t\t\t_currentCallPanel->replaceCall(raw);\n\t\t\tstd::swap(_currentCall, call);\n\t\t\tcall->hangup();\n\t\t} else {\n\t\t\t_currentCallPanel = std::make_unique<Panel>(raw);\n\t\t\t_currentCall = std::move(call);\n\t\t}\n\t\tif (raw->state() == Call::State::WaitingUserConfirmation) {\n\t\t\t_currentCallPanel->startOutgoingRequests(\n\t\t\t) | rpl::on_next([=](bool video) {\n\t\t\t\trepeater.callback(video, true, repeater);\n\t\t\t}, raw->lifetime());\n\t\t} else {\n\t\t\trefreshServerConfig(&user->session());\n\t\t\trefreshDhConfig();\n\t\t}\n\t\t_currentCallChanges.fire_copy(raw);\n\t});\n\tperformer.callback(args.video, args.isConfirmed, performer);\n}\n\nvoid Instance::destroyGroupCall(not_null<GroupCall*> call) {\n\tif (_currentGroupCall.get() == call) {\n\t\t_currentGroupCallPanel->closeBeforeDestroy();\n\t\t_currentGroupCallPanel = nullptr;\n\n\t\tauto taken = base::take(_currentGroupCall);\n\t\t_currentGroupCallChanges.fire(nullptr);\n\t\ttaken.reset();\n\n\t\tif (Core::Quitting()) {\n\t\t\tLOG((\"Calls::Instance doesn't prevent quit any more.\"));\n\t\t}\n\t\tCore::App().quitPreventFinished();\n\t} else if (_startingGroupCall.get() == call) {\n\t\tbase::take(_startingGroupCall);\n\t}\n}\n\nvoid Instance::createGroupCall(\n\t\tGroup::JoinInfo info,\n\t\tconst MTPInputGroupCall &inputCall) {\n\tdestroyCurrentCall();\n\n\tauto call = std::make_unique<GroupCall>(\n\t\t_delegate.get(),\n\t\tstd::move(info),\n\t\tinputCall);\n\tconst auto raw = call.get();\n\n\tinfo.peer->session().account().sessionChanges(\n\t) | rpl::on_next([=] {\n\t\tdestroyGroupCall(raw);\n\t}, raw->lifetime());\n\n\t_currentGroupCallPanel = std::make_unique<Group::Panel>(raw);\n\t_currentGroupCall = std::move(call);\n\t_currentGroupCallChanges.fire_copy(raw);\n}\n\nvoid Instance::refreshDhConfig() {\n\tExpects(_currentCall != nullptr);\n\tExpects(!_currentCall->conferenceInvite());\n\n\tconst auto weak = base::make_weak(_currentCall);\n\t_currentCall->user()->session().api().request(MTPmessages_GetDhConfig(\n\t\tMTP_int(_cachedDhConfig->version),\n\t\tMTP_int(MTP::ModExpFirst::kRandomPowerSize)\n\t)).done([=](const MTPmessages_DhConfig &result) {\n\t\tconst auto call = weak.get();\n\t\tconst auto random = updateDhConfig(result);\n\t\tif (!call) {\n\t\t\treturn;\n\t\t}\n\t\tif (!random.empty()) {\n\t\t\tAssert(random.size() == MTP::ModExpFirst::kRandomPowerSize);\n\t\t\tcall->start(random);\n\t\t} else {\n\t\t\t_delegate->callFailed(call);\n\t\t}\n\t}).fail([=] {\n\t\tconst auto call = weak.get();\n\t\tif (!call) {\n\t\t\treturn;\n\t\t}\n\t\t_delegate->callFailed(call);\n\t}).send();\n}\n\nbytes::const_span Instance::updateDhConfig(\n\t\tconst MTPmessages_DhConfig &data) {\n\tconst auto validRandom = [](const QByteArray & random) {\n\t\tif (random.size() != MTP::ModExpFirst::kRandomPowerSize) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t};\n\treturn data.match([&](const MTPDmessages_dhConfig &data)\n\t-> bytes::const_span {\n\t\tauto primeBytes = bytes::make_vector(data.vp().v);\n\t\tif (!MTP::IsPrimeAndGood(primeBytes, data.vg().v)) {\n\t\t\tLOG((\"API Error: bad p/g received in dhConfig.\"));\n\t\t\treturn {};\n\t\t} else if (!validRandom(data.vrandom().v)) {\n\t\t\treturn {};\n\t\t}\n\t\t_cachedDhConfig->g = data.vg().v;\n\t\t_cachedDhConfig->p = std::move(primeBytes);\n\t\t_cachedDhConfig->version = data.vversion().v;\n\t\treturn bytes::make_span(data.vrandom().v);\n\t}, [&](const MTPDmessages_dhConfigNotModified &data)\n\t-> bytes::const_span {\n\t\tif (!_cachedDhConfig->g || _cachedDhConfig->p.empty()) {\n\t\t\tLOG((\"API Error: dhConfigNotModified on zero version.\"));\n\t\t\treturn {};\n\t\t} else if (!validRandom(data.vrandom().v)) {\n\t\t\treturn {};\n\t\t}\n\t\treturn bytes::make_span(data.vrandom().v);\n\t});\n}\n\nvoid Instance::refreshServerConfig(not_null<Main::Session*> session) {\n\tif (_serverConfigRequestSession) {\n\t\treturn;\n\t}\n\tif (_lastServerConfigUpdateTime\n\t\t&& ((crl::now() - _lastServerConfigUpdateTime)\n\t\t\t< kServerConfigUpdateTimeoutMs)) {\n\t\treturn;\n\t}\n\t_serverConfigRequestSession = session;\n\tsession->api().request(MTPphone_GetCallConfig(\n\t)).done([=](const MTPDataJSON &result) {\n\t\t_serverConfigRequestSession = nullptr;\n\t\t_lastServerConfigUpdateTime = crl::now();\n\n\t\tconst auto &json = result.c_dataJSON().vdata().v;\n\t\tUpdateConfig(std::string(json.data(), json.size()));\n\t}).fail([=] {\n\t\t_serverConfigRequestSession = nullptr;\n\t}).send();\n}\n\nvoid Instance::handleUpdate(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPUpdate &update) {\n\tupdate.match([&](const MTPDupdatePhoneCall &data) {\n\t\thandleCallUpdate(session, data.vphone_call());\n\t}, [&](const MTPDupdatePhoneCallSignalingData &data) {\n\t\thandleSignalingData(session, data);\n\t}, [&](const MTPDupdateGroupCall &data) {\n\t\thandleGroupCallUpdate(session, update);\n\t}, [&](const MTPDupdateGroupCallConnection &data) {\n\t\thandleGroupCallUpdate(session, update);\n\t}, [&](const MTPDupdateGroupCallParticipants &data) {\n\t\thandleGroupCallUpdate(session, update);\n\t}, [&](const MTPDupdateGroupCallChainBlocks &data) {\n\t\thandleGroupCallUpdate(session, update);\n\t}, [&](const MTPDupdateGroupCallMessage &data) {\n\t\thandleGroupCallUpdate(session, update);\n\t}, [&](const MTPDupdateGroupCallEncryptedMessage &data) {\n\t\thandleGroupCallUpdate(session, update);\n\t}, [&](const MTPDupdateDeleteGroupCallMessages &data) {\n\t\thandleGroupCallUpdate(session, update);\n\t}, [&](const MTPDupdateMessageID &data) {\n\t\thandleGroupCallUpdate(session, update);\n\t}, [](const auto &) {\n\t\tUnexpected(\"Update type in Calls::Instance::handleUpdate.\");\n\t});\n}\n\nvoid Instance::showInfoPanel(not_null<Call*> call) {\n\tif (_currentCall.get() == call) {\n\t\t_currentCallPanel->showAndActivate();\n\t}\n}\n\nvoid Instance::showInfoPanel(not_null<GroupCall*> call) {\n\tif (_currentGroupCall.get() == call) {\n\t\t_currentGroupCallPanel->showAndActivate();\n\t}\n}\n\nFnMut<void()> Instance::addAsyncWaiter() {\n\tauto semaphore = std::make_unique<crl::semaphore>();\n\tconst auto raw = semaphore.get();\n\tconst auto weak = base::make_weak(this);\n\t_asyncWaiters.emplace(std::move(semaphore));\n\treturn [raw, weak] {\n\t\traw->release();\n\t\tcrl::on_main(weak, [raw, weak] {\n\t\t\tauto &waiters = weak->_asyncWaiters;\n\t\t\tauto wrapped = std::unique_ptr<crl::semaphore>(raw);\n\t\t\tconst auto i = waiters.find(wrapped);\n\t\t\twrapped.release();\n\n\t\t\tif (i != end(waiters)) {\n\t\t\t\twaiters.erase(i);\n\t\t\t}\n\t\t});\n\t};\n}\n\nvoid Instance::registerVideoStream(not_null<GroupCall*> call) {\n\t_streams[&call->peer()->session()].push_back(call);\n}\n\nbool Instance::isSharingScreen() const {\n\treturn (_currentCall && _currentCall->isSharingScreen())\n\t\t|| (_currentGroupCall && _currentGroupCall->isSharingScreen());\n}\n\nbool Instance::isQuitPrevent() {\n\tif (!_currentCall || _currentCall->isIncomingWaiting()) {\n\t\treturn false;\n\t}\n\t_currentCall->hangup();\n\tif (!_currentCall) {\n\t\treturn false;\n\t}\n\tLOG((\"Calls::Instance prevents quit, hanging up a call...\"));\n\treturn true;\n}\n\nvoid Instance::handleCallUpdate(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPPhoneCall &call) {\n\tif (call.type() == mtpc_phoneCallRequested) {\n\t\tauto &phoneCall = call.c_phoneCallRequested();\n\t\tauto user = session->data().userLoaded(phoneCall.vadmin_id());\n\t\tif (!user) {\n\t\t\tLOG((\"API Error: User not loaded for phoneCallRequested.\"));\n\t\t} else if (user->isSelf()) {\n\t\t\tLOG((\"API Error: Self found in phoneCallRequested.\"));\n\t\t} else if (_currentCall\n\t\t\t&& _currentCall->user() == user\n\t\t\t&& _currentCall->id() == phoneCall.vid().v) {\n\t\t\t// May be a repeated phoneCallRequested update from getDifference.\n\t\t\treturn;\n\t\t}\n\t\tif (inCall()\n\t\t\t&& _currentCall->type() == Call::Type::Outgoing\n\t\t\t&& _currentCall->user()->id == session->userPeerId()\n\t\t\t&& (user->id == _currentCall->user()->session().userPeerId())) {\n\t\t\t// Ignore call from the same running app, other account.\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto &config = session->serverConfig();\n\t\tif (inCall() || inGroupCall() || !user || user->isSelf()) {\n\t\t\tconst auto flags = phoneCall.is_video()\n\t\t\t\t? MTPphone_DiscardCall::Flag::f_video\n\t\t\t\t: MTPphone_DiscardCall::Flag(0);\n\t\t\tsession->api().request(MTPphone_DiscardCall(\n\t\t\t\tMTP_flags(flags),\n\t\t\t\tMTP_inputPhoneCall(phoneCall.vid(), phoneCall.vaccess_hash()),\n\t\t\t\tMTP_int(0),\n\t\t\t\tMTP_phoneCallDiscardReasonBusy(),\n\t\t\t\tMTP_long(0)\n\t\t\t)).send();\n\t\t} else if (phoneCall.vdate().v + (config.callRingTimeoutMs / 1000)\n\t\t\t< base::unixtime::now()) {\n\t\t\tLOG((\"Ignoring too old call.\"));\n\t\t} else {\n\t\t\tcreateCall(user, Call::Type::Incoming, { phoneCall.is_video() });\n\t\t\t_currentCall->handleUpdate(call);\n\t\t}\n\t} else if (!_currentCall\n\t\t|| (&_currentCall->user()->session() != session)\n\t\t|| !_currentCall->handleUpdate(call)) {\n\t\tDEBUG_LOG((\"API Warning: unexpected phone call update %1\").arg(call.type()));\n\t}\n}\n\nvoid Instance::handleGroupCallUpdate(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPUpdate &update) {\n\tif (const auto i = _streams.find(session); i != end(_streams)) {\n\t\tfor (auto j = begin(i->second); j != end(i->second);) {\n\t\t\tif (const auto strong = j->get()) {\n\t\t\t\tupdate.match([&](const MTPDupdateGroupCall &data) {\n\t\t\t\t\tstrong->handlePossibleCreateOrJoinResponse(data);\n\t\t\t\t\tstrong->handleUpdate(update);\n\t\t\t\t}, [&](const MTPDupdateGroupCallConnection &data) {\n\t\t\t\t\tstrong->handlePossibleCreateOrJoinResponse(data);\n\t\t\t\t}, [&](const MTPDupdateGroupCallMessage &data) {\n\t\t\t\t\tstrong->handleIncomingMessage(data);\n\t\t\t\t}, [&](const MTPDupdateGroupCallEncryptedMessage &data) {\n\t\t\t\t\tstrong->handleIncomingMessage(data);\n\t\t\t\t}, [&](const MTPDupdateDeleteGroupCallMessages &data) {\n\t\t\t\t\tstrong->handleDeleteMessages(data);\n\t\t\t\t}, [&](const MTPDupdateMessageID &data) {\n\t\t\t\t\tstrong->handleMessageSent(data);\n\t\t\t\t}, [&](const MTPDupdateGroupCallParticipants &data) {\n\t\t\t\t\tstrong->handleUpdate(update);\n\t\t\t\t}, [&](const MTPDupdateGroupCallChainBlocks &data) {\n\t\t\t\t\tstrong->handleUpdate(update);\n\t\t\t\t}, [](const auto &) {\n\t\t\t\t});\n\t\t\t\t++j;\n\t\t\t} else {\n\t\t\t\tj = i->second.erase(j);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst auto groupCall = _currentGroupCall\n\t\t? _currentGroupCall.get()\n\t\t: _startingGroupCall.get();\n\tif (groupCall && (&groupCall->peer()->session() == session)) {\n\t\tupdate.match([&](const MTPDupdateGroupCall &data) {\n\t\t\tgroupCall->handlePossibleCreateOrJoinResponse(data);\n\t\t}, [&](const MTPDupdateGroupCallConnection &data) {\n\t\t\tgroupCall->handlePossibleCreateOrJoinResponse(data);\n\t\t}, [&](const MTPDupdateGroupCallMessage &data) {\n\t\t\tgroupCall->handleIncomingMessage(data);\n\t\t}, [&](const MTPDupdateGroupCallEncryptedMessage &data) {\n\t\t\tgroupCall->handleIncomingMessage(data);\n\t\t}, [&](const MTPDupdateDeleteGroupCallMessages &data) {\n\t\t\tgroupCall->handleDeleteMessages(data);\n\t\t}, [&](const MTPDupdateMessageID &data) {\n\t\t\tgroupCall->handleMessageSent(data);\n\t\t}, [](const auto &) {\n\t\t});\n\t}\n\n\tif (update.type() == mtpc_updateGroupCallConnection\n\t\t|| update.type() == mtpc_updateGroupCallMessage\n\t\t|| update.type() == mtpc_updateGroupCallEncryptedMessage\n\t\t|| update.type() == mtpc_updateDeleteGroupCallMessages\n\t\t|| update.type() == mtpc_updateMessageID) {\n\t\treturn;\n\t}\n\tconst auto callId = update.match([](const MTPDupdateGroupCall &data) {\n\t\treturn data.vcall().match([](const auto &data) {\n\t\t\treturn data.vid().v;\n\t\t});\n\t}, [](const MTPDupdateGroupCallParticipants &data) {\n\t\treturn data.vcall().match([&](const MTPDinputGroupCall &data) {\n\t\t\treturn data.vid().v;\n\t\t}, [](const auto &) -> CallId {\n\t\t\tUnexpected(\"slug/msg in Instance::handleGroupCallUpdate\");\n\t\t});\n\t}, [](const MTPDupdateGroupCallChainBlocks &data) {\n\t\treturn data.vcall().match([&](const MTPDinputGroupCall &data) {\n\t\t\treturn data.vid().v;\n\t\t}, [](const auto &) -> CallId {\n\t\t\tUnexpected(\"slug/msg in Instance::handleGroupCallUpdate\");\n\t\t});\n\t}, [](const auto &) -> CallId {\n\t\tUnexpected(\"Type in Instance::handleGroupCallUpdate.\");\n\t});\n\tif (update.type() == mtpc_updateGroupCallChainBlocks) {\n\t\tconst auto existing = session->data().groupCall(callId);\n\t\tif (existing && groupCall && groupCall->lookupReal() == existing) {\n\t\t\tgroupCall->handleUpdate(update);\n\t\t}\n\t} else if (const auto existing = session->data().groupCall(callId)) {\n\t\texisting->enqueueUpdate(update);\n\t} else {\n\t\tapplyGroupCallUpdateChecked(session, update);\n\t}\n}\n\nvoid Instance::applyGroupCallUpdateChecked(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPUpdate &update) {\n\tconst auto groupCall = _currentGroupCall\n\t\t? _currentGroupCall.get()\n\t\t: _startingGroupCall.get();\n\tif (groupCall && (&groupCall->peer()->session() == session)) {\n\t\tgroupCall->handleUpdate(update);\n\t}\n}\n\nvoid Instance::handleSignalingData(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDupdatePhoneCallSignalingData &data) {\n\tif (!_currentCall\n\t\t|| (&_currentCall->user()->session() != session)\n\t\t|| !_currentCall->handleSignalingData(data)) {\n\t\tDEBUG_LOG((\"API Warning: unexpected call signaling data %1\"\n\t\t\t).arg(data.vphone_call_id().v));\n\t}\n}\n\nbool Instance::inCall() const {\n\tif (!_currentCall) {\n\t\treturn false;\n\t}\n\tconst auto state = _currentCall->state();\n\treturn (state != Call::State::Busy)\n\t\t&& (state != Call::State::WaitingUserConfirmation);\n}\n\nbool Instance::inGroupCall() const {\n\tif (!_currentGroupCall) {\n\t\treturn false;\n\t}\n\tconst auto state = _currentGroupCall->state();\n\treturn (state != GroupCall::State::HangingUp)\n\t\t&& (state != GroupCall::State::Ended)\n\t\t&& (state != GroupCall::State::FailedHangingUp)\n\t\t&& (state != GroupCall::State::Failed);\n}\n\nvoid Instance::destroyCurrentCall(\n\t\tData::GroupCall *migrateCall,\n\t\tconst QString &migrateSlug) {\n\tif (const auto current = currentCall()) {\n\t\tcurrent->hangup(migrateCall, migrateSlug);\n\t\tif (const auto still = currentCall()) {\n\t\t\tdestroyCall(still);\n\t\t}\n\t}\n\tif (const auto current = currentGroupCall()) {\n\t\tif (!migrateCall || current->lookupReal() != migrateCall) {\n\t\t\tcurrent->hangup();\n\t\t\tif (const auto still = currentGroupCall()) {\n\t\t\t\tdestroyGroupCall(still);\n\t\t\t}\n\t\t}\n\t}\n\tbase::take(_startingGroupCall);\n}\n\nbool Instance::hasVisiblePanel(Main::Session *session) const {\n\tif (inCall()) {\n\t\treturn _currentCallPanel->isVisible()\n\t\t\t&& (!session || (&_currentCall->user()->session() == session));\n\t} else if (inGroupCall()) {\n\t\treturn _currentGroupCallPanel->isVisible()\n\t\t\t&& (!session || (&_currentGroupCall->peer()->session() == session));\n\t}\n\treturn false;\n}\n\nbool Instance::hasActivePanel(Main::Session *session) const {\n\tif (inCall()) {\n\t\treturn _currentCallPanel->isActive()\n\t\t\t&& (!session || (&_currentCall->user()->session() == session));\n\t} else if (inGroupCall()) {\n\t\treturn _currentGroupCallPanel->isActive()\n\t\t\t&& (!session || (&_currentGroupCall->peer()->session() == session));\n\t}\n\treturn false;\n}\n\nbool Instance::activateCurrentCall(const QString &joinHash) {\n\tif (inCall()) {\n\t\t_currentCallPanel->showAndActivate();\n\t\treturn true;\n\t} else if (inGroupCall()) {\n\t\tif (!joinHash.isEmpty()) {\n\t\t\t_currentGroupCall->rejoinWithHash(joinHash);\n\t\t}\n\t\t_currentGroupCallPanel->showAndActivate();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool Instance::minimizeCurrentActiveCall() {\n\tif (inCall() && _currentCallPanel->isActive()) {\n\t\t_currentCallPanel->minimize();\n\t\treturn true;\n\t} else if (inGroupCall() && _currentGroupCallPanel->isActive()) {\n\t\t_currentGroupCallPanel->minimize();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool Instance::toggleFullScreenCurrentActiveCall() {\n\tif (inCall() && _currentCallPanel->isActive()) {\n\t\t_currentCallPanel->toggleFullScreen();\n\t\treturn true;\n\t} else if (inGroupCall() && _currentGroupCallPanel->isActive()) {\n\t\t_currentGroupCallPanel->toggleFullScreen();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool Instance::closeCurrentActiveCall() {\n\tif (inGroupCall() && _currentGroupCallPanel->isActive()) {\n\t\t_currentGroupCallPanel->close();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nCall *Instance::currentCall() const {\n\treturn _currentCall.get();\n}\n\nrpl::producer<Call*> Instance::currentCallValue() const {\n\treturn _currentCallChanges.events_starting_with(currentCall());\n}\n\nGroupCall *Instance::currentGroupCall() const {\n\treturn _currentGroupCall.get();\n}\n\nrpl::producer<GroupCall*> Instance::currentGroupCallValue() const {\n\treturn _currentGroupCallChanges.events_starting_with(currentGroupCall());\n}\n\nvoid Instance::requestPermissionsOrFail(Fn<void()> onSuccess, bool video) {\n\tusing Type = Platform::PermissionType;\n\trequestPermissionOrFail(Type::Microphone, [=] {\n\t\tauto callback = [=] { crl::on_main(onSuccess); };\n\t\tif (video) {\n\t\t\trequestPermissionOrFail(Type::Camera, std::move(callback));\n\t\t} else {\n\t\t\tcallback();\n\t\t}\n\t});\n}\n\nvoid Instance::requestPermissionOrFail(Platform::PermissionType type, Fn<void()> onSuccess) {\n\tusing Status = Platform::PermissionStatus;\n\tconst auto status = Platform::GetPermissionStatus(type);\n\tif (status == Status::Granted) {\n\t\tonSuccess();\n\t} else if (status == Status::CanRequest) {\n\t\tPlatform::RequestPermission(type, crl::guard(this, [=](Status status) {\n\t\t\tif (status == Status::Granted) {\n\t\t\t\tcrl::on_main(onSuccess);\n\t\t\t} else {\n\t\t\t\tif (_currentCall) {\n\t\t\t\t\t_currentCall->hangup();\n\t\t\t\t}\n\t\t\t}\n\t\t}));\n\t} else {\n\t\tif (inCall()) {\n\t\t\t_currentCall->hangup();\n\t\t}\n\t\tif (inGroupCall()) {\n\t\t\t_currentGroupCall->hangup();\n\t\t}\n\t\tUi::show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_no_mic_permission(),\n\t\t\t.confirmed = crl::guard(this, [=](Fn<void()> &&close) {\n\t\t\t\tPlatform::OpenSystemSettingsForPermission(type);\n\t\t\t\tclose();\n\t\t\t}),\n\t\t\t.confirmText = tr::lng_menu_settings(),\n\t\t}));\n\t}\n}\n\nstd::shared_ptr<tgcalls::VideoCaptureInterface> Instance::getVideoCapture(\n\t\tstd::optional<QString> deviceId,\n\t\tbool isScreenCapture) {\n\tif (auto result = _videoCapture.lock()) {\n\t\tif (deviceId) {\n\t\t\tresult->switchToDevice(\n\t\t\t\t(deviceId->isEmpty()\n\t\t\t\t\t? Core::App().settings().cameraDeviceId()\n\t\t\t\t\t: *deviceId).toStdString(),\n\t\t\t\tisScreenCapture);\n\t\t}\n\t\treturn result;\n\t}\n\tconst auto startDeviceId = (deviceId && !deviceId->isEmpty())\n\t\t? *deviceId\n\t\t: Core::App().settings().cameraDeviceId();\n\tauto result = std::shared_ptr<tgcalls::VideoCaptureInterface>(\n\t\ttgcalls::VideoCaptureInterface::Create(\n\t\t\ttgcalls::StaticThreads::getThreads(),\n\t\t\tstartDeviceId.toStdString()));\n\t_videoCapture = result;\n\treturn result;\n}\n\nconst ConferenceInvites &Instance::conferenceInvites(\n\t\tCallId conferenceId) const {\n\tstatic const auto kEmpty = ConferenceInvites();\n\tconst auto i = _conferenceInvites.find(conferenceId);\n\treturn (i != end(_conferenceInvites)) ? i->second : kEmpty;\n}\n\nvoid Instance::registerConferenceInvite(\n\t\tCallId conferenceId,\n\t\tnot_null<UserData*> user,\n\t\tMsgId messageId,\n\t\tbool incoming) {\n\tauto &info = _conferenceInvites[conferenceId].users[user];\n\t(incoming ? info.incoming : info.outgoing).emplace(messageId);\n}\n\nvoid Instance::unregisterConferenceInvite(\n\t\tCallId conferenceId,\n\t\tnot_null<UserData*> user,\n\t\tMsgId messageId,\n\t\tbool incoming,\n\t\tbool onlyStopCalling) {\n\tconst auto i = _conferenceInvites.find(conferenceId);\n\tif (i == end(_conferenceInvites)) {\n\t\treturn;\n\t}\n\tconst auto j = i->second.users.find(user);\n\tif (j == end(i->second.users)) {\n\t\treturn;\n\t}\n\tauto &info = j->second;\n\tif (!(incoming ? info.incoming : info.outgoing).remove(messageId)) {\n\t\treturn;\n\t}\n\tif (!incoming) {\n\t\tuser->owner().unregisterInvitedToCallUser(\n\t\t\tconferenceId,\n\t\t\tuser,\n\t\t\tonlyStopCalling);\n\t}\n\tif (info.incoming.empty() && info.outgoing.empty()) {\n\t\ti->second.users.erase(j);\n\t\tif (i->second.users.empty()) {\n\t\t\t_conferenceInvites.erase(i);\n\t\t}\n\t}\n\tif (_currentCall\n\t\t&& _currentCall->user() == user\n\t\t&& _currentCall->conferenceInviteMsgId() == messageId\n\t\t&& _currentCall->state() == Call::State::WaitingIncoming) {\n\t\tdestroyCurrentCall();\n\t}\n}\n\nvoid Instance::declineIncomingConferenceInvites(CallId conferenceId) {\n\tconst auto i = _conferenceInvites.find(conferenceId);\n\tif (i == end(_conferenceInvites)) {\n\t\treturn;\n\t}\n\tfor (auto j = begin(i->second.users); j != end(i->second.users);) {\n\t\tconst auto api = &j->first->session().api();\n\t\tfor (const auto &messageId : base::take(j->second.incoming)) {\n\t\t\tapi->request(MTPphone_DeclineConferenceCallInvite(\n\t\t\t\tMTP_int(messageId.bare)\n\t\t\t)).send();\n\t\t}\n\t\tif (j->second.outgoing.empty()) {\n\t\t\tj = i->second.users.erase(j);\n\t\t} else {\n\t\t\t++j;\n\t\t}\n\t}\n\tif (i->second.users.empty()) {\n\t\t_conferenceInvites.erase(i);\n\t}\n}\n\nvoid Instance::declineOutgoingConferenceInvite(\n\t\tCallId conferenceId,\n\t\tnot_null<UserData*> user,\n\t\tbool discard) {\n\tconst auto i = _conferenceInvites.find(conferenceId);\n\tif (i == end(_conferenceInvites)) {\n\t\treturn;\n\t}\n\tconst auto j = i->second.users.find(user);\n\tif (j == end(i->second.users)) {\n\t\treturn;\n\t}\n\tconst auto api = &user->session().api();\n\tauto ids = base::take(j->second.outgoing);\n\tauto inputs = QVector<MTPint>();\n\tfor (const auto &messageId : ids) {\n\t\tif (discard) {\n\t\t\tinputs.push_back(MTP_int(messageId.bare));\n\t\t} else {\n\t\t\tapi->request(MTPphone_DeclineConferenceCallInvite(\n\t\t\t\tMTP_int(messageId.bare)\n\t\t\t)).send();\n\t\t}\n\t}\n\tif (!inputs.empty()) {\n\t\tuser->owner().histories().deleteMessages(\n\t\t\tuser->owner().history(user),\n\t\t\tstd::move(inputs),\n\t\t\ttrue);\n\t\tfor (const auto &messageId : ids) {\n\t\t\tif (const auto item = user->owner().message(user, messageId)) {\n\t\t\t\titem->destroy();\n\t\t\t}\n\t\t}\n\t}\n\tif (j->second.incoming.empty()) {\n\t\ti->second.users.erase(j);\n\t\tif (i->second.users.empty()) {\n\t\t\t_conferenceInvites.erase(i);\n\t\t}\n\t}\n\tuser->owner().unregisterInvitedToCallUser(conferenceId, user, !discard);\n}\n\nvoid Instance::showConferenceInvite(\n\t\tnot_null<UserData*> user,\n\t\tMsgId conferenceInviteMsgId) {\n\tconst auto item = user->owner().message(user, conferenceInviteMsgId);\n\tconst auto media = item ? item->media() : nullptr;\n\tconst auto call = media ? media->call() : nullptr;\n\tconst auto conferenceId = call ? call->conferenceId : 0;\n\tconst auto video = call->video;\n\tif (!conferenceId\n\t\t|| call->state != Data::CallState::Invitation\n\t\t|| user->isSelf()\n\t\t|| user->session().appConfig().callsDisabledForSession()) {\n\t\treturn;\n\t} else if (_currentCall\n\t\t&& _currentCall->conferenceId() == conferenceId) {\n\t\treturn;\n\t} else if (inGroupCall()\n\t\t&& _currentGroupCall->conference()\n\t\t&& _currentGroupCall->sharedCall()->id() == conferenceId) {\n\t\treturn;\n\t}\n\n\tauto conferenceParticipants = call->otherParticipants;\n\tif (!ranges::contains(conferenceParticipants, user)) {\n\t\tconferenceParticipants.push_back(user);\n\t}\n\n\tconst auto &config = user->session().serverConfig();\n\tif (inCall() || inGroupCall()) {\n\t\tdeclineIncomingConferenceInvites(conferenceId);\n\t} else if (item->date() + (config.callRingTimeoutMs / 1000)\n\t\t< base::unixtime::now()) {\n\t\tdeclineIncomingConferenceInvites(conferenceId);\n\t\tLOG((\"Ignoring too old conference call invitation.\"));\n\t} else {\n\t\tconst auto delegate = _delegate.get();\n\t\tauto call = std::make_unique<Call>(\n\t\t\tdelegate,\n\t\t\tuser,\n\t\t\tconferenceId,\n\t\t\tconferenceInviteMsgId,\n\t\t\tstd::move(conferenceParticipants),\n\t\t\tvideo);\n\t\tconst auto raw = call.get();\n\n\t\tuser->session().account().sessionChanges(\n\t\t) | rpl::on_next([=] {\n\t\t\tdestroyCall(raw);\n\t\t}, raw->lifetime());\n\n\t\tif (_currentCall) {\n\t\t\t_currentCallPanel->replaceCall(raw);\n\t\t\tstd::swap(_currentCall, call);\n\t\t\tcall->hangup();\n\t\t} else {\n\t\t\t_currentCallPanel = std::make_unique<Panel>(raw);\n\t\t\t_currentCall = std::move(call);\n\t\t}\n\t\t_currentCallChanges.fire_copy(raw);\n\t}\n}\n\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_instance.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n\nnamespace crl {\nclass semaphore;\n} // namespace crl\n\nnamespace Data {\nclass GroupCall;\n} // namespace Data\n\nnamespace Platform {\nenum class PermissionType;\n} // namespace Platform\n\nnamespace Media::Audio {\nclass Track;\n} // namespace Media::Audio\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass Show;\n} // namespace Ui\n\nnamespace Calls::Group {\nstruct JoinInfo;\nstruct ConferenceInfo;\nclass Panel;\nclass ChooseJoinAsProcess;\nclass StartRtmpProcess;\n} // namespace Calls::Group\n\nnamespace tgcalls {\nclass VideoCaptureInterface;\n} // namespace tgcalls\n\nnamespace Calls {\n\nclass Call;\nenum class CallType;\nclass GroupCall;\nclass Panel;\nstruct DhConfig;\nstruct InviteRequest;\nstruct StartConferenceInfo;\n\nstruct StartOutgoingCallArgs {\n\tbool video = false;\n\tbool isConfirmed = false;\n};\n\nstruct StartGroupCallArgs {\n\tenum class JoinConfirm {\n\t\tNone,\n\t\tIfNowInAnother,\n\t\tAlways,\n\t};\n\tQString joinHash;\n\tJoinConfirm confirm = JoinConfirm::IfNowInAnother;\n\tbool scheduleNeeded = false;\n};\n\nstruct ConferenceInviteMessages {\n\tbase::flat_set<MsgId> incoming;\n\tbase::flat_set<MsgId> outgoing;\n};\n\nstruct ConferenceInvites {\n\tbase::flat_map<not_null<UserData*>, ConferenceInviteMessages> users;\n};\n\nclass Instance final : public base::has_weak_ptr {\npublic:\n\tInstance();\n\t~Instance();\n\n\tvoid startOutgoingCall(not_null<UserData*> user, StartOutgoingCallArgs);\n\tvoid startOrJoinGroupCall(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tStartGroupCallArgs args);\n\tvoid startOrJoinConferenceCall(StartConferenceInfo args);\n\tvoid startedConferenceReady(\n\t\tnot_null<GroupCall*> call,\n\t\tStartConferenceInfo args);\n\tvoid showStartWithRtmp(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer);\n\tvoid handleUpdate(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPUpdate &update);\n\n\t// Called by Data::GroupCall when it is appropriate by the 'version'.\n\tvoid applyGroupCallUpdateChecked(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPUpdate &update);\n\n\tvoid showInfoPanel(not_null<Call*> call);\n\tvoid showInfoPanel(not_null<GroupCall*> call);\n\t[[nodiscard]] Call *currentCall() const;\n\t[[nodiscard]] rpl::producer<Call*> currentCallValue() const;\n\t[[nodiscard]] GroupCall *currentGroupCall() const;\n\t[[nodiscard]] rpl::producer<GroupCall*> currentGroupCallValue() const;\n\t[[nodiscard]] bool inCall() const;\n\t[[nodiscard]] bool inGroupCall() const;\n\t[[nodiscard]] bool hasVisiblePanel(\n\t\tMain::Session *session = nullptr) const;\n\t[[nodiscard]] bool hasActivePanel(\n\t\tMain::Session *session = nullptr) const;\n\tbool activateCurrentCall(const QString &joinHash = QString());\n\tbool minimizeCurrentActiveCall();\n\tbool toggleFullScreenCurrentActiveCall();\n\tbool closeCurrentActiveCall();\n\t[[nodiscard]] auto getVideoCapture(\n\t\tstd::optional<QString> deviceId = std::nullopt,\n\t\tbool isScreenCapture = false)\n\t\t-> std::shared_ptr<tgcalls::VideoCaptureInterface>;\n\tvoid requestPermissionsOrFail(Fn<void()> onSuccess, bool video = true);\n\n\t[[nodiscard]] const ConferenceInvites &conferenceInvites(\n\t\tCallId conferenceId) const;\n\tvoid registerConferenceInvite(\n\t\tCallId conferenceId,\n\t\tnot_null<UserData*> user,\n\t\tMsgId messageId,\n\t\tbool incoming);\n\tvoid unregisterConferenceInvite(\n\t\tCallId conferenceId,\n\t\tnot_null<UserData*> user,\n\t\tMsgId messageId,\n\t\tbool incoming,\n\t\tbool onlyStopCalling = false);\n\tvoid showConferenceInvite(\n\t\tnot_null<UserData*> user,\n\t\tMsgId conferenceInviteMsgId);\n\tvoid declineIncomingConferenceInvites(CallId conferenceId);\n\tvoid declineOutgoingConferenceInvite(\n\t\tCallId conferenceId,\n\t\tnot_null<UserData*> user,\n\t\tbool discard = false);\n\n\t[[nodiscard]] FnMut<void()> addAsyncWaiter();\n\n\tvoid registerVideoStream(not_null<GroupCall*> call);\n\n\t[[nodiscard]] bool isSharingScreen() const;\n\t[[nodiscard]] bool isQuitPrevent();\n\nprivate:\n\tclass Delegate;\n\tfriend class Delegate;\n\n\tnot_null<Media::Audio::Track*> ensureSoundLoaded(const QString &key);\n\tvoid playSoundOnce(const QString &key);\n\n\tvoid createCall(\n\t\tnot_null<UserData*> user,\n\t\tCallType type,\n\t\tStartOutgoingCallArgs);\n\tvoid destroyCall(not_null<Call*> call);\n\tvoid finishConferenceInvitations(const StartConferenceInfo &args);\n\n\tvoid createGroupCall(\n\t\tGroup::JoinInfo info,\n\t\tconst MTPInputGroupCall &inputCall);\n\tvoid destroyGroupCall(not_null<GroupCall*> call);\n\tvoid confirmLeaveCurrent(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tStartGroupCallArgs args,\n\t\tFn<void(StartGroupCallArgs)> confirmed);\n\n\tvoid requestPermissionOrFail(\n\t\tPlatform::PermissionType type,\n\t\tFn<void()> onSuccess);\n\n\tvoid refreshDhConfig();\n\tvoid refreshServerConfig(not_null<Main::Session*> session);\n\tbytes::const_span updateDhConfig(const MTPmessages_DhConfig &data);\n\n\tvoid destroyCurrentCall(\n\t\tData::GroupCall *migrateCall = nullptr,\n\t\tconst QString &migrateSlug = QString());\n\tvoid handleCallUpdate(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPPhoneCall &call);\n\tvoid handleSignalingData(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDupdatePhoneCallSignalingData &data);\n\tvoid handleGroupCallUpdate(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPUpdate &update);\n\n\tconst std::unique_ptr<Delegate> _delegate;\n\tconst std::unique_ptr<DhConfig> _cachedDhConfig;\n\n\tcrl::time _lastServerConfigUpdateTime = 0;\n\tbase::weak_ptr<Main::Session> _serverConfigRequestSession;\n\tstd::weak_ptr<tgcalls::VideoCaptureInterface> _videoCapture;\n\n\tstd::unique_ptr<Call> _currentCall;\n\trpl::event_stream<Call*> _currentCallChanges;\n\tstd::unique_ptr<Panel> _currentCallPanel;\n\n\tstd::unique_ptr<GroupCall> _currentGroupCall;\n\tstd::unique_ptr<GroupCall> _startingGroupCall;\n\trpl::event_stream<GroupCall*> _currentGroupCallChanges;\n\tstd::unique_ptr<Group::Panel> _currentGroupCallPanel;\n\n\tbase::flat_map<QString, std::unique_ptr<Media::Audio::Track>> _tracks;\n\n\tconst std::unique_ptr<Group::ChooseJoinAsProcess> _chooseJoinAs;\n\tconst std::unique_ptr<Group::StartRtmpProcess> _startWithRtmp;\n\n\tbase::flat_map<CallId, ConferenceInvites> _conferenceInvites;\n\n\tbase::flat_set<std::unique_ptr<crl::semaphore>> _asyncWaiters;\n\n\tbase::flat_map<\n\t\tnot_null<Main::Session*>,\n\t\tstd::vector<base::weak_ptr<GroupCall>>> _streams;\n\n};\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_panel.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/calls_panel.h\"\n\n#include \"boxes/peers/replace_boost_box.h\" // CreateUserpicsWithMoreBadge\n#include \"calls/calls_panel_background.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_cloud_file.h\"\n#include \"data/data_changes.h\"\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/group/calls_group_invite_controller.h\"\n#include \"calls/ui/calls_device_menu.h\"\n#include \"calls/calls_emoji_fingerprint.h\"\n#include \"calls/calls_instance.h\"\n#include \"calls/calls_signal_bars.h\"\n#include \"calls/calls_userpic.h\"\n#include \"calls/calls_video_bubble.h\"\n#include \"calls/calls_video_incoming.h\"\n#include \"calls/calls_window.h\"\n#include \"ui/platform/ui_platform_window_title.h\"\n#include \"ui/widgets/call_button.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/rp_window.h\"\n#include \"ui/layers/layer_manager.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/image/image.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/platform/ui_platform_utility.h\"\n#include \"ui/gl/gl_surface.h\"\n#include \"ui/gl/gl_shader.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/integration.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/session/session_show.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"platform/platform_specific.h\"\n#include \"base/event_filter.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/power_save_blocker.h\"\n#include \"media/streaming/media_streaming_utility.h\"\n#include \"window/main_window.h\"\n#include \"window/window_controller.h\"\n#include \"webrtc/webrtc_create_adm.h\"\n#include \"webrtc/webrtc_environment.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_chat.h\"\n\n#include <QtWidgets/QApplication>\n#include <QtGui/QWindow>\n#include <QtCore/QTimer>\n#include <QtSvg/QSvgRenderer>\n\nnamespace Calls {\nnamespace {\n\nconstexpr auto kHideControlsTimeout = 5 * crl::time(1000);\nconstexpr auto kHideControlsQuickTimeout = 2 * crl::time(1000);\n\n[[nodiscard]] QByteArray BatterySvg(\n\t\tconst QSize &s,\n\t\tconst QColor &c) {\n\tconst auto color = u\"rgb(%1,%2,%3)\"_q\n\t\t.arg(c.red())\n\t\t.arg(c.green())\n\t\t.arg(c.blue())\n\t\t.toUtf8();\n\tconst auto width = QString::number(s.width()).toUtf8();\n\tconst auto height = QString::number(s.height()).toUtf8();\n\treturn R\"(\n<svg width=\")\" + width + R\"(\" height=\")\" + height\n\t+ R\"(\" viewBox=\"0 0 )\" + width + R\"( )\" + height + R\"(\" fill=\"none\">\n\t<rect x=\"1.33598\" y=\"0.5\" width=\"24\" height=\"12\" rx=\"4\" stroke=\")\" + color + R\"(\"/>\n\t<path\n\t\td=\"M26.836 4.66666V8.66666C27.6407 8.32788 28.164 7.53979 28.164 6.66666C28.164 5.79352 27.6407 5.00543 26.836 4.66666Z\"\n\t\tfill=\")\" + color + R\"(\"/>\n\t<path\n\t\td=\"M 5.5 3.5 H 5.5 A 0.5 0.5 0 0 1 6 4 V 9 A 0.5 0.5 0 0 1 5.5 9.5 H 5.5 A 0.5 0.5 0 0 1 5 9 V 4 A 0.5 0.5 0 0 1 5.5 3.5 Z M 5 4 V 9 A 0.5 0.5 0 0 0 5.5 9.5 H 5.5 A 0.5 0.5 0 0 0 6 9 V 4 A 0.5 0.5 0 0 0 5.5 3.5 H 5.5 A 0.5 0.5 0 0 0 5 4 Z\"\n\t\ttransform=\"matrix(1, 0, 0, 1, 0, 0)\" + \")\\\" stroke=\\\"\" + color + R\"(\"/>\n</svg>)\";\n}\n\n} // namespace\n\nPanel::Panel(not_null<Call*> call)\n: _call(call)\n, _user(call->user())\n, _window(std::make_shared<Window>())\n, _bodySt(&st::callBodyLayout)\n, _answerHangupRedial(\n\tstd::in_place,\n\twidget(),\n\tst::callAnswer,\n\t&st::callHangup)\n, _decline(\n\tstd::in_place,\n\twidget(),\n\tobject_ptr<Ui::CallButton>(widget(), st::callHangup))\n, _cancel(\n\tstd::in_place,\n\twidget(),\n\tobject_ptr<Ui::CallButton>(widget(), st::callCancel))\n, _screencast(\n\tstd::in_place,\n\twidget(),\n\tobject_ptr<Ui::CallButton>(\n\t\twidget(),\n\t\tst::callScreencastOn,\n\t\t&st::callScreencastOff))\n, _camera(std::in_place, widget(), st::callCameraMute, &st::callCameraUnmute)\n, _mute(\n\tstd::in_place,\n\twidget(),\n\tobject_ptr<Ui::CallButton>(\n\t\twidget(),\n\t\tst::callMicrophoneMute,\n\t\t&st::callMicrophoneUnmute))\n, _addPeople(\n\tstd::in_place,\n\twidget(),\n\tobject_ptr<Ui::CallButton>(widget(), st::callAddPeople))\n, _name(std::in_place, widget(), st::callName)\n, _status(std::in_place, widget(), st::callStatus)\n, _hideControlsTimer([=] { requestControlsHidden(true); })\n, _controlsShownForceTimer([=] { controlsShownForce(false); }) {\n\t_decline->setDuration(st::callPanelDuration);\n\t_decline->entity()->setText(tr::lng_call_decline());\n\t_decline->entity()->setAccessibleName(tr::lng_call_decline(tr::now));\n\t_cancel->setDuration(st::callPanelDuration);\n\t_cancel->entity()->setText(tr::lng_call_cancel());\n\t_cancel->entity()->setAccessibleName(tr::lng_call_cancel(tr::now));\n\t_screencast->setDuration(st::callPanelDuration);\n\t_screencast->entity()->setAccessibleName(tr::lng_call_screencast(tr::now));\n\t_addPeople->setDuration(st::callPanelDuration);\n\t_addPeople->entity()->setText(tr::lng_call_add_people());\n\t_addPeople->entity()->setAccessibleName(tr::lng_call_add_people(tr::now));\n\t_camera->setAccessibleName(tr::lng_call_start_video(tr::now));\n\t_mute->entity()->setAccessibleName(tr::lng_call_mute_audio(tr::now));\n\n\tinitWindow();\n\tinitWidget();\n\tinitControls();\n\tinitConferenceInvite();\n\tinitLayout();\n\tinitMediaDeviceToggles();\n\tshowAndActivate();\n}\n\nPanel::~Panel() = default;\n\nbool Panel::isVisible() const {\n\treturn window()->isVisible()\n\t\t&& !(window()->windowState() & Qt::WindowMinimized);\n}\n\nbool Panel::isActive() const {\n\treturn window()->isActiveWindow() && isVisible();\n}\n\nQRect Panel::panelGeometry() const {\n\tconst auto saved = Core::App().settings().callPanelPosition();\n\tconst auto adjusted = Core::AdjustToScale(saved, u\"Call\"_q);\n\tconst auto center = Core::App().getPointForCallPanelCenter();\n\tconst auto simple = QRect(0, 0, st::callWidth, st::callHeight);\n\tconst auto initial = simple.translated(center - simple.center());\n\tconst auto initialPosition = Core::WindowPosition{\n\t\t.moncrc = 0,\n\t\t.scale = cScale(),\n\t\t.x = initial.x(),\n\t\t.y = initial.y(),\n\t\t.w = initial.width(),\n\t\t.h = initial.height(),\n\t};\n\treturn ::Window::CountInitialGeometry(\n\t\twindow(),\n\t\tadjusted,\n\t\tinitialPosition,\n\t\t{ st::callWidthMin, st::callHeightMin },\n\t\tu\"Call\"_q);\n}\n\nConferencePanelMigration Panel::migrationInfo() const {\n\treturn ConferencePanelMigration{ .window = _window };\n}\n\nstd::shared_ptr<Main::SessionShow> Panel::sessionShow() {\n\treturn Main::MakeSessionShow(uiShow(), &_user->session());\n}\n\nstd::shared_ptr<Ui::Show> Panel::uiShow() {\n\treturn _window->uiShow();\n}\n\nvoid Panel::showAndActivate() {\n\tif (window()->isHidden()) {\n\t\twindow()->show();\n\t}\n\tconst auto state = window()->windowState();\n\tif (state & Qt::WindowMinimized) {\n\t\twindow()->setWindowState(state & ~Qt::WindowMinimized);\n\t}\n\twindow()->raise();\n\twindow()->activateWindow();\n\twindow()->setFocus();\n}\n\nvoid Panel::minimize() {\n\twindow()->setWindowState(window()->windowState() | Qt::WindowMinimized);\n}\n\nvoid Panel::toggleFullScreen() {\n\ttoggleFullScreen(!window()->isFullScreen());\n}\n\nvoid Panel::replaceCall(not_null<Call*> call) {\n\treinitWithCall(call);\n\tupdateControlsGeometry();\n}\n\nvoid Panel::savePanelGeometry() {\n\tif (!window()->windowHandle()) {\n\t\treturn;\n\t}\n\tconst auto state = window()->windowHandle()->windowState();\n\tif (state == Qt::WindowMinimized) {\n\t\treturn;\n\t}\n\tconst auto &savedPosition = Core::App().settings().callPanelPosition();\n\tauto realPosition = savedPosition;\n\tif (state == Qt::WindowMaximized) {\n\t\trealPosition.maximized = 1;\n\t\trealPosition.moncrc = 0;\n\t} else {\n\t\tauto r = window()->body()->mapToGlobal(window()->body()->rect());\n\t\trealPosition.x = r.x();\n\t\trealPosition.y = r.y();\n\t\trealPosition.w = r.width();\n\t\trealPosition.h = r.height();\n\t\trealPosition.scale = cScale();\n\t\trealPosition.maximized = 0;\n\t\trealPosition.moncrc = 0;\n\t}\n\trealPosition = ::Window::PositionWithScreen(\n\t\trealPosition,\n\t\twindow(),\n\t\t{ st::callWidthMin, st::callHeightMin },\n\t\tu\"Call\"_q);\n\tif (realPosition.w >= st::callWidthMin\n\t\t&& realPosition.h >= st::callHeightMin\n\t\t&& realPosition != savedPosition) {\n\t\tCore::App().settings().setCallPanelPosition(realPosition);\n\t\tCore::App().saveSettingsDelayed();\n\t}\n}\n\nvoid Panel::initWindow() {\n\twindow()->setAttribute(Qt::WA_OpaquePaintEvent);\n\twindow()->setAttribute(Qt::WA_NoSystemBackground);\n\twindow()->setTitle(_user->name());\n\twindow()->setTitleStyle(st::callTitle);\n\n\tbase::install_event_filter(window().get(), [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::Close && handleClose()) {\n\t\t\te->ignore();\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t} else if (e->type() == QEvent::KeyPress) {\n\t\t\tif ((static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Escape)\n\t\t\t\t&& window()->isFullScreen()) {\n\t\t\t\twindow()->showNormal();\n\t\t\t}\n\t\t} else if (e->type() == QEvent::WindowStateChange) {\n\t\t\tconst auto state = window()->windowState();\n\t\t\t_fullScreenOrMaximized = (state & Qt::WindowFullScreen)\n\t\t\t\t|| (state & Qt::WindowMaximized);\n\t\t} else if (e->type() == QEvent::Enter) {\n\t\t\t_mouseInside = true;\n\t\t\tUi::Integration::Instance().registerLeaveSubscription(\n\t\t\t\twindow().get());\n\t\t\tif (!_fullScreenOrMaximized.current()) {\n\t\t\t\trequestControlsHidden(false);\n\t\t\t\t_hideControlsTimer.cancel();\n\t\t\t}\n\t\t} else if (e->type() == QEvent::Leave) {\n\t\t\t_mouseInside = false;\n\t\t\tUi::Integration::Instance().unregisterLeaveSubscription(\n\t\t\t\twindow().get());\n\t\t\tif (!_fullScreenOrMaximized.current()) {\n\t\t\t\t_hideControlsTimer.callOnce(kHideControlsQuickTimeout);\n\t\t\t}\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t}, lifetime());\n\n\tconst auto guard = base::make_weak(this);\n\twindow()->setBodyTitleArea([=](QPoint widgetPoint) {\n\t\tusing Flag = Ui::WindowTitleHitTestFlag;\n\t\tif (!guard\n\t\t\t|| !widget()->rect().contains(widgetPoint)\n\t\t\t|| _window->controlsHasHitTest(widgetPoint)) {\n\t\t\treturn Flag::None | Flag(0);\n\t\t}\n\t\tconst auto buttonWidth = st::callCancel.button.width;\n\t\tconst auto buttonsWidth = buttonWidth * 4;\n\t\tconst auto inControls = (_fingerprint\n\t\t\t&& _fingerprint->geometry().contains(widgetPoint))\n\t\t\t|| QRect(\n\t\t\t\t(widget()->width() - buttonsWidth) / 2,\n\t\t\t\t_answerHangupRedial->y(),\n\t\t\t\tbuttonsWidth,\n\t\t\t\t_answerHangupRedial->height()).contains(widgetPoint)\n\t\t\t|| (!_outgoingPreviewInBody\n\t\t\t\t&& _outgoingVideoBubble->geometry().contains(widgetPoint));\n\t\tif (inControls) {\n\t\t\treturn Flag::None | Flag(0);\n\t\t}\n\t\tconst auto shown = _window->topShownLayer();\n\t\treturn (!shown || !shown->geometry().contains(widgetPoint))\n\t\t\t? (Flag::Move | Flag::Menu | Flag::FullScreen)\n\t\t\t: Flag::None;\n\t});\n\n\t_window->maximizeRequests() | rpl::on_next([=](bool maximized) {\n\t\ttoggleFullScreen(maximized);\n\t}, lifetime());\n\t// Don't do that, it looks awful :(\n//#ifdef Q_OS_WIN\n//\t// On Windows we replace snap-to-top maximizing with fullscreen.\n//\t//\n//\t// We have to switch first to showNormal, so that showFullScreen\n//\t// will remember correct normal window geometry and next showNormal\n//\t// will show it instead of a moving maximized window.\n//\t//\n//\t// We have to do it in InvokeQueued, otherwise it still captures\n//\t// the maximized window geometry and saves it.\n//\t//\n//\t// I couldn't find a less glitchy way to do that *sigh*.\n//\tconst auto object = window()->windowHandle();\n//\tconst auto signal = &QWindow::windowStateChanged;\n//\tQObject::connect(object, signal, [=](Qt::WindowState state) {\n//\t\tif (state == Qt::WindowMaximized) {\n//\t\t\tInvokeQueued(object, [=] {\n//\t\t\t\twindow()->showNormal();\n//\t\t\t\tInvokeQueued(object, [=] {\n//\t\t\t\t\twindow()->showFullScreen();\n//\t\t\t\t});\n//\t\t\t});\n//\t\t}\n//\t});\n//#endif // Q_OS_WIN\n}\n\nvoid Panel::initWidget() {\n\twidget()->setMouseTracking(true);\n\n\twidget()->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tpaint(clip);\n\t}, lifetime());\n\n\twidget()->sizeValue(\n\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\tupdateControlsGeometry();\n\t}, lifetime());\n}\n\nvoid Panel::initControls() {\n\t_hangupShown = (_call->type() == Type::Outgoing);\n\t_mute->entity()->setClickedCallback([=] {\n\t\tif (_call) {\n\t\t\t_call->setMuted(!_call->muted());\n\t\t}\n\t});\n\t_screencast->entity()->setClickedCallback([=] {\n\t\tconst auto env = &Core::App().mediaDevices();\n\t\tif (!_call) {\n\t\t\treturn;\n\t\t} else if (!env->desktopCaptureAllowed()) {\n\t\t\tif (auto box = Group::ScreenSharingPrivacyRequestBox()) {\n\t\t\t\tuiShow()->showBox(std::move(box));\n\t\t\t}\n\t\t} else if (const auto source = env->uniqueDesktopCaptureSource()) {\n\t\t\tif (!chooseSourceActiveDeviceId().isEmpty()) {\n\t\t\t\tchooseSourceStop();\n\t\t\t} else if (chooseSourceWithAudioSupported()) {\n\t\t\t\tconst auto sourceId = *source;\n\t\t\t\tGroup::ShowUniqueCaptureOptions(\n\t\t\t\t\tuiShow(),\n\t\t\t\t\tcrl::guard(this, [=](bool audio) {\n\t\t\t\t\t\tchooseSourceAccepted(sourceId, audio);\n\t\t\t\t\t}));\n\t\t\t} else {\n\t\t\t\tchooseSourceAccepted(*source, false);\n\t\t\t}\n\t\t} else {\n\t\t\tGroup::Ui::DesktopCapture::ChooseSource(this);\n\t\t}\n\t});\n\t_camera->setClickedCallback([=] {\n\t\tif (!_call) {\n\t\t\treturn;\n\t\t}\n\t\t_call->toggleCameraSharing(!_call->isSharingCamera());\n\t});\n\t_addPeople->entity()->setClickedCallback([=] {\n\t\tif (!_call || _call->state() != Call::State::Established) {\n\t\t\tuiShow()->showToast(tr::lng_call_error_add_not_started(tr::now));\n\t\t\treturn;\n\t\t}\n\t\tconst auto call = _call;\n\t\tconst auto creating = std::make_shared<bool>();\n\t\tconst auto create = [=](std::vector<InviteRequest> users) {\n\t\t\tif (*creating) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t*creating = true;\n\t\t\tconst auto sharingLink = users.empty();\n\t\t\tCore::App().calls().startOrJoinConferenceCall({\n\t\t\t\t.show = sessionShow(),\n\t\t\t\t.invite = std::move(users),\n\t\t\t\t.sharingLink = sharingLink,\n\t\t\t\t.migrating = true,\n\t\t\t\t.muted = call->muted(),\n\t\t\t\t.videoCapture = (call->isSharingVideo()\n\t\t\t\t\t? call->peekVideoCapture()\n\t\t\t\t\t: nullptr),\n\t\t\t\t.videoCaptureScreenId = call->screenSharingDeviceId(),\n\t\t\t});\n\t\t};\n\t\tconst auto invite = crl::guard(call, [=](\n\t\t\t\tstd::vector<InviteRequest> users) {\n\t\t\tcreate(std::move(users));\n\t\t});\n\t\tconst auto share = crl::guard(call, [=] {\n\t\t\tcreate({});\n\t\t});\n\t\tuiShow()->showBox(Group::PrepareInviteBox(call, invite, share));\n\t});\n\n\t_updateDurationTimer.setCallback([this] {\n\t\tif (_call) {\n\t\t\tupdateStatusText(_call->state());\n\t\t}\n\t});\n\t_updateOuterRippleTimer.setCallback([this] {\n\t\tif (_call) {\n\t\t\t_answerHangupRedial->setOuterValue(\n\t\t\t\t_call->getWaitingSoundPeakValue());\n\t\t} else {\n\t\t\t_answerHangupRedial->setOuterValue(0.);\n\t\t\t_updateOuterRippleTimer.cancel();\n\t\t}\n\t});\n\t_answerHangupRedial->setClickedCallback([this] {\n\t\tif (!_call || _hangupShownProgress.animating()) {\n\t\t\treturn;\n\t\t}\n\t\tauto state = _call->state();\n\t\tif (state == State::Busy) {\n\t\t\t_call->redial();\n\t\t} else if (_call->isIncomingWaiting()) {\n\t\t\t_call->answer();\n\t\t} else if (state == State::WaitingUserConfirmation) {\n\t\t\t_startOutgoingRequests.fire(false);\n\t\t} else {\n\t\t\t_call->hangup();\n\t\t}\n\t});\n\tauto hangupCallback = [this] {\n\t\tif (_call) {\n\t\t\t_call->hangup();\n\t\t}\n\t};\n\t_decline->entity()->setClickedCallback(hangupCallback);\n\t_cancel->entity()->setClickedCallback(hangupCallback);\n\n\treinitWithCall(_call);\n\n\t_decline->finishAnimating();\n\t_cancel->finishAnimating();\n\t_screencast->finishAnimating();\n}\n\nvoid Panel::initConferenceInvite() {\n\tconst auto &participants = _call->conferenceParticipants();\n\tconst auto count = int(participants.size());\n\tif (count < 2) {\n\t\treturn;\n\t}\n\t_conferenceParticipants = base::make_unique_q<Ui::RpWidget>(widget());\n\t_conferenceParticipants->show();\n\tconst auto raw = _conferenceParticipants.get();\n\n\tauto peers = std::vector<not_null<PeerData*>>();\n\tfor (const auto &peer : participants) {\n\t\tif (peer == _user && count > 3) {\n\t\t\tcontinue;\n\t\t}\n\t\tpeers.push_back(peer);\n\t\tif (peers.size() == 3) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst auto userpics = CreateUserpicsWithMoreBadge(\n\t\traw,\n\t\trpl::single(peers),\n\t\tst::confcallInviteUserpics,\n\t\tpeers.size()).release();\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\ttr::lng_group_call_members(tr::now, lt_count, count),\n\t\tst::confcallInviteParticipants);\n\tconst auto padding = st::confcallInviteParticipantsPadding;\n\tconst auto add = padding.bottom();\n\tconst auto width = add\n\t\t+ userpics->width()\n\t\t+ padding.left()\n\t\t+ label->width()\n\t\t+ padding.right();\n\tconst auto height = add + userpics->height() + add;\n\n\t_status->geometryValue() | rpl::on_next([=] {\n\t\tconst auto top = _bodyTop + _bodySt->participantsTop;\n\t\tconst auto left = (widget()->width() - width) / 2;\n\t\traw->setGeometry(left, top, width, height);\n\t\tuserpics->move(add, add);\n\t\tlabel->move(add + userpics->width() + padding.left(), padding.top());\n\t}, raw->lifetime());\n\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto radius = raw->height() / 2.;\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::confcallInviteUserpicsBg);\n\t\tp.drawRoundedRect(raw->rect(), radius, radius);\n\t}, raw->lifetime());\n}\n\nvoid Panel::setIncomingSize(QSize size) {\n\tif (_incomingFrameSize == size) {\n\t\treturn;\n\t}\n\t_incomingFrameSize = size;\n\trefreshIncomingGeometry();\n\tshowControls();\n}\n\nQWidget *Panel::chooseSourceParent() {\n\treturn window().get();\n}\n\nQString Panel::chooseSourceActiveDeviceId() {\n\treturn _call->screenSharingDeviceId();\n}\n\nbool Panel::chooseSourceActiveWithAudio() {\n\treturn _call->screenSharingWithAudio();\n}\n\nbool Panel::chooseSourceWithAudioSupported() {\n\treturn Webrtc::LoopbackAudioCaptureSupported();\n}\n\nrpl::lifetime &Panel::chooseSourceInstanceLifetime() {\n\treturn lifetime();\n}\n\nrpl::producer<bool> Panel::startOutgoingRequests() const {\n\treturn _startOutgoingRequests.events(\n\t) | rpl::filter([=] {\n\t\treturn _call && (_call->state() == State::WaitingUserConfirmation);\n\t});\n}\n\nvoid Panel::chooseSourceAccepted(\n\t\tconst QString &deviceId,\n\t\tbool withAudio) {\n\t_call->toggleScreenSharing(deviceId, withAudio);\n}\n\nvoid Panel::chooseSourceStop() {\n\t_call->toggleScreenSharing(std::nullopt);\n}\n\nvoid Panel::refreshIncomingGeometry() {\n\tExpects(_call != nullptr);\n\tExpects(_incoming != nullptr);\n\n\tif (_incomingFrameSize.isEmpty()) {\n\t\t_incoming->widget()->hide();\n\t\treturn;\n\t}\n\tconst auto to = widget()->size();\n\tconst auto use = ::Media::Streaming::DecideFrameResize(\n\t\tto,\n\t\t_incomingFrameSize\n\t).result;\n\tconst auto pos = QPoint(\n\t\t(to.width() - use.width()) / 2,\n\t\t(to.height() - use.height()) / 2);\n\t_incoming->widget()->setGeometry(QRect(pos, use));\n\t_incoming->widget()->show();\n}\n\nvoid Panel::reinitWithCall(Call *call) {\n\t_callLifetime.destroy();\n\t_call = call;\n\tconst auto guard = gsl::finally([&] {\n\t\tupdateControlsShown();\n\t});\n\tif (!_call) {\n\t\t_fingerprint = nullptr;\n\t\t_incoming = nullptr;\n\t\t_outgoingVideoBubble = nullptr;\n\t\treturn;\n\t}\n\n\t_user = _call->user();\n\n\t_background = std::make_unique<PanelBackground>(\n\t\t_user,\n\t\t[=] {\n\t\t\tupdateTextColors();\n\t\t\twidget()->update();\n\t\t});\n\n\t_call->confereceSupportedValue(\n\t) | rpl::on_next([=](bool supported) {\n\t\t_conferenceSupported = supported;\n\t\t_addPeople->toggle(_conferenceSupported\n\t\t\t&& (_call->state() != State::WaitingUserConfirmation),\n\t\t\twindow()->isHidden() ? anim::type::instant : anim::type::normal);\n\n\t\tupdateHangupGeometry();\n\t}, _callLifetime);\n\n\tauto remoteMuted = _call->remoteAudioStateValue(\n\t) | rpl::map(rpl::mappers::_1 == Call::RemoteAudioState::Muted);\n\trpl::duplicate(\n\t\tremoteMuted\n\t) | rpl::on_next([=](bool muted) {\n\t\tif (muted) {\n\t\t\tcreateRemoteAudioMute();\n\t\t} else {\n\t\t\t_remoteAudioMute = nullptr;\n\t\t\tshowRemoteLowBattery();\n\t\t}\n\t}, _callLifetime);\n\t_call->remoteBatteryStateValue(\n\t) | rpl::on_next([=](Call::RemoteBatteryState state) {\n\t\tif (state == Call::RemoteBatteryState::Low) {\n\t\t\tcreateRemoteLowBattery();\n\t\t} else {\n\t\t\t_remoteLowBattery = nullptr;\n\t\t}\n\t}, _callLifetime);\n\t_userpic = std::make_unique<Userpic>(\n\t\twidget(),\n\t\t_user,\n\t\tstd::move(remoteMuted));\n\t_outgoingVideoBubble = std::make_unique<VideoBubble>(\n\t\twidget(),\n\t\t_call->videoOutgoing());\n\t_incoming = std::make_unique<Incoming>(\n\t\twidget(),\n\t\t_call->videoIncoming(),\n\t\t_window->backend());\n\t_incoming->widget()->hide();\n\n\t_incoming->rp()->shownValue() | rpl::on_next([=] {\n\t\tupdateControlsShown();\n\t}, _incoming->rp()->lifetime());\n\n\t_hideControlsFilter = nullptr;\n\t_fullScreenOrMaximized.value(\n\t) | rpl::on_next([=](bool fullScreenOrMaximized) {\n\t\tif (fullScreenOrMaximized) {\n\t\t\tclass Filter final : public QObject {\n\t\t\tpublic:\n\t\t\t\texplicit Filter(Fn<void(QObject*)> moved) : _moved(moved) {\n\t\t\t\t\tqApp->installEventFilter(this);\n\t\t\t\t}\n\n\t\t\t\tbool eventFilter(QObject *watched, QEvent *event) {\n\t\t\t\t\tif (event->type() == QEvent::MouseMove) {\n\t\t\t\t\t\t_moved(watched);\n\t\t\t\t\t}\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\tprivate:\n\t\t\t\tFn<void(QObject*)> _moved;\n\n\t\t\t};\n\t\t\t_hideControlsFilter.reset(new Filter([=](QObject *what) {\n\t\t\t\t_mouseInside = true;\n\t\t\t\tif (what->isWidgetType()\n\t\t\t\t\t&& window()->isAncestorOf(static_cast<QWidget*>(what))) {\n\t\t\t\t\t_hideControlsTimer.callOnce(kHideControlsTimeout);\n\t\t\t\t\trequestControlsHidden(false);\n\t\t\t\t\tupdateControlsShown();\n\t\t\t\t}\n\t\t\t}));\n\t\t\t_hideControlsTimer.callOnce(kHideControlsTimeout);\n\t\t} else {\n\t\t\t_hideControlsFilter = nullptr;\n\t\t\t_hideControlsTimer.cancel();\n\t\t\tif (_mouseInside) {\n\t\t\t\trequestControlsHidden(false);\n\t\t\t\tupdateControlsShown();\n\t\t\t}\n\t\t}\n\t}, _incoming->rp()->lifetime());\n\n\t_call->mutedValue(\n\t) | rpl::on_next([=](bool mute) {\n\t\t_mute->entity()->setProgress(mute ? 1. : 0.);\n\t\t_mute->entity()->setText(mute\n\t\t\t? tr::lng_call_unmute_audio()\n\t\t\t: tr::lng_call_mute_audio());\n\t\t_mute->entity()->setAccessibleName(mute\n\t\t\t? tr::lng_call_unmute_audio(tr::now)\n\t\t\t: tr::lng_call_mute_audio(tr::now));\n\t}, _callLifetime);\n\n\t_call->videoOutgoing()->stateValue(\n\t) | rpl::on_next([=] {\n\t\t{\n\t\t\tconst auto active = _call->isSharingCamera();\n\t\t\t_camera->setProgress(active ? 0. : 1.);\n\t\t\t_camera->setText(active\n\t\t\t\t? tr::lng_call_stop_video()\n\t\t\t\t: tr::lng_call_start_video());\n\t\t\t_camera->setAccessibleName(active\n\t\t\t\t? tr::lng_call_stop_video(tr::now)\n\t\t\t\t: tr::lng_call_start_video(tr::now));\n\t\t}\n\t\t{\n\t\t\tconst auto active = _call->isSharingScreen();\n\t\t\t_screencast->entity()->setProgress(active ? 0. : 1.);\n\t\t\t_screencast->entity()->setText(tr::lng_call_screencast());\n\t\t\t_outgoingVideoBubble->setMirrored(!active);\n\t\t}\n\t}, _callLifetime);\n\n\t_call->stateValue(\n\t) | rpl::on_next([=](State state) {\n\t\tstateChanged(state);\n\t}, _callLifetime);\n\n\t_call->videoIncoming()->renderNextFrame(\n\t) | rpl::on_next([=] {\n\t\tconst auto track = _call->videoIncoming();\n\t\tsetIncomingSize(track->state() == Webrtc::VideoState::Active\n\t\t\t? track->frameSize()\n\t\t\t: QSize());\n\t\tif (_incoming->widget()->isHidden()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto incoming = incomingFrameGeometry();\n\t\tconst auto outgoing = outgoingFrameGeometry();\n\t\t_incoming->widget()->update();\n\t\tif (incoming.intersects(outgoing)) {\n\t\t\twidget()->update(outgoing);\n\t\t}\n\t}, _callLifetime);\n\n\t_call->videoIncoming()->stateValue(\n\t) | rpl::on_next([=](Webrtc::VideoState state) {\n\t\tsetIncomingSize((state == Webrtc::VideoState::Active)\n\t\t\t? _call->videoIncoming()->frameSize()\n\t\t\t: QSize());\n\t}, _callLifetime);\n\n\t_call->videoOutgoing()->renderNextFrame(\n\t) | rpl::on_next([=] {\n\t\tconst auto incoming = incomingFrameGeometry();\n\t\tconst auto outgoing = outgoingFrameGeometry();\n\t\twidget()->update(outgoing);\n\t\tif (incoming.intersects(outgoing)) {\n\t\t\t_incoming->widget()->update();\n\t\t}\n\t}, _callLifetime);\n\n\trpl::combine(\n\t\t_call->stateValue(),\n\t\trpl::single(\n\t\t\trpl::empty_value()\n\t\t) | rpl::then(_call->videoOutgoing()->renderNextFrame())\n\t) | rpl::on_next([=](State state, auto) {\n\t\tif (state != State::Ended\n\t\t\t&& state != State::EndedByOtherDevice\n\t\t\t&& state != State::Failed\n\t\t\t&& state != State::FailedHangingUp\n\t\t\t&& state != State::MigrationHangingUp\n\t\t\t&& state != State::HangingUp) {\n\t\t\trefreshOutgoingPreviewInBody(state);\n\t\t}\n\t}, _callLifetime);\n\n\t_call->errors(\n\t) | rpl::on_next([=](Error error) {\n\t\tconst auto text = [=] {\n\t\t\tswitch (error.type) {\n\t\t\tcase ErrorType::NoCamera:\n\t\t\t\treturn tr::lng_call_error_no_camera(tr::now);\n\t\t\tcase ErrorType::NotVideoCall:\n\t\t\t\treturn tr::lng_call_error_camera_outdated(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\t_user->name());\n\t\t\tcase ErrorType::NotStartedCall:\n\t\t\t\treturn tr::lng_call_error_camera_not_started(tr::now);\n\t\t\t\t//case ErrorType::NoMicrophone:\n\t\t\t\t//\treturn tr::lng_call_error_no_camera(tr::now);\n\t\t\tcase ErrorType::Unknown:\n\t\t\t\treturn Lang::Hard::CallErrorIncompatible();\n\t\t\t}\n\t\t\tUnexpected(\"Error type in _call->errors().\");\n\t\t}();\n\t\tuiShow()->showToast(text);\n\t}, _callLifetime);\n\n\t_name->setText(_user->name());\n\tupdateStatusText(_call->state());\n\tupdateTextColors();\n\n\t_answerHangupRedial->raise();\n\t_decline->raise();\n\t_cancel->raise();\n\t_camera->raise();\n\tif (_startVideo) {\n\t\t_startVideo->raise();\n\t}\n\t_mute->raise();\n\t_addPeople->raise();\n\n\t_incoming->widget()->lower();\n}\n\nvoid Panel::createRemoteAudioMute() {\n\t_remoteAudioMute = base::make_unique_q<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\twidget(),\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\twidget(),\n\t\t\ttr::lng_call_microphone_off(\n\t\t\t\tlt_user,\n\t\t\t\t_user->session().changes().peerFlagsValue(\n\t\t\t\t\t_user,\n\t\t\t\t\tData::PeerUpdate::Flag::Name\n\t\t\t\t) | rpl::map([=] { return _user->shortName(); })),\n\t\t\tst::callRemoteAudioMute),\n\t\tst::callTooltipPadding);\n\t_remoteAudioMute->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t_remoteAudioMute->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(_remoteAudioMute);\n\t\tconst auto r = _remoteAudioMute->rect();\n\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setOpacity(_controlsShownAnimation.value(\n\t\t\t_controlsShown ? 1. : 0.));\n\t\tp.setBrush(st::videoPlayIconBg);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawRoundedRect(r, r.height() / 2, r.height() / 2);\n\n\t\tst::callTooltipMutedIcon.paint(\n\t\t\tp,\n\t\t\tst::callTooltipMutedIconPosition,\n\t\t\t_remoteAudioMute->width());\n\t}, _remoteAudioMute->lifetime());\n\n\tshowControls();\n\tupdateControlsGeometry();\n}\n\nvoid Panel::createRemoteLowBattery() {\n\t_remoteLowBattery = base::make_unique_q<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\twidget(),\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\twidget(),\n\t\t\ttr::lng_call_battery_level_low(\n\t\t\t\tlt_user,\n\t\t\t\t_user->session().changes().peerFlagsValue(\n\t\t\t\t\t_user,\n\t\t\t\t\tData::PeerUpdate::Flag::Name\n\t\t\t\t) | rpl::map([=] { return _user->shortName(); })),\n\t\t\tst::callRemoteAudioMute),\n\t\tst::callTooltipPadding);\n\t_remoteLowBattery->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_remoteLowBattery = nullptr;\n\t\tcreateRemoteLowBattery();\n\t}, _remoteLowBattery->lifetime());\n\n\tconstexpr auto kBatterySize = QSize(29, 13);\n\tconst auto scaledBatterySize = QSize(\n\t\tstyle::ConvertScale(kBatterySize.width()),\n\t\tstyle::ConvertScale(kBatterySize.height()));\n\n\tconst auto icon = [&] {\n\t\tauto svg = QSvgRenderer(\n\t\t\tBatterySvg(kBatterySize, st::videoPlayIconFg->c));\n\t\tauto image = QImage(\n\t\t\tscaledBatterySize * style::DevicePixelRatio(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\t\timage.fill(Qt::transparent);\n\t\t{\n\t\t\tauto p = QPainter(&image);\n\t\t\tsvg.render(&p, Rect(scaledBatterySize));\n\t\t}\n\t\treturn image;\n\t}();\n\n\t_remoteLowBattery->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(_remoteLowBattery);\n\t\tconst auto r = _remoteLowBattery->rect();\n\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setOpacity(_controlsShownAnimation.value(\n\t\t\t_controlsShown ? 1. : 0.));\n\t\tp.setBrush(st::videoPlayIconBg);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawRoundedRect(r, r.height() / 2, r.height() / 2);\n\n\t\tp.drawImage(\n\t\t\tst::callTooltipMutedIconPosition.x(),\n\t\t\t(r.height() - scaledBatterySize.height()) / 2,\n\t\t\ticon);\n\t}, _remoteLowBattery->lifetime());\n\n\tshowControls();\n\tupdateControlsGeometry();\n}\n\nvoid Panel::showRemoteLowBattery() {\n\tif (_remoteLowBattery) {\n\t\t_remoteLowBattery->setVisible(!_remoteAudioMute\n\t\t\t|| _remoteAudioMute->isHidden());\n\t}\n}\n\nvoid Panel::initLayout() {\n\tinitGeometry();\n\n\t_name->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_status->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tusing UpdateFlag = Data::PeerUpdate::Flag;\n\t_user->session().changes().peerUpdates(\n\t\tUpdateFlag::Name\n\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\t// _user may change for the same Panel.\n\t\treturn (_call != nullptr) && (update.peer == _user);\n\t}) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\t_name->setText(_call->user()->name());\n\t\tupdateControlsGeometry();\n\t}, lifetime());\n\n\t_window->raiseControls();\n}\n\nvoid Panel::showControls() {\n\tExpects(_call != nullptr);\n\n\twidget()->showChildren();\n\t_decline->setVisible(_decline->toggled());\n\t_cancel->setVisible(_cancel->toggled());\n\t_screencast->setVisible(_screencast->toggled());\n\t_addPeople->setVisible(_addPeople->toggled());\n\n\tconst auto shown = !_incomingFrameSize.isEmpty();\n\t_incoming->widget()->setVisible(shown);\n\t_name->setVisible(!shown);\n\t_status->setVisible(!shown);\n\t_userpic->setVisible(!shown);\n\tif (_remoteAudioMute) {\n\t\t_remoteAudioMute->setVisible(shown);\n\t}\n\tshowRemoteLowBattery();\n}\n\nvoid Panel::closeBeforeDestroy(bool windowIsReused) {\n\tif (!windowIsReused) {\n\t\twindow()->close();\n\t}\n\treinitWithCall(nullptr);\n\t_lifetime.destroy();\n}\n\nrpl::lifetime &Panel::lifetime() {\n\treturn _lifetime;\n}\n\nvoid Panel::initGeometry() {\n\twindow()->setGeometry(panelGeometry());\n\twindow()->setMinimumSize({ st::callWidthMin, st::callHeightMin });\n\twindow()->show();\n\tupdateControlsGeometry();\n\n\t_geometryLifetime = window()->geometryValue(\n\t) | rpl::skip(1) | rpl::on_next([=](QRect r) {\n\t\tsavePanelGeometry();\n\t});\n}\n\nvoid Panel::initMediaDeviceToggles() {\n\t_cameraDeviceToggle = _camera->addCornerButton(\n\t\tst::callCornerButton,\n\t\t&st::callCornerButtonInactive);\n\t_audioDeviceToggle = _mute->entity()->addCornerButton(\n\t\tst::callCornerButton,\n\t\t&st::callCornerButtonInactive);\n\n\t_cameraDeviceToggle->setClickedCallback([=] {\n\t\tshowDevicesMenu(_cameraDeviceToggle, {\n\t\t\t{ Webrtc::DeviceType::Camera, _call->cameraDeviceIdValue() },\n\t\t});\n\t});\n\t_cameraDeviceToggle->setAccessibleName(tr::lng_settings_call_camera(tr::now));\n\t_audioDeviceToggle->setClickedCallback([=] {\n\t\tshowDevicesMenu(_audioDeviceToggle, {\n\t\t\t{ Webrtc::DeviceType::Playback, _call->playbackDeviceIdValue() },\n\t\t\t{ Webrtc::DeviceType::Capture, _call->captureDeviceIdValue() },\n\t\t});\n\t});\n\t_audioDeviceToggle->setAccessibleName(tr::lng_settings_call_section_output(tr::now));\n}\n\nvoid Panel::showDevicesMenu(\n\t\tnot_null<QWidget*> button,\n\t\tstd::vector<DeviceSelection> types) {\n\tif (!_call || _devicesMenu) {\n\t\treturn;\n\t}\n\tconst auto chosen = [=](Webrtc::DeviceType type, QString id) {\n\t\tswitch (type) {\n\t\tcase Webrtc::DeviceType::Playback:\n\t\t\tCore::App().settings().setCallPlaybackDeviceId(id);\n\t\t\tbreak;\n\t\tcase Webrtc::DeviceType::Capture:\n\t\t\tCore::App().settings().setCallCaptureDeviceId(id);\n\t\t\tbreak;\n\t\tcase Webrtc::DeviceType::Camera:\n\t\t\tCore::App().settings().setCameraDeviceId(id);\n\t\t\tbreak;\n\t\t}\n\t\tCore::App().saveSettingsDelayed();\n\t};\n\tcontrolsShownForce(true);\n\tupdateControlsShown();\n\n\t_devicesMenu = MakeDeviceSelectionMenu(\n\t\twidget(),\n\t\t&Core::App().mediaDevices(),\n\t\tstd::move(types),\n\t\tchosen);\n\t_devicesMenu->setForcedVerticalOrigin(\n\t\tUi::PopupMenu::VerticalOrigin::Bottom);\n\t_devicesMenu->popup(button->mapToGlobal(QPoint())\n\t\t- QPoint(st::callDeviceSelectionMenu.menu.widthMin / 2, 0));\n\tQObject::connect(_devicesMenu.get(), &QObject::destroyed, window(), [=] {\n\t\t_controlsShownForceTimer.callOnce(kHideControlsQuickTimeout);\n\t});\n}\n\nvoid Panel::refreshOutgoingPreviewInBody(State state) {\n\tconst auto inBody = (state != State::Established)\n\t\t&& (_call->videoOutgoing()->state() != Webrtc::VideoState::Inactive)\n\t\t&& !_call->videoOutgoing()->frameSize().isEmpty();\n\tif (_outgoingPreviewInBody == inBody) {\n\t\treturn;\n\t}\n\t_outgoingPreviewInBody = inBody;\n\t_bodySt = inBody ? &st::callBodyWithPreview : &st::callBodyLayout;\n\tupdateControlsGeometry();\n}\n\nvoid Panel::toggleFullScreen(bool fullscreen) {\n\tif (fullscreen) {\n\t\twindow()->showFullScreen();\n\t} else {\n\t\twindow()->showNormal();\n\t}\n}\n\nQRect Panel::incomingFrameGeometry() const {\n\treturn (!_incoming || _incoming->widget()->isHidden())\n\t\t? QRect()\n\t\t: _incoming->widget()->geometry();\n}\n\nQRect Panel::outgoingFrameGeometry() const {\n\treturn _outgoingVideoBubble->geometry();\n}\n\nvoid Panel::requestControlsHidden(bool hidden) {\n\t_hideControlsRequested = hidden;\n\tupdateControlsShown();\n}\n\nvoid Panel::controlsShownForce(bool shown) {\n\t_controlsShownForce = shown;\n\tif (shown) {\n\t\t_controlsShownForceTimer.cancel();\n\t}\n\tupdateControlsShown();\n}\n\nvoid Panel::updateControlsShown() {\n\tconst auto shown = !_incoming\n\t\t|| _incoming->widget()->isHidden()\n\t\t|| _controlsShownForce\n\t\t|| !_hideControlsRequested;\n\tif (_controlsShown != shown) {\n\t\t_controlsShown = shown;\n\t\t_controlsShownAnimation.start([=] {\n\t\t\tupdateControlsGeometry();\n\t\t}, shown ? 0. : 1., shown ? 1. : 0., st::slideDuration);\n\t\tupdateControlsGeometry();\n\t}\n}\n\nvoid Panel::updateControlsGeometry() {\n\tif (widget()->size().isEmpty()) {\n\t\treturn;\n\t}\n\tif (_incoming) {\n\t\trefreshIncomingGeometry();\n\t}\n\tconst auto shown = _controlsShownAnimation.value(\n\t\t_controlsShown ? 1. : 0.);\n\tif (_fingerprint) {\n#ifndef Q_OS_MAC\n\t\tconst auto controlsGeometry = _window->controlsGeometry();\n\t\tconst auto halfWidth = widget()->width() / 2;\n\t\tconst auto minLeft = (controlsGeometry.center().x() < halfWidth)\n\t\t\t? (controlsGeometry.width() + st::callFingerprintTop)\n\t\t\t: 0;\n\t\tconst auto minRight = (controlsGeometry.center().x() >= halfWidth)\n\t\t\t? (controlsGeometry.width() + st::callFingerprintTop)\n\t\t\t: 0;\n\t\t_incoming->setControlsAlignment(minLeft\n\t\t\t? style::al_left\n\t\t\t: style::al_right);\n#else // !Q_OS_MAC\n\t\tconst auto minLeft = 0;\n\t\tconst auto minRight = 0;\n#endif // _controls\n\t\tconst auto desired = (widget()->width() - _fingerprint->width()) / 2;\n\t\tconst auto top = anim::interpolate(\n\t\t\t-_fingerprint->height(),\n\t\t\tst::callFingerprintTop,\n\t\t\tshown);\n\t\tif (minLeft) {\n\t\t\t_fingerprint->moveToLeft(std::max(desired, minLeft), top);\n\t\t} else {\n\t\t\t_fingerprint->moveToRight(std::max(desired, minRight), top);\n\t\t}\n\t}\n\tconst auto innerHeight = std::max(widget()->height(), st::callHeightMin);\n\tconst auto innerWidth = widget()->width() - 2 * st::callInnerPadding;\n\tconst auto availableTop = st::callFingerprintTop\n\t\t+ (_fingerprint ? _fingerprint->height() : 0)\n\t\t+ st::callFingerprintBottom;\n\tconst auto available = widget()->height()\n\t\t- st::callBottomControlsHeight\n\t\t- availableTop;\n\tconst auto bodyPreviewSizeMax = st::callOutgoingPreviewMin\n\t\t+ ((st::callOutgoingPreview\n\t\t\t- st::callOutgoingPreviewMin)\n\t\t\t* (innerHeight - st::callHeightMin)\n\t\t\t/ (st::callHeight - st::callHeightMin));\n\tconst auto bodyPreviewSize = QSize(\n\t\tstd::min(\n\t\t\tbodyPreviewSizeMax.width(),\n\t\t\tstd::min(innerWidth, st::callOutgoingPreviewMax.width())),\n\t\tstd::min(\n\t\t\tbodyPreviewSizeMax.height(),\n\t\t\tst::callOutgoingPreviewMax.height()));\n\tconst auto bodyContentHeight = _bodySt->height\n\t\t+ (_conferenceParticipants\n\t\t\t? (_bodySt->participantsTop - _bodySt->statusTop)\n\t\t\t: 0);\n\tconst auto contentHeight = bodyContentHeight\n\t\t+ (_outgoingPreviewInBody ? bodyPreviewSize.height() : 0);\n\tconst auto remainingHeight = available - contentHeight;\n\tconst auto skipHeight = remainingHeight\n\t\t/ (_outgoingPreviewInBody ? 3 : 2);\n\n\t_bodyTop = availableTop + skipHeight;\n\t_buttonsTopShown = availableTop + available;\n\t_buttonsTop = anim::interpolate(\n\t\twidget()->height(),\n\t\t_buttonsTopShown,\n\t\tshown);\n\tconst auto previewTop = _bodyTop + bodyContentHeight + skipHeight;\n\n\t_userpic->setGeometry(\n\t\t(widget()->width() - _bodySt->photoSize) / 2,\n\t\t_bodyTop + _bodySt->photoTop,\n\t\t_bodySt->photoSize);\n\t_userpic->setMuteLayout(\n\t\t_bodySt->mutePosition,\n\t\t_bodySt->muteSize,\n\t\t_bodySt->muteStroke);\n\n\tif (_name->naturalWidth() > innerWidth) {\n\t\t_name->resizeToWidth(innerWidth);\n\t}\n\t_name->moveToLeft(\n\t\t(widget()->width() - _name->width()) / 2,\n\t\t_bodyTop + _bodySt->nameTop);\n\tupdateStatusGeometry();\n\n\tif (_remoteAudioMute) {\n\t\t_remoteAudioMute->moveToLeft(\n\t\t\t(widget()->width() - _remoteAudioMute->width()) / 2,\n\t\t\t(_buttonsTop\n\t\t\t\t- st::callRemoteAudioMuteSkip\n\t\t\t\t- _remoteAudioMute->height()));\n\t\t_remoteAudioMute->update();\n\t\t_remoteAudioMute->entity()->setOpacity(shown);\n\t}\n\tif (_remoteLowBattery) {\n\t\t_remoteLowBattery->moveToLeft(\n\t\t\t(widget()->width() - _remoteLowBattery->width()) / 2,\n\t\t\t(_buttonsTop\n\t\t\t\t- st::callRemoteAudioMuteSkip\n\t\t\t\t- _remoteLowBattery->height()));\n\t\t_remoteLowBattery->update();\n\t\t_remoteLowBattery->entity()->setOpacity(shown);\n\t}\n\n\tif (_outgoingPreviewInBody) {\n\t\t_outgoingVideoBubble->updateGeometry(\n\t\t\tVideoBubble::DragMode::None,\n\t\t\tQRect(\n\t\t\t\t(widget()->width() - bodyPreviewSize.width()) / 2,\n\t\t\t\tpreviewTop,\n\t\t\t\tbodyPreviewSize.width(),\n\t\t\t\tbodyPreviewSize.height()));\n\t} else if (_outgoingVideoBubble) {\n\t\tupdateOutgoingVideoBubbleGeometry();\n\t}\n\n\tupdateHangupGeometry();\n}\n\nvoid Panel::updateOutgoingVideoBubbleGeometry() {\n\tExpects(!_outgoingPreviewInBody);\n\n\tconst auto size = st::callOutgoingDefaultSize;\n\t_outgoingVideoBubble->updateGeometry(\n\t\tVideoBubble::DragMode::SnapToCorners,\n\t\twidget()->rect() - Margins(st::callInnerPadding),\n\t\tsize);\n}\n\nvoid Panel::updateHangupGeometry() {\n\tconst auto isBusy = (_call\n\t\t&& _call->state() == State::Busy);\n\tconst auto isWaitingUser = (_call\n\t\t&& _call->state() == State::WaitingUserConfirmation);\n\tconst auto incomingWaiting = _call && _call->isIncomingWaiting();\n\tconst auto hangupProgress = isWaitingUser\n\t\t? 0.\n\t\t: _hangupShownProgress.value(_hangupShown ? 1. : 0.);\n\t_answerHangupRedial->setProgress(hangupProgress);\n\n\t// Screencast - Camera - Cancel/Decline - Answer/Hangup/Redial - Mute.\n\tconst auto buttonWidth = st::callCancel.button.width;\n\tconst auto cancelWidth = buttonWidth * (1. - hangupProgress);\n\tconst auto cancelLeft = (widget()->width() - buttonWidth) / 2\n\t\t- ((isBusy || incomingWaiting) ? buttonWidth : 0)\n\t\t+ ((isWaitingUser || _conferenceSupported) ? 0 : (buttonWidth / 2));\n\n\t_cancel->moveToLeft(cancelLeft, _buttonsTop);\n\t_decline->moveToLeft(cancelLeft, _buttonsTop);\n\t_camera->moveToLeft(cancelLeft - buttonWidth, _buttonsTop);\n\t_screencast->moveToLeft(_camera->x() - buttonWidth, _buttonsTop);\n\t_answerHangupRedial->moveToLeft(cancelLeft + cancelWidth, _buttonsTop);\n\t_mute->moveToLeft(_answerHangupRedial->x() + buttonWidth, _buttonsTop);\n\t_addPeople->moveToLeft(_mute->x() + buttonWidth, _buttonsTop);\n\tif (_startVideo) {\n\t\t_startVideo->moveToLeft(_camera->x(), _camera->y());\n\t}\n}\n\nvoid Panel::updateStatusGeometry() {\n\t_status->moveToLeft(\n\t\t(widget()->width() - _status->width()) / 2,\n\t\t_bodyTop + _bodySt->statusTop);\n}\n\nvoid Panel::paint(QRect clip) {\n\tauto p = QPainter(widget());\n\n\tauto region = QRegion(clip);\n\tif (!_incoming->widget()->isHidden()) {\n\t\tregion = region.subtracted(QRegion(_incoming->widget()->geometry()));\n\t}\n\n\tif (_background) {\n\t\t_background->paint(\n\t\t\tp,\n\t\t\twidget()->size(),\n\t\t\t_bodyTop,\n\t\t\t_bodySt->photoTop,\n\t\t\t_bodySt->photoSize,\n\t\t\tregion);\n\t} else {\n\t\tfor (const auto &rect : region) {\n\t\t\tp.fillRect(rect, st::callBgOpaque);\n\t\t}\n\t}\n\n\tif (_incoming && _incoming->widget()->isHidden()) {\n\t\t_call->videoIncoming()->markFrameShown();\n\t}\n}\n\nbool Panel::handleClose() const {\n\tif (_call) {\n\t\tif (_call->state() == Call::State::WaitingUserConfirmation\n\t\t\t|| _call->state() == Call::State::Busy\n\t\t\t|| _call->state() == Call::State::Starting\n\t\t\t|| _call->state() == Call::State::WaitingIncoming) {\n\t\t\t_call->hangup();\n\t\t} else {\n\t\t\twindow()->hide();\n\t\t}\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nnot_null<Ui::RpWindow*> Panel::window() const {\n\treturn _window->window();\n}\n\nnot_null<Ui::RpWidget*> Panel::widget() const {\n\treturn _window->widget();\n}\n\nnot_null<UserData*> Panel::user() const {\n\treturn _user;\n}\n\nvoid Panel::stateChanged(State state) {\n\tExpects(_call != nullptr);\n\n\tupdateStatusText(state);\n\n\tconst auto isBusy = (state == State::Busy);\n\tconst auto isWaitingUser = (state == State::WaitingUserConfirmation);\n\t_window->togglePowerSaveBlocker(!isBusy && !isWaitingUser);\n\n\tif ((state != State::HangingUp)\n\t\t&& (state != State::MigrationHangingUp)\n\t\t&& (state != State::Ended)\n\t\t&& (state != State::EndedByOtherDevice)\n\t\t&& (state != State::FailedHangingUp)\n\t\t&& (state != State::Failed)) {\n\t\tif (_startVideo && !isWaitingUser) {\n\t\t\t_startVideo = nullptr;\n\t\t} else if (!_startVideo && isWaitingUser) {\n\t\t\t_startVideo = base::make_unique_q<Ui::CallButton>(\n\t\t\t\twidget(),\n\t\t\t\tst::callStartVideo);\n\t\t\t_startVideo->show();\n\t\t\t_startVideo->setText(tr::lng_call_start_video());\n\t\t\t_startVideo->setAccessibleName(tr::lng_call_start_video(tr::now));\n\t\t\t_startVideo->clicks() | rpl::map_to(true) | rpl::start_to_stream(\n\t\t\t\t_startOutgoingRequests,\n\t\t\t\t_startVideo->lifetime());\n\t\t}\n\t\t_camera->setVisible(!_startVideo);\n\n\t\tconst auto windowHidden = window()->isHidden();\n\t\tconst auto toggleButton = [&](auto &&button, bool visible) {\n\t\t\tbutton->toggle(\n\t\t\t\tvisible,\n\t\t\t\t(windowHidden ? anim::type::instant : anim::type::normal));\n\t\t};\n\t\tconst auto incomingWaiting = _call->isIncomingWaiting();\n\t\tif (incomingWaiting) {\n\t\t\t_updateOuterRippleTimer.callEach(Call::kSoundSampleMs);\n\t\t}\n\t\ttoggleButton(_decline, incomingWaiting);\n\t\ttoggleButton(_cancel, (isBusy || isWaitingUser));\n\t\ttoggleButton(_mute, !isWaitingUser);\n\t\ttoggleButton(\n\t\t\t_screencast,\n\t\t\t!(isBusy || isWaitingUser || incomingWaiting));\n\t\ttoggleButton(_addPeople, !isWaitingUser && _conferenceSupported);\n\t\tconst auto hangupShown = !_decline->toggled()\n\t\t\t&& !_cancel->toggled();\n\t\tif (_hangupShown != hangupShown) {\n\t\t\t_hangupShown = hangupShown;\n\t\t\t_hangupShownProgress.start(\n\t\t\t\t[this] { updateHangupGeometry(); },\n\t\t\t\t_hangupShown ? 0. : 1.,\n\t\t\t\t_hangupShown ? 1. : 0.,\n\t\t\t\tst::callPanelDuration,\n\t\t\t\tanim::sineInOut);\n\t\t}\n\t\tconst auto answerHangupRedialState = incomingWaiting\n\t\t\t? AnswerHangupRedialState::Answer\n\t\t\t: isBusy\n\t\t\t? AnswerHangupRedialState::Redial\n\t\t\t: isWaitingUser\n\t\t\t? AnswerHangupRedialState::StartCall\n\t\t\t: AnswerHangupRedialState::Hangup;\n\t\tif (_answerHangupRedialState != answerHangupRedialState) {\n\t\t\t_answerHangupRedialState = answerHangupRedialState;\n\t\t\trefreshAnswerHangupRedialLabel();\n\t\t}\n\t\tif (!_call->isKeyShaForFingerprintReady()) {\n\t\t\t_fingerprint = nullptr;\n\t\t} else if (!_fingerprint) {\n\t\t\t_fingerprint = CreateFingerprintAndSignalBars(widget(), _call);\n\t\t\tupdateControlsGeometry();\n\t\t}\n\t}\n}\n\nvoid Panel::refreshAnswerHangupRedialLabel() {\n\tExpects(_answerHangupRedialState.has_value());\n\n\tconst auto phrase = [&] {\n\t\tswitch (*_answerHangupRedialState) {\n\t\tcase AnswerHangupRedialState::Answer: return tr::lng_call_accept;\n\t\tcase AnswerHangupRedialState::Hangup: return tr::lng_call_end_call;\n\t\tcase AnswerHangupRedialState::Redial: return tr::lng_call_redial;\n\t\tcase AnswerHangupRedialState::StartCall: return tr::lng_call_start;\n\t\t}\n\t\tUnexpected(\"AnswerHangupRedialState value.\");\n\t}();\n\t_answerHangupRedial->setText(phrase());\n\t_answerHangupRedial->setAccessibleName(phrase(tr::now));\n}\n\nvoid Panel::updateStatusText(State state) {\n\tauto statusText = [this, state]() -> QString {\n\t\tswitch (state) {\n\t\tcase State::Starting:\n\t\tcase State::WaitingInit:\n\t\tcase State::WaitingInitAck:\n\t\tcase State::MigrationHangingUp: return tr::lng_call_status_connecting(tr::now);\n\t\tcase State::Established: {\n\t\t\tif (_call) {\n\t\t\t\tauto durationMs = _call->getDurationMs();\n\t\t\t\tauto durationSeconds = durationMs / 1000;\n\t\t\t\tstartDurationUpdateTimer(durationMs);\n\t\t\t\treturn Ui::FormatDurationText(durationSeconds);\n\t\t\t}\n\t\t\treturn tr::lng_call_status_ended(tr::now);\n\t\t} break;\n\t\tcase State::FailedHangingUp:\n\t\tcase State::Failed: return tr::lng_call_status_failed(tr::now);\n\t\tcase State::HangingUp: return tr::lng_call_status_hanging(tr::now);\n\t\tcase State::Ended:\n\t\tcase State::EndedByOtherDevice: return tr::lng_call_status_ended(tr::now);\n\t\tcase State::ExchangingKeys: return tr::lng_call_status_exchanging(tr::now);\n\t\tcase State::Waiting: return tr::lng_call_status_waiting(tr::now);\n\t\tcase State::Requesting: return tr::lng_call_status_requesting(tr::now);\n\t\tcase State::WaitingIncoming:\n\t\t\treturn (_call->conferenceInvite()\n\t\t\t\t? tr::lng_call_status_group_invite(tr::now)\n\t\t\t\t: tr::lng_call_status_incoming(tr::now));\n\t\tcase State::Ringing: return tr::lng_call_status_ringing(tr::now);\n\t\tcase State::Busy: return tr::lng_call_status_busy(tr::now);\n\t\tcase State::WaitingUserConfirmation: return tr::lng_call_status_sure(tr::now);\n\t\t}\n\t\tUnexpected(\"State in stateChanged()\");\n\t};\n\t_status->setText(statusText());\n\tupdateStatusGeometry();\n}\n\nvoid Panel::updateTextColors() {\n\tif (!_background) {\n\t\t_name->setTextColorOverride(std::nullopt);\n\t\t_status->setTextColorOverride(std::nullopt);\n\t\treturn;\n\t}\n\t_name->setTextColorOverride(\n\t\t_background->textColorOverride(st::callName.textFg));\n\t_status->setTextColorOverride(\n\t\t_background->textColorOverride(st::callStatus.textFg));\n}\n\nvoid Panel::startDurationUpdateTimer(crl::time currentDuration) {\n\tauto msTillNextSecond = 1000 - (currentDuration % 1000);\n\t_updateDurationTimer.callOnce(msTillNextSecond + 5);\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_panel.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"calls/calls_call.h\"\n#include \"calls/group/ui/desktop_capture_choose_source.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/rp_widget.h\"\n\nclass Image;\n\nnamespace base {\nclass PowerSaveBlocker;\n} // namespace base\n\nnamespace Data {\nclass PhotoMedia;\n} // namespace Data\n\nnamespace Main {\nclass SessionShow;\n} // namespace Main\n\nnamespace Ui {\nclass Show;\nclass BoxContent;\nclass LayerWidget;\nenum class LayerOption;\nusing LayerOptions = base::flags<LayerOption>;\nclass IconButton;\nclass CallButton;\nclass LayerManager;\nclass FlatLabel;\ntemplate <typename Widget>\nclass FadeWrap;\ntemplate <typename Widget>\nclass PaddingWrap;\nclass RpWindow;\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Ui::Toast {\nclass Instance;\nstruct Config;\n} // namespace Ui::Toast\n\nnamespace Ui::Platform {\nstruct SeparateTitleControls;\n} // namespace Ui::Platform\n\nnamespace style {\nstruct CallSignalBars;\nstruct CallBodyLayout;\n} // namespace style\n\nnamespace Calls {\n\nclass Window;\nclass Userpic;\nclass SignalBars;\nclass VideoBubble;\nclass PanelBackground;\nstruct DeviceSelection;\nstruct ConferencePanelMigration;\n\nclass Panel final\n\t: public base::has_weak_ptr\n\t, private Group::Ui::DesktopCapture::ChooseSourceDelegate {\npublic:\n\tPanel(not_null<Call*> call);\n\t~Panel();\n\n\t[[nodiscard]] not_null<Ui::RpWidget*> widget() const;\n\t[[nodiscard]] not_null<UserData*> user() const;\n\t[[nodiscard]] bool isVisible() const;\n\t[[nodiscard]] bool isActive() const;\n\t[[nodiscard]] QRect panelGeometry() const;\n\n\t[[nodiscard]] ConferencePanelMigration migrationInfo() const;\n\n\tvoid showAndActivate();\n\tvoid minimize();\n\tvoid toggleFullScreen();\n\tvoid replaceCall(not_null<Call*> call);\n\tvoid savePanelGeometry();\n\tvoid closeBeforeDestroy(bool windowIsReused = false);\n\n\tQWidget *chooseSourceParent() override;\n\tQString chooseSourceActiveDeviceId() override;\n\tbool chooseSourceActiveWithAudio() override;\n\tbool chooseSourceWithAudioSupported() override;\n\trpl::lifetime &chooseSourceInstanceLifetime() override;\n\tvoid chooseSourceAccepted(\n\t\tconst QString &deviceId,\n\t\tbool withAudio) override;\n\tvoid chooseSourceStop() override;\n\n\t[[nodiscard]] rpl::producer<bool> startOutgoingRequests() const;\n\n\t[[nodiscard]] std::shared_ptr<Main::SessionShow> sessionShow();\n\t[[nodiscard]] std::shared_ptr<Ui::Show> uiShow();\n\n\t[[nodiscard]] not_null<Ui::RpWindow*> window() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tclass Incoming;\n\tusing State = Call::State;\n\tusing Type = Call::Type;\n\tenum class AnswerHangupRedialState : uchar {\n\t\tAnswer,\n\t\tHangup,\n\t\tRedial,\n\t\tStartCall,\n\t};\n\n\tvoid paint(QRect clip);\n\n\tvoid initWindow();\n\tvoid initWidget();\n\tvoid initControls();\n\tvoid initConferenceInvite();\n\tvoid reinitWithCall(Call *call);\n\tvoid initLayout();\n\tvoid initMediaDeviceToggles();\n\tvoid initGeometry();\n\n\t[[nodiscard]] bool handleClose() const;\n\n\tvoid requestControlsHidden(bool hidden);\n\tvoid controlsShownForce(bool shown);\n\tvoid updateControlsShown();\n\tvoid updateControlsGeometry();\n\tvoid updateHangupGeometry();\n\tvoid updateStatusGeometry();\n\tvoid updateOutgoingVideoBubbleGeometry();\n\tvoid stateChanged(State state);\n\tvoid showControls();\n\tvoid updateStatusText(State state);\n\tvoid updateTextColors();\n\tvoid startDurationUpdateTimer(crl::time currentDuration);\n\tvoid setIncomingSize(QSize size);\n\tvoid refreshIncomingGeometry();\n\n\tvoid refreshOutgoingPreviewInBody(State state);\n\tvoid toggleFullScreen(bool fullscreen);\n\tvoid createRemoteAudioMute();\n\tvoid createRemoteLowBattery();\n\tvoid showRemoteLowBattery();\n\tvoid refreshAnswerHangupRedialLabel();\n\n\tvoid showDevicesMenu(\n\t\tnot_null<QWidget*> button,\n\t\tstd::vector<DeviceSelection> types);\n\n\t[[nodiscard]] QRect incomingFrameGeometry() const;\n\t[[nodiscard]] QRect outgoingFrameGeometry() const;\n\n\tCall *_call = nullptr;\n\tnot_null<UserData*> _user;\n\n\tstd::shared_ptr<Window> _window;\n\tstd::unique_ptr<Incoming> _incoming;\n\n\tQSize _incomingFrameSize;\n\n\trpl::lifetime _callLifetime;\n\n\tnot_null<const style::CallBodyLayout*> _bodySt;\n\tbase::unique_qptr<Ui::CallButton> _answerHangupRedial;\n\tbase::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _decline;\n\tbase::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _cancel;\n\tbool _hangupShown = false;\n\tbool _conferenceSupported = false;\n\tbool _outgoingPreviewInBody = false;\n\tstd::optional<AnswerHangupRedialState> _answerHangupRedialState;\n\tUi::Animations::Simple _hangupShownProgress;\n\tbase::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _screencast;\n\tbase::unique_qptr<Ui::CallButton> _camera;\n\tUi::CallButton *_cameraDeviceToggle = nullptr;\n\tbase::unique_qptr<Ui::CallButton> _startVideo;\n\tbase::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _mute;\n\tUi::CallButton *_audioDeviceToggle = nullptr;\n\tbase::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _addPeople;\n\tbase::unique_qptr<Ui::FlatLabel> _name;\n\tbase::unique_qptr<Ui::FlatLabel> _status;\n\tbase::unique_qptr<Ui::RpWidget> _conferenceParticipants;\n\tbase::unique_qptr<Ui::RpWidget> _fingerprint;\n\tbase::unique_qptr<Ui::PaddingWrap<Ui::FlatLabel>> _remoteAudioMute;\n\tbase::unique_qptr<Ui::PaddingWrap<Ui::FlatLabel>> _remoteLowBattery;\n\tstd::unique_ptr<Userpic> _userpic;\n\tstd::unique_ptr<VideoBubble> _outgoingVideoBubble;\n\tQPixmap _bottomShadow;\n\tint _bodyTop = 0;\n\tint _buttonsTopShown = 0;\n\tint _buttonsTop = 0;\n\n\tbase::Timer _hideControlsTimer;\n\tbase::Timer _controlsShownForceTimer;\n\tstd::unique_ptr<QObject> _hideControlsFilter;\n\tbool _hideControlsRequested = false;\n\trpl::variable<bool> _fullScreenOrMaximized;\n\tUi::Animations::Simple _controlsShownAnimation;\n\tbool _controlsShownForce = false;\n\tbool _controlsShown = true;\n\tbool _mouseInside = false;\n\n\tbase::unique_qptr<Ui::PopupMenu> _devicesMenu;\n\n\tbase::Timer _updateDurationTimer;\n\tbase::Timer _updateOuterRippleTimer;\n\n\trpl::event_stream<bool> _startOutgoingRequests;\n\n\tstd::unique_ptr<PanelBackground> _background;\n\n\trpl::lifetime _geometryLifetime;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_panel_background.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/calls_panel_background.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_peer_colors.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_document.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"main/main_session.h\"\n#include \"ui/color_contrast.h\"\n#include \"ui/top_background_gradient.h\"\n#include \"styles/style_calls.h\"\n\nnamespace Calls {\n\nPanelBackground::PanelBackground(\n\tnot_null<PeerData*> peer,\n\tFn<void()> updateCallback)\n: _peer(peer)\n, _updateCallback(std::move(updateCallback)) {\n\tupdateColors();\n\tupdateEmojiId();\n\n\t_peer->session().changes().peerFlagsValue(\n\t\t_peer,\n\t\tData::PeerUpdate::Flag::ColorProfile\n\t\t\t| Data::PeerUpdate::Flag::EmojiStatus\n\t) | rpl::on_next([=] {\n\t\tupdateColors();\n\t\t_brushSize = QSize();\n\t\tif (_updateCallback) {\n\t\t\t_updateCallback();\n\t\t}\n\t}, _lifetime);\n\n\t_peer->session().changes().peerFlagsValue(\n\t\t_peer,\n\t\tData::PeerUpdate::Flag::BackgroundEmoji\n\t\t\t| Data::PeerUpdate::Flag::EmojiStatus\n\t) | rpl::on_next([=] {\n\t\tupdateEmojiId();\n\t\tif (_updateCallback) {\n\t\t\t_updateCallback();\n\t\t}\n\t}, _lifetime);\n}\n\nvoid PanelBackground::paint(\n\t\tQPainter &p,\n\t\tQSize widgetSize,\n\t\tint bodyTop,\n\t\tint photoTop,\n\t\tint photoSize,\n\t\tconst QRegion &region) {\n\tif (!_colors || _colors->bg.empty()) {\n\t\tfor (const auto &rect : region) {\n\t\t\tp.fillRect(rect, st::callBgOpaque);\n\t\t}\n\t\treturn;\n\t}\n\n\tconst auto &colors = _colors->bg;\n\tif (colors.size() == 1) {\n\t\tfor (const auto &rect : region) {\n\t\t\tp.fillRect(rect, colors.front());\n\t\t}\n\t} else {\n\t\tconst auto center = QPoint(\n\t\t\twidgetSize.width() / 2,\n\t\t\tbodyTop + photoTop + photoSize / 2);\n\t\tconst auto radius = std::max(\n\t\t\tstd::hypot(center.x(), center.y()),\n\t\t\tstd::hypot(widgetSize.width() - center.x(),\n\t\t\t\twidgetSize.height() - center.y()));\n\t\tif (_brushSize != widgetSize) {\n\t\t\tupdateBrush(widgetSize, center, radius, colors);\n\t\t}\n\t\tfor (const auto &rect : region) {\n\t\t\tp.fillRect(rect, _brush);\n\t\t}\n\t}\n\n\tconst auto emojiId = _currentEmojiId;\n\tif (!emojiId) {\n\t\t_emoji = nullptr;\n\t\t_cachedImage = QImage();\n\t\t_cachedEmojiId = 0;\n\t\treturn;\n\t}\n\n\tconst auto userpicX = (widgetSize.width() - photoSize) / 2;\n\tconst auto userpicY = bodyTop + photoTop;\n\tconst auto padding = photoSize;\n\tconst auto patternRect = QRect(\n\t\tuserpicX - padding,\n\t\tuserpicY - padding / 2,\n\t\tphotoSize + padding * 2,\n\t\tphotoSize + padding);\n\n\tif (!_emoji\n\t\t|| _emoji->entityData()\n\t\t\t!= Data::SerializeCustomEmojiId(emojiId)) {\n\t\tconst auto document = _peer->owner().document(emojiId);\n\t\t_emoji = document->owner().customEmojiManager().create(\n\t\t\tdocument,\n\t\t\t[=] {\n\t\t\t\t_cachedImage = QImage();\n\t\t\t\tif (_updateCallback) {\n\t\t\t\t\t_updateCallback();\n\t\t\t\t}\n\t\t\t},\n\t\t\tData::CustomEmojiSizeTag::Large);\n\t\t_cachedImage = QImage();\n\t\t_cachedEmojiId = 0;\n\t}\n\n\tif (_emoji && _emoji->ready()) {\n\t\tif (_cachedImage.isNull()\n\t\t\t|| _cachedRect != patternRect\n\t\t\t|| _cachedEmojiId != emojiId) {\n\t\t\trenderPattern(patternRect, emojiId);\n\t\t}\n\t\tp.drawImage(patternRect, _cachedImage);\n\t}\n}\n\nvoid PanelBackground::updateBrush(\n\t\tQSize widgetSize,\n\t\tQPoint center,\n\t\tfloat64 radius,\n\t\tconst std::vector<QColor> &colors) {\n\t_brushSize = widgetSize;\n\tauto gradient = QRadialGradient(center, radius);\n\tconst auto step = 1.0 / (colors.size() - 1);\n\tfor (auto i = 0; i < colors.size(); ++i) {\n\t\tgradient.setColorAt(i * step, colors[colors.size() - 1 - i]);\n\t}\n\t_brush = QBrush(gradient);\n}\n\nvoid PanelBackground::renderPattern(const QRect &rect, DocumentId emojiId) {\n\tconst auto ratio = style::DevicePixelRatio();\n\t_cachedImage = QImage(\n\t\trect.size() * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_cachedImage.setDevicePixelRatio(ratio);\n\t_cachedImage.fill(Qt::transparent);\n\n\tauto painter = QPainter(&_cachedImage);\n\tconst auto patternColor = QColor(0, 0, 0, int(0.6 * 255));\n\tconst auto &points = Ui::PatternBgPoints();\n\tconst auto localRect = QRect(QPoint(), rect.size());\n\tUi::PaintBgPoints(\n\t\tpainter,\n\t\tpoints,\n\t\t_cache,\n\t\t_emoji.get(),\n\t\tpatternColor,\n\t\tlocalRect,\n\t\t1.);\n\n\t_cachedRect = rect;\n\t_cachedEmojiId = emojiId;\n}\n\nvoid PanelBackground::updateColors() {\n\tconst auto collectible = _peer->emojiStatusId().collectible;\n\tif (collectible && collectible->centerColor.isValid()) {\n\t\t_colors = Data::ColorProfileSet{\n\t\t\t.bg = { collectible->edgeColor, collectible->centerColor },\n\t\t};\n\t} else {\n\t\t_colors = _peer->session().api().peerColors().colorProfileFor(_peer);\n\t}\n}\n\nvoid PanelBackground::updateEmojiId() {\n\tconst auto collectible = _peer->emojiStatusId().collectible;\n\t_currentEmojiId = (collectible && collectible->patternDocumentId)\n\t\t? collectible->patternDocumentId\n\t\t: _peer->profileBackgroundEmojiId();\n}\n\nstd::optional<QColor> PanelBackground::edgeColor() const {\n\tconst auto collectible = _peer->emojiStatusId().collectible;\n\tif (collectible && collectible->edgeColor.isValid()) {\n\t\treturn collectible->edgeColor;\n\t}\n\tif (_colors && !_colors->bg.empty()) {\n\t\treturn _colors->bg.front();\n\t}\n\treturn std::nullopt;\n}\n\nstd::optional<QColor> PanelBackground::textColorOverride(\n\t\tconst style::color &defaultColor) const {\n\tconst auto collectible = _peer->emojiStatusId().collectible;\n\tif (collectible && collectible->textColor.isValid()) {\n\t\treturn collectible->textColor;\n\t}\n\tconst auto edge = edgeColor();\n\tif (!edge) {\n\t\treturn std::nullopt;\n\t}\n\tconstexpr auto kMinContrast = 5.5;\n\tif (kMinContrast > Ui::CountContrast(defaultColor->c, *edge)) {\n\t\treturn st::groupCallMembersFg->c;\n\t}\n\treturn std::nullopt;\n}\n\nrpl::lifetime &PanelBackground::lifetime() {\n\treturn _lifetime;\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_panel_background.h",
    "content": "// This file is part of Desktop App Toolkit,\n// a set of libraries for developing nice desktop applications.\n//\n// For license and copyright information please follow this link:\n// https://github.com/desktop-app/legal/blob/master/LEGAL\n//\n#pragma once\n\n#include \"data/data_peer_colors.h\"\n\nclass DocumentData;\nclass PeerData;\n\nnamespace Data {\nclass CustomEmoji;\n} // namespace Data\n\nnamespace Ui {\nnamespace Text {\nclass CustomEmoji;\n} // namespace Text\n} // namespace Ui\n\nnamespace Calls {\n\nclass PanelBackground final {\npublic:\n\texplicit PanelBackground(\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> updateCallback);\n\n\tvoid paint(\n\t\tQPainter &p,\n\t\tQSize widgetSize,\n\t\tint bodyTop,\n\t\tint photoTop,\n\t\tint photoSize,\n\t\tconst QRegion &region);\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\n\t[[nodiscard]] std::optional<QColor> textColorOverride(\n\t\tconst style::color &defaultColor) const;\n\nprivate:\n\tvoid updateBrush(\n\t\tQSize widgetSize,\n\t\tQPoint center,\n\t\tfloat64 radius,\n\t\tconst std::vector<QColor> &colors);\n\tvoid renderPattern(const QRect &rect, DocumentId emojiId);\n\tvoid updateColors();\n\tvoid updateEmojiId();\n\t[[nodiscard]] std::optional<QColor> edgeColor() const;\n\n\tconst not_null<PeerData*> _peer;\n\tconst Fn<void()> _updateCallback;\n\n\tQBrush _brush;\n\tQSize _brushSize;\n\n\tstd::optional<Data::ColorProfileSet> _colors;\n\n\tstd::unique_ptr<Ui::Text::CustomEmoji> _emoji;\n\tbase::flat_map<float64, QImage> _cache;\n\tQImage _cachedImage;\n\tQRect _cachedRect;\n\tDocumentId _cachedEmojiId = 0;\n\tDocumentId _currentEmojiId = 0;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_signal_bars.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/calls_signal_bars.h\"\n\n#include \"calls/calls_call.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_calls.h\"\n\nnamespace Calls {\n\nSignalBars::SignalBars(\n\tQWidget *parent,\n\tnot_null<Call*> call,\n\tconst style::CallSignalBars &st)\n: RpWidget(parent)\n, _st(st)\n, _count(Call::kSignalBarStarting) {\n\tresize(\n\t\t_st.width + (_st.width + _st.skip) * (Call::kSignalBarCount - 1),\n\t\t_st.max);\n\tcall->signalBarCountValue(\n\t) | rpl::on_next([=](int count) {\n\t\tchanged(count);\n\t}, lifetime());\n}\n\nvoid SignalBars::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(_st.color);\n\tfor (auto i = 0; i < Call::kSignalBarCount; ++i) {\n\t\tp.setOpacity((i < _count) ? 1. : _st.inactiveOpacity);\n\t\tconst auto barHeight = _st.min\n\t\t\t+ (_st.max - _st.min) * (i / float64(Call::kSignalBarCount - 1));\n\t\tconst auto barLeft = i * (_st.width + _st.skip);\n\t\tconst auto barTop = height() - barHeight;\n\t\tp.drawRoundedRect(\n\t\t\tQRectF(\n\t\t\t\tbarLeft,\n\t\t\t\tbarTop,\n\t\t\t\t_st.width,\n\t\t\t\tbarHeight),\n\t\t\t_st.radius,\n\t\t\t_st.radius);\n\t}\n\tp.setOpacity(1.);\n}\n\nvoid SignalBars::changed(int count) {\n\tif (_count == Call::kSignalBarFinished) {\n\t\treturn;\n\t} else if (_count != count) {\n\t\t_count = count;\n\t\tupdate();\n\t}\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_signal_bars.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace style {\nstruct CallSignalBars;\n} // namespace style\n\nnamespace Calls {\n\nclass Call;\n\nclass SignalBars final : public Ui::RpWidget {\npublic:\n\tSignalBars(\n\t\tQWidget *parent,\n\t\tnot_null<Call*> call,\n\t\tconst style::CallSignalBars &st);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid changed(int count);\n\n\tconst style::CallSignalBars &_st;\n\tint _count = 0;\n\n};\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_top_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/calls_top_bar.h\"\n\n#include \"ui/effects/cross_line.h\"\n#include \"ui/paint/blobs_linear.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/chat/group_call_userpics.h\" // Ui::GroupCallUser.\n#include \"ui/chat/group_call_bar.h\" // Ui::GroupCallBarContent.\n#include \"ui/layers/generic_box.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/power_saving.h\"\n#include \"lang/lang_keys.h\"\n#include \"core/application.h\"\n#include \"calls/calls_call.h\"\n#include \"calls/calls_instance.h\"\n#include \"calls/calls_signal_bars.h\"\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_menu.h\" // Group::LeaveBox.\n#include \"history/view/history_view_group_call_bar.h\" // ContentByCall.\n#include \"data/data_user.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"boxes/abstract_box.h\"\n#include \"base/timer.h\"\n#include \"styles/style_basic.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_chat_helpers.h\" // style::GroupCallUserpics\n#include \"styles/style_layers.h\"\n\nnamespace Calls {\n\nenum class BarState {\n\tConnecting,\n\tActive,\n\tMuted,\n\tForceMuted,\n};\n\nnamespace {\n\nconstexpr auto kUpdateDebugTimeoutMs = crl::time(500);\n\nconstexpr auto kMinorBlobAlpha = 76. / 255.;\n\nconstexpr auto kHideBlobsDuration = crl::time(500);\nconstexpr auto kBlobLevelDuration = crl::time(250);\nconstexpr auto kBlobUpdateInterval = crl::time(100);\n\nauto BarStateFromMuteState(\n\t\tMuteState state,\n\t\tGroupCall::InstanceState instanceState,\n\t\tTimeId scheduledDate) {\n\treturn scheduledDate\n\t\t? BarState::ForceMuted\n\t\t: (instanceState == GroupCall::InstanceState::Disconnected)\n\t\t? BarState::Connecting\n\t\t: (state == MuteState::ForceMuted || state == MuteState::RaisedHand)\n\t\t? BarState::ForceMuted\n\t\t: (state == MuteState::Muted)\n\t\t? BarState::Muted\n\t\t: BarState::Active;\n};\n\nauto LinearBlobs() {\n\treturn std::vector<Ui::Paint::LinearBlobs::BlobData>{\n\t\t{\n\t\t\t.segmentsCount = 5,\n\t\t\t.minRadius = 0.,\n\t\t\t.maxRadius = (float)st::groupCallMajorBlobMaxRadius,\n\t\t\t.idleRadius = (float)st::groupCallMinorBlobIdleRadius,\n\t\t\t.speedScale = .3,\n\t\t\t.alpha = 1.,\n\t\t},\n\t\t{\n\t\t\t.segmentsCount = 7,\n\t\t\t.minRadius = 0.,\n\t\t\t.maxRadius = (float)st::groupCallMinorBlobMaxRadius,\n\t\t\t.idleRadius = (float)st::groupCallMinorBlobIdleRadius,\n\t\t\t.speedScale = .7,\n\t\t\t.alpha = kMinorBlobAlpha,\n\t\t},\n\t\t{\n\t\t\t.segmentsCount = 8,\n\t\t\t.minRadius = 0.,\n\t\t\t.maxRadius = (float)st::groupCallMinorBlobMaxRadius,\n\t\t\t.idleRadius = (float)st::groupCallMinorBlobIdleRadius,\n\t\t\t.speedScale = .7,\n\t\t\t.alpha = kMinorBlobAlpha,\n\t\t},\n\t};\n}\n\nauto Colors() {\n\tusing Vector = std::vector<QColor>;\n\tusing Colors = anim::gradient_colors;\n\treturn base::flat_map<BarState, Colors>{\n\t\t{\n\t\t\tBarState::ForceMuted,\n\t\t\tColors(QGradientStops{\n\t\t\t\t{ 0.0, st::groupCallForceMutedBar1->c },\n\t\t\t\t{ .35, st::groupCallForceMutedBar2->c },\n\t\t\t\t{ 1.0, st::groupCallForceMutedBar3->c } })\n\t\t},\n\t\t{\n\t\t\tBarState::Active,\n\t\t\tColors(Vector{ st::groupCallLive1->c, st::groupCallLive2->c })\n\t\t},\n\t\t{\n\t\t\tBarState::Muted,\n\t\t\tColors(Vector{ st::groupCallMuted1->c, st::groupCallMuted2->c })\n\t\t},\n\t\t{\n\t\t\tBarState::Connecting,\n\t\t\tColors(st::callBarBgMuted->c)\n\t\t},\n\t};\n}\n\nclass DebugInfoBox : public Ui::BoxContent {\npublic:\n\tDebugInfoBox(QWidget*, base::weak_ptr<Call> call);\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\tvoid updateText();\n\n\tbase::weak_ptr<Call> _call;\n\tQPointer<Ui::FlatLabel> _text;\n\tbase::Timer _updateTextTimer;\n\n};\n\nDebugInfoBox::DebugInfoBox(QWidget*, base::weak_ptr<Call> call)\n: _call(call) {\n}\n\nvoid DebugInfoBox::prepare() {\n\tsetTitle(u\"Call Debug\"_q);\n\n\taddButton(tr::lng_close(), [this] { closeBox(); });\n\t_text = setInnerWidget(\n\t\tobject_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::FlatLabel>(this, st::callDebugLabel),\n\t\t\tst::callDebugPadding))->entity();\n\t_text->setSelectable(true);\n\tupdateText();\n\t_updateTextTimer.setCallback([this] { updateText(); });\n\t_updateTextTimer.callEach(kUpdateDebugTimeoutMs);\n\tsetDimensions(st::boxWideWidth, st::boxMaxListHeight);\n}\n\nvoid DebugInfoBox::updateText() {\n\tif (auto call = _call.get()) {\n\t\t_text->setText(call->getDebugLog());\n\t}\n}\n\n} // namespace\n\nstruct TopBar::User {\n\tUi::GroupCallUser data;\n};\n\nclass Mute final : public Ui::IconButton {\npublic:\n\tMute(QWidget *parent, const style::IconButton &st)\n\t: Ui::IconButton(parent, st)\n\t, _st(st)\n\t, _crossLineMuteAnimation(st::callTopBarMuteCrossLine) {\n\t\tresize(_st.width, _st.height);\n\t\tinstallEventFilter(this);\n\n\t\tstyle::PaletteChanged(\n\t\t) | rpl::on_next([=] {\n\t\t\t_crossLineMuteAnimation.invalidate();\n\t\t}, lifetime());\n\t}\n\n\tvoid setProgress(float64 progress) {\n\t\tif (_progress == progress) {\n\t\t\treturn;\n\t\t}\n\t\t_progress = progress;\n\t\tupdate();\n\t}\n\n\tvoid setRippleColorOverride(const style::color *colorOverride) {\n\t\t_rippleColorOverride = colorOverride;\n\t}\n\nprotected:\n\tbool eventFilter(QObject *object, QEvent *event) {\n\t\tif (event->type() == QEvent::Paint) {\n\t\t\tauto p = QPainter(this);\n\t\t\tpaintRipple(\n\t\t\t\tp,\n\t\t\t\t_st.rippleAreaPosition.x(),\n\t\t\t\t_st.rippleAreaPosition.y(),\n\t\t\t\t_rippleColorOverride ? &(*_rippleColorOverride)->c : nullptr);\n\t\t\t_crossLineMuteAnimation.paint(p, _st.iconPosition, _progress);\n\t\t\treturn true;\n\t\t}\n\t\treturn QObject::eventFilter(object, event);\n\t}\n\nprivate:\n\tfloat64 _progress = 0.;\n\n\tconst style::IconButton &_st;\n\tUi::CrossLineAnimation _crossLineMuteAnimation;\n\tconst style::color *_rippleColorOverride = nullptr;\n\n};\n\nTopBar::TopBar(\n\tQWidget *parent,\n\tCall *call,\n\tstd::shared_ptr<Ui::Show> show)\n: TopBar(parent, show, call, nullptr) {\n}\n\nTopBar::TopBar(\n\tQWidget *parent,\n\tGroupCall *call,\n\tstd::shared_ptr<Ui::Show> show)\n: TopBar(parent, show, nullptr, call) {\n}\n\nTopBar::TopBar(\n\tQWidget *parent,\n\tstd::shared_ptr<Ui::Show> show,\n\tCall *call,\n\tGroupCall *groupCall)\n: RpWidget(parent)\n, _call(call)\n, _groupCall(groupCall)\n, _show(show)\n, _userpics(call\n\t? nullptr\n\t: std::make_unique<Ui::GroupCallUserpics>(\n\t\tst::groupCallTopBarUserpics,\n\t\trpl::single(true),\n\t\t[=] { updateUserpics(); }))\n, _durationLabel(_call\n\t? object_ptr<Ui::LabelSimple>(this, st::callBarLabel)\n\t: object_ptr<Ui::LabelSimple>(nullptr))\n, _signalBars(_call\n\t? object_ptr<SignalBars>(this, _call.get(), st::callBarSignalBars)\n\t: object_ptr<SignalBars>(nullptr))\n, _fullInfoLabel(this, st::callBarInfoLabel)\n, _shortInfoLabel(this, st::callBarInfoLabel)\n, _hangupLabel(_call\n\t? object_ptr<Ui::LabelSimple>(\n\t\tthis,\n\t\tst::callBarLabel,\n\t\ttr::lng_call_bar_hangup(tr::now))\n\t: object_ptr<Ui::LabelSimple>(nullptr))\n, _mute(this, st::callBarMuteToggle)\n, _info(this)\n, _hangup(this, st::callBarHangup)\n, _gradients(Colors(), QPointF(), QPointF())\n, _updateDurationTimer([=] { updateDurationText(); }) {\n\tinitControls();\n\tresize(width(), st::callBarHeight);\n\tsetupInitialBrush();\n}\n\nvoid TopBar::setupInitialBrush() {\n\tExpects(_switchStateCallback != nullptr);\n\n\t_switchStateAnimation.stop();\n\t_switchStateCallback(1.);\n}\n\nvoid TopBar::initControls() {\n\t_mute->setClickedCallback([=] {\n\t\tif (const auto call = _call.get()) {\n\t\t\tcall->setMuted(!call->muted());\n\t\t} else if (const auto group = _groupCall.get()) {\n\t\t\tif (group->mutedByAdmin()) {\n\t\t\t\t_show->showToast(\n\t\t\t\t\ttr::lng_group_call_force_muted_sub(tr::now));\n\t\t\t} else {\n\t\t\t\tgroup->setMuted((group->muted() == MuteState::Muted)\n\t\t\t\t\t? MuteState::Active\n\t\t\t\t\t: MuteState::Muted);\n\t\t\t}\n\t\t}\n\t});\n\n\tconst auto mapToState = [](bool muted) {\n\t\treturn muted ? MuteState::Muted : MuteState::Active;\n\t};\n\tconst auto fromState = _mute->lifetime().make_state<BarState>(\n\t\tBarStateFromMuteState(\n\t\t\t_call\n\t\t\t\t? mapToState(_call->muted())\n\t\t\t\t: _groupCall->muted(),\n\t\t\tGroupCall::InstanceState::Connected,\n\t\t\t_call ? TimeId(0) : _groupCall->scheduleDate()));\n\tusing namespace rpl::mappers;\n\tauto muted = _call\n\t\t? rpl::combine(\n\t\t\t_call->mutedValue() | rpl::map(mapToState),\n\t\t\trpl::single(GroupCall::InstanceState::Connected),\n\t\t\trpl::single(TimeId(0))\n\t\t) | rpl::type_erased\n\t\t: rpl::combine(\n\t\t\t(_groupCall->mutedValue()\n\t\t\t\t| MapPushToTalkToActive()\n\t\t\t\t| rpl::distinct_until_changed()\n\t\t\t\t| rpl::type_erased),\n\t\t\trpl::single(\n\t\t\t\t_groupCall->instanceState()\n\t\t\t) | rpl::then(_groupCall->instanceStateValue() | rpl::filter(\n\t\t\t\t_1 != GroupCall::InstanceState::TransitionToRtc)),\n\t\t\trpl::single(\n\t\t\t\t_groupCall->scheduleDate()\n\t\t\t) | rpl::then(_groupCall->real(\n\t\t\t) | rpl::map([](not_null<Data::GroupCall*> call) {\n\t\t\t\treturn call->scheduleDateValue();\n\t\t\t}) | rpl::flatten_latest()));\n\tstd::move(\n\t\tmuted\n\t) | rpl::map(\n\t\tBarStateFromMuteState\n\t) | rpl::on_next([=](BarState state) {\n\t\t_isGroupConnecting = (state == BarState::Connecting);\n\t\tsetMuted(state != BarState::Active);\n\t\tupdate();\n\n\t\tconst auto isForceMuted = (state == BarState::ForceMuted);\n\t\tif (isForceMuted) {\n\t\t\t_mute->clearState();\n\t\t}\n\t\t_mute->setPointerCursor(!isForceMuted);\n\n\t\tconst auto to = 1.;\n\t\tconst auto from = _switchStateAnimation.animating()\n\t\t\t? (to - _switchStateAnimation.value(0.))\n\t\t\t: 0.;\n\t\tconst auto fromMuted = *fromState;\n\t\tconst auto toMuted = state;\n\t\t*fromState = state;\n\n\t\tconst auto crossFrom = (fromMuted != BarState::Active) ? 1. : 0.;\n\t\tconst auto crossTo = (toMuted != BarState::Active) ? 1. : 0.;\n\n\t\t_switchStateCallback = [=](float64 value) {\n\t\t\tif (_groupCall) {\n\t\t\t\t_groupBrush = QBrush(\n\t\t\t\t\t_gradients.gradient(fromMuted, toMuted, value));\n\t\t\t\tupdate();\n\t\t\t}\n\n\t\t\tconst auto crossProgress = (crossFrom == crossTo)\n\t\t\t\t? crossTo\n\t\t\t\t: anim::interpolateToF(crossFrom, crossTo, value);\n\t\t\t_mute->setProgress(crossProgress);\n\t\t};\n\n\t\t_switchStateAnimation.stop();\n\t\tconst auto duration = (to - from) * st::universalDuration;\n\t\t_switchStateAnimation.start(\n\t\t\t_switchStateCallback,\n\t\t\tfrom,\n\t\t\tto,\n\t\t\tduration);\n\t}, _mute->lifetime());\n\n\tif (const auto group = _groupCall.get()) {\n\t\tsubscribeToMembersChanges(group);\n\n\t\t_isGroupConnecting.value(\n\t\t) | rpl::on_next([=](bool isConnecting) {\n\t\t\t_mute->setAttribute(\n\t\t\t\tQt::WA_TransparentForMouseEvents,\n\t\t\t\tisConnecting);\n\t\t\tupdateInfoLabels();\n\t\t}, lifetime());\n\t}\n\n\tif (const auto call = _call.get()) {\n\t\tcall->user()->session().changes().peerUpdates(\n\t\t\tData::PeerUpdate::Flag::Name\n\t\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\t\t// _user may change for the same Panel.\n\t\t\treturn (_call != nullptr) && (update.peer == _call->user());\n\t\t}) | rpl::on_next([=] {\n\t\t\tupdateInfoLabels();\n\t\t}, lifetime());\n\t}\n\n\tsetInfoLabels();\n\t_info->setClickedCallback([=] {\n\t\tif (const auto call = _call.get()) {\n\t\t\tif (Logs::DebugEnabled()\n\t\t\t\t&& (_info->clickModifiers() & Qt::ControlModifier)) {\n\t\t\t\t_show->showBox(\n\t\t\t\t\tBox<DebugInfoBox>(_call),\n\t\t\t\t\tUi::LayerOption::CloseOther);\n\t\t\t} else {\n\t\t\t\tCore::App().calls().showInfoPanel(call);\n\t\t\t}\n\t\t} else if (const auto group = _groupCall.get()) {\n\t\t\tCore::App().calls().showInfoPanel(group);\n\t\t}\n\t});\n\t_hangup->setClickedCallback([this] {\n\t\tif (const auto call = _call.get()) {\n\t\t\tcall->hangup();\n\t\t} else if (const auto group = _groupCall.get()) {\n\t\t\tif (!group->canManage()) {\n\t\t\t\tgroup->hangup();\n\t\t\t} else {\n\t\t\t\t_show->showBox(\n\t\t\t\t\tBox(\n\t\t\t\t\t\tGroup::LeaveBox,\n\t\t\t\t\t\tgroup,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\tGroup::BoxContext::MainWindow),\n\t\t\t\t\tUi::LayerOption::CloseOther);\n\t\t\t}\n\t\t}\n\t});\n\tupdateDurationText();\n}\n\nvoid TopBar::initBlobsUnder(\n\t\tQWidget *blobsParent,\n\t\trpl::producer<QRect> barGeometry) {\n\tconst auto group = _groupCall.get();\n\tif (!group) {\n\t\treturn;\n\t}\n\n\tstruct State {\n\t\tUi::Paint::LinearBlobs paint = {\n\t\t\tLinearBlobs(),\n\t\t\tkBlobLevelDuration,\n\t\t\t1.,\n\t\t\tUi::Paint::LinearBlob::Direction::TopDown\n\t\t};\n\t\tUi::Animations::Simple hideAnimation;\n\t\tUi::Animations::Basic animation;\n\t\tbase::Timer levelTimer;\n\t\tcrl::time hideLastTime = 0;\n\t\tcrl::time lastTime = 0;\n\t\tfloat lastLevel = 0.;\n\t\tfloat levelBeforeLast = 0.;\n\t};\n\n\t_blobs = base::make_unique_q<Ui::RpWidget>(blobsParent);\n\n\tconst auto state = _blobs->lifetime().make_state<State>();\n\tstate->levelTimer.setCallback([=] {\n\t\tstate->levelBeforeLast = state->lastLevel;\n\t\tstate->lastLevel = 0.;\n\t\tif (state->levelBeforeLast == 0.) {\n\t\t\tstate->paint.setLevel(0.);\n\t\t\tstate->levelTimer.cancel();\n\t\t}\n\t});\n\n\tstate->animation.init([=](crl::time now) {\n\t\tif (const auto last = state->hideLastTime; (last > 0)\n\t\t\t&& (now - last >= kHideBlobsDuration)) {\n\t\t\tstate->animation.stop();\n\t\t\treturn false;\n\t\t}\n\t\tstate->paint.updateLevel(now - state->lastTime);\n\t\tstate->lastTime = now;\n\n\t\t_blobs->update();\n\t\treturn true;\n\t});\n\n\tgroup->stateValue(\n\t) | rpl::on_next([=](Calls::GroupCall::State state) {\n\t\tif (state == Calls::GroupCall::State::HangingUp) {\n\t\t\t_blobs->hide();\n\t\t}\n\t}, lifetime());\n\n\tusing namespace rpl::mappers;\n\tauto hideBlobs = rpl::combine(\n\t\tPowerSaving::OnValue(PowerSaving::kCalls),\n\t\tCore::App().appDeactivatedValue(),\n\t\tgroup->instanceStateValue()\n\t) | rpl::map(_1 || _2 || _3 == GroupCall::InstanceState::Disconnected);\n\n\tstd::move(\n\t\thideBlobs\n\t) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](bool hide) {\n\t\tif (hide) {\n\t\t\tstate->paint.setLevel(0.);\n\t\t}\n\t\tstate->hideLastTime = hide ? crl::now() : 0;\n\t\tif (!hide && !state->animation.animating()) {\n\t\t\tstate->animation.start();\n\t\t}\n\t\tif (hide) {\n\t\t\tstate->levelTimer.cancel();\n\t\t} else {\n\t\t\tstate->lastLevel = 0.;\n\t\t}\n\n\t\tconst auto from = hide ? 0. : 1.;\n\t\tconst auto to = hide ? 1. : 0.;\n\t\tstate->hideAnimation.start([=](float64) {\n\t\t\t_blobs->update();\n\t\t}, from, to, kHideBlobsDuration);\n\t}, lifetime());\n\n\tstd::move(\n\t\tbarGeometry\n\t) | rpl::on_next([=](QRect rect) {\n\t\t_blobs->resize(\n\t\t\trect.width(),\n\t\t\t(int)state->paint.maxRadius());\n\t\t_blobs->moveToLeft(rect.x(), rect.y() + rect.height());\n\t}, lifetime());\n\n\tshownValue(\n\t) | rpl::on_next([=](bool shown) {\n\t\t_blobs->setVisible(shown);\n\t}, lifetime());\n\n\t_blobs->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tconst auto hidden = state->hideAnimation.value(\n\t\t\tstate->hideLastTime ? 1. : 0.);\n\t\tif (hidden == 1.) {\n\t\t\treturn;\n\t\t}\n\n\t\tauto p = QPainter(_blobs);\n\t\tif (hidden > 0.) {\n\t\t\tp.setOpacity(1. - hidden);\n\t\t}\n\t\tconst auto top = -_blobs->height() * hidden;\n\t\tconst auto width = _blobs->width();\n\t\tp.translate(0, top);\n\t\tstate->paint.paint(p, _groupBrush, width);\n\t}, _blobs->lifetime());\n\n\tgroup->levelUpdates(\n\t) | rpl::filter([=](const LevelUpdate &update) {\n\t\treturn !state->hideLastTime && (update.value > state->lastLevel);\n\t}) | rpl::on_next([=](const LevelUpdate &update) {\n\t\tif (state->lastLevel == 0.) {\n\t\t\tstate->levelTimer.callEach(kBlobUpdateInterval);\n\t\t}\n\t\tstate->lastLevel = update.value;\n\t\tstate->paint.setLevel(update.value);\n\t}, _blobs->lifetime());\n\n\t_blobs->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_blobs->show();\n\n\tif (!state->hideLastTime) {\n\t\tstate->animation.start();\n\t}\n}\n\nvoid TopBar::subscribeToMembersChanges(not_null<GroupCall*> call) {\n\tconst auto peer = call->peer();\n\tconst auto group = _groupCall.get();\n\tconst auto conference = group && group->conference();\n\tauto realValue = conference\n\t\t? (rpl::single(group->sharedCall().get()) | rpl::type_erased)\n\t\t: peer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tData::PeerUpdate::Flag::GroupCall\n\t\t) | rpl::map([=] {\n\t\t\treturn peer->groupCall();\n\t\t}) | rpl::filter([=](Data::GroupCall *real) {\n\t\t\tconst auto call = _groupCall.get();\n\t\t\treturn call && real && (real->id() == call->id());\n\t\t}) | rpl::take(1);\n\tstd::move(\n\t\trealValue\n\t) | rpl::before_next([=](not_null<Data::GroupCall*> real) {\n\t\treal->titleValue() | rpl::on_next([=] {\n\t\t\tupdateInfoLabels();\n\t\t}, lifetime());\n\t}) | rpl::map([=](not_null<Data::GroupCall*> real) {\n\t\treturn HistoryView::GroupCallBarContentByCall(\n\t\t\treal,\n\t\t\tst::groupCallTopBarUserpics.size);\n\t}) | rpl::flatten_latest(\n\t) | rpl::filter([=](const Ui::GroupCallBarContent &content) {\n\t\tif (_users.size() != content.users.size()\n\t\t\t|| (conference && _usersCount != content.count)) {\n\t\t\treturn true;\n\t\t}\n\t\tfor (auto i = 0, count = int(_users.size()); i != count; ++i) {\n\t\t\tif (_users[i].userpicKey != content.users[i].userpicKey\n\t\t\t\t|| _users[i].id != content.users[i].id) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}) | rpl::on_next([=](const Ui::GroupCallBarContent &content) {\n\t\t_users = content.users;\n\t\t_usersCount = content.count;\n\t\tfor (auto &user : _users) {\n\t\t\tuser.speaking = false;\n\t\t}\n\t\t_userpics->update(_users, !isHidden());\n\t\tif (conference) {\n\t\t\tupdateInfoLabels();\n\t\t}\n\t}, lifetime());\n\n\t_userpics->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_userpicsWidth = width;\n\t\tupdateControlsGeometry();\n\t}, lifetime());\n\n\tcall->peer()->session().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::Name\n\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\t// _peer may change for the same Panel.\n\t\tconst auto call = _groupCall.get();\n\t\treturn (call != nullptr) && (update.peer == call->peer());\n\t}) | rpl::on_next([=] {\n\t\tupdateInfoLabels();\n\t}, lifetime());\n}\n\nvoid TopBar::updateUserpics() {\n\tupdate(_mute->width(), 0, _userpics->maxWidth(), height());\n}\n\nvoid TopBar::updateInfoLabels() {\n\tsetInfoLabels();\n\tupdateControlsGeometry();\n}\n\nvoid TopBar::setInfoLabels() {\n\tif (const auto call = _call.get()) {\n\t\tconst auto user = call->user();\n\t\tconst auto fullName = user->name();\n\t\tconst auto shortName = user->firstName;\n\t\t_fullInfoLabel->setText(fullName);\n\t\t_shortInfoLabel->setText(shortName);\n\t} else if (const auto group = _groupCall.get()) {\n\t\tconst auto peer = group->peer();\n\t\tconst auto real = peer->groupCall();\n\t\tconst auto connecting = _isGroupConnecting.current();\n\t\tif (!group->conference()) {\n\t\t\t_shortInfoLabel.destroy();\n\t\t}\n\t\tif (!group->conference() || connecting) {\n\t\t\tconst auto name = peer->name();\n\t\t\tconst auto title = (real && real->id() == group->id())\n\t\t\t\t? real->title()\n\t\t\t\t: QString();\n\t\t\tconst auto text = _isGroupConnecting.current()\n\t\t\t\t? tr::lng_group_call_connecting(tr::now)\n\t\t\t\t: !title.isEmpty()\n\t\t\t\t? title\n\t\t\t\t: name;\n\t\t\t_fullInfoLabel->setText(text);\n\t\t\tif (_shortInfoLabel) {\n\t\t\t\t_shortInfoLabel->setText(text);\n\t\t\t}\n\t\t} else if (!_usersCount\n\t\t\t|| _users.empty()\n\t\t\t|| (_users.size() == 1\n\t\t\t\t&& _users.front().id == peer->session().userPeerId().value\n\t\t\t\t&& _usersCount == 1)) {\n\t\t\t_fullInfoLabel->setText(tr::lng_confcall_join_title(tr::now));\n\t\t\t_shortInfoLabel->setText(tr::lng_confcall_join_title(tr::now));\n\t\t} else {\n\t\t\tconst auto textWithUserpics = [&](int userpics) {\n\t\t\t\tconst auto other = std::max(_usersCount - userpics, 0);\n\t\t\t\tauto names = QStringList();\n\t\t\t\tfor (const auto &entry : _users) {\n\t\t\t\t\tconst auto user = peer->owner().peer(PeerId(entry.id));\n\t\t\t\t\tnames.push_back(user->shortName());\n\t\t\t\t\tif (names.size() >= userpics) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (other > 0) {\n\t\t\t\t\treturn tr::lng_forwarding_from(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tother,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\tnames.join(u\", \"_q));\n\t\t\t\t} else if (userpics > 1) {\n\t\t\t\t\treturn tr::lng_forwarding_from_two(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\tnames.mid(0, userpics - 1).join(u\", \"_q),\n\t\t\t\t\t\tlt_second_user,\n\t\t\t\t\t\tnames.back());\n\t\t\t\t}\n\t\t\t\treturn names.back();\n\t\t\t};\n\t\t\t_fullInfoLabel->setText(textWithUserpics(int(_users.size())));\n\t\t\t_shortInfoLabel->setText(textWithUserpics(1));\n\t\t}\n\t}\n}\n\nvoid TopBar::setMuted(bool mute) {\n\t_mute->setRippleColorOverride(&st::shadowFg);\n\t_hangup->setRippleColorOverride(&st::shadowFg);\n\t_muted = mute;\n}\n\nvoid TopBar::updateDurationText() {\n\tif (!_call || !_durationLabel) {\n\t\treturn;\n\t}\n\tauto wasWidth = _durationLabel->width();\n\tauto durationMs = _call->getDurationMs();\n\tauto durationSeconds = durationMs / 1000;\n\tstartDurationUpdateTimer(durationMs);\n\t_durationLabel->setText(Ui::FormatDurationText(durationSeconds));\n\tif (_durationLabel->width() != wasWidth) {\n\t\tupdateControlsGeometry();\n\t}\n}\n\nvoid TopBar::startDurationUpdateTimer(crl::time currentDuration) {\n\tauto msTillNextSecond = 1000 - (currentDuration % 1000);\n\t_updateDurationTimer.callOnce(msTillNextSecond + 5);\n}\n\nvoid TopBar::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid TopBar::updateControlsGeometry() {\n\tauto left = 0;\n\t_mute->moveToLeft(left, 0);\n\tleft += _mute->width();\n\tif (_durationLabel) {\n\t\t_durationLabel->moveToLeft(left, st::callBarLabelTop);\n\t\tleft += _durationLabel->width() + st::callBarSkip;\n\t}\n\tif (_userpicsWidth) {\n\t\tconst auto single = st::groupCallTopBarUserpics.size;\n\t\tconst auto skip = anim::interpolate(\n\t\t\t0,\n\t\t\tst::callBarSkip,\n\t\t\tstd::min(_userpicsWidth, single) / float64(single));\n\t\tleft += _userpicsWidth + skip;\n\t}\n\tif (_signalBars) {\n\t\t_signalBars->moveToLeft(left, (height() - _signalBars->height()) / 2);\n\t\tleft += _signalBars->width() + st::callBarSkip;\n\t}\n\n\tauto right = st::callBarRightSkip;\n\tif (_hangupLabel) {\n\t\t_hangupLabel->moveToRight(right, st::callBarLabelTop);\n\t\tright += _hangupLabel->width();\n\t} else {\n\t\t//right -= st::callBarRightSkip;\n\t}\n\tright += st::callBarHangup.width;\n\t_hangup->setGeometryToRight(0, 0, right, height());\n\t_info->setGeometryToLeft(\n\t\t_mute->width(),\n\t\t0,\n\t\twidth() - _mute->width() - _hangup->width(),\n\t\theight());\n\n\tauto fullWidth = _fullInfoLabel->textMaxWidth();\n\tauto showFull = !_shortInfoLabel\n\t\t|| (left + fullWidth + right <= width());\n\tauto setInfoLabelGeometry = [this, left, right](auto &&infoLabel) {\n\t\tauto minPadding = qMax(left, right);\n\t\tauto infoWidth = infoLabel->textMaxWidth();\n\t\tauto infoLeft = (width() - infoWidth) / 2;\n\t\tif (infoLeft < minPadding) {\n\t\t\tinfoLeft = left;\n\t\t\tinfoWidth = width() - left - right;\n\t\t}\n\t\tinfoLabel->setGeometryToLeft(infoLeft, st::callBarLabelTop, infoWidth, st::callBarInfoLabel.style.font->height);\n\t};\n\n\t_fullInfoLabel->setVisible(showFull);\n\tsetInfoLabelGeometry(_fullInfoLabel);\n\tif (_shortInfoLabel) {\n\t\t_shortInfoLabel->setVisible(!showFull);\n\t\tsetInfoLabelGeometry(_shortInfoLabel);\n\t}\n\n\t_gradients.set_points(\n\t\tQPointF(0, st::callBarHeight / 2),\n\t\tQPointF(width(), st::callBarHeight / 2));\n\tif (!_switchStateAnimation.animating()) {\n\t\t_switchStateCallback(1.);\n\t}\n}\n\nvoid TopBar::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tauto brush = _groupCall\n\t\t? _groupBrush\n\t\t: (_muted ? st::callBarBgMuted : st::callBarBg);\n\tp.fillRect(e->rect(), std::move(brush));\n\n\tif (_userpicsWidth) {\n\t\tconst auto size = st::groupCallTopBarUserpics.size;\n\t\tconst auto top = (height() - size) / 2;\n\t\t_userpics->paint(p, _mute->width(), top, size);\n\t}\n}\n\nTopBar::~TopBar() = default;\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_top_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"base/timer.h\"\n#include \"base/object_ptr.h\"\n#include \"base/unique_qptr.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/gradient.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\nclass IconButton;\nclass AbstractButton;\nclass LabelSimple;\nclass FlatLabel;\nstruct GroupCallUser;\nclass GroupCallUserpics;\nclass Show;\n} // namespace Ui\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Calls {\n\nclass Call;\nclass GroupCall;\nclass SignalBars;\nclass Mute;\nenum class MuteState;\nenum class BarState;\n\nclass TopBar : public Ui::RpWidget {\npublic:\n\tTopBar(\n\t\tQWidget *parent,\n\t\tCall *call,\n\t\tstd::shared_ptr<Ui::Show> show);\n\tTopBar(\n\t\tQWidget *parent,\n\t\tGroupCall *call,\n\t\tstd::shared_ptr<Ui::Show> show);\n\t~TopBar();\n\n\tvoid initBlobsUnder(\n\t\tQWidget *blobsParent,\n\t\trpl::producer<QRect> barGeometry);\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tstruct User;\n\n\tTopBar(\n\t\tQWidget *parent,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tCall *call,\n\t\tGroupCall *groupCall);\n\n\tvoid initControls();\n\tvoid setupInitialBrush();\n\tvoid updateInfoLabels();\n\tvoid setInfoLabels();\n\tvoid updateDurationText();\n\tvoid updateControlsGeometry();\n\tvoid startDurationUpdateTimer(crl::time currentDuration);\n\tvoid setMuted(bool mute);\n\n\tvoid subscribeToMembersChanges(not_null<GroupCall*> call);\n\tvoid updateUserpics();\n\n\tconst base::weak_ptr<Call> _call;\n\tconst base::weak_ptr<GroupCall> _groupCall;\n\tconst std::shared_ptr<Ui::Show> _show;\n\n\tbool _muted = false;\n\tstd::vector<Ui::GroupCallUser> _users;\n\tint _usersCount = 0;\n\tstd::unique_ptr<Ui::GroupCallUserpics> _userpics;\n\tint _userpicsWidth = 0;\n\tobject_ptr<Ui::LabelSimple> _durationLabel;\n\tobject_ptr<SignalBars> _signalBars;\n\tobject_ptr<Ui::FlatLabel> _fullInfoLabel;\n\tobject_ptr<Ui::FlatLabel> _shortInfoLabel;\n\tobject_ptr<Ui::LabelSimple> _hangupLabel;\n\tobject_ptr<Mute> _mute;\n\tobject_ptr<Ui::AbstractButton> _info;\n\tobject_ptr<Ui::IconButton> _hangup;\n\tbase::unique_qptr<Ui::RpWidget> _blobs;\n\n\trpl::variable<bool> _isGroupConnecting = false;\n\n\tstd::vector<not_null<PeerData*>> _conferenceFirstUsers;\n\n\tQBrush _groupBrush;\n\tanim::linear_gradients<BarState> _gradients;\n\tUi::Animations::Simple _switchStateAnimation;\n\tFn<void(float64)> _switchStateCallback;\n\n\tbase::Timer _updateDurationTimer;\n\n};\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_userpic.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/calls_userpic.h\"\n\n#include \"data/data_peer.h\"\n#include \"main/main_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_cloud_file.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/painter.h\"\n#include \"apiwrap.h\" // requestFullPeer.\n#include \"styles/style_calls.h\"\n\nnamespace Calls {\nnamespace {\n\n} // namespace\n\nUserpic::Userpic(\n\tnot_null<QWidget*> parent,\n\tnot_null<PeerData*> peer,\n\trpl::producer<bool> muted)\n: _content(parent)\n, _peer(peer) {\n\tsetGeometry(0, 0, 0);\n\tsetup(std::move(muted));\n}\n\nUserpic::~Userpic() = default;\n\nvoid Userpic::setVisible(bool visible) {\n\t_content.setVisible(visible);\n}\n\nvoid Userpic::setGeometry(int x, int y, int size) {\n\tif (this->size() != size) {\n\t\t_userPhoto = QPixmap();\n\t\t_userPhotoFull = false;\n\t}\n\t_content.setGeometry(x, y, size, size);\n\t_content.update();\n\tif (_userPhoto.isNull()) {\n\t\trefreshPhoto();\n\t}\n}\n\nvoid Userpic::setup(rpl::producer<bool> muted) {\n\t_content.show();\n\t_content.setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t_content.paintRequest(\n\t) | rpl::on_next([=] {\n\t\tpaint();\n\t}, lifetime());\n\n\tstd::move(\n\t\tmuted\n\t) | rpl::on_next([=](bool muted) {\n\t\tsetMuted(muted);\n\t}, lifetime());\n\n\t_peer->session().changes().peerFlagsValue(\n\t\t_peer,\n\t\tData::PeerUpdate::Flag::Photo\n\t) | rpl::on_next([=] {\n\t\tprocessPhoto();\n\t}, lifetime());\n\n\t_peer->session().downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\trefreshPhoto();\n\t}, lifetime());\n\n\t_mutedAnimation.stop();\n}\n\nvoid Userpic::setMuteLayout(QPoint position, int size, int stroke) {\n\t_mutePosition = position;\n\t_muteSize = size;\n\t_muteStroke = stroke;\n\t_content.update();\n}\n\nvoid Userpic::paint() {\n\tauto p = QPainter(&_content);\n\n\tp.drawPixmap(0, 0, _userPhoto);\n\tif (_muted && _muteSize > 0) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tauto pen = st::callBgOpaque->p;\n\t\tpen.setWidth(_muteStroke);\n\t\tp.setPen(pen);\n\t\tp.setBrush(st::callHangupBg);\n\t\tconst auto rect = QRect(\n\t\t\t_mutePosition.x() - _muteSize / 2,\n\t\t\t_mutePosition.y() - _muteSize / 2,\n\t\t\t_muteSize,\n\t\t\t_muteSize);\n\t\tp.drawEllipse(rect);\n\t\tst::callMutedPeerIcon.paintInCenter(p, rect);\n\t}\n}\n\nvoid Userpic::setMuted(bool muted) {\n\tif (_muted == muted) {\n\t\treturn;\n\t}\n\t_muted = muted;\n\t_content.update();\n\t//_mutedAnimation.start(\n\t//\t[=] { _content.update(); },\n\t//\t_muted ? 0. : 1.,\n\t//\t_muted ? 1. : 0.,\n\t//\tst::fadeWrapDuration);\n}\n\nint Userpic::size() const {\n\treturn _content.width();\n}\n\nvoid Userpic::processPhoto() {\n\t_userpic = _peer->createUserpicView();\n\t_peer->loadUserpic();\n\tconst auto photo = _peer->userpicPhotoId()\n\t\t? _peer->owner().photo(_peer->userpicPhotoId()).get()\n\t\t: nullptr;\n\tif (isGoodPhoto(photo)) {\n\t\t_photo = photo->createMediaView();\n\t\t_photo->wanted(Data::PhotoSize::Thumbnail, _peer->userpicPhotoOrigin());\n\t} else {\n\t\t_photo = nullptr;\n\t\tif (_peer->userpicPhotoUnknown() || (photo && photo->isNull())) {\n\t\t\t_peer->session().api().requestFullPeer(_peer);\n\t\t}\n\t}\n\trefreshPhoto();\n}\n\nvoid Userpic::refreshPhoto() {\n\tif (!size()) {\n\t\treturn;\n\t}\n\tconst auto isNewBigPhoto = [&] {\n\t\treturn _photo\n\t\t\t&& (_photo->image(Data::PhotoSize::Thumbnail) != nullptr)\n\t\t\t&& (_photo->owner()->id != _userPhotoId || !_userPhotoFull);\n\t}();\n\tif (isNewBigPhoto) {\n\t\t_userPhotoId = _photo->owner()->id;\n\t\t_userPhotoFull = true;\n\t\tcreateCache(_photo->image(Data::PhotoSize::Thumbnail));\n\t} else if (_userPhoto.isNull()) {\n\t\tif (const auto cloud = _peer->userpicCloudImage(_userpic)) {\n\t\t\tauto image = Image(base::duplicate(*cloud));\n\t\t\tcreateCache(&image);\n\t\t} else {\n\t\t\tcreateCache(nullptr);\n\t\t}\n\t}\n}\n\nvoid Userpic::createCache(Image *image) {\n\tconst auto size = this->size();\n\tconst auto real = size * style::DevicePixelRatio();\n\t//_useTransparency\n\t//\t? (Images::Option::RoundLarge\n\t//\t\t| Images::Option::RoundSkipBottomLeft\n\t//\t\t| Images::Option::RoundSkipBottomRight)\n\t//\t: Images::Option::None;\n\tif (image) {\n\t\tauto width = image->width();\n\t\tauto height = image->height();\n\t\tif (width > height) {\n\t\t\twidth = qMax((width * real) / height, 1);\n\t\t\theight = real;\n\t\t} else {\n\t\t\theight = qMax((height * real) / width, 1);\n\t\t\twidth = real;\n\t\t}\n\t\t_userPhoto = image->pixNoCache(\n\t\t\t{ width, height },\n\t\t\t{\n\t\t\t\t.options = Images::Option::RoundCircle,\n\t\t\t\t.outer = { size, size },\n\t\t\t});\n\t\t_userPhoto.setDevicePixelRatio(style::DevicePixelRatio());\n\t} else {\n\t\tauto filled = QImage(\n\t\t\tQSize(real, real),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tfilled.setDevicePixelRatio(style::DevicePixelRatio());\n\t\tfilled.fill(Qt::transparent);\n\t\t{\n\t\t\tauto p = QPainter(&filled);\n\t\t\tUi::EmptyUserpic(\n\t\t\t\tUi::EmptyUserpic::UserpicColor(_peer->colorIndex()),\n\t\t\t\t_peer->name()\n\t\t\t).paintCircle(p, 0, 0, size, size);\n\t\t}\n\t\t//_userPhoto = Images::PixmapFast(Images::Round(\n\t\t//\tstd::move(filled),\n\t\t//\tImageRoundRadius::Large,\n\t\t//\tRectPart::TopLeft | RectPart::TopRight));\n\t\t_userPhoto = Images::PixmapFast(std::move(filled));\n\t}\n\n\t_content.update();\n}\n\nbool Userpic::isGoodPhoto(PhotoData *photo) const {\n\tif (!photo || photo->isNull()) {\n\t\treturn false;\n\t}\n\tconst auto badAspect = [](int a, int b) {\n\t\treturn a > 10 * b;\n\t};\n\tconst auto width = photo->width();\n\tconst auto height = photo->height();\n\treturn !badAspect(width, height) && !badAspect(height, width);\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_userpic.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/userpic_view.h\"\n#include \"ui/effects/animations.h\"\n\nclass PeerData;\nclass Image;\n\nnamespace Data {\nclass PhotoMedia;\n} // namespace Data\n\nnamespace Calls {\n\nclass Userpic final {\npublic:\n\tUserpic(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<bool> muted);\n\t~Userpic();\n\n\tvoid setVisible(bool visible);\n\tvoid setGeometry(int x, int y, int size);\n\tvoid setMuteLayout(QPoint position, int size, int stroke);\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _content.lifetime();\n\t}\n\nprivate:\n\tvoid setup(rpl::producer<bool> muted);\n\n\tvoid paint();\n\tvoid setMuted(bool muted);\n\t[[nodiscard]] int size() const;\n\n\tvoid processPhoto();\n\tvoid refreshPhoto();\n\t[[nodiscard]] bool isGoodPhoto(PhotoData *photo) const;\n\tvoid createCache(Image *image);\n\n\tUi::RpWidget _content;\n\n\tnot_null<PeerData*> _peer;\n\tUi::PeerUserpicView _userpic;\n\tstd::shared_ptr<Data::PhotoMedia> _photo;\n\tUi::Animations::Simple _mutedAnimation;\n\tQPixmap _userPhoto;\n\tPhotoId _userPhotoId = 0;\n\tQPoint _mutePosition;\n\tint _muteSize = 0;\n\tint _muteStroke = 0;\n\tbool _userPhotoFull = false;\n\tbool _muted = false;\n\n};\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_video_bubble.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/calls_video_bubble.h\"\n\n#include \"webrtc/webrtc_video_track.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Calls {\n\nVideoBubble::VideoBubble(\n\tnot_null<QWidget*> parent,\n\tnot_null<Webrtc::VideoTrack*> track)\n: _content(parent)\n, _track(track)\n, _state(Webrtc::VideoState::Inactive) {\n\tsetup();\n}\n\nvoid VideoBubble::setup() {\n\t_content.show();\n\tapplyDragMode(_dragMode);\n\n\t_content.paintRequest(\n\t) | rpl::on_next([=] {\n\t\tpaint();\n\t}, lifetime());\n\n\t_track->stateValue(\n\t) | rpl::on_next([=](Webrtc::VideoState state) {\n\t\tsetState(state);\n\t}, lifetime());\n\n\t_track->renderNextFrame(\n\t) | rpl::on_next([=] {\n\t\tif (_track->frameSize().isEmpty()) {\n\t\t\t_track->markFrameShown();\n\t\t} else {\n\t\t\tupdateVisibility();\n\t\t\t// We update whole parent widget in this case.\n\t\t\t// In case we update only bubble without the parent incoming\n\t\t\t// video frame we may get full parent of old frame with a\n\t\t\t// rectangular piece of a new frame rendered with that update().\n\t\t\t//_content.update();\n\t\t}\n\t}, lifetime());\n}\n\nvoid VideoBubble::updateGeometry(\n\t\tDragMode mode,\n\t\tQRect boundingRect,\n\t\tQSize sizeMin,\n\t\tQSize sizeMax) {\n\tExpects(!boundingRect.isEmpty());\n\tExpects(sizeMax.isEmpty() || !sizeMin.isEmpty());\n\tExpects(sizeMax.isEmpty() || sizeMin.width() <= sizeMax.width());\n\tExpects(sizeMax.isEmpty() || sizeMin.height() <= sizeMax.height());\n\n\tif (sizeMin.isEmpty()) {\n\t\tsizeMin = boundingRect.size();\n\t}\n\tif (sizeMax.isEmpty()) {\n\t\tsizeMax = sizeMin;\n\t}\n\tif (_dragMode != mode) {\n\t\tapplyDragMode(mode);\n\t}\n\tif (_boundingRect != boundingRect) {\n\t\tapplyBoundingRect(boundingRect);\n\t}\n\tif (_min != sizeMin || _max != sizeMax) {\n\t\tapplySizeConstraints(sizeMin, sizeMax);\n\t}\n\tif (_geometryDirty && !_lastFrameSize.isEmpty()) {\n\t\tupdateSizeToFrame(base::take(_lastFrameSize));\n\t}\n}\n\nQRect VideoBubble::geometry() const {\n\treturn _content.isHidden() ? QRect() : _content.geometry();\n}\n\nvoid VideoBubble::applyBoundingRect(QRect rect) {\n\t_boundingRect = rect;\n\t_geometryDirty = true;\n}\n\nvoid VideoBubble::applyDragMode(DragMode mode) {\n\t_dragMode = mode;\n\tif (_dragMode == DragMode::None) {\n\t\t_dragging = false;\n\t\t_content.setCursor(style::cur_default);\n\t}\n\t_content.setAttribute(\n\t\tQt::WA_TransparentForMouseEvents,\n\t\ttrue/*(_dragMode == DragMode::None)*/);\n\tif (_dragMode == DragMode::SnapToCorners) {\n\t\t_corner = RectPart::BottomRight;\n\t} else {\n\t\t_corner = RectPart::None;\n\t\t_lastDraggableSize = _size;\n\t}\n\t_size = QSize();\n\t_geometryDirty = true;\n}\n\nvoid VideoBubble::applySizeConstraints(QSize min, QSize max) {\n\t_min = min;\n\t_max = max;\n\t_geometryDirty = true;\n}\n\nvoid VideoBubble::paint() {\n\tauto p = QPainter(&_content);\n\n\tprepareFrame();\n\tif (!_frame.isNull()) {\n\t\tconst auto padding = st::boxRoundShadow.extend;\n\t\tconst auto inner = _content.rect().marginsRemoved(padding);\n\t\tUi::Shadow::paint(p, inner, _content.width(), st::boxRoundShadow);\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tconst auto left = _mirrored\n\t\t\t? (_frame.width() - (inner.width() * factor))\n\t\t\t: 0;\n\t\tp.drawImage(\n\t\t\tinner,\n\t\t\t_frame,\n\t\t\tQRect(QPoint(left, 0), inner.size() * factor));\n\t}\n\t_track->markFrameShown();\n}\n\nvoid VideoBubble::prepareFrame() {\n\tconst auto original = _track->frameSize();\n\tif (original.isEmpty()) {\n\t\t_frame = QImage();\n\t\treturn;\n\t}\n\tconst auto padding = st::boxRoundShadow.extend;\n\tconst auto size = (_content.rect() - padding).size()\n\t\t* style::DevicePixelRatio();\n\n\t// Should we check 'original' and 'size' aspect ratios?..\n\tconst auto request = Webrtc::FrameRequest{\n\t\t.resize = size,\n\t\t.outer = size,\n\t};\n\tconst auto frame = _track->frame(request);\n\tif (_frame.width() < size.width() || _frame.height() < size.height()) {\n\t\t_frame = QImage(size, QImage::Format_ARGB32_Premultiplied);\n\t\t_frame.fill(Qt::transparent);\n\t}\n\tAssert(_frame.width() >= frame.width()\n\t\t&& _frame.height() >= frame.height());\n\tconst auto dstPerLine = _frame.bytesPerLine();\n\tconst auto srcPerLine = frame.bytesPerLine();\n\tconst auto lineSize = frame.width() * 4;\n\tauto dst = _frame.bits();\n\tauto src = frame.bits();\n\tconst auto till = src + frame.height() * srcPerLine;\n\tfor (; src != till; src += srcPerLine, dst += dstPerLine) {\n\t\tmemcpy(dst, src, lineSize);\n\t}\n\t_frame = Images::Round(\n\t\tstd::move(_frame),\n\t\tImageRoundRadius::Large,\n\t\tRectPart::AllCorners,\n\t\tQRect(QPoint(), size)\n\t).mirrored(_mirrored, false);\n}\n\nvoid VideoBubble::setState(Webrtc::VideoState state) {\n\tif (state == Webrtc::VideoState::Paused) {\n\t\tusing namespace Images;\n\t\tstatic constexpr auto kRadius = 24;\n\t\t_pausedFrame = Images::BlurLargeImage(_track->frame({}), kRadius);\n\t\tif (_pausedFrame.isNull()) {\n\t\t\tstate = Webrtc::VideoState::Inactive;\n\t\t}\n\t}\n\t_state = state;\n\tupdateVisibility();\n}\n\nvoid VideoBubble::updateSizeToFrame(QSize frame) {\n\tExpects(!frame.isEmpty());\n\n\tif (_lastFrameSize == frame) {\n\t\treturn;\n\t}\n\t_lastFrameSize = frame;\n\n\tauto size = !_size.isEmpty()\n\t\t? QSize(\n\t\t\tstd::clamp(_size.width(), _min.width(), _max.width()),\n\t\t\tstd::clamp(_size.height(), _min.height(), _max.height()))\n\t\t: (_dragMode == DragMode::None || _lastDraggableSize.isEmpty())\n\t\t? QSize()\n\t\t: _lastDraggableSize;\n\tif (size.isEmpty()) {\n\t\tsize = frame.scaled((_min + _max) / 2, Qt::KeepAspectRatio);\n\t} else {\n\t\tconst auto area = size.width() * size.height();\n\t\tconst auto w = int(base::SafeRound(std::max(\n\t\t\tstd::sqrt((frame.width() * float64(area)) / (frame.height() * 1.)),\n\t\t\t1.)));\n\t\tconst auto h = area / w;\n\t\tsize = QSize(w, h);\n\t\tif (w > _max.width() || h > _max.height()) {\n\t\t\tsize = size.scaled(_max, Qt::KeepAspectRatio);\n\t\t}\n\t}\n\tsize = QSize(std::max(1, size.width()), std::max(1, size.height()));\n\tsetInnerSize(size);\n}\n\nvoid VideoBubble::setInnerSize(QSize size) {\n\tif (_size == size && !_geometryDirty) {\n\t\treturn;\n\t}\n\t_geometryDirty = false;\n\t_size = size;\n\tconst auto topLeft = [&] {\n\t\tswitch (_corner) {\n\t\tcase RectPart::None:\n\t\t\treturn _boundingRect.topLeft() + QPoint(\n\t\t\t\t(_boundingRect.width() - size.width()) / 2,\n\t\t\t\t(_boundingRect.height() - size.height()) / 2);\n\t\tcase RectPart::TopLeft:\n\t\t\treturn _boundingRect.topLeft();\n\t\tcase RectPart::TopRight:\n\t\t\treturn QPoint(\n\t\t\t\t_boundingRect.x() + _boundingRect.width() - size.width(),\n\t\t\t\t_boundingRect.y());\n\t\tcase RectPart::BottomRight:\n\t\t\treturn QPoint(\n\t\t\t\t_boundingRect.x() + _boundingRect.width() - size.width(),\n\t\t\t\t_boundingRect.y() + _boundingRect.height() - size.height());\n\t\tcase RectPart::BottomLeft:\n\t\t\treturn QPoint(\n\t\t\t\t_boundingRect.x(),\n\t\t\t\t_boundingRect.y() + _boundingRect.height() - size.height());\n\t\t}\n\t\tUnexpected(\"Corner value in VideoBubble::setInnerSize.\");\n\t}();\n\tconst auto inner = QRect(topLeft, size);\n\t_content.setGeometry(inner.marginsAdded(st::boxRoundShadow.extend));\n}\n\nvoid VideoBubble::updateVisibility() {\n\tconst auto size = _track->frameSize();\n\tconst auto visible = (_state != Webrtc::VideoState::Inactive)\n\t\t&& !size.isEmpty();\n\tif (visible) {\n\t\tupdateSizeToFrame(size);\n\t}\n\t_content.setVisible(visible);\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_video_bubble.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rect_part.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Webrtc {\nclass VideoTrack;\nenum class VideoState;\n} // namespace Webrtc\n\nnamespace Calls {\n\nclass VideoBubble final {\npublic:\n\tVideoBubble(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Webrtc::VideoTrack*> track);\n\n\tenum class DragMode {\n\t\tNone,\n\t\tSnapToCorners,\n\t};\n\tvoid updateGeometry(\n\t\tDragMode mode,\n\t\tQRect boundingRect,\n\t\tQSize sizeMin = QSize(),\n\t\tQSize sizeMax = QSize());\n\t[[nodiscard]] QRect geometry() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _content.lifetime();\n\t}\n\n\tvoid setMirrored(bool mirrored) {\n\t\t_mirrored = mirrored;\n\t}\n\nprivate:\n\tvoid setup();\n\tvoid paint();\n\tvoid setState(Webrtc::VideoState state);\n\tvoid applyDragMode(DragMode mode);\n\tvoid applyBoundingRect(QRect rect);\n\tvoid applySizeConstraints(QSize min, QSize max);\n\tvoid updateSizeToFrame(QSize frame);\n\tvoid updateVisibility();\n\tvoid setInnerSize(QSize size);\n\tvoid prepareFrame();\n\n\tUi::RpWidget _content;\n\tconst not_null<Webrtc::VideoTrack*> _track;\n\tWebrtc::VideoState _state = Webrtc::VideoState();\n\tQImage _frame, _pausedFrame;\n\tQSize _min, _max, _size, _lastDraggableSize, _lastFrameSize;\n\tQRect _boundingRect;\n\tDragMode _dragMode = DragMode::None;\n\tRectPart _corner = RectPart::None;\n\tbool _dragging = false;\n\tbool _geometryDirty = false;\n\tbool _mirrored = true;\n\n};\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_video_incoming.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/calls_video_incoming.h\"\n\n#include \"ui/gl/gl_surface.h\"\n#include \"ui/gl/gl_shader.h\"\n#include \"ui/gl/gl_image.h\"\n#include \"ui/gl/gl_primitives.h\"\n#include \"ui/rhi/rhi_renderer.h\"\n#include \"ui/rhi/rhi_shader.h\"\n#include \"ui/painter.h\"\n#include \"media/view/media_view_pip.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"styles/style_calls.h\"\n\n#include <QOpenGLShader>\n#include <QOpenGLBuffer>\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n#include <rhi/qrhi.h>\n#endif\n\nnamespace Calls {\nnamespace {\n\nconstexpr auto kBottomShadowAlphaMax = 74;\n\nusing namespace Ui::GL;\n\n[[nodiscard]] ShaderPart FragmentBottomShadow() {\n\treturn {\n\t\t.header = R\"(\nuniform vec3 shadow; // fullHeight, shadowTop, maxOpacity\n)\",\n\t\t.body = R\"(\n\tfloat shadowCoord = shadow.y - gl_FragCoord.y;\n\tfloat shadowValue = clamp(shadowCoord / shadow.x, 0., 1.);\n\tfloat shadowShown = shadowValue * shadow.z;\n\tresult = vec4(min(result.rgb, vec3(1.)) * (1. - shadowShown), result.a);\n)\",\n\t};\n}\n\n} // namespace\n\nclass Panel::Incoming::RendererGL final : public Ui::GL::Renderer {\npublic:\n\texplicit RendererGL(not_null<Incoming*> owner);\n\n\tvoid init(QOpenGLFunctions &f) override;\n\n\tvoid deinit(QOpenGLFunctions *f) override;\n\n\tvoid paint(\n\t\tnot_null<QOpenGLWidget*> widget,\n\t\tQOpenGLFunctions &f) override;\n\nprivate:\n\tvoid uploadTexture(\n\t\tQOpenGLFunctions &f,\n\t\tGLint internalformat,\n\t\tGLint format,\n\t\tQSize size,\n\t\tQSize hasSize,\n\t\tint stride,\n\t\tconst void *data) const;\n\tvoid validateShadowImage();\n\n\tconst not_null<Incoming*> _owner;\n\n\tQSize _viewport;\n\tfloat _factor = 1.;\n\tint _ifactor = 1;\n\tQVector2D _uniformViewport;\n\n\tstd::optional<QOpenGLBuffer> _contentBuffer;\n\tstd::optional<QOpenGLShaderProgram> _argb32Program;\n\tQOpenGLShader *_texturedVertexShader = nullptr;\n\tstd::optional<QOpenGLShaderProgram> _yuv420Program;\n\tstd::optional<QOpenGLShaderProgram> _imageProgram;\n\tUi::GL::Textures<4> _textures;\n\tQSize _rgbaSize;\n\tQSize _lumaSize;\n\tQSize _chromaSize;\n\tint _trackFrameIndex = 0;\n\n\tUi::GL::Image _controlsShadowImage;\n\tQRect _controlsShadowLeft;\n\tQRect _controlsShadowRight;\n\n\trpl::lifetime _lifetime;\n\n};\n\nclass Panel::Incoming::RendererSW final : public Ui::GL::Renderer {\npublic:\n\texplicit RendererSW(not_null<Incoming*> owner);\n\n\tvoid paintFallback(\n\t\tPainter &p,\n\t\tconst QRegion &clip,\n\t\tUi::GL::Backend backend) override;\n\nprivate:\n\tvoid initBottomShadow();\n\tvoid fillTopShadow(QPainter &p);\n\tvoid fillBottomShadow(QPainter &p);\n\n\tconst not_null<Incoming*> _owner;\n\n\tQImage _bottomShadow;\n\n};\n\nPanel::Incoming::RendererGL::RendererGL(not_null<Incoming*> owner)\n: _owner(owner) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_controlsShadowImage.invalidate();\n\t}, _lifetime);\n}\n\nvoid Panel::Incoming::RendererGL::init(QOpenGLFunctions &f) {\n\tconstexpr auto kQuads = 2;\n\tconstexpr auto kQuadVertices = kQuads * 4;\n\tconstexpr auto kQuadValues = kQuadVertices * 4;\n\n\t_contentBuffer.emplace();\n\t_contentBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw);\n\t_contentBuffer->create();\n\t_contentBuffer->bind();\n\t_contentBuffer->allocate(kQuadValues * sizeof(GLfloat));\n\n\t_textures.ensureCreated(f);\n\n\t_imageProgram.emplace();\n\t_texturedVertexShader = LinkProgram(\n\t\t&*_imageProgram,\n\t\tVertexShader({\n\t\t\tVertexViewportTransform(),\n\t\t\tVertexPassTextureCoord(),\n\t\t}),\n\t\tFragmentShader({\n\t\t\tFragmentSampleARGB32Texture(),\n\t\t})).vertex;\n\n\t_argb32Program.emplace();\n\tLinkProgram(\n\t\t&*_argb32Program,\n\t\t_texturedVertexShader,\n\t\tFragmentShader({\n\t\t\tFragmentSampleARGB32Texture(),\n\t\t\tFragmentBottomShadow(),\n\t\t}));\n\n\t_yuv420Program.emplace();\n\tLinkProgram(\n\t\t&*_yuv420Program,\n\t\t_texturedVertexShader,\n\t\tFragmentShader({\n\t\t\tFragmentSampleYUV420Texture(),\n\t\t\tFragmentBottomShadow(),\n\t\t}));\n}\n\nvoid Panel::Incoming::RendererGL::deinit(QOpenGLFunctions *f) {\n\t_controlsShadowImage.destroy(f);\n\t_textures.destroy(f);\n\t_imageProgram = std::nullopt;\n\t_texturedVertexShader = nullptr;\n\t_argb32Program = std::nullopt;\n\t_yuv420Program = std::nullopt;\n\t_contentBuffer = std::nullopt;\n}\n\nvoid Panel::Incoming::RendererGL::paint(\n\t\tnot_null<QOpenGLWidget*> widget,\n\t\tQOpenGLFunctions &f) {\n\tconst auto markGuard = gsl::finally([&] {\n\t\t_owner->_track->markFrameShown();\n\t});\n\tconst auto data = _owner->_track->frameWithInfo(false);\n\tif (data.format == Webrtc::FrameFormat::None) {\n\t\treturn;\n\t}\n\n\tconst auto factor = widget->devicePixelRatioF();\n\tif (_factor != factor) {\n\t\t_factor = factor;\n\t\t_ifactor = int(std::ceil(_factor));\n\t\t_controlsShadowImage.invalidate();\n\t}\n\t_viewport = widget->size();\n\t_uniformViewport = QVector2D(\n\t\t_viewport.width() * _factor,\n\t\t_viewport.height() * _factor);\n\n\tconst auto rgbaFrame = (data.format == Webrtc::FrameFormat::ARGB32);\n\tconst auto upload = (_trackFrameIndex != data.index);\n\t_trackFrameIndex = data.index;\n\tauto &program = rgbaFrame ? _argb32Program : _yuv420Program;\n\tprogram->bind();\n\tif (rgbaFrame) {\n\t\tAssert(!data.original.isNull());\n\t\tf.glActiveTexture(GL_TEXTURE0);\n\t\t_textures.bind(f, 0);\n\t\tif (upload) {\n\t\t\tuploadTexture(\n\t\t\t\tf,\n\t\t\t\tUi::GL::kFormatRGBA,\n\t\t\t\tUi::GL::kFormatRGBA,\n\t\t\t\tdata.original.size(),\n\t\t\t\t_rgbaSize,\n\t\t\t\tdata.original.bytesPerLine() / 4,\n\t\t\t\tdata.original.constBits());\n\t\t\t_rgbaSize = data.original.size();\n\t\t}\n\t\tprogram->setUniformValue(\"s_texture\", GLint(0));\n\t} else {\n\t\tAssert(data.format == Webrtc::FrameFormat::YUV420);\n\t\tAssert(!data.yuv420->size.isEmpty());\n\t\tconst auto yuv = data.yuv420;\n\n\t\tf.glActiveTexture(GL_TEXTURE0);\n\t\t_textures.bind(f, 1);\n\t\tif (upload) {\n\t\t\tf.glPixelStorei(GL_UNPACK_ALIGNMENT, 1);\n\t\t\tuploadTexture(\n\t\t\t\tf,\n\t\t\t\tGL_ALPHA,\n\t\t\t\tGL_ALPHA,\n\t\t\t\tyuv->size,\n\t\t\t\t_lumaSize,\n\t\t\t\tyuv->y.stride,\n\t\t\t\tyuv->y.data);\n\t\t\t_lumaSize = yuv->size;\n\t\t}\n\t\tf.glActiveTexture(GL_TEXTURE1);\n\t\t_textures.bind(f, 2);\n\t\tif (upload) {\n\t\t\tuploadTexture(\n\t\t\t\tf,\n\t\t\t\tGL_ALPHA,\n\t\t\t\tGL_ALPHA,\n\t\t\t\tyuv->chromaSize,\n\t\t\t\t_chromaSize,\n\t\t\t\tyuv->u.stride,\n\t\t\t\tyuv->u.data);\n\t\t}\n\t\tf.glActiveTexture(GL_TEXTURE2);\n\t\t_textures.bind(f, 3);\n\t\tif (upload) {\n\t\t\tuploadTexture(\n\t\t\t\tf,\n\t\t\t\tGL_ALPHA,\n\t\t\t\tGL_ALPHA,\n\t\t\t\tyuv->chromaSize,\n\t\t\t\t_chromaSize,\n\t\t\t\tyuv->v.stride,\n\t\t\t\tyuv->v.data);\n\t\t\t_chromaSize = yuv->chromaSize;\n\t\t\tf.glPixelStorei(GL_UNPACK_ALIGNMENT, 4);\n\t\t}\n\t\tprogram->setUniformValue(\"y_texture\", GLint(0));\n\t\tprogram->setUniformValue(\"u_texture\", GLint(1));\n\t\tprogram->setUniformValue(\"v_texture\", GLint(2));\n\t}\n\tconst auto rect = TransformRect(\n\t\twidget->rect(),\n\t\t_viewport,\n\t\t_factor);\n\tstd::array<std::array<GLfloat, 2>, 4> texcoords = { {\n\t\t{ { 0.f, 1.f } },\n\t\t{ { 1.f, 1.f } },\n\t\t{ { 1.f, 0.f } },\n\t\t{ { 0.f, 0.f } },\n\t} };\n\tif (const auto shift = (data.rotation / 90); shift != 0) {\n\t\tstd::rotate(\n\t\t\tbegin(texcoords),\n\t\t\tbegin(texcoords) + shift,\n\t\t\tend(texcoords));\n\t}\n\n\tconst auto width = widget->parentWidget()->width();\n\tconst auto left = (_owner->_topControlsAlignment == style::al_left);\n\tvalidateShadowImage();\n\tconst auto position = left\n\t\t? QPoint()\n\t\t: QPoint(width - st::callTitleShadowRight.width(), 0);\n\tconst auto translated = position - widget->pos();\n\tconst auto shadowArea = QRect(translated, st::callTitleShadowLeft.size());\n\tconst auto shadow = _controlsShadowImage.texturedRect(\n\t\tshadowArea,\n\t\t(left ? _controlsShadowLeft : _controlsShadowRight),\n\t\twidget->rect());\n\tconst auto shadowRect = TransformRect(\n\t\tshadow.geometry,\n\t\t_viewport,\n\t\t_factor);\n\n\tconst GLfloat coords[] = {\n\t\trect.left(), rect.top(),\n\t\ttexcoords[0][0], texcoords[0][1],\n\n\t\trect.right(), rect.top(),\n\t\ttexcoords[1][0], texcoords[1][1],\n\n\t\trect.right(), rect.bottom(),\n\t\ttexcoords[2][0], texcoords[2][1],\n\n\t\trect.left(), rect.bottom(),\n\t\ttexcoords[3][0], texcoords[3][1],\n\n\t\tshadowRect.left(), shadowRect.top(),\n\t\tshadow.texture.left(), shadow.texture.bottom(),\n\n\t\tshadowRect.right(), shadowRect.top(),\n\t\tshadow.texture.right(), shadow.texture.bottom(),\n\n\t\tshadowRect.right(), shadowRect.bottom(),\n\t\tshadow.texture.right(), shadow.texture.top(),\n\n\t\tshadowRect.left(), shadowRect.bottom(),\n\t\tshadow.texture.left(), shadow.texture.top(),\n\t};\n\n\t_contentBuffer->bind();\n\t_contentBuffer->write(0, coords, sizeof(coords));\n\n\tconst auto bottomShadowArea = QRect(\n\t\t0,\n\t\twidget->parentWidget()->height() - st::callBottomShadowSize,\n\t\twidget->parentWidget()->width(),\n\t\tst::callBottomShadowSize);\n\tconst auto bottomShadowFill = bottomShadowArea.intersected(\n\t\twidget->geometry()).translated(-widget->pos());\n\tconst auto shadowHeight = bottomShadowFill.height();\n\tconst auto shadowAlpha = (shadowHeight * kBottomShadowAlphaMax)\n\t\t/ (st::callBottomShadowSize * 255.);\n\n\tprogram->setUniformValue(\"viewport\", _uniformViewport);\n\tprogram->setUniformValue(\"shadow\", QVector3D(\n\t\tshadowHeight * _factor,\n\t\tTransformRect(bottomShadowFill, _viewport, _factor).bottom(),\n\t\tshadowAlpha));\n\n\tFillTexturedRectangle(f, &*program);\n\n#ifndef Q_OS_MAC\n\tif (!shadowRect.empty()) {\n\t\tf.glEnable(GL_BLEND);\n\t\tf.glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\tf.glDisable(GL_BLEND);\n\t\t});\n\n\t\t_imageProgram->bind();\n\t\t_imageProgram->setUniformValue(\"viewport\", _uniformViewport);\n\t\t_imageProgram->setUniformValue(\"s_texture\", GLint(0));\n\n\t\tf.glActiveTexture(GL_TEXTURE0);\n\t\t_controlsShadowImage.bind(f);\n\n\t\tFillTexturedRectangle(f, &*_imageProgram, 4);\n\t}\n#endif // Q_OS_MAC\n}\n\nvoid Panel::Incoming::RendererGL::validateShadowImage() {\n\tif (_controlsShadowImage) {\n\t\treturn;\n\t}\n\tconst auto size = st::callTitleShadowLeft.size();\n\tconst auto full = QSize(size.width(), 2 * size.height()) * _ifactor;\n\tauto image = QImage(full, QImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(_ifactor);\n\timage.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&image);\n\t\tst::callTitleShadowLeft.paint(p, 0, 0, size.width());\n\t\t_controlsShadowLeft = QRect(0, 0, full.width(), full.height() / 2);\n\t\tst::callTitleShadowRight.paint(p, 0, size.height(), size.width());\n\t\t_controlsShadowRight = QRect(\n\t\t\t0,\n\t\t\tfull.height() / 2,\n\t\t\tfull.width(),\n\t\t\tfull.height() / 2);\n\t}\n\t_controlsShadowImage.setImage(std::move(image));\n}\n\nvoid Panel::Incoming::RendererGL::uploadTexture(\n\t\tQOpenGLFunctions &f,\n\t\tGLint internalformat,\n\t\tGLint format,\n\t\tQSize size,\n\t\tQSize hasSize,\n\t\tint stride,\n\t\tconst void *data) const {\n\tf.glPixelStorei(GL_UNPACK_ROW_LENGTH, stride);\n\tif (hasSize != size) {\n\t\tf.glTexImage2D(\n\t\t\tGL_TEXTURE_2D,\n\t\t\t0,\n\t\t\tinternalformat,\n\t\t\tsize.width(),\n\t\t\tsize.height(),\n\t\t\t0,\n\t\t\tformat,\n\t\t\tGL_UNSIGNED_BYTE,\n\t\t\tdata);\n\t} else {\n\t\tf.glTexSubImage2D(\n\t\t\tGL_TEXTURE_2D,\n\t\t\t0,\n\t\t\t0,\n\t\t\t0,\n\t\t\tsize.width(),\n\t\t\tsize.height(),\n\t\t\tformat,\n\t\t\tGL_UNSIGNED_BYTE,\n\t\t\tdata);\n\t}\n\tf.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);\n}\n\nPanel::Incoming::RendererSW::RendererSW(not_null<Incoming*> owner)\n: _owner(owner) {\n\tinitBottomShadow();\n}\n\nvoid Panel::Incoming::RendererSW::paintFallback(\n\t\tPainter &p,\n\t\tconst QRegion &clip,\n\t\tUi::GL::Backend backend) {\n\tconst auto markGuard = gsl::finally([&] {\n\t\t_owner->_track->markFrameShown();\n\t});\n\tconst auto data = _owner->_track->frameWithInfo(true);\n\tconst auto &image = data.original;\n\tconst auto rotation = data.rotation;\n\tif (image.isNull()) {\n\t\tp.fillRect(clip.boundingRect(), Qt::black);\n\t} else {\n\t\tconst auto rect = _owner->widget()->rect();\n\t\tusing namespace Media::View;\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tif (UsePainterRotation(rotation)) {\n\t\t\tif (rotation) {\n\t\t\t\tp.save();\n\t\t\t\tp.rotate(rotation);\n\t\t\t}\n\t\t\tp.drawImage(RotatedRect(rect, rotation), image);\n\t\t\tif (rotation) {\n\t\t\t\tp.restore();\n\t\t\t}\n\t\t} else if (rotation) {\n\t\t\tp.drawImage(rect, RotateFrameImage(image, rotation));\n\t\t} else {\n\t\t\tp.drawImage(rect, image);\n\t\t}\n\t\tfillBottomShadow(p);\n\t\tfillTopShadow(p);\n\t}\n}\n\nvoid Panel::Incoming::RendererSW::initBottomShadow() {\n\tauto image = QImage(\n\t\tQSize(1, st::callBottomShadowSize) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tconst auto colorFrom = uint32(0);\n\tconst auto colorTill = uint32(kBottomShadowAlphaMax);\n\tconst auto rows = image.height();\n\tconst auto step = (uint64(colorTill - colorFrom) << 32) / rows;\n\tauto accumulated = uint64();\n\tauto bytes = image.bits();\n\tfor (auto y = 0; y != rows; ++y) {\n\t\taccumulated += step;\n\t\tconst auto color = (colorFrom + uint32(accumulated >> 32)) << 24;\n\t\tfor (auto x = 0; x != image.width(); ++x) {\n\t\t\t*(reinterpret_cast<uint32*>(bytes) + x) = color;\n\t\t}\n\t\tbytes += image.bytesPerLine();\n\t}\n\t_bottomShadow = std::move(image);\n}\n\nvoid Panel::Incoming::RendererSW::fillTopShadow(QPainter &p) {\n#ifndef Q_OS_MAC\n\tconst auto widget = _owner->widget();\n\tconst auto width = widget->parentWidget()->width();\n\tconst auto left = (_owner->_topControlsAlignment == style::al_left);\n\tconst auto &icon = left\n\t\t? st::callTitleShadowLeft\n\t\t: st::callTitleShadowRight;\n\tconst auto position = left\n\t\t? QPoint()\n\t\t: QPoint(width - icon.width(), 0);\n\tconst auto shadowArea = QRect(position, icon.size());\n\tconst auto fill = shadowArea.intersected(\n\t\twidget->geometry()).translated(-widget->pos());\n\tif (fill.isEmpty()) {\n\t\treturn;\n\t}\n\tp.save();\n\tp.setClipRect(fill);\n\ticon.paint(p, position - widget->pos(), width);\n\tp.restore();\n#endif // Q_OS_MAC\n}\n\nvoid Panel::Incoming::RendererSW::fillBottomShadow(QPainter &p) {\n\tconst auto widget = _owner->widget();\n\tconst auto shadowArea = QRect(\n\t\t0,\n\t\twidget->parentWidget()->height() - st::callBottomShadowSize,\n\t\twidget->parentWidget()->width(),\n\t\tst::callBottomShadowSize);\n\tconst auto fill = shadowArea.intersected(\n\t\twidget->geometry()).translated(-widget->pos());\n\tif (fill.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto factor = style::DevicePixelRatio();\n\tp.drawImage(\n\t\tfill,\n\t\t_bottomShadow,\n\t\tQRect(\n\t\t\t0,\n\t\t\t(factor\n\t\t\t\t* (fill.y() - shadowArea.translated(-widget->pos()).y())),\n\t\t\tfactor,\n\t\t\tfactor * fill.height()));\n}\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\nstruct IncomingShadowUniforms {\n\tfloat viewport[2];\n\tfloat shadow[3];\n\tfloat _pad0;\n};\nstatic_assert(sizeof(IncomingShadowUniforms) == 24);\n\nclass Panel::Incoming::RendererRhi final\n\t: public Ui::GL::Renderer\n\t, public Ui::Rhi::Renderer {\npublic:\n\texplicit RendererRhi(not_null<Incoming*> owner) : _owner(owner) {\n\t}\n\t~RendererRhi() {\n\t\treleaseResources();\n\t}\n\n\tvoid initialize(\n\t\t\tQRhi *rhi,\n\t\t\tQRhiRenderTarget *rt,\n\t\t\tQRhiCommandBuffer *cb) override {\n\t\tif (_initialized && _rhi == rhi) {\n\t\t\treturn;\n\t\t}\n\t\t_rhi = rhi;\n\t\t_vertexBuffer = rhi->newBuffer(\n\t\t\tQRhiBuffer::Dynamic,\n\t\t\tQRhiBuffer::VertexBuffer,\n\t\t\t4 * 4 * sizeof(float));\n\t\t_vertexBuffer->create();\n\t\t_uniformBuffer = rhi->newBuffer(\n\t\t\tQRhiBuffer::Dynamic,\n\t\t\tQRhiBuffer::UniformBuffer,\n\t\t\t256);\n\t\t_uniformBuffer->create();\n\t\t_sampler = rhi->newSampler(\n\t\t\tQRhiSampler::Linear, QRhiSampler::Linear,\n\t\t\tQRhiSampler::None,\n\t\t\tQRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);\n\t\t_sampler->create();\n\t\t_placeholder = rhi->newTexture(QRhiTexture::BGRA8, QSize(1, 1));\n\t\t_placeholder->create();\n\n\t\tconst auto rpDesc = rt->renderPassDescriptor();\n\t\tconst auto vs = Ui::Rhi::ShaderFromFile(\n\t\t\tu\":/shaders/argb32.vert.qsb\"_q);\n\t\tconst auto argb32Fs = Ui::Rhi::ShaderFromFile(\n\t\t\tu\":/shaders/incoming_shadow.frag.qsb\"_q);\n\t\tconst auto yuv420Fs = Ui::Rhi::ShaderFromFile(\n\t\t\tu\":/shaders/incoming_yuv420.frag.qsb\"_q);\n\n\t\tQRhiVertexInputLayout layout;\n\t\tlayout.setBindings({ { 4 * sizeof(float) } });\n\t\tlayout.setAttributes({\n\t\t\t{ 0, 0, QRhiVertexInputAttribute::Float2, 0 },\n\t\t\t{ 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) },\n\t\t});\n\n\t\t_argb32Srb = rhi->newShaderResourceBindings();\n\t\t_argb32Srb->setBindings({\n\t\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t\t0,\n\t\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_uniformBuffer),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t1, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_placeholder, _sampler),\n\t\t});\n\t\t_argb32Srb->create();\n\n\t\t_argb32Pipeline = rhi->newGraphicsPipeline();\n\t\t_argb32Pipeline->setShaderStages({\n\t\t\t{ QRhiShaderStage::Vertex, vs },\n\t\t\t{ QRhiShaderStage::Fragment, argb32Fs },\n\t\t});\n\t\t_argb32Pipeline->setVertexInputLayout(layout);\n\t\t_argb32Pipeline->setTopology(\n\t\t\tQRhiGraphicsPipeline::TriangleStrip);\n\t\t_argb32Pipeline->setShaderResourceBindings(_argb32Srb);\n\t\t_argb32Pipeline->setRenderPassDescriptor(rpDesc);\n\t\t_argb32Pipeline->create();\n\n\t\t_yuv420Srb = rhi->newShaderResourceBindings();\n\t\t_yuv420Srb->setBindings({\n\t\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t\t0,\n\t\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_uniformBuffer),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t1, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_placeholder, _sampler),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t2, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_placeholder, _sampler),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t3, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_placeholder, _sampler),\n\t\t});\n\t\t_yuv420Srb->create();\n\n\t\t_yuv420Pipeline = rhi->newGraphicsPipeline();\n\t\t_yuv420Pipeline->setShaderStages({\n\t\t\t{ QRhiShaderStage::Vertex, vs },\n\t\t\t{ QRhiShaderStage::Fragment, yuv420Fs },\n\t\t});\n\t\t_yuv420Pipeline->setVertexInputLayout(layout);\n\t\t_yuv420Pipeline->setTopology(\n\t\t\tQRhiGraphicsPipeline::TriangleStrip);\n\t\t_yuv420Pipeline->setShaderResourceBindings(_yuv420Srb);\n\t\t_yuv420Pipeline->setRenderPassDescriptor(rpDesc);\n\t\t_yuv420Pipeline->create();\n\n#ifndef Q_OS_MAC\n\t\tconst auto shadowFs = Ui::Rhi::ShaderFromFile(\n\t\t\tu\":/shaders/argb32.frag.qsb\"_q);\n\t\tQRhiGraphicsPipeline::TargetBlend blend;\n\t\tblend.enable = true;\n\t\tblend.srcColor = QRhiGraphicsPipeline::One;\n\t\tblend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;\n\t\tblend.srcAlpha = QRhiGraphicsPipeline::One;\n\t\tblend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;\n\t\t_shadowBlendPipeline = rhi->newGraphicsPipeline();\n\t\t_shadowBlendPipeline->setShaderStages({\n\t\t\t{ QRhiShaderStage::Vertex, vs },\n\t\t\t{ QRhiShaderStage::Fragment, shadowFs },\n\t\t});\n\t\t_shadowBlendPipeline->setVertexInputLayout(layout);\n\t\t_shadowBlendPipeline->setTargetBlends({ blend });\n\t\t_shadowBlendPipeline->setTopology(\n\t\t\tQRhiGraphicsPipeline::TriangleStrip);\n\t\t_shadowBlendPipeline->setShaderResourceBindings(_argb32Srb);\n\t\t_shadowBlendPipeline->setRenderPassDescriptor(rpDesc);\n\t\t_shadowBlendPipeline->create();\n#endif\n\n\t\t_initialized = true;\n\t}\n\n\tvoid render(\n\t\t\tQRhi *rhi,\n\t\t\tQRhiRenderTarget *rt,\n\t\t\tQRhiCommandBuffer *cb) override {\n\t\t_rhi = rhi;\n\t\tconst auto markGuard = gsl::finally([&] {\n\t\t\t_owner->_track->markFrameShown();\n\t\t});\n\t\tconst auto data = _owner->_track->frameWithInfo(false);\n\t\tif (data.format == Webrtc::FrameFormat::None) {\n\t\t\tauto *rub = rhi->nextResourceUpdateBatch();\n\t\t\tcb->beginPass(rt, Qt::black, { 1.0f, 0 }, rub);\n\t\t\tcb->endPass();\n\t\t\treturn;\n\t\t}\n\n\t\tauto *rub = rhi->nextResourceUpdateBatch();\n\t\tconst auto rgbaFrame =\n\t\t\t(data.format == Webrtc::FrameFormat::ARGB32);\n\t\tconst auto upload = (_trackFrameIndex != data.index);\n\t\t_trackFrameIndex = data.index;\n\t\tconst auto rotation = data.rotation;\n\n\t\tauto *pipeline = _argb32Pipeline;\n\t\tauto *srb = _argb32Srb;\n\n\t\tif (rgbaFrame) {\n\t\t\tAssert(!data.original.isNull());\n\t\t\tconst auto &image = data.original;\n\t\t\tif (upload || !_rgbaTexture\n\t\t\t\t|| _rgbaSize != image.size()) {\n\t\t\t\tdelete _rgbaTexture;\n\t\t\t\t_rgbaTexture = rhi->newTexture(\n\t\t\t\t\tQRhiTexture::BGRA8, image.size());\n\t\t\t\t_rgbaTexture->create();\n\t\t\t\t_rgbaSize = image.size();\n\t\t\t}\n\t\t\tif (upload) {\n\t\t\t\trub->uploadTexture(_rgbaTexture,\n\t\t\t\t\tQRhiTextureUploadDescription(\n\t\t\t\t\t\tQRhiTextureUploadEntry(0, 0,\n\t\t\t\t\t\t\tQRhiTextureSubresourceUploadDescription(\n\t\t\t\t\t\t\t\timage))));\n\t\t\t}\n\t\t\tsrb->setBindings({\n\t\t\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t\t\t0,\n\t\t\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t\t_uniformBuffer),\n\t\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t\t1, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t\t_rgbaTexture, _sampler),\n\t\t\t});\n\t\t} else {\n\t\t\tAssert(data.format == Webrtc::FrameFormat::YUV420);\n\t\t\tAssert(!data.yuv420->size.isEmpty());\n\t\t\tconst auto yuv = data.yuv420;\n\t\t\tpipeline = _yuv420Pipeline;\n\t\t\tsrb = _yuv420Srb;\n\n\t\t\tif (upload) {\n\t\t\t\tif (!_yTexture || _lumaSize != yuv->size) {\n\t\t\t\t\tdelete _yTexture;\n\t\t\t\t\t_yTexture = rhi->newTexture(\n\t\t\t\t\t\tQRhiTexture::R8, yuv->size);\n\t\t\t\t\t_yTexture->create();\n\t\t\t\t\t_lumaSize = yuv->size;\n\t\t\t\t}\n\t\t\t\tauto yDesc = QRhiTextureSubresourceUploadDescription(\n\t\t\t\t\tyuv->y.data,\n\t\t\t\t\tyuv->y.stride * yuv->size.height());\n\t\t\t\tyDesc.setDataStride(yuv->y.stride);\n\t\t\t\trub->uploadTexture(_yTexture,\n\t\t\t\t\tQRhiTextureUploadDescription(\n\t\t\t\t\t\tQRhiTextureUploadEntry(0, 0, yDesc)));\n\n\t\t\t\tif (!_uTexture || _chromaSize != yuv->chromaSize) {\n\t\t\t\t\tdelete _uTexture;\n\t\t\t\t\t_uTexture = rhi->newTexture(\n\t\t\t\t\t\tQRhiTexture::R8, yuv->chromaSize);\n\t\t\t\t\t_uTexture->create();\n\t\t\t\t\tdelete _vTexture;\n\t\t\t\t\t_vTexture = rhi->newTexture(\n\t\t\t\t\t\tQRhiTexture::R8, yuv->chromaSize);\n\t\t\t\t\t_vTexture->create();\n\t\t\t\t\t_chromaSize = yuv->chromaSize;\n\t\t\t\t}\n\t\t\t\tauto uDesc = QRhiTextureSubresourceUploadDescription(\n\t\t\t\t\tyuv->u.data,\n\t\t\t\t\tyuv->u.stride * yuv->chromaSize.height());\n\t\t\t\tuDesc.setDataStride(yuv->u.stride);\n\t\t\t\trub->uploadTexture(_uTexture,\n\t\t\t\t\tQRhiTextureUploadDescription(\n\t\t\t\t\t\tQRhiTextureUploadEntry(0, 0, uDesc)));\n\t\t\t\tauto vDesc = QRhiTextureSubresourceUploadDescription(\n\t\t\t\t\tyuv->v.data,\n\t\t\t\t\tyuv->v.stride * yuv->chromaSize.height());\n\t\t\t\tvDesc.setDataStride(yuv->v.stride);\n\t\t\t\trub->uploadTexture(_vTexture,\n\t\t\t\t\tQRhiTextureUploadDescription(\n\t\t\t\t\t\tQRhiTextureUploadEntry(0, 0, vDesc)));\n\t\t\t}\n\t\t\tsrb->setBindings({\n\t\t\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t\t\t0,\n\t\t\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t\t_uniformBuffer),\n\t\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t\t1, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t\t_yTexture, _sampler),\n\t\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t\t2, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t\t_uTexture, _sampler),\n\t\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t\t3, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t\t_vTexture, _sampler),\n\t\t\t});\n\t\t}\n\t\tsrb->create();\n\n\t\tconst auto pw = float(rt->pixelSize().width());\n\t\tconst auto ph = float(rt->pixelSize().height());\n\t\tconst auto factor = style::DevicePixelRatio();\n\n\t\tconst auto widget = _owner->widget();\n\t\tconst auto bottomShadowArea = QRect(\n\t\t\t0,\n\t\t\twidget->parentWidget()->height()\n\t\t\t\t- st::callBottomShadowSize,\n\t\t\twidget->parentWidget()->width(),\n\t\t\tst::callBottomShadowSize);\n\t\tconst auto bottomShadowFill = bottomShadowArea.intersected(\n\t\t\twidget->geometry()).translated(-widget->pos());\n\t\tconst auto shadowHeight = bottomShadowFill.height();\n\t\tconst auto shadowAlpha = float(\n\t\t\tshadowHeight * kBottomShadowAlphaMax)\n\t\t\t/ float(st::callBottomShadowSize * 255);\n\t\tconst auto viewport = QSize(\n\t\t\tint(pw / factor), int(ph / factor));\n\t\tconst auto shadowBottom = Ui::GL::TransformRect(\n\t\t\tUi::GL::Rect(bottomShadowFill),\n\t\t\tviewport,\n\t\t\tfactor);\n\n\t\tIncomingShadowUniforms uniforms{};\n\t\tuniforms.viewport[0] = pw;\n\t\tuniforms.viewport[1] = ph;\n\t\tuniforms.shadow[0] = shadowHeight * factor;\n\t\tuniforms.shadow[1] = shadowBottom.bottom();\n\t\tuniforms.shadow[2] = shadowAlpha;\n\t\trub->updateDynamicBuffer(\n\t\t\t_uniformBuffer, 0, sizeof(uniforms), &uniforms);\n\n\t\tstd::array<std::array<float, 2>, 4> texCoords = { {\n\t\t\t{ { 0.f, 0.f } }, { { 1.f, 0.f } },\n\t\t\t{ { 0.f, 1.f } }, { { 1.f, 1.f } },\n\t\t} };\n\t\tif (const auto shift = (rotation / 90); shift != 0) {\n\t\t\tstd::rotate(\n\t\t\t\ttexCoords.begin(),\n\t\t\t\ttexCoords.begin() + shift,\n\t\t\t\ttexCoords.end());\n\t\t}\n\t\tconst float coords[] = {\n\t\t\t0.f, ph, texCoords[0][0], texCoords[0][1],\n\t\t\tpw,  ph, texCoords[1][0], texCoords[1][1],\n\t\t\t0.f, 0.f, texCoords[2][0], texCoords[2][1],\n\t\t\tpw,  0.f, texCoords[3][0], texCoords[3][1],\n\t\t};\n\t\trub->updateDynamicBuffer(\n\t\t\t_vertexBuffer, 0, sizeof(coords), coords);\n\n\t\tcb->beginPass(rt, Qt::black, { 1.0f, 0 }, rub);\n\t\tcb->setGraphicsPipeline(pipeline);\n\t\tcb->setShaderResources(srb);\n\t\tcb->setViewport({ 0, 0, pw, ph });\n\t\tconst QRhiCommandBuffer::VertexInput vbuf(_vertexBuffer, 0);\n\t\tcb->setVertexInput(0, 1, &vbuf);\n\t\tcb->draw(4);\n\n#ifndef Q_OS_MAC\n\t\tpaintTitleShadow(rt, cb, pw, ph);\n#endif\n\n\t\tcb->endPass();\n\t}\n\n\tvoid paintTitleShadow(\n\t\t\tQRhiRenderTarget *rt,\n\t\t\tQRhiCommandBuffer *cb,\n\t\t\tfloat pw,\n\t\t\tfloat ph) {\n\t\tif (!_shadowBlendPipeline) {\n\t\t\treturn;\n\t\t}\n\t\tvalidateShadowImage();\n\t\tif (!_shadowTexture) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tconst auto widget = _owner->widget();\n\t\tconst auto left = (_owner->_topControlsAlignment\n\t\t\t== style::al_left);\n\t\tconst auto width = widget->parentWidget()->width();\n\t\tconst auto position = left\n\t\t\t? QPoint()\n\t\t\t: QPoint(\n\t\t\t\twidth - st::callTitleShadowRight.width(),\n\t\t\t\t0);\n\t\tconst auto translated = position - widget->pos();\n\t\tconst auto shadowArea = QRect(\n\t\t\ttranslated,\n\t\t\tst::callTitleShadowLeft.size());\n\t\tconst auto viewport = QSize(\n\t\t\tint(pw / factor), int(ph / factor));\n\t\tconst auto shadowRect = Ui::GL::TransformRect(\n\t\t\tUi::GL::Rect(shadowArea),\n\t\t\tviewport,\n\t\t\tfactor);\n\n\t\tconst auto &texRect = left\n\t\t\t? _shadowLeftRect : _shadowRightRect;\n\t\tconst auto atlasW = float(_shadowSize.width());\n\t\tconst auto atlasH = float(_shadowSize.height());\n\t\tconst auto tl = texRect.left() / atlasW;\n\t\tconst auto tr = texRect.right() / atlasW;\n\t\tconst auto tt = texRect.top() / atlasH;\n\t\tconst auto tb = texRect.bottom() / atlasH;\n\n\t\tauto *rub2 = _rhi->nextResourceUpdateBatch();\n\t\tensureShadowUploaded(rub2);\n\t\tconst float shadowCoords[] = {\n\t\t\tshadowRect.left(), shadowRect.bottom(), tl, tt,\n\t\t\tshadowRect.right(), shadowRect.bottom(), tr, tt,\n\t\t\tshadowRect.left(), shadowRect.top(), tl, tb,\n\t\t\tshadowRect.right(), shadowRect.top(), tr, tb,\n\t\t};\n\t\trub2->updateDynamicBuffer(\n\t\t\t_vertexBuffer, 0, sizeof(shadowCoords), shadowCoords);\n\t\tconst float viewport2[] = { pw, ph, 0.f, 0.f };\n\t\trub2->updateDynamicBuffer(\n\t\t\t_uniformBuffer, 0, sizeof(viewport2), viewport2);\n\n\t\t_argb32Srb->setBindings({\n\t\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t\t0,\n\t\t\t\tQRhiShaderResourceBinding::VertexStage,\n\t\t\t\t_uniformBuffer),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t1,\n\t\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_shadowTexture, _sampler),\n\t\t});\n\t\t_argb32Srb->create();\n\n\t\tcb->resourceUpdate(rub2);\n\t\tcb->setGraphicsPipeline(_shadowBlendPipeline);\n\t\tcb->setShaderResources(_argb32Srb);\n\t\tcb->setViewport({ 0, 0, pw, ph });\n\t\tconst QRhiCommandBuffer::VertexInput vbuf2(_vertexBuffer, 0);\n\t\tcb->setVertexInput(0, 1, &vbuf2);\n\t\tcb->draw(4);\n\t}\n\n\tvoid validateShadowImage() {\n\t\tif (_shadowTexture) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto ifactor = int(std::ceil(\n\t\t\tstyle::DevicePixelRatio()));\n\t\tconst auto size = st::callTitleShadowLeft.size();\n\t\tconst auto full = QSize(\n\t\t\tsize.width(), 2 * size.height()) * ifactor;\n\t\tauto image = QImage(\n\t\t\tfull, QImage::Format_ARGB32_Premultiplied);\n\t\timage.setDevicePixelRatio(ifactor);\n\t\timage.fill(Qt::transparent);\n\t\t{\n\t\t\tauto p = QPainter(&image);\n\t\t\tst::callTitleShadowLeft.paint(\n\t\t\t\tp, 0, 0, size.width());\n\t\t\t_shadowLeftRect = QRect(\n\t\t\t\t0, 0, full.width(), full.height() / 2);\n\t\t\tst::callTitleShadowRight.paint(\n\t\t\t\tp, 0, size.height(), size.width());\n\t\t\t_shadowRightRect = QRect(\n\t\t\t\t0, full.height() / 2,\n\t\t\t\tfull.width(), full.height() / 2);\n\t\t}\n\t\t_shadowTexture = _rhi->newTexture(\n\t\t\tQRhiTexture::BGRA8, full);\n\t\t_shadowTexture->create();\n\t\t_shadowSize = full;\n\t\t_shadowUploadImage = std::move(image);\n\t}\n\n\tQImage _shadowUploadImage;\n\n\tvoid ensureShadowUploaded(QRhiResourceUpdateBatch *rub) {\n\t\tif (_shadowUploadImage.isNull()) {\n\t\t\treturn;\n\t\t}\n\t\trub->uploadTexture(\n\t\t\t_shadowTexture,\n\t\t\tQRhiTextureUploadDescription(\n\t\t\t\tQRhiTextureUploadEntry(0, 0,\n\t\t\t\t\tQRhiTextureSubresourceUploadDescription(\n\t\t\t\t\t\t_shadowUploadImage))));\n\t\t_shadowUploadImage = QImage();\n\t}\n\n\tvoid releaseResources() override {\n\t\tdelete _argb32Pipeline; _argb32Pipeline = nullptr;\n\t\tdelete _yuv420Pipeline; _yuv420Pipeline = nullptr;\n\t\tdelete _shadowBlendPipeline; _shadowBlendPipeline = nullptr;\n\t\tdelete _argb32Srb; _argb32Srb = nullptr;\n\t\tdelete _yuv420Srb; _yuv420Srb = nullptr;\n\t\tdelete _shadowTexture; _shadowTexture = nullptr;\n\t\tdelete _rgbaTexture; _rgbaTexture = nullptr;\n\t\tdelete _yTexture; _yTexture = nullptr;\n\t\tdelete _uTexture; _uTexture = nullptr;\n\t\tdelete _vTexture; _vTexture = nullptr;\n\t\tdelete _placeholder; _placeholder = nullptr;\n\t\tdelete _vertexBuffer; _vertexBuffer = nullptr;\n\t\tdelete _uniformBuffer; _uniformBuffer = nullptr;\n\t\tdelete _sampler; _sampler = nullptr;\n\t\t_initialized = false;\n\t}\n\n\tQColor rhiClearColor() override { return Qt::black; }\n\nprivate:\n\tconst not_null<Incoming*> _owner;\n\tQRhi *_rhi = nullptr;\n\tQRhiBuffer *_vertexBuffer = nullptr;\n\tQRhiBuffer *_uniformBuffer = nullptr;\n\tQRhiSampler *_sampler = nullptr;\n\tQRhiTexture *_placeholder = nullptr;\n\tQRhiTexture *_rgbaTexture = nullptr;\n\tQSize _rgbaSize;\n\tQRhiTexture *_yTexture = nullptr;\n\tQRhiTexture *_uTexture = nullptr;\n\tQRhiTexture *_vTexture = nullptr;\n\tQSize _lumaSize;\n\tQSize _chromaSize;\n\tint _trackFrameIndex = 0;\n\tQRhiGraphicsPipeline *_argb32Pipeline = nullptr;\n\tQRhiGraphicsPipeline *_yuv420Pipeline = nullptr;\n\tQRhiGraphicsPipeline *_shadowBlendPipeline = nullptr;\n\tQRhiShaderResourceBindings *_argb32Srb = nullptr;\n\tQRhiShaderResourceBindings *_yuv420Srb = nullptr;\n\tQRhiTexture *_shadowTexture = nullptr;\n\tQSize _shadowSize;\n\tQRect _shadowLeftRect;\n\tQRect _shadowRightRect;\n\tbool _initialized = false;\n};\n#endif // Qt >= 6.7\n\nPanel::Incoming::Incoming(\n\tnot_null<QWidget*> parent,\n\tnot_null<Webrtc::VideoTrack*> track,\n\tUi::GL::Backend backend)\n: _surface(Ui::GL::CreateSurface(parent, chooseRenderer(backend)))\n, _track(track) {\n\twidget()->setAttribute(Qt::WA_OpaquePaintEvent);\n\twidget()->setAttribute(Qt::WA_TransparentForMouseEvents);\n}\n\nnot_null<QWidget*> Panel::Incoming::widget() const {\n\treturn _surface->rpWidget();\n}\n\nnot_null<Ui::RpWidgetWrap*> Panel::Incoming::rp() const {\n\treturn _surface.get();\n}\n\nvoid Panel::Incoming::setControlsAlignment(style::align align) {\n\tif (_topControlsAlignment != align) {\n\t\t_topControlsAlignment = align;\n\t\twidget()->update();\n\t}\n}\n\nUi::GL::ChosenRenderer Panel::Incoming::chooseRenderer(\n\t\tUi::GL::Backend backend) {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\tif (backend == Ui::GL::Backend::QRhi) {\n\t\t_opengl = true;\n\t\treturn {\n\t\t\t.renderer = std::make_unique<RendererRhi>(this),\n\t\t\t.backend = Ui::GL::Backend::QRhi,\n\t\t};\n\t}\n#endif // Qt >= 6.7\n\t_opengl = (backend == Ui::GL::Backend::OpenGL);\n\treturn {\n\t\t.renderer = (_opengl\n\t\t\t? std::unique_ptr<Ui::GL::Renderer>(\n\t\t\t\tstd::make_unique<RendererGL>(this))\n\t\t\t: std::make_unique<RendererSW>(this)),\n\t\t.backend = backend,\n\t};\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_video_incoming.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"calls/calls_panel.h\"\n\nnamespace Ui::GL {\nenum class Backend;\nstruct ChosenRenderer;\n} // namespace Ui::GL\n\nnamespace Calls {\n\nclass Panel::Incoming final {\npublic:\n\tIncoming(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Webrtc::VideoTrack*> track,\n\t\tUi::GL::Backend backend);\n\n\t[[nodiscard]] not_null<QWidget*> widget() const;\n\t[[nodiscard]] not_null<Ui::RpWidgetWrap*> rp() const;\n\n\tvoid setControlsAlignment(style::align align);\n\nprivate:\n\tclass RendererGL;\n\tclass RendererSW;\n\tclass RendererRhi;\n\n\t[[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer(\n\t\tUi::GL::Backend backend);\n\n\tconst std::unique_ptr<Ui::RpWidgetWrap> _surface;\n\tconst not_null<Webrtc::VideoTrack*> _track;\n\tstyle::align _topControlsAlignment = style::al_left;\n\tbool _opengl = false;\n\n};\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_window.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/calls_window.h\"\n\n#include \"base/power_save_blocker.h\"\n#include \"ui/platform/ui_platform_window_title.h\"\n#include \"ui/widgets/rp_window.h\"\n#include \"ui/layers/layer_manager.h\"\n#include \"ui/layers/show.h\"\n#include \"styles/style_calls.h\"\n\nnamespace Calls {\nnamespace {\n\nclass Show final : public Ui::Show {\npublic:\n\texplicit Show(not_null<Window*> window);\n\t~Show();\n\n\tvoid showOrHideBoxOrLayer(\n\t\tstd::variant<\n\t\t\tv::null_t,\n\t\t\tobject_ptr<Ui::BoxContent>,\n\t\t\tstd::unique_ptr<Ui::LayerWidget>> &&layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) const override;\n\t[[nodiscard]] not_null<QWidget*> toastParent() const override;\n\t[[nodiscard]] bool valid() const override;\n\toperator bool() const override;\n\nprivate:\n\tconst base::weak_ptr<Window> _window;\n\n};\n\nShow::Show(not_null<Window*> window)\n: _window(base::make_weak(window)) {\n}\n\nShow::~Show() = default;\n\nvoid Show::showOrHideBoxOrLayer(\n\t\tstd::variant<\n\t\t\tv::null_t,\n\t\t\tobject_ptr<Ui::BoxContent>,\n\t\t\tstd::unique_ptr<Ui::LayerWidget>> &&layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) const {\n\tusing UniqueLayer = std::unique_ptr<Ui::LayerWidget>;\n\tusing ObjectBox = object_ptr<Ui::BoxContent>;\n\tif (auto layerWidget = std::get_if<UniqueLayer>(&layer)) {\n\t\tif (const auto window = _window.get()) {\n\t\t\twindow->showLayer(std::move(*layerWidget), options, animated);\n\t\t}\n\t} else if (auto box = std::get_if<ObjectBox>(&layer)) {\n\t\tif (const auto window = _window.get()) {\n\t\t\twindow->showBox(std::move(*box), options, animated);\n\t\t}\n\t} else if (const auto window = _window.get()) {\n\t\twindow->hideLayer(animated);\n\t}\n}\n\nnot_null<QWidget*> Show::toastParent() const {\n\tconst auto window = _window.get();\n\tAssert(window != nullptr);\n\treturn window->widget();\n}\n\nbool Show::valid() const {\n\treturn !_window.empty();\n}\n\nShow::operator bool() const {\n\treturn valid();\n}\n\n} // namespace\n\nWindow::Window()\n: _layerBg(std::make_unique<Ui::LayerManager>(widget()))\n#ifndef Q_OS_MAC\n, _controls(Ui::Platform::SetupSeparateTitleControls(\n\twindow(),\n\tst::callTitle,\n\t[=](bool maximized) { _maximizeRequests.fire_copy(maximized); },\n\t_controlsTop.value()))\n#endif // !Q_OS_MAC\n{\n\t_layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox);\n\t_layerBg->setHideByBackgroundClick(true);\n}\n\nWindow::~Window() = default;\n\nUi::GL::Backend Window::backend() const {\n\treturn _window.backend();\n}\n\nnot_null<Ui::RpWindow*> Window::window() const {\n\treturn _window.window();\n}\n\nnot_null<Ui::RpWidget*> Window::widget() const {\n\treturn _window.widget();\n}\n\nvoid Window::raiseControls() {\n#ifndef Q_OS_MAC\n\t_controls->wrap.raise();\n#endif // !Q_OS_MAC\n}\n\nvoid Window::setControlsStyle(const style::WindowTitle &st) {\n#ifndef Q_OS_MAC\n\t_controls->controls.setStyle(st);\n#endif // Q_OS_MAC\n}\n\nvoid Window::setControlsShown(float64 shown) {\n#ifndef Q_OS_MAC\n\t_controlsTop = anim::interpolate(-_controls->wrap.height(), 0, shown);\n#endif // Q_OS_MAC\n}\n\nint Window::controlsWrapTop() const {\n#ifndef Q_OS_MAC\n\treturn _controls->wrap.y();\n#else // Q_OS_MAC\n\treturn 0;\n#endif // Q_OS_MAC\n}\n\nQRect Window::controlsGeometry() const {\n#ifndef Q_OS_MAC\n\treturn _controls->controls.geometry();\n#else // Q_OS_MAC\n\treturn QRect();\n#endif // Q_OS_MAC\n}\n\nauto Window::controlsLayoutChanges() const\n-> rpl::producer<Ui::Platform::TitleLayout> {\n#ifndef Q_OS_MAC\n\treturn _controls->controls.layout().changes();\n#else // Q_OS_MAC\n\treturn rpl::never<Ui::Platform::TitleLayout>();\n#endif // Q_OS_MAC\n}\n\nbool Window::controlsHasHitTest(QPoint widgetPoint) const {\n#ifndef Q_OS_MAC\n\tusing Result = Ui::Platform::HitTestResult;\n\tconst auto windowPoint = widget()->mapTo(window(), widgetPoint);\n\treturn (_controls->controls.hitTest(windowPoint) != Result::None);\n#else // Q_OS_MAC\n\treturn false;\n#endif // Q_OS_MAC\n}\n\nrpl::producer<bool> Window::maximizeRequests() const {\n\treturn _maximizeRequests.events();\n}\n\nbase::weak_ptr<Ui::Toast::Instance> Window::showToast(\n\t\tconst QString &text,\n\t\tcrl::time duration) {\n\treturn Show(this).showToast(text, duration);\n}\n\nbase::weak_ptr<Ui::Toast::Instance> Window::showToast(\n\t\tTextWithEntities &&text,\n\t\tcrl::time duration) {\n\treturn Show(this).showToast(std::move(text), duration);\n}\n\nbase::weak_ptr<Ui::Toast::Instance> Window::showToast(\n\t\tUi::Toast::Config &&config) {\n\treturn Show(this).showToast(std::move(config));\n}\n\nvoid Window::raiseLayers() {\n\t_layerBg->raise();\n}\n\nconst Ui::LayerWidget *Window::topShownLayer() const {\n\treturn _layerBg->topShownLayer();\n}\n\nvoid Window::showBox(object_ptr<Ui::BoxContent> box) {\n\tshowBox(std::move(box), Ui::LayerOption::KeepOther, anim::type::normal);\n}\n\nvoid Window::showBox(\n\t\tobject_ptr<Ui::BoxContent> box,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) {\n\t_showingLayer.fire({});\n\tif (window()->width() < st::groupCallWidth\n\t\t|| window()->height() < st::groupCallWidth) {\n\t\twindow()->resize(\n\t\t\tstd::max(window()->width(), st::groupCallWidth),\n\t\t\tstd::max(window()->height(), st::groupCallWidth));\n\t}\n\t_layerBg->showBox(std::move(box), options, animated);\n}\n\nvoid Window::showLayer(\n\t\tstd::unique_ptr<Ui::LayerWidget> layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) {\n\t_showingLayer.fire({});\n\tif (window()->width() < st::groupCallWidth\n\t\t|| window()->height() < st::groupCallWidth) {\n\t\twindow()->resize(\n\t\t\tstd::max(window()->width(), st::groupCallWidth),\n\t\t\tstd::max(window()->height(), st::groupCallWidth));\n\t}\n\t_layerBg->showLayer(std::move(layer), options, animated);\n}\n\nvoid Window::hideLayer(anim::type animated) {\n\t_layerBg->hideAll(animated);\n}\n\nbool Window::isLayerShown() const {\n\treturn _layerBg->topShownLayer() != nullptr;\n}\n\nstd::shared_ptr<Ui::Show> Window::uiShow() {\n\treturn std::make_shared<Show>(this);\n}\n\nvoid Window::togglePowerSaveBlocker(bool enabled) {\n\tif (!enabled) {\n\t\t_powerSaveBlocker = nullptr;\n\t} else if (!_powerSaveBlocker) {\n\t\t_powerSaveBlocker = std::make_unique<base::PowerSaveBlocker>(\n\t\t\tbase::PowerSaveBlockType::PreventDisplaySleep,\n\t\t\tu\"Video call is active\"_q,\n\t\t\twindow()->windowHandle());\n\t}\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/calls_window.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/gl/gl_window.h\"\n\nnamespace base {\nclass PowerSaveBlocker;\n} // namespace base\n\nnamespace style {\nstruct WindowTitle;\n} // namespace style\n\nnamespace Ui {\nclass BoxContent;\nclass RpWindow;\nclass RpWidget;\nclass LayerManager;\nclass LayerWidget;\nenum class LayerOption;\nusing LayerOptions = base::flags<LayerOption>;\nclass Show;\n} // namespace Ui\n\nnamespace Ui::Platform {\nstruct SeparateTitleControls;\nstruct TitleLayout;\n} // namespace Ui::Platform\n\nnamespace Ui::Toast {\nstruct Config;\nclass Instance;\n} // namespace Ui::Toast\n\nnamespace Calls {\n\nclass Window final : public base::has_weak_ptr {\npublic:\n\tWindow();\n\t~Window();\n\n\t[[nodiscard]] Ui::GL::Backend backend() const;\n\t[[nodiscard]] not_null<Ui::RpWindow*> window() const;\n\t[[nodiscard]] not_null<Ui::RpWidget*> widget() const;\n\n\tvoid raiseControls();\n\tvoid setControlsStyle(const style::WindowTitle &st);\n\tvoid setControlsShown(float64 shown);\n\t[[nodiscard]] int controlsWrapTop() const;\n\t[[nodiscard]] QRect controlsGeometry() const;\n\t[[nodiscard]] auto controlsLayoutChanges() const\n\t\t-> rpl::producer<Ui::Platform::TitleLayout>;\n\t[[nodiscard]] bool controlsHasHitTest(QPoint widgetPoint) const;\n\t[[nodiscard]] rpl::producer<bool> maximizeRequests() const;\n\n\tvoid raiseLayers();\n\t[[nodiscard]] const Ui::LayerWidget *topShownLayer() const;\n\n\tbase::weak_ptr<Ui::Toast::Instance> showToast(\n\t\tconst QString &text,\n\t\tcrl::time duration = 0);\n\tbase::weak_ptr<Ui::Toast::Instance> showToast(\n\t\tTextWithEntities &&text,\n\t\tcrl::time duration = 0);\n\tbase::weak_ptr<Ui::Toast::Instance> showToast(\n\t\tUi::Toast::Config &&config);\n\n\tvoid showBox(object_ptr<Ui::BoxContent> box);\n\tvoid showBox(\n\t\tobject_ptr<Ui::BoxContent> box,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated = anim::type::normal);\n\tvoid showLayer(\n\t\tstd::unique_ptr<Ui::LayerWidget> layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated = anim::type::normal);\n\tvoid hideLayer(anim::type animated = anim::type::normal);\n\t[[nodiscard]] bool isLayerShown() const;\n\n\t[[nodiscard]] rpl::producer<> showingLayer() const {\n\t\treturn _showingLayer.events();\n\t}\n\n\t[[nodiscard]] std::shared_ptr<Ui::Show> uiShow();\n\n\tvoid togglePowerSaveBlocker(bool enabled);\n\nprivate:\n\tUi::GL::Window _window;\n\tconst std::unique_ptr<Ui::LayerManager> _layerBg;\n\n#ifndef Q_OS_MAC\n\trpl::variable<int> _controlsTop = 0;\n\tconst std::unique_ptr<Ui::Platform::SeparateTitleControls> _controls;\n#endif // !Q_OS_MAC\n\n\tstd::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;\n\n\trpl::event_stream<bool> _maximizeRequests;\n\trpl::event_stream<> _showingLayer;\n\n};\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_choose_join_as.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_choose_join_as.h\"\n\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/group/calls_group_menu.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_group_call.h\"\n#include \"main/main_session.h\"\n#include \"main/main_account.h\"\n#include \"lang/lang_keys.h\"\n#include \"apiwrap.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/boxes/choose_date_time.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"base/unixtime.h\"\n#include \"base/timer_rpl.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Calls::Group {\nnamespace {\n\nconstexpr auto kLabelRefreshInterval = 10 * crl::time(1000);\n\nusing Context = ChooseJoinAsProcess::Context;\n\nclass ListController : public PeerListController {\npublic:\n\tListController(\n\t\tstd::vector<not_null<PeerData*>> list,\n\t\tnot_null<PeerData*> selected);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\n\t[[nodiscard]] not_null<PeerData*> selected() const;\n\nprivate:\n\tstd::unique_ptr<PeerListRow> createRow(not_null<PeerData*> peer);\n\n\tstd::vector<not_null<PeerData*>> _list;\n\tnot_null<PeerData*> _selected;\n\n};\n\nListController::ListController(\n\tstd::vector<not_null<PeerData*>> list,\n\tnot_null<PeerData*> selected)\n: PeerListController()\n, _list(std::move(list))\n, _selected(selected) {\n}\n\nMain::Session &ListController::session() const {\n\treturn _selected->session();\n}\n\nstd::unique_ptr<PeerListRow> ListController::createRow(\n\t\tnot_null<PeerData*> peer) {\n\tauto result = std::make_unique<PeerListRow>(peer);\n\tif (peer->isSelf()) {\n\t\tresult->setCustomStatus(\n\t\t\ttr::lng_group_call_join_as_personal(tr::now));\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tresult->setCustomStatus(\n\t\t\t(channel->isMegagroup()\n\t\t\t\t? tr::lng_chat_status_members\n\t\t\t\t: tr::lng_chat_status_subscribers)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tchannel->membersCount()));\n\t}\n\treturn result;\n}\n\nvoid ListController::prepare() {\n\tdelegate()->peerListSetSearchMode(PeerListSearchMode::Disabled);\n\tfor (const auto &peer : _list) {\n\t\tauto row = createRow(peer);\n\t\tconst auto raw = row.get();\n\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\tif (peer == _selected) {\n\t\t\tdelegate()->peerListSetRowChecked(raw, true);\n\t\t\traw->finishCheckedAnimation();\n\t\t}\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid ListController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto peer = row->peer();\n\tif (peer == _selected) {\n\t\treturn;\n\t}\n\tconst auto previous = delegate()->peerListFindRow(_selected->id.value);\n\tAssert(previous != nullptr);\n\tdelegate()->peerListSetRowChecked(previous, false);\n\tdelegate()->peerListSetRowChecked(row, true);\n\t_selected = peer;\n}\n\nnot_null<PeerData*> ListController::selected() const {\n\treturn _selected;\n}\n\nvoid ScheduleGroupCallBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst JoinInfo &info,\n\t\tFn<void(JoinInfo)> done) {\n\tconst auto send = [=](TimeId date) {\n\t\tbox->closeBox();\n\n\t\tauto copy = info;\n\t\tcopy.scheduleDate = date;\n\t\tdone(std::move(copy));\n\t};\n\tconst auto livestream = info.peer->isBroadcast();\n\tconst auto duration = box->lifetime().make_state<\n\t\trpl::variable<QString>>();\n\tauto description = (info.peer->isBroadcast()\n\t\t? tr::lng_group_call_schedule_notified_channel\n\t\t: tr::lng_group_call_schedule_notified_group)(\n\t\t\tlt_duration,\n\t\t\tduration->value());\n\n\tconst auto now = QDateTime::currentDateTime();\n\tconst auto min = [] {\n\t\treturn base::unixtime::serialize(\n\t\t\tQDateTime::currentDateTime().addSecs(12));\n\t};\n\tconst auto max = [] {\n\t\treturn base::unixtime::serialize(\n\t\t\tQDateTime(QDate::currentDate().addDays(8), QTime(0, 0))) - 1;\n\t};\n\n\t// At least half an hour later, at zero minutes/seconds.\n\tconst auto schedule = QDateTime(\n\t\tnow.date(),\n\t\tQTime(now.time().hour(), 0)\n\t).addSecs(60 * 60 * (now.time().minute() < 30 ? 1 : 2));\n\n\tauto descriptor = Ui::ChooseDateTimeBox(box, {\n\t\t.title = (livestream\n\t\t\t? tr::lng_group_call_schedule_title_channel()\n\t\t\t: tr::lng_group_call_schedule_title()),\n\t\t.submit = tr::lng_schedule_button(),\n\t\t.done = send,\n\t\t.min = min,\n\t\t.time = base::unixtime::serialize(schedule),\n\t\t.max = max,\n\t\t.description = std::move(description),\n\t});\n\n\tusing namespace rpl::mappers;\n\t*duration = rpl::combine(\n\t\trpl::single(rpl::empty) | rpl::then(\n\t\t\tbase::timer_each(kLabelRefreshInterval)\n\t\t),\n\t\tstd::move(descriptor.values) | rpl::filter(_1 != 0),\n\t\t_2\n\t) | rpl::map([](TimeId date) {\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto duration = (date - now);\n\t\tif (duration >= 24 * 60 * 60) {\n\t\t\treturn tr::lng_days(tr::now, lt_count, duration / (24 * 60 * 60));\n\t\t} else if (duration >= 60 * 60) {\n\t\t\treturn tr::lng_hours(tr::now, lt_count, duration / (60 * 60));\n\t\t}\n\t\treturn tr::lng_minutes(tr::now, lt_count, std::max(duration / 60, 1));\n\t});\n}\n\nvoid ChooseJoinAsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tContext context,\n\t\tJoinInfo info,\n\t\tFn<void(JoinInfo)> done) {\n\tbox->setWidth(st::groupCallJoinAsWidth);\n\tconst auto livestream = info.peer->isBroadcast();\n\tbox->setTitle([&] {\n\t\tswitch (context) {\n\t\tcase Context::Create: return livestream\n\t\t\t? tr::lng_group_call_start_as_header_channel()\n\t\t\t: tr::lng_group_call_start_as_header();\n\t\tcase Context::Join:\n\t\tcase Context::JoinWithConfirm: return livestream\n\t\t\t? tr::lng_group_call_join_as_header_channel()\n\t\t\t: tr::lng_group_call_join_as_header();\n\t\tcase Context::Switch: return tr::lng_group_call_display_as_header();\n\t\t}\n\t\tUnexpected(\"Context in ChooseJoinAsBox.\");\n\t}());\n\tconst auto &labelSt = (context == Context::Switch)\n\t\t? st::groupCallJoinAsLabel\n\t\t: st::confirmPhoneAboutLabel;\n\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\tbox,\n\t\ttr::lng_group_call_join_as_about(),\n\t\tlabelSt));\n\n\tauto &lifetime = box->lifetime();\n\tconst auto delegate = lifetime.make_state<\n\t\tPeerListContentDelegateSimple\n\t>();\n\tconst auto controller = lifetime.make_state<ListController>(\n\t\tinfo.possibleJoinAs,\n\t\tinfo.joinAs);\n\tif (context == Context::Switch) {\n\t\tcontroller->setStyleOverrides(\n\t\t\t&st::groupCallJoinAsList,\n\t\t\t&st::groupCallMultiSelect);\n\t} else {\n\t\tcontroller->setStyleOverrides(\n\t\t\t&st::defaultChooseSendAs.list,\n\t\t\tnullptr);\n\t}\n\tconst auto content = box->addRow(\n\t\tobject_ptr<PeerListContent>(box, controller),\n\t\tstyle::margins());\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\tconst auto &peer = info.peer;\n\tif ((context == Context::Create)\n\t\t&& (peer->isChannel() && peer->asChannel()->hasAdminRights())) {\n\t\tconst auto makeLink = [](const QString &text) {\n\t\t\treturn tr::link(text);\n\t\t};\n\t\tconst auto label = box->addRow(object_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_group_call_or_schedule(\n\t\t\t\tlt_link,\n\t\t\t\t(livestream\n\t\t\t\t\t? tr::lng_group_call_schedule_channel\n\t\t\t\t\t: tr::lng_group_call_schedule)(makeLink),\n\t\t\t\ttr::marked),\n\t\t\tlabelSt));\n\t\tlabel->overrideLinkClickHandler([=] {\n\t\t\tauto withJoinAs = info;\n\t\t\twithJoinAs.joinAs = controller->selected();\n\t\t\tbox->getDelegate()->show(\n\t\t\t\tBox(ScheduleGroupCallBox, withJoinAs, done));\n\t\t});\n\t}\n\tauto next = (context == Context::Switch)\n\t\t? tr::lng_settings_save()\n\t\t: tr::lng_continue();\n\tbox->addButton(std::move(next), [=] {\n\t\tauto copy = info;\n\t\tcopy.joinAs = controller->selected();\n\t\tdone(std::move(copy));\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\n[[nodiscard]] TextWithEntities CreateOrJoinConfirmation(\n\t\tnot_null<PeerData*> peer,\n\t\tChooseJoinAsProcess::Context context,\n\t\tbool joinAsAlreadyUsed) {\n\tconst auto existing = peer->groupCall();\n\tif (!existing) {\n\t\treturn { peer->isBroadcast()\n\t\t\t? tr::lng_group_call_create_sure_channel(tr::now)\n\t\t\t: tr::lng_group_call_create_sure(tr::now) };\n\t}\n\tconst auto channel = peer->asChannel();\n\tconst auto anonymouseAdmin = channel\n\t\t&& ((channel->isMegagroup() && channel->amAnonymous())\n\t\t\t|| (channel->isBroadcast()\n\t\t\t\t&& (channel->amCreator() || channel->hasAdminRights())));\n\tif (anonymouseAdmin && !joinAsAlreadyUsed) {\n\t\treturn { tr::lng_group_call_join_sure_personal(tr::now) };\n\t} else if (context != ChooseJoinAsProcess::Context::JoinWithConfirm) {\n\t\treturn {};\n\t}\n\tconst auto name = !existing->title().isEmpty()\n\t\t? existing->title()\n\t\t: peer->name();\n\treturn (peer->isBroadcast()\n\t\t? tr::lng_group_call_join_confirm_channel\n\t\t: tr::lng_group_call_join_confirm)(\n\t\ttr::now,\n\t\tlt_chat,\n\t\ttr::bold(name),\n\t\ttr::marked);\n}\n\n} // namespace\n\nChooseJoinAsProcess::~ChooseJoinAsProcess() {\n\tif (_request) {\n\t\t_request->peer->session().api().request(_request->id).cancel();\n\t}\n}\n\nvoid ChooseJoinAsProcess::start(\n\t\tnot_null<PeerData*> peer,\n\t\tContext context,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tFn<void(JoinInfo)> done,\n\t\tPeerData *changingJoinAsFrom) {\n\tExpects(done != nullptr);\n\n\tconst auto isScheduled = (context == Context::CreateScheduled);\n\n\tconst auto session = &peer->session();\n\tif (_request) {\n\t\tif (_request->peer == peer && !isScheduled) {\n\t\t\t_request->context = context;\n\t\t\t_request->show = std::move(show);\n\t\t\t_request->done = std::move(done);\n\t\t\t_request->changingJoinAsFrom = changingJoinAsFrom;\n\t\t\treturn;\n\t\t}\n\t\tsession->api().request(_request->id).cancel();\n\t\t_request = nullptr;\n\t}\n\n\tconst auto createRequest = [=, done = std::move(done)] {\n\t\t_request = std::make_unique<ChannelsListRequest>(ChannelsListRequest{\n\t\t\t.peer = peer,\n\t\t\t.show = show,\n\t\t\t.done = std::move(done),\n\t\t\t.context = context,\n\t\t\t.changingJoinAsFrom = changingJoinAsFrom });\n\t};\n\n\tif (isScheduled) {\n\t\tauto box = Box(\n\t\t\tScheduleGroupCallBox,\n\t\t\tJoinInfo{ .peer = peer, .joinAs = peer },\n\t\t\t[=, createRequest = std::move(createRequest)](JoinInfo info) {\n\t\t\t\tcreateRequest();\n\t\t\t\tfinish(info);\n\t\t\t});\n\t\tshow->showBox(std::move(box));\n\t\treturn;\n\t}\n\n\tcreateRequest();\n\tsession->account().sessionChanges(\n\t) | rpl::on_next([=] {\n\t\t_request = nullptr;\n\t}, _request->lifetime);\n\n\trequestList();\n}\n\nvoid ChooseJoinAsProcess::requestList() {\n\tconst auto session = &_request->peer->session();\n\t_request->id = session->api().request(MTPphone_GetGroupCallJoinAs(\n\t\t_request->peer->input()\n\t)).done([=](const MTPphone_JoinAsPeers &result) {\n\t\tauto list = result.match([&](const MTPDphone_joinAsPeers &data) {\n\t\t\tsession->data().processUsers(data.vusers());\n\t\t\tsession->data().processChats(data.vchats());\n\t\t\tconst auto &peers = data.vpeers().v;\n\t\t\tauto list = std::vector<not_null<PeerData*>>();\n\t\t\tlist.reserve(peers.size());\n\t\t\tfor (const auto &peer : peers) {\n\t\t\t\tconst auto peerId = peerFromMTP(peer);\n\t\t\t\tif (const auto peer = session->data().peerLoaded(peerId)) {\n\t\t\t\t\tif (!ranges::contains(list, not_null{ peer })) {\n\t\t\t\t\t\tlist.push_back(peer);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn list;\n\t\t});\n\t\tprocessList(std::move(list));\n\t}).fail([=] {\n\t\tfinish({\n\t\t\t.peer = _request->peer,\n\t\t\t.joinAs = _request->peer->session().user(),\n\t\t});\n\t}).send();\n}\n\nvoid ChooseJoinAsProcess::finish(JoinInfo info) {\n\tconst auto done = std::move(_request->done);\n\tconst auto box = _request->box;\n\t_request = nullptr;\n\tdone(std::move(info));\n\tif (const auto strong = box.get()) {\n\t\tstrong->closeBox();\n\t}\n}\n\nvoid ChooseJoinAsProcess::processList(\n\t\tstd::vector<not_null<PeerData*>> &&list) {\n\tconst auto session = &_request->peer->session();\n\tconst auto peer = _request->peer;\n\tconst auto self = peer->session().user();\n\tauto info = JoinInfo{ .peer = peer, .joinAs = self };\n\tconst auto selectedId = peer->groupCallDefaultJoinAs();\n\tif (list.empty()) {\n\t\t_request->show->showToast(Lang::Hard::ServerError());\n\t\treturn;\n\t}\n\tinfo.joinAs = [&]() -> not_null<PeerData*> {\n\t\tconst auto loaded = selectedId\n\t\t\t? session->data().peerLoaded(selectedId)\n\t\t\t: nullptr;\n\t\tconst auto changingJoinAsFrom = _request->changingJoinAsFrom;\n\t\treturn (changingJoinAsFrom\n\t\t\t&& ranges::contains(list, not_null{ changingJoinAsFrom }))\n\t\t\t? not_null(changingJoinAsFrom)\n\t\t\t: (loaded && ranges::contains(list, not_null{ loaded }))\n\t\t\t? not_null(loaded)\n\t\t\t: ranges::contains(list, self)\n\t\t\t? self\n\t\t\t: list.front();\n\t}();\n\tinfo.possibleJoinAs = std::move(list);\n\n\tconst auto onlyByMe = (info.possibleJoinAs.size() == 1)\n\t\t&& (info.possibleJoinAs.front() == self);\n\n\t// We already joined this voice chat, just rejoin with the same.\n\tconst auto byAlreadyUsed = selectedId\n\t\t&& (info.joinAs->id == selectedId)\n\t\t&& (peer->groupCall() != nullptr);\n\n\tif (!_request->changingJoinAsFrom && (onlyByMe || byAlreadyUsed)) {\n\t\tauto confirmation = CreateOrJoinConfirmation(\n\t\t\tpeer,\n\t\t\t_request->context,\n\t\t\tbyAlreadyUsed);\n\t\tif (confirmation.text.isEmpty()) {\n\t\t\tfinish(info);\n\t\t\treturn;\n\t\t}\n\t\tconst auto livestream = peer->isBroadcast();\n\t\tconst auto creating = !peer->groupCall();\n\t\tif (creating) {\n\t\t\tconfirmation\n\t\t\t\t.append(\"\\n\\n\")\n\t\t\t\t.append(tr::lng_group_call_or_schedule(\n\t\t\t\ttr::now,\n\t\t\t\tlt_link,\n\t\t\t\ttr::link((livestream\n\t\t\t\t\t? tr::lng_group_call_schedule_channel\n\t\t\t\t\t: tr::lng_group_call_schedule)(tr::now)),\n\t\t\t\ttr::marked));\n\t\t}\n\t\tconst auto guard = base::make_weak(&_request->guard);\n\t\tconst auto safeFinish = crl::guard(guard, [=] { finish(info); });\n\t\tconst auto filter = [=](const auto &...) {\n\t\t\tif (guard) {\n\t\t\t\t_request->show->showBox(Box(\n\t\t\t\t\tScheduleGroupCallBox,\n\t\t\t\t\tinfo,\n\t\t\t\t\tcrl::guard(guard, [=](auto info) { finish(info); })));\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\t\tauto box = Ui::MakeConfirmBox({\n\t\t\t.text = confirmation,\n\t\t\t.confirmed = crl::guard(guard, [=] { finish(info); }),\n\t\t\t.confirmText = (creating\n\t\t\t\t? tr::lng_create_group_create()\n\t\t\t\t: tr::lng_group_call_join()),\n\t\t\t.labelFilter = filter,\n\t\t});\n\t\tbox->boxClosing(\n\t\t) | rpl::on_next([=] {\n\t\t\t_request = nullptr;\n\t\t}, _request->lifetime);\n\n\t\t_request->box = box.data();\n\t\t_request->show->showBox(std::move(box));\n\t\treturn;\n\t}\n\tauto box = Box(\n\t\tChooseJoinAsBox,\n\t\t_request->context,\n\t\tstd::move(info),\n\t\tcrl::guard(&_request->guard, [=](auto info) { finish(info); }));\n\tbox->boxClosing(\n\t) | rpl::on_next([=] {\n\t\t_request = nullptr;\n\t}, _request->lifetime);\n\n\t_request->box = box.data();\n\t_request->show->showBox(std::move(box));\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_choose_join_as.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"base/object_ptr.h\"\n\nclass PeerData;\n\nnamespace Ui {\nclass Show;\nclass BoxContent;\n} // namespace Ui\n\nnamespace Calls::Group {\n\nstruct JoinInfo;\n\nclass ChooseJoinAsProcess final {\npublic:\n\tChooseJoinAsProcess() = default;\n\t~ChooseJoinAsProcess();\n\n\tenum class Context {\n\t\tCreate,\n\t\tCreateScheduled,\n\t\tJoin,\n\t\tJoinWithConfirm,\n\t\tSwitch,\n\t};\n\n\tvoid start(\n\t\tnot_null<PeerData*> peer,\n\t\tContext context,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tFn<void(JoinInfo)> done,\n\t\tPeerData *changingJoinAsFrom = nullptr);\n\nprivate:\n\tvoid requestList();\n\tvoid processList(std::vector<not_null<PeerData*>> &&list);\n\tvoid finish(JoinInfo info);\n\n\tstruct ChannelsListRequest {\n\t\tnot_null<PeerData*> peer;\n\t\tstd::shared_ptr<Ui::Show> show;\n\t\tFn<void(JoinInfo)> done;\n\t\tbase::has_weak_ptr guard;\n\t\tbase::weak_qptr<Ui::BoxContent> box;\n\t\trpl::lifetime lifetime;\n\t\tContext context = Context();\n\t\tmtpRequestId id = 0;\n\t\tPeerData *changingJoinAsFrom = nullptr;\n\t};\n\tstd::unique_ptr<ChannelsListRequest> _request;\n\n};\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_cover_item.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_cover_item.h\"\n\n#include \"boxes/peers/prepare_short_info_box.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_info.h\"\n\nnamespace Calls {\n\nCoverItem::CoverItem(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::Menu &stMenu,\n\tconst style::ShortInfoCover &st,\n\trpl::producer<QString> name,\n\trpl::producer<QString> status,\n\tPreparedShortInfoUserpic userpic)\n: Ui::Menu::ItemBase(parent, stMenu)\n, _cover(\n\tthis,\n\tst,\n\tstd::move(name),\n\tstd::move(status),\n\tstd::move(userpic.value),\n\t[] { return false; })\n, _dummyAction(new QAction(parent))\n, _st(st) {\n\tsetPointerCursor(false);\n\n\tfitToMenuWidth();\n\tenableMouseSelecting();\n\tenableMouseSelecting(_cover.widget());\n\n\t_cover.widget()->move(0, 0);\n\t_cover.moveRequests(\n\t) | rpl::on_next(userpic.move, lifetime());\n}\n\nnot_null<QAction*> CoverItem::action() const {\n\treturn _dummyAction;\n}\n\nbool CoverItem::isEnabled() const {\n\treturn false;\n}\n\nint CoverItem::contentHeight() const {\n\treturn _st.size + st::groupCallMenu.separator.padding.bottom();\n}\n\nAboutItem::AboutItem(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::Menu &st,\n\tTextWithEntities &&about)\n: Ui::Menu::ItemBase(parent, st)\n, _st(st)\n, _text(base::make_unique_q<Ui::FlatLabel>(\n\tthis,\n\trpl::single(std::move(about)),\n\tst::groupCallMenuAbout))\n, _dummyAction(new QAction(parent)) {\n\tsetPointerCursor(false);\n\n\tfitToMenuWidth();\n\tenableMouseSelecting();\n\tenableMouseSelecting(_text.get());\n\n\t_text->setSelectable(true);\n\t_text->resizeToWidth(st::groupCallMenuAbout.minWidth);\n\t_text->moveToLeft(st.itemPadding.left(), st.itemPadding.top());\n}\n\nnot_null<QAction*> AboutItem::action() const {\n\treturn _dummyAction;\n}\n\nbool AboutItem::isEnabled() const {\n\treturn false;\n}\n\nint AboutItem::contentHeight() const {\n\treturn _st.itemPadding.top()\n\t\t+ _text->height()\n\t\t+ _st.itemPadding.bottom();\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_cover_item.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/widgets/menu/menu_item_base.h\"\n#include \"boxes/peers/peer_short_info_box.h\"\n\nstruct PreparedShortInfoUserpic;\n\nnamespace style {\nstruct ShortInfoCover;\n} // namespace style\n\nnamespace Calls {\n\nnamespace Group {\nstruct MuteRequest;\nstruct VolumeRequest;\nstruct ParticipantState;\n} // namespace Group\n\nclass CoverItem final : public Ui::Menu::ItemBase {\npublic:\n\tCoverItem(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::Menu &stMenu,\n\t\tconst style::ShortInfoCover &st,\n\t\trpl::producer<QString> name,\n\t\trpl::producer<QString> status,\n\t\tPreparedShortInfoUserpic userpic);\n\n\tnot_null<QAction*> action() const override;\n\tbool isEnabled() const override;\n\nprivate:\n\tint contentHeight() const override;\n\n\tconst PeerShortInfoCover _cover;\n\tconst not_null<QAction*> _dummyAction;\n\tconst style::ShortInfoCover &_st;\n\n};\n\nclass AboutItem final : public Ui::Menu::ItemBase {\npublic:\n\tAboutItem(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::Menu &st,\n\t\tTextWithEntities &&about);\n\n\tnot_null<QAction*> action() const override;\n\tbool isEnabled() const override;\n\nprivate:\n\tint contentHeight() const override;\n\n\tconst style::Menu &_st;\n\tconst base::unique_qptr<Ui::FlatLabel> _text;\n\tconst not_null<QAction*> _dummyAction;\n\n};\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_call.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_call.h\"\n\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/group/calls_group_messages.h\"\n#include \"calls/calls_instance.h\"\n#include \"main/session/session_show.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"api/api_send_progress.h\"\n#include \"api/api_updates.h\"\n#include \"apiwrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"lang/lang_hardcoded.h\"\n#include \"boxes/peers/edit_participants_box.h\" // SubscribeToMigration.\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/ui_utility.h\"\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_session.h\"\n#include \"base/global_shortcuts.h\"\n#include \"base/random.h\"\n#include \"tde2e/tde2e_api.h\"\n#include \"tde2e/tde2e_integration.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"webrtc/webrtc_create_adm.h\"\n#include \"webrtc/webrtc_environment.h\"\n\n#include <tgcalls/group/GroupInstanceCustomImpl.h>\n#include <tgcalls/VideoCaptureInterface.h>\n#include <tgcalls/StaticThreads.h>\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonObject>\n#include <QtCore/QJsonArray>\n\nnamespace Calls {\nnamespace {\n\nconstexpr auto kMaxInvitePerSlice = 10;\nconstexpr auto kCheckLastSpokeInterval = crl::time(1000);\nconstexpr auto kCheckJoinedTimeout = 4 * crl::time(1000);\nconstexpr auto kUpdateSendActionEach = crl::time(500);\nconstexpr auto kPlayConnectingEach = crl::time(1056) + 2 * crl::time(1000);\nconstexpr auto kFixManualLargeVideoDuration = 5 * crl::time(1000);\nconstexpr auto kFixSpeakingLargeVideoDuration = 3 * crl::time(1000);\nconstexpr auto kFullAsMediumsCount = 4; // 1 Full is like 4 Mediums.\nconstexpr auto kMaxMediumQualities = 16; // 4 Fulls or 16 Mediums.\nconstexpr auto kShortPollChainBlocksPerRequest = 50;\n\n[[nodiscard]] const Data::GroupCallParticipant *LookupParticipant(\n\t\tnot_null<GroupCall*> call,\n\t\tnot_null<PeerData*> participantPeer) {\n\tconst auto real = call->lookupReal();\n\treturn real ? real->participantByPeer(participantPeer) : nullptr;\n}\n\n[[nodiscard]] double TimestampFromMsgId(mtpMsgId msgId) {\n\treturn msgId / double(1ULL << 32);\n}\n\n[[nodiscard]] int64 TimestampInMsFromMsgId(mtpMsgId msgId) {\n\t// return (msgId * 1000) / (1ULL << 32); // Almost... But this overflows.\n\treturn ((msgId / (1ULL << 10)) * 1000) / (1ULL << 22);\n}\n\n[[nodiscard]] uint64 FindLocalRaisedHandRating(\n\t\tconst std::vector<Data::GroupCallParticipant> &list) {\n\tconst auto i = ranges::max_element(\n\t\tlist,\n\t\tranges::less(),\n\t\t&Data::GroupCallParticipant::raisedHandRating);\n\treturn (i == end(list)) ? 1 : (i->raisedHandRating + 1);\n}\n\nstruct JoinVideoEndpoint {\n\tstd::string id;\n};\n\nstruct JoinBroadcastStream {\n\tbool rtmp = false;\n\tGroup::RtmpInfo rtmpInfo;\n};\n\nusing JoinClientFields = std::variant<\n\tv::null_t,\n\tJoinVideoEndpoint,\n\tJoinBroadcastStream>;\n\n[[nodiscard]] JoinClientFields ParseJoinResponse(const QByteArray &json) {\n\tauto error = QJsonParseError{ 0, QJsonParseError::NoError };\n\tconst auto document = QJsonDocument::fromJson(json, &error);\n\tif (error.error != QJsonParseError::NoError) {\n\t\tLOG((\"API Error: \"\n\t\t\t\"Failed to parse join response params, error: %1.\"\n\t\t\t).arg(error.errorString()));\n\t\treturn {};\n\t} else if (!document.isObject()) {\n\t\tLOG((\"API Error: \"\n\t\t\t\"Not an object received in join response params.\"));\n\t\treturn {};\n\t}\n\tif (document.object().value(\"stream\").toBool()) {\n\t\treturn JoinBroadcastStream{\n\t\t\t.rtmp = document.object().value(\"rtmp\").toBool(),\n\t\t\t.rtmpInfo = {\n\t\t\t\t.url = document.object().value(\"rtmp_stream_url\").toString(),\n\t\t\t\t.key = document.object().value(\"rtmp_stream_key\").toString(),\n\t\t\t},\n\t\t};\n\t}\n\tconst auto video = document.object().value(\"video\").toObject();\n\treturn JoinVideoEndpoint{\n\t\tvideo.value(\"endpoint\").toString().toStdString(),\n\t};\n}\n\n[[nodiscard]] const std::string &EmptyString() {\n\tstatic const auto result = std::string();\n\treturn result;\n}\n\n} // namespace\n\nclass GroupCall::LoadPartTask final : public tgcalls::BroadcastPartTask {\npublic:\n\tusing Quality = tgcalls::VideoChannelDescription::Quality;\n\tLoadPartTask(\n\t\tbase::weak_ptr<GroupCall> call,\n\t\tint64 time,\n\t\tint64 period,\n\t\tFn<void(tgcalls::BroadcastPart&&)> done);\n\tLoadPartTask(\n\t\tbase::weak_ptr<GroupCall> call,\n\t\tint64 time,\n\t\tint64 period,\n\t\tint32 videoChannel,\n\t\tQuality videoQuality,\n\t\tFn<void(tgcalls::BroadcastPart&&)> done);\n\n\t[[nodiscard]] int64 time() const {\n\t\treturn _time;\n\t}\n\t[[nodiscard]] int32 scale() const {\n\t\treturn _scale;\n\t}\n\t[[nodiscard]] int32 videoChannel() const {\n\t\treturn _videoChannel;\n\t}\n\t[[nodiscard]] Quality videoQuality() const {\n\t\treturn _videoQuality;\n\t}\n\n\tvoid done(tgcalls::BroadcastPart &&part);\n\tvoid cancel() override;\n\nprivate:\n\tconst base::weak_ptr<GroupCall> _call;\n\tconst int64 _time = 0;\n\tconst int32 _scale = 0;\n\tconst int32 _videoChannel = 0;\n\tconst Quality _videoQuality = {};\n\tFn<void(tgcalls::BroadcastPart &&)> _done;\n\tQMutex _mutex;\n\n};\n\nclass GroupCall::MediaChannelDescriptionsTask final\n\t: public tgcalls::RequestMediaChannelDescriptionTask {\npublic:\n\tMediaChannelDescriptionsTask(\n\t\tbase::weak_ptr<GroupCall> call,\n\t\tconst std::vector<std::uint32_t> &ssrcs,\n\t\tFn<void(std::vector<tgcalls::MediaChannelDescription>&&)> done);\n\n\t[[nodiscard]] base::flat_set<uint32> ssrcs() const;\n\n\t[[nodiscard]] bool finishWithAdding(\n\t\tuint32 ssrc,\n\t\tstd::optional<tgcalls::MediaChannelDescription> description,\n\t\tbool screen = false);\n\n\tvoid cancel() override;\n\nprivate:\n\tconst base::weak_ptr<GroupCall> _call;\n\tbase::flat_set<uint32> _ssrcs;\n\tbase::flat_set<uint32> _cameraAdded;\n\tbase::flat_set<uint32> _screenAdded;\n\tstd::vector<tgcalls::MediaChannelDescription> _result;\n\tFn<void(std::vector<tgcalls::MediaChannelDescription>&&)> _done;\n\tQMutex _mutex;\n\n};\n\nclass GroupCall::RequestCurrentTimeTask final\n\t: public tgcalls::BroadcastPartTask {\npublic:\n\tRequestCurrentTimeTask(\n\t\tbase::weak_ptr<GroupCall> call,\n\t\tFn<void(int64)> done);\n\n\tvoid done(int64 value);\n\tvoid cancel() override;\n\nprivate:\n\tconst base::weak_ptr<GroupCall> _call;\n\tFn<void(int64)> _done;\n\tQMutex _mutex;\n\n};\n\nstruct GroupCall::SinkPointer {\n\tstd::weak_ptr<Webrtc::SinkInterface> data;\n};\n\nstruct GroupCall::VideoTrack {\n\tVideoTrack(bool paused, bool requireARGB32, not_null<PeerData*> peer);\n\n\tWebrtc::VideoTrack track;\n\trpl::variable<QSize> trackSize;\n\tnot_null<PeerData*> peer;\n\trpl::lifetime lifetime;\n\tGroup::VideoQuality quality = Group::VideoQuality();\n\tbool shown = false;\n};\n\nGroupCall::VideoTrack::VideoTrack(\n\tbool paused,\n\tbool requireARGB32,\n\tnot_null<PeerData*> peer)\n: track((paused\n\t? Webrtc::VideoState::Paused\n\t: Webrtc::VideoState::Active),\n\trequireARGB32)\n, peer(peer) {\n}\n\n[[nodiscard]] bool IsGroupCallAdmin(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<PeerData*> participantPeer) {\n\tconst auto user = participantPeer->asUser();\n\tif (!user) {\n\t\treturn (peer == participantPeer);\n\t}\n\tif (const auto chat = peer->asChat()) {\n\t\treturn chat->admins.contains(user)\n\t\t\t|| (chat->creator == peerToUser(user->id));\n\t} else if (const auto group = peer->asChannel()) {\n\t\tif (const auto mgInfo = group->mgInfo.get()) {\n\t\t\tif (mgInfo->creator == user) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst auto i = mgInfo->lastAdmins.find(user);\n\t\t\tif (i == mgInfo->lastAdmins.end()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn (i->second.rights.flags & ChatAdminRight::ManageCall);\n\t\t}\n\t}\n\treturn false;\n}\n\nbool VideoEndpoint::rtmp() const noexcept {\n\treturn (id == Data::RtmpEndpointId());\n}\n\nstruct VideoParams {\n\tstd::string endpointId;\n\tstd::vector<tgcalls::MediaSsrcGroup> ssrcGroups;\n\tuint32 additionalSsrc = 0;\n\tbool paused = false;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn !additionalSsrc && (endpointId.empty() || ssrcGroups.empty());\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n};\n\nstruct ParticipantVideoParams {\n\tVideoParams camera;\n\tVideoParams screen;\n};\n\n[[nodiscard]] bool VideoParamsAreEqual(\n\t\tconst VideoParams &was,\n\t\tconst tl::conditional<MTPGroupCallParticipantVideo> &now) {\n\tif (!now) {\n\t\treturn !was;\n\t}\n\treturn now->match([&](const MTPDgroupCallParticipantVideo &data) {\n\t\tif (data.is_paused() != was.paused\n\t\t\t|| data.vaudio_source().value_or_empty() != was.additionalSsrc) {\n\t\t\treturn false;\n\t\t}\n\t\tif (gsl::make_span(data.vendpoint().v)\n\t\t\t!= gsl::make_span(was.endpointId)) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto &list = data.vsource_groups().v;\n\t\tif (list.size() != was.ssrcGroups.size()) {\n\t\t\treturn false;\n\t\t}\n\t\tauto index = 0;\n\t\tfor (const auto &group : list) {\n\t\t\tconst auto equal = group.match([&](\n\t\t\t\t\tconst MTPDgroupCallParticipantVideoSourceGroup &data) {\n\t\t\t\tconst auto &group = was.ssrcGroups[index++];\n\t\t\t\tif (gsl::make_span(data.vsemantics().v)\n\t\t\t\t\t!= gsl::make_span(group.semantics)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst auto list = data.vsources().v;\n\t\t\t\tif (list.size() != group.ssrcs.size()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tauto i = 0;\n\t\t\t\tfor (const auto &ssrc : list) {\n\t\t\t\t\tif (ssrc.v != group.ssrcs[i++]) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\t\t\tif (!equal) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t});\n}\n\n[[nodiscard]] VideoParams ParseVideoParams(\n\t\tconst tl::conditional<MTPGroupCallParticipantVideo> &params) {\n\tif (!params) {\n\t\treturn VideoParams();\n\t}\n\tauto result = VideoParams();\n\tparams->match([&](const MTPDgroupCallParticipantVideo &data) {\n\t\tresult.paused = data.is_paused();\n\t\tresult.endpointId = data.vendpoint().v.toStdString();\n\t\tresult.additionalSsrc = data.vaudio_source().value_or_empty();\n\t\tconst auto &list = data.vsource_groups().v;\n\t\tresult.ssrcGroups.reserve(list.size());\n\t\tfor (const auto &group : list) {\n\t\t\tgroup.match([&](\n\t\t\t\t\tconst MTPDgroupCallParticipantVideoSourceGroup &data) {\n\t\t\t\tconst auto &list = data.vsources().v;\n\t\t\t\tauto ssrcs = std::vector<uint32_t>();\n\t\t\t\tssrcs.reserve(list.size());\n\t\t\t\tfor (const auto &ssrc : list) {\n\t\t\t\t\tssrcs.push_back(ssrc.v);\n\t\t\t\t}\n\t\t\t\tresult.ssrcGroups.push_back({\n\t\t\t\t\t.semantics = data.vsemantics().v.toStdString(),\n\t\t\t\t\t.ssrcs = std::move(ssrcs),\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t});\n\treturn result;\n}\n\nconst std::string &GetCameraEndpoint(\n\t\tconst std::shared_ptr<ParticipantVideoParams> &params) {\n\treturn params ? params->camera.endpointId : EmptyString();\n}\n\nconst std::string &GetScreenEndpoint(\n\t\tconst std::shared_ptr<ParticipantVideoParams> &params) {\n\treturn params ? params->screen.endpointId : EmptyString();\n}\n\nbool IsCameraPaused(const std::shared_ptr<ParticipantVideoParams> &params) {\n\treturn params && params->camera.paused;\n}\n\nbool IsScreenPaused(const std::shared_ptr<ParticipantVideoParams> &params) {\n\treturn params && params->screen.paused;\n}\n\nuint32 GetAdditionalAudioSsrc(\n\t\tconst std::shared_ptr<ParticipantVideoParams> &params) {\n\treturn params ? params->screen.additionalSsrc : 0;\n}\n\nstd::shared_ptr<ParticipantVideoParams> ParseVideoParams(\n\t\tconst tl::conditional<MTPGroupCallParticipantVideo> &camera,\n\t\tconst tl::conditional<MTPGroupCallParticipantVideo> &screen,\n\t\tconst std::shared_ptr<ParticipantVideoParams> &existing) {\n\tusing namespace tgcalls;\n\n\tif (!camera && !screen) {\n\t\treturn nullptr;\n\t}\n\tif (existing\n\t\t&& VideoParamsAreEqual(existing->camera, camera)\n\t\t&& VideoParamsAreEqual(existing->screen, screen)) {\n\t\treturn existing;\n\t}\n\t// We don't reuse existing pointer, that way we can compare pointers\n\t// to see if anything was changed in video params.\n\tconst auto data = /*existing\n\t\t? existing\n\t\t: */std::make_shared<ParticipantVideoParams>();\n\tdata->camera = ParseVideoParams(camera);\n\tdata->screen = ParseVideoParams(screen);\n\treturn data;\n}\n\nGroupCall::LoadPartTask::LoadPartTask(\n\tbase::weak_ptr<GroupCall> call,\n\tint64 time,\n\tint64 period,\n\tFn<void(tgcalls::BroadcastPart&&)> done)\n: LoadPartTask(std::move(call), time, period, 0, {}, std::move(done)) {\n}\n\nGroupCall::LoadPartTask::LoadPartTask(\n\tbase::weak_ptr<GroupCall> call,\n\tint64 time,\n\tint64 period,\n\tint32 videoChannel,\n\ttgcalls::VideoChannelDescription::Quality videoQuality,\n\tFn<void(tgcalls::BroadcastPart&&)> done)\n: _call(std::move(call))\n, _time(time ? time : (base::unixtime::now() * int64(1000)))\n, _scale([&] {\n\tswitch (period) {\n\tcase 1000: return 0;\n\tcase 500: return 1;\n\tcase 250: return 2;\n\tcase 125: return 3;\n\t}\n\tUnexpected(\"Period in LoadPartTask.\");\n}())\n, _videoChannel(videoChannel)\n, _videoQuality(videoQuality)\n, _done(std::move(done)) {\n}\n\nvoid GroupCall::LoadPartTask::done(tgcalls::BroadcastPart &&part) {\n\tQMutexLocker lock(&_mutex);\n\tif (_done) {\n\t\tbase::take(_done)(std::move(part));\n\t}\n}\n\nvoid GroupCall::LoadPartTask::cancel() {\n\tQMutexLocker lock(&_mutex);\n\tif (!_done) {\n\t\treturn;\n\t}\n\t_done = nullptr;\n\tlock.unlock();\n\n\tif (_call) {\n\t\tconst auto that = this;\n\t\tcrl::on_main(_call, [weak = _call, that] {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->broadcastPartCancel(that);\n\t\t\t}\n\t\t});\n\t}\n}\n\nGroupCall::MediaChannelDescriptionsTask::MediaChannelDescriptionsTask(\n\tbase::weak_ptr<GroupCall> call,\n\tconst std::vector<std::uint32_t> &ssrcs,\n\tFn<void(std::vector<tgcalls::MediaChannelDescription>&&)> done)\n: _call(std::move(call))\n, _ssrcs(ssrcs.begin(), ssrcs.end())\n, _done(std::move(done)) {\n}\n\nauto GroupCall::MediaChannelDescriptionsTask::ssrcs() const\n-> base::flat_set<uint32> {\n\treturn _ssrcs;\n}\n\nbool GroupCall::MediaChannelDescriptionsTask::finishWithAdding(\n\t\tuint32 ssrc,\n\t\tstd::optional<tgcalls::MediaChannelDescription> description,\n\t\tbool screen) {\n\tExpects(_ssrcs.contains(ssrc));\n\n\tusing Type = tgcalls::MediaChannelDescription::Type;\n\t_ssrcs.remove(ssrc);\n\tif (!description) {\n\t} else if (description->type == Type::Audio\n\t\t|| (!screen && _cameraAdded.emplace(description->audioSsrc).second)\n\t\t|| (screen && _screenAdded.emplace(description->audioSsrc).second)) {\n\t\t_result.push_back(std::move(*description));\n\t}\n\n\tif (!_ssrcs.empty()) {\n\t\treturn false;\n\t}\n\tQMutexLocker lock(&_mutex);\n\tif (_done) {\n\t\tbase::take(_done)(std::move(_result));\n\t}\n\treturn true;\n}\n\nvoid GroupCall::MediaChannelDescriptionsTask::cancel() {\n\tQMutexLocker lock(&_mutex);\n\tif (!_done) {\n\t\treturn;\n\t}\n\t_done = nullptr;\n\tlock.unlock();\n\n\tif (_call) {\n\t\tconst auto that = this;\n\t\tcrl::on_main(_call, [weak = _call, that] {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->mediaChannelDescriptionsCancel(that);\n\t\t\t}\n\t\t});\n\t}\n}\n\nGroupCall::RequestCurrentTimeTask::RequestCurrentTimeTask(\n\tbase::weak_ptr<GroupCall> call,\n\tFn<void(int64)> done)\n: _call(call)\n, _done(std::move(done)) {\n}\n\nvoid GroupCall::RequestCurrentTimeTask::done(int64 value) {\n\tQMutexLocker lock(&_mutex);\n\tif (_done) {\n\t\tbase::take(_done)(value);\n\t}\n}\n\nvoid GroupCall::RequestCurrentTimeTask::cancel() {\n\tQMutexLocker lock(&_mutex);\n\t_done = nullptr;\n}\n\nnot_null<PeerData*> GroupCall::TrackPeer(\n\t\tconst std::unique_ptr<VideoTrack> &track) {\n\treturn track->peer;\n}\n\nnot_null<Webrtc::VideoTrack*> GroupCall::TrackPointer(\n\t\tconst std::unique_ptr<VideoTrack> &track) {\n\treturn &track->track;\n}\n\nrpl::producer<QSize> GroupCall::TrackSizeValue(\n\t\tconst std::unique_ptr<VideoTrack> &track) {\n\treturn track->trackSize.value();\n}\n\nGroupCall::GroupCall(\n\tnot_null<Delegate*> delegate,\n\tGroup::JoinInfo info,\n\tconst MTPInputGroupCall &inputCall)\n: GroupCall(delegate, info, {}, inputCall) {\n}\n\nGroupCall::GroupCall(\n\tnot_null<Delegate*> delegate,\n\tStartConferenceInfo info)\n: GroupCall(delegate, Group::JoinInfo{\n\t.peer = info.call ? info.call->peer() : info.show->session().user(),\n\t.joinAs = info.show ? info.show->session().user() : info.call->peer(),\n}, info, info.call\n\t? info.call->input()\n\t: MTP_inputGroupCall(MTP_long(0), MTP_long(0))) {\n}\n\nGroupCall::GroupCall(\n\tnot_null<Delegate*> delegate,\n\tGroup::JoinInfo join,\n\tStartConferenceInfo startInfo,\n\tconst MTPInputGroupCall &inputCall)\n: _delegate(delegate)\n, _sharedCall(std::move(startInfo.call))\n, _peer(join.peer)\n, _history(_peer->owner().history(_peer))\n, _api(&_peer->session().mtp())\n, _messages(std::make_unique<Group::Messages>(this, &_api))\n, _joinAs(join.joinAs)\n, _possibleJoinAs(std::move(join.possibleJoinAs))\n, _joinHash(join.joinHash)\n, _conferenceLinkSlug(startInfo.linkSlug)\n, _conferenceJoinMessageId(startInfo.joinMessageId)\n, _rtmpUrl(join.rtmpInfo.url)\n, _rtmpKey(join.rtmpInfo.key)\n, _canManage(Data::CanManageGroupCallValue(_peer))\n, _scheduleDate(join.scheduleDate)\n, _lastSpokeCheckTimer([=] { checkLastSpoke(); })\n, _checkJoinedTimer([=] { checkJoined(); })\n, _playbackDeviceId(\n\t&Core::App().mediaDevices(),\n\tWebrtc::DeviceType::Playback,\n\tWebrtc::DeviceIdValueWithFallback(\n\t\tCore::App().settings().callPlaybackDeviceIdValue(),\n\t\tCore::App().settings().playbackDeviceIdValue()))\n, _captureDeviceId(\n\t&Core::App().mediaDevices(),\n\tWebrtc::DeviceType::Capture,\n\tWebrtc::DeviceIdValueWithFallback(\n\t\tCore::App().settings().callCaptureDeviceIdValue(),\n\t\tCore::App().settings().captureDeviceIdValue()))\n, _cameraDeviceId(\n\t&Core::App().mediaDevices(),\n\tWebrtc::DeviceType::Camera,\n\tWebrtc::DeviceIdOrDefault(Core::App().settings().cameraDeviceIdValue()))\n, _pushToTalkCancelTimer([=] { pushToTalkCancel(); })\n, _connectingSoundTimer([=] { playConnectingSoundOnce(); })\n, _listenersHidden(join.rtmp)\n, _rtmp(join.rtmp)\n, _singleSourceVolume(Group::kDefaultVolume) {\n\tapplyInputCall(inputCall);\n\n\t_muted.value(\n\t) | rpl::combine_previous(\n\t) | rpl::on_next([=](MuteState previous, MuteState state) {\n\t\tif (_instance) {\n\t\t\tupdateInstanceMuteState();\n\t\t}\n\t\tif (_joinState.ssrc\n\t\t\t&& (!_initialMuteStateSent || state == MuteState::Active)) {\n\t\t\t_initialMuteStateSent = true;\n\t\t\tmaybeSendMutedUpdate(previous);\n\t\t}\n\t}, _lifetime);\n\n\t_instanceState.value(\n\t) | rpl::filter([=] {\n\t\treturn _hadJoinedState;\n\t}) | rpl::on_next([=](InstanceState state) {\n\t\tif (state == InstanceState::Disconnected) {\n\t\t\tplayConnectingSound();\n\t\t} else {\n\t\t\tstopConnectingSound();\n\t\t}\n\t}, _lifetime);\n\n\tcheckGlobalShortcutAvailability();\n\n\tif (const auto real = lookupReal()) {\n\t\tsubscribeToReal(real);\n\t\tif (!canManage() && real->joinMuted()) {\n\t\t\t_muted = MuteState::ForceMuted;\n\t\t}\n\t} else if (!startInfo.migrating && !startInfo.show) {\n\t\t_peer->session().changes().peerFlagsValue(\n\t\t\t_peer,\n\t\t\tData::PeerUpdate::Flag::GroupCall\n\t\t) | rpl::map([=] {\n\t\t\treturn lookupReal();\n\t\t}) | rpl::filter([](Data::GroupCall *real) {\n\t\t\treturn real != nullptr;\n\t\t}) | rpl::map([](Data::GroupCall *real) {\n\t\t\treturn not_null{ real };\n\t\t}) | rpl::take(\n\t\t\t1\n\t\t) | rpl::on_next([=](not_null<Data::GroupCall*> real) {\n\t\t\tsubscribeToReal(real);\n\t\t\t_realChanges.fire_copy(real);\n\t\t}, _lifetime);\n\t}\n\n\tsetupMediaDevices();\n\tsetupOutgoingVideo();\n\tif (_sharedCall && conference()) {\n\t\tsetupConferenceCall();\n\t\tinitConferenceE2E();\n\t} else if (!_sharedCall && (startInfo.migrating || startInfo.show)) {\n\t\tinitConferenceE2E();\n\t}\n\tif (startInfo.migrating || (startInfo.show && !_sharedCall)) {\n\t\tif (!startInfo.muted) {\n\t\t\tsetMuted(MuteState::Active);\n\t\t}\n\t\t_startConferenceInfo = std::make_shared<StartConferenceInfo>(\n\t\t\tstd::move(startInfo));\n\t}\n\n\tif (_id || (!_sharedCall && _startConferenceInfo)) {\n\t\tinitialJoin();\n\t} else {\n\t\tstart(join.scheduleDate, join.rtmp);\n\t}\n\tif (_scheduleDate) {\n\t\tsaveDefaultJoinAs(joinAs());\n\t}\n}\n\nvoid GroupCall::processConferenceStart(StartConferenceInfo conference) {\n\tif (!conference.videoCapture) {\n\t\treturn;\n\t}\n\tfillActiveVideoEndpoints();\n\tconst auto weak = base::make_weak(this);\n\tif (!conference.videoCaptureScreenId.isEmpty()) {\n\t\t_screenCapture = std::move(conference.videoCapture);\n\t\t_screenDeviceId = conference.videoCaptureScreenId;\n\t\t_screenCapture->setOnFatalError([=] {\n\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\temitShareScreenError(Error::ScreenFailed);\n\t\t\t});\n\t\t});\n\t\t_screenCapture->setOnPause([=](bool paused) {\n\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\tif (isSharingScreen()) {\n\t\t\t\t\t_screenState = paused\n\t\t\t\t\t\t? Webrtc::VideoState::Paused\n\t\t\t\t\t\t: Webrtc::VideoState::Active;\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t\t_screenState = Webrtc::VideoState::Active;\n\t} else {\n\t\t_cameraCapture = std::move(conference.videoCapture);\n\t\t_cameraCapture->setOnFatalError([=] {\n\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\temitShareCameraError(Error::CameraFailed);\n\t\t\t});\n\t\t});\n\t\t_cameraState = Webrtc::VideoState::Active;\n\t}\n}\n\nGroupCall::~GroupCall() {\n\t_e2e = nullptr;\n\tdestroyScreencast();\n\tdestroyController();\n\tif (!_rtmp) {\n\t\tCore::App().mediaDevices().setCaptureMuteTracker(this, false);\n\t}\n\t_messages->undoScheduledPaidOnDestroy();\n}\n\nvoid GroupCall::initConferenceE2E() {\n\tif (!_e2eEncryptDecrypt) {\n\t\t_e2eEncryptDecrypt = std::make_shared<TdE2E::EncryptDecrypt>();\n\t}\n\n\tfor (auto &state : _subchains) {\n\t\t_api.request(base::take(state.requestId)).cancel();\n\t\tstate = SubChainState();\n\t}\n\t_e2e = nullptr;\n\t_pendingOutboundBlock = QByteArray();\n\n\tconst auto tde2eUserId = TdE2E::MakeUserId(_peer->session().user());\n\t_e2e = std::make_unique<TdE2E::Call>(tde2eUserId);\n\n\t_e2e->subchainRequests(\n\t) | rpl::on_next([=](TdE2E::Call::SubchainRequest request) {\n\t\trequestSubchainBlocks(request.subchain, request.height);\n\t}, _e2e->lifetime());\n\n\t_e2e->sendOutboundBlock(\n\t) | rpl::on_next([=](QByteArray &&block) {\n\t\tsendOutboundBlock(std::move(block));\n\t}, _e2e->lifetime());\n\n\t_e2e->failures() | rpl::on_next([=] {\n\t\tLOG((\"TdE2E: Got failure, scheduling rejoin!\"));\n\t\tcrl::on_main(this, [=] { startRejoin(); });\n\t}, _e2e->lifetime());\n\n\t_e2e->registerEncryptDecrypt(_e2eEncryptDecrypt);\n\n\t_emojiHash = _e2e->emojiHashValue();\n}\n\nvoid GroupCall::setupConferenceCall() {\n\tExpects(_sharedCall != nullptr);\n\n\t_sharedCall->staleParticipantIds(\n\t) | rpl::on_next([=](const base::flat_set<UserId> &staleIds) {\n\t\tremoveConferenceParticipants(staleIds, true);\n\t}, _lifetime);\n}\n\nvoid GroupCall::trackParticipantsWithAccess() {\n\tif (!_sharedCall || !_e2e) {\n\t\treturn;\n\t}\n\n\t_e2e->participantsSetValue(\n\t) | rpl::on_next([=](const TdE2E::ParticipantsSet &set) {\n\t\tauto users = base::flat_set<UserId>();\n\t\tusers.reserve(set.list.size());\n\t\tfor (const auto &id : set.list) {\n\t\t\tusers.emplace(UserId(id.v));\n\t\t}\n\t\t_sharedCall->setParticipantsWithAccess(std::move(users));\n\t}, _e2e->lifetime());\n}\n\nvoid GroupCall::removeConferenceParticipants(\n\t\tconst base::flat_set<UserId> userIds,\n\t\tbool removingStale) {\n\tExpects(_e2e != nullptr);\n\tExpects(!userIds.empty());\n\n\tauto inputs = QVector<MTPlong>();\n\tinputs.reserve(userIds.size());\n\tauto ids = base::flat_set<TdE2E::UserId>();\n\tids.reserve(userIds.size());\n\tfor (const auto &id : userIds) {\n\t\tinputs.push_back(MTP_long(peerToUser(id).bare));\n\t\tids.emplace(TdE2E::MakeUserId(id));\n\t}\n\tconst auto block = _e2e->makeRemoveBlock(ids);\n\tif (block.data.isEmpty()) {\n\t\treturn;\n\t}\n\tusing Flag = MTPphone_DeleteConferenceCallParticipants::Flag;\n\t_api.request(MTPphone_DeleteConferenceCallParticipants(\n\t\tMTP_flags(removingStale ? Flag::f_only_left : Flag::f_kick),\n\t\tinputCall(),\n\t\tMTP_vector<MTPlong>(std::move(inputs)),\n\t\tMTP_bytes(block.data)\n\t)).done([=](const MTPUpdates &result) {\n\t\t_peer->session().api().applyUpdates(result);\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto type = error.type();\n\t\tif (type == u\"GROUPCALL_FORBIDDEN\"_q) {\n\t\t\tLOG((\"Call Info: \"\n\t\t\t\t\"Rejoin after error '%1' in delete confcall participants\"\n\t\t\t\t).arg(type));\n\t\t\tstartRejoin();\n\t\t} else {\n\t\t\tLOG((\"Call Error: Could not remove confcall participants: %1\"\n\t\t\t\t).arg(type));\n\t\t}\n\t}).send();\n}\n\nbool GroupCall::isSharingScreen() const {\n\treturn _isSharingScreen.current();\n}\n\nrpl::producer<bool> GroupCall::isSharingScreenValue() const {\n\treturn _isSharingScreen.value();\n}\n\nbool GroupCall::isScreenPaused() const {\n\treturn (_screenState.current() == Webrtc::VideoState::Paused);\n}\n\nconst std::string &GroupCall::screenSharingEndpoint() const {\n\treturn isSharingScreen() ? _screenEndpoint : EmptyString();\n}\n\nbool GroupCall::isSharingCamera() const {\n\treturn _isSharingCamera.current();\n}\n\nrpl::producer<bool> GroupCall::isSharingCameraValue() const {\n\treturn _isSharingCamera.value();\n}\n\nbool GroupCall::isCameraPaused() const {\n\treturn (_cameraState.current() == Webrtc::VideoState::Paused);\n}\n\nconst std::string &GroupCall::cameraSharingEndpoint() const {\n\treturn isSharingCamera() ? _cameraEndpoint : EmptyString();\n}\n\nQString GroupCall::screenSharingDeviceId() const {\n\treturn isSharingScreen() ? _screenDeviceId : QString();\n}\n\nbool GroupCall::screenSharingWithAudio() const {\n\treturn isSharingScreen() && _screenWithAudio;\n}\n\nbool GroupCall::mutedByAdmin() const {\n\tconst auto mute = muted();\n\treturn _rtmp\n\t\t|| (mute == MuteState::ForceMuted)\n\t\t|| (mute == MuteState::RaisedHand);\n}\n\nbool GroupCall::canManage() const {\n\treturn _canManage.current();\n}\n\nrpl::producer<bool> GroupCall::canManageValue() const {\n\treturn _canManage.value();\n}\n\nvoid GroupCall::toggleVideo(bool active) {\n\tif (!_instance || !_id) {\n\t\treturn;\n\t}\n\t_cameraState = active\n\t\t? Webrtc::VideoState::Active\n\t\t: Webrtc::VideoState::Inactive;\n}\n\nvoid GroupCall::toggleScreenSharing(\n\t\tstd::optional<QString> uniqueId,\n\t\tbool withAudio) {\n\tif (!_instance || !_id) {\n\t\treturn;\n\t} else if (!uniqueId) {\n\t\t_screenState = Webrtc::VideoState::Inactive;\n\t\treturn;\n\t}\n\tconst auto changed = (_screenDeviceId != *uniqueId);\n\tconst auto wasSharing = isSharingScreen();\n\t_screenDeviceId = *uniqueId;\n\t_screenWithAudio = withAudio;\n\t_screenState = Webrtc::VideoState::Active;\n\tif (changed && wasSharing && isSharingScreen()) {\n\t\t_screenCapture->switchToDevice(uniqueId->toStdString(), true);\n\t}\n\tif (_screenInstance) {\n\t\t_screenInstance->setIsMuted(!withAudio);\n\t}\n}\n\nbool GroupCall::hasVideoWithFrames() const {\n\treturn !_shownVideoTracks.empty();\n}\n\nrpl::producer<bool> GroupCall::hasVideoWithFramesValue() const {\n\treturn _videoStreamShownUpdates.events_starting_with(\n\t\tVideoStateToggle()\n\t) | rpl::map([=] {\n\t\treturn hasVideoWithFrames();\n\t}) | rpl::distinct_until_changed();\n}\n\nvoid GroupCall::setScheduledDate(TimeId date) {\n\tconst auto was = _scheduleDate;\n\t_scheduleDate = date;\n\tif (was && !date) {\n\t\tinitialJoin();\n\t}\n}\n\nvoid GroupCall::setMessagesEnabled(bool enabled) {\n\t_messagesEnabled = enabled && (!_rtmp || videoStream());\n}\n\nvoid GroupCall::subscribeToReal(not_null<Data::GroupCall*> real) {\n\t_listenersHidden = real->listenersHidden();\n\n\treal->scheduleDateValue(\n\t) | rpl::on_next([=](TimeId date) {\n\t\tsetScheduledDate(date);\n\t}, _lifetime);\n\n\treal->messagesEnabledValue(\n\t) | rpl::on_next([=](bool enabled) {\n\t\tsetMessagesEnabled(enabled);\n\t}, _lifetime);\n\n\t// Postpone creating video tracks, so that we know if Panel\n\t// supports OpenGL and we don't need ARGB32 frames at all.\n\tUi::PostponeCall(this, [=] {\n\t\tif (const auto real = lookupReal()) {\n\t\t\treal->participantsReloaded(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tfillActiveVideoEndpoints();\n\t\t\t}, _lifetime);\n\t\t\tfillActiveVideoEndpoints();\n\t\t}\n\t});\n\n\tusing Update = Data::GroupCall::ParticipantUpdate;\n\treal->participantUpdated(\n\t) | rpl::on_next([=](const Update &data) {\n\t\tconst auto regularEndpoint = [&](const std::string &endpoint)\n\t\t-> const std::string & {\n\t\t\treturn (endpoint.empty()\n\t\t\t\t\t|| endpoint == _cameraEndpoint\n\t\t\t\t\t|| endpoint == _screenEndpoint)\n\t\t\t\t? EmptyString()\n\t\t\t\t: endpoint;\n\t\t};\n\n\t\tconst auto peer = data.was ? data.was->peer : data.now->peer;\n\t\tif (peer == joinAs()) {\n\t\t\tconst auto working = data.now && data.now->videoJoined;\n\t\t\tif (videoIsWorking() != working) {\n\t\t\t\tfillActiveVideoEndpoints();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst auto &wasCameraEndpoint = data.was\n\t\t\t? regularEndpoint(GetCameraEndpoint(data.was->videoParams))\n\t\t\t: EmptyString();\n\t\tconst auto &nowCameraEndpoint = data.now\n\t\t\t? regularEndpoint(GetCameraEndpoint(data.now->videoParams))\n\t\t\t: EmptyString();\n\t\tconst auto wasCameraPaused = !wasCameraEndpoint.empty()\n\t\t\t&& IsCameraPaused(data.was->videoParams);\n\t\tconst auto nowCameraPaused = !nowCameraEndpoint.empty()\n\t\t\t&& IsCameraPaused(data.now->videoParams);\n\t\tif (wasCameraEndpoint != nowCameraEndpoint) {\n\t\t\tmarkEndpointActive({\n\t\t\t\tVideoEndpointType::Camera,\n\t\t\t\tpeer,\n\t\t\t\tnowCameraEndpoint,\n\t\t\t}, true, nowCameraPaused);\n\t\t\tmarkEndpointActive({\n\t\t\t\tVideoEndpointType::Camera,\n\t\t\t\tpeer,\n\t\t\t\twasCameraEndpoint,\n\t\t\t}, false, false);\n\t\t} else if (wasCameraPaused != nowCameraPaused) {\n\t\t\tmarkTrackPaused({\n\t\t\t\tVideoEndpointType::Camera,\n\t\t\t\tpeer,\n\t\t\t\tnowCameraEndpoint,\n\t\t\t}, nowCameraPaused);\n\t\t}\n\t\tconst auto &wasScreenEndpoint = data.was\n\t\t\t? regularEndpoint(data.was->screenEndpoint())\n\t\t\t: EmptyString();\n\t\tconst auto &nowScreenEndpoint = data.now\n\t\t\t? regularEndpoint(data.now->screenEndpoint())\n\t\t\t: EmptyString();\n\t\tconst auto wasScreenPaused = !wasScreenEndpoint.empty()\n\t\t\t&& IsScreenPaused(data.was->videoParams);\n\t\tconst auto nowScreenPaused = !nowScreenEndpoint.empty()\n\t\t\t&& IsScreenPaused(data.now->videoParams);\n\t\tif (wasScreenEndpoint != nowScreenEndpoint) {\n\t\t\tmarkEndpointActive({\n\t\t\t\tVideoEndpointType::Screen,\n\t\t\t\tpeer,\n\t\t\t\tnowScreenEndpoint,\n\t\t\t}, true, nowScreenPaused);\n\t\t\tmarkEndpointActive({\n\t\t\t\tVideoEndpointType::Screen,\n\t\t\t\tpeer,\n\t\t\t\twasScreenEndpoint,\n\t\t\t}, false, false);\n\t\t} else if (wasScreenPaused != nowScreenPaused) {\n\t\t\tmarkTrackPaused({\n\t\t\t\tVideoEndpointType::Screen,\n\t\t\t\tpeer,\n\t\t\t\twasScreenEndpoint,\n\t\t\t}, nowScreenPaused);\n\t\t}\n\t}, _lifetime);\n\n\treal->participantsResolved(\n\t) | rpl::on_next([=](\n\t\tnot_null<const base::flat_map<\n\t\t\tuint32,\n\t\t\tData::LastSpokeTimes>*> ssrcs) {\n\t\tcheckMediaChannelDescriptions([&](uint32 ssrc) {\n\t\t\treturn ssrcs->contains(ssrc);\n\t\t});\n\t}, _lifetime);\n\n\treal->participantSpeaking(\n\t) | rpl::filter([=] {\n\t\treturn _videoEndpointLarge.current();\n\t}) | rpl::on_next([=](not_null<Data::GroupCallParticipant*> p) {\n\t\tconst auto now = crl::now();\n\t\tif (_videoEndpointLarge.current().peer == p->peer) {\n\t\t\t_videoLargeTillTime = std::max(\n\t\t\t\t_videoLargeTillTime,\n\t\t\t\tnow + kFixSpeakingLargeVideoDuration);\n\t\t\treturn;\n\t\t} else if (videoEndpointPinned() || _videoLargeTillTime > now) {\n\t\t\treturn;\n\t\t}\n\t\tusing Type = VideoEndpointType;\n\t\tconst auto &params = p->videoParams;\n\t\tif (GetCameraEndpoint(params).empty()\n\t\t\t&& GetScreenEndpoint(params).empty()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto tryEndpoint = [&](Type type, const std::string &id) {\n\t\t\tif (id.empty()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto endpoint = VideoEndpoint{ type, p->peer, id };\n\t\t\tif (!shownVideoTracks().contains(endpoint)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tsetVideoEndpointLarge(endpoint);\n\t\t\treturn true;\n\t\t};\n\t\tif (tryEndpoint(Type::Screen, GetScreenEndpoint(params))\n\t\t\t|| tryEndpoint(Type::Camera, GetCameraEndpoint(params))) {\n\t\t\t_videoLargeTillTime = now + kFixSpeakingLargeVideoDuration;\n\t\t}\n\t}, _lifetime);\n}\n\nvoid GroupCall::checkGlobalShortcutAvailability() {\n\tauto &settings = Core::App().settings();\n\tif (!settings.groupCallPushToTalk()) {\n\t\treturn;\n\t} else if (!base::GlobalShortcutsAllowed()) {\n\t\tsettings.setGroupCallPushToTalk(false);\n\t\tCore::App().saveSettingsDelayed();\n\t}\n}\n\nvoid GroupCall::setState(State state) {\n\tconst auto current = _state.current();\n\tif (current == State::Failed) {\n\t\treturn;\n\t} else if (current == State::Ended && state != State::Failed) {\n\t\treturn;\n\t} else if (current == State::FailedHangingUp && state != State::Failed) {\n\t\treturn;\n\t} else if (current == State::HangingUp\n\t\t&& state != State::Ended\n\t\t&& state != State::Failed) {\n\t\treturn;\n\t}\n\tif (current == state) {\n\t\treturn;\n\t}\n\t_state = state;\n\n\tif (state == State::Joined) {\n\t\tstopConnectingSound();\n\t\tif (const auto real = lookupReal()) {\n\t\t\treal->setInCall();\n\t\t}\n\t}\n\n\tif (false\n\t\t|| state == State::Ended\n\t\t|| state == State::Failed) {\n\t\t// Destroy controller before destroying Call Panel,\n\t\t// so that the panel hide animation is smooth.\n\t\tdestroyScreencast();\n\t\tdestroyController();\n\t}\n\tswitch (state) {\n\tcase State::HangingUp:\n\tcase State::FailedHangingUp:\n\t\tstopConnectingSound();\n\t\t_delegate->groupCallPlaySound(Delegate::GroupCallSound::Ended);\n\t\tbreak;\n\tcase State::Ended:\n\t\tstopConnectingSound();\n\t\t_delegate->groupCallFinished(this);\n\t\tbreak;\n\tcase State::Failed:\n\t\tstopConnectingSound();\n\t\t_delegate->groupCallFailed(this);\n\t\tbreak;\n\tcase State::Connecting:\n\t\tif (!_checkJoinedTimer.isActive()) {\n\t\t\t_checkJoinedTimer.callOnce(kCheckJoinedTimeout);\n\t\t}\n\t\tbreak;\n\t}\n}\n\nvoid GroupCall::playConnectingSound() {\n\tconst auto state = _state.current();\n\tif (_connectingSoundTimer.isActive()\n\t\t|| state == State::HangingUp\n\t\t|| state == State::FailedHangingUp\n\t\t|| state == State::Ended\n\t\t|| state == State::Failed) {\n\t\treturn;\n\t}\n\tplayConnectingSoundOnce();\n\t_connectingSoundTimer.callEach(kPlayConnectingEach);\n}\n\nvoid GroupCall::stopConnectingSound() {\n\t_connectingSoundTimer.cancel();\n}\n\nvoid GroupCall::playConnectingSoundOnce() {\n\t_delegate->groupCallPlaySound(Delegate::GroupCallSound::Connecting);\n}\n\nnot_null<PeerData*> GroupCall::messagesFrom() const {\n\tif (!videoStream()) {\n\t\treturn joinAs();\n\t} else if (const auto real = lookupReal()) {\n\t\treturn real->resolveSendAs();\n\t}\n\treturn _peer->session().user();\n}\n\nbool GroupCall::showChooseJoinAs() const {\n\treturn !_rtmp\n\t\t&& !videoStream()\n\t\t&& ((_possibleJoinAs.size() > 1)\n\t\t\t|| (_possibleJoinAs.size() == 1\n\t\t\t\t&& !_possibleJoinAs.front()->isSelf()));\n}\n\nbool GroupCall::scheduleStartSubscribed() const {\n\tif (const auto real = lookupReal()) {\n\t\treturn real->scheduleStartSubscribed();\n\t}\n\treturn false;\n}\n\nbool GroupCall::rtmp() const {\n\treturn _rtmp;\n}\n\nbool GroupCall::conference() const {\n\tif (const auto raw = _sharedCall.get()) {\n\t\treturn (raw->origin() == Data::GroupCallOrigin::Conference);\n\t} else if (_startConferenceInfo.get()) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool GroupCall::videoStream() const {\n\tif (const auto raw = _sharedCall.get()) {\n\t\treturn (raw->origin() == Data::GroupCallOrigin::VideoStream);\n\t}\n\treturn false;\n}\n\nData::GroupCallOrigin GroupCall::origin() const {\n\tif (const auto raw = _sharedCall.get()) {\n\t\treturn raw->origin();\n\t} else if (_startConferenceInfo.get()) {\n\t\treturn Data::GroupCallOrigin::Conference;\n\t}\n\treturn Data::GroupCallOrigin::Group;\n}\n\nbool GroupCall::listenersHidden() const {\n\treturn _listenersHidden;\n}\n\nbool GroupCall::emptyRtmp() const {\n\treturn _emptyRtmp.current();\n}\n\nrpl::producer<bool> GroupCall::emptyRtmpValue() const {\n\treturn _emptyRtmp.value();\n}\n\nint GroupCall::rtmpVolume() const {\n\treturn _singleSourceVolume;\n}\n\nCalls::Group::RtmpInfo GroupCall::rtmpInfo() const {\n\treturn { _rtmpUrl, _rtmpKey };\n}\n\nvoid GroupCall::setRtmpInfo(const Calls::Group::RtmpInfo &value) {\n\t_rtmpUrl = value.url;\n\t_rtmpKey = value.key;\n}\n\nData::GroupCall *GroupCall::lookupReal() const {\n\tif (const auto shared = _sharedCall.get()) {\n\t\treturn shared;\n\t}\n\tconst auto real = _peer->groupCall();\n\treturn (real && real->id() == _id) ? real : nullptr;\n}\n\nstd::shared_ptr<Data::GroupCall> GroupCall::sharedCall() const {\n\treturn _sharedCall;\n}\n\nrpl::producer<not_null<Data::GroupCall*>> GroupCall::real() const {\n\tif (const auto real = lookupReal()) {\n\t\treturn rpl::single(not_null{ real });\n\t}\n\treturn _realChanges.events();\n}\n\nrpl::producer<QByteArray> GroupCall::emojiHashValue() const {\n\treturn _emojiHash.value();\n}\n\nvoid GroupCall::start(TimeId scheduleDate, bool rtmp) {\n\tusing Flag = MTPphone_CreateGroupCall::Flag;\n\t_createRequestId = _api.request(MTPphone_CreateGroupCall(\n\t\tMTP_flags((scheduleDate ? Flag::f_schedule_date : Flag(0))\n\t\t\t| (rtmp ? Flag::f_rtmp_stream : Flag(0))),\n\t\t_peer->input(),\n\t\tMTP_int(base::RandomValue<int32>()),\n\t\tMTPstring(), // title\n\t\tMTP_int(scheduleDate)\n\t)).done([=](const MTPUpdates &result) {\n\t\t_createRequestId = 0;\n\t\t_reloadedStaleCall = true;\n\t\t_acceptFields = true;\n\t\t_peer->session().api().applyUpdates(result);\n\t\t_acceptFields = false;\n\t}).fail([=](const MTP::Error &error) {\n\t\t_createRequestId = 0;\n\t\tLOG((\"Call Error: Could not create, error: %1\"\n\t\t\t).arg(error.type()));\n\t\thangup();\n\t}).send();\n}\n\nvoid GroupCall::applyInputCall(const MTPInputGroupCall &inputCall) {\n\tinputCall.match([&](const MTPDinputGroupCall &data) {\n\t\t_id = data.vid().v;\n\t\t_accessHash = data.vaccess_hash().v;\n\t}, [&](const auto &) {\n\t\tUnexpected(\"slug/msg in GroupCall::join.\");\n\t});\n}\n\nvoid GroupCall::initialJoin() {\n\tsetState(_scheduleDate ? State::Waiting : State::Joining);\n\tif (_scheduleDate) {\n\t\treturn;\n\t}\n\trejoin();\n\tif (_id) {\n\t\tinitialJoinRequested();\n\t}\n}\n\nvoid GroupCall::initialJoinRequested() {\n\tusing Update = Data::GroupCall::ParticipantUpdate;\n\tconst auto real = lookupReal();\n\tAssert(real != nullptr);\n\treal->participantUpdated(\n\t) | rpl::filter([=](const Update &update) {\n\t\treturn (_instance != nullptr);\n\t}) | rpl::on_next([=](const Update &update) {\n\t\tif (!update.now) {\n\t\t\t_instance->removeSsrcs({\n\t\t\t\tupdate.was->ssrc,\n\t\t\t\tGetAdditionalAudioSsrc(update.was->videoParams),\n\t\t\t});\n\t\t} else if (videoStream()) {\n\t\t\tconst auto value = singleSourceVolumeValue();\n\t\t\t_instance->setVolume(update.now->ssrc, value);\n\t\t} else if (!_rtmp) {\n\t\t\tupdateInstanceVolume(update.was, *update.now);\n\t\t}\n\t}, _lifetime);\n\n\tif (_sharedCall && conference()) {\n\t\t_canManage = _sharedCall->canManage();\n\t\treturn;\n\t}\n\t_peer->session().updates().addActiveChat(\n\t\t_peerStream.events_starting_with_copy(_peer));\n\t_canManage = Data::CanManageGroupCallValue(_peer);\n\tSubscribeToMigration(_peer, _lifetime, [=](not_null<ChannelData*> peer) {\n\t\t_peer = peer;\n\t\t_canManage = Data::CanManageGroupCallValue(_peer);\n\t\t_peerStream.fire_copy(peer);\n\t});\n}\n\nvoid GroupCall::setScreenEndpoint(std::string endpoint) {\n\tif (_screenEndpoint == endpoint) {\n\t\treturn;\n\t}\n\tif (!_screenEndpoint.empty()) {\n\t\tmarkEndpointActive({\n\t\t\tVideoEndpointType::Screen,\n\t\t\tjoinAs(),\n\t\t\t_screenEndpoint\n\t\t}, false, false);\n\t}\n\t_screenEndpoint = std::move(endpoint);\n\tif (_screenEndpoint.empty()) {\n\t\treturn;\n\t}\n\tif (isSharingScreen()) {\n\t\tmarkEndpointActive({\n\t\t\tVideoEndpointType::Screen,\n\t\t\tjoinAs(),\n\t\t\t_screenEndpoint\n\t\t}, true, isScreenPaused());\n\t}\n}\n\nvoid GroupCall::setCameraEndpoint(std::string endpoint) {\n\tif (_cameraEndpoint == endpoint) {\n\t\treturn;\n\t}\n\tif (!_cameraEndpoint.empty()) {\n\t\tmarkEndpointActive({\n\t\t\tVideoEndpointType::Camera,\n\t\t\tjoinAs(),\n\t\t\t_cameraEndpoint\n\t\t}, false, false);\n\t}\n\t_cameraEndpoint = std::move(endpoint);\n\tif (_cameraEndpoint.empty()) {\n\t\treturn;\n\t}\n\tif (isSharingCamera()) {\n\t\tmarkEndpointActive({\n\t\t\tVideoEndpointType::Camera,\n\t\t\tjoinAs(),\n\t\t\t_cameraEndpoint\n\t\t}, true, isCameraPaused());\n\t}\n}\n\nvoid GroupCall::addVideoOutput(\n\t\tconst std::string &endpoint,\n\t\tSinkPointer sink) {\n\tif (_cameraEndpoint == endpoint) {\n\t\tif (auto strong = sink.data.lock()) {\n\t\t\t_cameraCapture->setOutput(std::move(strong));\n\t\t}\n\t} else if (_screenEndpoint == endpoint) {\n\t\tif (auto strong = sink.data.lock()) {\n\t\t\t_screenCapture->setOutput(std::move(strong));\n\t\t}\n\t} else if (_instance) {\n\t\t_instance->addIncomingVideoOutput(endpoint, std::move(sink.data));\n\t} else {\n\t\t_pendingVideoOutputs.emplace(endpoint, std::move(sink));\n\t}\n}\n\nvoid GroupCall::markEndpointActive(\n\t\tVideoEndpoint endpoint,\n\t\tbool active,\n\t\tbool paused) {\n\tif (!endpoint) {\n\t\treturn;\n\t}\n\tconst auto i = _activeVideoTracks.find(endpoint);\n\tconst auto changed = active\n\t\t? (i == end(_activeVideoTracks))\n\t\t: (i != end(_activeVideoTracks));\n\tif (!changed) {\n\t\tif (active) {\n\t\t\tmarkTrackPaused(endpoint, paused);\n\t\t}\n\t\treturn;\n\t}\n\tauto shown = false;\n\tif (active) {\n\t\tconst auto i = _activeVideoTracks.emplace(\n\t\t\tendpoint,\n\t\t\tstd::make_unique<VideoTrack>(\n\t\t\t\tpaused,\n\t\t\t\t_requireARGB32,\n\t\t\t\tendpoint.peer)).first;\n\t\tconst auto track = &i->second->track;\n\n\t\ttrack->renderNextFrame(\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto activeTrack = _activeVideoTracks[endpoint].get();\n\t\t\tconst auto size = track->frameSize();\n\t\t\tif (size.isEmpty()) {\n\t\t\t\ttrack->markFrameShown();\n\t\t\t} else if (!activeTrack->shown) {\n\t\t\t\tactiveTrack->shown = true;\n\t\t\t\tmarkTrackShown(endpoint, true);\n\t\t\t}\n\t\t\tactiveTrack->trackSize = size;\n\t\t}, i->second->lifetime);\n\n\t\tconst auto size = track->frameSize();\n\t\ti->second->trackSize = size;\n\t\tif (!size.isEmpty() || paused) {\n\t\t\ti->second->shown = true;\n\t\t\tshown = true;\n\t\t} else {\n\t\t\ttrack->stateValue(\n\t\t\t) | rpl::filter([=](Webrtc::VideoState state) {\n\t\t\t\treturn (state == Webrtc::VideoState::Paused)\n\t\t\t\t\t&& !_activeVideoTracks[endpoint]->shown;\n\t\t\t}) | rpl::on_next([=] {\n\t\t\t\t_activeVideoTracks[endpoint]->shown = true;\n\t\t\t\tmarkTrackShown(endpoint, true);\n\t\t\t}, i->second->lifetime);\n\t\t}\n\t\taddVideoOutput(i->first.id, { track->sink() });\n\t} else {\n\t\tif (_videoEndpointLarge.current() == endpoint) {\n\t\t\tsetVideoEndpointLarge({});\n\t\t}\n\t\tmarkTrackShown(endpoint, false);\n\t\tmarkTrackPaused(endpoint, false);\n\t\t_activeVideoTracks.erase(i);\n\t}\n\tupdateRequestedVideoChannelsDelayed();\n\t_videoStreamActiveUpdates.fire({ endpoint, active });\n\tif (active) {\n\t\tmarkTrackShown(endpoint, shown);\n\t\tmarkTrackPaused(endpoint, paused);\n\t}\n}\n\nvoid GroupCall::markTrackShown(const VideoEndpoint &endpoint, bool shown) {\n\tconst auto changed = shown\n\t\t? _shownVideoTracks.emplace(endpoint).second\n\t\t: _shownVideoTracks.remove(endpoint);\n\tif (!changed) {\n\t\treturn;\n\t}\n\t_videoStreamShownUpdates.fire_copy({ endpoint, shown });\n\tif (shown && endpoint.type == VideoEndpointType::Screen) {\n\t\tcrl::on_main(this, [=] {\n\t\t\tif (_shownVideoTracks.contains(endpoint)) {\n\t\t\t\tpinVideoEndpoint(endpoint);\n\t\t\t}\n\t\t});\n\t}\n}\n\nvoid GroupCall::markTrackPaused(const VideoEndpoint &endpoint, bool paused) {\n\tif (!endpoint) {\n\t\treturn;\n\t}\n\n\tconst auto i = _activeVideoTracks.find(endpoint);\n\tAssert(i != end(_activeVideoTracks));\n\n\ti->second->track.setState(paused\n\t\t? Webrtc::VideoState::Paused\n\t\t: Webrtc::VideoState::Active);\n}\n\nvoid GroupCall::startRejoin() {\n\tif (_joinState.action != JoinAction::None || _createRequestId) {\n\t\t// Don't reset _e2e in that case, if rejoin() is a no-op.\n\t\treturn;\n\t}\n\tfor (const auto &[task, part] : _broadcastParts) {\n\t\t_api.request(part.requestId).cancel();\n\t}\n\tif (conference()) {\n\t\tinitConferenceE2E();\n\t}\n\tsetState(State::Joining);\n\trejoin();\n}\n\nvoid GroupCall::rejoin() {\n\trejoin(joinAs());\n}\n\nvoid GroupCall::rejoinWithHash(const QString &hash) {\n\tif (!hash.isEmpty() && mutedByAdmin()) {\n\t\t_joinHash = hash;\n\t\trejoin();\n\t}\n}\n\nvoid GroupCall::setJoinAs(not_null<PeerData*> as) {\n\t_joinAs = as;\n\tif (const auto chat = _peer->asChat()) {\n\t\tchat->setGroupCallDefaultJoinAs(joinAs()->id);\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\tchannel->setGroupCallDefaultJoinAs(joinAs()->id);\n\t}\n}\n\nvoid GroupCall::saveDefaultJoinAs(not_null<PeerData*> as) {\n\tsetJoinAs(as);\n\t_api.request(MTPphone_SaveDefaultGroupCallJoinAs(\n\t\t_peer->input(),\n\t\tjoinAs()->input()\n\t)).send();\n}\n\nvoid GroupCall::rejoin(not_null<PeerData*> as) {\n\tif (state() != State::Joining\n\t\t&& state() != State::Joined\n\t\t&& state() != State::Connecting) {\n\t\treturn;\n\t} else if (_joinState.action != JoinAction::None || _createRequestId) {\n\t\treturn;\n\t}\n\n\tif (joinAs() != as) {\n\t\ttoggleVideo(false);\n\t\ttoggleScreenSharing(std::nullopt);\n\t}\n\n\t_joinState.action = JoinAction::Joining;\n\t_joinState.ssrc = 0;\n\t_initialMuteStateSent = false;\n\tsetState(State::Joining);\n\tif (!tryCreateController()) {\n\t\tsetInstanceMode(InstanceMode::None);\n\t}\n\tapplyMeInCallLocally();\n\tLOG((\"Call Info: Requesting join payload.\"));\n\n\tsetJoinAs(as);\n\n\tconst auto weak = base::make_weak(&_instanceGuard);\n\t_instance->emitJoinPayload([=](tgcalls::GroupJoinPayload payload) {\n\t\tcrl::on_main(weak, [=, payload = std::move(payload)] {\n\t\t\t_joinState.payload = {\n\t\t\t\t.ssrc = payload.audioSsrc,\n\t\t\t\t.json = QByteArray::fromStdString(payload.json),\n\t\t\t};\n\t\t\tLOG((\"Call Info: Join payload received, joining with ssrc: %1.\"\n\t\t\t\t).arg(_joinState.payload.ssrc));\n\t\t\tif (!_sharedCall && _startConferenceInfo) {\n\t\t\t\tstartConference();\n\t\t\t} else if (conference()\n\t\t\t\t&& _sharedCall\n\t\t\t\t&& !_sharedCall->blockchainMayBeEmpty()\n\t\t\t\t&& !_e2e->hasLastBlock0()) {\n\t\t\t\trefreshLastBlockAndJoin();\n\t\t\t} else {\n\t\t\t\tsendJoinRequest();\n\t\t\t}\n\t\t});\n\t});\n}\n\nvoid GroupCall::sendJoinRequest() {\n\tif (state() != State::Joining) {\n\t\t_joinState.finish();\n\t\tcheckNextJoinAction();\n\t\treturn;\n\t}\n\tconst auto joinBlock = _e2e ? _e2e->makeJoinBlock().data : QByteArray();\n\tif (_e2e && joinBlock.isEmpty()) {\n\t\t_joinState.finish();\n\t\tLOG((\"Call Error: Could not generate join block.\"));\n\t\thangup();\n\t\tUi::Toast::Show(u\"Could not generate join block.\"_q);\n\t\treturn;\n\t}\n\tconst auto wasMuteState = muted();\n\tconst auto wasVideoStopped = !isSharingCamera();\n\tusing Flag = MTPphone_JoinGroupCall::Flag;\n\tconst auto flags = (wasMuteState != MuteState::Active\n\t\t? Flag::f_muted\n\t\t: Flag(0))\n\t\t| (_joinHash.isEmpty() ? Flag(0) : Flag::f_invite_hash)\n\t\t| (wasVideoStopped ? Flag::f_video_stopped : Flag(0))\n\t\t| (_e2e ? (Flag::f_public_key | Flag::f_block) : Flag());\n\t_api.request(MTPphone_JoinGroupCall(\n\t\tMTP_flags(flags),\n\t\tinputCallSafe(),\n\t\tjoinAs()->input(),\n\t\tMTP_string(_joinHash),\n\t\t(_e2e ? TdE2E::PublicKeyToMTP(_e2e->myKey()) : MTPint256()),\n\t\tMTP_bytes(joinBlock),\n\t\tMTP_dataJSON(MTP_bytes(_joinState.payload.json))\n\t)).done([=](\n\t\t\tconst MTPUpdates &result,\n\t\t\tconst MTP::Response &response) {\n\t\tjoinDone(\n\t\t\tTimestampInMsFromMsgId(response.outerMsgId),\n\t\t\tresult,\n\t\t\twasMuteState,\n\t\t\twasVideoStopped);\n\t}).fail([=](const MTP::Error &error) {\n\t\tjoinFail(error.type());\n\t}).send();\n}\n\nvoid GroupCall::refreshLastBlockAndJoin() {\n\tExpects(_e2e != nullptr);\n\n\tif (state() != State::Joining) {\n\t\t_joinState.finish();\n\t\tcheckNextJoinAction();\n\t\treturn;\n\t}\n\t_api.request(MTPphone_GetGroupCallChainBlocks(\n\t\tinputCallSafe(),\n\t\tMTP_int(0),\n\t\tMTP_int(-1),\n\t\tMTP_int(1)\n\t)).done([=](const MTPUpdates &result) {\n\t\tif (result.type() != mtpc_updates) {\n\t\t\t_joinState.finish();\n\t\t\tLOG((\"Call Error: Bad result in GroupCallChainBlocks.\"));\n\t\t\thangup();\n\t\t\tUi::Toast::Show(u\"Bad Updates in GroupCallChainBlocks.\"_q);\n\t\t\treturn;\n\t\t}\n\t\t_e2e->refreshLastBlock0({});\n\t\tconst auto &data = result.c_updates();\n\t\tfor (const auto &update : data.vupdates().v) {\n\t\t\tif (update.type() != mtpc_updateGroupCallChainBlocks) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto &data = update.c_updateGroupCallChainBlocks();\n\t\t\tconst auto &blocks = data.vblocks().v;\n\t\t\tif (!blocks.isEmpty()) {\n\t\t\t\t_e2e->refreshLastBlock0(TdE2E::Block{ blocks.back().v });\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tsendJoinRequest();\n\t}).fail([=](const MTP::Error &error) {\n\t\t_joinState.finish();\n\t\tconst auto &type = error.type();\n\t\tLOG((\"Call Error: Could not get last block, error: %1\").arg(type));\n\t\thangup();\n\t\tUi::Toast::Show(error.type());\n\t}).send();\n}\n\nvoid GroupCall::startConference() {\n\tExpects(_e2e != nullptr && _startConferenceInfo != nullptr);\n\n\tconst auto joinBlock = _e2e->makeJoinBlock().data;\n\tAssert(!joinBlock.isEmpty());\n\n\tconst auto wasMuteState = muted();\n\tconst auto wasVideoStopped = !isSharingCamera();\n\tusing Flag = MTPphone_CreateConferenceCall::Flag;\n\tconst auto flags = Flag::f_join\n\t\t| Flag::f_public_key\n\t\t| Flag::f_block\n\t\t| Flag::f_params\n\t\t| ((wasMuteState != MuteState::Active) ? Flag::f_muted : Flag(0))\n\t\t| (wasVideoStopped ? Flag::f_video_stopped : Flag(0));\n\t_createRequestId = _api.request(MTPphone_CreateConferenceCall(\n\t\tMTP_flags(flags),\n\t\tMTP_int(base::RandomValue<int32>()),\n\t\tTdE2E::PublicKeyToMTP(_e2e->myKey()),\n\t\tMTP_bytes(joinBlock),\n\t\tMTP_dataJSON(MTP_bytes(_joinState.payload.json))\n\t)).done([=](\n\t\t\tconst MTPUpdates &result,\n\t\t\tconst MTP::Response &response) {\n\t\t_createRequestId = 0;\n\t\t_sharedCall = _peer->owner().sharedConferenceCallFind(result);\n\t\tif (!_sharedCall) {\n\t\t\tjoinFail(u\"Call not found!\"_q);\n\t\t\treturn;\n\t\t}\n\t\tapplyInputCall(_sharedCall->input());\n\t\t_realChanges.fire_copy(_sharedCall.get());\n\n\t\tinitialJoinRequested();\n\t\tjoinDone(\n\t\t\tTimestampInMsFromMsgId(response.outerMsgId),\n\t\t\tresult,\n\t\t\twasMuteState,\n\t\t\twasVideoStopped,\n\t\t\ttrue);\n\t}).fail([=](const MTP::Error &error) {\n\t\t_createRequestId = 0;\n\t\tLOG((\"Call Error: Could not create, error: %1\"\n\t\t\t).arg(error.type()));\n\t\thangup();\n\t}).send();\n}\n\nvoid GroupCall::joinDone(\n\t\tint64 serverTimeMs,\n\t\tconst MTPUpdates &result,\n\t\tMuteState wasMuteState,\n\t\tbool wasVideoStopped,\n\t\tbool justCreated) {\n\tExpects(!justCreated || _startConferenceInfo != nullptr);\n\n\t_serverTimeMs = serverTimeMs;\n\t_serverTimeMsGotAt = crl::now();\n\n\t_joinState.finish(_joinState.payload.ssrc);\n\t_mySsrcs.emplace(_joinState.ssrc);\n\n\tsetState((_instanceState.current()\n\t\t== InstanceState::Disconnected)\n\t\t? State::Connecting\n\t\t: State::Joined);\n\tapplyMeInCallLocally();\n\tmaybeSendMutedUpdate(wasMuteState);\n\n\tfor (auto &state : _subchains) {\n\t\t// Accept initial join blocks.\n\t\t_api.request(base::take(state.requestId)).cancel();\n\t\tstate.inShortPoll = true;\n\t}\n\t_messages->setApplyingInitial(true);\n\t_peer->session().api().applyUpdates(result);\n\t_messages->setApplyingInitial(false);\n\tfor (auto &state : _subchains) {\n\t\tstate.inShortPoll = false;\n\t}\n\n\tif (justCreated) {\n\t\tsubscribeToReal(_sharedCall.get());\n\t\tsetupConferenceCall();\n\t\t_conferenceLinkSlug = Group::ExtractConferenceSlug(\n\t\t\t_sharedCall->conferenceInviteLink());\n\t\tCore::App().calls().startedConferenceReady(\n\t\t\tthis,\n\t\t\t*_startConferenceInfo);\n\t}\n\n\ttrackParticipantsWithAccess();\n\tapplyQueuedSelfUpdates();\n\tcheckFirstTimeJoined();\n\t_screenJoinState.nextActionPending = true;\n\tcheckNextJoinAction();\n\tif (wasVideoStopped == isSharingCamera()) {\n\t\tsendSelfUpdate(SendUpdateType::CameraStopped);\n\t}\n\tif (isCameraPaused()) {\n\t\tsendSelfUpdate(SendUpdateType::CameraPaused);\n\t}\n\tsendPendingSelfUpdates();\n\tif (!_reloadedStaleCall\n\t\t&& _state.current() != State::Joining) {\n\t\tif (const auto real = lookupReal()) {\n\t\t\t_reloadedStaleCall = true;\n\t\t\treal->reloadIfStale();\n\t\t}\n\t}\n\tif (_e2e) {\n\t\t_e2e->joined();\n\t\tif (!_pendingOutboundBlock.isEmpty()) {\n\t\t\tsendOutboundBlock(base::take(_pendingOutboundBlock));\n\t\t}\n\t}\n\tif (const auto once = base::take(_startConferenceInfo)) {\n\t\tprocessConferenceStart(*once);\n\t}\n\tfor (const auto &callback : base::take(_rejoinedCallbacks)) {\n\t\tcallback();\n\t}\n}\n\nvoid GroupCall::joinFail(const QString &error) {\n\tif (_e2e) {\n\t\tif (error.startsWith(u\"CONF_WRITE_CHAIN_INVALID\"_q)) {\n\t\t\tif (_id) {\n\t\t\t\trefreshLastBlockAndJoin();\n\t\t\t} else {\n\t\t\t\thangup();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t}\n\t_joinState.finish();\n\tLOG((\"Call Error: Could not join, error: %1\").arg(error));\n\n\tif (_id && error == u\"GROUPCALL_SSRC_DUPLICATE_MUCH\") {\n\t\trejoin();\n\t\treturn;\n\t}\n\n\thangup();\n\tUi::Toast::Show((error == u\"GROUPCALL_FORBIDDEN\"_q\n\t\t|| error == u\"GROUPCALL_INVALID\"_q)\n\t\t? tr::lng_confcall_not_accessible(tr::now)\n\t\t: error);\n}\n\nvoid GroupCall::requestSubchainBlocks(int subchain, int height) {\n\tExpects(subchain >= 0 && subchain < kSubChainsCount);\n\n\tauto &state = _subchains[subchain];\n\t_api.request(base::take(state.requestId)).cancel();\n\tstate.requestId = _api.request(MTPphone_GetGroupCallChainBlocks(\n\t\tinputCall(),\n\t\tMTP_int(subchain),\n\t\tMTP_int(height),\n\t\tMTP_int(kShortPollChainBlocksPerRequest)\n\t)).done([=](const MTPUpdates &result) {\n\t\tauto &state = _subchains[subchain];\n\t\tstate.requestId = 0;\n\t\tstate.inShortPoll = true;\n\t\t_peer->session().api().applyUpdates(result);\n\t\tstate.inShortPoll = false;\n\t\tfor (const auto &data : base::take(state.pending)) {\n\t\t\tapplySubChainUpdate(subchain, data.blocks, data.next);\n\t\t}\n\t\t_e2e->subchainBlocksRequestFinished(subchain);\n\t}).fail([=](const MTP::Error &error) {\n\t\tauto &state = _subchains[subchain];\n\t\tstate.requestId = 0;\n\t\t_e2e->subchainBlocksRequestFinished(subchain);\n\t\tif (error.type() == u\"GROUPCALL_FORBIDDEN\"_q) {\n\t\t\tLOG((\"Call Info: Rejoin after error '%1' in get chain blocks.\"\n\t\t\t\t).arg(error.type()));\n\t\t\tstartRejoin();\n\t\t}\n\t}).send();\n}\n\nvoid GroupCall::sendOutboundBlock(QByteArray block) {\n\t_pendingOutboundBlock = QByteArray();\n\t_api.request(MTPphone_SendConferenceCallBroadcast(\n\t\tinputCall(),\n\t\tMTP_bytes(block)\n\t)).done([=](const MTPUpdates &result) {\n\t\t_peer->session().api().applyUpdates(result);\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto type = error.type();\n\t\tif (type == u\"GROUPCALL_FORBIDDEN\"_q) {\n\t\t\t_pendingOutboundBlock = block;\n\t\t\tLOG((\"Call Info: Rejoin after error '%1' in send confcall block.\"\n\t\t\t\t).arg(type));\n\t\t\tstartRejoin();\n\t\t} else if (type == u\"BLOCK_INVALID\"_q\n\t\t\t|| type.startsWith(u\"CONF_WRITE_CHAIN_INVALID\"_q)) {\n\t\t\tLOG((\"Call Error: Could not broadcast block: %1\").arg(type));\n\t\t} else {\n\t\t\tLOG((\"Call Error: Got '%1' in send confcall block.\").arg(type));\n\t\t\tsendOutboundBlock(block);\n\t\t}\n\t}).send();\n}\n\nvoid GroupCall::checkNextJoinAction() {\n\tif (_joinState.action != JoinAction::None) {\n\t\treturn;\n\t} else if (_joinState.nextActionPending) {\n\t\t_joinState.nextActionPending = false;\n\t\tconst auto state = _state.current();\n\t\tif (state != State::HangingUp && state != State::FailedHangingUp) {\n\t\t\trejoin();\n\t\t} else {\n\t\t\tleave();\n\t\t}\n\t} else if (!_joinState.ssrc) {\n\t\trejoin();\n\t} else if (_screenJoinState.action != JoinAction::None\n\t\t|| !_screenJoinState.nextActionPending) {\n\t\treturn;\n\t} else {\n\t\t_screenJoinState.nextActionPending = false;\n\t\tif (isSharingScreen()) {\n\t\t\trejoinPresentation();\n\t\t} else {\n\t\t\tleavePresentation();\n\t\t}\n\t}\n}\n\nvoid GroupCall::rejoinPresentation() {\n\tif (!_joinState.ssrc\n\t\t|| _screenJoinState.action == JoinAction::Joining\n\t\t|| !isSharingScreen()) {\n\t\treturn;\n\t} else if (_screenJoinState.action != JoinAction::None) {\n\t\t_screenJoinState.nextActionPending = true;\n\t\treturn;\n\t}\n\n\t_screenJoinState.action = JoinAction::Joining;\n\t_screenJoinState.ssrc = 0;\n\tif (!tryCreateScreencast()) {\n\t\tsetScreenInstanceMode(InstanceMode::None);\n\t}\n\tLOG((\"Call Info: Requesting join screen payload.\"));\n\n\tconst auto weak = base::make_weak(&_screenInstanceGuard);\n\t_screenInstance->emitJoinPayload([=](tgcalls::GroupJoinPayload payload) {\n\t\tcrl::on_main(weak, [=, payload = std::move(payload)]{\n\t\t\tif (!isSharingScreen() || !_joinState.ssrc) {\n\t\t\t\t_screenJoinState.finish();\n\t\t\t\tcheckNextJoinAction();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto withMainSsrc = _joinState.ssrc;\n\t\t\tconst auto ssrc = payload.audioSsrc;\n\t\t\tLOG((\"Call Info: Join screen payload received, ssrc: %1.\"\n\t\t\t\t).arg(ssrc));\n\n\t\t\tconst auto json = QByteArray::fromStdString(payload.json);\n\t\t\t_api.request(\n\t\t\t\tMTPphone_JoinGroupCallPresentation(\n\t\t\t\t\tinputCall(),\n\t\t\t\t\tMTP_dataJSON(MTP_bytes(json)))\n\t\t\t).done([=](const MTPUpdates &updates) {\n\t\t\t\t_screenJoinState.finish(ssrc);\n\t\t\t\t_mySsrcs.emplace(ssrc);\n\n\t\t\t\t_peer->session().api().applyUpdates(updates);\n\t\t\t\tcheckNextJoinAction();\n\t\t\t\tif (isScreenPaused()) {\n\t\t\t\t\tsendSelfUpdate(SendUpdateType::ScreenPaused);\n\t\t\t\t}\n\t\t\t\tsendPendingSelfUpdates();\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\t_screenJoinState.finish();\n\n\t\t\t\tconst auto type = error.type();\n\t\t\t\tif (type == u\"GROUPCALL_SSRC_DUPLICATE_MUCH\") {\n\t\t\t\t\t_screenJoinState.nextActionPending = true;\n\t\t\t\t\tcheckNextJoinAction();\n\t\t\t\t} else if (type == u\"GROUPCALL_JOIN_MISSING\"_q\n\t\t\t\t\t|| type == u\"GROUPCALL_FORBIDDEN\"_q) {\n\t\t\t\t\tif (_joinState.ssrc != withMainSsrc) {\n\t\t\t\t\t\t// We've rejoined, rejoin presentation again.\n\t\t\t\t\t\t_screenJoinState.nextActionPending = true;\n\t\t\t\t\t\tcheckNextJoinAction();\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tLOG((\"Call Error: \"\n\t\t\t\t\t\t\"Could not screen join, error: %1\").arg(type));\n\t\t\t\t\t_screenState = Webrtc::VideoState::Inactive;\n\t\t\t\t\t_errors.fire_copy(mutedByAdmin()\n\t\t\t\t\t\t? Error::MutedNoScreen\n\t\t\t\t\t\t: Error::ScreenFailed);\n\t\t\t\t}\n\t\t\t}).send();\n\t\t});\n\t});\n}\n\nvoid GroupCall::leavePresentation() {\n\tdestroyScreencast();\n\tif (!_screenJoinState.ssrc) {\n\t\tsetScreenEndpoint(std::string());\n\t\treturn;\n\t} else if (_screenJoinState.action == JoinAction::Leaving) {\n\t\treturn;\n\t} else if (_screenJoinState.action != JoinAction::None) {\n\t\t_screenJoinState.nextActionPending = true;\n\t\treturn;\n\t}\n\t_api.request(\n\t\tMTPphone_LeaveGroupCallPresentation(inputCall())\n\t).done([=](const MTPUpdates &updates) {\n\t\t_screenJoinState.finish();\n\n\t\t_peer->session().api().applyUpdates(updates);\n\t\tsetScreenEndpoint(std::string());\n\t\tcheckNextJoinAction();\n\t}).fail([=](const MTP::Error &error) {\n\t\t_screenJoinState.finish();\n\n\t\tconst auto type = error.type();\n\t\tLOG((\"Call Error: \"\n\t\t\t\"Could not screen leave, error: %1\").arg(type));\n\t\tsetScreenEndpoint(std::string());\n\t\tcheckNextJoinAction();\n\t}).send();\n}\n\nvoid GroupCall::applyMeInCallLocally() {\n\tconst auto real = lookupReal();\n\tif (!real) {\n\t\treturn;\n\t}\n\tusing Flag = MTPDgroupCallParticipant::Flag;\n\tconst auto participant = real->participantByPeer(joinAs());\n\tconst auto date = participant\n\t\t? participant->date\n\t\t: base::unixtime::now();\n\tconst auto lastActive = participant\n\t\t? participant->lastActive\n\t\t: TimeId(0);\n\tconst auto volume = participant\n\t\t? participant->volume\n\t\t: Group::kDefaultVolume;\n\tconst auto canSelfUnmute = !mutedByAdmin();\n\tconst auto raisedHandRating = (muted() != MuteState::RaisedHand)\n\t\t? uint64(0)\n\t\t: participant\n\t\t? participant->raisedHandRating\n\t\t: FindLocalRaisedHandRating(real->participants());\n\tconst auto flags = (canSelfUnmute ? Flag::f_can_self_unmute : Flag(0))\n\t\t| (lastActive ? Flag::f_active_date : Flag(0))\n\t\t| (_joinState.ssrc ? Flag(0) : Flag::f_left)\n\t\t| (_videoIsWorking.current() ? Flag::f_video_joined : Flag(0))\n\t\t| Flag::f_self\n\t\t| Flag::f_volume // Without flag the volume is reset to 100%.\n\t\t| Flag::f_volume_by_admin // Self volume can only be set by admin.\n\t\t| ((muted() != MuteState::Active) ? Flag::f_muted : Flag(0))\n\t\t| (raisedHandRating > 0 ? Flag::f_raise_hand_rating : Flag(0));\n\treal->applyLocalUpdate(\n\t\tMTP_updateGroupCallParticipants(\n\t\t\tinputCall(),\n\t\t\tMTP_vector<MTPGroupCallParticipant>(\n\t\t\t\t1,\n\t\t\t\tMTP_groupCallParticipant(\n\t\t\t\t\tMTP_flags(flags),\n\t\t\t\t\tpeerToMTP(joinAs()->id),\n\t\t\t\t\tMTP_int(date),\n\t\t\t\t\tMTP_int(lastActive),\n\t\t\t\t\tMTP_int(_joinState.ssrc),\n\t\t\t\t\tMTP_int(volume),\n\t\t\t\t\tMTPstring(), // Don't update about text in local updates.\n\t\t\t\t\tMTP_long(raisedHandRating),\n\t\t\t\t\tMTPGroupCallParticipantVideo(),\n\t\t\t\t\tMTPGroupCallParticipantVideo(),\n\t\t\t\t\tMTPlong())),\n\t\t\tMTP_int(0)).c_updateGroupCallParticipants());\n}\n\nvoid GroupCall::applyParticipantLocally(\n\t\tnot_null<PeerData*> participantPeer,\n\t\tbool mute,\n\t\tstd::optional<int> volume) {\n\tconst auto participant = LookupParticipant(this, participantPeer);\n\tif (!participant || !participant->ssrc) {\n\t\treturn;\n\t}\n\tconst auto canManageCall = canManage();\n\tconst auto isMuted = participant->muted || (mute && canManageCall);\n\tconst auto canSelfUnmute = !canManageCall\n\t\t? participant->canSelfUnmute\n\t\t: (!mute || IsGroupCallAdmin(_peer, participantPeer));\n\tconst auto isMutedByYou = mute && !canManageCall;\n\tusing Flag = MTPDgroupCallParticipant::Flag;\n\tconst auto flags = (canSelfUnmute ? Flag::f_can_self_unmute : Flag(0))\n\t\t| Flag::f_volume // Without flag the volume is reset to 100%.\n\t\t| ((participant->applyVolumeFromMin && !volume)\n\t\t\t? Flag::f_volume_by_admin\n\t\t\t: Flag(0))\n\t\t| (participant->videoJoined ? Flag::f_video_joined : Flag(0))\n\t\t| (participant->lastActive ? Flag::f_active_date : Flag(0))\n\t\t| (isMuted ? Flag::f_muted : Flag(0))\n\t\t| (isMutedByYou ? Flag::f_muted_by_you : Flag(0))\n\t\t| (participantPeer == joinAs() ? Flag::f_self : Flag(0))\n\t\t| (participant->raisedHandRating\n\t\t\t? Flag::f_raise_hand_rating\n\t\t\t: Flag(0));\n\tlookupReal()->applyLocalUpdate(\n\t\tMTP_updateGroupCallParticipants(\n\t\t\tinputCall(),\n\t\t\tMTP_vector<MTPGroupCallParticipant>(\n\t\t\t\t1,\n\t\t\t\tMTP_groupCallParticipant(\n\t\t\t\t\tMTP_flags(flags),\n\t\t\t\t\tpeerToMTP(participantPeer->id),\n\t\t\t\t\tMTP_int(participant->date),\n\t\t\t\t\tMTP_int(participant->lastActive),\n\t\t\t\t\tMTP_int(participant->ssrc),\n\t\t\t\t\tMTP_int(volume.value_or(participant->volume)),\n\t\t\t\t\tMTPstring(), // Don't update about text in local updates.\n\t\t\t\t\tMTP_long(participant->raisedHandRating),\n\t\t\t\t\tMTPGroupCallParticipantVideo(),\n\t\t\t\t\tMTPGroupCallParticipantVideo(),\n\t\t\t\t\tMTPlong())),\n\t\t\tMTP_int(0)).c_updateGroupCallParticipants());\n}\n\nvoid GroupCall::hangup() {\n\tfinish(FinishType::Ended);\n}\n\nvoid GroupCall::discard() {\n\tif (!_id) {\n\t\t_api.request(_createRequestId).cancel();\n\t\thangup();\n\t\treturn;\n\t}\n\t_api.request(MTPphone_DiscardGroupCall(\n\t\tinputCall()\n\t)).done([=](const MTPUpdates &result) {\n\t\t// Here 'this' could be destroyed by updates, so we set Ended after\n\t\t// updates being handled, but in a guarded way.\n\t\tcrl::on_main(this, [=] { hangup(); });\n\t\t_peer->session().api().applyUpdates(result);\n\t}).fail([=] {\n\t\thangup();\n\t}).send();\n}\n\nvoid GroupCall::rejoinAs(Group::JoinInfo info) {\n\t_possibleJoinAs = std::move(info.possibleJoinAs);\n\tif (info.joinAs == joinAs()) {\n\t\treturn;\n\t}\n\tconst auto event = Group::RejoinEvent{\n\t\t.wasJoinAs = joinAs(),\n\t\t.nowJoinAs = info.joinAs,\n\t};\n\tif (_scheduleDate) {\n\t\tsaveDefaultJoinAs(info.joinAs);\n\t} else {\n\t\tsetState(State::Joining);\n\t\trejoin(info.joinAs);\n\t}\n\t_rejoinEvents.fire_copy(event);\n}\n\nvoid GroupCall::finish(FinishType type) {\n\tExpects(type != FinishType::None);\n\n\tconst auto finalState = (type == FinishType::Ended)\n\t\t? State::Ended\n\t\t: State::Failed;\n\tconst auto hangupState = (type == FinishType::Ended)\n\t\t? State::HangingUp\n\t\t: State::FailedHangingUp;\n\tconst auto state = _state.current();\n\tif (state == State::HangingUp\n\t\t|| state == State::FailedHangingUp\n\t\t|| state == State::Ended\n\t\t|| state == State::Failed) {\n\t\treturn;\n\t} else if (_joinState.action == JoinAction::None && !_joinState.ssrc) {\n\t\tsetState(finalState);\n\t\treturn;\n\t}\n\tsetState(hangupState);\n\t_joinState.nextActionPending = true;\n\tcheckNextJoinAction();\n}\n\nvoid GroupCall::leave() {\n\tExpects(_joinState.action == JoinAction::None);\n\n\t_joinState.action = JoinAction::Leaving;\n\n\tconst auto finalState = (_state.current() == State::HangingUp)\n\t\t? State::Ended\n\t\t: State::Failed;\n\n\t// We want to leave request still being sent and processed even if\n\t// the call is already destroyed.\n\tconst auto session = &_peer->session();\n\tconst auto weak = base::make_weak(this);\n\tsession->api().request(MTPphone_LeaveGroupCall(\n\t\tinputCall(),\n\t\tMTP_int(base::take(_joinState.ssrc))\n\t)).done([=](const MTPUpdates &result) {\n\t\t// Here 'this' could be destroyed by updates, so we set Ended after\n\t\t// updates being handled, but in a guarded way.\n\t\tcrl::on_main(weak, [=] { setState(finalState); });\n\t\tsession->api().applyUpdates(result);\n\t}).fail(crl::guard(weak, [=] {\n\t\tsetState(finalState);\n\t})).send();\n}\n\nvoid GroupCall::startScheduledNow() {\n\tif (!lookupReal()) {\n\t\treturn;\n\t}\n\t_api.request(MTPphone_StartScheduledGroupCall(\n\t\tinputCall()\n\t)).done([=](const MTPUpdates &result) {\n\t\t_peer->session().api().applyUpdates(result);\n\t}).send();\n}\n\nvoid GroupCall::toggleScheduleStartSubscribed(bool subscribed) {\n\tif (!lookupReal()) {\n\t\treturn;\n\t}\n\t_api.request(MTPphone_ToggleGroupCallStartSubscription(\n\t\tinputCall(),\n\t\tMTP_bool(subscribed)\n\t)).done([=](const MTPUpdates &result) {\n\t\t_peer->session().api().applyUpdates(result);\n\t}).send();\n}\n\nvoid GroupCall::setNoiseSuppression(bool enabled) {\n\tif (_instance) {\n\t\t_instance->setIsNoiseSuppressionEnabled(enabled);\n\t}\n}\n\nvoid GroupCall::addVideoOutput(\n\t\tconst std::string &endpoint,\n\t\tnot_null<Webrtc::VideoTrack*> track) {\n\taddVideoOutput(endpoint, { track->sink() });\n}\n\nvoid GroupCall::setMuted(MuteState mute) {\n\tconst auto set = [=] {\n\t\tconst auto was = muted();\n\t\tconst auto wasSpeaking = (was == MuteState::Active)\n\t\t\t|| (was == MuteState::PushToTalk);\n\t\tconst auto wasMuted = (was == MuteState::Muted)\n\t\t\t|| (was == MuteState::PushToTalk);\n\t\tconst auto wasRaiseHand = (was == MuteState::RaisedHand);\n\t\t_muted = mute;\n\t\tconst auto now = muted();\n\t\tconst auto nowSpeaking = (now == MuteState::Active)\n\t\t\t|| (now == MuteState::PushToTalk);\n\t\tconst auto nowMuted = (now == MuteState::Muted)\n\t\t\t|| (now == MuteState::PushToTalk);\n\t\tconst auto nowRaiseHand = (now == MuteState::RaisedHand);\n\t\tif (wasMuted != nowMuted || wasRaiseHand != nowRaiseHand) {\n\t\t\tapplyMeInCallLocally();\n\t\t}\n\t\tif (mutedByAdmin()) {\n\t\t\ttoggleVideo(false);\n\t\t\ttoggleScreenSharing(std::nullopt);\n\t\t}\n\t\tif (wasSpeaking && !nowSpeaking && _joinState.ssrc) {\n\t\t\t_levelUpdates.fire(LevelUpdate{\n\t\t\t\t.ssrc = _joinState.ssrc,\n\t\t\t\t.value = 0.f,\n\t\t\t\t.voice = false,\n\t\t\t\t.me = true,\n\t\t\t});\n\t\t}\n\t};\n\tif (mute == MuteState::Active || mute == MuteState::PushToTalk) {\n\t\t_delegate->groupCallRequestPermissionsOrFail(crl::guard(this, set));\n\t} else {\n\t\tset();\n\t}\n}\n\nvoid GroupCall::setMutedAndUpdate(MuteState mute) {\n\tconst auto was = muted();\n\n\t// Active state is sent from _muted changes,\n\t// because it may be set delayed, after permissions request, not now.\n\tconst auto send = _initialMuteStateSent && (mute != MuteState::Active);\n\tsetMuted(mute);\n\tif (send) {\n\t\tmaybeSendMutedUpdate(was);\n\t}\n}\n\nvoid GroupCall::handlePossibleCreateOrJoinResponse(\n\t\tconst MTPDupdateGroupCall &data) {\n\tdata.vcall().match([&](const MTPDgroupCall &data) {\n\t\thandlePossibleCreateOrJoinResponse(data);\n\t}, [&](const MTPDgroupCallDiscarded &data) {\n\t\thandlePossibleDiscarded(data);\n\t});\n}\n\nvoid GroupCall::handlePossibleCreateOrJoinResponse(\n\t\tconst MTPDgroupCall &data) {\n\tif (_acceptFields) {\n\t\tif (!_instance && !_id) {\n\t\t\tconst auto input = MTP_inputGroupCall(\n\t\t\t\tdata.vid(),\n\t\t\t\tdata.vaccess_hash());\n\t\t\tconst auto scheduleDate = data.vschedule_date().value_or_empty();\n\t\t\tconst auto rtmp = data.is_rtmp_stream();\n\t\t\t_rtmp = rtmp;\n\t\t\tsetScheduledDate(scheduleDate);\n\t\t\tif (!conference()) {\n\t\t\t\tsetMessagesEnabled(data.is_messages_enabled());\n\t\t\t}\n\t\t\tif (const auto chat = _peer->asChat()) {\n\t\t\t\tchat->setGroupCall(input, scheduleDate, rtmp);\n\t\t\t} else if (const auto group = _peer->asChannel()) {\n\t\t\t\tgroup->setGroupCall(input, scheduleDate, rtmp);\n\t\t\t} else {\n\t\t\t\tUnexpected(\"Peer type in GroupCall::join.\");\n\t\t\t}\n\t\t\tapplyInputCall(input);\n\t\t\tinitialJoin();\n\t\t}\n\t\treturn;\n\t} else if (_id != data.vid().v || !_instance) {\n\t\treturn;\n\t}\n\tsetScheduledDate(data.vschedule_date().value_or_empty());\n\tif (!conference()) {\n\t\tsetMessagesEnabled(data.is_messages_enabled());\n\t}\n\tif (const auto streamDcId = data.vstream_dc_id()) {\n\t\t_broadcastDcId = MTP::BareDcId(streamDcId->v);\n\t}\n}\n\nvoid GroupCall::handlePossibleCreateOrJoinResponse(\n\t\tconst MTPDupdateGroupCallConnection &data) {\n\tif (data.is_presentation()) {\n\t\tif (!_screenInstance) {\n\t\t\treturn;\n\t\t}\n\t\tsetScreenInstanceMode(InstanceMode::Rtc);\n\t\tdata.vparams().match([&](const MTPDdataJSON &data) {\n\t\t\tconst auto json = data.vdata().v;\n\t\t\tconst auto response = ParseJoinResponse(json);\n\t\t\tconst auto endpoint = std::get_if<JoinVideoEndpoint>(&response);\n\t\t\tif (endpoint) {\n\t\t\t\tsetScreenEndpoint(endpoint->id);\n\t\t\t} else {\n\t\t\t\tLOG((\"Call Error: Bad response for 'presentation' flag.\"));\n\t\t\t}\n\t\t\t_screenInstance->setJoinResponsePayload(json.toStdString());\n\t\t});\n\t} else {\n\t\tif (!_instance) {\n\t\t\treturn;\n\t\t}\n\t\tdata.vparams().match([&](const MTPDdataJSON &data) {\n\t\t\tconst auto json = data.vdata().v;\n\t\t\tconst auto response = ParseJoinResponse(json);\n\t\t\tconst auto stream = std::get_if<JoinBroadcastStream>(&response);\n\t\t\tconst auto endpoint = std::get_if<JoinVideoEndpoint>(&response);\n\t\t\tif (stream) {\n\t\t\t\tif (!_broadcastDcId) {\n\t\t\t\t\tLOG((\"Api Error: Empty stream_dc_id in groupCall.\"));\n\t\t\t\t\t_broadcastDcId = _peer->session().mtp().mainDcId();\n\t\t\t\t}\n\t\t\t\tif (stream->rtmp) {\n\t\t\t\t\t_rtmp = true;\n\t\t\t\t\t_rtmpUrl = stream->rtmpInfo.url;\n\t\t\t\t\t_rtmpKey = stream->rtmpInfo.key;\n\t\t\t\t}\n\t\t\t\tsetInstanceMode(InstanceMode::Stream);\n\t\t\t} else {\n\t\t\t\tsetInstanceMode(InstanceMode::Rtc);\n\t\t\t\tsetCameraEndpoint(endpoint ? endpoint->id : std::string());\n\t\t\t\t_instance->setJoinResponsePayload(json.toStdString());\n\t\t\t}\n\t\t\tupdateInstanceVolumes();\n\t\t\tfillActiveVideoEndpoints();\n\t\t\tupdateRequestedVideoChannels();\n\t\t\tcheckMediaChannelDescriptions();\n\t\t});\n\t}\n}\n\nvoid GroupCall::handleIncomingMessage(\n\t\tconst MTPDupdateGroupCallMessage &data) {\n\tconst auto id = data.vcall().match([&](const MTPDinputGroupCall &data) {\n\t\treturn data.vid().v;\n\t}, [](const auto &) -> CallId {\n\t\tUnexpected(\"slug/msg in GroupCall::handleIncomingMessage\");\n\t});\n\tif (id != _id || conference()) {\n\t\treturn;\n\t}\n\t_messages->received(data);\n}\n\nvoid GroupCall::handleIncomingMessage(\n\t\tconst MTPDupdateGroupCallEncryptedMessage &data) {\n\tconst auto id = data.vcall().match([&](const MTPDinputGroupCall &data) {\n\t\treturn data.vid().v;\n\t}, [](const auto &) -> CallId {\n\t\tUnexpected(\"slug/msg in GroupCall::handleIncomingMessage\");\n\t});\n\tif (id != _id || !conference()) {\n\t\treturn;\n\t}\n\t_messages->received(data);\n}\n\nvoid GroupCall::handleDeleteMessages(\n\t\tconst MTPDupdateDeleteGroupCallMessages &data) {\n\tconst auto id = data.vcall().match([&](const MTPDinputGroupCall &data) {\n\t\treturn data.vid().v;\n\t}, [](const auto &) -> CallId {\n\t\tUnexpected(\"slug/msg in GroupCall::handleIncomingMessage\");\n\t});\n\tif (id != _id || conference()) {\n\t\treturn;\n\t}\n\t_messages->deleted(data);\n}\n\nvoid GroupCall::handleMessageSent(const MTPDupdateMessageID &data) {\n\tif (conference()) {\n\t\treturn;\n\t}\n\t_messages->sent(data);\n}\n\nvoid GroupCall::handlePossibleDiscarded(const MTPDgroupCallDiscarded &data) {\n\tif (data.vid().v == _id) {\n\t\tLOG((\"Call Info: Hangup after groupCallDiscarded.\"));\n\t\t_joinState.finish();\n\t\thangup();\n\t}\n}\n\nvoid GroupCall::checkMediaChannelDescriptions(\n\t\tFn<bool(uint32)> resolved) {\n\tconst auto real = lookupReal();\n\tif (!real || (_instanceMode == InstanceMode::None)) {\n\t\treturn;\n\t}\n\tfor (auto i = begin(_mediaChannelDescriptionses)\n\t\t; i != end(_mediaChannelDescriptionses);) {\n\t\tif (mediaChannelDescriptionsFill(i->get(), resolved)) {\n\t\t\ti = _mediaChannelDescriptionses.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tif (!_unresolvedSsrcs.empty()) {\n\t\treal->resolveParticipants(base::take(_unresolvedSsrcs));\n\t}\n}\n\nvoid GroupCall::handleUpdate(const MTPUpdate &update) {\n\tupdate.match([&](const MTPDupdateGroupCall &data) {\n\t\thandleUpdate(data);\n\t}, [&](const MTPDupdateGroupCallParticipants &data) {\n\t\thandleUpdate(data);\n\t}, [&](const MTPDupdateGroupCallChainBlocks &data) {\n\t\thandleUpdate(data);\n\t}, [](const auto &) {\n\t\tUnexpected(\"Type in Instance::applyGroupCallUpdateChecked.\");\n\t});\n}\n\nvoid GroupCall::handleUpdate(const MTPDupdateGroupCall &data) {\n\tdata.vcall().match([](const MTPDgroupCall &) {\n\t}, [&](const MTPDgroupCallDiscarded &data) {\n\t\thandlePossibleDiscarded(data);\n\t});\n}\n\nvoid GroupCall::handleUpdate(const MTPDupdateGroupCallParticipants &data) {\n\tconst auto callId = data.vcall().match([](\n\t\t\tconst MTPDinputGroupCall &data) {\n\t\treturn data.vid().v;\n\t}, [](const auto &) -> CallId {\n\t\tUnexpected(\"slug/msg in GroupCall::handleUpdate.\");\n\t});\n\tif (_id != callId) {\n\t\treturn;\n\t}\n\tconst auto state = _state.current();\n\tconst auto joined = (state == State::Joined)\n\t\t|| (state == State::Connecting);\n\tfor (const auto &participant : data.vparticipants().v) {\n\t\tparticipant.match([&](const MTPDgroupCallParticipant &data) {\n\t\t\tconst auto isSelf = data.is_self()\n\t\t\t\t|| (data.is_min()\n\t\t\t\t\t&& peerFromMTP(data.vpeer()) == joinAs()->id);\n\t\t\tif (!isSelf) {\n\t\t\t\tapplyOtherParticipantUpdate(data);\n\t\t\t} else if (joined) {\n\t\t\t\tapplySelfUpdate(data);\n\t\t\t} else {\n\t\t\t\t_queuedSelfUpdates.push_back(participant);\n\t\t\t}\n\t\t});\n\t}\n}\n\nvoid GroupCall::handleUpdate(const MTPDupdateGroupCallChainBlocks &data) {\n\tconst auto callId = data.vcall().match([](\n\t\t\tconst MTPDinputGroupCall &data) {\n\t\treturn data.vid().v;\n\t}, [](const auto &) -> CallId {\n\t\tUnexpected(\"slug/msg in GroupCall::handleUpdate.\");\n\t});\n\tif (_id != callId || !_e2e) {\n\t\treturn;\n\t}\n\tconst auto subchain = data.vsub_chain_id().v;\n\tif (subchain < 0 || subchain >= kSubChainsCount) {\n\t\treturn;\n\t}\n\tauto &entry = _subchains[subchain];\n\tconst auto &blocks = data.vblocks().v;\n\tconst auto next = data.vnext_offset().v;\n\tif (entry.requestId) {\n\t\tAssert(!entry.inShortPoll);\n\t\tentry.pending.push_back({ blocks, next });\n\t} else {\n\t\tapplySubChainUpdate(subchain, blocks, next);\n\t}\n}\n\nvoid GroupCall::applySubChainUpdate(\n\t\tint subchain,\n\t\tconst QVector<MTPbytes> &blocks,\n\t\tint next) {\n\tExpects(subchain >= 0 && subchain < kSubChainsCount);\n\n\tauto &entry = _subchains[subchain];\n\tauto raw = std::vector<TdE2E::Block>();\n\traw.reserve(blocks.size());\n\tfor (const auto &block : blocks) {\n\t\traw.push_back({ block.v });\n\t}\n\t_e2e->apply(subchain, next, raw, entry.inShortPoll);\n}\n\nvoid GroupCall::applyQueuedSelfUpdates() {\n\tconst auto weak = base::make_weak(this);\n\twhile (weak\n\t\t&& !_queuedSelfUpdates.empty()\n\t\t&& (_state.current() == State::Joined\n\t\t\t|| _state.current() == State::Connecting)) {\n\t\tconst auto update = _queuedSelfUpdates.front();\n\t\t_queuedSelfUpdates.erase(_queuedSelfUpdates.begin());\n\t\tupdate.match([&](const MTPDgroupCallParticipant &data) {\n\t\t\tapplySelfUpdate(data);\n\t\t});\n\t}\n}\n\nvoid GroupCall::applySelfUpdate(const MTPDgroupCallParticipant &data) {\n\tif (data.is_left()) {\n\t\tif (data.vsource().v == _joinState.ssrc) {\n\t\t\t// I was removed from the call, rejoin.\n\t\t\tLOG((\"Call Info: \"\n\t\t\t\t\"Rejoin after got 'left' with my ssrc.\"));\n\t\t\tstartRejoin();\n\t\t}\n\t\treturn;\n\t} else if (data.vsource().v != _joinState.ssrc) {\n\t\tconst auto ssrc = uint32(data.vsource().v);\n\t\tif (!_mySsrcs.contains(ssrc)) {\n\t\t\t// I joined from another device, hangup.\n\t\t\tLOG((\"Call Info: \"\n\t\t\t\t\"Hangup after '!left' with ssrc %1, my %2.\"\n\t\t\t\t).arg(data.vsource().v\n\t\t\t\t).arg(_joinState.ssrc));\n\t\t\t_joinState.finish();\n\t\t\thangup();\n\t\t} else {\n\t\t\tLOG((\"Call Info: \"\n\t\t\t\t\"Some old 'self' with '!left' and ssrc %1, my %2.\"\n\t\t\t\t).arg(data.vsource().v\n\t\t\t\t).arg(_joinState.ssrc));\n\t\t}\n\t\treturn;\n\t}\n\tif (data.is_muted() && !data.is_can_self_unmute()) {\n\t\tsetMuted(data.vraise_hand_rating().value_or_empty()\n\t\t\t? MuteState::RaisedHand\n\t\t\t: MuteState::ForceMuted);\n\t} else if (_instanceMode == InstanceMode::Stream) {\n\t\tLOG((\"Call Info: Rejoin after unforcemute in stream mode.\"));\n\t\tstartRejoin();\n\t} else if (mutedByAdmin()) {\n\t\tsetMuted(MuteState::Muted);\n\t\tif (!_instanceTransitioning) {\n\t\t\tnotifyAboutAllowedToSpeak();\n\t\t}\n\t} else if (data.is_muted() && muted() != MuteState::Muted) {\n\t\tsetMuted(MuteState::Muted);\n\t}\n}\n\nvoid GroupCall::applyOtherParticipantUpdate(\n\t\tconst MTPDgroupCallParticipant &data) {\n\tif (data.is_min()) {\n\t\t// No real information about mutedByMe or my custom volume.\n\t\treturn;\n\t}\n\tconst auto participantPeer = _peer->owner().peer(\n\t\tpeerFromMTP(data.vpeer()));\n\tif (!LookupParticipant(this, participantPeer)) {\n\t\treturn;\n\t}\n\t_otherParticipantStateValue.fire(Group::ParticipantState{\n\t\t.peer = participantPeer,\n\t\t.volume = data.vvolume().value_or_empty(),\n\t\t.mutedByMe = data.is_muted_by_you(),\n\t});\n}\n\nvoid GroupCall::setupMediaDevices() {\n\t_playbackDeviceId.changes() | rpl::filter([=] {\n\t\treturn _instance && _setDeviceIdCallback;\n\t}) | rpl::on_next([=](const Webrtc::DeviceResolvedId &deviceId) {\n\t\t_setDeviceIdCallback(deviceId);\n\n\t\t// Value doesn't matter here, just trigger reading of the new value.\n\t\t_instance->setAudioOutputDevice(deviceId.value.toStdString());\n\t}, _lifetime);\n\n\t_captureDeviceId.changes() | rpl::filter([=] {\n\t\treturn _instance && _setDeviceIdCallback;\n\t}) | rpl::on_next([=](const Webrtc::DeviceResolvedId &deviceId) {\n\t\t_setDeviceIdCallback(deviceId);\n\n\t\t// Value doesn't matter here, just trigger reading of the new value.\n\t\t_instance->setAudioInputDevice(deviceId.value.toStdString());\n\t}, _lifetime);\n\n\t_cameraDeviceId.changes() | rpl::filter([=] {\n\t\treturn _cameraCapture != nullptr;\n\t}) | rpl::on_next([=](const Webrtc::DeviceResolvedId &deviceId) {\n\t\t_cameraCapture->switchToDevice(deviceId.value.toStdString(), false);\n\t}, _lifetime);\n\n\tif (!_rtmp) {\n\t\t_muted.value() | rpl::on_next([=](MuteState state) {\n\t\t\tconst auto devices = &Core::App().mediaDevices();\n\t\t\tconst auto muted = (state != MuteState::Active)\n\t\t\t\t&& (state != MuteState::PushToTalk);\n\t\t\tconst auto track = !muted || (state == MuteState::Muted);\n\t\t\tdevices->setCaptureMuteTracker(this, track);\n\t\t\tdevices->setCaptureMuted(muted);\n\t\t}, _lifetime);\n\t}\n}\n\nvoid GroupCall::captureMuteChanged(bool mute) {\n\tconst auto oldState = muted();\n\tif (mute\n\t\t&& (oldState == MuteState::ForceMuted\n\t\t\t|| oldState == MuteState::RaisedHand\n\t\t\t|| oldState == MuteState::Muted)) {\n\t\treturn;\n\t} else if (!mute && oldState != MuteState::Muted) {\n\t\treturn;\n\t}\n\tsetMutedAndUpdate(mute ? MuteState::Muted : MuteState::Active);\n}\n\nrpl::producer<Webrtc::DeviceResolvedId> GroupCall::captureMuteDeviceId() {\n\treturn _captureDeviceId.value();\n}\n\nint GroupCall::activeVideoSendersCount() const {\n\tauto result = 0;\n\tfor (const auto &[endpoint, track] : _activeVideoTracks) {\n\t\tif (endpoint.type == VideoEndpointType::Camera) {\n\t\t\t++result;\n\t\t} else {\n\t\t\tauto sharesCameraToo = false;\n\t\t\tfor (const auto &[other, _] : _activeVideoTracks) {\n\t\t\t\tif (other.type == VideoEndpointType::Camera\n\t\t\t\t\t&& other.peer == endpoint.peer) {\n\t\t\t\t\tsharesCameraToo = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!sharesCameraToo) {\n\t\t\t\t++result;\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nbool GroupCall::emitShareCameraError() {\n\tconst auto emitError = [=](Error error) {\n\t\temitShareCameraError(error);\n\t\treturn true;\n\t};\n\tif (const auto real = lookupReal()\n\t\t; real && activeVideoSendersCount() >= real->unmutedVideoLimit()) {\n\t\treturn emitError(Error::DisabledNoCamera);\n\t} else if (!videoIsWorking()) {\n\t\treturn emitError(Error::DisabledNoCamera);\n\t} else if (mutedByAdmin()) {\n\t\treturn emitError(Error::MutedNoCamera);\n\t} else if (_cameraDeviceId.current().value.isEmpty()) {\n\t\treturn emitError(Error::NoCamera);\n\t}\n\treturn false;\n}\n\nvoid GroupCall::emitShareCameraError(Error error) {\n\t_cameraState = Webrtc::VideoState::Inactive;\n\tif (error == Error::CameraFailed\n\t\t&& _cameraDeviceId.current().value.isEmpty()) {\n\t\terror = Error::NoCamera;\n\t}\n\t_errors.fire_copy(error);\n}\n\nbool GroupCall::emitShareScreenError() {\n\tconst auto emitError = [=](Error error) {\n\t\temitShareScreenError(error);\n\t\treturn true;\n\t};\n\tif (const auto real = lookupReal()\n\t\t; real && activeVideoSendersCount() >= real->unmutedVideoLimit()) {\n\t\treturn emitError(Error::DisabledNoScreen);\n\t} else if (!videoIsWorking()) {\n\t\treturn emitError(Error::DisabledNoScreen);\n\t} else if (mutedByAdmin()) {\n\t\treturn emitError(Error::MutedNoScreen);\n\t}\n\treturn false;\n}\n\nvoid GroupCall::emitShareScreenError(Error error) {\n\t_screenState = Webrtc::VideoState::Inactive;\n\t_errors.fire_copy(error);\n}\n\nvoid GroupCall::playSoundRecordingStarted() const {\n\t_delegate->groupCallPlaySound(Delegate::GroupCallSound::RecordingStarted);\n}\n\nvoid GroupCall::setupOutgoingVideo() {\n\tusing Webrtc::VideoState;\n\n\t_cameraState.value(\n\t) | rpl::combine_previous(\n\t) | rpl::filter([=](VideoState previous, VideoState state) {\n\t\t// Recursive entrance may happen if error happens when activating.\n\t\treturn (previous != state);\n\t}) | rpl::on_next([=](VideoState previous, VideoState state) {\n\t\tconst auto wasActive = (previous != VideoState::Inactive);\n\t\tconst auto nowPaused = (state == VideoState::Paused);\n\t\tconst auto nowActive = (state != VideoState::Inactive);\n\t\tif (wasActive == nowActive) {\n\t\t\tAssert(wasActive && nowActive);\n\t\t\tsendSelfUpdate(SendUpdateType::CameraPaused);\n\t\t\tmarkTrackPaused({\n\t\t\t\tVideoEndpointType::Camera,\n\t\t\t\tjoinAs(),\n\t\t\t\t_cameraEndpoint\n\t\t\t}, nowPaused);\n\t\t\treturn;\n\t\t}\n\t\tif (nowActive) {\n\t\t\tif (emitShareCameraError()) {\n\t\t\t\treturn;\n\t\t\t} else if (!_cameraCapture) {\n\t\t\t\t_cameraCapture = _delegate->groupCallGetVideoCapture(\n\t\t\t\t\t_cameraDeviceId.current().value);\n\t\t\t\tif (!_cameraCapture) {\n\t\t\t\t\treturn emitShareCameraError(Error::CameraFailed);\n\t\t\t\t}\n\t\t\t\tconst auto weak = base::make_weak(this);\n\t\t\t\t_cameraCapture->setOnFatalError([=] {\n\t\t\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\t\t\temitShareCameraError(Error::CameraFailed);\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t_cameraCapture->switchToDevice(\n\t\t\t\t\t_cameraDeviceId.current().value.toStdString(),\n\t\t\t\t\tfalse);\n\t\t\t}\n\t\t\tif (_instance) {\n\t\t\t\t_instance->setVideoCapture(_cameraCapture);\n\t\t\t}\n\t\t\t_cameraCapture->setState(tgcalls::VideoState::Active);\n\t\t} else if (_cameraCapture) {\n\t\t\t_cameraCapture->setState(tgcalls::VideoState::Inactive);\n\t\t}\n\t\t_isSharingCamera = nowActive;\n\t\tmarkEndpointActive({\n\t\t\tVideoEndpointType::Camera,\n\t\t\tjoinAs(),\n\t\t\t_cameraEndpoint\n\t\t}, nowActive, nowPaused);\n\t\tsendSelfUpdate(SendUpdateType::CameraStopped);\n\t\tapplyMeInCallLocally();\n\t}, _lifetime);\n\n\t_screenState.value(\n\t) | rpl::combine_previous(\n\t) | rpl::filter([=](VideoState previous, VideoState state) {\n\t\t// Recursive entrance may happen if error happens when activating.\n\t\treturn (previous != state);\n\t}) | rpl::on_next([=](VideoState previous, VideoState state) {\n\t\tconst auto wasActive = (previous != VideoState::Inactive);\n\t\tconst auto nowPaused = (state == VideoState::Paused);\n\t\tconst auto nowActive = (state != VideoState::Inactive);\n\t\tif (wasActive == nowActive) {\n\t\t\tAssert(wasActive && nowActive);\n\t\t\tsendSelfUpdate(SendUpdateType::ScreenPaused);\n\t\t\tmarkTrackPaused({\n\t\t\t\tVideoEndpointType::Screen,\n\t\t\t\tjoinAs(),\n\t\t\t\t_screenEndpoint\n\t\t\t}, nowPaused);\n\t\t\treturn;\n\t\t}\n\t\tif (nowActive) {\n\t\t\tif (emitShareScreenError()) {\n\t\t\t\treturn;\n\t\t\t} else if (!_screenCapture) {\n\t\t\t\t_screenCapture = std::shared_ptr<\n\t\t\t\t\ttgcalls::VideoCaptureInterface\n\t\t\t\t>(tgcalls::VideoCaptureInterface::Create(\n\t\t\t\t\ttgcalls::StaticThreads::getThreads(),\n\t\t\t\t\t_screenDeviceId.toStdString()));\n\t\t\t\tif (!_screenCapture) {\n\t\t\t\t\treturn emitShareScreenError(Error::ScreenFailed);\n\t\t\t\t}\n\t\t\t\tconst auto weak = base::make_weak(this);\n\t\t\t\t_screenCapture->setOnFatalError([=] {\n\t\t\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\t\t\temitShareScreenError(Error::ScreenFailed);\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t\t_screenCapture->setOnPause([=](bool paused) {\n\t\t\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\t\t\tif (isSharingScreen()) {\n\t\t\t\t\t\t\t_screenState = paused\n\t\t\t\t\t\t\t\t? VideoState::Paused\n\t\t\t\t\t\t\t\t: VideoState::Active;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t_screenCapture->switchToDevice(\n\t\t\t\t\t_screenDeviceId.toStdString(),\n\t\t\t\t\ttrue);\n\t\t\t}\n\t\t\tif (_screenInstance) {\n\t\t\t\t_screenInstance->setVideoCapture(_screenCapture);\n\t\t\t}\n\t\t\t_screenCapture->setState(tgcalls::VideoState::Active);\n\t\t} else if (_screenCapture) {\n\t\t\t_screenCapture->setState(tgcalls::VideoState::Inactive);\n\t\t}\n\t\t_isSharingScreen = nowActive;\n\t\tmarkEndpointActive({\n\t\t\tVideoEndpointType::Screen,\n\t\t\tjoinAs(),\n\t\t\t_screenEndpoint\n\t\t}, nowActive, nowPaused);\n\t\t_screenJoinState.nextActionPending = true;\n\t\tcheckNextJoinAction();\n\t}, _lifetime);\n}\n\nvoid GroupCall::changeTitle(const QString &title) {\n\tconst auto real = lookupReal();\n\tif (!real || real->title() == title) {\n\t\treturn;\n\t}\n\n\t_api.request(MTPphone_EditGroupCallTitle(\n\t\tinputCall(),\n\t\tMTP_string(title)\n\t)).done([=](const MTPUpdates &result) {\n\t\t_peer->session().api().applyUpdates(result);\n\t\t_titleChanged.fire({});\n\t}).send();\n}\n\nvoid GroupCall::toggleRecording(\n\t\tbool enabled,\n\t\tconst QString &title,\n\t\tbool video,\n\t\tbool videoPortrait) {\n\tconst auto real = lookupReal();\n\tif (!real) {\n\t\treturn;\n\t}\n\n\tconst auto already = (real->recordStartDate() != 0);\n\tif (already == enabled) {\n\t\treturn;\n\t}\n\n\tif (!enabled) {\n\t\t_recordingStoppedByMe = true;\n\t}\n\tusing Flag = MTPphone_ToggleGroupCallRecord::Flag;\n\t_api.request(MTPphone_ToggleGroupCallRecord(\n\t\tMTP_flags((enabled ? Flag::f_start : Flag(0))\n\t\t\t| (video ? Flag::f_video : Flag(0))\n\t\t\t| (title.isEmpty() ? Flag(0) : Flag::f_title)),\n\t\tinputCall(),\n\t\tMTP_string(title),\n\t\tMTP_bool(videoPortrait)\n\t)).done([=](const MTPUpdates &result) {\n\t\t_peer->session().api().applyUpdates(result);\n\t\t_recordingStoppedByMe = false;\n\t}).fail([=] {\n\t\t_recordingStoppedByMe = false;\n\t}).send();\n}\n\nauto GroupCall::lookupVideoCodecPreferences() const\n-> std::vector<tgcalls::VideoCodecName> {\n\tauto result = std::vector<tgcalls::VideoCodecName>();\n\tif (_peer->session().appConfig().confcallPrioritizeVP8()) {\n\t\tresult.push_back(tgcalls::VideoCodecName::VP8);\n\t}\n\treturn result;\n}\n\nbool GroupCall::tryCreateController() {\n\tif (_instance) {\n\t\treturn false;\n\t}\n\tconst auto &settings = Core::App().settings();\n\n\tconst auto weak = base::make_weak(&_instanceGuard);\n\tconst auto myLevel = std::make_shared<tgcalls::GroupLevelValue>();\n\tconst auto playbackDeviceIdInitial = _playbackDeviceId.current();\n\tconst auto captureDeviceIdInitial = _captureDeviceId.current();\n\tconst auto saveSetDeviceIdCallback = [=](\n\t\t\tFn<void(Webrtc::DeviceResolvedId)> setDeviceIdCallback) {\n\t\tsetDeviceIdCallback(playbackDeviceIdInitial);\n\t\tsetDeviceIdCallback(captureDeviceIdInitial);\n\t\tcrl::on_main(weak, [=] {\n\t\t\t_setDeviceIdCallback = std::move(setDeviceIdCallback);\n\t\t\tconst auto playback = _playbackDeviceId.current();\n\t\t\tif (_instance && playback != playbackDeviceIdInitial) {\n\t\t\t\t_setDeviceIdCallback(playback);\n\n\t\t\t\t// Value doesn't matter here, just trigger reading of the...\n\t\t\t\t_instance->setAudioOutputDevice(\n\t\t\t\t\tplayback.value.toStdString());\n\t\t\t}\n\t\t\tconst auto capture = _captureDeviceId.current();\n\t\t\tif (_instance && capture != captureDeviceIdInitial) {\n\t\t\t\t_setDeviceIdCallback(capture);\n\n\t\t\t\t// Value doesn't matter here, just trigger reading of the...\n\t\t\t\t_instance->setAudioInputDevice(capture.value.toStdString());\n\t\t\t}\n\t\t});\n\t};\n\n\ttgcalls::GroupInstanceDescriptor descriptor = {\n\t\t.threads = tgcalls::StaticThreads::getThreads(),\n\t\t.config = tgcalls::GroupConfig{\n\t\t},\n\t\t.networkStateUpdated = [=](tgcalls::GroupNetworkState networkState) {\n\t\t\tcrl::on_main(weak, [=] { setInstanceConnected(networkState); });\n\t\t},\n\t\t.audioLevelsUpdated = [=](const tgcalls::GroupLevelsUpdate &data) {\n\t\t\tconst auto &updates = data.updates;\n\t\t\tif (updates.empty()) {\n\t\t\t\treturn;\n\t\t\t} else if (updates.size() == 1 && !updates.front().ssrc) {\n\t\t\t\tconst auto &value = updates.front().value;\n\t\t\t\t// Don't send many 0 while we're muted.\n\t\t\t\tif (myLevel->level == value.level\n\t\t\t\t\t&& myLevel->voice == value.voice) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t*myLevel = updates.front().value;\n\t\t\t}\n\t\t\tcrl::on_main(weak, [=] { audioLevelsUpdated(data); });\n\t\t},\n\t\t.initialInputDeviceId = captureDeviceIdInitial.value.toStdString(),\n\t\t.initialOutputDeviceId = playbackDeviceIdInitial.value.toStdString(),\n\t\t.createAudioDeviceModule = Webrtc::AudioDeviceModuleCreator(\n\t\t\tsaveSetDeviceIdCallback),\n\t\t.videoCapture = _cameraCapture,\n\t\t.requestCurrentTime = [=, call = base::make_weak(this)](\n\t\t\t\tstd::function<void(int64_t)> done) {\n\t\t\tauto result = std::make_shared<RequestCurrentTimeTask>(\n\t\t\t\tcall,\n\t\t\t\tstd::move(done));\n\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\trequestCurrentTimeStart(std::move(result));\n\t\t\t});\n\t\t\treturn result;\n\t\t},\n\t\t.requestAudioBroadcastPart = [=, call = base::make_weak(this)](\n\t\t\t\tint64_t time,\n\t\t\t\tint64_t period,\n\t\t\t\tstd::function<void(tgcalls::BroadcastPart &&)> done) {\n\t\t\tauto result = std::make_shared<LoadPartTask>(\n\t\t\t\tcall,\n\t\t\t\ttime,\n\t\t\t\tperiod,\n\t\t\t\tstd::move(done));\n\t\t\tcrl::on_main(weak, [=]() mutable {\n\t\t\t\tbroadcastPartStart(std::move(result));\n\t\t\t});\n\t\t\treturn result;\n\t\t},\n\t\t.requestVideoBroadcastPart = [=, call = base::make_weak(this)](\n\t\t\t\tint64_t time,\n\t\t\t\tint64_t period,\n\t\t\t\tint32_t channel,\n\t\t\t\ttgcalls::VideoChannelDescription::Quality quality,\n\t\t\t\tstd::function<void(tgcalls::BroadcastPart &&)> done) {\n\t\t\tauto result = std::make_shared<LoadPartTask>(\n\t\t\t\tcall,\n\t\t\t\ttime,\n\t\t\t\tperiod,\n\t\t\t\tchannel,\n\t\t\t\tquality,\n\t\t\t\tstd::move(done));\n\t\t\tcrl::on_main(weak, [=]() mutable {\n\t\t\t\tbroadcastPartStart(std::move(result));\n\t\t\t});\n\t\t\treturn result;\n\t\t},\n\t\t.videoContentType = tgcalls::VideoContentType::Generic,\n\t\t.initialEnableNoiseSuppression\n\t\t\t= settings.groupCallNoiseSuppression(),\n\t\t.videoCodecPreferences = lookupVideoCodecPreferences(),\n\t\t.requestMediaChannelDescriptions = [=, call = base::make_weak(this)](\n\t\t\tconst std::vector<uint32_t> &ssrcs,\n\t\t\tstd::function<void(\n\t\t\t\tstd::vector<tgcalls::MediaChannelDescription> &&)> done) {\n\t\t\tauto result = std::make_shared<MediaChannelDescriptionsTask>(\n\t\t\t\tcall,\n\t\t\t\tssrcs,\n\t\t\t\tstd::move(done));\n\t\t\tcrl::on_main(weak, [=]() mutable {\n\t\t\t\tmediaChannelDescriptionsStart(std::move(result));\n\t\t\t});\n\t\t\treturn result;\n\t\t},\n\t\t.e2eEncryptDecrypt = e2eEncryptDecrypt(),\n\t};\n\tif (Logs::DebugEnabled()) {\n\t\tauto callLogFolder = cWorkingDir() + u\"DebugLogs\"_q;\n\t\tauto callLogPath = callLogFolder + u\"/last_group_call_log.txt\"_q;\n\t\tauto callLogNative = QDir::toNativeSeparators(callLogPath);\n\t\tdescriptor.config.need_log = true;\n#ifdef Q_OS_WIN\n\t\tdescriptor.config.logPath.data = callLogNative.toStdWString();\n#else // Q_OS_WIN\n\t\tconst auto callLogUtf = QFile::encodeName(callLogNative);\n\t\tdescriptor.config.logPath.data.resize(callLogUtf.size());\n\t\tranges::copy(callLogUtf, descriptor.config.logPath.data.begin());\n#endif // Q_OS_WIN\n\t\tQFile(callLogPath).remove();\n\t\tQDir().mkpath(callLogFolder);\n\t} else {\n\t\tdescriptor.config.need_log = false;\n\t}\n\n\tLOG((\"Call Info: Creating group instance\"));\n\t_instance = std::make_unique<tgcalls::GroupInstanceCustomImpl>(\n\t\tstd::move(descriptor));\n\n\tupdateInstanceMuteState();\n\tupdateInstanceVolumes();\n\tfor (auto &[endpoint, sink] : base::take(_pendingVideoOutputs)) {\n\t\t_instance->addIncomingVideoOutput(endpoint, std::move(sink.data));\n\t}\n\t//raw->setAudioOutputDuckingEnabled(settings.callAudioDuckingEnabled());\n\treturn true;\n}\n\nbool GroupCall::tryCreateScreencast() {\n\tif (_screenInstance) {\n\t\treturn false;\n\t}\n\n\tconst auto weak = base::make_weak(&_screenInstanceGuard);\n\ttgcalls::GroupInstanceDescriptor descriptor = {\n\t\t.threads = tgcalls::StaticThreads::getThreads(),\n\t\t.config = tgcalls::GroupConfig{\n\t\t\t.need_log = Logs::DebugEnabled(),\n\t\t},\n\t\t.networkStateUpdated = [=](tgcalls::GroupNetworkState networkState) {\n\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\tsetScreenInstanceConnected(networkState);\n\t\t\t});\n\t\t},\n\t\t.createAudioDeviceModule = Webrtc::LoopbackAudioDeviceModuleCreator(),\n\t\t.videoCapture = _screenCapture,\n\t\t.videoContentType = tgcalls::VideoContentType::Screencast,\n\t\t.videoCodecPreferences = lookupVideoCodecPreferences(),\n\t\t.e2eEncryptDecrypt = e2eEncryptDecrypt(),\n\t};\n\n\tLOG((\"Call Info: Creating group screen instance\"));\n\t_screenInstance = std::make_unique<tgcalls::GroupInstanceCustomImpl>(\n\t\tstd::move(descriptor));\n\n\t_screenInstance->setIsMuted(!_screenWithAudio);\n\n\treturn true;\n}\n\nvoid GroupCall::broadcastPartStart(std::shared_ptr<LoadPartTask> task) {\n\tconst auto raw = task.get();\n\tconst auto time = raw->time();\n\tconst auto scale = raw->scale();\n\tconst auto videoChannel = raw->videoChannel();\n\tconst auto videoQuality = raw->videoQuality();\n\tconst auto finish = [=](tgcalls::BroadcastPart &&part) {\n\t\traw->done(std::move(part));\n\t\t_broadcastParts.erase(raw);\n\t};\n\tusing Status = tgcalls::BroadcastPart::Status;\n\tusing Quality = tgcalls::VideoChannelDescription::Quality;\n\tusing Flag = MTPDinputGroupCallStream::Flag;\n\tconst auto requestId = _api.request(MTPupload_GetFile(\n\t\tMTP_flags(0),\n\t\tMTP_inputGroupCallStream(\n\t\t\tMTP_flags(videoChannel\n\t\t\t\t? (Flag::f_video_channel | Flag::f_video_quality)\n\t\t\t\t: Flag(0)),\n\t\t\tinputCall(),\n\t\t\tMTP_long(time),\n\t\t\tMTP_int(scale),\n\t\t\tMTP_int(videoChannel),\n\t\t\tMTP_int((videoQuality == Quality::Full)\n\t\t\t\t? 2\n\t\t\t\t: (videoQuality == Quality::Medium)\n\t\t\t\t? 1\n\t\t\t\t: 0)),\n\t\tMTP_long(0),\n\t\tMTP_int(128 * 1024)\n\t)).done([=](\n\t\t\tconst MTPupload_File &result,\n\t\t\tconst MTP::Response &response) {\n\t\tresult.match([&](const MTPDupload_file &data) {\n\t\t\tconst auto size = data.vbytes().v.size();\n\t\t\tauto bytes = std::vector<uint8_t>(size);\n\t\t\tmemcpy(bytes.data(), data.vbytes().v.constData(), size);\n\t\t\tfinish({\n\t\t\t\t.timestampMilliseconds = time,\n\t\t\t\t.responseTimestamp = TimestampFromMsgId(response.outerMsgId),\n\t\t\t\t.status = Status::Success,\n\t\t\t\t.data = std::move(bytes),\n\t\t\t});\n\t\t}, [&](const MTPDupload_fileCdnRedirect &data) {\n\t\t\tLOG((\"Voice Chat Stream Error: fileCdnRedirect received.\"));\n\t\t\tfinish({\n\t\t\t\t.timestampMilliseconds = time,\n\t\t\t\t.responseTimestamp = TimestampFromMsgId(response.outerMsgId),\n\t\t\t\t.status = Status::ResyncNeeded,\n\t\t\t});\n\t\t});\n\t}).fail([=](const MTP::Error &error, const MTP::Response &response) {\n\t\tif (error.type() == u\"GROUPCALL_JOIN_MISSING\"_q\n\t\t\t|| error.type() == u\"GROUPCALL_FORBIDDEN\"_q) {\n\t\t\tstartRejoin();\n\t\t\treturn;\n\t\t}\n\t\tconst auto status = (MTP::IsFloodError(error)\n\t\t\t|| error.type() == u\"TIME_TOO_BIG\"_q)\n\t\t\t? Status::NotReady\n\t\t\t: Status::ResyncNeeded;\n\t\tfinish({\n\t\t\t.timestampMilliseconds = time,\n\t\t\t.responseTimestamp = TimestampFromMsgId(response.outerMsgId),\n\t\t\t.status = status,\n\t\t});\n\t}).handleAllErrors().toDC(\n\t\tMTP::groupCallStreamDcId(_broadcastDcId)\n\t).send();\n\t_broadcastParts.emplace(raw, LoadingPart{ std::move(task), requestId });\n}\n\nvoid GroupCall::broadcastPartCancel(not_null<LoadPartTask*> task) {\n\tconst auto i = _broadcastParts.find(task);\n\tif (i != end(_broadcastParts)) {\n\t\t_api.request(i->second.requestId).cancel();\n\t\t_broadcastParts.erase(i);\n\t}\n}\n\nvoid GroupCall::mediaChannelDescriptionsStart(\n\t\tstd::shared_ptr<MediaChannelDescriptionsTask> task) {\n\tconst auto real = lookupReal();\n\tif (!real || (_instanceMode == InstanceMode::None)) {\n\t\tfor (const auto ssrc : task->ssrcs()) {\n\t\t\t_unresolvedSsrcs.emplace(ssrc);\n\t\t}\n\t\t_mediaChannelDescriptionses.emplace(std::move(task));\n\t\treturn;\n\t}\n\tif (!mediaChannelDescriptionsFill(task.get())) {\n\t\t_mediaChannelDescriptionses.emplace(std::move(task));\n\t\tAssert(!_unresolvedSsrcs.empty());\n\t}\n\tif (!_unresolvedSsrcs.empty()) {\n\t\treal->resolveParticipants(base::take(_unresolvedSsrcs));\n\t}\n}\n\nbool GroupCall::mediaChannelDescriptionsFill(\n\t\tnot_null<MediaChannelDescriptionsTask*> task,\n\t\tFn<bool(uint32)> resolved) {\n\tusing Channel = tgcalls::MediaChannelDescription;\n\tauto result = false;\n\tconst auto real = lookupReal();\n\tAssert(real != nullptr);\n\tfor (const auto ssrc : task->ssrcs()) {\n\t\tconst auto add = [&](\n\t\t\t\tstd::optional<Channel> channel,\n\t\t\t\tbool screen = false) {\n\t\t\tif (task->finishWithAdding(ssrc, std::move(channel), screen)) {\n\t\t\t\tresult = true;\n\t\t\t}\n\t\t};\n\t\tif (const auto byAudio = real->participantPeerByAudioSsrc(ssrc)) {\n\t\t\tadd(Channel{\n\t\t\t\t.type = Channel::Type::Audio,\n\t\t\t\t.audioSsrc = ssrc,\n\t\t\t\t.userId = int64_t(peerToUser(byAudio->id).bare),\n\t\t\t});\n\t\t} else if (!resolved) {\n\t\t\t_unresolvedSsrcs.emplace(ssrc);\n\t\t} else if (resolved(ssrc)) {\n\t\t\tadd(std::nullopt);\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid GroupCall::mediaChannelDescriptionsCancel(\n\t\tnot_null<MediaChannelDescriptionsTask*> task) {\n\tconst auto i = _mediaChannelDescriptionses.find(task.get());\n\tif (i != end(_mediaChannelDescriptionses)) {\n\t\t_mediaChannelDescriptionses.erase(i);\n\t}\n}\n\nvoid GroupCall::requestCurrentTimeStart(\n\t\tstd::shared_ptr<RequestCurrentTimeTask> task) {\n\tif (!_rtmp) {\n\t\ttask->done(approximateServerTimeInMs());\n\t\treturn;\n\t}\n\t_requestCurrentTimes.emplace(std::move(task));\n\tif (_requestCurrentTimeRequestId) {\n\t\treturn;\n\t}\n\tconst auto finish = [=](int64 value) {\n\t\t_requestCurrentTimeRequestId = 0;\n\t\tfor (const auto &task : base::take(_requestCurrentTimes)) {\n\t\t\ttask->done(value);\n\t\t}\n\t};\n\t_requestCurrentTimeRequestId = _api.request(\n\t\tMTPphone_GetGroupCallStreamChannels(inputCall())\n\t).done([=](const MTPphone_GroupCallStreamChannels &result) {\n\t\tresult.match([&](const MTPDphone_groupCallStreamChannels &data) {\n\t\t\tconst auto &list = data.vchannels().v;\n\t\t\tconst auto empty = list.isEmpty();\n\t\t\tif (!empty) {\n\t\t\t\tconst auto &first = list.front();\n\t\t\t\tfirst.match([&](const MTPDgroupCallStreamChannel &data) {\n\t\t\t\t\tfinish(data.vlast_timestamp_ms().v);\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tfinish(0);\n\t\t\t}\n\t\t\t_emptyRtmp = empty;\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\tfinish(0);\n\n\t\tif (error.type() == u\"GROUPCALL_JOIN_MISSING\"_q\n\t\t\t|| error.type() == u\"GROUPCALL_FORBIDDEN\"_q) {\n\t\t\tstartRejoin();\n\t\t}\n\t}).handleAllErrors().toDC(\n\t\tMTP::groupCallStreamDcId(_broadcastDcId)\n\t).send();\n}\n\nvoid GroupCall::requestCurrentTimeCancel(\n\t\tnot_null<RequestCurrentTimeTask*> task) {\n\tconst auto i = _requestCurrentTimes.find(task.get());\n\tif (i != end(_requestCurrentTimes)) {\n\t\t_requestCurrentTimes.erase(i);\n\t}\n}\n\nint64 GroupCall::approximateServerTimeInMs() const {\n\tExpects(_serverTimeMs != 0);\n\n\treturn _serverTimeMs + (crl::now() - _serverTimeMsGotAt);\n}\n\nvoid GroupCall::updateRequestedVideoChannels() {\n\t_requestedVideoChannelsUpdateScheduled = false;\n\tconst auto real = lookupReal();\n\tif (!real || !_instance) {\n\t\treturn;\n\t}\n\tauto channels = std::vector<tgcalls::VideoChannelDescription>();\n\tusing Quality = tgcalls::VideoChannelDescription::Quality;\n\tchannels.reserve(_activeVideoTracks.size());\n\tconst auto &camera = cameraSharingEndpoint();\n\tconst auto &screen = screenSharingEndpoint();\n\tauto mediums = 0;\n\tauto fullcameras = 0;\n\tauto fullscreencasts = 0;\n\tfor (const auto &[endpoint, video] : _activeVideoTracks) {\n\t\tconst auto &endpointId = endpoint.id;\n\t\tif (endpointId == camera || endpointId == screen) {\n\t\t\tcontinue;\n\t\t} else if (endpointId == Data::RtmpEndpointId()) {\n\t\t\tchannels.push_back({\n\t\t\t\t.endpointId = endpointId,\n\t\t\t\t.minQuality = (video->quality == Group::VideoQuality::Full\n\t\t\t\t\t? Quality::Full\n\t\t\t\t\t: Quality::Thumbnail),\n\t\t\t\t.maxQuality = Quality::Full,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto participant = real->participantByEndpoint(endpointId);\n\t\tconst auto params = (participant && participant->ssrc)\n\t\t\t? participant->videoParams.get()\n\t\t\t: nullptr;\n\t\tif (!params) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto min = (video->quality == Group::VideoQuality::Full\n\t\t\t&& endpoint.type == VideoEndpointType::Screen)\n\t\t\t? Quality::Full\n\t\t\t: Quality::Thumbnail;\n\t\tconst auto max = (video->quality == Group::VideoQuality::Full)\n\t\t\t? Quality::Full\n\t\t\t: (video->quality == Group::VideoQuality::Medium\n\t\t\t\t&& endpoint.type != VideoEndpointType::Screen)\n\t\t\t? Quality::Medium\n\t\t\t: Quality::Thumbnail;\n\t\tif (max == Quality::Full) {\n\t\t\tif (endpoint.type == VideoEndpointType::Screen) {\n\t\t\t\t++fullscreencasts;\n\t\t\t} else {\n\t\t\t\t++fullcameras;\n\t\t\t}\n\t\t} else if (max == Quality::Medium) {\n\t\t\t++mediums;\n\t\t}\n\t\tchannels.push_back({\n\t\t\t.audioSsrc = participant->ssrc,\n\t\t\t.userId = int64_t(peerToUser(participant->peer->id).bare),\n\t\t\t.endpointId = endpointId,\n\t\t\t.ssrcGroups = (params->camera.endpointId == endpointId\n\t\t\t\t? params->camera.ssrcGroups\n\t\t\t\t: params->screen.ssrcGroups),\n\t\t\t.minQuality = min,\n\t\t\t.maxQuality = max,\n\t\t});\n\t}\n\n\t// We limit `count(Full) * kFullAsMediumsCount + count(medium)`.\n\t//\n\t// Try to preserve all qualities; If not\n\t// Try to preserve all screencasts as Full and cameras as Medium; If not\n\t// Try to preserve all screencasts as Full; If not\n\t// Try to preserve all cameras as Medium;\n\tconst auto mediumsCount = mediums\n\t\t+ (fullcameras + fullscreencasts) * kFullAsMediumsCount;\n\tconst auto downgradeSome = (mediumsCount > kMaxMediumQualities);\n\tconst auto downgradeAll = (fullscreencasts * kFullAsMediumsCount)\n\t\t> kMaxMediumQualities;\n\tif (downgradeSome) {\n\t\tfor (auto &channel : channels) {\n\t\t\tif (channel.maxQuality == Quality::Full) {\n\t\t\t\tconst auto camera = (channel.minQuality != Quality::Full);\n\t\t\t\tif (camera) {\n\t\t\t\t\tchannel.maxQuality = Quality::Medium;\n\t\t\t\t} else if (downgradeAll) {\n\t\t\t\t\tchannel.maxQuality\n\t\t\t\t\t\t= channel.minQuality\n\t\t\t\t\t\t= Quality::Thumbnail;\n\t\t\t\t\t--fullscreencasts;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tmediums += fullcameras;\n\t\tfullcameras = 0;\n\t\tif (downgradeAll) {\n\t\t\tfullscreencasts = 0;\n\t\t}\n\t}\n\tif (mediums > kMaxMediumQualities) {\n\t\tfor (auto &channel : channels) {\n\t\t\tif (channel.maxQuality == Quality::Medium) {\n\t\t\t\tchannel.maxQuality = Quality::Thumbnail;\n\t\t\t}\n\t\t}\n\t}\n\t_instance->setRequestedVideoChannels(std::move(channels));\n}\n\nvoid GroupCall::updateRequestedVideoChannelsDelayed() {\n\tif (_requestedVideoChannelsUpdateScheduled) {\n\t\treturn;\n\t}\n\t_requestedVideoChannelsUpdateScheduled = true;\n\tcrl::on_main(this, [=] {\n\t\tif (_requestedVideoChannelsUpdateScheduled) {\n\t\t\tupdateRequestedVideoChannels();\n\t\t}\n\t});\n}\n\nvoid GroupCall::fillActiveVideoEndpoints() {\n\tconst auto real = lookupReal();\n\tAssert(real != nullptr);\n\n\tif (_rtmp) {\n\t\t_videoIsWorking = true;\n\t\tmarkEndpointActive({\n\t\t\tVideoEndpointType::Screen,\n\t\t\t_peer,\n\t\t\tData::RtmpEndpointId(),\n\t\t}, true, false);\n\t\tupdateRequestedVideoChannels();\n\t\treturn;\n\t}\n\n\tconst auto me = real->participantByPeer(joinAs());\n\tif (me && me->videoJoined) {\n\t\t_videoIsWorking = true;\n\t} else {\n\t\t_videoIsWorking = false;\n\t\ttoggleVideo(false);\n\t\ttoggleScreenSharing(std::nullopt);\n\t}\n\n\tconst auto &large = _videoEndpointLarge.current();\n\tauto largeFound = false;\n\tauto endpoints = _activeVideoTracks | ranges::views::transform([](\n\t\t\tconst auto &pair) {\n\t\treturn pair.first;\n\t});\n\tauto removed = base::flat_set<VideoEndpoint>(\n\t\tbegin(endpoints),\n\t\tend(endpoints));\n\tconst auto feedOne = [&](VideoEndpoint endpoint, bool paused) {\n\t\tif (endpoint.empty()) {\n\t\t\treturn;\n\t\t} else if (endpoint == large) {\n\t\t\tlargeFound = true;\n\t\t}\n\t\tif (removed.remove(endpoint)) {\n\t\t\tmarkTrackPaused(endpoint, paused);\n\t\t} else {\n\t\t\tmarkEndpointActive(std::move(endpoint), true, paused);\n\t\t}\n\t};\n\tusing Type = VideoEndpointType;\n\tfor (const auto &participant : real->participants()) {\n\t\tconst auto camera = GetCameraEndpoint(participant.videoParams);\n\t\tif (camera != _cameraEndpoint\n\t\t\t&& camera != _screenEndpoint\n\t\t\t&& participant.peer != joinAs()) {\n\t\t\tconst auto paused = IsCameraPaused(participant.videoParams);\n\t\t\tfeedOne({ Type::Camera, participant.peer, camera }, paused);\n\t\t}\n\t\tconst auto screen = GetScreenEndpoint(participant.videoParams);\n\t\tif (screen != _cameraEndpoint\n\t\t\t&& screen != _screenEndpoint\n\t\t\t&& participant.peer != joinAs()) {\n\t\t\tconst auto paused = IsScreenPaused(participant.videoParams);\n\t\t\tfeedOne({ Type::Screen, participant.peer, screen }, paused);\n\t\t}\n\t}\n\tfeedOne(\n\t\t{ Type::Camera, joinAs(), cameraSharingEndpoint() },\n\t\tisCameraPaused());\n\tfeedOne(\n\t\t{ Type::Screen, joinAs(), screenSharingEndpoint() },\n\t\tisScreenPaused());\n\tif (large && !largeFound) {\n\t\tsetVideoEndpointLarge({});\n\t}\n\tfor (const auto &endpoint : removed) {\n\t\tmarkEndpointActive(endpoint, false, false);\n\t}\n\tupdateRequestedVideoChannels();\n}\n\nvoid GroupCall::updateInstanceMuteState() {\n\tExpects(_instance != nullptr);\n\n\tconst auto state = muted();\n\t_instance->setIsMuted(state != MuteState::Active\n\t\t&& state != MuteState::PushToTalk);\n}\n\nfloat64 GroupCall::singleSourceVolumeValue() const {\n\treturn _singleSourceVolume / float64(Group::kDefaultVolume);\n}\n\nvoid GroupCall::updateInstanceVolumes() {\n\tconst auto real = lookupReal();\n\tif (!real) {\n\t\treturn;\n\t}\n\n\tif (_rtmp) {\n\t\t_instance->setVolume(1, singleSourceVolumeValue());\n\t} else if (videoStream()) {\n\t\tconst auto value = singleSourceVolumeValue();\n\t\tfor (const auto &participant : real->participants()) {\n\t\t\t_instance->setVolume(participant.ssrc, value);\n\t\t}\n\t} else {\n\t\tconst auto &participants = real->participants();\n\t\tfor (const auto &participant : participants) {\n\t\t\tupdateInstanceVolume(std::nullopt, participant);\n\t\t}\n\t}\n}\n\nvoid GroupCall::updateInstanceVolume(\n\t\tconst std::optional<Data::GroupCallParticipant> &was,\n\t\tconst Data::GroupCallParticipant &now) {\n\tconst auto nonDefault = now.mutedByMe\n\t\t|| (now.volume != Group::kDefaultVolume);\n\tconst auto volumeChanged = was\n\t\t? (was->volume != now.volume || was->mutedByMe != now.mutedByMe)\n\t\t: nonDefault;\n\tconst auto additionalSsrc = GetAdditionalAudioSsrc(now.videoParams);\n\tconst auto set = now.ssrc\n\t\t&& (volumeChanged || (was && was->ssrc != now.ssrc));\n\tconst auto additionalSet = additionalSsrc\n\t\t&& (volumeChanged\n\t\t\t|| (was && (GetAdditionalAudioSsrc(was->videoParams)\n\t\t\t\t!= additionalSsrc)));\n\tconst auto localVolume = now.mutedByMe\n\t\t? 0.\n\t\t: (now.volume / float64(Group::kDefaultVolume));\n\tif (set) {\n\t\t_instance->setVolume(now.ssrc, localVolume);\n\t}\n\tif (additionalSet) {\n\t\t_instance->setVolume(additionalSsrc, localVolume);\n\t}\n}\n\nvoid GroupCall::audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data) {\n\tExpects(!data.updates.empty());\n\n\tauto check = false;\n\tauto checkNow = false;\n\tconst auto now = crl::now();\n\tconst auto meMuted = [&] {\n\t\tconst auto state = muted();\n\t\treturn (state != MuteState::Active)\n\t\t\t&& (state != MuteState::PushToTalk);\n\t};\n\tfor (const auto &[ssrcOrZero, value] : data.updates) {\n\t\tconst auto ssrc = ssrcOrZero ? ssrcOrZero : _joinState.ssrc;\n\t\tif (!ssrc) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto level = value.level;\n\t\tconst auto voice = value.voice;\n\t\tconst auto me = (ssrc == _joinState.ssrc);\n\t\tconst auto ignore = me && meMuted();\n\t\t_levelUpdates.fire(LevelUpdate{\n\t\t\t.ssrc = ssrc,\n\t\t\t.value = ignore ? 0.f : level,\n\t\t\t.voice = (!ignore && voice),\n\t\t\t.me = me,\n\t\t});\n\t\tif (level <= kSpeakLevelThreshold) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (me\n\t\t\t&& voice\n\t\t\t&& (!_lastSendProgressUpdate\n\t\t\t\t|| _lastSendProgressUpdate + kUpdateSendActionEach < now)) {\n\t\t\t_lastSendProgressUpdate = now;\n\t\t\t_peer->session().sendProgressManager().update(\n\t\t\t\t_history,\n\t\t\t\tApi::SendProgressType::Speaking);\n\t\t}\n\n\t\tcheck = true;\n\t\tconst auto i = _lastSpoke.find(ssrc);\n\t\tif (i == _lastSpoke.end()) {\n\t\t\t_lastSpoke.emplace(ssrc, Data::LastSpokeTimes{\n\t\t\t\t.anything = now,\n\t\t\t\t.voice = voice ? now : 0,\n\t\t\t});\n\t\t\tcheckNow = true;\n\t\t} else {\n\t\t\tif ((i->second.anything + kCheckLastSpokeInterval / 3 <= now)\n\t\t\t\t|| (voice\n\t\t\t\t\t&& i->second.voice + kCheckLastSpokeInterval / 3 <= now)) {\n\t\t\t\tcheckNow = true;\n\t\t\t}\n\t\t\ti->second.anything = now;\n\t\t\tif (voice) {\n\t\t\t\ti->second.voice = now;\n\t\t\t}\n\t\t}\n\t}\n\tif (checkNow) {\n\t\tcheckLastSpoke();\n\t} else if (check && !_lastSpokeCheckTimer.isActive()) {\n\t\t_lastSpokeCheckTimer.callEach(kCheckLastSpokeInterval / 2);\n\t}\n}\n\nvoid GroupCall::checkLastSpoke() {\n\tconst auto real = lookupReal();\n\tif (!real) {\n\t\treturn;\n\t}\n\n\tconstexpr auto kKeepInListFor = kCheckLastSpokeInterval * 2;\n\tstatic_assert(Data::GroupCall::kSoundStatusKeptFor\n\t\t<= kKeepInListFor - (kCheckLastSpokeInterval / 3));\n\n\tauto hasRecent = false;\n\tconst auto now = crl::now();\n\tauto list = base::take(_lastSpoke);\n\tfor (auto i = list.begin(); i != list.end();) {\n\t\tconst auto &[ssrc, when] = *i;\n\t\tif (when.anything + kKeepInListFor >= now) {\n\t\t\thasRecent = true;\n\t\t\t++i;\n\t\t} else {\n\t\t\ti = list.erase(i);\n\t\t}\n\n\t\t// Ignore my levels from microphone if I'm already muted.\n\t\tif (ssrc != _joinState.ssrc\n\t\t\t|| muted() == MuteState::Active\n\t\t\t|| muted() == MuteState::PushToTalk) {\n\t\t\treal->applyLastSpoke(ssrc, when, now);\n\t\t} else {\n\t\t\treal->applyLastSpoke(ssrc, { crl::time(), crl::time() }, now);\n\t\t}\n\t}\n\t_lastSpoke = std::move(list);\n\n\tif (!hasRecent) {\n\t\t_lastSpokeCheckTimer.cancel();\n\t} else if (!_lastSpokeCheckTimer.isActive()) {\n\t\t_lastSpokeCheckTimer.callEach(kCheckLastSpokeInterval / 3);\n\t}\n}\n\nvoid GroupCall::checkJoined() {\n\tif (state() != State::Connecting || !_id || !_joinState.ssrc) {\n\t\treturn;\n\t}\n\tauto sources = QVector<MTPint>(1, MTP_int(_joinState.ssrc));\n\tif (_screenJoinState.ssrc) {\n\t\tsources.push_back(MTP_int(_screenJoinState.ssrc));\n\t}\n\t_api.request(MTPphone_CheckGroupCall(\n\t\tinputCall(),\n\t\tMTP_vector<MTPint>(std::move(sources))\n\t)).done([=](const MTPVector<MTPint> &result) {\n\t\tif (!ranges::contains(result.v, MTP_int(_joinState.ssrc))) {\n\t\t\tLOG((\"Call Info: Rejoin after no my ssrc in checkGroupCall.\"));\n\t\t\t_joinState.nextActionPending = true;\n\t\t\tcheckNextJoinAction();\n\t\t} else {\n\t\t\tif (state() == State::Connecting) {\n\t\t\t\t_checkJoinedTimer.callOnce(kCheckJoinedTimeout);\n\t\t\t}\n\t\t\tif (_screenJoinState.ssrc\n\t\t\t\t&& !ranges::contains(\n\t\t\t\t\tresult.v,\n\t\t\t\t\tMTP_int(_screenJoinState.ssrc))) {\n\t\t\t\tLOG((\"Call Info: \"\n\t\t\t\t\t\"Screen rejoin after _screenSsrc not found.\"));\n\t\t\t\t_screenJoinState.nextActionPending = true;\n\t\t\t\tcheckNextJoinAction();\n\t\t\t}\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tLOG((\"Call Info: Full rejoin after error '%1' in checkGroupCall.\"\n\t\t\t).arg(error.type()));\n\t\tstartRejoin();\n\t}).send();\n}\n\nvoid GroupCall::setInstanceConnected(\n\t\ttgcalls::GroupNetworkState networkState) {\n\tconst auto inTransit = networkState.isTransitioningFromBroadcastToRtc;\n\tconst auto instanceState = !networkState.isConnected\n\t\t? InstanceState::Disconnected\n\t\t: inTransit\n\t\t? InstanceState::TransitionToRtc\n\t\t: InstanceState::Connected;\n\tconst auto connected = (instanceState != InstanceState::Disconnected);\n\tif (_instanceState.current() == instanceState\n\t\t&& _instanceTransitioning == inTransit) {\n\t\treturn;\n\t}\n\tconst auto nowCanSpeak = connected\n\t\t&& _instanceTransitioning\n\t\t&& !inTransit\n\t\t&& (muted() == MuteState::Muted);\n\t_instanceTransitioning = inTransit;\n\t_instanceState = instanceState;\n\tif (state() == State::Connecting && connected) {\n\t\tsetState(State::Joined);\n\t} else if (state() == State::Joined && !connected) {\n\t\tsetState(State::Connecting);\n\t}\n\tif (nowCanSpeak) {\n\t\tnotifyAboutAllowedToSpeak();\n\t}\n\tif (!_hadJoinedState && state() == State::Joined) {\n\t\tcheckFirstTimeJoined();\n\t}\n}\n\nvoid GroupCall::setScreenInstanceConnected(\n\t\ttgcalls::GroupNetworkState networkState) {\n\tconst auto inTransit = networkState.isTransitioningFromBroadcastToRtc;\n\tconst auto screenInstanceState = !networkState.isConnected\n\t\t? InstanceState::Disconnected\n\t\t: inTransit\n\t\t? InstanceState::TransitionToRtc\n\t\t: InstanceState::Connected;\n\tif (_screenInstanceState.current() == screenInstanceState) {\n\t\treturn;\n\t}\n\t_screenInstanceState = screenInstanceState;\n}\n\nvoid GroupCall::checkFirstTimeJoined() {\n\tif (_hadJoinedState || state() != State::Joined) {\n\t\treturn;\n\t}\n\t_hadJoinedState = true;\n\tapplyGlobalShortcutChanges();\n\t_delegate->groupCallPlaySound(Delegate::GroupCallSound::Started);\n}\n\nvoid GroupCall::notifyAboutAllowedToSpeak() {\n\tif (!_hadJoinedState) {\n\t\treturn;\n\t}\n\t_delegate->groupCallPlaySound(\n\t\tDelegate::GroupCallSound::AllowedToSpeak);\n\t_allowedToSpeakNotifications.fire({});\n}\n\nvoid GroupCall::setInstanceMode(InstanceMode mode) {\n\tExpects(_instance != nullptr);\n\n\t_instanceMode = mode;\n\n\tusing Mode = tgcalls::GroupConnectionMode;\n\t_instance->setConnectionMode([&] {\n\t\tswitch (_instanceMode) {\n\t\tcase InstanceMode::None: return Mode::GroupConnectionModeNone;\n\t\tcase InstanceMode::Rtc: return Mode::GroupConnectionModeRtc;\n\t\tcase InstanceMode::Stream: return Mode::GroupConnectionModeBroadcast;\n\t\t}\n\t\tUnexpected(\"Mode in GroupCall::setInstanceMode.\");\n\t}(), true, _rtmp);\n}\n\nvoid GroupCall::setScreenInstanceMode(InstanceMode mode) {\n\tExpects(_screenInstance != nullptr);\n\n\t_screenInstanceMode = mode;\n\n\tusing Mode = tgcalls::GroupConnectionMode;\n\t_screenInstance->setConnectionMode([&] {\n\t\tswitch (_screenInstanceMode) {\n\t\tcase InstanceMode::None: return Mode::GroupConnectionModeNone;\n\t\tcase InstanceMode::Rtc: return Mode::GroupConnectionModeRtc;\n\t\tcase InstanceMode::Stream: return Mode::GroupConnectionModeBroadcast;\n\t\t}\n\t\tUnexpected(\"Mode in GroupCall::setInstanceMode.\");\n\t}(), true, false);\n}\n\nvoid GroupCall::maybeSendMutedUpdate(MuteState previous) {\n\t// Send Active <-> !Active or ForceMuted <-> RaisedHand changes.\n\tconst auto now = muted();\n\tif ((previous == MuteState::Active && now == MuteState::Muted)\n\t\t|| (now == MuteState::Active\n\t\t\t&& (previous == MuteState::Muted\n\t\t\t\t|| previous == MuteState::PushToTalk))) {\n\t\tsendSelfUpdate(SendUpdateType::Mute);\n\t} else if ((now == MuteState::ForceMuted\n\t\t&& previous == MuteState::RaisedHand)\n\t\t|| (now == MuteState::RaisedHand\n\t\t\t&& previous == MuteState::ForceMuted)) {\n\t\tsendSelfUpdate(SendUpdateType::RaiseHand);\n\t}\n}\n\nvoid GroupCall::sendPendingSelfUpdates() {\n\tif ((state() != State::Connecting && state() != State::Joined)\n\t\t|| _selfUpdateRequestId) {\n\t\treturn;\n\t}\n\tconst auto updates = {\n\t\tSendUpdateType::Mute,\n\t\tSendUpdateType::RaiseHand,\n\t\tSendUpdateType::CameraStopped,\n\t\tSendUpdateType::CameraPaused,\n\t\tSendUpdateType::ScreenPaused,\n\t};\n\tfor (const auto type : updates) {\n\t\tif (type == SendUpdateType::ScreenPaused\n\t\t\t&& _screenJoinState.action != JoinAction::None) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (_pendingSelfUpdates & type) {\n\t\t\t_pendingSelfUpdates &= ~type;\n\t\t\tsendSelfUpdate(type);\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nvoid GroupCall::sendSelfUpdate(SendUpdateType type) {\n\tif ((state() != State::Connecting && state() != State::Joined)\n\t\t|| _selfUpdateRequestId) {\n\t\t_pendingSelfUpdates |= type;\n\t\treturn;\n\t}\n\tusing Flag = MTPphone_EditGroupCallParticipant::Flag;\n\t_selfUpdateRequestId = _api.request(MTPphone_EditGroupCallParticipant(\n\t\tMTP_flags((type == SendUpdateType::RaiseHand)\n\t\t\t? Flag::f_raise_hand\n\t\t\t: (type == SendUpdateType::CameraStopped)\n\t\t\t? Flag::f_video_stopped\n\t\t\t: (type == SendUpdateType::CameraPaused)\n\t\t\t? Flag::f_video_paused\n\t\t\t: (type == SendUpdateType::ScreenPaused)\n\t\t\t? Flag::f_presentation_paused\n\t\t\t: Flag::f_muted),\n\t\tinputCall(),\n\t\tjoinAs()->input(),\n\t\tMTP_bool(muted() != MuteState::Active),\n\t\tMTP_int(100000), // volume\n\t\tMTP_bool(muted() == MuteState::RaisedHand),\n\t\tMTP_bool(!isSharingCamera()),\n\t\tMTP_bool(isCameraPaused()),\n\t\tMTP_bool(isScreenPaused())\n\t)).done([=](const MTPUpdates &result) {\n\t\t_selfUpdateRequestId = 0;\n\t\t_peer->session().api().applyUpdates(result);\n\t\tsendPendingSelfUpdates();\n\t}).fail([=](const MTP::Error &error) {\n\t\t_selfUpdateRequestId = 0;\n\t\tif (error.type() == u\"GROUPCALL_FORBIDDEN\"_q) {\n\t\t\tLOG((\"Call Info: Rejoin after error '%1' in editGroupCallMember.\"\n\t\t\t\t).arg(error.type()));\n\t\t\tstartRejoin();\n\t\t}\n\t}).send();\n}\n\nvoid GroupCall::pinVideoEndpoint(VideoEndpoint endpoint) {\n\t_videoEndpointPinned = false;\n\tif (endpoint) {\n\t\tsetVideoEndpointLarge(std::move(endpoint));\n\t\t_videoEndpointPinned = true;\n\t}\n}\n\nvoid GroupCall::showVideoEndpointLarge(VideoEndpoint endpoint) {\n\tif (_videoEndpointLarge.current() == endpoint) {\n\t\treturn;\n\t}\n\t_videoEndpointPinned = false;\n\tsetVideoEndpointLarge(std::move(endpoint));\n\t_videoLargeTillTime = crl::now() + kFixManualLargeVideoDuration;\n}\n\nvoid GroupCall::setVideoEndpointLarge(VideoEndpoint endpoint) {\n\tif (!endpoint) {\n\t\t_videoEndpointPinned = false;\n\t}\n\t_videoEndpointLarge = endpoint;\n}\n\nvoid GroupCall::requestVideoQuality(\n\t\tconst VideoEndpoint &endpoint,\n\t\tGroup::VideoQuality quality) {\n\tif (!endpoint) {\n\t\treturn;\n\t}\n\tconst auto i = _activeVideoTracks.find(endpoint);\n\tif (i == end(_activeVideoTracks) || i->second->quality == quality) {\n\t\treturn;\n\t}\n\ti->second->quality = quality;\n\tupdateRequestedVideoChannelsDelayed();\n}\n\nvoid GroupCall::toggleMute(const Group::MuteRequest &data) {\n\tif (_rtmp || videoStream()) {\n\t\t_singleSourceVolume = data.mute ? 0 : Group::kDefaultVolume;\n\t\tupdateInstanceVolumes();\n\t} else if (data.locallyOnly) {\n\t\tapplyParticipantLocally(data.peer, data.mute, std::nullopt);\n\t} else {\n\t\teditParticipant(data.peer, data.mute, std::nullopt);\n\t}\n}\n\nvoid GroupCall::changeVolume(const Group::VolumeRequest &data) {\n\tif (_rtmp || videoStream()) {\n\t\t_singleSourceVolume = data.volume;\n\t\tupdateInstanceVolumes();\n\t} else if (data.locallyOnly) {\n\t\tapplyParticipantLocally(data.peer, false, data.volume);\n\t} else {\n\t\teditParticipant(data.peer, false, data.volume);\n\t}\n}\n\nvoid GroupCall::editParticipant(\n\t\tnot_null<PeerData*> participantPeer,\n\t\tbool mute,\n\t\tstd::optional<int> volume) {\n\tconst auto participant = LookupParticipant(this, participantPeer);\n\tif (!participant) {\n\t\treturn;\n\t}\n\tapplyParticipantLocally(participantPeer, mute, volume);\n\n\tusing Flag = MTPphone_EditGroupCallParticipant::Flag;\n\tconst auto flags = Flag::f_muted\n\t\t| (volume.has_value() ? Flag::f_volume : Flag(0));\n\t_api.request(MTPphone_EditGroupCallParticipant(\n\t\tMTP_flags(flags),\n\t\tinputCall(),\n\t\tparticipantPeer->input(),\n\t\tMTP_bool(mute),\n\t\tMTP_int(std::clamp(volume.value_or(0), 1, Group::kMaxVolume)),\n\t\tMTPBool(), // raise_hand\n\t\tMTPBool(), // video_muted\n\t\tMTPBool(), // video_paused\n\t\tMTPBool() // presentation_paused\n\t)).done([=](const MTPUpdates &result) {\n\t\t_peer->session().api().applyUpdates(result);\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (error.type() == u\"GROUPCALL_FORBIDDEN\"_q) {\n\t\t\tLOG((\"Call Info: Rejoin after error '%1' in editGroupCallMember.\"\n\t\t\t\t).arg(error.type()));\n\t\t\tstartRejoin();\n\t\t}\n\t}).send();\n}\n\nvoid GroupCall::inviteToConference(\n\t\tInviteRequest request,\n\t\tFn<not_null<InviteResult*>()> resultAddress,\n\t\tFn<void()> finishRequest) {\n\tusing Flag = MTPphone_InviteConferenceCallParticipant::Flag;\n\tconst auto user = request.user;\n\t_api.request(MTPphone_InviteConferenceCallParticipant(\n\t\tMTP_flags(request.video ? Flag::f_video : Flag()),\n\t\tinputCall(),\n\t\tuser->inputUser()\n\t)).done([=](const MTPUpdates &result) {\n\t\tconst auto call = _sharedCall.get();\n\t\tuser->owner().registerInvitedToCallUser(_id, call, user, true);\n\t\t_peer->session().api().applyUpdates(result);\n\t\tresultAddress()->invited.push_back(user);\n\t\tfinishRequest();\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto result = resultAddress();\n\t\tconst auto type = error.type();\n\t\tif (type == u\"USER_PRIVACY_RESTRICTED\"_q) {\n\t\t\tresult->privacyRestricted.push_back(user);\n\t\t} else if (type == u\"USER_ALREADY_PARTICIPANT\"_q) {\n\t\t\tresult->alreadyIn.push_back(user);\n\t\t} else if (type == u\"USER_WAS_KICKED\"_q) {\n\t\t\tresult->kicked.push_back(user);\n\t\t} else if (type == u\"GROUPCALL_FORBIDDEN\"_q) {\n\t\t\tstartRejoin();\n\t\t\t_rejoinedCallbacks.push_back([=] {\n\t\t\t\tinviteToConference(request, resultAddress, finishRequest);\n\t\t\t});\n\t\t\treturn;\n\t\t} else {\n\t\t\tresult->failed.push_back(user);\n\t\t}\n\t\tfinishRequest();\n\t}).send();\n}\n\nvoid GroupCall::inviteUsers(\n\t\tconst std::vector<InviteRequest> &requests,\n\t\tFn<void(InviteResult)> done) {\n\tconst auto real = lookupReal();\n\tif (!real) {\n\t\tif (done) {\n\t\t\tdone({});\n\t\t}\n\t\treturn;\n\t}\n\tconst auto owner = &_peer->owner();\n\n\tstruct State {\n\t\tInviteResult result;\n\t\tint requests = 0;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tconst auto finishRequest = [=] {\n\t\tif (!--state->requests) {\n\t\t\tif (done) {\n\t\t\t\tdone(std::move(state->result));\n\t\t\t}\n\t\t}\n\t};\n\n\tif (_sharedCall.get()) {\n\t\tfor (const auto &request : requests) {\n\t\t\tinviteToConference(request, [=] {\n\t\t\t\treturn &state->result;\n\t\t\t}, finishRequest);\n\t\t\t++state->requests;\n\t\t}\n\t\treturn;\n\t}\n\n\tauto usersSlice = std::vector<not_null<UserData*>>();\n\tusersSlice.reserve(kMaxInvitePerSlice);\n\tauto slice = QVector<MTPInputUser>();\n\tslice.reserve(kMaxInvitePerSlice);\n\tconst auto sendSlice = [&] {\n\t\t_api.request(MTPphone_InviteToGroupCall(\n\t\t\tinputCall(),\n\t\t\tMTP_vector<MTPInputUser>(slice)\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t_peer->session().api().applyUpdates(result);\n\t\t\tfor (const auto &user : usersSlice) {\n\t\t\t\tstate->result.invited.push_back(user);\n\t\t\t}\n\t\t\tfinishRequest();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tfinishRequest();\n\t\t}).send();\n\t\t++state->requests;\n\n\t\tslice.clear();\n\t\tusersSlice.clear();\n\t};\n\tfor (const auto &request : requests) {\n\t\tconst auto user = request.user;\n\t\towner->registerInvitedToCallUser(_id, _peer, user, false);\n\t\tusersSlice.push_back(user);\n\t\tslice.push_back(user->inputUser());\n\t\tif (slice.size() == kMaxInvitePerSlice) {\n\t\t\tsendSlice();\n\t\t}\n\t}\n\tif (!slice.empty()) {\n\t\tsendSlice();\n\t}\n}\n\nauto GroupCall::ensureGlobalShortcutManager()\n-> std::shared_ptr<GlobalShortcutManager> {\n\tif (!_shortcutManager) {\n\t\t_shortcutManager = base::CreateGlobalShortcutManager();\n\t}\n\treturn _shortcutManager;\n}\n\nvoid GroupCall::applyGlobalShortcutChanges() {\n\tauto &settings = Core::App().settings();\n\tif (!settings.groupCallPushToTalk()\n\t\t|| settings.groupCallPushToTalkShortcut().isEmpty()\n\t\t|| !base::GlobalShortcutsAvailable()\n\t\t|| !base::GlobalShortcutsAllowed()) {\n\t\t_shortcutManager = nullptr;\n\t\t_pushToTalk = nullptr;\n\t\treturn;\n\t}\n\tensureGlobalShortcutManager();\n\tconst auto shortcut = _shortcutManager->shortcutFromSerialized(\n\t\tsettings.groupCallPushToTalkShortcut());\n\tif (!shortcut) {\n\t\tsettings.setGroupCallPushToTalkShortcut(QByteArray());\n\t\tsettings.setGroupCallPushToTalk(false);\n\t\tCore::App().saveSettingsDelayed();\n\t\t_shortcutManager = nullptr;\n\t\t_pushToTalk = nullptr;\n\t\treturn;\n\t}\n\tif (_pushToTalk) {\n\t\tif (shortcut->serialize() == _pushToTalk->serialize()) {\n\t\t\treturn;\n\t\t}\n\t\t_shortcutManager->stopWatching(_pushToTalk);\n\t}\n\t_pushToTalk = shortcut;\n\t_shortcutManager->startWatching(_pushToTalk, [=](bool pressed) {\n\t\tpushToTalk(\n\t\t\tpressed,\n\t\t\tCore::App().settings().groupCallPushToTalkDelay());\n\t});\n}\n\nvoid GroupCall::pushToTalk(bool pressed, crl::time delay) {\n\tif (mutedByAdmin() || muted() == MuteState::Active) {\n\t\treturn;\n\t} else if (pressed) {\n\t\t_pushToTalkCancelTimer.cancel();\n\t\tsetMuted(MuteState::PushToTalk);\n\t} else if (delay) {\n\t\t_pushToTalkCancelTimer.callOnce(delay);\n\t} else {\n\t\tpushToTalkCancel();\n\t}\n}\n\nvoid GroupCall::pushToTalkCancel() {\n\t_pushToTalkCancelTimer.cancel();\n\tif (muted() == MuteState::PushToTalk) {\n\t\tsetMuted(MuteState::Muted);\n\t}\n}\n\nvoid GroupCall::setNotRequireARGB32() {\n\t_requireARGB32 = false;\n}\n\nstd::function<std::vector<uint8_t>(\n\t\tstd::vector<uint8_t> const &,\n\t\tint64_t, bool,\n\t\tint32_t)> GroupCall::e2eEncryptDecrypt() const {\n\treturn _e2eEncryptDecrypt ? _e2eEncryptDecrypt->callback() : nullptr;\n}\n\nvoid GroupCall::sendMessage(TextWithTags message) {\n\t_messages->send(std::move(message), 0);\n}\n\nauto GroupCall::otherParticipantStateValue() const\n-> rpl::producer<Group::ParticipantState> {\n\treturn _otherParticipantStateValue.events();\n}\n\nMTPInputGroupCall GroupCall::inputCall() const {\n\tExpects(_id != 0);\n\n\treturn MTP_inputGroupCall(MTP_long(_id), MTP_long(_accessHash));\n}\n\nMTPInputGroupCall GroupCall::inputCallSafe() const {\n\tconst auto inviteMsgId = _conferenceJoinMessageId.bare;\n\treturn inviteMsgId\n\t\t? MTP_inputGroupCallInviteMessage(MTP_int(inviteMsgId))\n\t\t: _conferenceLinkSlug.isEmpty()\n\t\t? inputCall()\n\t\t: MTP_inputGroupCallSlug(MTP_string(_conferenceLinkSlug));\n}\n\nvoid GroupCall::destroyController() {\n\tif (_instance) {\n\t\tDEBUG_LOG((\"Call Info: Destroying call controller..\"));\n\t\tinvalidate_weak_ptrs(&_instanceGuard);\n\n\t\t_instance->stop(nullptr);\n\t\tcrl::async([\n\t\t\tinstance = base::take(_instance),\n\t\t\tdone = _delegate->groupCallAddAsyncWaiter()\n\t\t]() mutable {\n\t\t\tinstance = nullptr;\n\t\t\tDEBUG_LOG((\"Call Info: Call controller destroyed.\"));\n\t\t\tdone();\n\t\t});\n\t}\n}\n\nvoid GroupCall::destroyScreencast() {\n\tif (_screenInstance) {\n\t\tDEBUG_LOG((\"Call Info: Destroying call screen controller..\"));\n\t\tinvalidate_weak_ptrs(&_screenInstanceGuard);\n\n\t\t_screenInstance->stop(nullptr);\n\t\tcrl::async([\n\t\t\tinstance = base::take(_screenInstance),\n\t\t\tdone = _delegate->groupCallAddAsyncWaiter()\n\t\t]() mutable {\n\t\t\tinstance = nullptr;\n\t\t\tDEBUG_LOG((\"Call Info: Call screen controller destroyed.\"));\n\t\t\tdone();\n\t\t});\n\t}\n}\n\nTextWithEntities ComposeInviteResultToast(\n\t\tconst InviteResult &result) {\n\tauto text = TextWithEntities();\n\tconst auto append = [&](TextWithEntities part) {\n\t\tif (!text.empty()) {\n\t\t\ttext.append(u\"\\n\\n\"_q);\n\t\t}\n\t\ttext.append(part);\n\t};\n\n\tconst auto invited = int(result.invited.size());\n\tconst auto already = int(result.alreadyIn.size());\n\tconst auto restricted = int(result.privacyRestricted.size());\n\tconst auto kicked = int(result.kicked.size());\n\tconst auto failed = int(result.failed.size());\n\tif (invited == 1) {\n\t\tappend(tr::lng_confcall_invite_done_user(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\ttr::bold(result.invited.front()->shortName()),\n\t\t\ttr::rich));\n\t} else if (invited > 1) {\n\t\tappend(tr::lng_confcall_invite_done_many(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tinvited,\n\t\t\ttr::rich));\n\t}\n\tif (already == 1) {\n\t\tappend(tr::lng_confcall_invite_already_user(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\ttr::bold(result.alreadyIn.front()->shortName()),\n\t\t\ttr::rich));\n\t} else if (already > 1) {\n\t\tappend(tr::lng_confcall_invite_already_many(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\talready,\n\t\t\ttr::rich));\n\t}\n\tif (restricted == 1) {\n\t\tappend(tr::lng_confcall_invite_fail_user(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\ttr::bold(result.privacyRestricted.front()->shortName()),\n\t\t\ttr::rich));\n\t} else if (restricted > 1) {\n\t\tappend(tr::lng_confcall_invite_fail_many(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\trestricted,\n\t\t\ttr::rich));\n\t}\n\tif (kicked == 1) {\n\t\tappend(tr::lng_confcall_invite_kicked_user(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\ttr::bold(result.kicked.front()->shortName()),\n\t\t\ttr::rich));\n\t} else if (kicked > 1) {\n\t\tappend(tr::lng_confcall_invite_kicked_many(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tkicked,\n\t\t\ttr::rich));\n\t}\n\tif (failed == 1) {\n\t\tappend(tr::lng_confcall_invite_fail_user(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\ttr::bold(result.failed.front()->shortName()),\n\t\t\ttr::rich));\n\t} else if (failed > 1) {\n\t\tappend(tr::lng_confcall_invite_fail_many(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tfailed,\n\t\t\ttr::rich));\n\t}\n\treturn text;\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_call.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"base/timer.h\"\n#include \"base/bytes.h\"\n#include \"mtproto/sender.h\"\n#include \"mtproto/mtproto_auth_key.h\"\n#include \"webrtc/webrtc_device_common.h\"\n#include \"webrtc/webrtc_device_resolver.h\"\n\nclass History;\n\nnamespace tgcalls {\nclass GroupInstanceCustomImpl;\nstruct GroupLevelsUpdate;\nstruct GroupNetworkState;\nstruct GroupParticipantDescription;\nclass VideoCaptureInterface;\nenum class VideoCodecName;\n} // namespace tgcalls\n\nnamespace base {\nclass GlobalShortcutManager;\nclass GlobalShortcutValue;\n} // namespace base\n\nnamespace Webrtc {\nclass MediaDevices;\nclass VideoTrack;\nenum class VideoState;\n} // namespace Webrtc\n\nnamespace Data {\nstruct LastSpokeTimes;\nstruct GroupCallParticipant;\nclass GroupCall;\nenum class GroupCallOrigin : uchar;\n} // namespace Data\n\nnamespace TdE2E {\nclass Call;\nclass EncryptDecrypt;\n} // namespace TdE2E\n\nnamespace Calls::Group {\nstruct MuteRequest;\nstruct VolumeRequest;\nstruct ParticipantState;\nstruct JoinInfo;\nstruct ConferenceInfo;\nstruct RejoinEvent;\nstruct RtmpInfo;\nenum class VideoQuality;\nenum class Error;\nclass Messages;\n} // namespace Calls::Group\n\nnamespace Calls {\n\nstruct InviteRequest;\nstruct InviteResult;\nstruct StartConferenceInfo;\n\nenum class MuteState {\n\tActive,\n\tPushToTalk,\n\tMuted,\n\tForceMuted,\n\tRaisedHand,\n};\n\n[[nodiscard]] inline auto MapPushToTalkToActive() {\n\treturn rpl::map([=](MuteState state) {\n\t\treturn (state == MuteState::PushToTalk) ? MuteState::Active : state;\n\t});\n}\n\n[[nodiscard]] bool IsGroupCallAdmin(\n\tnot_null<PeerData*> peer,\n\tnot_null<PeerData*> participantPeer);\n\nstruct LevelUpdate {\n\tuint32 ssrc = 0;\n\tfloat value = 0.;\n\tbool voice = false;\n\tbool me = false;\n};\n\nenum class VideoEndpointType {\n\tCamera,\n\tScreen,\n};\n\nstruct VideoEndpoint {\n\tVideoEndpoint() = default;\n\tVideoEndpoint(\n\t\tVideoEndpointType type,\n\t\tnot_null<PeerData*> peer,\n\t\tstd::string id)\n\t: type(type)\n\t, peer(peer)\n\t, id(std::move(id)) {\n\t}\n\n\tVideoEndpointType type = VideoEndpointType::Camera;\n\tPeerData *peer = nullptr;\n\tstd::string id;\n\n\t[[nodiscard]] bool rtmp() const noexcept;\n\t[[nodiscard]] bool empty() const noexcept {\n\t\tExpects(id.empty() || peer != nullptr);\n\n\t\treturn id.empty();\n\t}\n\t[[nodiscard]] explicit operator bool() const noexcept {\n\t\treturn !empty();\n\t}\n};\n\ninline bool operator==(\n\t\tconst VideoEndpoint &a,\n\t\tconst VideoEndpoint &b) noexcept {\n\treturn (a.id == b.id);\n}\n\ninline bool operator!=(\n\t\tconst VideoEndpoint &a,\n\t\tconst VideoEndpoint &b) noexcept {\n\treturn !(a == b);\n}\n\ninline bool operator<(\n\t\tconst VideoEndpoint &a,\n\t\tconst VideoEndpoint &b) noexcept {\n\treturn (a.peer < b.peer)\n\t\t|| (a.peer == b.peer && a.id < b.id);\n}\n\ninline bool operator>(\n\t\tconst VideoEndpoint &a,\n\t\tconst VideoEndpoint &b) noexcept {\n\treturn (b < a);\n}\n\ninline bool operator<=(\n\t\tconst VideoEndpoint &a,\n\t\tconst VideoEndpoint &b) noexcept {\n\treturn !(b < a);\n}\n\ninline bool operator>=(\n\t\tconst VideoEndpoint &a,\n\t\tconst VideoEndpoint &b) noexcept {\n\treturn !(a < b);\n}\n\nstruct VideoStateToggle {\n\tVideoEndpoint endpoint;\n\tbool value = false;\n};\n\nstruct VideoQualityRequest {\n\tVideoEndpoint endpoint;\n\tGroup::VideoQuality quality = Group::VideoQuality();\n};\n\nstruct ParticipantVideoParams;\n\n[[nodiscard]] std::shared_ptr<ParticipantVideoParams> ParseVideoParams(\n\tconst tl::conditional<MTPGroupCallParticipantVideo> &camera,\n\tconst tl::conditional<MTPGroupCallParticipantVideo> &screen,\n\tconst std::shared_ptr<ParticipantVideoParams> &existing);\n\n[[nodiscard]] const std::string &GetCameraEndpoint(\n\tconst std::shared_ptr<ParticipantVideoParams> &params);\n[[nodiscard]] const std::string &GetScreenEndpoint(\n\tconst std::shared_ptr<ParticipantVideoParams> &params);\n[[nodiscard]] bool IsCameraPaused(\n\tconst std::shared_ptr<ParticipantVideoParams> &params);\n[[nodiscard]] bool IsScreenPaused(\n\tconst std::shared_ptr<ParticipantVideoParams> &params);\n[[nodiscard]] uint32 GetAdditionalAudioSsrc(\n\tconst std::shared_ptr<ParticipantVideoParams> &params);\n\nclass GroupCall final\n\t: public base::has_weak_ptr\n\t, private Webrtc::CaptureMuteTracker {\npublic:\n\tclass Delegate {\n\tpublic:\n\t\tvirtual ~Delegate() = default;\n\n\t\tvirtual void groupCallFinished(not_null<GroupCall*> call) = 0;\n\t\tvirtual void groupCallFailed(not_null<GroupCall*> call) = 0;\n\t\tvirtual void groupCallRequestPermissionsOrFail(\n\t\t\tFn<void()> onSuccess) = 0;\n\n\t\tenum class GroupCallSound {\n\t\t\tStarted,\n\t\t\tConnecting,\n\t\t\tAllowedToSpeak,\n\t\t\tEnded,\n\t\t\tRecordingStarted,\n\t\t};\n\t\tvirtual void groupCallPlaySound(GroupCallSound sound) = 0;\n\t\tvirtual auto groupCallGetVideoCapture(const QString &deviceId)\n\t\t\t-> std::shared_ptr<tgcalls::VideoCaptureInterface> = 0;\n\n\t\t[[nodiscard]] virtual FnMut<void()> groupCallAddAsyncWaiter() = 0;\n\t};\n\n\tusing GlobalShortcutManager = base::GlobalShortcutManager;\n\n\tstruct VideoTrack;\n\n\t[[nodiscard]] static not_null<PeerData*> TrackPeer(\n\t\tconst std::unique_ptr<VideoTrack> &track);\n\t[[nodiscard]] static not_null<Webrtc::VideoTrack*> TrackPointer(\n\t\tconst std::unique_ptr<VideoTrack> &track);\n\t[[nodiscard]] static rpl::producer<QSize> TrackSizeValue(\n\t\tconst std::unique_ptr<VideoTrack> &track);\n\n\tGroupCall(\n\t\tnot_null<Delegate*> delegate,\n\t\tGroup::JoinInfo info,\n\t\tconst MTPInputGroupCall &inputCall);\n\tGroupCall(not_null<Delegate*> delegate, StartConferenceInfo info);\n\t~GroupCall();\n\n\t[[nodiscard]] CallId id() const {\n\t\treturn _id;\n\t}\n\t[[nodiscard]] not_null<PeerData*> peer() const {\n\t\treturn _peer;\n\t}\n\t[[nodiscard]] not_null<PeerData*> joinAs() const {\n\t\treturn _joinAs.current();\n\t}\n\t[[nodiscard]] rpl::producer<not_null<PeerData*>> joinAsValue() const {\n\t\treturn _joinAs.value();\n\t}\n\t[[nodiscard]] not_null<Group::Messages*> messages() const {\n\t\treturn _messages.get();\n\t}\n\t[[nodiscard]] not_null<PeerData*> messagesFrom() const;\n\t[[nodiscard]] bool showChooseJoinAs() const;\n\t[[nodiscard]] TimeId scheduleDate() const {\n\t\treturn _scheduleDate;\n\t}\n\t[[nodiscard]] bool scheduleStartSubscribed() const;\n\t[[nodiscard]] bool rtmp() const;\n\t[[nodiscard]] bool conference() const;\n\t[[nodiscard]] bool videoStream() const;\n\t[[nodiscard]] Data::GroupCallOrigin origin() const;\n\t[[nodiscard]] bool listenersHidden() const;\n\t[[nodiscard]] bool emptyRtmp() const;\n\t[[nodiscard]] rpl::producer<bool> emptyRtmpValue() const;\n\t[[nodiscard]] int rtmpVolume() const;\n\n\t[[nodiscard]] Group::RtmpInfo rtmpInfo() const;\n\n\tvoid setRtmpInfo(const Group::RtmpInfo &value);\n\n\t[[nodiscard]] Data::GroupCall *lookupReal() const;\n\t[[nodiscard]] std::shared_ptr<Data::GroupCall> sharedCall() const;\n\t[[nodiscard]] rpl::producer<not_null<Data::GroupCall*>> real() const;\n\t[[nodiscard]] rpl::producer<QByteArray> emojiHashValue() const;\n\n\tvoid applyInputCall(const MTPInputGroupCall &inputCall);\n\tvoid startConference();\n\tvoid start(TimeId scheduleDate, bool rtmp);\n\tvoid hangup();\n\tvoid discard();\n\tvoid rejoinAs(Group::JoinInfo info);\n\tvoid rejoinWithHash(const QString &hash);\n\tvoid initialJoin();\n\tvoid initialJoinRequested();\n\tvoid handleUpdate(const MTPUpdate &update);\n\tvoid handlePossibleCreateOrJoinResponse(const MTPDupdateGroupCall &data);\n\tvoid handlePossibleCreateOrJoinResponse(\n\t\tconst MTPDupdateGroupCallConnection &data);\n\tvoid handleIncomingMessage(const MTPDupdateGroupCallMessage &data);\n\tvoid handleIncomingMessage(\n\t\tconst MTPDupdateGroupCallEncryptedMessage &data);\n\tvoid handleDeleteMessages(const MTPDupdateDeleteGroupCallMessages &data);\n\tvoid handleMessageSent(const MTPDupdateMessageID &data);\n\tvoid changeTitle(const QString &title);\n\tvoid toggleRecording(\n\t\tbool enabled,\n\t\tconst QString &title,\n\t\tbool video,\n\t\tbool videoPortrait);\n\tvoid playSoundRecordingStarted() const;\n\t[[nodiscard]] bool recordingStoppedByMe() const {\n\t\treturn _recordingStoppedByMe;\n\t}\n\tvoid startScheduledNow();\n\tvoid toggleScheduleStartSubscribed(bool subscribed);\n\tvoid setNoiseSuppression(bool enabled);\n\tvoid removeConferenceParticipants(\n\t\tconst base::flat_set<UserId> userIds,\n\t\tbool removingStale = false);\n\n\tbool emitShareScreenError();\n\tbool emitShareCameraError();\n\n\tvoid joinDone(\n\t\tint64 serverTimeMs,\n\t\tconst MTPUpdates &result,\n\t\tMuteState wasMuteState,\n\t\tbool wasVideoStopped,\n\t\tbool justCreated = false);\n\tvoid joinFail(const QString &error);\n\n\t[[nodiscard]] rpl::producer<Group::Error> errors() const {\n\t\treturn _errors.events();\n\t}\n\n\tvoid addVideoOutput(\n\t\tconst std::string &endpoint,\n\t\tnot_null<Webrtc::VideoTrack*> track);\n\n\tvoid setMuted(MuteState mute);\n\tvoid setMutedAndUpdate(MuteState mute);\n\t[[nodiscard]] MuteState muted() const {\n\t\treturn _muted.current();\n\t}\n\t[[nodiscard]] rpl::producer<MuteState> mutedValue() const {\n\t\treturn _muted.value();\n\t}\n\n\t[[nodiscard]] auto otherParticipantStateValue() const\n\t\t-> rpl::producer<Group::ParticipantState>;\n\n\tenum State {\n\t\tCreating,\n\t\tWaiting,\n\t\tJoining,\n\t\tConnecting,\n\t\tJoined,\n\t\tFailedHangingUp,\n\t\tFailed,\n\t\tHangingUp,\n\t\tEnded,\n\t};\n\t[[nodiscard]] State state() const {\n\t\treturn _state.current();\n\t}\n\t[[nodiscard]] rpl::producer<State> stateValue() const {\n\t\treturn _state.value();\n\t}\n\n\tenum class InstanceState {\n\t\tDisconnected,\n\t\tTransitionToRtc,\n\t\tConnected,\n\t};\n\t[[nodiscard]] InstanceState instanceState() const {\n\t\treturn _instanceState.current();\n\t}\n\t[[nodiscard]] rpl::producer<InstanceState> instanceStateValue() const {\n\t\treturn _instanceState.value();\n\t}\n\n\t[[nodiscard]] rpl::producer<LevelUpdate> levelUpdates() const {\n\t\treturn _levelUpdates.events();\n\t}\n\t[[nodiscard]] auto videoStreamActiveUpdates() const\n\t-> rpl::producer<VideoStateToggle> {\n\t\treturn _videoStreamActiveUpdates.events();\n\t}\n\t[[nodiscard]] auto videoStreamShownUpdates() const\n\t-> rpl::producer<VideoStateToggle> {\n\t\treturn _videoStreamShownUpdates.events();\n\t}\n\tvoid requestVideoQuality(\n\t\tconst VideoEndpoint &endpoint,\n\t\tGroup::VideoQuality quality);\n\n\t[[nodiscard]] bool videoEndpointPinned() const {\n\t\treturn _videoEndpointPinned.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> videoEndpointPinnedValue() const {\n\t\treturn _videoEndpointPinned.value();\n\t}\n\tvoid pinVideoEndpoint(VideoEndpoint endpoint);\n\n\tvoid showVideoEndpointLarge(VideoEndpoint endpoint);\n\t[[nodiscard]] const VideoEndpoint &videoEndpointLarge() const {\n\t\treturn _videoEndpointLarge.current();\n\t}\n\t[[nodiscard]] auto videoEndpointLargeValue() const\n\t-> rpl::producer<VideoEndpoint> {\n\t\treturn _videoEndpointLarge.value();\n\t}\n\t[[nodiscard]] auto activeVideoTracks() const\n\t-> const base::flat_map<VideoEndpoint, std::unique_ptr<VideoTrack>> & {\n\t\treturn _activeVideoTracks;\n\t}\n\t[[nodiscard]] auto shownVideoTracks() const\n\t-> const base::flat_set<VideoEndpoint> & {\n\t\treturn _shownVideoTracks;\n\t}\n\t[[nodiscard]] rpl::producer<Group::RejoinEvent> rejoinEvents() const {\n\t\treturn _rejoinEvents.events();\n\t}\n\t[[nodiscard]] rpl::producer<> allowedToSpeakNotifications() const {\n\t\treturn _allowedToSpeakNotifications.events();\n\t}\n\t[[nodiscard]] rpl::producer<> titleChanged() const {\n\t\treturn _titleChanged.events();\n\t}\n\tstatic constexpr auto kSpeakLevelThreshold = 0.2;\n\n\t[[nodiscard]] bool mutedByAdmin() const;\n\t[[nodiscard]] bool canManage() const;\n\t[[nodiscard]] rpl::producer<bool> canManageValue() const;\n\t[[nodiscard]] bool videoIsWorking() const {\n\t\treturn _videoIsWorking.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> videoIsWorkingValue() const {\n\t\treturn _videoIsWorking.value();\n\t}\n\t[[nodiscard]] bool messagesEnabled() const {\n\t\treturn _messagesEnabled.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> messagesEnabledValue() const {\n\t\treturn _messagesEnabled.value();\n\t}\n\n\t[[nodiscard]] bool isSharingScreen() const;\n\t[[nodiscard]] rpl::producer<bool> isSharingScreenValue() const;\n\t[[nodiscard]] bool isScreenPaused() const;\n\t[[nodiscard]] const std::string &screenSharingEndpoint() const;\n\t[[nodiscard]] bool isSharingCamera() const;\n\t[[nodiscard]] rpl::producer<bool> isSharingCameraValue() const;\n\t[[nodiscard]] bool isCameraPaused() const;\n\t[[nodiscard]] const std::string &cameraSharingEndpoint() const;\n\t[[nodiscard]] QString screenSharingDeviceId() const;\n\t[[nodiscard]] bool screenSharingWithAudio() const;\n\tvoid toggleVideo(bool active);\n\tvoid toggleScreenSharing(\n\t\tstd::optional<QString> uniqueId,\n\t\tbool withAudio = false);\n\t[[nodiscard]] bool hasVideoWithFrames() const;\n\t[[nodiscard]] rpl::producer<bool> hasVideoWithFramesValue() const;\n\n\tvoid toggleMute(const Group::MuteRequest &data);\n\tvoid changeVolume(const Group::VolumeRequest &data);\n\n\tvoid inviteUsers(\n\t\tconst std::vector<InviteRequest> &requests,\n\t\tFn<void(InviteResult)> done);\n\n\tstd::shared_ptr<GlobalShortcutManager> ensureGlobalShortcutManager();\n\tvoid applyGlobalShortcutChanges();\n\n\tvoid pushToTalk(bool pressed, crl::time delay);\n\tvoid setNotRequireARGB32();\n\n\t[[nodiscard]] std::function<std::vector<uint8_t>(\n\t\tstd::vector<uint8_t> const &,\n\t\tint64_t, bool,\n\t\tint32_t)> e2eEncryptDecrypt() const;\n\tvoid sendMessage(TextWithTags message);\n\t[[nodiscard]] MTPInputGroupCall inputCall() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tclass LoadPartTask;\n\tclass MediaChannelDescriptionsTask;\n\tclass RequestCurrentTimeTask;\n\tusing GlobalShortcutValue = base::GlobalShortcutValue;\n\tusing Error = Group::Error;\n\tstruct SinkPointer;\n\n\tstatic constexpr uint32 kDisabledSsrc = uint32(-1);\n\tstatic constexpr int kSubChainsCount = 2;\n\n\tstruct LoadingPart {\n\t\tstd::shared_ptr<LoadPartTask> task;\n\t\tmtpRequestId requestId = 0;\n\t};\n\n\tenum class FinishType {\n\t\tNone,\n\t\tEnded,\n\t\tFailed,\n\t};\n\tenum class InstanceMode {\n\t\tNone,\n\t\tRtc,\n\t\tStream,\n\t};\n\tenum class SendUpdateType {\n\t\tMute          = 0x01,\n\t\tRaiseHand     = 0x02,\n\t\tCameraStopped = 0x04,\n\t\tCameraPaused  = 0x08,\n\t\tScreenPaused  = 0x10,\n\t};\n\tenum class JoinAction {\n\t\tNone,\n\t\tJoining,\n\t\tLeaving,\n\t};\n\tstruct JoinPayload {\n\t\tuint32 ssrc = 0;\n\t\tQByteArray json;\n\t};\n\tstruct JoinState {\n\t\tuint32 ssrc = 0;\n\t\tJoinAction action = JoinAction::None;\n\t\tJoinPayload payload;\n\t\tbool nextActionPending = false;\n\n\t\tvoid finish(uint32 updatedSsrc = 0) {\n\t\t\taction = JoinAction::None;\n\t\t\tssrc = updatedSsrc;\n\t\t}\n\t};\n\tstruct SubChainPending {\n\t\tQVector<MTPbytes> blocks;\n\t\tint next = 0;\n\t};\n\tstruct SubChainState {\n\t\tstd::vector<SubChainPending> pending;\n\t\tmtpRequestId requestId = 0;\n\t\tbool inShortPoll = false;\n\t};\n\n\tfriend inline constexpr bool is_flag_type(SendUpdateType) {\n\t\treturn true;\n\t}\n\n\tGroupCall(\n\t\tnot_null<Delegate*> delegate,\n\t\tGroup::JoinInfo join,\n\t\tStartConferenceInfo conference,\n\t\tconst MTPInputGroupCall &inputCall);\n\n\tvoid broadcastPartStart(std::shared_ptr<LoadPartTask> task);\n\tvoid broadcastPartCancel(not_null<LoadPartTask*> task);\n\tvoid mediaChannelDescriptionsStart(\n\t\tstd::shared_ptr<MediaChannelDescriptionsTask> task);\n\tvoid mediaChannelDescriptionsCancel(\n\t\tnot_null<MediaChannelDescriptionsTask*> task);\n\tvoid requestCurrentTimeStart(\n\t\tstd::shared_ptr<RequestCurrentTimeTask> task);\n\tvoid requestCurrentTimeCancel(\n\t\tnot_null<RequestCurrentTimeTask*> task);\n\t[[nodiscard]] int64 approximateServerTimeInMs() const;\n\n\t[[nodiscard]] bool mediaChannelDescriptionsFill(\n\t\tnot_null<MediaChannelDescriptionsTask*> task,\n\t\tFn<bool(uint32)> resolved = nullptr);\n\tvoid checkMediaChannelDescriptions(Fn<bool(uint32)> resolved = nullptr);\n\n\tvoid handlePossibleCreateOrJoinResponse(const MTPDgroupCall &data);\n\tvoid handlePossibleDiscarded(const MTPDgroupCallDiscarded &data);\n\tvoid handleUpdate(const MTPDupdateGroupCall &data);\n\tvoid handleUpdate(const MTPDupdateGroupCallParticipants &data);\n\tvoid handleUpdate(const MTPDupdateGroupCallChainBlocks &data);\n\tvoid applySubChainUpdate(\n\t\tint subchain,\n\t\tconst QVector<MTPbytes> &blocks,\n\t\tint next);\n\t[[nodiscard]] auto lookupVideoCodecPreferences() const\n\t\t-> std::vector<tgcalls::VideoCodecName>;\n\tbool tryCreateController();\n\tvoid destroyController();\n\tbool tryCreateScreencast();\n\tvoid destroyScreencast();\n\n\tvoid emitShareCameraError(Error error);\n\tvoid emitShareScreenError(Error error);\n\n\tvoid setState(State state);\n\tvoid finish(FinishType type);\n\tvoid maybeSendMutedUpdate(MuteState previous);\n\tvoid sendSelfUpdate(SendUpdateType type);\n\tvoid updateInstanceMuteState();\n\tvoid updateInstanceVolumes();\n\tvoid updateInstanceVolume(\n\t\tconst std::optional<Data::GroupCallParticipant> &was,\n\t\tconst Data::GroupCallParticipant &now);\n\tvoid applyMeInCallLocally();\n\tvoid startRejoin();\n\tvoid rejoin();\n\tvoid leave();\n\tvoid rejoin(not_null<PeerData*> as);\n\tvoid setJoinAs(not_null<PeerData*> as);\n\tvoid saveDefaultJoinAs(not_null<PeerData*> as);\n\tvoid subscribeToReal(not_null<Data::GroupCall*> real);\n\tvoid setScheduledDate(TimeId date);\n\tvoid setMessagesEnabled(bool enabled);\n\tvoid rejoinPresentation();\n\tvoid leavePresentation();\n\tvoid checkNextJoinAction();\n\tvoid sendJoinRequest();\n\tvoid refreshLastBlockAndJoin();\n\tvoid requestSubchainBlocks(int subchain, int height);\n\tvoid sendOutboundBlock(QByteArray block);\n\n\tvoid audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data);\n\tvoid setInstanceConnected(tgcalls::GroupNetworkState networkState);\n\tvoid setInstanceMode(InstanceMode mode);\n\tvoid setScreenInstanceConnected(tgcalls::GroupNetworkState networkState);\n\tvoid setScreenInstanceMode(InstanceMode mode);\n\tvoid checkLastSpoke();\n\tvoid pushToTalkCancel();\n\n\tvoid checkGlobalShortcutAvailability();\n\tvoid checkJoined();\n\tvoid checkFirstTimeJoined();\n\tvoid notifyAboutAllowedToSpeak();\n\n\tvoid playConnectingSound();\n\tvoid stopConnectingSound();\n\tvoid playConnectingSoundOnce();\n\n\tvoid updateRequestedVideoChannels();\n\tvoid updateRequestedVideoChannelsDelayed();\n\tvoid fillActiveVideoEndpoints();\n\n\tvoid editParticipant(\n\t\tnot_null<PeerData*> participantPeer,\n\t\tbool mute,\n\t\tstd::optional<int> volume);\n\tvoid applyParticipantLocally(\n\t\tnot_null<PeerData*> participantPeer,\n\t\tbool mute,\n\t\tstd::optional<int> volume);\n\tvoid applyQueuedSelfUpdates();\n\tvoid sendPendingSelfUpdates();\n\tvoid applySelfUpdate(const MTPDgroupCallParticipant &data);\n\tvoid applyOtherParticipantUpdate(const MTPDgroupCallParticipant &data);\n\n\tvoid captureMuteChanged(bool mute) override;\n\trpl::producer<Webrtc::DeviceResolvedId> captureMuteDeviceId() override;\n\n\tvoid setupMediaDevices();\n\tvoid setupOutgoingVideo();\n\tvoid initConferenceE2E();\n\tvoid setupConferenceCall();\n\tvoid trackParticipantsWithAccess();\n\tvoid setScreenEndpoint(std::string endpoint);\n\tvoid setCameraEndpoint(std::string endpoint);\n\tvoid addVideoOutput(const std::string &endpoint, SinkPointer sink);\n\tvoid setVideoEndpointLarge(VideoEndpoint endpoint);\n\n\tvoid markEndpointActive(\n\t\tVideoEndpoint endpoint,\n\t\tbool active,\n\t\tbool paused);\n\tvoid markTrackPaused(const VideoEndpoint &endpoint, bool paused);\n\tvoid markTrackShown(const VideoEndpoint &endpoint, bool shown);\n\n\tvoid processConferenceStart(StartConferenceInfo conference);\n\tvoid inviteToConference(\n\t\tInviteRequest request,\n\t\tFn<not_null<InviteResult*>()> resultAddress,\n\t\tFn<void()> finishRequest);\n\n\t[[nodiscard]] float64 singleSourceVolumeValue() const;\n\t[[nodiscard]] int activeVideoSendersCount() const;\n\n\t[[nodiscard]] MTPInputGroupCall inputCallSafe() const;\n\n\tconst not_null<Delegate*> _delegate;\n\tstd::shared_ptr<Data::GroupCall> _sharedCall;\n\tstd::unique_ptr<TdE2E::Call> _e2e;\n\tstd::shared_ptr<TdE2E::EncryptDecrypt> _e2eEncryptDecrypt;\n\trpl::variable<QByteArray> _emojiHash;\n\tQByteArray _pendingOutboundBlock;\n\tstd::shared_ptr<StartConferenceInfo> _startConferenceInfo;\n\n\tnot_null<PeerData*> _peer; // Can change in legacy group migration.\n\trpl::event_stream<PeerData*> _peerStream;\n\tnot_null<History*> _history; // Can change in legacy group migration.\n\tMTP::Sender _api;\n\trpl::event_stream<not_null<Data::GroupCall*>> _realChanges;\n\trpl::variable<State> _state = State::Creating;\n\tbase::flat_set<uint32> _unresolvedSsrcs;\n\trpl::event_stream<Error> _errors;\n\tstd::vector<Fn<void()>> _rejoinedCallbacks;\n\tconst std::unique_ptr<Group::Messages> _messages;\n\tbool _recordingStoppedByMe = false;\n\tbool _requestedVideoChannelsUpdateScheduled = false;\n\n\tMTP::DcId _broadcastDcId = 0;\n\tbase::flat_map<not_null<LoadPartTask*>, LoadingPart> _broadcastParts;\n\tbase::flat_set<\n\t\tstd::shared_ptr<MediaChannelDescriptionsTask>,\n\t\tbase::pointer_comparator<\n\t\t\tMediaChannelDescriptionsTask>> _mediaChannelDescriptionses;\n\tbase::flat_set<\n\t\tstd::shared_ptr<RequestCurrentTimeTask>,\n\t\tbase::pointer_comparator<\n\t\t\tRequestCurrentTimeTask>> _requestCurrentTimes;\n\tmtpRequestId _requestCurrentTimeRequestId = 0;\n\n\trpl::variable<not_null<PeerData*>> _joinAs;\n\tstd::vector<not_null<PeerData*>> _possibleJoinAs;\n\tQString _joinHash;\n\tQString _conferenceLinkSlug;\n\tMsgId _conferenceJoinMessageId;\n\tint64 _serverTimeMs = 0;\n\tcrl::time _serverTimeMsGotAt = 0;\n\n\tQString _rtmpUrl;\n\tQString _rtmpKey;\n\n\trpl::variable<MuteState> _muted = MuteState::Muted;\n\trpl::variable<bool> _canManage = false;\n\trpl::variable<bool> _videoIsWorking = false;\n\trpl::variable<bool> _emptyRtmp = false;\n\trpl::variable<bool> _messagesEnabled = false;\n\tbool _initialMuteStateSent = false;\n\tbool _acceptFields = false;\n\n\trpl::event_stream<Group::ParticipantState> _otherParticipantStateValue;\n\tstd::vector<MTPGroupCallParticipant> _queuedSelfUpdates;\n\n\tCallId _id = 0;\n\tCallId _accessHash = 0;\n\tJoinState _joinState;\n\tJoinState _screenJoinState;\n\tstd::string _cameraEndpoint;\n\tstd::string _screenEndpoint;\n\tTimeId _scheduleDate = 0;\n\tbase::flat_set<uint32> _mySsrcs;\n\tmtpRequestId _createRequestId = 0;\n\tmtpRequestId _selfUpdateRequestId = 0;\n\n\trpl::variable<InstanceState> _instanceState\n\t\t= InstanceState::Disconnected;\n\tbool _instanceTransitioning = false;\n\tInstanceMode _instanceMode = InstanceMode::None;\n\tstd::unique_ptr<tgcalls::GroupInstanceCustomImpl> _instance;\n\tbase::has_weak_ptr _instanceGuard;\n\tstd::shared_ptr<tgcalls::VideoCaptureInterface> _cameraCapture;\n\trpl::variable<Webrtc::VideoState> _cameraState;\n\trpl::variable<bool> _isSharingCamera = false;\n\tbase::flat_map<std::string, SinkPointer> _pendingVideoOutputs;\n\n\trpl::variable<InstanceState> _screenInstanceState\n\t\t= InstanceState::Disconnected;\n\tInstanceMode _screenInstanceMode = InstanceMode::None;\n\tstd::unique_ptr<tgcalls::GroupInstanceCustomImpl> _screenInstance;\n\tbase::has_weak_ptr _screenInstanceGuard;\n\tstd::shared_ptr<tgcalls::VideoCaptureInterface> _screenCapture;\n\trpl::variable<Webrtc::VideoState> _screenState;\n\trpl::variable<bool> _isSharingScreen = false;\n\tQString _screenDeviceId;\n\tbool _screenWithAudio = false;\n\n\tbase::flags<SendUpdateType> _pendingSelfUpdates;\n\tbool _requireARGB32 = true;\n\n\trpl::event_stream<LevelUpdate> _levelUpdates;\n\trpl::event_stream<VideoStateToggle> _videoStreamActiveUpdates;\n\trpl::event_stream<VideoStateToggle> _videoStreamPausedUpdates;\n\trpl::event_stream<VideoStateToggle> _videoStreamShownUpdates;\n\tbase::flat_map<\n\t\tVideoEndpoint,\n\t\tstd::unique_ptr<VideoTrack>> _activeVideoTracks;\n\tbase::flat_set<VideoEndpoint> _shownVideoTracks;\n\trpl::variable<VideoEndpoint> _videoEndpointLarge;\n\trpl::variable<bool> _videoEndpointPinned = false;\n\tcrl::time _videoLargeTillTime = 0;\n\tbase::flat_map<uint32, Data::LastSpokeTimes> _lastSpoke;\n\trpl::event_stream<Group::RejoinEvent> _rejoinEvents;\n\trpl::event_stream<> _allowedToSpeakNotifications;\n\trpl::event_stream<> _titleChanged;\n\tbase::Timer _lastSpokeCheckTimer;\n\tbase::Timer _checkJoinedTimer;\n\n\tcrl::time _lastSendProgressUpdate = 0;\n\n\tFn<void(Webrtc::DeviceResolvedId)> _setDeviceIdCallback;\n\tWebrtc::DeviceResolver _playbackDeviceId;\n\tWebrtc::DeviceResolver _captureDeviceId;\n\tWebrtc::DeviceResolver _cameraDeviceId;\n\n\tstd::shared_ptr<GlobalShortcutManager> _shortcutManager;\n\tstd::shared_ptr<GlobalShortcutValue> _pushToTalk;\n\tbase::Timer _pushToTalkCancelTimer;\n\tbase::Timer _connectingSoundTimer;\n\tbool _hadJoinedState = false;\n\tbool _listenersHidden = false;\n\tbool _rtmp = false;\n\tbool _reloadedStaleCall = false;\n\tint _singleSourceVolume = 0;\n\n\tSubChainState _subchains[kSubChainsCount];\n\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] TextWithEntities ComposeInviteResultToast(\n\tconst InviteResult &result);\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_common.h\"\n\n#include \"apiwrap.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/random.h\"\n#include \"boxes/peers/replace_boost_box.h\" // CreateUserpicsWithMoreBadge\n#include \"boxes/share_box.h\"\n#include \"calls/calls_instance.h\"\n#include \"core/application.h\"\n#include \"core/local_url_handlers.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_session.h\"\n#include \"info/bot/starref/info_bot_starref_common.h\"\n#include \"tde2e/tde2e_api.h\"\n#include \"tde2e/tde2e_integration.h\"\n#include \"ui/boxes/boost_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_media_view.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_chat.h\"\n\n#include <QtWidgets/QApplication>\n#include <QtGui/QClipboard>\n\nnamespace Calls::Group {\n\nobject_ptr<Ui::GenericBox> ScreenSharingPrivacyRequestBox() {\n#ifdef Q_OS_MAC\n\tif (!Platform::IsMac10_15OrGreater()) {\n\t\treturn { nullptr };\n\t}\n\treturn Box([=](not_null<Ui::GenericBox*> box) {\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox.get(),\n\t\t\t\trpl::combine(\n\t\t\t\t\ttr::lng_group_call_mac_screencast_access(),\n\t\t\t\t\ttr::lng_group_call_mac_recording()\n\t\t\t\t) | rpl::map([](QString a, QString b) {\n\t\t\t\t\tauto result = tr::rich(a);\n\t\t\t\t\tresult.append(\"\\n\\n\").append(tr::rich(b));\n\t\t\t\t\treturn result;\n\t\t\t\t}),\n\t\t\t\tst::groupCallBoxLabel),\n\t\t\tstyle::margins(\n\t\t\t\tst::boxRowPadding.left(),\n\t\t\t\tst::boxPadding.top(),\n\t\t\t\tst::boxRowPadding.right(),\n\t\t\t\tst::boxPadding.bottom()));\n\t\tbox->addButton(tr::lng_group_call_mac_settings(), [=] {\n\t\t\tPlatform::OpenDesktopCapturePrivacySettings();\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t});\n#else // Q_OS_MAC\n\treturn { nullptr };\n#endif // Q_OS_MAC\n}\n\nvoid ShowUniqueCaptureOptions(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tFn<void(bool withAudio)> done) {\n\tshow->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tbox->setTitle(tr::lng_group_call_sharing_screen_options());\n\t\tconst auto withAudio = box->addRow(\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_group_call_screen_share_audio(tr::now),\n\t\t\t\tfalse,\n\t\t\t\tst::groupCallCheckbox));\n\t\tbox->addButton(\n\t\t\ttr::lng_group_call_choose_source(),\n\t\t\t[=] {\n\t\t\t\tconst auto audio = withAudio->checked();\n\t\t\t\tbox->closeBox();\n\t\t\t\tdone(audio);\n\t\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t}));\n}\n\nobject_ptr<Ui::RpWidget> MakeRoundActiveLogo(\n\t\tnot_null<QWidget*> parent,\n\t\tconst style::icon &icon,\n\t\tconst style::margins &padding) {\n\tconst auto logoSize = icon.size();\n\tconst auto logoOuter = logoSize.grownBy(padding);\n\tauto result = object_ptr<Ui::RpWidget>(parent);\n\tconst auto logo = result.data();\n\tlogo->resize(logo->width(), logoOuter.height());\n\tlogo->paintRequest() | rpl::on_next([=, &icon] {\n\t\tif (logo->width() < logoOuter.width()) {\n\t\t\treturn;\n\t\t}\n\t\tauto p = QPainter(logo);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto x = (logo->width() - logoOuter.width()) / 2;\n\t\tconst auto outer = QRect(QPoint(x, 0), logoOuter);\n\t\tp.setBrush(st::windowBgActive);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawEllipse(outer);\n\t\ticon.paintInCenter(p, outer);\n\t}, logo->lifetime());\n\treturn result;\n}\n\nobject_ptr<Ui::RpWidget> MakeJoinCallLogo(not_null<QWidget*> parent) {\n\treturn MakeRoundActiveLogo(\n\t\tparent,\n\t\tst::confcallJoinLogo,\n\t\tst::confcallJoinLogoPadding);\n}\n\nvoid ConferenceCallJoinConfirm(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<Data::GroupCall> call,\n\t\tUserData *maybeInviter,\n\t\tFn<void(Fn<void()> close)> join) {\n\tbox->setStyle(st::confcallJoinBox);\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setNoContentMargin(true);\n\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\tbox->closeBox();\n\t});\n\n\tbox->addRow(\n\t\tMakeJoinCallLogo(box),\n\t\tst::boxRowPadding + st::confcallLinkHeaderIconPadding);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_confcall_join_title(),\n\t\t\tst::boxTitle),\n\t\tst::boxRowPadding + st::confcallLinkTitlePadding,\n\t\tstyle::al_top);\n\tconst auto wrapName = [&](not_null<PeerData*> peer) {\n\t\treturn rpl::single(tr::bold(peer->shortName()));\n\t};\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\t(maybeInviter\n\t\t\t\t? tr::lng_confcall_join_text_inviter(\n\t\t\t\t\tlt_user,\n\t\t\t\t\twrapName(maybeInviter),\n\t\t\t\t\ttr::rich)\n\t\t\t\t: tr::lng_confcall_join_text(tr::rich)),\n\t\t\tst::confcallLinkCenteredText),\n\t\tst::boxRowPadding,\n\t\tstyle::al_top\n\t)->setTryMakeSimilarLines(true);\n\n\tconst auto &participants = call->participants();\n\tconst auto known = int(participants.size());\n\tif (known) {\n\t\tconst auto sep = box->addRow(\n\t\t\tobject_ptr<Ui::RpWidget>(box),\n\t\t\tst::boxRowPadding + st::confcallJoinSepPadding);\n\t\tsep->resize(sep->width(), st::normalFont->height);\n\t\tsep->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(sep);\n\t\t\tconst auto line = st::lineWidth;\n\t\t\tconst auto top = st::confcallLinkFooterOrLineTop;\n\t\t\tconst auto fg = st::windowSubTextFg->b;\n\t\t\tp.setOpacity(0.2);\n\t\t\tp.fillRect(0, top, sep->width(), line, fg);\n\t\t}, sep->lifetime());\n\n\t\tauto peers = std::vector<not_null<PeerData*>>();\n\t\tfor (const auto &participant : participants) {\n\t\t\tpeers.push_back(participant.peer);\n\t\t\tif (peers.size() == 3) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tbox->addRow(\n\t\t\tCreateUserpicsWithMoreBadge(\n\t\t\t\tbox,\n\t\t\t\trpl::single(peers),\n\t\t\t\tst::confcallJoinUserpics,\n\t\t\t\tknown),\n\t\t\tst::boxRowPadding + st::confcallJoinUserpicsPadding);\n\n\t\tconst auto wrapByIndex = [&](int index) {\n\t\t\tExpects(index >= 0 && index < known);\n\n\t\t\treturn wrapName(participants[index].peer);\n\t\t};\n\t\tauto text = (known == 1)\n\t\t\t? tr::lng_confcall_already_joined_one(\n\t\t\t\tlt_user,\n\t\t\t\twrapByIndex(0),\n\t\t\t\ttr::rich)\n\t\t\t: (known == 2)\n\t\t\t? tr::lng_confcall_already_joined_two(\n\t\t\t\tlt_user,\n\t\t\t\twrapByIndex(0),\n\t\t\t\tlt_other,\n\t\t\t\twrapByIndex(1),\n\t\t\t\ttr::rich)\n\t\t\t: (known == 3)\n\t\t\t? tr::lng_confcall_already_joined_three(\n\t\t\t\tlt_user,\n\t\t\t\twrapByIndex(0),\n\t\t\t\tlt_other,\n\t\t\t\twrapByIndex(1),\n\t\t\t\tlt_third,\n\t\t\t\twrapByIndex(2),\n\t\t\t\ttr::rich)\n\t\t\t: tr::lng_confcall_already_joined_many(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(1. * (std::max(known, call->fullCount()) - 2)),\n\t\t\t\tlt_user,\n\t\t\t\twrapByIndex(0),\n\t\t\t\tlt_other,\n\t\t\t\twrapByIndex(1),\n\t\t\t\ttr::rich);\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\tstd::move(text),\n\t\t\t\tst::confcallLinkCenteredText),\n\t\t\tst::boxRowPadding,\n\t\t\tstyle::al_top\n\t\t)->setTryMakeSimilarLines(true);\n\t}\n\tbox->addButton(tr::lng_confcall_join_button(), [=] {\n\t\tjoin([weak = base::make_weak(box)] {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t});\n\t});\n}\n\nConferenceCallLinkStyleOverrides DarkConferenceCallLinkStyle() {\n\treturn {\n\t\t.box = &st::groupCallLinkBox,\n\t\t.menuToggle = &st::groupCallLinkMenu,\n\t\t.menu = &st::groupCallPopupMenuWithIcons,\n\t\t.close = &st::storiesStealthBoxClose,\n\t\t.centerLabel = &st::groupCallLinkCenteredText,\n\t\t.linkPreview = &st::groupCallLinkPreview,\n\t\t.contextRevoke = &st::mediaMenuIconRemove,\n\t\t.shareBox = std::make_shared<ShareBoxStyleOverrides>(\n\t\t\tDarkShareBoxStyle()),\n\t};\n}\n\nvoid ShowConferenceCallLinkBox(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tstd::shared_ptr<Data::GroupCall> call,\n\t\tconst ConferenceCallLinkArgs &args) {\n\tconst auto st = args.st;\n\tconst auto initial = args.initial;\n\tconst auto link = call->conferenceInviteLink();\n\tshow->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tstruct State {\n\t\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t\t\tbool resetting = false;\n\t\t};\n\t\tconst auto state = box->lifetime().make_state<State>();\n\n\t\tbox->setStyle(st.box\n\t\t\t? *st.box\n\t\t\t: initial\n\t\t\t? st::confcallLinkBoxInitial\n\t\t\t: st::confcallLinkBox);\n\t\tbox->setWidth(st::boxWideWidth);\n\t\tbox->setNoContentMargin(true);\n\t\tconst auto close = box->addTopButton(\n\t\t\tst.close ? *st.close : st::boxTitleClose,\n\t\t\t[=] { box->closeBox(); });\n\n\t\tif (!args.initial && call->canManage()) {\n\t\t\tconst auto toggle = Ui::CreateChild<Ui::IconButton>(\n\t\t\t\tclose->parentWidget(),\n\t\t\t\tst.menuToggle ? *st.menuToggle : st::boxTitleMenu);\n\t\t\tconst auto handler = [=] {\n\t\t\t\tif (state->resetting) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tstate->resetting = true;\n\t\t\t\tusing Flag = MTPphone_ToggleGroupCallSettings::Flag;\n\t\t\t\tconst auto weak = base::make_weak(box);\n\t\t\t\tcall->session().api().request(\n\t\t\t\t\tMTPphone_ToggleGroupCallSettings(\n\t\t\t\t\t\tMTP_flags(Flag::f_reset_invite_hash),\n\t\t\t\t\t\tcall->input(),\n\t\t\t\t\t\tMTPBool(), // join_muted\n\t\t\t\t\t\tMTPBool(), // messages_enabled\n\t\t\t\t\t\tMTPlong()) // send_paid_messages_stars\n\t\t\t\t).done([=](const MTPUpdates &result) {\n\t\t\t\t\tcall->session().api().applyUpdates(result);\n\t\t\t\t\tShowConferenceCallLinkBox(show, call, args);\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t\tshow->showToast({\n\t\t\t\t\t\t.title = tr::lng_confcall_link_revoked_title(\n\t\t\t\t\t\t\ttr::now),\n\t\t\t\t\t\t.text = {\n\t\t\t\t\t\t\ttr::lng_confcall_link_revoked_text(tr::now),\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}).send();\n\t\t\t};\n\t\t\ttoggle->setClickedCallback([=] {\n\t\t\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\t\ttoggle,\n\t\t\t\t\tst.menu ? *st.menu : st::popupMenuWithIcons);\n\t\t\t\tstate->menu->addAction(\n\t\t\t\t\ttr::lng_confcall_link_revoke(tr::now),\n\t\t\t\t\thandler,\n\t\t\t\t\t(st.contextRevoke\n\t\t\t\t\t\t? st.contextRevoke\n\t\t\t\t\t\t: &st::menuIconRemove));\n\t\t\t\tstate->menu->popup(QCursor::pos());\n\t\t\t});\n\n\t\t\tclose->geometryValue(\n\t\t\t) | rpl::on_next([=](QRect geometry) {\n\t\t\t\ttoggle->moveToLeft(\n\t\t\t\t\tgeometry.x() - toggle->width(),\n\t\t\t\t\tgeometry.y());\n\t\t\t}, close->lifetime());\n\t\t}\n\n\t\tbox->addRow(\n\t\t\tInfo::BotStarRef::CreateLinkHeaderIcon(box, &call->session()),\n\t\t\tst::boxRowPadding + st::confcallLinkHeaderIconPadding);\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_confcall_link_title(),\n\t\t\t\tst.box ? st.box->title : st::boxTitle),\n\t\t\tst::boxRowPadding + st::confcallLinkTitlePadding,\n\t\t\tstyle::al_top);\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_confcall_link_about(),\n\t\t\t\t(st.centerLabel\n\t\t\t\t\t? *st.centerLabel\n\t\t\t\t\t: st::confcallLinkCenteredText)),\n\t\t\tst::boxRowPadding,\n\t\t\tstyle::al_top\n\t\t)->setTryMakeSimilarLines(true);\n\n\t\tUi::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 2);\n\t\tconst auto preview = box->addRow(\n\t\t\tInfo::BotStarRef::MakeLinkLabel(box, link, st.linkPreview));\n\t\tUi::AddSkip(box->verticalLayout());\n\n\t\tconst auto copyCallback = [=] {\n\t\t\tQApplication::clipboard()->setText(link);\n\t\t\tshow->showToast(tr::lng_username_copied(tr::now));\n\t\t};\n\t\tconst auto shareCallback = [=] {\n\t\t\tFastShareLink(\n\t\t\t\tshow,\n\t\t\t\tlink,\n\t\t\t\tst.shareBox ? *st.shareBox : ShareBoxStyleOverrides());\n\t\t};\n\t\tpreview->setClickedCallback(copyCallback);\n\t\tconst auto share = box->addButton(\n\t\t\ttr::lng_group_invite_share(),\n\t\t\tshareCallback,\n\t\t\tst::confcallLinkShareButton);\n\t\tconst auto copy = box->addButton(\n\t\t\ttr::lng_group_invite_copy(),\n\t\t\tcopyCallback,\n\t\t\tst::confcallLinkCopyButton);\n\n\t\trpl::combine(\n\t\t\tbox->widthValue(),\n\t\t\tcopy->widthValue(),\n\t\t\tshare->widthValue()\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto width = st::boxWideWidth;\n\t\t\tconst auto padding = st::confcallLinkBox.buttonPadding;\n\t\t\tconst auto available = width - 2 * padding.right();\n\t\t\tconst auto buttonWidth = (available - padding.left()) / 2;\n\t\t\tcopy->resizeToWidth(buttonWidth);\n\t\t\tshare->resizeToWidth(buttonWidth);\n\t\t\tcopy->moveToLeft(padding.right(), copy->y(), width);\n\t\t\tshare->moveToRight(padding.right(), share->y(), width);\n\t\t}, box->lifetime());\n\n\t\tif (!initial) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto sep = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tcopy->parentWidget(),\n\t\t\ttr::lng_confcall_link_or(),\n\t\t\tst::confcallLinkFooterOr);\n\t\tsep->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(sep);\n\t\t\tconst auto text = sep->textMaxWidth();\n\t\t\tconst auto white = (sep->width() - 2 * text) / 2;\n\t\t\tconst auto line = st::lineWidth;\n\t\t\tconst auto top = st::confcallLinkFooterOrLineTop;\n\t\t\tconst auto fg = st::windowSubTextFg->b;\n\t\t\tp.setOpacity(0.4);\n\t\t\tp.fillRect(0, top, white, line, fg);\n\t\t\tp.fillRect(sep->width() - white, top, white, line, fg);\n\t\t}, sep->lifetime());\n\n\t\tconst auto footer = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tcopy->parentWidget(),\n\t\t\ttr::lng_confcall_link_join(\n\t\t\t\tlt_link,\n\t\t\t\ttr::lng_confcall_link_join_link(\n\t\t\t\t\tlt_arrow,\n\t\t\t\t\trpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\t\t\t[](QString v) { return tr::link(v); }),\n\t\t\t\ttr::marked),\n\t\t\t(st.centerLabel\n\t\t\t\t? *st.centerLabel\n\t\t\t\t: st::confcallLinkCenteredText));\n\t\tfooter->setTryMakeSimilarLines(true);\n\t\tfooter->setClickHandlerFilter([=](const auto &...) {\n\t\t\tif (auto slug = ExtractConferenceSlug(link); !slug.isEmpty()) {\n\t\t\t\tCore::App().calls().startOrJoinConferenceCall({\n\t\t\t\t\t.call = call,\n\t\t\t\t\t.linkSlug = std::move(slug),\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t\tcopy->geometryValue() | rpl::on_next([=](QRect geometry) {\n\t\t\tconst auto width = st::boxWideWidth\n\t\t\t\t- st::boxRowPadding.left()\n\t\t\t\t- st::boxRowPadding.right();\n\t\t\tfooter->resizeToWidth(width);\n\t\t\tconst auto top = geometry.y()\n\t\t\t\t+ geometry.height()\n\t\t\t\t+ st::confcallLinkFooterOrTop;\n\t\t\tsep->resizeToWidth(width / 2);\n\t\t\tsep->move(\n\t\t\t\tst::boxRowPadding.left() + (width - sep->width()) / 2,\n\t\t\t\ttop);\n\t\t\tfooter->moveToLeft(\n\t\t\t\tst::boxRowPadding.left(),\n\t\t\t\ttop + sep->height() + st::confcallLinkFooterOrSkip);\n\t\t}, footer->lifetime());\n\t}));\n}\n\nvoid MakeConferenceCall(ConferenceFactoryArgs &&args) {\n\tconst auto show = std::move(args.show);\n\tconst auto finished = std::move(args.finished);\n\tconst auto session = &show->session();\n\tconst auto fail = [=](QString error) {\n\t\tshow->showToast(error);\n\t\tif (const auto onstack = finished) {\n\t\t\tonstack(false);\n\t\t}\n\t};\n\tsession->api().request(MTPphone_CreateConferenceCall(\n\t\tMTP_flags(0),\n\t\tMTP_int(base::RandomValue<int32>()),\n\t\tMTPint256(), // public_key\n\t\tMTPbytes(), // block\n\t\tMTPDataJSON() // params\n\t)).done([=](const MTPUpdates &result) {\n\t\tauto call = session->data().sharedConferenceCallFind(result);\n\t\tif (!call) {\n\t\t\tfail(u\"Call not found!\"_q);\n\t\t\treturn;\n\t\t}\n\t\tsession->api().applyUpdates(result);\n\n\t\tconst auto link = call ? call->conferenceInviteLink() : QString();\n\t\tif (link.isEmpty()) {\n\t\t\tfail(u\"Call link not found!\"_q);\n\t\t\treturn;\n\t\t}\n\t\tCalls::Group::ShowConferenceCallLinkBox(\n\t\t\tshow,\n\t\t\tcall,\n\t\t\t{ .initial = true });\n\t\tif (const auto onstack = finished) {\n\t\t\tfinished(true);\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tfail(error.type());\n\t}).send();\n}\n\nQString ExtractConferenceSlug(const QString &link) {\n\tconst auto local = Core::TryConvertUrlToLocal(link);\n\tconst auto parts1 = QStringView(local).split('#');\n\tif (!parts1.isEmpty()) {\n\t\tconst auto parts2 = parts1.front().split('&');\n\t\tif (!parts2.isEmpty()) {\n\t\t\tconst auto parts3 = parts2.front().split(u\"slug=\"_q);\n\t\t\tif (parts3.size() > 1) {\n\t\t\t\treturn parts3.back().toString();\n\t\t\t}\n\t\t}\n\t}\n\treturn QString();\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"base/weak_ptr.h\"\n\nclass UserData;\nstruct ShareBoxStyleOverrides;\n\nnamespace style {\nstruct Box;\nstruct FlatLabel;\nstruct IconButton;\nstruct InputField;\nstruct PopupMenu;\n} // namespace style\n\nnamespace Data {\nclass GroupCall;\n} // namespace Data\n\nnamespace Main {\nclass SessionShow;\n} // namespace Main\n\nnamespace Ui {\nclass Show;\nclass RpWidget;\nclass GenericBox;\n} // namespace Ui\n\nnamespace TdE2E {\nclass Call;\n} // namespace TdE2E\n\nnamespace tgcalls {\nclass VideoCaptureInterface;\n} // namespace tgcalls\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Calls {\n\nclass Window;\n\nstruct InviteRequest {\n\tnot_null<UserData*> user;\n\tbool video = false;\n};\n\nstruct InviteResult {\n\tstd::vector<not_null<UserData*>> invited;\n\tstd::vector<not_null<UserData*>> alreadyIn;\n\tstd::vector<not_null<UserData*>> privacyRestricted;\n\tstd::vector<not_null<UserData*>> kicked;\n\tstd::vector<not_null<UserData*>> failed;\n};\n\nstruct StartConferenceInfo {\n\tstd::shared_ptr<Main::SessionShow> show;\n\tstd::shared_ptr<Data::GroupCall> call;\n\tQString linkSlug;\n\tMsgId joinMessageId;\n\tstd::vector<InviteRequest> invite;\n\tbool sharingLink = false;\n\tbool migrating = false;\n\tbool muted = false;\n\tstd::shared_ptr<tgcalls::VideoCaptureInterface> videoCapture;\n\tQString videoCaptureScreenId;\n};\n\nstruct ConferencePanelMigration {\n\tstd::shared_ptr<Window> window;\n};\n\n} // namespace Calls\n\nnamespace Calls::Group {\n\nconstexpr auto kDefaultVolume = 10000;\nconstexpr auto kMaxVolume = 20000;\nconstexpr auto kBlobsEnterDuration = crl::time(250);\n\nstruct MuteRequest {\n\tnot_null<PeerData*> peer;\n\tbool mute = false;\n\tbool locallyOnly = false;\n};\n\nstruct VolumeRequest {\n\tnot_null<PeerData*> peer;\n\tint volume = kDefaultVolume;\n\tbool finalized = true;\n\tbool locallyOnly = false;\n};\n\nstruct ParticipantState {\n\tnot_null<PeerData*> peer;\n\tstd::optional<int> volume;\n\tbool mutedByMe = false;\n\tbool locallyOnly = false;\n};\n\nstruct RejoinEvent {\n\tnot_null<PeerData*> wasJoinAs;\n\tnot_null<PeerData*> nowJoinAs;\n};\n\nstruct RtmpInfo {\n\tQString url;\n\tQString key;\n};\n\nstruct JoinInfo {\n\tnot_null<PeerData*> peer;\n\tnot_null<PeerData*> joinAs;\n\tstd::vector<not_null<PeerData*>> possibleJoinAs;\n\tQString joinHash;\n\tRtmpInfo rtmpInfo;\n\tTimeId scheduleDate = 0;\n\tbool rtmp = false;\n};\n\nenum class PanelMode {\n\tDefault,\n\tWide,\n\tVideoStream,\n};\n\nenum class VideoQuality {\n\tThumbnail,\n\tMedium,\n\tFull,\n};\n\nenum class Error {\n\tNoCamera,\n\tCameraFailed,\n\tScreenFailed,\n\tMutedNoCamera,\n\tMutedNoScreen,\n\tDisabledNoCamera,\n\tDisabledNoScreen,\n};\n\nenum class StickedTooltip {\n\tCamera     = 0x01,\n\tMicrophone = 0x02,\n};\nconstexpr inline bool is_flag_type(StickedTooltip) {\n\treturn true;\n}\nusing StickedTooltips = base::flags<StickedTooltip>;\n\n[[nodiscard]] object_ptr<Ui::GenericBox> ScreenSharingPrivacyRequestBox();\n\nvoid ShowUniqueCaptureOptions(\n\tstd::shared_ptr<Ui::Show> show,\n\tFn<void(bool withAudio)> done);\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeRoundActiveLogo(\n\tnot_null<QWidget*> parent,\n\tconst style::icon &icon,\n\tconst style::margins &padding);\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeJoinCallLogo(\n\tnot_null<QWidget*> parent);\n\nvoid ConferenceCallJoinConfirm(\n\tnot_null<Ui::GenericBox*> box,\n\tstd::shared_ptr<Data::GroupCall> call,\n\tUserData *maybeInviter,\n\tFn<void(Fn<void()> close)> join);\n\nstruct ConferenceCallLinkStyleOverrides {\n\tconst style::Box *box = nullptr;\n\tconst style::IconButton *menuToggle = nullptr;\n\tconst style::PopupMenu *menu = nullptr;\n\tconst style::IconButton *close = nullptr;\n\tconst style::FlatLabel *centerLabel = nullptr;\n\tconst style::InputField *linkPreview = nullptr;\n\tconst style::icon *contextRevoke = nullptr;\n\tstd::shared_ptr<ShareBoxStyleOverrides> shareBox;\n};\n[[nodiscard]] ConferenceCallLinkStyleOverrides DarkConferenceCallLinkStyle();\n\nstruct ConferenceCallLinkArgs {\n\tConferenceCallLinkStyleOverrides st;\n\tbool initial = false;\n};\nvoid ShowConferenceCallLinkBox(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tstd::shared_ptr<Data::GroupCall> call,\n\tconst ConferenceCallLinkArgs &args);\n\nstruct ConferenceFactoryArgs {\n\tstd::shared_ptr<Main::SessionShow> show;\n\tFn<void(bool)> finished;\n\tbool joining = false;\n\tStartConferenceInfo info;\n};\nvoid MakeConferenceCall(ConferenceFactoryArgs &&args);\n\n[[nodiscard]] QString ExtractConferenceSlug(const QString &link);\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_invite_controller.h\"\n\n#include \"api/api_chat_participants.h\"\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/group/calls_group_menu.h\"\n#include \"calls/calls_call.h\"\n#include \"calls/calls_instance.h\"\n#include \"core/application.h\"\n#include \"boxes/peer_lists_box.h\"\n#include \"data/data_user.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_group_call.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"main/session/session_show.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"apiwrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\" // membersMarginTop\n#include \"styles/style_calls.h\"\n#include \"styles/style_dialogs.h\" // searchedBarHeight\n#include \"styles/style_layers.h\" // boxWideWidth\n\nnamespace Calls::Group {\nnamespace {\n\n[[nodiscard]] object_ptr<Ui::RpWidget> CreateSectionSubtitle(\n\t\tQWidget *parent,\n\t\trpl::producer<QString> text) {\n\tauto result = object_ptr<Ui::FixedHeightWidget>(\n\t\tparent,\n\t\tst::searchedBarHeight);\n\n\tconst auto raw = result.data();\n\traw->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(raw);\n\t\tp.fillRect(clip, st::groupCallMembersBgOver);\n\t}, raw->lifetime());\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\tstd::move(text),\n\t\tst::groupCallBoxLabel);\n\traw->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto padding = st::groupCallInviteDividerPadding;\n\t\tconst auto available = width - padding.left() - padding.right();\n\t\tlabel->resizeToNaturalWidth(available);\n\t\tlabel->moveToLeft(padding.left(), padding.top(), width);\n\t}, label->lifetime());\n\n\treturn result;\n}\n\nstruct ConfInviteStyles {\n\tconst style::IconButton *video = nullptr;\n\tconst style::icon *videoActive = nullptr;\n\tconst style::IconButton *audio = nullptr;\n\tconst style::icon *audioActive = nullptr;\n\tconst style::SettingsButton *inviteViaLink = nullptr;\n\tconst style::icon *inviteViaLinkIcon = nullptr;\n};\n\nclass ConfInviteRow final : public PeerListRow {\npublic:\n\tConfInviteRow(not_null<UserData*> user, const ConfInviteStyles &st);\n\n\tvoid setAlreadyIn(bool alreadyIn);\n\tvoid setVideo(bool video);\n\n\tint elementsCount() const override;\n\tQRect elementGeometry(int element, int outerWidth) const override;\n\tbool elementDisabled(int element) const override;\n\tbool elementOnlySelect(int element) const override;\n\tvoid elementAddRipple(\n\t\tint element,\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) override;\n\tvoid elementsStopLastRipple() override;\n\tvoid elementsPaint(\n\t\tPainter &p,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tint selectedElement) override;\n\nprivate:\n\t[[nodiscard]] const style::IconButton &buttonSt(int element) const;\n\n\tconst ConfInviteStyles &_st;\n\tstd::unique_ptr<Ui::RippleAnimation> _videoRipple;\n\tstd::unique_ptr<Ui::RippleAnimation> _audioRipple;\n\tbool _alreadyIn = false;\n\tbool _video = false;\n\n};\n\nstruct PrioritizedSelector {\n\tobject_ptr<Ui::RpWidget> content = { nullptr };\n\tFn<void()> init;\n\tFn<bool(int, int, int)> overrideKey;\n\tFn<void(PeerListRowId)> deselect;\n\tFn<void()> activate;\n\trpl::producer<Ui::ScrollToRequest> scrollToRequests;\n};\n\nclass ConfInviteController final : public ContactsBoxController {\npublic:\n\tConfInviteController(\n\t\tnot_null<Main::Session*> session,\n\t\tConfInviteStyles st,\n\t\tbase::flat_set<not_null<UserData*>> alreadyIn,\n\t\tFn<void()> shareLink,\n\t\tstd::vector<not_null<UserData*>> prioritize);\n\n\t[[nodiscard]] rpl::producer<bool> hasSelectedValue() const;\n\t[[nodiscard]] std::vector<InviteRequest> requests(\n\t\tconst std::vector<not_null<PeerData*>> &peers) const;\n\n\tvoid noSearchSubmit();\n\t[[nodiscard]] auto prioritizeScrollRequests() const\n\t\t-> rpl::producer<Ui::ScrollToRequest>;\n\nprotected:\n\tvoid prepareViewHook() override;\n\n\tstd::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<UserData*> user) override;\n\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowElementClicked(not_null<PeerListRow*> row, int element) override;\n\tbool handleDeselectForeignRow(PeerListRowId itemId) override;\n\n\tbool overrideKeyboardNavigation(\n\t\t\tint direction,\n\t\t\tint fromIndex,\n\t\t\tint toIndex) override;\nprivate:\n\t[[nodiscard]] int fullCount() const;\n\tvoid toggleRowSelected(not_null<PeerListRow*> row, bool video);\n\t[[nodiscard]] bool toggleRowGetChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool video);\n\tvoid addShareLinkButton();\n\tvoid addPriorityInvites();\n\n\tconst ConfInviteStyles _st;\n\tconst base::flat_set<not_null<UserData*>> _alreadyIn;\n\tconst std::vector<not_null<UserData*>> _prioritize;\n\tconst Fn<void()> _shareLink;\n\tPrioritizedSelector _prioritizeRows;\n\trpl::event_stream<Ui::ScrollToRequest> _prioritizeScrollRequests;\n\tbase::flat_set<not_null<UserData*>> _skip;\n\trpl::variable<bool> _hasSelected;\n\tbase::flat_set<not_null<UserData*>> _withVideo;\n\tbool _lastSelectWithVideo = false;\n\n};\n\n[[nodiscard]] ConfInviteStyles ConfInviteDarkStyles() {\n\treturn {\n\t\t.video = &st::confcallInviteVideo,\n\t\t.videoActive = &st::confcallInviteVideoActive,\n\t\t.audio = &st::confcallInviteAudio,\n\t\t.audioActive = &st::confcallInviteAudioActive,\n\t\t.inviteViaLink = &st::groupCallInviteLink,\n\t\t.inviteViaLinkIcon = &st::groupCallInviteLinkIcon,\n\t};\n}\n\n[[nodiscard]] ConfInviteStyles ConfInviteDefaultStyles() {\n\treturn {\n\t\t.video = &st::createCallVideo,\n\t\t.videoActive = &st::createCallVideoActive,\n\t\t.audio = &st::createCallAudio,\n\t\t.audioActive = &st::createCallAudioActive,\n\t\t.inviteViaLink = &st::createCallInviteLink,\n\t\t.inviteViaLinkIcon = &st::createCallInviteLinkIcon,\n\t};\n}\n\nConfInviteRow::ConfInviteRow(not_null<UserData*> user, const ConfInviteStyles &st)\n: PeerListRow(user)\n, _st(st) {\n}\n\nvoid ConfInviteRow::setAlreadyIn(bool alreadyIn) {\n\t_alreadyIn = alreadyIn;\n\tsetDisabledState(alreadyIn ? State::DisabledChecked : State::Active);\n}\n\nvoid ConfInviteRow::setVideo(bool video) {\n\t_video = video;\n}\n\nconst style::IconButton &ConfInviteRow::buttonSt(int element) const {\n\treturn (element == 1)\n\t\t? (_st.video ? *_st.video : st::createCallVideo)\n\t\t: (_st.audio ? *_st.audio : st::createCallAudio);\n}\n\nint ConfInviteRow::elementsCount() const {\n\treturn _alreadyIn ? 0 : 2;\n}\n\nQRect ConfInviteRow::elementGeometry(int element, int outerWidth) const {\n\tif (_alreadyIn || (element != 1 && element != 2)) {\n\t\treturn QRect();\n\t}\n\tconst auto &st = buttonSt(element);\n\tconst auto size = QSize(st.width, st.height);\n\tconst auto margins = (element == 1)\n\t\t? st::createCallVideoMargins\n\t\t: st::createCallAudioMargins;\n\tconst auto right = margins.right();\n\tconst auto top = margins.top();\n\tconst auto side = (element == 1)\n\t\t? outerWidth\n\t\t: elementGeometry(1, outerWidth).x();\n\tconst auto left = side - right - size.width();\n\treturn QRect(QPoint(left, top), size);\n}\n\nbool ConfInviteRow::elementDisabled(int element) const {\n\treturn _alreadyIn\n\t\t|| (checked()\n\t\t\t&& ((_video && element == 1) || (!_video && element == 2)));\n}\n\nbool ConfInviteRow::elementOnlySelect(int element) const {\n\treturn false;\n}\n\nvoid ConfInviteRow::elementAddRipple(\n\t\tint element,\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) {\n\tif (_alreadyIn || (element != 1 && element != 2)) {\n\t\treturn;\n\t}\n\tauto &ripple = (element == 1) ? _videoRipple : _audioRipple;\n\tconst auto &st = buttonSt(element);\n\tif (!ripple) {\n\t\tauto mask = Ui::RippleAnimation::EllipseMask(QSize(\n\t\t\t\tst.rippleAreaSize,\n\t\t\t\tst.rippleAreaSize));\n\t\tripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\tst.ripple,\n\t\t\tstd::move(mask),\n\t\t\tstd::move(updateCallback));\n\t}\n\tripple->add(point - st.rippleAreaPosition);\n}\n\nvoid ConfInviteRow::elementsStopLastRipple() {\n\tif (_videoRipple) {\n\t\t_videoRipple->lastStop();\n\t}\n\tif (_audioRipple) {\n\t\t_audioRipple->lastStop();\n\t}\n}\n\nvoid ConfInviteRow::elementsPaint(\n\t\tPainter &p,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tint selectedElement) {\n\tif (_alreadyIn) {\n\t\treturn;\n\t}\n\tconst auto paintElement = [&](int element) {\n\t\tconst auto &st = buttonSt(element);\n\t\tauto &ripple = (element == 1) ? _videoRipple : _audioRipple;\n\t\tconst auto active = checked() && ((element == 1) ? _video : !_video);\n\t\tconst auto geometry = elementGeometry(element, outerWidth);\n\t\tif (ripple) {\n\t\t\tripple->paint(\n\t\t\t\tp,\n\t\t\t\tgeometry.x() + st.rippleAreaPosition.x(),\n\t\t\t\tgeometry.y() + st.rippleAreaPosition.y(),\n\t\t\t\touterWidth);\n\t\t\tif (ripple->empty()) {\n\t\t\t\tripple.reset();\n\t\t\t}\n\t\t}\n\t\tconst auto selected = (element == selectedElement);\n\t\tconst auto &icon = active\n\t\t\t? (element == 1\n\t\t\t\t? (_st.videoActive\n\t\t\t\t\t? *_st.videoActive\n\t\t\t\t\t: st::createCallVideoActive)\n\t\t\t\t: (_st.audioActive\n\t\t\t\t\t? *_st.audioActive\n\t\t\t\t\t: st::createCallAudioActive))\n\t\t\t: (selected ? st.iconOver : st.icon);\n\t\ticon.paintInCenter(p, geometry);\n\t};\n\tpaintElement(1);\n\tpaintElement(2);\n}\n\n[[nodiscard]] PrioritizedSelector PrioritizedInviteSelector(\n\t\tconst ConfInviteStyles &st,\n\t\tstd::vector<not_null<UserData*>> users,\n\t\tFn<bool(not_null<PeerListRow*>, bool, anim::type)> toggleGetChecked,\n\t\tFn<bool()> lastSelectWithVideo,\n\t\tFn<void(bool)> setLastSelectWithVideo) {\n\tclass PrioritizedController final : public PeerListController {\n\tpublic:\n\t\tPrioritizedController(\n\t\t\tconst ConfInviteStyles &st,\n\t\t\tstd::vector<not_null<UserData*>> users,\n\t\t\tFn<bool(\n\t\t\t\tnot_null<PeerListRow*>,\n\t\t\t\tbool,\n\t\t\t\tanim::type)> toggleGetChecked,\n\t\t\tFn<bool()> lastSelectWithVideo,\n\t\t\tFn<void(bool)> setLastSelectWithVideo)\n\t\t: _st(st)\n\t\t, _users(std::move(users))\n\t\t, _toggleGetChecked(std::move(toggleGetChecked))\n\t\t, _lastSelectWithVideo(std::move(lastSelectWithVideo))\n\t\t, _setLastSelectWithVideo(std::move(setLastSelectWithVideo)) {\n\t\t\tExpects(!_users.empty());\n\t\t}\n\n\t\tvoid prepare() override {\n\t\t\tfor (const auto &user : _users) {\n\t\t\t\tdelegate()->peerListAppendRow(\n\t\t\t\t\tstd::make_unique<ConfInviteRow>(user, _st));\n\t\t\t}\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t\tvoid loadMoreRows() override {\n\t\t}\n\t\tvoid rowClicked(not_null<PeerListRow*> row) override {\n\t\t\ttoggleRowSelected(row, _lastSelectWithVideo());\n\t\t}\n\t\tvoid rowElementClicked(\n\t\t\t\tnot_null<PeerListRow*> row,\n\t\t\t\tint element) override {\n\t\t\tif (row->checked()) {\n\t\t\t\tstatic_cast<ConfInviteRow*>(row.get())->setVideo(\n\t\t\t\t\telement == 1);\n\t\t\t\t_setLastSelectWithVideo(element == 1);\n\t\t\t} else if (element == 1) {\n\t\t\t\ttoggleRowSelected(row, true);\n\t\t\t} else if (element == 2) {\n\t\t\t\ttoggleRowSelected(row, false);\n\t\t\t}\n\t\t}\n\n\t\tvoid toggleRowSelected(not_null<PeerListRow*> row, bool video) {\n\t\t\tdelegate()->peerListSetRowChecked(\n\t\t\t\trow,\n\t\t\t\t_toggleGetChecked(row, video, anim::type::normal));\n\t\t}\n\n\t\tMain::Session &session() const override {\n\t\t\treturn _users.front()->session();\n\t\t}\n\n\t\tvoid toggleFirst() {\n\t\t\trowClicked(delegate()->peerListRowAt(0));\n\t\t}\n\n\tprivate:\n\t\tconst ConfInviteStyles &_st;\n\t\tstd::vector<not_null<UserData*>> _users;\n\t\tFn<bool(not_null<PeerListRow*>, bool, anim::type)> _toggleGetChecked;\n\t\tFn<bool()> _lastSelectWithVideo;\n\t\tFn<void(bool)> _setLastSelectWithVideo;\n\n\t};\n\n\tauto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tconst auto container = result.data();\n\n\tconst auto delegate = container->lifetime().make_state<\n\t\tPeerListContentDelegateSimple\n\t>();\n\tconst auto controller = container->lifetime(\n\t).make_state<PrioritizedController>(\n\t\tst,\n\t\tusers,\n\t\ttoggleGetChecked,\n\t\tlastSelectWithVideo,\n\t\tsetLastSelectWithVideo);\n\tcontroller->setStyleOverrides(&st::createCallList);\n\tconst auto content = container->add(object_ptr<PeerListContent>(\n\t\tcontainer,\n\t\tcontroller));\n\tconst auto activate = [=] {\n\t\tcontent->submitted();\n\t};\n\tcontent->noSearchSubmits() | rpl::on_next([=] {\n\t\tcontroller->toggleFirst();\n\t}, content->lifetime());\n\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\n\tUi::AddDivider(container);\n\n\tconst auto overrideKey = [=](int direction, int from, int to) {\n\t\tif (!content->isVisible()) {\n\t\t\treturn false;\n\t\t} else if (direction > 0 && from < 0 && to >= 0) {\n\t\t\tif (content->hasSelection()) {\n\t\t\t\tconst auto was = content->selectedIndex();\n\t\t\t\tconst auto now = content->selectSkip(1).reallyMovedTo;\n\t\t\t\tif (was != now) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tcontent->clearSelection();\n\t\t\t} else {\n\t\t\t\tcontent->selectSkip(1);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (direction < 0 && to < 0) {\n\t\t\tif (!content->hasSelection()) {\n\t\t\t\tcontent->selectLast();\n\t\t\t} else if (from >= 0 || content->hasSelection()) {\n\t\t\t\tcontent->selectSkip(-1);\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\n\tconst auto deselect = [=](PeerListRowId rowId) {\n\t\tif (const auto row = delegate->peerListFindRow(rowId)) {\n\t\t\tdelegate->peerListSetRowChecked(row, false);\n\t\t}\n\t};\n\n\tconst auto init = [=] {\n\t\tfor (const auto &user : users) {\n\t\t\tif (const auto row = delegate->peerListFindRow(user->id.value)) {\n\t\t\t\tdelegate->peerListSetRowChecked(\n\t\t\t\t\trow,\n\t\t\t\t\ttoggleGetChecked(row, false, anim::type::instant));\n\t\t\t}\n\t\t}\n\t};\n\n\treturn {\n\t\t.content = std::move(result),\n\t\t.init = init,\n\t\t.overrideKey = overrideKey,\n\t\t.deselect = deselect,\n\t\t.activate = activate,\n\t\t.scrollToRequests = content->scrollToRequests(),\n\t};\n}\n\nConfInviteController::ConfInviteController(\n\tnot_null<Main::Session*> session,\n\tConfInviteStyles st,\n\tbase::flat_set<not_null<UserData*>> alreadyIn,\n\tFn<void()> shareLink,\n\tstd::vector<not_null<UserData*>> prioritize)\n: ContactsBoxController(session)\n, _st(st)\n, _alreadyIn(std::move(alreadyIn))\n, _prioritize(std::move(prioritize))\n, _shareLink(std::move(shareLink)) {\n\tif (!_shareLink) {\n\t\t_skip.reserve(_prioritize.size());\n\t\tfor (const auto &user : _prioritize) {\n\t\t\t_skip.emplace(user);\n\t\t}\n\t}\n}\n\nrpl::producer<bool> ConfInviteController::hasSelectedValue() const {\n\treturn _hasSelected.value();\n}\n\nstd::vector<InviteRequest> ConfInviteController::requests(\n\t\tconst std::vector<not_null<PeerData*>> &peers) const {\n\tauto result = std::vector<InviteRequest>();\n\tresult.reserve(peers.size());\n\tfor (const auto &peer : peers) {\n\t\tif (const auto user = peer->asUser()) {\n\t\t\tresult.push_back({ user, _withVideo.contains(user) });\n\t\t}\n\t}\n\treturn result;\n}\n\nstd::unique_ptr<PeerListRow> ConfInviteController::createRow(\n\t\tnot_null<UserData*> user) {\n\tif (user->isSelf()\n\t\t|| user->isBot()\n\t\t|| user->isServiceUser()\n\t\t|| user->isInaccessible()\n\t\t|| _skip.contains(user)) {\n\t\treturn nullptr;\n\t}\n\tauto result = std::make_unique<ConfInviteRow>(user, _st);\n\tif (_alreadyIn.contains(user)) {\n\t\tresult->setAlreadyIn(true);\n\t}\n\tif (_withVideo.contains(user)) {\n\t\tresult->setVideo(true);\n\t}\n\treturn result;\n}\n\nint ConfInviteController::fullCount() const {\n\treturn _alreadyIn.size()\n\t\t+ delegate()->peerListSelectedRowsCount()\n\t\t+ (_alreadyIn.contains(session().user()) ? 1 : 0);\n}\n\nvoid ConfInviteController::rowClicked(not_null<PeerListRow*> row) {\n\ttoggleRowSelected(row, _lastSelectWithVideo);\n}\n\nvoid ConfInviteController::rowElementClicked(\n\t\tnot_null<PeerListRow*> row,\n\t\tint element) {\n\tif (row->checked()) {\n\t\tstatic_cast<ConfInviteRow*>(row.get())->setVideo(element == 1);\n\t\t_lastSelectWithVideo = (element == 1);\n\t} else if (element == 1) {\n\t\ttoggleRowSelected(row, true);\n\t} else if (element == 2) {\n\t\ttoggleRowSelected(row, false);\n\t}\n}\n\nbool ConfInviteController::handleDeselectForeignRow(PeerListRowId itemId) {\n\tif (_prioritizeRows.deselect) {\n\t\tconst auto userId = peerToUser(PeerId(itemId));\n\t\tif (ranges::contains(_prioritize, session().data().user(userId))) {\n\t\t\t_prioritizeRows.deselect(itemId);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool ConfInviteController::overrideKeyboardNavigation(\n\t\tint direction,\n\t\tint fromIndex,\n\t\tint toIndex) {\n\treturn _prioritizeRows.overrideKey\n\t\t&& _prioritizeRows.overrideKey(direction, fromIndex, toIndex);\n}\n\nvoid ConfInviteController::toggleRowSelected(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool video) {\n\tdelegate()->peerListSetRowChecked(row, toggleRowGetChecked(row, video));\n\n\t// row may have been destroyed here, from search.\n\t_hasSelected = (delegate()->peerListSelectedRowsCount() > 0);\n}\n\nbool ConfInviteController::toggleRowGetChecked(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool video) {\n\tauto count = fullCount();\n\tconst auto conferenceLimit = session().appConfig().confcallSizeLimit();\n\tif (!row->checked() && count >= conferenceLimit) {\n\t\tdelegate()->peerListUiShow()->showToast(\n\t\t\ttr::lng_group_call_invite_limit(tr::now));\n\t\treturn false;\n\t}\n\tconst auto real = static_cast<ConfInviteRow*>(row.get());\n\tif (!row->checked()) {\n\t\treal->setVideo(video);\n\t\t_lastSelectWithVideo = video;\n\t}\n\tconst auto user = row->peer()->asUser();\n\tif (!row->checked() && video) {\n\t\t_withVideo.emplace(user);\n\t} else {\n\t\t_withVideo.remove(user);\n\t}\n\treturn !row->checked();\n}\n\nvoid ConfInviteController::noSearchSubmit() {\n\tif (const auto onstack = _prioritizeRows.activate) {\n\t\tonstack();\n\t} else if (delegate()->peerListFullRowsCount() > 0) {\n\t\trowClicked(delegate()->peerListRowAt(0));\n\t}\n}\n\nauto ConfInviteController::prioritizeScrollRequests() const\n-> rpl::producer<Ui::ScrollToRequest> {\n\treturn _prioritizeScrollRequests.events();\n}\n\nvoid ConfInviteController::prepareViewHook() {\n\tif (_shareLink) {\n\t\taddShareLinkButton();\n\t} else if (!_prioritize.empty()) {\n\t\taddPriorityInvites();\n\t}\n}\n\nvoid ConfInviteController::addPriorityInvites() {\n\tconst auto toggleGetChecked = [=](\n\t\t\tnot_null<PeerListRow*> row,\n\t\t\tbool video,\n\t\t\tanim::type animated) {\n\t\tconst auto result = toggleRowGetChecked(row, video);\n\t\tdelegate()->peerListSetForeignRowChecked(\n\t\t\trow,\n\t\t\tresult,\n\t\t\tanimated);\n\n\t\t_hasSelected = (delegate()->peerListSelectedRowsCount() > 0);\n\n\t\treturn result;\n\t};\n\t_prioritizeRows = PrioritizedInviteSelector(\n\t\t_st,\n\t\t_prioritize,\n\t\ttoggleGetChecked,\n\t\t[=] { return _lastSelectWithVideo; },\n\t\t[=](bool video) { _lastSelectWithVideo = video; });\n\tif (auto &scrollTo = _prioritizeRows.scrollToRequests) {\n\t\tstd::move(\n\t\t\tscrollTo\n\t\t) | rpl::start_to_stream(_prioritizeScrollRequests, lifetime());\n\t}\n\tif (const auto onstack = _prioritizeRows.init) {\n\t\tonstack();\n\n\t\t// Force finishing in instant adding checked rows bunch.\n\t\tdelegate()->peerListAddSelectedPeers(\n\t\t\tstd::vector<not_null<PeerData*>>());\n\t}\n\tdelegate()->peerListSetAboveWidget(std::move(_prioritizeRows.content));\n}\n\nvoid ConfInviteController::addShareLinkButton() {\n\tauto button = object_ptr<Ui::PaddingWrap<Ui::SettingsButton>>(\n\t\tnullptr,\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\tnullptr,\n\t\t\ttr::lng_profile_add_via_link(),\n\t\t\t(_st.inviteViaLink\n\t\t\t\t? *_st.inviteViaLink\n\t\t\t\t: st::createCallInviteLink)),\n\t\tstyle::margins(0, st::membersMarginTop, 0, 0));\n\n\tconst auto icon = Ui::CreateChild<Info::Profile::FloatingIcon>(\n\t\tbutton->entity(),\n\t\t(_st.inviteViaLinkIcon\n\t\t\t? *_st.inviteViaLinkIcon\n\t\t\t: st::createCallInviteLinkIcon),\n\t\tQPoint());\n\tbutton->entity()->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\ticon->moveToLeft(\n\t\t\tst::createCallInviteLinkIconPosition.x(),\n\t\t\t(height - st::groupCallInviteLinkIcon.height()) / 2);\n\t}, icon->lifetime());\n\n\tbutton->entity()->setClickedCallback(_shareLink);\n\tbutton->entity()->events(\n\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\treturn (e->type() == QEvent::Enter);\n\t}) | rpl::on_next([=] {\n\t\tdelegate()->peerListMouseLeftGeometry();\n\t}, button->lifetime());\n\tdelegate()->peerListSetAboveWidget(std::move(button));\n}\n\n} // namespace\n\nInviteController::InviteController(\n\tnot_null<PeerData*> peer,\n\tbase::flat_set<not_null<UserData*>> alreadyIn)\n: ParticipantsBoxController(CreateTag{}, nullptr, peer, Role::Members)\n, _peer(peer)\n, _alreadyIn(std::move(alreadyIn)) {\n\tSubscribeToMigration(\n\t\t_peer,\n\t\tlifetime(),\n\t\t[=](not_null<ChannelData*> channel) { _peer = channel; });\n}\n\nvoid InviteController::prepare() {\n\tdelegate()->peerListSetHideEmpty(true);\n\tParticipantsBoxController::prepare();\n\tdelegate()->peerListSetAboveWidget(CreateSectionSubtitle(\n\t\tnullptr,\n\t\ttr::lng_group_call_invite_members()));\n\tdelegate()->peerListSetAboveSearchWidget(CreateSectionSubtitle(\n\t\tnullptr,\n\t\ttr::lng_group_call_invite_members()));\n}\n\nvoid InviteController::rowClicked(not_null<PeerListRow*> row) {\n\tdelegate()->peerListSetRowChecked(row, !row->checked());\n}\n\nbase::unique_qptr<Ui::PopupMenu> InviteController::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\treturn nullptr;\n}\n\nvoid InviteController::itemDeselectedHook(not_null<PeerData*> peer) {\n}\n\nbool InviteController::hasRowFor(not_null<PeerData*> peer) const {\n\treturn (delegate()->peerListFindRow(peer->id.value) != nullptr);\n}\n\nbool InviteController::isAlreadyIn(not_null<UserData*> user) const {\n\treturn _alreadyIn.contains(user);\n}\n\nstd::unique_ptr<PeerListRow> InviteController::createRow(\n\t\tnot_null<PeerData*> participant) const {\n\tconst auto user = participant->asUser();\n\tif (!user\n\t\t|| user->isSelf()\n\t\t|| user->isBot()\n\t\t|| user->isInaccessible()) {\n\t\treturn nullptr;\n\t}\n\tauto result = std::make_unique<Row>(\n\t\tuser,\n\t\tType{ .chatStyle = _chatStyle.get(), .circleCache = &_pillCircleCache });\n\t_rowAdded.fire_copy(user);\n\t_inGroup.emplace(user);\n\tif (isAlreadyIn(user)) {\n\t\tresult->setDisabledState(PeerListRow::State::DisabledChecked);\n\t}\n\treturn result;\n}\n\nauto InviteController::peersWithRows() const\n-> not_null<const base::flat_set<not_null<UserData*>>*> {\n\treturn &_inGroup;\n}\n\nrpl::producer<not_null<UserData*>> InviteController::rowAdded() const {\n\treturn _rowAdded.events();\n}\n\nInviteContactsController::InviteContactsController(\n\tnot_null<PeerData*> peer,\n\tbase::flat_set<not_null<UserData*>> alreadyIn,\n\tnot_null<const base::flat_set<not_null<UserData*>>*> inGroup,\n\trpl::producer<not_null<UserData*>> discoveredInGroup)\n: AddParticipantsBoxController(peer, std::move(alreadyIn))\n, _inGroup(inGroup)\n, _discoveredInGroup(std::move(discoveredInGroup)) {\n}\n\nvoid InviteContactsController::prepareViewHook() {\n\tAddParticipantsBoxController::prepareViewHook();\n\n\tdelegate()->peerListSetAboveWidget(CreateSectionSubtitle(\n\t\tnullptr,\n\t\ttr::lng_contacts_header()));\n\tdelegate()->peerListSetAboveSearchWidget(CreateSectionSubtitle(\n\t\tnullptr,\n\t\ttr::lng_group_call_invite_search_results()));\n\n\tstd::move(\n\t\t_discoveredInGroup\n\t) | rpl::on_next([=](not_null<UserData*> user) {\n\t\tif (auto row = delegate()->peerListFindRow(user->id.value)) {\n\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t}\n\t}, _lifetime);\n}\n\nstd::unique_ptr<PeerListRow> InviteContactsController::createRow(\n\t\tnot_null<UserData*> user) {\n\treturn _inGroup->contains(user)\n\t\t? nullptr\n\t\t: AddParticipantsBoxController::createRow(user);\n}\n\nobject_ptr<Ui::BoxContent> PrepareInviteBox(\n\t\tnot_null<GroupCall*> call,\n\t\tFn<void(TextWithEntities&&)> showToast,\n\t\tFn<void()> shareConferenceLink) {\n\tconst auto real = call->lookupReal();\n\tif (!real) {\n\t\treturn nullptr;\n\t}\n\tconst auto peer = call->peer();\n\tconst auto conference = call->conference();\n\tconst auto weak = base::make_weak(call);\n\tconst auto &invited = peer->owner().invitedToCallUsers(real->id());\n\tauto alreadyIn = base::flat_set<not_null<UserData*>>();\n\talreadyIn.reserve(invited.size() + real->participants().size() + 1);\n\talreadyIn.emplace(peer->session().user());\n\tfor (const auto &participant : real->participants()) {\n\t\tif (const auto user = participant.peer->asUser()) {\n\t\t\talreadyIn.emplace(user);\n\t\t}\n\t}\n\tfor (const auto &[user, calling] : invited) {\n\t\tif (!conference || calling) {\n\t\t\talreadyIn.emplace(user);\n\t\t}\n\t}\n\tif (conference) {\n\t\tconst auto close = std::make_shared<Fn<void()>>();\n\t\tconst auto shareLink = [=] {\n\t\t\tExpects(shareConferenceLink != nullptr);\n\n\t\t\tshareConferenceLink();\n\t\t\t(*close)();\n\t\t};\n\t\tauto controller = std::make_unique<ConfInviteController>(\n\t\t\t&real->session(),\n\t\t\tConfInviteDarkStyles(),\n\t\t\talreadyIn,\n\t\t\tshareLink,\n\t\t\tstd::vector<not_null<UserData*>>());\n\t\tconst auto raw = controller.get();\n\t\traw->setStyleOverrides(\n\t\t\t&st::groupCallInviteMembersList,\n\t\t\t&st::groupCallMultiSelect);\n\t\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\t\tbox->setTitle(tr::lng_group_call_invite_conf());\n\t\t\traw->hasSelectedValue() | rpl::on_next([=](bool has) {\n\t\t\t\tbox->clearButtons();\n\t\t\t\tif (has) {\n\t\t\t\t\tbox->addButton(tr::lng_group_call_confcall_add(), [=] {\n\t\t\t\t\t\tconst auto call = weak.get();\n\t\t\t\t\t\tif (!call) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst auto done = [=](InviteResult result) {\n\t\t\t\t\t\t\t(*close)();\n\t\t\t\t\t\t\tshowToast({ ComposeInviteResultToast(result) });\n\t\t\t\t\t\t};\n\t\t\t\t\t\tcall->inviteUsers(\n\t\t\t\t\t\t\traw->requests(box->collectSelectedRows()),\n\t\t\t\t\t\t\tdone);\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t\t\t}, box->lifetime());\n\t\t\t*close = crl::guard(box, [=] { box->closeBox(); });\n\t\t};\n\t\treturn Box<PeerListBox>(std::move(controller), initBox);\n\t}\n\n\tauto controller = std::make_unique<InviteController>(peer, alreadyIn);\n\tcontroller->setStyleOverrides(\n\t\t&st::groupCallInviteMembersList,\n\t\t&st::groupCallMultiSelect);\n\n\tauto contactsController = std::make_unique<InviteContactsController>(\n\t\tpeer,\n\t\tstd::move(alreadyIn),\n\t\tcontroller->peersWithRows(),\n\t\tcontroller->rowAdded());\n\tcontactsController->setStyleOverrides(\n\t\t&st::groupCallInviteMembersList,\n\t\t&st::groupCallMultiSelect);\n\n\tconst auto invite = [=](const std::vector<not_null<UserData*>> &users) {\n\t\tconst auto call = weak.get();\n\t\tif (!call) {\n\t\t\treturn;\n\t\t}\n\t\tauto requests = ranges::views::all(\n\t\t\tusers\n\t\t) | ranges::views::transform([](not_null<UserData*> user) {\n\t\t\treturn InviteRequest{ user };\n\t\t}) | ranges::to_vector;\n\t\tcall->inviteUsers(std::move(requests), [=](InviteResult result) {\n\t\t\tif (result.invited.size() == 1) {\n\t\t\t\tshowToast(tr::lng_group_call_invite_done_user(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::bold(result.invited.front()->firstName),\n\t\t\t\t\ttr::marked));\n\t\t\t} else if (result.invited.size() > 1) {\n\t\t\t\tshowToast(tr::lng_group_call_invite_done_many(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tresult.invited.size(),\n\t\t\t\t\ttr::rich));\n\t\t\t}\n\t\t});\n\t};\n\tconst auto inviteWithAdd = [=](\n\t\t\tstd::shared_ptr<Ui::Show> show,\n\t\t\tconst std::vector<not_null<UserData*>> &users,\n\t\t\tconst std::vector<not_null<UserData*>> &nonMembers,\n\t\t\tFn<void()> finish) {\n\t\tpeer->session().api().chatParticipants().add(\n\t\t\tshow,\n\t\t\tpeer,\n\t\t\tnonMembers,\n\t\t\ttrue,\n\t\t\t[=](bool) { invite(users); finish(); });\n\t};\n\tconst auto inviteWithConfirmation = [=](\n\t\t\tnot_null<PeerListsBox*> parentBox,\n\t\t\tconst std::vector<not_null<UserData*>> &users,\n\t\t\tconst std::vector<not_null<UserData*>> &nonMembers,\n\t\t\tFn<void()> finish) {\n\t\tif (nonMembers.empty()) {\n\t\t\tinvite(users);\n\t\t\tfinish();\n\t\t\treturn;\n\t\t}\n\t\tconst auto name = peer->name();\n\t\tconst auto text = (nonMembers.size() == 1)\n\t\t\t? tr::lng_group_call_add_to_group_one(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tnonMembers.front()->shortName(),\n\t\t\t\tlt_group,\n\t\t\t\tname)\n\t\t\t: (nonMembers.size() < users.size())\n\t\t\t? tr::lng_group_call_add_to_group_some(tr::now, lt_group, name)\n\t\t\t: tr::lng_group_call_add_to_group_all(tr::now, lt_group, name);\n\t\tconst auto shared = std::make_shared<base::weak_qptr<Ui::GenericBox>>();\n\t\tconst auto finishWithConfirm = [=] {\n\t\t\tif (*shared) {\n\t\t\t\t(*shared)->closeBox();\n\t\t\t}\n\t\t\tfinish();\n\t\t};\n\t\tconst auto done = [=] {\n\t\t\tconst auto show = (*shared) ? (*shared)->uiShow() : nullptr;\n\t\t\tinviteWithAdd(show, users, nonMembers, finishWithConfirm);\n\t\t};\n\t\tauto box = ConfirmBox({\n\t\t\t.text = text,\n\t\t\t.confirmed = done,\n\t\t\t.confirmText = tr::lng_participant_invite(),\n\t\t});\n\t\t*shared = box.data();\n\t\tparentBox->getDelegate()->showBox(\n\t\t\tstd::move(box),\n\t\t\tUi::LayerOption::KeepOther,\n\t\t\tanim::type::normal);\n\t};\n\tauto initBox = [=, controller = controller.get()](\n\t\t\tnot_null<PeerListsBox*> box) {\n\t\tbox->setTitle(tr::lng_group_call_invite_title());\n\t\tbox->addButton(tr::lng_group_call_invite_button(), [=] {\n\t\t\tconst auto rows = box->collectSelectedRows();\n\n\t\t\tconst auto users = ranges::views::all(\n\t\t\t\trows\n\t\t\t) | ranges::views::transform([](not_null<PeerData*> peer) {\n\t\t\t\treturn not_null<UserData*>(peer->asUser());\n\t\t\t}) | ranges::to_vector;\n\n\t\t\tconst auto nonMembers = ranges::views::all(\n\t\t\t\tusers\n\t\t\t) | ranges::views::filter([&](not_null<UserData*> user) {\n\t\t\t\treturn !controller->hasRowFor(user);\n\t\t\t}) | ranges::to_vector;\n\n\t\t\tconst auto finish = [box = base::make_weak(box)]() {\n\t\t\t\tif (box) {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t}\n\t\t\t};\n\t\t\tinviteWithConfirmation(box, users, nonMembers, finish);\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t};\n\n\tauto controllers = std::vector<std::unique_ptr<PeerListController>>();\n\tcontrollers.push_back(std::move(controller));\n\tcontrollers.push_back(std::move(contactsController));\n\treturn Box<PeerListsBox>(std::move(controllers), initBox);\n}\n\nobject_ptr<Ui::BoxContent> PrepareInviteBox(\n\t\tnot_null<Call*> call,\n\t\tFn<void(std::vector<InviteRequest>)> inviteUsers,\n\t\tFn<void()> shareLink) {\n\tconst auto user = call->user();\n\tconst auto weak = base::make_weak(call);\n\tauto alreadyIn = base::flat_set<not_null<UserData*>>{ user };\n\tauto controller = std::make_unique<ConfInviteController>(\n\t\t&user->session(),\n\t\tConfInviteDarkStyles(),\n\t\talreadyIn,\n\t\tshareLink,\n\t\tstd::vector<not_null<UserData*>>());\n\tconst auto raw = controller.get();\n\traw->setStyleOverrides(\n\t\t&st::groupCallInviteMembersList,\n\t\t&st::groupCallMultiSelect);\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tbox->setTitle(tr::lng_group_call_invite_conf());\n\t\traw->hasSelectedValue() | rpl::on_next([=](bool has) {\n\t\t\tbox->clearButtons();\n\t\t\tif (has) {\n\t\t\t\tbox->addButton(tr::lng_group_call_invite_button(), [=] {\n\t\t\t\t\tconst auto call = weak.get();\n\t\t\t\t\tif (!call) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tinviteUsers(raw->requests(box->collectSelectedRows()));\n\t\t\t\t});\n\t\t\t}\n\t\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t\t}, box->lifetime());\n\t};\n\treturn Box<PeerListBox>(std::move(controller), initBox);\n}\n\nnot_null<Ui::RpWidget*> CreateReActivateHeader(not_null<QWidget*> parent) {\n\tconst auto result = Ui::CreateChild<Ui::VerticalLayout>(parent);\n\tresult->add(\n\t\tMakeJoinCallLogo(result),\n\t\tst::boxRowPadding + st::confcallLinkHeaderIconPadding);\n\n\tresult->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tresult,\n\t\t\ttr::lng_confcall_inactive_title(),\n\t\t\tst::boxTitle),\n\t\tst::boxRowPadding + st::confcallLinkTitlePadding,\n\t\tstyle::al_top);\n\tresult->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tresult,\n\t\t\ttr::lng_confcall_inactive_about(),\n\t\t\tst::confcallLinkCenteredText),\n\t\tst::boxRowPadding + st::confcallLinkTitlePadding,\n\t\tstyle::al_top\n\t)->setTryMakeSimilarLines(true);\n\tUi::AddDivider(result);\n\n\treturn result;\n}\n\nvoid InitReActivate(not_null<PeerListBox*> box) {\n\tbox->setTitle(rpl::producer<TextWithEntities>(nullptr));\n\tbox->setNoContentMargin(true);\n\n\tconst auto header = CreateReActivateHeader(box);\n\theader->resizeToWidth(st::boxWideWidth);\n\theader->heightValue() | rpl::on_next([=](int height) {\n\t\tbox->setAddedTopScrollSkip(height, true);\n\t}, header->lifetime());\n\theader->moveToLeft(0, 0);\n}\n\nobject_ptr<Ui::BoxContent> PrepareInviteToEmptyBox(\n\t\tstd::shared_ptr<Data::GroupCall> call,\n\t\tMsgId inviteMsgId,\n\t\tstd::vector<not_null<UserData*>> prioritize) {\n\tauto controller = std::make_unique<ConfInviteController>(\n\t\t&call->session(),\n\t\tConfInviteDefaultStyles(),\n\t\tbase::flat_set<not_null<UserData*>>(),\n\t\tnullptr,\n\t\tstd::move(prioritize));\n\tconst auto raw = controller.get();\n\traw->setStyleOverrides(&st::createCallList);\n\tconst auto initBox = [=](not_null<PeerListBox*> box) {\n\t\tInitReActivate(box);\n\n\t\tbox->noSearchSubmits() | rpl::on_next([=] {\n\t\t\traw->noSearchSubmit();\n\t\t}, box->lifetime());\n\n\t\traw->prioritizeScrollRequests(\n\t\t) | rpl::on_next([=](Ui::ScrollToRequest request) {\n\t\t\tbox->scrollTo(request);\n\t\t}, box->lifetime());\n\n\t\tconst auto join = [=] {\n\t\t\tconst auto weak = base::make_weak(box);\n\t\t\tauto selected = raw->requests(box->collectSelectedRows());\n\t\t\tCore::App().calls().startOrJoinConferenceCall({\n\t\t\t\t.call = call,\n\t\t\t\t.joinMessageId = inviteMsgId,\n\t\t\t\t.invite = std::move(selected),\n\t\t\t});\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t};\n\t\tbox->addButton(\n\t\t\trpl::conditional(\n\t\t\t\traw->hasSelectedValue(),\n\t\t\t\ttr::lng_group_call_confcall_add(),\n\t\t\t\ttr::lng_create_group_create()),\n\t\t\tjoin);\n\t\tbox->addButton(tr::lng_close(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t};\n\treturn Box<PeerListBox>(std::move(controller), initBox);\n}\n\nobject_ptr<Ui::BoxContent> PrepareCreateCallBox(\n\t\tnot_null<::Window::SessionController*> window,\n\t\tFn<void()> created,\n\t\tMsgId discardedInviteMsgId,\n\t\tstd::vector<not_null<UserData*>> prioritize) {\n\tstruct State {\n\t\tbool creatingLink = false;\n\t\tQPointer<PeerListBox> box;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tconst auto finished = [=](bool ok) {\n\t\tif (!ok) {\n\t\t\tstate->creatingLink = false;\n\t\t} else {\n\t\t\tif (const auto strong = state->box.data()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t\tif (const auto onstack = created) {\n\t\t\t\tonstack();\n\t\t\t}\n\t\t}\n\t};\n\tconst auto shareLink = [=] {\n\t\tif (state->creatingLink) {\n\t\t\treturn;\n\t\t}\n\t\tstate->creatingLink = true;\n\t\tMakeConferenceCall({\n\t\t\t.show = window->uiShow(),\n\t\t\t.finished = finished,\n\t\t});\n\t};\n\tauto controller = std::make_unique<ConfInviteController>(\n\t\t&window->session(),\n\t\tConfInviteDefaultStyles(),\n\t\tbase::flat_set<not_null<UserData*>>(),\n\t\tdiscardedInviteMsgId ? Fn<void()>() : shareLink,\n\t\tstd::move(prioritize));\n\tconst auto raw = controller.get();\n\tif (discardedInviteMsgId) {\n\t\traw->setStyleOverrides(&st::createCallList);\n\t}\n\tconst auto initBox = [=](not_null<PeerListBox*> box) {\n\t\tif (discardedInviteMsgId) {\n\t\t\tInitReActivate(box);\n\t\t} else {\n\t\t\tbox->setTitle(tr::lng_confcall_create_title());\n\t\t}\n\n\t\tbox->noSearchSubmits() | rpl::on_next([=] {\n\t\t\traw->noSearchSubmit();\n\t\t}, box->lifetime());\n\n\t\traw->prioritizeScrollRequests(\n\t\t) | rpl::on_next([=](Ui::ScrollToRequest request) {\n\t\t\tbox->scrollTo(request);\n\t\t}, box->lifetime());\n\n\t\tconst auto create = [=] {\n\t\t\tauto selected = raw->requests(box->collectSelectedRows());\n\t\t\tif (selected.size() != 1 || discardedInviteMsgId) {\n\t\t\t\tCore::App().calls().startOrJoinConferenceCall({\n\t\t\t\t\t.show = window->uiShow(),\n\t\t\t\t\t.invite = std::move(selected),\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconst auto &invite = selected.front();\n\t\t\t\tCore::App().calls().startOutgoingCall(\n\t\t\t\t\tinvite.user,\n\t\t\t\t\t{ invite.video });\n\t\t\t}\n\t\t\tfinished(true);\n\t\t};\n\t\tbox->addButton(\n\t\t\trpl::conditional(\n\t\t\t\traw->hasSelectedValue(),\n\t\t\t\ttr::lng_group_call_confcall_add(),\n\t\t\t\ttr::lng_create_group_create()),\n\t\t\tcreate);\n\t\tbox->addButton(tr::lng_close(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t};\n\tauto result = Box<PeerListBox>(std::move(controller), initBox);\n\tstate->box = result.data();\n\treturn result;\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_invite_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peers/edit_participants_box.h\"\n#include \"boxes/peers/add_participants_box.h\"\n\nnamespace Calls {\nclass Call;\nclass GroupCall;\nstruct InviteRequest;\n} // namespace Calls\n\nnamespace Data {\nclass GroupCall;\n} // namespace Data\n\nnamespace Calls::Group {\n\nclass InviteController final : public ParticipantsBoxController {\npublic:\n\tInviteController(\n\t\tnot_null<PeerData*> peer,\n\t\tbase::flat_set<not_null<UserData*>> alreadyIn);\n\n\tvoid prepare() override;\n\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\n\tvoid itemDeselectedHook(not_null<PeerData*> peer) override;\n\n\t[[nodiscard]] auto peersWithRows() const\n\t\t-> not_null<const base::flat_set<not_null<UserData*>>*>;\n\t[[nodiscard]] rpl::producer<not_null<UserData*>> rowAdded() const;\n\n\t[[nodiscard]] bool hasRowFor(not_null<PeerData*> peer) const;\n\nprivate:\n\t[[nodiscard]] bool isAlreadyIn(not_null<UserData*> user) const;\n\n\tstd::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<PeerData*> participant) const override;\n\n\tnot_null<PeerData*> _peer;\n\tconst base::flat_set<not_null<UserData*>> _alreadyIn;\n\tmutable base::flat_set<not_null<UserData*>> _inGroup;\n\trpl::event_stream<not_null<UserData*>> _rowAdded;\n\n};\n\nclass InviteContactsController final : public AddParticipantsBoxController {\npublic:\n\tInviteContactsController(\n\t\tnot_null<PeerData*> peer,\n\t\tbase::flat_set<not_null<UserData*>> alreadyIn,\n\t\tnot_null<const base::flat_set<not_null<UserData*>>*> inGroup,\n\t\trpl::producer<not_null<UserData*>> discoveredInGroup);\n\nprivate:\n\tvoid prepareViewHook() override;\n\n\tstd::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<UserData*> user) override;\n\n\tbool needsInviteLinkButton() override {\n\t\treturn false;\n\t}\n\n\tconst not_null<const base::flat_set<not_null<UserData*>>*> _inGroup;\n\trpl::producer<not_null<UserData*>> _discoveredInGroup;\n\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteBox(\n\tnot_null<GroupCall*> call,\n\tFn<void(TextWithEntities&&)> showToast,\n\tFn<void()> shareConferenceLink = nullptr);\n\n[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteBox(\n\tnot_null<Call*> call,\n\tFn<void(std::vector<InviteRequest>)> inviteUsers,\n\tFn<void()> shareLink);\n\n[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteToEmptyBox(\n\tstd::shared_ptr<Data::GroupCall> call,\n\tMsgId inviteMsgId,\n\tstd::vector<not_null<UserData*>> prioritize);\n\n[[nodiscard]] object_ptr<Ui::BoxContent> PrepareCreateCallBox(\n\tnot_null<::Window::SessionController*> window,\n\tFn<void()> created = nullptr,\n\tMsgId discardedInviteMsgId = 0,\n\tstd::vector<not_null<UserData*>> prioritize = {});\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_members.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_members.h\"\n\n#include \"calls/group/calls_cover_item.h\"\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_menu.h\"\n#include \"calls/group/calls_volume_item.h\"\n#include \"calls/group/calls_group_members_row.h\"\n#include \"calls/group/calls_group_viewport.h\"\n#include \"calls/calls_emoji_fingerprint.h\"\n#include \"calls/calls_instance.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_peer_values.h\" // Data::CanWriteValue.\n#include \"data/data_session.h\" // Data::Session::invitedToCallUsers.\n#include \"settings/settings_common.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/cross_line.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"core/application.h\" // Core::App().domain, .activeWindow.\n#include \"main/main_domain.h\" // Core::App().domain().activate.\n#include \"main/main_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"info/profile/info_profile_values.h\" // Info::Profile::NameValue.\n#include \"boxes/peers/edit_participants_box.h\" // SubscribeToMigration.\n#include \"boxes/peers/prepare_short_info_box.h\" // PrepareShortInfo...\n#include \"window/window_controller.h\" // Controller::sessionController.\n#include \"window/window_session_controller.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"styles/style_calls.h\"\n\nnamespace Calls::Group {\nnamespace {\n\nconstexpr auto kKeepRaisedHandStatusDuration = 3 * crl::time(1000);\n\nusing Row = MembersRow;\n\n} // namespace\n\nclass Members::Controller final\n\t: public PeerListController\n\t, public MembersRowDelegate\n\t, public base::has_weak_ptr {\npublic:\n\tController(\n\t\tnot_null<GroupCall*> call,\n\t\tnot_null<QWidget*> menuParent,\n\t\tPanelMode mode);\n\t~Controller();\n\n\tusing MuteRequest = Group::MuteRequest;\n\tusing VolumeRequest = Group::VolumeRequest;\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override;\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\n\t[[nodiscard]] rpl::producer<int> fullCountValue() const {\n\t\treturn _fullCount.value();\n\t}\n\t[[nodiscard]] rpl::producer<MuteRequest> toggleMuteRequests() const;\n\t[[nodiscard]] rpl::producer<VolumeRequest> changeVolumeRequests() const;\n\t[[nodiscard]] auto kickParticipantRequests() const\n\t\t-> rpl::producer<not_null<PeerData*>>;\n\n\tRow *findRow(not_null<PeerData*> participantPeer) const;\n\tvoid setMode(PanelMode mode);\n\n\tbool rowIsMe(not_null<PeerData*> participantPeer) override;\n\tbool rowCanMuteMembers() override;\n\tvoid rowUpdateRow(not_null<Row*> row) override;\n\tvoid rowScheduleRaisedHandStatusRemove(not_null<Row*> row) override;\n\tvoid rowPaintIcon(\n\t\tQPainter &p,\n\t\tQRect rect,\n\t\tconst IconState &state) override;\n\tint rowPaintStatusIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tnot_null<MembersRow*> row,\n\t\tconst IconState &state) override;\n\tbool rowIsNarrow() override;\n\tvoid rowShowContextMenu(not_null<PeerListRow*> row) override;\n\nprivate:\n\t[[nodiscard]] std::unique_ptr<Row> createRowForMe();\n\t[[nodiscard]] std::unique_ptr<Row> createRow(\n\t\tconst Data::GroupCallParticipant &participant);\n\t[[nodiscard]] std::unique_ptr<Row> createInvitedRow(\n\t\tnot_null<PeerData*> participantPeer,\n\t\tbool calling);\n\t[[nodiscard]] std::unique_ptr<Row> createWithAccessRow(\n\t\tnot_null<PeerData*> participantPeer);\n\n\t[[nodiscard]] bool isMe(not_null<PeerData*> participantPeer) const;\n\tvoid prepareRows(not_null<Data::GroupCall*> real);\n\n\t[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row);\n\tvoid addMuteActionsToContextMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<PeerData*> participantPeer,\n\t\tbool participantIsCallAdmin,\n\t\tnot_null<Row*> row);\n\tvoid setupListChangeViewers();\n\tvoid subscribeToChanges(not_null<Data::GroupCall*> real);\n\tvoid updateRow(\n\t\tconst std::optional<Data::GroupCallParticipant> &was,\n\t\tconst Data::GroupCallParticipant &now);\n\tvoid updateRow(\n\t\tnot_null<Row*> row,\n\t\tconst std::optional<Data::GroupCallParticipant> &was,\n\t\tconst Data::GroupCallParticipant *participant,\n\t\tRow::State noParticipantState = Row::State::Invited);\n\tvoid updateRowInSoundingMap(\n\t\tnot_null<Row*> row,\n\t\tbool wasSounding,\n\t\tuint32 wasSsrc,\n\t\tuint32 wasAdditionalSsrc,\n\t\tconst Data::GroupCallParticipant *participant);\n\tvoid updateRowInSoundingMap(\n\t\tnot_null<Row*> row,\n\t\tbool wasSounding,\n\t\tuint32 wasSsrc,\n\t\tbool nowSounding,\n\t\tuint32 nowSsrc);\n\tvoid removeRow(not_null<Row*> row);\n\tvoid removeRowFromSoundingMap(not_null<Row*> row);\n\tvoid updateRowLevel(not_null<Row*> row, float level);\n\tvoid checkRowPosition(not_null<Row*> row);\n\t[[nodiscard]] bool needToReorder(not_null<Row*> row) const;\n\t[[nodiscard]] bool allRowsAboveAreSpeaking(not_null<Row*> row) const;\n\t[[nodiscard]] bool allRowsAboveMoreImportantThanHand(\n\t\tnot_null<Row*> row,\n\t\tuint64 raiseHandRating) const;\n\t[[nodiscard]] const Data::GroupCallParticipant *findParticipant(\n\t\tconst std::string &endpoint) const;\n\t[[nodiscard]] const std::string &computeScreenEndpoint(\n\t\tnot_null<const Data::GroupCallParticipant*> participant) const;\n\t[[nodiscard]] const std::string &computeCameraEndpoint(\n\t\tnot_null<const Data::GroupCallParticipant*> participant) const;\n\tvoid showRowMenu(not_null<PeerListRow*> row, bool highlightRow);\n\n\tvoid toggleVideoEndpointActive(\n\t\tconst VideoEndpoint &endpoint,\n\t\tbool active);\n\n\tvoid partitionRows();\n\tvoid setupInvitedUsers();\n\t[[nodiscard]] bool appendInvitedUsers();\n\tvoid setupWithAccessUsers();\n\t[[nodiscard]] bool appendWithAccessUsers();\n\tvoid scheduleRaisedHandStatusRemove();\n\tvoid refreshWithAccessRows(base::flat_set<UserId> &&nowIds);\n\n\tvoid hideRowsWithVideoExcept(const VideoEndpoint &large);\n\tvoid showAllHiddenRows();\n\tvoid hideRowWithVideo(const VideoEndpoint &endpoint);\n\tvoid showRowWithVideo(const VideoEndpoint &endpoint);\n\n\tconst not_null<GroupCall*> _call;\n\tnot_null<PeerData*> _peer;\n\tstd::string _largeEndpoint;\n\tbool _prepared = false;\n\n\trpl::event_stream<MuteRequest> _toggleMuteRequests;\n\trpl::event_stream<VolumeRequest> _changeVolumeRequests;\n\trpl::event_stream<not_null<PeerData*>> _kickParticipantRequests;\n\trpl::variable<int> _fullCount = 1;\n\n\tnot_null<QWidget*> _menuParent;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\tbase::flat_set<not_null<PeerData*>> _menuCheckRowsAfterHidden;\n\n\tbase::flat_map<PeerListRowId, crl::time> _raisedHandStatusRemoveAt;\n\tbase::Timer _raisedHandStatusRemoveTimer;\n\n\tbase::flat_map<uint32, not_null<Row*>> _soundingRowBySsrc;\n\tbase::flat_set<not_null<PeerData*>> _cameraActive;\n\tbase::flat_set<not_null<PeerData*>> _screenActive;\n\tUi::Animations::Basic _soundingAnimation;\n\n\tcrl::time _soundingAnimationHideLastTime = 0;\n\tbool _skipRowLevelUpdate = false;\n\n\tPanelMode _mode = PanelMode::Default;\n\tUi::CrossLineAnimation _inactiveCrossLine;\n\tUi::CrossLineAnimation _coloredCrossLine;\n\tUi::CrossLineAnimation _inactiveNarrowCrossLine;\n\tUi::CrossLineAnimation _coloredNarrowCrossLine;\n\tUi::CrossLineAnimation _videoCrossLine;\n\tUi::RoundRect _narrowRoundRectSelected;\n\tUi::RoundRect _narrowRoundRect;\n\tQImage _narrowShadow;\n\n\tbase::flat_set<UserId> _withAccess;\n\n\trpl::lifetime _lifetime;\n\n};\n\nMembers::Controller::Controller(\n\tnot_null<GroupCall*> call,\n\tnot_null<QWidget*> menuParent,\n\tPanelMode mode)\n: _call(call)\n, _peer(call->peer())\n, _menuParent(menuParent)\n, _raisedHandStatusRemoveTimer([=] { scheduleRaisedHandStatusRemove(); })\n, _mode(mode)\n, _inactiveCrossLine(st::groupCallMemberInactiveCrossLine)\n, _coloredCrossLine(st::groupCallMemberColoredCrossLine)\n, _inactiveNarrowCrossLine(st::groupCallNarrowInactiveCrossLine)\n, _coloredNarrowCrossLine(st::groupCallNarrowColoredCrossLine)\n, _videoCrossLine(st::groupCallVideoCrossLine)\n, _narrowRoundRectSelected(\n\tImageRoundRadius::Large,\n\tst::groupCallMembersBgOver)\n, _narrowRoundRect(ImageRoundRadius::Large, st::groupCallMembersBg) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_inactiveCrossLine.invalidate();\n\t\t_coloredCrossLine.invalidate();\n\t\t_inactiveNarrowCrossLine.invalidate();\n\t\t_coloredNarrowCrossLine.invalidate();\n\t}, _lifetime);\n\n\trpl::combine(\n\t\tPowerSaving::OnValue(PowerSaving::kCalls),\n\t\tCore::App().appDeactivatedValue()\n\t) | rpl::on_next([=](bool disabled, bool deactivated) {\n\t\tconst auto hide = disabled || deactivated;\n\n\t\tif (!(hide && _soundingAnimationHideLastTime)) {\n\t\t\t_soundingAnimationHideLastTime = hide ? crl::now() : 0;\n\t\t}\n\t\tfor (const auto &[_, row] : _soundingRowBySsrc) {\n\t\t\tif (hide) {\n\t\t\t\tupdateRowLevel(row, 0.);\n\t\t\t}\n\t\t\trow->setSkipLevelUpdate(hide);\n\t\t}\n\t\tif (!hide && !_soundingAnimation.animating()) {\n\t\t\t_soundingAnimation.start();\n\t\t}\n\t\t_skipRowLevelUpdate = hide;\n\t}, _lifetime);\n\n\t_soundingAnimation.init([=](crl::time now) {\n\t\tif (const auto &last = _soundingAnimationHideLastTime; (last > 0)\n\t\t\t&& (now - last >= kBlobsEnterDuration)) {\n\t\t\t_soundingAnimation.stop();\n\t\t\treturn false;\n\t\t}\n\t\tfor (const auto &[ssrc, row] : _soundingRowBySsrc) {\n\t\t\trow->updateBlobAnimation(now);\n\t\t\tdelegate()->peerListUpdateRow(row);\n\t\t}\n\t\treturn true;\n\t});\n\n\t_peer->session().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::About\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tif (const auto row = findRow(update.peer)) {\n\t\t\trow->setAbout(update.peer->about());\n\t\t}\n\t}, _lifetime);\n}\n\nMembers::Controller::~Controller() {\n\tbase::take(_menu);\n}\n\nvoid Members::Controller::setupListChangeViewers() {\n\t_call->real(\n\t) | rpl::on_next([=](not_null<Data::GroupCall*> real) {\n\t\tsubscribeToChanges(real);\n\t}, _lifetime);\n\n\t_call->levelUpdates(\n\t) | rpl::on_next([=](const LevelUpdate &update) {\n\t\tconst auto i = _soundingRowBySsrc.find(update.ssrc);\n\t\tif (i != end(_soundingRowBySsrc)) {\n\t\t\tupdateRowLevel(i->second, update.value);\n\t\t}\n\t}, _lifetime);\n\n\t_call->videoEndpointLargeValue(\n\t) | rpl::on_next([=](const VideoEndpoint &large) {\n\t\tif (large) {\n\t\t\thideRowsWithVideoExcept(large);\n\t\t} else {\n\t\t\tshowAllHiddenRows();\n\t\t}\n\t}, _lifetime);\n\n\t_call->videoStreamShownUpdates(\n\t) | rpl::filter([=](const VideoStateToggle &update) {\n\t\tconst auto &large = _call->videoEndpointLarge();\n\t\treturn large && (update.endpoint != large);\n\t}) | rpl::on_next([=](const VideoStateToggle &update) {\n\t\tif (update.value) {\n\t\t\thideRowWithVideo(update.endpoint);\n\t\t} else {\n\t\t\tshowRowWithVideo(update.endpoint);\n\t\t}\n\t}, _lifetime);\n\n\t_call->rejoinEvents(\n\t) | rpl::on_next([=](const Group::RejoinEvent &event) {\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t});\n\t\tif (const auto row = findRow(event.wasJoinAs)) {\n\t\t\tremoveRow(row);\n\t\t}\n\t\tif (findRow(event.nowJoinAs)) {\n\t\t\treturn;\n\t\t} else if (auto row = createRowForMe()) {\n\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t}\n\t}, _lifetime);\n}\n\nvoid Members::Controller::hideRowsWithVideoExcept(\n\t\tconst VideoEndpoint &large) {\n\tauto changed = false;\n\tauto showLargeRow = true;\n\tfor (const auto &endpoint : _call->shownVideoTracks()) {\n\t\tif (endpoint != large) {\n\t\t\tif (const auto row = findRow(endpoint.peer)) {\n\t\t\t\tif (endpoint.peer == large.peer) {\n\t\t\t\t\tshowLargeRow = false;\n\t\t\t\t}\n\t\t\t\tdelegate()->peerListSetRowHidden(row, true);\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t}\n\t}\n\tif (const auto row = showLargeRow ? findRow(large.peer) : nullptr) {\n\t\tdelegate()->peerListSetRowHidden(row, false);\n\t\tchanged = true;\n\t}\n\tif (changed) {\n\t\tdelegate()->peerListRefreshRows();\n\t}\n}\n\nvoid Members::Controller::showAllHiddenRows() {\n\tauto shown = false;\n\tfor (const auto &endpoint : _call->shownVideoTracks()) {\n\t\tif (const auto row = findRow(endpoint.peer)) {\n\t\t\tdelegate()->peerListSetRowHidden(row, false);\n\t\t\tshown = true;\n\t\t}\n\t}\n\tif (shown) {\n\t\tdelegate()->peerListRefreshRows();\n\t}\n}\n\nvoid Members::Controller::hideRowWithVideo(const VideoEndpoint &endpoint) {\n\tif (const auto row = findRow(endpoint.peer)) {\n\t\tdelegate()->peerListSetRowHidden(row, true);\n\t\tdelegate()->peerListRefreshRows();\n\t}\n}\n\nvoid Members::Controller::showRowWithVideo(const VideoEndpoint &endpoint) {\n\tconst auto peer = endpoint.peer;\n\tconst auto &large = _call->videoEndpointLarge();\n\tif (large) {\n\t\tfor (const auto &endpoint : _call->shownVideoTracks()) {\n\t\t\tif (endpoint != large && endpoint.peer == peer) {\n\t\t\t\t// Still hidden with another video.\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\tif (const auto row = findRow(endpoint.peer)) {\n\t\tdelegate()->peerListSetRowHidden(row, false);\n\t\tdelegate()->peerListRefreshRows();\n\t}\n}\n\nvoid Members::Controller::subscribeToChanges(not_null<Data::GroupCall*> real) {\n\t_fullCount = real->fullCountValue();\n\n\treal->participantsReloaded(\n\t) | rpl::on_next([=] {\n\t\tprepareRows(real);\n\t}, _lifetime);\n\n\tusing Update = Data::GroupCall::ParticipantUpdate;\n\treal->participantUpdated(\n\t) | rpl::on_next([=](const Update &update) {\n\t\tExpects(update.was.has_value() || update.now.has_value());\n\n\t\tconst auto participantPeer = update.was\n\t\t\t? update.was->peer\n\t\t\t: update.now->peer;\n\t\tif (!update.now) {\n\t\t\tif (const auto row = findRow(participantPeer)) {\n\t\t\t\tif (isMe(participantPeer)) {\n\t\t\t\t\tupdateRow(row, update.was, nullptr);\n\t\t\t\t} else if (_withAccess.contains(peerToUser(participantPeer->id))) {\n\t\t\t\t\tupdateRow(row, update.was, nullptr, Row::State::WithAccess);\n\t\t\t\t\tpartitionRows();\n\t\t\t\t} else {\n\t\t\t\t\tremoveRow(row);\n\t\t\t\t\tdelegate()->peerListRefreshRows();\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tupdateRow(update.was, *update.now);\n\t\t}\n\t}, _lifetime);\n\n\tfor (const auto &[endpoint, track] : _call->activeVideoTracks()) {\n\t\ttoggleVideoEndpointActive(endpoint, true);\n\t}\n\t_call->videoStreamActiveUpdates(\n\t) | rpl::on_next([=](const VideoStateToggle &update) {\n\t\ttoggleVideoEndpointActive(update.endpoint, update.value);\n\t}, _lifetime);\n}\n\nvoid Members::Controller::toggleVideoEndpointActive(\n\t\tconst VideoEndpoint &endpoint,\n\t\tbool active) {\n\tconst auto toggleOne = [=](\n\t\t\tbase::flat_set<not_null<PeerData*>> &set,\n\t\t\tnot_null<PeerData*> participantPeer,\n\t\t\tbool active) {\n\t\tif ((active && set.emplace(participantPeer).second)\n\t\t\t|| (!active && set.remove(participantPeer))) {\n\t\t\tif (_mode == PanelMode::Wide) {\n\t\t\t\tif (const auto row = findRow(participantPeer)) {\n\t\t\t\t\tdelegate()->peerListUpdateRow(row);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\tconst auto &id = endpoint.id;\n\tconst auto participantPeer = endpoint.peer;\n\tconst auto real = _call->lookupReal();\n\tif (active) {\n\t\tif (const auto participant = findParticipant(id)) {\n\t\t\tif (computeCameraEndpoint(participant) == id) {\n\t\t\t\ttoggleOne(_cameraActive, participantPeer, true);\n\t\t\t} else if (computeScreenEndpoint(participant) == id) {\n\t\t\t\ttoggleOne(_screenActive, participantPeer, true);\n\t\t\t}\n\t\t}\n\t} else if (const auto participant = real->participantByPeer(\n\t\t\tparticipantPeer)) {\n\t\tconst auto &camera = computeCameraEndpoint(participant);\n\t\tconst auto &screen = computeScreenEndpoint(participant);\n\t\tif (camera == id || camera.empty()) {\n\t\t\ttoggleOne(_cameraActive, participantPeer, false);\n\t\t}\n\t\tif (screen == id || screen.empty()) {\n\t\t\ttoggleOne(_screenActive, participantPeer, false);\n\t\t}\n\t} else {\n\t\ttoggleOne(_cameraActive, participantPeer, false);\n\t\ttoggleOne(_screenActive, participantPeer, false);\n\t}\n\n}\n\nbool Members::Controller::appendInvitedUsers() {\n\tauto changed = false;\n\tif (const auto id = _call->id()) {\n\t\tconst auto &invited = _peer->owner().invitedToCallUsers(id);\n\t\tfor (const auto &[user, calling] : invited) {\n\t\t\tif (auto row = createInvitedRow(user, calling)) {\n\t\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t}\n\t}\n\treturn changed;\n}\n\nvoid Members::Controller::setupInvitedUsers() {\n\tif (appendInvitedUsers()) {\n\t\tdelegate()->peerListRefreshRows();\n\t}\n\n\tusing Invite = Data::Session::InviteToCall;\n\t_peer->owner().invitesToCalls(\n\t) | rpl::filter([=](const Invite &invite) {\n\t\treturn (invite.id == _call->id());\n\t}) | rpl::on_next([=](const Invite &invite) {\n\t\tconst auto user = invite.user;\n\t\tif (invite.removed) {\n\t\t\tif (const auto row = findRow(user)) {\n\t\t\t\tif (row->state() == Row::State::Invited\n\t\t\t\t\t|| row->state() == Row::State::Calling) {\n\t\t\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t\t\t\tdelegate()->peerListRefreshRows();\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (auto row = createInvitedRow(user, invite.calling)) {\n\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t}, _lifetime);\n}\n\nbool Members::Controller::appendWithAccessUsers() {\n\tauto changed = false;\n\tfor (const auto id : _withAccess) {\n\t\tif (auto row = createWithAccessRow(_peer->owner().user(id))) {\n\t\t\tchanged = true;\n\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t}\n\t}\n\treturn changed;\n}\n\nvoid Members::Controller::setupWithAccessUsers() {\n\tconst auto conference = _call->sharedCall().get();\n\tif (!_call->conference() || !conference) {\n\t\treturn;\n\t}\n\tconference->participantsWithAccessValue(\n\t) | rpl::on_next([=](base::flat_set<UserId> &&nowIds) {\n\t\tfor (auto i = begin(_withAccess); i != end(_withAccess);) {\n\t\t\tconst auto oldId = *i;\n\t\t\tif (nowIds.remove(oldId)) {\n\t\t\t\t++i;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto user = _peer->owner().user(oldId);\n\t\t\tif (const auto row = findRow(user)) {\n\t\t\t\tif (row->state() == Row::State::WithAccess) {\n\t\t\t\t\tremoveRow(row);\n\t\t\t\t}\n\t\t\t}\n\t\t\ti = _withAccess.erase(i);\n\t\t}\n\t\tauto partition = false;\n\t\tauto partitionChecked = false;\n\t\tfor (const auto nowId : nowIds) {\n\t\t\tconst auto user = _peer->owner().user(nowId);\n\t\t\tif (!findRow(user)) {\n\t\t\t\tif (auto row = createWithAccessRow(user)) {\n\t\t\t\t\tif (!partitionChecked) {\n\t\t\t\t\t\tpartitionChecked = true;\n\t\t\t\t\t\tif (const auto count = delegate()->peerListFullRowsCount()) {\n\t\t\t\t\t\t\tconst auto last = delegate()->peerListRowAt(count - 1);\n\t\t\t\t\t\t\tconst auto state = static_cast<Row*>(last.get())->state();\n\t\t\t\t\t\t\tif (state == Row::State::Invited\n\t\t\t\t\t\t\t\t|| state == Row::State::Calling) {\n\t\t\t\t\t\t\t\tpartition = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t\t\t}\n\t\t\t}\n\t\t\t_withAccess.emplace(nowId);\n\t\t}\n\t\tif (partition) {\n\t\t\tdelegate()->peerListPartitionRows([](const PeerListRow &row) {\n\t\t\t\tconst auto state = static_cast<const Row&>(row).state();\n\t\t\t\treturn (state != Row::State::Invited)\n\t\t\t\t\t&& (state != Row::State::Calling);\n\t\t\t});\n\t\t}\n\t\tdelegate()->peerListRefreshRows();\n\t}, _lifetime);\n}\n\nvoid Members::Controller::updateRow(\n\t\tconst std::optional<Data::GroupCallParticipant> &was,\n\t\tconst Data::GroupCallParticipant &now) {\n\tauto reorderIfNonRealBefore = 0;\n\tauto checkPosition = (Row*)nullptr;\n\tauto addedToBottom = (Row*)nullptr;\n\tif (const auto row = findRow(now.peer)) {\n\t\tif (row->state() == Row::State::Invited\n\t\t\t|| row->state() == Row::State::Calling\n\t\t\t|| row->state() == Row::State::WithAccess) {\n\t\t\treorderIfNonRealBefore = row->absoluteIndex();\n\t\t}\n\t\tupdateRow(row, was, &now);\n\t\tif ((now.speaking && (!was || !was->speaking))\n\t\t\t|| (now.raisedHandRating != (was ? was->raisedHandRating : 0))\n\t\t\t|| (!now.canSelfUnmute && was && was->canSelfUnmute)) {\n\t\t\tcheckPosition = row;\n\t\t}\n\t} else if (auto row = createRow(now)) {\n\t\tif (row->speaking()) {\n\t\t\tdelegate()->peerListPrependRow(std::move(row));\n\t\t} else {\n\t\t\treorderIfNonRealBefore = delegate()->peerListFullRowsCount();\n\t\t\tif (now.raisedHandRating != 0) {\n\t\t\t\tcheckPosition = row.get();\n\t\t\t} else {\n\t\t\t\taddedToBottom = row.get();\n\t\t\t}\n\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t}\n\t\tdelegate()->peerListRefreshRows();\n\t}\n\tconst auto reorder = [&] {\n\t\tconst auto count = reorderIfNonRealBefore;\n\t\tif (count <= 0) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto row = delegate()->peerListRowAt(\n\t\t\treorderIfNonRealBefore - 1).get();\n\t\tusing State = Row::State;\n\t\tconst auto state = static_cast<Row*>(row)->state();\n\t\treturn (state == State::Invited)\n\t\t\t|| (state == State::Calling)\n\t\t\t|| (state == State::WithAccess);\n\t}();\n\tif (reorder) {\n\t\tpartitionRows();\n\t}\n\tif (checkPosition) {\n\t\tcheckRowPosition(checkPosition);\n\t} else if (addedToBottom) {\n\t\tconst auto real = _call->lookupReal();\n\t\tif (real && real->joinedToTop()) {\n\t\t\tconst auto proj = [&](const PeerListRow &other) {\n\t\t\t\tconst auto &real = static_cast<const Row&>(other);\n\t\t\t\treturn real.speaking()\n\t\t\t\t\t? 2\n\t\t\t\t\t: (&real == addedToBottom)\n\t\t\t\t\t? 1\n\t\t\t\t\t: 0;\n\t\t\t};\n\t\t\tdelegate()->peerListSortRows([&](\n\t\t\t\t\tconst PeerListRow &a,\n\t\t\t\t\tconst PeerListRow &b) {\n\t\t\t\treturn proj(a) > proj(b);\n\t\t\t});\n\t\t}\n\t}\n}\n\nvoid Members::Controller::partitionRows() {\n\tauto hadWithAccess = false;\n\tdelegate()->peerListPartitionRows([&](const PeerListRow &row) {\n\t\tusing State = Row::State;\n\t\tconst auto state = static_cast<const Row&>(row).state();\n\t\tif (state == State::WithAccess) {\n\t\t\thadWithAccess = true;\n\t\t}\n\t\treturn (state != State::Invited)\n\t\t\t&& (state != State::Calling)\n\t\t\t&& (state != State::WithAccess);\n\t});\n\tif (hadWithAccess) {\n\t\tdelegate()->peerListPartitionRows([](const PeerListRow &row) {\n\t\t\tconst auto state = static_cast<const Row&>(row).state();\n\t\t\treturn (state != Row::State::Invited)\n\t\t\t\t&& (state != Row::State::Calling);\n\t\t});\n\t}\n}\n\nbool Members::Controller::allRowsAboveAreSpeaking(not_null<Row*> row) const {\n\tconst auto count = delegate()->peerListFullRowsCount();\n\tfor (auto i = 0; i != count; ++i) {\n\t\tconst auto above = delegate()->peerListRowAt(i);\n\t\tif (above == row) {\n\t\t\t// All rows above are speaking.\n\t\t\treturn true;\n\t\t} else if (!static_cast<Row*>(above.get())->speaking()) {\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool Members::Controller::allRowsAboveMoreImportantThanHand(\n\t\tnot_null<Row*> row,\n\t\tuint64 raiseHandRating) const {\n\tExpects(raiseHandRating > 0);\n\n\tconst auto count = delegate()->peerListFullRowsCount();\n\tfor (auto i = 0; i != count; ++i) {\n\t\tconst auto above = delegate()->peerListRowAt(i);\n\t\tif (above == row) {\n\t\t\t// All rows above are 'more important' than this raised hand.\n\t\t\treturn true;\n\t\t}\n\t\tconst auto real = static_cast<Row*>(above.get());\n\t\tconst auto state = real->state();\n\t\tif (state == Row::State::Muted\n\t\t\t|| (state == Row::State::RaisedHand\n\t\t\t\t&& real->raisedHandRating() < raiseHandRating)) {\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool Members::Controller::needToReorder(not_null<Row*> row) const {\n\t// All reorder cases:\n\t// - bring speaking up\n\t// - bring raised hand up\n\t// - bring muted down\n\n\tif (row->speaking()) {\n\t\treturn !allRowsAboveAreSpeaking(row);\n\t} else if (!_call->canManage()) {\n\t\t// Raising hands reorder participants only for voice chat admins.\n\t\treturn false;\n\t}\n\n\tconst auto rating = row->raisedHandRating();\n\tif (!rating && row->state() != Row::State::Muted) {\n\t\treturn false;\n\t}\n\tif (rating > 0 && !allRowsAboveMoreImportantThanHand(row, rating)) {\n\t\treturn true;\n\t}\n\tconst auto index = row->absoluteIndex();\n\tif (index + 1 == delegate()->peerListFullRowsCount()) {\n\t\t// Last one, can't bring lower.\n\t\treturn false;\n\t}\n\tconst auto next = delegate()->peerListRowAt(index + 1);\n\tconst auto state = static_cast<Row*>(next.get())->state();\n\tif ((state != Row::State::Muted) && (state != Row::State::RaisedHand)) {\n\t\treturn true;\n\t}\n\tif (!rating && static_cast<Row*>(next.get())->raisedHandRating()) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Members::Controller::checkRowPosition(not_null<Row*> row) {\n\tif (_menu) {\n\t\t// Don't reorder rows while we show the popup menu.\n\t\t_menuCheckRowsAfterHidden.emplace(row->peer());\n\t\treturn;\n\t} else if (!needToReorder(row)) {\n\t\treturn;\n\t}\n\n\t// Someone started speaking and has a non-speaking row above him.\n\t// Or someone raised hand and has force muted above him.\n\t// Or someone was forced muted and had can_unmute_self below him. Sort.\n\tstatic constexpr auto kTop = std::numeric_limits<uint64>::max();\n\tconst auto projForAdmin = [&](const PeerListRow &other) {\n\t\tconst auto &real = static_cast<const Row&>(other);\n\t\treturn real.speaking()\n\t\t\t// Speaking 'row' to the top, all other speaking below it.\n\t\t\t? (&real == row.get() ? kTop : (kTop - 1))\n\t\t\t: (real.raisedHandRating() > 0)\n\t\t\t// Then all raised hands sorted by rating.\n\t\t\t? real.raisedHandRating()\n\t\t\t: (real.state() == Row::State::Muted)\n\t\t\t// All force muted at the bottom, but 'row' still above others.\n\t\t\t? (&real == row.get() ? 1ULL : 0ULL)\n\t\t\t// All not force-muted lie between raised hands and speaking.\n\t\t\t: (kTop - 2);\n\t};\n\tconst auto projForOther = [&](const PeerListRow &other) {\n\t\tconst auto &real = static_cast<const Row&>(other);\n\t\treturn real.speaking()\n\t\t\t// Speaking 'row' to the top, all other speaking below it.\n\t\t\t? (&real == row.get() ? kTop : (kTop - 1))\n\t\t\t: 0ULL;\n\t};\n\n\tusing Comparator = Fn<bool(const PeerListRow&, const PeerListRow&)>;\n\tconst auto makeComparator = [&](const auto &proj) -> Comparator {\n\t\treturn [&](const PeerListRow &a, const PeerListRow &b) {\n\t\t\treturn proj(a) > proj(b);\n\t\t};\n\t};\n\tdelegate()->peerListSortRows(_call->canManage()\n\t\t? makeComparator(projForAdmin)\n\t\t: makeComparator(projForOther));\n}\n\nvoid Members::Controller::updateRow(\n\t\tnot_null<Row*> row,\n\t\tconst std::optional<Data::GroupCallParticipant> &was,\n\t\tconst Data::GroupCallParticipant *participant,\n\t\tRow::State noParticipantState) {\n\tconst auto wasSounding = row->sounding();\n\tconst auto wasSsrc = was ? was->ssrc : 0;\n\tconst auto wasAdditionalSsrc = was\n\t\t? GetAdditionalAudioSsrc(was->videoParams)\n\t\t: 0;\n\trow->setSkipLevelUpdate(_skipRowLevelUpdate);\n\tif (participant) {\n\t\trow->updateState(*participant);\n\t} else if (noParticipantState == Row::State::WithAccess) {\n\t\trow->updateStateWithAccess();\n\t} else {\n\t\trow->updateStateInvited(noParticipantState == Row::State::Calling);\n\t}\n\n\tconst auto wasNoSounding = _soundingRowBySsrc.empty();\n\tupdateRowInSoundingMap(\n\t\trow,\n\t\twasSounding,\n\t\twasSsrc,\n\t\twasAdditionalSsrc,\n\t\tparticipant);\n\tconst auto nowNoSounding = _soundingRowBySsrc.empty();\n\tif (wasNoSounding && !nowNoSounding) {\n\t\t_soundingAnimation.start();\n\t} else if (nowNoSounding && !wasNoSounding) {\n\t\t_soundingAnimation.stop();\n\t}\n\n\tdelegate()->peerListUpdateRow(row);\n}\n\nvoid Members::Controller::updateRowInSoundingMap(\n\t\tnot_null<Row*> row,\n\t\tbool wasSounding,\n\t\tuint32 wasSsrc,\n\t\tuint32 wasAdditionalSsrc,\n\t\tconst Data::GroupCallParticipant *participant) {\n\tconst auto nowSounding = row->sounding();\n\tconst auto nowSsrc = participant ? participant->ssrc : 0;\n\tconst auto nowAdditionalSsrc = participant\n\t\t? GetAdditionalAudioSsrc(participant->videoParams)\n\t\t: 0;\n\tupdateRowInSoundingMap(row, wasSounding, wasSsrc, nowSounding, nowSsrc);\n\tupdateRowInSoundingMap(\n\t\trow,\n\t\twasSounding,\n\t\twasAdditionalSsrc,\n\t\tnowSounding,\n\t\tnowAdditionalSsrc);\n}\n\nvoid Members::Controller::updateRowInSoundingMap(\n\t\tnot_null<Row*> row,\n\t\tbool wasSounding,\n\t\tuint32 wasSsrc,\n\t\tbool nowSounding,\n\t\tuint32 nowSsrc) {\n\tif (wasSsrc == nowSsrc) {\n\t\tif (nowSsrc && nowSounding != wasSounding) {\n\t\t\tif (nowSounding) {\n\t\t\t\t_soundingRowBySsrc.emplace(nowSsrc, row);\n\t\t\t} else {\n\t\t\t\t_soundingRowBySsrc.remove(nowSsrc);\n\t\t\t}\n\t\t}\n\t} else {\n\t\t_soundingRowBySsrc.remove(wasSsrc);\n\t\tif (nowSounding && nowSsrc) {\n\t\t\t_soundingRowBySsrc.emplace(nowSsrc, row);\n\t\t}\n\t}\n}\n\nvoid Members::Controller::removeRow(not_null<Row*> row) {\n\tremoveRowFromSoundingMap(row);\n\tdelegate()->peerListRemoveRow(row);\n}\n\nvoid Members::Controller::removeRowFromSoundingMap(not_null<Row*> row) {\n\t// There may be 0, 1 or 2 entries for a row.\n\tfor (auto i = begin(_soundingRowBySsrc); i != end(_soundingRowBySsrc);) {\n\t\tif (i->second == row) {\n\t\t\ti = _soundingRowBySsrc.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n}\n\nvoid Members::Controller::updateRowLevel(\n\t\tnot_null<Row*> row,\n\t\tfloat level) {\n\tif (_skipRowLevelUpdate) {\n\t\treturn;\n\t}\n\trow->updateLevel(level);\n}\n\nRow *Members::Controller::findRow(\n\t\tnot_null<PeerData*> participantPeer) const {\n\treturn static_cast<Row*>(\n\t\tdelegate()->peerListFindRow(participantPeer->id.value));\n}\n\nvoid Members::Controller::setMode(PanelMode mode) {\n\t_mode = mode;\n}\n\nconst Data::GroupCallParticipant *Members::Controller::findParticipant(\n\t\tconst std::string &endpoint) const {\n\tif (endpoint.empty()) {\n\t\treturn nullptr;\n\t}\n\tconst auto real = _call->lookupReal();\n\tif (!real) {\n\t\treturn nullptr;\n\t} else if (endpoint == _call->screenSharingEndpoint()\n\t\t|| endpoint == _call->cameraSharingEndpoint()) {\n\t\treturn real->participantByPeer(_call->joinAs());\n\t} else {\n\t\treturn real->participantByEndpoint(endpoint);\n\t}\n}\n\nconst std::string &Members::Controller::computeScreenEndpoint(\n\t\tnot_null<const Data::GroupCallParticipant*> participant) const {\n\treturn (participant->peer == _call->joinAs())\n\t\t? _call->screenSharingEndpoint()\n\t\t: participant->screenEndpoint();\n}\n\nconst std::string &Members::Controller::computeCameraEndpoint(\n\t\tnot_null<const Data::GroupCallParticipant*> participant) const {\n\treturn (participant->peer == _call->joinAs())\n\t\t? _call->cameraSharingEndpoint()\n\t\t: participant->cameraEndpoint();\n}\n\nMain::Session &Members::Controller::session() const {\n\treturn _call->peer()->session();\n}\n\nvoid Members::Controller::prepare() {\n\tdelegate()->peerListSetSearchMode(PeerListSearchMode::Disabled);\n\tsetDescription(nullptr);\n\tsetSearchNoResults(nullptr);\n\n\tif (const auto real = _call->lookupReal()) {\n\t\tprepareRows(real);\n\t} else if (auto row = createRowForMe()) {\n\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\tdelegate()->peerListRefreshRows();\n\t}\n\n\tloadMoreRows();\n\tsetupWithAccessUsers();\n\tsetupInvitedUsers();\n\t_prepared = true;\n\n\tsetupListChangeViewers();\n}\n\nbool Members::Controller::isMe(not_null<PeerData*> participantPeer) const {\n\treturn (_call->joinAs() == participantPeer);\n}\n\nvoid Members::Controller::prepareRows(not_null<Data::GroupCall*> real) {\n\tauto foundMe = false;\n\tauto changed = false;\n\tauto count = delegate()->peerListFullRowsCount();\n\tfor (auto i = 0; i != count;) {\n\t\tconst auto row = static_cast<Row*>(\n\t\t\tdelegate()->peerListRowAt(i).get());\n\t\tremoveRowFromSoundingMap(row);\n\t\tconst auto participantPeer = row->peer();\n\t\tconst auto me = isMe(participantPeer);\n\t\tif (me) {\n\t\t\tfoundMe = true;\n\t\t}\n\t\tif (const auto found = real->participantByPeer(participantPeer)) {\n\t\t\tupdateRowInSoundingMap(row, false, 0, 0, found);\n\t\t\t++i;\n\t\t} else if (me) {\n\t\t\t++i;\n\t\t} else {\n\t\t\tchanged = true;\n\t\t\tremoveRow(row);\n\t\t\t--count;\n\t\t}\n\t}\n\tif (!foundMe) {\n\t\tconst auto me = _call->joinAs();\n\t\tconst auto participant = real->participantByPeer(me);\n\t\tauto row = participant\n\t\t\t? createRow(*participant)\n\t\t\t: createRowForMe();\n\t\tif (row) {\n\t\t\tchanged = true;\n\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t}\n\t}\n\tfor (const auto &participant : real->participants()) {\n\t\tif (auto row = createRow(participant)) {\n\t\t\tchanged = true;\n\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t}\n\t}\n\tif (appendWithAccessUsers()) {\n\t\tchanged = true;\n\t}\n\tif (appendInvitedUsers()) {\n\t\tchanged = true;\n\t}\n\tif (changed) {\n\t\tdelegate()->peerListRefreshRows();\n\t}\n}\n\nvoid Members::Controller::loadMoreRows() {\n\tif (const auto real = _call->lookupReal()) {\n\t\treal->requestParticipants();\n\t}\n}\n\nauto Members::Controller::toggleMuteRequests() const\n-> rpl::producer<MuteRequest> {\n\treturn _toggleMuteRequests.events();\n}\n\nauto Members::Controller::changeVolumeRequests() const\n-> rpl::producer<VolumeRequest> {\n\treturn _changeVolumeRequests.events();\n}\n\nbool Members::Controller::rowIsMe(not_null<PeerData*> participantPeer) {\n\treturn isMe(participantPeer);\n}\n\nbool Members::Controller::rowCanMuteMembers() {\n\treturn _call->canManage();\n}\n\nvoid Members::Controller::rowUpdateRow(not_null<Row*> row) {\n\tdelegate()->peerListUpdateRow(row);\n}\n\nvoid Members::Controller::rowScheduleRaisedHandStatusRemove(\n\t\tnot_null<Row*> row) {\n\tconst auto id = row->id();\n\tconst auto when = crl::now() + kKeepRaisedHandStatusDuration;\n\tconst auto i = _raisedHandStatusRemoveAt.find(id);\n\tif (i != _raisedHandStatusRemoveAt.end()) {\n\t\ti->second = when;\n\t} else {\n\t\t_raisedHandStatusRemoveAt.emplace(id, when);\n\t}\n\tscheduleRaisedHandStatusRemove();\n}\n\nvoid Members::Controller::scheduleRaisedHandStatusRemove() {\n\tauto waiting = crl::time(0);\n\tconst auto now = crl::now();\n\tfor (auto i = begin(_raisedHandStatusRemoveAt)\n\t\t; i != end(_raisedHandStatusRemoveAt);) {\n\t\tif (i->second <= now) {\n\t\t\tif (const auto row = delegate()->peerListFindRow(i->first)) {\n\t\t\t\tstatic_cast<Row*>(row)->clearRaisedHandStatus();\n\t\t\t}\n\t\t\ti = _raisedHandStatusRemoveAt.erase(i);\n\t\t} else {\n\t\t\tif (!waiting || waiting > (i->second - now)) {\n\t\t\t\twaiting = i->second - now;\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t}\n\tif (waiting > 0) {\n\t\tif (!_raisedHandStatusRemoveTimer.isActive()\n\t\t\t|| _raisedHandStatusRemoveTimer.remainingTime() > waiting) {\n\t\t\t_raisedHandStatusRemoveTimer.callOnce(waiting);\n\t\t}\n\t}\n}\n\nvoid Members::Controller::rowPaintIcon(\n\t\tQPainter &p,\n\t\tQRect rect,\n\t\tconst IconState &state) {\n\tif (_mode == PanelMode::Wide\n\t\t&& state.style == MembersRowStyle::Default) {\n\t\treturn;\n\t}\n\tconst auto narrow = (state.style == MembersRowStyle::Narrow);\n\tif (state.invited || state.calling) {\n\t\tif (narrow) {\n\t\t\t(state.invited\n\t\t\t\t? st::groupCallNarrowInvitedIcon\n\t\t\t\t: st::groupCallNarrowCallingIcon).paintInCenter(p, rect);\n\t\t} else {\n\t\t\tconst auto &icon = state.invited\n\t\t\t\t? st::groupCallMemberInvited\n\t\t\t\t: st::groupCallMemberCalling;\n\t\t\tconst auto shift = state.invited\n\t\t\t\t? st::groupCallMemberInvitedPosition\n\t\t\t\t: st::groupCallMemberCallingPosition;\n\t\t\ticon.paintInCenter(\n\t\t\t\tp,\n\t\t\t\tQRect(rect.topLeft() + shift, icon.size()));\n\t\t}\n\t\treturn;\n\t}\n\tconst auto video = (state.style == MembersRowStyle::Video);\n\tconst auto &greenIcon = video\n\t\t? st::groupCallVideoCrossLine.icon\n\t\t: narrow\n\t\t? st::groupCallNarrowColoredCrossLine.icon\n\t\t: st::groupCallMemberColoredCrossLine.icon;\n\tconst auto left = rect.x() + (rect.width() - greenIcon.width()) / 2;\n\tconst auto top = rect.y() + (rect.height() - greenIcon.height()) / 2;\n\tif (state.speaking == 1. && !state.mutedByMe) {\n\t\t// Just green icon, no cross, no coloring.\n\t\tgreenIcon.paintInCenter(p, rect);\n\t\treturn;\n\t} else if (state.speaking == 0. && (!narrow || !state.mutedByMe)) {\n\t\tif (state.active == 1.) {\n\t\t\t// Just gray icon, no cross, no coloring.\n\t\t\tconst auto &grayIcon = video\n\t\t\t\t? st::groupCallVideoCrossLine.icon\n\t\t\t\t: narrow\n\t\t\t\t? st::groupCallNarrowInactiveCrossLine.icon\n\t\t\t\t: st::groupCallMemberInactiveCrossLine.icon;\n\t\t\tgrayIcon.paintInCenter(p, rect);\n\t\t\treturn;\n\t\t} else if (state.active == 0.) {\n\t\t\tif (state.muted == 1.) {\n\t\t\t\tif (state.raisedHand) {\n\t\t\t\t\t(narrow\n\t\t\t\t\t\t? st::groupCallNarrowRaisedHand\n\t\t\t\t\t\t: st::groupCallMemberRaisedHand).paintInCenter(p, rect);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Red crossed icon, colorized once, cached as last frame.\n\t\t\t\tauto &line = video\n\t\t\t\t\t? _videoCrossLine\n\t\t\t\t\t: narrow\n\t\t\t\t\t? _coloredNarrowCrossLine\n\t\t\t\t\t: _coloredCrossLine;\n\t\t\t\tconst auto color = video\n\t\t\t\t\t? std::nullopt\n\t\t\t\t\t: std::make_optional(st::groupCallMemberMutedIcon->c);\n\t\t\t\tline.paint(\n\t\t\t\t\tp,\n\t\t\t\t\tleft,\n\t\t\t\t\ttop,\n\t\t\t\t\t1.,\n\t\t\t\t\tcolor);\n\t\t\t\treturn;\n\t\t\t} else if (state.muted == 0.) {\n\t\t\t\t// Gray crossed icon, no coloring, cached as last frame.\n\t\t\t\tauto &line = video\n\t\t\t\t\t? _videoCrossLine\n\t\t\t\t\t: narrow\n\t\t\t\t\t? _inactiveNarrowCrossLine\n\t\t\t\t\t: _inactiveCrossLine;\n\t\t\t\tline.paint(p, left, top, 1.);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto activeInactiveColor = anim::color(\n\t\t(narrow\n\t\t\t? st::groupCallMemberNotJoinedStatus\n\t\t\t: st::groupCallMemberInactiveIcon),\n\t\t(narrow\n\t\t\t? st::groupCallMemberActiveStatus\n\t\t\t: state.mutedByMe\n\t\t\t? st::groupCallMemberMutedIcon\n\t\t\t: st::groupCallMemberActiveIcon),\n\t\tstate.speaking);\n\tconst auto iconColor = anim::color(\n\t\tactiveInactiveColor,\n\t\tst::groupCallMemberMutedIcon,\n\t\tstate.muted);\n\tconst auto color = video\n\t\t? std::nullopt\n\t\t: std::make_optional((narrow && state.mutedByMe)\n\t\t\t? st::groupCallMemberMutedIcon->c\n\t\t\t: (narrow && state.raisedHand)\n\t\t\t? st::groupCallMemberInactiveStatus->c\n\t\t\t: iconColor);\n\n\t// Don't use caching of the last frame,\n\t// because 'muted' may animate color.\n\tconst auto crossProgress = std::min(1. - state.active, 0.9999);\n\tauto &line = video\n\t\t? _videoCrossLine\n\t\t: narrow\n\t\t? _inactiveNarrowCrossLine\n\t\t: _inactiveCrossLine;\n\tline.paint(p, left, top, crossProgress, color);\n}\n\nint Members::Controller::rowPaintStatusIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tnot_null<MembersRow*> row,\n\t\tconst IconState &state) {\n\tExpects(state.style == MembersRowStyle::Narrow);\n\n\tif (_mode != PanelMode::Wide) {\n\t\treturn 0;\n\t}\n\tconst auto &icon = st::groupCallNarrowColoredCrossLine.icon;\n\tx += st::groupCallNarrowIconPosition.x();\n\ty += st::groupCallNarrowIconPosition.y();\n\tconst auto rect = QRect(x, y, icon.width(), icon.height());\n\trowPaintIcon(p, rect, state);\n\tx += icon.width();\n\tauto result = st::groupCallNarrowIconSkip;\n\tconst auto participantPeer = row->peer();\n\tconst auto camera = _cameraActive.contains(participantPeer);\n\tconst auto screen = _screenActive.contains(participantPeer);\n\tif (camera || screen) {\n\t\tconst auto activeInactiveColor = anim::color(\n\t\t\tst::groupCallMemberNotJoinedStatus,\n\t\t\tst::groupCallMemberActiveStatus,\n\t\t\tstate.speaking);\n\t\tconst auto iconColor = anim::color(\n\t\t\tactiveInactiveColor,\n\t\t\tst::groupCallMemberNotJoinedStatus,\n\t\t\tstate.muted);\n\t\tconst auto other = state.mutedByMe\n\t\t\t? st::groupCallMemberMutedIcon->c\n\t\t\t: state.raisedHand\n\t\t\t? st::groupCallMemberInactiveStatus->c\n\t\t\t: iconColor;\n\t\tif (camera) {\n\t\t\tst::groupCallNarrowCameraIcon.paint(p, x, y, outerWidth, other);\n\t\t\tx += st::groupCallNarrowCameraIcon.width();\n\t\t\tresult += st::groupCallNarrowCameraIcon.width();\n\t\t}\n\t\tif (screen) {\n\t\t\tst::groupCallNarrowScreenIcon.paint(p, x, y, outerWidth, other);\n\t\t\tx += st::groupCallNarrowScreenIcon.width();\n\t\t\tresult += st::groupCallNarrowScreenIcon.width();\n\t\t}\n\t}\n\treturn result;\n}\n\nbool Members::Controller::rowIsNarrow() {\n\treturn (_mode == PanelMode::Wide);\n}\n\nvoid Members::Controller::rowShowContextMenu(not_null<PeerListRow*> row) {\n\tshowRowMenu(row, false);\n}\n\nauto Members::Controller::kickParticipantRequests() const\n-> rpl::producer<not_null<PeerData*>>{\n\treturn _kickParticipantRequests.events();\n}\n\nvoid Members::Controller::rowClicked(not_null<PeerListRow*> row) {\n\tshowRowMenu(row, true);\n}\n\nvoid Members::Controller::showRowMenu(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool highlightRow) {\n\tconst auto cleanup = [=](not_null<Ui::PopupMenu*> menu) {\n\t\tif (!_menu || _menu.get() != menu) {\n\t\t\treturn;\n\t\t}\n\t\tauto saved = base::take(_menu);\n\t\tfor (const auto &peer : base::take(_menuCheckRowsAfterHidden)) {\n\t\t\tif (const auto row = findRow(peer)) {\n\t\t\t\tcheckRowPosition(row);\n\t\t\t}\n\t\t}\n\t\t_menu = std::move(saved);\n\t};\n\tdelegate()->peerListShowRowMenu(row, highlightRow, cleanup);\n}\n\nvoid Members::Controller::rowRightActionClicked(\n\t\tnot_null<PeerListRow*> row) {\n\tshowRowMenu(row, true);\n}\n\nbase::unique_qptr<Ui::PopupMenu> Members::Controller::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tauto result = createRowContextMenu(parent, row);\n\n\tif (result) {\n\t\t// First clear _menu value, so that we don't check row positions yet.\n\t\tbase::take(_menu);\n\n\t\t// Here unique_qptr is used like a shared pointer, where\n\t\t// not the last destroyed pointer destroys the object, but the first.\n\t\t_menu = base::unique_qptr<Ui::PopupMenu>(result.get());\n\t}\n\n\treturn result;\n}\n\nbase::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tconst auto participantPeer = row->peer();\n\tconst auto real = static_cast<Row*>(row.get());\n\tconst auto muteState = real->state();\n\tif (muteState == Row::State::WithAccess) {\n\t\treturn nullptr;\n\t}\n\tconst auto muted = (muteState == Row::State::Muted)\n\t\t|| (muteState == Row::State::RaisedHand);\n\tconst auto addCover = !_call->rtmp() && !_call->videoStream();\n\tconst auto addVolumeItem = (!muted || isMe(participantPeer));\n\tconst auto admin = IsGroupCallAdmin(_peer, participantPeer);\n\tconst auto session = &_peer->session();\n\tconst auto account = &session->account();\n\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\t(addCover\n\t\t\t? st::groupCallPopupMenuWithCover\n\t\t\t: addVolumeItem\n\t\t\t? st::groupCallPopupMenuWithVolume\n\t\t\t: st::groupCallPopupMenu));\n\tconst auto weakMenu = base::make_weak(result.get());\n\tconst auto withActiveWindow = [=](auto callback) {\n\t\tif (const auto window = Core::App().activePrimaryWindow()) {\n\t\t\tif (const auto menu = weakMenu.get()) {\n\t\t\t\tmenu->discardParentReActivate();\n\n\t\t\t\t// We must hide PopupMenu before we activate the MainWindow,\n\t\t\t\t// otherwise we set focus in field inside MainWindow and then\n\t\t\t\t// PopupMenu::hide activates back the group call panel :(\n\t\t\t\tdelete weakMenu.get();\n\t\t\t}\n\t\t\twindow->invokeForSessionController(\n\t\t\t\taccount,\n\t\t\t\tparticipantPeer,\n\t\t\t\t[&](not_null<::Window::SessionController*> newController) {\n\t\t\t\t\tcallback(newController);\n\t\t\t\t\tnewController->widget()->activate();\n\t\t\t\t});\n\t\t}\n\t};\n\tconst auto showProfile = [=] {\n\t\twithActiveWindow([=](not_null<::Window::SessionController*> window) {\n\t\t\twindow->showPeerInfo(participantPeer);\n\t\t});\n\t};\n\tconst auto showHistory = [=] {\n\t\twithActiveWindow([=](not_null<::Window::SessionController*> window) {\n\t\t\twindow->showPeerHistory(\n\t\t\t\tparticipantPeer,\n\t\t\t\t::Window::SectionShow::Way::Forward);\n\t\t});\n\t};\n\tconst auto removeFromVoiceChat = crl::guard(this, [=] {\n\t\t_kickParticipantRequests.fire_copy(participantPeer);\n\t});\n\n\tif (addCover) {\n\t\tresult->addAction(base::make_unique_q<CoverItem>(\n\t\t\tresult->menu(),\n\t\t\tst::groupCallPopupCoverMenu,\n\t\t\tst::groupCallMenuCover,\n\t\t\tInfo::Profile::NameValue(participantPeer),\n\t\t\tPrepareShortInfoStatus(participantPeer),\n\t\t\tPrepareShortInfoUserpic(\n\t\t\t\tparticipantPeer,\n\t\t\t\tst::groupCallMenuCover)));\n\n\t\tif (const auto about = participantPeer->about(); !about.isEmpty()) {\n\t\t\tresult->addAction(base::make_unique_q<AboutItem>(\n\t\t\t\tresult->menu(),\n\t\t\t\tst::groupCallPopupCoverMenu,\n\t\t\t\tInfo::Profile::AboutWithEntities(participantPeer, about)));\n\t\t}\n\t}\n\n\tif (const auto real = _call->lookupReal()) {\n\t\tauto oneFound = false;\n\t\tauto hasTwoOrMore = false;\n\t\tconst auto &shown = _call->shownVideoTracks();\n\t\tfor (const auto &[endpoint, track] : _call->activeVideoTracks()) {\n\t\t\tif (shown.contains(endpoint)) {\n\t\t\t\tif (oneFound) {\n\t\t\t\t\thasTwoOrMore = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\toneFound = true;\n\t\t\t}\n\t\t}\n\t\tconst auto participant = real->participantByPeer(participantPeer);\n\t\tif (participant && hasTwoOrMore) {\n\t\t\tconst auto &large = _call->videoEndpointLarge();\n\t\t\tconst auto pinned = _call->videoEndpointPinned();\n\t\t\tconst auto camera = VideoEndpoint{\n\t\t\t\tVideoEndpointType::Camera,\n\t\t\t\tparticipantPeer,\n\t\t\t\tcomputeCameraEndpoint(participant),\n\t\t\t};\n\t\t\tconst auto screen = VideoEndpoint{\n\t\t\t\tVideoEndpointType::Screen,\n\t\t\t\tparticipantPeer,\n\t\t\t\tcomputeScreenEndpoint(participant),\n\t\t\t};\n\t\t\tif (shown.contains(camera)) {\n\t\t\t\tif (pinned && large == camera) {\n\t\t\t\t\tresult->addAction(\n\t\t\t\t\t\ttr::lng_group_call_context_unpin_camera(tr::now),\n\t\t\t\t\t\t[=] { _call->pinVideoEndpoint({}); });\n\t\t\t\t} else {\n\t\t\t\t\tresult->addAction(\n\t\t\t\t\t\ttr::lng_group_call_context_pin_camera(tr::now),\n\t\t\t\t\t\t[=] { _call->pinVideoEndpoint(camera); });\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (shown.contains(screen)) {\n\t\t\t\tif (pinned && large == screen) {\n\t\t\t\t\tresult->addAction(\n\t\t\t\t\t\ttr::lng_group_call_context_unpin_screen(tr::now),\n\t\t\t\t\t\t[=] { _call->pinVideoEndpoint({}); });\n\t\t\t\t} else {\n\t\t\t\t\tresult->addAction(\n\t\t\t\t\t\ttr::lng_group_call_context_pin_screen(tr::now),\n\t\t\t\t\t\t[=] { _call->pinVideoEndpoint(screen); });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (_call->rtmp() || _call->videoStream()) {\n\t\t\taddMuteActionsToContextMenu(\n\t\t\t\tresult,\n\t\t\t\trow->peer(),\n\t\t\t\tfalse,\n\t\t\t\tstatic_cast<Row*>(row.get()));\n\t\t} else if (participant\n\t\t\t&& (!isMe(participantPeer) || _call->canManage())\n\t\t\t&& (participant->ssrc != 0\n\t\t\t\t|| GetAdditionalAudioSsrc(participant->videoParams) != 0)) {\n\t\t\taddMuteActionsToContextMenu(\n\t\t\t\tresult,\n\t\t\t\tparticipantPeer,\n\t\t\t\tadmin,\n\t\t\t\tstatic_cast<Row*>(row.get()));\n\t\t}\n\t}\n\n\tif (isMe(participantPeer)) {\n\t\tif (_call->muted() == MuteState::RaisedHand) {\n\t\t\tconst auto removeHand = [=] {\n\t\t\t\tif (_call->muted() == MuteState::RaisedHand) {\n\t\t\t\t\t_call->setMutedAndUpdate(MuteState::ForceMuted);\n\t\t\t\t}\n\t\t\t};\n\t\t\tresult->addAction(\n\t\t\t\ttr::lng_group_call_context_remove_hand(tr::now),\n\t\t\t\tremoveHand);\n\t\t}\n\t} else {\n\t\tconst auto invited = (muteState == Row::State::Invited)\n\t\t\t|| (muteState == Row::State::Calling);\n\t\tconst auto conference = _call->sharedCall().get();\n\t\tif (_call->conference()\n\t\t\t&& conference\n\t\t\t&& participantPeer->isUser()\n\t\t\t&& invited) {\n\t\t\tconst auto id = conference->id();\n\t\t\tconst auto cancelInvite = [=](bool discard) {\n\t\t\t\tCore::App().calls().declineOutgoingConferenceInvite(\n\t\t\t\t\tid,\n\t\t\t\t\tparticipantPeer->asUser(),\n\t\t\t\t\tdiscard);\n\t\t\t};\n\t\t\tif (muteState == Row::State::Calling) {\n\t\t\t\tresult->addAction(\n\t\t\t\t\ttr::lng_group_call_context_stop_ringing(tr::now),\n\t\t\t\t\t[=] { cancelInvite(false); });\n\t\t\t}\n\t\t\tresult->addAction(\n\t\t\t\ttr::lng_group_call_context_cancel_invite(tr::now),\n\t\t\t\t[=] { cancelInvite(true); });\n\t\t\tresult->addSeparator();\n\t\t}\n\t\tresult->addAction(\n\t\t\t(participantPeer->isUser()\n\t\t\t\t? tr::lng_context_view_profile(tr::now)\n\t\t\t\t: participantPeer->isBroadcast()\n\t\t\t\t? tr::lng_context_view_channel(tr::now)\n\t\t\t\t: tr::lng_context_view_group(tr::now)),\n\t\t\tshowProfile);\n\t\tif (participantPeer->isUser()) {\n\t\t\tresult->addAction(\n\t\t\t\ttr::lng_context_send_message(tr::now),\n\t\t\t\tshowHistory);\n\t\t}\n\t\tconst auto canKick = [&] {\n\t\t\tconst auto user = participantPeer->asUser();\n\t\t\tif (muteState == Row::State::Invited\n\t\t\t\t|| muteState == Row::State::Calling\n\t\t\t\t|| muteState == Row::State::WithAccess) {\n\t\t\t\treturn false;\n\t\t\t} else if (conference && _call->canManage()) {\n\t\t\t\treturn true;\n\t\t\t} else if (const auto chat = _peer->asChat()) {\n\t\t\t\treturn chat->amCreator()\n\t\t\t\t\t|| (user\n\t\t\t\t\t\t&& chat->canBanMembers()\n\t\t\t\t\t\t&& !chat->admins.contains(user));\n\t\t\t} else if (const auto channel = _peer->asChannel()) {\n\t\t\t\treturn !participantPeer->isMegagroup() // That's the creator.\n\t\t\t\t\t&& channel->canRestrictParticipant(participantPeer);\n\t\t\t}\n\t\t\treturn false;\n\t\t}();\n\t\tif (canKick) {\n\t\t\tresult->addAction(MakeAttentionAction(\n\t\t\t\tresult->menu(),\n\t\t\t\ttr::lng_group_call_context_remove(tr::now),\n\t\t\t\tremoveFromVoiceChat));\n\t\t}\n\t}\n\tif (result->actions().size() < (addCover ? 2 : 1)) {\n\t\treturn nullptr;\n\t}\n\treturn result;\n}\n\nvoid Members::Controller::addMuteActionsToContextMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<PeerData*> participantPeer,\n\t\tbool participantIsCallAdmin,\n\t\tnot_null<Row*> row) {\n\tconst auto muteUnmuteString = [=](bool muted, bool mutedByMe) {\n\t\treturn (muted && _call->canManage())\n\t\t\t? tr::lng_group_call_context_unmute(tr::now)\n\t\t\t: mutedByMe\n\t\t\t? tr::lng_group_call_context_unmute_for_me(tr::now)\n\t\t\t: _call->canManage()\n\t\t\t? tr::lng_group_call_context_mute(tr::now)\n\t\t\t: tr::lng_group_call_context_mute_for_me(tr::now);\n\t};\n\n\tconst auto toggleMute = crl::guard(this, [=](bool mute, bool local) {\n\t\t_toggleMuteRequests.fire(Group::MuteRequest{\n\t\t\t.peer = participantPeer,\n\t\t\t.mute = mute,\n\t\t\t.locallyOnly = local,\n\t\t});\n\t});\n\tconst auto changeVolume = crl::guard(this, [=](\n\t\t\tint volume,\n\t\t\tbool local) {\n\t\t_changeVolumeRequests.fire(Group::VolumeRequest{\n\t\t\t.peer = participantPeer,\n\t\t\t.volume = std::clamp(volume, 1, Group::kMaxVolume),\n\t\t\t.locallyOnly = local,\n\t\t});\n\t});\n\n\tconst auto muteState = row->state();\n\tconst auto muted = (muteState == Row::State::Muted)\n\t\t|| (muteState == Row::State::RaisedHand);\n\tconst auto mutedByMe = row->mutedByMe();\n\n\tauto mutesFromVolume = rpl::never<bool>() | rpl::type_erased;\n\n\tconst auto addVolumeItem = (!muted || isMe(participantPeer));\n\tif (addVolumeItem) {\n\t\tauto otherParticipantStateValue\n\t\t\t= _call->otherParticipantStateValue(\n\t\t) | rpl::filter([=](const Group::ParticipantState &data) {\n\t\t\treturn data.peer == participantPeer;\n\t\t});\n\n\t\tauto volumeItem = base::make_unique_q<MenuVolumeItem>(\n\t\t\tmenu->menu(),\n\t\t\tst::groupCallPopupVolumeMenu,\n\t\t\tst::groupCallMenuVolumeSlider,\n\t\t\totherParticipantStateValue,\n\t\t\t_call->rtmp() ? _call->rtmpVolume() : row->volume(),\n\t\t\tGroup::kMaxVolume,\n\t\t\tmuted,\n\t\t\tst::groupCallMenuVolumePadding);\n\n\t\tmutesFromVolume = volumeItem->toggleMuteRequests();\n\n\t\tvolumeItem->toggleMuteRequests(\n\t\t) | rpl::on_next([=](bool muted) {\n\t\t\tif (muted) {\n\t\t\t\t// Slider value is changed after the callback is called.\n\t\t\t\t// To capture good state inside the slider frame we postpone.\n\t\t\t\tcrl::on_main(menu, [=] {\n\t\t\t\t\tmenu->hideMenu();\n\t\t\t\t});\n\t\t\t}\n\t\t\ttoggleMute(muted, false);\n\t\t}, volumeItem->lifetime());\n\n\t\tvolumeItem->toggleMuteLocallyRequests(\n\t\t) | rpl::on_next([=](bool muted) {\n\t\t\tif (!isMe(participantPeer)) {\n\t\t\t\ttoggleMute(muted, true);\n\t\t\t}\n\t\t}, volumeItem->lifetime());\n\n\t\tvolumeItem->changeVolumeRequests(\n\t\t) | rpl::on_next([=](int volume) {\n\t\t\tchangeVolume(volume, false);\n\t\t}, volumeItem->lifetime());\n\n\t\tvolumeItem->changeVolumeLocallyRequests(\n\t\t) | rpl::on_next([=](int volume) {\n\t\t\tif (!isMe(participantPeer)) {\n\t\t\t\tchangeVolume(volume, true);\n\t\t\t}\n\t\t}, volumeItem->lifetime());\n\n\t\tif (menu->actions().size() > 1) { // First - cover.\n\t\t\tmenu->addSeparator();\n\t\t}\n\n\t\tmenu->addAction(std::move(volumeItem));\n\n\t\tif (!_call->rtmp()\n\t\t\t&& !_call->videoStream()\n\t\t\t&& !isMe(participantPeer)) {\n\t\t\tmenu->addSeparator();\n\t\t}\n\t};\n\n\tconst auto muteAction = [&]() -> QAction* {\n\t\tif (muteState == Row::State::Invited\n\t\t\t|| muteState == Row::State::Calling\n\t\t\t|| muteState == Row::State::WithAccess\n\t\t\t|| _call->rtmp()\n\t\t\t|| _call->videoStream()\n\t\t\t|| isMe(participantPeer)\n\t\t\t|| (muteState == Row::State::Inactive\n\t\t\t\t&& participantIsCallAdmin\n\t\t\t\t&& _call->canManage())) {\n\t\t\treturn nullptr;\n\t\t}\n\t\tauto callback = [=] {\n\t\t\tconst auto state = row->state();\n\t\t\tconst auto muted = (state == Row::State::Muted)\n\t\t\t\t|| (state == Row::State::RaisedHand);\n\t\t\tconst auto mutedByMe = row->mutedByMe();\n\t\t\ttoggleMute(!mutedByMe && (!_call->canManage() || !muted), false);\n\t\t};\n\t\treturn menu->addAction(\n\t\t\tmuteUnmuteString(muted, mutedByMe),\n\t\t\tstd::move(callback));\n\t}();\n\n\tif (muteAction) {\n\t\tstd::move(\n\t\t\tmutesFromVolume\n\t\t) | rpl::on_next([=](bool mutedFromVolume) {\n\t\t\tconst auto state = _call->canManage()\n\t\t\t\t? (mutedFromVolume\n\t\t\t\t\t? (row->raisedHandRating()\n\t\t\t\t\t\t? Row::State::RaisedHand\n\t\t\t\t\t\t: Row::State::Muted)\n\t\t\t\t\t: Row::State::Inactive)\n\t\t\t\t: row->state();\n\t\t\tconst auto muted = (state == Row::State::Muted)\n\t\t\t\t|| (state == Row::State::RaisedHand);\n\t\t\tconst auto mutedByMe = _call->canManage()\n\t\t\t\t? false\n\t\t\t\t: mutedFromVolume;\n\t\t\tmuteAction->setText(muteUnmuteString(muted, mutedByMe));\n\t\t}, menu->lifetime());\n\t}\n}\n\nstd::unique_ptr<Row> Members::Controller::createRowForMe() {\n\tauto result = std::make_unique<Row>(this, _call->joinAs());\n\tupdateRow(result.get(), std::nullopt, nullptr);\n\treturn result;\n}\n\nstd::unique_ptr<Row> Members::Controller::createRow(\n\t\tconst Data::GroupCallParticipant &participant) {\n\tauto result = std::make_unique<Row>(this, participant.peer);\n\tupdateRow(result.get(), std::nullopt, &participant);\n\treturn result;\n}\n\nstd::unique_ptr<Row> Members::Controller::createInvitedRow(\n\t\tnot_null<PeerData*> participantPeer,\n\t\tbool calling) {\n\tif (const auto row = findRow(participantPeer)) {\n\t\tif (row->state() == Row::State::Invited\n\t\t\t|| row->state() == Row::State::Calling) {\n\t\t\trow->updateStateInvited(calling);\n\t\t\tdelegate()->peerListUpdateRow(row);\n\t\t}\n\t\treturn nullptr;\n\t}\n\tconst auto state = calling ? Row::State::Calling : Row::State::Invited;\n\tauto result = std::make_unique<Row>(this, participantPeer);\n\tupdateRow(result.get(), std::nullopt, nullptr, state);\n\treturn result;\n}\n\nstd::unique_ptr<Row> Members::Controller::createWithAccessRow(\n\t\tnot_null<PeerData*> participantPeer) {\n\tif (findRow(participantPeer)) {\n\t\treturn nullptr;\n\t}\n\tauto result = std::make_unique<Row>(this, participantPeer);\n\tupdateRow(result.get(), std::nullopt, nullptr, Row::State::WithAccess);\n\treturn result;\n}\n\nMembers::Members(\n\tnot_null<QWidget*> parent,\n\tnot_null<GroupCall*> call,\n\tPanelMode mode,\n\tUi::GL::Backend backend)\n: RpWidget(parent)\n, _call(call)\n, _mode(mode)\n, _scroll(this)\n, _listController(std::make_unique<Controller>(call, parent, mode))\n, _layout(_scroll->setOwnedWidget(\n\tobject_ptr<Ui::VerticalLayout>(_scroll.data())))\n, _fingerprint(call->conference()\n\t? _layout->add(object_ptr<Ui::RpWidget>(_layout.get()))\n\t: nullptr)\n, _videoWrap(_layout->add(object_ptr<Ui::RpWidget>(_layout.get())))\n, _viewport(\n\tstd::make_unique<Viewport>(\n\t\t_videoWrap.get(),\n\t\tPanelMode::Default,\n\t\tbackend)) {\n\tsetupList();\n\tsetupAddMember(call);\n\tsetupFingerprint();\n\tsetContent(_list);\n\tsetupFakeRoundCorners();\n\t_listController->setDelegate(static_cast<PeerListDelegate*>(this));\n\ttrackViewportGeometry();\n}\n\nMembers::~Members() {\n\t_viewport = nullptr;\n}\n\nauto Members::toggleMuteRequests() const\n-> rpl::producer<Group::MuteRequest> {\n\treturn _listController->toggleMuteRequests();\n}\n\nauto Members::changeVolumeRequests() const\n-> rpl::producer<Group::VolumeRequest> {\n\treturn _listController->changeVolumeRequests();\n}\n\nauto Members::kickParticipantRequests() const\n-> rpl::producer<not_null<PeerData*>> {\n\treturn _listController->kickParticipantRequests();\n}\n\nnot_null<Viewport*> Members::viewport() const {\n\treturn _viewport.get();\n}\n\nint Members::desiredHeight() const {\n\tconst auto count = [&] {\n\t\tif (const auto real = _call->lookupReal()) {\n\t\t\treturn real->fullCount();\n\t\t}\n\t\treturn 0;\n\t}();\n\tconst auto use = std::max(count, _list->fullRowsCount());\n\tconst auto single = st::groupCallMembersList.item.height;\n\tconst auto desired = (_layout->height() - _list->height())\n\t\t+ (use * single)\n\t\t+ (use ? st::lineWidth : 0);\n\treturn std::max(height(), desired);\n}\n\nrpl::producer<int> Members::desiredHeightValue() const {\n\treturn rpl::combine(\n\t\theightValue(),\n\t\t_addMemberButton.value(),\n\t\t_shareLinkButton.value(),\n\t\t_listController->fullCountValue(),\n\t\t_mode.value()\n\t) | rpl::map([=] {\n\t\treturn desiredHeight();\n\t});\n}\n\nvoid Members::setupAddMember(not_null<GroupCall*> call) {\n\tusing namespace rpl::mappers;\n\n\tconst auto peer = call->peer();\n\tconst auto conference = call->conference();\n\tconst auto canAddByPeer = [=](not_null<PeerData*> peer) {\n\t\tif (conference) {\n\t\t\treturn rpl::single(true) | rpl::type_erased;\n\t\t} else if (peer->isBroadcast()) {\n\t\t\treturn rpl::single(false) | rpl::type_erased;\n\t\t}\n\t\treturn rpl::combine(\n\t\t\tData::CanSendValue(peer, ChatRestriction::SendOther, false),\n\t\t\t_call->joinAsValue()\n\t\t) | rpl::map([=](bool can, not_null<PeerData*> joinAs) {\n\t\t\treturn can && joinAs->isSelf();\n\t\t}) | rpl::type_erased;\n\t};\n\tconst auto canInviteByLinkByPeer = [=](not_null<PeerData*> peer) {\n\t\tif (conference) {\n\t\t\treturn rpl::single(true) | rpl::type_erased;\n\t\t}\n\t\tconst auto channel = peer->asChannel();\n\t\tif (!channel) {\n\t\t\treturn rpl::single(false) | rpl::type_erased;\n\t\t}\n\t\treturn rpl::single(\n\t\t\tfalse\n\t\t) | rpl::then(_call->real(\n\t\t) | rpl::map([=] {\n\t\t\treturn Data::PeerFlagValue(\n\t\t\t\tchannel,\n\t\t\t\tChannelDataFlag::Username);\n\t\t}) | rpl::flatten_latest()) | rpl::type_erased;\n\t};\n\t_canAddMembers = canAddByPeer(peer);\n\t_canInviteByLink = canInviteByLinkByPeer(peer);\n\tSubscribeToMigration(\n\t\tpeer,\n\t\tlifetime(),\n\t\t[=](not_null<ChannelData*> channel) {\n\t\t\t_canAddMembers = canAddByPeer(channel);\n\t\t\t_canInviteByLink = canInviteByLinkByPeer(channel);\n\t\t});\n\n\tconst auto baseIndex = _layout->count() - 2;\n\n\trpl::combine(\n\t\t_canAddMembers.value(),\n\t\t_canInviteByLink.value(),\n\t\t_mode.value()\n\t) | rpl::on_next([=](bool add, bool invite, PanelMode mode) {\n\t\tif (!add && !invite) {\n\t\t\tif (const auto old = _addMemberButton.current()) {\n\t\t\t\tdelete old;\n\t\t\t\t_addMemberButton = nullptr;\n\t\t\t\tupdateControlsGeometry();\n\t\t\t}\n\t\t\tif (const auto old = _shareLinkButton.current()) {\n\t\t\t\tdelete old;\n\t\t\t\t_shareLinkButton = nullptr;\n\t\t\t\tupdateControlsGeometry();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tauto addMember = Settings::CreateButtonWithIcon(\n\t\t\t_layout.get(),\n\t\t\t(conference\n\t\t\t\t? tr::lng_group_call_invite_conf()\n\t\t\t\t: tr::lng_group_call_invite()),\n\t\t\tst::groupCallAddMember,\n\t\t\t{ .icon = &st::groupCallAddMemberIcon });\n\t\taddMember->clicks(\n\t\t) | rpl::to_empty | rpl::start_to_stream(\n\t\t\t_addMemberRequests,\n\t\t\taddMember->lifetime());\n\t\taddMember->show();\n\t\taddMember->resizeToWidth(_layout->width());\n\t\tdelete _addMemberButton.current();\n\t\t_addMemberButton = addMember.data();\n\t\t_layout->insert(baseIndex, std::move(addMember));\n\t\tif (conference) {\n\t\t\tauto shareLink = Settings::CreateButtonWithIcon(\n\t\t\t\t_layout.get(),\n\t\t\t\ttr::lng_group_invite_share(),\n\t\t\t\tst::groupCallAddMember,\n\t\t\t\t{ .icon = &st::groupCallShareLinkIcon });\n\t\t\tshareLink->clicks() | rpl::to_empty | rpl::start_to_stream(\n\t\t\t\t_shareLinkRequests,\n\t\t\t\tshareLink->lifetime());\n\t\t\tshareLink->show();\n\t\t\tshareLink->resizeToWidth(_layout->width());\n\t\t\tdelete _shareLinkButton.current();\n\t\t\t_shareLinkButton = shareLink.data();\n\t\t\t_layout->insert(baseIndex + 1, std::move(shareLink));\n\t\t}\n\t}, lifetime());\n\n\tupdateControlsGeometry();\n}\n\nRow *Members::lookupRow(not_null<PeerData*> peer) const {\n\treturn _listController->findRow(peer);\n}\n\nnot_null<MembersRow*> Members::rtmpFakeRow(not_null<PeerData*> peer) const {\n\tif (!_rtmpFakeRow) {\n\t\t_rtmpFakeRow = std::make_unique<Row>(_listController.get(), peer);\n\t}\n\treturn _rtmpFakeRow.get();\n}\n\nvoid Members::setMode(PanelMode mode) {\n\tif (_mode.current() == mode) {\n\t\treturn;\n\t}\n\t_mode = mode;\n\t_listController->setMode(mode);\n}\n\nQRect Members::getInnerGeometry() const {\n\tconst auto shareLink = _shareLinkButton.current();\n\tconst auto addMembers = _addMemberButton.current();\n\tconst auto share = shareLink ? shareLink->height() : 0;\n\tconst auto add = addMembers ? addMembers->height() : 0;\n\treturn QRect(\n\t\t0,\n\t\t-_scroll->scrollTop(),\n\t\twidth(),\n\t\t_list->y() + _list->height() + _bottomSkip->height() + add + share);\n}\n\nrpl::producer<int> Members::fullCountValue() const {\n\treturn _listController->fullCountValue();\n}\n\nvoid Members::setupList() {\n\t_listController->setStyleOverrides(&st::groupCallMembersList);\n\tconst auto addSkip = [&] {\n\t\tconst auto result = _layout->add(\n\t\t\tobject_ptr<Ui::FixedHeightWidget>(\n\t\t\t\t_layout.get(),\n\t\t\t\tst::groupCallMembersTopSkip));\n\t\tresult->paintRequest(\n\t\t) | rpl::on_next([=](QRect clip) {\n\t\t\tQPainter(result).fillRect(clip, st::groupCallMembersBg);\n\t\t}, result->lifetime());\n\t\treturn result;\n\t};\n\t_topSkip = addSkip();\n\t_list = _layout->add(\n\t\tobject_ptr<ListWidget>(\n\t\t\t_layout.get(),\n\t\t\t_listController.get()));\n\t_bottomSkip = addSkip();\n\n\tusing namespace rpl::mappers;\n\trpl::combine(\n\t\t_list->heightValue() | rpl::map(_1 > 0),\n\t\t_addMemberButton.value() | rpl::map(_1 != nullptr)\n\t) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](bool hasList, bool hasAddMembers) {\n\t\t_topSkip->resize(\n\t\t\t_topSkip->width(),\n\t\t\thasList ? st::groupCallMembersTopSkip : 0);\n\t\t_bottomSkip->resize(\n\t\t\t_bottomSkip->width(),\n\t\t\t(hasList && !hasAddMembers) ? st::groupCallMembersTopSkip : 0);\n\t}, _list->lifetime());\n\n\tconst auto skip = _layout->add(object_ptr<Ui::RpWidget>(_layout.get()));\n\t_mode.value(\n\t) | rpl::on_next([=](PanelMode mode) {\n\t\tskip->resize(skip->width(), (mode == PanelMode::Default)\n\t\t\t? st::groupCallMembersBottomSkip\n\t\t\t: 0);\n\t}, skip->lifetime());\n\n\trpl::combine(\n\t\t_mode.value(),\n\t\t_layout->heightValue()\n\t) | rpl::on_next([=] {\n\t\tresizeToList();\n\t}, _layout->lifetime());\n\n\trpl::combine(\n\t\t_scroll->scrollTopValue(),\n\t\t_scroll->heightValue()\n\t) | rpl::on_next([=](int scrollTop, int scrollHeight) {\n\t\t_layout->setVisibleTopBottom(scrollTop, scrollTop + scrollHeight);\n\t}, _scroll->lifetime());\n}\n\nvoid Members::setupFingerprint() {\n\tif (const auto raw = _fingerprint) {\n\t\tauto badge = SetupFingerprintBadge(\n\t\t\traw->lifetime(),\n\t\t\t_call->emojiHashValue());\n\t\tstd::move(badge.repaints) | rpl::start_to_stream(\n\t\t\t_fingerprintRepaints,\n\t\t\traw->lifetime());\n\t\t_fingerprintState = badge.state;\n\n\t\tSetupFingerprintBadgeWidget(\n\t\t\traw,\n\t\t\t_fingerprintState,\n\t\t\t_fingerprintRepaints.events());\n\t}\n}\n\nvoid Members::trackViewportGeometry() {\n\t_call->videoEndpointLargeValue(\n\t) | rpl::on_next([=](const VideoEndpoint &large) {\n\t\t_viewport->showLarge(large);\n\t}, _viewport->lifetime());\n\n\tconst auto move = [=] {\n\t\tconst auto maxTop = _viewport->fullHeight()\n\t\t\t- _viewport->widget()->height();\n\t\tif (maxTop < 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto scrollTop = _scroll->scrollTop();\n\t\tconst auto shift = std::min(scrollTop, maxTop);\n\t\t_viewport->setScrollTop(shift);\n\t\tif (_viewport->widget()->y() != shift) {\n\t\t\t_viewport->widget()->move(0, shift);\n\t\t}\n\t};\n\tconst auto resize = [=] {\n\t\t_viewport->widget()->resize(\n\t\t\t_layout->width(),\n\t\t\tstd::min(_scroll->height(), _viewport->fullHeight()));\n\t};\n\t_layout->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_viewport->resizeToWidth(width);\n\t\tresize();\n\t}, _viewport->lifetime());\n\n\t_scroll->heightValue(\n\t) | rpl::skip(1) | rpl::on_next(resize, _viewport->lifetime());\n\n\t_scroll->scrollTopValue(\n\t) | rpl::skip(1) | rpl::on_next(move, _viewport->lifetime());\n\n\t_viewport->fullHeightValue(\n\t) | rpl::on_next([=](int viewport) {\n\t\t_videoWrap->resize(_videoWrap->width(), viewport);\n\t\tif (viewport > 0) {\n\t\t\tmove();\n\t\t\tresize();\n\t\t}\n\t}, _viewport->lifetime());\n}\n\nvoid Members::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid Members::resizeToList() {\n\tif (!_list) {\n\t\treturn;\n\t}\n\tconst auto newHeight = (_list->height() > 0)\n\t\t? (_layout->height() + st::lineWidth)\n\t\t: 0;\n\tif (height() == newHeight) {\n\t\tupdateControlsGeometry();\n\t} else {\n\t\tresize(width(), newHeight);\n\t}\n}\n\nvoid Members::updateControlsGeometry() {\n\t_scroll->setGeometry(rect());\n\t_layout->resizeToWidth(width());\n}\n\nvoid Members::setupFakeRoundCorners() {\n\tconst auto size = st::roundRadiusLarge;\n\tconst auto full = 3 * size;\n\tconst auto imagePartSize = size * style::DevicePixelRatio();\n\tconst auto imageSize = full * style::DevicePixelRatio();\n\tconst auto image = std::make_shared<QImage>(\n\t\tQImage(imageSize, imageSize, QImage::Format_ARGB32_Premultiplied));\n\timage->setDevicePixelRatio(style::DevicePixelRatio());\n\n\tconst auto refreshImage = [=] {\n\t\timage->fill(st::groupCallBg->c);\n\t\t{\n\t\t\tQPainter p(image.get());\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(Qt::transparent);\n\t\t\tp.drawRoundedRect(0, 0, full, full, size, size);\n\t\t}\n\t};\n\n\tconst auto create = [&](QPoint imagePartOrigin) {\n\t\tconst auto result = Ui::CreateChild<Ui::RpWidget>(_layout.get());\n\t\tresult->show();\n\t\tresult->resize(size, size);\n\t\tresult->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tresult->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tQPainter(result).drawImage(\n\t\t\t\tresult->rect(),\n\t\t\t\t*image,\n\t\t\t\tQRect(imagePartOrigin, QSize(imagePartSize, imagePartSize)));\n\t\t}, result->lifetime());\n\t\tresult->raise();\n\t\treturn result;\n\t};\n\tconst auto shift = imageSize - imagePartSize;\n\tconst auto topleft = create({ 0, 0 });\n\tconst auto topright = create({ shift, 0 });\n\tconst auto bottomleft = create({ 0, shift });\n\tconst auto bottomright = create({ shift, shift });\n\n\tconst auto heightValue = [=](Ui::RpWidget *widget) {\n\t\ttopleft->raise();\n\t\ttopright->raise();\n\t\tbottomleft->raise();\n\t\tbottomright->raise();\n\t\treturn widget ? widget->heightValue() : rpl::single(0);\n\t};\n\trpl::combine(\n\t\t_list->geometryValue(),\n\t\t_addMemberButton.value() | rpl::map(\n\t\t\theightValue\n\t\t) | rpl::flatten_latest(),\n\t\t_shareLinkButton.value() | rpl::map(\n\t\t\theightValue\n\t\t) | rpl::flatten_latest()\n\t) | rpl::on_next([=](QRect list, int addMembers, int shareLink) {\n\t\tconst auto left = list.x();\n\t\tconst auto top = list.y() - _topSkip->height();\n\t\tconst auto right = left + list.width() - topright->width();\n\t\tconst auto bottom = top\n\t\t\t+ _topSkip->height()\n\t\t\t+ list.height()\n\t\t\t+ _bottomSkip->height()\n\t\t\t+ addMembers\n\t\t\t+ shareLink\n\t\t\t- bottomleft->height();\n\t\ttopleft->move(left, top);\n\t\ttopright->move(right, top);\n\t\tbottomleft->move(left, bottom);\n\t\tbottomright->move(right, bottom);\n\t}, lifetime());\n\n\trefreshImage();\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\trefreshImage();\n\t\ttopleft->update();\n\t\ttopright->update();\n\t\tbottomleft->update();\n\t\tbottomright->update();\n\t}, lifetime());\n}\n\nvoid Members::peerListSetTitle(rpl::producer<QString> title) {\n}\n\nvoid Members::peerListSetAdditionalTitle(rpl::producer<QString> title) {\n}\n\nvoid Members::peerListSetHideEmpty(bool hide) {\n}\n\nbool Members::peerListIsRowChecked(not_null<PeerListRow*> row) {\n\treturn false;\n}\n\nvoid Members::peerListScrollToTop() {\n}\n\nint Members::peerListSelectedRowsCount() {\n\treturn 0;\n}\n\nvoid Members::peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) {\n\tUnexpected(\"Item selection in Calls::Members.\");\n}\n\nvoid Members::peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) {\n\tUnexpected(\"Item selection in Calls::Members.\");\n}\n\nvoid Members::peerListFinishSelectedRowsBunch() {\n}\n\nvoid Members::peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) {\n\tdescription.destroy();\n}\n\nstd::shared_ptr<Main::SessionShow> Members::peerListUiShow() {\n\tUnexpected(\"...Members::peerListUiShow\");\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_members.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peer_list_box.h\"\n\nnamespace Ui {\nclass RpWidget;\nclass ScrollArea;\nclass VerticalLayout;\nclass SettingsButton;\nnamespace GL {\nenum class Backend;\n} // namespace GL\n} // namespace Ui\n\nnamespace Data {\nclass GroupCall;\n} // namespace Data\n\nnamespace Calls {\nclass GroupCall;\nstruct FingerprintBadgeState;\n} // namespace Calls\n\nnamespace Calls::Group {\n\nclass Viewport;\nclass MembersRow;\nstruct VolumeRequest;\nstruct MuteRequest;\nenum class PanelMode;\n\nclass Members final\n\t: public Ui::RpWidget\n\t, private PeerListContentDelegate {\npublic:\n\tMembers(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<GroupCall*> call,\n\t\tPanelMode mode,\n\t\tUi::GL::Backend backend);\n\t~Members();\n\n\t[[nodiscard]] not_null<Viewport*> viewport() const;\n\t[[nodiscard]] int desiredHeight() const;\n\t[[nodiscard]] rpl::producer<int> desiredHeightValue() const override;\n\t[[nodiscard]] rpl::producer<int> fullCountValue() const;\n\t[[nodiscard]] auto toggleMuteRequests() const\n\t\t-> rpl::producer<Group::MuteRequest>;\n\t[[nodiscard]] auto changeVolumeRequests() const\n\t\t-> rpl::producer<Group::VolumeRequest>;\n\t[[nodiscard]] auto kickParticipantRequests() const\n\t\t-> rpl::producer<not_null<PeerData*>>;\n\t[[nodiscard]] rpl::producer<> addMembersRequests() const {\n\t\treturn _addMemberRequests.events();\n\t}\n\t[[nodiscard]] rpl::producer<> shareLinkRequests() const {\n\t\treturn _shareLinkRequests.events();\n\t}\n\n\t[[nodiscard]] MembersRow *lookupRow(not_null<PeerData*> peer) const;\n\t[[nodiscard]] not_null<MembersRow*> rtmpFakeRow(\n\t\tnot_null<PeerData*> peer) const;\n\n\tvoid setMode(PanelMode mode);\n\t[[nodiscard]] QRect getInnerGeometry() const;\n\nprivate:\n\tclass Controller;\n\tstruct VideoTile;\n\tusing ListWidget = PeerListContent;\n\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\t// PeerListContentDelegate interface.\n\tvoid peerListSetTitle(rpl::producer<QString> title) override;\n\tvoid peerListSetAdditionalTitle(rpl::producer<QString> title) override;\n\tvoid peerListSetHideEmpty(bool hide) override;\n\tbool peerListIsRowChecked(not_null<PeerListRow*> row) override;\n\tint peerListSelectedRowsCount() override;\n\tvoid peerListScrollToTop() override;\n\tvoid peerListAddSelectedPeerInBunch(\n\t\tnot_null<PeerData*> peer) override;\n\tvoid peerListAddSelectedRowInBunch(\n\t\tnot_null<PeerListRow*> row) override;\n\tvoid peerListFinishSelectedRowsBunch() override;\n\tvoid peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) override;\n\tstd::shared_ptr<Main::SessionShow> peerListUiShow() override;\n\n\tvoid setupAddMember(not_null<GroupCall*> call);\n\tvoid resizeToList();\n\tvoid setupList();\n\tvoid setupFingerprint();\n\tvoid setupFakeRoundCorners();\n\n\tvoid trackViewportGeometry();\n\tvoid updateControlsGeometry();\n\n\tconst not_null<GroupCall*> _call;\n\trpl::variable<PanelMode> _mode = PanelMode();\n\tobject_ptr<Ui::ScrollArea> _scroll;\n\tstd::unique_ptr<Controller> _listController;\n\tnot_null<Ui::VerticalLayout*> _layout;\n\tUi::RpWidget *_fingerprint = nullptr;\n\trpl::event_stream<> _fingerprintRepaints;\n\tconst FingerprintBadgeState *_fingerprintState = nullptr;\n\tconst not_null<Ui::RpWidget*> _videoWrap;\n\tstd::unique_ptr<Viewport> _viewport;\n\trpl::variable<Ui::RpWidget*> _addMemberButton = nullptr;\n\trpl::variable<Ui::RpWidget*> _shareLinkButton = nullptr;\n\tRpWidget *_topSkip = nullptr;\n\tRpWidget *_bottomSkip = nullptr;\n\tListWidget *_list = nullptr;\n\trpl::event_stream<> _addMemberRequests;\n\trpl::event_stream<> _shareLinkRequests;\n\n\tmutable std::unique_ptr<MembersRow> _rtmpFakeRow;\n\n\trpl::variable<bool> _canInviteByLink;\n\trpl::variable<bool> _canAddMembers;\n\n};\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_members_row.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_members_row.h\"\n\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_common.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_group_call.h\"\n#include \"ui/paint/arcs.h\"\n#include \"ui/paint/blobs.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"lang/lang_keys.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"styles/style_calls.h\"\n\nnamespace Calls::Group {\nnamespace {\n\nconstexpr auto kLevelDuration = 100. + 500. * 0.23;\nconstexpr auto kBlobScale = 0.605;\nconstexpr auto kMinorBlobFactor = 0.9f;\nconstexpr auto kUserpicMinScale = 0.8;\nconstexpr auto kMaxLevel = 1.;\nconstexpr auto kWideScale = 5;\n\nconstexpr auto kArcsStrokeRatio = 0.8;\n\nconst auto kSpeakerThreshold = std::vector<float>{\n\tGroup::kDefaultVolume * 0.1f / Group::kMaxVolume,\n\tGroup::kDefaultVolume * 0.9f / Group::kMaxVolume };\n\nauto RowBlobs() -> std::array<Ui::Paint::Blobs::BlobData, 2> {\n\treturn { {\n\t\t{\n\t\t\t.segmentsCount = 6,\n\t\t\t.minScale = kBlobScale * kMinorBlobFactor,\n\t\t\t.minRadius = st::groupCallRowBlobMinRadius * kMinorBlobFactor,\n\t\t\t.maxRadius = st::groupCallRowBlobMaxRadius * kMinorBlobFactor,\n\t\t\t.speedScale = 1.,\n\t\t\t.alpha = .5,\n\t\t},\n\t\t{\n\t\t\t.segmentsCount = 8,\n\t\t\t.minScale = kBlobScale,\n\t\t\t.minRadius = (float)st::groupCallRowBlobMinRadius,\n\t\t\t.maxRadius = (float)st::groupCallRowBlobMaxRadius,\n\t\t\t.speedScale = 1.,\n\t\t\t.alpha = .2,\n\t\t},\n\t} };\n}\n\n[[nodiscard]] QString StatusPercentString(float volume) {\n\treturn QString::number(int(base::SafeRound(volume * 200))) + '%';\n}\n\n[[nodiscard]] int StatusPercentWidth(const QString &percent) {\n\treturn st::normalFont->width(percent);\n}\n\n} // namespace\n\nstruct MembersRow::BlobsAnimation {\n\tBlobsAnimation(\n\t\tstd::vector<Ui::Paint::Blobs::BlobData> blobDatas,\n\t\tfloat levelDuration,\n\t\tfloat maxLevel);\n\n\tUi::Paint::Blobs blobs;\n\tcrl::time lastTime = 0;\n\tcrl::time lastSoundingUpdateTime = 0;\n\tfloat64 enter = 0.;\n\n\tQImage userpicCache;\n\tInMemoryKey userpicKey;\n\n\trpl::lifetime lifetime;\n};\n\nstruct MembersRow::StatusIcon {\n\tStatusIcon(bool shown, float volume);\n\n\tconst style::icon &speaker;\n\tUi::Paint::ArcsAnimation arcs;\n\tUi::Animations::Simple arcsAnimation;\n\tUi::Animations::Simple shownAnimation;\n\tQString percent;\n\tint percentWidth = 0;\n\tint arcsWidth = 0;\n\tint wasArcsWidth = 0;\n\tbool shown = true;\n\n\trpl::lifetime lifetime;\n};\n\nMembersRow::BlobsAnimation::BlobsAnimation(\n\tstd::vector<Ui::Paint::Blobs::BlobData> blobDatas,\n\tfloat levelDuration,\n\tfloat maxLevel)\n: blobs(std::move(blobDatas), levelDuration, maxLevel) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tuserpicCache = QImage();\n\t}, lifetime);\n}\n\nMembersRow::StatusIcon::StatusIcon(bool shown, float volume)\n: speaker(st::groupCallStatusSpeakerIcon)\n, arcs(\n\tst::groupCallStatusSpeakerArcsAnimation,\n\tkSpeakerThreshold,\n\tvolume,\n\tUi::Paint::ArcsAnimation::Direction::Right)\n, percent(StatusPercentString(volume))\n, percentWidth(StatusPercentWidth(percent))\n, shown(shown) {\n}\n\nMembersRow::MembersRow(\n\tnot_null<MembersRowDelegate*> delegate,\n\tnot_null<PeerData*> participantPeer)\n: PeerListRow(participantPeer)\n, _delegate(delegate) {\n\trefreshStatus();\n\t_about.setText(st::defaultTextStyle, participantPeer->about());\n}\n\nMembersRow::~MembersRow() = default;\n\nvoid MembersRow::setSkipLevelUpdate(bool value) {\n\t_skipLevelUpdate = value;\n}\n\nvoid MembersRow::updateStateInvited(bool calling) {\n\tsetVolume(Group::kDefaultVolume);\n\tsetState(calling ? State::Calling : State::Invited);\n\tsetSounding(false);\n\tsetSpeaking(false);\n\t_mutedByMe = false;\n\t_raisedHandRating = 0;\n\trefreshStatus();\n}\n\nvoid MembersRow::updateStateWithAccess() {\n\tsetVolume(Group::kDefaultVolume);\n\tsetState(State::WithAccess);\n\tsetSounding(false);\n\tsetSpeaking(false);\n\t_mutedByMe = false;\n\t_raisedHandRating = 0;\n\trefreshStatus();\n}\n\nvoid MembersRow::updateState(const Data::GroupCallParticipant &participant) {\n\tsetVolume(participant.volume);\n\tif (!participant.muted\n\t\t|| (participant.sounding && participant.ssrc != 0)\n\t\t|| (participant.additionalSounding\n\t\t\t&& GetAdditionalAudioSsrc(participant.videoParams) != 0)) {\n\t\tsetState(State::Active);\n\t\tsetSounding((participant.sounding && participant.ssrc != 0)\n\t\t\t|| (participant.additionalSounding\n\t\t\t\t&& GetAdditionalAudioSsrc(participant.videoParams) != 0));\n\t\tsetSpeaking((participant.speaking && participant.ssrc != 0)\n\t\t\t|| (participant.additionalSpeaking\n\t\t\t\t&& GetAdditionalAudioSsrc(participant.videoParams) != 0));\n\t\t_mutedByMe = participant.mutedByMe;\n\t\t_raisedHandRating = 0;\n\t} else if (participant.canSelfUnmute) {\n\t\tsetState(State::Inactive);\n\t\tsetSounding(false);\n\t\tsetSpeaking(false);\n\t\t_mutedByMe = participant.mutedByMe;\n\t\t_raisedHandRating = 0;\n\t} else {\n\t\tsetSounding(false);\n\t\tsetSpeaking(false);\n\t\t_mutedByMe = participant.mutedByMe;\n\t\t_raisedHandRating = participant.raisedHandRating;\n\t\tsetState(_raisedHandRating ? State::RaisedHand : State::Muted);\n\t}\n\trefreshStatus();\n}\n\nvoid MembersRow::setSpeaking(bool speaking) {\n\tif (_speaking == speaking) {\n\t\treturn;\n\t}\n\t_speaking = speaking;\n\t_speakingAnimation.start(\n\t\t[=] { _delegate->rowUpdateRow(this); },\n\t\t_speaking ? 0. : 1.,\n\t\t_speaking ? 1. : 0.,\n\t\tst::widgetFadeDuration);\n\n\tif (!_speaking\n\t\t|| _mutedByMe\n\t\t|| (_state == State::Muted)\n\t\t|| (_state == State::RaisedHand)) {\n\t\tif (_statusIcon) {\n\t\t\t_statusIcon = nullptr;\n\t\t\t_delegate->rowUpdateRow(this);\n\t\t}\n\t} else if (!_statusIcon) {\n\t\t_statusIcon = std::make_unique<StatusIcon>(\n\t\t\t(_volume != Group::kDefaultVolume),\n\t\t\t(float)_volume / Group::kMaxVolume);\n\t\t_statusIcon->arcs.setStrokeRatio(kArcsStrokeRatio);\n\t\t_statusIcon->arcsWidth = _statusIcon->arcs.finishedWidth();\n\t\t_statusIcon->arcs.startUpdateRequests(\n\t\t) | rpl::on_next([=] {\n\t\t\tif (!_statusIcon->arcsAnimation.animating()) {\n\t\t\t\t_statusIcon->wasArcsWidth = _statusIcon->arcsWidth;\n\t\t\t}\n\t\t\tauto callback = [=](float64 value) {\n\t\t\t\t_statusIcon->arcs.update(crl::now());\n\t\t\t\t_statusIcon->arcsWidth = anim::interpolate(\n\t\t\t\t\t_statusIcon->wasArcsWidth,\n\t\t\t\t\t_statusIcon->arcs.finishedWidth(),\n\t\t\t\t\tvalue);\n\t\t\t\t_delegate->rowUpdateRow(this);\n\t\t\t};\n\t\t\t_statusIcon->arcsAnimation.start(\n\t\t\t\tstd::move(callback),\n\t\t\t\t0.,\n\t\t\t\t1.,\n\t\t\t\tst::groupCallSpeakerArcsAnimation.duration);\n\t\t}, _statusIcon->lifetime);\n\t}\n}\n\nvoid MembersRow::setSounding(bool sounding) {\n\tif (_sounding == sounding) {\n\t\treturn;\n\t}\n\t_sounding = sounding;\n\tif (!_sounding) {\n\t\t_blobsAnimation = nullptr;\n\t} else if (!_blobsAnimation) {\n\t\t_blobsAnimation = std::make_unique<BlobsAnimation>(\n\t\t\tRowBlobs() | ranges::to_vector,\n\t\t\tkLevelDuration,\n\t\t\tkMaxLevel);\n\t\t_blobsAnimation->lastTime = crl::now();\n\t\tupdateLevel(GroupCall::kSpeakLevelThreshold);\n\t}\n}\n\nvoid MembersRow::clearRaisedHandStatus() {\n\tif (!_raisedHandStatus) {\n\t\treturn;\n\t}\n\t_raisedHandStatus = false;\n\trefreshStatus();\n\t_delegate->rowUpdateRow(this);\n}\n\nvoid MembersRow::setState(State state) {\n\tif (_state == state) {\n\t\treturn;\n\t}\n\tconst auto wasActive = (_state == State::Active);\n\tconst auto wasMuted = (_state == State::Muted)\n\t\t|| (_state == State::RaisedHand);\n\tconst auto wasRaisedHand = (_state == State::RaisedHand);\n\t_state = state;\n\tconst auto nowActive = (_state == State::Active);\n\tconst auto nowMuted = (_state == State::Muted)\n\t\t|| (_state == State::RaisedHand);\n\tconst auto nowRaisedHand = (_state == State::RaisedHand);\n\tif (!wasRaisedHand && nowRaisedHand) {\n\t\t_raisedHandStatus = true;\n\t\t_delegate->rowScheduleRaisedHandStatusRemove(this);\n\t}\n\tif (nowActive != wasActive) {\n\t\t_activeAnimation.start(\n\t\t\t[=] { _delegate->rowUpdateRow(this); },\n\t\t\tnowActive ? 0. : 1.,\n\t\t\tnowActive ? 1. : 0.,\n\t\t\tst::widgetFadeDuration);\n\t}\n\tif (nowMuted != wasMuted) {\n\t\t_mutedAnimation.start(\n\t\t\t[=] { _delegate->rowUpdateRow(this); },\n\t\t\tnowMuted ? 0. : 1.,\n\t\t\tnowMuted ? 1. : 0.,\n\t\t\tst::widgetFadeDuration);\n\t}\n}\n\nvoid MembersRow::setVolume(int volume) {\n\t_volume = volume;\n\tif (_statusIcon) {\n\t\tconst auto floatVolume = (float)volume / Group::kMaxVolume;\n\t\t_statusIcon->arcs.setValue(floatVolume);\n\t\t_statusIcon->percent = StatusPercentString(floatVolume);\n\t\t_statusIcon->percentWidth = StatusPercentWidth(_statusIcon->percent);\n\n\t\tconst auto shown = (volume != Group::kDefaultVolume);\n\t\tif (_statusIcon->shown != shown) {\n\t\t\t_statusIcon->shown = shown;\n\t\t\t_statusIcon->shownAnimation.start(\n\t\t\t\t[=] { _delegate->rowUpdateRow(this); },\n\t\t\t\tshown ? 0. : 1.,\n\t\t\t\tshown ? 1. : 0.,\n\t\t\t\tst::groupCallSpeakerArcsAnimation.duration);\n\t\t}\n\t}\n}\n\nvoid MembersRow::updateLevel(float level) {\n\tExpects(_blobsAnimation != nullptr);\n\n\tconst auto spoke = (level >= GroupCall::kSpeakLevelThreshold)\n\t\t? crl::now()\n\t\t: crl::time();\n\tif (spoke && _speaking) {\n\t\t_speakingLastTime = spoke;\n\t}\n\n\tif (_skipLevelUpdate) {\n\t\treturn;\n\t}\n\n\tif (spoke) {\n\t\t_blobsAnimation->lastSoundingUpdateTime = spoke;\n\t}\n\t_blobsAnimation->blobs.setLevel(level);\n}\n\nvoid MembersRow::updateBlobAnimation(crl::time now) {\n\tExpects(_blobsAnimation != nullptr);\n\n\tconst auto soundingFinishesAt = _blobsAnimation->lastSoundingUpdateTime\n\t\t+ Data::GroupCall::kSoundStatusKeptFor;\n\tconst auto soundingStartsFinishing = soundingFinishesAt\n\t\t- kBlobsEnterDuration;\n\tconst auto soundingFinishes = (soundingStartsFinishing < now);\n\tif (soundingFinishes) {\n\t\t_blobsAnimation->enter = std::clamp(\n\t\t\t(soundingFinishesAt - now) / float64(kBlobsEnterDuration),\n\t\t\t0.,\n\t\t\t1.);\n\t} else if (_blobsAnimation->enter < 1.) {\n\t\t_blobsAnimation->enter = std::clamp(\n\t\t\t(_blobsAnimation->enter\n\t\t\t\t+ ((now - _blobsAnimation->lastTime)\n\t\t\t\t\t/ float64(kBlobsEnterDuration))),\n\t\t\t0.,\n\t\t\t1.);\n\t}\n\t_blobsAnimation->blobs.updateLevel(now - _blobsAnimation->lastTime);\n\t_blobsAnimation->lastTime = now;\n}\n\nvoid MembersRow::ensureUserpicCache(\n\t\tUi::PeerUserpicView &view,\n\t\tint size) {\n\tExpects(_blobsAnimation != nullptr);\n\n\tconst auto user = peer();\n\tconst auto key = user->userpicUniqueKey(view);\n\tconst auto full = QSize(size, size)\n\t\t* kWideScale\n\t\t* style::DevicePixelRatio();\n\tauto &cache = _blobsAnimation->userpicCache;\n\tif (cache.isNull()) {\n\t\tcache = QImage(full, QImage::Format_ARGB32_Premultiplied);\n\t\tcache.setDevicePixelRatio(style::DevicePixelRatio());\n\t} else if (_blobsAnimation->userpicKey == key\n\t\t&& cache.size() == full) {\n\t\treturn;\n\t}\n\t_blobsAnimation->userpicKey = key;\n\tcache.fill(Qt::transparent);\n\t{\n\t\tPainter p(&cache);\n\t\tconst auto skip = (kWideScale - 1) / 2 * size;\n\t\tuser->paintUserpicLeft(p, view, skip, skip, kWideScale * size, size);\n\t}\n}\n\nvoid MembersRow::paintBlobs(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint sizew,\n\t\tint sizeh,\n\t\tPanelMode mode) {\n\tif (!_blobsAnimation) {\n\t\treturn;\n\t}\n\tauto size = sizew;\n\tconst auto shift = QPointF(x + size / 2., y + size / 2.);\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.translate(shift);\n\tconst auto brush = _mutedByMe\n\t\t? st::groupCallMemberMutedIcon->b\n\t\t: anim::brush(\n\t\t\tst::groupCallMemberInactiveStatus,\n\t\t\tst::groupCallMemberActiveStatus,\n\t\t\t_speakingAnimation.value(_speaking ? 1. : 0.));\n\t_blobsAnimation->blobs.paint(p, brush);\n\tp.translate(-shift);\n\tp.setOpacity(1.);\n}\n\nvoid MembersRow::paintScaledUserpic(\n\t\tPainter &p,\n\t\tUi::PeerUserpicView &userpic,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint sizew,\n\t\tint sizeh,\n\t\tPanelMode mode) {\n\tauto size = sizew;\n\tif (!_blobsAnimation) {\n\t\tpeer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size);\n\t\treturn;\n\t}\n\tconst auto enter = _blobsAnimation->enter;\n\tconst auto &minScale = kUserpicMinScale;\n\tconst auto scaleUserpic = minScale\n\t\t+ (1. - minScale) * _blobsAnimation->blobs.currentLevel();\n\tconst auto scale = scaleUserpic * enter + 1. * (1. - enter);\n\tif (scale == 1.) {\n\t\tpeer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size);\n\t\treturn;\n\t}\n\tensureUserpicCache(userpic, size);\n\n\tPainterHighQualityEnabler hq(p);\n\n\tauto target = QRect(\n\t\tx + (1 - kWideScale) / 2 * size,\n\t\ty + (1 - kWideScale) / 2 * size,\n\t\tkWideScale * size,\n\t\tkWideScale * size);\n\tauto shrink = anim::interpolate(\n\t\t(1 - kWideScale) / 2 * size,\n\t\t0,\n\t\tscale);\n\tauto margins = QMargins(shrink, shrink, shrink, shrink);\n\tp.drawImage(\n\t\ttarget.marginsAdded(margins),\n\t\t_blobsAnimation->userpicCache);\n}\n\nvoid MembersRow::paintMuteIcon(\n\t\tQPainter &p,\n\t\tQRect iconRect,\n\t\tMembersRowStyle style) {\n\t_delegate->rowPaintIcon(p, iconRect, computeIconState(style));\n}\n\nQString MembersRow::generateName() {\n\tconst auto result = peer()->name();\n\tif (result.isEmpty()) {\n\t\tDEBUG_LOG((\"UnknownParticipant: %1, Loaded: %2, Name Version: %3\"\n\t\t\t).arg(peerToUser(peer()->id).bare\n\t\t\t).arg(peer()->isLoaded() ? \"TRUE\" : \"FALSE\"\n\t\t\t).arg(peer()->nameVersion()));\n\t}\n\treturn result.isEmpty()\n\t\t? u\"User #%1\"_q.arg(peerToUser(peer()->id).bare)\n\t\t: result;\n}\n\nQString MembersRow::generateShortName() {\n\tconst auto result = peer()->shortName();\n\treturn result.isEmpty()\n\t\t? u\"User #%1\"_q.arg(peerToUser(peer()->id).bare)\n\t\t: result;\n}\n\nauto MembersRow::generatePaintUserpicCallback(bool forceRound)\n-> PaintRoundImageCallback {\n\treturn [=](Painter &p, int x, int y, int outerWidth, int size) {\n\t\tconst auto outer = outerWidth;\n\t\tpaintComplexUserpic(p, x, y, outer, size, size, PanelMode::Default);\n\t};\n}\n\nvoid MembersRow::paintComplexUserpic(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint sizew,\n\t\tint sizeh,\n\t\tPanelMode mode,\n\t\tbool selected) {\n\tpaintBlobs(p, x, y, sizew, sizeh, mode);\n\tpaintScaledUserpic(\n\t\tp,\n\t\tensureUserpicView(),\n\t\tx,\n\t\ty,\n\t\touterWidth,\n\t\tsizew,\n\t\tsizeh,\n\t\tmode);\n}\n\nint MembersRow::statusIconWidth(bool skipIcon) const {\n\tif (!_statusIcon || !_speaking) {\n\t\treturn 0;\n\t}\n\tconst auto shown = _statusIcon->shownAnimation.value(\n\t\t_statusIcon->shown ? 1. : 0.);\n\tconst auto iconWidth = skipIcon\n\t\t? 0\n\t\t: (_statusIcon->speaker.width() + _statusIcon->arcsWidth);\n\tconst auto full = iconWidth\n\t\t+ _statusIcon->percentWidth\n\t\t+ st::normalFont->spacew;\n\treturn int(base::SafeRound(shown * full));\n}\n\nint MembersRow::statusIconHeight() const {\n\treturn (_statusIcon && _speaking) ? _statusIcon->speaker.height() : 0;\n}\n\nvoid MembersRow::paintStatusIcon(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tconst style::PeerListItem &st,\n\t\tconst style::font &font,\n\t\tbool selected,\n\t\tbool skipIcon) {\n\tif (!_statusIcon) {\n\t\treturn;\n\t}\n\tconst auto shown = _statusIcon->shownAnimation.value(\n\t\t_statusIcon->shown ? 1. : 0.);\n\tif (shown == 0.) {\n\t\treturn;\n\t}\n\n\tp.setFont(font);\n\tconst auto color = (_speaking\n\t\t? st.statusFgActive\n\t\t: (selected ? st.statusFgOver : st.statusFg))->c;\n\tp.setPen(color);\n\n\tconst auto speakerRect = QRect(\n\t\tQPoint(x, y + (font->height - statusIconHeight()) / 2),\n\t\t_statusIcon->speaker.size());\n\tconst auto arcPosition = speakerRect.topLeft()\n\t\t+ QPoint(\n\t\t\tspeakerRect.width() - st::groupCallStatusSpeakerArcsSkip,\n\t\t\tspeakerRect.height() / 2);\n\tconst auto iconWidth = skipIcon\n\t\t? 0\n\t\t: (speakerRect.width() + _statusIcon->arcsWidth);\n\tconst auto fullWidth = iconWidth\n\t\t+ _statusIcon->percentWidth\n\t\t+ st::normalFont->spacew;\n\n\tp.save();\n\tif (shown < 1.) {\n\t\tconst auto centerx = speakerRect.x() + fullWidth / 2;\n\t\tconst auto centery = speakerRect.y() + speakerRect.height() / 2;\n\t\tp.translate(centerx, centery);\n\t\tp.scale(shown, shown);\n\t\tp.translate(-centerx, -centery);\n\t}\n\tif (!skipIcon) {\n\t\t_statusIcon->speaker.paint(\n\t\t\tp,\n\t\t\tspeakerRect.topLeft(),\n\t\t\tspeakerRect.width(),\n\t\t\tcolor);\n\t\tp.translate(arcPosition);\n\t\t_statusIcon->arcs.paint(p, color);\n\t\tp.translate(-arcPosition);\n\t}\n\tp.setFont(st::normalFont);\n\tp.setPen(st.statusFgActive);\n\tp.drawTextLeft(\n\t\tx + iconWidth,\n\t\ty,\n\t\tfullWidth,\n\t\t_statusIcon->percent);\n\tp.restore();\n}\n\nvoid MembersRow::setAbout(const QString &about) {\n\tif (_about.toString() == about) {\n\t\treturn;\n\t}\n\t_about.setText(st::defaultTextStyle, about);\n\t_delegate->rowUpdateRow(this);\n}\n\nvoid MembersRow::paintStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) {\n\tpaintComplexStatusText(\n\t\tp,\n\t\tst,\n\t\tx,\n\t\ty,\n\t\tavailableWidth,\n\t\touterWidth,\n\t\tselected,\n\t\tMembersRowStyle::Default);\n}\n\nvoid MembersRow::paintComplexStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tMembersRowStyle style) {\n\tconst auto skip = (style == MembersRowStyle::Default)\n\t\t? _delegate->rowPaintStatusIcon(\n\t\t\tp,\n\t\t\tx,\n\t\t\ty,\n\t\t\touterWidth,\n\t\t\tthis,\n\t\t\tcomputeIconState(MembersRowStyle::Narrow))\n\t\t: 0;\n\tconst auto narrowMode = (skip > 0);\n\tx += skip;\n\tavailableWidth -= skip;\n\tconst auto &font = st::normalFont;\n\tconst auto useAbout = !_about.isEmpty()\n\t\t&& (_state != State::WithAccess)\n\t\t&& (_state != State::Invited)\n\t\t&& (_state != State::Calling)\n\t\t&& (style != MembersRowStyle::Video)\n\t\t&& ((_state == State::RaisedHand && !_raisedHandStatus)\n\t\t\t|| (_state != State::RaisedHand && !_speaking));\n\tif (!useAbout\n\t\t&& _state != State::Invited\n\t\t&& _state != State::Calling\n\t\t&& _state != State::WithAccess\n\t\t&& !_mutedByMe) {\n\t\tpaintStatusIcon(p, x, y, st, font, selected, narrowMode);\n\n\t\tconst auto translatedWidth = statusIconWidth(narrowMode);\n\t\tp.translate(translatedWidth, 0);\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\tp.translate(-translatedWidth, 0);\n\t\t});\n\n\t\tconst auto &style = (!narrowMode\n\t\t\t|| (_state == State::RaisedHand && _raisedHandStatus))\n\t\t\t? st\n\t\t\t: st::groupCallNarrowMembersListItem;\n\t\tPeerListRow::paintStatusText(\n\t\t\tp,\n\t\t\tstyle,\n\t\t\tx,\n\t\t\ty,\n\t\t\tavailableWidth - translatedWidth,\n\t\t\touterWidth,\n\t\t\tselected);\n\t\treturn;\n\t}\n\tp.setPen((style == MembersRowStyle::Video)\n\t\t? st::groupCallVideoSubTextFg\n\t\t: _mutedByMe\n\t\t? st::groupCallMemberMutedIcon\n\t\t: st::groupCallMemberNotJoinedStatus);\n\tif (!_mutedByMe && useAbout) {\n\t\treturn _about.draw(p, {\n\t\t\t.position = QPoint(x, y),\n\t\t\t.outerWidth = outerWidth,\n\t\t\t.availableWidth = availableWidth,\n\t\t\t.elisionLines = 1,\n\t\t});\n\t} else {\n\t\tp.setFont(font);\n\t\tp.drawTextLeft(\n\t\t\tx,\n\t\t\ty,\n\t\t\touterWidth,\n\t\t\t(_mutedByMe\n\t\t\t\t? tr::lng_group_call_muted_by_me_status(tr::now)\n\t\t\t\t: _delegate->rowIsMe(peer())\n\t\t\t\t? tr::lng_status_connecting(tr::now)\n\t\t\t\t: (_state == State::WithAccess)\n\t\t\t\t? tr::lng_group_call_blockchain_only_status(tr::now)\n\t\t\t\t: (_state == State::Calling)\n\t\t\t\t? tr::lng_group_call_calling_status(tr::now)\n\t\t\t\t: tr::lng_group_call_invited_status(tr::now)));\n\t}\n}\n\nQSize MembersRow::rightActionSize() const {\n\treturn _delegate->rowIsNarrow() ? QSize() : QSize(\n\t\tst::groupCallActiveButton.width,\n\t\tst::groupCallActiveButton.height);\n}\n\nbool MembersRow::rightActionDisabled() const {\n\treturn _delegate->rowIsMe(peer())\n\t\t|| (_state == State::Invited)\n\t\t|| (_state == State::Calling)\n\t\t|| !_delegate->rowCanMuteMembers();\n}\n\nQMargins MembersRow::rightActionMargins() const {\n\treturn QMargins(\n\t\t0,\n\t\t0,\n\t\tst::groupCallMemberButtonSkip,\n\t\t0);\n}\n\nvoid MembersRow::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\tauto size = rightActionSize();\n\tconst auto iconRect = style::rtlrect(\n\t\tx,\n\t\ty,\n\t\tsize.width(),\n\t\tsize.height(),\n\t\touterWidth);\n\tif (_state == State::Invited\n\t\t|| _state == State::Calling\n\t\t|| _state == State::WithAccess) {\n\t\t_actionRipple = nullptr;\n\t}\n\tif (_actionRipple) {\n\t\t_actionRipple->paint(\n\t\t\tp,\n\t\t\tx + st::groupCallActiveButton.rippleAreaPosition.x(),\n\t\t\ty + st::groupCallActiveButton.rippleAreaPosition.y(),\n\t\t\touterWidth);\n\t\tif (_actionRipple->empty()) {\n\t\t\t_actionRipple.reset();\n\t\t}\n\t}\n\tpaintMuteIcon(p, iconRect);\n}\n\nMembersRowDelegate::IconState MembersRow::computeIconState(\n\t\tMembersRowStyle style) const {\n\tconst auto speaking = _speakingAnimation.value(_speaking ? 1. : 0.);\n\tconst auto active = _activeAnimation.value(\n\t\t(_state == State::Active) ? 1. : 0.);\n\tconst auto muted = _mutedAnimation.value(\n\t\t(_state == State::Muted || _state == State::RaisedHand) ? 1. : 0.);\n\treturn {\n\t\t.speaking = speaking,\n\t\t.active = active,\n\t\t.muted = muted,\n\t\t.mutedByMe = _mutedByMe,\n\t\t.raisedHand = (_state == State::RaisedHand),\n\t\t.invited = (_state == State::Invited),\n\t\t.calling = (_state == State::Calling),\n\t\t.style = style,\n\t};\n}\n\nvoid MembersRow::showContextMenu() {\n\treturn _delegate->rowShowContextMenu(this);\n}\n\nvoid MembersRow::refreshStatus() {\n\tsetCustomStatus(\n\t\t(_speaking\n\t\t\t? tr::lng_group_call_active(tr::now)\n\t\t\t: _raisedHandStatus\n\t\t\t? tr::lng_group_call_raised_hand_status(tr::now)\n\t\t\t: tr::lng_group_call_inactive(tr::now)),\n\t\t_speaking);\n}\n\nvoid MembersRow::rightActionAddRipple(\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) {\n\tif (!_actionRipple) {\n\t\tauto mask = Ui::RippleAnimation::EllipseMask(QSize(\n\t\t\tst::groupCallActiveButton.rippleAreaSize,\n\t\t\tst::groupCallActiveButton.rippleAreaSize));\n\t\t_actionRipple = std::make_unique<Ui::RippleAnimation>(\n\t\t\tst::groupCallActiveButton.ripple,\n\t\t\tstd::move(mask),\n\t\t\tstd::move(updateCallback));\n\t}\n\t_actionRipple->add(point - st::groupCallActiveButton.rippleAreaPosition);\n}\n\nvoid MembersRow::refreshName(const style::PeerListItem &st) {\n\tPeerListRow::refreshName(st);\n\t//_narrowName = Ui::Text::String();\n}\n\nvoid MembersRow::rightActionStopLastRipple() {\n\tif (_actionRipple) {\n\t\t_actionRipple->lastStop();\n\t}\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_members_row.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peer_list_box.h\"\n#include \"calls/group/calls_group_common.h\"\n\nclass PeerData;\nclass Painter;\n\nnamespace Data {\nstruct GroupCallParticipant;\n} // namespace Data\n\nnamespace Ui {\nclass RippleAnimation;\nstruct PeerUserpicView;\n} // namespace Ui\n\nnamespace Calls::Group {\n\nenum class MembersRowStyle : uchar {\n\tDefault,\n\tNarrow,\n\tVideo,\n};\n\nclass MembersRow;\nclass MembersRowDelegate {\npublic:\n\tstruct IconState {\n\t\tfloat64 speaking = 0.;\n\t\tfloat64 active = 0.;\n\t\tfloat64 muted = 0.;\n\t\tbool mutedByMe = false;\n\t\tbool raisedHand = false;\n\t\tbool invited = false;\n\t\tbool calling = false;\n\t\tMembersRowStyle style = MembersRowStyle::Default;\n\t};\n\tvirtual bool rowIsMe(not_null<PeerData*> participantPeer) = 0;\n\tvirtual bool rowCanMuteMembers() = 0;\n\tvirtual void rowUpdateRow(not_null<MembersRow*> row) = 0;\n\tvirtual void rowScheduleRaisedHandStatusRemove(\n\t\tnot_null<MembersRow*> row) = 0;\n\tvirtual void rowPaintIcon(\n\t\tQPainter &p,\n\t\tQRect rect,\n\t\tconst IconState &state) = 0;\n\tvirtual int rowPaintStatusIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tnot_null<MembersRow*> row,\n\t\tconst IconState &state) = 0;\n\tvirtual bool rowIsNarrow() = 0;\n\tvirtual void rowShowContextMenu(not_null<PeerListRow*> row) = 0;\n};\n\nclass MembersRow final : public PeerListRow {\npublic:\n\tMembersRow(\n\t\tnot_null<MembersRowDelegate*> delegate,\n\t\tnot_null<PeerData*> participantPeer);\n\t~MembersRow();\n\n\tenum class State {\n\t\tActive,\n\t\tInactive,\n\t\tMuted,\n\t\tRaisedHand,\n\t\tInvited,\n\t\tCalling,\n\t\tWithAccess,\n\t};\n\n\tvoid setAbout(const QString &about);\n\tvoid setSkipLevelUpdate(bool value);\n\tvoid updateState(const Data::GroupCallParticipant &participant);\n\tvoid updateStateInvited(bool calling);\n\tvoid updateStateWithAccess();\n\tvoid updateLevel(float level);\n\tvoid updateBlobAnimation(crl::time now);\n\tvoid clearRaisedHandStatus();\n\t[[nodiscard]] State state() const {\n\t\treturn _state;\n\t}\n\t[[nodiscard]] bool sounding() const {\n\t\treturn _sounding;\n\t}\n\t[[nodiscard]] bool speaking() const {\n\t\treturn _speaking;\n\t}\n\t[[nodiscard]] bool mutedByMe() const {\n\t\treturn _mutedByMe;\n\t}\n\t[[nodiscard]] crl::time speakingLastTime() const {\n\t\treturn _speakingLastTime;\n\t}\n\t[[nodiscard]] int volume() const {\n\t\treturn _volume;\n\t}\n\t[[nodiscard]] uint64 raisedHandRating() const {\n\t\treturn _raisedHandRating;\n\t}\n\n\tvoid refreshName(const style::PeerListItem &st) override;\n\n\tvoid rightActionAddRipple(\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) override;\n\tvoid rightActionStopLastRipple() override;\n\tQSize rightActionSize() const override;\n\tbool rightActionDisabled() const override;\n\tQMargins rightActionMargins() const override;\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\n\tQString generateName() override;\n\tQString generateShortName() override;\n\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\tvoid paintComplexUserpic(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint sizew,\n\t\tint sizeh,\n\t\tPanelMode mode,\n\t\tbool selected = false);\n\n\tvoid paintStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) override;\n\tvoid paintComplexStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tMembersRowStyle style);\n\tvoid paintMuteIcon(\n\t\tQPainter &p,\n\t\tQRect iconRect,\n\t\tMembersRowStyle style = MembersRowStyle::Default);\n\t[[nodiscard]] MembersRowDelegate::IconState computeIconState(\n\t\tMembersRowStyle style = MembersRowStyle::Default) const;\n\n\tvoid showContextMenu();\n\nprivate:\n\tstruct BlobsAnimation;\n\tstruct StatusIcon;\n\n\tint statusIconWidth(bool skipIcon) const;\n\tint statusIconHeight() const;\n\tvoid paintStatusIcon(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tconst style::PeerListItem &st,\n\t\tconst style::font &font,\n\t\tbool selected,\n\t\tbool skipIcon);\n\n\tvoid refreshStatus() override;\n\tvoid setSounding(bool sounding);\n\tvoid setSpeaking(bool speaking);\n\tvoid setState(State state);\n\tvoid setVolume(int volume);\n\n\tvoid ensureUserpicCache(\n\t\tUi::PeerUserpicView &view,\n\t\tint size);\n\tvoid paintBlobs(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint sizew,\n\t\tint sizeh, PanelMode mode);\n\tvoid paintScaledUserpic(\n\t\tPainter &p,\n\t\tUi::PeerUserpicView &userpic,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint sizew,\n\t\tint sizeh,\n\t\tPanelMode mode);\n\n\tconst not_null<MembersRowDelegate*> _delegate;\n\tState _state = State::Inactive;\n\tstd::unique_ptr<Ui::RippleAnimation> _actionRipple;\n\tstd::unique_ptr<BlobsAnimation> _blobsAnimation;\n\tstd::unique_ptr<StatusIcon> _statusIcon;\n\tUi::Animations::Simple _speakingAnimation; // For gray-red/green icon.\n\tUi::Animations::Simple _mutedAnimation; // For gray/red icon.\n\tUi::Animations::Simple _activeAnimation; // For icon cross animation.\n\tUi::Text::String _about;\n\tcrl::time _speakingLastTime = 0;\n\tuint64 _raisedHandRating = 0;\n\tint _volume = Group::kDefaultVolume;\n\tbool _sounding : 1 = false;\n\tbool _speaking : 1 = false;\n\tbool _raisedHandStatus : 1 = false;\n\tbool _skipLevelUpdate : 1 = false;\n\tbool _mutedByMe : 1 = false;\n\n};\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_menu.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_menu.h\"\n\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_settings.h\"\n#include \"calls/group/calls_group_panel.h\"\n#include \"calls/group/ui/calls_group_recording_box.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_group_call.h\"\n#include \"info/profile/info_profile_values.h\" // Info::Profile::NameValue.\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/widgets/menu/menu.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"lang/lang_keys.h\"\n#include \"base/unixtime.h\"\n#include \"base/timer_rpl.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace Calls::Group {\nnamespace {\n\nclass JoinAsAction final : public Ui::Menu::ItemBase {\npublic:\n\tJoinAsAction(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::Menu &st,\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> callback);\n\n\tbool isEnabled() const override;\n\tnot_null<QAction*> action() const override;\n\n\tvoid handleKeyPress(not_null<QKeyEvent*> e) override;\n\nprotected:\n\tQPoint prepareRippleStartPosition() const override;\n\tQImage prepareRippleMask() const override;\n\n\tint contentHeight() const override;\n\nprivate:\n\tvoid prepare();\n\tvoid paint(Painter &p);\n\n\tconst not_null<QAction*> _dummyAction;\n\tconst style::Menu &_st;\n\tconst not_null<PeerData*> _peer;\n\tUi::PeerUserpicView _userpicView;\n\n\tUi::Text::String _text;\n\tUi::Text::String _name;\n\tint _textWidth = 0;\n\tint _nameWidth = 0;\n\tconst int _height = 0;\n\n};\n\nclass RecordingAction final : public Ui::Menu::ItemBase {\npublic:\n\tRecordingAction(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::Menu &st,\n\t\trpl::producer<QString> text,\n\t\trpl::producer<TimeId> startAtValues,\n\t\tFn<void()> callback);\n\n\tbool isEnabled() const override;\n\tnot_null<QAction*> action() const override;\n\n\tvoid handleKeyPress(not_null<QKeyEvent*> e) override;\n\nprotected:\n\tQPoint prepareRippleStartPosition() const override;\n\tQImage prepareRippleMask() const override;\n\n\tint contentHeight() const override;\n\nprivate:\n\tvoid prepare(rpl::producer<QString> text);\n\tvoid refreshElapsedText();\n\tvoid paint(Painter &p);\n\n\tconst not_null<QAction*> _dummyAction;\n\tconst style::Menu &_st;\n\tTimeId _startAt = 0;\n\tcrl::time _startedAt = 0;\n\tbase::Timer _refreshTimer;\n\n\tUi::Text::String _text;\n\tint _textWidth = 0;\n\tQString _elapsedText;\n\tconst int _smallHeight = 0;\n\tconst int _bigHeight = 0;\n\n};\n\nTextParseOptions MenuTextOptions = {\n\tTextParseLinks, // flags\n\t0, // maxw\n\t0, // maxh\n\tQt::LayoutDirectionAuto, // dir\n};\n\nJoinAsAction::JoinAsAction(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::Menu &st,\n\tnot_null<PeerData*> peer,\n\tFn<void()> callback)\n: ItemBase(parent, st)\n, _dummyAction(new QAction(parent))\n, _st(st)\n, _peer(peer)\n, _height(st::groupCallJoinAsPadding.top()\n\t+ st::groupCallJoinAsPhotoSize\n\t+ st::groupCallJoinAsPadding.bottom()) {\n\tsetAcceptBoth(true);\n\tfitToMenuWidth();\n\tsetActionTriggered(std::move(callback));\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(this);\n\t\tpaint(p);\n\t}, lifetime());\n\n\tenableMouseSelecting();\n\tprepare();\n}\n\nvoid JoinAsAction::paint(Painter &p) {\n\tconst auto selected = isSelected();\n\tconst auto height = contentHeight();\n\tif (selected && _st.itemBgOver->c.alpha() < 255) {\n\t\tp.fillRect(0, 0, width(), height, _st.itemBg);\n\t}\n\tp.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg);\n\tif (isEnabled()) {\n\t\tpaintRipple(p, 0, 0);\n\t}\n\n\tconst auto &padding = st::groupCallJoinAsPadding;\n\t_peer->paintUserpic(\n\t\tp,\n\t\t_userpicView,\n\t\tpadding.left(),\n\t\tpadding.top(),\n\t\tst::groupCallJoinAsPhotoSize);\n\tconst auto textLeft = padding.left()\n\t\t+ st::groupCallJoinAsPhotoSize\n\t\t+ padding.left();\n\tp.setPen(selected ? _st.itemFgOver : _st.itemFg);\n\t_text.drawLeftElided(\n\t\tp,\n\t\ttextLeft,\n\t\tst::groupCallJoinAsTextTop,\n\t\t_textWidth,\n\t\twidth());\n\tp.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);\n\t_name.drawLeftElided(\n\t\tp,\n\t\ttextLeft,\n\t\tst::groupCallJoinAsNameTop,\n\t\t_nameWidth,\n\t\twidth());\n}\n\nvoid JoinAsAction::prepare() {\n\trpl::combine(\n\t\ttr::lng_group_call_display_as_header(),\n\t\tInfo::Profile::NameValue(_peer)\n\t) | rpl::on_next([=](QString text, QString name) {\n\t\tconst auto &padding = st::groupCallJoinAsPadding;\n\t\t_text.setMarkedText(_st.itemStyle, { text }, MenuTextOptions);\n\t\t_name.setMarkedText(_st.itemStyle, { name }, MenuTextOptions);\n\t\tconst auto textWidth = _text.maxWidth();\n\t\tconst auto nameWidth = _name.maxWidth();\n\t\tconst auto textLeft = padding.left()\n\t\t\t+ st::groupCallJoinAsPhotoSize\n\t\t\t+ padding.left();\n\t\tconst auto w = std::clamp(\n\t\t\t(textLeft\n\t\t\t\t+ std::max(textWidth, nameWidth)\n\t\t\t\t+ padding.right()),\n\t\t\t_st.widthMin,\n\t\t\t_st.widthMax);\n\t\tsetMinWidth(w);\n\t\t_textWidth = w - textLeft - padding.right();\n\t\t_nameWidth = w - textLeft - padding.right();\n\t\tupdate();\n\t}, lifetime());\n}\n\nbool JoinAsAction::isEnabled() const {\n\treturn true;\n}\n\nnot_null<QAction*> JoinAsAction::action() const {\n\treturn _dummyAction;\n}\n\nQPoint JoinAsAction::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos());\n}\n\nQImage JoinAsAction::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::RectMask(size());\n}\n\nint JoinAsAction::contentHeight() const {\n\treturn _height;\n}\n\nvoid JoinAsAction::handleKeyPress(not_null<QKeyEvent*> e) {\n\tif (!isSelected()) {\n\t\treturn;\n\t}\n\tconst auto key = e->key();\n\tif (key == Qt::Key_Enter || key == Qt::Key_Return) {\n\t\tsetClicked(Ui::Menu::TriggeredSource::Keyboard);\n\t}\n}\n\nRecordingAction::RecordingAction(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::Menu &st,\n\trpl::producer<QString> text,\n\trpl::producer<TimeId> startAtValues,\n\tFn<void()> callback)\n: ItemBase(parent, st)\n, _dummyAction(new QAction(parent))\n, _st(st)\n, _refreshTimer([=] { refreshElapsedText(); })\n, _smallHeight(st.itemPadding.top()\n\t+ _st.itemStyle.font->height\n\t+ st.itemPadding.bottom())\n, _bigHeight(st::groupCallRecordingTimerPadding.top()\n\t+ _st.itemStyle.font->height\n\t+ st::groupCallRecordingTimerFont->height\n\t+ st::groupCallRecordingTimerPadding.bottom()) {\n\tstd::move(\n\t\tstartAtValues\n\t) | rpl::on_next([=](TimeId startAt) {\n\t\t_startAt = startAt;\n\t\t_startedAt = crl::now();\n\t\t_refreshTimer.cancel();\n\t\trefreshElapsedText();\n\t\tresize(width(), contentHeight());\n\t}, lifetime());\n\n\tsetAcceptBoth(true);\n\tfitToMenuWidth();\n\tsetActionTriggered(std::move(callback));\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(this);\n\t\tpaint(p);\n\t}, lifetime());\n\n\tenableMouseSelecting();\n\tprepare(std::move(text));\n}\n\nvoid RecordingAction::paint(Painter &p) {\n\tconst auto selected = isSelected();\n\tconst auto height = contentHeight();\n\tif (selected && _st.itemBgOver->c.alpha() < 255) {\n\t\tp.fillRect(0, 0, width(), height, _st.itemBg);\n\t}\n\tp.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg);\n\tif (isEnabled()) {\n\t\tpaintRipple(p, 0, 0);\n\t}\n\tconst auto smallTop = st::groupCallRecordingTimerPadding.top();\n\tconst auto textTop = _startAt ? smallTop : _st.itemPadding.top();\n\tp.setPen(selected ? _st.itemFgOver : _st.itemFg);\n\t_text.drawLeftElided(\n\t\tp,\n\t\t_st.itemPadding.left(),\n\t\ttextTop,\n\t\t_textWidth,\n\t\twidth());\n\tif (_startAt) {\n\t\tp.setFont(st::groupCallRecordingTimerFont);\n\t\tp.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);\n\t\tp.drawTextLeft(\n\t\t\t_st.itemPadding.left(),\n\t\t\tsmallTop + _st.itemStyle.font->height,\n\t\t\twidth(),\n\t\t\t_elapsedText);\n\t}\n}\n\nvoid RecordingAction::refreshElapsedText() {\n\tconst auto now = base::unixtime::now();\n\tconst auto elapsed = std::max(now - _startAt, 0);\n\tconst auto text = !_startAt\n\t\t? QString()\n\t\t: (elapsed >= 3600)\n\t\t? QString(\"%1:%2:%3\"\n\t\t).arg(elapsed / 3600\n\t\t).arg((elapsed % 3600) / 60, 2, 10, QChar('0')\n\t\t).arg(elapsed % 60, 2, 10, QChar('0'))\n\t\t: QString(\"%1:%2\"\n\t\t).arg(elapsed / 60\n\t\t).arg(elapsed % 60, 2, 10, QChar('0'));\n\tif (_elapsedText != text) {\n\t\t_elapsedText = text;\n\t\tupdate();\n\t}\n\n\tconst auto nextCall = crl::time(500) - ((crl::now() - _startedAt) % 500);\n\t_refreshTimer.callOnce(nextCall);\n}\n\nvoid RecordingAction::prepare(rpl::producer<QString> text) {\n\trefreshElapsedText();\n\n\tconst auto &padding = _st.itemPadding;\n\tconst auto textWidth1 = _st.itemStyle.font->width(\n\t\ttr::lng_group_call_recording_start(tr::now));\n\tconst auto textWidth2 = _st.itemStyle.font->width(\n\t\ttr::lng_group_call_recording_stop(tr::now));\n\tconst auto maxWidth = st::groupCallRecordingTimerFont->width(\"23:59:59\");\n\tconst auto w = std::clamp(\n\t\t(padding.left()\n\t\t\t+ std::max({ textWidth1, textWidth2, maxWidth })\n\t\t\t+ padding.right()),\n\t\t_st.widthMin,\n\t\t_st.widthMax);\n\tsetMinWidth(w);\n\n\tstd::move(text) | rpl::on_next([=](QString text) {\n\t\tconst auto &padding = _st.itemPadding;\n\t\t_text.setMarkedText(_st.itemStyle, { text }, MenuTextOptions);\n\t\t_textWidth = w - padding.left() - padding.right();\n\t\tupdate();\n\t}, lifetime());\n}\n\nbool RecordingAction::isEnabled() const {\n\treturn true;\n}\n\nnot_null<QAction*> RecordingAction::action() const {\n\treturn _dummyAction;\n}\n\nQPoint RecordingAction::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos());\n}\n\nQImage RecordingAction::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::RectMask(size());\n}\n\nint RecordingAction::contentHeight() const {\n\treturn _startAt ? _bigHeight : _smallHeight;\n}\n\nvoid RecordingAction::handleKeyPress(not_null<QKeyEvent*> e) {\n\tif (!isSelected()) {\n\t\treturn;\n\t}\n\tconst auto key = e->key();\n\tif (key == Qt::Key_Enter || key == Qt::Key_Return) {\n\t\tsetClicked(Ui::Menu::TriggeredSource::Keyboard);\n\t}\n}\n\nbase::unique_qptr<Ui::Menu::ItemBase> MakeJoinAsAction(\n\t\tnot_null<Ui::Menu::Menu*> menu,\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> callback) {\n\treturn base::make_unique_q<JoinAsAction>(\n\t\tmenu,\n\t\tmenu->st(),\n\t\tpeer,\n\t\tstd::move(callback));\n}\n\nbase::unique_qptr<Ui::Menu::ItemBase> MakeRecordingAction(\n\t\tnot_null<Ui::Menu::Menu*> menu,\n\t\trpl::producer<TimeId> startDate,\n\t\tFn<void()> callback) {\n\tusing namespace rpl::mappers;\n\treturn base::make_unique_q<RecordingAction>(\n\t\tmenu,\n\t\tmenu->st(),\n\t\trpl::conditional(\n\t\t\trpl::duplicate(startDate) | rpl::map(!!_1),\n\t\t\ttr::lng_group_call_recording_stop(),\n\t\t\ttr::lng_group_call_recording_start()),\n\t\trpl::duplicate(startDate),\n\t\tstd::move(callback));\n}\n\n} // namespace\n\nvoid LeaveBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<GroupCall*> call,\n\t\tbool discardChecked,\n\t\tBoxContext context) {\n\tconst auto conference = call->conference();\n\tconst auto livestream = call->peer()->isBroadcast();\n\tconst auto scheduled = (call->scheduleDate() != 0);\n\tif (!scheduled) {\n\t\tbox->setTitle(conference\n\t\t\t? tr::lng_group_call_leave_title_call()\n\t\t\t: livestream\n\t\t\t? tr::lng_group_call_leave_title_channel()\n\t\t\t: tr::lng_group_call_leave_title());\n\t}\n\tconst auto inCall = (context == BoxContext::GroupCallPanel);\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox.get(),\n\t\t\t(scheduled\n\t\t\t\t? (livestream\n\t\t\t\t\t? tr::lng_group_call_close_sure_channel()\n\t\t\t\t\t: tr::lng_group_call_close_sure())\n\t\t\t\t: (conference\n\t\t\t\t\t? tr::lng_group_call_leave_sure_call()\n\t\t\t\t\t: livestream\n\t\t\t\t\t? tr::lng_group_call_leave_sure_channel()\n\t\t\t\t\t: tr::lng_group_call_leave_sure())),\n\t\t\t(inCall ? st::groupCallBoxLabel : st::boxLabel)),\n\t\tscheduled ? st::boxPadding : st::boxRowPadding);\n\tconst auto discard = call->canManage()\n\t\t? box->addRow(object_ptr<Ui::Checkbox>(\n\t\t\tbox.get(),\n\t\t\t(scheduled\n\t\t\t\t? (livestream\n\t\t\t\t\t? tr::lng_group_call_also_cancel_channel()\n\t\t\t\t\t: tr::lng_group_call_also_cancel())\n\t\t\t\t: (livestream\n\t\t\t\t\t? tr::lng_group_call_also_end_channel()\n\t\t\t\t\t: tr::lng_group_call_also_end())),\n\t\t\tdiscardChecked,\n\t\t\t(inCall ? st::groupCallCheckbox : st::defaultBoxCheckbox),\n\t\t\t(inCall ? st::groupCallCheck : st::defaultCheck)),\n\t\t\tstyle::margins(\n\t\t\t\tst::boxRowPadding.left(),\n\t\t\t\tst::boxRowPadding.left(),\n\t\t\t\tst::boxRowPadding.right(),\n\t\t\t\tst::boxRowPadding.bottom()))\n\t\t: nullptr;\n\tconst auto weak = base::make_weak(call);\n\tauto label = scheduled\n\t\t? tr::lng_group_call_close()\n\t\t: tr::lng_group_call_leave();\n\tbox->addButton(std::move(label), [=] {\n\t\tconst auto discardCall = (discard && discard->checked());\n\t\tbox->closeBox();\n\n\t\tif (!weak) {\n\t\t\treturn;\n\t\t} else if (discardCall) {\n\t\t\tcall->discard();\n\t\t} else {\n\t\t\tcall->hangup();\n\t\t}\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\nobject_ptr<Ui::GenericBox> ConfirmBox(Ui::ConfirmBoxArgs &&args) {\n\tauto copy = std::move(args);\n\tcopy.labelStyle = &st::groupCallBoxLabel;\n\treturn Ui::MakeConfirmBox(std::move(copy));\n}\n\nvoid FillMenu(\n\t\tnot_null<Ui::DropdownMenu*> menu,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<GroupCall*> call,\n\t\tbool wide,\n\t\tFn<void()> chooseJoinAs,\n\t\tFn<void()> chooseShareScreenSource,\n\t\tFn<void(object_ptr<Ui::BoxContent>)> showBox) {\n\tconst auto weak = base::make_weak(call);\n\tconst auto resolveReal = [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tif (const auto real = strong->lookupReal()) {\n\t\t\t\treturn real;\n\t\t\t}\n\t\t}\n\t\treturn (Data::GroupCall*)nullptr;\n\t};\n\tconst auto real = resolveReal();\n\tif (!real) {\n\t\treturn;\n\t}\n\n\tconst auto conference = call->conference();\n\tconst auto addEditJoinAs = call->showChooseJoinAs();\n\tconst auto addEditTitle = !conference && call->canManage();\n\tconst auto addEditRecording = !conference\n\t\t&& call->canManage()\n\t\t&& !real->scheduleDate();\n\tconst auto addScreenCast = call->videoIsWorking()\n\t\t&& !real->scheduleDate();\n\tif (addEditJoinAs) {\n\t\tmenu->addAction(MakeJoinAsAction(\n\t\t\tmenu->menu(),\n\t\t\tcall->joinAs(),\n\t\t\tchooseJoinAs));\n\t\tmenu->addSeparator();\n\t}\n\tif (addEditTitle) {\n\t\tconst auto livestream = call->peer()->isBroadcast();\n\t\tconst auto text = (livestream\n\t\t\t? tr::lng_group_call_edit_title_channel\n\t\t\t: tr::lng_group_call_edit_title)(tr::now);\n\t\tmenu->addAction(text, [=] {\n\t\t\tconst auto done = [=](const QString &title) {\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->changeTitle(title);\n\t\t\t\t}\n\t\t\t};\n\t\t\tif (const auto real = resolveReal()) {\n\t\t\t\tshowBox(Box(\n\t\t\t\t\tEditGroupCallTitleBox,\n\t\t\t\t\tpeer->name(),\n\t\t\t\t\treal->title(),\n\t\t\t\t\tlivestream,\n\t\t\t\t\tdone));\n\t\t\t}\n\t\t});\n\t}\n\tif (addEditRecording) {\n\t\tconst auto handler = [=] {\n\t\t\tconst auto real = resolveReal();\n\t\t\tif (!real) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto type = std::make_shared<RecordingType>();\n\t\t\tconst auto recordStartDate = real->recordStartDate();\n\t\t\tconst auto done = [=](QString title) {\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->toggleRecording(\n\t\t\t\t\t\t!recordStartDate,\n\t\t\t\t\t\ttitle,\n\t\t\t\t\t\t(*type) != RecordingType::AudioOnly,\n\t\t\t\t\t\t(*type) == RecordingType::VideoPortrait);\n\t\t\t\t}\n\t\t\t};\n\t\t\tif (recordStartDate) {\n\t\t\t\tshowBox(Box(\n\t\t\t\t\tStopGroupCallRecordingBox,\n\t\t\t\t\tdone));\n\t\t\t} else {\n\t\t\t\tconst auto typeDone = [=](RecordingType newType) {\n\t\t\t\t\t*type = newType;\n\t\t\t\t\tshowBox(Box(\n\t\t\t\t\t\tAddTitleGroupCallRecordingBox,\n\t\t\t\t\t\treal->title(),\n\t\t\t\t\t\tdone));\n\t\t\t\t};\n\t\t\t\tshowBox(Box(StartGroupCallRecordingBox, typeDone));\n\t\t\t}\n\t\t};\n\t\tmenu->addAction(MakeRecordingAction(\n\t\t\tmenu->menu(),\n\t\t\treal->recordStartDateValue(),\n\t\t\thandler));\n\t}\n\tif (addScreenCast) {\n\t\tconst auto sharing = call->isSharingScreen();\n\t\tconst auto toggle = [=] {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tif (sharing) {\n\t\t\t\t\tstrong->toggleScreenSharing(std::nullopt);\n\t\t\t\t} else {\n\t\t\t\t\tchooseShareScreenSource();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tmenu->addAction(\n\t\t\t(call->isSharingScreen()\n\t\t\t\t? tr::lng_group_call_screen_share_stop(tr::now)\n\t\t\t\t: tr::lng_group_call_screen_share_start(tr::now)),\n\t\t\ttoggle);\n\t}\n\tmenu->addAction(tr::lng_group_call_settings(tr::now), [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tshowBox(Box(SettingsBox, strong));\n\t\t}\n\t});\n\tconst auto finish = [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tshowBox(Box(\n\t\t\t\tLeaveBox,\n\t\t\t\tstrong,\n\t\t\t\ttrue,\n\t\t\t\tBoxContext::GroupCallPanel));\n\t\t}\n\t};\n\tconst auto livestream = real->peer()->isBroadcast();\n\tmenu->addAction(MakeAttentionAction(\n\t\tmenu->menu(),\n\t\t(!call->canManage()\n\t\t\t? tr::lng_group_call_leave\n\t\t\t: real->scheduleDate()\n\t\t\t? (livestream\n\t\t\t\t? tr::lng_group_call_cancel_channel\n\t\t\t\t: tr::lng_group_call_cancel)\n\t\t\t: (livestream\n\t\t\t\t? tr::lng_group_call_end_channel\n\t\t\t\t: tr::lng_group_call_end))(tr::now),\n\t\tfinish));\n}\n\nbase::unique_qptr<Ui::Menu::ItemBase> MakeAttentionAction(\n\t\tnot_null<Ui::Menu::Menu*> menu,\n\t\tconst QString &text,\n\t\tFn<void()> callback) {\n\treturn base::make_unique_q<Ui::Menu::Action>(\n\t\tmenu,\n\t\tst::groupCallFinishMenu,\n\t\tUi::Menu::CreateAction(\n\t\t\tmenu,\n\t\t\ttext,\n\t\t\tstd::move(callback)),\n\t\tnullptr,\n\t\tnullptr);\n\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_menu.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"base/unique_qptr.h\"\n#include \"ui/boxes/confirm_box.h\"\n\nnamespace style {\nstruct FlatLabel;\n} // namespace style\n\nnamespace Ui {\nclass DropdownMenu;\nclass GenericBox;\nclass BoxContent;\n} // namespace Ui\n\nnamespace Ui::Menu {\nclass ItemBase;\nclass Menu;\n} // namespace Ui::Menu\n\nnamespace Calls {\nclass GroupCall;\n} // namespace Calls\n\nnamespace Calls::Group {\n\nenum class BoxContext {\n\tGroupCallPanel,\n\tMainWindow,\n};\n\nvoid LeaveBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<GroupCall*> call,\n\tbool discardChecked,\n\tBoxContext context);\n\n[[nodiscard]] object_ptr<Ui::GenericBox> ConfirmBox(\n\tUi::ConfirmBoxArgs &&args);\n\nvoid FillMenu(\n\tnot_null<Ui::DropdownMenu*> menu,\n\tnot_null<PeerData*> peer,\n\tnot_null<GroupCall*> call,\n\tbool wide,\n\tFn<void()> chooseJoinAs,\n\tFn<void()> chooseShareScreenSource,\n\tFn<void(object_ptr<Ui::BoxContent>)> showBox);\n\n[[nodiscard]] base::unique_qptr<Ui::Menu::ItemBase> MakeAttentionAction(\n\tnot_null<Ui::Menu::Menu*> menu,\n\tconst QString &text,\n\tFn<void()> callback);\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_message_encryption.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_message_encryption.h\"\n\n#include <QtCore/QJsonValue>\n#include <QtCore/QJsonObject>\n#include <QtCore/QJsonArray>\n#include <QtCore/QJsonDocument>\n\nnamespace Calls::Group {\nnamespace {\n\n//[[nodiscard]] MTPJSONValue String(const QByteArray &value) {\n//\treturn MTP_jsonString(MTP_bytes(value));\n//}\n//\n//[[nodiscard]] MTPJSONValue Int(int value) {\n//\treturn MTP_jsonNumber(MTP_double(value));\n//}\n//\n//[[nodiscard]] MTPJSONObjectValue Value(\n//\t\tconst QByteArray &name,\n//\t\tconst MTPJSONValue &value) {\n//\treturn MTP_jsonObjectValue(MTP_bytes(name), value);\n//}\n//\n//[[nodiscard]] MTPJSONValue Object(\n//\t\tconst QByteArray &cons,\n//\t\tQVector<MTPJSONObjectValue> &&values) {\n//\tvalues.insert(values.begin(), Value(\"_\", String(cons)));\n//\treturn MTP_jsonObject(MTP_vector<MTPJSONObjectValue>(std::move(values)));\n//}\n//\n//[[nodiscard]] MTPJSONValue Array(QVector<MTPJSONValue> &&values) {\n//\treturn MTP_jsonArray(MTP_vector<MTPJSONValue>(std::move(values)));\n//}\n//\n//template <typename MTPD>\n//[[nodiscard]] MTPJSONValue SimpleEntity(\n//\t\tconst QByteArray &name,\n//\t\tconst MTPD &data) {\n//\treturn Object(name, {\n//\t\tValue(\"offset\", Int(data.voffset().v)),\n//\t\tValue(\"length\", Int(data.vlength().v)),\n//\t});\n//}\n//\n//[[nodiscard]] MTPJSONValue Entity(const MTPMessageEntity &entity) {\n//\treturn entity.match([](const MTPDmessageEntityBold &data) {\n//\t\treturn SimpleEntity(\"messageEntityBold\", data);\n//\t}, [](const MTPDmessageEntityItalic &data) {\n//\t\treturn SimpleEntity(\"messageEntityItalic\", data);\n//\t}, [](const MTPDmessageEntityUnderline &data) {\n//\t\treturn SimpleEntity(\"messageEntityUnderline\", data);\n//\t}, [](const MTPDmessageEntityStrike &data) {\n//\t\treturn SimpleEntity(\"messageEntityStrike\", data);\n//\t}, [](const MTPDmessageEntitySpoiler &data) {\n//\t\treturn SimpleEntity(\"messageEntitySpoiler\", data);\n//\t}, [](const MTPDmessageEntityCustomEmoji &data) {\n//\t\treturn Object(\"messageEntityCustomEmoji\", {\n//\t\t\tValue(\"offset\", Int(data.voffset().v)),\n//\t\t\tValue(\"length\", Int(data.vlength().v)),\n//\t\t\tValue(\n//\t\t\t\t\"document_id\",\n//\t\t\t\tString(QByteArray::number(int64(data.vdocument_id().v)))),\n//\t\t});\n//\t}, [](const auto &data) {\n//\t\treturn MTP_jsonNull();\n//\t});\n//}\n//\n//[[nodiscard]] QVector<MTPJSONValue> Entities(\n//\t\tconst QVector<MTPMessageEntity> &list) {\n//\tauto result = QVector<MTPJSONValue>();\n//\tresult.reserve(list.size());\n//\tfor (const auto &entity : list) {\n//\t\tif (const auto e = Entity(entity); e.type() != mtpc_jsonNull) {\n//\t\t\tresult.push_back(e);\n//\t\t}\n//\t}\n//\treturn result;\n//}\n//\n//[[nodiscard]] QByteArray Serialize(const MTPJSONValue &value) {\n//\tauto counter = ::tl::details::LengthCounter();\n//\tvalue.write(counter);\n//\tauto buffer = mtpBuffer();\n//\tbuffer.reserve(counter.length);\n//\tvalue.write(buffer);\n//\treturn QByteArray(\n//\t\treinterpret_cast<const char*>(buffer.constData()),\n//\t\tbuffer.size() * sizeof(buffer.front()));\n//}\n\n[[nodiscard]] QJsonValue String(const QByteArray &value) {\n\treturn QJsonValue(QString::fromUtf8(value));\n}\n\n[[nodiscard]] QJsonValue Int(int value) {\n\treturn QJsonValue(double(value));\n}\n\nstruct JsonObjectValue {\n\tconst char *name = nullptr;\n\tQJsonValue value;\n};\n\n[[nodiscard]] JsonObjectValue Value(\n\t\tconst char *name,\n\t\tconst QJsonValue &value) {\n\treturn JsonObjectValue{ name, value };\n}\n\n[[nodiscard]] QJsonValue Object(\n\t\tconst char *cons,\n\t\tQVector<JsonObjectValue> &&values) {\n\tauto result = QJsonObject();\n\tresult.insert(\"_\", cons);\n\tfor (const auto &value : values) {\n\t\tresult.insert(value.name, value.value);\n\t}\n\treturn result;\n}\n\n[[nodiscard]] QJsonValue Array(QVector<QJsonValue> &&values) {\n\tauto result = QJsonArray();\n\tfor (const auto &value : values) {\n\t\tresult.push_back(value);\n\t}\n\treturn result;\n}\n\ntemplate <typename MTPD>\n[[nodiscard]] QJsonValue SimpleEntity(\n\t\tconst char *name,\n\t\tconst MTPD &data) {\n\treturn Object(name, {\n\t\tValue(\"offset\", Int(data.voffset().v)),\n\t\tValue(\"length\", Int(data.vlength().v)),\n\t});\n}\n\n[[nodiscard]] QJsonValue Entity(const MTPMessageEntity &entity) {\n\treturn entity.match([](const MTPDmessageEntityBold &data) {\n\t\treturn SimpleEntity(\"messageEntityBold\", data);\n\t}, [](const MTPDmessageEntityItalic &data) {\n\t\treturn SimpleEntity(\"messageEntityItalic\", data);\n\t}, [](const MTPDmessageEntityUnderline &data) {\n\t\treturn SimpleEntity(\"messageEntityUnderline\", data);\n\t}, [](const MTPDmessageEntityStrike &data) {\n\t\treturn SimpleEntity(\"messageEntityStrike\", data);\n\t}, [](const MTPDmessageEntitySpoiler &data) {\n\t\treturn SimpleEntity(\"messageEntitySpoiler\", data);\n\t}, [](const MTPDmessageEntityCustomEmoji &data) {\n\t\treturn Object(\"messageEntityCustomEmoji\", {\n\t\t\tValue(\"offset\", Int(data.voffset().v)),\n\t\t\tValue(\"length\", Int(data.vlength().v)),\n\t\t\tValue(\n\t\t\t\t\"document_id\",\n\t\t\t\tString(QByteArray::number(int64(data.vdocument_id().v)))),\n\t\t});\n\t}, [](const auto &data) {\n\t\treturn QJsonValue(QJsonValue::Null);\n\t});\n}\n\n[[nodiscard]] QVector<QJsonValue> Entities(\n\t\tconst QVector<MTPMessageEntity> &list) {\n\tauto result = QVector<QJsonValue>();\n\tresult.reserve(list.size());\n\tfor (const auto &entity : list) {\n\t\tif (const auto e = Entity(entity); !e.isNull()) {\n\t\t\tresult.push_back(e);\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] QByteArray Serialize(const QJsonValue &value) {\n\treturn QJsonDocument(value.toObject()).toJson(QJsonDocument::Compact);\n}\n\n[[nodiscard]] std::optional<QJsonValue> GetValue(\n\t\tconst QJsonObject &object,\n\t\tconst char *name) {\n\tconst auto i = object.find(name);\n\treturn (i != object.end()) ? *i : std::optional<QJsonValue>();\n}\n\n[[nodiscard]] std::optional<int> GetInt(\n\t\tconst QJsonObject &object,\n\t\tconst char *name) {\n\tif (const auto maybeValue = GetValue(object, name)) {\n\t\tif (maybeValue->isDouble()) {\n\t\t\treturn int(base::SafeRound(maybeValue->toDouble()));\n\t\t} else if (maybeValue->isString()) {\n\t\t\tauto ok = false;\n\t\t\tconst auto result = maybeValue->toString().toInt(&ok);\n\t\t\treturn ok ? result : std::optional<int>();\n\t\t}\n\t}\n\treturn {};\n}\n\n[[nodiscard]] std::optional<uint64> GetLong(\n\t\tconst QJsonObject &object,\n\t\tconst char *name) {\n\tif (const auto maybeValue = GetValue(object, name)) {\n\t\tif (maybeValue->isDouble()) {\n\t\t\tconst auto value = maybeValue->toDouble();\n\t\t\treturn (value >= 0.)\n\t\t\t\t? uint64(base::SafeRound(value))\n\t\t\t\t: std::optional<uint64>();\n\t\t} else if (maybeValue->isString()) {\n\t\t\tauto ok = false;\n\t\t\tconst auto result = maybeValue->toString().toLongLong(&ok);\n\t\t\treturn ok ? uint64(result) : std::optional<uint64>();\n\t\t}\n\t}\n\treturn {};\n}\n\n\n[[nodiscard]] std::optional<QString> GetString(\n\t\tconst QJsonObject &object,\n\t\tconst char *name) {\n\tconst auto maybeValue = GetValue(object, name);\n\treturn (maybeValue && maybeValue->isString())\n\t\t? maybeValue->toString()\n\t\t: std::optional<QString>();\n}\n\n\n[[nodiscard]] std::optional<QString> GetCons(const QJsonObject &object) {\n\treturn GetString(object, \"_\");\n}\n\n[[nodiscard]] bool Unsupported(\n\t\tconst QJsonObject &object,\n\t\tconst QString &cons = QString()) {\n\tconst auto maybeMinLayer = GetInt(object, \"_min_layer\");\n\tconst auto layer = int(MTP::details::kCurrentLayer);\n\tif (maybeMinLayer.value_or(layer) > layer) {\n\t\tLOG((\"E2E Error: _min_layer too large: %1 > %2\").arg(*maybeMinLayer).arg(layer));\n\t\treturn true;\n\t} else if (!cons.isEmpty() && GetCons(object) != cons) {\n\t\tLOG((\"E2E Error: Expected %1 here.\").arg(cons));\n\t\treturn true;\n\t}\n\treturn false;\n}\n\n[[nodiscard]] std::optional<MTPMessageEntity> GetEntity(\n\t\tconst QString &text,\n\t\tconst QJsonObject &object) {\n\tconst auto cons = GetCons(object).value_or(QString());\n\tconst auto offset = GetInt(object, \"offset\").value_or(-1);\n\tconst auto length = GetInt(object, \"length\").value_or(0);\n\tif (Unsupported(object)\n\t\t|| (offset < 0)\n\t\t|| (length <= 0)\n\t\t|| (offset >= text.size())\n\t\t|| (length > text.size())\n\t\t|| (offset + length > text.size())) {\n\t\treturn {};\n\t}\n\tconst auto simple = [&](const auto &make) {\n\t\treturn make(MTP_int(offset), MTP_int(length));\n\t};\n\tif (cons == \"messageEntityBold\") {\n\t\treturn simple(MTP_messageEntityBold);\n\t} else if (cons == \"messageEntityItalic\") {\n\t\treturn simple(MTP_messageEntityItalic);\n\t} else if (cons == \"messageEntityUnderline\") {\n\t\treturn simple(MTP_messageEntityUnderline);\n\t} else if (cons == \"messageEntityStrike\") {\n\t\treturn simple(MTP_messageEntityStrike);\n\t} else if (cons == \"messageEntitySpoiler\") {\n\t\treturn simple(MTP_messageEntitySpoiler);\n\t} else if (cons == \"messageEntityCustomEmoji\") {\n\t\tconst auto maybeDocumentId = GetLong(object, \"document_id\");\n\t\tif (const auto documentId = maybeDocumentId.value_or(0)) {\n\t\t\treturn MTP_messageEntityCustomEmoji(\n\t\t\t\tMTP_int(offset),\n\t\t\t\tMTP_int(length),\n\t\t\t\tMTP_long(documentId));\n\t\t}\n\t}\n\treturn {};\n}\n\n[[nodiscard]] QVector<MTPMessageEntity> GetEntities(\n\t\tconst QString &text,\n\t\tconst QJsonArray &list) {\n\tauto result = QVector<MTPMessageEntity>();\n\tresult.reserve(list.size());\n\tfor (const auto &entry : list) {\n\t\tif (const auto entity = GetEntity(text, entry.toObject())) {\n\t\t\tresult.push_back(*entity);\n\t\t}\n\t}\n\treturn result;\n}\n\n} // namespace\n\nQByteArray SerializeMessage(const PreparedMessage &data) {\n\treturn Serialize(Object(\"groupCallMessage\", {\n\t\tValue(\n\t\t\t\"random_id\",\n\t\t\tString(QByteArray::number(int64(data.randomId)))),\n\t\tValue(\n\t\t\t\"message\",\n\t\t\tObject(\"textWithEntities\", {\n\t\t\t\tValue(\"text\", String(data.message.data().vtext().v)),\n\t\t\t\tValue(\n\t\t\t\t\t\"entities\",\n\t\t\t\t\tArray(Entities(data.message.data().ventities().v))),\n\t\t\t})),\n\t}));\n}\n\nstd::optional<PreparedMessage> DeserializeMessage(\n\t\tconst QByteArray &data) {\n\tauto error = QJsonParseError();\n\tauto document = QJsonDocument::fromJson(data, &error);\n\tif (error.error != QJsonParseError::NoError\n\t\t|| !document.isObject()) {\n\t\tLOG((\"E2E Error: Bad json in Calls::Group::DeserializeMessage.\"));\n\t\treturn {};\n\t}\n\tconst auto groupCallMessage = document.object();\n\tif (Unsupported(groupCallMessage, \"groupCallMessage\")) {\n\t\treturn {};\n\t}\n\tconst auto randomId = GetLong(groupCallMessage, \"random_id\").value_or(0);\n\tif (!randomId) {\n\t\treturn {};\n\t}\n\tconst auto message = groupCallMessage[\"message\"].toObject();\n\tif (Unsupported(message, \"textWithEntities\")) {\n\t\treturn {};\n\t}\n\tconst auto maybeText = GetString(message, \"text\");\n\tif (!maybeText) {\n\t\treturn {};\n\t}\n\tconst auto &text = *maybeText;\n\tconst auto maybeEntities = GetValue(message, \"entities\");\n\tif (!maybeEntities || !maybeEntities->isArray()) {\n\t\treturn {};\n\t}\n\tconst auto entities = GetEntities(text, maybeEntities->toArray());\n\treturn PreparedMessage{\n\t\t.randomId = randomId,\n\t\t.message = MTP_textWithEntities(\n\t\t\tMTP_string(text),\n\t\t\tMTP_vector<MTPMessageEntity>(entities)),\n\t};\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_message_encryption.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Calls::Group {\n\nstruct PreparedMessage {\n\tuint64 randomId = 0;\n\tMTPTextWithEntities message;\n};\n\n[[nodiscard]] QByteArray SerializeMessage(const PreparedMessage &data);\n[[nodiscard]] std::optional<PreparedMessage> DeserializeMessage(\n\tconst QByteArray &data);\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_message_field.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_message_field.h\"\n\n#include \"base/event_filter.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"calls/group/calls_group_messages.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/ui_integration.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"history/view/reactions/history_view_reactions_selector.h\"\n#include \"history/view/reactions/history_view_reactions_strip.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/controls/send_button.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/userpic_view.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Calls::Group {\nnamespace {\n\nconstexpr auto kErrorLimit = 99;\n\nusing Chosen = HistoryView::Reactions::ChosenReaction;\n\n} // namespace\n\nclass ReactionPanel final {\npublic:\n\tReactionPanel(\n\t\tnot_null<QWidget*> outer,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\trpl::producer<QRect> fieldGeometry);\n\t~ReactionPanel();\n\n\t[[nodiscard]] rpl::producer<Chosen> chosen() const;\n\n\tvoid show();\n\tvoid hide();\n\tvoid raise();\n\tvoid hideIfCollapsed();\n\tvoid collapse();\n\nprivate:\n\tstruct Hiding;\n\n\tvoid create();\n\tvoid updateShowState();\n\tvoid fadeOutSelector();\n\tvoid startAnimation();\n\n\tconst not_null<QWidget*> _outer;\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tstd::unique_ptr<Ui::RpWidget> _parent;\n\tstd::unique_ptr<HistoryView::Reactions::Selector> _selector;\n\tstd::vector<std::unique_ptr<Hiding>> _hiding;\n\trpl::event_stream<Chosen> _chosen;\n\tUi::Animations::Simple _showing;\n\trpl::variable<float64> _shownValue;\n\trpl::variable<QRect> _fieldGeometry;\n\trpl::variable<bool> _expanded;\n\trpl::variable<bool> _shown = false;\n\n};\n\nstruct ReactionPanel::Hiding {\n\texplicit Hiding(not_null<QWidget*> parent) : widget(parent) {\n\t}\n\n\tUi::RpWidget widget;\n\tUi::Animations::Simple animation;\n\tQImage frame;\n};\n\nReactionPanel::ReactionPanel(\n\tnot_null<QWidget*> outer,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\trpl::producer<QRect> fieldGeometry)\n: _outer(outer)\n, _show(std::move(show))\n, _fieldGeometry(std::move(fieldGeometry)) {\n}\n\nReactionPanel::~ReactionPanel() = default;\n\nauto ReactionPanel::chosen() const -> rpl::producer<Chosen> {\n\treturn _chosen.events();\n}\n\nvoid ReactionPanel::show() {\n\tif (_shown.current()) {\n\t\treturn;\n\t}\n\tcreate();\n\tif (!_selector) {\n\t\treturn;\n\t}\n\tconst auto duration = st::defaultPanelAnimation.heightDuration\n\t\t* st::defaultPopupMenu.showDuration;\n\t_shown = true;\n\t_showing.start([=] { updateShowState(); }, 0., 1., duration);\n\tupdateShowState();\n\t_parent->show();\n}\n\nvoid ReactionPanel::hide() {\n\tif (!_selector) {\n\t\treturn;\n\t}\n\t_selector->beforeDestroy();\n\tif (!anim::Disabled()) {\n\t\tfadeOutSelector();\n\t}\n\t_shown = false;\n\t_expanded = false;\n\t_showing.stop();\n\t_selector = nullptr;\n\t_parent = nullptr;\n}\n\nvoid ReactionPanel::raise() {\n\tif (_parent) {\n\t\t_parent->raise();\n\t}\n}\n\nvoid ReactionPanel::hideIfCollapsed() {\n\tif (!_expanded.current()) {\n\t\thide();\n\t}\n}\n\nvoid ReactionPanel::collapse() {\n\tif (_expanded.current()) {\n\t\thide();\n\t\tshow();\n\t}\n}\n\nvoid ReactionPanel::create() {\n\tauto reactions = Data::LookupPossibleReactions(&_show->session());\n\tif (reactions.recent.empty()) {\n\t\treturn;\n\t}\n\t_parent = std::make_unique<Ui::RpWidget>(_outer);\n\t_parent->show();\n\n\t_parent->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::MouseButtonPress) {\n\t\t\tconst auto event = static_cast<QMouseEvent*>(e.get());\n\t\t\tif (event->button() == Qt::LeftButton) {\n\t\t\t\tif (!_selector\n\t\t\t\t\t|| !_selector->geometry().contains(event->pos())) {\n\t\t\t\t\tcollapse();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, _parent->lifetime());\n\n\t_selector = std::make_unique<HistoryView::Reactions::Selector>(\n\t\t_parent.get(),\n\t\tst::storiesReactionsPan,\n\t\t_show,\n\t\tstd::move(reactions),\n\t\tTextWithEntities(),\n\t\t[=](bool fast) { hide(); },\n\t\tnullptr, // iconFactory\n\t\tnullptr, // paused\n\t\ttrue);\n\n\t_selector->chosen(\n\t) | rpl::on_next([=](Chosen reaction) {\n\t\tif (reaction.id.custom() && !_show->session().premium()) {\n\t\t\tShowPremiumPreviewBox(\n\t\t\t\t_show,\n\t\t\t\tPremiumFeature::AnimatedEmoji);\n\t\t} else {\n\t\t\t_chosen.fire(std::move(reaction));\n\t\t\thide();\n\t\t}\n\t}, _selector->lifetime());\n\n\tconst auto desiredWidth = st::storiesReactionsWidth;\n\tconst auto maxWidth = desiredWidth * 2;\n\tconst auto width = _selector->countWidth(desiredWidth, maxWidth);\n\tconst auto margins = _selector->marginsForShadow();\n\tconst auto categoriesTop = _selector->extendTopForCategoriesAndAbout(\n\t\twidth);\n\tconst auto full = margins.left() + width + margins.right();\n\n\t_shownValue = 0.;\n\trpl::combine(\n\t\t_fieldGeometry.value(),\n\t\t_shownValue.value(),\n\t\t_expanded.value()\n\t) | rpl::on_next([=](QRect field, float64 shown, bool expanded) {\n\t\tconst auto width = margins.left()\n\t\t\t+ _selector->countAppearedWidth(shown)\n\t\t\t+ margins.right();\n\t\tconst auto available = field.y();\n\t\tconst auto min = st::storiesReactionsBottomSkip\n\t\t\t+ st::reactStripHeight;\n\t\tconst auto max = min\n\t\t\t+ margins.top()\n\t\t\t+ categoriesTop\n\t\t\t+ st::storiesReactionsAddedTop;\n\t\tconst auto height = expanded ? std::min(available, max) : min;\n\t\tconst auto top = field.y() - height;\n\t\tconst auto shift = (width / 2);\n\t\tconst auto right = (field.x() + field.width() / 2 + shift);\n\t\t_parent->setGeometry(QRect((right - width), top, full, height));\n\t\tconst auto innerTop = height\n\t\t\t- st::storiesReactionsBottomSkip\n\t\t\t- st::reactStripHeight;\n\t\tconst auto maxAdded = innerTop - margins.top() - categoriesTop;\n\t\tconst auto added = std::min(maxAdded, st::storiesReactionsAddedTop);\n\t\t_selector->setSpecialExpandTopSkip(added);\n\t\t_selector->initGeometry(innerTop);\n\t}, _selector->lifetime());\n\n\t_selector->willExpand(\n\t) | rpl::on_next([=] {\n\t\t_expanded = true;\n\n\t\tconst auto raw = _parent.get();\n\t\tbase::install_event_filter(raw, qApp, [=](not_null<QEvent*> e) {\n\t\t\tif (e->type() == QEvent::MouseButtonPress) {\n\t\t\t\tconst auto event = static_cast<QMouseEvent*>(e.get());\n\t\t\t\tif (event->button() == Qt::LeftButton) {\n\t\t\t\t\tif (!_selector\n\t\t\t\t\t\t|| !_selector->geometry().contains(\n\t\t\t\t\t\t\t_parent->mapFromGlobal(event->globalPos()))) {\n\t\t\t\t\t\tcollapse();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t});\n\t}, _selector->lifetime());\n\n\t_selector->escapes() | rpl::on_next([=] {\n\t\tcollapse();\n\t}, _selector->lifetime());\n}\n\nvoid ReactionPanel::fadeOutSelector() {\n\tconst auto geometry = Ui::MapFrom(\n\t\t_outer,\n\t\t_parent.get(),\n\t\t_selector->geometry());\n\t_hiding.push_back(std::make_unique<Hiding>(_outer));\n\tconst auto raw = _hiding.back().get();\n\traw->frame = Ui::GrabWidgetToImage(_selector.get());\n\traw->widget.setGeometry(geometry);\n\traw->widget.show();\n\traw->widget.paintRequest(\n\t) | rpl::on_next([=] {\n\t\tif (const auto opacity = raw->animation.value(0.)) {\n\t\t\tauto p = QPainter(&raw->widget);\n\t\t\tp.setOpacity(opacity);\n\t\t\tp.drawImage(0, 0, raw->frame);\n\t\t}\n\t}, raw->widget.lifetime());\n\tUi::PostponeCall(&raw->widget, [=] {\n\t\traw->animation.start([=] {\n\t\t\tif (raw->animation.animating()) {\n\t\t\t\traw->widget.update();\n\t\t\t} else {\n\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\t_hiding,\n\t\t\t\t\traw,\n\t\t\t\t\t&std::unique_ptr<Hiding>::get);\n\t\t\t\tif (i != end(_hiding)) {\n\t\t\t\t\t_hiding.erase(i);\n\t\t\t\t}\n\t\t\t}\n\t\t}, 1., 0., st::slideWrapDuration);\n\t});\n}\n\nvoid ReactionPanel::updateShowState() {\n\tconst auto progress = _showing.value(_shown.current() ? 1. : 0.);\n\tconst auto opacity = 1.;\n\tconst auto appearing = _showing.animating();\n\tconst auto toggling = false;\n\t_shownValue = progress;\n\t_selector->updateShowState(progress, opacity, appearing, toggling);\n}\n\nMessageField::MessageField(\n\tnot_null<QWidget*> parent,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tPeerData *peer)\n: _parent(parent)\n, _show(std::move(show))\n, _wrap(std::make_unique<Ui::RpWidget>(_parent))\n, _limit(_show->session().appConfig().groupCallMessageLengthLimit()) {\n\tcreateControls(peer);\n}\n\nMessageField::~MessageField() = default;\n\nvoid MessageField::createControls(PeerData *peer) {\n\tsetupBackground();\n\n\tconst auto &st = st::storiesComposeControls;\n\t_field = Ui::CreateChild<Ui::InputField>(\n\t\t_wrap.get(),\n\t\tst.field,\n\t\tUi::InputField::Mode::MultiLine,\n\t\ttr::lng_message_ph());\n\t_field->setMaxLength(_limit + kErrorLimit);\n\t_field->setMinHeight(st.attach.height\n\t\t- st.padding.top()\n\t\t- st.padding.bottom());\n\t_field->setMaxHeight(st::historyComposeFieldMaxHeight);\n\t_field->setDocumentMargin(4.);\n\t_field->setAdditionalMargin(style::ConvertScale(4) - 4);\n\n\t_reactionPanel = std::make_unique<ReactionPanel>(\n\t\t_parent,\n\t\t_show,\n\t\t_wrap->geometryValue());\n\t_fieldFocused = _field->focusedChanges();\n\t_fieldEmpty = _field->changes() | rpl::map([field = _field] {\n\t\treturn field->getLastText().trimmed().isEmpty();\n\t});\n\trpl::combine(\n\t\t_fieldFocused.value(),\n\t\t_fieldEmpty.value()\n\t) | rpl::on_next([=](bool focused, bool empty) {\n\t\tif (!focused) {\n\t\t\t_reactionPanel->hideIfCollapsed();\n\t\t} else if (empty) {\n\t\t\t_reactionPanel->show();\n\t\t} else {\n\t\t\t_reactionPanel->hide();\n\t\t}\n\t}, _field->lifetime());\n\n\t_reactionPanel->chosen(\n\t) | rpl::on_next([=](Chosen reaction) {\n\t\tif (const auto customId = reaction.id.custom()) {\n\t\t\tconst auto document = _show->session().data().document(customId);\n\t\t\tif (const auto sticker = document->sticker()) {\n\t\t\t\tif (const auto alt = sticker->alt; !alt.isEmpty()) {\n\t\t\t\t\tconst auto length = int(alt.size());\n\t\t\t\t\tconst auto data = Data::SerializeCustomEmojiId(customId);\n\t\t\t\t\tconst auto tag = Ui::InputField::CustomEmojiLink(data);\n\t\t\t\t\t_submitted.fire({ alt, { { 0, length, tag } } });\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t_submitted.fire({ reaction.id.emoji() });\n\t\t}\n\t\t_reactionPanel->hide();\n\t}, _field->lifetime());\n\n\tconst auto show = _show;\n\tconst auto allow = [=](not_null<DocumentData*> emoji) {\n\t\tif (peer && Data::AllowEmojiWithoutPremium(peer, emoji)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\tInitMessageFieldHandlers({\n\t\t.session = &show->session(),\n\t\t.show = show,\n\t\t.field = _field,\n\t\t.customEmojiPaused = [=] {\n\t\t\treturn show->paused(ChatHelpers::PauseReason::Layer);\n\t\t},\n\t\t.allowPremiumEmoji = allow,\n\t\t.fieldStyle = &st.files.caption,\n\t\t.allowMarkdownTags = {\n\t\t\tUi::InputField::kTagBold,\n\t\t\tUi::InputField::kTagItalic,\n\t\t\tUi::InputField::kTagUnderline,\n\t\t\tUi::InputField::kTagStrikeOut,\n\t\t\tUi::InputField::kTagSpoiler,\n\t\t},\n\t});\n\tUi::Emoji::SuggestionsController::Init(\n\t\t_parent,\n\t\t_field,\n\t\t&_show->session(),\n\t\t{\n\t\t\t.suggestCustomEmoji = true,\n\t\t\t.allowCustomWithoutPremium = allow,\n\t\t\t.st = &st.suggestions,\n\t\t});\n\n\t_send = Ui::CreateChild<Ui::SendButton>(_wrap.get(), st.send);\n\t_send->show();\n\n\tusing Selector = ChatHelpers::TabbedSelector;\n\t_emojiPanel = std::make_unique<ChatHelpers::TabbedPanel>(\n\t\t_parent,\n\t\tChatHelpers::TabbedPanelDescriptor{\n\t\t\t.ownedSelector = object_ptr<Selector>(\n\t\t\t\tnullptr,\n\t\t\t\tChatHelpers::TabbedSelectorDescriptor{\n\t\t\t\t\t.show = _show,\n\t\t\t\t\t.st = st.tabbed,\n\t\t\t\t\t.level = ChatHelpers::PauseReason::Layer,\n\t\t\t\t\t.mode = ChatHelpers::TabbedSelector::Mode::EmojiOnly,\n\t\t\t\t\t.features = {\n\t\t\t\t\t\t.stickersSettings = false,\n\t\t\t\t\t\t.openStickerSets = false,\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t});\n\tconst auto panel = _emojiPanel.get();\n\tpanel->setDesiredHeightValues(\n\t\t1.,\n\t\tst::emojiPanMinHeight / 2,\n\t\tst::emojiPanMinHeight);\n\tpanel->hide();\n\tpanel->selector()->setCurrentPeer(peer);\n\tpanel->selector()->emojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\tUi::InsertEmojiAtCursor(_field->textCursor(), data.emoji);\n\t}, lifetime());\n\tpanel->selector()->customEmojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\tconst auto info = data.document->sticker();\n\t\tif (info\n\t\t\t&& info->setType == Data::StickersType::Emoji\n\t\t\t&& !_show->session().premium()) {\n\t\t\tShowPremiumPreviewBox(\n\t\t\t\t_show,\n\t\t\t\tPremiumFeature::AnimatedEmoji);\n\t\t} else {\n\t\t\tData::InsertCustomEmoji(_field, data.document);\n\t\t}\n\t}, lifetime());\n\n\t_emojiToggle = Ui::CreateChild<Ui::EmojiButton>(_wrap.get(), st.emoji);\n\t_emojiToggle->show();\n\n\t_emojiToggle->installEventFilter(panel);\n\t_emojiToggle->addClickHandler([=] {\n\t\tpanel->toggleAnimated();\n\t});\n\n\t_width.value(\n\t) | rpl::filter(\n\t\trpl::mappers::_1 > 0\n\t) | rpl::on_next([=](int newWidth) {\n\t\tconst auto &st = st::storiesComposeControls;\n\t\tconst auto fieldWidth = newWidth\n\t\t\t- st.padding.top()\n\t\t\t- _emojiToggle->width()\n\t\t\t- _send->width();\n\t\t_field->resizeToWidth(fieldWidth);\n\t\t_field->moveToLeft(st.padding.top(), st.padding.top(), newWidth);\n\t\tupdateWrapSize(newWidth);\n\t}, _lifetime);\n\n\trpl::combine(\n\t\t_width.value(),\n\t\t_field->heightValue()\n\t) | rpl::on_next([=](int width, int height) {\n\t\tif (width <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto &st = st::storiesComposeControls;\n\t\tconst auto minHeight = st.attach.height\n\t\t\t- st.padding.top()\n\t\t\t- st.padding.bottom();\n\t\t_send->moveToRight(0, height - minHeight, width);\n\t\t_emojiToggle->moveToRight(_send->width(), height - minHeight, width);\n\t\tupdateWrapSize();\n\t}, _lifetime);\n\n\t_field->cancelled() | rpl::on_next([=] {\n\t\t_closeRequests.fire({});\n\t}, _lifetime);\n\n\tconst auto updateLimitPosition = [=](QSize parent, QSize label) {\n\t\tconst auto &st = st::storiesComposeControls;\n\t\tconst auto skip = st.padding.top();\n\t\treturn QPoint(parent.width() - label.width() - skip, skip);\n\t};\n\tUi::AddLengthLimitLabel(_field, _limit, {\n\t\t.customParent = _wrap.get(),\n\t\t.customUpdatePosition = updateLimitPosition,\n\t});\n\n\trpl::merge(\n\t\t_field->submits() | rpl::to_empty,\n\t\t_send->clicks() | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\tauto text = _field->getTextWithTags();\n\t\tif (text.text.size() <= _limit) {\n\t\t\t_submitted.fire(std::move(text));\n\t\t}\n\t}, _lifetime);\n}\n\nvoid MessageField::updateEmojiPanelGeometry() {\n\tconst auto global = _emojiToggle->mapToGlobal({ 0, 0 });\n\tconst auto local = _parent->mapFromGlobal(global);\n\t_emojiPanel->moveBottomRight(\n\t\tlocal.y(),\n\t\tlocal.x() + _emojiToggle->width() * 3);\n}\n\nvoid MessageField::setupBackground() {\n\t_wrap->paintRequest() | rpl::on_next([=] {\n\t\tconst auto &st = st::storiesComposeControls;\n\t\tconst auto radius = st.attach.height / 2.;\n\t\tauto p = QPainter(_wrap.get());\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::storiesComposeBg);\n\t\tp.drawRoundedRect(_wrap->rect(), radius, radius);\n\t}, _lifetime);\n}\n\nvoid MessageField::resizeToWidth(int newWidth) {\n\t_width = newWidth;\n\tif (_wrap->isHidden()) {\n\t\tUi::SendPendingMoveResizeEvents(_wrap.get());\n\t}\n\tupdateEmojiPanelGeometry();\n}\n\nvoid MessageField::move(int x, int y) {\n\t_wrap->move(x, y);\n\tif (_cache) {\n\t\t_cache->move(x, y);\n\t}\n}\n\nvoid MessageField::toggle(bool shown) {\n\tif (_shown == shown) {\n\t\treturn;\n\t} else if (shown) {\n\t\tAssert(_width.current() > 0);\n\t\tUi::SendPendingMoveResizeEvents(_wrap.get());\n\t} else if (Ui::InFocusChain(_field)) {\n\t\t_parent->setFocus();\n\t}\n\t_shown = shown;\n\tif (!anim::Disabled()) {\n\t\tif (!_cache) {\n\t\t\tauto image = Ui::GrabWidgetToImage(_wrap.get());\n\t\t\t_cache = std::make_unique<Ui::RpWidget>(_parent);\n\t\t\tconst auto raw = _cache.get();\n\t\t\traw->paintRequest() | rpl::on_next([=] {\n\t\t\t\tauto p = QPainter(raw);\n\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\tconst auto scale = raw->height() / float64(_wrap->height());\n\t\t\t\tconst auto target = _wrap->rect();\n\t\t\t\tconst auto center = target.center();\n\t\t\t\tp.translate(center);\n\t\t\t\tp.scale(scale, scale);\n\t\t\t\tp.translate(-center);\n\t\t\t\tp.drawImage(target, image);\n\t\t\t}, raw->lifetime());\n\t\t\traw->show();\n\t\t\traw->move(_wrap->pos());\n\t\t\traw->resize(_wrap->width(), 0);\n\n\t\t\t_wrap->hide();\n\t\t}\n\t\t_shownAnimation.start(\n\t\t\t[=] { shownAnimationCallback(); },\n\t\t\tshown ? 0. : 1.,\n\t\t\tshown ? 1. : 0.,\n\t\t\tst::slideWrapDuration,\n\t\t\tanim::easeOutCirc);\n\t}\n\tshownAnimationCallback();\n}\n\nvoid MessageField::raise() {\n\t_wrap->raise();\n\tif (_cache) {\n\t\t_cache->raise();\n\t}\n\tif (_reactionPanel) {\n\t\t_reactionPanel->raise();\n\t}\n\tif (_emojiPanel) {\n\t\t_emojiPanel->raise();\n\t}\n}\n\nvoid MessageField::updateWrapSize(int widthOverride) {\n\tconst auto &st = st::storiesComposeControls;\n\tconst auto width = widthOverride ? widthOverride : _wrap->width();\n\tconst auto height = _field->height()\n\t\t+ st.padding.top()\n\t\t+ st.padding.bottom();\n\t_wrap->resize(width, height);\n\tupdateHeight();\n}\n\nvoid MessageField::updateHeight() {\n\t_height = int(base::SafeRound(\n\t\t_shownAnimation.value(_shown ? 1. : 0.) * _wrap->height()));\n}\n\nvoid MessageField::shownAnimationCallback() {\n\tupdateHeight();\n\tif (_shownAnimation.animating()) {\n\t\tAssert(_cache != nullptr);\n\t\t_cache->resize(_cache->width(), _height.current());\n\t\t_cache->update();\n\t} else if (_shown) {\n\t\t_cache = nullptr;\n\t\t_wrap->show();\n\t\t_field->setFocusFast();\n\t} else {\n\t\t_closed.fire({});\n\t}\n}\n\nint MessageField::height() const {\n\treturn _height.current();\n}\n\nrpl::producer<int> MessageField::heightValue() const {\n\treturn _height.value();\n}\n\nrpl::producer<TextWithTags> MessageField::submitted() const {\n\treturn _submitted.events();\n}\n\nrpl::producer<> MessageField::closeRequests() const {\n\treturn _closeRequests.events();\n}\n\nrpl::producer<> MessageField::closed() const {\n\treturn _closed.events();\n}\n\nrpl::lifetime &MessageField::lifetime() {\n\treturn _lifetime;\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_message_field.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n\nstruct TextWithTags;\n\nnamespace ChatHelpers {\nclass Show;\nclass TabbedPanel;\n} // namespace ChatHelpers\n\nnamespace Ui {\nclass EmojiButton;\nclass InputField;\nclass SendButton;\nclass RpWidget;\n} // namespace Ui\n\nnamespace Calls::Group {\n\nclass ReactionPanel;\n\nclass MessageField final {\npublic:\n\tMessageField(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tPeerData *peer);\n\t~MessageField();\n\n\tvoid resizeToWidth(int newWidth);\n\tvoid move(int x, int y);\n\tvoid toggle(bool shown);\n\tvoid raise();\n\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] rpl::producer<int> heightValue() const;\n\n\t[[nodiscard]] rpl::producer<TextWithTags> submitted() const;\n\t[[nodiscard]] rpl::producer<> closeRequests() const;\n\t[[nodiscard]] rpl::producer<> closed() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tvoid createControls(PeerData *peer);\n\tvoid setupBackground();\n\tvoid shownAnimationCallback();\n\tvoid updateEmojiPanelGeometry();\n\tvoid updateWrapSize(int widthOverride = 0);\n\tvoid updateHeight();\n\n\tconst not_null<QWidget*> _parent;\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst std::unique_ptr<Ui::RpWidget> _wrap;\n\n\tint _limit = 0;\n\tUi::InputField *_field = nullptr;\n\tUi::SendButton *_send = nullptr;\n\tUi::EmojiButton *_emojiToggle = nullptr;\n\tstd::unique_ptr<ChatHelpers::TabbedPanel> _emojiPanel;\n\tstd::unique_ptr<ReactionPanel> _reactionPanel;\n\trpl::variable<bool> _fieldFocused;\n\trpl::variable<bool> _fieldEmpty = true;\n\n\trpl::variable<int> _width;\n\trpl::variable<int> _height;\n\n\tbool _shown = false;\n\tUi::Animations::Simple _shownAnimation;\n\tstd::unique_ptr<Ui::RpWidget> _cache;\n\n\trpl::event_stream<TextWithTags> _submitted;\n\trpl::event_stream<> _closeRequests;\n\trpl::event_stream<> _closed;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_messages.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_messages.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_blocked_peers.h\"\n#include \"api/api_chat_participants.h\"\n#include \"api/api_text_entities.h\"\n#include \"base/random.h\"\n#include \"base/unixtime.h\"\n#include \"calls/group/ui/calls_group_stars_coloring.h\"\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_message_encryption.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/sender.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/ui_utility.h\"\n\nnamespace Calls::Group {\nnamespace {\n\nconstexpr auto kMaxShownVideoStreamMessages = 100;\nconstexpr auto kStarsStatsShortPollDelay = 30 * crl::time(1000);\n\n[[nodiscard]] StarsTop ParseStarsTop(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst MTPphone_GroupCallStars &stars) {\n\tconst auto &data = stars.data();\n\tconst auto &list = data.vtop_donors().v;\n\tauto result = StarsTop{ .total = int(data.vtotal_stars().v) };\n\tresult.topDonors.reserve(list.size());\n\tfor (const auto &entry : list) {\n\t\tconst auto &fields = entry.data();\n\t\tresult.topDonors.push_back({\n\t\t\t.peer = (fields.vpeer_id()\n\t\t\t\t? owner->peer(peerFromMTP(*fields.vpeer_id())).get()\n\t\t\t\t: nullptr),\n\t\t\t.stars = int(fields.vstars().v),\n\t\t\t.my = fields.is_my(),\n\t\t});\n\t}\n\treturn result;\n}\n\n[[nodiscard]] TimeId PinFinishDate(\n\t\tnot_null<PeerData*> peer,\n\t\tTimeId date,\n\t\tint stars) {\n\tif (!date || !stars) {\n\t\treturn 0;\n\t}\n\tconst auto &colorings = peer->session().appConfig().groupCallColorings();\n\treturn date + Ui::StarsColoringForCount(colorings, stars).secondsPin;\n}\n\n[[nodiscard]] TimeId PinFinishDate(const Message &message) {\n\treturn PinFinishDate(message.peer, message.date, message.stars);\n}\n\n} // namespace\n\nMessages::Messages(not_null<GroupCall*> call, not_null<MTP::Sender*> api)\n: _call(call)\n, _session(&call->peer()->session())\n, _api(api)\n, _destroyTimer([=] { checkDestroying(); })\n, _ttl(_session->appConfig().groupCallMessageTTL())\n, _starsStatsTimer([=] { requestStarsStats(); }) {\n\tUi::PostponeCall(_call, [=] {\n\t\t_call->real(\n\t\t) | rpl::on_next([=](not_null<Data::GroupCall*> call) {\n\t\t\t_real = call;\n\t\t\tif (ready()) {\n\t\t\t\tsendPending();\n\t\t\t} else {\n\t\t\t\tUnexpected(\"Not ready call.\");\n\t\t\t}\n\t\t}, _lifetime);\n\n\t\trequestStarsStats();\n\t});\n}\n\nMessages::~Messages() {\n\tif (_paid.sending > 0) {\n\t\tfinishPaidSending({\n\t\t\t.count = int(_paid.sending),\n\t\t\t.valid = true,\n\t\t\t.shownPeer = _paid.sendingShownPeer,\n\t\t}, false);\n\t}\n}\n\nvoid Messages::requestStarsStats() {\n\tif (!_call->videoStream()) {\n\t\treturn;\n\t}\n\t_starsStatsTimer.cancel();\n\t_starsTopRequestId = _api->request(MTPphone_GetGroupCallStars(\n\t\t_call->inputCall()\n\t)).done([=](const MTPphone_GroupCallStars &result) {\n\t\tconst auto &data = result.data();\n\n\t\tconst auto owner = &_session->data();\n\t\towner->processUsers(data.vusers());\n\t\towner->processChats(data.vchats());\n\n\t\t_paid.top = ParseStarsTop(owner, result);\n\t\t_paidChanges.fire({});\n\n\t\t_starsStatsTimer.callOnce(kStarsStatsShortPollDelay);\n\t}).fail([=](const MTP::Error &error) {\n\t\t[[maybe_unused]] const auto &type = error.type();\n\t\t_starsStatsTimer.callOnce(kStarsStatsShortPollDelay);\n\t}).send();\n\n}\n\nbool Messages::ready() const {\n\treturn _real && (!_call->conference() || _call->e2eEncryptDecrypt());\n}\n\nvoid Messages::send(TextWithTags text, int stars) {\n\tif (text.empty() && !stars) {\n\t\treturn;\n\t} else if (!ready()) {\n\t\t_pending.push_back({ std::move(text), stars });\n\t\treturn;\n\t}\n\n\tauto prepared = TextWithEntities{\n\t\ttext.text,\n\t\tTextUtilities::ConvertTextTagsToEntities(text.tags)\n\t};\n\tauto serialized = MTPTextWithEntities(MTP_textWithEntities(\n\t\tMTP_string(prepared.text),\n\t\tApi::EntitiesToMTP(\n\t\t\t&_real->session(),\n\t\t\tprepared.entities,\n\t\t\tApi::ConvertOption::SkipLocal)));\n\n\tconst auto localId = _call->peer()->owner().nextLocalMessageId();\n\tconst auto randomId = base::RandomValue<uint64>();\n\t_sendingIdByRandomId.emplace(randomId, localId);\n\n\tconst auto from = _call->messagesFrom();\n\tconst auto creator = _real->creator();\n\tconst auto skip = skipMessage(prepared, stars);\n\tif (skip) {\n\t\t_skippedIds.emplace(localId);\n\t} else {\n\t\t_messages.push_back({\n\t\t\t.id = localId,\n\t\t\t.peer = from,\n\t\t\t.text = std::move(prepared),\n\t\t\t.stars = stars,\n\t\t\t.admin = (from == _call->peer()) || (creator && from->isSelf()),\n\t\t\t.mine = true,\n\t\t});\n\t}\n\tif (!_call->conference()) {\n\t\tusing Flag = MTPphone_SendGroupCallMessage::Flag;\n\t\t_api->request(MTPphone_SendGroupCallMessage(\n\t\t\tMTP_flags(Flag::f_send_as\n\t\t\t\t| (stars ? Flag::f_allow_paid_stars : Flag())),\n\t\t\t_call->inputCall(),\n\t\t\tMTP_long(randomId),\n\t\t\tserialized,\n\t\t\tMTP_long(stars),\n\t\t\tfrom->input()\n\t\t)).done([=](\n\t\t\t\tconst MTPUpdates &result,\n\t\t\t\tconst MTP::Response &response) {\n\t\t\t_session->api().applyUpdates(result, randomId);\n\t\t}).fail([=](const MTP::Error &, const MTP::Response &response) {\n\t\t\tfailed(randomId, response);\n\t\t}).send();\n\t} else {\n\t\tconst auto bytes = SerializeMessage({ randomId, serialized });\n\t\tauto v = std::vector<std::uint8_t>(bytes.size());\n\t\tbytes::copy(bytes::make_span(v), bytes::make_span(bytes));\n\n\t\tconst auto userId = peerToUser(from->id).bare;\n\t\tconst auto encrypt = _call->e2eEncryptDecrypt();\n\t\tconst auto encrypted = encrypt(v, int64_t(userId), true, 0);\n\n\t\t_api->request(MTPphone_SendGroupCallEncryptedMessage(\n\t\t\t_call->inputCall(),\n\t\t\tMTP_bytes(bytes::make_span(encrypted))\n\t\t)).done([=](const MTPBool &, const MTP::Response &response) {\n\t\t\tsent(randomId, response);\n\t\t}).fail([=](const MTP::Error &, const MTP::Response &response) {\n\t\t\tfailed(randomId, response);\n\t\t}).send();\n\t}\n\n\taddStars(from, stars, true);\n\tif (!skip) {\n\t\tcheckDestroying(true);\n\t}\n}\n\nvoid Messages::setApplyingInitial(bool value) {\n\t_applyingInitial = value;\n}\n\nvoid Messages::received(const MTPDupdateGroupCallMessage &data) {\n\tif (!ready()) {\n\t\treturn;\n\t}\n\tconst auto &fields = data.vmessage().data();\n\treceived(\n\t\tfields.vid().v,\n\t\tfields.vfrom_id(),\n\t\tfields.vmessage(),\n\t\tfields.vdate().v,\n\t\tfields.vpaid_message_stars().value_or_empty(),\n\t\tfields.is_from_admin());\n}\n\nvoid Messages::received(const MTPDupdateGroupCallEncryptedMessage &data) {\n\tif (!ready()) {\n\t\treturn;\n\t}\n\tconst auto fromId = data.vfrom_id();\n\tconst auto &bytes = data.vencrypted_message().v;\n\tauto v = std::vector<std::uint8_t>(bytes.size());\n\tbytes::copy(bytes::make_span(v), bytes::make_span(bytes));\n\n\tconst auto userId = peerToUser(peerFromMTP(fromId)).bare;\n\tconst auto decrypt = _call->e2eEncryptDecrypt();\n\tconst auto decrypted = decrypt(v, int64_t(userId), false, 0);\n\n\tconst auto deserialized = DeserializeMessage(QByteArray::fromRawData(\n\t\treinterpret_cast<const char*>(decrypted.data()),\n\t\tdecrypted.size()));\n\tif (!deserialized) {\n\t\tLOG((\"API Error: Can't parse decrypted message\"));\n\t\treturn;\n\t}\n\tconst auto realId = ++_conferenceIdAutoIncrement;\n\tconst auto randomId = deserialized->randomId;\n\tif (!_conferenceIdByRandomId.emplace(randomId, realId).second) {\n\t\t// Already received.\n\t\treturn;\n\t}\n\treceived(\n\t\trealId,\n\t\tfromId,\n\t\tdeserialized->message,\n\t\tbase::unixtime::now(), // date\n\t\t0, // stars\n\t\tfalse,\n\t\ttrue); // checkCustomEmoji\n}\n\nvoid Messages::deleted(const MTPDupdateDeleteGroupCallMessages &data) {\n\tconst auto was = _messages.size();\n\tfor (const auto &id : data.vmessages().v) {\n\t\tconst auto i = ranges::find(_messages, id.v, &Message::id);\n\t\tif (i != end(_messages)) {\n\t\t\t_messages.erase(i);\n\t\t}\n\t}\n\tif (_messages.size() < was) {\n\t\tpushChanges();\n\t}\n}\n\nvoid Messages::sent(const MTPDupdateMessageID &data) {\n\tsent(data.vrandom_id().v, data.vid().v);\n}\n\nvoid Messages::sent(uint64 randomId, const MTP::Response &response) {\n\tconst auto realId = ++_conferenceIdAutoIncrement;\n\t_conferenceIdByRandomId.emplace(randomId, realId);\n\tsent(randomId, realId);\n\n\tconst auto i = ranges::find(_messages, realId, &Message::id);\n\tif (i != end(_messages) && !i->date) {\n\t\ti->date = Api::UnixtimeFromMsgId(response.outerMsgId);\n\t\ti->pinFinishDate = PinFinishDate(*i);\n\t\tcheckDestroying(true);\n\t}\n}\n\nvoid Messages::sent(uint64 randomId, MsgId realId) {\n\tconst auto i = _sendingIdByRandomId.find(randomId);\n\tif (i == end(_sendingIdByRandomId)) {\n\t\treturn;\n\t}\n\tconst auto localId = i->second;\n\t_sendingIdByRandomId.erase(i);\n\n\tconst auto j = ranges::find(_messages, localId, &Message::id);\n\tif (j == end(_messages)) {\n\t\t_skippedIds.emplace(realId);\n\t\treturn;\n\t}\n\tj->id = realId;\n\tcrl::on_main(this, [=] {\n\t\tconst auto i = ranges::find(_messages, realId, &Message::id);\n\t\tif (i != end(_messages) && !i->date) {\n\t\t\ti->date = base::unixtime::now();\n\t\t\ti->pinFinishDate = PinFinishDate(*i);\n\t\t\tcheckDestroying(true);\n\t\t}\n\t});\n\t_idUpdates.fire({ .localId = localId, .realId = realId });\n}\n\nvoid Messages::received(\n\t\tMsgId id,\n\t\tconst MTPPeer &from,\n\t\tconst MTPTextWithEntities &message,\n\t\tTimeId date,\n\t\tint stars,\n\t\tbool fromAdmin,\n\t\tbool checkCustomEmoji) {\n\tconst auto peer = _call->peer();\n\tconst auto i = ranges::find(_messages, id, &Message::id);\n\tif (i != end(_messages)) {\n\t\tconst auto fromId = peerFromMTP(from);\n\t\tconst auto me1 = peer->session().userPeerId();\n\t\tconst auto me2 = _call->messagesFrom()->id;\n\t\tif (((fromId == me1) || (fromId == me2)) && !i->date) {\n\t\t\ti->date = date;\n\t\t\ti->pinFinishDate = PinFinishDate(*i);\n\t\t\tcheckDestroying(true);\n\t\t}\n\t\treturn;\n\t} else if (_skippedIds.contains(id)) {\n\t\treturn;\n\t}\n\tauto allowedEntityTypes = std::vector<EntityType>{\n\t\tEntityType::Code,\n\t\tEntityType::Bold,\n\t\tEntityType::Semibold,\n\t\tEntityType::Spoiler,\n\t\tEntityType::StrikeOut,\n\t\tEntityType::Underline,\n\t\tEntityType::Italic,\n\t\tEntityType::CustomEmoji,\n\t};\n\tif (checkCustomEmoji && !peer->isSelf() && !peer->isPremium()) {\n\t\tallowedEntityTypes.pop_back();\n\t}\n\tconst auto author = peer->owner().peer(peerFromMTP(from));\n\n\tauto text = Ui::Text::Filtered(\n\t\tApi::ParseTextWithEntities(&author->session(), message),\n\t\tallowedEntityTypes);\n\tconst auto mine = author->isSelf()\n\t\t|| (author->isChannel() && author->asChannel()->amCreator());\n\tconst auto skip = skipMessage(text, stars);\n\tif (skip) {\n\t\t_skippedIds.emplace(id);\n\t} else {\n\t\t// Should check by sendAsPeers() list instead, but it may not be\n\t\t// loaded here yet.\n\t\t_messages.push_back({\n\t\t\t.id = id,\n\t\t\t.date = date,\n\t\t\t.pinFinishDate = PinFinishDate(author, date, stars),\n\t\t\t.peer = author,\n\t\t\t.text = std::move(text),\n\t\t\t.stars = stars,\n\t\t\t.admin = fromAdmin,\n\t\t\t.mine = mine,\n\t\t});\n\t\tranges::sort(_messages, ranges::less(), &Message::id);\n\t}\n\tif (!_applyingInitial) {\n\t\taddStars(author, stars, mine);\n\t}\n\tif (!skip) {\n\t\tcheckDestroying(true);\n\t}\n}\n\nbool Messages::skipMessage(const TextWithEntities &text, int stars) const {\n\tconst auto real = _call->lookupReal();\n\treturn text.empty()\n\t\t&& real\n\t\t&& (stars < real->messagesMinPrice());\n}\n\nvoid Messages::checkDestroying(bool afterChanges) {\n\tauto next = TimeId();\n\tconst auto now = base::unixtime::now();\n\tconst auto initial = int(_messages.size());\n\tif (_call->videoStream()) {\n\t\tif (initial > kMaxShownVideoStreamMessages) {\n\t\t\tconst auto remove = initial - kMaxShownVideoStreamMessages;\n\t\t\tauto i = begin(_messages);\n\t\t\tfor (auto k = 0; k != remove; ++k) {\n\t\t\t\tif (i->date && i->pinFinishDate <= now) {\n\t\t\t\t\ti = _messages.erase(i);\n\t\t\t\t} else if (!next || next > i->pinFinishDate - now) {\n\t\t\t\t\tnext = i->pinFinishDate - now;\n\t\t\t\t\t++i;\n\t\t\t\t} else {\n\t\t\t\t\t++i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else for (auto i = begin(_messages); i != end(_messages);) {\n\t\tconst auto date = i->date;\n\t\t//const auto ttl = i->stars\n\t\t//\t? (Ui::StarsColoringForCount(i->stars).minutesPin * 60)\n\t\t//\t: _ttl;\n\t\tconst auto ttl = _ttl;\n\t\tif (!date) {\n\t\t\tif (i->id < 0) {\n\t\t\t\t++i;\n\t\t\t} else {\n\t\t\t\ti = _messages.erase(i);\n\t\t\t}\n\t\t} else if (date + ttl <= now) {\n\t\t\ti = _messages.erase(i);\n\t\t} else if (!next || next > date + ttl - now) {\n\t\t\tnext = date + ttl - now;\n\t\t\t++i;\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tif (!next) {\n\t\t_destroyTimer.cancel();\n\t} else {\n\t\tconst auto delay = next * crl::time(1000);\n\t\tif (!_destroyTimer.isActive()\n\t\t\t|| (_destroyTimer.remainingTime() > delay)) {\n\t\t\t_destroyTimer.callOnce(delay);\n\t\t}\n\t}\n\tif (afterChanges || (_messages.size() < initial)) {\n\t\tpushChanges();\n\t}\n}\n\nrpl::producer<std::vector<Message>> Messages::listValue() const {\n\treturn _changes.events_starting_with_copy(_messages);\n}\n\nrpl::producer<MessageIdUpdate> Messages::idUpdates() const {\n\treturn _idUpdates.events();\n}\n\nvoid Messages::sendPending() {\n\tExpects(_real != nullptr);\n\n\tfor (auto &pending : base::take(_pending)) {\n\t\tsend(std::move(pending.text), pending.stars);\n\t}\n\tif (_paidSendingPending) {\n\t\treactionsPaidSend();\n\t}\n}\n\nvoid Messages::pushChanges() {\n\tif (_changesScheduled) {\n\t\treturn;\n\t}\n\t_changesScheduled = true;\n\tUi::PostponeCall(this, [=] {\n\t\t_changesScheduled = false;\n\t\t_changes.fire_copy(_messages);\n\t});\n}\n\nvoid Messages::failed(uint64 randomId, const MTP::Response &response) {\n\tconst auto i = _sendingIdByRandomId.find(randomId);\n\tif (i == end(_sendingIdByRandomId)) {\n\t\treturn;\n\t}\n\tconst auto localId = i->second;\n\t_sendingIdByRandomId.erase(i);\n\n\tconst auto j = ranges::find(_messages, localId, &Message::id);\n\tif (j != end(_messages) && !j->date) {\n\t\tj->date = Api::UnixtimeFromMsgId(response.outerMsgId);\n\t\tj->stars = 0;\n\t\tj->failed = true;\n\t\tcheckDestroying(true);\n\t}\n}\n\nint Messages::reactionsPaidScheduled() const {\n\treturn _paid.scheduled;\n}\n\nPeerId Messages::reactionsLocalShownPeer() const {\n\tconst auto minePaidShownPeer = [&] {\n\t\tfor (const auto &entry : _paid.top.topDonors) {\n\t\t\tif (entry.my) {\n\t\t\t\treturn entry.peer ? entry.peer->id : PeerId();\n\t\t\t}\n\t\t}\n\t\treturn _call->messagesFrom()->id;\n\t\t//const auto api = &_session->api();\n\t\t//return api->globalPrivacy().paidReactionShownPeerCurrent();\n\t};\n\treturn _paid.scheduledFlag\n\t\t? _paid.scheduledShownPeer\n\t\t: _paid.sendingFlag\n\t\t? _paid.sendingShownPeer\n\t\t: minePaidShownPeer();\n}\n\nvoid Messages::reactionsPaidAdd(int count) {\n\tExpects(count >= 0);\n\n\t_paid.scheduled += count;\n\t_paid.scheduledFlag = 1;\n\t_paid.scheduledShownPeer = _call->messagesFrom()->id;\n\tif (count > 0) {\n\t\t_session->credits().lock(CreditsAmount(count));\n\t}\n\t_call->peer()->owner().reactions().schedulePaid(_call);\n\t_paidChanges.fire({});\n}\n\nvoid Messages::reactionsPaidScheduledCancel() {\n\tif (!_paid.scheduledFlag) {\n\t\treturn;\n\t}\n\tif (const auto amount = int(_paid.scheduled)) {\n\t\t_session->credits().unlock(\n\t\t\tCreditsAmount(amount));\n\t}\n\t_paid.scheduled = 0;\n\t_paid.scheduledFlag = 0;\n\t_paid.scheduledShownPeer = 0;\n\t_paidChanges.fire({});\n}\n\nData::PaidReactionSend Messages::startPaidReactionSending() {\n\t_paidSendingPending = false;\n\tif (!_paid.scheduledFlag || !_paid.scheduled) {\n\t\treturn {};\n\t} else if (_paid.sendingFlag || !ready()) {\n\t\t_paidSendingPending = true;\n\t\treturn {};\n\t}\n\t_paid.sending = _paid.scheduled;\n\t_paid.sendingFlag = _paid.scheduledFlag;\n\t_paid.sendingShownPeer = _paid.scheduledShownPeer;\n\t_paid.scheduled = 0;\n\t_paid.scheduledFlag = 0;\n\t_paid.scheduledShownPeer = 0;\n\treturn {\n\t\t.count = int(_paid.sending),\n\t\t.valid = true,\n\t\t.shownPeer = _paid.sendingShownPeer,\n\t};\n}\n\nvoid Messages::finishPaidSending(\n\t\tData::PaidReactionSend send,\n\t\tbool success) {\n\tExpects(send.count == _paid.sending);\n\tExpects(send.valid == (_paid.sendingFlag == 1));\n\tExpects(send.shownPeer == _paid.sendingShownPeer);\n\n\t_paid.sending = 0;\n\t_paid.sendingFlag = 0;\n\t_paid.sendingShownPeer = 0;\n\tif (const auto amount = send.count) {\n\t\tif (success) {\n\t\t\tconst auto from = _session->data().peer(*send.shownPeer);\n\t\t\t_session->credits().withdrawLocked(CreditsAmount(amount));\n\n\t\t\tauto &donors = _paid.top.topDonors;\n\t\t\tconst auto i = ranges::find(donors, true, &StarsDonor::my);\n\t\t\tif (i != end(donors)) {\n\t\t\t\ti->peer = from;\n\t\t\t\ti->stars += amount;\n\t\t\t} else {\n\t\t\t\tdonors.push_back({\n\t\t\t\t\t.peer = from,\n\t\t\t\t\t.stars = amount,\n\t\t\t\t\t.my = true,\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\t_session->credits().unlock(CreditsAmount(amount));\n\t\t\t_paidChanges.fire({});\n\t\t}\n\t}\n\tif (_paidSendingPending) {\n\t\treactionsPaidSend();\n\t}\n}\n\nvoid Messages::reactionsPaidSend() {\n\tconst auto send = startPaidReactionSending();\n\tif (!send.valid || !send.count) {\n\t\treturn;\n\t}\n\n\tconst auto localId = _call->peer()->owner().nextLocalMessageId();\n\tconst auto randomId = base::RandomValue<uint64>();\n\t_sendingIdByRandomId.emplace(randomId, localId);\n\n\tconst auto from = _session->data().peer(*send.shownPeer);\n\tconst auto stars = int(send.count);\n\tconst auto skip = skipMessage({}, stars);\n\tif (skip) {\n\t\t_skippedIds.emplace(localId);\n\t} else {\n\t\t_messages.push_back({\n\t\t\t.id = localId,\n\t\t\t.peer = from,\n\t\t\t.stars = stars,\n\t\t\t.admin = (from == _call->peer()),\n\t\t\t.mine = true,\n\t\t});\n\t}\n\tusing Flag = MTPphone_SendGroupCallMessage::Flag;\n\t_api->request(MTPphone_SendGroupCallMessage(\n\t\tMTP_flags(Flag::f_send_as | Flag::f_allow_paid_stars),\n\t\t_call->inputCall(),\n\t\tMTP_long(randomId),\n\t\tMTP_textWithEntities(MTP_string(), MTP_vector<MTPMessageEntity>()),\n\t\tMTP_long(stars),\n\t\tfrom->input()\n\t)).done([=](\n\t\t\tconst MTPUpdates &result,\n\t\t\tconst MTP::Response &response) {\n\t\tfinishPaidSending(send, true);\n\t\t_session->api().applyUpdates(result, randomId);\n\t}).fail([=](const MTP::Error &, const MTP::Response &response) {\n\t\tfinishPaidSending(send, false);\n\t\tfailed(randomId, response);\n\t}).send();\n\n\taddStars(from, stars, true);\n\tif (!skip) {\n\t\tcheckDestroying(true);\n\t}\n}\n\nvoid Messages::undoScheduledPaidOnDestroy() {\n\t_call->peer()->owner().reactions().undoScheduledPaid(_call);\n}\n\nMessages::PaidLocalState Messages::starsLocalState() const {\n\tconst auto &donors = _paid.top.topDonors;\n\tconst auto i = ranges::find(donors, true, &StarsDonor::my);\n\tconst auto local = int(_paid.scheduled);\n\tconst auto my = (i != end(donors) ? i->stars : 0) + local;\n\tconst auto total = _paid.top.total + local;\n\treturn { .total = total, .my = my };\n}\n\nvoid Messages::deleteConfirmed(MessageDeleteRequest request) {\n\tconst auto eraseFrom = [&](auto iterator) {\n\t\tif (iterator != end(_messages)) {\n\t\t\t_messages.erase(iterator, end(_messages));\n\t\t\tpushChanges();\n\t\t}\n\t};\n\tconst auto peer = _call->peer();\n\tif (const auto from = request.deleteAllFrom) {\n\t\tusing Flag = MTPphone_DeleteGroupCallParticipantMessages::Flag;\n\t\t_api->request(MTPphone_DeleteGroupCallParticipantMessages(\n\t\t\tMTP_flags(request.reportSpam ? Flag::f_report_spam : Flag()),\n\t\t\t_call->inputCall(),\n\t\t\tfrom->input()\n\t\t)).send();\n\t\teraseFrom(ranges::remove(_messages, not_null(from), &Message::peer));\n\t} else {\n\t\tusing Flag = MTPphone_DeleteGroupCallMessages::Flag;\n\t\t_api->request(MTPphone_DeleteGroupCallMessages(\n\t\t\tMTP_flags(request.reportSpam ? Flag::f_report_spam : Flag()),\n\t\t\t_call->inputCall(),\n\t\t\tMTP_vector<MTPint>(1, MTP_int(request.id.bare))\n\t\t)).send();\n\t\teraseFrom(ranges::remove(_messages, request.id, &Message::id));\n\t}\n\tif (const auto ban = request.ban) {\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\tban->session().api().chatParticipants().kick(\n\t\t\t\tchannel,\n\t\t\t\tban,\n\t\t\t\tChatRestrictionsInfo());\n\t\t} else {\n\t\t\tban->session().api().blockedPeers().block(ban);\n\t\t}\n\t}\n}\n\nvoid Messages::addStars(not_null<PeerData*> from, int stars, bool mine) {\n\tif (stars <= 0) {\n\t\treturn;\n\t}\n\t_paid.top.total += stars;\n\tconst auto i = ranges::find(\n\t\t_paid.top.topDonors,\n\t\tfrom.get(),\n\t\t&StarsDonor::peer);\n\tif (i != end(_paid.top.topDonors)) {\n\t\ti->stars += stars;\n\t} else {\n\t\t_paid.top.topDonors.push_back({\n\t\t\t.peer = from,\n\t\t\t.stars = stars,\n\t\t\t.my = mine,\n\t\t});\n\t}\n\tranges::stable_sort(\n\t\t_paid.top.topDonors,\n\t\tranges::greater(),\n\t\t&StarsDonor::stars);\n\t_paidChanges.fire({ .peer = from, .stars = stars });\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_messages.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n\nnamespace Calls {\nclass GroupCall;\n} // namespace Calls\n\nnamespace Data {\nclass GroupCall;\nstruct PaidReactionSend;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace MTP {\nclass Sender;\nstruct Response;\n} // namespace MTP\n\nnamespace Calls::Group {\n\nstruct Message {\n\tMsgId id = 0;\n\tTimeId date = 0;\n\tTimeId pinFinishDate = 0;\n\tnot_null<PeerData*> peer;\n\tTextWithEntities text;\n\tint stars = 0;\n\tbool failed = false;\n\tbool admin = false;\n\tbool mine = false;\n};\n\nstruct MessageIdUpdate {\n\tMsgId localId = 0;\n\tMsgId realId = 0;\n};\n\nstruct MessageDeleteRequest {\n\tMsgId id = 0;\n\tPeerData *deleteAllFrom = nullptr;\n\tPeerData *ban = nullptr;\n\tbool reportSpam = false;\n};\n\nstruct StarsDonor {\n\tPeerData *peer = nullptr;\n\tint stars = 0;\n\tbool my = false;\n\n\tfriend inline bool operator==(\n\t\tconst StarsDonor &,\n\t\tconst StarsDonor &) = default;\n};\n\nstruct StarsTop {\n\tstd::vector<StarsDonor> topDonors;\n\tint total = 0;\n\n\tfriend inline bool operator==(\n\t\tconst StarsTop &,\n\t\tconst StarsTop &) = default;\n};\n\nclass Messages final : public base::has_weak_ptr {\npublic:\n\tMessages(not_null<GroupCall*> call, not_null<MTP::Sender*> api);\n\t~Messages();\n\n\tvoid send(TextWithTags text, int stars);\n\n\tvoid setApplyingInitial(bool value);\n\tvoid received(const MTPDupdateGroupCallMessage &data);\n\tvoid received(const MTPDupdateGroupCallEncryptedMessage &data);\n\tvoid deleted(const MTPDupdateDeleteGroupCallMessages &data);\n\tvoid sent(const MTPDupdateMessageID &data);\n\n\t[[nodiscard]] rpl::producer<std::vector<Message>> listValue() const;\n\t[[nodiscard]] rpl::producer<MessageIdUpdate> idUpdates() const;\n\n\t[[nodiscard]] int reactionsPaidScheduled() const;\n\t[[nodiscard]] PeerId reactionsLocalShownPeer() const;\n\tvoid reactionsPaidAdd(int count);\n\tvoid reactionsPaidScheduledCancel();\n\tvoid reactionsPaidSend();\n\tvoid undoScheduledPaidOnDestroy();\n\n\tstruct PaidLocalState {\n\t\tint total = 0;\n\t\tint my = 0;\n\t};\n\t[[nodiscard]] PaidLocalState starsLocalState() const;\n\t[[nodiscard]] rpl::producer<StarsDonor> starsValueChanges() const {\n\t\treturn _paidChanges.events();\n\t}\n\t[[nodiscard]] const StarsTop &starsTop() const {\n\t\treturn _paid.top;\n\t}\n\n\tvoid requestHiddenShow() {\n\t\t_hiddenShowRequests.fire({});\n\t}\n\t[[nodiscard]] rpl::producer<> hiddenShowRequested() const {\n\t\treturn _hiddenShowRequests.events();\n\t}\n\n\tvoid deleteConfirmed(MessageDeleteRequest request);\n\nprivate:\n\tstruct Pending {\n\t\tTextWithTags text;\n\t\tint stars = 0;\n\t};\n\tstruct Paid {\n\t\tStarsTop top;\n\t\tPeerId scheduledShownPeer = 0;\n\t\tPeerId sendingShownPeer = 0;\n\t\tuint32 scheduled : 30 = 0;\n\t\tuint32 scheduledFlag : 1 = 0;\n\t\tuint32 scheduledPrivacySet : 1 = 0;\n\t\tuint32 sending : 30 = 0;\n\t\tuint32 sendingFlag : 1 = 0;\n\t\tuint32 sendingPrivacySet : 1 = 0;\n\t};\n\n\t[[nodiscard]] bool ready() const;\n\tvoid sendPending();\n\tvoid pushChanges();\n\tvoid checkDestroying(bool afterChanges = false);\n\n\tvoid received(\n\t\tMsgId id,\n\t\tconst MTPPeer &from,\n\t\tconst MTPTextWithEntities &message,\n\t\tTimeId date,\n\t\tint stars,\n\t\tbool fromAdmin,\n\t\tbool checkCustomEmoji = false);\n\tvoid sent(uint64 randomId, const MTP::Response &response);\n\tvoid sent(uint64 randomId, MsgId realId);\n\tvoid failed(uint64 randomId, const MTP::Response &response);\n\n\t[[nodiscard]] bool skipMessage(\n\t\tconst TextWithEntities &text,\n\t\tint stars) const;\n\t[[nodiscard]] Data::PaidReactionSend startPaidReactionSending();\n\tvoid finishPaidSending(\n\t\tData::PaidReactionSend send,\n\t\tbool success);\n\tvoid addStars(not_null<PeerData*> from, int stars, bool mine);\n\tvoid requestStarsStats();\n\n\tconst not_null<GroupCall*> _call;\n\tconst not_null<Main::Session*> _session;\n\tconst not_null<MTP::Sender*> _api;\n\n\tMsgId _conferenceIdAutoIncrement = 0;\n\tbase::flat_map<uint64, MsgId> _conferenceIdByRandomId;\n\n\tbase::flat_map<uint64, MsgId> _sendingIdByRandomId;\n\n\tData::GroupCall *_real = nullptr;\n\n\tstd::vector<Pending> _pending;\n\n\tbase::Timer _destroyTimer;\n\tstd::vector<Message> _messages;\n\tbase::flat_set<MsgId> _skippedIds;\n\trpl::event_stream<std::vector<Message>> _changes;\n\trpl::event_stream<MessageIdUpdate> _idUpdates;\n\tbool _applyingInitial = false;\n\n\tmtpRequestId _starsTopRequestId = 0;\n\tPaid _paid;\n\trpl::event_stream<StarsDonor> _paidChanges;\n\tbool _paidSendingPending = false;\n\n\tTimeId _ttl = 0;\n\tbool _changesScheduled = false;\n\n\trpl::event_stream<> _hiddenShowRequests;\n\tbase::Timer _starsStatsTimer;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_messages_ui.h\"\n\n#include \"base/unixtime.h\"\n#include \"boxes/peers/prepare_short_info_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"calls/group/ui/calls_group_stars_coloring.h\"\n#include \"calls/group/calls_group_messages.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/ui_integration.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"data/data_document.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_message_reaction_id.h\"\n#include \"data/data_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/controls/send_button.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/effects/reaction_fly_animation.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/custom_emoji_text_badge.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/menu/menu_item_base.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/elastic_scroll.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/color_int_conversion.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/userpic_view.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_info_levels.h\"\n#include \"styles/style_media_view.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QWindow>\n\nnamespace Calls::Group {\nnamespace {\n\nconstexpr auto kMessageBgOpacity = 0.8;\nconstexpr auto kDarkOverOpacity = 0.25;\nconstexpr auto kColoredMessageBgOpacity = 0.65;\nconstexpr auto kAdminBadgeTextOpacity = 0.6;\n\n[[nodiscard]] int CountMessageRadius() {\n\tconst auto minHeight = st::groupCallMessagePadding.top()\n\t\t+ st::messageTextStyle.font->height\n\t\t+ st::groupCallMessagePadding.bottom();\n\treturn minHeight / 2;\n}\n\n[[nodiscard]] int CountPriceRadius() {\n\tconst auto height = st::groupCallPricePadding.top()\n\t\t+ st::normalFont->height\n\t\t+ st::groupCallPricePadding.bottom();\n\treturn height / 2;\n}\n\n[[nodiscard]] int CountPinnedRadius() {\n\tconst auto height = st::groupCallUserpicPadding.top()\n\t\t+ st::groupCallPinnedUserpic\n\t\t+ st::groupCallUserpicPadding.bottom();\n\treturn height / 2;\n}\n\n[[nodiscard]] uint64 ColoringKey(const Ui::StarsColoring &value) {\n\treturn uint64(uint32(value.bgLight))\n\t\t| (uint64(uint32(value.bgDark)) << 32);\n}\n\nvoid ReceiveSomeMouseEvents(\n\t\tnot_null<Ui::ElasticScroll*> scroll,\n\t\tFn<bool(QPoint)> handleClick) {\n\tclass EventFilter final : public QObject {\n\tpublic:\n\t\texplicit EventFilter(\n\t\t\tnot_null<Ui::ElasticScroll*> scroll,\n\t\t\tFn<bool(QPoint)> handleClick)\n\t\t: QObject(scroll)\n\t\t, _handleClick(std::move(handleClick)) {\n\t\t}\n\n\t\tbool eventFilter(QObject *watched, QEvent *event) {\n\t\t\tif (event->type() == QEvent::MouseButtonPress) {\n\t\t\t\treturn mousePressFilter(\n\t\t\t\t\twatched,\n\t\t\t\t\tstatic_cast<QMouseEvent*>(event));\n\t\t\t} else if (event->type() == QEvent::Wheel) {\n\t\t\t\treturn wheelFilter(\n\t\t\t\t\twatched,\n\t\t\t\t\tstatic_cast<QWheelEvent*>(event));\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\tbool mousePressFilter(\n\t\t\t\tQObject *watched,\n\t\t\t\tnot_null<QMouseEvent*> event) {\n\t\t\tExpects(parent()->isWidgetType());\n\n\t\t\tconst auto scroll = static_cast<Ui::ElasticScroll*>(parent());\n\t\t\tif (watched != scroll->window()->windowHandle()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto global = event->globalPos();\n\t\t\tconst auto local = scroll->mapFromGlobal(global);\n\t\t\tif (!scroll->rect().contains(local)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn _handleClick(local + QPoint(0, scroll->scrollTop()));\n\t\t}\n\n\t\tbool wheelFilter(QObject *watched, not_null<QWheelEvent*> event) {\n\t\t\tExpects(parent()->isWidgetType());\n\n\t\t\tconst auto scroll = static_cast<Ui::ElasticScroll*>(parent());\n\t\t\tif (watched != scroll->window()->windowHandle()\n\t\t\t\t|| !scroll->scrollTopMax()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto global = event->globalPosition().toPoint();\n\t\t\tconst auto local = scroll->mapFromGlobal(global);\n\t\t\tif (!scroll->rect().contains(local)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tauto e = QWheelEvent(\n\t\t\t\tevent->position(),\n\t\t\t\tevent->globalPosition(),\n\t\t\t\tevent->pixelDelta(),\n\t\t\t\tevent->angleDelta(),\n\t\t\t\tevent->buttons(),\n\t\t\t\tevent->modifiers(),\n\t\t\t\tevent->phase(),\n\t\t\t\tevent->inverted(),\n\t\t\t\tevent->source());\n\t\t\te.setTimestamp(crl::now());\n\t\t\tQGuiApplication::sendEvent(scroll, &e);\n\t\t\treturn true;\n\t\t}\n\n\tprivate:\n\t\tFn<bool(QPoint)> _handleClick;\n\n\t};\n\n\tscroll->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tqApp->installEventFilter(\n\t\tnew EventFilter(scroll, std::move(handleClick)));\n}\n\n[[nodiscard]] base::unique_qptr<Ui::Menu::ItemBase> MakeMessageDateAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tTimeId value) {\n\tconst auto parent = menu->menu();\n\n\tconst auto parsed = base::unixtime::parse(value);\n\tconst auto date = parsed.date();\n\tconst auto time = QLocale().toString(\n\t\tparsed.time(),\n\t\tQLocale::ShortFormat);\n\tconst auto today = QDateTime::currentDateTime().date();\n\tconst auto text = (date == today)\n\t\t? tr::lng_context_sent_today(tr::now, lt_time, time)\n\t\t: (date.addDays(1) == today)\n\t\t? tr::lng_context_sent_yesterday(tr::now, lt_time, time)\n\t\t: tr::lng_context_sent_date(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\tlangDayOfMonthFull(date),\n\t\t\tlt_time,\n\t\t\ttime);\n\tconst auto action = Ui::Menu::CreateAction(parent, text, [] {});\n\taction->setDisabled(true);\n\treturn base::make_unique_q<Ui::Menu::Action>(\n\t\tmenu->menu(),\n\t\tst::storiesCommentSentAt,\n\t\taction,\n\t\tnullptr,\n\t\tnullptr);\n}\n\nvoid ShowDeleteMessageConfirmation(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tMsgId id,\n\t\tnot_null<PeerData*> from,\n\t\tbool canModerate,\n\t\tFn<void(MessageDeleteRequest)> callback) {\n\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tstruct State {\n\t\t\trpl::variable<bool> report;\n\t\t\trpl::variable<bool> all;\n\t\t\trpl::variable<bool> ban;\n\t\t};\n\t\tconst auto state = box->lifetime().make_state<State>();\n\t\tconst auto confirmed = [=](Fn<void()> close) {\n\t\t\tcallback(MessageDeleteRequest{\n\t\t\t\t.id = id,\n\t\t\t\t.deleteAllFrom = state->all.current() ? from.get() : nullptr,\n\t\t\t\t.ban = state->ban.current() ? from.get() : nullptr,\n\t\t\t\t.reportSpam = state->report.current(),\n\t\t\t});\n\t\t\tclose();\n\t\t};\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = tr::lng_selected_delete_sure_this(),\n\t\t\t.confirmed = confirmed,\n\t\t\t.confirmText = tr::lng_box_delete(),\n\t\t\t.labelStyle = &st::groupCallBoxLabel,\n\t\t});\n\t\tif (canModerate) {\n\t\t\tconst auto check = [&](rpl::producer<QString> text) {\n\t\t\t\tconst auto add = st::groupCallCheckbox.margin;\n\t\t\t\tconst auto added = QMargins(0, add.top(), 0, add.bottom());\n\t\t\t\tconst auto margin = st::boxRowPadding + added;\n\n\t\t\t\treturn box->addRow(object_ptr<Ui::Checkbox>(\n\t\t\t\t\tbox,\n\t\t\t\t\tstd::move(text),\n\t\t\t\t\tfalse,\n\t\t\t\t\tst::groupCallCheckbox\n\t\t\t\t), margin)->checkedValue();\n\t\t\t};\n\t\t\tstate->report = check(tr::lng_report_spam());\n\t\t\tstate->all = check(tr::lng_delete_all_from_user(\n\t\t\t\tlt_user,\n\t\t\t\trpl::single(from->shortName()))),\n\t\t\tstate->ban = check(tr::lng_ban_user());\n\t\t}\n\t}));\n}\n\n[[nodiscard]] QImage CrownMask(int place) {\n\tconst auto &icon = st::paidReactCrownSmall;\n\tconst auto size = icon.size();\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto full = size * ratio;\n\tauto result = QImage(full, QImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::transparent);\n\tresult.setDevicePixelRatio(ratio);\n\n\tauto p = QPainter(&result);\n\ticon.paint(p, 0, 0, size.width(), QColor(255, 255, 255));\n\n\tconst auto top = st::paidReactCrownSmallTop;\n\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\tp.setPen(Qt::transparent);\n\tp.setFont(st::levelStyle.font);\n\tp.drawText(\n\t\tQRect(0, top, icon.width(), icon.height()),\n\t\tQString::number(place),\n\t\tstyle::al_top);\n\tp.end();\n\n\treturn result;\n}\n\n} // namespace\n\nstruct MessagesUi::MessageView {\n\tMsgId id = 0;\n\tMsgId sendingId = 0;\n\tnot_null<PeerData*> from;\n\tClickHandlerPtr fromLink;\n\tUi::Animations::Simple toggleAnimation;\n\tUi::Animations::Simple sentAnimation;\n\tData::ReactionId reactionId;\n\tstd::unique_ptr<Ui::InfiniteRadialAnimation> sendingAnimation;\n\tstd::unique_ptr<Ui::ReactionFlyAnimation> reactionAnimation;\n\tstd::unique_ptr<Ui::RpWidget> reactionWidget;\n\tQPoint reactionShift;\n\tTextWithEntities original;\n\tUi::PeerUserpicView view;\n\tUi::Text::String name;\n\tUi::Text::String text;\n\tUi::Text::String price;\n\tTimeId date = 0;\n\tint stars = 0;\n\tint place = 0;\n\tint top = 0;\n\tint width = 0;\n\tint left = 0;\n\tint height = 0;\n\tint realHeight = 0;\n\tint nameWidth = 0;\n\tint textLeft = 0;\n\tint textTop = 0;\n\tbool removed = false;\n\tbool sending = false;\n\tbool failed = false;\n\tbool simple = false;\n\tbool admin = false;\n\tbool mine = false;\n};\n\nstruct MessagesUi::PinnedView {\n\tMsgId id = 0;\n\tnot_null<PeerData*> from;\n\tUi::Animations::Simple toggleAnimation;\n\tUi::PeerUserpicView view;\n\tUi::Text::String text;\n\tcrl::time duration = 0;\n\tcrl::time end = 0;\n\tint stars = 0;\n\tint place = 0;\n\tint top = 0;\n\tint width = 0;\n\tint left = 0;\n\tint height = 0;\n\tint realWidth = 0;\n\tbool removed = false;\n\tbool requiresSmooth = false;\n};\n\nMessagesUi::PayedBg::PayedBg(const Ui::StarsColoring &coloring)\n: light(Ui::ColorFromSerialized(coloring.bgLight))\n, dark(Ui::ColorFromSerialized(coloring.bgDark))\n, pinnedLight(CountPinnedRadius(), light.color())\n, pinnedDark(CountPinnedRadius(), dark.color())\n, messageLight(CountMessageRadius(), light.color())\n, priceDark(CountPriceRadius(), dark.color())\n, badgeDark(st::roundRadiusLarge, dark.color()) {\n}\n\nMessagesUi::MessagesUi(\n\tnot_null<QWidget*> parent,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tMessagesMode mode,\n\trpl::producer<std::vector<Message>> messages,\n\trpl::producer<std::vector<not_null<PeerData*>>> topDonorsValue,\n\trpl::producer<MessageIdUpdate> idUpdates,\n\trpl::producer<bool> canManageValue,\n\trpl::producer<bool> shown)\n: _parent(parent)\n, _show(std::move(show))\n, _mode(mode)\n, _canManage(std::move(canManageValue))\n, _messageBg([] {\n\tauto result = st::groupCallBg->c;\n\tresult.setAlphaF(kMessageBgOpacity);\n\treturn result;\n})\n, _messageBgRect(CountMessageRadius(), _messageBg.color())\n, _crownHelper(Core::TextContext({ .session = &_show->session() }))\n, _topDonors(std::move(topDonorsValue))\n, _fadeHeight(st::normalFont->height * 2)\n, _fadeWidth(st::normalFont->height * 2)\n, _streamMode(_mode == MessagesMode::VideoStream) {\n\tsetupBadges();\n\tsetupList(std::move(messages), std::move(shown));\n\thandleIdUpdates(std::move(idUpdates));\n}\n\nMessagesUi::~MessagesUi() = default;\n\nvoid MessagesUi::setupBadges() {\n\tauto helper = Ui::Text::CustomEmojiHelper();\n\tconst auto liveText = helper.paletteDependent(\n\t\tUi::Text::CustomEmojiTextBadge(\n\t\t\ttr::lng_video_stream_live(tr::now).toUpper(),\n\t\t\tst::groupCallMessageBadge,\n\t\t\tst::groupCallMessageBadgeMargin));\n\t_liveBadge.setMarkedText(\n\t\tst::messageTextStyle,\n\t\tliveText,\n\t\tkMarkupTextOptions,\n\t\thelper.context());\n\n\t_adminBadge.setText(st::messageTextStyle, tr::lng_admin_badge(tr::now));\n\n\t_topDonors.value(\n\t) | rpl::on_next([=] {\n\t\tfor (auto &entry : _views) {\n\t\t\tconst auto place = donorPlace(entry.from);\n\t\t\tif (entry.place != place) {\n\t\t\t\tentry.place = place;\n\t\t\t\tif (!entry.failed) {\n\t\t\t\t\tsetContent(entry);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor (auto &entry : _pinnedViews) {\n\t\t\tconst auto place = donorPlace(entry.from);\n\t\t\tif (entry.place != place) {\n\t\t\t\tentry.place = place;\n\t\t\t\tsetContent(entry);\n\t\t\t}\n\t\t}\n\t\tapplyGeometry();\n\t}, _lifetime);\n}\n\nvoid MessagesUi::setupList(\n\t\trpl::producer<std::vector<Message>> messages,\n\t\trpl::producer<bool> shown) {\n\trpl::combine(\n\t\tstd::move(messages),\n\t\tstd::move(shown)\n\t) | rpl::on_next([=](std::vector<Message> &&list, bool shown) {\n\t\tif (shown) {\n\t\t\t_hidden = std::nullopt;\n\t\t} else {\n\t\t\t_hidden = base::take(list);\n\t\t}\n\t\tshowList(list);\n\t}, _lifetime);\n}\n\nvoid MessagesUi::showList(const std::vector<Message> &list) {\n\tconst auto now = base::unixtime::now();\n\tauto from = begin(list);\n\tauto till = end(list);\n\tfor (auto &entry : _views) {\n\t\tif (entry.removed) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto id = entry.id;\n\t\tconst auto i = ranges::find(\n\t\t\tfrom,\n\t\t\ttill,\n\t\t\tid,\n\t\t\t&Message::id);\n\t\tif (i == till) {\n\t\t\ttoggleMessage(entry, false);\n\t\t\tcontinue;\n\t\t} else if (entry.failed != i->failed) {\n\t\t\tsetContentFailed(entry);\n\t\t\tupdateMessageSize(entry);\n\t\t\trepaintMessage(entry.id);\n\t\t} else if (entry.sending != (i->date == 0)) {\n\t\t\tanimateMessageSent(entry);\n\t\t}\n\t\tentry.date = i->date;\n\t\tif (i == from) {\n\t\t\tappendPinned(*i, now);\n\t\t\t++from;\n\t\t}\n\t}\n\tauto addedSendingToBottom = false;\n\tfor (auto i = from; i != till; ++i) {\n\t\tconst auto j = ranges::find(_views, i->id, &MessageView::id);\n\t\tif (j != end(_views)) {\n\t\t\tif (!j->removed) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (j->failed != i->failed) {\n\t\t\t\tsetContentFailed(*j);\n\t\t\t\tupdateMessageSize(*j);\n\t\t\t\trepaintMessage(j->id);\n\t\t\t} else if (j->sending != (i->date == 0)) {\n\t\t\t\tanimateMessageSent(*j);\n\t\t\t}\n\t\t\tj->date = i->date;\n\t\t\ttoggleMessage(*j, true);\n\t\t} else {\n\t\t\tif (i + 1 == till && !i->date) {\n\t\t\t\taddedSendingToBottom = true;\n\t\t\t}\n\t\t\tappendMessage(*i);\n\t\t\tappendPinned(*i, now);\n\t\t}\n\t}\n\tif (addedSendingToBottom) {\n\t\tconst auto from = _scroll->scrollTop();\n\t\tconst auto till = _scroll->scrollTopMax();\n\t\tif (from >= till) {\n\t\t\treturn;\n\t\t}\n\t\t_scrollToAnimation.stop();\n\t\t_scrollToAnimation.start([=] {\n\t\t\t_scroll->scrollToY(_scroll->scrollTopMax()\n\t\t\t\t- _scrollToAnimation.value(0));\n\t\t}, till - from, 0, st::slideDuration, anim::easeOutCirc);\n\t}\n}\n\nvoid MessagesUi::handleIdUpdates(rpl::producer<MessageIdUpdate> idUpdates) {\n\tstd::move(\n\t\tidUpdates\n\t) | rpl::on_next([=](MessageIdUpdate update) {\n\t\tconst auto i = ranges::find(\n\t\t\t_views,\n\t\t\tupdate.localId,\n\t\t\t&MessageView::id);\n\t\tif (i == end(_views)) {\n\t\t\treturn;\n\t\t}\n\t\ti->sendingId = update.localId;\n\t\ti->id = update.realId;\n\t\tif (_revealedSpoilerId == update.localId) {\n\t\t\t_revealedSpoilerId = update.realId;\n\t\t}\n\t}, _lifetime);\n}\n\nvoid MessagesUi::animateMessageSent(MessageView &entry) {\n\tconst auto id = entry.id;\n\tentry.sending = false;\n\tentry.sentAnimation.start([=] {\n\t\trepaintMessage(id);\n\t}, 0., 1, st::slideDuration, anim::easeOutCirc);\n\trepaintMessage(id);\n}\n\nvoid MessagesUi::updateMessageSize(MessageView &entry) {\n\tconst auto &padding = st::groupCallMessagePadding;\n\tconst auto &pricePadding = st::groupCallPricePadding;\n\n\tconst auto hasUserpic = !entry.failed;\n\tconst auto userpicPadding = st::groupCallUserpicPadding;\n\tconst auto userpicSize = st::groupCallUserpic;\n\tconst auto leftSkip = hasUserpic\n\t\t? (userpicPadding.left() + userpicSize + userpicPadding.right())\n\t\t: padding.left();\n\tconst auto widthSkip = leftSkip + padding.right();\n\tconst auto inner = _width - widthSkip;\n\n\tconst auto size = Ui::Text::CountOptimalTextSize(\n\t\tentry.text,\n\t\tstd::min(st::groupCallWidth / 2, inner),\n\t\tinner);\n\tconst auto price = entry.simple\n\t\t? (pricePadding.left() + pricePadding.right() + entry.price.maxWidth())\n\t\t: 0;\n\tconst auto space = st::normalFont->spacew;\n\tconst auto nameWidth = entry.name.isEmpty() ? 0 : entry.name.maxWidth();\n\tconst auto nameLineWidth = nameWidth\n\t\t? (nameWidth\n\t\t\t+ space\n\t\t\t+ _liveBadge.maxWidth()\n\t\t\t+ space\n\t\t\t+ _adminBadge.maxWidth())\n\t\t: 0;\n\n\tconst auto nameHeight = entry.name.isEmpty()\n\t\t? 0\n\t\t: st::messageTextStyle.font->height;\n\tconst auto textHeight = size.height();\n\tentry.width = widthSkip\n\t\t+ std::max(size.width() + price, std::min(nameLineWidth, inner));\n\tentry.left = _streamMode ? 0 : (_width - entry.width) / 2;\n\tentry.textLeft = leftSkip;\n\tentry.textTop = padding.top() + nameHeight;\n\tentry.nameWidth = std::min(\n\t\tnameWidth,\n\t\t(entry.width\n\t\t\t- widthSkip\n\t\t\t- space\n\t\t\t- _liveBadge.maxWidth()\n\t\t\t- space\n\t\t\t- _adminBadge.maxWidth()));\n\tupdateReactionPosition(entry);\n\n\tconst auto contentHeight = entry.textTop + textHeight + padding.bottom();\n\tconst auto userpicHeight = hasUserpic\n\t\t? (userpicPadding.top() + userpicSize + userpicPadding.bottom())\n\t\t: 0;\n\n\tconst auto skip = st::groupCallMessageSkip;\n\tentry.realHeight = skip + std::max(contentHeight, userpicHeight);\n}\n\nbool MessagesUi::updateMessageHeight(MessageView &entry) {\n\tconst auto height = entry.toggleAnimation.animating()\n\t\t? anim::interpolate(\n\t\t\t0,\n\t\t\tentry.realHeight,\n\t\t\tentry.toggleAnimation.value(entry.removed ? 0. : 1.))\n\t\t: entry.realHeight;\n\tif (entry.height == height) {\n\t\treturn false;\n\t}\n\tentry.height = height;\n\treturn true;\n}\n\nvoid MessagesUi::updatePinnedSize(PinnedView &entry) {\n\tconst auto &padding = st::groupCallPinnedPadding;\n\n\tconst auto userpicPadding = st::groupCallUserpicPadding;\n\tconst auto userpicSize = st::groupCallPinnedUserpic;\n\tconst auto leftSkip = userpicPadding.left()\n\t\t+ userpicSize\n\t\t+ userpicPadding.right();\n\tconst auto inner = std::min(\n\t\tentry.text.maxWidth(),\n\t\tst::groupCallPinnedMaxWidth);\n\n\tentry.height = userpicPadding.top()\n\t\t+ userpicSize\n\t\t+ userpicPadding.bottom();\n\tentry.top = 0;\n\n\tconst auto skip = st::groupCallMessageSkip;\n\tentry.realWidth = skip + leftSkip + inner + padding.right();\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tentry.requiresSmooth = (entry.realWidth * ratio * 1000 > entry.duration);\n}\n\nbool MessagesUi::updatePinnedWidth(PinnedView &entry) {\n\tconst auto width = entry.toggleAnimation.animating()\n\t\t? anim::interpolate(\n\t\t\t0,\n\t\t\tentry.realWidth,\n\t\t\tentry.toggleAnimation.value(entry.removed ? 0. : 1.))\n\t\t: entry.realWidth;\n\tif (entry.width == width) {\n\t\treturn false;\n\t}\n\tentry.width = width;\n\treturn true;\n}\n\nvoid MessagesUi::setContentFailed(MessageView &entry) {\n\tentry.failed = true;\n\tentry.name = Ui::Text::String();\n\tentry.text = Ui::Text::String(\n\t\tst::messageTextStyle,\n\t\tTextWithEntities().append(\n\t\t\tQString::fromUtf8(\"\\xe2\\x9d\\x97\\xef\\xb8\\x8f\")\n\t\t).append(' ').append(\n\t\t\ttr::italic(u\"Failed to send the message.\"_q)),\n\t\tkMarkupTextOptions,\n\t\tst::groupCallWidth / 8);\n\tentry.price = Ui::Text::String();\n}\n\nvoid MessagesUi::setContent(MessageView &entry) {\n\tentry.simple = !entry.admin && entry.original.empty() && entry.stars > 0;\n\n\tconst auto name = nameText(entry.from, entry.place);\n\tentry.name = entry.admin\n\t\t? Ui::Text::String(\n\t\t\tst::messageTextStyle,\n\t\t\tname,\n\t\t\tkMarkupTextOptions,\n\t\t\tUi::kQFixedMax,\n\t\t\t_crownHelper.context())\n\t\t: Ui::Text::String();\n\tif (const auto stars = entry.stars) {\n\t\tentry.price = Ui::Text::String(\n\t\t\tentry.simple ? st::messageTextStyle : st::whoReadDateStyle,\n\t\t\tUi::Text::IconEmoji(\n\t\t\t\t&st::starIconEmojiSmall\n\t\t\t).append(Lang::FormatCountDecimal(stars)),\n\t\t\tkMarkupTextOptions);\n\t} else {\n\t\tentry.price = Ui::Text::String();\n\t}\n\tauto composed = entry.admin\n\t\t? entry.original\n\t\t: tr::link(name, 1).append(' ').append(entry.original);\n\tif (!entry.admin) {\n\t\tcomposed.text.replace(QChar('\\n'), QChar(' '));\n\t}\n\tentry.text = Ui::Text::String(\n\t\tst::messageTextStyle,\n\t\tcomposed,\n\t\tkMarkupTextOptions,\n\t\tst::groupCallWidth / 8,\n\t\t_crownHelper.context([this, id = entry.id] { repaintMessage(id); }));\n\tif (!entry.simple && !entry.price.isEmpty()) {\n\t\tentry.text.updateSkipBlock(\n\t\t\tentry.price.maxWidth(),\n\t\t\tst::normalFont->height);\n\t}\n\tif (!entry.simple && !entry.admin) {\n\t\tentry.text.setLink(1, entry.fromLink);\n\t}\n\tif (entry.text.hasSpoilers()) {\n\t\tconst auto id = entry.id;\n\t\tconst auto guard = base::make_weak(_messages);\n\t\tentry.text.setSpoilerLinkFilter([=](const ClickContext &context) {\n\t\t\tif (context.button != Qt::LeftButton || !guard) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto i = ranges::find(\n\t\t\t\t_views,\n\t\t\t\t_revealedSpoilerId,\n\t\t\t\t&MessageView::id);\n\t\t\tif (i != end(_views) && _revealedSpoilerId != id) {\n\t\t\t\ti->text.setSpoilerRevealed(false, anim::type::normal);\n\t\t\t}\n\t\t\t_revealedSpoilerId = id;\n\t\t\treturn true;\n\t\t});\n\t}\n}\n\nvoid MessagesUi::setContent(PinnedView &entry) {\n\tconst auto text = nameText(entry.from, entry.place);\n\tentry.text.setMarkedText(\n\t\tst::messageTextStyle,\n\t\ttext,\n\t\tkMarkupTextOptions,\n\t\t_crownHelper.context());\n}\n\nvoid MessagesUi::toggleMessage(MessageView &entry, bool shown) {\n\tconst auto id = entry.id;\n\tentry.removed = !shown;\n\tentry.toggleAnimation.start(\n\t\t[=] { repaintMessage(id); },\n\t\tshown ? 0. : 1.,\n\t\tshown ? 1. : 0.,\n\t\tst::slideWrapDuration,\n\t\tshown ? anim::easeOutCirc : anim::easeInCirc);\n\trepaintMessage(id);\n}\n\nvoid MessagesUi::repaintMessage(MsgId id) {\n\tauto i = ranges::find(_views, id, &MessageView::id);\n\tif (i == end(_views) && id < 0) {\n\t\ti = ranges::find(_views, id, &MessageView::sendingId);\n\t}\n\tif (i == end(_views)) {\n\t\treturn;\n\t} else if (i->removed && !i->toggleAnimation.animating()) {\n\t\tconst auto top = i->top;\n\t\ti = _views.erase(i);\n\t\trecountHeights(i, top);\n\t\treturn;\n\t}\n\tif (!i->sending && !i->sentAnimation.animating()) {\n\t\ti->sendingAnimation = nullptr;\n\t}\n\tif (!i->toggleAnimation.animating() && id == _delayedHighlightId) {\n\t\thighlightMessage(base::take(_delayedHighlightId));\n\t}\n\tif (i->toggleAnimation.animating() || i->height != i->realHeight) {\n\t\tif (updateMessageHeight(*i)) {\n\t\t\trecountHeights(i, i->top);\n\t\t\treturn;\n\t\t}\n\t}\n\t_messages->update(0, i->top, _messages->width(), i->height);\n}\n\nvoid MessagesUi::recountHeights(\n\t\tstd::vector<MessageView>::iterator i,\n\t\tint top) {\n\tauto from = top;\n\tfor (auto e = end(_views); i != e; ++i) {\n\t\ti->top = top;\n\t\ttop += i->height;\n\t\tupdateReactionPosition(*i);\n\t}\n\tif (_views.empty()) {\n\t\t_scrollToAnimation.stop();\n\t\tdelete base::take(_messages);\n\t\t_scroll = nullptr;\n\t} else {\n\t\tupdateGeometries();\n\t\t_messages->update(0, from, _messages->width(), top - from);\n\t}\n}\n\nvoid MessagesUi::appendMessage(const Message &data) {\n\tconst auto top = _views.empty()\n\t\t? 0\n\t\t: (_views.back().top + _views.back().height);\n\n\tif (!_scroll) {\n\t\tsetupMessagesWidget();\n\t}\n\n\tconst auto id = data.id;\n\tconst auto peer = data.peer;\n\tauto &entry = _views.emplace_back(MessageView{\n\t\t.id = id,\n\t\t.from = peer,\n\t\t.original = data.text,\n\t\t.date = data.date,\n\t\t.stars = data.stars,\n\t\t.place = donorPlace(peer),\n\t\t.top = top,\n\t\t.sending = !data.date,\n\t\t.admin = data.admin && _streamMode,\n\t\t.mine = data.mine,\n\t});\n\tconst auto repaint = [=] {\n\t\trepaintMessage(id);\n\t};\n\tentry.fromLink = std::make_shared<LambdaClickHandler>([=] {\n\t\t_show->show(\n\t\t\tPrepareShortInfoBox(peer, _show, &st::storiesShortInfoBox));\n\t});\n\tif (data.failed) {\n\t\tsetContentFailed(entry);\n\t} else {\n\t\tsetContent(entry);\n\t}\n\tupdateMessageSize(entry);\n\tif (entry.sending) {\n\t\tusing namespace Ui;\n\t\tconst auto &st = st::defaultInfiniteRadialAnimation;\n\t\tentry.sendingAnimation = std::make_unique<InfiniteRadialAnimation>(\n\t\t\trepaint,\n\t\t\tst);\n\t\tentry.sendingAnimation->start(0);\n\t}\n\tentry.height = 0;\n\ttoggleMessage(entry, true);\n\tcheckReactionContent(entry, data.text);\n}\n\nvoid MessagesUi::togglePinned(PinnedView &entry, bool shown) {\n\tconst auto id = entry.id;\n\tentry.removed = !shown;\n\tentry.toggleAnimation.start(\n\t\t[=] { repaintPinned(id); },\n\t\tshown ? 0. : 1.,\n\t\tshown ? 1. : 0.,\n\t\tst::slideWrapDuration,\n\t\tshown ? anim::easeOutCirc : anim::easeInCirc);\n\trepaintPinned(id);\n}\n\nvoid MessagesUi::repaintPinned(MsgId id) {\n\tconst auto i = ranges::find(_pinnedViews, id, &PinnedView::id);\n\tif (i == end(_pinnedViews)) {\n\t\treturn;\n\t} else if (i->removed && !i->toggleAnimation.animating()) {\n\t\tconst auto left = i->left;\n\t\trecountWidths(_pinnedViews.erase(i), left);\n\t\treturn;\n\t}\n\tif (i->toggleAnimation.animating() || i->width != i->realWidth) {\n\t\tconst auto was = i->width;\n\t\tif (updatePinnedWidth(*i)) {\n\t\t\tif (i->width > was) {\n\t\t\t\tconst auto larger = countPinnedScrollSkip(*i);\n\t\t\t\tif (larger > _pinnedScrollSkip) {\n\t\t\t\t\tsetPinnedScrollSkip(larger);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tapplyGeometryToPinned();\n\t\t\t}\n\t\t\trecountWidths(i, i->left);\n\t\t\treturn;\n\t\t}\n\t}\n\t_pinned->update(i->left, 0, i->width, _pinned->height());\n}\n\nvoid MessagesUi::recountWidths(\n\t\tstd::vector<PinnedView>::iterator i,\n\t\tint left) {\n\tauto from = left;\n\tfor (auto e = end(_pinnedViews); i != e; ++i) {\n\t\ti->left = left;\n\t\tleft += i->width;\n\t}\n\tif (_pinnedViews.empty()) {\n\t\tdelete base::take(_pinned);\n\t\t_pinnedScroll = nullptr;\n\t} else {\n\t\tupdateGeometries();\n\t\t_pinned->update(from, 0, left - from, _pinned->height());\n\t}\n}\n\nvoid MessagesUi::appendPinned(const Message &data, TimeId now) {\n\tif (!data.date\n\t\t|| data.pinFinishDate <= data.date\n\t\t|| data.pinFinishDate <= now\n\t\t|| ranges::contains(\n\t\t\t_pinnedViews,\n\t\t\tdata.id,\n\t\t\t&PinnedView::id)) {\n\t\treturn;\n\t}\n\n\tconst auto peer = data.peer;\n\tconst auto finishes = crl::now()\n\t\t+ (data.pinFinishDate - now) * crl::time(1000);\n\tconst auto i = ranges::find(_pinnedViews, peer, &PinnedView::from);\n\tif (i != end(_pinnedViews)) {\n\t\tif (i->end > finishes) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto left = i->left;\n\t\trecountWidths(_pinnedViews.erase(i), left);\n\t}\n\n\tif (!_pinnedScroll) {\n\t\tsetupPinnedWidget();\n\t}\n\tconst auto j = ranges::lower_bound(\n\t\t_pinnedViews,\n\t\tdata.stars,\n\t\tranges::greater(),\n\t\t&PinnedView::stars);\n\tconst auto left = (j != end(_pinnedViews))\n\t\t? j->left\n\t\t: _pinnedViews.empty()\n\t\t? 0\n\t\t: (_pinnedViews.back().left + _pinnedViews.back().width);\n\tauto &entry = *_pinnedViews.insert(j, PinnedView{\n\t\t.id = data.id,\n\t\t.from = peer,\n\t\t.duration = (data.pinFinishDate - data.date) * crl::time(1000),\n\t\t.end = finishes,\n\t\t.stars = data.stars,\n\t\t.place = donorPlace(peer),\n\t\t.left = left,\n\t});\n\tsetContent(entry);\n\tupdatePinnedSize(entry);\n\tentry.width = 0;\n\ttogglePinned(entry, true);\n}\n\nint MessagesUi::donorPlace(not_null<PeerData*> peer) const {\n\tconst auto &donors = _topDonors.current();\n\tconst auto i = ranges::find(donors, peer);\n\tif (i == end(donors)) {\n\t\treturn 0;\n\t}\n\treturn static_cast<int>(std::distance(begin(donors), i)) + 1;\n}\n\nTextWithEntities MessagesUi::nameText(\n\t\tnot_null<PeerData*> peer,\n\t\tint place) {\n\tauto result = TextWithEntities();\n\tif (place > 0) {\n\t\tauto i = _crownEmojiDataCache.find(place);\n\t\tif (i == _crownEmojiDataCache.end()) {\n\t\t\ti = _crownEmojiDataCache.emplace(\n\t\t\t\tplace,\n\t\t\t\t_crownHelper.imageData(Ui::Text::ImageEmoji{\n\t\t\t\t\t.image = CrownMask(place),\n\t\t\t\t\t.margin = st::paidReactCrownMargin,\n\t\t\t\t})).first;\n\t\t}\n\t\tresult.append(Ui::Text::SingleCustomEmoji(i->second)).append(' ');\n\t}\n\tresult.append(tr::bold(peer->shortName()));\n\treturn result;\n}\n\nvoid MessagesUi::checkReactionContent(\n\t\tMessageView &entry,\n\t\tconst TextWithEntities &text) {\n\tauto outLength = 0;\n\tusing Type = Data::Reactions::Type;\n\tconst auto reactions = &_show->session().data().reactions();\n\tconst auto set = [&](Data::ReactionId id) {\n\t\treactions->preloadAnimationsFor(id);\n\t\tentry.reactionId = std::move(id);\n\t};\n\tif (text.entities.size() == 1\n\t\t&& text.entities.front().type() == EntityType::CustomEmoji\n\t\t&& text.entities.front().offset() == 0\n\t\t&& text.entities.front().length() == text.text.size()) {\n\t\tset({ text.entities.front().data().toULongLong() });\n\t} else if (const auto emoji = Ui::Emoji::Find(text.text, &outLength)) {\n\t\tif (outLength < text.text.size()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto &all = reactions->list(Type::All);\n\t\tfor (const auto &reaction : all) {\n\t\t\tif (reaction.id.custom()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto &text = reaction.id.emoji();\n\t\t\tif (Ui::Emoji::Find(text) != emoji) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tset(reaction.id);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nvoid MessagesUi::startReactionAnimation(MessageView &entry) {\n\tentry.reactionWidget = std::make_unique<Ui::RpWidget>(_parent);\n\tconst auto raw = entry.reactionWidget.get();\n\traw->show();\n\traw->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tif (!_effectsLifetime) {\n\t\trpl::combine(\n\t\t\t_scroll->scrollTopValue(),\n\t\t\t_scroll->RpWidget::positionValue()\n\t\t) | rpl::on_next([=](int yshift, QPoint point) {\n\t\t\t_reactionBasePosition = point - QPoint(0, yshift);\n\t\t\tfor (auto &view : _views) {\n\t\t\t\tupdateReactionPosition(view);\n\t\t\t}\n\t\t}, _effectsLifetime);\n\t}\n\n\tentry.reactionAnimation = std::make_unique<Ui::ReactionFlyAnimation>(\n\t\t&_show->session().data().reactions(),\n\t\tUi::ReactionFlyAnimationArgs{\n\t\t\t.id = entry.reactionId,\n\t\t\t.effectOnly = true,\n\t\t},\n\t\t[=] { raw->update(); },\n\t\tst::reactionInlineImage);\n\tupdateReactionPosition(entry);\n\n\tconst auto effectSize = st::reactionInlineImage * 2;\n\tconst auto animation = entry.reactionAnimation.get();\n\traw->resize(effectSize, effectSize);\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tif (animation->finished()) {\n\t\t\tcrl::on_main(raw, [=] {\n\t\t\t\tremoveReaction(raw);\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tauto p = QPainter(raw);\n\t\tconst auto size = raw->width();\n\t\tconst auto skip = (size - st::reactionInlineImage) / 2;\n\t\tconst auto target = QRect(\n\t\t\tQPoint(skip, skip),\n\t\t\tQSize(st::reactionInlineImage, st::reactionInlineImage));\n\t\tanimation->paintGetArea(\n\t\t\tp,\n\t\t\tQPoint(),\n\t\t\ttarget,\n\t\t\tst::radialFg->c,\n\t\t\tQRect(),\n\t\t\tcrl::now());\n\t}, raw->lifetime());\n}\n\nvoid MessagesUi::removeReaction(not_null<Ui::RpWidget*> widget) {\n\tconst auto i = ranges::find_if(_views, [&](const MessageView &entry) {\n\t\treturn entry.reactionWidget.get() == widget;\n\t});\n\tif (i != end(_views)) {\n\t\ti->reactionId = {};\n\t\ti->reactionWidget = nullptr;\n\t\ti->reactionAnimation = nullptr;\n\t};\n}\n\nvoid MessagesUi::updateReactionPosition(MessageView &entry) {\n\tif (const auto widget = entry.reactionWidget.get()) {\n\t\tif (entry.failed) {\n\t\t\twidget->resize(0, 0);\n\t\t\treturn;\n\t\t}\n\t\tconst auto padding = st::groupCallMessagePadding;\n\t\tconst auto userpicSize = st::groupCallUserpic;\n\t\tconst auto userpicPadding = st::groupCallUserpicPadding;\n\t\tconst auto esize = st::emojiSize;\n\t\tconst auto eleft = entry.text.maxWidth() - st::emojiPadding - esize;\n\t\tconst auto etop = (st::normalFont->height - esize) / 2;\n\t\tconst auto effectSize = st::reactionInlineImage * 2;\n\t\tentry.reactionShift = QPoint(entry.left, entry.top)\n\t\t\t+ QPoint(\n\t\t\t\tuserpicPadding.left() + userpicSize + userpicPadding.right(),\n\t\t\t\tpadding.top())\n\t\t\t+ QPoint(eleft + (esize / 2), etop + (esize / 2))\n\t\t\t- QPoint(effectSize / 2, effectSize / 2);\n\t\twidget->move(_reactionBasePosition + entry.reactionShift);\n\t}\n}\n\nvoid MessagesUi::updateTopFade() {\n\tconst auto topFadeShown = (_scroll->scrollTop() > 0);\n\tif (_topFadeShown != topFadeShown) {\n\t\t_topFadeShown = topFadeShown;\n\t\t//const auto from = topFadeShown ? 0. : 1.;\n\t\t//const auto till = topFadeShown ? 1. : 0.;\n\t\t//_topFadeAnimation.start([=] {\n\t\t\t_messages->update(\n\t\t\t\t0,\n\t\t\t\t_scroll->scrollTop(),\n\t\t\t\t_messages->width(),\n\t\t\t\t_fadeHeight);\n\t\t//}, from, till, st::slideWrapDuration);\n\t}\n}\n\nvoid MessagesUi::updateBottomFade() {\n\tconst auto max = _scroll->scrollTopMax();\n\tconst auto bottomFadeShown = (_scroll->scrollTop() < max);\n\tif (_bottomFadeShown != bottomFadeShown) {\n\t\t_bottomFadeShown = bottomFadeShown;\n\t\t//const auto from = bottomFadeShown ? 0. : 1.;\n\t\t//const auto till = bottomFadeShown ? 1. : 0.;\n\t\t//_bottomFadeAnimation.start([=] {\n\t\t\t_messages->update(\n\t\t\t\t0,\n\t\t\t\t_scroll->scrollTop() + _scroll->height() - _fadeHeight,\n\t\t\t\t_messages->width(),\n\t\t\t\t_fadeHeight);\n\t\t//}, from, till, st::slideWrapDuration);\n\t}\n}\n\nvoid MessagesUi::updateLeftFade() {\n\tconst auto leftFadeShown = (_pinnedScroll->scrollLeft() > 0);\n\tif (_leftFadeShown != leftFadeShown) {\n\t\t_leftFadeShown = leftFadeShown;\n\t\t//const auto from = leftFadeShown ? 0. : 1.;\n\t\t//const auto till = rightFadeShown ? 1. : 0.;\n\t\t//_leftFadeAnimation.start([=] {\n\t\t\t_pinned->update(\n\t\t\t\t_pinnedScroll->scrollLeft(),\n\t\t\t\t0,\n\t\t\t\t_fadeWidth,\n\t\t\t\t_pinned->height());\n\t\t//}, from, till, st::slideWrapDuration);\n\t}\n}\n\nvoid MessagesUi::updateRightFade() {\n\tconst auto max = _pinnedScroll->scrollLeftMax();\n\tconst auto rightFadeShown = (_pinnedScroll->scrollLeft() < max);\n\tif (_rightFadeShown != rightFadeShown) {\n\t\t_rightFadeShown = rightFadeShown;\n\t\t//const auto from = rightFadeShown ? 0. : 1.;\n\t\t//const auto till = rightFadeShown ? 1. : 0.;\n\t\t//_rightFadeAnimation.start([=] {\n\t\t\t_pinned->update(\n\t\t\t\t(_pinnedScroll->scrollLeft()\n\t\t\t\t\t+ _pinnedScroll->width()\n\t\t\t\t\t- _fadeWidth),\n\t\t\t\t0,\n\t\t\t\t_fadeWidth,\n\t\t\t\t_pinned->height());\n\t\t//}, from, till, st::slideWrapDuration);\n\t}\n}\n\nvoid MessagesUi::setupMessagesWidget() {\n\t_scroll = std::make_unique<Ui::ElasticScroll>(\n\t\t_parent,\n\t\tst::groupCallMessagesScroll);\n\tconst auto scroll = _scroll.get();\n\n\t_messages = scroll->setOwnedWidget(object_ptr<Ui::RpWidget>(scroll));\n\t_messages->move(0, 0);\n\trpl::combine(\n\t\tscroll->scrollTopValue(),\n\t\tscroll->heightValue(),\n\t\t_messages->heightValue()\n\t) | rpl::on_next([=] {\n\t\tupdateTopFade();\n\t\tupdateBottomFade();\n\t}, scroll->lifetime());\n\n\tif (_mode == MessagesMode::GroupCall) {\n\t\treceiveSomeMouseEvents();\n\t} else {\n\t\treceiveAllMouseEvents();\n\t}\n\n\t_messages->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tconst auto start = scroll->scrollTop();\n\t\tconst auto end = start + scroll->height();\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto session = &_show->session();\n\t\tconst auto &colorings = session->appConfig().groupCallColorings();\n\n\t\tif ((_canvas.width() < scroll->width() * ratio)\n\t\t\t|| (_canvas.height() < scroll->height() * ratio)) {\n\t\t\t_canvas = QImage(\n\t\t\t\tscroll->size() * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_canvas.setDevicePixelRatio(ratio);\n\t\t}\n\t\tauto p = Painter(&_canvas);\n\n\t\tp.setCompositionMode(QPainter::CompositionMode_Clear);\n\t\tp.fillRect(QRect(QPoint(), scroll->size()), QColor(0, 0, 0, 0));\n\n\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\tconst auto now = crl::now();\n\t\tconst auto skip = st::groupCallMessageSkip;\n\t\tconst auto padding = st::groupCallMessagePadding;\n\t\tp.translate(0, -start);\n\t\tfor (auto &entry : _views) {\n\t\t\tif (entry.height <= skip || entry.top + entry.height <= start) {\n\t\t\t\tcontinue;\n\t\t\t} else if (entry.top >= end) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst auto x = entry.left;\n\t\t\tconst auto y = entry.top;\n\t\t\tconst auto use = entry.realHeight - skip;\n\t\t\tconst auto width = entry.width;\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tconst auto scaled = (entry.height < entry.realHeight);\n\t\t\tif (scaled) {\n\t\t\t\tconst auto used = entry.height - skip;\n\t\t\t\tconst auto mx = scaled ? (x + (width / 2)) : 0;\n\t\t\t\tconst auto my = scaled ? (y + (used / 2)) : 0;\n\t\t\t\tconst auto scale = used / float64(use);\n\t\t\t\tp.save();\n\t\t\t\tp.translate(mx, my);\n\t\t\t\tp.scale(scale, scale);\n\t\t\t\tp.setOpacity(scale);\n\t\t\t\tp.translate(-mx, -my);\n\t\t\t}\n\t\t\tauto bg = (std::unique_ptr<PayedBg>*)nullptr;\n\t\t\tif (!_streamMode) {\n\t\t\t\t_messageBgRect.paint(p, { x, y, width, use });\n\t\t\t} else if (entry.stars) {\n\t\t\t\tconst auto coloring = Ui::StarsColoringForCount(\n\t\t\t\t\tcolorings,\n\t\t\t\t\tentry.stars);\n\t\t\t\tbg = &_bgs[ColoringKey(coloring)];\n\t\t\t\tif (!*bg) {\n\t\t\t\t\t*bg = std::make_unique<PayedBg>(coloring);\n\t\t\t\t}\n\t\t\t\tp.setOpacity(kColoredMessageBgOpacity);\n\t\t\t\t(*bg)->messageLight.paint(p, { x, y, width, use });\n\t\t\t\tp.setOpacity(1.);\n\t\t\t\tif (_highlightAnimation.animating()\n\t\t\t\t\t&& entry.id == _highlightId) {\n\t\t\t\t\tconst auto radius = CountMessageRadius();\n\t\t\t\t\tconst auto progress = _highlightAnimation.value(3.);\n\t\t\t\t\tp.setBrush(st::white);\n\t\t\t\t\tp.setOpacity(\n\t\t\t\t\t\tstd::min((1.5 - std::abs(1.5 - progress)), 1.));\n\t\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\t\tp.drawRoundedRect(x, y, width, use, radius, radius);\n\t\t\t\t\tp.setOpacity(1.);\n\t\t\t\t}\n\t\t\t} else if (entry.admin) {\n\t\t\t\t_messageBgRect.paint(p, { x, y, width, use });\n\t\t\t}\n\n\t\t\tconst auto textLeft = entry.textLeft;\n\t\t\tconst auto hasUserpic = !entry.failed;\n\t\t\tif (hasUserpic) {\n\t\t\t\tconst auto userpicSize = st::groupCallUserpic;\n\t\t\t\tconst auto userpicPadding = st::groupCallUserpicPadding;\n\t\t\t\tconst auto position = QPoint(\n\t\t\t\t\tx + userpicPadding.left(),\n\t\t\t\t\ty + userpicPadding.top());\n\t\t\t\tconst auto rect = QRect(\n\t\t\t\t\tposition,\n\t\t\t\t\tQSize(userpicSize, userpicSize));\n\t\t\t\tentry.from->paintUserpic(p, entry.view, {\n\t\t\t\t\t.position = position,\n\t\t\t\t\t.size = userpicSize,\n\t\t\t\t\t.shape = Ui::PeerUserpicShape::Circle,\n\t\t\t\t});\n\t\t\t\tif (const auto animation = entry.sendingAnimation.get()) {\n\t\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\t\tauto pen = st::groupCallBg->p;\n\t\t\t\t\tconst auto shift = userpicPadding.left();\n\t\t\t\t\tpen.setWidthF(shift);\n\t\t\t\t\tp.setPen(pen);\n\t\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t\t\tconst auto state = animation->computeState();\n\t\t\t\t\tconst auto sent = entry.sending\n\t\t\t\t\t\t? 0.\n\t\t\t\t\t\t: entry.sentAnimation.value(1.);\n\t\t\t\t\tp.setOpacity(state.shown * (1. - sent));\n\t\t\t\t\tp.drawArc(\n\t\t\t\t\t\trect.marginsRemoved({ shift, shift, shift, shift }),\n\t\t\t\t\t\tstate.arcFrom,\n\t\t\t\t\t\tstate.arcLength);\n\t\t\t\t\tp.setOpacity(1.);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tp.setPen(st::white);\n\t\t\tif (!entry.name.isEmpty()) {\n\t\t\t\tconst auto space = st::normalFont->spacew;\n\t\t\t\tentry.name.draw(p, {\n\t\t\t\t\t.position = {\n\t\t\t\t\t\tx + textLeft,\n\t\t\t\t\t\ty + padding.top(),\n\t\t\t\t\t},\n\t\t\t\t\t.availableWidth = entry.nameWidth,\n\t\t\t\t\t.palette = &st::groupCallMessagePalette,\n\t\t\t\t\t.elisionLines = 1,\n\t\t\t\t});\n\t\t\t\tconst auto liveLeft = x + textLeft + entry.nameWidth + space;\n\t\t\t\t_liveBadge.draw(p, {\n\t\t\t\t\t.position = { liveLeft, y + padding.top() },\n\t\t\t\t});\n\n\t\t\t\tp.setOpacity(kAdminBadgeTextOpacity);\n\t\t\t\tconst auto adminLeft = x\n\t\t\t\t\t+ entry.width\n\t\t\t\t\t- padding.right()\n\t\t\t\t\t- _adminBadge.maxWidth();\n\t\t\t\t_adminBadge.draw(p, {\n\t\t\t\t\t.position = { adminLeft, y + padding.top() },\n\t\t\t\t});\n\t\t\t\tp.setOpacity(1.);\n\t\t\t}\n\t\t\tconst auto pricePadding = st::groupCallPricePadding;\n\t\t\tconst auto textRight = padding.right()\n\t\t\t\t+ (entry.simple\n\t\t\t\t\t? (entry.price.maxWidth()\n\t\t\t\t\t\t+ pricePadding.left()\n\t\t\t\t\t\t+ pricePadding.right())\n\t\t\t\t\t: 0);\n\t\t\tentry.text.draw(p, {\n\t\t\t\t.position = {\n\t\t\t\t\tx + textLeft,\n\t\t\t\t\ty + entry.textTop,\n\t\t\t\t},\n\t\t\t\t.availableWidth = entry.width - textLeft - textRight,\n\t\t\t\t.palette = &st::groupCallMessagePalette,\n\t\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t\t.now = now,\n\t\t\t\t.paused = !_messages->window()->isActiveWindow(),\n\t\t\t});\n\t\t\tif (!entry.price.isEmpty()) {\n\t\t\t\tconst auto priceRight = x\n\t\t\t\t\t+ entry.width\n\t\t\t\t\t- entry.price.maxWidth();\n\t\t\t\tconst auto priceLeft = entry.simple\n\t\t\t\t\t? (priceRight\n\t\t\t\t\t\t- (padding.top() - pricePadding.top())\n\t\t\t\t\t\t- pricePadding.right())\n\t\t\t\t\t: (priceRight - (padding.right() / 2));\n\t\t\t\tconst auto priceTop = entry.simple\n\t\t\t\t\t? (y + entry.textTop)\n\t\t\t\t\t: (y + use - st::normalFont->height);\n\t\t\t\tif (entry.simple && bg) {\n\t\t\t\t\tp.setOpacity(kDarkOverOpacity);\n\t\t\t\t\tconst auto r = QRect(\n\t\t\t\t\t\tpriceLeft,\n\t\t\t\t\t\tpriceTop,\n\t\t\t\t\t\tentry.price.maxWidth(),\n\t\t\t\t\t\tst::normalFont->height\n\t\t\t\t\t).marginsAdded(pricePadding);\n\t\t\t\t\t(*bg)->priceDark.paint(p, r);\n\t\t\t\t\tp.setOpacity(1.);\n\t\t\t\t}\n\t\t\t\tentry.price.draw(p, {\n\t\t\t\t\t.position = { priceLeft, priceTop },\n\t\t\t\t\t.availableWidth = entry.price.maxWidth(),\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (!scaled && entry.reactionId && !entry.reactionAnimation) {\n\t\t\t\tstartReactionAnimation(entry);\n\t\t\t}\n\n\t\t\tif (scaled) {\n\t\t\t\tp.restore();\n\t\t\t}\n\t\t}\n\t\tp.translate(0, start);\n\n\t\tp.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\tp.setPen(Qt::NoPen);\n\n\t\tconst auto topFade = (//_topFadeAnimation.value(\n\t\t\t_topFadeShown ? 1. : 0.);\n\t\tif (topFade) {\n\t\t\tauto gradientTop = QLinearGradient(0, 0, 0, _fadeHeight);\n\t\t\tgradientTop.setStops({\n\t\t\t\t{ 0., QColor(255, 255, 255, 0) },\n\t\t\t\t{ 1., QColor(255, 255, 255, 255) },\n\t\t\t});\n\t\t\tp.setOpacity(topFade);\n\t\t\tp.setBrush(gradientTop);\n\t\t\tp.drawRect(0, 0, scroll->width(), _fadeHeight);\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t\tconst auto bottomFade = (//_bottomFadeAnimation.value(\n\t\t\t_bottomFadeShown ? 1. : 0.);\n\t\tif (bottomFade) {\n\t\t\tconst auto till = scroll->height();\n\t\t\tconst auto from = till - _fadeHeight;\n\t\t\tauto gradientBottom = QLinearGradient(0, from, 0, till);\n\t\t\tgradientBottom.setStops({\n\t\t\t\t{ 0., QColor(255, 255, 255, 255) },\n\t\t\t\t{ 1., QColor(255, 255, 255, 0) },\n\t\t\t});\n\t\t\tp.setBrush(gradientBottom);\n\t\t\tp.drawRect(0, from, scroll->width(), _fadeHeight);\n\t\t}\n\t\tQPainter(_messages).drawImage(\n\t\t\tQRect(QPoint(0, start), scroll->size()),\n\t\t\t_canvas,\n\t\t\tQRect(QPoint(), scroll->size() * ratio));\n\t}, _messages->lifetime());\n\n\tscroll->show();\n\tapplyGeometry();\n}\n\nvoid MessagesUi::receiveSomeMouseEvents() {\n\tReceiveSomeMouseEvents(_scroll.get(), [=](QPoint point) {\n\t\tfor (const auto &entry : _views) {\n\t\t\tif (entry.failed || entry.top + entry.height <= point.y()) {\n\t\t\t\tcontinue;\n\t\t\t} else if (entry.top < point.y()\n\t\t\t\t&& entry.left < point.x()\n\t\t\t\t&& entry.left + entry.width > point.x()) {\n\t\t\t\thandleClick(entry, point);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\treturn false;\n\t});\n}\n\nvoid MessagesUi::receiveAllMouseEvents() {\n\t_messages->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type != QEvent::MouseButtonPress) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto m = static_cast<QMouseEvent*>(e.get());\n\t\tconst auto point = m->pos();\n\t\tfor (const auto &entry : _views) {\n\t\t\tif (entry.failed || entry.top + entry.height <= point.y()) {\n\t\t\t\tcontinue;\n\t\t\t} else if (entry.top < point.y()\n\t\t\t\t&& entry.left < point.x()\n\t\t\t\t&& entry.left + entry.width > point.x()) {\n\t\t\t\tif (m->button() == Qt::LeftButton) {\n\t\t\t\t\thandleClick(entry, point);\n\t\t\t\t} else {\n\t\t\t\t\tshowContextMenu(entry, m->globalPos());\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}, _messages->lifetime());\n}\n\nvoid MessagesUi::handleClick(const MessageView &entry, QPoint point) {\n\tconst auto padding = st::groupCallMessagePadding;\n\tconst auto userpicSize = st::groupCallUserpic;\n\tconst auto userpicPadding = st::groupCallUserpicPadding;\n\tconst auto userpic = QRect(\n\t\tentry.left + userpicPadding.left(),\n\t\tentry.top + userpicPadding.top(),\n\t\tuserpicSize,\n\t\tuserpicSize);\n\tconst auto name = entry.name.isEmpty()\n\t\t? QRect()\n\t\t: QRect(\n\t\t\tentry.left + entry.textLeft,\n\t\t\tentry.top + padding.top(),\n\t\t\tentry.nameWidth,\n\t\t\tst::messageTextStyle.font->height);\n\tconst auto link = (userpic.contains(point) || name.contains(point))\n\t\t? entry.fromLink\n\t\t: entry.text.getState(point - QPoint(\n\t\t\tentry.left + entry.textLeft,\n\t\t\tentry.top + entry.textTop\n\t\t), entry.width - entry.textLeft - padding.right()).link;\n\tif (link) {\n\t\tActivateClickHandler(_messages, link, Qt::LeftButton);\n\t}\n}\n\nvoid MessagesUi::showContextMenu(\n\t\tconst MessageView &entry,\n\t\tQPoint globalPoint) {\n\tif (_menu || !entry.date || entry.failed) {\n\t\treturn;\n\t}\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t_parent,\n\t\tst::groupCallPopupMenuWithIcons);\n\t_menu->addAction(MakeMessageDateAction(_menu.get(), entry.date));\n\tconst auto &original = entry.original;\n\tconst auto canCopy = !original.empty();\n\tconst auto canDelete = entry.mine || _canManage.current();\n\tif (canCopy || canDelete) {\n\t\t_menu->addSeparator(&st::mediaviewWideMenuSeparator);\n\t}\n\tif (canCopy) {\n\t\t_menu->addAction(tr::lng_context_copy_text(tr::now), [=] {\n\t\t\tTextUtilities::SetClipboardText(\n\t\t\t\tTextForMimeData::WithExpandedLinks(original));\n\t\t}, &st::mediaMenuIconCopy);\n\t}\n\tif (canDelete) {\n\t\tconst auto id = entry.id;\n\t\tconst auto from = entry.from;\n\t\tconst auto canModerate = _canManage.current() && !entry.mine;\n\t\t_menu->addAction(tr::lng_context_delete_msg(tr::now), [=] {\n\t\t\tShowDeleteMessageConfirmation(_show, id, from, canModerate, [=](\n\t\t\t\t\tMessageDeleteRequest request) {\n\t\t\t\t_deleteRequests.fire_copy(request);\n\t\t\t});\n\t\t}, &st::mediaMenuIconDelete);\n\t}\n\t_menu->popup(globalPoint);\n}\n\nvoid MessagesUi::setupPinnedWidget() {\n\t_pinnedScroll = std::make_unique<Ui::ElasticScroll>(\n\t\t_parent,\n\t\tst::groupCallMessagesScroll,\n\t\tQt::Horizontal);\n\tconst auto scroll = _pinnedScroll.get();\n\n\t_pinned = scroll->setOwnedWidget(object_ptr<Ui::RpWidget>(scroll));\n\t_pinned->move(0, 0);\n\trpl::combine(\n\t\tscroll->scrollLeftValue(),\n\t\tscroll->widthValue(),\n\t\t_pinned->widthValue()\n\t) | rpl::on_next([=] {\n\t\tupdateLeftFade();\n\t\tupdateRightFade();\n\t}, scroll->lifetime());\n\n\tstruct Animation {\n\t\tbase::Timer seconds;\n\t\tUi::Animations::Simple smooth;\n\t\tbool requiresSmooth = false;\n\t};\n\tconst auto animation = _pinned->lifetime().make_state<Animation>();\n\tanimation->seconds.setCallback([=] {\n\t\tconst auto now = crl::now();\n\t\tauto smooth = false;\n\t\tauto off = base::flat_set<MsgId>();\n\t\tfor (auto &entry : _pinnedViews) {\n\t\t\tif (entry.removed) {\n\t\t\t\tcontinue;\n\t\t\t} else if (entry.end <= now) {\n\t\t\t\toff.emplace(entry.id);\n\t\t\t\tentry.requiresSmooth = false;\n\t\t\t} else if (entry.requiresSmooth) {\n\t\t\t\tsmooth = true;\n\t\t\t}\n\t\t}\n\t\tif (smooth && !anim::Disabled()) {\n\t\t\tanimation->smooth.start([=] {\n\t\t\t\t_pinned->update();\n\t\t\t}, 0., 1., 900.);\n\t\t} else {\n\t\t\t_pinned->update();\n\t\t}\n\t\tfor (const auto &id : off) {\n\t\t\tconst auto i = ranges::find(_pinnedViews, id, &PinnedView::id);\n\t\t\tif (i != end(_pinnedViews)) {\n\t\t\t\ttogglePinned(*i, false);\n\t\t\t}\n\t\t}\n\t});\n\tanimation->seconds.callEach(crl::time(1000));\n\n\t_pinned->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tconst auto session = &_show->session();\n\t\tconst auto &colorings = session->appConfig().groupCallColorings();\n\t\tconst auto start = scroll->scrollLeft();\n\t\tconst auto end = start + scroll->width();\n\t\tconst auto ratio = style::DevicePixelRatio();\n\n\t\tif ((_pinnedCanvas.width() < scroll->width() * ratio)\n\t\t\t|| (_pinnedCanvas.height() < scroll->height() * ratio)) {\n\t\t\t_pinnedCanvas = QImage(\n\t\t\t\tscroll->size() * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_pinnedCanvas.setDevicePixelRatio(ratio);\n\t\t}\n\t\tauto p = Painter(&_pinnedCanvas);\n\n\t\tp.setCompositionMode(QPainter::CompositionMode_Clear);\n\t\tp.fillRect(QRect(QPoint(), scroll->size()), QColor(0, 0, 0, 0));\n\n\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\tconst auto now = crl::now();\n\t\tconst auto skip = st::groupCallMessageSkip;\n\t\tconst auto padding = st::groupCallPinnedPadding;\n\t\tp.translate(-start, 0);\n\t\tfor (auto &entry : _pinnedViews) {\n\t\t\tif (entry.width <= skip || entry.left + entry.width <= start) {\n\t\t\t\tcontinue;\n\t\t\t} else if (entry.left >= end) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst auto x = entry.left;\n\t\t\tconst auto y = entry.top;\n\t\t\tconst auto use = entry.realWidth - skip;\n\t\t\tconst auto height = entry.height;\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tconst auto scaled = (entry.width < entry.realWidth);\n\t\t\tif (scaled) {\n\t\t\t\tconst auto used = entry.width - skip;\n\t\t\t\tconst auto mx = scaled ? (x + (used / 2)) : 0;\n\t\t\t\tconst auto my = scaled ? (y + (height / 2)) : 0;\n\t\t\t\tconst auto scale = used / float64(use);\n\t\t\t\tp.save();\n\t\t\t\tp.translate(mx, my);\n\t\t\t\tp.scale(scale, scale);\n\t\t\t\tp.setOpacity(scale);\n\t\t\t\tp.translate(-mx, -my);\n\t\t\t}\n\t\t\tconst auto coloring = Ui::StarsColoringForCount(\n\t\t\t\tcolorings,\n\t\t\t\tentry.stars);\n\t\t\tauto &bg = _bgs[ColoringKey(coloring)];\n\t\t\tif (!bg) {\n\t\t\t\tbg = std::make_unique<PayedBg>(coloring);\n\t\t\t}\n\t\t\tconst auto still = (entry.end - now) / float64(entry.duration);\n\t\t\tconst auto part = int(base::SafeRound(still * use));\n\t\t\tconst auto line = st::lineWidth;\n\t\t\tif (part > 0) {\n\t\t\t\tp.setOpacity(kColoredMessageBgOpacity);\n\t\t\t\tbg->pinnedLight.paint(p, { x, y, use, height });\n\t\t\t}\n\t\t\tif (part < use) {\n\t\t\t\tp.setClipRect(x + part, y, use - part + line, height);\n\t\t\t\tp.setOpacity(kDarkOverOpacity);\n\t\t\t\tbg->pinnedDark.paint(p, { x, y, use, height });\n\t\t\t}\n\t\t\tp.setClipping(false);\n\t\t\tp.setOpacity(1.);\n\n\t\t\tconst auto userpicSize = st::groupCallPinnedUserpic;\n\t\t\tconst auto userpicPadding = st::groupCallUserpicPadding;\n\t\t\tconst auto position = QPoint(\n\t\t\t\tx + userpicPadding.left(),\n\t\t\t\ty + userpicPadding.top());\n\t\t\tentry.from->paintUserpic(p, entry.view, {\n\t\t\t\t.position = position,\n\t\t\t\t.size = userpicSize,\n\t\t\t\t.shape = Ui::PeerUserpicShape::Circle,\n\t\t\t});\n\t\t\tconst auto leftSkip = userpicPadding.left()\n\t\t\t\t+ userpicSize\n\t\t\t\t+ userpicPadding.right();\n\n\t\t\tp.setPen(st::white);\n\t\t\tentry.text.draw(p, {\n\t\t\t\t.position = { x + leftSkip, y + padding.top() },\n\t\t\t\t.availableWidth = entry.width - leftSkip - padding.right(),\n\t\t\t\t.elisionLines = 1,\n\t\t\t});\n\t\t\tif (scaled) {\n\t\t\t\tp.restore();\n\t\t\t}\n\t\t}\n\t\tp.translate(start, 0);\n\n\t\tp.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\tp.setPen(Qt::NoPen);\n\n\t\tconst auto leftFade = (//_leftFadeAnimation.value(\n\t\t\t_leftFadeShown ? 1. : 0.);\n\t\tif (leftFade) {\n\t\t\tauto gradientLeft = QLinearGradient(0, 0, _fadeWidth, 0);\n\t\t\tgradientLeft.setStops({\n\t\t\t\t{ 0., QColor(255, 255, 255, 0) },\n\t\t\t\t{ 1., QColor(255, 255, 255, 255) },\n\t\t\t});\n\t\t\tp.setOpacity(leftFade);\n\t\t\tp.setBrush(gradientLeft);\n\t\t\tp.drawRect(0, 0, _fadeWidth, scroll->height());\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t\tconst auto rightFade = (//_rightFadeAnimation.value(\n\t\t\t_rightFadeShown ? 1. : 0.);\n\t\tif (rightFade) {\n\t\t\tconst auto till = scroll->width();\n\t\t\tconst auto from = till - _fadeWidth;\n\t\t\tauto gradientRight = QLinearGradient(from, 0, till, 0);\n\t\t\tgradientRight.setStops({\n\t\t\t\t{ 0., QColor(255, 255, 255, 255) },\n\t\t\t\t{ 1., QColor(255, 255, 255, 0) },\n\t\t\t});\n\t\t\tp.setBrush(gradientRight);\n\t\t\tp.drawRect(from, 0, _fadeWidth, scroll->height());\n\t\t}\n\t\tQPainter(_pinned).drawImage(\n\t\t\tQRect(QPoint(start, 0), scroll->size()),\n\t\t\t_pinnedCanvas,\n\t\t\tQRect(QPoint(), scroll->size() * ratio));\n\t}, _pinned->lifetime());\n\n\t_pinned->setMouseTracking(true);\n\tconst auto find = [=](QPoint position) {\n\t\tif (position.y() < 0 || position.y() >= _pinned->height()) {\n\t\t\treturn MsgId();\n\t\t} else for (const auto &entry : _pinnedViews) {\n\t\t\tif (entry.left > position.x()) {\n\t\t\t\tbreak;\n\t\t\t} else if (entry.left + entry.width > position.x()) {\n\t\t\t\treturn entry.id;\n\t\t\t}\n\t\t}\n\t\treturn MsgId();\n\t};\n\t_pinned->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::MouseButtonPress) {\n\t\t\tconst auto pos = static_cast<QMouseEvent*>(e.get())->pos();\n\t\t\tif (const auto id = find(pos)) {\n\t\t\t\tif (_hidden) {\n\t\t\t\t\tshowList(*base::take(_hidden));\n\t\t\t\t\t_hiddenShowRequested.fire({});\n\t\t\t\t}\n\t\t\t\thighlightMessage(id);\n\t\t\t}\n\t\t} else if (type == QEvent::MouseMove) {\n\t\t\tconst auto pos = static_cast<QMouseEvent*>(e.get())->pos();\n\t\t\t_pinned->setCursor(find(pos)\n\t\t\t\t? style::cur_pointer\n\t\t\t\t: style::cur_default);\n\t\t}\n\t}, _pinned->lifetime());\n\n\tscroll->show();\n\tapplyGeometry();\n}\n\nvoid MessagesUi::highlightMessage(MsgId id) {\n\tif (!_scroll) {\n\t\treturn;\n\t}\n\tconst auto i = ranges::find(_views, id, &MessageView::id);\n\tif (i == end(_views) || i->top < 0) {\n\t\treturn;\n\t} else if (i->toggleAnimation.animating()) {\n\t\t_delayedHighlightId = id;\n\t\treturn;\n\t}\n\t_delayedHighlightId = 0;\n\tconst auto top = std::clamp(\n\t\ti->top - ((_scroll->height() - i->realHeight) / 2),\n\t\t0,\n\t\ti->top);\n\tconst auto to = top - i->top;\n\tconst auto from = _scroll->scrollTop() - i->top;\n\tif (from == to) {\n\t\tstartHighlight(id);\n\t\treturn;\n\t}\n\t_scrollToAnimation.stop();\n\t_scrollToAnimation.start([=] {\n\t\tconst auto i = ranges::find(_views, id, &MessageView::id);\n\t\tif (i == end(_views)) {\n\t\t\t_scrollToAnimation.stop();\n\t\t\treturn;\n\t\t}\n\t\t_scroll->scrollToY(i->top + _scrollToAnimation.value(to));\n\t\tif (!_scrollToAnimation.animating()) {\n\t\t\tstartHighlight(id);\n\t\t}\n\t}, from, to, st::slideDuration, anim::easeOutCirc);\n}\n\nvoid MessagesUi::startHighlight(MsgId id) {\n\t_highlightId = id;\n\t_highlightAnimation.start([=] {\n\t\trepaintMessage(id);\n\t}, 0., 3., 1000);\n}\n\nvoid MessagesUi::applyGeometry() {\n\tif (_scroll) {\n\t\tauto top = 0;\n\t\tfor (auto &entry : _views) {\n\t\t\tentry.top = top;\n\n\t\t\tupdateMessageSize(entry);\n\t\t\tupdateMessageHeight(entry);\n\n\t\t\ttop += entry.height;\n\t\t}\n\t}\n\tapplyGeometryToPinned();\n\tupdateGeometries();\n}\n\nvoid MessagesUi::applyGeometryToPinned() {\n\tif (!_pinnedScroll) {\n\t\tsetPinnedScrollSkip(0);\n\t\treturn;\n\t}\n\tconst auto skip = st::groupCallMessageSkip;\n\tauto maxHeight = 0;\n\tauto left = 0;\n\tfor (auto &entry : _pinnedViews) {\n\t\tentry.left = left;\n\t\tupdatePinnedSize(entry);\n\t\tupdatePinnedWidth(entry);\n\t\tleft += entry.width;\n\n\t\tif (maxHeight < entry.height + skip) {\n\t\t\tconst auto possible = countPinnedScrollSkip(entry);\n\t\t\tmaxHeight = std::max(possible, maxHeight);\n\t\t}\n\t}\n\tsetPinnedScrollSkip(maxHeight);\n}\n\nint MessagesUi::countPinnedScrollSkip(const PinnedView &entry) const {\n\tconst auto skip = st::groupCallMessageSkip;\n\tif (!entry.toggleAnimation.animating()) {\n\t\treturn entry.height + skip;\n\t}\n\tconst auto used = ((entry.height + skip) * entry.width)\n\t\t/ float64(entry.realWidth);\n\treturn int(base::SafeRound(used));\n}\n\nvoid MessagesUi::setPinnedScrollSkip(int skip) {\n\tif (_pinnedScrollSkip != skip) {\n\t\t_pinnedScrollSkip = skip;\n\t\tupdateGeometries();\n\t}\n}\n\nvoid MessagesUi::updateGeometries() {\n\tif (_pinnedScroll) {\n\t\tconst auto skip = st::groupCallMessageSkip;\n\t\tconst auto width = _pinnedViews.empty()\n\t\t\t? 0\n\t\t\t: (_pinnedViews.back().left + _pinnedViews.back().width - skip);\n\t\tconst auto height = _pinnedViews.empty()\n\t\t\t? 0\n\t\t\t: _pinnedViews.back().height;\n\t\tconst auto bottom = _bottom - st::groupCallMessageSkip;\n\t\t_pinned->resize(width, height);\n\n\t\tconst auto min = std::min(width, _width);\n\t\t_pinnedScroll->setGeometry(_left, bottom - height, min, height);\n\t}\n\tif (_scroll) {\n\t\tconst auto scrollBottom = (_scroll->scrollTop() + _scroll->height());\n\t\tconst auto atBottom = (scrollBottom >= _messages->height());\n\t\tconst auto bottom = _bottom - _pinnedScrollSkip;\n\n\t\tconst auto height = _views.empty()\n\t\t\t? 0\n\t\t\t: (_views.back().top + _views.back().height);\n\t\t_messages->resize(_width, height);\n\n\t\tconst auto min = std::min(height, _availableHeight);\n\t\t_scroll->setGeometry(_left, bottom - min, _width, min);\n\n\t\tif (atBottom) {\n\t\t\t_scroll->scrollToY(std::max(height - _scroll->height(), 0));\n\t\t}\n\t}\n}\n\nvoid MessagesUi::move(int left, int bottom, int width, int availableHeight) {\n\tconst auto min = st::groupCallWidth * 2 / 6;\n\tif (width < min) {\n\t\tconst auto add = min - width;\n\t\twidth += add;\n\t\tleft -= add / 2;\n\t}\n\tif (_left != left\n\t\t|| _bottom != bottom\n\t\t|| _width != width\n\t\t|| _availableHeight != availableHeight) {\n\t\t_left = left;\n\t\t_bottom = bottom;\n\t\t_width = width;\n\t\t_availableHeight = availableHeight;\n\t\tapplyGeometry();\n\t}\n}\n\nvoid MessagesUi::raise() {\n\tif (_scroll) {\n\t\t_scroll->raise();\n\t}\n\tfor (const auto &view : _views) {\n\t\tif (const auto widget = view.reactionWidget.get()) {\n\t\t\twidget->raise();\n\t\t}\n\t}\n}\n\nrpl::producer<> MessagesUi::hiddenShowRequested() const {\n\treturn _hiddenShowRequested.events();\n}\n\nrpl::producer<MessageDeleteRequest> MessagesUi::deleteRequests() const {\n\treturn _deleteRequests.events();\n}\n\nrpl::lifetime &MessagesUi::lifetime() {\n\treturn _lifetime;\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_messages_ui.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/round_rect.h\"\n\nstruct TextWithTags;\n\nnamespace ChatHelpers {\nclass Show;\nclass TabbedPanel;\n} // namespace ChatHelpers\n\nnamespace Data {\nstruct ReactionId;\n} // namespace Data\n\nnamespace Ui {\nclass ElasticScroll;\nclass EmojiButton;\nclass InputField;\nclass SendButton;\nclass PopupMenu;\nclass RpWidget;\n} // namespace Ui\n\nnamespace Calls::Group::Ui {\nusing namespace ::Ui;\nstruct StarsColoring;\n} // namespace Calls::Group::Ui\n\nnamespace Calls::Group {\n\nstruct Message;\nstruct MessageIdUpdate;\nstruct MessageDeleteRequest;\n\nenum class MessagesMode {\n\tGroupCall,\n\tVideoStream,\n};\n\nclass MessagesUi final {\npublic:\n\tMessagesUi(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tMessagesMode mode,\n\t\trpl::producer<std::vector<Message>> messages,\n\t\trpl::producer<std::vector<not_null<PeerData*>>> topDonorsValue,\n\t\trpl::producer<MessageIdUpdate> idUpdates,\n\t\trpl::producer<bool> canManageValue,\n\t\trpl::producer<bool> shown);\n\t~MessagesUi();\n\n\tvoid move(int left, int bottom, int width, int availableHeight);\n\tvoid raise();\n\n\t[[nodiscard]] rpl::producer<> hiddenShowRequested() const;\n\t[[nodiscard]] rpl::producer<MessageDeleteRequest> deleteRequests() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tstruct MessageView;\n\tstruct PinnedView;\n\tstruct PayedBg {\n\t\texplicit PayedBg(const Ui::StarsColoring &coloring);\n\n\t\tstyle::owned_color light;\n\t\tstyle::owned_color dark;\n\t\tUi::RoundRect pinnedLight;\n\t\tUi::RoundRect pinnedDark;\n\t\tUi::RoundRect messageLight;\n\t\tUi::RoundRect priceDark;\n\t\tUi::RoundRect badgeDark;\n\t};\n\n\tvoid setupBadges();\n\tvoid setupList(\n\t\trpl::producer<std::vector<Message>> messages,\n\t\trpl::producer<bool> shown);\n\tvoid showList(const std::vector<Message> &list);\n\tvoid handleIdUpdates(rpl::producer<MessageIdUpdate> idUpdates);\n\tvoid toggleMessage(MessageView &entry, bool shown);\n\tvoid setContentFailed(MessageView &entry);\n\tvoid setContent(MessageView &entry);\n\tvoid setContent(PinnedView &entry);\n\tvoid updateMessageSize(MessageView &entry);\n\tbool updateMessageHeight(MessageView &entry);\n\tvoid updatePinnedSize(PinnedView &entry);\n\tbool updatePinnedWidth(PinnedView &entry);\n\tvoid animateMessageSent(MessageView &entry);\n\tvoid repaintMessage(MsgId id);\n\tvoid highlightMessage(MsgId id);\n\tvoid startHighlight(MsgId id);\n\tvoid recountHeights(std::vector<MessageView>::iterator i, int top);\n\tvoid appendMessage(const Message &data);\n\tvoid checkReactionContent(\n\t\tMessageView &entry,\n\t\tconst TextWithEntities &text);\n\tvoid startReactionAnimation(MessageView &entry);\n\tvoid updateReactionPosition(MessageView &entry);\n\tvoid removeReaction(not_null<Ui::RpWidget*> widget);\n\tvoid setupMessagesWidget();\n\n\tvoid togglePinned(PinnedView &entry, bool shown);\n\tvoid repaintPinned(MsgId id);\n\tvoid recountWidths(std::vector<PinnedView>::iterator i, int left);\n\tvoid appendPinned(const Message &data, TimeId now);\n\tvoid setupPinnedWidget();\n\n\tvoid applyGeometry();\n\tvoid applyGeometryToPinned();\n\tvoid updateGeometries();\n\t[[nodiscard]] int countPinnedScrollSkip(const PinnedView &entry) const;\n\tvoid setPinnedScrollSkip(int skip);\n\n\tvoid updateTopFade();\n\tvoid updateBottomFade();\n\tvoid updateLeftFade();\n\tvoid updateRightFade();\n\n\tvoid receiveSomeMouseEvents();\n\tvoid receiveAllMouseEvents();\n\tvoid handleClick(const MessageView &entry, QPoint point);\n\tvoid showContextMenu(const MessageView &entry, QPoint globalPoint);\n\n\t[[nodiscard]] int donorPlace(not_null<PeerData*> peer) const;\n\t[[nodiscard]] TextWithEntities nameText(\n\t\tnot_null<PeerData*> peer,\n\t\tint place);\n\n\tconst not_null<QWidget*> _parent;\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst MessagesMode _mode;\n\tstd::unique_ptr<Ui::ElasticScroll> _scroll;\n\tUi::Animations::Simple _scrollToAnimation;\n\tUi::RpWidget *_messages = nullptr;\n\tQImage _canvas;\n\n\tstd::unique_ptr<Ui::ElasticScroll> _pinnedScroll;\n\tUi::RpWidget *_pinned = nullptr;\n\tQImage _pinnedCanvas;\n\tint _pinnedScrollSkip = 0;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\trpl::variable<bool> _canManage;\n\trpl::event_stream<> _hiddenShowRequested;\n\trpl::event_stream<MessageDeleteRequest> _deleteRequests;\n\tstd::optional<std::vector<Message>> _hidden;\n\tstd::vector<MessageView> _views;\n\tstyle::complex_color _messageBg;\n\tUi::RoundRect _messageBgRect;\n\tMsgId _delayedHighlightId = 0;\n\tMsgId _highlightId = 0;\n\tUi::Animations::Simple _highlightAnimation;\n\n\tstd::vector<PinnedView> _pinnedViews;\n\tbase::flat_map<uint64, std::unique_ptr<PayedBg>> _bgs;\n\n\tQPoint _reactionBasePosition;\n\trpl::lifetime _effectsLifetime;\n\n\tUi::Text::String _liveBadge;\n\tUi::Text::String _adminBadge;\n\n\tUi::Text::CustomEmojiHelper _crownHelper;\n\tbase::flat_map<int, QString> _crownEmojiDataCache;\n\trpl::variable<std::vector<not_null<PeerData*>>> _topDonors;\n\t//Ui::Animations::Simple _topFadeAnimation;\n\t//Ui::Animations::Simple _bottomFadeAnimation;\n\t//Ui::Animations::Simple _leftFadeAnimation;\n\t//Ui::Animations::Simple _rightFadeAnimation;\n\tint _fadeHeight = 0;\n\tint _fadeWidth = 0;\n\tbool _topFadeShown = false;\n\tbool _bottomFadeShown = false;\n\tbool _leftFadeShown = false;\n\tbool _rightFadeShown = false;\n\tbool _streamMode = false;\n\n\tint _left = 0;\n\tint _bottom = 0;\n\tint _width = 0;\n\tint _availableHeight = 0;\n\n\tMsgId _revealedSpoilerId = 0;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_panel.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_panel.h\"\n\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/group/calls_group_invite_controller.h\"\n#include \"calls/group/calls_group_members.h\"\n#include \"calls/group/calls_group_menu.h\"\n#include \"calls/group/calls_group_message_field.h\"\n#include \"calls/group/calls_group_messages.h\"\n#include \"calls/group/calls_group_messages_ui.h\"\n#include \"calls/group/calls_group_settings.h\"\n#include \"calls/group/calls_group_toasts.h\"\n#include \"calls/group/calls_group_viewport.h\"\n#include \"calls/group/ui/calls_group_scheduled_labels.h\"\n#include \"calls/group/ui/desktop_capture_choose_source.h\"\n#include \"calls/calls_emoji_fingerprint.h\"\n#include \"calls/calls_window.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"data/data_file_origin.h\"\n#include \"ui/platform/ui_platform_window_title.h\" // TitleLayout\n#include \"ui/platform/ui_platform_utility.h\"\n#include \"ui/controls/call_mute_button.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/call_button.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/widgets/rp_window.h\"\n#include \"ui/chat/group_call_bar.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/integration.h\"\n#include \"ui/painter.h\"\n#include \"ui/round_rect.h\"\n#include \"info/profile/info_profile_values.h\" // Info::Profile::Value.\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"main/session/session_show.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_send.h\"\n#include \"base/event_filter.h\"\n#include \"base/unixtime.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"base/timer_rpl.h\"\n#include \"apiwrap.h\" // api().kick.\n#include \"api/api_chat_participants.h\" // api().kick.\n#include \"webrtc/webrtc_environment.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"webrtc/webrtc_audio_input_tester.h\"\n#include \"webrtc/webrtc_create_adm.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_layers.h\"\n\n#include <QtWidgets/QApplication>\n#include <QtGui/QWindow>\n#include <QtGui/QScreen>\n\nnamespace Calls::Group {\nnamespace {\n\nconstexpr auto kSpacePushToTalkDelay = crl::time(250);\nconstexpr auto kRecordingAnimationDuration = crl::time(1200);\nconstexpr auto kRecordingOpacity = 0.6;\nconstexpr auto kStartNoConfirmation = TimeId(10);\nconstexpr auto kControlsBackgroundOpacity = 0.8;\nconstexpr auto kOverrideActiveColorBgAlpha = 172;\nconstexpr auto kHideControlsTimeout = 5 * crl::time(1000);\n\n#ifdef Q_OS_WIN\nvoid UnpinMaximized(not_null<QWidget*> widget) {\n\tSetWindowPos(\n\t\treinterpret_cast<HWND>(widget->window()->windowHandle()->winId()),\n\t\tHWND_NOTOPMOST,\n\t\t0,\n\t\t0,\n\t\t0,\n\t\t0,\n\t\t(SWP_NOMOVE\n\t\t\t| SWP_NOSIZE\n\t\t\t| SWP_NOOWNERZORDER\n\t\t\t| SWP_FRAMECHANGED\n\t\t\t| SWP_NOACTIVATE));\n}\n#endif // Q_OS_WIN\n\nclass Show final : public ChatHelpers::Show {\npublic:\n\tShow(not_null<Panel*> panel, std::shared_ptr<Ui::Show> base)\n\t: _panel(panel)\n\t, _base(std::move(base)) {\n\t}\n\n\tvoid activate() override {\n\t\tif (const auto panel = _panel.get()) {\n\t\t\tif (!panel->window()->isHidden()) {\n\t\t\t\tpanel->window()->activateWindow();\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid showOrHideBoxOrLayer(\n\t\t\tstd::variant<\n\t\t\t\tv::null_t,\n\t\t\t\tobject_ptr<Ui::BoxContent>,\n\t\t\t\tstd::unique_ptr<Ui::LayerWidget>> &&layer,\n\t\t\tUi::LayerOptions options,\n\t\t\tanim::type animated) const override {\n\t\t_base->showOrHideBoxOrLayer(\n\t\t\tstd::move(layer),\n\t\t\toptions,\n\t\t\tanim::type::normal);\n\t}\n\tnot_null<QWidget*> toastParent() const override {\n\t\treturn _base->toastParent();\n\t}\n\tbool valid() const override {\n\t\treturn _panel.get() != nullptr;\n\t}\n\toperator bool() const override {\n\t\treturn valid();\n\t}\n\n\tMain::Session &session() const override {\n\t\tconst auto panel = _panel.get();\n\t\tAssert(panel != nullptr);\n\n\t\treturn panel->call()->peer()->session();\n\t}\n\tbool paused(ChatHelpers::PauseReason reason) const override {\n\t\tconst auto panel = _panel.get();\n\t\tif (!panel) {\n\t\t\treturn false;\n\t\t} else if (panel->window()->isHidden()\n\t\t\t|| (!panel->window()->isFullScreen()\n\t\t\t\t&& !panel->window()->isActiveWindow())) {\n\t\t\treturn true;\n\t\t} else if (reason < ChatHelpers::PauseReason::Layer\n\t\t\t&& panel->callWindow()->topShownLayer() != nullptr) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\trpl::producer<> pauseChanged() const override {\n\t\treturn rpl::never<>();\n\t}\n\n\trpl::producer<bool> adjustShadowLeft() const override {\n\t\treturn rpl::single(false);\n\t}\n\tSendMenu::Details sendMenuDetails() const override {\n\t\treturn { SendMenu::Type::Disabled };\n\t}\n\n\tbool showMediaPreview(\n\t\t\tData::FileOrigin origin,\n\t\t\tnot_null<DocumentData*> document) const override {\n\t\treturn false; // #TODO stories\n\t}\n\tbool showMediaPreview(\n\t\t\tData::FileOrigin origin,\n\t\t\tnot_null<PhotoData*> photo) const override {\n\t\treturn false; // #TODO stories\n\t}\n\n\tvoid processChosenSticker(\n\t\t\tChatHelpers::FileChosen &&chosen) const override {\n\t\t//_panel->emojiChosen(std::move(chosen));\n\t}\n\nprivate:\n\tconst base::weak_ptr<Panel> _panel;\n\tconst std::shared_ptr<Ui::Show> _base;\n\n};\n\n} // namespace\n\nstruct Panel::ControlsBackgroundNarrow {\n\texplicit ControlsBackgroundNarrow(not_null<QWidget*> parent)\n\t: shadow(parent)\n\t, blocker(parent) {\n\t}\n\n\tUi::RpWidget shadow;\n\tUi::RpWidget blocker;\n\tint shadowHeight = 0;\n};\n\nPanel::Panel(not_null<GroupCall*> call)\n: Panel(call, ConferencePanelMigration()) {\n}\n\nPanel::Panel(not_null<GroupCall*> call, ConferencePanelMigration info)\n: _call(call)\n, _peer(call->peer())\n, _window(info.window ? info.window : std::make_shared<Window>())\n, _viewport(\n\tstd::make_unique<Viewport>(\n\t\twidget(),\n\t\tPanelMode::Wide,\n\t\t_window->backend()))\n, _mute(std::make_unique<Ui::CallMuteButton>(\n\twidget(),\n\tst::callMuteButton,\n\tCore::App().appDeactivatedValue(),\n\tUi::CallMuteButtonState{\n\t\t.text = (_call->scheduleDate()\n\t\t\t? tr::lng_group_call_start_now(tr::now)\n\t\t\t: tr::lng_group_call_connecting(tr::now)),\n\t\t.type = (!_call->scheduleDate()\n\t\t\t? Ui::CallMuteButtonType::Connecting\n\t\t\t: _peer->canManageGroupCall()\n\t\t\t? Ui::CallMuteButtonType::ScheduledCanStart\n\t\t\t: _call->scheduleStartSubscribed()\n\t\t\t? Ui::CallMuteButtonType::ScheduledNotify\n\t\t\t: Ui::CallMuteButtonType::ScheduledSilent),\n\t\t.expandType = ((_call->scheduleDate() || !_call->rtmp())\n\t\t\t? Ui::CallMuteButtonExpandType::None\n\t\t\t: Ui::CallMuteButtonExpandType::Normal),\n\t}))\n, _hangup(widget(), st::groupCallHangup)\n, _stickedTooltipsShown(Core::App().settings().hiddenGroupCallTooltips()\n\t& ~StickedTooltip::Microphone) // Always show tooltip about mic.\n, _messages(std::make_unique<MessagesUi>(\n\twidget(),\n\tuiShow(),\n\tMessagesMode::GroupCall,\n\t_call->messages()->listValue(),\n\tnullptr,\n\t_call->messages()->idUpdates(),\n\t_call->canManageValue(),\n\t_call->messagesEnabledValue()))\n, _toasts(std::make_unique<Toasts>(this))\n, _controlsBackgroundColor([] {\n\tauto result = st::groupCallBg->c;\n\tresult.setAlphaF(kControlsBackgroundOpacity);\n\treturn result;\n})\n, _hideControlsTimer([=] { toggleWideControls(false); }) {\n\t_viewport->widget()->hide();\n\tif (!_viewport->requireARGB32()) {\n\t\t_call->setNotRequireARGB32();\n\t}\n\n\tSubscribeToMigration(\n\t\t_peer,\n\t\tlifetime(),\n\t\t[=](not_null<ChannelData*> channel) { migrate(channel); });\n\tsetupRealCallViewers();\n\n\tinitWindow();\n\tinitWidget();\n\tinitControls();\n\tinitLayout(info);\n\tshowAndActivate();\n}\n\nPanel::~Panel() {\n\t_menu.destroy();\n\t_viewport = nullptr;\n}\n\nvoid Panel::setupRealCallViewers() {\n\t_call->real(\n\t) | rpl::on_next([=](not_null<Data::GroupCall*> real) {\n\t\tsubscribeToChanges(real);\n\t}, lifetime());\n}\n\nnot_null<GroupCall*> Panel::call() const {\n\treturn _call;\n}\n\nbool Panel::isVisible() const {\n\treturn window()->isVisible()\n\t\t&& !(window()->windowState() & Qt::WindowMinimized);\n}\n\nbool Panel::isActive() const {\n\treturn window()->isActiveWindow() && isVisible();\n}\n\nstd::shared_ptr<ChatHelpers::Show> Panel::uiShow() {\n\tif (!_cachedShow) {\n\t\t_cachedShow = std::make_shared<Show>(this, _window->uiShow());\n\t}\n\treturn _cachedShow;\n}\n\nvoid Panel::minimize() {\n\twindow()->setWindowState(window()->windowState() | Qt::WindowMinimized);\n}\n\nvoid Panel::close() {\n\twindow()->close();\n}\n\nvoid Panel::showAndActivate() {\n\tif (window()->isHidden()) {\n\t\twindow()->show();\n\t}\n\tconst auto state = window()->windowState();\n\tif (state & Qt::WindowMinimized) {\n\t\twindow()->setWindowState(state & ~Qt::WindowMinimized);\n\t}\n\twindow()->raise();\n\twindow()->activateWindow();\n\twindow()->setFocus();\n}\n\nvoid Panel::migrate(not_null<ChannelData*> channel) {\n\t_peer = channel;\n\t_peerLifetime.destroy();\n\tsubscribeToPeerChanges();\n\t_title.destroy();\n\t_titleSeparator.destroy();\n\t_viewers.destroy();\n\trefreshTitle();\n}\n\nvoid Panel::subscribeToPeerChanges() {\n\tInfo::Profile::NameValue(\n\t\t_peer\n\t) | rpl::on_next([=](const QString &name) {\n\t\twindow()->setTitle(name);\n\t}, _peerLifetime);\n}\n\nQWidget *Panel::chooseSourceParent() {\n\treturn window().get();\n}\n\nQString Panel::chooseSourceActiveDeviceId() {\n\treturn _call->screenSharingDeviceId();\n}\n\nbool Panel::chooseSourceActiveWithAudio() {\n\treturn _call->screenSharingWithAudio();\n}\n\nbool Panel::chooseSourceWithAudioSupported() {\n\treturn Webrtc::LoopbackAudioCaptureSupported();\n}\n\nrpl::lifetime &Panel::chooseSourceInstanceLifetime() {\n\treturn lifetime();\n}\n\nvoid Panel::chooseSourceAccepted(\n\t\tconst QString &deviceId,\n\t\tbool withAudio) {\n\t_call->toggleScreenSharing(deviceId, withAudio);\n}\n\nvoid Panel::chooseSourceStop() {\n\t_call->toggleScreenSharing(std::nullopt);\n}\n\nvoid Panel::initWindow() {\n\twindow()->setAttribute(Qt::WA_OpaquePaintEvent);\n\twindow()->setAttribute(Qt::WA_NoSystemBackground);\n\twindow()->setTitleStyle(st::groupCallTitle);\n\n\tif (_call->conference()) {\n\t\ttitleText() | rpl::on_next([=](const QString &text) {\n\t\t\twindow()->setTitle(text);\n\t\t}, lifetime());\n\t} else {\n\t\tsubscribeToPeerChanges();\n\t}\n\n\tconst auto updateFullScreen = [=] {\n\t\tconst auto state = window()->windowState();\n\t\tconst auto full = (state & Qt::WindowFullScreen)\n\t\t\t|| (state & Qt::WindowMaximized);\n\t\t_rtmpFull = _call->rtmp() && full;\n\t\t_fullScreenOrMaximized = full;\n\t};\n\tbase::install_event_filter(window().get(), [=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::Close && handleClose()) {\n\t\t\te->ignore();\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t} else if (_call->rtmp()\n\t\t\t&& (type == QEvent::KeyPress || type == QEvent::KeyRelease)) {\n\t\t\tconst auto key = static_cast<QKeyEvent*>(e.get())->key();\n\t\t\tif (key == Qt::Key_Space) {\n\t\t\t\t_call->pushToTalk(\n\t\t\t\t\te->type() == QEvent::KeyPress,\n\t\t\t\t\tkSpacePushToTalkDelay);\n\t\t\t} else if (key == Qt::Key_Escape\n\t\t\t\t&& _fullScreenOrMaximized.current()) {\n\t\t\t\ttoggleFullScreen();\n\t\t\t}\n\t\t} else if (type == QEvent::WindowStateChange) {\n\t\t\tupdateFullScreen();\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t}, lifetime());\n\tupdateFullScreen();\n\n\tconst auto guard = base::make_weak(this);\n\twindow()->setBodyTitleArea([=](QPoint widgetPoint) {\n\t\tusing Flag = Ui::WindowTitleHitTestFlag;\n\t\tif (!guard) {\n\t\t\treturn (Flag::None | Flag(0));\n\t\t}\n\t\tconst auto titleRect = QRect(\n\t\t\t0,\n\t\t\t0,\n\t\t\twidget()->width(),\n\t\t\t(mode() == PanelMode::Wide\n\t\t\t\t? st::groupCallWideVideoTop\n\t\t\t\t: st::groupCallMembersTop));\n\t\tconst auto moveable = (titleRect.contains(widgetPoint)\n\t\t\t&& (!_menuToggle || !_menuToggle->geometry().contains(widgetPoint))\n\t\t\t&& (!_menu || !_menu->geometry().contains(widgetPoint))\n\t\t\t&& (!_recordingMark || !_recordingMark->geometry().contains(widgetPoint))\n\t\t\t&& (!_joinAsToggle || !_joinAsToggle->geometry().contains(widgetPoint)));\n\t\tif (!moveable) {\n\t\t\treturn (Flag::None | Flag(0));\n\t\t}\n\t\tconst auto shown = _window->topShownLayer();\n\t\treturn (!shown || !shown->geometry().contains(widgetPoint))\n\t\t\t? (Flag::Move | Flag::Menu | Flag::Maximize)\n\t\t\t: Flag::None;\n\t});\n\n\t_call->hasVideoWithFramesValue(\n\t) | rpl::on_next([=] {\n\t\tupdateMode();\n\t}, lifetime());\n\n\t_window->maximizeRequests() | rpl::on_next([=](bool maximized) {\n\t\tif (_call->rtmp()) {\n\t\t\ttoggleFullScreen(maximized);\n\t\t} else {\n\t\t\twindow()->setWindowState(maximized\n\t\t\t\t? Qt::WindowMaximized\n\t\t\t\t: Qt::WindowNoState);\n\t\t}\n\t}, lifetime());\n\n\t_window->showingLayer() | rpl::on_next([=] {\n\t\thideStickedTooltip(StickedTooltipHide::Unavailable);\n\t}, lifetime());\n\n\t_window->setControlsStyle(st::groupCallTitle);\n\t_window->togglePowerSaveBlocker(true);\n\n\tuiShow()->hideLayer(anim::type::instant);\n}\n\nvoid Panel::initWidget() {\n\twidget()->setMouseTracking(true);\n\n\twidget()->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tpaint(clip);\n\t}, lifetime());\n\n\twidget()->sizeValue(\n\t) | rpl::skip(1) | rpl::on_next([=](QSize size) {\n\t\tif (!updateMode()) {\n\t\t\tupdateControlsGeometry();\n\t\t}\n\n\t\t// some geometries depends on _controls->controls.geometry,\n\t\t// which is not updated here yet.\n\t\tcrl::on_main(this, [=] { updateControlsGeometry(); });\n\t}, lifetime());\n}\n\nvoid Panel::toggleMessageTyping() {\n\tconst auto typing = !_messageTyping.current();\n\tif (_messageField) {\n\t\t_messageField->toggle(typing);\n\t} else if (typing) {\n\t\t_messageField = std::make_unique<MessageField>(\n\t\t\twidget(),\n\t\t\tuiShow(),\n\t\t\t_call->conference() ? nullptr : _call->peer().get());\n\n\t\tupdateButtonsGeometry();\n\t\t_messageField->toggle(true);\n\n\t\t_messageField->submitted(\n\t\t) | rpl::on_next([=](TextWithTags text) {\n\t\t\t_call->sendMessage(std::move(text));\n\n\t\t\t_messageField->toggle(false);\n\t\t\t_messageTyping = false;\n\t\t\tupdateWideControlsVisibility();\n\t\t}, _messageField->lifetime());\n\n\t\t_messageField->heightValue() | rpl::on_next([=] {\n\t\t\tupdateButtonsGeometry();\n\t\t}, _messageField->lifetime());\n\n\t\t_messageField->closeRequests() | rpl::on_next([=] {\n\t\t\tif (_messageTyping.current()) {\n\t\t\t\ttoggleMessageTyping();\n\t\t\t}\n\t\t}, _messageField->lifetime());\n\n\t\t_messageField->closed() | rpl::on_next([=] {\n\t\t\t_messageField = nullptr;\n\t\t\tupdateButtonsGeometry();\n\t\t}, _messageField->lifetime());\n\t}\n\t_messageTyping = typing;\n\tupdateWideControlsVisibility();\n}\n\nvoid Panel::endCall() {\n\tif (!_call->canManage()) {\n\t\t_call->hangup();\n\t\treturn;\n\t}\n\tuiShow()->showBox(Box(\n\t\tLeaveBox,\n\t\t_call,\n\t\tfalse,\n\t\tBoxContext::GroupCallPanel));\n}\n\nvoid Panel::startScheduledNow() {\n\tconst auto date = _call->scheduleDate();\n\tconst auto now = base::unixtime::now();\n\tif (!date) {\n\t\treturn;\n\t} else if (now + kStartNoConfirmation >= date) {\n\t\t_call->startScheduledNow();\n\t} else {\n\t\tconst auto box = std::make_shared<base::weak_qptr<Ui::GenericBox>>();\n\t\tconst auto done = [=] {\n\t\t\tif (*box) {\n\t\t\t\t(*box)->closeBox();\n\t\t\t}\n\t\t\t_call->startScheduledNow();\n\t\t};\n\t\tauto owned = ConfirmBox({\n\t\t\t.text = (_call->peer()->isBroadcast()\n\t\t\t\t? tr::lng_group_call_start_now_sure_channel\n\t\t\t\t: tr::lng_group_call_start_now_sure)(),\n\t\t\t.confirmed = done,\n\t\t\t.confirmText = tr::lng_group_call_start_now(),\n\t\t});\n\t\t*box = owned.data();\n\t\tuiShow()->showBox(std::move(owned));\n\t}\n}\n\nvoid Panel::initControls() {\n\t_mute->clicks(\n\t) | rpl::filter([=](Qt::MouseButton button) {\n\t\treturn (button == Qt::LeftButton);\n\t}) | rpl::on_next([=] {\n\t\tif (_call->scheduleDate()) {\n\t\t\tif (_call->canManage()) {\n\t\t\t\tstartScheduledNow();\n\t\t\t} else if (const auto real = _call->lookupReal()) {\n\t\t\t\t_call->toggleScheduleStartSubscribed(\n\t\t\t\t\t!real->scheduleStartSubscribed());\n\t\t\t}\n\t\t\treturn;\n\t\t} else if (_call->rtmp()) {\n\t\t\ttoggleFullScreen();\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto oldState = _call->muted();\n\t\tconst auto newState = (oldState == MuteState::ForceMuted)\n\t\t\t? (_call->conference()\n\t\t\t\t? MuteState::ForceMuted\n\t\t\t\t: MuteState::RaisedHand)\n\t\t\t: (oldState == MuteState::RaisedHand)\n\t\t\t? MuteState::RaisedHand\n\t\t\t: (oldState == MuteState::Muted)\n\t\t\t? MuteState::Active\n\t\t\t: MuteState::Muted;\n\t\t_call->setMutedAndUpdate(newState);\n\t}, _mute->lifetime());\n\n\tinitShareAction();\n\tcreateMessageButton();\n\trefreshLeftButton();\n\trefreshVideoButtons();\n\n\trpl::combine(\n\t\t_mode.value(),\n\t\t_call->canManageValue()\n\t) | rpl::on_next([=] {\n\t\trefreshTopButton();\n\t}, lifetime());\n\n\t_hangup->setClickedCallback([=] { endCall(); });\n\t_hangup->setAccessibleName(tr::lng_group_call_leave(tr::now));\n\n\tconst auto scheduleDate = _call->scheduleDate();\n\tif (scheduleDate) {\n\t\tauto changes = _call->real(\n\t\t) | rpl::map([=](not_null<Data::GroupCall*> real) {\n\t\t\treturn real->scheduleDateValue();\n\t\t}) | rpl::flatten_latest();\n\n\t\tsetupScheduledLabels(rpl::single(\n\t\t\tscheduleDate\n\t\t) | rpl::then(rpl::duplicate(changes)));\n\n\t\tauto started = std::move(changes) | rpl::filter([](TimeId date) {\n\t\t\treturn (date == 0);\n\t\t}) | rpl::take(1);\n\n\t\trpl::merge(\n\t\t\trpl::duplicate(started) | rpl::to_empty,\n\t\t\t_peer->session().changes().peerFlagsValue(\n\t\t\t\t_peer,\n\t\t\t\tData::PeerUpdate::Flag::Username\n\t\t\t) | rpl::skip(1) | rpl::to_empty\n\t\t) | rpl::on_next([=] {\n\t\t\trefreshLeftButton();\n\t\t\tupdateControlsGeometry();\n\t\t}, _callLifetime);\n\n\t\tstd::move(started) | rpl::on_next([=] {\n\t\t\trefreshVideoButtons();\n\t\t\tupdateButtonsStyles();\n\t\t\tsetupMembers();\n\t\t}, _callLifetime);\n\t}\n\n\t_call->stateValue(\n\t) | rpl::before_next([=] {\n\t\tshowStickedTooltip();\n\t}) | rpl::filter([](State state) {\n\t\treturn (state == State::HangingUp)\n\t\t\t|| (state == State::Ended)\n\t\t\t|| (state == State::FailedHangingUp)\n\t\t\t|| (state == State::Failed);\n\t}) | rpl::on_next([=] {\n\t\tcloseBeforeDestroy();\n\t}, _callLifetime);\n\n\t_call->levelUpdates(\n\t) | rpl::filter([=](const LevelUpdate &update) {\n\t\treturn update.me;\n\t}) | rpl::on_next([=](const LevelUpdate &update) {\n\t\t_mute->setLevel(update.value);\n\t}, _callLifetime);\n\n\t_call->real(\n\t) | rpl::on_next([=](not_null<Data::GroupCall*> real) {\n\t\tsetupRealMuteButtonState(real);\n\t}, _callLifetime);\n\n\trefreshControlsBackground();\n}\n\nvoid Panel::toggleFullScreen() {\n\ttoggleFullScreen(\n\t\t!_fullScreenOrMaximized.current() && !window()->isFullScreen());\n}\n\nvoid Panel::toggleFullScreen(bool fullscreen) {\n\tif (fullscreen) {\n\t\twindow()->showFullScreen();\n\t} else {\n\t\twindow()->showNormal();\n\t}\n}\n\nvoid Panel::refreshLeftButton() {\n\tconst auto share = _call->scheduleDate()\n\t\t&& _peer->isBroadcast()\n\t\t&& _peer->asChannel()->hasUsername();\n\tif ((share && _callShare) || (!share && _settings)) {\n\t\treturn;\n\t}\n\tif (share) {\n\t\t_settings.destroy();\n\t\t_callShare.create(widget(), st::groupCallShare);\n\t\t_callShare->setClickedCallback(_callShareLinkCallback);\n\t\t_callShare->setAccessibleName(tr::lng_group_call_invite(tr::now));\n\t} else {\n\t\t_callShare.destroy();\n\t\t_settings.create(widget(), st::groupCallSettings);\n\t\t_settings->setClickedCallback([=] {\n\t\t\tuiShow()->showBox(Box(SettingsBox, _call));\n\t\t});\n\t\t_settings->setAccessibleName(tr::lng_group_call_settings_title(tr::now));\n\t\ttrackControls(_trackControls, true);\n\t}\n\tconst auto raw = _callShare ? _callShare.data() : _settings.data();\n\traw->show();\n\traw->setColorOverrides(_mute->colorOverrides());\n\tupdateButtonsStyles();\n}\n\nrpl::producer<Ui::CallButtonColors> Panel::toggleableOverrides(\n\t\trpl::producer<bool> active) {\n\treturn rpl::combine(\n\t\tstd::move(active),\n\t\t_mute->colorOverrides()\n\t) | rpl::map([](bool active, Ui::CallButtonColors colors) {\n\t\tif (active && colors.bg) {\n\t\t\tcolors.bg->setAlpha(kOverrideActiveColorBgAlpha);\n\t\t}\n\t\treturn colors;\n\t});\n}\n\nvoid Panel::refreshVideoButtons(std::optional<bool> overrideWideMode) {\n\tconst auto create = overrideWideMode.value_or(mode() == PanelMode::Wide)\n\t\t|| (!_call->scheduleDate() && _call->videoIsWorking());\n\tconst auto created = _video && _screenShare;\n\tif (created == create) {\n\t\treturn;\n\t} else if (created) {\n\t\t_video.destroy();\n\t\t_screenShare.destroy();\n\t\tif (!overrideWideMode) {\n\t\t\tupdateButtonsGeometry();\n\t\t}\n\t\treturn;\n\t}\n\tif (!_video) {\n\t\t_video.create(\n\t\t\twidget(),\n\t\t\tst::groupCallVideoSmall,\n\t\t\t&st::groupCallVideoActiveSmall);\n\t\t_video->show();\n\t\t_video->setClickedCallback([=] {\n\t\t\thideStickedTooltip(\n\t\t\t\tStickedTooltip::Camera,\n\t\t\t\tStickedTooltipHide::Activated);\n\t\t\t_call->toggleVideo(!_call->isSharingCamera());\n\t\t});\n\t\t_video->setAccessibleName(tr::lng_call_start_video(tr::now));\n\t\t_video->setColorOverrides(\n\t\t\ttoggleableOverrides(_call->isSharingCameraValue()));\n\t\t_call->isSharingCameraValue(\n\t\t) | rpl::on_next([=](bool sharing) {\n\t\t\tif (sharing) {\n\t\t\t\thideStickedTooltip(\n\t\t\t\t\tStickedTooltip::Camera,\n\t\t\t\t\tStickedTooltipHide::Activated);\n\t\t\t}\n\t\t\t_video->setProgress(sharing ? 1. : 0.);\n\t\t\t_video->setAccessibleName(sharing\n\t\t\t\t? tr::lng_call_stop_video(tr::now)\n\t\t\t\t: tr::lng_call_start_video(tr::now));\n\t\t}, _video->lifetime());\n\t}\n\tif (!_screenShare) {\n\t\t_screenShare.create(widget(), st::groupCallScreenShareSmall);\n\t\t_screenShare->show();\n\t\t_screenShare->setClickedCallback([=] {\n\t\t\tchooseShareScreenSource();\n\t\t});\n\t\t_screenShare->setAccessibleName(tr::lng_group_call_screen_share_start(tr::now));\n\t\t_screenShare->setColorOverrides(\n\t\t\ttoggleableOverrides(_call->isSharingScreenValue()));\n\t\t_call->isSharingScreenValue(\n\t\t) | rpl::on_next([=](bool sharing) {\n\t\t\t_screenShare->setProgress(sharing ? 1. : 0.);\n\t\t\t_screenShare->setAccessibleName(sharing\n\t\t\t\t? tr::lng_group_call_screen_share_stop(tr::now)\n\t\t\t\t: tr::lng_group_call_screen_share_start(tr::now));\n\t\t}, _screenShare->lifetime());\n\t}\n\tif (!_wideMenu) {\n\t\t_wideMenu.create(widget(), st::groupCallMenuToggleSmall);\n\t\t_wideMenu->show();\n\t\t_wideMenu->setClickedCallback([=] { showMainMenu(); });\n\t\t_wideMenu->setAccessibleName(tr::lng_sr_group_call_menu(tr::now));\n\t\t_wideMenu->setColorOverrides(\n\t\t\ttoggleableOverrides(_wideMenuShown.value()));\n\t}\n\tupdateButtonsStyles();\n\tupdateButtonsGeometry();\n\traiseControls();\n}\n\nvoid Panel::createMessageButton() {\n\tif (!_message) {\n\t\t_message.create(\n\t\t\twidget(),\n\t\t\tst::groupCallMessageSmall,\n\t\t\t&st::groupCallMessageActiveSmall);\n\t\t_message->show();\n\t\t_message->setClickedCallback([=] { toggleMessageTyping(); });\n\t\t_message->setAccessibleName(tr::lng_group_call_message(tr::now));\n\t\t_message->setColorOverrides(\n\t\t\ttoggleableOverrides(_messageTyping.value()));\n\t}\n}\n\nvoid Panel::hideStickedTooltip(StickedTooltipHide hide) {\n\tif (!_stickedTooltipClose || !_niceTooltipControl) {\n\t\treturn;\n\t}\n\tif (_niceTooltipControl.data() == _video.data()) {\n\t\thideStickedTooltip(StickedTooltip::Camera, hide);\n\t} else if (_niceTooltipControl.data() == _mute->outer().get()) {\n\t\thideStickedTooltip(StickedTooltip::Microphone, hide);\n\t}\n}\n\nvoid Panel::hideStickedTooltip(\n\t\tStickedTooltip type,\n\t\tStickedTooltipHide hide) {\n\tif (hide != StickedTooltipHide::Unavailable) {\n\t\t_stickedTooltipsShown |= type;\n\t\tif (hide == StickedTooltipHide::Discarded) {\n\t\t\tCore::App().settings().setHiddenGroupCallTooltip(type);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}\n\t}\n\tconst auto control = (type == StickedTooltip::Camera)\n\t\t? _video.data()\n\t\t: (type == StickedTooltip::Microphone)\n\t\t? _mute->outer().get()\n\t\t: nullptr;\n\tif (_niceTooltipControl.data() == control) {\n\t\thideNiceTooltip();\n\t}\n}\n\nvoid Panel::hideNiceTooltip() {\n\tif (!_niceTooltip) {\n\t\treturn;\n\t}\n\t_stickedTooltipClose = nullptr;\n\t_niceTooltip.release()->toggleAnimated(false);\n\t_niceTooltipControl = nullptr;\n}\n\nvoid Panel::initShareAction() {\n\tauto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction(\n\t\t_peer,\n\t\tuiShow());\n\t_callShareLinkCallback = [=, callback = std::move(shareLinkCallback)] {\n\t\tif (_call->lookupReal()) {\n\t\t\tcallback();\n\t\t}\n\t};\n\tlifetime().add(std::move(shareLinkLifetime));\n}\n\nvoid Panel::setupRealMuteButtonState(not_null<Data::GroupCall*> real) {\n\tusing namespace rpl::mappers;\n\trpl::combine(\n\t\t_call->mutedValue() | MapPushToTalkToActive(),\n\t\t_call->instanceStateValue(),\n\t\treal->scheduleDateValue(),\n\t\treal->scheduleStartSubscribedValue(),\n\t\t_call->canManageValue(),\n\t\t_mode.value(),\n\t\t_fullScreenOrMaximized.value()\n\t) | rpl::distinct_until_changed(\n\t) | rpl::filter(\n\t\t_2 != GroupCall::InstanceState::TransitionToRtc\n\t) | rpl::on_next([=](\n\t\t\tMuteState mute,\n\t\t\tGroupCall::InstanceState state,\n\t\t\tTimeId scheduleDate,\n\t\t\tbool scheduleStartSubscribed,\n\t\t\tbool canManage,\n\t\t\tPanelMode mode,\n\t\t\tbool fullScreenOrMaximized) {\n\t\tconst auto wide = (mode == PanelMode::Wide);\n\t\tusing Type = Ui::CallMuteButtonType;\n\t\tusing ExpandType = Ui::CallMuteButtonExpandType;\n\t\tconst auto text = (wide\n\t\t\t\t? QString()\n\t\t\t\t: scheduleDate\n\t\t\t\t? (canManage\n\t\t\t\t\t? tr::lng_group_call_start_now(tr::now)\n\t\t\t\t\t: scheduleStartSubscribed\n\t\t\t\t\t? tr::lng_group_call_cancel_reminder(tr::now)\n\t\t\t\t\t: tr::lng_group_call_set_reminder(tr::now))\n\t\t\t\t: state == GroupCall::InstanceState::Disconnected\n\t\t\t\t? tr::lng_group_call_connecting(tr::now)\n\t\t\t\t: mute == MuteState::ForceMuted\n\t\t\t\t? tr::lng_group_call_force_muted(tr::now)\n\t\t\t\t: mute == MuteState::RaisedHand\n\t\t\t\t? tr::lng_group_call_raised_hand(tr::now)\n\t\t\t\t: mute == MuteState::Muted\n\t\t\t\t? tr::lng_group_call_unmute(tr::now)\n\t\t\t\t: tr::lng_group_call_you_are_live(tr::now));\n\t\t_mute->outer()->setAccessibleName(text);\n\t\t_mute->setState(Ui::CallMuteButtonState{\n\t\t\t.text = text,\n\t\t\t.tooltip = ((!scheduleDate && mute == MuteState::Muted)\n\t\t\t\t? tr::lng_group_call_unmute_sub(tr::now)\n\t\t\t\t: QString()),\n\t\t\t.type = (scheduleDate\n\t\t\t\t? (canManage\n\t\t\t\t\t? Type::ScheduledCanStart\n\t\t\t\t\t: scheduleStartSubscribed\n\t\t\t\t\t? Type::ScheduledNotify\n\t\t\t\t\t: Type::ScheduledSilent)\n\t\t\t\t: state == GroupCall::InstanceState::Disconnected\n\t\t\t\t? Type::Connecting\n\t\t\t\t: mute == MuteState::ForceMuted\n\t\t\t\t? (_call->conference()\n\t\t\t\t\t? Type::ConferenceForceMuted\n\t\t\t\t\t: Type::ForceMuted)\n\t\t\t\t: mute == MuteState::RaisedHand\n\t\t\t\t? Type::RaisedHand\n\t\t\t\t: mute == MuteState::Muted\n\t\t\t\t? Type::Muted\n\t\t\t\t: Type::Active),\n\t\t\t.expandType = ((scheduleDate || !_call->rtmp())\n\t\t\t\t? ExpandType::None\n\t\t\t\t: fullScreenOrMaximized\n\t\t\t\t? ExpandType::Expanded\n\t\t\t\t: ExpandType::Normal),\n\t\t});\n\t}, _callLifetime);\n}\n\nvoid Panel::setupScheduledLabels(rpl::producer<TimeId> date) {\n\tusing namespace rpl::mappers;\n\tdate = std::move(date) | rpl::take_while(_1 != 0);\n\t_startsWhen.create(\n\t\twidget(),\n\t\tUi::StartsWhenText(rpl::duplicate(date)),\n\t\tst::groupCallStartsWhen);\n\tauto countdownCreated = std::move(\n\t\tdate\n\t) | rpl::map([=](TimeId date) {\n\t\t_countdownData = std::make_shared<Ui::GroupCallScheduledLeft>(date);\n\t\treturn rpl::empty;\n\t}) | rpl::start_spawning(lifetime());\n\n\t_countdown = Ui::CreateGradientLabel(widget(), rpl::duplicate(\n\t\tcountdownCreated\n\t) | rpl::map([=] {\n\t\treturn _countdownData->text(\n\t\t\tUi::GroupCallScheduledLeft::Negative::Ignore);\n\t}) | rpl::flatten_latest());\n\n\t_startsIn.create(\n\t\twidget(),\n\t\trpl::conditional(\n\t\t\tstd::move(\n\t\t\t\tcountdownCreated\n\t\t\t) | rpl::map(\n\t\t\t\t[=] { return _countdownData->late(); }\n\t\t\t) | rpl::flatten_latest(),\n\t\t\ttr::lng_group_call_late_by(),\n\t\t\ttr::lng_group_call_starts_in()),\n\t\tst::groupCallStartsIn);\n\n\tconst auto top = [=] {\n\t\tconst auto muteTop = widget()->height() - st::groupCallMuteBottomSkip;\n\t\tconst auto membersTop = st::groupCallMembersTop;\n\t\tconst auto height = st::groupCallScheduledBodyHeight;\n\t\treturn (membersTop + (muteTop - membersTop - height) / 2);\n\t};\n\trpl::combine(\n\t\twidget()->sizeValue(),\n\t\t_startsIn->widthValue()\n\t) | rpl::on_next([=](QSize size, int width) {\n\t\t_startsIn->move(\n\t\t\t(size.width() - width) / 2,\n\t\t\ttop() + st::groupCallStartsInTop);\n\t}, _startsIn->lifetime());\n\n\trpl::combine(\n\t\twidget()->sizeValue(),\n\t\t_startsWhen->widthValue()\n\t) | rpl::on_next([=](QSize size, int width) {\n\t\t_startsWhen->move(\n\t\t\t(size.width() - width) / 2,\n\t\t\ttop() + st::groupCallStartsWhenTop);\n\t}, _startsWhen->lifetime());\n\n\trpl::combine(\n\t\twidget()->sizeValue(),\n\t\t_countdown->widthValue()\n\t) | rpl::on_next([=](QSize size, int width) {\n\t\t_countdown->move(\n\t\t\t(size.width() - width) / 2,\n\t\t\ttop() + st::groupCallCountdownTop);\n\t}, _startsWhen->lifetime());\n}\n\nPanelMode Panel::mode() const {\n\treturn _mode.current();\n}\n\nvoid Panel::setupMembers() {\n\tif (_members) {\n\t\treturn;\n\t}\n\n\t_startsIn.destroy();\n\t_countdown.destroy();\n\t_startsWhen.destroy();\n\n\t_members.create(widget(), _call, mode(), _window->backend());\n\n\tsetupVideo(_viewport.get());\n\tsetupVideo(_members->viewport());\n\t_viewport->mouseInsideValue(\n\t) | rpl::filter([=] {\n\t\treturn !_rtmpFull;\n\t}) | rpl::on_next([=](bool inside) {\n\t\ttoggleWideControls(inside);\n\t}, _viewport->lifetime());\n\n\t_members->show();\n\n\tsetupEmptyRtmp();\n\trefreshControlsBackground();\n\traiseControls();\n\n\t_members->desiredHeightValue(\n\t) | rpl::on_next([=] {\n\t\tupdateMembersGeometry();\n\t}, _members->lifetime());\n\n\t_members->toggleMuteRequests(\n\t) | rpl::on_next([=](MuteRequest request) {\n\t\t_call->toggleMute(request);\n\t}, _callLifetime);\n\n\t_members->changeVolumeRequests(\n\t) | rpl::on_next([=](VolumeRequest request) {\n\t\t_call->changeVolume(request);\n\t}, _callLifetime);\n\n\t_members->kickParticipantRequests(\n\t) | rpl::on_next([=](not_null<PeerData*> participantPeer) {\n\t\tkickParticipant(participantPeer);\n\t}, _callLifetime);\n\n\t_members->addMembersRequests(\n\t) | rpl::on_next([=] {\n\t\tif (_call->conference()) {\n\t\t\taddMembers();\n\t\t} else if (!_peer->isBroadcast()\n\t\t\t&& Data::CanSend(_peer, ChatRestriction::SendOther, false)\n\t\t\t&& _call->joinAs()->isSelf()) {\n\t\t\taddMembers();\n\t\t} else if (const auto channel = _peer->asChannel()) {\n\t\t\tif (channel->hasUsername()) {\n\t\t\t\t_callShareLinkCallback();\n\t\t\t}\n\t\t}\n\t}, _callLifetime);\n\n\t_members->shareLinkRequests(\n\t) | rpl::on_next(shareConferenceLinkCallback(), _callLifetime);\n\n\t_call->videoEndpointLargeValue(\n\t) | rpl::on_next([=](const VideoEndpoint &large) {\n\t\tif (large && mode() != PanelMode::Wide) {\n\t\t\tenlargeVideo();\n\t\t}\n\t\t_viewport->showLarge(large);\n\t}, _callLifetime);\n}\n\nFn<void()> Panel::shareConferenceLinkCallback() {\n\treturn [=] {\n\t\tExpects(_call->conference());\n\n\t\tShowConferenceCallLinkBox(uiShow(), _call->sharedCall(), {\n\t\t\t.st = DarkConferenceCallLinkStyle(),\n\t\t});\n\t};\n}\n\nvoid Panel::migrationShowShareLink() {\n\tShowConferenceCallLinkBox(\n\t\tuiShow(),\n\t\t_call->sharedCall(),\n\t\t{ .st = DarkConferenceCallLinkStyle() });\n}\n\nvoid Panel::migrationInviteUsers(std::vector<InviteRequest> users) {\n\tconst auto done = [=](InviteResult result) {\n\t\tuiShow()->showToast({ ComposeInviteResultToast(result) });\n\t};\n\t_call->inviteUsers(std::move(users), crl::guard(this, done));\n}\n\nvoid Panel::enlargeVideo() {\n\t_lastSmallGeometry = window()->geometry();\n\n\tconst auto available = window()->screen()->availableGeometry();\n\tconst auto width = std::max(\n\t\twindow()->width(),\n\t\tstd::max(\n\t\t\tstd::min(available.width(), st::groupCallWideModeSize.width()),\n\t\t\tst::groupCallWideModeWidthMin));\n\tconst auto height = std::max(\n\t\twindow()->height(),\n\t\tstd::min(available.height(), st::groupCallWideModeSize.height()));\n\tauto geometry = QRect(window()->pos(), QSize(width, height));\n\tif (geometry.x() < available.x()) {\n\t\tgeometry.moveLeft(std::min(available.x(), window()->x()));\n\t}\n\tif (geometry.x() + geometry.width()\n\t\t> available.x() + available.width()) {\n\t\tgeometry.moveLeft(std::max(\n\t\t\tavailable.x() + available.width(),\n\t\t\twindow()->x() + window()->width()) - geometry.width());\n\t}\n\tif (geometry.y() < available.y()) {\n\t\tgeometry.moveTop(std::min(available.y(), window()->y()));\n\t}\n\tif (geometry.y() + geometry.height() > available.y() + available.height()) {\n\t\tgeometry.moveTop(std::max(\n\t\t\tavailable.y() + available.height(),\n\t\t\twindow()->y() + window()->height()) - geometry.height());\n\t}\n\tif (_lastLargeMaximized) {\n\t\twindow()->setWindowState(\n\t\t\twindow()->windowState() | Qt::WindowMaximized);\n\t} else {\n\t\twindow()->setGeometry((_lastLargeGeometry\n\t\t\t&& available.intersects(*_lastLargeGeometry))\n\t\t\t? *_lastLargeGeometry\n\t\t\t: geometry);\n\t}\n}\n\nvoid Panel::raiseControls() {\n\tif (_controlsBackgroundWide) {\n\t\t_controlsBackgroundWide->raise();\n\t}\n\tif (_controlsBackgroundNarrow) {\n\t\t_controlsBackgroundNarrow->shadow.raise();\n\t\t_controlsBackgroundNarrow->blocker.raise();\n\t}\n\tconst auto buttons = {\n\t\t&_settings,\n\t\t&_callShare,\n\t\t&_screenShare,\n\t\t&_wideMenu,\n\t\t&_video,\n\t\t&_message,\n\t\t&_hangup\n\t};\n\tfor (const auto button : buttons) {\n\t\tif (const auto raw = button->data()) {\n\t\t\traw->raise();\n\t\t}\n\t}\n\t_mute->raise();\n\tif (_titleBackground) {\n\t\t_titleBackground->raise();\n\t}\n\tif (_title) {\n\t\t_title->raise();\n\t}\n\tif (_viewers) {\n\t\t_titleSeparator->raise();\n\t\t_viewers->raise();\n\t}\n\tif (_menuToggle) {\n\t\t_menuToggle->raise();\n\t}\n\tif (_recordingMark) {\n\t\t_recordingMark->raise();\n\t}\n\tif (_pinOnTop) {\n\t\t_pinOnTop->raise();\n\t}\n\t_messages->raise();\n\tif (_messageField) {\n\t\t_messageField->raise();\n\t}\n\t_window->raiseLayers();\n\tif (_niceTooltip) {\n\t\t_niceTooltip->raise();\n\t}\n}\n\nvoid Panel::setupVideo(not_null<Viewport*> viewport) {\n\tconst auto setupTile = [=](\n\t\t\tconst VideoEndpoint &endpoint,\n\t\t\tconst std::unique_ptr<GroupCall::VideoTrack> &track) {\n\t\tusing namespace rpl::mappers;\n\t\tconst auto row = endpoint.rtmp()\n\t\t\t? _members->rtmpFakeRow(GroupCall::TrackPeer(track)).get()\n\t\t\t: _members->lookupRow(GroupCall::TrackPeer(track));\n\t\tAssert(row != nullptr);\n\n\t\tauto pinned = rpl::combine(\n\t\t\t_call->videoEndpointLargeValue(),\n\t\t\t_call->videoEndpointPinnedValue()\n\t\t) | rpl::map(_1 == endpoint && _2);\n\t\tconst auto self = (endpoint.peer == _call->joinAs());\n\t\tviewport->add(\n\t\t\tendpoint,\n\t\t\tVideoTileTrack{ GroupCall::TrackPointer(track), row },\n\t\t\tGroupCall::TrackSizeValue(track),\n\t\t\tstd::move(pinned),\n\t\t\tself);\n\t};\n\tfor (const auto &[endpoint, track] : _call->activeVideoTracks()) {\n\t\tsetupTile(endpoint, track);\n\t}\n\t_call->videoStreamActiveUpdates(\n\t) | rpl::on_next([=](const VideoStateToggle &update) {\n\t\tif (update.value) {\n\t\t\t// Add async (=> the participant row is definitely in Members).\n\t\t\tconst auto endpoint = update.endpoint;\n\t\t\tcrl::on_main(viewport->widget(), [=] {\n\t\t\t\tconst auto &tracks = _call->activeVideoTracks();\n\t\t\t\tconst auto i = tracks.find(endpoint);\n\t\t\t\tif (i != end(tracks)) {\n\t\t\t\t\tsetupTile(endpoint, i->second);\n\t\t\t\t}\n\t\t\t});\n\t\t} else {\n\t\t\t// Remove sync.\n\t\t\tviewport->remove(update.endpoint);\n\t\t}\n\t}, viewport->lifetime());\n\n\tviewport->pinToggled(\n\t) | rpl::on_next([=](bool pinned) {\n\t\t_call->pinVideoEndpoint(pinned\n\t\t\t? _call->videoEndpointLarge()\n\t\t\t: VideoEndpoint{});\n\t}, viewport->lifetime());\n\n\tviewport->clicks(\n\t) | rpl::on_next([=](VideoEndpoint &&endpoint) {\n\t\tif (_call->videoEndpointLarge() == endpoint) {\n\t\t\t_call->showVideoEndpointLarge({});\n\t\t} else if (_call->videoEndpointPinned()) {\n\t\t\t_call->pinVideoEndpoint(std::move(endpoint));\n\t\t} else {\n\t\t\t_call->showVideoEndpointLarge(std::move(endpoint));\n\t\t}\n\t}, viewport->lifetime());\n\n\tviewport->qualityRequests(\n\t) | rpl::on_next([=](const VideoQualityRequest &request) {\n\t\t_call->requestVideoQuality(request.endpoint, request.quality);\n\t}, viewport->lifetime());\n}\n\nvoid Panel::toggleWideControls(bool shown) {\n\tif (_showWideControls == shown) {\n\t\treturn;\n\t}\n\t_showWideControls = shown;\n\tcrl::on_main(this, [=] {\n\t\tupdateWideControlsVisibility();\n\t});\n}\n\nvoid Panel::updateWideControlsVisibility() {\n\tconst auto shown = _showWideControls\n\t\t|| (_stickedTooltipClose != nullptr)\n\t\t|| _messageTyping.current();\n\tif (_wideControlsShown == shown) {\n\t\treturn;\n\t}\n\t_viewport->setCursorShown(!_rtmpFull || shown);\n\t_wideControlsShown = shown;\n\t_wideControlsAnimation.start(\n\t\t[=] { updateButtonsGeometry(); },\n\t\t_wideControlsShown ? 0. : 1.,\n\t\t_wideControlsShown ? 1. : 0.,\n\t\tst::slideWrapDuration);\n}\n\nvoid Panel::subscribeToChanges(not_null<Data::GroupCall*> real) {\n\tconst auto livestream = real->peer()->isBroadcast();\n\tconst auto validateRecordingMark = [=](bool recording) {\n\t\tif (!recording && _recordingMark) {\n\t\t\t_recordingMark.destroy();\n\t\t} else if (recording && !_recordingMark) {\n\t\t\tstruct State {\n\t\t\t\tUi::Animations::Simple animation;\n\t\t\t\tbase::Timer timer;\n\t\t\t\tbool opaque = true;\n\t\t\t};\n\t\t\t_recordingMark.create(widget());\n\t\t\t_recordingMark->show();\n\t\t\tconst auto state = _recordingMark->lifetime().make_state<State>();\n\t\t\tconst auto size = st::groupCallRecordingMark;\n\t\t\tconst auto skip = st::groupCallRecordingMarkSkip;\n\t\t\t_recordingMark->resize(size + 2 * skip, size + 2 * skip);\n\t\t\t_recordingMark->setClickedCallback([=] {\n\t\t\t\tuiShow()->showToast({ (livestream\n\t\t\t\t\t? tr::lng_group_call_is_recorded_channel\n\t\t\t\t\t: real->recordVideo()\n\t\t\t\t\t? tr::lng_group_call_is_recorded_video\n\t\t\t\t\t: tr::lng_group_call_is_recorded)(tr::now) });\n\t\t\t});\n\t\t\tconst auto animate = [=] {\n\t\t\t\tconst auto opaque = state->opaque;\n\t\t\t\tstate->opaque = !opaque;\n\t\t\t\tstate->animation.start(\n\t\t\t\t\t[=] { _recordingMark->update(); },\n\t\t\t\t\topaque ? 1. : kRecordingOpacity,\n\t\t\t\t\topaque ? kRecordingOpacity : 1.,\n\t\t\t\t\tkRecordingAnimationDuration);\n\t\t\t};\n\t\t\tstate->timer.setCallback(animate);\n\t\t\tstate->timer.callEach(kRecordingAnimationDuration);\n\t\t\tanimate();\n\n\t\t\t_recordingMark->paintRequest(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tauto p = QPainter(_recordingMark.data());\n\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(st::groupCallMemberMutedIcon);\n\t\t\t\tp.setOpacity(state->animation.value(\n\t\t\t\t\tstate->opaque ? 1. : kRecordingOpacity));\n\t\t\t\tp.drawEllipse(skip, skip, size, size);\n\t\t\t}, _recordingMark->lifetime());\n\t\t}\n\t\trefreshTitleGeometry();\n\t};\n\n\tusing namespace rpl::mappers;\n\tconst auto startedAsVideo = std::make_shared<bool>(real->recordVideo());\n\treal->recordStartDateChanges(\n\t) | rpl::map(\n\t\t_1 != 0\n\t) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](bool recorded) {\n\t\tconst auto livestream = _call->peer()->isBroadcast();\n\t\tconst auto isVideo = real->recordVideo();\n\t\tif (recorded) {\n\t\t\t*startedAsVideo = isVideo;\n\t\t\t_call->playSoundRecordingStarted();\n\t\t}\n\t\tvalidateRecordingMark(recorded);\n\t\tuiShow()->showToast((recorded\n\t\t\t? (livestream\n\t\t\t\t? tr::lng_group_call_recording_started_channel\n\t\t\t\t: isVideo\n\t\t\t\t? tr::lng_group_call_recording_started_video\n\t\t\t\t: tr::lng_group_call_recording_started)\n\t\t\t: _call->recordingStoppedByMe()\n\t\t\t? ((*startedAsVideo)\n\t\t\t\t? tr::lng_group_call_recording_saved_video\n\t\t\t\t: tr::lng_group_call_recording_saved)\n\t\t\t: (livestream\n\t\t\t\t? tr::lng_group_call_recording_stopped_channel\n\t\t\t\t: tr::lng_group_call_recording_stopped))(\n\t\t\t\ttr::now,\n\t\t\t\ttr::rich));\n\t}, lifetime());\n\tvalidateRecordingMark(real->recordStartDate() != 0);\n\n\trpl::combine(\n\t\t_call->videoIsWorkingValue(),\n\t\t_call->isSharingCameraValue()\n\t) | rpl::on_next([=] {\n\t\trefreshVideoButtons();\n\t\tshowStickedTooltip();\n\t}, lifetime());\n\n\t_call->messagesEnabledValue() | rpl::on_next([=] {\n\t\tupdateButtonsGeometry();\n\t\traiseControls();\n\t}, lifetime());\n\n\trpl::combine(\n\t\t_call->videoIsWorkingValue(),\n\t\t_call->isSharingScreenValue()\n\t) | rpl::on_next([=] {\n\t\trefreshTopButton();\n\t}, lifetime());\n\n\t_call->mutedValue(\n\t) | rpl::skip(1) | rpl::on_next([=](MuteState state) {\n\t\tupdateButtonsGeometry();\n\t\tif (state == MuteState::Active\n\t\t\t|| state == MuteState::PushToTalk) {\n\t\t\thideStickedTooltip(\n\t\t\t\tStickedTooltip::Microphone,\n\t\t\t\tStickedTooltipHide::Activated);\n\t\t}\n\t\tshowStickedTooltip();\n\t}, lifetime());\n\n\tupdateControlsGeometry();\n}\n\nvoid Panel::createPinOnTop() {\n\t_pinOnTop.create(widget(), st::groupCallPinOnTop);\n\tconst auto pinned = [=] {\n\t\tconst auto handle = window()->windowHandle();\n\t\treturn handle && (handle->flags() & Qt::WindowStaysOnTopHint);\n\t};\n\tconst auto pin = [=](bool pin) {\n\t\tif (const auto handle = window()->windowHandle()) {\n\t\t\thandle->setFlag(Qt::WindowStaysOnTopHint, pin);\n\t\t\t_pinOnTop->setIconOverride(\n\t\t\t\tpin ? &st::groupCallPinnedOnTop : nullptr,\n\t\t\t\tpin ? &st::groupCallPinnedOnTop : nullptr);\n\t\t\tif (!_pinOnTop->isHidden()) {\n\t\t\t\tuiShow()->showToast({ pin\n\t\t\t\t\t? tr::lng_group_call_pinned_on_top(tr::now)\n\t\t\t\t\t: tr::lng_group_call_unpinned_on_top(tr::now) });\n\t\t\t}\n\t\t}\n\t};\n\t_fullScreenOrMaximized.value(\n\t) | rpl::on_next([=](bool fullScreenOrMaximized) {\n\t\t_window->setControlsStyle(fullScreenOrMaximized\n\t\t\t? st::callTitle\n\t\t\t: st::groupCallTitle);\n\n\t\t_pinOnTop->setVisible(!fullScreenOrMaximized);\n\t\tif (fullScreenOrMaximized) {\n#ifdef Q_OS_WIN\n\t\t\tUnpinMaximized(window());\n\t\t\t_unpinnedMaximized = true;\n#else // Q_OS_WIN\n\t\t\tpin(false);\n#endif // Q_OS_WIN\n\n\t\t\t_viewport->rp()->events(\n\t\t\t) | rpl::filter([](not_null<QEvent*> event) {\n\t\t\t\treturn (event->type() == QEvent::MouseMove);\n\t\t\t}) | rpl::on_next([=] {\n\t\t\t\t_hideControlsTimer.callOnce(kHideControlsTimeout);\n\t\t\t\ttoggleWideControls(true);\n\t\t\t}, _hideControlsTimerLifetime);\n\n\t\t\t_hideControlsTimer.callOnce(kHideControlsTimeout);\n\t\t} else {\n\t\t\tif (_unpinnedMaximized) {\n\t\t\t\tpin(false);\n\t\t\t}\n\t\t\t_hideControlsTimerLifetime.destroy();\n\t\t\t_hideControlsTimer.cancel();\n\t\t\trefreshTitleGeometry();\n\t\t}\n\t\trefreshTitleBackground();\n\t\tupdateMembersGeometry();\n\t}, _pinOnTop->lifetime());\n\n\t_pinOnTop->setClickedCallback([=] {\n\t\tpin(!pinned());\n\t});\n\n\tupdateControlsGeometry();\n}\n\nvoid Panel::refreshTopButton() {\n\tif (_call->rtmp() && !_pinOnTop) {\n\t\tcreatePinOnTop();\n\t}\n\tif (_mode.current() == PanelMode::Wide) {\n\t\t_menuToggle.destroy();\n\t\t_joinAsToggle.destroy();\n\t\tupdateButtonsGeometry(); // _wideMenu <-> _settings\n\t\treturn;\n\t}\n\tconst auto hasJoinAs = _call->showChooseJoinAs();\n\tconst auto showNarrowMenu = _call->canManage()\n\t\t|| _call->videoIsWorking();\n\tconst auto showNarrowUserpic = !showNarrowMenu && hasJoinAs;\n\tif (showNarrowMenu) {\n\t\t_joinAsToggle.destroy();\n\t\tif (!_menuToggle) {\n\t\t\t_menuToggle.create(widget(), st::groupCallMenuToggle);\n\t\t\t_menuToggle->show();\n\t\t\t_menuToggle->setAccessibleName(tr::lng_sr_group_call_menu(tr::now));\n\t\t\t_menuToggle->setClickedCallback([=] { showMainMenu(); });\n\t\t\tupdateControlsGeometry();\n\t\t\traiseControls();\n\t\t}\n\t} else if (showNarrowUserpic) {\n\t\t_menuToggle.destroy();\n\t\trpl::single(\n\t\t\t_call->joinAs()\n\t\t) | rpl::then(_call->rejoinEvents(\n\t\t) | rpl::map([](const RejoinEvent &event) {\n\t\t\treturn event.nowJoinAs;\n\t\t})) | rpl::on_next([=](not_null<PeerData*> joinAs) {\n\t\t\tauto joinAsToggle = object_ptr<Ui::UserpicButton>(\n\t\t\t\twidget(),\n\t\t\t\tjoinAs,\n\t\t\t\tst::groupCallJoinAsToggle);\n\t\t\t_joinAsToggle.destroy();\n\t\t\t_joinAsToggle = std::move(joinAsToggle);\n\t\t\t_joinAsToggle->show();\n\t\t\t_joinAsToggle->setClickedCallback([=] {\n\t\t\t\tchooseJoinAs();\n\t\t\t});\n\t\t\tupdateControlsGeometry();\n\t\t}, lifetime());\n\t} else {\n\t\t_menuToggle.destroy();\n\t\t_joinAsToggle.destroy();\n\t}\n}\n\nvoid Panel::screenSharingPrivacyRequest() {\n\tif (auto box = ScreenSharingPrivacyRequestBox()) {\n\t\tuiShow()->showBox(std::move(box));\n\t}\n}\n\nvoid Panel::chooseShareScreenSource() {\n\tif (_call->emitShareScreenError()) {\n\t\treturn;\n\t}\n\tconst auto choose = [=] {\n\t\tconst auto env = &Core::App().mediaDevices();\n\t\tif (!env->desktopCaptureAllowed()) {\n\t\t\tscreenSharingPrivacyRequest();\n\t\t} else if (const auto source = env->uniqueDesktopCaptureSource()) {\n\t\t\tif (_call->isSharingScreen()) {\n\t\t\t\t_call->toggleScreenSharing(std::nullopt);\n\t\t\t} else if (chooseSourceWithAudioSupported()) {\n\t\t\t\tconst auto sourceId = *source;\n\t\t\t\tShowUniqueCaptureOptions(\n\t\t\t\t\tuiShow(),\n\t\t\t\t\tcrl::guard(this, [=](bool audio) {\n\t\t\t\t\t\tchooseSourceAccepted(sourceId, audio);\n\t\t\t\t\t}));\n\t\t\t} else {\n\t\t\t\tchooseSourceAccepted(*source, false);\n\t\t\t}\n\t\t} else {\n\t\t\tUi::DesktopCapture::ChooseSource(this);\n\t\t}\n\t};\n\tconst auto screencastFromPeer = [&]() -> PeerData* {\n\t\tfor (const auto &[endpoint, track] : _call->activeVideoTracks()) {\n\t\t\tif (endpoint.type == VideoEndpointType::Screen) {\n\t\t\t\treturn endpoint.peer;\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t}();\n\tif (!screencastFromPeer || _call->isSharingScreen()) {\n\t\tchoose();\n\t\treturn;\n\t}\n\tconst auto text = tr::lng_group_call_sure_screencast(\n\t\ttr::now,\n\t\tlt_user,\n\t\tscreencastFromPeer->shortName());\n\tconst auto shared = std::make_shared<base::weak_qptr<Ui::GenericBox>>();\n\tconst auto done = [=] {\n\t\tif (*shared) {\n\t\t\tbase::take(*shared)->closeBox();\n\t\t}\n\t\tchoose();\n\t};\n\tauto box = ConfirmBox({\n\t\t.text = text,\n\t\t.confirmed = done,\n\t\t.confirmText = tr::lng_continue(),\n\t});\n\t*shared = box.data();\n\tuiShow()->showBox(std::move(box));\n}\n\nvoid Panel::chooseJoinAs() {\n\tconst auto context = ChooseJoinAsProcess::Context::Switch;\n\tconst auto callback = [=](JoinInfo info) {\n\t\t_call->rejoinAs(info);\n\t};\n\t_joinAsProcess.start(\n\t\t_peer,\n\t\tcontext,\n\t\tuiShow(),\n\t\tcallback,\n\t\t_call->joinAs());\n}\n\nvoid Panel::showMainMenu() {\n\tif (_menu) {\n\t\treturn;\n\t}\n\tconst auto wide = (_mode.current() == PanelMode::Wide) && _wideMenu;\n\tif (!wide && !_menuToggle) {\n\t\treturn;\n\t}\n\t_menu.create(widget(), st::groupCallDropdownMenu);\n\tFillMenu(\n\t\t_menu.data(),\n\t\t_peer,\n\t\t_call,\n\t\twide,\n\t\t[=] { chooseJoinAs(); },\n\t\t[=] { chooseShareScreenSource(); },\n\t\t[=](auto box) { uiShow()->showBox(std::move(box)); });\n\tif (_menu->empty()) {\n\t\t_wideMenuShown = false;\n\t\t_menu.destroy();\n\t\treturn;\n\t}\n\n\tconst auto raw = _menu.data();\n\traw->setHiddenCallback([=] {\n\t\traw->deleteLater();\n\t\tif (_menu == raw) {\n\t\t\t_menu = nullptr;\n\t\t\t_wideMenuShown = false;\n\t\t\t_trackControlsMenuLifetime.destroy();\n\t\t\tif (_menuToggle) {\n\t\t\t\t_menuToggle->setForceRippled(false);\n\t\t\t}\n\t\t}\n\t});\n\traw->setShowStartCallback([=] {\n\t\tif (_menu == raw) {\n\t\t\tif (wide) {\n\t\t\t\t_wideMenuShown = true;\n\t\t\t} else if (_menuToggle) {\n\t\t\t\t_menuToggle->setForceRippled(true);\n\t\t\t}\n\t\t}\n\t});\n\traw->setHideStartCallback([=] {\n\t\tif (_menu == raw) {\n\t\t\t_wideMenuShown = false;\n\t\t\tif (_menuToggle) {\n\t\t\t\t_menuToggle->setForceRippled(false);\n\t\t\t}\n\t\t}\n\t});\n\n\tif (wide) {\n\t\t_wideMenu->installEventFilter(_menu);\n\t\ttrackControl(_menu, _trackControlsMenuLifetime);\n\n\t\tconst auto x = st::groupCallWideMenuPosition.x();\n\t\tconst auto y = st::groupCallWideMenuPosition.y();\n\t\t_menu->moveToLeft(\n\t\t\t_wideMenu->x() + x,\n\t\t\t_wideMenu->y() - _menu->height() + y);\n\t\t_menu->showAnimated(Ui::PanelAnimation::Origin::BottomLeft);\n\t} else {\n\t\t_menuToggle->installEventFilter(_menu);\n\t\tconst auto x = st::groupCallMenuPosition.x();\n\t\tconst auto y = st::groupCallMenuPosition.y();\n\t\tif (_menuToggle->x() > widget()->width() / 2) {\n\t\t\t_menu->moveToRight(x, y);\n\t\t\t_menu->showAnimated(Ui::PanelAnimation::Origin::TopRight);\n\t\t} else {\n\t\t\t_menu->moveToLeft(x, y);\n\t\t\t_menu->showAnimated(Ui::PanelAnimation::Origin::TopLeft);\n\t\t}\n\t}\n}\n\nvoid Panel::addMembers() {\n\tconst auto &appConfig = _call->peer()->session().appConfig();\n\tconst auto conferenceLimit = appConfig.confcallSizeLimit();\n\tif (_call->conference()\n\t\t&& _call->sharedCall()->fullCount() >= conferenceLimit) {\n\t\tuiShow()->showToast({ tr::lng_group_call_invite_limit(tr::now) });\n\t}\n\tconst auto showToastCallback = [=](TextWithEntities &&text) {\n\t\tuiShow()->showToast(std::move(text));\n\t};\n\tconst auto link = _call->conference()\n\t\t? shareConferenceLinkCallback()\n\t\t: nullptr;\n\tif (auto box = PrepareInviteBox(_call, showToastCallback, link)) {\n\t\tuiShow()->showBox(std::move(box));\n\t}\n}\n\nvoid Panel::kickParticipant(not_null<PeerData*> participantPeer) {\n\tuiShow()->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox.get(),\n\t\t\t\t(!participantPeer->isUser()\n\t\t\t\t\t? (_peer->isBroadcast()\n\t\t\t\t\t\t? tr::lng_group_call_remove_channel_from_channel\n\t\t\t\t\t\t: tr::lng_group_call_remove_channel)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_channel,\n\t\t\t\t\t\t\tparticipantPeer->name())\n\t\t\t\t\t: (_call->conference()\n\t\t\t\t\t\t? tr::lng_confcall_sure_remove\n\t\t\t\t\t\t: _peer->isBroadcast()\n\t\t\t\t\t\t? tr::lng_profile_sure_kick_channel\n\t\t\t\t\t\t: tr::lng_profile_sure_kick)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\t\tparticipantPeer->asUser()->firstName)),\n\t\t\t\tst::groupCallBoxLabel),\n\t\t\tstyle::margins(\n\t\t\t\tst::boxRowPadding.left(),\n\t\t\t\tst::boxPadding.top(),\n\t\t\t\tst::boxRowPadding.right(),\n\t\t\t\tst::boxPadding.bottom()));\n\t\tbox->addButton(tr::lng_box_remove(), [=] {\n\t\t\tbox->closeBox();\n\t\t\tkickParticipantSure(participantPeer);\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t}));\n}\n\nvoid Panel::kickParticipantSure(not_null<PeerData*> participantPeer) {\n\tif (_call->conference()) {\n\t\tif (const auto user = participantPeer->asUser()) {\n\t\t\t_call->removeConferenceParticipants({ peerToUser(user->id) });\n\t\t}\n\t} else if (const auto chat = _peer->asChat()) {\n\t\tchat->session().api().chatParticipants().kick(chat, participantPeer);\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\tconst auto currentRestrictedRights = [&] {\n\t\t\tconst auto user = participantPeer->asUser();\n\t\t\tif (!channel->mgInfo || !user) {\n\t\t\t\treturn ChatRestrictionsInfo();\n\t\t\t}\n\t\t\tconst auto i = channel->mgInfo->lastRestricted.find(user);\n\t\t\treturn (i != channel->mgInfo->lastRestricted.cend())\n\t\t\t\t? i->second.rights\n\t\t\t\t: ChatRestrictionsInfo();\n\t\t}();\n\t\tchannel->session().api().chatParticipants().kick(\n\t\t\tchannel,\n\t\t\tparticipantPeer,\n\t\t\tcurrentRestrictedRights);\n\t}\n}\n\nvoid Panel::initLayout(ConferencePanelMigration info) {\n\tinitGeometry(info);\n\n\t_window->raiseControls();\n\n\t_window->controlsLayoutChanges(\n\t) | rpl::on_next([=] {\n\t\t// _menuToggle geometry depends on _controls arrangement.\n\t\tcrl::on_main(this, [=] { updateControlsGeometry(); });\n\t}, lifetime());\n\n\traiseControls();\n\tupdateControlsGeometry();\n}\n\nvoid Panel::showControls() {\n\tExpects(_call != nullptr);\n\n\twidget()->showChildren();\n}\n\nvoid Panel::closeBeforeDestroy() {\n\twindow()->close();\n\t_callLifetime.destroy();\n}\n\nrpl::lifetime &Panel::lifetime() {\n\treturn _lifetime;\n}\n\nvoid Panel::initGeometry(ConferencePanelMigration info) {\n\tconst auto minWidth = _call->rtmp()\n\t\t? st::groupCallWidthRtmpMin\n\t\t: st::groupCallWidth;\n\tconst auto minHeight = _call->rtmp()\n\t\t? st::groupCallHeightRtmpMin\n\t\t: st::groupCallHeight;\n\tif (!info.window) {\n\t\tconst auto center = Core::App().getPointForCallPanelCenter();\n\t\tconst auto width = _call->rtmp()\n\t\t\t? st::groupCallWidthRtmp\n\t\t\t: st::groupCallWidth;\n\t\tconst auto height = _call->rtmp()\n\t\t\t? st::groupCallHeightRtmp\n\t\t\t: st::groupCallHeight;\n\t\tconst auto rect = QRect(0, 0, width, height);\n\t\twindow()->setGeometry(rect.translated(center - rect.center()));\n\t}\n\twindow()->setMinimumSize({ minWidth, minHeight });\n\twindow()->show();\n}\n\nQRect Panel::computeTitleRect() const {\n\tconst auto skip = st::groupCallTitleSeparator;\n\tconst auto remove = skip\n\t\t+ (_menuToggle\n\t\t\t? (_menuToggle->width() + st::groupCallMenuTogglePosition.x())\n\t\t\t: 0)\n\t\t+ (_joinAsToggle\n\t\t\t? (_joinAsToggle->width() + st::groupCallMenuTogglePosition.x())\n\t\t\t: 0)\n\t\t+ (_pinOnTop\n\t\t\t? (_pinOnTop->width() + skip)\n\t\t\t: 0);\n\tconst auto width = widget()->width();\n#ifdef Q_OS_MAC\n\treturn QRect(70, 0, width - remove - 70, 28);\n#else // Q_OS_MAC\n\tconst auto controls = _window->controlsGeometry();\n\tconst auto right = controls.x() + controls.width() + skip;\n\treturn (controls.center().x() < width / 2)\n\t\t? QRect(right, 0, width - right - remove, controls.height())\n\t\t: QRect(remove, 0, controls.x() - skip - remove, controls.height());\n#endif // !Q_OS_MAC\n}\n\nbool Panel::updateMode() {\n\tif (!_viewport) {\n\t\treturn false;\n\t}\n\tconst auto wide = _call->rtmp()\n\t\t|| (_call->hasVideoWithFrames()\n\t\t\t&& (widget()->width() >= st::groupCallWideModeWidthMin));\n\tconst auto mode = wide ? PanelMode::Wide : PanelMode::Default;\n\tif (_mode.current() == mode) {\n\t\treturn false;\n\t}\n\tif (!wide && _call->videoEndpointLarge()) {\n\t\t_call->showVideoEndpointLarge({});\n\t}\n\trefreshVideoButtons(wide);\n\tif (!_stickedTooltipClose\n\t\t|| _niceTooltipControl.data() != _mute->outer().get()) {\n\t\t_niceTooltip.destroy();\n\t}\n\t_mode = mode;\n\trefreshTitleColors();\n\tif (wide && _subtitle) {\n\t\t_subtitle.destroy();\n\t} else if (!wide && !_subtitle) {\n\t\trefreshTitle();\n\t} else if (!_members) {\n\t\tsetupMembers();\n\t}\n\t_wideControlsShown = _showWideControls = true;\n\t_wideControlsAnimation.stop();\n\t_viewport->widget()->setVisible(wide);\n\tif (_members) {\n\t\t_members->setMode(mode);\n\t}\n\tupdateButtonsStyles();\n\trefreshControlsBackground();\n\tupdateControlsGeometry();\n\tshowStickedTooltip();\n\treturn true;\n}\n\nvoid Panel::updateButtonsStyles() {\n\tconst auto wide = (_mode.current() == PanelMode::Wide);\n\t_mute->setStyle(wide ? st::callMuteButtonSmall : st::callMuteButton);\n\tif (_video) {\n\t\t_video->setStyle(\n\t\t\twide ? st::groupCallVideoSmall : st::groupCallVideo,\n\t\t\t(wide\n\t\t\t\t? &st::groupCallVideoActiveSmall\n\t\t\t\t: &st::groupCallVideoActive));\n\t\t_video->setText(wide\n\t\t\t? rpl::single(QString())\n\t\t\t: tr::lng_group_call_video());\n\t}\n\tif (_message) {\n\t\t_message->setStyle(\n\t\t\twide ? st::groupCallMessageSmall : st::groupCallMessage,\n\t\t\t(wide\n\t\t\t\t? &st::groupCallMessageActiveSmall\n\t\t\t\t: &st::groupCallMessageActive));\n\t\t_message->setText(wide\n\t\t\t? rpl::single(QString())\n\t\t\t: tr::lng_group_call_message());\n\t}\n\tif (_settings) {\n\t\t_settings->setText(wide\n\t\t\t? rpl::single(QString())\n\t\t\t: tr::lng_group_call_settings());\n\t\t_settings->setStyle(wide\n\t\t\t? st::groupCallSettingsSmall\n\t\t\t: st::groupCallSettings);\n\t}\n\t_hangup->setText(wide\n\t\t? rpl::single(QString())\n\t\t: _call->scheduleDate()\n\t\t? tr::lng_group_call_close()\n\t\t: tr::lng_group_call_leave());\n\t_hangup->setStyle(wide\n\t\t? st::groupCallHangupSmall\n\t\t: st::groupCallHangup);\n}\n\nvoid Panel::setupEmptyRtmp() {\n\t_call->emptyRtmpValue(\n\t) | rpl::on_next([=](bool empty) {\n\t\tif (!empty) {\n\t\t\t_emptyRtmp.destroy();\n\t\t\treturn;\n\t\t} else if (_emptyRtmp) {\n\t\t\treturn;\n\t\t}\n\t\tstruct Label {\n\t\t\tLabel(\n\t\t\t\tQWidget *parent,\n\t\t\t\trpl::producer<QString> text,\n\t\t\t\tconst style::color &color)\n\t\t\t: widget(parent, std::move(text), st::groupCallVideoLimitLabel)\n\t\t\t, corners(st::groupCallControlsBackRadius, color) {\n\t\t\t}\n\n\t\t\tUi::FlatLabel widget;\n\t\t\tUi::RoundRect corners;\n\t\t};\n\t\t_emptyRtmp.create(widget());\n\t\tconst auto label = _emptyRtmp->lifetime().make_state<Label>(\n\t\t\t_emptyRtmp.data(),\n\t\t\t(_call->rtmpInfo().url.isEmpty()\n\t\t\t\t? tr::lng_group_call_no_stream(\n\t\t\t\t\tlt_group,\n\t\t\t\t\trpl::single(_peer->name()))\n\t\t\t\t: tr::lng_group_call_no_stream_admin()),\n\t\t\t_controlsBackgroundColor.color());\n\t\t_emptyRtmp->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t_emptyRtmp->show();\n\t\t_emptyRtmp->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = QPainter(_emptyRtmp.data());\n\t\t\tlabel->corners.paint(p, _emptyRtmp->rect());\n\t\t}, _emptyRtmp->lifetime());\n\n\t\twidget()->sizeValue(\n\t\t) | rpl::on_next([=](QSize size) {\n\t\t\tconst auto padding = st::groupCallWidth / 30;\n\t\t\tconst auto width = std::min(\n\t\t\t\tsize.width() - padding * 4,\n\t\t\t\tst::groupCallWidth);\n\t\t\tlabel->widget.resizeToWidth(width);\n\t\t\tlabel->widget.move(padding, padding);\n\t\t\t_emptyRtmp->resize(\n\t\t\t\twidth + 2 * padding,\n\t\t\t\tlabel->widget.height() + 2 * padding);\n\t\t\t_emptyRtmp->move(\n\t\t\t\t(size.width() - _emptyRtmp->width()) / 2,\n\t\t\t\t(size.height() - _emptyRtmp->height()) / 3);\n\t\t}, _emptyRtmp->lifetime());\n\n\t\traiseControls();\n\t}, lifetime());\n\n}\n\nvoid Panel::refreshControlsBackground() {\n\tif (!_members) {\n\t\treturn;\n\t}\n\tif (mode() == PanelMode::Default) {\n\t\ttrackControls(false);\n\t\t_controlsBackgroundWide.destroy();\n\t\tif (_controlsBackgroundNarrow) {\n\t\t\treturn;\n\t\t}\n\t\tsetupControlsBackgroundNarrow();\n\t} else {\n\t\t_controlsBackgroundNarrow = nullptr;\n\t\tif (_controlsBackgroundWide) {\n\t\t\treturn;\n\t\t}\n\t\tsetupControlsBackgroundWide();\n\t}\n\traiseControls();\n\tupdateButtonsGeometry();\n}\n\nvoid Panel::refreshTitleBackground() {\n\tif (!_rtmpFull) {\n\t\t_titleBackground.destroy();\n\t\treturn;\n\t} else if (_titleBackground) {\n\t\treturn;\n\t}\n\t_titleBackground.create(widget());\n\t_titleBackground->show();\n\traiseControls();\n\tauto &lifetime = _titleBackground->lifetime();\n\tconst auto corners = lifetime.make_state<Ui::RoundRect>(\n\t\tst::roundRadiusLarge,\n\t\t_controlsBackgroundColor.color());\n\t_titleBackground->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(_titleBackground.data());\n\t\tcorners->paintSomeRounded(\n\t\t\tp,\n\t\t\t_titleBackground->rect(),\n\t\t\tRectPart::FullBottom);\n\t}, lifetime);\n\trefreshTitleGeometry();\n}\n\nvoid Panel::setupControlsBackgroundNarrow() {\n\t_controlsBackgroundNarrow = std::make_unique<ControlsBackgroundNarrow>(\n\t\twidget());\n\t_controlsBackgroundNarrow->shadow.show();\n\t_controlsBackgroundNarrow->blocker.show();\n\tauto &lifetime = _controlsBackgroundNarrow->shadow.lifetime();\n\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto height = std::max(\n\t\tst::groupCallMembersShadowHeight,\n\t\tst::groupCallMembersFadeSkip + st::groupCallMembersFadeHeight);\n\t_controlsBackgroundNarrow->shadowHeight = height;\n\tconst auto full = lifetime.make_state<QImage>(\n\t\tQSize(1, height * factor),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\trpl::single(rpl::empty) | rpl::then(\n\t\tstyle::PaletteChanged()\n\t) | rpl::on_next([=] {\n\t\tfull->fill(Qt::transparent);\n\n\t\tauto p = QPainter(full);\n\t\tconst auto bottom = (height - st::groupCallMembersFadeSkip) * factor;\n\t\tp.fillRect(\n\t\t\t0,\n\t\t\tbottom,\n\t\t\tfull->width(),\n\t\t\tst::groupCallMembersFadeSkip * factor,\n\t\t\tst::groupCallMembersBg);\n\t\tp.drawImage(\n\t\t\tQRect(\n\t\t\t\t0,\n\t\t\t\tbottom - (st::groupCallMembersFadeHeight * factor),\n\t\t\t\tfull->width(),\n\t\t\t\tst::groupCallMembersFadeHeight * factor),\n\t\t\tImages::GenerateShadow(\n\t\t\t\tst::groupCallMembersFadeHeight,\n\t\t\t\t0,\n\t\t\t\t255,\n\t\t\t\tst::groupCallMembersBg->c));\n\t\tp.drawImage(\n\t\t\tQRect(\n\t\t\t\t0,\n\t\t\t\t(height - st::groupCallMembersShadowHeight) * factor,\n\t\t\t\tfull->width(),\n\t\t\t\tst::groupCallMembersShadowHeight * factor),\n\t\t\tImages::GenerateShadow(\n\t\t\t\tst::groupCallMembersShadowHeight,\n\t\t\t\t0,\n\t\t\t\t255,\n\t\t\t\tst::groupCallBg->c));\n\t}, lifetime);\n\n\t_controlsBackgroundNarrow->shadow.resize(\n\t\t(widget()->width()\n\t\t\t- st::groupCallMembersMargin.left()\n\t\t\t- st::groupCallMembersMargin.right()),\n\t\theight);\n\t_controlsBackgroundNarrow->shadow.paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(&_controlsBackgroundNarrow->shadow);\n\t\tclip = clip.intersected(_controlsBackgroundNarrow->shadow.rect());\n\t\tconst auto inner = _members->getInnerGeometry().translated(\n\t\t\t_members->x() - _controlsBackgroundNarrow->shadow.x(),\n\t\t\t_members->y() - _controlsBackgroundNarrow->shadow.y());\n\t\tconst auto bottom = _controlsBackgroundNarrow->shadowHeight;\n\t\tconst auto faded = clip.intersected(inner).intersected(\n\t\t\tQRect(clip.x(), 0, clip.width(), bottom));\n\t\tif (!faded.isEmpty()) {\n\t\t\tconst auto factor = style::DevicePixelRatio();\n\t\t\tp.drawImage(\n\t\t\t\tfaded,\n\t\t\t\t*full,\n\t\t\t\tQRect(\n\t\t\t\t\t0,\n\t\t\t\t\tfaded.y() * factor,\n\t\t\t\t\tfull->width(),\n\t\t\t\t\tfaded.height() * factor));\n\t\t}\n\t\tconst auto after = clip.intersected(QRect(\n\t\t\tinner.x(),\n\t\t\tbottom,\n\t\t\tinner.width(),\n\t\t\t_controlsBackgroundNarrow->shadow.height() - bottom));\n\t\tif (!after.isEmpty()) {\n\t\t\tp.fillRect(after, st::groupCallBg);\n\t\t}\n\t}, lifetime);\n\t_controlsBackgroundNarrow->shadow.setAttribute(\n\t\tQt::WA_TransparentForMouseEvents);\n\t_controlsBackgroundNarrow->blocker.setUpdatesEnabled(false);\n}\n\nvoid Panel::setupControlsBackgroundWide() {\n\t_controlsBackgroundWide.create(widget());\n\t_controlsBackgroundWide->show();\n\tauto &lifetime = _controlsBackgroundWide->lifetime();\n\tconst auto corners = lifetime.make_state<Ui::RoundRect>(\n\t\tst::groupCallControlsBackRadius,\n\t\t_controlsBackgroundColor.color());\n\t_controlsBackgroundWide->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(_controlsBackgroundWide.data());\n\t\tcorners->paint(p, _controlsBackgroundWide->rect());\n\t}, lifetime);\n\n\ttrackControls(true);\n}\n\nvoid Panel::trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime) {\n\tif (!widget) {\n\t\treturn;\n\t}\n\tconst auto over = std::make_shared<bool>();\n\twidget->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::Enter) {\n\t\t\t// Enter events may come from widget destructors,\n\t\t\t// in that case sync-showing tooltip (calling Grab)\n\t\t\t// crashes the whole thing.\n\t\t\t*over = true;\n\t\t\tcrl::on_main(widget, [=] {\n\t\t\t\tif (*over) {\n\t\t\t\t\ttrackControlOver(widget, true);\n\t\t\t\t}\n\t\t\t});\n\t\t\ttoggleWideControls(true);\n\t\t} else if (type == QEvent::Leave) {\n\t\t\t*over = false;\n\t\t\tcrl::on_main(widget, [=] {\n\t\t\t\tif (!*over) {\n\t\t\t\t\ttrackControlOver(widget, false);\n\t\t\t\t}\n\t\t\t});\n\t\t\ttoggleWideControls(false);\n\t\t}\n\t}, lifetime);\n}\n\nvoid Panel::trackControlOver(not_null<Ui::RpWidget*> control, bool over) {\n\tif (_rtmpFull) {\n\t\treturn;\n\t} else if (_stickedTooltipClose) {\n\t\tif (!over) {\n\t\t\treturn;\n\t\t}\n\t} else {\n\t\thideNiceTooltip();\n\t}\n\tif (over) {\n\t\tUi::Integration::Instance().registerLeaveSubscription(control);\n\t\tshowNiceTooltip(control);\n\t} else {\n\t\tUi::Integration::Instance().unregisterLeaveSubscription(control);\n\t}\n}\n\nvoid Panel::showStickedTooltip() {\n\tstatic const auto kHasCamera = !Core::App().mediaDevices().defaultId(\n\t\tWebrtc::DeviceType::Camera).isEmpty();\n\tconst auto callReady = (_call->state() == State::Joined\n\t\t|| _call->state() == State::Connecting);\n\tif (!(_stickedTooltipsShown & StickedTooltip::Camera)\n\t\t&& callReady\n\t\t&& (_mode.current() == PanelMode::Wide)\n\t\t&& _video\n\t\t&& _call->videoIsWorking()\n\t\t&& !_call->mutedByAdmin()\n\t\t&& kHasCamera) { // Don't recount this every time for now.\n\t\tshowNiceTooltip(_video, NiceTooltipType::Sticked);\n\t\treturn;\n\t}\n\thideStickedTooltip(\n\t\tStickedTooltip::Camera,\n\t\tStickedTooltipHide::Unavailable);\n\n\tif (!(_stickedTooltipsShown & StickedTooltip::Microphone)\n\t\t&& callReady\n\t\t&& _mute\n\t\t&& !_call->mutedByAdmin()\n\t\t&& !_window->topShownLayer()) {\n\t\tif (_stickedTooltipClose) {\n\t\t\t// Showing already.\n\t\t\treturn;\n\t\t} else if (!_micLevelTester) {\n\t\t\t// Check if there is incoming sound.\n\t\t\t_micLevelTester = std::make_unique<MicLevelTester>([=] {\n\t\t\t\tshowStickedTooltip();\n\t\t\t});\n\t\t}\n\t\tif (_micLevelTester->showTooltip()) {\n\t\t\t_micLevelTester = nullptr;\n\t\t\tshowNiceTooltip(_mute->outer(), NiceTooltipType::Sticked);\n\t\t}\n\t\treturn;\n\t}\n\t_micLevelTester = nullptr;\n\thideStickedTooltip(\n\t\tStickedTooltip::Microphone,\n\t\tStickedTooltipHide::Unavailable);\n}\n\nvoid Panel::showNiceTooltip(\n\t\tnot_null<Ui::RpWidget*> control,\n\t\tNiceTooltipType type) {\n\tauto text = [&]() -> rpl::producer<QString> {\n\t\tif (control == _screenShare.data()) {\n\t\t\tif (_call->mutedByAdmin()) {\n\t\t\t\treturn nullptr;\n\t\t\t}\n\t\t\treturn tr::lng_group_call_tooltip_screen();\n\t\t} else if (control == _video.data()) {\n\t\t\tif (_call->mutedByAdmin()) {\n\t\t\t\treturn nullptr;\n\t\t\t}\n\t\t\treturn _call->isSharingCameraValue(\n\t\t\t) | rpl::map([=](bool sharing) {\n\t\t\t\treturn sharing\n\t\t\t\t\t? tr::lng_group_call_tooltip_camera_off()\n\t\t\t\t\t: tr::lng_group_call_tooltip_camera();\n\t\t\t}) | rpl::flatten_latest();\n\t\t} else if (control == _settings.data()) {\n\t\t\treturn tr::lng_group_call_settings();\n\t\t} else if (control == _mute->outer()) {\n\t\t\treturn MuteButtonTooltip(_call);\n\t\t} else if (control == _hangup.data()) {\n\t\t\treturn tr::lng_group_call_leave();\n\t\t} else if (control == _message.data()) {\n\t\t\treturn tr::lng_group_call_message();\n\t\t}\n\t\treturn rpl::producer<QString>();\n\t}();\n\tif (!text || _stickedTooltipClose) {\n\t\treturn;\n\t} else if (_wideControlsAnimation.animating() || !_wideControlsShown) {\n\t\tif (type == NiceTooltipType::Normal) {\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto inner = [&]() -> Ui::RpWidget* {\n\t\tconst auto normal = (type == NiceTooltipType::Normal);\n\t\tauto container = normal\n\t\t\t? nullptr\n\t\t\t: Ui::CreateChild<Ui::RpWidget>(widget().get());\n\t\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t(normal ? widget().get() : container),\n\t\t\tstd::move(text),\n\t\t\tst::groupCallNiceTooltipLabel);\n\t\tlabel->resizeToWidth(label->textMaxWidth());\n\t\tif (normal) {\n\t\t\treturn label;\n\t\t}\n\t\tconst auto button = Ui::CreateChild<Ui::IconButton>(\n\t\t\tcontainer,\n\t\t\tst::groupCallStickedTooltipClose);\n\t\trpl::combine(\n\t\t\tlabel->sizeValue(),\n\t\t\tbutton->sizeValue()\n\t\t) | rpl::on_next([=](QSize text, QSize close) {\n\t\t\tconst auto height = std::max(text.height(), close.height());\n\t\t\tcontainer->resize(text.width() + close.width(), height);\n\t\t\tlabel->move(0, (height - text.height()) / 2);\n\t\t\tbutton->move(text.width(), (height - close.height()) / 2);\n\t\t}, container->lifetime());\n\t\tbutton->setClickedCallback([=] {\n\t\t\thideStickedTooltip(StickedTooltipHide::Discarded);\n\t\t});\n\t\t_stickedTooltipClose = button;\n\t\tupdateWideControlsVisibility();\n\t\treturn container;\n\t}();\n\t_niceTooltip.create(\n\t\twidget().get(),\n\t\tobject_ptr<Ui::RpWidget>::fromRaw(inner),\n\t\t(type == NiceTooltipType::Sticked\n\t\t\t? st::groupCallStickedTooltip\n\t\t\t: st::groupCallNiceTooltip));\n\tconst auto tooltip = _niceTooltip.data();\n\tconst auto weak = base::make_weak(tooltip);\n\tconst auto destroy = [=] {\n\t\tdelete weak.get();\n\t};\n\tif (type != NiceTooltipType::Sticked) {\n\t\ttooltip->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t}\n\ttooltip->setHiddenCallback(destroy);\n\tbase::qt_signal_producer(\n\t\tcontrol.get(),\n\t\t&QObject::destroyed\n\t) | rpl::on_next(destroy, tooltip->lifetime());\n\n\t_niceTooltipControl = control;\n\tupdateTooltipGeometry();\n\ttooltip->toggleAnimated(true);\n}\n\nvoid Panel::updateTooltipGeometry() {\n\tif (!_niceTooltip) {\n\t\treturn;\n\t} else if (!_niceTooltipControl) {\n\t\thideNiceTooltip();\n\t\treturn;\n\t}\n\tconst auto geometry = _niceTooltipControl->geometry();\n\tconst auto weak = base::make_weak(_niceTooltip);\n\tconst auto countPosition = [=](QSize size) {\n\t\tconst auto strong = weak.get();\n\t\tconst auto wide = (_mode.current() == PanelMode::Wide);\n\t\tconst auto top = geometry.y()\n\t\t\t- (wide ? st::groupCallNiceTooltipTop : 0)\n\t\t\t- size.height();\n\t\tconst auto middle = geometry.center().x();\n\t\tif (!strong) {\n\t\t\treturn QPoint();\n\t\t} else if (!wide) {\n\t\t\treturn QPoint(\n\t\t\t\tstd::max(\n\t\t\t\t\tstd::min(\n\t\t\t\t\t\tmiddle - size.width() / 2,\n\t\t\t\t\t\t(widget()->width()\n\t\t\t\t\t\t\t- st::groupCallMembersMargin.right()\n\t\t\t\t\t\t\t- size.width())),\n\t\t\t\t\tst::groupCallMembersMargin.left()),\n\t\t\t\ttop);\n\t\t}\n\t\tconst auto back = _controlsBackgroundWide.data();\n\t\tif (size.width() >= _viewport->widget()->width()) {\n\t\t\treturn QPoint(_viewport->widget()->x(), top);\n\t\t} else if (back && size.width() >= back->width()) {\n\t\t\treturn QPoint(\n\t\t\t\tback->x() - (size.width() - back->width()) / 2,\n\t\t\t\ttop);\n\t\t} else if (back && (middle - back->x() < size.width() / 2)) {\n\t\t\treturn QPoint(back->x(), top);\n\t\t} else if (back\n\t\t\t&& (back->x() + back->width() - middle < size.width() / 2)) {\n\t\t\treturn QPoint(back->x() + back->width() - size.width(), top);\n\t\t} else {\n\t\t\treturn QPoint(middle - size.width() / 2, top);\n\t\t}\n\t};\n\t_niceTooltip->pointAt(geometry, RectPart::Top, countPosition);\n}\n\nvoid Panel::trackControls(bool track, bool force) {\n\tif (!force && _trackControls == track) {\n\t\treturn;\n\t}\n\t_trackControls = track;\n\t_trackControlsOverStateLifetime.destroy();\n\t_trackControlsMenuLifetime.destroy();\n\tif (!track) {\n\t\ttoggleWideControls(true);\n\t\tif (_wideControlsAnimation.animating()) {\n\t\t\t_wideControlsAnimation.stop();\n\t\t\tupdateButtonsGeometry();\n\t\t}\n\t\treturn;\n\t}\n\n\tconst auto trackOne = [=](auto &&widget) {\n\t\ttrackControl(widget, _trackControlsOverStateLifetime);\n\t};\n\ttrackOne(_mute->outer());\n\ttrackOne(_video);\n\ttrackOne(_message);\n\ttrackOne(_screenShare);\n\ttrackOne(_wideMenu);\n\ttrackOne(_settings);\n\ttrackOne(_hangup);\n\ttrackOne(_controlsBackgroundWide);\n\ttrackControl(_menu, _trackControlsMenuLifetime);\n}\n\nvoid Panel::updateControlsGeometry() {\n\tif (widget()->size().isEmpty() || (!_settings && !_callShare)) {\n\t\treturn;\n\t}\n\tupdateButtonsGeometry();\n\tupdateMembersGeometry();\n\trefreshTitle();\n\n#ifdef Q_OS_MAC\n\tconst auto controlsOnTheLeft = true;\n\tconst auto controlsPadding = 0;\n#else // Q_OS_MAC\n\tconst auto center = _window->controlsGeometry().center();\n\tconst auto controlsOnTheLeft = center.x()\n\t\t< widget()->width() / 2;\n\tconst auto controlsPadding = _window->controlsWrapTop();\n#endif // Q_OS_MAC\n\tconst auto menux = st::groupCallMenuTogglePosition.x();\n\tconst auto menuy = st::groupCallMenuTogglePosition.y();\n\tif (controlsOnTheLeft) {\n\t\tif (_pinOnTop) {\n\t\t\t_pinOnTop->moveToRight(controlsPadding, controlsPadding);\n\t\t}\n\t\tif (_menuToggle) {\n\t\t\t_menuToggle->moveToRight(menux, menuy);\n\t\t} else if (_joinAsToggle) {\n\t\t\t_joinAsToggle->moveToRight(menux, menuy);\n\t\t}\n\t} else {\n\t\tif (_pinOnTop) {\n\t\t\t_pinOnTop->moveToLeft(controlsPadding, controlsPadding);\n\t\t}\n\t\tif (_menuToggle) {\n\t\t\t_menuToggle->moveToLeft(menux, menuy);\n\t\t} else if (_joinAsToggle) {\n\t\t\t_joinAsToggle->moveToLeft(menux, menuy);\n\t\t}\n\t}\n}\n\nvoid Panel::updateButtonsGeometry() {\n\tif (widget()->size().isEmpty() || (!_settings && !_callShare)) {\n\t\treturn;\n\t}\n\tconst auto toggle = [](auto &widget, bool shown) {\n\t\tif (widget && widget->isHidden() == shown) {\n\t\t\twidget->setVisible(shown);\n\t\t}\n\t};\n\tconst auto messagesEnabled = _call->messagesEnabled();\n\tauto messagesBottomSkip = 0;\n\tif (mode() == PanelMode::Wide) {\n\t\tAssert(_video != nullptr);\n\t\tAssert(_message != nullptr);\n\t\tAssert(_screenShare != nullptr);\n\t\tAssert(_wideMenu != nullptr);\n\t\tAssert(_settings != nullptr);\n\t\tAssert(_callShare == nullptr);\n\n\t\tconst auto rtmp = _call->rtmp();\n\t\tconst auto shown = _wideControlsAnimation.value(\n\t\t\t_wideControlsShown ? 1. : 0.);\n\t\tconst auto hidden = (shown == 0.);\n\n\t\tif (_viewport) {\n\t\t\t_viewport->setControlsShown(rtmp ? 0. : shown);\n\t\t}\n\n\t\tconst auto addSkip = st::callMuteButtonSmall.active.outerRadius;\n\t\tconst auto muteSize = _mute->innerSize().width() + 2 * addSkip;\n\t\tconst auto skip = st::groupCallButtonSkipSmall;\n\t\tconst auto fullWidth = (rtmp ? 0 : (_video->width() + skip))\n\t\t\t+ (rtmp ? 0 : (_message->width() + skip))\n\t\t\t+ (muteSize + skip)\n\t\t\t+ (_settings->width() + skip)\n\t\t\t+ _hangup->width();\n\t\tconst auto membersSkip = st::groupCallNarrowSkip;\n\t\tconst auto membersWidth = rtmp\n\t\t\t? membersSkip\n\t\t\t: (st::groupCallNarrowMembersWidth + 2 * membersSkip);\n\t\tauto left = membersSkip + (widget()->width()\n\t\t\t- membersWidth\n\t\t\t- membersSkip\n\t\t\t- fullWidth) / 2;\n\n\t\tconst auto forMessagesLeft = left\n\t\t\t- st::groupCallControlsBackMargin.left();\n\t\tconst auto forMessagesWidth = fullWidth\n\t\t\t+ st::groupCallControlsBackMargin.left()\n\t\t\t+ st::groupCallControlsBackMargin.right();\n\t\tconst auto existingBottomSkip = st::groupCallButtonBottomSkipWide\n\t\t\t- _hangup->height()\n\t\t\t- st::groupCallControlsBackMargin.bottom();\n\t\tif (_messageField) {\n\t\t\t_messageField->resizeToWidth(forMessagesWidth);\n\t\t\tmessagesBottomSkip += _messageField->height();\n\n\t\t\tconst auto y = widget()->height()\n\t\t\t\t- messagesBottomSkip\n\t\t\t\t- (existingBottomSkip / 2);\n\t\t\t_messageField->move(forMessagesLeft, y);\n\t\t}\n\n\t\tconst auto buttonsTop = widget()->height()\n\t\t\t- messagesBottomSkip\n\t\t\t- anim::interpolate(0, st::groupCallButtonBottomSkipWide, shown);\n\t\tconst auto muteTop = buttonsTop + addSkip;\n\n\t\tconst auto forMessagesBottom = buttonsTop\n\t\t\t- st::groupCallControlsBackMargin.top()\n\t\t\t- (existingBottomSkip / 6);\n\t\tconst auto forMessagesHeight = forMessagesBottom\n\t\t\t- (st::groupCallWideVideoTop * 1.5);\n\n\t\t_messages->move(\n\t\t\tforMessagesLeft,\n\t\t\tforMessagesBottom,\n\t\t\tforMessagesWidth,\n\t\t\tforMessagesHeight);\n\n\t\ttoggle(_screenShare, !hidden && !rtmp && !messagesEnabled);\n\t\ttoggle(_message, !hidden && !rtmp && messagesEnabled);\n\t\tif (!rtmp && !messagesEnabled) {\n\t\t\t_screenShare->moveToLeft(left, buttonsTop);\n\t\t\tleft += _screenShare->width() + skip;\n\t\t} else if (!rtmp) {\n\t\t\t_wideMenu->moveToLeft(left, buttonsTop);\n\t\t\t_settings->moveToLeft(left, buttonsTop);\n\t\t\tleft += _settings->width() + skip;\n\t\t}\n\n\t\tconst auto wideMenuShown = _call->canManage()\n\t\t\t|| _call->showChooseJoinAs()\n\t\t\t|| (!rtmp && messagesEnabled); // Screen share there.\n\t\ttoggle(_settings, !hidden && !wideMenuShown);\n\t\ttoggle(_wideMenu, !hidden && wideMenuShown);\n\n\t\ttoggle(_video, !hidden && !rtmp);\n\t\tif (!rtmp) {\n\t\t\t_video->moveToLeft(left, buttonsTop);\n\t\t\tleft += _video->width() + skip;\n\t\t} else {\n\t\t\t_wideMenu->moveToLeft(left, buttonsTop);\n\t\t\t_settings->moveToLeft(left, buttonsTop);\n\t\t\tleft += _settings->width() + skip;\n\t\t}\n\t\ttoggle(_mute, !hidden);\n\t\t_mute->moveInner({ left + addSkip, muteTop });\n\t\tleft += muteSize + skip;\n\t\tif (!rtmp && messagesEnabled) {\n\t\t\t_message->moveToLeft(left, buttonsTop);\n\t\t\tleft += _message->width() + skip;\n\t\t} else if (!rtmp) {\n\t\t\t_wideMenu->moveToLeft(left, buttonsTop);\n\t\t\t_settings->moveToLeft(left, buttonsTop);\n\t\t\tleft += _settings->width() + skip;\n\t\t}\n\t\ttoggle(_hangup, !hidden);\n\t\t_hangup->moveToLeft(left, buttonsTop);\n\t\tleft += _hangup->width();\n\t\tif (_controlsBackgroundWide) {\n\t\t\tconst auto rect = QRect(\n\t\t\t\tleft - fullWidth,\n\t\t\t\tbuttonsTop,\n\t\t\t\tfullWidth,\n\t\t\t\t_hangup->height());\n\t\t\t_controlsBackgroundWide->setGeometry(\n\t\t\t\trect.marginsAdded(st::groupCallControlsBackMargin));\n\t\t}\n\t\tif (_rtmpFull) {\n\t\t\trefreshTitleGeometry();\n\t\t}\n\t} else {\n\t\tconst auto addSkip = st::callMuteButton.active.outerRadius;\n\t\tconst auto muteSize = _mute->innerSize().width();\n\t\tconst auto single = (_settings ? _settings : _callShare)->width();\n\t\tconst auto showVideoButton = videoButtonInNarrowMode();\n\t\tconst auto four = !_callShare && !showVideoButton && messagesEnabled;\n\t\tconst auto five = !four && !_callShare && messagesEnabled;\n\t\tconst auto buttonSkip = four\n\t\t\t? (st::groupCallButtonSkip / 2)\n\t\t\t: five\n\t\t\t? ((st::groupCallWidth - 5 * single) / 6)\n\t\t\t: st::groupCallButtonSkip;\n\t\tconst auto fullWidth = five\n\t\t\t? st::groupCallWidth\n\t\t\t: four\n\t\t\t? (4 * single + 3 * buttonSkip)\n\t\t\t: (muteSize + 2 * (single + st::groupCallButtonSkip));\n\t\tconst auto forMessagesWidth = st::groupCallWidth\n\t\t\t- st::groupCallMembersMargin.left()\n\t\t\t- st::groupCallMembersMargin.right();\n\t\tconst auto forMessagesLeft = (widget()->width() - forMessagesWidth)\n\t\t\t/ 2;\n\t\tconst auto existingBottomSkip = st::groupCallButtonBottomSkip\n\t\t\t- _mute->innerSize().height();\n\t\tif (_messageField) {\n\t\t\t_messageField->resizeToWidth(forMessagesWidth);\n\n\t\t\tconst auto height = _messageField->height();\n\t\t\tmessagesBottomSkip += height;\n\n\t\t\tconst auto y = widget()->height()\n\t\t\t\t- messagesBottomSkip\n\t\t\t\t- (existingBottomSkip / 3);\n\t\t\t_messageField->move(forMessagesLeft, y);\n\t\t}\n\n\t\tconst auto buttonsTop = widget()->height()\n\t\t\t- messagesBottomSkip\n\t\t\t- st::groupCallButtonBottomSkip;\n\t\tconst auto muteTop = buttonsTop + addSkip;\n\t\t//const auto muteTop = widget()->height()\n\t\t//\t- messagesBottomSkip\n\t\t//\t- st::groupCallMuteBottomSkip;\n\t\tconst auto forMessagesBottom = muteTop\n\t\t\t- (existingBottomSkip / 3);\n\t\tconst auto forMessagesHeight = forMessagesBottom\n\t\t\t- st::groupCallMembersTop\n\t\t\t- (st::normalFont->height / 2);\n\n\t\t_messages->move(\n\t\t\tforMessagesLeft,\n\t\t\tforMessagesBottom,\n\t\t\tforMessagesWidth,\n\t\t\tforMessagesHeight);\n\n\t\ttoggle(_mute, true);\n\t\tconst auto leftButtonLeft = (widget()->width() - fullWidth) / 2\n\t\t\t+ (five ? buttonSkip : 0);\n\t\tconst auto nextButtonLeft = leftButtonLeft\n\t\t\t+ ((five || four) ? (single + buttonSkip) : 0);\n\t\tconst auto muteButtonLeft = four\n\t\t\t? (nextButtonLeft + addSkip)\n\t\t\t: ((widget()->width() - muteSize) / 2);\n\t\t_mute->moveInner({ muteButtonLeft, muteTop });\n\t\ttoggle(_screenShare, false);\n\t\ttoggle(_wideMenu, false);\n\t\ttoggle(_callShare, true);\n\t\tif (_callShare) {\n\t\t\t_callShare->moveToLeft(leftButtonLeft, buttonsTop);\n\t\t}\n\t\ttoggle(_video, !_callShare && showVideoButton);\n\t\tif (_video) {\n\t\t\t_video->setStyle(st::groupCallVideo, &st::groupCallVideoActive);\n\t\t\t_video->moveToLeft(leftButtonLeft, buttonsTop);\n\t\t}\n\t\ttoggle(_settings, !_callShare && (five || !showVideoButton));\n\t\tif (_settings) {\n\t\t\t_settings->moveToLeft(\n\t\t\t\tfour ? leftButtonLeft : nextButtonLeft,\n\t\t\t\tbuttonsTop);\n\t\t}\n\t\ttoggle(_message, !_callShare && messagesEnabled);\n\t\tif (_message) {\n\t\t\t_message->moveToRight(nextButtonLeft, buttonsTop);\n\t\t}\n\n\t\ttoggle(_hangup, true);\n\t\t_hangup->moveToRight(leftButtonLeft, buttonsTop);\n\t}\n\tif (_controlsBackgroundNarrow) {\n\t\tconst auto left = st::groupCallMembersMargin.left();\n\t\tconst auto width = (widget()->width()\n\t\t\t- st::groupCallMembersMargin.left()\n\t\t\t- st::groupCallMembersMargin.right());\n\t\t_controlsBackgroundNarrow->shadow.setGeometry(\n\t\t\tleft,\n\t\t\t(widget()->height()\n\t\t\t\t- messagesBottomSkip\n\t\t\t\t- st::groupCallMembersMargin.bottom()\n\t\t\t\t- _controlsBackgroundNarrow->shadowHeight),\n\t\t\twidth,\n\t\t\tmessagesBottomSkip + _controlsBackgroundNarrow->shadowHeight);\n\t\t_controlsBackgroundNarrow->blocker.setGeometry(\n\t\t\tleft,\n\t\t\t(widget()->height()\n\t\t\t\t- messagesBottomSkip\n\t\t\t\t- st::groupCallMembersMargin.bottom()\n\t\t\t\t- st::groupCallMembersBottomSkip),\n\t\t\twidth,\n\t\t\tmessagesBottomSkip + st::groupCallMembersBottomSkip);\n\t}\n\tupdateTooltipGeometry();\n}\n\nbool Panel::videoButtonInNarrowMode() const {\n\treturn (_video != nullptr) && !_call->mutedByAdmin();\n}\n\nvoid Panel::updateMembersGeometry() {\n\tif (!_members) {\n\t\treturn;\n\t}\n\t_members->setVisible(!_call->rtmp());\n\tconst auto desiredHeight = _members->desiredHeight();\n\tif (mode() == PanelMode::Wide) {\n\t\tconst auto skip = _rtmpFull ? 0 : st::groupCallNarrowSkip;\n\t\tconst auto membersWidth = st::groupCallNarrowMembersWidth;\n\t\tconst auto top = _rtmpFull ? 0 : st::groupCallWideVideoTop;\n\t\t_members->setGeometry(\n\t\t\twidget()->width() - skip - membersWidth,\n\t\t\ttop,\n\t\t\tmembersWidth,\n\t\t\tstd::min(desiredHeight, widget()->height() - top - skip));\n\t\tconst auto viewportSkip = _call->rtmp()\n\t\t\t? 0\n\t\t\t: (skip + membersWidth);\n\t\t_viewport->setGeometry(_rtmpFull, {\n\t\t\tskip,\n\t\t\ttop,\n\t\t\twidget()->width() - viewportSkip - 2 * skip,\n\t\t\twidget()->height() - top - skip,\n\t\t});\n\t} else {\n\t\tconst auto membersBottom = widget()->height();\n\t\tconst auto membersTop = st::groupCallMembersTop;\n\t\tconst auto availableHeight = membersBottom\n\t\t\t- st::groupCallMembersMargin.bottom()\n\t\t\t- membersTop;\n\t\tconst auto membersWidthAvailable = widget()->width()\n\t\t\t- st::groupCallMembersMargin.left()\n\t\t\t- st::groupCallMembersMargin.right();\n\t\tconst auto membersWidthMin = st::groupCallWidth\n\t\t\t- st::groupCallMembersMargin.left()\n\t\t\t- st::groupCallMembersMargin.right();\n\t\tconst auto membersWidth = std::clamp(\n\t\t\tmembersWidthAvailable,\n\t\t\tmembersWidthMin,\n\t\t\tst::groupCallMembersWidthMax);\n\t\t_members->setGeometry(\n\t\t\t(widget()->width() - membersWidth) / 2,\n\t\t\tmembersTop,\n\t\t\tmembersWidth,\n\t\t\tstd::min(desiredHeight, availableHeight));\n\t}\n}\n\nrpl::producer<QString> Panel::titleText() {\n\tif (_call->conference()) {\n\t\treturn tr::lng_confcall_join_title();\n\t}\n\treturn rpl::combine(\n\t\tInfo::Profile::NameValue(_peer),\n\t\trpl::single(\n\t\t\tQString()\n\t\t) | rpl::then(_call->real(\n\t\t) | rpl::map([=](not_null<Data::GroupCall*> real) {\n\t\treturn real->titleValue();\n\t}) | rpl::flatten_latest())\n\t) | rpl::map([=](const QString &name, const QString &title) {\n\t\treturn title.isEmpty() ? name : title;\n\t});\n}\n\nvoid Panel::refreshTitle() {\n\tif (!_title) {\n\t\tauto text = titleText() | rpl::after_next([=] {\n\t\t\trefreshTitleGeometry();\n\t\t});\n\t\t_title.create(\n\t\t\twidget(),\n\t\t\trpl::duplicate(text),\n\t\t\tst::groupCallTitleLabel);\n\t\t_title->show();\n\t\t_title->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tif (_call->rtmp()) {\n\t\t\t_titleSeparator.create(\n\t\t\t\twidget(),\n\t\t\t\trpl::single(Ui::kQBullet),\n\t\t\t\tst::groupCallTitleLabel);\n\t\t\t_titleSeparator->show();\n\t\t\t_titleSeparator->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\tauto countText = _call->real(\n\t\t\t) | rpl::map([=](not_null<Data::GroupCall*> real) {\n\t\t\t\treturn tr::lng_group_call_rtmp_viewers(\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\treal->fullCountValue(\n\t\t\t\t\t) | rpl::map([=](int count) {\n\t\t\t\t\t\treturn std::max(float64(count), 1.);\n\t\t\t\t\t}));\n\t\t\t}) | rpl::flatten_latest(\n\t\t\t) | rpl::after_next([=] {\n\t\t\t\trefreshTitleGeometry();\n\t\t\t});\n\t\t\t_viewers.create(\n\t\t\t\twidget(),\n\t\t\t\tstd::move(countText),\n\t\t\t\tst::groupCallTitleLabel);\n\t\t\t_viewers->show();\n\t\t\t_viewers->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t}\n\n\t\trefreshTitleColors();\n\t\tstyle::PaletteChanged(\n\t\t) | rpl::on_next([=] {\n\t\t\trefreshTitleColors();\n\t\t}, _title->lifetime());\n\t}\n\trefreshTitleGeometry();\n\tif (!_subtitle && mode() == PanelMode::Default) {\n\t\t_subtitle.create(\n\t\t\twidget(),\n\t\t\trpl::single(\n\t\t\t\t_call->scheduleDate()\n\t\t\t) | rpl::then(\n\t\t\t\t_call->real(\n\t\t\t\t) | rpl::map([=](not_null<Data::GroupCall*> real) {\n\t\t\t\t\treturn real->scheduleDateValue();\n\t\t\t\t}) | rpl::flatten_latest()\n\t\t\t) | rpl::map([=](TimeId scheduleDate) {\n\t\t\t\tif (scheduleDate) {\n\t\t\t\t\treturn tr::lng_group_call_scheduled_status();\n\t\t\t\t} else if (!_members) {\n\t\t\t\t\tsetupMembers();\n\t\t\t\t}\n\t\t\t\treturn tr::lng_group_call_members(\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t_members->fullCountValue() | rpl::map([](int value) {\n\t\t\t\t\t\treturn (value > 0) ? float64(value) : 1.;\n\t\t\t\t\t}));\n\t\t\t}) | rpl::flatten_latest(),\n\t\t\tst::groupCallSubtitleLabel);\n\t\t_subtitle->show();\n\t\t_subtitle->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t}\n\tif (_subtitle) {\n\t\tconst auto top = _title\n\t\t\t? st::groupCallSubtitleTop\n\t\t\t: st::groupCallTitleTop;\n\t\t_subtitle->moveToLeft(\n\t\t\t(widget()->width() - _subtitle->width()) / 2,\n\t\t\ttop);\n\t}\n}\n\nvoid Panel::refreshTitleGeometry() {\n\tif (!_title) {\n\t\treturn;\n\t}\n\tconst auto fullRect = computeTitleRect();\n\tconst auto titleRect = _recordingMark\n\t\t? QRect(\n\t\t\tfullRect.x(),\n\t\t\tfullRect.y(),\n\t\t\tfullRect.width() - _recordingMark->width(),\n\t\t\tfullRect.height())\n\t\t: fullRect;\n\tconst auto sep = st::groupCallTitleSeparator;\n\tconst auto best = _title->textMaxWidth() + (_viewers\n\t\t? (_titleSeparator->width() + sep * 2 + _viewers->textMaxWidth())\n\t\t: 0);\n\tconst auto from = (widget()->width() - best) / 2;\n\tconst auto shownTop = (mode() == PanelMode::Default)\n\t\t? st::groupCallTitleTop\n\t\t: (st::groupCallWideVideoTop\n\t\t\t- st::groupCallTitleLabel.style.font->height) / 2;\n\tconst auto shown = _rtmpFull\n\t\t? _wideControlsAnimation.value(_wideControlsShown ? 1. : 0.)\n\t\t: 1.;\n\tconst auto top = anim::interpolate(\n\t\t-_title->height() - st::boxRadius,\n\t\tshownTop,\n\t\tshown);\n\tconst auto left = titleRect.x();\n\n\tconst auto notEnough = std::max(0, best - titleRect.width());\n\tconst auto titleMaxWidth = _title->textMaxWidth();\n\tconst auto viewersMaxWidth = _viewers ? _viewers->textMaxWidth() : 0;\n\tconst auto viewersNotEnough = std::clamp(\n\t\tviewersMaxWidth - titleMaxWidth,\n\t\t0,\n\t\tnotEnough\n\t) + std::max(\n\t\t(notEnough - std::abs(viewersMaxWidth - titleMaxWidth)) / 2,\n\t\t0);\n\t_title->resizeToWidth(\n\t\t_title->textMaxWidth() - (notEnough - viewersNotEnough));\n\tif (_viewers) {\n\t\t_viewers->resizeToWidth(_viewers->textMaxWidth() - viewersNotEnough);\n\t}\n\tconst auto layout = [&](int position) {\n\t\t_title->moveToLeft(position, top);\n\t\tposition += _title->width();\n\t\tif (_viewers) {\n\t\t\t_titleSeparator->moveToLeft(position + sep, top);\n\t\t\tposition += sep + _titleSeparator->width() + sep;\n\t\t\t_viewers->moveToLeft(position, top);\n\t\t\tposition += _viewers->width();\n\t\t}\n\t\tif (_recordingMark) {\n\t\t\tconst auto markTop = top + st::groupCallRecordingMarkTop;\n\t\t\t_recordingMark->move(\n\t\t\t\tposition,\n\t\t\t\tmarkTop - st::groupCallRecordingMarkSkip);\n\t\t}\n\t\tif (_titleBackground) {\n\t\t\tconst auto bottom = _title->y()\n\t\t\t\t+ _title->height()\n\t\t\t\t+ (st::boxRadius / 2);\n\t\t\tconst auto height = std::max(bottom, st::boxRadius * 2);\n\t\t\t_titleBackground->setGeometry(\n\t\t\t\t_title->x() - st::boxRadius,\n\t\t\t\tbottom - height,\n\t\t\t\t(position - _title->x()\n\t\t\t\t\t+ st::boxRadius\n\t\t\t\t\t+ (_recordingMark\n\t\t\t\t\t\t? (_recordingMark->width() + st::boxRadius / 2)\n\t\t\t\t\t\t: st::boxRadius)),\n\t\t\t\theight);\n\t\t}\n\t};\n\n\tif (from >= left && from + best <= left + titleRect.width()) {\n\t\tlayout(from);\n\t} else if (titleRect.width() < best) {\n\t\tlayout(left);\n\t} else if (from < left) {\n\t\tlayout(left);\n\t} else {\n\t\tlayout(left + titleRect.width() - best);\n\t}\n\t_window->setControlsShown(shown);\n}\n\nvoid Panel::refreshTitleColors() {\n\tif (!_title) {\n\t\treturn;\n\t}\n\tauto gray = st::groupCallMemberNotJoinedStatus->c;\n\tconst auto wide = (_mode.current() == PanelMode::Wide);\n\t_title->setTextColorOverride(wide\n\t\t? std::make_optional(gray)\n\t\t: std::nullopt);\n\tif (_viewers) {\n\t\t_viewers->setTextColorOverride(gray);\n\t\tgray.setAlphaF(gray.alphaF() * 0.5);\n\t\t_titleSeparator->setTextColorOverride(gray);\n\t}\n}\n\nvoid Panel::paint(QRect clip) {\n\tauto p = QPainter(widget());\n\n\tauto region = QRegion(clip);\n\tfor (const auto &rect : region) {\n\t\tp.fillRect(rect, st::groupCallBg);\n\t}\n}\n\nbool Panel::handleClose() {\n\tif (_call) {\n\t\twindow()->hide();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nnot_null<Window*> Panel::callWindow() const {\n\treturn _window.get();\n}\n\nnot_null<Ui::RpWindow*> Panel::window() const {\n\treturn _window->window();\n}\n\nnot_null<Ui::RpWidget*> Panel::widget() const {\n\treturn _window->widget();\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_panel.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/group/calls_choose_join_as.h\"\n#include \"calls/group/ui/desktop_capture_choose_source.h\"\n#include \"ui/effects/animations.h\"\n\nclass Image;\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nclass PhotoMedia;\nclass GroupCall;\n} // namespace Data\n\nnamespace Ui {\nclass Show;\nclass BoxContent;\nclass AbstractButton;\nclass ImportantTooltip;\nclass DropdownMenu;\nclass CallButton;\nclass CallMuteButton;\nclass IconButton;\nclass FlatLabel;\nclass RpWidget;\nclass RpWindow;\ntemplate <typename Widget>\nclass FadeWrap;\ntemplate <typename Widget>\nclass PaddingWrap;\nclass ScrollArea;\nclass GenericBox;\nclass GroupCallScheduledLeft;\nstruct CallButtonColors;\n} // namespace Ui\n\nnamespace Ui::Toast {\nclass Instance;\nstruct Config;\n} // namespace Ui::Toast\n\nnamespace style {\nstruct CallSignalBars;\nstruct CallBodyLayout;\n} // namespace style\n\nnamespace Calls {\nstruct InviteRequest;\nstruct ConferencePanelMigration;\nclass Window;\n} // namespace Calls\n\nnamespace Calls::Group {\n\nclass Toasts;\nclass Members;\nclass Viewport;\nenum class PanelMode;\nenum class StickedTooltip;\nclass MicLevelTester;\nclass MessageField;\nclass MessagesUi;\n\nclass Panel final\n\t: public base::has_weak_ptr\n\t, private Ui::DesktopCapture::ChooseSourceDelegate {\npublic:\n\texplicit Panel(not_null<GroupCall*> call);\n\tPanel(not_null<GroupCall*> call, ConferencePanelMigration info);\n\t~Panel();\n\n\t[[nodiscard]] not_null<Ui::RpWidget*> widget() const;\n\t[[nodiscard]] not_null<GroupCall*> call() const;\n\t[[nodiscard]] bool isVisible() const;\n\t[[nodiscard]] bool isActive() const;\n\n\tvoid migrationShowShareLink();\n\tvoid migrationInviteUsers(std::vector<InviteRequest> users);\n\n\tvoid minimize();\n\tvoid toggleFullScreen();\n\tvoid toggleFullScreen(bool fullscreen);\n\tvoid close();\n\tvoid showAndActivate();\n\tvoid closeBeforeDestroy();\n\n\t[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow();\n\t[[nodiscard]] not_null<Window*> callWindow() const;\n\t[[nodiscard]] not_null<Ui::RpWindow*> window() const;\n\n\trpl::lifetime &lifetime();\n\nprivate:\n\tusing State = GroupCall::State;\n\tstruct ControlsBackgroundNarrow;\n\n\tenum class NiceTooltipType {\n\t\tNormal,\n\t\tSticked,\n\t};\n\tenum class StickedTooltipHide {\n\t\tUnavailable,\n\t\tActivated,\n\t\tDiscarded,\n\t};\n\n\t[[nodiscard]] PanelMode mode() const;\n\n\tvoid paint(QRect clip);\n\n\tvoid initWindow();\n\tvoid initWidget();\n\tvoid initControls();\n\tvoid initShareAction();\n\tvoid initLayout(ConferencePanelMigration info);\n\tvoid initGeometry(ConferencePanelMigration info);\n\tvoid setupScheduledLabels(rpl::producer<TimeId> date);\n\tvoid setupMembers();\n\tvoid setupVideo(not_null<Viewport*> viewport);\n\tvoid setupRealMuteButtonState(not_null<Data::GroupCall*> real);\n\t[[nodiscard]] rpl::producer<QString> titleText();\n\n\tbool handleClose();\n\tvoid startScheduledNow();\n\tvoid trackControls(bool track, bool force = false);\n\tvoid raiseControls();\n\tvoid enlargeVideo();\n\n\tvoid trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime);\n\tvoid trackControlOver(not_null<Ui::RpWidget*> control, bool over);\n\tvoid showNiceTooltip(\n\t\tnot_null<Ui::RpWidget*> control,\n\t\tNiceTooltipType type = NiceTooltipType::Normal);\n\tvoid showStickedTooltip();\n\tvoid hideStickedTooltip(StickedTooltipHide hide);\n\tvoid hideStickedTooltip(StickedTooltip type, StickedTooltipHide hide);\n\tvoid hideNiceTooltip();\n\n\tbool updateMode();\n\tvoid updateControlsGeometry();\n\tvoid updateButtonsGeometry();\n\tvoid updateTooltipGeometry();\n\tvoid updateButtonsStyles();\n\tvoid updateMembersGeometry();\n\tvoid refreshControlsBackground();\n\tvoid refreshTitleBackground();\n\tvoid setupControlsBackgroundWide();\n\tvoid setupControlsBackgroundNarrow();\n\tvoid showControls();\n\tvoid createMessageButton();\n\tvoid refreshLeftButton();\n\tvoid refreshVideoButtons(\n\t\tstd::optional<bool> overrideWideMode = std::nullopt);\n\tvoid refreshTopButton();\n\tvoid createPinOnTop();\n\tvoid setupEmptyRtmp();\n\tvoid toggleWideControls(bool shown);\n\tvoid updateWideControlsVisibility();\n\t[[nodiscard]] bool videoButtonInNarrowMode() const;\n\t[[nodiscard]] Fn<void()> shareConferenceLinkCallback();\n\tvoid toggleMessageTyping();\n\t[[nodiscard]] rpl::producer<Ui::CallButtonColors> toggleableOverrides(\n\t\trpl::producer<bool> active);\n\n\tvoid endCall();\n\n\tvoid showMainMenu();\n\tvoid chooseJoinAs();\n\tvoid chooseShareScreenSource();\n\tvoid screenSharingPrivacyRequest();\n\tvoid addMembers();\n\tvoid kickParticipant(not_null<PeerData*> participantPeer);\n\tvoid kickParticipantSure(not_null<PeerData*> participantPeer);\n\t[[nodiscard]] QRect computeTitleRect() const;\n\tvoid refreshTitle();\n\tvoid refreshTitleGeometry();\n\tvoid refreshTitleColors();\n\tvoid setupRealCallViewers();\n\tvoid subscribeToChanges(not_null<Data::GroupCall*> real);\n\n\tvoid migrate(not_null<ChannelData*> channel);\n\tvoid subscribeToPeerChanges();\n\n\tQWidget *chooseSourceParent() override;\n\tQString chooseSourceActiveDeviceId() override;\n\tbool chooseSourceActiveWithAudio() override;\n\tbool chooseSourceWithAudioSupported() override;\n\trpl::lifetime &chooseSourceInstanceLifetime() override;\n\tvoid chooseSourceAccepted(\n\t\tconst QString &deviceId,\n\t\tbool withAudio) override;\n\tvoid chooseSourceStop() override;\n\n\tconst not_null<GroupCall*> _call;\n\tnot_null<PeerData*> _peer;\n\n\tstd::shared_ptr<Window> _window;\n\trpl::variable<PanelMode> _mode;\n\trpl::variable<bool> _fullScreenOrMaximized = false;\n\tbool _unpinnedMaximized = false;\n\tbool _rtmpFull = false;\n\n\trpl::lifetime _callLifetime;\n\n\tobject_ptr<Ui::RpWidget> _titleBackground = { nullptr };\n\tobject_ptr<Ui::FlatLabel> _title = { nullptr };\n\tobject_ptr<Ui::FlatLabel> _titleSeparator = { nullptr };\n\tobject_ptr<Ui::FlatLabel> _viewers = { nullptr };\n\tobject_ptr<Ui::FlatLabel> _subtitle = { nullptr };\n\tobject_ptr<Ui::AbstractButton> _recordingMark = { nullptr };\n\tobject_ptr<Ui::IconButton> _menuToggle = { nullptr };\n\tobject_ptr<Ui::IconButton> _pinOnTop = { nullptr };\n\tobject_ptr<Ui::DropdownMenu> _menu = { nullptr };\n\trpl::variable<bool> _wideMenuShown = false;\n\trpl::variable<bool> _messageTyping = false;\n\tobject_ptr<Ui::AbstractButton> _joinAsToggle = { nullptr };\n\tobject_ptr<Members> _members = { nullptr };\n\tstd::unique_ptr<Viewport> _viewport;\n\trpl::lifetime _trackControlsOverStateLifetime;\n\trpl::lifetime _trackControlsMenuLifetime;\n\tobject_ptr<Ui::FlatLabel> _startsIn = { nullptr };\n\tobject_ptr<Ui::RpWidget> _countdown = { nullptr };\n\tstd::shared_ptr<Ui::GroupCallScheduledLeft> _countdownData;\n\tobject_ptr<Ui::FlatLabel> _startsWhen = { nullptr };\n\tobject_ptr<Ui::RpWidget> _emptyRtmp = { nullptr };\n\tChooseJoinAsProcess _joinAsProcess;\n\tstd::optional<QRect> _lastSmallGeometry;\n\tstd::optional<QRect> _lastLargeGeometry;\n\tbool _lastLargeMaximized = false;\n\tbool _showWideControls = false;\n\tbool _trackControls = false;\n\tbool _wideControlsShown = false;\n\tUi::Animations::Simple _wideControlsAnimation;\n\n\tobject_ptr<Ui::RpWidget> _controlsBackgroundWide = { nullptr };\n\tstd::unique_ptr<ControlsBackgroundNarrow> _controlsBackgroundNarrow;\n\tobject_ptr<Ui::CallButton> _settings = { nullptr };\n\tobject_ptr<Ui::CallButton> _wideMenu = { nullptr };\n\tobject_ptr<Ui::CallButton> _callShare = { nullptr };\n\tobject_ptr<Ui::CallButton> _video = { nullptr };\n\tobject_ptr<Ui::CallButton> _screenShare = { nullptr };\n\tobject_ptr<Ui::CallButton> _message = { nullptr };\n\tstd::unique_ptr<Ui::CallMuteButton> _mute;\n\tobject_ptr<Ui::CallButton> _hangup;\n\tobject_ptr<Ui::ImportantTooltip> _niceTooltip = { nullptr };\n\tQPointer<Ui::IconButton> _stickedTooltipClose;\n\tQPointer<Ui::RpWidget> _niceTooltipControl;\n\tStickedTooltips _stickedTooltipsShown;\n\tFn<void()> _callShareLinkCallback;\n\n\tstd::shared_ptr<ChatHelpers::Show> _cachedShow;\n\tstd::unique_ptr<MessageField> _messageField;\n\tstd::unique_ptr<MessagesUi> _messages;\n\n\tconst std::unique_ptr<Toasts> _toasts;\n\n\tstd::unique_ptr<MicLevelTester> _micLevelTester;\n\n\tstyle::complex_color _controlsBackgroundColor;\n\tbase::Timer _hideControlsTimer;\n\trpl::lifetime _hideControlsTimerLifetime;\n\n\trpl::lifetime _peerLifetime;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_rtmp.h\"\n\n#include \"apiwrap.h\"\n#include \"calls/group/calls_group_common.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/vertical_list.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QGuiApplication>\n#include <QStyle>\n\nnamespace Calls::Group {\nnamespace {\n\nconstexpr auto kPasswordCharAmount = 24;\n\nvoid StartWithBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tFn<void()> done,\n\t\tFn<void()> revoke,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\trpl::producer<RtmpInfo> &&data) {\n\tstruct State {\n\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\n\t{\n\t\tauto icon = Settings::CreateLottieIcon(\n\t\t\tbox->verticalLayout(),\n\t\t\t{\n\t\t\t\t.name = u\"rtmp\"_q,\n\t\t\t\t.sizeOverride = st::normalBoxLottieSize,\n\t\t\t},\n\t\t\t{});\n\t\tbox->verticalLayout()->add(std::move(icon.widget), {}, style::al_top);\n\t\tbox->setShowFinishedCallback([animate = icon.animate] {\n\t\t\tanimate(anim::repeat::loop);\n\t\t});\n\t\tUi::AddSkip(box->verticalLayout());\n\t}\n\n\tStartRtmpProcess::FillRtmpRows(\n\t\tbox->verticalLayout(),\n\t\ttrue,\n\t\tstd::move(show),\n\t\tstd::move(data),\n\t\t&st::boxLabel,\n\t\t&st::groupCallRtmpShowButton,\n\t\t&st::defaultSubsectionTitle,\n\t\t&st::attentionBoxButton,\n\t\t&st::defaultPopupMenu);\n\n\tbox->setTitle(tr::lng_group_call_rtmp_title());\n\n\tUi::AddDividerText(\n\t\tbox->verticalLayout(),\n\t\ttr::lng_group_call_rtmp_info());\n\n\tbox->addButton(tr::lng_group_call_rtmp_start(), done);\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\tbox->setWidth(st::boxWideWidth);\n\t{\n\t\tconst auto top = box->addTopButton(st::infoTopBarMenu);\n\t\ttop->setClickedCallback([=] {\n\t\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\ttop,\n\t\t\t\tst::popupMenuWithIcons);\n\t\t\tstate->menu->addAction(\n\t\t\t\ttr::lng_group_invite_context_revoke(tr::now),\n\t\t\t\trevoke,\n\t\t\t\t&st::menuIconRemove);\n\t\t\tstate->menu->setForcedOrigin(\n\t\t\t\tUi::PanelAnimation::Origin::TopRight);\n\t\t\ttop->setForceRippled(true);\n\t\t\tconst auto raw = state->menu.get();\n\t\t\traw->setDestroyedCallback([=] {\n\t\t\t\tif ((state->menu == raw) && top) {\n\t\t\t\t\ttop->setForceRippled(false);\n\t\t\t\t}\n\t\t\t});\n\t\t\tstate->menu->popup(top->mapToGlobal(top->rect().center()));\n\t\t\treturn true;\n\t\t});\n\t}\n\n}\n\n} // namespace\n\nStartRtmpProcess::~StartRtmpProcess() {\n\tclose();\n}\n\nvoid StartRtmpProcess::start(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tFn<void(JoinInfo)> done) {\n\tExpects(done != nullptr);\n\n\tconst auto session = &peer->session();\n\tif (_request) {\n\t\tif (_request->peer == peer) {\n\t\t\t_request->show = std::move(show);\n\t\t\t_request->done = std::move(done);\n\t\t\treturn;\n\t\t}\n\t\tsession->api().request(_request->id).cancel();\n\t\t_request = nullptr;\n\t}\n\t_request = std::make_unique<RtmpRequest>(\n\t\tRtmpRequest{\n\t\t\t.peer = peer,\n\t\t\t.show = std::move(show),\n\t\t\t.done = std::move(done),\n\t\t});\n\tsession->account().sessionChanges(\n\t) | rpl::on_next([=] {\n\t\t_request = nullptr;\n\t}, _request->lifetime);\n\n\trequestUrl(false);\n}\n\nvoid StartRtmpProcess::close() {\n\tif (_request) {\n\t\t_request->peer->session().api().request(_request->id).cancel();\n\t\tif (const auto strong = _request->box.get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t\t_request = nullptr;\n\t}\n}\n\nvoid StartRtmpProcess::requestUrl(bool revoke) {\n\tconst auto session = &_request->peer->session();\n\t_request->id = session->api().request(MTPphone_GetGroupCallStreamRtmpUrl(\n\t\tMTP_flags(0),\n\t\t_request->peer->input(),\n\t\tMTP_bool(revoke)\n\t)).done([=](const MTPphone_GroupCallStreamRtmpUrl &result) {\n\t\tauto data = result.match([&](\n\t\t\t\tconst MTPDphone_groupCallStreamRtmpUrl &data) {\n\t\t\treturn RtmpInfo{ .url = qs(data.vurl()), .key = qs(data.vkey()) };\n\t\t});\n\t\tprocessUrl(std::move(data));\n\t}).fail([=] {\n\t\t_request->show->showToast(Lang::Hard::ServerError());\n\t}).send();\n}\n\nvoid StartRtmpProcess::processUrl(RtmpInfo data) {\n\tif (!_request->box) {\n\t\tcreateBox();\n\t}\n\t_request->data = std::move(data);\n}\n\nvoid StartRtmpProcess::finish(JoinInfo info) {\n\tinfo.rtmpInfo = _request->data.current();\n\t_request->done(std::move(info));\n}\n\nvoid StartRtmpProcess::createBox() {\n\tauto done = [=] {\n\t\tconst auto peer = _request->peer;\n\t\tconst auto joinAs = (peer->isChat() && peer->asChat()->amCreator())\n\t\t\t? peer\n\t\t\t: (peer->isChannel() && peer->asChannel()->amCreator())\n\t\t\t? peer\n\t\t\t: peer->session().user();\n\t\tfinish({ .peer = peer, .joinAs = joinAs, .rtmp = true });\n\t};\n\tauto revoke = [=] {\n\t\tconst auto guard = base::make_weak(&_request->guard);\n\t\t_request->show->showBox(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_group_call_rtmp_revoke_sure(),\n\t\t\t.confirmed = crl::guard(guard, [=](Fn<void()> &&close) {\n\t\t\t\trequestUrl(true);\n\t\t\t\tclose();\n\t\t\t}),\n\t\t\t.confirmText = tr::lng_group_invite_context_revoke(),\n\t\t}));\n\t};\n\tauto object = Box(\n\t\tStartWithBox,\n\t\tstd::move(done),\n\t\tstd::move(revoke),\n\t\t_request->show,\n\t\t_request->data.value());\n\tobject->boxClosing(\n\t) | rpl::on_next([=] {\n\t\t_request = nullptr;\n\t}, _request->lifetime);\n\t_request->box = base::make_weak(object.data());\n\t_request->show->showBox(std::move(object));\n}\n\nvoid StartRtmpProcess::FillRtmpRows(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tbool divider,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\trpl::producer<RtmpInfo> &&data,\n\t\tconst style::FlatLabel *labelStyle,\n\t\tconst style::IconButton *showButtonStyle,\n\t\tconst style::FlatLabel *subsectionTitleStyle,\n\t\tconst style::RoundButton *attentionButtonStyle,\n\t\tconst style::PopupMenu *popupMenuStyle) {\n\tstruct State {\n\t\trpl::variable<QString> key;\n\t\trpl::variable<QString> url;\n\t\tbool warned = false;\n\t};\n\n\tconst auto &rowPadding = st::boxRowPadding;\n\n\tconst auto state = container->lifetime().make_state<State>();\n\tstate->key = rpl::duplicate(\n\t\tdata\n\t) | rpl::map([=](const auto &d) { return d.key; });\n\tstate->url = std::move(\n\t\tdata\n\t) | rpl::map([=](const auto &d) { return d.url; });\n\n\tconst auto showToast = [=](const QString &text) {\n\t\tshow->showToast(text);\n\t};\n\tconst auto addButton = [&](\n\t\t\tbool key,\n\t\t\trpl::producer<QString> &&text) {\n\t\tauto wrap = object_ptr<Ui::RpWidget>(container);\n\t\tauto button = Ui::CreateChild<Ui::RoundButton>(\n\t\t\twrap.data(),\n\t\t\trpl::duplicate(text),\n\t\t\tst::groupCallRtmpCopyButton);\n\t\tbutton->setClickedCallback(key\n\t\t\t? Fn<void()>([=] {\n\t\t\t\tQGuiApplication::clipboard()->setText(state->key.current());\n\t\t\t\tshowToast(tr::lng_group_call_rtmp_key_copied(tr::now));\n\t\t\t})\n\t\t\t: Fn<void()>([=] {\n\t\t\t\tQGuiApplication::clipboard()->setText(state->url.current());\n\t\t\t\tshowToast(tr::lng_group_call_rtmp_url_copied(tr::now));\n\t\t\t}));\n\t\tUi::AddSkip(container, st::groupCallRtmpCopyButtonTopSkip);\n\t\tconst auto weak = container->add(std::move(wrap), rowPadding);\n\t\tUi::AddSkip(container, st::groupCallRtmpCopyButtonBottomSkip);\n\t\tbutton->heightValue(\n\t\t) | rpl::on_next([=](int height) {\n\t\t\tweak->resize(weak->width(), height);\n\t\t}, container->lifetime());\n\t\treturn weak;\n\t};\n\n\tconst auto addLabel = [&](v::text::data &&text) {\n\t\tconst auto label = container->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\tv::text::take_marked(std::move(text)),\n\t\t\t\t*labelStyle,\n\t\t\t\t*popupMenuStyle),\n\t\t\tst::boxRowPadding + QMargins(0, 0, showButtonStyle->width, 0));\n\t\tlabel->setSelectable(true);\n\t\tlabel->setBreakEverywhere(true);\n\t\treturn label;\n\t};\n\n\t// Server URL.\n\tUi::AddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_group_call_rtmp_url_subtitle(),\n\t\tst::groupCallRtmpSubsectionTitleAddPadding,\n\t\tsubsectionTitleStyle);\n\n\tauto urlLabelContent = state->url.value();\n\taddLabel(std::move(urlLabelContent));\n\tUi::AddSkip(container, st::groupCallRtmpUrlSkip);\n\taddButton(false, tr::lng_group_call_rtmp_url_copy());\n\t//\n\n\tif (divider) {\n\t\tUi::AddDivider(container);\n\t}\n\n\t// Stream Key.\n\tUi::AddSkip(container, st::groupCallRtmpKeySubsectionTitleSkip);\n\n\tUi::AddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_group_call_rtmp_key_subtitle(),\n\t\tst::groupCallRtmpSubsectionTitleAddPadding,\n\t\tsubsectionTitleStyle);\n\n\tauto keyLabelContent = state->key.value(\n\t) | rpl::map([](const QString &key) {\n\t\tconst auto size = int(key.size());\n\t\tauto result = TextWithEntities{ key };\n\t\tif (size > 0) {\n\t\t\tresult.entities.push_back({ EntityType::Spoiler, 0, size });\n\t\t}\n\t\treturn result;\n\t}) | rpl::after_next([=] {\n\t\tcontainer->resizeToWidth(container->widthNoMargins());\n\t});\n\tconst auto streamKeyLabel = addLabel(std::move(keyLabelContent));\n\tstreamKeyLabel->setClickHandlerFilter([=](\n\t\t\tconst ClickHandlerPtr &handler,\n\t\t\tQt::MouseButton button) {\n\t\tif (button == Qt::LeftButton) {\n\t\t\tshow->showBox(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_group_call_rtmp_key_warning(\n\t\t\t\t\ttr::rich),\n\t\t\t\t.confirmed = [=](Fn<void()> &&close) {\n\t\t\t\t\thandler->onClick({});\n\t\t\t\t\tclose();\n\t\t\t\t},\n\t\t\t\t.confirmText = tr::lng_from_request_understand(),\n\t\t\t\t.cancelText = tr::lng_cancel(),\n\t\t\t\t.confirmStyle = attentionButtonStyle,\n\t\t\t\t.labelStyle = labelStyle,\n\t\t\t}));\n\t\t}\n\t\treturn false;\n\t});\n\n\taddButton(true, tr::lng_group_call_rtmp_key_copy());\n\t//\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_rtmp.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"base/object_ptr.h\"\n#include \"calls/group/calls_group_common.h\"\n\nclass PeerData;\n\nnamespace Ui {\nclass Show;\nclass BoxContent;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace style {\nstruct FlatLabel;\nstruct RoundButton;\nstruct IconButton;\nstruct PopupMenu;\n} // namespace style\n\nnamespace Calls::Group {\n\nstruct JoinInfo;\n\nclass StartRtmpProcess final {\npublic:\n\tStartRtmpProcess() = default;\n\t~StartRtmpProcess();\n\n\tvoid start(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tFn<void(JoinInfo)> done);\n\tvoid close();\n\n\tstatic void FillRtmpRows(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tbool divider,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\trpl::producer<RtmpInfo> &&data,\n\t\tconst style::FlatLabel *labelStyle,\n\t\tconst style::IconButton *showButtonStyle,\n\t\tconst style::FlatLabel *subsectionTitleStyle,\n\t\tconst style::RoundButton *attentionButtonStyle,\n\t\tconst style::PopupMenu *popupMenuStyle);\n\nprivate:\n\tvoid requestUrl(bool revoke);\n\tvoid processUrl(RtmpInfo data);\n\tvoid createBox();\n\tvoid finish(JoinInfo info);\n\n\tstruct RtmpRequest {\n\t\tnot_null<PeerData*> peer;\n\t\trpl::variable<RtmpInfo> data;\n\t\tstd::shared_ptr<Ui::Show> show;\n\t\tFn<void(JoinInfo)> done;\n\t\tbase::has_weak_ptr guard;\n\t\tbase::weak_qptr<Ui::BoxContent> box;\n\t\trpl::lifetime lifetime;\n\t\tmtpRequestId id = 0;\n\t};\n\tstd::unique_ptr<RtmpRequest> _request;\n\n};\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_settings.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_settings.h\"\n\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_menu.h\" // LeaveBox.\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/group/calls_choose_join_as.h\"\n#include \"calls/group/calls_volume_item.h\"\n#include \"calls/calls_instance.h\"\n#include \"ui/widgets/level_meter.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"lang/lang_keys.h\"\n#include \"boxes/share_box.h\"\n#include \"history/view/history_view_schedule_box.h\"\n#include \"history/history_item_helpers.h\" // GetErrorForSending.\n#include \"history/history.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_session.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/event_filter.h\"\n#include \"base/global_shortcuts.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/unixtime.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_user.h\"\n#include \"calls/group/calls_group_rtmp.h\"\n#include \"ui/toast/toast.h\"\n#include \"data/data_changes.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"webrtc/webrtc_audio_input_tester.h\"\n#include \"webrtc/webrtc_device_resolver.h\"\n#include \"settings/sections/settings_calls.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"api/api_invite_links.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_settings.h\"\n\n#include <QtGui/QGuiApplication>\n\nnamespace Calls::Group {\nnamespace {\n\nconstexpr auto kDelaysCount = 201;\nconstexpr auto kMicrophoneTooltipAfterLoudCount = 3;\nconstexpr auto kDropLoudAfterQuietCount = 5;\nconstexpr auto kMicrophoneTooltipLevelThreshold = 0.2;\nconstexpr auto kMicrophoneTooltipCheckInterval = crl::time(500);\n\n#ifdef Q_OS_MAC\nconstexpr auto kCheckAccessibilityInterval = crl::time(500);\n#endif // Q_OS_MAC\n\nvoid SaveCallJoinMuted(\n\t\tnot_null<PeerData*> peer,\n\t\tCallId callId,\n\t\tbool joinMuted) {\n\tconst auto call = peer->groupCall();\n\tif (!call\n\t\t|| call->id() != callId\n\t\t|| peer->isUser()\n\t\t|| !peer->canManageGroupCall()\n\t\t|| !call->canChangeJoinMuted()\n\t\t|| call->joinMuted() == joinMuted) {\n\t\treturn;\n\t}\n\tusing Flag = MTPphone_ToggleGroupCallSettings::Flag;\n\tcall->setJoinMutedLocally(joinMuted);\n\tpeer->session().api().request(MTPphone_ToggleGroupCallSettings(\n\t\tMTP_flags(Flag::f_join_muted),\n\t\tcall->input(),\n\t\tMTP_bool(joinMuted),\n\t\tMTPBool(), // messages_enabled\n\t\tMTPlong() // send_paid_messages_stars\n\t)).send();\n}\n\nvoid SaveCallMessagesEnabled(\n\t\tnot_null<PeerData*> peer,\n\t\tCallId callId,\n\t\tbool messagesEnabled) {\n\tconst auto call = peer->groupCall();\n\tif (!call\n\t\t|| call->id() != callId\n\t\t|| !peer->canManageGroupCall()\n\t\t|| !call->canChangeMessagesEnabled()\n\t\t|| call->messagesEnabled() == messagesEnabled) {\n\t\treturn;\n\t}\n\tusing Flag = MTPphone_ToggleGroupCallSettings::Flag;\n\tcall->setMessagesEnabledLocally(messagesEnabled);\n\tpeer->session().api().request(MTPphone_ToggleGroupCallSettings(\n\t\tMTP_flags(Flag::f_messages_enabled),\n\t\tcall->input(),\n\t\tMTPBool(), // join_muted\n\t\tMTP_bool(messagesEnabled),\n\t\tMTPlong() // send_paid_messages_stars\n\t)).send();\n}\n\n[[nodiscard]] crl::time DelayByIndex(int index) {\n\treturn index * crl::time(10);\n}\n\n[[nodiscard]] QString FormatDelay(crl::time delay) {\n\treturn (delay < crl::time(1000))\n\t\t? tr::lng_group_call_ptt_delay_ms(\n\t\t\ttr::now,\n\t\t\tlt_amount,\n\t\t\tQString::number(delay))\n\t\t: tr::lng_group_call_ptt_delay_s(\n\t\t\ttr::now,\n\t\t\tlt_amount,\n\t\t\tQString::number(delay / 1000., 'f', 2));\n}\n\nobject_ptr<ShareBox> ShareInviteLinkBox(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &linkSpeaker,\n\t\tconst QString &linkListener,\n\t\tstd::shared_ptr<Ui::Show> show) {\n\tconst auto sending = std::make_shared<bool>();\n\tconst auto box = std::make_shared<base::weak_qptr<ShareBox>>();\n\n\tauto bottom = linkSpeaker.isEmpty()\n\t\t? nullptr\n\t\t: object_ptr<Ui::PaddingWrap<Ui::Checkbox>>(\n\t\t\tnullptr,\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tnullptr,\n\t\t\t\ttr::lng_group_call_share_speaker(tr::now),\n\t\t\t\ttrue,\n\t\t\t\tst::groupCallCheckbox),\n\t\t\tst::groupCallShareMutedMargin);\n\tconst auto speakerCheckbox = bottom ? bottom->entity() : nullptr;\n\tconst auto currentLink = [=] {\n\t\treturn (!speakerCheckbox || !speakerCheckbox->checked())\n\t\t\t? linkListener\n\t\t\t: linkSpeaker;\n\t};\n\tauto copyCallback = [=] {\n\t\tQGuiApplication::clipboard()->setText(currentLink());\n\t\tshow->showToast(tr::lng_group_invite_copied(tr::now));\n\t};\n\tauto countMessagesCallback = [=](const TextWithTags &comment) {\n\t\treturn 1;\n\t};\n\tauto submitCallback = [=](\n\t\t\tstd::vector<not_null<Data::Thread*>> &&result,\n\t\t\tFn<bool()> checkPaid,\n\t\t\tTextWithTags &&comment,\n\t\t\tApi::SendOptions options,\n\t\t\tData::ForwardOptions) {\n\t\tif (*sending || result.empty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto error = GetErrorForSending(\n\t\t\tresult,\n\t\t\t{ .text = &comment });\n\t\tif (error.error) {\n\t\t\tif (const auto weak = *box) {\n\t\t\t\tweak->getDelegate()->show(\n\t\t\t\t\tMakeSendErrorBox(error, result.size() > 1));\n\t\t\t}\n\t\t\treturn;\n\t\t} else if (!checkPaid()) {\n\t\t\treturn;\n\t\t}\n\n\t\t*sending = true;\n\t\tconst auto link = currentLink();\n\t\tif (!comment.text.isEmpty()) {\n\t\t\tcomment.text = link + \"\\n\" + comment.text;\n\t\t\tconst auto add = link.size() + 1;\n\t\t\tfor (auto &tag : comment.tags) {\n\t\t\t\ttag.offset += add;\n\t\t\t}\n\t\t} else {\n\t\t\tcomment.text = link;\n\t\t}\n\t\tauto &api = peer->session().api();\n\t\tfor (const auto &thread : result) {\n\t\t\tauto message = Api::MessageToSend(\n\t\t\t\tApi::SendAction(thread, options));\n\t\t\tmessage.textWithTags = comment;\n\t\t\tmessage.action.clearDraft = false;\n\t\t\tapi.sendMessage(std::move(message));\n\t\t}\n\t\tif (*box) {\n\t\t\t(*box)->closeBox();\n\t\t}\n\t\tshow->showToast(tr::lng_share_done(tr::now));\n\t};\n\tauto filterCallback = [](not_null<Data::Thread*> thread) {\n\t\tif (const auto user = thread->peer()->asUser()) {\n\t\t\tif (user->canSendIgnoreMoneyRestrictions()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn Data::CanSend(thread, ChatRestriction::SendOther);\n\t};\n\n\tconst auto st = ::Settings::DarkCreditsEntryBoxStyle();\n\tauto result = Box<ShareBox>(ShareBox::Descriptor{\n\t\t.session = &peer->session(),\n\t\t.copyCallback = std::move(copyCallback),\n\t\t.countMessagesCallback = std::move(countMessagesCallback),\n\t\t.submitCallback = std::move(submitCallback),\n\t\t.filterCallback = std::move(filterCallback),\n\t\t.bottomWidget = std::move(bottom),\n\t\t.copyLinkText = rpl::conditional(\n\t\t\t(speakerCheckbox\n\t\t\t\t? speakerCheckbox->checkedValue()\n\t\t\t\t: rpl::single(false)),\n\t\t\ttr::lng_group_call_copy_speaker_link(),\n\t\t\ttr::lng_group_call_copy_listener_link()),\n\t\t.st = st.shareBox ? *st.shareBox : ShareBoxStyleOverrides(),\n\t\t.moneyRestrictionError = ShareMessageMoneyRestrictionError(),\n\t});\n\t*box = result.data();\n\treturn result;\n}\n\n} // namespace\n\nvoid SettingsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<GroupCall*> call) {\n\tusing namespace Settings;\n\n\tconst auto weakCall = base::make_weak(call);\n\tconst auto weakBox = base::make_weak(box);\n\n\tstruct State {\n\t\tstd::unique_ptr<Webrtc::DeviceResolver> deviceId;\n\t\tstd::unique_ptr<Webrtc::AudioInputTester> micTester;\n\t\tUi::LevelMeter *micTestLevel = nullptr;\n\t\tfloat micLevel = 0.;\n\t\tUi::Animations::Simple micLevelAnimation;\n\t\tbase::Timer levelUpdateTimer;\n\t\tbool generatingLink = false;\n\t};\n\tconst auto peer = call->peer();\n\tconst auto state = box->lifetime().make_state<State>();\n\tconst auto real = call->conference()\n\t\t? call->lookupReal()\n\t\t: peer->groupCall();\n\tconst auto rtmp = call->rtmp();\n\tconst auto id = call->id();\n\tconst auto goodReal = (real && real->id() == id);\n\n\tconst auto layout = box->verticalLayout();\n\tconst auto &settings = Core::App().settings();\n\n\tconst auto joinMuted = !call->conference()\n\t\t&& goodReal\n\t\t&& real->joinMuted();\n\tconst auto messagesEnabled = goodReal && real->messagesEnabled();\n\tconst auto canChangeJoinMuted = !rtmp\n\t\t&& goodReal\n\t\t&& real->canChangeJoinMuted();\n\tconst auto canChangeMessagesEnabled = !rtmp\n\t\t&& goodReal\n\t\t&& real->canChangeMessagesEnabled();\n\tconst auto addCheck = canChangeJoinMuted && peer->canManageGroupCall();\n\tconst auto addMessages = canChangeMessagesEnabled\n\t\t&& (call->conference() || peer->canManageGroupCall());\n\n\tconst auto addDivider = [&] {\n\t\tlayout->add(object_ptr<Ui::BoxContentDivider>(\n\t\t\tlayout,\n\t\t\tst::boxDividerHeight,\n\t\t\tst::groupCallDividerBar));\n\t};\n\n\tif (addCheck || addMessages) {\n\t\tUi::AddSkip(layout);\n\t}\n\tconst auto muteJoined = addCheck\n\t\t? layout->add(object_ptr<Ui::SettingsButton>(\n\t\t\tlayout,\n\t\t\ttr::lng_group_call_new_muted(),\n\t\t\tst::groupCallSettingsButton))->toggleOn(rpl::single(joinMuted))\n\t\t: nullptr;\n\tconst auto enableMessages = addMessages\n\t\t? layout->add(object_ptr<Ui::SettingsButton>(\n\t\t\tlayout,\n\t\t\ttr::lng_group_call_enable_messages(),\n\t\t\tst::groupCallSettingsButton))->toggleOn(\n\t\t\t\trpl::single(messagesEnabled))\n\t\t: nullptr;\n\tif (addCheck || addMessages) {\n\t\tUi::AddSkip(layout);\n\t}\n\n\tauto playbackIdWithFallback = Webrtc::DeviceIdValueWithFallback(\n\t\tCore::App().settings().callPlaybackDeviceIdValue(),\n\t\tCore::App().settings().playbackDeviceIdValue());\n\tAddButtonWithLabel(\n\t\tlayout,\n\t\ttr::lng_group_call_speakers(),\n\t\tPlaybackDeviceNameValue(rpl::duplicate(playbackIdWithFallback)),\n\t\tst::groupCallSettingsButton\n\t)->addClickHandler([=] {\n\t\tbox->getDelegate()->show(ChoosePlaybackDeviceBox(\n\t\t\trpl::duplicate(playbackIdWithFallback),\n\t\t\tcrl::guard(box, [=](const QString &id) {\n\t\t\t\tCore::App().settings().setCallPlaybackDeviceId(id);\n\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t}),\n\t\t\t&st::groupCallCheckbox,\n\t\t\t&st::groupCallRadio));\n\t});\n\n\tif (!rtmp) {\n\t\tauto captureIdWithFallback = Webrtc::DeviceIdValueWithFallback(\n\t\t\tCore::App().settings().callCaptureDeviceIdValue(),\n\t\t\tCore::App().settings().captureDeviceIdValue());\n\t\tAddButtonWithLabel(\n\t\t\tlayout,\n\t\t\ttr::lng_group_call_microphone(),\n\t\t\tCaptureDeviceNameValue(rpl::duplicate(captureIdWithFallback)),\n\t\t\tst::groupCallSettingsButton\n\t\t)->addClickHandler([=] {\n\t\t\tbox->getDelegate()->show(ChooseCaptureDeviceBox(\n\t\t\t\trpl::duplicate(captureIdWithFallback),\n\t\t\t\tcrl::guard(box, [=](const QString &id) {\n\t\t\t\t\tCore::App().settings().setCallCaptureDeviceId(id);\n\t\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t\t}),\n\t\t\t\t&st::groupCallCheckbox,\n\t\t\t\t&st::groupCallRadio));\n\t\t});\n\n\t\tstate->micTestLevel = box->addRow(\n\t\t\tobject_ptr<Ui::LevelMeter>(\n\t\t\t\tbox.get(),\n\t\t\t\tst::groupCallLevelMeter),\n\t\t\tst::settingsLevelMeterPadding);\n\t\tstate->micTestLevel->resize(QSize(0, st::defaultLevelMeter.height));\n\n\t\tstate->levelUpdateTimer.setCallback([=] {\n\t\t\tconst auto was = state->micLevel;\n\t\t\tstate->micLevel = state->micTester->getAndResetLevel();\n\t\t\tstate->micLevelAnimation.start([=] {\n\t\t\t\tstate->micTestLevel->setValue(\n\t\t\t\t\tstate->micLevelAnimation.value(state->micLevel));\n\t\t\t}, was, state->micLevel, kMicTestAnimationDuration);\n\t\t});\n\n\t\tUi::AddSkip(layout);\n\t\t//Ui::AddDivider(layout);\n\t\t//Ui::AddSkip(layout);\n\n\t\tlayout->add(object_ptr<Ui::SettingsButton>(\n\t\t\tlayout,\n\t\t\ttr::lng_group_call_noise_suppression(),\n\t\t\tst::groupCallSettingsButton\n\t\t))->toggleOn(rpl::single(\n\t\t\tsettings.groupCallNoiseSuppression()\n\t\t))->toggledChanges(\n\t\t) | rpl::on_next([=](bool enabled) {\n\t\t\tCore::App().settings().setGroupCallNoiseSuppression(enabled);\n\t\t\tcall->setNoiseSuppression(enabled);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}, layout->lifetime());\n\n\t\tusing GlobalShortcut = base::GlobalShortcut;\n\t\tstruct PushToTalkState {\n\t\t\trpl::variable<QString> recordText = tr::lng_group_call_ptt_shortcut();\n\t\t\trpl::variable<QString> shortcutText;\n\t\t\trpl::event_stream<bool> pushToTalkToggles;\n\t\t\tstd::shared_ptr<base::GlobalShortcutManager> manager;\n\t\t\tGlobalShortcut shortcut;\n\t\t\tcrl::time delay = 0;\n\t\t\tbool recording = false;\n\t\t};\n\t\tif (base::GlobalShortcutsAvailable()) {\n\t\t\tconst auto state = box->lifetime().make_state<PushToTalkState>();\n\t\t\tif (!base::GlobalShortcutsAllowed()) {\n\t\t\t\tCore::App().settings().setGroupCallPushToTalk(false);\n\t\t\t}\n\t\t\tconst auto tryFillFromManager = [=] {\n\t\t\t\tstate->shortcut = state->manager\n\t\t\t\t\t? state->manager->shortcutFromSerialized(\n\t\t\t\t\t\tCore::App().settings().groupCallPushToTalkShortcut())\n\t\t\t\t\t: nullptr;\n\t\t\t\tstate->shortcutText = state->shortcut\n\t\t\t\t\t? state->shortcut->toDisplayString()\n\t\t\t\t\t: QString();\n\t\t\t};\n\t\t\tstate->manager = settings.groupCallPushToTalk()\n\t\t\t\t? call->ensureGlobalShortcutManager()\n\t\t\t\t: nullptr;\n\t\t\ttryFillFromManager();\n\n\t\t\tstate->delay = settings.groupCallPushToTalkDelay();\n\t\t\tconst auto pushToTalk = layout->add(\n\t\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\t\tlayout,\n\t\t\t\t\ttr::lng_group_call_push_to_talk(),\n\t\t\t\t\tst::groupCallSettingsButton\n\t\t\t))->toggleOn(rpl::single(\n\t\t\t\tsettings.groupCallPushToTalk()\n\t\t\t) | rpl::then(state->pushToTalkToggles.events()));\n\t\t\tconst auto pushToTalkWrap = layout->add(\n\t\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\t\tlayout,\n\t\t\t\t\tobject_ptr<Ui::VerticalLayout>(layout)));\n\t\t\tconst auto pushToTalkInner = pushToTalkWrap->entity();\n\t\t\tconst auto recording = pushToTalkInner->add(\n\t\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\t\tpushToTalkInner,\n\t\t\t\t\tstate->recordText.value(),\n\t\t\t\t\tst::groupCallSettingsButton));\n\t\t\tCreateRightLabel(\n\t\t\t\trecording,\n\t\t\t\tstate->shortcutText.value(),\n\t\t\t\tst::groupCallSettingsButton,\n\t\t\t\tstate->recordText.value());\n\n\t\t\tconst auto applyAndSave = [=] {\n\t\t\t\tcall->applyGlobalShortcutChanges();\n\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t};\n\t\t\tconst auto showPrivacyRequest = [=] {\n#ifdef Q_OS_MAC\n\t\t\t\tif (!Platform::IsMac10_14OrGreater()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto requestInputMonitoring = Platform::IsMac10_15OrGreater();\n\t\t\t\tbox->getDelegate()->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\t\tbox->addRow(\n\t\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\t\tbox.get(),\n\t\t\t\t\t\t\trpl::combine(\n\t\t\t\t\t\t\t\ttr::lng_group_call_mac_access(),\n\t\t\t\t\t\t\t\t(requestInputMonitoring\n\t\t\t\t\t\t\t\t\t? tr::lng_group_call_mac_input()\n\t\t\t\t\t\t\t\t\t: tr::lng_group_call_mac_accessibility())\n\t\t\t\t\t\t\t) | rpl::map([](QString a, QString b) {\n\t\t\t\t\t\t\t\tauto result = tr::rich(a);\n\t\t\t\t\t\t\t\tresult.append(\"\\n\\n\").append(tr::rich(b));\n\t\t\t\t\t\t\t\treturn result;\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tst::groupCallBoxLabel),\n\t\t\t\t\t\tstyle::margins(\n\t\t\t\t\t\t\tst::boxRowPadding.left(),\n\t\t\t\t\t\t\tst::boxPadding.top(),\n\t\t\t\t\t\t\tst::boxRowPadding.right(),\n\t\t\t\t\t\t\tst::boxPadding.bottom()));\n\t\t\t\t\tbox->addButton(tr::lng_group_call_mac_settings(), [=] {\n\t\t\t\t\t\tif (requestInputMonitoring) {\n\t\t\t\t\t\t\tPlatform::OpenInputMonitoringPrivacySettings();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tPlatform::OpenAccessibilityPrivacySettings();\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\n\t\t\t\t\tif (!requestInputMonitoring) {\n\t\t\t\t\t\t// Accessibility is enabled without app restart, so short-poll it.\n\t\t\t\t\t\tbase::timer_each(\n\t\t\t\t\t\t\tkCheckAccessibilityInterval\n\t\t\t\t\t\t) | rpl::filter([] {\n\t\t\t\t\t\t\treturn base::GlobalShortcutsAllowed();\n\t\t\t\t\t\t}) | rpl::on_next([=] {\n\t\t\t\t\t\t\tbox->closeBox();\n\t\t\t\t\t\t}, box->lifetime());\n\t\t\t\t\t}\n\t\t\t\t}));\n#endif // Q_OS_MAC\n\t\t\t};\n\t\t\tconst auto ensureManager = [=] {\n\t\t\t\tif (state->manager) {\n\t\t\t\t\treturn true;\n\t\t\t\t} else if (base::GlobalShortcutsAllowed()) {\n\t\t\t\t\tstate->manager = call->ensureGlobalShortcutManager();\n\t\t\t\t\ttryFillFromManager();\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tshowPrivacyRequest();\n\t\t\t\treturn false;\n\t\t\t};\n\t\t\tconst auto stopRecording = [=] {\n\t\t\t\tstate->recording = false;\n\t\t\t\tstate->recordText = tr::lng_group_call_ptt_shortcut();\n\t\t\t\tstate->shortcutText = state->shortcut\n\t\t\t\t\t? state->shortcut->toDisplayString()\n\t\t\t\t\t: QString();\n\t\t\t\trecording->setColorOverride(std::nullopt);\n\t\t\t\tif (state->manager) {\n\t\t\t\t\tstate->manager->stopRecording();\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto startRecording = [=] {\n\t\t\t\tif (!ensureManager()) {\n\t\t\t\t\tstate->pushToTalkToggles.fire(false);\n\t\t\t\t\tpushToTalkWrap->hide(anim::type::instant);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tstate->recording = true;\n\t\t\t\tstate->recordText = tr::lng_group_call_ptt_recording();\n\t\t\t\trecording->setColorOverride(\n\t\t\t\t\tst::groupCallSettingsAttentionButton.textFg->c);\n\t\t\t\tauto progress = crl::guard(box, [=](GlobalShortcut shortcut) {\n\t\t\t\t\tstate->shortcutText = shortcut->toDisplayString();\n\t\t\t\t});\n\t\t\t\tauto done = crl::guard(box, [=](GlobalShortcut shortcut) {\n\t\t\t\t\tstate->shortcut = shortcut;\n\t\t\t\t\tCore::App().settings().setGroupCallPushToTalkShortcut(shortcut\n\t\t\t\t\t\t? shortcut->serialize()\n\t\t\t\t\t\t: QByteArray());\n\t\t\t\t\tapplyAndSave();\n\t\t\t\t\tstopRecording();\n\t\t\t\t});\n\t\t\t\tstate->manager->startRecording(std::move(progress), std::move(done));\n\t\t\t};\n\t\t\trecording->addClickHandler([=] {\n\t\t\t\tif (state->recording) {\n\t\t\t\t\tstopRecording();\n\t\t\t\t} else {\n\t\t\t\t\tstartRecording();\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tconst auto label = pushToTalkInner->add(\n\t\t\t\tobject_ptr<Ui::LabelSimple>(\n\t\t\t\t\tpushToTalkInner,\n\t\t\t\t\tst::groupCallDelayLabel),\n\t\t\t\tst::groupCallDelayLabelMargin);\n\t\t\tconst auto value = std::clamp(\n\t\t\t\tstate->delay,\n\t\t\t\tcrl::time(0),\n\t\t\t\tDelayByIndex(kDelaysCount - 1));\n\t\t\tconst auto callback = [=](crl::time delay) {\n\t\t\t\tstate->delay = delay;\n\t\t\t\tlabel->setText(tr::lng_group_call_ptt_delay(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_delay,\n\t\t\t\t\tFormatDelay(delay)));\n\t\t\t\tif (Core::App().settings().groupCallPushToTalkDelay() != delay) {\n\t\t\t\t\tCore::App().settings().setGroupCallPushToTalkDelay(delay);\n\t\t\t\t\tapplyAndSave();\n\t\t\t\t}\n\t\t\t};\n\t\t\tcallback(value);\n\t\t\tconst auto slider = pushToTalkInner->add(\n\t\t\t\tobject_ptr<Ui::MediaSlider>(\n\t\t\t\t\tpushToTalkInner,\n\t\t\t\t\tst::groupCallDelaySlider),\n\t\t\t\tst::groupCallDelayMargin);\n\t\t\tslider->resize(st::groupCallDelaySlider.seekSize);\n\t\t\tslider->setPseudoDiscrete(\n\t\t\t\tkDelaysCount,\n\t\t\t\tDelayByIndex,\n\t\t\t\tvalue,\n\t\t\t\tcallback);\n\n\t\t\tpushToTalkWrap->toggle(\n\t\t\t\tsettings.groupCallPushToTalk(),\n\t\t\t\tanim::type::instant);\n\t\t\tpushToTalk->toggledChanges(\n\t\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\t\tif (!toggled) {\n\t\t\t\t\tstopRecording();\n\t\t\t\t} else if (!ensureManager()) {\n\t\t\t\t\tstate->pushToTalkToggles.fire(false);\n\t\t\t\t\tpushToTalkWrap->hide(anim::type::instant);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tCore::App().settings().setGroupCallPushToTalk(toggled);\n\t\t\t\tapplyAndSave();\n\t\t\t\tpushToTalkWrap->toggle(toggled, anim::type::normal);\n\t\t\t}, pushToTalk->lifetime());\n\n\t\t\tauto boxKeyFilter = [=](not_null<QEvent*> e) {\n\t\t\t\treturn (e->type() == QEvent::KeyPress && state->recording)\n\t\t\t\t\t? base::EventFilterResult::Cancel\n\t\t\t\t\t: base::EventFilterResult::Continue;\n\t\t\t};\n\t\t\tbox->lifetime().make_state<base::unique_qptr<QObject>>(\n\t\t\t\tbase::install_event_filter(box, std::move(boxKeyFilter)));\n\t\t}\n\n\t\tUi::AddSkip(layout);\n\t\t//Ui::AddDivider(layout);\n\t\t//Ui::AddSkip(layout);\n\t}\n\tauto shareLink = Fn<void()>();\n\tif (peer->isChannel()\n\t\t&& peer->asChannel()->hasUsername()\n\t\t&& goodReal) {\n\t\tconst auto showBox = crl::guard(box, [=](\n\t\t\t\tobject_ptr<Ui::BoxContent> next) {\n\t\t\tbox->getDelegate()->show(std::move(next));\n\t\t});\n\t\tconst auto showToast = crl::guard(box, [=](QString text) {\n\t\t\tbox->showToast(text);\n\t\t});\n\t\tauto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction(\n\t\t\tpeer,\n\t\t\tbox->uiShow());\n\t\tshareLink = std::move(shareLinkCallback);\n\t\tbox->lifetime().add(std::move(shareLinkLifetime));\n\t} else {\n\t\tconst auto lookupLink = [=] {\n\t\t\tif (const auto group = peer->asMegagroup()) {\n\t\t\t\treturn group->hasUsername()\n\t\t\t\t\t? group->session().createInternalLinkFull(\n\t\t\t\t\t\tgroup->username())\n\t\t\t\t\t: group->inviteLink();\n\t\t\t} else if (const auto chat = peer->asChat()) {\n\t\t\t\treturn chat->inviteLink();\n\t\t\t}\n\t\t\treturn QString();\n\t\t};\n\t\tconst auto canCreateLink = [&] {\n\t\t\tif (const auto chat = peer->asChat()) {\n\t\t\t\treturn chat->canHaveInviteLink();\n\t\t\t} else if (const auto group = peer->asMegagroup()) {\n\t\t\t\treturn group->canHaveInviteLink();\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\t\tconst auto alreadyHasLink = !lookupLink().isEmpty();\n\t\tif (alreadyHasLink || canCreateLink()) {\n\t\t\tif (!alreadyHasLink) {\n\t\t\t\t// Request invite link.\n\t\t\t\tpeer->session().api().requestFullPeer(peer);\n\t\t\t}\n\t\t\tconst auto copyLink = [=] {\n\t\t\t\tconst auto link = lookupLink();\n\t\t\t\tif (link.isEmpty()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tQGuiApplication::clipboard()->setText(link);\n\t\t\t\tif (weakBox) {\n\t\t\t\t\tbox->showToast(\n\t\t\t\t\t\ttr::lng_create_channel_link_copied(tr::now));\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t};\n\t\t\tshareLink = [=] {\n\t\t\t\tif (!copyLink() && !state->generatingLink) {\n\t\t\t\t\tstate->generatingLink = true;\n\t\t\t\t\tpeer->session().api().inviteLinks().create({\n\t\t\t\t\t\tpeer,\n\t\t\t\t\t\tcrl::guard(layout, [=](auto&&) { copyLink(); })\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t}\n\tif (shareLink) {\n\t\tlayout->add(object_ptr<Ui::SettingsButton>(\n\t\t\tlayout,\n\t\t\ttr::lng_group_call_share(),\n\t\t\tst::groupCallSettingsButton\n\t\t))->addClickHandler(std::move(shareLink));\n\t}\n\tif (rtmp && !call->rtmpInfo().url.isEmpty()) {\n\t\tUi::AddSkip(layout);\n\t\taddDivider();\n\t\tUi::AddSkip(layout);\n\n\t\tstruct State {\n\t\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t\t\tmtpRequestId requestId;\n\t\t\trpl::event_stream<RtmpInfo> data;\n\t\t};\n\t\tconst auto top = box->addTopButton(st::groupCallMenuToggle);\n\t\tconst auto state = top->lifetime().make_state<State>();\n\t\tconst auto revokeSure = [=] {\n\t\t\tconst auto session = &peer->session();\n\t\t\tstate->requestId = session->api().request(\n\t\t\t\tMTPphone_GetGroupCallStreamRtmpUrl(\n\t\t\t\t\tMTP_flags(0),\n\t\t\t\t\tpeer->input(),\n\t\t\t\t\tMTP_bool(true)\n\t\t\t)).done([=](const MTPphone_GroupCallStreamRtmpUrl &result) {\n\t\t\t\tauto data = result.match([&](\n\t\t\t\t\t\tconst MTPDphone_groupCallStreamRtmpUrl &data) {\n\t\t\t\t\treturn RtmpInfo{\n\t\t\t\t\t\t.url = qs(data.vurl()),\n\t\t\t\t\t\t.key = qs(data.vkey()),\n\t\t\t\t\t};\n\t\t\t\t});\n\t\t\t\tif (const auto call = weakCall.get()) {\n\t\t\t\t\tcall->setRtmpInfo(data);\n\t\t\t\t}\n\t\t\t\tif (!top) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tstate->requestId = 0;\n\t\t\t\tstate->data.fire(std::move(data));\n\t\t\t}).fail([=] {\n\t\t\t\tstate->requestId = 0;\n\t\t\t}).send();\n\t\t};\n\t\tconst auto revoke = [=] {\n\t\t\tif (state->requestId || !top) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tbox->getDelegate()->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_group_call_rtmp_revoke_sure(),\n\t\t\t\t.confirmed = [=](Fn<void()> &&close) {\n\t\t\t\t\trevokeSure();\n\t\t\t\t\tclose();\n\t\t\t\t},\n\t\t\t\t.confirmText = tr::lng_group_invite_context_revoke(),\n\t\t\t\t.labelStyle = &st::groupCallBoxLabel,\n\t\t\t}));\n\t\t};\n\t\ttop->setClickedCallback([=] {\n\t\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\tbox,\n\t\t\t\tst::groupCallPopupMenu);\n\t\t\tstate->menu->addAction(\n\t\t\t\ttr::lng_group_call_rtmp_revoke(tr::now),\n\t\t\t\trevoke);\n\t\t\tstate->menu->setForcedOrigin(\n\t\t\t\tUi::PanelAnimation::Origin::TopRight);\n\t\t\ttop->setForceRippled(true);\n\t\t\tconst auto raw = state->menu.get();\n\t\t\traw->setDestroyedCallback([=] {\n\t\t\t\tif ((state->menu == raw) && top) {\n\t\t\t\t\ttop->setForceRippled(false);\n\t\t\t\t}\n\t\t\t});\n\t\t\tstate->menu->popup(\n\t\t\t\ttop->mapToGlobal(QPoint(top->width() / 2, top->height())));\n\t\t\treturn true;\n\t\t});\n\n\t\tStartRtmpProcess::FillRtmpRows(\n\t\t\tlayout,\n\t\t\tfalse,\n\t\t\tbox->uiShow(),\n\t\t\tstate->data.events(),\n\t\t\t&st::groupCallBoxLabel,\n\t\t\t&st::groupCallSettingsRtmpShowButton,\n\t\t\t&st::groupCallSubsectionTitle,\n\t\t\t&st::groupCallAttentionBoxButton,\n\t\t\t&st::groupCallPopupMenu);\n\t\tstate->data.fire(call->rtmpInfo());\n\n\t\taddDivider();\n\t\tUi::AddSkip(layout);\n\t}\n\tif (rtmp) {\n\t\tconst auto fakeMenu = layout->add(object_ptr<Ui::Menu::Menu>(\n\t\t\tlayout,\n\t\t\tst::groupCallVolumeSettings));\n\t\tauto volumeItem = base::make_unique_q<MenuVolumeItem>(\n\t\t\tfakeMenu,\n\t\t\tst::groupCallVolumeSettings,\n\t\t\tst::groupCallVolumeSettingsSlider,\n\t\t\tcall->otherParticipantStateValue(\n\t\t\t) | rpl::filter([=](const Group::ParticipantState &data) {\n\t\t\t\treturn data.peer == peer;\n\t\t\t}),\n\t\t\tcall->rtmpVolume(),\n\t\t\tGroup::kMaxVolume,\n\t\t\tfalse,\n\t\t\tst::groupCallVolumeSettingsPadding);\n\n\t\tconst auto toggleMute = crl::guard(layout, [=](bool m, bool local) {\n\t\t\tif (call) {\n\t\t\t\tcall->toggleMute({\n\t\t\t\t\t.peer = peer,\n\t\t\t\t\t.mute = m,\n\t\t\t\t\t.locallyOnly = local,\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t\tconst auto changeVolume = crl::guard(layout, [=](int v, bool local) {\n\t\t\tif (call) {\n\t\t\t\tcall->changeVolume({\n\t\t\t\t\t.peer = peer,\n\t\t\t\t\t.volume = std::clamp(v, 1, Group::kMaxVolume),\n\t\t\t\t\t.locallyOnly = local,\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\n\t\tvolumeItem->toggleMuteLocallyRequests(\n\t\t) | rpl::on_next([=](bool muted) {\n\t\t\ttoggleMute(muted, true);\n\t\t}, volumeItem->lifetime());\n\n\t\tvolumeItem->changeVolumeLocallyRequests(\n\t\t) | rpl::on_next([=](int volume) {\n\t\t\tchangeVolume(volume, true);\n\t\t}, volumeItem->lifetime());\n\n\t\tfakeMenu->addAction(std::move(volumeItem));\n\t}\n\n\tif (call->canManage()) {\n\t\tlayout->add(object_ptr<Ui::SettingsButton>(\n\t\t\tlayout,\n\t\t\t(peer->isBroadcast()\n\t\t\t\t? tr::lng_group_call_end_channel()\n\t\t\t\t: tr::lng_group_call_end()),\n\t\t\tst::groupCallSettingsAttentionButton\n\t\t))->addClickHandler([=] {\n\t\t\tif (const auto call = weakCall.get()) {\n\t\t\t\tbox->getDelegate()->show(Box(\n\t\t\t\t\tLeaveBox,\n\t\t\t\t\tcall,\n\t\t\t\t\ttrue,\n\t\t\t\t\tBoxContext::GroupCallPanel));\n\t\t\t\tbox->closeBox();\n\t\t\t}\n\t\t});\n\t}\n\n\tif (!rtmp) {\n\t\tbox->setShowFinishedCallback([=] {\n\t\t\t// Means we finished showing the box.\n\t\t\tcrl::on_main(box, [=] {\n\t\t\t\tstate->deviceId = std::make_unique<Webrtc::DeviceResolver>(\n\t\t\t\t\t&Core::App().mediaDevices(),\n\t\t\t\t\tWebrtc::DeviceType::Capture,\n\t\t\t\t\tWebrtc::DeviceIdValueWithFallback(\n\t\t\t\t\t\tCore::App().settings().callCaptureDeviceIdValue(),\n\t\t\t\t\t\tCore::App().settings().captureDeviceIdValue()));\n\t\t\t\tstate->micTester = std::make_unique<Webrtc::AudioInputTester>(\n\t\t\t\t\tstate->deviceId->value());\n\t\t\t\tstate->levelUpdateTimer.callEach(kMicTestUpdateInterval);\n\t\t\t});\n\t\t});\n\t}\n\n\tbox->setTitle(tr::lng_group_call_settings_title());\n\tbox->boxClosing(\n\t) | rpl::on_next([=] {\n\t\tif (canChangeJoinMuted\n\t\t\t&& muteJoined\n\t\t\t&& muteJoined->toggled() != joinMuted) {\n\t\t\tSaveCallJoinMuted(peer, id, muteJoined->toggled());\n\t\t}\n\t\tif (canChangeMessagesEnabled\n\t\t\t&& enableMessages\n\t\t\t&& enableMessages->toggled() != messagesEnabled) {\n\t\t\tconst auto value = enableMessages->toggled();\n\t\t\tif (!call->conference()) {\n\t\t\t\tSaveCallMessagesEnabled(peer, id, value);\n\t\t\t} else if (const auto real = call->lookupReal()) {\n\t\t\t\treal->setMessagesEnabledLocally(value);\n\t\t\t}\n\t\t}\n\t}, box->lifetime());\n\tbox->addButton(tr::lng_box_done(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nstd::pair<Fn<void()>, rpl::lifetime> ShareInviteLinkAction(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<Ui::Show> show) {\n\tauto lifetime = rpl::lifetime();\n\tstruct State {\n\t\tState(not_null<Main::Session*> session) : session(session) {\n\t\t}\n\t\t~State() {\n\t\t\tsession->api().request(linkListenerRequestId).cancel();\n\t\t\tsession->api().request(linkSpeakerRequestId).cancel();\n\t\t}\n\n\t\tnot_null<Main::Session*> session;\n\t\tstd::optional<QString> linkSpeaker;\n\t\tQString linkListener;\n\t\tmtpRequestId linkListenerRequestId = 0;\n\t\tmtpRequestId linkSpeakerRequestId = 0;\n\t\tbool generatingLink = false;\n\t};\n\tconst auto state = lifetime.make_state<State>(&peer->session());\n\tif (peer->isUser() || !peer->canManageGroupCall()) {\n\t\tstate->linkSpeaker = QString();\n\t}\n\n\tconst auto shareReady = [=] {\n\t\tif (!state->linkSpeaker.has_value()\n\t\t\t|| state->linkListener.isEmpty()) {\n\t\t\treturn false;\n\t\t}\n\t\tshow->showBox(ShareInviteLinkBox(\n\t\t\tpeer,\n\t\t\t*state->linkSpeaker,\n\t\t\tstate->linkListener,\n\t\t\tshow));\n\t\treturn true;\n\t};\n\tauto callback = [=] {\n\t\tconst auto real = peer->migrateToOrMe()->groupCall();\n\t\tif (shareReady() || state->generatingLink || !real) {\n\t\t\treturn;\n\t\t}\n\t\tstate->generatingLink = true;\n\n\t\tstate->linkListenerRequestId = peer->session().api().request(\n\t\t\tMTPphone_ExportGroupCallInvite(\n\t\t\t\tMTP_flags(0),\n\t\t\t\treal->input()\n\t\t\t)\n\t\t).done([=](const MTPphone_ExportedGroupCallInvite &result) {\n\t\t\tstate->linkListenerRequestId = 0;\n\t\t\tresult.match([&](\n\t\t\t\tconst MTPDphone_exportedGroupCallInvite &data) {\n\t\t\t\tstate->linkListener = qs(data.vlink());\n\t\t\t\tshareReady();\n\t\t\t});\n\t\t}).send();\n\n\t\tif (real->rtmp()) {\n\t\t\tstate->linkSpeaker = QString();\n\t\t\tstate->linkSpeakerRequestId = 0;\n\t\t\tshareReady();\n\t\t} else if (!state->linkSpeaker.has_value()) {\n\t\t\tusing Flag = MTPphone_ExportGroupCallInvite::Flag;\n\t\t\tstate->linkSpeakerRequestId = peer->session().api().request(\n\t\t\t\tMTPphone_ExportGroupCallInvite(\n\t\t\t\t\tMTP_flags(Flag::f_can_self_unmute),\n\t\t\t\t\treal->input())\n\t\t\t).done([=](const MTPphone_ExportedGroupCallInvite &result) {\n\t\t\t\tstate->linkSpeakerRequestId = 0;\n\t\t\t\tresult.match([&](\n\t\t\t\t\t\tconst MTPDphone_exportedGroupCallInvite &data) {\n\t\t\t\t\tstate->linkSpeaker = qs(data.vlink());\n\t\t\t\t\tshareReady();\n\t\t\t\t});\n\t\t\t}).fail([=] {\n\t\t\t\tstate->linkSpeakerRequestId = 0;\n\t\t\t\tstate->linkSpeaker = QString();\n\t\t\t\tshareReady();\n\t\t\t}).send();\n\t\t}\n\t};\n\treturn { std::move(callback), std::move(lifetime) };\n}\n\nMicLevelTester::MicLevelTester(Fn<void()> show)\n: _show(std::move(show))\n, _timer([=] { check(); })\n, _deviceId(std::make_unique<Webrtc::DeviceResolver>(\n\t&Core::App().mediaDevices(),\n\tWebrtc::DeviceType::Capture,\n\tWebrtc::DeviceIdValueWithFallback(\n\t\tCore::App().settings().callCaptureDeviceIdValue(),\n\t\tCore::App().settings().captureDeviceIdValue())))\n, _tester(std::make_unique<Webrtc::AudioInputTester>(_deviceId->value())) {\n\t_timer.callEach(kMicrophoneTooltipCheckInterval);\n}\n\nbool MicLevelTester::showTooltip() const {\n\treturn (_loudCount >= kMicrophoneTooltipAfterLoudCount);\n}\n\nvoid MicLevelTester::check() {\n\tconst auto level = _tester->getAndResetLevel();\n\tif (level >= kMicrophoneTooltipLevelThreshold) {\n\t\t_quietCount = 0;\n\t\tif (++_loudCount >= kMicrophoneTooltipAfterLoudCount) {\n\t\t\t_show();\n\t\t}\n\t} else if (_loudCount > 0 && ++_quietCount >= kDropLoudAfterQuietCount) {\n\t\t_quietCount = 0;\n\t\t_loudCount = 0;\n\t}\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_settings.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n\nnamespace Webrtc {\nclass AudioInputTester;\nclass DeviceResolver;\n} // namespace Webrtc\n\nnamespace Calls {\nclass GroupCall;\n} // namespace Calls\n\nnamespace Calls::Group {\n\nvoid SettingsBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<GroupCall*> call);\n\n[[nodiscard]] std::pair<Fn<void()>, rpl::lifetime> ShareInviteLinkAction(\n\tnot_null<PeerData*> peer,\n\tstd::shared_ptr<Ui::Show> show);\n\nclass MicLevelTester final {\npublic:\n\texplicit MicLevelTester(Fn<void()> show);\n\n\t[[nodiscard]] bool showTooltip() const;\n\nprivate:\n\tvoid check();\n\n\tFn<void()> _show;\n\tbase::Timer _timer;\n\tstd::unique_ptr<Webrtc::DeviceResolver> _deviceId;\n\tstd::unique_ptr<Webrtc::AudioInputTester> _tester;\n\tint _loudCount = 0;\n\tint _quietCount = 0;\n\n};\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_stars_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_stars_box.h\"\n\n#include \"boxes/send_credits_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"payments/ui/payments_reaction_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace Calls::Group {\nnamespace {\n\nconstexpr auto kMaxStarsFallback = 10'000;\nconstexpr auto kDefaultStars = 10;\n\n} // namespace\n\nint MaxVideoStreamStarsCount(not_null<Main::Session*> session) {\n\tconst auto appConfig = &session->appConfig();\n\treturn std::max(\n\t\tappConfig->get<int>(\n\t\t\tu\"stars_groupcall_message_amount_max\"_q,\n\t\t\tkMaxStarsFallback),\n\t\t2);\n}\n\nvoid VideoStreamStarsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tVideoStreamStarsBoxArgs &&args) {\n\targs.show->session().credits().load();\n\n\tconst auto admin = args.admin;\n\tconst auto sending = args.sending;\n\tauto submitText = [=](rpl::producer<int> amount) {\n\t\tauto nice = std::move(amount) | rpl::map([=](int count) {\n\t\t\treturn Ui::CreditsEmojiSmall().append(\n\t\t\t\tLang::FormatCountDecimal(count));\n\t\t});\n\t\treturn admin\n\t\t\t? tr::lng_box_ok(tr::marked)\n\t\t\t: (sending\n\t\t\t\t? tr::lng_paid_reaction_button\n\t\t\t\t: tr::lng_paid_comment_button)(\n\t\t\t\t\tlt_stars,\n\t\t\t\t\tstd::move(nice),\n\t\t\t\t\ttr::rich);\n\t};\n\tconst auto &show = args.show;\n\tconst auto session = &show->session();\n\tconst auto max = std::max(args.min, MaxVideoStreamStarsCount(session));\n\tconst auto chosen = std::clamp(\n\t\targs.current ? args.current : kDefaultStars,\n\t\targs.min,\n\t\tmax);\n\tauto top = std::vector<Ui::PaidReactionTop>();\n\tconst auto add = [&](const Data::MessageReactionsTopPaid &entry) {\n\t\tconst auto peer = entry.peer;\n\t\tconst auto name = peer\n\t\t\t? peer->shortName()\n\t\t\t: tr::lng_paid_react_anonymous(tr::now);\n\t\tconst auto open = [=] {\n\t\t\tif (const auto controller = show->resolveWindow()) {\n\t\t\t\tcontroller->showPeerInfo(peer);\n\t\t\t}\n\t\t};\n\t\ttop.push_back({\n\t\t\t.name = name,\n\t\t\t.photo = (peer\n\t\t\t\t? Ui::MakeUserpicThumbnail(peer)\n\t\t\t\t: Ui::MakeHiddenAuthorThumbnail()),\n\t\t\t.barePeerId = peer ? uint64(peer->id.value) : 0,\n\t\t\t.count = int(entry.count),\n\t\t\t.click = peer ? open : Fn<void()>(),\n\t\t\t.my = (entry.my == 1),\n\t\t});\n\t};\n\n\ttop.reserve(args.top.size() + 1);\n\tfor (const auto &entry : args.top) {\n\t\tadd(entry);\n\t}\n\n\tauto myAdded = base::flat_set<uint64>();\n\tconst auto i = ranges::find(top, true, &Ui::PaidReactionTop::my);\n\tif (i != end(top)) {\n\t\tmyAdded.emplace(i->barePeerId);\n\t}\n\tconst auto myCount = uint32((i != end(top)) ? i->count : 0);\n\tconst auto myAdd = [&](not_null<PeerData*> peer) {\n\t\tconst auto barePeerId = uint64(peer->id.value);\n\t\tif (!myAdded.emplace(barePeerId).second) {\n\t\t\treturn;\n\t\t}\n\t\tadd(Data::MessageReactionsTopPaid{\n\t\t\t.peer = peer,\n\t\t\t.count = myCount,\n\t\t\t.my = true,\n\t\t});\n\t};\n\tmyAdd(session->user());\n\tranges::stable_sort(top, ranges::greater(), &Ui::PaidReactionTop::count);\n\tconst auto weak = base::make_weak(box);\n\tUi::PaidReactionsBox(box, {\n\t\t.min = args.min,\n\t\t.chosen = chosen,\n\t\t.max = max,\n\t\t.top = std::move(top),\n\t\t.session = &show->session(),\n\t\t.name = args.name,\n\t\t.submit = std::move(submitText),\n\t\t.colorings = show->session().appConfig().groupCallColorings(),\n\t\t.balanceValue = session->credits().balanceValue(),\n\t\t.send = [=, save = args.save](int count, uint64 barePeerId) {\n\t\t\tif (!admin) {\n\t\t\t\tsave(count);\n\t\t\t}\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t},\n\t\t.videoStreamChoosing = !sending,\n\t\t.videoStreamSending = sending,\n\t\t.videoStreamAdmin = admin,\n\t\t.dark = true,\n\t});\n}\n\nobject_ptr<Ui::BoxContent> MakeVideoStreamStarsBox(\n\t\tVideoStreamStarsBoxArgs &&args) {\n\treturn Box(VideoStreamStarsBox, std::move(args));\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_stars_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nstruct MessageReactionsTopPaid;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass BoxContent;\nclass GenericBox;\n} // namespace Ui\n\nnamespace Calls::Group::Ui {\nusing namespace ::Ui;\nstruct StarsColoring;\n} // namespace Calls::Group::Ui\n\nnamespace Calls::Group {\n\n[[nodiscard]] int MaxVideoStreamStarsCount(not_null<Main::Session*> session);\n\nstruct VideoStreamStarsBoxArgs {\n\tstd::shared_ptr<ChatHelpers::Show> show;\n\tstd::vector<Data::MessageReactionsTopPaid> top;\n\tint min = 0;\n\tint current = 0;\n\tbool sending = false;\n\tbool admin = false;\n\tFn<void(int)> save;\n\tQString name;\n};\n\nvoid VideoStreamStarsBox(\n\tnot_null<Ui::GenericBox*> box,\n\tVideoStreamStarsBoxArgs &&args);\n\n[[nodiscard]] object_ptr<Ui::BoxContent> MakeVideoStreamStarsBox(\n\tVideoStreamStarsBoxArgs &&args);\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_toasts.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_toasts.h\"\n\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/group/calls_group_panel.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_group_call.h\"\n#include \"ui/layers/show.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"lang/lang_keys.h\"\n\nnamespace Calls::Group {\nnamespace {\n\nconstexpr auto kErrorDuration = 2 * crl::time(1000);\n\nusing State = GroupCall::State;\n\n} // namespace\n\nToasts::Toasts(not_null<Panel*> panel)\n: _panel(panel)\n, _call(panel->call()) {\n\tsetup();\n}\n\nvoid Toasts::setup() {\n\tsetupJoinAsChanged();\n\tsetupTitleChanged();\n\tsetupRequestedToSpeak();\n\tsetupAllowedToSpeak();\n\tsetupPinnedVideo();\n\tsetupError();\n}\n\nvoid Toasts::setupJoinAsChanged() {\n\t_call->rejoinEvents(\n\t) | rpl::filter([](RejoinEvent event) {\n\t\treturn (event.wasJoinAs != event.nowJoinAs);\n\t}) | rpl::map([=] {\n\t\treturn _call->stateValue() | rpl::filter([](State state) {\n\t\t\treturn (state == State::Joined);\n\t\t}) | rpl::take(1);\n\t}) | rpl::flatten_latest() | rpl::on_next([=] {\n\t\t_panel->uiShow()->showToast((_call->peer()->isBroadcast()\n\t\t\t? tr::lng_group_call_join_as_changed_channel\n\t\t\t: tr::lng_group_call_join_as_changed)(\n\t\t\ttr::now,\n\t\t\tlt_name,\n\t\t\ttr::bold(_call->joinAs()->name()),\n\t\t\ttr::marked));\n\t}, _lifetime);\n}\n\nvoid Toasts::setupTitleChanged() {\n\t_call->titleChanged(\n\t) | rpl::filter([=] {\n\t\treturn (_call->lookupReal() != nullptr);\n\t}) | rpl::map([=] {\n\t\tconst auto peer = _call->peer();\n\t\treturn peer->groupCall()->title().isEmpty()\n\t\t\t? peer->name()\n\t\t\t: peer->groupCall()->title();\n\t}) | rpl::on_next([=](const QString &title) {\n\t\t_panel->uiShow()->showToast((_call->peer()->isBroadcast()\n\t\t\t? tr::lng_group_call_title_changed_channel\n\t\t\t: tr::lng_group_call_title_changed)(\n\t\t\ttr::now,\n\t\t\tlt_title,\n\t\t\ttr::bold(title),\n\t\t\ttr::marked));\n\t}, _lifetime);\n}\n\nvoid Toasts::setupAllowedToSpeak() {\n\t_call->allowedToSpeakNotifications(\n\t) | rpl::on_next([=] {\n\t\tif (_panel->isActive()) {\n\t\t\t_panel->uiShow()->showToast(\n\t\t\t\ttr::lng_group_call_can_speak_here(tr::now));\n\t\t} else {\n\t\t\tconst auto real = _call->lookupReal();\n\t\t\tconst auto name = (real && !real->title().isEmpty())\n\t\t\t\t? real->title()\n\t\t\t\t: _call->peer()->name();\n\t\t\tUi::Toast::Show({\n\t\t\t\t.text = tr::lng_group_call_can_speak(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_chat,\n\t\t\t\t\ttr::bold(name),\n\t\t\t\t\ttr::marked),\n\t\t\t});\n\t\t}\n\t}, _lifetime);\n}\n\nvoid Toasts::setupPinnedVideo() {\n\t_call->videoEndpointPinnedValue(\n\t) | rpl::map([=](bool pinned) {\n\t\treturn pinned\n\t\t\t? _call->videoEndpointLargeValue()\n\t\t\t: rpl::single(_call->videoEndpointLarge());\n\t}) | rpl::flatten_latest(\n\t) | rpl::filter([=] {\n\t\treturn (_call->shownVideoTracks().size() > 1);\n\t}) | rpl::on_next([=](const VideoEndpoint &endpoint) {\n\t\tconst auto pinned = _call->videoEndpointPinned();\n\t\tconst auto peer = endpoint.peer;\n\t\tif (!peer) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto text = [&] {\n\t\t\tconst auto me = (peer == _call->joinAs());\n\t\t\tconst auto camera = (endpoint.type == VideoEndpointType::Camera);\n\t\t\tif (me) {\n\t\t\t\tconst auto key = camera\n\t\t\t\t\t? (pinned\n\t\t\t\t\t\t? tr::lng_group_call_pinned_camera_me\n\t\t\t\t\t\t: tr::lng_group_call_unpinned_camera_me)\n\t\t\t\t\t: (pinned\n\t\t\t\t\t\t? tr::lng_group_call_pinned_screen_me\n\t\t\t\t\t\t: tr::lng_group_call_unpinned_screen_me);\n\t\t\t\treturn key(tr::now);\n\t\t\t}\n\t\t\tconst auto key = camera\n\t\t\t\t? (pinned\n\t\t\t\t\t? tr::lng_group_call_pinned_camera\n\t\t\t\t\t: tr::lng_group_call_unpinned_camera)\n\t\t\t\t: (pinned\n\t\t\t\t\t? tr::lng_group_call_pinned_screen\n\t\t\t\t\t: tr::lng_group_call_unpinned_screen);\n\t\t\treturn key(tr::now, lt_user, peer->shortName());\n\t\t}();\n\t\t_panel->uiShow()->showToast(text);\n\t}, _lifetime);\n}\n\nvoid Toasts::setupRequestedToSpeak() {\n\t_call->mutedValue(\n\t) | rpl::combine_previous(\n\t) | rpl::on_next([=](MuteState was, MuteState now) {\n\t\tif (was == MuteState::ForceMuted && now == MuteState::RaisedHand) {\n\t\t\t_panel->uiShow()->showToast(\n\t\t\t\ttr::lng_group_call_tooltip_raised_hand(tr::now));\n\t\t}\n\t}, _lifetime);\n}\n\nvoid Toasts::setupError() {\n\t_call->errors(\n\t) | rpl::on_next([=](Error error) {\n\t\tconst auto key = [&] {\n\t\t\tswitch (error) {\n\t\t\tcase Error::NoCamera: return tr::lng_call_error_no_camera;\n\t\t\tcase Error::CameraFailed:\n\t\t\t\treturn tr::lng_group_call_failed_camera;\n\t\t\tcase Error::ScreenFailed:\n\t\t\t\treturn tr::lng_group_call_failed_screen;\n\t\t\tcase Error::MutedNoCamera:\n\t\t\t\treturn tr::lng_group_call_muted_no_camera;\n\t\t\tcase Error::MutedNoScreen:\n\t\t\t\treturn tr::lng_group_call_muted_no_screen;\n\t\t\tcase Error::DisabledNoCamera:\n\t\t\t\treturn tr::lng_group_call_chat_no_camera;\n\t\t\tcase Error::DisabledNoScreen:\n\t\t\t\treturn tr::lng_group_call_chat_no_screen;\n\t\t\t}\n\t\t\tUnexpected(\"Error in Calls::Group::Toasts::setupErrorToasts.\");\n\t\t}();\n\t\t_panel->uiShow()->showToast({ key(tr::now) }, kErrorDuration);\n\t}, _lifetime);\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_toasts.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Calls {\nclass GroupCall;\n} // namespace Calls\n\nnamespace Calls::Group {\n\nclass Panel;\n\nclass Toasts final {\npublic:\n\texplicit Toasts(not_null<Panel*> panel);\n\nprivate:\n\tvoid setup();\n\tvoid setupJoinAsChanged();\n\tvoid setupTitleChanged();\n\tvoid setupRequestedToSpeak();\n\tvoid setupAllowedToSpeak();\n\tvoid setupPinnedVideo();\n\tvoid setupError();\n\n\tconst not_null<Panel*> _panel;\n\tconst not_null<GroupCall*> _call;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_viewport.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_viewport.h\"\n\n#include \"calls/group/calls_group_viewport_tile.h\"\n#include \"calls/group/calls_group_viewport_opengl.h\"\n#include \"calls/group/calls_group_viewport_raster.h\"\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n#include \"calls/group/calls_group_viewport_rhi.h\"\n#include \"ui/rhi/rhi_renderer.h\"\n#endif\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_members_row.h\"\n#include \"media/view/media_view_pip.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"ui/integration.h\"\n#include \"ui/painter.h\"\n#include \"ui/abstract_button.h\"\n#include \"ui/gl/gl_surface.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/cross_line.h\"\n#include \"data/data_group_call.h\" // MuteButtonTooltip.\n#include \"lang/lang_keys.h\"\n#include \"styles/style_calls.h\"\n\n#include <QOpenGLShader>\n#include <QtGui/QtEvents>\n#include <QOpenGLWidget>\n\nnamespace Calls::Group {\nnamespace {\n\n[[nodiscard]] QRect InterpolateRect(QRect a, QRect b, float64 ratio) {\n\tconst auto left = anim::interpolate(a.x(), b.x(), ratio);\n\tconst auto top = anim::interpolate(a.y(), b.y(), ratio);\n\tconst auto right = anim::interpolate(\n\t\ta.x() + a.width(),\n\t\tb.x() + b.width(),\n\t\tratio);\n\tconst auto bottom = anim::interpolate(\n\t\ta.y() + a.height(),\n\t\tb.y() + b.height(),\n\t\tratio);\n\treturn { left, top, right - left, bottom - top };\n}\n\n} // namespace\n\nViewport::Viewport(\n\tnot_null<QWidget*> parent,\n\tPanelMode mode,\n\tUi::GL::Backend backend,\n\tUi::RpWidgetWrap *borrowedRp,\n\tbool borrowedOpenGL)\n: _mode(mode)\n, _opengl(borrowedOpenGL)\n, _qrhi(borrowedRp && (backend == Ui::GL::Backend::QRhi))\n, _content(borrowedRp\n\t? nullptr\n\t: Ui::GL::CreateSurface(parent, chooseRenderer(backend)))\n, _borrowed(borrowedRp) {\n\tsetup();\n}\n\nViewport::~Viewport() {\n\tif (_borrowed) {\n\t\tif (_opengl) {\n\t\t\tconst auto w = static_cast<QOpenGLWidget*>(widget().get());\n\t\t\tw->makeCurrent();\n\t\t\tconst auto context = w->context();\n\t\t\tconst auto valid = w->isValid()\n\t\t\t\t&& context\n\t\t\t\t&& (QOpenGLContext::currentContext() == context);\n\t\t\tensureBorrowedCleared(valid ? context->functions() : nullptr);\n\t\t} else {\n\t\t\tensureBorrowedCleared();\n\t\t}\n\t}\n\t// Release ownership: _content is a QWidget child of the panel body,\n\t// which may already be destroyed by QWidget::deleteChildren().\n\t// Let Qt parent-child mechanism handle the lifetime.\n\t_content.release();\n}\n\nnot_null<QWidget*> Viewport::widget() const {\n\treturn _borrowed ? _borrowed->rpWidget() : _content->rpWidget();\n}\n\nnot_null<Ui::RpWidgetWrap*> Viewport::rp() const {\n\treturn _borrowed ? _borrowed : _content.get();\n}\n\nvoid Viewport::setup() {\n\tif (_borrowed) {\n\t\treturn;\n\t}\n\n\tconst auto raw = widget();\n\n\traw->resize(0, 0);\n\traw->setAttribute(Qt::WA_OpaquePaintEvent);\n\traw->setMouseTracking(true);\n\n\t_content->sizeValue(\n\t) | rpl::filter([=] {\n\t\treturn wide() || videoStream();\n\t}) | rpl::on_next([=] {\n\t\tupdateTilesGeometry();\n\t}, lifetime());\n\n\t_content->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::Enter) {\n\t\t\tUi::Integration::Instance().registerLeaveSubscription(raw);\n\t\t\t_mouseInside = true;\n\t\t} else if (type == QEvent::Leave) {\n\t\t\tUi::Integration::Instance().unregisterLeaveSubscription(raw);\n\t\t\tsetSelected({});\n\t\t\t_mouseInside = false;\n\t\t} else if (type == QEvent::MouseButtonPress) {\n\t\t\thandleMousePress(\n\t\t\t\tstatic_cast<QMouseEvent*>(e.get())->pos(),\n\t\t\t\tstatic_cast<QMouseEvent*>(e.get())->button());\n\t\t} else if (type == QEvent::MouseButtonRelease) {\n\t\t\thandleMouseRelease(\n\t\t\t\tstatic_cast<QMouseEvent*>(e.get())->pos(),\n\t\t\t\tstatic_cast<QMouseEvent*>(e.get())->button());\n\t\t} else if (type == QEvent::MouseMove) {\n\t\t\thandleMouseMove(static_cast<QMouseEvent*>(e.get())->pos());\n\t\t}\n\t}, lifetime());\n}\n\nvoid Viewport::setGeometry(bool fullscreen, QRect geometry) {\n\tExpects(wide() || videoStream());\n\n\tif (_borrowed) {\n\t\tupdateMyWidgetPart();\n\t\t_borrowedGeometry = geometry;\n\t\tupdateMyWidgetPart();\n\t\tupdateTilesGeometry();\n\t\treturn;\n\t}\n\n\tconst auto changed = (_fullscreen != fullscreen);\n\tif (changed) {\n\t\t_fullscreen = fullscreen;\n\t}\n\tif (widget()->geometry() != geometry) {\n\t\twidget()->setGeometry(geometry);\n\t} else if (changed) {\n\t\tupdateTilesGeometry();\n\t}\n}\n\nvoid Viewport::resizeToWidth(int width) {\n\tExpects(!wide() && !videoStream());\n\n\tupdateTilesGeometry(width);\n}\n\nvoid Viewport::setScrollTop(int scrollTop) {\n\tif (_scrollTop == scrollTop) {\n\t\treturn;\n\t}\n\t_scrollTop = scrollTop;\n\tupdateTilesGeometry();\n}\n\nbool Viewport::wide() const {\n\treturn (_mode == PanelMode::Wide);\n}\n\nbool Viewport::videoStream() const {\n\treturn (_mode == PanelMode::VideoStream);\n}\n\nvoid Viewport::setMode(PanelMode mode, not_null<QWidget*> parent) {\n\tif (_mode == mode && widget()->parent() == parent) {\n\t\treturn;\n\t}\n\t_mode = mode;\n\t_scrollTop = 0;\n\tsetControlsShown(1.);\n\tif (widget()->parent() != parent) {\n\t\tconst auto hidden = widget()->isHidden();\n\t\twidget()->setParent(parent);\n\t\tif (!hidden) {\n\t\t\twidget()->show();\n\t\t}\n\t}\n\tif (!wide() && !videoStream()) {\n\t\tfor (const auto &tile : _tiles) {\n\t\t\ttile->toggleTopControlsShown(false);\n\t\t}\n\t} else if (_selected.tile) {\n\t\t_selected.tile->toggleTopControlsShown(true);\n\t}\n}\n\nvoid Viewport::handleMousePress(QPoint position, Qt::MouseButton button) {\n\thandleMouseMove(position);\n\tsetPressed(_selected);\n}\n\nvoid Viewport::handleMouseRelease(QPoint position, Qt::MouseButton button) {\n\thandleMouseMove(position);\n\tconst auto pressed = _pressed;\n\tsetPressed({});\n\tif (const auto tile = pressed.tile) {\n\t\tif (pressed == _selected) {\n\t\t\tif (videoStream()) {\n\t\t\t\treturn;\n\t\t\t} else if (button == Qt::RightButton) {\n\t\t\t\ttile->row()->showContextMenu();\n\t\t\t} else if (!wide()\n\t\t\t\t|| (_hasTwoOrMore && !_large)\n\t\t\t\t|| pressed.element != Selection::Element::PinButton) {\n\t\t\t\t_clicks.fire_copy(tile->endpoint());\n\t\t\t} else if (pressed.element == Selection::Element::PinButton) {\n\t\t\t\t_pinToggles.fire(!tile->pinned());\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Viewport::handleMouseMove(QPoint position) {\n\tupdateSelected(position);\n}\n\nvoid Viewport::updateSelected(QPoint position) {\n\tif (_borrowed || !widget()->rect().contains(position)) {\n\t\tsetSelected({});\n\t\treturn;\n\t}\n\tfor (const auto &tile : _tiles) {\n\t\tconst auto geometry = tile->visible()\n\t\t\t? tile->geometry()\n\t\t\t: QRect();\n\t\tif (geometry.contains(position)) {\n\t\t\tconst auto pin = wide()\n\t\t\t\t&& tile->pinOuter().contains(position - geometry.topLeft());\n\t\t\tconst auto back = wide()\n\t\t\t\t&& tile->backOuter().contains(position - geometry.topLeft());\n\t\t\tsetSelected({\n\t\t\t\t.tile = tile.get(),\n\t\t\t\t.element = (pin\n\t\t\t\t\t? Selection::Element::PinButton\n\t\t\t\t\t: back\n\t\t\t\t\t? Selection::Element::BackButton\n\t\t\t\t\t: Selection::Element::Tile),\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t}\n\tsetSelected({});\n}\n\nvoid Viewport::updateSelected() {\n\tif (_borrowed) {\n\t\treturn;\n\t}\n\tupdateSelected(widget()->mapFromGlobal(QCursor::pos()));\n}\n\nvoid Viewport::setControlsShown(float64 shown) {\n\t_controlsShownRatio = shown;\n\tupdateMyWidgetPart();\n}\n\nvoid Viewport::updateMyWidgetPart() {\n\tif (!_borrowed) {\n\t\twidget()->update();\n\t} else if (!_borrowedGeometry.isEmpty()) {\n\t\twidget()->update(_borrowedGeometry);\n\t}\n}\n\nvoid Viewport::setCursorShown(bool shown) {\n\tif (_cursorHidden == shown) {\n\t\t_cursorHidden = !shown;\n\t\tupdateCursor();\n\t}\n}\n\nvoid Viewport::add(\n\t\tconst VideoEndpoint &endpoint,\n\t\tVideoTileTrack track,\n\t\trpl::producer<QSize> trackSize,\n\t\trpl::producer<bool> pinned,\n\t\tbool self) {\n\t_tiles.push_back(std::make_unique<VideoTile>(\n\t\tendpoint,\n\t\ttrack,\n\t\tstd::move(trackSize),\n\t\tstd::move(pinned),\n\t\t[=] { updateMyWidgetPart(); },\n\t\tself));\n\n\t_tiles.back()->trackSizeValue(\n\t) | rpl::filter([](QSize size) {\n\t\treturn !size.isEmpty();\n\t}) | rpl::on_next([=] {\n\t\tupdateTilesGeometry();\n\t}, _tiles.back()->lifetime());\n\n\t_tiles.back()->track()->stateValue(\n\t) | rpl::on_next([=] {\n\t\tupdateTilesGeometry();\n\t}, _tiles.back()->lifetime());\n}\n\nvoid Viewport::remove(const VideoEndpoint &endpoint) {\n\tconst auto i = ranges::find(_tiles, endpoint, &VideoTile::endpoint);\n\tif (i == end(_tiles)) {\n\t\treturn;\n\t}\n\tconst auto removing = i->get();\n\tconst auto largeRemoved = (_large == removing);\n\tif (largeRemoved) {\n\t\tprepareLargeChangeAnimation();\n\t\t_large = nullptr;\n\t}\n\tif (_selected.tile == removing) {\n\t\tsetSelected({});\n\t}\n\tif (_pressed.tile == removing) {\n\t\tsetPressed({});\n\t}\n\tfor (auto &geometry : _startTilesLayout.list) {\n\t\tif (geometry.tile == removing) {\n\t\t\tgeometry.tile = nullptr;\n\t\t}\n\t}\n\tfor (auto &geometry : _finishTilesLayout.list) {\n\t\tif (geometry.tile == removing) {\n\t\t\tgeometry.tile = nullptr;\n\t\t}\n\t}\n\t_tiles.erase(i);\n\tif (largeRemoved) {\n\t\tstartLargeChangeAnimation();\n\t} else {\n\t\tupdateTilesGeometry();\n\t}\n}\n\nvoid Viewport::prepareLargeChangeAnimation() {\n\tif (!wide()) {\n\t\treturn;\n\t} else if (_largeChangeAnimation.animating()) {\n\t\tupdateTilesAnimated();\n\t\tconst auto field = _finishTilesLayout.useColumns\n\t\t\t? &Geometry::columns\n\t\t\t: &Geometry::rows;\n\t\tfor (auto &finish : _finishTilesLayout.list) {\n\t\t\tconst auto tile = finish.tile;\n\t\t\tif (!tile) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfinish.*field = tile->geometry();\n\t\t}\n\t\t_startTilesLayout = std::move(_finishTilesLayout);\n\t\t_largeChangeAnimation.stop();\n\n\t\t_startTilesLayout.list.erase(\n\t\t\tranges::remove(_startTilesLayout.list, nullptr, &Geometry::tile),\n\t\t\tend(_startTilesLayout.list));\n\t} else {\n\t\t_startTilesLayout = applyLarge(std::move(_startTilesLayout));\n\t}\n}\n\nvoid Viewport::startLargeChangeAnimation() {\n\tExpects(!_largeChangeAnimation.animating());\n\n\tif (_borrowed\n\t\t|| !wide()\n\t\t|| anim::Disabled()\n\t\t|| (_startTilesLayout.list.size() < 2)\n\t\t|| !_opengl\n\t\t|| widget()->size().isEmpty()) {\n\t\tupdateTilesGeometry();\n\t\treturn;\n\t}\n\t_finishTilesLayout = applyLarge(\n\t\tcountWide(widget()->width(), widget()->height()));\n\tif (_finishTilesLayout.list.empty()\n\t\t|| _finishTilesLayout.outer != _startTilesLayout.outer) {\n\t\tupdateTilesGeometry();\n\t\treturn;\n\t}\n\t_largeChangeAnimation.start(\n\t\t[=] { updateTilesAnimated(); },\n\t\t0.,\n\t\t1.,\n\t\tst::slideDuration);\n}\n\nViewport::Layout Viewport::applyLarge(Layout layout) const {\n\tauto &list = layout.list;\n\tif (!_large) {\n\t\treturn layout;\n\t}\n\tconst auto i = ranges::find(list, _large, &Geometry::tile);\n\tif (i == end(list)) {\n\t\treturn layout;\n\t}\n\tconst auto field = layout.useColumns\n\t\t? &Geometry::columns\n\t\t: &Geometry::rows;\n\tconst auto fullWidth = layout.outer.width();\n\tconst auto fullHeight = layout.outer.height();\n\tconst auto largeRect = (*i).*field;\n\tconst auto largeLeft = largeRect.x();\n\tconst auto largeTop = largeRect.y();\n\tconst auto largeRight = largeLeft + largeRect.width();\n\tconst auto largeBottom = largeTop + largeRect.height();\n\tfor (auto &geometry : list) {\n\t\tif (geometry.tile == _large) {\n\t\t\tgeometry.*field = { QPoint(), layout.outer };\n\t\t} else if (layout.useColumns) {\n\t\t\tauto &rect = geometry.columns;\n\t\t\tconst auto center = rect.center();\n\t\t\tif (center.x() < largeLeft) {\n\t\t\t\trect = rect.translated(-largeLeft, 0);\n\t\t\t} else if (center.x() > largeRight) {\n\t\t\t\trect = rect.translated(fullWidth - largeRight, 0);\n\t\t\t} else if (center.y() < largeTop) {\n\t\t\t\trect = QRect(\n\t\t\t\t\t0,\n\t\t\t\t\trect.y() - largeTop,\n\t\t\t\t\tfullWidth,\n\t\t\t\t\trect.height());\n\t\t\t} else if (center.y() > largeBottom) {\n\t\t\t\trect = QRect(\n\t\t\t\t\t0,\n\t\t\t\t\trect.y() + (fullHeight - largeBottom),\n\t\t\t\t\tfullWidth,\n\t\t\t\t\trect.height());\n\t\t\t}\n\t\t} else {\n\t\t\tauto &rect = geometry.rows;\n\t\t\tconst auto center = rect.center();\n\t\t\tif (center.y() < largeTop) {\n\t\t\t\trect = rect.translated(0, -largeTop);\n\t\t\t} else if (center.y() > largeBottom) {\n\t\t\t\trect = rect.translated(0, fullHeight - largeBottom);\n\t\t\t} else if (center.x() < largeLeft) {\n\t\t\t\trect = QRect(\n\t\t\t\t\trect.x() - largeLeft,\n\t\t\t\t\t0,\n\t\t\t\t\trect.width(),\n\t\t\t\t\tfullHeight);\n\t\t\t} else {\n\t\t\t\trect = QRect(\n\t\t\t\t\trect.x() + (fullWidth - largeRight),\n\t\t\t\t\t0,\n\t\t\t\t\trect.width(),\n\t\t\t\t\tfullHeight);\n\t\t\t}\n\t\t}\n\t}\n\treturn layout;\n}\n\nvoid Viewport::updateTilesAnimated() {\n\tif (_borrowed || !_largeChangeAnimation.animating()) {\n\t\tupdateTilesGeometry();\n\t\treturn;\n\t}\n\tconst auto ratio = _largeChangeAnimation.value(1.);\n\tconst auto field = _finishTilesLayout.useColumns\n\t\t? &Geometry::columns\n\t\t: &Geometry::rows;\n\tfor (const auto &finish : _finishTilesLayout.list) {\n\t\tconst auto tile = finish.tile;\n\t\tif (!tile) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto i = ranges::find(\n\t\t\t_startTilesLayout.list,\n\t\t\ttile,\n\t\t\t&Geometry::tile);\n\t\tif (i == end(_startTilesLayout.list)) {\n\t\t\tLOG((\"Tiles Animation Error 1!\"));\n\t\t\t_largeChangeAnimation.stop();\n\t\t\tupdateTilesGeometry();\n\t\t\treturn;\n\t\t}\n\t\tconst auto from = (*i).*field;\n\t\tconst auto to = finish.*field;\n\t\ttile->setGeometry(\n\t\t\tInterpolateRect(from, to, ratio),\n\t\t\tTileAnimation{ from.size(), to.size(), ratio });\n\t}\n\twidget()->update();\n}\n\nViewport::Layout Viewport::countWide(int outerWidth, int outerHeight) const {\n\tauto result = Layout{ .outer = QSize(outerWidth, outerHeight) };\n\tauto &sizes = result.list;\n\tsizes.reserve(_tiles.size());\n\tfor (const auto &tile : _tiles) {\n\t\tconst auto video = tile.get();\n\t\tconst auto size = video->trackOrUserpicSize();\n\t\tif (!size.isEmpty()) {\n\t\t\tsizes.push_back(Geometry{ video, size });\n\t\t}\n\t}\n\tif (sizes.empty()) {\n\t\treturn result;\n\t} else if (sizes.size() == 1) {\n\t\tsizes.front().rows = { 0, 0, outerWidth, outerHeight };\n\t\treturn result;\n\t}\n\n\tauto columnsBlack = uint64();\n\tauto rowsBlack = uint64();\n\tconst auto count = int(sizes.size());\n\tconst auto skip = st::groupCallVideoLargeSkip;\n\tconst auto slices = int(std::ceil(std::sqrt(float64(count))));\n\t{\n\t\tauto index = 0;\n\t\tconst auto columns = slices;\n\t\tconst auto sizew = (outerWidth + skip) / float64(columns);\n\t\tfor (auto column = 0; column != columns; ++column) {\n\t\t\tconst auto left = int(base::SafeRound(column * sizew));\n\t\t\tconst auto width = int(\n\t\t\t\tbase::SafeRound(column * sizew + sizew - skip)) - left;\n\t\t\tconst auto rows = int(base::SafeRound((count - index)\n\t\t\t\t/ float64(columns - column)));\n\t\t\tconst auto sizeh = (outerHeight + skip) / float64(rows);\n\t\t\tfor (auto row = 0; row != rows; ++row) {\n\t\t\t\tconst auto top = int(base::SafeRound(row * sizeh));\n\t\t\t\tconst auto height = int(base::SafeRound(\n\t\t\t\t\trow * sizeh + sizeh - skip)) - top;\n\t\t\t\tauto &geometry = sizes[index];\n\t\t\t\tgeometry.columns = {\n\t\t\t\t\tleft,\n\t\t\t\t\ttop,\n\t\t\t\t\twidth,\n\t\t\t\t\theight };\n\t\t\t\tconst auto scaled = geometry.size.scaled(\n\t\t\t\t\twidth,\n\t\t\t\t\theight,\n\t\t\t\t\tQt::KeepAspectRatio);\n\t\t\t\tcolumnsBlack += (scaled.width() < width)\n\t\t\t\t\t? (width - scaled.width()) * height\n\t\t\t\t\t: (height - scaled.height()) * width;\n\t\t\t\t++index;\n\t\t\t}\n\t\t}\n\t}\n\t{\n\t\tauto index = 0;\n\t\tconst auto rows = slices;\n\t\tconst auto sizeh = (outerHeight + skip) / float64(rows);\n\t\tfor (auto row = 0; row != rows; ++row) {\n\t\t\tconst auto top = int(base::SafeRound(row * sizeh));\n\t\t\tconst auto height = int(\n\t\t\t\tbase::SafeRound(row * sizeh + sizeh - skip)) - top;\n\t\t\tconst auto columns = int(base::SafeRound((count - index)\n\t\t\t\t/ float64(rows - row)));\n\t\t\tconst auto sizew = (outerWidth + skip) / float64(columns);\n\t\t\tfor (auto column = 0; column != columns; ++column) {\n\t\t\t\tconst auto left = int(base::SafeRound(column * sizew));\n\t\t\t\tconst auto width = int(base::SafeRound(\n\t\t\t\t\tcolumn * sizew + sizew - skip)) - left;\n\t\t\t\tauto &geometry = sizes[index];\n\t\t\t\tgeometry.rows = {\n\t\t\t\t\tleft,\n\t\t\t\t\ttop,\n\t\t\t\t\twidth,\n\t\t\t\t\theight };\n\t\t\t\tconst auto scaled = geometry.size.scaled(\n\t\t\t\t\twidth,\n\t\t\t\t\theight,\n\t\t\t\t\tQt::KeepAspectRatio);\n\t\t\t\trowsBlack += (scaled.width() < width)\n\t\t\t\t\t? (width - scaled.width()) * height\n\t\t\t\t\t: (height - scaled.height()) * width;\n\t\t\t\t++index;\n\t\t\t}\n\t\t}\n\t}\n\tresult.useColumns = (columnsBlack < rowsBlack);\n\treturn result;\n}\n\nvoid Viewport::showLarge(const VideoEndpoint &endpoint) {\n\tif (_borrowed) {\n\t\treturn;\n\t}\n\n\t// If a video gets switched off, GroupCall first unpins it,\n\t// then removes it from Large endpoint, then removes from active tracks.\n\t//\n\t// If we want to animate large video removal properly, we need to\n\t// delay this update and start animation directly from removing of the\n\t// track from the active list. Otherwise final state won't be correct.\n\t_updateLargeScheduled = [=] {\n\t\tconst auto i = ranges::find(_tiles, endpoint, &VideoTile::endpoint);\n\t\tconst auto large = (i != end(_tiles)) ? i->get() : nullptr;\n\t\tif (_large != large) {\n\t\t\tprepareLargeChangeAnimation();\n\t\t\t_large = large;\n\t\t\tupdateTopControlsVisibility();\n\t\t\tstartLargeChangeAnimation();\n\t\t}\n\n\t\tEnsures(!_large || !_large->trackOrUserpicSize().isEmpty());\n\t};\n\tcrl::on_main(widget(), [=] {\n\t\tif (!_updateLargeScheduled) {\n\t\t\treturn;\n\t\t}\n\t\tbase::take(_updateLargeScheduled)();\n\t});\n}\n\nvoid Viewport::updateTilesGeometry() {\n\tupdateTilesGeometry(_borrowed\n\t\t? _borrowedGeometry.width()\n\t\t: widget()->width());\n}\n\nvoid Viewport::updateTilesGeometry(int outerWidth) {\n\tconst auto mouseInside = _mouseInside.current();\n\tconst auto guard = gsl::finally([&] {\n\t\tif (mouseInside) {\n\t\t\tupdateSelected();\n\t\t}\n\t\tupdateMyWidgetPart();\n\t});\n\n\tconst auto outerHeight = _borrowed\n\t\t? _borrowedGeometry.height()\n\t\t: widget()->height();\n\tif (_tiles.empty() || !outerWidth) {\n\t\t_fullHeight = 0;\n\t\treturn;\n\t}\n\n\tif (wide() || videoStream()) {\n\t\tupdateTilesGeometryWide(outerWidth, outerHeight);\n\t\trefreshHasTwoOrMore();\n\t\t_fullHeight = 0;\n\t} else {\n\t\tupdateTilesGeometryNarrow(outerWidth);\n\t}\n}\n\nvoid Viewport::refreshHasTwoOrMore() {\n\tauto hasTwoOrMore = false;\n\tauto oneFound = false;\n\tfor (const auto &tile : _tiles) {\n\t\tif (!tile->trackOrUserpicSize().isEmpty()) {\n\t\t\tif (oneFound) {\n\t\t\t\thasTwoOrMore = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\toneFound = true;\n\t\t}\n\t}\n\tif (_hasTwoOrMore == hasTwoOrMore) {\n\t\treturn;\n\t}\n\t_hasTwoOrMore = hasTwoOrMore;\n\tupdateCursor();\n\tupdateTopControlsVisibility();\n}\n\nvoid Viewport::updateTopControlsVisibility() {\n\tif (_selected.tile) {\n\t\t_selected.tile->toggleTopControlsShown(\n\t\t\t_hasTwoOrMore && wide() && _large && _large == _selected.tile);\n\t}\n}\n\nvoid Viewport::updateTilesGeometryWide(int outerWidth, int outerHeight) {\n\tif (!outerHeight) {\n\t\treturn;\n\t} else if (_largeChangeAnimation.animating()) {\n\t\tif (_startTilesLayout.outer == QSize(outerWidth, outerHeight)) {\n\t\t\treturn;\n\t\t}\n\t\t_largeChangeAnimation.stop();\n\t}\n\n\t_startTilesLayout = countWide(outerWidth, outerHeight);\n\tif (_large && !_large->trackOrUserpicSize().isEmpty()) {\n\t\tfor (const auto &geometry : _startTilesLayout.list) {\n\t\t\tif (geometry.tile == _large) {\n\t\t\t\tsetTileGeometry(_large, { 0, 0, outerWidth, outerHeight });\n\t\t\t} else {\n\t\t\t\tgeometry.tile->hide();\n\t\t\t}\n\t\t}\n\t} else {\n\t\tconst auto field = _startTilesLayout.useColumns\n\t\t\t? &Geometry::columns\n\t\t\t: &Geometry::rows;\n\t\tfor (const auto &geometry : _startTilesLayout.list) {\n\t\t\tif (const auto video = geometry.tile) {\n\t\t\t\tsetTileGeometry(video, geometry.*field);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Viewport::updateTilesGeometryNarrow(int outerWidth) {\n\tif (outerWidth <= st::groupCallNarrowMembersWidth) {\n\t\tupdateTilesGeometryColumn(outerWidth);\n\t\treturn;\n\t}\n\n\tconst auto y = -_scrollTop;\n\tauto sizes = base::flat_map<not_null<VideoTile*>, QSize>();\n\tsizes.reserve(_tiles.size());\n\tfor (const auto &tile : _tiles) {\n\t\tconst auto video = tile.get();\n\t\tconst auto size = video->trackOrUserpicSize();\n\t\tif (size.isEmpty()) {\n\t\t\tvideo->hide();\n\t\t} else {\n\t\t\tsizes.emplace(video, size);\n\t\t}\n\t}\n\tif (sizes.empty()) {\n\t\t_fullHeight = 0;\n\t\treturn;\n\t} else if (sizes.size() == 1) {\n\t\tconst auto size = sizes.front().second;\n\t\tconst auto heightMin = (outerWidth * 9) / 16;\n\t\tconst auto heightMax = (outerWidth * 3) / 4;\n\t\tconst auto scaled = size.scaled(\n\t\t\tQSize(outerWidth, heightMax),\n\t\t\tQt::KeepAspectRatio);\n\t\tconst auto height = std::max(scaled.height(), heightMin);\n\t\tconst auto skip = st::groupCallVideoSmallSkip;\n\t\tsetTileGeometry(sizes.front().first, { 0, y, outerWidth, height });\n\t\t_fullHeight = height + skip;\n\t\treturn;\n\t}\n\tconst auto min = (st::groupCallWidth\n\t\t- st::groupCallMembersMargin.left()\n\t\t- st::groupCallMembersMargin.right()\n\t\t- st::groupCallVideoSmallSkip) / 2;\n\tconst auto square = (outerWidth - st::groupCallVideoSmallSkip) / 2;\n\tconst auto skip = (outerWidth - 2 * square);\n\tconst auto put = [&](not_null<VideoTile*> tile, int column, int row) {\n\t\tsetTileGeometry(tile, {\n\t\t\t(column == 2) ? 0 : column ? (outerWidth - square) : 0,\n\t\t\ty + row * (min + skip),\n\t\t\t(column == 2) ? outerWidth : square,\n\t\t\tmin,\n\t\t});\n\t};\n\tconst auto rows = (sizes.size() + 1) / 2;\n\tif (sizes.size() == 3) {\n\t\tput(sizes.front().first, 2, 0);\n\t\tput((sizes.begin() + 1)->first, 0, 1);\n\t\tput((sizes.begin() + 2)->first, 1, 1);\n\t} else {\n\t\tauto row = 0;\n\t\tauto column = 0;\n\t\tfor (const auto &[video, endpoint] : sizes) {\n\t\t\tput(video, column, row);\n\t\t\tif (column) {\n\t\t\t\t++row;\n\t\t\t\tcolumn = (row + 1 == rows && sizes.size() % 2) ? 2 : 0;\n\t\t\t} else {\n\t\t\t\tcolumn = 1;\n\t\t\t}\n\t\t}\n\t}\n\t_fullHeight = rows * (min + skip);\n}\n\nvoid Viewport::updateTilesGeometryColumn(int outerWidth) {\n\tconst auto y = -_scrollTop;\n\tauto top = 0;\n\tconst auto layoutNext = [&](not_null<VideoTile*> tile) {\n\t\tconst auto size = tile->trackOrUserpicSize();\n\t\tconst auto shown = !size.isEmpty() && _large && tile != _large;\n\t\tconst auto height = st::groupCallNarrowVideoHeight;\n\t\tif (!shown) {\n\t\t\ttile->hide();\n\t\t} else {\n\t\t\tsetTileGeometry(tile, { 0, y + top, outerWidth, height });\n\t\t\ttop += height + st::groupCallVideoSmallSkip;\n\t\t}\n\t};\n\tconst auto topPeer = _large ? _large->peer().get() : nullptr;\n\tconst auto reorderNeeded = [&] {\n\t\tif (!topPeer) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (const auto &tile : _tiles) {\n\t\t\tif (tile.get() != _large && tile->peer() == topPeer) {\n\t\t\t\treturn (tile.get() != _tiles.front().get())\n\t\t\t\t\t&& !tile->trackOrUserpicSize().isEmpty();\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}();\n\tif (reorderNeeded) {\n\t\t_tilesForOrder.clear();\n\t\t_tilesForOrder.reserve(_tiles.size());\n\t\tfor (const auto &tile : _tiles) {\n\t\t\t_tilesForOrder.push_back(tile.get());\n\t\t}\n\t\tranges::stable_partition(\n\t\t\t_tilesForOrder,\n\t\t\t[&](not_null<VideoTile*> tile) {\n\t\t\t\treturn (tile->peer() == topPeer);\n\t\t\t});\n\t\tfor (const auto &tile : _tilesForOrder) {\n\t\t\tlayoutNext(tile);\n\t\t}\n\t} else {\n\t\tfor (const auto &tile : _tiles) {\n\t\t\tlayoutNext(tile.get());\n\t\t}\n\t}\n\t_fullHeight = top;\n}\n\nvoid Viewport::setTileGeometry(not_null<VideoTile*> tile, QRect geometry) {\n\ttile->setGeometry(geometry);\n\n\tconst auto min = std::min(geometry.width(), geometry.height());\n\tconst auto kMedium = style::ConvertScale(540);\n\tconst auto kSmall = style::ConvertScale(240);\n\tconst auto &endpoint = tile->endpoint();\n\tconst auto forceThumbnailQuality = !wide()\n\t\t&& !videoStream()\n\t\t&& (ranges::count(_tiles, false, &VideoTile::hidden) > 1);\n\tconst auto forceFullQuality = videoStream()\n\t\t|| (wide() && (tile.get() == _large));\n\tconst auto quality = forceThumbnailQuality\n\t\t? VideoQuality::Thumbnail\n\t\t: (forceFullQuality || min >= kMedium)\n\t\t? VideoQuality::Full\n\t\t: (min >= kSmall)\n\t\t? VideoQuality::Medium\n\t\t: VideoQuality::Thumbnail;\n\tif (tile->updateRequestedQuality(quality)) {\n\t\t_qualityRequests.fire(VideoQualityRequest{\n\t\t\t.endpoint = endpoint,\n\t\t\t.quality = quality,\n\t\t});\n\t}\n}\n\nvoid Viewport::setSelected(Selection value) {\n\tif (_selected == value) {\n\t\treturn;\n\t}\n\tif (_selected.tile) {\n\t\t_selected.tile->toggleTopControlsShown(false);\n\t}\n\t_selected = value;\n\tupdateTopControlsVisibility();\n\tupdateCursor();\n}\n\nvoid Viewport::updateCursor() {\n\tif (_borrowed) {\n\t\treturn;\n\t}\n\tconst auto pointer = _selected.tile && (!wide() || _hasTwoOrMore);\n\twidget()->setCursor(_cursorHidden\n\t\t? Qt::BlankCursor\n\t\t: pointer\n\t\t? style::cur_pointer\n\t\t: style::cur_default);\n}\n\nvoid Viewport::setPressed(Selection value) {\n\tif (_pressed == value) {\n\t\treturn;\n\t}\n\t_pressed = value;\n}\n\nUi::GL::ChosenRenderer Viewport::chooseRenderer(Ui::GL::Backend backend) {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\tif (backend == Ui::GL::Backend::QRhi) {\n\t\t_opengl = true;\n\t\t_qrhi = true;\n\t\treturn {\n\t\t\t.renderer = std::make_unique<RendererRhi>(this),\n\t\t\t.backend = Ui::GL::Backend::QRhi,\n\t\t};\n\t}\n#else\n\tif (backend == Ui::GL::Backend::QRhi) {\n\t\treturn {\n\t\t\t.renderer = std::make_unique<RendererSW>(this),\n\t\t\t.backend = Ui::GL::Backend::QRhi,\n\t\t};\n\t}\n#endif\n\t_opengl = (backend == Ui::GL::Backend::OpenGL);\n\treturn {\n\t\t.renderer = makeRenderer(),\n\t\t.backend = backend,\n\t};\n}\n\nstd::unique_ptr<Ui::GL::Renderer> Viewport::makeRenderer() {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\tif (_qrhi) {\n\t\treturn std::make_unique<RendererRhi>(this);\n\t}\n#endif\n\treturn _opengl\n\t\t? std::unique_ptr<Ui::GL::Renderer>(\n\t\t\tstd::make_unique<RendererGL>(this))\n\t\t: std::make_unique<RendererSW>(this);\n}\n\nbool Viewport::requireARGB32() const {\n\treturn !_opengl;\n}\n\nint Viewport::fullHeight() const {\n\treturn _fullHeight.current();\n}\n\nrpl::producer<int> Viewport::fullHeightValue() const {\n\treturn _fullHeight.value();\n}\n\nrpl::producer<bool> Viewport::pinToggled() const {\n\treturn _pinToggles.events();\n}\n\nrpl::producer<VideoEndpoint> Viewport::clicks() const {\n\treturn _clicks.events();\n}\n\nrpl::producer<VideoQualityRequest> Viewport::qualityRequests() const {\n\treturn _qualityRequests.events();\n}\n\nrpl::producer<bool> Viewport::mouseInsideValue() const {\n\treturn _mouseInside.value();\n}\n\nvoid Viewport::ensureBorrowedRenderer(QOpenGLFunctions &f) {\n\tExpects(_borrowed != nullptr);\n\tExpects(_opengl);\n\n\tif (_borrowedRenderer) {\n\t\treturn;\n\t}\n\t_borrowedRenderer = makeRenderer();\n\t_borrowedRenderer->init(f);\n}\n\nvoid Viewport::ensureBorrowedCleared(QOpenGLFunctions *f) {\n\tExpects(_borrowed != nullptr);\n\tExpects(_opengl);\n\n\tif (const auto renderer = base::take(_borrowedRenderer)) {\n\t\trenderer->deinit(f);\n\t}\n}\n\nvoid Viewport::borrowedPaint(QOpenGLFunctions &f) {\n\tExpects(_borrowedRenderer != nullptr);\n\tExpects(_opengl);\n\n\t_borrowedRenderer->paint(static_cast<QOpenGLWidget*>(widget().get()), f);\n}\n\nvoid Viewport::ensureBorrowedRenderer() {\n\tExpects(_borrowed != nullptr);\n\tExpects(!_opengl);\n\n\tif (_borrowedRenderer) {\n\t\treturn;\n\t}\n\t_borrowedRenderer = makeRenderer();\n}\n\nvoid Viewport::ensureBorrowedCleared() {\n\tExpects(_borrowed != nullptr);\n\tExpects(!_opengl);\n\n\tbase::take(_borrowedRenderer);\n}\n\nvoid Viewport::borrowedPaint(Painter &p, const QRegion &clip) {\n\tExpects(_borrowedRenderer != nullptr);\n\tExpects(!_opengl);\n\n\t_borrowedRenderer->paintFallback(p, clip, Ui::GL::Backend::Raster);\n}\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\nUi::Rhi::Renderer *Viewport::ensureBorrowedRhi(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\tExpects(_borrowed != nullptr);\n\n\tif (!_borrowedRenderer) {\n\t\t_borrowedRenderer = makeRenderer();\n\t}\n\tif (const auto r = dynamic_cast<Ui::Rhi::Renderer*>(\n\t\t\t_borrowedRenderer.get())) {\n\t\tr->initialize(rhi, rt, cb);\n\t\treturn r;\n\t}\n\treturn nullptr;\n}\n\nvoid Viewport::borrowedPaintOffscreen(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\tif (const auto r = ensureBorrowedRhi(rhi, rt, cb)) {\n\t\tr->renderOffscreen(rhi, rt, cb);\n\t}\n}\n\nvoid Viewport::borrowedPaintOnscreen(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\tif (const auto r = ensureBorrowedRhi(rhi, rt, cb)) {\n\t\tr->renderOnscreen(rhi, rt, cb);\n\t}\n}\n#endif\n\nQPoint Viewport::borrowedOrigin() const {\n\treturn _borrowed ? _borrowedGeometry.topLeft() : QPoint();\n}\n\nrpl::lifetime &Viewport::lifetime() {\n\treturn _lifetime;\n}\n\nrpl::producer<QString> MuteButtonTooltip(not_null<GroupCall*> call) {\n\t//return rpl::single(std::make_tuple(\n\t//\t(Data::GroupCall*)nullptr,\n\t//\tcall->scheduleDate()\n\t//)) | rpl::then(call->real(\n\t//) | rpl::map([](not_null<Data::GroupCall*> real) {\n\t//\tusing namespace rpl::mappers;\n\t//\treturn real->scheduleDateValue(\n\t//\t) | rpl::map([=](TimeId scheduleDate) {\n\t//\t\treturn std::make_tuple(real.get(), scheduleDate);\n\t//\t});\n\t//}) | rpl::flatten_latest(\n\t//)) | rpl::map([=](\n\t//\t\tData::GroupCall *real,\n\t//\t\tTimeId scheduleDate) -> rpl::producer<QString> {\n\t//\tif (scheduleDate) {\n\t//\t\treturn rpl::combine(\n\t//\t\t\tcall->canManageValue(),\n\t//\t\t\t(real\n\t//\t\t\t\t? real->scheduleStartSubscribedValue()\n\t//\t\t\t\t: rpl::single(false))\n\t//\t\t) | rpl::map([](bool canManage, bool subscribed) {\n\t//\t\t\treturn canManage\n\t//\t\t\t\t? tr::lng_group_call_start_now()\n\t//\t\t\t\t: subscribed\n\t//\t\t\t\t? tr::lng_group_call_cancel_reminder()\n\t//\t\t\t\t: tr::lng_group_call_set_reminder();\n\t//\t\t}) | rpl::flatten_latest();\n\t//\t}\n\tif (call->rtmp()) {\n\t\treturn nullptr;\n\t}\n\t\treturn call->mutedValue(\n\t\t) | rpl::map([](MuteState muted) {\n\t\t\tswitch (muted) {\n\t\t\tcase MuteState::Active:\n\t\t\tcase MuteState::PushToTalk:\n\t\t\t\treturn tr::lng_group_call_you_are_live();\n\t\t\tcase MuteState::ForceMuted:\n\t\t\t\treturn tr::lng_group_call_tooltip_force_muted();\n\t\t\tcase MuteState::RaisedHand:\n\t\t\t\treturn tr::lng_group_call_tooltip_raised_hand();\n\t\t\tcase MuteState::Muted:\n\t\t\t\treturn tr::lng_group_call_tooltip_microphone();\n\t\t\t}\n\t\t\tUnexpected(\"Value in MuteState in showNiceTooltip.\");\n\t\t}) | rpl::flatten_latest();\n\t//}) | rpl::flatten_latest();\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_viewport.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n\nclass Painter;\nclass QOpenGLFunctions;\nclass QRhi;\nclass QRhiRenderTarget;\nclass QRhiCommandBuffer;\n\nnamespace Ui {\nclass AbstractButton;\nclass RpWidgetWrap;\n} // namespace Ui\n\nnamespace Ui::Rhi {\nclass Renderer;\n} // namespace Ui::Rhi\n\nnamespace Ui::GL {\nenum class Backend;\nstruct Capabilities;\nstruct ChosenRenderer;\nclass Renderer;\n} // namespace Ui::GL\n\nnamespace Calls {\nclass GroupCall;\nstruct VideoEndpoint;\nstruct VideoQualityRequest;\n} // namespace Calls\n\nnamespace Webrtc {\nclass VideoTrack;\n} // namespace Webrtc\n\nnamespace Calls::Group {\n\nclass MembersRow;\nenum class PanelMode;\nenum class VideoQuality;\n\nstruct VideoTileTrack {\n\tWebrtc::VideoTrack *track = nullptr;\n\tMembersRow *row = nullptr;\n\trpl::variable<QSize> trackSize;\n\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn track != nullptr;\n\t}\n};\n\n[[nodiscard]] inline bool operator==(\n\t\tVideoTileTrack a,\n\t\tVideoTileTrack b) noexcept {\n\treturn (a.track == b.track) && (a.row == b.row);\n}\n\n[[nodiscard]] inline bool operator!=(\n\t\tVideoTileTrack a,\n\t\tVideoTileTrack b) noexcept {\n\treturn !(a == b);\n}\n\nclass Viewport final {\npublic:\n\tViewport(\n\t\tnot_null<QWidget*> parent,\n\t\tPanelMode mode,\n\t\tUi::GL::Backend backend,\n\t\tUi::RpWidgetWrap *borrowedRp = nullptr,\n\t\tbool borrowedOpenGL = false);\n\t~Viewport();\n\n\t[[nodiscard]] not_null<QWidget*> widget() const;\n\t[[nodiscard]] not_null<Ui::RpWidgetWrap*> rp() const;\n\n\tvoid setMode(PanelMode mode, not_null<QWidget*> parent);\n\tvoid setControlsShown(float64 shown);\n\tvoid setCursorShown(bool shown);\n\tvoid setGeometry(bool fullscreen, QRect geometry);\n\tvoid resizeToWidth(int newWidth);\n\tvoid setScrollTop(int scrollTop);\n\n\tvoid add(\n\t\tconst VideoEndpoint &endpoint,\n\t\tVideoTileTrack track,\n\t\trpl::producer<QSize> trackSize,\n\t\trpl::producer<bool> pinned,\n\t\tbool self);\n\tvoid remove(const VideoEndpoint &endpoint);\n\tvoid showLarge(const VideoEndpoint &endpoint);\n\n\t[[nodiscard]] bool requireARGB32() const;\n\t[[nodiscard]] int fullHeight() const;\n\t[[nodiscard]] rpl::producer<int> fullHeightValue() const;\n\t[[nodiscard]] rpl::producer<bool> pinToggled() const;\n\t[[nodiscard]] rpl::producer<VideoEndpoint> clicks() const;\n\t[[nodiscard]] rpl::producer<VideoQualityRequest> qualityRequests() const;\n\t[[nodiscard]] rpl::producer<bool> mouseInsideValue() const;\n\n\tvoid ensureBorrowedRenderer(QOpenGLFunctions &f);\n\tvoid ensureBorrowedCleared(QOpenGLFunctions *f);\n\tvoid borrowedPaint(QOpenGLFunctions &f);\n\n\tvoid ensureBorrowedRenderer();\n\tvoid ensureBorrowedCleared();\n\tvoid borrowedPaint(Painter &p, const QRegion &clip);\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\tvoid borrowedPaintOffscreen(QRhi *rhi, QRhiRenderTarget *rt, QRhiCommandBuffer *cb);\n\tvoid borrowedPaintOnscreen(QRhi *rhi, QRhiRenderTarget *rt, QRhiCommandBuffer *cb);\nprivate:\n\t[[nodiscard]] Ui::Rhi::Renderer *ensureBorrowedRhi(QRhi *rhi, QRhiRenderTarget *rt, QRhiCommandBuffer *cb);\npublic:\n#endif\n\n\t[[nodiscard]] QPoint borrowedOrigin() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\n\tstatic constexpr auto kShadowMaxAlpha = 80;\n\nprivate:\n\tstruct Textures;\n\tclass VideoTile;\n\tclass RendererSW;\n\tclass RendererGL;\n\tclass RendererRhi;\n\tusing TileId = quintptr;\n\n\tstruct Geometry {\n\t\tVideoTile *tile = nullptr;\n\t\tQSize size;\n\t\tQRect rows;\n\t\tQRect columns;\n\t};\n\n\tstruct Layout {\n\t\tstd::vector<Geometry> list;\n\t\tQSize outer;\n\t\tbool useColumns = false;\n\t};\n\n\tstruct TileAnimation {\n\t\tQSize from;\n\t\tQSize to;\n\t\tfloat64 ratio = -1.;\n\t};\n\n\tstruct Selection {\n\t\tenum class Element {\n\t\t\tNone,\n\t\t\tTile,\n\t\t\tPinButton,\n\t\t\tBackButton,\n\t\t};\n\t\tVideoTile *tile = nullptr;\n\t\tElement element = Element::None;\n\n\t\tinline bool operator==(Selection other) const {\n\t\t\treturn (tile == other.tile) && (element == other.element);\n\t\t}\n\t};\n\n\tvoid setup();\n\t[[nodiscard]] bool wide() const;\n\t[[nodiscard]] bool videoStream() const;\n\n\tvoid updateCursor();\n\tvoid updateTilesGeometry();\n\tvoid updateTilesGeometry(int outerWidth);\n\tvoid updateTilesGeometryWide(int outerWidth, int outerHeight);\n\tvoid updateTilesGeometryNarrow(int outerWidth);\n\tvoid updateTilesGeometryColumn(int outerWidth);\n\tvoid setTileGeometry(not_null<VideoTile*> tile, QRect geometry);\n\tvoid refreshHasTwoOrMore();\n\tvoid updateTopControlsVisibility();\n\n\tvoid prepareLargeChangeAnimation();\n\tvoid startLargeChangeAnimation();\n\tvoid updateTilesAnimated();\n\t[[nodiscard]] Layout countWide(int outerWidth, int outerHeight) const;\n\t[[nodiscard]] Layout applyLarge(Layout layout) const;\n\n\tvoid setSelected(Selection value);\n\tvoid setPressed(Selection value);\n\n\tvoid handleMousePress(QPoint position, Qt::MouseButton button);\n\tvoid handleMouseRelease(QPoint position, Qt::MouseButton button);\n\tvoid handleMouseMove(QPoint position);\n\tvoid updateSelected(QPoint position);\n\tvoid updateSelected();\n\n\t[[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer(\n\t\tUi::GL::Backend backend);\n\t[[nodiscard]] std::unique_ptr<Ui::GL::Renderer> makeRenderer();\n\tvoid updateMyWidgetPart();\n\n\tPanelMode _mode = PanelMode();\n\tbool _opengl = false;\n\tbool _qrhi = false;\n\tstd::unique_ptr<Ui::RpWidgetWrap> _content;\n\tstd::vector<std::unique_ptr<VideoTile>> _tiles;\n\tstd::vector<not_null<VideoTile*>> _tilesForOrder;\n\trpl::variable<int> _fullHeight = 0;\n\tbool _hasTwoOrMore = false;\n\tbool _fullscreen = false;\n\tbool _cursorHidden = false;\n\tint _scrollTop = 0;\n\tQImage _shadow;\n\trpl::event_stream<VideoEndpoint> _clicks;\n\trpl::event_stream<bool> _pinToggles;\n\trpl::event_stream<VideoQualityRequest> _qualityRequests;\n\tfloat64 _controlsShownRatio = 1.;\n\tVideoTile *_large = nullptr;\n\tFn<void()> _updateLargeScheduled;\n\tUi::Animations::Simple _largeChangeAnimation;\n\tLayout _startTilesLayout;\n\tLayout _finishTilesLayout;\n\tSelection _selected;\n\tSelection _pressed;\n\trpl::variable<bool> _mouseInside = false;\n\n\tUi::RpWidgetWrap * const _borrowed = nullptr;\n\tQRect _borrowedGeometry;\n\tstd::unique_ptr<Ui::GL::Renderer> _borrowedRenderer;\n\tQMetaObject::Connection _borrowedConnection;\n\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] QImage GenerateShadow(\n\tint height,\n\tint topAlpha,\n\tint bottomAlpha,\n\tQColor color = QColor(0, 0, 0));\n\n[[nodiscard]] rpl::producer<QString> MuteButtonTooltip(\n\tnot_null<GroupCall*> call);\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_viewport_opengl.h\"\n\n#include \"calls/group/calls_group_viewport_tile.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"media/view/media_view_pip.h\"\n#include \"media/streaming/media_streaming_utility.h\"\n#include \"calls/group/calls_group_members_row.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/gl/gl_shader.h\"\n#include \"ui/painter.h\"\n#include \"data/data_peer.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_media_view.h\"\n\n#include <QOpenGLShader>\n\nnamespace Calls::Group {\nnamespace {\n\nusing namespace Ui::GL;\n\nconstexpr auto kScaleForBlurTextureIndex = 3;\nconstexpr auto kFirstBlurPassTextureIndex = 4;\nconstexpr auto kNoiseTextureSize = 256;\n\n// The more the scale - more blurred the image.\nconstexpr auto kBlurTextureSizeFactor = 4.;\nconstexpr auto kBlurOpacity = 0.65;\nconstexpr auto kDitherNoiseAmount = 0.002;\n\nconstexpr auto kQuads = 9;\nconstexpr auto kQuadVertices = kQuads * 4;\nconstexpr auto kQuadValues = kQuadVertices * 4;\nconstexpr auto kValues = kQuadValues + 8; // Blur texture coordinates.\n\n[[nodiscard]] ShaderPart FragmentBlurTexture(\n\t\tbool vertical,\n\t\tchar prefix = 'v') {\n\tconst auto offsets = (vertical ? QString(\"0, 1\") : QString(\"1, 0\"));\n\tconst auto name = prefix + QString(\"_texcoord\");\n\treturn {\n\t\t.header = R\"(\nvarying vec2 )\" + name + R\"(;\nuniform sampler2D b_texture;\nuniform float texelOffset;\nconst vec3 satLuminanceWeighting = vec3(0.2126, 0.7152, 0.0722);\nconst vec2 offsets = vec2()\" + offsets + R\"();\nconst int radius = 15;\nconst int diameter = 2 * radius + 1;\n)\",\n\t\t.body = R\"(\n\tvec4 accumulated = vec4(0.);\n\tfor (int i = 0; i != diameter; i++) {\n\t\tfloat stepOffset = float(i - radius) * texelOffset;\n\t\tvec2 offset = vec2(stepOffset) * offsets;\n\t\tvec4 sampled = vec4(texture2D(b_texture, )\" + name + R\"( + offset));\n\t\tfloat fradius = float(radius);\n\t\tfloat boxWeight = fradius + 1.0 - abs(float(i) - fradius);\n\t\taccumulated += sampled * boxWeight;\n\t}\n\tvec3 blurred = accumulated.rgb / accumulated.a;\n\tfloat satLuminance = dot(blurred, satLuminanceWeighting);\n\tvec3 mixinColor = vec3(satLuminance);\n\tresult = vec4(clamp(mix(mixinColor, blurred, 1.1), 0.0, 1.0), 1.0);\n)\",\n\t};\n}\n\n[[nodiscard]] ShaderPart FragmentGenerateNoise() {\n\tconst auto size = QString::number(kNoiseTextureSize);\n\treturn {\n\t\t.header = R\"(\nconst float permTexUnit = 1.0 / )\" + size + R\"(.0;\nconst float permTexUnitHalf = 0.5 / )\" + size + R\"(.0;\nconst float grainsize = 1.3;\nconst float noiseCoordRotation = 1.425;\nconst vec2 dimensions = vec2()\" + size + \", \" + size + R\"();\n\nvec4 rnm(vec2 tc) {\n\tfloat noise = sin(dot(tc, vec2(12.9898, 78.233))) * 43758.5453;\n\treturn vec4(\n\t\tfract(noise),\n\t\tfract(noise * 1.2154),\n\t\tfract(noise * 1.3453),\n\t\tfract(noise * 1.3647)\n\t) * 2.0 - 1.0;\n}\n\nfloat fade(float t) {\n\treturn t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\n\nfloat pnoise3D(vec3 p) {\n\tvec3 pi = permTexUnit * floor(p) + permTexUnitHalf;\n\tvec3 pf = fract(p);\n\tfloat perm = rnm(pi.xy).a;\n\tfloat n000 = dot(rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, pf);\n\tfloat n001 = dot(\n\t\trnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0,\n\t\tpf - vec3(0.0, 0.0, 1.0));\n\tperm = rnm(pi.xy + vec2(0.0, permTexUnit)).a;\n\tfloat n010 = dot(\n\t\trnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0,\n\t\tpf - vec3(0.0, 1.0, 0.0));\n\tfloat n011 = dot(\n\t\trnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0,\n\t\tpf - vec3(0.0, 1.0, 1.0));\n\tperm = rnm(pi.xy + vec2(permTexUnit, 0.0)).a;\n\tfloat n100 = dot(\n\t\trnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0,\n\t\tpf - vec3(1.0, 0.0, 0.0));\n\tfloat n101 = dot(\n\t\trnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0,\n\t\tpf - vec3(1.0, 0.0, 1.0));\n\tperm = rnm(pi.xy + vec2(permTexUnit, permTexUnit)).a;\n\tfloat n110 = dot(\n\t\trnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0,\n\t\tpf - vec3(1.0, 1.0, 0.0));\n\tfloat n111 = dot(\n\t\trnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0,\n\t\tpf - vec3(1.0, 1.0, 1.0));\n\tvec4 n_x = mix(\n\t\tvec4(n000, n001, n010, n011),\n\t\tvec4(n100, n101, n110, n111),\n\t\tfade(pf.x));\n\tvec2 n_xy = mix(n_x.xy, n_x.zw, fade(pf.y));\n\treturn mix(n_xy.x, n_xy.y, fade(pf.z));\n}\n\nvec2 rotateTexCoords(in lowp vec2 tc, in lowp float angle) {\n\tfloat cosa = cos(angle);\n\tfloat sina = sin(angle);\n\treturn vec2(\n\t\t((tc.x * 2.0 - 1.0) * cosa - (tc.y * 2.0 - 1.0) * sina) * 0.5 + 0.5,\n\t\t((tc.y * 2.0 - 1.0) * cosa + (tc.x * 2.0 - 1.0) * sina) * 0.5 + 0.5);\n}\n)\",\n\t\t.body = R\"(\n\tvec2 rotatedCoords = rotateTexCoords(\n\t\tgl_FragCoord.xy / dimensions.xy,\n\t\tnoiseCoordRotation);\n\tfloat intensity = pnoise3D(vec3(\n\t\trotatedCoords.x * dimensions.x / grainsize,\n\t\trotatedCoords.y * dimensions.y / grainsize,\n\t\t0.0));\n\n\t// Looks like intensity is almost always in [-2, 2] range.\n\tfloat clamped = clamp((intensity + 2.) * 0.25, 0., 1.);\n\tresult = vec4(clamped, 0., 0., 1.);\n)\",\n\t};\n}\n\n[[nodiscard]] ShaderPart FragmentDitherNoise() {\n\tconst auto size = QString::number(kNoiseTextureSize);\n\treturn {\n\t\t.header = R\"(\nuniform sampler2D n_texture;\n)\",\n\t\t.body = R\"(\n\tvec2 noiseTextureCoord = gl_FragCoord.xy / )\" + size + R\"(.;\n\tfloat noiseClamped = texture2D(n_texture, noiseTextureCoord).r;\n\tfloat noiseIntensity = (noiseClamped * 4.) - 2.;\n\n\tvec3 lumcoeff = vec3(0.299, 0.587, 0.114);\n\tfloat luminance = dot(result.rgb, lumcoeff);\n\tfloat lum = smoothstep(0.2, 0.0, luminance) + luminance;\n\tvec3 noiseColor = mix(vec3(noiseIntensity), vec3(0.0), pow(lum, 4.0));\n\n\tresult.rgb = result.rgb + noiseColor * noiseGrain;\n)\",\n\t};\n}\n\n// Depends on FragmentSampleTexture().\n[[nodiscard]] ShaderPart FragmentFrameColor() {\n\tconst auto round = FragmentRoundCorners();\n\tconst auto blur = FragmentBlurTexture(true, 'b');\n\tconst auto noise = FragmentDitherNoise();\n\treturn {\n\t\t.header = R\"(\nuniform vec4 frameBg;\nuniform vec4 shadow; // fullHeight, shown, maxOpacity, blur opacity\nuniform float paused; // 0. <-> 1.\n\n)\" + blur.header + round.header + noise.header + R\"(\n\nconst float noiseGrain = )\" + QString::number(kDitherNoiseAmount) + R\"(;\n\nfloat insideTexture() {\n\tvec2 textureHalf = vec2(0.5, 0.5);\n\tvec2 fromTextureCenter = abs(v_texcoord - textureHalf);\n\tvec2 fromTextureEdge = max(fromTextureCenter, textureHalf) - textureHalf;\n\tfloat outsideCheck = dot(fromTextureEdge, fromTextureEdge);\n\treturn step(outsideCheck, 0.);\n}\n\nvec4 background() {\n\tvec4 result;\n\n)\" + blur.body + noise.body + R\"(\n\n\treturn result;\n}\n)\",\n\t\t.body = R\"(\n\tfloat inside = insideTexture() * (1. - paused);\n\tfloat backgroundOpacity = shadow.w;\n\tresult = result * inside\n\t\t+ (1. - inside) * (backgroundOpacity * background()\n\t\t\t+ (1. - backgroundOpacity) * frameBg);\n\n\tfloat shadowCoord = gl_FragCoord.y - roundRect.y;\n\tfloat shadowValue = max(1. - (shadowCoord / shadow.x), 0.);\n\tfloat shadowShown = max(shadowValue * shadow.y, paused) * shadow.z;\n\tresult = vec4(result.rgb * (1. - shadowShown), result.a);\n)\" + round.body,\n\t};\n}\n\n[[nodiscard]] bool UseExpandForCamera(QSize original, QSize viewport) {\n\tusing namespace ::Media::Streaming;\n\treturn DecideFrameResize(viewport, original).expanding;\n}\n\n[[nodiscard]] QSize NonEmpty(QSize size) {\n\treturn QSize(std::max(size.width(), 1), std::max(size.height(), 1));\n}\n\n[[nodiscard]] QSize CountBlurredSize(\n\t\tQSize unscaled,\n\t\tQSize outer,\n\t\tfloat factor) {\n\tfactor *= kBlurTextureSizeFactor;\n\tconst auto area = outer / int(base::SafeRound(factor * cScale() / 100));\n\tconst auto scaled = unscaled.scaled(area, Qt::KeepAspectRatio);\n\treturn (scaled.width() > unscaled.width()\n\t\t|| scaled.height() > unscaled.height())\n\t\t? unscaled\n\t\t: NonEmpty(scaled);\n}\n\n[[nodiscard]] QSize InterpolateScaledSize(\n\t\tQSize unscaled,\n\t\tQSize size,\n\t\tfloat64 ratio) {\n\tif (ratio == 0.) {\n\t\treturn NonEmpty(unscaled.scaled(\n\t\t\tsize,\n\t\t\tQt::KeepAspectRatio));\n\t} else if (ratio == 1.) {\n\t\treturn NonEmpty(unscaled.scaled(\n\t\t\tsize,\n\t\t\tQt::KeepAspectRatioByExpanding));\n\t}\n\tconst auto notExpanded = NonEmpty(unscaled.scaled(\n\t\tsize,\n\t\tQt::KeepAspectRatio));\n\tconst auto expanded = NonEmpty(unscaled.scaled(\n\t\tsize,\n\t\tQt::KeepAspectRatioByExpanding));\n\treturn QSize(\n\t\tanim::interpolate(notExpanded.width(), expanded.width(), ratio),\n\t\tanim::interpolate(notExpanded.height(), expanded.height(), ratio));\n}\n\n[[nodiscard]] std::array<std::array<GLfloat, 2>, 4> CountTexCoords(\n\t\tQSize unscaled,\n\t\tQSize size,\n\t\tfloat64 expandRatio,\n\t\tbool swap = false) {\n\tconst auto scaled = InterpolateScaledSize(unscaled, size, expandRatio);\n\tconst auto left = (size.width() - scaled.width()) / 2;\n\tconst auto top = (size.height() - scaled.height()) / 2;\n\tauto dleft = float(left) / scaled.width();\n\tauto dright = float(size.width() - left) / scaled.width();\n\tauto dtop = float(top) / scaled.height();\n\tauto dbottom = float(size.height() - top) / scaled.height();\n\tif (swap) {\n\t\tstd::swap(dleft, dtop);\n\t\tstd::swap(dright, dbottom);\n\t}\n\treturn { {\n\t\t{ { -dleft, 1.f + dtop } },\n\t\t{ { dright, 1.f + dtop } },\n\t\t{ { dright, 1.f - dbottom } },\n\t\t{ { -dleft, 1.f - dbottom } },\n\t} };\n}\n\n} // namespace\n\nViewport::RendererGL::RendererGL(not_null<Viewport*> owner)\n: _owner(owner)\n, _pinIcon(st::groupCallVideoTile.pin)\n, _muteIcon(st::groupCallVideoCrossLine)\n, _pinBackground(\n\t(st::groupCallVideoTile.pinPadding.top()\n\t\t+ st::groupCallVideoTile.pin.icon.height()\n\t\t+ st::groupCallVideoTile.pinPadding.bottom()) / 2,\n\tst::radialBg) {\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_buttons.invalidate();\n\t}, _lifetime);\n}\n\nvoid Viewport::RendererGL::init(QOpenGLFunctions &f) {\n\t_frameBuffer.emplace();\n\t_frameBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw);\n\t_frameBuffer->create();\n\t_frameBuffer->bind();\n\t_frameBuffer->allocate(kValues * sizeof(GLfloat));\n\t_downscaleProgram.yuv420.emplace();\n\t_downscaleVertexShader = LinkProgram(\n\t\t&*_downscaleProgram.yuv420,\n\t\tVertexShader({\n\t\t\tVertexPassTextureCoord(),\n\t\t}),\n\t\tFragmentShader({\n\t\t\tFragmentSampleYUV420Texture(),\n\t\t})).vertex;\n\tif (!_downscaleProgram.yuv420->isLinked()) {\n\t\t//...\n\t}\n\t_blurProgram.emplace();\n\tLinkProgram(\n\t\t&*_blurProgram,\n\t\t_downscaleVertexShader,\n\t\tFragmentShader({\n\t\t\tFragmentBlurTexture(false),\n\t\t}));\n\t_frameProgram.yuv420.emplace();\n\t_frameVertexShader = LinkProgram(\n\t\t&*_frameProgram.yuv420,\n\t\tVertexShader({\n\t\t\tVertexViewportTransform(),\n\t\t\tVertexPassTextureCoord(),\n\t\t\tVertexPassTextureCoord('b'),\n\t\t}),\n\t\tFragmentShader({\n\t\t\tFragmentSampleYUV420Texture(),\n\t\t\tFragmentFrameColor(),\n\t\t})).vertex;\n\n\t_imageProgram.emplace();\n\tLinkProgram(\n\t\t&*_imageProgram,\n\t\tVertexShader({\n\t\t\tVertexViewportTransform(),\n\t\t\tVertexPassTextureCoord(),\n\t\t}),\n\t\tFragmentShader({\n\t\t\tFragmentSampleARGB32Texture(),\n\t\t\tFragmentGlobalOpacity(),\n\t\t}));\n\n\tvalidateNoiseTexture(f, 0);\n}\n\nvoid Viewport::RendererGL::ensureARGB32Program() {\n\tExpects(_downscaleVertexShader != nullptr);\n\tExpects(_frameVertexShader != nullptr);\n\n\t_downscaleProgram.argb32.emplace();\n\tLinkProgram(\n\t\t&*_downscaleProgram.argb32,\n\t\t_downscaleVertexShader,\n\t\tFragmentShader({\n\t\t\tFragmentSampleARGB32Texture(),\n\t\t}));\n\n\t_frameProgram.argb32.emplace();\n\tLinkProgram(\n\t\t&*_frameProgram.argb32,\n\t\t_frameVertexShader,\n\t\tFragmentShader({\n\t\t\tFragmentSampleARGB32Texture(),\n\t\t\tFragmentFrameColor(),\n\t\t}));\n}\n\nvoid Viewport::RendererGL::deinit(QOpenGLFunctions *f) {\n\t_frameBuffer = std::nullopt;\n\t_frameVertexShader = nullptr;\n\t_imageProgram = std::nullopt;\n\t_downscaleProgram.argb32 = std::nullopt;\n\t_downscaleProgram.yuv420 = std::nullopt;\n\t_blurProgram = std::nullopt;\n\t_frameProgram.argb32 = std::nullopt;\n\t_frameProgram.yuv420 = std::nullopt;\n\t_noiseTexture.destroy(f);\n\t_noiseFramebuffer.destroy(f);\n\tfor (auto &data : _tileData) {\n\t\tdata.textures.destroy(f);\n\t\tdata.framebuffers.destroy(f);\n\t}\n\t_tileData.clear();\n\t_tileDataIndices.clear();\n\t_buttons.destroy(f);\n\t_names.destroy(f);\n}\n\nvoid Viewport::RendererGL::setDefaultViewport(QOpenGLFunctions &f) {\n\tf.glViewport(\n\t\t0,\n\t\t0,\n\t\t_viewport.width() * _factor,\n\t\t_viewport.height() * _factor);\n}\n\nvoid Viewport::RendererGL::paint(\n\t\tnot_null<QOpenGLWidget*> widget,\n\t\tQOpenGLFunctions &f) {\n\tconst auto factor = widget->devicePixelRatioF();\n\tif (_factor != factor) {\n\t\t_factor = factor;\n\t\t_ifactor = int(std::ceil(_factor));\n\t\t_buttons.invalidate();\n\t}\n\t_viewport = widget->size();\n\n\tconst auto defaultFramebufferObject = widget->defaultFramebufferObject();\n\n\tvalidateDatas();\n\tauto index = 0;\n\tfor (const auto &tile : _owner->_tiles) {\n\t\tif (!tile->visible()) {\n\t\t\tindex++;\n\t\t\tcontinue;\n\t\t}\n\t\tpaintTile(\n\t\t\tf,\n\t\t\tdefaultFramebufferObject,\n\t\t\ttile.get(),\n\t\t\t_tileData[_tileDataIndices[index++]]);\n\t}\n}\n\nstd::optional<QColor> Viewport::RendererGL::clearColor() {\n\treturn _owner->videoStream() ? st::mediaviewBg->c : st::groupCallBg->c;\n}\n\nvoid Viewport::RendererGL::validateUserpicFrame(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &tileData) {\n\tif (!_userpicFrame) {\n\t\ttileData.userpicFrame = QImage();\n\t\treturn;\n\t} else if (!tileData.userpicFrame.isNull()) {\n\t\treturn;\n\t}\n\tconst auto size = tile->trackOrUserpicSize();\n\ttileData.userpicFrame = PeerData::GenerateUserpicImage(\n\t\ttile->peer(),\n\t\ttile->row()->ensureUserpicView(),\n\t\tsize.width(),\n\t\t0);\n}\n\nvoid Viewport::RendererGL::paintTile(\n\t\tQOpenGLFunctions &f,\n\t\tGLuint defaultFramebufferObject,\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &tileData) {\n\tconst auto track = tile->track();\n\tconst auto markGuard = gsl::finally([&] {\n\t\ttile->track()->markFrameShown();\n\t});\n\tconst auto data = track->frameWithInfo(false);\n\t_userpicFrame = (data.format == Webrtc::FrameFormat::None);\n\tvalidateUserpicFrame(tile, tileData);\n\tconst auto frameSize = _userpicFrame\n\t\t? tileData.userpicFrame.size()\n\t\t: data.yuv420->size;\n\tconst auto frameRotation = _userpicFrame\n\t\t? 0\n\t\t: data.rotation;\n\tAssert(!frameSize.isEmpty());\n\n\t_rgbaFrame = (data.format == Webrtc::FrameFormat::ARGB32)\n\t\t|| _userpicFrame;\n\tconst auto geometry = tile->geometry().translated(\n\t\t_owner->borrowedOrigin());\n\tconst auto x = geometry.x();\n\tconst auto y = geometry.y();\n\tconst auto width = geometry.width();\n\tconst auto height = geometry.height();\n\tconst auto &st = st::groupCallVideoTile;\n\tconst auto shown = _owner->_controlsShownRatio;\n\tconst auto fullscreen = _owner->_fullscreen;\n\tconst auto fullNameShift = st.namePosition.y() + st::normalFont->height;\n\tconst auto nameShift = anim::interpolate(fullNameShift, 0, shown);\n\tconst auto row = tile->row();\n\n\tvalidateOutlineAnimation(tile, tileData);\n\tvalidatePausedAnimation(tile, tileData);\n\tconst auto outline = tileData.outlined.value(tileData.outline ? 1. : 0.);\n\tconst auto paused = tileData.paused.value(tileData.pause ? 1. : 0.);\n\n\tensureButtonsImage();\n\n\t// Frame.\n\tconst auto unscaled = Media::View::FlipSizeByRotation(\n\t\tframeSize,\n\t\tframeRotation);\n\tconst auto tileSize = geometry.size();\n\tconst auto swap = (((frameRotation / 90) % 2) == 1);\n\tconst auto expand = isExpanded(tile, unscaled, tileSize);\n\tconst auto animation = tile->animation();\n\tconst auto expandRatio = (animation.ratio >= 0.)\n\t\t? countExpandRatio(tile, unscaled, animation)\n\t\t: expand\n\t\t? 1.\n\t\t: 0.;\n\tauto texCoords = CountTexCoords(unscaled, tileSize, expandRatio, swap);\n\tauto blurTexCoords = (expandRatio == 1. && !swap)\n\t\t? texCoords\n\t\t: CountTexCoords(unscaled, tileSize, 1.);\n\tconst auto rect = transformRect(geometry);\n\tauto toBlurTexCoords = std::array<std::array<GLfloat, 2>, 4> { {\n\t\t{ { 0.f, 1.f } },\n\t\t{ { 1.f, 1.f } },\n\t\t{ { 1.f, 0.f } },\n\t\t{ { 0.f, 0.f } },\n\t} };\n\tif (tile->mirror()) {\n\t\tstd::swap(toBlurTexCoords[0], toBlurTexCoords[1]);\n\t\tstd::swap(toBlurTexCoords[2], toBlurTexCoords[3]);\n\t\tstd::swap(texCoords[0], texCoords[1]);\n\t\tstd::swap(texCoords[2], texCoords[3]);\n\t}\n\tif (const auto shift = (frameRotation / 90); shift > 0) {\n\t\tstd::rotate(\n\t\t\ttoBlurTexCoords.begin(),\n\t\t\ttoBlurTexCoords.begin() + shift,\n\t\t\ttoBlurTexCoords.end());\n\t\tstd::rotate(\n\t\t\ttexCoords.begin(),\n\t\t\ttexCoords.begin() + shift,\n\t\t\ttexCoords.end());\n\t}\n\n\tconst auto nameTop = y + (height\n\t\t- st.namePosition.y()\n\t\t- st::semiboldFont->height);\n\n\t// Paused icon and text.\n\tconst auto middle = (st::groupCallVideoPlaceholderHeight\n\t\t- st::groupCallPaused.height()) / 2;\n\tconst auto pausedSpace = (nameTop - y)\n\t\t- st::groupCallPaused.height()\n\t\t- st::semiboldFont->height;\n\tconst auto pauseIconSkip = middle - st::groupCallVideoPlaceholderIconTop;\n\tconst auto pauseTextSkip = st::groupCallVideoPlaceholderTextTop\n\t\t- st::groupCallVideoPlaceholderIconTop;\n\tconst auto pauseIconTop = !_owner->wide()\n\t\t? (y + (height - st::groupCallPaused.height()) / 2)\n\t\t: (pausedSpace < 3 * st::semiboldFont->height)\n\t\t? (pausedSpace / 3)\n\t\t: std::min(\n\t\t\ty + (height / 2) - pauseIconSkip,\n\t\t\t(nameTop\n\t\t\t\t- st::semiboldFont->height * 3\n\t\t\t\t- st::groupCallPaused.height()));\n\tconst auto pauseTextTop = (pausedSpace < 3 * st::semiboldFont->height)\n\t\t? (nameTop - (pausedSpace / 3) - st::semiboldFont->height)\n\t\t: std::min(\n\t\t\tpauseIconTop + pauseTextSkip,\n\t\t\tnameTop - st::semiboldFont->height * 2);\n\n\tconst auto pauseIcon = _buttons.texturedRect(\n\t\tQRect(\n\t\t\tx + (width - st::groupCallPaused.width()) / 2,\n\t\t\tpauseIconTop,\n\t\t\tst::groupCallPaused.width(),\n\t\t\tst::groupCallPaused.height()),\n\t\t_paused);\n\tconst auto pauseRect = transformRect(pauseIcon.geometry);\n\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto pausedPosition = QPoint(\n\t\tx + (width - (_pausedTextRect.width() / factor)) / 2,\n\t\tpauseTextTop);\n\tconst auto pausedText = _names.texturedRect(\n\t\tQRect(pausedPosition, _pausedTextRect.size() / factor),\n\t\t_pausedTextRect);\n\tconst auto pausedRect = transformRect(pausedText.geometry);\n\n\t// Pin.\n\tconst auto pin = _buttons.texturedRect(\n\t\ttile->pinInner().translated(x, y),\n\t\ttile->pinned() ? _pinOn : _pinOff,\n\t\tgeometry);\n\tconst auto pinRect = transformRect(pin.geometry);\n\n\t// Back.\n\tconst auto back = _buttons.texturedRect(\n\t\ttile->backInner().translated(x, y),\n\t\t_back,\n\t\tgeometry);\n\tconst auto backRect = transformRect(back.geometry);\n\n\t// Mute.\n\tconst auto &icon = st::groupCallVideoCrossLine.icon;\n\tconst auto iconLeft = x + width - st.iconPosition.x() - icon.width();\n\tconst auto iconTop = y + (height\n\t\t- st.iconPosition.y()\n\t\t- icon.height()\n\t\t+ nameShift);\n\tconst auto mute = _buttons.texturedRect(\n\t\tQRect(iconLeft, iconTop, icon.width(), icon.height()),\n\t\t(row->state() == MembersRow::State::Active\n\t\t\t? _muteOff\n\t\t\t: _muteOn),\n\t\tgeometry);\n\tconst auto muteRect = transformRect(mute.geometry);\n\n\t// Name.\n\tconst auto namePosition = QPoint(\n\t\tx + st.namePosition.x(),\n\t\tnameTop + nameShift);\n\tconst auto name = _names.texturedRect(\n\t\tQRect(namePosition, tileData.nameRect.size() / factor),\n\t\ttileData.nameRect,\n\t\tgeometry);\n\tconst auto nameRect = transformRect(name.geometry);\n\n\tconst GLfloat coords[] = {\n\t\t// YUV -> RGB-for-blur quad.\n\t\t-1.f, 1.f,\n\t\ttoBlurTexCoords[0][0], toBlurTexCoords[0][1],\n\n\t\t1.f, 1.f,\n\t\ttoBlurTexCoords[1][0], toBlurTexCoords[1][1],\n\n\t\t1.f, -1.f,\n\t\ttoBlurTexCoords[2][0], toBlurTexCoords[2][1],\n\n\t\t-1.f, -1.f,\n\t\ttoBlurTexCoords[3][0], toBlurTexCoords[3][1],\n\n\t\t// First RGB -> RGB blur pass.\n\t\t-1.f, 1.f,\n\t\t0.f, 1.f,\n\n\t\t1.f, 1.f,\n\t\t1.f, 1.f,\n\n\t\t1.f, -1.f,\n\t\t1.f, 0.f,\n\n\t\t-1.f, -1.f,\n\t\t0.f, 0.f,\n\n\t\t// Second blur pass + paint final frame.\n\t\trect.left(), rect.top(),\n\t\ttexCoords[0][0], texCoords[0][1],\n\n\t\trect.right(), rect.top(),\n\t\ttexCoords[1][0], texCoords[1][1],\n\n\t\trect.right(), rect.bottom(),\n\t\ttexCoords[2][0], texCoords[2][1],\n\n\t\trect.left(), rect.bottom(),\n\t\ttexCoords[3][0], texCoords[3][1],\n\n\t\t// Additional blurred background texture coordinates.\n\t\tblurTexCoords[0][0], blurTexCoords[0][1],\n\t\tblurTexCoords[1][0], blurTexCoords[1][1],\n\t\tblurTexCoords[2][0], blurTexCoords[2][1],\n\t\tblurTexCoords[3][0], blurTexCoords[3][1],\n\n\t\t// Pin button.\n\t\tpinRect.left(), pinRect.top(),\n\t\tpin.texture.left(), pin.texture.bottom(),\n\n\t\tpinRect.right(), pinRect.top(),\n\t\tpin.texture.right(), pin.texture.bottom(),\n\n\t\tpinRect.right(), pinRect.bottom(),\n\t\tpin.texture.right(), pin.texture.top(),\n\n\t\tpinRect.left(), pinRect.bottom(),\n\t\tpin.texture.left(), pin.texture.top(),\n\n\t\t// Back button.\n\t\tbackRect.left(), backRect.top(),\n\t\tback.texture.left(), back.texture.bottom(),\n\n\t\tbackRect.right(), backRect.top(),\n\t\tback.texture.right(), back.texture.bottom(),\n\n\t\tbackRect.right(), backRect.bottom(),\n\t\tback.texture.right(), back.texture.top(),\n\n\t\tbackRect.left(), backRect.bottom(),\n\t\tback.texture.left(), back.texture.top(),\n\n\t\t// Mute icon.\n\t\tmuteRect.left(), muteRect.top(),\n\t\tmute.texture.left(), mute.texture.bottom(),\n\n\t\tmuteRect.right(), muteRect.top(),\n\t\tmute.texture.right(), mute.texture.bottom(),\n\n\t\tmuteRect.right(), muteRect.bottom(),\n\t\tmute.texture.right(), mute.texture.top(),\n\n\t\tmuteRect.left(), muteRect.bottom(),\n\t\tmute.texture.left(), mute.texture.top(),\n\n\t\t// Name.\n\t\tnameRect.left(), nameRect.top(),\n\t\tname.texture.left(), name.texture.bottom(),\n\n\t\tnameRect.right(), nameRect.top(),\n\t\tname.texture.right(), name.texture.bottom(),\n\n\t\tnameRect.right(), nameRect.bottom(),\n\t\tname.texture.right(), name.texture.top(),\n\n\t\tnameRect.left(), nameRect.bottom(),\n\t\tname.texture.left(), name.texture.top(),\n\n\t\t// Paused icon.\n\t\tpauseRect.left(), pauseRect.top(),\n\t\tpauseIcon.texture.left(), pauseIcon.texture.bottom(),\n\n\t\tpauseRect.right(), pauseRect.top(),\n\t\tpauseIcon.texture.right(), pauseIcon.texture.bottom(),\n\n\t\tpauseRect.right(), pauseRect.bottom(),\n\t\tpauseIcon.texture.right(), pauseIcon.texture.top(),\n\n\t\tpauseRect.left(), pauseRect.bottom(),\n\t\tpauseIcon.texture.left(), pauseIcon.texture.top(),\n\n\t\t// Paused text.\n\t\tpausedRect.left(), pausedRect.top(),\n\t\tpausedText.texture.left(), pausedText.texture.bottom(),\n\n\t\tpausedRect.right(), pausedRect.top(),\n\t\tpausedText.texture.right(), pausedText.texture.bottom(),\n\n\t\tpausedRect.right(), pausedRect.bottom(),\n\t\tpausedText.texture.right(), pausedText.texture.top(),\n\n\t\tpausedRect.left(), pausedRect.bottom(),\n\t\tpausedText.texture.left(), pausedText.texture.top(),\n\t};\n\n\t_frameBuffer->bind();\n\t_frameBuffer->write(0, coords, sizeof(coords));\n\n\tconst auto blurSize = CountBlurredSize(\n\t\tunscaled,\n\t\tgeometry.size(),\n\t\t_factor);\n\tprepareObjects(f, tileData, blurSize);\n\tf.glViewport(0, 0, blurSize.width(), blurSize.height());\n\n\tbindFrame(f, data, tileData, _downscaleProgram);\n\n\tdrawDownscalePass(f, tileData);\n\tdrawFirstBlurPass(f, tileData, blurSize);\n\n\tf.glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebufferObject);\n\tsetDefaultViewport(f);\n\n\tbindFrame(f, data, tileData, _frameProgram);\n\n\tconst auto program = _rgbaFrame\n\t\t? &*_frameProgram.argb32\n\t\t: &*_frameProgram.yuv420;\n\tconst auto uniformViewport = QSizeF(_viewport) * _factor;\n\n\tprogram->bind();\n\tprogram->setUniformValue(\"viewport\", uniformViewport);\n\tprogram->setUniformValue(\n\t\t\"frameBg\",\n\t\tfullscreen ? QColor(0, 0, 0) : *clearColor());\n\tconst auto radius = _owner->videoStream()\n\t\t? st::storiesRadius\n\t\t: st::roundRadiusLarge;\n\tprogram->setUniformValue(\"radiusOutline\", QVector2D(\n\t\tGLfloat(radius * _factor * (fullscreen ? 0. : 1.)),\n\t\t(outline > 0) ? (st::groupCallOutline * _factor) : 0.f));\n\tprogram->setUniformValue(\"roundRect\", Uniform(rect));\n\tprogram->setUniformValue(\"roundBg\", *clearColor());\n\tprogram->setUniformValue(\"outlineFg\", QVector4D(\n\t\tst::groupCallMemberActiveIcon->c.redF(),\n\t\tst::groupCallMemberActiveIcon->c.greenF(),\n\t\tst::groupCallMemberActiveIcon->c.blueF(),\n\t\tst::groupCallMemberActiveIcon->c.alphaF() * outline));\n\n\tconst auto shadowHeight = st.shadowHeight * _factor;\n\tconst auto shadowAlpha = kShadowMaxAlpha / 255.f;\n\tprogram->setUniformValue(\"shadow\", QVector4D(\n\t\tshadowHeight,\n\t\tshown,\n\t\tshadowAlpha,\n\t\tfullscreen ? 0. : kBlurOpacity));\n\tprogram->setUniformValue(\"paused\", GLfloat(paused));\n\n\tf.glActiveTexture(_rgbaFrame ? GL_TEXTURE1 : GL_TEXTURE3);\n\ttileData.textures.bind(f, kFirstBlurPassTextureIndex);\n\tprogram->setUniformValue(\"b_texture\", GLint(_rgbaFrame ? 1 : 3));\n\tf.glActiveTexture(_rgbaFrame ? GL_TEXTURE2 : GL_TEXTURE5);\n\t_noiseTexture.bind(f, 0);\n\tprogram->setUniformValue(\"n_texture\", GLint(_rgbaFrame ? 2 : 5));\n\tprogram->setUniformValue(\n\t\t\"texelOffset\",\n\t\tGLfloat(1.f / blurSize.height()));\n\tGLint blurTexcoord = program->attributeLocation(\"b_texcoordIn\");\n\tf.glVertexAttribPointer(\n\t\tblurTexcoord,\n\t\t2,\n\t\tGL_FLOAT,\n\t\tGL_FALSE,\n\t\t2 * sizeof(GLfloat),\n\t\treinterpret_cast<const void*>(48 * sizeof(GLfloat)));\n\tf.glEnableVertexAttribArray(blurTexcoord);\n\tFillTexturedRectangle(f, program, 8);\n\tf.glDisableVertexAttribArray(blurTexcoord);\n\n\tconst auto pinVisible = _owner->wide()\n\t\t&& (pin.geometry.bottom() > y);\n\tconst auto nameVisible = (nameShift != fullNameShift);\n\tconst auto pausedVisible = (paused > 0.);\n\tif (!nameVisible && !pinVisible && !pausedVisible) {\n\t\treturn;\n\t}\n\n\tf.glEnable(GL_BLEND);\n\tf.glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);\n\tconst auto guard = gsl::finally([&] {\n\t\tf.glDisable(GL_BLEND);\n\t});\n\n\t_imageProgram->bind();\n\t_imageProgram->setUniformValue(\"viewport\", uniformViewport);\n\t_imageProgram->setUniformValue(\"s_texture\", GLint(0));\n\n\tf.glActiveTexture(GL_TEXTURE0);\n\t_buttons.bind(f);\n\n\t// Paused icon.\n\tif (pausedVisible) {\n\t\t_imageProgram->setUniformValue(\"g_opacity\", GLfloat(paused));\n\t\tFillTexturedRectangle(f, &*_imageProgram, 30);\n\t}\n\t_imageProgram->setUniformValue(\"g_opacity\", GLfloat(1.f));\n\n\t// Pin.\n\tif (pinVisible) {\n\t\tFillTexturedRectangle(f, &*_imageProgram, 14);\n\t\tFillTexturedRectangle(f, &*_imageProgram, 18);\n\t}\n\n\t// Mute.\n\tif (nameVisible && !muteRect.empty()) {\n\t\tFillTexturedRectangle(f, &*_imageProgram, 22);\n\t}\n\n\tif (!nameVisible && !pausedVisible) {\n\t\treturn;\n\t}\n\n\t_names.bind(f);\n\n\t// Name.\n\tif (nameVisible && !nameRect.empty()) {\n\t\tFillTexturedRectangle(f, &*_imageProgram, 26);\n\t}\n\n\t// Paused text.\n\tif (pausedVisible && _owner->wide()) {\n\t\t_imageProgram->setUniformValue(\"g_opacity\", GLfloat(paused));\n\t\tFillTexturedRectangle(f, &*_imageProgram, 34);\n\t}\n}\n\nvoid Viewport::RendererGL::prepareObjects(\n\t\tQOpenGLFunctions &f,\n\t\tTileData &tileData,\n\t\tQSize blurSize) {\n\tif (!tileData.textures.created()) {\n\t\ttileData.textures.ensureCreated(f); // All are GL_LINEAR, except..\n\t\ttileData.textures.bind(f, kScaleForBlurTextureIndex);\n\n\t\t// kScaleForBlurTextureIndex is attached to framebuffer 0,\n\t\t// and is used to draw to framebuffer 1 of the same size.\n\t\tf.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);\n\t\tf.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);\n\t}\n\ttileData.framebuffers.ensureCreated(f);\n\n\tif (tileData.textureBlurSize == blurSize) {\n\t\treturn;\n\t}\n\ttileData.textureBlurSize = blurSize;\n\n\tconst auto create = [&](int framebufferIndex, int index) {\n\t\ttileData.textures.bind(f, index);\n\t\tf.glTexImage2D(\n\t\t\tGL_TEXTURE_2D,\n\t\t\t0,\n\t\t\tGL_RGB,\n\t\t\tblurSize.width(),\n\t\t\tblurSize.height(),\n\t\t\t0,\n\t\t\tGL_RGB,\n\t\t\tGL_UNSIGNED_BYTE,\n\t\t\tnullptr);\n\n\t\ttileData.framebuffers.bind(f, framebufferIndex);\n\t\tf.glFramebufferTexture2D(\n\t\t\tGL_FRAMEBUFFER,\n\t\t\tGL_COLOR_ATTACHMENT0,\n\t\t\tGL_TEXTURE_2D,\n\t\t\ttileData.textures.id(index),\n\t\t\t0);\n\t};\n\tcreate(0, kScaleForBlurTextureIndex);\n\tcreate(1, kFirstBlurPassTextureIndex);\n}\n\nbool Viewport::RendererGL::isExpanded(\n\t\tnot_null<VideoTile*> tile,\n\t\tQSize unscaled,\n\t\tQSize tileSize) const {\n\treturn !tile->screencast()\n\t\t&& (!_owner->wide() || UseExpandForCamera(unscaled, tileSize));\n}\n\nfloat64 Viewport::RendererGL::countExpandRatio(\n\t\tnot_null<VideoTile*> tile,\n\t\tQSize unscaled,\n\t\tconst TileAnimation &animation) const {\n\tconst auto expandedFrom = isExpanded(tile, unscaled, animation.from);\n\tconst auto expandedTo = isExpanded(tile, unscaled, animation.to);\n\treturn (expandedFrom && expandedTo)\n\t\t? 1.\n\t\t: (!expandedFrom && !expandedTo)\n\t\t? 0.\n\t\t: expandedFrom\n\t\t? (1. - animation.ratio)\n\t\t: animation.ratio;\n}\n\nvoid Viewport::RendererGL::bindFrame(\n\t\tQOpenGLFunctions &f,\n\t\tconst Webrtc::FrameWithInfo &data,\n\t\tTileData &tileData,\n\t\tProgram &program) {\n\tconst auto imageIndex = _userpicFrame ? 0 : (data.index + 1);\n\tconst auto upload = (tileData.trackIndex != imageIndex);\n\ttileData.trackIndex = imageIndex;\n\tif (_rgbaFrame) {\n\t\tensureARGB32Program();\n\t\tprogram.argb32->bind();\n\t\tf.glActiveTexture(GL_TEXTURE0);\n\t\ttileData.textures.bind(f, 0);\n\t\tif (upload) {\n\t\t\tconst auto &image = _userpicFrame\n\t\t\t\t? tileData.userpicFrame\n\t\t\t\t: data.original;\n\t\t\tconst auto stride = image.bytesPerLine() / 4;\n\t\t\tconst auto data = image.constBits();\n\t\t\tuploadTexture(\n\t\t\t\tf,\n\t\t\t\tUi::GL::kFormatRGBA,\n\t\t\t\tUi::GL::kFormatRGBA,\n\t\t\t\timage.size(),\n\t\t\t\ttileData.rgbaSize,\n\t\t\t\tstride,\n\t\t\t\tdata);\n\t\t\ttileData.rgbaSize = image.size();\n\t\t\ttileData.textureSize = QSize();\n\t\t}\n\t\tprogram.argb32->setUniformValue(\"s_texture\", GLint(0));\n\t} else {\n\t\tconst auto yuv = data.yuv420;\n\t\tprogram.yuv420->bind();\n\t\tf.glActiveTexture(GL_TEXTURE0);\n\t\ttileData.textures.bind(f, 0);\n\t\tif (upload) {\n\t\t\tf.glPixelStorei(GL_UNPACK_ALIGNMENT, 1);\n\t\t\tuploadTexture(\n\t\t\t\tf,\n\t\t\t\tGL_ALPHA,\n\t\t\t\tGL_ALPHA,\n\t\t\t\tyuv->size,\n\t\t\t\ttileData.textureSize,\n\t\t\t\tyuv->y.stride,\n\t\t\t\tyuv->y.data);\n\t\t\ttileData.textureSize = yuv->size;\n\t\t\ttileData.rgbaSize = QSize();\n\t\t}\n\t\tf.glActiveTexture(GL_TEXTURE1);\n\t\ttileData.textures.bind(f, 1);\n\t\tif (upload) {\n\t\t\tuploadTexture(\n\t\t\t\tf,\n\t\t\t\tGL_ALPHA,\n\t\t\t\tGL_ALPHA,\n\t\t\t\tyuv->chromaSize,\n\t\t\t\ttileData.textureChromaSize,\n\t\t\t\tyuv->u.stride,\n\t\t\t\tyuv->u.data);\n\t\t}\n\t\tf.glActiveTexture(GL_TEXTURE2);\n\t\ttileData.textures.bind(f, 2);\n\t\tif (upload) {\n\t\t\tuploadTexture(\n\t\t\t\tf,\n\t\t\t\tGL_ALPHA,\n\t\t\t\tGL_ALPHA,\n\t\t\t\tyuv->chromaSize,\n\t\t\t\ttileData.textureChromaSize,\n\t\t\t\tyuv->v.stride,\n\t\t\t\tyuv->v.data);\n\t\t\ttileData.textureChromaSize = yuv->chromaSize;\n\t\t\tf.glPixelStorei(GL_UNPACK_ALIGNMENT, 4);\n\t\t}\n\t\tprogram.yuv420->setUniformValue(\"y_texture\", GLint(0));\n\t\tprogram.yuv420->setUniformValue(\"u_texture\", GLint(1));\n\t\tprogram.yuv420->setUniformValue(\"v_texture\", GLint(2));\n\t}\n}\n\nvoid Viewport::RendererGL::uploadTexture(\n\t\tQOpenGLFunctions &f,\n\t\tGLint internalformat,\n\t\tGLint format,\n\t\tQSize size,\n\t\tQSize hasSize,\n\t\tint stride,\n\t\tconst void *data) const {\n\tf.glPixelStorei(GL_UNPACK_ROW_LENGTH, stride);\n\tif (hasSize != size) {\n\t\tf.glTexImage2D(\n\t\t\tGL_TEXTURE_2D,\n\t\t\t0,\n\t\t\tinternalformat,\n\t\t\tsize.width(),\n\t\t\tsize.height(),\n\t\t\t0,\n\t\t\tformat,\n\t\t\tGL_UNSIGNED_BYTE,\n\t\t\tdata);\n\t} else {\n\t\tf.glTexSubImage2D(\n\t\t\tGL_TEXTURE_2D,\n\t\t\t0,\n\t\t\t0,\n\t\t\t0,\n\t\t\tsize.width(),\n\t\t\tsize.height(),\n\t\t\tformat,\n\t\t\tGL_UNSIGNED_BYTE,\n\t\t\tdata);\n\t}\n\tf.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);\n}\n\nvoid Viewport::RendererGL::drawDownscalePass(\n\t\tQOpenGLFunctions &f,\n\t\tTileData &tileData) {\n\ttileData.framebuffers.bind(f, 0);\n\n\tconst auto program = _rgbaFrame\n\t\t? &*_downscaleProgram.argb32\n\t\t: &*_downscaleProgram.yuv420;\n\n\tprogram->bind();\n\tFillTexturedRectangle(f, program);\n}\n\nvoid Viewport::RendererGL::drawFirstBlurPass(\n\t\tQOpenGLFunctions &f,\n\t\tTileData &tileData,\n\t\tQSize blurSize) {\n\ttileData.framebuffers.bind(f, 1);\n\n\t_blurProgram->bind();\n\tf.glActiveTexture(GL_TEXTURE0);\n\ttileData.textures.bind(f, kScaleForBlurTextureIndex);\n\n\t_blurProgram->setUniformValue(\"b_texture\", GLint(0));\n\t_blurProgram->setUniformValue(\n\t\t\"texelOffset\",\n\t\tGLfloat(1.f / blurSize.width()));\n\n\tFillTexturedRectangle(f, &*_blurProgram, 4);\n}\n\nRect Viewport::RendererGL::transformRect(const Rect &raster) const {\n\treturn TransformRect(raster, _viewport, _factor);\n}\n\nRect Viewport::RendererGL::transformRect(const QRect &raster) const {\n\treturn TransformRect(Rect(raster), _viewport, _factor);\n}\n\nvoid Viewport::RendererGL::ensureButtonsImage() {\n\tif (_buttons) {\n\t\treturn;\n\t}\n\tconst auto pinOnSize = VideoTile::PinInnerSize(true);\n\tconst auto pinOffSize = VideoTile::PinInnerSize(false);\n\tconst auto backSize = VideoTile::BackInnerSize();\n\tconst auto muteSize = st::groupCallVideoCrossLine.icon.size();\n\tconst auto pausedSize = st::groupCallPaused.size();\n\n\tconst auto fullSize = QSize(\n\t\tstd::max({\n\t\t\tpinOnSize.width(),\n\t\t\tpinOffSize.width(),\n\t\t\tbackSize.width(),\n\t\t\t2 * muteSize.width(),\n\t\t\tpausedSize.width(),\n\t\t}),\n\t\t(pinOnSize.height()\n\t\t\t+ pinOffSize.height()\n\t\t\t+ backSize.height()\n\t\t\t+ muteSize.height()\n\t\t\t+ pausedSize.height()));\n\tconst auto imageSize = fullSize * _ifactor;\n\tauto image = _buttons.takeImage();\n\tif (image.size() != imageSize) {\n\t\timage = QImage(imageSize, QImage::Format_ARGB32_Premultiplied);\n\t}\n\timage.fill(Qt::transparent);\n\timage.setDevicePixelRatio(_ifactor);\n\t{\n\t\tauto p = Painter(&image);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\t_pinOn = QRect(QPoint(), pinOnSize * _ifactor);\n\t\tVideoTile::PaintPinButton(\n\t\t\tp,\n\t\t\ttrue,\n\t\t\t0,\n\t\t\t0,\n\t\t\tfullSize.width(),\n\t\t\t&_pinBackground,\n\t\t\t&_pinIcon);\n\n\t\tconst auto pinOffTop = pinOnSize.height();\n\t\t_pinOff = QRect(\n\t\t\tQPoint(0, pinOffTop) * _ifactor,\n\t\t\tpinOffSize * _ifactor);\n\t\tVideoTile::PaintPinButton(\n\t\t\tp,\n\t\t\tfalse,\n\t\t\t0,\n\t\t\tpinOnSize.height(),\n\t\t\tfullSize.width(),\n\t\t\t&_pinBackground,\n\t\t\t&_pinIcon);\n\n\t\tconst auto backTop = pinOffTop + pinOffSize.height();\n\t\t_back = QRect(QPoint(0, backTop) * _ifactor, backSize * _ifactor);\n\t\tVideoTile::PaintBackButton(\n\t\t\tp,\n\t\t\t0,\n\t\t\tpinOnSize.height() + pinOffSize.height(),\n\t\t\tfullSize.width(),\n\t\t\t&_pinBackground);\n\n\t\tconst auto muteTop = backTop + backSize.height();\n\t\t_muteOn = QRect(QPoint(0, muteTop) * _ifactor, muteSize * _ifactor);\n\t\t_muteIcon.paint(p, { 0, muteTop }, 1.);\n\n\t\t_muteOff = QRect(\n\t\t\tQPoint(muteSize.width(), muteTop) * _ifactor,\n\t\t\tmuteSize * _ifactor);\n\t\t_muteIcon.paint(p, { muteSize.width(), muteTop }, 0.);\n\n\t\tconst auto pausedTop = muteTop + muteSize.height();\n\t\t_paused = QRect(\n\t\t\tQPoint(0, pausedTop) * _ifactor,\n\t\t\tpausedSize * _ifactor);\n\t\tst::groupCallPaused.paint(p, 0, pausedTop, fullSize.width());\n\t}\n\t_buttons.setImage(std::move(image));\n}\n\nvoid Viewport::RendererGL::validateDatas() {\n\tconst auto &tiles = _owner->_tiles;\n\tconst auto &st = st::groupCallVideoTile;\n\tconst auto count = int(tiles.size());\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto nameHeight = st::semiboldFont->height * factor;\n\tconst auto pausedText = tr::lng_group_call_video_paused(tr::now);\n\tconst auto pausedBottom = nameHeight;\n\tconst auto pausedWidth = st::semiboldFont->width(pausedText) * factor;\n\tstruct Request {\n\t\tint index = 0;\n\t\tbool updating = false;\n\t};\n\tauto requests = std::vector<Request>();\n\tauto available = std::max(_names.image().width(), pausedWidth);\n\tfor (auto &data : _tileData) {\n\t\tdata.stale = true;\n\t}\n\t_tileDataIndices.resize(count);\n\tconst auto nameWidth = [&](int i) {\n\t\tconst auto row = tiles[i]->row();\n\t\tconst auto hasWidth = tiles[i]->geometry().width()\n\t\t\t- st.iconPosition.x()\n\t\t\t- st::groupCallVideoCrossLine.icon.width()\n\t\t\t- st.namePosition.x();\n\t\tif (hasWidth < 1) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn std::clamp(row->name().maxWidth(), 1, hasWidth) * factor;\n\t};\n\tfor (auto i = 0; i != count; ++i) {\n\t\ttiles[i]->row()->lazyInitialize(st::groupCallMembersListItem);\n\t\tconst auto width = nameWidth(i);\n\t\tif (width > available) {\n\t\t\tavailable = width;\n\t\t}\n\t\tconst auto id = quintptr(tiles[i]->track().get());\n\t\tconst auto j = ranges::find(_tileData, id, &TileData::id);\n\t\tif (j != end(_tileData)) {\n\t\t\tj->stale = false;\n\t\t\tconst auto index = (j - begin(_tileData));\n\t\t\t_tileDataIndices[i] = index;\n\t\t\tconst auto peer = tiles[i]->peer();\n\t\t\tif ((j->peer != peer)\n\t\t\t\t|| (j->nameVersion != peer->nameVersion())\n\t\t\t\t|| (j->nameRect.width() != width)) {\n\t\t\t\tconst auto nameTop = pausedBottom + index * nameHeight;\n\t\t\t\tj->nameRect = QRect(0, nameTop, width, nameHeight);\n\t\t\t\trequests.push_back({ .index = i, .updating = true });\n\t\t\t}\n\t\t} else {\n\t\t\t_tileDataIndices[i] = -1;\n\t\t\trequests.push_back({ .index = i, .updating = false });\n\t\t}\n\t}\n\tif (requests.empty()) {\n\t\treturn;\n\t}\n\tauto maybeStaleAfter = begin(_tileData);\n\tauto maybeStaleEnd = end(_tileData);\n\tfor (auto &request : requests) {\n\t\tconst auto i = request.index;\n\t\tif (_tileDataIndices[i] >= 0) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto id = quintptr(tiles[i]->track().get());\n\t\tconst auto peer = tiles[i]->peer();\n\t\tconst auto paused = (tiles[i]->track()->state()\n\t\t\t== Webrtc::VideoState::Paused);\n\t\tauto index = int(_tileData.size());\n\t\tmaybeStaleAfter = ranges::find(\n\t\t\tmaybeStaleAfter,\n\t\t\tmaybeStaleEnd,\n\t\t\ttrue,\n\t\t\t&TileData::stale);\n\t\tif (maybeStaleAfter != maybeStaleEnd) {\n\t\t\tindex = (maybeStaleAfter - begin(_tileData));\n\t\t\tmaybeStaleAfter->id = id;\n\t\t\tmaybeStaleAfter->peer = peer;\n\t\t\tmaybeStaleAfter->stale = false;\n\t\t\tmaybeStaleAfter->pause = paused;\n\t\t\tmaybeStaleAfter->paused.stop();\n\t\t\trequest.updating = true;\n\t\t} else {\n\t\t\t// This invalidates maybeStale*, but they're already equal.\n\t\t\t_tileData.push_back({\n\t\t\t\t.id = id,\n\t\t\t\t.peer = peer,\n\t\t\t\t.pause = paused,\n\t\t\t});\n\t\t}\n\t\tconst auto nameTop = pausedBottom + index * nameHeight;\n\t\t_tileData[index].nameVersion = peer->nameVersion();\n\t\t_tileData[index].nameRect = QRect(\n\t\t\t0,\n\t\t\tnameTop,\n\t\t\tnameWidth(i),\n\t\t\tnameHeight);\n\t\t_tileDataIndices[i] = index;\n\t}\n\tauto image = _names.takeImage();\n\tconst auto imageSize = QSize(\n\t\tavailable,\n\t\tpausedBottom + _tileData.size() * nameHeight);\n\tconst auto allocate = (image.size() != imageSize);\n\tauto paintToImage = allocate\n\t\t? QImage(imageSize, QImage::Format_ARGB32_Premultiplied)\n\t\t: base::take(image);\n\tpaintToImage.setDevicePixelRatio(factor);\n\tif (allocate && image.isNull()) {\n\t\tpaintToImage.fill(Qt::transparent);\n\t}\n\t{\n\t\tauto p = Painter(&paintToImage);\n\t\tp.setPen(st::groupCallVideoTextFg);\n\t\tif (!image.isNull()) {\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\t\tp.drawImage(0, 0, image);\n\t\t\tif (paintToImage.width() > image.width()) {\n\t\t\t\tp.fillRect(\n\t\t\t\t\timage.width() / factor,\n\t\t\t\t\t0,\n\t\t\t\t\t(paintToImage.width() - image.width()) / factor,\n\t\t\t\t\timage.height() / factor,\n\t\t\t\t\tQt::transparent);\n\t\t\t}\n\t\t\tif (paintToImage.height() > image.height()) {\n\t\t\t\tp.fillRect(\n\t\t\t\t\t0,\n\t\t\t\t\timage.height() / factor,\n\t\t\t\t\tpaintToImage.width() / factor,\n\t\t\t\t\t(paintToImage.height() - image.height()) / factor,\n\t\t\t\t\tQt::transparent);\n\t\t\t}\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\t} else if (allocate) {\n\t\t\tp.setFont(st::semiboldFont);\n\t\t\tp.drawText(0, st::semiboldFont->ascent, pausedText);\n\t\t\t_pausedTextRect = QRect(0, 0, pausedWidth, nameHeight);\n\t\t}\n\t\tfor (const auto &request : requests) {\n\t\t\tconst auto i = request.index;\n\t\t\tconst auto &data = _tileData[_tileDataIndices[i]];\n\t\t\tif (data.nameRect.isEmpty()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto row = tiles[i]->row();\n\t\t\tif (request.updating) {\n\t\t\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\t\t\tp.fillRect(\n\t\t\t\t\t0,\n\t\t\t\t\tdata.nameRect.y() / factor,\n\t\t\t\t\tpaintToImage.width() / factor,\n\t\t\t\t\tnameHeight / factor,\n\t\t\t\t\tQt::transparent);\n\t\t\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\t\t}\n\t\t\trow->name().drawLeftElided(\n\t\t\t\tp,\n\t\t\t\t0,\n\t\t\t\tdata.nameRect.y() / factor,\n\t\t\t\tdata.nameRect.width() / factor,\n\t\t\t\tpaintToImage.width() / factor);\n\t\t}\n\t}\n\t_names.setImage(std::move(paintToImage));\n}\n\nvoid Viewport::RendererGL::validateNoiseTexture(\n\t\tQOpenGLFunctions &f,\n\t\tGLuint defaultFramebufferObject) {\n\tif (_noiseTexture.created()) {\n\t\treturn;\n\t}\n\t_noiseTexture.ensureCreated(f, GL_NEAREST, GL_REPEAT);\n\t_noiseTexture.bind(f, 0);\n\n\t// Rendering to GL_ALPHA is not supported.\n\tf.glTexImage2D(\n\t\tGL_TEXTURE_2D,\n\t\t0,\n\t\tGL_R8,\n\t\tkNoiseTextureSize,\n\t\tkNoiseTextureSize,\n\t\t0,\n\t\tGL_RED,\n\t\tGL_UNSIGNED_BYTE,\n\t\tnullptr);\n\tif (f.glGetError() != GL_NO_ERROR) {\n\t\t// Direct3D 9 doesn't support GL_R8 textures.\n\t\tf.glTexImage2D(\n\t\t\tGL_TEXTURE_2D,\n\t\t\t0,\n\t\t\tGL_RGB,\n\t\t\tkNoiseTextureSize,\n\t\t\tkNoiseTextureSize,\n\t\t\t0,\n\t\t\tGL_RGB,\n\t\t\tGL_UNSIGNED_BYTE,\n\t\t\tnullptr);\n\t}\n\n\t_noiseFramebuffer.ensureCreated(f);\n\t_noiseFramebuffer.bind(f, 0);\n\n\tf.glFramebufferTexture2D(\n\t\tGL_FRAMEBUFFER,\n\t\tGL_COLOR_ATTACHMENT0,\n\t\tGL_TEXTURE_2D,\n\t\t_noiseTexture.id(0),\n\t\t0);\n\n\tf.glViewport(0, 0, kNoiseTextureSize, kNoiseTextureSize);\n\n\tconst GLfloat coords[] = {\n\t\t-1, -1,\n\t\t-1,  1,\n\t\t 1,  1,\n\t\t 1, -1,\n\t};\n\tauto buffer = QOpenGLBuffer();\n\tbuffer.setUsagePattern(QOpenGLBuffer::StaticDraw);\n\tbuffer.create();\n\tbuffer.bind();\n\tbuffer.allocate(coords, sizeof(coords));\n\n\tauto program = QOpenGLShaderProgram();\n\tLinkProgram(\n\t\t&program,\n\t\tVertexShader({}),\n\t\tFragmentShader({ FragmentGenerateNoise() }));\n\tprogram.bind();\n\n\tGLint position = program.attributeLocation(\"position\");\n\tf.glVertexAttribPointer(\n\t\tposition,\n\t\t2,\n\t\tGL_FLOAT,\n\t\tGL_FALSE,\n\t\t2 * sizeof(GLfloat),\n\t\tnullptr);\n\tf.glEnableVertexAttribArray(position);\n\n\tf.glDrawArrays(GL_TRIANGLE_FAN, 0, 4);\n\n\tf.glDisableVertexAttribArray(position);\n\n\tf.glUseProgram(0);\n}\n\nvoid Viewport::RendererGL::validateOutlineAnimation(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &data) {\n\tconst auto outline = tile->row()->speaking();\n\tif (data.outline == outline) {\n\t\treturn;\n\t}\n\tdata.outline = outline;\n\tdata.outlined.start(\n\t\t[=] { _owner->widget()->update(); },\n\t\toutline ? 0. : 1.,\n\t\toutline ? 1. : 0.,\n\t\tst::fadeWrapDuration);\n}\n\nvoid Viewport::RendererGL::validatePausedAnimation(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &data) {\n\tconst auto paused = (_userpicFrame\n\t\t&& tile->track()->frameSize().isEmpty())\n\t\t|| (tile->track()->state() == Webrtc::VideoState::Paused);\n\tif (data.pause == paused) {\n\t\treturn;\n\t}\n\tdata.pause = paused;\n\tdata.paused.start(\n\t\t[=] { _owner->widget()->update(); },\n\t\tpaused ? 0. : 1.,\n\t\tpaused ? 1. : 0.,\n\t\tst::fadeWrapDuration);\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"calls/group/calls_group_viewport.h\"\n#include \"ui/round_rect.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/cross_line.h\"\n#include \"ui/gl/gl_primitives.h\"\n#include \"ui/gl/gl_surface.h\"\n#include \"ui/gl/gl_image.h\"\n\n#include <QOpenGLBuffer>\n#include <QOpenGLShaderProgram>\n\nnamespace Webrtc {\nstruct FrameWithInfo;\n} // namespace Webrtc\n\nnamespace Calls::Group {\n\nclass Viewport::RendererGL final : public Ui::GL::Renderer {\npublic:\n\texplicit RendererGL(not_null<Viewport*> owner);\n\n\tvoid init(QOpenGLFunctions &f) override;\n\n\tvoid deinit(QOpenGLFunctions *f) override;\n\n\tvoid paint(\n\t\tnot_null<QOpenGLWidget*> widget,\n\t\tQOpenGLFunctions &f) override;\n\n\tstd::optional<QColor> clearColor() override;\n\nprivate:\n\tstruct TileData {\n\t\tquintptr id = 0;\n\t\tnot_null<PeerData*> peer;\n\t\tUi::GL::Textures<5> textures;\n\t\tUi::GL::Framebuffers<2> framebuffers;\n\t\tUi::Animations::Simple outlined;\n\t\tUi::Animations::Simple paused;\n\t\tQImage userpicFrame;\n\t\tQRect nameRect;\n\t\tint nameVersion = 0;\n\t\tmutable int trackIndex = -1;\n\t\tmutable QSize rgbaSize;\n\t\tmutable QSize textureSize;\n\t\tmutable QSize textureChromaSize;\n\t\tmutable QSize textureBlurSize;\n\t\tbool stale = false;\n\t\tbool pause = false;\n\t\tbool outline = false;\n\t};\n\tstruct Program {\n\t\tstd::optional<QOpenGLShaderProgram> argb32;\n\t\tstd::optional<QOpenGLShaderProgram> yuv420;\n\t};\n\n\tvoid setDefaultViewport(QOpenGLFunctions &f);\n\tvoid paintTile(\n\t\tQOpenGLFunctions &f,\n\t\tGLuint defaultFramebufferObject,\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &nameData);\n\t[[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const;\n\t[[nodiscard]] Ui::GL::Rect transformRect(\n\t\tconst Ui::GL::Rect &raster) const;\n\n\tvoid ensureARGB32Program();\n\tvoid ensureButtonsImage();\n\tvoid prepareObjects(\n\t\tQOpenGLFunctions &f,\n\t\tTileData &tileData,\n\t\tQSize blurSize);\n\tvoid bindFrame(\n\t\tQOpenGLFunctions &f,\n\t\tconst Webrtc::FrameWithInfo &data,\n\t\tTileData &tileData,\n\t\tProgram &program);\n\tvoid drawDownscalePass(\n\t\tQOpenGLFunctions &f,\n\t\tTileData &tileData);\n\tvoid drawFirstBlurPass(\n\t\tQOpenGLFunctions &f,\n\t\tTileData &tileData,\n\t\tQSize blurSize);\n\tvoid validateDatas();\n\tvoid validateNoiseTexture(\n\t\tQOpenGLFunctions &f,\n\t\tGLuint defaultFramebufferObject);\n\tvoid validateOutlineAnimation(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &data);\n\tvoid validatePausedAnimation(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &data);\n\tvoid validateUserpicFrame(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &tileData);\n\n\tvoid uploadTexture(\n\t\tQOpenGLFunctions &f,\n\t\tGLint internalformat,\n\t\tGLint format,\n\t\tQSize size,\n\t\tQSize hasSize,\n\t\tint stride,\n\t\tconst void *data) const;\n\n\t[[nodiscard]] bool isExpanded(\n\t\tnot_null<VideoTile*> tile,\n\t\tQSize unscaled,\n\t\tQSize tileSize) const;\n\t[[nodiscard]] float64 countExpandRatio(\n\t\tnot_null<VideoTile*> tile,\n\t\tQSize unscaled,\n\t\tconst TileAnimation &animation) const;\n\n\tconst not_null<Viewport*> _owner;\n\n\tGLfloat _factor = 1.;\n\tint _ifactor = 1;\n\tQSize _viewport;\n\tbool _rgbaFrame = false;\n\tbool _userpicFrame;\n\tstd::optional<QOpenGLBuffer> _frameBuffer;\n\tProgram _downscaleProgram;\n\tstd::optional<QOpenGLShaderProgram> _blurProgram;\n\tProgram _frameProgram;\n\tstd::optional<QOpenGLShaderProgram> _imageProgram;\n\tUi::GL::Textures<1> _noiseTexture;\n\tUi::GL::Framebuffers<1> _noiseFramebuffer;\n\tQOpenGLShader *_downscaleVertexShader = nullptr;\n\tQOpenGLShader *_frameVertexShader = nullptr;\n\n\tUi::GL::Image _buttons;\n\tQRect _pinOn;\n\tQRect _pinOff;\n\tQRect _back;\n\tQRect _muteOn;\n\tQRect _muteOff;\n\tQRect _paused;\n\n\tUi::GL::Image _names;\n\tQRect _pausedTextRect;\n\tstd::vector<TileData> _tileData;\n\tstd::vector<int> _tileDataIndices;\n\n\tUi::CrossLineAnimation _pinIcon;\n\tUi::CrossLineAnimation _muteIcon;\n\n\tUi::RoundRect _pinBackground;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_viewport_raster.h\"\n\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/group/calls_group_viewport_tile.h\"\n#include \"calls/group/calls_group_members_row.h\"\n#include \"data/data_peer.h\"\n#include \"media/view/media_view_pip.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/palette.h\"\n\nnamespace Calls::Group {\nnamespace {\n\nconstexpr auto kBlurRadius = 15;\n\n} // namespace\n\nViewport::RendererSW::RendererSW(not_null<Viewport*> owner)\n: _owner(owner)\n, _pinIcon(st::groupCallVideoTile.pin)\n, _pinBackground(\n\t(st::groupCallVideoTile.pinPadding.top()\n\t\t+ st::groupCallVideoTile.pin.icon.height()\n\t\t+ st::groupCallVideoTile.pinPadding.bottom()) / 2,\n\tst::radialBg) {\n}\n\nvoid Viewport::RendererSW::paintFallback(\n\t\tPainter &p,\n\t\tconst QRegion &clip,\n\t\tUi::GL::Backend backend) {\n\tauto bg = clip;\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto bounding = clip.boundingRect();\n\tfor (auto &[tile, tileData] : _tileData) {\n\t\ttileData.stale = true;\n\t}\n\tfor (const auto &tile : _owner->_tiles) {\n\t\tif (!tile->visible()) {\n\t\t\tcontinue;\n\t\t}\n\t\tpaintTile(p, tile.get(), bounding, bg);\n\t}\n\tif (_owner->borrowedOrigin().isNull()) {\n\t\tconst auto fullscreen = _owner->_fullscreen;\n\t\tconst auto color = fullscreen\n\t\t\t? QColor(0, 0, 0)\n\t\t\t: st::groupCallBg->c;\n\t\tfor (const auto &rect : bg) {\n\t\t\tp.fillRect(rect, color);\n\t\t}\n\t}\n\tfor (auto i = _tileData.begin(); i != _tileData.end();) {\n\t\tif (i->second.stale) {\n\t\t\ti = _tileData.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n}\n\nvoid Viewport::RendererSW::validateUserpicFrame(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &data) {\n\tif (!_userpicFrame) {\n\t\tdata.userpicFrame = QImage();\n\t\treturn;\n\t} else if (!data.userpicFrame.isNull()) {\n\t\treturn;\n\t}\n\tconst auto size = tile->trackOrUserpicSize();\n\tdata.userpicFrame = Images::BlurLargeImage(\n\t\tPeerData::GenerateUserpicImage(\n\t\t\ttile->peer(),\n\t\t\ttile->row()->ensureUserpicView(),\n\t\t\tsize.width(),\n\t\t\t0),\n\t\tkBlurRadius);\n}\n\nvoid Viewport::RendererSW::paintTile(\n\t\tPainter &p,\n\t\tnot_null<VideoTile*> tile,\n\t\tconst QRect &clip,\n\t\tQRegion &bg) {\n\tconst auto track = tile->track();\n\tconst auto markGuard = gsl::finally([&] {\n\t\ttile->track()->markFrameShown();\n\t});\n\tconst auto data = track->frameWithInfo(true);\n\tauto &tileData = _tileData[tile];\n\ttileData.stale = false;\n\t_userpicFrame = (data.format == Webrtc::FrameFormat::None);\n\t_pausedFrame = (track->state() == Webrtc::VideoState::Paused);\n\tvalidateUserpicFrame(tile, tileData);\n\tif (_userpicFrame || !_pausedFrame) {\n\t\ttileData.blurredFrame = QImage();\n\t} else if (tileData.blurredFrame.isNull()) {\n\t\ttileData.blurredFrame = Images::BlurLargeImage(\n\t\t\tdata.original.scaled(\n\t\t\t\tVideoTile::PausedVideoSize(),\n\t\t\t\tQt::KeepAspectRatio).mirrored(tile->mirror(), false),\n\t\t\tkBlurRadius);\n\t}\n\tconst auto &image = _userpicFrame\n\t\t? tileData.userpicFrame\n\t\t: _pausedFrame\n\t\t? tileData.blurredFrame\n\t\t: data.original.mirrored(tile->mirror(), false);\n\tconst auto frameRotation = _userpicFrame ? 0 : data.rotation;\n\tAssert(!image.isNull());\n\n\tconst auto background = _owner->_fullscreen\n\t\t? QColor(0, 0, 0)\n\t\t: st::groupCallMembersBg->c;\n\tconst auto fill = [&](QRect rect) {\n\t\tconst auto intersected = rect.intersected(clip);\n\t\tif (!intersected.isEmpty()) {\n\t\t\tp.fillRect(intersected, background);\n\t\t\tbg -= intersected;\n\t\t}\n\t};\n\n\tusing namespace Media::View;\n\tconst auto geometry = tile->geometry().translated(\n\t\t_owner->borrowedOrigin());\n\tconst auto x = geometry.x();\n\tconst auto y = geometry.y();\n\tconst auto width = geometry.width();\n\tconst auto height = geometry.height();\n\tconst auto scaled = FlipSizeByRotation(\n\t\timage.size(),\n\t\tframeRotation\n\t).scaled(QSize(width, height), Qt::KeepAspectRatio);\n\tconst auto left = (width - scaled.width()) / 2;\n\tconst auto top = (height - scaled.height()) / 2;\n\tconst auto target = QRect(QPoint(x + left, y + top), scaled);\n\tif (UsePainterRotation(frameRotation)) {\n\t\tif (frameRotation) {\n\t\t\tp.save();\n\t\t\tp.rotate(frameRotation);\n\t\t}\n\t\tp.drawImage(RotatedRect(target, frameRotation), image);\n\t\tif (frameRotation) {\n\t\t\tp.restore();\n\t\t}\n\t} else if (frameRotation) {\n\t\tp.drawImage(target, RotateFrameImage(image, frameRotation));\n\t} else {\n\t\tp.drawImage(target, image);\n\t}\n\tbg -= target;\n\n\tif (left > 0) {\n\t\tfill({ x, y, left, height });\n\t}\n\tif (const auto right = left + scaled.width(); right < width) {\n\t\tfill({ x + right, y, width - right, height });\n\t}\n\tif (top > 0) {\n\t\tfill({ x, y, width, top });\n\t}\n\tif (const auto bottom = top + scaled.height(); bottom < height) {\n\t\tfill({ x, y + bottom, width, height - bottom });\n\t}\n\n\tpaintTileControls(p, x, y, width, height, tile);\n\tpaintTileOutline(p, x, y, width, height, tile);\n}\n\nvoid Viewport::RendererSW::paintTileOutline(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint width,\n\t\tint height,\n\t\tnot_null<VideoTile*> tile) {\n\tif (!tile->row()->speaking()) {\n\t\treturn;\n\t}\n\tconst auto outline = st::groupCallOutline;\n\tconst auto &color = st::groupCallMemberActiveIcon;\n\tp.setPen(Qt::NoPen);\n\tp.fillRect(x, y, outline, height - outline, color);\n\tp.fillRect(x + outline, y, width - outline, outline, color);\n\tp.fillRect(\n\t\tx + width - outline,\n\t\ty + outline,\n\t\toutline,\n\t\theight - outline,\n\t\tcolor);\n\tp.fillRect(x, y + height - outline, width - outline, outline, color);\n}\n\nvoid Viewport::RendererSW::paintTileControls(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint width,\n\t\tint height,\n\t\tnot_null<VideoTile*> tile) {\n\tp.setClipRect(x, y, width, height);\n\tconst auto guard = gsl::finally([&] { p.setClipping(false); });\n\n\tconst auto wide = _owner->wide();\n\tif (wide) {\n\t\t// Pin.\n\t\tconst auto pinInner = tile->pinInner();\n\t\tVideoTile::PaintPinButton(\n\t\t\tp,\n\t\t\ttile->pinned(),\n\t\t\tx + pinInner.x(),\n\t\t\ty + pinInner.y(),\n\t\t\t_owner->widget()->width(),\n\t\t\t&_pinBackground,\n\t\t\t&_pinIcon);\n\n\t\t// Back.\n\t\tconst auto backInner = tile->backInner();\n\t\tVideoTile::PaintBackButton(\n\t\t\tp,\n\t\t\tx + backInner.x(),\n\t\t\ty + backInner.y(),\n\t\t\t_owner->widget()->width(),\n\t\t\t&_pinBackground);\n\t}\n\n\tconst auto &st = st::groupCallVideoTile;\n\tconst auto nameTop = y + (height\n\t\t- st.namePosition.y()\n\t\t- st::semiboldFont->height);\n\n\tif (_pausedFrame) {\n\t\tp.fillRect(x, y, width, height, QColor(0, 0, 0, kShadowMaxAlpha));\n\n\t\tconst auto middle = (st::groupCallVideoPlaceholderHeight\n\t\t\t- st::groupCallPaused.height()) / 2;\n\t\tconst auto pausedSpace = (nameTop - y)\n\t\t\t- st::groupCallPaused.height()\n\t\t\t- st::semiboldFont->height;\n\t\tconst auto pauseIconSkip = middle - st::groupCallVideoPlaceholderIconTop;\n\t\tconst auto pauseTextSkip = st::groupCallVideoPlaceholderTextTop\n\t\t\t- st::groupCallVideoPlaceholderIconTop;\n\t\tconst auto pauseIconTop = !_owner->wide()\n\t\t\t? (y + (height - st::groupCallPaused.height()) / 2)\n\t\t\t: (pausedSpace < 3 * st::semiboldFont->height)\n\t\t\t? (pausedSpace / 3)\n\t\t\t: std::min(\n\t\t\t\ty + (height / 2) - pauseIconSkip,\n\t\t\t\t(nameTop\n\t\t\t\t\t- st::semiboldFont->height * 3\n\t\t\t\t\t- st::groupCallPaused.height()));\n\t\tconst auto pauseTextTop = (pausedSpace < 3 * st::semiboldFont->height)\n\t\t\t? (nameTop - (pausedSpace / 3) - st::semiboldFont->height)\n\t\t\t: std::min(\n\t\t\t\tpauseIconTop + pauseTextSkip,\n\t\t\t\tnameTop - st::semiboldFont->height * 2);\n\n\t\tst::groupCallPaused.paint(\n\t\t\tp,\n\t\t\tx + (width - st::groupCallPaused.width()) / 2,\n\t\t\tpauseIconTop,\n\t\t\twidth);\n\t\tif (_owner->wide()) {\n\t\t\tp.drawText(\n\t\t\t\tQRect(x, pauseTextTop, width, y + height - pauseTextTop),\n\t\t\t\ttr::lng_group_call_video_paused(tr::now),\n\t\t\t\tstyle::al_top);\n\t\t}\n\t}\n\n\tconst auto shown = _owner->_controlsShownRatio;\n\tif (shown == 0.) {\n\t\treturn;\n\t}\n\n\tconst auto fullShift = st.namePosition.y() + st::normalFont->height;\n\tconst auto shift = anim::interpolate(fullShift, 0, shown);\n\n\t// Shadow.\n\tif (_shadow.isNull()) {\n\t\t_shadow = Images::GenerateShadow(\n\t\t\tst.shadowHeight,\n\t\t\t0,\n\t\t\tkShadowMaxAlpha);\n\t}\n\tconst auto shadowRect = QRect(\n\t\tx,\n\t\ty + (height - anim::interpolate(0, st.shadowHeight, shown)),\n\t\twidth,\n\t\tst.shadowHeight);\n\tconst auto shadowFill = shadowRect.intersected({ x, y, width, height });\n\tif (shadowFill.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto factor = style::DevicePixelRatio();\n\tif (!_pausedFrame) {\n\t\tp.drawImage(\n\t\t\tshadowFill,\n\t\t\t_shadow,\n\t\t\tQRect(\n\t\t\t\t0,\n\t\t\t\t(shadowFill.y() - shadowRect.y()) * factor,\n\t\t\t\t_shadow.width(),\n\t\t\t\tshadowFill.height() * factor));\n\t}\n\tconst auto row = tile->row();\n\trow->lazyInitialize(st::groupCallMembersListItem);\n\n\t// Mute.\n\tconst auto &icon = st::groupCallVideoCrossLine.icon;\n\tconst auto iconLeft = x + width - st.iconPosition.x() - icon.width();\n\tconst auto iconTop = y + (height\n\t\t- st.iconPosition.y()\n\t\t- icon.height()\n\t\t+ shift);\n\trow->paintMuteIcon(\n\t\tp,\n\t\t{ iconLeft, iconTop, icon.width(), icon.height() },\n\t\tMembersRowStyle::Video);\n\n\t// Name.\n\tp.setPen(st::groupCallVideoTextFg);\n\tconst auto hasWidth = width\n\t\t- st.iconPosition.x() - icon.width()\n\t\t- st.namePosition.x();\n\tconst auto nameLeft = x + st.namePosition.x();\n\trow->name().drawLeftElided(\n\t\tp,\n\t\tnameLeft,\n\t\tnameTop + shift,\n\t\thasWidth,\n\t\twidth);\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_viewport_raster.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"calls/group/calls_group_viewport.h\"\n#include \"ui/round_rect.h\"\n#include \"ui/effects/cross_line.h\"\n#include \"ui/gl/gl_surface.h\"\n#include \"ui/text/text.h\"\n\nnamespace Calls::Group {\n\nclass Viewport::RendererSW final : public Ui::GL::Renderer {\npublic:\n\texplicit RendererSW(not_null<Viewport*> owner);\n\n\tvoid paintFallback(\n\t\tPainter &p,\n\t\tconst QRegion &clip,\n\t\tUi::GL::Backend backend) override;\n\nprivate:\n\tstruct TileData {\n\t\tQImage userpicFrame;\n\t\tQImage blurredFrame;\n\t\tbool stale = false;\n\t};\n\tvoid paintTile(\n\t\tPainter &p,\n\t\tnot_null<VideoTile*> tile,\n\t\tconst QRect &clip,\n\t\tQRegion &bg);\n\tvoid paintTileOutline(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint width,\n\t\tint height,\n\t\tnot_null<VideoTile*> tile);\n\tvoid paintTileControls(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint width,\n\t\tint height,\n\t\tnot_null<VideoTile*> tile);\n\tvoid validateUserpicFrame(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &data);\n\n\tconst not_null<Viewport*> _owner;\n\n\tQImage _shadow;\n\tbool _userpicFrame = false;\n\tbool _pausedFrame = false;\n\tbase::flat_map<not_null<VideoTile*>, TileData> _tileData;\n\tUi::CrossLineAnimation _pinIcon;\n\tUi::RoundRect _pinBackground;\n\n};\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_viewport_rhi.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_viewport_rhi.h\"\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\n#include \"calls/group/calls_group_viewport_tile.h\"\n#include \"calls/group/calls_group_members_row.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"media/view/media_view_pip.h\"\n#include \"media/streaming/media_streaming_utility.h\"\n#include \"ui/rhi/rhi_shader.h\"\n#include \"ui/painter.h\"\n#include \"data/data_peer.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_media_view.h\"\n\n#include <rhi/qrhi.h>\n\nnamespace Calls::Group {\nnamespace {\n\nusing namespace Ui::GL;\n\nconstexpr auto kNoiseTextureSize = 256;\nconstexpr auto kBlurTextureSizeFactor = 4.;\nconstexpr auto kBlurOpacity = 0.65f;\n\nstruct GroupFrameUniforms {\n\tfloat viewport[2];\n\tfloat _pad0[2];\n\tfloat frameBg[4];\n\tfloat shadow[4];\n\tfloat paused;\n\tfloat _pad1[3];\n\tfloat roundRect[4];\n\tfloat radiusOutline[2];\n\tfloat _pad2[2];\n\tfloat roundBg[4];\n\tfloat outlineFg[4];\n};\nstatic_assert(sizeof(GroupFrameUniforms) == 128);\n\nstruct BlurUniforms {\n\tfloat texelOffset;\n\tfloat _pad[3];\n};\nstatic_assert(sizeof(BlurUniforms) == 16);\n\nstruct ImageUniforms {\n\tfloat viewport[2];\n\tfloat g_opacity;\n\tfloat _pad;\n};\nstatic_assert(sizeof(ImageUniforms) == 16);\n\n[[nodiscard]] QShader LoadShader(const QString &name) {\n\treturn Ui::Rhi::ShaderFromFile(\n\t\tu\":/shaders/\"_q + name + u\".qsb\"_q);\n}\n\n[[nodiscard]] bool UseExpandForCamera(QSize original, QSize viewport) {\n\tusing namespace ::Media::Streaming;\n\treturn DecideFrameResize(viewport, original).expanding;\n}\n\n[[nodiscard]] QSize NonEmpty(QSize size) {\n\treturn QSize(std::max(size.width(), 1), std::max(size.height(), 1));\n}\n\n[[nodiscard]] QSize CountBlurredSize(\n\t\tQSize unscaled,\n\t\tQSize outer,\n\t\tfloat factor) {\n\tfactor *= kBlurTextureSizeFactor;\n\tconst auto area = outer / int(base::SafeRound(factor * cScale() / 100));\n\tconst auto scaled = unscaled.scaled(area, Qt::KeepAspectRatio);\n\treturn (scaled.width() > unscaled.width()\n\t\t|| scaled.height() > unscaled.height())\n\t\t? unscaled\n\t\t: NonEmpty(scaled);\n}\n\n[[nodiscard]] QSize InterpolateScaledSize(\n\t\tQSize unscaled,\n\t\tQSize size,\n\t\tfloat64 ratio) {\n\tif (ratio == 0.) {\n\t\treturn NonEmpty(unscaled.scaled(size, Qt::KeepAspectRatio));\n\t} else if (ratio == 1.) {\n\t\treturn NonEmpty(unscaled.scaled(\n\t\t\tsize,\n\t\t\tQt::KeepAspectRatioByExpanding));\n\t}\n\tconst auto notExpanded = NonEmpty(unscaled.scaled(\n\t\tsize,\n\t\tQt::KeepAspectRatio));\n\tconst auto expanded = NonEmpty(unscaled.scaled(\n\t\tsize,\n\t\tQt::KeepAspectRatioByExpanding));\n\treturn QSize(\n\t\tanim::interpolate(notExpanded.width(), expanded.width(), ratio),\n\t\tanim::interpolate(notExpanded.height(), expanded.height(), ratio));\n}\n\n[[nodiscard]] std::array<std::array<float, 2>, 4> CountTexCoords(\n\t\tQSize unscaled,\n\t\tQSize size,\n\t\tfloat64 expandRatio,\n\t\tbool swap = false) {\n\tconst auto scaled = InterpolateScaledSize(unscaled, size, expandRatio);\n\tconst auto left = (size.width() - scaled.width()) / 2;\n\tconst auto top = (size.height() - scaled.height()) / 2;\n\tauto dleft = float(left) / scaled.width();\n\tauto dright = float(size.width() - left) / scaled.width();\n\tauto dtop = float(top) / scaled.height();\n\tauto dbottom = float(size.height() - top) / scaled.height();\n\tif (swap) {\n\t\tstd::swap(dleft, dtop);\n\t\tstd::swap(dright, dbottom);\n\t}\n\treturn { {\n\t\t{ { -dleft, 1.f + dtop } },\n\t\t{ { dright, 1.f + dtop } },\n\t\t{ { dright, 1.f - dbottom } },\n\t\t{ { -dleft, 1.f - dbottom } },\n\t} };\n}\n\n} // namespace\n\nViewport::RendererRhi::RendererRhi(not_null<Viewport*> owner)\n: _owner(owner)\n, _pinIcon(st::groupCallVideoTile.pin)\n, _muteIcon(st::groupCallVideoCrossLine)\n, _pinBackground(\n\t(st::groupCallVideoTile.pinPadding.top()\n\t\t+ st::groupCallVideoTile.pin.icon.height()\n\t\t+ st::groupCallVideoTile.pinPadding.bottom()) / 2,\n\tst::radialBg) {\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_buttons.invalidate();\n\t}, _lifetime);\n}\n\nViewport::RendererRhi::~RendererRhi() {\n\tif (_initialized) {\n\t\treleaseResources();\n\t}\n}\n\nQColor Viewport::RendererRhi::rhiClearColor() {\n\treturn _owner->_fullscreen\n\t\t? QColor(0, 0, 0)\n\t\t: _owner->videoStream()\n\t\t? st::mediaviewBg->c\n\t\t: st::groupCallBg->c;\n}\n\nstd::optional<QColor> Viewport::RendererRhi::clearColor() {\n\treturn rhiClearColor();\n}\n\nvoid Viewport::RendererRhi::initialize(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\tif (_initialized && _rhi == rhi) {\n\t\treturn;\n\t}\n\treleaseResources();\n\n\t_rhi = rhi;\n\t_rt = rt;\n\t_cb = cb;\n\n\t_offscreenVertexBuffer = rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::VertexBuffer,\n\t\t4 * 4 * sizeof(float));\n\t_offscreenVertexBuffer->create();\n\n\t_onscreenVertexBuffer = rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::VertexBuffer,\n\t\tkMaxOnscreenDraws * kOnscreenVertexSlot);\n\t_onscreenVertexBuffer->create();\n\n\t_uniformBuffer = rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::UniformBuffer,\n\t\tkMaxOnscreenDraws * kUniformSlot);\n\t_uniformBuffer->create();\n\n\t_linearSampler = rhi->newSampler(\n\t\tQRhiSampler::Linear,\n\t\tQRhiSampler::Linear,\n\t\tQRhiSampler::None,\n\t\tQRhiSampler::ClampToEdge,\n\t\tQRhiSampler::ClampToEdge);\n\t_linearSampler->create();\n\n\t_nearestSampler = rhi->newSampler(\n\t\tQRhiSampler::Nearest,\n\t\tQRhiSampler::Nearest,\n\t\tQRhiSampler::None,\n\t\tQRhiSampler::ClampToEdge,\n\t\tQRhiSampler::ClampToEdge);\n\t_nearestSampler->create();\n\n\t_noiseRepeatSampler = rhi->newSampler(\n\t\tQRhiSampler::Nearest,\n\t\tQRhiSampler::Nearest,\n\t\tQRhiSampler::None,\n\t\tQRhiSampler::Repeat,\n\t\tQRhiSampler::Repeat);\n\t_noiseRepeatSampler->create();\n\n\t_placeholderTexture = rhi->newTexture(QRhiTexture::RGBA8, QSize(1, 1));\n\t_placeholderTexture->create();\n\n\tcreatePipelines();\n\t_initialized = true;\n}\n\nvoid Viewport::RendererRhi::createPipelines() {\n\tconst auto rpDesc = _rt->renderPassDescriptor();\n\n\tconst auto passthroughVert = LoadShader(u\"passthrough.vert\"_q);\n\tconst auto argb32Vert = LoadShader(u\"argb32.vert\"_q);\n\tconst auto groupFrameVert = LoadShader(u\"group_frame.vert\"_q);\n\tconst auto argb32Frag = LoadShader(u\"argb32.frag\"_q);\n\tconst auto yuv420Frag = LoadShader(u\"yuv420.frag\"_q);\n\tconst auto blurHFrag = LoadShader(u\"blur_h.frag\"_q);\n\tconst auto blurVFrag = LoadShader(u\"blur_v.frag\"_q);\n\tconst auto groupFrameFrag = LoadShader(u\"group_frame.frag\"_q);\n\tconst auto controlsFrag = LoadShader(u\"controls.frag\"_q);\n\n\tQRhiVertexInputLayout passthroughLayout;\n\tpassthroughLayout.setBindings({ { 4 * sizeof(float) } });\n\tpassthroughLayout.setAttributes({\n\t\t{ 0, 0, QRhiVertexInputAttribute::Float2, 0 },\n\t\t{ 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) },\n\t});\n\n\tQRhiVertexInputLayout argb32Layout;\n\targb32Layout.setBindings({ { 4 * sizeof(float) } });\n\targb32Layout.setAttributes({\n\t\t{ 0, 0, QRhiVertexInputAttribute::Float2, 0 },\n\t\t{ 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) },\n\t});\n\n\tQRhiVertexInputLayout groupFrameLayout;\n\tgroupFrameLayout.setBindings({ { 6 * sizeof(float) } });\n\tgroupFrameLayout.setAttributes({\n\t\t{ 0, 0, QRhiVertexInputAttribute::Float2, 0 },\n\t\t{ 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) },\n\t\t{ 0, 2, QRhiVertexInputAttribute::Float2, 4 * sizeof(float) },\n\t});\n\n\t// Downscale ARGB32: passthrough vert + argb32 frag\n\t// argb32.frag needs: s_texture at binding 1, no uniform block\n\t_downscaleArgb32Srb = _rhi->newShaderResourceBindings();\n\t_downscaleArgb32Srb->setBindings({\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_linearSampler),\n\t});\n\t_downscaleArgb32Srb->create();\n\n\t// Downscale YUV420: passthrough vert + yuv420 frag\n\t// yuv420.frag needs: y_texture(1), u_texture(2), v_texture(3)\n\t_downscaleYuv420Srb = _rhi->newShaderResourceBindings();\n\t_downscaleYuv420Srb->setBindings({\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_linearSampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t2,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_linearSampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t3,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_linearSampler),\n\t});\n\t_downscaleYuv420Srb->create();\n\n\t// Blur H: passthrough vert + blur_h frag\n\t// blur_h.frag needs: BlurParams at binding 0, b_texture at binding 1\n\t_blurHSrb = _rhi->newShaderResourceBindings();\n\t_blurHSrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer,\n\t\t\t0,\n\t\t\tsizeof(BlurUniforms)),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_nearestSampler),\n\t});\n\t_blurHSrb->create();\n\n\t{\n\t\tauto *tex = _rhi->newTexture(\n\t\t\tQRhiTexture::RGBA8,\n\t\t\tQSize(1, 1),\n\t\t\t1,\n\t\t\tQRhiTexture::RenderTarget);\n\t\ttex->create();\n\t\tauto colorAtt = QRhiColorAttachment(tex);\n\t\tauto *offscreenRT = _rhi->newTextureRenderTarget(\n\t\t\tQRhiTextureRenderTargetDescription(colorAtt));\n\t\t_offscreenRpDesc = offscreenRT->newCompatibleRenderPassDescriptor();\n\t\toffscreenRT->setRenderPassDescriptor(_offscreenRpDesc);\n\t\toffscreenRT->create();\n\t\tdelete offscreenRT;\n\t\tdelete tex;\n\t}\n\n\t_downscaleArgb32Pipeline = _rhi->newGraphicsPipeline();\n\t_downscaleArgb32Pipeline->setShaderStages({\n\t\t{ QRhiShaderStage::Vertex, passthroughVert },\n\t\t{ QRhiShaderStage::Fragment, argb32Frag },\n\t});\n\t_downscaleArgb32Pipeline->setVertexInputLayout(passthroughLayout);\n\t_downscaleArgb32Pipeline->setTopology(\n\t\tQRhiGraphicsPipeline::TriangleStrip);\n\t_downscaleArgb32Pipeline->setShaderResourceBindings(_downscaleArgb32Srb);\n\t_downscaleArgb32Pipeline->setRenderPassDescriptor(_offscreenRpDesc);\n\t_downscaleArgb32Pipeline->create();\n\n\t_downscaleYuv420Pipeline = _rhi->newGraphicsPipeline();\n\t_downscaleYuv420Pipeline->setShaderStages({\n\t\t{ QRhiShaderStage::Vertex, passthroughVert },\n\t\t{ QRhiShaderStage::Fragment, yuv420Frag },\n\t});\n\t_downscaleYuv420Pipeline->setVertexInputLayout(passthroughLayout);\n\t_downscaleYuv420Pipeline->setTopology(\n\t\tQRhiGraphicsPipeline::TriangleStrip);\n\t_downscaleYuv420Pipeline->setShaderResourceBindings(\n\t\t_downscaleYuv420Srb);\n\t_downscaleYuv420Pipeline->setRenderPassDescriptor(_offscreenRpDesc);\n\t_downscaleYuv420Pipeline->create();\n\n\t_blurHPipeline = _rhi->newGraphicsPipeline();\n\t_blurHPipeline->setShaderStages({\n\t\t{ QRhiShaderStage::Vertex, passthroughVert },\n\t\t{ QRhiShaderStage::Fragment, blurHFrag },\n\t});\n\t_blurHPipeline->setVertexInputLayout(passthroughLayout);\n\t_blurHPipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);\n\t_blurHPipeline->setShaderResourceBindings(_blurHSrb);\n\t_blurHPipeline->setRenderPassDescriptor(_offscreenRpDesc);\n\t_blurHPipeline->create();\n\n\t_blurVPipeline = _rhi->newGraphicsPipeline();\n\t_blurVPipeline->setShaderStages({\n\t\t{ QRhiShaderStage::Vertex, passthroughVert },\n\t\t{ QRhiShaderStage::Fragment, blurVFrag },\n\t});\n\t_blurVPipeline->setVertexInputLayout(passthroughLayout);\n\t_blurVPipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);\n\t_blurVPipeline->setShaderResourceBindings(_blurHSrb);\n\t_blurVPipeline->setRenderPassDescriptor(_offscreenRpDesc);\n\t_blurVPipeline->create();\n\n\t// Frame composite: group_frame vert + group_frame frag (on-screen)\n\t// group_frame.frag needs: uniform block(0), s_texture(1),\n\t// b_texture(2), n_texture(3)\n\tauto *frameSrb = _rhi->newShaderResourceBindings();\n\tframeSrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer,\n\t\t\t0,\n\t\t\tsizeof(GroupFrameUniforms)),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_linearSampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t2,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_linearSampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t3,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_noiseRepeatSampler),\n\t});\n\tframeSrb->create();\n\t_perDrawSrbs.push_back(frameSrb);\n\n\t_framePipeline = _rhi->newGraphicsPipeline();\n\t_framePipeline->setShaderStages({\n\t\t{ QRhiShaderStage::Vertex, groupFrameVert },\n\t\t{ QRhiShaderStage::Fragment, groupFrameFrag },\n\t});\n\t_framePipeline->setVertexInputLayout(groupFrameLayout);\n\t_framePipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);\n\t_framePipeline->setShaderResourceBindings(frameSrb);\n\t_framePipeline->setRenderPassDescriptor(rpDesc);\n\t_framePipeline->create();\n\n\t// Controls: argb32 vert + controls frag (blending on-screen)\n\tauto *controlsSrb = _rhi->newShaderResourceBindings();\n\tcontrolsSrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer,\n\t\t\t0,\n\t\t\tsizeof(ImageUniforms)),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_linearSampler),\n\t});\n\tcontrolsSrb->create();\n\t_perDrawSrbs.push_back(controlsSrb);\n\n\tQRhiGraphicsPipeline::TargetBlend blend;\n\tblend.enable = true;\n\tblend.srcColor = QRhiGraphicsPipeline::One;\n\tblend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;\n\tblend.srcAlpha = QRhiGraphicsPipeline::One;\n\tblend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;\n\n\t_controlsPipeline = _rhi->newGraphicsPipeline();\n\t_controlsPipeline->setShaderStages({\n\t\t{ QRhiShaderStage::Vertex, argb32Vert },\n\t\t{ QRhiShaderStage::Fragment, controlsFrag },\n\t});\n\t_controlsPipeline->setVertexInputLayout(argb32Layout);\n\t_controlsPipeline->setTargetBlends({ blend });\n\t_controlsPipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);\n\t_controlsPipeline->setShaderResourceBindings(controlsSrb);\n\t_controlsPipeline->setRenderPassDescriptor(rpDesc);\n\t_controlsPipeline->create();\n}\n\nvoid Viewport::RendererRhi::releaseResources() {\n\tfor (auto *srb : _perDrawSrbs) {\n\t\tdelete srb;\n\t}\n\t_perDrawSrbs.clear();\n\n\tfor (auto &entry : _texturePool) {\n\t\tdelete entry.texture;\n\t}\n\t_texturePool.clear();\n\n\tfor (auto &data : _tileData) {\n\t\tdelete data.rgbaTexture;\n\t\tdelete data.yTexture;\n\t\tdelete data.uTexture;\n\t\tdelete data.vTexture;\n\t\tdelete data.convertedTexture;\n\t\tdelete data.convertedRpDesc;\n\t\tdelete data.convertedRt;\n\t\tdelete data.downscaleTexture;\n\t\tdelete data.downscaleRpDesc;\n\t\tdelete data.downscaleRt;\n\t\tdelete data.blurHTexture;\n\t\tdelete data.blurHRpDesc;\n\t\tdelete data.blurHRt;\n\t\tdelete data.blurVTexture;\n\t\tdelete data.blurVRpDesc;\n\t\tdelete data.blurVRt;\n\t}\n\t_tileData.clear();\n\t_tileDataIndices.clear();\n\n\tdelete _downscaleArgb32Pipeline; _downscaleArgb32Pipeline = nullptr;\n\tdelete _downscaleYuv420Pipeline; _downscaleYuv420Pipeline = nullptr;\n\tdelete _blurHPipeline; _blurHPipeline = nullptr;\n\tdelete _blurVPipeline; _blurVPipeline = nullptr;\n\tdelete _framePipeline; _framePipeline = nullptr;\n\tdelete _controlsPipeline; _controlsPipeline = nullptr;\n\n\tdelete _downscaleArgb32Srb; _downscaleArgb32Srb = nullptr;\n\tdelete _downscaleYuv420Srb; _downscaleYuv420Srb = nullptr;\n\tdelete _blurHSrb; _blurHSrb = nullptr;\n\n\tdelete _offscreenRpDesc; _offscreenRpDesc = nullptr;\n\tdelete _noiseTexture; _noiseTexture = nullptr;\n\tdelete _offscreenVertexBuffer; _offscreenVertexBuffer = nullptr;\n\tdelete _onscreenVertexBuffer; _onscreenVertexBuffer = nullptr;\n\tdelete _uniformBuffer; _uniformBuffer = nullptr;\n\tdelete _linearSampler; _linearSampler = nullptr;\n\tdelete _nearestSampler; _nearestSampler = nullptr;\n\tdelete _noiseRepeatSampler; _noiseRepeatSampler = nullptr;\n\tdelete _placeholderTexture; _placeholderTexture = nullptr;\n\n\t_buttons.destroy();\n\t_names.destroy();\n\n\t_initialized = false;\n}\n\nQRhiTexture *Viewport::RendererRhi::acquirePoolTexture(QSize size) {\n\tif (_nextPoolIndex < int(_texturePool.size())) {\n\t\tauto &entry = _texturePool[_nextPoolIndex++];\n\t\tif (entry.size == size) {\n\t\t\treturn entry.texture;\n\t\t}\n\t\tdelete entry.texture;\n\t\tentry.texture = _rhi->newTexture(QRhiTexture::BGRA8, size);\n\t\tentry.texture->create();\n\t\tentry.size = size;\n\t\treturn entry.texture;\n\t}\n\tauto *tex = _rhi->newTexture(QRhiTexture::BGRA8, size);\n\ttex->create();\n\t_texturePool.push_back({ tex, size });\n\t_nextPoolIndex++;\n\treturn tex;\n}\n\nvoid Viewport::RendererRhi::render(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\trenderOffscreen(rhi, rt, cb);\n\n\t// Prepare onscreen: accumulate all resource updates into _rub.\n\t_nextOnscreenSlot = 0;\n\t_onscreenDraws.clear();\n\tauto *screenRub = _rhi->nextResourceUpdateBatch();\n\tif (_rub) {\n\t\tscreenRub->merge(_rub);\n\t}\n\t_rub = screenRub;\n\trenderOnscreen(rhi, rt, cb);\n\t// Apply all accumulated updates via beginPass.\n\tcb->beginPass(rt, *clearColor(), { 1.0f, 0 }, _rub);\n\t_rub = nullptr;\n\tconst auto pw = float(rt->pixelSize().width());\n\tconst auto ph = float(rt->pixelSize().height());\n\tfor (const auto &draw : _onscreenDraws) {\n\t\tcb->setGraphicsPipeline(draw.pipeline);\n\t\tcb->setShaderResources(draw.srb);\n\t\tcb->setViewport({ 0, 0, pw, ph });\n\t\tconst QRhiCommandBuffer::VertexInput vbuf(\n\t\t\t_onscreenVertexBuffer, draw.vertexOffset);\n\t\tcb->setVertexInput(0, 1, &vbuf);\n\t\tcb->draw(4);\n\t}\n\t_onscreenDraws.clear();\n\tcb->endPass();\n}\n\nvoid Viewport::RendererRhi::renderOffscreen(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\t_rhi = rhi;\n\t_rt = rt;\n\t_cb = cb;\n\t_nextPoolIndex = 0;\n\n\tconst auto size = rt->pixelSize();\n\tconst auto factor = style::DevicePixelRatio();\n\tif (_factor != factor) {\n\t\t_factor = factor;\n\t\t_ifactor = int(std::ceil(factor));\n\t\t_buttons.invalidate();\n\t}\n\t_viewport = QSize(\n\t\tint(size.width() / _factor),\n\t\tint(size.height() / _factor));\n\n\tvalidateDatas();\n\tensureNoiseTexture();\n\n\tfor (auto *srb : _perDrawSrbs) {\n\t\tdelete srb;\n\t}\n\t_perDrawSrbs.clear();\n\n\tauto index = 0;\n\tfor (const auto &tile : _owner->_tiles) {\n\t\tif (!tile->visible()) {\n\t\t\tindex++;\n\t\t\tcontinue;\n\t\t}\n\t\tpaintTileOffscreen(\n\t\t\ttile.get(),\n\t\t\t_tileData[_tileDataIndices[index++]]);\n\t}\n}\n\nvoid Viewport::RendererRhi::renderOnscreen(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\t_rhi = rhi;\n\t_rt = rt;\n\t_cb = cb;\n\n\tconst auto pw = float(rt->pixelSize().width());\n\tconst auto ph = float(rt->pixelSize().height());\n\n\tauto index = 0;\n\tfor (const auto &tile : _owner->_tiles) {\n\t\tif (!tile->visible()) {\n\t\t\tindex++;\n\t\t\tcontinue;\n\t\t}\n\t\tpaintTileOnscreen(\n\t\t\ttile.get(),\n\t\t\t_tileData[_tileDataIndices[index++]],\n\t\t\tpw, ph);\n\t}\n}\n\nvoid Viewport::RendererRhi::ensureNoiseTexture() {\n\tif (_noiseTexture) {\n\t\treturn;\n\t}\n\n\tauto noiseImage = QImage(\n\t\tkNoiseTextureSize,\n\t\tkNoiseTextureSize,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tnoiseImage.fill(Qt::transparent);\n\n\t_noiseTexture = _rhi->newTexture(\n\t\tQRhiTexture::RGBA8,\n\t\tQSize(kNoiseTextureSize, kNoiseTextureSize));\n\t_noiseTexture->create();\n\n\tconst auto fade = [](float t) {\n\t\treturn t * t * t * (t * (t * 6.f - 15.f) + 10.f);\n\t};\n\tconst auto rnm = [](float x, float y) {\n\t\tconst auto noise = std::sin(\n\t\t\tx * 12.9898f + y * 78.233f) * 43758.5453f;\n\t\treturn std::array<float, 4>{\n\t\t\tstd::fmod(noise, 1.f) * 2.f - 1.f,\n\t\t\tstd::fmod(noise * 1.2154f, 1.f) * 2.f - 1.f,\n\t\t\tstd::fmod(noise * 1.3453f, 1.f) * 2.f - 1.f,\n\t\t\tstd::fmod(noise * 1.4651f, 1.f) * 2.f - 1.f,\n\t\t};\n\t};\n\tconst auto pnoise3D = [&](float px, float py, float pz) {\n\t\tconst auto unit = 1.f / 256.f;\n\t\tconst auto half = 0.5f / 256.f;\n\t\tconst auto pix = unit * std::floor(px) + half;\n\t\tconst auto piy = unit * std::floor(py) + half;\n\t\tconst auto piz = unit * std::floor(pz) + half;\n\t\tconst auto pfx = px - std::floor(px);\n\t\tconst auto pfy = py - std::floor(py);\n\t\tconst auto pfz = pz - std::floor(pz);\n\t\tconst auto perm00 = rnm(pix, piy)[3];\n\t\tconst auto g000 = rnm(perm00, piz);\n\t\tconst auto n000 = g000[0]*pfx + g000[1]*pfy + g000[2]*pfz;\n\t\tconst auto g001 = rnm(perm00, piz + unit);\n\t\tconst auto n001 = g001[0]*pfx + g001[1]*pfy\n\t\t\t+ g001[2]*(pfz - 1.f);\n\t\tconst auto perm01 = rnm(pix, piy + unit)[3];\n\t\tconst auto g010 = rnm(perm01, piz);\n\t\tconst auto n010 = g010[0]*pfx + g010[1]*(pfy - 1.f)\n\t\t\t+ g010[2]*pfz;\n\t\tconst auto g011 = rnm(perm01, piz + unit);\n\t\tconst auto n011 = g011[0]*pfx + g011[1]*(pfy - 1.f)\n\t\t\t+ g011[2]*(pfz - 1.f);\n\t\tconst auto perm10 = rnm(pix + unit, piy)[3];\n\t\tconst auto g100 = rnm(perm10, piz);\n\t\tconst auto n100 = g100[0]*(pfx - 1.f) + g100[1]*pfy\n\t\t\t+ g100[2]*pfz;\n\t\tconst auto g101 = rnm(perm10, piz + unit);\n\t\tconst auto n101 = g101[0]*(pfx - 1.f) + g101[1]*pfy\n\t\t\t+ g101[2]*(pfz - 1.f);\n\t\tconst auto perm11 = rnm(pix + unit, piy + unit)[3];\n\t\tconst auto g110 = rnm(perm11, piz);\n\t\tconst auto n110 = g110[0]*(pfx - 1.f) + g110[1]*(pfy - 1.f)\n\t\t\t+ g110[2]*pfz;\n\t\tconst auto g111 = rnm(perm11, piz + unit);\n\t\tconst auto n111 = g111[0]*(pfx - 1.f) + g111[1]*(pfy - 1.f)\n\t\t\t+ g111[2]*(pfz - 1.f);\n\t\tconst auto fx = fade(pfx);\n\t\tconst auto fy = fade(pfy);\n\t\tconst auto fz = fade(pfz);\n\t\tconst auto nx0 = n000 + (n100 - n000) * fx;\n\t\tconst auto nx1 = n001 + (n101 - n001) * fx;\n\t\tconst auto nx2 = n010 + (n110 - n010) * fx;\n\t\tconst auto nx3 = n011 + (n111 - n011) * fx;\n\t\tconst auto nxy0 = nx0 + (nx2 - nx0) * fy;\n\t\tconst auto nxy1 = nx1 + (nx3 - nx1) * fy;\n\t\treturn nxy0 + (nxy1 - nxy0) * fz;\n\t};\n\n\tconstexpr auto grainsize = 1.3f;\n\tconstexpr auto rotation = 1.425f;\n\tconst auto sinR = std::sin(rotation);\n\tconst auto cosR = std::cos(rotation);\n\n\tauto *noiseData = noiseImage.bits();\n\tfor (int py = 0; py < kNoiseTextureSize; ++py) {\n\t\tfor (int px = 0; px < kNoiseTextureSize; ++px) {\n\t\t\tauto tx = float(px) / grainsize;\n\t\t\tauto ty = float(py) / grainsize;\n\t\t\ttx -= 0.5f * kNoiseTextureSize / grainsize;\n\t\t\tty -= 0.5f * kNoiseTextureSize / grainsize;\n\t\t\tconst auto rx = tx * cosR - ty * sinR;\n\t\t\tconst auto ry = tx * sinR + ty * cosR;\n\t\t\ttx = rx + 0.5f * kNoiseTextureSize / grainsize;\n\t\t\tty = ry + 0.5f * kNoiseTextureSize / grainsize;\n\t\t\tconst auto noise = pnoise3D(tx, ty, 0.f) * 0.5f + 0.5f;\n\t\t\tconst auto v = quint8(std::clamp(noise, 0.f, 1.f) * 255.f);\n\t\t\tauto *pixel = noiseData\n\t\t\t\t+ (py * noiseImage.bytesPerLine())\n\t\t\t\t+ (px * 4);\n\t\t\tpixel[0] = v;\n\t\t\tpixel[1] = v;\n\t\t\tpixel[2] = v;\n\t\t\tpixel[3] = 255;\n\t\t}\n\t}\n\t_rub = _rhi->nextResourceUpdateBatch();\n\t_rub->uploadTexture(\n\t\t_noiseTexture,\n\t\tQRhiTextureUploadDescription(\n\t\t\tQRhiTextureUploadEntry(0, 0,\n\t\t\t\tQRhiTextureSubresourceUploadDescription(noiseImage))));\n}\n\nvoid Viewport::RendererRhi::paintTileOffscreen(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &tileData) {\n\tconst auto track = tile->track();\n\tconst auto markGuard = gsl::finally([&] {\n\t\ttile->track()->markFrameShown();\n\t});\n\tconst auto data = track->frameWithInfo(false);\n\t_userpicFrame = (data.format == Webrtc::FrameFormat::None);\n\tvalidateUserpicFrame(tile, tileData);\n\tconst auto frameSize = _userpicFrame\n\t\t? tileData.userpicFrame.size()\n\t\t: data.yuv420->size;\n\tAssert(!frameSize.isEmpty());\n\n\t_rgbaFrame = (data.format == Webrtc::FrameFormat::ARGB32)\n\t\t|| _userpicFrame;\n\n\tconst auto geometry = tile->geometry().translated(\n\t\t_owner->borrowedOrigin());\n\tconst auto unscaled = Media::View::FlipSizeByRotation(\n\t\tframeSize,\n\t\t_userpicFrame ? 0 : data.rotation);\n\n\tvalidateOutlineAnimation(tile, tileData);\n\tvalidatePausedAnimation(tile, tileData);\n\n\tconst auto blurSize = CountBlurredSize(\n\t\tunscaled,\n\t\tgeometry.size(),\n\t\t_factor);\n\n\tuploadFrame(data, tileData);\n\tif (!_rgbaFrame) {\n\t\tdrawYuv2RgbPass(tileData, frameSize);\n\t}\n\tprepareOffscreenTargets(tileData, blurSize);\n\tdrawDownscalePass(tileData, blurSize);\n\tdrawBlurPass(tileData, blurSize);\n}\n\nvoid Viewport::RendererRhi::paintTileOnscreen(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &tileData,\n\t\tfloat pw,\n\t\tfloat ph) {\n\tconst auto data = tile->track()->frameWithInfo(false);\n\t_userpicFrame = (data.format == Webrtc::FrameFormat::None);\n\tconst auto frameSize = _userpicFrame\n\t\t? tileData.userpicFrame.size()\n\t\t: data.yuv420->size;\n\tconst auto frameRotation = _userpicFrame ? 0 : data.rotation;\n\t_rgbaFrame = (data.format == Webrtc::FrameFormat::ARGB32)\n\t\t|| _userpicFrame;\n\n\tconst auto geometry = tile->geometry().translated(\n\t\t_owner->borrowedOrigin());\n\tconst auto unscaled = Media::View::FlipSizeByRotation(\n\t\tframeSize,\n\t\tframeRotation);\n\tconst auto blurSize = CountBlurredSize(\n\t\tunscaled,\n\t\tgeometry.size(),\n\t\t_factor);\n\n\tdrawFramePass(tile, tileData, blurSize);\n\tdrawControls(tile, tileData);\n}\n\nvoid Viewport::RendererRhi::uploadFrame(\n\t\tconst Webrtc::FrameWithInfo &data,\n\t\tTileData &tileData) {\n\tconst auto imageIndex = _userpicFrame ? 0 : (data.index + 1);\n\tconst auto upload = (tileData.trackIndex != imageIndex);\n\ttileData.trackIndex = imageIndex;\n\tif (!upload) {\n\t\treturn;\n\t}\n\n\t_rub = _rhi->nextResourceUpdateBatch();\n\n\tif (_rgbaFrame) {\n\t\tconst auto &image = _userpicFrame\n\t\t\t? tileData.userpicFrame\n\t\t\t: data.original;\n\t\tif (!tileData.rgbaTexture\n\t\t\t|| tileData.rgbaSize != image.size()) {\n\t\t\tdelete tileData.rgbaTexture;\n\t\t\ttileData.rgbaTexture = _rhi->newTexture(\n\t\t\t\tQRhiTexture::BGRA8, image.size());\n\t\t\ttileData.rgbaTexture->create();\n\t\t\ttileData.rgbaSize = image.size();\n\t\t}\n\t\t_rub->uploadTexture(\n\t\t\ttileData.rgbaTexture,\n\t\t\tQRhiTextureUploadDescription(\n\t\t\t\tQRhiTextureUploadEntry(0, 0,\n\t\t\t\t\tQRhiTextureSubresourceUploadDescription(image))));\n\t} else {\n\t\tconst auto yuv = data.yuv420;\n\t\tif (!tileData.yTexture || tileData.lumaSize != yuv->size) {\n\t\t\tdelete tileData.yTexture;\n\t\t\ttileData.yTexture = _rhi->newTexture(\n\t\t\t\tQRhiTexture::R8, yuv->size);\n\t\t\ttileData.yTexture->create();\n\t\t\ttileData.lumaSize = yuv->size;\n\t\t}\n\t\tauto yDesc = QRhiTextureSubresourceUploadDescription(\n\t\t\tyuv->y.data,\n\t\t\tyuv->y.stride * yuv->size.height());\n\t\tyDesc.setDataStride(yuv->y.stride);\n\t\t_rub->uploadTexture(\n\t\t\ttileData.yTexture,\n\t\t\tQRhiTextureUploadDescription(\n\t\t\t\tQRhiTextureUploadEntry(0, 0, yDesc)));\n\n\t\tif (!tileData.uTexture\n\t\t\t|| tileData.chromaSize != yuv->chromaSize) {\n\t\t\tdelete tileData.uTexture;\n\t\t\ttileData.uTexture = _rhi->newTexture(\n\t\t\t\tQRhiTexture::R8, yuv->chromaSize);\n\t\t\ttileData.uTexture->create();\n\t\t\tdelete tileData.vTexture;\n\t\t\ttileData.vTexture = _rhi->newTexture(\n\t\t\t\tQRhiTexture::R8, yuv->chromaSize);\n\t\t\ttileData.vTexture->create();\n\t\t\ttileData.chromaSize = yuv->chromaSize;\n\t\t}\n\t\tauto uDesc = QRhiTextureSubresourceUploadDescription(\n\t\t\tyuv->u.data,\n\t\t\tyuv->u.stride * yuv->chromaSize.height());\n\t\tuDesc.setDataStride(yuv->u.stride);\n\t\t_rub->uploadTexture(\n\t\t\ttileData.uTexture,\n\t\t\tQRhiTextureUploadDescription(\n\t\t\t\tQRhiTextureUploadEntry(0, 0, uDesc)));\n\t\tauto vDesc = QRhiTextureSubresourceUploadDescription(\n\t\t\tyuv->v.data,\n\t\t\tyuv->v.stride * yuv->chromaSize.height());\n\t\tvDesc.setDataStride(yuv->v.stride);\n\t\t_rub->uploadTexture(\n\t\t\ttileData.vTexture,\n\t\t\tQRhiTextureUploadDescription(\n\t\t\t\tQRhiTextureUploadEntry(0, 0, vDesc)));\n\t}\n}\n\nvoid Viewport::RendererRhi::prepareOffscreenTargets(\n\t\tTileData &tileData,\n\t\tQSize blurSize) {\n\tif (tileData.blurSize == blurSize\n\t\t&& tileData.downscaleRt\n\t\t&& tileData.blurHRt\n\t\t&& tileData.blurVRt) {\n\t\treturn;\n\t}\n\ttileData.blurSize = blurSize;\n\n\tconst auto createOffscreenRT = [&](\n\t\t\tQRhiTexture *&tex,\n\t\t\tQRhiTextureRenderTarget *&rt,\n\t\t\tQRhiRenderPassDescriptor *&rpDesc) {\n\t\tdelete rt;\n\t\tdelete rpDesc;\n\t\tdelete tex;\n\t\ttex = _rhi->newTexture(\n\t\t\tQRhiTexture::RGBA8, blurSize, 1, QRhiTexture::RenderTarget);\n\t\ttex->create();\n\t\tauto colorAtt = QRhiColorAttachment(tex);\n\t\trt = _rhi->newTextureRenderTarget(\n\t\t\tQRhiTextureRenderTargetDescription(colorAtt));\n\t\trpDesc = rt->newCompatibleRenderPassDescriptor();\n\t\trt->setRenderPassDescriptor(rpDesc);\n\t\trt->create();\n\t};\n\tcreateOffscreenRT(\n\t\ttileData.downscaleTexture,\n\t\ttileData.downscaleRt,\n\t\ttileData.downscaleRpDesc);\n\tcreateOffscreenRT(\n\t\ttileData.blurHTexture,\n\t\ttileData.blurHRt,\n\t\ttileData.blurHRpDesc);\n\tcreateOffscreenRT(\n\t\ttileData.blurVTexture,\n\t\ttileData.blurVRt,\n\t\ttileData.blurVRpDesc);\n}\n\nvoid Viewport::RendererRhi::drawYuv2RgbPass(\n\t\tTileData &tileData,\n\t\tQSize frameSize) {\n\tif (!tileData.convertedTexture\n\t\t|| tileData.convertedSize != frameSize) {\n\t\tdelete tileData.convertedRt;\n\t\tdelete tileData.convertedRpDesc;\n\t\tdelete tileData.convertedTexture;\n\t\ttileData.convertedTexture = _rhi->newTexture(\n\t\t\tQRhiTexture::RGBA8,\n\t\t\tframeSize,\n\t\t\t1,\n\t\t\tQRhiTexture::RenderTarget);\n\t\ttileData.convertedTexture->create();\n\t\tauto colorAtt = QRhiColorAttachment(tileData.convertedTexture);\n\t\ttileData.convertedRt = _rhi->newTextureRenderTarget(\n\t\t\tQRhiTextureRenderTargetDescription(colorAtt));\n\t\ttileData.convertedRpDesc =\n\t\t\ttileData.convertedRt->newCompatibleRenderPassDescriptor();\n\t\ttileData.convertedRt->setRenderPassDescriptor(\n\t\t\ttileData.convertedRpDesc);\n\t\ttileData.convertedRt->create();\n\t\ttileData.convertedSize = frameSize;\n\t}\n\n\tconst float coords[] = {\n\t\t-1.f, -1.f, 0.f, 1.f,\n\t\t 1.f, -1.f, 1.f, 1.f,\n\t\t-1.f,  1.f, 0.f, 0.f,\n\t\t 1.f,  1.f, 1.f, 0.f,\n\t};\n\n\tif (!_rub) {\n\t\t_rub = _rhi->nextResourceUpdateBatch();\n\t}\n\t_rub->updateDynamicBuffer(\n\t\t_offscreenVertexBuffer, 0, sizeof(coords), coords);\n\n\tauto *srb = _rhi->newShaderResourceBindings();\n\t_perDrawSrbs.push_back(srb);\n\tsrb->setBindings({\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\ttileData.yTexture,\n\t\t\t_linearSampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t2,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\ttileData.uTexture,\n\t\t\t_linearSampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t3,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\ttileData.vTexture,\n\t\t\t_linearSampler),\n\t});\n\tsrb->create();\n\n\t_cb->beginPass(\n\t\ttileData.convertedRt, Qt::black, { 1.0f, 0 }, _rub);\n\t_rub = nullptr;\n\t_cb->setGraphicsPipeline(_downscaleYuv420Pipeline);\n\t_cb->setShaderResources(srb);\n\t_cb->setViewport({\n\t\t0, 0, float(frameSize.width()), float(frameSize.height()) });\n\tconst QRhiCommandBuffer::VertexInput vbuf(\n\t\t_offscreenVertexBuffer, 0);\n\t_cb->setVertexInput(0, 1, &vbuf);\n\t_cb->draw(4);\n\t_cb->endPass();\n}\n\nvoid Viewport::RendererRhi::drawDownscalePass(\n\t\tTileData &tileData,\n\t\tQSize blurSize) {\n\tconst float w = float(blurSize.width());\n\tconst float h = float(blurSize.height());\n\tconst float coords[] = {\n\t\t-1.f, -1.f, 0.f, 1.f,\n\t\t 1.f, -1.f, 1.f, 1.f,\n\t\t-1.f,  1.f, 0.f, 0.f,\n\t\t 1.f,  1.f, 1.f, 0.f,\n\t};\n\n\tif (!_rub) {\n\t\t_rub = _rhi->nextResourceUpdateBatch();\n\t}\n\t_rub->updateDynamicBuffer(\n\t\t_offscreenVertexBuffer, 0, sizeof(coords), coords);\n\n\tauto *srb = _rhi->newShaderResourceBindings();\n\t_perDrawSrbs.push_back(srb);\n\n\t// After drawYuv2RgbPass, YUV420 frames have convertedTexture (RGBA).\n\t// Use ARGB32 pipeline for both cases in the downscale pass.\n\tauto *srcTexture = _rgbaFrame\n\t\t? tileData.rgbaTexture\n\t\t: tileData.convertedTexture;\n\tsrb->setBindings({\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\tsrcTexture,\n\t\t\t_linearSampler),\n\t});\n\tsrb->create();\n\n\t_cb->beginPass(tileData.downscaleRt, Qt::black, { 1.0f, 0 }, _rub);\n\t_rub = nullptr;\n\t_cb->setGraphicsPipeline(_downscaleArgb32Pipeline);\n\t_cb->setShaderResources(srb);\n\t_cb->setViewport({ 0, 0, w, h });\n\tconst QRhiCommandBuffer::VertexInput vbuf(\n\t\t_offscreenVertexBuffer, 0);\n\t_cb->setVertexInput(0, 1, &vbuf);\n\t_cb->draw(4);\n\t_cb->endPass();\n}\n\nvoid Viewport::RendererRhi::drawBlurPass(\n\t\tTileData &tileData,\n\t\tQSize blurSize) {\n\tconst float w = float(blurSize.width());\n\tconst float h = float(blurSize.height());\n\tconst float coords[] = {\n\t\t-1.f, -1.f, 0.f, 1.f,\n\t\t 1.f, -1.f, 1.f, 1.f,\n\t\t-1.f,  1.f, 0.f, 0.f,\n\t\t 1.f,  1.f, 1.f, 0.f,\n\t};\n\n\t{\n\t\tBlurUniforms blurUniforms{};\n\t\tblurUniforms.texelOffset = 1.f / w;\n\t\tauto *rub = _rhi->nextResourceUpdateBatch();\n\t\trub->updateDynamicBuffer(\n\t\t\t_offscreenVertexBuffer, 0, sizeof(coords), coords);\n\t\trub->updateDynamicBuffer(\n\t\t\t_uniformBuffer, 0, sizeof(blurUniforms), &blurUniforms);\n\n\t\tauto *srb = _rhi->newShaderResourceBindings();\n\t\t_perDrawSrbs.push_back(srb);\n\t\tsrb->setBindings({\n\t\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t\t0,\n\t\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_uniformBuffer, 0, sizeof(BlurUniforms)),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t1,\n\t\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t\ttileData.downscaleTexture,\n\t\t\t\t_nearestSampler),\n\t\t});\n\t\tsrb->create();\n\n\t\t_cb->beginPass(\n\t\t\ttileData.blurHRt, Qt::black, { 1.0f, 0 }, rub);\n\t\t_cb->setGraphicsPipeline(_blurHPipeline);\n\t\t_cb->setShaderResources(srb);\n\t\t_cb->setViewport({ 0, 0, w, h });\n\t\tconst QRhiCommandBuffer::VertexInput vbuf(\n\t\t\t_offscreenVertexBuffer, 0);\n\t\t_cb->setVertexInput(0, 1, &vbuf);\n\t\t_cb->draw(4);\n\t\t_cb->endPass();\n\t}\n\n\t{\n\t\tBlurUniforms blurUniforms{};\n\t\tblurUniforms.texelOffset = 1.f / h;\n\t\tauto *rub = _rhi->nextResourceUpdateBatch();\n\t\trub->updateDynamicBuffer(\n\t\t\t_offscreenVertexBuffer, 0, sizeof(coords), coords);\n\t\trub->updateDynamicBuffer(\n\t\t\t_uniformBuffer, 0, sizeof(blurUniforms), &blurUniforms);\n\n\t\tauto *srb = _rhi->newShaderResourceBindings();\n\t\t_perDrawSrbs.push_back(srb);\n\t\tsrb->setBindings({\n\t\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t\t0,\n\t\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_uniformBuffer, 0, sizeof(BlurUniforms)),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t1,\n\t\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t\ttileData.blurHTexture,\n\t\t\t\t_nearestSampler),\n\t\t});\n\t\tsrb->create();\n\n\t\t_cb->beginPass(\n\t\t\ttileData.blurVRt, Qt::black, { 1.0f, 0 }, rub);\n\t\t_cb->setGraphicsPipeline(_blurVPipeline);\n\t\t_cb->setShaderResources(srb);\n\t\t_cb->setViewport({ 0, 0, w, h });\n\t\tconst QRhiCommandBuffer::VertexInput vbuf(\n\t\t\t_offscreenVertexBuffer, 0);\n\t\t_cb->setVertexInput(0, 1, &vbuf);\n\t\t_cb->draw(4);\n\t\t_cb->endPass();\n\t}\n}\n\nvoid Viewport::RendererRhi::drawFramePass(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &tileData,\n\t\tQSize blurSize) {\n\tconst auto geometry = tile->geometry().translated(\n\t\t_owner->borrowedOrigin());\n\tconst auto x = geometry.x();\n\tconst auto y = geometry.y();\n\tconst auto width = geometry.width();\n\tconst auto height = geometry.height();\n\n\tconst auto data = tile->track()->frameWithInfo(false);\n\tconst auto frameSize = _userpicFrame\n\t\t? tileData.userpicFrame.size()\n\t\t: data.yuv420->size;\n\tconst auto frameRotation = _userpicFrame ? 0 : data.rotation;\n\tconst auto unscaled = Media::View::FlipSizeByRotation(\n\t\tframeSize,\n\t\tframeRotation);\n\tconst auto tileSize = geometry.size();\n\tconst auto swap = (((frameRotation / 90) % 2) == 1);\n\tconst auto expand = isExpanded(tile, unscaled, tileSize);\n\tconst auto animation = tile->animation();\n\tconst auto expandRatio = (animation.ratio >= 0.)\n\t\t? countExpandRatio(tile, unscaled, animation)\n\t\t: expand\n\t\t? 1.\n\t\t: 0.;\n\n\tauto texCoords = CountTexCoords(unscaled, tileSize, expandRatio, swap);\n\tauto blurTexCoords = (expandRatio == 1. && !swap)\n\t\t? texCoords\n\t\t: CountTexCoords(unscaled, tileSize, 1.);\n\n\tif (tile->mirror()) {\n\t\tstd::swap(texCoords[0], texCoords[1]);\n\t\tstd::swap(texCoords[2], texCoords[3]);\n\t\tstd::swap(blurTexCoords[0], blurTexCoords[1]);\n\t\tstd::swap(blurTexCoords[2], blurTexCoords[3]);\n\t}\n\tif (const auto shift = (frameRotation / 90); shift > 0) {\n\t\tstd::rotate(\n\t\t\ttexCoords.begin(),\n\t\t\ttexCoords.begin() + shift,\n\t\t\ttexCoords.end());\n\t\tstd::rotate(\n\t\t\tblurTexCoords.begin(),\n\t\t\tblurTexCoords.begin() + shift,\n\t\t\tblurTexCoords.end());\n\t}\n\n\tconst auto outline = tileData.outlined.value(\n\t\ttileData.outline ? 1. : 0.);\n\tconst auto paused = tileData.paused.value(\n\t\ttileData.pause ? 1. : 0.);\n\tconst auto &st = st::groupCallVideoTile;\n\tconst auto fullscreen = _owner->_fullscreen;\n\tconst auto shown = _owner->_controlsShownRatio;\n\n\tconst auto pw = float(_rt->pixelSize().width());\n\tconst auto ph = float(_rt->pixelSize().height());\n\n\tconst auto rect = transformRect(geometry);\n\n\tconst float frameCoords[] = {\n\t\trect.left(), rect.top(),\n\t\ttexCoords[0][0], texCoords[0][1],\n\t\tblurTexCoords[0][0], blurTexCoords[0][1],\n\n\t\trect.right(), rect.top(),\n\t\ttexCoords[1][0], texCoords[1][1],\n\t\tblurTexCoords[1][0], blurTexCoords[1][1],\n\n\t\trect.left(), rect.bottom(),\n\t\ttexCoords[3][0], texCoords[3][1],\n\t\tblurTexCoords[3][0], blurTexCoords[3][1],\n\n\t\trect.right(), rect.bottom(),\n\t\ttexCoords[2][0], texCoords[2][1],\n\t\tblurTexCoords[2][0], blurTexCoords[2][1],\n\t};\n\n\tGroupFrameUniforms uniforms{};\n\tuniforms.viewport[0] = pw;\n\tuniforms.viewport[1] = ph;\n\n\tconst auto bg = fullscreen ? QColor(0, 0, 0) : rhiClearColor();\n\tuniforms.frameBg[0] = bg.redF();\n\tuniforms.frameBg[1] = bg.greenF();\n\tuniforms.frameBg[2] = bg.blueF();\n\tuniforms.frameBg[3] = bg.alphaF();\n\n\tconst auto shadowHeight = st.shadowHeight * _factor;\n\tconst auto shadowAlpha = Viewport::kShadowMaxAlpha / 255.f;\n\tuniforms.shadow[0] = shadowHeight;\n\tuniforms.shadow[1] = shown;\n\tuniforms.shadow[2] = shadowAlpha;\n\tuniforms.shadow[3] = fullscreen ? 0.f : kBlurOpacity;\n\n\tuniforms.paused = float(paused);\n\n\tuniforms.roundRect[0] = rect.x();\n\tuniforms.roundRect[1] = rect.y();\n\tuniforms.roundRect[2] = rect.width();\n\tuniforms.roundRect[3] = rect.height();\n\n\tconst auto radius = _owner->videoStream()\n\t\t? st::storiesRadius\n\t\t: st::roundRadiusLarge;\n\tuniforms.radiusOutline[0] = float(\n\t\tradius * _factor * (fullscreen ? 0. : 1.));\n\tuniforms.radiusOutline[1] = (outline > 0)\n\t\t? float(st::groupCallOutline * _factor)\n\t\t: 0.f;\n\n\tconst auto roundBg = rhiClearColor();\n\tuniforms.roundBg[0] = roundBg.redF();\n\tuniforms.roundBg[1] = roundBg.greenF();\n\tuniforms.roundBg[2] = roundBg.blueF();\n\tuniforms.roundBg[3] = roundBg.alphaF();\n\n\tuniforms.outlineFg[0] = st::groupCallMemberActiveIcon->c.redF();\n\tuniforms.outlineFg[1] = st::groupCallMemberActiveIcon->c.greenF();\n\tuniforms.outlineFg[2] = st::groupCallMemberActiveIcon->c.blueF();\n\tuniforms.outlineFg[3] = st::groupCallMemberActiveIcon->c.alphaF()\n\t\t* outline;\n\n\tconst auto slot = _nextOnscreenSlot++;\n\tif (slot >= kMaxOnscreenDraws) {\n\t\treturn;\n\t}\n\tconst auto vOffset = slot * kOnscreenVertexSlot;\n\tconst auto uOffset = slot * kUniformSlot;\n\n\t_rub->updateDynamicBuffer(\n\t\t_onscreenVertexBuffer, vOffset, sizeof(frameCoords), frameCoords);\n\t_rub->updateDynamicBuffer(\n\t\t_uniformBuffer, uOffset, sizeof(uniforms), &uniforms);\n\n\tauto *srb = _rhi->newShaderResourceBindings();\n\t_perDrawSrbs.push_back(srb);\n\n\tauto *sTex = _rgbaFrame\n\t\t? tileData.rgbaTexture\n\t\t: tileData.convertedTexture;\n\tsrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer,\n\t\t\tuOffset,\n\t\t\tsizeof(GroupFrameUniforms)),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\tsTex ? sTex : _placeholderTexture,\n\t\t\t_linearSampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t2,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\ttileData.blurVTexture\n\t\t\t\t? tileData.blurVTexture\n\t\t\t\t: _placeholderTexture,\n\t\t\t_linearSampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t3,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_noiseTexture ? _noiseTexture : _placeholderTexture,\n\t\t\t_noiseRepeatSampler),\n\t});\n\tsrb->create();\n\n\t_onscreenDraws.push_back({ _framePipeline, srb, vOffset });\n}\n\nvoid Viewport::RendererRhi::drawControls(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &tileData) {\n\tconst auto geometry = tile->geometry().translated(\n\t\t_owner->borrowedOrigin());\n\tconst auto x = geometry.x();\n\tconst auto y = geometry.y();\n\tconst auto width = geometry.width();\n\tconst auto height = geometry.height();\n\tconst auto &st = st::groupCallVideoTile;\n\tconst auto shown = _owner->_controlsShownRatio;\n\tconst auto fullNameShift = st.namePosition.y() + st::normalFont->height;\n\tconst auto nameShift = anim::interpolate(fullNameShift, 0, shown);\n\tconst auto row = tile->row();\n\tconst auto outline = tileData.outlined.value(\n\t\ttileData.outline ? 1. : 0.);\n\tconst auto paused = tileData.paused.value(\n\t\ttileData.pause ? 1. : 0.);\n\tconst auto factor = style::DevicePixelRatio();\n\n\tconst auto nameTop = y + (height\n\t\t- st.namePosition.y()\n\t\t- st::semiboldFont->height);\n\tconst auto pinVisible = _owner->wide()\n\t\t&& (tile->pinInner().translated(x, y).bottom() > y);\n\tconst auto nameVisible = (nameShift != fullNameShift);\n\tconst auto pausedVisible = (paused > 0.);\n\n\tif (!nameVisible && !pinVisible && !pausedVisible) {\n\t\treturn;\n\t}\n\n\tensureButtonsImage();\n\trow->lazyInitialize(st::groupCallMembersListItem);\n\n\tconst auto pw = float(_rt->pixelSize().width());\n\tconst auto ph = float(_rt->pixelSize().height());\n\n\tauto drawRasterOverlay = [&](\n\t\t\tQRect rect,\n\t\t\tFn<void(Painter&)> method,\n\t\t\tfloat opacity) {\n\t\tpaintUsingRaster(rect, std::move(method), opacity);\n\t};\n\n\tif (pausedVisible) {\n\t\tconst auto middle = (st::groupCallVideoPlaceholderHeight\n\t\t\t- st::groupCallPaused.height()) / 2;\n\t\tconst auto pausedSpace = (nameTop - y)\n\t\t\t- st::groupCallPaused.height()\n\t\t\t- st::semiboldFont->height;\n\t\tconst auto pauseIconSkip = middle\n\t\t\t- st::groupCallVideoPlaceholderIconTop;\n\t\tconst auto pauseTextSkip = st::groupCallVideoPlaceholderTextTop\n\t\t\t- st::groupCallVideoPlaceholderIconTop;\n\t\tconst auto pauseIconTop = !_owner->wide()\n\t\t\t? (y + (height - st::groupCallPaused.height()) / 2)\n\t\t\t: (pausedSpace < 3 * st::semiboldFont->height)\n\t\t\t? (pausedSpace / 3)\n\t\t\t: std::min(\n\t\t\t\ty + (height / 2) - pauseIconSkip,\n\t\t\t\t(nameTop\n\t\t\t\t\t- st::semiboldFont->height * 3\n\t\t\t\t\t- st::groupCallPaused.height()));\n\n\t\tconst auto iconRect = QRect(\n\t\t\tx + (width - st::groupCallPaused.width()) / 2,\n\t\t\tpauseIconTop,\n\t\t\tst::groupCallPaused.width(),\n\t\t\tst::groupCallPaused.height());\n\t\tdrawRasterOverlay(iconRect, [&](Painter &p) {\n\t\t\tst::groupCallPaused.paint(\n\t\t\t\tp,\n\t\t\t\ticonRect.x(),\n\t\t\t\ticonRect.y(),\n\t\t\t\twidth);\n\t\t}, float(paused));\n\n\t\tif (_owner->wide()) {\n\t\t\tconst auto pauseTextTop =\n\t\t\t\t(pausedSpace < 3 * st::semiboldFont->height)\n\t\t\t\t? (nameTop - (pausedSpace / 3) - st::semiboldFont->height)\n\t\t\t\t: std::min(\n\t\t\t\t\tpauseIconTop + pauseTextSkip,\n\t\t\t\t\tnameTop - st::semiboldFont->height * 2);\n\t\t\tconst auto pausedText =\n\t\t\t\ttr::lng_group_call_video_paused(tr::now);\n\t\t\tconst auto textWidth =\n\t\t\t\tst::semiboldFont->width(pausedText);\n\t\t\tconst auto textRect = QRect(\n\t\t\t\tx + (width - textWidth) / 2,\n\t\t\t\tpauseTextTop,\n\t\t\t\ttextWidth,\n\t\t\t\tst::semiboldFont->height);\n\t\t\tdrawRasterOverlay(textRect, [&](Painter &p) {\n\t\t\t\tp.setPen(st::groupCallVideoTextFg);\n\t\t\t\tp.setFont(st::semiboldFont);\n\t\t\t\tp.drawText(\n\t\t\t\t\ttextRect,\n\t\t\t\t\tpausedText,\n\t\t\t\t\tstyle::al_top);\n\t\t\t}, float(paused));\n\t\t}\n\t}\n\n\tif (pinVisible) {\n\t\tconst auto pinInner = tile->pinInner();\n\t\tconst auto pinRect = pinInner.translated(x, y);\n\t\tdrawRasterOverlay(pinRect, [&](Painter &p) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tVideoTile::PaintPinButton(\n\t\t\t\tp,\n\t\t\t\ttile->pinned(),\n\t\t\t\tpinRect.x(),\n\t\t\t\tpinRect.y(),\n\t\t\t\t_owner->widget()->width(),\n\t\t\t\t&_pinBackground,\n\t\t\t\t&_pinIcon);\n\t\t}, 1.f);\n\n\t\tconst auto backInner = tile->backInner();\n\t\tconst auto backRect = backInner.translated(x, y);\n\t\tdrawRasterOverlay(backRect, [&](Painter &p) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tVideoTile::PaintBackButton(\n\t\t\t\tp,\n\t\t\t\tbackRect.x(),\n\t\t\t\tbackRect.y(),\n\t\t\t\t_owner->widget()->width(),\n\t\t\t\t&_pinBackground);\n\t\t}, 1.f);\n\t}\n\n\tif (nameVisible) {\n\t\tconst auto &icon = st::groupCallVideoCrossLine.icon;\n\t\tconst auto iconLeft = x + width\n\t\t\t- st.iconPosition.x() - icon.width();\n\t\tconst auto iconTop = y + (height\n\t\t\t- st.iconPosition.y()\n\t\t\t- icon.height()\n\t\t\t+ nameShift);\n\t\tconst auto muteRect = QRect(\n\t\t\ticonLeft,\n\t\t\ticonTop,\n\t\t\ticon.width(),\n\t\t\ticon.height());\n\t\tif (!muteRect.isEmpty()) {\n\t\t\tdrawRasterOverlay(muteRect, [&](Painter &p) {\n\t\t\t\trow->paintMuteIcon(\n\t\t\t\t\tp,\n\t\t\t\t\tmuteRect,\n\t\t\t\t\tMembersRowStyle::Video);\n\t\t\t}, 1.f);\n\t\t}\n\n\t\tconst auto hasWidth = width\n\t\t\t- st.iconPosition.x() - icon.width()\n\t\t\t- st.namePosition.x();\n\t\tconst auto nameLeft = x + st.namePosition.x();\n\t\tconst auto nameRect = QRect(\n\t\t\tnameLeft,\n\t\t\tnameTop + nameShift,\n\t\t\thasWidth,\n\t\t\tst::semiboldFont->height);\n\t\tif (!nameRect.isEmpty()) {\n\t\t\tdrawRasterOverlay(nameRect, [&](Painter &p) {\n\t\t\t\tp.setPen(st::groupCallVideoTextFg);\n\t\t\t\trow->name().drawLeftElided(\n\t\t\t\t\tp,\n\t\t\t\t\tnameRect.x(),\n\t\t\t\t\tnameRect.y(),\n\t\t\t\t\tnameRect.width(),\n\t\t\t\t\tnameRect.x() + nameRect.width());\n\t\t\t}, 1.f);\n\t\t}\n\t}\n}\n\nvoid Viewport::RendererRhi::paintUsingRaster(\n\t\tQRect rect,\n\t\tFn<void(Painter&)> method,\n\t\tfloat opacity) {\n\tif (!_controlsPipeline || rect.isEmpty()) {\n\t\treturn;\n\t}\n\n\tconst auto size = rect.size() * _ifactor;\n\tauto raster = QImage(size, QImage::Format_ARGB32_Premultiplied);\n\traster.setDevicePixelRatio(_factor);\n\traster.fill(Qt::transparent);\n\t{\n\t\tauto painter = Painter(&raster);\n\t\tmethod(painter);\n\t}\n\n\tconst auto slot = _nextOnscreenSlot++;\n\tif (slot >= kMaxOnscreenDraws) {\n\t\treturn;\n\t}\n\tconst auto vOffset = slot * kOnscreenVertexSlot;\n\tconst auto uOffset = slot * kUniformSlot;\n\n\tauto *tex = acquirePoolTexture(size);\n\t_rub->uploadTexture(\n\t\ttex,\n\t\tQRhiTextureUploadDescription(\n\t\t\tQRhiTextureUploadEntry(0, 0,\n\t\t\t\tQRhiTextureSubresourceUploadDescription(raster))));\n\n\tImageUniforms imgUniforms{};\n\tconst auto pw = float(_rt->pixelSize().width());\n\tconst auto ph = float(_rt->pixelSize().height());\n\timgUniforms.viewport[0] = pw;\n\timgUniforms.viewport[1] = ph;\n\timgUniforms.g_opacity = opacity;\n\n\t_rub->updateDynamicBuffer(\n\t\t_uniformBuffer, uOffset, sizeof(imgUniforms), &imgUniforms);\n\n\tconst auto rRect = transformRect(rect);\n\tconst float coords[] = {\n\t\trRect.left(), rRect.bottom(),\n\t\t0.f, 0.f,\n\n\t\trRect.right(), rRect.bottom(),\n\t\t1.f, 0.f,\n\n\t\trRect.left(), rRect.top(),\n\t\t0.f, 1.f,\n\n\t\trRect.right(), rRect.top(),\n\t\t1.f, 1.f,\n\t};\n\t_rub->updateDynamicBuffer(\n\t\t_onscreenVertexBuffer, vOffset, sizeof(coords), coords);\n\n\tauto *srb = _rhi->newShaderResourceBindings();\n\t_perDrawSrbs.push_back(srb);\n\tsrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer,\n\t\t\tuOffset,\n\t\t\tsizeof(ImageUniforms)),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\ttex,\n\t\t\t_linearSampler),\n\t});\n\tsrb->create();\n\n\t_onscreenDraws.push_back({ _controlsPipeline, srb, vOffset });\n}\n\nRect Viewport::RendererRhi::transformRect(const QRect &raster) const {\n\treturn TransformRect(Rect(raster), _viewport, _factor);\n}\n\nRect Viewport::RendererRhi::transformRect(const Rect &raster) const {\n\treturn TransformRect(raster, _viewport, _factor);\n}\n\nbool Viewport::RendererRhi::isExpanded(\n\t\tnot_null<VideoTile*> tile,\n\t\tQSize unscaled,\n\t\tQSize tileSize) const {\n\treturn !tile->screencast()\n\t\t&& (!_owner->wide() || UseExpandForCamera(unscaled, tileSize));\n}\n\nfloat64 Viewport::RendererRhi::countExpandRatio(\n\t\tnot_null<VideoTile*> tile,\n\t\tQSize unscaled,\n\t\tconst TileAnimation &animation) const {\n\tconst auto expandedFrom = isExpanded(tile, unscaled, animation.from);\n\tconst auto expandedTo = isExpanded(tile, unscaled, animation.to);\n\treturn (expandedFrom && expandedTo)\n\t\t? 1.\n\t\t: (!expandedFrom && !expandedTo)\n\t\t? 0.\n\t\t: expandedFrom\n\t\t? (1. - animation.ratio)\n\t\t: animation.ratio;\n}\n\nvoid Viewport::RendererRhi::ensureButtonsImage() {\n\tif (_buttons) {\n\t\treturn;\n\t}\n\tconst auto pinOnSize = VideoTile::PinInnerSize(true);\n\tconst auto pinOffSize = VideoTile::PinInnerSize(false);\n\tconst auto backSize = VideoTile::BackInnerSize();\n\tconst auto muteSize = st::groupCallVideoCrossLine.icon.size();\n\tconst auto pausedSize = st::groupCallPaused.size();\n\n\tconst auto fullSize = QSize(\n\t\tstd::max({\n\t\t\tpinOnSize.width(),\n\t\t\tpinOffSize.width(),\n\t\t\tbackSize.width(),\n\t\t\t2 * muteSize.width(),\n\t\t\tpausedSize.width(),\n\t\t}),\n\t\t(pinOnSize.height()\n\t\t\t+ pinOffSize.height()\n\t\t\t+ backSize.height()\n\t\t\t+ muteSize.height()\n\t\t\t+ pausedSize.height()));\n\tconst auto imageSize = fullSize * _ifactor;\n\tauto image = _buttons.takeImage();\n\tif (image.size() != imageSize) {\n\t\timage = QImage(imageSize, QImage::Format_ARGB32_Premultiplied);\n\t}\n\timage.fill(Qt::transparent);\n\timage.setDevicePixelRatio(_ifactor);\n\t{\n\t\tauto p = Painter(&image);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\t_pinOn = QRect(QPoint(), pinOnSize * _ifactor);\n\t\tVideoTile::PaintPinButton(\n\t\t\tp, true, 0, 0, fullSize.width(),\n\t\t\t&_pinBackground, &_pinIcon);\n\n\t\tconst auto pinOffTop = pinOnSize.height();\n\t\t_pinOff = QRect(\n\t\t\tQPoint(0, pinOffTop) * _ifactor,\n\t\t\tpinOffSize * _ifactor);\n\t\tVideoTile::PaintPinButton(\n\t\t\tp, false, 0, pinOnSize.height(), fullSize.width(),\n\t\t\t&_pinBackground, &_pinIcon);\n\n\t\tconst auto backTop = pinOffTop + pinOffSize.height();\n\t\t_back = QRect(QPoint(0, backTop) * _ifactor, backSize * _ifactor);\n\t\tVideoTile::PaintBackButton(\n\t\t\tp, 0, backTop, fullSize.width(), &_pinBackground);\n\n\t\tconst auto muteTop = backTop + backSize.height();\n\t\t_muteOn = QRect(\n\t\t\tQPoint(0, muteTop) * _ifactor, muteSize * _ifactor);\n\t\t_muteIcon.paint(p, { 0, muteTop }, 1.);\n\n\t\t_muteOff = QRect(\n\t\t\tQPoint(muteSize.width(), muteTop) * _ifactor,\n\t\t\tmuteSize * _ifactor);\n\t\t_muteIcon.paint(p, { muteSize.width(), muteTop }, 0.);\n\n\t\tconst auto pausedTop = muteTop + muteSize.height();\n\t\t_pausedIcon = QRect(\n\t\t\tQPoint(0, pausedTop) * _ifactor,\n\t\t\tpausedSize * _ifactor);\n\t\tst::groupCallPaused.paint(p, 0, pausedTop, fullSize.width());\n\t}\n\t_buttons.setImage(std::move(image));\n}\n\nvoid Viewport::RendererRhi::validateDatas() {\n\tconst auto &tiles = _owner->_tiles;\n\tconst auto count = int(tiles.size());\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto nameHeight = st::semiboldFont->height * factor;\n\n\tfor (auto &data : _tileData) {\n\t\tdata.stale = true;\n\t}\n\t_tileDataIndices.resize(count);\n\tfor (auto i = 0; i != count; ++i) {\n\t\ttiles[i]->row()->lazyInitialize(st::groupCallMembersListItem);\n\t\tconst auto id = quintptr(tiles[i]->track().get());\n\t\tconst auto j = ranges::find(_tileData, id, &TileData::id);\n\t\tif (j != end(_tileData)) {\n\t\t\tj->stale = false;\n\t\t\t_tileDataIndices[i] = int(j - begin(_tileData));\n\t\t\tconst auto peer = tiles[i]->peer();\n\t\t\tif (j->peer != peer\n\t\t\t\t|| j->nameVersion != peer->nameVersion()) {\n\t\t\t\tj->peer = peer;\n\t\t\t\tj->nameVersion = peer->nameVersion();\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto peer = tiles[i]->peer();\n\t\t\tconst auto paused = (tiles[i]->track()->state()\n\t\t\t\t== Webrtc::VideoState::Paused);\n\t\t\t_tileDataIndices[i] = int(_tileData.size());\n\t\t\t_tileData.push_back({\n\t\t\t\t.id = id,\n\t\t\t\t.peer = peer,\n\t\t\t\t.nameVersion = peer->nameVersion(),\n\t\t\t\t.pause = paused,\n\t\t\t});\n\t\t}\n\t}\n\tfor (auto it = _tileData.begin(); it != _tileData.end();) {\n\t\tif (it->stale) {\n\t\t\tdelete it->rgbaTexture;\n\t\t\tdelete it->yTexture;\n\t\t\tdelete it->uTexture;\n\t\t\tdelete it->vTexture;\n\t\t\tdelete it->downscaleTexture;\n\t\t\tdelete it->downscaleRpDesc;\n\t\t\tdelete it->downscaleRt;\n\t\t\tdelete it->blurHTexture;\n\t\t\tdelete it->blurHRpDesc;\n\t\t\tdelete it->blurHRt;\n\t\t\tdelete it->blurVTexture;\n\t\t\tdelete it->blurVRpDesc;\n\t\t\tdelete it->blurVRt;\n\t\t\tit = _tileData.erase(it);\n\t\t} else {\n\t\t\t++it;\n\t\t}\n\t}\n}\n\nvoid Viewport::RendererRhi::validateOutlineAnimation(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &data) {\n\tconst auto outline = tile->row()->speaking();\n\tif (data.outline == outline) {\n\t\treturn;\n\t}\n\tdata.outline = outline;\n\tdata.outlined.start(\n\t\t[=] { _owner->widget()->update(); },\n\t\toutline ? 0. : 1.,\n\t\toutline ? 1. : 0.,\n\t\tst::fadeWrapDuration);\n}\n\nvoid Viewport::RendererRhi::validatePausedAnimation(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &data) {\n\tconst auto paused = (_userpicFrame\n\t\t&& tile->track()->frameSize().isEmpty())\n\t\t|| (tile->track()->state() == Webrtc::VideoState::Paused);\n\tif (data.pause == paused) {\n\t\treturn;\n\t}\n\tdata.pause = paused;\n\tdata.paused.start(\n\t\t[=] { _owner->widget()->update(); },\n\t\tpaused ? 0. : 1.,\n\t\tpaused ? 1. : 0.,\n\t\tst::fadeWrapDuration);\n}\n\nvoid Viewport::RendererRhi::validateUserpicFrame(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &data) {\n\tif (!_userpicFrame) {\n\t\tdata.userpicFrame = QImage();\n\t\treturn;\n\t} else if (!data.userpicFrame.isNull()) {\n\t\treturn;\n\t}\n\tconst auto size = tile->trackOrUserpicSize();\n\tdata.userpicFrame = PeerData::GenerateUserpicImage(\n\t\ttile->peer(),\n\t\ttile->row()->ensureUserpicView(),\n\t\tsize.width(),\n\t\t0);\n}\n\n} // namespace Calls::Group\n\n#endif // Qt >= 6.7\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_viewport_rhi.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"calls/group/calls_group_viewport.h\"\n#include \"ui/gl/gl_surface.h\"\n#include \"ui/rhi/rhi_renderer.h\"\n#include \"ui/rhi/rhi_image.h\"\n#include \"ui/round_rect.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/cross_line.h\"\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\nclass QRhi;\nclass QRhiBuffer;\nclass QRhiTexture;\nclass QRhiSampler;\nclass QRhiGraphicsPipeline;\nclass QRhiShaderResourceBindings;\nclass QRhiResourceUpdateBatch;\nclass QRhiTextureRenderTarget;\nclass QRhiRenderPassDescriptor;\n\nnamespace Webrtc {\nstruct FrameWithInfo;\n} // namespace Webrtc\n\nnamespace Calls::Group {\n\nclass Viewport::RendererRhi final\n\t: public Ui::GL::Renderer\n\t, public Ui::Rhi::Renderer {\npublic:\n\texplicit RendererRhi(not_null<Viewport*> owner);\n\t~RendererRhi();\n\n\tvoid initialize(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) override;\n\tvoid render(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) override;\n\tvoid renderOffscreen(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) override;\n\tvoid renderOnscreen(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) override;\n\tvoid releaseResources() override;\n\n\tQColor rhiClearColor() override;\n\n\tstd::optional<QColor> clearColor() override;\n\nprivate:\n\tstruct TileData {\n\t\tquintptr id = 0;\n\t\tnot_null<PeerData*> peer;\n\t\tUi::Animations::Simple outlined;\n\t\tUi::Animations::Simple paused;\n\t\tQImage userpicFrame;\n\t\tQRect nameRect;\n\t\tint nameVersion = 0;\n\t\tmutable int trackIndex = -1;\n\n\t\tQRhiTexture *rgbaTexture = nullptr;\n\t\tQSize rgbaSize;\n\n\t\tQRhiTexture *yTexture = nullptr;\n\t\tQRhiTexture *uTexture = nullptr;\n\t\tQRhiTexture *vTexture = nullptr;\n\t\tQSize lumaSize;\n\t\tQSize chromaSize;\n\n\t\tQRhiTexture *convertedTexture = nullptr;\n\t\tQRhiTextureRenderTarget *convertedRt = nullptr;\n\t\tQRhiRenderPassDescriptor *convertedRpDesc = nullptr;\n\t\tQSize convertedSize;\n\n\t\tQRhiTexture *downscaleTexture = nullptr;\n\t\tQRhiTextureRenderTarget *downscaleRt = nullptr;\n\t\tQRhiRenderPassDescriptor *downscaleRpDesc = nullptr;\n\n\t\tQRhiTexture *blurHTexture = nullptr;\n\t\tQRhiTextureRenderTarget *blurHRt = nullptr;\n\t\tQRhiRenderPassDescriptor *blurHRpDesc = nullptr;\n\n\t\tQRhiTexture *blurVTexture = nullptr;\n\t\tQRhiTextureRenderTarget *blurVRt = nullptr;\n\t\tQRhiRenderPassDescriptor *blurVRpDesc = nullptr;\n\n\t\tQSize blurSize;\n\t\tbool stale = false;\n\t\tbool pause = false;\n\t\tbool outline = false;\n\t};\n\n\tstruct PoolTexture {\n\t\tQRhiTexture *texture = nullptr;\n\t\tQSize size;\n\t};\n\n\tvoid createPipelines();\n\tvoid ensureNoiseTexture();\n\tvoid validateDatas();\n\tvoid validateOutlineAnimation(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &data);\n\tvoid validatePausedAnimation(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &data);\n\tvoid validateUserpicFrame(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &data);\n\tvoid ensureButtonsImage();\n\n\tvoid paintTileOffscreen(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &tileData);\n\tvoid paintTileOnscreen(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &tileData,\n\t\tfloat pw,\n\t\tfloat ph);\n\tvoid uploadFrame(\n\t\tconst Webrtc::FrameWithInfo &data,\n\t\tTileData &tileData);\n\tvoid drawYuv2RgbPass(\n\t\tTileData &tileData,\n\t\tQSize frameSize);\n\tvoid prepareOffscreenTargets(\n\t\tTileData &tileData,\n\t\tQSize blurSize);\n\tvoid drawDownscalePass(\n\t\tTileData &tileData,\n\t\tQSize blurSize);\n\tvoid drawBlurPass(\n\t\tTileData &tileData,\n\t\tQSize blurSize);\n\tvoid drawFramePass(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &tileData,\n\t\tQSize blurSize);\n\tvoid drawControls(\n\t\tnot_null<VideoTile*> tile,\n\t\tTileData &tileData);\n\n\tvoid paintUsingRaster(\n\t\tQRect rect,\n\t\tFn<void(Painter&)> method,\n\t\tfloat opacity = 1.f);\n\n\t[[nodiscard]] QRhiTexture *acquirePoolTexture(QSize size);\n\t[[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const;\n\t[[nodiscard]] Ui::GL::Rect transformRect(\n\t\tconst Ui::GL::Rect &raster) const;\n\n\t[[nodiscard]] bool isExpanded(\n\t\tnot_null<VideoTile*> tile,\n\t\tQSize unscaled,\n\t\tQSize tileSize) const;\n\t[[nodiscard]] float64 countExpandRatio(\n\t\tnot_null<VideoTile*> tile,\n\t\tQSize unscaled,\n\t\tconst TileAnimation &animation) const;\n\n\tconst not_null<Viewport*> _owner;\n\n\tQRhi *_rhi = nullptr;\n\tQRhiRenderTarget *_rt = nullptr;\n\tQRhiCommandBuffer *_cb = nullptr;\n\tQRhiResourceUpdateBatch *_rub = nullptr;\n\tQSize _viewport;\n\tfloat _factor = 1.f;\n\tint _ifactor = 1;\n\tbool _rgbaFrame = false;\n\tbool _userpicFrame = false;\n\n\tstatic constexpr int kMaxOnscreenDraws = 64;\n\tstatic constexpr int kOnscreenVertexSlot = 128;\n\tstatic constexpr int kUniformSlot = 256;\n\n\tstruct OnscreenDraw {\n\t\tQRhiGraphicsPipeline *pipeline = nullptr;\n\t\tQRhiShaderResourceBindings *srb = nullptr;\n\t\tint vertexOffset = 0;\n\t};\n\tstd::vector<OnscreenDraw> _onscreenDraws;\n\tint _nextOnscreenSlot = 0;\n\n\tQRhiBuffer *_offscreenVertexBuffer = nullptr;\n\tQRhiBuffer *_onscreenVertexBuffer = nullptr;\n\tQRhiBuffer *_uniformBuffer = nullptr;\n\tQRhiSampler *_linearSampler = nullptr;\n\tQRhiSampler *_nearestSampler = nullptr;\n\tQRhiSampler *_noiseRepeatSampler = nullptr;\n\tQRhiTexture *_placeholderTexture = nullptr;\n\n\tQRhiGraphicsPipeline *_downscaleArgb32Pipeline = nullptr;\n\tQRhiGraphicsPipeline *_downscaleYuv420Pipeline = nullptr;\n\tQRhiGraphicsPipeline *_blurHPipeline = nullptr;\n\tQRhiGraphicsPipeline *_blurVPipeline = nullptr;\n\tQRhiGraphicsPipeline *_framePipeline = nullptr;\n\tQRhiGraphicsPipeline *_controlsPipeline = nullptr;\n\n\tQRhiShaderResourceBindings *_downscaleArgb32Srb = nullptr;\n\tQRhiShaderResourceBindings *_downscaleYuv420Srb = nullptr;\n\tQRhiShaderResourceBindings *_blurHSrb = nullptr;\n\n\tQRhiRenderPassDescriptor *_offscreenRpDesc = nullptr;\n\n\tQRhiTexture *_noiseTexture = nullptr;\n\n\tUi::Rhi::Image _buttons;\n\tQRect _pinOn;\n\tQRect _pinOff;\n\tQRect _back;\n\tQRect _muteOn;\n\tQRect _muteOff;\n\tQRect _pausedIcon;\n\n\tUi::Rhi::Image _names;\n\tQRect _pausedTextRect;\n\tstd::vector<TileData> _tileData;\n\tstd::vector<int> _tileDataIndices;\n\n\tstd::vector<PoolTexture> _texturePool;\n\tint _nextPoolIndex = 0;\n\n\tstd::vector<QRhiShaderResourceBindings*> _perDrawSrbs;\n\n\tUi::CrossLineAnimation _pinIcon;\n\tUi::CrossLineAnimation _muteIcon;\n\n\tUi::RoundRect _pinBackground;\n\n\tbool _initialized = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Calls::Group\n\n#endif // Qt >= 6.7\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_group_viewport_tile.h\"\n\n#include \"calls/group/calls_group_members_row.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/round_rect.h\"\n#include \"ui/painter.h\"\n#include \"ui/effects/cross_line.h\"\n#include \"styles/style_calls.h\"\n\n#include <QtGui/QOpenGLFunctions>\n\nnamespace Calls::Group {\nnamespace {\n\nconstexpr auto kPausedVideoSize = 90;\n\n} // namespace\n\nViewport::VideoTile::VideoTile(\n\tconst VideoEndpoint &endpoint,\n\tVideoTileTrack track,\n\trpl::producer<QSize> trackSize,\n\trpl::producer<bool> pinned,\n\tFn<void()> update,\n\tbool self)\n: _endpoint(endpoint)\n, _update(std::move(update))\n, _track(std::move(track))\n, _peer(_track.row->peer())\n, _trackSize(std::move(trackSize))\n, _rtmp(endpoint.rtmp())\n, _self(self) {\n\tExpects(_track.track != nullptr);\n\n\tusing namespace rpl::mappers;\n\t_track.track->stateValue(\n\t) | rpl::filter(\n\t\t_1 == Webrtc::VideoState::Paused\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t_wasPaused = true;\n\t}, _lifetime);\n\n\tsetup(std::move(pinned));\n}\n\nbool Viewport::VideoTile::mirror() const {\n\treturn _self && (_endpoint.type == VideoEndpointType::Camera);\n}\n\nQRect Viewport::VideoTile::pinOuter() const {\n\treturn _pinOuter;\n}\n\nQRect Viewport::VideoTile::pinInner() const {\n\treturn _pinInner.translated(0, -topControlsSlide());\n}\n\nQRect Viewport::VideoTile::backOuter() const {\n\treturn _backOuter;\n}\n\nQRect Viewport::VideoTile::backInner() const {\n\treturn _backInner.translated(0, -topControlsSlide());\n}\n\nint Viewport::VideoTile::topControlsSlide() const {\n\treturn anim::interpolate(\n\t\tst::groupCallVideoTile.pinPosition.y() + _pinInner.height(),\n\t\t0,\n\t\t_topControlsShownAnimation.value(_topControlsShown ? 1. : 0.));\n}\n\nQSize Viewport::VideoTile::PausedVideoSize() {\n\treturn QSize(kPausedVideoSize, kPausedVideoSize);\n}\n\nQSize Viewport::VideoTile::trackOrUserpicSize() const {\n\tif (const auto size = trackSize(); !size.isEmpty()) {\n\t\treturn size;\n\t}\n\treturn _wasPaused ? PausedVideoSize() : QSize();\n}\n\nbool Viewport::VideoTile::screencast() const {\n\treturn (_endpoint.type == VideoEndpointType::Screen);\n}\n\nvoid Viewport::VideoTile::setGeometry(\n\t\tQRect geometry,\n\t\tTileAnimation animation) {\n\t_hidden = false;\n\t_geometry = geometry;\n\t_animation = animation;\n\tupdateTopControlsPosition();\n}\n\nvoid Viewport::VideoTile::hide() {\n\t_hidden = true;\n\t_quality = std::nullopt;\n}\n\nvoid Viewport::VideoTile::toggleTopControlsShown(bool shown) {\n\tif (_topControlsShown == shown) {\n\t\treturn;\n\t}\n\t_topControlsShown = shown;\n\t_topControlsShownAnimation.start(\n\t\t_update,\n\t\tshown ? 0. : 1.,\n\t\tshown ? 1. : 0.,\n\t\tst::slideWrapDuration);\n}\n\nbool Viewport::VideoTile::updateRequestedQuality(VideoQuality quality) {\n\tif (_hidden) {\n\t\t_quality = std::nullopt;\n\t\treturn false;\n\t} else if (_quality && *_quality == quality) {\n\t\treturn false;\n\t}\n\t_quality = quality;\n\treturn true;\n}\n\nQSize Viewport::VideoTile::PinInnerSize(bool pinned) {\n\tconst auto &st = st::groupCallVideoTile;\n\tconst auto &icon = st::groupCallVideoTile.pin.icon;\n\tconst auto innerWidth = icon.width()\n\t\t+ st.pinTextPosition.x()\n\t\t+ st::semiboldFont->width(pinned\n\t\t\t? tr::lng_pinned_unpin(tr::now)\n\t\t\t: tr::lng_pinned_pin(tr::now));\n\tconst auto innerHeight = icon.height();\n\tconst auto buttonWidth = st.pinPadding.left()\n\t\t+ innerWidth\n\t\t+ st.pinPadding.right();\n\tconst auto buttonHeight = st.pinPadding.top()\n\t\t+ innerHeight\n\t\t+ st.pinPadding.bottom();\n\treturn { buttonWidth, buttonHeight };\n}\n\nvoid Viewport::VideoTile::PaintPinButton(\n\t\tPainter &p,\n\t\tbool pinned,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tnot_null<Ui::RoundRect*> background,\n\t\tnot_null<Ui::CrossLineAnimation*> icon) {\n\tconst auto &st = st::groupCallVideoTile;\n\tconst auto rect = QRect(QPoint(x, y), PinInnerSize(pinned));\n\tbackground->paint(p, rect);\n\ticon->paint(\n\t\tp,\n\t\trect.marginsRemoved(st.pinPadding).topLeft(),\n\t\tpinned ? 1. : 0.);\n\tp.setPen(st::groupCallVideoTextFg);\n\tp.setFont(st::semiboldFont);\n\tp.drawTextLeft(\n\t\t(x\n\t\t\t+ st.pinPadding.left()\n\t\t\t+ st::groupCallVideoTile.pin.icon.width()\n\t\t\t+ st.pinTextPosition.x()),\n\t\t(y\n\t\t\t+ st.pinPadding.top()\n\t\t\t+ st.pinTextPosition.y()),\n\t\touterWidth,\n\t\t(pinned\n\t\t\t? tr::lng_pinned_unpin(tr::now)\n\t\t\t: tr::lng_pinned_pin(tr::now)));\n\n}\n\nQSize Viewport::VideoTile::BackInnerSize() {\n\tconst auto &st = st::groupCallVideoTile;\n\tconst auto &icon = st::groupCallVideoTile.back;\n\tconst auto innerWidth = icon.width()\n\t\t+ st.pinTextPosition.x()\n\t\t+ st::semiboldFont->width(tr::lng_create_group_back(tr::now));\n\tconst auto innerHeight = icon.height();\n\tconst auto buttonWidth = st.pinPadding.left()\n\t\t+ innerWidth\n\t\t+ st.pinPadding.right();\n\tconst auto buttonHeight = st.pinPadding.top()\n\t\t+ innerHeight\n\t\t+ st.pinPadding.bottom();\n\treturn { buttonWidth, buttonHeight };\n}\n\nvoid Viewport::VideoTile::PaintBackButton(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tnot_null<Ui::RoundRect*> background) {\n\tconst auto &st = st::groupCallVideoTile;\n\tconst auto rect = QRect(QPoint(x, y), BackInnerSize());\n\tbackground->paint(p, rect);\n\tst.back.paint(\n\t\tp,\n\t\trect.marginsRemoved(st.pinPadding).topLeft(),\n\t\touterWidth);\n\tp.setPen(st::groupCallVideoTextFg);\n\tp.setFont(st::semiboldFont);\n\tp.drawTextLeft(\n\t\t(x\n\t\t\t+ st.pinPadding.left()\n\t\t\t+ st::groupCallVideoTile.pin.icon.width()\n\t\t\t+ st.pinTextPosition.x()),\n\t\t(y\n\t\t\t+ st.pinPadding.top()\n\t\t\t+ st.pinTextPosition.y()),\n\t\touterWidth,\n\t\ttr::lng_create_group_back(tr::now));\n}\n\nvoid Viewport::VideoTile::updateTopControlsSize() {\n\tconst auto &st = st::groupCallVideoTile;\n\n\tconst auto pinSize = PinInnerSize(_pinned);\n\tconst auto pinWidth = st.pinPosition.x() * 2 + pinSize.width();\n\tconst auto pinHeight = st.pinPosition.y() * 2 + pinSize.height();\n\t_pinInner = QRect(QPoint(), pinSize);\n\t_pinOuter = QRect(0, 0, pinWidth, pinHeight);\n\n\tconst auto backSize = BackInnerSize();\n\tconst auto backWidth = st.pinPosition.x() * 2 + backSize.width();\n\tconst auto backHeight = st.pinPosition.y() * 2 + backSize.height();\n\t_backInner = QRect(QPoint(), backSize);\n\t_backOuter = QRect(0, 0, backWidth, backHeight);\n}\n\nvoid Viewport::VideoTile::updateTopControlsPosition() {\n\tconst auto &st = st::groupCallVideoTile;\n\n\t_pinInner = QRect(\n\t\t_geometry.width() - st.pinPosition.x() - _pinInner.width(),\n\t\tst.pinPosition.y(),\n\t\t_pinInner.width(),\n\t\t_pinInner.height());\n\t_pinOuter = QRect(\n\t\t_geometry.width() - _pinOuter.width(),\n\t\t0,\n\t\t_pinOuter.width(),\n\t\t_pinOuter.height());\n\t_backInner = QRect(st.pinPosition, _backInner.size());\n}\n\nvoid Viewport::VideoTile::setup(rpl::producer<bool> pinned) {\n\tstd::move(\n\t\tpinned\n\t) | rpl::filter([=](bool pinned) {\n\t\treturn (_pinned != pinned);\n\t}) | rpl::on_next([=](bool pinned) {\n\t\t_pinned = pinned;\n\t\tupdateTopControlsSize();\n\t\tif (!_hidden) {\n\t\t\tupdateTopControlsPosition();\n\t\t\t_update();\n\t\t}\n\t}, _lifetime);\n\n\t_track.track->renderNextFrame(\n\t) | rpl::on_next(_update, _lifetime);\n\n\tupdateTopControlsSize();\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"calls/group/calls_group_viewport.h\"\n#include \"calls/group/calls_group_call.h\"\n#include \"ui/effects/animations.h\"\n\nclass Painter;\nclass QOpenGLFunctions;\n\nnamespace Ui {\nclass CrossLineAnimation;\nclass RoundRect;\n} // namespace Ui\n\nnamespace Calls::Group {\n\nclass Viewport::VideoTile final {\npublic:\n\tVideoTile(\n\t\tconst VideoEndpoint &endpoint,\n\t\tVideoTileTrack track,\n\t\trpl::producer<QSize> trackSize,\n\t\trpl::producer<bool> pinned,\n\t\tFn<void()> update,\n\t\tbool self);\n\n\t[[nodiscard]] not_null<Webrtc::VideoTrack*> track() const {\n\t\treturn _track.track;\n\t}\n\t[[nodiscard]] not_null<MembersRow*> row() const {\n\t\treturn _track.row;\n\t}\n\t[[nodiscard]] not_null<PeerData*> peer() const {\n\t\treturn _peer;\n\t}\n\t[[nodiscard]] bool rtmp() const {\n\t\treturn _rtmp;\n\t}\n\t[[nodiscard]] QRect geometry() const {\n\t\treturn _geometry;\n\t}\n\t[[nodiscard]] TileAnimation animation() const {\n\t\treturn _animation;\n\t}\n\t[[nodiscard]] bool pinned() const {\n\t\treturn _pinned;\n\t}\n\t[[nodiscard]] bool hidden() const {\n\t\treturn _hidden;\n\t}\n\t[[nodiscard]] bool visible() const {\n\t\treturn !_hidden && !_geometry.isEmpty();\n\t}\n\t[[nodiscard]] bool self() const {\n\t\treturn _self;\n\t}\n\t[[nodiscard]] bool mirror() const;\n\t[[nodiscard]] QRect pinOuter() const;\n\t[[nodiscard]] QRect pinInner() const;\n\t[[nodiscard]] QRect backOuter() const;\n\t[[nodiscard]] QRect backInner() const;\n\t[[nodiscard]] const VideoEndpoint &endpoint() const {\n\t\treturn _endpoint;\n\t}\n\t[[nodiscard]] QSize trackSize() const {\n\t\treturn _trackSize.current();\n\t}\n\t[[nodiscard]] rpl::producer<QSize> trackSizeValue() const {\n\t\treturn _trackSize.value();\n\t}\n\t[[nodiscard]] QSize trackOrUserpicSize() const;\n\t[[nodiscard]] static QSize PausedVideoSize();\n\n\t[[nodiscard]] bool screencast() const;\n\tvoid setGeometry(\n\t\tQRect geometry,\n\t\tTileAnimation animation = TileAnimation());\n\tvoid hide();\n\tvoid toggleTopControlsShown(bool shown);\n\tbool updateRequestedQuality(VideoQuality quality);\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\n\t[[nodiscard]] static QSize PinInnerSize(bool pinned);\n\tstatic void PaintPinButton(\n\t\tPainter &p,\n\t\tbool pinned,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tnot_null<Ui::RoundRect*> background,\n\t\tnot_null<Ui::CrossLineAnimation*> icon);\n\n\t[[nodiscard]] static QSize BackInnerSize();\n\tstatic void PaintBackButton(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tnot_null<Ui::RoundRect*> background);\n\nprivate:\n\tvoid setup(rpl::producer<bool> pinned);\n\t[[nodiscard]] int topControlsSlide() const;\n\tvoid updateTopControlsSize();\n\tvoid updateTopControlsPosition();\n\n\tconst VideoEndpoint _endpoint;\n\tconst Fn<void()> _update;\n\tconst VideoTileTrack _track;\n\tconst not_null<PeerData*> _peer;\n\n\tQRect _geometry;\n\tTileAnimation _animation;\n\trpl::variable<QSize> _trackSize;\n\tQRect _pinOuter;\n\tQRect _pinInner;\n\tQRect _backOuter;\n\tQRect _backInner;\n\tUi::Animations::Simple _topControlsShownAnimation;\n\tbool _wasPaused = false;\n\tbool _topControlsShown = false;\n\tbool _pinned = false;\n\tbool _hidden = true;\n\tbool _rtmp = false;\n\tbool _self = false;\n\tstd::optional<VideoQuality> _quality;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_volume_item.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/calls_volume_item.h\"\n\n#include \"calls/group/calls_group_common.h\"\n#include \"ui/color_int_conversion.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/effects/cross_line.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/rect.h\"\n#include \"styles/style_calls.h\"\n\n#include \"ui/paint/arcs.h\"\n\nnamespace Calls {\nnamespace {\n\nconstexpr auto kMaxVolumePercent = 200;\n\nconst auto kSpeakerThreshold = std::vector<float>{\n\t10.0f / kMaxVolumePercent,\n\t50.0f / kMaxVolumePercent,\n\t150.0f / kMaxVolumePercent };\n\nconstexpr auto kVolumeStickedValues\n\t= std::array<std::pair<float64, float64>, 7>{{\n\t\t{ 25. / kMaxVolumePercent, 2. / kMaxVolumePercent },\n\t\t{ 50. / kMaxVolumePercent, 2. / kMaxVolumePercent },\n\t\t{ 75. / kMaxVolumePercent, 2. / kMaxVolumePercent },\n\t\t{ 100. / kMaxVolumePercent, 10. / kMaxVolumePercent },\n\t\t{ 125. / kMaxVolumePercent, 2. / kMaxVolumePercent },\n\t\t{ 150. / kMaxVolumePercent, 2. / kMaxVolumePercent },\n\t\t{ 175. / kMaxVolumePercent, 2. / kMaxVolumePercent },\n\t}};\n\n} // namespace\n\nMenuVolumeItem::MenuVolumeItem(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::Menu &st,\n\tconst style::MediaSlider &stSlider,\n\trpl::producer<Group::ParticipantState> participantState,\n\tint startVolume,\n\tint maxVolume,\n\tbool muted,\n\tconst QMargins &padding)\n: Ui::Menu::ItemBase(parent, st)\n, _maxVolume(maxVolume)\n, _cloudMuted(muted)\n, _localMuted(muted)\n, _slider(base::make_unique_q<Ui::MediaSlider>(this, stSlider))\n, _dummyAction(new QAction(parent))\n, _st(st)\n, _stCross(st::groupCallMuteCrossLine)\n, _padding(padding)\n, _crossLineMute(std::make_unique<Ui::CrossLineAnimation>(_stCross, true))\n, _arcs(std::make_unique<Ui::Paint::ArcsAnimation>(\n\tst::groupCallSpeakerArcsAnimation,\n\tkSpeakerThreshold,\n\t_localMuted ? 0. : (startVolume / float(maxVolume)),\n\tUi::Paint::ArcsAnimation::Direction::Right)) {\n\n\tfitToMenuWidth();\n\tenableMouseSelecting();\n\tenableMouseSelecting(_slider.get());\n\n\t_slider->setAlwaysDisplayMarker(true);\n\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\tconst auto geometry = QRect(QPoint(), size);\n\t\t_itemRect = geometry - _padding;\n\t\t_speakerRect = QRect(_itemRect.topLeft(), _stCross.icon.size());\n\t\t_arcPosition = _speakerRect.center()\n\t\t\t+ QPoint(0, st::groupCallMenuSpeakerArcsSkip);\n\t\t_slider->setGeometry(\n\t\t\tst::groupCallMenuVolumeMargin.left(),\n\t\t\t_speakerRect.y(),\n\t\t\t(geometry.width()\n\t\t\t\t- st::groupCallMenuVolumeMargin.left()\n\t\t\t\t- st::groupCallMenuVolumeMargin.right()),\n\t\t\t_speakerRect.height());\n\t}, lifetime());\n\n\tsetCloudVolume(startVolume);\n\n\tpaintRequest(\n\t) | rpl::on_next([=](const QRect &clip) {\n\t\tauto p = QPainter(this);\n\n\t\tconst auto volume = _localMuted\n\t\t\t? 0\n\t\t\t: base::SafeRound(_slider->value() * kMaxVolumePercent);\n\t\tconst auto muteProgress\n\t\t\t= _crossLineAnimation.value((!volume) ? 1. : 0.);\n\n\t\tconst auto selected = isSelected();\n\t\tp.fillRect(clip, selected ? st.itemBgOver : st.itemBg);\n\n\t\tconst auto mutePen = anim::color(\n\t\t\tunmuteColor(),\n\t\t\tmuteColor(),\n\t\t\tmuteProgress);\n\n\t\t_crossLineMute->paint(\n\t\t\tp,\n\t\t\t_speakerRect.topLeft(),\n\t\t\tmuteProgress,\n\t\t\t(muteProgress > 0) ? std::make_optional(mutePen) : std::nullopt);\n\n\t\t{\n\t\t\tp.translate(_arcPosition);\n\t\t\t_arcs->paint(p);\n\t\t}\n\t}, lifetime());\n\n\t_slider->setChangeProgressCallback([=](float64 value) {\n\t\tconst auto newMuted = (value == 0);\n\t\tif (_localMuted != newMuted) {\n\t\t\t_localMuted = newMuted;\n\t\t\t_toggleMuteLocallyRequests.fire_copy(newMuted);\n\n\t\t\t_crossLineAnimation.start(\n\t\t\t\t[=] { update(_speakerRect); },\n\t\t\t\t_localMuted ? 0. : 1.,\n\t\t\t\t_localMuted ? 1. : 0.,\n\t\t\t\tst::callPanelDuration);\n\t\t}\n\t\tif (value > 0) {\n\t\t\t_changeVolumeLocallyRequests.fire(value * _maxVolume);\n\t\t}\n\t\t_arcs->setValue(value);\n\t\tupdateSliderColor(value);\n\t});\n\n\tconst auto returnVolume = [=] {\n\t\t_changeVolumeLocallyRequests.fire_copy(_cloudVolume);\n\t};\n\n\t_slider->setChangeFinishedCallback([=](float64 value) {\n\t\tconst auto newVolume = base::SafeRound(value * _maxVolume);\n\t\tconst auto muted = (value == 0);\n\n\t\tif (!_cloudMuted && muted) {\n\t\t\treturnVolume();\n\t\t\t_localMuted = true;\n\t\t\t_toggleMuteRequests.fire(true);\n\t\t}\n\t\tif (_cloudMuted && muted) {\n\t\t\treturnVolume();\n\t\t}\n\t\tif (_cloudMuted && !muted) {\n\t\t\t_waitingForUpdateVolume = true;\n\t\t\t_localMuted = false;\n\t\t\t_toggleMuteRequests.fire(false);\n\t\t}\n\t\tif (!_cloudMuted && !muted) {\n\t\t\t_changeVolumeRequests.fire_copy(newVolume);\n\t\t}\n\t\tupdateSliderColor(value);\n\t});\n\n\tstd::move(\n\t\tparticipantState\n\t) | rpl::on_next([=](const Group::ParticipantState &state) {\n\t\tconst auto newMuted = state.mutedByMe;\n\t\tconst auto newVolume = state.volume.value_or(0);\n\n\t\t_cloudMuted = _localMuted = newMuted;\n\n\t\tif (!newVolume) {\n\t\t\treturn;\n\t\t}\n\t\tif (_waitingForUpdateVolume) {\n\t\t\tconst auto localVolume\n\t\t\t\t= base::SafeRound(_slider->value() * _maxVolume);\n\t\t\tif ((localVolume != newVolume)\n\t\t\t\t&& (_cloudVolume == newVolume)) {\n\t\t\t\t_changeVolumeRequests.fire(int(localVolume));\n\t\t\t}\n\t\t} else {\n\t\t\tsetCloudVolume(newVolume);\n\t\t}\n\t\t_waitingForUpdateVolume = false;\n\t}, lifetime());\n\n\t_slider->setAdjustCallback([=](float64 value) {\n\t\tfor (const auto &snap : kVolumeStickedValues) {\n\t\t\tif (value > (snap.first - snap.second)\n\t\t\t\t&& value < (snap.first + snap.second)) {\n\t\t\t\treturn snap.first;\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t});\n\n\tinitArcsAnimation();\n}\n\nvoid MenuVolumeItem::initArcsAnimation() {\n\tconst auto lastTime = lifetime().make_state<int>(0);\n\t_arcsAnimation.init([=](crl::time now) {\n\t\t_arcs->update(now);\n\t\tupdate(_speakerRect);\n\t});\n\n\t_arcs->startUpdateRequests(\n\t) | rpl::on_next([=] {\n\t\tif (!_arcsAnimation.animating()) {\n\t\t\t*lastTime = crl::now();\n\t\t\t_arcsAnimation.start();\n\t\t}\n\t}, lifetime());\n\n\t_arcs->stopUpdateRequests(\n\t) | rpl::on_next([=] {\n\t\t_arcsAnimation.stop();\n\t}, lifetime());\n}\n\nQColor MenuVolumeItem::unmuteColor() const {\n\treturn (isSelected()\n\t\t? _st.itemFgOver\n\t\t: isEnabled()\n\t\t? _st.itemFg\n\t\t: _st.itemFgDisabled)->c;\n}\n\nQColor MenuVolumeItem::muteColor() const {\n\treturn (isSelected()\n\t\t? st::attentionButtonFgOver\n\t\t: st::attentionButtonFg)->c;\n}\n\nvoid MenuVolumeItem::setCloudVolume(int volume) {\n\tif (_cloudVolume == volume) {\n\t\treturn;\n\t}\n\t_cloudVolume = volume;\n\tif (!_slider->isChanging()) {\n\t\tsetSliderVolume(_cloudMuted ? 0. : volume);\n\t}\n}\n\nvoid MenuVolumeItem::setSliderVolume(int volume) {\n\tconst auto value = float64(volume) / _maxVolume;\n\t_slider->setValue(value);\n\tupdateSliderColor(value);\n}\n\nvoid MenuVolumeItem::updateSliderColor(float64 value) {\n\tvalue = std::clamp(value, 0., 1.);\n\tconst auto colors = std::array<QColor, 4>{ {\n\t\tUi::ColorFromSerialized(0xF66464),\n\t\tUi::ColorFromSerialized(0xD0B738),\n\t\tUi::ColorFromSerialized(0x24CD80),\n\t\tUi::ColorFromSerialized(0x3BBCEC),\n\t} };\n\t_slider->setColorOverrides({\n\t\t.activeFg = (value < 0.25)\n\t\t\t? anim::color(colors[0], colors[1], value / 0.25)\n\t\t\t: (value < 0.5)\n\t\t\t? anim::color(colors[1], colors[2], (value - 0.25) / 0.25)\n\t\t\t: anim::color(colors[2], colors[3], (value - 0.5) / 0.5),\n\t});\n}\n\nnot_null<QAction*> MenuVolumeItem::action() const {\n\treturn _dummyAction;\n}\n\nbool MenuVolumeItem::isEnabled() const {\n\treturn true;\n}\n\nint MenuVolumeItem::contentHeight() const {\n\treturn rect::m::sum::v(_padding) + _stCross.icon.height();\n}\n\nrpl::producer<bool> MenuVolumeItem::toggleMuteRequests() const {\n\treturn _toggleMuteRequests.events();\n}\n\nrpl::producer<bool> MenuVolumeItem::toggleMuteLocallyRequests() const {\n\treturn _toggleMuteLocallyRequests.events();\n}\n\nrpl::producer<int> MenuVolumeItem::changeVolumeRequests() const {\n\treturn _changeVolumeRequests.events();\n}\n\nrpl::producer<int> MenuVolumeItem::changeVolumeLocallyRequests() const {\n\treturn _changeVolumeLocallyRequests.events();\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/calls_volume_item.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/widgets/menu/menu_item_base.h\"\n\nnamespace Ui {\nclass CrossLineAnimation;\nclass MediaSlider;\nnamespace Paint {\nclass ArcsAnimation;\n} // namespace Paint\n} // namespace Ui\n\nnamespace Calls {\n\nnamespace Group {\nstruct MuteRequest;\nstruct VolumeRequest;\nstruct ParticipantState;\n} // namespace Group\n\nclass MenuVolumeItem final : public Ui::Menu::ItemBase {\npublic:\n\tMenuVolumeItem(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::Menu &st,\n\t\tconst style::MediaSlider &stSlider,\n\t\trpl::producer<Group::ParticipantState> participantState,\n\t\tint startVolume,\n\t\tint maxVolume,\n\t\tbool muted,\n\t\tconst QMargins &padding);\n\n\tnot_null<QAction*> action() const override;\n\tbool isEnabled() const override;\n\n\t[[nodiscard]] rpl::producer<bool> toggleMuteRequests() const;\n\t[[nodiscard]] rpl::producer<bool> toggleMuteLocallyRequests() const;\n\t[[nodiscard]] rpl::producer<int> changeVolumeRequests() const;\n\t[[nodiscard]] rpl::producer<int> changeVolumeLocallyRequests() const;\n\nprotected:\n\tint contentHeight() const override;\n\nprivate:\n\tvoid initArcsAnimation();\n\n\tvoid setCloudVolume(int volume);\n\tvoid setSliderVolume(int volume);\n\tvoid updateSliderColor(float64 value);\n\n\tQColor unmuteColor() const;\n\tQColor muteColor() const;\n\n\tconst int _maxVolume;\n\tint _cloudVolume = 0;\n\tbool _waitingForUpdateVolume = false;\n\tbool _cloudMuted = false;\n\tbool _localMuted = false;\n\n\tQRect _itemRect;\n\tQRect _speakerRect;\n\tQPoint _arcPosition;\n\n\tconst base::unique_qptr<Ui::MediaSlider> _slider;\n\tconst not_null<QAction*> _dummyAction;\n\tconst style::Menu &_st;\n\tconst style::CrossLineAnimation &_stCross;\n\tconst QMargins &_padding;\n\n\tconst std::unique_ptr<Ui::CrossLineAnimation> _crossLineMute;\n\tUi::Animations::Simple _crossLineAnimation;\n\tconst std::unique_ptr<Ui::Paint::ArcsAnimation> _arcs;\n\tUi::Animations::Basic _arcsAnimation;\n\n\trpl::event_stream<bool> _toggleMuteRequests;\n\trpl::event_stream<bool> _toggleMuteLocallyRequests;\n\trpl::event_stream<int> _changeVolumeRequests;\n\trpl::event_stream<int> _changeVolumeLocallyRequests;\n\n};\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/ui/calls_group_recording_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/ui/calls_group_recording_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"styles/style_basic.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\n#include <QSvgRenderer>\n\nnamespace Calls::Group {\nnamespace {\n\nconstexpr auto kRoundRadius = 9;\nconstexpr auto kMaxGroupCallLength = 40;\nconstexpr auto kSwitchDuration = 200;\n\nclass GraphicButton final : public Ui::AbstractButton {\npublic:\n\tGraphicButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst QString &filename,\n\t\tint selectWidth = 0);\n\n\tvoid setToggled(bool value);\nprotected:\n\tvoid paintEvent(QPaintEvent *e);\n\nprivate:\n\tconst style::margins _margins;\n\tQSvgRenderer _renderer;\n\tUi::RoundRect _roundRect;\n\tUi::RoundRect _roundRectSelect;\n\tUi::Animations::Simple _animation;\n\tbool _toggled = false;\n};\n\nclass RecordingInfo final : public Ui::RpWidget {\npublic:\n\tRecordingInfo(not_null<Ui::RpWidget*> parent);\n\n\tvoid prepareAudio();\n\tvoid prepareVideo();\n\n\tRecordingType type() const;\n\nprivate:\n\tvoid setLabel(const QString &text);\n\n\tconst object_ptr<Ui::VerticalLayout> _container;\n\tRecordingType _type = RecordingType::AudioOnly;\n};\n\nclass Switcher final : public Ui::RpWidget {\npublic:\n\tSwitcher(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<bool> &&toggled);\n\n\tRecordingType type() const;\n\nprivate:\n\tconst object_ptr<Ui::BoxContentDivider> _background;\n\tconst object_ptr<RecordingInfo> _audio;\n\tconst object_ptr<RecordingInfo> _video;\n\tbool _toggled = false;\n\n\tUi::Animations::Simple _animation;\n};\n\nGraphicButton::GraphicButton(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst QString &filename,\n\tint selectWidth)\n: AbstractButton(parent)\n, _margins(selectWidth, selectWidth, selectWidth, selectWidth)\n, _renderer(u\":/gui/recording/%1.svg\"_q.arg(filename))\n, _roundRect(kRoundRadius, st::groupCallMembersBg)\n, _roundRectSelect(kRoundRadius, st::groupCallActiveFg) {\n\tconst auto size = style::ConvertScale(_renderer.defaultSize());\n\tresize((QRect(QPoint(), size) + _margins).size());\n}\n\nvoid GraphicButton::setToggled(bool value) {\n\tif (_toggled == value) {\n\t\treturn;\n\t}\n\t_toggled = value;\n\t_animation.start(\n\t\t[=] { update(); },\n\t\t_toggled ? 0. : 1.,\n\t\t_toggled ? 1. : 0.,\n\t\tst::universalDuration);\n}\n\nvoid GraphicButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tconst auto progress = _animation.value(_toggled ? 1. : 0.);\n\tp.setOpacity(progress);\n\t_roundRectSelect.paint(p, rect());\n\tp.setOpacity(1.);\n\tconst auto r = rect() - _margins;\n\t_roundRect.paint(p, r);\n\t_renderer.render(&p, r);\n}\n\nRecordingInfo::RecordingInfo(not_null<Ui::RpWidget*> parent)\n: RpWidget(parent)\n, _container(this) {\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\t_container->resizeToWidth(size.width());\n\t}, _container->lifetime());\n}\n\nvoid RecordingInfo::prepareAudio() {\n\t_type = RecordingType::AudioOnly;\n\tsetLabel(tr::lng_group_call_recording_start_audio_subtitle(tr::now));\n\n\tconst auto wrap = _container->add(\n\t\tobject_ptr<Ui::RpWidget>(_container),\n\t\tstyle::margins(0, st::groupCallRecordingAudioSkip, 0, 0));\n\tconst auto audioIcon = Ui::CreateChild<GraphicButton>(\n\t\twrap,\n\t\t\"info_audio\");\n\twrap->resize(width(), audioIcon->height());\n\taudioIcon->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\taudioIcon->moveToLeft((size.width() - audioIcon->width()) / 2, 0);\n\t}, lifetime());\n}\n\nvoid RecordingInfo::prepareVideo() {\n\tsetLabel(tr::lng_group_call_recording_start_video_subtitle(tr::now));\n\n\tconst auto wrap = _container->add(\n\t\tobject_ptr<Ui::RpWidget>(_container),\n\t\tstyle::margins());\n\n\tconst auto landscapeIcon = Ui::CreateChild<GraphicButton>(\n\t\twrap,\n\t\t\"info_video_landscape\",\n\t\tst::groupCallRecordingSelectWidth);\n\tconst auto portraitIcon = Ui::CreateChild<GraphicButton>(\n\t\twrap,\n\t\t\"info_video_portrait\",\n\t\tst::groupCallRecordingSelectWidth);\n\twrap->resize(width(), portraitIcon->height());\n\n\tlandscapeIcon->setToggled(true);\n\t_type = RecordingType::VideoLandscape;\n\n\tconst auto icons = std::vector<GraphicButton*>{\n\t\tlandscapeIcon,\n\t\tportraitIcon,\n\t};\n\tconst auto types = std::map<GraphicButton*, RecordingType>{\n\t\t{ landscapeIcon, RecordingType::VideoLandscape },\n\t\t{ portraitIcon, RecordingType::VideoPortrait },\n\t};\n\tfor (const auto icon : icons) {\n\t\ticon->clicks(\n\t\t) | rpl::on_next([=] {\n\t\t\tfor (const auto &i : icons) {\n\t\t\t\ti->setToggled(icon == i);\n\t\t\t}\n\t\t\t_type = types.at(icon);\n\t\t}, lifetime());\n\t}\n\n\twrap->sizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\tconst auto wHalf = size.width() / icons.size();\n\t\tfor (auto i = 0; i < icons.size(); i++) {\n\t\t\tconst auto &icon = icons[i];\n\t\t\ticon->moveToLeft(\n\t\t\t\twHalf * i + (wHalf - icon->width()) / 2,\n\t\t\t\t(size.height() - icon->height()) / 2);\n\t\t}\n\t}, lifetime());\n}\n\nvoid RecordingInfo::setLabel(const QString &text) {\n\t_container->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_container,\n\t\t\ttext,\n\t\t\tst::groupCallRecordingSubLabel),\n\t\tst::groupCallRecordingSubLabelMargins,\n\t\tstyle::al_top);\n}\n\nRecordingType RecordingInfo::type() const {\n\treturn _type;\n}\n\nSwitcher::Switcher(\n\tnot_null<Ui::RpWidget*> parent,\n\trpl::producer<bool> &&toggled)\n: RpWidget(parent)\n, _background(\n\tthis,\n\tst::groupCallRecordingInfoHeight,\n\tst::groupCallDividerBar)\n, _audio(this)\n, _video(this) {\n\t_audio->prepareAudio();\n\t_video->prepareVideo();\n\n\tresize(0, st::groupCallRecordingInfoHeight);\n\n\tconst auto updatePositions = [=](float64 progress) {\n\t\t_audio->moveToLeft(-width() * progress, 0);\n\t\t_video->moveToLeft(_audio->x() + _audio->width(), 0);\n\t};\n\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\t_audio->resize(size.width(), size.height());\n\t\t_video->resize(size.width(), size.height());\n\n\t\tupdatePositions(_toggled ? 1. : 0.);\n\n\t\t_background->lower();\n\t\t_background->setGeometry(QRect(QPoint(), size));\n\t}, lifetime());\n\n\tstd::move(\n\t\ttoggled\n\t) | rpl::on_next([=](bool toggled) {\n\t\t_toggled = toggled;\n\t\t_animation.start(\n\t\t\tupdatePositions,\n\t\t\ttoggled ? 0. : 1.,\n\t\t\ttoggled ? 1. : 0.,\n\t\t\tkSwitchDuration);\n\t}, lifetime());\n}\n\nRecordingType Switcher::type() const {\n\treturn _toggled ? _video->type() : _audio->type();\n}\n\n} // namespace\n\nvoid EditGroupCallTitleBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst QString &placeholder,\n\t\tconst QString &title,\n\t\tbool livestream,\n\t\tFn<void(QString)> done) {\n\tbox->setTitle(livestream\n\t\t? tr::lng_group_call_edit_title_channel()\n\t\t: tr::lng_group_call_edit_title());\n\tconst auto input = box->addRow(object_ptr<Ui::InputField>(\n\t\tbox,\n\t\tst::groupCallField,\n\t\trpl::single(placeholder),\n\t\ttitle));\n\tinput->setMaxLength(kMaxGroupCallLength);\n\tbox->setFocusCallback([=] {\n\t\tinput->setFocusFast();\n\t});\n\tconst auto submit = [=] {\n\t\tconst auto result = input->getLastText().trimmed();\n\t\tbox->closeBox();\n\t\tdone(result);\n\t};\n\tinput->submits() | rpl::on_next(submit, input->lifetime());\n\tbox->addButton(tr::lng_settings_save(), submit);\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\nvoid StartGroupCallRecordingBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tFn<void(RecordingType)> done) {\n\tbox->setTitle(tr::lng_group_call_recording_start());\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox.get(),\n\t\t\ttr::lng_group_call_recording_start_sure(),\n\t\t\tst::groupCallBoxLabel));\n\n\tconst auto checkbox = box->addRow(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tbox,\n\t\t\ttr::lng_group_call_recording_start_checkbox(),\n\t\t\tfalse,\n\t\t\tst::groupCallCheckbox),\n\t\tstyle::margins(\n\t\t\tst::boxRowPadding.left(),\n\t\t\tst::boxRowPadding.left(),\n\t\t\tst::boxRowPadding.right(),\n\t\t\tst::boxRowPadding.bottom()));\n\n\tconst auto switcher = box->addRow(\n\t\tobject_ptr<Switcher>(box, checkbox->checkedChanges()),\n\t\tst::groupCallRecordingInfoMargins);\n\n\tbox->addButton(tr::lng_continue(), [=] {\n\t\tconst auto type = switcher->type();\n\t\tbox->closeBox();\n\t\tdone(type);\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\nvoid AddTitleGroupCallRecordingBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst QString &title,\n\t\tFn<void(QString)> done) {\n\tbox->setTitle(tr::lng_group_call_recording_start_title());\n\n\tconst auto input = box->addRow(object_ptr<Ui::InputField>(\n\t\tbox,\n\t\tst::groupCallField,\n\t\ttr::lng_group_call_recording_start_field(),\n\t\ttitle));\n\tbox->setFocusCallback([=] {\n\t\tinput->setFocusFast();\n\t});\n\tconst auto submit = [=] {\n\t\tconst auto result = input->getLastText().trimmed();\n\t\tbox->closeBox();\n\t\tdone(result);\n\t};\n\tinput->submits() | rpl::on_next(submit, input->lifetime());\n\tbox->addButton(tr::lng_group_call_recording_start_button(), submit);\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\n\nvoid StopGroupCallRecordingBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tFn<void(QString)> done) {\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox.get(),\n\t\t\ttr::lng_group_call_recording_stop_sure(),\n\t\t\tst::groupCallBoxLabel),\n\t\tstyle::margins(\n\t\t\tst::boxRowPadding.left(),\n\t\t\tst::boxPadding.top(),\n\t\t\tst::boxRowPadding.right(),\n\t\t\tst::boxPadding.bottom()));\n\n\tbox->addButton(tr::lng_box_ok(), [=] {\n\t\tbox->closeBox();\n\t\tdone(QString());\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/ui/calls_group_recording_box.h",
    "content": " /*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nnamespace Calls::Group {\n\nenum class RecordingType {\n\tAudioOnly,\n\tVideoLandscape,\n\tVideoPortrait,\n};\n\nvoid EditGroupCallTitleBox(\n\tnot_null<Ui::GenericBox*> box,\n\tconst QString &placeholder,\n\tconst QString &title,\n\tbool livestream,\n\tFn<void(QString)> done);\n\nvoid StartGroupCallRecordingBox(\n\tnot_null<Ui::GenericBox*> box,\n\tFn<void(RecordingType)> done);\n\nvoid AddTitleGroupCallRecordingBox(\n\tnot_null<Ui::GenericBox*> box,\n\tconst QString &title,\n\tFn<void(QString)> done);\n\nvoid StopGroupCallRecordingBox(\n\tnot_null<Ui::GenericBox*> box,\n\tFn<void(QString)> done);\n\n} // namespace Calls::Group\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/ui/calls_group_scheduled_labels.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/ui/calls_group_scheduled_labels.h\"\n\n#include \"ui/rp_widget.h\"\n#include \"ui/painter.h\"\n#include \"lang/lang_keys.h\"\n#include \"base/unixtime.h\"\n#include \"base/timer_rpl.h\"\n#include \"styles/style_calls.h\"\n\n#include <QtCore/QDateTime>\n#include <QtCore/QLocale>\n\nnamespace Calls::Group::Ui {\n\nrpl::producer<QString> StartsWhenText(rpl::producer<TimeId> date) {\n\treturn std::move(\n\t\tdate\n\t) | rpl::map([](TimeId date) -> rpl::producer<QString> {\n\t\tconst auto parsedDate = base::unixtime::parse(date);\n\t\tconst auto dateDay = QDateTime(parsedDate.date(), QTime(0, 0));\n\t\tconst auto previousDay = QDateTime(\n\t\t\tparsedDate.date().addDays(-1),\n\t\t\tQTime(0, 0));\n\t\tconst auto now = QDateTime::currentDateTime();\n\t\tconst auto kDay = int64(24 * 60 * 60);\n\t\tconst auto tillTomorrow = int64(now.secsTo(previousDay));\n\t\tconst auto tillToday = tillTomorrow + kDay;\n\t\tconst auto tillAfter = tillToday + kDay;\n\n\t\tconst auto time = QLocale().toString(\n\t\t\tparsedDate.time(),\n\t\t\tQLocale::ShortFormat);\n\t\tauto exact = tr::lng_group_call_starts_short_date(\n\t\t\tlt_date,\n\t\t\trpl::single(langDayOfMonthFull(dateDay.date())),\n\t\t\tlt_time,\n\t\t\trpl::single(time)\n\t\t) | rpl::type_erased;\n\t\tauto tomorrow = tr::lng_group_call_starts_short_tomorrow(\n\t\t\tlt_time,\n\t\t\trpl::single(time));\n\t\tauto today = tr::lng_group_call_starts_short_today(\n\t\t\tlt_time,\n\t\t\trpl::single(time));\n\n\t\tauto todayAndAfter = rpl::single(\n\t\t\tstd::move(today)\n\t\t) | rpl::then(base::timer_once(\n\t\t\tstd::min(tillAfter, kDay) * crl::time(1000)\n\t\t) | rpl::map([=] {\n\t\t\treturn rpl::duplicate(exact);\n\t\t})) | rpl::flatten_latest() | rpl::type_erased;\n\n\t\tauto tomorrowAndAfter = rpl::single(\n\t\t\tstd::move(tomorrow)\n\t\t) | rpl::then(base::timer_once(\n\t\t\tstd::min(tillToday, kDay) * crl::time(1000)\n\t\t) | rpl::map([=] {\n\t\t\treturn rpl::duplicate(todayAndAfter);\n\t\t})) | rpl::flatten_latest() | rpl::type_erased;\n\n\t\tauto full = rpl::single(\n\t\t\trpl::duplicate(exact)\n\t\t) | rpl::then(base::timer_once(\n\t\t\ttillTomorrow * crl::time(1000)\n\t\t) | rpl::map([=] {\n\t\t\treturn rpl::duplicate(tomorrowAndAfter);\n\t\t})) | rpl::flatten_latest() | rpl::type_erased;\n\n\t\tif (tillTomorrow > 0) {\n\t\t\treturn full;\n\t\t} else if (tillToday > 0) {\n\t\t\treturn tomorrowAndAfter;\n\t\t} else if (tillAfter > 0) {\n\t\t\treturn todayAndAfter;\n\t\t} else {\n\t\t\treturn exact;\n\t\t}\n\t}) | rpl::flatten_latest();\n}\n\nobject_ptr<Ui::RpWidget> CreateGradientLabel(\n\t\tQWidget *parent,\n\t\trpl::producer<QString> text) {\n\tstruct State {\n\t\tQBrush brush;\n\t\tQPainterPath path;\n\t};\n\tauto result = object_ptr<Ui::RpWidget>(parent);\n\tconst auto raw = result.data();\n\tconst auto state = raw->lifetime().make_state<State>();\n\n\tstd::move(\n\t\ttext\n\t) | rpl::on_next([=](const QString &text) {\n\t\tstate->path = QPainterPath();\n\t\tconst auto &font = st::groupCallCountdownFont;\n\t\tstate->path.addText(0, font->ascent, font->f, text);\n\t\tconst auto width = font->width(text);\n\t\traw->resize(width, font->height);\n\t\tauto gradient = QLinearGradient(QPoint(width, 0), QPoint());\n\t\tgradient.setStops(QGradientStops{\n\t\t\t{ 0.0, st::groupCallForceMutedBar1->c },\n\t\t\t{ .7, st::groupCallForceMutedBar2->c },\n\t\t\t{ 1.0, st::groupCallForceMutedBar3->c }\n\t\t});\n\t\tstate->brush = QBrush(std::move(gradient));\n\t\traw->update();\n\t}, raw->lifetime());\n\n\traw->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto skip = st::groupCallWidth / 20;\n\t\tconst auto available = parent->width() - 2 * skip;\n\t\tconst auto full = raw->width();\n\t\tif (available > 0 && full > available) {\n\t\t\tconst auto scale = available / float64(full);\n\t\t\tconst auto shift = raw->rect().center();\n\t\t\tp.translate(shift);\n\t\t\tp.scale(scale, scale);\n\t\t\tp.translate(-shift);\n\t\t}\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(state->brush);\n\t\tp.drawPath(state->path);\n\t}, raw->lifetime());\n\treturn result;\n}\n\n} // namespace Calls::Group::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/ui/calls_group_scheduled_labels.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Calls::Group::Ui {\n\nusing namespace ::Ui;\n\n[[nodiscard]] rpl::producer<QString> StartsWhenText(\n\trpl::producer<TimeId> date);\n\n[[nodiscard]] object_ptr<RpWidget> CreateGradientLabel(\n\tQWidget *parent,\n\trpl::producer<QString> text);\n\n} // namespace Calls::Group::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/ui/calls_group_stars_coloring.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/ui/calls_group_stars_coloring.h\"\n\n#include \"base/object_ptr.h\"\n#include \"lang/lang_keys.h\"\n#include \"payments/ui/payments_reaction_box.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/painter.h\"\n#include \"ui/rp_widget.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_premium.h\"\n\nnamespace Calls::Group::Ui {\n\nStarsColoring StarsColoringForCount(\n\t\tconst std::vector<StarsColoring> &colorings,\n\t\tint stars) {\n\tfor (auto i = begin(colorings), e = end(colorings); i != e; ++i) {\n\t\tif (i->fromStars > stars) {\n\t\t\tAssert(i != begin(colorings));\n\t\t\treturn *(std::prev(i));\n\t\t}\n\t}\n\treturn colorings.back();\n}\n\nint StarsRequiredForMessage(\n\t\tconst std::vector<StarsColoring> &colorings,\n\t\tconst TextWithTags &text) {\n\tExpects(!colorings.empty());\n\n\tauto emojis = 0;\n\tauto outLength = 0;\n\tauto view = QStringView(text.text);\n\tconst auto length = int(view.size());\n\twhile (!view.isEmpty()) {\n\t\tif (Emoji::Find(view, &outLength)) {\n\t\t\tview = view.mid(outLength);\n\t\t\t++emojis;\n\t\t} else {\n\t\t\tview = view.mid(1);\n\t\t}\n\t}\n\tfor (const auto &entry : colorings) {\n\t\tif (emojis <= entry.emojiLimit && length <= entry.charactersMax) {\n\t\t\treturn entry.fromStars;\n\t\t}\n\t}\n\treturn colorings.back().fromStars + 1;\n}\n\nobject_ptr<RpWidget> VideoStreamStarsLevel(\n\t\tnot_null<RpWidget*> box,\n\t\tconst std::vector<StarsColoring> &colorings,\n\t\trpl::producer<int> starsValue) {\n\tstruct State {\n\t\trpl::variable<int> stars;\n\t\trpl::variable<StarsColoring> coloring;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tstate->stars = std::move(starsValue);\n\tstate->coloring = state->stars.value(\n\t) | rpl::map([=](int stars) {\n\t\treturn StarsColoringForCount(colorings, stars);\n\t});\n\n\tauto pinTitle = state->coloring.value(\n\t) | rpl::map([=](const StarsColoring &value) {\n\t\tconst auto seconds = value.secondsPin;\n\t\treturn (seconds >= 3600)\n\t\t\t? tr::lng_hours_tiny(tr::now, lt_count, seconds / 3600)\n\t\t\t: (seconds >= 60)\n\t\t\t? tr::lng_minutes_tiny(tr::now, lt_count, seconds / 60)\n\t\t\t: tr::lng_seconds_tiny(tr::now, lt_count, seconds);\n\t});\n\tauto limitTitle = state->coloring.value(\n\t) | rpl::map([=](const StarsColoring &value) {\n\t\treturn QString::number(value.charactersMax);\n\t});\n\tauto limitSubtext = state->coloring.value(\n\t) | rpl::map([=](const StarsColoring &value) {\n\t\treturn tr::lng_paid_comment_limit_about(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tvalue.charactersMax);\n\t});\n\tauto emojiTitle = state->coloring.value(\n\t) | rpl::map([=](const StarsColoring &value) {\n\t\treturn QString::number(value.emojiLimit);\n\t});\n\tauto emojiSubtext = state->coloring.value(\n\t) | rpl::map([=](const StarsColoring &value) {\n\t\treturn tr::lng_paid_comment_emoji_about(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tvalue.emojiLimit);\n\t});\n\treturn MakeStarSelectInfoBlocks(box, {\n\t\t{\n\t\t\t.title = std::move(pinTitle) | rpl::map(tr::marked),\n\t\t\t.subtext = tr::lng_paid_comment_pin_about(),\n\t\t},\n\t\t{\n\t\t\t.title = std::move(limitTitle) | rpl::map(tr::marked),\n\t\t\t.subtext = std::move(limitSubtext),\n\t\t},\n\t\t{\n\t\t\t.title = std::move(emojiTitle) | rpl::map(tr::marked),\n\t\t\t.subtext = std::move(emojiSubtext),\n\t\t},\n\t}, {}, true);\n}\n\n} // namespace Calls::Group::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/ui/calls_group_stars_coloring.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Calls::Group::Ui {\n\nusing namespace ::Ui;\n\nstruct StarsColoring {\n\tint bgLight = 0;\n\tint bgDark = 0;\n\tint fromStars = 0;\n\tTimeId secondsPin = 0;\n\tint charactersMax = 0;\n\tint emojiLimit = 0;\n\n\tfriend inline auto operator<=>(\n\t\tconst StarsColoring &,\n\t\tconst StarsColoring &) = default;\n\tfriend inline bool operator==(\n\t\tconst StarsColoring &,\n\t\tconst StarsColoring &) = default;\n};\n\n[[nodiscard]] StarsColoring StarsColoringForCount(\n\tconst std::vector<StarsColoring> &colorings,\n\tint stars);\n\n[[nodiscard]] int StarsRequiredForMessage(\n\tconst std::vector<StarsColoring> &colorings,\n\tconst TextWithTags &text);\n\n[[nodiscard]] object_ptr<Ui::RpWidget> VideoStreamStarsLevel(\n\tnot_null<Ui::RpWidget*> box,\n\tconst std::vector<StarsColoring> &colorings,\n\trpl::producer<int> starsValue);\n\n} // namespace Calls::Group::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/group/ui/desktop_capture_choose_source.h\"\n\n#include \"ui/widgets/rp_window.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/image/image.h\"\n#include \"ui/platform/ui_platform_window_title.h\"\n#include \"ui/painter.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_calls.h\"\n\n#include <tgcalls/desktop_capturer/DesktopCaptureSourceManager.h>\n#include <tgcalls/desktop_capturer/DesktopCaptureSourceHelper.h>\n#include <QtGui/QWindow>\n\nnamespace Calls::Group::Ui::DesktopCapture {\nnamespace {\n\nconstexpr auto kColumns = 3;\nconstexpr auto kRows = 2;\n\nstruct Preview {\n\texplicit Preview(tgcalls::DesktopCaptureSource source);\n\n\ttgcalls::DesktopCaptureSourceHelper helper;\n\tWebrtc::VideoTrack track;\n\trpl::lifetime lifetime;\n};\n\nclass SourceButton final : public RippleButton {\npublic:\n\tusing RippleButton::RippleButton;\n\nprivate:\n\tQImage prepareRippleMask() const override;\n\n};\n\nQImage SourceButton::prepareRippleMask() const {\n\treturn RippleAnimation::RoundRectMask(size(), st::roundRadiusLarge);\n}\n\nclass Source final {\npublic:\n\tSource(\n\t\tnot_null<QWidget*> parent,\n\t\ttgcalls::DesktopCaptureSource source,\n\t\tconst QString &title);\n\n\tvoid setGeometry(QRect geometry);\n\tvoid clearHelper();\n\n\t[[nodiscard]] rpl::producer<> activations() const;\n\tvoid setActive(bool active);\n\t[[nodiscard]] QString deviceIdKey() const;\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tvoid paint();\n\tvoid setupPreview();\n\n\tSourceButton _widget;\n\tFlatLabel _label;\n\tUi::RoundRect _selectedRect;\n\tUi::RoundRect _activeRect;\n\ttgcalls::DesktopCaptureSource _source;\n\tstd::unique_ptr<Preview> _preview;\n\trpl::event_stream<> _activations;\n\tQImage _frame;\n\tbool _active = false;\n\n};\n\nclass ChooseSourceProcess final {\npublic:\n\tstatic void Start(not_null<ChooseSourceDelegate*> delegate);\n\n\texplicit ChooseSourceProcess(not_null<ChooseSourceDelegate*> delegate);\n\n\tvoid activate();\n\nprivate:\n\tvoid setupPanel();\n\tvoid setupSources();\n\tvoid setupGeometryWithParent(not_null<QWidget*> parent);\n\tvoid fillSources();\n\tvoid setupSourcesGeometry();\n\tvoid updateButtonsVisibility();\n\tvoid destroy();\n\n\tstatic base::flat_map<\n\t\tnot_null<ChooseSourceDelegate*>,\n\t\tstd::unique_ptr<ChooseSourceProcess>> &Map();\n\n\tconst not_null<ChooseSourceDelegate*> _delegate;\n\tconst std::unique_ptr<RpWindow> _window;\n\tconst std::unique_ptr<ScrollArea> _scroll;\n\tconst not_null<RpWidget*> _inner;\n\tconst not_null<RpWidget*> _bottom;\n\tconst not_null<RoundButton*> _submit;\n\tconst not_null<RoundButton*> _finish;\n\tconst not_null<Checkbox*> _withAudio;\n\n\tQSize _fixedSize;\n\tstd::vector<std::unique_ptr<Source>> _sources;\n\tSource *_selected = nullptr;\n\tQString _selectedId;\n\n};\n\n[[nodiscard]] tgcalls::DesktopCaptureSourceData SourceData() {\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto size = st::desktopCaptureSourceSize * factor;\n\treturn {\n\t\t.aspectSize = { size.width(), size.height() },\n\t\t.fps = 1,\n\t\t.captureMouse = false,\n\t};\n}\n\nPreview::Preview(tgcalls::DesktopCaptureSource source)\n: helper(source, SourceData())\n, track(Webrtc::VideoState::Active) {\n\thelper.setOutput(track.sink());\n\thelper.start();\n}\n\nSource::Source(\n\tnot_null<QWidget*> parent,\n\ttgcalls::DesktopCaptureSource source,\n\tconst QString &title)\n: _widget(parent, st::groupCallRipple)\n, _label(&_widget, title, st::desktopCaptureLabel)\n, _selectedRect(ImageRoundRadius::Large, st::groupCallMembersBgOver)\n, _activeRect(ImageRoundRadius::Large, st::groupCallMuted1)\n, _source(source) {\n\t_widget.paintRequest(\n\t) | rpl::on_next([=] {\n\t\tpaint();\n\t}, _widget.lifetime());\n\n\t_label.setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t_widget.sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tconst auto padding = st::desktopCapturePadding;\n\t\t_label.resizeToNaturalWidth(\n\t\t\tsize.width() - padding.left() - padding.right());\n\t\t_label.move(\n\t\t\t(size.width() - _label.width()) / 2,\n\t\t\tsize.height() - _label.height() - st::desktopCaptureLabelBottom);\n\t}, _label.lifetime());\n\n\t_widget.setClickedCallback([=] {\n\t\tsetActive(true);\n\t});\n}\n\nrpl::producer<> Source::activations() const {\n\treturn _activations.events();\n}\n\nQString Source::deviceIdKey() const {\n\treturn QString::fromStdString(_source.deviceIdKey());\n}\n\nvoid Source::setActive(bool active) {\n\tif (_active != active) {\n\t\t_active = active;\n\t\t_widget.update();\n\t\tif (active) {\n\t\t\t_activations.fire({});\n\t\t}\n\t}\n}\n\nvoid Source::setGeometry(QRect geometry) {\n\t_widget.setGeometry(geometry);\n}\n\nvoid Source::clearHelper() {\n\t_preview = nullptr;\n}\n\nvoid Source::paint() {\n\tauto p = QPainter(&_widget);\n\n\tif (_frame.isNull() && !_preview) {\n\t\tsetupPreview();\n\t}\n\tif (_active) {\n\t\t_activeRect.paint(p, _widget.rect());\n\t} else if (_widget.isOver() || _widget.isDown()) {\n\t\t_selectedRect.paint(p, _widget.rect());\n\t}\n\t_widget.paintRipple(\n\t\tp,\n\t\t{ 0, 0 },\n\t\t_active ? &st::shadowFg->c : nullptr);\n\n\tconst auto size = _preview ? _preview->track.frameSize() : QSize();\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto padding = st::desktopCapturePadding;\n\tconst auto rect = _widget.rect();\n\tconst auto inner = rect.marginsRemoved(padding);\n\tif (!size.isEmpty()) {\n\t\tconst auto scaled = size.scaled(inner.size(), Qt::KeepAspectRatio);\n\t\tconst auto request = Webrtc::FrameRequest{\n\t\t\t.resize = scaled * factor,\n\t\t\t.outer = scaled * factor,\n\t\t};\n\t\t_frame = _preview->track.frame(request);\n\t\t_preview->track.markFrameShown();\n\t}\n\tif (!_frame.isNull()) {\n\t\tclearHelper();\n\t\tconst auto size = _frame.size() / factor;\n\t\tconst auto x = inner.x() + (inner.width() - size.width()) / 2;\n\t\tconst auto y = inner.y() + (inner.height() - size.height()) / 2;\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.drawImage(QRect(x, y, size.width(), size.height()), _frame);\n\t}\n}\n\nvoid Source::setupPreview() {\n\t_preview = std::make_unique<Preview>(_source);\n\t_preview->track.renderNextFrame(\n\t) | rpl::on_next([=] {\n\t\tif (_preview->track.frameSize().isEmpty()) {\n\t\t\t_preview->track.markFrameShown();\n\t\t}\n\t\t_widget.update();\n\t}, _preview->lifetime);\n}\n\nrpl::lifetime &Source::lifetime() {\n\treturn _widget.lifetime();\n}\n\nChooseSourceProcess::ChooseSourceProcess(\n\tnot_null<ChooseSourceDelegate*> delegate)\n: _delegate(delegate)\n, _window(std::make_unique<RpWindow>())\n, _scroll(std::make_unique<ScrollArea>(_window->body()))\n, _inner(_scroll->setOwnedWidget(object_ptr<RpWidget>(_scroll.get())))\n, _bottom(CreateChild<RpWidget>(_window->body().get()))\n, _submit(\n\tCreateChild<RoundButton>(\n\t\t_bottom.get(),\n\t\ttr::lng_group_call_screen_share_start(),\n\t\tst::desktopCaptureSubmit))\n, _finish(\n\tCreateChild<RoundButton>(\n\t\t_bottom.get(),\n\t\ttr::lng_group_call_screen_share_stop(),\n\t\tst::desktopCaptureFinish))\n, _withAudio(\n\tCreateChild<Checkbox>(\n\t\t_bottom.get(),\n\t\ttr::lng_group_call_screen_share_audio(tr::now),\n\t\tfalse,\n\t\tst::desktopCaptureWithAudio)) {\n\t_submit->setTextTransform(RoundButtonTextTransform::ToUpper);\n\t_finish->setTextTransform(RoundButtonTextTransform::ToUpper);\n\tsetupPanel();\n\tsetupSources();\n\tactivate();\n}\n\nvoid ChooseSourceProcess::Start(not_null<ChooseSourceDelegate*> delegate) {\n\tauto &map = Map();\n\tauto i = map.find(delegate);\n\tif (i == end(map)) {\n\t\ti = map.emplace(delegate, nullptr).first;\n\t\tdelegate->chooseSourceInstanceLifetime().add([=] {\n\t\t\tMap().erase(delegate);\n\t\t});\n\t}\n\tif (!i->second) {\n\t\ti->second = std::make_unique<ChooseSourceProcess>(delegate);\n\t} else {\n\t\ti->second->activate();\n\t}\n}\n\nvoid ChooseSourceProcess::activate() {\n\tif (_window->windowState() & Qt::WindowMinimized) {\n\t\t_window->showNormal();\n\t} else {\n\t\t_window->show();\n\t}\n\t_window->raise();\n\t_window->activateWindow();\n}\n\n[[nodiscard]] base::flat_map<\n\t\tnot_null<ChooseSourceDelegate*>,\n\t\tstd::unique_ptr<ChooseSourceProcess>> &ChooseSourceProcess::Map() {\n\tstatic auto result = base::flat_map<\n\t\tnot_null<ChooseSourceDelegate*>,\n\t\tstd::unique_ptr<ChooseSourceProcess>>();\n\treturn result;\n}\n\nvoid ChooseSourceProcess::setupPanel() {\n#ifndef Q_OS_LINUX\n\t//_window->setAttribute(Qt::WA_OpaquePaintEvent);\n#endif // Q_OS_LINUX\n\t//_window->setAttribute(Qt::WA_NoSystemBackground);\n\n\t_window->setWindowIcon(QIcon(\n\t\tQPixmap::fromImage(Image::Empty()->original(), Qt::ColorOnly)));\n\t_window->setTitleStyle(st::desktopCaptureSourceTitle);\n\n\tconst auto skips = st::desktopCaptureSourceSkips;\n\tconst auto margins = st::desktopCaptureMargins;\n\tconst auto padding = st::desktopCapturePadding;\n\tconst auto bottomSkip = margins.right() + padding.right();\n\tconst auto bottomHeight = 2 * bottomSkip\n\t\t+ st::desktopCaptureCancel.height;\n\tconst auto width = margins.left()\n\t\t+ kColumns * st::desktopCaptureSourceSize.width()\n\t\t+ (kColumns - 1) * skips.width()\n\t\t+ margins.right();\n\tconst auto height = margins.top()\n\t\t+ kRows * st::desktopCaptureSourceSize.height()\n\t\t+ (kRows - 1) * skips.height()\n\t\t+ (st::desktopCaptureSourceSize.height() / 2)\n\t\t+ bottomHeight;\n\t_fixedSize = QSize(width, height);\n\t_window->setStaysOnTop(true);\n\n\t_window->body()->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tQPainter(_window->body()).fillRect(clip, st::groupCallMembersBg);\n\t}, _window->lifetime());\n\n\t_bottom->setGeometry(0, height - bottomHeight, width, bottomHeight);\n\n\t_submit->setClickedCallback([=] {\n\t\tif (_selectedId.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto weak = base::make_weak(_window.get());\n\t\t_delegate->chooseSourceAccepted(\n\t\t\t_selectedId,\n\t\t\t!_withAudio->isHidden() && _withAudio->checked());\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->close();\n\t\t}\n\t});\n\t_finish->setClickedCallback([=] {\n\t\tconst auto weak = base::make_weak(_window.get());\n\t\t_delegate->chooseSourceStop();\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->close();\n\t\t}\n\t});\n\tconst auto cancel = CreateChild<RoundButton>(\n\t\t_bottom.get(),\n\t\ttr::lng_cancel(),\n\t\tst::desktopCaptureCancel);\n\tcancel->setTextTransform(RoundButtonTextTransform::ToUpper);\n\tcancel->setClickedCallback([=] {\n\t\t_window->close();\n\t});\n\n\trpl::combine(\n\t\t_submit->widthValue(),\n\t\t_submit->shownValue(),\n\t\t_finish->widthValue(),\n\t\t_finish->shownValue(),\n\t\tcancel->widthValue()\n\t) | rpl::on_next([=](\n\t\t\tint submitWidth,\n\t\t\tbool submitShown,\n\t\t\tint finishWidth,\n\t\t\tbool finishShown,\n\t\t\tint cancelWidth) {\n\t\t_finish->moveToRight(bottomSkip, bottomSkip);\n\t\t_submit->moveToRight(bottomSkip, bottomSkip);\n\t\tcancel->moveToRight(\n\t\t\tbottomSkip * 2 + (submitShown ? submitWidth : finishWidth),\n\t\t\tbottomSkip);\n\t}, _bottom->lifetime());\n\n\t_withAudio->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto top = (bottomHeight - _withAudio->heightNoMargins()) / 2;\n\t\t_withAudio->moveToLeft(bottomSkip, top);\n\t}, _withAudio->lifetime());\n\n\t_withAudio->setChecked(_delegate->chooseSourceActiveWithAudio());\n\t_withAudio->checkedChanges(\n\t) | rpl::on_next([=] {\n\t\tupdateButtonsVisibility();\n\t}, _withAudio->lifetime());\n\n\tconst auto sharing = !_delegate->chooseSourceActiveDeviceId().isEmpty();\n\t_finish->setVisible(sharing);\n\t_submit->setVisible(!sharing);\n\n\t_window->body()->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\t_scroll->setGeometry(\n\t\t\t0,\n\t\t\t0,\n\t\t\tsize.width(),\n\t\t\tsize.height() - _bottom->height());\n\t}, _scroll->lifetime());\n\n\t_scroll->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto rows = int(std::ceil(_sources.size() / float(kColumns)));\n\t\tconst auto innerHeight = margins.top()\n\t\t\t+ rows * st::desktopCaptureSourceSize.height()\n\t\t\t+ (rows - 1) * skips.height()\n\t\t\t+ margins.bottom();\n\t\t_inner->resize(width, innerHeight);\n\t}, _inner->lifetime());\n\n\tif (const auto parent = _delegate->chooseSourceParent()) {\n\t\tsetupGeometryWithParent(parent);\n\t}\n\n\t_window->events(\n\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\treturn e->type() == QEvent::Close;\n\t}) | rpl::on_next([=] {\n\t\tdestroy();\n\t}, _window->lifetime());\n}\n\nvoid ChooseSourceProcess::setupSources() {\n\tfillSources();\n\tsetupSourcesGeometry();\n}\n\nvoid ChooseSourceProcess::fillSources() {\n\tusing Type = tgcalls::DesktopCaptureType;\n\tauto screensManager = tgcalls::DesktopCaptureSourceManager(Type::Screen);\n\tauto windowsManager = tgcalls::DesktopCaptureSourceManager(Type::Window);\n\n\t_withAudio->setVisible(_delegate->chooseSourceWithAudioSupported());\n\n\tauto screenIndex = 0;\n\tauto windowIndex = 0;\n\tauto firstScreenSelected = false;\n\tconst auto active = _delegate->chooseSourceActiveDeviceId();\n\tconst auto append = [&](const tgcalls::DesktopCaptureSource &source) {\n\t\tconst auto firstScreen = !source.isWindow() && !screenIndex;\n\t\tconst auto title = !source.isWindow()\n\t\t\t? tr::lng_group_call_screen_title(\n\t\t\t\ttr::now,\n\t\t\t\tlt_index,\n\t\t\t\tQString::number(++screenIndex))\n\t\t\t: !source.title().empty()\n\t\t\t? QString::fromStdString(source.title())\n\t\t\t: \"Window \" + QString::number(++windowIndex);\n\t\tconst auto id = source.deviceIdKey();\n\t\t_sources.push_back(std::make_unique<Source>(_inner, source, title));\n\n\t\tconst auto raw = _sources.back().get();\n\t\tif (!active.isEmpty() && active.toStdString() == id) {\n\t\t\t_selected = raw;\n\t\t\traw->setActive(true);\n\t\t} else if (active.isEmpty() && firstScreen) {\n\t\t\t_selected = raw;\n\t\t\traw->setActive(true);\n\t\t\tfirstScreenSelected = true;\n\t\t}\n\t\t_sources.back()->activations(\n\t\t) | rpl::filter([=] {\n\t\t\treturn (_selected != raw);\n\t\t}) | rpl::on_next([=]{\n\t\t\tif (_selected) {\n\t\t\t\t_selected->setActive(false);\n\t\t\t}\n\t\t\t_selected = raw;\n\t\t\tupdateButtonsVisibility();\n\t\t}, raw->lifetime());\n\t};\n\tfor (const auto &source : screensManager.sources()) {\n\t\tappend(source);\n\t}\n\tfor (const auto &source : windowsManager.sources()) {\n\t\tappend(source);\n\t}\n\tif (firstScreenSelected) {\n\t\tupdateButtonsVisibility();\n\t}\n}\n\nvoid ChooseSourceProcess::updateButtonsVisibility() {\n\tconst auto selectedId = _selected\n\t\t? _selected->deviceIdKey()\n\t\t: QString();\n\tif (selectedId == _delegate->chooseSourceActiveDeviceId()\n\t\t&& (!_delegate->chooseSourceWithAudioSupported()\n\t\t\t|| (_withAudio->checked()\n\t\t\t\t== _delegate->chooseSourceActiveWithAudio()))) {\n\t\t_selectedId = QString();\n\t\t_finish->setVisible(true);\n\t\t_submit->setVisible(false);\n\t} else {\n\t\t_selectedId = selectedId;\n\t\t_finish->setVisible(false);\n\t\t_submit->setVisible(true);\n\t}\n}\n\nvoid ChooseSourceProcess::setupSourcesGeometry() {\n\tif (_sources.empty()) {\n\t\tdestroy();\n\t\treturn;\n\t}\n\t_inner->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto rows = int(std::ceil(_sources.size() / float(kColumns)));\n\t\tconst auto margins = st::desktopCaptureMargins;\n\t\tconst auto skips = st::desktopCaptureSourceSkips;\n\t\tconst auto single = (width\n\t\t\t- margins.left()\n\t\t\t- margins.right()\n\t\t\t- (kColumns - 1) * skips.width()) / kColumns;\n\t\tconst auto height = st::desktopCaptureSourceSize.height();\n\t\tauto top = margins.top();\n\t\tauto index = 0;\n\t\tfor (auto row = 0; row != rows; ++row) {\n\t\t\tauto left = margins.left();\n\t\t\tfor (auto column = 0; column != kColumns; ++column) {\n\t\t\t\t_sources[index]->setGeometry({ left, top, single, height });\n\t\t\t\tif (++index == _sources.size()) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tleft += single + skips.width();\n\t\t\t}\n\t\t\tif (index >= _sources.size()) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ttop += height + skips.height();\n\t\t}\n\t}, _inner->lifetime());\n\n\trpl::combine(\n\t\t_scroll->scrollTopValue(),\n\t\t_scroll->heightValue()\n\t) | rpl::on_next([=](int scrollTop, int scrollHeight) {\n\t\tconst auto rows = int(std::ceil(_sources.size() / float(kColumns)));\n\t\tconst auto margins = st::desktopCaptureMargins;\n\t\tconst auto skips = st::desktopCaptureSourceSkips;\n\t\tconst auto height = st::desktopCaptureSourceSize.height();\n\t\tauto top = margins.top();\n\t\tauto index = 0;\n\t\tfor (auto row = 0; row != rows; ++row) {\n\t\t\tconst auto hidden = (top + height <= scrollTop)\n\t\t\t\t|| (top >= scrollTop + scrollHeight);\n\t\t\tif (hidden) {\n\t\t\t\tfor (auto column = 0; column != kColumns; ++column) {\n\t\t\t\t\t_sources[index]->clearHelper();\n\t\t\t\t\tif (++index == _sources.size()) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tindex += kColumns;\n\t\t\t}\n\t\t\tif (index >= _sources.size()) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ttop += height + skips.height();\n\t\t}\n\t}, _inner->lifetime());\n}\n\nvoid ChooseSourceProcess::setupGeometryWithParent(\n\t\tnot_null<QWidget*> parent) {\n\tconst auto parentScreen = parent->screen();\n\tconst auto myScreen = _window->screen();\n\tif (parentScreen && myScreen != parentScreen) {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)\n\t\t_window->setScreen(parentScreen);\n#else // Qt >= 6.0.0\n\t\t_window->createWinId();\n\t\t_window->windowHandle()->setScreen(parentScreen);\n#endif // Qt < 6.0.0\n\t}\n\t_window->setFixedSize(_fixedSize);\n\t_window->move(\n\t\tparent->x() + (parent->width() - _window->width()) / 2,\n\t\tparent->y() + (parent->height() - _window->height()) / 2);\n}\n\nvoid ChooseSourceProcess::destroy() {\n\tauto &map = Map();\n\tif (const auto i = map.find(_delegate); i != end(map)) {\n\t\tif (i->second.get() == this) {\n\t\t\tbase::take(i->second);\n\t\t}\n\t}\n}\n\n} // namespace\n\nvoid ChooseSource(not_null<ChooseSourceDelegate*> delegate) {\n\tChooseSourceProcess::Start(delegate);\n}\n\n} // namespace Calls::Group::Ui::DesktopCapture\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n} // namespace Ui\n\nnamespace Calls::Group::Ui {\nusing namespace ::Ui;\n} // namespace Calls::Group::Ui\n\nnamespace Calls::Group::Ui::DesktopCapture {\n\nclass ChooseSourceDelegate {\npublic:\n\tvirtual QWidget *chooseSourceParent() = 0;\n\tvirtual QString chooseSourceActiveDeviceId() = 0;\n\tvirtual bool chooseSourceActiveWithAudio() = 0;\n\tvirtual bool chooseSourceWithAudioSupported() = 0;\n\tvirtual rpl::lifetime &chooseSourceInstanceLifetime() = 0;\n\tvirtual void chooseSourceAccepted(\n\t\tconst QString &deviceId,\n\t\tbool withAudio) = 0;\n\tvirtual void chooseSourceStop() = 0;\n};\n\nvoid ChooseSource(not_null<ChooseSourceDelegate*> delegate);\n\n} // namespace Calls::Group::Ui::DesktopCapture\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/ui/calls_device_menu.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"calls/ui/calls_device_menu.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/menu/menu_item_base.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"webrtc/webrtc_device_common.h\"\n#include \"webrtc/webrtc_environment.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Calls {\nnamespace {\n\nclass Subsection final : public Ui::Menu::ItemBase {\npublic:\n\tSubsection(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::Menu &st,\n\t\tconst QString &text);\n\n\tnot_null<QAction*> action() const override;\n\tbool isEnabled() const override;\n\nprivate:\n\tint contentHeight() const override;\n\n\tconst style::Menu &_st;\n\tconst base::unique_qptr<Ui::FlatLabel> _text;\n\tconst not_null<QAction*> _dummyAction;\n\n};\n\nclass Selector final : public Ui::Menu::ItemBase {\npublic:\n\tSelector(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::Menu &st,\n\t\trpl::producer<std::vector<Webrtc::DeviceInfo>> devices,\n\t\trpl::producer<Webrtc::DeviceResolvedId> chosen,\n\t\tFn<void(QString)> selected);\n\n\tnot_null<QAction*> action() const override;\n\tbool isEnabled() const override;\n\nprivate:\n\tint contentHeight() const override;\n\t[[nodiscard]] int registerId(const QString &id);\n\n\tconst base::unique_qptr<Ui::ScrollArea> _scroll;\n\tconst not_null<Ui::VerticalLayout*> _list;\n\tconst not_null<QAction*> _dummyAction;\n\n\tbase::flat_map<QString, int> _ids;\n\n};\n\nSubsection::Subsection(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::Menu &st,\n\tconst QString &text)\n: Ui::Menu::ItemBase(parent, st)\n, _st(st)\n, _text(base::make_unique_q<Ui::FlatLabel>(\n\tthis,\n\ttext,\n\tst::callDeviceSelectionLabel))\n, _dummyAction(Ui::CreateChild<QAction>(parent)) {\n\tsetPointerCursor(false);\n\n\tfitToMenuWidth();\n\n\t_text->resizeToWidth(st::callDeviceSelectionLabel.minWidth);\n\t_text->moveToLeft(st.itemPadding.left(), st.itemPadding.top());\n}\n\nnot_null<QAction*> Subsection::action() const {\n\treturn _dummyAction;\n}\n\nbool Subsection::isEnabled() const {\n\treturn false;\n}\n\nint Subsection::contentHeight() const {\n\treturn _st.itemPadding.top()\n\t\t+ _text->height()\n\t\t+ _st.itemPadding.bottom();\n}\n\nSelector::Selector(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::Menu &st,\n\trpl::producer<std::vector<Webrtc::DeviceInfo>> devices,\n\trpl::producer<Webrtc::DeviceResolvedId> chosen,\n\tFn<void(QString)> selected)\n: Ui::Menu::ItemBase(parent, st)\n, _scroll(base::make_unique_q<Ui::ScrollArea>(this))\n, _list(_scroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))\n, _dummyAction(Ui::CreateChild<QAction>(parent)) {\n\tsetPointerCursor(false);\n\n\tfitToMenuWidth();\n\n\tconst auto padding = st.itemPadding;\n\tconst auto group = std::make_shared<Ui::RadiobuttonGroup>();\n\tstd::move(\n\t\tchosen\n\t) | rpl::on_next([=](Webrtc::DeviceResolvedId id) {\n\t\tconst auto value = id.isDefault() ? 0 : registerId(id.value);\n\t\tif (!group->hasValue() || group->current() != value) {\n\t\t\tgroup->setValue(value);\n\t\t}\n\t}, lifetime());\n\n\tgroup->setChangedCallback([=](int value) {\n\t\tif (value == 0) {\n\t\t\tselected({});\n\t\t} else {\n\t\t\tfor (const auto &[id, index] : _ids) {\n\t\t\t\tif (index == value) {\n\t\t\t\t\tselected(id);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\n\tstd::move(\n\t\tdevices\n\t) | rpl::on_next([=](const std::vector<Webrtc::DeviceInfo> &v) {\n\t\twhile (_list->count()) {\n\t\t\tdelete _list->widgetAt(0);\n\t\t}\n\t\t_list->add(\n\t\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\t\t_list.get(),\n\t\t\t\tgroup,\n\t\t\t\t0,\n\t\t\t\ttr::lng_settings_call_device_default(tr::now),\n\t\t\t\tst::groupCallCheckbox,\n\t\t\t\tst::groupCallRadio),\n\t\t\tpadding);\n\t\tfor (const auto &device : v) {\n\t\t\tif (device.inactive) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t_list->add(\n\t\t\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\t\t\t_list.get(),\n\t\t\t\t\tgroup,\n\t\t\t\t\tregisterId(device.id),\n\t\t\t\t\tdevice.name,\n\t\t\t\t\tst::groupCallCheckbox,\n\t\t\t\t\tst::groupCallRadio),\n\t\t\t\tpadding);\n\t\t}\n\t\tresize(width(), contentHeight());\n\t}, lifetime());\n}\n\nnot_null<QAction*> Selector::action() const {\n\treturn _dummyAction;\n}\n\nbool Selector::isEnabled() const {\n\treturn false;\n}\n\nint Selector::contentHeight() const {\n\t_list->resizeToWidth(width());\n\tif (_list->count() <= 3) {\n\t\t_scroll->resize(width(), _list->height());\n\t} else {\n\t\t_scroll->resize(\n\t\t\twidth(),\n\t\t\t3.5 * st::defaultRadio.diameter);\n\t}\n\treturn _scroll->height();\n}\n\nint Selector::registerId(const QString &id) {\n\tauto &result = _ids[id];\n\tif (!result) {\n\t\tresult = int(_ids.size());\n\t}\n\treturn result;\n}\n\nvoid AddDeviceSelection(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<Webrtc::Environment*> environment,\n\t\tDeviceSelection type,\n\t\tFn<void(QString)> selected) {\n\tconst auto title = [&] {\n\t\tswitch (type.type) {\n\t\tcase Webrtc::DeviceType::Camera:\n\t\t\treturn tr::lng_settings_call_camera(tr::now);\n\t\tcase Webrtc::DeviceType::Playback:\n\t\t\treturn tr::lng_settings_call_section_output(tr::now);\n\t\tcase Webrtc::DeviceType::Capture:\n\t\t\treturn tr::lng_settings_call_section_input(tr::now);\n\t\t}\n\t\tUnexpected(\"Type in AddDeviceSelection.\");\n\t}();\n\tmenu->addAction(\n\t\tbase::make_unique_q<Subsection>(\n\t\t\tmenu->menu(),\n\t\t\tmenu->st().menu,\n\t\t\ttitle));\n\tmenu->addAction(\n\t\tbase::make_unique_q<Selector>(\n\t\t\tmenu->menu(),\n\t\t\tmenu->st().menu,\n\t\t\tenvironment->devicesValue(type.type),\n\t\t\tstd::move(type.chosen),\n\t\t\tselected));\n}\n\n} // namespace\n\nbase::unique_qptr<Ui::PopupMenu> MakeDeviceSelectionMenu(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Webrtc::Environment*> environment,\n\t\tstd::vector<DeviceSelection> types,\n\t\tFn<void(Webrtc::DeviceType, QString)> choose) {\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::callDeviceSelectionMenu);\n\tconst auto raw = result.get();\n\tfor (auto type : types) {\n\t\tif (!raw->empty()) {\n\t\t\traw->addSeparator();\n\t\t}\n\t\tconst auto selected = [=, type = type.type](QString id) {\n\t\t\tchoose(type, id);\n\t\t};\n\t\tAddDeviceSelection(raw, environment, std::move(type), selected);\n\t}\n\treturn result;\n}\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/calls/ui/calls_device_menu.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n\nnamespace Webrtc {\nclass Environment;\nstruct DeviceResolvedId;\nenum class DeviceType : uchar;\n} // namespace Webrtc\n\nnamespace Ui {\nclass RpWidget;\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Calls {\n\nstruct DeviceSelection {\n\tWebrtc::DeviceType type;\n\trpl::producer<Webrtc::DeviceResolvedId> chosen;\n};\n\n[[nodiscard]] base::unique_qptr<Ui::PopupMenu> MakeDeviceSelectionMenu(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Webrtc::Environment*> environment,\n\tstd::vector<DeviceSelection> types,\n\tFn<void(Webrtc::DeviceType, QString)> choose);\n\n} // namespace Calls\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/bot_command.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/bot_command.h\"\n\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"history/history_item.h\"\n\nnamespace Bot {\n\nQString WrapCommandInChat(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) {\n\tauto result = command;\n\tif (const auto item = peer->owner().message(context)) {\n\t\tif (const auto user = item->fromOriginal()->asUser()) {\n\t\t\treturn WrapCommandInChat(peer, command, user);\n\t\t}\n\t}\n\treturn result;\n}\n\nQString WrapCommandInChat(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &command,\n\t\tnot_null<UserData*> bot) {\n\tif (!bot->isBot() || bot->username().isEmpty()) {\n\t\treturn command;\n\t}\n\tconst auto botStatus = peer->isChat()\n\t\t? peer->asChat()->botStatus\n\t\t: peer->isMegagroup()\n\t\t? peer->asChannel()->mgInfo->botStatus\n\t\t: Data::BotStatus::NoBots;\n\treturn ((command.indexOf('@') < 2) && (botStatus != Data::BotStatus::NoBots))\n\t\t? command + '@' + bot->username()\n\t\t: command;\n}\n\n} // namespace Bot\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/bot_command.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\nclass UserData;\n\nnamespace Bot {\n\nstruct SendCommandRequest {\n\tnot_null<PeerData*> peer;\n\tQString command;\n\tFullMsgId context;\n\tFullReplyTo replyTo;\n};\n\n[[nodiscard]] QString WrapCommandInChat(\n\tnot_null<PeerData*> peer,\n\tconst QString &command,\n\tconst FullMsgId &context);\n[[nodiscard]] QString WrapCommandInChat(\n\tnot_null<PeerData*> peer,\n\tconst QString &command,\n\tnot_null<UserData*> bot);\n\n} // namespace Bot\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/bot_keyboard.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/bot_keyboard.h\"\n\n#include \"api/api_bot.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"history/history_item_components.h\"\n#include \"main/main_session.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/chat/chat_style_radius.h\"\n#include \"ui/painter.h\"\n#include \"ui/round_rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace {\n\nconst auto kBotKeyboardRounding = Ui::BubbleRounding{\n\tUi::BubbleCornerRounding::Large,\n\tUi::BubbleCornerRounding::Large,\n\tUi::BubbleCornerRounding::Large,\n\tUi::BubbleCornerRounding::Large,\n};\n\nclass Style : public ReplyKeyboard::Style {\npublic:\n\tStyle(\n\t\tnot_null<BotKeyboard*> parent,\n\t\tconst style::BotKeyboardButton &st);\n\n\tImages::CornersMaskRef buttonRounding(\n\t\tUi::BubbleRounding outer,\n\t\tRectParts sides) const override;\n\n\tconst style::TextStyle &textStyle() const override;\n\tvoid repaint(not_null<const HistoryItem*> item) const override;\n\nprotected:\n\tvoid paintButtonBg(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tconst QRect &rect,\n\t\tHistoryMessageMarkupButton::Color color,\n\t\tUi::BubbleRounding rounding,\n\t\tfloat64 howMuchOver) const override;\n\tvoid paintButtonStart(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tHistoryMessageMarkupButton::Color color) const override;\n\tvoid paintButtonIcon(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tconst QRect &rect,\n\t\tint outerWidth,\n\t\tHistoryMessageMarkupButton::Type type) const override;\n\tvoid paintButtonLoading(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tconst QRect &rect,\n\t\tHistoryMessageMarkupButton::Color color,\n\t\tint outerWidth,\n\t\tUi::BubbleRounding rounding) const override;\n\tint minButtonWidth(HistoryMessageMarkupButton::Type type) const override;\n\nprivate:\n\tnot_null<BotKeyboard*> _parent;\n\n};\n\nStyle::Style(\n\tnot_null<BotKeyboard*> parent,\n\tconst style::BotKeyboardButton &st)\n: ReplyKeyboard::Style(st), _parent(parent) {\n}\n\nvoid Style::paintButtonStart(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tHistoryMessageMarkupButton::Color color) const {\n\tusing Color = HistoryMessageMarkupButton::Color;\n\tp.setPen((color == Color::Normal) ? st::botKbColor : st::white);\n\tp.setFont(st::botKbStyle.font);\n}\n\nconst style::TextStyle &Style::textStyle() const {\n\treturn st::botKbStyle;\n}\n\nvoid Style::repaint(not_null<const HistoryItem*> item) const {\n\t_parent->update();\n}\n\nImages::CornersMaskRef Style::buttonRounding(\n\t\tUi::BubbleRounding outer,\n\t\tRectParts sides) const {\n\tusing namespace Images;\n\tusing namespace Ui;\n\tusing Radius = CachedCornerRadius;\n\tusing Corner = BubbleCornerRounding;\n\tauto result = CornersMaskRef(CachedCornersMasks(Radius::BubbleSmall));\n\tconst auto &large = CachedCornersMasks(Radius::BubbleLarge);\n\tconst auto round = [&](\n\t\t\tRectPart vertSide,\n\t\t\tRectPart horizSide,\n\t\t\tint index) {\n\t\tif ((sides & vertSide)\n\t\t\t&& (sides & horizSide)\n\t\t\t&& (outer[index] == Corner::Large)) {\n\t\t\tresult.p[index] = &large[index];\n\t\t}\n\t};\n\tround(RectPart::Top, RectPart::Left, kTopLeft);\n\tround(RectPart::Top, RectPart::Right, kTopRight);\n\tround(RectPart::Bottom, RectPart::Left, kBottomLeft);\n\tround(RectPart::Bottom, RectPart::Right, kBottomRight);\n\treturn result;\n}\n\nvoid Style::paintButtonBg(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tconst QRect &rect,\n\t\tHistoryMessageMarkupButton::Color color,\n\t\tUi::BubbleRounding rounding,\n\t\tfloat64 howMuchOver) const {\n\tusing Color = HistoryMessageMarkupButton::Color;\n\tusing Corner = Ui::BubbleCornerRounding;\n\tconst auto bg = (color == Color::Normal)\n\t\t? st::botKbBg->c\n\t\t: (color == Color::Primary)\n\t\t? st::botKbPrimaryBg->c\n\t\t: (color == Color::Danger)\n\t\t? st::botKbDangerBg->c\n\t\t: st::botKbSuccessBg->c;\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(bg);\n\tconst auto large = Ui::BubbleRadiusLarge();\n\tconst auto small = Ui::BubbleRadiusSmall();\n\tconst auto radius = [&](int index) {\n\t\treturn (rounding[index] == Corner::Large) ? large : small;\n\t};\n\tp.drawPath(\n\t\tUi::ComplexRoundedRectPath(\n\t\t\trect,\n\t\t\tradius(0),\n\t\t\tradius(1),\n\t\t\tradius(2),\n\t\t\tradius(3)));\n}\n\nvoid Style::paintButtonIcon(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tconst QRect &rect,\n\t\tint outerWidth,\n\t\tHistoryMessageMarkupButton::Type type) const {\n\t// Buttons with icons should not appear here.\n}\n\nvoid Style::paintButtonLoading(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tconst QRect &rect,\n\t\tHistoryMessageMarkupButton::Color color,\n\t\tint outerWidth,\n\t\tUi::BubbleRounding rounding) const {\n\t// Buttons with loading progress should not appear here.\n}\n\nint Style::minButtonWidth(HistoryMessageMarkupButton::Type type) const {\n\tint result = 2 * buttonPadding();\n\treturn result;\n}\n\n} // namespace\n\nBotKeyboard::BotKeyboard(\n\tnot_null<Window::SessionController*> controller,\n\tQWidget *parent)\n: RpWidget(parent)\n, _controller(controller)\n, _st(&st::botKbButton) {\n\tsetGeometry(0, 0, _st->margin, st::botKbScroll.deltat);\n\t_height = st::botKbScroll.deltat;\n\tsetMouseTracking(true);\n}\n\nvoid BotKeyboard::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tauto clip = e->rect();\n\tp.fillRect(clip, st::historyComposeAreaBg);\n\n\tif (_impl) {\n\t\tint x = rtl() ? st::botKbScroll.width : _st->margin;\n\t\tp.translate(x, st::botKbScroll.deltat);\n\t\t_impl->paint(\n\t\t\tp,\n\t\t\tnullptr,\n\t\t\tkBotKeyboardRounding,\n\t\t\twidth(),\n\t\t\tclip.translated(-x, -st::botKbScroll.deltat),\n\t\t\t_controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any));\n\t}\n}\n\nvoid BotKeyboard::mousePressEvent(QMouseEvent *e) {\n\t_lastMousePos = e->globalPos();\n\tupdateSelected();\n\n\tClickHandler::pressed();\n}\n\nvoid BotKeyboard::mouseMoveEvent(QMouseEvent *e) {\n\t_lastMousePos = e->globalPos();\n\tupdateSelected();\n}\n\nvoid BotKeyboard::mouseReleaseEvent(QMouseEvent *e) {\n\t_lastMousePos = e->globalPos();\n\tupdateSelected();\n\n\tif (ClickHandlerPtr activated = ClickHandler::unpressed()) {\n\t\tActivateClickHandler(window(), activated, {\n\t\t\te->button(),\n\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.sessionWindow = base::make_weak(_controller),\n\t\t\t})\n\t\t});\n\t}\n}\n\nvoid BotKeyboard::enterEventHook(QEnterEvent *e) {\n\t_lastMousePos = QCursor::pos();\n\tupdateSelected();\n}\n\nvoid BotKeyboard::leaveEventHook(QEvent *e) {\n\tclearSelection();\n}\n\nbool BotKeyboard::moderateKeyActivate(\n\t\tint key,\n\t\tFn<ClickContext(FullMsgId)> context) {\n\tconst auto &data = _controller->session().data();\n\n\tconst auto botCommand = [](int key) {\n\t\tif (key == Qt::Key_Q || key == Qt::Key_6) {\n\t\t\treturn u\"/translate\"_q;\n\t\t} else if (key == Qt::Key_W || key == Qt::Key_5) {\n\t\t\treturn u\"/eng\"_q;\n\t\t} else if (key == Qt::Key_3) {\n\t\t\treturn u\"/pattern\"_q;\n\t\t} else if (key == Qt::Key_4) {\n\t\t\treturn u\"/abuse\"_q;\n\t\t} else if (key == Qt::Key_0 || key == Qt::Key_E || key == Qt::Key_9) {\n\t\t\treturn u\"/undo\"_q;\n\t\t} else if (key == Qt::Key_Plus\n\t\t\t\t|| key == Qt::Key_QuoteLeft\n\t\t\t\t|| key == Qt::Key_7) {\n\t\t\treturn u\"/next\"_q;\n\t\t} else if (key == Qt::Key_Period\n\t\t\t\t|| key == Qt::Key_S\n\t\t\t\t|| key == Qt::Key_8) {\n\t\t\treturn u\"/stats\"_q;\n\t\t}\n\t\treturn QString();\n\t};\n\n\tif (const auto item = data.message(_wasForMsgId)) {\n\t\tif (const auto markup = item->Get<HistoryMessageReplyMarkup>()) {\n\t\t\tif (key >= Qt::Key_1 && key <= Qt::Key_2) {\n\t\t\t\tconst auto index = int(key - Qt::Key_1);\n\t\t\t\tif (!markup->data.rows.empty()\n\t\t\t\t\t&& index >= 0\n\t\t\t\t\t&& index < int(markup->data.rows.front().size())) {\n\t\t\t\t\tApi::ActivateBotCommand(\n\t\t\t\t\t\tcontext(\n\t\t\t\t\t\t\t_wasForMsgId).other.value<ClickHandlerContext>(),\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tindex);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} else if (const auto user = item->history()->peer->asUser()) {\n\t\t\t\tif (user->isBot() && item->from() == user) {\n\t\t\t\t\tconst auto command = botCommand(key);\n\t\t\t\t\tif (!command.isEmpty()) {\n\t\t\t\t\t\t_sendCommandRequests.fire({\n\t\t\t\t\t\t\t.peer = user,\n\t\t\t\t\t\t\t.command = command,\n\t\t\t\t\t\t\t.context = item->fullId(),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid BotKeyboard::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {\n\tif (!_impl) return;\n\t_impl->clickHandlerActiveChanged(p, active);\n}\n\nvoid BotKeyboard::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {\n\tif (!_impl) return;\n\t_impl->clickHandlerPressedChanged(p, pressed, kBotKeyboardRounding);\n}\n\nbool BotKeyboard::updateMarkup(HistoryItem *to, bool force) {\n\tif (!to || !to->definesReplyKeyboard()) {\n\t\tif (_wasForMsgId.msg) {\n\t\t\t_maximizeSize = _singleUse = _forceReply = _persistent = false;\n\t\t\t_wasForMsgId = FullMsgId();\n\t\t\t_placeholder = QString();\n\t\t\t_impl = nullptr;\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\tconst auto peerId = to->history()->peer->id;\n\tif (_wasForMsgId == FullMsgId(peerId, to->id) && !force) {\n\t\treturn false;\n\t}\n\n\t_wasForMsgId = FullMsgId(peerId, to->id);\n\n\tauto markupFlags = to->replyKeyboardFlags();\n\t_forceReply = markupFlags & ReplyMarkupFlag::ForceReply;\n\t_maximizeSize = !(markupFlags & ReplyMarkupFlag::Resize);\n\t_singleUse = _forceReply || (markupFlags & ReplyMarkupFlag::SingleUse);\n\t_persistent = (markupFlags & ReplyMarkupFlag::Persistent);\n\n\tif (const auto markup = to->Get<HistoryMessageReplyMarkup>()) {\n\t\t_placeholder = markup->data.placeholder;\n\t} else {\n\t\t_placeholder = QString();\n\t}\n\n\t_impl = nullptr;\n\tif (auto markup = to->Get<HistoryMessageReplyMarkup>()) {\n\t\tif (!markup->data.rows.empty()) {\n\t\t\t_impl = std::make_unique<ReplyKeyboard>(\n\t\t\t\tto,\n\t\t\t\tstd::make_unique<Style>(this, *_st));\n\t\t}\n\t}\n\n\tresizeToWidth(width(), _maxOuterHeight);\n\n\treturn true;\n}\n\nbool BotKeyboard::hasMarkup() const {\n\treturn _impl != nullptr;\n}\n\nbool BotKeyboard::forceReply() const {\n\treturn _forceReply;\n}\n\nint BotKeyboard::resizeGetHeight(int newWidth) {\n\tupdateStyle(newWidth);\n\t_height = st::botKbScroll.deltat + st::botKbScroll.deltab + (_impl ? _impl->naturalHeight() : 0);\n\tif (_maximizeSize) {\n\t\taccumulate_max(_height, _maxOuterHeight);\n\t}\n\tif (_impl) {\n\t\tint implWidth = newWidth - _st->margin - st::botKbScroll.width;\n\t\tint implHeight = _height - (st::botKbScroll.deltat + st::botKbScroll.deltab);\n\t\t_impl->resize(implWidth, implHeight);\n\t}\n\treturn _height;\n}\n\nbool BotKeyboard::maximizeSize() const {\n\treturn _maximizeSize;\n}\n\nbool BotKeyboard::singleUse() const {\n\treturn _singleUse;\n}\n\nbool BotKeyboard::persistent() const {\n\treturn _persistent;\n}\n\nvoid BotKeyboard::updateStyle(int newWidth) {\n\tif (!_impl) return;\n\n\tint implWidth = newWidth - st::botKbButton.margin - st::botKbScroll.width;\n\t_st = _impl->isEnoughSpace(implWidth, st::botKbButton) ? &st::botKbButton : &st::botKbTinyButton;\n\n\t_impl->setStyle(std::make_unique<Style>(this, *_st));\n}\n\nvoid BotKeyboard::clearSelection() {\n\tif (_impl) {\n\t\tif (ClickHandler::setActive(ClickHandlerPtr(), this)) {\n\t\t\tUi::Tooltip::Hide();\n\t\t\tsetCursor(style::cur_default);\n\t\t}\n\t}\n}\n\nQPoint BotKeyboard::tooltipPos() const {\n\treturn _lastMousePos;\n}\n\nbool BotKeyboard::tooltipWindowActive() const {\n\treturn Ui::AppInFocus() && Ui::InFocusChain(window());\n}\n\nQString BotKeyboard::tooltipText() const {\n\tif (ClickHandlerPtr lnk = ClickHandler::getActive()) {\n\t\treturn lnk->tooltip();\n\t}\n\treturn QString();\n}\n\nvoid BotKeyboard::updateSelected() {\n\tUi::Tooltip::Show(1000, this);\n\n\tif (!_impl) return;\n\n\tauto p = mapFromGlobal(_lastMousePos);\n\tauto x = rtl() ? st::botKbScroll.width : _st->margin;\n\n\tauto link = _impl->getLink(p - QPoint(x, _st->margin));\n\tif (ClickHandler::setActive(link, this)) {\n\t\tUi::Tooltip::Hide();\n\t\tsetCursor(link ? style::cur_pointer : style::cur_default);\n\t}\n}\n\nauto BotKeyboard::sendCommandRequests() const\n-> rpl::producer<Bot::SendCommandRequest> {\n\treturn _sendCommandRequests.events();\n}\n\nBotKeyboard::~BotKeyboard() = default;\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/bot_keyboard.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/tooltip.h\"\n#include \"chat_helpers/bot_command.h\"\n\nclass ReplyKeyboard;\n\nnamespace style {\nstruct BotKeyboardButton;\n} // namespace style\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nclass BotKeyboard\n\t: public Ui::RpWidget\n\t, public Ui::AbstractTooltipShower\n\t, public ClickHandlerHost {\npublic:\n\tBotKeyboard(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tQWidget *parent);\n\n\tbool moderateKeyActivate(int index, Fn<ClickContext(FullMsgId)> context);\n\n\t// With force=true the markup is updated even if it is\n\t// already shown for the passed history item.\n\tbool updateMarkup(HistoryItem *last, bool force = false);\n\t[[nodiscard]] bool hasMarkup() const;\n\t[[nodiscard]] bool forceReply() const;\n\n\t[[nodiscard]] QString placeholder() const {\n\t\treturn _placeholder;\n\t}\n\n\tvoid step_selected(crl::time ms, bool timer);\n\tvoid resizeToWidth(int newWidth, int maxOuterHeight) {\n\t\t_maxOuterHeight = maxOuterHeight;\n\t\treturn RpWidget::resizeToWidth(newWidth);\n\t}\n\n\t[[nodiscard]] bool maximizeSize() const;\n\t[[nodiscard]] bool singleUse() const;\n\t[[nodiscard]] bool persistent() const;\n\n\t[[nodiscard]] FullMsgId forMsgId() const {\n\t\treturn _wasForMsgId;\n\t}\n\n\t// AbstractTooltipShower interface\n\tQString tooltipText() const override;\n\tQPoint tooltipPos() const override;\n\tbool tooltipWindowActive() const override;\n\n\t// ClickHandlerHost interface\n\tvoid clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;\n\tvoid clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;\n\n\trpl::producer<Bot::SendCommandRequest> sendCommandRequests() const;\n\n\t~BotKeyboard();\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\nprivate:\n\tvoid updateSelected();\n\n\tvoid updateStyle(int newWidth);\n\tvoid clearSelection();\n\n\tconst not_null<Window::SessionController*> _controller;\n\tFullMsgId _wasForMsgId;\n\tQString _placeholder;\n\tint _height = 0;\n\tint _maxOuterHeight = 0;\n\tbool _maximizeSize = false;\n\tbool _singleUse = false;\n\tbool _forceReply = false;\n\tbool _persistent = false;\n\n\tQPoint _lastMousePos;\n\tstd::unique_ptr<ReplyKeyboard> _impl;\n\n\trpl::event_stream<Bot::SendCommandRequest> _sendCommandRequests;\n\n\tconst style::BotKeyboardButton *_st = nullptr;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/chat_helpers.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\n\nusing \"boxes/boxes.style\";\nusing \"ui/layers/layers.style\";\nusing \"ui/widgets/widgets.style\";\nusing \"ui/menu_icons.style\";\nusing \"ui/effects/premium.style\";\n\nGroupCallUserpics {\n\tsize: pixels;\n\tshift: pixels;\n\tstroke: pixels;\n\talign: align;\n}\n\nTabbedSearch {\n\touter: color;\n\tbg: color;\n\tbgActive: color;\n\tfg: color;\n\tfgActive: color;\n\tfadeLeft: icon;\n\tfadeRight: icon;\n\tfield: InputField;\n\tsearch: IconButton;\n\tback: IconButton;\n\tcancel: CrossButton;\n\tdefaultFieldWidth: pixels;\n\tgroupWidth: pixels;\n\tgroupSkip: pixels;\n\theight: pixels;\n}\n\nComposeIcons {\n\tsettings: icon;\n\tcollectibles: icon;\n\n\trecent: icon;\n\trecentActive: icon;\n\tpeople: icon;\n\tpeopleActive: icon;\n\tnature: icon;\n\tnatureActive: icon;\n\tfood: icon;\n\tfoodActive: icon;\n\tactivity: icon;\n\tactivityActive: icon;\n\ttravel: icon;\n\ttravelActive: icon;\n\tobjects: icon;\n\tobjectsActive: icon;\n\tsymbols: icon;\n\tsymbolsActive: icon;\n\n\tmenuFave: icon;\n\tmenuUnfave: icon;\n\tmenuStickerSet: icon;\n\tmenuRecentRemove: icon;\n\tmenuGifAdd: icon;\n\tmenuGifRemove: icon;\n\tmenuMute: icon;\n\tmenuSchedule: icon;\n\tmenuWhenOnline: icon;\n\tmenuSpoiler: icon;\n\tmenuBelow: icon;\n\tmenuAbove: icon;\n\tmenuQualityHigh: icon;\n\tmenuPrice: icon;\n\tmenuEditStars: icon;\n\n\tstripBubble: icon;\n\tstripExpandPanel: icon;\n\tstripExpandDropdown: icon;\n}\n\nEmojiSuggestions {\n\tdropdown: InnerDropdown;\n\tbg: color;\n\toverBg: color;\n\ttextFg: color;\n\tfadeLeft: icon;\n\tfadeRight: icon;\n}\n\nEmojiPan {\n\tmargin: margins;\n\tpadding: margins;\n\tshowAnimation: PanelAnimation;\n\tdesiredSize: pixels;\n\tverticalSizeSub: pixels;\n\theader: pixels;\n\theaderLeft: pixels;\n\theaderLockLeft: pixels;\n\theaderLockedLeft: pixels;\n\theaderTop: pixels;\n\tfooter: pixels;\n\ticonSkip: pixels;\n\ticonWidth: pixels;\n\ticonArea: pixels;\n\tbg: color;\n\theaderFg: color;\n\ttrendingHeaderFg: color;\n\ttrendingSubheaderFg: color;\n\ttrendingUnreadFg: color;\n\toverBg: color;\n\tpathBg: color;\n\tpathFg: color;\n\ttextFg: color;\n\tcategoriesBg: color;\n\tcategoriesBgOver: color;\n\tfadeLeft: icon;\n\tfadeRight: icon;\n\tmenu: PopupMenu;\n\texpandedSeparator: MenuSeparator;\n\ttabs: SettingsSlider;\n\tsearch: TabbedSearch;\n\tsearchMargin: margins;\n\tcolorAll: IconButton;\n\tcolorAllLabel: FlatLabel;\n\tremoveSet: IconButton;\n\tboxLabel: FlatLabel;\n\ticons: ComposeIcons;\n\tabout: FlatLabel;\n\taboutPadding: margins;\n\tautocompleteBottomSkip: pixels;\n}\n\nMessageBar {\n\ttitle: TextStyle;\n\ttitleFg: color;\n\ttext: TextStyle;\n\ttextFg: color;\n\ttextPalette: TextPalette;\n\tduration: int;\n}\n\nEmojiButton {\n\tinner: IconButton;\n\tbg: color;\n\tlineFg: color;\n\tlineFgOver: color;\n}\n\nSendButton {\n\tsendIconPosition: point;\n\tsendIconFillPadding: pixels;\n\tinner: IconButton;\n\tstars: RoundButton;\n\trecordSize: size;\n\tsendDisabledFg: color;\n\tsendIconFg: color;\n}\n\nRecordBarLock {\n\tripple: RippleAnimation;\n\toriginTop: icon;\n\toriginBottom: icon;\n\toriginBody: icon;\n\tshadowTop: icon;\n\tshadowBottom: icon;\n\tshadowBody: icon;\n\tarrow: icon;\n\tfg: color;\n}\n\nRecordBar {\n\tradius: pixels;\n\tbg: color;\n\tdurationFg: color;\n\tcancel: color;\n\tcancelActive: color;\n\tcancelRipple: RippleAnimation;\n\tlock: RecordBarLock;\n\tremove: IconButton;\n}\n\nComposeFiles {\n\tcheck: Check;\n\tcheckbox: Checkbox;\n\tmenu: IconButton;\n\tcaption: InputField;\n\temoji: EmojiButton;\n\tconfirmBg: color;\n\tbuttonFile: IconButton;\n\tbuttonFileEdit: icon;\n\tbuttonFileDelete: icon;\n\ticonBg: color;\n\ticonPlay: icon;\n\ticonImage: icon;\n\ticonDocument: icon;\n\tnameFg: color;\n\tstatusFg: color;\n}\n\nSendAsButton {\n\twidth: pixels;\n\theight: pixels;\n\tsize: pixels;\n\tactiveBg: color;\n\tactiveFg: color;\n\tcross: CrossAnimation;\n\tduration: int;\n}\n\nChooseSendAs {\n\tbutton: SendAsButton;\n\tlabel: FlatLabel;\n\tlist: PeerList;\n}\n\nComposeControls {\n\tbg: color;\n\tradius: pixels;\n\tpadding: margins;\n\n\tfield: InputField;\n\tfieldLeft: pixels;\n\tsend: SendButton;\n\tattach: IconButton;\n\temoji: EmojiButton;\n\tlike: IconButton;\n\tliked: icon;\n\teditStars: IconButton;\n\tcommentsShow: IconButton;\n\tcommentsShown: icon;\n\tcommentsSkip: pixels;\n\tcommentsUnreadSize: pixels;\n\tcommentsUnreadMargin: pixels;\n\tcommentsUnreadPosition: point;\n\tstarsReactionCounter: RoundButton;\n\tstarsSkip: pixels;\n\tsuggestions: EmojiSuggestions;\n\ttabbed: EmojiPan;\n\ttabbedHeightMin: pixels;\n\ttabbedHeightMax: pixels;\n\trecord: RecordBar;\n\tfiles: ComposeFiles;\n\tphotoQualityBadgeOuterSkip: pixels;\n\tpremium: PremiumLimits;\n\tboxField: InputField;\n\trestrictionLabel: FlatLabel;\n\tpremiumRequired: ComposePremiumRequired;\n\tchooseSendAs: ChooseSendAs;\n}\n\nReportBox {\n\tbutton: SettingsButton;\n\tnoIconButton: SettingsButton;\n\tlabel: FlatLabel;\n\tfield: InputField;\n\tdivider: DividerLabel;\n\tspam: icon;\n\tfake: icon;\n\tviolence: icon;\n\tchildren: icon;\n\tpornography: icon;\n\tcopyright: icon;\n\tdrugs: icon;\n\tpersonal: icon;\n\tother: icon;\n}\n\nWhoRead {\n\tuserpics: GroupCallUserpics;\n\tphotoLeft: pixels;\n\tphotoSize: pixels;\n\tphotoSkip: pixels;\n\tnameLeft: pixels;\n\ticonPosition: point;\n\titemPadding: margins;\n}\n\ndefaultWhoRead: WhoRead {\n\tuserpics: GroupCallUserpics {\n\t\tsize: 22px;\n\t\tshift: 8px;\n\t\tstroke: 4px;\n\t\talign: align(right);\n\t}\n\tphotoLeft: 13px;\n\tphotoSize: 30px;\n\tphotoSkip: 5px;\n\tnameLeft: 57px;\n\ticonPosition: point(15px, 7px);\n\titemPadding: margins(44px, 9px, 17px, 7px);\n}\nwhenReadStyle: TextStyle(defaultTextStyle) {\n\tfont: font(12px);\n}\nwhenReadPadding: margins(34px, 3px, 17px, 4px);\nwhenReadIconPosition: point(8px, 0px);\nwhenReadSkip: 3px;\nwhenReadShowPadding: margins(6px, 0px, 6px, 2px);\nwhoSentItem: Menu(defaultMenu) {\n\titemPadding: margins(17px, 3px, 17px, 4px);\n\titemRightSkip: 0px;\n\titemStyle: whenReadStyle;\n}\n\nswitchPmButton: RoundButton(defaultBoxButton) {\n\twidth: 320px;\n\theight: 34px;\n\ttextTop: 7px;\n}\nstickersRestrictedLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 200px;\n\talign: align(center);\n\ttextFg: noContactsColor;\n}\n\nstickersTrendingHeader: 56px;\nstickersTrendingSkip: 4px;\n\nstickersTrendingHeaderFont: semiboldFont;\nstickersTrendingHeaderFg: windowFg;\nstickersTrendingHeaderTop: 11px;\nstickersTrendingSubheaderFont: normalFont;\nstickersTrendingSubheaderFg: windowSubTextFg;\nstickersTrendingSubheaderTop: 31px;\n\nstickersHeaderBadgeFont: font(10px);\nstickersHeaderBadgeFontTop: 12px;\nstickersHeaderBadgeFontSkip: 12px;\n\nemojiPanButtonRight: 7px;\nemojiPanButtonTop: 8px;\nemojiPanButton: RoundButton(defaultActiveButton) {\n\twidth: -24px;\n\theight: 23px;\n\ttextTop: 2px;\n}\nemojiPanExpand: RoundButton(defaultActiveButton) {\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(12px bold);\n\t}\n\twidth: -8px;\n\theight: 19px;\n\ttextTop: 1px;\n}\n\nstickersTrendingAddTop: 14px;\nstickersTrendingAdd: RoundButton(defaultActiveButton) {\n\twidth: -16px;\n\theight: 26px;\n\ttextTop: 4px;\n}\nstickersTrendingInstalled: RoundButton(stickersTrendingAdd) {\n\ttextFg: activeButtonBg;\n\ttextFgOver: activeButtonBgOver;\n\ttextBg: lightButtonBgOver;\n\ttextBgOver: lightButtonBgOver;\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: activeButtonSecondaryFg;\n\t}\n}\nstickersRemove: IconButton(defaultIconButton) {\n\twidth: 40px;\n\theight: 40px;\n\n\ticon: icon {{ \"info/info_media_delete\", menuIconFg }};\n\ticonOver: icon {{ \"info/info_media_delete\", menuIconFgOver }};\n\n\trippleAreaSize: 40px;\n\trippleAreaPosition: point(0px, 0px);\n\tripple: defaultRippleAnimationBgOver;\n}\nstickersUndoRemove: RoundButton(defaultLightButton) {\n\twidth: -16px;\n\theight: 26px;\n\ttextTop: 4px;\n}\nstickersRemoveSkip: 4px;\nstickersReorderIcon: icon {{ \"stickers_reorder\", menuIconFg }};\nstickersReorderSkip: 18px;\n\nstickersTabs: defaultTabsSlider;\n\nstickersRowItem: PeerListItem(defaultPeerListItem) {\n\theight: 52px;\n\tphotoSize: 32px;\n\tphotoPosition: point(18px, 10px);\n\tnamePosition: point(66px, 7px);\n\tstatusPosition: point(66px, 26px);\n\tbutton: OutlineButton(defaultPeerListButton) {\n\t\ttextBg: contactsBg;\n\t\ttextBgOver: contactsBgOver;\n\t\tripple: defaultRippleAnimation;\n\t}\n\tstatusFg: contactsStatusFg;\n\tstatusFgOver: contactsStatusFgOver;\n\tstatusFgActive: contactsStatusFgOnline;\n}\n\nstickerEmojiSkip: 5px;\n\nstickersFeaturedBadgeFont: font(12px bold);\nstickersFeaturedBadgeSize: 15px;\nstickersFeaturedBadgeTextTop: -1px;\nstickersFeaturedBadgePadding: 4px;\nstickersFeaturedBadgeSkip: 4px;\nstickersFeaturedBadgeTop: 9px;\nstickersFeaturedUnreadBg: msgFileInBg;\nstickersFeaturedUnreadSize: 5px;\nstickersFeaturedUnreadSkip: 5px;\nstickersFeaturedUnreadTop: 7px;\n\nstickersMaxHeight: 320px;\nstickersPadding: margins(19px, 13px, 19px, 13px);\nstickersSize: size(64px, 64px);\nemojiSetPadding: margins(12px, 0px, 12px, 0px);\nemojiSetMaxHeight: 197px;\nemojiSetSize: size(42px, 39px);\nstickersScroll: ScrollArea(boxScroll) {\n\tdeltat: 19px;\n\tdeltab: 9px;\n}\nstickersRowDisabledOpacity: 0.4;\nstickersRowDuration: 200;\n\nemojiStatusDefault: icon {{ \"emoji/stickers_premium\", emojiIconFg }};\n\nfiltersRemove: IconButton(stickersRemove) {\n\tripple: defaultRippleAnimation;\n}\n\nemojiPanMargins: margins(10px, 10px, 10px, 10px);\n\nemojiTabs: defaultTabsSlider;\n\nemojiCategoryIconTop: 6px;\nemojiPanAnimation: PanelAnimation(defaultPanelAnimation) {\n\tfadeBg: emojiPanBg;\n}\nemojiPanWidth: 345px;\nemojiPanMinHeight: 278px;\nemojiPanMaxHeight: 640px;\nemojiPanHeightRatio: 0.75;\nemojiPanShowDuration: 200;\nemojiPanDuration: 200;\nemojiPanHover: windowBgOver;\nemojiPanSlideDuration: 200;\nemojiPanArea: size(34px, 32px);\nemojiPanRadius: 8px;\nemojiPanReactionsPreviewPadding: margins(10px, 20px, 10px, 20px);\nemojiPanEmojiPreviewMinHeight: 160px;\nemojiPanEmojiPreviewRadius: 8px + 8px + 4px;\n\ndefaultTabbedSearchCancel: CrossButton {\n\twidth: 33px;\n\theight: 33px;\n\n\tcross: CrossAnimation {\n\t\tsize: 27px;\n\t\tskip: 8px;\n\t\tstroke: 1.;\n\t\tminScale: 0.3;\n\t}\n\tcrossFg: menuIconFg;\n\tcrossFgOver: menuIconFg;\n\tcrossPosition: point(1px, 3px);\n\n\tduration: 150;\n\tloadingPeriod: 1000;\n\tripple: emptyRippleAnimation;\n}\ndefaultTabbedSearchField: InputField(defaultMultiSelectSearchField) {\n\ttextMargins: margins(2px, 7px, 2px, 0px);\n}\ndefaultTabbedSearchButton: IconButton(defaultIconButton) {\n\twidth: 33px;\n\theight: 33px;\n\ticon: icon{{ \"emoji/emoji_search_input\", emojiIconFg }};\n\ticonOver: icon{{ \"emoji/emoji_search_input\", emojiIconFg }};\n\ticonPosition: point(7px, -1px);\n\tripple: emptyRippleAnimation;\n}\ndefaultTabbedSearchBack: IconButton(defaultIconButton) {\n\twidth: 33px;\n\theight: 33px;\n\ticon: icon{{ \"emoji/emoji_back\", menuIconFg }};\n\ticonOver: icon{{ \"emoji/emoji_back\", menuIconFg }};\n\ticonPosition: point(7px, -1px);\n\tripple: emptyRippleAnimation;\n}\ndefaultTabbedSearch: TabbedSearch {\n\touter: emojiPanBg;\n\tbg: emojiPanHover;\n\tbgActive: windowBgRipple;\n\tfg: emojiIconFg;\n\tfgActive: emojiSubIconFgActive;\n\tfadeLeft: icon {{ \"fade_horizontal-flip_horizontal\", emojiPanHover }};\n\tfadeRight: icon {{ \"fade_horizontal\", emojiPanHover }};\n\tfield: defaultTabbedSearchField;\n\tsearch: defaultTabbedSearchButton;\n\tback: defaultTabbedSearchBack;\n\tcancel: defaultTabbedSearchCancel;\n\tdefaultFieldWidth: 103px;\n\tgroupWidth: 30px;\n\tgroupSkip: 2px;\n\theight: 33px;\n}\n\ninlineResultsMinHeight: 278px;\ninlineResultsMaxHeight: 640px;\n\nemojiPanHeaderFont: semiboldFont;\nemojiPanRemoveSkip: 10px;\nemojiPanRemoveTop: 10px;\nemojiPanColorAllSkip: 9px;\n\nemojiColorsPadding: 8px;\nemojiColorsSep: 1px;\nemojiColorsSepColor: shadowFg;\n\nemojiIconSelectSkip: 3px;\nemojiPremiumRequired: icon{{ \"emoji/premium_lock\", windowSubTextFg }};\n\nhashtagClose: IconButton {\n\twidth: 30px;\n\theight: 30px;\n\n\ticon: smallCloseIcon;\n\ticonOver: smallCloseIconOver;\n\ticonPosition: point(10px, 10px);\n\n\trippleAreaPosition: point(5px, 5px);\n\trippleAreaSize: 20px;\n\tripple: defaultRippleAnimationBgOver;\n}\n\nstickerPanWidthMin: 64px;\nstickerPanSize: size(stickerPanWidthMin, stickerPanWidthMin);\nstickerEffectWidthMin: 48px;\nstickerPanPadding: 11px;\nstickerPanDeleteIconBg: icon {{ \"emoji/emoji_delete_bg\", stickerPanDeleteBg }};\nstickerPanDeleteIconFg: icon {{ \"emoji/emoji_delete\", stickerPanDeleteFg }};\nstickerPanDeleteOpacityBg: 0.3;\nstickerPanDeleteOpacityBgOver: 0.5;\nstickerPanDeleteOpacityFg: 0.8;\nstickerPanDeleteOpacityFgOver: 1.;\nstickerPanRemoveSet: IconButton(hashtagClose) {\n\twidth: 20px;\n\theight: 20px;\n\ticonPosition: point(-1px, -1px);\n\trippleAreaPosition: point(0px, 0px);\n}\nstickerIconMove: 400;\nstickerPreviewDuration: 150;\nstickerPreviewMin: 0.1;\nmediaPreviewPhotoSkip: 48px;\n\nemojiPanColorAll: IconButton(stickerPanRemoveSet) {\n\twidth: 24px;\n\theight: 24px;\n\trippleAreaSize: 24px;\n\ticon: icon {{ \"emoji/emoji_skin\", smallCloseIconFg }};\n\ticonOver: icon {{ \"emoji/emoji_skin\", smallCloseIconFgOver }};\n}\nemojiPanColorAllLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n\talign: align(top);\n\tminWidth: 40px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px);\n\t}\n}\nemojiPanColorAllPadding: margins(10px, 6px, 10px, -1px);\n\nstickerGroupCategorySize: 28px;\nstickerGroupCategoryAbout: defaultTextStyle;\nstickerGroupCategoryAddMargin: margins(0px, 10px, 0px, 5px);\nstickerGroupCategoryAdd: stickersTrendingAdd;\n\nstickersToast: Toast(defaultToast) {\n\tminWidth: 340px;\n\tmaxWidth: 340px;\n\tpadding: margins(16px, 13px, 16px, 12px);\n}\n\nstickersEmpty: icon {{ \"stickers_empty\", windowSubTextFg }};\nemojiEmpty: icon {{ \"emoji_empty\", windowSubTextFg }};\n\neditMediaButtonSize: 32px;\n\neditMediaButtonIconFile: icon {{ \"send_media/send_media_more\", menuIconFg }};\neditMediaButton: IconButton(defaultIconButton) {\n\twidth: editMediaButtonSize;\n\theight: editMediaButtonSize;\n\n\ticon: editMediaButtonIconFile;\n\n\trippleAreaSize: editMediaButtonSize;\n\tripple: defaultRippleAnimation;\n}\n\nsendBoxAlbumGroupEditInternalSkip: 8px;\nsendBoxAlbumGroupSkipRight: 5px;\nsendBoxAlbumGroupSkipTop: 5px;\nsendBoxAlbumGroupSize: size(48px, 26px);\nsendBoxAlbumGroupSizeVertical: size(30px, 50px);\nsendBoxAlbumSmallGroupSize: size(30px, 25px);\nsendBoxAlbumSmallGroupCircleSize: 27px;\n\nsendBoxFileGroupSkipTop: 2px;\nsendBoxFileGroupSkipRight: 5px;\nsendBoxFileGroupEditInternalSkip: -1px;\n\nsendBoxAlbumGroupButtonFile: IconButton(editMediaButton) {\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: windowBgRipple;\n\t}\n}\nsendBoxAlbumGroupEditButtonIconFile: editMediaButtonIconFile;\nsendBoxAlbumGroupDeleteButtonIconFile: icon {{ \"send_media/send_media_cross\", menuIconFg }};\n\nsendBoxAlbumButtonMediaMore: icon {{ \"send_media/send_media_more\", roundedFg }};\nsendBoxAlbumGroupButtonMediaMore: icon {{ \"send_media/send_media_more\", roundedFg, point(4px, 1px) }};\nsendBoxAlbumGroupButtonMediaDelete: icon {{ \"send_media/send_media_cross\", roundedFg, point(-2px, 1px) }};\n\ndefaultComposeIcons: ComposeIcons {\n\tsettings: icon {{ \"emoji/emoji_settings\", emojiIconFg }};\n\tcollectibles: icon {{ \"menu/unique\", emojiIconFg }};\n\n\trecent: icon {{ \"emoji/emoji_recent\", emojiIconFg }};\n\trecentActive: icon {{ \"emoji/emoji_recent\", emojiSubIconFgActive }};\n\tpeople: icon {{ \"emoji/emoji_smile\", emojiIconFg }};\n\tpeopleActive: icon {{ \"emoji/emoji_smile\", emojiSubIconFgActive }};\n\tnature: icon {{ \"emoji/emoji_nature\", emojiIconFg }};\n\tnatureActive: icon {{ \"emoji/emoji_nature\", emojiSubIconFgActive }};\n\tfood: icon {{ \"emoji/emoji_food\", emojiIconFg }};\n\tfoodActive: icon {{ \"emoji/emoji_food\", emojiSubIconFgActive }};\n\tactivity: icon {{ \"emoji/emoji_activities\", emojiIconFg }};\n\tactivityActive: icon {{ \"emoji/emoji_activities\", emojiSubIconFgActive }};\n\ttravel: icon {{ \"emoji/emoji_travel\", emojiIconFg }};\n\ttravelActive: icon {{ \"emoji/emoji_travel\", emojiSubIconFgActive }};\n\tobjects: icon {{ \"emoji/emoji_objects\", emojiIconFg }};\n\tobjectsActive: icon {{ \"emoji/emoji_objects\", emojiSubIconFgActive }};\n\tsymbols: icon {{ \"emoji/emoji_love\", emojiIconFg }};\n\tsymbolsActive: icon {{ \"emoji/emoji_love\", emojiSubIconFgActive }};\n\n\tmenuFave: menuIconFave;\n\tmenuUnfave: menuIconUnfave;\n\tmenuStickerSet: menuIconStickers;\n\tmenuRecentRemove: menuIconDelete;\n\tmenuGifAdd: menuIconGif;\n\tmenuGifRemove: menuIconDelete;\n\tmenuMute: menuIconMute;\n\tmenuSchedule: menuIconSchedule;\n\tmenuWhenOnline: menuIconWhenOnline;\n\tmenuSpoiler: menuIconSpoiler;\n\tmenuBelow: menuIconBelow;\n\tmenuAbove: menuIconAbove;\n\tmenuQualityHigh: menuIconQualityHigh;\n\tmenuPrice: menuIconEarn;\n\n\tstripBubble: icon{\n\t\t{ \"chat/reactions_bubble_shadow\", windowShadowFg },\n\t\t{ \"chat/reactions_bubble\", emojiPanBg },\n\t};\n\tstripExpandPanel: icon{\n\t\t{ \"chat/reactions_round_big\", windowBgRipple },\n\t\t{ \"chat/reactions_expand_panel\", windowSubTextFg },\n\t};\n\tstripExpandDropdown: icon{\n\t\t{ \"chat/reactions_round_small\", windowBgRipple },\n\t\t{ \"chat/reactions_expand_panel\", windowSubTextFg },\n\t};\n}\ndefaultEmojiPanAbout: FlatLabel(defaultFlatLabel) {\n\tminWidth: 10px;\n\talign: align(top);\n\ttextFg: windowSubTextFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(11px);\n\t}\n}\ndefaultEmojiPan: EmojiPan {\n\tmargin: margins(7px, 0px, 7px, 0px);\n\tpadding: margins(7px, 0px, 4px, 7px);\n\tshowAnimation: emojiPanAnimation;\n\tdesiredSize: 37px;\n\tverticalSizeSub: 1px;\n\theader: 33px;\n\theaderLeft: 14px;\n\theaderLockLeft: 7px;\n\theaderLockedLeft: 26px;\n\theaderTop: 10px;\n\tfooter: 36px;\n\ticonSkip: 3px;\n\ticonWidth: 30px;\n\ticonArea: 28px;\n\tbg: emojiPanBg;\n\theaderFg: emojiPanHeaderFg;\n\ttrendingHeaderFg: stickersTrendingHeaderFg;\n\ttrendingSubheaderFg: stickersTrendingSubheaderFg;\n\ttrendingUnreadFg: stickersFeaturedUnreadBg;\n\toverBg: emojiPanHover;\n\tpathBg: windowBgRipple;\n\tpathFg: windowBgOver;\n\ttextFg: windowFg;\n\tcategoriesBg: emojiPanCategories;\n\tcategoriesBgOver: windowBgRipple;\n\tfadeLeft: icon {{ \"fade_horizontal-flip_horizontal\", emojiPanCategories }};\n\tfadeRight: icon {{ \"fade_horizontal\", emojiPanCategories }};\n\tmenu: popupMenuWithIcons;\n\texpandedSeparator: MenuSeparator(defaultMenuSeparator) {\n\t\tpadding: margins(0px, 4px, 0px, 4px);\n\t\twidth: 6px;\n\t}\n\ttabs: emojiTabs;\n\tsearch: defaultTabbedSearch;\n\tsearchMargin: margins(1px, 11px, 2px, 5px);\n\tcolorAll: emojiPanColorAll;\n\tcolorAllLabel: emojiPanColorAllLabel;\n\tremoveSet: stickerPanRemoveSet;\n\tboxLabel: boxLabel;\n\ticons: defaultComposeIcons;\n\tabout: defaultEmojiPanAbout;\n\taboutPadding: margins(12px, 3px, 12px, 2px);\n\tautocompleteBottomSkip: 0px;\n}\nstatusEmojiPan: EmojiPan(defaultEmojiPan) {\n\tcategoriesBg: windowBg;\n\tcategoriesBgOver: windowBgOver;\n\tfadeLeft: icon {{ \"fade_horizontal-flip_horizontal\", windowBg }};\n\tfadeRight: icon {{ \"fade_horizontal\", windowBg }};\n}\nbackgroundEmojiPan: EmojiPan(defaultEmojiPan) {\n\tpadding: margins(7px, 7px, 4px, 0px);\n}\n\ninlineBotsScroll: ScrollArea(defaultSolidScroll) {\n\tdeltat: stickerPanPadding;\n\tdeltab: stickerPanPadding;\n}\n\ngifsPadding: margins(9px, 5px, 3px, 9px);\n\nemojiSuggestionsDropdown: InnerDropdown(defaultInnerDropdown) {\n\tscrollMargin: margins(0px, emojiColorsPadding, 0px, emojiColorsPadding);\n\tscrollPadding: margins(0px, 0px, 0px, 0px);\n}\nemojiSuggestionSize: 40px;\nemojiSuggestionsScrolledWidth: 240px;\nemojiSuggestionsPadding: margins(emojiColorsPadding, 0px, emojiColorsPadding, 0px);\nemojiSuggestionsFadeAfter: 20px;\n\ndefaultEmojiSuggestions: EmojiSuggestions {\n\tdropdown: emojiSuggestionsDropdown;\n\tbg: menuBg;\n\toverBg: emojiPanHover;\n\ttextFg: windowFg;\n\tfadeLeft: icon {{ \"fade_horizontal-flip_horizontal\", boxBg }};\n\tfadeRight: icon {{ \"fade_horizontal\", boxBg }};\n}\n\nmentionHeight: 40px;\nmentionPadding: margins(8px, 5px, 8px, 5px);\nmentionTop: 11px;\nmentionFont: linkFont;\nmentionNameFg: windowFg;\nmentionNameFgOver: windowFgOver;\nmentionPhotoSize: 33px;\nmentionBg: windowBg;\nmentionBgOver: windowBgOver;\nmentionFg: windowSubTextFg;\nmentionFgOver: windowSubTextFgOver;\nmentionFgActive: windowActiveTextFg;\nmentionFgOverActive: windowActiveTextFg;\n\nautocompleteSearchPadding: margins(16px, 5px, 16px, 5px);\nautocompleteRowPadding: margins(16px, 5px, 16px, 5px);\nautocompleteRowTitle: semiboldTextStyle;\nautocompleteRowKeys: defaultTextStyle;\nautocompleteRowAnswer: defaultTextStyle;\n\nmanageEmojiPreview: 22px;\nmanageEmojiPreviewWidth: 48px;\nmanageEmojiPreviewHeight: 48px;\nmanageEmojiPreviewPadding: margins(22px, 9px, 19px, 9px);\nmanageEmojiMarginRight: 21px;\nmanageEmojiNameTop: 3px;\nmanageEmojiStatusTop: 25px;\n\ninlineRadialSize: 44px;\ninlineFileSize: 44px;\ninlineResultsLeft: 11px;\ninlineResultsSkip: 3px;\ninlineMediaHeight: 96px;\ninlineThumbSize: 64px;\ninlineThumbSkip: 10px;\ninlineTitleFg: windowFg;\ninlineDescriptionFg: windowSubTextFg;\ninlineRowMargin: 6px;\ninlineRowBorder: 1px;\ninlineRowBorderFg: shadowFg;\ninlineRowFileNameTop: 2px;\ninlineRowFileDescriptionTop: 23px;\ninlineResultsMinWidth: 48px;\ninlineDurationMargin: 3px;\n\nstickersPremiumLock: icon{{ \"emoji/premium_lock\", premiumButtonFg }};\nemojiPremiumLock: icon{{ \"chat/mini_lock\", premiumButtonFg }};\n\nreactStripExtend: margins(21px, 49px, 39px, 0px);\nreactStripHeight: 40px;\nreactStripSize: 32px;\nreactStripMinWidth: 60px;\nreactStripImage: 26px;\nreactStripSkip: 7px;\nreactStripBubbleRight: 20px;\nuserpicBuilderEmojiPan: EmojiPan(statusEmojiPan) {\n\tmargin: margins(reactStripSkip, 0px, reactStripSkip, 0px);\n\tpadding: margins(reactStripSkip, 0px, reactStripSkip, reactStripSkip);\n\tdesiredSize: reactStripSize;\n\tverticalSizeSub: 0px;\n\toverBg: transparent;\n\tsearch: TabbedSearch(defaultTabbedSearch) {\n\t\tdefaultFieldWidth: 88px;\n\t}\n}\nreactPanelEmojiPan: EmojiPan(userpicBuilderEmojiPan) {\n\tsearchMargin: margins(1px, 10px, 2px, 6px);\n}\nemojiScroll: ScrollArea(defaultSolidScroll) {\n\tdeltat: 3px;\n\tdeltab: 3px;\n\tround: -1px;\n\twidth: 7px;\n\tdeltax: 2px;\n\thiding: 0;\n}\nreactPanelScroll: ScrollArea(emojiScroll) {\n\tdeltab: 7px;\n}\nreactPanelScrollRounded: ScrollArea(emojiScroll) {\n\twidth: 8px;\n\tdeltax: 3px;\n\tdeltat: 14px;\n\tdeltab: 14px;\n}\nselfForwardsTaggerStripSkip: 26px;\n\nselfForwardsTaggerIconPadding: margins(14px, 2px, 8px, 2px);\nselfForwardsTaggerToast: Toast(defaultToast) {\n\tminWidth: 160px;\n\tmaxWidth: 380px;\n\tradius: 9px;\n\tpadding: margins(19px, 12px, 19px, 12px);\n}\njoinChatAddToFilterToast: Toast(defaultToast) {\n\tminWidth: 160px;\n\tmaxWidth: 380px;\n\tradius: 9px;\n\tpadding: margins(7px, 12px, 44px, 12px);\n}\njoinChatAddToFilterToastButton: IconButton(defaultIconButton) {\n\twidth: 40px;\n\theight: 40px;\n\n\ticon: icon{\n\t\t{ \"chat/reactions_round_small\", toastFg },\n\t\t{ \"chat/reactions_expand_panel\", toastBg },\n\t};\n\ticonOver: icon{\n\t\t{ \"chat/reactions_round_small\", toastFg },\n\t\t{ \"chat/reactions_expand_panel\", toastBg },\n\t};\n\trippleAreaPosition: point(10px, 10px);\n\trippleAreaSize: 20px;\n\tripple: universalRippleAnimation;\n}\n\nchoosePeerGroupIcon: icon {{ \"info/edit/create_group\", lightButtonFg }};\nchoosePeerChannelIcon: icon {{ \"info/edit/create_channel\", lightButtonFg }};\nchoosePeerCreateIconLeft: 25px;\n\nhistoryRequestsUserpics: GroupCallUserpics {\n\tsize: 22px;\n\tshift: 8px;\n\tstroke: 4px;\n\talign: align(left);\n}\nhistoryRequestsHeight: 33px;\n\nhistorySlowmodeCounterMargins: margins(0px, 0px, 10px, 0px);\n\nhistoryComposeAreaPalette: TextPalette(defaultTextPalette) {\n\tlinkFg: historyComposeAreaFgService;\n}\n\ndefaultMessageBar: MessageBar {\n\ttitle: semiboldTextStyle;\n\ttitleFg: windowActiveTextFg;\n\ttext: defaultTextStyle;\n\ttextFg: historyComposeAreaFg;\n\ttextPalette: historyComposeAreaPalette;\n\tduration: 160;\n}\n\nhistoryComposeButton: FlatButton {\n\tcolor: windowActiveTextFg;\n\toverColor: windowActiveTextFg;\n\n\tbgColor: historyComposeButtonBg;\n\toverBgColor: historyComposeButtonBgOver;\n\n\twidth: -32px;\n\theight: 46px;\n\n\ttextTop: 14px;\n\n\tfont: semiboldFont;\n\toverFont: semiboldFont;\n\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: historyComposeButtonBgRipple;\n\t}\n}\nhistoryComposeButtonText: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n\ttextFg: windowActiveTextFg;\n}\nhistoryGiftToChannel: IconButton(defaultIconButton) {\n\twidth: 46px;\n\theight: 46px;\n\n\ticon: icon{{ \"menu/gift_premium\", windowActiveTextFg }};\n\ticonOver: icon{{ \"menu/gift_premium\", windowActiveTextFg }};\n\n\trippleAreaPosition: point(3px, 3px);\n\trippleAreaSize: 40px;\n\tripple: universalRippleAnimation;\n}\nhistoryDirectMessage: IconButton(historyGiftToChannel) {\n\ticon: icon{{ \"menu/chat_discuss\", windowActiveTextFg }};\n\ticonOver: icon{{ \"menu/chat_discuss\", windowActiveTextFg }};\n}\nhistoryUnblock: FlatButton(historyComposeButton) {\n\tcolor: attentionButtonFg;\n\toverColor: attentionButtonFgOver;\n}\nhistoryContactStatusButton: FlatButton(historyComposeButton) {\n\theight: 49px;\n\ttextTop: 16px;\n\toverBgColor: historyComposeButtonBg;\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: historyComposeButtonBgOver;\n\t}\n}\nhistoryContactStatusBlock: FlatButton(historyContactStatusButton) {\n\tcolor: attentionButtonFg;\n\toverColor: attentionButtonFg;\n}\nhistoryContactStatusLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 240px;\n}\nhistoryEmojiStatusInfoLabel: FlatLabel(historyContactStatusLabel) {\n\talign: align(top);\n\ttextFg: windowSubTextFg;\n}\nhistoryContactStatusMinSkip: 16px;\n\nhistorySetBotPhotoIcon: icon {{ \"menu/photo_set\", windowActiveTextFg }};\nhistorySetBotPhotoIconSize: 49px;\nhistorySetBotPhotoLabel: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n\ttextFg: windowActiveTextFg;\n\talign: align(center);\n\tmaxHeight: 30px;\n}\nhistorySetBotPhotoLabelMarginRight: 20px;\n\nhistoryBusinessBotPhoto: UserpicButton(defaultUserpicButton) {\n\tsize: size(46px, 46px);\n\tphotoSize: 46px;\n\tphotoPosition: point(0px, 0px);\n}\nhistoryBusinessBotName: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n}\nhistoryBusinessBotStatus: FlatLabel(historyContactStatusLabel) {\n\ttextFg: windowSubTextFg;\n}\nhistoryBusinessBotToggle: defaultActiveButton;\nhistoryBusinessBotSettings: IconButton(defaultIconButton) {\n\ticon: icon{{ \"menu/customize\", menuIconFg }};\n\ticonOver: icon{{ \"menu/customize\", menuIconFgOver }};\n\ticonPosition: point(-1px, -1px);\n\trippleAreaSize: 40px;\n\trippleAreaPosition: point(4px, 9px);\n\tripple: defaultRippleAnimationBgOver;\n\theight: 58px;\n\twidth: 48px;\n}\npaysStatusLabel: FlatLabel(historyBusinessBotStatus) {\n\talign: align(top);\n\tminWidth: 240px;\n}\n\nhistoryReplyCancelIcon: icon {{ \"box_button_close\", historyReplyCancelFg }};\nhistoryReplyCancelIconOver: icon {{ \"box_button_close\", historyReplyCancelFgOver }};\n\nhistoryReplySkip: 53px;\nhistoryReplyNameFg: windowActiveTextFg;\nhistoryReplyHeight: 49px;\nhistoryReplyIconPosition: point(7px, 7px);\nhistoryReplyIcon: icon {{ \"chat/input_reply_settings\", historyReplyIconFg }};\nhistoryLinkIcon: icon {{ \"chat/input_link_settings\", historyReplyIconFg }};\nhistoryQuoteIcon: icon {{ \"chat/input_reply_quote\", historyReplyIconFg }};\nhistoryForwardIcon: icon {{ \"chat/input_forward\", historyReplyIconFg }};\nhistoryEditIcon: icon {{ \"chat/input_edit\", historyReplyIconFg }};\nhistoryReplyCancel: IconButton {\n\twidth: 49px;\n\theight: 49px;\n\n\ticon: historyReplyCancelIcon;\n\ticonOver: historyReplyCancelIconOver;\n\ticonPosition: point(-1px, -1px);\n\n\trippleAreaPosition: point(4px, 4px);\n\trippleAreaSize: 40px;\n\tripple: defaultRippleAnimationBgOver;\n}\nhistoryPinnedShowAll: IconButton(historyReplyCancel) {\n\ticon: icon {{ \"pinned_show_all\", historyReplyCancelFg }};\n\ticonOver: icon {{ \"pinned_show_all\", historyReplyCancelFgOver }};\n}\nhistoryPinnedBotButton: RoundButton(defaultActiveButton) {\n\twidth: -34px;\n\theight: 30px;\n\ttextTop: 6px;\n\tpadding: margins(2px, 10px, 10px, 9px);\n}\nhistoryPinnedBotLabel: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n\talign: align(center);\n\tmaxHeight: 30px;\n}\nhistoryPinnedBotButtonMaxWidth: 120px;\n\nhistoryToDownPosition: point(12px, 10px);\nhistoryToDownAbove: icon {{ \"history_down_arrow\", historyToDownFg }};\nhistoryToDownAboveOver: icon {{ \"history_down_arrow\", historyToDownFgOver }};\nhistoryToDownPaddingTop: 10px;\nhistoryToDownBelow: icon {\n\t{ \"history_down_shadow\", historyToDownShadow },\n\t{ \"history_down_circle\", historyToDownBg },\n};\nhistoryToDownBelowOver: icon {\n\t{ \"history_down_shadow\", historyToDownShadow },\n\t{ \"history_down_circle\", historyToDownBgOver },\n};\nhistoryToDown: TwoIconButton {\n\twidth: 52px;\n\theight: 62px;\n\n\ticonBelow: historyToDownBelow;\n\ticonBelowOver: historyToDownBelowOver;\n\ticonAbove: historyToDownAbove;\n\ticonAboveOver: historyToDownAboveOver;\n\ticonPosition: point(0px, historyToDownPaddingTop);\n\n\trippleAreaPosition: point(5px, 15px);\n\trippleAreaSize: 42px;\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: historyToDownBgRipple;\n\t}\n}\nhistoryToDownBadgeFont: semiboldFont;\nhistoryToDownBadgeSize: 22px;\n\nhistoryToDownShownAfter: 480px;\nhistoryToDownDuration: 150;\n\ndialogsToUpAbove: icon {{ \"history_down_arrow-flip_vertical\", historyToDownFg, point(0px, 1px) }};\ndialogsToUpAboveOver: icon {{ \"history_down_arrow-flip_vertical\", historyToDownFgOver, point(0px, 1px) }};\n\ndialogsToUp: TwoIconButton(historyToDown) {\n\ticonAbove: dialogsToUpAbove;\n\ticonAboveOver: dialogsToUpAboveOver;\n}\n\nhistoryUnreadMentions: TwoIconButton(historyToDown) {\n\ticonAbove: icon {{ \"history_unread_mention\", historyToDownFg }};\n\ticonAboveOver: icon {{ \"history_unread_mention\", historyToDownFgOver }};\n}\nhistoryUnreadReactions: TwoIconButton(historyToDown) {\n\ticonAbove: icon {{ \"history_unread_reaction\", historyToDownFg }};\n\ticonAboveOver: icon {{ \"history_unread_reaction\", historyToDownFgOver }};\n}\nhistoryUnreadPollVotes: TwoIconButton(historyToDown) {\n\ticonAbove: icon {{ \"history_unread_poll_vote-40x40\", historyToDownFg, point(6px, 6px) }};\n\ticonAboveOver: icon {{ \"history_unread_poll_vote-40x40\", historyToDownFgOver, point(6px, 6px) }};\n}\nhistoryUnreadThingsSkip: 4px;\n\nhistoryQuoteStyle: QuoteStyle(defaultQuoteStyle) {\n\tpadding: margins(10px, 2px, 4px, 2px);\n\tverticalSkip: 4px;\n\toutline: 3px;\n\toutlineShift: 2px;\n\tradius: 5px;\n}\nhistoryTextStyle: TextStyle(defaultTextStyle) {\n\tblockquote: QuoteStyle(historyQuoteStyle) {\n\t\tpadding: margins(10px, 2px, 20px, 2px);\n\t\ticon: icon{{ \"chat/mini_quote\", windowFg }};\n\t\ticonPosition: point(4px, 4px);\n\t\texpand: icon{{ \"intro_country_dropdown\", windowFg }};\n\t\texpandPosition: point(6px, 4px);\n\t\tcollapse: icon{{ \"intro_country_dropdown-flip_vertical\", windowFg }};\n\t\tcollapsePosition: point(6px, 4px);\n\t}\n\tpre: QuoteStyle(historyQuoteStyle) {\n\t\theader: 20px;\n\t\theaderPosition: point(10px, 2px);\n\t\tscrollable: true;\n\t\ticon: icon{{ \"chat/mini_copy\", windowFg }};\n\t\ticonPosition: point(4px, 2px);\n\t}\n}\nhistoryComposeField: InputField(defaultInputField) {\n\tstyle: historyTextStyle;\n\ttextMargins: margins(0px, 0px, 0px, 0px);\n\ttextAlign: align(left);\n\ttextFg: historyComposeAreaFg;\n\ttextBg: historyComposeAreaBg;\n\theightMin: 36px;\n\theightMax: 72px;\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(2px, 0px, 2px, 0px);\n\tplaceholderAlign: align(topleft);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n\tplaceholderShift: -50px;\n\tborder: 0px;\n\tborderActive: 0px;\n\tduration: 100;\n}\nhistoryComposeFieldMaxHeight: 224px;\nhistoryComposeFieldFadeHeight: 6px;\nhistorySendRight: 2px;\nhistorySendPadding: 9px;\n// historyMinHeight: 56px;\n\nhistoryAttach: IconButton(defaultIconButton) {\n\twidth: 44px;\n\theight: 46px;\n\n\ticon: icon {{ \"chat/input_attach\", historyComposeIconFg }};\n\ticonOver: icon {{ \"chat/input_attach\", historyComposeIconFgOver }};\n\n\trippleAreaPosition: point(2px, 3px);\n\trippleAreaSize: 40px;\n\tripple: defaultRippleAnimationBgOver;\n}\n\nhistoryMessagesTTL: IconButtonWithText {\n\ticonButton: IconButton(historyAttach) {\n\t\ticon: icon {{ \"chat/input_autodelete\", historyComposeIconFg }};\n\t\ticonOver: icon {{ \"chat/input_autodelete\", historyComposeIconFgOver }};\n\t}\n\ttextFg: historyComposeIconFg;\n\ttextFgOver: historyComposeIconFgOver;\n\ttextPadding: margins(21px, 20px, 3px, 7px);\n\ttextAlign: align(left);\n\n\tfont: font(10px semibold);\n}\nhistoryReplaceMedia: IconButton(historyAttach) {\n\ticon: icon {{ \"chat/input_replace\", windowBgActive }};\n\ticonOver: icon {{ \"chat/input_replace\", windowBgActive }};\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: lightButtonBgOver;\n\t}\n}\nhistoryAddMedia: IconButton(historyAttach) {\n\ticon: icon {{ \"chat/input_attach\", windowBgActive }};\n\ticonOver: icon {{ \"chat/input_attach\", windowBgActive }};\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: lightButtonBgOver;\n\t}\n}\n\nhistoryAttachEmojiActive: icon {{ \"chat/input_smile_face\", windowBgActive }};\nhistoryEmojiCircle: size(20px, 20px);\nhistoryEmojiCircleLine: 1.5;\nhistoryEmojiCircleFg: historyComposeIconFg;\nhistoryEmojiCircleFgOver: historyComposeIconFgOver;\nhistoryBotKeyboardShow: IconButton(historyAttach) {\n\ticon: icon {{ \"chat/input_bot_keyboard\", historyComposeIconFg }};\n\ticonOver: icon {{ \"chat/input_bot_keyboard\", historyComposeIconFgOver }};\n}\nhistoryBotKeyboardHide: IconButton(historyAttach) {\n\ticon: icon {{ \"chat/input_bot_keyboard_hide\", historyComposeIconFg }};\n\ticonOver: icon {{ \"chat/input_bot_keyboard_hide\", historyComposeIconFgOver }};\n}\nhistoryBotCommandStart: IconButton(historyAttach) {\n\ticon: icon {{ \"chat/input_bot_command\", historyComposeIconFg }};\n\ticonOver: icon {{ \"chat/input_bot_command\", historyComposeIconFgOver }};\n}\nhistoryScheduledToggle: IconButton(historyAttach) {\n\ticon: icon {\n\t\t{ \"chat/input_scheduled\", historyComposeIconFg },\n\t\t{ \"chat/input_scheduled_dot\", attentionButtonFg }\n\t};\n\ticonOver: icon {\n\t\t{ \"chat/input_scheduled\", historyComposeIconFgOver },\n\t\t{ \"chat/input_scheduled_dot\", attentionButtonFg }\n\t};\n}\nhistoryGiftToUser: IconButton(historyAttach) {\n\ticon: icon {{ \"chat/input_gift\", historyComposeIconFg }};\n\ticonOver: icon {{ \"chat/input_gift\", historyComposeIconFgOver }};\n}\nhistorySuggestPostToggle: IconButton(historyAttach) {\n\ticon: icon{{ \"chat/input_paid\", historyComposeIconFg }};\n\ticonOver: icon{{ \"chat/input_paid\", historyComposeIconFgOver }};\n}\nhistorySuggestIconPosition: point(4px, 4px);\nhistorySuggestIconActive: icon{{ \"chat/input_paid\", windowActiveTextFg }};\n\nsuggestOptionsPrice: InputField(defaultInputField) {\n\ttextBg: transparent;\n\ttextMargins: margins(2px, 20px, 2px, 0px);\n\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(2px, 0px, 2px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n\n\tborder: 0px;\n\tborderActive: 0px;\n\n\theightMin: 32px;\n\n\tstyle: defaultTextStyle;\n}\n\nhistoryAttachEmojiInner: IconButton(historyAttach) {\n\ticon: icon {{ \"chat/input_smile_face\", historyComposeIconFg }};\n\ticonOver: icon {{ \"chat/input_smile_face\", historyComposeIconFgOver }};\n}\nhistoryAttachEmoji: EmojiButton {\n\tinner: historyAttachEmojiInner;\n\tbg: historyComposeAreaBg;\n\tlineFg: historyEmojiCircleFg;\n\tlineFgOver: historyEmojiCircleFgOver;\n}\nboxAttachEmoji: EmojiButton(historyAttachEmoji) {\n\tbg: transparent;\n\tinner: IconButton(historyAttachEmojiInner) {\n\t\twidth: 30px;\n\t\theight: 30px;\n\t\trippleAreaSize: 0px;\n\t}\n}\nboxAttachEmojiTop: 20px;\n\nhistorySendIcon: icon {{ \"chat/input_send\", historySendIconFg }};\nhistorySendIconOver: icon {{ \"chat/input_send\", historySendIconFgOver }};\nhistorySendSize: size(44px, 46px);\nhistoryScheduleIcon: icon {{ \"chat/input_schedule\", historyComposeAreaBg }};\nhistoryScheduleIconPosition: point(7px, 8px);\nhistoryEditSaveIcon: icon {{ \"chat/input_save\", historySendIconFg }};\nhistoryEditSaveIconOver: icon {{ \"chat/input_save\", historySendIconFgOver }};\n\nhistoryEditMediaBg: videoPlayIconBg;\nhistoryEditMedia: icon{{ \"chat/input_draw\", videoPlayIconFg }};\nhistoryMessagesTTLPickerHeight: 200px;\nhistoryMessagesTTLPickerItemHeight: 40px;\nhistoryMessagesTTLLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 200px;\n\talign: align(topleft);\n\ttextFg: windowSubTextFg;\n}\n\nhistoryCharsLimitationLabel: FlatLabel(defaultFlatLabel) {\n\t// The same as a width of the historySendSize.\n\tminWidth: 44px;\n\talign: align(center);\n\ttextFg: attentionButtonFg;\n}\n\nhistoryRecordVoiceFg: historyComposeIconFg;\nhistoryRecordVoiceFgOver: historyComposeIconFgOver;\nhistoryRecordVoiceFgInactive: attentionButtonFg;\nhistoryRecordVoiceFgActive: windowBgActive;\nhistoryRecordVoiceFgActiveIcon: windowFgActive;\nhistoryRecordVoiceOnceBg: icon {{ \"voice_lock/audio_once_bg\", historySendIconFg }};\nhistoryRecordVoiceOnceBgOver: icon {{ \"voice_lock/audio_once_bg\", historySendIconFgOver }};\nhistoryRecordVoiceOnceFg: icon {{ \"voice_lock/audio_once_number\", windowFgActive }};\nhistoryRecordVoiceOnceFgOver: icon {{ \"voice_lock/audio_once_number\", windowFgActive }};\nhistoryRecordVoiceOnceInactive: icon {{ \"chat/audio_once\", windowSubTextFg }};\nhistoryRecordSendIconPosition: point(2px, 0px);\nhistoryRecordVoiceRippleBgActive: lightButtonBgOver;\nhistoryRecordSignalRadius: 5px;\nhistoryRecordCancel: windowSubTextFg;\nhistoryRecordCancelActive: historySendIconFg;\nhistoryRecordFont: font(13px);\nhistoryRecordDurationSkip: 12px;\nhistoryRecordDurationFg: historyComposeAreaFg;\nhistoryRecordTTLLineWidth: 2px;\n\nhistoryRecordMainBlobMinRadius: 23px;\nhistoryRecordMainBlobMaxRadius: 37px;\nhistoryRecordMinorBlobMinRadius: 40px;\nhistoryRecordMinorBlobMaxRadius: 47px;\nhistoryRecordMajorBlobMinRadius: 43px;\nhistoryRecordMajorBlobMaxRadius: 50px;\n\nhistoryRecordTextStyle: TextStyle(defaultTextStyle) {\n\tfont: historyRecordFont;\n}\n\nhistoryRecordTextWidthForWrap: 210px;\nhistoryRecordTextLeft: 15px;\nhistoryRecordTextRight: 25px;\n\nhistoryRecordLockShowDuration: historyToDownDuration;\nhistoryRecordLockSize: size(75px, 133px);\n\nhistoryRecordLockIconSize: size(14px, 17px);\nhistoryRecordLockIconBottomHeight: 9px;\nhistoryRecordLockIconLineHeight: 2px;\nhistoryRecordLockIconLineSkip: 3px;\nhistoryRecordLockIconLineWidth: 2px;\nhistoryRecordLockIconArcHeight: 4px;\nhistoryRecordStopIconWidth: 12px;\n\nhistoryRecordLockTopShadow: icon {{ \"voice_lock/record_lock_top_shadow\", historyToDownShadow }};\nhistoryRecordLockTop: icon {{ \"voice_lock/record_lock_top\", historyToDownBg }};\nhistoryRecordLockBottomShadow: icon {{ \"voice_lock/record_lock_bottom_shadow\", historyToDownShadow }};\nhistoryRecordLockBottom: icon {{ \"voice_lock/record_lock_bottom\", historyToDownBg }};\nhistoryRecordLockBodyShadow: icon {{ \"voice_lock/record_lock_body_shadow\", historyToDownShadow }};\nhistoryRecordLockBody: icon {{ \"voice_lock/record_lock_body\", historyToDownBg }};\nhistoryRecordLockMargin: margins(4px, 4px, 4px, 4px);\nhistoryRecordLockArrow: icon {{ \"voice_lock/voice_arrow\", historyToDownFg }};\nhistoryRecordLockInput: icon {{ \"voice_lock/input_mic_s\", historyToDownFg }};\nhistoryRecordLockRound: icon {{ \"voice_lock/input_round_s\", historyToDownFg }};\nhistoryRecordLockRippleMargin: margins(6px, 6px, 6px, 6px);\n\nhistoryRecordDelete: IconButton(historyAttach) {\n\ticon: icon {{ \"voice_lock/recorded_delete\", historyComposeIconFg }};\n\ticonOver: icon {{ \"voice_lock/recorded_delete\", historyComposeIconFgOver }};\n\ticonPosition: point(10px, 11px);\n}\nhistoryRecordWaveformRightSkip: 10px;\nhistoryRecordWaveformBgMargins: margins(5px, 8px, 5px, 9px);\nhistoryRecordWaveformBgRadius: 7px;\nhistoryRecordWaveformOutsideAlpha: 0.6;\nhistoryRecordWaveformInactiveAlpha: 0.7;\nhistoryRecordCenterControlHeight: 18px;\nhistoryRecordCenterControlIconScale: 0.6;\nhistoryRecordCenterControlPadding: 4px;\nhistoryRecordCenterControlTextSkip: 2px;\nhistoryRecordCenterControlMinimumProgressPadding: 5px;\n\nhistoryRecordWaveformBar: 3px;\nhistoryRecordTrimFrameRadius: 5px;\nhistoryRecordTrimFrameBorder: 1px;\nhistoryRecordTrimHandleWidth: 10px;\nhistoryRecordTrimHandleInnerSize: size(2px, 8px);\nhistoryRecordTrimHandleInnerSkip: 2px;\n\nhistoryRecordLockPosition: point(1px, 22px);\n\nhistoryRecordCancelButtonWidth: 100px;\nhistoryRecordCancelButtonFg: lightButtonFg;\n\nhistoryRecordTooltipSkip: 8px;\nhistoryRecordTooltip: ImportantTooltip(defaultImportantTooltip) {\n\tpadding: margins(4px, 4px, 4px, 4px);\n\tradius: 11px;\n\tarrow: 6px;\n}\n\nhistorySilentToggle: IconButton(historyBotKeyboardShow) {\n\ticon: icon {{ \"chat/input_silent\", historyComposeIconFg }};\n\ticonOver: icon {{ \"chat/input_silent\", historyComposeIconFgOver }};\n}\nhistorySilentToggleOn: icon {{ \"chat/input_silent_on\", historyComposeIconFg }};\nhistorySilentToggleOnOver: icon {{ \"chat/input_silent_on\", historyComposeIconFgOver }};\n\ndefaultRecordBarLock: RecordBarLock {\n\tripple: defaultRippleAnimation;\n\toriginTop: historyRecordLockTop;\n\toriginBottom: historyRecordLockBottom;\n\toriginBody: historyRecordLockBody;\n\tshadowTop: historyRecordLockTopShadow;\n\tshadowBottom: historyRecordLockBottomShadow;\n\tshadowBody: historyRecordLockBodyShadow;\n\tarrow: historyRecordLockArrow;\n\tfg: historyToDownFg;\n}\ndefaultRecordBar: RecordBar {\n\tbg: historyComposeAreaBg;\n\tdurationFg: historyRecordDurationFg;\n\tcancel: historyRecordCancel;\n\tcancelActive: historyRecordCancelActive;\n\tcancelRipple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: lightButtonBgRipple;\n\t}\n\tlock: defaultRecordBarLock;\n\tremove: historyRecordDelete;\n}\n\nhistorySend: SendButton {\n\tsendIconPosition: point(10px, 11px);\n\tinner: IconButton(historyAttach) {\n\t\ticon: historySendIcon;\n\t\ticonOver: historySendIconOver;\n\t}\n\tstars: RoundButton(defaultActiveButton) {\n\t\theight: 28px;\n\t\tpadding: margins(0px, 0px, 6px, 0px);\n\t\ttextTop: 5px;\n\t\twidth: -8px;\n\t}\n\trecordSize: size(26px, 26px);\n\tsendDisabledFg: historyComposeIconFg;\n\tsendIconFg: historySendIconFg;\n}\nhistoryAiComposeButton: IconButton(historyAttach) {\n\twidth: 28px;\n\theight: 28px;\n\trippleAreaSize: 28px;\n\trippleAreaPosition: point(0px, 0px);\n}\nhistoryAiComposeButtonLetters: icon{{ \"chat/ai_letters-20x20\", historyComposeIconFg }};\nhistoryAiComposeButtonStar1: icon{{ \"chat/ai_star1-20x20\", historyComposeIconFg }};\nhistoryAiComposeButtonStar2: icon{{ \"chat/ai_star2-20x20\", historyComposeIconFg }};\nhistoryAiComposeButtonPosition: point(-8px, -4px);\nhistorySendAsFileButton: IconButton(historyAiComposeButton) {\n\ticon: icon {{ \"chat/text_to_file-18x18\", historyComposeIconFg }};\n\ticonOver: icon {{ \"chat/text_to_file-18x18\", historyComposeIconFgOver }};\n}\nhistoryAiComposeTooltipSkip: 8px;\nimportantTooltipHide: IconButton(defaultIconButton) {\n\twidth: 34px;\n\theight: 20px;\n\ticonPosition: point(-1px, -1px);\n\ticon: icon {{ \"calls/video_tooltip\", importantTooltipFg }};\n\ticonOver: icon {{ \"calls/video_tooltip\", importantTooltipFg }};\n\tripple: emptyRippleAnimation;\n}\nboxAiComposeButtonPosition: point(0px, -4px);\nhistoryRecordFrameIndex: 30;\n\ndefaultComposeFilesMenu: IconButton(defaultIconButton) {\n\twidth: 48px;\n\theight: 54px;\n\n\ticon: icon {{ \"title_menu_dots\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"title_menu_dots\", boxTitleCloseFgOver }};\n\ticonPosition: point(18px, -1px);\n\n\trippleAreaPosition: point(1px, 6px);\n\trippleAreaSize: 42px;\n\tripple: defaultRippleAnimationBgOver;\n}\ndefaultComposeFilesField: InputField(defaultInputField) {\n\ttextMargins: margins(1px, 26px, 31px, 4px);\n\theightMax: 158px;\n\tstyle: historyTextStyle;\n}\ndefaultComposeFiles: ComposeFiles {\n\tcheck: defaultCheck;\n\tcheckbox: defaultBoxCheckbox;\n\tmenu: defaultComposeFilesMenu;\n\tcaption: defaultComposeFilesField;\n\temoji: boxAttachEmoji;\n\tconfirmBg: windowBgOver;\n\tbuttonFile: sendBoxAlbumGroupButtonFile;\n\tbuttonFileEdit: sendBoxAlbumGroupEditButtonIconFile;\n\tbuttonFileDelete: sendBoxAlbumGroupDeleteButtonIconFile;\n\ticonBg: msgFileInBg;\n\ticonPlay: icon {{ \"history_file_play\", historyFileInIconFg }};\n\ticonImage: icon {{ \"history_file_image\", historyFileInIconFg }};\n\ticonDocument: icon {{ \"history_file_document\", historyFileInIconFg }};\n\tnameFg: historyFileNameInFg;\n\tstatusFg: mediaInFg;\n}\ndefaultRestrictionLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 12px;\n\ttextFg: placeholderFg;\n\talign: align(top);\n}\ndefaultSendAsButton: SendAsButton {\n\twidth: 44px;\n\theight: 46px;\n\tsize: 28px;\n\tactiveBg: activeButtonBg;\n\tactiveFg: activeButtonFg;\n\tcross: CrossAnimation {\n\t\tsize: 28px;\n\t\tskip: 10px;\n\t\tstroke: 1.5;\n\t\tminScale: 0.3;\n\t}\n\tduration: 150;\n}\ndefaultChooseSendAs: ChooseSendAs {\n\tbutton: defaultSendAsButton;\n\tlabel: FlatLabel(defaultFlatLabel) {\n\t\tminWidth: 272px;\n\t}\n\tlist: PeerList(peerListBox) {\n\t\titem: PeerListItem(peerListBoxItem) {\n\t\t\theight: 56px;\n\t\t\tcheckbox: RoundImageCheckbox(defaultPeerListCheckbox) {\n\t\t\t\tcheck: RoundCheckbox(defaultRoundCheckbox) {\n\t\t\t\t\tsize: 0px;\n\t\t\t\t}\n\t\t\t\timageRadius: 19px;\n\t\t\t\timageSmallRadius: 15px;\n\t\t\t}\n\t\t\tphotoSize: 38px;\n\t\t\tphotoPosition: point(24px, 9px);\n\t\t\tnamePosition: point(73px, 9px);\n\t\t\tstatusPosition: point(73px, 28px);\n\t\t}\n\t}\n}\ndefaultComposeControls: ComposeControls {\n\tbg: historyComposeAreaBg;\n\tradius: 0px;\n\tpadding: margins(historySendRight, historySendPadding, historySendRight, historySendPadding);\n\n\tfield: historyComposeField;\n\tsend: historySend;\n\tattach: historyAttach;\n\temoji: historyAttachEmoji;\n\tsuggestions: defaultEmojiSuggestions;\n\ttabbed: defaultEmojiPan;\n\ttabbedHeightMin: emojiPanMinHeight;\n\ttabbedHeightMax: emojiPanMaxHeight;\n\trecord: defaultRecordBar;\n\tfiles: defaultComposeFiles;\n\tphotoQualityBadgeOuterSkip: 3px;\n\tpremium: defaultPremiumLimits;\n\tboxField: defaultInputField;\n\trestrictionLabel: defaultRestrictionLabel;\n\tchooseSendAs: defaultChooseSendAs;\n}\n\nmoreChatsBarHeight: 48px;\nmoreChatsBarTextPosition: point(12px, 4px);\nmoreChatsBarStatusPosition: point(12px, 24px);\nmoreChatsBarClose: IconButton(defaultIconButton) {\n\twidth: 48px;\n\theight: 48px;\n\n\ticon: boxTitleCloseIcon;\n\ticonOver: boxTitleCloseIconOver;\n\ticonPosition: point(12px, -1px);\n\n\trippleAreaPosition: point(0px, 4px);\n\trippleAreaSize: 40px;\n\tripple: defaultRippleAnimationBgOver;\n}\n\nreportReasonTopSkip: 8px;\nreportReasonButton: SettingsButton(defaultSettingsButton) {\n\tstyle: boxTextStyle;\n\tpadding: margins(62px, 7px, 8px, 7px);\n\ticonLeft: 22px;\n}\n\ndefaultReportBox: ReportBox {\n\tbutton: reportReasonButton;\n\tnoIconButton: SettingsButton(reportReasonButton) {\n\t\tpadding: margins(22px, 7px, 8px, 7px);\n\t}\n\tlabel: boxLabel;\n\tfield: newGroupDescription;\n\tdivider: defaultDividerLabel;\n\tspam: menuIconDelete;\n\tfake: menuIconFake;\n\tviolence: menuIconViolence;\n\tchildren: menuIconBlock;\n\tpornography: menuIconPorn;\n\tcopyright: menuIconCopyright;\n\tdrugs: menuIconDrugs;\n\tpersonal: menuIconPersonal;\n\tother: menuIconReport;\n}\n\ndragFont: font(27px semibold);\ndragSubfont: font(19px semibold);\ndragColor: windowSubTextFg;\ndragDropColor: windowActiveTextFg;\n\ndragMargin: margins(0px, 10px, 0px, 10px);\ndragPadding: margins(20px, 10px, 20px, 10px);\ndragHeight: 72px;\n\nttlMediaImportantTooltipLabel: FlatLabel(defaultImportantTooltipLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px);\n\t}\n}\nttlMediaButton: RoundButton(defaultActiveButton) {\n\ttextBg: shadowFg;\n\ttextBgOver: shadowFg;\n\tripple: universalRippleAnimation;\n\theight: 31px;\n\ttextTop: 6px;\n}\nttlMediaButtonBottomSkip: 14px;\n\neditTagAbout: FlatLabel(defaultFlatLabel) {\n\tminWidth: 256px;\n}\neditTagField: InputField(defaultInputField) {\n\ttextBg: transparent;\n\ttextMargins: margins(24px, 10px, 32px, 2px);\n\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(2px, 0px, 2px, 0px);\n\tplaceholderScale: 0.;\n\n\theightMin: 36px;\n}\n\neditStickerSetNameField: InputField(defaultInputField) {\n\ttextMargins: margins(0px, 8px, 26px, 4px);\n\theightMin: 36px;\n\theightMax: 36px;\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(2px, 0px, 2px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n}\neditStickerSetNameLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {\n\tcolor: lightButtonFg;\n\tthickness: 2px;\n}\n\npaidStarIcon: icon {{ \"settings/premium/star\", creditsBg1 }};\npaidStarIconTop: 7px;\npaidAmountAbout: FlatLabel(defaultFlatLabel) {\n\tminWidth: 256px;\n\ttextFg: windowSubTextFg;\n}\npaidTagLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: radialFg;\n\tpalette: TextPalette(defaultTextPalette) {\n\t\tlinkFg: creditsBg1;\n\t}\n\tstyle: semiboldTextStyle;\n}\npaidTagPadding: margins(16px, 6px, 16px, 6px);\n\npickLocationWindow: size(364px, 680px);\npickLocationMapHeight: 220px;\npickLocationCollapsedHeight: 92px;\npickLocationRowHeight: 52px;\npickLocationButton: FlatButton {\n\theight: pickLocationRowHeight;\n\tbgColor: contactsBg;\n\toverBgColor: contactsBgOver;\n\tripple: defaultRippleAnimation;\n}\npickLocationButtonText: FlatLabel(defaultFlatLabel) {\n\tminWidth: 128px;\n\tmaxHeight: 20px;\n\tstyle: semiboldTextStyle;\n\ttextFg: windowBoldFg;\n}\npickLocationButtonStatus: FlatLabel(defaultFlatLabel) {\n\tminWidth: 128px;\n\tmaxHeight: 20px;\n\ttextFg: windowSubTextFg;\n}\npickLocationButtonSkip: 6px;\npickLocationSendIcon: icon{{ \"chat/filled_location\", windowFgActive }};\npickLocationVenueItem: PeerListItem(defaultPeerListItem) {\n\theight: pickLocationRowHeight;\n\tphotoSize: 42px;\n\tphotoPosition: point(18px, 5px);\n\tnamePosition: point(70px, 7px);\n\tstatusPosition: point(70px, 27px);\n\tbutton: OutlineButton(defaultPeerListButton) {\n\t\ttextBg: contactsBg;\n\t\ttextBgOver: contactsBgOver;\n\t\tfont: normalFont;\n\t\tpadding: margins(11px, 5px, 11px, 5px);\n\t\tripple: defaultRippleAnimation;\n\t}\n\tstatusFg: contactsStatusFg;\n\tstatusFgOver: contactsStatusFgOver;\n\tstatusFgActive: contactsStatusFgOnline;\n}\npickLocationVenueList: PeerList(defaultPeerList) {\n\titem: pickLocationVenueItem;\n\tpadding: margins(0px, 0px, 0px, 0px);\n}\npickLocationIconSkip: 6px;\npickLocationLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {\n\tsize: size(56px, 56px);\n\tcolor: windowSubTextFg;\n\tthickness: 4px;\n}\npickLocationPromoHeight: 32px;\npickLocationChooseOnMap: RoundButton(defaultActiveButton) {\n\theight: 44px;\n\ttextTop: 11px;\n\twidth: -96px;\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(15px semibold);\n\t}\n}\n\nsendGifBox: Box(defaultBox) {\n\tshadowIgnoreBottomSkip: true;\n}\n\nprocessingVideoTipMaxWidth: 364px;\nprocessingVideoTipShift: 8px;\nprocessingVideoToast: Toast(defaultToast) {\n\tminWidth: 32px;\n\tmaxWidth: 380px;\n\tpadding: margins(19px, 17px, 19px, 17px);\n}\nprocessingVideoPreviewSkip: 8px;\nprocessingVideoView: RoundButton(defaultActiveButton) {\n\twidth: -24px;\n\theight: 52px;\n\ttextTop: 17px;\n\ttextFg: mediaviewTextLinkFg;\n\ttextFgOver: mediaviewTextLinkFg;\n\ttextBg: transparent;\n\ttextBgOver: transparent;\n\tripple: emptyRippleAnimation;\n}\n\nfrozenBarTitle: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n\ttextFg: attentionButtonFg;\n}\nfrozenRestrictionTitle: FlatLabel(frozenBarTitle) {\n\talign: align(top);\n}\nfrozenBarSubtitle: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n}\nfrozenRestrictionSubtitle: FlatLabel(frozenBarSubtitle) {\n\talign: align(top);\n}\nfrozenInfoBox: Box(defaultBox) {\n\tbuttonPadding: margins(16px, 11px, 16px, 16px);\n\tbuttonHeight: 42px;\n\tbutton: RoundButton(defaultActiveButton) {\n\t\theight: 42px;\n\t\ttextTop: 12px;\n\t\tstyle: semiboldTextStyle;\n\t}\n\tshadowIgnoreTopSkip: true;\n}\n\nmenuTranscribeItemPadding: margins(0px, 10px, 0px, 10px);\nmenuTranscribeDummyButton: IconButton(defaultIconButton) {\n\twidth: 40px;\n\theight: 40px;\n\n\ticon: icon {{ \"chat/input_attach\", historyComposeIconFgOver }};\n\ticonOver: icon {{ \"chat/input_attach\", historyComposeIconFgOver }};\n\n\trippleAreaPosition: point(3px, 3px);\n\trippleAreaSize: 34px;\n\tripple: defaultRippleAnimationBgOver;\n}\n\nroundVideoFont: font(14px semibold);\n\ntopPeersSelectorUserpicSize: 31px;\ntopPeersSelectorUserpicGap: 8px;\ntopPeersSelectorPadding: 6px;\ntopPeersSelectorUserpicExpand: 0.1;\ntopPeersSelectorSkip: point(9px, -5px);\n\ntopPeersSelectorImportantTooltip: ImportantTooltip(defaultImportantTooltip) {\n\tbg: msgServiceBg;\n\tradius: 9px;\n}\ntopPeersSelectorImportantTooltipLabel: FlatLabel(defaultImportantTooltipLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(semibold 12px);\n\t}\n}\n\naiComposeSendButton: SendButton(historySend) {\n\tsendIconPosition: point(9px, 9px);\n\tsendIconFillPadding: 5px;\n\tinner: IconButton(historyAttach) {\n\t\twidth: 42px;\n\t\theight: 42px;\n\t\ticon: icon {{ \"chat/input_send_round\", windowFgActive }};\n\t\ticonOver: icon {{ \"chat/input_send_round\", windowFgActive }};\n\t\trippleAreaSize: 40px;\n\t\trippleAreaPosition: point(1px, 1px);\n\t}\n\tsendIconFg: windowFgActive;\n}\n\naiComposeBodyLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 300px;\n\talign: align(topleft);\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tlineHeight: 22px;\n\t\tblockquote: QuoteStyle(historyQuoteStyle) {\n\t\t\tpadding: margins(10px, 2px, 20px, 2px);\n\t\t\ticon: icon{{ \"chat/mini_quote\", windowFg }};\n\t\t\ticonPosition: point(4px, 4px);\n\t\t\texpand: icon{{ \"intro_country_dropdown\", windowFg }};\n\t\t\texpandPosition: point(6px, 4px);\n\t\t\tcollapse: icon{{ \"intro_country_dropdown-flip_vertical\", windowFg }};\n\t\t\tcollapsePosition: point(6px, 4px);\n\t\t}\n\t\tpre: QuoteStyle(historyQuoteStyle) {\n\t\t\theader: 20px;\n\t\t\theaderPosition: point(10px, 2px);\n\t\t\tscrollable: true;\n\t\t\ticon: icon{{ \"chat/mini_copy\", windowFg }};\n\t\t\ticonPosition: point(4px, 2px);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/compose/compose_features.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace ChatHelpers {\n\nstruct ComposeFeatures {\n\tbool likes : 1 = false;\n\tbool sendAs : 1 = true;\n\tbool ttlInfo : 1 = true;\n\tbool attachments : 1 = true;\n\tbool botCommandSend : 1 = true;\n\tbool silentBroadcastToggle : 1 = true;\n\tbool attachBotsMenu : 1 = true;\n\tbool inlineBots : 1 = true;\n\tbool megagroupSet : 1 = true;\n\tbool collectibleStatus : 1 = false;\n\tbool stickersSettings : 1 = true;\n\tbool openStickerSets : 1 = true;\n\tbool autocompleteHashtags : 1 = true;\n\tbool autocompleteMentions : 1 = true;\n\tbool autocompleteCommands : 1 = true;\n\tbool suggestStickersByEmoji : 1 = true;\n\tbool commonTabbedPanel : 1 = true;\n\tbool recordMediaMessage : 1 = true;\n\tbool editMessageStars : 1 = false;\n\tbool emojiOnlyPanel : 1 = false;\n\tbool videoStream : 1 = false;\n};\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/compose/compose_show.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/compose/compose_show.h\"\n\n#include \"core/application.h\"\n#include \"main/main_session.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace ChatHelpers {\n\nrpl::producer<bool> Show::adjustShadowLeft() const {\n\treturn rpl::single(false);\n}\n\nResolveWindow ResolveWindowDefault() {\n\treturn [](not_null<Main::Session*> session)\n\t-> Window::SessionController* {\n\t\tconst auto check = [&](Window::Controller *window) {\n\t\t\tconst auto controller = window\n\t\t\t\t? window->sessionController()\n\t\t\t\t: nullptr;\n\t\t\treturn (controller && (&controller->session() == session))\n\t\t\t\t? controller\n\t\t\t\t: nullptr;\n\t\t};\n\t\tauto &app = Core::App();\n\t\tconst auto account = not_null(&session->account());\n\t\tif (const auto a = check(app.activeWindow())) {\n\t\t\treturn a;\n\t\t} else if (const auto b = check(app.activePrimaryWindow())) {\n\t\t\treturn b;\n\t\t} else if (const auto c = check(app.windowFor(account))) {\n\t\t\treturn c;\n\t\t} else if (Core::Quitting()) {\n\t\t\treturn nullptr;\n\t\t} else if (const auto d = check(app.ensureSeparateWindowFor(\n\t\t\t\taccount))) {\n\t\t\treturn d;\n\t\t}\n\t\treturn nullptr;\n\t};\n}\n\nWindow::SessionController *Show::resolveWindow() const {\n\treturn ResolveWindowDefault()(&session());\n}\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/compose/compose_show.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n#include \"main/session/session_show.h\"\n\nclass PhotoData;\nclass DocumentData;\n\nnamespace Data {\nstruct FileOrigin;\n} // namespace Data\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nnamespace ChatHelpers {\n\nstruct FileChosen;\n\nenum class PauseReason {\n\tAny = 0,\n\tInlineResults = (1 << 0),\n\tTabbedPanel = (1 << 1),\n\tLayer = (1 << 2),\n\tRoundPlaying = (1 << 3),\n\tMediaPreview = (1 << 4),\n};\nusing PauseReasons = base::flags<PauseReason>;\ninline constexpr bool is_flag_type(PauseReason) { return true; };\n\nusing ResolveWindow = Fn<Window::SessionController*(\n\tnot_null<Main::Session*>)>;\n[[nodiscard]] ResolveWindow ResolveWindowDefault();\n\nclass Show : public Main::SessionShow {\npublic:\n\tvirtual void activate() = 0;\n\n\t[[nodiscard]] virtual bool paused(PauseReason reason) const = 0;\n\t[[nodiscard]] virtual rpl::producer<> pauseChanged() const = 0;\n\n\t[[nodiscard]] virtual rpl::producer<bool> adjustShadowLeft() const;\n\t[[nodiscard]] virtual SendMenu::Details sendMenuDetails() const = 0;\n\n\tvirtual bool showMediaPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<DocumentData*> document) const = 0;\n\tvirtual bool showMediaPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<PhotoData*> photo) const = 0;\n\n\tvirtual void processChosenSticker(FileChosen &&chosen) const = 0;\n\n\t[[nodiscard]] virtual Window::SessionController *resolveWindow() const;\n};\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/emoji_interactions.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/emoji_interactions.h\"\n\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"ui/emoji_config.h\"\n#include \"base/random.h\"\n#include \"apiwrap.h\"\n\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonArray>\n#include <QtCore/QJsonObject>\n#include <QtCore/QJsonValue>\n\nnamespace ChatHelpers {\nnamespace {\n\nconstexpr auto kMinDelay = crl::time(200);\nconstexpr auto kAccumulateDelay = crl::time(1000);\nconstexpr auto kAccumulateSeenRequests = kAccumulateDelay;\nconstexpr auto kAcceptSeenSinceRequest = 3 * crl::time(1000);\nconstexpr auto kMaxDelay = 2 * crl::time(1000);\nconstexpr auto kTimeNever = std::numeric_limits<crl::time>::max();\nconstexpr auto kJsonVersion = 1;\n\n} // namespace\n\nauto EmojiInteractions::Combine(CheckResult a, CheckResult b) -> CheckResult {\n\treturn {\n\t\t.nextCheckAt = std::min(a.nextCheckAt, b.nextCheckAt),\n\t\t.waitingForDownload = a.waitingForDownload || b.waitingForDownload,\n\t};\n}\n\nEmojiInteractions::EmojiInteractions(not_null<Main::Session*> session)\n: _session(session)\n, _checkTimer([=] { check(); }) {\n\t_session->changes().messageUpdates(\n\t\tData::MessageUpdate::Flag::Destroyed\n\t\t| Data::MessageUpdate::Flag::Edited\n\t) | rpl::on_next([=](const Data::MessageUpdate &update) {\n\t\tif (update.flags & Data::MessageUpdate::Flag::Destroyed) {\n\t\t\t_outgoing.remove(update.item);\n\t\t\t_incoming.remove(update.item);\n\t\t} else if (update.flags & Data::MessageUpdate::Flag::Edited) {\n\t\t\tcheckEdition(update.item, _outgoing);\n\t\t\tcheckEdition(update.item, _incoming);\n\t\t}\n\t}, _lifetime);\n}\n\nEmojiInteractions::~EmojiInteractions() = default;\n\nvoid EmojiInteractions::checkEdition(\n\t\tnot_null<HistoryItem*> item,\n\t\tbase::flat_map<not_null<HistoryItem*>, std::vector<Animation>> &map) {\n\tconst auto &pack = _session->emojiStickersPack();\n\tconst auto i = map.find(item);\n\tif (i != end(map)\n\t\t&& (i->second.front().emoji != pack.chooseInteractionEmoji(item))) {\n\t\tmap.erase(i);\n\t}\n}\n\nvoid EmojiInteractions::startOutgoing(\n\t\tnot_null<const HistoryView::Element*> view) {\n\tconst auto item = view->data();\n\tif (!item->isRegular() || !item->history()->peer->isUser()) {\n\t\treturn;\n\t}\n\tconst auto &pack = _session->emojiStickersPack();\n\tconst auto emoticon = item->originalText().text;\n\tconst auto emoji = pack.chooseInteractionEmoji(emoticon);\n\tif (!emoji) {\n\t\treturn;\n\t}\n\tconst auto &list = pack.animationsForEmoji(emoji);\n\tif (list.empty()) {\n\t\treturn;\n\t}\n\tauto &animations = _outgoing[item];\n\tif (!animations.empty() && animations.front().emoji != emoji) {\n\t\t// The message was edited, forget the old emoji.\n\t\tanimations.clear();\n\t}\n\tconst auto last = !animations.empty() ? &animations.back() : nullptr;\n\tconst auto listSize = int(list.size());\n\tconst auto chooseDifferent = (last && listSize > 1);\n\tconst auto index = chooseDifferent\n\t\t? base::RandomIndex(listSize - 1)\n\t\t: base::RandomIndex(listSize);\n\tconst auto selected = (begin(list) + index)->second;\n\tconst auto document = (chooseDifferent && selected == last->document)\n\t\t? (begin(list) + index + 1)->second\n\t\t: selected;\n\tconst auto media = document->createMediaView();\n\tmedia->checkStickerLarge();\n\tconst auto now = crl::now();\n\tanimations.push_back({\n\t\t.emoticon = emoticon,\n\t\t.emoji = emoji,\n\t\t.document = document,\n\t\t.media = media,\n\t\t.scheduledAt = now,\n\t\t.index = index,\n\t});\n\tcheck(now);\n}\n\nvoid EmojiInteractions::startIncoming(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId messageId,\n\t\tconst QString &emoticon,\n\t\tEmojiInteractionsBunch &&bunch) {\n\tif (!peer->isUser() || bunch.interactions.empty()) {\n\t\treturn;\n\t}\n\tconst auto item = _session->data().message(peer->id, messageId);\n\tif (!item || !item->isRegular()) {\n\t\treturn;\n\t}\n\tconst auto &pack = _session->emojiStickersPack();\n\tconst auto emoji = pack.chooseInteractionEmoji(item);\n\tif (!emoji || emoji != pack.chooseInteractionEmoji(emoticon)) {\n\t\treturn;\n\t}\n\tconst auto &list = pack.animationsForEmoji(emoji);\n\tif (list.empty()) {\n\t\treturn;\n\t}\n\tauto &animations = _incoming[item];\n\tif (!animations.empty() && animations.front().emoji != emoji) {\n\t\t// The message was edited, forget the old emoji.\n\t\tanimations.clear();\n\t}\n\tconst auto now = crl::now();\n\tfor (const auto &single : bunch.interactions) {\n\t\tconst auto at = now + crl::time(base::SafeRound(single.time * 1000));\n\t\tif (!animations.empty() && animations.back().scheduledAt >= at) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto listSize = int(list.size());\n\t\tconst auto index = (single.index - 1);\n\t\tif (index < listSize) {\n\t\t\tconst auto document = (begin(list) + index)->second;\n\t\t\tconst auto media = document->createMediaView();\n\t\t\tmedia->checkStickerLarge();\n\t\t\tanimations.push_back({\n\t\t\t\t.emoticon = emoticon,\n\t\t\t\t.emoji = emoji,\n\t\t\t\t.document = document,\n\t\t\t\t.media = media,\n\t\t\t\t.scheduledAt = at,\n\t\t\t\t.incoming = true,\n\t\t\t\t.index = index,\n\t\t\t});\n\t\t}\n\t}\n\tif (animations.empty()) {\n\t\t_incoming.remove(item);\n\t} else {\n\t\tcheck(now);\n\t}\n}\n\nvoid EmojiInteractions::seenOutgoing(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &emoticon) {\n\tconst auto &pack = _session->emojiStickersPack();\n\tif (const auto i = _playsSent.find(peer); i != end(_playsSent)) {\n\t\tif (const auto emoji = pack.chooseInteractionEmoji(emoticon)) {\n\t\t\tif (const auto j = i->second.find(emoji); j != end(i->second)) {\n\t\t\t\tconst auto last = j->second.lastDoneReceivedAt;\n\t\t\t\tif (!last || last + kAcceptSeenSinceRequest > crl::now()) {\n\t\t\t\t\t_seen.fire({ peer, emoticon });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nauto EmojiInteractions::checkAnimations(crl::time now) -> CheckResult {\n\treturn Combine(\n\t\tcheckAnimations(now, _outgoing),\n\t\tcheckAnimations(now, _incoming));\n}\n\nauto EmojiInteractions::checkAnimations(\n\t\tcrl::time now,\n\t\tbase::flat_map<not_null<HistoryItem*>, std::vector<Animation>> &map\n) -> CheckResult {\n\tauto nearest = kTimeNever;\n\tauto waitingForDownload = false;\n\tfor (auto i = begin(map); i != end(map);) {\n\t\tauto lastStartedAt = crl::time();\n\n\t\tauto &animations = i->second;\n\t\t// Erase too old requests.\n\t\tconst auto j = ranges::find_if(animations, [&](const Animation &a) {\n\t\t\treturn !a.startedAt && (a.scheduledAt + kMaxDelay <= now);\n\t\t});\n\t\tif (j == begin(animations)) {\n\t\t\ti = map.erase(i);\n\t\t\tcontinue;\n\t\t} else if (j != end(animations)) {\n\t\t\tanimations.erase(j, end(animations));\n\t\t}\n\t\tconst auto item = i->first;\n\t\tfor (auto &animation : animations) {\n\t\t\tif (animation.startedAt) {\n\t\t\t\tlastStartedAt = animation.startedAt;\n\t\t\t} else if (!animation.media->loaded()) {\n\t\t\t\tanimation.media->checkStickerLarge();\n\t\t\t\twaitingForDownload = true;\n\t\t\t\tbreak;\n\t\t\t} else if (!lastStartedAt || lastStartedAt + kMinDelay <= now) {\n\t\t\t\tanimation.startedAt = now;\n\t\t\t\t_playRequests.fire({\n\t\t\t\t\tanimation.emoticon,\n\t\t\t\t\titem,\n\t\t\t\t\tanimation.media,\n\t\t\t\t\tanimation.scheduledAt,\n\t\t\t\t\tanimation.incoming,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t} else {\n\t\t\t\tnearest = std::min(nearest, lastStartedAt + kMinDelay);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t++i;\n\t}\n\treturn {\n\t\t.nextCheckAt = nearest,\n\t\t.waitingForDownload = waitingForDownload,\n\t};\n}\n\nvoid EmojiInteractions::sendAccumulatedOutgoing(\n\t\tcrl::time now,\n\t\tnot_null<HistoryItem*> item,\n\t\tstd::vector<Animation> &animations) {\n\tExpects(!animations.empty());\n\n\tconst auto firstStartedAt = animations.front().startedAt;\n\tconst auto intervalEnd = firstStartedAt + kAccumulateDelay;\n\tif (intervalEnd > now) {\n\t\treturn;\n\t}\n\tconst auto from = begin(animations);\n\tconst auto till = ranges::find_if(animations, [&](const auto &animation) {\n\t\treturn !animation.startedAt || (animation.startedAt >= intervalEnd);\n\t});\n\tauto bunch = EmojiInteractionsBunch();\n\tbunch.interactions.reserve(till - from);\n\tfor (const auto &animation : ranges::make_subrange(from, till)) {\n\t\tbunch.interactions.push_back({\n\t\t\t.index = animation.index + 1,\n\t\t\t.time = (animation.startedAt - firstStartedAt) / 1000.,\n\t\t});\n\t}\n\tif (bunch.interactions.empty()) {\n\t\treturn;\n\t}\n\tconst auto peer = item->history()->peer;\n\tconst auto emoji = from->emoji;\n\tconst auto requestId = _session->api().request(MTPmessages_SetTyping(\n\t\tMTP_flags(0),\n\t\tpeer->input(),\n\t\tMTPint(), // top_msg_id\n\t\tMTP_sendMessageEmojiInteraction(\n\t\t\tMTP_string(from->emoticon),\n\t\t\tMTP_int(item->id),\n\t\t\tMTP_dataJSON(MTP_bytes(ToJson(bunch))))\n\t)).done([=](const MTPBool &result, mtpRequestId requestId) {\n\t\tauto &sent = _playsSent[peer][emoji];\n\t\tif (sent.lastRequestId == requestId) {\n\t\t\tsent.lastDoneReceivedAt = crl::now();\n\t\t\tif (!_checkTimer.isActive()) {\n\t\t\t\t_checkTimer.callOnce(kAcceptSeenSinceRequest);\n\t\t\t}\n\t\t}\n\t}).send();\n\t_playsSent[peer][emoji] = PlaySent{ .lastRequestId = requestId };\n\tanimations.erase(from, till);\n}\n\nvoid EmojiInteractions::clearAccumulatedIncoming(\n\t\tcrl::time now,\n\t\tstd::vector<Animation> &animations) {\n\tExpects(!animations.empty());\n\n\tconst auto from = begin(animations);\n\tconst auto till = ranges::find_if(animations, [&](const auto &animation) {\n\t\treturn !animation.startedAt\n\t\t\t|| (animation.startedAt + kMinDelay) > now;\n\t});\n\tanimations.erase(from, till);\n}\n\nauto EmojiInteractions::checkAccumulated(crl::time now) -> CheckResult {\n\tauto nearest = kTimeNever;\n\tfor (auto i = begin(_outgoing); i != end(_outgoing);) {\n\t\tauto &[item, animations] = *i;\n\t\tsendAccumulatedOutgoing(now, item, animations);\n\t\tif (animations.empty()) {\n\t\t\ti = _outgoing.erase(i);\n\t\t\tcontinue;\n\t\t} else if (const auto firstStartedAt = animations.front().startedAt) {\n\t\t\tnearest = std::min(nearest, firstStartedAt + kAccumulateDelay);\n\t\t\tAssert(nearest > now);\n\t\t}\n\t\t++i;\n\t}\n\tfor (auto i = begin(_incoming); i != end(_incoming);) {\n\t\tauto &animations = i->second;\n\t\tclearAccumulatedIncoming(now, animations);\n\t\tif (animations.empty()) {\n\t\t\ti = _incoming.erase(i);\n\t\t\tcontinue;\n\t\t} else {\n\t\t\t// Doesn't really matter when, just clear them finally.\n\t\t\tnearest = std::min(nearest, now + kAccumulateDelay);\n\t\t}\n\t\t++i;\n\t}\n\treturn {\n\t\t.nextCheckAt = nearest,\n\t};\n}\n\nvoid EmojiInteractions::check(crl::time now) {\n\tif (!now) {\n\t\tnow = crl::now();\n\t}\n\tcheckSeenRequests(now);\n\tcheckSentRequests(now);\n\tconst auto result1 = checkAnimations(now);\n\tconst auto result2 = checkAccumulated(now);\n\tconst auto result = Combine(result1, result2);\n\tif (result.nextCheckAt < kTimeNever) {\n\t\tAssert(result.nextCheckAt > now);\n\t\t_checkTimer.callOnce(result.nextCheckAt - now);\n\t} else if (!_playStarted.empty()) {\n\t\t_checkTimer.callOnce(kAccumulateSeenRequests);\n\t} else if (!_playsSent.empty()) {\n\t\t_checkTimer.callOnce(kAcceptSeenSinceRequest);\n\t}\n\tsetWaitingForDownload(result.waitingForDownload);\n}\n\nvoid EmojiInteractions::checkSeenRequests(crl::time now) {\n\tfor (auto i = begin(_playStarted); i != end(_playStarted);) {\n\t\tauto &animations = i->second;\n\t\tfor (auto j = begin(animations); j != end(animations);) {\n\t\t\tif (j->second + kAccumulateSeenRequests <= now) {\n\t\t\t\tj = animations.erase(j);\n\t\t\t} else {\n\t\t\t\t++j;\n\t\t\t}\n\t\t}\n\t\tif (animations.empty()) {\n\t\t\ti = _playStarted.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n}\n\nvoid EmojiInteractions::checkSentRequests(crl::time now) {\n\tfor (auto i = begin(_playsSent); i != end(_playsSent);) {\n\t\tauto &animations = i->second;\n\t\tfor (auto j = begin(animations); j != end(animations);) {\n\t\t\tconst auto last = j->second.lastDoneReceivedAt;\n\t\t\tif (last && last + kAcceptSeenSinceRequest <= now) {\n\t\t\t\tj = animations.erase(j);\n\t\t\t} else {\n\t\t\t\t++j;\n\t\t\t}\n\t\t}\n\t\tif (animations.empty()) {\n\t\t\ti = _playsSent.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n}\n\nvoid EmojiInteractions::setWaitingForDownload(bool waiting) {\n\tif (_waitingForDownload == waiting) {\n\t\treturn;\n\t}\n\t_waitingForDownload = waiting;\n\tif (_waitingForDownload) {\n\t\t_session->downloaderTaskFinished(\n\t\t) | rpl::on_next([=] {\n\t\t\tcheck();\n\t\t}, _downloadCheckLifetime);\n\t} else {\n\t\t_downloadCheckLifetime.destroy();\n\t\t_downloadCheckLifetime.destroy();\n\t}\n}\n\nvoid EmojiInteractions::playStarted(not_null<PeerData*> peer, QString emoji) {\n\tauto &map = _playStarted[peer];\n\tconst auto i = map.find(emoji);\n\tconst auto now = crl::now();\n\tif (i != end(map) && now - i->second < kAccumulateSeenRequests) {\n\t\treturn;\n\t}\n\t_session->api().request(MTPmessages_SetTyping(\n\t\tMTP_flags(0),\n\t\tpeer->input(),\n\t\tMTPint(), // top_msg_id\n\t\tMTP_sendMessageEmojiInteractionSeen(MTP_string(emoji))\n\t)).send();\n\tmap[emoji] = now;\n\tif (!_checkTimer.isActive()) {\n\t\t_checkTimer.callOnce(kAccumulateSeenRequests);\n\t}\n}\n\nEmojiInteractionsBunch EmojiInteractions::Parse(const QByteArray &json) {\n\tauto error = QJsonParseError{ 0, QJsonParseError::NoError };\n\tconst auto document = QJsonDocument::fromJson(json, &error);\n\tif (error.error != QJsonParseError::NoError || !document.isObject()) {\n\t\tLOG((\"API Error: Bad interactions json received.\"));\n\t\treturn {};\n\t}\n\tconst auto root = document.object();\n\tconst auto version = root.value(\"v\").toInt();\n\tif (version != kJsonVersion) {\n\t\tLOG((\"API Error: Bad interactions version: %1\").arg(version));\n\t\treturn {};\n\t}\n\tconst auto actions = root.value(\"a\").toArray();\n\tif (actions.empty()) {\n\t\tLOG((\"API Error: Empty interactions list.\"));\n\t\treturn {};\n\t}\n\tauto result = EmojiInteractionsBunch();\n\tfor (const auto interaction : actions) {\n\t\tconst auto object = interaction.toObject();\n\t\tconst auto index = object.value(\"i\").toInt();\n\t\tif (index < 0 || index > 10) {\n\t\t\tLOG((\"API Error: Bad interaction index: %1\").arg(index));\n\t\t\treturn {};\n\t\t}\n\t\tconst auto time = object.value(\"t\").toDouble();\n\t\tif (time < 0.\n\t\t\t|| time > 1.\n\t\t\t|| (!result.interactions.empty()\n\t\t\t\t&& time <= result.interactions.back().time)) {\n\t\t\tLOG((\"API Error: Bad interaction time: %1\").arg(time));\n\t\t\tcontinue;\n\t\t}\n\t\tresult.interactions.push_back({ .index = index, .time = time });\n\t}\n\n\treturn result;\n}\n\nQByteArray EmojiInteractions::ToJson(const EmojiInteractionsBunch &bunch) {\n\tauto list = QJsonArray();\n\tfor (const auto &single : bunch.interactions) {\n\t\tlist.push_back(QJsonObject{\n\t\t\t{ \"i\", single.index },\n\t\t\t{ \"t\", single.time },\n\t\t});\n\t}\n\treturn QJsonDocument(QJsonObject{\n\t\t{ \"v\", kJsonVersion },\n\t\t{ \"a\", std::move(list) },\n\t}).toJson(QJsonDocument::Compact);\n}\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/emoji_interactions.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n\nclass PeerData;\nclass HistoryItem;\nclass DocumentData;\n\nnamespace Data {\nclass DocumentMedia;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace HistoryView {\nclass Element;\n} // namespace HistoryView\n\nnamespace ChatHelpers {\n\nstruct EmojiInteractionPlayRequest {\n\tQString emoticon;\n\tnot_null<HistoryItem*> item;\n\tstd::shared_ptr<Data::DocumentMedia> media;\n\tcrl::time shouldHaveStartedAt = 0;\n\tbool incoming = false;\n};\n\nstruct EmojiInteractionsBunch {\n\tstruct Single {\n\t\tint index = 0;\n\t\tdouble time = 0;\n\t};\n\tstd::vector<Single> interactions;\n};\n\nstruct EmojiInteractionSeen {\n\tnot_null<PeerData*> peer;\n\tQString emoticon;\n};\n\nclass EmojiInteractions final {\npublic:\n\texplicit EmojiInteractions(not_null<Main::Session*> session);\n\t~EmojiInteractions();\n\n\tusing PlayRequest = EmojiInteractionPlayRequest;\n\n\tvoid startOutgoing(not_null<const HistoryView::Element*> view);\n\tvoid startIncoming(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId messageId,\n\t\tconst QString &emoticon,\n\t\tEmojiInteractionsBunch &&bunch);\n\n\tvoid seenOutgoing(not_null<PeerData*> peer, const QString &emoticon);\n\t[[nodiscard]] rpl::producer<EmojiInteractionSeen> seen() const {\n\t\treturn _seen.events();\n\t}\n\n\t[[nodiscard]] rpl::producer<PlayRequest> playRequests() const {\n\t\treturn _playRequests.events();\n\t}\n\tvoid playStarted(not_null<PeerData*> peer, QString emoji);\n\n\t[[nodiscard]] static EmojiInteractionsBunch Parse(const QByteArray &json);\n\t[[nodiscard]] static QByteArray ToJson(\n\t\tconst EmojiInteractionsBunch &bunch);\n\nprivate:\n\tstruct Animation {\n\t\tQString emoticon;\n\t\tnot_null<EmojiPtr> emoji;\n\t\tnot_null<DocumentData*> document;\n\t\tstd::shared_ptr<Data::DocumentMedia> media;\n\t\tcrl::time scheduledAt = 0;\n\t\tcrl::time startedAt = 0;\n\t\tbool incoming = false;\n\t\tint index = 0;\n\t};\n\tstruct PlaySent {\n\t\tmtpRequestId lastRequestId = 0;\n\t\tcrl::time lastDoneReceivedAt = 0;\n\t};\n\tstruct CheckResult {\n\t\tcrl::time nextCheckAt = 0;\n\t\tbool waitingForDownload = false;\n\t};\n\t[[nodiscard]] static CheckResult Combine(CheckResult a, CheckResult b);\n\n\tvoid check(crl::time now = 0);\n\t[[nodiscard]] CheckResult checkAnimations(crl::time now);\n\t[[nodiscard]] CheckResult checkAnimations(\n\t\tcrl::time now,\n\t\tbase::flat_map<not_null<HistoryItem*>, std::vector<Animation>> &map);\n\t[[nodiscard]] CheckResult checkAccumulated(crl::time now);\n\tvoid sendAccumulatedOutgoing(\n\t\tcrl::time now,\n\t\tnot_null<HistoryItem*> item,\n\t\tstd::vector<Animation> &animations);\n\tvoid clearAccumulatedIncoming(\n\t\tcrl::time now,\n\t\tstd::vector<Animation> &animations);\n\tvoid setWaitingForDownload(bool waiting);\n\n\tvoid checkSeenRequests(crl::time now);\n\tvoid checkSentRequests(crl::time now);\n\tvoid checkEdition(\n\t\tnot_null<HistoryItem*> item,\n\t\tbase::flat_map<not_null<HistoryItem*>, std::vector<Animation>> &map);\n\n\tconst not_null<Main::Session*> _session;\n\n\tbase::flat_map<not_null<HistoryItem*>, std::vector<Animation>> _outgoing;\n\tbase::flat_map<not_null<HistoryItem*>, std::vector<Animation>> _incoming;\n\tbase::Timer _checkTimer;\n\trpl::event_stream<PlayRequest> _playRequests;\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tbase::flat_map<QString, crl::time>> _playStarted;\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tbase::flat_map<not_null<EmojiPtr>, PlaySent>> _playsSent;\n\trpl::event_stream<EmojiInteractionSeen> _seen;\n\n\tbool _waitingForDownload = false;\n\trpl::lifetime _downloadCheckLifetime;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/emoji_keywords.h\"\n\n#include \"emoji_suggestions_helper.h\"\n#include \"lang/lang_instance.h\"\n#include \"lang/lang_cloud_manager.h\"\n#include \"lang/lang_keys.h\"\n#include \"core/application.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"ui/emoji_config.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n\n#include <QtGui/QGuiApplication>\n\nnamespace ChatHelpers {\nnamespace {\n\nconstexpr auto kRefreshEach = 60 * 60 * crl::time(1000); // 1 hour.\nconstexpr auto kKeepNotUsedLangPacksCount = 4;\nconstexpr auto kKeepNotUsedInputLanguagesCount = 4;\n\nusing namespace Ui::Emoji;\n\nusing Result = EmojiKeywords::Result;\n\nstruct LangPackEmoji {\n\tEmojiPtr emoji = nullptr;\n\tQString text;\n};\n\nstruct LangPackData {\n\tint version = 0;\n\tint maxKeyLength = 0;\n\tstd::map<QString, std::vector<LangPackEmoji>> emoji;\n};\n\n[[nodiscard]] bool MustAddPostfix(const QString &text) {\n\tif (text.size() != 1) {\n\t\treturn false;\n\t}\n\tconst auto code = text[0].unicode();\n\treturn (code == 0x2122U) || (code == 0xA9U) || (code == 0xAEU);\n}\n\n[[nodiscard]] bool SkipExactKeyword(\n\t\tconst QString &language,\n\t\tconst QString &word) {\n\tif ((word.size() == 1) && !word[0].isLetter()) {\n\t\treturn true;\n\t} else if (word == u\"10\"_q) {\n\t\treturn true;\n\t} else if (language != u\"en\"_q) {\n\t\treturn false;\n\t} else if ((word.size() == 1)\n\t\t&& (word[0] != '$')\n\t\t&& (word[0].unicode() != 8364)) { // Euro.\n\t\treturn true;\n\t} else if ((word.size() == 2)\n\t\t&& (word != u\"us\"_q)\n\t\t&& (word != u\"uk\"_q)\n\t\t&& (word != u\"hi\"_q)\n\t\t&& (word != u\"ok\"_q)) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\n[[nodiscard]] EmojiPtr FindExact(const QString &text) {\n\tauto length = 0;\n\tconst auto result = Find(text, &length);\n\treturn (length < text.size()) ? nullptr : result;\n}\n\nvoid CreateCacheFilePath() {\n\tQDir().mkpath(internal::CacheFileFolder() + u\"/keywords\"_q);\n}\n\n[[nodiscard]] QString CacheFilePath(QString id) {\n\tstatic const auto BadSymbols = QRegularExpression(\"[^a-zA-Z0-9_\\\\.\\\\-]\");\n\tid.replace(BadSymbols, QString());\n\tif (id.isEmpty()) {\n\t\treturn QString();\n\t}\n\treturn internal::CacheFileFolder() + u\"/keywords/\"_q + id;\n}\n\n[[nodiscard]] LangPackData ReadLocalCache(const QString &id) {\n\tauto file = QFile(CacheFilePath(id));\n\tif (!file.open(QIODevice::ReadOnly)) {\n\t\treturn {};\n\t}\n\tauto result = LangPackData();\n\tauto stream = QDataStream(&file);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\tauto version = qint32();\n\tauto count = qint32();\n\tstream\n\t\t>> version\n\t\t>> count;\n\tif (version < 0 || count < 0 || stream.status() != QDataStream::Ok) {\n\t\treturn {};\n\t}\n\tfor (auto i = 0; i != count; ++i) {\n\t\tauto key = QString();\n\t\tauto size = qint32();\n\t\tstream\n\t\t\t>> key\n\t\t\t>> size;\n\t\tif (size < 0 || stream.status() != QDataStream::Ok) {\n\t\t\treturn {};\n\t\t}\n\t\tauto &list = result.emoji[key];\n\t\tfor (auto j = 0; j != size; ++j) {\n\t\t\tauto text = QString();\n\t\t\tstream >> text;\n\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\tconst auto emoji = MustAddPostfix(text)\n\t\t\t\t? (text + QChar(Ui::Emoji::kPostfix))\n\t\t\t\t: text;\n\t\t\tconst auto entry = LangPackEmoji{ FindExact(emoji), text };\n\t\t\tif (!entry.emoji) {\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\tlist.push_back(entry);\n\t\t}\n\t\tresult.maxKeyLength = std::max(result.maxKeyLength, int(key.size()));\n\t}\n\tresult.version = version;\n\treturn result;\n}\n\nvoid WriteLocalCache(const QString &id, const LangPackData &data) {\n\tif (!data.version && data.emoji.empty()) {\n\t\treturn;\n\t}\n\tCreateCacheFilePath();\n\tauto file = QFile(CacheFilePath(id));\n\tif (!file.open(QIODevice::WriteOnly)) {\n\t\treturn;\n\t}\n\tauto stream = QDataStream(&file);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\tstream\n\t\t<< qint32(data.version)\n\t\t<< qint32(data.emoji.size());\n\tfor (const auto &[key, list] : data.emoji) {\n\t\tstream\n\t\t\t<< key\n\t\t\t<< qint32(list.size());\n\t\tfor (const auto &emoji : list) {\n\t\t\tstream << emoji.text;\n\t\t}\n\t}\n}\n\n[[nodiscard]] QString NormalizeQuery(const QString &query) {\n\treturn query.toLower();\n}\n\n[[nodiscard]] QString NormalizeKey(const QString &key) {\n\treturn key.toLower().trimmed();\n}\n\nvoid AppendFoundEmoji(\n\t\tstd::vector<Result> &result,\n\t\tconst QString &label,\n\t\tconst std::vector<LangPackEmoji> &list) {\n\t// It is important that the 'result' won't relocate while inserting.\n\tresult.reserve(result.size() + list.size());\n\tconst auto alreadyBegin = begin(result);\n\tconst auto alreadyEnd = alreadyBegin + result.size();\n\n\tauto &&add = ranges::views::all(\n\t\tlist\n\t) | ranges::views::filter([&](const LangPackEmoji &entry) {\n\t\tconst auto i = ranges::find(\n\t\t\talreadyBegin,\n\t\t\talreadyEnd,\n\t\t\tentry.emoji,\n\t\t\t&Result::emoji);\n\t\treturn (i == alreadyEnd);\n\t}) | ranges::views::transform([&](const LangPackEmoji &entry) {\n\t\treturn Result{ entry.emoji, label, entry.text };\n\t});\n\tresult.insert(end(result), add.begin(), add.end());\n}\n\nvoid AppendLegacySuggestions(\n\t\tstd::vector<Result> &result,\n\t\tconst QString &query) {\n\tconst auto badSuggestionChar = [](QChar ch) {\n\t\treturn (ch < 'a' || ch > 'z')\n\t\t\t&& (ch < 'A' || ch > 'Z')\n\t\t\t&& (ch < '0' || ch > '9')\n\t\t\t&& (ch != '_')\n\t\t\t&& (ch != '-')\n\t\t\t&& (ch != '+');\n\t};\n\tif (ranges::any_of(query, badSuggestionChar)) {\n\t\treturn;\n\t}\n\n\tconst auto suggestions = GetSuggestions(QStringToUTF16(query));\n\n\t// It is important that the 'result' won't relocate while inserting.\n\tresult.reserve(result.size() + suggestions.size());\n\tconst auto alreadyBegin = begin(result);\n\tconst auto alreadyEnd = alreadyBegin + result.size();\n\n\tauto &&add = ranges::views::all(\n\t\tsuggestions\n\t) | ranges::views::transform([](const Suggestion &suggestion) {\n\t\treturn Result{\n\t\t\tFind(QStringFromUTF16(suggestion.emoji())),\n\t\t\tQStringFromUTF16(suggestion.label()),\n\t\t\tQStringFromUTF16(suggestion.replacement())\n\t\t};\n\t}) | ranges::views::filter([&](const Result &entry) {\n\t\tconst auto i = entry.emoji\n\t\t\t? ranges::find(\n\t\t\t\talreadyBegin,\n\t\t\t\talreadyEnd,\n\t\t\t\tentry.emoji,\n\t\t\t\t&Result::emoji)\n\t\t\t: alreadyEnd;\n\t\treturn (entry.emoji != nullptr)\n\t\t\t&& (i == alreadyEnd);\n\t});\n\tresult.insert(end(result), add.begin(), add.end());\n}\n\nvoid ApplyDifference(\n\t\tLangPackData &data,\n\t\tconst QVector<MTPEmojiKeyword> &keywords,\n\t\tint version) {\n\tdata.version = version;\n\tfor (const auto &keyword : keywords) {\n\t\tkeyword.match([&](const MTPDemojiKeyword &keyword) {\n\t\t\tconst auto word = NormalizeKey(qs(keyword.vkeyword()));\n\t\t\tif (word.isEmpty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto &list = data.emoji[word];\n\t\t\tauto &&emoji = ranges::views::all(\n\t\t\t\tkeyword.vemoticons().v\n\t\t\t) | ranges::views::transform([](const MTPstring &string) {\n\t\t\t\tconst auto text = qs(string);\n\t\t\t\tconst auto emoji = MustAddPostfix(text)\n\t\t\t\t\t? (text + QChar(Ui::Emoji::kPostfix))\n\t\t\t\t\t: text;\n\t\t\t\treturn LangPackEmoji{ FindExact(emoji), text };\n\t\t\t}) | ranges::views::filter([&](const LangPackEmoji &entry) {\n\t\t\t\tif (!entry.emoji) {\n\t\t\t\t\tLOG((\"API Warning: emoji %1 is not supported, word: %2.\"\n\t\t\t\t\t\t).arg(\n\t\t\t\t\t\t\tentry.text,\n\t\t\t\t\t\t\tword));\n\t\t\t\t}\n\t\t\t\treturn (entry.emoji != nullptr);\n\t\t\t});\n\t\t\tlist.insert(end(list), emoji.begin(), emoji.end());\n\t\t}, [&](const MTPDemojiKeywordDeleted &keyword) {\n\t\t\tconst auto word = NormalizeKey(qs(keyword.vkeyword()));\n\t\t\tif (word.isEmpty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto i = data.emoji.find(word);\n\t\t\tif (i == end(data.emoji)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto &list = i->second;\n\t\t\tfor (const auto &emoji : keyword.vemoticons().v) {\n\t\t\t\tlist.erase(\n\t\t\t\t\tranges::remove(list, qs(emoji), &LangPackEmoji::text),\n\t\t\t\t\tend(list));\n\t\t\t}\n\t\t\tif (list.empty()) {\n\t\t\t\tdata.emoji.erase(i);\n\t\t\t}\n\t\t});\n\t}\n\tif (data.emoji.empty()) {\n\t\tdata.maxKeyLength = 0;\n\t} else {\n\t\tauto &&lengths = ranges::views::all(\n\t\t\tdata.emoji\n\t\t) | ranges::views::transform([](auto &&pair) {\n\t\t\treturn pair.first.size();\n\t\t});\n\t\tdata.maxKeyLength = *ranges::max_element(lengths);\n\t}\n}\n\n} // namespace\n\nclass EmojiKeywords::LangPack final {\npublic:\n\tusing Delegate = details::EmojiKeywordsLangPackDelegate;\n\n\tLangPack(not_null<Delegate*> delegate, const QString &id);\n\tLangPack(const LangPack &other) = delete;\n\tLangPack &operator=(const LangPack &other) = delete;\n\t~LangPack();\n\n\t[[nodiscard]] QString id() const;\n\n\tvoid refresh();\n\tvoid apiChanged();\n\n\t[[nodiscard]] std::vector<Result> query(\n\t\tconst QString &normalized,\n\t\tbool exact) const;\n\t[[nodiscard]] int maxQueryLength() const;\n\nprivate:\n\tenum class State {\n\t\tReadingCache,\n\t\tPendingRequest,\n\t\tRequested,\n\t\tRefreshed,\n\t};\n\n\tvoid readLocalCache();\n\tvoid applyDifference(const MTPEmojiKeywordsDifference &result);\n\tvoid applyData(LangPackData &&data);\n\n\tnot_null<Delegate*> _delegate;\n\tQString _id;\n\tState _state = State::ReadingCache;\n\tLangPackData _data;\n\tcrl::time _lastRefreshTime = 0;\n\tmtpRequestId _requestId = 0;\n\tbase::binary_guard _guard;\n\n};\n\nEmojiKeywords::LangPack::LangPack(\n\tnot_null<Delegate*> delegate,\n\tconst QString &id)\n: _delegate(delegate)\n, _id(id) {\n\treadLocalCache();\n}\n\nEmojiKeywords::LangPack::~LangPack() {\n\tif (_requestId) {\n\t\tif (const auto api = _delegate->api()) {\n\t\t\tapi->request(_requestId).cancel();\n\t\t}\n\t}\n}\n\nvoid EmojiKeywords::LangPack::readLocalCache() {\n\tconst auto id = _id;\n\tauto callback = crl::guard(_guard.make_guard(), [=](\n\t\t\tLangPackData &&result) {\n\t\tapplyData(std::move(result));\n\t\trefresh();\n\t});\n\tcrl::async([id, callback = std::move(callback)]() mutable {\n\t\tcrl::on_main([\n\t\t\tcallback = std::move(callback),\n\t\t\tresult = ReadLocalCache(id)\n\t\t]() mutable {\n\t\t\tcallback(std::move(result));\n\t\t});\n\t});\n}\n\nQString EmojiKeywords::LangPack::id() const {\n\treturn _id;\n}\n\nvoid EmojiKeywords::LangPack::refresh() {\n\tif (_state != State::Refreshed) {\n\t\treturn;\n\t} else if (_lastRefreshTime > 0\n\t\t&& crl::now() - _lastRefreshTime < kRefreshEach) {\n\t\treturn;\n\t}\n\tconst auto api = _delegate->api();\n\tif (!api) {\n\t\t_state = State::PendingRequest;\n\t\treturn;\n\t}\n\t_state = State::Requested;\n\tconst auto send = [&](auto &&request) {\n\t\treturn api->request(\n\t\t\tstd::move(request)\n\t\t).done([=](const MTPEmojiKeywordsDifference &result) {\n\t\t\t_requestId = 0;\n\t\t\t_lastRefreshTime = crl::now();\n\t\t\tapplyDifference(result);\n\t\t}).fail([=] {\n\t\t\t_requestId = 0;\n\t\t\t_lastRefreshTime = crl::now();\n\t\t}).send();\n\t};\n\t_requestId = (_data.version > 0)\n\t\t? send(MTPmessages_GetEmojiKeywordsDifference(\n\t\t\tMTP_string(_id),\n\t\t\tMTP_int(_data.version)))\n\t\t: send(MTPmessages_GetEmojiKeywords(\n\t\t\tMTP_string(_id)));\n}\n\nvoid EmojiKeywords::LangPack::applyDifference(\n\t\tconst MTPEmojiKeywordsDifference &result) {\n\tresult.match([&](const MTPDemojiKeywordsDifference &data) {\n\t\tconst auto code = qs(data.vlang_code());\n\t\tconst auto version = data.vversion().v;\n\t\tconst auto &keywords = data.vkeywords().v;\n\t\tif (code != _id) {\n\t\t\tLOG((\"API Error: Bad lang_code for emoji keywords %1 -> %2\").arg(\n\t\t\t\t_id,\n\t\t\t\tcode));\n\t\t\t_data.version = 0;\n\t\t\t_state = State::Refreshed;\n\t\t\treturn;\n\t\t} else if (keywords.isEmpty() && _data.version >= version) {\n\t\t\t_state = State::Refreshed;\n\t\t\treturn;\n\t\t}\n\t\tconst auto id = _id;\n\t\tauto copy = _data;\n\t\tauto callback = crl::guard(_guard.make_guard(), [=](\n\t\t\t\tLangPackData &&result) {\n\t\t\tapplyData(std::move(result));\n\t\t});\n\t\tcrl::async([=,\n\t\t\tcopy = std::move(copy),\n\t\t\tcallback = std::move(callback)]() mutable {\n\t\t\tApplyDifference(copy, keywords, version);\n\t\t\tWriteLocalCache(id, copy);\n\t\t\tcrl::on_main([\n\t\t\t\tresult = std::move(copy),\n\t\t\t\tcallback = std::move(callback)\n\t\t\t]() mutable {\n\t\t\t\tcallback(std::move(result));\n\t\t\t});\n\t\t});\n\t});\n}\n\nvoid EmojiKeywords::LangPack::applyData(LangPackData &&data) {\n\t_data = std::move(data);\n\t_state = State::Refreshed;\n\t_delegate->langPackRefreshed();\n}\n\nvoid EmojiKeywords::LangPack::apiChanged() {\n\tif (_state == State::Requested && !_delegate->api()) {\n\t\t_requestId = 0;\n\t} else if (_state != State::PendingRequest) {\n\t\treturn;\n\t}\n\t_state = State::Refreshed;\n\trefresh();\n}\n\nstd::vector<Result> EmojiKeywords::LangPack::query(\n\t\tconst QString &normalized,\n\t\tbool exact) const {\n\tif (normalized.size() > _data.maxKeyLength\n\t\t|| _data.emoji.empty()\n\t\t|| (exact && SkipExactKeyword(_id, normalized))) {\n\t\treturn {};\n\t}\n\n\tconst auto from = _data.emoji.lower_bound(normalized);\n\tauto &&chosen = ranges::make_subrange(\n\t\tfrom,\n\t\tend(_data.emoji)\n\t) | ranges::views::take_while([&](const auto &pair) {\n\t\tconst auto &key = pair.first;\n\t\treturn exact ? (key == normalized) : key.startsWith(normalized);\n\t});\n\n\tauto result = std::vector<Result>();\n\tfor (const auto &[key, list] : chosen) {\n\t\tAppendFoundEmoji(result, key, list);\n\t}\n\treturn result;\n}\n\nint EmojiKeywords::LangPack::maxQueryLength() const {\n\treturn _data.maxKeyLength;\n}\n\nEmojiKeywords::EmojiKeywords() {\n\tcrl::on_main(&_guard, [=] {\n\t\thandleSessionChanges();\n\t});\n}\n\nEmojiKeywords::~EmojiKeywords() = default;\n\nnot_null<details::EmojiKeywordsLangPackDelegate*> EmojiKeywords::delegate() {\n\treturn static_cast<details::EmojiKeywordsLangPackDelegate*>(this);\n}\n\nApiWrap *EmojiKeywords::api() {\n\treturn _api;\n}\n\nvoid EmojiKeywords::langPackRefreshed() {\n\t_refreshed.fire({});\n}\n\nvoid EmojiKeywords::handleSessionChanges() {\n\tCore::App().domain().activeSessionValue( // #TODO multi someSessionValue\n\t) | rpl::map([](Main::Session *session) {\n\t\treturn session ? &session->api() : nullptr;\n\t}) | rpl::on_next([=](ApiWrap *api) {\n\t\tapiChanged(api);\n\t}, _lifetime);\n}\n\nvoid EmojiKeywords::apiChanged(ApiWrap *api) {\n\t_api = api;\n\tif (_api) {\n\t\tcrl::on_main(&_api->session(), crl::guard(&_guard, [=] {\n\t\t\tLang::CurrentCloudManager().firstLanguageSuggestion(\n\t\t\t) | rpl::filter([=] {\n\t\t\t\t// Refresh with the suggested language if we already were asked.\n\t\t\t\treturn !_data.empty();\n\t\t\t}) | rpl::on_next([=] {\n\t\t\t\trefresh();\n\t\t\t}, _suggestedChangeLifetime);\n\t\t}));\n\t} else {\n\t\t_langsRequestId = 0;\n\t\t_suggestedChangeLifetime.destroy();\n\t}\n\tfor (const auto &[language, item] : _data) {\n\t\titem->apiChanged();\n\t}\n}\n\nvoid EmojiKeywords::refresh() {\n\tauto list = languages();\n\tif (_localList != list) {\n\t\t_localList = std::move(list);\n\t\trefreshRemoteList();\n\t} else {\n\t\trefreshFromRemoteList();\n\t}\n}\n\nstd::vector<QString> EmojiKeywords::languages() {\n\tif (!_api) {\n\t\treturn {};\n\t}\n\trefreshInputLanguages();\n\n\tauto result = std::vector<QString>();\n\tconst auto yield = [&](const QString &language) {\n\t\tresult.push_back(language);\n\t};\n\tconst auto yieldList = [&](const QStringList &list) {\n\t\tresult.insert(end(result), list.begin(), list.end());\n\t};\n\tyield(Lang::Id());\n\tyield(Lang::DefaultLanguageId());\n\tyield(Lang::CurrentCloudManager().suggestedLanguage());\n\tyield(Platform::SystemLanguage());\n\tyieldList(QLocale::system().uiLanguages());\n\tfor (const auto &list : _inputLanguages) {\n\t\tyieldList(list);\n\t}\n\tranges::sort(result);\n\treturn result;\n}\n\nvoid EmojiKeywords::refreshInputLanguages() {\n\tconst auto method = QGuiApplication::inputMethod();\n\tif (!method) {\n\t\treturn;\n\t}\n\tconst auto list = method->locale().uiLanguages();\n\tconst auto i = ranges::find(_inputLanguages, list);\n\tif (i != end(_inputLanguages)) {\n\t\tstd::rotate(i, i + 1, end(_inputLanguages));\n\t} else {\n\t\tif (_inputLanguages.size() >= kKeepNotUsedInputLanguagesCount) {\n\t\t\t_inputLanguages.pop_front();\n\t\t}\n\t\t_inputLanguages.push_back(list);\n\t}\n}\n\nrpl::producer<> EmojiKeywords::refreshed() const {\n\treturn _refreshed.events();\n}\n\nstd::vector<Result> EmojiKeywords::query(\n\t\tconst QString &query,\n\t\tbool exact) const {\n\tconst auto normalized = NormalizeQuery(query);\n\tif (normalized.isEmpty()) {\n\t\treturn {};\n\t}\n\tauto result = std::vector<Result>();\n\tfor (const auto &[language, item] : _data) {\n\t\tconst auto list = item->query(normalized, exact);\n\n\t\t// It is important that the 'result' won't relocate while inserting.\n\t\tresult.reserve(result.size() + list.size());\n\t\tconst auto alreadyBegin = begin(result);\n\t\tconst auto alreadyEnd = alreadyBegin + result.size();\n\n\t\tauto &&add = ranges::views::all(\n\t\t\tlist\n\t\t) | ranges::views::filter([&](Result entry) {\n\t\t\t// In each item->query() result the list has no duplicates.\n\t\t\t// So we need to check only for duplicates between queries.\n\t\t\tconst auto i = ranges::find(\n\t\t\t\talreadyBegin,\n\t\t\t\talreadyEnd,\n\t\t\t\tentry.emoji,\n\t\t\t\t&Result::emoji);\n\t\t\treturn (i == alreadyEnd);\n\t\t});\n\t\tresult.insert(end(result), add.begin(), add.end());\n\t}\n\tif (!exact) {\n\t\tAppendLegacySuggestions(result, query);\n\t}\n\treturn result;\n}\n\nstd::vector<Result> EmojiKeywords::queryMine(\n\t\tconst QString &query,\n\t\tbool exact) const {\n\treturn ApplyVariants(PrioritizeRecent(this->query(query, exact)));\n}\n\nstd::vector<Result> EmojiKeywords::PrioritizeRecent(\n\t\tstd::vector<Result> list) {\n\tusing Entry = Result;\n\tauto lastRecent = begin(list);\n\tconst auto &recent = Core::App().settings().recentEmoji();\n\tfor (const auto &item : recent) {\n\t\tconst auto emoji = std::get_if<EmojiPtr>(&item.id.data);\n\t\tif (!emoji) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto original = (*emoji)->original()\n\t\t\t? (*emoji)->original()\n\t\t\t: (*emoji);\n\t\tconst auto it = ranges::find(list, original, [](const Entry &entry) {\n\t\t\treturn entry.emoji;\n\t\t});\n\t\tif (it > lastRecent && it != end(list)) {\n\t\t\tstd::rotate(lastRecent, it, it + 1);\n\t\t\t++lastRecent;\n\t\t}\n\t}\n\treturn list;\n}\n\nstd::vector<Result> EmojiKeywords::ApplyVariants(std::vector<Result> list) {\n\tauto &settings = Core::App().settings();\n\tfor (auto &item : list) {\n\t\titem.emoji = settings.lookupEmojiVariant(item.emoji);\n\t}\n\treturn list;\n}\n\nint EmojiKeywords::maxQueryLength() const {\n\tif (_data.empty()) {\n\t\treturn 0;\n\t}\n\tauto &&lengths = _data | ranges::views::transform([](const auto &pair) {\n\t\treturn pair.second->maxQueryLength();\n\t});\n\treturn *ranges::max_element(lengths);\n}\n\nvoid EmojiKeywords::refreshRemoteList() {\n\tif (!_api) {\n\t\t_localList.clear();\n\t\tsetRemoteList({});\n\t\treturn;\n\t}\n\t_api->request(base::take(_langsRequestId)).cancel();\n\tauto languages = QVector<MTPstring>();\n\tfor (const auto &id : _localList) {\n\t\tlanguages.push_back(MTP_string(id));\n\t}\n\t_langsRequestId = _api->request(MTPmessages_GetEmojiKeywordsLanguages(\n\t\tMTP_vector<MTPstring>(languages)\n\t)).done([=](const MTPVector<MTPEmojiLanguage> &result) {\n\t\tsetRemoteList(ranges::views::all(\n\t\t\tresult.v\n\t\t) | ranges::views::transform([](const MTPEmojiLanguage &language) {\n\t\t\treturn language.match([&](const MTPDemojiLanguage &language) {\n\t\t\t\treturn qs(language.vlang_code());\n\t\t\t});\n\t\t}) | ranges::to_vector);\n\t\t_langsRequestId = 0;\n\t}).fail([=] {\n\t\t_langsRequestId = 0;\n\t}).send();\n}\n\nvoid EmojiKeywords::setRemoteList(std::vector<QString> &&list) {\n\tif (_remoteList == list) {\n\t\treturn;\n\t}\n\t_remoteList = std::move(list);\n\tfor (auto i = begin(_data); i != end(_data);) {\n\t\tif (ranges::find(_remoteList, i->first) != end(_remoteList)) {\n\t\t\t++i;\n\t\t} else {\n\t\t\tif (_notUsedData.size() >= kKeepNotUsedLangPacksCount) {\n\t\t\t\t_notUsedData.pop_front();\n\t\t\t}\n\t\t\t_notUsedData.push_back(std::move(i->second));\n\t\t\ti = _data.erase(i);\n\t\t}\n\t}\n\trefreshFromRemoteList();\n}\n\nvoid EmojiKeywords::refreshFromRemoteList() {\n\tfor (const auto &id : _remoteList) {\n\t\tif (const auto i = _data.find(id); i != end(_data)) {\n\t\t\ti->second->refresh();\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto i = ranges::find(\n\t\t\t_notUsedData,\n\t\t\tid,\n\t\t\t[](const std::unique_ptr<LangPack> &p) { return p->id(); });\n\t\tif (i != end(_notUsedData)) {\n\t\t\t_data.emplace(id, std::move(*i));\n\t\t\t_notUsedData.erase(i);\n\t\t} else {\n\t\t\t_data.emplace(\n\t\t\t\tid,\n\t\t\t\tstd::make_unique<LangPack>(delegate(), id));\n\t\t}\n\t}\n}\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/emoji_keywords.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass ApiWrap;\n\nnamespace ChatHelpers {\nnamespace details {\n\nclass EmojiKeywordsLangPackDelegate {\npublic:\n\tvirtual ApiWrap *api() = 0;\n\tvirtual void langPackRefreshed() = 0;\n\nprotected:\n\t~EmojiKeywordsLangPackDelegate() = default;\n\n};\n\n} // namespace details\n\nclass EmojiKeywords final : private details::EmojiKeywordsLangPackDelegate {\npublic:\n\tEmojiKeywords();\n\tEmojiKeywords(const EmojiKeywords &other) = delete;\n\tEmojiKeywords &operator=(const EmojiKeywords &other) = delete;\n\t~EmojiKeywords();\n\n\tvoid refresh();\n\n\t[[nodiscard]] rpl::producer<> refreshed() const;\n\n\tstruct Result {\n\t\tEmojiPtr emoji = nullptr;\n\t\tQString label;\n\t\tQString replacement;\n\t};\n\t[[nodiscard]] std::vector<Result> query(\n\t\tconst QString &query,\n\t\tbool exact = false) const;\n\t[[nodiscard]] std::vector<Result> queryMine(\n\t\tconst QString &query,\n\t\tbool exact = false) const;\n\t[[nodiscard]] int maxQueryLength() const;\n\nprivate:\n\tclass LangPack;\n\n\tnot_null<details::EmojiKeywordsLangPackDelegate*> delegate();\n\tApiWrap *api() override;\n\tvoid langPackRefreshed() override;\n\n\t[[nodiscard]] static std::vector<Result> PrioritizeRecent(\n\t\tstd::vector<Result> list);\n\t[[nodiscard]] static std::vector<Result> ApplyVariants(\n\t\tstd::vector<Result> list);\n\n\tvoid handleSessionChanges();\n\tvoid apiChanged(ApiWrap *api);\n\tvoid refreshInputLanguages();\n\t[[nodiscard]] std::vector<QString> languages();\n\tvoid refreshRemoteList();\n\tvoid setRemoteList(std::vector<QString> &&list);\n\tvoid refreshFromRemoteList();\n\n\tApiWrap *_api = nullptr;\n\tstd::vector<QString> _localList;\n\tstd::vector<QString> _remoteList;\n\tmtpRequestId _langsRequestId = 0;\n\tbase::flat_map<QString, std::unique_ptr<LangPack>> _data;\n\tstd::deque<std::unique_ptr<LangPack>> _notUsedData;\n\tstd::deque<QStringList> _inputLanguages;\n\trpl::event_stream<> _refreshed;\n\n\trpl::lifetime _suggestedChangeLifetime;\n\n\trpl::lifetime _lifetime;\n\tbase::has_weak_ptr _guard;\n\n};\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/emoji_list_widget.h\"\n\n#include \"window/window_media_preview.h\"\n#include \"api/api_peer_photo.h\"\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/tabbed_search.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/text/custom_emoji_instance.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"layout/layout_position.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"chat_helpers/emoji_keywords.h\"\n#include \"chat_helpers/stickers_list_widget.h\"\n#include \"chat_helpers/stickers_list_footer.h\"\n#include \"emoji_suggestions_data.h\"\n#include \"emoji_suggestions_helper.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"mainwidget.h\"\n#include \"core/core_settings.h\"\n#include \"core/application.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace ChatHelpers {\nnamespace {\n\nconstexpr auto kCollapsedRows = 3;\nconstexpr auto kAppearDuration = 0.3;\nconstexpr auto kCustomSearchLimit = 256;\nconstexpr auto kCloudSearchPageLimit = 50;\nconstexpr auto kColorPickerDelay = crl::time(500);\nconstexpr auto kSearchRequestDelay = 400;\nconstexpr auto kPreloadSearchPages = 4;\n\nusing Core::RecentEmojiId;\nusing Core::RecentEmojiDocument;\n\n} // namespace\n\nclass EmojiColorPicker final : public Ui::RpWidget {\npublic:\n\tEmojiColorPicker(QWidget *parent, const style::EmojiPan &st);\n\n\tvoid showEmoji(EmojiPtr emoji, bool allLabel = false);\n\n\tvoid clearSelection();\n\tvoid handleMouseMove(QPoint globalPos);\n\tvoid handleMouseRelease(QPoint globalPos);\n\tvoid setSingleSize(QSize size);\n\n\tvoid showAnimated();\n\tvoid hideAnimated();\n\tvoid hideFast();\n\n\t[[nodiscard]] rpl::producer<EmojiChosen> chosen() const;\n\t[[nodiscard]] rpl::producer<> hidden() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\nprivate:\n\tvoid createAllLabel();\n\tvoid animationCallback();\n\tvoid updateSize();\n\t[[nodiscard]] int topColorAllSkip() const;\n\n\tvoid drawVariant(QPainter &p, int variant);\n\n\tvoid updateSelected();\n\tvoid setSelected(int newSelected);\n\n\tconst style::EmojiPan &_st;\n\n\tbool _ignoreShow = false;\n\n\tQVector<EmojiPtr> _variants;\n\n\tint _selected = -1;\n\tint _pressedSel = -1;\n\tQPoint _lastMousePos;\n\tQSize _singleSize;\n\tQPoint _areaPosition;\n\tQPoint _innerPosition;\n\tUi::RoundRect _backgroundRect;\n\tUi::RoundRect _overBg;\n\tUi::BoxShadow _shadow;\n\n\tbool _hiding = false;\n\tQPixmap _cache;\n\tUi::Animations::Simple _a_opacity;\n\n\tstd::unique_ptr<Ui::FlatLabel> _allLabel;\n\n\trpl::event_stream<EmojiChosen> _chosen;\n\trpl::event_stream<> _hidden;\n\n};\n\nstruct EmojiListWidget::CustomEmojiInstance {\n\tstd::unique_ptr<Ui::Text::CustomEmoji> emoji;\n\tbool recentOnly = false;\n};\n\nstruct EmojiListWidget::RecentOne {\n\tstd::shared_ptr<Data::EmojiStatusCollectible> collectible;\n\tUi::Text::CustomEmoji *custom = nullptr;\n\tRecentEmojiId id;\n\tmutable QImage premiumLock;\n};\n\nEmojiColorPicker::EmojiColorPicker(\n\tQWidget *parent,\n\tconst style::EmojiPan &st)\n: RpWidget(parent)\n, _st(st)\n, _backgroundRect(st::emojiPanRadius, _st.bg)\n, _overBg(st::emojiPanRadius, _st.overBg)\n, _shadow(_st.showAnimation.shadow) {\n\tsetMouseTracking(true);\n}\n\nvoid EmojiColorPicker::showEmoji(EmojiPtr emoji, bool allLabel) {\n\tif (!emoji || !emoji->hasVariants()) {\n\t\treturn;\n\t}\n\tif (!allLabel) {\n\t\t_allLabel = nullptr;\n\t} else if (!_allLabel) {\n\t\tcreateAllLabel();\n\t}\n\t_ignoreShow = false;\n\n\t_variants.resize(emoji->variantsCount() + 1);\n\tfor (auto i = 0, size = int(_variants.size()); i != size; ++i) {\n\t\t_variants[i] = emoji->variant(i);\n\t}\n\n\tupdateSize();\n\n\tif (!_cache.isNull()) {\n\t\t_cache = QPixmap();\n\t}\n\tshowAnimated();\n}\n\nvoid EmojiColorPicker::createAllLabel() {\n\t_allLabel = std::make_unique<Ui::FlatLabel>(\n\t\tthis,\n\t\ttr::lng_emoji_color_all(),\n\t\t_st.colorAllLabel);\n\t_allLabel->show();\n\t_allLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n}\n\nvoid EmojiColorPicker::updateSize() {\n\tauto width = st::emojiPanMargins.left()\n\t\t+ _singleSize.width() * _variants.size()\n\t\t+ (_variants.size() - 2) * st::emojiColorsPadding\n\t\t+ st::emojiColorsSep\n\t\t+ st::emojiPanMargins.right();\n\tauto height = st::emojiPanMargins.top()\n\t\t+ 2 * st::emojiColorsPadding\n\t\t+ _singleSize.height()\n\t\t+ st::emojiPanMargins.bottom();\n\tif (_allLabel) {\n\t\t_allLabel->resizeToWidth(width\n\t\t\t- st::emojiPanMargins.left()\n\t\t\t- st::emojiPanMargins.right()\n\t\t\t- st::emojiPanColorAllPadding.left()\n\t\t\t- st::emojiPanColorAllPadding.right());\n\t\t_allLabel->move(\n\t\t\tst::emojiPanMargins.left() + st::emojiPanColorAllPadding.left(),\n\t\t\tst::emojiPanMargins.top() + st::emojiPanColorAllPadding.top());\n\t\theight += topColorAllSkip();\n\t}\n\tresize(width, height);\n\tupdate();\n\tupdateSelected();\n}\n\nvoid EmojiColorPicker::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tauto opacity = _a_opacity.value(_hiding ? 0. : 1.);\n\tif (opacity < 1.) {\n\t\tif (opacity > 0.) {\n\t\t\tp.setOpacity(opacity);\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (e->rect() != rect()) {\n\t\tp.setClipRect(e->rect());\n\t}\n\n\tauto inner = rect().marginsRemoved(st::emojiPanMargins);\n\tif (!_cache.isNull()) {\n\t\tp.drawPixmap(0, 0, _cache);\n\t\treturn;\n\t}\n\t_shadow.paint(p, inner, st::emojiPanRadius);\n\t_backgroundRect.paint(p, inner);\n\n\tconst auto skip = topColorAllSkip();\n\tauto x = st::emojiPanMargins.left() + 2 * st::emojiColorsPadding + _singleSize.width();\n\tif (rtl()) x = width() - x - st::emojiColorsSep;\n\tp.fillRect(x, st::emojiPanMargins.top() + skip + st::emojiColorsPadding, st::emojiColorsSep, inner.height() - st::emojiColorsPadding * 2 - skip, st::emojiColorsSepColor);\n\n\tif (_variants.isEmpty()) {\n\t\treturn;\n\t}\n\tp.translate(0, skip);\n\tfor (auto i = 0, count = int(_variants.size()); i != count; ++i) {\n\t\tdrawVariant(p, i);\n\t}\n}\n\nvoid EmojiColorPicker::mousePressEvent(QMouseEvent *e) {\n\tif (e->button() != Qt::LeftButton) {\n\t\treturn;\n\t}\n\t_lastMousePos = e->globalPos();\n\tupdateSelected();\n\t_pressedSel = _selected;\n}\n\nvoid EmojiColorPicker::mouseReleaseEvent(QMouseEvent *e) {\n\thandleMouseRelease(e->globalPos());\n}\n\nvoid EmojiColorPicker::handleMouseRelease(QPoint globalPos) {\n\t_lastMousePos = globalPos;\n\tint32 pressed = _pressedSel;\n\t_pressedSel = -1;\n\n\tupdateSelected();\n\tif (_selected >= 0 && (pressed < 0 || _selected == pressed)) {\n\t\t_chosen.fire_copy({ .emoji = _variants[_selected] });\n\t}\n\t_ignoreShow = true;\n\thideAnimated();\n}\n\nvoid EmojiColorPicker::setSingleSize(QSize size) {\n\tconst auto area = st::emojiPanArea;\n\t_singleSize = size;\n\t_areaPosition = QPoint(\n\t\t(_singleSize.width() - area.width()) / 2,\n\t\t(_singleSize.height() - area.height()) / 2);\n\tconst auto esize = Ui::Emoji::GetSizeLarge() / style::DevicePixelRatio();\n\t_innerPosition = QPoint(\n\t\t(area.width() - esize) / 2,\n\t\t(area.height() - esize) / 2);\n\tupdateSize();\n}\n\nvoid EmojiColorPicker::handleMouseMove(QPoint globalPos) {\n\t_lastMousePos = globalPos;\n\tupdateSelected();\n}\n\nvoid EmojiColorPicker::mouseMoveEvent(QMouseEvent *e) {\n\thandleMouseMove(e->globalPos());\n}\n\nvoid EmojiColorPicker::animationCallback() {\n\tupdate();\n\tif (!_a_opacity.animating()) {\n\t\t_cache = QPixmap();\n\t\tif (_allLabel) {\n\t\t\t_allLabel->show();\n\t\t}\n\t\tif (_hiding) {\n\t\t\thide();\n\t\t\t_hidden.fire({});\n\t\t} else {\n\t\t\t_lastMousePos = QCursor::pos();\n\t\t\tupdateSelected();\n\t\t}\n\t}\n}\n\nvoid EmojiColorPicker::hideFast() {\n\tclearSelection();\n\t_a_opacity.stop();\n\t_cache = QPixmap();\n\thide();\n\t_hidden.fire({});\n}\n\nrpl::producer<EmojiChosen> EmojiColorPicker::chosen() const {\n\treturn _chosen.events();\n}\n\nrpl::producer<> EmojiColorPicker::hidden() const {\n\treturn _hidden.events();\n}\n\nvoid EmojiColorPicker::hideAnimated() {\n\tif (_cache.isNull()) {\n\t\tif (_allLabel) {\n\t\t\t_allLabel->show();\n\t\t}\n\t\t_cache = Ui::GrabWidget(this);\n\t\tclearSelection();\n\t}\n\t_hiding = true;\n\tif (_allLabel) {\n\t\t_allLabel->hide();\n\t}\n\t_a_opacity.start([this] { animationCallback(); }, 1., 0., st::emojiPanDuration);\n}\n\nvoid EmojiColorPicker::showAnimated() {\n\tif (_ignoreShow) return;\n\n\tif (!isHidden() && !_hiding) {\n\t\treturn;\n\t}\n\t_hiding = false;\n\tif (_cache.isNull()) {\n\t\tif (_allLabel) {\n\t\t\t_allLabel->show();\n\t\t}\n\t\t_cache = Ui::GrabWidget(this);\n\t\tclearSelection();\n\t}\n\tshow();\n\tif (_allLabel) {\n\t\t_allLabel->hide();\n\t}\n\t_a_opacity.start([this] { animationCallback(); }, 0., 1., st::emojiPanDuration);\n}\n\nvoid EmojiColorPicker::clearSelection() {\n\t_pressedSel = -1;\n\tsetSelected(-1);\n\t_lastMousePos = mapToGlobal(QPoint(-10, -10));\n}\n\nint EmojiColorPicker::topColorAllSkip() const {\n\treturn _allLabel\n\t\t? (st::emojiPanColorAllPadding.top()\n\t\t\t+ _allLabel->height()\n\t\t\t+ st::emojiPanColorAllPadding.bottom())\n\t\t: 0;\n}\n\nvoid EmojiColorPicker::updateSelected() {\n\tauto newSelected = -1;\n\tauto p = mapFromGlobal(_lastMousePos);\n\tauto sx = rtl() ? (width() - p.x()) : p.x(), y = p.y() - st::emojiPanMargins.top() - topColorAllSkip() - st::emojiColorsPadding;\n\tif (y >= 0 && y < _singleSize.height()) {\n\t\tauto x = sx - st::emojiPanMargins.left() - st::emojiColorsPadding;\n\t\tif (x >= 0 && x < _singleSize.width()) {\n\t\t\tnewSelected = 0;\n\t\t} else {\n\t\t\tx -= _singleSize.width() + 2 * st::emojiColorsPadding + st::emojiColorsSep;\n\t\t\tif (x >= 0 && x < _singleSize.width() * (_variants.size() - 1)) {\n\t\t\t\tnewSelected = (x / _singleSize.width()) + 1;\n\t\t\t}\n\t\t}\n\t}\n\n\tsetSelected(newSelected);\n}\n\nvoid EmojiColorPicker::setSelected(int newSelected) {\n\tif (_selected == newSelected) {\n\t\treturn;\n\t}\n\tconst auto skip = topColorAllSkip();\n\tconst auto updateSelectedRect = [&] {\n\t\tif (_selected < 0) return;\n\t\tauto addedSkip = (_selected > 0)\n\t\t\t? (2 * st::emojiColorsPadding + st::emojiColorsSep)\n\t\t\t: 0;\n\t\tauto left = st::emojiPanMargins.left()\n\t\t\t+ st::emojiColorsPadding\n\t\t\t+ _selected * _singleSize.width()\n\t\t\t+ addedSkip;\n\t\trtlupdate(\n\t\t\tleft,\n\t\t\tst::emojiPanMargins.top() + st::emojiColorsPadding + skip,\n\t\t\t_singleSize.width(),\n\t\t\t_singleSize.height());\n\t};\n\tupdateSelectedRect();\n\t_selected = newSelected;\n\tupdateSelectedRect();\n\tsetCursor((_selected >= 0) ? style::cur_pointer : style::cur_default);\n}\n\nvoid EmojiColorPicker::drawVariant(QPainter &p, int variant) {\n\tconst auto w = QPoint(\n\t\tst::emojiPanMargins.left(),\n\t\tst::emojiPanMargins.top()\n\t) + QPoint(\n\t\t(st::emojiColorsPadding\n\t\t\t+ variant * _singleSize.width()\n\t\t\t+ (variant\n\t\t\t\t? (2 * st::emojiColorsPadding + st::emojiColorsSep)\n\t\t\t\t: 0)),\n\t\tst::emojiColorsPadding\n\t) + _areaPosition;\n\tif (variant == _selected) {\n\t\tQPoint tl(w);\n\t\tif (rtl()) tl.setX(width() - tl.x() - st::emojiPanArea.width());\n\n\t\t_overBg.paint(p, QRect(tl, st::emojiPanArea));\n\t}\n\tUi::Emoji::Draw(\n\t\tp,\n\t\t_variants[variant],\n\t\tUi::Emoji::GetSizeLarge(),\n\t\tw.x() + _innerPosition.x(),\n\t\tw.y() + _innerPosition.y());\n}\n\nstd::vector<EmojiStatusId> DocumentListToRecent(\n\t\tconst std::vector<DocumentId> &documents) {\n\treturn documents | ranges::views::transform([](DocumentId id) {\n\t\treturn EmojiStatusId{ .documentId = id };\n\t}) | ranges::to_vector;\n}\n\nEmojiListWidget::EmojiListWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tPauseReason level,\n\tMode mode)\n: EmojiListWidget(parent, {\n\t.show = controller->uiShow(),\n\t.mode = mode,\n\t.paused = Window::PausedIn(controller, level),\n}) {\n}\n\nEmojiListWidget::EmojiListWidget(\n\tQWidget *parent,\n\tEmojiListDescriptor &&descriptor)\n: Inner(\n\tparent,\n\tdescriptor.st ? *descriptor.st : st::defaultEmojiPan,\n\tdescriptor.show,\n\tstd::move(descriptor.paused))\n, _show(std::move(descriptor.show))\n, _features(descriptor.features)\n, _onlyUnicodeEmoji(descriptor.mode == Mode::PeerTitle)\n, _mode(_onlyUnicodeEmoji ? Mode::Full : descriptor.mode)\n, _mediaPreviewParent(descriptor.mediaPreviewParent)\n, _mediaPreviewMargins(descriptor.mediaPreviewMargins)\n, _api(&session().mtp())\n, _staticCount(_mode == Mode::Full ? kEmojiSectionCount : 1)\n, _premiumIcon(_mode == Mode::EmojiStatus\n\t? std::make_unique<GradientPremiumStar>()\n\t: nullptr)\n, _localSetsManager(\n\tstd::make_unique<LocalStickersManager>(&session()))\n, _customRecentFactory(std::move(descriptor.customRecentFactory))\n, _freeEffects(std::move(descriptor.freeEffects))\n, _customTextColor(std::move(descriptor.customTextColor))\n, _overBg(st::emojiPanRadius, st().overBg)\n, _premiumMark(std::make_unique<StickerPremiumMark>(\n\t&session(),\n\tst::emojiPremiumLock))\n, _collapsedBg(st::emojiPanExpand.height / 2, st().headerFg)\n, _searchRequestTimer([=] { sendSearchRequest(); })\n, _picker(this, st())\n, _showPickerTimer([=] { showPicker(); })\n, _previewTimer([=] { showPreview(); }) {\n\tsetMouseTracking(true);\n\tif (st().bg->c.alpha() > 0) {\n\t\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\t}\n\n\tif (_mode != Mode::RecentReactions\n\t\t&& _mode != Mode::BackgroundEmoji\n\t\t&& _mode != Mode::ChannelStatus\n\t\t&& !_onlyUnicodeEmoji) {\n\t\tsetupSearch();\n\t}\n\n\tif (_mode == Mode::ChannelStatus) {\n\t\tsession().api().peerPhoto().emojiListValue(\n\t\t\tApi::PeerPhoto::EmojiListType::NoChannelStatus\n\t\t) | rpl::on_next([=](const std::vector<DocumentId> &list) {\n\t\t\t_restrictedCustomList = { begin(list), end(list) };\n\t\t\tif (!_custom.empty()) {\n\t\t\t\trefreshCustom();\n\t\t\t}\n\t\t}, lifetime());\n\t} else if (_mode == Mode::EmojiStatus && _features.collectibleStatus) {\n\t\tsession().data().emojiStatuses().collectiblesUpdates(\n\t\t) | rpl::on_next([=] {\n\t\t\trefreshCustom();\n\t\t}, lifetime());\n\t}\n\n\t_customSingleSize = Data::FrameSizeFromTag(\n\t\tData::CustomEmojiManager::SizeTag::Large\n\t) / style::DevicePixelRatio();\n\n\t_picker->hide();\n\n\tfor (auto i = 1; i != _staticCount; ++i) {\n\t\tconst auto section = static_cast<Section>(i);\n\t\t_counts[i] = Ui::Emoji::GetSectionCount(section);\n\t}\n\n\t_picker->chosen(\n\t) | rpl::on_next([=](EmojiChosen data) {\n\t\tcolorChosen(data);\n\t}, lifetime());\n\n\t_picker->hidden(\n\t) | rpl::on_next([=] {\n\t\tpickerHidden();\n\t}, lifetime());\n\n\tsession().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::EmojiSet\n\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\treturn (update.peer.get() == _megagroupSet);\n\t}) | rpl::on_next([=] {\n\t\trefreshCustom();\n\t\tresizeToWidth(width());\n\t}, lifetime());\n\n\tsession().data().stickers().updated(\n\t\tData::StickersType::Emoji\n\t) | rpl::on_next([=] {\n\t\trefreshCustom();\n\t\tresizeToWidth(width());\n\t}, lifetime());\n\n\trpl::combine(\n\t\tData::AmPremiumValue(&session()),\n\t\tsession().premiumPossibleValue()\n\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\trefreshCustom();\n\t\tresizeToWidth(width());\n\t}, lifetime());\n\n\trpl::single(\n\t\trpl::empty\n\t) | rpl::then(\n\t\tstyle::PaletteChanged()\n\t) | rpl::on_next([=] {\n\t\tinitButton(_add, tr::lng_stickers_featured_add(tr::now), false);\n\t\tinitButton(_unlock, tr::lng_emoji_featured_unlock(tr::now), true);\n\t\tinitButton(_restore, tr::lng_emoji_premium_restore(tr::now), true);\n\t}, lifetime());\n\n\tif (!descriptor.customRecentList.empty()) {\n\t\tfillRecentFrom(descriptor.customRecentList);\n\t}\n}\n\nEmojiListWidget::~EmojiListWidget() {\n\tbase::take(_customEmoji);\n}\n\nvoid EmojiListWidget::setupSearch() {\n\tconst auto session = &_show->session();\n\tconst auto type = (_mode == Mode::EmojiStatus)\n\t\t? TabbedSearchType::Status\n\t\t: (_mode == Mode::UserpicBuilder)\n\t\t? TabbedSearchType::ProfilePhoto\n\t\t: TabbedSearchType::Emoji;\n\t_search = MakeSearch(this, st(), [=](std::vector<QString> &&query) {\n\t\t_nextSearchQuery = std::move(query);\n\t\tInvokeQueued(this, [=] {\n\t\t\tapplyNextSearchQuery();\n\t\t});\n\t\t_searchQueries.fire_copy(_nextSearchQuery);\n\t}, session, type);\n}\n\nrpl::producer<std::vector<QString>> EmojiListWidget::searchQueries() const {\n\treturn _searchQueries.events();\n}\n\nrpl::producer<int> EmojiListWidget::recentShownCount() const {\n\treturn _recentShownCount.value();\n}\n\nvoid EmojiListWidget::applyNextSearchQuery() {\n\tif (_searchQuery == _nextSearchQuery) {\n\t\treturn;\n\t}\n\t_searchQuery = _nextSearchQuery;\n\tstd::swap(_searchEmoji, _searchEmojiPrevious);\n\t_searchEmoji.clear();\n\tconst auto finish = [&](bool searching = true) {\n\t\tif (!_searchMode && !searching) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto modeChanged = (_searchMode != searching);\n\t\tclearSelection();\n\t\tif (modeChanged) {\n\t\t\tif (_picker) {\n\t\t\t\t_picker->hideAnimated();\n\t\t\t}\n\t\t\t_colorAllRipple = nullptr;\n\t\t\tfor (auto &set : _custom) {\n\t\t\t\tset.ripple = nullptr;\n\t\t\t}\n\t\t\t_searchMode = searching;\n\t\t}\n\t\tif (!searching) {\n\t\t\t_searchResults.clear();\n\t\t\t_searchCustomIds.clear();\n\t\t\t_searchSets.clear();\n\t\t}\n\t\tresizeToWidth(width());\n\t\t_recentShownCount = searching\n\t\t\t? _searchResults.size()\n\t\t\t: _recent.size();\n\t\tupdate();\n\t\tif (modeChanged) {\n\t\t\tvisibleTopBottomUpdated(getVisibleTop(), getVisibleBottom());\n\t\t}\n\t\tupdateSelected();\n\t};\n\tif (_searchQuery.empty()) {\n\t\tcancelSearchRequest();\n\t\tfinish(false);\n\t\treturn;\n\t}\n\tconst auto guard = gsl::finally([&] { finish(); });\n\tauto plain = collectPlainSearchResults();\n\tif (_searchEmoji == _searchEmojiPrevious) {\n\t\treturn;\n\t}\n\t_searchEmoticon = QString();\n\tfor (const auto emoji : plain) {\n\t\t_searchEmoticon += emoji->text();\n\t}\n\t_searchResults.clear();\n\t_searchCustomIds.clear();\n\t_searchSets.clear();\n\tif (_mode == Mode::Full) {\n\t\tfor (const auto emoji : plain) {\n\t\t\t_searchResults.push_back({\n\t\t\t\t.id = { emoji },\n\t\t\t});\n\t\t}\n\t}\n\tif (_mode != Mode::Full || session().premium()) {\n\t\tappendPremiumSearchResults();\n\t}\n\tif (_mode == Mode::Full) {\n\t\tappendLocalPackSearchResults();\n\t}\n\n\t_searchQueryText = ranges::accumulate(\n\t\t_searchQuery,\n\t\tQString(),\n\t\t[](QString a, const QString &b) {\n\t\t\treturn a.isEmpty() ? b : (a + ' ' + b);\n\t\t}).trimmed();\n\tif (!_searchQueryText.isEmpty()) {\n\t\ttoggleSearchLoading(false);\n\t\tif (const auto requestId = base::take(_searchCloudRequestId)) {\n\t\t\t_api.request(requestId).cancel();\n\t\t}\n\t\tif (const auto requestId = base::take(_searchSetsRequestId)) {\n\t\t\t_api.request(requestId).cancel();\n\t\t}\n\t\t_searchNextRequestQuery = _searchQueryText;\n\t\t_searchRequestQuery = _searchQueryText;\n\t\tconst auto cloudCached = _searchCloudCache.find(_searchRequestQuery)\n\t\t\t!= _searchCloudCache.cend();\n\t\tconst auto setsCached = _searchSetsCache.find(_searchRequestQuery)\n\t\t\t!= _searchSetsCache.cend();\n\t\tif (cloudCached || setsCached) {\n\t\t\t_searchRequestTimer.cancel();\n\t\t\tfillCloudSearchResults();\n\t\t\tfillCloudSearchSets();\n\t\t\tif (!cloudCached || !setsCached) {\n\t\t\t\tsendSearchRequest();\n\t\t\t}\n\t\t} else {\n\t\t\t_searchRequestTimer.callOnce(kSearchRequestDelay);\n\t\t}\n\t}\n}\n\nvoid EmojiListWidget::showPreview() {\n\tif (const auto over = std::get_if<OverEmoji>(&_pressed)) {\n\t\tif (const auto custom = lookupCustomEmoji(over)) {\n\t\t\tshowPreviewFor(custom.document);\n\t\t\t_previewShown = true;\n\t\t}\n\t}\n}\n\nvoid EmojiListWidget::showPreviewFor(not_null<DocumentData*> document) {\n\tif ((_mode == Mode::FullReactions || _mode == Mode::RecentReactions)\n\t\t&& _mediaPreviewParent) {\n\t\tensureMediaPreview();\n\t\t_mediaPreview->showPreview(document->stickerSetOrigin(), document);\n\t} else {\n\t\t_show->showMediaPreview(document->stickerSetOrigin(), document);\n\t}\n}\n\nvoid EmojiListWidget::ensureMediaPreview() {\n\tif (!_mediaPreviewParent) {\n\t\treturn;\n\t}\n\tif (_mediaPreview) {\n\t\t_mediaPreview->raise();\n\t\treturn;\n\t}\n\tconst auto controller = Core::App().findWindow(_show->toastParent());\n\tconst auto sessionController = controller\n\t\t? controller->sessionController()\n\t\t: nullptr;\n\tif (sessionController) {\n\t\tconst auto tooSmall = _mediaPreviewParent->height()\n\t\t\t< st::emojiPanEmojiPreviewMinHeight;\n\t\tconst auto parent = tooSmall\n\t\t\t? sessionController->content()\n\t\t\t: _mediaPreviewParent;\n\t\t_mediaPreview = base::make_unique_q<Window::MediaPreviewWidget>(\n\t\t\tparent,\n\t\t\tsessionController);\n\t\tif (!tooSmall) {\n\t\t\t_mediaPreview->setCustomPadding(\n\t\t\t\tst::emojiPanReactionsPreviewPadding);\n\t\t\t_mediaPreview->setBackgroundMargins(_mediaPreviewMargins);\n\t\t\t_mediaPreview->setCustomRadius(st::emojiPanEmojiPreviewRadius);\n\t\t}\n\t\t_mediaPreview->show();\n\t\t_mediaPreview->setGeometry(parent->geometry());\n\t\t_mediaPreview->raise();\n\t}\n}\n\nstd::vector<EmojiPtr> EmojiListWidget::collectPlainSearchResults() {\n\treturn SearchEmoji(_searchQuery, _searchEmoji);\n}\n\nvoid EmojiListWidget::appendPremiumSearchResults() {\n\tconst auto test = session().isTestMode();\n\tauto &owner = session().data();\n\tconst auto checkCustom = [&](EmojiPtr emoji, DocumentId id) {\n\t\treturn emoji\n\t\t\t&& _searchEmoji.contains(emoji)\n\t\t\t&& (_searchResults.size() < kCustomSearchLimit)\n\t\t\t&& _searchCustomIds.emplace(id).second;\n\t};\n\tfor (const auto &recent : _recent) {\n\t\tif (!recent.custom) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto &idData = recent.id.data;\n\t\tconst auto id = std::get_if<Core::RecentEmojiDocument>(&idData);\n\t\tif (!id || id->test != test) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto sticker = owner.document(id->id)->sticker();\n\t\tconst auto emoji = sticker\n\t\t\t? Ui::Emoji::Find(sticker->alt)\n\t\t\t: nullptr;\n\t\tif (checkCustom(emoji, id->id)) {\n\t\t\t_searchResults.push_back(recent);\n\t\t}\n\t}\n\tfor (const auto &set : _custom) {\n\t\tfor (const auto &one : set.list) {\n\t\t\tconst auto id = one.document->id;\n\t\t\tif (checkCustom(one.emoji, id)) {\n\t\t\t\t_searchResults.push_back({\n\t\t\t\t\t.custom = one.custom,\n\t\t\t\t\t.id = { RecentEmojiDocument{ .id = id, .test = test } },\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid EmojiListWidget::appendLocalPackSearchResults() {\n\tconst auto text = _searchQueryText.toLower();\n\tif (text.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto test = session().isTestMode();\n\tconst auto &sets = session().data().stickers().sets();\n\tconst auto processSet = [&](uint64 setId) {\n\t\tconst auto it = sets.find(setId);\n\t\tif (it == sets.end()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto set = it->second.get();\n\t\tif (!(set->flags & Data::StickersSetFlag::Emoji)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto title = set->title.toLower();\n\t\tif (!title.startsWith(text)\n\t\t\t&& !title.contains(u' ' + text)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto &list = set->stickers.empty()\n\t\t\t? set->covers\n\t\t\t: set->stickers;\n\t\tfor (const auto document : list) {\n\t\t\tif (_searchResults.size() >= kCustomSearchLimit) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto sticker = document->sticker();\n\t\t\tif (!sticker) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto id = document->id;\n\t\t\tif (!_searchCustomIds.emplace(id).second) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto statusId = EmojiStatusId{ id };\n\t\t\t_searchResults.push_back({\n\t\t\t\t.custom = resolveCustomEmoji(\n\t\t\t\t\tstatusId,\n\t\t\t\t\tdocument,\n\t\t\t\t\tSearchEmojiSectionSetId()),\n\t\t\t\t.id = { RecentEmojiDocument{ .id = id, .test = test } },\n\t\t\t});\n\t\t}\n\t};\n\tfor (const auto setId\n\t\t: session().data().stickers().emojiSetsOrder()) {\n\t\tprocessSet(setId);\n\t}\n\tfor (const auto setId\n\t\t: session().data().stickers().featuredEmojiSetsOrder()) {\n\t\tprocessSet(setId);\n\t}\n}\n\nvoid EmojiListWidget::toggleSearchLoading(bool loading) {\n\tif (_search) {\n\t\t_search->setLoading(loading);\n\t}\n\tif (_searchLoading != loading) {\n\t\t_searchLoading = loading;\n\t\tupdate();\n\t}\n}\n\nvoid EmojiListWidget::sendSearchRequest() {\n\t_searchRequestQuery = _searchNextRequestQuery;\n\tif (_searchRequestQuery.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto query = _searchRequestQuery;\n\n\tconst auto cloudCached = _searchCloudCache.find(\n\t\tquery) != _searchCloudCache.cend();\n\tconst auto setsCached = _searchSetsCache.find(\n\t\tquery) != _searchSetsCache.cend();\n\tif (cloudCached && setsCached) {\n\t\ttoggleSearchLoading(false);\n\t\treturn;\n\t}\n\ttoggleSearchLoading(true);\n\n\tif (!cloudCached) {\n\t\trequestSearchCloud(query, 0, true);\n\t}\n\tif (!setsCached) {\n\t\tsendSearchSetsRequest(query);\n\t}\n}\n\nvoid EmojiListWidget::sendSearchSetsRequest(const QString &query) {\n\tconst auto hash = uint64(0);\n\t_searchSetsRequestId = _api.request(\n\t\tMTPmessages_SearchEmojiStickerSets(\n\t\t\tMTP_flags(0),\n\t\t\tMTP_string(query),\n\t\t\tMTP_long(hash))\n\t).done([=](const MTPmessages_FoundStickerSets &result) {\n\t\tsearchSetsResultsDone(query, result);\n\t}).fail([=] {\n\t\t_searchSetsRequestId = 0;\n\t\tif ((_searchRequestQuery == query) && !_searchCloudRequestId) {\n\t\t\ttoggleSearchLoading(false);\n\t\t}\n\t}).handleAllErrors().send();\n}\n\nvoid EmojiListWidget::requestSearchCloud(\n\t\tconst QString &query,\n\t\tint offset,\n\t\tbool fallbackToEmpty) {\n\tusing Flag = MTPmessages_SearchStickers::Flag;\n\tconst auto hash = uint64(0);\n\t_searchCloudRequestId = _api.request(MTPmessages_SearchStickers(\n\t\tMTP_flags(Flag::f_emojis),\n\t\tMTP_string(query),\n\t\tMTP_string(_searchEmoticon),\n\t\tMTP_vector<MTPstring>(SearchStickersLangCodes()),\n\t\tMTP_int(offset),\n\t\tMTP_int(kCloudSearchPageLimit),\n\t\tMTP_long(hash)\n\t)).done([=](const MTPmessages_FoundStickers &result) {\n\t\tsearchCloudResultsDone(query, offset, result);\n\t}).fail([=] {\n\t\t_searchCloudRequestId = 0;\n\t\tif (!fallbackToEmpty) {\n\t\t\treturn;\n\t\t}\n\t\t_searchCloudCache.emplace(query, std::vector<DocumentId>());\n\t\tif ((_searchRequestQuery == query) && !_searchSetsRequestId) {\n\t\t\ttoggleSearchLoading(false);\n\t\t\tshowSearchResults();\n\t\t}\n\t}).handleAllErrors().send();\n}\n\nvoid EmojiListWidget::cancelSearchRequest() {\n\ttoggleSearchLoading(false);\n\tif (const auto requestId = base::take(_searchCloudRequestId)) {\n\t\t_api.request(requestId).cancel();\n\t}\n\tif (const auto requestId = base::take(_searchSetsRequestId)) {\n\t\t_api.request(requestId).cancel();\n\t}\n\t_searchRequestTimer.cancel();\n\t_searchRequestQuery = QString();\n\t_searchNextRequestQuery = QString();\n\t_searchCloudCache.clear();\n\t_searchCloudNextOffset.clear();\n\t_searchSetsCache.clear();\n\t_searchSets.clear();\n}\n\nvoid EmojiListWidget::searchCloudResultsDone(\n\t\tconst QString &query,\n\t\tint requestedOffset,\n\t\tconst MTPmessages_FoundStickers &result) {\n\t_searchCloudRequestId = 0;\n\tconst auto active = (_searchRequestQuery == query);\n\n\tresult.match([&](const MTPDmessages_foundStickersNotModified &data) {\n\t\tLOG((\"API: messages.foundStickersNotModified.\"));\n\t\tauto it = _searchCloudCache.find(query);\n\t\tif (it == _searchCloudCache.cend()) {\n\t\t\tit = _searchCloudCache.emplace(\n\t\t\t\tquery,\n\t\t\t\tstd::vector<DocumentId>()).first;\n\t\t}\n\t\tif (const auto next = data.vnext_offset()) {\n\t\t\tif (next->v > requestedOffset) {\n\t\t\t\t_searchCloudNextOffset[query] = next->v;\n\t\t\t} else {\n\t\t\t\t_searchCloudNextOffset.erase(query);\n\t\t\t}\n\t\t} else {\n\t\t\t_searchCloudNextOffset.erase(query);\n\t\t}\n\t\tif (!active) {\n\t\t\treturn;\n\t\t}\n\t\tif (!_searchSetsRequestId) {\n\t\t\ttoggleSearchLoading(false);\n\t\t}\n\t\tshowSearchResults();\n\t\tcheckPaginateSearchCloud(getVisibleTop(), getVisibleBottom());\n\t}, [&](const MTPDmessages_foundStickers &data) {\n\t\tauto it = _searchCloudCache.find(query);\n\t\tif (it == _searchCloudCache.cend()) {\n\t\t\tit = _searchCloudCache.emplace(\n\t\t\t\tquery,\n\t\t\t\tstd::vector<DocumentId>()).first;\n\t\t}\n\n\t\tfor (const auto &sticker : data.vstickers().v) {\n\t\t\tif (const auto doc = session().data().processDocument(\n\t\t\t\t\tsticker)) {\n\t\t\t\tit->second.push_back(doc->id);\n\t\t\t}\n\t\t}\n\n\t\tif (const auto next = data.vnext_offset()) {\n\t\t\tif (next->v > requestedOffset) {\n\t\t\t\t_searchCloudNextOffset[query] = next->v;\n\t\t\t} else {\n\t\t\t\t_searchCloudNextOffset.erase(query);\n\t\t\t}\n\t\t} else {\n\t\t\t_searchCloudNextOffset.erase(query);\n\t\t}\n\n\t\tif (!active) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!_searchSetsRequestId) {\n\t\t\ttoggleSearchLoading(false);\n\t\t}\n\t\tshowSearchResults();\n\t\tcheckPaginateSearchCloud(\n\t\t\tgetVisibleTop(),\n\t\t\tgetVisibleBottom());\n\t});\n}\n\nvoid EmojiListWidget::loadMoreSearchCloud() {\n\tif (_searchCloudRequestId\n\t\t|| _searchRequestQuery.isEmpty()\n\t\t|| (_searchRequestQuery != _searchNextRequestQuery)) {\n\t\treturn;\n\t}\n\tconst auto query = _searchRequestQuery;\n\tconst auto offsetIt = _searchCloudNextOffset.find(query);\n\tif (offsetIt == _searchCloudNextOffset.end()) {\n\t\treturn;\n\t}\n\trequestSearchCloud(query, offsetIt->second, false);\n}\n\nvoid EmojiListWidget::checkPaginateSearchCloud(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tif (!_searchMode\n\t\t|| _searchRequestQuery.isEmpty()\n\t\t|| (_searchRequestQuery != _searchNextRequestQuery)\n\t\t|| _searchCloudRequestId) {\n\t\treturn;\n\t}\n\tconst auto visibleHeight = visibleBottom - visibleTop;\n\tif (visibleHeight <= 0) {\n\t\treturn;\n\t}\n\tif (visibleBottom > height() - visibleHeight * kPreloadSearchPages) {\n\t\tloadMoreSearchCloud();\n\t}\n}\n\nvoid EmojiListWidget::searchSetsResultsDone(\n\t\tconst QString &query,\n\t\tconst MTPmessages_FoundStickerSets &result) {\n\t_searchSetsRequestId = 0;\n\tif ((_searchRequestQuery == query) && !_searchCloudRequestId) {\n\t\ttoggleSearchLoading(false);\n\t}\n\n\tresult.match([&](const MTPDmessages_foundStickerSetsNotModified &) {\n\t\tLOG((\"API Error: \"\n\t\t\t\"messages.foundStickerSetsNotModified not expected.\"));\n\t}, [&](const MTPDmessages_foundStickerSets &data) {\n\t\tauto it = _searchSetsCache.find(query);\n\t\tif (it == _searchSetsCache.cend()) {\n\t\t\tit = _searchSetsCache.emplace(\n\t\t\t\tquery,\n\t\t\t\tstd::vector<uint64>()).first;\n\t\t}\n\t\tfor (const auto &setData : data.vsets().v) {\n\t\t\tconst auto set\n\t\t\t\t= session().data().stickers().feedSet(setData);\n\t\t\tif (set->stickers.empty() && set->covers.empty()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tit->second.push_back(set->id);\n\t\t}\n\t\tif (_searchRequestQuery == query) {\n\t\t\tshowSearchResults();\n\t\t}\n\t});\n}\n\nvoid EmojiListWidget::showSearchResults() {\n\tclearSelection();\n\n\t_searchResults.clear();\n\t_searchCustomIds.clear();\n\t_searchSets.clear();\n\t_searchEmoji.clear();\n\n\tauto plain = collectPlainSearchResults();\n\tif (_mode == Mode::Full) {\n\t\tfor (const auto emoji : plain) {\n\t\t\t_searchResults.push_back({\n\t\t\t\t.id = { emoji },\n\t\t\t});\n\t\t}\n\t}\n\tif (_mode != Mode::Full || session().premium()) {\n\t\tappendPremiumSearchResults();\n\t}\n\tfillCloudSearchResults();\n\tif (_mode == Mode::Full) {\n\t\tappendLocalPackSearchResults();\n\t}\n\tfillCloudSearchSets();\n\n\tresizeToWidth(width());\n\t_recentShownCount = _searchResults.size();\n\tupdate();\n\tupdateSelected();\n}\n\nvoid EmojiListWidget::fillCloudSearchResults() {\n\tconst auto it = _searchCloudCache.find(_searchRequestQuery);\n\tif (it == _searchCloudCache.cend() || it->second.empty()) {\n\t\treturn;\n\t}\n\tconst auto test = session().isTestMode();\n\tfor (const auto id : it->second) {\n\t\tif (!_searchCustomIds.emplace(id).second) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto document = session().data().document(id);\n\t\tconst auto sticker = document->sticker();\n\t\tif (!sticker) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto statusId = EmojiStatusId{ id };\n\t\t_searchResults.push_back({\n\t\t\t.custom = resolveCustomEmoji(\n\t\t\t\tstatusId,\n\t\t\t\tdocument,\n\t\t\t\tSearchEmojiSectionSetId()),\n\t\t\t.id = { RecentEmojiDocument{ .id = id, .test = test } },\n\t\t});\n\t}\n}\n\nvoid EmojiListWidget::fillCloudSearchSets() {\n\tconst auto it = _searchSetsCache.find(_searchRequestQuery);\n\tif (it == _searchSetsCache.cend() || it->second.empty()) {\n\t\treturn;\n\t}\n\tconst auto &sets = session().data().stickers().sets();\n\tfor (const auto setId : it->second) {\n\t\tconst auto setIt = sets.find(setId);\n\t\tif (setIt == sets.end()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto set = setIt->second.get();\n\t\tconst auto &list = set->stickers.empty()\n\t\t\t? set->covers\n\t\t\t: set->stickers;\n\t\tif (list.empty()) {\n\t\t\tcontinue;\n\t\t}\n\t\tauto customs = std::vector<CustomOne>();\n\t\tcustoms.reserve(list.size());\n\t\tfor (const auto document : list) {\n\t\t\tif (const auto sticker = document->sticker()) {\n\t\t\t\tconst auto statusId = EmojiStatusId{ document->id };\n\t\t\t\tcustoms.push_back({\n\t\t\t\t\t.custom = resolveCustomEmoji(\n\t\t\t\t\t\tstatusId,\n\t\t\t\t\t\tdocument,\n\t\t\t\t\t\tsetId),\n\t\t\t\t\t.document = document,\n\t\t\t\t\t.emoji = Ui::Emoji::Find(sticker->alt),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tif (customs.empty()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto installed = !!(set->flags\n\t\t\t& Data::StickersSetFlag::Installed);\n\t\t_searchSets.push_back({\n\t\t\t.id = setId,\n\t\t\t.set = set,\n\t\t\t.thumbnailDocument = set->lookupThumbnailDocument(),\n\t\t\t.title = set->title,\n\t\t\t.list = std::move(customs),\n\t\t\t.canRemove = installed,\n\t\t});\n\t}\n}\n\nEmojiListWidget::CustomSet &EmojiListWidget::searchSetBySection(\n\t\tint section) {\n\tExpects(section > 0 && section <= int(_searchSets.size()));\n\n\treturn _searchSets[section - 1];\n}\n\nconst EmojiListWidget::CustomSet &EmojiListWidget::searchSetBySection(\n\t\tint section) const {\n\tExpects(section > 0 && section <= int(_searchSets.size()));\n\n\treturn _searchSets[section - 1];\n}\n\nvoid EmojiListWidget::provideRecent(\n\t\tconst std::vector<EmojiStatusId> &customRecentList) {\n\tclearSelection();\n\tfillRecentFrom(customRecentList);\n\tresizeToWidth(width());\n}\n\nvoid EmojiListWidget::repaintCustom(uint64 setId) {\n\tif (!_repaintsScheduled.emplace(setId).second) {\n\t\treturn;\n\t}\n\tconst auto repaintSearch = (setId == SearchEmojiSectionSetId());\n\tif (_searchMode) {\n\t\tif (repaintSearch) {\n\t\t\tupdate();\n\t\t} else {\n\t\t\tenumerateSections([&](const SectionInfo &info) {\n\t\t\t\tif (info.section > 0\n\t\t\t\t\t&& searchSetBySection(info.section).id == setId) {\n\t\t\t\t\tupdate(\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tinfo.rowsTop,\n\t\t\t\t\t\twidth(),\n\t\t\t\t\t\tinfo.rowsBottom - info.rowsTop);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\t\treturn;\n\t}\n\tconst auto repaintRecent = (setId == RecentEmojiSectionSetId());\n\tenumerateSections([&](const SectionInfo &info) {\n\t\tconst auto repaint1 = repaintRecent\n\t\t\t&& (info.section == int(Section::Recent));\n\t\tconst auto repaint2 = !repaint1\n\t\t\t&& (info.section >= _staticCount)\n\t\t\t&& (setId == _custom[info.section - _staticCount].id);\n\t\tif (repaint1 || repaint2) {\n\t\t\tupdate(\n\t\t\t\t0,\n\t\t\t\tinfo.rowsTop,\n\t\t\t\twidth(),\n\t\t\t\tinfo.rowsBottom - info.rowsTop);\n\t\t}\n\t\treturn true;\n\t});\n}\n\nrpl::producer<EmojiChosen> EmojiListWidget::chosen() const {\n\treturn _chosen.events();\n}\n\nrpl::producer<FileChosen> EmojiListWidget::customChosen() const {\n\treturn _customChosen.events();\n}\n\nrpl::producer<> EmojiListWidget::jumpedToPremium() const {\n\treturn _jumpedToPremium.events();\n}\n\nrpl::producer<> EmojiListWidget::escapes() const {\n\treturn _search ? _search->escapes() : rpl::never<>();\n}\n\nvoid EmojiListWidget::prepareExpanding() {\n\tif (_search) {\n\t\t_searchExpandCache = _search->grab();\n\t}\n}\n\nvoid EmojiListWidget::paintExpanding(\n\t\tPainter &p,\n\t\tQRect clip,\n\t\tint finalBottom,\n\t\tfloat64 geometryProgress,\n\t\tfloat64 fullProgress,\n\t\tRectPart origin) {\n\tconst auto searchShift = _search\n\t\t? anim::interpolate(\n\t\t\tst().padding.top() - _search->height(),\n\t\t\t0,\n\t\t\tgeometryProgress)\n\t\t: 0;\n\tconst auto shift = clip.topLeft() + QPoint(0, searchShift);\n\tconst auto adjusted = clip.translated(-shift);\n\tconst auto finalHeight = (finalBottom - clip.y());\n\tif (!_searchExpandCache.isNull()) {\n\t\tp.setClipRect(clip);\n\t\tp.drawImage(\n\t\t\tclip.x() + st().searchMargin.left(),\n\t\t\tclip.y() + st().searchMargin.top() + searchShift,\n\t\t\t_searchExpandCache);\n\t}\n\tp.translate(shift);\n\tp.setClipRect(adjusted);\n\tpaint(p, ExpandingContext{\n\t\t.progress = fullProgress,\n\t\t.finalHeight = finalHeight,\n\t\t.expanding = true,\n\t}, adjusted);\n\tp.translate(-shift);\n}\n\nvoid EmojiListWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tInner::visibleTopBottomUpdated(visibleTop, visibleBottom);\n\tif (_footer) {\n\t\t_footer->validateSelectedIcon(\n\t\t\tcurrentSet(visibleTop),\n\t\t\tValidateIconAnimations::Full);\n\t}\n\tunloadNotSeenCustom(visibleTop, visibleBottom);\n\tcheckPaginateSearchCloud(visibleTop, visibleBottom);\n}\n\nvoid EmojiListWidget::unloadNotSeenCustom(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tenumerateSections([&](const SectionInfo &info) {\n\t\tif (info.rowsBottom <= visibleTop || info.rowsTop >= visibleBottom) {\n\t\t\tunloadCustomIn(info);\n\t\t}\n\t\treturn true;\n\t});\n}\n\nvoid EmojiListWidget::unloadAllCustom() {\n\tenumerateSections([&](const SectionInfo &info) {\n\t\tunloadCustomIn(info);\n\t\treturn true;\n\t});\n}\n\nvoid EmojiListWidget::unloadCustomIn(const SectionInfo &info) {\n\tif (!info.section && _recentPainted) {\n\t\t_recentPainted = false;\n\t\tfor (const auto &single : _recent) {\n\t\t\tif (const auto custom = single.custom) {\n\t\t\t\tcustom->unload();\n\t\t\t}\n\t\t}\n\t\treturn;\n\t} else if (_searchMode && info.section > 0) {\n\t\tauto &custom = searchSetBySection(info.section);\n\t\tif (!custom.painted) {\n\t\t\treturn;\n\t\t}\n\t\tcustom.painted = false;\n\t\tfor (const auto &single : custom.list) {\n\t\t\tsingle.custom->unload();\n\t\t}\n\t\treturn;\n\t} else if (info.section < _staticCount) {\n\t\treturn;\n\t}\n\tauto &custom = _custom[info.section - _staticCount];\n\tif (!custom.painted) {\n\t\treturn;\n\t}\n\tcustom.painted = false;\n\tfor (const auto &single : custom.list) {\n\t\tsingle.custom->unload();\n\t}\n}\n\nobject_ptr<TabbedSelector::InnerFooter> EmojiListWidget::createFooter() {\n\tExpects(_footer == nullptr);\n\n\tif (_mode == EmojiListMode::RecentReactions\n\t\t|| _mode == EmojiListMode::MessageEffects) {\n\t\treturn { nullptr };\n\t}\n\n\tusing FooterDescriptor = StickersListFooter::Descriptor;\n\tconst auto flag = powerSavingFlag();\n\tconst auto footerPaused = [method = pausedMethod(), flag]() {\n\t\treturn On(flag) || method();\n\t};\n\tauto result = object_ptr<StickersListFooter>(FooterDescriptor{\n\t\t.session = &session(),\n\t\t.customTextColor = _customTextColor,\n\t\t.paused = footerPaused,\n\t\t.parent = this,\n\t\t.st = &st(),\n\t\t.features = { .stickersSettings = false },\n\t\t.forceFirstFrame = (_mode == Mode::BackgroundEmoji),\n\t});\n\t_footer = result;\n\n\t_footer->setChosen(\n\t) | rpl::on_next([=](uint64 setId) {\n\t\tshowSet(setId);\n\t}, _footer->lifetime());\n\n\treturn result;\n}\n\nvoid EmojiListWidget::afterShown() {\n\tconst auto steal = (_mode == Mode::EmojiStatus)\n\t\t|| (_mode == Mode::FullReactions)\n\t\t|| (_mode == Mode::UserpicBuilder);\n\tif (_search && steal) {\n\t\t_search->stealFocus();\n\t}\n}\n\nvoid EmojiListWidget::beforeHiding() {\n\tif (_search) {\n\t\t_search->returnFocus();\n\t}\n}\n\ntemplate <typename Callback>\nbool EmojiListWidget::enumerateSections(Callback callback) const {\n\tExpects(_columnCount > 0);\n\n\tauto i = 0;\n\tauto info = SectionInfo();\n\tconst auto next = [&] {\n\t\tinfo.rowsCount = info.collapsed\n\t\t\t? kCollapsedRows\n\t\t\t: (info.count + _columnCount - 1) / _columnCount;\n\t\tinfo.rowsTop = info.top\n\t\t\t+ (i == 0 ? _rowsTop : st().header);\n\t\tinfo.rowsBottom = info.rowsTop\n\t\t\t+ (info.rowsCount * _singleSize.height());\n\t\tif (!callback(info)) {\n\t\t\treturn false;\n\t\t}\n\t\tinfo.top = info.rowsBottom;\n\t\treturn true;\n\t};\n\tif (_searchMode) {\n\t\tinfo.section = i;\n\t\tinfo.count = _searchResults.size();\n\t\tif (!next()) {\n\t\t\treturn false;\n\t\t}\n\t\t++i;\n\t\tfor (auto &section : _searchSets) {\n\t\t\tinfo.section = i++;\n\t\t\tinfo.premiumRequired = section.premiumRequired;\n\t\t\tinfo.count = int(section.list.size());\n\t\t\tinfo.collapsed = !section.expanded\n\t\t\t\t&& (!section.canRemove || section.premiumRequired)\n\t\t\t\t&& (info.count > _columnCount * kCollapsedRows);\n\t\t\tif (!next()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\tfor (; i != _staticCount; ++i) {\n\t\tinfo.section = i;\n\t\tinfo.count = i ? _counts[i] : _recent.size();\n\t\tif (!next()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\tfor (auto &section : _custom) {\n\t\tinfo.section = i++;\n\t\tinfo.premiumRequired = section.premiumRequired;\n\t\tinfo.count = int(section.list.size());\n\t\tinfo.collapsed = !section.expanded\n\t\t\t&& (!section.canRemove || section.premiumRequired)\n\t\t\t&& (info.count > _columnCount * kCollapsedRows);\n\t\tif (!next()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nEmojiListWidget::SectionInfo EmojiListWidget::sectionInfo(int section) const {\n\tExpects(section >= 0 && section < sectionsCount());\n\n\tauto result = SectionInfo();\n\tenumerateSections([&](const SectionInfo &info) {\n\t\tif (info.section == section) {\n\t\t\tresult = info;\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n\treturn result;\n}\n\nEmojiListWidget::SectionInfo EmojiListWidget::sectionInfoByOffset(\n\t\tint yOffset) const {\n\tauto result = SectionInfo();\n\tconst auto count = sectionsCount();\n\tenumerateSections([&result, count, yOffset](const SectionInfo &info) {\n\t\tif (yOffset < info.rowsBottom || info.section == count - 1) {\n\t\t\tresult = info;\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n\treturn result;\n}\n\nint EmojiListWidget::sectionsCount() const {\n\treturn _searchMode\n\t\t? (1 + int(_searchSets.size()))\n\t\t: (_staticCount + int(_custom.size()));\n}\n\nvoid EmojiListWidget::setSingleSize(QSize size) {\n\tconst auto area = st::emojiPanArea;\n\t_singleSize = size;\n\t_areaPosition = QPoint(\n\t\t(_singleSize.width() - area.width()) / 2,\n\t\t(_singleSize.height() - area.height()) / 2);\n\tconst auto esize = Ui::Emoji::GetSizeLarge() / style::DevicePixelRatio();\n\t_innerPosition = QPoint(\n\t\t(area.width() - esize) / 2,\n\t\t(area.height() - esize) / 2);\n\tconst auto customSkip = (esize - _customSingleSize) / 2;\n\t_customPosition = QPoint(customSkip, customSkip);\n\t_picker->setSingleSize(_singleSize);\n}\n\nvoid EmojiListWidget::setColorAllForceRippled(bool force) {\n\t_colorAllRippleForced = force;\n\tif (_colorAllRippleForced) {\n\t\t_colorAllRippleForcedLifetime = style::PaletteChanged(\n\t\t) | rpl::filter([=] {\n\t\t\treturn _colorAllRipple != nullptr;\n\t\t}) | rpl::on_next([=] {\n\t\t\t_colorAllRipple->forceRepaint();\n\t\t});\n\t\tif (!_colorAllRipple) {\n\t\t\t_colorAllRipple = createButtonRipple(int(Section::People));\n\t\t}\n\t\tif (_colorAllRipple->empty()) {\n\t\t\t_colorAllRipple->addFading();\n\t\t} else {\n\t\t\t_colorAllRipple->lastUnstop();\n\t\t}\n\t} else {\n\t\tif (_colorAllRipple) {\n\t\t\t_colorAllRipple->lastStop();\n\t\t}\n\t\t_colorAllRippleForcedLifetime.destroy();\n\t}\n}\n\nint EmojiListWidget::countDesiredHeight(int newWidth) {\n\tconst auto fullWidth = st().margin.left()\n\t\t+ newWidth\n\t\t+ st().margin.right();\n\tconst auto padding = st().padding;\n\tconst auto innerWidth = fullWidth - padding.left() - padding.right();\n\t_columnCount = std::max(innerWidth / st().desiredSize, 1);\n\tconst auto singleWidth = innerWidth / _columnCount;\n\t_rowsTop = _search ? _search->height() : padding.top();\n\t_rowsLeft = padding.left()\n\t\t+ (innerWidth - _columnCount * singleWidth) / 2\n\t\t- st().margin.left();\n\tsetSingleSize({ singleWidth, singleWidth - 2 * st().verticalSizeSub });\n\n\tconst auto countResult = [this](int minimalLastHeight) {\n\t\tconst auto info = sectionInfo(sectionsCount() - 1);\n\t\treturn info.top\n\t\t\t+ qMax(info.rowsBottom - info.top, minimalLastHeight);\n\t};\n\tconst auto minimalHeight = this->minimalHeight();\n\tconst auto minimalLastHeight = std::max(\n\t\tminimalHeight - padding.bottom(),\n\t\t0);\n\tconst auto result = countResult(minimalLastHeight);\n\treturn result\n\t\t? qMax(minimalHeight, result + padding.bottom())\n\t\t: 0;\n}\n\nint EmojiListWidget::defaultMinimalHeight() const {\n\treturn Inner::defaultMinimalHeight();\n}\n\nvoid EmojiListWidget::ensureLoaded(int section) {\n\tExpects(section >= 0 && section < sectionsCount());\n\n\tif (section == int(Section::Recent)) {\n\t\tif (_recent.empty()) {\n\t\t\tfillRecent();\n\t\t}\n\t\treturn;\n\t} else if (section >= _staticCount || !_emoji[section].empty()) {\n\t\treturn;\n\t}\n\t_emoji[section] = Ui::Emoji::GetSection(static_cast<Section>(section));\n\t_counts[section] = _emoji[section].size();\n\n\tconst auto &settings = Core::App().settings();\n\tfor (auto &emoji : _emoji[section]) {\n\t\temoji = settings.lookupEmojiVariant(emoji);\n\t}\n}\n\nvoid EmojiListWidget::fillRecent() {\n\tif (_mode != Mode::Full) {\n\t\treturn;\n\t}\n\t_recent.clear();\n\t_recentCustomIds.clear();\n\n\tconst auto &list = Core::App().settings().recentEmoji();\n\t_recent.reserve(std::min(int(list.size()), Core::kRecentEmojiLimit) + 1);\n\tconst auto test = session().isTestMode();\n\tfor (const auto &one : list) {\n\t\tconst auto document = std::get_if<RecentEmojiDocument>(&one.id.data);\n\t\tif (document && ((document->test != test) || _onlyUnicodeEmoji)) {\n\t\t\tcontinue;\n\t\t}\n\t\t_recent.push_back({\n\t\t\t.custom = resolveCustomRecent(one.id),\n\t\t\t.id = one.id,\n\t\t});\n\t\tif (document) {\n\t\t\t_recentCustomIds.emplace(document->id);\n\t\t}\n\t\tif (_recent.size() >= Core::kRecentEmojiLimit) {\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nvoid EmojiListWidget::fillRecentFrom(\n\t\tconst std::vector<EmojiStatusId> &list) {\n\tconst auto test = session().isTestMode();\n\t_recent.clear();\n\t_recent.reserve(list.size());\n\tfor (const auto &id : list) {\n\t\tif (!id && _mode == Mode::EmojiStatus) {\n\t\t\tconst auto star = QString::fromUtf8(\"\\xe2\\xad\\x90\\xef\\xb8\\x8f\");\n\t\t\t_recent.push_back({ .id = { Ui::Emoji::Find(star) } });\n\t\t} else if (!id\n\t\t\t&& (_mode == Mode::BackgroundEmoji\n\t\t\t\t|| _mode == Mode::ChannelStatus)) {\n\t\t\tconst auto fakeId = DocumentId(5246772116543512028ULL);\n\t\t\tconst auto no = QString::fromUtf8(\"\\xe2\\x9b\\x94\\xef\\xb8\\x8f\");\n\t\t\t_recent.push_back({\n\t\t\t\t.custom = resolveCustomRecent(fakeId),\n\t\t\t\t.id = { Ui::Emoji::Find(no) },\n\t\t\t});\n\t\t\t_recentCustomIds.emplace(fakeId);\n\t\t} else {\n\t\t\t_recent.push_back({\n\t\t\t\t.collectible = id.collectible,\n\t\t\t\t.custom = resolveCustomRecent(id),\n\t\t\t\t.id = {\n\t\t\t\t\tRecentEmojiDocument{ .id = id.documentId, .test = test },\n\t\t\t\t},\n\t\t\t});\n\t\t\t_recentCustomIds.emplace(id.collectible\n\t\t\t\t? id.collectible->documentId\n\t\t\t\t: id.documentId);\n\t\t}\n\t}\n}\n\nbase::unique_qptr<Ui::PopupMenu> EmojiListWidget::fillContextMenu(\n\t\tconst SendMenu::Details &details) {\n\tif (v::is_null(_selected)) {\n\t\treturn nullptr;\n\t}\n\tconst auto over = std::get_if<OverEmoji>(&_selected);\n\tif (!over) {\n\t\treturn nullptr;\n\t}\n\tconst auto section = over->section;\n\tconst auto index = over->index;\n\tauto menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\t(_mode == Mode::Full\n\t\t\t? st::popupMenuWithIcons\n\t\t\t: st::defaultPopupMenu));\n\tif (_mode == Mode::Full) {\n\t\tfillRecentMenu(menu, section, index);\n\t} else if (_mode == Mode::EmojiStatus || _mode == Mode::ChannelStatus) {\n\t\tfillEmojiStatusMenu(menu, section, index);\n\t}\n\tif (menu->empty()) {\n\t\treturn nullptr;\n\t}\n\treturn menu;\n}\n\nvoid EmojiListWidget::fillRecentMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tint section,\n\t\tint index) {\n\tconst auto recent = (section == int(Section::Recent));\n\tconst auto addAction = Ui::Menu::CreateAddActionCallback(menu);\n\tconst auto over = OverEmoji{ section, index };\n\tconst auto emoji = lookupOverEmoji(&over);\n\tconst auto custom = lookupCustomEmoji(&over);\n\tif (custom.collectible) {\n\t\treturn;\n\t}\n\tconst auto document = custom.document;\n\tif (document && document->sticker()) {\n\t\tconst auto sticker = document->sticker();\n\t\tconst auto emoji = sticker->alt;\n\t\tconst auto setId = sticker->set.id;\n\t\tif (!emoji.isEmpty()) {\n\t\t\tauto data = TextForMimeData{ emoji, { emoji } };\n\t\t\tdata.rich.entities.push_back({\n\t\t\t\tEntityType::CustomEmoji,\n\t\t\t\t0,\n\t\t\t\tint(emoji.size()),\n\t\t\t\tData::SerializeCustomEmojiId(document)\n\t\t\t});\n\t\t\taddAction(tr::lng_emoji_copy(tr::now), [=] {\n\t\t\t\tTextUtilities::SetClipboardText(data);\n\t\t\t}, &st::menuIconCopy);\n\t\t}\n\t\tif (recent && setId && _features.openStickerSets) {\n\t\t\taddAction(\n\t\t\t\ttr::lng_emoji_view_pack(tr::now),\n\t\t\t\tcrl::guard(this, [=] { displaySet(setId); }),\n\t\t\t\t&st::menuIconShowAll);\n\t\t}\n\t} else if (recent && emoji) {\n\t\taddAction(tr::lng_emoji_copy(tr::now), [=] {\n\t\t\tconst auto text = emoji->text();\n\t\t\tTextUtilities::SetClipboardText({ text, { text } });\n\t\t}, &st::menuIconCopy);\n\t}\n\tif (!recent) {\n\t\treturn;\n\t}\n\tauto id = RecentEmojiId{ emoji };\n\tif (custom) {\n\t\tid.data = RecentEmojiDocument{\n\t\t\t.id = custom.document->id,\n\t\t\t.test = custom.document->session().isTestMode(),\n\t\t};\n\t}\n\taddAction(tr::lng_emoji_remove_recent(tr::now), crl::guard(this, [=] {\n\t\tCore::App().settings().hideRecentEmoji(id);\n\t\trefreshRecent();\n\t}), &st::menuIconCancel);\n\n\tmenu->addSeparator(&st().expandedSeparator);\n\n\tconst auto resetRecent = [=] {\n\t\tconst auto sure = [=](Fn<void()> &&close) {\n\t\t\tCore::App().settings().resetRecentEmoji();\n\t\t\trefreshRecent();\n\t\t\tclose();\n\t\t};\n\t\tcheckHideWithBox(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_emoji_reset_recent_sure(),\n\t\t\t.confirmed = crl::guard(this, sure),\n\t\t\t.confirmText = tr::lng_emoji_reset_recent_button(tr::now),\n\t\t\t.labelStyle = &st().boxLabel,\n\t\t}));\n\t};\n\taddAction({\n\t\t.text = tr::lng_emoji_reset_recent(tr::now),\n\t\t.handler = crl::guard(this, resetRecent),\n\t\t.icon = &st::menuIconRestoreAttention,\n\t\t.isAttention = true,\n\t});\n}\n\nvoid EmojiListWidget::fillEmojiStatusMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tint section,\n\t\tint index) {\n\tconst auto chosen = lookupCustomEmoji(index, section);\n\tif (!chosen) {\n\t\treturn;\n\t}\n\tconst auto selectWith = [=](TimeId scheduled) {\n\t\tselectCustom(\n\t\t\tlookupChosen(chosen, nullptr, { .scheduled = scheduled }));\n\t};\n\tfor (const auto &value : { 3600, 3600 * 8, 3600 * 24, 3600 * 24 * 7 }) {\n\t\tconst auto text = tr::lng_emoji_status_menu_duration_any(\n\t\t\ttr::now,\n\t\t\tlt_duration,\n\t\t\tUi::FormatMuteFor(value));\n\t\tmenu->addAction(text, crl::guard(this, [=] {\n\t\t\tselectWith(base::unixtime::now() + value);\n\t\t}));\n\t}\n\tmenu->addAction(\n\t\ttr::lng_manage_messages_ttl_after_custom(tr::now),\n\t\tcrl::guard(this, [=] { selectWith(\n\t\t\tTabbedSelector::kPickCustomTimeId); }));\n}\n\nvoid EmojiListWidget::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\n\tconst auto clip = e ? e->rect() : rect();\n\n\t_repaintsScheduled.clear();\n\tif (_grabbingChosen) {\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tp.fillRect(clip, Qt::transparent);\n\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t} else if (st().bg->c.alpha() > 0) {\n\t\tp.fillRect(clip, st().bg);\n\t}\n\tif (!_searchExpandCache.isNull()) {\n\t\t_searchExpandCache = QImage();\n\t}\n\n\tpaint(p, {}, clip);\n}\n\nvoid EmojiListWidget::validateEmojiPaintContext(\n\t\tconst ExpandingContext &context) {\n\tauto value = Ui::Text::CustomEmojiPaintContext{\n\t\t.textColor = (_customTextColor\n\t\t\t? _customTextColor()\n\t\t\t: (_mode == Mode::EmojiStatus || _mode == Mode::ChannelStatus)\n\t\t\t? anim::color(\n\t\t\t\tst::stickerPanPremium1,\n\t\t\t\tst::stickerPanPremium2,\n\t\t\t\t0.5)\n\t\t\t: st().textFg->c),\n\t\t.size = QSize(_customSingleSize, _customSingleSize),\n\t\t.now = crl::now(),\n\t\t.scale = context.progress,\n\t\t.paused = On(powerSavingFlag()) || paused(),\n\t\t.scaled = context.expanding,\n\t\t.internal = { .forceFirstFrame = (_mode == Mode::BackgroundEmoji) },\n\t};\n\tif (!_emojiPaintContext) {\n\t\t_emojiPaintContext = std::make_unique<\n\t\t\tUi::Text::CustomEmojiPaintContext\n\t\t>(std::move(value));\n\t} else {\n\t\t*_emojiPaintContext = std::move(value);\n\t}\n}\n\nvoid EmojiListWidget::paint(\n\t\tPainter &p,\n\t\tExpandingContext context,\n\t\tQRect clip) {\n\tvalidateEmojiPaintContext(context);\n\n\t_paintAsPremium = session().premium();\n\n\tauto fromColumn = floorclamp(\n\t\tclip.x() - _rowsLeft,\n\t\t_singleSize.width(),\n\t\t0,\n\t\t_columnCount);\n\tauto toColumn = ceilclamp(\n\t\tclip.x() + clip.width() - _rowsLeft,\n\t\t_singleSize.width(),\n\t\t0,\n\t\t_columnCount);\n\tif (rtl()) {\n\t\tqSwap(fromColumn, toColumn);\n\t\tfromColumn = _columnCount - fromColumn;\n\t\ttoColumn = _columnCount - toColumn;\n\t}\n\tconst auto expandProgress = context.progress;\n\tauto selectedButton = std::get_if<OverButton>(!v::is_null(_pressed)\n\t\t? &_pressed\n\t\t: &_selected);\n\tif (_searchResults.empty()\n\t\t&& _searchSets.empty()\n\t\t&& _searchMode\n\t\t&& !_searchLoading\n\t\t&& !_searchRequestTimer.isActive()) {\n\t\tpaintEmptySearchResults(p);\n\t}\n\tenumerateSections([&](const SectionInfo &info) {\n\t\tif (clip.top() >= info.rowsBottom) {\n\t\t\treturn true;\n\t\t} else if (clip.top() + clip.height() <= info.top) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto buttonSelected = selectedButton\n\t\t\t? (selectedButton->section == info.section)\n\t\t\t: false;\n\t\tconst auto titleLeft = (info.premiumRequired\n\t\t\t? st().headerLockedLeft\n\t\t\t: st().headerLeft) - st().margin.left();\n\t\tconst auto widthForTitle = emojiRight()\n\t\t\t- titleLeft\n\t\t\t- paintButtonGetWidth(p, info, buttonSelected, clip);\n\t\tif (info.section > 0 && clip.top() < info.rowsTop) {\n\t\t\tp.setFont(st::emojiPanHeaderFont);\n\t\t\tp.setPen(st().headerFg);\n\t\t\tauto titleText = (_searchMode && info.section > 0)\n\t\t\t\t? searchSetBySection(info.section).title\n\t\t\t\t: (info.section < _staticCount)\n\t\t\t\t? ChatHelpers::EmojiCategoryTitle(info.section)(tr::now)\n\t\t\t\t: _custom[info.section - _staticCount].title;\n\t\t\tauto titleWidth = st::emojiPanHeaderFont->width(titleText);\n\t\t\tif (titleWidth > widthForTitle) {\n\t\t\t\ttitleText = st::emojiPanHeaderFont->elided(titleText, widthForTitle);\n\t\t\t\ttitleWidth = st::emojiPanHeaderFont->width(titleText);\n\t\t\t}\n\t\t\tconst auto top = info.top + st().headerTop;\n\t\t\tif (info.premiumRequired) {\n\t\t\t\tst::emojiPremiumRequired.paint(\n\t\t\t\t\tp,\n\t\t\t\t\tst().headerLockLeft - st().margin.left(),\n\t\t\t\t\ttop,\n\t\t\t\t\twidth());\n\t\t\t}\n\t\t\tconst auto textBaseline = top + st::emojiPanHeaderFont->ascent;\n\t\t\tp.setFont(st::emojiPanHeaderFont);\n\t\t\tp.setPen(st().headerFg);\n\t\t\tp.drawText(titleLeft, textBaseline, titleText);\n\t\t}\n\t\tif (clip.top() + clip.height() > info.rowsTop) {\n\t\t\tensureLoaded(info.section);\n\t\t\tauto fromRow = floorclamp(\n\t\t\t\tclip.y() - info.rowsTop,\n\t\t\t\t_singleSize.height(),\n\t\t\t\t0,\n\t\t\t\tinfo.rowsCount);\n\t\t\tauto toRow = ceilclamp(\n\t\t\t\tclip.y() + clip.height() - info.rowsTop,\n\t\t\t\t_singleSize.height(),\n\t\t\t\t0,\n\t\t\t\tinfo.rowsCount);\n\t\t\tfor (auto i = fromRow; i < toRow; ++i) {\n\t\t\t\tfor (auto j = fromColumn; j < toColumn; ++j) {\n\t\t\t\t\tconst auto index = i * _columnCount + j;\n\t\t\t\t\tif (index >= info.count) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst auto state = OverEmoji{\n\t\t\t\t\t\t.section = info.section,\n\t\t\t\t\t\t.index = index,\n\t\t\t\t\t};\n\t\t\t\t\tconst auto selected = (state == _selected)\n\t\t\t\t\t\t|| (!_picker->isHidden()\n\t\t\t\t\t\t\t&& state == _pickerSelected);\n\t\t\t\t\tconst auto position = QPoint(\n\t\t\t\t\t\t_rowsLeft + j * _singleSize.width(),\n\t\t\t\t\t\tinfo.rowsTop + i * _singleSize.height()\n\t\t\t\t\t);\n\t\t\t\t\tconst auto w = position + _areaPosition;\n\t\t\t\t\tif (context.expanding) {\n\t\t\t\t\t\tconst auto y = (position.y() - _rowsTop);\n\t\t\t\t\t\tconst auto x = (position.x() - _rowsLeft);\n\t\t\t\t\t\tconst auto sum = y\n\t\t\t\t\t\t\t+ std::max(std::min(y, width()) - x, 0);\n\t\t\t\t\t\tconst auto maxSum = context.finalHeight\n\t\t\t\t\t\t\t+ std::min(context.finalHeight, width());\n\t\t\t\t\t\tconst auto started = (sum / float64(maxSum))\n\t\t\t\t\t\t\t- kAppearDuration;\n\t\t\t\t\t\tcontext.progress = (expandProgress <= started)\n\t\t\t\t\t\t\t? 0.\n\t\t\t\t\t\t\t: (expandProgress >= started + kAppearDuration)\n\t\t\t\t\t\t\t? 1.\n\t\t\t\t\t\t\t: ((expandProgress - started) / kAppearDuration);\n\t\t\t\t\t}\n\t\t\t\t\tif (info.collapsed\n\t\t\t\t\t\t&& index + 1 == _columnCount * kCollapsedRows) {\n\t\t\t\t\t\tdrawCollapsedBadge(p, w - _areaPosition, info.count);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (!_grabbingChosen\n\t\t\t\t\t\t&& selected\n\t\t\t\t\t\t&& st().overBg->c.alpha() > 0) {\n\t\t\t\t\t\tauto tl = w;\n\t\t\t\t\t\tif (rtl()) {\n\t\t\t\t\t\t\ttl.setX(width() - tl.x() - st::emojiPanArea.width());\n\t\t\t\t\t\t}\n\t\t\t\t\t\t_overBg.paint(p, QRect(tl, st::emojiPanArea));\n\t\t\t\t\t}\n\t\t\t\t\tif (_searchMode && info.section == 0) {\n\t\t\t\t\t\tdrawRecent(p, context, w, _searchResults[index]);\n\t\t\t\t\t} else if (_searchMode && info.section > 0) {\n\t\t\t\t\t\tdrawSearchSetCustom(\n\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t\tw,\n\t\t\t\t\t\t\tinfo.section,\n\t\t\t\t\t\t\tindex);\n\t\t\t\t\t} else if (info.section == int(Section::Recent)) {\n\t\t\t\t\t\tdrawRecent(p, context, w, _recent[index]);\n\t\t\t\t\t} else if (info.section < _staticCount) {\n\t\t\t\t\t\tdrawEmoji(p, context, w, _emoji[info.section][index]);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst auto set = info.section - _staticCount;\n\t\t\t\t\t\tdrawCustom(p, context, w, set, index);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t});\n}\n\nvoid EmojiListWidget::drawCollapsedBadge(\n\t\tQPainter &p,\n\t\tQPoint position,\n\t\tint count) {\n\tconst auto &st = st::emojiPanExpand;\n\tconst auto text = u\"+%1\"_q.arg(count - _columnCount * kCollapsedRows + 1);\n\tconst auto textWidth = st.style.font->width(text);\n\tconst auto buttonw = std::max(textWidth - st.width, st.height);\n\tconst auto buttonh = st.height;\n\tconst auto buttonx = position.x() + (_singleSize.width() - buttonw) / 2;\n\tconst auto buttony = position.y() + (_singleSize.height() - buttonh) / 2;\n\t_collapsedBg.paint(p, QRect(buttonx, buttony, buttonw, buttonh));\n\tp.setPen(this->st().bg);\n\tp.setFont(st.style.font);\n\tp.drawText(\n\t\tbuttonx + (buttonw - textWidth) / 2,\n\t\t(buttony + st.textTop + st.style.font->ascent),\n\t\ttext);\n}\n\nvoid EmojiListWidget::drawRecent(\n\t\tQPainter &p,\n\t\tconst ExpandingContext &context,\n\t\tQPoint position,\n\t\tconst RecentOne &recent) {\n\t_recentPainted = true;\n\tconst auto locked = (_mode == Mode::MessageEffects)\n\t\t&& !_paintAsPremium\n\t\t&& v::is<RecentEmojiDocument>(recent.id.data)\n\t\t&& !_freeEffects.contains(\n\t\t\tv::get<RecentEmojiDocument>(recent.id.data).id);\n\tauto lockedPainted = false;\n\tif (locked) {\n\t\tif (_premiumMarkFrameCache.isNull()) {\n\t\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t\t_premiumMarkFrameCache = QImage(\n\t\t\t\tQSize(_customSingleSize, _customSingleSize) * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_premiumMarkFrameCache.setDevicePixelRatio(ratio);\n\t\t}\n\t\t_premiumMarkFrameCache.fill(Qt::transparent);\n\t}\n\tif (const auto custom = recent.custom) {\n\t\tconst auto exactPosition = position\n\t\t\t+ _innerPosition\n\t\t\t+ _customPosition;\n\t\t_emojiPaintContext->scale = context.progress;\n\t\tif (_mode == Mode::ChannelStatus) {\n\t\t\t_emojiPaintContext->internal.forceFirstFrame\n\t\t\t\t= (recent.id == _recent.front().id);\n\t\t}\n\t\tif (locked) {\n\t\t\tlockedPainted = custom->ready();\n\n\t\t\tauto q = Painter(&_premiumMarkFrameCache);\n\t\t\t_emojiPaintContext->position = QPoint();\n\t\t\tcustom->paint(q, *_emojiPaintContext);\n\t\t\tq.end();\n\n\t\t\tp.drawImage(exactPosition, _premiumMarkFrameCache);\n\t\t} else {\n\t\t\t_emojiPaintContext->position = exactPosition;\n\t\t\tcustom->paint(p, *_emojiPaintContext);\n\t\t}\n\t} else if (const auto emoji = std::get_if<EmojiPtr>(&recent.id.data)) {\n\t\tif (_mode == Mode::EmojiStatus) {\n\t\t\tposition += QPoint(\n\t\t\t\t(_singleSize.width() - st::emojiStatusDefault.width()) / 2,\n\t\t\t\t(_singleSize.height() - st::emojiStatusDefault.height()) / 2\n\t\t\t) - _areaPosition;\n\t\t\tp.drawImage(position, _premiumIcon->image());\n\t\t} else {\n\t\t\tdrawEmoji(p, context, position, *emoji);\n\t\t}\n\t} else {\n\t\tUnexpected(\"Empty custom emoji in EmojiListWidget::drawRecent.\");\n\t}\n\n\tif (locked) {\n\t\t_premiumMark->paint(\n\t\t\tp,\n\t\t\tlockedPainted ? _premiumMarkFrameCache : QImage(),\n\t\t\trecent.premiumLock,\n\t\t\tposition,\n\t\t\t_singleSize,\n\t\t\twidth());\n\t}\n}\n\nvoid EmojiListWidget::drawEmoji(\n\t\tQPainter &p,\n\t\tconst ExpandingContext &context,\n\t\tQPoint position,\n\t\tEmojiPtr emoji) {\n\tposition += _innerPosition;\n\tUi::Emoji::Draw(\n\t\tp,\n\t\temoji,\n\t\tUi::Emoji::GetSizeLarge(),\n\t\tposition.x(),\n\t\tposition.y());\n}\n\nvoid EmojiListWidget::drawCustom(\n\t\tQPainter &p,\n\t\tconst ExpandingContext &context,\n\t\tQPoint position,\n\t\tint set,\n\t\tint index) {\n\tauto &custom = _custom[set];\n\tcustom.painted = true;\n\tauto &entry = custom.list[index];\n\t_emojiPaintContext->scale = context.progress;\n\t_emojiPaintContext->position = position\n\t\t+ _innerPosition\n\t\t+ _customPosition;\n\tentry.custom->paint(p, *_emojiPaintContext);\n}\n\nvoid EmojiListWidget::drawSearchSetCustom(\n\t\tQPainter &p,\n\t\tconst ExpandingContext &context,\n\t\tQPoint position,\n\t\tint section,\n\t\tint index) {\n\tauto &custom = searchSetBySection(section);\n\tcustom.painted = true;\n\tauto &entry = custom.list[index];\n\t_emojiPaintContext->scale = context.progress;\n\t_emojiPaintContext->position = position\n\t\t+ _innerPosition\n\t\t+ _customPosition;\n\tentry.custom->paint(p, *_emojiPaintContext);\n}\n\nbool EmojiListWidget::checkPickerHide() {\n\tif (!_picker->isHidden() && !v::is_null(_pickerSelected)) {\n\t\t_picker->hideAnimated();\n\t\t_pickerSelected = v::null;\n\t\tupdateSelected();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nEmojiListWidget::ResolvedCustom EmojiListWidget::lookupCustomEmoji(\n\t\tconst OverEmoji *over) const {\n\treturn over\n\t\t? lookupCustomEmoji(over->index, over->section)\n\t\t: ResolvedCustom();\n}\n\nEmojiListWidget::ResolvedCustom EmojiListWidget::lookupCustomEmoji(\n\t\tint index,\n\t\tint section) const {\n\tif (_searchMode && section == 0) {\n\t\tif (index < _searchResults.size()) {\n\t\t\tconst auto document = std::get_if<RecentEmojiDocument>(\n\t\t\t\t&_searchResults[index].id.data);\n\t\t\tif (document) {\n\t\t\t\treturn { session().data().document(document->id) };\n\t\t\t}\n\t\t}\n\t\treturn {};\n\t} else if (_searchMode && section > 0) {\n\t\tconst auto &set = searchSetBySection(section);\n\t\tif (index < int(set.list.size())) {\n\t\t\tauto &entry = set.list[index];\n\t\t\treturn { entry.document, entry.collectible };\n\t\t}\n\t\treturn {};\n\t} else if (section == int(Section::Recent) && index < _recent.size()) {\n\t\tconst auto &recent = _recent[index];\n\t\tif (recent.collectible) {\n\t\t\treturn {\n\t\t\t\tsession().data().document(recent.collectible->documentId),\n\t\t\t\trecent.collectible,\n\t\t\t};\n\t\t}\n\t\tconst auto document = std::get_if<RecentEmojiDocument>(\n\t\t\t&recent.id.data);\n\t\tif (document) {\n\t\t\treturn { session().data().document(document->id) };\n\t\t}\n\t} else if (section >= _staticCount\n\t\t&& index < _custom[section - _staticCount].list.size()) {\n\t\tauto &set = _custom[section - _staticCount];\n\t\tauto &entry = set.list[index];\n\t\treturn { entry.document, entry.collectible };\n\t}\n\treturn {};\n}\n\nEmojiPtr EmojiListWidget::lookupOverEmoji(const OverEmoji *over) const {\n\tconst auto section = over ? over->section : -1;\n\tconst auto index = over ? over->index : -1;\n\treturn (_searchMode && section == 0)\n\t\t? ((index < _searchResults.size()\n\t\t\t&& v::is<EmojiPtr>(_searchResults[index].id.data))\n\t\t\t? v::get<EmojiPtr>(_searchResults[index].id.data)\n\t\t\t: nullptr)\n\t\t: (_searchMode && section > 0)\n\t\t? ((index < int(searchSetBySection(section).list.size()))\n\t\t\t? searchSetBySection(section).list[index].emoji\n\t\t\t: nullptr)\n\t\t: (section == int(Section::Recent)\n\t\t\t&& index < _recent.size()\n\t\t\t&& v::is<EmojiPtr>(_recent[index].id.data))\n\t\t? v::get<EmojiPtr>(_recent[index].id.data)\n\t\t: (section > int(Section::Recent)\n\t\t\t&& section < _staticCount\n\t\t\t&& index < _emoji[section].size())\n\t\t? _emoji[section][index]\n\t\t: nullptr;\n}\n\nEmojiChosen EmojiListWidget::lookupChosen(\n\t\tEmojiPtr emoji,\n\t\tnot_null<const OverEmoji*> over) {\n\tconst auto rect = emojiRect(over->section, over->index);\n\tconst auto size = st::emojiStatusDefault.size();\n\tconst auto icon = QRect(\n\t\trect.x() + (_singleSize.width() - size.width()) / 2,\n\t\trect.y() + (_singleSize.height() - size.height()) / 2,\n\t\trect.width(),\n\t\trect.height());\n\treturn {\n\t\t.emoji = emoji,\n\t\t.messageSendingFrom = {\n\t\t\t.type = Ui::MessageSendingAnimationFrom::Type::Emoji,\n\t\t\t.globalStartGeometry = mapToGlobal(icon),\n\t\t},\n\t};\n}\n\nFileChosen EmojiListWidget::lookupChosen(\n\t\tResolvedCustom custom,\n\t\tconst OverEmoji *over,\n\t\tApi::SendOptions options) {\n\tExpects(custom.document != nullptr);\n\n\t_grabbingChosen = true;\n\tconst auto guard = gsl::finally([&] { _grabbingChosen = false; });\n\tconst auto rect = over ? emojiRect(over->section, over->index) : QRect();\n\tconst auto emoji = over ? QRect(\n\t\trect.topLeft() + _areaPosition + _innerPosition + _customPosition,\n\t\tQSize(_customSingleSize, _customSingleSize)\n\t) : QRect();\n\n\treturn {\n\t\t.document = custom.document,\n\t\t.options = options,\n\t\t.messageSendingFrom = {\n\t\t\t.type = Ui::MessageSendingAnimationFrom::Type::Emoji,\n\t\t\t.globalStartGeometry = over ? mapToGlobal(emoji) : QRect(),\n\t\t\t.frame = over ? Ui::GrabWidgetToImage(this, emoji) : QImage(),\n\t\t},\n\t\t.collectible = custom.collectible,\n\t};\n}\n\nvoid EmojiListWidget::mousePressEvent(QMouseEvent *e) {\n\t_lastMousePos = e->globalPos();\n\tupdateSelected();\n\tif (checkPickerHide() || e->button() != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tsetPressed(_selected);\n\tif (const auto over = std::get_if<OverEmoji>(&_selected)) {\n\t\tconst auto emoji = lookupOverEmoji(over);\n\t\tif (emoji && emoji->hasVariants()) {\n\t\t\t_pickerSelected = _selected;\n\t\t\tsetCursor(style::cur_default);\n\t\t\tif (!Core::App().settings().hasChosenEmojiVariant(emoji)) {\n\t\t\t\tshowPicker();\n\t\t\t} else {\n\t\t\t\t_previewTimer.cancel();\n\t\t\t\t_showPickerTimer.callOnce(kColorPickerDelay);\n\t\t\t}\n\t\t} else if (lookupCustomEmoji(over)) {\n\t\t\t_showPickerTimer.cancel();\n\t\t\t_previewTimer.callOnce(QApplication::startDragTime());\n\t\t}\n\t}\n}\n\nvoid EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) {\n\t_previewTimer.cancel();\n\n\tauto pressed = _pressed;\n\tsetPressed(v::null);\n\t_lastMousePos = e->globalPos();\n\tif (!_picker->isHidden()) {\n\t\tif (_picker->rect().contains(_picker->mapFromGlobal(_lastMousePos))) {\n\t\t\treturn _picker->handleMouseRelease(QCursor::pos());\n\t\t} else if (const auto over = std::get_if<OverEmoji>(&_pickerSelected)) {\n\t\t\tconst auto emoji = lookupOverEmoji(over);\n\t\t\tif (emoji\n\t\t\t\t&& emoji->hasVariants()\n\t\t\t\t&& Core::App().settings().hasChosenEmojiVariant(emoji)) {\n\t\t\t\t_picker->hideAnimated();\n\t\t\t\t_pickerSelected = v::null;\n\t\t\t}\n\t\t}\n\t}\n\tupdateSelected();\n\n\tif (_showPickerTimer.isActive()) {\n\t\t_showPickerTimer.cancel();\n\t\t_pickerSelected = v::null;\n\t\t_picker->hide();\n\t}\n\n\tif (_previewShown) {\n\t\tif (_mediaPreview) {\n\t\t\t_mediaPreview->hidePreview();\n\t\t}\n\t\t_previewShown = false;\n\t\treturn;\n\t} else if (v::is_null(_selected) || _selected != pressed) {\n\t\treturn;\n\t}\n\n\tif (const auto over = std::get_if<OverEmoji>(&_selected)) {\n\t\tconst auto section = over->section;\n\t\tconst auto index = over->index;\n\t\tif (sectionInfo(section).collapsed\n\t\t\t&& index + 1 == _columnCount * kCollapsedRows) {\n\t\t\tif (_searchMode && section > 0) {\n\t\t\t\tsearchSetBySection(section).expanded = true;\n\t\t\t} else if (section >= _staticCount) {\n\t\t\t\t_custom[section - _staticCount].expanded = true;\n\t\t\t}\n\t\t\tresizeToWidth(width());\n\t\t\tupdate();\n\t\t\treturn;\n\t\t} else if (const auto emoji = lookupOverEmoji(over)) {\n\t\t\tif (emoji->hasVariants() && !_picker->isHidden()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tselectEmoji(lookupChosen(emoji, over));\n\t\t} else if (const auto custom = lookupCustomEmoji(over)) {\n\t\t\tselectCustom(lookupChosen(custom, over));\n\t\t}\n\t} else if (const auto set = std::get_if<OverSet>(&pressed)) {\n\t\tconst auto setId = (_searchMode && set->section > 0)\n\t\t\t? searchSetBySection(set->section).id\n\t\t\t: (set->section >= _staticCount)\n\t\t\t? _custom[set->section - _staticCount].id\n\t\t\t: uint64(0);\n\t\tif (setId) {\n\t\t\tdisplaySet(setId);\n\t\t}\n\t} else if (auto button = std::get_if<OverButton>(&pressed)) {\n\t\tAssert(hasButton(button->section));\n\t\tconst auto id = hasColorButton(button->section)\n\t\t\t? 0\n\t\t\t: (_searchMode && button->section > 0)\n\t\t\t? searchSetBySection(button->section).id\n\t\t\t: _custom[button->section - _staticCount].id;\n\t\tif (hasColorButton(button->section)) {\n\t\t\t_pickerSelected = pressed;\n\t\t\tshowPicker();\n\t\t} else if (hasRemoveButton(button->section)) {\n\t\t\tremoveSet(id);\n\t\t} else if (hasAddButton(button->section)) {\n\t\t\t_localSetsManager->install(id);\n\t\t} else if (const auto resolved = _show->resolveWindow()) {\n\t\t\t_jumpedToPremium.fire({});\n\t\t\tswitch (_mode) {\n\t\t\tcase Mode::Full:\n\t\t\tcase Mode::UserpicBuilder:\n\t\t\t\tSettings::ShowPremium(resolved, u\"animated_emoji\"_q);\n\t\t\t\tbreak;\n\t\t\tcase Mode::FullReactions:\n\t\t\tcase Mode::RecentReactions:\n\t\t\t\tSettings::ShowPremium(resolved, u\"infinite_reactions\"_q);\n\t\t\t\tbreak;\n\t\t\tcase Mode::EmojiStatus:\n\t\t\tcase Mode::ChannelStatus:\n\t\t\t\tSettings::ShowPremium(resolved, u\"emoji_status\"_q);\n\t\t\t\tbreak;\n\t\t\tcase Mode::TopicIcon:\n\t\t\t\tSettings::ShowPremium(resolved, u\"forum_topic_icon\"_q);\n\t\t\t\tbreak;\n\t\t\tcase Mode::BackgroundEmoji:\n\t\t\t\tSettings::ShowPremium(resolved, u\"name_color\"_q);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid EmojiListWidget::displaySet(uint64 setId) {\n\tif (setId == Data::Stickers::MegagroupSetId) {\n\t\tif (_megagroupSet->mgInfo->emojiSet.id) {\n\t\t\tsetId = _megagroupSet->mgInfo->emojiSet.id;\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t} else if (setId == Data::Stickers::CollectibleSetId) {\n\t\treturn;\n\t}\n\tconst auto &sets = session().data().stickers().sets();\n\tauto it = sets.find(setId);\n\tif (it != sets.cend()) {\n\t\tcheckHideWithBox(Box<StickerSetBox>(_show, it->second.get()));\n\t}\n}\n\nvoid EmojiListWidget::removeMegagroupSet(bool locally) {\n\tif (locally) {\n\t\tsession().settings().setGroupEmojiSectionHidden(_megagroupSet->id);\n\t\tsession().saveSettings();\n\t\trefreshCustom();\n\t\treturn;\n\t}\n\tcheckHideWithBox(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_emoji_remove_group_set(),\n\t\t.confirmed = crl::guard(this, [this, group = _megagroupSet](\n\t\t\t\tFn<void()> &&close) {\n\t\t\tExpects(group->mgInfo != nullptr);\n\n\t\t\tif (group->mgInfo->emojiSet) {\n\t\t\t\tsession().api().setGroupEmojiSet(group, {});\n\t\t\t}\n\t\t\tclose();\n\t\t}),\n\t\t.cancelled = [](Fn<void()> &&close) { close(); },\n\t\t.labelStyle = &st().boxLabel,\n\t}));\n}\n\nvoid EmojiListWidget::removeSet(uint64 setId) {\n\tconst auto &labelSt = st().boxLabel;\n\tif (setId == Data::Stickers::MegagroupSetId) {\n\t\tconst auto i = ranges::find(_custom, setId, &CustomSet::id);\n\t\tAssert(i != end(_custom));\n\t\tconst auto removeLocally = !_megagroupSet->canEditEmoji();\n\t\tremoveMegagroupSet(removeLocally);\n\t} else if (setId == Data::Stickers::CollectibleSetId) {\n\t} else if (auto box = MakeConfirmRemoveSetBox(&session(), labelSt, setId)) {\n\t\tcheckHideWithBox(std::move(box));\n\t}\n}\n\nvoid EmojiListWidget::selectEmoji(EmojiChosen data) {\n\tCore::App().settings().incrementRecentEmoji({ data.emoji });\n\t_chosen.fire(std::move(data));\n}\n\nvoid EmojiListWidget::selectCustom(FileChosen data) {\n\tconst auto document = data.document;\n\tconst auto skip = (document->isPremiumEmoji() && !session().premium());\n\tif (!skip && _mode == Mode::Full) {\n\t\tauto &settings = Core::App().settings();\n\t\tsettings.incrementRecentEmoji({ RecentEmojiDocument{\n\t\t\tdocument->id,\n\t\t\tdocument->session().isTestMode(),\n\t\t} });\n\t}\n\t_customChosen.fire(std::move(data));\n}\n\nvoid EmojiListWidget::showPicker() {\n\tif (v::is_null(_pickerSelected)) {\n\t\treturn;\n\t}\n\tconst auto showAt = [&](float64 xCoef, int y, int height) {\n\t\ty -= _picker->height() - st::emojiPanRadius + getVisibleTop();\n\t\tif (y < st().header) {\n\t\t\ty += _picker->height() + height;\n\t\t}\n\t\tauto xmax = width() - _picker->width();\n\t\tif (rtl()) xCoef = 1. - xCoef;\n\t\t_picker->move(qRound(xmax * xCoef), y);\n\n\t\tdisableScroll(true);\n\t};\n\tif (const auto button = std::get_if<OverButton>(&_pickerSelected)) {\n\t\tconst auto hand = QString::fromUtf8(\"\\xF0\\x9F\\x91\\x8B\");\n\t\tconst auto emoji = Ui::Emoji::Find(hand);\n\t\tAssert(emoji != nullptr && emoji->hasVariants());\n\t\t_picker->showEmoji(emoji, true);\n\t\tsetColorAllForceRippled(true);\n\t\tconst auto rect = buttonRect(button->section);\n\t\tshowAt(1., rect.y(), rect.height() - 2 * st::emojiPanRadius);\n\t} else if (const auto over = std::get_if<OverEmoji>(&_pickerSelected)) {\n\t\tconst auto emoji = lookupOverEmoji(over);\n\t\tif (emoji && emoji->hasVariants()) {\n\t\t\t_picker->showEmoji(emoji);\n\n\t\t\tconst auto coef = float64(over->index % _columnCount)\n\t\t\t\t/ float64(_columnCount - 1);\n\t\t\tconst auto h = _singleSize.height() - 2 * st::emojiPanRadius;\n\t\t\tshowAt(coef, emojiRect(over->section, over->index).y(), h);\n\t\t}\n\t}\n}\n\nvoid EmojiListWidget::pickerHidden() {\n\t_pickerSelected = v::null;\n\tupdate();\n\tdisableScroll(false);\n\tsetColorAllForceRippled(false);\n\n\t_lastMousePos = QCursor::pos();\n\tupdateSelected();\n}\n\nbool EmojiListWidget::hasColorButton(int index) const {\n\treturn (_staticCount > int(Section::People))\n\t\t&& (index == int(Section::People));\n}\n\nQRect EmojiListWidget::colorButtonRect(int index) const {\n\treturn colorButtonRect(sectionInfo(index));\n}\n\nQRect EmojiListWidget::colorButtonRect(const SectionInfo &info) const {\n\tif (_mode != Mode::Full) {\n\t\treturn QRect();\n\t}\n\tconst auto &colorSt = st().colorAll;\n\tconst auto buttonw = colorSt.rippleAreaPosition.x()\n\t\t+ colorSt.rippleAreaSize;\n\tconst auto buttonh = colorSt.height;\n\tconst auto buttonx = emojiRight() - st::emojiPanColorAllSkip - buttonw;\n\tconst auto buttony = info.top + st::emojiPanRemoveTop;\n\treturn QRect(buttonx, buttony, buttonw, buttonh);\n}\n\nbool EmojiListWidget::hasRemoveButton(int index) const {\n\tif (_searchMode) {\n\t\tif (index > 0 && index <= int(_searchSets.size())) {\n\t\t\treturn searchSetBySection(index).canRemove;\n\t\t}\n\t\treturn false;\n\t}\n\tif (index < _staticCount\n\t\t|| index >= _staticCount + _custom.size()) {\n\t\treturn false;\n\t}\n\tconst auto &set = _custom[index - _staticCount];\n\tif (set.id == Data::Stickers::MegagroupSetId) {\n\t\tAssert(_megagroupSet != nullptr);\n\t\tif (index + 1 != _staticCount + _custom.size()) {\n\t\t\treturn true;\n\t\t}\n\t\treturn !set.list.empty() && _megagroupSet->canEditEmoji();\n\t} else if (set.id == Data::Stickers::CollectibleSetId) {\n\t\treturn false;\n\t}\n\treturn set.canRemove && !set.premiumRequired;\n}\n\nQRect EmojiListWidget::removeButtonRect(int index) const {\n\treturn removeButtonRect(sectionInfo(index));\n}\n\nQRect EmojiListWidget::removeButtonRect(const SectionInfo &info) const {\n\tif (_mode != Mode::Full) {\n\t\treturn QRect();\n\t}\n\tconst auto &removeSt = st().removeSet;\n\tconst auto buttonw = removeSt.rippleAreaPosition.x()\n\t\t+ removeSt.rippleAreaSize;\n\tconst auto buttonh = removeSt.height;\n\tconst auto buttonx = emojiRight() - st::emojiPanRemoveSkip - buttonw;\n\tconst auto buttony = info.top + st::emojiPanRemoveTop;\n\treturn QRect(buttonx, buttony, buttonw, buttonh);\n}\n\nbool EmojiListWidget::hasAddButton(int index) const {\n\tif (_searchMode) {\n\t\tif (index > 0 && index <= int(_searchSets.size())) {\n\t\t\tconst auto &set = searchSetBySection(index);\n\t\t\treturn !set.canRemove && !set.premiumRequired;\n\t\t}\n\t\treturn false;\n\t}\n\tif (index < _staticCount\n\t\t|| index >= _staticCount + _custom.size()) {\n\t\treturn false;\n\t}\n\tconst auto &set = _custom[index - _staticCount];\n\treturn !set.canRemove\n\t\t&& !set.premiumRequired\n\t\t&& set.id != Data::Stickers::MegagroupSetId\n\t\t&& set.id != Data::Stickers::CollectibleSetId;\n}\n\nQRect EmojiListWidget::addButtonRect(int index) const {\n\treturn buttonRect(sectionInfo(index), _add);\n}\n\nbool EmojiListWidget::hasUnlockButton(int index) const {\n\tif (_searchMode) {\n\t\tif (index > 0 && index <= int(_searchSets.size())) {\n\t\t\treturn searchSetBySection(index).premiumRequired;\n\t\t}\n\t\treturn false;\n\t}\n\tif (index < _staticCount\n\t\t|| index >= _staticCount + _custom.size()) {\n\t\treturn false;\n\t}\n\tconst auto &set = _custom[index - _staticCount];\n\treturn set.premiumRequired;\n}\n\nQRect EmojiListWidget::unlockButtonRect(int index) const {\n\tExpects((_searchMode\n\t\t\t&& index > 0\n\t\t\t&& index <= int(_searchSets.size()))\n\t\t|| (index >= _staticCount\n\t\t\t&& index < _staticCount + _custom.size()));\n\n\treturn buttonRect(sectionInfo(index), rightButton(index));\n}\n\nbool EmojiListWidget::hasButton(int index) const {\n\tif (_searchMode) {\n\t\treturn (index > 0 && index <= int(_searchSets.size()));\n\t}\n\tif (hasColorButton(index)) {\n\t\treturn true;\n\t} else if (index >= _staticCount\n\t\t&& index < _staticCount + _custom.size()) {\n\t\tconst auto &custom = _custom[index - _staticCount];\n\t\treturn (custom.id != Data::Stickers::CollectibleSetId)\n\t\t\t&& ((custom.id != Data::Stickers::MegagroupSetId)\n\t\t\t\t|| custom.canRemove);\n\t}\n\treturn false;\n}\n\nQRect EmojiListWidget::buttonRect(int index) const {\n\treturn hasColorButton(index)\n\t\t? colorButtonRect(index)\n\t\t: hasRemoveButton(index)\n\t\t? removeButtonRect(index)\n\t\t: hasAddButton(index)\n\t\t? addButtonRect(index)\n\t\t: unlockButtonRect(index);\n}\n\nQRect EmojiListWidget::buttonRect(\n\t\tconst SectionInfo &info,\n\t\tconst RightButton &button) const {\n\tconst auto buttonw = button.textWidth - st::emojiPanButton.width;\n\tconst auto buttonh = st::emojiPanButton.height;\n\tconst auto buttonx = emojiRight() - buttonw - st::emojiPanButtonRight;\n\tconst auto buttony = info.top + st::emojiPanButtonTop;\n\treturn QRect(buttonx, buttony, buttonw, buttonh);\n}\n\nauto EmojiListWidget::rightButton(int index) const -> const RightButton & {\n\tif (_searchMode) {\n\t\tExpects(index > 0 && index <= int(_searchSets.size()));\n\n\t\treturn hasAddButton(index)\n\t\t\t? _add\n\t\t\t: searchSetBySection(index).canRemove\n\t\t\t? _restore\n\t\t\t: _unlock;\n\t}\n\tExpects(index >= _staticCount\n\t\t&& index < _staticCount + _custom.size());\n\n\treturn hasAddButton(index)\n\t\t? _add\n\t\t: _custom[index - _staticCount].canRemove\n\t\t? _restore\n\t\t: _unlock;\n}\n\nint EmojiListWidget::emojiRight() const {\n\treturn emojiLeft() + (_columnCount * _singleSize.width());\n}\n\nint EmojiListWidget::emojiLeft() const {\n\treturn _rowsLeft;\n}\n\nQRect EmojiListWidget::emojiRect(int section, int index) const {\n\tExpects(_columnCount > 0);\n\n\tconst auto info = sectionInfo(section);\n\tconst auto countTillItem = (index - (index % _columnCount));\n\tconst auto rowsToSkip = (countTillItem / _columnCount)\n\t\t+ ((countTillItem % _columnCount) ? 1 : 0);\n\tconst auto x = _rowsLeft + ((index % _columnCount) * _singleSize.width());\n\tconst auto y = info.rowsTop + rowsToSkip * _singleSize.height();\n\treturn QRect(x, y, _singleSize.width(), _singleSize.height());\n}\n\nvoid EmojiListWidget::colorChosen(EmojiChosen data) {\n\tExpects(data.emoji != nullptr && data.emoji->hasVariants());\n\n\tconst auto emoji = data.emoji;\n\tauto &settings = Core::App().settings();\n\tif (v::is<OverButton>(_pickerSelected)) {\n\t\tsettings.saveAllEmojiVariants(emoji);\n\t\tfor (auto section = int(Section::People)\n\t\t\t; section < _staticCount\n\t\t\t; ++section) {\n\t\t\tfor (auto &emoji : _emoji[section]) {\n\t\t\t\temoji = settings.lookupEmojiVariant(emoji);\n\t\t\t}\n\t\t}\n\t\tupdate();\n\t} else {\n\t\tsettings.saveEmojiVariant(emoji);\n\n\t\tconst auto over = std::get_if<OverEmoji>(&_pickerSelected);\n\t\tif (over\n\t\t\t&& over->section > int(Section::Recent)\n\t\t\t&& over->section < _staticCount\n\t\t\t&& over->index < _emoji[over->section].size()) {\n\t\t\t_emoji[over->section][over->index] = emoji;\n\t\t\trtlupdate(emojiRect(over->section, over->index));\n\t\t}\n\t\tselectEmoji(data);\n\t}\n\t_picker->hideAnimated();\n}\n\nvoid EmojiListWidget::mouseMoveEvent(QMouseEvent *e) {\n\t_lastMousePos = e->globalPos();\n\tif (!_picker->isHidden()) {\n\t\tif (_picker->rect().contains(_picker->mapFromGlobal(_lastMousePos))) {\n\t\t\treturn _picker->handleMouseMove(QCursor::pos());\n\t\t} else {\n\t\t\t_picker->clearSelection();\n\t\t}\n\t}\n\tupdateSelected();\n}\n\nvoid EmojiListWidget::leaveEventHook(QEvent *e) {\n\tclearSelection();\n}\n\nvoid EmojiListWidget::leaveToChildEvent(QEvent *e, QWidget *child) {\n\tclearSelection();\n}\n\nvoid EmojiListWidget::enterFromChildEvent(QEvent *e, QWidget *child) {\n\t_lastMousePos = QCursor::pos();\n\tupdateSelected();\n}\n\nvoid EmojiListWidget::clearSelection() {\n\tsetPressed(v::null);\n\tsetSelected(v::null);\n\t_lastMousePos = mapToGlobal(QPoint(-10, -10));\n}\n\nuint64 EmojiListWidget::currentSet(int yOffset) const {\n\treturn sectionSetId(sectionInfoByOffset(yOffset).section);\n}\n\nvoid EmojiListWidget::setAllowWithoutPremium(bool allow) {\n\tif (_allowWithoutPremium == allow) {\n\t\treturn;\n\t}\n\t_allowWithoutPremium = allow;\n\trefreshCustom();\n\tresizeToWidth(width());\n}\n\nvoid EmojiListWidget::showMegagroupSet(ChannelData *megagroup) {\n\tExpects(!megagroup || megagroup->isMegagroup());\n\n\tif (_megagroupSet != megagroup) {\n\t\t_megagroupSet = megagroup;\n\t\trefreshCustom();\n\t\tresizeToWidth(width());\n\t}\n}\n\nQString EmojiListWidget::tooltipText() const {\n\tif (_mode != Mode::Full) {\n\t\treturn {};\n\t}\n\tconst auto &replacements = Ui::Emoji::internal::GetAllReplacements();\n\tconst auto over = std::get_if<OverEmoji>(&_selected);\n\tif (const auto emoji = lookupOverEmoji(over)) {\n\t\tconst auto text = emoji->original()->text();\n\t\t// find the replacement belonging to the emoji\n\t\tconst auto it = ranges::find_if(replacements, [&](const auto &one) {\n\t\t\treturn text == Ui::Emoji::QStringFromUTF16(one.emoji);\n\t\t});\n\t\tif (it != replacements.end()) {\n\t\t\treturn Ui::Emoji::QStringFromUTF16(it->replacement);\n\t\t}\n\t}\n\treturn {};\n}\n\nQPoint EmojiListWidget::tooltipPos() const {\n\treturn _lastMousePos;\n}\n\nbool EmojiListWidget::tooltipWindowActive() const {\n\treturn Ui::AppInFocus() && Ui::InFocusChain(window());\n}\n\nTabbedSelector::InnerFooter *EmojiListWidget::getFooter() const {\n\treturn _footer;\n}\n\nvoid EmojiListWidget::processHideFinished() {\n\tif (!_picker->isHidden()) {\n\t\t_picker->hideFast();\n\t\t_pickerSelected = v::null;\n\t}\n\tcancelSearchRequest();\n\tunloadAllCustom();\n\tclearSelection();\n}\n\nvoid EmojiListWidget::processPanelHideFinished() {\n\tunloadAllCustom();\n\tif (_localSetsManager->clearInstalledLocally()) {\n\t\trefreshCustom();\n\t}\n}\n\nvoid EmojiListWidget::refreshRecent() {\n\tif (_mode != Mode::Full) {\n\t\treturn;\n\t}\n\tclearSelection();\n\tfillRecent();\n\tresizeToWidth(width());\n\tupdate();\n}\n\nvoid EmojiListWidget::refreshCustom() {\n\tif (_mode == Mode::RecentReactions || _mode == Mode::MessageEffects) {\n\t\treturn;\n\t}\n\tauto old = base::take(_custom);\n\tconst auto session = &this->session();\n\tconst auto premiumPossible = session->premiumPossible();\n\tconst auto onlyUnicodeEmoji = _onlyUnicodeEmoji || !premiumPossible;\n\tconst auto premiumMayBeBought = (!onlyUnicodeEmoji)\n\t\t&& premiumPossible\n\t\t&& !session->premium()\n\t\t&& !_allowWithoutPremium;\n\tconst auto owner = &session->data();\n\tconst auto &sets = owner->stickers().sets();\n\tconst auto push = [&](uint64 setId, bool installed) {\n\t\tconst auto megagroup = _megagroupSet\n\t\t\t&& (setId == Data::Stickers::MegagroupSetId);\n\t\tconst auto lookupId = megagroup\n\t\t\t? _megagroupSet->mgInfo->emojiSet.id\n\t\t\t: setId;\n\t\tif (!lookupId) {\n\t\t\treturn;\n\t\t} else if (!megagroup\n\t\t\t&& !_custom.empty()\n\t\t\t&& _custom.front().id == Data::Stickers::MegagroupSetId\n\t\t\t&& _megagroupSet->mgInfo->emojiSet.id == setId) {\n\t\t\t// Skip the set that is already added as a megagroup set.\n\t\t\treturn;\n\t\t} else if (megagroup\n\t\t\t&& ranges::contains(_custom, lookupId, &CustomSet::id)) {\n\t\t\t// Skip the set that is already added as a custom set.\n\t\t\treturn;\n\t\t}\n\t\tauto it = sets.find(lookupId);\n\t\tif (it == sets.cend()\n\t\t\t|| it->second->stickers.isEmpty()\n\t\t\t|| (_mode == Mode::BackgroundEmoji && !it->second->textColor())\n\t\t\t|| (_mode == Mode::ChannelStatus\n\t\t\t\t&& !it->second->channelStatus())) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto canRemove = megagroup\n\t\t\t? (_megagroupSet->canEditEmoji() || installed)\n\t\t\t: !!(it->second->flags & Data::StickersSetFlag::Installed);\n\t\tconst auto sortAsInstalled = canRemove\n\t\t\t&& (!(it->second->flags & Data::StickersSetFlag::Featured)\n\t\t\t\t|| !_localSetsManager->isInstalledLocally(lookupId));\n\t\tif (!megagroup && sortAsInstalled != installed) {\n\t\t\treturn;\n\t\t}\n\t\tauto premium = false;\n\t\tconst auto &list = it->second->stickers;\n\t\tconst auto i = ranges::find(old, setId, &CustomSet::id);\n\t\tif (i != end(old)) {\n\t\t\tconst auto valid = [&] {\n\t\t\t\tconst auto count = int(list.size());\n\t\t\t\tif (i->list.size() != count) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tfor (auto k = 0; k != count; ++k) {\n\t\t\t\t\tif (!premium && !megagroup && list[k]->isPremiumEmoji()) {\n\t\t\t\t\t\tpremium = true;\n\t\t\t\t\t}\n\t\t\t\t\tif (i->list[k].document != list[k]) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}();\n\t\t\tif (premium && onlyUnicodeEmoji) {\n\t\t\t\treturn;\n\t\t\t} else if (valid) {\n\t\t\t\ti->thumbnailDocument = it->second->lookupThumbnailDocument();\n\t\t\t\tconst auto premiumRequired = premium && premiumMayBeBought;\n\t\t\t\tif (i->canRemove != canRemove\n\t\t\t\t\t|| i->premiumRequired != premiumRequired) {\n\t\t\t\t\ti->canRemove = canRemove;\n\t\t\t\t\ti->premiumRequired = premiumRequired;\n\t\t\t\t\ti->ripple.reset();\n\t\t\t\t}\n\t\t\t\tif (i->canRemove && !i->premiumRequired) {\n\t\t\t\t\ti->expanded = false;\n\t\t\t\t}\n\t\t\t\t_custom.push_back(std::move(*i));\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tauto set = std::vector<CustomOne>();\n\t\tset.reserve(list.size());\n\t\tfor (const auto document : list) {\n\t\t\tconst auto id = EmojiStatusId{ document->id };\n\t\t\tif (_restrictedCustomList.contains(id.documentId)) {\n\t\t\t\tcontinue;\n\t\t\t} else if (const auto sticker = document->sticker()) {\n\t\t\t\tset.push_back({\n\t\t\t\t\t.custom = resolveCustomEmoji(id, document, lookupId),\n\t\t\t\t\t.document = document,\n\t\t\t\t\t.emoji = Ui::Emoji::Find(sticker->alt),\n\t\t\t\t});\n\t\t\t\tif (!premium && !megagroup && document->isPremiumEmoji()) {\n\t\t\t\t\tpremium = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (premium && onlyUnicodeEmoji) {\n\t\t\treturn;\n\t\t}\n\t\t_custom.push_back({\n\t\t\t.id = setId,\n\t\t\t.set = it->second.get(),\n\t\t\t.thumbnailDocument = it->second->lookupThumbnailDocument(),\n\t\t\t.title = it->second->title,\n\t\t\t.list = std::move(set),\n\t\t\t.canRemove = canRemove,\n\t\t\t.premiumRequired = premium && premiumMayBeBought,\n\t\t});\n\t};\n\trefreshEmojiStatusCollectibles();\n\trefreshMegagroupStickers(push, GroupStickersPlace::Visible);\n\tfor (const auto setId : owner->stickers().emojiSetsOrder()) {\n\t\tpush(setId, true);\n\t}\n\tfor (const auto setId : owner->stickers().featuredEmojiSetsOrder()) {\n\t\tpush(setId, false);\n\t}\n\trefreshMegagroupStickers(push, GroupStickersPlace::Hidden);\n\n\t_footer->refreshIcons(\n\t\tfillIcons(),\n\t\tcurrentSet(getVisibleTop()),\n\t\tnullptr,\n\t\tValidateIconAnimations::None);\n\tupdate();\n}\n\nFn<void()> EmojiListWidget::repaintCallback(\n\t\tDocumentId documentId,\n\t\tuint64 setId) {\n\treturn [=] {\n\t\trepaintCustom(setId);\n\t\tif (_recentCustomIds.contains(documentId)) {\n\t\t\trepaintCustom(RecentEmojiSectionSetId());\n\t\t}\n\t\tif (_searchCustomIds.contains(documentId)) {\n\t\t\trepaintCustom(SearchEmojiSectionSetId());\n\t\t}\n\t};\n}\n\nnot_null<Ui::Text::CustomEmoji*> EmojiListWidget::resolveCustomEmoji(\n\t\tEmojiStatusId id,\n\t\tnot_null<DocumentData*> document,\n\t\tuint64 setId) {\n\tconst auto documentId = document->id;\n\tconst auto i = _customEmoji.find(id);\n\tconst auto recentOnly = (i != end(_customEmoji)) && i->second.recentOnly;\n\tif (i != end(_customEmoji) && !recentOnly) {\n\t\treturn i->second.emoji.get();\n\t}\n\tauto instance = document->owner().customEmojiManager().create(\n\t\tData::EmojiStatusCustomId(id),\n\t\trepaintCallback(documentId, setId),\n\t\tData::CustomEmojiManager::SizeTag::Large);\n\tif (recentOnly) {\n\t\tfor (auto &recent : _recent) {\n\t\t\tif (recent.custom && recent.custom == i->second.emoji.get()) {\n\t\t\t\trecent.custom = instance.get();\n\t\t\t}\n\t\t}\n\t\ti->second.emoji = std::move(instance);\n\t\ti->second.recentOnly = false;\n\t\treturn i->second.emoji.get();\n\t}\n\treturn _customEmoji.emplace(\n\t\tid,\n\t\tCustomEmojiInstance{ .emoji = std::move(instance) }\n\t).first->second.emoji.get();\n}\n\nUi::Text::CustomEmoji *EmojiListWidget::resolveCustomRecent(\n\t\tRecentEmojiId customId) {\n\tconst auto &data = customId.data;\n\tif (const auto document = std::get_if<RecentEmojiDocument>(&data)) {\n\t\treturn resolveCustomRecent(document->id);\n\t} else if (v::is<EmojiPtr>(data)) {\n\t\treturn nullptr;\n\t}\n\tUnexpected(\"Custom recent emoji id.\");\n}\n\nnot_null<Ui::Text::CustomEmoji*> EmojiListWidget::resolveCustomRecent(\n\t\tDocumentId documentId) {\n\treturn resolveCustomRecent(EmojiStatusId{ documentId });\n}\n\nnot_null<Ui::Text::CustomEmoji*> EmojiListWidget::resolveCustomRecent(\n\t\tEmojiStatusId id) {\n\tconst auto i = id.collectible\n\t\t? end(_customRecent)\n\t\t: _customRecent.find(id.documentId);\n\tif (i != end(_customRecent)) {\n\t\treturn i->second.get();\n\t}\n\tconst auto j = _customEmoji.find(id);\n\tif (j != end(_customEmoji)) {\n\t\treturn j->second.emoji.get();\n\t}\n\tconst auto documentId = id.collectible\n\t\t? id.collectible->documentId\n\t\t: id.documentId;\n\tauto repaint = repaintCallback(documentId, RecentEmojiSectionSetId());\n\tif (_customRecentFactory && !id.collectible) {\n\t\treturn _customRecent.emplace(\n\t\t\tid.documentId,\n\t\t\t_customRecentFactory(id.documentId, std::move(repaint))\n\t\t).first->second.get();\n\t}\n\tauto custom = session().data().customEmojiManager().create(\n\t\tData::EmojiStatusCustomId(id),\n\t\tstd::move(repaint),\n\t\tData::CustomEmojiManager::SizeTag::Large);\n\treturn _customEmoji.emplace(\n\t\tid,\n\t\tCustomEmojiInstance{ .emoji = std::move(custom), .recentOnly = true }\n\t).first->second.emoji.get();\n}\n\nvoid EmojiListWidget::refreshEmojiStatusCollectibles() {\n\tif (_mode != Mode::EmojiStatus || !_features.collectibleStatus) {\n\t\treturn;\n\t}\n\tconst auto type = Data::EmojiStatuses::Type::Collectibles;\n\tconst auto &list = session().data().emojiStatuses().list(type);\n\tconst auto setId = Data::Stickers::CollectibleSetId;\n\tauto set = std::vector<CustomOne>();\n\tset.reserve(list.size());\n\tfor (const auto &status : list) {\n\t\tconst auto documentId = status.collectible\n\t\t\t? status.collectible->documentId\n\t\t\t: status.documentId;\n\t\tconst auto document = session().data().document(documentId);\n\t\tconst auto sticker = document->sticker();\n\t\tset.push_back({\n\t\t\t.collectible = status.collectible,\n\t\t\t.custom = resolveCustomEmoji(status, document, setId),\n\t\t\t.document = document,\n\t\t\t.emoji = sticker ? Ui::Emoji::Find(sticker->alt) : nullptr,\n\t\t});\n\t}\n\tif (set.empty()) {\n\t\treturn;\n\t}\n\tconst auto collectibles = session().data().stickers().collectibleSet();\n\t_custom.push_back({\n\t\t.id = setId,\n\t\t.set = collectibles,\n\t\t.thumbnailDocument = nullptr,\n\t\t.title = collectibles->title,\n\t\t.list = std::move(set),\n\t\t.canRemove = false,\n\t\t.premiumRequired = !session().premium(),\n\t});\n}\n\nvoid EmojiListWidget::refreshMegagroupStickers(\n\t\tFn<void(uint64 setId, bool installed)> push,\n\t\tGroupStickersPlace place) {\n\tif (!_features.megagroupSet\n\t\t|| !_megagroupSet\n\t\t|| !_megagroupSet->mgInfo->emojiSet) {\n\t\treturn;\n\t}\n\tauto canEdit = _megagroupSet->canEditEmoji();\n\tauto isShownHere = [place](bool hidden) {\n\t\treturn (hidden == (place == GroupStickersPlace::Hidden));\n\t};\n\tauto hidden = session().settings().isGroupEmojiSectionHidden(_megagroupSet->id);\n\tauto removeHiddenForGroup = [this, &hidden] {\n\t\tif (hidden) {\n\t\t\tsession().settings().removeGroupEmojiSectionHidden(_megagroupSet->id);\n\t\t\tsession().saveSettings();\n\t\t\thidden = false;\n\t\t}\n\t};\n\tif (canEdit && hidden) {\n\t\tremoveHiddenForGroup();\n\t}\n\tconst auto &set = _megagroupSet->mgInfo->emojiSet;\n\tif (!set.id || !isShownHere(hidden)) {\n\t\treturn;\n\t}\n\tpush(Data::Stickers::MegagroupSetId, !hidden);\n\tif (!_custom.empty()\n\t\t&& _custom.back().id == Data::Stickers::MegagroupSetId) {\n\t\treturn;\n\t} else if (_megagroupSetIdRequested == set.id) {\n\t\treturn;\n\t}\n\t_megagroupSetIdRequested = set.id;\n\t_api.request(MTPmessages_GetStickerSet(\n\t\tData::InputStickerSet(set),\n\t\tMTP_int(0) // hash\n\t)).done([=](const MTPmessages_StickerSet &result) {\n\t\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\tif (const auto set = session().data().stickers().feedSetFull(data)) {\n\t\t\t\trefreshCustom();\n\t\t\t\tif (set->id == _megagroupSetIdRequested) {\n\t\t\t\t\t_megagroupSetIdRequested = 0;\n\t\t\t\t} else {\n\t\t\t\t\tLOG((\"API Error: Got different set.\"));\n\t\t\t\t}\n\t\t\t}\n\t\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t\t});\n\t}).send();\n}\n\nstd::vector<StickerIcon> EmojiListWidget::fillIcons() {\n\tauto result = std::vector<StickerIcon>();\n\tresult.reserve(2 + _custom.size());\n\n\tresult.emplace_back(RecentEmojiSectionSetId());\n\tif (_mode != Mode::Full) {\n\t} else if (_custom.empty()) {\n\t\tusing Section = Ui::Emoji::Section;\n\t\tfor (auto i = int(Section::People); i <= int(Section::Symbols); ++i) {\n\t\t\tresult.emplace_back(EmojiSectionSetId(Section(i)));\n\t\t}\n\t} else {\n\t\tresult.emplace_back(AllEmojiSectionSetId());\n\t}\n\tconst auto esize = StickersListFooter::IconFrameSize();\n\tfor (const auto &custom : _custom) {\n\t\tif (custom.id == Data::Stickers::MegagroupSetId) {\n\t\t\tresult.emplace_back(Data::Stickers::MegagroupSetId);\n\t\t\tresult.back().megagroup = _megagroupSet;\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto set = custom.set;\n\t\tresult.emplace_back(set, custom.thumbnailDocument, esize, esize);\n\t}\n\treturn result;\n}\n\nint EmojiListWidget::paintButtonGetWidth(\n\t\tQPainter &p,\n\t\tconst SectionInfo &info,\n\t\tbool selected,\n\t\tQRect clip) const {\n\tif (!hasButton(info.section)) {\n\t\treturn 0;\n\t}\n\tauto &ripple = (_searchMode && info.section > 0)\n\t\t? searchSetBySection(info.section).ripple\n\t\t: (info.section >= _staticCount)\n\t\t? _custom[info.section - _staticCount].ripple\n\t\t: _colorAllRipple;\n\tconst auto colorAll = hasColorButton(info.section);\n\tif (colorAll || hasRemoveButton(info.section)) {\n\t\tconst auto rect = colorAll\n\t\t\t? colorButtonRect(info)\n\t\t\t: removeButtonRect(info);\n\t\tif (rect.isEmpty()) {\n\t\t\treturn 0;\n\t\t} else if (rect.intersects(clip)) {\n\t\t\tconst auto &bst = colorAll ? st().colorAll : st().removeSet;\n\t\t\tif (colorAll && _colorAllRippleForced) {\n\t\t\t\tselected = true;\n\t\t\t}\n\t\t\tif (ripple) {\n\t\t\t\tripple->paint(\n\t\t\t\t\tp,\n\t\t\t\t\trect.x() + bst.rippleAreaPosition.x(),\n\t\t\t\t\trect.y() + bst.rippleAreaPosition.y(),\n\t\t\t\t\twidth());\n\t\t\t\tif (ripple->empty()) {\n\t\t\t\t\tripple.reset();\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto &icon = selected ? bst.iconOver : bst.icon;\n\t\t\ticon.paint(\n\t\t\t\tp,\n\t\t\t\t(rect.topLeft()\n\t\t\t\t\t+ QPoint(\n\t\t\t\t\t\trect.width() - icon.width(),\n\t\t\t\t\t\trect.height() - icon.height()) / 2),\n\t\t\t\twidth());\n\t\t}\n\t\treturn emojiRight() - rect.x();\n\t}\n\tconst auto canAdd = hasAddButton(info.section);\n\tconst auto &button = rightButton(info.section);\n\tconst auto rect = buttonRect(info, button);\n\tp.drawImage(rect.topLeft(), selected ? button.backOver : button.back);\n\tif (ripple) {\n\t\tconst auto color = QColor(0, 0, 0, 36);\n\t\tripple->paint(p, rect.x(), rect.y(), width(), &color);\n\t\tif (ripple->empty()) {\n\t\t\tripple.reset();\n\t\t}\n\t}\n\tp.setPen(!canAdd\n\t\t? st::premiumButtonFg\n\t\t: selected\n\t\t? st::emojiPanButton.textFgOver\n\t\t: st::emojiPanButton.textFg);\n\tp.setFont(st::emojiPanButton.style.font);\n\tp.drawText(\n\t\trect.x() - (st::emojiPanButton.width / 2),\n\t\t(rect.y()\n\t\t\t+ st::emojiPanButton.textTop\n\t\t\t+ st::emojiPanButton.style.font->ascent),\n\t\tbutton.text);\n\treturn emojiRight() - rect.x();\n}\n\nvoid EmojiListWidget::paintEmptySearchResults(Painter &p) {\n\tInner::paintEmptySearchResults(\n\t\tp,\n\t\tst::emojiEmpty,\n\t\ttr::lng_emoji_nothing_found(tr::now));\n}\n\nbool EmojiListWidget::eventHook(QEvent *e) {\n\tif (e->type() == QEvent::ParentChange) {\n\t\tif (_picker->parentWidget() != parentWidget()) {\n\t\t\t_picker->setParent(parentWidget());\n\t\t}\n\t\t_picker->raise();\n\t}\n\treturn Inner::eventHook(e);\n}\n\nvoid EmojiListWidget::updateSelected() {\n\tif (!v::is_null(_pressed) || !v::is_null(_pickerSelected)) {\n\t\tif (!_previewShown) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tauto newSelected = OverState{ v::null };\n\tauto p = mapFromGlobal(_lastMousePos);\n\tauto info = sectionInfoByOffset(p.y());\n\tauto section = info.section;\n\tif (p.y() >= info.top && p.y() < info.rowsTop) {\n\t\tif (hasButton(section)\n\t\t\t&& myrtlrect(buttonRect(section)).contains(p.x(), p.y())) {\n\t\t\tnewSelected = OverButton{ section };\n\t\t} else if (_features.openStickerSets\n\t\t\t&& section >= _staticCount\n\t\t\t&& _mode == Mode::Full) {\n\t\t\tnewSelected = OverSet{ section };\n\t\t}\n\t} else if (p.y() >= info.rowsTop && p.y() < info.rowsBottom) {\n\t\tauto sx = (rtl() ? width() - p.x() : p.x()) - _rowsLeft;\n\t\tif (sx >= 0 && sx < _columnCount * _singleSize.width()) {\n\t\t\tconst auto index = qFloor((p.y() - info.rowsTop) / _singleSize.height()) * _columnCount + qFloor(sx / _singleSize.width());\n\t\t\tif (index < info.count) {\n\t\t\t\tnewSelected = OverEmoji{ .section = section, .index = index };\n\t\t\t}\n\t\t}\n\t}\n\tsetSelected(newSelected);\n}\n\nvoid EmojiListWidget::setSelected(OverState newSelected) {\n\tif (_selected == newSelected) {\n\t\treturn;\n\t}\n\tsetCursor(!v::is_null(newSelected)\n\t\t? style::cur_pointer\n\t\t: style::cur_default);\n\n\tconst auto updateSelected = [&] {\n\t\tif (const auto sticker = std::get_if<OverEmoji>(&_selected)) {\n\t\t\trtlupdate(emojiRect(sticker->section, sticker->index));\n\t\t} else if (const auto button = std::get_if<OverButton>(&_selected)) {\n\t\t\trtlupdate(buttonRect(button->section));\n\t\t}\n\t};\n\tupdateSelected();\n\t_selected = newSelected;\n\tupdateSelected();\n\n\tconst auto hasSelection = !v::is_null(_selected);\n\tif (hasSelection && Core::App().settings().suggestEmoji()) {\n\t\tUi::Tooltip::Show(1000, this);\n\t}\n\n\tsetCursor(hasSelection ? style::cur_pointer : style::cur_default);\n\tif (hasSelection && !_picker->isHidden()) {\n\t\tif (_selected != _pickerSelected) {\n\t\t\t_picker->hideAnimated();\n\t\t} else {\n\t\t\t_picker->showAnimated();\n\t\t}\n\t} else if (_previewShown && _pressed != _selected) {\n\t\tif (const auto over = std::get_if<OverEmoji>(&_selected)) {\n\t\t\tif (const auto custom = lookupCustomEmoji(over)) {\n\t\t\t\t_pressed = _selected;\n\t\t\t\tshowPreviewFor(custom.document);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid EmojiListWidget::setPressed(OverState newPressed) {\n\tif (auto button = std::get_if<OverButton>(&_pressed)) {\n\t\tAssert(hasColorButton(button->section)\n\t\t\t|| (_searchMode && button->section > 0\n\t\t\t\t&& button->section <= int(_searchSets.size()))\n\t\t\t|| (button->section >= _staticCount\n\t\t\t\t&& button->section < _staticCount + _custom.size()));\n\t\tauto &ripple = (_searchMode && button->section > 0)\n\t\t\t? searchSetBySection(button->section).ripple\n\t\t\t: (button->section >= _staticCount)\n\t\t\t? _custom[button->section - _staticCount].ripple\n\t\t\t: _colorAllRipple;\n\t\tif (ripple) {\n\t\t\tripple->lastStop();\n\t\t}\n\t}\n\t_pressed = newPressed;\n\tif (auto button = std::get_if<OverButton>(&_pressed)) {\n\t\tAssert(hasColorButton(button->section)\n\t\t\t|| (_searchMode && button->section > 0\n\t\t\t\t&& button->section <= int(_searchSets.size()))\n\t\t\t|| (button->section >= _staticCount\n\t\t\t\t&& button->section < _staticCount + _custom.size()));\n\t\tauto &ripple = (_searchMode && button->section > 0)\n\t\t\t? searchSetBySection(button->section).ripple\n\t\t\t: (button->section >= _staticCount)\n\t\t\t? _custom[button->section - _staticCount].ripple\n\t\t\t: _colorAllRipple;\n\t\tif (!ripple) {\n\t\t\tripple = createButtonRipple(button->section);\n\t\t}\n\t\tripple->add(mapFromGlobal(QCursor::pos()) - buttonRippleTopLeft(button->section));\n\t}\n}\n\nvoid EmojiListWidget::initButton(\n\t\tRightButton &button,\n\t\tconst QString &text,\n\t\tbool gradient) {\n\tbutton.text = text;\n\tbutton.textWidth = st::emojiPanButton.style.font->width(text);\n\tconst auto width = button.textWidth - st::emojiPanButton.width;\n\tconst auto height = st::emojiPanButton.height;\n\tconst auto factor = style::DevicePixelRatio();\n\tauto prepare = [&](QColor bg, QBrush fg) {\n\t\tauto image = QImage(\n\t\t\tQSize(width, height) * factor,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\timage.setDevicePixelRatio(factor);\n\t\timage.fill(Qt::transparent);\n\t\tauto p = QPainter(&image);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(fg);\n\t\tconst auto radius = height / 2.;\n\t\tp.drawRoundedRect(QRect(0, 0, width, height), radius, radius);\n\t\tp.end();\n\t\treturn image;\n\t};\n\tbutton.back = prepare(Qt::transparent, [&]() -> QBrush {\n\t\tif (gradient) {\n\t\t\tauto result = QLinearGradient(QPointF(0, 0), QPointF(width, 0));\n\t\t\tresult.setStops(Ui::Premium::GiftGradientStops());\n\t\t\treturn result;\n\t\t}\n\t\treturn st::emojiPanButton.textBg;\n\t}());\n\tbutton.backOver = gradient\n\t\t? button.back\n\t\t: prepare(Qt::transparent, st::emojiPanButton.textBgOver);\n\tbutton.rippleMask = prepare(Qt::black, Qt::white);\n}\n\nstd::unique_ptr<Ui::RippleAnimation> EmojiListWidget::createButtonRipple(\n\t\tint section) {\n\tExpects(hasButton(section));\n\n\tconst auto colorAll = hasColorButton(section);\n\tconst auto remove = hasRemoveButton(section);\n\tconst auto &staticSt = colorAll ? st().colorAll : st().removeSet;\n\tconst auto &st = (colorAll || remove)\n\t\t? staticSt.ripple\n\t\t: st::emojiPanButton.ripple;\n\tauto mask = (colorAll || remove)\n\t\t? Ui::RippleAnimation::EllipseMask(QSize(\n\t\t\tstaticSt.rippleAreaSize,\n\t\t\tstaticSt.rippleAreaSize))\n\t\t: rightButton(section).rippleMask;\n\treturn std::make_unique<Ui::RippleAnimation>(\n\t\tst,\n\t\tstd::move(mask),\n\t\t[this, section] { rtlupdate(buttonRect(section)); });\n}\n\nQPoint EmojiListWidget::buttonRippleTopLeft(int section) const {\n\tExpects(hasButton(section));\n\n\treturn myrtlrect(buttonRect(section)).topLeft()\n\t\t+ (hasColorButton(section)\n\t\t\t? st().colorAll.rippleAreaPosition\n\t\t\t: hasRemoveButton(section)\n\t\t\t? st().removeSet.rippleAreaPosition\n\t\t\t: QPoint());\n}\n\nPowerSaving::Flag EmojiListWidget::powerSavingFlag() const {\n\tconst auto reactions = (_mode == Mode::FullReactions)\n\t\t|| (_mode == Mode::RecentReactions);\n\treturn reactions\n\t\t? PowerSaving::kEmojiReactions\n\t\t: PowerSaving::kEmojiPanel;\n}\n\nvoid EmojiListWidget::refreshEmoji() {\n\trefreshRecent();\n\trefreshCustom();\n}\n\nvoid EmojiListWidget::showSet(uint64 setId) {\n\tclearSelection();\n\tif (_search && _searchMode) {\n\t\t_search->cancel();\n\t\tapplyNextSearchQuery();\n\t}\n\n\tauto y = 0;\n\tenumerateSections([&](const SectionInfo &info) {\n\t\tif (setId == sectionSetId(info.section)) {\n\t\t\ty = info.top;\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n\tscrollTo(y);\n\n\t_lastMousePos = QCursor::pos();\n\n\tupdate();\n}\n\nuint64 EmojiListWidget::sectionSetId(int section) const {\n\tExpects(_searchMode\n\t\t|| section < _staticCount\n\t\t|| (section - _staticCount) < _custom.size());\n\n\treturn (_searchMode && section == 0)\n\t\t? SearchEmojiSectionSetId()\n\t\t: (_searchMode && section > 0)\n\t\t? searchSetBySection(section).id\n\t\t: (section < _staticCount)\n\t\t? EmojiSectionSetId(static_cast<Section>(section))\n\t\t: _custom[section - _staticCount].id;\n}\n\ntr::phrase<> EmojiCategoryTitle(int index) {\n\tswitch (index) {\n\tcase 1: return tr::lng_emoji_category1;\n\tcase 2: return tr::lng_emoji_category2;\n\tcase 3: return tr::lng_emoji_category3;\n\tcase 4: return tr::lng_emoji_category4;\n\tcase 5: return tr::lng_emoji_category5;\n\tcase 6: return tr::lng_emoji_category6;\n\tcase 7: return tr::lng_emoji_category7;\n\t}\n\tUnexpected(\"Index in CategoryTitle.\");\n}\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/emoji_list_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"chat_helpers/compose/compose_features.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/round_rect.h\"\n#include \"base/timer.h\"\n\n#include <map>\n\nclass StickerPremiumMark;\n\nnamespace style {\nstruct EmojiPan;\n} // namespace style\n\nnamespace Core {\nstruct RecentEmojiId;\n} // namespace Core\n\nnamespace Data {\nclass StickersSet;\n} // namespace Data\n\nnamespace PowerSaving {\nenum Flag : uint32;\n} // namespace PowerSaving\n\nnamespace tr {\ntemplate <typename ...Tags>\nstruct phrase;\n} // namespace tr\n\nnamespace Ui {\nclass RippleAnimation;\nclass TabbedSearch;\n} // namespace Ui\n\nnamespace Ui::Emoji {\nenum class Section;\n} // namespace Ui::Emoji\n\nnamespace Ui::Text {\nclass CustomEmoji;\nstruct CustomEmojiPaintContext;\n} // namespace Ui::Text\n\nnamespace Ui::CustomEmoji {\nclass Loader;\nclass Instance;\nstruct RepaintRequest;\n} // namespace Ui::CustomEmoji\n\nnamespace Window {\nclass SessionController;\nclass MediaPreviewWidget;\n} // namespace Window\n\nnamespace ChatHelpers {\n\ninline constexpr auto kEmojiSectionCount = 8;\n\nstruct StickerIcon;\nclass EmojiColorPicker;\nclass StickersListFooter;\nclass GradientPremiumStar;\nclass LocalStickersManager;\n\nenum class EmojiListMode {\n\tFull,\n\tTopicIcon,\n\tEmojiStatus,\n\tChannelStatus,\n\tFullReactions,\n\tRecentReactions,\n\tUserpicBuilder,\n\tBackgroundEmoji,\n\tPeerTitle,\n\tMessageEffects,\n};\n\n[[nodiscard]] std::vector<EmojiStatusId> DocumentListToRecent(\n\tconst std::vector<DocumentId> &documents);\n\nstruct EmojiListDescriptor {\n\tstd::shared_ptr<Show> show;\n\tEmojiListMode mode = EmojiListMode::Full;\n\tFn<QColor()> customTextColor;\n\tFn<bool()> paused;\n\tstd::vector<EmojiStatusId> customRecentList;\n\tFn<std::unique_ptr<Ui::Text::CustomEmoji>(\n\t\tDocumentId,\n\t\tFn<void()>)> customRecentFactory;\n\tbase::flat_set<DocumentId> freeEffects;\n\tconst style::EmojiPan *st = nullptr;\n\tComposeFeatures features;\n\tQWidget *mediaPreviewParent = nullptr;\n\tQMargins mediaPreviewMargins;\n};\n\nclass EmojiListWidget final\n\t: public TabbedSelector::Inner\n\t, public Ui::AbstractTooltipShower {\npublic:\n\tusing Mode = EmojiListMode;\n\n\tEmojiListWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tPauseReason level,\n\t\tMode mode);\n\tEmojiListWidget(QWidget *parent, EmojiListDescriptor &&descriptor);\n\t~EmojiListWidget();\n\n\tusing Section = Ui::Emoji::Section;\n\n\tvoid refreshRecent() override;\n\tvoid clearSelection() override;\n\tobject_ptr<TabbedSelector::InnerFooter> createFooter() override;\n\n\tvoid afterShown() override;\n\tvoid beforeHiding() override;\n\n\tvoid showSet(uint64 setId);\n\t[[nodiscard]] uint64 currentSet(int yOffset) const;\n\tvoid setAllowWithoutPremium(bool allow);\n\tvoid showMegagroupSet(ChannelData *megagroup);\n\n\t// Ui::AbstractTooltipShower interface.\n\tQString tooltipText() const override;\n\tQPoint tooltipPos() const override;\n\tbool tooltipWindowActive() const override;\n\n\tvoid refreshEmoji();\n\n\t[[nodiscard]] rpl::producer<EmojiChosen> chosen() const;\n\t[[nodiscard]] rpl::producer<FileChosen> customChosen() const;\n\t[[nodiscard]] rpl::producer<> jumpedToPremium() const;\n\t[[nodiscard]] rpl::producer<> escapes() const;\n\n\tvoid provideRecent(const std::vector<EmojiStatusId> &customRecentList);\n\n\tvoid prepareExpanding();\n\tvoid paintExpanding(\n\t\tPainter &p,\n\t\tQRect clip,\n\t\tint finalBottom,\n\t\tfloat64 geometryProgress,\n\t\tfloat64 fullProgress,\n\t\tRectPart origin);\n\n\tbase::unique_qptr<Ui::PopupMenu> fillContextMenu(\n\t\tconst SendMenu::Details &details) override;\n\n\t[[nodiscard]] rpl::producer<std::vector<QString>> searchQueries() const;\n\t[[nodiscard]] rpl::producer<int> recentShownCount() const;\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid leaveToChildEvent(QEvent *e, QWidget *child) override;\n\tvoid enterFromChildEvent(QEvent *e, QWidget *child) override;\n\tbool eventHook(QEvent *e) override;\n\n\tTabbedSelector::InnerFooter *getFooter() const override;\n\tvoid processHideFinished() override;\n\tvoid processPanelHideFinished() override;\n\tint countDesiredHeight(int newWidth) override;\n\tint defaultMinimalHeight() const override;\n\nprivate:\n\tstruct SectionInfo {\n\t\tint section = 0;\n\t\tint count = 0;\n\t\tint top = 0;\n\t\tint rowsCount = 0;\n\t\tint rowsTop = 0;\n\t\tint rowsBottom = 0;\n\t\tbool premiumRequired = false;\n\t\tbool collapsed = false;\n\t};\n\tstruct CustomOne {\n\t\tstd::shared_ptr<Data::EmojiStatusCollectible> collectible;\n\t\tnot_null<Ui::Text::CustomEmoji*> custom;\n\t\tnot_null<DocumentData*> document;\n\t\tEmojiPtr emoji = nullptr;\n\t};\n\tstruct CustomSet {\n\t\tuint64 id = 0;\n\t\tnot_null<Data::StickersSet*> set;\n\t\tDocumentData *thumbnailDocument = nullptr;\n\t\tQString title;\n\t\tstd::vector<CustomOne> list;\n\t\tmutable std::unique_ptr<Ui::RippleAnimation> ripple;\n\t\tbool painted = false;\n\t\tbool expanded = false;\n\t\tbool canRemove = false;\n\t\tbool premiumRequired = false;\n\t};\n\tstruct CustomEmojiInstance;\n\tstruct RightButton {\n\t\tQImage back;\n\t\tQImage backOver;\n\t\tQImage rippleMask;\n\t\tQString text;\n\t\tint textWidth = 0;\n\t};\n\tstruct RecentOne;\n\tstruct OverEmoji {\n\t\tint section = 0;\n\t\tint index = 0;\n\n\t\tinline bool operator==(OverEmoji other) const {\n\t\t\treturn (section == other.section)\n\t\t\t\t&& (index == other.index);\n\t\t}\n\t\tinline bool operator!=(OverEmoji other) const {\n\t\t\treturn !(*this == other);\n\t\t}\n\t};\n\tstruct OverSet {\n\t\tint section = 0;\n\n\t\tinline bool operator==(OverSet other) const {\n\t\t\treturn (section == other.section);\n\t\t}\n\t\tinline bool operator!=(OverSet other) const {\n\t\t\treturn !(*this == other);\n\t\t}\n\t};\n\tstruct OverButton {\n\t\tint section = 0;\n\n\t\tinline bool operator==(OverButton other) const {\n\t\t\treturn (section == other.section);\n\t\t}\n\t\tinline bool operator!=(OverButton other) const {\n\t\t\treturn !(*this == other);\n\t\t}\n\t};\n\tusing OverState = std::variant<\n\t\tv::null_t,\n\t\tOverEmoji,\n\t\tOverSet,\n\t\tOverButton>;\n\tstruct ExpandingContext {\n\t\tfloat64 progress = 0.;\n\t\tint finalHeight = 0;\n\t\tbool expanding = false;\n\t};\n\tstruct ResolvedCustom {\n\t\tDocumentData *document = nullptr;\n\t\tstd::shared_ptr<Data::EmojiStatusCollectible> collectible;\n\n\t\texplicit operator bool() const {\n\t\t\treturn document != nullptr;\n\t\t}\n\t};\n\n\ttemplate <typename Callback>\n\tbool enumerateSections(Callback callback) const;\n\t[[nodiscard]] SectionInfo sectionInfo(int section) const;\n\t[[nodiscard]] SectionInfo sectionInfoByOffset(int yOffset) const;\n\t[[nodiscard]] int sectionsCount() const;\n\tvoid setSingleSize(QSize size);\n\tvoid setColorAllForceRippled(bool force);\n\n\tvoid showPicker();\n\tvoid pickerHidden();\n\tvoid colorChosen(EmojiChosen data);\n\tbool checkPickerHide();\n\tvoid refreshCustom();\n\tenum class GroupStickersPlace {\n\t\tVisible,\n\t\tHidden,\n\t};\n\tvoid refreshEmojiStatusCollectibles();\n\tvoid refreshMegagroupStickers(\n\t\tFn<void(uint64 setId, bool installed)> push,\n\t\tGroupStickersPlace place);\n\tvoid unloadNotSeenCustom(int visibleTop, int visibleBottom);\n\tvoid unloadAllCustom();\n\tvoid unloadCustomIn(const SectionInfo &info);\n\n\tvoid setupSearch();\n\t[[nodiscard]] std::vector<EmojiPtr> collectPlainSearchResults();\n\tvoid appendPremiumSearchResults();\n\tvoid appendLocalPackSearchResults();\n\tvoid sendSearchRequest();\n\tvoid sendSearchSetsRequest(const QString &query);\n\tvoid requestSearchCloud(\n\t\tconst QString &query,\n\t\tint offset,\n\t\tbool fallbackToEmpty);\n\tvoid cancelSearchRequest();\n\tvoid toggleSearchLoading(bool loading);\n\tvoid searchCloudResultsDone(\n\t\tconst QString &query,\n\t\tint requestedOffset,\n\t\tconst MTPmessages_FoundStickers &result);\n\tvoid loadMoreSearchCloud();\n\tvoid checkPaginateSearchCloud(int visibleTop, int visibleBottom);\n\tvoid searchSetsResultsDone(\n\t\tconst QString &query,\n\t\tconst MTPmessages_FoundStickerSets &result);\n\tvoid showSearchResults();\n\tvoid fillCloudSearchResults();\n\tvoid fillCloudSearchSets();\n\t[[nodiscard]] CustomSet &searchSetBySection(int section);\n\t[[nodiscard]] const CustomSet &searchSetBySection(int section) const;\n\tvoid ensureLoaded(int section);\n\tvoid updateSelected();\n\tvoid setSelected(OverState newSelected);\n\tvoid setPressed(OverState newPressed);\n\n\tvoid fillRecentMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tint section,\n\t\tint index);\n\tvoid fillEmojiStatusMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tint section,\n\t\tint index);\n\n\t[[nodiscard]] EmojiPtr lookupOverEmoji(const OverEmoji *over) const;\n\t[[nodiscard]] ResolvedCustom lookupCustomEmoji(\n\t\tconst OverEmoji *over) const;\n\t[[nodiscard]] ResolvedCustom lookupCustomEmoji(\n\t\tint index,\n\t\tint section) const;\n\t[[nodiscard]] EmojiChosen lookupChosen(\n\t\tEmojiPtr emoji,\n\t\tnot_null<const OverEmoji*> over);\n\t[[nodiscard]] FileChosen lookupChosen(\n\t\tResolvedCustom custom,\n\t\tconst OverEmoji *over,\n\t\tApi::SendOptions options = Api::SendOptions());\n\tvoid selectEmoji(EmojiChosen data);\n\tvoid selectCustom(FileChosen data);\n\tvoid paint(Painter &p, ExpandingContext context, QRect clip);\n\tvoid drawCollapsedBadge(QPainter &p, QPoint position, int count);\n\tvoid drawRecent(\n\t\tQPainter &p,\n\t\tconst ExpandingContext &context,\n\t\tQPoint position,\n\t\tconst RecentOne &recent);\n\tvoid drawEmoji(\n\t\tQPainter &p,\n\t\tconst ExpandingContext &context,\n\t\tQPoint position,\n\t\tEmojiPtr emoji);\n\tvoid drawCustom(\n\t\tQPainter &p,\n\t\tconst ExpandingContext &context,\n\t\tQPoint position,\n\t\tint set,\n\t\tint index);\n\tvoid drawSearchSetCustom(\n\t\tQPainter &p,\n\t\tconst ExpandingContext &context,\n\t\tQPoint position,\n\t\tint section,\n\t\tint index);\n\tvoid validateEmojiPaintContext(const ExpandingContext &context);\n\t[[nodiscard]] bool hasColorButton(int index) const;\n\t[[nodiscard]] QRect colorButtonRect(int index) const;\n\t[[nodiscard]] QRect colorButtonRect(const SectionInfo &info) const;\n\t[[nodiscard]] bool hasRemoveButton(int index) const;\n\t[[nodiscard]] QRect removeButtonRect(int index) const;\n\t[[nodiscard]] QRect removeButtonRect(const SectionInfo &info) const;\n\t[[nodiscard]] bool hasAddButton(int index) const;\n\t[[nodiscard]] QRect addButtonRect(int index) const;\n\t[[nodiscard]] bool hasUnlockButton(int index) const;\n\t[[nodiscard]] QRect unlockButtonRect(int index) const;\n\t[[nodiscard]] bool hasButton(int index) const;\n\t[[nodiscard]] QRect buttonRect(int index) const;\n\t[[nodiscard]] QRect buttonRect(\n\t\tconst SectionInfo &info,\n\t\tconst RightButton &button) const;\n\t[[nodiscard]] const RightButton &rightButton(int index) const;\n\t[[nodiscard]] QRect emojiRect(int section, int index) const;\n\t[[nodiscard]] int emojiRight() const;\n\t[[nodiscard]] int emojiLeft() const;\n\t[[nodiscard]] uint64 sectionSetId(int section) const;\n\t[[nodiscard]] std::vector<StickerIcon> fillIcons();\n\tint paintButtonGetWidth(\n\t\tQPainter &p,\n\t\tconst SectionInfo &info,\n\t\tbool selected,\n\t\tQRect clip) const;\n\tvoid paintEmptySearchResults(Painter &p);\n\n\tvoid displaySet(uint64 setId);\n\tvoid removeSet(uint64 setId);\n\tvoid removeMegagroupSet(bool locally);\n\n\tvoid initButton(RightButton &button, const QString &text, bool gradient);\n\t[[nodiscard]] std::unique_ptr<Ui::RippleAnimation> createButtonRipple(\n\t\tint section);\n\t[[nodiscard]] QPoint buttonRippleTopLeft(int section) const;\n\t[[nodiscard]] PowerSaving::Flag powerSavingFlag() const;\n\n\tvoid repaintCustom(uint64 setId);\n\n\tvoid fillRecent();\n\tvoid fillRecentFrom(const std::vector<EmojiStatusId> &list);\n\t[[nodiscard]] not_null<Ui::Text::CustomEmoji*> resolveCustomEmoji(\n\t\tEmojiStatusId id,\n\t\tnot_null<DocumentData*> document,\n\t\tuint64 setId);\n\t[[nodiscard]] Ui::Text::CustomEmoji *resolveCustomRecent(\n\t\tCore::RecentEmojiId customId);\n\t[[nodiscard]] not_null<Ui::Text::CustomEmoji*> resolveCustomRecent(\n\t\tDocumentId documentId);\n\t[[nodiscard]] not_null<Ui::Text::CustomEmoji*> resolveCustomRecent(\n\t\tEmojiStatusId id);\n\t[[nodiscard]] Fn<void()> repaintCallback(\n\t\tDocumentId documentId,\n\t\tuint64 setId);\n\n\tvoid showPreview();\n\tvoid showPreviewFor(not_null<DocumentData*> document);\n\tvoid ensureMediaPreview();\n\n\tvoid applyNextSearchQuery();\n\n\tconst std::shared_ptr<Show> _show;\n\tconst ComposeFeatures _features;\n\tconst bool _onlyUnicodeEmoji;\n\tMode _mode = Mode::Full;\n\tQWidget *_mediaPreviewParent = nullptr;\n\tQMargins _mediaPreviewMargins;\n\tstd::unique_ptr<Ui::TabbedSearch> _search;\n\tMTP::Sender _api;\n\tconst int _staticCount = 0;\n\tStickersListFooter *_footer = nullptr;\n\tstd::unique_ptr<GradientPremiumStar> _premiumIcon;\n\tstd::unique_ptr<LocalStickersManager> _localSetsManager;\n\tChannelData *_megagroupSet = nullptr;\n\tuint64 _megagroupSetIdRequested = 0;\n\tFn<std::unique_ptr<Ui::Text::CustomEmoji>(\n\t\tDocumentId,\n\t\tFn<void()>)> _customRecentFactory;\n\n\tint _counts[kEmojiSectionCount];\n\tstd::vector<RecentOne> _recent;\n\tbase::flat_set<DocumentId> _recentCustomIds;\n\tbase::flat_set<DocumentId> _freeEffects;\n\tbase::flat_set<uint64> _repaintsScheduled;\n\trpl::variable<int> _recentShownCount;\n\tstd::unique_ptr<Ui::Text::CustomEmojiPaintContext> _emojiPaintContext;\n\tbool _recentPainted = false;\n\tbool _grabbingChosen = false;\n\tbool _paintAsPremium = false;\n\tQVector<EmojiPtr> _emoji[kEmojiSectionCount];\n\tstd::vector<CustomSet> _custom;\n\tbase::flat_set<DocumentId> _restrictedCustomList;\n\tstd::map<EmojiStatusId, CustomEmojiInstance> _customEmoji;\n\tbase::flat_map<\n\t\tDocumentId,\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji>> _customRecent;\n\tFn<QColor()> _customTextColor;\n\tint _customSingleSize = 0;\n\tbool _allowWithoutPremium = false;\n\tUi::RoundRect _overBg;\n\tQImage _searchExpandCache;\n\n\tstd::unique_ptr<StickerPremiumMark> _premiumMark;\n\tQImage _premiumMarkFrameCache;\n\tmutable std::unique_ptr<Ui::RippleAnimation> _colorAllRipple;\n\tbool _colorAllRippleForced = false;\n\trpl::lifetime _colorAllRippleForcedLifetime;\n\n\trpl::event_stream<std::vector<QString>> _searchQueries;\n\tstd::vector<QString> _nextSearchQuery;\n\tstd::vector<QString> _searchQuery;\n\tQString _searchQueryText;\n\tbase::flat_set<EmojiPtr> _searchEmoji;\n\tbase::flat_set<EmojiPtr> _searchEmojiPrevious;\n\tbase::flat_set<DocumentId> _searchCustomIds;\n\tstd::vector<RecentOne> _searchResults;\n\tbool _searchMode = false;\n\tstd::map<QString, std::vector<DocumentId>> _searchCloudCache;\n\tstd::map<QString, int> _searchCloudNextOffset;\n\tstd::map<QString, std::vector<uint64>> _searchSetsCache;\n\tstd::vector<CustomSet> _searchSets;\n\tQString _searchRequestQuery;\n\tQString _searchNextRequestQuery;\n\tQString _searchEmoticon;\n\tmtpRequestId _searchCloudRequestId = 0;\n\tmtpRequestId _searchSetsRequestId = 0;\n\tbool _searchLoading = false;\n\n\tint _rowsTop = 0;\n\tint _rowsLeft = 0;\n\tint _columnCount = 1;\n\tQSize _singleSize;\n\tQPoint _areaPosition;\n\tQPoint _innerPosition;\n\tQPoint _customPosition;\n\n\tRightButton _add;\n\tRightButton _unlock;\n\tRightButton _restore;\n\tUi::RoundRect _collapsedBg;\n\n\tOverState _selected;\n\tOverState _pressed;\n\tOverState _pickerSelected;\n\tQPoint _lastMousePos;\n\n\tbase::Timer _searchRequestTimer;\n\tobject_ptr<EmojiColorPicker> _picker;\n\tbase::Timer _showPickerTimer;\n\tbase::Timer _previewTimer;\n\tbool _previewShown = false;\n\n\n\tbase::unique_qptr<Window::MediaPreviewWidget> _mediaPreview;\n\n\trpl::event_stream<EmojiChosen> _chosen;\n\trpl::event_stream<FileChosen> _customChosen;\n\trpl::event_stream<> _jumpedToPremium;\n\n};\n\ntr::phrase<> EmojiCategoryTitle(int index);\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/emoji_sets_manager.h\"\n\n#include \"mtproto/dedicated_file_loader.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"core/application.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"storage/storage_cloud_blob.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Ui {\nnamespace Emoji {\nnamespace {\n\nusing namespace Storage::CloudBlob;\n\nstruct Set : public Blob {\n\tQString previewPath;\n};\n\ninline auto PreviewPath(int i) {\n\treturn u\":/gui/emoji/set%1_preview.webp\"_q.arg(i);\n}\n\nconst auto kSets = {\n\tSet{ { 0,    0,         0, \"Mac\" },       PreviewPath(0) },\n\tSet{ { 1, 2774, 8'455'034, \"Android\" },   PreviewPath(1) },\n\tSet{ { 2, 2775, 5'713'503, \"Twemoji\" },   PreviewPath(2) },\n\tSet{ { 3, 2776, 7'347'332, \"JoyPixels\" }, PreviewPath(3) },\n};\n\nusing Loading = MTP::DedicatedLoader::Progress;\nusing SetState = BlobState;\n\nclass Loader final : public BlobLoader {\npublic:\n\tLoader(\n\t\tnot_null<Main::Session*> session,\n\t\tint id,\n\t\tMTP::DedicatedLoader::Location location,\n\t\tconst QString &folder,\n\t\tint size);\n\n\tvoid destroy() override;\n\tvoid unpack(const QString &path) override;\n\nprivate:\n\tvoid fail() override;\n\n};\n\nclass Inner : public Ui::RpWidget {\npublic:\n\tInner(QWidget *parent, not_null<Main::Session*> session);\n\nprivate:\n\tvoid setupContent();\n\n\tconst not_null<Main::Session*> _session;\n\n};\n\nclass Row : public Ui::RippleButton {\npublic:\n\tRow(QWidget *widget, not_null<Main::Session*> session, const Set &set);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid onStateChanged(State was, StateChangeSource source) override;\n\nprivate:\n\t[[nodiscard]] bool showOver() const;\n\t[[nodiscard]] bool showOver(State state) const;\n\tvoid updateStatusColorOverride();\n\tvoid setupContent(const Set &set);\n\tvoid setupLabels(const Set &set);\n\tvoid setupPreview(const Set &set);\n\tvoid setupAnimation();\n\tvoid paintPreview(QPainter &p) const;\n\tvoid paintRadio(QPainter &p);\n\tvoid setupHandler();\n\tvoid load();\n\tvoid radialAnimationCallback(crl::time now);\n\tvoid updateLoadingToFinished();\n\n\tconst not_null<Main::Session*> _session;\n\tint _id = 0;\n\tbool _switching = false;\n\trpl::variable<SetState> _state;\n\tUi::FlatLabel *_status = nullptr;\n\tstd::array<QPixmap, 4> _preview;\n\tUi::Animations::Simple _toggled;\n\tUi::Animations::Simple _active;\n\tstd::unique_ptr<Ui::RadialAnimation> _loading;\n\n};\n\nbase::unique_qptr<Loader> GlobalLoader;\nrpl::event_stream<Loader*> GlobalLoaderValues;\n\nvoid SetGlobalLoader(base::unique_qptr<Loader> loader) {\n\tGlobalLoader = std::move(loader);\n\tGlobalLoaderValues.fire(GlobalLoader.get());\n}\n\nint64 GetDownloadSize(int id) {\n\treturn ranges::find(kSets, id, &Set::id)->size;\n}\n\n[[nodiscard]] float64 CountProgress(not_null<const Loading*> loading) {\n\treturn (loading->size > 0)\n\t\t? (loading->already / float64(loading->size))\n\t\t: 0.;\n}\n\nMTP::DedicatedLoader::Location GetDownloadLocation(int id) {\n\tconst auto username = kCloudLocationUsername.utf16();\n\tconst auto i = ranges::find(kSets, id, &Set::id);\n\treturn MTP::DedicatedLoader::Location{ username, i->postId };\n}\n\nSetState ComputeState(int id) {\n\tif (id == CurrentSetId()) {\n\t\treturn Active();\n\t} else if (SetIsReady(id)) {\n\t\treturn Ready();\n\t}\n\treturn Available{ GetDownloadSize(id) };\n}\n\nQString StateDescription(const SetState &state) {\n\treturn StateDescription(\n\t\tstate,\n\t\ttr::lng_emoji_set_active);\n}\n\nbool GoodSetPartName(const QString &name) {\n\treturn (name == u\"config.json\"_q)\n\t\t|| (name.startsWith(u\"emoji_\"_q) && name.endsWith(u\".webp\"_q));\n}\n\nbool UnpackSet(const QString &path, const QString &folder) {\n\treturn UnpackBlob(path, folder, GoodSetPartName);\n}\n\n\nLoader::Loader(\n\tnot_null<Main::Session*> session,\n\tint id,\n\tMTP::DedicatedLoader::Location location,\n\tconst QString &folder,\n\tint size)\n: BlobLoader(nullptr, session, id, location, folder, size) {\n}\n\nvoid Loader::unpack(const QString &path) {\n\tconst auto folder = internal::SetDataPath(id());\n\tconst auto weak = base::make_weak(this);\n\tcrl::async([=] {\n\t\tif (UnpackSet(path, folder)) {\n\t\t\tQFile(path).remove();\n\t\t\tSwitchToSet(id(), crl::guard(weak, [=](bool success) {\n\t\t\t\tif (success) {\n\t\t\t\t\tdestroy();\n\t\t\t\t} else {\n\t\t\t\t\tfail();\n\t\t\t\t}\n\t\t\t}));\n\t\t} else {\n\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\tfail();\n\t\t\t});\n\t\t}\n\t});\n}\n\nvoid Loader::destroy() {\n\tExpects(GlobalLoader == this);\n\n\tSetGlobalLoader(nullptr);\n}\n\nvoid Loader::fail() {\n\tClearNeedSwitchToId();\n\tBlobLoader::fail();\n}\n\nInner::Inner(QWidget *parent, not_null<Main::Session*> session)\n: RpWidget(parent)\n, _session(session) {\n\tsetupContent();\n}\n\nvoid Inner::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tfor (const auto &set : kSets) {\n\t\tcontent->add(object_ptr<Row>(content, _session, set));\n\t}\n\n\tcontent->resizeToWidth(st::boxWidth);\n\tUi::ResizeFitChild(this, content);\n}\n\nRow::Row(QWidget *widget, not_null<Main::Session*> session, const Set &set)\n: RippleButton(widget, st::defaultRippleAnimation)\n, _session(session)\n, _id(set.id)\n, _state(Available{ set.size }) {\n\tsetupContent(set);\n\tsetupHandler();\n}\n\nvoid Row::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto over = showOver();\n\tconst auto bg = over ? st::windowBgOver : st::windowBg;\n\tp.fillRect(rect(), bg);\n\n\tpaintRipple(p, 0, 0);\n\tpaintPreview(p);\n\tpaintRadio(p);\n}\n\nvoid Row::paintPreview(QPainter &p) const {\n\tconst auto x = st::manageEmojiPreviewPadding.left();\n\tconst auto y = st::manageEmojiPreviewPadding.top();\n\tconst auto width = st::manageEmojiPreviewWidth;\n\tconst auto height = st::manageEmojiPreviewWidth;\n\tauto &&preview = ranges::views::zip(_preview, ranges::views::ints(0, int(_preview.size())));\n\tfor (const auto &[pixmap, index] : preview) {\n\t\tconst auto row = (index / 2);\n\t\tconst auto column = (index % 2);\n\t\tconst auto left = x + (column ? width - st::manageEmojiPreview : 0);\n\t\tconst auto top = y + (row ? height - st::manageEmojiPreview : 0);\n\t\tp.drawPixmap(left, top, pixmap);\n\t}\n}\n\nvoid Row::paintRadio(QPainter &p) {\n\tif (_loading && !_loading->animating()) {\n\t\t_loading = nullptr;\n\t}\n\tconst auto loading = _loading\n\t\t? _loading->computeState()\n\t\t: Ui::RadialState{ 0., 0, arc::kFullLength };\n\tconst auto isToggledSet = v::is<Active>(_state.current());\n\tconst auto isActiveSet = isToggledSet || v::is<Loading>(_state.current());\n\tconst auto toggled = _toggled.value(isToggledSet ? 1. : 0.);\n\tconst auto active = _active.value(isActiveSet ? 1. : 0.);\n\tconst auto _st = &st::defaultRadio;\n\n\tPainterHighQualityEnabler hq(p);\n\n\tconst auto left = width()\n\t\t- st::manageEmojiMarginRight\n\t\t- _st->diameter\n\t\t- _st->thickness;\n\tconst auto top = (height() - _st->diameter - _st->thickness) / 2;\n\tconst auto outerWidth = width();\n\n\tauto pen = anim::pen(_st->untoggledFg, _st->toggledFg, active);\n\tpen.setWidth(_st->thickness);\n\tpen.setCapStyle(Qt::RoundCap);\n\tp.setPen(pen);\n\tp.setBrush(_st->bg);\n\tconst auto rect = style::rtlrect(QRectF(\n\t\tleft,\n\t\ttop,\n\t\t_st->diameter,\n\t\t_st->diameter\n\t).marginsRemoved(QMarginsF(\n\t\t_st->thickness / 2.,\n\t\t_st->thickness / 2.,\n\t\t_st->thickness / 2.,\n\t\t_st->thickness / 2.\n\t)), outerWidth);\n\tif (loading.shown > 0 && anim::Disabled()) {\n\t\tanim::DrawStaticLoading(\n\t\t\tp,\n\t\t\trect,\n\t\t\t_st->thickness,\n\t\t\tpen.color(),\n\t\t\t_st->bg);\n\t} else if (loading.arcLength < arc::kFullLength) {\n\t\tp.drawArc(rect, loading.arcFrom, loading.arcLength);\n\t} else {\n\t\tp.drawEllipse(rect);\n\t}\n\n\tif (toggled > 0 && (!_loading || !anim::Disabled())) {\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(anim::brush(_st->untoggledFg, _st->toggledFg, toggled));\n\n\t\tconst auto skip0 = _st->diameter / 2.;\n\t\tconst auto skip1 = _st->skip / 10.;\n\t\tconst auto checkSkip = skip0 * (1. - toggled) + skip1 * toggled;\n\t\tp.drawEllipse(style::rtlrect(QRectF(\n\t\t\tleft,\n\t\t\ttop,\n\t\t\t_st->diameter,\n\t\t\t_st->diameter\n\t\t).marginsRemoved(QMarginsF(\n\t\t\tcheckSkip,\n\t\t\tcheckSkip,\n\t\t\tcheckSkip,\n\t\t\tcheckSkip\n\t\t)), outerWidth));\n\t}\n}\n\nbool Row::showOver(State state) const {\n\treturn (!(state & StateFlag::Disabled))\n\t\t&& (state & (StateFlag::Over | StateFlag::Down));\n}\n\nbool Row::showOver() const {\n\treturn showOver(state());\n}\n\nvoid Row::onStateChanged(State was, StateChangeSource source) {\n\tRippleButton::onStateChanged(was, source);\n\tif (showOver() != showOver(was)) {\n\t\tupdateStatusColorOverride();\n\t}\n}\n\nvoid Row::updateStatusColorOverride() {\n\tconst auto isToggledSet = v::is<Active>(_state.current());\n\tconst auto toggled = _toggled.value(isToggledSet ? 1. : 0.);\n\tconst auto over = showOver();\n\tif (toggled == 0. && !over) {\n\t\t_status->setTextColorOverride(std::nullopt);\n\t} else {\n\t\t_status->setTextColorOverride(anim::color(\n\t\t\tover ? st::contactsStatusFgOver : st::contactsStatusFg,\n\t\t\tst::contactsStatusFgOnline,\n\t\t\ttoggled));\n\t}\n}\n\nvoid Row::setupContent(const Set &set) {\n\t_state = GlobalLoaderValues.events_starting_with(\n\t\tGlobalLoader.get()\n\t) | rpl::map([=](Loader *loader) {\n\t\treturn (loader && loader->id() == _id)\n\t\t\t? loader->state()\n\t\t\t: rpl::single(rpl::empty) | rpl::then(\n\t\t\t\tUpdated()\n\t\t\t) | rpl::map([=] {\n\t\t\t\treturn ComputeState(_id);\n\t\t\t});\n\t}) | rpl::flatten_latest(\n\t) | rpl::filter([=](const SetState &state) {\n\t\treturn !v::is<Failed>(_state.current())\n\t\t\t|| !v::is<Available>(state);\n\t});\n\n\tsetupLabels(set);\n\tsetupPreview(set);\n\tsetupAnimation();\n\n\tconst auto height = st::manageEmojiPreviewPadding.top()\n\t\t+ st::manageEmojiPreviewHeight\n\t\t+ st::manageEmojiPreviewPadding.bottom();\n\tresize(width(), height);\n}\n\nvoid Row::setupHandler() {\n\tclicks(\n\t) | rpl::filter([=] {\n\t\tconst auto &state = _state.current();\n\t\treturn !_switching && (v::is<Ready>(state)\n\t\t\t|| v::is<Available>(state));\n\t}) | rpl::on_next([=] {\n\t\tif (v::is<Available>(_state.current())) {\n\t\t\tload();\n\t\t\treturn;\n\t\t}\n\t\t_switching = true;\n\t\tSwitchToSet(_id, crl::guard(this, [=](bool success) {\n\t\t\t_switching = false;\n\t\t\tif (!success) {\n\t\t\t\tload();\n\t\t\t} else if (GlobalLoader && GlobalLoader->id() == _id) {\n\t\t\t\tGlobalLoader->destroy();\n\t\t\t}\n\t\t}));\n\t}, lifetime());\n\n\t_state.value(\n\t) | rpl::map([=](const SetState &state) {\n\t\treturn v::is<Ready>(state) || v::is<Available>(state);\n\t}) | rpl::on_next([=](bool active) {\n\t\tsetDisabled(!active);\n\t\tsetPointerCursor(active);\n\t}, lifetime());\n}\n\nvoid Row::load() {\n\tLoadAndSwitchTo(_session, _id);\n}\n\nvoid Row::setupLabels(const Set &set) {\n\tusing namespace rpl::mappers;\n\n\tconst auto name = Ui::CreateChild<Ui::FlatLabel>(\n\t\tthis,\n\t\tset.name,\n\t\tst::localStorageRowTitle);\n\tname->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_status = Ui::CreateChild<Ui::FlatLabel>(\n\t\tthis,\n\t\t_state.value() | rpl::map(StateDescription),\n\t\tst::localStorageRowSize);\n\t_status->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tsizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tconst auto left = st::manageEmojiPreviewPadding.left()\n\t\t\t+ st::manageEmojiPreviewWidth\n\t\t\t+ st::manageEmojiPreviewPadding.right();\n\t\tconst auto namey = st::manageEmojiPreviewPadding.top()\n\t\t\t+ st::manageEmojiNameTop;\n\t\tconst auto statusy = st::manageEmojiPreviewPadding.top()\n\t\t\t+ st::manageEmojiStatusTop;\n\t\tname->moveToLeft(left, namey);\n\t\t_status->moveToLeft(left, statusy);\n\t}, name->lifetime());\n}\n\nvoid Row::setupPreview(const Set &set) {\n\tconst auto size = st::manageEmojiPreview * style::DevicePixelRatio();\n\tconst auto original = QImage(set.previewPath);\n\tconst auto full = original.height();\n\tauto &&preview = ranges::views::zip(_preview, ranges::views::ints(0, int(_preview.size())));\n\tfor (auto &&[pixmap, index] : preview) {\n\t\tpixmap = Ui::PixmapFromImage(original.copy(\n\t\t\t{ full * index, 0, full, full }\n\t\t).scaledToWidth(size, Qt::SmoothTransformation));\n\t\tpixmap.setDevicePixelRatio(style::DevicePixelRatio());\n\t}\n}\n\nvoid Row::updateLoadingToFinished() {\n\t_loading->update(\n\t\tv::is<Failed>(_state.current()) ? 0. : 1.,\n\t\ttrue,\n\t\tcrl::now());\n}\n\nvoid Row::radialAnimationCallback(crl::time now) {\n\tconst auto updated = [&] {\n\t\tconst auto state = _state.current();\n\t\tif (const auto loading = std::get_if<Loading>(&state)) {\n\t\t\treturn _loading->update(CountProgress(loading), false, now);\n\t\t} else {\n\t\t\tupdateLoadingToFinished();\n\t\t}\n\t\treturn false;\n\t}();\n\tif (!anim::Disabled() || updated) {\n\t\tupdate();\n\t}\n}\n\nvoid Row::setupAnimation() {\n\tusing namespace rpl::mappers;\n\n\t_state.value(\n\t) | rpl::on_next([=](const SetState &state) {\n\t\tupdate();\n\t}, lifetime());\n\n\t_state.value(\n\t) | rpl::map(\n\t\t_1 == SetState{ Active() }\n\t) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](bool toggled) {\n\t\t_toggled.start(\n\t\t\t[=] { updateStatusColorOverride(); update(); },\n\t\t\ttoggled ? 0. : 1.,\n\t\t\ttoggled ? 1. : 0.,\n\t\t\tst::defaultRadio.duration);\n\t}, lifetime());\n\n\t_state.value(\n\t) | rpl::map([](const SetState &state) {\n\t\treturn v::is<Loading>(state) || v::is<Active>(state);\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](bool active) {\n\t\t_active.start(\n\t\t\t[=] { update(); },\n\t\t\tactive ? 0. : 1.,\n\t\t\tactive ? 1. : 0.,\n\t\t\tst::defaultRadio.duration);\n\t}, lifetime());\n\n\t_state.value(\n\t) | rpl::map([](const SetState &state) {\n\t\treturn std::get_if<Loading>(&state);\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](const Loading *loading) {\n\t\tif (loading && !_loading) {\n\t\t\t_loading = std::make_unique<Ui::RadialAnimation>(\n\t\t\t\t[=](crl::time now) { radialAnimationCallback(now); });\n\t\t\t_loading->start(CountProgress(loading));\n\t\t} else if (!loading && _loading) {\n\t\t\tupdateLoadingToFinished();\n\t\t}\n\t}, lifetime());\n\n\t_toggled.stop();\n\t_active.stop();\n\tupdateStatusColorOverride();\n}\n\n} // namespace\n\nManageSetsBox::ManageSetsBox(QWidget*, not_null<Main::Session*> session)\n: _session(session) {\n}\n\nvoid ManageSetsBox::prepare() {\n\tconst auto inner = setInnerWidget(object_ptr<Inner>(this, _session));\n\n\tsetTitle(tr::lng_emoji_manage_sets());\n\n\taddButton(tr::lng_close(), [=] { closeBox(); });\n\n\tsetDimensionsToContent(st::boxWidth, inner);\n}\n\nvoid LoadAndSwitchTo(not_null<Main::Session*> session, int id) {\n\tif (!ranges::contains(kSets, id, &Set::id)) {\n\t\tClearNeedSwitchToId();\n\t\treturn;\n\t}\n\tSetGlobalLoader(base::make_unique_q<Loader>(\n\t\tsession,\n\t\tid,\n\t\tGetDownloadLocation(id),\n\t\tinternal::SetDataPath(id),\n\t\tGetDownloadSize(id)));\n}\n\n} // namespace Emoji\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/emoji_sets_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nnamespace Emoji {\n\nclass ManageSetsBox final : public Ui::BoxContent {\npublic:\n\tManageSetsBox(QWidget*, not_null<Main::Session*> session);\n\nprivate:\n\tvoid prepare() override;\n\n\tconst not_null<Main::Session*> _session;\n\n};\n\nvoid LoadAndSwitchTo(not_null<Main::Session*> session, int id);\n\n} // namespace Emoji\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n\n#include \"chat_helpers/emoji_keywords.h\"\n#include \"core/core_settings.h\"\n#include \"core/application.h\"\n#include \"emoji_suggestions_helper.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/inner_dropdown.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/round_rect.h\"\n#include \"platform/platform_specific.h\"\n#include \"core/application.h\"\n#include \"base/event_filter.h\"\n#include \"base/integration.h\"\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"styles/style_chat_helpers.h\"\n\n#include <QtWidgets/QApplication>\n#include <QtGui/QTextBlock>\n\nnamespace Ui {\nnamespace Emoji {\nnamespace {\n\nconstexpr auto kShowExactDelay = crl::time(300);\nconstexpr auto kMaxNonScrolledEmoji = 7;\n\n} // namespace\n\nclass SuggestionsWidget final : public Ui::RpWidget {\npublic:\n\tSuggestionsWidget(\n\t\tQWidget *parent,\n\t\tconst style::EmojiSuggestions &st,\n\t\tnot_null<Main::Session*> session,\n\t\tbool suggestCustomEmoji,\n\t\tFn<bool(not_null<DocumentData*>)> allowCustomWithoutPremium);\n\t~SuggestionsWidget();\n\n\tvoid showWithQuery(SuggestionsQuery query, bool force = false);\n\tvoid selectFirstResult();\n\tbool handleKeyEvent(int key);\n\n\t[[nodiscard]] rpl::producer<bool> toggleAnimated() const;\n\n\tstruct Chosen {\n\t\tQString emoji;\n\t\tQString customData;\n\t};\n\t[[nodiscard]] rpl::producer<Chosen> triggered() const;\n\nprivate:\n\tstruct Row {\n\t\tRow(not_null<EmojiPtr> emoji, const QString &replacement);\n\n\t\tUi::Text::CustomEmoji *custom = nullptr;\n\t\tDocumentData *document = nullptr;\n\t\tnot_null<EmojiPtr> emoji;\n\t\tQString replacement;\n\t};\n\tstruct Custom {\n\t\tnot_null<DocumentData*> document;\n\t\tnot_null<EmojiPtr> emoji;\n\t\tQString replacement;\n\t};\n\n\tbool eventHook(QEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\n\tvoid scrollByWheelEvent(not_null<QWheelEvent*> e);\n\tvoid paintFadings(QPainter &p) const;\n\n\t[[nodiscard]] std::vector<Row> getRowsByQuery(const QString &text) const;\n\t[[nodiscard]] base::flat_multi_map<int, Custom> lookupCustom(\n\t\tconst std::vector<Row> &rows) const;\n\t[[nodiscard]] std::vector<Row> appendCustom(\n\t\tstd::vector<Row> rows);\n\t[[nodiscard]] std::vector<Row> appendCustom(\n\t\tstd::vector<Row> rows,\n\t\tconst base::flat_multi_map<int, Custom> &custom);\n\tvoid resizeToRows();\n\tvoid setSelected(\n\t\tint selected,\n\t\tanim::type animated = anim::type::instant);\n\tvoid setPressed(int pressed);\n\tvoid clearMouseSelection();\n\tvoid clearSelection();\n\tvoid updateSelectedItem();\n\tvoid updateItem(int index);\n\t[[nodiscard]] QRect inner() const;\n\t[[nodiscard]] QPoint innerShift() const;\n\t[[nodiscard]] QPoint mapToInner(QPoint globalPosition) const;\n\tvoid selectByMouse(QPoint globalPosition);\n\tbool triggerSelectedRow() const;\n\tvoid triggerRow(const Row &row) const;\n\n\t[[nodiscard]] int scrollCurrent() const;\n\tvoid scrollTo(int value, anim::type animated = anim::type::instant);\n\tvoid stopAnimations();\n\n\t[[nodiscard]] not_null<Ui::Text::CustomEmoji*> resolveCustomEmoji(\n\t\tnot_null<DocumentData*> document);\n\tvoid customEmojiRepaint();\n\n\tconst style::EmojiSuggestions &_st;\n\tconst not_null<Main::Session*> _session;\n\tSuggestionsQuery _query;\n\tstd::vector<Row> _rows;\n\tbool _suggestCustomEmoji = false;\n\tFn<bool(not_null<DocumentData*>)> _allowCustomWithoutPremium;\n\n\tUi::RoundRect _overRect;\n\n\tbase::flat_map<\n\t\tnot_null<DocumentData*>,\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji>> _customEmoji;\n\tbool _repaintScheduled = false;\n\n\tstd::optional<QPoint> _lastMousePosition;\n\tbool _mouseSelection = false;\n\tint _selected = -1;\n\tint _pressed = -1;\n\n\tint _scrollValue = 0;\n\tUi::Animations::Simple _scrollAnimation;\n\tUi::Animations::Simple _selectedAnimation;\n\tint _scrollMax = 0;\n\tint _oneWidth = 0;\n\tQMargins _padding;\n\n\tQPoint _mousePressPosition;\n\tint _dragScrollStart = -1;\n\n\trpl::event_stream<bool> _toggleAnimated;\n\trpl::event_stream<Chosen> _triggered;\n\n};\n\nSuggestionsWidget::SuggestionsWidget(\n\tQWidget *parent,\n\tconst style::EmojiSuggestions &st,\n\tnot_null<Main::Session*> session,\n\tbool suggestCustomEmoji,\n\tFn<bool(not_null<DocumentData*>)> allowCustomWithoutPremium)\n: RpWidget(parent)\n, _st(st)\n, _session(session)\n, _suggestCustomEmoji(suggestCustomEmoji)\n, _allowCustomWithoutPremium(std::move(allowCustomWithoutPremium))\n, _overRect(st::roundRadiusLarge, _st.overBg)\n, _oneWidth(st::emojiSuggestionSize)\n, _padding(st::emojiSuggestionsPadding) {\n\tresize(\n\t\t_oneWidth + _padding.left() + _padding.right(),\n\t\t_oneWidth + _padding.top() + _padding.bottom());\n\tsetMouseTracking(true);\n}\n\nSuggestionsWidget::~SuggestionsWidget() = default;\n\nrpl::producer<bool> SuggestionsWidget::toggleAnimated() const {\n\treturn _toggleAnimated.events();\n}\n\nauto SuggestionsWidget::triggered() const -> rpl::producer<Chosen> {\n\treturn _triggered.events();\n}\n\nvoid SuggestionsWidget::showWithQuery(SuggestionsQuery query, bool force) {\n\tif (!force && (_query == query)) {\n\t\treturn;\n\t}\n\t_query = query;\n\tauto rows = [&] {\n\t\tif (const auto emoji = std::get_if<EmojiPtr>(&query)) {\n\t\t\treturn appendCustom(\n\t\t\t\t{},\n\t\t\t\tlookupCustom({ Row(*emoji, (*emoji)->text()) }));\n\t\t}\n\t\treturn appendCustom(getRowsByQuery(v::get<QString>(query)));\n\t}();\n\tif (rows.empty()) {\n\t\t_toggleAnimated.fire(false);\n\t}\n\tclearSelection();\n\tsetPressed(-1);\n\t_rows = std::move(rows);\n\tresizeToRows();\n\tupdate();\n\n\tUi::PostponeCall(this, [=] {\n\t\tif (!_rows.empty()) {\n\t\t\t_toggleAnimated.fire(true);\n\t\t}\n\t});\n}\n\nvoid SuggestionsWidget::selectFirstResult() {\n\tif (!_rows.empty() && _selected < 0) {\n\t\tsetSelected(0);\n\t}\n}\n\nauto SuggestionsWidget::appendCustom(std::vector<Row> rows)\n-> std::vector<Row> {\n\tconst auto custom = lookupCustom(rows);\n\treturn appendCustom(std::move(rows), custom);\n}\n\nauto SuggestionsWidget::lookupCustom(const std::vector<Row> &rows) const\n-> base::flat_multi_map<int, Custom> {\n\tif (rows.empty()\n\t\t|| !_suggestCustomEmoji\n\t\t|| !Core::App().settings().suggestAnimatedEmoji()) {\n\t\treturn {};\n\t}\n\tauto custom = base::flat_multi_map<int, Custom>();\n\tconst auto premium = _session->premium();\n\tconst auto stickers = &_session->data().stickers();\n\tfor (const auto setId : stickers->emojiSetsOrder()) {\n\t\tconst auto i = stickers->sets().find(setId);\n\t\tif (i == end(stickers->sets())) {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const auto &document : i->second->stickers) {\n\t\t\tif (!premium\n\t\t\t\t&& document->isPremiumEmoji()\n\t\t\t\t&& (!_allowCustomWithoutPremium\n\t\t\t\t\t|| !_allowCustomWithoutPremium(document))) {\n\t\t\t\t// Skip the whole premium emoji set.\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (const auto sticker = document->sticker()) {\n\t\t\t\tif (const auto emoji = Ui::Emoji::Find(sticker->alt)) {\n\t\t\t\t\tconst auto original = emoji->original();\n\t\t\t\t\tconst auto j = ranges::find_if(\n\t\t\t\t\t\trows,\n\t\t\t\t\t\t[&](const Row &row) {\n\t\t\t\t\t\t\treturn row.emoji->original() == original;\n\t\t\t\t\t\t});\n\t\t\t\t\tif (j != end(rows)) {\n\t\t\t\t\t\tcustom.emplace(int(j - begin(rows)), Custom{\n\t\t\t\t\t\t\t.document = document,\n\t\t\t\t\t\t\t.emoji = emoji,\n\t\t\t\t\t\t\t.replacement = j->replacement,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn custom;\n}\n\nauto SuggestionsWidget::appendCustom(\n\tstd::vector<Row> rows,\n\tconst base::flat_multi_map<int, Custom> &custom)\n-> std::vector<Row> {\n\trows.reserve(rows.size() + custom.size());\n\tfor (const auto &[position, one] : custom) {\n\t\trows.push_back(Row(one.emoji, one.replacement));\n\t\trows.back().document = one.document;\n\t\trows.back().custom = resolveCustomEmoji(one.document);\n\t}\n\treturn rows;\n}\n\nnot_null<Ui::Text::CustomEmoji*> SuggestionsWidget::resolveCustomEmoji(\n\t\tnot_null<DocumentData*> document) {\n\tconst auto i = _customEmoji.find(document);\n\tif (i != end(_customEmoji)) {\n\t\treturn i->second.get();\n\t}\n\tauto emoji = document->session().data().customEmojiManager().create(\n\t\tdocument,\n\t\t[=] { customEmojiRepaint(); },\n\t\tData::CustomEmojiManager::SizeTag::Large);\n\treturn _customEmoji.emplace(\n\t\tdocument,\n\t\tstd::move(emoji)\n\t).first->second.get();\n}\n\nvoid SuggestionsWidget::customEmojiRepaint() {\n\tif (_repaintScheduled) {\n\t\treturn;\n\t}\n\t_repaintScheduled = true;\n\tupdate();\n}\n\nSuggestionsWidget::Row::Row(\n\tnot_null<EmojiPtr> emoji,\n\tconst QString &replacement)\n: emoji(emoji)\n, replacement(replacement) {\n}\n\nauto SuggestionsWidget::getRowsByQuery(const QString &text) const\n-> std::vector<Row> {\n\tif (text.isEmpty()) {\n\t\treturn {};\n\t}\n\tconst auto middle = (text[0] == ':');\n\tconst auto real = middle ? text.mid(1) : text;\n\tconst auto simple = [&] {\n\t\tif (!middle || text.size() > 2) {\n\t\t\treturn false;\n\t\t}\n\t\t// Suggest :D and :-P only as exact matches.\n\t\treturn ranges::none_of(text, [](QChar ch) { return ch.isLower(); });\n\t}();\n\tconst auto exact = !middle || simple;\n\tconst auto list = Core::App().emojiKeywords().queryMine(real, exact);\n\tusing Entry = ChatHelpers::EmojiKeywords::Result;\n\treturn ranges::views::all(\n\t\tlist\n\t) | ranges::views::transform([](const Entry &result) {\n\t\treturn Row(result.emoji, result.replacement);\n\t}) | ranges::to_vector;\n}\n\nvoid SuggestionsWidget::resizeToRows() {\n\tconst auto count = int(_rows.size());\n\tconst auto scrolled = (count > kMaxNonScrolledEmoji);\n\tconst auto fullWidth = count * _oneWidth;\n\tconst auto newWidth = scrolled\n\t\t? st::emojiSuggestionsScrolledWidth\n\t\t: fullWidth;\n\t_scrollMax = std::max(0, fullWidth - newWidth);\n\tif (_scrollValue > _scrollMax || scrollCurrent() > _scrollMax) {\n\t\tscrollTo(std::min(_scrollValue, _scrollMax));\n\t}\n\tresize(_padding.left() + newWidth + _padding.right(), height());\n\tupdate();\n}\n\nbool SuggestionsWidget::eventHook(QEvent *e) {\n\tif (e->type() == QEvent::Wheel) {\n\t\tselectByMouse(QCursor::pos());\n\t\tif (_selected >= 0 && _pressed < 0) {\n\t\t\tscrollByWheelEvent(static_cast<QWheelEvent*>(e));\n\t\t}\n\t}\n\treturn RpWidget::eventHook(e);\n}\n\nvoid SuggestionsWidget::scrollByWheelEvent(not_null<QWheelEvent*> e) {\n\tconst auto horizontal = (e->angleDelta().x() != 0);\n\tconst auto vertical = (e->angleDelta().y() != 0);\n\tconst auto current = scrollCurrent();\n\tconst auto scroll = [&] {\n\t\tif (horizontal) {\n\t\t\tconst auto delta = e->pixelDelta().x()\n\t\t\t\t? e->pixelDelta().x()\n\t\t\t\t: e->angleDelta().x();\n\t\t\treturn std::clamp(\n\t\t\t\tcurrent - ((rtl() ? -1 : 1) * delta),\n\t\t\t\t0,\n\t\t\t\t_scrollMax);\n\t\t} else if (vertical) {\n\t\t\tconst auto delta = e->pixelDelta().y()\n\t\t\t\t? e->pixelDelta().y()\n\t\t\t\t: e->angleDelta().y();\n\t\t\treturn std::clamp(current - delta, 0, _scrollMax);\n\t\t}\n\t\treturn current;\n\t}();\n\tif (current != scroll) {\n\t\tscrollTo(scroll);\n\t\tif (!_lastMousePosition) {\n\t\t\t_lastMousePosition = QCursor::pos();\n\t\t}\n\t\tselectByMouse(*_lastMousePosition);\n\t\tupdate();\n\t}\n}\n\nvoid SuggestionsWidget::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\t_repaintScheduled = false;\n\n\tconst auto clip = e->rect();\n\tp.fillRect(clip, _st.bg);\n\n\tconst auto shift = innerShift();\n\tp.translate(-shift);\n\tconst auto paint = clip.translated(shift);\n\tconst auto from = std::max(paint.x(), 0) / _oneWidth;\n\tconst auto till = std::min(\n\t\t(paint.x() + paint.width() + _oneWidth - 1) / _oneWidth,\n\t\tint(_rows.size()));\n\n\tconst auto selected = (_pressed >= 0)\n\t\t? _pressed\n\t\t: _selectedAnimation.value(_selected);\n\tif (selected > -1.) {\n\t\t_overRect.paint(\n\t\t\tp,\n\t\t\tQRect(selected * _oneWidth, 0, _oneWidth, _oneWidth));\n\t}\n\n\tauto context = Ui::CustomEmoji::Context{\n\t\t.textColor = _st.textFg->c,\n\t\t.now = crl::now(),\n\t};\n\tfor (auto i = from; i != till; ++i) {\n\t\tconst auto &row = _rows[i];\n\t\tconst auto emoji = row.emoji;\n\t\tconst auto esize = Ui::Emoji::GetSizeLarge();\n\t\tconst auto size = esize / style::DevicePixelRatio();\n\t\tconst auto x = i * _oneWidth + (_oneWidth - size) / 2;\n\t\tconst auto y = (_oneWidth - size) / 2;\n\t\tif (row.custom) {\n\t\t\tcontext.position = { x, y };\n\t\t\trow.custom->paint(p, context);\n\t\t} else {\n\t\t\tUi::Emoji::Draw(p, emoji, esize, x, y);\n\t\t}\n\t}\n\tpaintFadings(p);\n}\n\nvoid SuggestionsWidget::paintFadings(QPainter &p) const {\n\tconst auto scroll = scrollCurrent();\n\tconst auto o_left = std::clamp(\n\t\tscroll / float64(st::emojiSuggestionsFadeAfter),\n\t\t0.,\n\t\t1.);\n\tconst auto shift = innerShift();\n\tif (o_left > 0.) {\n\t\tp.setOpacity(o_left);\n\t\tconst auto rect = myrtlrect(\n\t\t\tshift.x(),\n\t\t\t0,\n\t\t\t_st.fadeLeft.width(),\n\t\t\theight());\n\t\t_st.fadeLeft.fill(p, rect);\n\t\tp.setOpacity(1.);\n\t}\n\tconst auto o_right = std::clamp(\n\t\t(_scrollMax - scroll) / float64(st::emojiSuggestionsFadeAfter),\n\t\t0.,\n\t\t1.);\n\tif (o_right > 0.) {\n\t\tp.setOpacity(o_right);\n\t\tconst auto rect = myrtlrect(\n\t\t\tshift.x() + width() - _st.fadeRight.width(),\n\t\t\t0,\n\t\t\t_st.fadeRight.width(),\n\t\t\theight());\n\t\t_st.fadeRight.fill(p, rect);\n\t\tp.setOpacity(1.);\n\t}\n}\n\nvoid SuggestionsWidget::keyPressEvent(QKeyEvent *e) {\n\thandleKeyEvent(e->key());\n}\n\nbool SuggestionsWidget::handleKeyEvent(int key) {\n\tif (key == Qt::Key_Enter || key == Qt::Key_Return) {\n\t\treturn triggerSelectedRow();\n\t} else if (key == Qt::Key_Tab) {\n\t\tif (_selected < 0 || _selected >= _rows.size()) {\n\t\t\tsetSelected(0);\n\t\t}\n\t\treturn triggerSelectedRow();\n\t} else if (_rows.empty()\n\t\t|| (key != Qt::Key_Up\n\t\t\t&& key != Qt::Key_Down\n\t\t\t&& key != Qt::Key_Left\n\t\t\t&& key != Qt::Key_Right)) {\n\t\treturn false;\n\t}\n\n\tconst auto delta = (key == Qt::Key_Down || key == Qt::Key_Right)\n\t\t? 1\n\t\t: -1;\n\tif (delta < 0 && _selected < 0) {\n\t\treturn false;\n\t}\n\tauto start = _selected;\n\tif (start < 0 || start >= _rows.size()) {\n\t\tstart = (delta > 0) ? (_rows.size() - 1) : 0;\n\t}\n\tauto newSelected = start + delta;\n\tif (newSelected < 0) {\n\t\tnewSelected = -1;\n\t} else if (newSelected >= _rows.size()) {\n\t\tnewSelected -= _rows.size();\n\t}\n\n\t_mouseSelection = false;\n\t_lastMousePosition = std::nullopt;\n\tsetSelected(newSelected, anim::type::normal);\n\treturn true;\n}\n\nvoid SuggestionsWidget::setSelected(int selected, anim::type animated) {\n\tif (selected >= _rows.size()) {\n\t\tselected = -1;\n\t}\n\tif (animated == anim::type::normal) {\n\t\t_selectedAnimation.start(\n\t\t\t[=] { update(); },\n\t\t\t_selected,\n\t\t\tselected,\n\t\t\tst::universalDuration,\n\t\t\tanim::sineInOut);\n\t\tif (_scrollMax > 0) {\n\t\t\tconst auto selectedMax = int(_rows.size()) - 3;\n\t\t\tconst auto selectedForScroll = std::min(\n\t\t\t\tstd::max(selected, 1) - 1,\n\t\t\t\tselectedMax);\n\t\t\tscrollTo((_scrollMax * selectedForScroll) / selectedMax, animated);\n\t\t}\n\t} else if (_selectedAnimation.animating()) {\n\t\t_selectedAnimation.stop();\n\t\tupdate();\n\t}\n\tif (_selected != selected) {\n\t\tupdateSelectedItem();\n\t\t_selected = selected;\n\t\tupdateSelectedItem();\n\t}\n}\n\nint SuggestionsWidget::scrollCurrent() const {\n\treturn _scrollAnimation.value(_scrollValue);\n}\n\nvoid SuggestionsWidget::scrollTo(int value, anim::type animated) {\n\tif (animated == anim::type::instant) {\n\t\t_scrollAnimation.stop();\n\t} else {\n\t\t_scrollAnimation.start(\n\t\t\t[=] { update(); },\n\t\t\t_scrollValue,\n\t\t\tvalue,\n\t\t\tst::universalDuration,\n\t\t\tanim::sineInOut);\n\t}\n\t_scrollValue = value;\n\tupdate();\n}\n\nvoid SuggestionsWidget::stopAnimations() {\n\t_scrollValue = _scrollAnimation.value(_scrollValue);\n\t_scrollAnimation.stop();\n}\n\nvoid SuggestionsWidget::setPressed(int pressed) {\n\tif (pressed >= _rows.size()) {\n\t\tpressed = -1;\n\t}\n\tif (_pressed != pressed) {\n\t\t_pressed = pressed;\n\t\tif (_pressed >= 0) {\n\t\t\t_mousePressPosition = QCursor::pos();\n\t\t}\n\t}\n}\n\nvoid SuggestionsWidget::clearMouseSelection() {\n\tif (_mouseSelection) {\n\t\tclearSelection();\n\t}\n}\n\nvoid SuggestionsWidget::clearSelection() {\n\t_mouseSelection = false;\n\t_lastMousePosition = std::nullopt;\n\tsetSelected(-1);\n}\n\nvoid SuggestionsWidget::updateItem(int index) {\n\tif (index >= 0 && index < _rows.size()) {\n\t\tupdate(\n\t\t\t_padding.left() + index * _oneWidth - scrollCurrent(),\n\t\t\t_padding.top(),\n\t\t\t_oneWidth,\n\t\t\t_oneWidth);\n\t}\n}\n\nvoid SuggestionsWidget::updateSelectedItem() {\n\tupdateItem(_selected);\n}\n\nQRect SuggestionsWidget::inner() const {\n\treturn QRect(0, 0, _rows.size() * _oneWidth, _oneWidth);\n}\n\nQPoint SuggestionsWidget::innerShift() const {\n\treturn QPoint(scrollCurrent() - _padding.left(), -_padding.top());\n}\n\nQPoint SuggestionsWidget::mapToInner(QPoint globalPosition) const {\n\treturn mapFromGlobal(globalPosition) + innerShift();\n}\n\nvoid SuggestionsWidget::mouseMoveEvent(QMouseEvent *e) {\n\tconst auto globalPosition = e->globalPos();\n\tif (_dragScrollStart >= 0) {\n\t\tconst auto delta = (_mousePressPosition.x() - globalPosition.x());\n\t\tconst auto scroll = std::clamp(\n\t\t\t_dragScrollStart + (rtl() ? -1 : 1) * delta,\n\t\t\t0,\n\t\t\t_scrollMax);\n\t\tif (scrollCurrent() != scroll) {\n\t\t\tscrollTo(scroll);\n\t\t\tupdate();\n\t\t}\n\t\treturn;\n\t} else if ((_pressed >= 0)\n\t\t&& (_scrollMax > 0)\n\t\t&& ((_mousePressPosition - globalPosition).manhattanLength()\n\t\t\t>= QApplication::startDragDistance())) {\n\t\t_dragScrollStart = scrollCurrent();\n\t\t_mousePressPosition = globalPosition;\n\t\tscrollTo(_dragScrollStart);\n\t}\n\tif (inner().contains(mapToInner(globalPosition))) {\n\t\tif (!_lastMousePosition) {\n\t\t\t_lastMousePosition = globalPosition;\n\t\t\treturn;\n\t\t} else if (!_mouseSelection\n\t\t\t&& *_lastMousePosition == globalPosition) {\n\t\t\treturn;\n\t\t}\n\t\tselectByMouse(globalPosition);\n\t} else {\n\t\tclearMouseSelection();\n\t}\n}\n\nvoid SuggestionsWidget::selectByMouse(QPoint globalPosition) {\n\t_mouseSelection = true;\n\t_lastMousePosition = globalPosition;\n\tconst auto p = mapToInner(globalPosition);\n\tconst auto selected = (p.x() >= 0) ? (p.x() / _oneWidth) : -1;\n\tsetSelected((selected >= 0 && selected < _rows.size()) ? selected : -1);\n}\n\nvoid SuggestionsWidget::mousePressEvent(QMouseEvent *e) {\n\tselectByMouse(e->globalPos());\n\tif (_selected >= 0) {\n\t\tsetPressed(_selected);\n\t}\n}\n\nvoid SuggestionsWidget::mouseReleaseEvent(QMouseEvent *e) {\n\tif (_pressed >= 0) {\n\t\tconst auto pressed = _pressed;\n\t\tsetPressed(-1);\n\t\tif (_dragScrollStart >= 0) {\n\t\t\t_dragScrollStart = -1;\n\t\t} else if (pressed == _selected) {\n\t\t\ttriggerRow(_rows[_selected]);\n\t\t}\n\t}\n}\n\nbool SuggestionsWidget::triggerSelectedRow() const {\n\tif (_selected >= 0) {\n\t\ttriggerRow(_rows[_selected]);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid SuggestionsWidget::triggerRow(const Row &row) const {\n\t_triggered.fire({\n\t\trow.emoji->text(),\n\t\t(row.document\n\t\t\t? Data::SerializeCustomEmojiId(row.document)\n\t\t\t: QString()),\n\t});\n}\n\nvoid SuggestionsWidget::enterEventHook(QEnterEvent *e) {\n\tif (!inner().contains(mapToInner(QCursor::pos()))) {\n\t\tclearMouseSelection();\n\t}\n\treturn RpWidget::enterEventHook(e);\n}\n\nvoid SuggestionsWidget::leaveEventHook(QEvent *e) {\n\tclearMouseSelection();\n\treturn RpWidget::leaveEventHook(e);\n}\n\nSuggestionsController::SuggestionsController(\n\tnot_null<QWidget*> parent,\n\tnot_null<QWidget*> outer,\n\tnot_null<QTextEdit*> field,\n\tnot_null<Main::Session*> session,\n\tconst Options &options)\n: QObject(parent)\n, _st(options.st ? *options.st : st::defaultEmojiSuggestions)\n, _field(field)\n, _session(session)\n, _showExactTimer([=] { showWithQuery(getEmojiQuery()); })\n, _options(options) {\n\t_container = base::make_unique_q<InnerDropdown>(outer, _st.dropdown);\n\t_container->setAutoHiding(false);\n\t_suggestions = _container->setOwnedWidget(\n\t\tobject_ptr<Ui::Emoji::SuggestionsWidget>(\n\t\t\t_container,\n\t\t\t_st,\n\t\t\tsession,\n\t\t\t_options.suggestCustomEmoji,\n\t\t\t_options.allowCustomWithoutPremium));\n\n\tsetReplaceCallback(nullptr);\n\n\tconst auto fieldCallback = [=](not_null<QEvent*> event) {\n\t\treturn (_container && fieldFilter(event))\n\t\t\t? base::EventFilterResult::Cancel\n\t\t\t: base::EventFilterResult::Continue;\n\t};\n\t_fieldFilter.reset(base::install_event_filter(_field, fieldCallback));\n\n\tconst auto outerCallback = [=](not_null<QEvent*> event) {\n\t\treturn (_container && outerFilter(event))\n\t\t\t? base::EventFilterResult::Cancel\n\t\t\t: base::EventFilterResult::Continue;\n\t};\n\t_outerFilter.reset(base::install_event_filter(outer, outerCallback));\n\n\tQObject::connect(_field, &QTextEdit::textChanged, _container, [=] {\n\t\tbase::Integration::Instance().enterFromEventLoop([&] {\n\t\t\thandleTextChange();\n\t\t});\n\t});\n\tQObject::connect(\n\t\t_field,\n\t\t&QTextEdit::cursorPositionChanged,\n\t\t_container,\n\t\t[=] { handleCursorPositionChange(); });\n\n\t_suggestions->toggleAnimated(\n\t) | rpl::on_next([=](bool visible) {\n\t\tsuggestionsUpdated(visible);\n\t}, _lifetime);\n\t_suggestions->triggered(\n\t) | rpl::on_next([=](const SuggestionsWidget::Chosen &chosen) {\n\t\treplaceCurrent(chosen.emoji, chosen.customData);\n\t}, _lifetime);\n\tCore::App().emojiKeywords().refreshed(\n\t) | rpl::on_next([=] {\n\t\t_keywordsRefreshed = true;\n\t\tif (!_showExactTimer.isActive()) {\n\t\t\tshowWithQuery(_lastShownQuery);\n\t\t}\n\t}, _lifetime);\n\n\tupdateForceHidden();\n\n\t_container->shownValue(\n\t) | rpl::filter([=](bool shown) {\n\t\treturn shown && !_shown;\n\t}) | rpl::on_next([=] {\n\t\t_container->hide();\n\t}, _container->lifetime());\n\n\thandleTextChange();\n}\n\nnot_null<SuggestionsController*> SuggestionsController::Init(\n\t\tnot_null<QWidget*> outer,\n\t\tnot_null<Ui::InputField*> field,\n\t\tnot_null<Main::Session*> session,\n\t\tconst Options &options) {\n\tconst auto result = Ui::CreateChild<SuggestionsController>(\n\t\tfield.get(),\n\t\touter,\n\t\tfield->rawTextEdit(),\n\t\tsession,\n\t\toptions);\n\tresult->setReplaceCallback([=](\n\t\t\tint from,\n\t\t\tint till,\n\t\t\tconst QString &replacement,\n\t\t\tconst QString &customEmojiData) {\n\t\tfield->commitInstantReplacement(\n\t\t\tfrom,\n\t\t\ttill,\n\t\t\treplacement,\n\t\t\tcustomEmojiData);\n\t});\n\treturn result;\n}\n\nvoid SuggestionsController::setReplaceCallback(\n\tFn<void(\n\t\tint from,\n\t\tint till,\n\t\tconst QString &replacement,\n\t\tconst QString &customEmojiData)> callback) {\n\tif (callback) {\n\t\t_replaceCallback = std::move(callback);\n\t} else {\n\t\t_replaceCallback = [=](\n\t\t\t\tint from,\n\t\t\t\tint till,\n\t\t\t\tconst QString &replacement,\n\t\t\t\tconst QString &customEmojiData) {\n\t\t\tauto cursor = _field->textCursor();\n\t\t\tcursor.setPosition(from);\n\t\t\tcursor.setPosition(till, QTextCursor::KeepAnchor);\n\t\t\tcursor.insertText(replacement);\n\t\t};\n\t}\n}\n\nvoid SuggestionsController::handleTextChange() {\n\tif (Core::App().settings().suggestEmoji()\n\t\t&& _field->textCursor().position() > 0) {\n\t\tCore::App().emojiKeywords().refresh();\n\t}\n\n\t_ignoreCursorPositionChange = true;\n\tInvokeQueued(_container, [=] { _ignoreCursorPositionChange = false; });\n\n\tconst auto query = getEmojiQuery();\n\tif (v::is<EmojiPtr>(query)) {\n\t\tshowWithQuery(query);\n\t\tInvokeQueued(_container, [=] {\n\t\t\tif (_shown) {\n\t\t\t\tupdateGeometry();\n\t\t\t}\n\t\t});\n\t\treturn;\n\t}\n\tconst auto text = v::get<QString>(query);\n\tif (text.isEmpty() || _textChangeAfterKeyPress) {\n\t\tconst auto exact = !text.isEmpty() && (text[0] != ':');\n\t\tif (exact) {\n\t\t\tconst auto hidden = _container->isHidden()\n\t\t\t\t|| _container->isHiding();\n\t\t\t_showExactTimer.callOnce(hidden ? kShowExactDelay : 0);\n\t\t} else {\n\t\t\tshowWithQuery(query);\n\t\t\t_suggestions->selectFirstResult();\n\t\t}\n\t}\n}\n\nvoid SuggestionsController::showWithQuery(SuggestionsQuery query) {\n\t_showExactTimer.cancel();\n\tconst auto force = base::take(_keywordsRefreshed);\n\t_lastShownQuery = query;\n\t_suggestions->showWithQuery(_lastShownQuery, force);\n\t_container->resizeToContent();\n}\n\nSuggestionsQuery SuggestionsController::getEmojiQuery() {\n\tif (!Core::App().settings().suggestEmoji()) {\n\t\treturn QString();\n\t}\n\n\tconst auto cursor = _field->textCursor();\n\tif (cursor.hasSelection()) {\n\t\treturn QString();\n\t}\n\n\tconst auto modernLimit = Core::App().emojiKeywords().maxQueryLength();\n\tconst auto legacyLimit = GetSuggestionMaxLength();\n\tconst auto position = cursor.position();\n\tconst auto findTextPart = [&]() -> SuggestionsQuery {\n\t\tauto previousFragmentStart = 0;\n\t\tauto previousFragmentName = QString();\n\t\tauto document = _field->document();\n\t\tauto block = document->findBlock(position);\n\t\tfor (auto i = block.begin(); !i.atEnd(); ++i) {\n\t\t\tauto fragment = i.fragment();\n\t\t\tif (!fragment.isValid()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tauto from = fragment.position();\n\t\t\tauto till = from + fragment.length();\n\t\t\tconst auto format = fragment.charFormat();\n\t\t\tif (format.objectType() == InputField::kCustomEmojiFormat) {\n\t\t\t\tpreviousFragmentName = QString();\n\t\t\t\tcontinue;\n\t\t\t} else if (format.isImageFormat()) {\n\t\t\t\tconst auto imageName = format.toImageFormat().name();\n\t\t\t\tif (from >= position || till < position) {\n\t\t\t\t\tpreviousFragmentStart = from;\n\t\t\t\t\tpreviousFragmentName = imageName;\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (const auto emoji = Emoji::FromUrl(imageName)) {\n\t\t\t\t\t_queryStartPosition = position - 1;\n\t\t\t\t\tconst auto start = (previousFragmentName == imageName)\n\t\t\t\t\t\t? previousFragmentStart\n\t\t\t\t\t\t: from;\n\t\t\t\t\t_emojiQueryLength = (position - start);\n\t\t\t\t\treturn emoji;\n\t\t\t\t} else {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (from >= position || till < position) {\n\t\t\t\tpreviousFragmentName = QString();\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t_queryStartPosition = from;\n\t\t\t_emojiQueryLength = 0;\n\t\t\treturn fragment.text();\n\t\t}\n\t\treturn QString();\n\t};\n\n\tconst auto part = findTextPart();\n\tif (const auto emoji = std::get_if<EmojiPtr>(&part)) {\n\t\treturn *emoji;\n\t}\n\tconst auto text = v::get<QString>(part);\n\tif (text.isEmpty()) {\n\t\treturn QString();\n\t}\n\tconst auto length = position - _queryStartPosition;\n\tfor (auto i = length; i != 0;) {\n\t\tif (text[--i] == ':') {\n\t\t\tconst auto previous = (i > 0) ? text[i - 1] : QChar(0);\n\t\t\tif (i > 0 && (previous.isLetter() || previous.isDigit())) {\n\t\t\t\treturn QString();\n\t\t\t} else if (i + 1 == length || text[i + 1].isSpace()) {\n\t\t\t\treturn QString();\n\t\t\t}\n\t\t\t_queryStartPosition += i + 2;\n\t\t\treturn text.mid(i, length - i);\n\t\t}\n\t\tif (length - i > legacyLimit && length - i > modernLimit) {\n\t\t\treturn QString();\n\t\t}\n\t}\n\n\t// Exact query should be full input field value.\n\tconst auto end = [&] {\n\t\tauto cursor = _field->textCursor();\n\t\tcursor.movePosition(QTextCursor::End);\n\t\treturn cursor.position();\n\t}();\n\tif (!_options.suggestExactFirstWord\n\t\t|| !length\n\t\t|| text[0].isSpace()\n\t\t|| (length > modernLimit)\n\t\t|| (_queryStartPosition != 0)\n\t\t|| (position != end)) {\n\t\treturn QString();\n\t}\n\treturn text;\n}\n\nvoid SuggestionsController::replaceCurrent(\n\t\tconst QString &replacement,\n\t\tconst QString &customEmojiData) {\n\tconst auto cursor = _field->textCursor();\n\tconst auto position = cursor.position();\n\tconst auto suggestion = getEmojiQuery();\n\tif (v::is<EmojiPtr>(suggestion)) {\n\t\tconst auto weak = base::make_weak(_container.get());\n\t\tconst auto count = std::max(_emojiQueryLength, 1);\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tconst auto start = position - count + i;\n\t\t\t_replaceCallback(start, start + 1, replacement, customEmojiData);\n\t\t\tif (!weak) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t} else if (v::get<QString>(suggestion).isEmpty()) {\n\t\tshowWithQuery(QString());\n\t} else {\n\t\tconst auto from = position - v::get<QString>(suggestion).size();\n\t\t_replaceCallback(from, position, replacement, customEmojiData);\n\t}\n}\n\nvoid SuggestionsController::handleCursorPositionChange() {\n\tInvokeQueued(_container, [=] {\n\t\tif (_ignoreCursorPositionChange) {\n\t\t\treturn;\n\t\t}\n\t\tshowWithQuery(QString());\n\t});\n}\n\nvoid SuggestionsController::suggestionsUpdated(bool visible) {\n\t_shown = visible;\n\tif (_shown) {\n\t\t_container->resizeToContent();\n\t\tupdateGeometry();\n\t\tif (!_forceHidden) {\n\t\t\tif (_container->isHidden() || _container->isHiding()) {\n\t\t\t\traise();\n\t\t\t}\n\t\t\t_container->showAnimated(\n\t\t\t\tUi::PanelAnimation::Origin::BottomLeft);\n\t\t}\n\t} else if (!_forceHidden) {\n\t\t_container->hideAnimated();\n\t}\n}\n\nvoid SuggestionsController::updateGeometry() {\n\tauto cursor = _field->textCursor();\n\tcursor.setPosition(_queryStartPosition);\n\tauto aroundRect = _field->cursorRect(cursor);\n\taroundRect.setTopLeft(_field->viewport()->mapToGlobal(aroundRect.topLeft()));\n\taroundRect.setTopLeft(_container->parentWidget()->mapFromGlobal(aroundRect.topLeft()));\n\tauto boundingRect = _container->parentWidget()->rect();\n\tauto origin = rtl() ? PanelAnimation::Origin::BottomRight : PanelAnimation::Origin::BottomLeft;\n\tauto point = rtl() ? (aroundRect.topLeft() + QPoint(aroundRect.width(), 0)) : aroundRect.topLeft();\n\tconst auto padding = _st.dropdown.padding;\n\tconst auto shift = std::min(_container->width() - padding.left() - padding.right(), st::emojiSuggestionSize) / 2;\n\tpoint -= rtl() ? QPoint(_container->width() - padding.right() - shift, _container->height()) : QPoint(padding.left() + shift, _container->height());\n\tif (rtl()) {\n\t\tif (point.x() < boundingRect.x()) {\n\t\t\tpoint.setX(boundingRect.x());\n\t\t}\n\t\tif (point.x() + _container->width() > boundingRect.x() + boundingRect.width()) {\n\t\t\tpoint.setX(boundingRect.x() + boundingRect.width() - _container->width());\n\t\t}\n\t} else {\n\t\tif (point.x() + _container->width() > boundingRect.x() + boundingRect.width()) {\n\t\t\tpoint.setX(boundingRect.x() + boundingRect.width() - _container->width());\n\t\t}\n\t\tif (point.x() < boundingRect.x()) {\n\t\t\tpoint.setX(boundingRect.x());\n\t\t}\n\t}\n\tif (point.y() < boundingRect.y()) {\n\t\tpoint.setY(aroundRect.y() + aroundRect.height());\n\t\torigin = (origin == PanelAnimation::Origin::BottomRight) ? PanelAnimation::Origin::TopRight : PanelAnimation::Origin::TopLeft;\n\t}\n\t_container->move(point);\n}\n\nvoid SuggestionsController::updateForceHidden() {\n\t_forceHidden = !_field->isVisible() || !_field->hasFocus();\n\tif (_forceHidden) {\n\t\t_container->hideFast();\n\t} else if (_shown) {\n\t\t_container->showFast();\n\t}\n}\n\nbool SuggestionsController::fieldFilter(not_null<QEvent*> event) {\n\tauto type = event->type();\n\tswitch (type) {\n\tcase QEvent::Move:\n\tcase QEvent::Resize: {\n\t\tif (_shown) {\n\t\t\tupdateGeometry();\n\t\t}\n\t} break;\n\n\tcase QEvent::Show:\n\tcase QEvent::ShowToParent:\n\tcase QEvent::Hide:\n\tcase QEvent::HideToParent:\n\tcase QEvent::FocusIn:\n\tcase QEvent::FocusOut: {\n\t\tupdateForceHidden();\n\t} break;\n\n\tcase QEvent::KeyPress: {\n\t\tconst auto key = static_cast<QKeyEvent*>(event.get())->key();\n\t\tswitch (key) {\n\t\tcase Qt::Key_Enter:\n\t\tcase Qt::Key_Return:\n\t\tcase Qt::Key_Tab:\n\t\tcase Qt::Key_Up:\n\t\tcase Qt::Key_Down:\n\t\tcase Qt::Key_Left:\n\t\tcase Qt::Key_Right:\n\t\t\tif (_shown && !_forceHidden) {\n\t\t\t\treturn _suggestions->handleKeyEvent(key);\n\t\t\t}\n\t\t\tbreak;\n\n\t\tcase Qt::Key_Escape:\n\t\t\tif (_shown && !_forceHidden) {\n\t\t\t\tshowWithQuery(QString());\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\t_textChangeAfterKeyPress = true;\n\t\tInvokeQueued(_container, [=] { _textChangeAfterKeyPress = false; });\n\t} break;\n\t}\n\treturn false;\n}\n\nbool SuggestionsController::outerFilter(not_null<QEvent*> event) {\n\tauto type = event->type();\n\tswitch (type) {\n\tcase QEvent::Move:\n\tcase QEvent::Resize: {\n\t\t// updateGeometry uses not only container geometry, but also\n\t\t// container children geometries that will be updated later.\n\t\tInvokeQueued(_container, [=] {\n\t\t\tif (_shown) {\n\t\t\t\tupdateGeometry();\n\t\t\t}\n\t\t});\n\t} break;\n\t}\n\treturn false;\n}\n\nvoid SuggestionsController::raise() {\n\t_container->raise();\n}\n\n} // namespace Emoji\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n#include \"ui/rp_widget.h\"\n#include \"base/unique_qptr.h\"\n#include \"base/timer.h\"\n\n#include <QtWidgets/QTextEdit>\n\nnamespace style {\nstruct EmojiSuggestions;\n} // namespace style\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass InnerDropdown;\nclass InputField;\n} // namespace Ui\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nnamespace Ui::Emoji {\n\nclass SuggestionsWidget;\n\nusing SuggestionsQuery = std::variant<QString, EmojiPtr>;\n\nclass SuggestionsController final : public QObject {\npublic:\n\tstruct Options {\n\t\tbool suggestExactFirstWord = true;\n\t\tbool suggestCustomEmoji = false;\n\t\tFn<bool(not_null<DocumentData*>)> allowCustomWithoutPremium;\n\t\tconst style::EmojiSuggestions *st = nullptr;\n\t};\n\n\tSuggestionsController(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<QWidget*> outer,\n\t\tnot_null<QTextEdit*> field,\n\t\tnot_null<Main::Session*> session,\n\t\tconst Options &options);\n\n\tvoid raise();\n\tvoid setReplaceCallback(Fn<void(\n\t\tint from,\n\t\tint till,\n\t\tconst QString &replacement,\n\t\tconst QString &customEmojiData)> callback);\n\n\tstatic not_null<SuggestionsController*> Init(\n\t\t\tnot_null<QWidget*> outer,\n\t\t\tnot_null<Ui::InputField*> field,\n\t\t\tnot_null<Main::Session*> session) {\n\t\treturn Init(outer, field, session, {});\n\t}\n\tstatic not_null<SuggestionsController*> Init(\n\t\tnot_null<QWidget*> outer,\n\t\tnot_null<Ui::InputField*> field,\n\t\tnot_null<Main::Session*> session,\n\t\tconst Options &options);\n\nprivate:\n\tvoid handleCursorPositionChange();\n\tvoid handleTextChange();\n\tvoid showWithQuery(SuggestionsQuery query);\n\t[[nodiscard]] SuggestionsQuery getEmojiQuery();\n\tvoid suggestionsUpdated(bool visible);\n\tvoid updateGeometry();\n\tvoid updateForceHidden();\n\tvoid replaceCurrent(\n\t\tconst QString &replacement,\n\t\tconst QString &customEmojiData);\n\tbool fieldFilter(not_null<QEvent*> event);\n\tbool outerFilter(not_null<QEvent*> event);\n\n\tconst style::EmojiSuggestions &_st;\n\tbool _shown = false;\n\tbool _forceHidden = false;\n\tint _queryStartPosition = 0;\n\tint _emojiQueryLength = 0;\n\tbool _ignoreCursorPositionChange = false;\n\tbool _textChangeAfterKeyPress = false;\n\tQPointer<QTextEdit> _field;\n\tconst not_null<Main::Session*> _session;\n\tFn<void(\n\t\tint from,\n\t\tint till,\n\t\tconst QString &replacement,\n\t\tconst QString &customEmojiData)> _replaceCallback;\n\tbase::unique_qptr<InnerDropdown> _container;\n\tQPointer<SuggestionsWidget> _suggestions;\n\tbase::unique_qptr<QObject> _fieldFilter;\n\tbase::unique_qptr<QObject> _outerFilter;\n\tbase::Timer _showExactTimer;\n\tbool _keywordsRefreshed = false;\n\tSuggestionsQuery _lastShownQuery;\n\tOptions _options;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Ui::Emoji\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/field_autocomplete.h\"\n\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"menu/menu_send.h\" // SendMenu::FillSendMenu\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"chat_helpers/message_field.h\" // PrepareMentionTag.\n#include \"chat_helpers/tabbed_selector.h\" // ChatHelpers::FileChosen.\n#include \"mainwindow.h\"\n#include \"apiwrap.h\"\n#include \"api/api_chat_participants.h\"\n#include \"main/main_session.h\"\n#include \"storage/storage_account.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"media/clip/media_clip_reader.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/image/image.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"base/unixtime.h\"\n#include \"base/random.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"window/window_adaptive.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace ChatHelpers {\nnamespace {\n\n[[nodiscard]] QString PrimaryUsername(not_null<UserData*> user) {\n\tconst auto &usernames = user->usernames();\n\treturn usernames.empty() ? user->username() : usernames.front();\n}\n\ntemplate <typename T, typename U>\ninline int indexOfInFirstN(const T &v, const U &elem, int last) {\n\tfor (auto b = v.cbegin(), i = b, e = b + std::max(int(v.size()), last)\n\t\t; i != e\n\t\t; ++i) {\n\t\tif (i->user == elem) {\n\t\t\treturn (i - b);\n\t\t}\n\t}\n\treturn -1;\n}\n\n} // namespace\n\nclass FieldAutocomplete::Inner final : public Ui::RpWidget {\npublic:\n\tstruct ScrollTo {\n\t\tint top;\n\t\tint bottom;\n\t};\n\n\tInner(\n\t\tstd::shared_ptr<Show> show,\n\t\tconst style::EmojiPan &st,\n\t\tnot_null<FieldAutocomplete*> parent,\n\t\tnot_null<MentionRows*> mrows,\n\t\tnot_null<HashtagRows*> hrows,\n\t\tnot_null<BotCommandRows*> brows,\n\t\tnot_null<StickerRows*> srows);\n\n\tvoid clearSel(bool hidden = false);\n\tbool moveSel(int key);\n\tbool chooseSelected(FieldAutocomplete::ChooseMethod method) const;\n\tbool chooseAtIndex(\n\t\tFieldAutocomplete::ChooseMethod method,\n\t\tint index,\n\t\tApi::SendOptions options = {}) const;\n\n\tvoid setRecentInlineBotsInRows(int32 bots);\n\tvoid setSendMenuDetails(Fn<SendMenu::Details()> &&callback);\n\tvoid rowsUpdated();\n\n\trpl::producer<FieldAutocomplete::MentionChosen> mentionChosen() const;\n\trpl::producer<FieldAutocomplete::HashtagChosen> hashtagChosen() const;\n\trpl::producer<FieldAutocomplete::BotCommandChosen>\n\t\tbotCommandChosen() const;\n\trpl::producer<FieldAutocomplete::StickerChosen> stickerChosen() const;\n\trpl::producer<ScrollTo> scrollToRequested() const;\n\n\tvoid onParentGeometryChanged();\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\n\tQRect selectedRect(int index) const;\n\tvoid updateSelectedRow();\n\tvoid setSel(int sel, bool scroll = false);\n\tvoid showPreview();\n\tvoid selectByMouse(QPoint global);\n\n\tQSize stickerBoundingBox() const;\n\tvoid setupLottie(StickerSuggestion &suggestion);\n\tvoid setupWebm(StickerSuggestion &suggestion);\n\tvoid repaintSticker(not_null<DocumentData*> document);\n\tvoid repaintStickerAtIndex(int index);\n\tstd::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();\n\tvoid clipCallback(\n\t\tMedia::Clip::Notification notification,\n\t\tnot_null<DocumentData*> document);\n\n\tconst std::shared_ptr<Show> _show;\n\tconst not_null<Main::Session*> _session;\n\tconst style::EmojiPan &_st;\n\tconst not_null<FieldAutocomplete*> _parent;\n\tconst not_null<MentionRows*> _mrows;\n\tconst not_null<HashtagRows*> _hrows;\n\tconst not_null<BotCommandRows*> _brows;\n\tconst not_null<StickerRows*> _srows;\n\tUi::RoundRect _overBg;\n\trpl::lifetime _stickersLifetime;\n\tstd::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\tint _stickersPerRow = 1;\n\tint _recentInlineBotsInRows = 0;\n\tint _sel = -1;\n\tint _down = -1;\n\tstd::optional<QPoint> _lastMousePosition;\n\tbool _mouseSelection = false;\n\n\tbool _overDelete = false;\n\n\tbool _previewShown = false;\n\n\tbool _adjustShadowLeft = false;\n\n\tconst std::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\tStickerPremiumMark _premiumMark;\n\n\tFn<SendMenu::Details()> _sendMenuDetails;\n\n\trpl::event_stream<FieldAutocomplete::MentionChosen> _mentionChosen;\n\trpl::event_stream<FieldAutocomplete::HashtagChosen> _hashtagChosen;\n\trpl::event_stream<FieldAutocomplete::BotCommandChosen> _botCommandChosen;\n\trpl::event_stream<FieldAutocomplete::StickerChosen> _stickerChosen;\n\trpl::event_stream<ScrollTo> _scrollToRequested;\n\n\tbase::Timer _previewTimer;\n\n};\n\nstruct FieldAutocomplete::StickerSuggestion {\n\tnot_null<DocumentData*> document;\n\tstd::shared_ptr<Data::DocumentMedia> documentMedia;\n\tstd::unique_ptr<Lottie::SinglePlayer> lottie;\n\tMedia::Clip::ReaderPointer webm;\n\tQImage premiumLock;\n};\n\nstruct FieldAutocomplete::MentionRow {\n\tnot_null<UserData*> user;\n\tUi::Text::String name;\n\tUi::PeerUserpicView userpic;\n};\n\nstruct FieldAutocomplete::BotCommandRow {\n\tnot_null<UserData*> user;\n\tQString command;\n\tQString description;\n\tUi::PeerUserpicView userpic;\n\tUi::Text::String descriptionText;\n};\n\nFieldAutocomplete::FieldAutocomplete(\n\tQWidget *parent,\n\tstd::shared_ptr<Show> show,\n\tconst style::EmojiPan *stOverride)\n: RpWidget(parent)\n, _show(std::move(show))\n, _session(&_show->session())\n, _st(stOverride ? *stOverride : st::defaultEmojiPan)\n, _scroll(this) {\n\thide();\n\n\t_scroll->setGeometry(rect());\n\n\t_inner = _scroll->setOwnedWidget(\n\t\tobject_ptr<Inner>(\n\t\t\t_show,\n\t\t\t_st,\n\t\t\tthis,\n\t\t\t&_mrows,\n\t\t\t&_hrows,\n\t\t\t&_brows,\n\t\t\t&_srows));\n\t_inner->setGeometry(rect());\n\n\t_inner->scrollToRequested(\n\t) | rpl::on_next([=](Inner::ScrollTo data) {\n\t\t_scroll->scrollToY(data.top, data.bottom);\n\t}, lifetime());\n\n\t_scroll->show();\n\t_inner->show();\n\n\thide();\n\n\t_scroll->geometryChanged(\n\t) | rpl::on_next(crl::guard(_inner, [=] {\n\t\t_inner->onParentGeometryChanged();\n\t}), lifetime());\n}\n\nstd::shared_ptr<Show> FieldAutocomplete::uiShow() const {\n\treturn _show;\n}\n\nvoid FieldAutocomplete::requestRefresh() {\n\t_refreshRequests.fire({});\n}\n\nrpl::producer<> FieldAutocomplete::refreshRequests() const {\n\treturn _refreshRequests.events();\n}\n\nvoid FieldAutocomplete::requestStickersUpdate() {\n\t_stickersUpdateRequests.fire({});\n}\n\nrpl::producer<> FieldAutocomplete::stickersUpdateRequests() const {\n\treturn _stickersUpdateRequests.events();\n}\n\nauto FieldAutocomplete::mentionChosen() const\n-> rpl::producer<FieldAutocomplete::MentionChosen> {\n\treturn _inner->mentionChosen();\n}\n\nauto FieldAutocomplete::hashtagChosen() const\n-> rpl::producer<FieldAutocomplete::HashtagChosen> {\n\treturn _inner->hashtagChosen();\n}\n\nauto FieldAutocomplete::botCommandChosen() const\n-> rpl::producer<FieldAutocomplete::BotCommandChosen> {\n\treturn _inner->botCommandChosen();\n}\n\nauto FieldAutocomplete::stickerChosen() const\n-> rpl::producer<FieldAutocomplete::StickerChosen> {\n\treturn _inner->stickerChosen();\n}\n\nauto FieldAutocomplete::choosingProcesses() const\n-> rpl::producer<FieldAutocomplete::Type> {\n\treturn _scroll->scrollTopChanges(\n\t) | rpl::filter([](int top) {\n\t\treturn top != 0;\n\t}) | rpl::map([=] {\n\t\treturn !_mrows.empty()\n\t\t\t? Type::Mentions\n\t\t\t: !_hrows.empty()\n\t\t\t? Type::Hashtags\n\t\t\t: !_brows.empty()\n\t\t\t? Type::BotCommands\n\t\t\t: !_srows.empty()\n\t\t\t? Type::Stickers\n\t\t\t: _type;\n\t});\n}\n\nFieldAutocomplete::~FieldAutocomplete() = default;\n\nvoid FieldAutocomplete::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tauto opacity = _a_opacity.value(_hiding ? 0. : 1.);\n\tif (opacity < 1.) {\n\t\tif (opacity > 0.) {\n\t\t\tp.setOpacity(opacity);\n\t\t\tp.drawPixmap(0, 0, _cache);\n\t\t} else if (_hiding) {\n\n\t\t}\n\t\treturn;\n\t}\n\n\tp.fillRect(rect(), _st.bg);\n}\n\nvoid FieldAutocomplete::showFiltered(\n\t\tnot_null<PeerData*> peer,\n\t\tQString query,\n\t\tbool addInlineBots) {\n\t_chat = peer->asChat();\n\t_user = peer->asUser();\n\t_channel = peer->asChannel();\n\tif (query.isEmpty()) {\n\t\t_type = Type::Mentions;\n\t\trowsUpdated(\n\t\t\tMentionRows(),\n\t\t\tHashtagRows(),\n\t\t\tBotCommandRows(),\n\t\t\tbase::take(_srows),\n\t\t\tfalse);\n\t\treturn;\n\t}\n\n\t_emoji = nullptr;\n\n\tquery = query.toLower();\n\tauto type = Type::Stickers;\n\tauto plainQuery = QStringView(query);\n\tswitch (query.at(0).unicode()) {\n\tcase '@':\n\t\ttype = Type::Mentions;\n\t\tplainQuery = base::StringViewMid(query, 1);\n\t\tbreak;\n\tcase '#':\n\t\ttype = Type::Hashtags;\n\t\tplainQuery = base::StringViewMid(query, 1);\n\t\tbreak;\n\tcase '/':\n\t\ttype = Type::BotCommands;\n\t\tplainQuery = base::StringViewMid(query, 1);\n\t\tbreak;\n\t}\n\tconst auto resetScroll = (_type != type || _filter != plainQuery);\n\tif (resetScroll) {\n\t\t_type = type;\n\t\t_filter = TextUtilities::RemoveAccents(plainQuery.toString());\n\t}\n\t_addInlineBots = addInlineBots;\n\n\tupdateFiltered(resetScroll);\n}\n\nvoid FieldAutocomplete::showStickers(EmojiPtr emoji) {\n\tconst auto resetScroll = (_emoji != emoji);\n\tif (resetScroll || emoji) {\n\t\t_emoji = emoji;\n\t\t_type = Type::Stickers;\n\t} else if (!emoji) {\n\t\trowsUpdated(\n\t\t\tbase::take(_mrows),\n\t\t\tbase::take(_hrows),\n\t\t\tbase::take(_brows),\n\t\t\tStickerRows(),\n\t\t\tfalse);\n\t\treturn;\n\t}\n\n\t_chat = nullptr;\n\t_user = nullptr;\n\t_channel = nullptr;\n\n\tupdateFiltered(resetScroll);\n}\n\nEmojiPtr FieldAutocomplete::stickersEmoji() const {\n\treturn _emoji;\n}\n\nbool FieldAutocomplete::clearFilteredBotCommands() {\n\tif (_brows.empty()) {\n\t\treturn false;\n\t}\n\t_brows.clear();\n\treturn true;\n}\n\nFieldAutocomplete::StickerRows FieldAutocomplete::getStickerSuggestions() {\n\tconst auto data = &_session->data().stickers();\n\tconst auto list = data->getListByEmoji({ _emoji }, _stickersSeed);\n\tauto result = ranges::views::all(\n\t\tlist\n\t) | ranges::views::transform([](not_null<DocumentData*> sticker) {\n\t\treturn StickerSuggestion{\n\t\t\tsticker,\n\t\t\tsticker->createMediaView()\n\t\t};\n\t}) | ranges::to_vector;\n\tfor (auto &suggestion : _srows) {\n\t\tif (!suggestion.lottie && !suggestion.webm) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto i = ranges::find(\n\t\t\tresult,\n\t\t\tsuggestion.document,\n\t\t\t&StickerSuggestion::document);\n\t\tif (i != end(result)) {\n\t\t\ti->lottie = std::move(suggestion.lottie);\n\t\t\ti->webm = std::move(suggestion.webm);\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid FieldAutocomplete::updateFiltered(bool resetScroll) {\n\tint32 now = base::unixtime::now(), recentInlineBots = 0;\n\tMentionRows mrows;\n\tHashtagRows hrows;\n\tBotCommandRows brows;\n\tStickerRows srows;\n\tif (_emoji) {\n\t\tsrows = getStickerSuggestions();\n\t} else if (_type == Type::Mentions) {\n\t\tint maxListSize = _addInlineBots ? cRecentInlineBots().size() : 0;\n\t\tif (_chat) {\n\t\t\tmaxListSize += (_chat->participants.empty() ? _chat->lastAuthors.size() : _chat->participants.size());\n\t\t} else if (_channel && _channel->isMegagroup()) {\n\t\t\tif (!_channel->canViewMembers()) {\n\t\t\t\tmaxListSize += _channel->mgInfo->admins.size();\n\t\t\t} else if (!_channel->lastParticipantsRequestNeeded()) {\n\t\t\t\tmaxListSize += _channel->mgInfo->lastParticipants.size();\n\t\t\t}\n\t\t}\n\t\tif (maxListSize) {\n\t\t\tmrows.reserve(maxListSize);\n\t\t}\n\n\t\tauto filterNotPassedByUsername = [this](UserData *user) -> bool {\n\t\t\tif (PrimaryUsername(user).startsWith(_filter, Qt::CaseInsensitive)) {\n\t\t\t\tconst auto exactUsername\n\t\t\t\t\t= (PrimaryUsername(user).size() == _filter.size());\n\t\t\t\treturn exactUsername;\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\t\tauto filterNotPassedByName = [&](UserData *user) -> bool {\n\t\t\tfor (const auto &nameWord : user->nameWords()) {\n\t\t\t\tif (nameWord.startsWith(_filter, Qt::CaseInsensitive)) {\n\t\t\t\t\tconst auto exactUsername = PrimaryUsername(user).compare(\n\t\t\t\t\t\t_filter,\n\t\t\t\t\t\tQt::CaseInsensitive) == 0;\n\t\t\t\t\treturn exactUsername;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn filterNotPassedByUsername(user);\n\t\t};\n\n\t\tbool listAllSuggestions = _filter.isEmpty();\n\t\tif (_addInlineBots) {\n\t\t\tfor (const auto user : cRecentInlineBots()) {\n\t\t\t\tif (user->isInaccessible()\n\t\t\t\t\t|| (!listAllSuggestions\n\t\t\t\t\t\t&& filterNotPassedByUsername(user))) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tmrows.push_back({ user });\n\t\t\t\t++recentInlineBots;\n\t\t\t}\n\t\t}\n\t\tif (_chat) {\n\t\t\tauto sorted = base::flat_multi_map<TimeId, not_null<UserData*>>();\n\t\t\tconst auto byOnline = [&](not_null<UserData*> user) {\n\t\t\t\treturn Data::SortByOnlineValue(user, now);\n\t\t\t};\n\t\t\tmrows.reserve(mrows.size() + (_chat->participants.empty() ? _chat->lastAuthors.size() : _chat->participants.size()));\n\t\t\tif (_chat->noParticipantInfo()) {\n\t\t\t\t_chat->session().api().requestFullPeer(_chat);\n\t\t\t} else if (!_chat->participants.empty()) {\n\t\t\t\tfor (const auto &user : _chat->participants) {\n\t\t\t\t\tif (user->isInaccessible()) continue;\n\t\t\t\t\tif (!listAllSuggestions && filterNotPassedByName(user)) continue;\n\t\t\t\t\tif (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue;\n\t\t\t\t\tsorted.emplace(byOnline(user), user);\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const auto &user : _chat->lastAuthors) {\n\t\t\t\tif (user->isInaccessible()) continue;\n\t\t\t\tif (!listAllSuggestions && filterNotPassedByName(user)) continue;\n\t\t\t\tif (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue;\n\t\t\t\tmrows.push_back({ user });\n\t\t\t\tsorted.remove(byOnline(user), user);\n\t\t\t}\n\t\t\tfor (auto i = sorted.cend(), b = sorted.cbegin(); i != b;) {\n\t\t\t\t--i;\n\t\t\t\tmrows.push_back({ i->second });\n\t\t\t}\n\t\t} else if (_channel && _channel->isMegagroup()) {\n\t\t\tif (!_channel->canViewMembers()) {\n\t\t\t\tif (!_channel->mgInfo->adminsLoaded) {\n\t\t\t\t\t_channel->session().api().chatParticipants().requestAdmins(_channel);\n\t\t\t\t} else {\n\t\t\t\t\tmrows.reserve(mrows.size() + _channel->mgInfo->admins.size());\n\t\t\t\t\tfor (const auto &userId : _channel->mgInfo->admins) {\n\t\t\t\t\t\tif (const auto user = _channel->owner().userLoaded(userId)) {\n\t\t\t\t\t\t\tif (user->isInaccessible()) continue;\n\t\t\t\t\t\t\tif (!listAllSuggestions && filterNotPassedByName(user)) continue;\n\t\t\t\t\t\t\tif (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue;\n\t\t\t\t\t\t\tmrows.push_back({ user });\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (_channel->lastParticipantsRequestNeeded()) {\n\t\t\t\t_channel->session().api().chatParticipants().requestLast(\n\t\t\t\t\t_channel);\n\t\t\t} else {\n\t\t\t\tmrows.reserve(mrows.size() + _channel->mgInfo->lastParticipants.size());\n\t\t\t\tfor (const auto &user : _channel->mgInfo->lastParticipants) {\n\t\t\t\t\tif (user->isInaccessible()) continue;\n\t\t\t\t\tif (!listAllSuggestions && filterNotPassedByName(user)) continue;\n\t\t\t\t\tif (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue;\n\t\t\t\t\tmrows.push_back({ user });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if (_type == Type::Hashtags) {\n\t\tbool listAllSuggestions = _filter.isEmpty();\n\t\tauto &recent(cRecentWriteHashtags());\n\t\throws.reserve(recent.size());\n\t\tfor (const auto &item : recent) {\n\t\t\tconst auto &tag = item.first;\n\t\t\tif (!listAllSuggestions\n\t\t\t\t&& (tag.size() == _filter.size()\n\t\t\t\t\t|| !TextUtilities::RemoveAccents(tag).startsWith(\n\t\t\t\t\t\t_filter,\n\t\t\t\t\t\tQt::CaseInsensitive))) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\throws.push_back(tag);\n\t\t}\n\t} else if (_type == Type::BotCommands) {\n\t\tbool listAllSuggestions = _filter.isEmpty();\n\t\tbool hasUsername = _filter.indexOf('@') > 0;\n\t\tbase::flat_map<\n\t\t\tnot_null<UserData*>,\n\t\t\tnot_null<const std::vector<Data::BotCommand>*>> bots;\n\t\tint32 cnt = 0;\n\t\tif (_chat) {\n\t\t\tif (_chat->noParticipantInfo()) {\n\t\t\t\t_chat->session().api().requestFullPeer(_chat);\n\t\t\t} else if (!_chat->participants.empty()) {\n\t\t\t\tconst auto &commands = _chat->botCommands();\n\t\t\t\tfor (const auto &user : _chat->participants) {\n\t\t\t\t\tif (!user->isBot()) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst auto i = commands.find(peerToUser(user->id));\n\t\t\t\t\tif (i != end(commands)) {\n\t\t\t\t\t\tbots.emplace(user, &i->second);\n\t\t\t\t\t\tcnt += i->second.size();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (_user && _user->isBot()) {\n\t\t\tif (!_user->botInfo->inited) {\n\t\t\t\t_user->session().api().requestFullPeer(_user);\n\t\t\t}\n\t\t\tcnt = _user->botInfo->commands.size();\n\t\t\tbots.emplace(_user, &_user->botInfo->commands);\n\t\t} else if (_channel && _channel->isMegagroup()) {\n\t\t\tif (_channel->mgInfo->bots.empty()) {\n\t\t\t\tif (_channel->mgInfo->botStatus == Data::BotStatus::Unknown) {\n\t\t\t\t\t_channel->session().api().chatParticipants().requestBots(\n\t\t\t\t\t\t_channel);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst auto &commands = _channel->mgInfo->botCommands();\n\t\t\t\tfor (const auto &user : _channel->mgInfo->bots) {\n\t\t\t\t\tif (!user->isBot()) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst auto i = commands.find(peerToUser(user->id));\n\t\t\t\t\tif (i != end(commands)) {\n\t\t\t\t\t\tbots.emplace(user, &i->second);\n\t\t\t\t\t\tcnt += i->second.size();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (cnt) {\n\t\t\tconst auto make = [&](\n\t\t\t\t\tnot_null<UserData*> user,\n\t\t\t\t\tconst Data::BotCommand &command) {\n\t\t\t\treturn BotCommandRow{\n\t\t\t\t\tuser,\n\t\t\t\t\tcommand.command,\n\t\t\t\t\tcommand.description,\n\t\t\t\t\tuser->activeUserpicView()\n\t\t\t\t};\n\t\t\t};\n\t\t\tbrows.reserve(cnt);\n\t\t\tconst auto botStatus = _chat ? _chat->botStatus : ((_channel && _channel->isMegagroup()) ? _channel->mgInfo->botStatus : Data::BotStatus::NoBots);\n\t\t\tif (_chat) {\n\t\t\t\tfor (const auto &user : _chat->lastAuthors) {\n\t\t\t\t\tif (!user->isBot()) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst auto i = bots.find(user);\n\t\t\t\t\tif (i == end(bots)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tfor (const auto &command : *i->second) {\n\t\t\t\t\t\tif (!listAllSuggestions) {\n\t\t\t\t\t\t\tauto toFilter = (hasUsername || botStatus != Data::BotStatus::NoBots)\n\t\t\t\t\t\t\t\t? command.command + '@' + PrimaryUsername(user)\n\t\t\t\t\t\t\t\t: command.command;\n\t\t\t\t\t\t\tif (!toFilter.startsWith(_filter, Qt::CaseInsensitive)/* || toFilter.size() == _filter.size()*/) {\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbrows.push_back(make(user, command));\n\t\t\t\t\t}\n\t\t\t\t\tbots.erase(i);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!bots.empty()) {\n\t\t\t\tfor (auto i = bots.cbegin(), e = bots.cend(); i != e; ++i) {\n\t\t\t\t\tconst auto user = i->first;\n\t\t\t\t\tfor (const auto &command : *i->second) {\n\t\t\t\t\t\tif (!listAllSuggestions) {\n\t\t\t\t\t\t\tconst auto toFilter = (hasUsername\n\t\t\t\t\t\t\t\t\t|| botStatus != Data::BotStatus::NoBots)\n\t\t\t\t\t\t\t\t? command.command + '@' + PrimaryUsername(user)\n\t\t\t\t\t\t\t\t: command.command;\n\t\t\t\t\t\t\tif (!toFilter.startsWith(_filter, Qt::CaseInsensitive)/* || toFilter.size() == _filter.size()*/) continue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbrows.push_back(make(user, command));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst auto shortcuts = (_user && !_user->isBot())\n\t\t\t? _user->owner().shortcutMessages().shortcuts().list\n\t\t\t: base::flat_map<BusinessShortcutId, Data::Shortcut>();\n\t\tif (!hasUsername && brows.empty() && !shortcuts.empty()) {\n\t\t\tconst auto self = _user->session().user();\n\t\t\tfor (const auto &[id, shortcut] : shortcuts) {\n\t\t\t\tif (shortcut.count < 1) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (!listAllSuggestions) {\n\t\t\t\t\tif (!shortcut.name.startsWith(_filter, Qt::CaseInsensitive)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbrows.push_back(BotCommandRow{\n\t\t\t\t\tself,\n\t\t\t\t\tshortcut.name,\n\t\t\t\t\ttr::lng_forum_messages(tr::now, lt_count, shortcut.count),\n\t\t\t\t\tself->activeUserpicView()\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (!brows.empty()) {\n\t\t\t\tbrows.insert(begin(brows), BotCommandRow{ self }); // Edit.\n\t\t\t}\n\t\t}\n\t}\n\trowsUpdated(\n\t\tstd::move(mrows),\n\t\tstd::move(hrows),\n\t\tstd::move(brows),\n\t\tstd::move(srows),\n\t\tresetScroll);\n\t_inner->setRecentInlineBotsInRows(recentInlineBots);\n}\n\nvoid FieldAutocomplete::rowsUpdated(\n\t\tMentionRows &&mrows,\n\t\tHashtagRows &&hrows,\n\t\tBotCommandRows &&brows,\n\t\tStickerRows &&srows,\n\t\tbool resetScroll) {\n\tif (mrows.empty() && hrows.empty() && brows.empty() && srows.empty()) {\n\t\tif (!isHidden()) {\n\t\t\thideAnimated();\n\t\t}\n\t\t_scroll->scrollToY(0);\n\t\t_mrows.clear();\n\t\t_hrows.clear();\n\t\t_brows.clear();\n\t\t_srows.clear();\n\t} else {\n\t\t_mrows = std::move(mrows);\n\t\t_hrows = std::move(hrows);\n\t\t_brows = std::move(brows);\n\t\t_srows = std::move(srows);\n\n\t\tbool hidden = _hiding || isHidden();\n\t\tif (hidden) {\n\t\t\tshow();\n\t\t\t_scroll->show();\n\t\t}\n\t\trecount(resetScroll);\n\t\tupdate();\n\t\tif (hidden) {\n\t\t\thide();\n\t\t\tshowAnimated();\n\t\t}\n\t}\n\t_inner->rowsUpdated();\n}\n\nvoid FieldAutocomplete::setBoundings(QRect boundings) {\n\t_boundings = boundings;\n\trecount();\n}\n\nvoid FieldAutocomplete::recount(bool resetScroll) {\n\tint32 h = 0, oldst = _scroll->scrollTop(), st = oldst, maxh = 4.5 * st::mentionHeight;\n\tif (!_srows.empty()) {\n\t\tint32 stickersPerRow = qMax(1, int32(_boundings.width() - 2 * st::stickerPanPadding) / int32(st::stickerPanSize.width()));\n\t\tint32 rows = rowscount(_srows.size(), stickersPerRow);\n\t\th = st::stickerPanPadding + rows * st::stickerPanSize.height();\n\t} else if (!_mrows.empty()) {\n\t\th = _mrows.size() * st::mentionHeight;\n\t} else if (!_hrows.empty()) {\n\t\th = _hrows.size() * st::mentionHeight;\n\t} else if (!_brows.empty()) {\n\t\th = _brows.size() * st::mentionHeight;\n\t}\n\th += _st.autocompleteBottomSkip;\n\n\tif (_inner->width() != _boundings.width() || _inner->height() != h) {\n\t\t_inner->resize(_boundings.width(), h);\n\t}\n\tif (h > _boundings.height()) h = _boundings.height();\n\tif (h > maxh) h = maxh;\n\tif (width() != _boundings.width() || height() != h) {\n\t\tsetGeometry(\n\t\t\t_boundings.x(),\n\t\t\t_boundings.y() + _boundings.height() - h,\n\t\t\t_boundings.width(),\n\t\t\th);\n\t\t_scroll->resize(_boundings.width(), h);\n\t} else if (x() != _boundings.x()\n\t\t|| y() != _boundings.y() + _boundings.height() - h) {\n\t\tmove(_boundings.x(), _boundings.y() + _boundings.height() - h);\n\t}\n\tif (resetScroll) st = 0;\n\tif (st != oldst) _scroll->scrollToY(st);\n\tif (resetScroll) _inner->clearSel();\n}\n\nvoid FieldAutocomplete::hideFast() {\n\t_a_opacity.stop();\n\thideFinish();\n}\n\nvoid FieldAutocomplete::hideAnimated() {\n\tif (isHidden() || _hiding) {\n\t\treturn;\n\t}\n\n\tif (_cache.isNull()) {\n\t\t_scroll->show();\n\t\t_cache = Ui::GrabWidget(this);\n\t}\n\t_scroll->hide();\n\t_hiding = true;\n\t_a_opacity.start([this] { animationCallback(); }, 1., 0., st::emojiPanDuration);\n\tsetAttribute(Qt::WA_OpaquePaintEvent, false);\n}\n\nvoid FieldAutocomplete::hideFinish() {\n\thide();\n\t_hiding = false;\n\t_filter = u\"-\"_q;\n\t_inner->clearSel(true);\n}\n\nvoid FieldAutocomplete::showAnimated() {\n\tif (!isHidden() && !_hiding) {\n\t\treturn;\n\t}\n\tif (_cache.isNull()) {\n\t\t_stickersSeed = base::RandomValue<uint64>();\n\t\t_scroll->show();\n\t\t_cache = Ui::GrabWidget(this);\n\t}\n\t_scroll->hide();\n\t_hiding = false;\n\tshow();\n\t_a_opacity.start([this] { animationCallback(); }, 0., 1., st::emojiPanDuration);\n\tsetAttribute(Qt::WA_OpaquePaintEvent, false);\n}\n\nvoid FieldAutocomplete::animationCallback() {\n\tupdate();\n\tif (!_a_opacity.animating()) {\n\t\t_cache = QPixmap();\n\t\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\t\tif (_hiding) {\n\t\t\thideFinish();\n\t\t} else {\n\t\t\t_scroll->show();\n\t\t\t_inner->clearSel();\n\t\t}\n\t}\n}\n\nconst QString &FieldAutocomplete::filter() const {\n\treturn _filter;\n}\n\nChatData *FieldAutocomplete::chat() const {\n\treturn _chat;\n}\n\nChannelData *FieldAutocomplete::channel() const {\n\treturn _channel;\n}\n\nUserData *FieldAutocomplete::user() const {\n\treturn _user;\n}\n\nint32 FieldAutocomplete::innerTop() {\n\treturn _scroll->scrollTop();\n}\n\nint32 FieldAutocomplete::innerBottom() {\n\treturn _scroll->scrollTop() + _scroll->height();\n}\n\nbool FieldAutocomplete::chooseSelected(ChooseMethod method) const {\n\treturn _inner->chooseSelected(method);\n}\n\nvoid FieldAutocomplete::setSendMenuDetails(\n\t\tFn<SendMenu::Details()> &&callback) {\n\t_inner->setSendMenuDetails(std::move(callback));\n}\n\nbool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) {\n\tauto hidden = isHidden();\n\tauto moderate = Core::App().settings().moderateModeEnabled();\n\tif (hidden && !moderate) return QWidget::eventFilter(obj, e);\n\n\tif (e->type() == QEvent::KeyPress) {\n\t\tQKeyEvent *ev = static_cast<QKeyEvent*>(e);\n\t\tif (!(ev->modifiers() & (Qt::AltModifier | Qt::ControlModifier | Qt::ShiftModifier | Qt::MetaModifier))) {\n\t\t\tconst auto key = ev->key();\n\t\t\tif (!hidden) {\n\t\t\t\tif (key == Qt::Key_Up || key == Qt::Key_Down || (!_srows.empty() && (key == Qt::Key_Left || key == Qt::Key_Right))) {\n\t\t\t\t\treturn _inner->moveSel(key);\n\t\t\t\t} else if (key == Qt::Key_Enter || key == Qt::Key_Return) {\n\t\t\t\t\treturn _inner->chooseSelected(ChooseMethod::ByEnter);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (moderate\n\t\t\t\t&& ((key >= Qt::Key_1 && key <= Qt::Key_9)\n\t\t\t\t\t|| key == Qt::Key_Q\n\t\t\t\t\t|| key == Qt::Key_W)) {\n\n\t\t\t\treturn _moderateKeyActivateCallback\n\t\t\t\t\t? _moderateKeyActivateCallback(key)\n\t\t\t\t\t: false;\n\t\t\t}\n\t\t}\n\t}\n\treturn QWidget::eventFilter(obj, e);\n}\n\nFieldAutocomplete::Inner::Inner(\n\tstd::shared_ptr<Show> show,\n\tconst style::EmojiPan &st,\n\tnot_null<FieldAutocomplete*> parent,\n\tnot_null<MentionRows*> mrows,\n\tnot_null<HashtagRows*> hrows,\n\tnot_null<BotCommandRows*> brows,\n\tnot_null<StickerRows*> srows)\n: _show(std::move(show))\n, _session(&_show->session())\n, _st(st)\n, _parent(parent)\n, _mrows(mrows)\n, _hrows(hrows)\n, _brows(brows)\n, _srows(srows)\n, _overBg(st::roundRadiusSmall, _st.overBg)\n, _pathGradient(std::make_unique<Ui::PathShiftGradient>(\n\t_st.pathBg,\n\t_st.pathFg,\n\t[=] { update(); }))\n, _premiumMark(_session, st::stickersPremiumLock)\n, _previewTimer([=] { showPreview(); }) {\n\t_session->downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\t_show->adjustShadowLeft(\n\t) | rpl::on_next([=](bool adjust) {\n\t\t_adjustShadowLeft = adjust;\n\t\tupdate();\n\t}, lifetime());\n}\n\nvoid FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tQRect r(e->rect());\n\tif (r != rect()) p.setClipRect(r);\n\n\tauto mentionleft = 2 * st::mentionPadding.left() + st::mentionPhotoSize;\n\tauto mentionwidth = width()\n\t\t- mentionleft\n\t\t- 2 * st::mentionPadding.right();\n\tauto htagleft = st::historyAttach.width\n\t\t+ st::historyComposeField.textMargins.left()\n\t\t- st::lineWidth;\n\tauto htagwidth = width()\n\t\t- st::mentionPadding.right()\n\t\t- htagleft\n\t\t- st::defaultScrollArea.width;\n\n\tif (!_srows->empty()) {\n\t\t_pathGradient->startFrame(\n\t\t\t0,\n\t\t\twidth(),\n\t\t\tstd::min(st::msgMaxWidth / 2, width() / 2));\n\n\t\tconst auto now = crl::now();\n\t\tint32 rows = rowscount(_srows->size(), _stickersPerRow);\n\t\tint32 fromrow = floorclamp(r.y() - st::stickerPanPadding, st::stickerPanSize.height(), 0, rows);\n\t\tint32 torow = ceilclamp(r.y() + r.height() - st::stickerPanPadding, st::stickerPanSize.height(), 0, rows);\n\t\tint32 fromcol = floorclamp(r.x() - st::stickerPanPadding, st::stickerPanSize.width(), 0, _stickersPerRow);\n\t\tint32 tocol = ceilclamp(r.x() + r.width() - st::stickerPanPadding, st::stickerPanSize.width(), 0, _stickersPerRow);\n\t\tfor (int32 row = fromrow; row < torow; ++row) {\n\t\t\tfor (int32 col = fromcol; col < tocol; ++col) {\n\t\t\t\tint32 index = row * _stickersPerRow + col;\n\t\t\t\tif (index >= _srows->size()) break;\n\n\t\t\t\tauto &sticker = (*_srows)[index];\n\t\t\t\tconst auto document = sticker.document;\n\t\t\t\tconst auto &media = sticker.documentMedia;\n\t\t\t\tconst auto info = document->sticker();\n\t\t\t\tif (!info) continue;\n\n\t\t\t\tif (media->loaded()) {\n\t\t\t\t\tif (info->isLottie() && !sticker.lottie) {\n\t\t\t\t\t\tsetupLottie(sticker);\n\t\t\t\t\t} else if (info->isWebm() && !sticker.webm) {\n\t\t\t\t\t\tsetupWebm(sticker);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tQPoint pos(st::stickerPanPadding + col * st::stickerPanSize.width(), st::stickerPanPadding + row * st::stickerPanSize.height());\n\t\t\t\tif (_sel == index) {\n\t\t\t\t\tQPoint tl(pos);\n\t\t\t\t\tif (rtl()) tl.setX(width() - tl.x() - st::stickerPanSize.width());\n\t\t\t\t\t_overBg.paint(p, QRect(tl, st::stickerPanSize));\n\t\t\t\t}\n\n\t\t\t\tmedia->checkStickerSmall();\n\t\t\t\tconst auto paused = _show->paused(\n\t\t\t\t\tPauseReason::TabbedPanel);\n\t\t\t\tconst auto size = ComputeStickerSize(\n\t\t\t\t\tdocument,\n\t\t\t\t\tstickerBoundingBox());\n\t\t\t\tconst auto ppos = pos + QPoint(\n\t\t\t\t\t(st::stickerPanSize.width() - size.width()) / 2,\n\t\t\t\t\t(st::stickerPanSize.height() - size.height()) / 2);\n\t\t\t\tauto lottieFrame = QImage();\n\t\t\t\tif (sticker.lottie && sticker.lottie->ready()) {\n\t\t\t\t\tlottieFrame = sticker.lottie->frame();\n\t\t\t\t\tp.drawImage(\n\t\t\t\t\t\tQRect(\n\t\t\t\t\t\t\tppos,\n\t\t\t\t\t\t\tlottieFrame.size() / style::DevicePixelRatio()),\n\t\t\t\t\t\tlottieFrame);\n\t\t\t\t\tif (!paused) {\n\t\t\t\t\t\tsticker.lottie->markFrameShown();\n\t\t\t\t\t}\n\t\t\t\t} else if (sticker.webm && sticker.webm->started()) {\n\t\t\t\t\tp.drawImage(ppos, sticker.webm->current({\n\t\t\t\t\t\t.frame = size,\n\t\t\t\t\t\t.keepAlpha = true,\n\t\t\t\t\t}, paused ? 0 : now));\n\t\t\t\t} else if (const auto image = media->getStickerSmall()) {\n\t\t\t\t\tp.drawPixmapLeft(ppos, width(), image->pix(size));\n\t\t\t\t} else {\n\t\t\t\t\tPaintStickerThumbnailPath(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tmedia.get(),\n\t\t\t\t\t\tQRect(ppos, size),\n\t\t\t\t\t\t_pathGradient.get());\n\t\t\t\t}\n\n\t\t\t\tif (document->isPremiumSticker()) {\n\t\t\t\t\t_premiumMark.paint(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tlottieFrame,\n\t\t\t\t\t\tsticker.premiumLock,\n\t\t\t\t\t\tpos,\n\t\t\t\t\t\tst::stickerPanSize,\n\t\t\t\t\t\twidth());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tint32 from = qFloor(e->rect().top() / st::mentionHeight), to = qFloor(e->rect().bottom() / st::mentionHeight) + 1;\n\t\tint32 last = !_mrows->empty()\n\t\t\t? _mrows->size()\n\t\t\t: !_hrows->empty()\n\t\t\t? _hrows->size()\n\t\t\t: _brows->size();\n\t\tauto filter = _parent->filter();\n\t\tbool hasUsername = filter.indexOf('@') > 0;\n\t\tint filterSize = filter.size();\n\t\tbool filterIsEmpty = filter.isEmpty();\n\t\tfor (int32 i = from; i < to; ++i) {\n\t\t\tif (i >= last) break;\n\n\t\t\tbool selected = (i == _sel);\n\t\t\tif (selected) {\n\t\t\t\tp.fillRect(0, i * st::mentionHeight, width(), st::mentionHeight, st::mentionBgOver);\n\t\t\t\tint skip = (st::mentionHeight - st::smallCloseIconOver.height()) / 2;\n\t\t\t\tif (!_hrows->empty() || (!_mrows->empty() && i < _recentInlineBotsInRows)) {\n\t\t\t\t\tst::smallCloseIconOver.paint(p, QPoint(width() - st::smallCloseIconOver.width() - skip, i * st::mentionHeight + skip), width());\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!_mrows->empty()) {\n\t\t\t\tauto &row = _mrows->at(i);\n\t\t\t\tconst auto user = row.user;\n\t\t\t\tauto first = (!filterIsEmpty\n\t\t\t\t\t\t&& PrimaryUsername(user).startsWith(\n\t\t\t\t\t\t\tfilter,\n\t\t\t\t\t\t\tQt::CaseInsensitive))\n\t\t\t\t\t? ('@' + PrimaryUsername(user).mid(0, filterSize))\n\t\t\t\t\t: QString();\n\t\t\t\tauto second = first.isEmpty()\n\t\t\t\t\t? (PrimaryUsername(user).isEmpty()\n\t\t\t\t\t\t? QString()\n\t\t\t\t\t\t: ('@' + PrimaryUsername(user)))\n\t\t\t\t\t: PrimaryUsername(user).mid(filterSize);\n\t\t\t\tauto firstwidth = st::mentionFont->width(first);\n\t\t\t\tauto secondwidth = st::mentionFont->width(second);\n\t\t\t\tauto unamewidth = firstwidth + secondwidth;\n\t\t\t\tif (row.name.isEmpty()) {\n\t\t\t\t\trow.name.setText(st::msgNameStyle, user->name(), Ui::NameTextOptions());\n\t\t\t\t}\n\t\t\t\tauto namewidth = row.name.maxWidth();\n\t\t\t\tif (mentionwidth < unamewidth + namewidth) {\n\t\t\t\t\tnamewidth = (mentionwidth * namewidth) / (namewidth + unamewidth);\n\t\t\t\t\tunamewidth = mentionwidth - namewidth;\n\t\t\t\t\tif (firstwidth < unamewidth + st::mentionFont->elidew) {\n\t\t\t\t\t\tif (firstwidth < unamewidth) {\n\t\t\t\t\t\t\tfirst = st::mentionFont->elided(first, unamewidth);\n\t\t\t\t\t\t} else if (!second.isEmpty()) {\n\t\t\t\t\t\t\tfirst = st::mentionFont->elided(first + second, unamewidth);\n\t\t\t\t\t\t\tsecond = QString();\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsecond = st::mentionFont->elided(second, unamewidth - firstwidth);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tuser->loadUserpic();\n\t\t\t\tuser->paintUserpicLeft(p, row.userpic, st::mentionPadding.left(), i * st::mentionHeight + st::mentionPadding.top(), width(), st::mentionPhotoSize);\n\n\t\t\t\tp.setPen(selected ? st::mentionNameFgOver : st::mentionNameFg);\n\t\t\t\trow.name.drawElided(p, 2 * st::mentionPadding.left() + st::mentionPhotoSize, i * st::mentionHeight + st::mentionTop, namewidth);\n\n\t\t\t\tp.setFont(st::mentionFont);\n\t\t\t\tp.setPen(selected ? st::mentionFgOverActive : st::mentionFgActive);\n\t\t\t\tp.drawText(mentionleft + namewidth + st::mentionPadding.right(), i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, first);\n\t\t\t\tif (!second.isEmpty()) {\n\t\t\t\t\tp.setPen(selected ? st::mentionFgOver : st::mentionFg);\n\t\t\t\t\tp.drawText(mentionleft + namewidth + st::mentionPadding.right() + firstwidth, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, second);\n\t\t\t\t}\n\t\t\t} else if (!_hrows->empty()) {\n\t\t\t\tQString hrow = _hrows->at(i);\n\t\t\t\tQString first = filterIsEmpty ? QString() : ('#' + hrow.mid(0, filterSize));\n\t\t\t\tQString second = filterIsEmpty ? ('#' + hrow) : hrow.mid(filterSize);\n\t\t\t\tint32 firstwidth = st::mentionFont->width(first), secondwidth = st::mentionFont->width(second);\n\t\t\t\tif (htagwidth < firstwidth + secondwidth) {\n\t\t\t\t\tif (htagwidth < firstwidth + st::mentionFont->elidew) {\n\t\t\t\t\t\tfirst = st::mentionFont->elided(first + second, htagwidth);\n\t\t\t\t\t\tsecond = QString();\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsecond = st::mentionFont->elided(second, htagwidth - firstwidth);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tp.setFont(st::mentionFont);\n\t\t\t\tif (!first.isEmpty()) {\n\t\t\t\t\tp.setPen((selected ? st::mentionFgOverActive : st::mentionFgActive)->p);\n\t\t\t\t\tp.drawText(htagleft, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, first);\n\t\t\t\t}\n\t\t\t\tif (!second.isEmpty()) {\n\t\t\t\t\tp.setPen((selected ? st::mentionFgOver : st::mentionFg)->p);\n\t\t\t\t\tp.drawText(htagleft + firstwidth, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, second);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tauto &row = _brows->at(i);\n\t\t\t\tconst auto user = row.user;\n\t\t\t\tif (user->isSelf() && row.command.isEmpty()) {\n\t\t\t\t\tp.setPen(st::windowActiveTextFg);\n\t\t\t\t\tp.setFont(st::semiboldFont);\n\t\t\t\t\tp.drawText(\n\t\t\t\t\t\tQRect(0, i * st::mentionHeight, width(), st::mentionHeight),\n\t\t\t\t\t\ttr::lng_replies_edit_button(tr::now),\n\t\t\t\t\t\tstyle::al_center);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tauto toHighlight = row.command;\n\t\t\t\tconst auto botStatus = _parent->chat() ? _parent->chat()->botStatus : ((_parent->channel() && _parent->channel()->isMegagroup()) ? _parent->channel()->mgInfo->botStatus : Data::BotStatus::NoBots);\n\t\t\t\tif (hasUsername || botStatus != Data::BotStatus::NoBots) {\n\t\t\t\t\ttoHighlight += '@' + PrimaryUsername(user);\n\t\t\t\t}\n\t\t\t\tuser->loadUserpic();\n\t\t\t\tuser->paintUserpicLeft(p, row.userpic, st::mentionPadding.left(), i * st::mentionHeight + st::mentionPadding.top(), width(), st::mentionPhotoSize);\n\n\t\t\t\tauto commandText = '/' + toHighlight;\n\n\t\t\t\tp.setPen(selected ? st::mentionNameFgOver : st::mentionNameFg);\n\t\t\t\tp.setFont(st::semiboldFont);\n\t\t\t\tp.drawText(2 * st::mentionPadding.left() + st::mentionPhotoSize, i * st::mentionHeight + st::mentionTop + st::semiboldFont->ascent, commandText);\n\n\t\t\t\tauto commandTextWidth = st::semiboldFont->width(commandText);\n\t\t\t\tauto addleft = commandTextWidth + st::mentionPadding.left();\n\t\t\t\tauto widthleft = mentionwidth - addleft;\n\n\t\t\t\tif (!row.description.isEmpty()\n\t\t\t\t\t&& row.descriptionText.isEmpty()) {\n\t\t\t\t\trow.descriptionText.setText(\n\t\t\t\t\t\tst::defaultTextStyle,\n\t\t\t\t\t\trow.description,\n\t\t\t\t\t\tUi::NameTextOptions());\n\t\t\t\t}\n\t\t\t\tif (widthleft > st::mentionFont->elidew && !row.descriptionText.isEmpty()) {\n\t\t\t\t\tp.setPen((selected ? st::mentionFgOver : st::mentionFg)->p);\n\t\t\t\t\trow.descriptionText.drawElided(p, mentionleft + addleft, i * st::mentionHeight + st::mentionTop, widthleft);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tp.fillRect(\n\t\t\t_adjustShadowLeft ? st::lineWidth : 0,\n\t\t\t_parent->innerBottom() - st::lineWidth,\n\t\t\twidth() - (_adjustShadowLeft ? st::lineWidth : 0),\n\t\t\tst::lineWidth,\n\t\t\tst::shadowFg);\n\t}\n\tp.fillRect(\n\t\t_adjustShadowLeft ? st::lineWidth : 0,\n\t\t_parent->innerTop(),\n\t\twidth() - (_adjustShadowLeft ? st::lineWidth : 0),\n\t\tst::lineWidth,\n\t\tst::shadowFg);\n}\n\nvoid FieldAutocomplete::Inner::resizeEvent(QResizeEvent *e) {\n\t_stickersPerRow = qMax(1, int32(width() - 2 * st::stickerPanPadding) / int32(st::stickerPanSize.width()));\n}\n\nvoid FieldAutocomplete::Inner::mouseMoveEvent(QMouseEvent *e) {\n\tconst auto globalPosition = e->globalPos();\n\tif (!_lastMousePosition) {\n\t\t_lastMousePosition = globalPosition;\n\t\treturn;\n\t} else if (!_mouseSelection\n\t\t&& *_lastMousePosition == globalPosition) {\n\t\treturn;\n\t}\n\tselectByMouse(globalPosition);\n}\n\nvoid FieldAutocomplete::Inner::clearSel(bool hidden) {\n\t_overDelete = false;\n\t_mouseSelection = false;\n\t_lastMousePosition = std::nullopt;\n\tsetSel((_mrows->empty() && _brows->empty() && _hrows->empty())\n\t\t? -1\n\t\t: (_brows->size() > 1\n\t\t\t&& _brows->front().user->isSelf()\n\t\t\t&& _brows->front().command.isEmpty())\n\t\t? 1\n\t\t: 0);\n\tif (hidden) {\n\t\t_down = -1;\n\t\t_previewShown = false;\n\t}\n}\n\nbool FieldAutocomplete::Inner::moveSel(int key) {\n\t_mouseSelection = false;\n\t_lastMousePosition = std::nullopt;\n\n\tint32 maxSel = !_mrows->empty()\n\t\t? _mrows->size()\n\t\t: !_hrows->empty()\n\t\t? _hrows->size()\n\t\t: !_brows->empty()\n\t\t? _brows->size()\n\t\t: _srows->size();\n\tint32 direction = (key == Qt::Key_Up) ? -1 : (key == Qt::Key_Down ? 1 : 0);\n\tif (!_srows->empty()) {\n\t\tif (key == Qt::Key_Left) {\n\t\t\tdirection = -1;\n\t\t} else if (key == Qt::Key_Right) {\n\t\t\tdirection = 1;\n\t\t} else {\n\t\t\tdirection *= _stickersPerRow;\n\t\t}\n\t}\n\tif (_sel >= maxSel || _sel < 0) {\n\t\tif (direction < -1) {\n\t\t\tsetSel(((maxSel - 1) / _stickersPerRow) * _stickersPerRow, true);\n\t\t} else if (direction < 0) {\n\t\t\tsetSel(maxSel - 1, true);\n\t\t} else {\n\t\t\tsetSel(0, true);\n\t\t}\n\t\treturn (_sel >= 0 && _sel < maxSel);\n\t}\n\tsetSel((_sel + direction >= maxSel || _sel + direction < 0) ? -1 : (_sel + direction), true);\n\treturn true;\n}\n\nbool FieldAutocomplete::Inner::chooseSelected(\n\t\tFieldAutocomplete::ChooseMethod method) const {\n\treturn chooseAtIndex(method, _sel);\n}\n\nbool FieldAutocomplete::Inner::chooseAtIndex(\n\t\tFieldAutocomplete::ChooseMethod method,\n\t\tint index,\n\t\tApi::SendOptions options) const {\n\tif (index < 0 || (method == ChooseMethod::ByEnter && _mouseSelection)) {\n\t\treturn false;\n\t}\n\tif (!_srows->empty()) {\n\t\tif (index < _srows->size()) {\n\t\t\tconst auto document = (*_srows)[index].document;\n\n\t\t\tconst auto from = [&]() -> Ui::MessageSendingAnimationFrom {\n\t\t\t\tif (options.scheduled) {\n\t\t\t\t\treturn {};\n\t\t\t\t}\n\t\t\t\tconst auto bounding = selectedRect(index);\n\t\t\t\tauto contentRect = QRect(\n\t\t\t\t\tQPoint(),\n\t\t\t\t\tComputeStickerSize(\n\t\t\t\t\t\tdocument,\n\t\t\t\t\t\tstickerBoundingBox()));\n\t\t\t\tcontentRect.moveCenter(bounding.center());\n\t\t\t\treturn {\n\t\t\t\t\tUi::MessageSendingAnimationFrom::Type::Sticker,\n\t\t\t\t\t_show->session().data().nextLocalMessageId(),\n\t\t\t\t\tmapToGlobal(std::move(contentRect)),\n\t\t\t\t};\n\t\t\t};\n\n\t\t\t_stickerChosen.fire({ document, options, from() });\n\t\t\treturn true;\n\t\t}\n\t} else if (!_mrows->empty()) {\n\t\tif (index < _mrows->size()) {\n\t\t\tconst auto user = _mrows->at(index).user;\n\t\t\t_mentionChosen.fire({ user, PrimaryUsername(user), method });\n\t\t\treturn true;\n\t\t}\n\t} else if (!_hrows->empty()) {\n\t\tif (index < _hrows->size()) {\n\t\t\t_hashtagChosen.fire({ '#' + _hrows->at(index), method });\n\t\t\treturn true;\n\t\t}\n\t} else if (!_brows->empty()) {\n\t\tif (index < _brows->size()) {\n\t\t\tconst auto user = _brows->at(index).user;\n\t\t\tconst auto &command = _brows->at(index).command;\n\t\t\tconst auto botStatus = _parent->chat()\n\t\t\t\t? _parent->chat()->botStatus\n\t\t\t\t: ((_parent->channel() && _parent->channel()->isMegagroup())\n\t\t\t\t\t? _parent->channel()->mgInfo->botStatus\n\t\t\t\t\t: Data::BotStatus::NoBots);\n\n\t\t\tconst auto insertUsername = (botStatus != Data::BotStatus::NoBots\n\t\t\t\t|| _parent->filter().indexOf('@') > 0);\n\t\t\tconst auto commandString = QString(\"/%1%2\").arg(\n\t\t\t\tcommand,\n\t\t\t\tinsertUsername ? ('@' + PrimaryUsername(user)) : QString());\n\t\t\t_botCommandChosen.fire({ user, commandString, method });\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid FieldAutocomplete::Inner::setRecentInlineBotsInRows(int32 bots) {\n\t_recentInlineBotsInRows = bots;\n}\n\nvoid FieldAutocomplete::Inner::mousePressEvent(QMouseEvent *e) {\n\tselectByMouse(e->globalPos());\n\tif (e->button() == Qt::LeftButton) {\n\t\tif (_overDelete && _sel >= 0 && _sel < (_mrows->empty() ? _hrows->size() : _recentInlineBotsInRows)) {\n\t\t\tbool removed = false;\n\t\t\tif (_mrows->empty()) {\n\t\t\t\tQString toRemove = _hrows->at(_sel);\n\t\t\t\tRecentHashtagPack &recent(cRefRecentWriteHashtags());\n\t\t\t\tfor (RecentHashtagPack::iterator i = recent.begin(); i != recent.cend();) {\n\t\t\t\t\tif (i->first == toRemove) {\n\t\t\t\t\t\ti = recent.erase(i);\n\t\t\t\t\t\tremoved = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t++i;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tUserData *toRemove = _mrows->at(_sel).user;\n\t\t\t\tRecentInlineBots &recent(cRefRecentInlineBots());\n\t\t\t\tint32 index = recent.indexOf(toRemove);\n\t\t\t\tif (index >= 0) {\n\t\t\t\t\trecent.remove(index);\n\t\t\t\t\tremoved = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (removed) {\n\t\t\t\t_show->session().local().writeRecentHashtagsAndBots();\n\t\t\t}\n\t\t\t_parent->updateFiltered();\n\n\t\t\tselectByMouse(e->globalPos());\n\t\t} else if (_srows->empty()) {\n\t\t\tchooseSelected(FieldAutocomplete::ChooseMethod::ByClick);\n\t\t} else {\n\t\t\t_down = _sel;\n\t\t\t_previewTimer.callOnce(QApplication::startDragTime());\n\t\t}\n\t}\n}\n\nvoid FieldAutocomplete::Inner::mouseReleaseEvent(QMouseEvent *e) {\n\t_previewTimer.cancel();\n\n\tint32 pressed = _down;\n\t_down = -1;\n\n\tselectByMouse(e->globalPos());\n\n\tif (_previewShown) {\n\t\t_previewShown = false;\n\t\treturn;\n\t}\n\n\tif (_sel < 0 || _sel != pressed || _srows->empty()) return;\n\n\tchooseSelected(FieldAutocomplete::ChooseMethod::ByClick);\n}\n\nvoid FieldAutocomplete::Inner::contextMenuEvent(QContextMenuEvent *e) {\n\tif (_sel < 0 || _srows->empty() || _down >= 0) {\n\t\treturn;\n\t}\n\tconst auto index = _sel;\n\tconst auto details = _sendMenuDetails\n\t\t? _sendMenuDetails()\n\t\t: SendMenu::Details();\n\tconst auto method = FieldAutocomplete::ChooseMethod::ByClick;\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\tst::popupMenuWithIcons);\n\n\tconst auto send = crl::guard(this, [=](Api::SendOptions options) {\n\t\tchooseAtIndex(method, index, options);\n\t});\n\tSendMenu::FillSendMenu(\n\t\t_menu,\n\t\t_show,\n\t\tdetails,\n\t\tSendMenu::DefaultCallback(_show, send));\n\tif (!_menu->empty()) {\n\t\t_menu->popup(QCursor::pos());\n\t}\n}\n\nvoid FieldAutocomplete::Inner::enterEventHook(QEnterEvent *e) {\n\tsetMouseTracking(true);\n}\n\nvoid FieldAutocomplete::Inner::leaveEventHook(QEvent *e) {\n\tsetMouseTracking(false);\n\tif (_mouseSelection) {\n\t\tsetSel(-1);\n\t\t_mouseSelection = false;\n\t\t_lastMousePosition = std::nullopt;\n\t}\n}\n\nQRect FieldAutocomplete::Inner::selectedRect(int index) const {\n\tif (index < 0) {\n\t\treturn QRect();\n\t}\n\tif (_srows->empty()) {\n\t\treturn { 0, index * st::mentionHeight, width(), st::mentionHeight };\n\t} else {\n\t\tconst auto row = int(index / _stickersPerRow);\n\t\tconst auto col = int(index % _stickersPerRow);\n\t\treturn {\n\t\t\tst::stickerPanPadding + col * st::stickerPanSize.width(),\n\t\t\tst::stickerPanPadding + row * st::stickerPanSize.height(),\n\t\t\tst::stickerPanSize.width(),\n\t\t\tst::stickerPanSize.height()\n\t\t};\n\t}\n}\n\nvoid FieldAutocomplete::Inner::updateSelectedRow() {\n\tconst auto rect = selectedRect(_sel);\n\tif (rect.isValid()) {\n\t\tupdate(rect);\n\t}\n}\n\nvoid FieldAutocomplete::Inner::setSel(int sel, bool scroll) {\n\tupdateSelectedRow();\n\t_sel = sel;\n\tupdateSelectedRow();\n\n\tif (scroll && _sel >= 0) {\n\t\tif (_srows->empty()) {\n\t\t\t_scrollToRequested.fire({\n\t\t\t\t_sel * st::mentionHeight,\n\t\t\t\t(_sel + 1) * st::mentionHeight });\n\t\t} else {\n\t\t\tint32 row = _sel / _stickersPerRow;\n\t\t\tconst auto padding = st::stickerPanPadding;\n\t\t\t_scrollToRequested.fire({\n\t\t\t\t(row ? padding : 0) + row * st::stickerPanSize.height(),\n\t\t\t\t(padding\n\t\t\t\t\t+ (row + 1) * st::stickerPanSize.height()\n\t\t\t\t\t+ _st.autocompleteBottomSkip) });\n\t\t}\n\t}\n}\n\nvoid FieldAutocomplete::Inner::rowsUpdated() {\n\tif (_srows->empty()) {\n\t\t_stickersLifetime.destroy();\n\t}\n}\n\nauto FieldAutocomplete::Inner::getLottieRenderer()\n-> std::shared_ptr<Lottie::FrameRenderer> {\n\tif (auto result = _lottieRenderer.lock()) {\n\t\treturn result;\n\t}\n\tauto result = Lottie::MakeFrameRenderer();\n\t_lottieRenderer = result;\n\treturn result;\n}\n\nvoid FieldAutocomplete::Inner::setupLottie(StickerSuggestion &suggestion) {\n\tconst auto document = suggestion.document;\n\tsuggestion.lottie = LottiePlayerFromDocument(\n\t\tsuggestion.documentMedia.get(),\n\t\tStickerLottieSize::InlineResults,\n\t\tstickerBoundingBox() * style::DevicePixelRatio(),\n\t\tLottie::Quality::Default,\n\t\tgetLottieRenderer());\n\n\tsuggestion.lottie->updates(\n\t) | rpl::on_next([=] {\n\t\trepaintSticker(document);\n\t}, _stickersLifetime);\n}\n\nvoid FieldAutocomplete::Inner::setupWebm(StickerSuggestion &suggestion) {\n\tconst auto document = suggestion.document;\n\tauto callback = [=](Media::Clip::Notification notification) {\n\t\tclipCallback(notification, document);\n\t};\n\tsuggestion.webm = Media::Clip::MakeReader(\n\t\tsuggestion.documentMedia->owner()->location(),\n\t\tsuggestion.documentMedia->bytes(),\n\t\tstd::move(callback));\n}\n\nQSize FieldAutocomplete::Inner::stickerBoundingBox() const {\n\treturn QSize(\n\t\tst::stickerPanSize.width() - st::roundRadiusSmall * 2,\n\t\tst::stickerPanSize.height() - st::roundRadiusSmall * 2);\n}\n\nvoid FieldAutocomplete::Inner::repaintSticker(\n\t\tnot_null<DocumentData*> document) {\n\tconst auto i = ranges::find(\n\t\t*_srows,\n\t\tdocument,\n\t\t&StickerSuggestion::document);\n\tif (i == end(*_srows)) {\n\t\treturn;\n\t}\n\trepaintStickerAtIndex(i - begin(*_srows));\n}\n\nvoid FieldAutocomplete::Inner::repaintStickerAtIndex(int index) {\n\tconst auto row = (index / _stickersPerRow);\n\tconst auto col = (index % _stickersPerRow);\n\tupdate(\n\t\tst::stickerPanPadding + col * st::stickerPanSize.width(),\n\t\tst::stickerPanPadding + row * st::stickerPanSize.height(),\n\t\tst::stickerPanSize.width(),\n\t\tst::stickerPanSize.height());\n}\n\nvoid FieldAutocomplete::Inner::clipCallback(\n\t\tMedia::Clip::Notification notification,\n\t\tnot_null<DocumentData*> document) {\n\tconst auto i = ranges::find(\n\t\t*_srows,\n\t\tdocument,\n\t\t&StickerSuggestion::document);\n\tif (i == end(*_srows)) {\n\t\treturn;\n\t}\n\tusing namespace Media::Clip;\n\tswitch (notification) {\n\tcase Notification::Reinit: {\n\t\tif (!i->webm) {\n\t\t\tbreak;\n\t\t} else if (i->webm->state() == State::Error) {\n\t\t\ti->webm.setBad();\n\t\t} else if (i->webm->ready() && !i->webm->started()) {\n\t\t\tconst auto size = ComputeStickerSize(\n\t\t\t\ti->document,\n\t\t\t\tstickerBoundingBox());\n\t\t\ti->webm->start({ .frame = size, .keepAlpha = true });\n\t\t}\n\t} break;\n\n\tcase Notification::Repaint: break;\n\t}\n\trepaintStickerAtIndex(i - begin(*_srows));\n}\n\nvoid FieldAutocomplete::Inner::selectByMouse(QPoint globalPosition) {\n\t_mouseSelection = true;\n\t_lastMousePosition = globalPosition;\n\tconst auto mouse = mapFromGlobal(globalPosition);\n\n\tif (_down >= 0 && !_previewShown) {\n\t\treturn;\n\t}\n\n\tint32 sel = -1, maxSel = 0;\n\tif (!_srows->empty()) {\n\t\tint32 row = (mouse.y() >= st::stickerPanPadding) ? ((mouse.y() - st::stickerPanPadding) / st::stickerPanSize.height()) : -1;\n\t\tint32 col = (mouse.x() >= st::stickerPanPadding) ? ((mouse.x() - st::stickerPanPadding) / st::stickerPanSize.width()) : -1;\n\t\tif (row >= 0 && col >= 0) {\n\t\t\tsel = row * _stickersPerRow + col;\n\t\t}\n\t\tmaxSel = _srows->size();\n\t\t_overDelete = false;\n\t} else {\n\t\tsel = mouse.y() / int32(st::mentionHeight);\n\t\tmaxSel = !_mrows->empty()\n\t\t\t? _mrows->size()\n\t\t\t: !_hrows->empty()\n\t\t\t? _hrows->size()\n\t\t\t: _brows->size();\n\t\t_overDelete = (!_hrows->empty() || (!_mrows->empty() && sel < _recentInlineBotsInRows)) ? (mouse.x() >= width() - st::mentionHeight) : false;\n\t}\n\tif (sel < 0 || sel >= maxSel) {\n\t\tsel = -1;\n\t}\n\tif (sel != _sel) {\n\t\tsetSel(sel);\n\t\tif (_down >= 0 && _sel >= 0 && _down != _sel) {\n\t\t\t_down = _sel;\n\t\t\tshowPreview();\n\t\t}\n\t}\n}\n\nvoid FieldAutocomplete::Inner::onParentGeometryChanged() {\n\tconst auto globalPosition = QCursor::pos();\n\tif (rect().contains(mapFromGlobal(globalPosition))) {\n\t\tsetMouseTracking(true);\n\t\tif (_mouseSelection) {\n\t\t\tselectByMouse(globalPosition);\n\t\t}\n\t}\n}\n\nvoid FieldAutocomplete::Inner::showPreview() {\n\tif (_down >= 0 && _down < _srows->size()) {\n\t\tconst auto document = (*_srows)[_down].document;\n\t\t_show->showMediaPreview(document->stickerSetOrigin(), document);\n\t\t_previewShown = true;\n\t}\n}\n\nvoid FieldAutocomplete::Inner::setSendMenuDetails(\n\t\tFn<SendMenu::Details()> &&callback) {\n\t_sendMenuDetails = std::move(callback);\n}\n\nauto FieldAutocomplete::Inner::mentionChosen() const\n-> rpl::producer<FieldAutocomplete::MentionChosen> {\n\treturn _mentionChosen.events();\n}\n\nauto FieldAutocomplete::Inner::hashtagChosen() const\n-> rpl::producer<FieldAutocomplete::HashtagChosen> {\n\treturn _hashtagChosen.events();\n}\n\nauto FieldAutocomplete::Inner::botCommandChosen() const\n-> rpl::producer<FieldAutocomplete::BotCommandChosen> {\n\treturn _botCommandChosen.events();\n}\n\nauto FieldAutocomplete::Inner::stickerChosen() const\n-> rpl::producer<FieldAutocomplete::StickerChosen> {\n\treturn _stickerChosen.events();\n}\n\nauto FieldAutocomplete::Inner::scrollToRequested() const\n-> rpl::producer<ScrollTo> {\n\treturn _scrollToRequested.events();\n}\n\nvoid InitFieldAutocomplete(\n\t\tstd::unique_ptr<FieldAutocomplete> &autocomplete,\n\t\tFieldAutocompleteDescriptor &&descriptor) {\n\tExpects(!autocomplete);\n\n\tautocomplete = std::make_unique<FieldAutocomplete>(\n\t\tdescriptor.parent,\n\t\tdescriptor.show,\n\t\tdescriptor.stOverride);\n\tconst auto raw = autocomplete.get();\n\tconst auto field = descriptor.field;\n\n\tfield->rawTextEdit()->installEventFilter(raw);\n\n\traw->mentionChosen(\n\t) | rpl::on_next([=](FieldAutocomplete::MentionChosen data) {\n\t\tconst auto useUsername =\n\t\t\tCore::App().settings().fork().mentionByNameDisabled()\n\t\t\t\t|| ((data.method == FieldAutocomplete::ChooseMethod::ByClick)\n\t\t\t\t\t&& base::IsCtrlPressed());\n\t\tconst auto user = data.user;\n\t\tconst auto ctrlClick = base::IsCtrlPressed()\n\t\t\t&& data.method == FieldAutocomplete::ChooseMethod::ByClick;\n\t\tif (data.mention.isEmpty() || ctrlClick || !useUsername) {\n\t\t\tfield->insertTag(\n\t\t\t\tuser->firstName.isEmpty() ? user->name() : user->firstName,\n\t\t\t\tPrepareMentionTag(user));\n\t\t} else {\n\t\t\tfield->insertTag('@' + data.mention);\n\t\t}\n\t}, raw->lifetime());\n\n\tconst auto sendCommand = descriptor.sendBotCommand;\n\tconst auto setText = descriptor.setText;\n\n\traw->hashtagChosen(\n\t) | rpl::on_next([=](FieldAutocomplete::HashtagChosen data) {\n\t\tfield->insertTag(data.hashtag);\n\t}, raw->lifetime());\n\n\tconst auto peer = descriptor.peer;\n\tconst auto features = descriptor.features;\n\tconst auto processShortcut = descriptor.processShortcut;\n\tconst auto shortcutMessages = (processShortcut != nullptr)\n\t\t? &peer->owner().shortcutMessages()\n\t\t: nullptr;\n\traw->botCommandChosen(\n\t) | rpl::on_next([=](FieldAutocomplete::BotCommandChosen data) {\n\t\tif (!features().autocompleteCommands) {\n\t\t\treturn;\n\t\t}\n\t\tusing Method = FieldAutocompleteChooseMethod;\n\t\tconst auto byTab = (data.method == Method::ByTab);\n\t\tconst auto shortcut = data.user->isSelf();\n\n\t\t// Send bot command at once, if it was not inserted by pressing Tab.\n\t\tif (byTab && data.command.size() > 1) {\n\t\t\tfield->insertTag(data.command);\n\t\t} else if (!shortcut) {\n\t\t\tsendCommand(data.command);\n\t\t\tsetText(\n\t\t\t\tfield->getTextWithTagsPart(field->textCursor().position()));\n\t\t} else if (processShortcut) {\n\t\t\tprocessShortcut(data.command.mid(1));\n\t\t}\n\t}, raw->lifetime());\n\n\traw->setModerateKeyActivateCallback(std::move(descriptor.moderateKeyActivateCallback));\n\n\tif (const auto stickerChoosing = descriptor.stickerChoosing) {\n\t\traw->choosingProcesses(\n\t\t) | rpl::on_next([=](FieldAutocomplete::Type type) {\n\t\t\tif (type == FieldAutocomplete::Type::Stickers) {\n\t\t\t\tstickerChoosing();\n\t\t\t}\n\t\t}, raw->lifetime());\n\t}\n\tif (const auto chosen = descriptor.stickerChosen) {\n\t\traw->stickerChosen(\n\t\t) | rpl::on_next(chosen, raw->lifetime());\n\t}\n\n\tfield->tabbed(\n\t) | rpl::on_next([=](not_null<bool*> handled) {\n\t\tif (!raw->isHidden()) {\n\t\t\traw->chooseSelected(FieldAutocomplete::ChooseMethod::ByTab);\n\t\t\t*handled = true;\n\t\t}\n\t}, raw->lifetime());\n\n\tconst auto check = [=] {\n\t\tauto parsed = ParseMentionHashtagBotCommandQuery(field, features());\n\t\tif (parsed.query.isEmpty()) {\n\t\t} else if (parsed.query[0] == '#'\n\t\t\t&& cRecentWriteHashtags().isEmpty()\n\t\t\t&& cRecentSearchHashtags().isEmpty()) {\n\t\t\tpeer->session().local().readRecentHashtagsAndBots();\n\t\t} else if (parsed.query[0] == '@'\n\t\t\t&& cRecentInlineBots().isEmpty()) {\n\t\t\tpeer->session().local().readRecentHashtagsAndBots();\n\t\t} else if (parsed.query[0] == '/'\n\t\t\t&& peer->isUser()\n\t\t\t&& !peer->asUser()->isBot()\n\t\t\t&& (!shortcutMessages\n\t\t\t\t|| shortcutMessages->shortcuts().list.empty()\n\t\t\t\t|| peer->starsPerMessageChecked() != 0)) {\n\t\t\tparsed = {};\n\t\t}\n\t\traw->showFiltered(peer, parsed.query, parsed.fromStart);\n\t};\n\n\tconst auto updateStickersByEmoji = [=] {\n\t\tconst auto errorForStickers = Data::RestrictionError(\n\t\t\tpeer,\n\t\t\tChatRestriction::SendStickers);\n\t\tif (features().suggestStickersByEmoji && !errorForStickers) {\n\t\t\tconst auto &text = field->getTextWithTags().text;\n\t\t\tauto length = 0;\n\t\t\tif (const auto emoji = Ui::Emoji::Find(text, &length)) {\n\t\t\t\tif (text.size() <= length) {\n\t\t\t\t\traw->showStickers(emoji);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\traw->showStickers(nullptr);\n\t};\n\n\traw->refreshRequests(\n\t) | rpl::on_next(check, raw->lifetime());\n\n\traw->stickersUpdateRequests(\n\t) | rpl::on_next(updateStickersByEmoji, raw->lifetime());\n\n\tpeer->owner().botCommandsChanges(\n\t) | rpl::filter([=](not_null<PeerData*> changed) {\n\t\treturn (peer == changed);\n\t}) | rpl::on_next([=] {\n\t\tif (raw->clearFilteredBotCommands()) {\n\t\t\tcheck();\n\t\t}\n\t}, raw->lifetime());\n\n\tpeer->owner().stickers().updated(\n\t\tData::StickersType::Stickers\n\t) | rpl::on_next(updateStickersByEmoji, raw->lifetime());\n\n\tQObject::connect(\n\t\tfield->rawTextEdit(),\n\t\t&QTextEdit::cursorPositionChanged,\n\t\traw,\n\t\tcheck,\n\t\tQt::QueuedConnection);\n\n\tfield->changes() | rpl::on_next(\n\t\tupdateStickersByEmoji,\n\t\traw->lifetime());\n\n\tpeer->session().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::Rights\n\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\treturn (update.peer == peer);\n\t}) | rpl::on_next(updateStickersByEmoji, raw->lifetime());\n\n\tif (shortcutMessages) {\n\t\tshortcutMessages->shortcutsChanged(\n\t\t) | rpl::on_next(check, raw->lifetime());\n\t}\n\n\traw->setSendMenuDetails(std::move(descriptor.sendMenuDetails));\n\traw->hideFast();\n}\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/field_autocomplete.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_common.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/message_sending_animation_common.h\"\n#include \"ui/rp_widget.h\"\n#include \"base/timer.h\"\n#include \"base/object_ptr.h\"\n\nnamespace style {\nstruct EmojiPan;\n} // namespace style\n\nnamespace Ui {\nclass PopupMenu;\nclass ScrollArea;\nclass InputField;\n} // namespace Ui\n\nnamespace Lottie {\nclass SinglePlayer;\nclass FrameRenderer;\n} // namespace Lottie;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Data {\nclass DocumentMedia;\n} // namespace Data\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nnamespace ChatHelpers {\n\nstruct ComposeFeatures;\nstruct FileChosen;\nclass Show;\n\nenum class FieldAutocompleteChooseMethod {\n\tByEnter,\n\tByTab,\n\tByClick,\n};\n\nclass FieldAutocomplete final : public Ui::RpWidget {\npublic:\n\tFieldAutocomplete(\n\t\tQWidget *parent,\n\t\tstd::shared_ptr<Show> show,\n\t\tconst style::EmojiPan *stOverride = nullptr);\n\t~FieldAutocomplete();\n\n\t[[nodiscard]] std::shared_ptr<Show> uiShow() const;\n\n\tbool clearFilteredBotCommands();\n\tvoid showFiltered(\n\t\tnot_null<PeerData*> peer,\n\t\tQString query,\n\t\tbool addInlineBots);\n\n\tvoid showStickers(EmojiPtr emoji);\n\t[[nodiscard]] EmojiPtr stickersEmoji() const;\n\n\tvoid setBoundings(QRect boundings);\n\n\t[[nodiscard]] const QString &filter() const;\n\t[[nodiscard]] ChatData *chat() const;\n\t[[nodiscard]] ChannelData *channel() const;\n\t[[nodiscard]] UserData *user() const;\n\n\t[[nodiscard]] int32 innerTop();\n\t[[nodiscard]] int32 innerBottom();\n\n\tbool eventFilter(QObject *obj, QEvent *e) override;\n\n\tusing ChooseMethod = FieldAutocompleteChooseMethod;\n\tstruct MentionChosen {\n\t\tnot_null<UserData*> user;\n\t\tQString mention;\n\t\tChooseMethod method = ChooseMethod::ByEnter;\n\t};\n\tstruct HashtagChosen {\n\t\tQString hashtag;\n\t\tChooseMethod method = ChooseMethod::ByEnter;\n\t};\n\tstruct BotCommandChosen {\n\t\tnot_null<UserData*> user;\n\t\tQString command;\n\t\tChooseMethod method = ChooseMethod::ByEnter;\n\t};\n\tusing StickerChosen = FileChosen;\n\tenum class Type {\n\t\tMentions,\n\t\tHashtags,\n\t\tBotCommands,\n\t\tStickers,\n\t};\n\n\tbool chooseSelected(ChooseMethod method) const;\n\n\t[[nodiscard]] bool stickersShown() const {\n\t\treturn !_srows.empty();\n\t}\n\n\t[[nodiscard]] bool overlaps(const QRect &globalRect) {\n\t\tif (isHidden() || !testAttribute(Qt::WA_OpaquePaintEvent)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn rect().contains(QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size()));\n\t}\n\n\tvoid setModerateKeyActivateCallback(Fn<bool(int)> callback) {\n\t\t_moderateKeyActivateCallback = std::move(callback);\n\t}\n\tvoid setSendMenuDetails(Fn<SendMenu::Details()> &&callback);\n\n\tvoid hideFast();\n\tvoid showAnimated();\n\tvoid hideAnimated();\n\n\tvoid requestRefresh();\n\t[[nodiscard]] rpl::producer<> refreshRequests() const;\n\tvoid requestStickersUpdate();\n\t[[nodiscard]] rpl::producer<> stickersUpdateRequests() const;\n\n\t[[nodiscard]] rpl::producer<MentionChosen> mentionChosen() const;\n\t[[nodiscard]] rpl::producer<HashtagChosen> hashtagChosen() const;\n\t[[nodiscard]] rpl::producer<BotCommandChosen> botCommandChosen() const;\n\t[[nodiscard]] rpl::producer<StickerChosen> stickerChosen() const;\n\t[[nodiscard]] rpl::producer<Type> choosingProcesses() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tclass Inner;\n\tfriend class Inner;\n\tstruct StickerSuggestion;\n\tstruct MentionRow;\n\tstruct BotCommandRow;\n\n\tusing HashtagRows = std::vector<QString>;\n\tusing BotCommandRows = std::vector<BotCommandRow>;\n\tusing StickerRows = std::vector<StickerSuggestion>;\n\tusing MentionRows = std::vector<MentionRow>;\n\n\tvoid animationCallback();\n\tvoid hideFinish();\n\n\tvoid updateFiltered(bool resetScroll = false);\n\tvoid recount(bool resetScroll = false);\n\tStickerRows getStickerSuggestions();\n\n\tconst std::shared_ptr<Show> _show;\n\tconst not_null<Main::Session*> _session;\n\tconst style::EmojiPan &_st;\n\tQPixmap _cache;\n\tMentionRows _mrows;\n\tHashtagRows _hrows;\n\tBotCommandRows _brows;\n\tStickerRows _srows;\n\n\tvoid rowsUpdated(\n\t\tMentionRows &&mrows,\n\t\tHashtagRows &&hrows,\n\t\tBotCommandRows &&brows,\n\t\tStickerRows &&srows,\n\t\tbool resetScroll);\n\n\tobject_ptr<Ui::ScrollArea> _scroll;\n\tQPointer<Inner> _inner;\n\n\tChatData *_chat = nullptr;\n\tUserData *_user = nullptr;\n\tChannelData *_channel = nullptr;\n\tEmojiPtr _emoji;\n\tuint64 _stickersSeed = 0;\n\tType _type = Type::Mentions;\n\tQString _filter;\n\tQRect _boundings;\n\tbool _addInlineBots;\n\n\tbool _hiding = false;\n\n\tUi::Animations::Simple _a_opacity;\n\trpl::event_stream<> _refreshRequests;\n\trpl::event_stream<> _stickersUpdateRequests;\n\n\tFn<bool(int)> _moderateKeyActivateCallback;\n\n};\n\nstruct FieldAutocompleteDescriptor {\n\tnot_null<QWidget*> parent;\n\tstd::shared_ptr<Show> show;\n\tnot_null<Ui::InputField*> field;\n\tconst style::EmojiPan *stOverride = nullptr;\n\tnot_null<PeerData*> peer;\n\tFn<ComposeFeatures()> features;\n\tFn<SendMenu::Details()> sendMenuDetails;\n\tFn<void()> stickerChoosing;\n\tFn<void(FileChosen&&)> stickerChosen;\n\tFn<void(TextWithTags)> setText;\n\tFn<void(QString)> sendBotCommand;\n\tFn<void(QString)> processShortcut;\n\tFn<bool(int)> moderateKeyActivateCallback;\n};\nvoid InitFieldAutocomplete(\n\tstd::unique_ptr<FieldAutocomplete> &autocomplete,\n\tFieldAutocompleteDescriptor &&descriptor);\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/field_characters_count_manager.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/field_characters_count_manager.h\"\n\nFieldCharsCountManager::FieldCharsCountManager() = default;\n\nvoid FieldCharsCountManager::setCount(int count) {\n\t_previous = _current;\n\t_current = count;\n\tif (_previous != _current) {\n\t\tconstexpr auto kMax = 15;\n\t\tconst auto was = (_previous > kMax);\n\t\tconst auto now = (_current > kMax);\n\t\tif (was != now) {\n\t\t\t_isLimitExceeded = now;\n\t\t\t_limitExceeds.fire({});\n\t\t}\n\t}\n}\n\nint FieldCharsCountManager::count() const {\n\treturn _current;\n}\n\nbool FieldCharsCountManager::isLimitExceeded() const {\n\treturn _isLimitExceeded;\n}\n\nrpl::producer<> FieldCharsCountManager::limitExceeds() const {\n\treturn _limitExceeds.events();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/field_characters_count_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass FieldCharsCountManager final {\npublic:\n\tFieldCharsCountManager();\n\n\tvoid setCount(int count);\n\t[[nodiscard]] int count() const;\n\t[[nodiscard]] bool isLimitExceeded() const;\n\t[[nodiscard]] rpl::producer<> limitExceeds() const;\n\nprivate:\n\tint _current = 0;\n\tint _previous = 0;\n\tbool _isLimitExceeded = false;\n\n\trpl::event_stream<> _limitExceeds;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/gifs_list_widget.h\"\n\n#include \"api/api_toggling_media.h\" // Api::ToggleSavedGif\n#include \"base/const_string.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"chat_helpers/stickers_list_footer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_document.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_document_media.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"menu/menu_send.h\" // SendMenu::FillSendMenu\n#include \"mtproto/mtproto_config.h\"\n#include \"core/click_handler_types.h\"\n#include \"ui/controls/tabbed_search.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/image/image.h\"\n#include \"ui/painter.h\"\n#include \"boxes/send_gif_with_caption_box.h\"\n#include \"boxes/stickers_box.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"storage/localstorage.h\"\n#include \"lang/lang_keys.h\"\n#include \"layout/layout_position.h\"\n#include \"mainwindow.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"storage/storage_account.h\" // Account::writeSavedGifs\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace ChatHelpers {\nnamespace {\n\nconstexpr auto kSearchRequestDelay = 400;\nconstexpr auto kMinRepaintDelay = crl::time(33);\nconstexpr auto kMinAfterScrollDelay = crl::time(33);\n\n} // namespace\n\nvoid AddGifAction(\n\t\tFn<void(QString, Fn<void()> &&, const style::icon*)> callback,\n\t\tstd::shared_ptr<Show> show,\n\t\tnot_null<DocumentData*> document,\n\t\tconst style::ComposeIcons *iconsOverride) {\n\tif (!document->isGifv()) {\n\t\treturn;\n\t}\n\tauto &data = document->owner();\n\tconst auto index = data.stickers().savedGifs().indexOf(document);\n\tconst auto saved = (index >= 0);\n\tconst auto text = (saved\n\t\t? tr::lng_context_delete_gif\n\t\t: tr::lng_context_save_gif)(tr::now);\n\tconst auto &icons = iconsOverride\n\t\t? *iconsOverride\n\t\t: st::defaultComposeIcons;\n\tcallback(text, [=] {\n\t\tApi::ToggleSavedGif(\n\t\t\tshow,\n\t\t\tdocument,\n\t\t\tData::FileOriginSavedGifs(),\n\t\t\t!saved);\n\n\t\tauto &data = document->owner();\n\t\tif (saved) {\n\t\t\tdata.stickers().savedGifsRef().remove(index);\n\t\t\tdocument->session().local().writeSavedGifs();\n\t\t}\n\t\tdata.stickers().notifySavedGifsUpdated();\n\t}, saved ? &icons.menuGifRemove : &icons.menuGifAdd);\n}\n\nGifsListWidget::GifsListWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tPauseReason level)\n: GifsListWidget(parent, {\n\t.show = controller->uiShow(),\n\t.paused = Window::PausedIn(controller, level),\n}) {\n}\n\nGifsListWidget::GifsListWidget(\n\tQWidget *parent,\n\tGifsListDescriptor &&descriptor)\n: Inner(\n\tparent,\n\tdescriptor.st ? *descriptor.st : st::defaultEmojiPan,\n\tdescriptor.show,\n\tdescriptor.paused)\n, _show(std::move(descriptor.show))\n, _api(&session().mtp())\n, _section(Section::Gifs)\n, _updateInlineItems([=] { updateInlineItems(); })\n, _mosaic(st::emojiPanWidth - st::inlineResultsLeft)\n, _previewTimer([=] { showPreview(); }) {\n\tsetMouseTracking(true);\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\n\tsetupSearch();\n\n\t_inlineRequestTimer.setSingleShot(true);\n\tconnect(\n\t\t&_inlineRequestTimer,\n\t\t&QTimer::timeout,\n\t\tthis,\n\t\t[=] { sendInlineRequest(); });\n\n\tsession().data().stickers().savedGifsUpdated(\n\t) | rpl::on_next([=] {\n\t\trefreshSavedGifs();\n\t}, lifetime());\n\n\tsession().downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdateInlineItems();\n\t}, lifetime());\n\n\t_show->pauseChanged(\n\t) | rpl::on_next([=] {\n\t\tif (!paused()) {\n\t\t\tupdateInlineItems();\n\t\t}\n\t}, lifetime());\n\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\t_mosaic.setFullWidth(s.width());\n\t}, lifetime());\n\n\t_mosaic.setPadding(st::gifsPadding\n\t\t+ QMargins(-st::emojiPanRadius, _search->height(), 0, 0));\n\t_mosaic.setRightSkip(st::inlineResultsSkip);\n}\n\nrpl::producer<FileChosen> GifsListWidget::fileChosen() const {\n\treturn _fileChosen.events();\n}\n\nrpl::producer<PhotoChosen> GifsListWidget::photoChosen() const {\n\treturn _photoChosen.events();\n}\n\nauto GifsListWidget::inlineResultChosen() const\n-> rpl::producer<InlineChosen> {\n\treturn _inlineResultChosen.events();\n}\n\nobject_ptr<TabbedSelector::InnerFooter> GifsListWidget::createFooter() {\n\tExpects(_footer == nullptr);\n\n\tusing FooterDescriptor = StickersListFooter::Descriptor;\n\tauto result = object_ptr<StickersListFooter>(FooterDescriptor{\n\t\t.session = &session(),\n\t\t.paused = pausedMethod(),\n\t\t.parent = this,\n\t\t.st = &st(),\n\t\t.features = { .stickersSettings = false },\n\t});\n\t_footer = result;\n\t_chosenSetId = Data::Stickers::RecentSetId;\n\n\tGifSectionsValue(\n\t\t&session()\n\t) | rpl::on_next([=](std::vector<GifSection> &&list) {\n\t\t_sections = std::move(list);\n\t\trefreshIcons();\n\t}, _footer->lifetime());\n\n\t_footer->setChosen(\n\t) | rpl::on_next([=](uint64 setId) {\n\t\tif (_search) {\n\t\t\t_search->cancel();\n\t\t}\n\t\t_chosenSetId = setId;\n\t\trefreshIcons();\n\t\tconst auto i = ranges::find(_sections, setId, [](GifSection value) {\n\t\t\treturn value.document->id;\n\t\t});\n\t\tsearchForGifs((i != end(_sections)) ? i->emoji->text() : QString());\n\t}, _footer->lifetime());\n\n\treturn result;\n}\n\nvoid GifsListWidget::refreshIcons() {\n\tif (_footer) {\n\t\t_footer->refreshIcons(\n\t\t\tfillIcons(),\n\t\t\t_chosenSetId,\n\t\t\tnullptr,\n\t\t\tValidateIconAnimations::None);\n\t}\n}\n\nstd::vector<StickerIcon> GifsListWidget::fillIcons() {\n\tauto result = std::vector<StickerIcon>();\n\tresult.reserve(_sections.size() + 1);\n\tresult.emplace_back(Data::Stickers::RecentSetId);\n\tconst auto side = StickersListFooter::IconFrameSize();\n\tfor (const auto &section : _sections) {\n\t\tconst auto s = section.document;\n\t\tconst auto id = s->id;\n\t\tconst auto size = s->hasThumbnail()\n\t\t\t? QSize(\n\t\t\t\ts->thumbnailLocation().width(),\n\t\t\t\ts->thumbnailLocation().height())\n\t\t\t: QSize();\n\t\tconst auto pix = size.scaled(side, side, Qt::KeepAspectRatio);\n\t\tconst auto owner = &s->owner();\n\t\tconst auto already = _fakeSets.find(id);\n\t\tconst auto set = (already != end(_fakeSets))\n\t\t\t? already\n\t\t\t: _fakeSets.emplace(\n\t\t\t\tid,\n\t\t\t\tstd::make_unique<Data::StickersSet>(\n\t\t\t\t\towner,\n\t\t\t\t\tid,\n\t\t\t\t\t0,\n\t\t\t\t\t0,\n\t\t\t\t\tQString(),\n\t\t\t\t\tQString(),\n\t\t\t\t\t0,\n\t\t\t\t\tData::StickersSetFlag::Special,\n\t\t\t\t\t0)).first;\n\t\tresult.emplace_back(set->second.get(), s, pix.width(), pix.height());\n\t}\n\treturn result;\n}\n\nvoid GifsListWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tconst auto top = getVisibleTop();\n\tInner::visibleTopBottomUpdated(visibleTop, visibleBottom);\n\tif (top != getVisibleTop()) {\n\t\t_lastScrolledAt = crl::now();\n\t\tupdate();\n\t}\n\tcheckLoadMore();\n}\n\nvoid GifsListWidget::checkLoadMore() {\n\tauto visibleHeight = (getVisibleBottom() - getVisibleTop());\n\tif (getVisibleBottom() + visibleHeight > height()) {\n\t\tsendInlineRequest();\n\t}\n}\n\nint GifsListWidget::countDesiredHeight(int newWidth) {\n\treturn _mosaic.countDesiredHeight(newWidth);\n}\n\nGifsListWidget::~GifsListWidget() {\n\tclearInlineRows(true);\n\tdeleteUnusedGifLayouts();\n\tdeleteUnusedInlineLayouts();\n}\n\nvoid GifsListWidget::cancelGifsSearch() {\n\t_search->setLoading(false);\n\tif (_inlineRequestId) {\n\t\t_api.request(_inlineRequestId).cancel();\n\t\t_inlineRequestId = 0;\n\t}\n\t_inlineRequestTimer.stop();\n\t_inlineQuery = _inlineNextQuery = _inlineNextOffset = QString();\n\t_inlineCache.clear();\n\trefreshInlineRows(nullptr, true);\n}\n\nvoid GifsListWidget::inlineResultsDone(const MTPmessages_BotResults &result) {\n\t_search->setLoading(false);\n\t_inlineRequestId = 0;\n\n\tauto it = _inlineCache.find(_inlineQuery);\n\tauto adding = (it != _inlineCache.cend());\n\tif (result.type() == mtpc_messages_botResults) {\n\t\tauto &d = result.c_messages_botResults();\n\t\tsession().data().processUsers(d.vusers());\n\n\t\tauto &v = d.vresults().v;\n\t\tauto queryId = d.vquery_id().v;\n\n\t\tif (it == _inlineCache.cend()) {\n\t\t\tit = _inlineCache.emplace(\n\t\t\t\t_inlineQuery,\n\t\t\t\tstd::make_unique<InlineCacheEntry>()).first;\n\t\t}\n\t\tconst auto entry = it->second.get();\n\t\tentry->nextOffset = qs(d.vnext_offset().value_or_empty());\n\t\tif (const auto count = v.size()) {\n\t\t\tentry->results.reserve(entry->results.size() + count);\n\t\t}\n\t\tauto added = 0;\n\t\tfor (const auto &res : v) {\n\t\t\tauto result = InlineBots::Result::Create(\n\t\t\t\t&session(),\n\t\t\t\tqueryId,\n\t\t\t\tres);\n\t\t\tif (result) {\n\t\t\t\t++added;\n\t\t\t\tentry->results.push_back(std::move(result));\n\t\t\t}\n\t\t}\n\n\t\tif (!added) {\n\t\t\tentry->nextOffset = QString();\n\t\t}\n\t} else if (adding) {\n\t\tit->second->nextOffset = QString();\n\t}\n\n\tif (!showInlineRows(!adding)) {\n\t\tit->second->nextOffset = QString();\n\t}\n\tcheckLoadMore();\n}\n\nvoid GifsListWidget::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\tauto clip = e->rect();\n\tp.fillRect(clip, st().bg);\n\n\tpaintInlineItems(p, clip);\n}\n\nvoid GifsListWidget::paintInlineItems(Painter &p, QRect clip) {\n\tif (_mosaic.empty()) {\n\t\tp.setFont(st::normalFont);\n\t\tp.setPen(st::noContactsColor);\n\t\tauto text = _inlineQuery.isEmpty()\n\t\t\t? tr::lng_gifs_no_saved(tr::now)\n\t\t\t: tr::lng_inline_bot_no_results(tr::now);\n\t\tp.drawText(QRect(0, 0, width(), (height() / 3) * 2 + st::normalFont->height), text, style::al_center);\n\t\treturn;\n\t}\n\tconst auto gifPaused = paused();\n\tusing namespace InlineBots::Layout;\n\tPaintContext context(crl::now(), false, gifPaused, false);\n\n\tauto paintItem = [&](not_null<const ItemBase*> item, QPoint point) {\n\t\tp.translate(point.x(), point.y());\n\t\titem->paint(\n\t\t\tp,\n\t\t\tclip.translated(-point),\n\t\t\t&context);\n\t\tp.translate(-point.x(), -point.y());\n\t};\n\t_mosaic.paint(std::move(paintItem), clip);\n}\n\nvoid GifsListWidget::mousePressEvent(QMouseEvent *e) {\n\tif (e->button() != Qt::LeftButton) {\n\t\treturn;\n\t}\n\t_lastMousePos = e->globalPos();\n\tupdateSelected();\n\n\t_pressed = _selected;\n\tClickHandler::pressed();\n\t_previewTimer.callOnce(QApplication::startDragTime());\n}\n\nbase::unique_qptr<Ui::PopupMenu> GifsListWidget::fillContextMenu(\n\t\tconst SendMenu::Details &details) {\n\tif (_selected < 0 || _pressed >= 0) {\n\t\treturn nullptr;\n\t}\n\n\tauto menu = base::make_unique_q<Ui::PopupMenu>(this, st().menu);\n\tconst auto selected = _selected;\n\tconst auto send = crl::guard(this, [=](Api::SendOptions options) {\n\t\tselectInlineResult(selected, options, true);\n\t});\n\tconst auto item = _mosaic.maybeItemAt(_selected);\n\tconst auto isInlineResult = !item->getPhoto()\n\t\t&& !item->getDocument()\n\t\t&& item->getResult();\n\tconst auto icons = &st().icons;\n\tauto copyDetails = details;\n\tif (isInlineResult) {\n\t\t// inline results don't have effects\n\t\tcopyDetails.effectAllowed = false;\n\t}\n\n\t// In case we're adding items after FillSendMenu we have\n\t// to pass nullptr for showForEffect and attach selector later.\n\t// Otherwise added items widths won't be respected in menu geometry.\n\tSendMenu::FillSendMenu(\n\t\tmenu,\n\t\tnullptr, // showForMenu\n\t\tcopyDetails,\n\t\tSendMenu::DefaultCallback(_show, send),\n\t\ticons);\n\n\tif (!isInlineResult && _inlineQueryPeer) {\n\t\tauto done = crl::guard(this, [=](\n\t\t\t\tApi::SendOptions options,\n\t\t\t\tTextWithTags text) {\n\t\t\tselectInlineResult(selected, options, true, std::move(text));\n\t\t});\n\t\tconst auto show = _show;\n\t\tconst auto peer = _inlineQueryPeer;\n\t\tmenu->addAction(tr::lng_send_gif_with_caption(tr::now), [=] {\n\t\t\tshow->show(Box(\n\t\t\t\tUi::SendGifWithCaptionBox,\n\t\t\t\titem->getDocument(),\n\t\t\t\tpeer,\n\t\t\t\tcopyDetails,\n\t\t\t\tstd::move(done)));\n\t\t}, &st::menuIconEdit);\n\t}\n\n\tif (const auto item = _mosaic.maybeItemAt(_selected)) {\n\t\tconst auto document = item->getDocument()\n\t\t\t? item->getDocument() // Saved GIF.\n\t\t\t: item->getPreviewDocument(); // Searched GIF.\n\t\tif (document) {\n\t\t\tauto callback = [&](\n\t\t\t\t\tconst QString &text,\n\t\t\t\t\tFn<void()> &&done,\n\t\t\t\t\tconst style::icon *icon) {\n\t\t\t\tmenu->addAction(text, std::move(done), icon);\n\t\t\t};\n\t\t\tAddGifAction(std::move(callback), _show, document, icons);\n\t\t}\n\t}\n\n\tSendMenu::AttachSendMenuEffect(\n\t\tmenu,\n\t\t_show,\n\t\tcopyDetails,\n\t\tSendMenu::DefaultCallback(_show, send));\n\n\treturn menu;\n}\n\nvoid GifsListWidget::mouseReleaseEvent(QMouseEvent *e) {\n\t_previewTimer.cancel();\n\n\tauto pressed = std::exchange(_pressed, -1);\n\tauto activated = ClickHandler::unpressed();\n\n\tif (_previewShown) {\n\t\t_previewShown = false;\n\t\treturn;\n\t}\n\n\t_lastMousePos = e->globalPos();\n\tupdateSelected();\n\n\tif (_selected < 0 || _selected != pressed || !activated) {\n\t\treturn;\n\t}\n\n\tif (dynamic_cast<InlineBots::Layout::SendClickHandler*>(activated.get())) {\n\t\tselectInlineResult(_selected, {});\n\t} else {\n\t\tActivateClickHandler(window(), activated, {\n\t\t\te->button(),\n\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.show = _show,\n\t\t\t})\n\t\t});\n\t}\n}\n\nvoid GifsListWidget::selectInlineResult(\n\t\tint index,\n\t\tApi::SendOptions options,\n\t\tbool forceSend,\n\t\tTextWithTags caption) {\n\tconst auto item = _mosaic.maybeItemAt(index);\n\tif (!item) {\n\t\treturn;\n\t}\n\n\tconst auto messageSendingFrom = [&] {\n\t\tif (options.scheduled) {\n\t\t\treturn Ui::MessageSendingAnimationFrom();\n\t\t}\n\t\tconst auto rect = item->innerContentRect().translated(\n\t\t\t_mosaic.findRect(index).topLeft());\n\t\treturn Ui::MessageSendingAnimationFrom{\n\t\t\t.type = Ui::MessageSendingAnimationFrom::Type::Gif,\n\t\t\t.localId = session().data().nextLocalMessageId(),\n\t\t\t.globalStartGeometry = mapToGlobal(rect),\n\t\t\t.crop = true,\n\t\t};\n\t};\n\n\tforceSend |= base::IsCtrlPressed();\n\tif (const auto photo = item->getPhoto()) {\n\t\tusing Data::PhotoSize;\n\t\tconst auto media = photo->activeMediaView();\n\t\tif (forceSend\n\t\t\t|| (media && media->image(PhotoSize::Thumbnail))\n\t\t\t|| (media && media->image(PhotoSize::Large))) {\n\t\t\t_photoChosen.fire({\n\t\t\t\t.photo = photo,\n\t\t\t\t.options = options\n\t\t\t});\n\t\t} else if (!photo->loading(PhotoSize::Thumbnail)) {\n\t\t\tphoto->load(PhotoSize::Thumbnail, Data::FileOrigin());\n\t\t}\n\t} else if (const auto document = item->getDocument()) {\n\t\tconst auto media = document->activeMediaView();\n\t\tconst auto preview = Data::VideoPreviewState(media.get());\n\t\tif (forceSend || (media && preview.loaded())) {\n\t\t\t_fileChosen.fire({\n\t\t\t\t.document = document,\n\t\t\t\t.options = options,\n\t\t\t\t.messageSendingFrom = messageSendingFrom(),\n\t\t\t\t.caption = std::move(caption),\n\t\t\t});\n\t\t} else if (!preview.usingThumbnail()) {\n\t\t\tif (preview.loading()) {\n\t\t\t\tdocument->cancel();\n\t\t\t} else {\n\t\t\t\tdocument->save(\n\t\t\t\t\tdocument->stickerOrGifOrigin(),\n\t\t\t\t\tQString());\n\t\t\t}\n\t\t}\n\t} else if (const auto inlineResult = item->getResult()) {\n\t\tif (inlineResult->onChoose(item)) {\n\t\t\toptions.hideViaBot = true;\n\t\t\t_inlineResultChosen.fire({\n\t\t\t\t.result = inlineResult,\n\t\t\t\t.bot = _searchBot,\n\t\t\t\t.options = options,\n\t\t\t\t.messageSendingFrom = messageSendingFrom(),\n\t\t\t});\n\t\t}\n\t}\n}\n\nvoid GifsListWidget::mouseMoveEvent(QMouseEvent *e) {\n\t_lastMousePos = e->globalPos();\n\tupdateSelected();\n}\n\nvoid GifsListWidget::leaveEventHook(QEvent *e) {\n\tclearSelection();\n}\n\nvoid GifsListWidget::leaveToChildEvent(QEvent *e, QWidget *child) {\n\tclearSelection();\n}\n\nvoid GifsListWidget::enterFromChildEvent(QEvent *e, QWidget *child) {\n\t_lastMousePos = QCursor::pos();\n\tupdateSelected();\n}\n\nvoid GifsListWidget::clearSelection() {\n\tif (_selected >= 0) {\n\t\tClickHandler::clearActive(_mosaic.itemAt(_selected));\n\t\tsetCursor(style::cur_default);\n\t}\n\t_selected = _pressed = -1;\n\trepaintItems();\n}\n\nTabbedSelector::InnerFooter *GifsListWidget::getFooter() const {\n\treturn _footer;\n}\n\nvoid GifsListWidget::processHideFinished() {\n\tclearSelection();\n\tclearHeavyData();\n\tif (_footer) {\n\t\t_footer->clearHeavyData();\n\t}\n}\n\nvoid GifsListWidget::processPanelHideFinished() {\n\tclearHeavyData();\n\tif (_footer) {\n\t\t_footer->clearHeavyData();\n\t}\n}\n\nvoid GifsListWidget::clearHeavyData() {\n\t// Preserve panel state through visibility toggles.\n\t//clearInlineRows(false);\n\tfor (const auto &[document, layout] : _gifLayouts) {\n\t\tlayout->unloadHeavyPart();\n\t}\n\tfor (const auto &[document, layout] : _inlineLayouts) {\n\t\tlayout->unloadHeavyPart();\n\t}\n}\n\nvoid GifsListWidget::refreshSavedGifs() {\n\tif (_section == Section::Gifs) {\n\t\tclearInlineRows(false);\n\n\t\tconst auto &saved = session().data().stickers().savedGifs();\n\t\tif (!saved.isEmpty()) {\n\t\t\tconst auto layouts = ranges::views::all(\n\t\t\t\tsaved\n\t\t\t) | ranges::views::transform([&](not_null<DocumentData*> gif) {\n\t\t\t\treturn layoutPrepareSavedGif(gif);\n\t\t\t}) | ranges::views::filter([](const LayoutItem *item) {\n\t\t\t\treturn item != nullptr;\n\t\t\t}) | ranges::to<std::vector<not_null<LayoutItem*>>>;\n\n\t\t\t_mosaic.addItems(layouts);\n\t\t}\n\t\tdeleteUnusedGifLayouts();\n\n\t\tresizeToWidth(width());\n\t\trepaintItems();\n\t}\n\n\tif (isVisible()) {\n\t\tupdateSelected();\n\t} else {\n\t\tpreloadImages();\n\t}\n}\n\nvoid GifsListWidget::clearInlineRows(bool resultsDeleted) {\n\tif (resultsDeleted) {\n\t\t_selected = _pressed = -1;\n\t} else {\n\t\tclearSelection();\n\t}\n\t_mosaic.clearRows(resultsDeleted);\n}\n\nGifsListWidget::LayoutItem *GifsListWidget::layoutPrepareSavedGif(\n\t\tnot_null<DocumentData*> document) {\n\tauto it = _gifLayouts.find(document);\n\tif (it == _gifLayouts.cend()) {\n\t\tif (auto layout = LayoutItem::createLayoutGif(this, document)) {\n\t\t\tit = _gifLayouts.emplace(document, std::move(layout)).first;\n\t\t\tit->second->initDimensions();\n\t\t} else {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\tif (!it->second->maxWidth()) return nullptr;\n\n\treturn it->second.get();\n}\n\nGifsListWidget::LayoutItem *GifsListWidget::layoutPrepareInlineResult(\n\t\tstd::shared_ptr<InlineResult> result) {\n\tconst auto raw = result.get();\n\tauto it = _inlineLayouts.find(raw);\n\tif (it == _inlineLayouts.cend()) {\n\t\tif (auto layout = LayoutItem::createLayout(\n\t\t\t\tthis,\n\t\t\t\tstd::move(result),\n\t\t\t\t_inlineWithThumb)) {\n\t\t\tit = _inlineLayouts.emplace(raw, std::move(layout)).first;\n\t\t\tit->second->initDimensions();\n\t\t} else {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\tif (!it->second->maxWidth()) return nullptr;\n\n\treturn it->second.get();\n}\n\nvoid GifsListWidget::deleteUnusedGifLayouts() {\n\tif (_mosaic.empty() || _section != Section::Gifs) { // delete all\n\t\t_gifLayouts.clear();\n\t} else {\n\t\tfor (auto i = _gifLayouts.begin(); i != _gifLayouts.cend();) {\n\t\t\tif (i->second->position() < 0) {\n\t\t\t\ti = _gifLayouts.erase(i);\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid GifsListWidget::deleteUnusedInlineLayouts() {\n\tif (_mosaic.empty() || _section == Section::Gifs) { // delete all\n\t\t_inlineLayouts.clear();\n\t} else {\n\t\tfor (auto i = _inlineLayouts.begin(); i != _inlineLayouts.cend();) {\n\t\t\tif (i->second->position() < 0) {\n\t\t\t\ti = _inlineLayouts.erase(i);\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid GifsListWidget::preloadImages() {\n\t_mosaic.forEach([](not_null<const LayoutItem*> item) {\n\t\titem->preload();\n\t});\n}\n\nvoid GifsListWidget::switchToSavedGifs() {\n\tclearInlineRows(false);\n\t_section = Section::Gifs;\n\trefreshSavedGifs();\n\tscrollTo(0);\n}\n\nint GifsListWidget::refreshInlineRows(const InlineCacheEntry *entry, bool resultsDeleted) {\n\tif (!entry) {\n\t\tif (resultsDeleted) {\n\t\t\tclearInlineRows(true);\n\t\t\tdeleteUnusedInlineLayouts();\n\t\t}\n\t\tswitchToSavedGifs();\n\t\treturn 0;\n\t}\n\n\tclearSelection();\n\n\t_section = Section::Inlines;\n\tconst auto count = int(entry->results.size());\n\tconst auto from = validateExistingInlineRows(entry->results);\n\tauto added = 0;\n\tif (count) {\n\t\tconst auto resultLayouts = entry->results | ranges::views::slice(\n\t\t\tfrom,\n\t\t\tcount\n\t\t) | ranges::views::transform([&](\n\t\t\t\tconst std::shared_ptr<InlineBots::Result> &r) {\n\t\t\treturn layoutPrepareInlineResult(r);\n\t\t}) | ranges::views::filter([](const LayoutItem *item) {\n\t\t\treturn item != nullptr;\n\t\t}) | ranges::to<std::vector<not_null<LayoutItem*>>>;\n\n\t\t_mosaic.addItems(resultLayouts);\n\t\tadded = resultLayouts.size();\n\t\tpreloadImages();\n\t}\n\n\tresizeToWidth(width());\n\trepaintItems();\n\n\t_lastMousePos = QCursor::pos();\n\tupdateSelected();\n\n\treturn added;\n}\n\nint GifsListWidget::validateExistingInlineRows(const InlineResults &results) {\n\tconst auto until = _mosaic.validateExistingRows([&](\n\t\t\tnot_null<const LayoutItem*> item,\n\t\t\tint untilIndex) {\n\t\treturn item->getResult().get() != results[untilIndex].get();\n\t}, results.size());\n\n\tif (_mosaic.empty()) {\n\t\t_inlineWithThumb = false;\n\t\tfor (int i = until; i < results.size(); ++i) {\n\t\t\tif (results.at(i)->hasThumbDisplay()) {\n\t\t\t\t_inlineWithThumb = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\treturn until;\n}\n\nvoid GifsListWidget::inlineItemLayoutChanged(const InlineBots::Layout::ItemBase *layout) {\n\tif (_selected < 0 || !isVisible()) {\n\t\treturn;\n\t}\n\n\tif (const auto item = _mosaic.maybeItemAt(_selected)) {\n\t\tif (layout == item) {\n\t\t\tupdateSelected();\n\t\t}\n\t}\n}\n\nvoid GifsListWidget::inlineItemRepaint(\n\t\tconst InlineBots::Layout::ItemBase *layout) {\n\tupdateInlineItems();\n}\n\nbool GifsListWidget::inlineItemVisible(\n\t\tconst InlineBots::Layout::ItemBase *layout) {\n\tauto position = layout->position();\n\tif (position < 0 || !isVisible()) {\n\t\treturn false;\n\t}\n\n\tconst auto &[row, column] = Layout::IndexToPosition(position);\n\tauto top = 0;\n\tfor (auto i = 0; i != row; ++i) {\n\t\ttop += _mosaic.rowHeightAt(i);\n\t}\n\n\treturn (top < getVisibleBottom())\n\t\t&& (top + _mosaic.itemAt(row, column)->height() > getVisibleTop());\n}\n\nData::FileOrigin GifsListWidget::inlineItemFileOrigin() {\n\treturn _inlineQuery.isEmpty()\n\t\t? Data::FileOriginSavedGifs()\n\t\t: Data::FileOrigin();\n}\n\nvoid GifsListWidget::afterShown() {\n\tif (_search) {\n\t\t_search->stealFocus();\n\t}\n}\n\nvoid GifsListWidget::beforeHiding() {\n\tif (_search) {\n\t\t_search->returnFocus();\n\t}\n}\n\nbool GifsListWidget::refreshInlineRows(int32 *added) {\n\tauto it = _inlineCache.find(_inlineQuery);\n\tconst InlineCacheEntry *entry = nullptr;\n\tif (it != _inlineCache.cend()) {\n\t\tentry = it->second.get();\n\t\t_inlineNextOffset = it->second->nextOffset;\n\t}\n\tauto result = refreshInlineRows(entry, false);\n\tif (added) *added = result;\n\treturn (entry != nullptr);\n}\n\nvoid GifsListWidget::setupSearch() {\n\tconst auto session = &_show->session();\n\t_search = MakeSearch(this, st(), [=](std::vector<QString> &&query) {\n\t\tconst auto accumulated = ranges::accumulate(query, QString(), [](\n\t\t\t\tQString a,\n\t\t\t\tQString b) {\n\t\t\treturn a.isEmpty() ? b : (a + ' ' + b);\n\t\t});\n\t\t_chosenSetId = accumulated.isEmpty()\n\t\t\t? Data::Stickers::RecentSetId\n\t\t\t: SearchEmojiSectionSetId();\n\t\trefreshIcons();\n\t\tsearchForGifs(accumulated);\n\t}, session, TabbedSearchType::Emoji);\n}\n\nint32 GifsListWidget::showInlineRows(bool newResults) {\n\tauto added = 0;\n\trefreshInlineRows(&added);\n\tif (newResults) {\n\t\tscrollTo(0);\n\t}\n\treturn added;\n}\n\nvoid GifsListWidget::searchForGifs(const QString &query) {\n\tif (query.isEmpty()) {\n\t\tcancelGifsSearch();\n\t\treturn;\n\t}\n\n\tif (_inlineQuery != query) {\n\t\t_search->setLoading(false);\n\t\tif (_inlineRequestId) {\n\t\t\t_api.request(_inlineRequestId).cancel();\n\t\t\t_inlineRequestId = 0;\n\t\t}\n\t\tif (_inlineCache.find(query) != _inlineCache.cend()) {\n\t\t\t_inlineRequestTimer.stop();\n\t\t\t_inlineQuery = _inlineNextQuery = query;\n\t\t\tshowInlineRows(true);\n\t\t} else {\n\t\t\t_inlineNextQuery = query;\n\t\t\t_inlineRequestTimer.start(kSearchRequestDelay);\n\t\t}\n\t}\n\n\tif (!_searchBot && !_searchBotRequestId) {\n\t\tconst auto username = session().serverConfig().gifSearchUsername;\n\t\t_searchBotRequestId = _api.request(MTPcontacts_ResolveUsername(\n\t\t\tMTP_flags(0),\n\t\t\tMTP_string(username),\n\t\t\tMTP_string()\n\t\t)).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\t\tauto &data = result.data();\n\t\t\tsession().data().processUsers(data.vusers());\n\t\t\tsession().data().processChats(data.vchats());\n\t\t\tconst auto peer = session().data().peerLoaded(\n\t\t\t\tpeerFromMTP(data.vpeer()));\n\t\t\tif (const auto user = peer ? peer->asUser() : nullptr) {\n\t\t\t\t_searchBot = user;\n\t\t\t}\n\t\t}).send();\n\t}\n}\n\nvoid GifsListWidget::cancelled() {\n\t_cancelled.fire({});\n}\n\nrpl::producer<> GifsListWidget::cancelRequests() const {\n\treturn _cancelled.events();\n}\n\nvoid GifsListWidget::sendInlineRequest() {\n\tif (_inlineRequestId || !_inlineQueryPeer || _inlineNextQuery.isEmpty()) {\n\t\treturn;\n\t}\n\n\tif (!_searchBot) {\n\t\t// Wait for the bot being resolved.\n\t\t_search->setLoading(true);\n\t\t_inlineRequestTimer.start(kSearchRequestDelay);\n\t\treturn;\n\t}\n\t_inlineRequestTimer.stop();\n\t_inlineQuery = _inlineNextQuery;\n\n\tauto nextOffset = QString();\n\tauto it = _inlineCache.find(_inlineQuery);\n\tif (it != _inlineCache.cend()) {\n\t\tnextOffset = it->second->nextOffset;\n\t\tif (nextOffset.isEmpty()) {\n\t\t\t_search->setLoading(false);\n\t\t\treturn;\n\t\t}\n\t}\n\n\t_search->setLoading(true);\n\t_inlineRequestId = _api.request(MTPmessages_GetInlineBotResults(\n\t\tMTP_flags(0),\n\t\t_searchBot->inputUser(),\n\t\t_inlineQueryPeer->input(),\n\t\tMTPInputGeoPoint(),\n\t\tMTP_string(_inlineQuery),\n\t\tMTP_string(nextOffset)\n\t)).done([this](const MTPmessages_BotResults &result) {\n\t\tinlineResultsDone(result);\n\t}).fail([this] {\n\t\t// show error?\n\t\t_search->setLoading(false);\n\t\t_inlineRequestId = 0;\n\t}).handleAllErrors().send();\n}\n\nvoid GifsListWidget::refreshRecent() {\n\tif (_section == Section::Gifs) {\n\t\trefreshSavedGifs();\n\t}\n}\n\nvoid GifsListWidget::updateSelected() {\n\tif (_pressed >= 0 && !_previewShown) {\n\t\treturn;\n\t}\n\n\tconst auto p = mapFromGlobal(_lastMousePos);\n\tconst auto sx = rtl() ? (width() - p.x()) : p.x();\n\tconst auto sy = p.y();\n\tconst auto &[index, exact, relative] = _mosaic.findByPoint({ sx, sy });\n\tconst auto selected = exact ? index : -1;\n\tconst auto item = exact ? _mosaic.itemAt(selected).get() : nullptr;\n\tconst auto link = exact ? item->getState(relative, {}).link : nullptr;\n\n\tif (_selected != selected) {\n\t\tif (const auto s = _mosaic.maybeItemAt(_selected)) {\n\t\t\ts->update();\n\t\t}\n\t\t_selected = selected;\n\t\tif (item) {\n\t\t\titem->update();\n\t\t}\n\t\tif (_previewShown && _selected >= 0 && _pressed != _selected) {\n\t\t\t_pressed = _selected;\n\t\t\tif (item) {\n\t\t\t\tif (const auto preview = item->getPreviewDocument()) {\n\t\t\t\t\t_show->showMediaPreview(\n\t\t\t\t\t\tData::FileOriginSavedGifs(),\n\t\t\t\t\t\tpreview);\n\t\t\t\t} else if (const auto preview = item->getPreviewPhoto()) {\n\t\t\t\t\t_show->showMediaPreview(Data::FileOrigin(), preview);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (ClickHandler::setActive(link, item)) {\n\t\tsetCursor(link ? style::cur_pointer : style::cur_default);\n\t}\n}\n\nvoid GifsListWidget::showPreview() {\n\tif (_pressed < 0) {\n\t\treturn;\n\t}\n\tif (const auto layout = _mosaic.maybeItemAt(_pressed)) {\n\t\tif (const auto previewDocument = layout->getPreviewDocument()) {\n\t\t\t_previewShown = _show->showMediaPreview(\n\t\t\t\tData::FileOriginSavedGifs(),\n\t\t\t\tpreviewDocument);\n\t\t} else if (const auto previewPhoto = layout->getPreviewPhoto()) {\n\t\t\t_previewShown = _show->showMediaPreview(\n\t\t\t\tData::FileOrigin(),\n\t\t\t\tpreviewPhoto);\n\t\t}\n\t}\n}\n\nvoid GifsListWidget::updateInlineItems() {\n\tconst auto now = crl::now();\n\n\tconst auto delay = std::max(\n\t\t_lastScrolledAt + kMinAfterScrollDelay - now,\n\t\t_lastUpdatedAt + kMinRepaintDelay - now);\n\tif (delay <= 0) {\n\t\trepaintItems(now);\n\t} else if (!_updateInlineItems.isActive()\n\t\t|| _updateInlineItems.remainingTime() > kMinRepaintDelay) {\n\t\t_updateInlineItems.callOnce(std::max(delay, kMinRepaintDelay));\n\t}\n}\n\nvoid GifsListWidget::repaintItems(crl::time now) {\n\t_lastUpdatedAt = now ? now : crl::now();\n\tupdate();\n}\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/gifs_list_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"base/timer.h\"\n#include \"inline_bots/inline_bot_layout_item.h\"\n#include \"layout/layout_mosaic.h\"\n\n#include <QtCore/QTimer>\n\nnamespace style {\nstruct ComposeIcons;\n} // namespace style\n\nnamespace Api {\nstruct SendOptions;\n} // namespace Api\n\nnamespace InlineBots {\nnamespace Layout {\nclass ItemBase;\n} // namespace Layout\nclass Result;\n} // namespace InlineBots\n\nnamespace Ui {\nclass PopupMenu;\nclass RoundButton;\nclass TabbedSearch;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nnamespace Data {\nclass StickersSet;\n} // namespace Data\n\nnamespace ChatHelpers {\n\nvoid AddGifAction(\n\tFn<void(QString, Fn<void()> &&, const style::icon*)> callback,\n\tstd::shared_ptr<Show> show,\n\tnot_null<DocumentData*> document,\n\tconst style::ComposeIcons *iconsOverride = nullptr);\n\nclass StickersListFooter;\nstruct StickerIcon;\nstruct GifSection;\n\nstruct GifsListDescriptor {\n\tstd::shared_ptr<Show> show;\n\tFn<bool()> paused;\n\tconst style::EmojiPan *st = nullptr;\n};\n\nclass GifsListWidget final\n\t: public TabbedSelector::Inner\n\t, public InlineBots::Layout::Context {\npublic:\n\tGifsListWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tPauseReason level);\n\tGifsListWidget(QWidget *parent, GifsListDescriptor &&descriptor);\n\n\trpl::producer<FileChosen> fileChosen() const;\n\trpl::producer<PhotoChosen> photoChosen() const;\n\trpl::producer<InlineChosen> inlineResultChosen() const;\n\n\tvoid refreshRecent() override;\n\tvoid preloadImages() override;\n\tvoid clearSelection() override;\n\tobject_ptr<TabbedSelector::InnerFooter> createFooter() override;\n\n\tvoid inlineItemLayoutChanged(const InlineBots::Layout::ItemBase *layout) override;\n\tvoid inlineItemRepaint(const InlineBots::Layout::ItemBase *layout) override;\n\tbool inlineItemVisible(const InlineBots::Layout::ItemBase *layout) override;\n\tData::FileOrigin inlineItemFileOrigin() override;\n\n\tvoid afterShown() override;\n\tvoid beforeHiding() override;\n\n\tvoid setInlineQueryPeer(PeerData *peer) {\n\t\t_inlineQueryPeer = peer;\n\t}\n\tvoid searchForGifs(const QString &query);\n\tvoid sendInlineRequest();\n\n\tvoid cancelled();\n\trpl::producer<> cancelRequests() const;\n\n\tbase::unique_qptr<Ui::PopupMenu> fillContextMenu(\n\t\tconst SendMenu::Details &details) override;\n\n\t~GifsListWidget();\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid leaveToChildEvent(QEvent *e, QWidget *child) override;\n\tvoid enterFromChildEvent(QEvent *e, QWidget *child) override;\n\n\tTabbedSelector::InnerFooter *getFooter() const override;\n\tvoid processHideFinished() override;\n\tvoid processPanelHideFinished() override;\n\tint countDesiredHeight(int newWidth) override;\n\nprivate:\n\tenum class Section {\n\t\tInlines,\n\t\tGifs,\n\t};\n\n\tusing InlineResult = InlineBots::Result;\n\tusing InlineResults = std::vector<std::shared_ptr<InlineResult>>;\n\tusing LayoutItem = InlineBots::Layout::ItemBase;\n\n\tstruct InlineCacheEntry {\n\t\tQString nextOffset;\n\t\tInlineResults results;\n\t};\n\n\tvoid setupSearch();\n\tvoid clearHeavyData();\n\tvoid cancelGifsSearch();\n\tvoid switchToSavedGifs();\n\tvoid refreshSavedGifs();\n\tint refreshInlineRows(const InlineCacheEntry *results, bool resultsDeleted);\n\tvoid checkLoadMore();\n\n\tint32 showInlineRows(bool newResults);\n\tbool refreshInlineRows(int32 *added = 0);\n\tvoid inlineResultsDone(const MTPmessages_BotResults &result);\n\n\tvoid updateSelected();\n\tvoid paintInlineItems(Painter &p, QRect clip);\n\tvoid refreshIcons();\n\t[[nodiscard]] std::vector<StickerIcon> fillIcons();\n\n\tvoid updateInlineItems();\n\tvoid repaintItems(crl::time now = 0);\n\tvoid showPreview();\n\n\tvoid clearInlineRows(bool resultsDeleted);\n\tLayoutItem *layoutPrepareSavedGif(not_null<DocumentData*> document);\n\tLayoutItem *layoutPrepareInlineResult(\n\t\tstd::shared_ptr<InlineResult> result);\n\n\tvoid deleteUnusedGifLayouts();\n\n\tvoid deleteUnusedInlineLayouts();\n\n\tint validateExistingInlineRows(const InlineResults &results);\n\tvoid selectInlineResult(\n\t\tint index,\n\t\tApi::SendOptions options,\n\t\tbool forceSend = false,\n\t\tTextWithTags caption = {});\n\n\tconst std::shared_ptr<Show> _show;\n\tstd::unique_ptr<Ui::TabbedSearch> _search;\n\n\tMTP::Sender _api;\n\n\tSection _section = Section::Gifs;\n\tcrl::time _lastScrolledAt = 0;\n\tcrl::time _lastUpdatedAt = 0;\n\tbase::Timer _updateInlineItems;\n\tbool _inlineWithThumb = false;\n\n\tstd::map<\n\t\tnot_null<DocumentData*>,\n\t\tstd::unique_ptr<LayoutItem>> _gifLayouts;\n\tstd::map<\n\t\tnot_null<InlineResult*>,\n\t\tstd::unique_ptr<LayoutItem>> _inlineLayouts;\n\n\tStickersListFooter *_footer = nullptr;\n\tstd::vector<GifSection> _sections;\n\tbase::flat_map<uint64, std::unique_ptr<Data::StickersSet>> _fakeSets;\n\tuint64 _chosenSetId = 0;\n\n\tMosaic::Layout::MosaicLayout<LayoutItem> _mosaic;\n\n\tint _selected = -1;\n\tint _pressed = -1;\n\tQPoint _lastMousePos;\n\n\tbase::Timer _previewTimer;\n\tbool _previewShown = false;\n\n\tstd::map<QString, std::unique_ptr<InlineCacheEntry>> _inlineCache;\n\tQTimer _inlineRequestTimer;\n\n\tUserData *_searchBot = nullptr;\n\tmtpRequestId _searchBotRequestId = 0;\n\tPeerData *_inlineQueryPeer = nullptr;\n\tQString _inlineQuery, _inlineNextQuery, _inlineNextOffset;\n\tmtpRequestId _inlineRequestId = 0;\n\n\trpl::event_stream<FileChosen> _fileChosen;\n\trpl::event_stream<PhotoChosen> _photoChosen;\n\trpl::event_stream<InlineChosen> _inlineResultChosen;\n\trpl::event_stream<> _cancelled;\n\n};\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/message_field.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/message_field.h\"\n\n#include \"history/history_widget.h\"\n#include \"history/history.h\" // History::session\n#include \"history/history_item.h\" // HistoryItem::originalText\n#include \"history/history_item_helpers.h\" // DropDisallowedCustomEmoji\n#include \"base/unixtime.h\"\n#include \"base/qthelp_regex.h\"\n#include \"base/qthelp_url.h\"\n#include \"base/event_filter.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/boxes/calendar_box.h\"\n#include \"ui/boxes/choose_date_time.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"ui/rect.h\"\n#include \"core/shortcuts.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/ui_integration.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/ui_utility.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_document.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"history/view/controls/compose_controls_common.h\"\n#include \"window/window_session_controller.h\"\n#include \"lang/lang_keys.h\"\n#include \"mainwindow.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n#include \"base/qt/qt_common_adapters.h\"\n\n#include <QtCore/QMimeData>\n#include <QtCore/QStack>\n#include <QtGui/QGuiApplication>\n#include <QtGui/QTextBlock>\n#include <QtGui/QClipboard>\n#include <QtWidgets/QApplication>\n\nnamespace {\n\nusing namespace Ui::Text;\n\nusing EditLinkAction = Ui::InputField::EditLinkAction;\nusing EditLinkSelection = Ui::InputField::EditLinkSelection;\n\nconstexpr auto kParseLinksTimeout = crl::time(1000);\nconstexpr auto kTypesDuration = 4 * crl::time(1000);\nconstexpr auto kCodeLanguageLimit = 32;\n\nconstexpr auto kLinkProtocols = {\n    \"http://\",\n    \"https://\",\n    \"tonsite://\"\n};\n\n// For mention / custom emoji tags save and validate selfId,\n// ignore tags for different users.\n[[nodiscard]] Fn<QString(QStringView)> FieldTagMimeProcessor(\n\t\tnot_null<Main::Session*> session,\n\t\tFn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {\n\treturn [=](QStringView mimeTag) {\n\t\tconst auto id = session->userId().bare;\n\t\tauto all = TextUtilities::SplitTags(mimeTag);\n\t\tauto premiumSkipped = (DocumentData*)nullptr;\n\t\tfor (auto i = all.begin(); i != all.end();) {\n\t\t\tconst auto tag = *i;\n\t\t\tif (TextUtilities::IsMentionLink(tag)\n\t\t\t\t&& TextUtilities::MentionNameDataToFields(tag).selfId != id) {\n\t\t\t\ti = all.erase(i);\n\t\t\t\tcontinue;\n\t\t\t} else if (Ui::InputField::IsCustomEmojiLink(tag)) {\n\t\t\t\tconst auto data = Ui::InputField::CustomEmojiEntityData(tag);\n\t\t\t\tconst auto emoji = Data::ParseCustomEmojiData(data);\n\t\t\t\tif (!emoji) {\n\t\t\t\t\ti = all.erase(i);\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (!session->premium()) {\n\t\t\t\t\tconst auto document = session->data().document(emoji);\n\t\t\t\t\tif (document->isPremiumEmoji()) {\n\t\t\t\t\t\tif (!allowPremiumEmoji\n\t\t\t\t\t\t\t|| premiumSkipped\n\t\t\t\t\t\t\t|| !session->premiumPossible()\n\t\t\t\t\t\t\t|| !allowPremiumEmoji(document)) {\n\t\t\t\t\t\t\tpremiumSkipped = document;\n\t\t\t\t\t\t\ti = all.erase(i);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t\treturn TextUtilities::JoinTag(all);\n\t};\n}\n\n//bool ValidateUrl(const QString &value) {\n//\tconst auto match = qthelp::RegExpDomain().match(value);\n//\tif (!match.hasMatch() || match.capturedStart() != 0) {\n//\t\treturn false;\n//\t}\n//\tconst auto protocolMatch = RegExpProtocol().match(value);\n//\treturn protocolMatch.hasMatch()\n//\t\t&& IsGoodProtocol(protocolMatch.captured(1));\n//}\n\nvoid EditLinkBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tconst TextWithTags &startText,\n\t\tconst QString &startLink,\n\t\tFn<void(TextWithTags, QString)> callback,\n\t\tconst style::InputField *fieldStyle,\n\t\tFn<QString(QString)> validate) {\n\tExpects(callback != nullptr);\n\n\tconst auto &fieldSt = fieldStyle ? *fieldStyle : st::defaultInputField;\n\tconst auto content = box->verticalLayout();\n\n\tconst auto text = content->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tcontent,\n\t\t\tfieldSt,\n\t\t\tUi::InputField::Mode::SingleLine,\n\t\t\ttr::lng_formatting_link_text(),\n\t\t\tstartText),\n\t\tst::markdownLinkFieldPadding);\n\ttext->setInstantReplaces(Ui::InstantReplaces::Default());\n\ttext->setInstantReplacesEnabled(\n\t\tCore::App().settings().replaceEmojiValue(),\n\t\tCore::App().settings().systemTextReplaceValue());\n\tUi::Emoji::SuggestionsController::Init(\n\t\tbox->getDelegate()->outerContainer(),\n\t\ttext,\n\t\t&show->session());\n\tInitSpellchecker(show, text, fieldStyle != nullptr);\n\n\tconst auto placeholder = content->add(\n\t\tobject_ptr<Ui::RpWidget>(content),\n\t\tst::markdownLinkFieldPadding);\n\tplaceholder->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tconst auto link = [&] {\n\t\tif (!startLink.trimmed().isEmpty()) {\n\t\t\treturn startLink.trimmed();\n\t\t}\n\t\tconst auto clipboard = QGuiApplication::clipboard()->text().trimmed();\n\t\tconst auto starts = [&](const auto &protocol) {\n  \t\t\treturn clipboard.startsWith(protocol);\n\t\t};\n\t\treturn std::ranges::any_of(kLinkProtocols, starts) ? clipboard : QString();\n\t}();\n\tconst auto url = Ui::AttachParentChild(\n\t\tcontent,\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tcontent,\n\t\t\tfieldSt,\n\t\t\ttr::lng_formatting_link_url(),\n\t\t\tlink));\n\turl->heightValue(\n\t) | rpl::on_next([placeholder](int height) {\n\t\tplaceholder->resize(placeholder->width(), height);\n\t}, placeholder->lifetime());\n\tplaceholder->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\turl->resize(width, url->height());\n\t}, placeholder->lifetime());\n\turl->move(placeholder->pos());\n\n\tconst auto submit = [=] {\n\t\tconst auto linkText = text->getTextWithTags();\n\t\tconst auto linkUrl = validate(url->getLastText());\n\t\tif (linkText.text.isEmpty()) {\n\t\t\ttext->showError();\n\t\t\treturn;\n\t\t} else if (linkUrl.isEmpty()) {\n\t\t\turl->showError();\n\t\t\treturn;\n\t\t}\n\t\tconst auto weak = base::make_weak(box);\n\t\tcallback(linkText, linkUrl);\n\t\tif (weak) {\n\t\t\tbox->closeBox();\n\t\t}\n\t};\n\n\ttext->submits(\n\t) | rpl::on_next([=] {\n\t\turl->setFocusFast();\n\t}, text->lifetime());\n\turl->submits(\n\t) | rpl::on_next([=] {\n\t\tif (text->getLastText().isEmpty()) {\n\t\t\ttext->setFocusFast();\n\t\t} else {\n\t\t\tsubmit();\n\t\t}\n\t}, url->lifetime());\n\n\tbox->setTitle(url->getLastText().isEmpty()\n\t\t? tr::lng_formatting_link_create_title()\n\t\t: tr::lng_formatting_link_edit_title());\n\n\tbox->addButton(tr::lng_formatting_link_create(), submit);\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\n\tcontent->resizeToWidth(st::boxWidth);\n\tcontent->moveToLeft(0, 0);\n\tbox->setWidth(st::boxWidth);\n\n\tbox->setFocusCallback([=] {\n\t\tif (startText.text.isEmpty()) {\n\t\t\ttext->setFocusFast();\n\t\t} else {\n\t\t\tif (!url->empty()) {\n\t\t\t\turl->selectAll();\n\t\t\t}\n\t\t\turl->setFocusFast();\n\t\t}\n\t});\n\n\tconst auto clearFullSelection = [=](not_null<Ui::InputField*> input) {\n\t\tif (input->empty()) {\n\t\t\treturn;\n\t\t}\n\t\tauto cursor = input->rawTextEdit()->textCursor();\n\t\tconst auto hasFull = (!cursor.selectionStart()\n\t\t\t&& (cursor.selectionEnd()\n\t\t\t\t== (input->rawTextEdit()->document()->characterCount() - 1)));\n\t\tif (hasFull) {\n\t\t\tcursor.clearSelection();\n\t\t\tinput->setTextCursor(cursor);\n\t\t}\n\t};\n\n\turl->tabbed(\n\t) | rpl::on_next([=](not_null<bool*> handled) {\n\t\tclearFullSelection(url);\n\t\ttext->setFocus();\n\t\t*handled = true;\n\t}, url->lifetime());\n\n\ttext->tabbed(\n\t) | rpl::on_next([=](not_null<bool*> handled) {\n\t\tif (!url->empty()) {\n\t\t\turl->selectAll();\n\t\t}\n\t\tclearFullSelection(text);\n\t\turl->setFocus();\n\t\t*handled = true;\n\t}, text->lifetime());\n}\n\nvoid EditCodeLanguageBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tQString now,\n\t\tFn<void(QString)> save) {\n\tExpects(save != nullptr);\n\n\tbox->setTitle(tr::lng_formatting_code_title());\n\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\tbox,\n\t\ttr::lng_formatting_code_language(),\n\t\tst::settingsAddReplyLabel));\n\tconst auto field = box->addRow(object_ptr<Ui::InputField>(\n\t\tbox,\n\t\tst::settingsAddReplyField,\n\t\ttr::lng_formatting_code_auto(),\n\t\tnow.trimmed()));\n\tbox->setFocusCallback([=] {\n\t\tfield->setFocusFast();\n\t});\n\tfield->selectAll();\n\tfield->setMaxLength(kCodeLanguageLimit);\n\n\tUi::AddLengthLimitLabel(field, kCodeLanguageLimit);\n\n\tconst auto callback = [=] {\n\t\tconst auto name = field->getLastText().trimmed();\n\t\tconst auto check = QRegularExpression(\"^[a-zA-Z0-9\\\\+\\\\-]*$\");\n\t\tif (check.match(name).hasMatch()) {\n\t\t\tauto weak = base::make_weak(box);\n\t\t\tsave(name);\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t} else {\n\t\t\tfield->showError();\n\t\t}\n\t};\n\tfield->submits(\n\t) | rpl::on_next(callback, field->lifetime());\n\tbox->addButton(tr::lng_settings_save(), callback);\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nTextWithEntities StripSupportHashtag(TextWithEntities text) {\n\tstatic const auto expression = QRegularExpression(\n\t\tu\"\\\\n?#tsf[a-z0-9_-]*[\\\\s#a-z0-9_-]*$\"_q,\n\t\tQRegularExpression::CaseInsensitiveOption);\n\tconst auto match = expression.match(text.text);\n\tif (!match.hasMatch()) {\n\t\treturn text;\n\t}\n\ttext.text.chop(match.capturedLength());\n\tconst auto length = text.text.size();\n\tif (!length) {\n\t\treturn TextWithEntities();\n\t}\n\tfor (auto i = text.entities.begin(); i != text.entities.end();) {\n\t\tauto &entity = *i;\n\t\tif (entity.offset() >= length) {\n\t\t\ti = text.entities.erase(i);\n\t\t\tcontinue;\n\t\t} else if (entity.offset() + entity.length() > length) {\n\t\t\tentity.shrinkFromRight(length - entity.offset());\n\t\t}\n\t\t++i;\n\t}\n\treturn text;\n}\n\n} // namespace\n\nQString PrepareMentionTag(not_null<UserData*> user) {\n\treturn TextUtilities::kMentionTagStart\n\t\t+ QString::number(user->id.value)\n\t\t+ '.'\n\t\t+ QString::number(user->accessHash())\n\t\t+ ':'\n\t\t+ QString::number(user->session().userId().bare);\n}\n\nTextWithTags PrepareEditText(not_null<HistoryItem*> item) {\n\tauto original = item->history()->session().supportMode()\n\t\t? StripSupportHashtag(item->originalText())\n\t\t: item->originalText();\n\toriginal = DropDisallowedCustomEmoji(\n\t\titem->history()->peer,\n\t\tstd::move(original));\n\treturn TextWithTags{\n\t\toriginal.text,\n\t\tTextUtilities::ConvertEntitiesToTextTags(original.entities)\n\t};\n}\n\nbool EditTextChanged(\n\t\tnot_null<HistoryItem*> item,\n\t\tTextWithTags updated) {\n\tconst auto original = PrepareEditText(item);\n\n\tauto originalWithEntities = TextWithEntities{\n\t\tstd::move(original.text),\n\t\tTextUtilities::ConvertTextTagsToEntities(original.tags)\n\t};\n\tauto updatedWithEntities = TextWithEntities{\n\t\tstd::move(updated.text),\n\t\tTextUtilities::ConvertTextTagsToEntities(updated.tags)\n\t};\n\tTextUtilities::PrepareForSending(originalWithEntities, 0);\n\tTextUtilities::PrepareForSending(updatedWithEntities, 0);\n\n\t// Tags can be different for the same entities, because for\n\t// animated emoji each tag contains a different random number.\n\t// So we compare entities instead of tags.\n\treturn originalWithEntities != updatedWithEntities;\n}\n\nFn<bool(\n\tUi::InputField::EditLinkSelection selection,\n\tTextWithTags text,\n\tQString link,\n\tEditLinkAction action)> DefaultEditLinkCallback(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<Ui::InputField*> field,\n\t\tconst style::InputField *fieldStyle) {\n\tconst auto weak = base::make_weak(field);\n\treturn [=](\n\t\t\tEditLinkSelection selection,\n\t\t\tTextWithTags text,\n\t\t\tQString link,\n\t\t\tEditLinkAction action) {\n\t\tif (action == EditLinkAction::Check) {\n\t\t\treturn (Ui::InputField::IsValidMarkdownLink(link)\n\t\t\t\t\t&& !TextUtilities::IsMentionLink(link))\n\t\t\t\t|| Ui::InputField::IsCustomDateLink(link);\n\t\t}\n\t\tif (Ui::InputField::IsCustomDateLink(link)) {\n\t\t\tconst auto dateStr = link.mid(\n\t\t\t\tUi::InputField::kCustomDateTagStart.size());\n\t\t\tconst auto existingDate = dateStr.toInt();\n\t\t\tauto callback = [=](\n\t\t\t\t\tconst TextWithTags &t,\n\t\t\t\t\tconst QString &l) {\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->commitMarkdownLinkEdit(selection, t, l);\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto savedCallback = std::make_shared<\n\t\t\t\tFn<void(const TextWithTags &, const QString &)>>(\n\t\t\t\t\tstd::move(callback));\n\t\t\tconst auto savedText = std::make_shared<TextWithTags>(text);\n\t\t\tconst auto showDateTimeBox = [=](TimeId time) {\n\t\t\t\tconst auto dateBox = std::make_shared<\n\t\t\t\t\tbase::weak_qptr<Ui::GenericBox>>();\n\t\t\t\t*dateBox = show->show(Box(\n\t\t\t\t\tUi::ChooseDateTimeBox,\n\t\t\t\t\tUi::ChooseDateTimeBoxArgs{\n\t\t\t\t\t\t.title = tr::lng_formatting_date_title(),\n\t\t\t\t\t\t.submit = tr::lng_settings_save(),\n\t\t\t\t\t\t.done = [=](TimeId result) {\n\t\t\t\t\t\t\tconst auto dateLink\n\t\t\t\t\t\t\t\t= Ui::InputField::kCustomDateTagStart\n\t\t\t\t\t\t\t\t+ QString::number(result);\n\t\t\t\t\t\t\t(*savedCallback)(\n\t\t\t\t\t\t\t\t*savedText,\n\t\t\t\t\t\t\t\tdateLink);\n\t\t\t\t\t\t\tif (const auto box = dateBox->get()) {\n\t\t\t\t\t\t\t\tbox->closeBox();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t.min = [] { return TimeId(1); },\n\t\t\t\t\t\t.time = time,\n\t\t\t\t\t\t.max = [] { return TimeId(2114380800); },\n\t\t\t\t\t}));\n\t\t\t};\n\t\t\tif (existingDate > 0) {\n\t\t\t\tshowDateTimeBox(existingDate);\n\t\t\t} else {\n\t\t\t\tshow->show(Box<Ui::CalendarBox>(Ui::CalendarBoxArgs{\n\t\t\t\t\t.month = QDate::currentDate(),\n\t\t\t\t\t.highlighted = QDate::currentDate(),\n\t\t\t\t\t.callback = [=](QDate chosen, Fn<void()> close) {\n\t\t\t\t\t\tclose();\n\t\t\t\t\t\tconst auto midday = QDateTime(\n\t\t\t\t\t\t\tchosen,\n\t\t\t\t\t\t\tQTime(12, 0));\n\t\t\t\t\t\tshowDateTimeBox(\n\t\t\t\t\t\t\tbase::unixtime::serialize(midday));\n\t\t\t\t\t},\n\t\t\t\t\t.minDate = QDate(1970, 1, 1),\n\t\t\t\t\t.maxDate = QDate(2036, 12, 31),\n\t\t\t\t}));\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tauto callback = [=](const TextWithTags &text, const QString &link) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->commitMarkdownLinkEdit(selection, text, link);\n\t\t\t}\n\t\t};\n\t\tshow->showBox(Box(\n\t\t\tEditLinkBox,\n\t\t\tshow,\n\t\t\ttext,\n\t\t\tlink,\n\t\t\tstd::move(callback),\n\t\t\tfieldStyle,\n\t\t\tqthelp::validate_url));\n\t\treturn true;\n\t};\n}\n\nFn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(\n\t\tstd::shared_ptr<Ui::Show> show) {\n\treturn [=](QString now, Fn<void(QString)> save) {\n\t\tshow->showBox(Box(EditCodeLanguageBox, now, save));\n\t};\n}\n\nauto InitMessageFieldHandlers(MessageFieldHandlersArgs &&args)\n-> std::shared_ptr<Ui::ChatStyle> {\n\tconst auto paused = [passed = args.customEmojiPaused] {\n\t\treturn passed && passed();\n\t};\n\tconst auto field = args.field;\n\tconst auto session = args.session;\n\tfield->setTagMimeProcessor(\n\t\tFieldTagMimeProcessor(session, args.allowPremiumEmoji));\n\tfield->setCustomTextContext(Core::TextContext({\n\t\t.session = session\n\t}), [paused] {\n\t\treturn On(PowerSaving::kEmojiChat) || paused();\n\t}, [paused] {\n\t\treturn On(PowerSaving::kChatSpoiler) || paused();\n\t});\n\tfield->setInstantReplaces(Ui::InstantReplaces::Default());\n\tfield->setInstantReplacesEnabled(\n\t\tCore::App().settings().replaceEmojiValue(),\n\t\tCore::App().settings().systemTextReplaceValue());\n\tfield->setMarkdownReplacesEnabled(rpl::single(Ui::MarkdownEnabledState{\n\t\tUi::MarkdownEnabled{ std::move(args.allowMarkdownTags) }\n\t}));\n\tif (const auto &show = args.show) {\n\t\tfield->setEditLinkCallback(\n\t\t\tDefaultEditLinkCallback(show, field, args.fieldStyle));\n\t\tfield->setEditLanguageCallback(DefaultEditLanguageCallback(show));\n\t\tInitSpellchecker(show, field, args.fieldStyle != nullptr);\n\t}\n\tconst auto style = std::make_shared<Ui::ChatStyle>(\n\t\tsession->colorIndicesValue());\n\tfield->setPreCache([=] {\n\t\treturn style->messageStyle(false, false).preCache.get();\n\t});\n\tfield->setBlockquoteCache([=] {\n\t\tconst auto colorIndex = session->user()->colorIndex();\n\t\treturn style->coloredQuoteCache(false, colorIndex).get();\n\t});\n\treturn style;\n}\n\n[[nodiscard]] bool IsGoodFactcheckUrl(QStringView url) {\n\treturn url.startsWith(u\"t.me/\"_q) || url.startsWith(u\"https://t.me/\"_q);\n}\n\n[[nodiscard]] Fn<bool(\n\tUi::InputField::EditLinkSelection selection,\n\tTextWithTags text,\n\tQString link,\n\tEditLinkAction action)> FactcheckEditLinkCallback(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<Ui::InputField*> field) {\n\tconst auto weak = base::make_weak(field);\n\treturn [=](\n\t\t\tEditLinkSelection selection,\n\t\t\tTextWithTags text,\n\t\t\tQString link,\n\t\t\tEditLinkAction action) {\n\t\tconst auto validate = [=](QString url) {\n\t\t\tif (IsGoodFactcheckUrl(url)) {\n\t\t\t\tconst auto start = u\"https://\"_q;\n\t\t\t\treturn url.startsWith(start) ? url : (start + url);\n\t\t\t}\n\t\t\tshow->showToast(\n\t\t\t\ttr::lng_factcheck_links(tr::now, tr::rich));\n\t\t\treturn QString();\n\t\t};\n\t\tif (action == EditLinkAction::Check) {\n\t\t\treturn IsGoodFactcheckUrl(link);\n\t\t}\n\t\tauto callback = [=](const TextWithTags &text, const QString &link) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->commitMarkdownLinkEdit(selection, text, link);\n\t\t\t}\n\t\t};\n\t\tshow->showBox(Box(\n\t\t\tEditLinkBox,\n\t\t\tshow,\n\t\t\ttext,\n\t\t\tlink,\n\t\t\tstd::move(callback),\n\t\t\tnullptr,\n\t\t\tvalidate));\n\t\treturn true;\n\t};\n}\n\nFn<void(not_null<Ui::InputField*>)> FactcheckFieldIniter(\n\t\tstd::shared_ptr<Main::SessionShow> show) {\n\tExpects(show != nullptr);\n\n\treturn [=](not_null<Ui::InputField*> field) {\n\t\tfield->setTagMimeProcessor([](QStringView mimeTag) {\n\t\t\tusing Field = Ui::InputField;\n\t\t\tauto all = TextUtilities::SplitTags(mimeTag);\n\t\t\tfor (auto i = all.begin(); i != all.end();) {\n\t\t\t\tconst auto tag = *i;\n\t\t\t\tif (tag != Field::kTagBold\n\t\t\t\t\t&& tag != Field::kTagItalic\n\t\t\t\t\t&& (!Field::IsValidMarkdownLink(mimeTag)\n\t\t\t\t\t\t|| TextUtilities::IsMentionLink(mimeTag))) {\n\t\t\t\t\ti = all.erase(i);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t++i;\n\t\t\t}\n\t\t\treturn TextUtilities::JoinTag(all);\n\t\t});\n\t\tfield->setInstantReplaces(Ui::InstantReplaces::Default());\n\t\tfield->setInstantReplacesEnabled(\n\t\t\tCore::App().settings().replaceEmojiValue(),\n\t\t\tCore::App().settings().systemTextReplaceValue());\n\t\tfield->setMarkdownReplacesEnabled(rpl::single(\n\t\t\tUi::MarkdownEnabledState{\n\t\t\t\tUi::MarkdownEnabled{\n\t\t\t\t\t{ Ui::InputField::kTagBold, Ui::InputField::kTagItalic }\n\t\t\t\t}\n\t\t\t}\n\t\t));\n\t\tfield->setEditLinkCallback(FactcheckEditLinkCallback(show, field));\n\t\tInitSpellchecker(show, field);\n\t};\n}\n\nvoid InitMessageFieldHandlers(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::InputField*> field,\n\t\tChatHelpers::PauseReason pauseReasonLevel,\n\t\tFn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {\n\tInitMessageFieldHandlers({\n\t\t.session = &controller->session(),\n\t\t.show = controller->uiShow(),\n\t\t.field = field,\n\t\t.customEmojiPaused = [=] {\n\t\t\treturn controller->isGifPausedAtLeastFor(pauseReasonLevel);\n\t\t},\n\t\t.allowPremiumEmoji = std::move(allowPremiumEmoji),\n\t});\n}\n\nvoid InitMessageFieldGeometry(not_null<Ui::InputField*> field) {\n\tfield->setMinHeight(\n\t\tst::historySendSize.height() - 2 * st::historySendPadding);\n\tfield->setMaxHeight(st::historyComposeFieldMaxHeight);\n\n\t// st::messageSendingAnimationTextFromOffset.\n\tfield->setDocumentMargin(4.);\n\tfield->setAdditionalMargin(style::ConvertScale(4) - 4);\n}\n\nstd::shared_ptr<Ui::ChatStyle> InitMessageField(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<Ui::InputField*> field,\n\t\tFn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {\n\tconst auto style = InitMessageFieldHandlers({\n\t\t.session = &show->session(),\n\t\t.show = show,\n\t\t.field = field,\n\t\t.customEmojiPaused = [=] {\n\t\t\treturn show->paused(ChatHelpers::PauseReason::Any);\n\t\t},\n\t\t.allowPremiumEmoji = std::move(allowPremiumEmoji),\n\t});\n\tInitMessageFieldGeometry(field);\n\treturn style;\n}\n\nstd::shared_ptr<Ui::ChatStyle> InitMessageField(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::InputField*> field,\n\t\tFn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {\n\treturn InitMessageField(\n\t\tcontroller->uiShow(),\n\t\tfield,\n\t\tstd::move(allowPremiumEmoji));\n}\n\nvoid InitSpellchecker(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<Ui::InputField*> field,\n\t\tbool skipDictionariesManager) {\n#ifndef TDESKTOP_DISABLE_SPELLCHECK\n\tusing namespace Spellchecker;\n\tconst auto session = &show->session();\n\tconst auto menuItem = skipDictionariesManager\n\t\t? std::nullopt\n\t\t: std::make_optional(SpellingHighlighter::CustomContextMenuItem{\n\t\t\ttr::lng_settings_manage_dictionaries(tr::now),\n\t\t\t[=] { show->showBox(Box<Ui::ManageDictionariesBox>(session)); }\n\t\t});\n\tconst auto s = Ui::CreateChild<SpellingHighlighter>(\n\t\tfield.get(),\n\t\tCore::App().settings().spellcheckerEnabledValue(),\n\t\tmenuItem);\n\tfield->setExtendedContextMenu(s->contextMenuCreated());\n#endif // TDESKTOP_DISABLE_SPELLCHECK\n\n\t// Add wrong keyboard layout fixer.\n\tbase::install_event_filter(field.get(), [=](not_null<QEvent*> e) {\n\t\tif (e->type() != QEvent::KeyPress) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\t\tconst auto k = static_cast<QKeyEvent*>(e.get());\n\t\tconst auto rightBracket = (k->key() == Qt::Key_BracketRight)\n\t\t\t|| (k->key() == 1066);\n\t\tif (rightBracket\n\t\t\t&& k->modifiers().testFlag(Qt::ControlModifier)) {\n\t\t\tconst auto text = field->getLastText();\n\t\t\tconst auto translatedString = rusKeyboardLayoutSwitch(text);\n\t\t\tif (text != translatedString) {\n\t\t\t\tfield->setTextWithTags(\n\t\t\t\t\t{ translatedString, TextWithTags::Tags() });\n\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t}\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n}\n\nbool HasSendText(not_null<const Ui::InputField*> field) {\n\tconst auto &text = field->getTextWithTags().text;\n\tfor (const auto &ch : text) {\n\t\tconst auto code = ch.unicode();\n\t\tif (!IsTrimmed(ch) && !IsReplacedBySpace(code)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid InitMessageFieldFade(\n\t\tnot_null<Ui::InputField*> field,\n\t\tconst style::color &bg) {\n\tclass Fade final : public Ui::RpWidget {\n\tpublic:\n\t\tusing Ui::RpWidget::RpWidget;\n\n\t\tvoid setFade(QPixmap &&fade) {\n\t\t\t_fade = std::move(fade);\n\t\t}\n\n\t\tint resizeGetHeight(int newWidth) override {\n\t\t\treturn st::historyComposeFieldFadeHeight;\n\t\t}\n\n\tprivate:\n\t\tvoid paintEvent(QPaintEvent *event) override {\n\t\t\tauto p = QPainter(this);\n\t\t\tp.drawTiledPixmap(rect(), _fade);\n\t\t}\n\n\t\tQPixmap _fade;\n\n\t};\n\n\tconst auto topFade = Ui::CreateChild<Fade>(field.get());\n\tconst auto bottomFade = Ui::CreateChild<Fade>(field.get());\n\n\tconst auto generateFade = [=] {\n\t\tconst auto size = QSize(1, st::historyComposeFieldFadeHeight);\n\t\tauto fade = QPixmap(size * style::DevicePixelRatio());\n\t\tfade.setDevicePixelRatio(style::DevicePixelRatio());\n\t\tfade.fill(Qt::transparent);\n\t\t{\n\t\t\tauto p = QPainter(&fade);\n\n\t\t\tauto gradient = QLinearGradient(0, 1, 0, size.height());\n\t\t\tgradient.setStops({ { 0., bg->c }, { .9, Qt::transparent } });\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(gradient);\n\t\t\tp.drawRect(Rect(size));\n\t\t}\n\t\tbottomFade->setFade(fade.transformed(QTransform().scale(1, -1)));\n\t\ttopFade->setFade(std::move(fade));\n\t};\n\tgenerateFade();\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tgenerateFade();\n\t}, topFade->lifetime());\n\n\tfield->sizeValue(\n\t) | rpl::on_next_done([=](const QSize &size) {\n\t\ttopFade->resizeToWidth(size.width());\n\t\tbottomFade->resizeToWidth(size.width());\n\t\tbottomFade->move(\n\t\t\t0,\n\t\t\tsize.height() - st::historyComposeFieldFadeHeight);\n\t}, [t = base::make_weak(topFade), b = base::make_weak(bottomFade)] {\n\t\tUi::DestroyChild(t.get());\n\t\tUi::DestroyChild(b.get());\n\t}, topFade->lifetime());\n\n\tconst auto descent = field->st().style.font->descent;\n\trpl::merge(\n\t\tfield->changes(),\n\t\tfield->scrollTop().changes() | rpl::to_empty,\n\t\tfield->sizeValue() | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\t// InputField::changes fires before the auto-resize is being applied,\n\t\t// so for the scroll values to be accurate we enqueue the check.\n\t\tInvokeQueued(field, [=] {\n\t\t\tconst auto topHidden = !field->scrollTop().current();\n\t\t\tif (topFade->isHidden() != topHidden) {\n\t\t\t\ttopFade->setVisible(!topHidden);\n\t\t\t}\n\t\t\tconst auto adjusted = field->scrollTop().current() + descent;\n\t\t\tconst auto bottomHidden = (adjusted >= field->scrollTopMax());\n\t\t\tif (bottomFade->isHidden() != bottomHidden) {\n\t\t\t\tbottomFade->setVisible(!bottomHidden);\n\t\t\t}\n\t\t});\n\t}, topFade->lifetime());\n}\n\nInlineBotQuery ParseInlineBotQuery(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<const Ui::InputField*> field) {\n\tauto result = InlineBotQuery();\n\n\tconst auto &full = field->getTextWithTags();\n\tconst auto &text = full.text;\n\tconst auto textLength = text.size();\n\n\tauto inlineUsernameStart = 1;\n\tauto inlineUsernameLength = 0;\n\tif (textLength > 2 && text[0] == '@' && text[1].isLetter()) {\n\t\tinlineUsernameLength = 1;\n\t\tfor (auto i = inlineUsernameStart + 1; i != textLength; ++i) {\n\t\t\tconst auto ch = text[i];\n\t\t\tif (ch.isLetterOrNumber() || ch.unicode() == '_') {\n\t\t\t\t++inlineUsernameLength;\n\t\t\t\tcontinue;\n\t\t\t} else if (!ch.isSpace()) {\n\t\t\t\tinlineUsernameLength = 0;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tauto inlineUsernameEnd = inlineUsernameStart + inlineUsernameLength;\n\t\tauto inlineUsernameEqualsText = (inlineUsernameEnd == textLength);\n\t\tauto validInlineUsername = false;\n\t\tif (inlineUsernameEqualsText) {\n\t\t\tvalidInlineUsername = text.endsWith(u\"bot\"_q);\n\t\t} else if (inlineUsernameEnd < textLength && inlineUsernameLength) {\n\t\t\tvalidInlineUsername = text[inlineUsernameEnd].isSpace();\n\t\t}\n\t\tif (validInlineUsername) {\n\t\t\tif (!full.tags.isEmpty()\n\t\t\t\t&& (full.tags.front().offset\n\t\t\t\t\t< inlineUsernameStart + inlineUsernameLength)) {\n\t\t\t\treturn InlineBotQuery();\n\t\t\t}\n\t\t\tauto username = base::StringViewMid(text, inlineUsernameStart, inlineUsernameLength);\n\t\t\tif (username != result.username) {\n\t\t\t\tresult.username = username.toString();\n\t\t\t\tif (const auto peer = session->data().peerByUsername(result.username)) {\n\t\t\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\t\t\tresult.bot = user;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresult.bot = nullptr;\n\t\t\t\t\t}\n\t\t\t\t\tresult.lookingUpBot = false;\n\t\t\t\t} else {\n\t\t\t\t\tresult.bot = nullptr;\n\t\t\t\t\tresult.lookingUpBot = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (result.bot\n\t\t\t\t&& (!result.bot->isBot()\n\t\t\t\t\t|| result.bot->botInfo->inlinePlaceholder.isEmpty())) {\n\t\t\t\tresult.bot = nullptr;\n\t\t\t} else {\n\t\t\t\tresult.query = inlineUsernameEqualsText\n\t\t\t\t\t? QString()\n\t\t\t\t\t: text.mid(inlineUsernameEnd + 1);\n\t\t\t\treturn result;\n\t\t\t}\n\t\t} else {\n\t\t\tinlineUsernameLength = 0;\n\t\t}\n\t}\n\tif (inlineUsernameLength < 3) {\n\t\tresult.bot = nullptr;\n\t\tresult.username = QString();\n\t}\n\tresult.query = QString();\n\treturn result;\n}\n\nAutocompleteQuery ParseMentionHashtagBotCommandQuery(\n\t\tnot_null<const Ui::InputField*> field,\n\t\tChatHelpers::ComposeFeatures features) {\n\tauto result = AutocompleteQuery();\n\n\tconst auto cursor = field->textCursor();\n\tif (cursor.hasSelection()) {\n\t\treturn result;\n\t}\n\n\tconst auto position = cursor.position();\n\tconst auto document = field->document();\n\tconst auto block = document->findBlock(position);\n\tfor (auto item = block.begin(); !item.atEnd(); ++item) {\n\t\tconst auto fragment = item.fragment();\n\t\tif (!fragment.isValid()) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto fragmentPosition = fragment.position();\n\t\tconst auto fragmentEnd = fragmentPosition + fragment.length();\n\t\tif (fragmentPosition >= position || fragmentEnd < position) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto format = fragment.charFormat();\n\t\tif (format.isImageFormat()) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tbool mentionInCommand = false;\n\t\tconst auto text = fragment.text();\n\t\tfor (auto i = position - fragmentPosition; i != 0; --i) {\n\t\t\tif (text[i - 1] == '@') {\n\t\t\t\tif (!features.autocompleteMentions) {\n\t\t\t\t\treturn {};\n\t\t\t\t}\n\t\t\t\tif ((position - fragmentPosition - i < 1 || text[i].isLetter()) && (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_'))) {\n\t\t\t\t\tresult.fromStart = (i == 1) && (fragmentPosition == 0);\n\t\t\t\t\tresult.query = text.mid(i - 1, position - fragmentPosition - i + 1);\n\t\t\t\t} else if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && i > 2 && (text[i - 2].isLetterOrNumber() || text[i - 2] == '_') && !mentionInCommand) {\n\t\t\t\t\tmentionInCommand = true;\n\t\t\t\t\t--i;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t} else if (text[i - 1] == '#') {\n\t\t\t\tif (!features.autocompleteHashtags) {\n\t\t\t\t\treturn {};\n\t\t\t\t}\n\t\t\t\tif (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_')) {\n\t\t\t\t\tresult.fromStart = (i == 1) && (fragmentPosition == 0);\n\t\t\t\t\tresult.query = text.mid(i - 1, position - fragmentPosition - i + 1);\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t} else if (text[i - 1] == '/') {\n\t\t\t\tif (!features.autocompleteCommands) {\n\t\t\t\t\treturn {};\n\t\t\t\t}\n\t\t\t\tif (i < 2 && !fragmentPosition) {\n\t\t\t\t\tresult.fromStart = (i == 1) && (fragmentPosition == 0);\n\t\t\t\t\tresult.query = text.mid(i - 1, position - fragmentPosition - i + 1);\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tif (position - fragmentPosition - i > 127 || (!mentionInCommand && (position - fragmentPosition - i > 63))) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (!text[i - 1].isLetterOrNumber() && text[i - 1] != '_') {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tbreak;\n\t}\n\treturn result;\n}\n\nMessageLinksParser::MessageLinksParser(not_null<Ui::InputField*> field)\n: _field(field)\n, _timer([=] { parse(); }) {\n\t_lifetime = _field->changes(\n\t) | rpl::on_next([=] {\n\t\tconst auto length = _field->getTextWithTags().text.size();\n\t\tif (!length) {\n\t\t\t_lastLength = 0;\n\t\t\t_timer.cancel();\n\t\t\tparse();\n\t\t\treturn;\n\t\t}\n\t\tconst auto timeout = (std::abs(length - _lastLength) > 2)\n\t\t\t? 0\n\t\t\t: kParseLinksTimeout;\n\t\tif (!_timer.isActive() || timeout < _timer.remainingTime()) {\n\t\t\t_timer.callOnce(timeout);\n\t\t}\n\t\t_lastLength = length;\n\t});\n\t_field->installEventFilter(this);\n}\n\nvoid MessageLinksParser::parseNow() {\n\t_timer.cancel();\n\tparse();\n}\n\nvoid MessageLinksParser::setDisabled(bool disabled) {\n\t_disabled = disabled;\n}\n\nbool MessageLinksParser::eventFilter(QObject *object, QEvent *event) {\n\tif (object == _field) {\n\t\tif (event->type() == QEvent::KeyPress) {\n\t\t\tconst auto text = static_cast<QKeyEvent*>(event)->text();\n\t\t\tif (!text.isEmpty() && text.size() < 3) {\n\t\t\t\tconst auto ch = text[0];\n\t\t\t\tif (IsSpace(ch)) {\n\t\t\t\t\t_timer.callOnce(0);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (event->type() == QEvent::Drop) {\n\t\t\t_timer.callOnce(0);\n\t\t}\n\t}\n\treturn QObject::eventFilter(object, event);\n}\n\nvoid MessageLinksParser::parse() {\n\tconst auto &textWithTags = _field->getTextWithTags();\n\tconst auto &text = textWithTags.text;\n\tconst auto &tags = textWithTags.tags;\n\tconst auto &markdownTags = _field->getMarkdownTags();\n\tif (_disabled || text.isEmpty()) {\n\t\t_ranges = {};\n\t\t_list = QStringList();\n\t\treturn;\n\t}\n\tconst auto tagCanIntersectWithLink = [](const QString &tag) {\n\t\treturn (tag == Ui::InputField::kTagBold)\n\t\t\t|| (tag == Ui::InputField::kTagItalic)\n\t\t\t|| (tag == Ui::InputField::kTagUnderline)\n\t\t\t|| (tag == Ui::InputField::kTagStrikeOut)\n\t\t\t|| (tag == Ui::InputField::kTagSpoiler)\n\t\t\t|| (tag == Ui::InputField::kTagBlockquote)\n\t\t\t|| (tag == Ui::InputField::kTagBlockquoteCollapsed);\n\t};\n\n\t_ranges.clear();\n\n\tauto tag = tags.begin();\n\tconst auto tagsEnd = tags.end();\n\tconst auto processTag = [&] {\n\t\tExpects(tag != tagsEnd);\n\n\t\tif (Ui::InputField::IsValidMarkdownLink(tag->id)\n\t\t\t&& !TextUtilities::IsMentionLink(tag->id)) {\n\t\t\t_ranges.push_back({ tag->offset, tag->length, tag->id });\n\t\t}\n\t\t++tag;\n\t};\n\tconst auto processTagsBefore = [&](int offset) {\n\t\twhile (tag != tagsEnd\n\t\t\t&& (tag->offset + tag->length <= offset\n\t\t\t\t|| tagCanIntersectWithLink(tag->id))) {\n\t\t\tprocessTag();\n\t\t}\n\t};\n\tconst auto hasTagsIntersection = [&](int till) {\n\t\tif (tag == tagsEnd || tag->offset >= till) {\n\t\t\treturn false;\n\t\t}\n\t\twhile (tag != tagsEnd && tag->offset < till) {\n\t\t\tprocessTag();\n\t\t}\n\t\treturn true;\n\t};\n\n\tauto markdownTag = markdownTags.begin();\n\tconst auto markdownTagsEnd = markdownTags.end();\n\tconst auto markdownTagsAllow = [&](int from, int length) {\n\t\twhile (markdownTag != markdownTagsEnd\n\t\t\t&& (markdownTag->adjustedStart\n\t\t\t\t+ markdownTag->adjustedLength <= from\n\t\t\t\t|| !markdownTag->closed\n\t\t\t\t|| tagCanIntersectWithLink(markdownTag->tag))) {\n\t\t\t++markdownTag;\n\t\t}\n\t\tif (markdownTag == markdownTagsEnd\n\t\t\t|| markdownTag->adjustedStart >= from + length) {\n\t\t\treturn true;\n\t\t}\n\t\t// Ignore http-links that are completely inside some tags.\n\t\t// This will allow sending http://test.com/__test__/test correctly.\n\t\treturn (markdownTag->adjustedStart > from)\n\t\t\t|| (markdownTag->adjustedStart\n\t\t\t\t+ markdownTag->adjustedLength < from + length);\n\t};\n\n\tconst auto len = text.size();\n\tconst QChar *start = text.unicode(), *end = start + text.size();\n\tfor (auto offset = 0, matchOffset = offset; offset < len;) {\n\t\tauto m = qthelp::RegExpDomain().match(text, matchOffset);\n\t\tif (!m.hasMatch()) break;\n\n\t\tauto domainOffset = m.capturedStart();\n\n\t\tauto protocol = m.captured(1).toLower();\n\t\tauto topDomain = m.captured(3).toLower();\n\t\tauto isProtocolValid = protocol.isEmpty() || TextUtilities::IsValidProtocol(protocol);\n\t\tauto isTopDomainValid = !protocol.isEmpty() || TextUtilities::IsValidTopDomain(topDomain);\n\n\t\tif (protocol.isEmpty() && domainOffset > offset + 1 && *(start + domainOffset - 1) == QChar('@')) {\n\t\t\tauto forMailName = text.mid(offset, domainOffset - offset - 1);\n\t\t\tauto mMailName = TextUtilities::RegExpMailNameAtEnd().match(forMailName);\n\t\t\tif (mMailName.hasMatch()) {\n\t\t\t\toffset = matchOffset = m.capturedEnd();\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (!isProtocolValid || !isTopDomainValid) {\n\t\t\toffset = matchOffset = m.capturedEnd();\n\t\t\tcontinue;\n\t\t}\n\n\t\tQStack<const QChar*> parenth;\n\t\tconst QChar *domainEnd = start + m.capturedEnd(), *p = domainEnd;\n\t\tfor (; p < end; ++p) {\n\t\t\tQChar ch(*p);\n\t\t\tif (IsLinkEnd(ch)) {\n\t\t\t\tbreak; // link finished\n\t\t\t} else if (IsAlmostLinkEnd(ch)) {\n\t\t\t\tconst QChar *endTest = p + 1;\n\t\t\t\twhile (endTest < end && IsAlmostLinkEnd(*endTest)) {\n\t\t\t\t\t++endTest;\n\t\t\t\t}\n\t\t\t\tif (endTest >= end || IsLinkEnd(*endTest)) {\n\t\t\t\t\tbreak; // link finished at p\n\t\t\t\t}\n\t\t\t\tp = endTest;\n\t\t\t\tch = *p;\n\t\t\t}\n\t\t\tif (ch == '(' || ch == '[' || ch == '{' || ch == '<') {\n\t\t\t\tparenth.push(p);\n\t\t\t} else if (ch == ')' || ch == ']' || ch == '}' || ch == '>') {\n\t\t\t\tif (parenth.isEmpty()) break;\n\t\t\t\tconst QChar *q = parenth.pop(), open(*q);\n\t\t\t\tif ((ch == ')' && open != '(') || (ch == ']' && open != '[') || (ch == '}' && open != '{') || (ch == '>' && open != '<')) {\n\t\t\t\t\tp = q;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (p > domainEnd) { // check, that domain ended\n\t\t\tif (domainEnd->unicode() != '/' && domainEnd->unicode() != '?') {\n\t\t\t\tmatchOffset = domainEnd - start;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tconst auto range = MessageLinkRange{\n\t\t\tint(domainOffset),\n\t\t\tstatic_cast<int>(p - start - domainOffset),\n\t\t\tQString()\n\t\t};\n\t\tprocessTagsBefore(domainOffset);\n\t\tif (!hasTagsIntersection(range.start + range.length)) {\n\t\t\tif (markdownTagsAllow(range.start, range.length)) {\n\t\t\t\t_ranges.push_back(range);\n\t\t\t}\n\t\t}\n\t\toffset = matchOffset = p - start;\n\t}\n\tprocessTagsBefore(Ui::kQFixedMax);\n\n\tapplyRanges(text);\n}\n\nvoid MessageLinksParser::applyRanges(const QString &text) {\n\tconst auto count = int(_ranges.size());\n\tconst auto current = _list.current();\n\tconst auto computeLink = [&](const MessageLinkRange &range) {\n\t\treturn range.custom.isEmpty()\n\t\t\t? base::StringViewMid(text, range.start, range.length)\n\t\t\t: QStringView(range.custom);\n\t};\n\tconst auto changed = [&] {\n\t\tif (current.size() != count) {\n\t\t\treturn true;\n\t\t}\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tif (computeLink(_ranges[i]) != current[i]) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}();\n\tif (!changed) {\n\t\treturn;\n\t}\n\tauto parsed = QStringList();\n\tparsed.reserve(count);\n\tfor (const auto &range : _ranges) {\n\t\tparsed.push_back(computeLink(range).toString());\n\t}\n\t_list = std::move(parsed);\n}\n\nbase::unique_qptr<Ui::RpWidget> CreateDisabledFieldView(\n\t\tQWidget *parent,\n\t\tnot_null<PeerData*> peer) {\n\tauto result = base::make_unique_q<Ui::AbstractButton>(parent);\n\tconst auto raw = result.get();\n\tconst auto label = CreateChild<Ui::FlatLabel>(\n\t\tresult.get(),\n\t\ttr::lng_send_text_no(),\n\t\tst::historySendDisabled);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\traw->setPointerCursor(false);\n\n\tconst auto &st = st::historyComposeField;\n\n\tconst auto metrics = QFontMetricsF(st.style.font->f);\n\tconst auto realAscent = int(base::SafeRound(metrics.ascent()));\n\tconst auto ascentAdd = st.style.font->ascent - realAscent;\n\tconst auto customFontMarginTop = ascentAdd;\n\tconst auto leading = qMax(metrics.leading(), qreal(0.0));\n\tconst auto adjustment = (metrics.ascent() + leading)\n\t\t- ((st.style.font->height * 4) / 5);\n\tconst auto placeholderCustomFontSkip = int(base::SafeRound(-adjustment));\n\n\tconst auto margins = st.textMargins\n\t\t+ st.placeholderMargins\n\t\t+ QMargins(0, style::ConvertScale(4)\n\t\t\t+ placeholderCustomFontSkip\n\t\t\t+ customFontMarginTop, 0, 0);\n\n\traw->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto available = width - margins.left() - margins.right();\n\t\tconst auto skip = st::historySendDisabledIconSkip;\n\t\tlabel->resizeToWidth(available - skip);\n\t\tlabel->moveToLeft(margins.left() + skip, margins.top(), width);\n\t}, label->lifetime());\n\traw->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\tconst auto &icon = st::historySendDisabledIcon;\n\t\ticon.paint(\n\t\t\tp,\n\t\t\tmargins.left() + st::historySendDisabledPosition.x(),\n\t\t\tmargins.top() + st::historySendDisabledPosition.y(),\n\t\t\traw->width());\n\t}, raw->lifetime());\n\tusing WeakToast = base::weak_ptr<Ui::Toast::Instance>;\n\tconst auto toast = raw->lifetime().make_state<WeakToast>();\n\traw->setClickedCallback([=] {\n\t\tif (toast->get()) {\n\t\t\treturn;\n\t\t}\n\t\tusing Flag = ChatRestriction;\n\t\tconst auto map = base::flat_map<Flag, tr::phrase<>>{\n\t\t\t{ Flag::SendPhotos, tr::lng_send_text_type_photos },\n\t\t\t{ Flag::SendVideos, tr::lng_send_text_type_videos },\n\t\t\t{\n\t\t\t\tFlag::SendVideoMessages,\n\t\t\t\ttr::lng_send_text_type_video_messages,\n\t\t\t},\n\t\t\t{ Flag::SendMusic, tr::lng_send_text_type_music },\n\t\t\t{\n\t\t\t\tFlag::SendVoiceMessages,\n\t\t\t\ttr::lng_send_text_type_voice_messages,\n\t\t\t},\n\t\t\t{ Flag::SendFiles, tr::lng_send_text_type_files },\n\t\t\t{ Flag::SendStickers, tr::lng_send_text_type_stickers },\n\t\t\t{ Flag::SendPolls, tr::lng_send_text_type_polls },\n\t\t};\n\t\tauto list = QStringList();\n\t\tfor (const auto &[flag, phrase] : map) {\n\t\t\tif (Data::CanSend(peer, flag, false)) {\n\t\t\t\tlist.append(phrase(tr::now));\n\t\t\t}\n\t\t}\n\t\tif (list.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto types = (list.size() > 1)\n\t\t\t? tr::lng_send_text_type_and_last(\n\t\t\t\ttr::now,\n\t\t\t\tlt_types,\n\t\t\t\tlist.mid(0, list.size() - 1).join(\", \"),\n\t\t\t\tlt_last,\n\t\t\t\tlist.back())\n\t\t\t: list.back();\n\t\t*toast = Ui::Toast::Show(parent, {\n\t\t\t.text = { tr::lng_send_text_no_about(tr::now, lt_types, types) },\n\t\t\t.attach = RectPart::Bottom,\n\t\t\t.duration = kTypesDuration,\n\t\t});\n\t});\n\treturn result;\n}\n\nstd::unique_ptr<Ui::RpWidget> TextErrorSendRestriction(\n\t\tQWidget *parent,\n\t\tconst QString &text) {\n\tauto result = std::make_unique<Ui::RpWidget>(parent);\n\tconst auto raw = result.get();\n\tconst auto label = CreateChild<Ui::FlatLabel>(\n\t\tresult.get(),\n\t\ttext,\n\t\tst::historySendPremiumRequired);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\traw->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tQPainter(raw).fillRect(clip, st::windowBg);\n\t}, raw->lifetime());\n\traw->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tconst auto &st = st::historyComposeField;\n\t\tconst auto width = size.width();\n\t\tconst auto margins = (st.textMargins + st.placeholderMargins);\n\t\tconst auto available = width - margins.left() - margins.right();\n\t\tlabel->resizeToWidth(available);\n\t\tlabel->moveToLeft(\n\t\t\tmargins.left(),\n\t\t\t(size.height() - label->height()) / 2,\n\t\t\twidth);\n\t}, label->lifetime());\n\treturn result;\n}\n\nstd::unique_ptr<Ui::RpWidget> PremiumRequiredSendRestriction(\n\t\tQWidget *parent,\n\t\tnot_null<UserData*> user,\n\t\tnot_null<Window::SessionController*> controller) {\n\tauto result = std::make_unique<Ui::RpWidget>(parent);\n\tconst auto raw = result.get();\n\tconst auto label = CreateChild<Ui::FlatLabel>(\n\t\tresult.get(),\n\t\ttr::lng_restricted_send_non_premium(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\tuser->shortName()),\n\t\tst::historySendPremiumRequired);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tconst auto link = CreateChild<Ui::LinkButton>(\n\t\tresult.get(),\n\t\ttr::lng_restricted_send_non_premium_more(tr::now));\n\traw->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tQPainter(raw).fillRect(clip, st::windowBg);\n\t}, raw->lifetime());\n\traw->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto &st = st::historyComposeField;\n\t\tconst auto margins = (st.textMargins + st.placeholderMargins);\n\t\tconst auto available = width - margins.left() - margins.right();\n\t\tlabel->resizeToWidth(available);\n\t\tconst auto height = label->height() + link->height();\n\t\tconst auto top = (raw->height() - height) / 2;\n\t\tlabel->moveToLeft(margins.left(), top, width);\n\t\tlink->move(\n\t\t\t(width - link->width()) / 2,\n\t\t\tlabel->y() + label->height());\n\t}, label->lifetime());\n\tlink->setClickedCallback([=] {\n\t\tSettings::ShowPremium(controller, u\"require_premium\"_q);\n\t});\n\treturn result;\n}\n\nstd::unique_ptr<Ui::AbstractButton> BoostsToLiftWriteRestriction(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tint boosts) {\n\tauto result = std::make_unique<Ui::FlatButton>(\n\t\tparent,\n\t\ttr::lng_restricted_boost_group(tr::now),\n\t\tst::historyComposeButton);\n\tresult->setClickedCallback([=] {\n\t\tconst auto window = show->resolveWindow();\n\t\twindow->resolveBoostState(peer->asChannel(), boosts);\n\t});\n\treturn result;\n}\n\nstd::unique_ptr<Ui::AbstractButton> FrozenWriteRestriction(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tFrozenWriteRestrictionType type,\n\t\tFreezeInfoStyleOverride st) {\n\tusing namespace Ui;\n\n\tauto result = std::make_unique<FlatButton>(\n\t\tparent,\n\t\tQString(),\n\t\tst::historyComposeButton);\n\tconst auto raw = result.get();\n\n\tconst auto bar = (type == FrozenWriteRestrictionType::DialogsList);\n\tconst auto title = CreateChild<FlatLabel>(\n\t\traw,\n\t\t(bar ? tr::lng_frozen_bar_title : tr::lng_frozen_restrict_title)(\n\t\t\ttr::now),\n\t\tbar ? st::frozenBarTitle : st::frozenRestrictionTitle);\n\ttitle->setAttribute(Qt::WA_TransparentForMouseEvents);\n\ttitle->show();\n\tconst auto subtitle = CreateChild<FlatLabel>(\n\t\traw,\n\t\t(bar\n\t\t\t? tr::lng_frozen_bar_text(\n\t\t\t\tlt_arrow,\n\t\t\t\trpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\t\ttr::marked)\n\t\t\t: tr::lng_frozen_restrict_text(tr::marked)),\n\t\tbar ? st::frozenBarSubtitle : st::frozenRestrictionSubtitle);\n\tsubtitle->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tsubtitle->show();\n\n\tconst auto shadow = bar ? CreateChild<PlainShadow>(raw) : nullptr;\n\tconst auto icon = bar ? CreateChild<RpWidget>(raw) : nullptr;\n\tif (icon) {\n\t\ticon->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(icon);\n\t\t\tst::menuIconDisableAttention.paintInCenter(p, icon->rect());\n\t\t}, icon->lifetime());\n\t\ticon->show();\n\t}\n\n\traw->sizeValue() | rpl::on_next([=](QSize size) {\n\t\tif (bar) {\n\t\t\tconst auto toggle = [&](auto &&widget, bool shown) {\n\t\t\t\tif (widget->isHidden() == shown) {\n\t\t\t\t\twidget->setVisible(shown);\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto small = 2 * st::defaultDialogRow.photoSize;\n\t\t\tconst auto shown = (size.width() > small);\n\t\t\ttoggle(icon, !shown);\n\t\t\ttoggle(title, shown);\n\t\t\ttoggle(subtitle, shown);\n\t\t\ticon->setGeometry(0, 0, size.width(), size.height());\n\t\t}\n\t\tconst auto skip = bar\n\t\t\t? st::defaultDialogRow.padding.left()\n\t\t\t: 2 * st::normalFont->spacew;\n\t\tconst auto available = size.width() - skip * 2;\n\t\ttitle->resizeToWidth(available);\n\t\tsubtitle->resizeToWidth(available);\n\t\tconst auto height = title->height() + subtitle->height();\n\t\tconst auto top = (size.height() - height) / 2;\n\t\ttitle->moveToLeft(skip, top, size.width());\n\t\tsubtitle->moveToLeft(skip, top + title->height(), size.width());\n\n\t\tconst auto line = st::lineWidth;\n\t\tif (shadow) {\n\t\t\tshadow->setGeometry(0, size.height() - line, size.width(), line);\n\t\t}\n\t}, title->lifetime());\n\n\traw->setClickedCallback([=] {\n\t\tshow->show(Box(FrozenInfoBox, &show->session(), st));\n\t});\n\treturn result;\n}\n\nvoid SelectTextInFieldWithMargins(\n\t\tnot_null<Ui::InputField*> field,\n\t\tconst TextSelection &selection) {\n\tif (selection.empty()) {\n\t\treturn;\n\t}\n\tauto textCursor = field->textCursor();\n\t// Try to set equal margins for top and bottom sides.\n\tconst auto charsCountInLine = field->width()\n\t\t/ field->st().style.font->width('W');\n\tconst auto linesCount = field->height() / field->st().style.font->height;\n\tconst auto selectedLines = (selection.to - selection.from)\n\t\t/ charsCountInLine;\n\tconstexpr auto kMinDiff = ushort(3);\n\tif ((linesCount - selectedLines) > kMinDiff) {\n\t\ttextCursor.setPosition(selection.from\n\t\t\t- charsCountInLine * ((linesCount - 1) / 2));\n\t\tfield->setTextCursor(textCursor);\n\t}\n\ttextCursor.setPosition(selection.from);\n\tfield->setTextCursor(textCursor);\n\ttextCursor.setPosition(selection.to, QTextCursor::KeepAnchor);\n\tfield->setTextCursor(textCursor);\n}\n\nTextWithEntities PaidSendButtonText(tr::now_t, int stars) {\n\treturn Ui::Text::IconEmoji(&st::starIconEmoji).append(\n\t\tLang::FormatCountToShort(stars).string);\n}\n\nrpl::producer<TextWithEntities> PaidSendButtonText(\n\t\trpl::producer<int> stars,\n\t\trpl::producer<QString> fallback) {\n\tif (fallback) {\n\t\treturn rpl::combine(\n\t\t\tstd::move(fallback),\n\t\t\tstd::move(stars)\n\t\t) | rpl::map([=](QString zero, int count) {\n\t\t\treturn count\n\t\t\t\t? PaidSendButtonText(tr::now, count)\n\t\t\t\t: TextWithEntities{ zero };\n\t\t});\n\t}\n\treturn std::move(stars) | rpl::map([=](int count) {\n\t\treturn PaidSendButtonText(tr::now, count);\n\t});\n}\n\nvoid FrozenInfoBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tFreezeInfoStyleOverride st) {\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setStyle(st::frozenInfoBox);\n\tbox->setNoContentMargin(true);\n\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\tbox->closeBox();\n\t});\n\n\tconst auto info = session->frozen();\n\tconst auto content = box->verticalLayout();\n\tauto icon = Settings::CreateLottieIcon(\n\t\tcontent,\n\t\t{\n\t\t\t.name = u\"media_forbidden\"_q,\n\t\t\t.sizeOverride = st::normalBoxLottieSize,\n\t\t},\n\t\tst::settingLocalPasscodeIconPadding);\n\tcontent->add(std::move(icon.widget));\n\tbox->setShowFinishedCallback([animate = std::move(icon.animate)] {\n\t\tanimate(anim::repeat::once);\n\t});\n\n\tUi::AddSkip(content);\n\n\tconst auto infoRow = [&](\n\t\t\trpl::producer<QString> title,\n\t\t\trpl::producer<TextWithEntities> text,\n\t\t\tnot_null<const style::icon*> icon) {\n\t\tauto raw = content->add(\n\t\t\tobject_ptr<Ui::VerticalLayout>(content));\n\t\traw->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\traw,\n\t\t\t\tstd::move(title) | rpl::map(tr::bold),\n\t\t\t\tst.infoTitle ? *st.infoTitle : st::defaultFlatLabel),\n\t\t\tst::settingsPremiumRowTitlePadding);\n\t\traw->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\traw,\n\t\t\t\tstd::move(text),\n\t\t\t\tst.infoAbout ? *st.infoAbout : st::upgradeGiftSubtext),\n\t\t\tst::settingsPremiumRowAboutPadding);\n\t\tobject_ptr<Info::Profile::FloatingIcon>(\n\t\t\traw,\n\t\t\t*icon,\n\t\t\tst::starrefInfoIconPosition);\n\t};\n\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_frozen_title(),\n\t\t\tst.title ? *st.title : st::uniqueGiftTitle),\n\t\tst::settingsPremiumRowTitlePadding,\n\t\tstyle::al_top);\n\n\tUi::AddSkip(content, st::defaultVerticalListSkip * 3);\n\n\tinfoRow(\n\t\ttr::lng_frozen_subtitle1(),\n\t\ttr::lng_frozen_text1(tr::marked),\n\t\tst.violationIcon ? st.violationIcon : &st::menuIconBlock);\n\tinfoRow(\n\t\ttr::lng_frozen_subtitle2(),\n\t\ttr::lng_frozen_text2(tr::marked),\n\t\tst.readOnlyIcon ? st.readOnlyIcon : &st::menuIconLock);\n\tinfoRow(\n\t\ttr::lng_frozen_subtitle3(),\n\t\ttr::lng_frozen_text3(\n\t\t\tlt_link,\n\t\t\trpl::single(tr::link(u\"@SpamBot\"_q, info.appealUrl)),\n\t\t\tlt_date,\n\t\t\trpl::single(TextWithEntities{\n\t\t\t\tlangDayOfMonthFull(\n\t\t\t\t\tbase::unixtime::parse(info.until).date()),\n\t\t\t}),\n\t\t\ttr::marked),\n\t\tst.appealIcon ? st.appealIcon : &st::menuIconHourglass);\n\n\tconst auto button = box->addButton(\n\t\ttr::lng_frozen_appeal_button(),\n\t\t[url = info.appealUrl] { UrlClickHandler::Open(url); });\n\tconst auto buttonPadding = st::frozenInfoBox.buttonPadding;\n\tconst auto buttonWidth = st::boxWideWidth\n\t\t- buttonPadding.left()\n\t\t- buttonPadding.right();\n\tbutton->widthValue() | rpl::filter([=] {\n\t\treturn (button->widthNoMargins() != buttonWidth);\n\t}) | rpl::on_next([=] {\n\t\tbutton->resizeToWidth(buttonWidth);\n\t}, button->lifetime());\n}\n\nUi::InputField::MimeDataHook WrappedMessageFieldMimeHook(\n\t\tUi::InputField::MimeDataHook original,\n\t\tnot_null<Ui::InputField*> field) {\n\treturn [field, originalHook = std::move(original)](\n\t\t\tnot_null<const QMimeData*> data,\n\t\t\tUi::InputField::MimeAction action) {\n\t\tif (data->hasFormat(u\"application/x-telegram-input-field\"_q)) {\n\t\t\tif (action == Ui::InputField::MimeAction::Check) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst auto text = QString::fromUtf8(\n\t\t\t\tdata->data(u\"application/x-telegram-input-field\"_q));\n\t\t\tfield->textCursor().insertText(text);\n\t\t\treturn true;\n\t\t}\n\t\treturn originalHook ? originalHook(data, action) : false;\n\t};\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/message_field.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/qt/qt_compare.h\"\n#include \"base/timer.h\"\n#include \"chat_helpers/compose/compose_features.h\"\n#include \"ui/widgets/fields/input_field.h\"\n\n#ifndef TDESKTOP_DISABLE_SPELLCHECK\n#include \"boxes/dictionaries_manager.h\"\n#include \"spellcheck/spelling_highlighter.h\"\n#endif // TDESKTOP_DISABLE_SPELLCHECK\n\n#include <QtGui/QClipboard>\n\nnamespace tr {\nstruct now_t;\n} // namespace tr\n\nnamespace Main {\nclass Session;\nclass SessionShow;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace ChatHelpers {\nenum class PauseReason;\nclass Show;\n} // namespace ChatHelpers\n\nnamespace HistoryView::Controls {\nstruct WriteRestriction;\n} // namespace HistoryView::Controls\n\nnamespace Ui {\nclass ChatStyle;\nclass GenericBox;\nclass PopupMenu;\nclass Show;\n} // namespace Ui\n\n[[nodiscard]] QString PrepareMentionTag(not_null<UserData*> user);\n[[nodiscard]] TextWithTags PrepareEditText(not_null<HistoryItem*> item);\n[[nodiscard]] bool EditTextChanged(\n\tnot_null<HistoryItem*> item,\n\tTextWithTags updated);\n\nFn<bool(\n\tUi::InputField::EditLinkSelection selection,\n\tTextWithTags text,\n\tQString link,\n\tUi::InputField::EditLinkAction action)> DefaultEditLinkCallback(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<Ui::InputField*> field,\n\t\tconst style::InputField *fieldStyle = nullptr);\nFn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(\n\tstd::shared_ptr<Ui::Show> show);\n\nstruct MessageFieldHandlersArgs {\n\tnot_null<Main::Session*> session;\n\tstd::shared_ptr<Main::SessionShow> show; // may be null\n\tnot_null<Ui::InputField*> field;\n\tFn<bool()> customEmojiPaused;\n\tFn<bool(not_null<DocumentData*>)> allowPremiumEmoji;\n\tconst style::InputField *fieldStyle = nullptr;\n\tbase::flat_set<QString> allowMarkdownTags;\n};\nauto InitMessageFieldHandlers(MessageFieldHandlersArgs &&args)\n-> std::shared_ptr<Ui::ChatStyle>;\n\nvoid InitMessageFieldHandlers(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::InputField*> field,\n\tChatHelpers::PauseReason pauseReasonLevel,\n\tFn<bool(not_null<DocumentData*>)> allowPremiumEmoji = nullptr);\nstd::shared_ptr<Ui::ChatStyle> InitMessageField(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<Ui::InputField*> field,\n\tFn<bool(not_null<DocumentData*>)> allowPremiumEmoji);\nstd::shared_ptr<Ui::ChatStyle> InitMessageField(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::InputField*> field,\n\tFn<bool(not_null<DocumentData*>)> allowPremiumEmoji);\n\nvoid InitSpellchecker(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tnot_null<Ui::InputField*> field,\n\tbool skipDictionariesManager = false);\n\n[[nodiscard]] Fn<void(not_null<Ui::InputField*>)> FactcheckFieldIniter(\n\tstd::shared_ptr<Main::SessionShow> show);\n\nbool HasSendText(not_null<const Ui::InputField*> field);\n\nvoid InitMessageFieldFade(\n\tnot_null<Ui::InputField*> field,\n\tconst style::color &bg);\n\nstruct InlineBotQuery {\n\tQString query;\n\tQString username;\n\tUserData *bot = nullptr;\n\tbool lookingUpBot = false;\n};\nInlineBotQuery ParseInlineBotQuery(\n\tnot_null<Main::Session*> session,\n\tnot_null<const Ui::InputField*> field);\n\nstruct AutocompleteQuery {\n\tQString query;\n\tbool fromStart = false;\n};\nAutocompleteQuery ParseMentionHashtagBotCommandQuery(\n\tnot_null<const Ui::InputField*> field,\n\tChatHelpers::ComposeFeatures features);\n\nstruct MessageLinkRange {\n\tint start = 0;\n\tint length = 0;\n\tQString custom;\n\n\tfriend inline auto operator<=>(\n\t\tconst MessageLinkRange&,\n\t\tconst MessageLinkRange&) = default;\n\tfriend inline bool operator==(\n\t\tconst MessageLinkRange&,\n\t\tconst MessageLinkRange&) = default;\n};\n\nclass MessageLinksParser final : private QObject {\npublic:\n\tMessageLinksParser(not_null<Ui::InputField*> field);\n\n\tvoid parseNow();\n\tvoid setDisabled(bool disabled);\n\n\t[[nodiscard]] const rpl::variable<QStringList> &list() const {\n\t\treturn _list;\n\t}\n\t[[nodiscard]] const std::vector<MessageLinkRange> &ranges() const {\n\t\treturn _ranges;\n\t}\n\nprivate:\n\tbool eventFilter(QObject *object, QEvent *event) override;\n\n\tvoid parse();\n\tvoid applyRanges(const QString &text);\n\n\tnot_null<Ui::InputField*> _field;\n\trpl::variable<QStringList> _list;\n\tstd::vector<MessageLinkRange> _ranges;\n\tint _lastLength = 0;\n\tbool _disabled = false;\n\tbase::Timer _timer;\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] base::unique_qptr<Ui::RpWidget> CreateDisabledFieldView(\n\tQWidget *parent,\n\tnot_null<PeerData*> peer);\n[[nodiscard]] std::unique_ptr<Ui::RpWidget> TextErrorSendRestriction(\n\tQWidget *parent,\n\tconst QString &text);\n[[nodiscard]] std::unique_ptr<Ui::RpWidget> PremiumRequiredSendRestriction(\n\tQWidget *parent,\n\tnot_null<UserData*> user,\n\tnot_null<Window::SessionController*> controller);\n[[nodiscard]] auto BoostsToLiftWriteRestriction(\n\tnot_null<QWidget*> parent,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<PeerData*> peer,\n\tint boosts)\n-> std::unique_ptr<Ui::AbstractButton>;\n\nstruct FreezeInfoStyleOverride {\n\tconst style::Box *box = nullptr;\n\tconst style::FlatLabel *title = nullptr;\n\tconst style::FlatLabel *subtitle = nullptr;\n\tconst style::icon *violationIcon = nullptr;\n\tconst style::icon *readOnlyIcon = nullptr;\n\tconst style::icon *appealIcon = nullptr;\n\tconst style::FlatLabel *infoTitle = nullptr;\n\tconst style::FlatLabel *infoAbout = nullptr;\n};\n[[nodiscard]] FreezeInfoStyleOverride DarkFreezeInfoStyle();\n\nenum class FrozenWriteRestrictionType {\n\tMessageField,\n\tDialogsList,\n};\n[[nodiscard]] std::unique_ptr<Ui::AbstractButton> FrozenWriteRestriction(\n\tnot_null<QWidget*> parent,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tFrozenWriteRestrictionType type,\n\tFreezeInfoStyleOverride st = {});\n\nvoid SelectTextInFieldWithMargins(\n\tnot_null<Ui::InputField*> field,\n\tconst TextSelection &selection);\n\n[[nodiscard]] TextWithEntities PaidSendButtonText(tr::now_t, int stars);\n[[nodiscard]] rpl::producer<TextWithEntities> PaidSendButtonText(\n\trpl::producer<int> stars,\n\trpl::producer<QString> fallback = nullptr);\n\nvoid FrozenInfoBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session,\n\tFreezeInfoStyleOverride st);\n\n[[nodiscard]] Ui::InputField::MimeDataHook WrappedMessageFieldMimeHook(\n\tUi::InputField::MimeDataHook original,\n\tnot_null<Ui::InputField*> field);\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/share_message_phrase_factory.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/share_message_phrase_factory.h\"\n\n#include \"data/data_peer.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/text/text_utilities.h\"\n\nnamespace ChatHelpers {\n\nrpl::producer<TextWithEntities> ForwardedMessagePhrase(\n\t\tconst ForwardedMessagePhraseArgs &args) {\n\tif (args.toCount <= 1) {\n\t\tAssert(args.to1);\n\n\t\tif (args.to1->isSelf()) {\n\t\t\tif (args.toSelfWithPremiumIsEmpty && args.to1->isPremium()) {\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\treturn (args.singleMessage\n\t\t\t\t? tr::lng_share_message_to_saved_messages\n\t\t\t\t: tr::lng_share_messages_to_saved_messages)(\n\t\t\t\t\ttr::rich);\n\t\t} else {\n\t\t\treturn (args.singleMessage\n\t\t\t\t? tr::lng_share_message_to_chat\n\t\t\t\t: tr::lng_share_messages_to_chat)(\n\t\t\t\t\tlt_chat,\n\t\t\t\t\trpl::single(TextWithEntities{ args.to1->name() }),\n\t\t\t\t\ttr::rich);\n\t\t}\n\t} else if ((args.toCount == 2) && (args.to1 && args.to2)) {\n\t\treturn (args.singleMessage\n\t\t\t? tr::lng_share_message_to_two_chats\n\t\t\t: tr::lng_share_messages_to_two_chats)(\n\t\t\t\tlt_user,\n\t\t\t\trpl::single(TextWithEntities{ args.to1->name() }),\n\t\t\t\tlt_chat,\n\t\t\t\trpl::single(TextWithEntities{ args.to2->name() }),\n\t\t\t\ttr::rich);\n\t} else {\n\t\treturn (args.singleMessage\n\t\t\t? tr::lng_share_message_to_many_chats\n\t\t\t: tr::lng_share_messages_to_many_chats)(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(args.toCount) | tr::to_count(),\n\t\t\t\ttr::rich);\n\t}\n}\n\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/share_message_phrase_factory.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\n\nnamespace ChatHelpers {\n\nstruct ForwardedMessagePhraseArgs final {\n\tsize_t toCount = 0;\n\tbool singleMessage = false;\n\tPeerData *to1 = nullptr;\n\tPeerData *to2 = nullptr;\n\tbool toSelfWithPremiumIsEmpty = true;\n};\n\nrpl::producer<TextWithEntities> ForwardedMessagePhrase(\n\tconst ForwardedMessagePhraseArgs &args);\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/spellchecker_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/spellchecker_common.h\"\n\n#ifndef TDESKTOP_DISABLE_SPELLCHECK\n\n#include \"base/platform/base_platform_info.h\"\n#include \"base/zlib_help.h\"\n#include \"data/data_session.h\"\n#include \"lang/lang_instance.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"spellcheck/platform/platform_spellcheck.h\"\n#include \"spellcheck/spellcheck_utils.h\"\n#include \"spellcheck/spellcheck_value.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QInputMethod>\n\nnamespace Spellchecker {\n\nnamespace {\n\nusing namespace Storage::CloudBlob;\n\nconstexpr auto kDictExtensions = { \"dic\", \"aff\" };\n\nconstexpr auto kExceptions = {\n\tAppFile,\n\t\"\\xd0\\xa2\\xd0\\xb5\\xd0\\xbb\\xd0\\xb5\\xd0\\xb3\\xd1\\x80\\xd0\\xb0\\xd0\\xbc\"_cs,\n};\n\nconstexpr auto kLangsForLWC = { QLocale::English, QLocale::Portuguese };\nconstexpr auto kDefaultCountries = { QLocale::UnitedStates, QLocale::Brazil };\n\n// Language With Country.\ninline auto LWC(QLocale::Language language, QLocale::Country country) {\n\tif (ranges::contains(kDefaultCountries, country)) {\n\t\treturn int(language);\n\t}\n\treturn (language * 1000) + country;\n}\n\ninline auto LanguageFromLocale(QLocale loc) {\n\tconst auto locLang = loc.language();\n\treturn (ranges::contains(kLangsForLWC, locLang)\n\t\t&& (loc.country() != QLocale::AnyCountry))\n\t\t\t? LWC(locLang, loc.country())\n\t\t\t: int(locLang);\n}\n\nconst auto kDictionaries = {\n\tDict{{ QLocale::English,                               649,   174'516, \"English\" }}, // en_US\n\tDict{{ QLocale::Bulgarian,                             594,   229'658, \"\\xd0\\x91\\xd1\\x8a\\xd0\\xbb\\xd0\\xb3\\xd0\\xb0\\xd1\\x80\\xd1\\x81\\xd0\\xba\\xd0\\xb8\" }}, // bg_BG\n\tDict{{ QLocale::Catalan,                               595,   417'611, \"\\x43\\x61\\x74\\x61\\x6c\\xc3\\xa0\" }}, // ca_ES\n\tDict{{ QLocale::Czech,                                 596,   860'286, \"\\xc4\\x8c\\x65\\xc5\\xa1\\x74\\x69\\x6e\\x61\" }}, // cs_CZ\n\tDict{{ QLocale::Welsh,                                 597,   177'305, \"\\x43\\x79\\x6d\\x72\\x61\\x65\\x67\" }}, // cy_GB\n\tDict{{ QLocale::Danish,                                598,   345'874, \"\\x44\\x61\\x6e\\x73\\x6b\" }}, // da_DK\n\tDict{{ QLocale::German,                                599, 2'412'780, \"\\x44\\x65\\x75\\x74\\x73\\x63\\x68\" }}, // de_DE\n\tDict{{ QLocale::Greek,                                 600, 1'389'160, \"\\xce\\x95\\xce\\xbb\\xce\\xbb\\xce\\xb7\\xce\\xbd\\xce\\xb9\\xce\\xba\\xce\\xac\" }}, // el_GR\n\tDict{{ LWC(QLocale::English, QLocale::Australia),      601,   175'266, \"English (Australia)\" }}, // en_AU\n\tDict{{ LWC(QLocale::English, QLocale::Canada),         602,   174'295, \"English (Canada)\" }}, // en_CA\n\tDict{{ LWC(QLocale::English, QLocale::UnitedKingdom),  603,   174'433, \"English (United Kingdom)\" }}, // en_GB\n\tDict{{ QLocale::Spanish,                               604,   264'717, \"\\x45\\x73\\x70\\x61\\xc3\\xb1\\x6f\\x6c\" }}, // es_ES\n\tDict{{ QLocale::Estonian,                              605,   757'394, \"\\x45\\x65\\x73\\x74\\x69\" }}, // et_EE\n\tDict{{ QLocale::Persian,                               606,   333'911, \"\\xd9\\x81\\xd8\\xa7\\xd8\\xb1\\xd8\\xb3\\xdb\\x8c\" }}, // fa_IR\n\tDict{{ QLocale::French,                                607,   321'391, \"\\x46\\x72\\x61\\x6e\\xc3\\xa7\\x61\\x69\\x73\" }}, // fr_FR\n\tDict{{ QLocale::Hebrew,                                608,   622'550, \"\\xd7\\xa2\\xd7\\x91\\xd7\\xa8\\xd7\\x99\\xd7\\xaa\" }}, // he_IL\n\tDict{{ QLocale::Hindi,                                 609,    56'105, \"\\xe0\\xa4\\xb9\\xe0\\xa4\\xbf\\xe0\\xa4\\xa8\\xe0\\xa5\\x8d\\xe0\\xa4\\xa6\\xe0\\xa5\\x80\" }}, // hi_IN\n\tDict{{ QLocale::Croatian,                              610,   668'876, \"\\x48\\x72\\x76\\x61\\x74\\x73\\x6b\\x69\" }}, // hr_HR\n\tDict{{ QLocale::Hungarian,                             611,   660'402, \"\\x4d\\x61\\x67\\x79\\x61\\x72\" }}, // hu_HU\n\tDict{{ QLocale::Armenian,                              612,   928'746, \"\\xd5\\x80\\xd5\\xa1\\xd5\\xb5\\xd5\\xa5\\xd6\\x80\\xd5\\xa5\\xd5\\xb6\" }}, // hy_AM\n\tDict{{ QLocale::Indonesian,                            613,   100'134, \"\\x49\\x6e\\x64\\x6f\\x6e\\x65\\x73\\x69\\x61\" }}, // id_ID\n\tDict{{ QLocale::Italian,                               614,   324'613, \"\\x49\\x74\\x61\\x6c\\x69\\x61\\x6e\\x6f\" }}, // it_IT\n\tDict{{ QLocale::Korean,                                615, 1'256'987, \"\\xed\\x95\\x9c\\xea\\xb5\\xad\\xec\\x96\\xb4\" }}, // ko_KR\n\tDict{{ QLocale::Lithuanian,                            616,   267'427, \"\\x4c\\x69\\x65\\x74\\x75\\x76\\x69\\xc5\\xb3\" }}, // lt_LT\n\tDict{{ QLocale::Latvian,                               617,   641'602, \"\\x4c\\x61\\x74\\x76\\x69\\x65\\xc5\\xa1\\x75\" }}, // lv_LV\n\tDict{{ QLocale::NorwegianBokmal,                       618,   588'650, \"\\x4e\\x6f\\x72\\x73\\x6b\" }}, // nb_NO\n\tDict{{ QLocale::Dutch,                                 619,   743'406, \"\\x4e\\x65\\x64\\x65\\x72\\x6c\\x61\\x6e\\x64\\x73\" }}, // nl_NL\n\tDict{{ QLocale::Polish,                                620, 1'015'747, \"\\x50\\x6f\\x6c\\x73\\x6b\\x69\" }}, // pl_PL\n\tDict{{ QLocale::Portuguese,                            621, 1'231'999, \"\\x50\\x6f\\x72\\x74\\x75\\x67\\x75\\xc3\\xaa\\x73 (Brazil)\" }}, // pt_BR\n\tDict{{ LWC(QLocale::Portuguese, QLocale::Portugal),    622,   138'571, \"\\x50\\x6f\\x72\\x74\\x75\\x67\\x75\\xc3\\xaa\\x73\" }}, // pt_PT\n\tDict{{ QLocale::Romanian,                              623,   455'643, \"\\x52\\x6f\\x6d\\xc3\\xa2\\x6e\\xc4\\x83\" }}, // ro_RO\n\tDict{{ QLocale::Russian,                               624,   463'194, \"\\xd0\\xa0\\xd1\\x83\\xd1\\x81\\xd1\\x81\\xd0\\xba\\xd0\\xb8\\xd0\\xb9\" }}, // ru_RU\n\tDict{{ QLocale::Slovak,                                625,   525'328, \"\\x53\\x6c\\x6f\\x76\\x65\\x6e\\xc4\\x8d\\x69\\x6e\\x61\" }}, // sk_SK\n\tDict{{ QLocale::Slovenian,                             626, 1'143'710, \"\\x53\\x6c\\x6f\\x76\\x65\\x6e\\xc5\\xa1\\xc4\\x8d\\x69\\x6e\\x61\" }}, // sl_SI\n\tDict{{ QLocale::Albanian,                              627,   583'412, \"\\x53\\x68\\x71\\x69\\x70\" }}, // sq_AL\n\tDict{{ QLocale::Swedish,                               628,   593'877, \"\\x53\\x76\\x65\\x6e\\x73\\x6b\\x61\" }}, // sv_SE\n\tDict{{ QLocale::Tamil,                                 629,   323'193, \"\\xe0\\xae\\xa4\\xe0\\xae\\xae\\xe0\\xae\\xbf\\xe0\\xae\\xb4\\xe0\\xaf\\x8d\" }}, // ta_IN\n\tDict{{ QLocale::Tajik,                                 630,   369'931, \"\\xd0\\xa2\\xd0\\xbe\\xd2\\xb7\\xd0\\xb8\\xd0\\xba\\xd3\\xa3\" }}, // tg_TG\n\tDict{{ QLocale::Turkish,                               631, 4'301'099, \"\\x54\\xc3\\xbc\\x72\\x6b\\xc3\\xa7\\x65\" }}, // tr_TR\n\tDict{{ QLocale::Ukrainian,                             632,   445'711, \"\\xd0\\xa3\\xd0\\xba\\xd1\\x80\\xd0\\xb0\\xd1\\x97\\xd0\\xbd\\xd1\\x81\\xd1\\x8c\\xd0\\xba\\xd0\\xb0\" }}, // uk_UA\n\tDict{{ QLocale::Vietnamese,                            633,    12'949, \"\\x54\\x69\\xe1\\xba\\xbf\\x6e\\x67\\x20\\x56\\x69\\xe1\\xbb\\x87\\x74\" }}, // vi_VN\n\t// The Tajik code is 'tg_TG' in Chromium, but QT has only 'tg_TJ'.\n};\n\ninline auto IsSupportedLang(int lang) {\n\treturn ranges::contains(kDictionaries, lang, &Dict::id);\n}\n\nvoid EnsurePath() {\n\tif (!QDir::current().mkpath(Spellchecker::DictionariesPath())) {\n\t\tLOG((\"App Error: Could not create dictionaries path.\"));\n\t}\n}\n\nbool IsGoodPartName(const QString &name) {\n\treturn ranges::any_of(kDictExtensions, [&](const auto &ext) {\n\t\treturn name.endsWith(ext);\n\t});\n}\n\nusing DictLoaderPtr = std::shared_ptr<base::unique_qptr<DictLoader>>;\n\nDictLoaderPtr BackgroundLoader;\nrpl::event_stream<int> BackgroundLoaderChanged;\n\nvoid SetBackgroundLoader(DictLoaderPtr loader) {\n\tBackgroundLoader = std::move(loader);\n}\n\nvoid DownloadDictionaryInBackground(\n\t\tnot_null<Main::Session*> session,\n\t\tint counter,\n\t\tstd::vector<int> langs) {\n\tif (counter >= langs.size()) {\n\t\treturn;\n\t}\n\tconst auto id = langs[counter];\n\tcounter++;\n\tconst auto destroyer = [=] {\n\t\tBackgroundLoader = nullptr;\n\t\tBackgroundLoaderChanged.fire(0);\n\n\t\tif (DictionaryExists(id)) {\n\t\t\tauto dicts = Core::App().settings().dictionariesEnabled();\n\t\t\tif (!ranges::contains(dicts, id)) {\n\t\t\t\tdicts.push_back(id);\n\t\t\t\tCore::App().settings().setDictionariesEnabled(std::move(dicts));\n\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t}\n\t\t}\n\n\t\tDownloadDictionaryInBackground(session, counter, langs);\n\t};\n\tif (DictionaryExists(id)) {\n\t\tdestroyer();\n\t\treturn;\n\t}\n\n\tauto sharedLoader = std::make_shared<base::unique_qptr<DictLoader>>();\n\t*sharedLoader = base::make_unique_q<DictLoader>(\n\t\tQCoreApplication::instance(),\n\t\tsession,\n\t\tid,\n\t\tGetDownloadLocation(id),\n\t\tDictPathByLangId(id),\n\t\tGetDownloadSize(id),\n\t\tcrl::guard(session, destroyer));\n\tSetBackgroundLoader(std::move(sharedLoader));\n\tBackgroundLoaderChanged.fire_copy(id);\n}\n\nvoid AddExceptions() {\n\tconst auto exceptions = ranges::views::all(\n\t\tkExceptions\n\t) | ranges::views::transform([](const auto &word) {\n\t\treturn word.utf16();\n\t}) | ranges::views::filter([](const auto &word) {\n\t\treturn !(Platform::Spellchecker::IsWordInDictionary(word)\n\t\t\t|| Spellchecker::IsWordSkippable(word));\n\t}) | ranges::to_vector;\n\n\tranges::for_each(exceptions, Platform::Spellchecker::AddWord);\n}\n\n} // namespace\n\nDictLoaderPtr GlobalLoader() {\n\treturn BackgroundLoader;\n}\n\nrpl::producer<int> GlobalLoaderChanged() {\n\treturn BackgroundLoaderChanged.events();\n}\n\nDictLoader::DictLoader(\n\tQObject *parent,\n\tnot_null<Main::Session*> session,\n\tint id,\n\tMTP::DedicatedLoader::Location location,\n\tconst QString &folder,\n\tint64 size,\n\tFn<void()> destroyCallback)\n: BlobLoader(parent, session, id, location, folder, size)\n, _destroyCallback(std::move(destroyCallback)) {\n}\n\nvoid DictLoader::unpack(const QString &path) {\n\tcrl::async([=] {\n\t\tconst auto success = Spellchecker::UnpackDictionary(path, id());\n\t\tif (success) {\n\t\t\tQFile(path).remove();\n\t\t\tdestroy();\n\t\t\treturn;\n\t\t}\n\t\tcrl::on_main([=] { fail(); });\n\t});\n}\n\nvoid DictLoader::destroy() {\n\tExpects(_destroyCallback);\n\n\tcrl::on_main(_destroyCallback);\n}\n\nvoid DictLoader::fail() {\n\tBlobLoader::fail();\n\tdestroy();\n}\n\nstd::vector<Dict> Dictionaries() {\n\treturn kDictionaries | ranges::to_vector;\n}\n\nint64 GetDownloadSize(int id) {\n\treturn ranges::find(kDictionaries, id, &Spellchecker::Dict::id)->size;\n}\n\nMTP::DedicatedLoader::Location GetDownloadLocation(int id) {\n\tconst auto username = kCloudLocationUsername.utf16();\n\tconst auto i = ranges::find(kDictionaries, id, &Spellchecker::Dict::id);\n\treturn MTP::DedicatedLoader::Location{ username, i->postId };\n}\n\nQString DictPathByLangId(int langId) {\n\tEnsurePath();\n\treturn u\"%1/%2\"_q.arg(\n\t\tDictionariesPath(),\n\t\tSpellchecker::LocaleFromLangId(langId).name());\n}\n\nQString DictionariesPath() {\n\treturn cWorkingDir() + u\"tdata/dictionaries\"_q;\n}\n\nbool UnpackDictionary(const QString &path, int langId) {\n\tconst auto folder = DictPathByLangId(langId);\n\treturn UnpackBlob(path, folder, IsGoodPartName);\n}\n\nbool DictionaryExists(int langId) {\n\tif (!langId) {\n\t\treturn true;\n\t}\n\tconst auto folder = DictPathByLangId(langId) + '/';\n\treturn ranges::none_of(kDictExtensions, [&](const auto &ext) {\n\t\tconst auto name = Spellchecker::LocaleFromLangId(langId).name();\n\t\treturn !QFile(folder + name + '.' + ext).exists();\n\t});\n}\n\nbool RemoveDictionary(int langId) {\n\tif (!langId) {\n\t\treturn true;\n\t}\n\tconst auto fileName = Spellchecker::LocaleFromLangId(langId).name();\n\tconst auto folder = u\"%1/%2/\"_q.arg(\n\t\tDictionariesPath(),\n\t\tfileName);\n\treturn QDir(folder).removeRecursively();\n}\n\nbool WriteDefaultDictionary() {\n\t// This is an unused function.\n\tconst auto en = QLocale::English;\n\tif (DictionaryExists(en)) {\n\t\treturn false;\n\t}\n\tconst auto fileName = QLocale(en).name();\n\tconst auto folder = u\"%1/%2/\"_q.arg(\n\t\tDictionariesPath(),\n\t\tfileName);\n\tQDir(folder).removeRecursively();\n\n\tconst auto path = folder + fileName;\n\tQDir().mkpath(folder);\n\tauto input = QFile(u\":/misc/en_US_dictionary\"_q);\n\tauto output = QFile(path);\n\tif (input.open(QIODevice::ReadOnly)\n\t\t&& output.open(QIODevice::WriteOnly)) {\n\t\toutput.write(input.readAll());\n\t\tconst auto result = Spellchecker::UnpackDictionary(path, en);\n\t\toutput.remove();\n\t\treturn result;\n\t}\n\treturn false;\n}\n\nrpl::producer<QString> ButtonManageDictsState(\n\t\tnot_null<Main::Session*> session) {\n\tif (Platform::Spellchecker::IsSystemSpellchecker()) {\n\t\treturn rpl::single(QString());\n\t}\n\tconst auto computeString = [=] {\n\t\tif (!Core::App().settings().spellcheckerEnabled()) {\n\t\t\treturn QString();\n\t\t}\n\t\tif (!Core::App().settings().dictionariesEnabled().size()) {\n\t\t\treturn QString();\n\t\t}\n\t\tconst auto dicts = Core::App().settings().dictionariesEnabled();\n\t\tconst auto filtered = ranges::views::all(\n\t\t\tdicts\n\t\t) | ranges::views::filter(\n\t\t\tDictionaryExists\n\t\t) | ranges::to_vector;\n\t\tconst auto active = Platform::Spellchecker::ActiveLanguages();\n\n\t\treturn (active.size() == filtered.size())\n\t\t\t? QString::number(filtered.size())\n\t\t\t: tr::lng_contacts_loading(tr::now);\n\t};\n\treturn rpl::single(\n\t\tcomputeString()\n\t) | rpl::then(\n\t\trpl::merge(\n\t\t\tSpellchecker::SupportedScriptsChanged(),\n\t\t\tCore::App().settings().dictionariesEnabledChanges(\n\t\t\t) | rpl::to_empty,\n\t\t\tCore::App().settings().spellcheckerEnabledChanges(\n\t\t\t) | rpl::to_empty\n\t\t) | rpl::map(computeString)\n\t);\n}\n\nstd::vector<int> DefaultLanguages() {\n\tstd::vector<int> langs;\n\n\tconst auto append = [&](const auto loc) {\n\t\tconst auto l = LanguageFromLocale(loc);\n\t\tif (!ranges::contains(langs, l) && IsSupportedLang(l)) {\n\t\t\tlangs.push_back(l);\n\t\t}\n\t};\n\n\tconst auto method = QGuiApplication::inputMethod();\n\tlangs.reserve(method ? 3 : 2);\n\tif (method) {\n\t\tappend(method->locale());\n\t}\n\tappend(QLocale(Platform::SystemLanguage()));\n\tappend(QLocale(Lang::LanguageIdOrDefault(Lang::Id())));\n\n\treturn langs;\n}\n\nvoid Start(not_null<Main::Session*> session) {\n\tSpellchecker::SetPhrases({ {\n\t\t{ &ph::lng_spellchecker_submenu, tr::lng_spellchecker_submenu() },\n\t\t{ &ph::lng_spellchecker_add, tr::lng_spellchecker_add() },\n\t\t{ &ph::lng_spellchecker_remove, tr::lng_spellchecker_remove() },\n\t\t{ &ph::lng_spellchecker_ignore, tr::lng_spellchecker_ignore() },\n\t} });\n\tconst auto settings = &Core::App().settings();\n\tauto &lifetime = session->lifetime();\n\n\tconst auto onEnabled = [=](auto enabled) {\n\t\tPlatform::Spellchecker::UpdateLanguages(\n\t\t\tenabled\n\t\t\t\t? settings->dictionariesEnabled()\n\t\t\t\t: std::vector<int>());\n\t};\n\n\tconst auto guard = gsl::finally([=] {\n\t\tonEnabled(settings->spellcheckerEnabled());\n\t});\n\n\tif (Platform::Spellchecker::IsSystemSpellchecker()) {\n\t\tSpellchecker::SupportedScriptsChanged()\n\t\t| rpl::take(1)\n\t\t| rpl::on_next(AddExceptions, lifetime);\n\n\t\treturn;\n\t}\n\n\tSpellchecker::SupportedScriptsChanged(\n\t) | rpl::on_next(AddExceptions, lifetime);\n\n\tSpellchecker::SetWorkingDirPath(DictionariesPath());\n\n\tsettings->dictionariesEnabledChanges(\n\t) | rpl::on_next([](auto dictionaries) {\n\t\tPlatform::Spellchecker::UpdateLanguages(dictionaries);\n\t}, lifetime);\n\n\tsettings->spellcheckerEnabledChanges(\n\t) | rpl::on_next(onEnabled, lifetime);\n\n\tconst auto method = QGuiApplication::inputMethod();\n\n\tconst auto connectInput = [=] {\n\t\tif (!method || !settings->spellcheckerEnabled()) {\n\t\t\treturn;\n\t\t}\n\t\tauto callback = [=] {\n\t\t\tif (BackgroundLoader) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto l = LanguageFromLocale(method->locale());\n\t\t\tif (!IsSupportedLang(l) || DictionaryExists(l)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcrl::on_main(session, [=] {\n\t\t\t\tDownloadDictionaryInBackground(session, 0, { l });\n\t\t\t});\n\t\t};\n\t\tQObject::connect(\n\t\t\tmethod,\n\t\t\t&QInputMethod::localeChanged,\n\t\t\tstd::move(callback));\n\t};\n\n\tif (settings->autoDownloadDictionaries()) {\n\t\tsession->data().contactsLoaded().changes(\n\t\t) | rpl::on_next([=](bool loaded) {\n\t\t\tif (!loaded) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tDownloadDictionaryInBackground(session, 0, DefaultLanguages());\n\t\t}, lifetime);\n\n\t\tconnectInput();\n\t}\n\n\tconst auto disconnect = [=] {\n\t\tQObject::disconnect(\n\t\t\tmethod,\n\t\t\t&QInputMethod::localeChanged,\n\t\t\tnullptr,\n\t\t\tnullptr);\n\t};\n\tlifetime.add([=] {\n\t\tdisconnect();\n\t\tfor (auto &[index, account] : session->domain().accounts()) {\n\t\t\tif (const auto anotherSession = account->maybeSession()) {\n\t\t\t\tif (anotherSession->uniqueId() != session->uniqueId()) {\n\t\t\t\t\tSpellchecker::Start(anotherSession);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\n\trpl::combine(\n\t\tsettings->spellcheckerEnabledValue(),\n\t\tsettings->autoDownloadDictionariesValue()\n\t) | rpl::on_next([=](bool spell, bool download) {\n\t\tif (spell && download) {\n\t\t\tconnectInput();\n\t\t\treturn;\n\t\t}\n\t\tdisconnect();\n\t}, lifetime);\n\n}\n\n} // namespace Spellchecker\n\n#endif // !TDESKTOP_DISABLE_SPELLCHECK\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/spellchecker_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#ifndef TDESKTOP_DISABLE_SPELLCHECK\n\n#include \"storage/storage_cloud_blob.h\"\n#include \"base/unique_qptr.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Spellchecker {\n\nstruct Dict : public Storage::CloudBlob::Blob {\n};\n\nint64 GetDownloadSize(int id);\nMTP::DedicatedLoader::Location GetDownloadLocation(int id);\n\n[[nodiscard]] QString DictionariesPath();\n[[nodiscard]] QString DictPathByLangId(int langId);\nbool UnpackDictionary(const QString &path, int langId);\n[[nodiscard]] bool DictionaryExists(int langId);\nbool RemoveDictionary(int langId);\n[[nodiscard]] bool IsEn(int langId);\n\nbool WriteDefaultDictionary();\nstd::vector<Dict> Dictionaries();\n\nvoid Start(not_null<Main::Session*> session);\n[[nodiscard]] rpl::producer<QString> ButtonManageDictsState(\n\tnot_null<Main::Session*> session);\n\nstd::vector<int> DefaultLanguages();\n\nclass DictLoader : public Storage::CloudBlob::BlobLoader {\npublic:\n\tDictLoader(\n\t\tQObject *parent,\n\t\tnot_null<Main::Session*> session,\n\t\tint id,\n\t\tMTP::DedicatedLoader::Location location,\n\t\tconst QString &folder,\n\t\tint64 size,\n\t\tFn<void()> destroyCallback);\n\n\tvoid destroy() override;\n\n\trpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tvoid unpack(const QString &path) override;\n\tvoid fail() override;\n\n\t// Be sure to always call it in the main thread.\n\tFn<void()> _destroyCallback;\n\n\trpl::lifetime _lifetime;\n\n};\n\nstd::shared_ptr<base::unique_qptr<DictLoader>> GlobalLoader();\nrpl::producer<int> GlobalLoaderChanged();\n\n} // namespace Spellchecker\n\n#endif // !TDESKTOP_DISABLE_SPELLCHECK\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/stickers_dice_pack.h\"\n\n#include \"main/main_session.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"base/unixtime.h\"\n#include \"apiwrap.h\"\n\n#include <QtCore/QFile>\n#include <QtCore/QFileInfo>\n\nnamespace Stickers {\n\nconst QString DicePacks::kDiceString = QString::fromUtf8(\"\\xF0\\x9F\\x8E\\xB2\");\nconst QString DicePacks::kDartString = QString::fromUtf8(\"\\xF0\\x9F\\x8E\\xAF\");\nconst QString DicePacks::kSlotString = QString::fromUtf8(\"\\xF0\\x9F\\x8E\\xB0\");\nconst QString DicePacks::kFballString = QString::fromUtf8(\"\\xE2\\x9A\\xBD\");\nconst QString DicePacks::kBballString = QString::fromUtf8(\"\\xF0\\x9F\\x8F\\x80\");\nconst QString DicePacks::kPartyPopper = QString::fromUtf8(\"\\xf0\\x9f\\x8e\\x89\");\n\nDicePack::DicePack(not_null<Main::Session*> session, const QString &emoji)\n: _session(session)\n, _emoji(emoji) {\n}\n\nDicePack::~DicePack() = default;\n\nDocumentData *DicePack::lookup(int value) {\n\tif (!_requestId && _emoji != DicePacks::kPartyPopper) {\n\t\tload();\n\t}\n\ttryGenerateLocalZero();\n\tconst auto i = _map.find(value);\n\treturn (i != end(_map)) ? i->second.get() : nullptr;\n}\n\nvoid DicePack::load() {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\t_requestId = _session->api().request(MTPmessages_GetStickerSet(\n\t\tMTP_inputStickerSetDice(MTP_string(_emoji)),\n\t\tMTP_int(0) // hash\n\t)).done([=](const MTPmessages_StickerSet &result) {\n\t\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\tapplySet(data);\n\t\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t\t});\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t}).send();\n}\n\nvoid DicePack::applySet(const MTPDmessages_stickerSet &data) {\n\tconst auto isSlotMachine = DicePacks::IsSlot(_emoji);\n\tauto index = 0;\n\tauto documents = base::flat_map<DocumentId, not_null<DocumentData*>>();\n\tfor (const auto &sticker : data.vdocuments().v) {\n\t\tconst auto document = _session->data().processDocument(sticker);\n\t\tif (document->sticker()) {\n\t\t\tif (isSlotMachine) {\n\t\t\t\t_map.emplace(index++, document);\n\t\t\t} else {\n\t\t\t\tdocuments.emplace(document->id, document);\n\t\t\t}\n\t\t}\n\t}\n\tif (isSlotMachine) {\n\t\treturn;\n\t}\n\tfor (const auto &pack : data.vpacks().v) {\n\t\tpack.match([&](const MTPDstickerPack &data) {\n\t\t\tconst auto emoji = qs(data.vemoticon());\n\t\t\tif (emoji.isEmpty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto ch = int(emoji[0].unicode());\n\t\t\tconst auto index = (ch == '#') ? 0 : (ch + 1 - '1');\n\t\t\tif (index < 0 || index > 6) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfor (const auto &id : data.vdocuments().v) {\n\t\t\t\tif (const auto document = documents.take(id.v)) {\n\t\t\t\t\t_map.emplace(index, *document);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n}\n\nvoid DicePack::tryGenerateLocalZero() {\n\tif (!_map.empty()) {\n\t\treturn;\n\t}\n\n\tconst auto generateLocal = [&](int index, const QString &name) {\n\t\t_map.emplace(\n\t\t\tindex,\n\t\t\tChatHelpers::GenerateLocalTgsSticker(_session, name));\n\t};\n\tif (_emoji == DicePacks::kDiceString) {\n\t\tgenerateLocal(0, u\"dice_idle\"_q);\n\t} else if (_emoji == DicePacks::kDartString) {\n\t\tgenerateLocal(0, u\"dart_idle\"_q);\n\t} else if (_emoji == DicePacks::kBballString) {\n\t\tgenerateLocal(0, u\"bball_idle\"_q);\n\t} else if (_emoji == DicePacks::kFballString) {\n\t\tgenerateLocal(0, u\"fball_idle\"_q);\n\t} else if (_emoji == DicePacks::kSlotString) {\n\t\tgenerateLocal(0, u\"slot_back\"_q);\n\t\tgenerateLocal(2, u\"slot_pull\"_q);\n\t\tgenerateLocal(8, u\"slot_0_idle\"_q);\n\t\tgenerateLocal(14, u\"slot_1_idle\"_q);\n\t\tgenerateLocal(20, u\"slot_2_idle\"_q);\n\t} else if (_emoji == DicePacks::kPartyPopper) {\n\t\tgenerateLocal(0, u\"winners\"_q);\n\t}\n}\n\nDicePacks::DicePacks(not_null<Main::Session*> session)\n: _session(session) {\n}\n\nDocumentData *DicePacks::lookup(const QString &emoji, int value) {\n\tconst auto key = emoji.endsWith(QChar(0xFE0F))\n\t\t? emoji.mid(0, emoji.size() - 1)\n\t\t: emoji;\n\tconst auto i = _packs.find(key);\n\tif (i != end(_packs)) {\n\t\treturn i->second->lookup(value);\n\t}\n\treturn _packs.emplace(\n\t\tkey,\n\t\tstd::make_unique<DicePack>(_session, key)\n\t).first->second->lookup(value);\n}\n\nvoid DicePacks::resolveGameOptions(\n\t\tFn<void(const Data::DiceGameOptions &)> done) {\n\t_resolveGameOptionsCallback = std::move(done);\n\tif (_resolveGameOptionsRequestId) {\n\t\treturn;\n\t}\n\n\t_resolveGameOptionsRequestId = _session->api().request(\n\t\tMTPmessages_GetEmojiGameInfo()\n\t).done([=](const MTPmessages_EmojiGameInfo &result) {\n\t\t_resolveGameOptionsRequestId = 0;\n\t\tif (const auto onstack = base::take(_resolveGameOptionsCallback)) {\n\t\t\tonstack(result.match([&](\n\t\t\t\t\tconst MTPDmessages_emojiGameUnavailable &) {\n\t\t\t\treturn Data::DiceGameOptions();\n\t\t\t}, [&](const MTPDmessages_emojiGameDiceInfo &data) {\n\t\t\t\tauto jackpot = 0;\n\t\t\t\tauto rewards = std::array<int, 6>{};\n\t\t\t\tconst auto &params = data.vparams().v;\n\t\t\t\tconst auto count = int(params.size());\n\t\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\t\tif (i < 6) {\n\t\t\t\t\t\trewards[i] = params[i].v;\n\t\t\t\t\t} else if (i == 6) {\n\t\t\t\t\t\tjackpot = params[i].v;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn Data::DiceGameOptions{\n\t\t\t\t\t.seedHash = data.vgame_hash().v,\n\t\t\t\t\t.previousSteakNanoTon = int64(data.vprev_stake().v),\n\t\t\t\t\t.milliRewards = rewards,\n\t\t\t\t\t.jackpotMilliReward = jackpot,\n\t\t\t\t\t.currentStreak = data.vcurrent_streak().v,\n\t\t\t\t\t.playsLeft = data.vplays_left().value_or_empty(),\n\t\t\t\t};\n\t\t\t}));\n\t\t}\n\t}).fail([=] {\n\t\t_resolveGameOptionsRequestId = 0;\n\t\tif (const auto onstack = base::take(_resolveGameOptionsCallback)) {\n\t\t\tonstack({});\n\t\t}\n\t}).send();\n}\n\nvoid DicePacks::apply(const MTPDupdateEmojiGameInfo &update) {\n}\n\n} // namespace Stickers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/stickers_dice_pack.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass DocumentData;\n\nnamespace Data {\nstruct DiceGameOptions;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Stickers {\n\nclass DicePack final {\npublic:\n\tDicePack(not_null<Main::Session*> session, const QString &emoji);\n\t~DicePack();\n\n\t[[nodiscard]] DocumentData *lookup(int value);\n\nprivate:\n\tvoid load();\n\tvoid applySet(const MTPDmessages_stickerSet &data);\n\tvoid tryGenerateLocalZero();\n\n\tconst not_null<Main::Session*> _session;\n\tQString _emoji;\n\tbase::flat_map<int, not_null<DocumentData*>> _map;\n\tmtpRequestId _requestId = 0;\n\n};\n\nclass DicePacks final {\npublic:\n\texplicit DicePacks(not_null<Main::Session*> session);\n\n\tstatic const QString kDiceString;\n\tstatic const QString kDartString;\n\tstatic const QString kSlotString;\n\tstatic const QString kFballString;\n\tstatic const QString kBballString;\n\tstatic const QString kPartyPopper;\n\n\t[[nodiscard]] static bool IsSlot(const QString &emoji) {\n\t\treturn (emoji == kSlotString);\n\t}\n\n\t[[nodiscard]] DocumentData *lookup(const QString &emoji, int value);\n\n\tvoid resolveGameOptions(Fn<void(const Data::DiceGameOptions &)> done);\n\tvoid apply(const MTPDupdateEmojiGameInfo &update);\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\n\tbase::flat_map<QString, std::unique_ptr<DicePack>> _packs;\n\n\tmtpRequestId _resolveGameOptionsRequestId = 0;\n\tFn<void(const Data::DiceGameOptions &)> _resolveGameOptionsCallback;\n\n};\n\n} // namespace Stickers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/stickers_emoji_image_loader.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/stickers_emoji_image_loader.h\"\n\n#include \"styles/style_chat.h\"\n\n#include <QtCore/QtMath>\n\nnamespace Stickers {\n\nEmojiImageLoader::EmojiImageLoader(crl::weak_on_queue<EmojiImageLoader> weak)\n: _weak(std::move(weak)) {\n}\n\nvoid EmojiImageLoader::init(\n\t\tstd::shared_ptr<UniversalImages> images,\n\t\tbool largeEnabled) {\n\tExpects(images != nullptr);\n\n\t_images = std::move(images);\n\tif (largeEnabled) {\n\t\t_images->ensureLoaded();\n\t}\n}\n\nQImage EmojiImageLoader::prepare(EmojiPtr emoji) const {\n\tconst auto loaded = _images->ensureLoaded();\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto side = st::largeEmojiSize + 2 * st::largeEmojiOutline;\n\tauto tinted = QImage(\n\t\tQSize(st::largeEmojiSize, st::largeEmojiSize) * factor,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\ttinted.fill(Qt::white);\n\tif (loaded) {\n\t\tQPainter p(&tinted);\n\t\tp.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\t_images->draw(\n\t\t\tp,\n\t\t\temoji,\n\t\t\tst::largeEmojiSize * factor,\n\t\t\t0,\n\t\t\t0);\n\t}\n\tauto result = QImage(\n\t\tQSize(side, side) * factor,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::transparent);\n\tif (loaded) {\n\t\tQPainter p(&result);\n\t\tconst auto delta = st::largeEmojiOutline * factor;\n\t\tconst auto planar = std::array<QPoint, 4>{ {\n\t\t\t{ 0, -1 },\n\t\t\t{ -1, 0 },\n\t\t\t{ 1, 0 },\n\t\t\t{ 0, 1 },\n\t\t} };\n\t\tfor (const auto &shift : planar) {\n\t\t\tfor (auto i = 0; i != delta; ++i) {\n\t\t\t\tp.drawImage(QPoint(delta, delta) + shift * (i + 1), tinted);\n\t\t\t}\n\t\t}\n\t\tconst auto diagonal = std::array<QPoint, 4>{ {\n\t\t\t{ -1, -1 },\n\t\t\t{ 1, -1 },\n\t\t\t{ -1, 1 },\n\t\t\t{ 1, 1 },\n\t\t} };\n\t\tconst auto corrected = int(base::SafeRound(delta / M_SQRT2));\n\t\tfor (const auto &shift : diagonal) {\n\t\t\tfor (auto i = 0; i != corrected; ++i) {\n\t\t\t\tp.drawImage(QPoint(delta, delta) + shift * (i + 1), tinted);\n\t\t\t}\n\t\t}\n\t\t_images->draw(\n\t\t\tp,\n\t\t\temoji,\n\t\t\tst::largeEmojiSize * factor,\n\t\t\tdelta,\n\t\t\tdelta);\n\t}\n\treturn result;\n}\n\nvoid EmojiImageLoader::switchTo(std::shared_ptr<UniversalImages> images) {\n\t_images = std::move(images);\n}\n\nauto EmojiImageLoader::releaseImages() -> std::shared_ptr<UniversalImages> {\n\treturn std::exchange(\n\t\t_images,\n\t\tstd::make_shared<UniversalImages>(_images->id()));\n}\n\n} // namespace Stickers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/stickers_emoji_image_loader.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <crl/crl_object_on_queue.h>\n#include \"ui/emoji_config.h\"\n\nnamespace Stickers {\n\nclass EmojiImageLoader {\npublic:\n\tusing UniversalImages = Ui::Emoji::UniversalImages;\n\n\texplicit EmojiImageLoader(crl::weak_on_queue<EmojiImageLoader> weak);\n\n\tvoid init(\n\t\tstd::shared_ptr<UniversalImages> images,\n\t\tbool largeEnabled);\n\n\t[[nodiscard]] QImage prepare(EmojiPtr emoji) const;\n\tvoid switchTo(std::shared_ptr<UniversalImages> images);\n\tstd::shared_ptr<UniversalImages> releaseImages();\n\nprivate:\n\tcrl::weak_on_queue<EmojiImageLoader> _weak;\n\tstd::shared_ptr<UniversalImages> _images;\n\n};\n\n} // namespace Stickers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/stickers_emoji_pack.h\"\n\n#include \"chat_helpers/stickers_emoji_image_loader.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"lottie/lottie_common.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/text/text_isolated_emoji.h\"\n#include \"ui/image/image.h\"\n#include \"ui/rect.h\"\n#include \"main/main_session.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"core/core_settings.h\"\n#include \"core/application.h\"\n#include \"base/call_delayed.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_chat.h\"\n\n#include <QtCore/QBuffer>\n\nnamespace Stickers {\nnamespace {\n\nconstexpr auto kRefreshTimeout = 7200 * crl::time(1000);\nconstexpr auto kEmojiCachesCount = 4;\nconstexpr auto kPremiumCachesCount = 8;\n\n[[nodiscard]] std::optional<int> IndexFromEmoticon(const QString &emoticon) {\n\tif (emoticon.size() < 2) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto first = emoticon[0].unicode();\n\treturn (first >= '1' && first <= '9')\n\t\t? std::make_optional(first - '1')\n\t\t: (first == 55357 && emoticon[1].unicode() == 56607)\n\t\t? std::make_optional(9)\n\t\t: std::nullopt;\n}\n\n[[nodiscard]] QSize SingleSize() {\n\tconst auto single = st::largeEmojiSize;\n\tconst auto outline = st::largeEmojiOutline;\n\treturn Size(2 * outline + single) * style::DevicePixelRatio();\n}\n\n[[nodiscard]] const Lottie::ColorReplacements *ColorReplacements(int index) {\n\tExpects(index >= 1 && index <= 5);\n\n\tstatic const auto color1 = Lottie::ColorReplacements{\n\t\t.modifier = Lottie::SkinModifier::Color1,\n\t\t.tag = 1,\n\t};\n\tstatic const auto color2 = Lottie::ColorReplacements{\n\t\t.modifier = Lottie::SkinModifier::Color2,\n\t\t.tag = 2,\n\t};\n\tstatic const auto color3 = Lottie::ColorReplacements{\n\t\t.modifier = Lottie::SkinModifier::Color3,\n\t\t.tag = 3,\n\t};\n\tstatic const auto color4 = Lottie::ColorReplacements{\n\t\t.modifier = Lottie::SkinModifier::Color4,\n\t\t.tag = 4,\n\t};\n\tstatic const auto color5 = Lottie::ColorReplacements{\n\t\t.modifier = Lottie::SkinModifier::Color5,\n\t\t.tag = 5,\n\t};\n\tstatic const auto list = std::array{\n\t\t&color1,\n\t\t&color2,\n\t\t&color3,\n\t\t&color4,\n\t\t&color5,\n\t};\n\treturn list[index - 1];\n}\n\n} // namespace\n\nQSize LargeEmojiImage::Size() {\n\treturn SingleSize();\n}\n\nEmojiPack::EmojiPack(not_null<Main::Session*> session)\n: _session(session) {\n\trefresh();\n\n\tsession->data().viewRemoved(\n\t) | rpl::filter([](not_null<const ViewElement*> view) {\n\t\treturn view->isIsolatedEmoji() || view->isOnlyCustomEmoji();\n\t}) | rpl::on_next([=](not_null<const ViewElement*> item) {\n\t\tremove(item);\n\t}, _lifetime);\n\n\tCore::App().settings().largeEmojiChanges(\n\t) | rpl::on_next([=](bool large) {\n\t\trefreshAll();\n\t}, _lifetime);\n\n\tUi::Emoji::Updated(\n\t) | rpl::on_next([=] {\n\t\t_images.clear();\n\t\trefreshAll();\n\t}, _lifetime);\n}\n\nEmojiPack::~EmojiPack() = default;\n\nbool EmojiPack::add(not_null<ViewElement*> view) {\n\tif (const auto custom = view->onlyCustomEmoji()) {\n\t\t_onlyCustomItems.emplace(view);\n\t\treturn true;\n\t} else if (const auto emoji = view->isolatedEmoji()) {\n\t\t_items[emoji].emplace(view);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid EmojiPack::remove(not_null<const ViewElement*> view) {\n\tExpects(view->isIsolatedEmoji() || view->isOnlyCustomEmoji());\n\n\tif (view->isOnlyCustomEmoji()) {\n\t\t_onlyCustomItems.remove(view);\n\t} else if (const auto emoji = view->isolatedEmoji()) {\n\t\tconst auto i = _items.find(emoji);\n\t\tAssert(i != end(_items));\n\t\tconst auto j = i->second.find(view);\n\t\tAssert(j != end(i->second));\n\t\ti->second.erase(j);\n\t\tif (i->second.empty()) {\n\t\t\t_items.erase(i);\n\t\t}\n\t}\n}\n\nauto EmojiPack::stickerForEmoji(EmojiPtr emoji) -> Sticker {\n\tExpects(emoji != nullptr);\n\n\tconst auto i = _map.find(emoji);\n\tif (i != end(_map)) {\n\t\treturn { i->second.get(), nullptr };\n\t}\n\tif (!emoji->colored()) {\n\t\treturn {};\n\t}\n\tconst auto j = _map.find(emoji->original());\n\tif (j != end(_map)) {\n\t\tconst auto index = emoji->variantIndex(emoji);\n\t\treturn { j->second.get(), ColorReplacements(index) };\n\t}\n\treturn {};\n}\n\nauto EmojiPack::stickerForEmoji(const IsolatedEmoji &emoji) -> Sticker {\n\tExpects(!emoji.empty());\n\n\tif (!v::is_null(emoji.items[1])) {\n\t\treturn {};\n\t} else if (const auto regular = std::get_if<EmojiPtr>(&emoji.items[0])) {\n\t\treturn stickerForEmoji(*regular);\n\t}\n\treturn {};\n}\n\nstd::shared_ptr<LargeEmojiImage> EmojiPack::image(EmojiPtr emoji) {\n\tconst auto i = _images.emplace(\n\t\temoji,\n\t\tstd::weak_ptr<LargeEmojiImage>()).first;\n\tif (const auto result = i->second.lock()) {\n\t\treturn result;\n\t}\n\tauto result = std::make_shared<LargeEmojiImage>();\n\tconst auto raw = result.get();\n\tconst auto weak = base::make_weak(_session);\n\traw->load = [=] {\n\t\tCore::App().emojiImageLoader().with([=](\n\t\t\t\tconst EmojiImageLoader &loader) {\n\t\t\tcrl::on_main(weak, [\n\t\t\t\t=,\n\t\t\t\timage = loader.prepare(emoji)\n\t\t\t]() mutable {\n\t\t\t\tconst auto i = _images.find(emoji);\n\t\t\t\tif (i != end(_images)) {\n\t\t\t\t\tif (const auto strong = i->second.lock()) {\n\t\t\t\t\t\tif (!strong->image) {\n\t\t\t\t\t\t\tstrong->load = nullptr;\n\t\t\t\t\t\t\tstrong->image.emplace(std::move(image));\n\t\t\t\t\t\t\t_session->notifyDownloaderTaskFinished();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t\traw->load = nullptr;\n\t};\n\ti->second = result;\n\treturn result;\n}\n\nEmojiPtr EmojiPack::chooseInteractionEmoji(\n\t\tnot_null<HistoryItem*> item) const {\n\treturn chooseInteractionEmoji(item->originalText().text);\n}\n\nEmojiPtr EmojiPack::chooseInteractionEmoji(\n\t\tconst QString &emoticon) const {\n\tconst auto emoji = Ui::Emoji::Find(emoticon);\n\tif (!emoji) {\n\t\treturn nullptr;\n\t}\n\tif (!animationsForEmoji(emoji).empty()) {\n\t\treturn emoji;\n\t}\n\tif (const auto original = emoji->original(); original != emoji) {\n\t\tif (!animationsForEmoji(original).empty()) {\n\t\t\treturn original;\n\t\t}\n\t}\n\tstatic const auto kHearts = {\n\t\tQString::fromUtf8(\"\\xf0\\x9f\\x92\\x9b\"),\n\t\tQString::fromUtf8(\"\\xf0\\x9f\\x92\\x99\"),\n\t\tQString::fromUtf8(\"\\xf0\\x9f\\x92\\x9a\"),\n\t\tQString::fromUtf8(\"\\xf0\\x9f\\x92\\x9c\"),\n\t\tQString::fromUtf8(\"\\xf0\\x9f\\xa7\\xa1\"),\n\t\tQString::fromUtf8(\"\\xf0\\x9f\\x96\\xa4\"),\n\t\tQString::fromUtf8(\"\\xf0\\x9f\\xa4\\x8e\"),\n\t\tQString::fromUtf8(\"\\xf0\\x9f\\xa4\\x8d\"),\n\t};\n\treturn ranges::contains(kHearts, emoji->id())\n\t\t? Ui::Emoji::Find(QString::fromUtf8(\"\\xe2\\x9d\\xa4\"))\n\t\t: emoji;\n}\n\nauto EmojiPack::animationsForEmoji(EmojiPtr emoji) const\n-> const base::flat_map<int, not_null<DocumentData*>> & {\n\tstatic const auto empty = base::flat_map<int, not_null<DocumentData*>>();\n\tif (!emoji) {\n\t\treturn empty;\n\t}\n\tconst auto i = _animations.find(emoji);\n\treturn (i != end(_animations)) ? i->second : empty;\n}\n\nbool EmojiPack::hasAnimationsFor(not_null<HistoryItem*> item) const {\n\treturn !animationsForEmoji(chooseInteractionEmoji(item)).empty();\n}\n\nbool EmojiPack::hasAnimationsFor(const QString &emoticon) const {\n\treturn !animationsForEmoji(chooseInteractionEmoji(emoticon)).empty();\n}\n\nstd::unique_ptr<Lottie::SinglePlayer> EmojiPack::effectPlayer(\n\t\tnot_null<DocumentData*> document,\n\t\tQByteArray data,\n\t\tQString filepath,\n\t\tEffectType type) {\n\t// Shortened copy from stickers_lottie module.\n\tconst auto baseKey = document->bigFileBaseCacheKey();\n\tconst auto tag = uint8(type);\n\tconst auto keyShift = ((tag << 4) & 0xF0)\n\t\t| (uint8(ChatHelpers::StickerLottieSize::EmojiInteraction) & 0x0F);\n\tconst auto key = Storage::Cache::Key{\n\t\tbaseKey.high,\n\t\tbaseKey.low + keyShift\n\t};\n\tconst auto get = [=](int i, FnMut<void(QByteArray &&cached)> handler) {\n\t\tdocument->owner().cacheBigFile().get(\n\t\t\t{ key.high, key.low + i },\n\t\t\tstd::move(handler));\n\t};\n\tconst auto weak = base::make_weak(&document->session());\n\tconst auto put = [=](int i, QByteArray &&cached) {\n\t\tcrl::on_main(weak, [=, data = std::move(cached)]() mutable {\n\t\t\tweak->data().cacheBigFile().put(\n\t\t\t\t{ key.high, key.low + i },\n\t\t\t\tstd::move(data));\n\t\t});\n\t};\n\tconst auto size = (type == EffectType::PremiumSticker)\n\t\t? HistoryView::Sticker::PremiumEffectSize(document)\n\t\t: (type == EffectType::EmojiInteraction)\n\t\t? HistoryView::Sticker::EmojiEffectSize()\n\t\t: HistoryView::Sticker::MessageEffectSize();\n\tconst auto request = Lottie::FrameRequest{\n\t\tsize * style::DevicePixelRatio(),\n\t};\n\tauto &weakProvider = _sharedProviders[{ document, type }];\n\tauto shared = [&] {\n\t\tif (const auto result = weakProvider.lock()) {\n\t\t\treturn result;\n\t\t}\n\t\tconst auto count = (type == EffectType::PremiumSticker)\n\t\t\t? kPremiumCachesCount\n\t\t\t: kEmojiCachesCount;\n\t\tconst auto result = Lottie::SinglePlayer::SharedProvider(\n\t\t\tcount,\n\t\t\tget,\n\t\t\tput,\n\t\t\tLottie::ReadContent(data, filepath),\n\t\t\trequest,\n\t\t\tLottie::Quality::High);\n\t\tweakProvider = result;\n\t\treturn result;\n\t}();\n\treturn std::make_unique<Lottie::SinglePlayer>(std::move(shared), request);\n}\n\nvoid EmojiPack::refresh() {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\t_requestId = _session->api().request(MTPmessages_GetStickerSet(\n\t\tMTP_inputStickerSetAnimatedEmoji(),\n\t\tMTP_int(0) // hash\n\t)).done([=](const MTPmessages_StickerSet &result) {\n\t\t_requestId = 0;\n\t\trefreshAnimations();\n\t\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\tapplySet(data);\n\t\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\t_requestId = 0;\n\t\trefreshDelayed();\n\t}).send();\n}\n\nvoid EmojiPack::refreshAnimations() {\n\tif (_animationsRequestId) {\n\t\treturn;\n\t}\n\t_animationsRequestId = _session->api().request(MTPmessages_GetStickerSet(\n\t\tMTP_inputStickerSetAnimatedEmojiAnimations(),\n\t\tMTP_int(0) // hash\n\t)).done([=](const MTPmessages_StickerSet &result) {\n\t\t_animationsRequestId = 0;\n\t\trefreshDelayed();\n\t\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\tapplyAnimationsSet(data);\n\t\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t\t});\n\t}).fail([=] {\n\t\t_animationsRequestId = 0;\n\t\trefreshDelayed();\n\t}).send();\n}\n\nvoid EmojiPack::applySet(const MTPDmessages_stickerSet &data) {\n\tconst auto stickers = collectStickers(data.vdocuments().v);\n\tauto was = base::take(_map);\n\n\tfor (const auto &pack : data.vpacks().v) {\n\t\tpack.match([&](const MTPDstickerPack &data) {\n\t\t\tapplyPack(data, stickers);\n\t\t});\n\t}\n\n\tfor (const auto &[emoji, document] : _map) {\n\t\tconst auto i = was.find(emoji);\n\t\tif (i == end(was)) {\n\t\t\trefreshItems(emoji);\n\t\t} else {\n\t\t\tif (i->second != document) {\n\t\t\t\trefreshItems(i->first);\n\t\t\t}\n\t\t\twas.erase(i);\n\t\t}\n\t}\n\tfor (const auto &[emoji, document] : was) {\n\t\trefreshItems(emoji);\n\t}\n\t_refreshed.fire({});\n}\n\nvoid EmojiPack::applyAnimationsSet(const MTPDmessages_stickerSet &data) {\n\tconst auto stickers = collectStickers(data.vdocuments().v);\n\tconst auto &packs = data.vpacks().v;\n\tconst auto indices = collectAnimationsIndices(packs);\n\n\t_animations.clear();\n\tfor (const auto &pack : packs) {\n\t\tpack.match([&](const MTPDstickerPack &data) {\n\t\t\tconst auto emoticon = qs(data.vemoticon());\n\t\t\tif (IndexFromEmoticon(emoticon).has_value()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto emoji = Ui::Emoji::Find(emoticon);\n\t\t\tif (!emoji) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfor (const auto &id : data.vdocuments().v) {\n\t\t\t\tconst auto i = indices.find(id.v);\n\t\t\t\tif (i == end(indices)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto j = stickers.find(id.v);\n\t\t\t\tif (j == end(stickers)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tfor (const auto index : i->second) {\n\t\t\t\t\t_animations[emoji].emplace(index, j->second);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\t++_animationsVersion;\n}\n\nauto EmojiPack::collectAnimationsIndices(\n\tconst QVector<MTPStickerPack> &packs\n) const -> base::flat_map<uint64, base::flat_set<int>> {\n\tauto result = base::flat_map<uint64, base::flat_set<int>>();\n\tfor (const auto &pack : packs) {\n\t\tpack.match([&](const MTPDstickerPack &data) {\n\t\t\tif (const auto index = IndexFromEmoticon(qs(data.vemoticon()))) {\n\t\t\t\tfor (const auto &id : data.vdocuments().v) {\n\t\t\t\t\tresult[id.v].emplace(*index);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\treturn result;\n}\n\nvoid EmojiPack::refreshAll() {\n\tauto items = base::flat_set<not_null<HistoryItem*>>();\n\tauto count = 0;\n\tfor (const auto &[emoji, list] : _items) {\n\t\t// refreshItems(list); // This call changes _items!\n\t\tcount += int(list.size());\n\t}\n\titems.reserve(count);\n\tfor (const auto &[emoji, list] : _items) {\n\t\t// refreshItems(list); // This call changes _items!\n\t\tfor (const auto &view : list) {\n\t\t\titems.emplace(view->data());\n\t\t}\n\t}\n\trefreshItems(items);\n\trefreshItems(_onlyCustomItems);\n}\n\nvoid EmojiPack::refreshItems(EmojiPtr emoji) {\n\tconst auto i = _items.find(IsolatedEmoji{ { emoji } });\n\tif (!emoji->colored()) {\n\t\tif (const auto count = emoji->variantsCount()) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\trefreshItems(emoji->variant(i + 1));\n\t\t\t}\n\t\t}\n\t}\n\tif (i == end(_items)) {\n\t\treturn;\n\t}\n\trefreshItems(i->second);\n}\n\nvoid EmojiPack::refreshItems(\n\t\tconst base::flat_set<not_null<ViewElement*>> &list) {\n\tauto items = base::flat_set<not_null<HistoryItem*>>();\n\titems.reserve(list.size());\n\tfor (const auto &view : list) {\n\t\titems.emplace(view->data());\n\t}\n\trefreshItems(items);\n}\n\nvoid EmojiPack::refreshItems(\n\t\tconst base::flat_set<not_null<HistoryItem*>> &items) {\n\tfor (const auto &item : items) {\n\t\t_session->data().requestItemViewRefresh(item);\n\t}\n}\n\nvoid EmojiPack::applyPack(\n\t\tconst MTPDstickerPack &data,\n\t\tconst base::flat_map<uint64, not_null<DocumentData*>> &map) {\n\tconst auto emoji = [&] {\n\t\treturn Ui::Emoji::Find(qs(data.vemoticon()));\n\t}();\n\tconst auto document = [&]() -> DocumentData * {\n\t\tfor (const auto &id : data.vdocuments().v) {\n\t\t\tconst auto i = map.find(id.v);\n\t\t\tif (i != end(map)) {\n\t\t\t\treturn i->second.get();\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t}();\n\tif (emoji && document) {\n\t\t_map.emplace_or_assign(emoji, document);\n\t}\n}\n\nbase::flat_map<uint64, not_null<DocumentData*>> EmojiPack::collectStickers(\n\t\tconst QVector<MTPDocument> &list) const {\n\tauto result = base::flat_map<uint64, not_null<DocumentData*>>();\n\tfor (const auto &sticker : list) {\n\t\tconst auto document = _session->data().processDocument(\n\t\t\tsticker);\n\t\tif (document->sticker()) {\n\t\t\tresult.emplace(document->id, document);\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid EmojiPack::refreshDelayed() {\n\tbase::call_delayed(kRefreshTimeout, _session, [=] {\n\t\trefresh();\n\t});\n}\n\n} // namespace Stickers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/text/text_isolated_emoji.h\"\n#include \"ui/image/image.h\"\n#include \"base/timer.h\"\n\n#include <crl/crl_object_on_queue.h>\n\nclass HistoryItem;\nclass DocumentData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Lottie {\nclass SinglePlayer;\nclass FrameProvider;\nstruct ColorReplacements;\n} // namespace Lottie\n\nnamespace Ui {\nnamespace Text {\nclass String;\n} // namespace Text\nnamespace Emoji {\nclass UniversalImages;\n} // namespace Emoji\n} // namespace Ui\n\nnamespace HistoryView {\nclass Element;\n} // namespace HistoryView\n\nnamespace Stickers {\n\nusing IsolatedEmoji = Ui::Text::IsolatedEmoji;\n\nstruct LargeEmojiImage {\n\tstd::optional<Image> image;\n\tFnMut<void()> load;\n\n\t[[nodiscard]] static QSize Size();\n};\n\nenum class EffectType : uint8 {\n\tEmojiInteraction,\n\tPremiumSticker,\n\tMessageEffect,\n};\n\nclass EmojiPack final {\npublic:\n\tusing ViewElement = HistoryView::Element;\n\n\tstruct Sticker {\n\t\tDocumentData *document = nullptr;\n\t\tconst Lottie::ColorReplacements *replacements = nullptr;\n\n\t\t[[nodiscard]] bool empty() const {\n\t\t\treturn (document == nullptr);\n\t\t}\n\t\t[[nodiscard]] explicit operator bool() const {\n\t\t\treturn !empty();\n\t\t}\n\t};\n\n\texplicit EmojiPack(not_null<Main::Session*> session);\n\t~EmojiPack();\n\n\tbool add(not_null<ViewElement*> view);\n\tvoid remove(not_null<const ViewElement*> view);\n\n\t[[nodiscard]] Sticker stickerForEmoji(EmojiPtr emoji);\n\t[[nodiscard]] Sticker stickerForEmoji(const IsolatedEmoji &emoji);\n\t[[nodiscard]] std::shared_ptr<LargeEmojiImage> image(EmojiPtr emoji);\n\n\t[[nodiscard]] EmojiPtr chooseInteractionEmoji(\n\t\tnot_null<HistoryItem*> item) const;\n\t[[nodiscard]] EmojiPtr chooseInteractionEmoji(\n\t\tconst QString &emoticon) const;\n\t[[nodiscard]] auto animationsForEmoji(EmojiPtr emoji) const\n\t\t-> const base::flat_map<int, not_null<DocumentData*>> &;\n\t[[nodiscard]] bool hasAnimationsFor(not_null<HistoryItem*> item) const;\n\t[[nodiscard]] bool hasAnimationsFor(const QString &emoticon) const;\n\t[[nodiscard]] int animationsVersion() const {\n\t\treturn _animationsVersion;\n\t}\n\t[[nodiscard]] rpl::producer<> refreshed() const {\n\t\treturn _refreshed.events();\n\t}\n\n\t[[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> effectPlayer(\n\t\tnot_null<DocumentData*> document,\n\t\tQByteArray data,\n\t\tQString filepath,\n\t\tEffectType type);\n\nprivate:\n\tclass ImageLoader;\n\n\tstruct ProviderKey {\n\t\tnot_null<DocumentData*> document;\n\t\tStickers::EffectType type = {};\n\n\t\tfriend inline auto operator<=>(\n\t\t\tconst ProviderKey &,\n\t\t\tconst ProviderKey &) = default;\n\t\tfriend inline bool operator==(\n\t\t\tconst ProviderKey &,\n\t\t\tconst ProviderKey &) = default;\n\t};\n\n\tvoid refresh();\n\tvoid refreshDelayed();\n\tvoid refreshAnimations();\n\tvoid applySet(const MTPDmessages_stickerSet &data);\n\tvoid applyPack(\n\t\tconst MTPDstickerPack &data,\n\t\tconst base::flat_map<uint64, not_null<DocumentData*>> &map);\n\tvoid applyAnimationsSet(const MTPDmessages_stickerSet &data);\n\t[[nodiscard]] auto collectStickers(const QVector<MTPDocument> &list) const\n\t\t-> base::flat_map<uint64, not_null<DocumentData*>>;\n\t[[nodiscard]] auto collectAnimationsIndices(\n\t\tconst QVector<MTPStickerPack> &packs) const\n\t\t-> base::flat_map<uint64, base::flat_set<int>>;\n\tvoid refreshAll();\n\tvoid refreshItems(EmojiPtr emoji);\n\tvoid refreshItems(const base::flat_set<not_null<ViewElement*>> &list);\n\tvoid refreshItems(const base::flat_set<not_null<HistoryItem*>> &items);\n\n\tconst not_null<Main::Session*> _session;\n\tbase::flat_map<EmojiPtr, not_null<DocumentData*>> _map;\n\tbase::flat_map<\n\t\tIsolatedEmoji,\n\t\tbase::flat_set<not_null<HistoryView::Element*>>> _items;\n\tbase::flat_map<EmojiPtr, std::weak_ptr<LargeEmojiImage>> _images;\n\tmtpRequestId _requestId = 0;\n\n\tbase::flat_set<not_null<HistoryView::Element*>> _onlyCustomItems;\n\n\tint _animationsVersion = 0;\n\tbase::flat_map<\n\t\tEmojiPtr,\n\t\tbase::flat_map<int, not_null<DocumentData*>>> _animations;\n\tmtpRequestId _animationsRequestId = 0;\n\n\tbase::flat_map<\n\t\tProviderKey,\n\t\tstd::weak_ptr<Lottie::FrameProvider>> _sharedProviders;\n\n\trpl::event_stream<> _refreshed;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Stickers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/stickers_gift_box_pack.h\"\n\n#include \"apiwrap.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n\nnamespace Stickers {\n\nGiftBoxPack::GiftBoxPack(not_null<Main::Session*> session)\n: _session(session) {\n\t_premium.dividers = { 1, 3, 6, 12, 24 };\n\t_ton.dividers = { 0, 10, 50 };\n}\n\nGiftBoxPack::~GiftBoxPack() = default;\n\nrpl::producer<> GiftBoxPack::updated() const {\n\treturn _premium.updated.events();\n}\n\nrpl::producer<> GiftBoxPack::tonUpdated() const {\n\treturn _ton.updated.events();\n}\n\nint GiftBoxPack::monthsForStars(int stars) const {\n\tif (stars <= 1000) {\n\t\treturn 3;\n\t} else if (stars < 2500) {\n\t\treturn 6;\n\t} else {\n\t\treturn 12;\n\t}\n}\n\nDocumentData *GiftBoxPack::lookup(int months) const {\n\treturn lookup(_premium, months, false);\n}\n\nDocumentData *GiftBoxPack::tonLookup(int amount) const {\n\treturn lookup(_ton, amount, true);\n}\n\nDocumentData *GiftBoxPack::lookup(\n\t\tconst Pack &pack,\n\t\tint divider,\n\t\tbool exact) const {\n\tconst auto it = ranges::lower_bound(pack.dividers, divider);\n\tconst auto fallback = pack.documents.empty()\n\t\t? nullptr\n\t\t: pack.documents.front();\n\tif (it == begin(pack.dividers)) {\n\t\treturn fallback;\n\t} else if (it == end(pack.dividers)) {\n\t\treturn pack.documents.back();\n\t}\n\tconst auto shift = exact\n\t\t? ((*it > divider) ? 1 : 0)\n\t\t: (std::abs(divider - (*(it - 1))) < std::abs(divider - (*it)))\n\t\t? -1\n\t\t: 0;\n\tconst auto index = int(std::distance(begin(pack.dividers), it - shift));\n\treturn (index >= pack.documents.size())\n\t\t? fallback\n\t\t: pack.documents[index];\n}\n\nData::FileOrigin GiftBoxPack::origin() const {\n\treturn Data::FileOriginStickerSet(_premium.id, _premium.accessHash);\n}\n\nData::FileOrigin GiftBoxPack::tonOrigin() const {\n\treturn Data::FileOriginStickerSet(_ton.id, _ton.accessHash);\n}\n\nvoid GiftBoxPack::load() {\n\tload(_premium, MTP_inputStickerSetPremiumGifts());\n}\n\nvoid GiftBoxPack::tonLoad() {\n\tload(_ton, MTP_inputStickerSetTonGifts());\n}\n\nvoid GiftBoxPack::load(Pack &pack, const MTPInputStickerSet &set) {\n\tif (pack.requestId || !pack.documents.empty()) {\n\t\treturn;\n\t}\n\tpack.requestId = _session->api().request(MTPmessages_GetStickerSet(\n\t\tset,\n\t\tMTP_int(0) // Hash.\n\t)).done([=, &pack](const MTPmessages_StickerSet &result) {\n\t\tpack.requestId = 0;\n\t\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\tapplySet(pack, data);\n\t\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t\t});\n\t}).fail([=, &pack] {\n\t\tpack.requestId = 0;\n\t}).send();\n}\n\nvoid GiftBoxPack::applySet(Pack &pack, const MTPDmessages_stickerSet &data) {\n\tpack.id = data.vset().data().vid().v;\n\tpack.accessHash = data.vset().data().vaccess_hash().v;\n\tauto documents = base::flat_map<DocumentId, not_null<DocumentData*>>();\n\tfor (const auto &sticker : data.vdocuments().v) {\n\t\tconst auto document = _session->data().processDocument(sticker);\n\t\tif (document->sticker()) {\n\t\t\tdocuments.emplace(document->id, document);\n\t\t\tif (pack.documents.empty()) {\n\t\t\t\t// Fallback.\n\t\t\t\tpack.documents.resize(1);\n\t\t\t\tpack.documents[0] = document;\n\t\t\t}\n\t\t}\n\t}\n\tfor (const auto &info : data.vpacks().v) {\n\t\tconst auto &data = info.data();\n\t\tconst auto emoji = qs(data.vemoticon());\n\t\tif (emoji.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const auto &id : data.vdocuments().v) {\n\t\t\tif (const auto document = documents.take(id.v)) {\n\t\t\t\tif (const auto sticker = (*document)->sticker()) {\n\t\t\t\t\tif (!sticker->alt.isEmpty()) {\n\t\t\t\t\t\tconst auto ch = int(sticker->alt[0].unicode());\n\t\t\t\t\t\tconst auto index = (ch - '1'); // [0, 4];\n\t\t\t\t\t\tif (index < 0 || index >= pack.dividers.size()) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ((index + 1) > pack.documents.size()) {\n\t\t\t\t\t\t\tpack.documents.resize((index + 1));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpack.documents[index] = (*document);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tpack.updated.fire({});\n}\n\n} // namespace Stickers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass DocumentData;\n\nnamespace Data {\nstruct FileOrigin;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Stickers {\n\nclass GiftBoxPack final {\npublic:\n\texplicit GiftBoxPack(not_null<Main::Session*> session);\n\t~GiftBoxPack();\n\n\tvoid load();\n\t[[nodiscard]] int monthsForStars(int stars) const;\n\t[[nodiscard]] DocumentData *lookup(int months) const;\n\t[[nodiscard]] Data::FileOrigin origin() const;\n\t[[nodiscard]] rpl::producer<> updated() const;\n\n\tvoid tonLoad();\n\t[[nodiscard]] DocumentData *tonLookup(int amount) const;\n\t[[nodiscard]] Data::FileOrigin tonOrigin() const;\n\t[[nodiscard]] rpl::producer<> tonUpdated() const;\n\nprivate:\n\tusing SetId = uint64;\n\n\tstruct Pack {\n\t\tSetId id = 0;\n\t\tuint64 accessHash = 0;\n\t\tstd::vector<DocumentData*> documents;\n\t\tmtpRequestId requestId = 0;\n\t\tstd::vector<int> dividers;\n\t\trpl::event_stream<> updated;\n\t};\n\n\tvoid load(Pack &pack, const MTPInputStickerSet &set);\n\tvoid applySet(Pack &pack, const MTPDmessages_stickerSet &data);\n\t[[nodiscard]] DocumentData *lookup(\n\t\tconst Pack &pack,\n\t\tint divider,\n\t\tbool exact) const;\n\n\tconst not_null<Main::Session*> _session;\n\tconst std::vector<int> _localMonths;\n\tconst std::vector<int> _localTonAmounts;\n\n\tPack _premium;\n\tPack _ton;\n\n};\n\n} // namespace Stickers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/stickers_list_footer.h\"\n\n#include \"chat_helpers/emoji_keywords.h\"\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"core/application.h\"\n#include \"data/stickers/data_stickers_set.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"ui/dpr/dpr_icon.h\"\n#include \"ui/dpr/dpr_image.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect_part.h\"\n#include \"styles/style_chat_helpers.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace ChatHelpers {\nnamespace {\n\nconstexpr auto kEmojiSectionSetIdBase = uint64(0x77FF'FFFF'FFFF'FFF0ULL);\nconstexpr auto kEmojiSearchLimit = 32;\n\nusing EmojiSection = Ui::Emoji::Section;\n\nvoid UpdateAnimated(anim::value &value, int to) {\n\tif (int(base::SafeRound(value.to())) == to) {\n\t\treturn;\n\t}\n\tvalue = anim::value(\n\t\t(value.from() != value.to()) ? value.from() : to,\n\t\tto);\n}\n\nvoid UpdateAnimated(\n\t\tanim::value &value,\n\t\tint to,\n\t\tValidateIconAnimations animations) {\n\tif (animations == ValidateIconAnimations::Full) {\n\t\tvalue.start(to);\n\t} else {\n\t\tvalue = anim::value(to, to);\n\t}\n}\n\n} // namespace\n\nuint64 EmojiSectionSetId(EmojiSection section) {\n\tExpects(section >= EmojiSection::Recent\n\t\t&& section <= EmojiSection::Symbols);\n\n\treturn kEmojiSectionSetIdBase + static_cast<uint64>(section) + 1;\n}\n\nuint64 RecentEmojiSectionSetId() {\n\treturn EmojiSectionSetId(EmojiSection::Recent);\n}\n\nuint64 AllEmojiSectionSetId() {\n\treturn kEmojiSectionSetIdBase;\n}\n\nuint64 SearchEmojiSectionSetId() {\n\treturn kEmojiSectionSetIdBase\n\t\t+ static_cast<uint64>(EmojiSection::Symbols)\n\t\t+ 2;\n}\n\nstd::optional<EmojiSection> SetIdEmojiSection(uint64 id) {\n\tconst auto base = RecentEmojiSectionSetId();\n\tif (id < base) {\n\t\treturn {};\n\t}\n\tconst auto index = id - base;\n\treturn (index <= uint64(EmojiSection::Symbols))\n\t\t? static_cast<EmojiSection>(index)\n\t\t: std::optional<EmojiSection>();\n}\n\n[[nodiscard]] std::vector<QString> GifSearchEmojiFallback() {\n\treturn {\n\t\tu\"\\xf0\\x9f\\x91\\x8d\"_q,\n\t\tu\"\\xf0\\x9f\\x98\\x98\"_q,\n\t\tu\"\\xf0\\x9f\\x98\\x8d\"_q,\n\t\tu\"\\xf0\\x9f\\x98\\xa1\"_q,\n\t\tu\"\\xf0\\x9f\\xa5\\xb3\"_q,\n\t\tu\"\\xf0\\x9f\\x98\\x82\"_q,\n\t\tu\"\\xf0\\x9f\\x98\\xae\"_q,\n\t\tu\"\\xf0\\x9f\\x99\\x84\"_q,\n\t\tu\"\\xf0\\x9f\\x98\\x8e\"_q,\n\t\tu\"\\xf0\\x9f\\x91\\x8e\"_q,\n\t};\n}\n\nrpl::producer<std::vector<GifSection>> GifSectionsValue(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto config = &session->appConfig();\n\treturn config->value(\n\t) | rpl::map([=] {\n\t\treturn config->get<std::vector<QString>>(\n\t\t\tu\"gif_search_emojies\"_q,\n\t\t\tGifSearchEmojiFallback());\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::map([=](const std::vector<QString> &emoji) {\n\t\tconst auto list = ranges::views::all(\n\t\t\temoji\n\t\t) | ranges::views::transform([](const QString &val) {\n\t\t\treturn Ui::Emoji::Find(val);\n\t\t}) | ranges::views::filter([](EmojiPtr emoji) {\n\t\t\treturn emoji != nullptr;\n\t\t}) | ranges::to_vector;\n\n\t\tconst auto pack = &session->emojiStickersPack();\n\t\treturn rpl::single(\n\t\t\trpl::empty_value()\n\t\t) | rpl::then(\n\t\t\tpack->refreshed()\n\t\t) | rpl::map([=, list = std::move(list)] {\n\t\t\t return list | ranges::views::transform([&](EmojiPtr emoji) {\n\t\t\t\tconst auto document = pack->stickerForEmoji(emoji).document;\n\t\t\t\treturn GifSection{ document, emoji };\n\t\t\t}) | ranges::views::filter([](GifSection section) {\n\t\t\t\treturn (section.document != nullptr);\n\t\t\t}) | ranges::to_vector;\n\t\t}) | rpl::distinct_until_changed();\n\t}) | rpl::flatten_latest();\n}\n\n[[nodiscard]] std::vector<EmojiPtr> SearchEmoji(\n\t\tconst std::vector<QString> &query,\n\t\tbase::flat_set<EmojiPtr> &outResultSet) {\n\tauto result = std::vector<EmojiPtr>();\n\tconst auto pushPlain = [&](EmojiPtr emoji) {\n\t\tif (result.size() < kEmojiSearchLimit\n\t\t\t&& outResultSet.emplace(emoji).second) {\n\t\t\tresult.push_back(emoji);\n\t\t}\n\t\tif (const auto original = emoji->original(); original != emoji) {\n\t\t\toutResultSet.emplace(original);\n\t\t}\n\t};\n\tauto refreshed = false;\n\tauto &keywords = Core::App().emojiKeywords();\n\tfor (const auto &entry : query) {\n\t\tif (const auto emoji = Ui::Emoji::Find(entry)) {\n\t\t\tpushPlain(emoji);\n\t\t\tif (result.size() >= kEmojiSearchLimit) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t} else if (!entry.isEmpty()) {\n\t\t\tif (!refreshed) {\n\t\t\t\trefreshed = true;\n\t\t\t\tkeywords.refresh();\n\t\t\t}\n\t\t\tconst auto list = keywords.queryMine(entry);\n\t\t\tfor (const auto &entry : list) {\n\t\t\t\tpushPlain(entry.emoji);\n\t\t\t\tif (result.size() >= kEmojiSearchLimit) {\n\t\t\t\t\treturn result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nStickerIcon::StickerIcon(uint64 setId) : setId(setId) {\n}\n\nStickerIcon::StickerIcon(\n\tnot_null<Data::StickersSet*> set,\n\tDocumentData *sticker,\n\tint pixw,\n\tint pixh)\n: setId(set->id)\n, set(set)\n, sticker(sticker)\n, pixw(std::max(pixw, 1))\n, pixh(std::max(pixh, 1)) {\n}\n\nStickerIcon::StickerIcon(StickerIcon&&) = default;\n\nStickerIcon &StickerIcon::operator=(StickerIcon&&) = default;\n\nStickerIcon::~StickerIcon() = default;\n\nvoid StickerIcon::ensureMediaCreated() const {\n\tif (!sticker) {\n\t\treturn;\n\t} else if (set->hasThumbnail()) {\n\t\tif (!thumbnailMedia) {\n\t\t\tthumbnailMedia = set->createThumbnailView();\n\t\t\tset->loadThumbnail();\n\t\t}\n\t} else if (!stickerMedia) {\n\t\tstickerMedia = sticker->createMediaView();\n\t\tstickerMedia->thumbnailWanted(sticker->stickerSetOrigin());\n\t}\n}\n\ntemplate <typename UpdateCallback>\nStickersListFooter::ScrollState::ScrollState(UpdateCallback &&callback)\n: animation([=](crl::time now) {\n\tcallback();\n\treturn animationCallback(now);\n}) {\n}\n\nbool StickersListFooter::ScrollState::animationCallback(crl::time now) {\n\tif (anim::Disabled()) {\n\t\tnow += st::stickerIconMove;\n\t}\n\tif (!animationStart) {\n\t\treturn false;\n\t}\n\tconst auto dt = (now - animationStart) / float64(st::stickerIconMove);\n\tif (dt >= 1.) {\n\t\tanimationStart = 0;\n\t\tx.finish();\n\t\tselectionX.finish();\n\t\tselectionWidth.finish();\n\t\treturn false;\n\t}\n\tx.update(dt, anim::linear);\n\tselectionX.update(dt, anim::easeOutCubic);\n\tselectionWidth.update(dt, anim::easeOutCubic);\n\treturn true;\n}\n\nGradientPremiumStar::GradientPremiumStar() {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_image = QImage();\n\t}, _lifetime);\n}\n\nQImage GradientPremiumStar::image() const {\n\tif (_image.isNull()) {\n\t\trenderOnDemand();\n\t}\n\treturn _image;\n}\n\nvoid GradientPremiumStar::renderOnDemand() const {\n\tconst auto size = st::emojiStatusDefault.size();\n\tconst auto mask = st::emojiStatusDefault.instance(Qt::white);\n\tconst auto factor = style::DevicePixelRatio();\n\t_image = QImage(\n\t\tsize * factor,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_image.setDevicePixelRatio(factor);\n\n\tQPainter p(&_image);\n\tauto gradient = QLinearGradient(\n\t\tQPoint(0, size.height()),\n\t\tQPoint(size.width(), 0));\n\tgradient.setStops({\n\t\t{ 0., st::stickerPanPremium1->c },\n\t\t{ 1., st::stickerPanPremium2->c },\n\t});\n\tp.fillRect(QRect(QPoint(), size), gradient);\n\tp.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\tp.drawImage(QRect(QPoint(), size), mask);\n}\n\nStickersListFooter::StickersListFooter(Descriptor &&descriptor)\n: InnerFooter(\n\tdescriptor.parent,\n\tdescriptor.st ? *descriptor.st : st::defaultEmojiPan)\n, _session(descriptor.session)\n, _customTextColor(std::move(descriptor.customTextColor))\n, _paused(std::move(descriptor.paused))\n, _features(descriptor.features)\n, _iconState([=] { update(); })\n, _subiconState([=] { update(); })\n, _selectionBg(st::emojiPanRadius, st().categoriesBgOver)\n, _subselectionBg(st().iconArea / 2, st().categoriesBgOver)\n, _forceFirstFrame(descriptor.forceFirstFrame) {\n\tsetMouseTracking(true);\n\n\t_iconsLeft = st().iconSkip\n\t\t+ (_features.stickersSettings ? st().iconWidth : 0);\n\t_iconsRight = st().iconSkip;\n\n\t_session->downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n}\n\nvoid StickersListFooter::clearHeavyData() {\n\tenumerateIcons([&](const IconInfo &info) {\n\t\tauto &icon = _icons[info.index];\n\t\ticon.webm = nullptr;\n\t\ticon.lottie = nullptr;\n\t\ticon.lifetime.destroy();\n\t\ticon.stickerMedia = nullptr;\n\t\tif (!info.visible) {\n\t\t\ticon.savedFrame = QImage();\n\t\t}\n\t\treturn true;\n\t});\n}\n\nvoid StickersListFooter::paintExpanding(\n\t\tPainter &p,\n\t\tQRect clip,\n\t\tfloat64 radius,\n\t\tRectPart origin) {\n\tconst auto delta = ((origin | RectPart::None) & RectPart::FullBottom)\n\t\t? (height() - clip.height())\n\t\t: 0;\n\tconst auto shift = QPoint(clip.x(), clip.y() - delta);\n\tp.translate(shift);\n\tconst auto context = ExpandingContext{\n\t\t.clip = clip.translated(-shift),\n\t\t.progress = clip.height() / float64(height()),\n\t\t.radius = int(std::ceil(radius)),\n\t\t.expanding = true,\n\t};\n\tpaint(p, context);\n\tp.translate(-shift);\n\tp.setClipping(false);\n}\n\nint StickersListFooter::IconFrameSize() {\n\treturn Data::FrameSizeFromTag(\n\t\tData::CustomEmojiManager::SizeTag::SetIcon\n\t) / style::DevicePixelRatio();\n}\n\nvoid StickersListFooter::enumerateVisibleIcons(\n\t\tFn<void(const IconInfo &)> callback) const {\n\tenumerateIcons([&](const IconInfo &info) {\n\t\tif (info.visible) {\n\t\t\tcallback(info);\n\t\t} else if (info.adjustedLeft > 0) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n}\n\nvoid StickersListFooter::enumerateIcons(\n\t\tFn<bool(const IconInfo &)> callback) const {\n\tauto left = 0;\n\tconst auto iconsX = int(base::SafeRound(_iconState.x.current()));\n\tconst auto shift = _iconsLeft - iconsX;\n\tconst auto emojiId = AllEmojiSectionSetId();\n\tconst auto right = width();\n\tfor (auto i = 0, count = int(_icons.size()); i != count; ++i) {\n\t\tauto &icon = _icons[i];\n\t\tconst auto width = (icon.setId == emojiId)\n\t\t\t? _subiconsWidthAnimation.value(_subiconsExpanded\n\t\t\t\t? _subiconsWidth\n\t\t\t\t: _singleWidth)\n\t\t\t: _singleWidth;\n\t\tconst auto shifted = shift + left;\n\t\tconst auto visible = (shifted + width > 0 && shifted < right);\n\t\tconst auto result = callback({\n\t\t\t.index = i,\n\t\t\t.left = left,\n\t\t\t.adjustedLeft = shifted,\n\t\t\t.width = int(base::SafeRound(width)),\n\t\t\t.visible = visible,\n\t\t});\n\t\tif (!result) {\n\t\t\tbreak;\n\t\t}\n\t\tleft += width;\n\t}\n}\n\nvoid StickersListFooter::enumerateSubicons(\n\t\tFn<bool(const IconInfo &)> callback) const {\n\tauto left = 0;\n\tconst auto iconsX = int(base::SafeRound(_subiconState.x.current()));\n\tconst auto shift = -iconsX;\n\tconst auto right = _subiconsWidth;\n\tusing Section = Ui::Emoji::Section;\n\tfor (auto i = int(Section::People); i <= int(Section::Symbols); ++i) {\n\t\tconst auto shifted = shift + left;\n\t\tconst auto visible = (shifted + _singleWidth > 0 && shifted < right);\n\t\tconst auto result = callback({\n\t\t\t.index = i - int(Section::People),\n\t\t\t.left = left,\n\t\t\t.adjustedLeft = shifted,\n\t\t\t.width = _singleWidth,\n\t\t\t.visible = visible,\n\t\t});\n\t\tif (!result) {\n\t\t\tbreak;\n\t\t}\n\t\tleft += _singleWidth;\n\t}\n}\n\nauto StickersListFooter::iconInfo(int index) const -> IconInfo {\n\tif (index < 0) {\n\t\tconst auto iconsX = int(base::SafeRound(_iconState.x.current()));\n\t\treturn {\n\t\t\t.index = -1,\n\t\t\t.left = -_singleWidth - _iconsLeft,\n\t\t\t.adjustedLeft = -_singleWidth - _iconsLeft - iconsX,\n\t\t\t.width = _singleWidth,\n\t\t\t.visible = false,\n\t\t};\n\t}\n\tauto result = IconInfo();\n\tenumerateIcons([&](const IconInfo &info) {\n\t\tif (info.index == index) {\n\t\t\tresult = info;\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n\treturn result;\n}\n\nauto StickersListFooter::subiconInfo(int index) const -> IconInfo {\n\tauto result = IconInfo();\n\tenumerateSubicons([&](const IconInfo &info) {\n\t\tif (info.index == index) {\n\t\t\tresult = info;\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n\treturn result;\n}\n\nvoid StickersListFooter::preloadImages() {\n\tenumerateVisibleIcons([&](const IconInfo &info) {\n\t\tconst auto &icon = _icons[info.index];\n\t\tif (const auto sticker = icon.sticker) {\n\t\t\tAssert(icon.set != nullptr);\n\t\t\tif (icon.set->hasThumbnail()) {\n\t\t\t\ticon.set->loadThumbnail();\n\t\t\t} else {\n\t\t\t\tsticker->loadThumbnail(sticker->stickerSetOrigin());\n\t\t\t}\n\t\t}\n\t});\n}\n\nvoid StickersListFooter::validateSelectedIcon(\n\t\tuint64 setId,\n\t\tValidateIconAnimations animations) {\n\t_activeByScrollId = setId;\n\n\tusing EmojiSection = Ui::Emoji::Section;\n\tauto favedIconIndex = -1;\n\tauto newSelected = -1;\n\tauto newSubSelected = -1;\n\tconst auto emojiSection = SetIdEmojiSection(setId);\n\tconst auto isEmojiSection = emojiSection.has_value()\n\t\t&& (emojiSection != EmojiSection::Recent);\n\tconst auto allEmojiSetId = AllEmojiSectionSetId();\n\tfor (auto i = 0, l = int(_icons.size()); i != l; ++i) {\n\t\tif (_icons[i].setId == setId\n\t\t\t|| (_icons[i].setId == Data::Stickers::FavedSetId\n\t\t\t\t&& setId == Data::Stickers::RecentSetId)) {\n\t\t\tnewSelected = i;\n\t\t\tbreak;\n\t\t} else if (_icons[i].setId == Data::Stickers::FavedSetId\n\t\t\t&& setId != SearchEmojiSectionSetId()) {\n\t\t\tfavedIconIndex = i;\n\t\t} else if (isEmojiSection && _icons[i].setId == allEmojiSetId) {\n\t\t\tnewSelected = i;\n\t\t\tnewSubSelected = setId - EmojiSectionSetId(EmojiSection::People);\n\t\t}\n\t}\n\tsetSelectedIcon(\n\t\t(newSelected >= 0\n\t\t\t? newSelected\n\t\t\t: (favedIconIndex >= 0)\n\t\t\t? favedIconIndex\n\t\t\t: -1),\n\t\tanimations);\n\tsetSelectedSubicon(\n\t\t(newSubSelected >= 0 ? newSubSelected : 0),\n\t\tanimations);\n}\n\nvoid StickersListFooter::updateEmojiSectionWidth() {\n\tconst auto expanded = (_iconState.selected >= 0)\n\t\t&& (_iconState.selected < _icons.size())\n\t\t&& (_icons[_iconState.selected].setId == AllEmojiSectionSetId());\n\tif (_subiconsExpanded == expanded) {\n\t\treturn;\n\t}\n\t_subiconsExpanded = expanded;\n\t_subiconsWidthAnimation.start(\n\t\t[=] { updateEmojiWidthCallback(); },\n\t\texpanded ? _singleWidth : _subiconsWidth,\n\t\texpanded ? _subiconsWidth : _singleWidth,\n\t\tst::slideDuration);\n}\n\nvoid StickersListFooter::updateEmojiWidthCallback() {\n\trefreshScrollableDimensions();\n\tconst auto info = iconInfo(_iconState.selected);\n\tUpdateAnimated(_iconState.selectionX, info.left);\n\tUpdateAnimated(_iconState.selectionWidth, info.width);\n\tif (_iconState.animation.animating()) {\n\t\t_iconState.animationCallback(crl::now());\n\t}\n\tupdate();\n}\n\nvoid StickersListFooter::setSelectedIcon(\n\t\tint newSelected,\n\t\tValidateIconAnimations animations) {\n\tif (_iconState.selected == newSelected) {\n\t\treturn;\n\t}\n\tif ((_iconState.selected < 0) != (newSelected < 0)) {\n\t\tanimations = ValidateIconAnimations::None;\n\t}\n\t_iconState.selected = newSelected;\n\tupdateEmojiSectionWidth();\n\tconst auto info = iconInfo(_iconState.selected);\n\tUpdateAnimated(_iconState.selectionX, info.left, animations);\n\tUpdateAnimated(_iconState.selectionWidth, info.width, animations);\n\tconst auto relativeLeft = info.left - _iconsLeft;\n\tconst auto iconsWidthForCentering = 2 * relativeLeft + info.width;\n\tconst auto iconsXFinal = std::clamp(\n\t\t(_iconsLeft + iconsWidthForCentering + _iconsRight - width()) / 2,\n\t\t0,\n\t\t_iconState.max);\n\tif (animations == ValidateIconAnimations::None) {\n\t\t_iconState.x = anim::value(iconsXFinal, iconsXFinal);\n\t\t_iconState.animation.stop();\n\t} else {\n\t\t_iconState.x.start(iconsXFinal);\n\t\t_iconState.animationStart = crl::now();\n\t\t_iconState.animation.start();\n\t}\n\tupdateSelected();\n\tupdate();\n}\n\nvoid StickersListFooter::setSelectedSubicon(\n\t\tint newSelected,\n\t\tValidateIconAnimations animations) {\n\tif (_subiconState.selected == newSelected) {\n\t\treturn;\n\t}\n\t_subiconState.selected = newSelected;\n\tconst auto info = subiconInfo(_subiconState.selected);\n\tconst auto relativeLeft = info.left;\n\tconst auto subiconsWidthForCentering = 2 * relativeLeft + info.width;\n\tconst auto subiconsXFinal = std::clamp(\n\t\t(subiconsWidthForCentering - _subiconsWidth) / 2,\n\t\t0,\n\t\t_subiconState.max);\n\tif (animations == ValidateIconAnimations::None) {\n\t\t_subiconState.x = anim::value(subiconsXFinal, subiconsXFinal);\n\t\t_subiconState.animation.stop();\n\t} else {\n\t\t_subiconState.x.start(subiconsXFinal);\n\t\t_subiconState.animationStart = crl::now();\n\t\t_subiconState.animation.start();\n\t}\n\tupdateSelected();\n\tupdate();\n}\n\nvoid StickersListFooter::processHideFinished() {\n\t_selected = _pressed = SpecialOver::None;\n\t_iconState.animation.stop();\n\t_iconState.animationStart = 0;\n\t_iconState.x.finish();\n\t_iconState.selectionX.finish();\n\t_iconState.selectionWidth.finish();\n\t_subiconState.animation.stop();\n\t_subiconState.animationStart = 0;\n\t_subiconState.x.finish();\n}\n\nvoid StickersListFooter::leaveToChildEvent(QEvent *e, QWidget *child) {\n\t_iconsMousePos = QCursor::pos();\n\tupdateSelected();\n}\n\nvoid StickersListFooter::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\n\t_repaintScheduled = false;\n\tpaint(p, {});\n}\n\nvoid StickersListFooter::paint(\n\t\tPainter &p,\n\t\tconst ExpandingContext &context) const {\n\tif (_icons.empty()) {\n\t\treturn;\n\t}\n\n\tif (_features.stickersSettings) {\n\t\tpaintStickerSettingsIcon(p);\n\t}\n\n\tauto clip = QRect(\n\t\t_iconsLeft,\n\t\t_iconsTop,\n\t\twidth() - _iconsLeft - _iconsRight,\n\t\tst().footer);\n\tif (rtl()) {\n\t\tclip.moveLeft(width() - _iconsLeft - clip.width());\n\t}\n\tif (context.expanding) {\n\t\tconst auto both = clip.intersected(\n\t\t\tcontext.clip.marginsRemoved(\n\t\t\t\t{ 0/*context.radius*/, 0, context.radius, 0 }));\n\t\tif (both.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tp.setClipRect(both);\n\t} else {\n\t\tp.setClipRect(clip);\n\t}\n\tpaintSelectionBg(p, context);\n\n\tconst auto iconCacheSize = QSize(_singleWidth, st().footer);\n\tconst auto full = iconCacheSize * style::DevicePixelRatio();\n\tif (_setIconCache.size() != full) {\n\t\t_setIconCache = QImage(full, QImage::Format_ARGB32_Premultiplied);\n\t\t_setIconCache.setDevicePixelRatio(style::DevicePixelRatio());\n\t}\n\n\tconst auto now = crl::now();\n\tconst auto paused = _paused();\n\tp.setPen(st::windowFg);\n\tenumerateVisibleIcons([&](const IconInfo &info) {\n\t\tpaintSetIcon(p, context, info, now, paused);\n\t});\n\tpaintLeftRightFading(p, context);\n}\n\nvoid StickersListFooter::paintSelectionBg(\n\t\tQPainter &p,\n\t\tconst ExpandingContext &context) const {\n\tauto selxrel = _iconsLeft + qRound(_iconState.selectionX.current());\n\tauto selx = selxrel - qRound(_iconState.x.current());\n\tconst auto selw = qRound(_iconState.selectionWidth.current());\n\tif (rtl()) {\n\t\tselx = width() - selx - selw;\n\t}\n\tconst auto sely = _iconsTop;\n\tconst auto area = st().iconArea;\n\tauto rect = QRect(\n\t\tQPoint(selx, sely) + _areaPosition,\n\t\tQSize(selw - 2 * _areaPosition.x(), area));\n\tif (context.expanding) {\n\t\tconst auto recthalf = rect.height() / 2;\n\t\tconst auto myhalf = height() / 2;\n\t\tconst auto sub = anim::interpolate(recthalf, 0, context.progress);\n\t\tconst auto shift = anim::interpolate(myhalf, 0, context.progress);\n\t\trect = rect.marginsRemoved(\n\t\t\t{ sub, sub, sub, sub }\n\t\t).translated(0, shift);\n\t}\n\tif (rect.width() == rect.height() || _subiconsWidth <= _singleWidth) {\n\t\t_selectionBg.paint(p, rect);\n\t} else if (selw == _subiconsWidth) {\n\t\t_subselectionBg.paint(p, rect);\n\t} else {\n\t\tPainterHighQualityEnabler hq(p);\n\t\tconst auto progress = (selw - _singleWidth)\n\t\t\t/ float64(_subiconsWidth - _singleWidth);\n\t\tconst auto radius = anim::interpolate(\n\t\t\tst::roundRadiusLarge,\n\t\t\tarea / 2,\n\t\t\tprogress);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st().categoriesBgOver);\n\t\tp.drawRoundedRect(rect, radius, radius);\n\t}\n}\n\nvoid StickersListFooter::paintLeftRightFading(\n\t\tQPainter &p,\n\t\tconst ExpandingContext &context) const {\n\tconst auto o_left_normal = std::clamp(\n\t\t_iconState.x.current() / st().fadeLeft.width(),\n\t\t0.,\n\t\t1.);\n\tconst auto o_left = context.expanding\n\t\t? (1. - context.progress * (1. - o_left_normal))\n\t\t: o_left_normal;\n\tconst auto radiusSkip = context.expanding\n\t\t? std::max(context.radius - st::emojiPanRadius, 0)\n\t\t: 0;\n\tif (o_left > 0) {\n\t\tp.setOpacity(o_left);\n\t\tconst auto left = std::max(_iconsLeft, radiusSkip);\n\t\tconst auto top = _iconsTop;\n\t\tif (left >= st::emojiPanRadius) {\n\t\t\tst().fadeLeft.fill(\n\t\t\t\tp,\n\t\t\t\tQRect(left, top, st().fadeLeft.width(), st().footer));\n\t\t} else {\n\t\t\tvalidateFadeLeft(left + st().fadeLeft.width());\n\t\t\tp.drawImage(0, _iconsTop, _fadeLeftCache);\n\t\t}\n\t\tp.setOpacity(1.);\n\t}\n\tconst auto o_right_normal = std::clamp(\n\t\t(_iconState.max - _iconState.x.current()) / st().fadeRight.width(),\n\t\t0.,\n\t\t1.);\n\tconst auto o_right = context.expanding\n\t\t? (1. - context.progress * (1. - o_right_normal))\n\t\t: o_right_normal;\n\tif (o_right > 0) {\n\t\tp.setOpacity(o_right);\n\t\tconst auto right = std::max(_iconsRight, radiusSkip);\n\t\tconst auto rightWidth = right + st().fadeRight.width();\n\t\tif (right >= st::emojiPanRadius) {\n\t\t\tst().fadeRight.fill(\n\t\t\t\tp,\n\t\t\t\tQRect(\n\t\t\t\t\twidth() - rightWidth,\n\t\t\t\t\t_iconsTop,\n\t\t\t\t\tst().fadeRight.width(),\n\t\t\t\t\tst().footer));\n\t\t} else {\n\t\t\tvalidateFadeRight(rightWidth);\n\t\t\tp.drawImage(width() - rightWidth, _iconsTop, _fadeRightCache);\n\t\t}\n\t\tp.setOpacity(1.);\n\t}\n}\n\nvoid StickersListFooter::validateFadeLeft(int leftWidth) const {\n\tvalidateFadeMask();\n\n\tconst auto ratio = devicePixelRatioF();\n\tconst auto &color = st().categoriesBg->c;\n\tdpr::Validate(_fadeLeftCache, ratio, { leftWidth, st().footer }, [&](\n\t\t\tQPainter &p,\n\t\t\tQSize size) {\n\t\t_fadeLeftColor = color;\n\t\tconst auto frame = dpr::IconFrame(st().fadeLeft, color, ratio);\n\t\tp.drawImage(\n\t\t\tQRect(\n\t\t\t\tsize.width() - frame.width(),\n\t\t\t\t0,\n\t\t\t\tframe.width(),\n\t\t\t\tsize.height()),\n\t\t\tframe);\n\t\tp.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\tp.drawImage(0, 0, _fadeMask);\n\t}, (_fadeLeftColor != color), Qt::transparent);\n}\n\nvoid StickersListFooter::validateFadeRight(int rightWidth) const {\n\tvalidateFadeMask();\n\n\tconst auto ratio = devicePixelRatioF();\n\tconst auto &color = st().categoriesBg->c;\n\tdpr::Validate(_fadeRightCache, ratio, { rightWidth, st().footer }, [&](\n\t\t\tQPainter &p,\n\t\t\tQSize size) {\n\t\t_fadeRightColor = color;\n\t\tconst auto frame = dpr::IconFrame(st().fadeRight, color, ratio);\n\t\tp.drawImage(QRect(0, 0, frame.width(), size.height()), frame);\n\t\tp.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\tp.drawImage(size.width() - _fadeMask.width(), 0, _fadeMask);\n\t}, (_fadeRightColor != color), Qt::transparent);\n}\n\nvoid StickersListFooter::validateFadeMask() const {\n\tconst auto ratio = devicePixelRatioF();\n\tconst auto width = st().fadeLeft.width()\n\t\t+ st().fadeRight.width()\n\t\t+ 2 * st::emojiPanRadius;\n\tdpr::Validate(_fadeMask, ratio, { width, st().footer }, [&](\n\t\t\tQPainter &p,\n\t\t\tQSize size) {\n\t\tconst auto radius = st::emojiPanRadius * ratio;\n\t\tp.setBrush(Qt::white);\n\t\tp.setPen(Qt::NoPen);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.drawRoundedRect(QRect(QPoint(), size), radius, radius);\n\t}, false, Qt::transparent, false);\n}\n\nvoid StickersListFooter::resizeEvent(QResizeEvent *e) {\n\trefreshIconsGeometry(_activeByScrollId, ValidateIconAnimations::None);\n}\n\nrpl::producer<uint64> StickersListFooter::setChosen() const {\n\treturn _setChosen.events();\n}\n\nrpl::producer<> StickersListFooter::openSettingsRequests() const {\n\treturn _openSettingsRequests.events();\n}\n\nvoid StickersListFooter::mousePressEvent(QMouseEvent *e) {\n\tif (e->button() != Qt::LeftButton) {\n\t\treturn;\n\t}\n\t_iconsMousePos = e->globalPos();\n\tupdateSelected();\n\n\tif (_selected == SpecialOver::Settings) {\n\t\t_openSettingsRequests.fire({});\n\t} else {\n\t\t_pressed = _selected;\n\t\t_iconsMouseDown = _iconsMousePos;\n\t\t_iconState.draggingStartX = qRound(_iconState.x.current());\n\t\t_subiconState.draggingStartX = qRound(_subiconState.x.current());\n\t}\n}\n\nvoid StickersListFooter::mouseMoveEvent(QMouseEvent *e) {\n\t_iconsMousePos = e ? e->globalPos() : QCursor::pos();\n\tupdateSelected();\n\n\tif (!_iconState.dragging\n\t\t&& !_icons.empty()\n\t\t&& v::is<IconId>(_pressed)) {\n\t\tif ((_iconsMousePos - _iconsMouseDown).manhattanLength() >= QApplication::startDragDistance()) {\n\t\t\tconst auto &icon = _icons[v::get<IconId>(_pressed).index];\n\t\t\t(icon.setId == AllEmojiSectionSetId()\n\t\t\t\t? _subiconState\n\t\t\t\t: _iconState).dragging = true;\n\t\t}\n\t}\n\tcheckDragging(_iconState);\n\tcheckDragging(_subiconState);\n}\n\nvoid StickersListFooter::checkDragging(ScrollState &state) {\n\tif (state.dragging) {\n\t\tconst auto newX = std::clamp(\n\t\t\t(rtl() ? -1 : 1) * (_iconsMouseDown.x() - _iconsMousePos.x())\n\t\t\t\t+ state.draggingStartX,\n\t\t\t0,\n\t\t\tstate.max);\n\t\tif (newX != qRound(state.x.current())) {\n\t\t\tstate.x = anim::value(newX, newX);\n\t\t\tstate.animationStart = 0;\n\t\t\tstate.animation.stop();\n\t\t\tupdate();\n\t\t}\n\t}\n}\n\nvoid StickersListFooter::mouseReleaseEvent(QMouseEvent *e) {\n\tif (_icons.empty()) {\n\t\treturn;\n\t}\n\n\tconst auto wasDown = std::exchange(_pressed, SpecialOver::None);\n\n\t_iconsMousePos = e ? e->globalPos() : QCursor::pos();\n\tif (finishDragging()) {\n\t\treturn;\n\t}\n\n\tupdateSelected();\n\tif (wasDown == _selected) {\n\t\tif (const auto icon = std::get_if<IconId>(&_selected)) {\n\t\t\tconst auto info = iconInfo(icon->index);\n\t\t\t_iconState.selectionX = anim::value(info.left, info.left);\n\t\t\t_iconState.selectionWidth = anim::value(info.width, info.width);\n\t\t\tconst auto setId = _icons[icon->index].setId;\n\t\t\t_setChosen.fire_copy((setId == AllEmojiSectionSetId())\n\t\t\t\t? EmojiSectionSetId(\n\t\t\t\t\tEmojiSection(int(EmojiSection::People) + icon->subindex))\n\t\t\t\t: setId);\n\t\t}\n\t}\n}\n\nbool StickersListFooter::finishDragging() {\n\tconst auto icon = finishDragging(_iconState);\n\tconst auto subicon = finishDragging(_subiconState);\n\treturn icon || subicon;\n}\n\nbool StickersListFooter::finishDragging(ScrollState &state) {\n\tif (!state.dragging) {\n\t\treturn false;\n\t}\n\tconst auto newX = std::clamp(\n\t\tstate.draggingStartX + _iconsMouseDown.x() - _iconsMousePos.x(),\n\t\t0,\n\t\tstate.max);\n\tif (newX != qRound(state.x.current())) {\n\t\tstate.x = anim::value(newX, newX);\n\t\tstate.animationStart = 0;\n\t\tstate.animation.stop();\n\t\tupdate();\n\t}\n\tstate.dragging = false;\n\tupdateSelected();\n\treturn true;\n}\n\nbool StickersListFooter::eventHook(QEvent *e) {\n\tif (e->type() == QEvent::TouchBegin) {\n\t} else if (e->type() == QEvent::Wheel) {\n\t\tif (!_icons.empty()\n\t\t\t&& v::is<IconId>(_selected)\n\t\t\t&& (_pressed == SpecialOver::None)) {\n\t\t\tscrollByWheelEvent(static_cast<QWheelEvent*>(e));\n\t\t}\n\t}\n\treturn InnerFooter::eventHook(e);\n}\n\nvoid StickersListFooter::scrollByWheelEvent(\n\t\tnot_null<QWheelEvent*> e) {\n\tauto horizontal = (e->angleDelta().x() != 0);\n\tauto vertical = (e->angleDelta().y() != 0);\n\tif (!horizontal && !vertical) {\n\t\treturn;\n\t}\n\tauto delta = horizontal\n\t\t? ((rtl() ? -1 : 1) * (e->pixelDelta().x()\n\t\t\t? e->pixelDelta().x()\n\t\t\t: e->angleDelta().x()))\n\t\t: (e->pixelDelta().y()\n\t\t\t? e->pixelDelta().y()\n\t\t\t: e->angleDelta().y());\n\tconst auto use = [&](ScrollState &state) {\n\t\tconst auto now = qRound(state.x.current());\n\t\tconst auto used = now - delta;\n\t\tconst auto next = std::clamp(used, 0, state.max);\n\t\tdelta = next - used;\n\t\tif (next != now) {\n\t\t\tstate.x = anim::value(next, next);\n\t\t\tstate.animationStart = 0;\n\t\t\tstate.animation.stop();\n\t\t\tupdateSelected();\n\t\t\tupdate();\n\t\t}\n\t};\n\tconst auto index = v::get<IconId>(_selected).index;\n\tif (_subiconsExpanded\n\t\t&& _icons[index].setId == AllEmojiSectionSetId()) {\n\t\tuse(_subiconState);\n\t} else {\n\t\tuse(_iconState);\n\t}\n}\n\nvoid StickersListFooter::clipCallback(\n\t\tMedia::Clip::Notification notification,\n\t\tuint64 setId) {\n\tusing namespace Media::Clip;\n\tswitch (notification) {\n\tcase Notification::Reinit: {\n\t\tenumerateIcons([&](const IconInfo &info) {\n\t\t\tauto &icon = _icons[info.index];\n\t\t\tif (icon.setId != setId || !icon.webm) {\n\t\t\t\treturn true;\n\t\t\t} else if (icon.webm->state() == State::Error) {\n\t\t\t\ticon.webm.setBad();\n\t\t\t} else if (!info.visible) {\n\t\t\t\ticon.webm = nullptr;\n\t\t\t} else if (icon.webm->ready() && !icon.webm->started()) {\n\t\t\t\ticon.webm->start({\n\t\t\t\t\t.frame = { icon.pixw, icon.pixh },\n\t\t\t\t\t.keepAlpha = true,\n\t\t\t\t});\n\t\t\t}\n\t\t\tupdateSetIconAt(info.adjustedLeft);\n\t\t\treturn true;\n\t\t});\n\t} break;\n\n\tcase Notification::Repaint:\n\t\tupdateSetIcon(setId);\n\t\tbreak;\n\t}\n}\n\nvoid StickersListFooter::updateSelected() {\n\tif (_pressed != SpecialOver::None) {\n\t\treturn;\n\t}\n\n\tauto p = mapFromGlobal(_iconsMousePos);\n\tauto x = p.x(), y = p.y();\n\tif (rtl()) x = width() - x;\n\tconst auto settingsLeft = _iconsLeft - _singleWidth;\n\tauto newOver = OverState(SpecialOver::None);\n\tif (_features.stickersSettings\n\t\t&& x >= settingsLeft\n\t\t&& x < settingsLeft + _singleWidth\n\t\t&& y >= _iconsTop\n\t\t&& y < _iconsTop + st().footer) {\n\t\tif (!_icons.empty()) {\n\t\t\tnewOver = SpecialOver::Settings;\n\t\t}\n\t} else if (!_icons.empty()) {\n\t\tif (y >= _iconsTop\n\t\t\t&& y < _iconsTop + st().footer\n\t\t\t&& x >= _iconsLeft\n\t\t\t&& x < width() - _iconsRight) {\n\t\t\tenumerateIcons([&](const IconInfo &info) {\n\t\t\t\tif (x >= info.adjustedLeft\n\t\t\t\t\t&& x < info.adjustedLeft + info.width) {\n\t\t\t\t\tnewOver = IconId{ .index = info.index };\n\t\t\t\t\tif (_icons[info.index].setId == AllEmojiSectionSetId()) {\n\t\t\t\t\t\tconst auto subx = (x - info.adjustedLeft);\n\t\t\t\t\t\tenumerateSubicons([&](const IconInfo &info) {\n\t\t\t\t\t\t\tif (subx >= info.adjustedLeft\n\t\t\t\t\t\t\t\t&& subx < info.adjustedLeft + info.width) {\n\t\t\t\t\t\t\t\tv::get<IconId>(newOver).subindex = info.index;\n\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\t}\n\tif (newOver != _selected) {\n\t\tif (newOver == SpecialOver::None) {\n\t\t\tsetCursor(style::cur_default);\n\t\t} else if (_selected == SpecialOver::None) {\n\t\t\tsetCursor(style::cur_pointer);\n\t\t}\n\t\t_selected = newOver;\n\t}\n}\n\nauto StickersListFooter::getLottieRenderer()\n-> std::shared_ptr<Lottie::FrameRenderer> {\n\tif (auto result = _lottieRenderer.lock()) {\n\t\treturn result;\n\t}\n\tauto result = Lottie::MakeFrameRenderer();\n\t_lottieRenderer = result;\n\treturn result;\n}\n\nvoid StickersListFooter::refreshIcons(\n\t\tstd::vector<StickerIcon> icons,\n\t\tuint64 activeSetId,\n\t\tFn<std::shared_ptr<Lottie::FrameRenderer>()> renderer,\n\t\tValidateIconAnimations animations) {\n\t_renderer = renderer\n\t\t? std::move(renderer)\n\t\t: [=] { return getLottieRenderer(); };\n\n\tauto indices = base::flat_map<uint64, int>();\n\tindices.reserve(_icons.size());\n\tauto index = 0;\n\tfor (const auto &entry : _icons) {\n\t\tindices.emplace(entry.setId, index++);\n\t}\n\n\tfor (auto &now : icons) {\n\t\tif (const auto i = indices.find(now.setId); i != end(indices)) {\n\t\t\tauto &was = _icons[i->second];\n\t\t\tif (now.sticker == was.sticker) {\n\t\t\t\tnow.webm = std::move(was.webm);\n\t\t\t\tnow.lottie = std::move(was.lottie);\n\t\t\t\tnow.custom = std::move(was.custom);\n\t\t\t\tnow.lifetime = std::move(was.lifetime);\n\t\t\t\tnow.savedFrame = std::move(was.savedFrame);\n\t\t\t}\n\t\t}\n\t}\n\n\t_icons = std::move(icons);\n\trefreshIconsGeometry(activeSetId, animations);\n}\n\nvoid StickersListFooter::refreshScrollableDimensions() {\n\tconst auto &last = iconInfo(_icons.size() - 1);\n\t_iconState.max = std::max(\n\t\tlast.left + last.width + _iconsLeft + _iconsRight - width(),\n\t\t0);\n\tif (_iconState.x.current() > _iconState.max) {\n\t\t_iconState.x = anim::value(_iconState.max, _iconState.max);\n\t}\n}\n\nvoid StickersListFooter::refreshIconsGeometry(\n\t\tuint64 activeSetId,\n\t\tValidateIconAnimations animations) {\n\t_selected = _pressed = SpecialOver::None;\n\t_iconState.x.finish();\n\t_iconState.selectionX.finish();\n\t_iconState.selectionWidth.finish();\n\t_iconState.animationStart = 0;\n\t_iconState.animation.stop();\n\tif (_icons.size() > 1\n\t\t&& _icons[1].setId == EmojiSectionSetId(EmojiSection::People)) {\n\t\t_singleWidth = (width() - _iconsLeft - _iconsRight) / _icons.size();\n\t} else {\n\t\t_singleWidth = st().iconWidth;\n\t}\n\t_areaPosition = QPoint(\n\t\t(_singleWidth - st().iconArea) / 2,\n\t\t(st().footer - st().iconArea) / 2);\n\trefreshScrollableDimensions();\n\trefreshSubiconsGeometry();\n\t_iconState.selected = _subiconState.selected = -2;\n\tvalidateSelectedIcon(activeSetId, animations);\n\tupdate();\n}\n\nvoid StickersListFooter::refreshSubiconsGeometry() {\n\tusing Section = Ui::Emoji::Section;\n\t_subiconState.x.finish();\n\t_subiconState.animationStart = 0;\n\t_subiconState.animation.stop();\n\tconst auto half = _singleWidth / 2;\n\tconst auto count = int(Section::Symbols) - int(Section::Recent);\n\tconst auto widthMax = count * _singleWidth;\n\tconst auto widthMin = 5 * _singleWidth + half;\n\tconst auto collapsedWidth = int(_icons.size()) * _singleWidth;\n\t_subiconsWidth = std::clamp(\n\t\twidth() + _singleWidth - collapsedWidth,\n\t\twidthMin,\n\t\twidthMax);\n\tif (_subiconsWidth < widthMax) {\n\t\t_subiconsWidth = half\n\t\t+ (((_subiconsWidth - half) / _singleWidth) * _singleWidth);\n\t}\n\t_subiconState.max = std::max(\n\t\twidthMax - _subiconsWidth,\n\t\t0);\n\tif (_subiconState.x.current() > _subiconState.max) {\n\t\t_subiconState.x = anim::value(_subiconState.max, _subiconState.max);\n\t}\n\tupdateEmojiWidthCallback();\n}\n\nvoid StickersListFooter::paintStickerSettingsIcon(QPainter &p) const {\n\tconst auto settingsLeft = _iconsLeft - _singleWidth;\n\tst().icons.settings.paint(\n\t\tp,\n\t\t(settingsLeft + (_singleWidth - st().icons.settings.width()) / 2),\n\t\t_iconsTop + st::emojiCategoryIconTop,\n\t\twidth());\n}\n\nvoid StickersListFooter::customEmojiRepaint() {\n\tif (!_repaintScheduled) {\n\t\t_repaintScheduled = true;\n\t\tupdate();\n\t}\n}\n\nvoid StickersListFooter::validateIconLottieAnimation(\n\t\tconst StickerIcon &icon) {\n\ticon.ensureMediaCreated();\n\tif (icon.lottie\n\t\t|| !icon.sticker\n\t\t|| !HasLottieThumbnail(\n\t\t\ticon.set ? icon.set->thumbnailType() : StickerType(),\n\t\t\ticon.thumbnailMedia.get(),\n\t\t\ticon.stickerMedia.get())) {\n\t\treturn;\n\t}\n\tauto player = LottieThumbnail(\n\t\ticon.thumbnailMedia.get(),\n\t\ticon.stickerMedia.get(),\n\t\tStickerLottieSize::StickersFooter,\n\t\tQSize(icon.pixw, icon.pixh) * style::DevicePixelRatio(),\n\t\t_renderer());\n\tif (!player) {\n\t\treturn;\n\t}\n\ticon.lottie = std::move(player);\n\n\tconst auto id = icon.setId;\n\ticon.lottie->updates(\n\t) | rpl::on_next([=] {\n\t\tupdateSetIcon(id);\n\t}, icon.lifetime);\n}\n\nvoid StickersListFooter::validateIconWebmAnimation(\n\t\tconst StickerIcon &icon) {\n\ticon.ensureMediaCreated();\n\tif (icon.webm\n\t\t|| !icon.sticker\n\t\t|| !HasWebmThumbnail(\n\t\t\ticon.set ? icon.set->thumbnailType() : StickerType(),\n\t\t\ticon.thumbnailMedia.get(),\n\t\t\ticon.stickerMedia.get())) {\n\t\treturn;\n\t}\n\tconst auto id = icon.setId;\n\tauto callback = [=](Media::Clip::Notification notification) {\n\t\tclipCallback(notification, id);\n\t};\n\ticon.webm = WebmThumbnail(\n\t\ticon.thumbnailMedia.get(),\n\t\ticon.stickerMedia.get(),\n\t\tstd::move(callback));\n}\n\nvoid StickersListFooter::validateIconAnimation(\n\t\tconst StickerIcon &icon) {\n\tconst auto emoji = icon.sticker;\n\tif (emoji && emoji->sticker()->setType == Data::StickersType::Emoji) {\n\t\tif (!icon.custom) {\n\t\t\tconst auto tag = Data::CustomEmojiManager::SizeTag::SetIcon;\n\t\t\tauto &manager = emoji->owner().customEmojiManager();\n\t\t\ticon.custom = manager.create(\n\t\t\t\temoji->id,\n\t\t\t\t[=] { customEmojiRepaint(); },\n\t\t\t\ttag);\n\t\t}\n\t\treturn;\n\t}\n\tvalidateIconWebmAnimation(icon);\n\tvalidateIconLottieAnimation(icon);\n}\n\nvoid StickersListFooter::updateSetIcon(uint64 setId) {\n\tenumerateVisibleIcons([&](const IconInfo &info) {\n\t\tif (_icons[info.index].setId != setId) {\n\t\t\treturn;\n\t\t}\n\t\tupdateSetIconAt(info.adjustedLeft);\n\t});\n}\n\nvoid StickersListFooter::updateSetIconAt(int left) {\n\tupdate(left, _iconsTop, _singleWidth, st().footer);\n}\n\nvoid StickersListFooter::paintSetIcon(\n\t\tPainter &p,\n\t\tconst ExpandingContext &context,\n\t\tconst IconInfo &info,\n\t\tcrl::time now,\n\t\tbool paused) const {\n\tconst auto &icon = _icons[info.index];\n\tconst auto expandingShift = context.expanding\n\t\t? QPoint(\n\t\t\t0,\n\t\t\tanim::interpolate(height() / 2, 0, context.progress))\n\t\t: QPoint();\n\tif (icon.sticker) {\n\t\ticon.ensureMediaCreated();\n\t\tconst_cast<StickersListFooter*>(this)->validateIconAnimation(icon);\n\t}\n\tif (context.expanding) {\n\t\tif (icon.custom) {\n\t\t\tp.translate(expandingShift);\n\t\t} else {\n\t\t\tp.save();\n\t\t\tconst auto center = QPoint(\n\t\t\t\tinfo.adjustedLeft + _singleWidth / 2,\n\t\t\t\t_iconsTop + st().footer / 2);\n\t\t\tp.translate(expandingShift + center);\n\t\t\tp.scale(context.progress, context.progress);\n\t\t\tp.translate(-center);\n\t\t}\n\t}\n\tif (icon.sticker) {\n\t\tprepareSetIcon(context, info, now, paused);\n\t\tp.drawImage(info.adjustedLeft, _iconsTop, _setIconCache);\n\t} else {\n\t\tp.translate(info.adjustedLeft, _iconsTop);\n\t\tpaintSetIconToCache(p, context, info, now, paused);\n\t\tp.translate(-info.adjustedLeft, -_iconsTop);\n\t}\n\tif (context.expanding) {\n\t\tif (icon.custom) {\n\t\t\tp.translate(-expandingShift);\n\t\t} else {\n\t\t\tp.restore();\n\t\t}\n\t}\n}\n\nvoid StickersListFooter::prepareSetIcon(\n\t\tconst ExpandingContext &context,\n\t\tconst IconInfo &info,\n\t\tcrl::time now,\n\t\tbool paused) const {\n\t_setIconCache.fill(Qt::transparent);\n\tauto p = Painter(&_setIconCache);\n\tpaintSetIconToCache(p, context, info, now, paused);\n\tif (!_icons[info.index].sticker) {\n\t\treturn;\n\t}\n\t// Rounding the corners.\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\tp.setBrush(Qt::NoBrush);\n\tauto pen = QPen(Qt::transparent);\n\tpen.setWidth(style::ConvertScaleExact(4.));\n\tp.setPen(pen);\n\tconst auto area = st().iconArea;\n\tauto rect = QRect(_areaPosition, QSize(area, area));\n\tp.drawRoundedRect(rect, st::emojiPanRadius, st::emojiPanRadius);\n}\n\nvoid StickersListFooter::paintSetIconToCache(\n\t\tPainter &p,\n\t\tconst ExpandingContext &context,\n\t\tconst IconInfo &info,\n\t\tcrl::time now,\n\t\tbool paused) const {\n\tconst auto &icon = _icons[info.index];\n\tif (icon.sticker) {\n\t\tconst auto origin = icon.sticker->stickerSetOrigin();\n\t\tconst auto thumb = icon.thumbnailMedia\n\t\t\t? icon.thumbnailMedia->image()\n\t\t\t: icon.stickerMedia\n\t\t\t? icon.stickerMedia->thumbnail()\n\t\t\t: nullptr;\n\t\tconst auto x = (_singleWidth - icon.pixw) / 2;\n\t\tconst auto y = (st().footer - icon.pixh) / 2;\n\t\tif (icon.custom) {\n\t\t\ticon.custom->paint(p, Ui::Text::CustomEmoji::Context{\n\t\t\t\t.textColor = (_customTextColor\n\t\t\t\t\t? _customTextColor()\n\t\t\t\t\t: st().textFg->c),\n\t\t\t\t.size = QSize(icon.pixw, icon.pixh),\n\t\t\t\t.now = now,\n\t\t\t\t.scale = context.progress,\n\t\t\t\t.position = { x, y },\n\t\t\t\t.paused = paused,\n\t\t\t\t.scaled = context.expanding,\n\t\t\t\t.internal = { .forceFirstFrame = _forceFirstFrame },\n\t\t\t});\n\t\t} else if (icon.lottie && icon.lottie->ready()) {\n\t\t\tconst auto frame = icon.lottie->frame();\n\t\t\tconst auto size = frame.size() / style::DevicePixelRatio();\n\t\t\tif (icon.savedFrame.isNull()) {\n\t\t\t\ticon.savedFrame = frame;\n\t\t\t\ticon.savedFrame.setDevicePixelRatio(\n\t\t\t\t\tstyle::DevicePixelRatio());\n\t\t\t}\n\t\t\tp.drawImage(\n\t\t\t\tQRect(\n\t\t\t\t\t(_singleWidth - size.width()) / 2,\n\t\t\t\t\t(st().footer - size.height()) / 2,\n\t\t\t\t\tsize.width(),\n\t\t\t\t\tsize.height()),\n\t\t\t\tframe);\n\t\t\tif (!paused) {\n\t\t\t\ticon.lottie->markFrameShown();\n\t\t\t}\n\t\t} else if (icon.webm && icon.webm->started()) {\n\t\t\tconst auto frame = icon.webm->current(\n\t\t\t\t{ .frame = { icon.pixw, icon.pixh }, .keepAlpha = true },\n\t\t\t\tpaused ? 0 : now);\n\t\t\tif (icon.savedFrame.isNull()) {\n\t\t\t\ticon.savedFrame = frame;\n\t\t\t\ticon.savedFrame.setDevicePixelRatio(\n\t\t\t\t\tstyle::DevicePixelRatio());\n\t\t\t}\n\t\t\tp.drawImage(x, y, frame);\n\t\t} else if (!icon.savedFrame.isNull()) {\n\t\t\tp.drawImage(x, y, icon.savedFrame);\n\t\t} else if (thumb) {\n\t\t\tconst auto pixmap = (!icon.lottie && thumb)\n\t\t\t\t? thumb->pix(icon.pixw, icon.pixh)\n\t\t\t\t: QPixmap();\n\t\t\tif (pixmap.isNull()) {\n\t\t\t\treturn;\n\t\t\t} else if (icon.savedFrame.isNull()) {\n\t\t\t\ticon.savedFrame = pixmap.toImage();\n\t\t\t}\n\t\t\tp.drawPixmapLeft(x, y, width(), pixmap);\n\t\t}\n\t} else if (icon.megagroup) {\n\t\tconst auto size = st::stickerGroupCategorySize;\n\t\ticon.megagroup->paintUserpicLeft(\n\t\t\tp,\n\t\t\ticon.megagroupUserpic,\n\t\t\t(_singleWidth - size) / 2,\n\t\t\t(st().footer - size) / 2,\n\t\t\twidth(),\n\t\t\tst::stickerGroupCategorySize);\n\t} else {\n\t\tusing Section = Ui::Emoji::Section;\n\t\tconst auto sectionIcon = [&](Section section, bool active) {\n\t\t\tconst auto icons = std::array{\n\t\t\t\t&st().icons.recent,\n\t\t\t\t&st().icons.recentActive,\n\t\t\t\t&st().icons.people,\n\t\t\t\t&st().icons.peopleActive,\n\t\t\t\t&st().icons.nature,\n\t\t\t\t&st().icons.natureActive,\n\t\t\t\t&st().icons.food,\n\t\t\t\t&st().icons.foodActive,\n\t\t\t\t&st().icons.activity,\n\t\t\t\t&st().icons.activityActive,\n\t\t\t\t&st().icons.travel,\n\t\t\t\t&st().icons.travelActive,\n\t\t\t\t&st().icons.objects,\n\t\t\t\t&st().icons.objectsActive,\n\t\t\t\t&st().icons.symbols,\n\t\t\t\t&st().icons.symbolsActive,\n\t\t\t};\n\t\t\tconst auto index = int(section) * 2 + (active ? 1 : 0);\n\n\t\t\tAssert(index >= 0 && index < icons.size());\n\t\t\treturn icons[index];\n\t\t};\n\t\tconst auto paintOne = [&](int left, const style::icon *icon) {\n\t\t\tleft += (_singleWidth - icon->width()) / 2;\n\t\t\tconst auto top = (st().footer - icon->height()) / 2;\n\t\t\tif (_customTextColor) {\n\t\t\t\ticon->paint(p, left, top, width(), _customTextColor());\n\t\t\t} else {\n\t\t\t\ticon->paint(p, left, top, width());\n\t\t\t}\n\t\t};\n\t\tif (_icons[info.index].setId == AllEmojiSectionSetId()\n\t\t\t&& info.width > _singleWidth) {\n\t\t\tconst auto skip = st::emojiIconSelectSkip;\n\t\t\tp.save();\n\t\t\tp.setClipRect(\n\t\t\t\tskip,\n\t\t\t\t_iconsTop,\n\t\t\t\tinfo.width - 2 * skip,\n\t\t\t\tst().footer,\n\t\t\t\tQt::IntersectClip);\n\t\t\tenumerateSubicons([&](const IconInfo &info) {\n\t\t\t\tif (info.visible) {\n\t\t\t\t\tpaintOne(\n\t\t\t\t\t\tinfo.adjustedLeft,\n\t\t\t\t\t\tsectionIcon(\n\t\t\t\t\t\t\tSection(int(Section::People) + info.index),\n\t\t\t\t\t\t\t(_subiconState.selected == info.index)));\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\t\t\tp.restore();\n\t\t} else {\n\t\t\tpaintOne(0, [&] {\n\t\t\t\tconst auto selected = (info.index == _iconState.selected);\n\t\t\t\tif (icon.setId == AllEmojiSectionSetId()) {\n\t\t\t\t\treturn &st().icons.people;\n\t\t\t\t} else if (const auto section = SetIdEmojiSection(icon.setId)) {\n\t\t\t\t\treturn sectionIcon(*section, selected);\n\t\t\t\t} else if (icon.setId == Data::Stickers::CollectibleSetId) {\n\t\t\t\t\treturn &st().icons.collectibles;\n\t\t\t\t}\n\t\t\t\treturn sectionIcon(Section::Recent, selected);\n\t\t\t}());\n\t\t}\n\t}\n}\n\nLocalStickersManager::LocalStickersManager(not_null<Main::Session*> session)\n: _session(session)\n, _api(&session->mtp()) {\n}\n\nvoid LocalStickersManager::install(uint64 setId) {\n\tconst auto &sets = _session->data().stickers().sets();\n\tconst auto it = sets.find(setId);\n\tif (it == sets.cend()) {\n\t\treturn;\n\t}\n\tconst auto set = it->second.get();\n\tconst auto input = set->mtpInput();\n\tif (!(set->flags & Data::StickersSetFlag::NotLoaded)\n\t\t&& !set->stickers.empty()) {\n\t\tsendInstallRequest(setId, input);\n\t\treturn;\n\t}\n\t_api.request(MTPmessages_GetStickerSet(\n\t\tinput,\n\t\tMTP_int(0) // hash\n\t)).done([=](const MTPmessages_StickerSet &result) {\n\t\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\t_session->data().stickers().feedSetFull(data);\n\t\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t\t});\n\t\tsendInstallRequest(setId, input);\n\t}).send();\n}\n\nbool LocalStickersManager::isInstalledLocally(uint64 setId) const {\n\treturn _installedLocallySets.contains(setId);\n}\n\nvoid LocalStickersManager::sendInstallRequest(\n\t\tuint64 setId,\n\t\tconst MTPInputStickerSet &input) {\n\t_api.request(MTPmessages_InstallStickerSet(\n\t\tinput,\n\t\tMTP_bool(false)\n\t)).done([=](const MTPmessages_StickerSetInstallResult &result) {\n\t\tif (result.type() == mtpc_messages_stickerSetInstallResultArchive) {\n\t\t\t_session->data().stickers().applyArchivedResult(\n\t\t\t\tresult.c_messages_stickerSetInstallResultArchive());\n\t\t}\n\t}).fail([=] {\n\t\tnotInstalledLocally(setId);\n\t\t_session->data().stickers().undoInstallLocally(setId);\n\t}).send();\n\n\tinstalledLocally(setId);\n\t_session->data().stickers().installLocally(setId);\n}\n\nvoid LocalStickersManager::installedLocally(uint64 setId) {\n\t_installedLocallySets.insert(setId);\n}\n\nvoid LocalStickersManager::notInstalledLocally(uint64 setId) {\n\t_installedLocallySets.remove(setId);\n}\n\nvoid LocalStickersManager::removeInstalledLocally(uint64 setId) {\n\t_installedLocallySets.remove(setId);\n}\n\nbool LocalStickersManager::clearInstalledLocally() {\n\tif (_installedLocallySets.empty()) {\n\t\treturn false;\n\t}\n\t_installedLocallySets.clear();\n\treturn true;\n}\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/stickers_list_footer.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"chat_helpers/compose/compose_features.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"media/clip/media_clip_reader.h\"\n#include \"mtproto/sender.h\"\n#include \"ui/dpr/dpr_image.h\"\n#include \"ui/round_rect.h\"\n#include \"ui/userpic_view.h\"\n\nnamespace Ui {\nclass InputField;\nclass CrossButton;\n} // namespace Ui\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nnamespace Data {\nclass StickersSet;\nclass StickersSetThumbnailView;\nclass DocumentMedia;\n} // namespace Data\n\nnamespace Lottie {\nclass SinglePlayer;\nclass FrameRenderer;\n} // namespace Lottie\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace style {\nstruct EmojiPan;\n} // namespace style\n\nnamespace ChatHelpers {\n\nenum class ValidateIconAnimations {\n\tFull,\n\tScroll,\n\tNone,\n};\n\n[[nodiscard]] uint64 EmojiSectionSetId(Ui::Emoji::Section section);\n[[nodiscard]] uint64 RecentEmojiSectionSetId();\n[[nodiscard]] uint64 AllEmojiSectionSetId();\n[[nodiscard]] uint64 SearchEmojiSectionSetId();\n[[nodiscard]] std::optional<Ui::Emoji::Section> SetIdEmojiSection(uint64 id);\n\nstruct GifSection {\n\tDocumentData *document = nullptr;\n\tEmojiPtr emoji;\n\n\tfriend inline constexpr auto operator<=>(\n\t\tGifSection,\n\t\tGifSection) = default;\n};\n[[nodiscard]] rpl::producer<std::vector<GifSection>> GifSectionsValue(\n\tnot_null<Main::Session*> session);\n\n[[nodiscard]] std::vector<EmojiPtr> SearchEmoji(\n\tconst std::vector<QString> &query,\n\tbase::flat_set<EmojiPtr> &outResultSet);\n\nstruct StickerIcon {\n\texplicit StickerIcon(uint64 setId);\n\tStickerIcon(\n\t\tnot_null<Data::StickersSet*> set,\n\t\tDocumentData *sticker,\n\t\tint pixw,\n\t\tint pixh);\n\tStickerIcon(StickerIcon&&);\n\tStickerIcon &operator=(StickerIcon&&);\n\t~StickerIcon();\n\n\tvoid ensureMediaCreated() const;\n\n\tuint64 setId = 0;\n\tData::StickersSet *set = nullptr;\n\tmutable std::unique_ptr<Lottie::SinglePlayer> lottie;\n\tmutable std::unique_ptr<Ui::Text::CustomEmoji> custom;\n\tmutable Media::Clip::ReaderPointer webm;\n\tmutable QImage savedFrame;\n\tDocumentData *sticker = nullptr;\n\tChannelData *megagroup = nullptr;\n\tmutable std::shared_ptr<Data::StickersSetThumbnailView> thumbnailMedia;\n\tmutable std::shared_ptr<Data::DocumentMedia> stickerMedia;\n\tmutable Ui::PeerUserpicView megagroupUserpic;\n\tint pixw = 0;\n\tint pixh = 0;\n\tmutable rpl::lifetime lifetime;\n};\n\nclass GradientPremiumStar {\npublic:\n\tGradientPremiumStar();\n\n\t[[nodiscard]] QImage image() const;\n\nprivate:\n\tvoid renderOnDemand() const;\n\n\tmutable QImage _image;\n\trpl::lifetime _lifetime;\n\n};\n\nclass StickersListFooter final : public TabbedSelector::InnerFooter {\npublic:\n\tstruct Descriptor {\n\t\tnot_null<Main::Session*> session;\n\t\tFn<QColor()> customTextColor;\n\t\tFn<bool()> paused;\n\t\tnot_null<RpWidget*> parent;\n\t\tconst style::EmojiPan *st = nullptr;\n\t\tComposeFeatures features;\n\t\tbool forceFirstFrame = false;\n\t};\n\texplicit StickersListFooter(Descriptor &&descriptor);\n\n\tvoid preloadImages();\n\tvoid validateSelectedIcon(\n\t\tuint64 setId,\n\t\tValidateIconAnimations animations);\n\tvoid refreshIcons(\n\t\tstd::vector<StickerIcon> icons,\n\t\tuint64 activeSetId,\n\t\tFn<std::shared_ptr<Lottie::FrameRenderer>()> renderer,\n\t\tValidateIconAnimations animations);\n\n\tvoid leaveToChildEvent(QEvent *e, QWidget *child) override;\n\n\tvoid clearHeavyData();\n\n\t[[nodiscard]] rpl::producer<uint64> setChosen() const;\n\t[[nodiscard]] rpl::producer<> openSettingsRequests() const;\n\n\tvoid paintExpanding(\n\t\tPainter &p,\n\t\tQRect clip,\n\t\tfloat64 radius,\n\t\tRectPart origin);\n\n\t[[nodiscard]] static int IconFrameSize();\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tbool eventHook(QEvent *e) override;\n\n\tvoid processHideFinished() override;\n\nprivate:\n\tenum class SpecialOver {\n\t\tNone,\n\t\tSettings,\n\t};\n\tstruct IconId {\n\t\tint index = 0;\n\t\tint subindex = 0;\n\n\t\tfriend inline bool operator==(IconId a, IconId b) {\n\t\t\treturn (a.index == b.index) && (a.subindex == b.subindex);\n\t\t}\n\t};\n\tusing OverState = std::variant<SpecialOver, IconId>;\n\tstruct IconInfo {\n\t\tint index = 0;\n\t\tint left = 0;\n\t\tint adjustedLeft = 0;\n\t\tint width = 0;\n\t\tbool visible = false;\n\t};\n\tstruct ScrollState {\n\t\ttemplate <typename UpdateCallback>\n\t\texplicit ScrollState(UpdateCallback &&callback);\n\n\t\tbool animationCallback(crl::time now);\n\n\t\tint selected = 0;\n\t\tint max = 0;\n\t\tint draggingStartX = 0;\n\t\tbool dragging = false;\n\t\tanim::value x;\n\t\tanim::value selectionX;\n\t\tanim::value selectionWidth;\n\t\tcrl::time animationStart = 0;\n\t\tUi::Animations::Basic animation;\n\t};\n\tstruct ExpandingContext {\n\t\tQRect clip;\n\t\tfloat64 progress = 0.;\n\t\tint radius = 0;\n\t\tbool expanding = false;\n\t};\n\n\tvoid enumerateVisibleIcons(Fn<void(const IconInfo &)> callback) const;\n\tvoid enumerateIcons(Fn<bool(const IconInfo &)> callback) const;\n\tvoid enumerateSubicons(Fn<bool(const IconInfo &)> callback) const;\n\t[[nodiscard]] IconInfo iconInfo(int index) const;\n\t[[nodiscard]] IconInfo subiconInfo(int index) const;\n\n\t[[nodiscard]] std::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();\n\tvoid setSelectedIcon(\n\t\tint newSelected,\n\t\tValidateIconAnimations animations);\n\tvoid setSelectedSubicon(\n\t\tint newSelected,\n\t\tValidateIconAnimations animations);\n\tvoid validateIconLottieAnimation(const StickerIcon &icon);\n\tvoid validateIconWebmAnimation(const StickerIcon &icon);\n\tvoid validateIconAnimation(const StickerIcon &icon);\n\tvoid customEmojiRepaint();\n\n\tvoid refreshIconsGeometry(\n\t\tuint64 activeSetId,\n\t\tValidateIconAnimations animations);\n\tvoid refreshSubiconsGeometry();\n\tvoid refreshScrollableDimensions();\n\tvoid updateSelected();\n\tvoid updateSetIcon(uint64 setId);\n\tvoid updateSetIconAt(int left);\n\tvoid checkDragging(ScrollState &state);\n\tbool finishDragging(ScrollState &state);\n\tbool finishDragging();\n\n\tvoid paint(Painter &p, const ExpandingContext &context) const;\n\tvoid paintStickerSettingsIcon(QPainter &p) const;\n\tvoid paintSetIcon(\n\t\tPainter &p,\n\t\tconst ExpandingContext &context,\n\t\tconst IconInfo &info,\n\t\tcrl::time now,\n\t\tbool paused) const;\n\tvoid prepareSetIcon(\n\t\tconst ExpandingContext &context,\n\t\tconst IconInfo &info,\n\t\tcrl::time now,\n\t\tbool paused) const;\n\tvoid paintSetIconToCache(\n\t\tPainter &p,\n\t\tconst ExpandingContext &context,\n\t\tconst IconInfo &info,\n\t\tcrl::time now,\n\t\tbool paused) const;\n\tvoid paintSelectionBg(\n\t\tQPainter &p,\n\t\tconst ExpandingContext &context) const;\n\tvoid paintLeftRightFading(\n\t\tQPainter &p,\n\t\tconst ExpandingContext &context) const;\n\n\tvoid updateEmojiSectionWidth();\n\tvoid updateEmojiWidthCallback();\n\n\tvoid scrollByWheelEvent(not_null<QWheelEvent*> e);\n\n\tvoid validateFadeLeft(int leftWidth) const;\n\tvoid validateFadeRight(int rightWidth) const;\n\tvoid validateFadeMask() const;\n\n\tvoid clipCallback(Media::Clip::Notification notification, uint64 setId);\n\n\tconst not_null<Main::Session*> _session;\n\tconst Fn<QColor()> _customTextColor;\n\tconst Fn<bool()> _paused;\n\tconst ComposeFeatures _features;\n\n\tstatic constexpr auto kVisibleIconsCount = 8;\n\n\tstd::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;\n\tstd::vector<StickerIcon> _icons;\n\tFn<std::shared_ptr<Lottie::FrameRenderer>()> _renderer;\n\tuint64 _activeByScrollId = 0;\n\tOverState _selected = SpecialOver::None;\n\tOverState _pressed = SpecialOver::None;\n\n\tQPoint _iconsMousePos, _iconsMouseDown;\n\tint _iconsLeft = 0;\n\tint _iconsRight = 0;\n\tint _iconsTop = 0;\n\tint _singleWidth = 0;\n\tQPoint _areaPosition;\n\n\tmutable QImage _fadeLeftCache;\n\tmutable QColor _fadeLeftColor;\n\tmutable QImage _fadeRightCache;\n\tmutable QColor _fadeRightColor;\n\tmutable QImage _fadeMask;\n\tmutable QImage _setIconCache;\n\n\tScrollState _iconState;\n\tScrollState _subiconState;\n\n\tUi::RoundRect _selectionBg, _subselectionBg;\n\tUi::Animations::Simple _subiconsWidthAnimation;\n\tint _subiconsWidth = 0;\n\tbool _subiconsExpanded = false;\n\tbool _repaintScheduled = false;\n\tbool _forceFirstFrame = false;\n\n\trpl::event_stream<> _openSettingsRequests;\n\trpl::event_stream<uint64> _setChosen;\n\n};\n\nclass LocalStickersManager final {\npublic:\n\texplicit LocalStickersManager(not_null<Main::Session*> session);\n\n\tvoid install(uint64 setId);\n\t[[nodiscard]] bool isInstalledLocally(uint64 setId) const;\n\tvoid removeInstalledLocally(uint64 setId);\n\tbool clearInstalledLocally();\n\nprivate:\n\tvoid sendInstallRequest(\n\t\tuint64 setId,\n\t\tconst MTPInputStickerSet &input);\n\tvoid installedLocally(uint64 setId);\n\tvoid notInstalledLocally(uint64 setId);\n\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\n\tbase::flat_set<uint64> _installedLocallySets;\n\n};\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/stickers_list_widget.h\"\n\n#include \"core/core_settings.h\"\n#include \"base/options.h\"\n#include \"base/timer_rpl.h\"\n#include \"core/application.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_cloud_file.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_peer_values.h\"\n#include \"menu/menu_send.h\" // SendMenu::FillSendMenu\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"chat_helpers/stickers_list_footer.h\"\n#include \"ui/controls/tabbed_search.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/image/image.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/power_saving.h\"\n#include \"lottie/lottie_multi_player.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"lottie/lottie_animation.h\"\n#include \"boxes/stickers_box.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"storage/storage_account.h\"\n#include \"lang/lang_keys.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"boxes/stickers_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/painter.h\"\n#include \"window/window_session_controller.h\" // GifPauseReason.\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"media/clip/media_clip_reader.h\"\n#include \"apiwrap.h\"\n#include \"api/api_toggling_media.h\" // Api::ToggleFavedSticker\n#include \"api/api_premium.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"core/application.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace ChatHelpers {\n\n[[nodiscard]] QVector<MTPstring> SearchStickersLangCodes() {\n\tauto result = QVector<MTPstring>();\n\tif (const auto method = QGuiApplication::inputMethod()) {\n\t\tfor (const auto &lang : method->locale().uiLanguages()) {\n\t\t\tresult.push_back(MTP_string(lang));\n\t\t}\n\t}\n\treturn result;\n}\n\nnamespace {\n\nconstexpr auto kSearchRequestDelay = 400;\nconstexpr auto kRecentDisplayLimit = 20;\nconstexpr auto kPreloadOfficialPages = 4;\nconstexpr auto kOfficialLoadLimit = 40;\nconstexpr auto kMinRepaintDelay = crl::time(33);\nconstexpr auto kMinAfterScrollDelay = crl::time(33);\n\nusing Data::StickersSet;\nusing Data::StickersPack;\nusing Data::StickersSetThumbnailView;\nusing SetFlag = Data::StickersSetFlag;\n\nbase::options::toggle OptionUnlimitedRecentStickers({\n\t.id = kOptionUnlimitedRecentStickers,\n\t.name = \"Unlimited recent stickers\",\n\t.description = \"Display as much recent stickers as the server provides\",\n});\n\n[[nodiscard]] bool SetInMyList(Data::StickersSetFlags flags) {\n\treturn (flags & SetFlag::Installed) && !(flags & SetFlag::Archived);\n}\n\n} // namespace\n\nconst char kOptionUnlimitedRecentStickers[] = \"unlimited-recent-stickers\";\n\nstruct StickersListWidget::Sticker {\n\tnot_null<DocumentData*> document;\n\tstd::shared_ptr<Data::DocumentMedia> documentMedia;\n\tLottie::Animation *lottie = nullptr;\n\tMedia::Clip::ReaderPointer webm;\n\tQImage savedFrame;\n\tQSize savedFrameFor;\n\tQImage premiumLock;\n\n\tvoid ensureMediaCreated();\n};\n\nstruct StickersListWidget::Set {\n\tSet(\n\t\tuint64 id,\n\t\tData::StickersSet *set,\n\t\tData::StickersSetFlags flags,\n\t\tconst QString &title,\n\t\tconst QString &shortName,\n\t\tint count,\n\t\tbool externalLayout,\n\t\tstd::vector<Sticker> &&stickers = {});\n\tSet(Set &&other);\n\tSet &operator=(Set &&other);\n\t~Set();\n\n\tuint64 id = 0;\n\tData::StickersSet *set = nullptr;\n\tDocumentData *thumbnailDocument = nullptr;\n\tData::StickersSetFlags flags;\n\tQString title;\n\tQString shortName;\n\tstd::vector<Sticker> stickers;\n\tstd::unique_ptr<Ui::RippleAnimation> ripple;\n\tcrl::time lastUpdateTime = 0;\n\n\tstd::unique_ptr<Lottie::MultiPlayer> lottiePlayer;\n\trpl::lifetime lottieLifetime;\n\n\tint count = 0;\n\tbool externalLayout = false;\n};\n\nauto StickersListWidget::PrepareStickers(\n\tconst QVector<DocumentData*> &pack,\n\tbool skipPremium)\n-> std::vector<Sticker> {\n\treturn ranges::views::all(\n\t\tpack\n\t) | ranges::views::filter([&](DocumentData *document) {\n\t\treturn !skipPremium || !document->isPremiumSticker();\n\t}) | ranges::views::transform([](DocumentData *document) {\n\t\treturn Sticker{ document };\n\t}) | ranges::to_vector;\n}\n\nStickersListWidget::Set::Set(\n\tuint64 id,\n\tStickersSet *set,\n\tData::StickersSetFlags flags,\n\tconst QString &title,\n\tconst QString &shortName,\n\tint count,\n\tbool externalLayout,\n\tstd::vector<Sticker> &&stickers)\n: id(id)\n, set(set)\n, flags(flags)\n, title(title)\n, shortName(shortName)\n, stickers(std::move(stickers))\n, count(count)\n, externalLayout(externalLayout) {\n}\n\nStickersListWidget::Set::Set(Set &&other) = default;\nStickersListWidget::Set &StickersListWidget::Set::operator=(\n\tSet &&other) = default;\nStickersListWidget::Set::~Set() = default;\n\nvoid StickersListWidget::Sticker::ensureMediaCreated() {\n\tif (documentMedia) {\n\t\treturn;\n\t}\n\tdocumentMedia = document->createMediaView();\n}\n\nStickersListWidget::StickersListWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tPauseReason level,\n\tMode mode)\n: StickersListWidget(parent, {\n\t.show = controller->uiShow(),\n\t.mode = mode,\n\t.paused = Window::PausedIn(controller, level),\n}) {\n}\n\nStickersListWidget::StickersListWidget(\n\tQWidget *parent,\n\tStickersListDescriptor &&descriptor)\n: Inner(\n\tparent,\n\tdescriptor.st ? *descriptor.st : st::defaultEmojiPan,\n\tdescriptor.show,\n\tdescriptor.paused)\n, _mode(descriptor.mode)\n, _show(std::move(descriptor.show))\n, _features(descriptor.features)\n, _overBg(st::roundRadiusLarge, st().overBg)\n, _api(&session().mtp())\n, _localSetsManager(std::make_unique<LocalStickersManager>(&session()))\n, _customRecentIds(std::move(descriptor.customRecentList))\n, _section(Section::Stickers)\n, _isMasks(_mode == Mode::Masks)\n, _isEffects(_mode == Mode::MessageEffects)\n, _updateItemsTimer([=] { updateItems(); })\n, _updateSetsTimer([=] { updateSets(); })\n, _trendingAddBgOver(\n\tImageRoundRadius::Large,\n\tst::stickersTrendingAdd.textBgOver)\n, _trendingAddBg(ImageRoundRadius::Large, st::stickersTrendingAdd.textBg)\n, _inactiveButtonBg(\n\tImageRoundRadius::Large,\n\tst::stickersTrendingInstalled.textBg)\n, _groupCategoryAddBgOver(\n\tImageRoundRadius::Large,\n\tst::stickerGroupCategoryAdd.textBgOver)\n, _groupCategoryAddBg(\n\tImageRoundRadius::Large,\n\tst::stickerGroupCategoryAdd.textBg)\n, _pathGradient(std::make_unique<Ui::PathShiftGradient>(\n\tst().pathBg,\n\tst().pathFg,\n\t[=] { update(); }))\n, _megagroupSetAbout(st::columnMinimalWidthThird\n\t- st::emojiScroll.width\n\t- st().headerLeft)\n, _addText(tr::lng_stickers_featured_add(tr::now))\n, _addWidth(st::stickersTrendingAdd.style.font->width(_addText))\n, _installedText(tr::lng_stickers_featured_installed(tr::now))\n, _installedWidth(\n\tst::stickersTrendingInstalled.style.font->width(_installedText))\n, _settings(this, tr::lng_stickers_you_have(tr::now))\n, _previewTimer([=] { showPreview(); })\n, _premiumMark(std::make_unique<StickerPremiumMark>(\n\t&session(),\n\tst::stickersPremiumLock))\n, _searchRequestTimer([=] { sendSearchRequest(); }) {\n\tsetMouseTracking(true);\n\tif (st().bg->c.alpha() > 0) {\n\t\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\t}\n\n\tif (!_isMasks && !_isEffects) {\n\t\tsetupSearch();\n\t}\n\n\t_settings->addClickHandler([=] {\n\t\tif (const auto window = _show->resolveWindow()) {\n\t\t\t// While media viewer can't show StickersBox.\n\t\t\tusing Section = StickersBox::Section;\n\t\t\twindow->show(\n\t\t\t\tBox<StickersBox>(_show, Section::Installed, _isMasks));\n\t\t\tCore::App().hideMediaView();\n\t\t\tWindow::ActivateWindow(window);\n\t\t}\n\t});\n\n\tsession().downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tif (isVisible()) {\n\t\t\tupdateItems();\n\t\t\treadVisibleFeatured(getVisibleTop(), getVisibleBottom());\n\t\t}\n\t}, lifetime());\n\n\tsession().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::StickersSet\n\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\treturn (update.peer.get() == _megagroupSet);\n\t}) | rpl::on_next([=] {\n\t\trefreshStickers();\n\t}, lifetime());\n\n\tif (!_isEffects) {\n\t\tsession().data().stickers().recentUpdated(_isMasks\n\t\t\t? Data::StickersType::Masks\n\t\t\t: Data::StickersType::Stickers\n\t\t) | rpl::on_next([=] {\n\t\t\trefreshRecent();\n\t\t}, lifetime());\n\t}\n\n\tpositionValue(\n\t) | rpl::skip(1) | rpl::map_to(\n\t\tTabbedSelector::Action::Update\n\t) | rpl::start_to_stream(_choosingUpdated, lifetime());\n\n\tif (_isEffects) {\n\t\trefreshStickers();\n\t} else {\n\t\trpl::merge(\n\t\t\tData::AmPremiumValue(&session()) | rpl::to_empty,\n\t\t\tsession().api().premium().cloudSetUpdated()\n\t\t) | rpl::on_next([=] {\n\t\t\trefreshStickers();\n\t\t}, lifetime());\n\t}\n}\n\nrpl::producer<FileChosen> StickersListWidget::chosen() const {\n\treturn _chosen.events();\n}\n\nrpl::producer<> StickersListWidget::scrollUpdated() const {\n\treturn _scrollUpdated.events();\n}\n\nauto StickersListWidget::choosingUpdated() const\n-> rpl::producer<TabbedSelector::Action> {\n\treturn _choosingUpdated.events();\n}\n\nobject_ptr<TabbedSelector::InnerFooter> StickersListWidget::createFooter() {\n\tExpects(_footer == nullptr);\n\n\tconst auto footerPaused = [method = pausedMethod()] {\n\t\treturn On(PowerSaving::kStickersPanel) || method();\n\t};\n\n\tusing FooterDescriptor = StickersListFooter::Descriptor;\n\tauto result = object_ptr<StickersListFooter>(FooterDescriptor{\n\t\t.session = &session(),\n\t\t.paused = footerPaused,\n\t\t.parent = this,\n\t\t.st = &st(),\n\t\t.features = _features,\n\t});\n\t_footer = result;\n\n\t_footer->setChosen(\n\t) | rpl::on_next([=](uint64 setId) {\n\t\tshowStickerSet(setId);\n\t}, _footer->lifetime());\n\n\t_footer->openSettingsRequests(\n\t) | rpl::on_next([=] {\n\t\tconst auto onlyFeatured = !_isMasks && _mySets.empty();\n\t\t_show->showBox(Box<StickersBox>(\n\t\t\t_show,\n\t\t\t(onlyFeatured\n\t\t\t\t? StickersBox::Section::Featured\n\t\t\t\t: _isMasks\n\t\t\t\t? StickersBox::Section::Masks\n\t\t\t\t: StickersBox::Section::Installed),\n\t\t\tonlyFeatured ? false : _isMasks));\n\t}, _footer->lifetime());\n\n\treturn result;\n}\n\nvoid StickersListWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tconst auto top = getVisibleTop();\n\tInner::visibleTopBottomUpdated(visibleTop, visibleBottom);\n\tif (top != getVisibleTop()) {\n\t\t_lastScrolledAt = crl::now();\n\t\t_repaintSetsIds.clear();\n\t\tupdate();\n\t}\n\tif (_section == Section::Featured) {\n\t\tcheckVisibleFeatured(visibleTop, visibleBottom);\n\t} else {\n\t\tcheckVisibleLottie();\n\t\tif (_section == Section::Search) {\n\t\t\tcheckPaginateSearchStickers(visibleTop, visibleBottom);\n\t\t}\n\t}\n\tif (_footer) {\n\t\t_footer->validateSelectedIcon(\n\t\t\tcurrentSet(visibleTop),\n\t\t\tValidateIconAnimations::Full);\n\t}\n}\n\nvoid StickersListWidget::checkVisibleFeatured(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\treadVisibleFeatured(visibleTop, visibleBottom);\n\n\tconst auto visibleHeight = visibleBottom - visibleTop;\n\n\tif (visibleBottom > height() - visibleHeight * kPreloadOfficialPages) {\n\t\tpreloadMoreOfficial();\n\t}\n\n\tconst auto rowHeight = featuredRowHeight();\n\tconst auto destroyAbove = floorclamp(\n\t\tvisibleTop - visibleHeight,\n\t\trowHeight,\n\t\t0,\n\t\t_officialSets.size());\n\tconst auto destroyBelow = ceilclamp(\n\t\tvisibleBottom + visibleHeight,\n\t\trowHeight,\n\t\t0,\n\t\t_officialSets.size());\n\tfor (auto i = 0; i != destroyAbove; ++i) {\n\t\tclearHeavyIn(_officialSets[i]);\n\t}\n\tfor (auto i = destroyBelow; i != _officialSets.size(); ++i) {\n\t\tclearHeavyIn(_officialSets[i]);\n\t}\n}\n\nvoid StickersListWidget::preloadMoreOfficial() {\n\tif (_officialRequestId) {\n\t\treturn;\n\t}\n\t_officialRequestId = _api.request(MTPmessages_GetOldFeaturedStickers(\n\t\tMTP_int(_officialOffset),\n\t\tMTP_int(kOfficialLoadLimit),\n\t\tMTP_long(0) // hash\n\t)).done([=](const MTPmessages_FeaturedStickers &result) {\n\t\t_officialRequestId = 0;\n\t\tresult.match([&](const MTPDmessages_featuredStickersNotModified &d) {\n\t\t\tLOG((\"Api Error: messages.featuredStickersNotModified.\"));\n\t\t}, [&](const MTPDmessages_featuredStickers &data) {\n\t\t\tconst auto &list = data.vsets().v;\n\t\t\t_officialOffset += list.size();\n\t\t\tfor (int i = 0, l = list.size(); i != l; ++i) {\n\t\t\t\tconst auto set = session().data().stickers().feedSet(\n\t\t\t\t\tlist[i]);\n\t\t\t\tif (set->stickers.empty() && set->covers.empty()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto externalLayout = true;\n\t\t\t\tappendSet(\n\t\t\t\t\t_officialSets,\n\t\t\t\t\tset->id,\n\t\t\t\t\texternalLayout,\n\t\t\t\t\tAppendSkip::Installed);\n\t\t\t}\n\t\t});\n\t\tresizeToWidth(width());\n\t\trepaintItems();\n\t}).send();\n}\n\nvoid StickersListWidget::readVisibleFeatured(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tconst auto rowHeight = featuredRowHeight();\n\tconst auto rowFrom = floorclamp(\n\t\tvisibleTop,\n\t\trowHeight,\n\t\t0,\n\t\t_featuredSetsCount);\n\tconst auto rowTo = ceilclamp(\n\t\tvisibleBottom,\n\t\trowHeight,\n\t\t0,\n\t\t_featuredSetsCount);\n\tfor (auto i = rowFrom; i < rowTo; ++i) {\n\t\tauto &set = _officialSets[i];\n\t\tif (!(set.flags & SetFlag::Unread)) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (i * rowHeight < visibleTop\n\t\t\t|| (i + 1) * rowHeight > visibleBottom) {\n\t\t\tcontinue;\n\t\t}\n\t\tint count = qMin(int(set.stickers.size()), _columnCount);\n\t\tint loaded = 0;\n\t\tfor (int j = 0; j < count; ++j) {\n\t\t\tif (!set.stickers[j].document->hasThumbnail()\n\t\t\t\t|| !set.stickers[j].document->thumbnailLoading()\n\t\t\t\t|| (set.stickers[j].documentMedia\n\t\t\t\t\t&& set.stickers[j].documentMedia->loaded())) {\n\t\t\t\t++loaded;\n\t\t\t}\n\t\t}\n\t\tif (count > 0 && loaded == count) {\n\t\t\tsession().api().readFeaturedSetDelayed(set.id);\n\t\t}\n\t}\n}\n\nint StickersListWidget::featuredRowHeight() const {\n\treturn st::stickersTrendingHeader\n\t\t+ _singleSize.height()\n\t\t+ st::stickersTrendingSkip;\n}\n\ntemplate <typename Callback>\nbool StickersListWidget::enumerateSections(Callback callback) const {\n\tauto info = SectionInfo();\n\tinfo.top = _search ? _search->height() : 0;\n\tconst auto &sets = shownSets();\n\tfor (auto i = 0; i != sets.size(); ++i) {\n\t\tauto &set = sets[i];\n\t\tinfo.section = i;\n\t\tinfo.count = set.stickers.size();\n\t\tconst auto titleSkip = set.externalLayout\n\t\t\t? st::stickersTrendingHeader\n\t\t\t: setHasTitle(set)\n\t\t\t? st().header\n\t\t\t: st::stickerPanPadding;\n\t\tinfo.rowsTop = info.top + titleSkip;\n\t\tif (set.externalLayout) {\n\t\t\tinfo.rowsCount = 1;\n\t\t\tinfo.rowsBottom = info.top + featuredRowHeight();\n\t\t} else if (set.id == Data::Stickers::MegagroupSetId && !info.count) {\n\t\t\tinfo.rowsCount = 0;\n\t\t\tinfo.rowsBottom = info.rowsTop\n\t\t\t\t+ _megagroupSetButtonRect.y()\n\t\t\t\t+ _megagroupSetButtonRect.height()\n\t\t\t\t+ st::stickerGroupCategoryAddMargin.bottom();\n\t\t} else {\n\t\t\tinfo.rowsCount = (info.count / _columnCount)\n\t\t\t\t+ ((info.count % _columnCount) ? 1 : 0);\n\t\t\tinfo.rowsBottom = info.rowsTop\n\t\t\t\t+ info.rowsCount * _singleSize.height();\n\t\t}\n\t\tif (!callback(info)) {\n\t\t\treturn false;\n\t\t}\n\t\tinfo.top = info.rowsBottom;\n\t}\n\treturn true;\n}\n\nStickersListWidget::SectionInfo StickersListWidget::sectionInfo(\n\t\tint section) const {\n\tExpects(section >= 0 && section < shownSets().size());\n\n\tauto result = SectionInfo();\n\tenumerateSections([searchForSection = section, &result](\n\t\t\tconst SectionInfo &info) {\n\t\tif (info.section == searchForSection) {\n\t\t\tresult = info;\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n\treturn result;\n}\n\nStickersListWidget::SectionInfo StickersListWidget::sectionInfoByOffset(\n\t\tint yOffset) const {\n\tauto result = SectionInfo();\n\tenumerateSections([this, &result, yOffset](const SectionInfo &info) {\n\t\tif (yOffset < info.rowsBottom\n\t\t\t|| info.section == shownSets().size() - 1) {\n\t\t\tresult = info;\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n\treturn result;\n}\n\nint StickersListWidget::countDesiredHeight(int newWidth) {\n\tconst auto minSize = _isEffects\n\t\t? st::stickerEffectWidthMin\n\t\t: st::stickerPanWidthMin;\n\tif (newWidth < 2 * minSize) {\n\t\treturn 0;\n\t}\n\tauto availableWidth = newWidth\n\t\t- (st::stickerPanPadding - st().margin.left());\n\tauto columnCount = availableWidth / minSize;\n\tauto singleWidth = availableWidth / columnCount;\n\tauto fullWidth = (st().margin.left() + newWidth + st::emojiScroll.width);\n\tauto rowsRight = (fullWidth - columnCount * singleWidth) / 2;\n\taccumulate_max(rowsRight, st::emojiScroll.width);\n\t_rowsLeft = fullWidth\n\t\t- columnCount * singleWidth\n\t\t- rowsRight\n\t\t- st().margin.left();\n\t_singleSize = QSize(singleWidth, singleWidth);\n\tsetColumnCount(columnCount);\n\n\tauto visibleHeight = minimalHeight();\n\tauto minimalHeight = (visibleHeight - st::stickerPanPadding);\n\tauto countResult = [this](int minimalLastHeight) {\n\t\tconst auto &sets = shownSets();\n\t\tif (sets.empty()) {\n\t\t\treturn 0;\n\t\t}\n\t\tconst auto info = sectionInfo(sets.size() - 1);\n\t\treturn info.top\n\t\t\t+ qMax(info.rowsBottom - info.top, minimalLastHeight);\n\t};\n\tconst auto minimalLastHeight = (_section == Section::Stickers)\n\t\t? minimalHeight\n\t\t: 0;\n\tconst auto result = qMax(minimalHeight, countResult(minimalLastHeight));\n\treturn result ? (result + st::stickerPanPadding) : 0;\n}\n\nvoid StickersListWidget::sendSearchRequest() {\n\tif (_searchSetsRequestId\n\t\t|| _searchStickersRequestId\n\t\t|| _searchNextQuery.isEmpty()\n\t\t|| _isEffects) {\n\t\treturn;\n\t}\n\n\t_searchRequestTimer.cancel();\n\t_searchQuery = _searchNextQuery;\n\n\tauto it = _searchStickersCache.find(_searchQuery);\n\tif (it != _searchStickersCache.cend()) {\n\t\ttoggleSearchLoading(false);\n\t\treturn;\n\t}\n\ttoggleSearchLoading(true);\n\tif (_searchQuery == Ui::PremiumGroupFakeEmoticon()) {\n\t\ttoggleSearchLoading(false);\n\t\t_searchSetsRequestId = 0;\n\t\t_searchSetsCache.emplace(_searchQuery, std::vector<uint64>());\n\t\t_searchStickersCache.emplace(_searchQuery, std::vector<DocumentId>());\n\t\tshowSearchResults();\n\t\treturn;\n\t}\n\n\trequestSearchStickers(_searchQuery, 0, true);\n}\n\nvoid StickersListWidget::sendSearchSetsRequest(const QString &query) {\n\tconst auto hash = uint64(0);\n\t_searchSetsRequestId = _api.request(MTPmessages_SearchStickerSets(\n\t\tMTP_flags(0),\n\t\tMTP_string(query),\n\t\tMTP_long(hash)\n\t)).done([=](const MTPmessages_FoundStickerSets &result) {\n\t\tsearchResultsDone(query, result);\n\t}).fail([=] {\n\t\t_searchSetsRequestId = 0;\n\t\tif (_searchNextQuery == query) {\n\t\t\ttoggleSearchLoading(false);\n\t\t}\n\t}).handleAllErrors().send();\n}\n\nvoid StickersListWidget::requestSearchStickers(\n\t\tconst QString &query,\n\t\tint offset,\n\t\tbool requestSetsOnEmpty) {\n\tconst auto hash = uint64(0);\n\t_searchStickersRequestId = _api.request(MTPmessages_SearchStickers(\n\t\tMTP_flags(0),\n\t\tMTP_string(query),\n\t\tMTPstring(), // emoticon\n\t\tMTP_vector<MTPstring>(SearchStickersLangCodes()),\n\t\tMTP_int(offset),\n\t\tMTP_int(50),\n\t\tMTP_long(hash)\n\t)).done([=](const MTPmessages_FoundStickers &result) {\n\t\tsearchStickersResultsDone(\n\t\t\tquery,\n\t\t\toffset,\n\t\t\trequestSetsOnEmpty,\n\t\t\tresult);\n\t}).fail([=] {\n\t\t_searchStickersRequestId = 0;\n\t\tif (requestSetsOnEmpty) {\n\t\t\t_searchStickersCache.emplace(query, std::vector<DocumentId>());\n\t\t\tif (_searchNextQuery == query) {\n\t\t\t\tsendSearchSetsRequest(query);\n\t\t\t}\n\t\t}\n\t}).handleAllErrors().send();\n}\n\nvoid StickersListWidget::searchForSets(\n\t\tconst QString &query,\n\t\tstd::vector<EmojiPtr> emoji) {\n\tconst auto cleaned = query.trimmed();\n\tif (cleaned.isEmpty()) {\n\t\tcancelSetsSearch();\n\t\treturn;\n\t}\n\n\t_filterStickersCornerEmoji.clear();\n\tif (_isEffects) {\n\t\tfilterEffectsByEmoji(std::move(emoji));\n\t} else if (query == Ui::PremiumGroupFakeEmoticon()) {\n\t\t_filteredStickers = session().data().stickers().getPremiumList(0);\n\t} else {\n\t\t_filteredStickers = session().data().stickers().getListByEmoji(\n\t\t\tstd::move(emoji),\n\t\t\t0,\n\t\t\ttrue);\n\t}\n\tif (_searchQuery != cleaned) {\n\t\ttoggleSearchLoading(false);\n\t\tif (const auto requestId = base::take(_searchSetsRequestId)) {\n\t\t\t_api.request(requestId).cancel();\n\t\t}\n\t\tif (const auto requestId = base::take(_searchStickersRequestId)) {\n\t\t\t_api.request(requestId).cancel();\n\t\t}\n\t\tif (_searchStickersCache.find(cleaned) != _searchStickersCache.cend()\n\t\t\t|| _searchSetsCache.find(cleaned) != _searchSetsCache.cend()) {\n\t\t\t_searchRequestTimer.cancel();\n\t\t\t_searchQuery = _searchNextQuery = cleaned;\n\t\t} else {\n\t\t\t_searchNextQuery = cleaned;\n\t\t\t_searchRequestTimer.callOnce(kSearchRequestDelay);\n\t\t}\n\t\tshowSearchResults();\n\t}\n}\n\nvoid StickersListWidget::cancelSetsSearch() {\n\ttoggleSearchLoading(false);\n\tif (const auto requestId = base::take(_searchSetsRequestId)) {\n\t\t_api.request(requestId).cancel();\n\t}\n\tif (const auto requestId = base::take(_searchStickersRequestId)) {\n\t\t_api.request(requestId).cancel();\n\t}\n\t_searchRequestTimer.cancel();\n\t_searchQuery = _searchNextQuery = QString();\n\t_filteredStickers.clear();\n\t_filterStickersCornerEmoji.clear();\n\t_searchSetsCache.clear();\n\t_searchStickersCache.clear();\n\t_searchStickersNextOffset.clear();\n\trefreshSearchRows(nullptr);\n}\n\nvoid StickersListWidget::showSearchResults() {\n\trefreshSearchRows();\n\tscrollTo(0);\n}\n\nvoid StickersListWidget::refreshSearchRows() {\n\tauto it = _searchSetsCache.find(_searchNextQuery);\n\tauto sets = (it != end(_searchSetsCache))\n\t\t? &it->second\n\t\t: nullptr;\n\trefreshSearchRows(sets);\n}\n\nvoid StickersListWidget::refreshSearchRows(\n\t\tconst std::vector<uint64> *cloudSets) {\n\tclearSelection();\n\n\tconst auto wasSection = _section;\n\tauto wasSets = base::take(_searchSets);\n\tconst auto guard = gsl::finally([&] {\n\t\tif (_section == wasSection && _section == Section::Search) {\n\t\t\ttakeHeavyData(_searchSets, wasSets);\n\t\t}\n\t});\n\n\tconst auto foundStickersIt = _searchStickersCache.find(_searchNextQuery);\n\tconst auto hasCloudFoundStickers = true\n\t\t&& (foundStickersIt != _searchStickersCache.end())\n\t\t&& !foundStickersIt->second.empty();\n\n\tfillFilteredStickersRow();\n\n\tif (!_isEffects) {\n\t\tfillLocalSearchRows(_searchNextQuery);\n\t}\n\n\tif (hasCloudFoundStickers) {\n\t\tfillFoundStickersRow(foundStickersIt->second);\n\t}\n\tif (!cloudSets && _searchNextQuery.isEmpty()) {\n\t\tshowStickerSet(!_mySets.empty()\n\t\t\t? _mySets[0].id\n\t\t\t: Data::Stickers::FeaturedSetId);\n\t\treturn;\n\t}\n\n\tsetSection(Section::Search);\n\tif (!_isEffects && cloudSets) {\n\t\tfillCloudSearchRows(*cloudSets);\n\t}\n\trefreshIcons(ValidateIconAnimations::Scroll);\n\t_lastMousePosition = QCursor::pos();\n\n\tresizeToWidth(width());\n\t_recentShownCount = _filteredStickers.size();\n\tupdateSelected();\n}\n\nrpl::producer<int> StickersListWidget::recentShownCount() const {\n\treturn _recentShownCount.value();\n}\n\nvoid StickersListWidget::fillLocalSearchRows(const QString &query) {\n\tconst auto searchWordsList = TextUtilities::PrepareSearchWords(query);\n\tif (searchWordsList.isEmpty()) {\n\t\treturn;\n\t}\n\tauto searchWordInTitle = [](\n\t\t\tconst QStringList &titleWords,\n\t\t\tconst QString &searchWord) {\n\t\tfor (const auto &titleWord : titleWords) {\n\t\t\tif (titleWord.startsWith(searchWord)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\tauto allSearchWordsInTitle = [&](\n\t\t\tconst QStringList &titleWords) {\n\t\tfor (const auto &searchWord : searchWordsList) {\n\t\t\tif (!searchWordInTitle(titleWords, searchWord)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t};\n\n\tconst auto &sets = session().data().stickers().sets();\n\tfor (const auto &[setId, titleWords] : _searchIndex) {\n\t\tif (allSearchWordsInTitle(titleWords)) {\n\t\t\tif (const auto it = sets.find(setId); it != sets.end()) {\n\t\t\t\taddSearchRow(it->second.get());\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid StickersListWidget::fillCloudSearchRows(\n\t\tconst std::vector<uint64> &cloudSets) {\n\tconst auto &sets = session().data().stickers().sets();\n\tfor (const auto setId : cloudSets) {\n\t\tif (const auto it = sets.find(setId); it != sets.end()) {\n\t\t\taddSearchRow(it->second.get());\n\t\t}\n\t}\n}\n\nvoid StickersListWidget::fillFoundStickersRow(\n\t\tconst std::vector<DocumentId> &stickerIds) {\n\tif (stickerIds.empty()) {\n\t\treturn;\n\t}\n\tauto elements = std::vector<Sticker>();\n\telements.reserve(stickerIds.size());\n\tfor (const auto id : stickerIds) {\n\t\tif (const auto document = session().data().document(id)) {\n\t\t\telements.push_back(Sticker{ document });\n\t\t}\n\t}\n\tif (elements.empty()) {\n\t\treturn;\n\t}\n\n\t_searchSets.emplace_back(\n\t\tSearchEmojiSectionSetId(),\n\t\tnullptr,\n\t\tData::StickersSetFlag::Special,\n\t\tQString(),\n\t\tQString(),\n\t\telements.size(),\n\t\tfalse, // externalLayout\n\t\tstd::move(elements));\n}\n\nvoid StickersListWidget::fillFilteredStickersRow() {\n\tif (_filteredStickers.empty()) {\n\t\treturn;\n\t}\n\tauto elements = ranges::views::all(\n\t\t_filteredStickers\n\t) | ranges::views::transform([](not_null<DocumentData*> document) {\n\t\treturn Sticker{ document };\n\t}) | ranges::to_vector;\n\n\t_searchSets.emplace_back(\n\t\tSearchEmojiSectionSetId(),\n\t\tnullptr,\n\t\tData::StickersSetFlag::Special,\n\t\t_isEffects ? tr::lng_effect_stickers_title(tr::now) : QString(),\n\t\tQString(), // shortName\n\t\t_filteredStickers.size(),\n\t\tfalse, // externalLayout\n\t\tstd::move(elements));\n}\n\nvoid StickersListWidget::addSearchRow(not_null<StickersSet*> set) {\n\tconst auto skipPremium = !session().premiumPossible();\n\tauto elements = PrepareStickers(\n\t\tset->stickers.empty() ? set->covers : set->stickers,\n\t\tskipPremium);\n\t_searchSets.emplace_back(\n\t\tset->id,\n\t\tset,\n\t\tset->flags,\n\t\tset->title,\n\t\tset->shortName,\n\t\tset->count,\n\t\t!SetInMyList(set->flags),\n\t\tstd::move(elements));\n}\n\nvoid StickersListWidget::toggleSearchLoading(bool loading) {\n\tif (_search) {\n\t\t_search->setLoading(loading);\n\t}\n\tif (_searchLoading != loading) {\n\t\t_searchLoading = loading;\n\t\tupdate();\n\t}\n}\n\nvoid StickersListWidget::takeHeavyData(\n\t\tstd::vector<Set> &to,\n\t\tstd::vector<Set> &from) {\n\tauto indices = base::flat_map<uint64, int>();\n\tindices.reserve(from.size());\n\tauto index = 0;\n\tfor (const auto &set : from) {\n\t\tindices.emplace(set.id, index++);\n\t}\n\tfor (auto &toSet : to) {\n\t\tconst auto i = indices.find(toSet.id);\n\t\tif (i != end(indices)) {\n\t\t\ttakeHeavyData(toSet, from[i->second]);\n\t\t}\n\t}\n}\n\nvoid StickersListWidget::takeHeavyData(Set &to, Set &from) {\n\tto.lottiePlayer = std::move(from.lottiePlayer);\n\tto.lottieLifetime = std::move(from.lottieLifetime);\n\tauto &toList = to.stickers;\n\tauto &fromList = from.stickers;\n\tconst auto same = ranges::equal(\n\t\ttoList,\n\t\tfromList,\n\t\tranges::equal_to(),\n\t\t&Sticker::document,\n\t\t&Sticker::document);\n\tif (same) {\n\t\tfor (auto i = 0, count = int(toList.size()); i != count; ++i) {\n\t\t\ttakeHeavyData(toList[i], fromList[i]);\n\t\t}\n\t} else {\n\t\tauto indices = base::flat_map<not_null<DocumentData*>, int>();\n\t\tindices.reserve(fromList.size());\n\t\tauto index = 0;\n\t\tfor (const auto &fromSticker : fromList) {\n\t\t\tindices.emplace(fromSticker.document, index++);\n\t\t}\n\t\tfor (auto &toSticker : toList) {\n\t\t\tconst auto i = indices.find(toSticker.document);\n\t\t\tif (i != end(indices)) {\n\t\t\t\ttakeHeavyData(toSticker, fromList[i->second]);\n\t\t\t}\n\t\t}\n\t\tfor (const auto &sticker : fromList) {\n\t\t\tif (sticker.lottie && to.lottiePlayer) {\n\t\t\t\tto.lottiePlayer->remove(sticker.lottie);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid StickersListWidget::takeHeavyData(Sticker &to, Sticker &from) {\n\tto.documentMedia = std::move(from.documentMedia);\n\tto.savedFrame = std::move(from.savedFrame);\n\tto.savedFrameFor = from.savedFrameFor;\n\tto.lottie = base::take(from.lottie);\n\tto.webm = base::take(from.webm);\n}\n\nauto StickersListWidget::shownSets() const -> const std::vector<Set> & {\n\tswitch (_section) {\n\tcase Section::Featured: return _officialSets;\n\tcase Section::Search: return _searchSets;\n\tcase Section::Stickers: return _mySets;\n\t}\n\tUnexpected(\"Section in StickersListWidget.\");\n}\n\nauto StickersListWidget::shownSets() -> std::vector<Set> & {\n\tswitch (_section) {\n\tcase Section::Featured: return _officialSets;\n\tcase Section::Search: return _searchSets;\n\tcase Section::Stickers: return _mySets;\n\t}\n\tUnexpected(\"Section in StickersListWidget.\");\n}\n\nvoid StickersListWidget::searchStickersResultsDone(\n\t\tconst QString &query,\n\t\tint requestedOffset,\n\t\tbool requestSetsOnEmpty,\n\t\tconst MTPmessages_FoundStickers &result) {\n\t_searchStickersRequestId = 0;\n\tconst auto active = (_searchNextQuery == query);\n\n\tresult.match([&](const MTPDmessages_foundStickersNotModified &data) {\n\t\tLOG((\"API: messages.foundStickersNotModified.\"));\n\t\tif (const auto next = data.vnext_offset()) {\n\t\t\tif (next->v > requestedOffset) {\n\t\t\t\t_searchStickersNextOffset[query] = next->v;\n\t\t\t} else {\n\t\t\t\t_searchStickersNextOffset.erase(query);\n\t\t\t}\n\t\t} else {\n\t\t\t_searchStickersNextOffset.erase(query);\n\t\t}\n\t\t_searchStickersCache.emplace(query, std::vector<DocumentId>());\n\t\tif (!active) {\n\t\t\treturn;\n\t\t}\n\t\tif (requestSetsOnEmpty) {\n\t\t\tsendSearchSetsRequest(query);\n\t\t\treturn;\n\t\t}\n\t\trefreshSearchRows();\n\t\tcheckPaginateSearchStickers(\n\t\t\tgetVisibleTop(),\n\t\t\tgetVisibleBottom());\n\t}, [&](const MTPDmessages_foundStickers &data) {\n\t\tauto it = _searchStickersCache.find(query);\n\t\tif (it == _searchStickersCache.cend()) {\n\t\t\tit = _searchStickersCache.emplace(\n\t\t\t\tquery,\n\t\t\t\tstd::vector<DocumentId>()).first;\n\t\t}\n\n\t\tfor (const auto &sticker : data.vstickers().v) {\n\t\t\tif (const auto doc = session().data().processDocument(sticker)) {\n\t\t\t\tit->second.push_back(doc->id);\n\t\t\t}\n\t\t}\n\n\t\tif (const auto next = data.vnext_offset()) {\n\t\t\tif (next->v > requestedOffset) {\n\t\t\t\t_searchStickersNextOffset[query] = next->v;\n\t\t\t} else {\n\t\t\t\t_searchStickersNextOffset.erase(query);\n\t\t\t}\n\t\t} else {\n\t\t\t_searchStickersNextOffset.erase(query);\n\t\t}\n\n\t\tif (!active) {\n\t\t\treturn;\n\t\t}\n\t\tif (requestSetsOnEmpty && it->second.empty()) {\n\t\t\tsendSearchSetsRequest(query);\n\t\t\treturn;\n\t\t}\n\t\ttoggleSearchLoading(false);\n\t\tif (requestSetsOnEmpty) {\n\t\t\tshowSearchResults();\n\t\t} else {\n\t\t\trefreshSearchRows();\n\t\t}\n\t\tcheckPaginateSearchStickers(\n\t\t\tgetVisibleTop(),\n\t\t\tgetVisibleBottom());\n\t});\n}\n\nvoid StickersListWidget::loadMoreSearchStickers() {\n\tif (_searchStickersRequestId\n\t\t|| _searchQuery.isEmpty()\n\t\t|| _isEffects\n\t\t|| (_searchQuery != _searchNextQuery)) {\n\t\treturn;\n\t}\n\tconst auto query = _searchQuery;\n\tconst auto offsetIt = _searchStickersNextOffset.find(query);\n\tif (offsetIt == _searchStickersNextOffset.end()) {\n\t\treturn;\n\t}\n\trequestSearchStickers(query, offsetIt->second, false);\n}\n\nvoid StickersListWidget::checkPaginateSearchStickers(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tif (_section != Section::Search\n\t\t|| _searchQuery.isEmpty()\n\t\t|| (_searchQuery != _searchNextQuery)\n\t\t|| _searchStickersRequestId) {\n\t\treturn;\n\t}\n\tconst auto visibleHeight = visibleBottom - visibleTop;\n\tif (visibleHeight <= 0) {\n\t\treturn;\n\t}\n\tif (visibleBottom > height() - visibleHeight * kPreloadOfficialPages) {\n\t\tloadMoreSearchStickers();\n\t}\n}\n\nvoid StickersListWidget::searchResultsDone(\n\t\tconst QString &query,\n\t\tconst MTPmessages_FoundStickerSets &result) {\n\tif (_searchNextQuery == query) {\n\t\ttoggleSearchLoading(false);\n\t}\n\t_searchSetsRequestId = 0;\n\n\tresult.match([&](const MTPDmessages_foundStickerSetsNotModified &data) {\n\t\tLOG((\"API Error: \"\n\t\t\t\"messages.foundStickerSetsNotModified not expected.\"));\n\t}, [&](const MTPDmessages_foundStickerSets &data) {\n\t\tauto it = _searchSetsCache.find(query);\n\t\tif (it == _searchSetsCache.cend()) {\n\t\t\tit = _searchSetsCache.emplace(\n\t\t\t\tquery,\n\t\t\t\tstd::vector<uint64>()).first;\n\t\t}\n\t\tfor (const auto &setData : data.vsets().v) {\n\t\t\tconst auto set = session().data().stickers().feedSet(setData);\n\t\t\tif (set->stickers.empty() && set->covers.empty()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tit->second.push_back(set->id);\n\t\t}\n\t\tif (_searchNextQuery == query) {\n\t\t\tshowSearchResults();\n\t\t}\n\t});\n}\n\nint StickersListWidget::stickersLeft() const {\n\treturn _rowsLeft;\n}\n\nQRect StickersListWidget::stickerRect(int section, int sel) {\n\tconst auto info = sectionInfo(section);\n\tif (sel >= shownSets()[section].stickers.size()) {\n\t\tsel -= shownSets()[section].stickers.size();\n\t}\n\tconst auto countTillItem = (sel - (sel % _columnCount));\n\tconst auto rowsToSkip = (countTillItem / _columnCount)\n\t\t+ ((countTillItem % _columnCount) ? 1 : 0);\n\tconst auto x = stickersLeft()\n\t\t+ ((sel % _columnCount) * _singleSize.width());\n\tconst auto y = info.rowsTop + rowsToSkip * _singleSize.height();\n\treturn QRect(QPoint(x, y), _singleSize);\n}\n\nvoid StickersListWidget::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\tauto clip = e->rect();\n\tif (st().bg->c.alpha() > 0) {\n\t\tp.fillRect(clip, st().bg);\n\t}\n\n\tpaintStickers(p, clip);\n}\n\nvoid StickersListWidget::paintStickers(Painter &p, QRect clip) {\n\tauto fromColumn = floorclamp(\n\t\tclip.x() - stickersLeft(),\n\t\t_singleSize.width(),\n\t\t0,\n\t\t_columnCount);\n\tauto toColumn = ceilclamp(\n\t\tclip.x() + clip.width() - stickersLeft(),\n\t\t_singleSize.width(),\n\t\t0,\n\t\t_columnCount);\n\tif (rtl()) {\n\t\tqSwap(fromColumn, toColumn);\n\t\tfromColumn = _columnCount - fromColumn;\n\t\ttoColumn = _columnCount - toColumn;\n\t}\n\n\t_paintAsPremium = session().premium();\n\t_pathGradient->startFrame(0, width(), width() / 2);\n\n\tauto &sets = shownSets();\n\tconst auto selectedSticker = std::get_if<OverSticker>(&_selected);\n\tconst auto selectedButton = std::get_if<OverButton>(!v::is_null(_pressed)\n\t\t? &_pressed\n\t\t: &_selected);\n\n\tconst auto now = crl::now();\n\tconst auto paused = On(PowerSaving::kStickersPanel)\n\t\t|| this->paused();\n\tif (sets.empty() && _section == Section::Search) {\n\t\tconst auto loading = _searchLoading || _searchRequestTimer.isActive();\n\t\tInner::paintEmptySearchResults(\n\t\t\tp,\n\t\t\tst::stickersEmpty,\n\t\t\tloading\n\t\t\t\t? tr::lng_contacts_loading(tr::now)\n\t\t\t\t: tr::lng_stickers_nothing_found(tr::now),\n\t\t\tloading);\n\t}\n\tconst auto badgeText = tr::lng_stickers_creator_badge(tr::now);\n\tconst auto &badgeFont = st::stickersHeaderBadgeFont;\n\tconst auto badgeWidth = badgeFont->width(badgeText);\n\tenumerateSections([&](const SectionInfo &info) {\n\t\tif (clip.top() >= info.rowsBottom) {\n\t\t\treturn true;\n\t\t} else if (clip.top() + clip.height() <= info.top) {\n\t\t\treturn false;\n\t\t}\n\t\tauto &set = sets[info.section];\n\t\tif (set.externalLayout) {\n\t\t\tconst auto loadedCount = int(set.stickers.size());\n\t\t\tconst auto count = (set.flags & SetFlag::NotLoaded)\n\t\t\t\t? set.count\n\t\t\t\t: loadedCount;\n\n\t\t\tauto widthForTitle = stickersRight()\n\t\t\t\t- (st().headerLeft - st().margin.left());\n\t\t\t{\n\t\t\t\tconst auto installedSet = !featuredHasAddButton(info.section);\n\t\t\t\tconst auto add = featuredAddRect(info, installedSet);\n\t\t\t\tconst auto selected = selectedButton\n\t\t\t\t\t? (selectedButton->section == info.section)\n\t\t\t\t\t: false;\n\t\t\t\t(installedSet\n\t\t\t\t\t? _inactiveButtonBg\n\t\t\t\t\t: selected\n\t\t\t\t\t? _trendingAddBgOver\n\t\t\t\t\t: _trendingAddBg).paint(p, myrtlrect(add));\n\t\t\t\tif (set.ripple) {\n\t\t\t\t\tset.ripple->paint(p, add.x(), add.y(), width());\n\t\t\t\t\tif (set.ripple->empty()) {\n\t\t\t\t\t\tset.ripple.reset();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst auto &text = installedSet ? _installedText : _addText;\n\t\t\t\tconst auto textWidth = installedSet\n\t\t\t\t\t? _installedWidth\n\t\t\t\t\t: _addWidth;\n\t\t\t\tconst auto &st = installedSet\n\t\t\t\t\t? st::stickersTrendingInstalled\n\t\t\t\t\t: st::stickersTrendingAdd;\n\t\t\t\tp.setFont(st.style.font);\n\t\t\t\tp.setPen(selected ? st.textFgOver : st.textFg);\n\t\t\t\tp.drawTextLeft(\n\t\t\t\t\tadd.x() - (st.width / 2),\n\t\t\t\t\tadd.y() + st.textTop,\n\t\t\t\t\twidth(),\n\t\t\t\t\ttext,\n\t\t\t\t\ttextWidth);\n\n\t\t\t\twidthForTitle -= add.width() - (st.width / 2);\n\t\t\t}\n\t\t\tif (set.flags & SetFlag::Unread) {\n\t\t\t\twidthForTitle -= st::stickersFeaturedUnreadSize\n\t\t\t\t\t+ st::stickersFeaturedUnreadSkip;\n\t\t\t}\n\n\t\t\tauto titleText = set.title;\n\t\t\tauto titleWidth = st::stickersTrendingHeaderFont->width(\n\t\t\t\ttitleText);\n\t\t\tif (titleWidth > widthForTitle) {\n\t\t\t\ttitleText = st::stickersTrendingHeaderFont->elided(\n\t\t\t\t\ttitleText,\n\t\t\t\t\twidthForTitle);\n\t\t\t\ttitleWidth = st::stickersTrendingHeaderFont->width(titleText);\n\t\t\t}\n\t\t\tp.setFont(st::stickersTrendingHeaderFont);\n\t\t\tp.setPen(st().trendingHeaderFg);\n\t\t\tp.drawTextLeft(\n\t\t\t\tst().headerLeft - st().margin.left(),\n\t\t\t\tinfo.top + st::stickersTrendingHeaderTop,\n\t\t\t\twidth(),\n\t\t\t\ttitleText,\n\t\t\t\ttitleWidth);\n\n\t\t\tif (set.flags & SetFlag::Unread) {\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(st().trendingUnreadFg);\n\n\t\t\t\t{\n\t\t\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\t\t\tp.drawEllipse(\n\t\t\t\t\t\tstyle::rtlrect(\n\t\t\t\t\t\t\tst().headerLeft\n\t\t\t\t\t\t\t\t- st().margin.left()\n\t\t\t\t\t\t\t\t+ titleWidth\n\t\t\t\t\t\t\t\t+ st::stickersFeaturedUnreadSkip,\n\t\t\t\t\t\t\tinfo.top\n\t\t\t\t\t\t\t\t+ st::stickersTrendingHeaderTop\n\t\t\t\t\t\t\t\t+ st::stickersFeaturedUnreadTop,\n\t\t\t\t\t\t\tst::stickersFeaturedUnreadSize,\n\t\t\t\t\t\t\tst::stickersFeaturedUnreadSize, width()));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst auto statusText = (count > 0)\n\t\t\t\t? tr::lng_stickers_count(tr::now, lt_count, count)\n\t\t\t\t: tr::lng_contacts_loading(tr::now);\n\t\t\tp.setFont(st::stickersTrendingSubheaderFont);\n\t\t\tp.setPen(st().trendingSubheaderFg);\n\t\t\tp.drawTextLeft(\n\t\t\t\tst().headerLeft - st().margin.left(),\n\t\t\t\tinfo.top + st::stickersTrendingSubheaderTop,\n\t\t\t\twidth(),\n\t\t\t\tstatusText);\n\n\t\t\tif (info.rowsTop >= clip.y() + clip.height()) {\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tfor (auto j = fromColumn; j < toColumn; ++j) {\n\t\t\t\tconst auto index = j;\n\t\t\t\tif (index >= loadedCount) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tconst auto selected = selectedSticker\n\t\t\t\t\t? (selectedSticker->section == info.section\n\t\t\t\t\t\t&& selectedSticker->index == index)\n\t\t\t\t\t: false;\n\t\t\t\tconst auto deleteSelected = false;\n\t\t\t\tpaintSticker(\n\t\t\t\t\tp,\n\t\t\t\t\tset,\n\t\t\t\t\tinfo.rowsTop,\n\t\t\t\t\tinfo.section,\n\t\t\t\t\tindex,\n\t\t\t\t\tnow,\n\t\t\t\t\tpaused,\n\t\t\t\t\tselected,\n\t\t\t\t\tdeleteSelected);\n\t\t\t}\n\t\t\tif (!paused) {\n\t\t\t\tmarkLottieFrameShown(set);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tif (setHasTitle(set) && clip.top() < info.rowsTop) {\n\t\t\tauto titleText = set.title;\n\t\t\tauto titleWidth = st::stickersTrendingHeaderFont->width(\n\t\t\t\ttitleText);\n\t\t\tauto widthForTitle = stickersRight()\n\t\t\t\t- (st().headerLeft - st().margin.left());\n\t\t\tif (hasRemoveButton(info.section)) {\n\t\t\t\tconst auto remove = removeButtonRect(info);\n\t\t\t\tconst auto selected = selectedButton\n\t\t\t\t\t? (selectedButton->section == info.section)\n\t\t\t\t\t: false;\n\t\t\t\tconst auto &removeSt = st().removeSet;\n\t\t\t\tif (set.ripple) {\n\t\t\t\t\tset.ripple->paint(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tremove.x() + removeSt.rippleAreaPosition.x(),\n\t\t\t\t\t\tremove.y() + removeSt.rippleAreaPosition.y(),\n\t\t\t\t\t\twidth());\n\t\t\t\t\tif (set.ripple->empty()) {\n\t\t\t\t\t\tset.ripple.reset();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst auto &icon = selected\n\t\t\t\t\t? removeSt.iconOver\n\t\t\t\t\t: removeSt.icon;\n\t\t\t\ticon.paint(\n\t\t\t\t\tp,\n\t\t\t\t\tremove.x() + (remove.width() - icon.width()) / 2,\n\t\t\t\t\tremove.y() + (remove.height() - icon.height()) / 2,\n\t\t\t\t\twidth());\n\n\t\t\t\twidthForTitle -= remove.width();\n\t\t\t}\n\t\t\tconst auto amCreator\n\t\t\t\t= (set.flags & Data::StickersSetFlag::AmCreator);\n\t\t\tif (amCreator) {\n\t\t\t\twidthForTitle -= badgeWidth\n\t\t\t\t\t+ st::stickersFeaturedUnreadSkip\n\t\t\t\t\t+ st::stickersHeaderBadgeFontSkip;\n\t\t\t}\n\t\t\tif (titleWidth > widthForTitle) {\n\t\t\t\ttitleText = st::stickersTrendingHeaderFont->elided(\n\t\t\t\t\ttitleText,\n\t\t\t\t\twidthForTitle);\n\t\t\t\ttitleWidth = st::stickersTrendingHeaderFont->width(titleText);\n\t\t\t}\n\t\t\tp.setFont(st::emojiPanHeaderFont);\n\t\t\tp.setPen(st().headerFg);\n\t\t\tp.drawTextLeft(\n\t\t\t\tst().headerLeft - st().margin.left(),\n\t\t\t\tinfo.top + st().headerTop,\n\t\t\t\twidth(),\n\t\t\t\ttitleText,\n\t\t\t\ttitleWidth);\n\t\t\tif (amCreator) {\n\t\t\t\tconst auto badgeLeft = st().headerLeft\n\t\t\t\t\t- st().margin.left()\n\t\t\t\t\t+ titleWidth\n\t\t\t\t\t+ st::stickersFeaturedUnreadSkip;\n\t\t\t\t{\n\t\t\t\t\tauto color = st().headerFg->c;\n\t\t\t\t\tcolor.setAlphaF(st().headerFg->c.alphaF() * 0.15);\n\t\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\t\tp.setBrush(color);\n\t\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\t\tp.drawRoundedRect(\n\t\t\t\t\t\tstyle::rtlrect(\n\t\t\t\t\t\t\tbadgeLeft,\n\t\t\t\t\t\t\tinfo.top + st::stickersHeaderBadgeFontTop,\n\t\t\t\t\t\t\tbadgeWidth + badgeFont->height,\n\t\t\t\t\t\t\tbadgeFont->height,\n\t\t\t\t\t\t\twidth()),\n\t\t\t\t\t\tbadgeFont->height / 2.,\n\t\t\t\t\t\tbadgeFont->height / 2.);\n\t\t\t\t}\n\t\t\t\tp.setPen(st().headerFg);\n\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t\tp.setFont(badgeFont);\n\t\t\t\tp.drawText(\n\t\t\t\t\tQRect(\n\t\t\t\t\t\tbadgeLeft + badgeFont->height / 2,\n\t\t\t\t\t\tinfo.top + st::stickersHeaderBadgeFontTop,\n\t\t\t\t\t\tbadgeWidth,\n\t\t\t\t\t\tbadgeFont->height),\n\t\t\t\t\tbadgeText,\n\t\t\t\t\tstyle::al_center);\n\t\t\t}\n\t\t}\n\t\tif (clip.top() + clip.height() <= info.rowsTop) {\n\t\t\treturn true;\n\t\t} else if (set.id == Data::Stickers::MegagroupSetId\n\t\t\t\t&& set.stickers.empty()) {\n\t\t\tconst auto buttonSelected = (std::get_if<OverGroupAdd>(&_selected)\n\t\t\t\t!= nullptr);\n\t\t\tpaintMegagroupEmptySet(p, info.rowsTop, buttonSelected);\n\t\t\treturn true;\n\t\t}\n\t\tconst auto fromRow = floorclamp(\n\t\t\tclip.y() - info.rowsTop,\n\t\t\t_singleSize.height(),\n\t\t\t0,\n\t\t\tinfo.rowsCount);\n\t\tconst auto toRow = ceilclamp(\n\t\t\tclip.y() + clip.height() - info.rowsTop,\n\t\t\t_singleSize.height(),\n\t\t\t0,\n\t\t\tinfo.rowsCount);\n\t\tfor (auto i = fromRow; i < toRow; ++i) {\n\t\t\tfor (auto j = fromColumn; j < toColumn; ++j) {\n\t\t\t\tconst auto index = int(i * _columnCount + j);\n\t\t\t\tif (index >= info.count) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tconst auto selected = selectedSticker\n\t\t\t\t\t? (selectedSticker->section == info.section\n\t\t\t\t\t\t&& selectedSticker->index == index)\n\t\t\t\t\t: false;\n\t\t\t\tconst auto deleteSelected = selected\n\t\t\t\t\t&& selectedSticker->overDelete;\n\t\t\t\tpaintSticker(\n\t\t\t\t\tp,\n\t\t\t\t\tset,\n\t\t\t\t\tinfo.rowsTop,\n\t\t\t\t\tinfo.section,\n\t\t\t\t\tindex,\n\t\t\t\t\tnow,\n\t\t\t\t\tpaused,\n\t\t\t\t\tselected,\n\t\t\t\t\tdeleteSelected);\n\t\t\t}\n\t\t}\n\t\tif (!paused) {\n\t\t\tmarkLottieFrameShown(set);\n\t\t}\n\t\treturn true;\n\t});\n}\n\nvoid StickersListWidget::markLottieFrameShown(Set &set) {\n\tif (const auto player = set.lottiePlayer.get()) {\n\t\tplayer->markFrameShown();\n\t}\n}\n\nvoid StickersListWidget::checkVisibleLottie() {\n\tif (shownSets().empty()) {\n\t\treturn;\n\t}\n\tconst auto visibleTop = getVisibleTop();\n\tconst auto visibleBottom = getVisibleBottom();\n\tconst auto destroyAfterDistance = (visibleBottom - visibleTop) * 2;\n\tconst auto destroyAbove = visibleTop - destroyAfterDistance;\n\tconst auto destroyBelow = visibleBottom + destroyAfterDistance;\n\tenumerateSections([&](const SectionInfo &info) {\n\t\tif (destroyBelow <= info.rowsTop\n\t\t\t|| destroyAbove >= info.rowsBottom) {\n\t\t\tclearHeavyIn(shownSets()[info.section]);\n\t\t} else if ((visibleTop > info.rowsTop && visibleTop < info.rowsBottom)\n\t\t\t|| (visibleBottom > info.rowsTop\n\t\t\t\t&& visibleBottom < info.rowsBottom)) {\n\t\t\tpauseInvisibleLottieIn(info);\n\t\t}\n\t\treturn true;\n\t});\n}\n\nvoid StickersListWidget::clearHeavyIn(Set &set, bool clearSavedFrames) {\n\tconst auto player = base::take(set.lottiePlayer);\n\tconst auto lifetime = base::take(set.lottieLifetime);\n\tfor (auto &sticker : set.stickers) {\n\t\tif (clearSavedFrames) {\n\t\t\tsticker.savedFrame = QImage();\n\t\t\tsticker.savedFrameFor = QSize();\n\t\t}\n\t\tsticker.webm = nullptr;\n\t\tsticker.lottie = nullptr;\n\t\tsticker.documentMedia = nullptr;\n\t}\n}\n\nvoid StickersListWidget::pauseInvisibleLottieIn(const SectionInfo &info) {\n\tauto &set = shownSets()[info.section];\n\tconst auto player = set.lottiePlayer.get();\n\tif (!player) {\n\t\treturn;\n\t}\n\tconst auto pauseInRows = [&](int fromRow, int tillRow) {\n\t\tExpects(fromRow <= tillRow);\n\n\t\tfor (auto i = fromRow; i != tillRow; ++i) {\n\t\t\tfor (auto j = 0; j != _columnCount; ++j) {\n\t\t\t\tconst auto index = i * _columnCount + j;\n\t\t\t\tif (index >= info.count) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (const auto animated = set.stickers[index].lottie) {\n\t\t\t\t\tplayer->pause(animated);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\tconst auto visibleTop = getVisibleTop();\n\tconst auto visibleBottom = getVisibleBottom();\n\tif (visibleTop >= info.rowsTop + _singleSize.height()\n\t\t&& visibleTop < info.rowsBottom) {\n\t\tconst auto pauseHeight = (visibleTop - info.rowsTop);\n\t\tconst auto pauseRows = std::min(\n\t\t\tpauseHeight / _singleSize.height(),\n\t\t\tinfo.rowsCount);\n\t\tpauseInRows(0, pauseRows);\n\t}\n\tif (visibleBottom > info.rowsTop\n\t\t&& visibleBottom + _singleSize.height() <= info.rowsBottom) {\n\t\tconst auto pauseHeight = (info.rowsBottom - visibleBottom);\n\t\tconst auto pauseRows = std::min(\n\t\t\tpauseHeight / _singleSize.height(),\n\t\t\tinfo.rowsCount);\n\t\tpauseInRows(info.rowsCount - pauseRows, info.rowsCount);\n\t}\n}\n\nint StickersListWidget::megagroupSetInfoLeft() const {\n\treturn st().headerLeft - st().margin.left();\n}\n\nvoid StickersListWidget::paintMegagroupEmptySet(\n\t\tPainter &p,\n\t\tint y,\n\t\tbool buttonSelected) {\n\tp.setPen(st().headerFg);\n\n\tauto infoLeft = megagroupSetInfoLeft();\n\t_megagroupSetAbout.drawLeft(p, infoLeft, y, width() - infoLeft, width());\n\n\tauto button = _megagroupSetButtonRect.translated(0, y);\n\t(buttonSelected ? _groupCategoryAddBgOver : _groupCategoryAddBg).paint(\n\t\tp,\n\t\tmyrtlrect(button));\n\tif (_megagroupSetButtonRipple) {\n\t\t_megagroupSetButtonRipple->paint(p, button.x(), button.y(), width());\n\t\tif (_megagroupSetButtonRipple->empty()) {\n\t\t\t_megagroupSetButtonRipple.reset();\n\t\t}\n\t}\n\tp.setFont(st::stickerGroupCategoryAdd.style.font);\n\tp.setPen(buttonSelected\n\t\t? st::stickerGroupCategoryAdd.textFgOver\n\t\t: st::stickerGroupCategoryAdd.textFg);\n\tp.drawTextLeft(\n\t\tbutton.x() - (st::stickerGroupCategoryAdd.width / 2),\n\t\tbutton.y() + st::stickerGroupCategoryAdd.textTop,\n\t\twidth(),\n\t\t_megagroupSetButtonText,\n\t\t_megagroupSetButtonTextWidth);\n}\n\nvoid StickersListWidget::ensureLottiePlayer(Set &set) {\n\tif (set.lottiePlayer) {\n\t\treturn;\n\t}\n\tset.lottiePlayer = std::make_unique<Lottie::MultiPlayer>(\n\t\tLottie::Quality::Default,\n\t\tgetLottieRenderer());\n\tconst auto raw = set.lottiePlayer.get();\n\n\traw->updates(\n\t) | rpl::on_next([=] {\n\t\tauto &sets = shownSets();\n\t\tenumerateSections([&](const SectionInfo &info) {\n\t\t\tif (sets[info.section].lottiePlayer.get() != raw) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tupdateSet(info);\n\t\t\treturn false;\n\t\t});\n\t}, set.lottieLifetime);\n}\n\nvoid StickersListWidget::setupLottie(Set &set, int section, int index) {\n\tauto &sticker = set.stickers[index];\n\tensureLottiePlayer(set);\n\n\t// Document should be loaded already for the animation to be set up.\n\tAssert(sticker.documentMedia != nullptr);\n\tsticker.lottie = LottieAnimationFromDocument(\n\t\tset.lottiePlayer.get(),\n\t\tsticker.documentMedia.get(),\n\t\tStickerLottieSize::StickersPanel,\n\t\tboundingBoxSize() * style::DevicePixelRatio());\n}\n\nvoid StickersListWidget::setupWebm(Set &set, int section, int index) {\n\tauto &sticker = set.stickers[index];\n\n\t// Document should be loaded already for the animation to be set up.\n\tAssert(sticker.documentMedia != nullptr);\n\tconst auto setId = set.id;\n\tconst auto document = sticker.document;\n\tauto callback = [=](Media::Clip::Notification notification) {\n\t\tclipCallback(notification, setId, document, index);\n\t};\n\tsticker.webm = Media::Clip::MakeReader(\n\t\tsticker.documentMedia->owner()->location(),\n\t\tsticker.documentMedia->bytes(),\n\t\tstd::move(callback));\n}\n\nvoid StickersListWidget::clipCallback(\n\t\tMedia::Clip::Notification notification,\n\t\tuint64 setId,\n\t\tnot_null<DocumentData*> document,\n\t\tint indexHint) {\n\tExpects(indexHint >= 0);\n\n\tauto &sets = shownSets();\n\tenumerateSections([&](const SectionInfo &info) {\n\t\tauto &set = sets[info.section];\n\t\tif (set.id != setId) {\n\t\t\treturn true;\n\t\t}\n\t\tusing namespace Media::Clip;\n\t\tswitch (notification) {\n\t\tcase Notification::Reinit: {\n\t\t\tconst auto j = (indexHint < set.stickers.size()\n\t\t\t\t&& set.stickers[indexHint].document == document)\n\t\t\t\t? (begin(set.stickers) + indexHint)\n\t\t\t\t: ranges::find(set.stickers, document, &Sticker::document);\n\t\t\tif (j == end(set.stickers) || !j->webm) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst auto index = j - begin(set.stickers);\n\t\t\tauto &webm = j->webm;\n\t\t\tif (webm->state() == State::Error) {\n\t\t\t\twebm.setBad();\n\t\t\t} else if (webm->ready() && !webm->started()) {\n\t\t\t\tconst auto size = ComputeStickerSize(\n\t\t\t\t\tj->document,\n\t\t\t\t\tboundingBoxSize());\n\t\t\t\twebm->start({ .frame = size, .keepAlpha = true });\n\t\t\t} else if (webm->autoPausedGif() && !itemVisible(info, index)) {\n\t\t\t\twebm = nullptr;\n\t\t\t}\n\t\t} break;\n\n\t\tcase Notification::Repaint: break;\n\t\t}\n\n\t\tupdateSet(info);\n\t\treturn false;\n\t});\n}\n\nbool StickersListWidget::itemVisible(\n\t\tconst SectionInfo &info,\n\t\tint index) const {\n\tconst auto visibleTop = getVisibleTop();\n\tconst auto visibleBottom = getVisibleBottom();\n\tconst auto row = index / _columnCount;\n\tconst auto top = info.rowsTop + row * _singleSize.height();\n\tconst auto bottom = top + _singleSize.height();\n\treturn (visibleTop < bottom) && (visibleBottom > top);\n}\n\nvoid StickersListWidget::updateSets() {\n\tif (_repaintSetsIds.empty()) {\n\t\treturn;\n\t}\n\tauto repaint = base::take(_repaintSetsIds);\n\tauto &sets = shownSets();\n\tenumerateSections([&](const SectionInfo &info) {\n\t\tif (repaint.contains(sets[info.section].id)) {\n\t\t\tupdateSet(info);\n\t\t}\n\t\treturn true;\n\t});\n}\n\nvoid StickersListWidget::updateSet(const SectionInfo &info) {\n\tauto &set = shownSets()[info.section];\n\n\tconst auto now = crl::now();\n\tconst auto delay = std::max(\n\t\t_lastScrolledAt + kMinAfterScrollDelay - now,\n\t\tset.lastUpdateTime + kMinRepaintDelay - now);\n\tif (delay <= 0) {\n\t\trepaintItems(info, now);\n\t} else {\n\t\t_repaintSetsIds.emplace(set.id);\n\t\tif (!_updateSetsTimer.isActive()\n\t\t\t|| _updateSetsTimer.remainingTime() > kMinRepaintDelay) {\n\t\t\t_updateSetsTimer.callOnce(std::max(delay, kMinRepaintDelay));\n\t\t}\n\t}\n}\n\nvoid StickersListWidget::repaintItems(\n\t\tconst SectionInfo &info,\n\t\tcrl::time now) {\n\tupdate(\n\t\t0,\n\t\tinfo.rowsTop,\n\t\twidth(),\n\t\tinfo.rowsBottom - info.rowsTop);\n\tauto &set = shownSets()[info.section];\n\tset.lastUpdateTime = now;\n}\n\nvoid StickersListWidget::updateItems() {\n\tconst auto now = crl::now();\n\tconst auto delay = std::max(\n\t\t_lastScrolledAt + kMinAfterScrollDelay - now,\n\t\t_lastFullUpdatedAt + kMinRepaintDelay - now);\n\tif (delay <= 0) {\n\t\trepaintItems(now);\n\t} else if (!_updateItemsTimer.isActive()\n\t\t|| _updateItemsTimer.remainingTime() > kMinRepaintDelay) {\n\t\t_updateItemsTimer.callOnce(std::max(delay, kMinRepaintDelay));\n\t}\n}\n\nvoid StickersListWidget::repaintItems(crl::time now) {\n\tupdate();\n\t_repaintSetsIds.clear();\n\tif (!now) {\n\t\tnow = crl::now();\n\t}\n\t_lastFullUpdatedAt = now;\n\tfor (auto &set : shownSets()) {\n\t\tset.lastUpdateTime = now;\n\t}\n}\n\nQSize StickersListWidget::boundingBoxSize() const {\n\treturn QSize(\n\t\t_singleSize.width() - st::roundRadiusSmall * 2,\n\t\t_singleSize.height() - st::roundRadiusSmall * 2);\n}\n\nvoid StickersListWidget::paintSticker(\n\t\tPainter &p,\n\t\tSet &set,\n\t\tint y,\n\t\tint section,\n\t\tint index,\n\t\tcrl::time now,\n\t\tbool paused,\n\t\tbool selected,\n\t\tbool deleteSelected) {\n\tauto &sticker = set.stickers[index];\n\tsticker.ensureMediaCreated();\n\tconst auto document = sticker.document;\n\tconst auto &media = sticker.documentMedia;\n\tif (!document->sticker()) {\n\t\treturn;\n\t}\n\n\tconst auto premium = document->isPremiumSticker();\n\tconst auto isLottie = document->sticker()->isLottie();\n\tconst auto isWebm = document->sticker()->isWebm();\n\tif (isLottie\n\t\t&& !sticker.lottie\n\t\t&& media->loaded()) {\n\t\tsetupLottie(set, section, index);\n\t} else if (isWebm && !sticker.webm && media->loaded()) {\n\t\tsetupWebm(set, section, index);\n\t}\n\n\tconst auto row = int((index / _columnCount));\n\tconst auto col = int(index % _columnCount);\n\n\tconst auto pos = QPoint(\n\t\tstickersLeft() + col * _singleSize.width(),\n\t\ty + row * _singleSize.height());\n\tif (selected) {\n\t\tauto tl = pos;\n\t\tif (rtl()) {\n\t\t\ttl.setX(width() - tl.x() - _singleSize.width());\n\t\t}\n\t\t_overBg.paint(p, QRect(tl, _singleSize));\n\t}\n\n\tmedia->checkStickerSmall();\n\n\tconst auto size = ComputeStickerSize(document, boundingBoxSize());\n\tconst auto ppos = pos + QPoint(\n\t\t(_singleSize.width() - size.width()) / 2,\n\t\t(_singleSize.height() - size.height()) / 2);\n\n\tauto lottieFrame = QImage();\n\tif (sticker.lottie && sticker.lottie->ready()) {\n\t\tauto request = Lottie::FrameRequest();\n\t\trequest.box = boundingBoxSize() * style::DevicePixelRatio();\n\t\tlottieFrame = sticker.lottie->frame(request);\n\t\tp.drawImage(\n\t\t\tQRect(ppos, lottieFrame.size() / style::DevicePixelRatio()),\n\t\t\tlottieFrame);\n\t\tif (sticker.savedFrame.isNull()) {\n\t\t\tsticker.savedFrame = lottieFrame;\n\t\t\tsticker.savedFrame.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\tsticker.savedFrameFor = _singleSize;\n\t\t}\n\t\tset.lottiePlayer->unpause(sticker.lottie);\n\t} else if (sticker.webm && sticker.webm->started()) {\n\t\tconst auto frame = sticker.webm->current(\n\t\t\t{ .frame = size, .keepAlpha = true },\n\t\t\tpaused ? 0 : now);\n\t\tif (sticker.savedFrame.isNull()) {\n\t\t\tsticker.savedFrame = frame;\n\t\t\tsticker.savedFrame.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\tsticker.savedFrameFor = _singleSize;\n\t\t}\n\t\tp.drawImage(ppos, frame);\n\t} else {\n\t\tconst auto image = media->getStickerSmall();\n\t\tconst auto useSavedFrame = !sticker.savedFrame.isNull()\n\t\t\t&& (sticker.savedFrameFor == _singleSize);\n\t\tif (useSavedFrame) {\n\t\t\tp.drawImage(ppos, sticker.savedFrame);\n\t\t\tif (premium) {\n\t\t\t\tlottieFrame = sticker.savedFrame;\n\t\t\t}\n\t\t} else if (image) {\n\t\t\tconst auto pixmap = image->pixSingle(size, { .outer = size });\n\t\t\tp.drawPixmapLeft(ppos, width(), pixmap);\n\t\t\tif (sticker.savedFrame.isNull()) {\n\t\t\t\tsticker.savedFrame = pixmap.toImage().convertToFormat(\n\t\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t\tsticker.savedFrameFor = _singleSize;\n\t\t\t}\n\t\t\tif (premium) {\n\t\t\t\tlottieFrame = pixmap.toImage().convertToFormat(\n\t\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t}\n\t\t} else {\n\t\t\tp.setOpacity(1.);\n\t\t\tPaintStickerThumbnailPath(\n\t\t\t\tp,\n\t\t\t\tmedia.get(),\n\t\t\t\tQRect(ppos, size),\n\t\t\t\t_pathGradient.get());\n\t\t}\n\t}\n\n\tif (selected && stickerHasDeleteButton(set, index)) {\n\t\tconst auto xPos = pos\n\t\t\t+ QPoint(\n\t\t\t\t_singleSize.width() - st::stickerPanDeleteIconBg.width(),\n\t\t\t\t0);\n\t\tp.setOpacity(deleteSelected\n\t\t\t? st::stickerPanDeleteOpacityBgOver\n\t\t\t: st::stickerPanDeleteOpacityBg);\n\t\tst::stickerPanDeleteIconBg.paint(p, xPos, width());\n\t\tp.setOpacity(deleteSelected\n\t\t\t? st::stickerPanDeleteOpacityFgOver\n\t\t\t: st::stickerPanDeleteOpacityFg);\n\t\tst::stickerPanDeleteIconFg.paint(p, xPos, width());\n\t\tp.setOpacity(1.);\n\t}\n\n\tauto cornerPainted = false;\n\tconst auto corner = (set.id == Data::Stickers::RecentSetId)\n\t\t? &_cornerEmoji\n\t\t: (set.id == SearchEmojiSectionSetId())\n\t\t? &_filterStickersCornerEmoji\n\t\t: nullptr;\n\tif (corner && !corner->empty() && _paintAsPremium) {\n\t\tAssert(index < corner->size());\n\t\tif (const auto emoji = (*corner)[index]) {\n\t\t\tconst auto size = Ui::Emoji::GetSizeNormal();\n\t\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t\tconst auto radius = st::roundRadiusSmall;\n\t\t\tconst auto position = pos\n\t\t\t\t+ QPoint(_singleSize.width(), _singleSize.height())\n\t\t\t\t- QPoint(size / ratio + radius, size / ratio + radius);\n\t\t\tUi::Emoji::Draw(p, emoji, size, position.x(), position.y());\n\t\t\tcornerPainted = true;\n\t\t}\n\t}\n\tif (!cornerPainted && premium) {\n\t\t_premiumMark->paint(\n\t\t\tp,\n\t\t\tlottieFrame,\n\t\t\tsticker.premiumLock,\n\t\t\tpos,\n\t\t\t_singleSize,\n\t\t\twidth());\n\t}\n}\n\nint StickersListWidget::stickersRight() const {\n\treturn stickersLeft() + (_columnCount * _singleSize.width());\n}\n\nbool StickersListWidget::featuredHasAddButton(int index) const {\n\tif (index < 0\n\t\t|| index >= shownSets().size()\n\t\t|| !shownSets()[index].externalLayout) {\n\t\treturn false;\n\t}\n\tconst auto flags = shownSets()[index].flags;\n\treturn !SetInMyList(flags);\n}\n\nQRect StickersListWidget::featuredAddRect(int index) const {\n\treturn featuredAddRect(sectionInfo(index), false);\n}\n\nQRect StickersListWidget::featuredAddRect(\n\t\tconst SectionInfo &info,\n\t\tbool installedSet) const {\n\tconst auto addw = (installedSet ? _installedWidth : _addWidth)\n\t\t- st::stickersTrendingAdd.width;\n\tconst auto addh = st::stickersTrendingAdd.height;\n\tconst auto addx = stickersRight() - addw;\n\tconst auto addy = info.top + st::stickersTrendingAddTop;\n\treturn QRect(addx, addy, addw, addh);\n}\n\nbool StickersListWidget::hasRemoveButton(int index) const {\n\tif (index < 0 || index >= shownSets().size()) {\n\t\treturn false;\n\t}\n\tauto &set = shownSets()[index];\n\tif (set.externalLayout) {\n\t\treturn false;\n\t}\n\tauto flags = set.flags;\n\tif (!(flags & SetFlag::Special)) {\n\t\treturn true;\n\t}\n\tif (set.id == Data::Stickers::MegagroupSetId) {\n\t\tAssert(_megagroupSet != nullptr);\n\t\tif (index + 1 != shownSets().size()) {\n\t\t\treturn true;\n\t\t}\n\t\treturn !set.stickers.empty() && _megagroupSet->canEditStickers();\n\t}\n\treturn false;\n}\n\nQRect StickersListWidget::removeButtonRect(int index) const {\n\treturn removeButtonRect(sectionInfo(index));\n}\n\nQRect StickersListWidget::removeButtonRect(const SectionInfo &info) const {\n\tconst auto &removeSt = st().removeSet;\n\tauto buttonw = removeSt.width;\n\tauto buttonh = removeSt.height;\n\tauto buttonx = stickersRight() - buttonw;\n\tauto buttony = info.top + (st().header - buttonh) / 2;\n\treturn QRect(buttonx, buttony, buttonw, buttonh);\n}\n\nvoid StickersListWidget::mousePressEvent(QMouseEvent *e) {\n\tif (e->button() != Qt::LeftButton) {\n\t\treturn;\n\t}\n\t_lastMousePosition = e->globalPos();\n\tupdateSelected();\n\n\tsetPressed(_selected);\n\tClickHandler::pressed();\n\t_previewTimer.callOnce(QApplication::startDragTime());\n}\n\nvoid StickersListWidget::setPressed(OverState newPressed) {\n\tif (auto button = std::get_if<OverButton>(&_pressed)) {\n\t\tauto &sets = shownSets();\n\t\tAssert(button->section >= 0 && button->section < sets.size());\n\t\tauto &set = sets[button->section];\n\t\tif (set.ripple) {\n\t\t\tset.ripple->lastStop();\n\t\t}\n\t} else if (std::get_if<OverGroupAdd>(&_pressed)) {\n\t\tif (_megagroupSetButtonRipple) {\n\t\t\t_megagroupSetButtonRipple->lastStop();\n\t\t}\n\t}\n\t_pressed = newPressed;\n\tif (auto button = std::get_if<OverButton>(&_pressed)) {\n\t\tauto &sets = shownSets();\n\t\tAssert(button->section >= 0 && button->section < sets.size());\n\t\tauto &set = sets[button->section];\n\t\tif (!set.ripple) {\n\t\t\tset.ripple = createButtonRipple(button->section);\n\t\t}\n\t\tset.ripple->add(mapFromGlobal(QCursor::pos())\n\t\t\t- buttonRippleTopLeft(button->section));\n\t} else if (std::get_if<OverGroupAdd>(&_pressed)) {\n\t\tif (!_megagroupSetButtonRipple) {\n\t\t\tauto mask = Ui::RippleAnimation::RoundRectMask(\n\t\t\t\t_megagroupSetButtonRect.size(),\n\t\t\t\tst::roundRadiusLarge);\n\t\t\t_megagroupSetButtonRipple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\t\tst::stickerGroupCategoryAdd.ripple,\n\t\t\t\t\tstd::move(mask),\n\t\t\t\t\t[this] { rtlupdate(megagroupSetButtonRectFinal()); });\n\t\t}\n\t\t_megagroupSetButtonRipple->add(mapFromGlobal(QCursor::pos())\n\t\t\t- myrtlrect(megagroupSetButtonRectFinal()).topLeft());\n\t}\n}\n\nQRect StickersListWidget::megagroupSetButtonRectFinal() const {\n\tauto result = QRect();\n\tif (_section == Section::Stickers) {\n\t\tusing Stickers = Data::Stickers;\n\t\tenumerateSections([this, &result](const SectionInfo &info) {\n\t\t\tif (shownSets()[info.section].id == Stickers::MegagroupSetId) {\n\t\t\t\tresult = _megagroupSetButtonRect.translated(0, info.rowsTop);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\t}\n\treturn result;\n}\n\nstd::unique_ptr<Ui::RippleAnimation> StickersListWidget::createButtonRipple(\n\t\tint section) {\n\tExpects(section >= 0 && section < shownSets().size());\n\n\tif (shownSets()[section].externalLayout) {\n\t\tconst auto maskSize = QSize(\n\t\t\t_addWidth - st::stickersTrendingAdd.width,\n\t\t\tst::stickersTrendingAdd.height);\n\t\tauto mask = Ui::RippleAnimation::RoundRectMask(\n\t\t\tmaskSize,\n\t\t\tst::roundRadiusLarge);\n\t\treturn std::make_unique<Ui::RippleAnimation>(\n\t\t\tst::stickersTrendingAdd.ripple,\n\t\t\tstd::move(mask),\n\t\t\t[this, section] { rtlupdate(featuredAddRect(section)); });\n\t}\n\tconst auto &removeSt = st().removeSet;\n\tauto maskSize = QSize(removeSt.rippleAreaSize, removeSt.rippleAreaSize);\n\tauto mask = Ui::RippleAnimation::EllipseMask(maskSize);\n\treturn std::make_unique<Ui::RippleAnimation>(\n\t\tremoveSt.ripple,\n\t\tstd::move(mask),\n\t\t[this, section] { rtlupdate(removeButtonRect(section)); });\n}\n\nQPoint StickersListWidget::buttonRippleTopLeft(int section) const {\n\tExpects(section >= 0 && section < shownSets().size());\n\n\tif (shownSets()[section].externalLayout) {\n\t\treturn myrtlrect(featuredAddRect(section)).topLeft();\n\t}\n\treturn myrtlrect(removeButtonRect(section)).topLeft()\n\t\t+ st().removeSet.rippleAreaPosition;\n}\n\nvoid StickersListWidget::showStickerSetBox(\n\t\tnot_null<DocumentData*> document,\n\t\tuint64 setId) {\n\tif (document->sticker() && document->sticker()->set) {\n\t\tcheckHideWithBox(Box<StickerSetBox>(\n\t\t\t_show,\n\t\t\tdocument->sticker()->set,\n\t\t\tdocument->sticker()->setType));\n\t} else if ((setId == Data::Stickers::FavedSetId)\n\t\t\t|| (setId == Data::Stickers::RecentSetId)) {\n\t\tconst auto lifetime = std::make_shared<rpl::lifetime>();\n\t\tconstexpr auto kTimeout = 10000;\n\t\trpl::merge(\n\t\t\tbase::timer_once(kTimeout),\n\t\t\tdocument->owner().stickers().updated(\n\t\t\t\tData::StickersType::Stickers)\n\t\t) | rpl::on_next([=, weak = base::make_weak(this)] {\n\t\t\tif (weak.get()) {\n\t\t\t\tshowStickerSetBox(document, setId);\n\t\t\t}\n\t\t\tlifetime->destroy();\n\t\t}, *lifetime);\n\t\tdocument->session().api().requestSpecialStickersForce(\n\t\t\tsetId == Data::Stickers::FavedSetId,\n\t\t\tsetId == Data::Stickers::RecentSetId,\n\t\t\tfalse);\n\t}\n}\n\nbase::unique_qptr<Ui::PopupMenu> StickersListWidget::fillContextMenu(\n\t\tconst SendMenu::Details &details) {\n\tauto selected = _selected;\n\tauto &sets = shownSets();\n\tif (v::is_null(selected) || !v::is_null(_pressed)) {\n\t\treturn nullptr;\n\t}\n\tconst auto sticker = std::get_if<OverSticker>(&selected);\n\tif (!sticker) {\n\t\treturn nullptr;\n\t}\n\tconst auto section = sticker->section;\n\tconst auto index = sticker->index;\n\tAssert(section >= 0 && section < sets.size());\n\tauto &set = sets[section];\n\tAssert(index >= 0 && index < set.stickers.size());\n\n\tauto menu = base::make_unique_q<Ui::PopupMenu>(this, st().menu);\n\n\tconst auto document = set.stickers[sticker->index].document;\n\tconst auto send = crl::guard(this, [=](Api::SendOptions options) {\n\t\t_chosen.fire({\n\t\t\t.document = document,\n\t\t\t.options = options,\n\t\t\t.messageSendingFrom = options.scheduled\n\t\t\t\t? Ui::MessageSendingAnimationFrom()\n\t\t\t\t: messageSentAnimationInfo(section, index, document),\n\t\t});\n\t});\n\tconst auto icons = &st().icons;\n\n\t// In case we're adding items after FillSendMenu we have\n\t// to pass nullptr for showForEffect and attach selector later.\n\t// Otherwise added items widths won't be respected in menu geometry.\n\tSendMenu::FillSendMenu(\n\t\tmenu,\n\t\tnullptr, // showForEffect\n\t\tdetails,\n\t\tSendMenu::DefaultCallback(_show, send),\n\t\ticons);\n\n\tconst auto show = _show;\n\tconst auto toggleFavedSticker = [=] {\n\t\tApi::ToggleFavedSticker(\n\t\t\tshow,\n\t\t\tdocument,\n\t\t\tData::FileOriginStickerSet(Data::Stickers::FavedSetId, 0));\n\t};\n\tconst auto isFaved = document->owner().stickers().isFaved(document);\n\tmenu->addAction(\n\t\t(isFaved\n\t\t\t? tr::lng_faved_stickers_remove\n\t\t\t: tr::lng_faved_stickers_add)(tr::now),\n\t\ttoggleFavedSticker,\n\t\tisFaved ? &icons->menuUnfave : &icons->menuFave);\n\n\tif (_features.openStickerSets) {\n\t\tmenu->addAction(tr::lng_context_pack_info(tr::now), [=, id = set.id] {\n\t\t\tshowStickerSetBox(document, id);\n\t\t}, &icons->menuStickerSet);\n\t}\n\n\tif (const auto id = set.id; id == Data::Stickers::RecentSetId) {\n\t\tmenu->addAction(tr::lng_recent_stickers_remove(tr::now), [=] {\n\t\t\tApi::ToggleRecentSticker(\n\t\t\t\tdocument,\n\t\t\t\tData::FileOriginStickerSet(id, 0),\n\t\t\t\tfalse);\n\t\t}, &icons->menuRecentRemove);\n\t}\n\n\tSendMenu::AttachSendMenuEffect(\n\t\tmenu,\n\t\t_show,\n\t\tdetails,\n\t\tSendMenu::DefaultCallback(_show, send));\n\n\treturn menu;\n}\n\nUi::MessageSendingAnimationFrom StickersListWidget::messageSentAnimationInfo(\n\t\tint section,\n\t\tint index,\n\t\tnot_null<DocumentData*> document) {\n\tconst auto rect = stickerRect(section, index);\n\tconst auto size = ComputeStickerSize(document, boundingBoxSize());\n\tconst auto innerPos = QPoint(\n\t\t(rect.width() - size.width()) / 2,\n\t\t(rect.height() - size.height()) / 2);\n\n\treturn {\n\t\t.type = Ui::MessageSendingAnimationFrom::Type::Sticker,\n\t\t.localId = session().data().nextLocalMessageId(),\n\t\t.globalStartGeometry = mapToGlobal(\n\t\t\tQRect(rect.topLeft() + innerPos, size)),\n\t};\n}\n\nvoid StickersListWidget::mouseReleaseEvent(QMouseEvent *e) {\n\t_previewTimer.cancel();\n\n\tauto pressed = _pressed;\n\tsetPressed(v::null);\n\tif (pressed != _selected) {\n\t\trepaintItems();\n\t}\n\n\tauto activated = ClickHandler::unpressed();\n\tif (_previewShown) {\n\t\t_previewShown = false;\n\t\treturn;\n\t}\n\n\t_lastMousePosition = e->globalPos();\n\tupdateSelected();\n\n\tauto &sets = shownSets();\n\tif (!v::is_null(pressed) && pressed == _selected) {\n\t\tif (auto sticker = std::get_if<OverSticker>(&pressed)) {\n\t\t\tAssert(sticker->section >= 0 && sticker->section < sets.size());\n\t\t\tauto &set = sets[sticker->section];\n\t\t\tAssert(sticker->index >= 0 && sticker->index < set.stickers.size());\n\t\t\tif (stickerHasDeleteButton(set, sticker->index) && sticker->overDelete) {\n\t\t\t\tif (set.id == Data::Stickers::RecentSetId) {\n\t\t\t\t\tremoveRecentSticker(sticker->section, sticker->index);\n\t\t\t\t} else if (set.id == Data::Stickers::FavedSetId) {\n\t\t\t\t\tremoveFavedSticker(sticker->section, sticker->index);\n\t\t\t\t} else {\n\t\t\t\t\tUnexpected(\"Single sticker delete click.\");\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto document = set.stickers[sticker->index].document;\n\t\t\tif (_features.openStickerSets\n\t\t\t\t&& (e->modifiers() & Qt::ControlModifier)) {\n\t\t\t\tshowStickerSetBox(document, set.id);\n\t\t\t} else {\n\t\t\t\t_chosen.fire({\n\t\t\t\t\t.document = document,\n\t\t\t\t\t.messageSendingFrom = messageSentAnimationInfo(\n\t\t\t\t\t\tsticker->section,\n\t\t\t\t\t\tsticker->index,\n\t\t\t\t\t\tdocument),\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (auto set = std::get_if<OverSet>(&pressed)) {\n\t\t\tAssert(set->section >= 0 && set->section < sets.size());\n\t\t\tdisplaySet(sets[set->section].id);\n\t\t} else if (auto button = std::get_if<OverButton>(&pressed)) {\n\t\t\tAssert(button->section >= 0 && button->section < sets.size());\n\t\t\tif (sets[button->section].externalLayout) {\n\t\t\t\t_localSetsManager->install(sets[button->section].id);\n\t\t\t\tupdate();\n\t\t\t} else {\n\t\t\t\tremoveSet(sets[button->section].id);\n\t\t\t}\n\t\t} else if (std::get_if<OverGroupAdd>(&pressed)) {\n\t\t\tconst auto isEmoji = false;\n\t\t\t_show->showBox(Box<StickersBox>(_show, _megagroupSet, isEmoji));\n\t\t}\n\t}\n}\n\nvoid StickersListWidget::removeRecentSticker(int section, int index) {\n\tif ((_section != Section::Stickers)\n\t\t|| (section >= int(_mySets.size()))\n\t\t|| (_mySets[section].id != Data::Stickers::RecentSetId)) {\n\t\treturn;\n\t}\n\n\tclearSelection();\n\tbool refresh = false;\n\tconst auto &sticker = _mySets[section].stickers[index];\n\tconst auto document = sticker.document;\n\tauto &recent = session().data().stickers().getRecentPack();\n\tfor (int32 i = 0, l = recent.size(); i < l; ++i) {\n\t\tif (recent.at(i).first == document) {\n\t\t\trecent.removeAt(i);\n\t\t\tsession().saveSettings();\n\t\t\trefresh = true;\n\t\t\tbreak;\n\t\t}\n\t}\n\tauto &sets = session().data().stickers().setsRef();\n\tauto it = sets.find(Data::Stickers::CustomSetId);\n\tif (it != sets.cend()) {\n\t\tconst auto set = it->second.get();\n\t\tfor (int i = 0, l = set->stickers.size(); i < l; ++i) {\n\t\t\tif (set->stickers.at(i) == document) {\n\t\t\t\tset->stickers.removeAt(i);\n\t\t\t\tif (set->stickers.isEmpty()) {\n\t\t\t\t\tsets.erase(it);\n\t\t\t\t}\n\t\t\t\tsession().local().writeInstalledStickers();\n\t\t\t\trefresh = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\tif (refresh) {\n\t\trefreshRecentStickers();\n\t\tupdateSelected();\n\t\trepaintItems();\n\t}\n}\n\nvoid StickersListWidget::removeFavedSticker(int section, int index) {\n\tif ((_section != Section::Stickers)\n\t\t|| (section >= int(_mySets.size()))\n\t\t|| (_mySets[section].id != Data::Stickers::FavedSetId)) {\n\t\treturn;\n\t}\n\n\tclearSelection();\n\tconst auto &sticker = _mySets[section].stickers[index];\n\tconst auto document = sticker.document;\n\tsession().data().stickers().setFaved(_show, document, false);\n\tApi::ToggleFavedSticker(\n\t\t_show,\n\t\tdocument,\n\t\tData::FileOriginStickerSet(Data::Stickers::FavedSetId, 0),\n\t\tfalse);\n}\n\nvoid StickersListWidget::setColumnCount(int count) {\n\tExpects(count > 0);\n\n\tif (_columnCount != count) {\n\t\t_columnCount = count;\n\t\trefreshFooterIcons();\n\t}\n}\n\nvoid StickersListWidget::mouseMoveEvent(QMouseEvent *e) {\n\t_lastMousePosition = e->globalPos();\n\tupdateSelected();\n}\n\nvoid StickersListWidget::resizeEvent(QResizeEvent *e) {\n\t_settings->moveToLeft(\n\t\t(width() - _settings->width()) / 2,\n\t\theight() / 3);\n\tif (!_megagroupSetAbout.isEmpty()) {\n\t\trefreshMegagroupSetGeometry();\n\t}\n}\n\nvoid StickersListWidget::leaveEventHook(QEvent *e) {\n\tclearSelection();\n}\n\nvoid StickersListWidget::leaveToChildEvent(QEvent *e, QWidget *child) {\n\tclearSelection();\n}\n\nvoid StickersListWidget::enterFromChildEvent(QEvent *e, QWidget *child) {\n\t_lastMousePosition = QCursor::pos();\n\tupdateSelected();\n}\n\nvoid StickersListWidget::clearSelection() {\n\tsetPressed(v::null);\n\tsetSelected(v::null);\n\trepaintItems();\n}\n\nTabbedSelector::InnerFooter *StickersListWidget::getFooter() const {\n\treturn _footer;\n}\n\nvoid StickersListWidget::processHideFinished() {\n\t_choosingUpdated.fire(TabbedSelector::Action::Cancel);\n\tclearSelection();\n\tclearHeavyData();\n\tif (_footer) {\n\t\t_footer->clearHeavyData();\n\t}\n}\n\nvoid StickersListWidget::processPanelHideFinished() {\n\tif (_localSetsManager->clearInstalledLocally()) {\n\t\trefreshStickers();\n\t}\n\tclearHeavyData();\n\tif (_footer) {\n\t\t_footer->clearHeavyData();\n\t}\n}\n\nvoid StickersListWidget::setSection(Section section) {\n\tif (_section == section) {\n\t\treturn;\n\t}\n\tclearHeavyData();\n\t_section = section;\n\t_recentShownCount = (section == Section::Search)\n\t\t? _filteredStickers.size()\n\t\t: _mySets.empty()\n\t\t? 0\n\t\t: _mySets.front().stickers.size();\n}\n\nvoid StickersListWidget::clearHeavyData() {\n\tfor (auto &set : shownSets()) {\n\t\tclearHeavyIn(set, false);\n\t}\n}\n\nvoid StickersListWidget::refreshStickers() {\n\tclearSelection();\n\n\tif (_isEffects) {\n\t\trefreshEffects();\n\t} else {\n\t\trefreshMySets();\n\t\trefreshFeaturedSets();\n\t\trefreshSearchSets();\n\t}\n\tresizeToWidth(width());\n\n\tif (_footer) {\n\t\trefreshFooterIcons();\n\t}\n\trefreshSettingsVisibility();\n\n\t_lastMousePosition = QCursor::pos();\n\tupdateSelected();\n\trepaintItems();\n\n\tvisibleTopBottomUpdated(getVisibleTop(), getVisibleBottom());\n}\n\nvoid StickersListWidget::refreshEffects() {\n\tauto wasSets = base::take(_mySets);\n\t_mySets.reserve(1);\n\trefreshRecentStickers(false);\n\ttakeHeavyData(_mySets, wasSets);\n}\n\nvoid StickersListWidget::refreshMySets() {\n\tauto wasSets = base::take(_mySets);\n\t_favedStickersMap.clear();\n\t_mySets.reserve(defaultSetsOrder().size() + 3);\n\n\trefreshFavedStickers();\n\trefreshRecentStickers(false);\n\trefreshMegagroupStickers(GroupStickersPlace::Visible);\n\n\tfor (const auto setId : defaultSetsOrder()) {\n\t\tconst auto externalLayout = false;\n\t\tappendSet(_mySets, setId, externalLayout, AppendSkip::Archived);\n\t}\n\trefreshMegagroupStickers(GroupStickersPlace::Hidden);\n\n\ttakeHeavyData(_mySets, wasSets);\n}\n\nvoid StickersListWidget::refreshFeaturedSets() {\n\tauto wasFeaturedSetsCount = base::take(_featuredSetsCount);\n\tauto wereOfficial = base::take(_officialSets);\n\t_officialSets.reserve(\n\t\tsession().data().stickers().featuredSetsOrder().size()\n\t\t+ wereOfficial.size()\n\t\t- wasFeaturedSetsCount);\n\tfor (const auto setId : session().data().stickers().featuredSetsOrder()) {\n\t\tconst auto externalLayout = true;\n\t\tappendSet(\n\t\t\t_officialSets,\n\t\t\tsetId,\n\t\t\texternalLayout,\n\t\t\tAppendSkip::Installed);\n\t}\n\t_featuredSetsCount = _officialSets.size();\n\tif (wereOfficial.size() > wasFeaturedSetsCount) {\n\t\tconst auto &sets = session().data().stickers().sets();\n\t\tconst auto from = begin(wereOfficial) + wasFeaturedSetsCount;\n\t\tconst auto till = end(wereOfficial);\n\t\tfor (auto i = from; i != till; ++i) {\n\t\t\tauto &set = *i;\n\t\t\tauto it = sets.find(set.id);\n\t\t\tif (it == sets.cend()\n\t\t\t\t|| ((it->second->flags & SetFlag::Installed)\n\t\t\t\t\t&& !(it->second->flags & SetFlag::Archived)\n\t\t\t\t\t&& !_localSetsManager->isInstalledLocally(set.id))) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tset.flags = it->second->flags;\n\t\t\t_officialSets.push_back(std::move(set));\n\t\t}\n\t}\n}\n\nvoid StickersListWidget::refreshSearchSets() {\n\trefreshSearchIndex();\n\n\tconst auto &sets = session().data().stickers().sets();\n\tconst auto skipPremium = !session().premiumPossible();\n\tfor (auto &entry : _searchSets) {\n\t\tif (const auto it = sets.find(entry.id); it != sets.end()) {\n\t\t\tconst auto set = it->second.get();\n\t\t\tentry.flags = set->flags;\n\t\t\tauto elements = PrepareStickers(set->stickers, skipPremium);\n\t\t\tif (!elements.empty()) {\n\t\t\t\tentry.lottiePlayer = nullptr;\n\t\t\t\tentry.stickers = std::move(elements);\n\t\t\t}\n\t\t\tif (!SetInMyList(entry.flags)) {\n\t\t\t\t_localSetsManager->removeInstalledLocally(entry.id);\n\t\t\t\tentry.externalLayout = true;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid StickersListWidget::refreshSearchIndex() {\n\t_searchIndex.clear();\n\tfor (const auto &set : _mySets) {\n\t\tif (set.flags & SetFlag::Special) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto string = set.title + ' ' + set.shortName;\n\t\tconst auto list = TextUtilities::PrepareSearchWords(string);\n\t\t_searchIndex.emplace_back(set.id, list);\n\t}\n}\n\nvoid StickersListWidget::refreshSettingsVisibility() {\n\tconst auto visible = (_section == Section::Stickers)\n\t\t&& _mySets.empty()\n\t\t&& !_isMasks;\n\t_settings->setVisible(visible);\n}\n\nvoid StickersListWidget::refreshFooterIcons() {\n\trefreshIcons(ValidateIconAnimations::None);\n}\n\nvoid StickersListWidget::preloadImages() {\n\tif (_footer) {\n\t\t_footer->preloadImages();\n\t}\n}\n\nuint64 StickersListWidget::currentSet(int yOffset) const {\n\tif (_section == Section::Featured) {\n\t\treturn Data::Stickers::FeaturedSetId;\n\t}\n\tconst auto &sets = shownSets();\n\treturn sets.empty()\n\t\t? Data::Stickers::RecentSetId\n\t\t: sets[sectionInfoByOffset(yOffset).section].id;\n}\n\nbool StickersListWidget::appendSet(\n\t\tstd::vector<Set> &to,\n\t\tuint64 setId,\n\t\tbool externalLayout,\n\t\tAppendSkip skip) {\n\tconst auto &sets = session().data().stickers().sets();\n\tauto it = sets.find(setId);\n\tif (it == sets.cend()\n\t\t|| (!externalLayout && it->second->stickers.isEmpty())) {\n\t\treturn false;\n\t}\n\tconst auto set = it->second.get();\n\tif ((skip == AppendSkip::Archived)\n\t\t&& (set->flags & SetFlag::Archived)) {\n\t\treturn false;\n\t}\n\tif ((skip == AppendSkip::Installed)\n\t\t&& (set->flags & SetFlag::Installed)\n\t\t&& !(set->flags & SetFlag::Archived)) {\n\t\tif (!_localSetsManager->isInstalledLocally(setId)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\tconst auto skipPremium = !session().premiumPossible();\n\tauto elements = PrepareStickers(\n\t\t((set->stickers.empty() && externalLayout)\n\t\t\t? set->covers\n\t\t\t: set->stickers),\n\t\tskipPremium);\n\tif (elements.empty()) {\n\t\treturn false;\n\t}\n\tto.emplace_back(\n\t\tset->id,\n\t\tset,\n\t\tset->flags,\n\t\tset->title,\n\t\tset->shortName,\n\t\tset->count,\n\t\texternalLayout,\n\t\tstd::move(elements));\n\tto.back().thumbnailDocument = set->lookupThumbnailDocument();\n\treturn true;\n}\n\nvoid StickersListWidget::refreshRecent() {\n\tif (_section == Section::Stickers) {\n\t\trefreshRecentStickers();\n\t}\n}\n\nauto StickersListWidget::collectCustomRecents() -> std::vector<Sticker> {\n\t_custom.clear();\n\t_cornerEmoji.clear();\n\tauto result = std::vector<Sticker>();\n\n\tresult.reserve(_customRecentIds.size());\n\tfor (const auto &descriptor : _customRecentIds) {\n\t\tif (const auto document = descriptor.document; document->sticker()) {\n\t\t\tresult.push_back(Sticker{ document });\n\t\t\t_custom.push_back(false);\n\t\t\t_cornerEmoji.push_back(Ui::Emoji::Find(descriptor.cornerEmoji));\n\t\t}\n\t}\n\treturn result;\n}\n\nauto StickersListWidget::collectRecentStickers() -> std::vector<Sticker> {\n\tif (_isEffects) {\n\t\treturn collectCustomRecents();\n\t}\n\t_custom.clear();\n\tauto result = std::vector<Sticker>();\n\n\tconst auto &sets = session().data().stickers().sets();\n\tconst auto &recent = _isMasks\n\t\t? RecentStickerPack()\n\t\t: session().data().stickers().getRecentPack();\n\tconst auto customIt = _isMasks\n\t\t? sets.cend()\n\t\t: sets.find(Data::Stickers::CustomSetId);\n\tconst auto cloudIt = sets.find(_isMasks\n\t\t? Data::Stickers::CloudRecentAttachedSetId\n\t\t: Data::Stickers::CloudRecentSetId);\n\tconst auto customCount = (customIt != sets.cend())\n\t\t? customIt->second->stickers.size()\n\t\t: 0;\n\tconst auto cloudCount = (cloudIt != sets.cend())\n\t\t? cloudIt->second->stickers.size()\n\t\t: 0;\n\tresult.reserve(cloudCount + recent.size() + customCount);\n\t_custom.reserve(cloudCount + recent.size() + customCount);\n\n\tauto add = [&](not_null<DocumentData*> document, bool custom) {\n\t\tif (result.size() >= kRecentDisplayLimit\n\t\t\t&& (!OptionUnlimitedRecentStickers.value() || !Core::App().settings().fork().allRecentStickers())) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto i = ranges::find(result, document, &Sticker::document);\n\t\tif (i != end(result)) {\n\t\t\tconst auto index = (i - begin(result));\n\t\t\tif (index >= cloudCount && custom) {\n\t\t\t\t// Mark stickers from local recent as custom.\n\t\t\t\t_custom[index] = true;\n\t\t\t}\n\t\t} else if (!_favedStickersMap.contains(document)) {\n\t\t\tresult.push_back(Sticker{\n\t\t\t\tdocument\n\t\t\t});\n\t\t\t_custom.push_back(custom);\n\t\t}\n\t};\n\n\tif (cloudCount > 0) {\n\t\tfor (const auto document : cloudIt->second->stickers) {\n\t\t\tadd(document, false);\n\t\t}\n\t}\n\tfor (const auto &recentSticker : recent) {\n\t\tadd(recentSticker.first, false);\n\t}\n\tif (customCount > 0) {\n\t\tfor (const auto document : customIt->second->stickers) {\n\t\t\tadd(document, true);\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid StickersListWidget::refreshRecentStickers(bool performResize) {\n\tclearSelection();\n\n\tauto recentPack = collectRecentStickers();\n\tif (_section == Section::Stickers) {\n\t\t_recentShownCount = recentPack.size();\n\t}\n\tconst auto recentIt = ranges::find_if(_mySets, [](auto &set) {\n\t\treturn set.id == Data::Stickers::RecentSetId;\n\t});\n\tif (!recentPack.empty()) {\n\t\tconst auto shortName = QString();\n\t\tconst auto externalLayout = false;\n\t\tauto set = Set(\n\t\t\tData::Stickers::RecentSetId,\n\t\t\tnullptr,\n\t\t\t(SetFlag::Official | SetFlag::Special),\n\t\t\t(_isEffects\n\t\t\t\t? tr::lng_effect_stickers_title(tr::now)\n\t\t\t\t: tr::lng_recent_stickers(tr::now)),\n\t\t\tshortName,\n\t\t\trecentPack.size(),\n\t\t\texternalLayout,\n\t\t\tstd::move(recentPack));\n\t\tif (recentIt == _mySets.end()) {\n\t\t\tconst auto where = (_mySets.empty()\n\t\t\t\t|| _mySets.begin()->id != Data::Stickers::FavedSetId)\n\t\t\t\t? _mySets.begin()\n\t\t\t\t: (_mySets.begin() + 1);\n\t\t\t_mySets.insert(where, std::move(set));\n\t\t} else {\n\t\t\tstd::swap(*recentIt, set);\n\t\t\ttakeHeavyData(*recentIt, set);\n\t\t}\n\t} else if (recentIt != _mySets.end()) {\n\t\t_mySets.erase(recentIt);\n\t}\n\n\tif ((_section == Section::Stickers || _section == Section::Featured)\n\t\t\t&& performResize) {\n\t\tresizeToWidth(width());\n\t\tupdateSelected();\n\t}\n}\n\nvoid StickersListWidget::refreshFavedStickers() {\n\tif (_isMasks) {\n\t\treturn;\n\t}\n\tclearSelection();\n\tconst auto &sets = session().data().stickers().sets();\n\tconst auto it = sets.find(Data::Stickers::FavedSetId);\n\tif (it == sets.cend()) {\n\t\treturn;\n\t}\n\tconst auto skipPremium = !session().premiumPossible();\n\tconst auto set = it->second.get();\n\tconst auto externalLayout = false;\n\tconst auto shortName = QString();\n\tauto elements = PrepareStickers(set->stickers, skipPremium);\n\tif (elements.empty()) {\n\t\treturn;\n\t}\n\t_mySets.insert(_mySets.begin(), Set{\n\t\tData::Stickers::FavedSetId,\n\t\tnullptr,\n\t\t(SetFlag::Official | SetFlag::Special),\n\t\tLang::Hard::FavedSetTitle(),\n\t\tshortName,\n\t\tset->count,\n\t\texternalLayout,\n\t\tstd::move(elements)\n\t});\n\t_favedStickersMap = base::flat_set<not_null<DocumentData*>> {\n\t\tset->stickers.begin(),\n\t\tset->stickers.end()\n\t};\n}\n\nvoid StickersListWidget::refreshMegagroupStickers(GroupStickersPlace place) {\n\tif (!_features.megagroupSet || !_megagroupSet || _isMasks) {\n\t\treturn;\n\t}\n\tauto canEdit = _megagroupSet->canEditStickers();\n\tauto isShownHere = [place](bool hidden) {\n\t\treturn (hidden == (place == GroupStickersPlace::Hidden));\n\t};\n\tif (!_megagroupSet->mgInfo->stickerSet) {\n\t\tif (canEdit) {\n\t\t\tauto hidden = session().settings().isGroupStickersSectionHidden(\n\t\t\t\t_megagroupSet->id);\n\t\t\tif (isShownHere(hidden)) {\n\t\t\t\tconst auto shortName = QString();\n\t\t\t\tconst auto externalLayout = false;\n\t\t\t\tconst auto count = 0;\n\t\t\t\t_mySets.emplace_back(\n\t\t\t\t\tData::Stickers::MegagroupSetId,\n\t\t\t\t\tnullptr,\n\t\t\t\t\tSetFlag::Special,\n\t\t\t\t\ttr::lng_group_stickers(tr::now),\n\t\t\t\t\tshortName,\n\t\t\t\t\tcount,\n\t\t\t\t\texternalLayout);\n\t\t\t}\n\t\t}\n\t\treturn;\n\t}\n\tauto hidden = session().settings().isGroupStickersSectionHidden(\n\t\t_megagroupSet->id);\n\tauto removeHiddenForGroup = [this, &hidden] {\n\t\tif (hidden) {\n\t\t\tsession().settings().removeGroupStickersSectionHidden(\n\t\t\t\t_megagroupSet->id);\n\t\t\tsession().saveSettings();\n\t\t\thidden = false;\n\t\t}\n\t};\n\tif (canEdit && hidden) {\n\t\tremoveHiddenForGroup();\n\t}\n\tconst auto &set = _megagroupSet->mgInfo->stickerSet;\n\tif (!set.id) {\n\t\treturn;\n\t}\n\tconst auto &sets = session().data().stickers().sets();\n\tconst auto it = sets.find(set.id);\n\tif (it != sets.cend()) {\n\t\tconst auto set = it->second.get();\n\t\tauto isInstalled = (set->flags & SetFlag::Installed)\n\t\t\t&& !(set->flags & SetFlag::Archived);\n\t\tif (isInstalled && !canEdit) {\n\t\t\tremoveHiddenForGroup();\n\t\t} else if (isShownHere(hidden)) {\n\t\t\tconst auto shortName = QString();\n\t\t\tconst auto externalLayout = false;\n\t\t\tconst auto skipPremium = !session().premiumPossible();\n\t\t\tauto elements = PrepareStickers(set->stickers, skipPremium);\n\t\t\tif (!elements.empty()) {\n\t\t\t\t_mySets.emplace_back(\n\t\t\t\t\tData::Stickers::MegagroupSetId,\n\t\t\t\t\tset,\n\t\t\t\t\tSetFlag::Special,\n\t\t\t\t\ttr::lng_group_stickers(tr::now),\n\t\t\t\t\tshortName,\n\t\t\t\t\tset->count,\n\t\t\t\t\texternalLayout,\n\t\t\t\t\tstd::move(elements));\n\t\t\t}\n\t\t}\n\t\treturn;\n\t} else if (!isShownHere(hidden) || _megagroupSetIdRequested == set.id) {\n\t\treturn;\n\t}\n\t_megagroupSetIdRequested = set.id;\n\t_api.request(MTPmessages_GetStickerSet(\n\t\tData::InputStickerSet(set),\n\t\tMTP_int(0) // hash\n\t)).done([=](const MTPmessages_StickerSet &result) {\n\t\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\tif (const auto set = session().data().stickers().feedSetFull(\n\t\t\t\t\tdata)) {\n\t\t\t\trefreshStickers();\n\t\t\t\tif (set->id == _megagroupSetIdRequested) {\n\t\t\t\t\t_megagroupSetIdRequested = 0;\n\t\t\t\t} else {\n\t\t\t\t\tLOG((\"API Error: Got different set.\"));\n\t\t\t\t}\n\t\t\t}\n\t\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t\t});\n\t}).send();\n}\n\nstd::vector<StickerIcon> StickersListWidget::fillIcons() {\n\tauto result = std::vector<StickerIcon>();\n\tresult.reserve(_mySets.size() + 1);\n\tauto i = 0;\n\tif (i != _mySets.size() && _mySets[i].id == Data::Stickers::FavedSetId) {\n\t\t++i;\n\t\tresult.emplace_back(Data::Stickers::FavedSetId);\n\t}\n\tif (i != _mySets.size() && _mySets[i].id == Data::Stickers::RecentSetId) {\n\t\t++i;\n\t\tif (result.empty() || result.back().setId != Data::Stickers::FavedSetId) {\n\t\t\tresult.emplace_back(Data::Stickers::RecentSetId);\n\t\t}\n\t}\n\tconst auto side = StickersListFooter::IconFrameSize();\n\tfor (auto l = _mySets.size(); i != l; ++i) {\n\t\tif (_mySets[i].id == Data::Stickers::MegagroupSetId) {\n\t\t\tresult.emplace_back(Data::Stickers::MegagroupSetId);\n\t\t\tresult.back().megagroup = _megagroupSet;\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto set = _mySets[i].set;\n\t\tAssert(set != nullptr);\n\t\tconst auto s = _mySets[i].thumbnailDocument;\n\t\tconst auto size = set->hasThumbnail()\n\t\t\t? QSize(\n\t\t\t\tset->thumbnailLocation().width(),\n\t\t\t\tset->thumbnailLocation().height())\n\t\t\t: s->hasThumbnail()\n\t\t\t? QSize(\n\t\t\t\ts->thumbnailLocation().width(),\n\t\t\t\ts->thumbnailLocation().height())\n\t\t\t: QSize();\n\t\tconst auto pix = size.scaled(side, side, Qt::KeepAspectRatio);\n\t\tresult.emplace_back(set, s, pix.width(), pix.height());\n\t}\n\treturn result;\n}\n\nvoid StickersListWidget::updateSelected() {\n\tif (!v::is_null(_pressed) && !_previewShown) {\n\t\treturn;\n\t}\n\n\tauto newSelected = OverState { v::null };\n\tauto p = mapFromGlobal(_lastMousePosition);\n\tif (!rect().contains(p)\n\t\t|| p.y() < getVisibleTop() || p.y() >= getVisibleBottom()\n\t\t|| !isVisible()) {\n\t\tclearSelection();\n\t\treturn;\n\t}\n\tauto &sets = shownSets();\n\tauto sx = (rtl() ? width() - p.x() : p.x()) - stickersLeft();\n\tif (!shownSets().empty()) {\n\t\tauto info = sectionInfoByOffset(p.y());\n\t\tauto section = info.section;\n\t\tif (p.y() >= info.top && p.y() < info.rowsTop) {\n\t\t\tif (hasRemoveButton(section)\n\t\t\t\t\t&& myrtlrect(removeButtonRect(info)).contains(p)) {\n\t\t\t\tnewSelected = OverButton{ section };\n\t\t\t} else if (featuredHasAddButton(section)\n\t\t\t\t\t&& myrtlrect(featuredAddRect(info, false)).contains(p)) {\n\t\t\t\tnewSelected = OverButton{ section };\n\t\t\t} else if (_features.openStickerSets\n\t\t\t\t&& !(sets[section].flags & SetFlag::Special)) {\n\t\t\t\tnewSelected = OverSet{ section };\n\t\t\t} else if ((sets[section].id == Data::Stickers::MegagroupSetId)\n\t\t\t\t&& (_megagroupSet->canEditStickers()\n\t\t\t\t\t|| !sets[section].stickers.empty())) {\n\t\t\t\tnewSelected = OverSet{ section };\n\t\t\t}\n\t\t} else if (p.y() >= info.rowsTop\n\t\t\t\t&& p.y() < info.rowsBottom\n\t\t\t\t&& sx >= 0) {\n\t\t\tconst auto yOffset = p.y() - info.rowsTop;\n\t\t\tconst auto &set = sets[section];\n\t\t\tif (set.id == Data::Stickers::MegagroupSetId\n\t\t\t\t\t&& set.stickers.empty()) {\n\t\t\t\tif (_megagroupSetButtonRect.contains(\n\t\t\t\t\t\tstickersLeft() + sx,\n\t\t\t\t\t\tyOffset)) {\n\t\t\t\t\tnewSelected = OverGroupAdd{};\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst auto rowIndex = qFloor(yOffset / _singleSize.height());\n\t\t\t\tconst auto columnIndex = qFloor(sx / _singleSize.width());\n\t\t\t\tconst auto index = rowIndex * _columnCount + columnIndex;\n\t\t\t\tif (index >= 0 && index < set.stickers.size()) {\n\t\t\t\t\tauto overDelete = false;\n\t\t\t\t\tif (stickerHasDeleteButton(set, index)) {\n\t\t\t\t\t\tconst auto inx = sx\n\t\t\t\t\t\t\t- (columnIndex * _singleSize.width());\n\t\t\t\t\t\tconst auto iny = yOffset\n\t\t\t\t\t\t\t- (rowIndex * _singleSize.height());\n\t\t\t\t\t\tconst auto &icon = st::stickerPanDeleteIconBg;\n\t\t\t\t\t\tif (inx >= _singleSize.width() - icon.width()\n\t\t\t\t\t\t\t&& iny < icon.height()) {\n\t\t\t\t\t\t\toverDelete = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tnewSelected = OverSticker { section, index, overDelete };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tsetSelected(newSelected);\n}\n\nbool StickersListWidget::setHasTitle(const Set &set) const {\n\tif (_isEffects) {\n\t\treturn true;\n\t} else if (set.id == Data::Stickers::FavedSetId\n\t\t|| set.id == SearchEmojiSectionSetId()) {\n\t\treturn false;\n\t} else if (set.id == Data::Stickers::RecentSetId) {\n\t\treturn !_mySets.empty()\n\t\t\t&& (_isMasks || (_mySets[0].id == Data::Stickers::FavedSetId));\n\t}\n\treturn true;\n}\n\nbool StickersListWidget::stickerHasDeleteButton(const Set &set, int index) const {\n\tif (set.id == Data::Stickers::RecentSetId) {\n\t\tAssert(index >= 0 && index < _custom.size());\n\t\treturn _custom[index];\n\t}\n\treturn (set.id == Data::Stickers::FavedSetId);\n}\n\nvoid StickersListWidget::setSelected(OverState newSelected) {\n\tif (_selected != newSelected) {\n\t\tsetCursor(!v::is_null(newSelected)\n\t\t\t? style::cur_pointer\n\t\t\t: style::cur_default);\n\n\t\tauto &sets = shownSets();\n\t\tauto updateSelected = [&]() {\n\t\t\tif (auto sticker = std::get_if<OverSticker>(&_selected)) {\n\t\t\t\trtlupdate(stickerRect(sticker->section, sticker->index));\n\t\t\t} else if (auto button = std::get_if<OverButton>(&_selected)) {\n\t\t\t\tif (button->section >= 0\n\t\t\t\t\t&& button->section < sets.size()\n\t\t\t\t\t&& sets[button->section].externalLayout) {\n\t\t\t\t\trtlupdate(featuredAddRect(button->section));\n\t\t\t\t} else {\n\t\t\t\t\trtlupdate(removeButtonRect(button->section));\n\t\t\t\t}\n\t\t\t} else if (std::get_if<OverGroupAdd>(&_selected)) {\n\t\t\t\trtlupdate(megagroupSetButtonRectFinal());\n\t\t\t}\n\t\t};\n\t\tupdateSelected();\n\t\t_selected = newSelected;\n\t\tupdateSelected();\n\n\t\tif (_previewShown && _pressed != _selected) {\n\t\t\tif (const auto sticker = std::get_if<OverSticker>(&_selected)) {\n\t\t\t\t_pressed = _selected;\n\t\t\t\tAssert(sticker->section >= 0\n\t\t\t\t\t&& sticker->section < sets.size());\n\t\t\t\tconst auto &set = sets[sticker->section];\n\t\t\t\tAssert(sticker->index >= 0\n\t\t\t\t\t&& sticker->index < set.stickers.size());\n\t\t\t\tconst auto document = set.stickers[sticker->index].document;\n\t\t\t\t_show->showMediaPreview(\n\t\t\t\t\tdocument->stickerSetOrigin(),\n\t\t\t\t\tdocument);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid StickersListWidget::showPreview() {\n\tif (const auto sticker = std::get_if<OverSticker>(&_pressed)) {\n\t\tconst auto &sets = shownSets();\n\t\tAssert(sticker->section >= 0 && sticker->section < sets.size());\n\t\tconst auto &set = sets[sticker->section];\n\t\tAssert(sticker->index >= 0 && sticker->index < set.stickers.size());\n\t\tconst auto document = set.stickers[sticker->index].document;\n\t\t_show->showMediaPreview(document->stickerSetOrigin(), document);\n\t\t_previewShown = true;\n\t}\n}\n\nauto StickersListWidget::getLottieRenderer()\n-> std::shared_ptr<Lottie::FrameRenderer> {\n\tif (auto result = _lottieRenderer.lock()) {\n\t\treturn result;\n\t}\n\tauto result = Lottie::MakeFrameRenderer();\n\t_lottieRenderer = result;\n\treturn result;\n}\n\nvoid StickersListWidget::showStickerSet(uint64 setId) {\n\tif (_showingSetById) {\n\t\treturn;\n\t}\n\t_showingSetById = true;\n\tconst auto guard = gsl::finally([&] { _showingSetById = false; });\n\n\tclearSelection();\n\tif (!_searchQuery.isEmpty() || !_searchNextQuery.isEmpty()) {\n\t\tif (_search) {\n\t\t\t_search->cancel();\n\t\t}\n\t\tcancelSetsSearch();\n\t}\n\n\tif (setId == Data::Stickers::FeaturedSetId) {\n\t\tif (_section != Section::Featured) {\n\t\t\tsetSection(Section::Featured);\n\t\t\trefreshRecentStickers(true);\n\t\t\trefreshSettingsVisibility();\n\t\t\trefreshIcons(ValidateIconAnimations::Scroll);\n\t\t\trepaintItems();\n\t\t}\n\n\t\tscrollTo(0);\n\t\t_scrollUpdated.fire({});\n\t\treturn;\n\t}\n\n\tauto needRefresh = (_section != Section::Stickers);\n\tif (needRefresh) {\n\t\tsetSection(Section::Stickers);\n\t\trefreshRecentStickers(true);\n\t\trefreshSettingsVisibility();\n\t}\n\n\tauto y = 0;\n\tenumerateSections([this, setId, &y](const SectionInfo &info) {\n\t\tif (shownSets()[info.section].id == setId) {\n\t\t\ty = info.section ? info.top : 0;\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n\tscrollTo(y);\n\t_scrollUpdated.fire({});\n\n\tif (needRefresh) {\n\t\trefreshIcons(ValidateIconAnimations::Scroll);\n\t}\n\n\t_lastMousePosition = QCursor::pos();\n\n\trepaintItems();\n}\n\nvoid StickersListWidget::refreshIcons(ValidateIconAnimations animations) {\n\tif (_footer) {\n\t\t_footer->refreshIcons(\n\t\t\tfillIcons(),\n\t\t\tcurrentSet(getVisibleTop()),\n\t\t\t[=] { return getLottieRenderer(); },\n\t\t\tanimations);\n\t}\n}\n\nvoid StickersListWidget::refreshMegagroupSetGeometry() {\n\tconst auto left = megagroupSetInfoLeft();\n\tconst auto availableWidth = (width() - left);\n\tconst auto top = _megagroupSetAbout.countHeight(availableWidth)\n\t\t+ st::stickerGroupCategoryAddMargin.top();\n\tconst auto &st = st::stickerGroupCategoryAdd;\n\t_megagroupSetButtonTextWidth = st.style.font->width(\n\t\t_megagroupSetButtonText);\n\tconst auto buttonWidth = _megagroupSetButtonTextWidth - st.width;\n\t_megagroupSetButtonRect = QRect(left, top, buttonWidth, st.height);\n}\n\nvoid StickersListWidget::showMegagroupSet(ChannelData *megagroup) {\n\tExpects(!megagroup || megagroup->isMegagroup());\n\n\tif (_megagroupSet != megagroup) {\n\t\t_megagroupSet = megagroup;\n\n\t\tif (_megagroupSetAbout.isEmpty()) {\n\t\t\t_megagroupSetAbout.setText(\n\t\t\t\tst::stickerGroupCategoryAbout,\n\t\t\t\ttr::lng_group_stickers_description(tr::now));\n\t\t\t_megagroupSetButtonText = tr::lng_group_stickers_add(tr::now);\n\t\t\trefreshMegagroupSetGeometry();\n\t\t}\n\t\t_megagroupSetButtonRipple.reset();\n\n\t\trefreshStickers();\n\t}\n}\n\nvoid StickersListWidget::afterShown() {\n\tif (_search) {\n\t\t_search->stealFocus();\n\t}\n}\n\nvoid StickersListWidget::beforeHiding() {\n\tif (_search) {\n\t\t_search->returnFocus();\n\t}\n}\n\nvoid StickersListWidget::setupSearch() {\n\tconst auto session = &_show->session();\n\tconst auto type = (_mode == Mode::UserpicBuilder)\n\t\t? TabbedSearchType::ProfilePhoto\n\t\t: (_mode == Mode::ChatIntro)\n\t\t? TabbedSearchType::Greeting\n\t\t: TabbedSearchType::Stickers;\n\t_search = MakeSearch(this, st(), [=](std::vector<QString> &&query) {\n\t\tapplySearchQuery(std::move(query));\n\t}, session, type);\n}\n\nvoid StickersListWidget::applySearchQuery(std::vector<QString> &&query) {\n\tauto set = base::flat_set<EmojiPtr>();\n\tauto text = ranges::accumulate(query, QString(), [](\n\t\t\tQString a,\n\t\t\tQString b) {\n\t\treturn a.isEmpty() ? b : (a + ' ' + b);\n\t});\n\tsearchForSets(std::move(text), SearchEmoji(query, set));\n}\n\nvoid StickersListWidget::displaySet(uint64 setId) {\n\tif (setId == Data::Stickers::MegagroupSetId) {\n\t\tif (_megagroupSet->canEditStickers()) {\n\t\t\tconst auto isEmoji = false;\n\t\t\tcheckHideWithBox(Box<StickersBox>(_show, _megagroupSet, isEmoji));\n\t\t\treturn;\n\t\t} else if (_megagroupSet->mgInfo->stickerSet.id) {\n\t\t\tsetId = _megagroupSet->mgInfo->stickerSet.id;\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto &sets = session().data().stickers().sets();\n\tauto it = sets.find(setId);\n\tif (it != sets.cend()) {\n\t\tcheckHideWithBox(Box<StickerSetBox>(_show, it->second.get()));\n\t}\n}\n\nvoid StickersListWidget::removeMegagroupSet(bool locally) {\n\tif (locally) {\n\t\tsession().settings().setGroupStickersSectionHidden(_megagroupSet->id);\n\t\tsession().saveSettings();\n\t\trefreshStickers();\n\t\treturn;\n\t}\n\tcheckHideWithBox(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_stickers_remove_group_set(),\n\t\t.confirmed = crl::guard(this, [this, group = _megagroupSet](\n\t\t\t\tFn<void()> &&close) {\n\t\t\tExpects(group->mgInfo != nullptr);\n\n\t\t\tif (group->mgInfo->stickerSet) {\n\t\t\t\tsession().api().setGroupStickerSet(group, {});\n\t\t\t}\n\t\t\tclose();\n\t\t}),\n\t\t.cancelled = [](Fn<void()> &&close) { close(); },\n\t\t.labelStyle = &st().boxLabel,\n\t}));\n}\n\nvoid StickersListWidget::removeSet(uint64 setId) {\n\tconst auto &st = this->st().boxLabel;\n\tif (setId == Data::Stickers::MegagroupSetId) {\n\t\tconst auto &sets = shownSets();\n\t\tconst auto i = ranges::find(sets, setId, &Set::id);\n\t\tAssert(i != end(sets));\n\t\tconst auto removeLocally = i->stickers.empty()\n\t\t\t|| !_megagroupSet->canEditStickers();\n\t\tremoveMegagroupSet(removeLocally);\n\t} else if (auto box = MakeConfirmRemoveSetBox(&session(), st, setId)) {\n\t\tcheckHideWithBox(std::move(box));\n\t}\n}\n\nconst Data::StickersSetsOrder &StickersListWidget::defaultSetsOrder() const {\n\treturn _isMasks\n\t\t? session().data().stickers().maskSetsOrder()\n\t\t: session().data().stickers().setsOrder();\n}\n\nData::StickersSetsOrder &StickersListWidget::defaultSetsOrderRef() {\n\treturn _isMasks\n\t\t? session().data().stickers().maskSetsOrderRef()\n\t\t: session().data().stickers().setsOrderRef();\n}\n\nbool StickersListWidget::mySetsEmpty() const {\n\treturn _mySets.empty();\n}\n\nvoid StickersListWidget::filterEffectsByEmoji(\n\t\tconst std::vector<EmojiPtr> &emoji) {\n\t_filteredStickers.clear();\n\t_filterStickersCornerEmoji.clear();\n\tif (_mySets.empty()\n\t\t|| _mySets.front().id != Data::Stickers::RecentSetId\n\t\t|| _mySets.front().stickers.empty()) {\n\t\treturn;\n\t}\n\tconst auto &list = _mySets.front().stickers;\n\tauto all = base::flat_set<EmojiPtr>();\n\tfor (const auto &one : emoji) {\n\t\tall.emplace(one->original());\n\t}\n\tconst auto count = int(list.size());\n\t_filteredStickers.reserve(count);\n\t_filterStickersCornerEmoji.reserve(count);\n\tfor (auto i = 0; i != count; ++i) {\n\t\tAssert(i < _cornerEmoji.size());\n\t\tif (all.contains(_cornerEmoji[i])) {\n\t\t\t_filteredStickers.push_back(list[i].document);\n\t\t\t_filterStickersCornerEmoji.push_back(_cornerEmoji[i]);\n\t\t}\n\t}\n}\n\nStickersListWidget::~StickersListWidget() = default;\n\nobject_ptr<Ui::BoxContent> MakeConfirmRemoveSetBox(\n\t\tnot_null<Main::Session*> session,\n\t\tconst style::FlatLabel &st,\n\t\tuint64 setId) {\n\tconst auto &sets = session->data().stickers().sets();\n\tconst auto it = sets.find(setId);\n\tif (it == sets.cend()) {\n\t\treturn nullptr;\n\t}\n\tconst auto set = it->second.get();\n\tconst auto text = tr::lng_stickers_remove_pack(\n\t\ttr::now,\n\t\tlt_sticker_pack,\n\t\tset->title);\n\treturn Ui::MakeConfirmBox({\n\t\t.text = text,\n\t\t.confirmed = [=](Fn<void()> &&close) {\n\t\t\tclose();\n\t\t\tconst auto &sets = session->data().stickers().sets();\n\t\t\tconst auto it = sets.find(setId);\n\t\t\tif (it != sets.cend()) {\n\t\t\t\tconst auto set = it->second.get();\n\t\t\t\tif (set->id && set->accessHash) {\n\t\t\t\t\tsession->api().request(MTPmessages_UninstallStickerSet(\n\t\t\t\t\t\tMTP_inputStickerSetID(\n\t\t\t\t\t\t\tMTP_long(set->id),\n\t\t\t\t\t\t\tMTP_long(set->accessHash)))\n\t\t\t\t\t).send();\n\t\t\t\t} else if (!set->shortName.isEmpty()) {\n\t\t\t\t\tsession->api().request(MTPmessages_UninstallStickerSet(\n\t\t\t\t\t\tMTP_inputStickerSetShortName(\n\t\t\t\t\t\t\tMTP_string(set->shortName)))\n\t\t\t\t\t).send();\n\t\t\t\t}\n\t\t\t\tauto writeRecent = false;\n\t\t\t\tauto &recent = session->data().stickers().getRecentPack();\n\t\t\t\tfor (auto i = recent.begin(); i != recent.cend();) {\n\t\t\t\t\tif (set->stickers.indexOf(i->first) >= 0) {\n\t\t\t\t\t\ti = recent.erase(i);\n\t\t\t\t\t\twriteRecent = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t++i;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tset->flags &= ~SetFlag::Installed;\n\t\t\t\tset->installDate = TimeId(0);\n\t\t\t\tauto &orderRef = (set->type() == Data::StickersType::Emoji)\n\t\t\t\t\t? session->data().stickers().emojiSetsOrderRef()\n\t\t\t\t\t: (set->type() == Data::StickersType::Masks)\n\t\t\t\t\t? session->data().stickers().maskSetsOrderRef()\n\t\t\t\t\t: session->data().stickers().setsOrderRef();\n\t\t\t\tconst auto removeIndex = orderRef.indexOf(setId);\n\t\t\t\tif (removeIndex >= 0) {\n\t\t\t\t\torderRef.removeAt(removeIndex);\n\t\t\t\t}\n\t\t\t\tif (set->type() == Data::StickersType::Emoji) {\n\t\t\t\t\tsession->local().writeInstalledCustomEmoji();\n\t\t\t\t} else if (set->type() == Data::StickersType::Masks) {\n\t\t\t\t\tsession->local().writeInstalledMasks();\n\t\t\t\t} else {\n\t\t\t\t\tsession->local().writeInstalledStickers();\n\t\t\t\t}\n\t\t\t\tif (writeRecent) {\n\t\t\t\t\tsession->saveSettings();\n\t\t\t\t}\n\t\t\t\tsession->data().stickers().notifyUpdated(set->type());\n\t\t\t}\n\t\t},\n\t\t.confirmText = tr::lng_stickers_remove_pack_confirm(),\n\t\t.labelStyle = &st,\n\t});\n}\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/stickers_list_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"chat_helpers/compose/compose_features.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"ui/round_rect.h\"\n#include \"base/variant.h\"\n#include \"base/timer.h\"\n\nclass StickerPremiumMark;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass LinkButton;\nclass PopupMenu;\nclass RippleAnimation;\nclass BoxContent;\nclass PathShiftGradient;\nclass TabbedSearch;\n} // namespace Ui\n\nnamespace Lottie {\nclass Animation;\nclass MultiPlayer;\nclass FrameRenderer;\n} // namespace Lottie\n\nnamespace Data {\nclass DocumentMedia;\nclass StickersSet;\n} // namespace Data\n\nnamespace Media::Clip {\nclass ReaderPointer;\nenum class Notification;\n} // namespace Media::Clip\n\nnamespace style {\nstruct EmojiPan;\nstruct FlatLabel;\n} // namespace style\n\nnamespace ChatHelpers {\n\nextern const char kOptionUnlimitedRecentStickers[];\n[[nodiscard]] QVector<MTPstring> SearchStickersLangCodes();\n\nstruct StickerIcon;\nenum class ValidateIconAnimations;\nclass StickersListFooter;\nclass LocalStickersManager;\n\nenum class StickersListMode {\n\tFull,\n\tMasks,\n\tUserpicBuilder,\n\tChatIntro,\n\tMessageEffects,\n};\n\nstruct StickerCustomRecentDescriptor {\n\tnot_null<DocumentData*> document;\n\tQString cornerEmoji;\n};\n\nstruct StickersListDescriptor {\n\tstd::shared_ptr<Show> show;\n\tStickersListMode mode = StickersListMode::Full;\n\tFn<bool()> paused;\n\tstd::vector<StickerCustomRecentDescriptor> customRecentList;\n\tconst style::EmojiPan *st = nullptr;\n\tComposeFeatures features;\n};\n\nclass StickersListWidget final : public TabbedSelector::Inner {\npublic:\n\tusing Mode = StickersListMode;\n\n\tStickersListWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tPauseReason level,\n\t\tMode mode = Mode::Full);\n\tStickersListWidget(\n\t\tQWidget *parent,\n\t\tStickersListDescriptor &&descriptor);\n\n\trpl::producer<FileChosen> chosen() const;\n\trpl::producer<> scrollUpdated() const;\n\trpl::producer<TabbedSelector::Action> choosingUpdated() const;\n\n\tvoid refreshRecent() override;\n\tvoid preloadImages() override;\n\tvoid clearSelection() override;\n\tobject_ptr<TabbedSelector::InnerFooter> createFooter() override;\n\n\tvoid showStickerSet(uint64 setId);\n\tvoid showMegagroupSet(ChannelData *megagroup);\n\n\tvoid afterShown() override;\n\tvoid beforeHiding() override;\n\n\tvoid refreshStickers();\n\n\tstd::vector<StickerIcon> fillIcons();\n\n\tuint64 currentSet(int yOffset) const;\n\n\tvoid sendSearchRequest();\n\tvoid searchForSets(const QString &query, std::vector<EmojiPtr> emoji);\n\n\tstd::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();\n\n\tbase::unique_qptr<Ui::PopupMenu> fillContextMenu(\n\t\tconst SendMenu::Details &details) override;\n\n\tbool mySetsEmpty() const;\n\n\tvoid applySearchQuery(std::vector<QString> &&query);\n\t[[nodiscard]] rpl::producer<int> recentShownCount() const;\n\n\t~StickersListWidget();\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid leaveToChildEvent(QEvent *e, QWidget *child) override;\n\tvoid enterFromChildEvent(QEvent *e, QWidget *child) override;\n\n\tTabbedSelector::InnerFooter *getFooter() const override;\n\tvoid processHideFinished() override;\n\tvoid processPanelHideFinished() override;\n\tint countDesiredHeight(int newWidth) override;\n\nprivate:\n\tstruct Sticker;\n\tstruct Set;\n\n\tenum class Section {\n\t\tFeatured,\n\t\tStickers,\n\t\tSearch,\n\t};\n\n\tstruct OverSticker {\n\t\tint section = 0;\n\t\tint index = 0;\n\t\tbool overDelete = false;\n\n\t\tinline bool operator==(OverSticker other) const {\n\t\t\treturn (section == other.section)\n\t\t\t\t&& (index == other.index)\n\t\t\t\t&& (overDelete == other.overDelete);\n\t\t}\n\t\tinline bool operator!=(OverSticker other) const {\n\t\t\treturn !(*this == other);\n\t\t}\n\t};\n\tstruct OverSet {\n\t\tint section = 0;\n\n\t\tinline bool operator==(OverSet other) const {\n\t\t\treturn (section == other.section);\n\t\t}\n\t\tinline bool operator!=(OverSet other) const {\n\t\t\treturn !(*this == other);\n\t\t}\n\t};\n\tstruct OverButton {\n\t\tint section = 0;\n\n\t\tinline bool operator==(OverButton other) const {\n\t\t\treturn (section == other.section);\n\t\t}\n\t\tinline bool operator!=(OverButton other) const {\n\t\t\treturn !(*this == other);\n\t\t}\n\t};\n\tstruct OverGroupAdd {\n\t\tinline bool operator==(OverGroupAdd other) const {\n\t\t\treturn true;\n\t\t}\n\t\tinline bool operator!=(OverGroupAdd other) const {\n\t\t\treturn !(*this == other);\n\t\t}\n\t};\n\tusing OverState = std::variant<\n\t\tv::null_t,\n\t\tOverSticker,\n\t\tOverSet,\n\t\tOverButton,\n\t\tOverGroupAdd>;\n\n\tstruct SectionInfo {\n\t\tint section = 0;\n\t\tint count = 0;\n\t\tint top = 0;\n\t\tint rowsCount = 0;\n\t\tint rowsTop = 0;\n\t\tint rowsBottom = 0;\n\t};\n\n\tstruct FeaturedSet {\n\t\tuint64 id = 0;\n\t\tData::StickersSetFlags flags;\n\t\tstd::vector<Sticker> stickers;\n\t};\n\n\tstatic std::vector<Sticker> PrepareStickers(\n\t\tconst QVector<DocumentData*> &pack,\n\t\tbool skipPremium);\n\n\tvoid setupSearch();\n\tvoid preloadMoreOfficial();\n\tQSize boundingBoxSize() const;\n\n\ttemplate <typename Callback>\n\tbool enumerateSections(Callback callback) const;\n\tSectionInfo sectionInfo(int section) const;\n\tSectionInfo sectionInfoByOffset(int yOffset) const;\n\n\tvoid setSection(Section section);\n\tvoid displaySet(uint64 setId);\n\tvoid removeMegagroupSet(bool locally);\n\tvoid removeSet(uint64 setId);\n\tvoid refreshMySets();\n\tvoid refreshFeaturedSets();\n\tvoid refreshSearchSets();\n\tvoid refreshSearchIndex();\n\n\tbool setHasTitle(const Set &set) const;\n\tbool stickerHasDeleteButton(const Set &set, int index) const;\n\t[[nodiscard]] std::vector<Sticker> collectRecentStickers();\n\t[[nodiscard]] std::vector<Sticker> collectCustomRecents();\n\tvoid refreshRecentStickers(bool resize = true);\n\tvoid refreshEffects();\n\tvoid refreshFavedStickers();\n\tenum class GroupStickersPlace {\n\t\tVisible,\n\t\tHidden,\n\t};\n\tvoid refreshMegagroupStickers(GroupStickersPlace place);\n\tvoid refreshSettingsVisibility();\n\n\tvoid updateSelected();\n\tvoid setSelected(OverState newSelected);\n\tvoid setPressed(OverState newPressed);\n\t[[nodiscard]] std::unique_ptr<Ui::RippleAnimation> createButtonRipple(\n\t\tint section);\n\t[[nodiscard]] QPoint buttonRippleTopLeft(int section) const;\n\n\t[[nodiscard]] std::vector<Set> &shownSets();\n\t[[nodiscard]] const std::vector<Set> &shownSets() const;\n\t[[nodiscard]] int featuredRowHeight() const;\n\tvoid checkVisibleFeatured(int visibleTop, int visibleBottom);\n\tvoid readVisibleFeatured(int visibleTop, int visibleBottom);\n\n\tvoid paintStickers(Painter &p, QRect clip);\n\tvoid paintMegagroupEmptySet(Painter &p, int y, bool buttonSelected);\n\tvoid paintSticker(\n\t\tPainter &p,\n\t\tSet &set,\n\t\tint y,\n\t\tint section,\n\t\tint index,\n\t\tcrl::time now,\n\t\tbool paused,\n\t\tbool selected,\n\t\tbool deleteSelected);\n\n\tvoid ensureLottiePlayer(Set &set);\n\tvoid setupLottie(Set &set, int section, int index);\n\tvoid setupWebm(Set &set, int section, int index);\n\tvoid clipCallback(\n\t\tMedia::Clip::Notification notification,\n\t\tuint64 setId,\n\t\tnot_null<DocumentData*> document,\n\t\tint indexHint);\n\t[[nodiscard]] bool itemVisible(const SectionInfo &info, int index) const;\n\tvoid markLottieFrameShown(Set &set);\n\tvoid checkVisibleLottie();\n\tvoid pauseInvisibleLottieIn(const SectionInfo &info);\n\tvoid takeHeavyData(std::vector<Set> &to, std::vector<Set> &from);\n\tvoid takeHeavyData(Set &to, Set &from);\n\tvoid takeHeavyData(Sticker &to, Sticker &from);\n\tvoid clearHeavyIn(Set &set, bool clearSavedFrames = true);\n\tvoid clearHeavyData();\n\tvoid updateItems();\n\tvoid updateSets();\n\tvoid repaintItems(crl::time now = 0);\n\tvoid updateSet(const SectionInfo &info);\n\tvoid repaintItems(\n\t\tconst SectionInfo &info,\n\t\tcrl::time now);\n\n\t[[nodiscard]] int stickersRight() const;\n\t[[nodiscard]] bool featuredHasAddButton(int index) const;\n\t[[nodiscard]] QRect featuredAddRect(int index) const;\n\t[[nodiscard]] QRect featuredAddRect(\n\t\tconst SectionInfo &info,\n\t\tbool installedSet) const;\n\t[[nodiscard]] bool hasRemoveButton(int index) const;\n\t[[nodiscard]] QRect removeButtonRect(int index) const;\n\t[[nodiscard]] QRect removeButtonRect(const SectionInfo &info) const;\n\t[[nodiscard]] int megagroupSetInfoLeft() const;\n\tvoid refreshMegagroupSetGeometry();\n\t[[nodiscard]] QRect megagroupSetButtonRectFinal() const;\n\n\t[[nodiscard]] const Data::StickersSetsOrder &defaultSetsOrder() const;\n\t[[nodiscard]] Data::StickersSetsOrder &defaultSetsOrderRef();\n\tvoid filterEffectsByEmoji(const std::vector<EmojiPtr> &emoji);\n\n\tenum class AppendSkip {\n\t\tNone,\n\t\tArchived,\n\t\tInstalled,\n\t};\n\tbool appendSet(\n\t\tstd::vector<Set> &to,\n\t\tuint64 setId,\n\t\tbool externalLayout,\n\t\tAppendSkip skip = AppendSkip::None);\n\n\tint stickersLeft() const;\n\tQRect stickerRect(int section, int sel);\n\n\tvoid removeRecentSticker(int section, int index);\n\tvoid removeFavedSticker(int section, int index);\n\tvoid setColumnCount(int count);\n\tvoid refreshFooterIcons();\n\tvoid refreshIcons(ValidateIconAnimations animations);\n\n\tvoid showStickerSetBox(\n\t\tnot_null<DocumentData*> document,\n\t\tuint64 setId);\n\n\tvoid cancelSetsSearch();\n\tvoid showSearchResults();\n\tvoid sendSearchSetsRequest(const QString &query);\n\tvoid searchResultsDone(\n\t\tconst QString &query,\n\t\tconst MTPmessages_FoundStickerSets &result);\n\tvoid requestSearchStickers(\n\t\tconst QString &query,\n\t\tint offset,\n\t\tbool requestSetsOnEmpty);\n\tvoid searchStickersResultsDone(\n\t\tconst QString &query,\n\t\tint requestedOffset,\n\t\tbool requestSetsOnEmpty,\n\t\tconst MTPmessages_FoundStickers &result);\n\tvoid loadMoreSearchStickers();\n\tvoid checkPaginateSearchStickers(int visibleTop, int visibleBottom);\n\tvoid refreshSearchRows();\n\tvoid refreshSearchRows(const std::vector<uint64> *cloudSets);\n\tvoid fillFilteredStickersRow();\n\tvoid fillLocalSearchRows(const QString &query);\n\tvoid fillCloudSearchRows(const std::vector<uint64> &cloudSets);\n\tvoid fillFoundStickersRow(const std::vector<DocumentId> &stickerIds);\n\tvoid addSearchRow(not_null<Data::StickersSet*> set);\n\tvoid toggleSearchLoading(bool loading);\n\n\tvoid showPreview();\n\n\tUi::MessageSendingAnimationFrom messageSentAnimationInfo(\n\t\tint section,\n\t\tint index,\n\t\tnot_null<DocumentData*> document);\n\n\tconst Mode _mode;\n\tconst std::shared_ptr<Show> _show;\n\tconst ComposeFeatures _features;\n\tUi::RoundRect _overBg;\n\tstd::unique_ptr<Ui::TabbedSearch> _search;\n\tMTP::Sender _api;\n\tstd::unique_ptr<LocalStickersManager> _localSetsManager;\n\tChannelData *_megagroupSet = nullptr;\n\tuint64 _megagroupSetIdRequested = 0;\n\tstd::vector<StickerCustomRecentDescriptor> _customRecentIds;\n\tstd::vector<Set> _mySets;\n\tstd::vector<Set> _officialSets;\n\tstd::vector<Set> _searchSets;\n\tint _featuredSetsCount = 0;\n\tstd::vector<bool> _custom;\n\tstd::vector<EmojiPtr> _cornerEmoji;\n\tbase::flat_set<not_null<DocumentData*>> _favedStickersMap;\n\tstd::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;\n\n\tbool _paintAsPremium = false;\n\tbool _showingSetById = false;\n\tcrl::time _lastScrolledAt = 0;\n\tcrl::time _lastFullUpdatedAt = 0;\n\n\tmtpRequestId _officialRequestId = 0;\n\tint _officialOffset = 0;\n\n\tSection _section = Section::Stickers;\n\tconst bool _isMasks;\n\tconst bool _isEffects;\n\n\tbase::Timer _updateItemsTimer;\n\tbase::Timer _updateSetsTimer;\n\tbase::flat_set<uint64> _repaintSetsIds;\n\n\tStickersListFooter *_footer = nullptr;\n\tint _rowsLeft = 0;\n\tint _columnCount = 1;\n\tQSize _singleSize;\n\n\tOverState _selected;\n\tOverState _pressed;\n\tQPoint _lastMousePosition;\n\n\tUi::RoundRect _trendingAddBgOver, _trendingAddBg, _inactiveButtonBg;\n\tUi::RoundRect _groupCategoryAddBgOver, _groupCategoryAddBg;\n\n\tconst std::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\n\tUi::Text::String _megagroupSetAbout;\n\tQString _megagroupSetButtonText;\n\tint _megagroupSetButtonTextWidth = 0;\n\tQRect _megagroupSetButtonRect;\n\tstd::unique_ptr<Ui::RippleAnimation> _megagroupSetButtonRipple;\n\n\tQString _addText;\n\tint _addWidth;\n\tQString _installedText;\n\tint _installedWidth;\n\n\tobject_ptr<Ui::LinkButton> _settings;\n\n\tbase::Timer _previewTimer;\n\tbool _previewShown = false;\n\n\tstd::unique_ptr<StickerPremiumMark> _premiumMark;\n\n\tstd::vector<not_null<DocumentData*>> _filteredStickers;\n\tstd::vector<EmojiPtr> _filterStickersCornerEmoji;\n\trpl::variable<int> _recentShownCount;\n\tstd::map<QString, std::vector<uint64>> _searchSetsCache;\n\tstd::map<QString, std::vector<DocumentId>> _searchStickersCache;\n\tstd::map<QString, int> _searchStickersNextOffset;\n\tstd::vector<std::pair<uint64, QStringList>> _searchIndex;\n\tbase::Timer _searchRequestTimer;\n\tQString _searchQuery, _searchNextQuery;\n\tmtpRequestId _searchSetsRequestId = 0;\n\tmtpRequestId _searchStickersRequestId = 0;\n\tbool _searchLoading = false;\n\n\trpl::event_stream<FileChosen> _chosen;\n\trpl::event_stream<> _scrollUpdated;\n\trpl::event_stream<TabbedSelector::Action> _choosingUpdated;\n\n};\n\n[[nodiscard]] object_ptr<Ui::BoxContent> MakeConfirmRemoveSetBox(\n\tnot_null<Main::Session*> session,\n\tconst style::FlatLabel &st,\n\tuint64 setId);\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/stickers_lottie.h\"\n\n#include \"lottie/lottie_single_player.h\"\n#include \"lottie/lottie_multi_player.h\"\n#include \"data/stickers/data_stickers_set.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_file_origin.h\"\n#include \"storage/cache/storage_cache_database.h\"\n#include \"storage/localimageloader.h\"\n#include \"history/view/media/history_view_media_common.h\"\n#include \"media/clip/media_clip_reader.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/image/image_location_factory.h\"\n#include \"ui/painter.h\"\n#include \"main/main_session.h\"\n\n#include <xxhash.h>\n\nnamespace ChatHelpers {\nnamespace {\n\nconstexpr auto kDontCacheLottieAfterArea = 512 * 512;\n\n[[nodiscard]] uint64 LocalStickerId(QStringView name) {\n\tauto full = u\"local_sticker:\"_q;\n\tfull.append(name);\n\treturn XXH64(full.data(), full.size() * sizeof(QChar), 0);\n}\n\n} // namespace\n\nuint8 LottieCacheKeyShift(uint8 replacementsTag, StickerLottieSize sizeTag) {\n\treturn ((replacementsTag << 4) & 0xF0) | (uint8(sizeTag) & 0x0F);\n}\n\ntemplate <typename Method>\nauto LottieCachedFromContent(\n\t\tMethod &&method,\n\t\tStorage::Cache::Key baseKey,\n\t\tuint8 keyShift,\n\t\tnot_null<Main::Session*> session,\n\t\tconst QByteArray &content,\n\t\tQSize box) {\n\tconst auto key = Storage::Cache::Key{\n\t\tbaseKey.high,\n\t\tbaseKey.low + keyShift\n\t};\n\tconst auto get = [=](FnMut<void(QByteArray &&cached)> handler) {\n\t\tsession->data().cacheBigFile().get(\n\t\t\tkey,\n\t\t\tstd::move(handler));\n\t};\n\tconst auto weak = base::make_weak(session);\n\tconst auto put = [=](QByteArray &&cached) {\n\t\tcrl::on_main(weak, [=, data = std::move(cached)]() mutable {\n\t\t\tweak->data().cacheBigFile().put(key, std::move(data));\n\t\t});\n\t};\n\treturn method(\n\t\tget,\n\t\tput,\n\t\tcontent,\n\t\tLottie::FrameRequest{ box });\n}\n\ntemplate <typename Method>\nauto LottieFromDocument(\n\t\tMethod &&method,\n\t\tnot_null<Data::DocumentMedia*> media,\n\t\tuint8 keyShift,\n\t\tQSize box) {\n\tconst auto document = media->owner();\n\tconst auto data = media->bytes();\n\tconst auto filepath = document->filepath();\n\tif (box.width() * box.height() > kDontCacheLottieAfterArea) {\n\t\t// Don't use frame caching for large stickers.\n\t\treturn method(\n\t\t\tLottie::ReadContent(data, filepath),\n\t\t\tLottie::FrameRequest{ box });\n\t}\n\tif (const auto baseKey = document->bigFileBaseCacheKey()) {\n\t\treturn LottieCachedFromContent(\n\t\t\tstd::forward<Method>(method),\n\t\t\tbaseKey,\n\t\t\tkeyShift,\n\t\t\t&document->session(),\n\t\t\tLottie::ReadContent(data, filepath),\n\t\t\tbox);\n\t}\n\treturn method(\n\t\tLottie::ReadContent(data, filepath),\n\t\tLottie::FrameRequest{ box });\n}\n\nstd::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(\n\t\tnot_null<Data::DocumentMedia*> media,\n\t\tStickerLottieSize sizeTag,\n\t\tQSize box,\n\t\tLottie::Quality quality,\n\t\tstd::shared_ptr<Lottie::FrameRenderer> renderer) {\n\treturn LottiePlayerFromDocument(\n\t\tmedia,\n\t\tnullptr,\n\t\tsizeTag,\n\t\tbox,\n\t\tquality,\n\t\tstd::move(renderer));\n}\n\nstd::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(\n\t\tnot_null<Data::DocumentMedia*> media,\n\t\tconst Lottie::ColorReplacements *replacements,\n\t\tStickerLottieSize sizeTag,\n\t\tQSize box,\n\t\tLottie::Quality quality,\n\t\tstd::shared_ptr<Lottie::FrameRenderer> renderer) {\n\tconst auto method = [&](auto &&...args) {\n\t\treturn std::make_unique<Lottie::SinglePlayer>(\n\t\t\tstd::forward<decltype(args)>(args)...,\n\t\t\tquality,\n\t\t\treplacements,\n\t\t\tstd::move(renderer));\n\t};\n\tconst auto keyShift = LottieCacheKeyShift(\n\t\treplacements ? replacements->tag : uint8(0),\n\t\tsizeTag);\n\treturn LottieFromDocument(method, media, uint8(keyShift), box);\n}\n\nnot_null<Lottie::Animation*> LottieAnimationFromDocument(\n\t\tnot_null<Lottie::MultiPlayer*> player,\n\t\tnot_null<Data::DocumentMedia*> media,\n\t\tStickerLottieSize sizeTag,\n\t\tQSize box) {\n\tconst auto method = [&](auto &&...args) {\n\t\treturn player->append(std::forward<decltype(args)>(args)...);\n\t};\n\treturn LottieFromDocument(method, media, uint8(sizeTag), box);\n}\n\nbool HasLottieThumbnail(\n\t\tStickerType thumbType,\n\t\tData::StickersSetThumbnailView *thumb,\n\t\tData::DocumentMedia *media) {\n\tif (thumb) {\n\t\treturn (thumbType == StickerType::Tgs)\n\t\t\t&& !thumb->content().isEmpty();\n\t} else if (!media) {\n\t\treturn false;\n\t}\n\tconst auto document = media->owner();\n\tif (const auto info = document->sticker()) {\n\t\tif (!info->isLottie()) {\n\t\t\treturn false;\n\t\t}\n\t\tmedia->automaticLoad(document->stickerSetOrigin(), nullptr);\n\t\tif (!media->loaded()) {\n\t\t\treturn false;\n\t\t}\n\t\treturn document->bigFileBaseCacheKey().valid();\n\t}\n\treturn false;\n}\n\nstd::unique_ptr<Lottie::SinglePlayer> LottieThumbnail(\n\t\tData::StickersSetThumbnailView *thumb,\n\t\tData::DocumentMedia *media,\n\t\tStickerLottieSize sizeTag,\n\t\tQSize box,\n\t\tstd::shared_ptr<Lottie::FrameRenderer> renderer) {\n\tconst auto baseKey = thumb\n\t\t? thumb->owner()->thumbnailBigFileBaseCacheKey()\n\t\t: media\n\t\t? media->owner()->bigFileBaseCacheKey()\n\t\t: Storage::Cache::Key();\n\tif (!baseKey) {\n\t\treturn nullptr;\n\t}\n\tconst auto content = thumb\n\t\t? thumb->content()\n\t\t: Lottie::ReadContent(media->bytes(), media->owner()->filepath());\n\tif (content.isEmpty()) {\n\t\treturn nullptr;\n\t}\n\tconst auto method = [](auto &&...args) {\n\t\treturn std::make_unique<Lottie::SinglePlayer>(\n\t\t\tstd::forward<decltype(args)>(args)...);\n\t};\n\tconst auto session = thumb\n\t\t? &thumb->owner()->session()\n\t\t: &media->owner()->session();\n\treturn LottieCachedFromContent(\n\t\tmethod,\n\t\tbaseKey,\n\t\tuint8(sizeTag),\n\t\tsession,\n\t\tcontent,\n\t\tbox);\n}\n\nbool HasWebmThumbnail(\n\t\tStickerType thumbType,\n\t\tData::StickersSetThumbnailView *thumb,\n\t\tData::DocumentMedia *media) {\n\tif (thumb) {\n\t\treturn (thumbType == StickerType::Webm)\n\t\t\t&& !thumb->content().isEmpty();\n\t} else if (!media) {\n\t\treturn false;\n\t}\n\tconst auto document = media->owner();\n\tif (const auto info = document->sticker()) {\n\t\tif (!info->isWebm()) {\n\t\t\treturn false;\n\t\t}\n\t\tmedia->automaticLoad(document->stickerSetOrigin(), nullptr);\n\t\tif (!media->loaded()) {\n\t\t\treturn false;\n\t\t}\n\t\treturn document->bigFileBaseCacheKey().valid();\n\t}\n\treturn false;\n}\n\nMedia::Clip::ReaderPointer WebmThumbnail(\n\t\tData::StickersSetThumbnailView *thumb,\n\t\tData::DocumentMedia *media,\n\t\tFn<void(Media::Clip::Notification)> callback) {\n\treturn thumb\n\t\t? ::Media::Clip::MakeReader(\n\t\t\tthumb->content(),\n\t\t\tstd::move(callback))\n\t\t: ::Media::Clip::MakeReader(\n\t\t\tmedia->owner()->location(),\n\t\t\tmedia->bytes(),\n\t\t\tstd::move(callback));\n}\n\nbool PaintStickerThumbnailPath(\n\t\tQPainter &p,\n\t\tnot_null<Data::DocumentMedia*> media,\n\t\tQRect target,\n\t\tQLinearGradient *gradient,\n\t\tbool mirrorHorizontal) {\n\tconst auto &path = media->thumbnailPath();\n\tconst auto dimensions = media->owner()->dimensions;\n\tif (path.isEmpty() || dimensions.isEmpty() || target.isEmpty()) {\n\t\treturn false;\n\t}\n\tp.save();\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setPen(Qt::NoPen);\n\tp.translate(target.topLeft());\n\tif (gradient) {\n\t\tconst auto scale = dimensions.width() / float64(target.width());\n\t\tconst auto shift = p.worldTransform().dx();\n\t\tgradient->setStart((gradient->start().x() - shift) * scale, 0);\n\t\tgradient->setFinalStop(\n\t\t\t(gradient->finalStop().x() - shift) * scale,\n\t\t\t0);\n\t\tp.setBrush(*gradient);\n\t}\n\tif (mirrorHorizontal) {\n\t\tconst auto c = QPointF(target.width() / 2., target.height() / 2.);\n\t\tp.translate(c);\n\t\tp.scale(-1., 1.);\n\t\tp.translate(-c);\n\t}\n\tp.scale(\n\t\ttarget.width() / float64(dimensions.width()),\n\t\ttarget.height() / float64(dimensions.height()));\n\tp.drawPath(path);\n\tp.restore();\n\treturn true;\n}\n\nbool PaintStickerThumbnailPath(\n\t\tQPainter &p,\n\t\tnot_null<Data::DocumentMedia*> media,\n\t\tQRect target,\n\t\tnot_null<Ui::PathShiftGradient*> gradient,\n\t\tbool mirrorHorizontal) {\n\treturn gradient->paint([&](const Ui::PathShiftGradient::Background &bg) {\n\t\tif (const auto color = std::get_if<style::color>(&bg)) {\n\t\t\tp.setBrush(*color);\n\t\t\treturn PaintStickerThumbnailPath(\n\t\t\t\tp,\n\t\t\t\tmedia,\n\t\t\t\ttarget,\n\t\t\t\tnullptr,\n\t\t\t\tmirrorHorizontal);\n\t\t}\n\t\tconst auto gradient = v::get<QLinearGradient*>(bg);\n\t\treturn PaintStickerThumbnailPath(\n\t\t\tp,\n\t\t\tmedia,\n\t\t\ttarget,\n\t\t\tgradient,\n\t\t\tmirrorHorizontal);\n\t});\n}\n\nQSize ComputeStickerSize(not_null<DocumentData*> document, QSize box) {\n\tconst auto sticker = document->sticker();\n\tconst auto dimensions = document->dimensions;\n\tif (!sticker || !sticker->isLottie() || dimensions.isEmpty()) {\n\t\treturn HistoryView::DownscaledSize(dimensions, box);\n\t}\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto request = Lottie::FrameRequest{ box * ratio };\n\treturn HistoryView::NonEmptySize(request.size(dimensions, 8) / ratio);\n}\n\nnot_null<DocumentData*> GenerateLocalSticker(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &path) {\n\tauto task = FileLoadTask(FileLoadTask::Args{\n\t\t.session = session,\n\t\t.filepath = path,\n\t\t.type = SendMediaType::File,\n\t\t.to = FileLoadTo(0, {}, {}, 0),\n\t\t.caption = {},\n\t\t.idOverride = LocalStickerId(path),\n\t});\n\ttask.process({ .generateGoodThumbnail = false });\n\tconst auto result = task.peekResult();\n\tAssert(result != nullptr);\n\tconst auto document = session->data().processDocument(\n\t\tresult->document,\n\t\tImages::FromImageInMemory(\n\t\t\tresult->thumb,\n\t\t\t\"WEBP\",\n\t\t\tresult->thumbbytes));\n\tdocument->setLocation(Core::FileLocation(path));\n\n\tEnsures(document->sticker());\n\treturn document;\n}\n\nnot_null<DocumentData*> GenerateLocalTgsSticker(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &name,\n\t\tbool useTextColor) {\n\tconst auto cache = [&] {\n\t\tstruct Session {\n\t\t\tbase::weak_ptr<Main::Session> session;\n\t\t\tbase::flat_map<QString, not_null<DocumentData*>> cache;\n\t\t};\n\t\tstatic auto Map = std::vector<Session>();\n\t\tfor (auto i = begin(Map); i != end(Map);) {\n\t\t\tif (const auto strong = i->session.get()) {\n\t\t\t\tif (strong == session) {\n\t\t\t\t\treturn &i->cache;\n\t\t\t\t}\n\t\t\t\t++i;\n\t\t\t} else {\n\t\t\t\ti = Map.erase(i);\n\t\t\t}\n\t\t}\n\t\tMap.push_back({ .session = session });\n\t\treturn &Map.back().cache;\n\t}();\n\n\tconst auto key = useTextColor ? (name + u\"/:/1\"_q) : name;\n\tconst auto i = cache->find(key);\n\tif (i != end(*cache)) {\n\t\treturn i->second;\n\t}\n\n\tconst auto result = GenerateLocalSticker(\n\t\tsession,\n\t\tu\":/animations/\"_q + name + u\".tgs\"_q);\n\tif (useTextColor) {\n\t\tresult->overrideEmojiUsesTextColor(true);\n\t}\n\n\tcache->emplace(key, result);\n\n\tEnsures(result->sticker()->isLottie());\n\treturn result;\n}\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/stickers_lottie.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nenum class StickerType : uchar;\n\nnamespace base {\ntemplate <typename Enum>\nclass Flags;\n} // namespace base\n\nnamespace Storage {\nnamespace Cache {\nstruct Key;\n} // namespace Cache\n} // namespace Storage\n\nnamespace Media::Clip {\nclass ReaderPointer;\nenum class Notification;\n} // namespace Media::Clip\n\nnamespace Lottie {\nclass SinglePlayer;\nclass MultiPlayer;\nclass FrameRenderer;\nclass Animation;\nenum class Quality : char;\nstruct ColorReplacements;\n} // namespace Lottie\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass PathShiftGradient;\n} // namespace Ui\n\nnamespace Data {\nclass DocumentMedia;\nclass StickersSetThumbnailView;\nenum class StickersSetFlag : ushort;\nusing StickersSetFlags = base::flags<StickersSetFlag>;\n} // namespace Data\n\nnamespace ChatHelpers {\n\nenum class StickerLottieSize : uint8 {\n\tMessageHistory,\n\tStickerSet, // In Emoji used for forum topic profile cover icons.\n\tStickersPanel,\n\tStickersFooter,\n\tSetsListThumbnail,\n\tInlineResults,\n\tEmojiInteraction,\n\tEmojiInteractionReserved1,\n\tEmojiInteractionReserved2,\n\tEmojiInteractionReserved3,\n\tEmojiInteractionReserved4,\n\tEmojiInteractionReserved5,\n\tEmojiInteractionReserved6,\n\tEmojiInteractionReserved7,\n\tChatIntroHelloSticker,\n\tStickerEmojiSize,\n\tPinnedProfileUniqueGiftSize,\n};\n[[nodiscard]] uint8 LottieCacheKeyShift(\n\tuint8 replacementsTag,\n\tStickerLottieSize sizeTag);\n\n[[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(\n\tnot_null<Data::DocumentMedia*> media,\n\tStickerLottieSize sizeTag,\n\tQSize box,\n\tLottie::Quality quality = Lottie::Quality(),\n\tstd::shared_ptr<Lottie::FrameRenderer> renderer = nullptr);\n[[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(\n\tnot_null<Data::DocumentMedia*> media,\n\tconst Lottie::ColorReplacements *replacements,\n\tStickerLottieSize sizeTag,\n\tQSize box,\n\tLottie::Quality quality = Lottie::Quality(),\n\tstd::shared_ptr<Lottie::FrameRenderer> renderer = nullptr);\n[[nodiscard]] not_null<Lottie::Animation*> LottieAnimationFromDocument(\n\tnot_null<Lottie::MultiPlayer*> player,\n\tnot_null<Data::DocumentMedia*> media,\n\tStickerLottieSize sizeTag,\n\tQSize box);\n\n[[nodiscard]] bool HasLottieThumbnail(\n\tStickerType thumbType,\n\tData::StickersSetThumbnailView *thumb,\n\tData::DocumentMedia *media);\n[[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> LottieThumbnail(\n\tData::StickersSetThumbnailView *thumb,\n\tData::DocumentMedia *media,\n\tStickerLottieSize sizeTag,\n\tQSize box,\n\tstd::shared_ptr<Lottie::FrameRenderer> renderer = nullptr);\n\n[[nodiscard]] bool HasWebmThumbnail(\n\tStickerType thumbType,\n\tData::StickersSetThumbnailView *thumb,\n\tData::DocumentMedia *media);\n[[nodiscard]] Media::Clip::ReaderPointer WebmThumbnail(\n\tData::StickersSetThumbnailView *thumb,\n\tData::DocumentMedia *media,\n\tFn<void(Media::Clip::Notification)> callback);\n\nbool PaintStickerThumbnailPath(\n\tQPainter &p,\n\tnot_null<Data::DocumentMedia*> media,\n\tQRect target,\n\tQLinearGradient *gradient = nullptr,\n\tbool mirrorHorizontal = false);\n\nbool PaintStickerThumbnailPath(\n\tQPainter &p,\n\tnot_null<Data::DocumentMedia*> media,\n\tQRect target,\n\tnot_null<Ui::PathShiftGradient*> gradient,\n\tbool mirrorHorizontal = false);\n\n[[nodiscard]] QSize ComputeStickerSize(\n\tnot_null<DocumentData*> document,\n\tQSize box);\n\n[[nodiscard]] not_null<DocumentData*> GenerateLocalSticker(\n\tnot_null<Main::Session*> session,\n\tconst QString &path);\n\n[[nodiscard]] not_null<DocumentData*> GenerateLocalTgsSticker(\n\tnot_null<Main::Session*> session,\n\tconst QString &name,\n\tbool useTextColor = false);\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/tabbed_panel.h\"\n\n#include \"ui/widgets/shadow.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/ui_utility.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"window/window_session_controller.h\"\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"core/application.h\"\n#include \"base/options.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace ChatHelpers {\nnamespace {\n\nconstexpr auto kHideTimeoutMs = 300;\nconstexpr auto kDelayedHideTimeoutMs = 3000;\n\nbase::options::toggle TabbedPanelShowOnClick({\n\t.id = kOptionTabbedPanelShowOnClick,\n\t.name = \"Show tabbed panel by click\",\n\t.description = \"Show Emoji / Stickers / GIFs panel only after a click.\",\n});\n\n} // namespace\n\nconst char kOptionTabbedPanelShowOnClick[] = \"tabbed-panel-show-on-click\";\n\nbool ShowPanelOnClick() {\n\treturn TabbedPanelShowOnClick.value();\n}\n\nTabbedPanel::TabbedPanel(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<TabbedSelector*> selector)\n: TabbedPanel(parent, {\n\t.regularWindow = controller,\n\t.nonOwnedSelector = selector,\n}) {\n}\n\nTabbedPanel::TabbedPanel(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tobject_ptr<TabbedSelector> selector)\n: TabbedPanel(parent, {\n\t.regularWindow = controller,\n\t.ownedSelector = std::move(selector),\n}) {\n}\n\nTabbedPanel::TabbedPanel(\n\tQWidget *parent,\n\tTabbedPanelDescriptor &&descriptor)\n: RpWidget(parent)\n, _regularWindow(descriptor.regularWindow)\n, _ownedSelector(std::move(descriptor.ownedSelector))\n, _selector(descriptor.nonOwnedSelector\n\t? descriptor.nonOwnedSelector\n\t: _ownedSelector.data())\n, _heightRatio(st::emojiPanHeightRatio)\n, _minContentHeight(st::emojiPanMinHeight)\n, _maxContentHeight(st::emojiPanMaxHeight)\n, _shadow(_selector->st().showAnimation.shadow) {\n\tExpects(_selector != nullptr);\n\n\t_selector->setParent(this);\n\t_selector->setRoundRadius(st::emojiPanRadius);\n\t_selector->setAfterShownCallback([=](SelectorTab tab) {\n\t\tif (_regularWindow) {\n\t\t\t_regularWindow->enableGifPauseReason(_selector->level());\n\t\t}\n\t\t_pauseAnimations.fire(true);\n\t});\n\t_selector->setBeforeHidingCallback([=](SelectorTab tab) {\n\t\tif (_regularWindow) {\n\t\t\t_regularWindow->disableGifPauseReason(_selector->level());\n\t\t}\n\t\t_pauseAnimations.fire(false);\n\t});\n\t_selector->showRequests(\n\t) | rpl::on_next([=] {\n\t\tshowFromSelector();\n\t}, lifetime());\n\n\tresize(\n\t\tQRect(0, 0, st::emojiPanWidth, st::emojiPanMaxHeight).marginsAdded(\n\t\t\tinnerPadding()).size());\n\n\t_contentMaxHeight = st::emojiPanMaxHeight;\n\t_contentHeight = _contentMaxHeight;\n\n\t_selector->resize(st::emojiPanWidth, _contentHeight);\n\t_selector->move(innerRect().topLeft());\n\n\t_hideTimer.setCallback([this] { hideByTimerOrLeave(); });\n\n\t_selector->checkForHide(\n\t) | rpl::on_next([=] {\n\t\tif (!rect().contains(mapFromGlobal(QCursor::pos()))) {\n\t\t\t_hideTimer.callOnce(kDelayedHideTimeoutMs);\n\t\t}\n\t}, lifetime());\n\n\t_selector->cancelled(\n\t) | rpl::on_next([=] {\n\t\thideAnimated();\n\t}, lifetime());\n\n\tif (_regularWindow) {\n\t\t_regularWindow->session().data().stickers().gifWithCaptionSent(\n\t\t) | rpl::on_next([=] {\n\t\t\thideAnimated();\n\t\t}, lifetime());\n\t}\n\n\t_selector->slideFinished(\n\t) | rpl::on_next([=] {\n\t\tInvokeQueued(this, [=] {\n\t\t\tif (_hideAfterSlide) {\n\t\t\t\tstartOpacityAnimation(true);\n\t\t\t}\n\t\t});\n\t}, lifetime());\n\n\tmacWindowDeactivateEvents(\n\t) | rpl::filter([=] {\n\t\treturn !isHidden() && !preventAutoHide();\n\t}) | rpl::on_next([=] {\n\t\thideAnimated();\n\t}, lifetime());\n\n\tsetAttribute(Qt::WA_OpaquePaintEvent, false);\n\n\thideChildren();\n\thide();\n}\n\nnot_null<TabbedSelector*> TabbedPanel::selector() const {\n\treturn _selector;\n}\n\nrpl::producer<bool> TabbedPanel::pauseAnimations() const {\n\treturn _pauseAnimations.events();\n}\n\nbool TabbedPanel::isSelectorStolen() const {\n\treturn (_selector->parent() != this);\n}\n\nvoid TabbedPanel::moveBottomRight(int bottom, int right) {\n\tconst auto isNew = (_bottom != bottom || _right != right);\n\t_bottom = bottom;\n\t_right = right;\n\t// If the panel is already shown, update the position.\n\tif (!isHidden() && isNew) {\n\t\tmoveHorizontally();\n\t} else {\n\t\tupdateContentHeight();\n\t}\n}\n\nvoid TabbedPanel::moveTopRight(int top, int right) {\n\tconst auto isNew = (_top != top || _right != right);\n\t_top = top;\n\t_right = right;\n\t// If the panel is already shown, update the position.\n\tif (!isHidden() && isNew) {\n\t\tmoveHorizontally();\n\t} else {\n\t\tupdateContentHeight();\n\t}\n}\n\nvoid TabbedPanel::setDesiredHeightValues(\n\t\tfloat64 ratio,\n\t\tint minHeight,\n\t\tint maxHeight) {\n\t_heightRatio = ratio;\n\t_minContentHeight = minHeight;\n\t_maxContentHeight = maxHeight;\n\tupdateContentHeight();\n}\n\nvoid TabbedPanel::setDropDown(bool dropDown) {\n\tselector()->setDropDown(dropDown);\n\t_dropDown = dropDown;\n}\n\nvoid TabbedPanel::updateContentHeight() {\n\tauto addedHeight = innerPadding().top() + innerPadding().bottom();\n\tauto marginsHeight = _selector->marginTop() + _selector->marginBottom();\n\tauto availableHeight = _dropDown\n\t\t? (parentWidget()->height() - _top - marginsHeight)\n\t\t: (_bottom - marginsHeight);\n\tauto wantedContentHeight = qRound(_heightRatio * availableHeight)\n\t\t- addedHeight;\n\tauto contentHeight = marginsHeight + std::clamp(\n\t\twantedContentHeight,\n\t\t_minContentHeight,\n\t\t_maxContentHeight);\n\tauto resultTop = _dropDown\n\t\t? _top\n\t\t: (_bottom - addedHeight - contentHeight);\n\tif (contentHeight == _contentHeight) {\n\t\tmove(x(), resultTop);\n\t\treturn;\n\t}\n\n\t_contentHeight = contentHeight;\n\n\tresize(QRect(0, 0, innerRect().width(), _contentHeight).marginsAdded(innerPadding()).size());\n\tmove(x(), resultTop);\n\n\t_selector->resize(innerRect().width(), _contentHeight);\n\n\tupdate();\n}\n\nvoid TabbedPanel::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\t// This call can finish _a_show animation and destroy _showAnimation.\n\tauto opacityAnimating = _a_opacity.animating();\n\n\tauto showAnimating = _a_show.animating();\n\tif (_showAnimation && !showAnimating) {\n\t\t_showAnimation.reset();\n\t\tif (!opacityAnimating) {\n\t\t\tshowChildren();\n\t\t\t_selector->afterShown();\n\t\t}\n\t}\n\n\tif (showAnimating) {\n\t\tAssert(_showAnimation != nullptr);\n\t\tif (auto opacity = _a_opacity.value(_hiding ? 0. : 1.)) {\n\t\t\t_showAnimation->paintFrame(p, 0, 0, width(), _a_show.value(1.), opacity);\n\t\t}\n\t} else if (opacityAnimating) {\n\t\tp.setOpacity(_a_opacity.value(_hiding ? 0. : 1.));\n\t\tp.drawPixmap(0, 0, _cache);\n\t} else if (_hiding || isHidden()) {\n\t\thideFinished();\n\t} else {\n\t\tif (!_cache.isNull()) _cache = QPixmap();\n\t\t_shadow.paint(p, innerRect(), st::emojiPanRadius);\n\t}\n}\n\nvoid TabbedPanel::moveHorizontally() {\n\tconst auto padding = innerPadding();\n\tconst auto width = innerRect().width() + padding.left() + padding.right();\n\tconst auto right = std::max(\n\t\tparentWidget()->width() - std::max(_right, width),\n\t\t0);\n\tmoveToRight(right, y());\n\tupdateContentHeight();\n}\n\nvoid TabbedPanel::enterEventHook(QEnterEvent *e) {\n\tCore::App().registerLeaveSubscription(this);\n\tshowAnimated();\n}\n\nbool TabbedPanel::preventAutoHide() const {\n\treturn _selector->preventAutoHide();\n}\n\nvoid TabbedPanel::leaveEventHook(QEvent *e) {\n\tCore::App().unregisterLeaveSubscription(this);\n\tif (preventAutoHide()) {\n\t\treturn;\n\t}\n\tif (_a_show.animating() || _a_opacity.animating()) {\n\t\thideAnimated();\n\t} else {\n\t\t_hideTimer.callOnce(kHideTimeoutMs);\n\t}\n\treturn RpWidget::leaveEventHook(e);\n}\n\nvoid TabbedPanel::otherEnter() {\n\tshowAnimated();\n}\n\nvoid TabbedPanel::otherLeave() {\n\tif (preventAutoHide()) {\n\t\treturn;\n\t}\n\n\tif (_a_opacity.animating()) {\n\t\thideByTimerOrLeave();\n\t} else {\n\t\t// In case of animations disabled add some delay before hiding.\n\t\t// Otherwise if emoji suggestions panel is shown in between\n\t\t// (z-order wise) the emoji toggle button and tabbed panel,\n\t\t// we won't be able to move cursor from the button to the panel.\n\t\t_hideTimer.callOnce(anim::Disabled() ? kHideTimeoutMs : 0);\n\t}\n}\n\nvoid TabbedPanel::hideFast() {\n\tif (isHidden()) return;\n\n\tif (_selector && !_selector->isHidden()) {\n\t\t_selector->beforeHiding();\n\t}\n\t_hideTimer.cancel();\n\t_hiding = false;\n\t_a_opacity.stop();\n\thideFinished();\n}\n\nvoid TabbedPanel::opacityAnimationCallback() {\n\tupdate();\n\tif (!_a_opacity.animating()) {\n\t\tif (_hiding) {\n\t\t\t_hiding = false;\n\t\t\thideFinished();\n\t\t} else if (!_a_show.animating()) {\n\t\t\tshowChildren();\n\t\t\t_selector->afterShown();\n\t\t}\n\t}\n}\n\nvoid TabbedPanel::hideByTimerOrLeave() {\n\tif (isHidden() || preventAutoHide()) {\n\t\treturn;\n\t}\n\thideAnimated();\n}\n\nvoid TabbedPanel::prepareCacheFor(bool hiding) {\n\tif (_a_opacity.animating()) {\n\t\t_hiding = hiding;\n\t\treturn;\n\t}\n\n\tauto showAnimation = base::take(_a_show);\n\tauto showAnimationData = base::take(_showAnimation);\n\t_hiding = false;\n\tshowChildren();\n\n\t_cache = Ui::GrabWidget(this);\n\n\t_a_show = base::take(showAnimation);\n\t_showAnimation = base::take(showAnimationData);\n\t_hiding = hiding;\n\tif (_a_show.animating()) {\n\t\thideChildren();\n\t}\n}\n\nvoid TabbedPanel::startOpacityAnimation(bool hiding) {\n\tif (_selector && !_selector->isHidden()) {\n\t\t_selector->beforeHiding();\n\t}\n\tprepareCacheFor(hiding);\n\thideChildren();\n\t_a_opacity.start(\n\t\t[=] { opacityAnimationCallback(); },\n\t\t_hiding ? 1. : 0.,\n\t\t_hiding ? 0. : 1.,\n\t\tst::emojiPanDuration);\n}\n\nvoid TabbedPanel::startShowAnimation() {\n\tif (!_a_show.animating()) {\n\t\tauto image = grabForAnimation();\n\n\t\t_showAnimation = std::make_unique<Ui::PanelAnimation>(\n\t\t\t_selector->st().showAnimation,\n\t\t\t(_dropDown\n\t\t\t\t? Ui::PanelAnimation::Origin::TopRight\n\t\t\t\t: Ui::PanelAnimation::Origin::BottomRight));\n\t\tauto inner = rect().marginsRemoved(st::emojiPanMargins);\n\t\t_showAnimation->setFinalImage(\n\t\t\tstd::move(image),\n\t\t\tQRect(\n\t\t\t\tinner.topLeft() * style::DevicePixelRatio(),\n\t\t\t\tinner.size() * style::DevicePixelRatio()),\n\t\t\tst::emojiPanRadius);\n\t\t_showAnimation->setCornerMasks(Images::CornersMask(st::emojiPanRadius));\n\t\t_showAnimation->start();\n\t}\n\thideChildren();\n\t_a_show.start([this] { update(); }, 0., 1., st::emojiPanShowDuration);\n}\n\nQImage TabbedPanel::grabForAnimation() {\n\tauto cache = base::take(_cache);\n\tauto opacityAnimation = base::take(_a_opacity);\n\tauto showAnimationData = base::take(_showAnimation);\n\tauto showAnimation = base::take(_a_show);\n\n\tshowChildren();\n\tUi::SendPendingMoveResizeEvents(this);\n\n\tauto result = QImage(\n\t\tsize() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult.fill(Qt::transparent);\n\tif (_selector) {\n\t\tQPainter p(&result);\n\t\tUi::RenderWidget(p, _selector, _selector->pos());\n\t}\n\n\t_a_show = base::take(showAnimation);\n\t_showAnimation = base::take(showAnimationData);\n\t_a_opacity = base::take(opacityAnimation);\n\t_cache = base::take(cache);\n\n\treturn result;\n}\n\nvoid TabbedPanel::hideAnimated() {\n\tif (isHidden() || _hiding) {\n\t\treturn;\n\t}\n\n\t_hideTimer.cancel();\n\tif (_selector->isSliding()) {\n\t\t_hideAfterSlide = true;\n\t} else {\n\t\tstartOpacityAnimation(true);\n\t}\n\n\t// There is no reason to worry about the message scheduling box\n\t// while it moves the user to the separate scheduled section.\n\t_shouldFinishHide = _selector->hasMenu();\n}\n\nvoid TabbedPanel::toggleAnimated() {\n\tif (isHidden() || _hiding || _hideAfterSlide) {\n\t\tshowAnimated();\n\t} else {\n\t\thideAnimated();\n\t}\n}\n\nvoid TabbedPanel::hideFinished() {\n\thide();\n\t_a_show.stop();\n\t_showAnimation.reset();\n\t_cache = QPixmap();\n\t_hiding = false;\n\t_shouldFinishHide = false;\n\t_selector->hideFinished();\n}\n\nvoid TabbedPanel::showAnimated() {\n\t_hideTimer.cancel();\n\t_hideAfterSlide = false;\n\tshowStarted();\n}\n\nvoid TabbedPanel::showStarted() {\n\tif (_shouldFinishHide) {\n\t\treturn;\n\t}\n\tif (isHidden()) {\n\t\t_selector->showStarted();\n\t\tmoveHorizontally();\n\t\traise();\n\t\tshow();\n\t\tstartShowAnimation();\n\t} else if (_hiding) {\n\t\tstartOpacityAnimation(false);\n\t}\n}\n\nbool TabbedPanel::eventFilter(QObject *obj, QEvent *e) {\n\tif (TabbedPanelShowOnClick.value()) {\n\t\treturn false;\n\t} else if (e->type() == QEvent::Enter) {\n\t\totherEnter();\n\t} else if (e->type() == QEvent::Leave) {\n\t\totherLeave();\n\t}\n\treturn false;\n}\n\nvoid TabbedPanel::showFromSelector() {\n\tif (isHidden()) {\n\t\tmoveHorizontally();\n\t\tstartShowAnimation();\n\t\tshow();\n\t}\n\tshowChildren();\n\tshowAnimated();\n}\n\nstyle::margins TabbedPanel::innerPadding() const {\n\treturn st::emojiPanMargins;\n}\n\nQRect TabbedPanel::innerRect() const {\n\treturn rect().marginsRemoved(innerPadding());\n}\n\nbool TabbedPanel::overlaps(const QRect &globalRect) const {\n\tif (isHidden() || !_cache.isNull()) return false;\n\n\tauto testRect = QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size());\n\tauto inner = rect().marginsRemoved(st::emojiPanMargins);\n\tconst auto radius = st::emojiPanRadius;\n\treturn inner.marginsRemoved(QMargins(radius, 0, radius, 0)).contains(testRect)\n\t\t|| inner.marginsRemoved(QMargins(0, radius, 0, radius)).contains(testRect);\n}\n\nTabbedPanel::~TabbedPanel() {\n\thideFast();\n\tif (!_ownedSelector && _regularWindow) {\n\t\t_regularWindow->takeTabbedSelectorOwnershipFrom(this);\n\t}\n}\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/tabbed_panel.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"base/timer.h\"\n#include \"base/object_ptr.h\"\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass PanelAnimation;\n} // namespace Ui\n\nnamespace ChatHelpers {\n\nclass TabbedSelector;\n\nextern const char kOptionTabbedPanelShowOnClick[];\n[[nodiscard]] bool ShowPanelOnClick();\n\nstruct TabbedPanelDescriptor {\n\tWindow::SessionController *regularWindow = nullptr;\n\tobject_ptr<TabbedSelector> ownedSelector = { nullptr };\n\tTabbedSelector *nonOwnedSelector = nullptr;\n};\n\nclass TabbedPanel : public Ui::RpWidget {\npublic:\n\tTabbedPanel(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<TabbedSelector*> selector);\n\tTabbedPanel(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tobject_ptr<TabbedSelector> selector);\n\tTabbedPanel(QWidget *parent, TabbedPanelDescriptor &&descriptor);\n\n\t[[nodiscard]] bool isSelectorStolen() const;\n\t[[nodiscard]] not_null<TabbedSelector*> selector() const;\n\t[[nodiscard]] rpl::producer<bool> pauseAnimations() const;\n\n\tvoid moveBottomRight(int bottom, int right);\n\tvoid moveTopRight(int top, int right);\n\tvoid setDesiredHeightValues(\n\t\tfloat64 ratio,\n\t\tint minHeight,\n\t\tint maxHeight);\n\tvoid setDropDown(bool dropDown);\n\n\tvoid hideFast();\n\tbool hiding() const {\n\t\treturn _hiding || _hideTimer.isActive();\n\t}\n\n\tbool overlaps(const QRect &globalRect) const;\n\n\tvoid showAnimated();\n\tvoid hideAnimated();\n\tvoid toggleAnimated();\n\n\t~TabbedPanel();\n\nprotected:\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid otherEnter();\n\tvoid otherLeave();\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tbool eventFilter(QObject *obj, QEvent *e) override;\n\nprivate:\n\tvoid hideByTimerOrLeave();\n\tvoid moveHorizontally();\n\tvoid showFromSelector();\n\n\tstyle::margins innerPadding() const;\n\n\t// Rounded rect which has shadow around it.\n\tQRect innerRect() const;\n\n\tQImage grabForAnimation();\n\tvoid startShowAnimation();\n\tvoid startOpacityAnimation(bool hiding);\n\tvoid prepareCacheFor(bool hiding);\n\n\tvoid opacityAnimationCallback();\n\n\tvoid hideFinished();\n\tvoid showStarted();\n\n\tbool preventAutoHide() const;\n\tvoid updateContentHeight();\n\n\tWindow::SessionController * const _regularWindow = nullptr;\n\tconst object_ptr<TabbedSelector> _ownedSelector = { nullptr };\n\tconst not_null<TabbedSelector*> _selector;\n\trpl::event_stream<bool> _pauseAnimations;\n\n\tint _contentMaxHeight = 0;\n\tint _contentHeight = 0;\n\tint _top = 0;\n\tint _bottom = 0;\n\tint _right = 0;\n\tfloat64 _heightRatio = 1.;\n\tint _minContentHeight = 0;\n\tint _maxContentHeight = 0;\n\n\tstd::unique_ptr<Ui::PanelAnimation> _showAnimation;\n\tUi::Animations::Simple _a_show;\n\n\tbool _shouldFinishHide = false;\n\tbool _dropDown = false;\n\n\tbool _hiding = false;\n\tbool _hideAfterSlide = false;\n\tUi::BoxShadow _shadow;\n\tQPixmap _cache;\n\tUi::Animations::Simple _a_opacity;\n\tbase::Timer _hideTimer;\n\n};\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/tabbed_section.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/tabbed_section.h\"\n\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace ChatHelpers {\n\nobject_ptr<Window::SectionWidget> TabbedMemento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tWindow::Column column,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<TabbedSection>(parent, controller);\n\tresult->setGeometry(geometry);\n\treturn result;\n}\n\nTabbedSection::TabbedSection(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Window::SectionWidget(parent, controller)\n, _selector(controller->tabbedSelector()) {\n\tif (Ui::InFocusChain(_selector)) {\n\t\tparent->window()->setFocus();\n\t}\n\t_selector->setParent(this);\n\t_selector->setRoundRadius(0);\n\t_selector->setGeometry(rect());\n\t_selector->showStarted();\n\t_selector->show();\n\t_selector->setAfterShownCallback(nullptr);\n\t_selector->setBeforeHidingCallback(nullptr);\n\n\tsetAttribute(Qt::WA_OpaquePaintEvent, true);\n}\n\nvoid TabbedSection::beforeHiding() {\n\t_selector->beforeHiding();\n}\n\nvoid TabbedSection::afterShown() {\n\t_selector->afterShown();\n}\n\nvoid TabbedSection::resizeEvent(QResizeEvent *e) {\n\t_selector->setGeometry(rect());\n}\n\nvoid TabbedSection::showFinishedHook() {\n\tafterShown();\n}\n\nbool TabbedSection::showInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) {\n\treturn false;\n}\n\nbool TabbedSection::floatPlayerHandleWheelEvent(QEvent *e) {\n\treturn _selector->floatPlayerHandleWheelEvent(e);\n}\n\nQRect TabbedSection::floatPlayerAvailableRect() {\n\treturn _selector->floatPlayerAvailableRect();\n}\n\nTabbedSection::~TabbedSection() {\n\tbeforeHiding();\n\tcontroller()->takeTabbedSelectorOwnershipFrom(this);\n}\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/tabbed_section.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"window/section_widget.h\"\n#include \"window/section_memento.h\"\n\nnamespace ChatHelpers {\n\nclass TabbedSelector;\n\nclass TabbedMemento : public Window::SectionMemento {\npublic:\n\tTabbedMemento() = default;\n\tTabbedMemento(TabbedMemento &&other) = default;\n\tTabbedMemento &operator=(TabbedMemento &&other) = default;\n\n\tobject_ptr<Window::SectionWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tWindow::Column column,\n\t\tconst QRect &geometry) override;\n\n};\n\nclass TabbedSection : public Window::SectionWidget {\npublic:\n\tTabbedSection(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\tvoid beforeHiding();\n\tvoid afterShown();\n\n\tbool showInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) override;\n\tbool forceAnimateBack() const override {\n\t\treturn true;\n\t}\n\t// Float player interface.\n\tbool floatPlayerHandleWheelEvent(QEvent *e) override;\n\tQRect floatPlayerAvailableRect() override;\n\n\t~TabbedSection();\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\tvoid showFinishedHook() override;\n\nprivate:\n\tconst not_null<TabbedSelector*> _selector;\n\n};\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/tabbed_selector.h\"\n\n#include \"chat_helpers/emoji_list_widget.h\"\n#include \"chat_helpers/stickers_list_widget.h\"\n#include \"chat_helpers/gifs_list_widget.h\"\n#include \"menu/menu_send.h\"\n#include \"ui/controls/swipe_handler.h\"\n#include \"ui/controls/tabbed_search.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/discrete_sliders.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/layers/box_content.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_session_controller.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"storage/localstorage.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"data/stickers/data_custom_emoji.h\" // AllowEmojiWithoutPremium.\n#include \"boxes/premium_preview_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"mainwindow.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace ChatHelpers {\n\nclass TabbedSelector::SlideAnimation : public Ui::RoundShadowAnimation {\npublic:\n\tenum class Direction {\n\t\tLeftToRight,\n\t\tRightToLeft,\n\t};\n\tvoid setFinalImages(Direction direction, QImage &&left, QImage &&right, QRect inner, bool wasSectionIcons);\n\n\tvoid start();\n\tvoid paintFrame(QPainter &p, const style::EmojiPan &st, float64 dt, float64 opacity);\n\nprivate:\n\tDirection _direction = Direction::LeftToRight;\n\tQPixmap _leftImage, _rightImage;\n\tint _width = 0;\n\tint _height = 0;\n\tint _innerLeft = 0;\n\tint _innerTop = 0;\n\tint _innerRight = 0;\n\tint _innerBottom = 0;\n\tint _innerWidth = 0;\n\tint _innerHeight = 0;\n\n\tint _painterInnerLeft = 0;\n\tint _painterInnerTop = 0;\n\tint _painterInnerWidth = 0;\n\tint _painterInnerBottom = 0;\n\tint _painterCategoriesTop = 0;\n\tint _painterInnerHeight = 0;\n\tint _painterInnerRight = 0;\n\n\tint _frameIntsPerLineAdd = 0;\n\tbool _wasSectionIcons = false;\n\n};\n\nvoid TabbedSelector::SlideAnimation::setFinalImages(Direction direction, QImage &&left, QImage &&right, QRect inner, bool wasSectionIcons) {\n\tExpects(!started());\n\t_direction = direction;\n\t_leftImage = QPixmap::fromImage(std::move(left).convertToFormat(QImage::Format_ARGB32_Premultiplied), Qt::ColorOnly);\n\t_rightImage = QPixmap::fromImage(std::move(right).convertToFormat(QImage::Format_ARGB32_Premultiplied), Qt::ColorOnly);\n\n\tAssert(!_leftImage.isNull());\n\tAssert(!_rightImage.isNull());\n\t_width = _leftImage.width();\n\t_height = _rightImage.height();\n\tAssert(!(_width % style::DevicePixelRatio()));\n\tAssert(!(_height % style::DevicePixelRatio()));\n\tAssert(_leftImage.devicePixelRatio() == _rightImage.devicePixelRatio());\n\tAssert(_rightImage.width() == _width);\n\tAssert(_rightImage.height() == _height);\n\tAssert(QRect(0, 0, _width, _height).contains(inner));\n\t_innerLeft = inner.x();\n\t_innerTop = inner.y();\n\t_innerWidth = inner.width();\n\t_innerHeight = inner.height();\n\tAssert(!(_innerLeft % style::DevicePixelRatio()));\n\tAssert(!(_innerTop % style::DevicePixelRatio()));\n\tAssert(!(_innerWidth % style::DevicePixelRatio()));\n\tAssert(!(_innerHeight % style::DevicePixelRatio()));\n\t_innerRight = _innerLeft + _innerWidth;\n\t_innerBottom = _innerTop + _innerHeight;\n\n\t_painterInnerLeft = _innerLeft / style::DevicePixelRatio();\n\t_painterInnerTop = _innerTop / style::DevicePixelRatio();\n\t_painterInnerRight = _innerRight / style::DevicePixelRatio();\n\t_painterInnerBottom = _innerBottom / style::DevicePixelRatio();\n\t_painterInnerWidth = _innerWidth / style::DevicePixelRatio();\n\t_painterInnerHeight = _innerHeight / style::DevicePixelRatio();\n\t_painterCategoriesTop = _painterInnerBottom - st::defaultEmojiPan.footer;\n\n\t_wasSectionIcons = wasSectionIcons;\n}\n\nvoid TabbedSelector::SlideAnimation::start() {\n\tAssert(!_leftImage.isNull());\n\tAssert(!_rightImage.isNull());\n\tRoundShadowAnimation::start(_width, _height, _leftImage.devicePixelRatio());\n\tauto checkCorner = [this](const Corner &corner) {\n\t\tif (!corner.valid()) return;\n\t\tAssert(corner.width <= _innerWidth);\n\t\tAssert(corner.height <= _innerHeight);\n\t};\n\tcheckCorner(_topLeft);\n\tcheckCorner(_topRight);\n\tcheckCorner(_bottomLeft);\n\tcheckCorner(_bottomRight);\n\t_frameIntsPerLineAdd = (_width - _innerWidth) + _frameIntsPerLineAdded;\n}\n\nvoid TabbedSelector::SlideAnimation::paintFrame(\n\t\tQPainter &p,\n\t\tconst style::EmojiPan &st,\n\t\tfloat64 dt,\n\t\tfloat64 opacity) {\n\tExpects(started());\n\tExpects(dt >= 0.);\n\n\t_frameAlpha = anim::interpolate(1, 256, opacity);\n\n\tauto leftToRight = (_direction == Direction::LeftToRight);\n\n\tauto easeOut = anim::easeOutCirc(1., dt);\n\tauto easeIn = anim::easeInCirc(1., dt);\n\n\tauto arrivingCoord = anim::interpolate(_innerWidth, 0, easeOut);\n\tauto departingCoord = anim::interpolate(0, _innerWidth, easeIn);\n\tif (auto decrease = (arrivingCoord % style::DevicePixelRatio())) {\n\t\tarrivingCoord -= decrease;\n\t}\n\tif (auto decrease = (departingCoord % style::DevicePixelRatio())) {\n\t\tdepartingCoord -= decrease;\n\t}\n\tauto arrivingAlpha = easeIn;\n\tauto departingAlpha = 1. - easeOut;\n\tauto leftCoord = (leftToRight ? arrivingCoord : departingCoord) * -1;\n\tauto leftAlpha = (leftToRight ? arrivingAlpha : departingAlpha);\n\tauto rightCoord = (leftToRight ? departingCoord : arrivingCoord);\n\tauto rightAlpha = (leftToRight ? departingAlpha : arrivingAlpha);\n\n\t// _innerLeft ..(left).. leftTo ..(both).. bothTo ..(none).. noneTo ..(right).. _innerRight\n\tauto leftTo = _innerLeft\n\t\t+ std::clamp(_innerWidth + leftCoord, 0, _innerWidth);\n\tauto rightFrom = _innerLeft + std::clamp(rightCoord, 0, _innerWidth);\n\tauto painterRightFrom = rightFrom / style::DevicePixelRatio();\n\tif (opacity < 1.) {\n\t\t_frame.fill(Qt::transparent);\n\t}\n\t{\n\t\tauto p = QPainter(&_frame);\n\t\tp.setOpacity(opacity);\n\t\tp.fillRect(_painterInnerLeft, _painterInnerTop, _painterInnerWidth, _painterCategoriesTop - _painterInnerTop, st.bg);\n\t\tp.fillRect(_painterInnerLeft, _painterCategoriesTop, _painterInnerWidth, _painterInnerBottom - _painterCategoriesTop, _wasSectionIcons ? st.categoriesBg : st.bg);\n\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\tif (leftTo > _innerLeft) {\n\t\t\tp.setOpacity(opacity * leftAlpha);\n\t\t\tp.drawPixmap(_painterInnerLeft, _painterInnerTop, _leftImage, _innerLeft - leftCoord, _innerTop, leftTo - _innerLeft, _innerHeight);\n\t\t}\n\t\tif (rightFrom < _innerRight) {\n\t\t\tp.setOpacity(opacity * rightAlpha);\n\t\t\tp.drawPixmap(painterRightFrom, _painterInnerTop, _rightImage, _innerLeft, _innerTop, _innerRight - rightFrom, _innerHeight);\n\t\t}\n\t}\n\n\t// Draw corners\n\t//paintCorner(_topLeft, _innerLeft, _innerTop);\n\t//paintCorner(_topRight, _innerRight - _topRight.width, _innerTop);\n\tpaintCorner(_bottomLeft, _innerLeft, _innerBottom - _bottomLeft.height);\n\tpaintCorner(_bottomRight, _innerRight - _bottomRight.width, _innerBottom - _bottomRight.height);\n\n\t// Draw shadow upon the transparent\n\tauto outerLeft = _innerLeft;\n\tauto outerTop = _innerTop;\n\tauto outerRight = _innerRight;\n\tauto outerBottom = _innerBottom;\n\tif (_shadow.valid()) {\n\t\touterLeft -= _shadow.extend.left();\n\t\touterTop -= _shadow.extend.top();\n\t\touterRight += _shadow.extend.right();\n\t\touterBottom += _shadow.extend.bottom();\n\t}\n\tif (style::DevicePixelRatio() > 1) {\n\t\tif (auto skipLeft = (outerLeft % style::DevicePixelRatio())) {\n\t\t\touterLeft -= skipLeft;\n\t\t}\n\t\tif (auto skipTop = (outerTop % style::DevicePixelRatio())) {\n\t\t\touterTop -= skipTop;\n\t\t}\n\t\tif (auto skipRight = (outerRight % style::DevicePixelRatio())) {\n\t\t\touterRight += (style::DevicePixelRatio() - skipRight);\n\t\t}\n\t\tif (auto skipBottom = (outerBottom % style::DevicePixelRatio())) {\n\t\t\touterBottom += (style::DevicePixelRatio() - skipBottom);\n\t\t}\n\t}\n\n\tif (opacity == 1.) {\n\t\t// Fill above the frame top with transparent.\n\t\tauto fillTopInts = (_frameInts + outerTop * _frameIntsPerLine + outerLeft);\n\t\tauto fillWidth = (outerRight - outerLeft) * sizeof(uint32);\n\t\tfor (auto fillTop = _innerTop - outerTop; fillTop != 0; --fillTop) {\n\t\t\tmemset(fillTopInts, 0, fillWidth);\n\t\t\tfillTopInts += _frameIntsPerLine;\n\t\t}\n\n\t\t// Fill to the left and to the right of the frame with transparent.\n\t\tauto fillLeft = (_innerLeft - outerLeft) * sizeof(uint32);\n\t\tauto fillRight = (outerRight - _innerRight) * sizeof(uint32);\n\t\tif (fillLeft || fillRight) {\n\t\t\tauto fillInts = _frameInts + _innerTop * _frameIntsPerLine;\n\t\t\tfor (auto y = _innerTop; y != _innerBottom; ++y) {\n\t\t\t\tmemset(fillInts + outerLeft, 0, fillLeft);\n\t\t\t\tmemset(fillInts + _innerRight, 0, fillRight);\n\t\t\t\tfillInts += _frameIntsPerLine;\n\t\t\t}\n\t\t}\n\n\t\t// Fill below the frame bottom with transparent.\n\t\tauto fillBottomInts = (_frameInts + _innerBottom * _frameIntsPerLine + outerLeft);\n\t\tfor (auto fillBottom = outerBottom - _innerBottom; fillBottom != 0; --fillBottom) {\n\t\t\tmemset(fillBottomInts, 0, fillWidth);\n\t\t\tfillBottomInts += _frameIntsPerLine;\n\t\t}\n\t}\n\tif (_shadow.valid()) {\n\t\tpaintShadow(outerLeft, outerTop, outerRight, outerBottom);\n\t}\n\n\t// Debug\n\t//auto frameInts = _frameInts;\n\t//auto pattern = anim::shifted((static_cast<uint32>(0xFF) << 24) | (static_cast<uint32>(0xFF) << 16) | (static_cast<uint32>(0xFF) << 8) | static_cast<uint32>(0xFF));\n\t//for (auto y = 0; y != _finalHeight; ++y) {\n\t//\tfor (auto x = 0; x != _finalWidth; ++x) {\n\t//\t\tauto source = *frameInts;\n\t//\t\tauto sourceAlpha = (source >> 24);\n\t//\t\t*frameInts = anim::unshifted(anim::shifted(source) * 256 + pattern * (256 - sourceAlpha));\n\t//\t\t++frameInts;\n\t//\t}\n\t//\tframeInts += _frameIntsPerLineAdded;\n\t//}\n\n\tp.drawImage(\n\t\touterLeft / style::DevicePixelRatio(),\n\t\touterTop / style::DevicePixelRatio(),\n\t\t_frame,\n\t\touterLeft,\n\t\touterTop,\n\t\touterRight - outerLeft,\n\t\touterBottom - outerTop);\n}\n\nTabbedSelector::Tab::Tab(\n\tSelectorTab type,\n\tint index,\n\tobject_ptr<Inner> widget)\n: _type(type)\n, _index(index)\n, _widget(std::move(widget))\n, _weak(_widget)\n, _footer(_widget ? _widget->createFooter() : nullptr) {\n\tif (_footer) {\n\t\t_footer->setParent(_widget->parentWidget());\n\t}\n}\n\nobject_ptr<TabbedSelector::Inner> TabbedSelector::Tab::takeWidget() {\n\treturn std::move(_widget);\n}\n\nvoid TabbedSelector::Tab::returnWidget(object_ptr<Inner> widget) {\n\tExpects(widget == _weak);\n\n\t_widget = std::move(widget);\n}\n\nvoid TabbedSelector::Tab::saveScrollTop() {\n\tExpects(widget() != nullptr);\n\n\t_scrollTop = widget()->getVisibleTop();\n}\n\n[[nodiscard]] rpl::producer<std::vector<Ui::EmojiGroup>> GreetingGroupFirst(\n\t\tnot_null<Data::Session*> owner) {\n\treturn owner->emojiStatuses().stickerGroupsValue(\n\t) | rpl::map([](std::vector<Ui::EmojiGroup> &&groups) {\n\t\tconst auto i = ranges::find(\n\t\t\tgroups,\n\t\t\tUi::EmojiGroupType::Greeting,\n\t\t\t&Ui::EmojiGroup::type);\n\t\tif (i != begin(groups) && i != end(groups)) {\n\t\t\tranges::rotate(begin(groups), i, i + 1);\n\t\t}\n\t\treturn std::move(groups);\n\t});\n}\n\nstd::unique_ptr<Ui::TabbedSearch> MakeSearch(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::EmojiPan &st,\n\t\tFn<void(std::vector<QString>&&)> callback,\n\t\tnot_null<Main::Session*> session,\n\t\tTabbedSearchType type) {\n\tusing Descriptor = Ui::SearchDescriptor;\n\tconst auto owner = &session->data();\n\tauto result = std::make_unique<Ui::TabbedSearch>(parent, st, Descriptor{\n\t\t.st = st.search,\n\t\t.groups = ((type == TabbedSearchType::ProfilePhoto)\n\t\t\t? owner->emojiStatuses().profilePhotoGroupsValue()\n\t\t\t: (type == TabbedSearchType::Status)\n\t\t\t? owner->emojiStatuses().statusGroupsValue()\n\t\t\t: (type == TabbedSearchType::Stickers)\n\t\t\t? owner->emojiStatuses().stickerGroupsValue()\n\t\t\t: (type == TabbedSearchType::Greeting)\n\t\t\t? GreetingGroupFirst(owner)\n\t\t\t: owner->emojiStatuses().emojiGroupsValue()),\n\t\t.customEmojiFactory = owner->customEmojiManager().factory(\n\t\t\tData::CustomEmojiManager::SizeTag::SetIcon,\n\t\t\tUi::SearchWithGroups::IconSizeOverride())\n\t});\n\n\tresult->queryValue(\n\t) | rpl::skip(1) | rpl::on_next(\n\t\tstd::move(callback),\n\t\tparent->lifetime());\n\n\treturn result;\n}\n\nTabbedSelector::TabbedSelector(\n\tQWidget *parent,\n\tstd::shared_ptr<Show> show,\n\tPauseReason level,\n\tMode mode)\n: TabbedSelector(parent, {\n\t.show = std::move(show),\n\t.st = ((mode == Mode::EmojiStatus\n\t\t|| mode == Mode::ChannelStatus\n\t\t|| mode == Mode::BackgroundEmoji\n\t\t|| mode == Mode::FullReactions)\n\t\t? st::statusEmojiPan\n\t\t: (mode == Mode::RecentReactions)\n\t\t? st::backgroundEmojiPan\n\t\t: st::defaultEmojiPan),\n\t.level = level,\n\t.mode = mode,\n}) {\n}\n\nTabbedSelector::TabbedSelector(\n\tQWidget *parent,\n\tTabbedSelectorDescriptor &&descriptor)\n: RpWidget(parent)\n, _st(descriptor.st)\n, _features(descriptor.features)\n, _show(std::move(descriptor.show))\n, _level(descriptor.level)\n, _customTextColor(std::move(descriptor.customTextColor))\n, _mode(descriptor.mode)\n, _panelRounding(Ui::PrepareCornerPixmaps(st::emojiPanRadius, _st.bg))\n, _categoriesRounding(\n\tUi::PrepareCornerPixmaps(st::emojiPanRadius, _st.categoriesBg))\n, _topShadow(full() ? object_ptr<Ui::PlainShadow>(this) : nullptr)\n, _bottomShadow(this)\n, _scroll(this, st::emojiScroll)\n, _tabs([&] {\n\tstd::vector<Tab> tabs;\n\tif (full()) {\n\t\ttabs.reserve(3);\n\t\ttabs.push_back(createTab(SelectorTab::Emoji, 0));\n\t\ttabs.push_back(createTab(SelectorTab::Stickers, 1));\n\t\ttabs.push_back(createTab(SelectorTab::Gifs, 2));\n\t} else if (mediaEditor()) {\n\t\ttabs.reserve(2);\n\t\ttabs.push_back(createTab(SelectorTab::Stickers, 0));\n\t\ttabs.push_back(createTab(SelectorTab::Masks, 1));\n\t} else if (_mode == Mode::StickersOnly || _mode == Mode::ChatIntro) {\n\t\ttabs.reserve(1);\n\t\ttabs.push_back(createTab(SelectorTab::Stickers, 0));\n\t} else {\n\t\ttabs.reserve(1);\n\t\ttabs.push_back(createTab(SelectorTab::Emoji, 0));\n\t}\n\treturn tabs;\n}())\n, _currentTabType(full()\n\t? session().settings().selectorTab()\n\t: (mediaEditor()\n\t\t|| _mode == Mode::StickersOnly\n\t\t|| _mode == Mode::ChatIntro)\n\t? SelectorTab::Stickers\n\t: SelectorTab::Emoji)\n, _hasEmojiTab(ranges::contains(_tabs, SelectorTab::Emoji, &Tab::type))\n, _hasStickersTab(ranges::contains(_tabs, SelectorTab::Stickers, &Tab::type))\n, _hasGifsTab(ranges::contains(_tabs, SelectorTab::Gifs, &Tab::type))\n, _hasMasksTab(ranges::contains(_tabs, SelectorTab::Masks, &Tab::type))\n, _tabbed(_tabs.size() > 1) {\n\tresize(st::emojiPanWidth, st::emojiPanMaxHeight);\n\n\tfor (auto &tab : _tabs) {\n\t\tif (tab.hasFooter()) {\n\t\t\ttab.footer()->hide();\n\t\t} else {\n\t\t\t_noFooter = true;\n\t\t}\n\t\ttab.widget()->hide();\n\t}\n\tif (tabbed()) {\n\t\tcreateTabsSlider();\n\t}\n\tsetWidgetToScrollArea();\n\n\tfor (auto &tab : _tabs) {\n\t\tconst auto widget = tab.widget();\n\n\t\twidget->scrollToRequests(\n\t\t) | rpl::on_next([=, tab = &tab](int y) {\n\t\t\tif (tab == currentTab()) {\n\t\t\t\tscrollToY(y);\n\t\t\t} else {\n\t\t\t\ttab->saveScrollTop(y);\n\t\t\t}\n\t\t}, widget->lifetime());\n\n\t\twidget->disableScrollRequests(\n\t\t) | rpl::on_next([=, tab = &tab](bool disabled) {\n\t\t\tif (tab == currentTab()) {\n\t\t\t\t_scroll->disableScroll(disabled);\n\t\t\t}\n\t\t}, widget->lifetime());\n\t}\n\n\trpl::merge(\n\t\t(hasStickersTab()\n\t\t\t? stickers()->scrollUpdated() | rpl::map_to(0)\n\t\t\t: rpl::never<int>() | rpl::type_erased),\n\t\t_scroll->scrollTopChanges()\n\t) | rpl::on_next([=] {\n\t\thandleScroll();\n\t}, lifetime());\n\n\tif (_topShadow) {\n\t\t_topShadow->raise();\n\t}\n\t_bottomShadow->raise();\n\tif (_tabsSlider) {\n\t\t_tabsSlider->raise();\n\t}\n\n\tif (hasStickersTab()\n\t\t|| hasGifsTab()\n\t\t|| (hasEmojiTab() && _mode == Mode::Full)) {\n\t\tsession().changes().peerUpdates(\n\t\t\tData::PeerUpdate::Flag::Rights\n\t\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\t\treturn (update.peer.get() == _currentPeer);\n\t\t}) | rpl::on_next([=] {\n\t\t\tcheckRestrictedPeer();\n\t\t}, lifetime());\n\t}\n\n\tif (hasStickersTab()) {\n\t\tsession().data().stickers().stickerSetInstalled(\n\t\t) | rpl::on_next([=](uint64 setId) {\n\t\t\t_tabsSlider->setActiveSection(indexByType(SelectorTab::Stickers));\n\t\t\tstickers()->showStickerSet(setId);\n\t\t\tif (_currentPeer\n\t\t\t\t&& Data::CanSend(\n\t\t\t\t\t_currentPeer,\n\t\t\t\t\tChatRestriction::SendStickers)) {\n\t\t\t\t_showRequests.fire({});\n\t\t\t}\n\t\t}, lifetime());\n\n\t\trpl::merge(\n\t\t\tsession().premiumPossibleValue() | rpl::to_empty,\n\t\t\tsession().data().stickers().updated(hasMasksTab()\n\t\t\t\t? Data::StickersType::Masks\n\t\t\t\t: Data::StickersType::Stickers)\n\t\t) | rpl::on_next([=] {\n\t\t\trefreshStickers();\n\t\t}, lifetime());\n\t}\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_panelRounding = Ui::PrepareCornerPixmaps(\n\t\t\tst::emojiPanRadius,\n\t\t\t_st.bg);\n\t\t_categoriesRounding = Ui::PrepareCornerPixmaps(\n\t\t\tst::emojiPanRadius,\n\t\t\t_st.categoriesBg);\n\t}, lifetime());\n\n\tif (hasEmojiTab() && _mode == Mode::Full) {\n\t\tsession().data().stickers().emojiSetInstalled(\n\t\t) | rpl::on_next([=](uint64 setId) {\n\t\t\t_tabsSlider->setActiveSection(indexByType(SelectorTab::Emoji));\n\t\t\temoji()->showSet(setId);\n\t\t\tif (_currentPeer && Data::CanSendTexts(_currentPeer)) {\n\t\t\t\t_showRequests.fire({});\n\t\t\t}\n\t\t}, lifetime());\n\t}\n\tif (hasEmojiTab()) {\n\t\temoji()->refreshEmoji();\n\t}\n\tsetAttribute(Qt::WA_OpaquePaintEvent, false);\n\tshowAll();\n\thide();\n}\n\nTabbedSelector::~TabbedSelector() = default;\n\nvoid TabbedSelector::reinstallSwipe(not_null<Ui::RpWidget*> widget) {\n\t_swipeLifetime.destroy();\n\n\tauto update = [=](Ui::Controls::SwipeContextData data) {\n\t\tif (data.translation != 0) {\n\t\t\tif (!_swipeBackData.callback) {\n\t\t\t\t_swipeBackData = Ui::Controls::SetupSwipeBack(\n\t\t\t\t\tthis,\n\t\t\t\t\t[=]() -> std::pair<QColor, QColor> {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tst::historyForwardChooseBg->c,\n\t\t\t\t\t\t\tst::historyForwardChooseFg->c,\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t\tdata.translation < 0);\n\t\t\t}\n\t\t\t_swipeBackData.callback(data);\n\t\t\treturn;\n\t\t} else if (_swipeBackData.lifetime) {\n\t\t\t_swipeBackData = {};\n\t\t}\n\t};\n\n\tauto init = [=](int, Qt::LayoutDirection direction) {\n\t\tif (!_tabsSlider) {\n\t\t\treturn Ui::Controls::SwipeHandlerFinishData();\n\t\t}\n\t\tconst auto activeSection = _tabsSlider->activeSection();\n\t\tconst auto isToLeft = direction == Qt::RightToLeft;\n\t\tif ((isToLeft && activeSection > 0)\n\t\t\t|| (!isToLeft && activeSection < _tabs.size() - 1)) {\n\t\t\treturn Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {\n\t\t\t\tif (_tabsSlider\n\t\t\t\t\t&& _tabsSlider->activeSection() == activeSection) {\n\t\t\t\t\t_swipeBackData = {};\n\t\t\t\t\t_tabsSlider->setActiveSection(isToLeft\n\t\t\t\t\t\t? activeSection - 1\n\t\t\t\t\t\t: activeSection + 1);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\treturn Ui::Controls::SwipeHandlerFinishData();\n\t};\n\n\tUi::Controls::SetupSwipeHandler({\n\t\t.widget = widget,\n\t\t.scroll = _scroll.data(),\n\t\t.update = std::move(update),\n\t\t.init = std::move(init),\n\t\t.dontStart = nullptr,\n\t\t.onLifetime = &_swipeLifetime,\n\t});\n}\n\nconst style::EmojiPan &TabbedSelector::st() const {\n\treturn _st;\n}\n\nMain::Session &TabbedSelector::session() const {\n\treturn _show->session();\n}\n\nPauseReason TabbedSelector::level() const {\n\treturn _level;\n}\n\nTabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) {\n\tauto createWidget = [&]() -> object_ptr<Inner> {\n\t\tconst auto paused = [show = _show, level = _level] {\n\t\t\treturn show->paused(level);\n\t\t};\n\t\tswitch (type) {\n\t\tcase SelectorTab::Emoji: {\n\t\t\tusing EmojiMode = EmojiListWidget::Mode;\n\t\t\tusing Descriptor = EmojiListDescriptor;\n\t\t\treturn object_ptr<EmojiListWidget>(this, Descriptor{\n\t\t\t\t.show = _show,\n\t\t\t\t.mode = (_mode == Mode::EmojiStatus\n\t\t\t\t\t? EmojiMode::EmojiStatus\n\t\t\t\t\t: _mode == Mode::ChannelStatus\n\t\t\t\t\t? EmojiMode::ChannelStatus\n\t\t\t\t\t: _mode == Mode::BackgroundEmoji\n\t\t\t\t\t? EmojiMode::BackgroundEmoji\n\t\t\t\t\t: _mode == Mode::FullReactions\n\t\t\t\t\t? EmojiMode::FullReactions\n\t\t\t\t\t: _mode == Mode::RecentReactions\n\t\t\t\t\t? EmojiMode::RecentReactions\n\t\t\t\t\t: _mode == Mode::PeerTitle\n\t\t\t\t\t? EmojiMode::PeerTitle\n\t\t\t\t\t: EmojiMode::Full),\n\t\t\t\t.customTextColor = _customTextColor,\n\t\t\t\t.paused = paused,\n\t\t\t\t.st = &_st,\n\t\t\t\t.features = _features,\n\t\t\t});\n\t\t}\n\t\tcase SelectorTab::Stickers: {\n\t\t\tusing StickersMode = StickersListWidget::Mode;\n\t\t\tusing Descriptor = StickersListDescriptor;\n\t\t\treturn object_ptr<StickersListWidget>(this, Descriptor{\n\t\t\t\t.show = _show,\n\t\t\t\t.mode = (_mode == Mode::ChatIntro\n\t\t\t\t\t? StickersMode::ChatIntro\n\t\t\t\t\t: StickersMode::Full),\n\t\t\t\t.paused = paused,\n\t\t\t\t.st = &_st,\n\t\t\t\t.features = _features,\n\t\t\t});\n\t\t}\n\t\tcase SelectorTab::Gifs: {\n\t\t\tusing Descriptor = GifsListDescriptor;\n\t\t\treturn object_ptr<GifsListWidget>(this, Descriptor{\n\t\t\t\t.show = _show,\n\t\t\t\t.paused = paused,\n\t\t\t\t.st = &_st,\n\t\t\t});\n\t\t}\n\t\tcase SelectorTab::Masks: {\n\t\t\tusing StickersMode = StickersListWidget::Mode;\n\t\t\tusing Descriptor = StickersListDescriptor;\n\t\t\treturn object_ptr<StickersListWidget>(this, Descriptor{\n\t\t\t\t.show = _show,\n\t\t\t\t.mode = StickersMode::Masks,\n\t\t\t\t.paused = paused,\n\t\t\t\t.st = &_st,\n\t\t\t\t.features = _features,\n\t\t\t});\n\t\t}\n\t\t}\n\t\tUnexpected(\"Type in TabbedSelector::createTab.\");\n\t};\n\treturn Tab{ type, index, createWidget() };\n}\n\nbool TabbedSelector::full() const {\n\treturn (_mode == Mode::Full);\n}\n\nbool TabbedSelector::mediaEditor() const {\n\treturn (_mode == Mode::MediaEditor);\n}\n\nbool TabbedSelector::tabbed() const {\n\treturn _tabbed;\n}\n\nbool TabbedSelector::hasEmojiTab() const {\n\treturn _hasEmojiTab;\n}\n\nbool TabbedSelector::hasStickersTab() const {\n\treturn _hasStickersTab;\n}\n\nbool TabbedSelector::hasGifsTab() const {\n\treturn _hasGifsTab;\n}\n\nbool TabbedSelector::hasMasksTab() const {\n\treturn _hasMasksTab;\n}\n\nrpl::producer<EmojiChosen> TabbedSelector::emojiChosen() const {\n\treturn emoji()->chosen();\n}\n\nrpl::producer<FileChosen> TabbedSelector::customEmojiChosen() const {\n\treturn emoji()->customChosen();\n}\n\nrpl::producer<FileChosen> TabbedSelector::fileChosen() const {\n\tauto never = rpl::never<FileChosen>(\n\t) | rpl::type_erased;\n\treturn rpl::merge(\n\t\thasStickersTab() ? stickers()->chosen() : never,\n\t\thasGifsTab() ? gifs()->fileChosen() : never,\n\t\thasMasksTab() ? masks()->chosen() : never);\n}\n\nrpl::producer<PhotoChosen> TabbedSelector::photoChosen() const {\n\treturn hasGifsTab() ? gifs()->photoChosen() : nullptr;\n}\n\nauto TabbedSelector::inlineResultChosen() const\n-> rpl::producer<InlineChosen> {\n\treturn hasGifsTab() ? gifs()->inlineResultChosen() : nullptr;\n}\n\nauto TabbedSelector::choosingStickerUpdated() const\n-> rpl::producer<TabbedSelector::Action>{\n\treturn hasStickersTab()\n\t\t? stickers()->choosingUpdated()\n\t\t: rpl::never<Action>();\n}\n\nrpl::producer<> TabbedSelector::cancelled() const {\n\treturn hasGifsTab() ? gifs()->cancelRequests() : nullptr;\n}\n\nrpl::producer<> TabbedSelector::checkForHide() const {\n\tauto never = rpl::never<>();\n\treturn rpl::merge(\n\t\thasStickersTab() ? stickers()->checkForHide() : never,\n\t\thasMasksTab() ? masks()->checkForHide() : never,\n\t\thasEmojiTab() ? emoji()->checkForHide() : never);\n}\n\nrpl::producer<> TabbedSelector::slideFinished() const {\n\treturn _slideFinished.events();\n}\n\nvoid TabbedSelector::updateTabsSliderGeometry() {\n\tif (!_tabsSlider) {\n\t\treturn;\n\t}\n\tconst auto w = (mediaEditor() && hasMasksTab() && masks()->mySetsEmpty())\n\t\t? width() / 2\n\t\t: width();\n\t_tabsSlider->resizeToWidth(w);\n\t_tabsSlider->moveToLeft(0, 0);\n}\n\nvoid TabbedSelector::resizeEvent(QResizeEvent *e) {\n\tupdateTabsSliderGeometry();\n\tif (_topShadow && _tabsSlider) {\n\t\t_topShadow->setGeometry(\n\t\t\t_tabsSlider->x(),\n\t\t\t_tabsSlider->bottomNoMargins() - st::lineWidth,\n\t\t\t_tabsSlider->width(),\n\t\t\tst::lineWidth);\n\t}\n\tupdateScrollGeometry(e->oldSize());\n\tupdateRestrictedLabelGeometry();\n\tupdateFooterGeometry();\n\tupdate();\n}\n\nvoid TabbedSelector::updateScrollGeometry(QSize oldSize) {\n\tauto scrollWidth = width() - st::emojiPanRadius;\n\tauto scrollHeight = height() - scrollTop() - scrollBottom();\n\tauto inner = currentTab()->widget();\n\tauto innerWidth = scrollWidth - st::emojiScroll.width;\n\tauto setScrollGeometry = [&] {\n\t\t_scroll->setGeometryToLeft(\n\t\t\tst::emojiPanRadius,\n\t\t\tscrollTop(),\n\t\t\tscrollWidth,\n\t\t\tscrollHeight);\n\t};\n\tauto setInnerGeometry = [&] {\n\t\tauto scrollTop = _scroll->scrollTop();\n\t\tauto scrollBottom = scrollTop + scrollHeight;\n\t\tinner->setMinimalHeight(innerWidth, scrollHeight);\n\t\tinner->setVisibleTopBottom(scrollTop, scrollBottom);\n\t};\n\tif (oldSize.height() > height()) {\n\t\tsetScrollGeometry();\n\t\tsetInnerGeometry();\n\t} else {\n\t\tsetInnerGeometry();\n\t\tsetScrollGeometry();\n\t}\n\t_bottomShadow->setGeometry(\n\t\t0,\n\t\t_scroll->y() + (_dropDown ? 0 : (_scroll->height() - st::lineWidth)),\n\t\twidth(),\n\t\tst::lineWidth);\n}\n\nvoid TabbedSelector::updateFooterGeometry() {\n\t_footerTop = _dropDown\n\t\t? 0\n\t\t: _noFooter\n\t\t? (height() - _roundRadius)\n\t\t: (height() - _st.footer);\n\tfor (auto &tab : _tabs) {\n\t\tif (tab.hasFooter()) {\n\t\t\ttab.footer()->resizeToWidth(width());\n\t\t\ttab.footer()->moveToLeft(0, _footerTop);\n\t\t}\n\t}\n}\n\nvoid TabbedSelector::updateRestrictedLabelGeometry() {\n\tif (!_restrictedLabel) {\n\t\treturn;\n\t}\n\n\tauto labelWidth = width() - st::stickerPanPadding * 2;\n\t_restrictedLabel->resizeToWidth(labelWidth);\n\t_restrictedLabel->moveToLeft(\n\t\t(width() - _restrictedLabel->width()) / 2,\n\t\t(height() / 3 - _restrictedLabel->height() / 2));\n}\n\nvoid TabbedSelector::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tauto switching = (_slideAnimation != nullptr);\n\tif (switching) {\n\t\tpaintSlideFrame(p);\n\t\tif (!_a_slide.animating()) {\n\t\t\t_slideAnimation.reset();\n\t\t\tafterShown();\n\t\t\t_slideFinished.fire({});\n\t\t}\n\t} else {\n\t\tpaintContent(p);\n\t}\n}\n\nvoid TabbedSelector::paintSlideFrame(QPainter &p) {\n\tif (_roundRadius > 0) {\n\t\tpaintBgRoundedPart(p);\n\t} else if (_tabsSlider) {\n\t\tp.fillRect(0, 0, width(), _tabsSlider->height(), _st.bg);\n\t}\n\tauto slideDt = _a_slide.value(1.);\n\t_slideAnimation->paintFrame(p, _st, slideDt, 1.);\n}\n\nvoid TabbedSelector::paintBgRoundedPart(QPainter &p) {\n\tconst auto fill = _dropDown\n\t\t? QRect(0, height() - _roundRadius, width(), _roundRadius)\n\t\t: _tabsSlider\n\t\t? QRect(0, 0, width(), _tabsSlider->height())\n\t\t: QRect(0, 0, width(), _roundRadius);\n\tUi::FillRoundRect(p, fill, _st.bg, {\n\t\t.p = {\n\t\t\t_dropDown ? QPixmap() : _panelRounding.p[0],\n\t\t\t_dropDown ? QPixmap() : _panelRounding.p[1],\n\t\t\t_dropDown ? _panelRounding.p[2] : QPixmap(),\n\t\t\t_dropDown ? _panelRounding.p[3] : QPixmap(),\n\t\t},\n\t});\n}\n\nvoid TabbedSelector::paintContent(QPainter &p) {\n\tconst auto &footerBg = hasSectionIcons() ? _st.categoriesBg : _st.bg;\n\tif (_roundRadius > 0) {\n\t\tpaintBgRoundedPart(p);\n\n\t\tconst auto &pixmaps = hasSectionIcons()\n\t\t\t? _categoriesRounding\n\t\t\t: _panelRounding;\n\t\tconst auto footerPart = QRect(\n\t\t\t0,\n\t\t\t_footerTop,\n\t\t\twidth(),\n\t\t\t_noFooter ? _roundRadius : _st.footer);\n\t\tUi::FillRoundRect(p, footerPart, footerBg, {\n\t\t\t.p = {\n\t\t\t\t_dropDown ? pixmaps.p[0] : QPixmap(),\n\t\t\t\t_dropDown ? pixmaps.p[1] : QPixmap(),\n\t\t\t\t_dropDown ? QPixmap() : pixmaps.p[2],\n\t\t\t\t_dropDown ? QPixmap() : pixmaps.p[3],\n\t\t\t},\n\t\t});\n\t} else {\n\t\tif (_tabsSlider) {\n\t\t\tp.fillRect(0, 0, width(), _tabsSlider->height(), _st.bg);\n\t\t}\n\t\tp.fillRect(0, _footerTop, width(), _st.footer, footerBg);\n\t}\n\n\tauto sidesTop = marginTop();\n\tauto sidesHeight = height() - sidesTop - marginBottom();\n\tif (_restrictedLabel) {\n\t\tp.fillRect(0, sidesTop, width(), sidesHeight, st::emojiPanBg);\n\t} else {\n\t\tp.fillRect(\n\t\t\tmyrtlrect(\n\t\t\t\twidth() - st::emojiScroll.width,\n\t\t\t\tsidesTop,\n\t\t\t\tst::emojiScroll.width,\n\t\t\t\tsidesHeight),\n\t\t\t_st.bg);\n\t\tp.fillRect(\n\t\t\tmyrtlrect(0, sidesTop, st::emojiPanRadius, sidesHeight),\n\t\t\t_st.bg);\n\t}\n}\n\nint TabbedSelector::marginTop() const {\n\treturn (_dropDown && !_noFooter)\n\t\t? _st.footer\n\t\t: _tabsSlider\n\t\t? (_tabsSlider->height() - st::lineWidth)\n\t\t: _roundRadius;\n}\n\nint TabbedSelector::scrollTop() const {\n\treturn tabbed()\n\t\t? marginTop()\n\t\t: (_dropDown && !_noFooter)\n\t\t? _st.footer\n\t\t: 0;\n}\n\nint TabbedSelector::marginBottom() const {\n\treturn (_dropDown || _noFooter) ? _roundRadius : _st.footer;\n}\n\nint TabbedSelector::scrollBottom() const {\n\treturn (_dropDown || _noFooter) ? 0 : marginBottom();\n}\n\nvoid TabbedSelector::refreshStickers() {\n\tif (hasStickersTab()) {\n\t\tstickers()->refreshStickers();\n\t\tif (isHidden() || _currentTabType != SelectorTab::Stickers) {\n\t\t\tstickers()->preloadImages();\n\t\t}\n\t}\n\tif (hasMasksTab()) {\n\t\tconst auto masksList = masks();\n\t\tmasksList->refreshStickers();\n\t\tif (isHidden() || _currentTabType != SelectorTab::Masks) {\n\t\t\tmasksList->preloadImages();\n\t\t}\n\n\t\tfillTabsSliderSections();\n\t\tupdateTabsSliderGeometry();\n\t\tif (hasStickersTab() && masksList->mySetsEmpty()) {\n\t\t\t_tabsSlider->setActiveSection(indexByType(SelectorTab::Stickers));\n\t\t}\n\t}\n}\n\nbool TabbedSelector::preventAutoHide() const {\n\treturn (hasStickersTab() && stickers()->preventAutoHide())\n\t\t|| (hasMasksTab() && masks()->preventAutoHide())\n\t\t|| (hasEmojiTab() && emoji()->preventAutoHide())\n\t\t|| hasMenu();\n}\n\nbool TabbedSelector::hasMenu() const {\n\treturn (_menu && !_menu->empty());\n}\n\nQImage TabbedSelector::grabForAnimation() {\n\tauto slideAnimationData = base::take(_slideAnimation);\n\tauto slideAnimation = base::take(_a_slide);\n\n\tshowAll();\n\tif (_topShadow) {\n\t\t_topShadow->hide();\n\t}\n\tif (_tabsSlider) {\n\t\t_tabsSlider->hide();\n\t}\n\tUi::SendPendingMoveResizeEvents(this);\n\n\tauto result = QImage(\n\t\tsize() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult.fill(Qt::transparent);\n\trender(&result);\n\n\t_a_slide = base::take(slideAnimation);\n\t_slideAnimation = base::take(slideAnimationData);\n\n\treturn result;\n}\n\nbool TabbedSelector::floatPlayerHandleWheelEvent(QEvent *e) {\n\treturn _scroll->viewportEvent(e);\n}\n\nQRect TabbedSelector::floatPlayerAvailableRect() const {\n\treturn mapToGlobal(_scroll->geometry());\n}\n\nvoid TabbedSelector::hideFinished() {\n\tfor (auto &tab : _tabs) {\n\t\ttab.widget()->panelHideFinished();\n\t}\n\t_a_slide.stop();\n\t_slideAnimation.reset();\n}\n\nvoid TabbedSelector::showStarted() {\n\tif (hasStickersTab()) {\n\t\tsession().api().updateStickers();\n\t}\n\tif (hasMasksTab()) {\n\t\tsession().api().updateMasks();\n\t}\n\tif (hasEmojiTab()) {\n\t\tsession().api().updateCustomEmoji();\n\t}\n\tif (hasGifsTab()) {\n\t\tsession().api().updateSavedGifs();\n\t}\n\tcurrentTab()->widget()->refreshRecent();\n\tcurrentTab()->widget()->preloadImages();\n\t_a_slide.stop();\n\t_slideAnimation.reset();\n\tshowAll();\n}\n\nvoid TabbedSelector::beforeHiding() {\n\tif (!_scroll->isHidden()) {\n\t\tcurrentTab()->widget()->beforeHiding();\n\t\tif (_beforeHidingCallback) {\n\t\t\t_beforeHidingCallback(_currentTabType);\n\t\t}\n\t}\n\tif (Ui::InFocusChain(this)) {\n\t\twindow()->setFocus();\n\t}\n}\n\nvoid TabbedSelector::afterShown() {\n\tif (!_a_slide.animating()) {\n\t\tshowAll();\n\t\tcurrentTab()->widget()->afterShown();\n\t\tif (_afterShownCallback) {\n\t\t\t_afterShownCallback(_currentTabType);\n\t\t}\n\t}\n}\n\nvoid TabbedSelector::setCurrentPeer(PeerData *peer) {\n\tif (hasGifsTab()) {\n\t\tgifs()->setInlineQueryPeer(peer);\n\t}\n\t_currentPeer = peer;\n\tcheckRestrictedPeer();\n\tif (hasEmojiTab()) {\n\t\temoji()->showMegagroupSet(peer ? peer->asMegagroup() : nullptr);\n\t}\n\tif (hasStickersTab()) {\n\t\tstickers()->showMegagroupSet(peer ? peer->asMegagroup() : nullptr);\n\t}\n\tsetAllowEmojiWithoutPremium(\n\t\tpeer && Data::AllowEmojiWithoutPremium(peer));\n}\n\nvoid TabbedSelector::provideRecentEmoji(\n\t\tconst std::vector<EmojiStatusId> &customRecentList) {\n\tfor (const auto &tab : _tabs) {\n\t\tif (tab.type() == SelectorTab::Emoji) {\n\t\t\tconst auto emoji = static_cast<EmojiListWidget*>(tab.widget());\n\t\t\temoji->provideRecent(customRecentList);\n\t\t}\n\t}\n}\n\nvoid TabbedSelector::checkRestrictedPeer() {\n\tif (_currentPeer) {\n\t\tconst auto error = (_currentTabType == SelectorTab::Stickers)\n\t\t\t? Data::RestrictionError(\n\t\t\t\t_currentPeer,\n\t\t\t\tChatRestriction::SendStickers)\n\t\t\t: (_currentTabType == SelectorTab::Gifs)\n\t\t\t? Data::RestrictionError(\n\t\t\t\t_currentPeer,\n\t\t\t\tChatRestriction::SendGifs)\n\t\t\t: (_currentTabType == SelectorTab::Emoji && _mode == Mode::Full)\n\t\t\t? ((true || Data::RestrictionError(\n\t\t\t\t_currentPeer, // We don't allow input if texts are forbidden.\n\t\t\t\tChatRestriction::SendInline))\n\t\t\t\t? Data::RestrictionError(\n\t\t\t\t\t_currentPeer,\n\t\t\t\t\tChatRestriction::SendOther)\n\t\t\t\t: Data::SendError())\n\t\t\t: Data::SendError();\n\t\tconst auto changed = (_restrictedLabelKey != error.text);\n\t\tif (!changed) {\n\t\t\treturn;\n\t\t}\n\t\t_restrictedLabelKey = error.text;\n\t\tif (error) {\n\t\t\tconst auto show = _show;\n\t\t\tconst auto peer = _currentPeer;\n\t\t\t_restrictedLabel.create(\n\t\t\t\tthis,\n\t\t\t\trpl::single(error.boostsToLift\n\t\t\t\t\t? tr::link(error.text)\n\t\t\t\t\t: TextWithEntities{ error.text }),\n\t\t\t\tst::stickersRestrictedLabel);\n\t\t\tconst auto lifting = error.boostsToLift;\n\t\t\t_restrictedLabel->setClickHandlerFilter([=](auto...) {\n\t\t\t\tconst auto window = show->resolveWindow();\n\t\t\t\twindow->resolveBoostState(peer->asChannel(), lifting);\n\t\t\t\treturn false;\n\t\t\t});\n\t\t\t_restrictedLabel->show();\n\t\t\tupdateRestrictedLabelGeometry();\n\t\t\tcurrentTab()->footer()->hide();\n\t\t\t_scroll->hide();\n\t\t\t_bottomShadow->hide();\n\t\t\tupdate();\n\t\t\treturn;\n\t\t}\n\t} else {\n\t\t_restrictedLabelKey = QString();\n\t}\n\tif (_restrictedLabel) {\n\t\t_restrictedLabel.destroy();\n\t\tif (!_a_slide.animating()) {\n\t\t\tcurrentTab()->footer()->show();\n\t\t\t_scroll->show();\n\t\t\t_bottomShadow->setVisible(_mode == Mode::EmojiStatus);\n\t\t\tupdate();\n\t\t}\n\t}\n}\n\nbool TabbedSelector::isRestrictedView() {\n\tcheckRestrictedPeer();\n\treturn (_restrictedLabel != nullptr);\n}\n\nvoid TabbedSelector::showAll() {\n\tif (isRestrictedView()) {\n\t\t_restrictedLabel->show();\n\t} else {\n\t\tif (currentTab()->hasFooter()) {\n\t\t\tcurrentTab()->footer()->show();\n\t\t}\n\t\t_scroll->show();\n\t\t_bottomShadow->setVisible(_mode == Mode::EmojiStatus);\n\t}\n\tif (_topShadow) {\n\t\t_topShadow->show();\n\t}\n\tif (_tabsSlider) {\n\t\t_tabsSlider->show();\n\t}\n}\n\nvoid TabbedSelector::hideForSliding() {\n\thideChildren();\n\tif (_topShadow) {\n\t\t_topShadow->show();\n\t}\n\tif (_tabsSlider) {\n\t\t_tabsSlider->show();\n\t}\n\tcurrentTab()->widget()->clearSelection();\n}\n\nvoid TabbedSelector::handleScroll() {\n\tauto scrollTop = _scroll->scrollTop();\n\tauto scrollBottom = scrollTop + _scroll->height();\n\tcurrentTab()->widget()->setVisibleTopBottom(scrollTop, scrollBottom);\n}\n\nvoid TabbedSelector::setRoundRadius(int radius) {\n\t_roundRadius = radius;\n\tif (_tabsSlider) {\n\t\t_tabsSlider->setRippleTopRoundRadius(_roundRadius);\n\t}\n}\n\nvoid TabbedSelector::setAllowEmojiWithoutPremium(bool allow) {\n\tfor (const auto &tab : _tabs) {\n\t\tif (tab.type() == SelectorTab::Emoji) {\n\t\t\tconst auto emoji = static_cast<EmojiListWidget*>(tab.widget());\n\t\t\temoji->setAllowWithoutPremium(allow);\n\t\t}\n\t}\n}\n\nvoid TabbedSelector::createTabsSlider() {\n\t_tabsSlider.create(this, _st.tabs);\n\n\tfillTabsSliderSections();\n\n\t_tabsSlider->setActiveSectionFast(indexByType(_currentTabType));\n\t_tabsSlider->sectionActivated(\n\t) | rpl::on_next([=] {\n\t\tswitchTab();\n\t}, lifetime());\n}\n\nvoid TabbedSelector::fillTabsSliderSections() {\n\tif (!_tabsSlider) {\n\t\treturn;\n\t}\n\n\tconst auto sections = ranges::views::all(\n\t\t_tabs\n\t) | ranges::views::filter([&](const Tab &tab) {\n\t\treturn (tab.type() == SelectorTab::Masks)\n\t\t\t? !masks()->mySetsEmpty()\n\t\t\t: true;\n\t}) | ranges::views::transform([&](const Tab &tab) {\n\t\treturn [&] {\n\t\t\tswitch (tab.type()) {\n\t\t\tcase SelectorTab::Emoji:\n\t\t\t\treturn tr::lng_switch_emoji;\n\t\t\tcase SelectorTab::Stickers:\n\t\t\t\treturn tr::lng_switch_stickers;\n\t\t\tcase SelectorTab::Gifs:\n\t\t\t\treturn tr::lng_switch_gifs;\n\t\t\tcase SelectorTab::Masks:\n\t\t\t\treturn tr::lng_switch_masks;\n\t\t\t}\n\t\t\tUnexpected(\"SelectorTab value in fillTabsSliderSections.\");\n\t\t}()(tr::now);\n\t}) | ranges::to_vector;\n\t_tabsSlider->setSections(sections);\n}\n\nbool TabbedSelector::hasSectionIcons() const {\n\treturn !_restrictedLabel && !_noFooter;\n}\n\nvoid TabbedSelector::switchTab() {\n\tExpects(tabbed());\n\n\tconst auto tab = _tabsSlider->activeSection();\n\tAssert(tab >= 0 && tab < _tabs.size());\n\tconst auto newTabType = typeByIndex(tab);\n\tif (_currentTabType == newTabType) {\n\t\t_scroll->scrollToY(0);\n\t\treturn;\n\t}\n\n\tconst auto wasSectionIcons = hasSectionIcons();\n\tconst auto wasIndex = indexByType(_currentTabType);\n\tcurrentTab()->saveScrollTop();\n\n\tbeforeHiding();\n\n\tauto wasCache = grabForAnimation();\n\n\tauto widget = _scroll->takeWidget<Inner>();\n\twidget->setParent(this);\n\twidget->hide();\n\tif (currentTab()->hasFooter()) {\n\t\tcurrentTab()->footer()->hide();\n\t}\n\tcurrentTab()->returnWidget(std::move(widget));\n\n\t_currentTabType = newTabType;\n\t_restrictedLabel.destroy();\n\tcheckRestrictedPeer();\n\n\tcurrentTab()->widget()->refreshRecent();\n\tcurrentTab()->widget()->preloadImages();\n\tsetWidgetToScrollArea();\n\n\tauto nowCache = grabForAnimation();\n\n\tauto direction = (wasIndex > indexByType(_currentTabType))\n\t\t? SlideAnimation::Direction::LeftToRight\n\t\t: SlideAnimation::Direction::RightToLeft;\n\tif (direction == SlideAnimation::Direction::LeftToRight) {\n\t\tstd::swap(wasCache, nowCache);\n\t}\n\t_slideAnimation = std::make_unique<SlideAnimation>();\n\tconst auto slidingRect = QRect(\n\t\t0,\n\t\t_scroll->y() * style::DevicePixelRatio(),\n\t\twidth() * style::DevicePixelRatio(),\n\t\t(height() - _scroll->y()) * style::DevicePixelRatio());\n\t_slideAnimation->setFinalImages(\n\t\tdirection,\n\t\tstd::move(wasCache),\n\t\tstd::move(nowCache),\n\t\tslidingRect,\n\t\twasSectionIcons);\n\t_slideAnimation->setCornerMasks(\n\t\tImages::CornersMask(st::emojiPanRadius));\n\t_slideAnimation->start();\n\n\thideForSliding();\n\n\tgetTab(wasIndex)->widget()->hideFinished();\n\n\t_a_slide.start(\n\t\t[=] { update(); },\n\t\t0.,\n\t\t1.,\n\t\tst::emojiPanSlideDuration,\n\t\tanim::linear);\n\tupdate();\n\n\tif (full()) {\n\t\tsession().settings().setSelectorTab(_currentTabType);\n\t\tsession().saveSettingsDelayed();\n\t}\n}\n\nnot_null<EmojiListWidget*> TabbedSelector::emoji() const {\n\tExpects(hasEmojiTab());\n\n\treturn static_cast<EmojiListWidget*>(\n\t\tgetTab(indexByType(SelectorTab::Emoji))->widget());\n}\n\nnot_null<StickersListWidget*> TabbedSelector::stickers() const {\n\tExpects(hasStickersTab());\n\n\treturn static_cast<StickersListWidget*>(\n\t\tgetTab(indexByType(SelectorTab::Stickers))->widget());\n}\n\nnot_null<GifsListWidget*> TabbedSelector::gifs() const {\n\tExpects(hasGifsTab());\n\n\treturn static_cast<GifsListWidget*>(\n\t\tgetTab(indexByType(SelectorTab::Gifs))->widget());\n}\n\nnot_null<StickersListWidget*> TabbedSelector::masks() const {\n\tExpects(hasMasksTab());\n\n\treturn static_cast<StickersListWidget*>(\n\t\tgetTab(indexByType(SelectorTab::Masks))->widget());\n}\n\nvoid TabbedSelector::setWidgetToScrollArea() {\n\tauto inner = _scroll->setOwnedWidget(currentTab()->takeWidget());\n\tauto innerWidth = _scroll->width() - st::emojiScroll.width;\n\tauto scrollHeight = _scroll->height();\n\tinner->setMinimalHeight(innerWidth, scrollHeight);\n\tinner->moveToLeft(0, 0);\n\tinner->show();\n\n\tif (_tabs.size() > 1) {\n\t\treinstallSwipe(inner);\n\t}\n\n\t_scroll->disableScroll(false);\n\tscrollToY(currentTab()->getScrollTop());\n\thandleScroll();\n}\n\nvoid TabbedSelector::scrollToY(int y) {\n\t_scroll->scrollToY(y);\n\n\t// Qt render glitch workaround, shadow sometimes disappears if we just scroll to y.\n\tif (_topShadow) {\n\t\t_topShadow->update();\n\t}\n}\n\nvoid TabbedSelector::showMenuWithDetails(SendMenu::Details details) {\n\t_menu = currentTab()->widget()->fillContextMenu(details);\n\tif (_menu && !_menu->empty()) {\n\t\t_menu->popup(QCursor::pos());\n\t}\n}\n\nvoid TabbedSelector::setDropDown(bool dropDown) {\n\tif (_dropDown == dropDown) {\n\t\treturn;\n\t}\n\t_dropDown = dropDown;\n\tupdateFooterGeometry();\n\tupdateScrollGeometry(size());\n}\n\nrpl::producer<> TabbedSelector::contextMenuRequested() const {\n\treturn events(\n\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\treturn e->type() == QEvent::ContextMenu;\n\t}) | rpl::to_empty;\n}\n\nSelectorTab TabbedSelector::typeByIndex(int index) const {\n\tfor (const auto &tab : _tabs) {\n\t\tif (tab.index() == index) {\n\t\t\treturn tab.type();\n\t\t}\n\t}\n\tUnexpected(\"Type in TabbedSelector::typeByIndex.\");\n}\n\nint TabbedSelector::indexByType(SelectorTab type) const {\n\tfor (const auto &tab : _tabs) {\n\t\tif (tab.type() == type) {\n\t\t\treturn tab.index();\n\t\t}\n\t}\n\tUnexpected(\"Index in TabbedSelector::indexByType.\");\n}\n\nnot_null<TabbedSelector::Tab*> TabbedSelector::getTab(int index) {\n\treturn &(_tabs[index]);\n}\n\nnot_null<const TabbedSelector::Tab*> TabbedSelector::getTab(int index) const {\n\treturn &_tabs[index];\n}\n\nnot_null<TabbedSelector::Tab*> TabbedSelector::currentTab() {\n\treturn &_tabs[indexByType(_currentTabType)];\n}\n\nnot_null<const TabbedSelector::Tab*> TabbedSelector::currentTab() const {\n\treturn &_tabs[indexByType(_currentTabType)];\n}\n\nTabbedSelector::Inner::Inner(\n\tQWidget *parent,\n\tstd::shared_ptr<Show> show,\n\tPauseReason level)\n: Inner(\n\tparent,\n\tst::defaultEmojiPan,\n\tshow,\n\t[show, level] { return show->paused(level); }) {\n}\n\nTabbedSelector::Inner::Inner(\n\tQWidget *parent,\n\tconst style::EmojiPan &st,\n\tstd::shared_ptr<Show> show,\n\tFn<bool()> paused)\n: RpWidget(parent)\n, _st(st)\n, _show(std::move(show))\n, _session(&_show->session())\n, _paused(paused) {\n}\n\nrpl::producer<int> TabbedSelector::Inner::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nrpl::producer<bool> TabbedSelector::Inner::disableScrollRequests() const {\n\treturn _disableScrollRequests.events();\n}\n\nvoid TabbedSelector::Inner::scrollTo(int y) {\n\t_scrollToRequests.fire_copy(y);\n}\n\nvoid TabbedSelector::Inner::disableScroll(bool disabled) {\n\t_disableScrollRequests.fire_copy(disabled);\n}\n\nvoid TabbedSelector::Inner::checkHideWithBox(\n\t\tobject_ptr<Ui::BoxContent> box) {\n\tconst auto raw = base::make_weak(box.data());\n\t_show->showBox(std::move(box));\n\tif (!raw) {\n\t\treturn;\n\t}\n\t_preventHideWithBox = true;\n\tconnect(raw.get(), &QObject::destroyed, this, [=] {\n\t\t_preventHideWithBox = false;\n\t\t_checkForHide.fire({});\n\t});\n}\n\nvoid TabbedSelector::Inner::paintEmptySearchResults(\n\t\tPainter &p,\n\t\tconst style::icon &icon,\n\t\tconst QString &text,\n\t\tbool skipIcon) const {\n\tconst auto iconLeft = (width() - icon.width()) / 2;\n\tconst auto iconTop = std::max(\n\t\t(height() / 3) - (icon.height() / 2),\n\t\tst::normalFont->height);\n\tif (!skipIcon) {\n\t\ticon.paint(p, iconLeft, iconTop, width());\n\t}\n\n\tconst auto textWidth = st::normalFont->width(text);\n\tconst auto textTop = std::min(\n\t\ticonTop + icon.height() - st::normalFont->height,\n\t\theight() - 2 * st::normalFont->height);\n\tp.setFont(st::normalFont);\n\tp.setPen(_st.tabs.labelFg);\n\tp.drawTextLeft(\n\t\t(width() - textWidth) / 2,\n\t\ttextTop,\n\t\twidth(),\n\t\ttext,\n\t\ttextWidth);\n}\n\nvoid TabbedSelector::Inner::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\t_visibleTop = visibleTop;\n\t_visibleBottom = visibleBottom;\n}\n\nvoid TabbedSelector::Inner::setMinimalHeight(\n\t\tint newWidth,\n\t\tint newMinimalHeight) {\n\tif (_minimalHeight != newMinimalHeight) {\n\t\t_minimalHeight = newMinimalHeight;\n\t\tresizeToWidth(newWidth);\n\t} else if (newWidth != width()) {\n\t\tresizeToWidth(newWidth);\n\t}\n}\n\nint TabbedSelector::Inner::resizeGetHeight(int newWidth) {\n\tauto result = std::max(\n\t\tcountDesiredHeight(newWidth),\n\t\tminimalHeight());\n\tif (result != height()) {\n\t\tupdate();\n\t}\n\treturn result;\n}\n\nint TabbedSelector::Inner::minimalHeight() const {\n\treturn _minimalHeight.value_or(defaultMinimalHeight());\n}\n\nint TabbedSelector::Inner::defaultMinimalHeight() const {\n\treturn st::emojiPanMaxHeight - _st.footer;\n}\n\nvoid TabbedSelector::Inner::hideFinished() {\n\tprocessHideFinished();\n\tif (auto footer = getFooter()) {\n\t\tfooter->processHideFinished();\n\t}\n}\n\nvoid TabbedSelector::Inner::panelHideFinished() {\n\thideFinished();\n\tprocessPanelHideFinished();\n\tif (auto footer = getFooter()) {\n\t\tfooter->processPanelHideFinished();\n\t}\n}\n\nTabbedSelector::InnerFooter::InnerFooter(\n\tQWidget *parent,\n\tconst style::EmojiPan &st)\n: RpWidget(parent)\n, _st(st) {\n\tresize(st::emojiPanWidth, _st.footer);\n}\n\nconst style::EmojiPan &TabbedSelector::InnerFooter::st() const {\n\treturn _st;\n}\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/tabbed_selector.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_common.h\"\n#include \"chat_helpers/compose/compose_features.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/controls/swipe_handler_data.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/message_sending_animation_common.h\"\n#include \"ui/effects/panel_animation.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"mtproto/sender.h\"\n#include \"base/object_ptr.h\"\n\nnamespace InlineBots {\nstruct ResultSelected;\n} // namespace InlineBots\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass PlainShadow;\nclass PopupMenu;\nclass ScrollArea;\nclass SettingsSlider;\nclass FlatLabel;\nclass BoxContent;\nclass TabbedSearch;\n} // namespace Ui\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nnamespace style {\nstruct EmojiPan;\n} // namespace style\n\nnamespace ChatHelpers {\n\nclass Show;\nclass EmojiListWidget;\nclass StickersListWidget;\nclass GifsListWidget;\nenum class PauseReason;\n\nenum class SelectorTab {\n\tEmoji,\n\tStickers,\n\tGifs,\n\tMasks,\n};\n\nstruct FileChosen {\n\tnot_null<DocumentData*> document;\n\tApi::SendOptions options;\n\tUi::MessageSendingAnimationFrom messageSendingFrom;\n\tstd::shared_ptr<Data::EmojiStatusCollectible> collectible;\n\tTextWithTags caption;\n};\n\nstruct PhotoChosen {\n\tnot_null<PhotoData*> photo;\n\tApi::SendOptions options;\n};\n\nstruct EmojiChosen {\n\tEmojiPtr emoji;\n\tUi::MessageSendingAnimationFrom messageSendingFrom;\n};\n\nusing InlineChosen = InlineBots::ResultSelected;\n\nenum class TabbedSelectorMode {\n\tFull,\n\tEmojiOnly,\n\tStickersOnly,\n\tMediaEditor,\n\tEmojiStatus,\n\tChannelStatus,\n\tBackgroundEmoji,\n\tFullReactions,\n\tRecentReactions,\n\tPeerTitle,\n\tChatIntro,\n};\n\nstruct TabbedSelectorDescriptor {\n\tstd::shared_ptr<Show> show;\n\tconst style::EmojiPan &st;\n\tPauseReason level = {};\n\tTabbedSelectorMode mode = TabbedSelectorMode::Full;\n\tFn<QColor()> customTextColor;\n\tComposeFeatures features;\n};\n\nenum class TabbedSearchType {\n\tEmoji,\n\tStatus,\n\tProfilePhoto,\n\tStickers,\n\tGreeting,\n};\n[[nodiscard]] std::unique_ptr<Ui::TabbedSearch> MakeSearch(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst style::EmojiPan &st,\n\tFn<void(std::vector<QString>&&)> callback,\n\tnot_null<Main::Session*> session,\n\tTabbedSearchType type);\n\nclass TabbedSelector : public Ui::RpWidget {\npublic:\n\tstatic constexpr auto kPickCustomTimeId = -1;\n\tusing Mode = TabbedSelectorMode;\n\tenum class Action {\n\t\tUpdate,\n\t\tCancel,\n\t};\n\n\tTabbedSelector(\n\t\tQWidget *parent,\n\t\tstd::shared_ptr<Show> show,\n\t\tPauseReason level,\n\t\tMode mode = Mode::Full);\n\tTabbedSelector(\n\t\tQWidget *parent,\n\t\tTabbedSelectorDescriptor &&descriptor);\n\t~TabbedSelector();\n\n\t[[nodiscard]] const style::EmojiPan &st() const;\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] PauseReason level() const;\n\n\t[[nodiscard]] rpl::producer<EmojiChosen> emojiChosen() const;\n\t[[nodiscard]] rpl::producer<FileChosen> customEmojiChosen() const;\n\t[[nodiscard]] rpl::producer<FileChosen> fileChosen() const;\n\t[[nodiscard]] rpl::producer<PhotoChosen> photoChosen() const;\n\t[[nodiscard]] rpl::producer<InlineChosen> inlineResultChosen() const;\n\n\t[[nodiscard]] rpl::producer<> cancelled() const;\n\t[[nodiscard]] rpl::producer<> checkForHide() const;\n\t[[nodiscard]] rpl::producer<> slideFinished() const;\n\t[[nodiscard]] rpl::producer<> contextMenuRequested() const;\n\t[[nodiscard]] rpl::producer<Action> choosingStickerUpdated() const;\n\n\tvoid setAllowEmojiWithoutPremium(bool allow);\n\tvoid setRoundRadius(int radius);\n\tvoid refreshStickers();\n\tvoid setCurrentPeer(PeerData *peer);\n\tvoid provideRecentEmoji(\n\t\tconst std::vector<EmojiStatusId> &customRecentList);\n\n\tvoid hideFinished();\n\tvoid showStarted();\n\tvoid beforeHiding();\n\tvoid afterShown();\n\n\t[[nodiscard]] int marginTop() const;\n\t[[nodiscard]] int marginBottom() const;\n\t[[nodiscard]] int scrollTop() const;\n\t[[nodiscard]] int scrollBottom() const;\n\n\tbool preventAutoHide() const;\n\tbool isSliding() const {\n\t\treturn _a_slide.animating();\n\t}\n\tbool hasMenu() const;\n\n\tvoid setAfterShownCallback(Fn<void(SelectorTab)> callback) {\n\t\t_afterShownCallback = std::move(callback);\n\t}\n\tvoid setBeforeHidingCallback(Fn<void(SelectorTab)> callback) {\n\t\t_beforeHidingCallback = std::move(callback);\n\t}\n\n\tvoid showMenuWithDetails(SendMenu::Details details);\n\tvoid setDropDown(bool dropDown);\n\n\t// Float player interface.\n\tbool floatPlayerHandleWheelEvent(QEvent *e);\n\tQRect floatPlayerAvailableRect() const;\n\n\tauto showRequests() const {\n\t\treturn _showRequests.events();\n\t}\n\n\tclass Inner;\n\tclass InnerFooter;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tclass Tab {\n\tpublic:\n\t\tTab(SelectorTab type, int index, object_ptr<Inner> widget);\n\n\t\tobject_ptr<Inner> takeWidget();\n\t\tvoid returnWidget(object_ptr<Inner> widget);\n\n\t\t[[nodiscard]] SelectorTab type() const {\n\t\t\treturn _type;\n\t\t}\n\t\t[[nodiscard]] int index() const {\n\t\t\treturn _index;\n\t\t}\n\t\t[[nodiscard]] Inner *widget() const {\n\t\t\treturn _weak;\n\t\t}\n\t\t[[nodiscard]] bool hasFooter() const {\n\t\t\treturn _footer != nullptr;\n\t\t}\n\t\t[[nodiscard]] not_null<InnerFooter*> footer() const {\n\t\t\treturn _footer;\n\t\t}\n\n\t\tvoid saveScrollTop();\n\t\tvoid saveScrollTop(int scrollTop) {\n\t\t\t_scrollTop = scrollTop;\n\t\t}\n\t\t[[nodiscard]] int getScrollTop() const {\n\t\t\treturn _scrollTop;\n\t\t}\n\n\tprivate:\n\t\tconst SelectorTab _type;\n\t\tconst int _index;\n\t\tobject_ptr<Inner> _widget = { nullptr };\n\t\tQPointer<Inner> _weak;\n\t\tobject_ptr<InnerFooter> _footer;\n\t\tint _scrollTop = 0;\n\n\t};\n\n\tbool full() const;\n\tbool mediaEditor() const;\n\tbool tabbed() const;\n\tbool hasEmojiTab() const;\n\tbool hasStickersTab() const;\n\tbool hasGifsTab() const;\n\tbool hasMasksTab() const;\n\tTab createTab(SelectorTab type, int index);\n\n\tvoid paintSlideFrame(QPainter &p);\n\tvoid paintBgRoundedPart(QPainter &p);\n\tvoid paintContent(QPainter &p);\n\n\tvoid checkRestrictedPeer();\n\tbool isRestrictedView();\n\tvoid updateRestrictedLabelGeometry();\n\tvoid updateScrollGeometry(QSize oldSize);\n\tvoid updateFooterGeometry();\n\tvoid handleScroll();\n\n\tQImage grabForAnimation();\n\n\tvoid scrollToY(int y);\n\n\tvoid showAll();\n\tvoid hideForSliding();\n\n\tSelectorTab typeByIndex(int index) const;\n\tint indexByType(SelectorTab type) const;\n\n\tbool hasSectionIcons() const;\n\tvoid setWidgetToScrollArea();\n\tvoid createTabsSlider();\n\tvoid fillTabsSliderSections();\n\tvoid updateTabsSliderGeometry();\n\tvoid switchTab();\n\n\tnot_null<Tab*> getTab(int index);\n\tnot_null<const Tab*> getTab(int index) const;\n\tnot_null<Tab*> currentTab();\n\tnot_null<const Tab*> currentTab() const;\n\n\tnot_null<EmojiListWidget*> emoji() const;\n\tnot_null<StickersListWidget*> stickers() const;\n\tnot_null<GifsListWidget*> gifs() const;\n\tnot_null<StickersListWidget*> masks() const;\n\n\tvoid reinstallSwipe(not_null<Ui::RpWidget*> widget);\n\n\tconst style::EmojiPan &_st;\n\tconst ComposeFeatures _features;\n\tconst std::shared_ptr<Show> _show;\n\tconst PauseReason _level = {};\n\tconst Fn<QColor()> _customTextColor;\n\n\tUi::Controls::SwipeBackResult _swipeBackData;\n\n\tMode _mode = Mode::Full;\n\tint _roundRadius = 0;\n\tint _footerTop = 0;\n\tbool _noFooter = false;\n\tUi::CornersPixmaps _panelRounding;\n\tUi::CornersPixmaps _categoriesRounding;\n\tPeerData *_currentPeer = nullptr;\n\n\tclass SlideAnimation;\n\tstd::unique_ptr<SlideAnimation> _slideAnimation;\n\tUi::Animations::Simple _a_slide;\n\n\tobject_ptr<Ui::SettingsSlider> _tabsSlider = { nullptr };\n\tobject_ptr<Ui::PlainShadow> _topShadow;\n\tobject_ptr<Ui::PlainShadow> _bottomShadow;\n\tobject_ptr<Ui::ScrollArea> _scroll;\n\tobject_ptr<Ui::FlatLabel> _restrictedLabel = { nullptr };\n\tQString _restrictedLabelKey;\n\tstd::vector<Tab> _tabs;\n\tSelectorTab _currentTabType = SelectorTab::Emoji;\n\n\tconst bool _hasEmojiTab;\n\tconst bool _hasStickersTab;\n\tconst bool _hasGifsTab;\n\tconst bool _hasMasksTab;\n\tconst bool _tabbed;\n\tbool _dropDown = false;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tFn<void(SelectorTab)> _afterShownCallback;\n\tFn<void(SelectorTab)> _beforeHidingCallback;\n\n\trpl::event_stream<> _showRequests;\n\trpl::event_stream<> _slideFinished;\n\n\trpl::lifetime _swipeLifetime;\n\n};\n\nclass TabbedSelector::Inner : public Ui::RpWidget {\npublic:\n\tInner(\n\t\tQWidget *parent,\n\t\tstd::shared_ptr<Show> show,\n\t\tPauseReason level);\n\tInner(\n\t\tQWidget *parent,\n\t\tconst style::EmojiPan &st,\n\t\tstd::shared_ptr<Show> show,\n\t\tFn<bool()> paused);\n\n\t[[nodiscard]] Main::Session &session() const {\n\t\treturn *_session;\n\t}\n\t[[nodiscard]] const style::EmojiPan &st() const {\n\t\treturn _st;\n\t}\n\t[[nodiscard]] Fn<bool()> pausedMethod() const {\n\t\treturn _paused;\n\t}\n\t[[nodiscard]] bool paused() const {\n\t\treturn _paused();\n\t}\n\n\t[[nodiscard]] int getVisibleTop() const {\n\t\treturn _visibleTop;\n\t}\n\t[[nodiscard]] int getVisibleBottom() const {\n\t\treturn _visibleBottom;\n\t}\n\tvoid setMinimalHeight(int newWidth, int newMinimalHeight);\n\n\t[[nodiscard]] rpl::producer<> checkForHide() const {\n\t\treturn _checkForHide.events();\n\t}\n\t[[nodiscard]] bool preventAutoHide() const {\n\t\treturn _preventHideWithBox;\n\t}\n\n\tvirtual void refreshRecent() = 0;\n\tvirtual void preloadImages() {\n\t}\n\tvoid hideFinished();\n\tvoid panelHideFinished();\n\tvirtual void clearSelection() = 0;\n\n\tvirtual void afterShown() {\n\t}\n\tvirtual void beforeHiding() {\n\t}\n\t[[nodiscard]] virtual base::unique_qptr<Ui::PopupMenu> fillContextMenu(\n\t\t\tconst SendMenu::Details &details) {\n\t\treturn nullptr;\n\t}\n\n\trpl::producer<int> scrollToRequests() const;\n\trpl::producer<bool> disableScrollRequests() const;\n\n\tvirtual object_ptr<InnerFooter> createFooter() = 0;\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\tint minimalHeight() const;\n\tvirtual int defaultMinimalHeight() const;\n\tint resizeGetHeight(int newWidth) override final;\n\n\tvirtual int countDesiredHeight(int newWidth) = 0;\n\tvirtual InnerFooter *getFooter() const = 0;\n\tvirtual void processHideFinished() {\n\t}\n\tvirtual void processPanelHideFinished() {\n\t}\n\n\tvoid scrollTo(int y);\n\tvoid disableScroll(bool disabled);\n\n\tvoid checkHideWithBox(object_ptr<Ui::BoxContent> box);\n\n\tvoid paintEmptySearchResults(\n\t\tPainter &p,\n\t\tconst style::icon &icon,\n\t\tconst QString &text,\n\t\tbool skipIcon = false) const;\n\nprivate:\n\tconst style::EmojiPan &_st;\n\tconst std::shared_ptr<Show> _show;\n\tconst not_null<Main::Session*> _session;\n\tconst Fn<bool()> _paused;\n\n\tint _visibleTop = 0;\n\tint _visibleBottom = 0;\n\tstd::optional<int> _minimalHeight;\n\n\trpl::event_stream<int> _scrollToRequests;\n\trpl::event_stream<bool> _disableScrollRequests;\n\trpl::event_stream<> _checkForHide;\n\n\tbool _preventHideWithBox = false;\n\n};\n\nclass TabbedSelector::InnerFooter : public Ui::RpWidget {\npublic:\n\tInnerFooter(QWidget *parent, const style::EmojiPan &st);\n\n\t[[nodiscard]] const style::EmojiPan &st() const;\n\nprotected:\n\tvirtual void processHideFinished() {\n\t}\n\tvirtual void processPanelHideFinished() {\n\t}\n\tfriend class Inner;\n\nprivate:\n\tconst style::EmojiPan &_st;\n\n};\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"chat_helpers/ttl_media_layer_widget.h\"\n\n#include \"base/event_filter.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"editor/editor_layer_widget.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/media/history_view_document.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/section_widget.h\" // Window::ChatThemeValueFromPeer.\n#include \"window/themes/window_theme.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace ChatHelpers {\nnamespace {\n\nclass PreviewDelegate final : public HistoryView::DefaultElementDelegate {\npublic:\n\tPreviewDelegate(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Ui::ChatStyle*> st,\n\t\trpl::producer<bool> chatWideValue,\n\t\tFn<void()> update);\n\n\tbool elementAnimationsPaused() override;\n\tnot_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;\n\tHistoryView::Context elementContext() override;\n\tHistoryView::ElementChatMode elementChatMode() override;\n\nprivate:\n\tconst not_null<QWidget*> _parent;\n\tconst std::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\trpl::variable<bool> _chatWide;\n\n};\n\nPreviewDelegate::PreviewDelegate(\n\tnot_null<QWidget*> parent,\n\tnot_null<Ui::ChatStyle*> st,\n\trpl::producer<bool> chatWideValue,\n\tFn<void()> update)\n: _parent(parent)\n, _pathGradient(HistoryView::MakePathShiftGradient(st, update))\n, _chatWide(std::move(chatWideValue)) {\n}\n\nbool PreviewDelegate::elementAnimationsPaused() {\n\treturn _parent->window()->isActiveWindow();\n}\n\nnot_null<Ui::PathShiftGradient*> PreviewDelegate::elementPathShiftGradient() {\n\treturn _pathGradient.get();\n}\n\nHistoryView::Context PreviewDelegate::elementContext() {\n\treturn HistoryView::Context::TTLViewer;\n}\n\nHistoryView::ElementChatMode PreviewDelegate::elementChatMode() {\n\tusing Mode = HistoryView::ElementChatMode;\n\treturn _chatWide.current() ? Mode::Wide : Mode::Default;\n}\n\nclass PreviewWrap final : public Ui::RpWidget {\npublic:\n\tPreviewWrap(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<HistoryItem*> item,\n\t\trpl::producer<QRect> viewportValue,\n\t\trpl::producer<bool> chatWideValue,\n\t\trpl::producer<std::shared_ptr<Ui::ChatTheme>> theme);\n\t~PreviewWrap();\n\n\t[[nodiscard]] rpl::producer<> closeRequests() const;\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid createView();\n\t[[nodiscard]] bool goodItem() const;\n\tvoid clear();\n\n\tconst not_null<HistoryItem*> _item;\n\tconst std::unique_ptr<Ui::ChatStyle> _style;\n\tconst std::unique_ptr<PreviewDelegate> _delegate;\n\trpl::variable<QRect> _globalViewport;\n\trpl::variable<bool> _chatWide;\n\tstd::shared_ptr<Ui::ChatTheme> _theme;\n\tstd::unique_ptr<HistoryView::Element> _element;\n\tQRect _viewport;\n\tQRect _elementGeometry;\n\trpl::variable<QRect> _elementInner;\n\trpl::lifetime _elementLifetime;\n\n\tQImage _lastFrameCache;\n\n\trpl::event_stream<> _closeRequests;\n\n};\n\nPreviewWrap::PreviewWrap(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<HistoryItem*> item,\n\trpl::producer<QRect> viewportValue,\n\trpl::producer<bool> chatWideValue,\n\trpl::producer<std::shared_ptr<Ui::ChatTheme>> theme)\n: RpWidget(parent)\n, _item(item)\n, _style(std::make_unique<Ui::ChatStyle>(\n\titem->history()->session().colorIndicesValue()))\n, _delegate(std::make_unique<PreviewDelegate>(\n\tparent,\n\t_style.get(),\n\tstd::move(chatWideValue),\n\t[=] { update(_elementGeometry); }))\n, _globalViewport(std::move(viewportValue)) {\n\tconst auto closeCallback = [=] { _closeRequests.fire({}); };\n\tHistoryView::TTLVoiceStops(\n\t\titem->fullId()\n\t) | rpl::on_next([=] {\n\t\t_lastFrameCache = Ui::GrabWidgetToImage(this, _elementGeometry);\n\t\tcloseCallback();\n\t}, lifetime());\n\n\tconst auto isRound = _item\n\t\t&& _item->media()\n\t\t&& _item->media()->document()\n\t\t&& _item->media()->document()->isVideoMessage();\n\n\tstd::move(\n\t\ttheme\n\t) | rpl::on_next([=](std::shared_ptr<Ui::ChatTheme> theme) {\n\t\t_theme = std::move(theme);\n\t\t_style->apply(_theme.get());\n\t}, lifetime());\n\n\tconst auto session = &_item->history()->session();\n\tsession->data().viewRepaintRequest(\n\t) | rpl::on_next([=](Data::RequestViewRepaint data) {\n\t\tif (data.view == _element.get()) {\n\t\t\tupdate(_elementGeometry);\n\t\t}\n\t}, lifetime());\n\tsession->data().itemViewRefreshRequest(\n\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\tif (item == _item) {\n\t\t\tif (goodItem()) {\n\t\t\t\tcreateView();\n\t\t\t\tupdate();\n\t\t\t} else {\n\t\t\t\tclear();\n\t\t\t\t_closeRequests.fire({});\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\tsession->data().itemDataChanges(\n\t) | rpl::on_next([=](not_null<HistoryItem*> item) {\n\t\tif (item == _item) {\n\t\t\t_element->itemDataChanged();\n\t\t}\n\t}, lifetime());\n\tsession->data().itemRemoved(\n\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\tif (item == _item) {\n\t\t\t_closeRequests.fire({});\n\t\t}\n\t}, lifetime());\n\n\t{\n\t\tconst auto close = Ui::CreateChild<Ui::RoundButton>(\n\t\t\tthis,\n\t\t\titem->out()\n\t\t\t\t? tr::lng_close()\n\t\t\t\t: tr::lng_ttl_voice_close_in(),\n\t\t\tst::ttlMediaButton);\n\t\tclose->setFullRadius(true);\n\t\tclose->setClickedCallback(closeCallback);\n\n\t\trpl::combine(\n\t\t\tsizeValue(),\n\t\t\t_elementInner.value()\n\t\t) | rpl::on_next([=](QSize size, QRect inner) {\n\t\t\tclose->moveToLeft(\n\t\t\t\tinner.x() + (inner.width() - close->width()) / 2,\n\t\t\t\t(size.height()\n\t\t\t\t\t- close->height()\n\t\t\t\t\t- st::ttlMediaButtonBottomSkip));\n\t\t}, close->lifetime());\n\t}\n\n\tQWidget::setAttribute(Qt::WA_OpaquePaintEvent, false);\n\tcreateView();\n\n\t{\n\t\tauto text = item->out()\n\t\t\t? (isRound\n\t\t\t\t? tr::lng_ttl_round_tooltip_out\n\t\t\t\t: tr::lng_ttl_voice_tooltip_out)(\n\t\t\t\t\tlt_user,\n\t\t\t\t\trpl::single(\n\t\t\t\t\t\titem->history()->peer->shortName()\n\t\t\t\t\t) | rpl::map(tr::rich),\n\t\t\t\t\ttr::rich)\n\t\t\t: (isRound\n\t\t\t\t? tr::lng_ttl_round_tooltip_in\n\t\t\t\t: tr::lng_ttl_voice_tooltip_in)(tr::rich);\n\t\tconst auto tooltip = Ui::CreateChild<Ui::ImportantTooltip>(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\t\t\tthis,\n\t\t\t\tUi::MakeNiceTooltipLabel(\n\t\t\t\t\tparent,\n\t\t\t\t\tstd::move(text),\n\t\t\t\t\tst::dialogsStoriesTooltipMaxWidth,\n\t\t\t\t\tst::ttlMediaImportantTooltipLabel),\n\t\t\t\tst::defaultImportantTooltip.padding),\n\t\t\tst::dialogsStoriesTooltip);\n\t\ttooltip->toggleFast(true);\n\t\t_elementInner.value(\n\t\t) | rpl::filter([](const QRect &inner) {\n\t\t\treturn !inner.isEmpty();\n\t\t}) | rpl::on_next([=](const QRect &inner) {\n\t\t\ttooltip->pointAt(inner, RectPart::Top, [=](QSize size) {\n\t\t\t\treturn QPoint{\n\t\t\t\t\tinner.x() + (inner.width() - size.width()) / 2,\n\t\t\t\t\t(inner.y()\n\t\t\t\t\t\t- st::normalFont->height\n\t\t\t\t\t\t- size.height()\n\t\t\t\t\t\t- st::defaultImportantTooltip.padding.top()),\n\t\t\t\t};\n\t\t\t});\n\t\t}, tooltip->lifetime());\n\t}\n}\n\nrpl::producer<> PreviewWrap::closeRequests() const {\n\treturn _closeRequests.events();\n}\n\nbool PreviewWrap::goodItem() const {\n\tconst auto media = _item->media();\n\tif (!media || !media->ttlSeconds()) {\n\t\treturn false;\n\t}\n\tconst auto document = media->document();\n\treturn document\n\t\t&& (document->isVoiceMessage() || document->isVideoMessage());\n}\n\nvoid PreviewWrap::createView() {\n\tclear();\n\t_element = _item->createView(_delegate.get());\n\t_element->initDimensions();\n\trpl::combine(\n\t\tsizeValue(),\n\t\t_globalViewport.value()\n\t) | rpl::on_next([=](QSize outer, QRect globalViewport) {\n\t\t_viewport = globalViewport.isEmpty()\n\t\t\t? rect()\n\t\t\t: mapFromGlobal(globalViewport);\n\t\tif (_viewport.width() < st::msgMinWidth) {\n\t\t\treturn;\n\t\t}\n\t\t_element->resizeGetHeight(_viewport.width());\n\t\t_elementGeometry = QRect(\n\t\t\t(_viewport.width() - _element->width()) / 2,\n\t\t\t(_viewport.height() - _element->height()) / 2,\n\t\t\t_element->width(),\n\t\t\t_element->height()\n\t\t).translated(_viewport.topLeft());\n\t\t_elementInner = _element->innerGeometry().translated(\n\t\t\t_elementGeometry.topLeft());\n\t\tupdate();\n\t}, _elementLifetime);\n}\n\nvoid PreviewWrap::clear() {\n\t_elementLifetime.destroy();\n\t_element = nullptr;\n}\n\nPreviewWrap::~PreviewWrap() {\n\tclear();\n}\n\nvoid PreviewWrap::paintEvent(QPaintEvent *e) {\n\tif (!_element || _elementGeometry.isEmpty()) {\n\t\treturn;\n\t}\n\n\tauto p = Painter(this);\n\tp.translate(_elementGeometry.topLeft());\n\tif (!_lastFrameCache.isNull()) {\n\t\tp.drawImage(0, 0, _lastFrameCache);\n\t} else {\n\t\tauto context = _theme->preparePaintContext(\n\t\t\t_style.get(),\n\t\t\tRect(_element->currentSize()),\n\t\t\tRect(_element->currentSize()),\n\t\t\tRect(_element->currentSize()),\n\t\t\t!window()->isActiveWindow());\n\t\tcontext.outbg = _element->hasOutLayout();\n\t\t_element->draw(p, context);\n\t}\n}\n\nrpl::producer<QRect> GlobalViewportForWindow(\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto delegate = controller->window().floatPlayerDelegate();\n\treturn rpl::single(rpl::empty) | rpl::then(\n\t\tdelegate->floatPlayerAreaUpdates()\n\t) | rpl::map([=] {\n\t\tauto section = (Media::Player::FloatSectionDelegate*)nullptr;\n\t\tdelegate->floatPlayerEnumerateSections([&](\n\t\t\t\tnot_null<Media::Player::FloatSectionDelegate*> check,\n\t\t\t\tWindow::Column column) {\n\t\t\tif ((column == Window::Column::First && !section)\n\t\t\t\t|| column == Window::Column::Second) {\n\t\t\t\tsection = check;\n\t\t\t}\n\t\t});\n\t\tif (section) {\n\t\t\tconst auto rect = section->floatPlayerAvailableRect();\n\t\t\tif (rect.width() >= st::msgMinWidth) {\n\t\t\t\treturn rect;\n\t\t\t}\n\t\t}\n\t\treturn QRect();\n\t});\n}\n\n} // namespace\n\nvoid ShowTTLMediaLayerWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto parent = controller->content();\n\tconst auto show = controller->uiShow();\n\tauto preview = base::make_unique_q<PreviewWrap>(\n\t\tparent,\n\t\titem,\n\t\tGlobalViewportForWindow(controller),\n\t\tcontroller->adaptive().chatWideValue(),\n\t\tWindow::ChatThemeValueFromPeer(\n\t\t\tcontroller,\n\t\t\titem->history()->peer));\n\tpreview->closeRequests(\n\t) | rpl::on_next([=] {\n\t\tshow->hideLayer();\n\t}, preview->lifetime());\n\tauto layer = std::make_unique<Editor::LayerWidget>(\n\t\tparent,\n\t\tstd::move(preview));\n\tlayer->lifetime().add([] { ::Media::Player::instance()->stop(); });\n\tbase::install_event_filter(layer.get(), [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::KeyPress) {\n\t\t\tconst auto k = static_cast<QKeyEvent*>(e.get());\n\t\t\tif (k->key() == Qt::Key_Escape) {\n\t\t\t\tshow->hideLayer();\n\t\t\t}\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\tcontroller->showLayer(std::move(layer), Ui::LayerOption::KeepOther);\n}\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass HistoryItem;\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace ChatHelpers {\n\nvoid ShowTTLMediaLayerWidget(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<HistoryItem*> item);\n\n} // namespace ChatHelpers\n"
  },
  {
    "path": "Telegram/SourceFiles/codegen/scheme/codegen_scheme.py",
    "content": "'''\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n'''\nimport glob, re, binascii, os, sys\n\nsys.dont_write_bytecode = True\nscriptPath = os.path.dirname(os.path.realpath(__file__))\nsys.path.append(scriptPath + '/../../../lib_tl/tl')\nfrom generate_tl import generate\n\ngenerate({\n  'namespaces': {\n    'creator': 'MTP::details',\n  },\n  'prefixes': {\n    'type': 'MTP',\n    'data': 'MTPD',\n    'id': 'mtpc',\n    'construct': 'MTP_',\n  },\n  'types': {\n    'prime': 'mtpPrime',\n    'typeId': 'mtpTypeId',\n    'buffer': 'mtpBuffer',\n  },\n  'sections': [\n    'read-write',\n  ],\n\n  # define some checked flag conversions\n  # the key flag type should be a subset of the value flag type\n  # with exact the same names, then the key flag can be implicitly\n  # casted to the value flag type\n  'flagInheritance': {\n    'messageService': 'message',\n    'updateShortMessage': 'message',\n    'updateShortChatMessage': 'message',\n    'updateShortSentMessage': 'message',\n    'replyKeyboardHide': 'replyKeyboardMarkup',\n    'replyKeyboardForceReply': 'replyKeyboardMarkup',\n    'inputPeerNotifySettings': 'peerNotifySettings',\n    'peerNotifySettings': 'inputPeerNotifySettings',\n    'channelForbidden': 'channel',\n    'dialogFolder': 'dialog',\n  },\n\n  'typeIdExceptions': [\n    'channel#c88974ac',\n    'ipPortSecret#37982646',\n    'accessPointRule#4679b65f',\n    'help.configSimple#5a592a6c',\n    'messageReplies#81834865',\n  ],\n\n  'renamedTypes': {\n    'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow': 'passwordKdfAlgoModPow',\n   },\n\n  'skip': [\n    'int ? = Int;',\n    'long ? = Long;',\n    'double ? = Double;',\n    'string ? = String;',\n\n    'vector {t:Type} # [ t ] = Vector t;',\n\n    'int128 4*[ int ] = Int128;',\n    'int256 8*[ int ] = Int256;',\n\n    'vector#1cb5c415 {t:Type} # [ t ] = Vector t;',\n  ],\n  'builtin': [\n    'int',\n    'long',\n    'double',\n    'string',\n    'bytes',\n    'int128',\n    'int256',\n  ],\n  'builtinTemplates': [\n    'vector',\n    'flags',\n  ],\n  'synonyms': {\n    'bytes': 'string',\n  },\n  'builtinInclude': 'mtproto/core_types.h',\n  'optimizeSingleData': True,\n\n  'dumpToText': {\n    'include': 'mtproto/details/mtproto_dump_to_text.h',\n  },\n\n})\n"
  },
  {
    "path": "Telegram/SourceFiles/config.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"core/version.h\"\n#include \"settings.h\"\n\nenum {\n\tMaxSelectedItems = 100,\n\n\tLocalEncryptIterCount = 4000, // key derivation iteration count\n\tLocalEncryptNoPwdIterCount = 4, // key derivation iteration count without pwd (not secure anyway)\n\tLocalEncryptSaltSize = 32, // 256 bit\n\n\tRecentInlineBotsLimit = 10,\n\n\tAutoSearchTimeout = 900, // 0.9 secs\n\n\tPreloadHeightsCount = 3, // when 3 screens to scroll left make a preload request\n\n\tSearchPeopleLimit = 5,\n\n\tMaxMessageSize = 4096,\n\n\tWebPageUserId = 701000,\n\n\tUpdateDelayConstPart = 8 * 3600, // 8 hour min time between update check requests\n\tUpdateDelayRandPart = 8 * 3600, // 8 hour max - min time between update check requests\n\n\tWrongPasscodeTimeout = 1500,\n\n\tChoosePeerByDragTimeout = 1000, // 1 second mouse not moved to choose dialog when dragging a file\n};\n\ninline const char *cGUIDStr() {\n#ifndef OS_MAC_STORE\n\tstatic const char *gGuidStr = \"{87A94AB0-E370-4cde-98D3-ACC110C5967D}\";\n#else // OS_MAC_STORE\n\tstatic const char *gGuidStr = \"{E51FB841-8C0B-4EF9-9E9E-5A0078567627}\";\n#endif // OS_MAC_STORE\n\n\treturn gGuidStr;\n}\n\nstatic const char *UpdatesPublicKey = \"\\\n-----BEGIN RSA PUBLIC KEY-----\\n\\\nMIGJAoGBAMA4ViQrjkPZ9xj0lrer3r23JvxOnrtE8nI69XLGSr+sRERz9YnUptnU\\n\\\nBZpkIfKaRcl6XzNJiN28cVwO1Ui5JSa814UAiDHzWUqCaXUiUEQ6NmNTneiGx2sQ\\n\\\n+9PKKlb8mmr3BB9A45ZNwLT6G9AK3+qkZLHojeSA+m84/a6GP4svAgMBAAE=\\n\\\n-----END RSA PUBLIC KEY-----\\\n\";\n\nstatic const char *UpdatesPublicBetaKey = \"\\\n-----BEGIN RSA PUBLIC KEY-----\\n\\\nMIGJAoGBALWu9GGs0HED7KG7BM73CFZ6o0xufKBRQsdnq3lwA8nFQEvmdu+g/I1j\\n\\\n0LQ+0IQO7GW4jAgzF/4+soPDb6uHQeNFrlVx1JS9DZGhhjZ5rf65yg11nTCIHZCG\\n\\\nw/CVnbwQOw0g5GBwwFV3r0uTTvy44xx8XXxk+Qknu4eBCsmrAFNnAgMBAAE=\\n\\\n-----END RSA PUBLIC KEY-----\\\n\";\n\n#if defined TDESKTOP_API_ID && defined TDESKTOP_API_HASH\n\nconstexpr auto ApiId = TDESKTOP_API_ID;\nconstexpr auto ApiHash = QT_STRINGIFY(TDESKTOP_API_HASH);\n\n#else // TDESKTOP_API_ID && TDESKTOP_API_HASH\n\n// To build your version of Telegram Desktop you're required to provide\n// your own 'api_id' and 'api_hash' for the Telegram API access.\n//\n// How to obtain your 'api_id' and 'api_hash' is described here:\n// https://core.telegram.org/api/obtaining_api_id\n//\n// If you're building the application not for deployment,\n// but only for test purposes you can comment out the error below.\n//\n// This will allow you to use TEST ONLY 'api_id' and 'api_hash' which are\n// very limited by the Telegram API server.\n//\n// Your users will start getting internal server errors on login\n// if you deploy an app using those 'api_id' and 'api_hash'.\n\n#error You are required to provide API_ID and API_HASH.\n\nconstexpr auto ApiId = 17349;\nconstexpr auto ApiHash = \"344583e45741c457fe1862106095a5eb\";\n\n#endif // TDESKTOP_API_ID && TDESKTOP_API_HASH\n\n#if Q_BYTE_ORDER == Q_BIG_ENDIAN\n#error \"Only little endian is supported!\"\n#endif // Q_BYTE_ORDER == Q_BIG_ENDIAN\n\n#if (TDESKTOP_ALPHA_VERSION != 0)\n\n// Private key for downloading closed alphas.\n#include \"../../../DesktopPrivate/alpha_private.h\"\n\n#else\nstatic const char *AlphaPrivateKey = \"\";\n#endif\n\nextern QString gKeyFile;\ninline const QString &cDataFile() {\n\tif (!gKeyFile.isEmpty()) return gKeyFile;\n\tstatic const QString res(u\"data\"_q);\n\treturn res;\n}\n\ninline const QRegularExpression &cRussianLetters() {\n\tstatic QRegularExpression regexp(QString::fromUtf8(\"[а-яА-ЯёЁ]\"));\n\treturn regexp;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/core/application.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/application.h\"\n\n#include \"data/data_abstract_structure.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_session.h\"\n#include \"data/data_download_manager.h\"\n#include \"base/battery_saving.h\"\n#include \"base/event_filter.h\"\n#include \"base/invoke_queued.h\"\n#include \"base/concurrent_timer.h\"\n#include \"base/options.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"base/timer.h\"\n#include \"base/unixtime.h\"\n#include \"core/core_settings.h\"\n#include \"core/update_checker.h\"\n#include \"core/shortcuts.h\"\n#include \"core/sandbox.h\"\n#include \"core/local_url_handlers.h\"\n#include \"core/launcher.h\"\n#include \"core/ui_integration.h\"\n#include \"chat_helpers/emoji_keywords.h\"\n#include \"chat_helpers/stickers_emoji_image_loader.h\"\n#include \"base/platform/base_platform_global_shortcuts.h\"\n#include \"base/platform/base_platform_url_scheme.h\"\n#include \"base/platform/base_platform_last_input.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"platform/platform_specific.h\"\n#include \"platform/platform_integration.h\"\n#include \"history/history.h\"\n#include \"apiwrap.h\"\n#include \"api/api_updates.h\"\n#include \"calls/calls_instance.h\"\n#include \"countries/countries_manager.h\"\n#include \"iv/iv_delegate_impl.h\"\n#include \"iv/iv_instance.h\"\n#include \"iv/iv_data.h\"\n#include \"lang/lang_translator.h\"\n#include \"lang/lang_cloud_manager.h\"\n#include \"lang/lang_hardcoded.h\"\n#include \"lang/lang_instance.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n#include \"mainwidget.h\"\n#include \"tray.h\"\n#include \"core/click_handler_types.h\" // ClickHandlerContext.\n#include \"core/crash_reports.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"media/view/media_view_overlay_widget.h\"\n#include \"media/view/media_view_open_common.h\"\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"media/audio/media_audio_track.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"media/player/media_player_float.h\"\n#include \"media/clip/media_clip_reader.h\" // For Media::Clip::Finish().\n#include \"media/system_media_controls_manager.h\"\n#include \"window/notifications_manager.h\"\n#include \"window/themes/window_theme.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/gl/gl_detection.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/screen_reader_mode.h\"\n#include \"storage/storage_domain.h\"\n#include \"storage/storage_databases.h\"\n#include \"storage/localstorage.h\"\n#include \"payments/payments_checkout_process.h\"\n#include \"export/export_manager.h\"\n#include \"webrtc/webrtc_environment.h\"\n#include \"window/window_separate_id.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"boxes/abstract_box.h\"\n#include \"base/qthelp_regex.h\"\n#include \"base/qthelp_url.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"ui/accessible/ui_accessible_factory.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"core/cached_webview_availability.h\"\n#include \"styles/style_window.h\"\n\n#include <QtCore/QStandardPaths>\n#include <QtCore/QMimeDatabase>\n#include <QtGui/QGuiApplication>\n#include <QtGui/QScreen>\n#include <QtGui/QWindow>\n\n#include <ksandbox.h>\n\nnamespace Core {\nnamespace {\n\nconstexpr auto kQuitPreventTimeoutMs = crl::time(1500);\nconstexpr auto kAutoLockTimeoutLateMs = crl::time(3000);\nconstexpr auto kClearEmojiImageSourceTimeout = 10 * crl::time(1000);\n\nLaunchState GlobalLaunchState/* = LaunchState::Running*/;\n\nvoid SetCrashAnnotationsGL() {\n#ifdef DESKTOP_APP_USE_ANGLE\n\tCrashReports::SetAnnotation(\"OpenGL ANGLE\", [] {\n\t\tif (Core::App().settings().disableOpenGL()) {\n\t\t\treturn \"Disabled\";\n\t\t} else switch (Ui::GL::CurrentANGLE()) {\n\t\tcase Ui::GL::ANGLE::Auto: return \"Auto\";\n\t\tcase Ui::GL::ANGLE::D3D11: return \"Direct3D 11\";\n\t\tcase Ui::GL::ANGLE::D3D9: return \"Direct3D 9\";\n\t\tcase Ui::GL::ANGLE::D3D11on12: return \"D3D11on12\";\n\t\t//case Ui::GL::ANGLE::OpenGL: return \"OpenGL\";\n\t\t}\n\t\tUnexpected(\"Ui::GL::CurrentANGLE value in SetupANGLE.\");\n\t}());\n#else // DESKTOP_APP_USE_ANGLE\n\tCrashReports::SetAnnotation(\n\t\t\"OpenGL\",\n\t\tCore::App().settings().disableOpenGL() ? \"Disabled\" : \"Enabled\");\n#endif // DESKTOP_APP_USE_ANGLE\n}\n\nbase::options::toggle OptionSkipUrlSchemeRegister({\n\t.id = kOptionSkipUrlSchemeRegister,\n\t.name = \"Skip URL scheme register\",\n\t.description = \"Don't re-register tg:// URL scheme on autoupdate.\",\n});\n\n} // namespace\n\nApplication *Application::Instance = nullptr;\n\nconst char kOptionSkipUrlSchemeRegister[] = \"skip-url-scheme-register\";\n\nstruct Application::Private {\n\tbase::Timer quitTimer;\n\tUiIntegration uiIntegration;\n\tSettings settings;\n};\n\nApplication::Application()\n: QObject()\n, _private(std::make_unique<Private>())\n, _platformIntegration(Platform::Integration::Create())\n, _batterySaving(std::make_unique<base::BatterySaving>())\n, _mediaDevices(std::make_unique<Webrtc::Environment>())\n, _databases(std::make_unique<Storage::Databases>())\n, _animationsManager(std::make_unique<Ui::Animations::Manager>())\n, _clearEmojiImageLoaderTimer([=] { clearEmojiSourceImages(); })\n, _audio(std::make_unique<Media::Audio::Instance>())\n, _fallbackProductionConfig(\n\tstd::make_unique<MTP::Config>(MTP::Environment::Production))\n, _downloadManager(std::make_unique<Data::DownloadManager>())\n, _domain(std::make_unique<Main::Domain>(cDataFile()))\n, _exportManager(std::make_unique<Export::Manager>())\n, _calls(std::make_unique<Calls::Instance>())\n, _iv(std::make_unique<Iv::Instance>(\n\tUi::CreateChild<Iv::DelegateImpl>(this)))\n, _langpack(std::make_unique<Lang::Instance>())\n, _langCloudManager(std::make_unique<Lang::CloudManager>(langpack()))\n, _emojiKeywords(std::make_unique<ChatHelpers::EmojiKeywords>())\n, _tray(std::make_unique<Tray>())\n, _setupEmailLock(false)\n, _autoLockTimer([=] { checkAutoLock(); }) {\n\tUi::Integration::Set(&_private->uiIntegration);\n\n\t_platformIntegration->init();\n\n\tpasscodeLockChanges(\n\t) | rpl::on_next([=](bool locked) {\n\t\t_shouldLockAt = 0;\n\t\tif (locked) {\n\t\t\tcloseAdditionalWindows();\n\t\t}\n\t}, _lifetime);\n\n\tpasscodeLockChanges(\n\t) | rpl::on_next([=] {\n\t\t_notifications->updateAll();\n\t\tupdateWindowTitles();\n\t}, _lifetime);\n\n\tsettings().windowTitleContentChanges(\n\t) | rpl::on_next([=] {\n\t\tupdateWindowTitles();\n\t}, _lifetime);\n\n\t_domain->activeSessionChanges(\n\t) | rpl::on_next([=](Main::Session *session) {\n\t\tif (session && !UpdaterDisabled()) { // #TODO multi someSessionValue\n\t\t\tUpdateChecker().setMtproto(session);\n\t\t}\n\t}, _lifetime);\n}\n\nvoid Application::closeAdditionalWindows() {\n\tPayments::CheckoutProcess::ClearAll();\n\tfor (const auto &[index, account] : _domain->accounts()) {\n\t\tif (account->sessionExists()) {\n\t\t\taccount->session().attachWebView().closeAll();\n\t\t}\n\t}\n\t_iv->closeAll();\n}\n\nApplication::~Application() {\n\tif (_saveSettingsTimer && _saveSettingsTimer->isActive()) {\n\t\tLocal::writeSettings();\n\t}\n\n\t_windowStack.clear();\n\tsetLastActiveWindow(nullptr);\n\t_windowInSettings = _lastActivePrimaryWindow = nullptr;\n\t_closingAsyncWindows.clear();\n\t_windows.clear();\n\t_mediaView = nullptr;\n\t_notifications->clearAllFast();\n\n\t// We must manually destroy all windows before going further.\n\t// DestroyWindow on Windows (at least with an active WebView) enters\n\t// event loop and invoke scheduled crl::on_main callbacks.\n\t//\n\t// For example Domain::removeRedundantAccounts() is called from\n\t// Domain::finish() and there is a violation on Ensures(started()).\n\tcloseAdditionalWindows();\n\n\t_domain->finish();\n\n\tLocal::finish();\n\n\tShortcuts::Finish();\n\n\tUi::Emoji::Clear();\n\tMedia::Clip::Finish();\n\n\tUi::FinishCachedCorners();\n\tData::clearGlobalStructures();\n\n\tWindow::Theme::Uninitialize();\n\n\t_mediaControlsManager = nullptr;\n\n\tMedia::Player::finish(_audio.get());\n\tstyle::StopManager();\n\n\tInstance = nullptr;\n}\n\nvoid Application::run() {\n\t// Depends on OpenSSL on macOS, so on ThirdParty::start().\n\t// Depends on notifications settings.\n\t_notifications = std::make_unique<Window::Notifications::System>();\n\n\tstartLocalStorage();\n\n\tstyle::SetCustomFont(settings().customFontFamily());\n\tstyle::internal::StartFonts();\n\n\tValidateScale();\n\n\trefreshGlobalProxy(); // Depends on app settings being read.\n\n\tif (const auto old = Local::oldSettingsVersion(); old < AppVersion) {\n\t\tautoRegisterUrlScheme();\n\t\tPlatform::NewVersionLaunched(old);\n\t}\n\n\tif (cAutoStart() && !Platform::AutostartSupported()) {\n\t\tcSetAutoStart(false);\n\t}\n\n\tif (cLaunchMode() == LaunchModeAutoStart && Platform::AutostartSkip()) {\n\t\tPlatform::AutostartToggle(false);\n\t\tQuit();\n\t\treturn;\n\t}\n\n\t_translator = std::make_unique<Lang::Translator>();\n\tQCoreApplication::instance()->installTranslator(_translator.get());\n\n\tstyle::StartManager(cScale());\n\tUi::Accessible::Init();\n\tUi::InitTextOptions();\n\tUi::StartCachedCorners();\n\tUi::Emoji::Init();\n\tUi::PreloadTextSpoilerMask();\n\tstartShortcuts();\n\tstartEmojiImageLoader();\n\tstartSystemDarkModeViewer();\n\tMedia::Player::start(_audio.get());\n\n\tif (MediaControlsManager::Supported()) {\n\t\t_mediaControlsManager = std::make_unique<MediaControlsManager>();\n\t}\n\n\trpl::combine(\n\t\t_batterySaving->value(),\n\t\tsettings().ignoreBatterySavingValue()\n\t) | rpl::on_next([=](bool saving, bool ignore) {\n\t\tPowerSaving::SetForceAll(saving && !ignore);\n\t}, _lifetime);\n\n\tstyle::ShortAnimationPlaying(\n\t) | rpl::on_next([=](bool playing) {\n\t\tif (playing) {\n\t\t\tMTP::details::pause();\n\t\t} else {\n\t\t\tMTP::details::unpause();\n\t\t}\n\t}, _lifetime);\n\n\tDEBUG_LOG((\"Application Info: inited...\"));\n\n\tDEBUG_LOG((\"Application Info: starting app...\"));\n\n\t// Create mime database, so it won't be slow later.\n\tQMimeDatabase().mimeTypeForName(u\"text/plain\"_q);\n\n\t// Check now to avoid re-entrance later.\n\t[[maybe_unused]] const auto &webviewAvailability\n\t\t= Core::CachedWebviewAvailability();\n\n\t_windows.emplace(nullptr, std::make_unique<Window::Controller>());\n\tsetLastActiveWindow(_windows.front().second.get());\n\t_windowInSettings = _lastActivePrimaryWindow = _lastActiveWindow;\n\n\t_domain->activeChanges(\n\t) | rpl::on_next([=](not_null<Main::Account*> account) {\n\t\tshowAccount(account);\n\t}, _lifetime);\n\n\t(\n\t\t_domain->activeValue(\n\t\t) | rpl::to_empty | rpl::filter([=] {\n\t\t\treturn _domain->started();\n\t\t}) | rpl::take(1)\n\t) | rpl::then(\n\t\t_domain->accountsChanges()\n\t) | rpl::map([=] {\n\t\treturn (_domain->accounts().size() > Main::Domain::kMaxAccounts)\n\t\t\t? _domain->activeChanges()\n\t\t\t: rpl::never<not_null<Main::Account*>>();\n\t}) | rpl::flatten_latest(\n\t) | rpl::on_next([=](not_null<Main::Account*> account) {\n\t\tconst auto ordered = _domain->orderedAccounts();\n\t\tconst auto it = ranges::find(ordered, account);\n\t\tif (_lastActivePrimaryWindow && it != end(ordered)) {\n\t\t\tconst auto index = std::distance(begin(ordered), it);\n\t\t\tif ((index + 1) > _domain->maxAccounts()) {\n\t\t\t\t_lastActivePrimaryWindow->show(Box(\n\t\t\t\t\tAccountsLimitBox,\n\t\t\t\t\t&account->session()));\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n\n\tQCoreApplication::instance()->installEventFilter(this);\n\n\tappDeactivatedValue(\n\t) | rpl::on_next([=](bool deactivated) {\n\t\tif (deactivated) {\n\t\t\thandleAppDeactivated();\n\t\t} else {\n\t\t\thandleAppActivated();\n\t\t}\n\t}, _lifetime);\n\n\tDEBUG_LOG((\"Application Info: window created...\"));\n\n\tstartDomain();\n\tstyle::SetSquareUserpics(settings().fork().squareUserpics());\n\n\tstartTray();\n\n\t_lastActivePrimaryWindow->firstShow();\n\n\tstartMediaView();\n\n\tDEBUG_LOG((\"Application Info: showing.\"));\n\t_lastActivePrimaryWindow->finishFirstShow();\n\n\tif (!_lastActivePrimaryWindow->locked() && cStartToSettings()) {\n\t\t_lastActivePrimaryWindow->showSettings();\n\t}\n\n\t_lastActivePrimaryWindow->updateIsActiveFocus();\n\n\tfor (const auto &error : Shortcuts::Errors()) {\n\t\tLOG((\"Shortcuts Error: %1\").arg(error));\n\t}\n\n\tSetCrashAnnotationsGL();\n\tif (Ui::GL::LastCrashCheckFailed()) {\n\t\tshowOpenGLCrashNotification();\n\t}\n\n\t_openInMediaViewRequests.events(\n\t) | rpl::on_next([=](Media::View::OpenRequest &&request) {\n\t\tif (_mediaView) {\n\t\t\t_mediaView->show(std::move(request));\n\t\t}\n\t}, _lifetime);\n\t{\n\t\tconst auto countries = std::make_shared<Countries::Manager>(\n\t\t\t_domain.get());\n\t\tcountries->lifetime().add([=] {\n\t\t\t[[maybe_unused]] const auto countriesCopy = countries;\n\t\t});\n\t}\n\n\tprocessCreatedWindow(_lastActivePrimaryWindow);\n}\n\nvoid Application::autoRegisterUrlScheme() {\n\tif (!OptionSkipUrlSchemeRegister.value()) {\n\t\tInvokeQueued(this, [] { RegisterUrlScheme(); });\n\t}\n}\n\nvoid Application::showAccount(not_null<Main::Account*> account) {\n\tif (const auto separate = separateWindowFor(account)) {\n\t\t_lastActivePrimaryWindow = separate;\n\t\tseparate->activate();\n\t} else if (const auto last = activePrimaryWindow()) {\n\t\tlast->showAccount(account);\n\t}\n}\n\nvoid Application::checkWindowId(not_null<Window::Controller*> window) {\n\tconst auto id = window->id();\n\tfor (auto &[existingId, existing] : _windows) {\n\t\tif (existing.get() == window && existingId != id) {\n\t\t\tauto found = std::move(existing);\n\t\t\t_windows.remove(existingId);\n\t\t\t_windows.emplace(id, std::move(found));\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nvoid Application::showOpenGLCrashNotification() {\n\tconst auto enable = [=] {\n\t\tUi::GL::CrashCheckFinish();\n\t\tsettings().setDisableOpenGL(false);\n\t\tLocal::writeSettings();\n\t\tRestart();\n\t};\n\tconst auto keepDisabled = [=](Fn<void()> close) {\n\t\tUi::GL::CrashCheckFinish();\n\t\tsettings().setDisableOpenGL(true);\n\t\tLocal::writeSettings();\n\t\tclose();\n\t};\n\t_lastActivePrimaryWindow->show(Ui::MakeConfirmBox({\n\t\t.text = \"\"\n\t\t\"There may be a problem with your graphics drivers and OpenGL. \"\n\t\t\"Try updating your drivers.\\n\\n\"\n\t\t\"OpenGL has been disabled. You can try to enable it again \"\n\t\t\"or keep it disabled if crashes continue.\",\n\t\t.confirmed = enable,\n\t\t.cancelled = keepDisabled,\n\t\t.confirmText = \"Enable\",\n\t\t.cancelText = \"Keep Disabled\",\n\t}));\n}\n\nvoid Application::startDomain() {\n\tconst auto state = _domain->start(QByteArray());\n\tif (state != Storage::StartResult::IncorrectPasscodeLegacy) {\n\t\t// In case of non-legacy passcoded app all global settings are ready.\n\t\tstartSettingsAndBackground();\n\t}\n\tif (state != Storage::StartResult::Success) {\n\t\tlockByPasscode();\n\t\tDEBUG_LOG((\"Application Info: passcode needed...\"));\n\t}\n}\n\nvoid Application::startSettingsAndBackground() {\n\tLocal::rewriteSettingsIfNeeded();\n\tWindow::Theme::Background()->start();\n\tcheckSystemDarkMode();\n\tUi::SetScreenReaderModeDisabled(\n\t\tsettings().readPref<bool>(kScreenReaderModeDisabledKey));\n}\n\nvoid Application::checkSystemDarkMode() {\n\tconst auto maybeDarkMode = settings().systemDarkMode();\n\tconst auto darkModeEnabled = settings().systemDarkModeEnabled();\n\tconst auto needToSwitch = darkModeEnabled\n\t\t&& maybeDarkMode\n\t\t&& (*maybeDarkMode != Window::Theme::IsNightMode());\n\tif (needToSwitch) {\n\t\tWindow::Theme::ToggleNightMode();\n\t\tWindow::Theme::KeepApplied();\n\t}\n}\n\nvoid Application::startSystemDarkModeViewer() {\n\tif (Window::Theme::Background()->editingTheme()) {\n\t\tsettings().setSystemDarkModeEnabled(false);\n\t}\n\trpl::merge(\n\t\tsettings().systemDarkModeChanges() | rpl::to_empty,\n\t\tsettings().systemDarkModeEnabledChanges() | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\tcheckSystemDarkMode();\n\t}, _lifetime);\n}\n\nvoid Application::enumerateWindows(Fn<void(\n\t\tnot_null<Window::Controller*>)> callback) const {\n\tfor (const auto &window : ranges::views::values(_windows)) {\n\t\tcallback(window.get());\n\t}\n}\n\nvoid Application::processCreatedWindow(\n\t\tnot_null<Window::Controller*> window) {\n\twindow->openInMediaViewRequests(\n\t) | rpl::start_to_stream(_openInMediaViewRequests, window->lifetime());\n}\n\nvoid Application::startMediaView() {\n#ifdef Q_OS_MAC\n\t// On macOS we create some windows async, otherwise they're\n\t// added to the Dock Menu as a visible window and are removed\n\t// only after first show and then hide.\n\tInvokeQueued(this, [=] {\n\t\t_mediaView = std::make_unique<Media::View::OverlayWidget>();\n\t});\n#elif defined Q_OS_WIN // Q_OS_MAC || Q_OS_WIN\n\t// On Windows we needed such hack for the main window, otherwise\n\t// somewhere inside the media viewer creating code its geometry\n\t// was broken / lost to some invalid values.\n\tconst auto current = _lastActivePrimaryWindow->widget()->geometry();\n\t_mediaView = std::make_unique<Media::View::OverlayWidget>();\n\t_lastActivePrimaryWindow->widget()->Ui::RpWidget::setGeometry(current);\n#else\n\t_mediaView = std::make_unique<Media::View::OverlayWidget>();\n#endif // Q_OS_MAC || Q_OS_WIN\n}\n\nvoid Application::startTray() {\n#ifdef Q_OS_MAC\n\t// On macOS we create some windows async, otherwise they're\n\t// added to the Dock Menu as a visible window and are removed\n\t// only after first show and then hide, tray icon being \"Item-0\".\n\tInvokeQueued(this, [=] {\n\t\tcreateTray();\n\t});\n#else // Q_OS_MAC\n\tcreateTray();\n#endif // Q_OS_MAC\n}\n\nvoid Application::createTray() {\n\tusing WindowRaw = not_null<Window::Controller*>;\n\t_tray->create();\n\t_tray->aboutToShowRequests(\n\t) | rpl::on_next([=] {\n\t\tenumerateWindows([&](WindowRaw w) { w->updateIsActive(); });\n\t\t_tray->updateMenuText();\n\t}, _lifetime);\n\n\t_tray->showFromTrayRequests(\n\t) | rpl::on_next([=] {\n\t\tactivate();\n\t}, _lifetime);\n\n\t_tray->hideToTrayRequests(\n\t) | rpl::on_next([=] {\n\t\tenumerateWindows([&](WindowRaw w) {\n\t\t\tw->widget()->minimizeToTray();\n\t\t});\n\t}, _lifetime);\n}\n\nvoid Application::activate() {\n\tfor (const auto &window : _windowStack) {\n\t\tif (window == _lastActiveWindow) {\n\t\t\tbreak;\n\t\t}\n\t\tconst auto widget = window->widget();\n\t\tconst auto wasHidden = !widget->isVisible();\n\t\tconst auto state = widget->windowState();\n\t\tif (state & Qt::WindowMinimized) {\n\t\t\twidget->setWindowState(state & ~Qt::WindowMinimized);\n\t\t}\n\t\twidget->setVisible(true);\n\t\twidget->activateWindow();\n\t\tif (wasHidden) {\n\t\t\tif (const auto session = window->sessionController()) {\n\t\t\t\tsession->content()->windowShown();\n\t\t\t}\n\t\t}\n\t}\n\tif (_lastActiveWindow) {\n\t\t_lastActiveWindow->widget()->showFromTray();\n\t}\n}\n\nauto Application::prepareEmojiSourceImages()\n-> std::shared_ptr<Ui::Emoji::UniversalImages> {\n\tconst auto &images = Ui::Emoji::SourceImages();\n\tif (settings().largeEmoji()) {\n\t\treturn images;\n\t}\n\tUi::Emoji::ClearSourceImages(images);\n\treturn std::make_shared<Ui::Emoji::UniversalImages>(images->id());\n}\n\nvoid Application::clearEmojiSourceImages() {\n\t_emojiImageLoader.with([](Stickers::EmojiImageLoader &loader) {\n\t\tcrl::on_main([images = loader.releaseImages()]{\n\t\t\tUi::Emoji::ClearSourceImages(images);\n\t\t});\n\t});\n}\n\nbool Application::isActiveForTrayMenu() const {\n\treturn ranges::any_of(ranges::views::values(_windows), [=](\n\t\t\tconst std::unique_ptr<Window::Controller> &controller) {\n\t\treturn controller->widget()->isActiveForTrayMenu();\n\t});\n}\n\nbool Application::hideMediaView() {\n\tif (_mediaView\n\t\t&& _mediaView->isFullScreen()\n\t\t&& !_mediaView->isMinimized()\n\t\t&& !_mediaView->isHidden()) {\n\t\t_mediaView->close();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool Application::eventFilter(QObject *object, QEvent *e) {\n\tswitch (e->type()) {\n\tcase QEvent::KeyPress: {\n\t\tupdateNonIdle();\n\t\t_inAppKeyPressed.fire({});\n\t\tconst auto event = static_cast<QKeyEvent*>(e);\n\t\tif (base::Platform::GlobalShortcuts::IsToggleFullScreenKey(event)\n\t\t\t&& toggleActiveWindowFullScreen()) {\n\t\t\treturn true;\n\t\t} else if (Shortcuts::HandlePossibleChatSwitch(event)) {\n\t\t\treturn true;\n\t\t}\n\t} break;\n\tcase QEvent::MouseButtonPress:\n\tcase QEvent::TouchBegin:\n\tcase QEvent::Wheel: {\n\t\tupdateNonIdle();\n\t} break;\n\n\tcase QEvent::KeyRelease: {\n\t\tconst auto event = static_cast<QKeyEvent*>(e);\n\t\tif (Shortcuts::HandlePossibleChatSwitch(event)) {\n\t\t\treturn true;\n\t\t}\n\t} break;\n\n\tcase QEvent::ShortcutOverride: {\n\t\t// Ctrl+Tab/Ctrl+Shift+Tab chat switch is a special shortcut case,\n\t\t// because it not only does an action on the shortcut activation,\n\t\t// but also keeps the UI visible until you release the Ctrl key.\n\t\tShortcuts::HandlePossibleChatSwitch(static_cast<QKeyEvent*>(e));\n\n\t\t// Handle all the shortcut management manually.\n\t\treturn true;\n\t} break;\n\n\tcase QEvent::Shortcut: {\n\t\tconst auto event = static_cast<QShortcutEvent*>(e);\n\t\tDEBUG_LOG((\"Shortcut event caught: %1\"\n\t\t\t).arg(event->key().toString()));\n\t\tif (Shortcuts::HandleEvent(object, event)) {\n\t\t\treturn true;\n\t\t}\n\t} break;\n\n\tcase QEvent::ApplicationActivate: {\n\t\tif (object == QCoreApplication::instance()) {\n\t\t\tupdateNonIdle();\n\t\t}\n\t} break;\n\n\tcase QEvent::FileOpen: {\n\t\tif (object == QCoreApplication::instance()) {\n\t\t\tif (_urlsToOpen.isEmpty()) {\n\t\t\t\tInvokeQueued(this, [=] {\n\t\t\t\t\tconst auto activateRequired = ranges::any_of(\n\t\t\t\t\t\tranges::views::all(\n\t\t\t\t\t\t\t_urlsToOpen\n\t\t\t\t\t\t ) | ranges::views::transform([](const QUrl &url) {\n\t\t\t\t\t\t\treturn url.toString();\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tStartUrlRequiresActivate);\n\t\t\t\t\tcRefStartUrls() << base::take(_urlsToOpen);\n\t\t\t\t\tcheckStartUrls();\n\t\t\t\t\tif (_lastActivePrimaryWindow && activateRequired) {\n\t\t\t\t\t\t_lastActivePrimaryWindow->activate();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst auto event = static_cast<QFileOpenEvent*>(e);\n\t\t\t_urlsToOpen << event->url().toString(QUrl::FullyEncoded).mid(\n\t\t\t\t0,\n\t\t\t\t8192);\n\t\t}\n\t} break;\n\n\tcase QEvent::ThemeChange: {\n\t\tif (Platform::IsLinux()\n\t\t\t\t&& object == QGuiApplication::allWindows().constFirst()) {\n\t\t\tCore::App().refreshApplicationIcon();\n\t\t\tCore::App().tray().updateIconCounters();\n\t\t}\n\t} break;\n\t}\n\n\tswitch (e->type()) {\n\tcase QEvent::TouchBegin:\n\t\tUi::Integration::Instance().touchCounterIncrement();\n\t\t[[fallthrough]];\n\tcase QEvent::TouchUpdate:\n\tcase QEvent::TouchEnd: {\n\t\t_lastTouchProcessed = object->isWidgetType();\n\t} break;\n\n\tcase QEvent::MouseButtonPress:\n\tcase QEvent::MouseButtonRelease:\n\tcase QEvent::MouseButtonDblClick:\n\tcase QEvent::MouseMove: {\n\t\tconst auto ev = static_cast<QMouseEvent*>(e);\n\t\tif (ev->source() == Qt::MouseEventSynthesizedBySystem) {\n\t\t\tconst auto widget = static_cast<QWidget*>(object);\n\t\t\tif (_lastTouchProcessed\n\t\t\t\t|| (object->isWidgetType()\n\t\t\t\t\t&& widget->testAttribute(Qt::WA_AcceptTouchEvents))) {\n\t\t\t\t_lastMouseIgnored = true;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\t_lastMouseIgnored = false;\n\t} break;\n\n\tcase QEvent::ContextMenu: {\n\t\tconst auto ev = static_cast<QContextMenuEvent*>(e);\n\t\treturn (ev->reason() == QContextMenuEvent::Mouse)\n\t\t\t&& _lastMouseIgnored;\n\t} break;\n\t}\n\n\treturn QObject::eventFilter(object, e);\n}\n\nSettings &Application::settings() {\n\treturn _private->settings;\n}\n\nconst Settings &Application::settings() const {\n\treturn _private->settings;\n}\n\nvoid Application::saveSettingsDelayed(crl::time delay) {\n\tif (_saveSettingsTimer) {\n\t\t_saveSettingsTimer->callOnce(delay);\n\t}\n}\n\nvoid Application::saveSettings() {\n\tLocal::writeSettings();\n}\n\nbool Application::canReadDefaultDownloadPath() const {\n\treturn KSandbox::isInside()\n\t\t? base::CanReadDirectory(\n\t\t\tQStandardPaths::writableLocation(\n\t\t\t\tQStandardPaths::DownloadLocation))\n\t\t: true;\n}\n\nbool Application::canSaveFileWithoutAskingForPath() const {\n\treturn !settings().askDownloadPath();\n}\n\nMTP::Config &Application::fallbackProductionConfig() const {\n\tif (!_fallbackProductionConfig) {\n\t\t_fallbackProductionConfig = std::make_unique<MTP::Config>(\n\t\t\tMTP::Environment::Production);\n\t}\n\treturn *_fallbackProductionConfig;\n}\n\nvoid Application::refreshFallbackProductionConfig(\n\t\tconst MTP::Config &config) {\n\tif (config.environment() == MTP::Environment::Production) {\n\t\t_fallbackProductionConfig = std::make_unique<MTP::Config>(config);\n\t}\n}\n\nvoid Application::constructFallbackProductionConfig(\n\t\tconst QByteArray &serialized) {\n\tif (auto config = MTP::Config::FromSerialized(serialized)) {\n\t\tif (config->environment() == MTP::Environment::Production) {\n\t\t\t_fallbackProductionConfig = std::move(config);\n\t\t}\n\t}\n}\n\nvoid Application::setCurrentProxy(\n\t\tconst MTP::ProxyData &proxy,\n\t\tMTP::ProxyData::Settings settings) {\n\tauto &my = _private->settings.proxy();\n\tconst auto current = [&] {\n\t\treturn my.isEnabled() ? my.selected() : MTP::ProxyData();\n\t};\n\tconst auto was = current();\n\tmy.setSelected(proxy);\n\tmy.setSettings(settings);\n\tconst auto now = current();\n\trefreshGlobalProxy();\n\t_proxyChanges.fire({ was, now });\n\tmy.connectionTypeChangesNotify();\n}\n\nauto Application::proxyChanges() const -> rpl::producer<ProxyChange> {\n\treturn _proxyChanges.events();\n}\n\nvoid Application::badMtprotoConfigurationError() {\n\tif (settings().proxy().isEnabled() && !_badProxyDisableBox) {\n\t\tconst auto disableCallback = [=] {\n\t\t\tsetCurrentProxy(\n\t\t\t\tsettings().proxy().selected(),\n\t\t\t\tMTP::ProxyData::Settings::System);\n\t\t};\n\t\t_badProxyDisableBox = Ui::show(\n\t\t\tUi::MakeInformBox(Lang::Hard::ProxyConfigError()));\n\t\t_badProxyDisableBox->boxClosing(\n\t\t) | rpl::on_next(\n\t\t\tdisableCallback,\n\t\t\t_badProxyDisableBox->lifetime());\n\t}\n}\n\nvoid Application::startLocalStorage() {\n\tUi::GL::DetectLastCheckCrash();\n\tLocal::start();\n\t_saveSettingsTimer.emplace([=] { saveSettings(); });\n\tsettings().saveDelayedRequests() | rpl::on_next([=] {\n\t\tsaveSettingsDelayed();\n\t}, _lifetime);\n}\n\nvoid Application::startEmojiImageLoader() {\n\t_emojiImageLoader.with([\n\t\tsource = prepareEmojiSourceImages(),\n\t\tlarge = settings().largeEmoji()\n\t](Stickers::EmojiImageLoader &loader) mutable {\n\t\tloader.init(std::move(source), large);\n\t});\n\n\tsettings().largeEmojiChanges(\n\t) | rpl::on_next([=](bool large) {\n\t\tif (large) {\n\t\t\t_clearEmojiImageLoaderTimer.cancel();\n\t\t} else {\n\t\t\t_clearEmojiImageLoaderTimer.callOnce(\n\t\t\t\tkClearEmojiImageSourceTimeout);\n\t\t}\n\t}, _lifetime);\n\n\tUi::Emoji::Updated(\n\t) | rpl::on_next([=] {\n\t\t_emojiImageLoader.with([\n\t\t\tsource = prepareEmojiSourceImages()\n\t\t](Stickers::EmojiImageLoader &loader) mutable {\n\t\t\tloader.switchTo(std::move(source));\n\t\t});\n\t}, _lifetime);\n}\n\nvoid Application::setScreenIsLocked(bool locked) {\n\t_screenIsLocked = locked;\n}\n\nbool Application::screenIsLocked() const {\n\treturn _screenIsLocked;\n}\n\nvoid Application::floatPlayerToggleGifsPaused(bool paused) {\n\t_floatPlayerGifsPaused = paused;\n\tif (_lastActiveWindow) {\n\t\tif (const auto delegate = _lastActiveWindow->floatPlayerDelegate()) {\n\t\t\tdelegate->floatPlayerToggleGifsPaused(paused);\n\t\t}\n\t}\n}\n\nrpl::producer<FullMsgId> Application::floatPlayerClosed() const {\n\tExpects(_floatPlayers != nullptr);\n\n\treturn _floatPlayers->closeEvents();\n}\n\nvoid Application::logout(Main::Account *account) {\n\tif (account) {\n\t\taccount->logOut();\n\t} else {\n\t\t_domain->resetWithForgottenPasscode();\n\t}\n}\n\nvoid Application::logoutWithChecks(Main::Account *account) {\n\tconst auto weak = base::make_weak(account);\n\tconst auto retry = [=] {\n\t\tif (const auto account = weak.get()) {\n\t\t\tlogoutWithChecks(account);\n\t\t}\n\t};\n\tif (!account || !account->sessionExists()) {\n\t\tlogout(account);\n\t} else if (_exportManager->inProgress(&account->session())) {\n\t\t_exportManager->stopWithConfirmation(retry);\n\t} else if (account->session().uploadsInProgress()) {\n\t\taccount->session().uploadsStopWithConfirmation(retry);\n\t} else if (_downloadManager->loadingInProgress(&account->session())) {\n\t\t_downloadManager->loadingStopWithConfirmation(\n\t\t\tretry,\n\t\t\t&account->session());\n\t} else {\n\t\tlogout(account);\n\t}\n}\n\nvoid Application::forceLogOut(\n\t\tnot_null<Main::Account*> account,\n\t\tconst TextWithEntities &explanation) {\n\tconst auto box = Ui::show(Ui::MakeConfirmBox({\n\t\t.text = explanation,\n\t\t.confirmText = tr::lng_passcode_logout(tr::now),\n\t\t.inform = true,\n\t}));\n\tbox->setCloseByEscape(false);\n\tbox->setCloseByOutsideClick(false);\n\tconst auto weak = base::make_weak(account);\n\tconnect(box.get(), &QObject::destroyed, [=] {\n\t\tcrl::on_main(weak, [=] {\n\t\t\taccount->forcedLogOut();\n\t\t});\n\t});\n}\n\nvoid Application::checkLocalTime() {\n\tconst auto adjusted = crl::adjust_time();\n\tif (adjusted) {\n\t\tbase::Timer::Adjust();\n\t\tbase::ConcurrentTimerEnvironment::Adjust();\n\t\tbase::unixtime::http_invalidate();\n\t}\n\tif (const auto session = maybePrimarySession()) {\n\t\tsession->updates().checkLastUpdate(adjusted);\n\t}\n}\n\nvoid Application::handleAppActivated() {\n\tcheckLocalTime();\n\tif (_lastActiveWindow) {\n\t\t_lastActiveWindow->updateIsActiveFocus();\n\t}\n}\n\nvoid Application::handleAppDeactivated() {\n\tenumerateWindows([&](not_null<Window::Controller*> w) {\n\t\tw->updateIsActiveBlur();\n\t});\n\tconst auto session = _lastActiveWindow\n\t\t? _lastActiveWindow->maybeSession()\n\t\t: nullptr;\n\tif (session) {\n\t\tsession->updates().updateOnline();\n\t}\n\tUi::Tooltip::Hide();\n}\n\nrpl::producer<bool> Application::appDeactivatedValue() const {\n\tconst auto &app\n\t\t= static_cast<QGuiApplication*>(QCoreApplication::instance());\n\treturn rpl::single(\n\t\tapp->applicationState()\n\t) | rpl::then(\n\t\tbase::qt_signal_producer(\n\t\t\tapp,\n\t\t\t&QGuiApplication::applicationStateChanged\n\t)) | rpl::map([=](Qt::ApplicationState state) {\n\t\treturn (state != Qt::ApplicationActive);\n\t});\n}\n\nvoid Application::materializeLocalDrafts() {\n\t_materializeLocalDraftsRequests.fire({});\n}\n\nrpl::producer<> Application::materializeLocalDraftsRequests() const {\n\treturn _materializeLocalDraftsRequests.events();\n}\n\nvoid Application::switchDebugMode() {\n\tif (Logs::DebugEnabled()) {\n\t\tLogs::SetDebugEnabled(false);\n\t\tLauncher::Instance().writeDebugModeSetting();\n\t\tRestart();\n\t} else {\n\t\tLogs::SetDebugEnabled(true);\n\t\tLauncher::Instance().writeDebugModeSetting();\n\t\tDEBUG_LOG((\"Debug logs started.\"));\n\t\tif (_lastActivePrimaryWindow) {\n\t\t\t_lastActivePrimaryWindow->hideLayer();\n\t\t}\n\t}\n}\n\nMain::Account &Application::activeAccount() const {\n\treturn _domain->active();\n}\n\nMain::Session *Application::maybePrimarySession() const {\n\treturn _domain->started() ? activeAccount().maybeSession() : nullptr;\n}\n\nbool Application::exportPreventsQuit() {\n\tif (_exportManager->inProgress()) {\n\t\t_exportManager->stopWithConfirmation([] {\n\t\t\tQuit();\n\t\t});\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool Application::uploadPreventsQuit() {\n\tif (!_domain->started()) {\n\t\treturn false;\n\t}\n\tfor (const auto &[index, account] : _domain->accounts()) {\n\t\tif (!account->sessionExists()) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (account->session().uploadsInProgress()) {\n\t\t\taccount->session().uploadsStopWithConfirmation([=] {\n\t\t\t\tfor (const auto &[index, account] : _domain->accounts()) {\n\t\t\t\t\tif (account->sessionExists()) {\n\t\t\t\t\t\taccount->session().uploadsStop();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tQuit();\n\t\t\t});\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool Application::downloadPreventsQuit() {\n\tif (_downloadManager->loadingInProgress()) {\n\t\t_downloadManager->loadingStopWithConfirmation([=] { Quit(); });\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool Application::preventsQuit(QuitReason reason) {\n\tif (exportPreventsQuit()\n\t\t|| uploadPreventsQuit()\n\t\t|| downloadPreventsQuit()) {\n\t\treturn true;\n\t} else if ((!_mediaView\n\t\t|| _mediaView->isHidden()\n\t\t|| !_mediaView->isFullScreen())\n\t\t&& Platform::PreventsQuit(reason)) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nint Application::unreadBadge() const {\n\treturn _domain->unreadBadge();\n}\n\nbool Application::unreadBadgeMuted() const {\n\treturn _domain->unreadBadgeMuted();\n}\n\nrpl::producer<> Application::unreadBadgeChanges() const {\n\treturn _domain->unreadBadgeChanges();\n}\n\nbool Application::offerLegacyLangPackSwitch() const {\n\treturn (_domain->accounts().size() == 1)\n\t\t&& activeAccount().sessionExists();\n}\n\nbool Application::canApplyLangPackWithoutRestart() const {\n\tfor (const auto &[index, account] : _domain->accounts()) {\n\t\tif (account->sessionExists()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid Application::checkStartUrls() {\n\tif (!Core::App().passcodeLocked()) {\n\t\tcRefStartUrls() = ranges::views::all(\n\t\t\tcRefStartUrls()\n\t\t) | ranges::views::filter([&](const QUrl &url) {\n\t\t\tif (url.scheme() == u\"tonsite\"_q) {\n\t\t\t\tiv().showTonSite(url.toString(), {});\n\t\t\t\treturn false;\n\t\t\t} else if (_lastActivePrimaryWindow) {\n\t\t\t\tconst auto local = TryConvertUrlToLocal(url.toString());\n\t\t\t\treturn !openLocalUrl(local, {});\n\t\t\t}\n\t\t\treturn true;\n\t\t}) | ranges::to<QList<QUrl>>;\n\t}\n\tif (!cRefStartUrls().isEmpty()\n\t\t&& _lastActivePrimaryWindow\n\t\t&& !_lastActivePrimaryWindow->locked()) {\n\t\t_lastActivePrimaryWindow->widget()->sendPaths();\n\t}\n}\n\nbool Application::openLocalUrl(const QString &url, QVariant context) {\n\tconst auto urlTrimmed = url.trimmed();\n\tconst auto protocol = u\"tg://\"_q;\n\tif (urlTrimmed.startsWith(protocol, Qt::CaseInsensitive)\n\t\t&& !passcodeLocked()) {\n\t\tconst auto command = urlTrimmed.mid(protocol.size());\n\t\tconst auto my = context.value<ClickHandlerContext>();\n\t\tconst auto controller = my.sessionWindow.get()\n\t\t\t? my.sessionWindow.get()\n\t\t\t: _lastActivePrimaryWindow\n\t\t\t? _lastActivePrimaryWindow->sessionController()\n\t\t\t: nullptr;\n\t\tif (TryRouterForLocalUrl(controller, command)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn openCustomUrl(\"tg://\", LocalUrlHandlers(), url, context);\n}\n\nbool Application::openInternalUrl(const QString &url, QVariant context) {\n\treturn openCustomUrl(\"internal:\", InternalUrlHandlers(), url, context);\n}\n\nQString Application::changelogLink() const {\n\treturn u\"https://telegramdesktop.github.io/tdesktop/changelog/\"_q;\n}\n\nbool Application::openCustomUrl(\n\t\tconst QString &protocol,\n\t\tconst std::vector<LocalUrlHandler> &handlers,\n\t\tconst QString &url,\n\t\tconst QVariant &context) {\n\tconst auto urlTrimmed = url.trimmed();\n\tif (!urlTrimmed.startsWith(protocol, Qt::CaseInsensitive)\n\t\t|| passcodeLocked()) {\n\t\treturn false;\n\t}\n\tstatic const auto kTagExp = QRegularExpression(\n\t\tu\"^\\\\~[a-zA-Z0-9_\\\\-]+\\\\~:\"_q);\n\tauto skip = protocol.size();\n\tconst auto match = kTagExp.match(base::StringViewMid(urlTrimmed, skip));\n\tif (match.hasMatch()) {\n\t\tskip += match.capturedLength();\n\t}\n\tconst auto command = base::StringViewMid(urlTrimmed, skip, 8192);\n\tconst auto my = context.value<ClickHandlerContext>();\n\tconst auto controller = my.sessionWindow.get()\n\t\t? my.sessionWindow.get()\n\t\t: _lastActivePrimaryWindow\n\t\t? _lastActivePrimaryWindow->sessionController()\n\t\t: nullptr;\n\n\tusing namespace qthelp;\n\tconst auto options = RegExOption::CaseInsensitive;\n\tfor (const auto &[expression, handler] : handlers) {\n\t\tconst auto match = regex_match(expression, command, options);\n\t\tif (match) {\n\t\t\treturn handler(controller, match, context);\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Application::preventOrInvoke(Fn<void()> &&callback) {\n\t_lastActivePrimaryWindow->preventOrInvoke(std::move(callback));\n}\n\nvoid Application::updateWindowTitles() {\n\tenumerateWindows([](not_null<Window::Controller*> window) {\n\t\twindow->widget()->updateTitle();\n\t});\n}\n\nvoid Application::lockByPasscode() {\n\t_passcodeLock = true;\n\tenumerateWindows([&](not_null<Window::Controller*> w) {\n\t\tw->setupPasscodeLock();\n\t});\n\tif (_mediaView) {\n\t\t_mediaView->close();\n\t}\n}\n\nvoid Application::maybeLockByPasscode() {\n\tpreventOrInvoke([=] {\n\t\tlockByPasscode();\n\t});\n}\n\nvoid Application::unlockPasscode() {\n\tclearPasscodeLock();\n\tenumerateWindows([&](not_null<Window::Controller*> w) {\n\t\tw->clearPasscodeLock();\n\t});\n}\n\nvoid Application::clearPasscodeLock() {\n\tcSetPasscodeBadTries(0);\n\t_passcodeLock = false;\n}\n\nbool Application::passcodeLocked() const {\n\treturn _passcodeLock.current();\n}\n\nvoid Application::updateNonIdle() {\n\t_lastNonIdleTime = crl::now();\n\tif (const auto session = maybePrimarySession()) {\n\t\tsession->updates().checkIdleFinish(_lastNonIdleTime);\n\t}\n}\n\ncrl::time Application::lastNonIdleTime() const {\n\treturn std::max(\n\t\tbase::Platform::LastUserInputTime().value_or(0),\n\t\t_lastNonIdleTime);\n}\n\nrpl::producer<> Application::inAppKeyPressed() const {\n\treturn _inAppKeyPressed.events();\n}\n\nrpl::producer<bool> Application::passcodeLockChanges() const {\n\treturn _passcodeLock.changes();\n}\n\nrpl::producer<bool> Application::passcodeLockValue() const {\n\treturn _passcodeLock.value();\n}\n\nvoid Application::lockBySetupEmail() {\n\t_setupEmailLock = true;\n\tenumerateWindows([&](not_null<Window::Controller*> w) {\n\t\tw->setupSetupEmailLock();\n\t});\n}\n\nvoid Application::unlockSetupEmail() {\n\t_setupEmailLock = false;\n\tenumerateWindows([&](not_null<Window::Controller*> w) {\n\t\tw->clearSetupEmailLock();\n\t});\n}\n\nbool Application::someSessionExists() const {\n\tfor (const auto &[index, account] : _domain->accounts()) {\n\t\tif (account->sessionExists()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Application::checkAutoLock(crl::time lastNonIdleTime) {\n\tif (!_domain->local().hasLocalPasscode()\n\t\t|| passcodeLocked()\n\t\t|| !someSessionExists()) {\n\t\t_shouldLockAt = 0;\n\t\t_autoLockTimer.cancel();\n\t\treturn;\n\t} else if (!lastNonIdleTime) {\n\t\tlastNonIdleTime = this->lastNonIdleTime();\n\t}\n\n\tcheckLocalTime();\n\tconst auto now = crl::now();\n\tconst auto shouldLockInMs = settings().autoLock() * 1000LL;\n\tconst auto checkTimeMs = now - lastNonIdleTime;\n\tif (checkTimeMs >= shouldLockInMs\n\t\t|| (_shouldLockAt > 0\n\t\t\t&& now > _shouldLockAt + kAutoLockTimeoutLateMs)) {\n\t\t_shouldLockAt = 0;\n\t\t_autoLockTimer.cancel();\n\t\tlockByPasscode();\n\t} else {\n\t\t_shouldLockAt = now + (shouldLockInMs - checkTimeMs);\n\t\t_autoLockTimer.callOnce(shouldLockInMs - checkTimeMs);\n\t}\n}\n\nvoid Application::checkAutoLockIn(crl::time time) {\n\tif (_autoLockTimer.isActive()) {\n\t\tauto remain = _autoLockTimer.remainingTime();\n\t\tif (remain > 0 && remain <= time) return;\n\t}\n\t_autoLockTimer.callOnce(time);\n}\n\nvoid Application::localPasscodeChanged() {\n\t_shouldLockAt = 0;\n\t_autoLockTimer.cancel();\n\tcheckAutoLock(crl::now());\n}\n\nbool Application::savingPositionFor(\n\t\tnot_null<Window::Controller*> window) const {\n\treturn !_windowInSettings || (_windowInSettings == window);\n}\n\nbool Application::hasActiveWindow(not_null<Main::Session*> session) const {\n\tif (Quitting() || !_lastActiveWindow) {\n\t\treturn false;\n\t} else if (_calls->hasActivePanel(session)) {\n\t\treturn true;\n\t} else if (_iv->hasActiveWindow(session)) {\n\t\treturn true;\n\t} else if (const auto window = _lastActiveWindow) {\n\t\treturn (window->account().maybeSession() == session)\n\t\t\t&& window->widget()->isActive();\n\t}\n\treturn false;\n}\n\nWindow::Controller *Application::activePrimaryWindow() const {\n\treturn _lastActivePrimaryWindow;\n}\n\nWindow::Controller *Application::separateWindowFor(\n\t\tWindow::SeparateId id) const {\n\tfor (const auto &[existingId, window] : _windows) {\n\t\tif (existingId == id) {\n\t\t\treturn window.get();\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nWindow::Controller *Application::ensureSeparateWindowFor(\n\t\tWindow::SeparateId id,\n\t\tMsgId showAtMsgId) {\n\tconst auto activate = [&](not_null<Window::Controller*> window) {\n\t\twindow->activate();\n\t\treturn window;\n\t};\n\tif (const auto existing = separateWindowFor(id)) {\n\t\tif (id.thread && id.type == Window::SeparateType::Chat) {\n\t\t\texisting->sessionController()->showThread(\n\t\t\t\tid.thread,\n\t\t\t\tshowAtMsgId,\n\t\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\t}\n\t\treturn activate(existing);\n\t}\n\n\tconst auto result = _windows.emplace(\n\t\tid,\n\t\tstd::make_unique<Window::Controller>(id, showAtMsgId)\n\t).first->second.get();\n\tprocessCreatedWindow(result);\n\tresult->firstShow();\n\tresult->finishFirstShow();\n\treturn activate(result);\n}\n\nWindow::Controller *Application::windowFor(Window::SeparateId id) const {\n\tif (const auto separate = separateWindowFor(id)) {\n\t\treturn separate;\n\t} else if (id && !id.primary()) {\n\t\treturn windowFor(not_null(id.account));\n\t}\n\treturn activePrimaryWindow();\n}\n\nWindow::Controller *Application::windowForShowingHistory(\n\t\tnot_null<PeerData*> peer) const {\n\tif (const auto separate = separateWindowFor(peer)) {\n\t\treturn separate;\n\t}\n\tauto result = (Window::Controller*)nullptr;\n\tenumerateWindows([&](not_null<Window::Controller*> window) {\n\t\tif (const auto controller = window->sessionController()) {\n\t\t\tconst auto current = controller->activeChatCurrent();\n\t\t\tif (const auto history = current.history()) {\n\t\t\t\tif (history->peer == peer) {\n\t\t\t\t\tresult = window;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\treturn result;\n}\n\nWindow::Controller *Application::windowForShowingForum(\n\t\tnot_null<Data::Forum*> forum) const {\n\tconst auto tabs = forum->bot() || forum->peer()->useSubsectionTabs();\n\tconst auto id = Window::SeparateId(\n\t\ttabs ? Window::SeparateType::Chat : Window::SeparateType::Forum,\n\t\tforum->history());\n\tif (const auto separate = separateWindowFor(id)) {\n\t\treturn separate;\n\t}\n\tauto result = (Window::Controller*)nullptr;\n\tenumerateWindows([&](not_null<Window::Controller*> window) {\n\t\tif (const auto controller = window->sessionController()) {\n\t\t\tif (tabs) {\n\t\t\t\tif (controller->windowId() == id) {\n\t\t\t\t\tresult = window;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst auto current = controller->shownForum().current();\n\t\t\t\tif (forum == current) {\n\t\t\t\t\tresult = window;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\treturn result;\n}\n\nWindow::Controller *Application::findWindow(\n\t\tnot_null<QWidget*> widget) const {\n\tconst auto window = widget->window();\n\tif (_lastActiveWindow && _lastActiveWindow->widget() == window) {\n\t\treturn _lastActiveWindow;\n\t}\n\tfor (const auto &[id, controller] : _windows) {\n\t\tif (controller->widget() == window) {\n\t\t\treturn controller.get();\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nWindow::Controller *Application::activeWindow() const {\n\treturn _lastActiveWindow;\n}\n\nbool Application::closeNonLastAsync(not_null<Window::Controller*> window) {\n\tconst auto hasOther = [&] {\n\t\tfor (const auto &[id, controller] : _windows) {\n\t\t\tif (id.primary()\n\t\t\t\t&& !_closingAsyncWindows.contains(controller.get())\n\t\t\t\t&& controller.get() != window\n\t\t\t\t&& controller->maybeSession()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}();\n\tif (!hasOther) {\n\t\treturn false;\n\t}\n\t_closingAsyncWindows.emplace(window);\n\tcrl::on_main(window, [=] { closeWindow(window); });\n\treturn true;\n}\n\nvoid Application::setLastActiveWindow(Window::Controller *window) {\n\t_floatPlayerDelegateLifetime.destroy();\n\n\tif (_floatPlayerGifsPaused && _lastActiveWindow) {\n\t\tif (const auto delegate = _lastActiveWindow->floatPlayerDelegate()) {\n\t\t\tdelegate->floatPlayerToggleGifsPaused(false);\n\t\t}\n\t}\n\t_lastActiveWindow = window;\n\tif (window) {\n\t\tconst auto i = ranges::find(_windowStack, not_null(window));\n\t\tif (i == end(_windowStack)) {\n\t\t\t_windowStack.push_back(window);\n\t\t} else if (i + 1 != end(_windowStack)) {\n\t\t\tstd::rotate(i, i + 1, end(_windowStack));\n\t\t}\n\t}\n\tif (!window) {\n\t\t_floatPlayers = nullptr;\n\t\treturn;\n\t}\n\twindow->floatPlayerDelegateValue(\n\t) | rpl::on_next([=](Media::Player::FloatDelegate *value) {\n\t\tif (!value) {\n\t\t\t_floatPlayers = nullptr;\n\t\t} else if (_floatPlayers) {\n\t\t\t_floatPlayers->replaceDelegate(value);\n\t\t} else if (value) {\n\t\t\t_floatPlayers = std::make_unique<Media::Player::FloatController>(\n\t\t\t\tvalue);\n\t\t}\n\t\tif (value && _floatPlayerGifsPaused) {\n\t\t\tvalue->floatPlayerToggleGifsPaused(true);\n\t\t}\n\t}, _floatPlayerDelegateLifetime);\n}\n\nvoid Application::closeWindow(not_null<Window::Controller*> window) {\n\tconst auto stackIt = ranges::find(_windowStack, window);\n\tconst auto nextFromStack = _windowStack.empty()\n\t\t? nullptr\n\t\t: (stackIt == end(_windowStack) || stackIt + 1 != end(_windowStack))\n\t\t? _windowStack.back().get()\n\t\t: (_windowStack.size() > 1)\n\t\t? (stackIt - 1)->get()\n\t\t: nullptr;\n\tconst auto next = nextFromStack\n\t\t? nextFromStack\n\t\t: (_windows.front().second.get() != window)\n\t\t? _windows.front().second.get()\n\t\t: (_windows.back().second.get() != window)\n\t\t? _windows.back().second.get()\n\t\t: nullptr;\n\tAssert(next != window);\n\n\tif (_lastActivePrimaryWindow == window) {\n\t\t_lastActivePrimaryWindow = next;\n\t}\n\tif (_windowInSettings == window) {\n\t\t_windowInSettings = next;\n\t}\n\tif (stackIt != end(_windowStack)) {\n\t\t_windowStack.erase(stackIt);\n\t}\n\tif (_lastActiveWindow == window) {\n\t\tsetLastActiveWindow(next);\n\t\tif (_lastActiveWindow) {\n\t\t\t_lastActiveWindow->activate();\n\t\t\t_lastActiveWindow->widget()->updateGlobalMenu();\n\t\t}\n\t}\n\t_closingAsyncWindows.remove(window);\n\tfor (auto i = begin(_windows); i != end(_windows);) {\n\t\tif (i->second.get() == window) {\n\t\t\tAssert(_lastActiveWindow != window);\n\t\t\tAssert(_lastActivePrimaryWindow != window);\n\t\t\tAssert(_windowInSettings != window);\n\t\t\ti = _windows.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tconst auto account = domain().started()\n\t\t? &domain().active()\n\t\t: nullptr;\n\tif (account\n\t\t&& !_windows.contains(Window::SeparateId(account))\n\t\t&& _lastActiveWindow) {\n\t\tdomain().activate(&_lastActiveWindow->account());\n\t}\n}\n\nvoid Application::closeChatFromWindows(not_null<PeerData*> peer) {\n\tconst auto closeOne = [&] {\n\t\tfor (const auto &[id, window] : _windows) {\n\t\t\tif (id.thread && id.thread->peer() == peer) {\n\t\t\t\tcloseWindow(window.get());\n\t\t\t\treturn true;\n\t\t\t} else if (const auto controller = window->sessionController()) {\n\t\t\t\tif (controller->activeChatCurrent().peer() == peer) {\n\t\t\t\t\tcontroller->showByInitialId();\n\t\t\t\t}\n\t\t\t\tif (const auto forum = controller->shownForum().current()) {\n\t\t\t\t\tif (peer->forum() == forum) {\n\t\t\t\t\t\tcontroller->closeForum();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\n\twhile (closeOne()) {\n\t}\n}\n\nvoid Application::windowActivated(not_null<Window::Controller*> window) {\n\tconst auto was = _lastActiveWindow;\n\tconst auto now = window;\n\n\tsetLastActiveWindow(window);\n\n\tif (window->isPrimary()) {\n\t\t_lastActivePrimaryWindow = window;\n\t}\n\twindow->widget()->updateGlobalMenu();\n\n\tconst auto wasSession = was ? was->maybeSession() : nullptr;\n\tconst auto nowSession = now->maybeSession();\n\tif (wasSession != nowSession) {\n\t\tif (wasSession) {\n\t\t\twasSession->updates().updateOnline();\n\t\t}\n\t\tif (nowSession) {\n\t\t\tnowSession->updates().updateOnline();\n\t\t}\n\t}\n\tif (_mediaView && _mediaView->takeFocusFrom(now->widget())) {\n\t\t_mediaView->activate();\n\t}\n}\n\nbool Application::closeActiveWindow() {\n\tif (_mediaView && _mediaView->isActive()) {\n\t\t_mediaView->close();\n\t\treturn true;\n\t} else if (_iv->closeActive() || calls().closeCurrentActiveCall()) {\n\t\treturn true;\n\t} else if (const auto window = activeWindow()) {\n\t\tif (window->widget()->isActive()) {\n\t\t\twindow->close();\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool Application::minimizeActiveWindow() {\n\tif (_mediaView && _mediaView->isActive()) {\n\t\t_mediaView->minimize();\n\t\treturn true;\n\t} else if (_iv->minimizeActive()\n\t\t|| calls().minimizeCurrentActiveCall()) {\n\t\treturn true;\n\t} else {\n\t\tif (const auto window = activeWindow()) {\n\t\t\twindow->minimize();\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool Application::toggleActiveWindowFullScreen() {\n\tif (_mediaView && _mediaView->isActive()) {\n\t\t_mediaView->toggleFullScreen();\n\t\treturn true;\n\t} else if (calls().toggleFullScreenCurrentActiveCall()) {\n\t\treturn true;\n\t} else if (const auto window = activeWindow()) {\n\t\tif constexpr (Platform::IsMac()) {\n\t\t\tif (window->widget()->isFullScreen()) {\n\t\t\t\twindow->widget()->showNormal();\n\t\t\t} else {\n\t\t\t\twindow->widget()->showFullScreen();\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nQWidget *Application::getFileDialogParent() {\n\tif (const auto view = _mediaView.get(); view && !view->isHidden()) {\n\t\treturn view->widget();\n\t} else if (const auto active = activeWindow()) {\n\t\treturn active->widget();\n\t}\n\treturn nullptr;\n}\n\nvoid Application::notifyFileDialogShown(bool shown) {\n\tif (_mediaView) {\n\t\t_mediaView->notifyFileDialogShown(shown);\n\t}\n}\n\nQPoint Application::getPointForCallPanelCenter() const {\n\tif (const auto window = activeWindow()) {\n\t\treturn window->getPointForCallPanelCenter();\n\t}\n\treturn QGuiApplication::primaryScreen()->geometry().center();\n}\n\nbool Application::isSharingScreen() const {\n\treturn _calls->isSharingScreen();\n}\n\n// macOS Qt bug workaround, sometimes no leaveEvent() gets to the nested widgets.\nvoid Application::registerLeaveSubscription(not_null<QWidget*> widget) {\n#ifdef Q_OS_MAC\n\tif (const auto window = widget->window()) {\n\t\tauto i = _leaveFilters.find(window);\n\t\tif (i == end(_leaveFilters)) {\n\t\t\tconst auto check = [=](not_null<QEvent*> e) {\n\t\t\t\tif (e->type() == QEvent::Leave) {\n\t\t\t\t\tif (const auto taken = _leaveFilters.take(window)) {\n\t\t\t\t\t\tfor (const auto &weak : taken->registered) {\n\t\t\t\t\t\t\tif (const auto widget = weak.get()) {\n\t\t\t\t\t\t\t\tQEvent ev(QEvent::Leave);\n\t\t\t\t\t\t\t\tQCoreApplication::sendEvent(widget, &ev);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdelete taken->filter.data();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t};\n\t\t\tconst auto filter = base::install_event_filter(window, check);\n\t\t\tQObject::connect(filter, &QObject::destroyed, [=] {\n\t\t\t\t_leaveFilters.remove(window);\n\t\t\t});\n\t\t\ti = _leaveFilters.emplace(\n\t\t\t\twindow,\n\t\t\t\tLeaveFilter{ .filter = filter.get() }).first;\n\t\t}\n\t\ti->second.registered.push_back(widget.get());\n\t}\n#endif // Q_OS_MAC\n}\n\nvoid Application::unregisterLeaveSubscription(not_null<QWidget*> widget) {\n#ifdef Q_OS_MAC\n\tif (const auto topLevel = widget->window()) {\n\t\tconst auto i = _leaveFilters.find(topLevel);\n\t\tif (i != end(_leaveFilters)) {\n\t\t\ti->second.registered = std::move(\n\t\t\t\ti->second.registered\n\t\t\t) | ranges::actions::remove_if([&](QPointer<QWidget> widget) {\n\t\t\t\tconst auto pointer = widget.data();\n\t\t\t\treturn !pointer || (pointer == widget);\n\t\t\t});\n\t\t}\n\t}\n#endif // Q_OS_MAC\n}\n\nvoid Application::postponeCall(FnMut<void()> &&callable) {\n\tSandbox::Instance().postponeCall(std::move(callable));\n}\n\nvoid Application::refreshGlobalProxy() {\n\tSandbox::Instance().refreshGlobalProxy();\n}\n\nvoid QuitAttempt() {\n\tconst auto savingSession = Sandbox::Instance().isSavingSession();\n\tif (!IsAppLaunched()\n\t\t|| savingSession\n\t\t|| App().readyToQuit()) {\n\t\tSandbox::QuitWhenStarted();\n\t}\n}\n\nbool Application::readyToQuit() {\n\tauto prevented = false;\n\tif (_calls->isQuitPrevent()) {\n\t\tprevented = true;\n\t}\n\tif (_domain->started()) {\n\t\tfor (const auto &[index, account] : _domain->accounts()) {\n\t\t\tif (const auto session = account->maybeSession()) {\n\t\t\t\tif (session->updates().isQuitPrevent()) {\n\t\t\t\t\tprevented = true;\n\t\t\t\t}\n\t\t\t\tif (session->api().isQuitPrevent()) {\n\t\t\t\t\tprevented = true;\n\t\t\t\t}\n\t\t\t\tif (session->data().stories().isQuitPrevent()) {\n\t\t\t\t\tprevented = true;\n\t\t\t\t}\n\t\t\t\tif (session->data().reactions().isQuitPrevent()) {\n\t\t\t\t\tprevented = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (prevented) {\n\t\tquitDelayed();\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nvoid Application::quitPreventFinished() {\n\tif (Quitting()) {\n\t\tQuitAttempt();\n\t}\n}\n\nvoid Application::quitDelayed() {\n\tfor (const auto &[id, controller] : _windows) {\n\t\tcontroller->widget()->hide();\n\t}\n\tif (!_private->quitTimer.isActive()) {\n\t\t_private->quitTimer.setCallback([] { Sandbox::QuitWhenStarted(); });\n\t\t_private->quitTimer.callOnce(kQuitPreventTimeoutMs);\n\t}\n}\n\nvoid Application::refreshApplicationIcon() {\n\tconst auto session = (domain().started() && domain().active().sessionExists())\n\t\t? &domain().active().session()\n\t\t: nullptr;\n\trefreshApplicationIcon(session);\n}\n\nvoid Application::refreshApplicationIcon(Main::Session *session) {\n\tconst auto support = session && session->supportMode();\n\tShortcuts::ToggleSupportShortcuts(support);\n\tPlatform::SetApplicationIcon(Window::CreateIcon(\n\t\tsession,\n\t\tPlatform::IsMac()));\n}\n\nvoid Application::startShortcuts() {\n\tShortcuts::Start();\n\n\t_domain->activeSessionChanges(\n\t) | rpl::on_next([=](Main::Session *session) {\n\t\trefreshApplicationIcon(session);\n\t}, _lifetime);\n\n\tShortcuts::Requests(\n\t) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {\n\t\tusing Command = Shortcuts::Command;\n\t\trequest->check(Command::Quit) && request->handle([] {\n\t\t\tQuit();\n\t\t\treturn true;\n\t\t});\n\t\trequest->check(Command::Lock) && request->handle([=] {\n\t\t\tif (!passcodeLocked() && _domain->local().hasLocalPasscode()) {\n\t\t\t\tmaybeLockByPasscode();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t\trequest->check(Command::Minimize) && request->handle([=] {\n\t\t\treturn minimizeActiveWindow();\n\t\t});\n\t\trequest->check(Command::Close) && request->handle([=] {\n\t\t\treturn closeActiveWindow();\n\t\t});\n\t}, _lifetime);\n}\n\nvoid Application::RegisterUrlScheme() {\n\tconst auto arguments = Launcher::Instance().customWorkingDir()\n\t\t? u\"-workdir \\\"%1\\\"\"_q.arg(cWorkingDir())\n\t\t: QString();\n\n\tbase::Platform::RegisterUrlScheme(base::Platform::UrlSchemeDescriptor{\n\t\t.executable = Platform::ExecutablePathForShortcuts(),\n\t\t.arguments = arguments,\n\t\t.protocol = u\"tg\"_q,\n\t\t.protocolName = u\"Telegram Link\"_q,\n\t\t.shortAppName = u\"tdesktop\"_q,\n\t\t.longAppName = QCoreApplication::applicationName(),\n\t\t.displayAppName = AppName.utf16(),\n\t\t.displayAppDescription = AppName.utf16(),\n\t});\n\n\tbase::Platform::RegisterUrlScheme(base::Platform::UrlSchemeDescriptor{\n\t\t.executable = Platform::ExecutablePathForShortcuts(),\n\t\t.arguments = arguments,\n\t\t.protocol = u\"tonsite\"_q,\n\t\t.protocolName = u\"TonSite Link\"_q,\n\t\t.shortAppName = u\"tdesktop\"_q,\n\t\t.longAppName = QCoreApplication::applicationName(),\n\t\t.displayAppName = AppName.utf16(),\n\t\t.displayAppDescription = AppName.utf16(),\n\t});\n}\n\nbool IsAppLaunched() {\n\treturn (Application::Instance != nullptr);\n}\n\nApplication &App() {\n\tExpects(Application::Instance != nullptr);\n\n\treturn *Application::Instance;\n}\n\nvoid Quit(QuitReason reason) {\n   if (Quitting()) {\n\t   return;\n   } else if (IsAppLaunched() && App().preventsQuit(reason)) {\n\t   return;\n   }\n   SetLaunchState(LaunchState::QuitRequested);\n\n   QuitAttempt();\n}\n\nbool Quitting() {\n   return GlobalLaunchState != LaunchState::Running;\n}\n\nLaunchState CurrentLaunchState() {\n   return GlobalLaunchState;\n}\n\nvoid SetLaunchState(LaunchState state) {\n   GlobalLaunchState = state;\n}\n\nvoid Restart() {\n   const auto updateReady = !UpdaterDisabled()\n\t   && (UpdateChecker().state() == UpdateChecker::State::Ready);\n   if (updateReady) {\n\t   cSetRestartingUpdate(true);\n   } else {\n\t   cSetRestarting(true);\n\t   cSetRestartingToSettings(true);\n   }\n   Quit();\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/application.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"mtproto/mtproto_auth_key.h\"\n#include \"mtproto/mtproto_proxy_data.h\"\n#include \"window/window_separate_id.h\"\n\nclass History;\n\nnamespace base {\nclass BatterySaving;\n} // namespace base\n\nnamespace Platform {\nclass Integration;\n} // namespace Platform\n\nnamespace Storage {\nclass Databases;\n} // namespace Storage\n\nnamespace Window {\nclass Controller;\n} // namespace Window\n\nnamespace Window::Notifications {\nclass System;\n} // namespace Window::Notifications\n\nnamespace ChatHelpers {\nclass EmojiKeywords;\n} // namespace ChatHelpers\n\nnamespace Main {\nclass Domain;\nclass Account;\nclass Session;\n} // namespace Main\n\nnamespace Iv {\nclass Instance;\nclass DelegateImpl;\n} // namespace Iv\n\nnamespace Ui {\nnamespace Animations {\nclass Manager;\n} // namespace Animations\nnamespace Emoji {\nclass UniversalImages;\n} // namespace Emoji\nclass BoxContent;\n} // namespace Ui\n\nnamespace MTP {\nclass Config;\nclass Instance;\nclass AuthKey;\nusing AuthKeyPtr = std::shared_ptr<AuthKey>;\nusing AuthKeysList = std::vector<AuthKeyPtr>;\n} // namespace MTP\n\nnamespace Media {\nnamespace Audio {\nclass Instance;\n} // namespace Audio\nnamespace View {\nclass OverlayWidget;\nstruct OpenRequest;\n} // namespace View\nnamespace Player {\nclass FloatController;\nclass FloatDelegate;\n} // namespace Player\nclass SystemMediaControlsManager;\n} // namespace Media\n\nnamespace Lang {\nclass Instance;\nclass Translator;\nclass CloudManager;\n} // namespace Lang\n\nnamespace Data {\nstruct CloudTheme;\nclass DownloadManager;\n} // namespace Data\n\nnamespace Stickers {\nclass EmojiImageLoader;\n} // namespace Stickers\n\nnamespace Export {\nclass Manager;\n} // namespace Export\n\nnamespace Calls {\nclass Instance;\n} // namespace Calls\n\nnamespace Webrtc {\nclass Environment;\n} // namespace Webrtc\n\nnamespace Core {\n\nstruct LocalUrlHandler;\nclass Settings;\nclass Tray;\n\nenum class LaunchState {\n\tRunning,\n\tQuitRequested,\n\tQuitProcessed,\n};\n\nenum class QuitReason {\n\tDefault,\n\tQtQuitEvent,\n};\n\nextern const char kOptionSkipUrlSchemeRegister[];\n\nclass Application final : public QObject {\npublic:\n\tstruct ProxyChange {\n\t\tMTP::ProxyData was;\n\t\tMTP::ProxyData now;\n\t};\n\n\tApplication();\n\tApplication(const Application &other) = delete;\n\tApplication &operator=(const Application &other) = delete;\n\t~Application();\n\n\tvoid run();\n\n\t[[nodiscard]] Platform::Integration &platformIntegration() const {\n\t\treturn *_platformIntegration;\n\t}\n\t[[nodiscard]] Ui::Animations::Manager &animationManager() const {\n\t\treturn *_animationsManager;\n\t}\n\t[[nodiscard]] Window::Notifications::System &notifications() const {\n\t\tExpects(_notifications != nullptr);\n\n\t\treturn *_notifications;\n\t}\n\t[[nodiscard]] Data::DownloadManager &downloadManager() const {\n\t\treturn *_downloadManager;\n\t}\n\t[[nodiscard]] Tray &tray() const {\n\t\treturn *_tray;\n\t}\n\t[[nodiscard]] base::BatterySaving &batterySaving() const {\n\t\treturn *_batterySaving;\n\t}\n\n\t// Windows interface.\n\tbool hasActiveWindow(not_null<Main::Session*> session) const;\n\t[[nodiscard]] bool savingPositionFor(\n\t\tnot_null<Window::Controller*> window) const;\n\t[[nodiscard]] Window::Controller *findWindow(\n\t\tnot_null<QWidget*> widget) const;\n\t[[nodiscard]] Window::Controller *activeWindow() const;\n\t[[nodiscard]] Window::Controller *activePrimaryWindow() const;\n\t[[nodiscard]] Window::Controller *separateWindowFor(\n\t\tWindow::SeparateId id) const;\n\tWindow::Controller *ensureSeparateWindowFor(\n\t\tWindow::SeparateId id,\n\t\tMsgId showAtMsgId = 0);\n\t[[nodiscard]] Window::Controller *windowFor( // Doesn't auto-switch.\n\t\tWindow::SeparateId id) const;\n\t[[nodiscard]] Window::Controller *windowForShowingHistory(\n\t\tnot_null<PeerData*> peer) const;\n\t[[nodiscard]] Window::Controller *windowForShowingForum(\n\t\tnot_null<Data::Forum*> forum) const;\n\t[[nodiscard]] bool closeNonLastAsync(\n\t\tnot_null<Window::Controller*> window);\n\tvoid closeWindow(not_null<Window::Controller*> window);\n\tvoid windowActivated(not_null<Window::Controller*> window);\n\tbool closeActiveWindow();\n\tbool minimizeActiveWindow();\n\tbool toggleActiveWindowFullScreen();\n\t[[nodiscard]] QWidget *getFileDialogParent();\n\tvoid notifyFileDialogShown(bool shown);\n\tvoid checkSystemDarkMode();\n\t[[nodiscard]] bool isActiveForTrayMenu() const;\n\tvoid closeChatFromWindows(not_null<PeerData*> peer);\n\tvoid checkWindowId(not_null<Window::Controller*> window);\n\tvoid activate();\n\n\t// Media view interface.\n\tbool hideMediaView();\n\n\t[[nodiscard]] QPoint getPointForCallPanelCenter() const;\n\t[[nodiscard]] bool isSharingScreen() const;\n\n\tvoid startSettingsAndBackground();\n\t[[nodiscard]] Settings &settings();\n\t[[nodiscard]] const Settings &settings() const;\n\tvoid saveSettingsDelayed(crl::time delay = kDefaultSaveDelay);\n\tvoid saveSettings();\n\n\t[[nodiscard]] bool canReadDefaultDownloadPath() const;\n\t[[nodiscard]] bool canSaveFileWithoutAskingForPath() const;\n\n\t// Fallback config and proxy.\n\t[[nodiscard]] MTP::Config &fallbackProductionConfig() const;\n\tvoid refreshFallbackProductionConfig(const MTP::Config &config);\n\tvoid constructFallbackProductionConfig(const QByteArray &serialized);\n\tvoid setCurrentProxy(\n\t\tconst MTP::ProxyData &proxy,\n\t\tMTP::ProxyData::Settings settings);\n\t[[nodiscard]] rpl::producer<ProxyChange> proxyChanges() const;\n\tvoid badMtprotoConfigurationError();\n\n\t// Databases.\n\t[[nodiscard]] Storage::Databases &databases() {\n\t\treturn *_databases;\n\t}\n\n\t// Domain component.\n\t[[nodiscard]] Main::Domain &domain() const {\n\t\treturn *_domain;\n\t}\n\t[[nodiscard]] Main::Account &activeAccount() const;\n\t[[nodiscard]] bool someSessionExists() const;\n\t[[nodiscard]] Export::Manager &exportManager() const {\n\t\treturn *_exportManager;\n\t}\n\t[[nodiscard]] bool exportPreventsQuit();\n\n\t// Main::Session component.\n\tMain::Session *maybePrimarySession() const;\n\t[[nodiscard]] int unreadBadge() const;\n\t[[nodiscard]] bool unreadBadgeMuted() const;\n\t[[nodiscard]] rpl::producer<> unreadBadgeChanges() const;\n\n\t// Media component.\n\t[[nodiscard]] Media::Audio::Instance &audio() {\n\t\treturn *_audio;\n\t}\n\t[[nodiscard]] Webrtc::Environment &mediaDevices() {\n\t\treturn *_mediaDevices;\n\t}\n\n\t// Langpack and emoji keywords.\n\t[[nodiscard]] Lang::Instance &langpack() {\n\t\treturn *_langpack;\n\t}\n\t[[nodiscard]] Lang::CloudManager *langCloudManager() {\n\t\treturn _langCloudManager.get();\n\t}\n\t[[nodiscard]] bool offerLegacyLangPackSwitch() const;\n\t[[nodiscard]] bool canApplyLangPackWithoutRestart() const;\n\t[[nodiscard]] ChatHelpers::EmojiKeywords &emojiKeywords() {\n\t\treturn *_emojiKeywords;\n\t}\n\t[[nodiscard]] auto emojiImageLoader() const\n\t-> const crl::object_on_queue<Stickers::EmojiImageLoader> & {\n\t\treturn _emojiImageLoader;\n\t}\n\n\t// Internal links.\n\tvoid checkStartUrls();\n\tbool openLocalUrl(const QString &url, QVariant context);\n\tbool openInternalUrl(const QString &url, QVariant context);\n\t[[nodiscard]] QString changelogLink() const;\n\n\t// Float player.\n\tvoid floatPlayerToggleGifsPaused(bool paused);\n\t[[nodiscard]] rpl::producer<FullMsgId> floatPlayerClosed() const;\n\n\t// Calls.\n\tCalls::Instance &calls() const {\n\t\treturn *_calls;\n\t}\n\n\t// Iv.\n\tIv::Instance &iv() const {\n\t\treturn *_iv;\n\t}\n\n\tvoid logout(Main::Account *account = nullptr);\n\tvoid logoutWithChecks(Main::Account *account);\n\tvoid forceLogOut(\n\t\tnot_null<Main::Account*> account,\n\t\tconst TextWithEntities &explanation);\n\t[[nodiscard]] bool uploadPreventsQuit();\n\t[[nodiscard]] bool downloadPreventsQuit();\n\tvoid checkLocalTime();\n\tvoid lockByPasscode();\n\tvoid maybeLockByPasscode();\n\tvoid unlockPasscode();\n\t[[nodiscard]] bool passcodeLocked() const;\n\trpl::producer<bool> passcodeLockChanges() const;\n\trpl::producer<bool> passcodeLockValue() const;\n\n\tvoid lockBySetupEmail();\n\tvoid unlockSetupEmail();\n\n\tvoid checkAutoLock(crl::time lastNonIdleTime = 0);\n\tvoid checkAutoLockIn(crl::time time);\n\tvoid localPasscodeChanged();\n\n\t[[nodiscard]] bool preventsQuit(QuitReason reason);\n\n\t[[nodiscard]] crl::time lastNonIdleTime() const;\n\tvoid updateNonIdle();\n\n\tvoid registerLeaveSubscription(not_null<QWidget*> widget);\n\tvoid unregisterLeaveSubscription(not_null<QWidget*> widget);\n\n\t// Sandbox interface.\n\tvoid postponeCall(FnMut<void()> &&callable);\n\tvoid refreshGlobalProxy();\n\tvoid refreshApplicationIcon();\n\n\tvoid quitPreventFinished();\n\n\tvoid handleAppActivated();\n\tvoid handleAppDeactivated();\n\t[[nodiscard]] rpl::producer<bool> appDeactivatedValue() const;\n\t[[nodiscard]] rpl::producer<> inAppKeyPressed() const;\n\n\tvoid materializeLocalDrafts();\n\t[[nodiscard]] rpl::producer<> materializeLocalDraftsRequests() const;\n\n\tvoid switchDebugMode();\n\n\tvoid preventOrInvoke(Fn<void()> &&callback);\n\n\t// Global runtime variables.\n\tvoid setScreenIsLocked(bool locked);\n\tbool screenIsLocked() const;\n\n\tstatic void RegisterUrlScheme();\n\nprotected:\n\tbool eventFilter(QObject *object, QEvent *event) override;\n\nprivate:\n\tstatic constexpr auto kDefaultSaveDelay = crl::time(1000);\n\n\tfriend bool IsAppLaunched();\n\tfriend Application &App();\n\n\tvoid autoRegisterUrlScheme();\n\tvoid clearEmojiSourceImages();\n\t[[nodiscard]] auto prepareEmojiSourceImages()\n\t\t-> std::shared_ptr<Ui::Emoji::UniversalImages>;\n\tvoid startLocalStorage();\n\tvoid startShortcuts();\n\tvoid startDomain();\n\tvoid startEmojiImageLoader();\n\tvoid startSystemDarkModeViewer();\n\tvoid startMediaView();\n\tvoid startTray();\n\n\tvoid createTray();\n\tvoid updateWindowTitles();\n\tvoid setLastActiveWindow(Window::Controller *window);\n\tvoid showAccount(not_null<Main::Account*> account);\n\tvoid enumerateWindows(\n\t\tFn<void(not_null<Window::Controller*>)> callback) const;\n\tvoid processCreatedWindow(not_null<Window::Controller*> window);\n\tvoid refreshApplicationIcon(Main::Session *session);\n\n\tfriend void QuitAttempt();\n\tvoid quitDelayed();\n\t[[nodiscard]] bool readyToQuit();\n\n\tvoid showOpenGLCrashNotification();\n\tvoid clearPasscodeLock();\n\tvoid closeAdditionalWindows();\n\n\tbool openCustomUrl(\n\t\tconst QString &protocol,\n\t\tconst std::vector<LocalUrlHandler> &handlers,\n\t\tconst QString &url,\n\t\tconst QVariant &context);\n\n\tstatic Application *Instance;\n\tstruct InstanceSetter {\n\t\tInstanceSetter(not_null<Application*> instance) {\n\t\t\tExpects(Instance == nullptr);\n\n\t\t\tInstance = instance;\n\t\t}\n\t};\n\tInstanceSetter _setter = { this };\n\n\trpl::event_stream<ProxyChange> _proxyChanges;\n\n\t// Some fields are just moved from the declaration.\n\tstruct Private;\n\tconst std::unique_ptr<Private> _private;\n\tconst std::unique_ptr<Platform::Integration> _platformIntegration;\n\tconst std::unique_ptr<base::BatterySaving> _batterySaving;\n\tconst std::unique_ptr<Webrtc::Environment> _mediaDevices;\n\n\tconst std::unique_ptr<Storage::Databases> _databases;\n\tconst std::unique_ptr<Ui::Animations::Manager> _animationsManager;\n\tcrl::object_on_queue<Stickers::EmojiImageLoader> _emojiImageLoader;\n\tbase::Timer _clearEmojiImageLoaderTimer;\n\tconst std::unique_ptr<Media::Audio::Instance> _audio;\n\tmutable std::unique_ptr<MTP::Config> _fallbackProductionConfig;\n\n\t// Notifications should be destroyed before _audio, after _domain.\n\t// Mutable because is created in run() after OpenSSL is inited.\n\tstd::unique_ptr<Window::Notifications::System> _notifications;\n\n\tusing MediaControlsManager = Media::SystemMediaControlsManager;\n\tstd::unique_ptr<MediaControlsManager> _mediaControlsManager;\n\tconst std::unique_ptr<Data::DownloadManager> _downloadManager;\n\tconst std::unique_ptr<Main::Domain> _domain;\n\tconst std::unique_ptr<Export::Manager> _exportManager;\n\tconst std::unique_ptr<Calls::Instance> _calls;\n\tconst std::unique_ptr<Iv::Instance> _iv;\n\tbase::flat_map<\n\t\tWindow::SeparateId,\n\t\tstd::unique_ptr<Window::Controller>> _windows;\n\tbase::flat_set<not_null<Window::Controller*>> _closingAsyncWindows;\n\tstd::vector<not_null<Window::Controller*>> _windowStack;\n\tWindow::Controller *_lastActiveWindow = nullptr;\n\tWindow::Controller *_lastActivePrimaryWindow = nullptr;\n\tWindow::Controller *_windowInSettings = nullptr;\n\tbool _lastMouseIgnored = false;\n\tbool _lastTouchProcessed = false;\n\n\tstd::unique_ptr<Media::View::OverlayWidget> _mediaView;\n\tconst std::unique_ptr<Lang::Instance> _langpack;\n\tconst std::unique_ptr<Lang::CloudManager> _langCloudManager;\n\tconst std::unique_ptr<ChatHelpers::EmojiKeywords> _emojiKeywords;\n\tstd::unique_ptr<Lang::Translator> _translator;\n\tbase::weak_qptr<Ui::BoxContent> _badProxyDisableBox;\n\n\tconst std::unique_ptr<Tray> _tray;\n\n\tstd::unique_ptr<Media::Player::FloatController> _floatPlayers;\n\trpl::lifetime _floatPlayerDelegateLifetime;\n\tbool _floatPlayerGifsPaused = false;\n\n\trpl::variable<bool> _passcodeLock;\n\trpl::variable<bool> _setupEmailLock;\n\tbool _screenIsLocked = false;\n\n\tcrl::time _shouldLockAt = 0;\n\tbase::Timer _autoLockTimer;\n\n\tQList<QUrl> _urlsToOpen;\n\n\tstd::optional<base::Timer> _saveSettingsTimer;\n\n\tstruct LeaveFilter {\n\t\tstd::vector<QPointer<QWidget>> registered;\n\t\tQPointer<QObject> filter;\n\t};\n\tbase::flat_map<not_null<QWidget*>, LeaveFilter> _leaveFilters;\n\n\trpl::event_stream<Media::View::OpenRequest> _openInMediaViewRequests;\n\trpl::event_stream<> _inAppKeyPressed;\n\n\trpl::event_stream<> _materializeLocalDraftsRequests;\n\n\trpl::lifetime _lifetime;\n\n\tcrl::time _lastNonIdleTime = 0;\n\n};\n\n[[nodiscard]] bool IsAppLaunched();\n[[nodiscard]] Application &App();\n\n[[nodiscard]] LaunchState CurrentLaunchState();\nvoid SetLaunchState(LaunchState state);\n\nvoid Quit(QuitReason reason = QuitReason::Default);\n[[nodiscard]] bool Quitting();\n\nvoid Restart();\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/bank_card_click_handler.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/bank_card_click_handler.h\"\n\n#include \"core/click_handler_types.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"mtproto/sender.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/widgets/menu/menu_multiline_action.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_chat.h\" // popupMenuExpandedSeparator.\n#include \"styles/style_menu_icons.h\"\n\nnamespace {\n\nstruct State final {\n\tState(not_null<Main::Session*> session) : sender(&session->mtp()) {\n\t}\n\tMTP::Sender sender;\n};\n\nstruct BankCardData final {\n\tQString title;\n\tstd::vector<EntityLinkData> links;\n};\n\nenum class Status {\n\tLoading,\n\tResolved,\n\tFailed,\n};\n\nvoid RequestResolveBankCard(\n\t\tnot_null<State*> state,\n\t\tconst QString &bankCard,\n\t\tFn<void(BankCardData)> done,\n\t\tFn<void(QString)> fail) {\n\tstate->sender.request(MTPpayments_GetBankCardData(\n\t\tMTP_string(bankCard)\n\t)).done([=](const MTPpayments_BankCardData &result) {\n\t\tauto bankCardData = BankCardData{\n\t\t\t.title = qs(result.data().vtitle()),\n\t\t};\n\t\tfor (const auto &tl : result.data().vopen_urls().v) {\n\t\t\tconst auto url = qs(tl.data().vurl());\n\t\t\tconst auto name = qs(tl.data().vname());\n\n\t\t\tbankCardData.links.emplace_back(EntityLinkData{\n\t\t\t\t.text = name,\n\t\t\t\t.data = url,\n\t\t\t});\n\t\t}\n\t\tdone(std::move(bankCardData));\n\t}).fail([=](const MTP::Error &error) {\n\t\tfail(error.type());\n\t}).send();\n}\n\nclass ResolveBankCardAction final : public Ui::Menu::ItemBase {\npublic:\n\tResolveBankCardAction(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::Menu &st);\n\n\tvoid setStatus(Status status);\n\n\tbool isEnabled() const override;\n\tnot_null<QAction*> action() const override;\n\nprotected:\n\tint contentHeight() const override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid paint(Painter &p);\n\n\tconst not_null<QAction*> _dummyAction;\n\tconst style::Menu &_st;\n\tconst int _height = 0;\n\tStatus _status = Status::Loading;\n\n\tUi::Text::String _text;\n\n};\n\nResolveBankCardAction::ResolveBankCardAction(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::Menu &st)\n: ItemBase(parent, st)\n, _dummyAction(Ui::CreateChild<QAction>(parent))\n, _st(st)\n, _height(st::groupCallJoinAsPhotoSize) {\n\tsetAcceptBoth(true);\n\tfitToMenuWidth();\n\tsetStatus(Status::Loading);\n}\n\nvoid ResolveBankCardAction::setStatus(Status status) {\n\t_status = status;\n\tif (status == Status::Resolved) {\n\t\tresize(width(), 0);\n\t} else if (status == Status::Failed) {\n\t\t_text.setText(_st.itemStyle, tr::lng_attach_failed(tr::now));\n\t} else if (status == Status::Loading) {\n\t\t_text.setText(_st.itemStyle, tr::lng_contacts_loading(tr::now));\n\t}\n\tupdate();\n}\n\nvoid ResolveBankCardAction::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto selected = false;\n\tconst auto height = contentHeight();\n\tif (selected && _st.itemBgOver->c.alpha() < 255) {\n\t\tp.fillRect(0, 0, width(), height, _st.itemBg);\n\t}\n\tp.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg);\n\n\tconst auto &padding = st::groupCallJoinAsPadding;\n\t{\n\t\tp.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);\n\t\tconst auto w = width() - padding.left() - padding.right();\n\t\t_text.draw(p, Ui::Text::PaintContext{\n\t\t\t.position = QPoint(\n\t\t\t\t(width() - w) / 2,\n\t\t\t\t(height - _text.countHeight(w)) / 2),\n\t\t\t.outerWidth = w,\n\t\t\t.availableWidth = w,\n\t\t\t.align = style::al_center,\n\t\t\t.elisionLines = 2,\n\t\t});\n\t}\n}\n\nbool ResolveBankCardAction::isEnabled() const {\n\treturn false;\n}\n\nnot_null<QAction*> ResolveBankCardAction::action() const {\n\treturn _dummyAction;\n}\n\nint ResolveBankCardAction::contentHeight() const {\n\tif (_status == Status::Resolved) {\n\t\treturn 0;\n\t}\n\treturn _height;\n}\n\n} // namespace\n\nBankCardClickHandler::BankCardClickHandler(\n\tnot_null<Main::Session*> session,\n\tQString text)\n: _session(session)\n, _text(text) {\n}\n\nvoid BankCardClickHandler::onClick(ClickContext context) const {\n\tif (context.button != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tconst auto my = context.other.value<ClickHandlerContext>();\n\tconst auto controller = my.sessionWindow.get();\n\tconst auto pos = QCursor::pos();\n\tif (!controller) {\n\t\treturn;\n\t}\n\tconst auto menu = Ui::CreateChild<Ui::PopupMenu>(\n\t\tcontroller->content(),\n\t\tst::popupMenuWithIcons);\n\n\tconst auto bankCard = _text;\n\n\tconst auto copy = [bankCard, show = controller->uiShow()] {\n\t\tTextUtilities::SetClipboardText(\n\t\t\tTextForMimeData::Simple(bankCard));\n\t\tshow->showToast(tr::lng_context_bank_card_copied(tr::now));\n\t};\n\n\tmenu->addAction(\n\t\ttr::lng_context_bank_card_copy(tr::now),\n\t\tcopy,\n\t\t&st::menuIconCopy);\n\n\tauto resolveBankCardAction = base::make_unique_q<ResolveBankCardAction>(\n\t\tmenu->menu(),\n\t\tmenu->st().menu);\n\tconst auto resolveBankCardRaw = resolveBankCardAction.get();\n\n\tmenu->addSeparator(&st::popupMenuExpandedSeparator.menu.separator);\n\n\tmenu->addAction(std::move(resolveBankCardAction));\n\n\tconst auto addTitle = [=](const QString &name) {\n\t\tauto button = base::make_unique_q<Ui::Menu::MultilineAction>(\n\t\t\tmenu->menu(),\n\t\t\tmenu->st().menu,\n\t\t\tst::historyHasCustomEmoji,\n\t\t\tst::historyBankCardMenuMultilinePosition,\n\t\t\tTextWithEntities{ name });\n\t\tbutton->setActionTriggered(copy);\n\t\tmenu->addAction(std::move(button));\n\t};\n\n\tconst auto state = menu->lifetime().make_state<State>(\n\t\t&controller->session());\n\tRequestResolveBankCard(\n\t\tstate,\n\t\tbankCard,\n\t\t[=](BankCardData data) {\n\t\t\tresolveBankCardRaw->setStatus(Status::Resolved);\n\t\t\tfor (auto &link : data.links) {\n\t\t\t\tmenu->addAction(\n\t\t\t\t\tbase::take(link.text),\n\t\t\t\t\t[u = base::take(link.data)] { UrlClickHandler::Open(u); },\n\t\t\t\t\t&st::menuIconPayment);\n\t\t\t}\n\t\t\tif (!data.title.isEmpty()) {\n\t\t\t\taddTitle(base::take(data.title));\n\t\t\t}\n\t\t},\n\t\t[=](const QString &) {\n\t\t\tresolveBankCardRaw->setStatus(Status::Failed);\n\t\t});\n\n\tmenu->popup(pos);\n}\n\nauto BankCardClickHandler::getTextEntity() const -> TextEntity {\n\treturn { EntityType::BankCard };\n}\n\nQString BankCardClickHandler::tooltip() const {\n\treturn _text;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/core/bank_card_click_handler.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/basic_click_handlers.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nclass BankCardClickHandler : public ClickHandler {\npublic:\n\tBankCardClickHandler(not_null<Main::Session*> session, QString text);\n\n\tvoid onClick(ClickContext context) const override;\n\n\tTextEntity getTextEntity() const override;\n\n\tQString tooltip() const override;\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\tQString _text;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/core/base_integration.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/base_integration.h\"\n\n#include \"core/sandbox.h\"\n#include \"core/crash_reports.h\"\n\nnamespace Core {\n\nBaseIntegration::BaseIntegration(int argc, char *argv[])\n: Integration(argc, argv) {\n}\n\nvoid BaseIntegration::enterFromEventLoop(FnMut<void()> &&method) {\n\tCore::Sandbox::Instance().customEnterFromEventLoop(\n\t\tstd::move(method));\n}\n\nbool BaseIntegration::logSkipDebug() {\n\treturn !Logs::DebugEnabled() && Logs::started();\n}\n\nvoid BaseIntegration::logMessageDebug(const QString &message) {\n\tLogs::writeDebug(message);\n}\n\nvoid BaseIntegration::logMessage(const QString &message) {\n\tLogs::writeMain(message);\n}\n\nvoid BaseIntegration::setCrashAnnotation(\n\t\tconst std::string &key,\n\t\tconst QString &value) {\n\tCrashReports::SetAnnotation(key, value);\n}\n\nvoid BaseIntegration::logAssertionViolation(const QString &info) {\n\tLogs::writeMain(\"Assertion Failed! \" + info);\n\tCrashReports::SetAnnotation(\"Assertion\", info);\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/base_integration.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/integration.h\"\n\nnamespace Core {\n\nclass BaseIntegration final : public base::Integration {\npublic:\n\tBaseIntegration(int argc, char *argv[]);\n\n\tvoid enterFromEventLoop(FnMut<void()> &&method) override;\n\tbool logSkipDebug() override;\n\tvoid logMessageDebug(const QString &message) override;\n\tvoid logMessage(const QString &message) override;\n\tvoid logAssertionViolation(const QString &info) override;\n\tvoid setCrashAnnotation(\n\t\tconst std::string &key,\n\t\tconst QString &value) override;\n\n};\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/cached_webview_availability.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"webview/webview_interface.h\"\n\nnamespace Core {\n\n[[nodiscard]] inline const Webview::Available &CachedWebviewAvailability() {\n\tstatic const auto result = Webview::Availability();\n\treturn result;\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/changelogs.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/changelogs.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"core/application.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"storage/storage_domain.h\"\n#include \"data/data_session.h\"\n#include \"base/qt/qt_common_adapters.h\"\n#include \"mainwindow.h\"\n#include \"apiwrap.h\"\n\nnamespace Core {\nnamespace {\n\nstd::map<int, const char*> BetaLogs() {\n\treturn {\n\t{\n\t\t4008011,\n\t\t\"- Fix initial video playback speed.\\n\"\n\n\t\t\"- Use native window resize on Windows 11.\\n\"\n\n\t\t\"- Fix memory leak in Direct3D 11 media viewer on Windows.\\n\"\n\t},\n\t{\n\t\t4010004,\n\t\t\"- Statistics in channels and group chats.\\n\"\n\n\t\t\"- Nice looking code blocks with syntax highlight.\\n\"\n\n\t\t\"- Copy full code block by click on its header.\\n\"\n\n\t\t\"- Send a highlighted code block using ```language syntax.\\n\"\n\t}\n\t};\n};\n\n} // namespace\n\nChangelogs::Changelogs(not_null<Main::Session*> session, int oldVersion)\n: _session(session)\n, _oldVersion(oldVersion) {\n\t_session->data().chatsListChanges(\n\t) | rpl::filter([](Data::Folder *folder) {\n\t\treturn !folder;\n\t}) | rpl::on_next([=] {\n\t\trequestCloudLogs();\n\t}, _chatsSubscription);\n}\n\nstd::unique_ptr<Changelogs> Changelogs::Create(\n\t\tnot_null<Main::Session*> session) {\n\tauto &local = Core::App().domain().local();\n\tconst auto oldVersion = local.oldVersion();\n\tlocal.clearOldVersion();\n\treturn (oldVersion > 0 && oldVersion < AppVersion)\n\t\t? std::make_unique<Changelogs>(session, oldVersion)\n\t\t: nullptr;\n}\n\nvoid Changelogs::requestCloudLogs() {\n\t_chatsSubscription.destroy();\n\n\tconst auto callback = [this](const MTPUpdates &result) {\n\t\t_session->api().applyUpdates(result);\n\n\t\tauto resultEmpty = true;\n\t\tswitch (result.type()) {\n\t\tcase mtpc_updateShortMessage:\n\t\tcase mtpc_updateShortChatMessage:\n\t\tcase mtpc_updateShort:\n\t\t\tresultEmpty = false;\n\t\t\tbreak;\n\t\tcase mtpc_updatesCombined:\n\t\t\tresultEmpty = result.c_updatesCombined().vupdates().v.isEmpty();\n\t\t\tbreak;\n\t\tcase mtpc_updates:\n\t\t\tresultEmpty = result.c_updates().vupdates().v.isEmpty();\n\t\t\tbreak;\n\t\tcase mtpc_updatesTooLong:\n\t\tcase mtpc_updateShortSentMessage:\n\t\t\tLOG((\"API Error: Bad updates type in app changelog.\"));\n\t\t\tbreak;\n\t\t}\n\t\tif (resultEmpty) {\n\t\t\taddLocalLogs();\n\t\t}\n\t};\n\t_session->api().requestChangelog(\n\t\tFormatVersionPrecise(_oldVersion),\n\t\tcrl::guard(this, callback));\n}\n\nvoid Changelogs::addLocalLogs() {\n\tif (AppBetaVersion || cAlphaVersion()) {\n\t\taddBetaLogs();\n\t}\n\tif (!_addedSomeLocal) {\n\t\tconst auto text = tr::lng_new_version_wrap(\n\t\t\ttr::now,\n\t\t\tlt_version,\n\t\t\tQString::fromLatin1(AppVersionStr),\n\t\t\tlt_changes,\n\t\t\ttr::lng_new_version_minor(tr::now),\n\t\t\tlt_link,\n\t\t\tCore::App().changelogLink());\n\t\taddLocalLog(text.trimmed());\n\t}\n}\n\nvoid Changelogs::addLocalLog(const QString &text) {\n\tauto textWithEntities = TextWithEntities{ text };\n\tTextUtilities::ParseEntities(textWithEntities, TextParseLinks);\n\t_session->data().serviceNotification(textWithEntities);\n\t_addedSomeLocal = true;\n};\n\nvoid Changelogs::addBetaLogs() {\n\tfor (const auto &[version, changes] : BetaLogs()) {\n\t\taddBetaLog(version, changes);\n\t}\n}\n\nvoid Changelogs::addBetaLog(int changeVersion, const char *changes) {\n\tif (_oldVersion >= changeVersion) {\n\t\treturn;\n\t}\n\tconst auto text = [&] {\n\t\tstatic const auto simple = u\"\\n- \"_q;\n\t\tstatic const auto separator = '\\n' + Ui::kQBullet + ' ';\n\t\tauto result = QString::fromUtf8(changes).trimmed();\n\t\tif (result.startsWith(base::StringViewMid(simple, 1))) {\n\t\t\tresult = separator.mid(1) + result.mid(simple.size() - 1);\n\t\t}\n\t\treturn result.replace(simple, separator);\n\t}();\n\tconst auto version = FormatVersionDisplay(changeVersion);\n\tconst auto log = u\"New in version %1 beta:\\n\\n\"_q.arg(version) + text;\n\taddLocalLog(log);\n}\n\nQString FormatVersionDisplay(int version) {\n\treturn QString::number(version / 1000000)\n\t\t+ '.' + QString::number((version % 1000000) / 1000)\n\t\t+ ((version % 1000)\n\t\t\t? ('.' + QString::number(version % 1000))\n\t\t\t: QString());\n}\n\nQString FormatVersionPrecise(int version) {\n\treturn QString::number(version / 1000000)\n\t\t+ '.' + QString::number((version % 1000000) / 1000)\n\t\t+ '.' + QString::number(version % 1000);\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/changelogs.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Core {\n\n[[nodiscard]] QString FormatVersionDisplay(int version);\n[[nodiscard]] QString FormatVersionPrecise(int version);\n\nclass Changelogs final : public base::has_weak_ptr {\npublic:\n\tChangelogs(not_null<Main::Session*> session, int oldVersion);\n\n\tstatic std::unique_ptr<Changelogs> Create(\n\t\tnot_null<Main::Session*> session);\n\nprivate:\n\tvoid requestCloudLogs();\n\tvoid addLocalLogs();\n\tvoid addLocalLog(const QString &text);\n\tvoid addBetaLogs();\n\tvoid addBetaLog(int changeVersion, const char *changes);\n\n\tconst not_null<Main::Session*> _session;\n\tconst int _oldVersion = 0;\n\trpl::lifetime _chatsSubscription;\n\tbool _addedSomeLocal = false;\n\n};\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/click_handler_types.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/click_handler_types.h\"\n\n#include \"base/unixtime.h\"\n#include \"lang/lang_keys.h\"\n#include \"chat_helpers/bot_command.h\"\n#include \"core/application.h\"\n#include \"core/local_url_handlers.h\"\n#include \"core/file_utilities.h\"\n#include \"mainwidget.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"base/qthelp_regex.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"base/random.h\"\n#include \"storage/storage_account.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history_item.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n#include \"data/data_game.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_session_controller_link_info.h\"\n#include \"apiwrap.h\"\n#include \"history/view/history_view_schedule_box.h\"\n#include \"history/view/history_view_scheduled_section.h\"\n#include \"menu/menu_send.h\"\n#include \"data/data_types.h\"\n#include \"styles/style_calls.h\" // groupCallBoxLabel\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtCore/QDateTime>\n#include <QtCore/QLocale>\n\nnamespace {\n\nconstexpr auto kReminderSetToastDuration = 4 * crl::time(1000);\n\n[[nodiscard]] TextWithEntities BoldDomainInUrl(const QString &url) {\n\tauto result = TextWithEntities{ .text = url };\n\n\tif (const auto parsedUrl = QUrl(url); parsedUrl.isValid()) {\n\t\tif (const auto host = parsedUrl.host(); !host.isEmpty()) {\n\t\t\tif (const auto hostPos = url.indexOf(host); hostPos != -1) {\n\t\t\t\tauto boldEntity = EntityInText(\n\t\t\t\t\tEntityType::Bold,\n\t\t\t\t\thostPos,\n\t\t\t\t\thost.length());\n\n\t\t\t\tif (host.startsWith(\"www.\")) {\n\t\t\t\t\tboldEntity = EntityInText(\n\t\t\t\t\t\tEntityType::Bold,\n\t\t\t\t\t\thostPos + 4,\n\t\t\t\t\t\thost.length() - 4);\n\t\t\t\t}\n\n\t\t\t\tresult.entities.push_back(boldEntity);\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\n// Possible context owners: media viewer, profile, history widget.\n\nvoid SearchByHashtag(ClickContext context, const QString &tag) {\n\tconst auto my = context.other.value<ClickHandlerContext>();\n\tif (const auto delegate = my.elementDelegate\n\t\t? my.elementDelegate()\n\t\t: nullptr) {\n\t\tdelegate->elementSearchInList(tag, my.itemId);\n\t\treturn;\n\t}\n\tconst auto controller = my.sessionWindow.get();\n\tif (!controller) {\n\t\treturn;\n\t}\n\tif (controller->openedFolder().current()) {\n\t\tcontroller->closeFolder();\n\t}\n\n\tcontroller->widget()->ui_hideSettingsAndLayer(anim::type::normal);\n\tCore::App().hideMediaView();\n\n\tauto &data = controller->session().data();\n\tconst auto inPeer = my.peer\n\t\t? my.peer\n\t\t: my.itemId\n\t\t? data.message(my.itemId)->history()->peer.get()\n\t\t: nullptr;\n\tcontroller->content()->searchMessages(\n\t\ttag + ' ',\n\t\t(inPeer && !inPeer->isUser())\n\t\t\t? data.history(inPeer).get()\n\t\t\t: Dialogs::Key());\n}\n\nvoid ExportToCalendar(TimeId date, const QString &messageText) {\n\tconst auto start = QDateTime::fromSecsSinceEpoch(\n\t\tdate,\n\t\tQt::UTC);\n\tconst auto end = QDateTime::fromSecsSinceEpoch(\n\t\tdate + 3600,\n\t\tQt::UTC);\n\tconst auto now = QDateTime::currentDateTimeUtc();\n\tconst auto format = u\"yyyyMMdd'T'HHmmss'Z'\"_q;\n\tconst auto locale = QLocale();\n\tconst auto raw = locale.toString(\n\t\tbase::unixtime::parse(date),\n\t\tQLocale::LongFormat);\n\tauto summary = raw;\n\tsummary.replace('\\\\', u\"\\\\\\\\\"_q);\n\tsummary.replace(';', u\"\\\\;\"_q);\n\tsummary.replace(',', u\"\\\\,\"_q);\n\tsummary.replace('\\n', u\"\\\\n\"_q);\n\tauto description = messageText;\n\tdescription.replace('\\\\', u\"\\\\\\\\\"_q);\n\tdescription.replace(';', u\"\\\\;\"_q);\n\tdescription.replace(',', u\"\\\\,\"_q);\n\tdescription.replace('\\n', u\"\\\\n\"_q);\n\tconst auto uid = base::RandomValue<uint64>();\n\tconst auto content = u\"BEGIN:VCALENDAR\\r\\n\"\n\t\t\"VERSION:2.0\\r\\n\"\n\t\t\"PRODID:-//Telegram Desktop//EN\\r\\n\"\n\t\t\"BEGIN:VEVENT\\r\\n\"\n\t\t\"DTSTART:%1\\r\\n\"\n\t\t\"DTEND:%2\\r\\n\"\n\t\t\"DTSTAMP:%3\\r\\n\"\n\t\t\"UID:telegram-%4-%7@telegram.org\\r\\n\"\n\t\t\"SUMMARY:%5\\r\\n\"\n\t\t\"DESCRIPTION:%6\\r\\n\"\n\t\t\"END:VEVENT\\r\\n\"\n\t\t\"END:VCALENDAR\\r\\n\"_q\n\t\t\t.arg(start.toString(format))\n\t\t\t.arg(end.toString(format))\n\t\t\t.arg(now.toString(format))\n\t\t\t.arg(date)\n\t\t\t.arg(summary)\n\t\t\t.arg(description)\n\t\t\t.arg(uid, 0, 16);\n\tconst auto dir = cWorkingDir() + u\"tdata/temp\"_q;\n\tQDir().mkpath(dir);\n\tconst auto path = u\"%1/event_%2.ics\"_q\n\t\t.arg(dir)\n\t\t.arg(date);\n\tauto file = QFile(path);\n\tif (file.open(QIODevice::WriteOnly)) {\n\t\tfile.write(content.toUtf8());\n\t\tfile.close();\n\t\tFile::Launch(path);\n\t}\n}\n\nvoid DoneSetReminder(std::shared_ptr<ChatHelpers::Show> show) {\n\tif (!show->valid()) {\n\t\treturn;\n\t}\n\tconst auto text = tr::lng_reminder_scheduled_in(\n\t\ttr::now,\n\t\tlt_link,\n\t\ttr::link(tr::bold(tr::lng_saved_messages(tr::now))),\n\t\ttr::marked);\n\tconst auto session = &show->session();\n\tconst auto filter = [=](\n\t\t\tconst ClickHandlerPtr &,\n\t\t\tQt::MouseButton) {\n\t\tif (const auto controller = show->resolveWindow()) {\n\t\t\tcontroller->showSection(\n\t\t\t\tstd::make_shared<HistoryView::ScheduledMemento>(\n\t\t\t\t\tsession->data().history(session->user())));\n\t\t}\n\t\treturn false;\n\t};\n\tshow->showToast({\n\t\t.text = text,\n\t\t.filter = filter,\n\t\t.iconLottie = u\"toast/saved_messages\"_q,\n\t\t.iconPadding = st::selfForwardsTaggerIconPadding,\n\t\t.st = &st::selfForwardsTaggerToast,\n\t\t.attach = RectPart::Top,\n\t\t.duration = kReminderSetToastDuration,\n\t});\n};\n\n} // namespace\n\nbool UrlRequiresConfirmation(const QUrl &url) {\n\tusing namespace qthelp;\n\n\treturn !regex_match(\n\t\t\"(^|\\\\.)(\"\n\t\t\"telegram\\\\.(org|me|dog)\"\n\t\t\"|t\\\\.me\"\n\t\t\"|te\\\\.?legra\\\\.ph\"\n\t\t\"|graph\\\\.org\"\n\t\t\"|fragment\\\\.com\"\n\t\t\"|telesco\\\\.pe\"\n\t\t\")$\",\n\t\turl.host(),\n\t\tRegExOption::CaseInsensitive);\n}\n\nQString HiddenUrlClickHandler::copyToClipboardText() const {\n\treturn url().startsWith(u\"internal:url:\"_q)\n\t\t? url().mid(u\"internal:url:\"_q.size())\n\t\t: url();\n}\n\nQString HiddenUrlClickHandler::copyToClipboardContextItemText() const {\n\treturn url().isEmpty()\n\t\t? QString()\n\t\t: !url().startsWith(u\"internal:\"_q)\n\t\t? UrlClickHandler::copyToClipboardContextItemText()\n\t\t: url().startsWith(u\"internal:url:\"_q)\n\t\t? UrlClickHandler::copyToClipboardContextItemText()\n\t\t: QString();\n}\n\nQString HiddenUrlClickHandler::dragText() const {\n\tconst auto result = HiddenUrlClickHandler::copyToClipboardText();\n\treturn result.startsWith(u\"internal:\"_q) ? QString() : result;\n}\n\nvoid HiddenUrlClickHandler::Open(QString url, QVariant context) {\n\turl = Core::TryConvertUrlToLocal(url);\n\tif (Core::InternalPassportOrOAuthLink(url)) {\n\t\treturn;\n\t}\n\n\tconst auto open = [=] {\n\t\tUrlClickHandler::Open(url, context);\n\t};\n\tif (url.startsWith(u\"tg://\"_q, Qt::CaseInsensitive)\n\t\t|| url.startsWith(u\"internal:\"_q, Qt::CaseInsensitive)) {\n\t\tUrlClickHandler::Open(url, QVariant::fromValue([&] {\n\t\t\tauto result = context.value<ClickHandlerContext>();\n\t\t\tresult.mayShowConfirmation = !base::IsCtrlPressed();\n\t\t\treturn result;\n\t\t}()));\n\t} else {\n\t\tconst auto parsedUrl = url.startsWith(u\"tonsite://\"_q)\n\t\t\t? QUrl(url)\n\t\t\t: QUrl::fromUserInput(url);\n\t\tif (UrlRequiresConfirmation(parsedUrl) && !base::IsCtrlPressed()) {\n\t\t\tconst auto my = context.value<ClickHandlerContext>();\n\t\t\tif (!my.show) {\n\t\t\t\tCore::App().hideMediaView();\n\t\t\t}\n\t\t\tconst auto displayed = parsedUrl.isValid()\n\t\t\t\t? parsedUrl.toDisplayString()\n\t\t\t\t: url;\n\t\t\tconst auto displayUrl = !IsSuspicious(displayed)\n\t\t\t\t? displayed\n\t\t\t\t: parsedUrl.isValid()\n\t\t\t\t? QString::fromUtf8(parsedUrl.toEncoded())\n\t\t\t\t: ShowEncoded(displayed);\n\t\t\tconst auto controller = my.sessionWindow.get();\n\t\t\tconst auto use = controller\n\t\t\t\t? &controller->window()\n\t\t\t\t: Core::App().activeWindow();\n\t\t\tauto box = Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\tUi::ConfirmBox(box, {\n\t\t\t\t\t.text = (tr::lng_open_this_link(tr::now)),\n\t\t\t\t\t.confirmed = [=](Fn<void()> hide) { hide(); open(); },\n\t\t\t\t\t.confirmText = tr::lng_open_link(),\n\t\t\t\t\t.labelStyle = my.dark ? &st::groupCallBoxLabel : nullptr,\n\t\t\t\t});\n\t\t\t\tconst auto &st = my.dark\n\t\t\t\t\t? st::groupCallBoxLabel\n\t\t\t\t\t: st::boxLabel;\n\t\t\t\tbox->addSkip(st.style.lineHeight - st::boxPadding.bottom());\n\t\t\t\tconst auto url = box->addRow(\n\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\tbox,\n\t\t\t\t\t\trpl::single(BoldDomainInUrl(displayUrl)),\n\t\t\t\t\t\tst));\n\t\t\t\turl->setContextMenuHook([=](\n\t\t\t\t\t\tUi::FlatLabel::ContextMenuRequest request) {\n\t\t\t\t\tconst auto copyContextText = [=] {\n\t\t\t\t\t\tTextUtilities::SetClipboardText(\n\t\t\t\t\t\t\tTextForMimeData::Simple(displayUrl));\n\t\t\t\t\t};\n\t\t\t\t\tif (request.fullSelection) {\n\t\t\t\t\t\trequest.menu->addAction(\n\t\t\t\t\t\t\ttr::lng_context_copy_link(tr::now),\n\t\t\t\t\t\t\tcopyContextText);\n\t\t\t\t\t} else if (request.uponSelection\n\t\t\t\t\t\t&& !request.fullSelection) {\n\t\t\t\t\t\tconst auto selection = request.selection;\n\t\t\t\t\t\tconst auto copySelectedText = [=] {\n\t\t\t\t\t\t\tTextUtilities::SetClipboardText(\n\t\t\t\t\t\t\t\tTextForMimeData::Simple(\n\t\t\t\t\t\t\t\t\tdisplayUrl.mid(\n\t\t\t\t\t\t\t\t\t\tselection.from,\n\t\t\t\t\t\t\t\t\t\tselection.to - selection.from)));\n\t\t\t\t\t\t};\n\t\t\t\t\t\trequest.menu->addAction(\n\t\t\t\t\t\t\ttr::lng_context_copy_selected(tr::now),\n\t\t\t\t\t\t\tcopySelectedText);\n\t\t\t\t\t} else if (request.selection.empty()) {\n\t\t\t\t\t\trequest.menu->addAction(\n\t\t\t\t\t\t\ttr::lng_context_copy_link(tr::now),\n\t\t\t\t\t\t\tcopyContextText);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\turl->setSelectable(true);\n\t\t\t\turl->setContextCopyText(tr::lng_context_copy_link(tr::now));\n\t\t\t});\n\t\t\tif (my.show) {\n\t\t\t\tmy.show->showBox(std::move(box));\n\t\t\t} else if (use) {\n\t\t\t\tuse->show(std::move(box));\n\t\t\t\tuse->activate();\n\t\t\t}\n\t\t} else {\n\t\t\topen();\n\t\t}\n\t}\n}\n\nvoid BotGameUrlClickHandler::onClick(ClickContext context) const {\n\tconst auto url = Core::TryConvertUrlToLocal(this->url());\n\tif (Core::InternalPassportOrOAuthLink(url)) {\n\t\treturn;\n\t}\n\tconst auto openLink = [=] {\n\t\tUrlClickHandler::Open(url, context.other);\n\t};\n\tconst auto my = context.other.value<ClickHandlerContext>();\n\tconst auto weakController = my.sessionWindow;\n\tconst auto controller = weakController.get();\n\tconst auto item = controller\n\t\t? controller->session().data().message(my.itemId)\n\t\t: nullptr;\n\tconst auto media = item ? item->media() : nullptr;\n\tconst auto game = media ? media->game() : nullptr;\n\tif (url.startsWith(u\"tg://\"_q, Qt::CaseInsensitive) || !_bot || !game) {\n\t\topenLink();\n\t\treturn;\n\t}\n\tconst auto bot = _bot;\n\tconst auto title = game->title;\n\tconst auto itemId = my.itemId;\n\tconst auto openGame = [=] {\n\t\tbot->session().attachWebView().open({\n\t\t\t.bot = bot,\n\t\t\t.button = {.url = url.toUtf8() },\n\t\t\t.source = InlineBots::WebViewSourceGame{\n\t\t\t\t.messageId = itemId,\n\t\t\t\t.title = title,\n\t\t\t},\n\t\t});\n\t};\n\tif (_bot->isVerified()\n\t\t|| _bot->session().local().isPeerTrustedOpenGame(_bot->id)) {\n\t\topenGame();\n\t} else {\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tconst auto callback = [=, bot = _bot](Fn<void()> close) {\n\t\t\t\tclose();\n\t\t\t\tbot->session().local().markPeerTrustedOpenGame(bot->id);\n\t\t\t\topenGame();\n\t\t\t};\n\t\t\tcontroller->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_allow_bot_pass(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_bot_name,\n\t\t\t\t\t_bot->name()),\n\t\t\t\t.confirmed = callback,\n\t\t\t\t.confirmText = tr::lng_allow_bot(),\n\t\t\t\t.labelStyle = my.dark ? &st::groupCallBoxLabel : nullptr,\n\t\t\t}));\n\t\t}\n\t}\n}\n\nauto HiddenUrlClickHandler::getTextEntity() const -> TextEntity {\n\treturn { EntityType::CustomUrl, url() };\n}\n\nQString MentionClickHandler::copyToClipboardContextItemText() const {\n\treturn tr::lng_context_copy_mention(tr::now);\n}\n\nvoid MentionClickHandler::onClick(ClickContext context) const {\n\tconst auto button = context.button;\n\tif (button == Qt::LeftButton || button == Qt::MiddleButton) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tconst auto controller = my.sessionWindow.get();\n\t\tconst auto use = controller\n\t\t\t? controller\n\t\t\t: Core::App().activeWindow()\n\t\t\t? Core::App().activeWindow()->sessionController()\n\t\t\t: nullptr;\n\t\tif (use) {\n\t\t\tuse->showPeerByLink(Window::PeerByLinkInfo{\n\t\t\t\t.usernameOrId = _tag.mid(1),\n\t\t\t\t.resolveType = Window::ResolveType::Mention,\n\t\t\t});\n\t\t}\n\t}\n}\n\nauto MentionClickHandler::getTextEntity() const -> TextEntity {\n\treturn { EntityType::Mention };\n}\n\nvoid MentionNameClickHandler::onClick(ClickContext context) const {\n\tconst auto button = context.button;\n\tif (button == Qt::LeftButton || button == Qt::MiddleButton) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tif (auto user = _session->data().userLoaded(_userId)) {\n\t\t\t\tcontroller->showPeerInfo(user);\n\t\t\t}\n\t\t}\n\t}\n}\n\nauto MentionNameClickHandler::getTextEntity() const -> TextEntity {\n\tconst auto data = TextUtilities::MentionNameDataFromFields({\n\t\t.selfId = _session->userId().bare,\n\t\t.userId = _userId.bare,\n\t\t.accessHash = _accessHash,\n\t});\n\treturn { EntityType::MentionName, data };\n}\n\nQString MentionNameClickHandler::tooltip() const {\n\tif (const auto user = _session->data().userLoaded(_userId)) {\n\t\tconst auto name = user->name();\n\t\tif (name != _text) {\n\t\t\treturn name;\n\t\t}\n\t}\n\treturn QString();\n}\n\nQString HashtagClickHandler::copyToClipboardContextItemText() const {\n\treturn tr::lng_context_copy_hashtag(tr::now);\n}\n\nvoid HashtagClickHandler::onClick(ClickContext context) const {\n\tconst auto button = context.button;\n\tif (button == Qt::LeftButton || button == Qt::MiddleButton) {\n\t\tSearchByHashtag(context, _tag);\n\t}\n}\n\nauto HashtagClickHandler::getTextEntity() const -> TextEntity {\n\treturn { EntityType::Hashtag };\n}\n\nQString CashtagClickHandler::copyToClipboardContextItemText() const {\n\treturn tr::lng_context_copy_hashtag(tr::now);\n}\n\nvoid CashtagClickHandler::onClick(ClickContext context) const {\n\tconst auto button = context.button;\n\tif (button == Qt::LeftButton || button == Qt::MiddleButton) {\n\t\tSearchByHashtag(context, _tag);\n\t}\n}\n\nauto CashtagClickHandler::getTextEntity() const -> TextEntity {\n\treturn { EntityType::Cashtag };\n}\n\nvoid BotCommandClickHandler::onClick(ClickContext context) const {\n\tconst auto button = context.button;\n\tif (button != Qt::LeftButton && button != Qt::MiddleButton) {\n\t\treturn;\n\t}\n\tconst auto my = context.other.value<ClickHandlerContext>();\n\tif (const auto delegate = my.elementDelegate\n\t\t? my.elementDelegate()\n\t\t: nullptr) {\n\t\tdelegate->elementSendBotCommand(_cmd, my.itemId);\n\t} else if (const auto controller = my.sessionWindow.get()) {\n\t\tauto &data = controller->session().data();\n\t\tconst auto peer = my.peer\n\t\t\t? my.peer\n\t\t\t: my.itemId\n\t\t\t? data.message(my.itemId)->history()->peer.get()\n\t\t\t: nullptr;\n\t\t// Can't find context.\n\t\tif (!peer) {\n\t\t\treturn;\n\t\t}\n\t\tcontroller->widget()->ui_hideSettingsAndLayer(anim::type::normal);\n\t\tCore::App().hideMediaView();\n\t\tcontroller->content()->sendBotCommand({\n\t\t\t.peer = peer,\n\t\t\t.command = _cmd,\n\t\t\t.context = my.itemId,\n\t\t});\n\t}\n}\n\nauto BotCommandClickHandler::getTextEntity() const -> TextEntity {\n\treturn { EntityType::BotCommand };\n}\n\nMonospaceClickHandler::MonospaceClickHandler(\n\tconst QString &text,\n\tEntityType type)\n: _text(text)\n, _entity({ type }) {\n}\n\nvoid MonospaceClickHandler::onClick(ClickContext context) const {\n\tconst auto button = context.button;\n\tif (button != Qt::LeftButton && button != Qt::MiddleButton) {\n\t\treturn;\n\t}\n\tconst auto my = context.other.value<ClickHandlerContext>();\n\tif (const auto controller = my.sessionWindow.get()) {\n\t\tcontroller->showToast(tr::lng_text_copied(tr::now));\n\t}\n\tTextUtilities::SetClipboardText(TextForMimeData::Simple(_text.trimmed()));\n}\n\nauto MonospaceClickHandler::getTextEntity() const -> TextEntity {\n\treturn _entity;\n}\n\nQString MonospaceClickHandler::url() const {\n\treturn _text;\n}\n\nFormattedDateClickHandler::FormattedDateClickHandler(\n\tint32 date,\n\tFormattedDateFlags flags)\n: _date(date)\n, _entityData(SerializeFormattedDateData(date, flags)) {\n}\n\nvoid FormattedDateClickHandler::onClick(ClickContext context) const {\n\tif (context.button != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tconst auto my = context.other.value<ClickHandlerContext>();\n\tconst auto controller = my.sessionWindow.get();\n\tif (!controller) {\n\t\treturn;\n\t}\n\tconst auto menu = Ui::CreateChild<Ui::PopupMenu>(\n\t\tcontroller->content(),\n\t\tst::popupMenuWithIcons);\n\n\tconst auto date = _date;\n\tconst auto show = controller->uiShow();\n\n\tmenu->addAction(\n\t\ttr::lng_context_copy_date(tr::now),\n\t\t[date, show] {\n\t\t\tconst auto text = QLocale().toString(\n\t\t\t\tbase::unixtime::parse(date),\n\t\t\t\tQLocale::LongFormat);\n\t\t\tTextUtilities::SetClipboardText(TextForMimeData::Simple(text));\n\t\t\tshow->showToast(tr::lng_date_copied(tr::now));\n\t\t},\n\t\t&st::menuIconCopy);\n\n\tconst auto itemId = my.itemId;\n\tconst auto &owner = controller->session().data();\n\tconst auto item = owner.message(itemId);\n\n\tconst auto messageText = item ? item->originalText().text : QString();\n\tmenu->addAction(\n\t\ttr::lng_context_add_to_calendar(tr::now),\n\t\t[date, messageText] { ExportToCalendar(date, messageText); },\n\t\t&st::menuIconSchedule);\n\n\tconst auto canForward = item\n\t\t&& !item->forbidsForward()\n\t\t&& item->history()->peer->allowsForwarding();\n\tif (canForward) {\n\t\tmenu->addAction(\n\t\t\ttr::lng_context_set_reminder(tr::now),\n\t\t\t[itemId, show] {\n\t\t\t\tconst auto session = &show->session();\n\t\t\t\tconst auto item = session->data().message(itemId);\n\t\t\t\tif (!item) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto self = session->user();\n\t\t\t\tconst auto history = self->owner().history(self);\n\t\t\t\tshow->showBox(HistoryView::PrepareScheduleBox(\n\t\t\t\t\tsession,\n\t\t\t\t\tshow,\n\t\t\t\t\tSendMenu::Details{ .type = SendMenu::Type::Reminder },\n\t\t\t\t\t[=](Api::SendOptions options) {\n\t\t\t\t\t\tauto action = Api::SendAction(history, options);\n\t\t\t\t\t\taction.clearDraft = false;\n\t\t\t\t\t\taction.generateLocal = false;\n\t\t\t\t\t\tsession->api().forwardMessages(\n\t\t\t\t\t\t\tData::ResolvedForwardDraft{\n\t\t\t\t\t\t\t\t.items = { item },\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\taction,\n\t\t\t\t\t\t\t[=] { DoneSetReminder(show); });\n\t\t\t\t\t}));\n\t\t\t},\n\t\t\t&st::menuIconNotifications);\n\t}\n\n\tmenu->popup(QCursor::pos());\n}\n\nauto FormattedDateClickHandler::getTextEntity() const -> TextEntity {\n\treturn { EntityType::FormattedDate, _entityData };\n}\n\nQString FormattedDateClickHandler::tooltip() const {\n\treturn QLocale().toString(\n\t\tbase::unixtime::parse(_date),\n\t\tQLocale::LongFormat);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/core/click_handler_types.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/basic_click_handlers.h\"\n#include \"ui/text/text_entity.h\"\n#include \"data/data_msg_id.h\"\n\nconstexpr auto kPeerLinkPeerIdProperty = 0x01;\nconstexpr auto kPhotoLinkMediaProperty = 0x02;\nconstexpr auto kDocumentLinkMediaProperty = 0x03;\nconstexpr auto kSendReactionEmojiProperty = 0x04;\nconstexpr auto kReactionsCountEmojiProperty = 0x05;\nconstexpr auto kDocumentFilenameTooltipProperty = 0x06;\nconstexpr auto kPhoneNumberLinkProperty = 0x07;\nconstexpr auto kTodoListItemIdProperty = 0x08;\nconstexpr auto kPollOptionProperty = 0x09;\nconstexpr auto kFastShareProperty = 0x0a;\n\nnamespace Ui {\nclass Show;\n} // namespace Ui\n\nnamespace InlineBots {\nstruct WebViewContext;\n} // namespace InlineBots\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace HistoryView {\nclass ElementDelegate;\n} // namespace HistoryView\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\n[[nodiscard]] bool UrlRequiresConfirmation(const QUrl &url);\n\nclass PeerData;\nstruct ClickHandlerContext {\n\tFullMsgId itemId;\n\t// Is filled from sections.\n\tFn<HistoryView::ElementDelegate*()> elementDelegate;\n\tbase::weak_ptr<Window::SessionController> sessionWindow;\n\tstd::shared_ptr<InlineBots::WebViewContext> botWebviewContext;\n\tstd::shared_ptr<Ui::Show> show;\n\tbool mayShowConfirmation = false;\n\tbool skipBotAutoLogin = false;\n\tbool botStartAutoSubmit = false;\n\tbool ignoreIv = false;\n\tbool dark = false;\n\t// Is filled from peer info.\n\tPeerData *peer = nullptr;\n};\nQ_DECLARE_METATYPE(ClickHandlerContext);\n\nclass PhoneClickHandler;\n\nclass HiddenUrlClickHandler : public UrlClickHandler {\npublic:\n\tHiddenUrlClickHandler(QString url) : UrlClickHandler(url, false) {\n\t}\n\tQString copyToClipboardText() const override;\n\tQString copyToClipboardContextItemText() const override;\n\tQString dragText() const override;\n\n\tstatic void Open(QString url, QVariant context = {});\n\tvoid onClick(ClickContext context) const override {\n\t\tconst auto button = context.button;\n\t\tif (button == Qt::LeftButton || button == Qt::MiddleButton) {\n\t\t\tOpen(url(), context.other);\n\t\t}\n\t}\n\n\tTextEntity getTextEntity() const override;\n\n};\n\nclass UserData;\nclass BotGameUrlClickHandler : public UrlClickHandler {\npublic:\n\tBotGameUrlClickHandler(UserData *bot, QString url)\n\t: UrlClickHandler(url, false)\n\t, _bot(bot) {\n\t}\n\tvoid onClick(ClickContext context) const override;\n\nprivate:\n\tUserData *_bot;\n\n};\n\nclass MentionClickHandler : public TextClickHandler {\npublic:\n\tMentionClickHandler(const QString &tag) : _tag(tag) {\n\t}\n\n\tvoid onClick(ClickContext context) const override;\n\n\tQString dragText() const override {\n\t\treturn _tag;\n\t}\n\n\tQString copyToClipboardContextItemText() const override;\n\n\tTextEntity getTextEntity() const override;\n\nprotected:\n\tQString url() const override {\n\t\treturn _tag;\n\t}\n\nprivate:\n\tQString _tag;\n\n};\n\nclass MentionNameClickHandler : public ClickHandler {\npublic:\n\tMentionNameClickHandler(\n\t\tnot_null<Main::Session*> session,\n\t\tQString text,\n\t\tUserId userId,\n\t\tuint64 accessHash)\n\t: _session(session)\n\t, _text(text)\n\t, _userId(userId)\n\t, _accessHash(accessHash) {\n\t}\n\n\tvoid onClick(ClickContext context) const override;\n\n\tTextEntity getTextEntity() const override;\n\n\tQString tooltip() const override;\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\tQString _text;\n\tUserId _userId;\n\tuint64 _accessHash;\n\n};\n\nclass HashtagClickHandler : public TextClickHandler {\npublic:\n\tHashtagClickHandler(const QString &tag) : _tag(tag) {\n\t}\n\n\tvoid onClick(ClickContext context) const override;\n\n\tQString dragText() const override {\n\t\treturn _tag;\n\t}\n\n\tQString copyToClipboardContextItemText() const override;\n\n\tTextEntity getTextEntity() const override;\n\nprotected:\n\tQString url() const override {\n\t\treturn _tag;\n\t}\n\nprivate:\n\tQString _tag;\n\n};\n\nclass CashtagClickHandler : public TextClickHandler {\npublic:\n\tCashtagClickHandler(const QString &tag) : _tag(tag) {\n\t}\n\n\tvoid onClick(ClickContext context) const override;\n\n\tQString dragText() const override {\n\t\treturn _tag;\n\t}\n\n\tQString copyToClipboardContextItemText() const override;\n\n\tTextEntity getTextEntity() const override;\n\nprotected:\n\tQString url() const override {\n\t\treturn _tag;\n\t}\n\nprivate:\n\tQString _tag;\n\n};\n\nclass BotCommandClickHandler : public TextClickHandler {\npublic:\n\tBotCommandClickHandler(const QString &cmd) : _cmd(cmd) {\n\t}\n\n\tvoid onClick(ClickContext context) const override;\n\n\tQString dragText() const override {\n\t\treturn _cmd;\n\t}\n\n\tTextEntity getTextEntity() const override;\n\nprotected:\n\tQString url() const override {\n\t\treturn _cmd;\n\t}\n\nprivate:\n\tQString _cmd;\n\n};\n\nclass MonospaceClickHandler : public TextClickHandler {\npublic:\n\tMonospaceClickHandler(const QString &text, EntityType type);\n\n\tvoid onClick(ClickContext context) const override;\n\n\tTextEntity getTextEntity() const override;\n\nprotected:\n\tQString url() const override;\n\nprivate:\n\tconst QString _text;\n\tconst TextEntity _entity;\n\n};\n\nclass FormattedDateClickHandler : public ClickHandler {\npublic:\n\tFormattedDateClickHandler(TimeId date, FormattedDateFlags flags);\n\n\tvoid onClick(ClickContext context) const override;\n\tTextEntity getTextEntity() const override;\n\tQString tooltip() const override;\n\nprivate:\n\tTimeId _date = 0;\n\tQString _entityData;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/core/core_cloud_password.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/core_cloud_password.h\"\n\n#include \"base/openssl_help.h\"\n#include \"mtproto/mtproto_dh_utils.h\"\n\nnamespace Core {\nnamespace {\n\nusing namespace openssl;\n\nconstexpr auto kAdditionalSalt = size_type(32);\n\nconstexpr auto kSizeForHash = 256;\nbytes::vector NumBytesForHash(bytes::const_span number) {\n\tconst auto fill = kSizeForHash - number.size();\n\tif (!fill) {\n\t\treturn bytes::make_vector(number);\n\t}\n\tauto result = bytes::vector(kSizeForHash);\n\tconst auto storage = bytes::make_span(result);\n\tbytes::set_with_const(storage.subspan(0, fill), bytes::type(0));\n\tbytes::copy(storage.subspan(fill), number);\n\treturn result;\n}\n\nbytes::vector BigNumForHash(const BigNum &number) {\n\tauto result = number.getBytes();\n\tif (result.size() == kSizeForHash) {\n\t\treturn result;\n\t}\n\treturn NumBytesForHash(result);\n}\n\nbool IsPositive(const BigNum &number) {\n\treturn !number.isNegative() && (number.bitsSize() > 0);\n}\n\nbool IsGoodLarge(const BigNum &number, const BigNum &p) {\n\treturn IsPositive(number) && IsPositive(BigNum::Sub(p, number));\n}\n\nbytes::vector Xor(bytes::const_span a, bytes::const_span b) {\n\tExpects(a.size() == b.size());\n\n\tauto result = bytes::vector(a.size());\n\tfor (auto i = index_type(); i != a.size(); ++i) {\n\t\tresult[i] = a[i] ^ b[i];\n\t}\n\treturn result;\n};\n\nbytes::vector ComputeHash(\n\t\tconst CloudPasswordAlgoModPow &algo,\n\t\tbytes::const_span password) {\n\tconst auto hash1 = Sha256(algo.salt1, password, algo.salt1);\n\tconst auto hash2 = Sha256(algo.salt2, hash1, algo.salt2);\n\tconst auto hash3 = Pbkdf2Sha512(hash2, algo.salt1, algo.kIterations);\n\treturn Sha256(algo.salt2, hash3, algo.salt2);\n}\n\nCloudPasswordDigest ComputeDigest(\n\t\tconst CloudPasswordAlgoModPow &algo,\n\t\tbytes::const_span password) {\n\tif (!MTP::IsPrimeAndGood(algo.p, algo.g)) {\n\t\tLOG((\"API Error: Bad p/g in cloud password creation!\"));\n\t\treturn {};\n\t}\n\tconst auto value = BigNum::ModExp(\n\t\tBigNum(algo.g),\n\t\tBigNum(ComputeHash(algo, password)),\n\t\tBigNum(algo.p));\n\tif (value.failed()) {\n\t\tLOG((\"API Error: Failed to count g_x in cloud password creation!\"));\n\t\treturn {};\n\t}\n\treturn { BigNumForHash(value) };\n}\n\nCloudPasswordResult ComputeCheck(\n\t\tconst CloudPasswordCheckRequest &request,\n\t\tconst CloudPasswordAlgoModPow &algo,\n\t\tbytes::const_span hash) {\n\tconst auto failed = [] {\n\t\treturn CloudPasswordResult{ MTP_inputCheckPasswordEmpty() };\n\t};\n\n\tconst auto p = BigNum(algo.p);\n\tconst auto g = BigNum(algo.g);\n\tconst auto B = BigNum(request.B);\n\tif (!MTP::IsPrimeAndGood(algo.p, algo.g)) {\n\t\tLOG((\"API Error: Bad p/g in cloud password creation!\"));\n\t\treturn failed();\n\t} else if (!IsGoodLarge(B, p)) {\n\t\tLOG((\"API Error: Bad B in cloud password check!\"));\n\t\treturn failed();\n\t}\n\n\tconst auto context = Context();\n\tconst auto x = BigNum(hash);\n\tconst auto pForHash = NumBytesForHash(algo.p);\n\tconst auto gForHash = BigNumForHash(g);\n\tconst auto BForHash = NumBytesForHash(request.B);\n\tconst auto g_x = BigNum::ModExp(g, x, p, context);\n\tconst auto k = BigNum(Sha256(pForHash, gForHash));\n\tconst auto kg_x = BigNum::ModMul(k, g_x, p, context);\n\n\tconst auto GenerateAndCheckRandom = [&] {\n\t\tconstexpr auto kRandomSize = 256;\n\t\twhile (true) {\n\t\t\tauto random = bytes::vector(kRandomSize);\n\t\t\tbytes::set_random(random);\n\t\t\tconst auto a = BigNum(random);\n\t\t\tconst auto A = BigNum::ModExp(g, a, p, context);\n\t\t\tif (MTP::IsGoodModExpFirst(A, p)) {\n\t\t\t\tauto AForHash = BigNumForHash(A);\n\t\t\t\tconst auto u = BigNum(Sha256(AForHash, BForHash));\n\t\t\t\tif (IsPositive(u)) {\n\t\t\t\t\treturn std::make_tuple(a, std::move(AForHash), u);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\tconst auto &[a, AForHash, u] = GenerateAndCheckRandom();\n\tconst auto g_b = BigNum::ModSub(B, kg_x, p, context);\n\tif (!MTP::IsGoodModExpFirst(g_b, p)) {\n\t\tLOG((\"API Error: Bad g_b in cloud password check!\"));\n\t\treturn failed();\n\t}\n\tconst auto ux = BigNum::Mul(u, x, context);\n\tconst auto a_ux = BigNum::Add(a, ux);\n\tconst auto S = BigNum::ModExp(g_b, a_ux, p, context);\n\tif (S.failed()) {\n\t\tLOG((\"API Error: Failed to count S in cloud password check!\"));\n\t\treturn failed();\n\t}\n\tconst auto K = Sha256(BigNumForHash(S));\n\tconst auto M1 = Sha256(\n\t\tXor(Sha256(pForHash), Sha256(gForHash)),\n\t\tSha256(algo.salt1),\n\t\tSha256(algo.salt2),\n\t\tAForHash,\n\t\tBForHash,\n\t\tK);\n\treturn CloudPasswordResult{ MTP_inputCheckPasswordSRP(\n\t\tMTP_long(request.id),\n\t\tMTP_bytes(AForHash),\n\t\tMTP_bytes(M1))\n\t};\n}\n\nbytes::vector ComputeHash(\n\t\tv::null_t,\n\t\tbytes::const_span password) {\n\tUnexpected(\"Bad secure secret algorithm.\");\n}\n\nbytes::vector ComputeHash(\n\t\tconst SecureSecretAlgoSHA512 &algo,\n\t\tbytes::const_span password) {\n\treturn Sha512(algo.salt, password, algo.salt);\n}\n\nbytes::vector ComputeHash(\n\t\tconst SecureSecretAlgoPBKDF2 &algo,\n\t\tbytes::const_span password) {\n\treturn Pbkdf2Sha512(password, algo.salt, algo.kIterations);\n}\n\n} // namespace\n\nCloudPasswordAlgo ParseCloudPasswordAlgo(const MTPPasswordKdfAlgo &data) {\n\treturn data.match([](const MTPDpasswordKdfAlgoModPow &data) {\n\t\treturn CloudPasswordAlgo(CloudPasswordAlgoModPow{\n\t\t\tbytes::make_vector(data.vsalt1().v),\n\t\t\tbytes::make_vector(data.vsalt2().v),\n\t\t\tdata.vg().v,\n\t\t\tbytes::make_vector(data.vp().v) });\n\t}, [](const MTPDpasswordKdfAlgoUnknown &data) {\n\t\treturn CloudPasswordAlgo();\n\t});\n}\n\nCloudPasswordCheckRequest ParseCloudPasswordCheckRequest(\n\t\tconst MTPDaccount_password &data) {\n\tconst auto algo = data.vcurrent_algo();\n\treturn CloudPasswordCheckRequest{\n\t\tdata.vsrp_id().value_or_empty(),\n\t\tbytes::make_vector(data.vsrp_B().value_or_empty()),\n\t\t(algo ? ParseCloudPasswordAlgo(*algo) : CloudPasswordAlgo())\n\t};\n}\n\nCloudPasswordAlgo ValidateNewCloudPasswordAlgo(CloudPasswordAlgo &&parsed) {\n\tif (!v::is<CloudPasswordAlgoModPow>(parsed)) {\n\t\treturn v::null;\n\t}\n\tauto &value = v::get<CloudPasswordAlgoModPow>(parsed);\n\tconst auto already = value.salt1.size();\n\tvalue.salt1.resize(already + kAdditionalSalt);\n\tbytes::set_random(bytes::make_span(value.salt1).subspan(already));\n\treturn std::move(parsed);\n}\n\nMTPPasswordKdfAlgo PrepareCloudPasswordAlgo(const CloudPasswordAlgo &data) {\n\treturn v::match(data, [](const CloudPasswordAlgoModPow &data) {\n\t\treturn MTP_passwordKdfAlgoModPow(\n\t\t\tMTP_bytes(data.salt1),\n\t\t\tMTP_bytes(data.salt2),\n\t\t\tMTP_int(data.g),\n\t\t\tMTP_bytes(data.p));\n\t}, [](v::null_t) {\n\t\treturn MTP_passwordKdfAlgoUnknown();\n\t});\n}\n\nCloudPasswordResult::operator bool() const {\n\treturn (result.type() != mtpc_inputCheckPasswordEmpty);\n}\n\nbytes::vector ComputeCloudPasswordHash(\n\t\tconst CloudPasswordAlgo &algo,\n\t\tbytes::const_span password) {\n\treturn v::match(algo, [&](const CloudPasswordAlgoModPow &data) {\n\t\treturn ComputeHash(data, password);\n\t}, [](v::null_t) -> bytes::vector {\n\t\tUnexpected(\"Bad cloud password algorithm.\");\n\t});\n}\n\nCloudPasswordDigest ComputeCloudPasswordDigest(\n\t\tconst CloudPasswordAlgo &algo,\n\t\tbytes::const_span password) {\n\treturn v::match(algo, [&](const CloudPasswordAlgoModPow &data) {\n\t\treturn ComputeDigest(data, password);\n\t}, [](v::null_t) -> CloudPasswordDigest {\n\t\tUnexpected(\"Bad cloud password algorithm.\");\n\t});\n}\n\nCloudPasswordResult ComputeCloudPasswordCheck(\n\t\tconst CloudPasswordCheckRequest &request,\n\t\tbytes::const_span hash) {\n\treturn v::match(request.algo, [&](const CloudPasswordAlgoModPow &data) {\n\t\treturn ComputeCheck(request, data, hash);\n\t}, [](v::null_t) -> CloudPasswordResult {\n\t\tUnexpected(\"Bad cloud password algorithm.\");\n\t});\n}\n\nSecureSecretAlgo ParseSecureSecretAlgo(\n\t\tconst MTPSecurePasswordKdfAlgo &data) {\n\treturn data.match([](\n\tconst MTPDsecurePasswordKdfAlgoPBKDF2HMACSHA512iter100000 &data) {\n\t\treturn SecureSecretAlgo(SecureSecretAlgoPBKDF2{\n\t\t\tbytes::make_vector(data.vsalt().v) });\n\t}, [](const MTPDsecurePasswordKdfAlgoSHA512 &data) {\n\t\treturn SecureSecretAlgo(SecureSecretAlgoSHA512{\n\t\t\tbytes::make_vector(data.vsalt().v) });\n\t}, [](const MTPDsecurePasswordKdfAlgoUnknown &data) {\n\t\treturn SecureSecretAlgo();\n\t});\n}\n\nSecureSecretAlgo ValidateNewSecureSecretAlgo(SecureSecretAlgo &&parsed) {\n\tif (!v::is<SecureSecretAlgoPBKDF2>(parsed)) {\n\t\treturn v::null;\n\t}\n\tauto &value = v::get<SecureSecretAlgoPBKDF2>(parsed);\n\tconst auto already = value.salt.size();\n\tvalue.salt.resize(already + kAdditionalSalt);\n\tbytes::set_random(bytes::make_span(value.salt).subspan(already));\n\treturn std::move(parsed);\n}\n\nMTPSecurePasswordKdfAlgo PrepareSecureSecretAlgo(\n\t\tconst SecureSecretAlgo &data) {\n\treturn v::match(data, [](const SecureSecretAlgoPBKDF2 &data) {\n\t\treturn MTP_securePasswordKdfAlgoPBKDF2HMACSHA512iter100000(\n\t\t\tMTP_bytes(data.salt));\n\t}, [](const SecureSecretAlgoSHA512 &data) {\n\t\treturn MTP_securePasswordKdfAlgoSHA512(MTP_bytes(data.salt));\n\t}, [](v::null_t) {\n\t\treturn MTP_securePasswordKdfAlgoUnknown();\n\t});\n}\n\nbytes::vector ComputeSecureSecretHash(\n\t\tconst SecureSecretAlgo &algo,\n\t\tbytes::const_span password) {\n\treturn v::match(algo, [&](const auto &data) {\n\t\treturn ComputeHash(data, password);\n\t});\n}\n\nCloudPasswordState ParseCloudPasswordState(\n\t\tconst MTPDaccount_password &data) {\n\tauto result = CloudPasswordState();\n\tresult.mtp.request = ParseCloudPasswordCheckRequest(data);\n\tresult.hasPassword = (!!result.mtp.request);\n\tresult.mtp.unknownAlgorithm = data.vcurrent_algo() && !result.hasPassword;\n\tresult.hasRecovery = data.is_has_recovery();\n\tresult.notEmptyPassport = data.is_has_secure_values();\n\tresult.hint = qs(data.vhint().value_or_empty());\n\tresult.mtp.newPassword = ValidateNewCloudPasswordAlgo(\n\t\tParseCloudPasswordAlgo(data.vnew_algo()));\n\tresult.mtp.newSecureSecret = ValidateNewSecureSecretAlgo(\n\t\tParseSecureSecretAlgo(data.vnew_secure_algo()));\n\tresult.unconfirmedPattern = qs(\n\t\tdata.vemail_unconfirmed_pattern().value_or_empty());\n\tresult.loginEmailPattern = qs(\n\t\tdata.vlogin_email_pattern().value_or_empty());\n\tresult.pendingResetDate = data.vpending_reset_date().value_or_empty();\n\n\tresult.outdatedClient = [&] {\n\t\tconst auto badSecureAlgo = data.vnew_secure_algo().match([](\n\t\t\t\tconst MTPDsecurePasswordKdfAlgoUnknown &) {\n\t\t\treturn true;\n\t\t}, [](const auto &) {\n\t\t\treturn false;\n\t\t});\n\t\tif (badSecureAlgo) {\n\t\t\treturn true;\n\t\t}\n\t\tif (data.vcurrent_algo()) {\n\t\t\tconst auto badCurrentAlgo = data.vcurrent_algo()->match([](\n\t\t\t\t\tconst MTPDpasswordKdfAlgoUnknown &) {\n\t\t\t\treturn true;\n\t\t\t}, [](const auto &) {\n\t\t\t\treturn false;\n\t\t\t});\n\t\t\tif (badCurrentAlgo) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\tconst auto badNewAlgo = data.vnew_algo().match([](\n\t\t\t\tconst MTPDpasswordKdfAlgoUnknown &) {\n\t\t\treturn true;\n\t\t}, [](const auto &) {\n\t\t\treturn false;\n\t\t});\n\t\tif (badNewAlgo) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}();\n\n\treturn result;\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/core_cloud_password.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/bytes.h\"\n\nnamespace Core {\n\nconstexpr auto kHandleSrpIdInvalidTimeout = 60 * crl::time(1000);\n\nstruct CloudPasswordAlgoModPow {\n\tstatic constexpr auto kIterations = 100000;\n\n\tbytes::vector salt1;\n\tbytes::vector salt2;\n\tint g = 0;\n\tbytes::vector p;\n};\n\ninline bool operator==(\n\t\tconst CloudPasswordAlgoModPow &a,\n\t\tconst CloudPasswordAlgoModPow &b) {\n\treturn (a.salt1 == b.salt1)\n\t\t&& (a.salt2 == b.salt2)\n\t\t&& (a.g == b.g)\n\t\t&& (a.p == b.p);\n}\n\nusing CloudPasswordAlgo = std::variant<v::null_t, CloudPasswordAlgoModPow>;\n\nCloudPasswordAlgo ParseCloudPasswordAlgo(const MTPPasswordKdfAlgo &data);\nCloudPasswordAlgo ValidateNewCloudPasswordAlgo(CloudPasswordAlgo &&parsed);\nMTPPasswordKdfAlgo PrepareCloudPasswordAlgo(const CloudPasswordAlgo &data);\n\nstruct CloudPasswordCheckRequest {\n\tuint64 id = 0;\n\tbytes::vector B;\n\tCloudPasswordAlgo algo;\n\n\texplicit operator bool() const {\n\t\treturn !v::is_null(algo);\n\t}\n};\n\ninline bool operator==(\n\t\tconst CloudPasswordCheckRequest &a,\n\t\tconst CloudPasswordCheckRequest &b) {\n\treturn (a.id == b.id) && (a.B == b.B) && (a.algo == b.algo);\n}\n\ninline bool operator!=(\n\t\tconst CloudPasswordCheckRequest &a,\n\t\tconst CloudPasswordCheckRequest &b) {\n\treturn !(a == b);\n}\n\nCloudPasswordCheckRequest ParseCloudPasswordCheckRequest(\n\tconst MTPDaccount_password &data);\n\nstruct CloudPasswordResult {\n\tMTPInputCheckPasswordSRP result;\n\n\texplicit operator bool() const;\n};\n\nstruct CloudPasswordDigest {\n\tbytes::vector modpow;\n};\n\nbytes::vector ComputeCloudPasswordHash(\n\tconst CloudPasswordAlgo &algo,\n\tbytes::const_span password);\n\nCloudPasswordDigest ComputeCloudPasswordDigest(\n\tconst CloudPasswordAlgo &algo,\n\tbytes::const_span password);\n\nCloudPasswordResult ComputeCloudPasswordCheck(\n\tconst CloudPasswordCheckRequest &request,\n\tbytes::const_span hash);\n\nstruct SecureSecretAlgoSHA512 {\n\tbytes::vector salt;\n};\n\ninline bool operator==(\n\t\tconst SecureSecretAlgoSHA512 &a,\n\t\tconst SecureSecretAlgoSHA512 &b) {\n\treturn (a.salt == b.salt);\n}\n\nstruct SecureSecretAlgoPBKDF2 {\n\tstatic constexpr auto kIterations = 100000;\n\n\tbytes::vector salt;\n};\n\ninline bool operator==(\n\t\tconst SecureSecretAlgoPBKDF2 &a,\n\t\tconst SecureSecretAlgoPBKDF2 &b) {\n\treturn (a.salt == b.salt);\n}\n\nusing SecureSecretAlgo = std::variant<\n\tv::null_t,\n\tSecureSecretAlgoSHA512,\n\tSecureSecretAlgoPBKDF2>;\n\nSecureSecretAlgo ParseSecureSecretAlgo(\n\tconst MTPSecurePasswordKdfAlgo &data);\nSecureSecretAlgo ValidateNewSecureSecretAlgo(SecureSecretAlgo &&parsed);\nMTPSecurePasswordKdfAlgo PrepareSecureSecretAlgo(\n\tconst SecureSecretAlgo &data);\n\nbytes::vector ComputeSecureSecretHash(\n\tconst SecureSecretAlgo &algo,\n\tbytes::const_span password);\n\nstruct CloudPasswordState {\n\tstruct Mtp {\n\t\tCloudPasswordCheckRequest request;\n\t\tbool unknownAlgorithm = false;\n\t\tCloudPasswordAlgo newPassword;\n\t\tSecureSecretAlgo newSecureSecret;\n\t};\n\tMtp mtp;\n\tbool hasPassword = false;\n\tbool hasRecovery = false;\n\tbool notEmptyPassport = false;\n\tbool outdatedClient = false;\n\tQString hint;\n\tQString unconfirmedPattern;\n\tQString loginEmailPattern;\n\tTimeId pendingResetDate = 0;\n};\n\nCloudPasswordState ParseCloudPasswordState(\n\tconst MTPDaccount_password &data);\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/core_settings.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/core_settings.h\"\n\n#include \"base/platform/base_platform_info.h\"\n#include \"calls/group/calls_group_common.h\"\n#include \"history/view/history_view_quick_action.h\"\n#include \"lang/lang_keys.h\"\n#include \"platform/platform_notifications_manager.h\"\n#include \"spellcheck/spellcheck_types.h\"\n#include \"storage/serialize_common.h\"\n#include \"ui/gl/gl_detection.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"webrtc/webrtc_create_adm.h\"\n#include \"webrtc/webrtc_device_common.h\"\n#include \"window/section_widget.h\"\n\nnamespace Core {\nnamespace {\n\nconstexpr auto kInitialVideoQuality = 480; // Start with SD.\n\n[[nodiscard]] int DefaultIvZoom() {\n\tconst auto exact = cScale() * 100 / cScreenScale();\n\tconst auto snap10 = ((exact + 5) / 10) * 10;\n\tconst auto snap25 = ((exact + 12) / 25) * 25;\n\treturn (std::abs(exact - snap25) <= std::abs(exact - snap10))\n\t\t? snap25\n\t\t: snap10;\n}\n\n[[nodiscard]] int ResolveIvZoom(int value) {\n\treturn (value > 0) ? value : DefaultIvZoom();\n}\n\n[[nodiscard]] WindowPosition Deserialize(const QByteArray &data) {\n\tQDataStream stream(data);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\n\tauto result = WindowPosition();\n\tstream\n\t\t>> result.x\n\t\t>> result.y\n\t\t>> result.w\n\t\t>> result.h\n\t\t>> result.moncrc\n\t\t>> result.maximized\n\t\t>> result.scale;\n\treturn result;\n}\n\nvoid LogPosition(const WindowPosition &position, const QString &name) {\n\tDEBUG_LOG((\"%1 Pos: Writing to storage %2, %3, %4, %5\"\n\t\t\" (scale %6%, maximized %7)\")\n\t\t.arg(name)\n\t\t.arg(position.x)\n\t\t.arg(position.y)\n\t\t.arg(position.w)\n\t\t.arg(position.h)\n\t\t.arg(position.scale)\n\t\t.arg(position.maximized));\n}\n\n[[nodiscard]] QByteArray Serialize(const WindowPosition &position) {\n\tauto result = QByteArray();\n\tconst auto size = 7 * sizeof(qint32);\n\tresult.reserve(size);\n\t{\n\t\tQDataStream stream(&result, QIODevice::WriteOnly);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\t\tstream\n\t\t\t<< qint32(position.x)\n\t\t\t<< qint32(position.y)\n\t\t\t<< qint32(position.w)\n\t\t\t<< qint32(position.h)\n\t\t\t<< qint32(position.moncrc)\n\t\t\t<< qint32(position.maximized)\n\t\t\t<< qint32(position.scale);\n\t}\n\treturn result;\n}\n\n[[nodiscard]] QString Serialize(RecentEmojiDocument document) {\n\treturn u\"%1-%2\"_q.arg(document.id).arg(document.test ? 1 : 0);\n}\n\n[[nodiscard]] std::optional<RecentEmojiDocument> ParseRecentEmojiDocument(\n\t\tconst QString &serialized) {\n\tconst auto parts = QStringView(serialized).split('-');\n\tif (parts.size() != 2 || parts[1].size() != 1) {\n\t\treturn {};\n\t}\n\tconst auto id = parts[0].toULongLong();\n\tconst auto test = parts[1][0];\n\tif (!id || (test != '0' && test != '1')) {\n\t\treturn {};\n\t}\n\treturn RecentEmojiDocument{ id, (test == '1') };\n}\n\n[[nodiscard]] quint32 SerializeVideoQuality(Media::VideoQuality quality) {\n\tstatic_assert(sizeof(Media::VideoQuality) == sizeof(uint32));\n\tauto result = uint32();\n\tconst auto data = static_cast<const void*>(&quality);\n\tmemcpy(&result, data, sizeof(quality));\n\treturn result;\n}\n\n[[nodiscard]] Media::VideoQuality DeserializeVideoQuality(quint32 value) {\n\tauto result = Media::VideoQuality();\n\tconst auto data = static_cast<void*>(&result);\n\tmemcpy(data, &value, sizeof(result));\n\treturn (result.height <= 4320) ? result : Media::VideoQuality();\n}\n\n} // namespace\n\n[[nodiscard]] WindowPosition AdjustToScale(\n\t\tWindowPosition position,\n\t\tconst QString &name) {\n\tDEBUG_LOG((\"%1 Pos: Initializing first %2, %3, %4, %5 \"\n\t\t\"(scale %6%, maximized %7)\")\n\t\t.arg(name)\n\t\t.arg(position.x)\n\t\t.arg(position.y)\n\t\t.arg(position.w)\n\t\t.arg(position.h)\n\t\t.arg(position.scale)\n\t\t.arg(position.maximized));\n\n\tif (!position.scale) {\n\t\treturn position;\n\t}\n\tconst auto scaleFactor = cScale() / float64(position.scale);\n\tif (scaleFactor != 1.) {\n\t\t// Change scale while keeping the position center in place.\n\t\tposition.x += position.w / 2;\n\t\tposition.y += position.h / 2;\n\t\tposition.w *= scaleFactor;\n\t\tposition.h *= scaleFactor;\n\t\tposition.x -= position.w / 2;\n\t\tposition.y -= position.h / 2;\n\t}\n\treturn position;\n}\n\nSettings::Settings()\n: _sendSubmitWay(Ui::InputSubmitSettings::Enter)\n, _floatPlayerColumn(Window::Column::Second)\n, _floatPlayerCorner(RectPart::TopRight)\n, _dialogsWithChatWidthRatio(DefaultDialogsWidthRatio())\n, _dialogsNoChatWidthRatio(DefaultDialogsWidthRatio())\n, _videoQuality({ .height = kInitialVideoQuality }) {\n}\n\nSettings::~Settings() = default;\n\nQByteArray Settings::serialize() const {\n\tconst auto themesAccentColors = _themesAccentColors.serialize();\n\tconst auto windowPosition = Serialize(_windowPosition);\n\tLogPosition(_windowPosition, u\"Window\"_q);\n\tconst auto mediaViewPosition = Serialize(_mediaViewPosition);\n\tLogPosition(_mediaViewPosition, u\"Viewer\"_q);\n\tconst auto ivPosition = Serialize(_ivPosition);\n\tLogPosition(_ivPosition, u\"IV\"_q);\n\tconst auto callPanelPosition = Serialize(_callPanelPosition);\n\tLogPosition(_callPanelPosition, u\"CallPanel\"_q);\n\tconst auto proxy = _proxy.serialize();\n\tconst auto skipLanguages = _skipTranslationLanguages.current();\n\n\tauto recentEmojiPreloadGenerated = std::vector<RecentEmojiPreload>();\n\tif (_recentEmojiPreload.empty()) {\n\t\trecentEmojiPreloadGenerated.reserve(_recentEmoji.size());\n\t\tfor (const auto &[id, rating] : _recentEmoji) {\n\t\t\tauto string = QString();\n\t\t\tif (const auto document = std::get_if<RecentEmojiDocument>(\n\t\t\t\t\t&id.data)) {\n\t\t\t\tstring = Serialize(*document);\n\t\t\t} else if (const auto emoji = std::get_if<EmojiPtr>(&id.data)) {\n\t\t\t\tstring = (*emoji)->id();\n\t\t\t}\n\t\t\trecentEmojiPreloadGenerated.push_back({ string, rating });\n\t\t}\n\t}\n\tconst auto &recentEmojiPreloadData = _recentEmojiPreload.empty()\n\t\t? recentEmojiPreloadGenerated\n\t\t: _recentEmojiPreload;\n\tconst auto noWarningExtensions = QStringList(\n\t\tbegin(_noWarningExtensions),\n\t\tend(_noWarningExtensions)\n\t).join(' ');\n\n\tauto size = Serialize::bytearraySize(themesAccentColors)\n\t\t+ sizeof(qint32) * 5\n\t\t+ Serialize::stringSize(_downloadPath.current())\n\t\t+ Serialize::bytearraySize(_downloadPathBookmark)\n\t\t+ sizeof(qint32) * 9\n\t\t+ Serialize::stringSize(QString()) // legacy call output device id\n\t\t+ Serialize::stringSize(QString()) // legacy call input device id\n\t\t+ sizeof(qint32) * 5;\n\tfor (const auto &[key, value] : _soundOverrides) {\n\t\tsize += Serialize::stringSize(key) + Serialize::stringSize(value);\n\t}\n\tsize += sizeof(qint32) * 13\n\t\t+ Serialize::bytearraySize(_videoPipGeometry)\n\t\t+ sizeof(qint32)\n\t\t+ (_dictionariesEnabled.current().size() * sizeof(quint64))\n\t\t+ sizeof(qint32) * 12\n\t\t+ Serialize::stringSize(_cameraDeviceId.current())\n\t\t+ sizeof(qint32) * 2\n\t\t+ Serialize::bytearraySize(_groupCallPushToTalkShortcut)\n\t\t+ sizeof(qint64)\n\t\t+ sizeof(qint32) * 2\n\t\t+ Serialize::bytearraySize(windowPosition)\n\t\t+ sizeof(qint32);\n\tfor (const auto &[id, rating] : recentEmojiPreloadData) {\n\t\tsize += Serialize::stringSize(id) + sizeof(quint16);\n\t}\n\tsize += sizeof(qint32);\n\tfor (const auto &[id, variant] : _emojiVariants) {\n\t\tsize += Serialize::stringSize(id) + sizeof(quint8);\n\t}\n\tsize += sizeof(qint32) * 3\n\t\t+ Serialize::bytearraySize(proxy)\n\t\t+ sizeof(qint32) * 2\n\t\t+ Serialize::bytearraySize(_photoEditorBrush)\n\t\t+ sizeof(qint32) * 3\n\t\t+ Serialize::stringSize(_customDeviceModel.current())\n\t\t+ sizeof(qint32) * 4\n\t\t+ (_accountsOrder.size() * sizeof(quint64))\n\t\t+ sizeof(qint32) * 7\n\t\t+ (skipLanguages.size() * sizeof(quint64))\n\t\t+ sizeof(qint32) * 2\n\t\t+ sizeof(quint64)\n\t\t+ sizeof(qint32) * 3\n\t\t+ Serialize::bytearraySize(mediaViewPosition)\n\t\t+ sizeof(qint32)\n\t\t+ sizeof(quint64)\n\t\t+ sizeof(qint32) * 2;\n\tfor (const auto &id : _recentEmojiSkip) {\n\t\tsize += Serialize::stringSize(id);\n\t}\n\tsize += sizeof(qint32) * 2\n\t\t+ Serialize::stringSize(_playbackDeviceId.current())\n\t\t+ Serialize::stringSize(_captureDeviceId.current())\n\t\t+ Serialize::stringSize(_callPlaybackDeviceId.current())\n\t\t+ Serialize::stringSize(_callCaptureDeviceId.current())\n\t\t+ Serialize::bytearraySize(ivPosition)\n\t\t+ Serialize::stringSize(noWarningExtensions)\n\t\t+ Serialize::stringSize(_customFontFamily)\n\t\t+ sizeof(qint32) * 3\n\t\t+ Serialize::bytearraySize(_tonsiteStorageToken)\n\t\t+ sizeof(qint32) * 8\n\t\t+ sizeof(ushort)\n\t\t+ sizeof(qint32) // _notificationsDisplayChecksum\n\t\t+ Serialize::bytearraySize(callPanelPosition)\n\t\t+ sizeof(qint32) * 4;\n\tsize += sizeof(quint32);\n\tfor (const auto &[key, value] : _prefs) {\n\t\tsize += Serialize::bytearraySize(key)\n\t\t\t+ Serialize::bytearraySize(value);\n\t}\n\tsize += sizeof(qint32); // _audioPlaybackSpeed\n\n\t// Fork Settings.\n\tsize += sizeof(qint32);\n\tsize += sizeof(qint32);\n\tsize += sizeof(qint32);\n\tsize += sizeof(qint32);\n\tsize += Serialize::stringSize(QString());\n\tsize += Serialize::stringSize(QString());\n\tsize += sizeof(qint32);\n\tsize += sizeof(qint32);\n\tsize += sizeof(qint32);\n\tsize += sizeof(qint32);\n\tsize += sizeof(qint32);\n\t//\n\n\tauto result = QByteArray();\n\tresult.reserve(size);\n\t{\n\t\tQDataStream stream(&result, QIODevice::WriteOnly);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\t\tstream\n\t\t\t<< themesAccentColors\n\t\t\t<< qint32(_adaptiveForWide.current() ? 1 : 0)\n\t\t\t<< qint32(_moderateModeEnabled ? 1 : 0)\n\t\t\t<< qint32(qRound(_songVolume.current() * 1e6))\n\t\t\t<< qint32(qRound(_videoVolume.current() * 1e6))\n\t\t\t<< qint32(_askDownloadPath ? 1 : 0)\n\t\t\t<< _downloadPath.current()\n\t\t\t<< _downloadPathBookmark\n\t\t\t<< qint32(1)\n\t\t\t<< qint32(_soundNotify ? 1 : 0)\n\t\t\t<< qint32(_desktopNotify ? 1 : 0)\n\t\t\t<< qint32(_flashBounceNotify ? 1 : 0)\n\t\t\t<< static_cast<qint32>(_notifyView)\n\t\t\t<< qint32(_nativeNotifications ? (*_nativeNotifications ? 1 : 2) : 0)\n\t\t\t<< qint32(_notificationsCount)\n\t\t\t<< static_cast<qint32>(_notificationsCorner)\n\t\t\t<< qint32(_autoLock)\n\t\t\t<< QString() // legacy call output device id\n\t\t\t<< QString() // legacy call input device id\n\t\t\t<< qint32(_callOutputVolume)\n\t\t\t<< qint32(_callInputVolume)\n\t\t\t<< qint32(_callAudioDuckingEnabled ? 1 : 0)\n\t\t\t<< qint32(_lastSeenWarningSeen ? 1 : 0)\n\t\t\t<< qint32(_soundOverrides.size());\n\t\tfor (const auto &[key, value] : _soundOverrides) {\n\t\t\tstream << key << value;\n\t\t}\n\t\tstream\n\t\t\t<< qint32(_sendFilesWay.serialize())\n\t\t\t<< qint32(_sendSubmitWay.current())\n\t\t\t<< qint32(_includeMutedCounter ? 1 : 0)\n\t\t\t<< qint32(_countUnreadMessages ? 1 : 0)\n\t\t\t<< qint32(1) // legacy exe launch warning\n\t\t\t<< qint32(_notifyAboutPinned.current() ? 1 : 0)\n\t\t\t<< qint32(_loopAnimatedStickers ? 1 : 0)\n\t\t\t<< qint32(_largeEmoji.current() ? 1 : 0)\n\t\t\t<< qint32(_replaceEmoji.current() ? 1 : 0)\n\t\t\t<< qint32(_suggestEmoji ? 1 : 0)\n\t\t\t// Fork Settings.\n\t\t\t<< qint32(0)\n\t\t\t<< qint32(0)\n\t\t\t<< qint32(0)\n\t\t\t<< qint32(0)\n\t\t\t<< QString()\n\t\t\t<< QString()\n\t\t\t<< qint32(0)\n\t\t\t<< qint32(0)\n\t\t\t<< qint32(0)\n\t\t\t<< qint32(0)\n\t\t\t<< qint32(0)\n\t\t\t//\n\t\t\t<< qint32(_suggestStickersByEmoji ? 1 : 0)\n\t\t\t<< qint32(_spellcheckerEnabled.current() ? 1 : 0)\n\t\t\t<< qint32(SerializePlaybackSpeed(_videoPlaybackSpeed))\n\t\t\t<< _videoPipGeometry\n\t\t\t<< qint32(_dictionariesEnabled.current().size());\n\t\tfor (const auto i : _dictionariesEnabled.current()) {\n\t\t\tstream << quint64(i);\n\t\t}\n\t\tstream\n\t\t\t<< qint32(_autoDownloadDictionaries.current() ? 1 : 0)\n\t\t\t<< qint32(_mainMenuAccountsShown.current() ? 1 : 0)\n\t\t\t<< qint32(_tabbedSelectorSectionEnabled ? 1 : 0)\n\t\t\t<< qint32(_floatPlayerColumn)\n\t\t\t<< qint32(_floatPlayerCorner)\n\t\t\t<< qint32(_thirdSectionInfoEnabled ? 1 : 0)\n\t\t\t<< qint32(std::clamp(\n\t\t\t\tqRound(_dialogsWithChatWidthRatio.current() * 1000000),\n\t\t\t\t0,\n\t\t\t\t1000000))\n\t\t\t<< qint32(_thirdColumnWidth.current())\n\t\t\t<< qint32(_thirdSectionExtendedBy)\n\t\t\t<< qint32(_notifyFromAll ? 1 : 0)\n\t\t\t<< qint32(_nativeWindowFrame.current() ? 1 : 0)\n\t\t\t<< qint32(0) // Legacy system dark mode\n\t\t\t<< _cameraDeviceId.current()\n\t\t\t<< qint32(_ipRevealWarning ? 1 : 0)\n\t\t\t<< qint32(_groupCallPushToTalk ? 1 : 0)\n\t\t\t<< _groupCallPushToTalkShortcut\n\t\t\t<< qint64(_groupCallPushToTalkDelay)\n\t\t\t<< qint32(0) // Call audio backend\n\t\t\t<< qint32(0) // Legacy disable calls, now in session settings\n\t\t\t<< windowPosition\n\t\t\t<< qint32(recentEmojiPreloadData.size());\n\t\tfor (const auto &[id, rating] : recentEmojiPreloadData) {\n\t\t\tstream << id << quint16(rating);\n\t\t}\n\t\tstream\n\t\t\t<< qint32(_emojiVariants.size());\n\t\tfor (const auto &[id, variant] : _emojiVariants) {\n\t\t\tstream << id << quint8(variant);\n\t\t}\n\t\tstream\n\t\t\t<< qint32(0) // Old Disable OpenGL\n\t\t\t<< qint32(0) // Old Noise Suppression\n\t\t\t<< qint32(_workMode.current())\n\t\t\t<< proxy\n\t\t\t<< qint32(_hiddenGroupCallTooltips.value())\n\t\t\t<< qint32(_disableOpenGL ? 1 : 0)\n\t\t\t<< _photoEditorBrush\n\t\t\t<< qint32(_groupCallNoiseSuppression ? 1 : 0)\n\t\t\t<< qint32(SerializePlaybackSpeed(_voicePlaybackSpeed.current()))\n\t\t\t<< qint32(_closeBehavior)\n\t\t\t<< _customDeviceModel.current()\n\t\t\t<< qint32(_playerRepeatMode.current())\n\t\t\t<< qint32(_playerOrderMode.current())\n\t\t\t<< qint32(_macWarnBeforeQuit ? 1 : 0);\n\n\t\tstream\n\t\t\t<< qint32(_accountsOrder.size());\n\t\tfor (const auto &id : _accountsOrder) {\n\t\t\tstream << quint64(id);\n\t\t}\n\n\t\tstream\n\t\t\t<< qint32(0) // old hardwareAcceleratedVideo\n\t\t\t<< qint32(_chatQuickAction)\n\t\t\t<< qint32(_hardwareAcceleratedVideo ? 1 : 0)\n\t\t\t<< qint32(_suggestAnimatedEmoji ? 1 : 0)\n\t\t\t<< qint32(_cornerReaction.current() ? 1 : 0)\n\t\t\t<< qint32(_translateButtonEnabled ? 1 : 0);\n\n\t\tstream\n\t\t\t<< qint32(skipLanguages.size());\n\t\tfor (const auto &id : skipLanguages) {\n\t\t\tstream << quint64(id.value);\n\t\t}\n\n\t\tstream\n\t\t\t<< qint32(_rememberedDeleteMessageOnlyForYou ? 1 : 0)\n\t\t\t<< qint32(_translateChatEnabled.current() ? 1 : 0)\n\t\t\t<< quint64(QLocale::Language(_translateToRaw.current()))\n\t\t\t<< qint32(_windowTitleContent.current().hideChatName ? 1 : 0)\n\t\t\t<< qint32(_windowTitleContent.current().hideAccountName ? 1 : 0)\n\t\t\t<< qint32(_windowTitleContent.current().hideTotalUnread ? 1 : 0)\n\t\t\t<< mediaViewPosition\n\t\t\t<< qint32(_ignoreBatterySaving.current() ? 1 : 0)\n\t\t\t<< quint64(_macRoundIconDigest.value_or(0))\n\t\t\t<< qint32(_storiesClickTooltipHidden.current() ? 1 : 0)\n\t\t\t<< qint32(_recentEmojiSkip.size());\n\t\tfor (const auto &id : _recentEmojiSkip) {\n\t\t\tstream << id;\n\t\t}\n\t\tstream\n\t\t\t<< qint32(_trayIconMonochrome.current() ? 1 : 0)\n\t\t\t<< qint32(_ttlVoiceClickTooltipHidden.current() ? 1 : 0)\n\t\t\t<< _playbackDeviceId.current()\n\t\t\t<< _captureDeviceId.current()\n\t\t\t<< _callPlaybackDeviceId.current()\n\t\t\t<< _callCaptureDeviceId.current()\n\t\t\t<< ivPosition\n\t\t\t<< noWarningExtensions\n\t\t\t<< _customFontFamily\n\t\t\t<< qint32(std::clamp(\n\t\t\t\tqRound(_dialogsNoChatWidthRatio.current() * 1000000),\n\t\t\t\t0,\n\t\t\t\t1000000))\n\t\t\t<< qint32(_systemUnlockEnabled ? 1 : 0)\n\t\t\t<< qint32(!_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2)\n\t\t\t<< _tonsiteStorageToken\n\t\t\t<< qint32(_includeMutedCounterFolders ? 1 : 0)\n\t\t\t<< qint32(_chatFiltersHorizontal.current() ? 1 : 0)\n\t\t\t<< qint32(_skipToastsInFocus ? 1 : 0)\n\t\t\t<< qint32(_recordVideoMessages ? 1 : 0)\n\t\t\t<< SerializeVideoQuality(_videoQuality)\n\t\t\t<< qint32(_ivZoom.current())\n\t\t\t<< qint32(_systemDarkModeEnabled.current() ? 1 : 0)\n\t\t\t<< qint32(_quickDialogAction)\n\t\t\t<< _notificationsVolume\n\t\t\t<< _notificationsDisplayChecksum\n\t\t\t<< callPanelPosition\n\t\t\t<< qint32(_cornerReply.current() ? 1 : 0)\n\t\t\t<< qint32(_systemAccentColorEnabled ? 1 : 0)\n\t\t\t<< qint32(_usePlatformTranslation ? 1 : 0)\n\t\t\t<< qint32(_systemTextReplace.current() ? 1 : 0);\n\t\tstream << quint32(_prefs.size());\n\t\tfor (const auto &[key, value] : _prefs) {\n\t\t\tstream << key << value;\n\t\t}\n\t\tstream << qint32(SerializePlaybackSpeed(_audioPlaybackSpeed.current()));\n\t}\n\n\tEnsures(result.size() == size);\n\treturn result;\n}\n\nvoid Settings::addFromSerialized(const QByteArray &serialized) {\n\tif (serialized.isEmpty()) {\n\t\treturn;\n\t}\n\n\tQDataStream stream(serialized);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\n\t// Old.\n\tqint32 squareUserpics = 0;\n\tqint32 audioFade = 0;\n\tqint32 askUriScheme = 0;\n\tqint32 lastSeenInDialogs = 0;\n\tQString uriScheme = {};\n\tQString searchEngineUrl = {};\n\tqint32 searchEngine = 0;\n\tqint32 allRecentStickers = 0;\n\tqint32 customStickerSize = 0;\n\tqint32 useBlackTrayIcon = 0;\n\tqint32 useOriginalTrayIcon = 0;\n\n\tQByteArray themesAccentColors;\n\tqint32 adaptiveForWide = _adaptiveForWide.current() ? 1 : 0;\n\tqint32 moderateModeEnabled = _moderateModeEnabled ? 1 : 0;\n\tqint32 songVolume = qint32(qRound(_songVolume.current() * 1e6));\n\tqint32 videoVolume = qint32(qRound(_videoVolume.current() * 1e6));\n\tqint32 askDownloadPath = _askDownloadPath ? 1 : 0;\n\tQString downloadPath = _downloadPath.current();\n\tQByteArray downloadPathBookmark = _downloadPathBookmark;\n\tqint32 nonDefaultVoicePlaybackSpeed = 1;\n\tqint32 soundNotify = _soundNotify ? 1 : 0;\n\tqint32 desktopNotify = _desktopNotify ? 1 : 0;\n\tqint32 flashBounceNotify = _flashBounceNotify ? 1 : 0;\n\tqint32 notifyView = static_cast<qint32>(_notifyView);\n\tqint32 nativeNotifications = _nativeNotifications ? (*_nativeNotifications ? 1 : 2) : 0;\n\tqint32 notificationsCount = _notificationsCount;\n\tqint32 notificationsCorner = static_cast<qint32>(_notificationsCorner);\n\tqint32 notificationsDisplayChecksum = _notificationsDisplayChecksum;\n\tqint32 autoLock = _autoLock;\n\tQString playbackDeviceId = _playbackDeviceId.current();\n\tQString captureDeviceId = _captureDeviceId.current();\n\tQString cameraDeviceId = _cameraDeviceId.current();\n\tQString legacyCallPlaybackDeviceId = _callPlaybackDeviceId.current();\n\tQString legacyCallCaptureDeviceId = _callCaptureDeviceId.current();\n\tQString callPlaybackDeviceId = _callPlaybackDeviceId.current();\n\tQString callCaptureDeviceId = _callCaptureDeviceId.current();\n\tqint32 callOutputVolume = _callOutputVolume;\n\tqint32 callInputVolume = _callInputVolume;\n\tqint32 callAudioDuckingEnabled = _callAudioDuckingEnabled ? 1 : 0;\n\tqint32 lastSeenWarningSeen = _lastSeenWarningSeen ? 1 : 0;\n\tqint32 soundOverridesCount = 0;\n\tbase::flat_map<QString, QString> soundOverrides;\n\tqint32 sendFilesWay = _sendFilesWay.serialize();\n\tqint32 sendSubmitWay = static_cast<qint32>(_sendSubmitWay.current());\n\tqint32 includeMutedCounter = _includeMutedCounter ? 1 : 0;\n\tqint32 includeMutedCounterFolders = _includeMutedCounterFolders ? 1 : 0;\n\tqint32 countUnreadMessages = _countUnreadMessages ? 1 : 0;\n\tstd::optional<QString> noWarningExtensions;\n\tqint32 legacyExeLaunchWarning = 1;\n\tqint32 notifyAboutPinned = _notifyAboutPinned.current() ? 1 : 0;\n\tqint32 loopAnimatedStickers = _loopAnimatedStickers ? 1 : 0;\n\tqint32 largeEmoji = _largeEmoji.current() ? 1 : 0;\n\tqint32 replaceEmoji = _replaceEmoji.current() ? 1 : 0;\n\tqint32 suggestEmoji = _suggestEmoji ? 1 : 0;\n\tqint32 suggestStickersByEmoji = _suggestStickersByEmoji ? 1 : 0;\n\tqint32 spellcheckerEnabled = _spellcheckerEnabled.current() ? 1 : 0;\n\tqint32 videoPlaybackSpeed = SerializePlaybackSpeed(_videoPlaybackSpeed);\n\tqint32 voicePlaybackSpeed = SerializePlaybackSpeed(\n\t\t_voicePlaybackSpeed.current());\n\tauto audioPlaybackSpeed = std::optional<qint32>();\n\tQByteArray videoPipGeometry = _videoPipGeometry;\n\tqint32 dictionariesEnabledCount = 0;\n\tstd::vector<int> dictionariesEnabled;\n\tqint32 autoDownloadDictionaries = _autoDownloadDictionaries.current() ? 1 : 0;\n\tqint32 mainMenuAccountsShown = _mainMenuAccountsShown.current() ? 1 : 0;\n\tqint32 tabbedSelectorSectionEnabled = 1;\n\tqint32 floatPlayerColumn = static_cast<qint32>(Window::Column::Second);\n\tqint32 floatPlayerCorner = static_cast<qint32>(RectPart::TopRight);\n\tqint32 thirdSectionInfoEnabled = 0;\n\tfloat64 dialogsWithChatWidthRatio = _dialogsWithChatWidthRatio.current();\n\tfloat64 dialogsNoChatWidthRatio = _dialogsNoChatWidthRatio.current();\n\tqint32 thirdColumnWidth = _thirdColumnWidth.current();\n\tqint32 thirdSectionExtendedBy = _thirdSectionExtendedBy;\n\tqint32 notifyFromAll = _notifyFromAll ? 1 : 0;\n\tqint32 nativeWindowFrame = _nativeWindowFrame.current() ? 1 : 0;\n\tqint32 systemDarkModeEnabled = _systemDarkModeEnabled.current() ? 1 : 0;\n\tqint32 ipRevealWarning = _ipRevealWarning ? 1 : 0;\n\tqint32 groupCallPushToTalk = _groupCallPushToTalk ? 1 : 0;\n\tQByteArray groupCallPushToTalkShortcut = _groupCallPushToTalkShortcut;\n\tqint64 groupCallPushToTalkDelay = _groupCallPushToTalkDelay;\n\tqint32 legacyCallAudioBackend = 0;\n\tqint32 disableCallsLegacy = 0;\n\tQByteArray windowPosition;\n\tstd::vector<RecentEmojiPreload> recentEmojiPreload;\n\tbase::flat_map<QString, uint8> emojiVariants;\n\tqint32 disableOpenGL = _disableOpenGL ? 1 : 0;\n\tqint32 groupCallNoiseSuppression = _groupCallNoiseSuppression ? 1 : 0;\n\tqint32 workMode = static_cast<qint32>(_workMode.current());\n\tQByteArray proxy;\n\tqint32 hiddenGroupCallTooltips = qint32(_hiddenGroupCallTooltips.value());\n\tQByteArray photoEditorBrush = _photoEditorBrush;\n\tqint32 closeBehavior = qint32(_closeBehavior);\n\tQString customDeviceModel = _customDeviceModel.current();\n\tqint32 playerRepeatMode = static_cast<qint32>(_playerRepeatMode.current());\n\tqint32 playerOrderMode = static_cast<qint32>(_playerOrderMode.current());\n\tqint32 macWarnBeforeQuit = _macWarnBeforeQuit ? 1 : 0;\n\tqint32 accountsOrderCount = 0;\n\tstd::vector<uint64> accountsOrder;\n\tqint32 hardwareAcceleratedVideo = _hardwareAcceleratedVideo ? 1 : 0;\n\tqint32 chatQuickAction = static_cast<qint32>(_chatQuickAction);\n\tqint32 suggestAnimatedEmoji = _suggestAnimatedEmoji ? 1 : 0;\n\tqint32 cornerReply = _cornerReply.current() ? 1 : 0;\n\tqint32 cornerReaction = _cornerReaction.current() ? 1 : 0;\n\tqint32 legacySkipTranslationForLanguage = _translateButtonEnabled ? 1 : 0;\n\tqint32 skipTranslationLanguagesCount = 0;\n\tstd::vector<LanguageId> skipTranslationLanguages;\n\tqint32 rememberedDeleteMessageOnlyForYou = _rememberedDeleteMessageOnlyForYou ? 1 : 0;\n\tqint32 translateChatEnabled = _translateChatEnabled.current() ? 1 : 0;\n\tquint64 translateToRaw = _translateToRaw.current();\n\tqint32 hideChatName = _windowTitleContent.current().hideChatName ? 1 : 0;\n\tqint32 hideAccountName = _windowTitleContent.current().hideAccountName ? 1 : 0;\n\tqint32 hideTotalUnread = _windowTitleContent.current().hideTotalUnread ? 1 : 0;\n\tQByteArray mediaViewPosition;\n\tqint32 ignoreBatterySaving = _ignoreBatterySaving.current() ? 1 : 0;\n\tquint64 macRoundIconDigest = _macRoundIconDigest.value_or(0);\n\tqint32 storiesClickTooltipHidden = _storiesClickTooltipHidden.current() ? 1 : 0;\n\tbase::flat_set<QString> recentEmojiSkip;\n\tqint32 trayIconMonochrome = (_trayIconMonochrome.current() ? 1 : 0);\n\tqint32 ttlVoiceClickTooltipHidden = _ttlVoiceClickTooltipHidden.current() ? 1 : 0;\n\tQByteArray ivPosition;\n\tQByteArray callPanelPosition;\n\tQString customFontFamily = _customFontFamily;\n\tqint32 systemUnlockEnabled = _systemUnlockEnabled ? 1 : 0;\n\tqint32 weatherInCelsius = !_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2;\n\tQByteArray tonsiteStorageToken = _tonsiteStorageToken;\n\tqint32 ivZoom = _ivZoom.current();\n\tqint32 skipToastsInFocus = _skipToastsInFocus ? 1 : 0;\n\tqint32 recordVideoMessages = _recordVideoMessages ? 1 : 0;\n\tquint32 videoQuality = SerializeVideoQuality(_videoQuality);\n\tquint32 chatFiltersHorizontal = _chatFiltersHorizontal.current() ? 1 : 0;\n\tquint32 quickDialogAction = quint32(_quickDialogAction);\n\tushort notificationsVolume = _notificationsVolume;\n\tqint32 systemAccentColorEnabled = _systemAccentColorEnabled\n\t\t? 1\n\t\t: 0;\n\tqint32 usePlatformTranslation = _usePlatformTranslation ? 1 : 0;\n\tqint32 systemTextReplace = _systemTextReplace.current() ? 1 : 0;\n\n\tstream >> themesAccentColors;\n\tif (!stream.atEnd()) {\n\t\tstream\n\t\t\t>> adaptiveForWide\n\t\t\t>> moderateModeEnabled\n\t\t\t>> songVolume\n\t\t\t>> videoVolume\n\t\t\t>> askDownloadPath\n\t\t\t>> downloadPath\n\t\t\t>> downloadPathBookmark\n\t\t\t>> nonDefaultVoicePlaybackSpeed\n\t\t\t>> soundNotify\n\t\t\t>> desktopNotify\n\t\t\t>> flashBounceNotify\n\t\t\t>> notifyView\n\t\t\t>> nativeNotifications\n\t\t\t>> notificationsCount\n\t\t\t>> notificationsCorner\n\t\t\t>> autoLock\n\t\t\t>> legacyCallPlaybackDeviceId\n\t\t\t>> legacyCallCaptureDeviceId\n\t\t\t>> callOutputVolume\n\t\t\t>> callInputVolume\n\t\t\t>> callAudioDuckingEnabled\n\t\t\t>> lastSeenWarningSeen\n\t\t\t>> soundOverridesCount;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != soundOverridesCount; ++i) {\n\t\t\t\tQString key, value;\n\t\t\t\tstream >> key >> value;\n\t\t\t\tsoundOverrides.emplace(key, value);\n\t\t\t}\n\t\t}\n\t\tstream\n\t\t\t>> sendFilesWay\n\t\t\t>> sendSubmitWay\n\t\t\t>> includeMutedCounter\n\t\t\t>> countUnreadMessages\n\t\t\t>> legacyExeLaunchWarning\n\t\t\t>> notifyAboutPinned\n\t\t\t>> loopAnimatedStickers\n\t\t\t>> largeEmoji\n\t\t\t>> replaceEmoji\n\t\t\t>> suggestEmoji\n\t\t\t// Old Fork Settings.\n\t\t\t>> squareUserpics\n\t\t\t>> audioFade\n\t\t\t>> askUriScheme\n\t\t\t>> lastSeenInDialogs\n\t\t\t>> uriScheme\n\t\t\t>> searchEngineUrl\n\t\t\t>> searchEngine\n\t\t\t>> allRecentStickers\n\t\t\t>> customStickerSize\n\t\t\t>> useBlackTrayIcon\n\t\t\t>> useOriginalTrayIcon\n\t\t\t//\n\t\t\t>> suggestStickersByEmoji\n\t\t\t>> spellcheckerEnabled\n\t\t\t>> videoPlaybackSpeed\n\t\t\t>> videoPipGeometry\n\t\t\t>> dictionariesEnabledCount;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != dictionariesEnabledCount; ++i) {\n\t\t\t\tqint64 langId;\n\t\t\t\tstream >> langId;\n\t\t\t\tdictionariesEnabled.emplace_back(langId);\n\t\t\t}\n\t\t}\n\t\tstream\n\t\t\t>> autoDownloadDictionaries\n\t\t\t>> mainMenuAccountsShown;\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto dialogsWithChatWidthRatioInt = qint32();\n\t\tstream\n\t\t\t>> tabbedSelectorSectionEnabled\n\t\t\t>> floatPlayerColumn\n\t\t\t>> floatPlayerCorner\n\t\t\t>> thirdSectionInfoEnabled\n\t\t\t>> dialogsWithChatWidthRatioInt\n\t\t\t>> thirdColumnWidth\n\t\t\t>> thirdSectionExtendedBy\n\t\t\t>> notifyFromAll;\n\t\tdialogsWithChatWidthRatio = std::clamp(\n\t\t\tdialogsWithChatWidthRatioInt / 1000000.,\n\t\t\t0.,\n\t\t\t1.);\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> nativeWindowFrame;\n\t}\n\tif (!stream.atEnd()) {\n\t\t// Read over this one below, if was in the file.\n\t\tstream >> systemDarkModeEnabled;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> cameraDeviceId;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> ipRevealWarning;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream\n\t\t\t>> groupCallPushToTalk\n\t\t\t>> groupCallPushToTalkShortcut\n\t\t\t>> groupCallPushToTalkDelay;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> legacyCallAudioBackend;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> disableCallsLegacy;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> windowPosition;\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto recentCount = qint32(0);\n\t\tstream >> recentCount;\n\t\tif (recentCount > 0 && recentCount < 10000) {\n\t\t\trecentEmojiPreload.reserve(recentCount);\n\t\t\tfor (auto i = 0; i != recentCount; ++i) {\n\t\t\t\tauto id = QString();\n\t\t\t\tauto rating = quint16();\n\t\t\t\tstream >> id >> rating;\n\t\t\t\trecentEmojiPreload.push_back({ id, rating });\n\t\t\t}\n\t\t}\n\t\tauto variantsCount = qint32(0);\n\t\tstream >> variantsCount;\n\t\tif (variantsCount > 0 && variantsCount < 10000) {\n\t\t\temojiVariants.reserve(variantsCount);\n\t\t\tfor (auto i = 0; i != variantsCount; ++i) {\n\t\t\t\tauto id = QString();\n\t\t\t\tauto variant = quint8();\n\t\t\t\tstream >> id >> variant;\n\t\t\t\temojiVariants.emplace(id, variant);\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tqint32 disableOpenGLOld;\n\t\tstream >> disableOpenGLOld;\n\t}\n\tif (!stream.atEnd()) {\n\t\tqint32 groupCallNoiseSuppressionOld;\n\t\tstream >> groupCallNoiseSuppressionOld;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> workMode;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> proxy;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> hiddenGroupCallTooltips;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> disableOpenGL;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> photoEditorBrush;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> groupCallNoiseSuppression;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> voicePlaybackSpeed;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> closeBehavior;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> customDeviceModel;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream\n\t\t\t>> playerRepeatMode\n\t\t\t>> playerOrderMode;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> macWarnBeforeQuit;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> accountsOrderCount;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != accountsOrderCount; ++i) {\n\t\t\t\tquint64 sessionUniqueId;\n\t\t\t\tstream >> sessionUniqueId;\n\t\t\t\taccountsOrder.emplace_back(sessionUniqueId);\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tqint32 legacyHardwareAcceleratedVideo = 0;\n\t\tstream >> legacyHardwareAcceleratedVideo;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> chatQuickAction;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> hardwareAcceleratedVideo;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> suggestAnimatedEmoji;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> cornerReaction;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> legacySkipTranslationForLanguage;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> skipTranslationLanguagesCount;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != skipTranslationLanguagesCount; ++i) {\n\t\t\t\tquint64 language;\n\t\t\t\tstream >> language;\n\t\t\t\tskipTranslationLanguages.push_back({\n\t\t\t\t\tQLocale::Language(language)\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> rememberedDeleteMessageOnlyForYou;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream\n\t\t\t>> translateChatEnabled\n\t\t\t>> translateToRaw;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream\n\t\t\t>> hideChatName\n\t\t\t>> hideAccountName\n\t\t\t>> hideTotalUnread;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> mediaViewPosition;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> ignoreBatterySaving;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> macRoundIconDigest;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> storiesClickTooltipHidden;\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32();\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tauto id = QString();\n\t\t\t\tstream >> id;\n\t\t\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\t\t\trecentEmojiSkip.emplace(id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> trayIconMonochrome;\n\t} else {\n\t\t// Let existing clients use the old value.\n\t\ttrayIconMonochrome = 0;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> ttlVoiceClickTooltipHidden;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream\n\t\t\t>> playbackDeviceId\n\t\t\t>> captureDeviceId;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream\n\t\t\t>> callPlaybackDeviceId\n\t\t\t>> callCaptureDeviceId;\n\t} else {\n\t\tconst auto &defaultId = Webrtc::kDefaultDeviceId;\n\t\tcallPlaybackDeviceId = (legacyCallPlaybackDeviceId == defaultId)\n\t\t\t? QString()\n\t\t\t: legacyCallPlaybackDeviceId;\n\t\tcallCaptureDeviceId = (legacyCallCaptureDeviceId == defaultId)\n\t\t\t? QString()\n\t\t\t: legacyCallCaptureDeviceId;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> ivPosition;\n\t}\n\tif (!stream.atEnd()) {\n\t\tnoWarningExtensions = QString();\n\t\tstream >> *noWarningExtensions;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> customFontFamily;\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto dialogsNoChatWidthRatioInt = qint32();\n\t\tstream\n\t\t\t>> dialogsNoChatWidthRatioInt;\n\t\tdialogsNoChatWidthRatio = std::clamp(\n\t\t\tdialogsNoChatWidthRatioInt / 1000000.,\n\t\t\t0.,\n\t\t\t1.);\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> systemUnlockEnabled;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> weatherInCelsius;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> tonsiteStorageToken;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> includeMutedCounterFolders;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> chatFiltersHorizontal;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> skipToastsInFocus;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> recordVideoMessages;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> videoQuality;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> ivZoom;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> systemDarkModeEnabled;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> quickDialogAction;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> notificationsVolume;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> notificationsDisplayChecksum;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> callPanelPosition;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> cornerReply;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> systemAccentColorEnabled;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> usePlatformTranslation;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> systemTextReplace;\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto prefsCount = quint32();\n\t\tstream >> prefsCount;\n\t\tauto prefs = base::flat_map<QByteArray, QByteArray>();\n\t\tprefs.reserve(prefsCount);\n\t\tfor (auto i = quint32(); i != prefsCount; ++i) {\n\t\t\tauto key = QByteArray();\n\t\t\tauto value = QByteArray();\n\t\t\tstream >> key >> value;\n\t\t\tprefs.emplace(std::move(key), std::move(value));\n\t\t}\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\t_prefs = std::move(prefs);\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto speed = qint32();\n\t\tstream >> speed;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\taudioPlaybackSpeed = speed;\n\t\t}\n\t}\n\tif (stream.status() != QDataStream::Ok) {\n\t\tLOG((\"App Error: \"\n\t\t\t\"Bad data for Core::Settings::constructFromSerialized()\"));\n\t\treturn;\n\t} else if (!_themesAccentColors.setFromSerialized(themesAccentColors)) {\n\t\treturn;\n\t} else if (!_proxy.setFromSerialized(proxy)) {\n\t\treturn;\n\t}\n\t_adaptiveForWide = (adaptiveForWide == 1);\n\t_moderateModeEnabled = (moderateModeEnabled == 1);\n\t_songVolume = std::clamp(songVolume / 1e6, 0., 1.);\n\t_videoVolume = std::clamp(videoVolume / 1e6, 0., 1.);\n\t_askDownloadPath = (askDownloadPath == 1);\n\t_downloadPath = downloadPath;\n\t_downloadPathBookmark = downloadPathBookmark;\n\t_soundNotify = (soundNotify == 1);\n\t_desktopNotify = (desktopNotify == 1);\n\t_flashBounceNotify = (flashBounceNotify == 1);\n\tconst auto uncheckedNotifyView = static_cast<NotifyView>(notifyView);\n\tswitch (uncheckedNotifyView) {\n\tcase NotifyView::ShowNothing:\n\tcase NotifyView::ShowName:\n\tcase NotifyView::ShowPreview: _notifyView = uncheckedNotifyView; break;\n\t}\n\tswitch (nativeNotifications) {\n\tcase 0: _nativeNotifications = std::nullopt; break;\n\tcase 1: _nativeNotifications = true; break;\n\tcase 2: _nativeNotifications = false; break;\n\tdefault: break;\n\t}\n\t_notificationsCount = (notificationsCount > 0) ? notificationsCount : 3;\n\tconst auto uncheckedNotificationsCorner = static_cast<ScreenCorner>(notificationsCorner);\n\tswitch (uncheckedNotificationsCorner) {\n\tcase ScreenCorner::TopLeft:\n\tcase ScreenCorner::TopRight:\n\tcase ScreenCorner::BottomRight:\n\tcase ScreenCorner::BottomLeft: _notificationsCorner = uncheckedNotificationsCorner; break;\n\t}\n\t_notificationsDisplayChecksum = notificationsDisplayChecksum;\n\t_systemAccentColorEnabled = (systemAccentColorEnabled == 1);\n\t_usePlatformTranslation = (usePlatformTranslation == 1);\n\t_includeMutedCounter = (includeMutedCounter == 1);\n\t_includeMutedCounterFolders = (includeMutedCounterFolders == 1);\n\t_countUnreadMessages = (countUnreadMessages == 1);\n\t_notifyAboutPinned = (notifyAboutPinned == 1);\n\t_autoLock = autoLock;\n\t_playbackDeviceId = playbackDeviceId;\n\t_captureDeviceId = captureDeviceId;\n\tconst auto kOldDefault = u\"default\"_q;\n\t_cameraDeviceId = cameraDeviceId;\n\t_callPlaybackDeviceId = callPlaybackDeviceId;\n\t_callCaptureDeviceId = callCaptureDeviceId;\n\t_callOutputVolume = callOutputVolume;\n\t_callInputVolume = callInputVolume;\n\t_callAudioDuckingEnabled = (callAudioDuckingEnabled == 1);\n\t_lastSeenWarningSeen = (lastSeenWarningSeen == 1);\n\t_soundOverrides = std::move(soundOverrides);\n\t_sendFilesWay = Ui::SendFilesWay::FromSerialized(sendFilesWay).value_or(_sendFilesWay);\n\tauto uncheckedSendSubmitWay = static_cast<Ui::InputSubmitSettings>(sendSubmitWay);\n\tswitch (uncheckedSendSubmitWay) {\n\tcase Ui::InputSubmitSettings::Enter:\n\tcase Ui::InputSubmitSettings::CtrlEnter: _sendSubmitWay = uncheckedSendSubmitWay; break;\n\t}\n\tif (noWarningExtensions) {\n\t\tconst auto list = noWarningExtensions->mid(0, 10240)\n\t\t\t.split(' ', Qt::SkipEmptyParts)\n\t\t\t.mid(0, 1024);\n\t\t_noWarningExtensions = base::flat_set<QString>(list.begin(), list.end());\n\t}\n\t_ipRevealWarning = (ipRevealWarning == 1);\n\t_notifyAboutPinned = (notifyAboutPinned == 1);\n\t_loopAnimatedStickers = (loopAnimatedStickers == 1);\n\t_largeEmoji = (largeEmoji == 1);\n\t_replaceEmoji = (replaceEmoji == 1);\n\t_systemTextReplace = (systemTextReplace == 1);\n\t_suggestEmoji = (suggestEmoji == 1);\n\t_suggestStickersByEmoji = (suggestStickersByEmoji == 1);\n\t_spellcheckerEnabled = (spellcheckerEnabled == 1);\n\t_videoPlaybackSpeed = DeserializePlaybackSpeed(videoPlaybackSpeed);\n\t{\n\t\tauto speed = DeserializePlaybackSpeed(voicePlaybackSpeed);\n\t\tif (nonDefaultVoicePlaybackSpeed != 1) {\n\t\t\tspeed.enabled = false;\n\t\t}\n\t\t_voicePlaybackSpeed = speed;\n\t\t_audioPlaybackSpeed = audioPlaybackSpeed\n\t\t\t? DeserializePlaybackSpeed(*audioPlaybackSpeed)\n\t\t\t: speed;\n\t}\n\t_videoPipGeometry = (videoPipGeometry);\n\t_dictionariesEnabled = std::move(dictionariesEnabled);\n\t_autoDownloadDictionaries = (autoDownloadDictionaries == 1);\n\t_mainMenuAccountsShown = (mainMenuAccountsShown == 1);\n\n\t// Old.\n\t_forkSettings.setSquareUserpics(squareUserpics == 1);\n\t_forkSettings.setAudioFade(audioFade == 1);\n\t_forkSettings.setAskUriScheme(askUriScheme == 1);\n\t_forkSettings.setLastSeenInDialogs(lastSeenInDialogs == 1);\n\t_forkSettings.setUriScheme(uriScheme);\n\t_forkSettings.setSearchEngineUrl(searchEngineUrl);\n\t_forkSettings.setSearchEngine(searchEngine == 1);\n\t_forkSettings.setAllRecentStickers(allRecentStickers == 1);\n\t_forkSettings.setCustomStickerSize(customStickerSize);\n\t_forkSettings.setUseBlackTrayIcon(useBlackTrayIcon == 1);\n\t_forkSettings.setUseOriginalTrayIcon(useOriginalTrayIcon == 1);\n\n\t_tabbedSelectorSectionEnabled = (tabbedSelectorSectionEnabled == 1);\n\tauto uncheckedColumn = static_cast<Window::Column>(floatPlayerColumn);\n\tswitch (uncheckedColumn) {\n\tcase Window::Column::First:\n\tcase Window::Column::Second:\n\tcase Window::Column::Third: _floatPlayerColumn = uncheckedColumn; break;\n\t}\n\tauto uncheckedCorner = static_cast<RectPart>(floatPlayerCorner);\n\tswitch (uncheckedCorner) {\n\tcase RectPart::TopLeft:\n\tcase RectPart::TopRight:\n\tcase RectPart::BottomLeft:\n\tcase RectPart::BottomRight: _floatPlayerCorner = uncheckedCorner; break;\n\t}\n\t_thirdSectionInfoEnabled = thirdSectionInfoEnabled;\n\t_dialogsWithChatWidthRatio = dialogsWithChatWidthRatio;\n\t_dialogsNoChatWidthRatio = (dialogsWithChatWidthRatio > 0)\n\t\t? dialogsWithChatWidthRatio\n\t\t: dialogsNoChatWidthRatio;\n\t_thirdColumnWidth = thirdColumnWidth;\n\t_thirdSectionExtendedBy = thirdSectionExtendedBy;\n\tif (_thirdSectionInfoEnabled) {\n\t\t_tabbedSelectorSectionEnabled = false;\n\t}\n\t_notifyFromAll = (notifyFromAll == 1);\n\t_nativeWindowFrame = (nativeWindowFrame == 1);\n\t_systemDarkModeEnabled = (systemDarkModeEnabled == 1);\n\t_groupCallPushToTalk = (groupCallPushToTalk == 1);\n\t_groupCallPushToTalkShortcut = groupCallPushToTalkShortcut;\n\t_groupCallPushToTalkDelay = groupCallPushToTalkDelay;\n\t_disableCallsLegacy = (disableCallsLegacy == 1);\n\tif (!windowPosition.isEmpty()) {\n\t\t_windowPosition = Deserialize(windowPosition);\n\t}\n\t_recentEmojiPreload = std::move(recentEmojiPreload);\n\t_emojiVariants = std::move(emojiVariants);\n\t_disableOpenGL = (disableOpenGL == 1);\n\tUi::GL::ForceDisable(_disableOpenGL);\n\t_groupCallNoiseSuppression = (groupCallNoiseSuppression == 1);\n\tconst auto uncheckedWorkMode = static_cast<WorkMode>(workMode);\n\tswitch (uncheckedWorkMode) {\n\tcase WorkMode::WindowAndTray:\n\tcase WorkMode::TrayOnly:\n\tcase WorkMode::WindowOnly: _workMode = uncheckedWorkMode; break;\n\t}\n\t_hiddenGroupCallTooltips = [&] {\n\t\tusing Tooltip = Calls::Group::StickedTooltip;\n\t\treturn Tooltip(0)\n\t\t\t| ((hiddenGroupCallTooltips & int(Tooltip::Camera))\n\t\t\t\t? Tooltip::Camera\n\t\t\t\t: Tooltip(0))\n\t\t\t| ((hiddenGroupCallTooltips & int(Tooltip::Microphone))\n\t\t\t\t? Tooltip::Microphone\n\t\t\t\t: Tooltip(0));\n\t}();\n\t_photoEditorBrush = photoEditorBrush;\n\tconst auto uncheckedCloseBehavior = static_cast<CloseBehavior>(closeBehavior);\n\tswitch (uncheckedCloseBehavior) {\n\tcase CloseBehavior::CloseToTaskbar:\n\tcase CloseBehavior::RunInBackground:\n\tcase CloseBehavior::Quit: _closeBehavior = uncheckedCloseBehavior; break;\n\t}\n\t_customDeviceModel = customDeviceModel;\n\t_accountsOrder = accountsOrder;\n\tconst auto uncheckedPlayerRepeatMode = static_cast<Media::RepeatMode>(playerRepeatMode);\n\tswitch (uncheckedPlayerRepeatMode) {\n\tcase Media::RepeatMode::None:\n\tcase Media::RepeatMode::One:\n\tcase Media::RepeatMode::All: _playerRepeatMode = uncheckedPlayerRepeatMode; break;\n\t}\n\tconst auto uncheckedPlayerOrderMode = static_cast<Media::OrderMode>(playerOrderMode);\n\tswitch (uncheckedPlayerOrderMode) {\n\tcase Media::OrderMode::Default:\n\tcase Media::OrderMode::Reverse:\n\tcase Media::OrderMode::Shuffle: _playerOrderMode = uncheckedPlayerOrderMode; break;\n\t}\n\t_macWarnBeforeQuit = (macWarnBeforeQuit == 1);\n\t_hardwareAcceleratedVideo = (hardwareAcceleratedVideo == 1);\n\t{\n\t\tusing Quick = HistoryView::DoubleClickQuickAction;\n\t\tconst auto uncheckedChatQuickAction = static_cast<Quick>(\n\t\t\tchatQuickAction);\n\t\tswitch (uncheckedChatQuickAction) {\n\t\tcase Quick::None:\n\t\tcase Quick::Reply:\n\t\tcase Quick::React: _chatQuickAction = uncheckedChatQuickAction; break;\n\t\t}\n\t}\n\t_suggestAnimatedEmoji = (suggestAnimatedEmoji == 1);\n\t_cornerReply = (cornerReply == 1);\n\t_cornerReaction = (cornerReaction == 1);\n\t{ // Parse the legacy translation setting.\n\t\tif (legacySkipTranslationForLanguage == 0) {\n\t\t\t_translateButtonEnabled = false;\n\t\t} else if (legacySkipTranslationForLanguage == 1) {\n\t\t\t_translateButtonEnabled = true;\n\t\t} else {\n\t\t\t_translateButtonEnabled = (legacySkipTranslationForLanguage > 0);\n\t\t\tskipTranslationLanguages.push_back({\n\t\t\t\tQLocale::Language(std::abs(legacySkipTranslationForLanguage))\n\t\t\t});\n\t\t}\n\t\t_skipTranslationLanguages = std::move(skipTranslationLanguages);\n\t}\n\t_rememberedDeleteMessageOnlyForYou = (rememberedDeleteMessageOnlyForYou == 1);\n\t_translateChatEnabled = (translateChatEnabled == 1);\n\t_translateToRaw = int(QLocale::Language(translateToRaw));\n\t_windowTitleContent = WindowTitleContent{\n\t\t.hideChatName = (hideChatName == 1),\n\t\t.hideAccountName = (hideAccountName == 1),\n\t\t.hideTotalUnread = (hideTotalUnread == 1),\n\t};\n\tif (!mediaViewPosition.isEmpty()) {\n\t\t_mediaViewPosition = Deserialize(mediaViewPosition);\n\t\tif (!_mediaViewPosition.w && !_mediaViewPosition.maximized) {\n\t\t\t_mediaViewPosition = { .maximized = 2 };\n\t\t}\n\t}\n\t_ignoreBatterySaving = (ignoreBatterySaving == 1);\n\t_macRoundIconDigest = macRoundIconDigest ? macRoundIconDigest : std::optional<uint64>();\n\t_storiesClickTooltipHidden = (storiesClickTooltipHidden == 1);\n\t_recentEmojiSkip = std::move(recentEmojiSkip);\n\t_trayIconMonochrome = (trayIconMonochrome == 1);\n\t_ttlVoiceClickTooltipHidden = (ttlVoiceClickTooltipHidden == 1);\n\tif (!ivPosition.isEmpty()) {\n\t\t_ivPosition = Deserialize(ivPosition);\n\t}\n\tif (!callPanelPosition.isEmpty()) {\n\t\t_callPanelPosition = Deserialize(callPanelPosition);\n\t}\n\t_customFontFamily = customFontFamily;\n\t_systemUnlockEnabled = (systemUnlockEnabled == 1);\n\t_weatherInCelsius = !weatherInCelsius\n\t\t? std::optional<bool>()\n\t\t: (weatherInCelsius == 1);\n\t_tonsiteStorageToken = tonsiteStorageToken;\n\t_ivZoom = ivZoom;\n\t_skipToastsInFocus = (skipToastsInFocus == 1);\n\t_recordVideoMessages = (recordVideoMessages == 1);\n\t_videoQuality = DeserializeVideoQuality(videoQuality);\n\t_chatFiltersHorizontal = (chatFiltersHorizontal == 1);\n\t_quickDialogAction = Dialogs::Ui::QuickDialogAction(quickDialogAction);\n\t_notificationsVolume = notificationsVolume;\n}\n\nvoid Settings::clearPref(std::string_view key) {\n\tconst auto i = _prefs.find(QByteArray(key.data(), key.size()));\n\tif (i == end(_prefs)) {\n\t\treturn;\n\t}\n\t_prefs.erase(i);\n\t_saveDelayed.fire({});\n}\n\nvoid Settings::writePrefGeneric(\n\t\tstd::string_view key,\n\t\tconst QByteArray &value) {\n\tconst auto raw = QByteArray(key.data(), key.size());\n\tif (const auto i = _prefs.find(raw); i != end(_prefs)) {\n\t\tif (i->second == value) {\n\t\t\treturn;\n\t\t}\n\t\ti->second = value;\n\t} else {\n\t\t_prefs.emplace(raw, value);\n\t}\n\t_saveDelayed.fire({});\n}\n\nstd::optional<QByteArray> Settings::readPrefGeneric(std::string_view key) {\n\tconst auto i = _prefs.find(QByteArray(key.data(), key.size()));\n\treturn (i != end(_prefs)) ? i->second : std::optional<QByteArray>();\n}\n\ntemplate <>\nstd::optional<bool> Settings::readPrefImpl<bool>(std::string_view key) {\n\tif (const auto data = readPrefGeneric(key)) {\n\t\treturn !data->isEmpty();\n\t}\n\treturn {};\n}\n\ntemplate <>\nvoid Settings::writePrefImpl<bool>(std::string_view key, bool value) {\n\twritePrefGeneric(key, value ? \"\\x1\"_q : QByteArray());\n}\n\nQString Settings::getSoundPath(const QString &key) const {\n\tauto it = _soundOverrides.find(key);\n\tif (it != _soundOverrides.end()) {\n\t\treturn it->second;\n\t}\n\treturn u\":/sounds/\"_q + key + u\".mp3\"_q;\n}\n\nvoid Settings::setTabbedSelectorSectionEnabled(bool enabled) {\n\t_tabbedSelectorSectionEnabled = enabled;\n\tif (enabled) {\n\t\tsetThirdSectionInfoEnabled(false);\n\t}\n\tsetTabbedReplacedWithInfo(false);\n}\n\nrpl::producer<bool> Settings::tabbedReplacedWithInfoValue() const {\n\treturn _tabbedReplacedWithInfoValue.events_starting_with(\n\t\ttabbedReplacedWithInfo());\n}\n\nvoid Settings::setThirdSectionInfoEnabled(bool enabled) {\n\tif (_thirdSectionInfoEnabled != enabled) {\n\t\t_thirdSectionInfoEnabled = enabled;\n\t\tif (enabled) {\n\t\t\tsetTabbedSelectorSectionEnabled(false);\n\t\t}\n\t\tsetTabbedReplacedWithInfo(false);\n\t\t_thirdSectionInfoEnabledValue.fire_copy(enabled);\n\t}\n}\n\nrpl::producer<bool> Settings::thirdSectionInfoEnabledValue() const {\n\treturn _thirdSectionInfoEnabledValue.events_starting_with(\n\t\tthirdSectionInfoEnabled());\n}\n\nvoid Settings::setTabbedReplacedWithInfo(bool enabled) {\n\tif (_tabbedReplacedWithInfo != enabled) {\n\t\t_tabbedReplacedWithInfo = enabled;\n\t\t_tabbedReplacedWithInfoValue.fire_copy(enabled);\n\t}\n}\n\nvoid Settings::updateDialogsWidthRatio(float64 ratio, bool nochat) {\n\tconst auto changeWithChat = !nochat\n\t\t|| (dialogsWithChatWidthRatio() > 0)\n\t\t|| _dialogsWidthSetToZeroWithoutChat;\n\tconst auto changedWithChat = changeWithChat\n\t\t&& (dialogsWithChatWidthRatio() != ratio);\n\n\tconst auto changeNoChat = nochat\n\t\t|| (dialogsWithChatWidthRatio() != ratio);\n\tconst auto changedNoChat = changeNoChat\n\t\t&& (dialogsNoChatWidthRatio() != ratio);\n\n\tif (changedWithChat) {\n\t\t_dialogsWidthSetToZeroWithoutChat = nochat && !(ratio > 0);\n\t\t_dialogsWithChatWidthRatio = ratio;\n\t}\n\tif (changedNoChat) {\n\t\t_dialogsNoChatWidthRatio = ratio;\n\t}\n}\n\nfloat64 Settings::dialogsWidthRatio(bool nochat) const {\n\tconst auto withchat = dialogsWithChatWidthRatio();\n\treturn (!nochat || withchat > 0) ? withchat : dialogsNoChatWidthRatio();\n}\n\nfloat64 Settings::dialogsWithChatWidthRatio() const {\n\treturn _dialogsWithChatWidthRatio.current();\n}\n\nrpl::producer<float64> Settings::dialogsWithChatWidthRatioChanges() const {\n\treturn _dialogsWithChatWidthRatio.changes();\n}\n\nfloat64 Settings::dialogsNoChatWidthRatio() const {\n\treturn _dialogsNoChatWidthRatio.current();\n}\n\nrpl::producer<float64> Settings::dialogsNoChatWidthRatioChanges() const {\n\treturn _dialogsNoChatWidthRatio.changes();\n}\n\nvoid Settings::setThirdColumnWidth(int width) {\n\t_thirdColumnWidth = width;\n}\n\nQString Settings::deviceModel() const {\n\tconst auto custom = customDeviceModel();\n\treturn custom.isEmpty() ? Platform::DeviceModelPretty() : custom;\n}\n\nrpl::producer<QString> Settings::deviceModelChanges() const {\n\treturn customDeviceModelChanges() | rpl::map([=] {\n\t\treturn deviceModel();\n\t});\n}\n\nrpl::producer<QString> Settings::deviceModelValue() const {\n\treturn customDeviceModelValue() | rpl::map([=] {\n\t\treturn deviceModel();\n\t});\n}\n\nint Settings::thirdColumnWidth() const {\n\treturn _thirdColumnWidth.current();\n}\n\nrpl::producer<int> Settings::thirdColumnWidthChanges() const {\n\treturn _thirdColumnWidth.changes();\n}\n\nconst std::vector<RecentEmoji> &Settings::recentEmoji() const {\n\tif (!_recentEmojiResolved) {\n\t\t_recentEmojiResolved = true;\n\t\tresolveRecentEmoji();\n\t}\n\treturn _recentEmoji;\n}\n\nvoid Settings::resolveRecentEmoji() const {\n\tconst auto haveAlready = [&](RecentEmojiId id) {\n\t\treturn ranges::contains(\n\t\t\t_recentEmoji,\n\t\t\tid,\n\t\t\t[](const RecentEmoji &data) { return data.id; });\n\t};\n\tauto testCount = 0;\n\tauto nonTestCount = 0;\n\tif (!_recentEmojiPreload.empty()) {\n\t\t_recentEmoji.reserve(_recentEmojiPreload.size());\n\t\tfor (const auto &[id, rating] : base::take(_recentEmojiPreload)) {\n\t\t\tauto length = int();\n\t\t\tconst auto emoji = Ui::Emoji::Find(id, &length);\n\t\t\tif (emoji && length == id.size()) {\n\t\t\t\tif (!haveAlready({ emoji })) {\n\t\t\t\t\t_recentEmoji.push_back({ { emoji }, rating });\n\t\t\t\t}\n\t\t\t} else if (const auto document = ParseRecentEmojiDocument(id)) {\n\t\t\t\tif (!haveAlready({ *document })) {\n\t\t\t\t\t_recentEmoji.push_back({ { *document }, rating });\n\t\t\t\t\tif (document->test) {\n\t\t\t\t\t\t++testCount;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t++nonTestCount;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t_recentEmojiPreload.clear();\n\t}\n\tconst auto specialCount = std::max(testCount, nonTestCount);\n\tfor (const auto emoji : Ui::Emoji::GetDefaultRecent()) {\n\t\tif (_recentEmoji.size() >= specialCount + kRecentEmojiLimit) {\n\t\t\tbreak;\n\t\t} else if (_recentEmojiSkip.contains(emoji->id())) {\n\t\t\tcontinue;\n\t\t} else if (!haveAlready({ emoji })) {\n\t\t\t_recentEmoji.push_back({ { emoji }, 1 });\n\t\t}\n\t}\n}\n\nvoid Settings::incrementRecentEmoji(RecentEmojiId id) {\n\tresolveRecentEmoji();\n\n\tif (const auto emoji = std::get_if<EmojiPtr>(&id.data)) {\n\t\t_recentEmojiSkip.remove((*emoji)->id());\n\t}\n\tauto i = _recentEmoji.begin(), e = _recentEmoji.end();\n\tfor (; i != e; ++i) {\n\t\tif (i->id == id) {\n\t\t\t++i->rating;\n\t\t\tif (i->rating > 0x8000) {\n\t\t\t\tfor (auto j = _recentEmoji.begin(); j != e; ++j) {\n\t\t\t\t\tif (j->rating > 1) {\n\t\t\t\t\t\tj->rating /= 2;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tj->rating = 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (; i != _recentEmoji.begin(); --i) {\n\t\t\t\tif ((i - 1)->rating > i->rating) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tstd::swap(*i, *(i - 1));\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (i == e) {\n\t\t_recentEmoji.push_back({ id, 1 });\n\t\tfor (i = _recentEmoji.end() - 1; i != _recentEmoji.begin(); --i) {\n\t\t\tif ((i - 1)->rating > i->rating) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tstd::swap(*i, *(i - 1));\n\t\t}\n\t\tauto testCount = 0;\n\t\tauto nonTestCount = 0;\n\t\tfor (const auto &emoji : _recentEmoji) {\n\t\t\tconst auto id = &emoji.id.data;\n\t\t\tif (const auto document = std::get_if<RecentEmojiDocument>(id)) {\n\t\t\t\tif (document->test) {\n\t\t\t\t\t++testCount;\n\t\t\t\t} else {\n\t\t\t\t\t++nonTestCount;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst auto specialCount = std::max(testCount, nonTestCount);\n\t\twhile (_recentEmoji.size() >= specialCount + kRecentEmojiLimit) {\n\t\t\t_recentEmoji.pop_back();\n\t\t}\n\t}\n\t_recentEmojiUpdated.fire({});\n\t_saveDelayed.fire({});\n}\n\nvoid Settings::hideRecentEmoji(RecentEmojiId id) {\n\tresolveRecentEmoji();\n\n\t_recentEmoji.erase(\n\t\tranges::remove(_recentEmoji, id, &RecentEmoji::id),\n\t\tend(_recentEmoji));\n\tif (const auto emoji = std::get_if<EmojiPtr>(&id.data)) {\n\t\tfor (const auto always : Ui::Emoji::GetDefaultRecent()) {\n\t\t\tif (always == *emoji) {\n\t\t\t\t_recentEmojiSkip.emplace(always->id());\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\t_recentEmojiUpdated.fire({});\n\t_saveDelayed.fire({});\n}\n\nvoid Settings::resetRecentEmoji() {\n\tresolveRecentEmoji();\n\n\t_recentEmoji.clear();\n\t_recentEmojiSkip.clear();\n\t_recentEmojiPreload.clear();\n\t_recentEmojiResolved = false;\n\n\t_recentEmojiUpdated.fire({});\n\t_saveDelayed.fire({});\n}\n\nvoid Settings::setLegacyRecentEmojiPreload(\n\t\tQVector<QPair<QString, ushort>> data) {\n\tif (!_recentEmojiPreload.empty() || data.isEmpty()) {\n\t\treturn;\n\t}\n\t_recentEmojiPreload.reserve(data.size());\n\tfor (const auto &[id, rating] : data) {\n\t\t_recentEmojiPreload.push_back({ id, rating });\n\t}\n}\n\nEmojiPtr Settings::lookupEmojiVariant(EmojiPtr emoji) const {\n\tif (emoji->hasVariants()) {\n\t\tconst auto i = _emojiVariants.find(emoji->nonColoredId());\n\t\tif (i != end(_emojiVariants)) {\n\t\t\treturn emoji->variant(i->second);\n\t\t}\n\t\tconst auto j = _emojiVariants.find(QString());\n\t\tif (j != end(_emojiVariants)) {\n\t\t\treturn emoji->variant(j->second);\n\t\t}\n\t}\n\treturn emoji;\n}\n\nbool Settings::hasChosenEmojiVariant(EmojiPtr emoji) const {\n\treturn _emojiVariants.contains(QString())\n\t\t|| _emojiVariants.contains(emoji->nonColoredId());\n}\n\nvoid Settings::saveEmojiVariant(EmojiPtr emoji) {\n\tExpects(emoji->hasVariants());\n\n\t_emojiVariants[emoji->nonColoredId()] = emoji->variantIndex(emoji);\n\t_saveDelayed.fire({});\n}\n\nvoid Settings::saveAllEmojiVariants(EmojiPtr emoji) {\n\tExpects(emoji->hasVariants());\n\n\t_emojiVariants.clear();\n\t_emojiVariants[QString()] = emoji->variantIndex(emoji);\n\t_saveDelayed.fire({});\n}\n\nvoid Settings::setLegacyEmojiVariants(QMap<QString, int> data) {\n\tif (!_emojiVariants.empty() || data.isEmpty()) {\n\t\treturn;\n\t}\n\t_emojiVariants.reserve(data.size());\n\tfor (auto i = data.begin(), e = data.end(); i != e; ++i) {\n\t\t_emojiVariants.emplace(i.key(), i.value());\n\t}\n}\n\nvoid Settings::resetOnLastLogout() {\n\t_forkSettings.resetOnLastLogout();\n\n\t_adaptiveForWide = true;\n\t_moderateModeEnabled = false;\n\n\t_songVolume = kDefaultVolume;\n\t_videoVolume = kDefaultVolume;\n\n\t_askDownloadPath = false;\n\t_downloadPath = QString();\n\t_downloadPathBookmark = QByteArray();\n\n\t_soundNotify = true;\n\t_desktopNotify = true;\n\t_flashBounceNotify = true;\n\t_notifyView = NotifyView::ShowPreview;\n\t//_nativeNotifications = std::nullopt;\n\t//_skipToastsInFocus = false;\n\t//_notificationsCount = 3;\n\t//_notificationsCorner = ScreenCorner::BottomRight;\n\t_includeMutedCounter = true;\n\t_includeMutedCounterFolders = true;\n\t_countUnreadMessages = true;\n\t_notifyAboutPinned = true;\n\t//_autoLock = 3600;\n\n\t//_playbackDeviceId = QString();\n\t//_captureDeviceId = QString();\n\t//_cameraDeviceId = QString();\n\t//_callPlaybackDeviceId = QString();\n\t//_callCaptureDeviceId = QString();\n\t//_callOutputVolume = 100;\n\t//_callInputVolume = 100;\n\t//_callAudioDuckingEnabled = true;\n\n\t_disableCallsLegacy = false;\n\n\t_groupCallPushToTalk = false;\n\t_groupCallPushToTalkShortcut = QByteArray();\n\t_groupCallPushToTalkDelay = 20;\n\n\t_groupCallNoiseSuppression = false;\n\n\t//_themesAccentColors = Window::Theme::AccentColors();\n\n\t_lastSeenWarningSeen = false;\n\t_sendFilesWay = Ui::SendFilesWay();\n\t//_sendSubmitWay = Ui::InputSubmitSettings::Enter;\n\t_soundOverrides = {};\n\n\t_noWarningExtensions.clear();\n\t_ipRevealWarning = true;\n\t_loopAnimatedStickers = true;\n\t_largeEmoji = true;\n\t_replaceEmoji = true;\n\t_systemTextReplace = true;\n\t_suggestEmoji = true;\n\t_suggestStickersByEmoji = true;\n\t_suggestAnimatedEmoji = true;\n\t_spellcheckerEnabled = true;\n\t_videoPlaybackSpeed = PlaybackSpeed();\n\t_voicePlaybackSpeed = PlaybackSpeed();\n\t_audioPlaybackSpeed = PlaybackSpeed();\n\t//_videoPipGeometry = QByteArray();\n\t_dictionariesEnabled = std::vector<int>();\n\t_autoDownloadDictionaries = true;\n\t_mainMenuAccountsShown = true;\n\t_tabbedSelectorSectionEnabled = false; // per-window\n\t_floatPlayerColumn = Window::Column::Second; // per-window\n\t_floatPlayerCorner = RectPart::TopRight; // per-window\n\t_thirdSectionInfoEnabled = true; // per-window\n\t_thirdSectionExtendedBy = -1; // per-window\n\t_dialogsWithChatWidthRatio = DefaultDialogsWidthRatio(); // per-window\n\t_dialogsNoChatWidthRatio = DefaultDialogsWidthRatio(); // per-window\n\t_thirdColumnWidth = kDefaultThirdColumnWidth; // p-w\n\t_notifyFromAll = true;\n\t_tabbedReplacedWithInfo = false; // per-window\n\t_hiddenGroupCallTooltips = 0;\n\t_storiesClickTooltipHidden = false;\n\t_ttlVoiceClickTooltipHidden = false;\n\tconst auto srDisabled = readPref<bool>(kScreenReaderModeDisabledKey);\n\t_prefs.clear();\n\tif (srDisabled) {\n\t\twritePref<bool>(kScreenReaderModeDisabledKey, true);\n\t}\n\t_ivZoom = 0;\n\t_recordVideoMessages = false;\n\t_videoQuality = {};\n\t_chatFiltersHorizontal = false;\n\t_quickDialogAction = Dialogs::Ui::QuickDialogAction::Disabled;\n\t_notificationsVolume = 100;\n\n\t_recentEmojiPreload.clear();\n\t_recentEmoji.clear();\n\t_emojiVariants.clear();\n\n\t_accountsOrder.clear();\n}\n\nbool Settings::ThirdColumnByDefault() {\n\treturn Platform::IsMacStoreBuild();\n}\n\nfloat64 Settings::DefaultDialogsWidthRatio() {\n\treturn ThirdColumnByDefault()\n\t\t? kDefaultBigDialogsWidthRatio\n\t\t: kDefaultDialogsWidthRatio;\n}\n\nqint32 Settings::SerializePlaybackSpeed(PlaybackSpeed speed) {\n\tusing namespace Media;\n\n\tconst auto value = int(base::SafeRound(\n\t\tstd::clamp(speed.value, kSpeedMin, kSpeedMax) * 100));\n\treturn speed.enabled ? value : -value;\n}\n\nauto Settings::DeserializePlaybackSpeed(qint32 speed) -> PlaybackSpeed {\n\tusing namespace Media;\n\n\tauto enabled = true;\n\tconst auto validate = [&](float64 result) {\n\t\treturn PlaybackSpeed{\n\t\t\t.value = (result == 1.) ? kSpedUpDefault : result,\n\t\t\t.enabled = enabled && (result != 1.),\n\t\t};\n\t};\n\tif (speed >= 0 && speed < 10) {\n\t\t// The old values in settings.\n\t\treturn validate((std::clamp(speed, 0, 6) + 2) / 4.);\n\t} else if (speed < 0) {\n\t\tspeed = -speed;\n\t\tenabled = false;\n\t}\n\treturn validate(std::clamp(speed / 100., kSpeedMin, kSpeedMax));\n}\n\nbool Settings::nativeNotifications() const {\n\treturn _nativeNotifications.value_or(\n\t\tPlatform::Notifications::ByDefault());\n}\n\nvoid Settings::setNativeNotifications(bool value) {\n\t_nativeNotifications = (value == Platform::Notifications::ByDefault())\n\t\t? std::nullopt\n\t\t: std::make_optional(value);\n}\n\nbool Settings::skipToastsInFocus() const {\n\treturn _skipToastsInFocus;\n}\n\nvoid Settings::setSkipToastsInFocus(bool value) {\n\t_skipToastsInFocus = value;\n}\n\nvoid Settings::setTranslateButtonEnabled(bool value) {\n\t_translateButtonEnabled = value;\n}\n\nbool Settings::translateButtonEnabled() const {\n\treturn _translateButtonEnabled;\n}\n\nvoid Settings::setUsePlatformTranslation(bool value) {\n\t_usePlatformTranslation = value;\n}\n\nbool Settings::usePlatformTranslation() const {\n\treturn _usePlatformTranslation;\n}\n\nvoid Settings::setTranslateChatEnabled(bool value) {\n\t_translateChatEnabled = value;\n}\n\nbool Settings::translateChatEnabled() const {\n\treturn _translateChatEnabled.current();\n}\n\nrpl::producer<bool> Settings::translateChatEnabledValue() const {\n\treturn _translateChatEnabled.value();\n}\n\n[[nodiscard]] const std::vector<LanguageId> &DefaultSkipLanguages() {\n\tusing namespace Platform;\n\n\tstatic auto Result = [&] {\n\t\tauto list = std::vector<LanguageId>();\n\t\tlist.push_back({ LanguageId::FromName(Lang::Id()) });\n\t\tconst auto systemId = LanguageId::FromName(SystemLanguage());\n\t\tif (list.back() != systemId) {\n\t\t\tlist.push_back(systemId);\n\t\t}\n\n\t\tEnsures(!list.empty());\n\t\treturn list;\n\t}();\n\treturn Result;\n}\n\n[[nodiscard]] std::vector<LanguageId> NonEmptySkipList(\n\t\tstd::vector<LanguageId> list) {\n\treturn list.empty() ? DefaultSkipLanguages() : list;\n}\n\nvoid Settings::setTranslateTo(LanguageId id) {\n\t_translateToRaw = int(id.value);\n}\n\nLanguageId Settings::translateTo() const {\n\tif (const auto raw = _translateToRaw.current()) {\n\t\treturn { QLocale::Language(raw) };\n\t}\n\treturn DefaultSkipLanguages().front();\n}\n\nrpl::producer<LanguageId> Settings::translateToValue() const {\n\treturn _translateToRaw.value() | rpl::map([=](int raw) {\n\t\treturn raw\n\t\t\t? LanguageId{ QLocale::Language(raw) }\n\t\t\t: DefaultSkipLanguages().front();\n\t}) | rpl::distinct_until_changed();\n}\n\nvoid Settings::setSkipTranslationLanguages(\n\t\tstd::vector<LanguageId> languages) {\n\t_skipTranslationLanguages = std::move(languages);\n}\n\nauto Settings::skipTranslationLanguages() const -> std::vector<LanguageId> {\n\treturn NonEmptySkipList(_skipTranslationLanguages.current());\n}\n\nauto Settings::skipTranslationLanguagesValue() const\n-> rpl::producer<std::vector<LanguageId>> {\n\treturn _skipTranslationLanguages.value() | rpl::map(NonEmptySkipList);\n}\n\nvoid Settings::setRememberedDeleteMessageOnlyForYou(bool value) {\n\t_rememberedDeleteMessageOnlyForYou = value;\n}\n\nbool Settings::rememberedDeleteMessageOnlyForYou() const {\n\treturn _rememberedDeleteMessageOnlyForYou;\n}\n\nint Settings::ivZoom() const {\n\treturn ResolveIvZoom(_ivZoom.current());\n}\n\nrpl::producer<int> Settings::ivZoomValue() const {\n\treturn _ivZoom.value() | rpl::map(ResolveIvZoom);\n}\n\nvoid Settings::setIvZoom(int value) {\n\tif (!value || value == DefaultIvZoom()) {\n\t\t_ivZoom = 0;\n\t\treturn;\n\t}\n#ifdef Q_OS_WIN\n\tconstexpr auto kMin = 25;\n\tconstexpr auto kMax = 500;\n#else\n\tconstexpr auto kMin = 30;\n\tconstexpr auto kMax = 200;\n#endif\n\t_ivZoom = std::clamp(value, kMin, kMax);\n}\n\nbool Settings::normalizeIvZoom() {\n\tconst auto value = _ivZoom.current();\n\tif (value && value == DefaultIvZoom()) {\n\t\t_ivZoom = 0;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nMedia::VideoQuality Settings::videoQuality() const {\n\treturn _videoQuality;\n}\n\nvoid Settings::setVideoQuality(Media::VideoQuality value) {\n\t_videoQuality = value;\n}\n\nbool Settings::chatFiltersHorizontal() const {\n\treturn _chatFiltersHorizontal.current();\n}\n\nrpl::producer<bool> Settings::chatFiltersHorizontalChanges() const {\n\treturn _chatFiltersHorizontal.changes();\n}\n\nvoid Settings::setChatFiltersHorizontal(bool value) {\n\t_chatFiltersHorizontal = value;\n}\n\nDialogs::Ui::QuickDialogAction Settings::quickDialogAction() const {\n\treturn _quickDialogAction;\n}\n\nvoid Settings::setQuickDialogAction(Dialogs::Ui::QuickDialogAction action) {\n\t_quickDialogAction = action;\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/core_settings.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"core/core_settings_proxy.h\"\n#include \"media/media_common.h\"\n#include \"dialogs/ui/dialogs_quick_action.h\"\n#include \"window/themes/window_themes_embedded.h\"\n#include \"ui/chat/attach/attach_send_files_way.h\"\n#include \"base/flags.h\"\n#include \"emoji.h\"\n\n#include \"core/fork_settings.h\"\n\nenum class RectPart;\nstruct LanguageId;\n\nnamespace Ui {\nenum class InputSubmitSettings;\n} // namespace Ui\n\nnamespace HistoryView {\nenum class DoubleClickQuickAction;\n} // namespace HistoryView\n\nnamespace Window {\nenum class Column;\n} // namespace Window\n\nnamespace Calls::Group {\nenum class StickedTooltip;\n} // namespace Calls::Group\n\nnamespace Core {\n\ninline constexpr auto kScreenReaderModeDisabledKey\n\t= \"screen-reader-mode-disabled\"_cs;\n\nstruct WindowPosition {\n\tint32 moncrc = 0;\n\tint maximized = 0;\n\tint scale = 0;\n\tint x = 0;\n\tint y = 0;\n\tint w = 0;\n\tint h = 0;\n\n\tfriend inline constexpr auto operator<=>(\n\t\tWindowPosition,\n\t\tWindowPosition) = default;\n\n\t[[nodiscard]] QRect rect() const {\n\t\treturn QRect(x, y, w, h);\n\t}\n};\n\n[[nodiscard]] WindowPosition AdjustToScale(\n\tWindowPosition position,\n\tconst QString &name);\n\nstruct WindowTitleContent {\n\tbool hideChatName : 1 = false;\n\tbool hideAccountName : 1 = false;\n\tbool hideTotalUnread : 1 = false;\n\n\tfriend inline constexpr auto operator<=>(\n\t\tWindowTitleContent,\n\t\tWindowTitleContent) = default;\n};\n\nconstexpr auto kRecentEmojiLimit = 54;\n\nstruct RecentEmojiDocument {\n\tDocumentId id = 0;\n\tbool test = false;\n\n\tfriend inline auto operator<=>(\n\t\tRecentEmojiDocument,\n\t\tRecentEmojiDocument) = default;\n};\n\nstruct RecentEmojiId {\n\tstd::variant<EmojiPtr, RecentEmojiDocument> data;\n\n\tfriend inline bool operator==(\n\t\tRecentEmojiId,\n\t\tRecentEmojiId) = default;\n};\n\nstruct RecentEmoji {\n\tRecentEmojiId id;\n\tushort rating = 0;\n};\n\nclass Settings final {\npublic:\n\tenum class ScreenCorner {\n\t\tTopLeft = 0,\n\t\tTopRight = 1,\n\t\tBottomRight = 2,\n\t\tBottomLeft = 3,\n\t};\n\tenum class NotifyView {\n\t\tShowPreview = 0,\n\t\tShowName = 1,\n\t\tShowNothing = 2,\n\t};\n\tenum class WorkMode {\n\t\tWindowAndTray = 0,\n\t\tTrayOnly = 1,\n\t\tWindowOnly = 2,\n\t};\n\tenum class CloseBehavior {\n\t\tQuit = 0,\n\t\tCloseToTaskbar = 1,\n\t\tRunInBackground = 2,\n\t};\n\n\tstatic constexpr auto kDefaultVolume = 0.9;\n\n\tSettings();\n\t~Settings();\n\n\t[[nodiscard]] ForkSettings &fork() {\n\t\treturn _forkSettings;\n\t}\n\n\t[[nodiscard]] rpl::producer<> saveDelayedRequests() const {\n\t\treturn _saveDelayed.events();\n\t}\n\n\t[[nodiscard]] SettingsProxy &proxy() {\n\t\treturn _proxy;\n\t}\n\n\t[[nodiscard]] static bool IsLeftCorner(ScreenCorner corner) {\n\t\treturn (corner == ScreenCorner::TopLeft)\n\t\t\t|| (corner == ScreenCorner::BottomLeft);\n\t}\n\t[[nodiscard]] static bool IsTopCorner(ScreenCorner corner) {\n\t\treturn (corner == ScreenCorner::TopLeft)\n\t\t\t|| (corner == ScreenCorner::TopRight);\n\t}\n\n\t[[nodiscard]] QByteArray serialize() const;\n\tvoid addFromSerialized(const QByteArray &serialized);\n\n\t[[nodiscard]] bool adaptiveForWide() const {\n\t\treturn _adaptiveForWide.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> adaptiveForWideValue() const {\n\t\treturn _adaptiveForWide.value();\n\t}\n\t[[nodiscard]] rpl::producer<bool> adaptiveForWideChanges() const {\n\t\treturn _adaptiveForWide.changes();\n\t}\n\tvoid setAdaptiveForWide(bool value) {\n\t\t_adaptiveForWide = value;\n\t}\n\t[[nodiscard]] bool moderateModeEnabled() const {\n\t\treturn _moderateModeEnabled;\n\t}\n\tvoid setModerateModeEnabled(bool value) {\n\t\t_moderateModeEnabled = value;\n\t}\n\t[[nodiscard]] float64 songVolume() const {\n\t\treturn _songVolume.current();\n\t}\n\t[[nodiscard]] rpl::producer<float64> songVolumeChanges() const {\n\t\treturn _songVolume.changes();\n\t}\n\tvoid setSongVolume(float64 value) {\n\t\t_songVolume = value;\n\t}\n\t[[nodiscard]] float64 videoVolume() const {\n\t\treturn _videoVolume.current();\n\t}\n\t[[nodiscard]] rpl::producer<float64> videoVolumeChanges() const {\n\t\treturn _videoVolume.changes();\n\t}\n\tvoid setVideoVolume(float64 value) {\n\t\t_videoVolume = value;\n\t}\n\t[[nodiscard]] bool askDownloadPath() const {\n\t\treturn _askDownloadPath;\n\t}\n\tvoid setAskDownloadPath(bool value) {\n\t\t_askDownloadPath = value;\n\t}\n\t[[nodiscard]] QString downloadPath() const {\n\t\treturn _downloadPath.current();\n\t}\n\t[[nodiscard]] rpl::producer<QString> downloadPathValue() const {\n\t\treturn _downloadPath.value();\n\t}\n\tvoid setDownloadPath(const QString &value) {\n\t\t_downloadPath = value;\n\t}\n\t[[nodiscard]] QByteArray downloadPathBookmark() const {\n\t\treturn _downloadPathBookmark;\n\t}\n\tvoid setDownloadPathBookmark(const QByteArray &value) {\n\t\t_downloadPathBookmark = value;\n\t}\n\t[[nodiscard]] bool soundNotify() const {\n\t\treturn _soundNotify;\n\t}\n\tvoid setSoundNotify(bool value) {\n\t\t_soundNotify = value;\n\t}\n\t[[nodiscard]] bool desktopNotify() const {\n\t\treturn _desktopNotify;\n\t}\n\tvoid setDesktopNotify(bool value) {\n\t\t_desktopNotify = value;\n\t}\n\t[[nodiscard]] bool flashBounceNotify() const {\n\t\treturn _flashBounceNotify;\n\t}\n\tvoid setFlashBounceNotify(bool value) {\n\t\t_flashBounceNotify = value;\n\t}\n\t[[nodiscard]] NotifyView notifyView() const {\n\t\treturn _notifyView;\n\t}\n\tvoid setNotifyView(NotifyView value) {\n\t\t_notifyView = value;\n\t}\n\n\t[[nodiscard]] bool nativeNotifications() const;\n\tvoid setNativeNotifications(bool value);\n\n\t[[nodiscard]] bool skipToastsInFocus() const;\n\tvoid setSkipToastsInFocus(bool value);\n\n\t[[nodiscard]] int notificationsCount() const {\n\t\treturn _notificationsCount;\n\t}\n\tvoid setNotificationsCount(int value) {\n\t\t_notificationsCount = value;\n\t}\n\t[[nodiscard]] ScreenCorner notificationsCorner() const {\n\t\treturn _notificationsCorner;\n\t}\n\tvoid setNotificationsCorner(ScreenCorner corner) {\n\t\t_notificationsCorner = corner;\n\t}\n\t[[nodiscard]] int32 notificationsDisplayChecksum() const {\n\t\treturn _notificationsDisplayChecksum;\n\t}\n\tvoid setNotificationsDisplayChecksum(int32 checksum) {\n\t\t_notificationsDisplayChecksum = checksum;\n\t}\n\t[[nodiscard]] bool includeMutedCounter() const {\n\t\treturn _includeMutedCounter;\n\t}\n\tvoid setIncludeMutedCounter(bool value) {\n\t\t_includeMutedCounter = value;\n\t}\n\t[[nodiscard]] bool includeMutedCounterFolders() const {\n\t\treturn _includeMutedCounterFolders;\n\t}\n\tvoid setIncludeMutedCounterFolders(bool value) {\n\t\t_includeMutedCounterFolders = value;\n\t}\n\t[[nodiscard]] bool countUnreadMessages() const {\n\t\treturn _countUnreadMessages;\n\t}\n\tvoid setCountUnreadMessages(bool value) {\n\t\t_countUnreadMessages = value;\n\t}\n\tvoid setNotifyAboutPinned(bool notify) {\n\t\t_notifyAboutPinned = notify;\n\t}\n\t[[nodiscard]] bool notifyAboutPinned() const {\n\t\treturn _notifyAboutPinned.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> notifyAboutPinnedChanges() const {\n\t\treturn _notifyAboutPinned.changes();\n\t}\n\t[[nodiscard]] int autoLock() const {\n\t\treturn _autoLock;\n\t}\n\tvoid setAutoLock(int value) {\n\t\t_autoLock = value;\n\t}\n\n\t[[nodiscard]] QString playbackDeviceId() const {\n\t\treturn _playbackDeviceId.current();\n\t}\n\t[[nodiscard]] rpl::producer<QString> playbackDeviceIdChanges() const {\n\t\treturn _playbackDeviceId.changes();\n\t}\n\t[[nodiscard]] rpl::producer<QString> playbackDeviceIdValue() const {\n\t\treturn _playbackDeviceId.value();\n\t}\n\tvoid setPlaybackDeviceId(const QString &value) {\n\t\t_playbackDeviceId = value;\n\t}\n\t[[nodiscard]] QString captureDeviceId() const {\n\t\treturn _captureDeviceId.current();\n\t}\n\t[[nodiscard]] rpl::producer<QString> captureDeviceIdChanges() const {\n\t\treturn _captureDeviceId.changes();\n\t}\n\t[[nodiscard]] rpl::producer<QString> captureDeviceIdValue() const {\n\t\treturn _captureDeviceId.value();\n\t}\n\tvoid setCaptureDeviceId(const QString &value) {\n\t\t_captureDeviceId = value;\n\t}\n\t[[nodiscard]] QString cameraDeviceId() const {\n\t\treturn _cameraDeviceId.current();\n\t}\n\t[[nodiscard]] rpl::producer<QString> cameraDeviceIdChanges() const {\n\t\treturn _cameraDeviceId.changes();\n\t}\n\t[[nodiscard]] rpl::producer<QString> cameraDeviceIdValue() const {\n\t\treturn _cameraDeviceId.value();\n\t}\n\tvoid setCameraDeviceId(const QString &value) {\n\t\t_cameraDeviceId = value;\n\t}\n\t[[nodiscard]] QString callPlaybackDeviceId() const {\n\t\treturn _callPlaybackDeviceId.current();\n\t}\n\t[[nodiscard]] rpl::producer<QString> callPlaybackDeviceIdChanges() const {\n\t\treturn _callPlaybackDeviceId.changes();\n\t}\n\t[[nodiscard]] rpl::producer<QString> callPlaybackDeviceIdValue() const {\n\t\treturn _callPlaybackDeviceId.value();\n\t}\n\tvoid setCallPlaybackDeviceId(const QString &value) {\n\t\t_callPlaybackDeviceId = value;\n\t}\n\t[[nodiscard]] QString callCaptureDeviceId() const {\n\t\treturn _callCaptureDeviceId.current();\n\t}\n\t[[nodiscard]] rpl::producer<QString> callCaptureDeviceIdChanges() const {\n\t\treturn _callCaptureDeviceId.changes();\n\t}\n\t[[nodiscard]] rpl::producer<QString> callCaptureDeviceIdValue() const {\n\t\treturn _callCaptureDeviceId.value();\n\t}\n\tvoid setCallCaptureDeviceId(const QString &value) {\n\t\t_callCaptureDeviceId = value;\n\t}\n\n\t[[nodiscard]] int callOutputVolume() const {\n\t\treturn _callOutputVolume;\n\t}\n\tvoid setCallOutputVolume(int value) {\n\t\t_callOutputVolume = value;\n\t}\n\t[[nodiscard]] int callInputVolume() const {\n\t\treturn _callInputVolume;\n\t}\n\tvoid setCallInputVolume(int value) {\n\t\t_callInputVolume = value;\n\t}\n\t[[nodiscard]] bool callAudioDuckingEnabled() const {\n\t\treturn _callAudioDuckingEnabled;\n\t}\n\tvoid setCallAudioDuckingEnabled(bool value) {\n\t\t_callAudioDuckingEnabled = value;\n\t}\n\t[[nodiscard]] bool disableCallsLegacy() const {\n\t\treturn _disableCallsLegacy;\n\t}\n\t[[nodiscard]] bool groupCallPushToTalk() const {\n\t\treturn _groupCallPushToTalk;\n\t}\n\tvoid setGroupCallPushToTalk(bool value) {\n\t\t_groupCallPushToTalk = value;\n\t}\n\t[[nodiscard]] QByteArray groupCallPushToTalkShortcut() const {\n\t\treturn _groupCallPushToTalkShortcut;\n\t}\n\tvoid setGroupCallPushToTalkShortcut(const QByteArray &serialized) {\n\t\t_groupCallPushToTalkShortcut = serialized;\n\t}\n\t[[nodiscard]] crl::time groupCallPushToTalkDelay() const {\n\t\treturn _groupCallPushToTalkDelay;\n\t}\n\tvoid setGroupCallPushToTalkDelay(crl::time delay) {\n\t\t_groupCallPushToTalkDelay = delay;\n\t}\n\t[[nodiscard]] bool groupCallNoiseSuppression() const {\n\t\treturn _groupCallNoiseSuppression;\n\t}\n\tvoid setGroupCallNoiseSuppression(bool value) {\n\t\t_groupCallNoiseSuppression = value;\n\t}\n\t[[nodiscard]] Window::Theme::AccentColors &themesAccentColors() {\n\t\treturn _themesAccentColors;\n\t}\n\t[[nodiscard]] const Window::Theme::AccentColors &themesAccentColors() const {\n\t\treturn _themesAccentColors;\n\t}\n\tvoid setThemesAccentColors(Window::Theme::AccentColors &&colors) {\n\t\t_themesAccentColors = std::move(colors);\n\t}\n\tvoid setLastSeenWarningSeen(bool lastSeenWarningSeen) {\n\t\t_lastSeenWarningSeen = lastSeenWarningSeen;\n\t}\n\t[[nodiscard]] bool lastSeenWarningSeen() const {\n\t\treturn _lastSeenWarningSeen;\n\t}\n\tvoid setSendFilesWay(Ui::SendFilesWay way) {\n\t\t_sendFilesWay = way;\n\t}\n\t[[nodiscard]] Ui::SendFilesWay sendFilesWay() const {\n\t\treturn _sendFilesWay;\n\t}\n\tvoid setSendSubmitWay(Ui::InputSubmitSettings value) {\n\t\t_sendSubmitWay = value;\n\t}\n\t[[nodiscard]] Ui::InputSubmitSettings sendSubmitWay() const {\n\t\treturn _sendSubmitWay.current();\n\t}\n\t[[nodiscard]] auto sendSubmitWayValue() const\n\t-> rpl::producer<Ui::InputSubmitSettings> {\n\t\treturn _sendSubmitWay.value();\n\t}\n\tvoid setSoundOverride(const QString &key, const QString &path) {\n\t\t_soundOverrides.emplace(key, path);\n\t}\n\tvoid clearSoundOverrides() {\n\t\t_soundOverrides.clear();\n\t}\n\t[[nodiscard]] QString getSoundPath(const QString &key) const;\n\n\t[[nodiscard]] auto noWarningExtensions() const\n\t-> const base::flat_set<QString> & {\n\t\treturn _noWarningExtensions;\n\t}\n\tvoid setNoWarningExtensions(base::flat_set<QString> extensions) {\n\t\t_noWarningExtensions = std::move(extensions);\n\t}\n\t[[nodiscard]] bool ipRevealWarning() const {\n\t\treturn _ipRevealWarning;\n\t}\n\tvoid setIpRevealWarning(bool warning) {\n\t\t_ipRevealWarning = warning;\n\t}\n\t[[nodiscard]] bool loopAnimatedStickers() const {\n\t\treturn _loopAnimatedStickers;\n\t}\n\tvoid setLoopAnimatedStickers(bool value) {\n\t\t_loopAnimatedStickers = value;\n\t}\n\tvoid setLargeEmoji(bool value) {\n\t\t_largeEmoji = value;\n\t}\n\t[[nodiscard]] bool largeEmoji() const {\n\t\treturn _largeEmoji.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> largeEmojiValue() const {\n\t\treturn _largeEmoji.value();\n\t}\n\t[[nodiscard]] rpl::producer<bool> largeEmojiChanges() const {\n\t\treturn _largeEmoji.changes();\n\t}\n\tvoid setReplaceEmoji(bool value) {\n\t\t_replaceEmoji = value;\n\t}\n\t[[nodiscard]] bool replaceEmoji() const {\n\t\treturn _replaceEmoji.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> replaceEmojiValue() const {\n\t\treturn _replaceEmoji.value();\n\t}\n\t[[nodiscard]] rpl::producer<bool> replaceEmojiChanges() const {\n\t\treturn _replaceEmoji.changes();\n\t}\n\tvoid setSystemTextReplace(bool value) {\n\t\t_systemTextReplace = value;\n\t}\n\t[[nodiscard]] bool systemTextReplace() const {\n\t\treturn _systemTextReplace.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> systemTextReplaceValue() const {\n\t\treturn _systemTextReplace.value();\n\t}\n\t[[nodiscard]] rpl::producer<bool> systemTextReplaceChanges() const {\n\t\treturn _systemTextReplace.changes();\n\t}\n\t[[nodiscard]] bool suggestEmoji() const {\n\t\treturn _suggestEmoji;\n\t}\n\tvoid setSuggestEmoji(bool value) {\n\t\t_suggestEmoji = value;\n\t}\n\t[[nodiscard]] bool suggestStickersByEmoji() const {\n\t\treturn _suggestStickersByEmoji;\n\t}\n\tvoid setSuggestStickersByEmoji(bool value) {\n\t\t_suggestStickersByEmoji = value;\n\t}\n\t[[nodiscard]] bool suggestAnimatedEmoji() const {\n\t\treturn _suggestAnimatedEmoji;\n\t}\n\tvoid setSuggestAnimatedEmoji(bool value) {\n\t\t_suggestAnimatedEmoji = value;\n\t}\n\tvoid setCornerReaction(bool value) {\n\t\t_cornerReaction = value;\n\t}\n\t[[nodiscard]] bool cornerReaction() const {\n\t\treturn _cornerReaction.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> cornerReactionValue() const {\n\t\treturn _cornerReaction.value();\n\t}\n\tvoid setCornerReply(bool value) {\n\t\t_cornerReply = value;\n\t}\n\t[[nodiscard]] bool cornerReply() const {\n\t\treturn _cornerReply.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> cornerReplyValue() const {\n\t\treturn _cornerReply.value();\n\t}\n\n\tvoid setSpellcheckerEnabled(bool value) {\n\t\t_spellcheckerEnabled = value;\n\t}\n\tbool spellcheckerEnabled() const {\n\t\treturn _spellcheckerEnabled.current();\n\t}\n\trpl::producer<bool> spellcheckerEnabledValue() const {\n\t\treturn _spellcheckerEnabled.value();\n\t}\n\trpl::producer<bool> spellcheckerEnabledChanges() const {\n\t\treturn _spellcheckerEnabled.changes();\n\t}\n\n\tvoid setDictionariesEnabled(std::vector<int> dictionaries) {\n\t\t_dictionariesEnabled = std::move(dictionaries);\n\t}\n\n\tstd::vector<int> dictionariesEnabled() const {\n\t\treturn _dictionariesEnabled.current();\n\t}\n\n\trpl::producer<std::vector<int>> dictionariesEnabledChanges() const {\n\t\treturn _dictionariesEnabled.changes();\n\t}\n\n\tvoid setAutoDownloadDictionaries(bool value) {\n\t\t_autoDownloadDictionaries = value;\n\t}\n\tbool autoDownloadDictionaries() const {\n\t\treturn _autoDownloadDictionaries.current();\n\t}\n\trpl::producer<bool> autoDownloadDictionariesValue() const {\n\t\treturn _autoDownloadDictionaries.value();\n\t}\n\trpl::producer<bool> autoDownloadDictionariesChanges() const {\n\t\treturn _autoDownloadDictionaries.changes();\n\t}\n\n\t[[nodiscard]] float64 videoPlaybackSpeed(\n\t\t\tbool lastNonDefault = false) const {\n\t\treturn (_videoPlaybackSpeed.enabled || lastNonDefault)\n\t\t\t? _videoPlaybackSpeed.value\n\t\t\t: 1.;\n\t}\n\tvoid setVideoPlaybackSpeed(float64 speed) {\n\t\tif ((_videoPlaybackSpeed.enabled = !Media::EqualSpeeds(speed, 1.))) {\n\t\t\t_videoPlaybackSpeed.value = speed;\n\t\t}\n\t}\n\t[[nodiscard]] float64 voicePlaybackSpeed(\n\t\t\tbool lastNonDefault = false) const {\n\t\tconst auto &s = _voicePlaybackSpeed.current();\n\t\treturn (s.enabled || lastNonDefault) ? s.value : 1.;\n\t}\n\t[[nodiscard]] float64 audioPlaybackSpeed(\n\t\t\tbool lastNonDefault = false) const {\n\t\tconst auto &s = _audioPlaybackSpeed.current();\n\t\treturn (s.enabled || lastNonDefault) ? s.value : 1.;\n\t}\n\tvoid setVoicePlaybackSpeed(float64 speed) {\n\t\tconst auto enabled = !Media::EqualSpeeds(speed, 1.0);\n\t\t_voicePlaybackSpeed = PlaybackSpeed{\n\t\t\t.value = enabled ? speed : _voicePlaybackSpeed.current().value,\n\t\t\t.enabled = enabled,\n\t\t};\n\t}\n\tvoid setAudioPlaybackSpeed(float64 speed) {\n\t\tconst auto enabled = !Media::EqualSpeeds(speed, 1.0);\n\t\t_audioPlaybackSpeed = PlaybackSpeed{\n\t\t\t.value = enabled ? speed : _audioPlaybackSpeed.current().value,\n\t\t\t.enabled = enabled,\n\t\t};\n\t}\n\t[[nodiscard]] auto voicePlaybackSpeedChanges() const {\n\t\treturn _voicePlaybackSpeed.changes();\n\t}\n\t[[nodiscard]] auto audioPlaybackSpeedChanges() const {\n\t\treturn _audioPlaybackSpeed.changes();\n\t}\n\n\t// For legacy values read-write outside of Settings.\n\t[[nodiscard]] qint32 videoPlaybackSpeedSerialized() const {\n\t\treturn SerializePlaybackSpeed(_videoPlaybackSpeed);\n\t}\n\tvoid setVideoPlaybackSpeedSerialized(qint32 value) {\n\t\t_videoPlaybackSpeed = DeserializePlaybackSpeed(value);\n\t}\n\n\t[[nodiscard]] QByteArray videoPipGeometry() const {\n\t\treturn _videoPipGeometry;\n\t}\n\tvoid setVideoPipGeometry(QByteArray geometry) {\n\t\t_videoPipGeometry = geometry;\n\t}\n\n\t[[nodiscard]] QByteArray photoEditorBrush() const {\n\t\treturn _photoEditorBrush;\n\t}\n\tvoid setPhotoEditorBrush(QByteArray brush) {\n\t\t_photoEditorBrush = brush;\n\t}\n\n\t[[nodiscard]] float64 rememberedSongVolume() const {\n\t\treturn _rememberedSongVolume;\n\t}\n\tvoid setRememberedSongVolume(float64 value) {\n\t\t_rememberedSongVolume = value;\n\t}\n\t[[nodiscard]] bool rememberedSoundNotifyFromTray() const {\n\t\treturn _rememberedSoundNotifyFromTray;\n\t}\n\tvoid setRememberedSoundNotifyFromTray(bool value) {\n\t\t_rememberedSoundNotifyFromTray = value;\n\t}\n\t[[nodiscard]] bool rememberedFlashBounceNotifyFromTray() const {\n\t\treturn _rememberedFlashBounceNotifyFromTray;\n\t}\n\tvoid setRememberedFlashBounceNotifyFromTray(bool value) {\n\t\t_rememberedFlashBounceNotifyFromTray = value;\n\t}\n\t[[nodiscard]] bool mainMenuAccountsShown() const {\n\t\treturn _mainMenuAccountsShown.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> mainMenuAccountsShownValue() const {\n\t\treturn _mainMenuAccountsShown.value();\n\t}\n\tvoid setMainMenuAccountsShown(bool value) {\n\t\t_mainMenuAccountsShown = value;\n\t}\n\t[[nodiscard]] bool tabbedSelectorSectionEnabled() const {\n\t\treturn _tabbedSelectorSectionEnabled;\n\t}\n\tvoid setTabbedSelectorSectionEnabled(bool enabled);\n\t[[nodiscard]] bool thirdSectionInfoEnabled() const {\n\t\treturn _thirdSectionInfoEnabled;\n\t}\n\tvoid setThirdSectionInfoEnabled(bool enabled);\n\t[[nodiscard]] rpl::producer<bool> thirdSectionInfoEnabledValue() const;\n\t[[nodiscard]] int thirdSectionExtendedBy() const {\n\t\treturn _thirdSectionExtendedBy;\n\t}\n\tvoid setThirdSectionExtendedBy(int savedValue) {\n\t\t_thirdSectionExtendedBy = savedValue;\n\t}\n\t[[nodiscard]] bool tabbedReplacedWithInfo() const {\n\t\treturn _tabbedReplacedWithInfo;\n\t}\n\tvoid setTabbedReplacedWithInfo(bool enabled);\n\t[[nodiscard]] rpl::producer<bool> tabbedReplacedWithInfoValue() const;\n\tvoid setFloatPlayerColumn(Window::Column column) {\n\t\t_floatPlayerColumn = column;\n\t}\n\t[[nodiscard]] Window::Column floatPlayerColumn() const {\n\t\treturn _floatPlayerColumn;\n\t}\n\tvoid setFloatPlayerCorner(RectPart corner) {\n\t\t_floatPlayerCorner = corner;\n\t}\n\t[[nodiscard]] RectPart floatPlayerCorner() const {\n\t\treturn _floatPlayerCorner;\n\t}\n\n\t[[nodiscard]] bool recordVideoMessages() const {\n\t\treturn _recordVideoMessages;\n\t}\n\tvoid setRecordVideoMessages(bool value) {\n\t\t_recordVideoMessages = value;\n\t}\n\n\tvoid updateDialogsWidthRatio(float64 ratio, bool nochat);\n\t[[nodiscard]] float64 dialogsWidthRatio(bool nochat) const;\n\n\t[[nodiscard]] float64 dialogsWithChatWidthRatio() const;\n\t[[nodiscard]] rpl::producer<float64> dialogsWithChatWidthRatioChanges() const;\n\t[[nodiscard]] float64 dialogsNoChatWidthRatio() const;\n\t[[nodiscard]] rpl::producer<float64> dialogsNoChatWidthRatioChanges() const;\n\n\tvoid setThirdColumnWidth(int width);\n\t[[nodiscard]] int thirdColumnWidth() const;\n\t[[nodiscard]] rpl::producer<int> thirdColumnWidthChanges() const;\n\tvoid setNotifyFromAll(bool value) {\n\t\t_notifyFromAll = value;\n\t}\n\t[[nodiscard]] bool notifyFromAll() const {\n\t\treturn _notifyFromAll;\n\t}\n\tvoid setNativeWindowFrame(bool value) {\n\t\t_nativeWindowFrame = value;\n\t}\n\t[[nodiscard]] bool nativeWindowFrame() const {\n\t\treturn _nativeWindowFrame.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> nativeWindowFrameChanges() const {\n\t\treturn _nativeWindowFrame.changes();\n\t}\n\tvoid setSystemDarkMode(std::optional<bool> value) {\n\t\t_systemDarkMode = value;\n\t}\n\t[[nodiscard]] std::optional<bool> systemDarkMode() const {\n\t\treturn _systemDarkMode.current();\n\t}\n\t[[nodiscard]] rpl::producer<std::optional<bool>> systemDarkModeValue() const {\n\t\treturn _systemDarkMode.value();\n\t}\n\t[[nodiscard]] rpl::producer<std::optional<bool>> systemDarkModeChanges() const {\n\t\treturn _systemDarkMode.changes();\n\t}\n\tvoid setSystemDarkModeEnabled(bool value) {\n\t\t_systemDarkModeEnabled = value;\n\t}\n\t[[nodiscard]] bool systemDarkModeEnabled() const {\n\t\treturn _systemDarkModeEnabled.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> systemDarkModeEnabledValue() const {\n\t\treturn _systemDarkModeEnabled.value();\n\t}\n\t[[nodiscard]] rpl::producer<bool> systemDarkModeEnabledChanges() const {\n\t\treturn _systemDarkModeEnabled.changes();\n\t}\n\tvoid setSystemAccentColorEnabled(bool value) {\n\t\t_systemAccentColorEnabled = value;\n\t}\n\t[[nodiscard]] bool systemAccentColorEnabled() const {\n\t\treturn _systemAccentColorEnabled;\n\t}\n\t[[nodiscard]] WindowTitleContent windowTitleContent() const {\n\t\treturn _windowTitleContent.current();\n\t}\n\t[[nodiscard]] rpl::producer<WindowTitleContent> windowTitleContentChanges() const {\n\t\treturn _windowTitleContent.changes();\n\t}\n\tvoid setWindowTitleContent(WindowTitleContent content) {\n\t\t_windowTitleContent = content;\n\t}\n\t[[nodiscard]] const WindowPosition &windowPosition() const {\n\t\treturn _windowPosition;\n\t}\n\tvoid setWindowPosition(const WindowPosition &position) {\n\t\t_windowPosition = position;\n\t}\n\tvoid setWorkMode(WorkMode value) {\n\t\t_workMode = value;\n\t}\n\t[[nodiscard]] WorkMode workMode() const {\n\t\treturn _workMode.current();\n\t}\n\t[[nodiscard]] rpl::producer<WorkMode> workModeValue() const {\n\t\treturn _workMode.value();\n\t}\n\t[[nodiscard]] rpl::producer<WorkMode> workModeChanges() const {\n\t\treturn _workMode.changes();\n\t}\n\n\t[[nodiscard]] const std::vector<RecentEmoji> &recentEmoji() const;\n\tvoid incrementRecentEmoji(RecentEmojiId id);\n\tvoid hideRecentEmoji(RecentEmojiId id);\n\tvoid resetRecentEmoji();\n\tvoid setLegacyRecentEmojiPreload(QVector<QPair<QString, ushort>> data);\n\t[[nodiscard]] rpl::producer<> recentEmojiUpdated() const {\n\t\treturn _recentEmojiUpdated.events();\n\t}\n\n\t[[nodiscard]] const base::flat_map<QString, uint8> &emojiVariants() const {\n\t\treturn _emojiVariants;\n\t}\n\t[[nodiscard]] EmojiPtr lookupEmojiVariant(EmojiPtr emoji) const;\n\t[[nodiscard]] bool hasChosenEmojiVariant(EmojiPtr emoji) const;\n\tvoid saveEmojiVariant(EmojiPtr emoji);\n\tvoid saveAllEmojiVariants(EmojiPtr emoji);\n\tvoid setLegacyEmojiVariants(QMap<QString, int> data);\n\n\t[[nodiscard]] bool disableOpenGL() const {\n\t\treturn _disableOpenGL;\n\t}\n\tvoid setDisableOpenGL(bool value) {\n\t\t_disableOpenGL = value;\n\t}\n\n\t[[nodiscard]] base::flags<Calls::Group::StickedTooltip> hiddenGroupCallTooltips() const {\n\t\treturn _hiddenGroupCallTooltips;\n\t}\n\tvoid setHiddenGroupCallTooltip(Calls::Group::StickedTooltip value) {\n\t\t_hiddenGroupCallTooltips |= value;\n\t}\n\n\tvoid setCloseBehavior(CloseBehavior value) {\n\t\t_closeBehavior = value;\n\t}\n\t[[nodiscard]] CloseBehavior closeBehavior() const {\n\t\treturn _closeBehavior;\n\t}\n\tvoid setTrayIconMonochrome(bool value) {\n\t\t_trayIconMonochrome = value;\n\t}\n\t[[nodiscard]] bool trayIconMonochrome() const {\n\t\treturn _trayIconMonochrome.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> trayIconMonochromeChanges() const {\n\t\treturn _trayIconMonochrome.changes();\n\t}\n\n\tvoid setCustomDeviceModel(const QString &model) {\n\t\t_customDeviceModel = model;\n\t}\n\t[[nodiscard]] QString customDeviceModel() const {\n\t\treturn _customDeviceModel.current();\n\t}\n\t[[nodiscard]] rpl::producer<QString> customDeviceModelChanges() const {\n\t\treturn _customDeviceModel.changes();\n\t}\n\t[[nodiscard]] rpl::producer<QString> customDeviceModelValue() const {\n\t\treturn _customDeviceModel.value();\n\t}\n\t[[nodiscard]] QString deviceModel() const;\n\t[[nodiscard]] rpl::producer<QString> deviceModelChanges() const;\n\t[[nodiscard]] rpl::producer<QString> deviceModelValue() const;\n\n\tvoid setPlayerRepeatMode(Media::RepeatMode mode) {\n\t\t_playerRepeatMode = mode;\n\t}\n\t[[nodiscard]] Media::RepeatMode playerRepeatMode() const {\n\t\treturn _playerRepeatMode.current();\n\t}\n\t[[nodiscard]] rpl::producer<Media::RepeatMode> playerRepeatModeValue() const {\n\t\treturn _playerRepeatMode.value();\n\t}\n\t[[nodiscard]] rpl::producer<Media::RepeatMode> playerRepeatModeChanges() const {\n\t\treturn _playerRepeatMode.changes();\n\t}\n\tvoid setPlayerOrderMode(Media::OrderMode mode) {\n\t\t_playerOrderMode = mode;\n\t}\n\t[[nodiscard]] Media::OrderMode playerOrderMode() const {\n\t\treturn _playerOrderMode.current();\n\t}\n\t[[nodiscard]] rpl::producer<Media::OrderMode> playerOrderModeValue() const {\n\t\treturn _playerOrderMode.value();\n\t}\n\t[[nodiscard]] rpl::producer<Media::OrderMode> playerOrderModeChanges() const {\n\t\treturn _playerOrderMode.changes();\n\t}\n\t[[nodiscard]] std::vector<uint64> accountsOrder() const {\n\t\treturn _accountsOrder;\n\t}\n\tvoid setAccountsOrder(const std::vector<uint64> &order) {\n\t\t_accountsOrder = order;\n\t}\n\n\t[[nodiscard]] bool hardwareAcceleratedVideo() const {\n\t\treturn _hardwareAcceleratedVideo;\n\t}\n\tvoid setHardwareAcceleratedVideo(bool value) {\n\t\t_hardwareAcceleratedVideo = value;\n\t}\n\n\tvoid setMacWarnBeforeQuit(bool value) {\n\t\t_macWarnBeforeQuit = value;\n\t}\n\t[[nodiscard]] bool macWarnBeforeQuit() const {\n\t\treturn _macWarnBeforeQuit;\n\t}\n\tvoid setChatQuickAction(HistoryView::DoubleClickQuickAction value) {\n\t\t_chatQuickAction = value;\n\t}\n\t[[nodiscard]] HistoryView::DoubleClickQuickAction chatQuickAction() const {\n\t\treturn _chatQuickAction;\n\t}\n\n\tvoid setTranslateButtonEnabled(bool value);\n\t[[nodiscard]] bool translateButtonEnabled() const;\n\tvoid setUsePlatformTranslation(bool value);\n\t[[nodiscard]] bool usePlatformTranslation() const;\n\tvoid setTranslateChatEnabled(bool value);\n\t[[nodiscard]] bool translateChatEnabled() const;\n\t[[nodiscard]] rpl::producer<bool> translateChatEnabledValue() const;\n\tvoid setTranslateTo(LanguageId id);\n\t[[nodiscard]] LanguageId translateTo() const;\n\t[[nodiscard]] rpl::producer<LanguageId> translateToValue() const;\n\tvoid setSkipTranslationLanguages(std::vector<LanguageId> languages);\n\t[[nodiscard]] std::vector<LanguageId> skipTranslationLanguages() const;\n\t[[nodiscard]] auto skipTranslationLanguagesValue() const\n\t\t-> rpl::producer<std::vector<LanguageId>>;\n\n\tvoid setRememberedDeleteMessageOnlyForYou(bool value);\n\t[[nodiscard]] bool rememberedDeleteMessageOnlyForYou() const;\n\n\t[[nodiscard]] const WindowPosition &mediaViewPosition() const {\n\t\treturn _mediaViewPosition;\n\t}\n\tvoid setMediaViewPosition(const WindowPosition &position) {\n\t\t_mediaViewPosition = position;\n\t}\n\t[[nodiscard]] bool ignoreBatterySaving() const {\n\t\treturn _ignoreBatterySaving.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> ignoreBatterySavingValue() const {\n\t\treturn _ignoreBatterySaving.value();\n\t}\n\tvoid setIgnoreBatterySavingValue(bool value) {\n\t\t_ignoreBatterySaving = value;\n\t}\n\tvoid setMacRoundIconDigest(std::optional<uint64> value) {\n\t\t_macRoundIconDigest = value;\n\t}\n\t[[nodiscard]] std::optional<uint64> macRoundIconDigest() const {\n\t\treturn _macRoundIconDigest;\n\t}\n\t[[nodiscard]] bool storiesClickTooltipHidden() const {\n\t\treturn _storiesClickTooltipHidden.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> storiesClickTooltipHiddenValue() const {\n\t\treturn _storiesClickTooltipHidden.value();\n\t}\n\tvoid setStoriesClickTooltipHidden(bool value) {\n\t\t_storiesClickTooltipHidden = value;\n\t}\n\t[[nodiscard]] bool ttlVoiceClickTooltipHidden() const {\n\t\treturn _ttlVoiceClickTooltipHidden.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> ttlVoiceClickTooltipHiddenValue() const {\n\t\treturn _ttlVoiceClickTooltipHidden.value();\n\t}\n\tvoid setTtlVoiceClickTooltipHidden(bool value) {\n\t\t_ttlVoiceClickTooltipHidden = value;\n\t}\n\t[[nodiscard]] const WindowPosition &ivPosition() const {\n\t\treturn _ivPosition;\n\t}\n\tvoid setIvPosition(const WindowPosition &position) {\n\t\t_ivPosition = position;\n\t}\n\n\t[[nodiscard]] const WindowPosition &callPanelPosition() const {\n\t\treturn _callPanelPosition;\n\t}\n\tvoid setCallPanelPosition(const WindowPosition &position) {\n\t\t_callPanelPosition = position;\n\t}\n\n\t[[nodiscard]] QString customFontFamily() const {\n\t\treturn _customFontFamily;\n\t}\n\tvoid setCustomFontFamily(const QString &value) {\n\t\t_customFontFamily = value;\n\t}\n\n\t[[nodiscard]] bool systemUnlockEnabled() const {\n\t\treturn _systemUnlockEnabled;\n\t}\n\tvoid setSystemUnlockEnabled(bool enabled) {\n\t\t_systemUnlockEnabled = enabled;\n\t}\n\n\t[[nodiscard]] std::optional<bool> weatherInCelsius() const {\n\t\treturn _weatherInCelsius;\n\t}\n\tvoid setWeatherInCelsius(bool value) {\n\t\t_weatherInCelsius = value;\n\t}\n\n\t[[nodiscard]] QByteArray tonsiteStorageToken() const {\n\t\treturn _tonsiteStorageToken;\n\t}\n\tvoid setTonsiteStorageToken(const QByteArray &value) {\n\t\t_tonsiteStorageToken = value;\n\t}\n\n\t[[nodiscard]] int ivZoom() const;\n\t[[nodiscard]] rpl::producer<int> ivZoomValue() const;\n\tvoid setIvZoom(int value);\n\tbool normalizeIvZoom();\n\n\t[[nodiscard]] bool chatFiltersHorizontal() const;\n\t[[nodiscard]] rpl::producer<bool> chatFiltersHorizontalChanges() const;\n\tvoid setChatFiltersHorizontal(bool value);\n\n\t[[nodiscard]] Media::VideoQuality videoQuality() const;\n\tvoid setVideoQuality(Media::VideoQuality quality);\n\n\t[[nodiscard]] static bool ThirdColumnByDefault();\n\t[[nodiscard]] static float64 DefaultDialogsWidthRatio();\n\n\tstruct PlaybackSpeed {\n\t\tfloat64 value = Media::kSpedUpDefault;\n\t\tbool enabled = false;\n\n\t\tfriend bool operator==(\n\t\t\tconst PlaybackSpeed &,\n\t\t\tconst PlaybackSpeed &) = default;\n\t};\n\t[[nodiscard]] static qint32 SerializePlaybackSpeed(PlaybackSpeed speed);\n\t[[nodiscard]] static PlaybackSpeed DeserializePlaybackSpeed(\n\t\tqint32 speed);\n\n\t[[nodiscard]] Dialogs::Ui::QuickDialogAction quickDialogAction() const;\n\tvoid setQuickDialogAction(Dialogs::Ui::QuickDialogAction);\n\n\t[[nodiscard]] ushort notificationsVolume() const {\n\t\treturn _notificationsVolume;\n\t}\n\tvoid setNotificationsVolume(ushort value) {\n\t\t_notificationsVolume = value;\n\t}\n\n\ttemplate <typename Type, typename Other>\n\tvoid writePref(std::string_view key, Other &&value) {\n\t\twritePrefImpl<Type>(key, std::forward<Other>(value));\n\t}\n\tvoid clearPref(std::string_view key);\n\n\ttemplate <typename Type, typename Other = Type>\n\t[[nodiscard]] Type readPref(\n\t\t\tstd::string_view key,\n\t\t\tOther &&fallback = Type()) {\n\t\treturn readPrefImpl<Type>(key).value_or(\n\t\t\tstd::forward<Other>(fallback));\n\t}\n\n\tvoid resetOnLastLogout();\n\nprivate:\n\tvoid resolveRecentEmoji() const;\n\n\ttemplate <typename Type>\n\tvoid writePrefImpl(std::string_view key, Type value);\n\n\ttemplate <typename Type>\n\t[[nodiscard]] std::optional<Type> readPrefImpl(std::string_view key);\n\n\tvoid writePrefGeneric(std::string_view key, const QByteArray &value);\n\t[[nodiscard]] std::optional<QByteArray> readPrefGeneric(\n\t\tstd::string_view key);\n\n\tForkSettings _forkSettings;\n\n\tstatic constexpr auto kDefaultThirdColumnWidth = 0;\n\tstatic constexpr auto kDefaultDialogsWidthRatio = 5. / 14;\n\tstatic constexpr auto kDefaultBigDialogsWidthRatio = 0.275;\n\n\tstruct RecentEmojiPreload {\n\t\tQString emoji;\n\t\tushort rating = 0;\n\t};\n\n\tSettingsProxy _proxy;\n\n\trpl::variable<bool> _adaptiveForWide = true;\n\tbool _moderateModeEnabled = false;\n\trpl::variable<float64> _songVolume = kDefaultVolume;\n\trpl::variable<float64> _videoVolume = kDefaultVolume;\n\tbool _askDownloadPath = false;\n\trpl::variable<QString> _downloadPath;\n\tQByteArray _downloadPathBookmark;\n\tbool _soundNotify = true;\n\tbool _desktopNotify = true;\n\tbool _flashBounceNotify = true;\n\tNotifyView _notifyView = NotifyView::ShowPreview;\n\tstd::optional<bool> _nativeNotifications;\n\tbool _skipToastsInFocus = false;\n\tint _notificationsCount = 3;\n\tScreenCorner _notificationsCorner = ScreenCorner::BottomRight;\n\tint32 _notificationsDisplayChecksum = 0;\n\tbool _includeMutedCounter = true;\n\tbool _includeMutedCounterFolders = true;\n\tbool _countUnreadMessages = true;\n\trpl::variable<bool> _notifyAboutPinned = true;\n\tint _autoLock = 3600;\n\trpl::variable<QString> _playbackDeviceId;\n\trpl::variable<QString> _captureDeviceId;\n\trpl::variable<QString> _cameraDeviceId;\n\trpl::variable<QString> _callPlaybackDeviceId;\n\trpl::variable<QString> _callCaptureDeviceId;\n\tint _callOutputVolume = 100;\n\tint _callInputVolume = 100;\n\tbool _callAudioDuckingEnabled = true;\n\tbool _disableCallsLegacy = false;\n\tbool _groupCallPushToTalk = false;\n\tbool _groupCallNoiseSuppression = false;\n\tQByteArray _groupCallPushToTalkShortcut;\n\tcrl::time _groupCallPushToTalkDelay = 20;\n\tWindow::Theme::AccentColors _themesAccentColors;\n\tbool _lastSeenWarningSeen = false;\n\tUi::SendFilesWay _sendFilesWay = Ui::SendFilesWay();\n\trpl::variable<Ui::InputSubmitSettings> _sendSubmitWay\n\t\t= Ui::InputSubmitSettings();\n\tbase::flat_map<QString, QString> _soundOverrides;\n\tbase::flat_set<QString> _noWarningExtensions;\n\tbool _ipRevealWarning = true;\n\tbool _loopAnimatedStickers = true;\n\trpl::variable<bool> _largeEmoji = true;\n\trpl::variable<bool> _replaceEmoji = true;\n\trpl::variable<bool> _systemTextReplace = true;\n\tbool _suggestEmoji = true;\n\tbool _suggestStickersByEmoji = true;\n\tbool _suggestAnimatedEmoji = true;\n\trpl::variable<bool> _cornerReply = true;\n\trpl::variable<bool> _cornerReaction = true;\n\trpl::variable<bool> _spellcheckerEnabled = true;\n\tPlaybackSpeed _videoPlaybackSpeed;\n\trpl::variable<PlaybackSpeed> _voicePlaybackSpeed;\n\trpl::variable<PlaybackSpeed> _audioPlaybackSpeed;\n\tQByteArray _videoPipGeometry;\n\trpl::variable<std::vector<int>> _dictionariesEnabled;\n\trpl::variable<bool> _autoDownloadDictionaries = true;\n\trpl::variable<bool> _mainMenuAccountsShown = true;\n\tmutable std::vector<RecentEmojiPreload> _recentEmojiPreload;\n\tmutable std::vector<RecentEmoji> _recentEmoji;\n\tbase::flat_set<QString> _recentEmojiSkip;\n\tmutable bool _recentEmojiResolved = false;\n\tbase::flat_map<QString, uint8> _emojiVariants;\n\trpl::event_stream<> _recentEmojiUpdated;\n\tbool _tabbedSelectorSectionEnabled = false; // per-window\n\tWindow::Column _floatPlayerColumn = Window::Column(); // per-window\n\tRectPart _floatPlayerCorner = RectPart(); // per-window\n\tbool _thirdSectionInfoEnabled = true; // per-window\n\trpl::event_stream<bool> _thirdSectionInfoEnabledValue; // per-window\n\tint _thirdSectionExtendedBy = -1; // per-window\n\trpl::variable<float64> _dialogsWithChatWidthRatio; // per-window\n\trpl::variable<float64> _dialogsNoChatWidthRatio; // per-window\n\trpl::variable<int> _thirdColumnWidth = kDefaultThirdColumnWidth; // p-w\n\tbool _notifyFromAll = true;\n\trpl::variable<bool> _nativeWindowFrame = false;\n\trpl::variable<std::optional<bool>> _systemDarkMode = std::nullopt;\n\trpl::variable<bool> _systemDarkModeEnabled = true;\n\tbool _systemAccentColorEnabled = false;\n\trpl::variable<WindowTitleContent> _windowTitleContent;\n\tWindowPosition _windowPosition; // per-window\n\tbool _disableOpenGL = false;\n\trpl::variable<WorkMode> _workMode = WorkMode::WindowAndTray;\n\tbase::flags<Calls::Group::StickedTooltip> _hiddenGroupCallTooltips;\n\tCloseBehavior _closeBehavior = CloseBehavior::Quit;\n\trpl::variable<bool> _trayIconMonochrome = true;\n\trpl::variable<QString> _customDeviceModel;\n\trpl::variable<Media::RepeatMode> _playerRepeatMode;\n\trpl::variable<Media::OrderMode> _playerOrderMode;\n\tbool _macWarnBeforeQuit = true;\n\tstd::vector<uint64> _accountsOrder;\n#ifdef Q_OS_MAC\n\tbool _hardwareAcceleratedVideo = true;\n#else // Q_OS_MAC\n\tbool _hardwareAcceleratedVideo = false;\n#endif // Q_OS_MAC\n\tHistoryView::DoubleClickQuickAction _chatQuickAction\n\t\t= HistoryView::DoubleClickQuickAction();\n\tbool _translateButtonEnabled = false;\n\tbool _usePlatformTranslation = false;\n\trpl::variable<bool> _translateChatEnabled = true;\n\trpl::variable<int> _translateToRaw = 0;\n\trpl::variable<std::vector<LanguageId>> _skipTranslationLanguages;\n\trpl::event_stream<> _skipTranslationLanguagesChanges;\n\tbool _rememberedDeleteMessageOnlyForYou = false;\n\tWindowPosition _mediaViewPosition = { .maximized = 2 };\n\trpl::variable<bool> _ignoreBatterySaving = false;\n\tstd::optional<uint64> _macRoundIconDigest;\n\trpl::variable<bool> _storiesClickTooltipHidden = false;\n\trpl::variable<bool> _ttlVoiceClickTooltipHidden = false;\n\tWindowPosition _ivPosition;\n\tWindowPosition _callPanelPosition;\n\tQString _customFontFamily;\n\tbool _systemUnlockEnabled = false;\n\tstd::optional<bool> _weatherInCelsius;\n\tQByteArray _tonsiteStorageToken;\n\trpl::variable<int> _ivZoom = 0;\n\tMedia::VideoQuality _videoQuality;\n\trpl::variable<bool> _chatFiltersHorizontal = false;\n\tbase::flat_map<QByteArray, QByteArray> _prefs;\n\n\tbool _tabbedReplacedWithInfo = false; // per-window\n\trpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window\n\n\trpl::event_stream<> _saveDelayed;\n\tfloat64 _rememberedSongVolume = kDefaultVolume;\n\tbool _rememberedSoundNotifyFromTray = false;\n\tbool _rememberedFlashBounceNotifyFromTray = false;\n\tbool _dialogsWidthSetToZeroWithoutChat = false;\n\n\tbool _recordVideoMessages = false;\n\n\tDialogs::Ui::QuickDialogAction _quickDialogAction\n\t\t= Dialogs::Ui::QuickDialogAction::Disabled;\n\n\tushort _notificationsVolume = 100;\n\n\tQByteArray _photoEditorBrush;\n\n};\n\n} // namespace Core\n\n"
  },
  {
    "path": "Telegram/SourceFiles/core/core_settings_proxy.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/core_settings_proxy.h\"\n\n#include \"base/platform/base_platform_info.h\"\n#include \"storage/serialize_common.h\"\n\nnamespace Core {\nnamespace {\n\n[[nodiscard]] qint32 ProxySettingsToInt(MTP::ProxyData::Settings settings) {\n\tswitch(settings) {\n\tcase MTP::ProxyData::Settings::System: return 0;\n\tcase MTP::ProxyData::Settings::Enabled: return 1;\n\tcase MTP::ProxyData::Settings::Disabled: return 2;\n\t}\n\tUnexpected(\"Bad type in ProxySettingsToInt\");\n}\n\n[[nodiscard]] MTP::ProxyData::Settings IntToProxySettings(qint32 value) {\n\tswitch(value) {\n\tcase 0: return MTP::ProxyData::Settings::System;\n\tcase 1: return MTP::ProxyData::Settings::Enabled;\n\tcase 2: return MTP::ProxyData::Settings::Disabled;\n\t}\n\tUnexpected(\"Bad type in IntToProxySettings\");\n}\n\n[[nodiscard]] MTP::ProxyData DeserializeProxyData(const QByteArray &data) {\n\tQDataStream stream(data);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\n\tqint32 proxyType, port;\n\tMTP::ProxyData proxy;\n\tstream\n\t\t>> proxyType\n\t\t>> proxy.host\n\t\t>> port\n\t\t>> proxy.user\n\t\t>> proxy.password;\n\tproxy.port = port;\n\tproxy.type = [&] {\n\t\tswitch(proxyType) {\n\t\tcase 0: return MTP::ProxyData::Type::None;\n\t\tcase 1: return MTP::ProxyData::Type::Socks5;\n\t\tcase 2: return MTP::ProxyData::Type::Http;\n\t\tcase 3: return MTP::ProxyData::Type::Mtproto;\n\t\t}\n\t\tUnexpected(\"Bad type in DeserializeProxyData\");\n\t}();\n\treturn proxy;\n}\n\n[[nodiscard]] QByteArray SerializeProxyData(const MTP::ProxyData &proxy) {\n\tauto result = QByteArray();\n\tconst auto size = 1 * sizeof(qint32)\n\t\t+ Serialize::stringSize(proxy.host)\n\t\t+ 1 * sizeof(qint32)\n\t\t+ Serialize::stringSize(proxy.user)\n\t\t+ Serialize::stringSize(proxy.password);\n\n\tresult.reserve(size);\n\t{\n\t\tconst auto proxyType = [&] {\n\t\t\tswitch(proxy.type) {\n\t\t\tcase MTP::ProxyData::Type::None: return 0;\n\t\t\tcase MTP::ProxyData::Type::Socks5: return 1;\n\t\t\tcase MTP::ProxyData::Type::Http: return 2;\n\t\t\tcase MTP::ProxyData::Type::Mtproto: return 3;\n\t\t\t}\n\t\t\tUnexpected(\"Bad type in SerializeProxyData\");\n\t\t}();\n\n\t\tQDataStream stream(&result, QIODevice::WriteOnly);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\t\tstream\n\t\t\t<< qint32(proxyType)\n\t\t\t<< proxy.host\n\t\t\t<< qint32(proxy.port)\n\t\t\t<< proxy.user\n\t\t\t<< proxy.password;\n\t}\n\treturn result;\n}\n\n} // namespace\n\nSettingsProxy::SettingsProxy()\n: _tryIPv6(!Platform::IsWindows()) {\n}\n\nQByteArray SettingsProxy::serialize() const {\n\tconst auto serializedSelected = SerializeProxyData(_selected);\n\tconst auto serializedList = ranges::views::all(\n\t\t_list\n\t) | ranges::views::transform(SerializeProxyData) | ranges::to_vector;\n\n\tconst auto size = 3 * sizeof(qint32)\n\t\t+ Serialize::bytearraySize(serializedSelected)\n\t\t+ 1 * sizeof(qint32)\n\t\t+ ranges::accumulate(\n\t\t\tserializedList,\n\t\t\t0,\n\t\t\tranges::plus(),\n\t\t\t&Serialize::bytearraySize)\n\t\t+ 1 * sizeof(qint32); // _checkIpWarningShown\n\tauto stream = Serialize::ByteArrayWriter(size);\n\tstream\n\t\t<< qint32(_tryIPv6 ? 1 : 0)\n\t\t<< qint32(_useProxyForCalls ? 1 : 0)\n\t\t<< ProxySettingsToInt(_settings)\n\t\t<< serializedSelected\n\t\t<< qint32(_list.size());\n\tfor (const auto &i : serializedList) {\n\t\tstream << i;\n\t}\n\tstream << qint32(_checkIpWarningShown ? 1 : 0);\n\treturn std::move(stream).result();\n}\n\nbool SettingsProxy::setFromSerialized(const QByteArray &serialized) {\n\tif (serialized.isEmpty()) {\n\t\treturn true;\n\t}\n\n\tauto stream = Serialize::ByteArrayReader(serialized);\n\n\tauto tryIPv6 = qint32(_tryIPv6 ? 1 : 0);\n\tauto useProxyForCalls = qint32(_useProxyForCalls ? 1 : 0);\n\tauto settings = ProxySettingsToInt(_settings);\n\tauto listCount = qint32(_list.size());\n\tauto selectedProxy = QByteArray();\n\n\tif (!stream.atEnd()) {\n\t\tstream\n\t\t\t>> tryIPv6\n\t\t\t>> useProxyForCalls\n\t\t\t>> settings\n\t\t\t>> selectedProxy\n\t\t\t>> listCount;\n\t\tif (stream.ok()) {\n\t\t\tfor (auto i = 0; i != listCount; ++i) {\n\t\t\t\tQByteArray data;\n\t\t\t\tstream >> data;\n\t\t\t\t_list.push_back(DeserializeProxyData(data));\n\t\t\t}\n\t\t}\n\t}\n\n\tauto checkIpWarningShown = qint32(0);\n\tif (!stream.atEnd()) {\n\t\tstream >> checkIpWarningShown;\n\t}\n\n\tif (!stream.ok()) {\n\t\tLOG((\"App Error: \"\n\t\t\t\"Bad data for Core::SettingsProxy::setFromSerialized()\"));\n\t\treturn false;\n\t}\n\n\t_tryIPv6 = (tryIPv6 == 1);\n\t_useProxyForCalls = (useProxyForCalls == 1);\n\t_checkIpWarningShown = (checkIpWarningShown == 1);\n\t_settings = IntToProxySettings(settings);\n\t_selected = DeserializeProxyData(selectedProxy);\n\n\treturn true;\n}\n\nbool SettingsProxy::isEnabled() const {\n\treturn _settings == MTP::ProxyData::Settings::Enabled;\n}\n\nbool SettingsProxy::isSystem() const {\n\treturn _settings == MTP::ProxyData::Settings::System;\n}\n\nbool SettingsProxy::isDisabled() const {\n\treturn _settings == MTP::ProxyData::Settings::Disabled;\n}\n\nbool SettingsProxy::checkIpWarningShown() const {\n\treturn _checkIpWarningShown;\n}\n\nvoid SettingsProxy::setCheckIpWarningShown(bool value) {\n\t_checkIpWarningShown = value;\n}\n\nbool SettingsProxy::tryIPv6() const {\n\treturn _tryIPv6;\n}\n\nvoid SettingsProxy::setTryIPv6(bool value) {\n\t_tryIPv6 = value;\n}\n\nbool SettingsProxy::useProxyForCalls() const {\n\treturn _useProxyForCalls;\n}\n\nvoid SettingsProxy::setUseProxyForCalls(bool value) {\n\t_useProxyForCalls = value;\n}\n\nMTP::ProxyData::Settings SettingsProxy::settings() const {\n\treturn _settings;\n}\n\nvoid SettingsProxy::setSettings(MTP::ProxyData::Settings value) {\n\t_settings = value;\n}\n\nMTP::ProxyData SettingsProxy::selected() const {\n\treturn _selected;\n}\n\nvoid SettingsProxy::setSelected(MTP::ProxyData value) {\n\t_selected = value;\n}\n\nconst std::vector<MTP::ProxyData> &SettingsProxy::list() const {\n\treturn _list;\n}\n\nstd::vector<MTP::ProxyData> &SettingsProxy::list() {\n\treturn _list;\n}\n\nrpl::producer<> SettingsProxy::connectionTypeValue() const {\n\treturn _connectionTypeChanges.events_starting_with({});\n}\n\nrpl::producer<> SettingsProxy::connectionTypeChanges() const {\n\treturn _connectionTypeChanges.events();\n}\n\nvoid SettingsProxy::connectionTypeChangesNotify() {\n\t_connectionTypeChanges.fire({});\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/core_settings_proxy.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/mtproto_proxy_data.h\"\n\nnamespace Core {\n\nclass SettingsProxy final {\npublic:\n\tSettingsProxy();\n\n\t[[nodiscard]] bool isEnabled() const;\n\t[[nodiscard]] bool isSystem() const;\n\t[[nodiscard]] bool isDisabled() const;\n\n\t[[nodiscard]] rpl::producer<> connectionTypeChanges() const;\n\t[[nodiscard]] rpl::producer<> connectionTypeValue() const;\n\tvoid connectionTypeChangesNotify();\n\n\t[[nodiscard]] bool tryIPv6() const;\n\tvoid setTryIPv6(bool value);\n\n\t[[nodiscard]] bool useProxyForCalls() const;\n\tvoid setUseProxyForCalls(bool value);\n\n\t[[nodiscard]] MTP::ProxyData::Settings settings() const;\n\tvoid setSettings(MTP::ProxyData::Settings value);\n\n\t[[nodiscard]] MTP::ProxyData selected() const;\n\tvoid setSelected(MTP::ProxyData value);\n\n\t[[nodiscard]] bool checkIpWarningShown() const;\n\tvoid setCheckIpWarningShown(bool value);\n\n\t[[nodiscard]] const std::vector<MTP::ProxyData> &list() const;\n\t[[nodiscard]] std::vector<MTP::ProxyData> &list();\n\n\t[[nodiscard]] QByteArray serialize() const;\n\tbool setFromSerialized(const QByteArray &serialized);\n\nprivate:\n\tbool _tryIPv6 = false;\n\tbool _useProxyForCalls = false;\n\tbool _checkIpWarningShown = false;\n\tMTP::ProxyData::Settings _settings = MTP::ProxyData::Settings::System;\n\tMTP::ProxyData _selected;\n\tstd::vector<MTP::ProxyData> _list;\n\n\trpl::event_stream<> _connectionTypeChanges;\n\n};\n\n} // namespace Core\n\n"
  },
  {
    "path": "Telegram/SourceFiles/core/crash_report_window.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/crash_report_window.h\"\n\n#include \"core/crash_reports.h\"\n#include \"core/application.h\"\n#include \"core/sandbox.h\"\n#include \"core/update_checker.h\"\n#include \"core/ui_integration.h\"\n#include \"window/main_window.h\"\n#include \"platform/platform_specific.h\"\n#include \"base/zlib_help.h\"\n\n#include <QtWidgets/QFileDialog>\n#include <QtGui/QFontInfo>\n#include <QtGui/QScreen>\n#include <QtGui/QDesktopServices>\n#include <QtCore/QStandardPaths>\n#include <QtCore/QTimer>\n\nnamespace {\n\nconstexpr auto kDefaultProxyPort = 80;\n\n} // namespace\n\nPreLaunchWindow *PreLaunchWindowInstance = nullptr;\n\nPreLaunchWindow::PreLaunchWindow(QString title) {\n\tsetWindowIcon(Window::CreateIcon());\n\tsetWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);\n\n\tsetWindowTitle(title.isEmpty() ? u\"Telegram\"_q : title);\n\n\tQPalette p(palette());\n\tp.setColor(QPalette::Window, QColor(255, 255, 255));\n\tsetPalette(p);\n\n\t_size = QFontInfo(font()).pixelSize();\n\n\tint paddingVertical = (_size / 2);\n\tint paddingHorizontal = _size;\n\tint borderRadius = (_size / 5);\n\tsetStyleSheet(uR\"(\nQPushButton {\n\tpadding: %1px %2px;\n\tbackground-color: #ffffff;\n\tborder-radius: %3px;\n}\nQPushButton#confirm:hover,\nQPushButton#cancel:hover {\n\tbackground-color: #e3f1fa;\n\tcolor: #2f9fea;\n}\nQPushButton#confirm {\n\tcolor: #2f9fea;\n}\nQPushButton#cancel {\n\tcolor: #aeaeae;\n}\nQLineEdit {\n\tborder: 1px solid #e0e0e0;\n\tpadding: 5px;\n}\nQLineEdit:focus {\n\tborder: 2px solid #37a1de;\n\tpadding: 4px;\n}\n)\"_q.arg(paddingVertical).arg(paddingHorizontal).arg(borderRadius));\n\tif (!PreLaunchWindowInstance) {\n\t\tPreLaunchWindowInstance = this;\n\t}\n}\n\nvoid PreLaunchWindow::activate() {\n\tsetWindowState(windowState() & ~Qt::WindowMinimized);\n\tsetVisible(true);\n\tPlatform::ActivateThisProcess();\n\traise();\n\tactivateWindow();\n}\n\nPreLaunchWindow *PreLaunchWindow::instance() {\n\treturn PreLaunchWindowInstance;\n}\n\nPreLaunchWindow::~PreLaunchWindow() {\n\tif (PreLaunchWindowInstance == this) {\n\t\tPreLaunchWindowInstance = nullptr;\n\t}\n}\n\nPreLaunchLabel::PreLaunchLabel(QWidget *parent) : QLabel(parent) {\n\tQFont labelFont(font());\n\tlabelFont.setWeight(QFont::DemiBold);\n\tlabelFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());\n\tsetFont(labelFont);\n\n\tQPalette p(palette());\n\tp.setColor(QPalette::WindowText, QColor(0, 0, 0));\n\tp.setColor(QPalette::Text, QColor(0, 0, 0));\n\tsetPalette(p);\n\tshow();\n};\n\nvoid PreLaunchLabel::setText(const QString &text) {\n\tQLabel::setText(text);\n\tupdateGeometry();\n\tresize(sizeHint());\n}\n\nPreLaunchInput::PreLaunchInput(QWidget *parent, bool password) : QLineEdit(parent) {\n\tQFont logFont(font());\n\tlogFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());\n\tsetFont(logFont);\n\n\tQPalette p(palette());\n\tp.setColor(QPalette::Window, QColor(255, 255, 255));\n\tp.setColor(QPalette::Base, QColor(255, 255, 255));\n\tp.setColor(QPalette::WindowText, QColor(0, 0, 0));\n\tp.setColor(QPalette::Text, QColor(0, 0, 0));\n\tsetPalette(p);\n\n\tQLineEdit::setTextMargins(0, 0, 0, 0);\n\tsetContentsMargins(0, 0, 0, 0);\n\tif (password) {\n\t\tsetEchoMode(QLineEdit::Password);\n\t}\n\tshow();\n};\n\nPreLaunchLog::PreLaunchLog(QWidget *parent) : QTextEdit(parent) {\n\tQFont logFont(font());\n\tlogFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());\n\tsetFont(logFont);\n\n\tQPalette p(palette());\n\tp.setColor(QPalette::WindowText, QColor(96, 96, 96));\n\tp.setColor(QPalette::Text, QColor(96, 96, 96));\n\tsetPalette(p);\n\n\tsetReadOnly(true);\n\tsetFrameStyle(int(QFrame::NoFrame) | QFrame::Plain);\n\tviewport()->setAutoFillBackground(false);\n\tsetContentsMargins(0, 0, 0, 0);\n\tdocument()->setDocumentMargin(0);\n\tshow();\n};\n\nPreLaunchButton::PreLaunchButton(QWidget *parent, bool confirm) : QPushButton(parent) {\n\tsetFlat(true);\n\n\tsetObjectName(confirm ? \"confirm\" : \"cancel\");\n\n\tQFont closeFont(font());\n\tcloseFont.setWeight(QFont::DemiBold);\n\tcloseFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());\n\tsetFont(closeFont);\n\n\tsetCursor(Qt::PointingHandCursor);\n\tshow();\n};\n\nvoid PreLaunchButton::setText(const QString &text) {\n\tQPushButton::setText(text);\n\tupdateGeometry();\n\tresize(sizeHint());\n}\n\nPreLaunchCheckbox::PreLaunchCheckbox(QWidget *parent) : QCheckBox(parent) {\n\tsetTristate(false);\n\tsetCheckState(Qt::Checked);\n\n\tQFont closeFont(font());\n\tcloseFont.setWeight(QFont::DemiBold);\n\tcloseFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());\n\tsetFont(closeFont);\n\n\tQPalette p(palette());\n\tp.setColor(QPalette::WindowText, QColor(96, 96, 96));\n\tp.setColor(QPalette::Text, QColor(96, 96, 96));\n\tsetPalette(p);\n\n\tsetCursor(Qt::PointingHandCursor);\n\tshow();\n};\n\nvoid PreLaunchCheckbox::setText(const QString &text) {\n\tQCheckBox::setText(text);\n\tupdateGeometry();\n\tresize(sizeHint());\n}\n\nNotStartedWindow::NotStartedWindow()\n: _label(this)\n, _log(this)\n, _close(this) {\n\t_label.setText(u\"Could not start Telegram Desktop!\\nYou can see complete log below:\"_q);\n\n\t_log.setPlainText(Logs::full());\n\n\tconnect(&_close, &QPushButton::clicked, [=] { close(); });\n\t_close.setText(u\"CLOSE\"_q);\n\n\tQRect scr(QApplication::primaryScreen()->availableGeometry());\n\tmove(scr.x() + (scr.width() / 6), scr.y() + (scr.height() / 6));\n\tupdateControls();\n\tshow();\n}\n\nvoid NotStartedWindow::updateControls() {\n\t_label.show();\n\t_log.show();\n\t_close.show();\n\n\tQRect scr(QApplication::primaryScreen()->availableGeometry());\n\tQSize s(scr.width() / 2, scr.height() / 2);\n\tif (s == size()) {\n\t\tresizeEvent(0);\n\t} else {\n\t\tresize(s);\n\t}\n}\n\nvoid NotStartedWindow::closeEvent(QCloseEvent *e) {\n\tdeleteLater();\n\tCore::Quit();\n}\n\nvoid NotStartedWindow::resizeEvent(QResizeEvent *e) {\n\tint padding = _size;\n\t_label.setGeometry(padding, padding, width() - 2 * padding, _label.sizeHint().height());\n\t_log.setGeometry(padding, padding * 2 + _label.sizeHint().height(), width() - 2 * padding, height() - 4 * padding - _label.height() - _close.height());\n\t_close.setGeometry(width() - padding - _close.width(), height() - padding - _close.height(), _close.width(), _close.height());\n}\n\nLastCrashedWindow::UpdaterData::UpdaterData(QWidget *buttonParent)\n: check(buttonParent)\n, skip(buttonParent, false) {\n}\n\nLastCrashedWindow::LastCrashedWindow(\n\tconst QByteArray &crashdump,\n\tFn<void()> launch)\n: _dumpraw(crashdump)\n, _label(this)\n, _pleaseSendReport(this)\n, _yourReportName(this)\n, _minidump(this)\n, _report(this)\n, _send(this)\n, _sendSkip(this, false)\n, _networkSettings(this)\n, _continue(this)\n, _showReport(this)\n, _saveReport(this)\n, _getApp(this)\n, _includeUsername(this)\n, _reportText(QString::fromUtf8(crashdump))\n, _reportShown(false)\n, _reportSaved(false)\n, _sendingState(crashdump.isEmpty() ? SendingNoReport : SendingUpdateCheck)\n, _updating(this)\n, _updaterData(Core::UpdaterDisabled()\n\t? nullptr\n\t: std::make_unique<UpdaterData>(this))\n, _launch(std::move(launch)) {\n\texcludeReportUsername();\n\n\tif (!cInstallBetaVersion() && !cAlphaVersion()) {\n\t\t// Currently accept crash reports only from testers.\n\t\t_sendingState = SendingNoReport;\n\t} else if (Core::OpenGLLastCheckFailed()) {\n\t\t// Nothing we can do right now with graphics driver crashes in GL.\n\t\t_sendingState = SendingNoReport;\n\t}\n\tif (_sendingState != SendingNoReport) {\n\t\tqint64 dumpsize = 0;\n\t\tQString dumpspath = cWorkingDir() + u\"tdata/dumps\"_q;\n#if defined Q_OS_MAC && !defined MAC_USE_BREAKPAD\n\t\tdumpspath += u\"/completed\"_q;\n#endif\n\t\tQString possibleDump = getReportField(qstr(\"minidump\"), qstr(\"Minidump:\"));\n\t\tif (!possibleDump.isEmpty()) {\n\t\t\tif (!possibleDump.startsWith('/')) {\n\t\t\t\tpossibleDump = dumpspath + '/' + possibleDump;\n\t\t\t}\n\t\t\tif (!possibleDump.endsWith(qstr(\".dmp\"))) {\n\t\t\t\tpossibleDump += u\".dmp\"_q;\n\t\t\t}\n\t\t\tQFileInfo possibleInfo(possibleDump);\n\t\t\tif (possibleInfo.exists()) {\n\t\t\t\t_minidumpName = possibleInfo.fileName();\n\t\t\t\t_minidumpFull = possibleInfo.absoluteFilePath();\n\t\t\t\tdumpsize = possibleInfo.size();\n\t\t\t}\n\t\t}\n\t\tif (_minidumpFull.isEmpty()) {\n\t\t\tQString maxDump, maxDumpFull;\n\t\t\tQDateTime maxDumpModified, workingModified = QFileInfo(cWorkingDir() + u\"tdata/working\"_q).lastModified();\n\t\t\tQFileInfoList list = QDir(dumpspath).entryInfoList();\n\t\t\tfor (int32 i = 0, l = list.size(); i < l; ++i) {\n\t\t\t\tQString name = list.at(i).fileName();\n\t\t\t\tif (name.endsWith(qstr(\".dmp\"))) {\n\t\t\t\t\tQDateTime modified = list.at(i).lastModified();\n\t\t\t\t\tif (maxDump.isEmpty() || qAbs(workingModified.secsTo(modified)) < qAbs(workingModified.secsTo(maxDumpModified))) {\n\t\t\t\t\t\tmaxDump = name;\n\t\t\t\t\t\tmaxDumpModified = modified;\n\t\t\t\t\t\tmaxDumpFull = list.at(i).absoluteFilePath();\n\t\t\t\t\t\tdumpsize = list.at(i).size();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!maxDump.isEmpty() && qAbs(workingModified.secsTo(maxDumpModified)) < 10) {\n\t\t\t\t_minidumpName = maxDump;\n\t\t\t\t_minidumpFull = maxDumpFull;\n\t\t\t}\n\t\t}\n\t\tif (_minidumpName.isEmpty()) { // currently don't accept crash reports without dumps from google libraries\n\t\t\t_sendingState = SendingNoReport;\n\t\t} else {\n\t\t\t_minidump.setText(u\"+ %1 (%2 KB)\"_q.arg(_minidumpName).arg(dumpsize / 1024));\n\t\t}\n\t}\n\tif (_sendingState != SendingNoReport) {\n\t\tQString version = getReportField(qstr(\"version\"), qstr(\"Version:\"));\n\t\tQString current = cAlphaVersion() ? u\"-%1\"_q.arg(cAlphaVersion()) : QString::number(AppVersion);\n\t\tif (version != current) { // currently don't accept crash reports from not current app version\n\t\t\t_sendingState = SendingNoReport;\n\t\t}\n\t}\n\n\t_networkSettings.setText(u\"NETWORK SETTINGS\"_q);\n\tconnect(\n\t\t&_networkSettings,\n\t\t&QPushButton::clicked,\n\t\t[=] { networkSettings(); });\n\n\tif (_sendingState == SendingNoReport) {\n\t\t_label.setText(u\"Last time Telegram Desktop was not closed properly.\"_q);\n\t} else {\n\t\t_label.setText(u\"Last time Telegram Desktop crashed :(\"_q);\n\t}\n\n\tif (_updaterData) {\n\t\t_updaterData->check.setText(u\"TRY AGAIN\"_q);\n\t\tconnect(\n\t\t\t&_updaterData->check,\n\t\t\t&QPushButton::clicked,\n\t\t\t[=] { updateRetry(); });\n\t\t_updaterData->skip.setText(u\"SKIP\"_q);\n\t\tconnect(\n\t\t\t&_updaterData->skip,\n\t\t\t&QPushButton::clicked,\n\t\t\t[=] { updateSkip(); });\n\n\t\tCore::UpdateChecker checker;\n\t\tusing Progress = Core::UpdateChecker::Progress;\n\t\tchecker.checking(\n\t\t) | rpl::on_next([=] {\n\t\t\tAssert(_updaterData != nullptr);\n\n\t\t\tsetUpdatingState(UpdatingCheck);\n\t\t}, _lifetime);\n\n\t\tchecker.isLatest(\n\t\t) | rpl::on_next([=] {\n\t\t\tAssert(_updaterData != nullptr);\n\n\t\t\tsetUpdatingState(UpdatingLatest);\n\t\t}, _lifetime);\n\n\t\tchecker.progress(\n\t\t) | rpl::on_next([=](const Progress &result) {\n\t\t\tAssert(_updaterData != nullptr);\n\n\t\t\tsetUpdatingState(UpdatingDownload);\n\t\t\tsetDownloadProgress(result.already, result.size);\n\t\t}, _lifetime);\n\n\t\tchecker.failed(\n\t\t) | rpl::on_next([=] {\n\t\t\tAssert(_updaterData != nullptr);\n\n\t\t\tsetUpdatingState(UpdatingFail);\n\t\t}, _lifetime);\n\n\t\tchecker.ready(\n\t\t) | rpl::on_next([=] {\n\t\t\tAssert(_updaterData != nullptr);\n\n\t\t\tsetUpdatingState(UpdatingReady);\n\t\t}, _lifetime);\n\n\t\tswitch (checker.state()) {\n\t\tcase Core::UpdateChecker::State::Download:\n\t\t\tsetUpdatingState(UpdatingDownload, true);\n\t\t\tsetDownloadProgress(checker.already(), checker.size());\n\t\t\tbreak;\n\t\tcase Core::UpdateChecker::State::Ready:\n\t\t\tsetUpdatingState(UpdatingReady, true);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tsetUpdatingState(UpdatingCheck, true);\n\t\t\tbreak;\n\t\t}\n\n\t\tcSetLastUpdateCheck(0);\n\t\tchecker.start();\n\t} else {\n\t\t_updating.setText(u\"Please check if there is a new version available.\"_q);\n\t\tif (_sendingState != SendingNoReport) {\n\t\t\t_sendingState = SendingNone;\n\t\t}\n\t}\n\n\t_pleaseSendReport.setText(u\"Please send us a crash report.\"_q);\n\t_yourReportName.setText(u\"Crash ID: %1\"_q.arg(QString(_minidumpName).replace(\".dmp\", \"\")));\n\t_yourReportName.setCursor(style::cur_text);\n\t_yourReportName.setTextInteractionFlags(Qt::TextSelectableByMouse);\n\n\t_includeUsername.setText(u\"Include username @%1 as your contact info\"_q.arg(_reportUsername));\n\n\t_report.setPlainText(_reportTextNoUsername);\n\n\t_showReport.setText(u\"VIEW REPORT\"_q);\n\tconnect(&_showReport, &QPushButton::clicked, [=] {\n\t\t_reportShown = !_reportShown;\n\t\tupdateControls();\n\t});\n\t_saveReport.setText(u\"SAVE TO FILE\"_q);\n\tconnect(&_saveReport, &QPushButton::clicked, [=] { saveReport(); });\n\t_getApp.setText(u\"GET THE LATEST OFFICIAL VERSION OF TELEGRAM DESKTOP\"_q);\n\tconnect(&_getApp, &QPushButton::clicked, [=] {\n\t\tQDesktopServices::openUrl(u\"https://desktop.telegram.org\"_q);\n\t});\n\n\t_send.setText(u\"SEND CRASH REPORT\"_q);\n\tconnect(&_send, &QPushButton::clicked, [=] { sendReport(); });\n\n\t_sendSkip.setText(u\"SKIP\"_q);\n\tconnect(&_sendSkip, &QPushButton::clicked, [=] { processContinue(); });\n\t_continue.setText(u\"CONTINUE\"_q);\n\tconnect(&_continue, &QPushButton::clicked, [=] { processContinue(); });\n\n\tQRect scr(QApplication::primaryScreen()->availableGeometry());\n\tmove(scr.x() + (scr.width() / 6), scr.y() + (scr.height() / 6));\n\tupdateControls();\n\tshow();\n}\n\nvoid LastCrashedWindow::saveReport() {\n\tQString to = QFileDialog::getSaveFileName(0, u\"Telegram Crash Report\"_q, QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + u\"/report.telegramcrash\"_q, u\"Telegram crash report (*.telegramcrash)\"_q);\n\tif (!to.isEmpty()) {\n\t\tQFile file(to);\n\t\tif (file.open(QIODevice::WriteOnly)) {\n\t\t\tfile.write(getCrashReportRaw());\n\t\t\t_reportSaved = true;\n\t\t\tupdateControls();\n\t\t}\n\t}\n}\n\nQByteArray LastCrashedWindow::getCrashReportRaw() const {\n\tauto result = _dumpraw;\n\tif (!_reportUsername.isEmpty() && _includeUsername.checkState() != Qt::Checked) {\n\t\tresult.replace(\n\t\t\t(u\"Username: \"_q + _reportUsername).toUtf8(),\n\t\t\t\"Username: _not_included_\");\n\t}\n\treturn result;\n}\n\nvoid LastCrashedWindow::excludeReportUsername() {\n\tQString prefix = qstr(\"Username:\");\n\tQStringList lines = _reportText.split('\\n');\n\tfor (int32 i = 0, l = lines.size(); i < l; ++i) {\n\t\tif (lines.at(i).trimmed().startsWith(prefix)) {\n\t\t\t_reportUsername = lines.at(i).trimmed().mid(prefix.size()).trimmed();\n\t\t\tlines.removeAt(i);\n\t\t\tbreak;\n\t\t}\n\t}\n\t_reportTextNoUsername = _reportUsername.isEmpty() ? _reportText : lines.join('\\n');\n}\n\nQString LastCrashedWindow::getReportField(const QLatin1String &name, const QLatin1String &prefix) {\n\tQStringList lines = _reportText.split('\\n');\n\tfor (int32 i = 0, l = lines.size(); i < l; ++i) {\n\t\tif (lines.at(i).trimmed().startsWith(prefix)) {\n\t\t\tQString data = lines.at(i).trimmed().mid(prefix.size()).trimmed();\n\n\t\t\tif (name == qstr(\"version\")) {\n\t\t\t\tif (data.endsWith(qstr(\" alpha\"))) {\n\t\t\t\t\tdata = QString::number(-data.replace(QRegularExpression(u\"[^\\\\d]\"_q), \"\").toLongLong());\n\t\t\t\t} else {\n\t\t\t\t\tdata = QString::number(data.replace(QRegularExpression(u\"[^\\\\d]\"_q), \"\").toLongLong());\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn data;\n\t\t}\n\t}\n\treturn QString();\n}\n\nvoid LastCrashedWindow::addReportFieldPart(const QLatin1String &name, const QLatin1String &prefix, QHttpMultiPart *multipart) {\n\tQString data = getReportField(name, prefix);\n\tif (!data.isEmpty()) {\n\t\tQHttpPart reportPart;\n\t\treportPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant(u\"form-data; name=\\\"%1\\\"\"_q.arg(name)));\n\t\treportPart.setBody(data.toUtf8());\n\t\tmultipart->append(reportPart);\n\t}\n}\n\nvoid LastCrashedWindow::sendReport() {\n\tif (_checkReply) {\n\t\t_checkReply->deleteLater();\n\t\t_checkReply = nullptr;\n\t}\n\tif (_sendReply) {\n\t\t_sendReply->deleteLater();\n\t\t_sendReply = nullptr;\n\t}\n\n\tQString apiid = getReportField(qstr(\"apiid\"), qstr(\"ApiId:\")), version = getReportField(qstr(\"version\"), qstr(\"Version:\"));\n\t_checkReply = _sendManager.get(QNetworkRequest(u\"https://tdesktop.com/crash.php?act=query_report&apiid=%1&version=%2&dmp=%3&platform=%4\"_q.arg(\n\t\tapiid,\n\t\tversion,\n\t\tQString::number(minidumpFileName().isEmpty() ? 0 : 1),\n\t\tCrashReports::PlatformString())));\n\n\tconnect(\n\t\t_checkReply,\n\t\t&QNetworkReply::errorOccurred,\n\t\t[=](QNetworkReply::NetworkError code) { sendingError(code); });\n\tconnect(\n\t\t_checkReply,\n\t\t&QNetworkReply::finished,\n\t\t[=] { checkingFinished(); });\n\n\t_pleaseSendReport.setText(u\"Sending crash report...\"_q);\n\t_sendingState = SendingProgress;\n\t_reportShown = false;\n\tupdateControls();\n}\n\nQString LastCrashedWindow::minidumpFileName() {\n\tQFileInfo dmpFile(_minidumpFull);\n\tif (dmpFile.exists() && dmpFile.size() > 0 && dmpFile.size() < 20 * 1024 * 1024 &&\n\t\tQRegularExpression(u\"^[a-zA-Z0-9\\\\-]{1,64}\\\\.dmp$\"_q).match(dmpFile.fileName()).hasMatch()) {\n\t\treturn dmpFile.fileName();\n\t}\n\treturn QString();\n}\n\nvoid LastCrashedWindow::checkingFinished() {\n\tif (!_checkReply || _sendReply) return;\n\n\tQByteArray result = _checkReply->readAll().trimmed();\n\t_checkReply->deleteLater();\n\t_checkReply = nullptr;\n\n\tLOG((\"Crash report check for sending done, result: %1\").arg(QString::fromUtf8(result)));\n\n\tif (result == \"Old\") {\n\t\t_pleaseSendReport.setText(u\"This report is about some old version of Telegram Desktop.\"_q);\n\t\t_sendingState = SendingTooOld;\n\t\tupdateControls();\n\t\treturn;\n\t} else if (result == \"Unofficial\") {\n\t\t_pleaseSendReport.setText(u\"You use some custom version of Telegram Desktop.\"_q);\n\t\t_sendingState = SendingUnofficial;\n\t\tupdateControls();\n\t\treturn;\n\t} else if (result != \"Report\") {\n\t\t_pleaseSendReport.setText(u\"Thank you for your report!\"_q);\n\t\t_sendingState = SendingDone;\n\t\tupdateControls();\n\n\t\tCrashReports::Restart();\n\t\treturn;\n\t}\n\n\tauto multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType);\n\n\taddReportFieldPart(qstr(\"platform\"), qstr(\"Platform:\"), multipart);\n\taddReportFieldPart(qstr(\"version\"), qstr(\"Version:\"), multipart);\n\n\tQHttpPart reportPart;\n\treportPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant(\"application/octet-stream\"));\n\treportPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant(\"form-data; name=\\\"report\\\"; filename=\\\"report.telegramcrash\\\"\"));\n\treportPart.setBody(getCrashReportRaw());\n\tmultipart->append(reportPart);\n\n\tQString dmpName = minidumpFileName();\n\tif (!dmpName.isEmpty()) {\n\t\tQFile file(_minidumpFull);\n\t\tif (file.open(QIODevice::ReadOnly)) {\n\t\t\tQByteArray minidump = file.readAll();\n\t\t\tfile.close();\n\n\t\t\tQString zipName = QString(dmpName).replace(qstr(\".dmp\"), qstr(\".zip\"));\n\n\t\t\tzlib::FileToWrite minidumpZip;\n\n\t\t\tzip_fileinfo zfi = { { 0, 0, 0, 0, 0, 0 }, 0, 0, 0 };\n\t\t\tQByteArray dmpNameUtf = dmpName.toUtf8();\n\t\t\tminidumpZip.openNewFile(dmpNameUtf.constData(), &zfi, nullptr, 0, nullptr, 0, nullptr, Z_DEFLATED, Z_DEFAULT_COMPRESSION);\n\t\t\tminidumpZip.writeInFile(minidump.constData(), minidump.size());\n\t\t\tminidumpZip.closeFile();\n\t\t\tminidumpZip.close();\n\n\t\t\tif (minidumpZip.error() == ZIP_OK) {\n\t\t\t\tQHttpPart dumpPart;\n\t\t\t\tdumpPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant(\"application/octet-stream\"));\n\t\t\t\tdumpPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant(u\"form-data; name=\\\"dump\\\"; filename=\\\"%1\\\"\"_q.arg(zipName)));\n\t\t\t\tdumpPart.setBody(minidumpZip.result());\n\t\t\t\tmultipart->append(dumpPart);\n\n\t\t\t\t_minidump.setText(u\"+ %1 (%2 KB)\"_q.arg(zipName).arg(minidumpZip.result().size() / 1024));\n\t\t\t}\n\t\t}\n\t}\n\n\t_sendReply = _sendManager.post(QNetworkRequest(u\"https://tdesktop.com/crash.php?act=report\"_q), multipart);\n\tmultipart->setParent(_sendReply);\n\n\tconnect(\n\t\t_sendReply,\n\t\t&QNetworkReply::errorOccurred,\n\t\t[=](QNetworkReply::NetworkError code) { sendingError(code); });\n\tconnect(\n\t\t_sendReply,\n\t\t&QNetworkReply::finished,\n\t\t[=] { sendingFinished(); });\n\tconnect(\n\t\t_sendReply,\n\t\t&QNetworkReply::uploadProgress,\n\t\t[=](qint64 sent, qint64 total) { sendingProgress(sent, total); });\n\n\tupdateControls();\n}\n\nvoid LastCrashedWindow::updateControls() {\n\tint padding = _size, h = padding + _networkSettings.height() + padding;\n\n\t_label.show();\n\tif (_updaterData) {\n\t\th += _networkSettings.height() + padding;\n\t\tif (_updaterData->state == UpdatingFail && (_sendingState == SendingNoReport || _sendingState == SendingUpdateCheck)) {\n\t\t\t_networkSettings.show();\n\t\t\t_updaterData->check.show();\n\t\t\t_updaterData->skip.show();\n\t\t\t_send.hide();\n\t\t\t_sendSkip.hide();\n\t\t\t_continue.hide();\n\t\t\t_pleaseSendReport.hide();\n\t\t\t_yourReportName.hide();\n\t\t\t_includeUsername.hide();\n\t\t\t_getApp.hide();\n\t\t\t_showReport.hide();\n\t\t\t_report.hide();\n\t\t\t_minidump.hide();\n\t\t\t_saveReport.hide();\n\t\t\th += padding + _updaterData->check.height() + padding;\n\t\t} else {\n\t\t\tif (_updaterData->state == UpdatingCheck\n\t\t\t\t|| _sendingState == SendingFail\n\t\t\t\t|| _sendingState == SendingProgress) {\n\t\t\t\t_networkSettings.show();\n\t\t\t} else {\n\t\t\t\t_networkSettings.hide();\n\t\t\t}\n\t\t\tif (_updaterData->state == UpdatingNone\n\t\t\t\t|| _updaterData->state == UpdatingLatest\n\t\t\t\t|| _updaterData->state == UpdatingFail) {\n\t\t\t\th += padding + _updaterData->check.height() + padding;\n\t\t\t\tif (_sendingState == SendingNoReport) {\n\t\t\t\t\t_pleaseSendReport.hide();\n\t\t\t\t\t_yourReportName.hide();\n\t\t\t\t\t_includeUsername.hide();\n\t\t\t\t\t_getApp.hide();\n\t\t\t\t\t_showReport.hide();\n\t\t\t\t\t_report.hide();\n\t\t\t\t\t_minidump.hide();\n\t\t\t\t\t_saveReport.hide();\n\t\t\t\t\t_send.hide();\n\t\t\t\t\t_sendSkip.hide();\n\t\t\t\t\t_continue.show();\n\t\t\t\t} else {\n\t\t\t\t\th += _showReport.height() + padding + _yourReportName.height() + padding;\n\t\t\t\t\t_pleaseSendReport.show();\n\t\t\t\t\t_yourReportName.show();\n\t\t\t\t\tif (_reportUsername.isEmpty()) {\n\t\t\t\t\t\t_includeUsername.hide();\n\t\t\t\t\t} else {\n\t\t\t\t\t\th += _includeUsername.height() + padding;\n\t\t\t\t\t\t_includeUsername.show();\n\t\t\t\t\t}\n\t\t\t\t\tif (_sendingState == SendingTooOld || _sendingState == SendingUnofficial) {\n\t\t\t\t\t\tQString verStr = getReportField(qstr(\"version\"), qstr(\"Version:\"));\n\t\t\t\t\t\tqint64 ver = verStr.isEmpty() ? 0 : verStr.toLongLong();\n\t\t\t\t\t\tif (!ver || (ver == AppVersion) || (ver < 0 && (-ver / 1000) == AppVersion)) {\n\t\t\t\t\t\t\th += _getApp.height() + padding;\n\t\t\t\t\t\t\t_getApp.show();\n\t\t\t\t\t\t\th -= _yourReportName.height() + padding; // hide report name\n\t\t\t\t\t\t\t_yourReportName.hide();\n\t\t\t\t\t\t\tif (!_reportUsername.isEmpty()) {\n\t\t\t\t\t\t\t\th -= _includeUsername.height() + padding;\n\t\t\t\t\t\t\t\t_includeUsername.hide();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t_getApp.hide();\n\t\t\t\t\t\t}\n\t\t\t\t\t\t_showReport.hide();\n\t\t\t\t\t\t_report.hide();\n\t\t\t\t\t\t_minidump.hide();\n\t\t\t\t\t\t_saveReport.hide();\n\t\t\t\t\t\t_send.hide();\n\t\t\t\t\t\t_sendSkip.hide();\n\t\t\t\t\t\t_continue.show();\n\t\t\t\t\t} else {\n\t\t\t\t\t\t_getApp.hide();\n\t\t\t\t\t\tif (_reportShown) {\n\t\t\t\t\t\t\th += (_pleaseSendReport.height() * 12.5) + padding + (_minidumpName.isEmpty() ? 0 : (_minidump.height() + padding));\n\t\t\t\t\t\t\t_report.show();\n\t\t\t\t\t\t\tif (_minidumpName.isEmpty()) {\n\t\t\t\t\t\t\t\t_minidump.hide();\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t_minidump.show();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (_reportSaved || _sendingState == SendingFail || _sendingState == SendingProgress || _sendingState == SendingUploading) {\n\t\t\t\t\t\t\t\t_saveReport.hide();\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t_saveReport.show();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t_showReport.hide();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t_report.hide();\n\t\t\t\t\t\t\t_minidump.hide();\n\t\t\t\t\t\t\t_saveReport.hide();\n\t\t\t\t\t\t\tif (_sendingState == SendingFail || _sendingState == SendingProgress || _sendingState == SendingUploading) {\n\t\t\t\t\t\t\t\t_showReport.hide();\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t_showReport.show();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (_sendingState == SendingTooMany || _sendingState == SendingDone) {\n\t\t\t\t\t\t\t_send.hide();\n\t\t\t\t\t\t\t_sendSkip.hide();\n\t\t\t\t\t\t\t_continue.show();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tif (_sendingState == SendingProgress || _sendingState == SendingUploading) {\n\t\t\t\t\t\t\t\t_send.hide();\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t_send.show();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t_sendSkip.show();\n\t\t\t\t\t\t\t_continue.hide();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t_getApp.hide();\n\t\t\t\t_pleaseSendReport.hide();\n\t\t\t\t_yourReportName.hide();\n\t\t\t\t_includeUsername.hide();\n\t\t\t\t_showReport.hide();\n\t\t\t\t_report.hide();\n\t\t\t\t_minidump.hide();\n\t\t\t\t_saveReport.hide();\n\t\t\t\t_send.hide();\n\t\t\t\t_sendSkip.hide();\n\t\t\t\t_continue.hide();\n\t\t\t}\n\t\t\t_updaterData->check.hide();\n\t\t\tif (_updaterData->state == UpdatingCheck\n\t\t\t\t|| _updaterData->state == UpdatingDownload) {\n\t\t\t\th += padding + _updaterData->skip.height() + padding;\n\t\t\t\t_updaterData->skip.show();\n\t\t\t} else {\n\t\t\t\t_updaterData->skip.hide();\n\t\t\t}\n\t\t}\n\t} else {\n\t\th += _networkSettings.height() + padding;\n\t\th += padding + _send.height() + padding;\n\t\tif (_sendingState == SendingNoReport) {\n\t\t\t_pleaseSendReport.hide();\n\t\t\t_yourReportName.hide();\n\t\t\t_includeUsername.hide();\n\t\t\t_showReport.hide();\n\t\t\t_report.hide();\n\t\t\t_minidump.hide();\n\t\t\t_saveReport.hide();\n\t\t\t_send.hide();\n\t\t\t_sendSkip.hide();\n\t\t\t_continue.show();\n\t\t\t_networkSettings.hide();\n\t\t} else {\n\t\t\th += _showReport.height() + padding + _yourReportName.height() + padding;\n\t\t\t_pleaseSendReport.show();\n\t\t\t_yourReportName.show();\n\t\t\tif (_reportUsername.isEmpty()) {\n\t\t\t\t_includeUsername.hide();\n\t\t\t} else {\n\t\t\t\th += _includeUsername.height() + padding;\n\t\t\t\t_includeUsername.show();\n\t\t\t}\n\t\t\tif (_reportShown) {\n\t\t\t\th += (_pleaseSendReport.height() * 12.5) + padding + (_minidumpName.isEmpty() ? 0 : (_minidump.height() + padding));\n\t\t\t\t_report.show();\n\t\t\t\tif (_minidumpName.isEmpty()) {\n\t\t\t\t\t_minidump.hide();\n\t\t\t\t} else {\n\t\t\t\t\t_minidump.show();\n\t\t\t\t}\n\t\t\t\t_showReport.hide();\n\t\t\t\tif (_reportSaved || _sendingState == SendingFail || _sendingState == SendingProgress || _sendingState == SendingUploading) {\n\t\t\t\t\t_saveReport.hide();\n\t\t\t\t} else {\n\t\t\t\t\t_saveReport.show();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t_report.hide();\n\t\t\t\t_minidump.hide();\n\t\t\t\t_saveReport.hide();\n\t\t\t\tif (_sendingState == SendingFail || _sendingState == SendingProgress || _sendingState == SendingUploading) {\n\t\t\t\t\t_showReport.hide();\n\t\t\t\t} else {\n\t\t\t\t\t_showReport.show();\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_sendingState == SendingDone) {\n\t\t\t\t_send.hide();\n\t\t\t\t_sendSkip.hide();\n\t\t\t\t_continue.show();\n\t\t\t\t_networkSettings.hide();\n\t\t\t} else {\n\t\t\t\tif (_sendingState == SendingProgress || _sendingState == SendingUploading) {\n\t\t\t\t\t_send.hide();\n\t\t\t\t} else {\n\t\t\t\t\t_send.show();\n\t\t\t\t}\n\t\t\t\t_sendSkip.show();\n\t\t\t\tif (_sendingState == SendingFail) {\n\t\t\t\t\t_networkSettings.show();\n\t\t\t\t} else {\n\t\t\t\t\t_networkSettings.hide();\n\t\t\t\t}\n\t\t\t\t_continue.hide();\n\t\t\t}\n\t\t}\n\n\t\t_getApp.show();\n\t\th += _networkSettings.height() + padding;\n\t}\n\n\tQSize s(2 * padding + QFontMetrics(_label.font()).horizontalAdvance(u\"Last time Telegram Desktop was not closed properly.\"_q) + padding + _networkSettings.width(), h);\n\tif (s == size()) {\n\t\tresizeEvent(0);\n\t} else {\n\t\tresize(s);\n\t}\n}\n\nvoid LastCrashedWindow::networkSettings() {\n\tconst auto &proxy = Core::Sandbox::Instance().sandboxProxy();\n\tconst auto box = new NetworkSettingsWindow(\n\t\tthis,\n\t\tproxy.host,\n\t\tproxy.port ? proxy.port : kDefaultProxyPort,\n\t\tproxy.user,\n\t\tproxy.password);\n\tbox->saveRequests(\n\t) | rpl::on_next([=](MTP::ProxyData &&data) {\n\t\tAssert(data.host.isEmpty() || data.port != 0);\n\t\t_proxyChanges.fire(std::move(data));\n\t\tproxyUpdated();\n\t}, _lifetime);\n\tbox->show();\n}\n\nvoid LastCrashedWindow::proxyUpdated() {\n\tif (_updaterData\n\t\t&& ((_updaterData->state == UpdatingCheck)\n\t\t\t|| (_updaterData->state == UpdatingFail\n\t\t\t\t&& (_sendingState == SendingNoReport\n\t\t\t\t\t|| _sendingState == SendingUpdateCheck)))) {\n\t\tCore::UpdateChecker checker;\n\t\tchecker.stop();\n\t\tcSetLastUpdateCheck(0);\n\t\tchecker.start();\n\t} else if (_sendingState == SendingFail\n\t\t|| _sendingState == SendingProgress) {\n\t\tsendReport();\n\t}\n\tactivate();\n}\n\nrpl::producer<MTP::ProxyData> LastCrashedWindow::proxyChanges() const {\n\treturn _proxyChanges.events();\n}\n\nvoid LastCrashedWindow::setUpdatingState(UpdatingState state, bool force) {\n\tExpects(_updaterData != nullptr);\n\n\tif (_updaterData->state != state || force) {\n\t\t_updaterData->state = state;\n\t\tswitch (state) {\n\t\tcase UpdatingLatest:\n\t\t\t_updating.setText(u\"Latest version is installed.\"_q);\n\t\t\tif (_sendingState == SendingNoReport) {\n\t\t\t\tInvokeQueued(this, [=] { processContinue(); });\n\t\t\t} else {\n\t\t\t\t_sendingState = SendingNone;\n\t\t\t}\n\t\tbreak;\n\t\tcase UpdatingReady:\n\t\t\tif (Core::checkReadyUpdate()) {\n\t\t\t\tcSetRestartingUpdate(true);\n\t\t\t\tCore::Quit();\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tsetUpdatingState(UpdatingFail);\n\t\t\t\treturn;\n\t\t\t}\n\t\tbreak;\n\t\tcase UpdatingCheck:\n\t\t\t_updating.setText(u\"Checking for updates...\"_q);\n\t\tbreak;\n\t\tcase UpdatingFail:\n\t\t\t_updating.setText(u\"Update check failed :(\"_q);\n\t\tbreak;\n\t\t}\n\t\tupdateControls();\n\t}\n}\n\nvoid LastCrashedWindow::setDownloadProgress(qint64 ready, qint64 total) {\n\tExpects(_updaterData != nullptr);\n\n\tqint64 readyTenthMb = (ready * 10 / (1024 * 1024)), totalTenthMb = (total * 10 / (1024 * 1024));\n\tQString readyStr = QString::number(readyTenthMb / 10) + '.' + QString::number(readyTenthMb % 10);\n\tQString totalStr = QString::number(totalTenthMb / 10) + '.' + QString::number(totalTenthMb % 10);\n\tQString res = u\"Downloading update {ready} / {total} MB..\"_q.replace(qstr(\"{ready}\"), readyStr).replace(qstr(\"{total}\"), totalStr);\n\tif (_updaterData->newVersionDownload != res) {\n\t\t_updaterData->newVersionDownload = res;\n\t\t_updating.setText(_updaterData->newVersionDownload);\n\t\tupdateControls();\n\t}\n}\n\nvoid LastCrashedWindow::updateRetry() {\n\tExpects(_updaterData != nullptr);\n\n\tcSetLastUpdateCheck(0);\n\tCore::UpdateChecker checker;\n\tchecker.start();\n}\n\nvoid LastCrashedWindow::updateSkip() {\n\tExpects(_updaterData != nullptr);\n\n\tif (_sendingState == SendingNoReport) {\n\t\tprocessContinue();\n\t} else {\n\t\tif (_updaterData->state == UpdatingCheck\n\t\t\t|| _updaterData->state == UpdatingDownload) {\n\t\t\tCore::UpdateChecker checker;\n\t\t\tchecker.stop();\n\t\t\tsetUpdatingState(UpdatingFail);\n\t\t}\n\t\t_sendingState = SendingNone;\n\t\tupdateControls();\n\t}\n}\n\nvoid LastCrashedWindow::processContinue() {\n\tclose();\n}\n\nvoid LastCrashedWindow::sendingError(QNetworkReply::NetworkError e) {\n\tLOG((\"Crash report sending error: %1\").arg(e));\n\n\t_pleaseSendReport.setText(u\"Sending crash report failed :(\"_q);\n\t_sendingState = SendingFail;\n\tif (_checkReply) {\n\t\t_checkReply->deleteLater();\n\t\t_checkReply = nullptr;\n\t}\n\tif (_sendReply) {\n\t\t_sendReply->deleteLater();\n\t\t_sendReply = nullptr;\n\t}\n\tupdateControls();\n}\n\nvoid LastCrashedWindow::sendingFinished() {\n\tif (_sendReply) {\n\t\tQByteArray result = _sendReply->readAll();\n\t\tLOG((\"Crash report sending done, result: %1\").arg(QString::fromUtf8(result)));\n\n\t\t_sendReply->deleteLater();\n\t\t_sendReply = nullptr;\n\t\t_pleaseSendReport.setText(u\"Thank you for your report!\"_q);\n\t\t_sendingState = SendingDone;\n\t\tupdateControls();\n\n\t\tCrashReports::Restart();\n\t}\n}\n\nvoid LastCrashedWindow::sendingProgress(qint64 uploaded, qint64 total) {\n\tif (_sendingState != SendingProgress && _sendingState != SendingUploading) return;\n\t_sendingState = SendingUploading;\n\n\tif (total < 0) {\n\t\t_pleaseSendReport.setText(u\"Sending crash report %1 KB...\"_q.arg(uploaded / 1024));\n\t} else {\n\t\t_pleaseSendReport.setText(u\"Sending crash report %1 / %2 KB...\"_q.arg(uploaded / 1024).arg(total / 1024));\n\t}\n\tupdateControls();\n}\n\nvoid LastCrashedWindow::closeEvent(QCloseEvent *e) {\n\tdeleteLater();\n\n\tif (CrashReports::Restart() == CrashReports::CantOpen) {\n\t\tnew NotStartedWindow();\n\t} else {\n\t\t_launch();\n\t}\n}\n\nvoid LastCrashedWindow::resizeEvent(QResizeEvent *e) {\n\tint padding = _size;\n\t_label.move(padding, padding + (_networkSettings.height() - _label.height()) / 2);\n\n\t_send.move(width() - padding - _send.width(), height() - padding - _send.height());\n\tif (_sendingState == SendingProgress || _sendingState == SendingUploading) {\n\t\t_sendSkip.move(width() - padding - _sendSkip.width(), height() - padding - _sendSkip.height());\n\t} else {\n\t\t_sendSkip.move(width() - padding - _send.width() - padding - _sendSkip.width(), height() - padding - _sendSkip.height());\n\t}\n\n\t_updating.move(padding, padding * 2 + _networkSettings.height() + (_networkSettings.height() - _updating.height()) / 2);\n\n\tif (_updaterData) {\n\t\t_pleaseSendReport.move(padding, padding * 2 + _networkSettings.height() + _networkSettings.height() + padding + (_showReport.height() - _pleaseSendReport.height()) / 2);\n\t\t_showReport.move(padding * 2 + _pleaseSendReport.width(), padding * 2 + _networkSettings.height() + _networkSettings.height() + padding);\n\t\t_yourReportName.move(padding, _showReport.y() + _showReport.height() + padding);\n\t\t_includeUsername.move(padding, _yourReportName.y() + _yourReportName.height() + padding);\n\t\t_getApp.move((width() - _getApp.width()) / 2, _showReport.y() + _showReport.height() + padding);\n\n\t\tif (_sendingState == SendingFail || _sendingState == SendingProgress) {\n\t\t\t_networkSettings.move(padding * 2 + _pleaseSendReport.width(), padding * 2 + _networkSettings.height() + _networkSettings.height() + padding);\n\t\t} else {\n\t\t\t_networkSettings.move(padding * 2 + _updating.width(), padding * 2 + _networkSettings.height());\n\t\t}\n\n\t\tif (_updaterData->state == UpdatingCheck\n\t\t\t|| _updaterData->state == UpdatingDownload) {\n\t\t\t_updaterData->check.move(width() - padding - _updaterData->check.width(), height() - padding - _updaterData->check.height());\n\t\t\t_updaterData->skip.move(width() - padding - _updaterData->skip.width(), height() - padding - _updaterData->skip.height());\n\t\t} else {\n\t\t\t_updaterData->check.move(width() - padding - _updaterData->check.width(), height() - padding - _updaterData->check.height());\n\t\t\t_updaterData->skip.move(width() - padding - _updaterData->check.width() - padding - _updaterData->skip.width(), height() - padding - _updaterData->skip.height());\n\t\t}\n\t} else {\n\t\t_getApp.move((width() - _getApp.width()) / 2, _updating.y() + _updating.height() + padding);\n\n\t\t_pleaseSendReport.move(padding, padding * 2 + _networkSettings.height() + _networkSettings.height() + padding + _getApp.height() + padding + (_showReport.height() - _pleaseSendReport.height()) / 2);\n\t\t_showReport.move(padding * 2 + _pleaseSendReport.width(), padding * 2 + _networkSettings.height() + _networkSettings.height() + padding + _getApp.height() + padding);\n\t\t_yourReportName.move(padding, _showReport.y() + _showReport.height() + padding);\n\t\t_includeUsername.move(padding, _yourReportName.y() + _yourReportName.height() + padding);\n\n\t\t_networkSettings.move(padding * 2 + _pleaseSendReport.width(), padding * 2 + _networkSettings.height() + _networkSettings.height() + padding + _getApp.height() + padding);\n\t}\n\tif (_reportUsername.isEmpty()) {\n\t\t_report.setGeometry(padding, _yourReportName.y() + _yourReportName.height() + padding, width() - 2 * padding, _pleaseSendReport.height() * 12.5);\n\t} else {\n\t\t_report.setGeometry(padding, _includeUsername.y() + _includeUsername.height() + padding, width() - 2 * padding, _pleaseSendReport.height() * 12.5);\n\t}\n\t_minidump.move(padding, _report.y() + _report.height() + padding);\n\t_saveReport.move(_showReport.x(), _showReport.y());\n\n\t_continue.move(width() - padding - _continue.width(), height() - padding - _continue.height());\n}\n\nNetworkSettingsWindow::NetworkSettingsWindow(QWidget *parent, QString host, quint32 port, QString username, QString password)\n: PreLaunchWindow(u\"HTTP Proxy Settings\"_q)\n, _hostLabel(this)\n, _portLabel(this)\n, _usernameLabel(this)\n, _passwordLabel(this)\n, _hostInput(this)\n, _portInput(this)\n, _usernameInput(this)\n, _passwordInput(this, true)\n, _save(this)\n, _cancel(this, false)\n, _parent(parent) {\n\tsetWindowModality(Qt::ApplicationModal);\n\n\t_hostLabel.setText(u\"Hostname\"_q);\n\t_portLabel.setText(u\"Port\"_q);\n\t_usernameLabel.setText(u\"Username\"_q);\n\t_passwordLabel.setText(u\"Password\"_q);\n\n\t_save.setText(u\"SAVE\"_q);\n\tconnect(&_save, &QPushButton::clicked, [=] { save(); });\n\t_cancel.setText(u\"CANCEL\"_q);\n\tconnect(&_cancel, &QPushButton::clicked, [=] { close(); });\n\n\t_hostInput.setText(host);\n\t_portInput.setText(QString::number(port));\n\t_usernameInput.setText(username);\n\t_passwordInput.setText(password);\n\n\tQRect scr(QApplication::primaryScreen()->availableGeometry());\n\tmove(scr.x() + (scr.width() / 6), scr.y() + (scr.height() / 6));\n\tupdateControls();\n\tshow();\n\n\t_hostInput.setFocus();\n\t_hostInput.setCursorPosition(_hostInput.text().size());\n}\n\nvoid NetworkSettingsWindow::resizeEvent(QResizeEvent *e) {\n\tint padding = _size;\n\t_hostLabel.move(padding, padding);\n\t_hostInput.setGeometry(_hostLabel.x(), _hostLabel.y() + _hostLabel.height(), 2 * _hostLabel.width(), _hostInput.height());\n\t_portLabel.move(padding + _hostInput.width() + padding, padding);\n\t_portInput.setGeometry(_portLabel.x(), _portLabel.y() + _portLabel.height(), width() - padding - _portLabel.x(), _portInput.height());\n\t_usernameLabel.move(padding, _hostInput.y() + _hostInput.height() + padding);\n\t_usernameInput.setGeometry(_usernameLabel.x(), _usernameLabel.y() + _usernameLabel.height(), (width() - 3 * padding) / 2, _usernameInput.height());\n\t_passwordLabel.move(padding + _usernameInput.width() + padding, _usernameLabel.y());\n\t_passwordInput.setGeometry(_passwordLabel.x(), _passwordLabel.y() + _passwordLabel.height(), width() - padding - _passwordLabel.x(), _passwordInput.height());\n\n\t_save.move(width() - padding - _save.width(), height() - padding - _save.height());\n\t_cancel.move(_save.x() - padding - _cancel.width(), _save.y());\n}\n\nvoid NetworkSettingsWindow::save() {\n\tQString host = _hostInput.text().trimmed(), port = _portInput.text().trimmed(), username = _usernameInput.text().trimmed(), password = _passwordInput.text().trimmed();\n\tif (!port.isEmpty() && !port.toUInt()) {\n\t\t_portInput.setFocus();\n\t\treturn;\n\t} else if (!host.isEmpty() && port.isEmpty()) {\n\t\t_portInput.setFocus();\n\t\treturn;\n\t}\n\t_saveRequests.fire({\n\t\t.type = host.isEmpty()\n\t\t\t? MTP::ProxyData::Type::None\n\t\t\t: MTP::ProxyData::Type::Http,\n\t\t.host = host,\n\t\t.port = port.toUInt(),\n\t\t.user = username,\n\t\t.password = password,\n\t});\n\tclose();\n}\n\nvoid NetworkSettingsWindow::closeEvent(QCloseEvent *e) {\n\tdeleteLater();\n}\n\nrpl::producer<MTP::ProxyData> NetworkSettingsWindow::saveRequests() const {\n\treturn _saveRequests.events();\n}\n\nvoid NetworkSettingsWindow::updateControls() {\n\t_hostInput.updateGeometry();\n\t_hostInput.resize(_hostInput.sizeHint());\n\t_portInput.updateGeometry();\n\t_portInput.resize(_portInput.sizeHint());\n\t_usernameInput.updateGeometry();\n\t_usernameInput.resize(_usernameInput.sizeHint());\n\t_passwordInput.updateGeometry();\n\t_passwordInput.resize(_passwordInput.sizeHint());\n\n\tint padding = _size;\n\tint w = 2 * padding + _hostLabel.width() * 2 + padding + _portLabel.width() * 2 + padding;\n\tint h = padding + _hostLabel.height() + _hostInput.height() + padding + _usernameLabel.height() + _usernameInput.height() + padding + _save.height() + padding;\n\tif (w == width() && h == height()) {\n\t\tresizeEvent(0);\n\t} else {\n\t\tsetGeometry(_parent->x() + (_parent->width() - w) / 2, _parent->y() + (_parent->height() - h) / 2, w, h);\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/core/crash_report_window.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtWidgets/QLabel>\n#include <QtWidgets/QLineEdit>\n#include <QtWidgets/QTextEdit>\n#include <QtWidgets/QPushButton>\n#include <QtWidgets/QCheckBox>\n#include <QtNetwork/QNetworkReply>\n#include <QtNetwork/QHttpMultiPart>\n#include <QtNetwork/QNetworkAccessManager>\n\nnamespace MTP {\nstruct ProxyData;\n} // namespace MTP\n\nclass PreLaunchWindow : public QWidget {\npublic:\n\tPreLaunchWindow(QString title = QString());\n\tvoid activate();\n\tint basicSize() const {\n\t\treturn _size;\n\t}\n\t~PreLaunchWindow();\n\n\tstatic PreLaunchWindow *instance();\n\nprotected:\n\n\tint _size;\n\n};\n\nclass PreLaunchLabel : public QLabel {\npublic:\n\tPreLaunchLabel(QWidget *parent);\n\tvoid setText(const QString &text);\n\n};\n\nclass PreLaunchInput : public QLineEdit {\npublic:\n\tPreLaunchInput(QWidget *parent, bool password = false);\n\n};\n\nclass PreLaunchLog : public QTextEdit {\npublic:\n\tPreLaunchLog(QWidget *parent);\n\n};\n\nclass PreLaunchButton : public QPushButton {\npublic:\n\tPreLaunchButton(QWidget *parent, bool confirm = true);\n\tvoid setText(const QString &text);\n\n};\n\nclass PreLaunchCheckbox : public QCheckBox {\npublic:\n\tPreLaunchCheckbox(QWidget *parent);\n\tvoid setText(const QString &text);\n\n};\n\nclass NotStartedWindow : public PreLaunchWindow {\npublic:\n\tNotStartedWindow();\n\nprotected:\n\tvoid closeEvent(QCloseEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid updateControls();\n\n\tPreLaunchLabel _label;\n\tPreLaunchLog _log;\n\tPreLaunchButton _close;\n\n};\n\nclass LastCrashedWindow : public PreLaunchWindow {\n\npublic:\n\tLastCrashedWindow(const QByteArray &crashdump, Fn<void()> launch);\n\n\trpl::producer<MTP::ProxyData> proxyChanges() const;\n\n\trpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\n\tvoid saveReport();\n\tvoid sendReport();\n\n\tvoid networkSettings();\n\tvoid processContinue();\n\n\tvoid checkingFinished();\n\tvoid sendingError(QNetworkReply::NetworkError e);\n\tvoid sendingFinished();\n\tvoid sendingProgress(qint64 uploaded, qint64 total);\n\n\tvoid updateRetry();\n\tvoid updateSkip();\n\nprotected:\n\tvoid closeEvent(QCloseEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid proxyUpdated();\n\tQString minidumpFileName();\n\tvoid updateControls();\n\n\tvoid excludeReportUsername();\n\n\tQString getReportField(const QLatin1String &name, const QLatin1String &prefix);\n\tvoid addReportFieldPart(const QLatin1String &name, const QLatin1String &prefix, QHttpMultiPart *multipart);\n\n\tQByteArray _dumpraw;\n\n\tPreLaunchLabel _label, _pleaseSendReport, _yourReportName, _minidump;\n\tPreLaunchLog _report;\n\tPreLaunchButton _send, _sendSkip, _networkSettings, _continue, _showReport, _saveReport, _getApp;\n\tPreLaunchCheckbox _includeUsername;\n\n\tQString _minidumpName, _minidumpFull, _reportText;\n\tQString _reportUsername, _reportTextNoUsername;\n\tQByteArray getCrashReportRaw() const;\n\n\tbool _reportShown, _reportSaved;\n\n\tenum SendingState {\n\t\tSendingNoReport,\n\t\tSendingUpdateCheck,\n\t\tSendingNone,\n\t\tSendingTooOld,\n\t\tSendingTooMany,\n\t\tSendingUnofficial,\n\t\tSendingProgress,\n\t\tSendingUploading,\n\t\tSendingFail,\n\t\tSendingDone,\n\t};\n\tSendingState _sendingState;\n\n\tPreLaunchLabel _updating;\n\n\tQNetworkAccessManager _sendManager;\n\tQNetworkReply *_checkReply = nullptr;\n\tQNetworkReply *_sendReply = nullptr;\n\n\tenum UpdatingState {\n\t\tUpdatingNone,\n\t\tUpdatingCheck,\n\t\tUpdatingLatest,\n\t\tUpdatingDownload,\n\t\tUpdatingFail,\n\t\tUpdatingReady\n\t};\n\tstruct UpdaterData {\n\t\tUpdaterData(QWidget *buttonParent);\n\n\t\tPreLaunchButton check, skip;\n\t\tUpdatingState state;\n\t\tQString newVersionDownload;\n\t};\n\tconst std::unique_ptr<UpdaterData> _updaterData;\n\n\tvoid setUpdatingState(UpdatingState state, bool force = false);\n\tvoid setDownloadProgress(qint64 ready, qint64 total);\n\n\tFn<void()> _launch;\n\trpl::event_stream<MTP::ProxyData> _proxyChanges;\n\trpl::lifetime _lifetime;\n\n};\n\nclass NetworkSettingsWindow : public PreLaunchWindow {\n\npublic:\n\tNetworkSettingsWindow(QWidget *parent, QString host, quint32 port, QString username, QString password);\n\n\t[[nodiscard]] rpl::producer<MTP::ProxyData> saveRequests() const;\n\tvoid save();\n\nprotected:\n\tvoid closeEvent(QCloseEvent *e);\n\tvoid resizeEvent(QResizeEvent *e);\n\nprivate:\n\tvoid updateControls();\n\n\tPreLaunchLabel _hostLabel, _portLabel, _usernameLabel, _passwordLabel;\n\tPreLaunchInput _hostInput, _portInput, _usernameInput, _passwordInput;\n\tPreLaunchButton _save, _cancel;\n\n\tQWidget *_parent;\n\n\trpl::event_stream<MTP::ProxyData> _saveRequests;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/core/crash_reports.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/crash_reports.h\"\n\n#include \"platform/platform_specific.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"core/launcher.h\"\n\n#include <signal.h>\n#include <new>\n#include <mutex>\n\n#ifndef TDESKTOP_DISABLE_CRASH_REPORTS\n#ifdef Q_OS_WIN\n\n#include <new.h>\n\n#pragma warning(push)\n#pragma warning(disable:4091)\n#include <client/windows/handler/exception_handler.h>\n#pragma warning(pop)\n\n#else // Q_OS_WIN\n\n#include <execinfo.h>\n#include <sys/syscall.h>\n\n#ifdef Q_OS_MAC\n\n#include <dlfcn.h>\n#include <unistd.h>\n\n#ifdef MAC_USE_BREAKPAD\n#include <client/mac/handler/exception_handler.h>\n#else // MAC_USE_BREAKPAD\n#include <client/crashpad_client.h>\n#endif // else for MAC_USE_BREAKPAD\n\n#else // Q_OS_MAC\n\n#include <client/linux/handler/exception_handler.h>\n\n#endif // Q_OS_MAC\n\n#endif // Q_OS_WIN\n#endif // !TDESKTOP_DISABLE_CRASH_REPORTS\n\nnamespace CrashReports {\nnamespace {\n\nusing Annotations = std::map<std::string, std::string>;\nusing AnnotationRefs = std::map<std::string, const QString*>;\n\nAnnotations ProcessAnnotations;\nAnnotationRefs ProcessAnnotationRefs;\n\n#ifndef TDESKTOP_DISABLE_CRASH_REPORTS\n\nQString ReportPath;\nFILE *ReportFile = nullptr;\nint ReportFileNo = 0;\n\nvoid SafeWriteChar(char ch) {\n\tfwrite(&ch, 1, 1, ReportFile);\n}\n\ntemplate <bool Unsigned, typename Type>\nstruct writeNumberSignAndRemoveIt {\n\tstatic void call(Type &number) {\n\t\tif (number < 0) {\n\t\t\tSafeWriteChar('-');\n\t\t\tnumber = -number;\n\t\t}\n\t}\n};\ntemplate <typename Type>\nstruct writeNumberSignAndRemoveIt<true, Type> {\n\tstatic void call(Type &number) {\n\t}\n};\n\ntemplate <typename Type>\nconst dump &SafeWriteNumber(const dump &stream, Type number) {\n\tif (!ReportFile) return stream;\n\n\twriteNumberSignAndRemoveIt<(Type(-1) > Type(0)), Type>::call(number);\n\tType upper = 1, prev = number / 10;\n\twhile (prev >= upper) {\n\t\tupper *= 10;\n\t}\n\twhile (upper > 0) {\n\t\tint digit = (number / upper);\n\t\tSafeWriteChar('0' + digit);\n\t\tnumber -= digit * upper;\n\t\tupper /= 10;\n\t}\n\treturn stream;\n}\n\nusing ReservedMemoryChunk = std::array<gsl::byte, 1024 * 1024>;\nstd::unique_ptr<ReservedMemoryChunk> ReservedMemory;\n\nvoid InstallOperatorNewHandler() {\n\tReservedMemory = std::make_unique<ReservedMemoryChunk>();\n#ifdef Q_OS_WIN\n\t_set_new_handler([](size_t requested) -> int {\n\t\t_set_new_handler(nullptr);\n\t\tReservedMemory.reset();\n\t\tCrashReports::SetAnnotation(\"Requested\", QString::number(requested));\n\t\tUnexpected(\"Could not allocate!\");\n\t});\n#else // Q_OS_WIN\n\tstd::set_new_handler([] {\n\t\tstd::set_new_handler(nullptr);\n\t\tReservedMemory.reset();\n\t\tUnexpected(\"Could not allocate!\");\n\t});\n#endif // Q_OS_WIN\n}\n\nvoid InstallQtMessageHandler() {\n\tstatic QtMessageHandler original = nullptr;\n\toriginal = qInstallMessageHandler([](\n\t\t\tQtMsgType type,\n\t\t\tconst QMessageLogContext &context,\n\t\t\tconst QString &message) {\n\t\tif (original) {\n\t\t\toriginal(type, context, message);\n\t\t}\n\t\tif (type == QtFatalMsg) {\n\t\t\tCrashReports::SetAnnotation(\"QtFatal\", message);\n\t\t\tUnexpected(\"Qt FATAL message was generated!\");\n\t\t}\n\t});\n}\n\nstd::atomic<Qt::HANDLE> ReportingThreadId/* = nullptr*/;\nbool ReportingHeaderWritten/* = false*/;\nconst char *BreakpadDumpPath/* = nullptr*/;\nconst wchar_t *BreakpadDumpPathW/* = nullptr*/;\n\nvoid WriteReportHeader() {\n\tif (ReportingHeaderWritten) {\n\t\treturn;\n\t}\n\tReportingHeaderWritten = true;\n\tconst auto dec2hex = [](int value) -> char {\n\t\tif (value >= 0 && value < 10) {\n\t\t\treturn '0' + value;\n\t\t} else if (value >= 10 && value < 16) {\n\t\t\treturn 'a' + (value - 10);\n\t\t}\n\t\treturn '#';\n\t};\n\tfor (const auto &i : ProcessAnnotationRefs) {\n\t\tQByteArray utf8 = i.second->toUtf8();\n\t\tstd::string wrapped;\n\t\twrapped.reserve(4 * utf8.size());\n\t\tfor (auto ch : utf8) {\n\t\t\tauto uch = static_cast<uchar>(ch);\n\t\t\twrapped.append(\"\\\\x\", 2).append(1, dec2hex(uch >> 4)).append(1, dec2hex(uch & 0x0F));\n\t\t}\n\t\tProcessAnnotations[i.first] = wrapped;\n\t}\n\tfor (const auto &i : ProcessAnnotations) {\n\t\tdump() << i.first.c_str() << \": \" << i.second.c_str() << \"\\n\";\n\t}\n\tPlatform::WriteCrashDumpDetails();\n\tdump() << \"\\n\";\n}\n\nvoid WriteReportInfo(int signum, const char *name) {\n\tWriteReportHeader();\n\n\tconst auto thread = ReportingThreadId.load();\n\tif (name) {\n\t\tdump() << \"Caught signal \" << signum << \" (\" << name << \") in thread \" << uint64(thread) << \"\\n\";\n\t} else if (signum == -1) {\n\t\tdump() << \"Google Breakpad caught a crash, minidump written in thread \" << uint64(thread) << \"\\n\";\n\t\tif (BreakpadDumpPath) {\n\t\t\tdump() << \"Minidump: \" << BreakpadDumpPath << \"\\n\";\n\t\t} else if (BreakpadDumpPathW) {\n\t\t\tdump() << \"Minidump: \" << BreakpadDumpPathW << \"\\n\";\n\t\t}\n\t} else {\n\t\tdump() << \"Caught signal \" << signum << \" in thread \" << uint64(thread) << \"\\n\";\n\t}\n}\n\nconst int HandledSignals[] = {\n\tSIGSEGV,\n\tSIGABRT,\n\tSIGFPE,\n\tSIGILL,\n#ifndef Q_OS_WIN\n\tSIGBUS,\n\tSIGTRAP,\n#endif // !Q_OS_WIN\n};\n\n#ifdef Q_OS_WIN\nvoid SignalHandler(int signum) {\n#else // Q_OS_WIN\nstruct sigaction OldSigActions[32]/* = { 0 }*/;\n\nvoid RestoreSignalHandlers() {\n\tfor (const auto signum : HandledSignals) {\n\t\tsigaction(signum, &OldSigActions[signum], nullptr);\n\t}\n}\n\nvoid InvokeOldSignalHandler(int signum, siginfo_t *info, void *ucontext) {\n\tif (signum < 0 || signum > 31) {\n\t\treturn;\n\t} else if (OldSigActions[signum].sa_flags & SA_SIGINFO) {\n\t\tif (OldSigActions[signum].sa_sigaction) {\n\t\t\tOldSigActions[signum].sa_sigaction(signum, info, ucontext);\n\t\t}\n\t} else {\n\t\tif (OldSigActions[signum].sa_handler) {\n\t\t\tOldSigActions[signum].sa_handler(signum);\n\t\t}\n\t}\n}\n\nvoid SignalHandler(int signum, siginfo_t *info, void *ucontext) {\n\tRestoreSignalHandlers();\n\n#endif // else for Q_OS_WIN\n\n\tconst char* name = 0;\n\tswitch (signum) {\n\tcase SIGABRT: name = \"SIGABRT\"; break;\n\tcase SIGSEGV: name = \"SIGSEGV\"; break;\n\tcase SIGILL: name = \"SIGILL\"; break;\n\tcase SIGFPE: name = \"SIGFPE\"; break;\n#ifndef Q_OS_WIN\n\tcase SIGBUS: name = \"SIGBUS\"; break;\n\tcase SIGSYS: name = \"SIGSYS\"; break;\n#endif // !Q_OS_WIN\n\t}\n\n\tauto expected = Qt::HANDLE(nullptr);\n\tconst auto thread = QThread::currentThreadId();\n\n\tif (ReportingThreadId.compare_exchange_strong(expected, thread)) {\n\t\tWriteReportInfo(signum, name);\n\t\tReportingThreadId = nullptr;\n\t}\n\n#ifndef Q_OS_WIN\n\tInvokeOldSignalHandler(signum, info, ucontext);\n#endif // !Q_OS_WIN\n}\n\nbool SetSignalHandlers = true;\nbool CrashLogged = false;\n#if !defined Q_OS_MAC || defined MAC_USE_BREAKPAD\ngoogle_breakpad::ExceptionHandler* BreakpadExceptionHandler = 0;\n\n#ifdef Q_OS_WIN\nbool DumpCallback(const wchar_t* _dump_dir, const wchar_t* _minidump_id, void* context, EXCEPTION_POINTERS* exinfo, MDRawAssertionInfo* assertion, bool success)\n#elif defined Q_OS_MAC // Q_OS_WIN\nbool DumpCallback(const char* _dump_dir, const char* _minidump_id, void *context, bool success)\n#else // Q_OS_MAC\nbool DumpCallback(const google_breakpad::MinidumpDescriptor &md, void *context, bool success)\n#endif // else for Q_OS_WIN || Q_OS_MAC\n{\n\tif (CrashLogged) return success;\n\tCrashLogged = true;\n\n#ifdef Q_OS_WIN\n\tBreakpadDumpPathW = _minidump_id;\n\tSignalHandler(-1);\n#else // Q_OS_WIN\n\n#ifdef Q_OS_MAC\n\tBreakpadDumpPath = _minidump_id;\n#else // Q_OS_MAC\n\tBreakpadDumpPath = md.path();\n#endif // else for Q_OS_MAC\n\tSignalHandler(-1, 0, 0);\n#endif // else for Q_OS_WIN\n\treturn success;\n}\n#endif // !Q_OS_MAC || MAC_USE_BREAKPAD\n\n#endif // !TDESKTOP_DISABLE_CRASH_REPORTS\n\n} // namespace\n\nQString PlatformString() {\n\tif (Platform::IsWindowsStoreBuild()) {\n\t\treturn Platform::IsWindowsARM64()\n\t\t\t? u\"WinStoreARM64\"_q\n\t\t\t: Platform::IsWindows64Bit()\n\t\t\t? u\"WinStore64Bit\"_q\n\t\t\t: u\"WinStore32Bit\"_q;\n\t} else if (Platform::IsWindows32Bit()) {\n\t\treturn u\"Windows32Bit\"_q;\n\t} else if (Platform::IsWindows64Bit()) {\n\t\treturn u\"Windows64Bit\"_q;\n\t} else if (Platform::IsWindowsARM64()) {\n\t\treturn u\"WindowsARM64\"_q;\n\t} else if (Platform::IsMacStoreBuild()) {\n\t\treturn u\"MacAppStore\"_q;\n\t} else if (Platform::IsMac()) {\n\t\treturn u\"MacOS\"_q;\n\t} else if (Platform::IsLinux()) {\n\t\treturn u\"Linux\"_q;\n\t}\n\tUnexpected(\"Platform in CrashReports::PlatformString.\");\n}\n\nvoid StartCatching() {\n#ifndef TDESKTOP_DISABLE_CRASH_REPORTS\n\tProcessAnnotations[\"Binary\"] = cExeName().toUtf8().constData();\n\tProcessAnnotations[\"ApiId\"] = QString::number(ApiId).toUtf8().constData();\n\tProcessAnnotations[\"Version\"] = (cAlphaVersion()\n\t\t? u\"%1 alpha\"_q.arg(cAlphaVersion())\n\t\t: (AppBetaVersion\n\t\t\t? u\"%1 beta\"_q\n\t\t\t: u\"%1\"_q).arg(AppVersion)).toUtf8().constData();\n\tProcessAnnotations[\"Launched\"] = QDateTime::currentDateTime().toString(\"dd.MM.yyyy hh:mm:ss\").toUtf8().constData();\n\tProcessAnnotations[\"Platform\"] = PlatformString().toUtf8().constData();\n\tProcessAnnotations[\"UserTag\"] = QString::number(Core::Launcher::Instance().installationTag(), 16).toUtf8().constData();\n\n\tQString dumpspath = cWorkingDir() + u\"tdata/dumps\"_q;\n\tQDir().mkpath(dumpspath);\n\n#ifdef Q_OS_WIN\n\tBreakpadExceptionHandler = new google_breakpad::ExceptionHandler(\n\t\tdumpspath.toStdWString(),\n\t\tgoogle_breakpad::ExceptionHandler::FilterCallback(nullptr),\n\t\tDumpCallback,\n\t\t(void*)nullptr, // callback_context\n\t\tgoogle_breakpad::ExceptionHandler::HANDLER_ALL,\n\t\tMINIDUMP_TYPE(MiniDumpNormal),\n\t\t// MINIDUMP_TYPE(MiniDumpWithFullMemory | MiniDumpWithHandleData | MiniDumpWithThreadInfo | MiniDumpWithProcessThreadData | MiniDumpWithFullMemoryInfo | MiniDumpWithUnloadedModules | MiniDumpWithFullAuxiliaryState | MiniDumpIgnoreInaccessibleMemory | MiniDumpWithTokenInformation),\n\t\t(const wchar_t*)nullptr, // pipe_name\n\t\t(const google_breakpad::CustomClientInfo*)nullptr\n\t);\n#elif defined Q_OS_MAC // Q_OS_WIN\n\n#ifdef MAC_USE_BREAKPAD\n#ifndef _DEBUG\n\tBreakpadExceptionHandler = new google_breakpad::ExceptionHandler(\n\t\tQFile::encodeName(dumpspath).toStdString(),\n\t\t/*FilterCallback*/ 0,\n\t\tDumpCallback,\n\t\t/*context*/ 0,\n\t\ttrue,\n\t\t0\n\t);\n#endif // !_DEBUG\n\tSetSignalHandlers = false;\n#else // MAC_USE_BREAKPAD\n\tcrashpad::CrashpadClient crashpad_client;\n\tstd::string handler = (cExeDir() + cExeName() + u\"/Contents/Helpers/crashpad_handler\"_q).toUtf8().constData();\n\tstd::string database = QFile::encodeName(dumpspath).constData();\n\tif (crashpad_client.StartHandler(\n\t\t\tbase::FilePath(handler),\n\t\t\tbase::FilePath(database),\n\t\t\t{}, // metrics_dir\n\t\t\tstd::string(), // url\n\t\t\tProcessAnnotations,\n\t\t\tstd::vector<std::string>(), // arguments\n\t\t\tfalse, // restartable\n\t\t\tfalse)) { // asynchronous_start\n\t}\n#endif // else for MAC_USE_BREAKPAD\n#else\n\tBreakpadExceptionHandler = new google_breakpad::ExceptionHandler(\n\t\tgoogle_breakpad::MinidumpDescriptor(QFile::encodeName(dumpspath).toStdString()),\n\t\t/*FilterCallback*/ 0,\n\t\tDumpCallback,\n\t\t/*context*/ 0,\n\t\ttrue,\n\t\t-1\n\t);\n#endif // else for Q_OS_WIN || Q_OS_MAC\n#endif // !TDESKTOP_DISABLE_CRASH_REPORTS\n}\n\nvoid FinishCatching() {\n#ifndef TDESKTOP_DISABLE_CRASH_REPORTS\n#if !defined Q_OS_MAC || defined MAC_USE_BREAKPAD\n\n\tdelete base::take(BreakpadExceptionHandler);\n\n#endif // !Q_OS_MAC || MAC_USE_BREAKPAD\n#endif // !TDESKTOP_DISABLE_CRASH_REPORTS\n}\n\nStartResult Start() {\n#ifndef TDESKTOP_DISABLE_CRASH_REPORTS\n\tReportPath = cWorkingDir() + u\"tdata/working\"_q;\n\n#ifdef Q_OS_WIN\n\tFILE *f = nullptr;\n\tif (_wfopen_s(&f, ReportPath.toStdWString().c_str(), L\"rb\") != 0) {\n\t\tf = nullptr;\n\t} else {\n#else // !Q_OS_WIN\n\tif (FILE *f = fopen(QFile::encodeName(ReportPath).constData(), \"rb\")) {\n#endif // else for !Q_OS_WIN\n\t\tQByteArray lastdump;\n\t\tchar buffer[256 * 1024] = { 0 };\n\t\tint32 read = fread(buffer, 1, 256 * 1024, f);\n\t\tif (read > 0) {\n\t\t\tlastdump.append(buffer, read);\n\t\t}\n\t\tfclose(f);\n\n\t\tLOG((\"Opened '%1' for reading, the previous \"\n\t\t\t\"Telegram Desktop launch was not finished properly :( \"\n\t\t\t\"Crash log size: %2\").arg(ReportPath).arg(lastdump.size()));\n\n\t\treturn lastdump;\n\t}\n\n#endif // !TDESKTOP_DISABLE_CRASH_REPORTS\n\treturn Restart();\n}\n\nStatus Restart() {\n#ifndef TDESKTOP_DISABLE_CRASH_REPORTS\n\tif (ReportFile) {\n\t\treturn Started;\n\t}\n\n#ifdef Q_OS_WIN\n\tif (_wfopen_s(&ReportFile, ReportPath.toStdWString().c_str(), L\"wb\") != 0) {\n\t\tReportFile = nullptr;\n\t}\n#else // Q_OS_WIN\n\tReportFile = fopen(QFile::encodeName(ReportPath).constData(), \"wb\");\n#endif // else for Q_OS_WIN\n\tif (ReportFile) {\n#ifdef Q_OS_WIN\n\t\tReportFileNo = _fileno(ReportFile);\n#else // Q_OS_WIN\n\t\tReportFileNo = fileno(ReportFile);\n#endif // else for Q_OS_WIN\n\t\tif (SetSignalHandlers) {\n#ifndef Q_OS_WIN\n\t\t\tstruct sigaction sigact;\n\n\t\t\tsigact.sa_sigaction = SignalHandler;\n\t\t\tsigemptyset(&sigact.sa_mask);\n\t\t\tsigact.sa_flags = SA_NODEFER | SA_RESETHAND | SA_SIGINFO;\n\n\t\t\tfor (const auto signum : HandledSignals) {\n\t\t\t\tsigaction(signum, &sigact, &OldSigActions[signum]);\n\t\t\t}\n#else // !Q_OS_WIN\n\t\t\tfor (const auto signum : HandledSignals) {\n\t\t\t\tsignal(signum, SignalHandler);\n\t\t\t}\n#endif // else for !Q_OS_WIN\n\t\t}\n\n\t\tInstallOperatorNewHandler();\n\t\tInstallQtMessageHandler();\n\n\t\treturn Started;\n\t}\n\n\tLOG((\"FATAL: Could not open '%1' for writing!\").arg(ReportPath));\n\n\treturn CantOpen;\n#else // !TDESKTOP_DISABLE_CRASH_REPORTS\n\treturn Started;\n#endif // else for !TDESKTOP_DISABLE_CRASH_REPORTS\n}\n\nvoid Finish() {\n#ifndef TDESKTOP_DISABLE_CRASH_REPORTS\n\tFinishCatching();\n\n\tif (ReportFile) {\n\t\tfclose(ReportFile);\n\t\tReportFile = nullptr;\n\n#ifdef Q_OS_WIN\n\t\t_wunlink(ReportPath.toStdWString().c_str());\n#else // Q_OS_WIN\n\t\tunlink(ReportPath.toUtf8().constData());\n#endif // else for Q_OS_WIN\n\t}\n#endif // !TDESKTOP_DISABLE_CRASH_REPORTS\n}\n\nvoid SetAnnotation(const std::string &key, const QString &value) {\n\tstatic QMutex mutex;\n\tQMutexLocker lock(&mutex);\n\n\tif (!value.trimmed().isEmpty()) {\n\t\tProcessAnnotations[key] = value.toUtf8().constData();\n\t} else {\n\t\tProcessAnnotations.erase(key);\n\t}\n}\n\nvoid SetAnnotationHex(const std::string &key, const QString &value) {\n\tif (value.isEmpty()) {\n\t\treturn SetAnnotation(key, value);\n\t}\n\tconst auto utf = value.toUtf8();\n\tauto buffer = std::string();\n\tbuffer.reserve(4 * utf.size());\n\tconst auto hexDigit = [](std::uint8_t value) {\n\t\tif (value >= 10) {\n\t\t\treturn 'A' + (value - 10);\n\t\t}\n\t\treturn '0' + value;\n\t};\n\tconst auto appendHex = [&](std::uint8_t value) {\n\t\tbuffer.push_back('\\\\');\n\t\tbuffer.push_back('x');\n\t\tbuffer.push_back(hexDigit(value / 16));\n\t\tbuffer.push_back(hexDigit(value % 16));\n\t};\n\tfor (const auto ch : utf) {\n\t\tappendHex(ch);\n\t}\n\tProcessAnnotations[key] = std::move(buffer);\n}\n\nvoid SetAnnotationRef(const std::string &key, const QString *valuePtr) {\n\tstatic QMutex mutex;\n\tQMutexLocker lock(&mutex);\n\n\tif (valuePtr) {\n\t\tProcessAnnotationRefs[key] = valuePtr;\n\t} else {\n\t\tProcessAnnotationRefs.erase(key);\n\t}\n}\n\n#ifndef TDESKTOP_DISABLE_CRASH_REPORTS\n\ndump::~dump() {\n\tif (ReportFile) {\n\t\tfflush(ReportFile);\n\t}\n}\n\nconst dump &operator<<(const dump &stream, const char *str) {\n\tif (!ReportFile) return stream;\n\n\tfwrite(str, 1, strlen(str), ReportFile);\n\treturn stream;\n}\n\nconst dump &operator<<(const dump &stream, const wchar_t *str) {\n\tif (!ReportFile) return stream;\n\n\tfor (int i = 0, l = wcslen(str); i < l; ++i) {\n\t\tif (\n#if !defined(__WCHAR_UNSIGNED__)\n\t\t\tstr[i] >= 0 &&\n#endif\n\t\t\tstr[i] < 128) {\n\t\t\tSafeWriteChar(char(str[i]));\n\t\t} else {\n\t\t\tSafeWriteChar('?');\n\t\t}\n\t}\n\treturn stream;\n}\n\nconst dump &operator<<(const dump &stream, int num) {\n\treturn SafeWriteNumber(stream, num);\n}\n\nconst dump &operator<<(const dump &stream, unsigned int num) {\n\treturn SafeWriteNumber(stream, num);\n}\n\nconst dump &operator<<(const dump &stream, unsigned long num) {\n\treturn SafeWriteNumber(stream, num);\n}\n\nconst dump &operator<<(const dump &stream, unsigned long long num) {\n\treturn SafeWriteNumber(stream, num);\n}\n\nconst dump &operator<<(const dump &stream, double num) {\n\tif (num < 0) {\n\t\tSafeWriteChar('-');\n\t\tnum = -num;\n\t}\n\tSafeWriteNumber(stream, uint64(floor(num)));\n\tSafeWriteChar('.');\n\tnum -= floor(num);\n\tfor (int i = 0; i < 4; ++i) {\n\t\tnum *= 10;\n\t\tint digit = int(floor(num));\n\t\tSafeWriteChar('0' + digit);\n\t\tnum -= digit;\n\t}\n\treturn stream;\n}\n\n#endif // TDESKTOP_DISABLE_CRASH_REPORTS\n\n} // namespace CrashReports\n"
  },
  {
    "path": "Telegram/SourceFiles/core/crash_reports.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace CrashReports {\n\nQString PlatformString();\n\n#ifndef TDESKTOP_DISABLE_CRASH_REPORTS\n\nstruct dump {\n\t~dump();\n};\nconst dump &operator<<(const dump &stream, const char *str);\nconst dump &operator<<(const dump &stream, const wchar_t *str);\nconst dump &operator<<(const dump &stream, int num);\nconst dump &operator<<(const dump &stream, unsigned int num);\nconst dump &operator<<(const dump &stream, unsigned long num);\nconst dump &operator<<(const dump &stream, unsigned long long num);\nconst dump &operator<<(const dump &stream, double num);\n\n#endif // TDESKTOP_DISABLE_CRASH_REPORTS\n\nenum Status {\n\tCantOpen,\n\tStarted\n};\n// Open status or crash report dump.\nusing StartResult = std::variant<Status, QByteArray>;\nStartResult Start();\nStatus Restart(); // can be only CantOpen or Started\nvoid Finish();\n\nvoid SetAnnotation(const std::string &key, const QString &value);\nvoid SetAnnotationHex(const std::string &key, const QString &value);\ninline void ClearAnnotation(const std::string &key) {\n\tSetAnnotation(key, QString());\n}\n\n// Remembers value pointer and tries to add the value to the crash report.\n// Attention! You should call clearCrashAnnotationRef(key) before destroying value.\nvoid SetAnnotationRef(const std::string &key, const QString *valuePtr);\ninline void ClearAnnotationRef(const std::string &key) {\n\tSetAnnotationRef(key, nullptr);\n}\n\nvoid StartCatching();\nvoid FinishCatching();\n\n} // namespace CrashReports\n"
  },
  {
    "path": "Telegram/SourceFiles/core/credits_amount.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/algorithm.h\"\n#include \"base/basic_types.h\"\n\nclass MTPstarsAmount;\n\nnamespace tl {\ntemplate <typename bare>\nclass boxed;\n} // namespace tl\n\nusing MTPStarsAmount = tl::boxed<MTPstarsAmount>;\n\ninline constexpr auto kOneStarInNano = int64(1'000'000'000);\n\nenum class CreditsType {\n\tStars,\n\tTon,\n};\n\nclass CreditsAmount {\npublic:\n\tCreditsAmount() = default;\n\texplicit CreditsAmount(\n\t\tint64 whole,\n\t\tCreditsType type = CreditsType::Stars)\n\t: _ton((type == CreditsType::Ton) ? 1 : 0)\n\t, _whole(whole) {\n\t}\n\tCreditsAmount(\n\t\tint64 whole,\n\t\tint64 nano,\n\t\tCreditsType type = CreditsType::Stars)\n\t: _ton((type == CreditsType::Ton) ? 1 : 0)\n\t, _whole(whole)\n\t, _nano(nano) {\n\t\tnormalize();\n\t}\n\n\t[[nodiscard]] int64 whole() const {\n\t\treturn _whole;\n\t}\n\n\t[[nodiscard]] int64 nano() const {\n\t\treturn _nano;\n\t}\n\n\t[[nodiscard]] double value() const {\n\t\treturn double(_whole) + double(_nano) / kOneStarInNano;\n\t}\n\n\t[[nodiscard]] bool ton() const {\n\t\treturn (_ton == 1);\n\t}\n\t[[nodiscard]] bool stars() const {\n\t\treturn (_ton == 0);\n\t}\n\t[[nodiscard]] CreditsType type() const {\n\t\treturn !_ton ? CreditsType::Stars : CreditsType::Ton;\n\t}\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn !_whole && !_nano;\n\t}\n\n\t[[nodiscard]] inline bool operator!() const {\n\t\treturn empty();\n\t}\n\t[[nodiscard]] inline explicit operator bool() const {\n\t\treturn !empty();\n\t}\n\n\t[[nodiscard]] CreditsAmount multiplied(float64 rate) const {\n\t\tconst auto result = value() * rate;\n\t\tconst auto abs = std::abs(result);\n\t\tconst auto whole = std::floor(abs);\n\t\tconst auto nano = base::SafeRound((abs - whole) * kOneStarInNano);\n\t\treturn CreditsAmount(\n\t\t\t(result < 0) ? -whole : whole,\n\t\t\t(result < 0) ? -nano : nano,\n\t\t\ttype());\n\t}\n\n\tinline CreditsAmount &operator+=(CreditsAmount other) {\n\t\t_whole += other._whole;\n\t\t_nano += other._nano;\n\t\tnormalize();\n\t\treturn *this;\n\t}\n\tinline CreditsAmount &operator-=(CreditsAmount other) {\n\t\t_whole -= other._whole;\n\t\t_nano -= other._nano;\n\t\tnormalize();\n\t\treturn *this;\n\t}\n\tinline CreditsAmount &operator*=(int64 multiplier) {\n\t\t_whole *= multiplier;\n\t\t_nano *= multiplier;\n\t\tnormalize();\n\t\treturn *this;\n\t}\n\tinline CreditsAmount operator-() const {\n\t\tauto result = *this;\n\t\tresult *= -1;\n\t\treturn result;\n\t}\n\n// AppleClang :/\n//\tfriend inline auto operator<=>(CreditsAmount, CreditsAmount)\n//\t\t= default;\n\tfriend inline constexpr auto operator<=>(\n\t\t\tCreditsAmount a,\n\t\t\tCreditsAmount b) {\n\t\tif (const auto r1 = (a._whole <=> b._whole); r1 != 0) {\n\t\t\treturn r1;\n\t\t} else if (const auto r2 = (a._nano <=> b._nano); r2 != 0) {\n\t\t\treturn r2;\n\t\t}\n\t\treturn (a._whole || a._nano)\n\t\t\t? (int(a._ton) <=> int(b._ton))\n\t\t\t: std::strong_ordering::equal;\n\t}\n\n\tfriend inline bool operator==(CreditsAmount, CreditsAmount)\n\t\t= default;\n\n\t[[nodiscard]] CreditsAmount abs() const {\n\t\treturn (_whole < 0) ? CreditsAmount(-_whole, -_nano) : *this;\n\t}\n\nprivate:\n\tvoid normalize() {\n\t\tif (_nano < 0) {\n\t\t\tconst auto shifts = (-_nano + kOneStarInNano - 1)\n\t\t\t\t/ kOneStarInNano;\n\t\t\t_nano += shifts * kOneStarInNano;\n\t\t\t_whole -= shifts;\n\t\t} else if (_nano >= kOneStarInNano) {\n\t\t\tconst auto shifts = _nano / kOneStarInNano;\n\t\t\t_nano -= shifts * kOneStarInNano;\n\t\t\t_whole += shifts;\n\t\t}\n\t}\n\n\tint64 _ton : 2 = 0;\n\tint64 _whole : 62 = 0;\n\tint64 _nano = 0;\n\n};\n\n[[nodiscard]] inline CreditsAmount operator+(\n\t\tCreditsAmount a,\n\t\tCreditsAmount b) {\n\treturn a += b;\n}\n\n[[nodiscard]] inline CreditsAmount operator-(\n\t\tCreditsAmount a,\n\t\tCreditsAmount b) {\n\treturn a -= b;\n}\n\n[[nodiscard]] inline CreditsAmount operator*(CreditsAmount a, int64 b) {\n\treturn a *= b;\n}\n\n[[nodiscard]] inline CreditsAmount operator*(int64 a, CreditsAmount b) {\n\treturn b *= a;\n}\n\n[[nodiscard]] CreditsAmount CreditsAmountFromTL(\n\tconst MTPStarsAmount &amount);\n[[nodiscard]] CreditsAmount CreditsAmountFromTL(\n\tconst MTPStarsAmount *amount);\n[[nodiscard]] MTPStarsAmount StarsAmountToTL(CreditsAmount amount);\n\n[[nodiscard]] QString PrepareCreditsAmountText(CreditsAmount amount);\n"
  },
  {
    "path": "Telegram/SourceFiles/core/current_geo_location.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/current_geo_location.h\"\n\n#include \"base/platform/base_platform_info.h\"\n#include \"base/invoke_queued.h\"\n#include \"base/timer.h\"\n#include \"data/raw/raw_countries_bounds.h\"\n#include \"platform/platform_current_geo_location.h\"\n#include \"ui/ui_utility.h\"\n\n#include <QtNetwork/QNetworkAccessManager>\n#include <QtNetwork/QNetworkReply>\n#include <QtCore/QCoreApplication>\n#include <QtCore/QPointer>\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonObject>\n#include <QtCore/QJsonArray>\n\nnamespace Core {\nnamespace {\n\nconstexpr auto kDestroyManagerTimeout = 20 * crl::time(1000);\n\n[[nodiscard]] QString ChooseLanguage(const QString &language) {\n\t// https://docs.mapbox.com/api/search/geocoding#language-coverage\n\tauto result = language.toLower().replace('-', '_');\n\tstatic const auto kGood = std::array{\n\t\t// Global coverage.\n\t\tu\"de\"_q, u\"en\"_q, u\"es\"_q, u\"fr\"_q, u\"it\"_q, u\"nl\"_q, u\"pl\"_q,\n\n\t\t// Local coverage.\n\t\tu\"az\"_q, u\"bn\"_q, u\"ca\"_q, u\"cs\"_q, u\"da\"_q, u\"el\"_q, u\"fa\"_q,\n\t\tu\"fi\"_q, u\"ga\"_q, u\"hu\"_q, u\"id\"_q, u\"is\"_q, u\"ja\"_q, u\"ka\"_q,\n\t\tu\"km\"_q, u\"ko\"_q, u\"lt\"_q, u\"lv\"_q, u\"mn\"_q, u\"pt\"_q, u\"ro\"_q,\n\t\tu\"sk\"_q, u\"sq\"_q, u\"sv\"_q, u\"th\"_q, u\"tl\"_q, u\"uk\"_q, u\"vi\"_q,\n\t\tu\"zh\"_q, u\"zh_Hans\"_q, u\"zh_TW\"_q,\n\n\t\t// Limited coverage.\n\t\tu\"ar\"_q, u\"bs\"_q, u\"gu\"_q, u\"he\"_q, u\"hi\"_q, u\"kk\"_q, u\"lo\"_q,\n\t\tu\"my\"_q, u\"nb\"_q, u\"ru\"_q, u\"sr\"_q, u\"te\"_q, u\"tk\"_q, u\"tr\"_q,\n\t\tu\"zh_Hant\"_q,\n\t};\n\tfor (const auto &known : kGood) {\n\t\tif (known.toLower() == result) {\n\t\t\treturn known;\n\t\t}\n\t}\n\tif (const auto delimeter = result.indexOf('_'); delimeter > 0) {\n\t\tresult = result.mid(0, delimeter);\n\t\tfor (const auto &known : kGood) {\n\t\t\tif (known == result) {\n\t\t\t\treturn known;\n\t\t\t}\n\t\t}\n\t}\n\treturn u\"en\"_q;\n}\n\nvoid ResolveLocationAddressGeneric(\n\t\tconst GeoLocation &location,\n\t\tconst QString &language,\n\t\tconst QString &token,\n\t\tFn<void(GeoAddress)> callback) {\n\tconst auto partialUrl = u\"https://api.mapbox.com/search/geocode/v6\"\n\t\t\"/reverse?longitude=%1&latitude=%2&language=%3&access_token=%4\"_q\n\t\t.arg(location.point.y())\n\t\t.arg(location.point.x())\n\t\t.arg(ChooseLanguage(language));\n\tstatic auto Cache = base::flat_map<QString, GeoAddress>();\n\tconst auto i = Cache.find(partialUrl);\n\tif (i != end(Cache)) {\n\t\tcallback(i->second);\n\t\treturn;\n\t}\n\tconst auto finishWith = [=](GeoAddress result) {\n\t\tCache[partialUrl] = result;\n\t\tcallback(result);\n\t};\n\n\tstruct State final : QObject {\n\t\texplicit State(QObject *parent)\n\t\t: QObject(parent)\n\t\t, manager(this)\n\t\t, destroyer([=] { if (sent.empty()) delete this; }) {\n\t\t}\n\n\t\tQNetworkAccessManager manager;\n\t\tstd::vector<QPointer<QNetworkReply>> sent;\n\t\tbase::Timer destroyer;\n\t};\n\n\tstatic auto state = QPointer<State>();\n\tif (!state) {\n\t\tstate = Ui::CreateChild<State>(qApp);\n\t}\n\tconst auto destroyReplyDelayed = [](QNetworkReply *reply) {\n\t\tInvokeQueued(reply, [=] {\n\t\t\tfor (auto i = begin(state->sent); i != end(state->sent);) {\n\t\t\t\tif (!*i || *i == reply) {\n\t\t\t\t\ti = state->sent.erase(i);\n\t\t\t\t} else {\n\t\t\t\t\t++i;\n\t\t\t\t}\n\t\t\t}\n\t\t\tdelete reply;\n\t\t\tif (state->sent.empty()) {\n\t\t\t\tstate->destroyer.callOnce(kDestroyManagerTimeout);\n\t\t\t}\n\t\t});\n\t};\n\n\tauto request = QNetworkRequest(partialUrl.arg(token));\n\trequest.setRawHeader(\"Referer\", \"http://desktop-app-resource/\");\n\n\tconst auto reply = state->manager.get(request);\n\tQObject::connect(reply, &QNetworkReply::finished, [=] {\n\t\tdestroyReplyDelayed(reply);\n\n\t\tconst auto json = QJsonDocument::fromJson(reply->readAll());\n\t\tif (!json.isObject()) {\n\t\t\tfinishWith({});\n\t\t\treturn;\n\t\t}\n\t\tconst auto features = json[\"features\"].toArray();\n\t\tif (features.isEmpty()) {\n\t\t\tfinishWith({});\n\t\t\treturn;\n\t\t}\n\t\tconst auto feature = features.at(0).toObject();\n\t\tconst auto properties = feature[\"properties\"].toObject();\n\t\tconst auto context = properties[\"context\"].toObject();\n\t\tauto names = QStringList();\n\t\tauto add = [&](std::vector<QString> keys) {\n\t\t\tfor (const auto &key : keys) {\n\t\t\t\tconst auto value = context[key];\n\t\t\t\tif (value.isObject()) {\n\t\t\t\t\tconst auto name = value.toObject()[\"name\"].toString();\n\t\t\t\t\tif (!name.isEmpty()) {\n\t\t\t\t\t\tnames.push_back(name);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tadd({ /*u\"address\"_q, u\"street\"_q, */u\"neighborhood\"_q });\n\t\tadd({ u\"place\"_q, u\"region\"_q });\n\t\tadd({ u\"country\"_q });\n\t\tfinishWith({ .name = names.join(\", \") });\n\t});\n\tQObject::connect(reply, &QNetworkReply::errorOccurred, [=] {\n\t\tdestroyReplyDelayed(reply);\n\n\t\tfinishWith({});\n\t});\n}\n\n} // namespace\n\nGeoLocation ResolveCurrentCountryLocation() {\n\tconst auto iso2 = Platform::SystemCountry().toUpper();\n\tconst auto &bounds = Raw::CountryBounds();\n\tconst auto i = bounds.find(iso2);\n\tif (i == end(bounds)) {\n\t\treturn {\n\t\t\t.accuracy = GeoLocationAccuracy::Failed,\n\t\t};\n\t}\n\treturn {\n\t\t.point = {\n\t\t\t(i->second.minLat + i->second.maxLat) / 2.,\n\t\t\t(i->second.minLon + i->second.maxLon) / 2.,\n\t\t},\n\t\t.bounds = {\n\t\t\ti->second.minLat,\n\t\t\ti->second.minLon,\n\t\t\ti->second.maxLat - i->second.minLat,\n\t\t\ti->second.maxLon - i->second.minLon,\n\t\t},\n\t\t.accuracy = GeoLocationAccuracy::Country,\n\t};\n}\n\nvoid ResolveCurrentGeoLocation(Fn<void(GeoLocation)> callback) {\n\tusing namespace Platform;\n\treturn ResolveCurrentExactLocation([done = std::move(callback)](\n\t\t\tGeoLocation result) {\n\t\tdone(result.accuracy != GeoLocationAccuracy::Failed\n\t\t\t? result\n\t\t\t: ResolveCurrentCountryLocation());\n\t});\n}\n\nvoid ResolveLocationAddress(\n\t\tconst GeoLocation &location,\n\t\tconst QString &language,\n\t\tconst QString &token,\n\t\tFn<void(GeoAddress)> callback) {\n\tauto done = [=, done = std::move(callback)](GeoAddress result) mutable {\n\t\tif (!result && !token.isEmpty()) {\n\t\t\tResolveLocationAddressGeneric(\n\t\t\t\tlocation,\n\t\t\t\tlanguage,\n\t\t\t\ttoken,\n\t\t\t\tstd::move(done));\n\t\t} else {\n\t\t\tdone(result);\n\t\t}\n\t};\n\tPlatform::ResolveLocationAddress(location, language, std::move(done));\n}\n\nbool AreTheSame(const GeoLocation &a, const GeoLocation &b) {\n\tif (a.accuracy != GeoLocationAccuracy::Exact\n\t\t|| b.accuracy != GeoLocationAccuracy::Exact) {\n\t\treturn false;\n\t}\n\tconst auto normalize = [](float64 value) {\n\t\tvalue = std::fmod(value + 180., 360.);\n\t\treturn (value + (value < 0. ? 360. : 0.)) - 180.;\n\t};\n\tconstexpr auto kEpsilon = 0.0001;\n\tconst auto lon1 = normalize(a.point.y());\n\tconst auto lon2 = normalize(b.point.y());\n\tconst auto diffLat = std::abs(a.point.x() - b.point.x());\n\tif (std::abs(a.point.x()) >= (90. - kEpsilon)\n\t\t|| std::abs(b.point.x()) >= (90. - kEpsilon)) {\n\t\treturn diffLat <= kEpsilon;\n\t}\n\tauto diffLon = std::abs(lon1 - lon2);\n\tif (diffLon > 180.) {\n\t\tdiffLon = 360. - diffLon;\n\t}\n\n\treturn diffLat <= kEpsilon && diffLon <= kEpsilon;\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/current_geo_location.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Core {\n\nenum class GeoLocationAccuracy : uchar {\n\tExact,\n\tCountry,\n\tFailed,\n};\n\nstruct GeoLocation {\n\tQPointF point;\n\tQRectF bounds;\n\tGeoLocationAccuracy accuracy = GeoLocationAccuracy::Failed;\n\n\t[[nodiscard]] bool exact() const {\n\t\treturn accuracy == GeoLocationAccuracy::Exact;\n\t}\n\t[[nodiscard]] bool country() const {\n\t\treturn accuracy == GeoLocationAccuracy::Country;\n\t}\n\t[[nodiscard]] bool failed() const {\n\t\treturn accuracy == GeoLocationAccuracy::Failed;\n\t}\n\n\texplicit operator bool() const {\n\t\treturn !failed();\n\t}\n};\n\n[[nodiscard]] bool AreTheSame(const GeoLocation &a, const GeoLocation &b);\n\nstruct GeoAddress {\n\tQString name;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn name.isEmpty();\n\t}\n\texplicit operator bool() const {\n\t\treturn !empty();\n\t}\n};\n\n[[nodiscard]] GeoLocation ResolveCurrentCountryLocation();\nvoid ResolveCurrentGeoLocation(Fn<void(GeoLocation)> callback);\n\nvoid ResolveLocationAddress(\n\tconst GeoLocation &location,\n\tconst QString &language,\n\tconst QString &token,\n\tFn<void(GeoAddress)> callback);\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/deadlock_detector.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Core::DeadlockDetector {\n\nclass PingPongEvent : public QEvent {\npublic:\n\tstatic auto Type() {\n\t\tstatic const auto Result = QEvent::Type(QEvent::registerEventType());\n\t\treturn Result;\n\t}\n\n\tPingPongEvent(not_null<QObject*> sender)\n\t: QEvent(Type())\n\t, _sender(sender) {\n\t}\n\n\t[[nodiscard]] not_null<QObject*> sender() const {\n\t\treturn _sender;\n\t}\n\nprivate:\n\tnot_null<QObject*> _sender;\n\n};\n\nclass Pinger : public QObject {\npublic:\n\tPinger(not_null<QObject*> receiver)\n\t: _receiver(receiver)\n\t, _abortTimer([] { Unexpected(\"Deadlock found!\"); }) {\n\t\tconst auto callback = [=] {\n\t\t\tQCoreApplication::postEvent(_receiver, new PingPongEvent(this));\n\t\t\t_abortTimer.callOnce(30000);\n\t\t};\n\t\t_pingTimer.setCallback(callback);\n\t\t_pingTimer.callEach(60000);\n\t\tcallback();\n\t}\n\nprotected:\n\tbool event(QEvent *e) override {\n\t\tif (e->type() == PingPongEvent::Type()\n\t\t\t&& static_cast<PingPongEvent*>(e)->sender() == _receiver) {\n\t\t\t_abortTimer.cancel();\n\t\t}\n\t\treturn QObject::event(e);\n\t}\n\nprivate:\n\tnot_null<QObject*> _receiver;\n\tbase::Timer _pingTimer;\n\tbase::Timer _abortTimer;\n\n};\n\nclass PingThread : public QThread {\npublic:\n\tPingThread(not_null<QObject*> parent)\n\t: QThread(parent) {\n\t\tstart();\n\t}\n\n\t~PingThread() {\n\t\tquit();\n\t\twait();\n\t}\n\nprotected:\n\tvoid run() override {\n\t\tPinger pinger(parent());\n\t\tQThread::run();\n\t}\n\n};\n\n} // namespace Core::DeadlockDetector\n"
  },
  {
    "path": "Telegram/SourceFiles/core/deep_links/deep_links_chats.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/deep_links/deep_links_router.h\"\n\n#include \"dialogs/dialogs_key.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace Core::DeepLinks {\nnamespace {\n\nResult FocusSearch(const Context &ctx) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tctx.controller->content()->searchMessages(QString(), Dialogs::Key());\n\treturn Result::Handled;\n}\n\nResult ShowEmojiStatus(const Context &ctx) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tctx.controller->setHighlightControlId(u\"main-menu/emoji-status\"_q);\n\tctx.controller->widget()->showMainMenu();\n\treturn Result::Handled;\n}\n\n} // namespace\n\nvoid RegisterChatsHandlers(Router &router) {\n\trouter.add(u\"chats\"_q, {\n\t\t.path = u\"search\"_q,\n\t\t.action = CodeBlock{ FocusSearch },\n\t\t.skipActivation = true,\n\t});\n\n\trouter.add(u\"chats\"_q, {\n\t\t.path = u\"emoji-status\"_q,\n\t\t.action = CodeBlock{ ShowEmojiStatus },\n\t});\n}\n\n} // namespace Core::DeepLinks\n"
  },
  {
    "path": "Telegram/SourceFiles/core/deep_links/deep_links_chats.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Core::DeepLinks {\n\nclass Router;\n\nvoid RegisterChatsHandlers(Router &router);\n\n} // namespace Core::DeepLinks\n"
  },
  {
    "path": "Telegram/SourceFiles/core/deep_links/deep_links_contacts.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/deep_links/deep_links_router.h\"\n\n#include \"boxes/peer_list_controllers.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace Core::DeepLinks {\nnamespace {\n\nResult ShowContacts(const Context &ctx) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tctx.controller->show(PrepareContactsBox(ctx.controller));\n\treturn Result::Handled;\n}\n\nResult ShowAddContact(const Context &ctx) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tctx.controller->showAddContact();\n\treturn Result::Handled;\n}\n\n} // namespace\n\nvoid RegisterContactsHandlers(Router &router) {\n\trouter.add(u\"contacts\"_q, {\n\t\t.path = QString(),\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowContacts(ctx);\n\t\t}},\n\t});\n\n\trouter.add(u\"contacts\"_q, {\n\t\t.path = u\"search\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowContacts(ctx);\n\t\t}},\n\t});\n\n\trouter.add(u\"contacts\"_q, {\n\t\t.path = u\"sort\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tctx.controller->setHighlightControlId(u\"contacts/sort\"_q);\n\t\t\tctx.controller->show(PrepareContactsBox(ctx.controller));\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"contacts\"_q, {\n\t\t.path = u\"new\"_q,\n\t\t.action = CodeBlock{ ShowAddContact },\n\t});\n}\n\n} // namespace Core::DeepLinks\n"
  },
  {
    "path": "Telegram/SourceFiles/core/deep_links/deep_links_contacts.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Core::DeepLinks {\n\nclass Router;\n\nvoid RegisterContactsHandlers(Router &router);\n\n} // namespace Core::DeepLinks\n"
  },
  {
    "path": "Telegram/SourceFiles/core/deep_links/deep_links_new.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/deep_links/deep_links_router.h\"\n\n#include \"apiwrap.h\"\n#include \"boxes/peers/create_managed_bot_box.h\"\n#include \"data/data_peer_id.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/toast/toast.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n\nnamespace Core::DeepLinks {\nnamespace {\n\nResult ShowNewGroup(const Context &ctx) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tctx.controller->showNewGroup();\n\treturn Result::Handled;\n}\n\nResult ShowNewChannel(const Context &ctx) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tctx.controller->showNewChannel();\n\treturn Result::Handled;\n}\n\nResult ShowAddContact(const Context &ctx) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tctx.controller->showAddContact();\n\treturn Result::Handled;\n}\n\nResult ShowNewBot(const Context &ctx) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tconst auto manager = ctx.params.value(u\"manager\"_q);\n\tconst auto username = ctx.params.value(u\"username\"_q);\n\tconst auto title = ctx.params.value(u\"name\"_q);\n\tif (manager.isEmpty()) {\n\t\treturn Result::Handled;\n\t}\n\tconst auto session = &ctx.controller->session();\n\tconst auto weak = base::make_weak(ctx.controller);\n\tsession->api().request(MTPcontacts_ResolveUsername(\n\t\tMTP_flags(0),\n\t\tMTP_string(manager),\n\t\tMTP_string()\n\t)).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\tconst auto strong = weak.get();\n\t\tif (!strong) {\n\t\t\treturn;\n\t\t}\n\t\tresult.match([&](const MTPDcontacts_resolvedPeer &data) {\n\t\t\tstrong->session().data().processUsers(data.vusers());\n\t\t\tstrong->session().data().processChats(data.vchats());\n\t\t\tconst auto peerId = peerFromMTP(data.vpeer());\n\t\t\tif (const auto managerBot = strong->session().data().userLoaded(\n\t\t\t\t\tpeerToUser(peerId))) {\n\t\t\t\tif (!managerBot->isBot()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!managerBot->botInfo->canManageBots) {\n\t\t\t\t\tstrong->showToast(\n\t\t\t\t\t\ttr::lng_create_bot_no_manage(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_bot,\n\t\t\t\t\t\t\tu\"@\"_q + managerBot->username()));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tShowCreateManagedBotBox({\n\t\t\t\t\t.show = strong->uiShow(),\n\t\t\t\t\t.manager = managerBot,\n\t\t\t\t\t.suggestedName = title,\n\t\t\t\t\t.suggestedUsername = username,\n\t\t\t\t\t.viaDeeplink = true,\n\t\t\t\t\t.done = [weak, managerBot](not_null<UserData*> createdBot) {\n\t\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\t\tstrong->showPeerHistory(createdBot);\n\t\t\t\t\t\t\tstrong->showToast({\n\t\t\t\t\t\t\t\t.title = tr::lng_managed_bot_created_title(\n\t\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\t\t\t\tcreatedBot->name()),\n\t\t\t\t\t\t\t\t.text = { tr::lng_managed_bot_created_text(\n\t\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\t\tlt_parent_name,\n\t\t\t\t\t\t\t\t\tmanagerBot->name()) },\n\t\t\t\t\t\t\t\t.icon = &st::toastCheckIcon,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t}).send();\n\treturn Result::Handled;\n}\n\n} // namespace\n\nvoid RegisterNewHandlers(Router &router) {\n\trouter.add(u\"new\"_q, {\n\t\t.path = u\"group\"_q,\n\t\t.action = CodeBlock{ ShowNewGroup },\n\t});\n\n\trouter.add(u\"new\"_q, {\n\t\t.path = u\"channel\"_q,\n\t\t.action = CodeBlock{ ShowNewChannel },\n\t});\n\n\trouter.add(u\"new\"_q, {\n\t\t.path = u\"contact\"_q,\n\t\t.action = CodeBlock{ ShowAddContact },\n\t});\n\n\trouter.add(u\"newbot\"_q, {\n\t\t.path = QString(),\n\t\t.action = CodeBlock{ ShowNewBot },\n\t});\n}\n\n} // namespace Core::DeepLinks\n"
  },
  {
    "path": "Telegram/SourceFiles/core/deep_links/deep_links_new.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Core::DeepLinks {\n\nclass Router;\n\nvoid RegisterNewHandlers(Router &router);\n\n} // namespace Core::DeepLinks\n"
  },
  {
    "path": "Telegram/SourceFiles/core/deep_links/deep_links_router.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/deep_links/deep_links_router.h\"\n\n#include \"base/qthelp_url.h\"\n#include \"core/deep_links/deep_links_chats.h\"\n#include \"core/deep_links/deep_links_contacts.h\"\n#include \"core/deep_links/deep_links_new.h\"\n#include \"core/deep_links/deep_links_settings.h\"\n#include \"core/application.h\"\n#include \"main/main_session.h\"\n#include \"ui/toast/toast.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace Core::DeepLinks {\nnamespace {\n\nContext ParseCommand(\n\t\tWindow::SessionController *controller,\n\t\tconst QString &command) {\n\tauto result = Context{ .controller = controller };\n\n\tauto path = command;\n\tauto queryStart = path.indexOf('?');\n\tif (queryStart >= 0) {\n\t\tconst auto query = path.mid(queryStart + 1);\n\t\tpath = path.left(queryStart);\n\t\tfor (const auto &pair : query.split('&')) {\n\t\t\tconst auto eq = pair.indexOf('=');\n\t\t\tif (eq > 0) {\n\t\t\t\tresult.params[pair.left(eq).toLower()] = qthelp::url_decode(pair.mid(eq + 1));\n\t\t\t} else if (!pair.isEmpty()) {\n\t\t\t\tresult.params[pair.toLower()] = QString();\n\t\t\t}\n\t\t}\n\t}\n\n\tpath = path.trimmed();\n\twhile (path.startsWith('/')) {\n\t\tpath = path.mid(1);\n\t}\n\twhile (path.endsWith('/')) {\n\t\tpath.chop(1);\n\t}\n\n\tconst auto slash = path.indexOf('/');\n\tif (slash > 0) {\n\t\tresult.section = path.left(slash).toLower();\n\t\tresult.path = path.mid(slash + 1);\n\t} else {\n\t\tresult.section = path.toLower();\n\t}\n\n\treturn result;\n}\n\n} // namespace\n\nRouter &Router::Instance() {\n\tstatic auto instance = Router();\n\treturn instance;\n}\n\nRouter::Router() {\n\tRegisterSettingsHandlers(*this);\n\tRegisterContactsHandlers(*this);\n\tRegisterChatsHandlers(*this);\n\tRegisterNewHandlers(*this);\n}\n\nvoid Router::add(const QString &section, Entry entry) {\n\t_handlers[section].push_back(std::move(entry));\n}\n\nbool Router::tryHandle(\n\t\tWindow::SessionController *controller,\n\t\tconst QString &command) {\n\tconst auto ctx = ParseCommand(controller, command);\n\tconst auto [result, skipActivation] = dispatch(ctx);\n\n\tswitch (result) {\n\tcase Result::Handled:\n\t\tif (controller && !skipActivation) {\n\t\t\tcontroller->window().activate();\n\t\t}\n\t\treturn true;\n\tcase Result::NeedsAuth:\n\t\treturn false;\n\tcase Result::Unsupported:\n\t\tshowUnsupportedMessage(controller, command);\n\t\treturn true;\n\tcase Result::NotFound:\n\t\treturn false;\n\t}\n\treturn false;\n}\n\nRouter::DispatchResult Router::dispatch(const Context &ctx) {\n\tif (ctx.section.isEmpty()) {\n\t\treturn { Result::NotFound };\n\t}\n\treturn handleSection(ctx.section, ctx);\n}\n\nRouter::DispatchResult Router::handleSection(\n\t\tconst QString &section,\n\t\tconst Context &ctx) {\n\tconst auto it = _handlers.find(section);\n\tif (it == _handlers.end()) {\n\t\treturn { Result::NotFound };\n\t}\n\n\tconst auto &entries = it->second;\n\tconst auto path = ctx.path.toLower();\n\n\tfor (const auto &entry : entries) {\n\t\tif (entry.path == path || (entry.path.isEmpty() && path.isEmpty())) {\n\t\t\tif (entry.requiresAuth && !ctx.controller) {\n\t\t\t\treturn { Result::NeedsAuth };\n\t\t\t}\n\t\t\treturn {\n\t\t\t\texecuteAction(entry.action, ctx),\n\t\t\t\tentry.skipActivation,\n\t\t\t};\n\t\t}\n\t}\n\n\tfor (const auto &entry : entries) {\n\t\tif (!entry.path.isEmpty() && path.startsWith(entry.path + '/')) {\n\t\t\tif (entry.requiresAuth && !ctx.controller) {\n\t\t\t\treturn { Result::NeedsAuth };\n\t\t\t}\n\t\t\treturn {\n\t\t\t\texecuteAction(entry.action, ctx),\n\t\t\t\tentry.skipActivation,\n\t\t\t};\n\t\t}\n\t}\n\n\treturn { Result::Unsupported };\n}\n\nResult Router::executeAction(const Action &action, const Context &ctx) {\n\treturn v::match(action, [&](const SettingsSection &s) {\n\t\tif (!ctx.controller) {\n\t\t\treturn Result::NeedsAuth;\n\t\t}\n\t\tctx.controller->showSettings(s.sectionId);\n\t\treturn Result::Handled;\n\t}, [&](const SettingsControl &s) {\n\t\tif (!ctx.controller) {\n\t\t\treturn Result::NeedsAuth;\n\t\t}\n\t\tif (!s.controlId.isEmpty()) {\n\t\t\tctx.controller->setHighlightControlId(s.controlId);\n\t\t}\n\t\tctx.controller->showSettings(s.sectionId);\n\t\treturn Result::Handled;\n\t}, [&](const CodeBlock &c) {\n\t\treturn c.handler(ctx);\n\t}, [&](const AliasTo &a) {\n\t\tauto aliasCtx = ctx;\n\t\taliasCtx.section = a.section;\n\t\taliasCtx.path = a.path;\n\t\treturn handleSection(a.section, aliasCtx).result;\n\t});\n}\n\nvoid Router::showUnsupportedMessage(\n\t\tWindow::SessionController *controller,\n\t\tconst QString &url) {\n\tconst auto text = u\"This link is not supported on Desktop.\"_q;\n\tif (controller) {\n\t\tcontroller->showToast(text);\n\t} else if (const auto window = Core::App().activeWindow()) {\n\t\twindow->showToast(text);\n\t}\n}\n\n} // namespace Core::DeepLinks\n"
  },
  {
    "path": "Telegram/SourceFiles/core/deep_links/deep_links_router.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"core/deep_links/deep_links_types.h\"\n\nnamespace Core::DeepLinks {\n\nclass Router final {\npublic:\n\tstatic Router &Instance();\n\n\t[[nodiscard]] bool tryHandle(\n\t\tWindow::SessionController *controller,\n\t\tconst QString &command);\n\n\tvoid add(const QString &section, Entry entry);\n\nprivate:\n\tstruct DispatchResult {\n\t\tResult result = Result::NotFound;\n\t\tbool skipActivation = false;\n\t};\n\n\tRouter();\n\n\t[[nodiscard]] DispatchResult dispatch(const Context &ctx);\n\t[[nodiscard]] DispatchResult handleSection(\n\t\tconst QString &section,\n\t\tconst Context &ctx);\n\t[[nodiscard]] Result executeAction(\n\t\tconst Action &action,\n\t\tconst Context &ctx);\n\n\tvoid showUnsupportedMessage(\n\t\tWindow::SessionController *controller,\n\t\tconst QString &url);\n\n\tstd::map<QString, std::vector<Entry>> _handlers;\n\n};\n\n} // namespace Core::DeepLinks\n"
  },
  {
    "path": "Telegram/SourceFiles/core/deep_links/deep_links_settings.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/deep_links/deep_links_router.h\"\n\n#include \"apiwrap.h\"\n#include \"base/binary_guard.h\"\n#include \"boxes/add_contact_box.h\"\n#include \"boxes/gift_credits_box.h\"\n#include \"boxes/language_box.h\"\n#include \"boxes/stickers_box.h\"\n#include \"chat_helpers/emoji_sets_manager.h\"\n#include \"boxes/edit_privacy_box.h\"\n#include \"boxes/peers/edit_peer_color_box.h\"\n#include \"info/bot/earn/info_bot_earn_widget.h\"\n#include \"info/bot/starref/info_bot_starref_common.h\"\n#include \"info/bot/starref/info_bot_starref_join_widget.h\"\n#include \"settings/settings_privacy_controllers.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"boxes/username_box.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/data_user.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"info/info_memento.h\"\n#include \"info/peer_gifts/info_peer_gifts_widget.h\"\n#include \"info/settings/info_settings_widget.h\"\n#include \"info/stories/info_stories_widget.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/boxes/peer_qr_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"storage/storage_domain.h\"\n#include \"settings/sections/settings_active_sessions.h\"\n#include \"settings/sections/settings_advanced.h\"\n#include \"settings/sections/settings_blocked_peers.h\"\n#include \"settings/sections/settings_business.h\"\n#include \"settings/sections/settings_calls.h\"\n#include \"settings/sections/settings_chat.h\"\n#include \"settings/sections/settings_passkeys.h\"\n#include \"settings/sections/settings_fork.h\"\n#include \"data/components/passkeys.h\"\n#include \"calls/calls_box_controller.h\"\n#include \"settings/sections/settings_credits.h\"\n#include \"settings/sections/settings_folders.h\"\n#include \"settings/sections/settings_global_ttl.h\"\n#include \"settings/sections/settings_information.h\"\n#include \"settings/sections/settings_local_passcode.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"settings/cloud_password/settings_cloud_password_email_confirm.h\"\n#include \"settings/cloud_password/settings_cloud_password_input.h\"\n#include \"settings/cloud_password/settings_cloud_password_start.h\"\n#include \"settings/cloud_password/settings_cloud_password_login_email.h\"\n#include \"api/api_cloud_password.h\"\n#include \"core/core_cloud_password.h\"\n#include \"settings/sections/settings_notifications.h\"\n#include \"settings/sections/settings_notifications_type.h\"\n#include \"settings/settings_power_saving.h\"\n#include \"settings/settings_search.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/power_saving.h\"\n#include \"settings/sections/settings_privacy_security.h\"\n#include \"settings/sections/settings_websites.h\"\n#include \"boxes/connection_box.h\"\n#include \"boxes/local_storage_box.h\"\n#include \"mainwindow.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace Core::DeepLinks {\nnamespace {\n\nResult ShowLanguageBox(const Context &ctx, const QString &highlightId = QString()) {\n\tstatic auto Guard = base::binary_guard();\n\tif (!highlightId.isEmpty() && ctx.controller) {\n\t\tctx.controller->setHighlightControlId(highlightId);\n\t}\n\tGuard = LanguageBox::Show(ctx.controller, highlightId);\n\treturn Result::Handled;\n}\n\nResult ShowPowerSavingBox(\n\t\tconst Context &ctx,\n\t\tPowerSaving::Flags highlightFlags = PowerSaving::Flags()) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tctx.controller->show(\n\t\tBox(::Settings::PowerSavingBox, highlightFlags),\n\t\tUi::LayerOption::KeepOther,\n\t\tanim::type::normal);\n\treturn Result::Handled;\n}\n\nResult ShowMainMenuWithHighlight(const Context &ctx, const QString &highlightId) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tctx.controller->setHighlightControlId(highlightId);\n\tctx.controller->widget()->showMainMenu();\n\treturn Result::Handled;\n}\n\nResult ShowSavedMessages(const Context &ctx) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tctx.controller->showPeerHistory(\n\t\tctx.controller->session().userPeerId(),\n\t\tWindow::SectionShow::Way::Forward);\n\treturn Result::Handled;\n}\n\nResult ShowFaq(const Context &ctx) {\n\t::Settings::OpenFaq(\n\t\tctx.controller ? base::make_weak(ctx.controller) : nullptr);\n\treturn Result::Handled;\n}\n\nvoid ShowQrBox(not_null<Window::SessionController*> controller) {\n\tconst auto user = controller->session().user();\n\tcontroller->uiShow()->show(Box(\n\t\tUi::FillPeerQrBox,\n\t\tuser.get(),\n\t\tstd::nullopt,\n\t\trpl::single(QString())));\n}\n\nResult ShowPeerColorBox(\n\t\tconst Context &ctx,\n\t\tPeerColorTab tab,\n\t\tconst QString &highlightId = QString()) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tif (!highlightId.isEmpty()) {\n\t\tctx.controller->setHighlightControlId(highlightId);\n\t}\n\tctx.controller->show(Box(\n\t\tEditPeerColorBox,\n\t\tctx.controller,\n\t\tctx.controller->session().user(),\n\t\tstd::shared_ptr<Ui::ChatStyle>(),\n\t\tstd::shared_ptr<Ui::ChatTheme>(),\n\t\ttab));\n\treturn Result::Handled;\n}\n\nResult HandleQrCode(const Context &ctx, bool highlightCopy) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\n\tif (highlightCopy) {\n\t\tctx.controller->setHighlightControlId(u\"self-qr-code/copy\"_q);\n\t}\n\n\tconst auto user = ctx.controller->session().user();\n\tif (!user->username().isEmpty()) {\n\t\tShowQrBox(ctx.controller);\n\t} else {\n\t\tconst auto controller = ctx.controller;\n\t\tcontroller->uiShow()->show(Box(\n\t\t\tUsernamesBoxWithCallback,\n\t\t\tuser,\n\t\t\t[=] { ShowQrBox(controller); }));\n\t}\n\treturn Result::Handled;\n}\n\nResult ShowEditName(\n\t\tconst Context &ctx,\n\t\tEditNameBox::Focus focus = EditNameBox::Focus::FirstName) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tif (ctx.controller->showFrozenError()) {\n\t\treturn Result::Handled;\n\t}\n\tctx.controller->show(Box<EditNameBox>(\n\t\tctx.controller->session().user(),\n\t\tfocus));\n\treturn Result::Handled;\n}\n\nResult ShowEditUsername(const Context &ctx) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tif (ctx.controller->showFrozenError()) {\n\t\treturn Result::Handled;\n\t}\n\tctx.controller->show(Box(UsernamesBox, ctx.controller->session().user()));\n\treturn Result::Handled;\n}\n\nResult OpenInternalUrl(const Context &ctx, const QString &url) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tCore::App().openInternalUrl(\n\t\turl,\n\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t.sessionWindow = base::make_weak(ctx.controller),\n\t\t}));\n\treturn Result::Handled;\n}\n\nResult ShowMyProfile(const Context &ctx) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tctx.controller->showSection(\n\t\tInfo::Stories::Make(ctx.controller->session().user()));\n\treturn Result::Handled;\n}\n\nResult ShowLogOutMenu(const Context &ctx) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tctx.controller->setHighlightControlId(u\"settings/log-out\"_q);\n\tctx.controller->showSettings(::Settings::MainId());\n\treturn Result::Handled;\n}\n\nResult ShowPasskeys(const Context &ctx, bool highlightCreate) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tconst auto controller = ctx.controller;\n\tconst auto session = &controller->session();\n\tconst auto showBox = [=] {\n\t\tif (highlightCreate) {\n\t\t\tcontroller->setHighlightControlId(u\"passkeys/create\"_q);\n\t\t}\n\t\tif (session->passkeys().list().empty()) {\n\t\t\tcontroller->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\t::Settings::PasskeysNoneBox(box, session);\n\t\t\t\tbox->boxClosing() | rpl::on_next([=] {\n\t\t\t\t\tif (!session->passkeys().list().empty()) {\n\t\t\t\t\t\tcontroller->showSettings(::Settings::PasskeysId());\n\t\t\t\t\t}\n\t\t\t\t}, box->lifetime());\n\t\t\t}));\n\t\t} else {\n\t\t\tcontroller->showSettings(::Settings::PasskeysId());\n\t\t}\n\t};\n\tif (session->passkeys().listKnown()) {\n\t\tshowBox();\n\t} else {\n\t\tsession->passkeys().requestList(\n\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\tshowBox();\n\t\t}, controller->lifetime());\n\t}\n\treturn Result::Handled;\n}\n\nResult ShowAutoDeleteSetCustom(const Context &ctx) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tctx.controller->setHighlightControlId(u\"auto-delete/set-custom\"_q);\n\tctx.controller->showSettings(::Settings::GlobalTTLId());\n\treturn Result::Handled;\n}\n\nResult ShowLoginEmail(const Context &ctx) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tconst auto controller = ctx.controller;\n\tcontroller->session().api().cloudPassword().reload();\n\tcontroller->uiShow()->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t{\n\t\t\tbox->getDelegate()->setTitle(\n\t\t\t\tcontroller->session().api().cloudPassword().state(\n\t\t\t\t) | rpl::map([](const Core::CloudPasswordState &state) {\n\t\t\t\t\treturn state.loginEmailPattern;\n\t\t\t\t}) | rpl::map([](QString email) {\n\t\t\t\t\tif (email.contains(' ')) {\n\t\t\t\t\t\treturn tr::lng_settings_cloud_login_email_section_title(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\ttr::rich);\n\t\t\t\t\t}\n\t\t\t\t\treturn Ui::Text::WrapEmailPattern(std::move(email));\n\t\t\t\t}));\n\t\t\tfor (const auto &child : ranges::views::reverse(\n\t\t\t\t\tbox->parentWidget()->children())) {\n\t\t\t\tif (child && child->isWidgetType()) {\n\t\t\t\t\t(static_cast<QWidget*>(child))->setAttribute(\n\t\t\t\t\t\tQt::WA_TransparentForMouseEvents);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tUi::ConfirmBox(box, Ui::ConfirmBoxArgs{\n\t\t\t.text = tr::lng_settings_cloud_login_email_box_about(),\n\t\t\t.confirmed = [=](Fn<void()> close) {\n\t\t\t\tcontroller->showSettings(::Settings::CloudLoginEmailId());\n\t\t\t\tcontroller->window().activate();\n\t\t\t\tclose();\n\t\t\t},\n\t\t\t.confirmText = tr::lng_settings_cloud_login_email_box_ok(),\n\t\t});\n\t}));\n\treturn Result::Handled;\n}\n\nResult ShowNotificationType(\n\t\tconst Context &ctx,\n\t\tData::DefaultNotify type,\n\t\tconst QString &highlightId = QString()) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tif (!highlightId.isEmpty()) {\n\t\tctx.controller->setHighlightControlId(highlightId);\n\t}\n\tctx.controller->showSettings(::Settings::NotificationsType::Id(type));\n\treturn Result::Handled;\n}\n\nusing PrivacyKey = Api::UserPrivacy::Key;\n\ntemplate <typename ControllerFactory>\nResult ShowPrivacyBox(\n\t\tconst Context &ctx,\n\t\tPrivacyKey key,\n\t\tControllerFactory controllerFactory,\n\t\tconst QString &highlightControl = QString()) {\n\tif (!ctx.controller) {\n\t\treturn Result::NeedsAuth;\n\t}\n\tconst auto controller = ctx.controller;\n\tconst auto session = &controller->session();\n\tif (!highlightControl.isEmpty()) {\n\t\tcontroller->setHighlightControlId(highlightControl);\n\t}\n\tconst auto shower = std::make_shared<rpl::lifetime>();\n\t*shower = session->api().userPrivacy().value(\n\t\tkey\n\t) | rpl::take(\n\t\t1\n\t) | rpl::on_next(crl::guard(controller, [=, shower = shower](\n\t\t\tconst Api::UserPrivacy::Rule &value) {\n\t\tcontroller->show(Box<EditPrivacyBox>(\n\t\t\tcontroller,\n\t\t\tcontrollerFactory(),\n\t\t\tvalue));\n\t}));\n\tsession->api().userPrivacy().reload(key);\n\treturn Result::Handled;\n}\n\n} // namespace\n\nvoid RegisterSettingsHandlers(Router &router) {\n\trouter.add(u\"settings\"_q, {\n\t\t.path = QString(),\n\t\t.action = SettingsSection{ ::Settings::MainId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"edit\"_q,\n\t\t.action = SettingsSection{ ::Settings::InformationId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"my-profile\"_q,\n\t\t.action = CodeBlock{ ShowMyProfile },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"my-profile/edit\"_q,\n\t\t.action = SettingsSection{ ::Settings::InformationId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"my-profile/posts\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tctx.controller->setHighlightControlId(u\"my-profile/posts\"_q);\n\t\t\treturn ShowMyProfile(ctx);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"my-profile/posts/add-album\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tctx.controller->setHighlightControlId(u\"my-profile/posts/add-album\"_q);\n\t\t\treturn ShowMyProfile(ctx);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"my-profile/gifts\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tctx.controller->showSection(\n\t\t\t\tInfo::PeerGifts::Make(ctx.controller->session().user()));\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"my-profile/archived-posts\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tctx.controller->showSection(Info::Stories::Make(\n\t\t\t\tctx.controller->session().user(),\n\t\t\t\tInfo::Stories::ArchiveId()));\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"emoji-status\"_q,\n\t\t.action = AliasTo{ u\"chats\"_q, u\"emoji-status\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"profile-color\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"edit/your-color\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"profile-color/profile\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"edit/your-color\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"profile-color/profile/add-icons\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPeerColorBox(\n\t\t\t\tctx,\n\t\t\t\tPeerColorTab::Profile,\n\t\t\t\tu\"profile-color/add-icons\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"profile-color/profile/use-gift\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPeerColorBox(\n\t\t\t\tctx,\n\t\t\t\tPeerColorTab::Profile,\n\t\t\t\tu\"profile-color/use-gift\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"profile-color/profile/reset\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPeerColorBox(\n\t\t\t\tctx,\n\t\t\t\tPeerColorTab::Profile,\n\t\t\t\tu\"profile-color/reset\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"profile-color/name\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPeerColorBox(ctx, PeerColorTab::Name);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"profile-color/name/add-icons\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPeerColorBox(\n\t\t\t\tctx,\n\t\t\t\tPeerColorTab::Name,\n\t\t\t\tu\"profile-color/add-icons\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"profile-color/name/use-gift\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPeerColorBox(\n\t\t\t\tctx,\n\t\t\t\tPeerColorTab::Name,\n\t\t\t\tu\"profile-color/use-gift\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"profile-photo\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"edit/set-photo\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"profile-photo/use-emoji\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::MainId(),\n\t\t\tu\"profile-photo/use-emoji\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"devices\"_q,\n\t\t.action = SettingsSection{ ::Settings::SessionsId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"folders\"_q,\n\t\t.action = SettingsSection{ ::Settings::FoldersId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications\"_q,\n\t\t.action = SettingsSection{ ::Settings::NotificationsId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy\"_q,\n\t\t.action = SettingsSection{ ::Settings::PrivacySecurityId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/blocked\"_q,\n\t\t.action = SettingsSection{ ::Settings::BlockedPeersId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/blocked/block-user\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::BlockedPeersId(),\n\t\t\tu\"blocked/block-user\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/active-websites\"_q,\n\t\t.action = SettingsSection{ ::Settings::WebsitesId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/active-websites/disconnect-all\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::WebsitesId(),\n\t\t\tu\"websites/disconnect-all\"_q,\n\t\t},\n\t});\n\n\tconst auto openPasscode = [](const Context &ctx, const QString &highlight) {\n\t\tif (!ctx.controller) {\n\t\t\treturn Result::NeedsAuth;\n\t\t}\n\t\tif (!highlight.isEmpty()) {\n\t\t\tctx.controller->setHighlightControlId(highlight);\n\t\t}\n\t\tconst auto &local = ctx.controller->session().domain().local();\n\t\tif (local.hasLocalPasscode()) {\n\t\t\tctx.controller->showSettings(::Settings::LocalPasscodeCheckId());\n\t\t} else {\n\t\t\tctx.controller->showSettings(::Settings::LocalPasscodeCreateId());\n\t\t}\n\t\treturn Result::Handled;\n\t};\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/passcode\"_q,\n\t\t.action = CodeBlock{ [=](const Context &ctx) {\n\t\t\treturn openPasscode(ctx, QString());\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/passcode/disable\"_q,\n\t\t.action = CodeBlock{ [=](const Context &ctx) {\n\t\t\treturn openPasscode(ctx, u\"passcode/disable\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/passcode/change\"_q,\n\t\t.action = CodeBlock{ [=](const Context &ctx) {\n\t\t\treturn openPasscode(ctx, u\"passcode/change\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/passcode/auto-lock\"_q,\n\t\t.action = CodeBlock{ [=](const Context &ctx) {\n\t\t\treturn openPasscode(ctx, u\"passcode/auto-lock\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/passcode/face-id\"_q,\n\t\t.action = CodeBlock{ [=](const Context &ctx) {\n\t\t\treturn openPasscode(ctx, u\"passcode/biometrics\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/passcode/fingerprint\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"privacy/passcode/face-id\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/auto-delete\"_q,\n\t\t.action = SettingsSection{ ::Settings::GlobalTTLId() },\n\t});\n\n\tconst auto openCloudPassword = [](const Context &ctx, const QString &highlight) {\n\t\tif (!ctx.controller) {\n\t\t\treturn Result::NeedsAuth;\n\t\t}\n\t\tctx.controller->showCloudPassword(highlight);\n\t\treturn Result::Handled;\n\t};\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/2sv\"_q,\n\t\t.action = CodeBlock{ [=](const Context &ctx) {\n\t\t\treturn openCloudPassword(ctx, QString());\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/2sv/change\"_q,\n\t\t.action = CodeBlock{ [=](const Context &ctx) {\n\t\t\treturn openCloudPassword(ctx, u\"2sv/change\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/2sv/disable\"_q,\n\t\t.action = CodeBlock{ [=](const Context &ctx) {\n\t\t\treturn openCloudPassword(ctx, u\"2sv/disable\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/2sv/change-email\"_q,\n\t\t.action = CodeBlock{ [=](const Context &ctx) {\n\t\t\treturn openCloudPassword(ctx, u\"2sv/change-email\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/passkey\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPasskeys(ctx, false);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/passkey/create\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPasskeys(ctx, true);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/auto-delete/set-custom\"_q,\n\t\t.action = CodeBlock{ ShowAutoDeleteSetCustom },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/phone-number\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::PhoneNumber,\n\t\t\t\t[=] { return std::make_unique<::Settings::PhoneNumberPrivacyController>(ctx.controller); });\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/phone-number/never\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::PhoneNumber,\n\t\t\t\t[=] { return std::make_unique<::Settings::PhoneNumberPrivacyController>(ctx.controller); },\n\t\t\t\tu\"privacy/never\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/phone-number/always\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::PhoneNumber,\n\t\t\t\t[=] { return std::make_unique<::Settings::PhoneNumberPrivacyController>(ctx.controller); },\n\t\t\t\tu\"privacy/always\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/last-seen\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::LastSeen,\n\t\t\t\t[=] { return std::make_unique<::Settings::LastSeenPrivacyController>(&ctx.controller->session()); });\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/last-seen/never\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::LastSeen,\n\t\t\t\t[=] { return std::make_unique<::Settings::LastSeenPrivacyController>(&ctx.controller->session()); },\n\t\t\t\tu\"privacy/never\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/last-seen/always\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::LastSeen,\n\t\t\t\t[=] { return std::make_unique<::Settings::LastSeenPrivacyController>(&ctx.controller->session()); },\n\t\t\t\tu\"privacy/always\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/last-seen/hide-read-time\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::LastSeen,\n\t\t\t\t[=] { return std::make_unique<::Settings::LastSeenPrivacyController>(&ctx.controller->session()); },\n\t\t\t\tu\"privacy/hide-read-time\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/profile-photos\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::ProfilePhoto,\n\t\t\t\t[=] { return std::make_unique<::Settings::ProfilePhotoPrivacyController>(); });\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/profile-photos/never\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::ProfilePhoto,\n\t\t\t\t[=] { return std::make_unique<::Settings::ProfilePhotoPrivacyController>(); },\n\t\t\t\tu\"privacy/never\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/profile-photos/always\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::ProfilePhoto,\n\t\t\t\t[=] { return std::make_unique<::Settings::ProfilePhotoPrivacyController>(); },\n\t\t\t\tu\"privacy/always\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/profile-photos/set-public\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::ProfilePhoto,\n\t\t\t\t[=] { return std::make_unique<::Settings::ProfilePhotoPrivacyController>(); },\n\t\t\t\tu\"privacy/set-public\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/profile-photos/update-public\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::ProfilePhoto,\n\t\t\t\t[=] { return std::make_unique<::Settings::ProfilePhotoPrivacyController>(); },\n\t\t\t\tu\"privacy/update-public\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/profile-photos/remove-public\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::ProfilePhoto,\n\t\t\t\t[=] { return std::make_unique<::Settings::ProfilePhotoPrivacyController>(); },\n\t\t\t\tu\"privacy/remove-public\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/bio\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::About,\n\t\t\t\t[=] { return std::make_unique<::Settings::AboutPrivacyController>(); });\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/bio/never\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::About,\n\t\t\t\t[=] { return std::make_unique<::Settings::AboutPrivacyController>(); },\n\t\t\t\tu\"privacy/never\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/bio/always\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::About,\n\t\t\t\t[=] { return std::make_unique<::Settings::AboutPrivacyController>(); },\n\t\t\t\tu\"privacy/always\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/gifts\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::GiftsAutoSave,\n\t\t\t\t[=] { return std::make_unique<::Settings::GiftsAutoSavePrivacyController>(); });\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/gifts/show-icon\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::GiftsAutoSave,\n\t\t\t\t[=] { return std::make_unique<::Settings::GiftsAutoSavePrivacyController>(); },\n\t\t\t\tu\"privacy/show-icon\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/gifts/never\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::GiftsAutoSave,\n\t\t\t\t[=] { return std::make_unique<::Settings::GiftsAutoSavePrivacyController>(); },\n\t\t\t\tu\"privacy/never\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/gifts/always\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::GiftsAutoSave,\n\t\t\t\t[=] { return std::make_unique<::Settings::GiftsAutoSavePrivacyController>(); },\n\t\t\t\tu\"privacy/always\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/gifts/accepted-types\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::GiftsAutoSave,\n\t\t\t\t[=] { return std::make_unique<::Settings::GiftsAutoSavePrivacyController>(); },\n\t\t\t\tu\"privacy/accepted-types\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/birthday\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::Birthday,\n\t\t\t\t[=] { return std::make_unique<::Settings::BirthdayPrivacyController>(); });\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/birthday/add\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn OpenInternalUrl(ctx, u\"internal:edit_birthday\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/birthday/never\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::Birthday,\n\t\t\t\t[=] { return std::make_unique<::Settings::BirthdayPrivacyController>(); },\n\t\t\t\tu\"privacy/never\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/birthday/always\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::Birthday,\n\t\t\t\t[=] { return std::make_unique<::Settings::BirthdayPrivacyController>(); },\n\t\t\t\tu\"privacy/always\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/saved-music\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::SavedMusic,\n\t\t\t\t[=] { return std::make_unique<::Settings::SavedMusicPrivacyController>(); });\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/saved-music/never\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::SavedMusic,\n\t\t\t\t[=] { return std::make_unique<::Settings::SavedMusicPrivacyController>(); },\n\t\t\t\tu\"privacy/never\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/saved-music/always\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::SavedMusic,\n\t\t\t\t[=] { return std::make_unique<::Settings::SavedMusicPrivacyController>(); },\n\t\t\t\tu\"privacy/always\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/forwards\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::Forwards,\n\t\t\t\t[=] { return std::make_unique<::Settings::ForwardsPrivacyController>(ctx.controller); });\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/forwards/never\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::Forwards,\n\t\t\t\t[=] { return std::make_unique<::Settings::ForwardsPrivacyController>(ctx.controller); },\n\t\t\t\tu\"privacy/never\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/forwards/always\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::Forwards,\n\t\t\t\t[=] { return std::make_unique<::Settings::ForwardsPrivacyController>(ctx.controller); },\n\t\t\t\tu\"privacy/always\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/calls\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::Calls,\n\t\t\t\t[=] { return std::make_unique<::Settings::CallsPrivacyController>(); });\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/calls/never\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::Calls,\n\t\t\t\t[=] { return std::make_unique<::Settings::CallsPrivacyController>(); },\n\t\t\t\tu\"privacy/never\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/calls/always\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::Calls,\n\t\t\t\t[=] { return std::make_unique<::Settings::CallsPrivacyController>(); },\n\t\t\t\tu\"privacy/always\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/calls/p2p\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::CallsPeer2Peer,\n\t\t\t\t[=] { return std::make_unique<::Settings::CallsPeer2PeerPrivacyController>(); });\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/calls/p2p/never\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::CallsPeer2Peer,\n\t\t\t\t[=] { return std::make_unique<::Settings::CallsPeer2PeerPrivacyController>(); },\n\t\t\t\tu\"privacy/never\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/calls/p2p/always\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::CallsPeer2Peer,\n\t\t\t\t[=] { return std::make_unique<::Settings::CallsPeer2PeerPrivacyController>(); },\n\t\t\t\tu\"privacy/always\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/voice\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::Voices,\n\t\t\t\t[=] { return std::make_unique<::Settings::VoicesPrivacyController>(&ctx.controller->session()); });\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/voice/never\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::Voices,\n\t\t\t\t[=] { return std::make_unique<::Settings::VoicesPrivacyController>(&ctx.controller->session()); },\n\t\t\t\tu\"privacy/never\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/voice/always\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::Voices,\n\t\t\t\t[=] { return std::make_unique<::Settings::VoicesPrivacyController>(&ctx.controller->session()); },\n\t\t\t\tu\"privacy/always\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/messages\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tctx.controller->show(Box(EditMessagesPrivacyBox, ctx.controller, QString()));\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/messages/set-price\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tctx.controller->show(Box(\n\t\t\t\tEditMessagesPrivacyBox,\n\t\t\t\tctx.controller,\n\t\t\t\tu\"privacy/set-price\"_q));\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/messages/remove-fee\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tctx.controller->show(Box(\n\t\t\t\tEditMessagesPrivacyBox,\n\t\t\t\tctx.controller,\n\t\t\t\tu\"privacy/remove-fee\"_q));\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/invites\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::Invites,\n\t\t\t\t[=] { return std::make_unique<::Settings::GroupsInvitePrivacyController>(); });\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/invites/never\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::Invites,\n\t\t\t\t[=] { return std::make_unique<::Settings::GroupsInvitePrivacyController>(); },\n\t\t\t\tu\"privacy/never\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/invites/always\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPrivacyBox(\n\t\t\t\tctx,\n\t\t\t\tPrivacyKey::Invites,\n\t\t\t\t[=] { return std::make_unique<::Settings::GroupsInvitePrivacyController>(); },\n\t\t\t\tu\"privacy/always\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/self-destruct\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::PrivacySecurityId(),\n\t\t\tu\"privacy/self_destruct\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/data-settings/suggest-contacts\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::PrivacySecurityId(),\n\t\t\tu\"privacy/top_peers\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/data-settings/clear-payment-info\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::PrivacySecurityId(),\n\t\t\tu\"privacy/bots_payment\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"privacy/archive-and-mute\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::PrivacySecurityId(),\n\t\t\tu\"privacy/archive_and_mute\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"data/storage\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tLocalStorageBox::Show(ctx.controller);\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"data/storage/clear-cache\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tLocalStorageBox::Show(ctx.controller, u\"storage/clear-cache\"_q);\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"data/max-cache\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tLocalStorageBox::Show(ctx.controller, u\"storage/max-cache\"_q);\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"data/show-18-content\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::ChatId(),\n\t\t\tu\"chat/show-18-content\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"data/proxy\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tProxiesBoxController::Show(ctx.controller);\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"data/proxy/add-proxy\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tProxiesBoxController::Show(ctx.controller, u\"proxy/add-proxy\"_q);\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"data/proxy/share-list\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tProxiesBoxController::Show(ctx.controller, u\"proxy/share-list\"_q);\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance\"_q,\n\t\t.action = SettingsSection{ ::Settings::ChatId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"power-saving\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPowerSavingBox(ctx);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"power-saving/stickers\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPowerSavingBox(ctx, PowerSaving::kStickersPanel);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"power-saving/emoji\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPowerSavingBox(ctx, PowerSaving::kEmojiPanel);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"power-saving/effects\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPowerSavingBox(ctx, PowerSaving::kChatBackground);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/themes\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::ChatId(),\n\t\t\tu\"chat/themes\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/themes/edit\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::ChatId(),\n\t\t\tu\"chat/themes-edit\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/themes/create\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::ChatId(),\n\t\t\tu\"chat/themes-create\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/wallpapers\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::ChatId(),\n\t\t\tu\"chat/wallpapers\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/wallpapers/set\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::ChatId(),\n\t\t\tu\"chat/wallpapers-set\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/wallpapers/choose-photo\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::ChatId(),\n\t\t\tu\"chat/wallpapers-choose-photo\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/your-color\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"profile-color\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/your-color/profile\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"profile-color/profile\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/your-color/profile/add-icons\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"profile-color/profile/add-icons\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/your-color/profile/use-gift\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"profile-color/profile/use-gift\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/your-color/profile/reset\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"profile-color/profile/reset\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/your-color/name\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"profile-color/name\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/your-color/name/add-icons\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"profile-color/name/add-icons\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/your-color/name/use-gift\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"profile-color/name/use-gift\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/night-mode\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowMainMenuWithHighlight(ctx, u\"main-menu/night-mode\"_q);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/auto-night-mode\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::ChatId(),\n\t\t\tu\"chat/auto-night-mode\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/text-size\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::MainId(),\n\t\t\tu\"main/scale\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/animations\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"power-saving\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/stickers-and-emoji\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::ChatId(),\n\t\t\tu\"chat/stickers-emoji\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/stickers-and-emoji/edit\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tctx.controller->show(Box<StickersBox>(\n\t\t\t\tctx.controller->uiShow(),\n\t\t\t\tStickersBox::Section::Installed));\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/stickers-and-emoji/trending\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tctx.controller->show(Box<StickersBox>(\n\t\t\t\tctx.controller->uiShow(),\n\t\t\t\tStickersBox::Section::Featured));\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/stickers-and-emoji/archived\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tctx.controller->show(Box<StickersBox>(\n\t\t\t\tctx.controller->uiShow(),\n\t\t\t\tStickersBox::Section::Archived));\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/stickers-and-emoji/emoji\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tctx.controller->show(\n\t\t\t\tBox<Ui::Emoji::ManageSetsBox>(&ctx.controller->session()));\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/stickers-and-emoji/emoji/suggest\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::ChatId(),\n\t\t\tu\"chat/suggest-animated-emoji\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/stickers-and-emoji/emoji/quick-reaction\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::ChatId(),\n\t\t\tu\"chat/quick-reaction\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/stickers-and-emoji/emoji/quick-reaction/choose\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::ChatId(),\n\t\t\tu\"chat/quick-reaction-choose\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/stickers-and-emoji/suggest-by-emoji\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::ChatId(),\n\t\t\tu\"chat/suggest-by-emoji\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"appearance/stickers-and-emoji/emoji/large\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::ChatId(),\n\t\t\tu\"chat/large-emoji\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"language\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowLanguageBox(ctx);\n\t\t}},\n\t\t.requiresAuth = false,\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"language/show-button\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowLanguageBox(ctx, u\"language/show-button\"_q);\n\t\t}},\n\t\t.requiresAuth = false,\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"language/translate-chats\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowLanguageBox(ctx, u\"language/translate-chats\"_q);\n\t\t}},\n\t\t.requiresAuth = false,\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"language/do-not-translate\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowLanguageBox(ctx, u\"language/do-not-translate\"_q);\n\t\t}},\n\t\t.requiresAuth = false,\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"premium\"_q,\n\t\t.action = SettingsSection{ ::Settings::PremiumId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"stars\"_q,\n\t\t.action = SettingsSection{ ::Settings::CreditsId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"stars/top-up\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tstatic auto handler = ::Settings::BuyStarsHandler();\n\t\t\thandler.handler(ctx.controller->uiShow())();\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"stars/stats\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tconst auto self = ctx.controller->session().user();\n\t\t\tctx.controller->showSection(Info::BotEarn::Make(self));\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"stars/gift\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tUi::ShowGiftCreditsBox(ctx.controller, nullptr);\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"stars/earn\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tconst auto self = ctx.controller->session().user();\n\t\t\tif (Info::BotStarRef::Join::Allowed(self)) {\n\t\t\t\tctx.controller->showSection(Info::BotStarRef::Join::Make(self));\n\t\t\t}\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"ton\"_q,\n\t\t.action = SettingsSection{ ::Settings::CurrencyId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"business\"_q,\n\t\t.action = SettingsSection{ ::Settings::BusinessId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"business/do-not-hide-ads\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::BusinessId(),\n\t\t\tu\"business/sponsored\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"send-gift\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tUi::ChooseStarGiftRecipient(ctx.controller);\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"send-gift/self\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tUi::ShowStarGiftBox(ctx.controller, ctx.controller->session().user());\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"saved-messages\"_q,\n\t\t.action = CodeBlock{ ShowSavedMessages },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"calls\"_q,\n\t\t.action = SettingsSection{ ::Settings::CallsId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"calls/all\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tCalls::ShowCallsBox(ctx.controller);\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"faq\"_q,\n\t\t.action = CodeBlock{ ShowFaq },\n\t\t.requiresAuth = false,\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"ask-question\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\t::Settings::OpenAskQuestionConfirm(ctx.controller);\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"features\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tUrlClickHandler::Open(tr::lng_telegram_features_url(tr::now));\n\t\t\treturn Result::Handled;\n\t\t}},\n\t\t.requiresAuth = false,\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"search\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tconst auto self = ctx.controller->session().user();\n\t\t\tauto stack = std::vector<std::shared_ptr<Info::ContentMemento>>();\n\t\t\tstack.push_back(std::make_shared<Info::Settings::Memento>(\n\t\t\t\tself,\n\t\t\t\t::Settings::MainId()));\n\t\t\tstack.push_back(std::make_shared<Info::Settings::Memento>(\n\t\t\t\tself,\n\t\t\t\t::Settings::Search::Id()));\n\t\t\tctx.controller->showSection(\n\t\t\t\tstd::make_shared<Info::Memento>(std::move(stack)));\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"qr-code\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn HandleQrCode(ctx, false);\n\t\t}},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"qr-code/share\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn HandleQrCode(ctx, true);\n\t\t}},\n\t});\n\n\t// Edit profile deep links.\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"edit/set-photo\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::MainId(),\n\t\t\tu\"profile-photo\"_q,\n\t\t},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"edit/first-name\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowEditName(ctx, EditNameBox::Focus::FirstName);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"edit/last-name\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowEditName(ctx, EditNameBox::Focus::LastName);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"edit/bio\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::InformationId(),\n\t\t\tu\"edit/bio\"_q,\n\t\t},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"edit/birthday\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn OpenInternalUrl(ctx, u\"internal:edit_birthday\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"edit/change-number\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tctx.controller->show(\n\t\t\t\tUi::MakeInformBox(tr::lng_change_phone_error()));\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"edit/username\"_q,\n\t\t.action = CodeBlock{ ShowEditUsername },\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"edit/your-color\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowPeerColorBox(ctx, PeerColorTab::Profile);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"edit/channel\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::InformationId(),\n\t\t\tu\"edit/channel\"_q,\n\t\t},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"edit/add-account\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::InformationId(),\n\t\t\tu\"edit/add-account\"_q,\n\t\t},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"edit/log-out\"_q,\n\t\t.action = CodeBlock{ ShowLogOutMenu },\n\t});\n\n\t// Calls deep links.\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"calls/start-call\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\tif (!ctx.controller) {\n\t\t\t\treturn Result::NeedsAuth;\n\t\t\t}\n\t\t\tCalls::ShowCallsBox(ctx.controller, true);\n\t\t\treturn Result::Handled;\n\t\t}},\n\t});\n\n\t// Devices (sessions) deep links.\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"devices/terminate-sessions\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::SessionsId(),\n\t\t\tu\"sessions/terminate-all\"_q,\n\t\t},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"devices/auto-terminate\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::SessionsId(),\n\t\t\tu\"sessions/auto-terminate\"_q,\n\t\t},\n\t});\n\n\t// Folders deep links.\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"folders/create\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::FoldersId(),\n\t\t\tu\"folders/create\"_q,\n\t\t},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"folders/add-recommended\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::FoldersId(),\n\t\t\tu\"folders/add-recommended\"_q,\n\t\t},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"folders/show-tags\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::FoldersId(),\n\t\t\tu\"folders/show-tags\"_q,\n\t\t},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"folders/tab-view\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::FoldersId(),\n\t\t\tu\"folders/tab-view\"_q,\n\t\t},\n\t});\n\n\t// Notifications deep links.\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/accounts\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::NotificationsId(),\n\t\t\tu\"notifications/accounts\"_q,\n\t\t},\n\t});\n\n\t// Notification type deep links - Private Chats.\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/private-chats\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(ctx, Data::DefaultNotify::User);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/private-chats/edit\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(ctx, Data::DefaultNotify::User);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/private-chats/show\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(\n\t\t\t\tctx,\n\t\t\t\tData::DefaultNotify::User,\n\t\t\t\tu\"notifications/type/show\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/private-chats/sound\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(\n\t\t\t\tctx,\n\t\t\t\tData::DefaultNotify::User,\n\t\t\t\tu\"notifications/type/sound\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/private-chats/add-exception\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(\n\t\t\t\tctx,\n\t\t\t\tData::DefaultNotify::User,\n\t\t\t\tu\"notifications/type/add-exception\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/private-chats/delete-exceptions\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(\n\t\t\t\tctx,\n\t\t\t\tData::DefaultNotify::User,\n\t\t\t\tu\"notifications/type/delete-exceptions\"_q);\n\t\t}},\n\t});\n\n\t// Notification type deep links - Groups.\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/groups\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(ctx, Data::DefaultNotify::Group);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/groups/edit\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(ctx, Data::DefaultNotify::Group);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/groups/show\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(\n\t\t\t\tctx,\n\t\t\t\tData::DefaultNotify::Group,\n\t\t\t\tu\"notifications/type/show\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/groups/sound\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(\n\t\t\t\tctx,\n\t\t\t\tData::DefaultNotify::Group,\n\t\t\t\tu\"notifications/type/sound\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/groups/add-exception\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(\n\t\t\t\tctx,\n\t\t\t\tData::DefaultNotify::Group,\n\t\t\t\tu\"notifications/type/add-exception\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/groups/delete-exceptions\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(\n\t\t\t\tctx,\n\t\t\t\tData::DefaultNotify::Group,\n\t\t\t\tu\"notifications/type/delete-exceptions\"_q);\n\t\t}},\n\t});\n\n\t// Notification type deep links - Channels.\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/channels\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(ctx, Data::DefaultNotify::Broadcast);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/channels/edit\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(ctx, Data::DefaultNotify::Broadcast);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/channels/show\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(\n\t\t\t\tctx,\n\t\t\t\tData::DefaultNotify::Broadcast,\n\t\t\t\tu\"notifications/type/show\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/channels/sound\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(\n\t\t\t\tctx,\n\t\t\t\tData::DefaultNotify::Broadcast,\n\t\t\t\tu\"notifications/type/sound\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/channels/add-exception\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(\n\t\t\t\tctx,\n\t\t\t\tData::DefaultNotify::Broadcast,\n\t\t\t\tu\"notifications/type/add-exception\"_q);\n\t\t}},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/channels/delete-exceptions\"_q,\n\t\t.action = CodeBlock{ [](const Context &ctx) {\n\t\t\treturn ShowNotificationType(\n\t\t\t\tctx,\n\t\t\t\tData::DefaultNotify::Broadcast,\n\t\t\t\tu\"notifications/type/delete-exceptions\"_q);\n\t\t}},\n\t});\n\n\t// Other notification deep links.\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/include-muted-chats\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::NotificationsId(),\n\t\t\tu\"notifications/include-muted-chats\"_q,\n\t\t},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/count-unread-messages\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::NotificationsId(),\n\t\t\tu\"notifications/count-unread-messages\"_q,\n\t\t},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/new-contacts\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::NotificationsId(),\n\t\t\tu\"notifications/events/joined\"_q,\n\t\t},\n\t});\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"notifications/pinned-messages\"_q,\n\t\t.action = SettingsControl{\n\t\t\t::Settings::NotificationsId(),\n\t\t\tu\"notifications/events/pinned\"_q,\n\t\t},\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"themes\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"appearance/themes\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"themes/edit\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"appearance/themes/edit\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"themes/create\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"appearance/themes/create\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"change_number\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"edit/change-number\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"auto_delete\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"privacy/auto-delete\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"information\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"edit\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"edit_profile\"_q,\n\t\t.action = SettingsSection{ ::Settings::InformationId() },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"phone_privacy\"_q,\n\t\t.action = AliasTo{ u\"settings\"_q, u\"privacy/phone-number\"_q },\n\t});\n\n\trouter.add(u\"settings\"_q, {\n\t\t.path = u\"login_email\"_q,\n\t\t.action = CodeBlock{ ShowLoginEmail },\n\t});\n}\n\n} // namespace Core::DeepLinks\n"
  },
  {
    "path": "Telegram/SourceFiles/core/deep_links/deep_links_settings.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Core::DeepLinks {\n\nclass Router;\n\nvoid RegisterSettingsHandlers(Router &router);\n\n} // namespace Core::DeepLinks\n"
  },
  {
    "path": "Telegram/SourceFiles/core/deep_links/deep_links_types.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Core::DeepLinks {\n\nenum class Result {\n\tHandled,\n\tNotFound,\n\tNeedsAuth,\n\tUnsupported,\n};\n\nstruct Context {\n\tWindow::SessionController *controller = nullptr;\n\tQString section;\n\tQString path;\n\tQMap<QString, QString> params;\n};\n\nusing Handler = Fn<Result(const Context&)>;\n\nstruct SettingsSection {\n\tSettings::Type sectionId;\n};\n\nstruct SettingsControl {\n\tSettings::Type sectionId;\n\tQString controlId;\n};\n\nstruct CodeBlock {\n\tHandler handler;\n};\n\nstruct AliasTo {\n\tQString section;\n\tQString path;\n};\n\nusing Action = std::variant<\n\tSettingsSection,\n\tSettingsControl,\n\tCodeBlock,\n\tAliasTo>;\n\nstruct Entry {\n\tQString path;\n\tAction action;\n\tbool requiresAuth = true;\n\tbool skipActivation = false;\n};\n\n} // namespace Core::DeepLinks\n"
  },
  {
    "path": "Telegram/SourceFiles/core/file_location.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/file_location.h\"\n\n#include \"platform/platform_file_bookmark.h\"\n#include \"logs.h\"\n\n#include <QtCore/QFileInfo>\n\nnamespace Core {\nnamespace {\n\nconst auto kInMediaCacheLocation = u\"*media_cache*\"_q;\nconstexpr auto kMaxFileSize = 4000 * int64(1024 * 1024);\n\n} // namespace\n\nReadAccessEnabler::ReadAccessEnabler(const Platform::FileBookmark *bookmark)\n: _bookmark(bookmark)\n, _failed(_bookmark ? !_bookmark->enable() : false) {\n}\n\nReadAccessEnabler::ReadAccessEnabler(\n\tconst std::shared_ptr<Platform::FileBookmark> &bookmark)\n: _bookmark(bookmark.get())\n, _failed(_bookmark ? !_bookmark->enable() : false) {\n}\n\nReadAccessEnabler::~ReadAccessEnabler() {\n\tif (_bookmark && !_failed) _bookmark->disable();\n}\n\nFileLocation::FileLocation(const QString &name) : fname(name) {\n\tif (fname.isEmpty() || fname == kInMediaCacheLocation) {\n\t\tsize = 0;\n\t} else {\n\t\tsetBookmark(Platform::PathBookmark(name));\n\t\tresolveFromInfo(QFileInfo(name));\n\t}\n}\n\nFileLocation::FileLocation(const QFileInfo &info) : fname(info.filePath()) {\n\tif (fname.isEmpty()) {\n\t\tsize = 0;\n\t} else {\n\t\tsetBookmark(Platform::PathBookmark(fname));\n\t\tresolveFromInfo(info);\n\t}\n}\n\nvoid FileLocation::resolveFromInfo(const QFileInfo &info) {\n\tif (info.exists()) {\n\t\tconst auto s = info.size();\n\t\tif (s > kMaxFileSize) {\n\t\t\tfname = QString();\n\t\t\t_bookmark = nullptr;\n\t\t\tsize = 0;\n\t\t} else {\n\t\t\tmodified = info.lastModified();\n\t\t\tsize = s;\n\t\t}\n\t} else {\n\t\tfname = QString();\n\t\t_bookmark = nullptr;\n\t\tsize = 0;\n\t}\n}\n\nFileLocation FileLocation::InMediaCacheLocation() {\n\treturn FileLocation(kInMediaCacheLocation);\n}\n\nbool FileLocation::check() const {\n\tif (fname.isEmpty() || fname == kInMediaCacheLocation) {\n\t\treturn false;\n\t}\n\n\tReadAccessEnabler enabler(_bookmark);\n\tif (enabler.failed()) {\n\t\tconst_cast<FileLocation*>(this)->_bookmark = nullptr;\n\t}\n\n\tQFileInfo f(name());\n\tif (!f.isReadable()) return false;\n\n\tquint64 s = f.size();\n\tif (s > kMaxFileSize) {\n\t\tDEBUG_LOG((\"File location check: Wrong size %1\").arg(s));\n\t\treturn false;\n\t}\n\n\tif (s != size) {\n\t\tDEBUG_LOG((\"File location check: Wrong size %1 when should be %2\").arg(s).arg(size));\n\t\treturn false;\n\t}\n\tauto realModified = f.lastModified();\n\tif (realModified != modified) {\n\t\tDEBUG_LOG((\"File location check: Wrong last modified time %1 when should be %2\").arg(realModified.toMSecsSinceEpoch()).arg(modified.toMSecsSinceEpoch()));\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nconst QString &FileLocation::name() const {\n\treturn _bookmark ? _bookmark->name(fname) : fname;\n}\n\nQByteArray FileLocation::bookmark() const {\n\treturn _bookmark ? _bookmark->bookmark() : QByteArray();\n}\n\nbool FileLocation::inMediaCache() const {\n\treturn (fname == kInMediaCacheLocation);\n}\n\nvoid FileLocation::setBookmark(const QByteArray &bm) {\n\t_bookmark.reset(bm.isEmpty() ? nullptr : new Platform::FileBookmark(bm));\n}\n\nbool FileLocation::accessEnable() const {\n\treturn isEmpty() ? false : (_bookmark ? _bookmark->enable() : true);\n}\n\nvoid FileLocation::accessDisable() const {\n\treturn _bookmark ? _bookmark->disable() : (void)0;\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/file_location.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QDateTime>\n\nclass QFileInfo;\n\nnamespace Platform {\nclass FileBookmark;\n} // namespace Platform\n\nnamespace Core {\n\nclass ReadAccessEnabler {\npublic:\n\tReadAccessEnabler(const Platform::FileBookmark *bookmark);\n\tReadAccessEnabler(\n\t\tconst std::shared_ptr<Platform::FileBookmark> &bookmark);\n\tbool failed() const {\n\t\treturn _failed;\n\t}\n\t~ReadAccessEnabler();\n\nprivate:\n\tconst Platform::FileBookmark *_bookmark = nullptr;\n\tbool _failed;\n\n};\n\nclass FileLocation {\npublic:\n\tFileLocation() = default;\n\texplicit FileLocation(const QString &name);\n\texplicit FileLocation(const QFileInfo &info);\n\n\tstatic FileLocation InMediaCacheLocation();\n\n\t[[nodiscard]] bool check() const;\n\t[[nodiscard]] const QString &name() const;\n\tvoid setBookmark(const QByteArray &bookmark);\n\tQByteArray bookmark() const;\n\t[[nodiscard]] bool isEmpty() const {\n\t\treturn name().isEmpty();\n\t}\n\t[[nodiscard]] bool inMediaCache() const;\n\n\tbool accessEnable() const;\n\tvoid accessDisable() const;\n\n\tQString fname;\n\tQDateTime modified;\n\tqint64 size = 0;\n\nprivate:\n\tvoid resolveFromInfo(const QFileInfo &info);\n\n\tstd::shared_ptr<Platform::FileBookmark> _bookmark;\n\n};\n\ninline bool operator==(const FileLocation &a, const FileLocation &b) {\n\treturn (a.name() == b.name())\n\t\t&& (a.modified == b.modified)\n\t\t&& (a.size == b.size);\n}\n\ninline bool operator!=(const FileLocation &a, const FileLocation &b) {\n\treturn !(a == b);\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/file_utilities.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/file_utilities.h\"\n\n#include \"storage/localstorage.h\"\n#include \"storage/storage_account.h\"\n#include \"base/platform/base_platform_file_utilities.h\"\n#include \"platform/platform_file_utilities.h\"\n#include \"core/application.h\"\n#include \"base/unixtime.h\"\n#include \"ui/delayed_activation.h\"\n#include \"ui/chat/attach/attach_extensions.h\"\n#include \"main/main_session.h\"\n#include \"mainwindow.h\"\n\n#include <QtWidgets/QFileDialog>\n#include <QtCore/QCoreApplication>\n#include <QtCore/QStandardPaths>\n#include <QtGui/QDesktopServices>\n\nbool filedialogGetSaveFile(\n\t\tQPointer<QWidget> parent,\n\t\tQString &file,\n\t\tconst QString &caption,\n\t\tconst QString &filter,\n\t\tconst QString &initialPath) {\n\tQStringList files;\n\tQByteArray remoteContent;\n\tUi::PreventDelayedActivation();\n\tbool result = Platform::FileDialog::Get(\n\t\tparent,\n\t\tfiles,\n\t\tremoteContent,\n\t\tcaption,\n\t\tfilter,\n\t\tFileDialog::internal::Type::WriteFile,\n\t\tinitialPath);\n\tfile = files.isEmpty() ? QString() : files.at(0);\n\treturn result;\n}\n\nbool filedialogGetSaveFile(\n\t\tQString &file,\n\t\tconst QString &caption,\n\t\tconst QString &filter,\n\t\tconst QString &initialPath) {\n\treturn filedialogGetSaveFile(\n\t\tCore::App().getFileDialogParent(),\n\t\tfile,\n\t\tcaption,\n\t\tfilter,\n\t\tinitialPath);\n}\n\nQString filedialogDefaultName(\n\t\tconst QString &prefix,\n\t\tconst QString &extension,\n\t\tconst QString &path,\n\t\tbool skipExistance,\n\t\tTimeId fileTime) {\n\tauto directoryPath = path;\n\tif (directoryPath.isEmpty()) {\n\t\tif (cDialogLastPath().isEmpty()) {\n\t\t\tPlatform::FileDialog::InitLastPath();\n\t\t}\n\t\tdirectoryPath = cDialogLastPath();\n\t}\n\n\tQString base;\n\tif (fileTime) {\n\t\tconst auto date = base::unixtime::parse(fileTime);\n\t\tbase = prefix + date.toString(\"_yyyy-MM-dd_HH-mm-ss\");\n\t} else {\n\t\tstruct tm tm;\n\t\ttime_t t = time(NULL);\n\t\tmylocaltime(&tm, &t);\n\n\t\tconst auto zero = QChar('0');\n\t\tbase = prefix + u\"_%1-%2-%3_%4-%5-%6\"_q.arg(tm.tm_year + 1900).arg(tm.tm_mon + 1, 2, 10, zero).arg(tm.tm_mday, 2, 10, zero).arg(tm.tm_hour, 2, 10, zero).arg(tm.tm_min, 2, 10, zero).arg(tm.tm_sec, 2, 10, zero);\n\t}\n\n\tQString name;\n\tif (skipExistance) {\n\t\tname = base + extension;\n\t} else {\n\t\tQDir directory(directoryPath);\n\t\tconst auto dir = directory.absolutePath();\n\t\tconst auto nameBase = (dir.endsWith('/') ? dir : (dir + '/'))\n\t\t\t+ base;\n\t\tname = nameBase + extension;\n\t\tfor (int i = 0; QFileInfo::exists(name); ++i) {\n\t\t\tname = nameBase + u\" (%1)\"_q.arg(i + 2) + extension;\n\t\t}\n\t}\n\treturn name;\n}\n\nQString filedialogNextFilename(\n\t\tconst QString &name,\n\t\tconst QString &cur,\n\t\tconst QString &path) {\n\tQDir directory(path.isEmpty() ? cDialogLastPath() : path);\n\tint32 extIndex = name.lastIndexOf('.');\n\tQString prefix = name, extension;\n\tif (extIndex >= 0) {\n\t\textension = name.mid(extIndex);\n\t\tprefix = name.mid(0, extIndex);\n\t}\n\tconst auto dir = directory.absolutePath();\n\tconst auto nameBase = (dir.endsWith('/') ? dir : (dir + '/')) + prefix;\n\tauto result = nameBase + extension;\n\tfor (int i = 0; result.toLower() != cur.toLower() && QFileInfo::exists(result); ++i) {\n\t\tresult = nameBase + u\" (%1)\"_q.arg(i + 2) + extension;\n\t}\n\treturn result;\n}\n\nnamespace File {\n\nvoid OpenUrl(const QString &url) {\n\tcrl::on_main([=] {\n\t\tUi::PreventDelayedActivation();\n\t\tPlatform::File::UnsafeOpenUrl(url);\n\t});\n}\n\nvoid OpenEmailLink(const QString &email) {\n\tcrl::on_main([=] {\n\t\tUi::PreventDelayedActivation();\n\t\tPlatform::File::UnsafeOpenEmailLink(email);\n\t});\n}\n\nvoid OpenWith(const QString &filepath) {\n\tInvokeQueued(QCoreApplication::instance(), [=] {\n\t\tif (!Platform::File::UnsafeShowOpenWithDropdown(filepath)) {\n\t\t\tUi::PreventDelayedActivation();\n\t\t\tif (!Platform::File::UnsafeShowOpenWith(filepath)) {\n\t\t\t\tPlatform::File::UnsafeLaunch(filepath);\n\t\t\t}\n\t\t}\n\t});\n}\n\nvoid Launch(const QString &filepath) {\n\tcrl::on_main([=] {\n\t\tUi::PreventDelayedActivation();\n\t\tPlatform::File::UnsafeLaunch(filepath);\n\t});\n}\n\nvoid ShowInFolder(const QString &filepath) {\n\tcrl::on_main([=] {\n\t\tUi::PreventDelayedActivation();\n\t\tbase::Platform::ShowInFolder(filepath);\n\t});\n}\n\nQString DefaultDownloadPathFolder(not_null<Main::Session*> session) {\n#if OS_MAC_STORE\n\treturn u\"Telegram Lite\"_q;\n#else // OS_MAC_STORE\n\treturn session->supportMode() ? u\"Tsupport Desktop\"_q : AppName.utf16();\n#endif // OS_MAC_STORE\n}\n\nQString DefaultDownloadPath(not_null<Main::Session*> session) {\n\tif (!Core::App().canReadDefaultDownloadPath()) {\n\t\treturn session->local().tempDirectory();\n\t}\n\treturn QStandardPaths::writableLocation(\n\t\tQStandardPaths::DownloadLocation)\n\t\t+ '/'\n\t\t+ DefaultDownloadPathFolder(session)\n\t\t+ '/';\n}\n\nnamespace internal {\n\nvoid UnsafeOpenUrlDefault(const QString &url) {\n\tQDesktopServices::openUrl(url);\n}\n\nvoid UnsafeOpenEmailLinkDefault(const QString &email) {\n\tauto url = QUrl(u\"mailto:\"_q + email);\n\tQDesktopServices::openUrl(url);\n}\n\nvoid UnsafeLaunchDefault(const QString &filepath) {\n\tQDesktopServices::openUrl(QUrl::fromLocalFile(filepath));\n}\n\n} // namespace internal\n} // namespace File\n\nnamespace FileDialog {\n\nvoid GetOpenPath(\n\t\tQPointer<QWidget> parent,\n\t\tconst QString &caption,\n\t\tconst QString &filter,\n\t\tFn<void(OpenResult &&result)> callback,\n\t\tFn<void()> failed) {\n\tInvokeQueued(QCoreApplication::instance(), [=] {\n\t\tauto files = QStringList();\n\t\tauto remoteContent = QByteArray();\n\t\tUi::PreventDelayedActivation();\n\t\tconst auto success = Platform::FileDialog::Get(\n\t\t\tparent,\n\t\t\tfiles,\n\t\t\tremoteContent,\n\t\t\tcaption,\n\t\t\tfilter,\n\t\t\tFileDialog::internal::Type::ReadFile);\n\t\tif (success\n\t\t\t&& ((!files.isEmpty() && !files[0].isEmpty())\n\t\t\t\t|| !remoteContent.isEmpty())) {\n\t\t\tif (callback) {\n\t\t\t\tauto result = OpenResult();\n\t\t\t\tif (!files.isEmpty() && !files[0].isEmpty()) {\n\t\t\t\t\tresult.paths.push_back(files[0]);\n\t\t\t\t}\n\t\t\t\tresult.remoteContent = remoteContent;\n\t\t\t\tcallback(std::move(result));\n\t\t\t}\n\t\t} else if (failed) {\n\t\t\tfailed();\n\t\t}\n\t});\n}\n\nvoid GetOpenPaths(\n\t\tQPointer<QWidget> parent,\n\t\tconst QString &caption,\n\t\tconst QString &filter,\n\t\tFn<void(OpenResult &&result)> callback,\n\t\tFn<void()> failed) {\n\tInvokeQueued(QCoreApplication::instance(), [=] {\n\t\tauto files = QStringList();\n\t\tauto remoteContent = QByteArray();\n\t\tUi::PreventDelayedActivation();\n\t\tconst auto success = Platform::FileDialog::Get(\n\t\t\tparent,\n\t\t\tfiles,\n\t\t\tremoteContent,\n\t\t\tcaption,\n\t\t\tfilter,\n\t\t\tFileDialog::internal::Type::ReadFiles);\n\t\tif (success && (!files.isEmpty() || !remoteContent.isEmpty())) {\n\t\t\tif (callback) {\n\t\t\t\tauto result = OpenResult();\n\t\t\t\tresult.paths = files;\n\t\t\t\tresult.remoteContent = remoteContent;\n\t\t\t\tcallback(std::move(result));\n\t\t\t}\n\t\t} else if (failed) {\n\t\t\tfailed();\n\t\t}\n\t});\n}\n\nvoid GetWritePath(\n\t\tQPointer<QWidget> parent,\n\t\tconst QString &caption,\n\t\tconst QString &filter,\n\t\tconst QString &initialPath,\n\t\tFn<void(QString &&result)> callback,\n\t\tFn<void()> failed) {\n\tInvokeQueued(QCoreApplication::instance(), [=] {\n\t\tauto file = QString();\n\t\tif (filedialogGetSaveFile(parent, file, caption, filter, initialPath)) {\n\t\t\tif (callback) {\n\t\t\t\tcallback(std::move(file));\n\t\t\t}\n\t\t} else if (failed) {\n\t\t\tfailed();\n\t\t}\n\t});\n}\n\nvoid GetFolder(\n\t\tQPointer<QWidget> parent,\n\t\tconst QString &caption,\n\t\tconst QString &initialPath,\n\t\tFn<void(QString &&result)> callback,\n\t\tFn<void()> failed) {\n\tInvokeQueued(QCoreApplication::instance(), [=] {\n\t\tauto files = QStringList();\n\t\tauto remoteContent = QByteArray();\n\t\tUi::PreventDelayedActivation();\n\t\tconst auto success = Platform::FileDialog::Get(\n\t\t\tparent,\n\t\t\tfiles,\n\t\t\tremoteContent,\n\t\t\tcaption,\n\t\t\tQString(),\n\t\t\tFileDialog::internal::Type::ReadFolder,\n\t\t\tinitialPath);\n\t\tif (success && !files.isEmpty() && !files[0].isEmpty()) {\n\t\t\tif (callback) {\n\t\t\t\tcallback(std::move(files[0]));\n\t\t\t}\n\t\t} else if (failed) {\n\t\t\tfailed();\n\t\t}\n\t});\n}\n\nQString AllFilesFilter() {\n#ifdef Q_OS_WIN\n\treturn u\"All files (*.*)\"_q;\n#else // Q_OS_WIN\n\treturn u\"All files (*)\"_q;\n#endif // Q_OS_WIN\n}\n\nQString ImagesFilter() {\n\treturn u\"Image files (*\"_q + Ui::ImageExtensions().join(u\" *\"_q) + u\")\"_q;\n}\n\nQString AllOrImagesFilter() {\n\treturn AllFilesFilter() + u\";;\"_q + ImagesFilter();\n}\n\nQString ImagesOrAllFilter() {\n\treturn ImagesFilter() + u\";;\"_q + AllFilesFilter();\n}\n\nQString PhotoVideoFilesFilter() {\n\treturn u\"Image and Video Files (*\"_q + Ui::ImageExtensions().join(u\" *\"_q) + u\" *.mp4 *.mov *.m4v);;\"_q\n\t\t+ AllFilesFilter();\n}\n\nconst QString &Tmp() {\n\tstatic const auto tmp = u\"tmp\"_q;\n\treturn tmp;\n}\n\nnamespace internal {\n\nvoid InitLastPathDefault() {\n\tcSetDialogLastPath(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));\n}\n\nbool GetDefault(\n\t\tQPointer<QWidget> parent,\n\t\tQStringList &files,\n\t\tQByteArray &remoteContent,\n\t\tconst QString &caption,\n\t\tconst QString &filter,\n\t\tFileDialog::internal::Type type,\n\t\tQString startFile = QString()) {\n\tif (cDialogLastPath().isEmpty()) {\n\t\tPlatform::FileDialog::InitLastPath();\n\t}\n\n\tremoteContent = QByteArray();\n\tif (startFile.isEmpty() || startFile.at(0) != '/') {\n\t\tstartFile = cDialogLastPath() + '/' + startFile;\n\t}\n\tQString file;\n\n\tconst auto resolvedParent = (parent && parent->window()->isVisible())\n\t\t? parent->window()\n\t\t: Core::App().getFileDialogParent();\n\tCore::App().notifyFileDialogShown(true);\n\tconst auto guard = gsl::finally([] {\n\t\tCore::App().notifyFileDialogShown(false);\n\t});\n\tif (type == Type::ReadFiles) {\n\t\tfiles = QFileDialog::getOpenFileNames(resolvedParent, caption, startFile, filter);\n\t\tQString path = files.isEmpty() ? QString() : QFileInfo(files.back()).absoluteDir().absolutePath();\n\t\tif (!path.isEmpty() && path != cDialogLastPath()) {\n\t\t\tcSetDialogLastPath(path);\n\t\t\tLocal::writeSettings();\n\t\t}\n\t\treturn !files.isEmpty();\n\t} else if (type == Type::ReadFolder) {\n\t\tfile = QFileDialog::getExistingDirectory(resolvedParent, caption, startFile);\n\t} else if (type == Type::WriteFile) {\n\t\tfile = QFileDialog::getSaveFileName(resolvedParent, caption, startFile, filter);\n\t} else {\n\t\tfile = QFileDialog::getOpenFileName(resolvedParent, caption, startFile, filter);\n\t}\n\n\tif (file.isEmpty()) {\n\t\tfiles = QStringList();\n\t\treturn false;\n\t}\n\tif (type != Type::ReadFolder) {\n\t\t// Save last used directory for all queries except directory choosing.\n\t\tauto path = QFileInfo(file).absoluteDir().absolutePath();\n\t\tif (!path.isEmpty() && path != cDialogLastPath()) {\n\t\t\tcSetDialogLastPath(path);\n\t\t\tLocal::writeSettings();\n\t\t}\n\t}\n\tfiles = QStringList(file);\n\treturn true;\n}\n\n} // namespace internal\n} // namespace FileDialog\n"
  },
  {
    "path": "Telegram/SourceFiles/core/file_utilities.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\n// legacy\nbool filedialogGetSaveFile(\n\tQString &file,\n\tconst QString &caption,\n\tconst QString &filter,\n\tconst QString &initialPath);\n\nQString filedialogDefaultName(\n\tconst QString &prefix,\n\tconst QString &extension,\n\tconst QString &path = QString(),\n\tbool skipExistance = false,\n\tTimeId fileTime = TimeId(0));\nQString filedialogNextFilename(\n\tconst QString &name,\n\tconst QString &cur,\n\tconst QString &path = QString());\n\nnamespace File {\n\n// Those functions are async wrappers to Platform::File::Unsafe* calls.\nvoid OpenUrl(const QString &url);\nvoid OpenEmailLink(const QString &email);\nvoid OpenWith(const QString &filepath);\nvoid Launch(const QString &filepath);\nvoid ShowInFolder(const QString &filepath);\n\n[[nodiscard]] QString DefaultDownloadPathFolder(\n\tnot_null<Main::Session*> session);\n[[nodiscard]] QString DefaultDownloadPath(not_null<Main::Session*> session);\n\nnamespace internal {\n\ninline QString UrlToLocalDefault(const QUrl &url) {\n\treturn url.toLocalFile();\n}\n\nvoid UnsafeOpenUrlDefault(const QString &url);\nvoid UnsafeOpenEmailLinkDefault(const QString &email);\nvoid UnsafeLaunchDefault(const QString &filepath);\n\n} // namespace internal\n} // namespace File\n\nnamespace FileDialog {\n\nstruct OpenResult {\n\tQStringList paths;\n\tQByteArray remoteContent;\n};\nvoid GetOpenPath(\n\tQPointer<QWidget> parent,\n\tconst QString &caption,\n\tconst QString &filter,\n\tFn<void(OpenResult &&result)> callback,\n\tFn<void()> failed = Fn<void()>());\nvoid GetOpenPaths(\n\tQPointer<QWidget> parent,\n\tconst QString &caption,\n\tconst QString &filter,\n\tFn<void(OpenResult &&result)> callback,\n\tFn<void()> failed = Fn<void()>());\nvoid GetWritePath(\n\tQPointer<QWidget> parent,\n\tconst QString &caption,\n\tconst QString &filter,\n\tconst QString &initialPath,\n\tFn<void(QString &&result)> callback,\n\tFn<void()> failed = Fn<void()>());\nvoid GetFolder(\n\tQPointer<QWidget> parent,\n\tconst QString &caption,\n\tconst QString &initialPath,\n\tFn<void(QString &&result)> callback,\n\tFn<void()> failed = Fn<void()>());\n\n[[nodiscard]] QString AllFilesFilter();\n[[nodiscard]] QString ImagesFilter();\n[[nodiscard]] QString AllOrImagesFilter();\n[[nodiscard]] QString ImagesOrAllFilter();\n[[nodiscard]] QString PhotoVideoFilesFilter();\n[[nodiscard]] const QString &Tmp();\n\nnamespace internal {\n\nenum class Type {\n\tReadFile,\n\tReadFiles,\n\tReadFolder,\n\tWriteFile,\n};\n\nvoid InitLastPathDefault();\n\nbool GetDefault(\n\tQPointer<QWidget> parent,\n\tQStringList &files,\n\tQByteArray &remoteContent,\n\tconst QString &caption,\n\tconst QString &filter,\n\t::FileDialog::internal::Type type,\n\tQString startFile);\n\n} // namespace internal\n} // namespace FileDialog\n"
  },
  {
    "path": "Telegram/SourceFiles/core/fork_settings.cpp",
    "content": "/*\nAuthor: 23rd.\n*/\n#include \"core/fork_settings.h\"\n\n#include \"storage/serialize_common.h\"\n\nnamespace Core {\n\nnamespace {\n\nconstexpr auto kDefaultStickerSize = 256;\n\nbool StaticPrimaryUnmutedMessages = false;\n\n} // namespace\n\nForkSettings::ForkSettings() {\n}\n\nbool ForkSettings::PrimaryUnmutedMessages() {\n\treturn StaticPrimaryUnmutedMessages;\n}\n\nQByteArray ForkSettings::serialize() const {\n\n\tauto size = sizeof(qint32) * 4\n\t\t+ Serialize::stringSize(_uriScheme)\n\t\t+ Serialize::stringSize(_searchEngineUrl)\n\t\t+ sizeof(qint32) * 13\n\t\t+ sizeof(qint32) * 2\n\t\t+ Serialize::stringSize(_botsPlatforms);\n\n\tauto result = QByteArray();\n\tresult.reserve(size);\n\t{\n\t\tQDataStream stream(&result, QIODevice::WriteOnly);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\t\tstream\n\t\t\t<< qint32(_squareUserpics ? 1 : 0)\n\t\t\t<< qint32(_audioFade ? 1 : 0)\n\t\t\t<< qint32(_askUriScheme ? 1 : 0)\n\t\t\t<< qint32(_lastSeenInDialogs ? 1 : 0)\n\t\t\t<< _uriScheme\n\t\t\t<< _searchEngineUrl\n\t\t\t<< qint32(_searchEngine ? 1 : 0)\n\t\t\t<< qint32(_allRecentStickers ? 1 : 0)\n\t\t\t<< qint32(_customStickerSize)\n\t\t\t<< qint32(_useBlackTrayIcon ? 1 : 0)\n\t\t\t<< qint32(_useOriginalTrayIcon ? 1 : 0)\n\t\t\t<< qint32(_autoSubmitPasscode ? 1 : 0)\n\t\t\t<< qint32(_emojiPopupOnClick ? 1 : 0)\n\t\t\t<< qint32(_mentionByNameDisabled ? 1 : 0)\n\t\t\t<< qint32(_primaryUnmutedMessages ? 1 : 0)\n\t\t\t<< qint32(_addToMenuRememberMedia ? 1 : 0)\n\t\t\t<< qint32(_hideAllChatsTab ? 1 : 0)\n\t\t\t<< qint32(_globalSearchDisabled ? 1 : 0)\n\t\t\t<< qint32(_thirdButtonTopBar ? 1 : 0)\n\t\t\t<< qint32(_skipShareFromBot ? 1 : 0)\n\t\t\t<< qint32(_copyLoginCode ? 1 : 0)\n\t\t\t<< qint32(_additionalButtonsWebBot ? 1 : 0)\n\t\t\t<< _botsPlatforms\n\t\t\t<< qint32(_archivedStoriesAreHidden ? 1 : 0)\n\t\t\t;\n\t}\n\treturn result;\n}\n\nvoid ForkSettings::addFromSerialized(const QByteArray &serialized) {\n\tif (serialized.isEmpty()) {\n\t\treturn;\n\t}\n\n\tQDataStream stream(serialized);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\n\tqint32 squareUserpics = _squareUserpics;\n\tqint32 audioFade = _audioFade;\n\tqint32 askUriScheme = _askUriScheme;\n\tqint32 lastSeenInDialogs = _lastSeenInDialogs;\n\tQString uriScheme = _uriScheme;\n\tQString searchEngineUrl = _searchEngineUrl;\n\tqint32 searchEngine = _searchEngine;\n\tqint32 allRecentStickers = _allRecentStickers;\n\tqint32 customStickerSize = _customStickerSize;\n\tqint32 useBlackTrayIcon = _useBlackTrayIcon;\n\tqint32 useOriginalTrayIcon = _useOriginalTrayIcon;\n\tqint32 autoSubmitPasscode = _autoSubmitPasscode;\n\tqint32 emojiPopupOnClick = _emojiPopupOnClick;\n\tqint32 mentionByNameDisabled = _mentionByNameDisabled;\n\tqint32 primaryUnmutedMessages = _primaryUnmutedMessages;\n\tqint32 addToMenuRememberMedia = _addToMenuRememberMedia;\n\tqint32 hideAllChatsTab = _hideAllChatsTab;\n\tqint32 globalSearchDisabled = _globalSearchDisabled;\n\tqint32 thirdButtonTopBar = _thirdButtonTopBar;\n\tqint32 skipShareFromBot = _skipShareFromBot;\n\tqint32 copyLoginCode = _copyLoginCode;\n\tqint32 additionalButtonsWebBot = _additionalButtonsWebBot;\n\tqint32 archivedStoriesAreHidden = _archivedStoriesAreHidden;\n\tQString botsPlatforms = _botsPlatforms;\n\n\tif (!stream.atEnd()) {\n\t\tstream\n\t\t\t>> squareUserpics\n\t\t\t>> audioFade\n\t\t\t>> askUriScheme\n\t\t\t>> lastSeenInDialogs\n\t\t\t>> uriScheme\n\t\t\t>> searchEngineUrl\n\t\t\t>> searchEngine\n\t\t\t>> allRecentStickers\n\t\t\t>> customStickerSize\n\t\t\t>> useBlackTrayIcon\n\t\t\t>> useOriginalTrayIcon\n\t\t\t>> autoSubmitPasscode\n\t\t\t>> emojiPopupOnClick\n\t\t\t>> mentionByNameDisabled\n\t\t\t>> primaryUnmutedMessages\n\t\t\t;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> addToMenuRememberMedia;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> hideAllChatsTab;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> globalSearchDisabled;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> thirdButtonTopBar;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> skipShareFromBot;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> copyLoginCode;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> additionalButtonsWebBot;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> botsPlatforms;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> archivedStoriesAreHidden;\n\t}\n\tif (stream.status() != QDataStream::Ok) {\n\t\tLOG((\"App Error: \"\n\t\t\t\"Bad data for Core::ForkSettings::constructFromSerialized()\"));\n\t\treturn;\n\t}\n\t_squareUserpics = (squareUserpics == 1);\n\t_audioFade = (audioFade == 1);\n\t_askUriScheme = (askUriScheme == 1);\n\t_lastSeenInDialogs = (lastSeenInDialogs == 1);\n\t_uriScheme = std::move(uriScheme);\n\t_searchEngineUrl = std::move(searchEngineUrl);\n\t_searchEngine = (searchEngine == 1);\n\t_allRecentStickers = (allRecentStickers == 1);\n\t_customStickerSize = customStickerSize\n\t\t? std::clamp(customStickerSize, 50, kDefaultStickerSize)\n\t\t: kDefaultStickerSize;\n\t_useBlackTrayIcon = (useBlackTrayIcon == 1);\n\t_useOriginalTrayIcon = (useOriginalTrayIcon == 1);\n\t_autoSubmitPasscode = (autoSubmitPasscode == 1);\n\t_emojiPopupOnClick = (emojiPopupOnClick == 1);\n\t_mentionByNameDisabled = (mentionByNameDisabled == 1);\n\tsetPrimaryUnmutedMessages(primaryUnmutedMessages == 1);\n\t_addToMenuRememberMedia = (addToMenuRememberMedia == 1);\n\t_hideAllChatsTab = (hideAllChatsTab == 1);\n\t_globalSearchDisabled = (globalSearchDisabled == 1);\n\t_thirdButtonTopBar = (thirdButtonTopBar == 1);\n\t_skipShareFromBot = (skipShareFromBot == 1);\n\t_copyLoginCode = (copyLoginCode == 1);\n\t_additionalButtonsWebBot = (additionalButtonsWebBot == 1);\n\t_archivedStoriesAreHidden = (archivedStoriesAreHidden == 1);\n\t_botsPlatforms = std::move(botsPlatforms);\n}\n\nvoid ForkSettings::resetOnLastLogout() {\n\t_squareUserpics = false;\n\t_audioFade = true;\n\t_askUriScheme = false;\n\t_uriScheme = QString();\n\t_lastSeenInDialogs = false;\n\t_searchEngineUrl = qsl(\"https://dgg.gg/%q\");\n\t_searchEngine = false;\n\t_allRecentStickers = true;\n\t_customStickerSize = kDefaultStickerSize;\n\t_useOriginalTrayIcon = false;\n\t_autoSubmitPasscode = false;\n\t_emojiPopupOnClick = false;\n\t_mentionByNameDisabled = false;\n\tsetPrimaryUnmutedMessages(false);\n\t_addToMenuRememberMedia = false;\n\t_hideAllChatsTab = false;\n\t_globalSearchDisabled = false;\n\t_thirdButtonTopBar = false;\n\t_skipShareFromBot = false;\n\t_copyLoginCode = false;\n\t_additionalButtonsWebBot = false;\n\t_archivedStoriesAreHidden = false;\n\t_botsPlatforms = QString();\n}\n\n[[nodiscard]] bool ForkSettings::primaryUnmutedMessages() const {\n\treturn _primaryUnmutedMessages;\n}\nvoid ForkSettings::setPrimaryUnmutedMessages(bool newValue) {\n\tStaticPrimaryUnmutedMessages = newValue;\n\t_primaryUnmutedMessages = newValue;\n}\n\n[[nodiscard]] bool ForkSettings::addToMenuRememberMedia() const {\n\treturn _addToMenuRememberMedia;\n}\nvoid ForkSettings::setAddToMenuRememberMedia(bool newValue) {\n\t_addToMenuRememberMedia = newValue;\n}\n\n[[nodiscard]] bool ForkSettings::hideAllChatsTab() const {\n\treturn _hideAllChatsTab;\n}\nvoid ForkSettings::setHideAllChatsTab(bool newValue) {\n\t_hideAllChatsTab = newValue;\n}\n\n[[nodiscard]] bool ForkSettings::globalSearchDisabled() const {\n\treturn _globalSearchDisabled;\n}\nvoid ForkSettings::setGlobalSearchDisabled(bool newValue) {\n\t_globalSearchDisabled = newValue;\n}\n\n[[nodiscard]] bool ForkSettings::thirdButtonTopBar() const {\n\treturn _thirdButtonTopBar;\n}\nvoid ForkSettings::setThirdButtonTopBar(bool newValue) {\n\t_thirdButtonTopBar = newValue;\n}\n\n[[nodiscard]] bool ForkSettings::skipShareFromBot() const {\n\treturn _skipShareFromBot;\n}\nvoid ForkSettings::setSkipShareFromBot(bool newValue) {\n\t_skipShareFromBot = newValue;\n}\n\n[[nodiscard]] bool ForkSettings::copyLoginCode() const {\n\treturn _copyLoginCode;\n}\nvoid ForkSettings::setCopyLoginCode(bool newValue) {\n\t_copyLoginCode = newValue;\n}\n\n[[nodiscard]] bool ForkSettings::additionalButtonsWebBot() const {\n\treturn _additionalButtonsWebBot;\n}\nvoid ForkSettings::setAdditionalButtonsWebBot(bool newValue) {\n\t_additionalButtonsWebBot = newValue;\n}\n\n[[nodiscard]] QString ForkSettings::botsPlatforms() const {\n\treturn _botsPlatforms;\n}\nvoid ForkSettings::setBotsPlatforms(QString newValue) {\n\t_botsPlatforms = std::move(newValue);\n}\n\n[[nodiscard]] bool ForkSettings::archivedStoriesAreHidden() const {\n\treturn _archivedStoriesAreHidden;\n}\nvoid ForkSettings::setArchivedStoriesAreHidden(bool newValue) {\n\t_archivedStoriesAreHidden = newValue;\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/fork_settings.h",
    "content": "/*\nAuthor: 23rd.\n*/\n#pragma once\n\nnamespace Core {\n\nclass ForkSettings final {\npublic:\n\tForkSettings();\n\n\t[[nodiscard]] static bool PrimaryUnmutedMessages();\n\n\t[[nodiscard]] QByteArray serialize() const;\n\tvoid addFromSerialized(const QByteArray &serialized);\n\n\tvoid resetOnLastLogout();\n\n\t[[nodiscard]] bool squareUserpics() const {\n\t\treturn _squareUserpics;\n\t}\n\tvoid setSquareUserpics(bool newValue) {\n\t\t_squareUserpics = newValue;\n\t}\n\t[[nodiscard]] bool audioFade() const {\n\t\treturn _audioFade;\n\t}\n\tvoid setAudioFade(bool newValue) {\n\t\t_audioFade = newValue;\n\t}\n\t[[nodiscard]] bool askUriScheme() const {\n\t\treturn _askUriScheme;\n\t}\n\tvoid setAskUriScheme(bool newValue) {\n\t\t_askUriScheme = newValue;\n\t}\n\t[[nodiscard]] QString uriScheme() const {\n\t\treturn _uriScheme;\n\t}\n\tvoid setUriScheme(QString newValue) {\n\t\t_uriScheme = newValue;\n\t}\n\t[[nodiscard]] bool lastSeenInDialogs() const {\n\t\treturn _lastSeenInDialogs;\n\t}\n\tvoid setLastSeenInDialogs(bool newValue) {\n\t\t_lastSeenInDialogs = newValue;\n\t}\n\t[[nodiscard]] QString searchEngineUrl() const {\n\t\treturn _searchEngineUrl;\n\t}\n\tvoid setSearchEngineUrl(QString newValue) {\n\t\t_searchEngineUrl = newValue;\n\t}\n\t[[nodiscard]] bool searchEngine() const {\n\t\treturn _searchEngine;\n\t}\n\tvoid setSearchEngine(bool newValue) {\n\t\t_searchEngine = newValue;\n\t}\n\t[[nodiscard]] bool allRecentStickers() const {\n\t\treturn _allRecentStickers;\n\t}\n\tvoid setAllRecentStickers(bool newValue) {\n\t\t_allRecentStickers = newValue;\n\t}\n\t[[nodiscard]] int customStickerSize() const {\n\t\treturn _customStickerSize;\n\t}\n\tvoid setCustomStickerSize(int newValue) {\n\t\t_customStickerSize = newValue;\n\t}\n\t[[nodiscard]] bool useBlackTrayIcon() const {\n\t\treturn _useBlackTrayIcon;\n\t}\n\tvoid setUseBlackTrayIcon(bool newValue) {\n\t\t_useBlackTrayIcon = newValue;\n\t}\n\t[[nodiscard]] bool useOriginalTrayIcon() const {\n\t\treturn _useOriginalTrayIcon;\n\t}\n\tvoid setUseOriginalTrayIcon(bool newValue) {\n\t\t_useOriginalTrayIcon = newValue;\n\t}\n\t[[nodiscard]] bool autoSubmitPasscode() const {\n\t\treturn _autoSubmitPasscode;\n\t}\n\tvoid setAutoSubmitPasscode(bool newValue) {\n\t\t_autoSubmitPasscode = newValue;\n\t}\n\t[[nodiscard]] bool emojiPopupOnClick() const {\n\t\treturn _emojiPopupOnClick;\n\t}\n\tvoid setEmojiPopupOnClick(bool newValue) {\n\t\t_emojiPopupOnClick = newValue;\n\t}\n\t[[nodiscard]] bool mentionByNameDisabled() const {\n\t\treturn _mentionByNameDisabled;\n\t}\n\tvoid setMentionByNameDisabled(bool newValue) {\n\t\t_mentionByNameDisabled = newValue;\n\t}\n\t[[nodiscard]] bool primaryUnmutedMessages() const;\n\tvoid setPrimaryUnmutedMessages(bool newValue);\n\t[[nodiscard]] bool addToMenuRememberMedia() const;\n\tvoid setAddToMenuRememberMedia(bool newValue);\n\n\t[[nodiscard]] bool hideAllChatsTab() const;\n\tvoid setHideAllChatsTab(bool newValue);\n\n\t[[nodiscard]] bool globalSearchDisabled() const;\n\tvoid setGlobalSearchDisabled(bool newValue);\n\n\t[[nodiscard]] bool thirdButtonTopBar() const;\n\tvoid setThirdButtonTopBar(bool newValue);\n\n\t[[nodiscard]] bool skipShareFromBot() const;\n\tvoid setSkipShareFromBot(bool newValue);\n\n\t[[nodiscard]] bool additionalButtonsWebBot() const;\n\tvoid setAdditionalButtonsWebBot(bool newValue);\n\n\t[[nodiscard]] bool copyLoginCode() const;\n\tvoid setCopyLoginCode(bool);\n\n\t// \"bot_id1:platform1|bot_id2:platform2\"\n\t[[nodiscard]] QString botsPlatforms() const;\n\tvoid setBotsPlatforms(QString);\n\n\t[[nodiscard]] bool archivedStoriesAreHidden() const;\n\tvoid setArchivedStoriesAreHidden(bool newValue);\n\nprivate:\n\tbool _squareUserpics = false;\n\tbool _audioFade = true;\n\tbool _askUriScheme = false;\n\tbool _lastSeenInDialogs = false;\n\tQString _uriScheme;\n\tQString _searchEngineUrl = u\"https://dgg.gg/%q\"_q;\n\tbool _searchEngine = false;\n\tbool _allRecentStickers = true;\n\tint _customStickerSize = 256;\n\tbool _useBlackTrayIcon = false;\n\tbool _useOriginalTrayIcon = false;\n\tbool _autoSubmitPasscode = false;\n\tbool _emojiPopupOnClick = false;\n\tbool _mentionByNameDisabled = false;\n\tbool _primaryUnmutedMessages = false;\n\tbool _addToMenuRememberMedia = false;\n\tbool _hideAllChatsTab = false;\n\tbool _globalSearchDisabled = false;\n\tbool _thirdButtonTopBar = false;\n\tbool _skipShareFromBot = false;\n\tbool _copyLoginCode = false;\n\tbool _additionalButtonsWebBot = false;\n\tQString _botsPlatforms;\n\tbool _archivedStoriesAreHidden = false;\n\n};\n\n} // namespace Core\n\n"
  },
  {
    "path": "Telegram/SourceFiles/core/launcher.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/launcher.h\"\n\n#include \"platform/platform_launcher.h\"\n#include \"platform/platform_specific.h\"\n#include \"base/options.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/platform/base_platform_file_utilities.h\"\n#include \"ui/main_queue_processor.h\"\n#include \"core/crash_reports.h\"\n#include \"core/update_checker.h\"\n#include \"core/sandbox.h\"\n#include \"base/concurrent_timer.h\"\n#include \"base/options.h\"\n\n#include <QtCore/QLoggingCategory>\n#include <QtCore/QStandardPaths>\n#include <QtCore/QLibraryInfo>\n\nnamespace Core {\nnamespace {\n\nuint64 InstallationTag = 0;\n\nbase::options::toggle OptionHighDpiDownscale({\n\t.id = kOptionHighDpiDownscale,\n\t.name = \"High DPI downscale\",\n\t.description = \"Follow system interface scale settings exactly\"\n\t\t\" (another approach, likely better quality).\",\n\t.scope = [] {\n\t\treturn !Platform::IsMac()\n\t\t\t&& QLibraryInfo::version() >= QVersionNumber(6, 4);\n\t},\n\t.restartRequired = true,\n});\n\nbase::options::toggle OptionUseOpenGLRenderer({\n\t.id = kOptionUseOpenGLRenderer,\n\t.name = \"Use OpenGL renderer\",\n\t.description = \"Use the legacy OpenGL rendering backend instead of\"\n\t\t\" the default Metal/Direct3D/Vulkan (QRhi) backend.\",\n\t.scope = [] {\n\t\treturn QLibraryInfo::version() >= QVersionNumber(6, 7);\n\t},\n\t.restartRequired = true,\n});\n\nbase::options::toggle OptionFreeType({\n\t.id = kOptionFreeType,\n\t.name = \"FreeType font engine\",\n\t.description = \"Use the font engine from Linux instead of the system one.\",\n\t.scope = base::options::windows | base::options::macos,\n\t.restartRequired = true,\n});\n\nclass FilteredCommandLineArguments {\npublic:\n\tFilteredCommandLineArguments(int argc, char **argv);\n\n\tint &count();\n\tchar **values();\n\nprivate:\n\tstatic constexpr auto kForwardArgumentCount = 1;\n\n\tint _count = 0;\n\tstd::vector<QByteArray> _owned;\n\tstd::vector<char*> _arguments;\n\n\tvoid pushArgument(const char *text);\n\n};\n\nFilteredCommandLineArguments::FilteredCommandLineArguments(\n\tint argc,\n\tchar **argv) {\n\t// For now just pass only the first argument, the executable path.\n\tfor (auto i = 0; i != kForwardArgumentCount; ++i) {\n\t\tpushArgument(argv[i]);\n\t}\n\n#if defined Q_OS_WIN || defined Q_OS_MAC\n\tif (OptionFreeType.value() || OptionHighDpiDownscale.value()) {\n\t\tpushArgument(\"-platform\");\n#ifdef Q_OS_WIN\n\t\tpushArgument(\"windows:fontengine=freetype\");\n#else // Q_OS_WIN\n\t\tpushArgument(\"cocoa:fontengine=freetype\");\n#endif // !Q_OS_WIN\n\t}\n#endif // Q_OS_WIN || Q_OS_MAC\n\n\tpushArgument(nullptr);\n}\n\nint &FilteredCommandLineArguments::count() {\n\t_count = _arguments.size() - 1;\n\treturn _count;\n}\n\nchar **FilteredCommandLineArguments::values() {\n\treturn _arguments.data();\n}\n\nvoid FilteredCommandLineArguments::pushArgument(const char *text) {\n\t_owned.emplace_back(text);\n\t_arguments.push_back(_owned.back().data());\n}\n\nQString DebugModeSettingPath() {\n\treturn cWorkingDir() + u\"tdata/withdebug\"_q;\n}\n\nvoid WriteDebugModeSetting() {\n\tauto file = QFile(DebugModeSettingPath());\n\tif (file.open(QIODevice::WriteOnly)) {\n\t\tfile.write(Logs::DebugEnabled() ? \"1\" : \"0\");\n\t}\n}\n\nvoid ComputeDebugMode() {\n\tLogs::SetDebugEnabled(cAlphaVersion() != 0);\n\tconst auto debugModeSettingPath = DebugModeSettingPath();\n\tauto file = QFile(debugModeSettingPath);\n\tif (file.exists() && file.open(QIODevice::ReadOnly)) {\n\t\tLogs::SetDebugEnabled(file.read(1) != \"0\");\n#if defined _DEBUG && !defined Q_OS_MAC\n\t} else {\n\t\tLogs::SetDebugEnabled(true);\n#endif\n\t}\n\tif (cDebugMode()) {\n\t\tLogs::SetDebugEnabled(true);\n\t}\n\tif (Logs::DebugEnabled()) {\n\t\tQLoggingCategory::setFilterRules(\"qt.qpa.gl.debug=true\");\n\t}\n}\n\nvoid ComputeExternalUpdater() {\n\tauto locations = QStandardPaths::standardLocations(\n\t\tQStandardPaths::AppDataLocation);\n\tif (locations.isEmpty()) {\n\t\tlocations << QString();\n\t}\n\tlocations[0] = QDir::cleanPath(cWorkingDir());\n\tlocations << QDir::cleanPath(cExeDir());\n\tfor (const auto &location : locations) {\n\t\tconst auto dir = location + u\"/externalupdater.d\"_q;\n\t\tfor (const auto &info : QDir(dir).entryInfoList(QDir::Files)) {\n\t\t\tQFile file(info.absoluteFilePath());\n\t\t\tif (file.open(QIODevice::ReadOnly)) {\n\t\t\t\tQTextStream fileStream(&file);\n\t\t\t\twhile (!fileStream.atEnd()) {\n\t\t\t\t\tconst auto path = fileStream.readLine();\n\t\t\t\t\tif (path == (cExeDir() + cExeName())) {\n\t\t\t\t\t\tSetUpdaterDisabledAtStartup();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nQString InstallBetaVersionsSettingPath() {\n\treturn cWorkingDir() + u\"tdata/devversion\"_q;\n}\n\nvoid WriteInstallBetaVersionsSetting() {\n\tQFile f(InstallBetaVersionsSettingPath());\n\tif (f.open(QIODevice::WriteOnly)) {\n\t\tf.write(cInstallBetaVersion() ? \"1\" : \"0\");\n\t}\n}\n\nvoid ComputeInstallBetaVersions() {\n\tconst auto installBetaSettingPath = InstallBetaVersionsSettingPath();\n\tif (cAlphaVersion()) {\n\t\tcSetInstallBetaVersion(false);\n\t} else if (QFile::exists(installBetaSettingPath)) {\n\t\tQFile f(installBetaSettingPath);\n\t\tif (f.open(QIODevice::ReadOnly)) {\n\t\t\tcSetInstallBetaVersion(f.read(1) != \"0\");\n\t\t}\n\t} else if (AppBetaVersion) {\n\t\tWriteInstallBetaVersionsSetting();\n\t}\n}\n\nvoid ComputeInstallationTag() {\n\tInstallationTag = 0;\n\tauto file = QFile(cWorkingDir() + u\"tdata/usertag\"_q);\n\tif (file.open(QIODevice::ReadOnly)) {\n\t\tconst auto result = file.read(\n\t\t\treinterpret_cast<char*>(&InstallationTag),\n\t\t\tsizeof(uint64));\n\t\tif (result != sizeof(uint64)) {\n\t\t\tInstallationTag = 0;\n\t\t}\n\t\tfile.close();\n\t}\n\tif (!InstallationTag) {\n\t\tauto generator = std::mt19937(std::random_device()());\n\t\tauto distribution = std::uniform_int_distribution<uint64>();\n\t\tdo {\n\t\t\tInstallationTag = distribution(generator);\n\t\t} while (!InstallationTag);\n\n\t\tif (file.open(QIODevice::WriteOnly)) {\n\t\t\tfile.write(\n\t\t\t\treinterpret_cast<char*>(&InstallationTag),\n\t\t\t\tsizeof(uint64));\n\t\t\tfile.close();\n\t\t}\n\t}\n}\n\nbool MoveLegacyAlphaFolder(const QString &folder, const QString &file) {\n\tconst auto was = cExeDir() + folder;\n\tconst auto now = cExeDir() + u\"TelegramForcePortable\"_q;\n\tif (QDir(was).exists() && !QDir(now).exists()) {\n\t\tconst auto oldFile = was + \"/tdata/\" + file;\n\t\tconst auto newFile = was + \"/tdata/alpha\";\n\t\tif (QFile::exists(oldFile) && !QFile::exists(newFile)) {\n\t\t\tif (!QFile(oldFile).copy(newFile)) {\n\t\t\t\tLOG((\"FATAL: Could not copy '%1' to '%2'\").arg(\n\t\t\t\t\toldFile,\n\t\t\t\t\tnewFile));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\tif (!QDir().rename(was, now)) {\n\t\t\tLOG((\"FATAL: Could not rename '%1' to '%2'\").arg(was, now));\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nbool MoveLegacyAlphaFolder() {\n\tif (!MoveLegacyAlphaFolder(u\"TelegramAlpha_data\"_q, u\"alpha\"_q)\n\t\t|| !MoveLegacyAlphaFolder(u\"TelegramBeta_data\"_q, u\"beta\"_q)) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nbool CheckPortableVersionFolder() {\n\tif (!MoveLegacyAlphaFolder()) {\n\t\treturn false;\n\t}\n\n\tconst auto portable = cExeDir() + u\"TelegramForcePortable\"_q;\n\tQFile key(portable + u\"/tdata/alpha\"_q);\n\tif (cAlphaVersion()) {\n\t\tAssert(*AlphaPrivateKey != 0);\n\n\t\tcForceWorkingDir(portable);\n\t\tQDir().mkpath(cWorkingDir() + u\"tdata\"_q);\n\t\tcSetAlphaPrivateKey(QByteArray(AlphaPrivateKey));\n\t\tif (!key.open(QIODevice::WriteOnly)) {\n\t\t\tLOG((\"FATAL: Could not open '%1' for writing private key!\"\n\t\t\t\t).arg(key.fileName()));\n\t\t\treturn false;\n\t\t}\n\t\tQDataStream dataStream(&key);\n\t\tdataStream.setVersion(QDataStream::Qt_5_3);\n\t\tdataStream << quint64(cRealAlphaVersion()) << cAlphaPrivateKey();\n\t\treturn true;\n\t}\n\tif (!QDir(portable).exists()) {\n\t\treturn true;\n\t}\n\tcForceWorkingDir(portable);\n\tif (!key.exists()) {\n\t\treturn true;\n\t}\n\n\tif (!key.open(QIODevice::ReadOnly)) {\n\t\tLOG((\"FATAL: could not open '%1' for reading private key. \"\n\t\t\t\"Delete it or reinstall private alpha version.\"\n\t\t\t).arg(key.fileName()));\n\t\treturn false;\n\t}\n\tQDataStream dataStream(&key);\n\tdataStream.setVersion(QDataStream::Qt_5_3);\n\n\tquint64 v;\n\tQByteArray k;\n\tdataStream >> v >> k;\n\tif (dataStream.status() != QDataStream::Ok || k.isEmpty()) {\n\t\tLOG((\"FATAL: '%1' is corrupted. \"\n\t\t\t\"Delete it or reinstall private alpha version.\"\n\t\t\t).arg(key.fileName()));\n\t\treturn false;\n\t}\n\tcSetAlphaVersion(AppVersion * 1000ULL);\n\tcSetAlphaPrivateKey(k);\n\tcSetRealAlphaVersion(v);\n\treturn true;\n}\n\nbase::options::toggle OptionFractionalScalingEnabled({\n\t.id = kOptionFractionalScalingEnabled,\n\t.name = \"Enable precise High DPI scaling\",\n\t.description = \"Follow system interface scale settings exactly.\",\n\t.scope = base::options::windows | base::options::linux,\n\t.restartRequired = true,\n});\n\n} // namespace\n\nconst char kOptionFractionalScalingEnabled[] = \"fractional-scaling-enabled\";\nconst char kOptionHighDpiDownscale[] = \"high-dpi-downscale\";\nconst char kOptionFreeType[] = \"freetype\";\nconst char kOptionUseOpenGLRenderer[] = \"use-opengl-renderer\";\n\nLauncher *Launcher::InstanceSetter::Instance = nullptr;\n\nstd::unique_ptr<Launcher> Launcher::Create(int argc, char *argv[]) {\n\treturn std::make_unique<Platform::Launcher>(argc, argv);\n}\n\nLauncher::Launcher(int argc, char *argv[])\n: _argc(argc)\n, _argv(argv)\n, _arguments(readArguments(_argc, _argv))\n, _baseIntegration(_argc, _argv)\n, _initialWorkingDir(QDir::currentPath() + '/') {\n\tcrl::toggle_fp_exceptions(true);\n\n\tbase::Integration::Set(&_baseIntegration);\n}\n\nLauncher::~Launcher() {\n\tInstanceSetter::Instance = nullptr;\n}\n\nvoid Launcher::init() {\n\tprepareSettings();\n\tinitQtMessageLogging();\n\n\tQApplication::setApplicationName(u\"ForkgramDesktop\"_q);\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n\t// fallback session management is useless for tdesktop since it doesn't have\n\t// any \"are you sure you want to close this window?\" dialogs\n\t// but it produces bugs like https://github.com/telegramdesktop/tdesktop/issues/5022\n\t// and https://github.com/telegramdesktop/tdesktop/issues/7549\n\t// and https://github.com/telegramdesktop/tdesktop/issues/948\n\t// more info: https://doc.qt.io/qt-5/qguiapplication.html#isFallbackSessionManagementEnabled\n\tQApplication::setFallbackSessionManagementEnabled(false);\n#endif // Qt < 6.0.0\n\n\tinitHook();\n}\n\nvoid Launcher::initHighDpi() {\n#if QT_VERSION < QT_VERSION_CHECK(6, 2, 0)\n\tqputenv(\"QT_DPI_ADJUSTMENT_POLICY\", \"AdjustDpi\");\n#endif // Qt < 6.2.0\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n\tQApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true);\n#endif // Qt < 6.0.0\n\n\tif (OptionHighDpiDownscale.value()) {\n\t\tqputenv(\"QT_WIDGETS_HIGHDPI_DOWNSCALE\", \"1\");\n\t\tqputenv(\"QT_WIDGETS_RHI\", \"1\");\n\t\tqputenv(\"QT_WIDGETS_RHI_BACKEND\", \"opengl\");\n\t}\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\tif (!OptionUseOpenGLRenderer.value()) {\n\t\tqputenv(\"QT_WIDGETS_RHI\", \"1\");\n#ifdef Q_OS_MAC\n\t\tqputenv(\"QT_WIDGETS_RHI_BACKEND\",\n\t\t\tPlatform::MetalSupported() ? \"metal\" : \"opengl\");\n#elif defined(Q_OS_WIN)\n\t\tqputenv(\"QT_WIDGETS_RHI_BACKEND\", \"d3d11\");\n#else\n\t\tqputenv(\"QT_WIDGETS_RHI_BACKEND\", \"opengl\");\n#endif\n\t}\n#endif // Qt >= 6.7\n\n\tif (OptionFractionalScalingEnabled.value()\n\t\t\t|| OptionHighDpiDownscale.value()) {\n\t\tQApplication::setHighDpiScaleFactorRoundingPolicy(\n\t\t\tQt::HighDpiScaleFactorRoundingPolicy::PassThrough);\n\t} else {\n\t\tQApplication::setHighDpiScaleFactorRoundingPolicy(\n\t\t\tQt::HighDpiScaleFactorRoundingPolicy::RoundPreferFloor);\n\t}\n}\n\nint Launcher::exec() {\n\tinit();\n\n\tif (cLaunchMode() == LaunchModeFixPrevious) {\n\t\treturn psFixPrevious();\n\t} else if (cLaunchMode() == LaunchModeCleanup) {\n\t\treturn psCleanup();\n\t}\n\n\t// Must be started before Platform is started.\n\tLogs::start();\n\tbase::options::init(cWorkingDir() + \"tdata/experimental_options.json\");\n\n\t// Must be called after options are inited.\n\tinitHighDpi();\n\n\tif (Logs::DebugEnabled()) {\n\t\tconst auto openalLogPath = QDir::toNativeSeparators(\n\t\t\tcWorkingDir() + u\"DebugLogs/last_openal_log.txt\"_q);\n\n\t\tqputenv(\"ALSOFT_LOGLEVEL\", \"3\");\n\n#ifdef Q_OS_WIN\n\t\t_wputenv_s(\n\t\t\tL\"ALSOFT_LOGFILE\",\n\t\t\topenalLogPath.toStdWString().c_str());\n#else // Q_OS_WIN\n\t\tqputenv(\n\t\t\t\"ALSOFT_LOGFILE\",\n\t\t\tQFile::encodeName(openalLogPath));\n#endif // !Q_OS_WIN\n\t}\n\n\t// Must be started before Sandbox is created.\n\tPlatform::start();\n\tThirdParty::start();\n\tauto result = executeApplication();\n\n\tDEBUG_LOG((\"Telegram finished, result: %1\").arg(result));\n\n\tif (!UpdaterDisabled() && cRestartingUpdate()) {\n\t\tDEBUG_LOG((\"Sandbox Info: executing updater to install update.\"));\n\t\tif (!launchUpdater(UpdaterLaunch::PerformUpdate)) {\n\t\t\tbase::Platform::DeleteDirectory(cWorkingDir() + u\"tupdates/temp\"_q);\n\t\t}\n\t} else if (cRestarting()) {\n\t\tDEBUG_LOG((\"Sandbox Info: executing Telegram because of restart.\"));\n\t\tlaunchUpdater(UpdaterLaunch::JustRelaunch);\n\t}\n\n\tCrashReports::Finish();\n\tThirdParty::finish();\n\tPlatform::finish();\n\tLogs::finish();\n\n\treturn result;\n}\n\nbool Launcher::validateCustomWorkingDir() {\n\tif (customWorkingDir()) {\n\t\tif (_customWorkingDir == cWorkingDir()) {\n\t\t\t_customWorkingDir = QString();\n\t\t\treturn false;\n\t\t}\n\t\tcForceWorkingDir(_customWorkingDir);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Launcher::workingFolderReady() {\n\tsrand((unsigned int)time(nullptr));\n\n\tComputeDebugMode();\n\tComputeExternalUpdater();\n\tComputeInstallBetaVersions();\n\tComputeInstallationTag();\n}\n\nvoid Launcher::writeDebugModeSetting() {\n\tWriteDebugModeSetting();\n}\n\nvoid Launcher::writeInstallBetaVersionsSetting() {\n\tWriteInstallBetaVersionsSetting();\n}\n\nbool Launcher::checkPortableVersionFolder() {\n\treturn CheckPortableVersionFolder();\n}\n\nQStringList Launcher::readArguments(int argc, char *argv[]) const {\n\tExpects(argc >= 0);\n\n\tif (const auto native = readArgumentsHook(argc, argv)) {\n\t\treturn *native;\n\t}\n\n\tauto result = QStringList();\n\tresult.reserve(argc);\n\tfor (auto i = 0; i != argc; ++i) {\n\t\tresult.push_back(base::FromUtf8Safe(argv[i]));\n\t}\n\treturn result;\n}\n\nconst QStringList &Launcher::arguments() const {\n\treturn _arguments;\n}\n\nQString Launcher::initialWorkingDir() const {\n\treturn _initialWorkingDir;\n}\n\nbool Launcher::customWorkingDir() const {\n\treturn !_customWorkingDir.isEmpty();\n}\n\nvoid Launcher::prepareSettings() {\n\tauto path = base::Platform::CurrentExecutablePath(_argc, _argv);\n\tLOG((\"Executable path before check: %1\").arg(path));\n\tif (cExeName().isEmpty()) {\n\t\tLOG((\"WARNING: Could not compute executable path, some features will be disabled.\"));\n\t}\n\n\tprocessArguments();\n}\n\nvoid Launcher::initQtMessageLogging() {\n\tstatic QtMessageHandler OriginalMessageHandler = nullptr;\n\tOriginalMessageHandler = qInstallMessageHandler([](\n\t\t\tQtMsgType type,\n\t\t\tconst QMessageLogContext &context,\n\t\t\tconst QString &msg) {\n\t\tif (OriginalMessageHandler) {\n\t\t\tOriginalMessageHandler(type, context, msg);\n\t\t}\n\t\tif (Logs::DebugEnabled() || !Logs::started()) {\n\t\t\tif (!Logs::WritingEntry()) {\n\t\t\t\t// Sometimes Qt logs something inside our own logging.\n\t\t\t\tLOG((msg));\n\t\t\t}\n\t\t}\n\t});\n}\n\nuint64 Launcher::installationTag() const {\n\treturn InstallationTag;\n}\n\nQByteArray Launcher::instanceHash() const {\n\tstatic const auto Result = [&] {\n\t\tQByteArray h(32, 0);\n\t\tif (customWorkingDir()) {\n\t\t\tconst auto d = QFile::encodeName(\n\t\t\t\tQDir(cWorkingDir()).absolutePath());\n\t\t\thashMd5Hex(d.constData(), d.size(), h.data());\n\t\t} else {\n\t\t\tconst auto f = QFile::encodeName(cExeDir() + cExeName());\n\t\t\thashMd5Hex(f.constData(), f.size(), h.data());\n\t\t}\n\t\treturn h;\n\t}();\n\treturn Result;\n}\n\nvoid Launcher::processArguments() {\n\tenum class KeyFormat {\n\t\tNoValues,\n\t\tOneValue,\n\t\tAllLeftValues,\n\t};\n\tauto parseMap = std::map<QByteArray, KeyFormat> {\n\t\t{ \"-debug\"          , KeyFormat::NoValues },\n\t\t{ \"-key\"            , KeyFormat::OneValue },\n\t\t{ \"-autostart\"      , KeyFormat::NoValues },\n\t\t{ \"-fixprevious\"    , KeyFormat::NoValues },\n\t\t{ \"-cleanup\"        , KeyFormat::NoValues },\n\t\t{ \"-noupdate\"       , KeyFormat::NoValues },\n\t\t{ \"-tosettings\"     , KeyFormat::NoValues },\n\t\t{ \"-startintray\"    , KeyFormat::NoValues },\n\t\t{ \"-quit\"           , KeyFormat::NoValues },\n\t\t{ \"-workdir\"        , KeyFormat::OneValue },\n\t\t{ \"--\"              , KeyFormat::AllLeftValues },\n\t\t{ \"-scale\"          , KeyFormat::OneValue },\n\t};\n\tauto parseResult = QMap<QByteArray, QStringList>();\n\tauto parsingKey = QByteArray();\n\tauto parsingFormat = KeyFormat::NoValues;\n\tfor (auto i = _arguments.cbegin(); i != _arguments.cend(); ++i) {\n\t\tif (i == _arguments.cbegin()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto &argument = *i;\n\t\tswitch (parsingFormat) {\n\t\tcase KeyFormat::OneValue: {\n\t\t\tparseResult[parsingKey] = QStringList(argument.mid(0, 8192));\n\t\t\tparsingFormat = KeyFormat::NoValues;\n\t\t} break;\n\t\tcase KeyFormat::AllLeftValues: {\n\t\t\tparseResult[parsingKey].push_back(argument.mid(0, 8192));\n\t\t} break;\n\t\tcase KeyFormat::NoValues: {\n\t\t\tparsingKey = argument.toLatin1();\n\t\t\tauto it = parseMap.find(parsingKey);\n\t\t\tif (it != parseMap.end()) {\n\t\t\t\tparsingFormat = it->second;\n\t\t\t\tparseResult[parsingKey] = QStringList();\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tparseResult[\"--\"].push_back(argument.mid(0, 8192));\n\t\t} break;\n\t\t}\n\t}\n\n\tstatic const auto RegExp = QRegularExpression(\"[^a-z0-9\\\\-_]\");\n\tgDebugMode = parseResult.contains(\"-debug\");\n\tgKeyFile = parseResult\n\t\t.value(\"-key\", {})\n\t\t.join(QString())\n\t\t.toLower()\n\t\t.replace(RegExp, {});\n\tgLaunchMode = parseResult.contains(\"-autostart\") ? LaunchModeAutoStart\n\t\t: parseResult.contains(\"-fixprevious\") ? LaunchModeFixPrevious\n\t\t: parseResult.contains(\"-cleanup\") ? LaunchModeCleanup\n\t\t: LaunchModeNormal;\n\tgNoStartUpdate = parseResult.contains(\"-noupdate\");\n\tgStartToSettings = parseResult.contains(\"-tosettings\");\n\tgStartInTray = parseResult.contains(\"-startintray\");\n\tgQuit = parseResult.contains(\"-quit\");\n\t_customWorkingDir = parseResult.value(\"-workdir\", {}).join(QString());\n\tif (!_customWorkingDir.isEmpty()) {\n\t\t_customWorkingDir = QDir(_customWorkingDir).absolutePath() + '/';\n\t}\n\n\tconst auto startUrls = parseResult.value(\"--\", {});\n\tgStartUrls = startUrls | ranges::views::transform([&](const QString &url) {\n\t\treturn QUrl::fromUserInput(url, _initialWorkingDir);\n\t}) | ranges::views::filter(&QUrl::isValid) | ranges::to<QList<QUrl>>;\n\n\tconst auto scaleKey = parseResult.value(\"-scale\", {});\n\tif (scaleKey.size() > 0) {\n\t\tusing namespace style;\n\t\tconst auto value = scaleKey[0].toInt();\n\t\tgConfigScale = ((value < kScaleMin) || (value > kScaleMax))\n\t\t\t? kScaleAuto\n\t\t\t: value;\n\t}\n}\n\nint Launcher::executeApplication() {\n\tFilteredCommandLineArguments arguments(_argc, _argv);\n\tSandbox sandbox(arguments.count(), arguments.values());\n\tUi::MainQueueProcessor processor;\n\tbase::ConcurrentTimerEnvironment environment;\n\treturn sandbox.start();\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/launcher.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"core/base_integration.h\"\n\nnamespace Core {\n\nextern const char kOptionFractionalScalingEnabled[];\nextern const char kOptionHighDpiDownscale[];\nextern const char kOptionFreeType[];\nextern const char kOptionUseOpenGLRenderer[];\n\nclass Launcher {\npublic:\n\tLauncher(int argc, char *argv[]);\n\n\tstatic std::unique_ptr<Launcher> Create(int argc, char *argv[]);\n\n\tstatic Launcher &Instance() {\n\t\tExpects(InstanceSetter::Instance != nullptr);\n\n\t\treturn *InstanceSetter::Instance;\n\t}\n\n\tvirtual int exec();\n\n\tconst QStringList &arguments() const;\n\tQString initialWorkingDir() const;\n\tbool customWorkingDir() const;\n\n\tuint64 installationTag() const;\n\tQByteArray instanceHash() const;\n\n\tbool checkPortableVersionFolder();\n\tbool validateCustomWorkingDir();\n\tvoid workingFolderReady();\n\tvoid writeDebugModeSetting();\n\tvoid writeInstallBetaVersionsSetting();\n\n\tvirtual ~Launcher();\n\nprotected:\n\tenum class UpdaterLaunch {\n\t\tPerformUpdate,\n\t\tJustRelaunch,\n\t};\n\nprivate:\n\tvoid prepareSettings();\n\tvoid initQtMessageLogging();\n\tvoid processArguments();\n\n\tQStringList readArguments(int argc, char *argv[]) const;\n\tvirtual std::optional<QStringList> readArgumentsHook(\n\t\t\tint argc,\n\t\t\tchar *argv[]) const {\n\t\treturn std::nullopt;\n\t}\n\n\tvoid init();\n\tvirtual void initHook() {\n\t}\n\tvirtual void initHighDpi();\n\n\tvirtual bool launchUpdater(UpdaterLaunch action) = 0;\n\n\tint executeApplication();\n\n\tstruct InstanceSetter {\n\t\tInstanceSetter(not_null<Launcher*> instance) {\n\t\t\tExpects(Instance == nullptr);\n\n\t\t\tInstance = instance;\n\t\t}\n\n\t\tstatic Launcher *Instance;\n\t};\n\tInstanceSetter _setter = { this };\n\n\tint _argc;\n\tchar **_argv;\n\tQStringList _arguments;\n\tBaseIntegration _baseIntegration;\n\n\tQString _initialWorkingDir;\n\tQString _customWorkingDir;\n\n};\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/local_url_handlers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/local_url_handlers.h\"\n\n#include \"core/deep_links/deep_links_router.h\"\n#include \"api/api_confirm_phone.h\"\n#include \"api/api_chat_filters.h\"\n#include \"api/api_chat_invite.h\"\n#include \"api/api_premium.h\"\n#include \"base/qthelp_regex.h\"\n#include \"base/qthelp_url.h\"\n#include \"lang/lang_cloud_manager.h\"\n#include \"lang/lang_keys.h\"\n#include \"core/update_checker.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"dialogs/ui/dialogs_suggestions.h\"\n#include \"boxes/background_preview_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/edit_birthday_box.h\"\n#include \"ui/integration.h\"\n#include \"payments/payments_non_panel_process.h\"\n#include \"boxes/peers/edit_peer_info_box.h\"\n#include \"boxes/share_box.h\"\n#include \"boxes/connection_box.h\"\n#include \"boxes/gift_premium_box.h\"\n#include \"boxes/edit_privacy_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"boxes/language_box.h\"\n#include \"boxes/url_auth_box.h\"\n#include \"passport/passport_form_controller.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"data/components/credits.h\"\n#include \"data/data_birthday.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"media/view/media_view_open_common.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_session_controller_link_info.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/themes/window_theme_editor_box.h\" // GenerateSlug.\n#include \"payments/payments_checkout_process.h\"\n#include \"settings/sections/settings_credits.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"settings/settings_privacy_controllers.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"storage/storage_account.h\"\n#include \"mainwidget.h\"\n#include \"main/main_account.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"apiwrap.h\"\n\n#include <QtGui/QGuiApplication>\n\nnamespace Core {\nnamespace {\n\nusing Match = qthelp::RegularExpressionMatch;\n\nclass PersonalChannelController final : public PeerListController {\npublic:\n\texplicit PersonalChannelController(\n\t\tnot_null<Window::SessionController*> window);\n\t~PersonalChannelController();\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\t[[nodiscard]] rpl::producer<not_null<ChannelData*>> chosen() const;\n\nprivate:\n\tconst not_null<Window::SessionController*> _window;\n\trpl::event_stream<not_null<ChannelData*>> _chosen;\n\tmtpRequestId _requestId = 0;\n\n};\n\nPersonalChannelController::PersonalChannelController(\n\tnot_null<Window::SessionController*> window)\n: _window(window) {\n}\n\nPersonalChannelController::~PersonalChannelController() {\n\tif (_requestId) {\n\t\t_window->session().api().request(_requestId).cancel();\n\t}\n}\n\nMain::Session &PersonalChannelController::session() const {\n\treturn _window->session();\n}\n\nvoid PersonalChannelController::prepare() {\n\tsetDescription(object_ptr<Ui::FlatLabel>(\n\t\tnullptr,\n\t\ttr::lng_contacts_loading(),\n\t\tcomputeListSt().about));\n\n\tusing Flag = MTPchannels_GetAdminedPublicChannels::Flag;\n\t_requestId = _window->session().api().request(\n\t\tMTPchannels_GetAdminedPublicChannels(\n\t\t\tMTP_flags(Flag::f_for_personal))\n\t).done([=](const MTPmessages_Chats &result) {\n\t\t_requestId = 0;\n\n\t\tsetDescription(nullptr);\n\t\tconst auto &chats = result.match([](const auto &data) {\n\t\t\treturn data.vchats().v;\n\t\t});\n\t\tconst auto owner = &_window->session().data();\n\t\tfor (const auto &chat : chats) {\n\t\t\tif (const auto peer = owner->processChat(chat)) {\n\t\t\t\tconst auto rowId = peer->id.value;\n\t\t\t\tconst auto channel = peer->asChannel();\n\t\t\t\tif (channel && !delegate()->peerListFindRow(rowId)) {\n\t\t\t\t\tauto row = std::make_unique<PeerListRow>(peer);\n\t\t\t\t\trow->setCustomStatus(tr::lng_chat_status_subscribers(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tchannel->membersCount()));\n\t\t\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!delegate()->peerListFullRowsCount()) {\n\t\t\tauto none = rpl::combine(\n\t\t\t\ttr::lng_settings_channel_no_yet(tr::marked),\n\t\t\t\ttr::lng_settings_channel_start()\n\t\t\t) | rpl::map([](TextWithEntities &&text, const QString &link) {\n\t\t\t\treturn text.append('\\n').append(tr::link(link));\n\t\t\t});\n\t\t\tauto label = object_ptr<Ui::FlatLabel>(\n\t\t\t\tnullptr,\n\t\t\t\tstd::move(none),\n\t\t\t\tcomputeListSt().about);\n\t\t\tlabel->setClickHandlerFilter([=](const auto &...) {\n\t\t\t\t_window->showNewChannel();\n\t\t\t\treturn false;\n\t\t\t});\n\t\t\tsetDescription(std::move(label));\n\t\t}\n\t\tdelegate()->peerListRefreshRows();\n\t}).send();\n}\n\nvoid PersonalChannelController::rowClicked(not_null<PeerListRow*> row) {\n\tif (const auto channel = row->peer()->asChannel()) {\n\t\t_chosen.fire_copy(channel);\n\t}\n}\n\nauto PersonalChannelController::chosen() const\n-> rpl::producer<not_null<ChannelData*>> {\n\treturn _chosen.events();\n}\n\nWindow::SessionController *ApplyAccountIndex(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tint accountIndex) {\n\tif (accountIndex <= 0) {\n\t\treturn nullptr;\n\t}\n\tconst auto list = Core::App().domain().orderedAccounts();\n\tif (accountIndex > int(list.size())) {\n\t\treturn nullptr;\n\t}\n\tconst auto account = list[accountIndex - 1];\n\tif (account == &controller->session().account()) {\n\t\treturn controller;\n\t} else if (const auto window = Core::App().windowFor({ account })) {\n\t\tif (&window->account() != account) {\n\t\t\tCore::App().domain().maybeActivate(account);\n\t\t\tif (&window->account() != account) {\n\t\t\t\treturn nullptr;\n\t\t\t}\n\t\t}\n\t\tconst auto session = window->sessionController();\n\t\tif (session) {\n\t\t\treturn session;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid SavePersonalChannel(\n\t\tnot_null<Window::SessionController*> window,\n\t\tChannelData *channel) {\n\tconst auto self = window->session().user();\n\tconst auto history = channel\n\t\t? channel->owner().history(channel->id).get()\n\t\t: nullptr;\n\tconst auto item = history\n\t\t? history->lastServerMessage()\n\t\t: nullptr;\n\tconst auto channelId = channel\n\t\t? peerToChannel(channel->id)\n\t\t: ChannelId();\n\tconst auto messageId = item ? item->id : MsgId();\n\tif (self->personalChannelId() != channelId\n\t\t|| (messageId\n\t\t\t&& self->personalChannelMessageId() != messageId)) {\n\t\tself->setPersonalChannel(channelId, messageId);\n\t\tself->session().api().request(MTPaccount_UpdatePersonalChannel(\n\t\t\tchannel ? channel->inputChannel() : MTP_inputChannelEmpty()\n\t\t)).done(crl::guard(window, [=] {\n\t\t\twindow->showToast((channel\n\t\t\t\t? tr::lng_settings_channel_saved\n\t\t\t\t: tr::lng_settings_channel_removed)(tr::now));\n\t\t})).fail(crl::guard(window, [=](const MTP::Error &error) {\n\t\t\twindow->showToast(u\"Error: \"_q + error.type());\n\t\t})).send();\n\t}\n}\n\nbool JoinGroupByHash(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tApi::CheckChatInvite(controller, match->captured(1));\n\tcontroller->window().activate();\n\treturn true;\n}\n\nbool JoinFilterBySlug(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tApi::CheckFilterInvite(controller, match->captured(1));\n\tcontroller->window().activate();\n\treturn true;\n}\n\nbool ShowStickerSet(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tCore::App().hideMediaView();\n\tcontroller->show(Box<StickerSetBox>(\n\t\tcontroller->uiShow(),\n\t\tStickerSetIdentifier{ .shortName = match->captured(2) },\n\t\t(match->captured(1) == \"addemoji\"\n\t\t\t? Data::StickersType::Emoji\n\t\t\t: Data::StickersType::Stickers)));\n\tcontroller->window().activate();\n\treturn true;\n}\n\nbool ShowTheme(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto fromMessageId = context.value<ClickHandlerContext>().itemId;\n\tCore::App().hideMediaView();\n\tcontroller->session().data().cloudThemes().resolve(\n\t\t&controller->window(),\n\t\tmatch->captured(1),\n\t\tfromMessageId);\n\tcontroller->window().activate();\n\treturn true;\n}\n\nvoid ShowLanguagesBox(Window::SessionController *controller) {\n\tstatic auto Guard = base::binary_guard();\n\tGuard = LanguageBox::Show(controller);\n}\n\nbool SetLanguage(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (match->capturedView(1).isEmpty()) {\n\t\tShowLanguagesBox(controller);\n\t} else {\n\t\tconst auto languageId = match->captured(2);\n\t\tLang::CurrentCloudManager().switchWithWarning(languageId);\n\t}\n\tif (controller) {\n\t\tcontroller->window().activate();\n\t}\n\treturn true;\n}\n\nbool ShareUrl(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto params = url_parse_params(\n\t\tmatch->captured(1),\n\t\tqthelp::UrlParamNameTransform::ToLower);\n\tconst auto url = params.value(u\"url\"_q);\n\tif (url.isEmpty() || url.trimmed().startsWith('@')) {\n\t\t// Don't allow to insert an inline bot query by share url link.\n\t\treturn false;\n\t}\n\n\tconst auto text = params.value(\"text\");\n\tconst auto chosen = [=](not_null<Data::Thread*> thread) {\n\t\tconst auto content = controller->content();\n\t\treturn content->shareUrl(thread, url, text);\n\t};\n\tWindow::ShowChooseRecipientBox(controller, chosen);\n\tcontroller->window().activate();\n\treturn true;\n}\n\nbool ConfirmPhone(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto params = url_parse_params(\n\t\tmatch->captured(1),\n\t\tqthelp::UrlParamNameTransform::ToLower);\n\tconst auto phone = params.value(u\"phone\"_q);\n\tconst auto hash = params.value(u\"hash\"_q);\n\tif (phone.isEmpty() || hash.isEmpty()) {\n\t\treturn false;\n\t}\n\tcontroller->session().api().confirmPhone().resolve(\n\t\tcontroller,\n\t\tphone,\n\t\thash);\n\tcontroller->window().activate();\n\treturn true;\n}\n\nbool ApplySocksProxy(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tauto params = url_parse_params(\n\t\tmatch->captured(1),\n\t\tqthelp::UrlParamNameTransform::ToLower);\n\tProxiesBoxController::ShowApplyConfirmation(\n\t\tcontroller,\n\t\tMTP::ProxyData::Type::Socks5,\n\t\tparams);\n\tif (controller) {\n\t\tcontroller->window().activate();\n\t}\n\treturn true;\n}\n\nbool ApplyMtprotoProxy(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tauto params = url_parse_params(\n\t\tmatch->captured(1),\n\t\tqthelp::UrlParamNameTransform::ToLower);\n\tauto &secret = params[u\"secret\"_q];\n\tsecret.replace('+', '-').replace('/', '_');\n\tProxiesBoxController::ShowApplyConfirmation(\n\t\tcontroller,\n\t\tMTP::ProxyData::Type::Mtproto,\n\t\tparams);\n\tif (controller) {\n\t\tcontroller->window().activate();\n\t}\n\treturn true;\n}\n\nbool ShowPassportForm(\n\t\tWindow::SessionController *controller,\n\t\tconst QMap<QString, QString> &params) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto botId = params.value(\"bot_id\", QString()).toULongLong();\n\tconst auto scope = params.value(\"scope\", QString());\n\tconst auto callback = params.value(\"callback_url\", QString());\n\tconst auto publicKey = params.value(\"public_key\", QString());\n\tconst auto nonce = params.value(\n\t\tPassport::NonceNameByScope(scope),\n\t\tQString());\n\tcontroller->showPassportForm(Passport::FormRequest(\n\t\tbotId,\n\t\tscope,\n\t\tcallback,\n\t\tpublicKey,\n\t\tnonce));\n\treturn true;\n}\n\nbool ShowPassport(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\treturn ShowPassportForm(\n\t\tcontroller,\n\t\turl_parse_params(\n\t\t\tmatch->captured(1),\n\t\t\tqthelp::UrlParamNameTransform::ToLower));\n}\n\nbool ShowWallPaper(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto params = url_parse_params(\n\t\tmatch->captured(1),\n\t\tqthelp::UrlParamNameTransform::ToLower);\n\t// const auto bg = params.value(u\"bg_color\"_q);\n\tconst auto color = params.value(u\"color\"_q);\n\tconst auto gradient = params.value(u\"gradient\"_q);\n\tconst auto result = BackgroundPreviewBox::Start(\n\t\tcontroller,\n\t\t(!color.isEmpty()\n\t\t\t? color\n\t\t\t: !gradient.isEmpty()\n\t\t\t? gradient\n\t\t\t: params.value(u\"slug\"_q)),\n\t\tparams);\n\tcontroller->window().activate();\n\treturn result;\n}\n\n[[nodiscard]] ChatAdminRights ParseRequestedAdminRights(\n\t\tconst QString &value) {\n\tauto result = ChatAdminRights();\n\tfor (const auto &element : value.split(QRegularExpression(u\"[+ ]\"_q))) {\n\t\tif (element.isEmpty()) {\n\t\t\tcontinue;\n\t\t} else if (element == u\"change_info\"_q) {\n\t\t\tresult |= ChatAdminRight::ChangeInfo;\n\t\t} else if (element == u\"post_messages\"_q) {\n\t\t\tresult |= ChatAdminRight::PostMessages;\n\t\t} else if (element == u\"edit_messages\"_q) {\n\t\t\tresult |= ChatAdminRight::EditMessages;\n\t\t} else if (element == u\"delete_messages\"_q) {\n\t\t\tresult |= ChatAdminRight::DeleteMessages;\n\t\t} else if (element == u\"restrict_members\"_q) {\n\t\t\tresult |= ChatAdminRight::BanUsers;\n\t\t} else if (element == u\"invite_users\"_q) {\n\t\t\tresult |= ChatAdminRight::InviteByLinkOrAdd;\n\t\t} else if (element == u\"manage_topics\"_q) {\n\t\t\tresult |= ChatAdminRight::ManageTopics;\n\t\t} else if (element == u\"pin_messages\"_q) {\n\t\t\tresult |= ChatAdminRight::PinMessages;\n\t\t} else if (element == u\"promote_members\"_q) {\n\t\t\tresult |= ChatAdminRight::AddAdmins;\n\t\t} else if (element == u\"post_stories\"_q) {\n\t\t\tresult |= ChatAdminRight::PostStories;\n\t\t} else if (element == u\"edit_stories\"_q) {\n\t\t\tresult |= ChatAdminRight::EditStories;\n\t\t} else if (element == u\"delete_stories\"_q) {\n\t\t\tresult |= ChatAdminRight::DeleteStories;\n\t\t} else if (element == u\"manage_video_chats\"_q) {\n\t\t\tresult |= ChatAdminRight::ManageCall;\n\t\t} else if (element == u\"manage_direct_messages\"_q) {\n\t\t\tresult |= ChatAdminRight::ManageDirect;\n\t\t} else if (element == u\"anonymous\"_q) {\n\t\t\tresult |= ChatAdminRight::Anonymous;\n\t\t} else if (element == u\"manage_chat\"_q) {\n\t\t\tresult |= ChatAdminRight::Other;\n\t\t} else {\n\t\t\tcontinue;\n\t\t}\n\t}\n\treturn result;\n}\n\nbool ResolveUsernameOrPhone(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto params = url_parse_params(\n\t\tmatch->captured(1),\n\t\tqthelp::UrlParamNameTransform::ToLower);\n\n\tif (params.contains(u\"acc\"_q)) {\n\t\tconst auto switched = ApplyAccountIndex(\n\t\t\tcontroller,\n\t\t\tparams.value(u\"acc\"_q).toInt());\n\t\tif (switched) {\n\t\t\tcontroller = switched;\n\t\t} else {\n\t\t\tcontroller->showToast(u\"Could not activate account %1.\"_q.arg(\n\t\t\t\tparams.value(u\"acc\"_q)));\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tconst auto domainParam = params.value(u\"domain\"_q);\n\tconst auto appnameParam = params.value(u\"appname\"_q);\n\tconst auto myContext = context.value<ClickHandlerContext>();\n\n\tif (domainParam == u\"giftcode\"_q && !appnameParam.isEmpty()) {\n\t\tconst auto itemId = myContext.itemId;\n\t\tconst auto item = controller->session().data().message(itemId);\n\t\tconst auto fromId = item ? item->from()->id : PeerId();\n\t\tconst auto selfId = controller->session().userPeerId();\n\t\tconst auto toId = !item\n\t\t\t? PeerId()\n\t\t\t: (fromId == selfId)\n\t\t\t? item->history()->peer->id\n\t\t\t: selfId;\n\t\tResolveGiftCode(controller, appnameParam, fromId, toId);\n\t\treturn true;\n\t}\n\tif (domainParam == u\"oauth\"_q) {\n\t\tconst auto token = params.value(u\"startapp\"_q);\n\t\tif (!token.isEmpty()) {\n\t\t\tUrlAuthBox::ActivateUrl(\n\t\t\t\tcontroller->uiShow(),\n\t\t\t\t&controller->session(),\n\t\t\t\tu\"tg://resolve?domain=oauth&startapp=\"_q + token,\n\t\t\t\tcontext);\n\t\t\treturn true;\n\t\t}\n\t}\n\n\t// Fix t.me/s/username links.\n\tconst auto webChannelPreviewLink = (domainParam == u\"s\"_q)\n\t\t&& !appnameParam.isEmpty();\n\tconst auto domain = webChannelPreviewLink ? appnameParam : domainParam;\n\tconst auto phone = params.value(u\"phone\"_q);\n\tconst auto validDomain = [](const QString &domain) {\n\t\treturn qthelp::regex_match(\n\t\t\tu\"^[a-zA-Z0-9\\\\.\\\\_]+$\"_q,\n\t\t\tdomain,\n\t\t\t{}\n\t\t).valid();\n\t};\n\tconst auto validPhone = [](const QString &phone) {\n\t\treturn qthelp::regex_match(u\"^[0-9]+$\"_q, phone, {}).valid();\n\t};\n\tif (domain == u\"telegrampassport\"_q) {\n\t\treturn ShowPassportForm(controller, params);\n\t} else if (!validDomain(domain) && !validPhone(phone)) {\n\t\treturn false;\n\t}\n\tusing ResolveType = Window::ResolveType;\n\tauto resolveType = params.contains(u\"profile\"_q)\n\t\t? ResolveType::Profile\n\t\t: ResolveType::Default;\n\tauto startToken = params.value(u\"start\"_q);\n\tauto referral = params.value(u\"ref\"_q);\n\tif (!startToken.isEmpty()) {\n\t\tresolveType = ResolveType::BotStart;\n\t\tif (referral.isEmpty()) {\n\t\t\tconst auto appConfig = &controller->session().appConfig();\n\t\t\tconst auto &prefixes = appConfig->startRefPrefixes();\n\t\t\tfor (const auto &prefix : prefixes) {\n\t\t\t\tif (startToken.startsWith(prefix)) {\n\t\t\t\t\treferral = startToken.mid(prefix.size());\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if (params.contains(u\"startgroup\"_q)) {\n\t\tresolveType = ResolveType::AddToGroup;\n\t\tstartToken = params.value(u\"startgroup\"_q);\n\t} else if (params.contains(u\"startchannel\"_q)) {\n\t\tresolveType = ResolveType::AddToChannel;\n\t} else if (params.contains(u\"boost\"_q)) {\n\t\tresolveType = ResolveType::Boost;\n\t}\n\tauto post = ShowAtUnreadMsgId;\n\tauto adminRights = ChatAdminRights();\n\tif (resolveType == ResolveType::AddToGroup\n\t\t|| resolveType == ResolveType::AddToChannel) {\n\t\tadminRights = ParseRequestedAdminRights(params.value(u\"admin\"_q));\n\t}\n\tconst auto postParam = params.value(u\"post\"_q);\n\tif (const auto postId = postParam.toInt()) {\n\t\tpost = postId;\n\t}\n\tconst auto storyParam = params.value(u\"story\"_q);\n\tconst auto storyAlbumParam = params.value(u\"album\"_q);\n\tconst auto storyAlbumId = storyAlbumParam.toInt();\n\tconst auto giftCollectionParam = params.value(u\"collection\"_q);\n\tconst auto giftCollectionId = giftCollectionParam.toInt();\n\tconst auto appname = webChannelPreviewLink ? QString() : appnameParam;\n\tconst auto commentParam = params.value(u\"comment\"_q);\n\tconst auto commentId = commentParam.toInt();\n\tconst auto topicParam = params.value(u\"topic\"_q);\n\tconst auto topicId = topicParam.toInt();\n\tconst auto threadParam = params.value(u\"thread\"_q);\n\tconst auto threadId = topicId ? topicId : threadParam.toInt();\n\tconst auto gameParam = params.value(u\"game\"_q);\n\tconst auto videot = params.value(u\"t\"_q);\n\tconst auto pollOption = PollOptionFromLink(\n\t\tparams.value(u\"option\"_q));\n\tif (params.contains(u\"direct\"_q)) {\n\t\tresolveType = ResolveType::ChannelDirect;\n\t}\n\tif (!gameParam.isEmpty() && validDomain(gameParam)) {\n\t\tstartToken = gameParam;\n\t\tresolveType = ResolveType::ShareGame;\n\t}\n\tif (!appname.isEmpty()) {\n\t\tresolveType = ResolveType::BotApp;\n\t\tif (startToken.isEmpty() && params.contains(u\"startapp\"_q)) {\n\t\t\tstartToken = params.value(u\"startapp\"_q);\n\t\t}\n\t}\n\tcontroller->window().activate();\n\tcontroller->showPeerByLink(Window::PeerByLinkInfo{\n\t\t.usernameOrId = domain,\n\t\t.phone = phone,\n\t\t.messageId = post,\n\t\t.pollOption = pollOption,\n\t\t.storyParam = storyParam,\n\t\t.storyAlbumId = storyAlbumId,\n\t\t.giftCollectionId = giftCollectionId,\n\t\t.videoTimestamp = (!videot.isEmpty()\n\t\t\t? ParseVideoTimestamp(videot)\n\t\t\t: std::optional<TimeId>()),\n\t\t.text = params.value(u\"text\"_q),\n\t\t.repliesInfo = commentId\n\t\t\t? Window::RepliesByLinkInfo{\n\t\t\t\tWindow::CommentId{ commentId }\n\t\t\t}\n\t\t\t: threadId\n\t\t\t? Window::RepliesByLinkInfo{\n\t\t\t\tWindow::ThreadId{ threadId }\n\t\t\t}\n\t\t\t: Window::RepliesByLinkInfo{ v::null },\n\t\t.resolveType = resolveType,\n\t\t.referral = referral,\n\t\t.startToken = startToken,\n\t\t.startAdminRights = adminRights,\n\t\t.startAutoSubmit = myContext.botStartAutoSubmit,\n\t\t.botAppName = (appname.isEmpty() ? postParam : appname),\n\t\t.botAppForceConfirmation = myContext.mayShowConfirmation,\n\t\t.botAppFullScreen = (params.value(u\"mode\"_q) == u\"fullscreen\"_q),\n\t\t.attachBotUsername = params.value(u\"attach\"_q),\n\t\t.attachBotToggleCommand = (params.contains(u\"startattach\"_q)\n\t\t\t? params.value(u\"startattach\"_q)\n\t\t\t: (appname.isEmpty() && params.contains(u\"startapp\"_q))\n\t\t\t? params.value(u\"startapp\"_q)\n\t\t\t: std::optional<QString>()),\n\t\t.attachBotMainOpen = (appname.isEmpty()\n\t\t\t&& params.contains(u\"startapp\"_q)),\n\t\t.attachBotMainCompact = (appname.isEmpty()\n\t\t\t&& params.contains(u\"startapp\"_q)\n\t\t\t&& (params.value(u\"mode\"_q) == u\"compact\"_q)),\n\t\t.attachBotChooseTypes = InlineBots::ParseChooseTypes(\n\t\t\tparams.value(u\"choose\"_q)),\n\t\t.voicechatHash = (params.contains(u\"livestream\"_q)\n\t\t\t? std::make_optional(params.value(u\"livestream\"_q))\n\t\t\t: params.contains(u\"videochat\"_q)\n\t\t\t? std::make_optional(params.value(u\"videochat\"_q))\n\t\t\t: params.contains(u\"voicechat\"_q)\n\t\t\t? std::make_optional(params.value(u\"voicechat\"_q))\n\t\t\t: std::nullopt),\n\t\t.clickFromMessageId = myContext.itemId,\n\t\t.clickFromBotWebviewContext = myContext.botWebviewContext,\n\t\t.historyInNewWindow =\n\t\t\t(params.value(u\"tdesktop_target\"_q) == u\"blank\"_q),\n\t});\n\treturn true;\n}\n\nbool ResolvePrivatePost(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto params = url_parse_params(\n\t\tmatch->captured(1),\n\t\tqthelp::UrlParamNameTransform::ToLower);\n\tconst auto channelId = ChannelId(\n\t\tparams.value(u\"channel\"_q).toULongLong());\n\tconst auto msgId = params.value(u\"post\"_q).toInt();\n\tconst auto commentParam = params.value(u\"comment\"_q);\n\tconst auto commentId = commentParam.toInt();\n\tconst auto topicParam = params.value(u\"topic\"_q);\n\tconst auto topicId = topicParam.toInt();\n\tconst auto threadParam = params.value(u\"thread\"_q);\n\tconst auto threadId = topicId ? topicId : threadParam.toInt();\n\tconst auto pollOption = PollOptionFromLink(\n\t\tparams.value(u\"option\"_q));\n\tif (!channelId || (msgId && !IsServerMsgId(msgId))) {\n\t\treturn false;\n\t}\n\tconst auto my = context.value<ClickHandlerContext>();\n\tcontroller->showPeerByLink(Window::PeerByLinkInfo{\n\t\t.usernameOrId = channelId,\n\t\t.messageId = msgId,\n\t\t.pollOption = pollOption,\n\t\t.repliesInfo = commentId\n\t\t\t? Window::RepliesByLinkInfo{\n\t\t\t\tWindow::CommentId{ commentId }\n\t\t\t}\n\t\t\t: threadId\n\t\t\t? Window::RepliesByLinkInfo{\n\t\t\t\tWindow::ThreadId{ threadId }\n\t\t\t}\n\t\t\t: Window::RepliesByLinkInfo{ v::null },\n\t\t.clickFromMessageId = my.itemId,\n\t\t.clickFromBotWebviewContext = my.botWebviewContext,\n\t});\n\tcontroller->window().activate();\n\treturn true;\n}\n\nbool HandleUnknown(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto request = match->captured(1);\n\tconst auto callback = crl::guard(controller, [=](\n\t\t\tTextWithEntities message,\n\t\t\tbool updateRequired) {\n\t\tif (updateRequired) {\n\t\t\tconst auto callback = [=](Fn<void()> &&close) {\n\t\t\t\tCore::UpdateApplication();\n\t\t\t\tclose();\n\t\t\t};\n\t\t\tcontroller->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = message,\n\t\t\t\t.confirmed = callback,\n\t\t\t\t.confirmText = tr::lng_menu_update(),\n\t\t\t}));\n\t\t} else {\n\t\t\tcontroller->show(Ui::MakeInformBox(message));\n\t\t}\n\t});\n\tcontroller->session().api().requestDeepLinkInfo(request, callback);\n\treturn true;\n}\n\nbool OpenMediaTimestamp(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto position = match->captured(2).toInt();\n\tif (position < 0) {\n\t\treturn false;\n\t}\n\tconst auto base = match->captured(1);\n\tif (base.startsWith(u\"doc\"_q)) {\n\t\tconst auto parts = base.mid(3).split('_');\n\t\tconst auto documentId = parts.value(0).toULongLong();\n\t\tconst auto itemId = FullMsgId(\n\t\t\tPeerId(parts.value(1).toULongLong()),\n\t\t\tMsgId(parts.value(2).toLongLong()));\n\t\tconst auto session = &controller->session();\n\t\tconst auto document = session->data().document(documentId);\n\t\tconst auto context = session->data().message(itemId);\n\t\tconst auto time = position * crl::time(1000);\n\t\tif (document->isVideoFile()) {\n\t\t\tcontroller->window().openInMediaView(Media::View::OpenRequest(\n\t\t\t\tcontroller,\n\t\t\t\tdocument,\n\t\t\t\tcontext,\n\t\t\t\tcontext ? context->topicRootId() : MsgId(0),\n\t\t\t\tcontext ? context->sublistPeerId() : PeerId(0),\n\t\t\t\tfalse,\n\t\t\t\ttime));\n\t\t} else if (document->isSong() || document->isVoiceMessage()) {\n\t\t\tsession->local().setMediaLastPlaybackPosition(documentId, time);\n\t\t\tMedia::Player::instance()->play({ document, itemId });\n\t\t}\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool ShowInviteLink(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto base64link = match->captured(1).toLatin1();\n\tconst auto link = QString::fromUtf8(QByteArray::fromBase64(base64link));\n\tif (link.isEmpty()) {\n\t\treturn false;\n\t}\n\tQGuiApplication::clipboard()->setText(link);\n\tcontroller->showToast(tr::lng_group_invite_copied(tr::now));\n\treturn true;\n}\n\nbool OpenExternalLink(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\treturn Ui::Integration::Instance().handleUrlClick(\n\t\tmatch->captured(1),\n\t\tcontext);\n}\n\nbool CopyPeerId(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tTextUtilities::SetClipboardText({ match->captured(1) });\n\tif (controller) {\n\t\tcontroller->showToast(u\"ID copied to clipboard.\"_q);\n\t}\n\treturn true;\n}\n\nbool ShowSearchTagsPromo(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tShowPremiumPreviewBox(controller, PremiumFeature::TagsForMessages);\n\treturn true;\n}\n\nbool ShowEditBirthday(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t} else if (controller->showFrozenError()) {\n\t\treturn true;\n\t}\n\n\tconst auto captured = match->captured(1);\n\tif (captured.startsWith(u\":suggest:\"_q)) {\n\t\tconst auto userIdStr = captured.mid(9); // Skip \":suggest:\"\n\t\tconst auto userId = UserId(userIdStr.toULongLong());\n\t\tif (!userId) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst auto targetUser\n\t\t\t= controller->session().data().userLoaded(userId);\n\t\tif (!targetUser) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst auto save = [=](Data::Birthday result) {\n\t\t\tusing BFlag = MTPDbirthday::Flag;\n\t\t\tcontroller->session().api().request(MTPusers_SuggestBirthday(\n\t\t\t\ttargetUser->inputUser(),\n\t\t\t\tMTP_birthday(\n\t\t\t\t\tMTP_flags(result.year() ? BFlag::f_year : BFlag()),\n\t\t\t\t\tMTP_int(result.day()),\n\t\t\t\t\tMTP_int(result.month()),\n\t\t\t\t\tMTP_int(result.year()))\n\t\t\t)).done(crl::guard(controller, [=] {\n\t\t\t\tcontroller->showPeerHistory(targetUser);\n\t\t\t\tcontroller->showToast(\n\t\t\t\t\ttr::lng_settings_birthday_suggested(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\ttargetUser->name()));\n\t\t\t})).fail(crl::guard(controller, [=](const MTP::Error &error) {\n\t\t\t\tconst auto type = error.type();\n\t\t\t\tcontroller->showToast(type.startsWith(u\"FLOOD_WAIT_\"_q)\n\t\t\t\t\t? tr::lng_flood_error(tr::now)\n\t\t\t\t\t: (u\"Error: \"_q + error.type()));\n\t\t\t})).handleFloodErrors().send();\n\t\t};\n\n\t\tcontroller->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tbox->setTitle(tr::lng_suggest_birthday_box_title(\n\t\t\t\tlt_user,\n\t\t\t\tInfo::Profile::NameValue(targetUser)));\n\t\t\tUi::EditBirthdayBox(\n\t\t\t\tbox,\n\t\t\t\tData::Birthday(),\n\t\t\t\tsave,\n\t\t\t\tUi::EditBirthdayType::Suggest);\n\t\t}));\n\t\treturn true;\n\t}\n\n\tconst auto user = controller->session().user();\n\tconst auto save = [=](Data::Birthday result) {\n\t\tuser->setBirthday(result);\n\n\t\tusing Flag = MTPaccount_UpdateBirthday::Flag;\n\t\tusing BFlag = MTPDbirthday::Flag;\n\t\tuser->session().api().request(MTPaccount_UpdateBirthday(\n\t\t\tMTP_flags(result ? Flag::f_birthday : Flag()),\n\t\t\tMTP_birthday(\n\t\t\t\tMTP_flags(result.year() ? BFlag::f_year : BFlag()),\n\t\t\t\tMTP_int(result.day()),\n\t\t\t\tMTP_int(result.month()),\n\t\t\t\tMTP_int(result.year()))\n\t\t)).done(crl::guard(controller, [=] {\n\t\t\tcontroller->showToast(tr::lng_settings_birthday_saved(tr::now));\n\t\t})).fail(crl::guard(controller, [=](const MTP::Error &error) {\n\t\t\tconst auto type = error.type();\n\t\t\tcontroller->showToast(type.startsWith(u\"FLOOD_WAIT_\"_q)\n\t\t\t\t? tr::lng_flood_error(tr::now)\n\t\t\t\t: (u\"Error: \"_q + error.type()));\n\t\t})).handleFloodErrors().send();\n\t};\n\tif (captured.startsWith(u\":suggestion_\"_q)) {\n\t\tconst auto suggested = Data::Birthday::FromSerialized(\n\t\t\tcaptured.mid(u\":suggestion_\"_q.size()).toInt());\n\t\tcontroller->show(Box(\n\t\t\tUi::EditBirthdayBox,\n\t\t\tsuggested,\n\t\t\tsave,\n\t\t\tUi::EditBirthdayType::ConfirmSuggestion));\n\t} else if (captured.isEmpty()) {\n\t\tcontroller->show(Box(\n\t\t\tUi::EditBirthdayBox,\n\t\t\tuser->birthday(),\n\t\t\tsave,\n\t\t\tUi::EditBirthdayType::Edit));\n\t} else {\n\t\tcontroller->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tUi::EditBirthdayBox(box, user->birthday(), save);\n\n\t\t\tconst auto container = box->verticalLayout();\n\t\t\tconst auto session = &user->session();\n\t\t\tconst auto key = Api::UserPrivacy::Key::Birthday;\n\t\t\tsession->api().userPrivacy().reload(key);\n\t\t\tauto isExactlyContacts = session->api().userPrivacy().value(\n\t\t\t\tkey\n\t\t\t) | rpl::map([=](const Api::UserPrivacy::Rule &value) {\n\t\t\t\treturn (value.option == Api::UserPrivacy::Option::Contacts)\n\t\t\t\t\t&& value.always.peers.empty()\n\t\t\t\t\t&& !value.always.premiums\n\t\t\t\t\t&& value.never.peers.empty();\n\t\t\t}) | rpl::distinct_until_changed();\n\t\t\tUi::AddSkip(container);\n\t\t\tconst auto link = u\"internal:edit_privacy_birthday:from_box\"_q;\n\t\t\tUi::AddDividerText(container, rpl::conditional(\n\t\t\t\tstd::move(isExactlyContacts),\n\t\t\t\ttr::lng_settings_birthday_contacts(\n\t\t\t\t\tlt_link,\n\t\t\t\t\ttr::lng_settings_birthday_contacts_link(tr::url(link)),\n\t\t\t\t\ttr::marked),\n\t\t\t\ttr::lng_settings_birthday_about(\n\t\t\t\t\tlt_link,\n\t\t\t\t\ttr::lng_settings_birthday_about_link(tr::url(link)),\n\t\t\t\t\ttr::marked)));\n\t\t}));\n\n\t}\n\treturn true;\n}\n\nbool ShowEditBirthdayPrivacy(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto isFromBox = !match->captured(1).isEmpty();\n\tauto syncLifetime = controller->session().api().userPrivacy().value(\n\t\tApi::UserPrivacy::Key::Birthday\n\t) | rpl::take(\n\t\t1\n\t) | rpl::on_next([=](const Api::UserPrivacy::Rule &value) {\n\t\tif (isFromBox) {\n\t\t\tusing namespace ::Settings;\n\t\t\tclass Controller final : public BirthdayPrivacyController {\n\t\t\t\tobject_ptr<Ui::RpWidget> setupAboveWidget(\n\t\t\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\t\t\tnot_null<QWidget*> parent,\n\t\t\t\t\trpl::producer<Option> optionValue,\n\t\t\t\t\tnot_null<QWidget*> outerContainer) override {\n\t\t\t\t\treturn { nullptr };\n\t\t\t\t}\n\t\t\t};\n\t\t\tcontroller->show(Box<EditPrivacyBox>(\n\t\t\t\tcontroller,\n\t\t\t\tstd::make_unique<Controller>(),\n\t\t\t\tvalue));\n\t\t\treturn;\n\t\t}\n\t\tcontroller->show(Box<EditPrivacyBox>(\n\t\t\tcontroller,\n\t\t\tstd::make_unique<::Settings::BirthdayPrivacyController>(),\n\t\t\tvalue));\n\t});\n\treturn true;\n}\n\nbool ShowEditPersonalChannel(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t} else if (controller->showFrozenError()) {\n\t\treturn true;\n\t}\n\tconst auto maybePeerId = match->captured(1);\n\tconst auto maybeRemove = match->captured(2);\n\n\tif (!maybePeerId.isEmpty()) {\n\t\tif (const auto peerId = PeerId(maybePeerId.toULongLong())) {\n\t\t\tif (const auto peer = controller->session().data().peer(peerId)) {\n\t\t\t\tif (const auto channel = peer->asChannel()) {\n\t\t\t\t\tSavePersonalChannel(controller, channel);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if (!maybeRemove.isEmpty()) {\n\t\tSavePersonalChannel(controller, nullptr);\n\t\treturn true;\n\t}\n\n\tauto listController = std::make_unique<PersonalChannelController>(\n\t\tcontroller);\n\tconst auto rawController = listController.get();\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tbox->setTitle(tr::lng_settings_channel_label());\n\t\tbox->addButton(tr::lng_box_done(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\n\t\tconst auto save = [=](ChannelData *channel) {\n\t\t\tSavePersonalChannel(controller, channel);\n\t\t\tbox->closeBox();\n\t\t};\n\n\t\trawController->chosen(\n\t\t) | rpl::on_next([=](not_null<ChannelData*> channel) {\n\t\t\tsave(channel);\n\t\t}, box->lifetime());\n\n\t\tif (controller->session().user()->personalChannelId()) {\n\t\t\tbox->addLeftButton(tr::lng_settings_channel_remove(), [=] {\n\t\t\t\tsave(nullptr);\n\t\t\t});\n\t\t}\n\t};\n\tcontroller->show(Box<PeerListBox>(\n\t\tstd::move(listController),\n\t\tstd::move(initBox)));\n\treturn true;\n}\n\nbool ShowCollectiblePhone(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto phone = match->captured(1);\n\tconst auto peerId = PeerId(match->captured(2).toULongLong());\n\tcontroller->resolveCollectible(\n\t\tpeerId,\n\t\tphone.startsWith('+') ? phone : '+' + phone);\n\treturn true;\n}\n\nbool ShowCollectibleUsername(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto username = match->captured(1);\n\tconst auto peerId = PeerId(match->captured(2).toULongLong());\n\tconst auto weak = base::make_weak(controller);\n\tcontroller->resolveCollectible(peerId, username, [=](const QString &e) {\n\t\tif (e == u\"COLLECTIBLE_NOT_FOUND\"_q) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tTextUtilities::SetClipboardText({\n\t\t\t\t\tstrong->session().createInternalLinkFull(username)\n\t\t\t\t});\n\t\t\t\tstrong->showToast(tr::lng_username_copied(tr::now));\n\t\t\t}\n\t\t}\n\t});\n\treturn true;\n}\n\nbool CopyUsernameLink(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto username = match->captured(1);\n\tTextUtilities::SetClipboardText({\n\t\tcontroller->session().createInternalLinkFull(username)\n\t});\n\tcontroller->showToast(tr::lng_username_copied(tr::now));\n\treturn true;\n}\n\nbool CopyUsername(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto username = match->captured(1);\n\tTextUtilities::SetClipboardText({ '@' + username });\n\tcontroller->showToast(tr::lng_username_text_copied(tr::now));\n\treturn true;\n}\n\nbool EditPaidMessagesFee(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto peerId = PeerId(match->captured(1).toULongLong());\n\tif (const auto id = peerToChannel(peerId)) {\n\t\tconst auto channel = controller->session().data().channelLoaded(id);\n\t\tif (channel && channel->canEditPermissions()) {\n\t\t\tShowEditChatPermissions(controller, channel);\n\t\t}\n\t} else {\n\t\tcontroller->show(Box(EditMessagesPrivacyBox, controller, QString()));\n\t}\n\treturn true;\n}\n\nbool ShowCommonGroups(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto peerId = PeerId(match->captured(1).toULongLong());\n\tif (const auto id = peerToUser(peerId)) {\n\t\tconst auto user = controller->session().data().userLoaded(id);\n\t\tif (user) {\n\t\t\tcontroller->showSection(\n\t\t\t\tstd::make_shared<Info::Memento>(\n\t\t\t\t\tuser,\n\t\t\t\t\tInfo::Section::Type::CommonGroups));\n\t\t}\n\t}\n\treturn true;\n}\n\nbool EditPeer(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto peerId = PeerId(match->captured(1).toULongLong());\n\tif (const auto peer = controller->session().data().peerLoaded(peerId)) {\n\t\tcontroller->showEditPeerBox(peer);\n\t}\n\treturn true;\n}\n\nbool ShowStarsExamples(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tcontroller->show(Dialogs::StarsExamplesBox(controller));\n\treturn true;\n}\n\nbool ShowPopularAppsAbout(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tcontroller->show(Dialogs::PopularAppsAboutBox(controller));\n\treturn true;\n}\n\nvoid ExportTestChatTheme(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<const Data::CloudTheme*> theme) {\n\tconst auto session = &controller->session();\n\tconst auto show = controller->uiShow();\n\tconst auto inputSettings = [&](Data::CloudThemeType type)\n\t-> std::optional<MTPInputThemeSettings> {\n\t\tconst auto i = theme->settings.find(type);\n\t\tif (i == end(theme->settings)) {\n\t\t\tshow->showToast(u\"Something went wrong :(\"_q);\n\t\t\treturn std::nullopt;\n\t\t}\n\t\tconst auto &fields = i->second;\n\t\tif (!fields.paper\n\t\t\t|| !fields.paper->isPattern()\n\t\t\t|| fields.paper->backgroundColors().empty()\n\t\t\t|| !fields.paper->hasShareUrl()) {\n\t\t\tshow->showToast(u\"Something went wrong :(\"_q);\n\t\t\treturn std::nullopt;\n\t\t}\n\t\tconst auto &bg = fields.paper->backgroundColors();\n\t\tconst auto url = fields.paper->shareUrl(&show->session());\n\t\tconst auto from = url.indexOf(\"bg/\");\n\t\tconst auto till = url.indexOf(\"?\");\n\t\tif (from < 0 || till <= from) {\n\t\t\tshow->showToast(u\"Bad WallPaper link: \"_q + url);\n\t\t\treturn std::nullopt;\n\t\t}\n\n\t\tusing Setting = MTPDinputThemeSettings::Flag;\n\t\tusing Paper = MTPDwallPaperSettings::Flag;\n\t\tconst auto color = [](const QColor &color) {\n\t\t\tconst auto red = color.red();\n\t\t\tconst auto green = color.green();\n\t\t\tconst auto blue = color.blue();\n\t\t\treturn int(((uint32(red) & 0xFFU) << 16)\n\t\t\t\t| ((uint32(green) & 0xFFU) << 8)\n\t\t\t\t| (uint32(blue) & 0xFFU));\n\t\t};\n\t\tconst auto colors = [&](const std::vector<QColor> &colors) {\n\t\t\tauto result = QVector<MTPint>();\n\t\t\tresult.reserve(colors.size());\n\t\t\tfor (const auto &single : colors) {\n\t\t\t\tresult.push_back(MTP_int(color(single)));\n\t\t\t}\n\t\t\treturn result;\n\t\t};\n\t\tconst auto slug = url.mid(from + 3, till - from - 3);\n\t\tconst auto settings = Setting::f_wallpaper\n\t\t\t| Setting::f_wallpaper_settings\n\t\t\t| (fields.outgoingAccentColor\n\t\t\t\t? Setting::f_outbox_accent_color\n\t\t\t\t: Setting(0))\n\t\t\t| (!fields.outgoingMessagesColors.empty()\n\t\t\t\t? Setting::f_message_colors\n\t\t\t\t: Setting(0));\n\t\tconst auto papers = Paper::f_background_color\n\t\t\t| Paper::f_intensity\n\t\t\t| (bg.size() > 1\n\t\t\t\t? Paper::f_second_background_color\n\t\t\t\t: Paper(0))\n\t\t\t| (bg.size() > 2\n\t\t\t\t? Paper::f_third_background_color\n\t\t\t\t: Paper(0))\n\t\t\t| (bg.size() > 3\n\t\t\t\t? Paper::f_fourth_background_color\n\t\t\t\t: Paper(0));\n\t\treturn MTP_inputThemeSettings(\n\t\t\tMTP_flags(settings),\n\t\t\t((type == Data::CloudThemeType::Dark)\n\t\t\t\t? MTP_baseThemeTinted()\n\t\t\t\t: MTP_baseThemeClassic()),\n\t\t\tMTP_int(color(fields.accentColor)),\n\t\t\tMTP_int(color(fields.outgoingAccentColor.value_or(\n\t\t\t\tQt::black))),\n\t\t\tMTP_vector<MTPint>(colors(fields.outgoingMessagesColors)),\n\t\t\tMTP_inputWallPaperSlug(MTP_string(slug)),\n\t\t\tMTP_wallPaperSettings(\n\t\t\t\tMTP_flags(papers),\n\t\t\t\tMTP_int(color(bg[0])),\n\t\t\t\tMTP_int(color(bg.size() > 1 ? bg[1] : Qt::black)),\n\t\t\t\tMTP_int(color(bg.size() > 2 ? bg[2] : Qt::black)),\n\t\t\t\tMTP_int(color(bg.size() > 3 ? bg[3] : Qt::black)),\n\t\t\t\tMTP_int(fields.paper->patternIntensity()),\n\t\t\t\tMTP_int(0), // rotation\n\t\t\t\tMTPstring())); // emoticon\n\t};\n\tconst auto light = inputSettings(Data::CloudThemeType::Light);\n\tif (!light) {\n\t\treturn;\n\t}\n\tconst auto dark = inputSettings(Data::CloudThemeType::Dark);\n\tif (!dark) {\n\t\treturn;\n\t}\n\tsession->api().request(MTPaccount_CreateTheme(\n\t\tMTP_flags(MTPaccount_CreateTheme::Flag::f_settings),\n\t\tMTP_string(Window::Theme::GenerateSlug()),\n\t\tMTP_string(theme->title + \" Desktop\"),\n\t\tMTPInputDocument(),\n\t\tMTP_vector<MTPInputThemeSettings>(QVector<MTPInputThemeSettings>{\n\t\t\t*light,\n\t\t\t*dark,\n\t\t})\n\t)).done([=](const MTPTheme &result) {\n\t\tconst auto slug = Data::CloudTheme::Parse(session, result, true).slug;\n\t\tQGuiApplication::clipboard()->setText(\n\t\t\tsession->createInternalLinkFull(\"addtheme/\" + slug));\n\t\tshow->showToast(tr::lng_background_link_copied(tr::now));\n\t}).fail([=](const MTP::Error &error) {\n\t\tshow->showToast(u\"Error: \"_q + error.type());\n\t}).send();\n}\n\nbool ResolveTestChatTheme(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto params = url_parse_params(\n\t\tmatch->captured(1),\n\t\tqthelp::UrlParamNameTransform::ToLower);\n\tif (const auto history = controller->activeChatCurrent().history()) {\n\t\tcontroller->clearCachedChatThemes();\n\t\tconst auto owner = &history->owner();\n\t\tconst auto theme = owner->cloudThemes().updateThemeFromLink(\n\t\t\thistory->peer->themeToken(),\n\t\t\tparams);\n\t\tif (theme) {\n\t\t\tif (!params[\"export\"].isEmpty()) {\n\t\t\t\tExportTestChatTheme(controller, &*theme);\n\t\t\t}\n\t\t\tconst auto recache = [&](Data::CloudThemeType type) {\n\t\t\t\t[[maybe_unused]] auto value = theme->settings.contains(type)\n\t\t\t\t\t? controller->cachedChatThemeValue(\n\t\t\t\t\t\t*theme,\n\t\t\t\t\t\tData::WallPaper(0),\n\t\t\t\t\t\ttype)\n\t\t\t\t\t: nullptr;\n\t\t\t};\n\t\t\trecache(Data::CloudThemeType::Dark);\n\t\t\trecache(Data::CloudThemeType::Light);\n\t\t}\n\t}\n\treturn true;\n}\n\nbool ResolveInvoice(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto params = url_parse_params(\n\t\tmatch->captured(1),\n\t\tqthelp::UrlParamNameTransform::ToLower);\n\tconst auto slug = params.value(u\"slug\"_q);\n\tif (slug.isEmpty()) {\n\t\treturn false;\n\t}\n\tconst auto window = &controller->window();\n\tPayments::CheckoutProcess::Start(\n\t\t&controller->session(),\n\t\tslug,\n\t\tcrl::guard(window, [=](auto) { window->activate(); }),\n\t\tPayments::ProcessNonPanelPaymentFormFactory(controller));\n\treturn true;\n}\n\nbool ResolvePremiumOffer(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto params = url_parse_params(\n\t\tmatch->captured(1).mid(1),\n\t\tqthelp::UrlParamNameTransform::ToLower);\n\tconst auto refAddition = params.value(u\"ref\"_q);\n\tconst auto ref = \"deeplink\"\n\t\t+ (refAddition.isEmpty() ? QString() : '_' + refAddition);\n\t::Settings::ShowPremium(controller, ref);\n\tcontroller->window().activate();\n\treturn true;\n}\n\nbool ResolvePremiumMultigift(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tUi::ChooseStarGiftRecipient(controller);\n\tcontroller->window().activate();\n\treturn true;\n}\n\nbool ResolveLoginCode(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tconst auto loginCode = match->captured(2);\n\tconst auto &domain = Core::App().domain();\n\tif (loginCode.isEmpty() || (!controller && !domain.started())) {\n\t\treturn false;\n\t};\n\t(controller\n\t\t? controller->session().account()\n\t\t: domain.active()).handleLoginCode(loginCode);\n\tif (controller) {\n\t\tcontroller->window().activate();\n\t} else if (const auto window = Core::App().activeWindow()) {\n\t\twindow->activate();\n\t}\n\treturn true;\n}\n\nbool ResolveBoost(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto params = url_parse_params(\n\t\tmatch->captured(1),\n\t\tqthelp::UrlParamNameTransform::ToLower);\n\tconst auto domainParam = params.value(u\"domain\"_q);\n\tconst auto channelParam = params.contains(u\"c\"_q)\n\t\t? params.value(u\"c\"_q)\n\t\t: params.value(u\"channel\"_q);\n\n\tconst auto myContext = context.value<ClickHandlerContext>();\n\tcontroller->window().activate();\n\tcontroller->showPeerByLink(Window::PeerByLinkInfo{\n\t\t.usernameOrId = (!domainParam.isEmpty()\n\t\t\t? std::variant<QString, ChannelId>(domainParam)\n\t\t\t: ChannelId(BareId(channelParam.toULongLong()))),\n\t\t.resolveType = Window::ResolveType::Boost,\n\t\t.clickFromMessageId = myContext.itemId,\n\t});\n\treturn true;\n}\n\nbool ResolveTopUp(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto params = url_parse_params(\n\t\tmatch->captured(1),\n\t\tqthelp::UrlParamNameTransform::ToLower);\n\tconst auto amount = std::clamp(\n\t\tparams.value(u\"balance\"_q).toULongLong(),\n\t\tqulonglong(1),\n\t\tqulonglong(1'000'000));\n\tconst auto purpose = params.value(u\"purpose\"_q);\n\tconst auto weak = base::make_weak(controller);\n\tconst auto done = [=](::Settings::SmallBalanceResult result) {\n\t\tif (result == ::Settings::SmallBalanceResult::Already) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tconst auto filter = [=](const auto &...) {\n\t\t\t\t\tstrong->showSettings(::Settings::CreditsId());\n\t\t\t\t\treturn false;\n\t\t\t\t};\n\t\t\t\tstrong->showToast(Ui::Toast::Config{\n\t\t\t\t\t.text = tr::lng_credits_enough(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\ttr::link(\n\t\t\t\t\t\t\ttr::bold(\n\t\t\t\t\t\t\t\ttr::lng_credits_enough_link(tr::now))),\n\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t.filter = filter,\n\t\t\t\t\t.duration = 4 * crl::time(1000),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t};\n\t::Settings::MaybeRequestBalanceIncrease(\n\t\tcontroller->uiShow(),\n\t\tamount,\n\t\t::Settings::SmallBalanceDeepLink{ .purpose = purpose },\n\t\tdone);\n\treturn true;\n}\n\n\nbool ResolveChatLink(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto myContext = context.value<ClickHandlerContext>();\n\tconst auto slug = match->captured(1);\n\tcontroller->window().activate();\n\tcontroller->showPeerByLink(Window::PeerByLinkInfo{\n\t\t.chatLinkSlug = match->captured(1),\n\t\t.clickFromMessageId = myContext.itemId,\n\t\t.clickFromBotWebviewContext = myContext.botWebviewContext,\n\t});\n\treturn true;\n}\n\nbool ResolveUniqueGift(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto slug = match->captured(1);\n\tif (slug.isEmpty()) {\n\t\treturn false;\n\t}\n\tResolveAndShowUniqueGift(controller->uiShow(), slug);\n\treturn true;\n}\n\nbool ResolveGiftAuction(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto slug = match->captured(1);\n\tif (slug.isEmpty()) {\n\t\treturn false;\n\t}\n\tcontroller->showStarGiftAuction(slug);\n\treturn true;\n}\n\nbool ResolveConferenceCall(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto slug = match->captured(1);\n\tif (slug.isEmpty()) {\n\t\treturn false;\n\t}\n\tconst auto myContext = context.value<ClickHandlerContext>();\n\tcontroller->window().activate();\n\tcontroller->resolveConferenceCall(match->captured(1), myContext.itemId);\n\treturn true;\n}\n\nbool ResolveStarsSettings(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tcontroller->showSettings(::Settings::CreditsId());\n\tcontroller->window().activate();\n\treturn true;\n}\n\nbool ResolveTonSettings(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tcontroller->showSettings(::Settings::CurrencyId());\n\tcontroller->window().activate();\n\treturn true;\n}\n\nbool ResolveOAuth(\n\t\tWindow::SessionController *controller,\n\t\tconst Match &match,\n\t\tconst QVariant &context) {\n\tif (!controller) {\n\t\treturn false;\n\t}\n\tconst auto params = url_parse_params(\n\t\tmatch->captured(1),\n\t\tqthelp::UrlParamNameTransform::ToLower);\n\tconst auto token = params.value(u\"token\"_q);\n\tif (token.isEmpty()) {\n\t\treturn false;\n\t}\n\tUrlAuthBox::ActivateUrl(\n\t\tcontroller->uiShow(),\n\t\t&controller->session(),\n\t\tu\"tg://oauth?token=\"_q + token,\n\t\tcontext);\n\treturn true;\n}\n\n} // namespace\n\nbool TryRouterForLocalUrl(\n\t\tWindow::SessionController *controller,\n\t\tconst QString &command) {\n\treturn DeepLinks::Router::Instance().tryHandle(controller, command);\n}\n\nconst std::vector<LocalUrlHandler> &LocalUrlHandlers() {\n\tstatic auto Result = std::vector<LocalUrlHandler>{\n\t\t{\n\t\t\tu\"^join/?\\\\?invite=([a-zA-Z0-9\\\\.\\\\_\\\\-]+)(&|$)\"_q,\n\t\t\tJoinGroupByHash\n\t\t},\n\t\t{\n\t\t\tu\"^addlist/?\\\\?slug=([a-zA-Z0-9\\\\.\\\\_\\\\-]+)(&|$)\"_q,\n\t\t\tJoinFilterBySlug\n\t\t},\n\t\t{\n\t\t\tu\"^(addstickers|addemoji)/?\\\\?set=([a-zA-Z0-9\\\\.\\\\_]+)(&|$)\"_q,\n\t\t\tShowStickerSet\n\t\t},\n\t\t{\n\t\t\tu\"^addtheme/?\\\\?slug=([a-zA-Z0-9\\\\.\\\\_]+)(&|$)\"_q,\n\t\t\tShowTheme\n\t\t},\n\t\t{\n\t\t\tu\"^setlanguage/?(\\\\?lang=([a-zA-Z0-9\\\\.\\\\_\\\\-]+))?(&|$)\"_q,\n\t\t\tSetLanguage\n\t\t},\n\t\t{\n\t\t\tu\"^msg_url/?\\\\?(.+)(#|$)\"_q,\n\t\t\tShareUrl\n\t\t},\n\t\t{\n\t\t\tu\"^confirmphone/?\\\\?(.+)(#|$)\"_q,\n\t\t\tConfirmPhone\n\t\t},\n\t\t{\n\t\t\tu\"^socks/?\\\\?(.+)(#|$)\"_q,\n\t\t\tApplySocksProxy\n\t\t},\n\t\t{\n\t\t\tu\"^proxy/?\\\\?(.+)(#|$)\"_q,\n\t\t\tApplyMtprotoProxy\n\t\t},\n\t\t{\n\t\t\tu\"^passport/?\\\\?(.+)(#|$)\"_q,\n\t\t\tShowPassport\n\t\t},\n\t\t{\n\t\t\tu\"^bg/?\\\\?(.+)(#|$)\"_q,\n\t\t\tShowWallPaper\n\t\t},\n\t\t{\n\t\t\tu\"^resolve/?\\\\?(.+)(#|$)\"_q,\n\t\t\tResolveUsernameOrPhone\n\t\t},\n\t\t{\n\t\t\tu\"^privatepost/?\\\\?(.+)(#|$)\"_q,\n\t\t\tResolvePrivatePost\n\t\t},\n\t\t{\n\t\t\tu\"^test_chat_theme/?\\\\?(.+)(#|$)\"_q,\n\t\t\tResolveTestChatTheme,\n\t\t},\n\t\t{\n\t\t\tu\"^invoice/?\\\\?(.+)(#|$)\"_q,\n\t\t\tResolveInvoice,\n\t\t},\n\t\t{\n\t\t\tu\"^premium_offer/?(\\\\?.+)?(#|$)\"_q,\n\t\t\tResolvePremiumOffer,\n\t\t},\n\t\t{\n\t\t\tu\"^premium_multigift/?\\\\?(.+)(#|$)\"_q,\n\t\t\tResolvePremiumMultigift,\n\t\t},\n\t\t{\n\t\t\tu\"^login/?(\\\\?code=([0-9]+))(&|$)\"_q,\n\t\t\tResolveLoginCode\n\t\t},\n\t\t{\n\t\t\tu\"^boost/?\\\\?(.+)(#|$)\"_q,\n\t\t\tResolveBoost,\n\t\t},\n\t\t{\n\t\t\tu\"^message/?\\\\?slug=([a-zA-Z0-9\\\\.\\\\_\\\\-]+)(&|$)\"_q,\n\t\t\tResolveChatLink\n\t\t},\n\t\t{\n\t\t\tu\"^stars_topup/?\\\\?(.+)(#|$)\"_q,\n\t\t\tResolveTopUp\n\t\t},\n\t\t{\n\t\t\tu\"^nft/?\\\\?slug=([a-zA-Z0-9\\\\.\\\\_\\\\-]+)(&|$)\"_q,\n\t\t\tResolveUniqueGift\n\t\t},\n\t\t{\n\t\t\tu\"^stargift_auction/?\\\\?slug=([a-zA-Z0-9\\\\.\\\\_\\\\-]+)(&|$)\"_q,\n\t\t\tResolveGiftAuction\n\t\t},\n\t\t{\n\t\t\tu\"^call/?\\\\?slug=([a-zA-Z0-9\\\\.\\\\_\\\\-]+)(&|$)\"_q,\n\t\t\tResolveConferenceCall\n\t\t},\n\t\t{\n\t\t\tu\"^stars/?(^\\\\?.*)?(#|$)\"_q,\n\t\t\tResolveStarsSettings\n\t\t},\n\t\t{\n\t\t\tu\"^ton/?(^\\\\?.*)?(#|$)\"_q,\n\t\t\tResolveTonSettings\n\t\t},\n\t\t{\n\t\t\tu\"^oauth/?\\\\?(.+)(#|$)\"_q,\n\t\t\tResolveOAuth\n\t\t},\n\t\t{\n\t\t\tu\"^([^\\\\?]+)(\\\\?|#|$)\"_q,\n\t\t\tHandleUnknown\n\t\t},\n\t};\n\treturn Result;\n}\n\nconst std::vector<LocalUrlHandler> &InternalUrlHandlers() {\n\tstatic auto Result = std::vector<LocalUrlHandler>{\n\t\t{\n\t\t\tu\"^media_timestamp/?\\\\?base=([a-zA-Z0-9\\\\.\\\\_\\\\-]+)&t=(\\\\d+)(&|$)\"_q,\n\t\t\tOpenMediaTimestamp\n\t\t},\n\t\t{\n\t\t\tu\"^show_invite_link/?\\\\?link=([a-zA-Z0-9_\\\\+\\\\/\\\\=\\\\-]+)(&|$)\"_q,\n\t\t\tShowInviteLink\n\t\t},\n\t\t{\n\t\t\tu\"^url:(.+)$\"_q,\n\t\t\tOpenExternalLink\n\t\t},\n\t\t{\n\t\t\tu\"^copy:(.+)$\"_q,\n\t\t\tCopyPeerId\n\t\t},\n\t\t{\n\t\t\tu\"^about_tags$\"_q,\n\t\t\tShowSearchTagsPromo\n\t\t},\n\t\t{\n\t\t\tu\"^edit_birthday(.*)$\"_q,\n\t\t\tShowEditBirthday,\n\t\t},\n\t\t{\n\t\t\tu\"^edit_privacy_birthday(.*)$\"_q,\n\t\t\tShowEditBirthdayPrivacy,\n\t\t},\n\t\t{\n\t\t\tu\"^edit_personal_channel(?::(?:(\\\\d{2,})|(remove)))?$\"_q,\n\t\t\tShowEditPersonalChannel,\n\t\t},\n\t\t{\n\t\t\tu\"^collectible_phone/([\\\\+0-9\\\\-\\\\s]+)@([0-9]+)$\"_q,\n\t\t\tShowCollectiblePhone,\n\t\t},\n\t\t{\n\t\t\tu\"^collectible_username/([a-zA-Z0-9\\\\-\\\\_\\\\.]+)@([0-9]+)$\"_q,\n\t\t\tShowCollectibleUsername,\n\t\t},\n\t\t{\n\t\t\tu\"^username_link/([a-zA-Z0-9\\\\-\\\\_\\\\.]+)@([0-9]+)$\"_q,\n\t\t\tCopyUsernameLink,\n\t\t},\n\t\t{\n\t\t\tu\"^username_regular/([a-zA-Z0-9\\\\-\\\\_\\\\.]+)@([0-9]+)$\"_q,\n\t\t\tCopyUsername,\n\t\t},\n\t\t{\n\t\t\tu\"^edit_paid_messages_fee/([0-9]+)$\"_q,\n\t\t\tEditPaidMessagesFee,\n\t\t},\n\t\t{\n\t\t\tu\"^common_groups/([0-9]+)$\"_q,\n\t\t\tShowCommonGroups,\n\t\t},\n\t\t{\n\t\t\tu\"^edit_peer/([0-9]+)$\"_q,\n\t\t\tEditPeer,\n\t\t},\n\t\t{\n\t\t\tu\"^stars_examples$\"_q,\n\t\t\tShowStarsExamples,\n\t\t},\n\t\t{\n\t\t\tu\"^about_popular_apps$\"_q,\n\t\t\tShowPopularAppsAbout,\n\t\t},\n\t};\n\treturn Result;\n}\n\nQString TryConvertUrlToLocal(QString url) {\n\tif (url.size() > 8192) {\n\t\turl = url.mid(0, 8192);\n\t}\n\n\tusing namespace qthelp;\n\tauto matchOptions = RegExOption::CaseInsensitive;\n\tauto tonsiteMatch = (url.indexOf(u\".ton\") >= 0)\n\t\t? regex_match(u\"^(https?://)?[^/@:]+\\\\.ton($|/)\"_q, url, matchOptions)\n\t\t: RegularExpressionMatch(QRegularExpressionMatch());\n\tif (tonsiteMatch) {\n\t\tconst auto protocol = tonsiteMatch->captured(1);\n\t\treturn u\"tonsite://\"_q + url.mid(protocol.size());\n\t}\n\tauto subdomainMatch = regex_match(u\"^(https?://)?([a-zA-Z0-9\\\\_]+)\\\\.t\\\\.me(/\\\\d+)?/?(\\\\?.+)?\"_q, url, matchOptions);\n\tif (subdomainMatch) {\n\t\tconst auto name = subdomainMatch->captured(2);\n\t\tif (name.size() > 1 && name != \"www\") {\n\t\t\tconst auto result = TryConvertUrlToLocal(\n\t\t\t\tsubdomainMatch->captured(1)\n\t\t\t\t+ \"t.me/\"\n\t\t\t\t+ name\n\t\t\t\t+ subdomainMatch->captured(3)\n\t\t\t\t+ subdomainMatch->captured(4));\n\t\t\treturn result.startsWith(\"tg://resolve?domain=\")\n\t\t\t\t? result\n\t\t\t\t: url;\n\t\t}\n\t}\n\tauto telegramMeMatch = regex_match(u\"^(https?://)?(www\\\\.)?(telegram\\\\.(me|dog)|t\\\\.me)/(.+)$\"_q, url, matchOptions);\n\tif (telegramMeMatch) {\n\t\tconst auto query = telegramMeMatch->capturedView(5);\n\t\tif (const auto phoneMatch = regex_match(u\"^\\\\+([0-9]+)(\\\\?|$)\"_q, query, matchOptions)) {\n\t\t\tconst auto params = query.mid(phoneMatch->captured(0).size()).toString();\n\t\t\treturn u\"tg://resolve?phone=\"_q + phoneMatch->captured(1) + (params.isEmpty() ? QString() : '&' + params);\n\t\t} else if (const auto joinChatMatch = regex_match(u\"^(joinchat/|\\\\+|\\\\%20)([a-zA-Z0-9\\\\.\\\\_\\\\-]+)(\\\\?|$)\"_q, query, matchOptions)) {\n\t\t\treturn u\"tg://join?invite=\"_q + url_encode(joinChatMatch->captured(2));\n\t\t} else if (const auto joinFilterMatch = regex_match(u\"^(addlist/)([a-zA-Z0-9\\\\.\\\\_\\\\-]+)(\\\\?|$)\"_q, query, matchOptions)) {\n\t\t\treturn u\"tg://addlist?slug=\"_q + url_encode(joinFilterMatch->captured(2));\n\t\t} else if (const auto stickerSetMatch = regex_match(u\"^(addstickers|addemoji)/([a-zA-Z0-9\\\\.\\\\_]+)(\\\\?|$)\"_q, query, matchOptions)) {\n\t\t\treturn u\"tg://\"_q + stickerSetMatch->captured(1) + \"?set=\" + url_encode(stickerSetMatch->captured(2));\n\t\t} else if (const auto themeMatch = regex_match(u\"^addtheme/([a-zA-Z0-9\\\\.\\\\_]+)(\\\\?|$)\"_q, query, matchOptions)) {\n\t\t\treturn u\"tg://addtheme?slug=\"_q + url_encode(themeMatch->captured(1));\n\t\t} else if (const auto languageMatch = regex_match(u\"^setlanguage/([a-zA-Z0-9\\\\.\\\\_\\\\-]+)(\\\\?|$)\"_q, query, matchOptions)) {\n\t\t\treturn u\"tg://setlanguage?lang=\"_q + url_encode(languageMatch->captured(1));\n\t\t} else if (const auto shareUrlMatch = regex_match(u\"^share/url/?\\\\?(.+)$\"_q, query, matchOptions)) {\n\t\t\treturn u\"tg://msg_url?\"_q + shareUrlMatch->captured(1);\n\t\t} else if (const auto confirmPhoneMatch = regex_match(u\"^confirmphone/?\\\\?(.+)\"_q, query, matchOptions)) {\n\t\t\treturn u\"tg://confirmphone?\"_q + confirmPhoneMatch->captured(1);\n\t\t} else if (const auto ivMatch = regex_match(u\"^iv/?\\\\?(.+)(#|$)\"_q, query, matchOptions)) {\n\t\t\t//\n\t\t\t// We need to show our t.me page, not the url directly.\n\t\t\t//\n\t\t\t//auto params = url_parse_params(ivMatch->captured(1), UrlParamNameTransform::ToLower);\n\t\t\t//auto previewedUrl = params.value(u\"url\"_q);\n\t\t\t//if (previewedUrl.startsWith(u\"http://\"_q, Qt::CaseInsensitive)\n\t\t\t//\t|| previewedUrl.startsWith(u\"https://\"_q, Qt::CaseInsensitive)) {\n\t\t\t//\treturn previewedUrl;\n\t\t\t//}\n\t\t\treturn url;\n\t\t} else if (const auto socksMatch = regex_match(u\"^socks/?\\\\?(.+)(#|$)\"_q, query, matchOptions)) {\n\t\t\treturn u\"tg://socks?\"_q + socksMatch->captured(1);\n\t\t} else if (const auto proxyMatch = regex_match(u\"^proxy/?\\\\?(.+)(#|$)\"_q, query, matchOptions)) {\n\t\t\treturn u\"tg://proxy?\"_q + proxyMatch->captured(1);\n\t\t} else if (const auto invoiceMatch = regex_match(u\"^(invoice/|\\\\$)([a-zA-Z0-9_\\\\-]+)(\\\\?|#|$)\"_q, query, matchOptions)) {\n\t\t\treturn u\"tg://invoice?slug=\"_q + invoiceMatch->captured(2);\n\t\t} else if (const auto bgMatch = regex_match(u\"^bg/([a-zA-Z0-9\\\\.\\\\_\\\\-\\\\~]+)(\\\\?(.+)?)?$\"_q, query, matchOptions)) {\n\t\t\tconst auto params = bgMatch->captured(3);\n\t\t\tconst auto bg = bgMatch->captured(1);\n\t\t\tconst auto type = regex_match(u\"^[a-fA-F0-9]{6}^\"_q, bg)\n\t\t\t\t? \"color\"\n\t\t\t\t: (regex_match(u\"^[a-fA-F0-9]{6}\\\\-[a-fA-F0-9]{6}$\"_q, bg)\n\t\t\t\t\t|| regex_match(u\"^[a-fA-F0-9]{6}(\\\\~[a-fA-F0-9]{6}){1,3}$\"_q, bg))\n\t\t\t\t? \"gradient\"\n\t\t\t\t: \"slug\";\n\t\t\treturn u\"tg://bg?\"_q + type + '=' + bg + (params.isEmpty() ? QString() : '&' + params);\n\t\t} else if (const auto chatlinkMatch = regex_match(u\"^m/([a-zA-Z0-9\\\\.\\\\_\\\\-]+)(\\\\?|$)\"_q, query, matchOptions)) {\n\t\t\tconst auto slug = chatlinkMatch->captured(1);\n\t\t\treturn u\"tg://message?slug=\"_q + slug;\n\t\t} else if (const auto nftMatch = regex_match(u\"^nft/([a-zA-Z0-9\\\\.\\\\_\\\\-]+)(\\\\?|$)\"_q, query, matchOptions)) {\n\t\t\tconst auto slug = nftMatch->captured(1);\n\t\t\treturn u\"tg://nft?slug=\"_q + slug;\n\t\t} else if (const auto auctionMatch = regex_match(u\"^auction/([a-zA-Z0-9\\\\.\\\\_\\\\-]+)(\\\\?|$)\"_q, query, matchOptions)) {\n\t\t\tconst auto slug = auctionMatch->captured(1);\n\t\t\treturn u\"tg://stargift_auction?slug=\"_q + slug;\n\t\t} else if (const auto callMatch = regex_match(u\"^call/([a-zA-Z0-9\\\\.\\\\_\\\\-]+)(\\\\?|$)\"_q, query, matchOptions)) {\n\t\t\tconst auto slug = callMatch->captured(1);\n\t\t\treturn u\"tg://call?slug=\"_q + slug;\n\t\t} else if (const auto newbotMatch = regex_match(u\"^newbot/([a-zA-Z0-9\\\\.\\\\_]+)(/([a-zA-Z0-9\\\\.\\\\_]*))?(/?\\\\?(.+))?$\"_q, query, matchOptions)) {\n\t\t\tconst auto manager = newbotMatch->captured(1);\n\t\t\tconst auto username = newbotMatch->captured(3);\n\t\t\tconst auto params = newbotMatch->captured(5);\n\t\t\tauto result = u\"tg://newbot?manager=\"_q + url_encode(manager);\n\t\t\tif (!username.isEmpty()) {\n\t\t\t\tresult += u\"&username=\"_q + url_encode(username);\n\t\t\t}\n\t\t\tif (!params.isEmpty()) {\n\t\t\t\tresult += '&' + params;\n\t\t\t}\n\t\t\treturn result;\n\t\t} else if (const auto privateMatch = regex_match(u\"^\"\n\t\t\t\"c/(\\\\-?\\\\d+)\"\n\t\t\t\"(\"\n\t\t\t\t\"/?\\\\?|\"\n\t\t\t\t\"/?$|\"\n\t\t\t\t\"/\\\\d+/?(\\\\?|$)|\"\n\t\t\t\t\"/\\\\d+/\\\\d+/?(\\\\?|$)\"\n\t\t\t\")\"_q, query, matchOptions)) {\n\t\t\tconst auto channel = privateMatch->captured(1);\n\t\t\tconst auto params = query.mid(privateMatch->captured(0).size()).toString();\n\t\t\tif (params.indexOf(\"boost\", 0, Qt::CaseInsensitive) >= 0\n\t\t\t\t&& params.toLower().split('&').contains(u\"boost\"_q)) {\n\t\t\t\treturn u\"tg://boost?channel=\"_q + channel;\n\t\t\t}\n\t\t\tconst auto base = u\"tg://privatepost?channel=\"_q + channel;\n\t\t\tauto added = QString();\n\t\t\tif (const auto threadPostMatch = regex_match(u\"^/(\\\\d+)/(\\\\d+)(/?\\\\?|/?$)\"_q, privateMatch->captured(2))) {\n\t\t\t\tadded = u\"&topic=%1&post=%2\"_q.arg(threadPostMatch->captured(1), threadPostMatch->captured(2));\n\t\t\t} else if (const auto postMatch = regex_match(u\"^/(\\\\d+)(/?\\\\?|/?$)\"_q, privateMatch->captured(2))) {\n\t\t\t\tadded = u\"&post=\"_q + postMatch->captured(1);\n\t\t\t}\n\t\t\treturn base + added + (params.isEmpty() ? QString() : '&' + params);\n\t\t} else if (const auto usernameMatch = regex_match(u\"^\"\n\t\t\t\"([a-zA-Z0-9\\\\.\\\\_]+)\"\n\t\t\t\"(\"\n\t\t\t\t\"/?\\\\?|\"\n\t\t\t\t\"/?$|\"\n\t\t\t\t\"/[a-zA-Z0-9\\\\.\\\\_\\\\-]+/?(\\\\?|$)|\"\n\t\t\t\t\"/\\\\d+/?(\\\\?|$)|\"\n\t\t\t\t\"/s/(\\\\d+|live)/?(\\\\?|$)|\"\n\t\t\t\t\"/a/\\\\d+/?(\\\\?|$)|\"\n\t\t\t\t\"/c/\\\\d+/?(\\\\?|$)|\"\n\t\t\t\t\"/\\\\d+/\\\\d+/?(\\\\?|$)\"\n\t\t\t\")\"_q, query, matchOptions)) {\n\t\t\tconst auto domain = usernameMatch->captured(1);\n\t\t\tconst auto params = query.mid(usernameMatch->captured(0).size()).toString();\n\t\t\tif (params.indexOf(\"boost\", 0, Qt::CaseInsensitive) >= 0\n\t\t\t\t&& params.toLower().split('&').contains(u\"boost\"_q)) {\n\t\t\t\treturn u\"tg://boost?domain=\"_q + domain;\n\t\t\t} else if (domain == u\"boost\"_q) {\n\t\t\t\tif (const auto domainMatch = regex_match(u\"^/([a-zA-Z0-9\\\\.\\\\_]+)(/?\\\\?|/?$)\"_q, usernameMatch->captured(2))) {\n\t\t\t\t\treturn u\"tg://boost?domain=\"_q + domainMatch->captured(1);\n\t\t\t\t} else if (params.indexOf(\"c=\", 0, Qt::CaseInsensitive) >= 0) {\n\t\t\t\t\treturn u\"tg://boost?\"_q + params;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto base = u\"tg://resolve?domain=\"_q + url_encode(usernameMatch->captured(1));\n\t\t\tauto added = QString();\n\t\t\tif (const auto threadPostMatch = regex_match(u\"^/(\\\\d+)/(\\\\d+)(/?\\\\?|/?$)\"_q, usernameMatch->captured(2))) {\n\t\t\t\tadded = u\"&topic=%1&post=%2\"_q.arg(threadPostMatch->captured(1), threadPostMatch->captured(2));\n\t\t\t} else if (const auto postMatch = regex_match(u\"^/(\\\\d+)(/?\\\\?|/?$)\"_q, usernameMatch->captured(2))) {\n\t\t\t\tadded = u\"&post=\"_q + postMatch->captured(1);\n\t\t\t} else if (const auto storyMatch = regex_match(u\"^/s/(\\\\d+|live)(/?\\\\?|/?$)\"_q, usernameMatch->captured(2))) {\n\t\t\t\tadded = u\"&story=\"_q + storyMatch->captured(1);\n\t\t\t} else if (const auto albumMatch = regex_match(u\"^/a/(\\\\d+)(/?\\\\?|/?$)\"_q, usernameMatch->captured(2))) {\n\t\t\t\tadded = u\"&album=\"_q + albumMatch->captured(1);\n\t\t\t} else if (const auto collectionMatch = regex_match(u\"^/c/(\\\\d+)(/?\\\\?|/?$)\"_q, usernameMatch->captured(2))) {\n\t\t\t\tadded = u\"&collection=\"_q + collectionMatch->captured(1);\n\t\t\t} else if (const auto appNameMatch = regex_match(u\"^/([a-zA-Z0-9\\\\.\\\\_\\\\-]+)(/?\\\\?|/?$)\"_q, usernameMatch->captured(2))) {\n\t\t\t\tadded = u\"&appname=\"_q + appNameMatch->captured(1);\n\t\t\t}\n\t\t\treturn base + added + (params.isEmpty() ? QString() : '&' + params);\n\t\t}\n\t}\n\treturn url;\n}\n\nbool InternalPassportOrOAuthLink(const QString &url) {\n\tconst auto urlTrimmed = url.trimmed();\n\tif (!urlTrimmed.startsWith(u\"tg://\"_q, Qt::CaseInsensitive)) {\n\t\treturn false;\n\t}\n\tconst auto command = base::StringViewMid(urlTrimmed, u\"tg://\"_q.size());\n\n\tusing namespace qthelp;\n\tconst auto matchOptions = RegExOption::CaseInsensitive;\n\tconst auto authMatch = regex_match(\n\t\tu\"^passport/?\\\\?(.+)(#|$)\"_q,\n\t\tcommand,\n\t\tmatchOptions);\n\tconst auto oauthMatch = regex_match(\n\t\tu\"^oauth/?\\\\?(.+)(#|$)\"_q,\n\t\tcommand,\n\t\tmatchOptions);\n\tconst auto usernameMatch = regex_match(\n\t\tu\"^resolve/?\\\\?(.+)(#|$)\"_q,\n\t\tcommand,\n\t\tmatchOptions);\n\tauto usernameValue = QString();\n\tif (usernameMatch->hasMatch()) {\n\t\tconst auto params = url_parse_params(\n\t\t\tusernameMatch->captured(1),\n\t\t\tUrlParamNameTransform::ToLower);\n\t\tusernameValue = params.value(u\"domain\"_q);\n\t}\n\tconst auto authLegacy = (usernameValue == u\"telegrampassport\"_q);\n\tconst auto oauthLegacy = (usernameValue == u\"oauth\"_q);\n\treturn authMatch->hasMatch()\n\t\t|| oauthMatch->hasMatch()\n\t\t|| authLegacy\n\t\t|| oauthLegacy;\n}\n\nbool StartUrlRequiresActivate(const QString &url) {\n\treturn Core::App().passcodeLocked()\n\t\t? true\n\t\t: !InternalPassportOrOAuthLink(url);\n}\n\nvoid ResolveAndShowUniqueGift(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst QString &slug,\n\t\t::Settings::CreditsEntryBoxStyleOverrides st) {\n\tstruct Request {\n\t\tbase::weak_ptr<Main::Session> weak;\n\t\tQString slug;\n\t\tmtpRequestId id = 0;\n\t};\n\tstatic auto request = Request();\n\n\tconst auto session = &show->session();\n\tif (request.weak.get() == session && request.slug == slug) {\n\t\treturn;\n\t} else if (const auto strong = request.weak.get()) {\n\t\tstrong->api().request(request.id).cancel();\n\t}\n\trequest.weak = session;\n\trequest.slug = slug;\n\tconst auto clear = [=] {\n\t\tif (request.weak.get() == session && request.slug == slug) {\n\t\t\trequest = {};\n\t\t}\n\t};\n\trequest.id = session->api().request(\n\t\tMTPpayments_GetUniqueStarGift(MTP_string(slug))\n\t).done([=](const MTPpayments_UniqueStarGift &result) {\n\t\tclear();\n\n\t\tconst auto &data = result.data();\n\t\tsession->data().processUsers(data.vusers());\n\t\tif (const auto gift = Api::FromTL(session, data.vgift())) {\n\t\t\tCore::App().hideMediaView();\n\n\t\t\tusing namespace ::Settings;\n\t\t\tshow->show(Box(\n\t\t\t\tGlobalStarGiftBox,\n\t\t\t\tshow,\n\t\t\t\t*gift,\n\t\t\t\tStarGiftResaleInfo(),\n\t\t\t\tst));\n\t\t\tshow->activate();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tclear();\n\t\tif (!Ui::ShowGiftErrorToast(show, error)) {\n\t\t\tshow->showToast(u\"Error: \"_q + error.type());\n\t\t}\n\t}).send();\n}\n\nvoid ResolveAndShowUniqueGift(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst QString &slug) {\n\tResolveAndShowUniqueGift(std::move(show), slug, {});\n}\n\nTimeId ParseVideoTimestamp(QStringView value) {\n\tconst auto kExp = u\"^(?:(\\\\d+)h)?(?:(\\\\d+)m)?(?:(\\\\d+)s)?$\"_q;\n\tconst auto m = QRegularExpression(kExp).match(value);\n\treturn m.hasMatch()\n\t\t? (m.capturedView(1).toInt() * 3600\n\t\t\t+ m.capturedView(2).toInt() * 60\n\t\t\t+ m.capturedView(3).toInt())\n\t\t: value.toInt();\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/local_url_handlers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace qthelp {\nclass RegularExpressionMatch;\n} // namespace qthelp\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Settings {\nstruct CreditsEntryBoxStyleOverrides;\n} // namespace Settings\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Core {\n\nstruct LocalUrlHandler {\n\tQString expression;\n\tFn<bool(\n\t\tWindow::SessionController *controller,\n\t\tconst qthelp::RegularExpressionMatch &match,\n\t\tconst QVariant &context)> handler;\n};\n\n[[nodiscard]] bool TryRouterForLocalUrl(\n\tWindow::SessionController *controller,\n\tconst QString &command);\n\n[[nodiscard]] const std::vector<LocalUrlHandler> &LocalUrlHandlers();\n[[nodiscard]] const std::vector<LocalUrlHandler> &InternalUrlHandlers();\n\n[[nodiscard]] QString TryConvertUrlToLocal(QString url);\n\n[[nodiscard]] bool InternalPassportOrOAuthLink(const QString &url);\n\n[[nodiscard]] bool StartUrlRequiresActivate(const QString &url);\n\nvoid ResolveAndShowUniqueGift(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst QString &slug,\n\t::Settings::CreditsEntryBoxStyleOverrides st);\nvoid ResolveAndShowUniqueGift(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst QString &slug);\n\n[[nodiscard]] TimeId ParseVideoTimestamp(QStringView value);\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/mime_type.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/mime_type.h\"\n\n#include \"core/utils.h\"\n#include \"ui/image/image_prepare.h\"\n\n#include <QtCore/QMimeDatabase>\n#include <QtCore/QMimeData>\n\n#include <kurlmimedata.h>\n\nnamespace Core {\nnamespace {\n\n[[nodiscard]] bool IsImageFromFirefox(not_null<const QMimeData*> data) {\n\t// See https://bugs.telegram.org/c/6765/public\n\t// See https://github.com/telegramdesktop/tdesktop/issues/10564\n\t//\n\t// Usually we prefer pasting from URLs list instead of pasting from\n\t// image data, because sometimes a file is copied together with an\n\t// image data of its File Explorer thumbnail or smth like that. In\n\t// that case you end up sending this thumbnail instead of the file.\n\t//\n\t// But in case of \"Copy Image\" from Firefox on Windows we get both\n\t// URLs list with a file path to some Temp folder in the list and\n\t// the image data that was copied. The file is read slower + it may\n\t// have incorrect content in case the URL can't be accessed without\n\t// authorization. So in that case we want only image data and we\n\t// check for a special Firefox mime type to check for that case.\n\treturn data->hasFormat(u\"application/x-moz-nativeimage\"_q)\n\t\t&& data->hasImage();\n}\n\n[[nodiscard]] base::flat_set<QString> SplitExtensions(\n\t\tconst QString &joined) {\n\tconst auto list = joined.split(' ');\n\treturn base::flat_set<QString>(list.begin(), list.end());\n}\n\n} // namespace\n\nMimeType::MimeType(const QMimeType &type) : _typeStruct(type) {\n}\n\nMimeType::MimeType(Known type) : _type(type) {\n}\n\nQStringList MimeType::globPatterns() const {\n\tswitch (_type) {\n\tcase Known::WebP: return QStringList(u\"*.webp\"_q);\n\tcase Known::Tgs: return QStringList(u\"*.tgs\"_q);\n\tcase Known::Tgv: return QStringList(u\"*.tgv\"_q);\n\tcase Known::TDesktopTheme: return QStringList(u\"*.tdesktop-theme\"_q);\n\tcase Known::TDesktopPalette: return QStringList(u\"*.tdesktop-palette\"_q);\n\tdefault: break;\n\t}\n\treturn _typeStruct.globPatterns();\n}\n\nQString MimeType::filterString() const {\n\tswitch (_type) {\n\tcase Known::WebP: return u\"WebP image (*.webp)\"_q;\n\tcase Known::Tgs: return u\"Telegram sticker (*.tgs)\"_q;\n\tcase Known::Tgv: return u\"Wallpaper pattern (*.tgv)\"_q;\n\tcase Known::TDesktopTheme: return u\"Theme files (*.tdesktop-theme)\"_q;\n\tcase Known::TDesktopPalette: return u\"Palette files (*.tdesktop-palette)\"_q;\n\tdefault: break;\n\t}\n\treturn _typeStruct.filterString();\n}\n\nQString MimeType::name() const {\n\tswitch (_type) {\n\tcase Known::WebP: return u\"image/webp\"_q;\n\tcase Known::Tgs: return u\"application/x-tgsticker\"_q;\n\tcase Known::Tgv: return u\"application/x-tgwallpattern\"_q;\n\tcase Known::TDesktopTheme: return u\"application/x-tdesktop-theme\"_q;\n\tcase Known::TDesktopPalette: return u\"application/x-tdesktop-palette\"_q;\n\tdefault: break;\n\t}\n\treturn _typeStruct.name();\n}\n\nMimeType MimeTypeForName(const QString &mime) {\n\tif (mime == u\"image/webp\"_q) {\n\t\treturn MimeType(MimeType::Known::WebP);\n\t} else if (mime == u\"application/x-tgsticker\"_q) {\n\t\treturn MimeType(MimeType::Known::Tgs);\n\t} else if (mime == u\"application/x-tgwallpattern\"_q) {\n\t\treturn MimeType(MimeType::Known::Tgv);\n\t} else if (mime == u\"application/x-tdesktop-theme\"_q\n\t\t|| mime == u\"application/x-tgtheme-tdesktop\"_q) {\n\t\treturn MimeType(MimeType::Known::TDesktopTheme);\n\t} else if (mime == u\"application/x-tdesktop-palette\"_q) {\n\t\treturn MimeType(MimeType::Known::TDesktopPalette);\n\t} else if (mime == u\"audio/mpeg3\"_q) {\n\t\treturn MimeType(QMimeDatabase().mimeTypeForName(\"audio/mp3\"));\n\t}\n\treturn MimeType(QMimeDatabase().mimeTypeForName(mime));\n}\n\nMimeType MimeTypeForFile(const QFileInfo &file) {\n\tQString path = file.absoluteFilePath();\n\tif (path.endsWith(u\".webp\"_q, Qt::CaseInsensitive)) {\n\t\treturn MimeType(MimeType::Known::WebP);\n\t} else if (path.endsWith(u\".tgs\"_q, Qt::CaseInsensitive)) {\n\t\treturn MimeType(MimeType::Known::Tgs);\n\t} else if (path.endsWith(u\".tgv\"_q)) {\n\t\treturn MimeType(MimeType::Known::Tgv);\n\t} else if (path.endsWith(u\".tdesktop-theme\"_q, Qt::CaseInsensitive)) {\n\t\treturn MimeType(MimeType::Known::TDesktopTheme);\n\t} else if (path.endsWith(u\".tdesktop-palette\"_q, Qt::CaseInsensitive)) {\n\t\treturn MimeType(MimeType::Known::TDesktopPalette);\n\t}\n\n\t{\n\t\tQFile f(path);\n\t\tif (f.open(QIODevice::ReadOnly)) {\n\t\t\tQByteArray magic = f.read(12);\n\t\t\tif (magic.size() >= 12) {\n\t\t\t\tif (!memcmp(magic.constData(), \"RIFF\", 4) && !memcmp(magic.constData() + 8, \"WEBP\", 4)) {\n\t\t\t\t\treturn MimeType(MimeType::Known::WebP);\n\t\t\t\t}\n\t\t\t}\n\t\t\tf.close();\n\t\t}\n\t}\n\treturn MimeType(QMimeDatabase().mimeTypeForFile(file));\n}\n\nMimeType MimeTypeForData(const QByteArray &data) {\n\tif (data.size() >= 12) {\n\t\tif (!memcmp(data.constData(), \"RIFF\", 4) && !memcmp(data.constData() + 8, \"WEBP\", 4)) {\n\t\t\treturn MimeType(MimeType::Known::WebP);\n\t\t}\n\t}\n\treturn MimeType(QMimeDatabase().mimeTypeForData(data));\n}\n\nbool IsMimeStickerLottie(const QString &mime) {\n\treturn (mime == u\"application/x-tgsticker\"_q);\n}\n\nbool IsMimeStickerWebm(const QString &mime) {\n\treturn (mime == u\"video/webm\"_q);\n}\n\nbool IsMimeStickerAnimated(const QString &mime) {\n\treturn (mime == u\"application/x-tgsticker\"_q);\n}\n\nbool IsMimeSticker(const QString &mime) {\n\treturn (mime == u\"image/webp\"_q)\n\t\t|| IsMimeStickerAnimated(mime);\n}\n\nbool IsMimeAcceptedForPhotoVideoAlbum(const QString &mime) {\n\treturn (mime == u\"image/jpeg\"_q)\n\t\t|| (mime == u\"image/png\"_q)\n\t\t|| (mime == u\"video/mp4\"_q)\n\t\t|| (mime == u\"video/quicktime\"_q);\n}\n\nbool FileIsImage(const QString &name, const QString &mime) {\n\treturn name.isEmpty()\n\t\t? mime.toLower().startsWith(u\"image/\"_q)\n\t\t: (DetectNameType(name) == NameType::Image);\n}\n\nstd::shared_ptr<QMimeData> ShareMimeMediaData(\n\t\tnot_null<const QMimeData*> original) {\n\tauto result = std::make_shared<QMimeData>();\n\tif (original->hasFormat(u\"application/x-td-forward\"_q)) {\n\t\tresult->setData(u\"application/x-td-forward\"_q, \"1\");\n\t}\n\tif (original->hasImage()) {\n\t\tresult->setImageData(original->imageData());\n\t}\n\tif (original->hasFormat(u\"application/x-td-use-jpeg\"_q)\n\t\t&& original->hasFormat(u\"image/jpeg\"_q)) {\n\t\tresult->setData(u\"application/x-td-use-jpeg\"_q, \"1\");\n\t\tresult->setData(u\"image/jpeg\"_q, original->data(u\"image/jpeg\"_q));\n\t}\n\tif (auto list = ReadMimeUrls(original); !list.isEmpty()) {\n\t\tresult->setUrls(std::move(list));\n\t}\n\tresult->setText(ReadMimeText(original));\n\treturn result;\n}\n\nMimeImageData ReadMimeImage(not_null<const QMimeData*> data) {\n\tif (data->hasFormat(u\"application/x-td-use-jpeg\"_q)) {\n\t\tauto bytes = data->data(u\"image/jpeg\"_q);\n\t\tauto read = Images::Read({ .content = bytes });\n\t\tif (read.format == \"jpeg\" && !read.image.isNull()) {\n\t\t\treturn {\n\t\t\t\t.image = std::move(read.image),\n\t\t\t\t.content = std::move(bytes),\n\t\t\t};\n\t\t}\n\t} else if (data->hasImage()) {\n\t\treturn { .image = qvariant_cast<QImage>(data->imageData()) };\n\t}\n\treturn {};\n}\n\nQString ReadMimeText(not_null<const QMimeData*> data) {\n\treturn IsImageFromFirefox(data) ? QString() : data->text();\n}\n\nQList<QUrl> ReadMimeUrls(not_null<const QMimeData*> data) {\n\treturn (data->hasUrls() && !IsImageFromFirefox(data))\n\t\t? KUrlMimeData::urlsFromMimeData(\n\t\t\tdata,\n\t\t\tKUrlMimeData::PreferLocalUrls)\n\t\t: QList<QUrl>();\n}\n\nbool CanSendFiles(not_null<const QMimeData*> data) {\n\tif (data->hasImage()) {\n\t\treturn true;\n\t} else if (const auto urls = ReadMimeUrls(data); !urls.empty()) {\n\t\tif (ranges::all_of(urls, &QUrl::isLocalFile)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nQString FileExtension(const QString &filepath) {\n\tconst auto reversed = ranges::views::reverse(filepath);\n\tconst auto last = ranges::find_first_of(reversed, \".\\\\/\");\n\tif (last == reversed.end() || *last != '.') {\n\t\treturn QString();\n\t}\n\treturn QString(last.base(), last - reversed.begin());\n}\n\nNameType DetectNameType(const QString &filepath) {\n\tstatic const auto kImage = SplitExtensions(u\"\\\nafdesign ai avif bmp dng gif heic icns ico jfif jpeg jpg jpg-large jxl nef \\\npng png-large psd qoi raw sketch svg tga tif tiff webp\"_q);\n\tstatic const auto kVideo = SplitExtensions(u\"\\\n3g2 3gp 3gpp aep avi flv h264 m4s m4v mkv mov mp4 mpeg mpg ogv srt tgs tgv \\\nvob webm wmv\"_q);\n\tstatic const auto kAudio = SplitExtensions(u\"\\\naac ac3 aif amr caf cda cue flac m4a m4b mid midi mp3 ogg opus wav wma\"_q);\n\tstatic const auto kDocument = SplitExtensions(u\"\\\npdf doc docx ppt pptx pps ppsx xls xlsx txt rtf odt ods odp csv text log tl \\\ntex xspf xml djvu diag ps ost kml pub epub mobi cbr cbz fb2 prc ris pem p7b \\\nm3u m3u8 wpd wpl htm html xhtml key\"_q);\n\tstatic const auto kArchive = SplitExtensions(u\"\\\n7z arj bz2 gz rar tar xz z zip zst\"_q);\n\tstatic const auto kThemeFile = SplitExtensions(u\"\\\ntdesktop-theme tdesktop-palette tgios-theme attheme\"_q);\n\tstatic const auto kOtherBenign = SplitExtensions(u\"\\\nc cc cpp cxx h m mm swift cs ts class java css ninja cmake patch diff plist \\\ngyp gitignore strings asoundrc torrent csr json xaml md keylayout sql \\\nsln xib mk \\\n\\\ndmg img iso vcd \\\n\\\npdb eot ics ips ipa core mem pcap ovpn part pcapng dmp pkpass dat zxp crash \\\nfile bak gbr plain dlc fon fnt otf ttc ttf gpx db rss cur \\\n\\\ntdesktop-endpoints\"_q);\n\n\tstatic const auto kExecutable = SplitExtensions(\n#ifdef Q_OS_WIN\n\t\tu\"\\\nad ade adp ahk app application appref-ms asp aspx asx bas bat bin cab cdxml \\\ncer cfg cgi chi chm cmd cnt com conf cpl crt csh der diagcab dll drv eml \\\nexe fon fxp gadget grp hlp hpj hta htt inf ini ins inx isp isu its jar jnlp \\\njob js jse jsp key ksh lexe library-ms lnk local lua mad maf mag mam \\\nmanifest maq mar mas mat mau mav maw mcf mda mdb mde mdt mdw mdz mht mhtml \\\nmjs mmc mof msc msg msh msh1 msh2 msh1xml msh2xml mshxml msi msp mst ops \\\nosd paf pcd phar php php3 php4 php5 php7 phps php-s pht phtml pif pl plg pm \\\npod prf prg ps1 ps2 ps1xml ps2xml psc1 psc2 psd1 psm1 pssc pst py py3 pyc \\\npyd pyi pyo pyw pyzw pyz rb reg rgs scf scr sct search-ms settingcontent-ms \\\nsh shb shs slk sys swf t tmp u3p url vb vbe vbp vbs vbscript vdx vsmacros \\\nvsd vsdm vsdx vss vssm vssx vst vstm vstx vsw vsx vtx website wlua ws wsc \\\nwsf wsh xbap xll xlsb xlsm xnk xs\"_q\n#elif defined Q_OS_MAC // Q_OS_MAC\n\t\tu\"\\\napplescript action app bin command csh osx workflow terminal url caction \\\nmpkg pkg scpt scptd xhtm xhtml webarchive\"_q\n#else // Q_OS_WIN || Q_OS_MAC\n\t\tu\"bin csh deb desktop ksh out pet pkg pup rpm run sh shar slp zsh\"_q\n#endif // !Q_OS_WIN && !Q_OS_MAC\n\t);\n\n\tconst auto extension = FileExtension(filepath).toLower();\n\tif (kExecutable.contains(extension)) {\n\t\treturn NameType::Executable;\n\t} else if (kImage.contains(extension)) {\n\t\treturn NameType::Image;\n\t} else if (kVideo.contains(extension)) {\n\t\treturn NameType::Video;\n\t} else if (kAudio.contains(extension)) {\n\t\treturn NameType::Audio;\n\t} else if (kDocument.contains(extension)) {\n\t\treturn NameType::Document;\n\t} else if (kArchive.contains(extension)) {\n\t\treturn NameType::Archive;\n\t} else if (kThemeFile.contains(extension)) {\n\t\treturn NameType::ThemeFile;\n\t} else if (kOtherBenign.contains(extension)) {\n\t\treturn NameType::OtherBenign;\n\t}\n\treturn NameType::Unknown;\n}\n\nbool NameTypeAllowsThumbnail(NameType type) {\n\treturn type == NameType::Image\n\t\t|| type == NameType::Video\n\t\t|| type == NameType::Audio\n\t\t|| type == NameType::Document\n\t\t|| type == NameType::ThemeFile;\n}\n\nbool IsIpRevealingPath(const QString &filepath) {\n\tstatic const auto kExtensions = [] {\n\t\tconst auto joined = u\"htm html svg m4v m3u m3u8 xhtml xml\"_q;\n\t\tconst auto list = joined.split(' ');\n\t\treturn base::flat_set<QString>(list.begin(), list.end());\n\t}();\n\tstatic const auto kMimeTypes = [] {\n\t\tconst auto joined = u\"text/html image/svg+xml\"_q;\n\t\tconst auto list = joined.split(' ');\n\t\treturn base::flat_set<QString>(list.begin(), list.end());\n\t}();\n\n\treturn ranges::binary_search(\n\t\tkExtensions,\n\t\tFileExtension(filepath).toLower()\n\t) || ranges::binary_search(\n\t\tkMimeTypes,\n\t\tQMimeDatabase().mimeTypeForFile(QFileInfo(filepath)).name()\n\t);\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/mime_type.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QFileInfo>\n#include <QtCore/QString>\n#include <QtCore/QStringList>\n#include <QtCore/QMimeType>\n#include <QtGui/QImage>\n\nclass QMimeData;\n\nnamespace Core {\n\nclass MimeType {\npublic:\n\tenum class Known {\n\t\tUnknown,\n\t\tTDesktopTheme,\n\t\tTDesktopPalette,\n\t\tWebP,\n\t\tTgs,\n\t\tTgv,\n\t};\n\n\texplicit MimeType(const QMimeType &type);\n\texplicit MimeType(Known type);\n\tQStringList globPatterns() const;\n\tQString filterString() const;\n\tQString name() const;\n\nprivate:\n\tQMimeType _typeStruct;\n\tKnown _type = Known::Unknown;\n\n};\n\n[[nodiscard]] MimeType MimeTypeForName(const QString &mime);\n[[nodiscard]] MimeType MimeTypeForFile(const QFileInfo &file);\n[[nodiscard]] MimeType MimeTypeForData(const QByteArray &data);\n\n[[nodiscard]] bool IsMimeStickerAnimated(const QString &mime);\n[[nodiscard]] bool IsMimeSticker(const QString &mime);\n[[nodiscard]] bool IsMimeAcceptedForPhotoVideoAlbum(const QString &mime);\n\n[[nodiscard]] bool FileIsImage(const QString &name, const QString &mime);\n\n[[nodiscard]] std::shared_ptr<QMimeData> ShareMimeMediaData(\n\tnot_null<const QMimeData*> original);\n\nstruct MimeImageData {\n\tQImage image;\n\tQByteArray content;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn image.isNull();\n\t}\n\texplicit operator bool() const {\n\t\treturn !empty();\n\t}\n};\n[[nodiscard]] MimeImageData ReadMimeImage(not_null<const QMimeData*> data);\n[[nodiscard]] QString ReadMimeText(not_null<const QMimeData*> data);\n[[nodiscard]] QList<QUrl> ReadMimeUrls(not_null<const QMimeData*> data);\n[[nodiscard]] bool CanSendFiles(not_null<const QMimeData*> data);\n\nenum class NameType : uchar {\n\tUnknown,\n\tExecutable,\n\tImage,\n\tVideo,\n\tAudio,\n\tDocument,\n\tArchive,\n\tThemeFile,\n\tOtherBenign,\n};\n\n[[nodiscard]] QString FileExtension(const QString &filepath);\n[[nodiscard]] NameType DetectNameType(const QString &filepath);\n[[nodiscard]] bool NameTypeAllowsThumbnail(NameType type);\n[[nodiscard]] bool IsIpRevealingPath(const QString &filepath);\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/phone_click_handler.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/phone_click_handler.h\"\n\n#include \"boxes/add_contact_box.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"mtproto/sender.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/widgets/menu/menu_item_base.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_chat.h\" // popupMenuExpandedSeparator.\n#include \"styles/style_menu_icons.h\"\n\nnamespace {\n\n[[nodiscard]] QString Trim(QString text) {\n\treturn text\n\t\t.replace('+', QString())\n\t\t.replace(' ', QString())\n\t\t.replace('-', QString());\n}\n\nclass ResolvePhoneAction final : public Ui::Menu::ItemBase {\npublic:\n\tResolvePhoneAction(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::PopupMenu &st,\n\t\tconst QString &phone,\n\t\tnot_null<Window::SessionController*> controller);\n\n\tbool isEnabled() const override;\n\tnot_null<QAction*> action() const override;\n\n\tvoid handleKeyPress(not_null<QKeyEvent*> e) override;\n\n\t[[nodiscard]] QString firstName() const;\n\t[[nodiscard]] QString lastName() const;\n\nprotected:\n\tQPoint prepareRippleStartPosition() const override;\n\tQImage prepareRippleMask() const override;\n\n\tint contentHeight() const override;\n\nprivate:\n\tvoid prepare();\n\tvoid paint(Painter &p);\n\n\tconst not_null<QAction*> _dummyAction;\n\tconst style::Menu &_st;\n\trpl::variable<PeerData*> _peer;\n\trpl::variable<bool> _loaded;\n\tUi::PeerUserpicView _userpicView;\n\n\tMTP::Sender _api;\n\n\tUi::Text::String _above;\n\tUi::Text::String _below;\n\tint _aboveWidth = 0;\n\tint _belowWidth = 0;\n\tconst int _height = 0;\n\n};\n\nResolvePhoneAction::ResolvePhoneAction(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::PopupMenu &st,\n\tconst QString &phone,\n\tnot_null<Window::SessionController*> controller)\n: ItemBase(parent, st.menu)\n, _dummyAction(Ui::CreateChild<QAction>(parent))\n, _st(st.menu)\n, _api(&controller->session().mtp())\n, _height(rect::m::sum::v(st::groupCallJoinAsPadding)\n\t+ st::groupCallJoinAsPhotoSize) {\n\tsetAcceptBoth(true);\n\tfitToMenuWidth();\n\tsetActionTriggered([=] {\n\t\tif (const auto peer = _peer.current()) {\n\t\t\tcontroller->showPeerInfo(peer);\n\t\t}\n\t});\n\n\tconst auto formattedPhone = Trim(phone);\n\n\tconst auto owner = &controller->session().data();\n\n\tif (const auto peer = owner->userByPhone(formattedPhone)) {\n\t\t_peer = peer;\n\t\t_loaded.force_assign(true);\n\t} else {\n\t\t_api.request(MTPcontacts_ResolvePhone(\n\t\t\tMTP_string(phone)\n\t\t)).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\t\tresult.match([&](const MTPDcontacts_resolvedPeer &data) {\n\t\t\t\towner->processUsers(data.vusers());\n\t\t\t\towner->processChats(data.vchats());\n\t\t\t\tif (const auto peerId = peerFromMTP(data.vpeer())) {\n\t\t\t\t\t_peer = owner->peer(peerId);\n\t\t\t\t}\n\t\t\t\t_loaded.force_assign(true);\n\t\t\t});\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tif (error.code() == 400) {\n\t\t\t\t_peer.force_assign(nullptr);\n\t\t\t\t_loaded.force_assign(true);\n\t\t\t}\n\t\t}).send();\n\t}\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(this);\n\t\tpaint(p);\n\t}, lifetime());\n\n\tenableMouseSelecting();\n\tprepare();\n}\n\nQString ResolvePhoneAction::firstName() const {\n\tconst auto peer = _peer.current();\n\tconst auto user = peer ? peer->asUser() : nullptr;\n\treturn user ? user->firstName : QString();\n}\n\nQString ResolvePhoneAction::lastName() const {\n\tconst auto peer = _peer.current();\n\tconst auto user = peer ? peer->asUser() : nullptr;\n\treturn user ? user->lastName : QString();\n}\n\nvoid ResolvePhoneAction::paint(Painter &p) {\n\tconst auto selected = isSelected() && _peer.current();\n\tconst auto height = contentHeight();\n\tif (selected && _st.itemBgOver->c.alpha() < 255) {\n\t\tp.fillRect(0, 0, width(), height, _st.itemBg);\n\t}\n\tp.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg);\n\tif (isEnabled()) {\n\t\tpaintRipple(p, 0, 0);\n\t}\n\n\tconst auto &padding = st::groupCallJoinAsPadding;\n\tconst auto textLeft = padding.left()\n\t\t+ st::groupCallJoinAsPhotoSize\n\t\t+ padding.left();\n\tif (const auto peer = _peer.current()) {\n\t\tpeer->paintUserpic(\n\t\t\tp,\n\t\t\t_userpicView,\n\t\t\tpadding.left(),\n\t\t\tpadding.top(),\n\t\t\tst::groupCallJoinAsPhotoSize);\n\t\tp.setPen(selected ? _st.itemFgOver : _st.itemFg);\n\t\t_above.drawLeftElided(\n\t\t\tp,\n\t\t\ttextLeft,\n\t\t\tst::groupCallJoinAsTextTop,\n\t\t\twidth() - textLeft - padding.right(),\n\t\t\twidth());\n\t\tp.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);\n\t\t_below.drawLeftElided(\n\t\t\tp,\n\t\t\ttextLeft,\n\t\t\tst::groupCallJoinAsNameTop,\n\t\t\t_belowWidth,\n\t\t\twidth());\n\t} else {\n\t\tp.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);\n\t\tconst auto w = width() - rect::m::sum::h(padding);\n\t\t_below.draw(p, Ui::Text::PaintContext{\n\t\t\t.position = QPoint(\n\t\t\t\tpadding.left(),\n\t\t\t\t(height - _below.countHeight(w)) / 2),\n\t\t\t.outerWidth = w,\n\t\t\t.availableWidth = w,\n\t\t\t.align = style::al_center,\n\t\t\t.elisionLines = 2,\n\t\t});\n\t}\n}\n\nvoid ResolvePhoneAction::prepare() {\n\trpl::combine(\n\t\ttr::lng_context_view_profile(),\n\t\t_peer.value(\n\t\t) | rpl::map([](PeerData *peer) {\n\t\t\treturn peer\n\t\t\t\t? Info::Profile::NameValue(peer)\n\t\t\t\t: rpl::single(QString());\n\t\t}) | rpl::flatten_latest(),\n\t\ttr::lng_menu_not_contact(),\n\t\t_loaded.value(\n\t\t) | rpl::map([](bool loaded) {\n\t\t\treturn loaded\n\t\t\t\t? rpl::single(QString())\n\t\t\t\t: tr::lng_contacts_loading();\n\t\t}) | rpl::flatten_latest()\n\t) | rpl::on_next([=](\n\t\t\tQString text,\n\t\t\tQString name,\n\t\t\tQString no,\n\t\t\tQString loading) {\n\t\tconst auto &padding = st::groupCallJoinAsPadding;\n\t\tQWidget::setAttribute(\n\t\t\tQt::WA_TransparentForMouseEvents,\n\t\t\t!_peer.current());\n\t\tconst auto above = name;\n\t\tconst auto below = !loading.isEmpty()\n\t\t\t? loading\n\t\t\t: name.isEmpty()\n\t\t\t? no\n\t\t\t: text;\n\t\tconst auto options = kDefaultTextOptions;\n\t\tconst auto tempWidth = [&] {\n\t\t\t_below.setMarkedText(_st.itemStyle, { text }, options);\n\t\t\treturn _below.maxWidth();\n\t\t}();\n\t\tconst auto textLeft = padding.left()\n\t\t\t+ st::groupCallJoinAsPhotoSize\n\t\t\t+ padding.left();\n\t\tconst auto w = std::clamp(\n\t\t\t(textLeft + tempWidth + padding.right()),\n\t\t\t_st.widthMin,\n\t\t\t_st.widthMax);\n\t\tif (!no.isEmpty()) {\n\t\t\t_below = Ui::Text::String(w);\n\t\t}\n\t\t_above.setMarkedText(_st.itemStyle, { above }, options);\n\t\t_below.setMarkedText(_st.itemStyle, { below }, options);\n\t\tsetMinWidth(w);\n\t\t_aboveWidth = w - textLeft - padding.right();\n\t\t_belowWidth = w\n\t\t\t- ((loading.isEmpty() && name.isEmpty()) ? 0 : textLeft)\n\t\t\t- padding.right();\n\t\tupdate();\n\t}, lifetime());\n}\n\nbool ResolvePhoneAction::isEnabled() const {\n\treturn true;\n}\n\nnot_null<QAction*> ResolvePhoneAction::action() const {\n\treturn _dummyAction;\n}\n\nQPoint ResolvePhoneAction::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos());\n}\n\nQImage ResolvePhoneAction::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::RectMask(size());\n}\n\nint ResolvePhoneAction::contentHeight() const {\n\treturn _height;\n}\n\nvoid ResolvePhoneAction::handleKeyPress(not_null<QKeyEvent*> e) {\n\tif (!isSelected() || !_peer.current()) {\n\t\treturn;\n\t}\n\tconst auto key = e->key();\n\tif (key == Qt::Key_Enter || key == Qt::Key_Return) {\n\t\tsetClicked(Ui::Menu::TriggeredSource::Keyboard);\n\t}\n}\n\n} // namespace\n\nPhoneClickHandler::PhoneClickHandler(\n\tnot_null<Main::Session*> session,\n\tQString text)\n: _session(session)\n, _text(text) {\n\tsetProperty(kPhoneNumberLinkProperty, _text);\n}\n\nvoid PhoneClickHandler::onClick(ClickContext context) const {\n\tif (context.button != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tconst auto my = context.other.value<ClickHandlerContext>();\n\tconst auto controller = my.sessionWindow.get();\n\tconst auto pos = QCursor::pos();\n\tif (!controller) {\n\t\treturn;\n\t}\n\tconst auto menu = Ui::CreateChild<Ui::PopupMenu>(\n\t\tcontroller->content(),\n\t\tst::popupMenuWithIcons);\n\n\tconst auto phone = _text;\n\n#if 0\n\tconst auto maybeContact = [&]() -> PeerData* {\n\t\tconst auto &chats = controller->session().data().contactsList();\n\t\tfor (const auto &row : chats->all()) {\n\t\t\tif (const auto history = row->history()) {\n\t\t\t\tif (const auto user = history->peer->asUser()) {\n\t\t\t\t\tif (Trim(user->phone()) == Trim(phone)) {\n\t\t\t\t\t\treturn user;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t}();\n#endif\n\n\tmenu->addAction(tr::lng_profile_copy_phone(tr::now), [=] {\n\t\tTextUtilities::SetClipboardText(\n\t\t\tTextForMimeData::Simple(phone.trimmed()));\n\t}, &st::menuIconCopy);\n\n\tauto resolvePhoneAction = base::make_unique_q<ResolvePhoneAction>(\n\t\tmenu->menu(),\n\t\tmenu->st(),\n\t\tphone,\n\t\tcontroller);\n\n\tif (Trim(phone) != Trim(controller->session().user()->phone())) {\n\t\tmenu->addAction(\n\t\t\ttr::lng_info_add_as_contact(tr::now),\n\t\t\t[=, raw = base::make_weak(resolvePhoneAction.get())] {\n\t\t\t\tcontroller->show(\n\t\t\t\t\tBox<AddContactBox>(\n\t\t\t\t\t\t&controller->session(),\n\t\t\t\t\t\traw ? raw->firstName() : QString(),\n\t\t\t\t\t\traw ? raw->lastName() : QString(),\n\t\t\t\t\t\tTrim(phone)));\n\t\t\t},\n\t\t\t&st::menuIconInvite);\n\t}\n\n\tmenu->addSeparator(&st::popupMenuExpandedSeparator.menu.separator);\n\n\tmenu->addAction(std::move(resolvePhoneAction));\n\n\tmenu->popup(pos);\n}\n\nauto PhoneClickHandler::getTextEntity() const -> TextEntity {\n\treturn { EntityType::Phone };\n}\n\nQString PhoneClickHandler::tooltip() const {\n\treturn _text;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/core/phone_click_handler.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/basic_click_handlers.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nclass PhoneClickHandler : public ClickHandler {\npublic:\n\tPhoneClickHandler(not_null<Main::Session*> session, QString text);\n\n\tvoid onClick(ClickContext context) const override;\n\n\tTextEntity getTextEntity() const override;\n\n\tQString tooltip() const override;\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\tQString _text;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/core/sandbox.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/sandbox.h\"\n\n#include \"base/platform/base_platform_info.h\"\n#include \"platform/platform_specific.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"storage/localstorage.h\"\n#include \"window/notifications_manager.h\"\n#include \"window/window_controller.h\"\n#include \"core/crash_reports.h\"\n#include \"core/crash_report_window.h\"\n#include \"core/application.h\"\n#include \"core/launcher.h\"\n#include \"core/local_url_handlers.h\"\n#include \"core/update_checker.h\"\n#include \"core/deadlock_detector.h\"\n#include \"base/timer.h\"\n#include \"base/concurrent_timer.h\"\n#include \"base/invoke_queued.h\"\n#include \"base/options.h\"\n#include \"base/qthelp_url.h\"\n#include \"base/qthelp_regex.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/effects/animations.h\"\n\n#include <QtCore/QLockFile>\n#include <QtGui/QSessionManager>\n#include <QtGui/QScreen>\n#include <QtGui/qpa/qplatformscreen.h>\n\nnamespace Core {\nnamespace {\n\nbase::options::toggle OptionDeadlockDetector({\n\t.id = kOptionDeadlockDetector,\n\t.name = \"Deadlock Detector\",\n\t.description = \"Check once every 30 seconds that main thread is still responsive.\",\n\t.restartRequired = true,\n});\n\n} // namespace\n\nconst char kOptionDeadlockDetector[] = \"deadlock-detector\";\n\nbool Sandbox::QuitOnStartRequested = false;\n\nSandbox::Sandbox(int &argc, char **argv)\n: QApplication(argc, argv)\n, _mainThreadId(QThread::currentThreadId()) {\n}\n\nint Sandbox::start() {\n\tif (!Core::UpdaterDisabled()) {\n\t\t_updateChecker = std::make_unique<Core::UpdateChecker>();\n\t}\n\n\t{\n\t\tconst auto d = QFile::encodeName(QDir(cWorkingDir()).absolutePath());\n\t\tchar h[33] = { 0 };\n\t\thashMd5Hex(d.constData(), d.size(), h);\n\t\t_localServerName = Platform::SingleInstanceLocalServerName(h);\n\t}\n\n\t{\n\t\tconst auto d = QFile::encodeName(cExeDir() + cExeName());\n\t\tQByteArray h;\n\t\th.resize(32);\n\t\thashMd5Hex(d.constData(), d.size(), h.data());\n\t\t_lockFile = std::make_unique<QLockFile>(QDir::tempPath() + '/' + h + '-' + cGUIDStr());\n\t\t_lockFile->setStaleLockTime(0);\n\t\tif (!_lockFile->tryLock()\n\t\t\t&& Launcher::Instance().customWorkingDir()) {\n\t\t\t// On Windows, QLockFile has problems detecting a stale lock\n\t\t\t// if the machine's hostname contains characters outside the US-ASCII character set.\n\t\t\tif constexpr (Platform::IsWindows()) {\n\t\t\t\t// QLockFile::removeStaleLockFile returns false on Windows,\n\t\t\t\t// when the application owning the lock is still running.\n\t\t\t\tif (!_lockFile->removeStaleLockFile()) {\n\t\t\t\t\tgManyInstance = true;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tgManyInstance = true;\n\t\t\t}\n\t\t}\n\t}\n\n#if defined Q_OS_LINUX && QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)\n\t_localServer.setSocketOptions(QLocalServer::AbstractNamespaceOption);\n\t_localSocket.setSocketOptions(QLocalSocket::AbstractNamespaceOption);\n#endif // Q_OS_LINUX && Qt >= 6.2.0\n\n\tconnect(\n\t\t&_localSocket,\n\t\t&QLocalSocket::connected,\n\t\t[=] { socketConnected(); });\n\tconnect(\n\t\t&_localSocket,\n\t\t&QLocalSocket::disconnected,\n\t\t[=] { socketDisconnected(); });\n\tconnect(\n\t\t&_localSocket,\n\t\t&QLocalSocket::errorOccurred,\n\t\t[=](QLocalSocket::LocalSocketError error) { socketError(error); });\n\tconnect(\n\t\t&_localSocket,\n\t\t&QLocalSocket::bytesWritten,\n\t\t[=](qint64 bytes) { socketWritten(bytes); });\n\tconnect(\n\t\t&_localSocket,\n\t\t&QLocalSocket::readyRead,\n\t\t[=] { socketReading(); });\n\tconnect(\n\t\t&_localServer,\n\t\t&QLocalServer::newConnection,\n\t\t[=] { newInstanceConnected(); });\n\n\tcrl::on_main(this, [=] { checkForQuit(); });\n\tconnect(this, &QCoreApplication::aboutToQuit, [=] {\n\t\tcustomEnterFromEventLoop([&] {\n\t\t\tcloseApplication();\n\t\t});\n\t});\n\n\t// https://github.com/telegramdesktop/tdesktop/issues/948\n\t// and https://github.com/telegramdesktop/tdesktop/issues/5022\n\tconnect(this, &QGuiApplication::saveStateRequest, [](auto &manager) {\n\t\tmanager.setRestartHint(QSessionManager::RestartNever);\n\t});\n\n\tLOG((\"Connecting local socket to %1...\").arg(_localServerName));\n\t_localSocket.connectToServer(_localServerName);\n\n\tif (QuitOnStartRequested) {\n\t\tcloseApplication();\n\t\treturn 0;\n\t}\n\t_started = true;\n\treturn exec();\n}\n\nvoid Sandbox::QuitWhenStarted() {\n\tif (!QApplication::instance() || !Instance()._started) {\n\t\tQuitOnStartRequested = true;\n\t} else {\n\t\t// Use exit(0) instead of quit() to avoid recursive\n\t\t// [NSApp terminate:] on macOS. Since Qt 6.0, quit() routes\n\t\t// through QCocoaIntegration::quit() -> [NSApp terminate:],\n\t\t// which when called from within applicationShouldTerminate:\n\t\t// causes a nested terminate that leads to exit() being called\n\t\t// directly, bypassing normal cleanup. exit(0) properly exits\n\t\t// event loops without going through the platform plugin.\n\t\tQCoreApplication::exit(0);\n\t}\n}\n\nvoid Sandbox::launchApplication() {\n\tInvokeQueued(this, [=] {\n\t\tif (Quitting()) {\n\t\t\tquit();\n\t\t} else if (_application) {\n\t\t\treturn;\n\t\t}\n\t\tsetupScreenScale();\n\n\t\tif (OptionDeadlockDetector.value()) {\n\t\t\tusing DeadlockDetector::PingThread;\n\t\t\t_deadlockDetector = std::make_unique<PingThread>(this);\n\t\t}\n\n\t\t_application = std::make_unique<Application>();\n\n\t\t// Ideally this should go to constructor.\n\t\t// But we want to catch all native events and Application installs\n\t\t// its own filter that can filter out some of them. So we install\n\t\t// our filter after the Application constructor installs his.\n\t\tinstallNativeEventFilter(this);\n\n\t\t_application->run();\n\t});\n}\n\nvoid Sandbox::setupScreenScale() {\n\tconst auto ratio = devicePixelRatio();\n\tLOG((\"Global devicePixelRatio: %1\").arg(ratio));\n\tconst auto logEnv = [](const char *name) {\n\t\tconst auto value = qEnvironmentVariable(name);\n\t\tif (!value.isEmpty()) {\n\t\t\tLOG((\"%1: %2\").arg(name, value));\n\t\t}\n\t};\n\tlogEnv(\"QT_DEVICE_PIXEL_RATIO\");\n\tlogEnv(\"QT_AUTO_SCREEN_SCALE_FACTOR\");\n\tlogEnv(\"QT_ENABLE_HIGHDPI_SCALING\");\n\tlogEnv(\"QT_SCALE_FACTOR\");\n\tlogEnv(\"QT_SCREEN_SCALE_FACTORS\");\n\tlogEnv(\"QT_SCALE_FACTOR_ROUNDING_POLICY\");\n\tlogEnv(\"QT_DPI_ADJUSTMENT_POLICY\");\n\tlogEnv(\"QT_USE_PHYSICAL_DPI\");\n\tlogEnv(\"QT_FONT_DPI\");\n\n\tconst auto useRatio = std::clamp(qCeil(ratio), 1, 3);\n\tstyle::SetDevicePixelRatio(useRatio);\n\n\tconst auto screen = Sandbox::primaryScreen();\n\tconst auto dpi = screen->logicalDotsPerInch();\n\tconst auto basePair = screen->handle()->logicalBaseDpi();\n\tconst auto base = (basePair.first + basePair.second) * 0.5;\n\tconst auto screenScaleExact = dpi / base;\n\tconst auto screenScale = int(base::SafeRound(screenScaleExact * 20)) * 5;\n\tLOG((\"Primary screen DPI: %1, Base: %2.\").arg(dpi).arg(base));\n\tLOG((\"Computed screen scale: %1\").arg(screenScale));\n\tif (Platform::IsMac()) {\n\t\t// 110% for Retina screens by default.\n\t\tcSetScreenScale((useRatio == 2) ? 110 : style::kScaleDefault);\n\t} else {\n\t\tcSetScreenScale(std::clamp(\n\t\t\tscreenScale,\n\t\t\tstyle::kScaleMin,\n\t\t\tstyle::MaxScaleForRatio(useRatio)));\n\t}\n\tLOG((\"DevicePixelRatio: %1\").arg(useRatio));\n\tLOG((\"ScreenScale: %1\").arg(cScreenScale()));\n}\n\nSandbox::~Sandbox() = default;\n\nbool Sandbox::event(QEvent *e) {\n\tif (e->type() == QEvent::Quit) {\n\t\tif (Quitting()) {\n\t\t\treturn QCoreApplication::event(e);\n\t\t}\n\t\tQuit(QuitReason::QtQuitEvent);\n\t\te->ignore();\n\t\treturn false;\n\t} else if (e->type() == QEvent::Close) {\n\t\tQuit();\n\t} else if (e->type() == DeadlockDetector::PingPongEvent::Type()) {\n\t\tpostEvent(\n\t\t\tstatic_cast<DeadlockDetector::PingPongEvent*>(e)->sender(),\n\t\t\tnew DeadlockDetector::PingPongEvent(this));\n\t}\n\treturn QApplication::event(e);\n}\n\nvoid Sandbox::socketConnected() {\n\tLOG((\"Socket connected, this is not the first application instance, sending show command...\"));\n\t_secondInstance = true;\n\n\tQString commands;\n\tif (qEnvironmentVariableIsSet(\"XDG_ACTIVATION_TOKEN\")) {\n\t\tcommands += u\"XDG_ACTIVATION_TOKEN:\"_q + qgetenv(\"XDG_ACTIVATION_TOKEN\").toBase64() + ';';\n\t}\n\tfor (const auto &url : cRefStartUrls()) {\n\t\tcommands += u\"OPEN:\"_q + url.toString(QUrl::FullyEncoded) + ';';\n\t}\n\tif (cQuit()) {\n\t\tcommands += u\"CMD:quit;\"_q;\n\t} else if (cRefStartUrls().isEmpty()) {\n\t\tcommands += u\"CMD:show;\"_q;\n\t}\n\n\tDEBUG_LOG((\"Sandbox Info: writing commands %1\").arg(commands));\n\t_localSocket.write(commands.toLatin1());\n}\n\nvoid Sandbox::socketWritten(qint64/* bytes*/) {\n\tif (_localSocket.state() != QLocalSocket::ConnectedState) {\n\t\tLOG((\"Socket is not connected %1\").arg(_localSocket.state()));\n\t\treturn;\n\t}\n\tif (_localSocket.bytesToWrite()) {\n\t\treturn;\n\t}\n\tLOG((\"Show command written, waiting response...\"));\n}\n\nvoid Sandbox::socketReading() {\n\tif (_localSocket.state() != QLocalSocket::ConnectedState) {\n\t\tLOG((\"Socket is not connected %1\").arg(_localSocket.state()));\n\t\treturn;\n\t}\n\t_localSocketReadData.append(_localSocket.readAll());\n\tconst auto m = QRegularExpression(u\"RES:(\\\\d+)_(\\\\d+);\"_q).match(\n\t\t_localSocketReadData);\n\tif (!m.hasMatch()) {\n\t\treturn;\n\t}\n\tconst auto processId = m.capturedView(1).toULongLong();\n\tconst auto windowId = m.capturedView(2).toULongLong();\n\tif (windowId) {\n\t\tPlatform::ActivateOtherProcess(processId, windowId);\n\t}\n\tLOG((\"Show command response received, processId = %1, windowId = %2, \"\n\t\t\"activating and quitting...\"\n\t\t).arg(processId\n\t\t).arg(windowId));\n\treturn Quit();\n}\n\nvoid Sandbox::socketError(QLocalSocket::LocalSocketError e) {\n\tif (Quitting()) return;\n\n\tif (_secondInstance) {\n\t\tLOG((\"Could not write show command, error %1, quitting...\").arg(e));\n\t\treturn Quit();\n\t}\n\n\tif (e == QLocalSocket::ServerNotFoundError) {\n\t\tLOG((\"This is the only instance of Telegram, starting server and app...\"));\n\t} else {\n\t\tLOG((\"Socket connect error %1, starting server and app...\").arg(e));\n\t}\n\t_localSocket.close();\n\n\t// Local server does not work in WinRT build.\n#ifndef Q_OS_WINRT\n\tpsCheckLocalSocket(_localServerName);\n\n\tif (!_localServer.listen(_localServerName)) {\n\t\tLOG((\"Failed to start listening to %1 server: %2\").arg(_localServerName, _localServer.errorString()));\n\t\treturn Quit();\n\t}\n#endif // !Q_OS_WINRT\n\n\tif (!Core::UpdaterDisabled()\n\t\t&& !cNoStartUpdate()\n\t\t&& Core::checkReadyUpdate()) {\n\t\tcSetRestartingUpdate(true);\n\t\tDEBUG_LOG((\"Sandbox Info: installing update instead of starting app...\"));\n\t\treturn Quit();\n\t}\n\n\tif (cQuit()) {\n\t\treturn Quit();\n\t}\n\n\tsingleInstanceChecked();\n}\n\nvoid Sandbox::singleInstanceChecked() {\n\tif (cManyInstance()) {\n\t\tLOG((\"App Info: Detected another instance\"));\n\t}\n\n\trefreshGlobalProxy();\n\tif (!Logs::started() || !Logs::instanceChecked()) {\n\t\tnew NotStartedWindow();\n\t\treturn;\n\t}\n\tconst auto result = CrashReports::Start();\n\tv::match(result, [&](CrashReports::Status status) {\n\t\tif (status == CrashReports::CantOpen) {\n\t\t\tnew NotStartedWindow();\n\t\t} else {\n\t\t\tlaunchApplication();\n\t\t}\n\t}, [&](const QByteArray &crashdump) {\n\t\t// If crash dump is empty with that status it means that we\n\t\t// didn't close the application properly. Just ignore for now.\n\t\tif (crashdump.isEmpty()) {\n\t\t\tif (CrashReports::Restart() == CrashReports::CantOpen) {\n\t\t\t\tnew NotStartedWindow();\n\t\t\t} else {\n\t\t\t\tlaunchApplication();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t_lastCrashDump = crashdump;\n\t\tauto window = new LastCrashedWindow(\n\t\t\t_lastCrashDump,\n\t\t\t[=] { launchApplication(); });\n\t\twindow->proxyChanges(\n\t\t) | rpl::on_next([=](MTP::ProxyData &&proxy) {\n\t\t\t_sandboxProxy = std::move(proxy);\n\t\t\trefreshGlobalProxy();\n\t\t}, window->lifetime());\n\t});\n}\n\nvoid Sandbox::socketDisconnected() {\n\tif (_secondInstance) {\n\t\tDEBUG_LOG((\"Sandbox Error: socket disconnected before command response received, quitting...\"));\n\t\treturn Quit();\n\t}\n}\n\nvoid Sandbox::newInstanceConnected() {\n\tDEBUG_LOG((\"Sandbox Info: new local socket connected\"));\n\tfor (auto client = _localServer.nextPendingConnection(); client; client = _localServer.nextPendingConnection()) {\n\t\t_localClients.push_back(LocalClient(client, QByteArray()));\n\t\tconnect(\n\t\t\tclient,\n\t\t\t&QLocalSocket::readyRead,\n\t\t\t[=] { readClients(); });\n\t\tconnect(\n\t\t\tclient,\n\t\t\t&QLocalSocket::disconnected,\n\t\t\t[=] { removeClients(); });\n\t}\n}\n\nvoid Sandbox::readClients() {\n\t// This method can be called before Application is constructed.\n\tQList<QUrl> startUrls;\n\tfor (LocalClients::iterator i = _localClients.begin(), e = _localClients.end(); i != e; ++i) {\n\t\ti->second.append(i->first->readAll());\n\t\tif (i->second.size()) {\n\t\t\tbool activationRequired = false;\n\t\t\tQString cmds(QString::fromLatin1(i->second));\n\t\t\tint32 from = 0, l = cmds.length();\n\t\t\tfor (int32 to = cmds.indexOf(QChar(';'), from); to >= from; to = (from < l) ? cmds.indexOf(QChar(';'), from) : -1) {\n\t\t\t\tauto cmd = base::StringViewMid(cmds, from, to - from);\n\t\t\t\tif (cmd.startsWith(u\"CMD:\"_q)) {\n\t\t\t\t\tconst auto processId = QApplication::applicationPid();\n\t\t\t\t\tconst auto windowId = execExternal(cmds.mid(from + 4, to - from - 4));\n\t\t\t\t\tconst auto response = u\"RES:%1_%2;\"_q.arg(processId).arg(windowId).toLatin1();\n\t\t\t\t\ti->first->write(response.data(), response.size());\n\t\t\t\t} else if (cmd.startsWith(u\"XDG_ACTIVATION_TOKEN:\"_q)) {\n\t\t\t\t\tqputenv(\"XDG_ACTIVATION_TOKEN\", QByteArray::fromBase64(cmds.mid(from + 21, to - from - 21).toLatin1()));\n\t\t\t\t} else if (cmd.startsWith(u\"OPEN:\"_q)) {\n\t\t\t\t\tstartUrls.append(cmds.mid(from + 5, to - from - 5).mid(0, 8192));\n\t\t\t\t\tif (!activationRequired) {\n\t\t\t\t\t\tactivationRequired = StartUrlRequiresActivate(startUrls.back().toString());\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tLOG((\"Sandbox Error: unknown command %1 passed in local socket\").arg(cmd.toString()));\n\t\t\t\t}\n\t\t\t\tfrom = to + 1;\n\t\t\t}\n\t\t\tif (from > 0) {\n\t\t\t\ti->second = i->second.mid(from);\n\t\t\t}\n\t\t\tconst auto processId = QApplication::applicationPid();\n\t\t\tconst auto windowId = activationRequired\n\t\t\t\t? execExternal(\"show\")\n\t\t\t\t: 0;\n\t\t\tconst auto response = u\"RES:%1_%2;\"_q.arg(processId).arg(windowId).toLatin1();\n\t\t\ti->first->write(response.data(), response.size());\n\t\t}\n\t}\n\tcRefStartUrls() << base::take(startUrls);\n\tif (_application) {\n\t\t_application->checkStartUrls();\n\t}\n}\n\nvoid Sandbox::removeClients() {\n\tDEBUG_LOG((\"Sandbox Info: remove clients slot called, clients %1\"\n\t\t).arg(_localClients.size()));\n\tfor (auto i = _localClients.begin(), e = _localClients.end(); i != e;) {\n\t\tif (i->first->state() != QLocalSocket::ConnectedState) {\n\t\t\tDEBUG_LOG((\"Sandbox Info: removing client\"));\n\t\t\ti = _localClients.erase(i);\n\t\t\te = _localClients.end();\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n}\n\nvoid Sandbox::checkForQuit() {\n\tif (Quitting()) {\n\t\tquit();\n\t}\n}\n\nvoid Sandbox::refreshGlobalProxy() {\n\tconst auto proxy = !Core::IsAppLaunched()\n\t\t? _sandboxProxy\n\t\t: Core::App().settings().proxy().isEnabled()\n\t\t? Core::App().settings().proxy().selected()\n\t\t: MTP::ProxyData();\n\tif (proxy.type == MTP::ProxyData::Type::Socks5\n\t\t|| proxy.type == MTP::ProxyData::Type::Http) {\n\t\tQNetworkProxy::setApplicationProxy(\n\t\t\tMTP::ToNetworkProxy(MTP::ToDirectIpProxy(proxy)));\n\t} else if (!Core::IsAppLaunched()\n\t\t|| Core::App().settings().proxy().isSystem()) {\n\t\tQNetworkProxyFactory::setUseSystemConfiguration(true);\n\t} else {\n\t\tQNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy);\n\t}\n}\n\nvoid Sandbox::checkForEmptyLoopNestingLevel() {\n\t// _loopNestingLevel == _eventNestingLevel means that we had a\n\t// native event in a nesting loop that didn't get a notify() call\n\t// after. That means we already have exited the nesting loop and\n\t// there must not be any postponed calls with that nesting level.\n\tif (_loopNestingLevel == _eventNestingLevel) {\n\t\tAssert(_postponedCalls.empty()\n\t\t\t|| _postponedCalls.back().loopNestingLevel < _loopNestingLevel);\n\t\tAssert(!_previousLoopNestingLevels.empty());\n\n\t\t_loopNestingLevel = _previousLoopNestingLevels.back();\n\t\t_previousLoopNestingLevels.pop_back();\n\t}\n}\n\nvoid Sandbox::postponeCall(FnMut<void()> &&callable) {\n\tExpects(callable != nullptr);\n\tExpects(_eventNestingLevel >= _loopNestingLevel);\n\n\tcheckForEmptyLoopNestingLevel();\n\t_postponedCalls.push_back({\n\t\t_loopNestingLevel,\n\t\tstd::move(callable)\n\t});\n}\n\nvoid Sandbox::incrementEventNestingLevel() {\n\t++_eventNestingLevel;\n}\n\nvoid Sandbox::decrementEventNestingLevel() {\n\tExpects(_eventNestingLevel >= _loopNestingLevel);\n\n\tif (_eventNestingLevel == _loopNestingLevel) {\n\t\t_loopNestingLevel = _previousLoopNestingLevels.back();\n\t\t_previousLoopNestingLevels.pop_back();\n\t}\n\tconst auto processTillLevel = _eventNestingLevel - 1;\n\tprocessPostponedCalls(processTillLevel);\n\tcheckForEmptyLoopNestingLevel();\n\t_eventNestingLevel = processTillLevel;\n\n\tEnsures(_eventNestingLevel >= _loopNestingLevel);\n}\n\nvoid Sandbox::registerEnterFromEventLoop() {\n\tExpects(_eventNestingLevel >= _loopNestingLevel);\n\n\tif (_eventNestingLevel > _loopNestingLevel) {\n\t\t_previousLoopNestingLevels.push_back(_loopNestingLevel);\n\t\t_loopNestingLevel = _eventNestingLevel;\n\t}\n}\n\nbool Sandbox::notifyOrInvoke(QObject *receiver, QEvent *e) {\n\tif (e->type() == base::InvokeQueuedEvent::Type()) {\n\t\tstatic_cast<base::InvokeQueuedEvent*>(e)->invoke();\n\t\treturn true;\n\t}\n\treturn QApplication::notify(receiver, e);\n}\n\nbool Sandbox::notify(QObject *receiver, QEvent *e) {\n\tif (QThread::currentThreadId() != _mainThreadId) {\n\t\treturn notifyOrInvoke(receiver, e);\n\t}\n\n\tconst auto wrap = createEventNestingLevel();\n\tif (e->type() == QEvent::UpdateRequest) {\n\t\tconst auto weak = QPointer<QObject>(receiver);\n\t\t_widgetUpdateRequests.fire({});\n\t\tif (!weak) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn notifyOrInvoke(receiver, e);\n}\n\nvoid Sandbox::processPostponedCalls(int level) {\n\twhile (!_postponedCalls.empty()) {\n\t\tauto &last = _postponedCalls.back();\n\t\tif (last.loopNestingLevel != level) {\n\t\t\tbreak;\n\t\t}\n\t\tauto taken = std::move(last);\n\t\t_postponedCalls.pop_back();\n\t\ttaken.callable();\n\t}\n}\n\nbool Sandbox::nativeEventFilter(\n\t\tconst QByteArray &eventType,\n\t\tvoid *message,\n\t\tnative_event_filter_result *result) {\n\tregisterEnterFromEventLoop();\n\treturn false;\n}\n\nrpl::producer<> Sandbox::widgetUpdateRequests() const {\n\treturn _widgetUpdateRequests.events();\n}\n\nMTP::ProxyData Sandbox::sandboxProxy() const {\n\treturn _sandboxProxy;\n}\n\nvoid Sandbox::closeApplication() {\n\tif (CurrentLaunchState() == LaunchState::QuitProcessed) {\n\t\treturn;\n\t}\n\tSetLaunchState(LaunchState::QuitProcessed);\n\n\t_application = nullptr;\n\n\t_localServer.close();\n\tfor (const auto &localClient : base::take(_localClients)) {\n\t\tlocalClient.first->close();\n\t}\n\t_localClients.clear();\n\n\t_localSocket.close();\n\n\t_updateChecker = nullptr;\n}\n\nuint64 Sandbox::execExternal(const QString &cmd) {\n\tDEBUG_LOG((\"Sandbox Info: executing external command '%1'\").arg(cmd));\n\tif (cmd == \"show\") {\n\t\tif (Core::IsAppLaunched() && Core::App().activePrimaryWindow()) {\n\t\t\tconst auto window = Core::App().activePrimaryWindow();\n\t\t\twindow->activate();\n\t\t\treturn Platform::ActivationWindowId(window->widget());\n\t\t} else if (const auto window = PreLaunchWindow::instance()) {\n\t\t\twindow->activate();\n\t\t\treturn Platform::ActivationWindowId(window);\n\t\t}\n\t} else if (cmd == \"quit\") {\n\t\tQuit();\n\t}\n\treturn 0;\n}\n\n} // namespace Core\n\nnamespace crl {\n\nrpl::producer<> on_main_update_requests() {\n\treturn Core::Sandbox::Instance().widgetUpdateRequests();\n}\n\n} // namespace crl\n"
  },
  {
    "path": "Telegram/SourceFiles/core/sandbox.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/mtproto_proxy_data.h\"\n\n#include <QtWidgets/QApplication>\n#include <QtNetwork/QLocalServer>\n#include <QtNetwork/QLocalSocket>\n#include <QtCore/QAbstractNativeEventFilter>\n\nclass QLockFile;\n\nnamespace Core {\n\nextern const char kOptionDeadlockDetector[];\n\nclass UpdateChecker;\nclass Application;\n\nclass Sandbox final\n\t: public QApplication\n\t, private QAbstractNativeEventFilter {\nprivate:\n\tauto createEventNestingLevel() {\n\t\tincrementEventNestingLevel();\n\t\treturn gsl::finally([=] { decrementEventNestingLevel(); });\n\t}\n\npublic:\n\tSandbox(int &argc, char **argv);\n\n\tSandbox(const Sandbox &other) = delete;\n\tSandbox &operator=(const Sandbox &other) = delete;\n\n\tint start();\n\n\tvoid refreshGlobalProxy();\n\n\tvoid postponeCall(FnMut<void()> &&callable);\n\tbool notify(QObject *receiver, QEvent *e) override;\n\n\ttemplate <typename Callable>\n\tauto customEnterFromEventLoop(Callable &&callable) {\n\t\tregisterEnterFromEventLoop();\n\t\tconst auto wrap = createEventNestingLevel();\n\t\treturn callable();\n\t}\n\n\trpl::producer<> widgetUpdateRequests() const;\n\n\tMTP::ProxyData sandboxProxy() const;\n\n\tstatic Sandbox &Instance() {\n\t\tExpects(QCoreApplication::instance() != nullptr);\n\n\t\treturn *static_cast<Sandbox*>(QCoreApplication::instance());\n\t}\n\tstatic void QuitWhenStarted();\n\n\t~Sandbox();\n\nprotected:\n\tbool event(QEvent *e) override;\n\nprivate:\n\ttypedef QPair<QLocalSocket*, QByteArray> LocalClient;\n\ttypedef QList<LocalClient> LocalClients;\n\n\tstruct PostponedCall {\n\t\tint loopNestingLevel = 0;\n\t\tFnMut<void()> callable;\n\t};\n\n\tbool notifyOrInvoke(QObject *receiver, QEvent *e);\n\n\tvoid closeApplication(); // will be done in aboutToQuit()\n\tvoid checkForQuit(); // will be done in exec()\n\tvoid checkForEmptyLoopNestingLevel();\n\tvoid registerEnterFromEventLoop();\n\tvoid incrementEventNestingLevel();\n\tvoid decrementEventNestingLevel();\n\tbool nativeEventFilter(\n\t\tconst QByteArray &eventType,\n\t\tvoid *message,\n\t\tnative_event_filter_result *result) override;\n\tvoid processPostponedCalls(int level);\n\tvoid singleInstanceChecked();\n\tvoid launchApplication();\n\tvoid setupScreenScale();\n\n\t// Return window id for activation.\n\tuint64 execExternal(const QString &cmd);\n\n\t// Single instance application\n\tvoid socketConnected();\n\tvoid socketError(QLocalSocket::LocalSocketError e);\n\tvoid socketDisconnected();\n\tvoid socketWritten(qint64 bytes);\n\tvoid socketReading();\n\tvoid newInstanceConnected();\n\n\tvoid readClients();\n\tvoid removeClients();\n\n\tQEventLoopLocker _eventLoopLocker;\n\tconst Qt::HANDLE _mainThreadId = nullptr;\n\tint _eventNestingLevel = 0;\n\tint _loopNestingLevel = 0;\n\tstd::vector<int> _previousLoopNestingLevels;\n\tstd::vector<PostponedCall> _postponedCalls;\n\n\tstd::unique_ptr<Application> _application;\n\n\tQString _localServerName, _localSocketReadData;\n\tQLocalServer _localServer;\n\tQLocalSocket _localSocket;\n\tLocalClients _localClients;\n\tstd::unique_ptr<QLockFile> _lockFile;\n\tbool _secondInstance = false;\n\tbool _started = false;\n\tstatic bool QuitOnStartRequested;\n\n\tstd::unique_ptr<UpdateChecker> _updateChecker;\n\n\tQByteArray _lastCrashDump;\n\tMTP::ProxyData _sandboxProxy;\n\n\trpl::event_stream<> _widgetUpdateRequests;\n\n\tstd::unique_ptr<QThread> _deadlockDetector;\n\n};\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/shortcuts.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/shortcuts.h\"\n\n#include \"base/platform/base_platform_info.h\"\n#include \"base/event_filter.h\"\n#include \"base/parse_helper.h\"\n#include \"core/application.h\"\n#include \"mainwindow.h\"\n#include \"mainwidget.h\"\n#include \"window/window_controller.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"platform/platform_specific.h\"\n\n#include <QAction>\n#include <QShortcut>\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonObject>\n#include <QtCore/QJsonArray>\n\nnamespace Shortcuts {\nnamespace {\n\nconstexpr auto kCountLimit = 256; // How many shortcuts can be in json file.\n\nrpl::event_stream<not_null<Request*>> RequestsStream;\nbool Paused/* = false*/;\n\nconst auto kChatSwitchSpecialKeys = std::array{\n\tQt::Key_Left,\n\tQt::Key_Right,\n\tQt::Key_Up,\n\tQt::Key_Down,\n\tQt::Key_Q,\n};\nQt::Key ChatSwitchModifier/* = Qt::Key()*/;\nbool ChatSwitchStarted/* = false*/;\nQObject *ChatSwitchFilter/* = nullptr*/;\nrpl::event_stream<ChatSwitchRequest> ChatSwitchStream;\nstd::array<Qt::Key, kChatSwitchSpecialKeys.size()> ChatSwitchKeyPressHandled;\n\nconst auto AutoRepeatCommands = base::flat_set<Command>{\n\tCommand::MediaPrevious,\n\tCommand::MediaNext,\n\tCommand::ChatPrevious,\n\tCommand::ChatNext,\n\tCommand::ChatFirst,\n\tCommand::ChatLast,\n};\n\nconst auto MediaCommands = base::flat_set<Command>{\n\tCommand::MediaPlay,\n\tCommand::MediaPause,\n\tCommand::MediaPlayPause,\n\tCommand::MediaStop,\n\tCommand::MediaPrevious,\n\tCommand::MediaNext,\n};\n\nconst auto SupportCommands = base::flat_set<Command>{\n\tCommand::SupportReloadTemplates,\n\tCommand::SupportToggleMuted,\n\tCommand::SupportScrollToCurrent,\n\tCommand::SupportHistoryBack,\n\tCommand::SupportHistoryForward,\n};\n\nconst auto CommandByName = base::flat_map<QString, Command>{\n\t{ u\"close_telegram\"_q    , Command::Close },\n\t{ u\"lock_telegram\"_q     , Command::Lock },\n\t{ u\"minimize_telegram\"_q , Command::Minimize },\n\t{ u\"quit_telegram\"_q     , Command::Quit },\n\n\t{ u\"media_play\"_q        , Command::MediaPlay },\n\t{ u\"media_pause\"_q       , Command::MediaPause },\n\t{ u\"media_playpause\"_q   , Command::MediaPlayPause },\n\t{ u\"media_stop\"_q        , Command::MediaStop },\n\t{ u\"media_previous\"_q    , Command::MediaPrevious },\n\t{ u\"media_next\"_q        , Command::MediaNext },\n\n\t{ u\"search\"_q            , Command::Search },\n\n\t{ u\"previous_chat\"_q     , Command::ChatPrevious },\n\t{ u\"next_chat\"_q         , Command::ChatNext },\n\t{ u\"first_chat\"_q        , Command::ChatFirst },\n\t{ u\"last_chat\"_q         , Command::ChatLast },\n\t{ u\"self_chat\"_q         , Command::ChatSelf },\n\t{ u\"pinned_chat1\"_q      , Command::ChatPinned1 },\n\t{ u\"pinned_chat2\"_q      , Command::ChatPinned2 },\n\t{ u\"pinned_chat3\"_q      , Command::ChatPinned3 },\n\t{ u\"pinned_chat4\"_q      , Command::ChatPinned4 },\n\t{ u\"pinned_chat5\"_q      , Command::ChatPinned5 },\n\t{ u\"pinned_chat6\"_q      , Command::ChatPinned6 },\n\t{ u\"pinned_chat7\"_q      , Command::ChatPinned7 },\n\t{ u\"pinned_chat8\"_q      , Command::ChatPinned8 },\n\n\t{ u\"previous_folder\"_q   , Command::FolderPrevious },\n\t{ u\"next_folder\"_q       , Command::FolderNext },\n\t{ u\"all_chats\"_q         , Command::ShowAllChats },\n\n\t{ u\"account1\"_q          , Command::ShowAccount1 },\n\t{ u\"account2\"_q          , Command::ShowAccount2 },\n\t{ u\"account3\"_q          , Command::ShowAccount3 },\n\t{ u\"account4\"_q          , Command::ShowAccount4 },\n\t{ u\"account5\"_q          , Command::ShowAccount5 },\n\t{ u\"account6\"_q          , Command::ShowAccount6 },\n\n\t{ u\"folder1\"_q           , Command::ShowFolder1 },\n\t{ u\"folder2\"_q           , Command::ShowFolder2 },\n\t{ u\"folder3\"_q           , Command::ShowFolder3 },\n\t{ u\"folder4\"_q           , Command::ShowFolder4 },\n\t{ u\"folder5\"_q           , Command::ShowFolder5 },\n\t{ u\"folder6\"_q           , Command::ShowFolder6 },\n\t{ u\"last_folder\"_q       , Command::ShowFolderLast },\n\n\t{ u\"show_archive\"_q      , Command::ShowArchive },\n\t{ u\"show_contacts\"_q     , Command::ShowContacts },\n\n\t{ u\"read_chat\"_q         , Command::ReadChat },\n\n\t{ u\"show_chat_menu\"_q    , Command::ShowChatMenu },\n\t{ u\"show_chat_preview\"_q , Command::ShowChatPreview },\n\n\t{ u\"record_voice\"_q      , Command::RecordVoice },\n\n\t// Shortcuts that have no default values.\n\t{ u\"message\"_q                       , Command::JustSendMessage },\n\t{ u\"message_silently\"_q              , Command::SendSilentMessage },\n\t{ u\"message_scheduled\"_q             , Command::ScheduleMessage },\n\t{ u\"media_viewer_video_fullscreen\"_q , Command::MediaViewerFullscreen },\n\t{ u\"show_scheduled\"_q                , Command::ShowScheduled },\n\t{ u\"archive_chat\"_q                  , Command::ArchiveChat },\n\t{ u\"record_round\"_q                  , Command::RecordRound },\n\t{ u\"show_admin_log\"_q                , Command::ShowAdminLog },\n\t//\n};\n\nconst base::flat_map<Command, QString> &CommandNames() {\n\tstatic const auto result = [&] {\n\t\tauto result = base::flat_map<Command, QString>();\n\t\tfor (const auto &[name, command] : CommandByName) {\n\t\t\tresult.emplace(command, name);\n\t\t}\n\t\treturn result;\n\t}();\n\treturn result;\n};\n\n[[maybe_unused]] constexpr auto kNoValue = {\n\tCommand::JustSendMessage,\n\tCommand::SendSilentMessage,\n\tCommand::ScheduleMessage,\n\tCommand::MediaViewerFullscreen,\n\tCommand::ShowScheduled,\n\tCommand::ArchiveChat,\n\tCommand::RecordRound,\n};\n\nclass Manager {\npublic:\n\tvoid fill();\n\tvoid clear();\n\n\t[[nodiscard]] std::vector<Command> lookup(\n\t\tnot_null<QObject*> object) const;\n\tvoid toggleMedia(bool toggled);\n\tvoid toggleSupport(bool toggled);\n\tvoid listen(not_null<QWidget*> widget);\n\t[[nodiscard]] bool handles(const QKeySequence &sequence) const;\n\n\t[[nodiscard]] const QStringList &errors() const;\n\n\t[[nodiscard]] auto keysDefaults() const\n\t\t-> base::flat_map<QKeySequence, base::flat_set<Command>>;\n\t[[nodiscard]] auto keysCurrents() const\n\t\t-> base::flat_map<QKeySequence, base::flat_set<Command>>;\n\n\tvoid change(\n\t\tQKeySequence was,\n\t\tQKeySequence now,\n\t\tCommand command,\n\t\tstd::optional<Command> restore);\n\tvoid resetToDefaults();\n\nprivate:\n\tvoid fillDefaults();\n\tvoid writeDefaultFile();\n\tvoid writeCustomFile();\n\tbool readCustomFile();\n\n\tvoid set(const QString &keys, Command command, bool replace = false);\n\tvoid set(const QKeySequence &result, Command command, bool replace);\n\tvoid remove(const QString &keys);\n\tvoid remove(const QKeySequence &keys);\n\tvoid remove(const QKeySequence &keys, Command command);\n\tvoid unregister(base::unique_qptr<QAction> shortcut);\n\n\tvoid pruneListened();\n\n\tQStringList _errors;\n\n\tbase::flat_map<QKeySequence, base::unique_qptr<QAction>> _shortcuts;\n\tbase::flat_multi_map<not_null<QObject*>, Command> _commandByObject;\n\tstd::vector<QPointer<QWidget>> _listened;\n\n\tbase::flat_map<QKeySequence, base::flat_set<Command>> _defaults;\n\n\tbase::flat_set<QAction*> _mediaShortcuts;\n\tbase::flat_set<QAction*> _supportShortcuts;\n\n};\n\nQString DefaultFilePath() {\n\treturn cWorkingDir() + u\"tdata/shortcuts-default.json\"_q;\n}\n\nQString CustomFilePath() {\n\treturn cWorkingDir() + u\"tdata/shortcuts-custom.json\"_q;\n}\n\nbool DefaultFileIsValid() {\n\tQFile file(DefaultFilePath());\n\tif (!file.open(QIODevice::ReadOnly)) {\n\t\treturn false;\n\t}\n\tauto error = QJsonParseError{ 0, QJsonParseError::NoError };\n\tconst auto document = QJsonDocument::fromJson(\n\t\tbase::parse::stripComments(file.readAll()),\n\t\t&error);\n\tfile.close();\n\n\tif (error.error != QJsonParseError::NoError || !document.isArray()) {\n\t\treturn false;\n\t}\n\tconst auto shortcuts = document.array();\n\tif (shortcuts.isEmpty() || !(*shortcuts.constBegin()).isObject()) {\n\t\treturn false;\n\t}\n\tconst auto versionObject = (*shortcuts.constBegin()).toObject();\n\tconst auto version = versionObject.constFind(u\"version\"_q);\n\tif (version == versionObject.constEnd()\n\t\t|| !(*version).isString()\n\t\t|| (*version).toString() != QString::number(AppVersion)) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nvoid WriteDefaultCustomFile() {\n\tconst auto path = CustomFilePath();\n\tauto input = QFile(\":/misc/default_shortcuts-custom.json\");\n\tauto output = QFile(path);\n\tif (input.open(QIODevice::ReadOnly)\n\t\t&& output.open(QIODevice::WriteOnly)) {\n#ifdef Q_OS_MAC\n\t\tauto text = qs(input.readAll());\n\t\tconst auto note = R\"(\n// Note:\n// On Apple platforms, reference to \"ctrl\" corresponds to the Command keys )\"\n\t\t\t+ QByteArray()\n\t\t\t+ R\"(on the Macintosh keyboard.\n// On Apple platforms, reference to \"meta\" corresponds to the Control keys.\n\n[\n)\";\n\t\ttext.replace(u\"\\n\\n[\"_q, QString(note));\n\t\toutput.write(text.toUtf8());\n#else\n\t\toutput.write(input.readAll());\n#endif // !Q_OS_MAC\n\t}\n}\n\nvoid Manager::fill() {\n\tfillDefaults();\n\n\tif (!DefaultFileIsValid()) {\n\t\twriteDefaultFile();\n\t}\n\tif (!readCustomFile()) {\n\t\tWriteDefaultCustomFile();\n\t}\n}\n\nvoid Manager::clear() {\n\t_errors.clear();\n\t_shortcuts.clear();\n\t_commandByObject.clear();\n\t_mediaShortcuts.clear();\n\t_supportShortcuts.clear();\n}\n\nconst QStringList &Manager::errors() const {\n\treturn _errors;\n}\n\nauto Manager::keysDefaults() const\n-> base::flat_map<QKeySequence, base::flat_set<Command>> {\n\treturn _defaults;\n}\n\nauto Manager::keysCurrents() const\n-> base::flat_map<QKeySequence, base::flat_set<Command>> {\n\tauto result = base::flat_map<QKeySequence, base::flat_set<Command>>();\n\tfor (const auto &[keys, command] : _shortcuts) {\n\t\tauto i = _commandByObject.findFirst(command);\n\t\tconst auto end = _commandByObject.end();\n\t\tfor (; i != end && (i->first == command); ++i) {\n\t\t\tresult[keys].emplace(i->second);\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid Manager::change(\n\t\tQKeySequence was,\n\t\tQKeySequence now,\n\t\tCommand command,\n\t\tstd::optional<Command> restore) {\n\tif (!was.isEmpty()) {\n\t\tremove(was, command);\n\t}\n\tif (!now.isEmpty()) {\n\t\tset(now, command, true);\n\t}\n\tif (restore) {\n\t\tAssert(!was.isEmpty());\n\t\tset(was, *restore, true);\n\t}\n\twriteCustomFile();\n}\n\nvoid Manager::resetToDefaults() {\n\twhile (!_shortcuts.empty()) {\n\t\tremove(_shortcuts.begin()->first);\n\t}\n\tfor (const auto &[sequence, commands] : _defaults) {\n\t\tfor (const auto command : commands) {\n\t\t\tset(sequence, command, false);\n\t\t}\n\t}\n\twriteCustomFile();\n}\n\nstd::vector<Command> Manager::lookup(not_null<QObject*> object) const {\n\tauto result = std::vector<Command>();\n\tauto i = _commandByObject.findFirst(object);\n\tconst auto end = _commandByObject.end();\n\tfor (; i != end && (i->first == object); ++i) {\n\t\tresult.push_back(i->second);\n\t}\n\treturn result;\n}\n\nvoid Manager::toggleMedia(bool toggled) {\n\tfor (const auto shortcut : _mediaShortcuts) {\n\t\tshortcut->setEnabled(toggled);\n\t}\n}\n\nvoid Manager::toggleSupport(bool toggled) {\n\tfor (const auto shortcut : _supportShortcuts) {\n\t\tshortcut->setEnabled(toggled);\n\t}\n}\n\nvoid Manager::listen(not_null<QWidget*> widget) {\n\tpruneListened();\n\t_listened.push_back(widget.get());\n\tfor (const auto &[keys, shortcut] : _shortcuts) {\n\t\twidget->addAction(shortcut.get());\n\t}\n}\n\nbool Manager::handles(const QKeySequence &sequence) const {\n\treturn _shortcuts.contains(sequence);\n}\n\nvoid Manager::pruneListened() {\n\tfor (auto i = begin(_listened); i != end(_listened);) {\n\t\tif (i->data()) {\n\t\t\t++i;\n\t\t} else {\n\t\t\ti = _listened.erase(i);\n\t\t}\n\t}\n}\n\nbool Manager::readCustomFile() {\n\t// read custom shortcuts from file if it exists or write an empty custom shortcuts file\n\tQFile file(CustomFilePath());\n\tif (!file.exists()) {\n\t\treturn false;\n\t}\n\tconst auto guard = gsl::finally([&] {\n\t\tif (!_errors.isEmpty()) {\n\t\t\t_errors.push_front((u\"While reading file '%1'...\"_q\n\t\t\t).arg(file.fileName()));\n\t\t}\n\t});\n\tif (!file.open(QIODevice::ReadOnly)) {\n\t\t_errors.push_back(u\"Could not read the file!\"_q);\n\t\treturn true;\n\t}\n\tauto error = QJsonParseError{ 0, QJsonParseError::NoError };\n\tconst auto document = QJsonDocument::fromJson(\n\t\tbase::parse::stripComments(file.readAll()),\n\t\t&error);\n\tfile.close();\n\n\tif (error.error != QJsonParseError::NoError) {\n\t\t_errors.push_back((u\"Failed to parse! Error: %2\"_q\n\t\t).arg(error.errorString()));\n\t\treturn true;\n\t} else if (!document.isArray()) {\n\t\t_errors.push_back(u\"Failed to parse! Error: array expected\"_q);\n\t\treturn true;\n\t}\n\tconst auto shortcuts = document.array();\n\tauto limit = kCountLimit;\n\tfor (auto i = shortcuts.constBegin(), e = shortcuts.constEnd(); i != e; ++i) {\n\t\tif (!(*i).isObject()) {\n\t\t\t_errors.push_back(u\"Bad entry! Error: object expected\"_q);\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto entry = (*i).toObject();\n\t\tconst auto keys = entry.constFind(u\"keys\"_q);\n\t\tconst auto command = entry.constFind(u\"command\"_q);\n\t\tconst auto removed = entry.constFind(u\"removed\"_q);\n\t\tif (keys == entry.constEnd()\n\t\t\t|| command == entry.constEnd()\n\t\t\t|| !(*keys).isString()\n\t\t\t|| (!(*command).isString() && !(*command).isNull())) {\n\t\t\t_errors.push_back(qsl(\"Bad entry! \"\n\t\t\t\t\"{\\\"keys\\\": \\\"...\\\", \\\"command\\\": [ \\\"...\\\" | null ]} \"\n\t\t\t\t\"expected.\"));\n\t\t} else if ((*command).isNull()) {\n\t\t\tremove((*keys).toString());\n\t\t} else {\n\t\t\tconst auto name = (*command).toString();\n\t\t\tconst auto i = CommandByName.find(name);\n\t\t\tif (i != end(CommandByName)) {\n\t\t\t\tif (removed != entry.constEnd() && removed->toBool()) {\n\t\t\t\t\tremove((*keys).toString(), i->second);\n\t\t\t\t} else {\n\t\t\t\t\tset((*keys).toString(), i->second, true);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tLOG((\"Shortcut Warning: \"\n\t\t\t\t\t\"could not find shortcut command handler '%1'\"\n\t\t\t\t\t).arg(name));\n\t\t\t}\n\t\t}\n\t\tif (!--limit) {\n\t\t\t_errors.push_back(u\"Too many entries! Limit is %1\"_q.arg(\n\t\t\t\tkCountLimit));\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid Manager::fillDefaults() {\n\tconst auto ctrl = Platform::IsMac() ? u\"meta\"_q : u\"ctrl\"_q;\n\n\tset(u\"ctrl+w\"_q, Command::Close);\n\tset(u\"ctrl+f4\"_q, Command::Close);\n\tset(u\"ctrl+l\"_q, Command::Lock);\n\tset(u\"ctrl+m\"_q, Command::Minimize);\n\tset(u\"ctrl+q\"_q, Command::Quit);\n\n\tset(u\"media play\"_q, Command::MediaPlay);\n\tset(u\"media pause\"_q, Command::MediaPause);\n\tset(u\"toggle media play/pause\"_q, Command::MediaPlayPause);\n\tset(u\"media stop\"_q, Command::MediaStop);\n\tset(u\"media previous\"_q, Command::MediaPrevious);\n\tset(u\"media next\"_q, Command::MediaNext);\n\n\tset(u\"ctrl+f\"_q, Command::Search);\n\tset(u\"search\"_q, Command::Search);\n\tset(u\"find\"_q, Command::Search);\n\n\tset(u\"ctrl+pgdown\"_q, Command::ChatNext);\n\tset(u\"alt+down\"_q, Command::ChatNext);\n\tset(u\"ctrl+pgup\"_q, Command::ChatPrevious);\n\tset(u\"alt+up\"_q, Command::ChatPrevious);\n\n\tset(u\"ctrl+alt+home\"_q, Command::ChatFirst);\n\tset(u\"ctrl+alt+end\"_q, Command::ChatLast);\n\n\tset(u\"f5\"_q, Command::SupportReloadTemplates);\n\tset(u\"ctrl+delete\"_q, Command::SupportToggleMuted);\n\tset(u\"ctrl+insert\"_q, Command::SupportScrollToCurrent);\n\tset(u\"ctrl+shift+x\"_q, Command::SupportHistoryBack);\n\tset(u\"ctrl+shift+c\"_q, Command::SupportHistoryForward);\n\n\tset(u\"ctrl+1\"_q, Command::ChatPinned1);\n\tset(u\"ctrl+2\"_q, Command::ChatPinned2);\n\tset(u\"ctrl+3\"_q, Command::ChatPinned3);\n\tset(u\"ctrl+4\"_q, Command::ChatPinned4);\n\tset(u\"ctrl+5\"_q, Command::ChatPinned5);\n\tset(u\"ctrl+6\"_q, Command::ChatPinned6);\n\tset(u\"ctrl+7\"_q, Command::ChatPinned7);\n\tset(u\"ctrl+8\"_q, Command::ChatPinned8);\n\n\tauto &&folders = ranges::views::zip(\n\t\tkShowFolder,\n\t\tranges::views::ints(1, ranges::unreachable));\n\n\tfor (const auto &[command, index] : folders) {\n\t\tset(u\"%1+%2\"_q.arg(ctrl).arg(index), command);\n\t}\n\n\tset(u\"%1+shift+down\"_q.arg(ctrl), Command::FolderNext);\n\tset(u\"%1+shift+up\"_q.arg(ctrl), Command::FolderPrevious);\n\n\tset(u\"ctrl+0\"_q, Command::ChatSelf);\n\n\tset(u\"ctrl+9\"_q, Command::ShowArchive);\n\tset(u\"ctrl+j\"_q, Command::ShowContacts);\n\n\tset(u\"ctrl+r\"_q, Command::ReadChat);\n\n\tset(u\"ctrl+\\\\\"_q, Command::ShowChatMenu);\n\tset(u\"ctrl+]\"_q, Command::ShowChatPreview);\n\n\tset(u\"ctrl+r\"_q, Command::RecordVoice);\n\n\t_defaults = keysCurrents();\n}\n\nvoid Manager::writeDefaultFile() {\n\tauto file = QFile(DefaultFilePath());\n\tif (!file.open(QIODevice::WriteOnly)) {\n\t\treturn;\n\t}\n\tconst char *defaultHeader = R\"HEADER(\n// This is a list of default shortcuts for Telegram Desktop\n// Please don't modify it, its content is not used in any way\n// You can place your own shortcuts in the 'shortcuts-custom.json' file\n\n)HEADER\";\n\tfile.write(defaultHeader);\n\n\tauto shortcuts = QJsonArray();\n\tauto version = QJsonObject();\n\tversion.insert(u\"version\"_q, QString::number(AppVersion));\n\tshortcuts.push_back(version);\n\n\tfor (const auto &[sequence, shortcut] : _shortcuts) {\n\t\tconst auto object = shortcut.get();\n\t\tauto i = _commandByObject.findFirst(object);\n\t\tconst auto end = _commandByObject.end();\n\t\tfor (; i != end && i->first == object; ++i) {\n\t\t\tconst auto j = CommandNames().find(i->second);\n\t\t\tif (j != CommandNames().end()) {\n\t\t\t\tQJsonObject entry;\n\t\t\t\tentry.insert(u\"keys\"_q, sequence.toString().toLower());\n\t\t\t\tentry.insert(u\"command\"_q, j->second);\n\t\t\t\tshortcuts.append(entry);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Commands without a default value.\n\tfor (const auto c : ranges::views::concat(kShowAccount, kNoValue)) {\n\t\tfor (const auto &[name, command] : CommandByName) {\n\t\t\tif (c == command) {\n\t\t\t\tauto entry = QJsonObject();\n\t\t\t\tentry.insert(u\"keys\"_q, QJsonValue());\n\t\t\t\tentry.insert(u\"command\"_q, name);\n\t\t\t\tshortcuts.append(entry);\n\t\t\t}\n\t\t}\n\t}\n\n\tauto document = QJsonDocument();\n\tdocument.setArray(shortcuts);\n\tfile.write(document.toJson(QJsonDocument::Indented));\n}\n\nvoid Manager::writeCustomFile() {\n\tauto shortcuts = QJsonArray();\n\tfor (const auto &[sequence, shortcut] : _shortcuts) {\n\t\tconst auto object = shortcut.get();\n\t\tauto i = _commandByObject.findFirst(object);\n\t\tconst auto end = _commandByObject.end();\n\t\tfor (; i != end && i->first == object; ++i) {\n\t\t\tconst auto d = _defaults.find(sequence);\n\t\t\tif (d == _defaults.end() || !d->second.contains(i->second)) {\n\t\t\t\tconst auto j = CommandNames().find(i->second);\n\t\t\t\tif (j != CommandNames().end()) {\n\t\t\t\t\tQJsonObject entry;\n\t\t\t\t\tentry.insert(u\"keys\"_q, sequence.toString().toLower());\n\t\t\t\t\tentry.insert(u\"command\"_q, j->second);\n\t\t\t\t\tshortcuts.append(entry);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tconst auto has = [&](not_null<QObject*> shortcut, Command command) {\n\t\tfor (auto i = _commandByObject.findFirst(shortcut)\n\t\t\t; i != end(_commandByObject) && i->first == shortcut\n\t\t\t; ++i) {\n\t\t\tif (i->second == command) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\tfor (const auto &[sequence, commands] : _defaults) {\n\t\tconst auto i = _shortcuts.find(sequence);\n\t\tif (i == end(_shortcuts)) {\n\t\t\tQJsonObject entry;\n\t\t\tentry.insert(u\"keys\"_q, sequence.toString().toLower());\n\t\t\tentry.insert(u\"command\"_q, QJsonValue());\n\t\t\tshortcuts.append(entry);\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const auto command : commands) {\n\t\t\tif (!has(i->second.get(), command)) {\n\t\t\t\tconst auto j = CommandNames().find(command);\n\t\t\t\tif (j != CommandNames().end()) {\n\t\t\t\t\tQJsonObject entry;\n\t\t\t\t\tentry.insert(u\"keys\"_q, sequence.toString().toLower());\n\t\t\t\t\tentry.insert(u\"command\"_q, j->second);\n\t\t\t\t\tentry.insert(u\"removed\"_q, true);\n\t\t\t\t\tshortcuts.append(entry);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (shortcuts.isEmpty()) {\n\t\tWriteDefaultCustomFile();\n\t\treturn;\n\t}\n\n\tauto file = QFile(CustomFilePath());\n\tif (!file.open(QIODevice::WriteOnly)) {\n\t\tLOG((\"Shortcut Warning: could not write custom shortcuts file.\"));\n\t\treturn;\n\t}\n\tconst char *customHeader = R\"HEADER(\n// This is a list of changed shortcuts for Telegram Desktop\n// You can edit them in Settings > Chat Settings > Keyboard Shortcuts.\n\n)HEADER\";\n\tfile.write(customHeader);\n\n\tauto document = QJsonDocument();\n\tdocument.setArray(shortcuts);\n\tfile.write(document.toJson(QJsonDocument::Indented));\n}\n\nvoid Manager::set(const QString &keys, Command command, bool replace) {\n\tif (keys.isEmpty()) {\n\t\treturn;\n\t}\n\n\tconst auto result = QKeySequence(keys, QKeySequence::PortableText);\n\tif (result.isEmpty()) {\n\t\t_errors.push_back(u\"Could not derive key sequence '%1'!\"_q.arg(keys));\n\t\treturn;\n\t}\n\tset(result, command, replace);\n}\n\nvoid Manager::set(\n\t\tconst QKeySequence &keys,\n\t\tCommand command,\n\t\tbool replace) {\n\tauto shortcut = base::make_unique_q<QAction>();\n\tshortcut->setShortcut(keys);\n\tshortcut->setShortcutContext(Qt::ApplicationShortcut);\n\tif (!AutoRepeatCommands.contains(command)) {\n\t\tshortcut->setAutoRepeat(false);\n\t}\n\tconst auto isMediaShortcut = MediaCommands.contains(command);\n\tconst auto isSupportShortcut = SupportCommands.contains(command);\n\tif (isMediaShortcut || isSupportShortcut) {\n\t\tshortcut->setEnabled(false);\n\t}\n\tauto object = shortcut.get();\n\tauto i = _shortcuts.find(keys);\n\tif (i == end(_shortcuts)) {\n\t\ti = _shortcuts.emplace(keys, std::move(shortcut)).first;\n\t} else if (replace) {\n\t\tunregister(std::exchange(i->second, std::move(shortcut)));\n\t} else {\n\t\tobject = i->second.get();\n\t}\n\t_commandByObject.emplace(object, command);\n\tif (!shortcut) { // Added the new one.\n\t\tif (isMediaShortcut) {\n\t\t\t_mediaShortcuts.emplace(i->second.get());\n\t\t}\n\t\tif (isSupportShortcut) {\n\t\t\t_supportShortcuts.emplace(i->second.get());\n\t\t}\n\t\tpruneListened();\n\t\tfor (const auto &widget : _listened) {\n\t\t\twidget->addAction(i->second.get());\n\t\t}\n\t}\n}\n\nvoid Manager::remove(const QString &keys) {\n\tif (keys.isEmpty()) {\n\t\treturn;\n\t}\n\n\tconst auto result = QKeySequence(keys, QKeySequence::PortableText);\n\tif (result.isEmpty()) {\n\t\t_errors.push_back(u\"Could not derive key sequence '%1'!\"_q.arg(keys));\n\t\treturn;\n\t}\n\tremove(result);\n}\n\nvoid Manager::remove(const QKeySequence &keys) {\n\tconst auto i = _shortcuts.find(keys);\n\tif (i != end(_shortcuts)) {\n\t\tunregister(std::move(i->second));\n\t\t_shortcuts.erase(i);\n\t}\n}\n\nvoid Manager::remove(const QKeySequence &keys, Command command) {\n\tconst auto i = _shortcuts.find(keys);\n\tif (i != end(_shortcuts)) {\n\t\t_commandByObject.remove(i->second.get(), command);\n\t\tif (!_commandByObject.contains(i->second.get())) {\n\t\t\tunregister(std::move(i->second));\n\t\t\t_shortcuts.erase(i);\n\t\t}\n\t}\n}\n\nvoid Manager::unregister(base::unique_qptr<QAction> shortcut) {\n\tif (shortcut) {\n\t\t_commandByObject.removeAll(shortcut.get());\n\t\t_mediaShortcuts.erase(shortcut.get());\n\t\t_supportShortcuts.erase(shortcut.get());\n\t}\n}\n\nManager Data;\n\n} // namespace\n\nRequest::Request(std::vector<Command> commands)\n: _commands(std::move(commands)) {\n}\n\nbool Request::check(Command command, int priority) {\n\tif (ranges::contains(_commands, command)\n\t\t&& priority > _handlerPriority) {\n\t\t_handlerPriority = priority;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool Request::handle(FnMut<bool()> handler) {\n\t_handler = std::move(handler);\n\treturn true;\n}\n\nFnMut<bool()> RequestHandler(std::vector<Command> commands) {\n\tauto request = Request(std::move(commands));\n\tRequestsStream.fire(&request);\n\treturn std::move(request._handler);\n}\n\nFnMut<bool()> RequestHandler(Command command) {\n\treturn RequestHandler(std::vector<Command>{ command });\n}\n\nbool Launch(Command command) {\n\tif (auto handler = RequestHandler(command)) {\n\t\treturn handler();\n\t}\n\treturn false;\n}\n\nbool Launch(std::vector<Command> commands) {\n\tif (Paused) {\n\t\treturn false;\n\t} else if (auto handler = RequestHandler(std::move(commands))) {\n\t\treturn handler();\n\t}\n\treturn false;\n}\n\nrpl::producer<not_null<Request*>> Requests() {\n\treturn RequestsStream.events();\n}\n\nvoid Start() {\n\tData.fill();\n}\n\nconst QStringList &Errors() {\n\treturn Data.errors();\n}\n\nbool MarkChatSwitchKeyPressHandled(Qt::Key key, bool handled) {\n\tif (!key) {\n\t\treturn false;\n\t} else if (handled) {\n\t\tif (ranges::contains(ChatSwitchKeyPressHandled, key)) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto i = ranges::find(ChatSwitchKeyPressHandled, Qt::Key(0));\n\t\tAssert(i != end(ChatSwitchKeyPressHandled));\n\t\t*i = key;\n\t\treturn true;\n\t}\n\tconst auto i = ranges::find(ChatSwitchKeyPressHandled, key);\n\tif (i == end(ChatSwitchKeyPressHandled)) {\n\t\treturn false;\n\t}\n\t*i = Qt::Key(0);\n\treturn true;\n}\n\nbool CancelChatSwitch(Qt::Key result) {\n\tChatSwitchModifier = Qt::Key();\n\tif (!ChatSwitchStarted) {\n\t\treturn false;\n\t}\n\tChatSwitchStarted = false;\n\tdelete base::take(ChatSwitchFilter);\n\tChatSwitchStream.fire({ .action = result });\n\treturn true;\n}\n\nbool NavigateChatSwitch(Qt::Key result) {\n\tif (!ChatSwitchStarted) {\n\t\treturn false;\n\t}\n\tChatSwitchStream.fire({ .action = result });\n\treturn true;\n}\n\nbool CheckChatSwitchEvent(Qt::Key key) {\n\tconst auto plain = Qt::Key(int(key)\n\t\t& ~(Qt::ControlModifier\n\t\t\t| Qt::ShiftModifier\n\t\t\t| Qt::AltModifier\n\t\t\t| Qt::MetaModifier));\n\tif (MarkChatSwitchKeyPressHandled(plain, false)) {\n\t\treturn true;\n\t} else if (plain == Qt::Key_Escape) {\n\t\treturn CancelChatSwitch(Qt::Key_Escape);\n\t} else if (plain == Qt::Key_Return || plain == Qt::Key_Enter) {\n\t\treturn CancelChatSwitch(Qt::Key_Enter);\n\t} else if (ranges::contains(kChatSwitchSpecialKeys, plain)) {\n\t\treturn NavigateChatSwitch(Qt::Key(plain));\n\t}\n\treturn false;\n}\n\nbool HandleEvent(\n\t\tnot_null<QObject*> object,\n\t\tnot_null<QShortcutEvent*> event) {\n\tif (ChatSwitchStarted) {\n\t\tconst auto full = event->key();\n\t\tfor (auto i = 0; i != full.count(); ++i) {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)\n\t\t\tconst auto raw = full[i].key();\n#else // Qt >= 6.0.0\n\t\t\tconst auto raw = full[i];\n#endif // Qt < 6.0.0\n\t\t\tif (CheckChatSwitchEvent(Qt::Key(raw))) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\treturn Launch(Data.lookup(object));\n}\n\nrpl::producer<ChatSwitchRequest> ChatSwitchRequests() {\n\treturn ChatSwitchStream.events();\n}\n\nbool HandlePossibleChatSwitch(not_null<QKeyEvent*> event) {\n\tconst auto type = event->type();\n\tif (Paused) {\n\t\treturn false;\n\t} else if (type == QEvent::ShortcutOverride) {\n\t\tconst auto key = Qt::Key(event->key());\n\t\tconst auto ctrl = Platform::IsMac()\n\t\t\t? Qt::MetaModifier\n\t\t\t: Qt::ControlModifier;\n\t\tif ((event->modifiers() & ctrl)\n\t\t\t&& ranges::contains(kChatSwitchSpecialKeys, key)) {\n\t\t\t// For Ctrl+Q in case of active chat switch we have\n\t\t\t// on Windows always ShortcutOverride + KeyPress,\n\t\t\t// while on macOS we have just ShortcutOverride for\n\t\t\t// the first press and KeyPress only for repeat events.\n\t\t\t//\n\t\t\t// This complex scheme allows to handle both cases with\n\t\t\t// firing the switch event Key_Q exactly once initially.\n\t\t\tif (CheckChatSwitchEvent(key)) {\n\t\t\t\tMarkChatSwitchKeyPressHandled(key, true);\n\t\t\t}\n\t\t\tcrl::on_main([=] {\n\t\t\t\tMarkChatSwitchKeyPressHandled(key, false);\n\t\t\t});\n\t\t}\n\t\tif (Data.handles(QKeySequence(ctrl | Qt::Key_Tab))\n\t\t\t&& (Data.handles(ctrl | Qt::ShiftModifier | Qt::Key_Backtab)\n\t\t\t\t|| Data.handles(ctrl | Qt::ShiftModifier | Qt::Key_Tab)\n\t\t\t\t|| Data.handles(QKeySequence(ctrl | Qt::Key_Backtab)))) {\n\t\t\treturn false;\n\t\t} else if (key == Qt::Key_Control || key == Qt::Key_Meta) {\n\t\t\tChatSwitchModifier = key;\n\t\t} else if (key == Qt::Key_Tab || key == Qt::Key_Backtab) {\n\t\t\tconst auto modifiers = event->modifiers();\n\t\t\tif (modifiers & ctrl) {\n\t\t\t\tif (ChatSwitchModifier == Qt::Key()) {\n\t\t\t\t\tChatSwitchModifier = Platform::IsMac()\n\t\t\t\t\t\t? Qt::Key_Meta\n\t\t\t\t\t\t: Qt::Key_Control;\n\t\t\t\t}\n\t\t\t\tconst auto action = (modifiers & Qt::ShiftModifier)\n\t\t\t\t\t? Qt::Key_Backtab\n\t\t\t\t\t: key;\n\t\t\t\tif (Data.handles(modifiers | key)) {\n\t\t\t\t\treturn false;\n\t\t\t\t} else if (action == Qt::Key_Tab\n\t\t\t\t\t&& Data.handles(QKeySequence(ctrl | Qt::Key_Tab))) {\n\t\t\t\t\treturn false;\n\t\t\t\t} else if (action == Qt::Key_Backtab\n\t\t\t\t\t&& Data.handles(QKeySequence(ctrl | Qt::Key_Backtab))) {\n\t\t\t\t\treturn false;\n\t\t\t\t} else if (action == Qt::Key_Backtab\n\t\t\t\t\t&& Data.handles(ctrl | Qt::ShiftModifier | Qt::Key_Tab)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst auto started = !std::exchange(ChatSwitchStarted, true);\n\t\t\t\tif (started) {\n\t\t\t\t\tAssert(!ChatSwitchFilter);\n\t\t\t\t\tChatSwitchFilter = base::install_event_filter(qApp, [=](not_null<QEvent*> e) {\n\t\t\t\t\t\treturn (e->type() == QEvent::InputMethod)\n\t\t\t\t\t\t\t? base::EventFilterResult::Cancel\n\t\t\t\t\t\t\t: base::EventFilterResult::Continue;\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tChatSwitchStream.fire({\n\t\t\t\t\t.action = action,\n\t\t\t\t\t.started = started,\n\t\t\t\t});\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t} else if (type == QEvent::KeyPress) {\n\t\treturn CheckChatSwitchEvent(Qt::Key(event->key()));\n\t} else if (type == QEvent::KeyRelease) {\n\t\tconst auto key = Qt::Key(event->key());\n\t\tif (key == ChatSwitchModifier) {\n\t\t\tCancelChatSwitch(Qt::Key_Enter);\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid ToggleMediaShortcuts(bool toggled) {\n\tData.toggleMedia(toggled);\n}\n\nvoid ToggleSupportShortcuts(bool toggled) {\n\tData.toggleSupport(toggled);\n}\n\nvoid Pause() {\n\tPaused = true;\n}\n\nvoid Unpause() {\n\tPaused = false;\n}\n\nauto KeysDefaults()\n-> base::flat_map<QKeySequence, base::flat_set<Command>> {\n\treturn Data.keysDefaults();\n}\n\nauto KeysCurrents()\n-> base::flat_map<QKeySequence, base::flat_set<Command>> {\n\treturn Data.keysCurrents();\n}\n\nvoid Change(\n\t\tQKeySequence was,\n\t\tQKeySequence now,\n\t\tCommand command,\n\t\tstd::optional<Command> restore) {\n\tData.change(was, now, command, restore);\n}\n\nvoid ResetToDefaults() {\n\tData.resetToDefaults();\n}\n\nbool AllowWithoutModifiers(int key) {\n\tconst auto service = {\n\t\tQt::Key_Escape,\n\t\tQt::Key_Tab,\n\t\tQt::Key_Backtab,\n\t\tQt::Key_Backspace,\n\t\tQt::Key_Return,\n\t\tQt::Key_Enter,\n\t\tQt::Key_Insert,\n\t\tQt::Key_Delete,\n\t\tQt::Key_Pause,\n\t\tQt::Key_Print,\n\t\tQt::Key_SysReq,\n\t\tQt::Key_Clear,\n\t\tQt::Key_Home,\n\t\tQt::Key_End,\n\t\tQt::Key_Left,\n\t\tQt::Key_Up,\n\t\tQt::Key_Right,\n\t\tQt::Key_Down,\n\t\tQt::Key_PageUp,\n\t\tQt::Key_PageDown,\n\t\tQt::Key_Shift,\n\t\tQt::Key_Control,\n\t\tQt::Key_Meta,\n\t\tQt::Key_Alt,\n\t\tQt::Key_CapsLock,\n\t\tQt::Key_NumLock,\n\t\tQt::Key_ScrollLock,\n\t};\n\treturn (key >= 0x80) && !ranges::contains(service, key);\n}\n\nvoid Finish() {\n\tData.clear();\n}\n\nvoid Listen(not_null<QWidget*> widget) {\n\tData.listen(widget);\n}\n\n} // namespace Shortcuts\n"
  },
  {
    "path": "Telegram/SourceFiles/core/shortcuts.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass QKeyEvent;\nclass QShortcutEvent;\n\nnamespace Shortcuts {\n\nenum class Command {\n\tClose,\n\tLock,\n\tMinimize,\n\tQuit,\n\n\tMediaPlay,\n\tMediaPause,\n\tMediaPlayPause,\n\tMediaStop,\n\tMediaPrevious,\n\tMediaNext,\n\n\tSearch,\n\n\tChatPrevious,\n\tChatNext,\n\tChatFirst,\n\tChatLast,\n\tChatSelf,\n\tChatPinned1,\n\tChatPinned2,\n\tChatPinned3,\n\tChatPinned4,\n\tChatPinned5,\n\tChatPinned6,\n\tChatPinned7,\n\tChatPinned8,\n\n\tShowAccount1,\n\tShowAccount2,\n\tShowAccount3,\n\tShowAccount4,\n\tShowAccount5,\n\tShowAccount6,\n\n\tShowAllChats,\n\tShowFolder1,\n\tShowFolder2,\n\tShowFolder3,\n\tShowFolder4,\n\tShowFolder5,\n\tShowFolder6,\n\tShowFolderLast,\n\n\tFolderNext,\n\tFolderPrevious,\n\n\tShowScheduled,\n\n\tShowArchive,\n\tShowContacts,\n\n\tJustSendMessage,\n\tSendSilentMessage,\n\tScheduleMessage,\n\n\tRecordVoice,\n\tRecordRound,\n\n\tReadChat,\n\tArchiveChat,\n\n\tMediaViewerFullscreen,\n\n\tShowChatMenu,\n\tShowChatPreview,\n\n\tShowAdminLog,\n\n\tSupportReloadTemplates,\n\tSupportToggleMuted,\n\tSupportScrollToCurrent,\n\tSupportHistoryBack,\n\tSupportHistoryForward,\n};\n\n[[maybe_unused]] constexpr auto kShowFolder = {\n\tCommand::ShowAllChats,\n\tCommand::ShowFolder1,\n\tCommand::ShowFolder2,\n\tCommand::ShowFolder3,\n\tCommand::ShowFolder4,\n\tCommand::ShowFolder5,\n\tCommand::ShowFolder6,\n\tCommand::ShowFolderLast,\n};\n\n[[maybe_unused]] constexpr auto kShowAccount = {\n\tCommand::ShowAccount1,\n\tCommand::ShowAccount2,\n\tCommand::ShowAccount3,\n\tCommand::ShowAccount4,\n\tCommand::ShowAccount5,\n\tCommand::ShowAccount6,\n};\n\n[[nodiscard]] FnMut<bool()> RequestHandler(Command command);\n\nclass Request {\npublic:\n\tbool check(Command command, int priority = 0);\n\tbool handle(FnMut<bool()> handler);\n\nprivate:\n\texplicit Request(std::vector<Command> commands);\n\n\tstd::vector<Command> _commands;\n\tint _handlerPriority = -1;\n\tFnMut<bool()> _handler;\n\n\tfriend FnMut<bool()> RequestHandler(std::vector<Command> commands);\n\n};\n\n[[nodiscard]] rpl::producer<not_null<Request*>> Requests();\n\nvoid Start();\nvoid Finish();\n\nvoid Listen(not_null<QWidget*> widget);\n\nbool Launch(Command command);\nbool HandleEvent(not_null<QObject*> object, not_null<QShortcutEvent*> event);\n\nbool HandlePossibleChatSwitch(not_null<QKeyEvent*> event);\n\nstruct ChatSwitchRequest {\n\tQt::Key action = Qt::Key_Tab; // Key_Tab, Key_Backtab or Key_Escape.\n\tbool started = false;\n};\n[[nodiscard]] rpl::producer<ChatSwitchRequest> ChatSwitchRequests();\n\n[[nodiscard]] const QStringList &Errors();\n\n// Media shortcuts are not enabled by default, because other\n// applications also use them. They are enabled only when\n// the in-app player is active and disabled back after.\nvoid ToggleMediaShortcuts(bool toggled);\n\n// Support shortcuts are not enabled by default, because they\n// have some conflicts with default input shortcuts, like Ctrl+Delete.\nvoid ToggleSupportShortcuts(bool toggled);\n\nvoid Pause();\nvoid Unpause();\n\n[[nodiscard]] auto KeysDefaults()\n-> base::flat_map<QKeySequence, base::flat_set<Command>>;\n[[nodiscard]] auto KeysCurrents()\n-> base::flat_map<QKeySequence, base::flat_set<Command>>;\n\nvoid Change(\n\tQKeySequence was,\n\tQKeySequence now,\n\tCommand command,\n\tstd::optional<Command> restore = {});\nvoid ResetToDefaults();\n\n[[nodiscard]] bool AllowWithoutModifiers(int key);\n\n} // namespace Shortcuts\n"
  },
  {
    "path": "Telegram/SourceFiles/core/ui_integration.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/ui_integration.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"core/local_url_handlers.h\"\n#include \"core/file_utilities.h\"\n#include \"core/application.h\"\n#include \"core/bank_card_click_handler.h\"\n#include \"core/sandbox.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_session.h\"\n#include \"iv/iv_instance.h\"\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"ui/emoji_config.h\"\n#include \"lang/lang_keys.h\"\n#include \"platform/platform_specific.h\"\n#include \"boxes/url_auth_box.h\"\n#include \"core/phone_click_handler.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"main/main_app_config.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"mainwindow.h\"\n#include \"base/unixtime.h\"\n\n#include <QtCore/QDateTime>\n#include <QtCore/QLocale>\n\nnamespace Core {\nnamespace {\n\nconst auto kGoodPrefix = u\"https://\"_q;\nconst auto kBadPrefix = u\"http://\"_q;\n\n[[nodiscard]] QUrl UrlForAutoLogin(const QString &url) {\n\treturn (url.startsWith(kGoodPrefix, Qt::CaseInsensitive)\n\t\t|| url.startsWith(kBadPrefix, Qt::CaseInsensitive))\n\t\t? QUrl(url)\n\t\t: QUrl();\n}\n\n[[nodiscard]] QString DomainForAutoLogin(const QUrl &url) {\n\treturn url.isValid() ? url.host().toLower() : QString();\n}\n\n[[nodiscard]] QString UrlWithAutoLoginToken(\n\t\tconst QString &url,\n\t\tQUrl parsed,\n\t\tconst QString &domain,\n\t\tQVariant context) {\n\tconst auto my = context.value<ClickHandlerContext>();\n\tconst auto window = my.sessionWindow.get();\n\tconst auto &active = window\n\t\t? window->session().account()\n\t\t: Core::App().activeAccount();\n\tconst auto token = active.mtp().configValues().autologinToken;\n\tconst auto domains = active.appConfig().get<std::vector<QString>>(\n\t\t\"autologin_domains\",\n\t\t{});\n\tif (token.isEmpty()\n\t\t|| domain.isEmpty()\n\t\t|| !ranges::contains(domains, domain)) {\n\t\treturn url;\n\t}\n\tconst auto added = \"autologin_token=\" + token;\n\tparsed.setQuery(parsed.hasQuery()\n\t\t? (parsed.query() + '&' + added)\n\t\t: added);\n\tif (url.startsWith(kBadPrefix, Qt::CaseInsensitive)) {\n\t\tparsed.setScheme(\"https\");\n\t}\n\treturn QString::fromUtf8(parsed.toEncoded());\n}\n\n[[nodiscard]] bool BotAutoLogin(\n\t\tconst QString &url,\n\t\tconst QString &domain,\n\t\tQVariant context) {\n\tauto &account = Core::App().activeAccount();\n\tconst auto &config = account.appConfig();\n\tconst auto my = context.value<ClickHandlerContext>();\n\tconst auto window = my.sessionWindow.get();\n\tconst auto show = window\n\t\t? window->uiShow()\n\t\t: my.show;\n\tconst auto domains = config.get<std::vector<QString>>(\n\t\t\"url_auth_domains\",\n\t\t{});\n\tif (!account.sessionExists()\n\t\t|| domain.isEmpty()\n\t\t|| !show\n\t\t|| !ranges::contains(domains, domain)) {\n\t\treturn false;\n\t}\n\tconst auto good = url.startsWith(kBadPrefix, Qt::CaseInsensitive)\n\t\t? (kGoodPrefix + url.mid(kBadPrefix.size()))\n\t\t: url;\n\tUrlAuthBox::ActivateUrl(show, &account.session(), good, context);\n\treturn true;\n}\n\n[[nodiscard]] QString OpenGLCheckFilePath() {\n\treturn cWorkingDir() + \"tdata/opengl_crash_check\";\n}\n\n[[nodiscard]] QString ANGLEBackendFilePath() {\n\treturn cWorkingDir() + \"tdata/angle_backend\";\n}\n\n[[nodiscard]] Ui::Text::FormattedDateResult FormatDateRelative(TimeId date) {\n\tconst auto now = base::unixtime::now();\n\tconst auto delta = int64(date) - int64(now);\n\tconst auto absDelta = std::abs(delta);\n\tconst auto future = (delta > 0);\n\tauto text = QString();\n\tauto nextUpdate = int32(0);\n\n\tif (absDelta < 1) {\n\t\ttext = tr::lng_date_relative_now(tr::now);\n\t\tnextUpdate = now + 1;\n\t} else if (absDelta < 60) {\n\t\tconst auto count = int(absDelta);\n\t\ttext = (future\n\t\t\t? tr::lng_date_relative_in_seconds\n\t\t\t: tr::lng_date_relative_seconds_ago)(tr::now, lt_count, count);\n\t\tnextUpdate = now + 1;\n\t} else if (absDelta < 3600) {\n\t\tconst auto count = int(absDelta / 60);\n\t\ttext = (future\n\t\t\t? tr::lng_date_relative_in_minutes\n\t\t\t: tr::lng_date_relative_minutes_ago)(tr::now, lt_count, count);\n\t\tnextUpdate = future\n\t\t\t? ((count > 1)\n\t\t\t\t? (date - (count - 1) * 60)\n\t\t\t\t: (date - 59))\n\t\t\t: (date + (count + 1) * 60);\n\t} else if (absDelta < 86400) {\n\t\tconst auto count = int(absDelta / 3600);\n\t\ttext = (future\n\t\t\t? tr::lng_date_relative_in_hours\n\t\t\t: tr::lng_date_relative_hours_ago)(tr::now, lt_count, count);\n\t\tnextUpdate = future\n\t\t\t? ((count > 1)\n\t\t\t\t? (date - (count - 1) * 3600)\n\t\t\t\t: (date - 3599))\n\t\t\t: (date + (count + 1) * 3600);\n\t} else if (absDelta < 30 * 86400) {\n\t\tconst auto count = int(absDelta / 86400);\n\t\ttext = (future\n\t\t\t? tr::lng_date_relative_in_days\n\t\t\t: tr::lng_date_relative_days_ago)(tr::now, lt_count, count);\n\t\tnextUpdate = future\n\t\t\t? ((count > 1)\n\t\t\t\t? (date - (count - 1) * 86400)\n\t\t\t\t: (date - 86399))\n\t\t\t: (date + (count + 1) * 86400);\n\t} else if (absDelta < 365 * 86400) {\n\t\tconst auto count = int(absDelta / (30 * 86400));\n\t\ttext = (future\n\t\t\t? tr::lng_date_relative_in_months\n\t\t\t: tr::lng_date_relative_months_ago)(tr::now, lt_count, count);\n\t\tnextUpdate = future\n\t\t\t? ((count > 1)\n\t\t\t\t? (date - (count - 1) * 30 * 86400)\n\t\t\t\t: (date - 30 * 86400 + 1))\n\t\t\t: (date + (count + 1) * 30 * 86400);\n\t} else {\n\t\tconst auto count = int(absDelta / (365 * 86400));\n\t\ttext = (future\n\t\t\t? tr::lng_date_relative_in_years\n\t\t\t: tr::lng_date_relative_years_ago)(tr::now, lt_count, count);\n\t\tnextUpdate = future\n\t\t\t? ((count > 1)\n\t\t\t\t? (date - (count - 1) * 365 * 86400)\n\t\t\t\t: (date - 365 * 86400 + 1))\n\t\t\t: (date + (count + 1) * 365 * 86400);\n\t}\n\treturn { text, nextUpdate };\n}\n\n[[nodiscard]] Ui::Text::FormattedDateResult FormatDateWithFlags(\n\t\tTimeId date,\n\t\tFormattedDateFlags flags) {\n\tif (flags & FormattedDateFlag::Relative) {\n\t\treturn FormatDateRelative(date);\n\t}\n\tconst auto dateTime = QDateTime::fromSecsSinceEpoch(date);\n\tconst auto locale = QLocale();\n\tauto parts = QStringList();\n\tconst auto hasDayOfWeek = (flags & FormattedDateFlag::DayOfWeek);\n\tconst auto hasShortDate = (flags & FormattedDateFlag::ShortDate);\n\tconst auto hasLongDate = (flags & FormattedDateFlag::LongDate);\n\tconst auto hasShortTime = (flags & FormattedDateFlag::ShortTime);\n\tconst auto hasLongTime = (flags & FormattedDateFlag::LongTime);\n\tif (hasDayOfWeek) {\n\t\tparts.push_back(hasLongDate\n\t\t\t? langDayOfWeekFull(dateTime.date())\n\t\t\t: langDayOfWeek(dateTime.date()));\n\t}\n\tif (hasLongDate) {\n\t\tparts.push_back(langDayOfMonthFull(dateTime.date()));\n\t} else if (hasShortDate) {\n\t\tparts.push_back(langDayOfMonth(dateTime.date()));\n\t}\n\tif (hasLongTime) {\n\t\tparts.push_back(locale.toString(\n\t\t\tdateTime.time(),\n\t\t\tQLocale::LongFormat));\n\t} else if (hasShortTime) {\n\t\tparts.push_back(locale.toString(\n\t\t\tdateTime.time(),\n\t\t\tQLocale::ShortFormat));\n\t}\n\tauto text = parts.join(u\" \"_q);\n\tif (text.isEmpty()) {\n\t\ttext = locale.toString(dateTime, QLocale::ShortFormat);\n\t}\n\treturn { text, 0 };\n}\n\n} // namespace\n\nUi::Text::MarkedContext TextContext(TextContextArgs &&args) {\n\tusing Context = Ui::Text::MarkedContext;\n\tusing Factory = Ui::Text::CustomEmojiFactory;\n\n\tconst auto session = args.session;\n\tauto simple = [session](QStringView data, const Context &context) {\n\t\treturn session->data().customEmojiManager().create(\n\t\t\tdata,\n\t\t\tcontext.repaint);\n\t};\n\tauto factory = !args.customEmojiLoopLimit\n\t\t? Factory(simple)\n\t\t: (args.customEmojiLoopLimit > 0)\n\t\t? Factory([simple, loop = args.customEmojiLoopLimit](\n\t\t\t\tQStringView data,\n\t\t\t\tconst Context &context) {\n\t\t\treturn std::make_unique<Ui::Text::LimitedLoopsEmoji>(\n\t\t\t\tsimple(data, context),\n\t\t\t\tloop);\n\t\t})\n\t\t: Factory([simple](\n\t\t\t\tQStringView data,\n\t\t\t\tconst Context &context) {\n\t\t\treturn std::make_unique<Ui::Text::FirstFrameEmoji>(\n\t\t\t\tsimple(data, context));\n\t\t});\n\targs.details.session = session;\n\treturn {\n\t\t.repaint = std::move(args.repaint),\n\t\t.customEmojiFactory = std::move(factory),\n\t\t.formattedDateFactory = FormatDateWithFlags,\n\t\t.other = std::move(args.details),\n\t};\n}\n\nvoid UiIntegration::postponeCall(FnMut<void()> &&callable) {\n\tSandbox::Instance().postponeCall(std::move(callable));\n}\n\nvoid UiIntegration::registerLeaveSubscription(not_null<QWidget*> widget) {\n\tCore::App().registerLeaveSubscription(widget);\n}\n\nvoid UiIntegration::unregisterLeaveSubscription(not_null<QWidget*> widget) {\n\tCore::App().unregisterLeaveSubscription(widget);\n}\n\nQString UiIntegration::emojiCacheFolder() {\n\treturn cWorkingDir() + \"tdata/emoji\";\n}\n\nQString UiIntegration::openglCheckFilePath() {\n\treturn OpenGLCheckFilePath();\n}\n\nQString UiIntegration::angleBackendFilePath() {\n\treturn ANGLEBackendFilePath();\n}\n\nvoid UiIntegration::textActionsUpdated() {\n\tif (const auto window = Core::App().activeWindow()) {\n\t\twindow->widget()->updateGlobalMenu();\n\t}\n}\n\nvoid UiIntegration::activationFromTopPanel() {\n\tPlatform::IgnoreApplicationActivationRightNow();\n}\n\nvoid UiIntegration::touchCounterIncrement() {\n\t++_touchCounter;\n}\n\nint UiIntegration::touchCounterNow() {\n\treturn _touchCounter;\n}\n\nbool UiIntegration::screenIsLocked() {\n\treturn Core::App().screenIsLocked();\n}\n\nstd::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(\n\t\tconst EntityLinkData &data,\n\t\tconst Ui::Text::MarkedContext &context) {\n\tconst auto my = std::any_cast<Core::TextContextDetails>(&context.other);\n\tswitch (data.type) {\n\tcase EntityType::Url:\n\t\treturn (!data.data.isEmpty()\n\t\t\t&& UrlClickHandler::IsSuspicious(data.data))\n\t\t\t? std::make_shared<HiddenUrlClickHandler>(data.data)\n\t\t\t: Integration::createLinkHandler(data, context);\n\n\tcase EntityType::CustomUrl:\n\t\treturn !data.data.isEmpty()\n\t\t\t? std::make_shared<HiddenUrlClickHandler>(data.data)\n\t\t\t: Integration::createLinkHandler(data, context);\n\n\tcase EntityType::BotCommand:\n\t\treturn std::make_shared<BotCommandClickHandler>(data.data);\n\n\tcase EntityType::Hashtag:\n\t\tusing HashtagMentionType = TextContextDetails::HashtagMentionType;\n\t\tif (my && my->type == HashtagMentionType::Twitter) {\n\t\t\treturn std::make_shared<UrlClickHandler>(\n\t\t\t\t(u\"https://twitter.com/hashtag/\"_q\n\t\t\t\t\t+ data.data.mid(1)\n\t\t\t\t\t+ u\"?src=hash\"_q),\n\t\t\t\ttrue);\n\t\t} else if (my && my->type == HashtagMentionType::Instagram) {\n\t\t\treturn std::make_shared<UrlClickHandler>(\n\t\t\t\t(u\"https://instagram.com/explore/tags/\"_q\n\t\t\t\t\t+ data.data.mid(1)\n\t\t\t\t\t+ '/'),\n\t\t\t\ttrue);\n\t\t}\n\t\treturn std::make_shared<HashtagClickHandler>(data.data);\n\n\tcase EntityType::Cashtag:\n\t\treturn std::make_shared<CashtagClickHandler>(data.data);\n\n\tcase EntityType::Mention:\n\t\tusing HashtagMentionType = TextContextDetails::HashtagMentionType;\n\t\tif (my && my->type == HashtagMentionType::Twitter) {\n\t\t\treturn std::make_shared<UrlClickHandler>(\n\t\t\t\tu\"https://twitter.com/\"_q + data.data.mid(1),\n\t\t\t\ttrue);\n\t\t} else if (my && my->type == HashtagMentionType::Instagram) {\n\t\t\treturn std::make_shared<UrlClickHandler>(\n\t\t\t\tu\"https://instagram.com/\"_q + data.data.mid(1) + '/',\n\t\t\t\ttrue);\n\t\t}\n\t\treturn std::make_shared<MentionClickHandler>(data.data);\n\n\tcase EntityType::MentionName: {\n\t\tauto fields = TextUtilities::MentionNameDataToFields(data.data);\n\t\tif (!my || !my->session) {\n\t\t\tLOG((\"Mention name without a session: %1\").arg(data.data));\n\t\t} else if (fields.userId) {\n\t\t\treturn std::make_shared<MentionNameClickHandler>(\n\t\t\t\tmy->session,\n\t\t\t\tdata.text,\n\t\t\t\tfields.userId,\n\t\t\t\tfields.accessHash);\n\t\t} else {\n\t\t\tLOG((\"Bad mention name: %1\").arg(data.data));\n\t\t}\n\t} break;\n\n\tcase EntityType::Code:\n\t\treturn std::make_shared<MonospaceClickHandler>(data.text, data.type);\n\tcase EntityType::Pre:\n\t\treturn std::make_shared<MonospaceClickHandler>(data.text, data.type);\n\tcase EntityType::Phone:\n\t\treturn (my && my->session)\n\t\t\t? std::make_shared<PhoneClickHandler>(my->session, data.text)\n\t\t\t: nullptr;\n\tcase EntityType::BankCard:\n\t\treturn (my && my->session)\n\t\t\t? std::make_shared<BankCardClickHandler>(my->session, data.text)\n\t\t\t: nullptr;\n\tcase EntityType::FormattedDate: {\n\t\tconst auto [date, flags] = DeserializeFormattedDateData(data.data);\n\t\tif (date) {\n\t\t\treturn std::make_shared<FormattedDateClickHandler>(date, flags);\n\t\t}\n\t} break;\n\t}\n\treturn Integration::createLinkHandler(data, context);\n}\n\nbool UiIntegration::handleUrlClick(\n\t\tconst QString &url,\n\t\tconst QVariant &context) {\n\tconst auto local = Core::TryConvertUrlToLocal(url);\n\tif (Core::InternalPassportOrOAuthLink(local)) {\n\t\treturn true;\n\t}\n\n\tif (UrlClickHandler::IsEmail(url)) {\n\t\tFile::OpenEmailLink(url);\n\t\treturn true;\n\t} else if (local.startsWith(u\"tg://\"_q, Qt::CaseInsensitive)) {\n\t\tCore::App().openLocalUrl(local, context);\n\t\treturn true;\n\t} else if (local.startsWith(u\"tonsite://\"_q, Qt::CaseInsensitive)) {\n\t\tCore::App().iv().showTonSite(local, context);\n\t\treturn true;\n\t} else if (local.startsWith(u\"internal:\"_q, Qt::CaseInsensitive)) {\n\t\tCore::App().openInternalUrl(local, context);\n\t\treturn true;\n\t} else if (Iv::PreferForUri(url)\n\t\t&& !context.value<ClickHandlerContext>().ignoreIv) {\n\t\tconst auto my = context.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tCore::App().iv().openWithIvPreferred(controller, url, context);\n\t\t\treturn true;\n\t\t}\n\t}\n\n\tauto parsed = UrlForAutoLogin(url);\n\tconst auto domain = DomainForAutoLogin(parsed);\n\tconst auto skip = context.value<ClickHandlerContext>().skipBotAutoLogin;\n\tif (skip || !BotAutoLogin(url, domain, context)) {\n\t\tFile::OpenUrl(\n\t\t\tUrlWithAutoLoginToken(url, std::move(parsed), domain, context));\n\t}\n\treturn true;\n}\n\nbool UiIntegration::copyPreOnClick(const QVariant &context) {\n\tconst auto my = context.value<ClickHandlerContext>();\n\tif (const auto window = my.sessionWindow.get()) {\n\t\twindow->showToast(tr::lng_code_copied(tr::now));\n\t} else if (my.show) {\n\t\tmy.show->showToast(tr::lng_code_copied(tr::now));\n\t}\n\treturn true;\n}\n\nrpl::producer<> UiIntegration::forcePopupMenuHideRequests() {\n\treturn Core::App().passcodeLockChanges() | rpl::to_empty;\n}\n\nconst Ui::Emoji::One *UiIntegration::defaultEmojiVariant(\n\t\tconst Ui::Emoji::One *emoji) {\n\tif (!emoji) {\n\t\treturn emoji;\n\t}\n\tconst auto result = Core::App().settings().lookupEmojiVariant(emoji);\n\tCore::App().settings().incrementRecentEmoji({ result });\n\treturn result;\n}\n\nQString UiIntegration::phraseContextCopyText() {\n\treturn tr::lng_context_copy_text(tr::now);\n}\n\nQString UiIntegration::phraseContextCopyEmail() {\n\treturn tr::lng_context_copy_email(tr::now);\n}\n\nQString UiIntegration::phraseContextCopyLink() {\n\treturn tr::lng_context_copy_link(tr::now);\n}\n\nQString UiIntegration::phraseContextCopySelected() {\n\treturn tr::lng_context_copy_selected(tr::now);\n}\n\nQString UiIntegration::phraseFormattingTitle() {\n\treturn tr::lng_menu_formatting(tr::now);\n}\n\nQString UiIntegration::phraseFormattingLinkCreate() {\n\treturn tr::lng_menu_formatting_link_create(tr::now);\n}\n\nQString UiIntegration::phraseFormattingLinkEdit() {\n\treturn tr::lng_menu_formatting_link_edit(tr::now);\n}\n\nQString UiIntegration::phraseFormattingClear() {\n\treturn tr::lng_menu_formatting_clear(tr::now);\n}\n\nQString UiIntegration::phraseFormattingBold() {\n\treturn tr::lng_menu_formatting_bold(tr::now);\n}\n\nQString UiIntegration::phraseFormattingItalic() {\n\treturn tr::lng_menu_formatting_italic(tr::now);\n}\n\nQString UiIntegration::phraseFormattingUnderline() {\n\treturn tr::lng_menu_formatting_underline(tr::now);\n}\n\nQString UiIntegration::phraseFormattingStrikeOut() {\n\treturn tr::lng_menu_formatting_strike_out(tr::now);\n}\n\nQString UiIntegration::phraseFormattingBlockquote() {\n\treturn tr::lng_menu_formatting_blockquote(tr::now);\n}\n\nQString UiIntegration::phraseFormattingMonospace() {\n\treturn tr::lng_menu_formatting_monospace(tr::now);\n}\n\nQString UiIntegration::phraseFormattingSpoiler() {\n\treturn tr::lng_menu_formatting_spoiler(tr::now);\n}\n\nQString UiIntegration::phraseFormattingDate() {\n\treturn tr::lng_menu_formatting_date(tr::now);\n}\n\nQString UiIntegration::phraseButtonOk() {\n\treturn tr::lng_box_ok(tr::now);\n}\n\nQString UiIntegration::phraseButtonClose() {\n\treturn tr::lng_close(tr::now);\n}\n\nQString UiIntegration::phraseButtonCancel() {\n\treturn tr::lng_cancel(tr::now);\n}\n\nQString UiIntegration::phrasePanelCloseWarning() {\n\treturn tr::lng_bot_close_warning_title(tr::now);\n}\n\nQString UiIntegration::phrasePanelCloseUnsaved() {\n\treturn tr::lng_bot_close_warning(tr::now);\n}\n\nQString UiIntegration::phrasePanelCloseAnyway() {\n\treturn tr::lng_bot_close_warning_sure(tr::now);\n}\n\nQString UiIntegration::phraseBotSharePhone() {\n\treturn tr::lng_bot_share_phone(tr::now);\n}\n\nQString UiIntegration::phraseBotSharePhoneTitle() {\n\treturn tr::lng_settings_phone_label(tr::now);\n}\n\nQString UiIntegration::phraseBotSharePhoneConfirm() {\n\treturn tr::lng_bot_share_phone_confirm(tr::now);\n}\n\nQString UiIntegration::phraseBotAllowWrite() {\n\treturn tr::lng_bot_allow_write(tr::now);\n}\n\nQString UiIntegration::phraseBotAllowWriteTitle() {\n\treturn tr::lng_bot_allow_write_title(tr::now);\n}\n\nQString UiIntegration::phraseBotAllowWriteConfirm() {\n\treturn tr::lng_bot_allow_write_confirm(tr::now);\n}\n\nQString UiIntegration::phraseQuoteHeaderCopy() {\n\treturn tr::lng_code_block_header_copy(tr::now);\n}\n\nQString UiIntegration::phraseMinimize() {\n\treturn tr::lng_minimize_window(tr::now);\n}\n\nQString UiIntegration::phraseMaximize() {\n\treturn tr::lng_maximize_window(tr::now);\n}\n\nQString UiIntegration::phraseRestore() {\n\treturn tr::lng_restore_window(tr::now);\n}\n\nbool OpenGLLastCheckFailed() {\n\treturn QFile::exists(OpenGLCheckFilePath());\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/ui_integration.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/integration.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace HistoryView {\nclass ElementDelegate;\n} // namespace HistoryView\n\nnamespace Core {\n\nstruct TextContextDetails {\n\tenum class HashtagMentionType : uchar {\n\t\tTelegram,\n\t\tTwitter,\n\t\tInstagram,\n\t};\n\n\tMain::Session *session = nullptr;\n\tHashtagMentionType type = HashtagMentionType::Telegram;\n};\n\nstruct TextContextArgs {\n\tnot_null<Main::Session*> session;\n\tTextContextDetails details;\n\tFn<void()> repaint;\n\tint customEmojiLoopLimit = 0;\n};\n[[nodiscard]] Ui::Text::MarkedContext TextContext(TextContextArgs &&args);\n\nclass UiIntegration final : public Ui::Integration {\npublic:\n\tvoid postponeCall(FnMut<void()> &&callable) override;\n\tvoid registerLeaveSubscription(not_null<QWidget*> widget) override;\n\tvoid unregisterLeaveSubscription(not_null<QWidget*> widget) override;\n\n\tQString emojiCacheFolder() override;\n\tQString openglCheckFilePath() override;\n\tQString angleBackendFilePath() override;\n\n\tvoid textActionsUpdated() override;\n\tvoid activationFromTopPanel() override;\n\tvoid touchCounterIncrement() override;\n\tint touchCounterNow() override;\n\n\tbool screenIsLocked() override;\n\n\tstd::shared_ptr<ClickHandler> createLinkHandler(\n\t\tconst EntityLinkData &data,\n\t\tconst Ui::Text::MarkedContext &context) override;\n\tbool handleUrlClick(\n\t\tconst QString &url,\n\t\tconst QVariant &context) override;\n\tbool copyPreOnClick(const QVariant &context) override;\n\trpl::producer<> forcePopupMenuHideRequests() override;\n\tconst Ui::Emoji::One *defaultEmojiVariant(\n\t\tconst Ui::Emoji::One *emoji) override;\n\n\tQString phraseContextCopyText() override;\n\tQString phraseContextCopyEmail() override;\n\tQString phraseContextCopyLink() override;\n\tQString phraseContextCopySelected() override;\n\tQString phraseFormattingTitle() override;\n\tQString phraseFormattingLinkCreate() override;\n\tQString phraseFormattingLinkEdit() override;\n\tQString phraseFormattingClear() override;\n\tQString phraseFormattingBold() override;\n\tQString phraseFormattingItalic() override;\n\tQString phraseFormattingUnderline() override;\n\tQString phraseFormattingStrikeOut() override;\n\tQString phraseFormattingBlockquote() override;\n\tQString phraseFormattingMonospace() override;\n\tQString phraseFormattingSpoiler() override;\n\tQString phraseFormattingDate() override;\n\tQString phraseButtonOk() override;\n\tQString phraseButtonClose() override;\n\tQString phraseButtonCancel() override;\n\tQString phrasePanelCloseWarning() override;\n\tQString phrasePanelCloseUnsaved() override;\n\tQString phrasePanelCloseAnyway() override;\n\tQString phraseBotSharePhone() override;\n\tQString phraseBotSharePhoneTitle() override;\n\tQString phraseBotSharePhoneConfirm() override;\n\tQString phraseBotAllowWrite() override;\n\tQString phraseBotAllowWriteTitle() override;\n\tQString phraseBotAllowWriteConfirm() override;\n\tQString phraseQuoteHeaderCopy() override;\n\tQString phraseMinimize() override;\n\tQString phraseMaximize() override;\n\tQString phraseRestore() override;\n\nprivate:\n\tint _touchCounter = 0;\n\n};\n\n[[nodiscard]] bool OpenGLLastCheckFailed();\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/update_checker.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/update_checker.h\"\n\n#include \"platform/platform_specific.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/platform/base_platform_file_utilities.h\"\n#include \"base/timer.h\"\n#include \"base/bytes.h\"\n#include \"base/unixtime.h\"\n#include \"storage/localstorage.h\"\n#include \"core/application.h\"\n#include \"core/changelogs.h\"\n#include \"core/click_handler_types.h\"\n#include \"mainwindow.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"main/main_domain.h\"\n#include \"info/info_memento.h\"\n#include \"info/info_controller.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"settings/sections/settings_advanced.h\"\n#include \"settings/settings_intro.h\"\n#include \"ui/layers/box_content.h\"\n\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonObject>\n\n#include <ksandbox.h>\n\nextern \"C\" {\n#include <openssl/rsa.h>\n#include <openssl/pem.h>\n#include <openssl/bio.h>\n#include <openssl/err.h>\n} // extern \"C\"\n\n#ifndef TDESKTOP_DISABLE_AUTOUPDATE\n#if defined Q_OS_WIN && !defined TDESKTOP_USE_PACKAGED // use Lzma SDK for win\n#include <LzmaLib.h>\n#else // Q_OS_WIN && !TDESKTOP_USE_PACKAGED\n#include <lzma.h>\n#endif // else of Q_OS_WIN && !TDESKTOP_USE_PACKAGED\n#endif // !TDESKTOP_DISABLE_AUTOUPDATE\n\n#ifndef Q_OS_WIN\n#include <unistd.h>\n#endif // !Q_OS_WIN\n\nnamespace Core {\nnamespace {\n\nconstexpr auto kUpdaterTimeout = 10 * crl::time(1000);\nconstexpr auto kMaxResponseSize = 1024 * 1024;\n\n#ifdef TDESKTOP_DISABLE_AUTOUPDATE\nbool UpdaterIsDisabled = true;\n#else // TDESKTOP_DISABLE_AUTOUPDATE\nbool UpdaterIsDisabled = false;\n#endif // TDESKTOP_DISABLE_AUTOUPDATE\n\nstd::weak_ptr<Updater> UpdaterInstance;\n\nusing Progress = UpdateChecker::Progress;\nusing State = UpdateChecker::State;\n\n#ifdef Q_OS_WIN\nusing VersionInt = DWORD;\nusing VersionChar = WCHAR;\n#else // Q_OS_WIN\nusing VersionInt = int;\nusing VersionChar = wchar_t;\n#endif // Q_OS_WIN\n\nusing Loader = MTP::AbstractDedicatedLoader;\n\nstruct BIODeleter {\n\tvoid operator()(BIO *value) {\n\t\tBIO_free(value);\n\t}\n};\n\ninline auto MakeBIO(const void *buf, int len) {\n\treturn std::unique_ptr<BIO, BIODeleter>{\n\t\tBIO_new_mem_buf(buf, len),\n\t};\n}\n\nclass Checker : public base::has_weak_ptr {\npublic:\n\tChecker(bool testing);\n\n\tvirtual void start() = 0;\n\n\trpl::producer<std::shared_ptr<Loader>> ready() const;\n\trpl::producer<> failed() const;\n\n\trpl::lifetime &lifetime();\n\n\tvirtual ~Checker() = default;\n\nprotected:\n\tbool testing() const;\n\tvoid done(std::shared_ptr<Loader> result);\n\tvoid fail();\n\nprivate:\n\tbool _testing = false;\n\trpl::event_stream<std::shared_ptr<Loader>> _ready;\n\trpl::event_stream<> _failed;\n\n\trpl::lifetime _lifetime;\n\n};\n\nstruct Implementation {\n\tstd::unique_ptr<Checker> checker;\n\tstd::shared_ptr<Loader> loader;\n\tbool failed = false;\n\n};\n\nclass HttpChecker : public Checker {\npublic:\n\tHttpChecker(bool testing);\n\n\tvoid start() override;\n\n\t~HttpChecker();\n\nprivate:\n\tvoid gotResponse();\n\tvoid gotFailure(QNetworkReply::NetworkError e);\n\tvoid clearSentRequest();\n\tbool handleResponse(const QByteArray &response);\n\tstd::optional<QString> parseOldResponse(\n\t\tconst QByteArray &response) const;\n\tstd::optional<QString> parseResponse(const QByteArray &response) const;\n\tQString validateLatestUrl(\n\t\tuint64 availableVersion,\n\t\tbool isAvailableAlpha,\n\t\tQString url) const;\n\n\tstd::unique_ptr<QNetworkAccessManager> _manager;\n\tQNetworkReply *_reply = nullptr;\n\n};\n\nclass HttpLoaderActor;\n\nclass HttpLoader : public Loader {\npublic:\n\tHttpLoader(const QString &url);\n\n\t~HttpLoader();\n\nprivate:\n\tvoid startLoading() override;\n\n\tfriend class HttpLoaderActor;\n\n\tQString _url;\n\tstd::unique_ptr<QThread> _thread;\n\tHttpLoaderActor *_actor = nullptr;\n\n};\n\nclass HttpLoaderActor : public QObject {\npublic:\n\tHttpLoaderActor(\n\t\tnot_null<HttpLoader*> parent,\n\t\tnot_null<QThread*> thread,\n\t\tconst QString &url);\n\nprivate:\n\tvoid start();\n\tvoid sendRequest();\n\n\tvoid gotMetaData();\n\tvoid partFinished(qint64 got, qint64 total);\n\tvoid partFailed(QNetworkReply::NetworkError e);\n\n\tnot_null<HttpLoader*> _parent;\n\tQString _url;\n\tQNetworkAccessManager _manager;\n\tstd::unique_ptr<QNetworkReply> _reply;\n\n};\n\nclass MtpChecker : public Checker {\npublic:\n\tMtpChecker(base::weak_ptr<Main::Session> session, bool testing);\n\n\tvoid start() override;\n\nprivate:\n\tusing FileLocation = MTP::DedicatedLoader::Location;\n\n\tusing Checker::fail;\n\tFn<void(const MTP::Error &error)> failHandler();\n\n\tvoid gotMessage(const MTPmessages_Messages &result);\n\tstd::optional<FileLocation> parseMessage(\n\t\tconst MTPmessages_Messages &result) const;\n\tstd::optional<FileLocation> parseText(const QByteArray &text) const;\n\tFileLocation validateLatestLocation(\n\t\tuint64 availableVersion,\n\t\tconst FileLocation &location) const;\n\n\tMTP::WeakInstance _mtp;\n\n};\n\nstd::shared_ptr<Updater> GetUpdaterInstance() {\n\tif (const auto result = UpdaterInstance.lock()) {\n\t\treturn result;\n\t}\n\tconst auto result = std::make_shared<Updater>();\n\tUpdaterInstance = result;\n\treturn result;\n}\n\nQString UpdatesFolder() {\n\treturn cWorkingDir() + u\"tupdates\"_q;\n}\n\nvoid ClearAll() {\n\tbase::Platform::DeleteDirectory(UpdatesFolder());\n}\n\nQString FindUpdateFile() {\n\tQDir updates(UpdatesFolder());\n\tif (!updates.exists()) {\n\t\treturn QString();\n\t}\n\tconst auto list = updates.entryInfoList(QDir::Files);\n\tfor (const auto &info : list) {\n\t\tstatic const auto RegExp = QRegularExpression(\n\t\t\t\"^(\"\n\t\t\t\"tupdate|\"\n\t\t\t\"tx64upd|\"\n\t\t\t\"tarm64upd|\"\n\t\t\t\"tmacupd|\"\n\t\t\t\"tarmacupd|\"\n\t\t\t\"tlinuxupd|\"\n\t\t\t\")\\\\d+(_[a-z\\\\d]+)?$\",\n\t\t\tQRegularExpression::CaseInsensitiveOption\n\t\t);\n\t\tif (RegExp.match(info.fileName()).hasMatch()) {\n\t\t\treturn info.absoluteFilePath();\n\t\t}\n\t}\n\treturn QString();\n}\n\nQString ExtractFilename(const QString &url) {\n\tconst auto expression = QRegularExpression(u\"/([^/\\\\?]+)(\\\\?|$)\"_q);\n\tif (const auto match = expression.match(url); match.hasMatch()) {\n\t\treturn match.captured(1).replace(\n\t\t\tQRegularExpression(u\"[^a-zA-Z0-9_\\\\-]\"_q),\n\t\t\tQString());\n\t}\n\treturn QString();\n}\n\nbool UnpackUpdate(const QString &filepath) {\n#ifndef TDESKTOP_DISABLE_AUTOUPDATE\n\tQFile input(filepath);\n\tif (!input.open(QIODevice::ReadOnly)) {\n\t\tLOG((\"Update Error: cant read updates file!\"));\n\t\treturn false;\n\t}\n\n#if defined Q_OS_WIN && !defined TDESKTOP_USE_PACKAGED // use Lzma SDK for win\n\tconst int32 hSigLen = 128, hShaLen = 20, hPropsLen = LZMA_PROPS_SIZE, hOriginalSizeLen = sizeof(int32), hSize = hSigLen + hShaLen + hPropsLen + hOriginalSizeLen; // header\n#else // Q_OS_WIN && !TDESKTOP_USE_PACKAGED\n\tconst int32 hSigLen = 128, hShaLen = 20, hPropsLen = 0, hOriginalSizeLen = sizeof(int32), hSize = hSigLen + hShaLen + hOriginalSizeLen; // header\n#endif // Q_OS_WIN && !TDESKTOP_USE_PACKAGED\n\n\tQByteArray compressed = input.readAll();\n\tint32 compressedLen = compressed.size() - hSize;\n\tif (compressedLen <= 0) {\n\t\tLOG((\"Update Error: bad compressed size: %1\").arg(compressed.size()));\n\t\treturn false;\n\t}\n\tinput.close();\n\n\tQString tempDirPath = cWorkingDir() + u\"tupdates/temp\"_q, readyFilePath = cWorkingDir() + u\"tupdates/temp/ready\"_q;\n\tbase::Platform::DeleteDirectory(tempDirPath);\n\n\tQDir tempDir(tempDirPath);\n\tif (tempDir.exists() || QFile(readyFilePath).exists()) {\n\t\tLOG((\"Update Error: cant clear tupdates/temp dir!\"));\n\t\treturn false;\n\t}\n\n\tuchar sha1Buffer[20];\n\tbool goodSha1 = !memcmp(compressed.constData() + hSigLen, hashSha1(compressed.constData() + hSigLen + hShaLen, compressedLen + hPropsLen + hOriginalSizeLen, sha1Buffer), hShaLen);\n\tif (!goodSha1) {\n\t\tLOG((\"Update Error: bad SHA1 hash of update file!\"));\n\t\treturn false;\n\t}\n\n\t/*\n\tRSA *pbKey = [] {\n\t\tconst auto bio = MakeBIO(\n\t\t\tconst_cast<char*>(\n\t\t\t\tAppBetaVersion\n\t\t\t\t\t? UpdatesPublicBetaKey\n\t\t\t\t\t: UpdatesPublicKey),\n\t\t\t-1);\n\t\treturn PEM_read_bio_RSAPublicKey(bio.get(), 0, 0, 0);\n\t}();\n\tif (!pbKey) {\n\t\tLOG((\"Update Error: cant read public rsa key!\"));\n\t\treturn false;\n\t}\n\tif (RSA_verify(NID_sha1, (const uchar*)(compressed.constData() + hSigLen), hShaLen, (const uchar*)(compressed.constData()), hSigLen, pbKey) != 1) { // verify signature\n\t\tRSA_free(pbKey);\n\n\t\t// try other public key, if we update from beta to stable or vice versa\n\t\tpbKey = [] {\n\t\t\tconst auto bio = MakeBIO(\n\t\t\t\tconst_cast<char*>(\n\t\t\t\t\tAppBetaVersion\n\t\t\t\t\t\t? UpdatesPublicKey\n\t\t\t\t\t\t: UpdatesPublicBetaKey),\n\t\t\t\t-1);\n\t\t\treturn PEM_read_bio_RSAPublicKey(bio.get(), 0, 0, 0);\n\t\t}();\n\t\tif (!pbKey) {\n\t\t\tLOG((\"Update Error: cant read public rsa key!\"));\n\t\t\treturn false;\n\t\t}\n\t\tif (RSA_verify(NID_sha1, (const uchar*)(compressed.constData() + hSigLen), hShaLen, (const uchar*)(compressed.constData()), hSigLen, pbKey) != 1) { // verify signature\n\t\t\tRSA_free(pbKey);\n\t\t\tLOG((\"Update Error: bad RSA signature of update file!\"));\n\t\t\treturn false;\n\t\t}\n\t}\n\tRSA_free(pbKey);\n\t*/\n\n\tQByteArray uncompressed;\n\n\tint32 uncompressedLen;\n\tmemcpy(&uncompressedLen, compressed.constData() + hSigLen + hShaLen + hPropsLen, hOriginalSizeLen);\n\tuncompressed.resize(uncompressedLen);\n\n\tsize_t resultLen = uncompressed.size();\n#if defined Q_OS_WIN && !defined TDESKTOP_USE_PACKAGED // use Lzma SDK for win\n\tSizeT srcLen = compressedLen;\n\tint uncompressRes = LzmaUncompress((uchar*)uncompressed.data(), &resultLen, (const uchar*)(compressed.constData() + hSize), &srcLen, (const uchar*)(compressed.constData() + hSigLen + hShaLen), LZMA_PROPS_SIZE);\n\tif (uncompressRes != SZ_OK) {\n\t\tLOG((\"Update Error: could not uncompress lzma, code: %1\").arg(uncompressRes));\n\t\treturn false;\n\t}\n#else // Q_OS_WIN && !TDESKTOP_USE_PACKAGED\n\tlzma_stream stream = LZMA_STREAM_INIT;\n\n\tlzma_ret ret = lzma_stream_decoder(&stream, UINT64_MAX, LZMA_CONCATENATED);\n\tif (ret != LZMA_OK) {\n\t\tconst char *msg;\n\t\tswitch (ret) {\n\t\tcase LZMA_MEM_ERROR: msg = \"Memory allocation failed\"; break;\n\t\tcase LZMA_OPTIONS_ERROR: msg = \"Specified preset is not supported\"; break;\n\t\tcase LZMA_UNSUPPORTED_CHECK: msg = \"Specified integrity check is not supported\"; break;\n\t\tdefault: msg = \"Unknown error, possibly a bug\"; break;\n\t\t}\n\t\tLOG((\"Error initializing the decoder: %1 (error code %2)\").arg(msg).arg(ret));\n\t\treturn false;\n\t}\n\n\tstream.avail_in = compressedLen;\n\tstream.next_in = (uint8_t*)(compressed.constData() + hSize);\n\tstream.avail_out = resultLen;\n\tstream.next_out = (uint8_t*)uncompressed.data();\n\n\tlzma_ret res = lzma_code(&stream, LZMA_FINISH);\n\tif (stream.avail_in) {\n\t\tLOG((\"Error in decompression, %1 bytes left in _in of %2 whole.\").arg(stream.avail_in).arg(compressedLen));\n\t\treturn false;\n\t} else if (stream.avail_out) {\n\t\tLOG((\"Error in decompression, %1 bytes free left in _out of %2 whole.\").arg(stream.avail_out).arg(resultLen));\n\t\treturn false;\n\t}\n\tlzma_end(&stream);\n\tif (res != LZMA_OK && res != LZMA_STREAM_END) {\n\t\tconst char *msg;\n\t\tswitch (res) {\n\t\tcase LZMA_MEM_ERROR: msg = \"Memory allocation failed\"; break;\n\t\tcase LZMA_FORMAT_ERROR: msg = \"The input data is not in the .xz format\"; break;\n\t\tcase LZMA_OPTIONS_ERROR: msg = \"Unsupported compression options\"; break;\n\t\tcase LZMA_DATA_ERROR: msg = \"Compressed file is corrupt\"; break;\n\t\tcase LZMA_BUF_ERROR: msg = \"Compressed data is truncated or otherwise corrupt\"; break;\n\t\tdefault: msg = \"Unknown error, possibly a bug\"; break;\n\t\t}\n\t\tLOG((\"Error in decompression: %1 (error code %2)\").arg(msg).arg(res));\n\t\treturn false;\n\t}\n#endif // Q_OS_WIN && !TDESKTOP_USE_PACKAGED\n\n\ttempDir.mkdir(tempDir.absolutePath());\n\n\tquint32 version;\n\t{\n\t\tQDataStream stream(uncompressed);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\n\t\tstream >> version;\n\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\tLOG((\"Update Error: cant read version from downloaded stream, status: %1\").arg(stream.status()));\n\t\t\treturn false;\n\t\t}\n\n\t\tquint64 alphaVersion = 0;\n\t\tif (version == 0x7FFFFFFF) { // alpha version\n\t\t\tstream >> alphaVersion;\n\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\tLOG((\"Update Error: cant read alpha version from downloaded stream, status: %1\").arg(stream.status()));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (!cAlphaVersion() || alphaVersion <= cAlphaVersion()) {\n\t\t\t\tLOG((\"Update Error: downloaded alpha version %1 is not greater, than mine %2\").arg(alphaVersion).arg(cAlphaVersion()));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else if (int32(version) <= AppVersion) {\n\t\t\tLOG((\"Update Error: downloaded version %1 is not greater, than mine %2\").arg(version).arg(AppVersion));\n\t\t\treturn false;\n\t\t}\n\n\t\tquint32 filesCount;\n\t\tstream >> filesCount;\n\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\tLOG((\"Update Error: cant read files count from downloaded stream, status: %1\").arg(stream.status()));\n\t\t\treturn false;\n\t\t}\n\t\tif (!filesCount) {\n\t\t\tLOG((\"Update Error: update is empty!\"));\n\t\t\treturn false;\n\t\t}\n\t\tfor (uint32 i = 0; i < filesCount; ++i) {\n\t\t\tQString relativeName;\n\t\t\tquint32 fileSize;\n\t\t\tQByteArray fileInnerData;\n\t\t\tbool executable = false;\n\n\t\t\tstream >> relativeName >> fileSize >> fileInnerData;\n#ifndef Q_OS_WIN\n\t\t\tstream >> executable;\n#endif // !Q_OS_WIN\n\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\tLOG((\"Update Error: cant read file from downloaded stream, status: %1\").arg(stream.status()));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (fileSize != quint32(fileInnerData.size())) {\n\t\t\t\tLOG((\"Update Error: bad file size %1 not matching data size %2\").arg(fileSize).arg(fileInnerData.size()));\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tQFile f(tempDirPath + '/' + relativeName);\n\t\t\tif (!QDir().mkpath(QFileInfo(f).absolutePath())) {\n\t\t\t\tLOG((\"Update Error: cant mkpath for file '%1'\").arg(tempDirPath + '/' + relativeName));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (!f.open(QIODevice::WriteOnly)) {\n\t\t\t\tLOG((\"Update Error: cant open file '%1' for writing\").arg(tempDirPath + '/' + relativeName));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tauto writtenBytes = f.write(fileInnerData);\n\t\t\tif (writtenBytes != fileSize) {\n\t\t\t\tf.close();\n\t\t\t\tLOG((\"Update Error: cant write file '%1', desiredSize: %2, write result: %3\").arg(tempDirPath + '/' + relativeName).arg(fileSize).arg(writtenBytes));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tf.close();\n\t\t\tif (executable) {\n\t\t\t\tQFileDevice::Permissions p = f.permissions();\n\t\t\t\tp |= QFileDevice::ExeOwner | QFileDevice::ExeUser | QFileDevice::ExeGroup | QFileDevice::ExeOther;\n\t\t\t\tf.setPermissions(p);\n\t\t\t}\n\t\t}\n\n\t\t// create tdata/version file\n\t\ttempDir.mkdir(QDir(tempDirPath + u\"/tdata\"_q).absolutePath());\n\t\tstd::wstring versionString = FormatVersionDisplay(version).toStdWString();\n\n\t\tconst auto versionNum = VersionInt(version);\n\t\tconst auto versionLen = VersionInt(versionString.size() * sizeof(VersionChar));\n\t\tVersionChar versionStr[32];\n\t\tmemcpy(versionStr, versionString.c_str(), versionLen);\n\n\t\tQFile fVersion(tempDirPath + u\"/tdata/version\"_q);\n\t\tif (!fVersion.open(QIODevice::WriteOnly)) {\n\t\t\tLOG((\"Update Error: cant write version file '%1'\").arg(tempDirPath + u\"/version\"_q));\n\t\t\treturn false;\n\t\t}\n\t\tfVersion.write((const char*)&versionNum, sizeof(VersionInt));\n\t\tif (versionNum == 0x7FFFFFFF) { // alpha version\n\t\t\tfVersion.write((const char*)&alphaVersion, sizeof(quint64));\n\t\t} else {\n\t\t\tfVersion.write((const char*)&versionLen, sizeof(VersionInt));\n\t\t\tfVersion.write((const char*)&versionStr[0], versionLen);\n\t\t}\n\t\tfVersion.close();\n\t}\n\n\tQFile readyFile(readyFilePath);\n\tif (readyFile.open(QIODevice::WriteOnly)) {\n\t\tif (readyFile.write(\"1\", 1)) {\n\t\t\treadyFile.close();\n\t\t} else {\n\t\t\tLOG((\"Update Error: cant write ready file '%1'\").arg(readyFilePath));\n\t\t\treturn false;\n\t\t}\n\t} else {\n\t\tLOG((\"Update Error: cant create ready file '%1'\").arg(readyFilePath));\n\t\treturn false;\n\t}\n\tinput.remove();\n\n\treturn true;\n#else // !TDESKTOP_DISABLE_AUTOUPDATE\n\treturn false;\n#endif // TDESKTOP_DISABLE_AUTOUPDATE\n}\n\ntemplate <typename Callback>\nbool ParseCommonMap(\n\t\tconst QByteArray &json,\n\t\tbool testing,\n\t\tCallback &&callback) {\n\tauto error = QJsonParseError{ 0, QJsonParseError::NoError };\n\tconst auto document = QJsonDocument::fromJson(json, &error);\n\tif (error.error != QJsonParseError::NoError) {\n\t\tLOG((\"Update Error: MTP failed to parse JSON, error: %1\"\n\t\t\t).arg(error.errorString()));\n\t\treturn false;\n\t} else if (!document.isObject()) {\n\t\tLOG((\"Update Error: MTP not an object received in JSON.\"));\n\t\treturn false;\n\t}\n\tconst auto platforms = document.object();\n\tconst auto platform = Platform::AutoUpdateKey();\n\tconst auto it = platforms.constFind(platform);\n\tif (it == platforms.constEnd()) {\n\t\tLOG((\"Update Error: MTP platform '%1' not found in response.\"\n\t\t\t).arg(platform));\n\t\treturn false;\n\t} else if (!(*it).isObject()) {\n\t\tLOG((\"Update Error: MTP not an object found for platform '%1'.\"\n\t\t\t).arg(platform));\n\t\treturn false;\n\t}\n\tconst auto types = (*it).toObject();\n\tconst auto list = [&]() -> std::vector<QString> {\n\t\tif (cAlphaVersion()) {\n\t\t\treturn { \"alpha\", \"beta\", \"stable\" };\n\t\t} else if (cInstallBetaVersion()) {\n\t\t\treturn { \"beta\", \"stable\" };\n\t\t}\n\t\treturn { \"stable\" };\n\t}();\n\tauto bestIsAvailableAlpha = false;\n\tauto bestAvailableVersion = 0ULL;\n\tfor (const auto &type : list) {\n\t\tconst auto it = types.constFind(type);\n\t\tif (it == types.constEnd()) {\n\t\t\tcontinue;\n\t\t} else if (!(*it).isObject()) {\n\t\t\tLOG((\"Update Error: Not an object found for '%1:%2'.\"\n\t\t\t\t).arg(platform).arg(type));\n\t\t\treturn false;\n\t\t}\n\t\tconst auto map = (*it).toObject();\n\t\tconst auto key = testing ? \"testing\" : \"released\";\n\t\tconst auto version = map.constFind(key);\n\t\tif (version == map.constEnd()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto isAvailableAlpha = (type == \"alpha\");\n\t\tconst auto availableVersion = [&] {\n\t\t\tif ((*version).isString()) {\n\t\t\t\tconst auto string = (*version).toString();\n\t\t\t\tif (const auto index = string.indexOf(':'); index > 0) {\n\t\t\t\t\treturn base::StringViewMid(string, 0, index).toULongLong();\n\t\t\t\t}\n\t\t\t\treturn string.toULongLong();\n\t\t\t} else if ((*version).isDouble()) {\n\t\t\t\treturn uint64(base::SafeRound((*version).toDouble()));\n\t\t\t}\n\t\t\treturn 0ULL;\n\t\t}();\n\t\tif (!availableVersion) {\n\t\t\tLOG((\"Update Error: Version is not valid for '%1:%2:%3'.\"\n\t\t\t\t).arg(platform).arg(type).arg(key));\n\t\t\treturn false;\n\t\t}\n\t\tconst auto compare = isAvailableAlpha\n\t\t\t? availableVersion\n\t\t\t: availableVersion * 1000;\n\t\tconst auto bestCompare = bestIsAvailableAlpha\n\t\t\t? bestAvailableVersion\n\t\t\t: bestAvailableVersion * 1000;\n\t\tif (compare > bestCompare) {\n\t\t\tbestAvailableVersion = availableVersion;\n\t\t\tbestIsAvailableAlpha = isAvailableAlpha;\n\t\t\tif (!callback(availableVersion, isAvailableAlpha, map)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\tif (!bestAvailableVersion) {\n\t\tLOG((\"Update Error: No valid entry found for platform '%1'.\"\n\t\t\t).arg(platform));\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nChecker::Checker(bool testing) : _testing(testing) {\n}\n\nrpl::producer<std::shared_ptr<Loader>> Checker::ready() const {\n\treturn _ready.events();\n}\n\nrpl::producer<> Checker::failed() const {\n\treturn _failed.events();\n}\n\nbool Checker::testing() const {\n\treturn _testing;\n}\n\nvoid Checker::done(std::shared_ptr<Loader> result) {\n\t_ready.fire(std::move(result));\n}\n\nvoid Checker::fail() {\n\t_failed.fire({});\n}\n\nrpl::lifetime &Checker::lifetime() {\n\treturn _lifetime;\n}\n\n// HttpChecker::HttpChecker(bool testing) : Checker(testing) {\n// }\n\nvoid HttpChecker::start() {\n\tconst auto updaterVersion = Platform::AutoUpdateVersion();\n\tconst auto path = Local::readAutoupdatePrefix()\n\t\t+ qstr(\"/current\")\n\t\t+ (updaterVersion > 1 ? QString::number(updaterVersion) : QString());\n\tauto url = QUrl(path);\n\tDEBUG_LOG((\"Update Info: requesting update state\"));\n\tconst auto request = QNetworkRequest(url);\n\t_manager = std::make_unique<QNetworkAccessManager>();\n\t_reply = _manager->get(request);\n\t_reply->connect(_reply, &QNetworkReply::finished, [=] {\n\t\tgotResponse();\n\t});\n\t_reply->connect(_reply, &QNetworkReply::errorOccurred, [=](auto e) {\n\t\tgotFailure(e);\n\t});\n}\n\nvoid HttpChecker::gotResponse() {\n\tif (!_reply) {\n\t\treturn;\n\t}\n\n\tcSetLastUpdateCheck(base::unixtime::now());\n\tconst auto response = _reply->readAll();\n\tclearSentRequest();\n\n\tif (response.size() >= kMaxResponseSize || !handleResponse(response)) {\n\t\tLOG((\"Update Error: Bad update map size: %1\").arg(response.size()));\n\t\tgotFailure(QNetworkReply::UnknownContentError);\n\t}\n}\n\nbool HttpChecker::handleResponse(const QByteArray &response) {\n\tconst auto handle = [&](const QString &url) {\n\t\tdone(url.isEmpty() ? nullptr : std::make_shared<HttpLoader>(url));\n\t\treturn true;\n\t};\n\tif (const auto url = parseOldResponse(response)) {\n\t\treturn handle(*url);\n\t} else if (const auto url = parseResponse(response)) {\n\t\treturn handle(*url);\n\t}\n\treturn false;\n}\n\nvoid HttpChecker::clearSentRequest() {\n\tconst auto reply = base::take(_reply);\n\tif (!reply) {\n\t\treturn;\n\t}\n\treply->disconnect(reply, &QNetworkReply::finished, nullptr, nullptr);\n\treply->disconnect(reply, &QNetworkReply::errorOccurred, nullptr, nullptr);\n\treply->abort();\n\treply->deleteLater();\n\t_manager = nullptr;\n}\n\nvoid HttpChecker::gotFailure(QNetworkReply::NetworkError e) {\n\tLOG((\"Update Error: \"\n\t\t\"could not get current version %1\").arg(e));\n\tif (const auto reply = base::take(_reply)) {\n\t\treply->deleteLater();\n\t}\n\n\tfail();\n}\n\nstd::optional<QString> HttpChecker::parseOldResponse(\n\t\tconst QByteArray &response) const {\n\tconst auto string = QString::fromLatin1(response);\n\tconst auto old = QRegularExpression(\n\t\tu\"^\\\\s*(\\\\d+)\\\\s*:\\\\s*([\\\\x21-\\\\x7f]+)\\\\s*$\"_q\n\t).match(string);\n\tif (!old.hasMatch()) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto availableVersion = old.captured(1).toULongLong();\n\tconst auto url = old.captured(2);\n\tconst auto isAvailableAlpha = url.startsWith(qstr(\"beta_\"));\n\treturn validateLatestUrl(\n\t\tavailableVersion,\n\t\tisAvailableAlpha,\n\t\tisAvailableAlpha ? url.mid(5) + \"_{signature}\" : url);\n}\n\nstd::optional<QString> HttpChecker::parseResponse(\n\t\tconst QByteArray &response) const {\n\tauto bestAvailableVersion = 0ULL;\n\tauto bestIsAvailableAlpha = false;\n\tauto bestLink = QString();\n\tconst auto accumulate = [&](\n\t\t\tuint64 version,\n\t\t\tbool isAlpha,\n\t\t\tconst QJsonObject &map) {\n\t\tbestAvailableVersion = version;\n\t\tbestIsAvailableAlpha = isAlpha;\n\t\tconst auto link = map.constFind(\"link\");\n\t\tif (link == map.constEnd()) {\n\t\t\tLOG((\"Update Error: Link not found for version %1.\"\n\t\t\t\t).arg(version));\n\t\t\treturn false;\n\t\t} else if (!(*link).isString()) {\n\t\t\tLOG((\"Update Error: Link is not a string for version %1.\"\n\t\t\t\t).arg(version));\n\t\t\treturn false;\n\t\t}\n\t\tbestLink = (*link).toString();\n\t\treturn true;\n\t};\n\tconst auto result = ParseCommonMap(response, testing(), accumulate);\n\tif (!result) {\n\t\treturn std::nullopt;\n\t}\n\treturn validateLatestUrl(\n\t\tbestAvailableVersion,\n\t\tbestIsAvailableAlpha,\n\t\tLocal::readAutoupdatePrefix() + bestLink);\n}\n\nQString HttpChecker::validateLatestUrl(\n\t\tuint64 availableVersion,\n\t\tbool isAvailableAlpha,\n\t\tQString url) const {\n\tconst auto myVersion = isAvailableAlpha\n\t\t? cAlphaVersion()\n\t\t: uint64(AppVersion);\n\tconst auto validVersion = (cAlphaVersion() || !isAvailableAlpha);\n\tif (!validVersion || availableVersion <= myVersion) {\n\t\treturn QString();\n\t}\n\tconst auto versionUrl = url.replace(\n\t\t\"{version}\",\n\t\tQString::number(availableVersion));\n\tconst auto finalUrl = isAvailableAlpha\n\t\t? QString(versionUrl).replace(\n\t\t\t\"{signature}\",\n\t\t\tcountAlphaVersionSignature(availableVersion))\n\t\t: versionUrl;\n\treturn finalUrl;\n}\n\nHttpChecker::~HttpChecker() {\n\tclearSentRequest();\n}\n\nHttpLoader::HttpLoader(const QString &url)\n: Loader(UpdatesFolder() + '/' + ExtractFilename(url), kChunkSize)\n, _url(url) {\n}\n\nvoid HttpLoader::startLoading() {\n\tLOG((\"Update Info: Loading using HTTP from '%1'.\").arg(_url));\n\n\t_thread = std::make_unique<QThread>();\n\t_actor = new HttpLoaderActor(this, _thread.get(), _url);\n\t_thread->start();\n}\n\nHttpLoader::~HttpLoader() {\n\tif (const auto thread = base::take(_thread)) {\n\t\tif (const auto actor = base::take(_actor)) {\n\t\t\tQObject::connect(\n\t\t\t\tthread.get(),\n\t\t\t\t&QThread::finished,\n\t\t\t\tactor,\n\t\t\t\t&QObject::deleteLater);\n\t\t}\n\t\tthread->quit();\n\t\tthread->wait();\n\t}\n}\n\nHttpLoaderActor::HttpLoaderActor(\n\t\tnot_null<HttpLoader*> parent,\n\t\tnot_null<QThread*> thread,\n\t\tconst QString &url)\n: _parent(parent) {\n\t_url = url;\n\tmoveToThread(thread);\n\t_manager.moveToThread(thread);\n\n\tconnect(thread, &QThread::started, this, [=] { start(); });\n}\n\nvoid HttpLoaderActor::start() {\n\tsendRequest();\n}\n\nvoid HttpLoaderActor::sendRequest() {\n\tauto request = QNetworkRequest(_url);\n\tconst auto rangeHeaderValue = \"bytes=\"\n\t\t+ QByteArray::number(_parent->alreadySize())\n\t\t+ \"-\";\n\trequest.setRawHeader(\"Range\", rangeHeaderValue);\n\trequest.setAttribute(\n\t\tQNetworkRequest::HttpPipeliningAllowedAttribute,\n\t\ttrue);\n\t_reply.reset(_manager.get(request));\n\tconnect(\n\t\t_reply.get(),\n\t\t&QNetworkReply::downloadProgress,\n\t\tthis,\n\t\t&HttpLoaderActor::partFinished);\n\tconnect(\n\t\t_reply.get(),\n\t\t&QNetworkReply::errorOccurred,\n\t\tthis,\n\t\t&HttpLoaderActor::partFailed);\n\tconnect(\n\t\t_reply.get(),\n\t\t&QNetworkReply::metaDataChanged,\n\t\tthis,\n\t\t&HttpLoaderActor::gotMetaData);\n}\n\nvoid HttpLoaderActor::gotMetaData() {\n\tconst auto pairs = _reply->rawHeaderPairs();\n\tfor (const auto &pair : pairs) {\n\t\tif (QString::fromUtf8(pair.first).toLower() == \"content-range\") {\n\t\t\tconst auto m = QRegularExpression(u\"/(\\\\d+)([^\\\\d]|$)\"_q).match(QString::fromUtf8(pair.second));\n\t\t\tif (m.hasMatch()) {\n\t\t\t\t_parent->writeChunk({}, m.captured(1).toInt());\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid HttpLoaderActor::partFinished(qint64 got, qint64 total) {\n\tif (!_reply) return;\n\n\tconst auto statusCode = _reply->attribute(\n\t\tQNetworkRequest::HttpStatusCodeAttribute);\n\tif (statusCode.isValid()) {\n\t\tconst auto status = statusCode.toInt();\n\t\tif (status != 200 && status != 206 && status != 416) {\n\t\t\tLOG((\"Update Error: \"\n\t\t\t\t\"Bad HTTP status received in partFinished(): %1\"\n\t\t\t\t).arg(status));\n\t\t\t_parent->threadSafeFailed();\n\t\t\treturn;\n\t\t}\n\t}\n\n\tDEBUG_LOG((\"Update Info: part %1 of %2\").arg(got).arg(total));\n\n\tconst auto data = _reply->readAll();\n\t_parent->writeChunk(bytes::make_span(data), total);\n}\n\nvoid HttpLoaderActor::partFailed(QNetworkReply::NetworkError e) {\n\tif (!_reply) return;\n\n\tconst auto statusCode = _reply->attribute(\n\t\tQNetworkRequest::HttpStatusCodeAttribute);\n\t_reply.release()->deleteLater();\n\tif (statusCode.isValid()) {\n\t\tconst auto status = statusCode.toInt();\n\t\tif (status == 416) { // Requested range not satisfiable\n\t\t\t_parent->writeChunk({}, _parent->alreadySize());\n\t\t\treturn;\n\t\t}\n\t}\n\tLOG((\"Update Error: failed to download part after %1, error %2\"\n\t\t).arg(_parent->alreadySize()\n\t\t).arg(e));\n\t_parent->threadSafeFailed();\n}\n\nMtpChecker::MtpChecker(\n\tbase::weak_ptr<Main::Session> session,\n\tbool testing)\n: Checker(testing)\n, _mtp(session) {\n}\n\nvoid MtpChecker::start() {\n\tif (!_mtp.valid()) {\n\t\tLOG((\"Update Info: MTP is unavailable.\"));\n\t\tcrl::on_main(this, [=] { fail(); });\n\t\treturn;\n\t}\n\tconst auto updaterVersion = 2;//Platform::AutoUpdateVersion();\n\tconst auto feed = \"frkgrmfeed\"\n\t\t+ (updaterVersion > 1 ? QString::number(updaterVersion) : QString());\n\tMTP::ResolveChannel(&_mtp, feed, [=](\n\t\t\tconst MTPInputChannel &channel) {\n\t\t_mtp.send(\n\t\t\tMTPmessages_GetHistory(\n\t\t\t\tMTP_inputPeerChannel(\n\t\t\t\t\tchannel.c_inputChannel().vchannel_id(),\n\t\t\t\t\tchannel.c_inputChannel().vaccess_hash()),\n\t\t\t\tMTP_int(0),  // offset_id\n\t\t\t\tMTP_int(0),  // offset_date\n\t\t\t\tMTP_int(0),  // add_offset\n\t\t\t\tMTP_int(1),  // limit\n\t\t\t\tMTP_int(0),  // max_id\n\t\t\t\tMTP_int(0),  // min_id\n\t\t\t\tMTP_long(0)), // hash\n\t\t\t[=](const MTPmessages_Messages &result) { gotMessage(result); },\n\t\t\tfailHandler());\n\t}, [=] { fail(); });\n}\n\nvoid MtpChecker::gotMessage(const MTPmessages_Messages &result) {\n\tconst auto location = parseMessage(result);\n\tif (!location) {\n\t\tfail();\n\t\treturn;\n\t} else if (location->username.isEmpty()) {\n\t\tdone(nullptr);\n\t\treturn;\n\t}\n\tconst auto ready = [=](std::unique_ptr<MTP::DedicatedLoader> loader) {\n\t\tif (loader) {\n\t\t\tdone(std::move(loader));\n\t\t} else {\n\t\t\tfail();\n\t\t}\n\t};\n\tMTP::StartDedicatedLoader(&_mtp, *location, UpdatesFolder(), ready);\n}\n\nauto MtpChecker::parseMessage(const MTPmessages_Messages &result) const\n-> std::optional<FileLocation> {\n\tconst auto message = MTP::GetMessagesElement(result);\n\tif (!message || message->type() != mtpc_message) {\n\t\tLOG((\"Update Error: MTP feed message not found.\"));\n\t\treturn std::nullopt;\n\t}\n\treturn parseText(message->c_message().vmessage().v);\n}\n\nauto MtpChecker::parseText(const QByteArray &text) const\n-> std::optional<FileLocation> {\n\tauto bestAvailableVersion = 0ULL;\n\tauto bestLocation = FileLocation();\n\tconst auto accumulate = [&](\n\t\t\tuint64 version,\n\t\t\tbool isAlpha,\n\t\t\tconst QJsonObject &map) {\n\t\tif (isAlpha) {\n\t\t\tLOG((\"Update Error: MTP closed alpha found.\"));\n\t\t\treturn false;\n\t\t}\n\t\tbestAvailableVersion = version;\n\t\tconst auto key = testing() ? \"testing\" : \"released\";\n\t\tconst auto entry = map.constFind(key);\n\t\tif (entry == map.constEnd()) {\n\t\t\tLOG((\"Update Error: MTP entry not found for version %1.\"\n\t\t\t\t).arg(version));\n\t\t\treturn false;\n\t\t} else if (!(*entry).isString()) {\n\t\t\tLOG((\"Update Error: MTP entry is not a string for version %1.\"\n\t\t\t\t).arg(version));\n\t\t\treturn false;\n\t\t}\n\t\tconst auto full = (*entry).toString();\n\t\tconst auto start = full.indexOf(':');\n\t\tconst auto post = full.indexOf('#');\n\t\tif (start <= 0 || post < start) {\n\t\t\tLOG((\"Update Error: MTP entry '%1' is bad for version %2.\"\n\t\t\t\t).arg(full\n\t\t\t\t).arg(version));\n\t\t\treturn false;\n\t\t}\n\t\tbestLocation.username = full.mid(start + 1, post - start - 1);\n\t\tbestLocation.postId = base::StringViewMid(full, post + 1).toInt();\n\t\tif (bestLocation.username.isEmpty() || !bestLocation.postId) {\n\t\t\tLOG((\"Update Error: MTP entry '%1' is bad for version %2.\"\n\t\t\t\t).arg(full\n\t\t\t\t).arg(version));\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t};\n\tconst auto result = ParseCommonMap(text, testing(), accumulate);\n\tif (!result) {\n\t\treturn std::nullopt;\n\t}\n\treturn validateLatestLocation(bestAvailableVersion, bestLocation);\n}\n\nauto MtpChecker::validateLatestLocation(\n\t\tuint64 availableVersion,\n\t\tconst FileLocation &location) const -> FileLocation {\n\tconst auto myVersion = uint64(AppVersion);\n\treturn (availableVersion <= myVersion) ? FileLocation() : location;\n}\n\nFn<void(const MTP::Error &error)> MtpChecker::failHandler() {\n\treturn [=](const MTP::Error &error) {\n\t\tLOG((\"Update Error: MTP check failed with '%1'\"\n\t\t\t).arg(QString::number(error.code()) + ':' + error.type()));\n\t\tfail();\n\t};\n}\n\n} // namespace\n\nbool UpdaterDisabled() {\n\treturn UpdaterIsDisabled;\n}\n\nvoid SetUpdaterDisabledAtStartup() {\n\tExpects(UpdaterInstance.lock() == nullptr);\n\n\tUpdaterIsDisabled = true;\n}\n\nclass Updater : public base::has_weak_ptr {\npublic:\n\tUpdater();\n\n\trpl::producer<> checking() const;\n\trpl::producer<> isLatest() const;\n\trpl::producer<Progress> progress() const;\n\trpl::producer<> failed() const;\n\trpl::producer<> ready() const;\n\n\tvoid start(bool forceWait);\n\tvoid stop();\n\tvoid test();\n\n\tState state() const;\n\tint already() const;\n\tint size() const;\n\n\tvoid setMtproto(base::weak_ptr<Main::Session> session);\n\n\t~Updater();\n\nprivate:\n\tenum class Action {\n\t\tWaiting,\n\t\tChecking,\n\t\tLoading,\n\t\tUnpacking,\n\t\tReady,\n\t};\n\tvoid check();\n\tvoid startImplementation(\n\t\tnot_null<Implementation*> which,\n\t\tstd::unique_ptr<Checker> checker);\n\tbool tryLoaders();\n\tvoid handleTimeout();\n\tvoid checkerDone(\n\t\tnot_null<Implementation*> which,\n\t\tstd::shared_ptr<Loader> loader);\n\tvoid checkerFail(not_null<Implementation*> which);\n\n\tvoid finalize(QString filepath);\n\tvoid unpackDone(bool ready);\n\tvoid handleChecking();\n\tvoid handleProgress();\n\tvoid handleLatest();\n\tvoid handleFailed();\n\tvoid handleReady();\n\tvoid scheduleNext();\n\n\tbool _testing = false;\n\tAction _action = Action::Waiting;\n\tbase::Timer _timer;\n\tbase::Timer _retryTimer;\n\trpl::event_stream<> _checking;\n\trpl::event_stream<> _isLatest;\n\trpl::event_stream<Progress> _progress;\n\trpl::event_stream<> _failed;\n\trpl::event_stream<> _ready;\n\tImplementation _httpImplementation;\n\tImplementation _mtpImplementation;\n\tstd::shared_ptr<Loader> _activeLoader;\n\tbool _usingMtprotoLoader = (cAlphaVersion() != 0);\n\tbase::weak_ptr<Main::Session> _session;\n\n\trpl::lifetime _lifetime;\n\n};\n\nUpdater::Updater()\n: _timer([=] { check(); })\n, _retryTimer([=] { handleTimeout(); }) {\n\tchecking() | rpl::on_next([=] {\n\t\thandleChecking();\n\t}, _lifetime);\n\tprogress() | rpl::on_next([=] {\n\t\thandleProgress();\n\t}, _lifetime);\n\tfailed() | rpl::on_next([=] {\n\t\thandleFailed();\n\t}, _lifetime);\n\tready() | rpl::on_next([=] {\n\t\thandleReady();\n\t}, _lifetime);\n\tisLatest() | rpl::on_next([=] {\n\t\thandleLatest();\n\t}, _lifetime);\n}\n\nrpl::producer<> Updater::checking() const {\n\treturn _checking.events();\n}\n\nrpl::producer<> Updater::isLatest() const {\n\treturn _isLatest.events();\n}\n\nauto Updater::progress() const\n-> rpl::producer<Progress> {\n\treturn _progress.events();\n}\n\nrpl::producer<> Updater::failed() const {\n\treturn _failed.events();\n}\n\nrpl::producer<> Updater::ready() const {\n\treturn _ready.events();\n}\n\nvoid Updater::check() {\n\tstart(false);\n}\n\nvoid Updater::handleReady() {\n\tstop();\n\t_action = Action::Ready;\n\tif (!Quitting()) {\n\t\tcSetLastUpdateCheck(base::unixtime::now());\n\t\tLocal::writeSettings();\n\t}\n}\n\nvoid Updater::handleFailed() {\n\tscheduleNext();\n}\n\nvoid Updater::handleLatest() {\n\tif (const auto update = FindUpdateFile(); !update.isEmpty()) {\n\t\tQFile(update).remove();\n\t}\n\tscheduleNext();\n}\n\nvoid Updater::handleChecking() {\n\t_action = Action::Checking;\n\t_retryTimer.callOnce(kUpdaterTimeout);\n}\n\nvoid Updater::handleProgress() {\n\t_retryTimer.callOnce(kUpdaterTimeout);\n}\n\nvoid Updater::scheduleNext() {\n\tstop();\n\tif (!Quitting()) {\n\t\tcSetLastUpdateCheck(base::unixtime::now());\n\t\tLocal::writeSettings();\n\t\tstart(true);\n\t}\n}\n\nauto Updater::state() const -> State {\n\tif (_action == Action::Ready) {\n\t\treturn State::Ready;\n\t} else if (_action == Action::Loading) {\n\t\treturn State::Download;\n\t}\n\treturn State::None;\n}\n\nint Updater::size() const {\n\treturn _activeLoader ? _activeLoader->totalSize() : 0;\n}\n\nint Updater::already() const {\n\treturn _activeLoader ? _activeLoader->alreadySize() : 0;\n}\n\nvoid Updater::stop() {\n\t_httpImplementation = Implementation();\n\t_mtpImplementation = Implementation();\n\t_activeLoader = nullptr;\n\t_action = Action::Waiting;\n}\n\nvoid Updater::start(bool forceWait) {\n\tif (cExeName().isEmpty()) {\n\t\treturn;\n\t}\n\n\t_timer.cancel();\n\tif (!cAutoUpdate() || _action != Action::Waiting) {\n\t\treturn;\n\t}\n\n\t_retryTimer.cancel();\n\tconst auto constDelay = cAlphaVersion() ? 600 : UpdateDelayConstPart;\n\tconst auto randDelay = cAlphaVersion() ? 300 : UpdateDelayRandPart;\n\tconst auto updateInSecs = cLastUpdateCheck()\n\t\t+ constDelay\n\t\t+ int(rand() % randDelay)\n\t\t- base::unixtime::now();\n\tauto sendRequest = (updateInSecs <= 0)\n\t\t|| (updateInSecs > constDelay + randDelay);\n\tif (!sendRequest && !forceWait) {\n\t\tif (!FindUpdateFile().isEmpty()) {\n\t\t\tsendRequest = true;\n\t\t}\n\t}\n\tif (cManyInstance() && !Logs::DebugEnabled()) {\n\t\t// Only main instance is updating.\n\t\treturn;\n\t}\n\n\tif (sendRequest) {\n\t\t// startImplementation(\n\t\t// \t&_httpImplementation,\n\t\t// \tstd::make_unique<HttpChecker>(_testing));\n\t\tstartImplementation(\n\t\t\t&_mtpImplementation,\n\t\t\tstd::make_unique<MtpChecker>(_session, _testing));\n\n\t\t_checking.fire({});\n\t} else {\n\t\t_timer.callOnce((updateInSecs + 5) * crl::time(1000));\n\t}\n}\n\nvoid Updater::startImplementation(\n\t\tnot_null<Implementation*> which,\n\t\tstd::unique_ptr<Checker> checker) {\n\tif (!checker) {\n\t\tclass EmptyChecker : public Checker {\n\t\tpublic:\n\t\t\tEmptyChecker() : Checker(false) {\n\t\t\t}\n\n\t\t\tvoid start() override {\n\t\t\t\tcrl::on_main(this, [=] { fail(); });\n\t\t\t}\n\n\t\t};\n\t\tchecker = std::make_unique<EmptyChecker>();\n\t}\n\n\tchecker->ready(\n\t) | rpl::on_next([=](std::shared_ptr<Loader> &&loader) {\n\t\tcheckerDone(which, std::move(loader));\n\t}, checker->lifetime());\n\tchecker->failed(\n\t) | rpl::on_next([=] {\n\t\tcheckerFail(which);\n\t}, checker->lifetime());\n\n\t*which = Implementation{ std::move(checker) };\n\n\tcrl::on_main(which->checker.get(), [=] {\n\t\twhich->checker->start();\n\t});\n}\n\nvoid Updater::checkerDone(\n\t\tnot_null<Implementation*> which,\n\t\tstd::shared_ptr<Loader> loader) {\n\twhich->checker = nullptr;\n\twhich->loader = std::move(loader);\n\n\ttryLoaders();\n}\n\nvoid Updater::checkerFail(not_null<Implementation*> which) {\n\twhich->checker = nullptr;\n\twhich->failed = true;\n\n\ttryLoaders();\n}\n\nvoid Updater::test() {\n\t_testing = true;\n\tcSetLastUpdateCheck(0);\n\tstart(false);\n}\n\nvoid Updater::setMtproto(base::weak_ptr<Main::Session> session) {\n\t_session = session;\n}\n\nvoid Updater::handleTimeout() {\n\tif (_action == Action::Checking) {\n\t\tconst auto reset = [&](Implementation &which) {\n\t\t\tif (base::take(which.checker)) {\n\t\t\t\twhich.failed = true;\n\t\t\t}\n\t\t};\n\t\treset(_httpImplementation);\n\t\treset(_mtpImplementation);\n\t\tif (!tryLoaders()) {\n\t\t\tcSetLastUpdateCheck(0);\n\t\t\t_timer.callOnce(kUpdaterTimeout);\n\t\t}\n\t} else if (_action == Action::Loading) {\n\t\t_failed.fire({});\n\t}\n}\n\nbool Updater::tryLoaders() {\n\tif (_httpImplementation.checker || _mtpImplementation.checker) {\n\t\t// Some checkers didn't finish yet.\n\t\treturn true;\n\t}\n\t_retryTimer.cancel();\n\n\tconst auto tryOne = [&](Implementation &which) {\n\t\t_activeLoader = std::move(which.loader);\n\t\tif (const auto loader = _activeLoader.get()) {\n\t\t\t_action = Action::Loading;\n\n\t\t\tloader->progress(\n\t\t\t) | rpl::start_to_stream(_progress, loader->lifetime());\n\t\t\tloader->ready(\n\t\t\t) | rpl::on_next([=](QString &&filepath) {\n\t\t\t\tfinalize(std::move(filepath));\n\t\t\t}, loader->lifetime());\n\t\t\tloader->failed(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\t_failed.fire({});\n\t\t\t}, loader->lifetime());\n\n\t\t\t_retryTimer.callOnce(kUpdaterTimeout);\n\t\t\tloader->wipeFolder();\n\t\t\tloader->start();\n\t\t} else {\n\t\t\t_isLatest.fire({});\n\t\t}\n\t};\n\tif (_mtpImplementation.failed && _httpImplementation.failed) {\n\t\t_failed.fire({});\n\t\treturn false;\n\t} else if (!_mtpImplementation.loader) {\n\t\ttryOne(_httpImplementation);\n\t} else if (!_httpImplementation.loader) {\n\t\ttryOne(_mtpImplementation);\n\t} else {\n\t\ttryOne(_usingMtprotoLoader\n\t\t\t? _mtpImplementation\n\t\t\t: _httpImplementation);\n\t\t_usingMtprotoLoader = !_usingMtprotoLoader;\n\t}\n\treturn true;\n}\n\nvoid Updater::finalize(QString filepath) {\n\tif (_action != Action::Loading) {\n\t\treturn;\n\t}\n\t_retryTimer.cancel();\n\t_activeLoader = nullptr;\n\t_action = Action::Unpacking;\n\tcrl::async([=] {\n\t\tconst auto ready = UnpackUpdate(filepath);\n\t\tcrl::on_main([=] {\n\t\t\tGetUpdaterInstance()->unpackDone(ready);\n\t\t});\n\t});\n}\n\nvoid Updater::unpackDone(bool ready) {\n\tif (ready) {\n\t\t_ready.fire({});\n\t} else {\n\t\tClearAll();\n\t\t_failed.fire({});\n\t}\n}\n\nUpdater::~Updater() {\n\tstop();\n}\n\nUpdateChecker::UpdateChecker()\n: _updater(GetUpdaterInstance()) {\n\tif (IsAppLaunched() && Core::App().domain().started()) {\n\t\tif (const auto session = Core::App().activeAccount().maybeSession()) {\n\t\t\t_updater->setMtproto(session);\n\t\t}\n\t}\n}\n\nrpl::producer<> UpdateChecker::checking() const {\n\treturn _updater->checking();\n}\n\nrpl::producer<> UpdateChecker::isLatest() const {\n\treturn _updater->isLatest();\n}\n\nauto UpdateChecker::progress() const\n-> rpl::producer<Progress> {\n\treturn _updater->progress();\n}\n\nrpl::producer<> UpdateChecker::failed() const {\n\treturn _updater->failed();\n}\n\nrpl::producer<> UpdateChecker::ready() const {\n\treturn _updater->ready();\n}\n\nvoid UpdateChecker::start(bool forceWait) {\n\t_updater->start(forceWait);\n}\n\nvoid UpdateChecker::test() {\n\t_updater->test();\n}\n\nvoid UpdateChecker::setMtproto(base::weak_ptr<Main::Session> session) {\n\t_updater->setMtproto(session);\n}\n\nvoid UpdateChecker::stop() {\n\t_updater->stop();\n}\n\nauto UpdateChecker::state() const\n-> State {\n\treturn _updater->state();\n}\n\nint UpdateChecker::already() const {\n\treturn _updater->already();\n}\n\nint UpdateChecker::size() const {\n\treturn _updater->size();\n}\n\n//QString winapiErrorWrap() {\n//\tWCHAR errMsg[2048];\n//\tDWORD errorCode = GetLastError();\n//\tLPTSTR errorText = NULL, errorTextDefault = L\"(Unknown error)\";\n//\tFormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&errorText, 0, 0);\n//\tif (!errorText) {\n//\t\terrorText = errorTextDefault;\n//\t}\n//\tStringCbPrintf(errMsg, sizeof(errMsg), L\"Error code: %d, error message: %s\", errorCode, errorText);\n//\tif (errorText != errorTextDefault) {\n//\t\tLocalFree(errorText);\n//\t}\n//\treturn QString::fromWCharArray(errMsg);\n//}\n\nbool checkReadyUpdate() {\n\tQString readyFilePath = cWorkingDir() + u\"tupdates/temp/ready\"_q, readyPath = cWorkingDir() + u\"tupdates/temp\"_q;\n\tif (!QFile(readyFilePath).exists() || cExeName().isEmpty()) {\n\t\tif (QDir(cWorkingDir() + u\"tupdates/ready\"_q).exists() || QDir(cWorkingDir() + u\"tupdates/temp\"_q).exists()) {\n\t\t\tClearAll();\n\t\t}\n\t\treturn false;\n\t}\n\n\t// check ready version\n\tQString versionPath = readyPath + u\"/tdata/version\"_q;\n\t{\n\t\tQFile fVersion(versionPath);\n\t\tif (!fVersion.open(QIODevice::ReadOnly)) {\n\t\t\tLOG((\"Update Error: cant read version file '%1'\").arg(versionPath));\n\t\t\tClearAll();\n\t\t\treturn false;\n\t\t}\n\t\tauto versionNum = VersionInt();\n\t\tif (fVersion.read((char*)&versionNum, sizeof(VersionInt)) != sizeof(VersionInt)) {\n\t\t\tLOG((\"Update Error: cant read version from file '%1'\").arg(versionPath));\n\t\t\tClearAll();\n\t\t\treturn false;\n\t\t}\n\t\tif (versionNum == 0x7FFFFFFF) { // alpha version\n\t\t\tquint64 alphaVersion = 0;\n\t\t\tif (fVersion.read((char*)&alphaVersion, sizeof(quint64)) != sizeof(quint64)) {\n\t\t\t\tLOG((\"Update Error: cant read alpha version from file '%1'\").arg(versionPath));\n\t\t\t\tClearAll();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (!cAlphaVersion() || alphaVersion <= cAlphaVersion()) {\n\t\t\t\tLOG((\"Update Error: cant install alpha version %1 having alpha version %2\").arg(alphaVersion).arg(cAlphaVersion()));\n\t\t\t\tClearAll();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else if (versionNum <= AppVersion) {\n\t\t\tLOG((\"Update Error: cant install version %1 having version %2\").arg(versionNum).arg(AppVersion));\n\t\t\tClearAll();\n\t\t\treturn false;\n\t\t}\n\t\tfVersion.close();\n\t}\n\n#ifdef Q_OS_WIN\n\tQString curUpdater = (cExeDir() + u\"Updater.exe\"_q);\n\tQFileInfo updater(cWorkingDir() + u\"tupdates/temp/Updater.exe\"_q);\n#elif defined Q_OS_MAC // Q_OS_WIN\n\tQString curUpdater = (cExeDir() + cExeName() + u\"/Contents/Frameworks/Updater\"_q);\n\tQFileInfo updater(cWorkingDir() + u\"tupdates/temp/Telegram.app/Contents/Frameworks/Updater\"_q);\n#else // Q_OS_MAC\n\tQString curUpdater = (cExeDir() + u\"Updater\"_q);\n\tQFileInfo updater(cWorkingDir() + u\"tupdates/temp/Updater\"_q);\n#endif // else for Q_OS_WIN || Q_OS_MAC\n\tif (!updater.exists()) {\n\t\tQFileInfo current(curUpdater);\n\t\tif (!current.exists()) {\n\t\t\tClearAll();\n\t\t\treturn false;\n\t\t}\n\t\tif (!QFile(current.absoluteFilePath()).copy(updater.absoluteFilePath())) {\n\t\t\tClearAll();\n\t\t\treturn false;\n\t\t}\n\t}\n#ifdef Q_OS_WIN\n\tif (CopyFile(updater.absoluteFilePath().toStdWString().c_str(), curUpdater.toStdWString().c_str(), FALSE) == FALSE) {\n\t\tDWORD errorCode = GetLastError();\n\t\tif (errorCode == ERROR_ACCESS_DENIED) { // we are in write-protected dir, like Program Files\n\t\t\tcSetWriteProtected(true);\n\t\t\treturn true;\n\t\t} else {\n\t\t\tClearAll();\n\t\t\treturn false;\n\t\t}\n\t}\n\tif (DeleteFile(updater.absoluteFilePath().toStdWString().c_str()) == FALSE) {\n\t\tClearAll();\n\t\treturn false;\n\t}\n#elif defined Q_OS_MAC // Q_OS_WIN\n\tQDir().mkpath(QFileInfo(curUpdater).absolutePath());\n\tDEBUG_LOG((\"Update Info: moving %1 to %2...\").arg(updater.absoluteFilePath()).arg(curUpdater));\n\tif (!objc_moveFile(updater.absoluteFilePath(), curUpdater)) {\n\t\tClearAll();\n\t\treturn false;\n\t}\n#else // Q_OS_MAC\n\t// if the files in the directory are owned by user, while the directory is not,\n\t// update will still fail since it's not possible to remove files\n\tif (QFile::exists(curUpdater)\n\t\t&& unlink(QFile::encodeName(curUpdater).constData())) {\n\t\tif (errno == EACCES) {\n\t\t\tDEBUG_LOG((\"Update Info: \"\n\t\t\t\t\"could not unlink current Updater, access denied.\"));\n\t\t\tcSetWriteProtected(true);\n\t\t\treturn true;\n\t\t} else {\n\t\t\tDEBUG_LOG((\"Update Error: could not unlink current Updater.\"));\n\t\t\tClearAll();\n\t\t\treturn false;\n\t\t}\n\t}\n\tif (!linuxMoveFile(QFile::encodeName(updater.absoluteFilePath()).constData(), QFile::encodeName(curUpdater).constData())) {\n\t\tif (errno == EACCES) {\n\t\t\tDEBUG_LOG((\"Update Info: \"\n\t\t\t\t\"could not copy new Updater, access denied.\"));\n\t\t\tcSetWriteProtected(true);\n\t\t\treturn true;\n\t\t} else {\n\t\t\tDEBUG_LOG((\"Update Error: could not copy new Updater.\"));\n\t\t\tClearAll();\n\t\t\treturn false;\n\t\t}\n\t}\n#endif // else for Q_OS_WIN || Q_OS_MAC\n\n#ifdef Q_OS_MAC\n\tbase::Platform::RemoveQuarantine(QFileInfo(curUpdater).absolutePath());\n\tbase::Platform::RemoveQuarantine(updater.absolutePath());\n#endif // Q_OS_MAC\n\n\treturn true;\n}\n\nvoid UpdateApplication() {\n\tif (UpdaterDisabled()) {\n\t\tconst auto url = [&] {\n#ifdef OS_WIN_STORE\n\t\t\treturn \"https://www.microsoft.com/en-us/store/p/telegram-desktop/9nztwsqntd0s\";\n#elif defined OS_MAC_STORE // OS_WIN_STORE\n\t\t\treturn \"https://itunes.apple.com/ae/app/telegram-desktop/id946399090\";\n#else // OS_WIN_STORE || OS_MAC_STORE\n\t\t\tif (KSandbox::isFlatpak()) {\n\t\t\t\treturn \"https://flathub.org/apps/details/io.github.forkgram.tdesktop\";\n\t\t\t} else if (KSandbox::isSnap()) {\n\t\t\t\treturn \"https://snapcraft.io/forkgram\";\n\t\t\t}\n\t\t\treturn \"https://desktop.telegram.org\";\n#endif // OS_WIN_STORE || OS_MAC_STORE\n\t\t}();\n\t\tUrlClickHandler::Open(url);\n\t} else {\n\t\tcSetAutoUpdate(true);\n\t\tconst auto window = Core::IsAppLaunched()\n\t\t\t? Core::App().activePrimaryWindow()\n\t\t\t: nullptr;\n\t\tif (window) {\n\t\t\tif (const auto controller = window->sessionController()) {\n\t\t\t\tcontroller->showSection(\n\t\t\t\t\tstd::make_shared<Info::Memento>(\n\t\t\t\t\t\tInfo::Settings::Tag{ controller->session().user() },\n\t\t\t\t\t\t::Settings::AdvancedId()),\n\t\t\t\t\tWindow::SectionShow());\n\t\t\t} else {\n\t\t\t\twindow->widget()->showSpecialLayer(\n\t\t\t\t\tBox<::Settings::LayerWidget>(window),\n\t\t\t\t\tanim::type::normal);\n\t\t\t}\n\t\t\twindow->widget()->showFromTray();\n\t\t}\n\t\tcSetLastUpdateCheck(0);\n\t\tCore::UpdateChecker().start();\n\t}\n}\n\nQString countAlphaVersionSignature(uint64 version) { // duplicated in packer.cpp\n\tif (cAlphaPrivateKey().isEmpty()) {\n\t\tLOG((\"Error: Trying to count alpha version signature without alpha private key!\"));\n\t\treturn QString();\n\t}\n\n\tQByteArray signedData = (qstr(\"TelegramBeta_\") + QString::number(version, 16).toLower()).toUtf8();\n\n\tstatic const int32 shaSize = 20, keySize = 128;\n\n\tuchar sha1Buffer[shaSize];\n\thashSha1(signedData.constData(), signedData.size(), sha1Buffer); // count sha1\n\n\tuint32 siglen = 0;\n\n\tRSA *prKey = [] {\n\t\tconst auto bio = MakeBIO(\n\t\t\tconst_cast<char*>(cAlphaPrivateKey().constData()),\n\t\t\t-1);\n\t\treturn PEM_read_bio_RSAPrivateKey(bio.get(), 0, 0, 0);\n\t}();\n\tif (!prKey) {\n\t\tLOG((\"Error: Could not read alpha private key!\"));\n\t\treturn QString();\n\t}\n\tif (RSA_size(prKey) != keySize) {\n\t\tLOG((\"Error: Bad alpha private key size: %1\").arg(RSA_size(prKey)));\n\t\tRSA_free(prKey);\n\t\treturn QString();\n\t}\n\tQByteArray signature;\n\tsignature.resize(keySize);\n\tif (RSA_sign(NID_sha1, (const uchar*)(sha1Buffer), shaSize, (uchar*)(signature.data()), &siglen, prKey) != 1) { // count signature\n\t\tLOG((\"Error: Counting alpha version signature failed!\"));\n\t\tRSA_free(prKey);\n\t\treturn QString();\n\t}\n\tRSA_free(prKey);\n\n\tif (siglen != keySize) {\n\t\tLOG((\"Error: Bad alpha version signature length: %1\").arg(siglen));\n\t\treturn QString();\n\t}\n\n\tsignature = signature.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);\n\tsignature = signature.replace('-', '8').replace('_', 'B');\n\treturn QString::fromUtf8(signature.mid(19, 32));\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/update_checker.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/dedicated_file_loader.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Core {\n\nbool UpdaterDisabled();\nvoid SetUpdaterDisabledAtStartup();\n\nclass Updater;\n\nclass UpdateChecker {\npublic:\n\tenum class State {\n\t\tNone,\n\t\tDownload,\n\t\tReady,\n\t};\n\tusing Progress = MTP::AbstractDedicatedLoader::Progress;\n\n\tUpdateChecker();\n\n\trpl::producer<> checking() const;\n\trpl::producer<> isLatest() const;\n\trpl::producer<Progress> progress() const;\n\trpl::producer<> failed() const;\n\trpl::producer<> ready() const;\n\n\tvoid start(bool forceWait = false);\n\tvoid stop();\n\tvoid test();\n\n\tvoid setMtproto(base::weak_ptr<Main::Session> session);\n\n\tState state() const;\n\tint already() const;\n\tint size() const;\n\nprivate:\n\tconst std::shared_ptr<Updater> _updater;\n\n};\n\nbool checkReadyUpdate();\nvoid UpdateApplication();\nQString countAlphaVersionSignature(uint64 version);\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/core/utils.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/utils.h\"\n\n#include \"base/qthelp_url.h\"\n#include \"platform/platform_specific.h\"\n\nextern \"C\" {\n#include <openssl/crypto.h>\n#include <openssl/sha.h>\n#include <openssl/err.h>\n#include <openssl/evp.h>\n#include <openssl/conf.h>\n#include <openssl/ssl.h>\n#include <openssl/rand.h>\n#include <libavcodec/avcodec.h>\n#include <libavformat/avformat.h>\n} // extern \"C\"\n\n#ifdef Q_OS_WIN\n#elif defined Q_OS_MAC\n#include <mach/mach_time.h>\n#else\n#include <time.h>\n#endif\n\nuint64 _SharedMemoryLocation[4] = { 0x00, 0x01, 0x02, 0x03 };\n\n// Base types compile-time check\nstatic_assert(sizeof(char) == 1, \"Basic types size check failed\");\nstatic_assert(sizeof(uchar) == 1, \"Basic types size check failed\");\nstatic_assert(sizeof(int16) == 2, \"Basic types size check failed\");\nstatic_assert(sizeof(uint16) == 2, \"Basic types size check failed\");\nstatic_assert(sizeof(int32) == 4, \"Basic types size check failed\");\nstatic_assert(sizeof(uint32) == 4, \"Basic types size check failed\");\nstatic_assert(sizeof(int64) == 8, \"Basic types size check failed\");\nstatic_assert(sizeof(uint64) == 8, \"Basic types size check failed\");\nstatic_assert(sizeof(float32) == 4, \"Basic types size check failed\");\nstatic_assert(sizeof(float64) == 8, \"Basic types size check failed\");\nstatic_assert(sizeof(mtpPrime) == 4, \"Basic types size check failed\");\nstatic_assert(sizeof(MTPint) == 4, \"Basic types size check failed\");\nstatic_assert(sizeof(MTPlong) == 8, \"Basic types size check failed\");\nstatic_assert(sizeof(MTPint128) == 16, \"Basic types size check failed\");\nstatic_assert(sizeof(MTPint256) == 32, \"Basic types size check failed\");\nstatic_assert(sizeof(MTPdouble) == 8, \"Basic types size check failed\");\n\nstatic_assert(sizeof(int) >= 4, \"Basic types size check failed\");\n\n// Precise timing functions / rand init\n\nnamespace ThirdParty {\n\n\tvoid start() {\n\t\tPlatform::ThirdParty::start();\n\n\t\tif (!RAND_status()) { // should be always inited in all modern OS\n\t\t\tconst auto FeedSeed = [](auto value) {\n\t\t\t\tRAND_seed(&value, sizeof(value));\n\t\t\t};\n#ifdef Q_OS_WIN\n\t\t\tLARGE_INTEGER li;\n\t\t\tQueryPerformanceFrequency(&li);\n\t\t\tFeedSeed(li.QuadPart);\n\t\t\tQueryPerformanceCounter(&li);\n\t\t\tFeedSeed(li.QuadPart);\n#elif defined Q_OS_MAC\n\t\t\tmach_timebase_info_data_t tb = { 0 };\n\t\t\tmach_timebase_info(&tb);\n\t\t\tFeedSeed(tb);\n\t\t\tFeedSeed(mach_absolute_time());\n#else\n\t\t\ttimespec ts = { 0 };\n\t\t\tclock_gettime(CLOCK_MONOTONIC, &ts);\n\t\t\tFeedSeed(ts);\n#endif\n\t\t\tif (!RAND_status()) {\n\t\t\t\tLOG((\"MTP Error: Could not init OpenSSL rand, RAND_status() is 0...\"));\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid finish() {\n#if OPENSSL_VERSION_NUMBER >= 0x30000000L\n\t\tEVP_default_properties_enable_fips(nullptr, 0);\n#else\n\t\tFIPS_mode_set(0);\n#endif\n\t\tCONF_modules_unload(1);\n\t}\n}\n\nint32 *hashSha1(const void *data, uint32 len, void *dest) {\n\treturn (int32*)SHA1((const uchar*)data, (size_t)len, (uchar*)dest);\n}\n\nint32 *hashSha256(const void *data, uint32 len, void *dest) {\n\treturn (int32*)SHA256((const uchar*)data, (size_t)len, (uchar*)dest);\n}\n\n// md5 hash, taken somewhere from the internet\n\nnamespace {\n\n\tinline void _md5_decode(uint32 *output, const uchar *input, uint32 len) {\n\t\tfor (uint32 i = 0, j = 0; j < len; i++, j += 4) {\n\t\t\toutput[i] = ((uint32)input[j]) | (((uint32)input[j + 1]) << 8) | (((uint32)input[j + 2]) << 16) | (((uint32)input[j + 3]) << 24);\n\t\t}\n\t}\n\n\tinline void _md5_encode(uchar *output, const uint32 *input, uint32 len) {\n\t\tfor (uint32 i = 0, j = 0; j < len; i++, j += 4) {\n\t\t\toutput[j + 0] = (input[i]) & 0xFF;\n\t\t\toutput[j + 1] = (input[i] >> 8) & 0xFF;\n\t\t\toutput[j + 2] = (input[i] >> 16) & 0xFF;\n\t\t\toutput[j + 3] = (input[i] >> 24) & 0xFF;\n\t\t}\n\t}\n\n\tinline uint32 _md5_rotate_left(uint32 x, int n) {\n\t\treturn (x << n) | (x >> (32 - n));\n\t}\n\n\tinline uint32 _md5_F(uint32 x, uint32 y, uint32 z) {\n\t\treturn (x & y) | (~x & z);\n\t}\n\n\tinline uint32 _md5_G(uint32 x, uint32 y, uint32 z) {\n\t\treturn (x & z) | (y & ~z);\n\t}\n\n\tinline uint32 _md5_H(uint32 x, uint32 y, uint32 z) {\n\t\treturn x ^ y ^ z;\n\t}\n\n\tinline uint32 _md5_I(uint32 x, uint32 y, uint32 z) {\n\t\treturn y ^ (x | ~z);\n\t}\n\n\tinline void _md5_FF(uint32 &a, uint32 b, uint32 c, uint32 d, uint32 x, uint32 s, uint32 ac) {\n\t\ta = _md5_rotate_left(a + _md5_F(b, c, d) + x + ac, s) + b;\n\t}\n\n\tinline void _md5_GG(uint32 &a, uint32 b, uint32 c, uint32 d, uint32 x, uint32 s, uint32 ac) {\n\t\ta = _md5_rotate_left(a + _md5_G(b, c, d) + x + ac, s) + b;\n\t}\n\n\tinline void _md5_HH(uint32 &a, uint32 b, uint32 c, uint32 d, uint32 x, uint32 s, uint32 ac) {\n\t\ta = _md5_rotate_left(a + _md5_H(b, c, d) + x + ac, s) + b;\n\t}\n\n\tinline void _md5_II(uint32 &a, uint32 b, uint32 c, uint32 d, uint32 x, uint32 s, uint32 ac) {\n\t\ta = _md5_rotate_left(a + _md5_I(b, c, d) + x + ac, s) + b;\n\t}\n\n\tstatic uchar _md5_padding[64] = {\n\t\t0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n\t\t0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n\t\t0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0\n\t};\n}\n\nHashMd5::HashMd5(const void *input, uint32 length) : _finalized(false) {\n\tinit();\n\tif (input && length > 0) feed(input, length);\n}\n\nvoid HashMd5::feed(const void *input, uint32 length) {\n\tuint32 index = _count[0] / 8 % _md5_block_size;\n\n\tconst uchar *buf = (const uchar *)input;\n\n\tif ((_count[0] += (length << 3)) < (length << 3)) {\n\t\t_count[1]++;\n\t}\n\t_count[1] += (length >> 29);\n\n\tuint32 firstpart = 64 - index;\n\n\tuint32 i;\n\n\tif (length >= firstpart) {\n\t\tmemcpy(&_buffer[index], buf, firstpart);\n\t\ttransform(_buffer);\n\n\t\tfor (i = firstpart; i + _md5_block_size <= length; i += _md5_block_size) {\n\t\t\ttransform(&buf[i]);\n\t\t}\n\n\t\tindex = 0;\n\t} else {\n\t\ti = 0;\n\t}\n\n\tmemcpy(&_buffer[index], &buf[i], length - i);\n}\n\nint32 *HashMd5::result() {\n\tif (!_finalized) finalize();\n\treturn (int32*)_digest;\n}\n\nvoid HashMd5::init() {\n\t_count[0] = 0;\n\t_count[1] = 0;\n\n\t_state[0] = 0x67452301;\n\t_state[1] = 0xefcdab89;\n\t_state[2] = 0x98badcfe;\n\t_state[3] = 0x10325476;\n}\n\nvoid HashMd5::finalize() {\n\tif (!_finalized) {\n\t\tuchar bits[8];\n\t\t_md5_encode(bits, _count, 8);\n\n\t\tuint32 index = _count[0] / 8 % 64, paddingLen = (index < 56) ? (56 - index) : (120 - index);\n\t\tfeed(_md5_padding, paddingLen);\n\t\tfeed(bits, 8);\n\n\t\t_md5_encode(_digest, _state, 16);\n\n\t\t_finalized = true;\n\t}\n}\n\nvoid HashMd5::transform(const uchar *block) {\n\tuint32 a = _state[0], b = _state[1], c = _state[2], d = _state[3], x[16];\n\t_md5_decode(x, block, _md5_block_size);\n\n\t_md5_FF(a, b, c, d, x[0] , 7 , 0xd76aa478);\n\t_md5_FF(d, a, b, c, x[1] , 12, 0xe8c7b756);\n\t_md5_FF(c, d, a, b, x[2] , 17, 0x242070db);\n\t_md5_FF(b, c, d, a, x[3] , 22, 0xc1bdceee);\n\t_md5_FF(a, b, c, d, x[4] , 7 , 0xf57c0faf);\n\t_md5_FF(d, a, b, c, x[5] , 12, 0x4787c62a);\n\t_md5_FF(c, d, a, b, x[6] , 17, 0xa8304613);\n\t_md5_FF(b, c, d, a, x[7] , 22, 0xfd469501);\n\t_md5_FF(a, b, c, d, x[8] , 7 , 0x698098d8);\n\t_md5_FF(d, a, b, c, x[9] , 12, 0x8b44f7af);\n\t_md5_FF(c, d, a, b, x[10], 17, 0xffff5bb1);\n\t_md5_FF(b, c, d, a, x[11], 22, 0x895cd7be);\n\t_md5_FF(a, b, c, d, x[12], 7 , 0x6b901122);\n\t_md5_FF(d, a, b, c, x[13], 12, 0xfd987193);\n\t_md5_FF(c, d, a, b, x[14], 17, 0xa679438e);\n\t_md5_FF(b, c, d, a, x[15], 22, 0x49b40821);\n\n\t_md5_GG(a, b, c, d, x[1] , 5 , 0xf61e2562);\n\t_md5_GG(d, a, b, c, x[6] , 9 , 0xc040b340);\n\t_md5_GG(c, d, a, b, x[11], 14, 0x265e5a51);\n\t_md5_GG(b, c, d, a, x[0] , 20, 0xe9b6c7aa);\n\t_md5_GG(a, b, c, d, x[5] , 5 , 0xd62f105d);\n\t_md5_GG(d, a, b, c, x[10], 9 , 0x2441453);\n\t_md5_GG(c, d, a, b, x[15], 14, 0xd8a1e681);\n\t_md5_GG(b, c, d, a, x[4] , 20, 0xe7d3fbc8);\n\t_md5_GG(a, b, c, d, x[9] , 5 , 0x21e1cde6);\n\t_md5_GG(d, a, b, c, x[14], 9 , 0xc33707d6);\n\t_md5_GG(c, d, a, b, x[3] , 14, 0xf4d50d87);\n\t_md5_GG(b, c, d, a, x[8] , 20, 0x455a14ed);\n\t_md5_GG(a, b, c, d, x[13], 5 , 0xa9e3e905);\n\t_md5_GG(d, a, b, c, x[2] , 9 , 0xfcefa3f8);\n\t_md5_GG(c, d, a, b, x[7] , 14, 0x676f02d9);\n\t_md5_GG(b, c, d, a, x[12], 20, 0x8d2a4c8a);\n\n\t_md5_HH(a, b, c, d, x[5] , 4 , 0xfffa3942);\n\t_md5_HH(d, a, b, c, x[8] , 11, 0x8771f681);\n\t_md5_HH(c, d, a, b, x[11], 16, 0x6d9d6122);\n\t_md5_HH(b, c, d, a, x[14], 23, 0xfde5380c);\n\t_md5_HH(a, b, c, d, x[1] , 4 , 0xa4beea44);\n\t_md5_HH(d, a, b, c, x[4] , 11, 0x4bdecfa9);\n\t_md5_HH(c, d, a, b, x[7] , 16, 0xf6bb4b60);\n\t_md5_HH(b, c, d, a, x[10], 23, 0xbebfbc70);\n\t_md5_HH(a, b, c, d, x[13], 4 , 0x289b7ec6);\n\t_md5_HH(d, a, b, c, x[0] , 11, 0xeaa127fa);\n\t_md5_HH(c, d, a, b, x[3] , 16, 0xd4ef3085);\n\t_md5_HH(b, c, d, a, x[6] , 23, 0x4881d05);\n\t_md5_HH(a, b, c, d, x[9] , 4 , 0xd9d4d039);\n\t_md5_HH(d, a, b, c, x[12], 11, 0xe6db99e5);\n\t_md5_HH(c, d, a, b, x[15], 16, 0x1fa27cf8);\n\t_md5_HH(b, c, d, a, x[2] , 23, 0xc4ac5665);\n\n\t_md5_II(a, b, c, d, x[0] , 6 , 0xf4292244);\n\t_md5_II(d, a, b, c, x[7] , 10, 0x432aff97);\n\t_md5_II(c, d, a, b, x[14], 15, 0xab9423a7);\n\t_md5_II(b, c, d, a, x[5] , 21, 0xfc93a039);\n\t_md5_II(a, b, c, d, x[12], 6 , 0x655b59c3);\n\t_md5_II(d, a, b, c, x[3] , 10, 0x8f0ccc92);\n\t_md5_II(c, d, a, b, x[10], 15, 0xffeff47d);\n\t_md5_II(b, c, d, a, x[1] , 21, 0x85845dd1);\n\t_md5_II(a, b, c, d, x[8] , 6 , 0x6fa87e4f);\n\t_md5_II(d, a, b, c, x[15], 10, 0xfe2ce6e0);\n\t_md5_II(c, d, a, b, x[6] , 15, 0xa3014314);\n\t_md5_II(b, c, d, a, x[13], 21, 0x4e0811a1);\n\t_md5_II(a, b, c, d, x[4] , 6 , 0xf7537e82);\n\t_md5_II(d, a, b, c, x[11], 10, 0xbd3af235);\n\t_md5_II(c, d, a, b, x[2] , 15, 0x2ad7d2bb);\n\t_md5_II(b, c, d, a, x[9] , 21, 0xeb86d391);\n\n\t_state[0] += a;\n\t_state[1] += b;\n\t_state[2] += c;\n\t_state[3] += d;\n}\n\nint32 *hashMd5(const void *data, uint32 len, void *dest) {\n\tHashMd5 md5(data, len);\n\tmemcpy(dest, md5.result(), 16);\n\n\treturn (int32*)dest;\n}\n\nchar *hashMd5Hex(const int32 *hashmd5, void *dest) {\n\tchar *md5To = (char*)dest;\n\tconst uchar *res = (const uchar*)hashmd5;\n\n\tfor (int i = 0; i < 16; ++i) {\n\t\tuchar ch(res[i]), high = (ch >> 4) & 0x0F, low = ch & 0x0F;\n\t\tmd5To[i * 2 + 0] = high + ((high > 0x09) ? ('a' - 0x0A) : '0');\n\t\tmd5To[i * 2 + 1] = low + ((low > 0x09) ? ('a' - 0x0A) : '0');\n\t}\n\n\treturn md5To;\n}\n\nnamespace {\n\tQMap<QString, QString> fastRusEng;\n\tQHash<QChar, QString> fastLetterRusEng;\n\tQMap<uint32, QString> fastDoubleLetterRusEng;\n\tQHash<QChar, QChar> fastRusKeyboardSwitch;\n\tQHash<QChar, QChar> fastUkrKeyboardSwitch;\n}\n\nQString translitLetterRusEng(QChar letter, QChar next, int32 &toSkip) {\n\tif (fastDoubleLetterRusEng.isEmpty()) {\n\t\tfastDoubleLetterRusEng.insert((QString::fromUtf8(\"Ы\").at(0).unicode() << 16) | QString::fromUtf8(\"й\").at(0).unicode(), u\"Y\"_q);\n\t\tfastDoubleLetterRusEng.insert((QString::fromUtf8(\"и\").at(0).unicode() << 16) | QString::fromUtf8(\"я\").at(0).unicode(), u\"ia\"_q);\n\t\tfastDoubleLetterRusEng.insert((QString::fromUtf8(\"и\").at(0).unicode() << 16) | QString::fromUtf8(\"й\").at(0).unicode(), u\"y\"_q);\n\t\tfastDoubleLetterRusEng.insert((QString::fromUtf8(\"к\").at(0).unicode() << 16) | QString::fromUtf8(\"с\").at(0).unicode(), u\"x\"_q);\n\t\tfastDoubleLetterRusEng.insert((QString::fromUtf8(\"ы\").at(0).unicode() << 16) | QString::fromUtf8(\"й\").at(0).unicode(), u\"y\"_q);\n\t\tfastDoubleLetterRusEng.insert((QString::fromUtf8(\"ь\").at(0).unicode() << 16) | QString::fromUtf8(\"е\").at(0).unicode(), u\"ye\"_q);\n\t}\n\tQMap<uint32, QString>::const_iterator i = fastDoubleLetterRusEng.constFind((letter.unicode() << 16) | next.unicode());\n\tif (i != fastDoubleLetterRusEng.cend()) {\n\t\ttoSkip = 2;\n\t\treturn i.value();\n\t}\n\n\ttoSkip = 1;\n\tif (fastLetterRusEng.isEmpty()) {\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"А\").at(0), u\"A\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Б\").at(0), u\"B\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"В\").at(0), u\"V\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Г\").at(0), u\"G\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Ґ\").at(0), u\"G\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Д\").at(0), u\"D\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Е\").at(0), u\"E\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Є\").at(0), u\"Ye\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Ё\").at(0), u\"Yo\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Ж\").at(0), u\"Zh\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"З\").at(0), u\"Z\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"И\").at(0), u\"I\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Ї\").at(0), u\"Yi\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"І\").at(0), u\"I\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Й\").at(0), u\"J\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"К\").at(0), u\"K\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Л\").at(0), u\"L\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"М\").at(0), u\"M\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Н\").at(0), u\"N\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"О\").at(0), u\"O\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"П\").at(0), u\"P\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Р\").at(0), u\"R\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"С\").at(0), u\"S\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Т\").at(0), u\"T\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"У\").at(0), u\"U\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Ў\").at(0), u\"W\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Ф\").at(0), u\"F\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Х\").at(0), u\"Kh\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Ц\").at(0), u\"Ts\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Ч\").at(0), u\"Ch\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Ш\").at(0), u\"Sh\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Щ\").at(0), u\"Sch\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Э\").at(0), u\"E\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Ю\").at(0), u\"Yu\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Я\").at(0), u\"Ya\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Ў\").at(0), u\"W\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"а\").at(0), u\"a\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"б\").at(0), u\"b\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"в\").at(0), u\"v\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"г\").at(0), u\"g\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"ґ\").at(0), u\"g\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"д\").at(0), u\"d\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"е\").at(0), u\"e\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"є\").at(0), u\"ye\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"ё\").at(0), u\"yo\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"ж\").at(0), u\"zh\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"з\").at(0), u\"z\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"й\").at(0), u\"y\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"ї\").at(0), u\"yi\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"і\").at(0), u\"i\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"л\").at(0), u\"l\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"м\").at(0), u\"m\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"н\").at(0), u\"n\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"о\").at(0), u\"o\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"п\").at(0), u\"p\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"р\").at(0), u\"r\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"с\").at(0), u\"s\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"т\").at(0), u\"t\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"у\").at(0), u\"u\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"ў\").at(0), u\"w\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"ф\").at(0), u\"f\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"х\").at(0), u\"kh\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"ц\").at(0), u\"ts\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"ч\").at(0), u\"ch\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"ш\").at(0), u\"sh\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"щ\").at(0), u\"sch\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"ъ\").at(0), QString());\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"э\").at(0), u\"e\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"ю\").at(0), u\"yu\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"я\").at(0), u\"ya\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"ў\").at(0), u\"w\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"Ы\").at(0), u\"Y\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"и\").at(0), u\"i\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"к\").at(0), u\"k\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"ы\").at(0), u\"y\"_q);\n\t\tfastLetterRusEng.insert(QString::fromUtf8(\"ь\").at(0), QString());\n\t}\n\tQHash<QChar, QString>::const_iterator j = fastLetterRusEng.constFind(letter);\n\tif (j != fastLetterRusEng.cend()) {\n\t\treturn j.value();\n\t}\n\treturn QString(1, letter);\n}\n\nQString translitRusEng(const QString &rus) {\n\tif (fastRusEng.isEmpty()) {\n\t\tfastRusEng.insert(QString::fromUtf8(\"Александр\"), u\"Alexander\"_q);\n\t\tfastRusEng.insert(QString::fromUtf8(\"александр\"), u\"alexander\"_q);\n\t\tfastRusEng.insert(QString::fromUtf8(\"Филипп\"), u\"Philip\"_q);\n\t\tfastRusEng.insert(QString::fromUtf8(\"филипп\"), u\"philip\"_q);\n\t\tfastRusEng.insert(QString::fromUtf8(\"Пётр\"), u\"Petr\"_q);\n\t\tfastRusEng.insert(QString::fromUtf8(\"пётр\"), u\"petr\"_q);\n\t\tfastRusEng.insert(QString::fromUtf8(\"Гай\"), u\"Gai\"_q);\n\t\tfastRusEng.insert(QString::fromUtf8(\"гай\"), u\"gai\"_q);\n\t\tfastRusEng.insert(QString::fromUtf8(\"Ильин\"), u\"Ilyin\"_q);\n\t\tfastRusEng.insert(QString::fromUtf8(\"ильин\"), u\"ilyin\"_q);\n\t}\n\tQMap<QString, QString>::const_iterator i = fastRusEng.constFind(rus);\n\tif (i != fastRusEng.cend()) {\n\t\treturn i.value();\n\t}\n\n\tQString result;\n\tresult.reserve(rus.size() * 2);\n\n\tint32 toSkip = 0;\n\tfor (QString::const_iterator i = rus.cbegin(), e = rus.cend(); i != e; i += toSkip) {\n\t\tresult += translitLetterRusEng(*i, (i + 1 == e) ? ' ' : *(i + 1), toSkip);\n\t}\n\treturn result;\n}\n\nQString engAlphabet = \"qwertyuiop[]asdfghjkl;'zxcvbnm,.\";\nQString engAlphabetUpper = engAlphabet.toUpper();\n\nvoid initializeKeyboardSwitch() {\n\tif (fastRusKeyboardSwitch.isEmpty()) {\n\t\tQString rusAlphabet = \"йцукенгшщзхъфывапролджэячсмитьбю\";\n\t\tQString rusAlphabetUpper = rusAlphabet.toUpper();\n\t\tfor (int i = 0; i < rusAlphabet.size(); ++i) {\n\t\t\tfastRusKeyboardSwitch.insert(engAlphabetUpper[i], rusAlphabetUpper[i]);\n\t\t\tfastRusKeyboardSwitch.insert(engAlphabet[i], rusAlphabet[i]);\n\t\t\tfastRusKeyboardSwitch.insert(rusAlphabetUpper[i], engAlphabetUpper[i]);\n\t\t\tfastRusKeyboardSwitch.insert(rusAlphabet[i], engAlphabet[i]);\n\t\t}\n\t}\n\tif (fastUkrKeyboardSwitch.isEmpty()) {\n\t\tQString ukrAlphabet = \"йцукенгшщзхїфівапролджєячсмитьбю\";\n\t\tQString ukrAlphabetUpper = ukrAlphabet.toUpper();\n\t\tfor (int i = 0; i < ukrAlphabet.size(); ++i) {\n\t\t\tfastUkrKeyboardSwitch.insert(engAlphabetUpper[i], ukrAlphabetUpper[i]);\n\t\t\tfastUkrKeyboardSwitch.insert(engAlphabet[i], ukrAlphabet[i]);\n\t\t\tfastUkrKeyboardSwitch.insert(ukrAlphabetUpper[i], engAlphabetUpper[i]);\n\t\t\tfastUkrKeyboardSwitch.insert(ukrAlphabet[i], engAlphabet[i]);\n\t\t}\n\t}\n}\n\nQString switchKeyboardLayout(const QString& from, QHash<QChar, QChar>& keyboardSwitch) {\n\tQString result;\n\tresult.reserve(from.size());\n\tfor (QString::const_iterator i = from.cbegin(), e = from.cend(); i != e; ++i) {\n\t\tQHash<QChar, QChar>::const_iterator j = keyboardSwitch.constFind(*i);\n\t\tif (j == keyboardSwitch.cend()) {\n\t\t\tresult += *i;\n\t\t} else {\n\t\t\tresult += j.value();\n\t\t}\n\t}\n\treturn result;\n}\n\nQString rusKeyboardLayoutSwitch(const QString& from) {\n\tinitializeKeyboardSwitch();\n\tQString rus = switchKeyboardLayout(from, fastRusKeyboardSwitch);\n\tQString ukr = switchKeyboardLayout(from, fastUkrKeyboardSwitch);\n\treturn rus == ukr ? rus : rus + ' ' + ukr;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/core/utils.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/basic_types.h\"\n#include \"base/flags.h\"\n#include \"base/algorithm.h\"\n#include \"base/bytes.h\"\n\n#include <crl/crl_time.h>\n#include <QtCore/QReadWriteLock>\n#include <QtCore/QRegularExpression>\n#include <QtCore/QMimeData>\n#include <QtNetwork/QNetworkProxy>\n#include <cmath>\n#include <set>\n#include <filesystem>\n\n#define qsl(s) QStringLiteral(s)\n\nnamespace base {\n\ntemplate <typename Value, typename From, typename Till>\ninline bool in_range(Value &&value, From &&from, Till &&till) {\n\treturn (value >= from) && (value < till);\n}\n\ninline bool CanReadDirectory(const QString &path) {\n#ifndef Q_OS_MAC // directory_iterator since 10.15\n\tstd::error_code error;\n\tstd::filesystem::directory_iterator(path.toStdString(), error);\n\treturn !error;\n#else\n\tUnexpected(\"Not implemented.\");\n#endif\n}\n\n} // namespace base\n\nstatic const int32 ScrollMax = INT_MAX;\n\nextern uint64 _SharedMemoryLocation[];\ntemplate <typename T, unsigned int N>\nT *SharedMemoryLocation() {\n\tstatic_assert(N < 4, \"Only 4 shared memory locations!\");\n\treturn reinterpret_cast<T*>(_SharedMemoryLocation + N);\n}\n\ninline void mylocaltime(struct tm * _Tm, const time_t * _Time) {\n#ifdef Q_OS_WIN\n\tlocaltime_s(_Tm, _Time);\n#else\n\tlocaltime_r(_Time, _Tm);\n#endif\n}\n\nnamespace ThirdParty {\n\nvoid start();\nvoid finish();\n\n} // namespace ThirdParty\n\nconst static uint32 _md5_block_size = 64;\nclass HashMd5 {\npublic:\n\n\tHashMd5(const void *input = 0, uint32 length = 0);\n\tvoid feed(const void *input, uint32 length);\n\tint32 *result();\n\nprivate:\n\n\tvoid init();\n\tvoid finalize();\n\tvoid transform(const uchar *block);\n\n\tbool _finalized;\n\tuchar _buffer[_md5_block_size];\n\tuint32 _count[2];\n\tuint32 _state[4];\n\tuchar _digest[16];\n\n};\n\nint32 *hashSha1(const void *data, uint32 len, void *dest); // dest - ptr to 20 bytes, returns (int32*)dest\ninline std::array<char, 20> hashSha1(const void *data, int size) {\n\tauto result = std::array<char, 20>();\n\thashSha1(data, size, result.data());\n\treturn result;\n}\n\nint32 *hashSha256(const void *data, uint32 len, void *dest); // dest - ptr to 32 bytes, returns (int32*)dest\ninline std::array<char, 32> hashSha256(const void *data, int size) {\n\tauto result = std::array<char, 32>();\n\thashSha256(data, size, result.data());\n\treturn result;\n}\n\nint32 *hashMd5(const void *data, uint32 len, void *dest); // dest = ptr to 16 bytes, returns (int32*)dest\ninline std::array<char, 16> hashMd5(const void *data, int size) {\n\tauto result = std::array<char, 16>();\n\thashMd5(data, size, result.data());\n\treturn result;\n}\n\nchar *hashMd5Hex(const int32 *hashmd5, void *dest); // dest = ptr to 32 bytes, returns (char*)dest\ninline char *hashMd5Hex(const void *data, uint32 len, void *dest) { // dest = ptr to 32 bytes, returns (char*)dest\n\treturn hashMd5Hex(HashMd5(data, len).result(), dest);\n}\ninline std::array<char, 32> hashMd5Hex(const void *data, int size) {\n\tauto result = std::array<char, 32>();\n\thashMd5Hex(data, size, result.data());\n\treturn result;\n}\n\nQString translitRusEng(const QString &rus);\nQString rusKeyboardLayoutSwitch(const QString &from);\n\ninline int rowscount(int fullCount, int countPerRow) {\n\treturn (fullCount + countPerRow - 1) / countPerRow;\n}\ninline int floorclamp(int value, int step, int lowest, int highest) {\n\treturn std::clamp(value / step, lowest, highest);\n}\ninline int floorclamp(float64 value, int step, int lowest, int highest) {\n\treturn std::clamp(\n\t\tstatic_cast<int>(std::floor(value / step)),\n\t\tlowest,\n\t\thighest);\n}\ninline int ceilclamp(int value, int step, int lowest, int highest) {\n\treturn std::clamp((value + step - 1) / step, lowest, highest);\n}\ninline int ceilclamp(float64 value, int32 step, int32 lowest, int32 highest) {\n\treturn std::clamp(\n\t\tstatic_cast<int>(std::ceil(value / step)),\n\t\tlowest,\n\t\thighest);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/core/version.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/const_string.h\"\n\nconstexpr auto AppNameF = \"Forkgram Desktop\"_cs;\n\n#define TDESKTOP_REQUESTED_ALPHA_VERSION (0ULL)\n\n#ifdef TDESKTOP_ALLOW_CLOSED_ALPHA\n#define TDESKTOP_ALPHA_VERSION TDESKTOP_REQUESTED_ALPHA_VERSION\n#else // TDESKTOP_ALLOW_CLOSED_ALPHA\n#define TDESKTOP_ALPHA_VERSION (0ULL)\n#endif // TDESKTOP_ALLOW_CLOSED_ALPHA\n\n// used in Updater.cpp and Setup.iss for Windows\nconstexpr auto AppId = \"{53F49750-6209-4FBF-9CA8-7A333C87D1ED}\"_cs;\nconstexpr auto AppNameOld = \"Telegram Win (Unofficial)\"_cs;\nconstexpr auto AppName = \"Telegram Desktop\"_cs;\nconstexpr auto AppFile = \"Telegram\"_cs;\nconstexpr auto AppVersion = 6007006;\nconstexpr auto AppVersionStr = \"6.7.6\";\nconstexpr auto AppBetaVersion = false;\nconstexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;\n"
  },
  {
    "path": "Telegram/SourceFiles/countries/countries_instance.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"countries/countries_instance.h\"\n\n#include \"base/qt/qt_common_adapters.h\"\n#include \"base/qt/qt_string_view.h\"\n\nnamespace Countries {\nnamespace {\n\nauto SingleInstance = CountriesInstance();\n\nconst std::array<Info, 231> FallbackList = { {\n\t{ \"Andorra\", \"AD\", \"\", { CallingCodeInfo{ \"376\", {}, { \"XX XX XX\" } } }, false },\n\t{ \"United Arab Emirates\", \"AE\", \"\", { CallingCodeInfo{ \"971\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Afghanistan\", \"AF\", \"\", { CallingCodeInfo{ \"93\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Antigua & Barbuda\", \"AG\", \"\", { CallingCodeInfo{ \"1268\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Anguilla\", \"AI\", \"\", { CallingCodeInfo{ \"1264\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Albania\", \"AL\", \"\", { CallingCodeInfo{ \"355\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Armenia\", \"AM\", \"\", { CallingCodeInfo{ \"374\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Angola\", \"AO\", \"\", { CallingCodeInfo{ \"244\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Argentina\", \"AR\", \"\", { CallingCodeInfo{ \"54\", {}, {} } }, false },\n\t{ \"American Samoa\", \"AS\", \"\", { CallingCodeInfo{ \"1684\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Austria\", \"AT\", \"\", { CallingCodeInfo{ \"43\", {}, { \"X XXXXXXXX\" } } }, false },\n\t{ \"Australia\", \"AU\", \"\", { CallingCodeInfo{ \"61\", {}, { \"X XXXX XXXX\" } } }, false },\n\t{ \"Aruba\", \"AW\", \"\", { CallingCodeInfo{ \"297\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Azerbaijan\", \"AZ\", \"\", { CallingCodeInfo{ \"994\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Bosnia & Herzegovina\", \"BA\", \"\", { CallingCodeInfo{ \"387\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Barbados\", \"BB\", \"\", { CallingCodeInfo{ \"1246\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Bangladesh\", \"BD\", \"\", { CallingCodeInfo{ \"880\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Belgium\", \"BE\", \"\", { CallingCodeInfo{ \"32\", {}, { \"XXX XX XX XX\" } } }, false },\n\t{ \"Burkina Faso\", \"BF\", \"\", { CallingCodeInfo{ \"226\", {}, { \"XX XX XX XX\" } } }, false },\n\t{ \"Bulgaria\", \"BG\", \"\", { CallingCodeInfo{ \"359\", {}, {} } }, false },\n\t{ \"Bahrain\", \"BH\", \"\", { CallingCodeInfo{ \"973\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Burundi\", \"BI\", \"\", { CallingCodeInfo{ \"257\", {}, { \"XX XX XXXX\" } } }, false },\n\t{ \"Benin\", \"BJ\", \"\", { CallingCodeInfo{ \"229\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Bermuda\", \"BM\", \"\", { CallingCodeInfo{ \"1441\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Brunei Darussalam\", \"BN\", \"\", { CallingCodeInfo{ \"673\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Bolivia\", \"BO\", \"\", { CallingCodeInfo{ \"591\", {}, { \"X XXX XXXX\" } } }, false },\n\t{ \"Bonaire, Sint Eustatius & Saba\", \"BQ\", \"\", { CallingCodeInfo{ \"599\", {}, {} } }, false },\n\t{ \"Brazil\", \"BR\", \"\", { CallingCodeInfo{ \"55\", {}, { \"XX XXXXX XXXX\" } } }, false },\n\t{ \"Bahamas\", \"BS\", \"\", { CallingCodeInfo{ \"1242\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Bhutan\", \"BT\", \"\", { CallingCodeInfo{ \"975\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Botswana\", \"BW\", \"\", { CallingCodeInfo{ \"267\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Belarus\", \"BY\", \"\", { CallingCodeInfo{ \"375\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Belize\", \"BZ\", \"\", { CallingCodeInfo{ \"501\", {}, {} } }, false },\n\t{ \"Canada\", \"CA\", \"\", { CallingCodeInfo{ \"1\", { \"403\" }, { \"XXX XXX XXXX\" } } }, false },\n\t{ \"Congo (Dem. Rep.)\", \"CD\", \"\", { CallingCodeInfo{ \"243\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Central African Rep.\", \"CF\", \"\", { CallingCodeInfo{ \"236\", {}, { \"XX XX XX XX\" } } }, false },\n\t{ \"Congo (Rep.)\", \"CG\", \"\", { CallingCodeInfo{ \"242\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Switzerland\", \"CH\", \"\", { CallingCodeInfo{ \"41\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Côte d'Ivoire\", \"CI\", \"\", { CallingCodeInfo{ \"225\", {}, { \"XX XX XX XXXX\" } } }, false },\n\t{ \"Cook Islands\", \"CK\", \"\", { CallingCodeInfo{ \"682\", {}, {} } }, false },\n\t{ \"Chile\", \"CL\", \"\", { CallingCodeInfo{ \"56\", {}, { \"X XXXX XXXX\" } } }, false },\n\t{ \"Cameroon\", \"CM\", \"\", { CallingCodeInfo{ \"237\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"China\", \"CN\", \"\", { CallingCodeInfo{ \"86\", {}, { \"XXX XXXX XXXX\" } } }, false },\n\t{ \"Colombia\", \"CO\", \"\", { CallingCodeInfo{ \"57\", {}, { \"XXX XXX XXXX\" } } }, false },\n\t{ \"Costa Rica\", \"CR\", \"\", { CallingCodeInfo{ \"506\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Cuba\", \"CU\", \"\", { CallingCodeInfo{ \"53\", {}, { \"X XXX XXXX\" } } }, false },\n\t{ \"Cape Verde\", \"CV\", \"\", { CallingCodeInfo{ \"238\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Curaçao\", \"CW\", \"\", { CallingCodeInfo{ \"599\", { \"9\" }, {} } }, false },\n\t{ \"Cyprus\", \"CY\", \"\", { CallingCodeInfo{ \"357\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Czech Republic\", \"CZ\", \"\", { CallingCodeInfo{ \"420\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Germany\", \"DE\", \"\", { CallingCodeInfo{ \"49\", {}, { \"XXXX XXXXXXX\" } } }, false },\n\t{ \"Djibouti\", \"DJ\", \"\", { CallingCodeInfo{ \"253\", {}, { \"XX XX XX XX\" } } }, false },\n\t{ \"Denmark\", \"DK\", \"\", { CallingCodeInfo{ \"45\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Dominica\", \"DM\", \"\", { CallingCodeInfo{ \"1767\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Dominican Rep.\", \"DO\", \"\", { CallingCodeInfo{ \"1809\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Algeria\", \"DZ\", \"\", { CallingCodeInfo{ \"213\", {}, { \"XXX XX XX XX\" } } }, false },\n\t{ \"Ecuador\", \"EC\", \"\", { CallingCodeInfo{ \"593\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Estonia\", \"EE\", \"\", { CallingCodeInfo{ \"372\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Egypt\", \"EG\", \"\", { CallingCodeInfo{ \"20\", {}, { \"XX XXXX XXXX\" } } }, false },\n\t{ \"Eritrea\", \"ER\", \"\", { CallingCodeInfo{ \"291\", {}, { \"X XXX XXX\" } } }, false },\n\t{ \"Spain\", \"ES\", \"\", { CallingCodeInfo{ \"34\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Ethiopia\", \"ET\", \"\", { CallingCodeInfo{ \"251\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Finland\", \"FI\", \"\", { CallingCodeInfo{ \"358\", {}, {} } }, false },\n\t{ \"Fiji\", \"FJ\", \"\", { CallingCodeInfo{ \"679\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Falkland Islands\", \"FK\", \"\", { CallingCodeInfo{ \"500\", {}, {} } }, false },\n\t{ \"Micronesia\", \"FM\", \"\", { CallingCodeInfo{ \"691\", {}, {} } }, false },\n\t{ \"Faroe Islands\", \"FO\", \"\", { CallingCodeInfo{ \"298\", {}, { \"XXX XXX\" } } }, false },\n\t{ \"France\", \"FR\", \"\", { CallingCodeInfo{ \"33\", {}, { \"X XX XX XX XX\" } } }, false },\n\t{ \"Gabon\", \"GA\", \"\", { CallingCodeInfo{ \"241\", {}, { \"X XX XX XX\" } } }, false },\n\t{ \"United Kingdom\", \"GB\", \"\", { CallingCodeInfo{ \"44\", {}, { \"XXXX XXXXXX\" } } }, false },\n\t{ \"Grenada\", \"GD\", \"\", { CallingCodeInfo{ \"1473\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Georgia\", \"GE\", \"\", { CallingCodeInfo{ \"995\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"French Guiana\", \"GF\", \"\", { CallingCodeInfo{ \"594\", {}, {} } }, false },\n\t{ \"Ghana\", \"GH\", \"\", { CallingCodeInfo{ \"233\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Gibraltar\", \"GI\", \"\", { CallingCodeInfo{ \"350\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Greenland\", \"GL\", \"\", { CallingCodeInfo{ \"299\", {}, { \"XXX XXX\" } } }, false },\n\t{ \"Gambia\", \"GM\", \"\", { CallingCodeInfo{ \"220\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Guinea\", \"GN\", \"\", { CallingCodeInfo{ \"224\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Guadeloupe\", \"GP\", \"\", { CallingCodeInfo{ \"590\", {}, { \"XXX XX XX XX\" } } }, false },\n\t{ \"Equatorial Guinea\", \"GQ\", \"\", { CallingCodeInfo{ \"240\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Greece\", \"GR\", \"\", { CallingCodeInfo{ \"30\", {}, { \"XXX XXX XXXX\" } } }, false },\n\t{ \"Guatemala\", \"GT\", \"\", { CallingCodeInfo{ \"502\", {}, { \"X XXX XXXX\" } } }, false },\n\t{ \"Guam\", \"GU\", \"\", { CallingCodeInfo{ \"1671\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Guinea-Bissau\", \"GW\", \"\", { CallingCodeInfo{ \"245\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Guyana\", \"GY\", \"\", { CallingCodeInfo{ \"592\", {}, {} } }, false },\n\t{ \"Hong Kong\", \"HK\", \"\", { CallingCodeInfo{ \"852\", {}, { \"X XXX XXXX\" } } }, false },\n\t{ \"Honduras\", \"HN\", \"\", { CallingCodeInfo{ \"504\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Croatia\", \"HR\", \"\", { CallingCodeInfo{ \"385\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Haiti\", \"HT\", \"\", { CallingCodeInfo{ \"509\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Hungary\", \"HU\", \"\", { CallingCodeInfo{ \"36\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Indonesia\", \"ID\", \"\", { CallingCodeInfo{ \"62\", {}, { \"XXX XXXXXX\" } } }, false },\n\t{ \"Ireland\", \"IE\", \"\", { CallingCodeInfo{ \"353\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Israel\", \"IL\", \"\", { CallingCodeInfo{ \"972\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"India\", \"IN\", \"\", { CallingCodeInfo{ \"91\", {}, { \"XXXXX XXXXX\" } } }, false },\n\t{ \"Diego Garcia\", \"IO\", \"\", { CallingCodeInfo{ \"246\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Iraq\", \"IQ\", \"\", { CallingCodeInfo{ \"964\", {}, { \"XXX XXX XXXX\" } } }, false },\n\t{ \"Iran\", \"IR\", \"\", { CallingCodeInfo{ \"98\", {}, { \"XXX XXX XXXX\" } } }, false },\n\t{ \"Iceland\", \"IS\", \"\", { CallingCodeInfo{ \"354\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Italy\", \"IT\", \"\", { CallingCodeInfo{ \"39\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Jamaica\", \"JM\", \"\", { CallingCodeInfo{ \"1876\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Jordan\", \"JO\", \"\", { CallingCodeInfo{ \"962\", {}, { \"X XXXX XXXX\" } } }, false },\n\t{ \"Japan\", \"JP\", \"\", { CallingCodeInfo{ \"81\", {}, { \"XX XXXX XXXX\" } } }, false },\n\t{ \"Kenya\", \"KE\", \"\", { CallingCodeInfo{ \"254\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Kyrgyzstan\", \"KG\", \"\", { CallingCodeInfo{ \"996\", {}, { \"XXX XXXXXX\" } } }, false },\n\t{ \"Cambodia\", \"KH\", \"\", { CallingCodeInfo{ \"855\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Kiribati\", \"KI\", \"\", { CallingCodeInfo{ \"686\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Comoros\", \"KM\", \"\", { CallingCodeInfo{ \"269\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Saint Kitts & Nevis\", \"KN\", \"\", { CallingCodeInfo{ \"1869\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"North Korea\", \"KP\", \"\", { CallingCodeInfo{ \"850\", {}, {} } }, false },\n\t{ \"South Korea\", \"KR\", \"\", { CallingCodeInfo{ \"82\", {}, { \"XX XXXX XXX\" } } }, false },\n\t{ \"Kuwait\", \"KW\", \"\", { CallingCodeInfo{ \"965\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Cayman Islands\", \"KY\", \"\", { CallingCodeInfo{ \"1345\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Kazakhstan\", \"KZ\", \"\", { CallingCodeInfo{ \"7\", { \"6\" }, { \"XXX XXX XX XX\" } } }, false },\n\t{ \"Laos\", \"LA\", \"\", { CallingCodeInfo{ \"856\", {}, { \"XX XX XXX XXX\" } } }, false },\n\t{ \"Lebanon\", \"LB\", \"\", { CallingCodeInfo{ \"961\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Saint Lucia\", \"LC\", \"\", { CallingCodeInfo{ \"1758\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Liechtenstein\", \"LI\", \"\", { CallingCodeInfo{ \"423\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Sri Lanka\", \"LK\", \"\", { CallingCodeInfo{ \"94\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Liberia\", \"LR\", \"\", { CallingCodeInfo{ \"231\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Lesotho\", \"LS\", \"\", { CallingCodeInfo{ \"266\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Lithuania\", \"LT\", \"\", { CallingCodeInfo{ \"370\", {}, { \"XXX XXXXX\" } } }, false },\n\t{ \"Luxembourg\", \"LU\", \"\", { CallingCodeInfo{ \"352\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Latvia\", \"LV\", \"\", { CallingCodeInfo{ \"371\", {}, { \"XXX XXXXX\" } } }, false },\n\t{ \"Libya\", \"LY\", \"\", { CallingCodeInfo{ \"218\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Morocco\", \"MA\", \"\", { CallingCodeInfo{ \"212\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Monaco\", \"MC\", \"\", { CallingCodeInfo{ \"377\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Moldova\", \"MD\", \"\", { CallingCodeInfo{ \"373\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Montenegro\", \"ME\", \"\", { CallingCodeInfo{ \"382\", {}, {} } }, false },\n\t{ \"Madagascar\", \"MG\", \"\", { CallingCodeInfo{ \"261\", {}, { \"XX XX XXX XX\" } } }, false },\n\t{ \"Marshall Islands\", \"MH\", \"\", { CallingCodeInfo{ \"692\", {}, {} } }, false },\n\t{ \"North Macedonia\", \"MK\", \"\", { CallingCodeInfo{ \"389\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Mali\", \"ML\", \"\", { CallingCodeInfo{ \"223\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Myanmar\", \"MM\", \"\", { CallingCodeInfo{ \"95\", {}, {} } }, false },\n\t{ \"Mongolia\", \"MN\", \"\", { CallingCodeInfo{ \"976\", {}, { \"XX XX XXXX\" } } }, false },\n\t{ \"Macau\", \"MO\", \"\", { CallingCodeInfo{ \"853\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Northern Mariana Islands\", \"MP\", \"\", { CallingCodeInfo{ \"1670\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Martinique\", \"MQ\", \"\", { CallingCodeInfo{ \"596\", {}, {} } }, false },\n\t{ \"Mauritania\", \"MR\", \"\", { CallingCodeInfo{ \"222\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Montserrat\", \"MS\", \"\", { CallingCodeInfo{ \"1664\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Malta\", \"MT\", \"\", { CallingCodeInfo{ \"356\", {}, { \"XX XX XX XX\" } } }, false },\n\t{ \"Mauritius\", \"MU\", \"\", { CallingCodeInfo{ \"230\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Maldives\", \"MV\", \"\", { CallingCodeInfo{ \"960\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Malawi\", \"MW\", \"\", { CallingCodeInfo{ \"265\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Mexico\", \"MX\", \"\", { CallingCodeInfo{ \"52\", {}, {} } }, false },\n\t{ \"Malaysia\", \"MY\", \"\", { CallingCodeInfo{ \"60\", {}, { \"XX XXXX XXXX\" } } }, false },\n\t{ \"Mozambique\", \"MZ\", \"\", { CallingCodeInfo{ \"258\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Namibia\", \"NA\", \"\", { CallingCodeInfo{ \"264\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"New Caledonia\", \"NC\", \"\", { CallingCodeInfo{ \"687\", {}, {} } }, false },\n\t{ \"Niger\", \"NE\", \"\", { CallingCodeInfo{ \"227\", {}, { \"XX XX XX XX\" } } }, false },\n\t{ \"Norfolk Island\", \"NF\", \"\", { CallingCodeInfo{ \"672\", {}, {} } }, false },\n\t{ \"Nigeria\", \"NG\", \"\", { CallingCodeInfo{ \"234\", {}, { \"XX XXXX XXXX\" } } }, false },\n\t{ \"Nicaragua\", \"NI\", \"\", { CallingCodeInfo{ \"505\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Netherlands\", \"NL\", \"\", { CallingCodeInfo{ \"31\", {}, { \"X XX XX XX XX\" } } }, false },\n\t{ \"Norway\", \"NO\", \"\", { CallingCodeInfo{ \"47\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Nepal\", \"NP\", \"\", { CallingCodeInfo{ \"977\", {}, { \"XX XXXX XXXX\" } } }, false },\n\t{ \"Nauru\", \"NR\", \"\", { CallingCodeInfo{ \"674\", {}, {} } }, false },\n\t{ \"Niue\", \"NU\", \"\", { CallingCodeInfo{ \"683\", {}, {} } }, false },\n\t{ \"New Zealand\", \"NZ\", \"\", { CallingCodeInfo{ \"64\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Oman\", \"OM\", \"\", { CallingCodeInfo{ \"968\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Panama\", \"PA\", \"\", { CallingCodeInfo{ \"507\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Peru\", \"PE\", \"\", { CallingCodeInfo{ \"51\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"French Polynesia\", \"PF\", \"\", { CallingCodeInfo{ \"689\", {}, {} } }, false },\n\t{ \"Papua New Guinea\", \"PG\", \"\", { CallingCodeInfo{ \"675\", {}, {} } }, false },\n\t{ \"Philippines\", \"PH\", \"\", { CallingCodeInfo{ \"63\", {}, { \"XXX XXX XXXX\" } } }, false },\n\t{ \"Pakistan\", \"PK\", \"\", { CallingCodeInfo{ \"92\", {}, { \"XXX XXX XXXX\" } } }, false },\n\t{ \"Poland\", \"PL\", \"\", { CallingCodeInfo{ \"48\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Saint Pierre & Miquelon\", \"PM\", \"\", { CallingCodeInfo{ \"508\", {}, {} } }, false },\n\t{ \"Puerto Rico\", \"PR\", \"\", { CallingCodeInfo{ \"1787\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Palestine\", \"PS\", \"\", { CallingCodeInfo{ \"970\", {}, { \"XXX XX XXXX\" } } }, false },\n\t{ \"Portugal\", \"PT\", \"\", { CallingCodeInfo{ \"351\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Palau\", \"PW\", \"\", { CallingCodeInfo{ \"680\", {}, {} } }, false },\n\t{ \"Paraguay\", \"PY\", \"\", { CallingCodeInfo{ \"595\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Qatar\", \"QA\", \"\", { CallingCodeInfo{ \"974\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Réunion\", \"RE\", \"\", { CallingCodeInfo{ \"262\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Romania\", \"RO\", \"\", { CallingCodeInfo{ \"40\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Serbia\", \"RS\", \"\", { CallingCodeInfo{ \"381\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Russian Federation\", \"RU\", \"\", { CallingCodeInfo{ \"7\", {}, { \"XXX XXX XXXX\" } } }, false },\n\t{ \"Rwanda\", \"RW\", \"\", { CallingCodeInfo{ \"250\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Saudi Arabia\", \"SA\", \"\", { CallingCodeInfo{ \"966\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Solomon Islands\", \"SB\", \"\", { CallingCodeInfo{ \"677\", {}, {} } }, false },\n\t{ \"Seychelles\", \"SC\", \"\", { CallingCodeInfo{ \"248\", {}, { \"X XX XX XX\" } } }, false },\n\t{ \"Sudan\", \"SD\", \"\", { CallingCodeInfo{ \"249\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Sweden\", \"SE\", \"\", { CallingCodeInfo{ \"46\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Singapore\", \"SG\", \"\", { CallingCodeInfo{ \"65\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Saint Helena\", \"SH\", \"\", { CallingCodeInfo{ \"247\", {}, {} } }, false },\n\t{ \"Slovenia\", \"SI\", \"\", { CallingCodeInfo{ \"386\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Slovakia\", \"SK\", \"\", { CallingCodeInfo{ \"421\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Sierra Leone\", \"SL\", \"\", { CallingCodeInfo{ \"232\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"San Marino\", \"SM\", \"\", { CallingCodeInfo{ \"378\", {}, { \"XXX XXX XXXX\" } } }, false },\n\t{ \"Senegal\", \"SN\", \"\", { CallingCodeInfo{ \"221\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Somalia\", \"SO\", \"\", { CallingCodeInfo{ \"252\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Suriname\", \"SR\", \"\", { CallingCodeInfo{ \"597\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"South Sudan\", \"SS\", \"\", { CallingCodeInfo{ \"211\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"São Tomé & Príncipe\", \"ST\", \"\", { CallingCodeInfo{ \"239\", {}, { \"XX XXXXX\" } } }, false },\n\t{ \"El Salvador\", \"SV\", \"\", { CallingCodeInfo{ \"503\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Sint Maarten\", \"SX\", \"\", { CallingCodeInfo{ \"1721\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Syria\", \"SY\", \"\", { CallingCodeInfo{ \"963\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Eswatini\", \"SZ\", \"\", { CallingCodeInfo{ \"268\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Turks & Caicos Islands\", \"TC\", \"\", { CallingCodeInfo{ \"1649\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Chad\", \"TD\", \"\", { CallingCodeInfo{ \"235\", {}, { \"XX XX XX XX\" } } }, false },\n\t{ \"Togo\", \"TG\", \"\", { CallingCodeInfo{ \"228\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Thailand\", \"TH\", \"\", { CallingCodeInfo{ \"66\", {}, { \"X XXXX XXXX\" } } }, false },\n\t{ \"Tajikistan\", \"TJ\", \"\", { CallingCodeInfo{ \"992\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Tokelau\", \"TK\", \"\", { CallingCodeInfo{ \"690\", {}, {} } }, false },\n\t{ \"Timor-Leste\", \"TL\", \"\", { CallingCodeInfo{ \"670\", {}, {} } }, false },\n\t{ \"Turkmenistan\", \"TM\", \"\", { CallingCodeInfo{ \"993\", {}, { \"XX XXXXXX\" } } }, false },\n\t{ \"Tunisia\", \"TN\", \"\", { CallingCodeInfo{ \"216\", {}, { \"XX XXX XXX\" } } }, false },\n\t{ \"Tonga\", \"TO\", \"\", { CallingCodeInfo{ \"676\", {}, {} } }, false },\n\t{ \"Turkey\", \"TR\", \"\", { CallingCodeInfo{ \"90\", {}, { \"XXX XXX XXXX\" } } }, false },\n\t{ \"Trinidad & Tobago\", \"TT\", \"\", { CallingCodeInfo{ \"1868\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Tuvalu\", \"TV\", \"\", { CallingCodeInfo{ \"688\", {}, {} } }, false },\n\t{ \"Taiwan\", \"TW\", \"\", { CallingCodeInfo{ \"886\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"Tanzania\", \"TZ\", \"\", { CallingCodeInfo{ \"255\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Ukraine\", \"UA\", \"\", { CallingCodeInfo{ \"380\", {}, { \"XX XXX XX XX\" } } }, false },\n\t{ \"Uganda\", \"UG\", \"\", { CallingCodeInfo{ \"256\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"USA\", \"US\", \"United States of America\", { CallingCodeInfo{ \"1\", {}, { \"XXX XXX XXXX\" } } }, false },\n\t{ \"Uruguay\", \"UY\", \"\", { CallingCodeInfo{ \"598\", {}, { \"X XXX XXXX\" } } }, false },\n\t{ \"Uzbekistan\", \"UZ\", \"\", { CallingCodeInfo{ \"998\", {}, { \"XX XXX XX XX\" } } }, false },\n\t{ \"Saint Vincent & the Grenadines\", \"VC\", \"\", { CallingCodeInfo{ \"1784\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Venezuela\", \"VE\", \"\", { CallingCodeInfo{ \"58\", {}, { \"XXX XXX XXXX\" } } }, false },\n\t{ \"British Virgin Islands\", \"VG\", \"\", { CallingCodeInfo{ \"1284\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"US Virgin Islands\", \"VI\", \"\", { CallingCodeInfo{ \"1340\", {}, { \"XXX XXXX\" } } }, false },\n\t{ \"Vietnam\", \"VN\", \"\", { CallingCodeInfo{ \"84\", {}, {} } }, false },\n\t{ \"Vanuatu\", \"VU\", \"\", { CallingCodeInfo{ \"678\", {}, {} } }, false },\n\t{ \"Wallis & Futuna\", \"WF\", \"\", { CallingCodeInfo{ \"681\", {}, {} } }, false },\n\t{ \"Samoa\", \"WS\", \"\", { CallingCodeInfo{ \"685\", {}, {} } }, false },\n\t{ \"Kosovo\", \"XK\", \"\", { CallingCodeInfo{ \"383\", {}, { \"XXXX XXXX\" } } }, false },\n\t{ \"Yemen\", \"YE\", \"\", { CallingCodeInfo{ \"967\", {}, { \"XXX XXX XXX\" } } }, false },\n\t{ \"South Africa\", \"ZA\", \"\", { CallingCodeInfo{ \"27\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Zambia\", \"ZM\", \"\", { CallingCodeInfo{ \"260\", {}, { \"XX XXX XXXX\" } } }, false },\n\t{ \"Zimbabwe\", \"ZW\", \"\", { CallingCodeInfo{ \"263\", {}, { \"XX XXX XXXX\" } } }, false },\n} };\n\n} // namespace\n\nCountriesInstance::CountriesInstance() {\n}\n\nconst std::vector<Info> &CountriesInstance::list() const {\n\tif (_list.empty()) {\n\t\t_list = (FallbackList | ranges::to_vector);\n\t}\n\treturn _list;\n}\n\nvoid CountriesInstance::setList(std::vector<Info> &&infos) {\n\t_list = std::move(infos);\n\t_byCode.clear();\n\t_byISO2.clear();\n\t_updated.fire({});\n}\n\nconst CountriesInstance::Map &CountriesInstance::byCode() const {\n\tif (_byCode.empty()) {\n\t\t_byCode.reserve(list().size());\n\t\tfor (const auto &entry : list()) {\n\t\t\tfor (const auto &code : entry.codes) {\n\t\t\t\t_byCode.insert(code.callingCode, &entry);\n\t\t\t}\n\t\t}\n\t}\n\treturn _byCode;\n}\n\nconst CountriesInstance::Map &CountriesInstance::byISO2() const {\n\tif (_byISO2.empty()) {\n\t\t_byISO2.reserve(list().size());\n\t\tfor (const auto &entry : list()) {\n\t\t\t_byISO2.insert(entry.iso2, &entry);\n\t\t}\n\t}\n\treturn _byISO2;\n}\n\nQString CountriesInstance::validPhoneCode(QString fullCode) const {\n\tconst auto &listByCode = byCode();\n\twhile (fullCode.length()) {\n\t\tconst auto i = listByCode.constFind(fullCode);\n\t\tif (i != listByCode.cend()) {\n\t\t\treturn fullCode;\n\t\t}\n\t\tfullCode.chop(1);\n\t}\n\treturn QString();\n}\n\nQString CountriesInstance::countryNameByISO2(const QString &iso) const {\n\tconst auto &listByISO2 = byISO2();\n\tconst auto i = listByISO2.constFind(iso);\n\treturn (i != listByISO2.cend()) ? (*i)->name : QString();\n}\n\nQString CountriesInstance::countryISO2ByPhone(const QString &phone) const {\n\tconst auto &listByCode = byCode();\n\tconst auto code = validPhoneCode(phone);\n\tconst auto i = listByCode.find(code);\n\treturn (i != listByCode.cend()) ? (*i)->iso2 : QString();\n}\n\nQString CountriesInstance::flagEmojiByISO2(const QString &iso) const {\n\tif (iso.size() != 2\n\t\t|| iso.front() < 'A'\n\t\t|| iso.front() > 'Z'\n\t\t|| iso.back() < 'A'\n\t\t|| iso.back() > 'Z') {\n\t\treturn QString();\n\t} else if (iso == u\"FT\"_q) {\n\t\treturn QString::fromUtf8(\n\t\t\t\"\\xF0\\x9F\\x8F\\xB4\\xE2\\x80\\x8D\\xE2\\x98\\xA0\\xEF\\xB8\\x8F\");\n\t}\n\tauto result = QString(4, QChar(0xD83C));\n\tresult[1] = QChar(iso.front().unicode() - 'A' + 0xDDE6);\n\tresult[3] = QChar(iso.back().unicode() - 'A' + 0xDDE6);\n\treturn result;\n}\n\nFormatResult CountriesInstance::format(FormatArgs args) const {\n\t// Ported from TDLib.\n\tif (args.phone.isEmpty()) {\n\t\treturn FormatResult();\n\t}\n\tconst auto &phoneNumber = args.phone;\n\n\tconst Info *bestCountryPtr = nullptr;\n\tconst CallingCodeInfo *bestCallingCodePtr = nullptr;\n\tauto bestLength = size_t(0);\n\t[[maybe_unused]] auto isPrefix = false;\n\tfor (const auto &country : list()) {\n\t\tfor (auto &callingCode : country.codes) {\n\t\t\tif (phoneNumber.startsWith(callingCode.callingCode)) {\n\t\t\t\tconst auto codeSize = callingCode.callingCode.size();\n\t\t\t\tfor (const auto &prefix : callingCode.prefixes) {\n\t\t\t\t\tif (prefix.startsWith(base::StringViewMid(phoneNumber, codeSize))) {\n\t\t\t\t\t\tisPrefix = true;\n\t\t\t\t\t}\n\t\t\t\t\tif ((codeSize + prefix.size()) > bestLength &&\n\t\t\t\t\t\t\tbase::StringViewMid(phoneNumber, codeSize).startsWith(prefix)) {\n\t\t\t\t\t\tbestCountryPtr = &country;\n\t\t\t\t\t\tbestCallingCodePtr = &callingCode;\n\t\t\t\t\t\tbestLength = codeSize + prefix.size();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (callingCode.callingCode.startsWith(phoneNumber)) {\n\t\t\t\tisPrefix = true;\n\t\t\t}\n\t\t}\n\t}\n\tif (bestCountryPtr == nullptr) {\n\t\treturn FormatResult{ .formatted = phoneNumber };\n\t}\n\tif (args.onlyCode) {\n\t\treturn FormatResult{ .code = bestCallingCodePtr->callingCode };\n\t}\n\n\tconst auto codeSize = int(bestCallingCodePtr->callingCode.size());\n\n\tif (args.onlyGroups && args.incomplete) {\n\t\tauto initialGroups = args.skipCode\n\t\t\t? QVector<int>()\n\t\t\t: QVector<int>{ codeSize };\n\t\tauto initialGroupsSize = 0;\n\t\tif (bestCallingCodePtr->patterns.empty()) {\n\t\t\treturn FormatResult{ .groups = std::move(initialGroups) };\n\t\t}\n\t\tauto bestGroups = initialGroups;\n\t\tauto bestGroupsSize = initialGroupsSize;\n\t\tauto bestPatternMaxMatches = -1;\n\t\tfor (const auto &pattern : bestCallingCodePtr->patterns) {\n\t\t\tauto groups = initialGroups;\n\t\t\tauto groupSize = initialGroupsSize;\n\t\t\tauto lastSpacesCount = 0;\n\t\t\tauto maxMatchedDigits = 0;\n\t\t\tauto isNotBestPattern = false;\n\t\t\tfor (auto i = 0; i < pattern.size(); i++) {\n\t\t\t\tconst auto c = pattern.at(i);\n\t\t\t\tif (c.isDigit()) {\n\t\t\t\t\tconst auto n = (i - lastSpacesCount) + codeSize;\n\t\t\t\t\tif (n < phoneNumber.size()) {\n\t\t\t\t\t\tif (phoneNumber.at(n) == c) {\n\t\t\t\t\t\t\tmaxMatchedDigits++;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tisNotBestPattern = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tisNotBestPattern = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (c.isSpace()) {\n\t\t\t\t\tgroups.push_back(base::take(groupSize));\n\t\t\t\t\tlastSpacesCount++;\n\t\t\t\t} else {\n\t\t\t\t\tgroupSize++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (maxMatchedDigits > bestPatternMaxMatches) {\n\t\t\t\tbestPatternMaxMatches = isNotBestPattern\n\t\t\t\t\t? -1\n\t\t\t\t\t: maxMatchedDigits;\n\t\t\t\tbestGroups = std::move(groups);\n\t\t\t\tbestGroupsSize = groupSize;\n\t\t\t}\n\t\t}\n\t\tif (bestGroupsSize) {\n\t\t\tbestGroups.push_back(base::take(bestGroupsSize));\n\t\t}\n\t\treturn FormatResult{ .groups = std::move(bestGroups) };\n\t}\n\n\tconst auto formattedPart = phoneNumber.mid(codeSize);\n\tauto formattedResult = formattedPart;\n\tauto groups = QVector<int>();\n\tauto maxMatchedDigits = size_t(0);\n\tfor (auto &pattern : bestCallingCodePtr->patterns) {\n\t\tauto resultGroups = QVector<int>();\n\t\tauto result = QString();\n\t\tauto currentPatternPos = int(0);\n\t\tauto isFailedMatch = false;\n\t\tauto matchedDigits = size_t(0);\n\t\tauto groupSize = 0;\n\t\tfor (const auto &c : formattedPart) {\n\t\t\twhile ((currentPatternPos < pattern.size())\n\t\t\t\t&& (pattern[currentPatternPos] != 'X')\n\t\t\t\t&& !pattern[currentPatternPos].isDigit()) {\n\t\t\t\tif (args.onlyGroups) {\n\t\t\t\t\tresultGroups.push_back(groupSize);\n\t\t\t\t\tgroupSize = 0;\n\t\t\t\t} else {\n\t\t\t\t\tresult += pattern[currentPatternPos];\n\t\t\t\t}\n\t\t\t\tcurrentPatternPos++;\n\t\t\t}\n\t\t\t// Don't add an extra space to the end.\n\t\t\t// if (!args.onlyGroups && (currentPatternPos == pattern.size())) {\n\t\t\t// \tresult += ' ';\n\t\t\t// }\n\t\t\tif ((currentPatternPos >= pattern.size())\n\t\t\t\t|| (pattern[currentPatternPos] == 'X')) {\n\t\t\t\tcurrentPatternPos++;\n\t\t\t\tif (args.onlyGroups) {\n\t\t\t\t\tgroupSize++;\n\t\t\t\t} else {\n\t\t\t\t\tresult += c;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (c == pattern[currentPatternPos]) {\n\t\t\t\t\tmatchedDigits++;\n\t\t\t\t\tcurrentPatternPos++;\n\t\t\t\t\tif (args.onlyGroups) {\n\t\t\t\t\t\tgroupSize++;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresult += c;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tisFailedMatch = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (groupSize) {\n\t\t\tresultGroups.push_back(groupSize);\n\t\t}\n\t\tif (!isFailedMatch && matchedDigits >= maxMatchedDigits) {\n\t\t\tmaxMatchedDigits = matchedDigits;\n\t\t\tif (args.onlyGroups) {\n\t\t\t\tgroups = std::move(resultGroups);\n\t\t\t} else {\n\t\t\t\tformattedResult = std::move(result);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!args.skipCode) {\n\t\tif (args.onlyGroups) {\n\t\t\tgroups.push_front(codeSize);\n\t\t} else {\n\t\t\tformattedResult = '+'\n\t\t\t\t+ bestCallingCodePtr->callingCode\n\t\t\t\t+ ' '\n\t\t\t\t+ std::move(formattedResult);\n\t\t}\n\t}\n\n\treturn FormatResult{\n\t\t.formatted = (args.onlyGroups\n\t\t\t? QString()\n\t\t\t: std::move(formattedResult)),\n\t\t.groups = std::move(groups),\n\t};\n}\n\nrpl::producer<> CountriesInstance::updated() const {\n\treturn _updated.events();\n}\n\nCountriesInstance &Instance() {\n\treturn SingleInstance;\n}\n\nQString ExtractPhoneCode(const QString &phone) {\n\treturn Instance().format({ .phone = phone, .onlyCode = true }).code;\n}\n\nQVector<int> Groups(const QString &phone) {\n\treturn Instance().format({\n\t\t.phone = phone,\n\t\t.onlyGroups = true,\n\t\t.incomplete = true,\n\t}).groups;\n}\n\n} // namespace Countries\n"
  },
  {
    "path": "Telegram/SourceFiles/countries/countries_instance.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\n#pragma once\n\nnamespace Countries {\n\nstruct CallingCodeInfo {\n\tQString callingCode;\n\tstd::vector<QString> prefixes;\n\tstd::vector<QString> patterns;\n};\n\nstruct Info {\n\tQString name;\n\tQString iso2;\n\tQString alternativeName;\n\tstd::vector<CallingCodeInfo> codes;\n\tbool isHidden = false;\n};\n\nstruct FormatResult {\n\tQString formatted;\n\tQVector<int> groups;\n\tQString code;\n};\n\nstruct FormatArgs {\n\tQString phone;\n\tbool onlyGroups = false;\n\tbool skipCode = false;\n\tbool incomplete = false;\n\tbool onlyCode = false;\n};\n\nclass CountriesInstance final {\npublic:\n\tusing Map = QHash<QString, const Info *>;\n\n\tCountriesInstance();\n\t[[nodiscard]] const std::vector<Info> &list() const;\n\tvoid setList(std::vector<Info> &&infos);\n\n\t[[nodiscard]] const Map &byCode() const;\n\t[[nodiscard]] const Map &byISO2() const;\n\n\t[[nodiscard]] QString validPhoneCode(QString fullCode) const;\n\t[[nodiscard]] QString countryNameByISO2(const QString &iso) const;\n\t[[nodiscard]] QString countryISO2ByPhone(const QString &phone) const;\n\t[[nodiscard]] QString flagEmojiByISO2(const QString &iso) const;\n\n\t[[nodiscard]] FormatResult format(FormatArgs args) const;\n\n\t[[nodiscard]] rpl::producer<> updated() const;\n\nprivate:\n\tmutable std::vector<Info> _list;\n\n\tmutable Map _byCode;\n\tmutable Map _byISO2;\n\n\trpl::event_stream<> _updated;\n\n};\n\nCountriesInstance &Instance();\n\n[[nodiscard]] QString ExtractPhoneCode(const QString &phone);\n[[nodiscard]] QVector<int> Groups(const QString &phone);\n\n} // namespace Countries\n"
  },
  {
    "path": "Telegram/SourceFiles/countries/countries_manager.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"countries/countries_manager.h\"\n\n#include \"core/application.h\"\n#include \"countries/countries_instance.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"mtproto/mtp_instance.h\"\n\n#include <QtCore/QFile>\n\nnamespace Countries {\nnamespace {\n\nstruct FileData {\n\tint hash = 0;\n\tstd::vector<Info> infos;\n};\n\nauto ProcessAlternativeName(Info &&info) {\n\tif (info.name == u\"USA\"_q) {\n\t\tinfo.alternativeName = u\"United States of America\"_q;\n\t}\n\treturn std::move(info);\n}\n\n[[nodiscard]] QByteArray SerializeCodeInfo(const CallingCodeInfo &info) {\n\tauto result = QByteArray();\n\tauto stream = QDataStream(&result, QIODevice::WriteOnly);\n\tstream.setVersion(QDataStream::Qt_5_3);\n\tstream\n\t\t<< info.callingCode\n\t\t<< int(info.prefixes.size())\n\t\t<< int(info.patterns.size());\n\tfor (const auto &prefix : info.prefixes) {\n\t\tstream << prefix;\n\t}\n\tfor (const auto &pattern : info.patterns) {\n\t\tstream << pattern;\n\t}\n\tstream.device()->close();\n\n\treturn result;\n}\n\n[[nodiscard]] CallingCodeInfo DeserializeCodeInfo(const QByteArray &data) {\n\tauto stream = QDataStream(data);\n\tauto result = CallingCodeInfo();\n\tauto prefixesCount = qint32(0);\n\tauto patternsCount = qint32(0);\n\tstream\n\t\t>> result.callingCode\n\t\t>> prefixesCount\n\t\t>> patternsCount;\n\tfor (auto i = 0; i < prefixesCount; i++) {\n\t\tauto prefix = QString();\n\t\tstream >> prefix;\n\t\tresult.prefixes.push_back(std::move(prefix));\n\t}\n\tfor (auto i = 0; i < patternsCount; i++) {\n\t\tauto pattern = QString();\n\t\tstream >> pattern;\n\t\tresult.patterns.push_back(std::move(pattern));\n\t}\n\treturn (stream.status() != QDataStream::Ok)\n\t\t? CallingCodeInfo()\n\t\t: result;\n}\n\n[[nodiscard]] QByteArray SerializeInfo(const Info &info) {\n\tauto result = QByteArray();\n\tauto stream = QDataStream(&result, QIODevice::WriteOnly);\n\tstream.setVersion(QDataStream::Qt_5_3);\n\tstream\n\t\t<< info.name\n\t\t<< info.iso2\n\t\t<< info.alternativeName\n\t\t<< info.isHidden\n\t\t<< int(info.codes.size());\n\tfor (const auto &code : info.codes) {\n\t\tstream << SerializeCodeInfo(code);\n\t}\n\tstream.device()->close();\n\n\treturn result;\n}\n\n[[nodiscard]] Info DeserializeInfo(const QByteArray &data) {\n\tauto stream = QDataStream(data);\n\tauto result = Info();\n\tauto codesCount = qint32(0);\n\tstream\n\t\t>> result.name\n\t\t>> result.iso2\n\t\t>> result.alternativeName\n\t\t>> result.isHidden\n\t\t>> codesCount;\n\tfor (auto i = 0; i < codesCount; i++) {\n\t\tauto code = QByteArray();\n\t\tstream >> code;\n\t\tresult.codes.push_back(DeserializeCodeInfo(code));\n\t}\n\treturn (stream.status() != QDataStream::Ok)\n\t\t? Info()\n\t\t: result;\n}\n\n[[nodiscard]] QByteArray Serialize(const FileData &data) {\n\tauto result = QByteArray();\n\tauto stream = QDataStream(&result, QIODevice::WriteOnly);\n\tstream.setVersion(QDataStream::Qt_5_3);\n\tstream\n\t\t<< data.hash\n\t\t<< int(data.infos.size());\n\tfor (const auto &info : data.infos) {\n\t\tstream << SerializeInfo(info);\n\t}\n\tstream.device()->close();\n\n\treturn result;\n}\n\n[[nodiscard]] FileData Deserialize(const QByteArray &data) {\n\tauto stream = QDataStream(data);\n\tauto hash = int(0);\n\tauto infosCount = qint32(0);\n\tauto infos = std::vector<Info>();\n\tstream >> hash >> infosCount;\n\tfor (auto i = 0; i < infosCount; i++) {\n\t\tauto info = QByteArray();\n\t\tstream >> info;\n\t\tinfos.push_back(DeserializeInfo(info));\n\t}\n\treturn (stream.status() != QDataStream::Ok)\n\t\t? FileData()\n\t\t: FileData{ .hash = hash, .infos = std::move(infos) };\n}\n\n} // namespace\n\nManager::Manager(not_null<Main::Domain*> domain)\n: _path(cWorkingDir() + \"tdata/countries\") {\n\tread();\n\n\tconst auto mtpLifetime = _lifetime.make_state<rpl::lifetime>();\n\tdomain->activeValue(\n\t) | rpl::filter([=](Main::Account *account) {\n\t\treturn (account != nullptr);\n\t}) | rpl::on_next_done([=](Main::Account *account) {\n\t\t*mtpLifetime = account->mtpMainSessionValue(\n\t\t) | rpl::on_next([=](not_null<MTP::Instance*> instance) {\n\t\t\t_api.emplace(instance);\n\t\t\trequest();\n\t\t});\n\t}, [=] {\n\t\t_api.reset();\n\t}, _lifetime);\n}\n\nvoid Manager::read() {\n\tauto file = QFile(_path);\n\tif (!file.open(QIODevice::ReadOnly)) {\n\t\treturn;\n\t}\n\n\tauto stream = QDataStream(&file);\n\tauto data = QByteArray();\n\tstream >> data;\n\tauto fileData = Deserialize(data);\n\n\t_hash = fileData.hash;\n\tInstance().setList(base::take(fileData.infos));\n}\n\nvoid Manager::write() const {\n\tauto file = QFile(_path);\n\tif (!file.open(QIODevice::WriteOnly)) {\n\t\treturn;\n\t}\n\n\tauto stream = QDataStream(&file);\n\tstream << Serialize({ .hash = _hash, .infos = Instance().list() });\n}\n\nvoid Manager::request() {\n\tExpects(_api.has_value());\n\n\tconst auto convertMTP = [](const auto &vector, bool force = false) {\n\t\tif (!vector) {\n\t\t\treturn std::vector<QString>(force ? 1 : 0);\n\t\t}\n\t\treturn ranges::views::all(\n\t\t\tvector->v\n\t\t) | ranges::views::transform([](const MTPstring &s) -> QString {\n\t\t\treturn qs(s);\n\t\t}) | ranges::to_vector;\n\t};\n\n\t_api->request(MTPhelp_GetCountriesList(\n\t\tMTP_string(),\n\t\tMTP_int(_hash)\n\t)).done([=](const MTPhelp_CountriesList &result) {\n\t\tresult.match([&](const MTPDhelp_countriesList &data) {\n\t\t\t_hash = data.vhash().v;\n\n\t\t\tauto infos = std::vector<Info>();\n\n\t\t\tfor (const auto &country : data.vcountries().v) {\n\n\t\t\t\tconst auto &countryData = country.c_help_country();\n\t\t\t\tif (countryData.is_hidden()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tauto info = Info(ProcessAlternativeName({\n\t\t\t\t\t.name = countryData.vdefault_name().v,\n\t\t\t\t\t.iso2 = countryData.viso2().v,\n\t\t\t\t\t.isHidden = countryData.is_hidden(),\n\t\t\t\t}));\n\t\t\t\tfor (const auto &code : countryData.vcountry_codes().v) {\n\t\t\t\t\tconst auto &codeData = code.c_help_countryCode();\n\t\t\t\t\tinfo.codes.push_back(CallingCodeInfo{\n\t\t\t\t\t\t.callingCode = codeData.vcountry_code().v,\n\t\t\t\t\t\t.prefixes = convertMTP(codeData.vprefixes(), true),\n\t\t\t\t\t\t.patterns = convertMTP(codeData.vpatterns()),\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tinfos.push_back(std::move(info));\n\t\t\t}\n\n\t\t\tInstance().setList(std::move(infos));\n\t\t\twrite();\n\t\t}, [](const MTPDhelp_countriesListNotModified &data) {\n\t\t});\n\t\t_lifetime.destroy();\n\t}).fail([=](const MTP::Error &error) {\n\t\tLOG((\"API Error: getting countries failed with error %1\"\n\t\t\t).arg(error.type()));\n\t\t_lifetime.destroy();\n\t}).send();\n}\n\nrpl::lifetime &Manager::lifetime() {\n\treturn _lifetime;\n}\n\nManager::~Manager() {\n}\n\n} // namespace Countries\n"
  },
  {
    "path": "Telegram/SourceFiles/countries/countries_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n\nnamespace Main {\nclass Domain;\n} // namespace Main\n\nnamespace Countries {\n\nclass Manager final {\npublic:\n\tManager(not_null<Main::Domain*> domain);\n\t~Manager();\n\n\tvoid read();\n\tvoid write() const;\n\n\trpl::lifetime &lifetime();\n\nprivate:\n\tvoid request();\n\n\tstd::optional<MTP::Sender> _api;\n\tconst QString _path;\n\tint _hash = 0;\n\n\trpl::lifetime _lifetime;\n};\n\n} // namespace Countries\n"
  },
  {
    "path": "Telegram/SourceFiles/data/business/data_business_chatbots.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/business/data_business_chatbots.h\"\n\n#include \"apiwrap.h\"\n#include \"boxes/peers/edit_peer_permissions_box.h\"\n#include \"data/business/data_business_common.h\"\n#include \"data/business/data_business_info.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n\nnamespace Data {\n\nChatbots::Chatbots(not_null<Session*> owner)\n: _owner(owner) {\n}\n\nChatbots::~Chatbots() = default;\n\nvoid Chatbots::preload() {\n\tif (_loaded || _requestId) {\n\t\treturn;\n\t}\n\t_requestId = _owner->session().api().request(\n\t\tMTPaccount_GetConnectedBots()\n\t).done([=](const MTPaccount_ConnectedBots &result) {\n\t\t_requestId = 0;\n\t\t_loaded = true;\n\n\t\tconst auto &data = result.data();\n\t\t_owner->processUsers(data.vusers());\n\t\tconst auto &list = data.vconnected_bots().v;\n\t\tif (!list.isEmpty()) {\n\t\t\tconst auto &bot = list.front().data();\n\t\t\tconst auto botId = bot.vbot_id().v;\n\t\t\t_settings = ChatbotsSettings{\n\t\t\t\t.bot = _owner->session().data().user(botId),\n\t\t\t\t.recipients = FromMTP(_owner, bot.vrecipients()),\n\t\t\t\t.permissions = FromMTP(bot.vrights()),\n\t\t\t};\n\t\t} else {\n\t\t\t_settings.force_assign(ChatbotsSettings());\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\t_requestId = 0;\n\t\tLOG((\"API Error: Could not get connected bots %1 (%2)\"\n\t\t\t).arg(error.code()\n\t\t\t).arg(error.type()));\n\t}).send();\n}\n\nbool Chatbots::loaded() const {\n\treturn _loaded;\n}\n\nconst ChatbotsSettings &Chatbots::current() const {\n\treturn _settings.current();\n}\n\nrpl::producer<ChatbotsSettings> Chatbots::changes() const {\n\treturn _settings.changes();\n}\n\nrpl::producer<ChatbotsSettings> Chatbots::value() const {\n\treturn _settings.value();\n}\n\nvoid Chatbots::save(\n\t\tChatbotsSettings settings,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail) {\n\tconst auto was = _settings.current();\n\tif (was == settings) {\n\t\treturn;\n\t} else if (was.bot || settings.bot) {\n\t\tusing Flag = MTPaccount_UpdateConnectedBot::Flag;\n\t\tconst auto api = &_owner->session().api();\n\t\tapi->request(MTPaccount_UpdateConnectedBot(\n\t\t\tMTP_flags(!settings.bot ? Flag::f_deleted : Flag::f_rights),\n\t\t\tToMTP(settings.permissions),\n\t\t\t(settings.bot ? settings.bot : was.bot)->inputUser(),\n\t\t\tForBotsToMTP(settings.recipients)\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\tapi->applyUpdates(result);\n\t\t\tif (done) {\n\t\t\t\tdone();\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t_settings = was;\n\t\t\tif (fail) {\n\t\t\t\tfail(error.type());\n\t\t\t}\n\t\t}).send();\n\t}\n\t_settings = settings;\n}\n\nvoid Chatbots::togglePaused(not_null<PeerData*> peer, bool paused) {\n\tconst auto type = paused\n\t\t? SentRequestType::Pause\n\t\t: SentRequestType::Unpause;\n\tconst auto api = &_owner->session().api();\n\tconst auto i = _sentRequests.find(peer);\n\tif (i != end(_sentRequests)) {\n\t\tconst auto already = i->second.type;\n\t\tif (already == SentRequestType::Remove || already == type) {\n\t\t\treturn;\n\t\t}\n\t\tapi->request(i->second.requestId).cancel();\n\t\t_sentRequests.erase(i);\n\t}\n\tconst auto id = api->request(MTPaccount_ToggleConnectedBotPaused(\n\t\tpeer->input(),\n\t\tMTP_bool(paused)\n\t)).done([=] {\n\t\tif (_sentRequests[peer].type != type) {\n\t\t\treturn;\n\t\t} else if (const auto settings = peer->barSettings()) {\n\t\t\tpeer->setBarSettings(paused\n\t\t\t\t? ((*settings | PeerBarSetting::BusinessBotPaused)\n\t\t\t\t\t& ~PeerBarSetting::BusinessBotCanReply)\n\t\t\t\t: ((*settings & ~PeerBarSetting::BusinessBotPaused)\n\t\t\t\t\t| PeerBarSetting::BusinessBotCanReply));\n\t\t} else {\n\t\t\tapi->requestPeerSettings(peer);\n\t\t}\n\t\t_sentRequests.remove(peer);\n\t}).fail([=] {\n\t\tif (_sentRequests[peer].type != type) {\n\t\t\treturn;\n\t\t}\n\t\tapi->requestPeerSettings(peer);\n\t\t_sentRequests.remove(peer);\n\t}).send();\n\t_sentRequests[peer] = SentRequest{ type, id };\n}\n\nvoid Chatbots::removeFrom(not_null<PeerData*> peer) {\n\tconst auto type = SentRequestType::Remove;\n\tconst auto api = &_owner->session().api();\n\tconst auto i = _sentRequests.find(peer);\n\tif (i != end(_sentRequests)) {\n\t\tconst auto already = i->second.type;\n\t\tif (already == type) {\n\t\t\treturn;\n\t\t}\n\t\tapi->request(i->second.requestId).cancel();\n\t\t_sentRequests.erase(i);\n\t}\n\tconst auto id = api->request(MTPaccount_DisablePeerConnectedBot(\n\t\tpeer->input()\n\t)).done([=] {\n\t\tif (_sentRequests[peer].type != type) {\n\t\t\treturn;\n\t\t} else if (const auto settings = peer->barSettings()) {\n\t\t\tpeer->clearBusinessBot();\n\t\t} else {\n\t\t\tapi->requestPeerSettings(peer);\n\t\t}\n\t\t_sentRequests.remove(peer);\n\t\treload();\n\t}).fail([=] {\n\t\tapi->requestPeerSettings(peer);\n\t\t_sentRequests.remove(peer);\n\t}).send();\n\t_sentRequests[peer] = SentRequest{ type, id };\n}\n\nvoid Chatbots::reload() {\n\t_loaded = false;\n\t_owner->session().api().request(base::take(_requestId)).cancel();\n\tpreload();\n}\n\nEditFlagsDescriptor<ChatbotsPermissions> ChatbotsPermissionsLabels() {\n\tusing Flag = ChatbotsPermission;\n\n\tusing PermissionLabel = EditFlagsLabel<ChatbotsPermissions>;\n\tauto messages = std::vector<PermissionLabel>{\n\t\t{ Flag::ViewMessages, tr::lng_chatbots_read(tr::now) },\n\t\t{ Flag::ReplyToMessages, tr::lng_chatbots_reply(tr::now) },\n\t\t{ Flag::MarkAsRead, tr::lng_chatbots_mark_as_read(tr::now) },\n\t\t{ Flag::DeleteSent, tr::lng_chatbots_delete_sent(tr::now) },\n\t\t{ Flag::DeleteReceived, tr::lng_chatbots_delete_received(tr::now) },\n\t};\n\tauto manage = std::vector<PermissionLabel>{\n\t\t{ Flag::EditName, tr::lng_chatbots_edit_name(tr::now) },\n\t\t{ Flag::EditBio, tr::lng_chatbots_edit_bio(tr::now) },\n\t\t{ Flag::EditUserpic, tr::lng_chatbots_edit_userpic(tr::now) },\n\t\t{ Flag::EditUsername, tr::lng_chatbots_edit_username(tr::now) },\n\t};\n\tauto gifts = std::vector<PermissionLabel>{\n\t\t{ Flag::ViewGifts, tr::lng_chatbots_view_gifts(tr::now) },\n\t\t{ Flag::SellGifts, tr::lng_chatbots_sell_gifts(tr::now) },\n\t\t{ Flag::GiftSettings, tr::lng_chatbots_gift_settings(tr::now) },\n\t\t{ Flag::TransferGifts, tr::lng_chatbots_transfer_gifts(tr::now) },\n\t\t{ Flag::TransferStars, tr::lng_chatbots_transfer_stars(tr::now) },\n\t};\n\tauto stories = std::vector<PermissionLabel>{\n\t\t{ Flag::ManageStories, tr::lng_chatbots_manage_stories(tr::now) },\n\t};\n\treturn { .labels = {\n\t\t{ tr::lng_chatbots_manage_messages(), std::move(messages) },\n\t\t{ tr::lng_chatbots_manage_profile(), std::move(manage) },\n\t\t{ tr::lng_chatbots_manage_gifts(), std::move(gifts) },\n\t\t{ std::nullopt, std::move(stories) },\n\t}, .st = nullptr };\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/business/data_business_chatbots.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/business/data_business_common.h\"\n\nclass UserData;\n\ntemplate <typename Flags>\nstruct EditFlagsDescriptor;\n\nnamespace Data {\n\nclass Session;\n\nstruct ChatbotsSettings {\n\tUserData *bot = nullptr;\n\tBusinessRecipients recipients;\n\tChatbotsPermissions permissions;\n\n\tfriend inline bool operator==(\n\t\tconst ChatbotsSettings &,\n\t\tconst ChatbotsSettings &) = default;\n};\n\nclass Chatbots final {\npublic:\n\texplicit Chatbots(not_null<Session*> owner);\n\t~Chatbots();\n\n\tvoid preload();\n\t[[nodiscard]] bool loaded() const;\n\t[[nodiscard]] const ChatbotsSettings &current() const;\n\t[[nodiscard]] rpl::producer<ChatbotsSettings> changes() const;\n\t[[nodiscard]] rpl::producer<ChatbotsSettings> value() const;\n\n\tvoid save(\n\t\tChatbotsSettings settings,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail);\n\n\tvoid togglePaused(not_null<PeerData*> peer, bool paused);\n\tvoid removeFrom(not_null<PeerData*> peer);\n\nprivate:\n\tenum class SentRequestType {\n\t\tPause,\n\t\tUnpause,\n\t\tRemove,\n\t};\n\tstruct SentRequest {\n\t\tSentRequestType type = SentRequestType::Pause;\n\t\tmtpRequestId requestId = 0;\n\t};\n\n\tvoid reload();\n\n\tconst not_null<Session*> _owner;\n\n\trpl::variable<ChatbotsSettings> _settings;\n\tmtpRequestId _requestId = 0;\n\tbool _loaded = false;\n\n\tbase::flat_map<not_null<PeerData*>, SentRequest> _sentRequests;\n\n};\n\n[[nodiscard]] auto ChatbotsPermissionsLabels()\n-> EditFlagsDescriptor<ChatbotsPermissions>;\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/business/data_business_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/business/data_business_common.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kDay = WorkingInterval::kDay;\nconstexpr auto kWeek = WorkingInterval::kWeek;\nconstexpr auto kInNextDayMax = WorkingInterval::kInNextDayMax;\n\n[[nodiscard]] WorkingIntervals SortAndMerge(WorkingIntervals intervals) {\n\tauto &list = intervals.list;\n\tranges::sort(list, ranges::less(), &WorkingInterval::start);\n\tfor (auto i = 0, count = int(list.size()); i != count; ++i) {\n\t\tif (i && list[i] && list[i -1] && list[i].start <= list[i - 1].end) {\n\t\t\tlist[i - 1] = list[i - 1].united(list[i]);\n\t\t\tlist[i] = {};\n\t\t}\n\t\tif (!list[i]) {\n\t\t\tlist.erase(list.begin() + i);\n\t\t\t--i;\n\t\t\t--count;\n\t\t}\n\t}\n\treturn intervals;\n}\n\n[[nodiscard]] WorkingIntervals MoveTailToFront(WorkingIntervals intervals) {\n\tauto &list = intervals.list;\n\tauto after = WorkingInterval{ kWeek, kWeek + kDay };\n\twhile (!list.empty()) {\n\t\tif (const auto tail = list.back().intersected(after)) {\n\t\t\tlist.back().end = tail.start;\n\t\t\tif (!list.back()) {\n\t\t\t\tlist.pop_back();\n\t\t\t}\n\t\t\tlist.insert(begin(list), tail.shifted(-kWeek));\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn intervals;\n}\n\ntemplate <typename Flag>\nauto RecipientsFlags(const BusinessRecipients &data) {\n\tconst auto &chats = data.allButExcluded\n\t\t? data.excluded\n\t\t: data.included;\n\tusing Type = BusinessChatType;\n\treturn Flag()\n\t\t| ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag())\n\t\t| ((chats.types & Type::ExistingChats)\n\t\t\t? Flag::f_existing_chats\n\t\t\t: Flag())\n\t\t| ((chats.types & Type::Contacts) ? Flag::f_contacts : Flag())\n\t\t| ((chats.types & Type::NonContacts) ? Flag::f_non_contacts : Flag())\n\t\t| (chats.list.empty() ? Flag() : Flag::f_users)\n\t\t| (data.allButExcluded ? Flag::f_exclude_selected : Flag());\n}\n\n} // namespace\n\nBusinessRecipients BusinessRecipients::MakeValid(BusinessRecipients value) {\n\tif (value.included.empty()) {\n\t\tvalue.allButExcluded = true;\n\t}\n\treturn value;\n}\n\nMTPInputBusinessRecipients ForMessagesToMTP(const BusinessRecipients &data) {\n\tusing Flag = MTPDinputBusinessRecipients::Flag;\n\tconst auto &chats = data.allButExcluded ? data.excluded : data.included;\n\treturn MTP_inputBusinessRecipients(\n\t\tMTP_flags(RecipientsFlags<Flag>(data)),\n\t\tMTP_vector_from_range(chats.list\n\t\t\t| ranges::views::transform(&UserData::inputUser)));\n}\n\nMTPInputBusinessBotRecipients ForBotsToMTP(const BusinessRecipients &data) {\n\tusing Flag = MTPDinputBusinessBotRecipients::Flag;\n\tconst auto &chats = data.allButExcluded ? data.excluded : data.included;\n\treturn MTP_inputBusinessBotRecipients(\n\t\tMTP_flags(RecipientsFlags<Flag>(data)\n\t\t\t| ((data.allButExcluded || data.excluded.empty())\n\t\t\t\t? Flag()\n\t\t\t\t: Flag::f_exclude_users)),\n\t\tMTP_vector_from_range(chats.list\n\t\t\t| ranges::views::transform(&UserData::inputUser)),\n\t\tMTP_vector_from_range(data.excluded.list\n\t\t\t| ranges::views::transform(&UserData::inputUser)));\n}\n\nBusinessRecipients FromMTP(\n\t\tnot_null<Session*> owner,\n\t\tconst MTPBusinessRecipients &recipients) {\n\tusing Type = BusinessChatType;\n\n\tconst auto &data = recipients.data();\n\tauto result = BusinessRecipients{\n\t\t.allButExcluded = data.is_exclude_selected(),\n\t};\n\tauto &chats = result.allButExcluded\n\t\t? result.excluded\n\t\t: result.included;\n\tchats.types = Type()\n\t\t| (data.is_new_chats() ? Type::NewChats : Type())\n\t\t| (data.is_existing_chats() ? Type::ExistingChats : Type())\n\t\t| (data.is_contacts() ? Type::Contacts : Type())\n\t\t| (data.is_non_contacts() ? Type::NonContacts : Type());\n\tif (const auto users = data.vusers()) {\n\t\tfor (const auto &userId : users->v) {\n\t\t\tchats.list.push_back(owner->user(UserId(userId.v)));\n\t\t}\n\t}\n\treturn result;\n}\n\nBusinessRecipients FromMTP(\n\t\tnot_null<Session*> owner,\n\t\tconst MTPBusinessBotRecipients &recipients) {\n\tusing Type = BusinessChatType;\n\n\tconst auto &data = recipients.data();\n\tauto result = BusinessRecipients{\n\t\t.allButExcluded = data.is_exclude_selected(),\n\t};\n\tauto &chats = result.allButExcluded\n\t\t? result.excluded\n\t\t: result.included;\n\tchats.types = Type()\n\t\t| (data.is_new_chats() ? Type::NewChats : Type())\n\t\t| (data.is_existing_chats() ? Type::ExistingChats : Type())\n\t\t| (data.is_contacts() ? Type::Contacts : Type())\n\t\t| (data.is_non_contacts() ? Type::NonContacts : Type());\n\tif (const auto users = data.vusers()) {\n\t\tfor (const auto &userId : users->v) {\n\t\t\tchats.list.push_back(owner->user(UserId(userId.v)));\n\t\t}\n\t}\n\tif (!result.allButExcluded) {\n\t\tif (const auto excluded = data.vexclude_users()) {\n\t\t\tfor (const auto &userId : excluded->v) {\n\t\t\t\tresult.excluded.list.push_back(\n\t\t\t\t\towner->user(UserId(userId.v)));\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nChatbotsPermissions FromMTP(const MTPBusinessBotRights &rights) {\n\tusing Flag = ChatbotsPermission;\n\tconst auto &data = rights.data();\n\n\treturn Flag::ViewMessages\n\t\t| (data.is_reply() ? Flag::ReplyToMessages : Flag())\n\t\t| (data.is_read_messages() ? Flag::MarkAsRead : Flag())\n\t\t| (data.is_delete_sent_messages() ? Flag::DeleteSent : Flag())\n\t\t| (data.is_delete_received_messages() ? Flag::DeleteReceived : Flag())\n\t\t| (data.is_edit_name() ? Flag::EditName : Flag())\n\t\t| (data.is_edit_bio() ? Flag::EditBio : Flag())\n\t\t| (data.is_edit_profile_photo() ? Flag::EditUserpic : Flag())\n\t\t| (data.is_edit_username() ? Flag::EditUsername : Flag())\n\t\t| (data.is_view_gifts() ? Flag::ViewGifts : Flag())\n\t\t| (data.is_sell_gifts() ? Flag::SellGifts : Flag())\n\t\t| (data.is_change_gift_settings() ? Flag::GiftSettings : Flag())\n\t\t| (data.is_transfer_and_upgrade_gifts() ? Flag::TransferGifts : Flag())\n\t\t| (data.is_transfer_stars() ? Flag::TransferStars : Flag())\n\t\t| (data.is_manage_stories() ? Flag::ManageStories : Flag());\n}\n\nMTPBusinessBotRights ToMTP(ChatbotsPermissions rights) {\n\tusing Flag = MTPDbusinessBotRights::Flag;\n\tusing Right = ChatbotsPermission;\n\treturn MTP_businessBotRights(MTP_flags(Flag()\n\t\t| ((rights & Right::ReplyToMessages) ? Flag::f_reply : Flag())\n\t\t| ((rights & Right::MarkAsRead) ? Flag::f_read_messages : Flag())\n\t\t| ((rights & Right::DeleteSent) ? Flag::f_delete_sent_messages : Flag())\n\t\t| ((rights & Right::DeleteReceived) ? Flag::f_delete_received_messages : Flag())\n\t\t| ((rights & Right::EditName) ? Flag::f_edit_name : Flag())\n\t\t| ((rights & Right::EditBio) ? Flag::f_edit_bio : Flag())\n\t\t| ((rights & Right::EditUserpic) ? Flag::f_edit_profile_photo : Flag())\n\t\t| ((rights & Right::EditUsername) ? Flag::f_edit_username : Flag())\n\t\t| ((rights & Right::ViewGifts) ? Flag::f_view_gifts : Flag())\n\t\t| ((rights & Right::SellGifts) ? Flag::f_sell_gifts : Flag())\n\t\t| ((rights & Right::GiftSettings) ? Flag::f_change_gift_settings : Flag())\n\t\t| ((rights & Right::TransferGifts) ? Flag::f_transfer_and_upgrade_gifts : Flag())\n\t\t| ((rights & Right::TransferStars) ? Flag::f_transfer_stars : Flag())\n\t\t| ((rights & Right::ManageStories) ? Flag::f_manage_stories : Flag())));\n}\n\nBusinessDetails FromMTP(\n\t\tnot_null<Session*> owner,\n\t\tconst tl::conditional<MTPBusinessWorkHours> &hours,\n\t\tconst tl::conditional<MTPBusinessLocation> &location,\n\t\tconst tl::conditional<MTPBusinessIntro> &intro) {\n\tauto result = BusinessDetails();\n\tif (hours) {\n\t\tconst auto &data = hours->data();\n\t\tresult.hours.timezoneId = qs(data.vtimezone_id());\n\t\tresult.hours.intervals.list = ranges::views::all(\n\t\t\tdata.vweekly_open().v\n\t\t) | ranges::views::transform([](const MTPBusinessWeeklyOpen &open) {\n\t\t\tconst auto &data = open.data();\n\t\t\treturn WorkingInterval{\n\t\t\t\tdata.vstart_minute().v * 60,\n\t\t\t\tdata.vend_minute().v * 60,\n\t\t\t};\n\t\t}) | ranges::to_vector;\n\t}\n\tif (location) {\n\t\tconst auto &data = location->data();\n\t\tresult.location.address = qs(data.vaddress());\n\t\tif (const auto point = data.vgeo_point()) {\n\t\t\tpoint->match([&](const MTPDgeoPoint &data) {\n\t\t\t\tresult.location.point = LocationPoint(data);\n\t\t\t}, [&](const MTPDgeoPointEmpty &) {\n\t\t\t});\n\t\t}\n\t}\n\tif (intro) {\n\t\tconst auto &data = intro->data();\n\t\tresult.intro.title = qs(data.vtitle());\n\t\tresult.intro.description = qs(data.vdescription());\n\t\tif (const auto document = data.vsticker()) {\n\t\t\tresult.intro.sticker = owner->processDocument(*document);\n\t\t\tif (!result.intro.sticker->sticker()) {\n\t\t\t\tresult.intro.sticker = nullptr;\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] AwaySettings FromMTP(\n\t\tnot_null<Session*> owner,\n\t\tconst tl::conditional<MTPBusinessAwayMessage> &message) {\n\tif (!message) {\n\t\treturn AwaySettings();\n\t}\n\tconst auto &data = message->data();\n\tauto result = AwaySettings{\n\t\t.recipients = FromMTP(owner, data.vrecipients()),\n\t\t.shortcutId = data.vshortcut_id().v,\n\t\t.offlineOnly = data.is_offline_only(),\n\t};\n\tdata.vschedule().match([&](\n\t\t\tconst MTPDbusinessAwayMessageScheduleAlways &) {\n\t\tresult.schedule.type = AwayScheduleType::Always;\n\t}, [&](const MTPDbusinessAwayMessageScheduleOutsideWorkHours &) {\n\t\tresult.schedule.type = AwayScheduleType::OutsideWorkingHours;\n\t}, [&](const MTPDbusinessAwayMessageScheduleCustom &data) {\n\t\tresult.schedule.type = AwayScheduleType::Custom;\n\t\tresult.schedule.customInterval = WorkingInterval{\n\t\t\tdata.vstart_date().v,\n\t\t\tdata.vend_date().v,\n\t\t};\n\t});\n\treturn result;\n}\n\n[[nodiscard]] GreetingSettings FromMTP(\n\t\tnot_null<Session*> owner,\n\t\tconst tl::conditional<MTPBusinessGreetingMessage> &message) {\n\tif (!message) {\n\t\treturn GreetingSettings();\n\t}\n\tconst auto &data = message->data();\n\treturn GreetingSettings{\n\t\t.recipients = FromMTP(owner, data.vrecipients()),\n\t\t.noActivityDays = data.vno_activity_days().v,\n\t\t.shortcutId = data.vshortcut_id().v,\n\t};\n}\n\nWorkingIntervals WorkingIntervals::normalized() const {\n\treturn SortAndMerge(MoveTailToFront(SortAndMerge(*this)));\n}\n\nWorkingIntervals ExtractDayIntervals(\n\t\tconst WorkingIntervals &intervals,\n\t\tint dayIndex) {\n\tExpects(dayIndex >= 0 && dayIndex < 7);\n\n\tauto result = WorkingIntervals();\n\tauto &list = result.list;\n\tfor (const auto &interval : intervals.list) {\n\t\tconst auto now = interval.intersected(\n\t\t\t{ (dayIndex - 1) * kDay, (dayIndex + 2) * kDay });\n\t\tconst auto after = interval.intersected(\n\t\t\t{ (dayIndex + 6) * kDay, (dayIndex + 9) * kDay });\n\t\tconst auto before = interval.intersected(\n\t\t\t{ (dayIndex - 8) * kDay, (dayIndex - 5) * kDay });\n\t\tif (now) {\n\t\t\tlist.push_back(now.shifted(-dayIndex * kDay));\n\t\t}\n\t\tif (after) {\n\t\t\tlist.push_back(after.shifted(-(dayIndex + 7) * kDay));\n\t\t}\n\t\tif (before) {\n\t\t\tlist.push_back(before.shifted(-(dayIndex - 7) * kDay));\n\t\t}\n\t}\n\tresult = result.normalized();\n\n\tconst auto outside = [&](WorkingInterval interval) {\n\t\treturn (interval.end <= 0) || (interval.start >= kDay);\n\t};\n\tlist.erase(ranges::remove_if(list, outside), end(list));\n\n\tif (!list.empty() && list.back().start <= 0 && list.back().end >= kDay) {\n\t\tlist.back() = { 0, kDay };\n\t} else if (!list.empty() && (list.back().end > kDay + kInNextDayMax)) {\n\t\tlist.back() = list.back().intersected({ 0, kDay });\n\t}\n\tif (!list.empty() && list.front().start <= 0) {\n\t\tif (list.front().start < 0\n\t\t\t&& list.front().end <= kInNextDayMax\n\t\t\t&& list.front().start > -kDay) {\n\t\t\tlist.erase(begin(list));\n\t\t} else {\n\t\t\tlist.front() = list.front().intersected({ 0, kDay });\n\t\t\tif (!list.front()) {\n\t\t\t\tlist.erase(begin(list));\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result;\n}\n\nbool IsFullOpen(const WorkingIntervals &extractedDay) {\n\treturn extractedDay // 00:00-23:59 or 00:00-00:00 (next day)\n\t\t&& (extractedDay.list.front() == WorkingInterval{ 0, kDay - 60 }\n\t\t\t|| extractedDay.list.front() == WorkingInterval{ 0, kDay });\n}\n\nWorkingIntervals RemoveDayIntervals(\n\t\tconst WorkingIntervals &intervals,\n\t\tint dayIndex) {\n\tauto result = intervals.normalized();\n\tauto &list = result.list;\n\tconst auto day = WorkingInterval{ 0, kDay };\n\tconst auto shifted = day.shifted(dayIndex * kDay);\n\tauto before = WorkingInterval{ 0, shifted.start };\n\tauto after = WorkingInterval{ shifted.end, kWeek };\n\tfor (auto i = 0, count = int(list.size()); i != count; ++i) {\n\t\tif (list[i].end <= shifted.start || list[i].start >= shifted.end) {\n\t\t\tcontinue;\n\t\t} else if (list[i].end <= shifted.start + kInNextDayMax\n\t\t\t&& (list[i].start < shifted.start\n\t\t\t\t|| (!dayIndex // This 'Sunday' finishing on next day <= 6:00.\n\t\t\t\t\t&& list[i].start == shifted.start\n\t\t\t\t\t&& list.back().end >= kWeek))) {\n\t\t\tcontinue;\n\t\t} else if (const auto first = list[i].intersected(before)) {\n\t\t\tlist[i] = first;\n\t\t\tif (const auto second = list[i].intersected(after)) {\n\t\t\t\tlist.push_back(second);\n\t\t\t}\n\t\t} else if (const auto second = list[i].intersected(after)) {\n\t\t\tlist[i] = second;\n\t\t} else {\n\t\t\tlist.erase(list.begin() + i);\n\t\t\t--i;\n\t\t\t--count;\n\t\t}\n\t}\n\treturn result.normalized();\n}\n\nWorkingIntervals ReplaceDayIntervals(\n\t\tconst WorkingIntervals &intervals,\n\t\tint dayIndex,\n\t\tWorkingIntervals replacement) {\n\tauto result = RemoveDayIntervals(intervals, dayIndex);\n\tconst auto first = result.list.insert(\n\t\tend(result.list),\n\t\tbegin(replacement.list),\n\t\tend(replacement.list));\n\tfor (auto &interval : ranges::make_subrange(first, end(result.list))) {\n\t\tinterval = interval.shifted(dayIndex * kDay);\n\t}\n\treturn result.normalized();\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/business/data_business_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n#include \"data/data_location.h\"\n\nclass UserData;\n\nnamespace Data {\n\nclass Session;\n\nenum class BusinessChatType {\n\tNewChats = (1 << 0),\n\tExistingChats = (1 << 1),\n\tContacts = (1 << 2),\n\tNonContacts = (1 << 3),\n};\ninline constexpr bool is_flag_type(BusinessChatType) { return true; }\n\nusing BusinessChatTypes = base::flags<BusinessChatType>;\n\nstruct BusinessChats {\n\tBusinessChatTypes types;\n\tstd::vector<not_null<UserData*>> list;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn !types && list.empty();\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst BusinessChats &a,\n\t\tconst BusinessChats &b) = default;\n};\n\nstruct BusinessRecipients {\n\tBusinessChats included;\n\tBusinessChats excluded;\n\tbool allButExcluded = false;\n\n\t[[nodiscard]] static BusinessRecipients MakeValid(\n\t\tBusinessRecipients value);\n\n\tfriend inline bool operator==(\n\t\tconst BusinessRecipients &a,\n\t\tconst BusinessRecipients &b) = default;\n};\n\nenum class BusinessRecipientsType : uchar {\n\tMessages,\n\tBots,\n};\n\nenum class ChatbotsPermission {\n\tViewMessages    = 0x0001,\n\tReplyToMessages = 0x0002,\n\tMarkAsRead      = 0x0004,\n\tDeleteSent      = 0x0008,\n\tDeleteReceived  = 0x0010,\n\tEditName        = 0x0020,\n\tEditBio         = 0x0040,\n\tEditUserpic     = 0x0080,\n\tEditUsername    = 0x0100,\n\tViewGifts       = 0x0200,\n\tSellGifts       = 0x0400,\n\tGiftSettings    = 0x0800,\n\tTransferGifts   = 0x1000,\n\tTransferStars   = 0x2000,\n\tManageStories   = 0x4000,\n};\ninline constexpr bool is_flag_type(ChatbotsPermission) { return true; }\nusing ChatbotsPermissions = base::flags<ChatbotsPermission>;\n\n[[nodiscard]] MTPInputBusinessRecipients ForMessagesToMTP(\n\tconst BusinessRecipients &data);\n[[nodiscard]] MTPInputBusinessBotRecipients ForBotsToMTP(\n\tconst BusinessRecipients &data);\n[[nodiscard]] BusinessRecipients FromMTP(\n\tnot_null<Session*> owner,\n\tconst MTPBusinessRecipients &recipients);\n[[nodiscard]] BusinessRecipients FromMTP(\n\tnot_null<Session*> owner,\n\tconst MTPBusinessBotRecipients &recipients);\n[[nodiscard]] ChatbotsPermissions FromMTP(\n\tconst MTPBusinessBotRights &rights);\n[[nodiscard]] MTPBusinessBotRights ToMTP(ChatbotsPermissions rights);\n\nstruct Timezone {\n\tQString id;\n\tQString name;\n\tTimeId utcOffset = 0;\n\n\tfriend inline bool operator==(\n\t\tconst Timezone &a,\n\t\tconst Timezone &b) = default;\n};\n\nstruct Timezones {\n\tstd::vector<Timezone> list;\n\n\tfriend inline bool operator==(\n\t\tconst Timezones &a,\n\t\tconst Timezones &b) = default;\n};;\n\nstruct WorkingInterval {\n\tstatic constexpr auto kDay = 24 * 3600;\n\tstatic constexpr auto kWeek = 7 * kDay;\n\tstatic constexpr auto kInNextDayMax = 6 * 3600;\n\n\tTimeId start = 0;\n\tTimeId end = 0;\n\n\texplicit operator bool() const {\n\t\treturn start < end;\n\t}\n\n\t[[nodiscard]] WorkingInterval shifted(TimeId offset) const {\n\t\treturn { start + offset, end + offset };\n\t}\n\t[[nodiscard]] WorkingInterval united(WorkingInterval other) const {\n\t\tif (!*this) {\n\t\t\treturn other;\n\t\t} else if (!other) {\n\t\t\treturn *this;\n\t\t}\n\t\treturn {\n\t\t\tstd::min(start, other.start),\n\t\t\tstd::max(end, other.end),\n\t\t};\n\t}\n\t[[nodiscard]] WorkingInterval intersected(WorkingInterval other) const {\n\t\tconst auto result = WorkingInterval{\n\t\t\tstd::max(start, other.start),\n\t\t\tstd::min(end, other.end),\n\t\t};\n\t\treturn result ? result : WorkingInterval();\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst WorkingInterval &a,\n\t\tconst WorkingInterval &b) = default;\n};\n\nstruct WorkingIntervals {\n\tstd::vector<WorkingInterval> list;\n\n\t[[nodiscard]] WorkingIntervals normalized() const;\n\n\texplicit operator bool() const {\n\t\tfor (const auto &interval : list) {\n\t\t\tif (interval) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\tfriend inline bool operator==(\n\t\tconst WorkingIntervals &a,\n\t\tconst WorkingIntervals &b) = default;\n};\n\nstruct WorkingHours {\n\tWorkingIntervals intervals;\n\tQString timezoneId;\n\n\t[[nodiscard]] WorkingHours normalized() const {\n\t\treturn { intervals.normalized(), timezoneId };\n\t}\n\n\texplicit operator bool() const {\n\t\treturn !timezoneId.isEmpty() && !intervals.list.empty();\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst WorkingHours &a,\n\t\tconst WorkingHours &b) = default;\n};\n\n[[nodiscard]] WorkingIntervals ExtractDayIntervals(\n\tconst WorkingIntervals &intervals,\n\tint dayIndex);\n[[nodiscard]] bool IsFullOpen(const WorkingIntervals &extractedDay);\n[[nodiscard]] WorkingIntervals RemoveDayIntervals(\n\tconst WorkingIntervals &intervals,\n\tint dayIndex);\n[[nodiscard]] WorkingIntervals ReplaceDayIntervals(\n\tconst WorkingIntervals &intervals,\n\tint dayIndex,\n\tWorkingIntervals replacement);\n\nstruct BusinessLocation {\n\tQString address;\n\tstd::optional<LocationPoint> point;\n\n\texplicit operator bool() const {\n\t\treturn !address.isEmpty();\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst BusinessLocation &a,\n\t\tconst BusinessLocation &b) = default;\n};\n\nstruct ChatIntro {\n\tQString title;\n\tQString description;\n\tDocumentData *sticker = nullptr;\n\n\t[[nodiscard]] bool customPhrases() const {\n\t\treturn !title.isEmpty() || !description.isEmpty();\n\t}\n\n\texplicit operator bool() const {\n\t\treturn customPhrases() || sticker;\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst ChatIntro &a,\n\t\tconst ChatIntro &b) = default;\n};\n\nstruct BusinessDetails {\n\tWorkingHours hours;\n\tBusinessLocation location;\n\tChatIntro intro;\n\n\texplicit operator bool() const {\n\t\treturn hours || location || intro;\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst BusinessDetails &a,\n\t\tconst BusinessDetails &b) = default;\n};\n\n[[nodiscard]] BusinessDetails FromMTP(\n\tnot_null<Session*> owner,\n\tconst tl::conditional<MTPBusinessWorkHours> &hours,\n\tconst tl::conditional<MTPBusinessLocation> &location,\n\tconst tl::conditional<MTPBusinessIntro> &intro);\n\nenum class AwayScheduleType : uchar {\n\tNever = 0,\n\tAlways = 1,\n\tOutsideWorkingHours = 2,\n\tCustom = 3,\n};\n\nstruct AwaySchedule {\n\tAwayScheduleType type = AwayScheduleType::Never;\n\tWorkingInterval customInterval;\n\n\tfriend inline bool operator==(\n\t\tconst AwaySchedule &a,\n\t\tconst AwaySchedule &b) = default;\n};\n\nstruct AwaySettings {\n\tBusinessRecipients recipients;\n\tAwaySchedule schedule;\n\tBusinessShortcutId shortcutId = 0;\n\tbool offlineOnly = false;\n\n\texplicit operator bool() const {\n\t\treturn schedule.type != AwayScheduleType::Never;\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst AwaySettings &a,\n\t\tconst AwaySettings &b) = default;\n};\n\n[[nodiscard]] AwaySettings FromMTP(\n\tnot_null<Session*> owner,\n\tconst tl::conditional<MTPBusinessAwayMessage> &message);\n\nstruct GreetingSettings {\n\tBusinessRecipients recipients;\n\tint noActivityDays = 0;\n\tBusinessShortcutId shortcutId = 0;\n\n\texplicit operator bool() const {\n\t\treturn noActivityDays > 0;\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst GreetingSettings &a,\n\t\tconst GreetingSettings &b) = default;\n};\n\n[[nodiscard]] GreetingSettings FromMTP(\n\tnot_null<Session*> owner,\n\tconst tl::conditional<MTPBusinessGreetingMessage> &message);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/business/data_business_info.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/business/data_business_info.h\"\n\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"data/business/data_business_common.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"main/main_session.h\"\n\nnamespace Data {\nnamespace {\n\n[[nodiscard]] MTPBusinessWorkHours ToMTP(const WorkingHours &data) {\n\tconst auto list = data.intervals.normalized().list;\n\tconst auto proj = [](const WorkingInterval &data) {\n\t\treturn MTPBusinessWeeklyOpen(MTP_businessWeeklyOpen(\n\t\t\tMTP_int(data.start / 60),\n\t\t\tMTP_int(data.end / 60)));\n\t};\n\treturn MTP_businessWorkHours(\n\t\tMTP_flags(0),\n\t\tMTP_string(data.timezoneId),\n\t\tMTP_vector_from_range(list | ranges::views::transform(proj)));\n}\n\n[[nodiscard]] MTPBusinessAwayMessageSchedule ToMTP(\n\t\tconst AwaySchedule &data) {\n\tExpects(data.type != AwayScheduleType::Never);\n\n\treturn (data.type == AwayScheduleType::Always)\n\t\t? MTP_businessAwayMessageScheduleAlways()\n\t\t: (data.type == AwayScheduleType::OutsideWorkingHours)\n\t\t? MTP_businessAwayMessageScheduleOutsideWorkHours()\n\t\t: MTP_businessAwayMessageScheduleCustom(\n\t\t\tMTP_int(data.customInterval.start),\n\t\t\tMTP_int(data.customInterval.end));\n}\n\n[[nodiscard]] MTPInputBusinessAwayMessage ToMTP(const AwaySettings &data) {\n\tusing Flag = MTPDinputBusinessAwayMessage::Flag;\n\treturn MTP_inputBusinessAwayMessage(\n\t\tMTP_flags(data.offlineOnly ? Flag::f_offline_only : Flag()),\n\t\tMTP_int(data.shortcutId),\n\t\tToMTP(data.schedule),\n\t\tForMessagesToMTP(data.recipients));\n}\n\n[[nodiscard]] MTPInputBusinessGreetingMessage ToMTP(\n\t\tconst GreetingSettings &data) {\n\treturn MTP_inputBusinessGreetingMessage(\n\t\tMTP_int(data.shortcutId),\n\t\tForMessagesToMTP(data.recipients),\n\t\tMTP_int(data.noActivityDays));\n}\n\n} // namespace\n\nBusinessInfo::BusinessInfo(not_null<Session*> owner)\n: _owner(owner) {\n}\n\nBusinessInfo::~BusinessInfo() = default;\n\nvoid BusinessInfo::saveWorkingHours(\n\t\tWorkingHours data,\n\t\tFn<void(QString)> fail) {\n\tconst auto session = &_owner->session();\n\tauto details = session->user()->businessDetails();\n\tconst auto &was = details.hours;\n\tif (was == data) {\n\t\treturn;\n\t}\n\n\tusing Flag = MTPaccount_UpdateBusinessWorkHours::Flag;\n\tsession->api().request(MTPaccount_UpdateBusinessWorkHours(\n\t\tMTP_flags(data ? Flag::f_business_work_hours : Flag()),\n\t\tToMTP(data)\n\t)).fail([=](const MTP::Error &error) {\n\t\tauto details = session->user()->businessDetails();\n\t\tdetails.hours = was;\n\t\tsession->user()->setBusinessDetails(std::move(details));\n\t\tif (fail) {\n\t\t\tfail(error.type());\n\t\t}\n\t}).send();\n\n\tdetails.hours = std::move(data);\n\tsession->user()->setBusinessDetails(std::move(details));\n}\n\nvoid BusinessInfo::saveChatIntro(ChatIntro data, Fn<void(QString)> fail) {\n\tconst auto session = &_owner->session();\n\tauto details = session->user()->businessDetails();\n\tconst auto &was = details.intro;\n\tif (was == data) {\n\t\treturn;\n\t} else {\n\t\tconst auto session = &_owner->session();\n\t\tusing Flag = MTPaccount_UpdateBusinessIntro::Flag;\n\t\tsession->api().request(MTPaccount_UpdateBusinessIntro(\n\t\t\tMTP_flags(data ? Flag::f_intro : Flag()),\n\t\t\tMTP_inputBusinessIntro(\n\t\t\t\tMTP_flags(data.sticker\n\t\t\t\t\t? MTPDinputBusinessIntro::Flag::f_sticker\n\t\t\t\t\t: MTPDinputBusinessIntro::Flag()),\n\t\t\t\tMTP_string(data.title),\n\t\t\t\tMTP_string(data.description),\n\t\t\t\t(data.sticker\n\t\t\t\t\t? data.sticker->mtpInput()\n\t\t\t\t\t: MTP_inputDocumentEmpty()))\n\t\t)).fail([=](const MTP::Error &error) {\n\t\t\tauto details = session->user()->businessDetails();\n\t\t\tdetails.intro = was;\n\t\t\tsession->user()->setBusinessDetails(std::move(details));\n\t\t\tif (fail) {\n\t\t\t\tfail(error.type());\n\t\t\t}\n\t\t}).send();\n\t}\n\n\tdetails.intro = std::move(data);\n\tsession->user()->setBusinessDetails(std::move(details));\n}\n\nvoid BusinessInfo::saveLocation(\n\t\tBusinessLocation data,\n\t\tFn<void(QString)> fail) {\n\tconst auto session = &_owner->session();\n\tauto details = session->user()->businessDetails();\n\tconst auto &was = details.location;\n\tif (was == data) {\n\t\treturn;\n\t} else {\n\t\tconst auto session = &_owner->session();\n\t\tusing Flag = MTPaccount_UpdateBusinessLocation::Flag;\n\t\tsession->api().request(MTPaccount_UpdateBusinessLocation(\n\t\t\tMTP_flags((data.point ? Flag::f_geo_point : Flag())\n\t\t\t\t| (data.address.isEmpty() ? Flag() : Flag::f_address)),\n\t\t\t(data.point\n\t\t\t\t? MTP_inputGeoPoint(\n\t\t\t\t\tMTP_flags(0),\n\t\t\t\t\tMTP_double(data.point->lat()),\n\t\t\t\t\tMTP_double(data.point->lon()),\n\t\t\t\t\tMTPint()) // accuracy_radius\n\t\t\t\t: MTP_inputGeoPointEmpty()),\n\t\t\tMTP_string(data.address)\n\t\t)).fail([=](const MTP::Error &error) {\n\t\t\tauto details = session->user()->businessDetails();\n\t\t\tdetails.location = was;\n\t\t\tsession->user()->setBusinessDetails(std::move(details));\n\t\t\tif (fail) {\n\t\t\t\tfail(error.type());\n\t\t\t}\n\t\t}).send();\n\t}\n\n\tdetails.location = std::move(data);\n\tsession->user()->setBusinessDetails(std::move(details));\n}\n\nvoid BusinessInfo::applyAwaySettings(AwaySettings data) {\n\tif (_awaySettings == data) {\n\t\treturn;\n\t}\n\t_awaySettings = data;\n\t_awaySettingsChanged.fire({});\n}\n\nvoid BusinessInfo::saveAwaySettings(\n\t\tAwaySettings data,\n\t\tFn<void(QString)> fail) {\n\tconst auto &was = _awaySettings;\n\tif (was == data) {\n\t\treturn;\n\t} else if (!data || data.shortcutId) {\n\t\tusing Flag = MTPaccount_UpdateBusinessAwayMessage::Flag;\n\t\tconst auto session = &_owner->session();\n\t\tsession->api().request(MTPaccount_UpdateBusinessAwayMessage(\n\t\t\tMTP_flags(data ? Flag::f_message : Flag()),\n\t\t\tdata ? ToMTP(data) : MTPInputBusinessAwayMessage()\n\t\t)).fail([=](const MTP::Error &error) {\n\t\t\t_awaySettings = was;\n\t\t\t_awaySettingsChanged.fire({});\n\t\t\tif (fail) {\n\t\t\t\tfail(error.type());\n\t\t\t}\n\t\t}).send();\n\t}\n\t_awaySettings = std::move(data);\n\t_awaySettingsChanged.fire({});\n}\n\nbool BusinessInfo::awaySettingsLoaded() const {\n\treturn _awaySettings.has_value();\n}\n\nAwaySettings BusinessInfo::awaySettings() const {\n\treturn _awaySettings.value_or(AwaySettings());\n}\n\nrpl::producer<> BusinessInfo::awaySettingsChanged() const {\n\treturn _awaySettingsChanged.events();\n}\n\nvoid BusinessInfo::applyGreetingSettings(GreetingSettings data) {\n\tif (_greetingSettings == data) {\n\t\treturn;\n\t}\n\t_greetingSettings = data;\n\t_greetingSettingsChanged.fire({});\n}\n\nvoid BusinessInfo::saveGreetingSettings(\n\t\tGreetingSettings data,\n\t\tFn<void(QString)> fail) {\n\tconst auto &was = _greetingSettings;\n\tif (was == data) {\n\t\treturn;\n\t} else if (!data || data.shortcutId) {\n\t\tusing Flag = MTPaccount_UpdateBusinessGreetingMessage::Flag;\n\t\t_owner->session().api().request(\n\t\t\tMTPaccount_UpdateBusinessGreetingMessage(\n\t\t\t\tMTP_flags(data ? Flag::f_message : Flag()),\n\t\t\t\tdata ? ToMTP(data) : MTPInputBusinessGreetingMessage())\n\t\t).fail([=](const MTP::Error &error) {\n\t\t\t_greetingSettings = was;\n\t\t\t_greetingSettingsChanged.fire({});\n\t\t\tif (fail) {\n\t\t\t\tfail(error.type());\n\t\t\t}\n\t\t}).send();\n\t}\n\t_greetingSettings = std::move(data);\n\t_greetingSettingsChanged.fire({});\n}\n\nbool BusinessInfo::greetingSettingsLoaded() const {\n\treturn _greetingSettings.has_value();\n}\n\nGreetingSettings BusinessInfo::greetingSettings() const {\n\treturn _greetingSettings.value_or(GreetingSettings());\n}\n\nrpl::producer<> BusinessInfo::greetingSettingsChanged() const {\n\treturn _greetingSettingsChanged.events();\n}\n\nvoid BusinessInfo::preload() {\n\tpreloadTimezones();\n}\n\nvoid BusinessInfo::preloadTimezones() {\n\tif (!_timezones.current().list.empty() || _timezonesRequestId) {\n\t\treturn;\n\t}\n\t_timezonesRequestId = _owner->session().api().request(\n\t\tMTPhelp_GetTimezonesList(MTP_int(_timezonesHash))\n\t).done([=](const MTPhelp_TimezonesList &result) {\n\t\tresult.match([&](const MTPDhelp_timezonesList &data) {\n\t\t\t_timezonesHash = data.vhash().v;\n\t\t\tconst auto proj = [](const MTPtimezone &result) {\n\t\t\t\treturn Timezone{\n\t\t\t\t\t.id = qs(result.data().vid()),\n\t\t\t\t\t.name = qs(result.data().vname()),\n\t\t\t\t\t.utcOffset = result.data().vutc_offset().v,\n\t\t\t\t};\n\t\t\t};\n\t\t\t_timezones = Timezones{\n\t\t\t\t.list = ranges::views::all(\n\t\t\t\t\tdata.vtimezones().v\n\t\t\t\t) | ranges::views::transform(\n\t\t\t\t\tproj\n\t\t\t\t) | ranges::to_vector,\n\t\t\t};\n\t\t}, [](const MTPDhelp_timezonesListNotModified &) {\n\t\t});\n\t}).send();\n}\n\nrpl::producer<Timezones> BusinessInfo::timezonesValue() const {\n\tconst_cast<BusinessInfo*>(this)->preloadTimezones();\n\treturn _timezones.value();\n}\n\nbool BusinessInfo::timezonesLoaded() const {\n\treturn !_timezones.current().list.empty();\n}\n\nQString FindClosestTimezoneId(const std::vector<Timezone> &list) {\n\tconst auto local = QDateTime::currentDateTime();\n\tconst auto utc = QDateTime(local.date(), local.time(), Qt::UTC);\n\tconst auto shift = base::unixtime::now() - (TimeId)::time(nullptr);\n\tconst auto delta = int(utc.toSecsSinceEpoch())\n\t\t- int(local.toSecsSinceEpoch())\n\t\t- shift;\n\tconst auto proj = [&](const Timezone &value) {\n\t\tauto distance = value.utcOffset - delta;\n\t\twhile (distance > 12 * 3600) {\n\t\t\tdistance -= 24 * 3600;\n\t\t}\n\t\twhile (distance < -12 * 3600) {\n\t\t\tdistance += 24 * 3600;\n\t\t}\n\t\treturn std::abs(distance);\n\t};\n\treturn ranges::min_element(list, ranges::less(), proj)->id;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/business/data_business_info.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/business/data_business_common.h\"\n\nnamespace Data {\n\nclass Session;\n\nclass BusinessInfo final {\npublic:\n\texplicit BusinessInfo(not_null<Session*> owner);\n\t~BusinessInfo();\n\n\tvoid preload();\n\n\tvoid saveWorkingHours(WorkingHours data, Fn<void(QString)> fail);\n\tvoid saveChatIntro(ChatIntro data, Fn<void(QString)> fail);\n\tvoid saveLocation(BusinessLocation data, Fn<void(QString)> fail);\n\n\tvoid saveAwaySettings(AwaySettings data, Fn<void(QString)> fail);\n\tvoid applyAwaySettings(AwaySettings data);\n\t[[nodiscard]] AwaySettings awaySettings() const;\n\t[[nodiscard]] bool awaySettingsLoaded() const;\n\t[[nodiscard]] rpl::producer<> awaySettingsChanged() const;\n\n\tvoid saveGreetingSettings(\n\t\tGreetingSettings data,\n\t\tFn<void(QString)> fail);\n\tvoid applyGreetingSettings(GreetingSettings data);\n\t[[nodiscard]] GreetingSettings greetingSettings() const;\n\t[[nodiscard]] bool greetingSettingsLoaded() const;\n\t[[nodiscard]] rpl::producer<> greetingSettingsChanged() const;\n\n\tvoid preloadTimezones();\n\t[[nodiscard]] bool timezonesLoaded() const;\n\t[[nodiscard]] rpl::producer<Timezones> timezonesValue() const;\n\nprivate:\n\tconst not_null<Session*> _owner;\n\n\trpl::variable<Timezones> _timezones;\n\n\tstd::optional<AwaySettings> _awaySettings;\n\trpl::event_stream<> _awaySettingsChanged;\n\n\tstd::optional<GreetingSettings> _greetingSettings;\n\trpl::event_stream<> _greetingSettingsChanged;\n\n\tmtpRequestId _timezonesRequestId = 0;\n\tint32 _timezonesHash = 0;\n\n};\n\n[[nodiscard]] QString FindClosestTimezoneId(\n\tconst std::vector<Timezone> &list);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/business/data_shortcut_messages.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/business/data_shortcut_messages.h\"\n\n#include \"api/api_hash.h\"\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"api/api_text_entities.h\"\n#include \"main/main_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"apiwrap.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kRequestTimeLimit = 60 * crl::time(1000);\n\n[[nodiscard]] MsgId RemoteToLocalMsgId(MsgId id) {\n\tExpects(IsServerMsgId(id));\n\n\treturn ScheduledMaxMsgId + id + 1;\n}\n\n[[nodiscard]] MsgId LocalToRemoteMsgId(MsgId id) {\n\tExpects(IsShortcutMsgId(id));\n\n\treturn (id - ScheduledMaxMsgId - 1);\n}\n\n[[nodiscard]] bool TooEarlyForRequest(crl::time received) {\n\treturn (received > 0) && (received + kRequestTimeLimit > crl::now());\n}\n\n[[nodiscard]] MTPMessage PrepareMessage(\n\t\tBusinessShortcutId shortcutId,\n\t\tconst MTPMessage &message) {\n\treturn message.match([&](const MTPDmessageEmpty &data) {\n\t\treturn MTP_messageEmpty(\n\t\t\tdata.vflags(),\n\t\t\tdata.vid(),\n\t\t\tdata.vpeer_id() ? *data.vpeer_id() : MTPPeer());\n\t}, [&](const MTPDmessageService &data) {\n\t\treturn MTP_messageService(\n\t\t\tdata.vflags(),\n\t\t\tdata.vid(),\n\t\t\tdata.vfrom_id() ? *data.vfrom_id() : MTPPeer(),\n\t\t\tdata.vpeer_id(),\n\t\t\tdata.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(),\n\t\t\tdata.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(),\n\t\t\tdata.vdate(),\n\t\t\tdata.vaction(),\n\t\t\tdata.vreactions() ? *data.vreactions() : MTPMessageReactions(),\n\t\t\tMTP_int(data.vttl_period().value_or_empty()));\n\t}, [&](const MTPDmessage &data) {\n\t\treturn MTP_message(\n\t\t\tMTP_flags(data.vflags().v\n\t\t\t\t| MTPDmessage::Flag::f_quick_reply_shortcut_id),\n\t\t\tdata.vid(),\n\t\t\tdata.vfrom_id() ? *data.vfrom_id() : MTPPeer(),\n\t\t\tMTPint(), // from_boosts_applied\n\t\t\tMTPstring(), // from_rank\n\t\t\tdata.vpeer_id(),\n\t\t\tdata.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(),\n\t\t\tdata.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(),\n\t\t\tMTP_long(data.vvia_bot_id().value_or_empty()),\n\t\t\tMTP_long(data.vvia_business_bot_id().value_or_empty()),\n\t\t\tdata.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(),\n\t\t\tdata.vdate(),\n\t\t\tdata.vmessage(),\n\t\t\tdata.vmedia() ? *data.vmedia() : MTPMessageMedia(),\n\t\t\tdata.vreply_markup() ? *data.vreply_markup() : MTPReplyMarkup(),\n\t\t\t(data.ventities()\n\t\t\t\t? *data.ventities()\n\t\t\t\t: MTPVector<MTPMessageEntity>()),\n\t\t\tMTP_int(data.vviews().value_or_empty()),\n\t\t\tMTP_int(data.vforwards().value_or_empty()),\n\t\t\tdata.vreplies() ? *data.vreplies() : MTPMessageReplies(),\n\t\t\tMTP_int(data.vedit_date().value_or_empty()),\n\t\t\tMTP_bytes(data.vpost_author().value_or_empty()),\n\t\t\tMTP_long(data.vgrouped_id().value_or_empty()),\n\t\t\tMTPMessageReactions(),\n\t\t\tMTPVector<MTPRestrictionReason>(),\n\t\t\tMTP_int(data.vttl_period().value_or_empty()),\n\t\t\tMTP_int(shortcutId),\n\t\t\tMTP_long(data.veffect().value_or_empty()),\n\t\t\t(data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck()),\n\t\t\tMTP_int(data.vreport_delivery_until_date().value_or_empty()),\n\t\t\tMTP_long(data.vpaid_message_stars().value_or_empty()),\n\t\t\t(data.vsuggested_post()\n\t\t\t\t? *data.vsuggested_post()\n\t\t\t\t: MTPSuggestedPost()),\n\t\t\tMTP_int(data.vschedule_repeat_period().value_or_empty()),\n\t\t\tMTP_string(qs(data.vsummary_from_language().value_or_empty())));\n\t});\n}\n\n} // namespace\n\nbool IsShortcutMsgId(MsgId id) {\n\treturn (id > ScheduledMaxMsgId) && (id < ShortcutMaxMsgId);\n}\n\nShortcutMessages::ShortcutMessages(not_null<Session*> owner)\n: _session(&owner->session())\n, _history(owner->history(_session->userPeerId()))\n, _clearTimer([=] { clearOldRequests(); }) {\n\towner->itemRemoved(\n\t) | rpl::filter([](not_null<const HistoryItem*> item) {\n\t\treturn item->isBusinessShortcut();\n\t}) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\tremove(item);\n\t}, _lifetime);\n}\n\nShortcutMessages::~ShortcutMessages() {\n\tfor (const auto &request : _requests) {\n\t\t_session->api().request(request.second.requestId).cancel();\n\t}\n}\n\nvoid ShortcutMessages::clearOldRequests() {\n\tconst auto now = crl::now();\n\twhile (true) {\n\t\tconst auto i = ranges::find_if(_requests, [&](const auto &value) {\n\t\t\tconst auto &request = value.second;\n\t\t\treturn !request.requestId\n\t\t\t\t&& (request.lastReceived + kRequestTimeLimit <= now);\n\t\t});\n\t\tif (i == end(_requests)) {\n\t\t\tbreak;\n\t\t}\n\t\t_requests.erase(i);\n\t}\n}\n\nvoid ShortcutMessages::updateShortcuts(const QVector<MTPQuickReply> &list) {\n\tauto shortcuts = parseShortcuts(list);\n\tauto changes = std::vector<ShortcutIdChange>();\n\tfor (auto &[id, shortcut] : _shortcuts.list) {\n\t\tif (shortcuts.list.contains(id)) {\n\t\t\tcontinue;\n\t\t}\n\t\tauto foundId = BusinessShortcutId();\n\t\tfor (auto &[realId, real] : shortcuts.list) {\n\t\t\tif (real.name == shortcut.name) {\n\t\t\t\tfoundId = realId;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (foundId) {\n\t\t\tmergeMessagesFromTo(id, foundId);\n\t\t\tchanges.push_back({ .oldId = id, .newId = foundId });\n\t\t} else {\n\t\t\tshortcuts.list.emplace(id, shortcut);\n\t\t}\n\t}\n\tconst auto changed = !_shortcutsLoaded\n\t\t|| (shortcuts != _shortcuts);\n\tif (changed) {\n\t\t_shortcuts = std::move(shortcuts);\n\t\t_shortcutsLoaded = true;\n\t\tfor (const auto &change : changes) {\n\t\t\t_shortcutIdChanges.fire_copy(change);\n\t\t}\n\t\t_shortcutsChanged.fire({});\n\t} else {\n\t\tAssert(changes.empty());\n\t}\n}\n\nvoid ShortcutMessages::mergeMessagesFromTo(\n\t\tBusinessShortcutId fromId,\n\t\tBusinessShortcutId toId) {\n\tauto &to = _data[toId];\n\tconst auto i = _data.find(fromId);\n\tif (i == end(_data)) {\n\t\treturn;\n\t}\n\n\tauto &from = i->second;\n\tauto destroy = base::flat_set<not_null<HistoryItem*>>();\n\tfor (auto &item : from.items) {\n\t\tif (item->isSending() || item->hasFailed()) {\n\t\t\titem->setRealShortcutId(toId);\n\t\t\tto.items.push_back(std::move(item));\n\t\t} else {\n\t\t\tdestroy.emplace(item.get());\n\t\t}\n\t}\n\tfor (const auto &item : destroy) {\n\t\titem->destroy();\n\t}\n\t_data.remove(fromId);\n\n\tcancelRequest(fromId);\n\n\t_updates.fire_copy(toId);\n\tif (!destroy.empty()) {\n\t\tcancelRequest(toId);\n\t\trequest(toId);\n\t}\n}\n\nShortcuts ShortcutMessages::parseShortcuts(\n\t\tconst QVector<MTPQuickReply> &list) const {\n\tauto result = Shortcuts();\n\tfor (const auto &reply : list) {\n\t\tconst auto shortcut = parseShortcut(reply);\n\t\tresult.list.emplace(shortcut.id, shortcut);\n\t}\n\treturn result;\n}\n\nShortcut ShortcutMessages::parseShortcut(const MTPQuickReply &reply) const {\n\tconst auto &data = reply.data();\n\treturn Shortcut{\n\t\t.id = BusinessShortcutId(data.vshortcut_id().v),\n\t\t.count = data.vcount().v,\n\t\t.name = qs(data.vshortcut()),\n\t\t.topMessageId = localMessageId(data.vtop_message().v),\n\t};\n}\n\nMsgId ShortcutMessages::localMessageId(MsgId remoteId) const {\n\treturn RemoteToLocalMsgId(remoteId);\n}\n\nMsgId ShortcutMessages::lookupId(not_null<const HistoryItem*> item) const {\n\tExpects(item->isBusinessShortcut());\n\tExpects(!item->isSending());\n\tExpects(!item->hasFailed());\n\n\treturn LocalToRemoteMsgId(item->id);\n}\n\nint ShortcutMessages::count(BusinessShortcutId shortcutId) const {\n\tconst auto i = _data.find(shortcutId);\n\treturn (i != end(_data)) ? i->second.items.size() : 0;\n}\n\nvoid ShortcutMessages::apply(const MTPDupdateQuickReplies &update) {\n\tupdateShortcuts(update.vquick_replies().v);\n\tscheduleShortcutsReload();\n}\n\nvoid ShortcutMessages::scheduleShortcutsReload() {\n\tconst auto hasUnknownMessages = [&] {\n\t\tconst auto selfId = _session->userPeerId();\n\t\tfor (const auto &[id, shortcut] : _shortcuts.list) {\n\t\t\tif (!_session->data().message({ selfId, shortcut.topMessageId })) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\tif (hasUnknownMessages()) {\n\t\t_shortcutsLoaded = false;\n\t\tconst auto cancelledId = base::take(_shortcutsRequestId);\n\t\t_session->api().request(cancelledId).cancel();\n\t\tcrl::on_main(_session, [=] {\n\t\t\tif (cancelledId || hasUnknownMessages()) {\n\t\t\t\tpreloadShortcuts();\n\t\t\t}\n\t\t});\n\t}\n}\n\nvoid ShortcutMessages::apply(const MTPDupdateNewQuickReply &update) {\n\tconst auto &reply = update.vquick_reply();\n\tauto foundId = BusinessShortcutId();\n\tconst auto shortcut = parseShortcut(reply);\n\tfor (auto &[id, existing] : _shortcuts.list) {\n\t\tif (id == shortcut.id) {\n\t\t\tfoundId = id;\n\t\t\tbreak;\n\t\t} else if (existing.name == shortcut.name) {\n\t\t\tfoundId = id;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (foundId == shortcut.id) {\n\t\tauto &already = _shortcuts.list[shortcut.id];\n\t\tif (already != shortcut) {\n\t\t\talready = shortcut;\n\t\t\t_shortcutsChanged.fire({});\n\t\t}\n\t\treturn;\n\t} else if (foundId) {\n\t\t_shortcuts.list.emplace(shortcut.id, shortcut);\n\t\tmergeMessagesFromTo(foundId, shortcut.id);\n\t\t_shortcuts.list.remove(foundId);\n\t\t_shortcutIdChanges.fire({ foundId, shortcut.id });\n\t\t_shortcutsChanged.fire({});\n\t}\n}\n\nvoid ShortcutMessages::apply(const MTPDupdateQuickReplyMessage &update) {\n\tconst auto &message = update.vmessage();\n\tconst auto shortcutId = BusinessShortcutIdFromMessage(message);\n\tif (!shortcutId) {\n\t\treturn;\n\t}\n\tconst auto loaded = _data.contains(shortcutId);\n\tauto &list = _data[shortcutId];\n\tappend(shortcutId, list, message);\n\tsort(list);\n\t_updates.fire_copy(shortcutId);\n\tupdateCount(shortcutId);\n\tif (!loaded) {\n\t\trequest(shortcutId);\n\t}\n}\n\nvoid ShortcutMessages::updateCount(BusinessShortcutId shortcutId) {\n\tconst auto i = _data.find(shortcutId);\n\tconst auto j = _shortcuts.list.find(shortcutId);\n\tif (j == end(_shortcuts.list)) {\n\t\treturn;\n\t}\n\tconst auto count = (i != end(_data))\n\t\t? int(i->second.itemById.size())\n\t\t: 0;\n\tif (j->second.count != count) {\n\t\t_shortcuts.list[shortcutId].count = count;\n\t\t_shortcutsChanged.fire({});\n\t}\n}\n\nvoid ShortcutMessages::apply(\n\t\tconst MTPDupdateDeleteQuickReplyMessages &update) {\n\tconst auto shortcutId = update.vshortcut_id().v;\n\tif (!shortcutId) {\n\t\treturn;\n\t}\n\tauto i = _data.find(shortcutId);\n\tif (i == end(_data)) {\n\t\treturn;\n\t}\n\tfor (const auto &id : update.vmessages().v) {\n\t\tconst auto &list = i->second;\n\t\tconst auto j = list.itemById.find(id.v);\n\t\tif (j != end(list.itemById)) {\n\t\t\tj->second->destroy();\n\t\t\ti = _data.find(shortcutId);\n\t\t\tif (i == end(_data)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\t_updates.fire_copy(shortcutId);\n\tupdateCount(shortcutId);\n\n\tcancelRequest(shortcutId);\n\trequest(shortcutId);\n}\n\nvoid ShortcutMessages::apply(const MTPDupdateDeleteQuickReply &update) {\n\tconst auto shortcutId = update.vshortcut_id().v;\n\tif (!shortcutId) {\n\t\treturn;\n\t}\n\tauto i = _data.find(shortcutId);\n\twhile (i != end(_data) && !i->second.itemById.empty()) {\n\t\ti->second.itemById.back().second->destroy();\n\t\ti = _data.find(shortcutId);\n\t}\n\t_updates.fire_copy(shortcutId);\n\tif (_data.contains(shortcutId)) {\n\t\tupdateCount(shortcutId);\n\t} else {\n\t\t_shortcuts.list.remove(shortcutId);\n\t\t_shortcutIdChanges.fire({ shortcutId, 0 });\n\t}\n}\n\nvoid ShortcutMessages::apply(\n\t\tconst MTPDupdateMessageID &update,\n\t\tnot_null<HistoryItem*> local) {\n\tconst auto id = update.vid().v;\n\tconst auto i = _data.find(local->shortcutId());\n\tAssert(i != end(_data));\n\tauto &list = i->second;\n\tconst auto j = list.itemById.find(id);\n\tif (j != end(list.itemById) || !IsServerMsgId(id)) {\n\t\tlocal->destroy();\n\t} else {\n\t\tAssert(!list.itemById.contains(local->id));\n\t\tlocal->setRealId(localMessageId(id));\n\t\tlist.itemById.emplace(id, local);\n\t}\n}\n\nvoid ShortcutMessages::appendSending(not_null<HistoryItem*> item) {\n\tExpects(item->isSending());\n\tExpects(item->isBusinessShortcut());\n\n\tconst auto shortcutId = item->shortcutId();\n\tauto &list = _data[shortcutId];\n\tlist.items.emplace_back(item);\n\tsort(list);\n\t_updates.fire_copy(shortcutId);\n}\n\nvoid ShortcutMessages::removeSending(not_null<HistoryItem*> item) {\n\tExpects(item->isSending() || item->hasFailed());\n\tExpects(item->isBusinessShortcut());\n\n\titem->destroy();\n}\n\nrpl::producer<> ShortcutMessages::updates(BusinessShortcutId shortcutId) {\n\trequest(shortcutId);\n\n\treturn _updates.events(\n\t) | rpl::filter([=](BusinessShortcutId value) {\n\t\treturn (value == shortcutId);\n\t}) | rpl::to_empty;\n}\n\nData::MessagesSlice ShortcutMessages::list(BusinessShortcutId shortcutId) {\n\tauto result = Data::MessagesSlice();\n\tconst auto i = _data.find(shortcutId);\n\tif (i == end(_data)) {\n\t\tconst auto i = _requests.find(shortcutId);\n\t\tif (i == end(_requests)) {\n\t\t\treturn result;\n\t\t}\n\t\tresult.fullCount = result.skippedAfter = result.skippedBefore = 0;\n\t\treturn result;\n\t}\n\tconst auto &list = i->second.items;\n\tresult.skippedAfter = result.skippedBefore = 0;\n\tresult.fullCount = int(list.size());\n\tresult.ids = ranges::views::all(\n\t\tlist\n\t) | ranges::views::transform(\n\t\t&HistoryItem::fullId\n\t) | ranges::to_vector;\n\treturn result;\n}\n\nvoid ShortcutMessages::preloadShortcuts() {\n\tif (_shortcutsLoaded || _shortcutsRequestId) {\n\t\treturn;\n\t}\n\tconst auto owner = &_session->data();\n\t_shortcutsRequestId = owner->session().api().request(\n\t\tMTPmessages_GetQuickReplies(MTP_long(_shortcutsHash))\n\t).done([=](const MTPmessages_QuickReplies &result) {\n\t\tresult.match([&](const MTPDmessages_quickReplies &data) {\n\t\t\towner->processUsers(data.vusers());\n\t\t\towner->processChats(data.vchats());\n\t\t\tupdateShortcuts(data.vquick_replies().v);\n\t\t}, [&](const MTPDmessages_quickRepliesNotModified &) {\n\t\t\tif (!_shortcutsLoaded) {\n\t\t\t\t_shortcutsLoaded = true;\n\t\t\t\t_shortcutsChanged.fire({});\n\t\t\t}\n\t\t});\n\t}).send();\n}\n\nconst Shortcuts &ShortcutMessages::shortcuts() const {\n\treturn _shortcuts;\n}\n\nbool ShortcutMessages::shortcutsLoaded() const {\n\treturn _shortcutsLoaded;\n}\n\nrpl::producer<> ShortcutMessages::shortcutsChanged() const {\n\treturn _shortcutsChanged.events();\n}\n\nauto ShortcutMessages::shortcutIdChanged() const\n-> rpl::producer<ShortcutIdChange> {\n\treturn _shortcutIdChanges.events();\n}\n\nBusinessShortcutId ShortcutMessages::emplaceShortcut(QString name) {\n\tExpects(_shortcutsLoaded);\n\n\tfor (auto &[id, shortcut] : _shortcuts.list) {\n\t\tif (shortcut.name == name) {\n\t\t\treturn id;\n\t\t}\n\t}\n\tconst auto result = --_localShortcutId;\n\t_shortcuts.list.emplace(result, Shortcut{ .id = result, .name = name });\n\treturn result;\n}\n\nShortcut ShortcutMessages::lookupShortcut(BusinessShortcutId id) const {\n\tconst auto i = _shortcuts.list.find(id);\n\n\tEnsures(i != end(_shortcuts.list));\n\treturn i->second;\n}\n\nBusinessShortcutId ShortcutMessages::lookupShortcutId(\n\t\tconst QString &name) const {\n\tfor (const auto &[id, shortcut] : _shortcuts.list) {\n\t\tif (!shortcut.name.compare(name, Qt::CaseInsensitive)) {\n\t\t\treturn id;\n\t\t}\n\t}\n\treturn {};\n}\n\nvoid ShortcutMessages::editShortcut(\n\t\tBusinessShortcutId id,\n\t\tQString name,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail) {\n\tname = name.trimmed();\n\tif (name.isEmpty()) {\n\t\tfail(QString());\n\t\treturn;\n\t}\n\tconst auto finish = [=] {\n\t\tconst auto i = _shortcuts.list.find(id);\n\t\tif (i != end(_shortcuts.list)) {\n\t\t\ti->second.name = name;\n\t\t\t_shortcutsChanged.fire({});\n\t\t}\n\t\tdone();\n\t};\n\tfor (const auto &[existingId, shortcut] : _shortcuts.list) {\n\t\tif (shortcut.name == name) {\n\t\t\tif (existingId == id) {\n\t\t\t\t//done();\n\t\t\t\t//return;\n\t\t\t\tbreak;\n\t\t\t} else if (_data[existingId].items.empty() && !shortcut.count) {\n\t\t\t\tremoveShortcut(existingId);\n\t\t\t\tbreak;\n\t\t\t} else {\n\t\t\t\tfail(u\"SHORTCUT_OCCUPIED\"_q);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\t_session->api().request(MTPmessages_EditQuickReplyShortcut(\n\t\tMTP_int(id),\n\t\tMTP_string(name)\n\t)).done(finish).fail([=](const MTP::Error &error) {\n\t\tconst auto type = error.type();\n\t\tif (type == u\"SHORTCUT_ID_INVALID\"_q) {\n\t\t\t// Not on the server (yet).\n\t\t\tfinish();\n\t\t} else {\n\t\t\tfail(type);\n\t\t}\n\t}).send();\n}\n\nvoid ShortcutMessages::removeShortcut(BusinessShortcutId shortcutId) {\n\tauto i = _data.find(shortcutId);\n\twhile (i != end(_data)) {\n\t\tif (i->second.items.empty()) {\n\t\t\t_data.erase(i);\n\t\t} else {\n\t\t\ti->second.items.front()->destroy();\n\t\t}\n\t\ti = _data.find(shortcutId);\n\t}\n\t_shortcuts.list.remove(shortcutId);\n\t_shortcutIdChanges.fire({ shortcutId, 0 });\n\n\t_session->api().request(MTPmessages_DeleteQuickReplyShortcut(\n\t\tMTP_int(shortcutId)\n\t)).send();\n}\n\nvoid ShortcutMessages::cancelRequest(BusinessShortcutId shortcutId) {\n\tconst auto j = _requests.find(shortcutId);\n\tif (j != end(_requests)) {\n\t\t_session->api().request(j->second.requestId).cancel();\n\t\t_requests.erase(j);\n\t}\n}\n\nvoid ShortcutMessages::request(BusinessShortcutId shortcutId) {\n\tauto &request = _requests[shortcutId];\n\tif (request.requestId || TooEarlyForRequest(request.lastReceived)) {\n\t\treturn;\n\t}\n\tconst auto i = _data.find(shortcutId);\n\tconst auto hash = (i != end(_data))\n\t\t? countListHash(i->second)\n\t\t: uint64(0);\n\trequest.requestId = _session->api().request(\n\t\tMTPmessages_GetQuickReplyMessages(\n\t\t\tMTP_flags(0),\n\t\t\tMTP_int(shortcutId),\n\t\t\tMTPVector<MTPint>(),\n\t\t\tMTP_long(hash))\n\t).done([=](const MTPmessages_Messages &result) {\n\t\tparse(shortcutId, result);\n\t}).fail([=] {\n\t\t_requests.remove(shortcutId);\n\t}).send();\n}\n\nvoid ShortcutMessages::parse(\n\t\tBusinessShortcutId shortcutId,\n\t\tconst MTPmessages_Messages &list) {\n\tauto &request = _requests[shortcutId];\n\trequest.lastReceived = crl::now();\n\trequest.requestId = 0;\n\tif (!_clearTimer.isActive()) {\n\t\t_clearTimer.callOnce(kRequestTimeLimit * 2);\n\t}\n\n\tlist.match([&](const MTPDmessages_messagesNotModified &data) {\n\t}, [&](const auto &data) {\n\t\t_session->data().processUsers(data.vusers());\n\t\t_session->data().processChats(data.vchats());\n\n\t\tconst auto &messages = data.vmessages().v;\n\t\tif (messages.isEmpty()) {\n\t\t\tclearNotSending(shortcutId);\n\t\t\treturn;\n\t\t}\n\t\tauto received = base::flat_set<not_null<HistoryItem*>>();\n\t\tauto clear = base::flat_set<not_null<HistoryItem*>>();\n\t\tauto &list = _data.emplace(shortcutId, List()).first->second;\n\t\tfor (const auto &message : messages) {\n\t\t\tif (const auto item = append(shortcutId, list, message)) {\n\t\t\t\treceived.emplace(item);\n\t\t\t}\n\t\t}\n\t\tfor (const auto &owned : list.items) {\n\t\t\tconst auto item = owned.get();\n\t\t\tif (!item->isSending() && !received.contains(item)) {\n\t\t\t\tclear.emplace(item);\n\t\t\t}\n\t\t}\n\t\tupdated(shortcutId, received, clear);\n\t});\n}\n\nHistoryItem *ShortcutMessages::append(\n\t\tBusinessShortcutId shortcutId,\n\t\tList &list,\n\t\tconst MTPMessage &message) {\n\tconst auto id = message.match([&](const auto &data) {\n\t\treturn data.vid().v;\n\t});\n\tconst auto i = list.itemById.find(id);\n\tif (i != end(list.itemById)) {\n\t\tconst auto existing = i->second;\n\t\tmessage.match([&](const MTPDmessage &data) {\n\t\t\tif (data.is_edit_hide()) {\n\t\t\t\texisting->applyEdition(HistoryMessageEdition(_session, data));\n\t\t\t} else {\n\t\t\t\texisting->updateSentContent({\n\t\t\t\t\tqs(data.vmessage()),\n\t\t\t\t\tApi::EntitiesFromMTP(\n\t\t\t\t\t\t_session,\n\t\t\t\t\t\tdata.ventities().value_or_empty())\n\t\t\t\t}, data.vmedia());\n\t\t\t\texisting->updateReplyMarkup(\n\t\t\t\t\tHistoryMessageMarkupData(data.vreply_markup()));\n\t\t\t\texisting->updateForwardedInfo(data.vfwd_from());\n\t\t\t}\n\t\t\texisting->updateDate(data.vdate().v);\n\t\t\t_history->owner().requestItemTextRefresh(existing);\n\t\t}, [&](const auto &data) {});\n\t\treturn existing;\n\t}\n\n\tif (!IsServerMsgId(id)) {\n\t\tLOG((\"API Error: Bad id in quick reply messages: %1.\").arg(id));\n\t\treturn nullptr;\n\t}\n\tconst auto item = _session->data().addNewMessage(\n\t\tlocalMessageId(id),\n\t\tPrepareMessage(shortcutId, message),\n\t\tMessageFlags(), // localFlags\n\t\tNewMessageType::Existing);\n\tif (!item\n\t\t|| item->history() != _history\n\t\t|| item->shortcutId() != shortcutId) {\n\t\tLOG((\"API Error: Bad data received in quick reply messages.\"));\n\t\treturn nullptr;\n\t}\n\tlist.items.emplace_back(item);\n\tlist.itemById.emplace(id, item);\n\treturn item;\n}\n\nvoid ShortcutMessages::clearNotSending(BusinessShortcutId shortcutId) {\n\tconst auto i = _data.find(shortcutId);\n\tif (i == end(_data)) {\n\t\treturn;\n\t}\n\tauto clear = base::flat_set<not_null<HistoryItem*>>();\n\tfor (const auto &owned : i->second.items) {\n\t\tif (!owned->isSending() && !owned->hasFailed()) {\n\t\t\tclear.emplace(owned.get());\n\t\t}\n\t}\n\tupdated(shortcutId, {}, clear);\n}\n\nvoid ShortcutMessages::updated(\n\t\tBusinessShortcutId shortcutId,\n\t\tconst base::flat_set<not_null<HistoryItem*>> &added,\n\t\tconst base::flat_set<not_null<HistoryItem*>> &clear) {\n\tif (!clear.empty()) {\n\t\tfor (const auto &item : clear) {\n\t\t\titem->destroy();\n\t\t}\n\t}\n\tconst auto i = _data.find(shortcutId);\n\tif (i != end(_data)) {\n\t\tsort(i->second);\n\t}\n\tif (!added.empty() || !clear.empty()) {\n\t\t_updates.fire_copy(shortcutId);\n\t}\n}\n\nvoid ShortcutMessages::sort(List &list) {\n\tranges::sort(list.items, ranges::less(), &HistoryItem::position);\n}\n\nvoid ShortcutMessages::remove(not_null<const HistoryItem*> item) {\n\tconst auto shortcutId = item->shortcutId();\n\tconst auto i = _data.find(shortcutId);\n\tAssert(i != end(_data));\n\tauto &list = i->second;\n\n\tif (!item->isSending() && !item->hasFailed()) {\n\t\tlist.itemById.remove(lookupId(item));\n\t}\n\tconst auto k = ranges::find(list.items, item, &OwnedItem::get);\n\tAssert(k != list.items.end());\n\tk->release();\n\tlist.items.erase(k);\n\n\tif (list.items.empty()) {\n\t\t_data.erase(i);\n\t}\n\t_updates.fire_copy(shortcutId);\n\tupdateCount(shortcutId);\n}\n\nuint64 ShortcutMessages::countListHash(const List &list) const {\n\tusing namespace Api;\n\n\tauto hash = HashInit();\n\tauto &&serverside = ranges::views::all(\n\t\tlist.items\n\t) | ranges::views::filter([](const OwnedItem &item) {\n\t\treturn !item->isSending() && !item->hasFailed();\n\t}) | ranges::views::reverse;\n\tfor (const auto &item : serverside) {\n\t\tHashUpdate(hash, lookupId(item.get()).bare);\n\t\tif (const auto edited = item->Get<HistoryMessageEdited>()) {\n\t\t\tHashUpdate(hash, edited->date);\n\t\t} else {\n\t\t\tHashUpdate(hash, TimeId(0));\n\t\t}\n\t}\n\treturn HashFinalize(hash);\n}\n\nMTPInputQuickReplyShortcut ShortcutIdToMTP(\n\t\tnot_null<Main::Session*> session,\n\t\tBusinessShortcutId id) {\n\treturn id\n\t\t? MTP_inputQuickReplyShortcut(MTP_string(\n\t\t\tsession->data().shortcutMessages().lookupShortcut(id).name))\n\t\t: MTPInputQuickReplyShortcut();\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/business/data_shortcut_messages.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/history_item.h\"\n#include \"base/timer.h\"\n\nclass History;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nclass Session;\nstruct MessagesSlice;\n\nstruct Shortcut {\n\tBusinessShortcutId id = 0;\n\tint count = 0;\n\tQString name;\n\tMsgId topMessageId = 0;\n\n\tfriend inline bool operator==(\n\t\tconst Shortcut &a,\n\t\tconst Shortcut &b) = default;\n};\n\nstruct ShortcutIdChange {\n\tBusinessShortcutId oldId = 0;\n\tBusinessShortcutId newId = 0;\n};\n\nstruct Shortcuts {\n\tbase::flat_map<BusinessShortcutId, Shortcut> list;\n\n\tfriend inline bool operator==(\n\t\tconst Shortcuts &a,\n\t\tconst Shortcuts &b) = default;\n};\n\n[[nodiscard]] bool IsShortcutMsgId(MsgId id);\n\nclass ShortcutMessages final {\npublic:\n\texplicit ShortcutMessages(not_null<Session*> owner);\n\t~ShortcutMessages();\n\n\t[[nodiscard]] MsgId lookupId(not_null<const HistoryItem*> item) const;\n\t[[nodiscard]] int count(BusinessShortcutId shortcutId) const;\n\t[[nodiscard]] MsgId localMessageId(MsgId remoteId) const;\n\n\tvoid apply(const MTPDupdateQuickReplies &update);\n\tvoid apply(const MTPDupdateNewQuickReply &update);\n\tvoid apply(const MTPDupdateQuickReplyMessage &update);\n\tvoid apply(const MTPDupdateDeleteQuickReplyMessages &update);\n\tvoid apply(const MTPDupdateDeleteQuickReply &update);\n\tvoid apply(\n\t\tconst MTPDupdateMessageID &update,\n\t\tnot_null<HistoryItem*> local);\n\n\tvoid appendSending(not_null<HistoryItem*> item);\n\tvoid removeSending(not_null<HistoryItem*> item);\n\n\t[[nodiscard]] rpl::producer<> updates(BusinessShortcutId shortcutId);\n\t[[nodiscard]] Data::MessagesSlice list(BusinessShortcutId shortcutId);\n\n\tvoid preloadShortcuts();\n\t[[nodiscard]] const Shortcuts &shortcuts() const;\n\t[[nodiscard]] bool shortcutsLoaded() const;\n\t[[nodiscard]] rpl::producer<> shortcutsChanged() const;\n\t[[nodiscard]] rpl::producer<ShortcutIdChange> shortcutIdChanged() const;\n\t[[nodiscard]] BusinessShortcutId emplaceShortcut(QString name);\n\t[[nodiscard]] Shortcut lookupShortcut(BusinessShortcutId id) const;\n\t[[nodiscard]] BusinessShortcutId lookupShortcutId(\n\t\tconst QString &name) const;\n\tvoid editShortcut(\n\t\tBusinessShortcutId id,\n\t\tQString name,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail);\n\tvoid removeShortcut(BusinessShortcutId shortcutId);\n\nprivate:\n\tusing OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;\n\tstruct List {\n\t\tstd::vector<OwnedItem> items;\n\t\tbase::flat_map<MsgId, not_null<HistoryItem*>> itemById;\n\t};\n\tstruct Request {\n\t\tmtpRequestId requestId = 0;\n\t\tcrl::time lastReceived = 0;\n\t};\n\n\tvoid request(BusinessShortcutId shortcutId);\n\tvoid parse(\n\t\tBusinessShortcutId shortcutId,\n\t\tconst MTPmessages_Messages &list);\n\tHistoryItem *append(\n\t\tBusinessShortcutId shortcutId,\n\t\tList &list,\n\t\tconst MTPMessage &message);\n\tvoid clearNotSending(BusinessShortcutId shortcutId);\n\tvoid updated(\n\t\tBusinessShortcutId shortcutId,\n\t\tconst base::flat_set<not_null<HistoryItem*>> &added,\n\t\tconst base::flat_set<not_null<HistoryItem*>> &clear);\n\tvoid sort(List &list);\n\tvoid remove(not_null<const HistoryItem*> item);\n\t[[nodiscard]] uint64 countListHash(const List &list) const;\n\tvoid clearOldRequests();\n\tvoid cancelRequest(BusinessShortcutId shortcutId);\n\tvoid updateCount(BusinessShortcutId shortcutId);\n\n\tvoid scheduleShortcutsReload();\n\tvoid mergeMessagesFromTo(\n\t\tBusinessShortcutId fromId,\n\t\tBusinessShortcutId toId);\n\tvoid updateShortcuts(const QVector<MTPQuickReply> &list);\n\t[[nodiscard]] Shortcut parseShortcut(const MTPQuickReply &reply) const;\n\t[[nodiscard]] Shortcuts parseShortcuts(\n\t\tconst QVector<MTPQuickReply> &list) const;\n\n\tconst not_null<Main::Session*> _session;\n\tconst not_null<History*> _history;\n\n\tbase::Timer _clearTimer;\n\tbase::flat_map<BusinessShortcutId, List> _data;\n\tbase::flat_map<BusinessShortcutId, Request> _requests;\n\trpl::event_stream<BusinessShortcutId> _updates;\n\n\tShortcuts _shortcuts;\n\trpl::event_stream<> _shortcutsChanged;\n\trpl::event_stream<ShortcutIdChange> _shortcutIdChanges;\n\tBusinessShortcutId _localShortcutId = 0;\n\tuint64 _shortcutsHash = 0;\n\tmtpRequestId _shortcutsRequestId = 0;\n\tbool _shortcutsLoaded = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] MTPInputQuickReplyShortcut ShortcutIdToMTP(\n\tnot_null<Main::Session*> session,\n\tBusinessShortcutId id);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/credits.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/components/credits.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_credits.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kReloadThreshold = 60 * crl::time(1000);\n\n} // namespace\n\nCredits::Credits(not_null<Main::Session*> session)\n: _session(session)\n, _reload([=] { load(true); }) {\n}\n\nCredits::~Credits() = default;\n\nvoid Credits::apply(const MTPDupdateStarsBalance &data) {\n\tapply(CreditsAmountFromTL(data.vbalance()));\n}\n\nrpl::producer<float64> Credits::rateValue(\n\t\tnot_null<PeerData*> ownedBotOrChannel) {\n\treturn rpl::single(_session->appConfig().starsWithdrawRate());\n}\n\nfloat64 Credits::usdRate() const {\n\treturn _session->appConfig().currencyWithdrawRate();\n}\n\nvoid Credits::load(bool force) {\n\tif (_loader\n\t\t|| (!force\n\t\t\t&& _lastLoaded\n\t\t\t&& _lastLoaded + kReloadThreshold > crl::now())) {\n\t\treturn;\n\t}\n\tconst auto self = _session->user();\n\t_loader = std::make_unique<rpl::lifetime>();\n\t_loader->make_state<Api::CreditsStatus>(self)->request({}, [=](\n\t\t\tData::CreditsStatusSlice slice) {\n\t\tconst auto balance = slice.balance;\n\t\tconst auto apiStats\n\t\t\t= _loader->make_state<Api::CreditsEarnStatistics>(self);\n\t\tconst auto finish = [=](bool statsEnabled) {\n\t\t\t_statsEnabled = statsEnabled;\n\t\t\tapply(balance);\n\t\t\t_loader = nullptr;\n\t\t};\n\t\tapiStats->request() | rpl::on_error_done([=] {\n\t\t\tfinish(false);\n\t\t}, [=] {\n\t\t\tfinish(true);\n\t\t}, *_loader);\n\t});\n}\n\nbool Credits::loaded() const {\n\treturn _lastLoaded != 0;\n}\n\nrpl::producer<bool> Credits::loadedValue() const {\n\tif (loaded()) {\n\t\treturn rpl::single(true);\n\t}\n\treturn rpl::single(\n\t\tfalse\n\t) | rpl::then(_loadedChanges.events() | rpl::map_to(true));\n}\n\nCreditsAmount Credits::balance() const {\n\treturn _nonLockedBalance.current();\n}\n\nCreditsAmount Credits::balance(PeerId peerId) const {\n\tconst auto it = _cachedPeerBalances.find(peerId);\n\treturn (it != _cachedPeerBalances.end()) ? it->second : CreditsAmount();\n}\n\nCreditsAmount Credits::balanceCurrency(PeerId peerId) const {\n\tconst auto it = _cachedPeerCurrencyBalances.find(peerId);\n\treturn (it != _cachedPeerCurrencyBalances.end())\n\t\t? it->second\n\t\t: CreditsAmount(0, 0, CreditsType::Ton);\n}\n\nrpl::producer<CreditsAmount> Credits::balanceValue() const {\n\treturn _nonLockedBalance.value();\n}\n\nvoid Credits::tonLoad(bool force) {\n\tif (_tonRequestId\n\t\t|| (!force\n\t\t\t&& _tonLastLoaded\n\t\t\t&& _tonLastLoaded + kReloadThreshold > crl::now())) {\n\t\treturn;\n\t}\n\t_tonRequestId = _session->api().request(MTPpayments_GetStarsStatus(\n\t\tMTP_flags(MTPpayments_GetStarsStatus::Flag::f_ton),\n\t\tMTP_inputPeerSelf()\n\t)).done([=](const MTPpayments_StarsStatus &result) {\n\t\t_tonRequestId = 0;\n\t\tconst auto amount = CreditsAmountFromTL(result.data().vbalance());\n\t\tif (amount.ton()) {\n\t\t\tapply(amount);\n\t\t} else if (amount.empty()) {\n\t\t\tapply(CreditsAmount(0, CreditsType::Ton));\n\t\t} else {\n\t\t\tLOG((\"API Error: Got weird balance.\"));\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\t_tonRequestId = 0;\n\t\tLOG((\"API Error: Couldn't get TON balance, error: %1\"\n\t\t\t).arg(error.type()));\n\t}).send();\n}\n\nbool Credits::tonLoaded() const {\n\treturn _tonLastLoaded != 0;\n}\n\nrpl::producer<bool> Credits::tonLoadedValue() const {\n\tif (tonLoaded()) {\n\t\treturn rpl::single(true);\n\t}\n\treturn rpl::single(\n\t\tfalse\n\t) | rpl::then(_tonLoadedChanges.events() | rpl::map_to(true));\n}\n\nCreditsAmount Credits::tonBalance() const {\n\treturn _tonBalance.current();\n}\n\nrpl::producer<CreditsAmount> Credits::tonBalanceValue() const {\n\treturn _tonBalance.value();\n}\n\nvoid Credits::updateNonLockedValue() {\n\t_nonLockedBalance = (_balance >= _locked)\n\t\t? (_balance - _locked)\n\t\t: CreditsAmount();\n}\n\nvoid Credits::lock(CreditsAmount count) {\n\tExpects(loaded());\n\tExpects(count >= CreditsAmount(0));\n\tExpects(_locked + count <= _balance);\n\n\t_locked += count;\n\n\tupdateNonLockedValue();\n}\n\nvoid Credits::unlock(CreditsAmount count) {\n\tExpects(count >= CreditsAmount(0));\n\tExpects(_locked >= count);\n\n\t_locked -= count;\n\n\tupdateNonLockedValue();\n}\n\nvoid Credits::withdrawLocked(CreditsAmount count) {\n\tExpects(count >= CreditsAmount(0));\n\tExpects(_locked >= count);\n\n\t_locked -= count;\n\tapply(_balance >= count ? (_balance - count) : CreditsAmount(0));\n\tinvalidate();\n}\n\nvoid Credits::invalidate() {\n\t_reload.call();\n}\n\nvoid Credits::apply(CreditsAmount balance) {\n\tif (balance.ton()) {\n\t\t_tonBalance = balance;\n\n\t\tconst auto was = std::exchange(_tonLastLoaded, crl::now());\n\t\tif (!was) {\n\t\t\t_tonLoadedChanges.fire({});\n\t\t}\n\t} else {\n\t\t_balance = balance;\n\t\tupdateNonLockedValue();\n\n\t\tconst auto was = std::exchange(_lastLoaded, crl::now());\n\t\tif (!was) {\n\t\t\t_loadedChanges.fire({});\n\t\t}\n\t}\n}\n\nvoid Credits::apply(PeerId peerId, CreditsAmount balance) {\n\t_cachedPeerBalances[peerId] = balance;\n\t_refreshedByPeerId.fire_copy(peerId);\n}\n\nvoid Credits::applyCurrency(PeerId peerId, CreditsAmount balance) {\n\t_cachedPeerCurrencyBalances[peerId] = balance;\n\t_refreshedByPeerId.fire_copy(peerId);\n}\n\nrpl::producer<> Credits::refreshedByPeerId(PeerId peerId) {\n\treturn _refreshedByPeerId.events(\n\t) | rpl::filter(rpl::mappers::_1 == peerId) | rpl::to_empty;\n}\n\nbool Credits::statsEnabled() const {\n\treturn _statsEnabled;\n}\n\n} // namespace Data\n\nCreditsAmount CreditsAmountFromTL(const MTPStarsAmount &amount) {\n\treturn amount.match([&](const MTPDstarsAmount &data) {\n\t\treturn CreditsAmount(\n\t\t\tdata.vamount().v,\n\t\t\tdata.vnanos().v,\n\t\t\tCreditsType::Stars);\n\t}, [&](const MTPDstarsTonAmount &data) {\n\t\tconst auto isNegative = (static_cast<int64_t>(data.vamount().v) < 0);\n\t\tconst auto absValue = isNegative\n\t\t\t? uint64(~data.vamount().v + 1)\n\t\t\t: data.vamount().v;\n\t\tconst auto result = CreditsAmount(\n\t\t\tint64(absValue / 1'000'000'000),\n\t\t\tabsValue % 1'000'000'000,\n\t\t\tCreditsType::Ton);\n\t\treturn isNegative\n\t\t\t? CreditsAmount(0, CreditsType::Ton) - result\n\t\t\t: result;\n\t});\n}\n\nCreditsAmount CreditsAmountFromTL(const MTPStarsAmount *amount) {\n\treturn amount ? CreditsAmountFromTL(*amount) : CreditsAmount();\n}\n\nMTPStarsAmount StarsAmountToTL(CreditsAmount amount) {\n\treturn amount.ton() ? MTP_starsTonAmount(\n\t\tMTP_long(amount.whole() * uint64(1'000'000'000) + amount.nano())\n\t) : MTP_starsAmount(MTP_long(amount.whole()), MTP_int(amount.nano()));\n}\n\nQString PrepareCreditsAmountText(CreditsAmount amount) {\n\treturn amount.stars()\n\t\t? tr::lng_action_gift_for_stars(\n\t\t\ttr::now,\n\t\t\tlt_count_decimal,\n\t\t\tamount.value())\n\t\t: tr::lng_action_gift_for_ton(\n\t\t\ttr::now,\n\t\t\tlt_count_decimal,\n\t\t\tamount.value());\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/credits.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nclass Credits final {\npublic:\n\texplicit Credits(not_null<Main::Session*> session);\n\t~Credits();\n\n\tvoid load(bool force = false);\n\t[[nodiscard]] bool loaded() const;\n\t[[nodiscard]] rpl::producer<bool> loadedValue() const;\n\t[[nodiscard]] CreditsAmount balance() const;\n\t[[nodiscard]] CreditsAmount balance(PeerId peerId) const;\n\t[[nodiscard]] rpl::producer<CreditsAmount> balanceValue() const;\n\t[[nodiscard]] float64 usdRate() const;\n\t[[nodiscard]] rpl::producer<float64> rateValue(\n\t\tnot_null<PeerData*> ownedBotOrChannel);\n\n\t[[nodiscard]] rpl::producer<> refreshedByPeerId(PeerId peerId);\n\n\tvoid tonLoad(bool force = false);\n\t[[nodiscard]] bool tonLoaded() const;\n\t[[nodiscard]] rpl::producer<bool> tonLoadedValue() const;\n\t[[nodiscard]] CreditsAmount tonBalance() const;\n\t[[nodiscard]] rpl::producer<CreditsAmount> tonBalanceValue() const;\n\n\tvoid apply(CreditsAmount balance);\n\tvoid apply(PeerId peerId, CreditsAmount balance);\n\n\t[[nodiscard]] bool statsEnabled() const;\n\n\tvoid applyCurrency(PeerId peerId, CreditsAmount balance);\n\t[[nodiscard]] CreditsAmount balanceCurrency(PeerId peerId) const;\n\n\tvoid lock(CreditsAmount count);\n\tvoid unlock(CreditsAmount count);\n\tvoid withdrawLocked(CreditsAmount count);\n\tvoid invalidate();\n\n\tvoid apply(const MTPDupdateStarsBalance &data);\n\nprivate:\n\tvoid updateNonLockedValue();\n\n\tconst not_null<Main::Session*> _session;\n\n\tstd::unique_ptr<rpl::lifetime> _loader;\n\n\tbase::flat_map<PeerId, CreditsAmount> _cachedPeerBalances;\n\tbase::flat_map<PeerId, CreditsAmount> _cachedPeerCurrencyBalances;\n\n\tCreditsAmount _balance;\n\tCreditsAmount _locked;\n\trpl::variable<CreditsAmount> _nonLockedBalance;\n\trpl::event_stream<> _loadedChanges;\n\tcrl::time _lastLoaded = 0;\n\n\trpl::variable<CreditsAmount> _tonBalance;\n\trpl::event_stream<> _tonLoadedChanges;\n\tcrl::time _tonLastLoaded = false;\n\tmtpRequestId _tonRequestId = 0;\n\n\tbool _statsEnabled = false;\n\n\trpl::event_stream<PeerId> _refreshedByPeerId;\n\n\tSingleQueuedInvokation _reload;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/factchecks.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/components/factchecks.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"apiwrap.h\"\n#include \"base/random.h\"\n#include \"data/data_session.h\"\n#include \"data/data_web_page.h\"\n#include \"history/view/media/history_view_web_page.h\"\n#include \"history/view/history_view_message.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"ui/layers/show.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kRequestDelay = crl::time(1000);\n\n} // namespace\n\nFactchecks::Factchecks(not_null<Main::Session*> session)\n: _session(session)\n, _requestTimer([=] { request(); }) {\n}\n\nvoid Factchecks::requestFor(not_null<HistoryItem*> item) {\n\tsubscribeIfNotYet();\n\n\tif (const auto factcheck = item->Get<HistoryMessageFactcheck>()) {\n\t\tfactcheck->requested = true;\n\t}\n\tif (!_requestTimer.isActive()) {\n\t\t_requestTimer.callOnce(kRequestDelay);\n\t}\n\tconst auto changed = !_pending.empty()\n\t\t&& (_pending.front()->history() != item->history());\n\tconst auto added = _pending.emplace(item).second;\n\tif (changed) {\n\t\trequest();\n\t} else if (added && _pending.size() == 1) {\n\t\t_requestTimer.callOnce(kRequestDelay);\n\t}\n}\n\nvoid Factchecks::subscribeIfNotYet() {\n\tif (_subscribed) {\n\t\treturn;\n\t}\n\t_subscribed = true;\n\n\t_session->data().itemRemoved(\n\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\t_pending.remove(item);\n\t\tconst auto i = ranges::find(_requested, item.get());\n\t\tif (i != end(_requested)) {\n\t\t\t*i = nullptr;\n\t\t}\n\t}, _lifetime);\n}\n\nvoid Factchecks::request() {\n\t_requestTimer.cancel();\n\n\tif (!_requested.empty() || _pending.empty()) {\n\t\treturn;\n\t}\n\t_session->api().request(base::take(_requestId)).cancel();\n\n\tauto ids = QVector<MTPint>();\n\tids.reserve(_pending.size());\n\tconst auto history = _pending.front()->history();\n\tfor (auto i = begin(_pending); i != end(_pending);) {\n\t\tconst auto &item = *i;\n\t\tif (item->history() == history) {\n\t\t\t_requested.push_back(item);\n\t\t\tids.push_back(MTP_int(item->id.bare));\n\t\t\ti = _pending.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\t_requestId = _session->api().request(MTPmessages_GetFactCheck(\n\t\thistory->peer->input(),\n\t\tMTP_vector<MTPint>(std::move(ids))\n\t)).done([=](const MTPVector<MTPFactCheck> &result) {\n\t\t_requestId = 0;\n\t\tconst auto &list = result.v;\n\t\tauto index = 0;\n\t\tfor (const auto &item : base::take(_requested)) {\n\t\t\tif (!item) {\n\t\t\t} else if (index >= list.size()) {\n\t\t\t\titem->setFactcheck({});\n\t\t\t} else {\n\t\t\t\titem->setFactcheck(FromMTP(item, &list[index]));\n\t\t\t}\n\t\t\t++index;\n\t\t}\n\t\tif (!_pending.empty()) {\n\t\t\trequest();\n\t\t}\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t\tfor (const auto &item : base::take(_requested)) {\n\t\t\tif (item) {\n\t\t\t\titem->setFactcheck({});\n\t\t\t}\n\t\t}\n\t\tif (!_pending.empty()) {\n\t\t\trequest();\n\t\t}\n\t}).send();\n}\n\nstd::unique_ptr<HistoryView::WebPage> Factchecks::makeMedia(\n\t\tnot_null<HistoryView::Message*> view,\n\t\tnot_null<HistoryMessageFactcheck*> factcheck) {\n\tif (!factcheck->page) {\n\t\tfactcheck->page = view->history()->owner().webpage(\n\t\t\tbase::RandomValue<WebPageId>(),\n\t\t\ttr::lng_factcheck_title(tr::now),\n\t\t\tfactcheck->data.text);\n\t\tfactcheck->page->type = WebPageType::Factcheck;\n\t}\n\treturn std::make_unique<HistoryView::WebPage>(\n\t\tview,\n\t\tfactcheck->page,\n\t\tMediaWebPageFlags());\n}\n\nbool Factchecks::canEdit(not_null<HistoryItem*> item) const {\n\tif (!canEdit()\n\t\t|| !item->isRegular()\n\t\t|| !item->history()->peer->isBroadcast()) {\n\t\treturn false;\n\t}\n\tconst auto media = item->media();\n\tif (!media || media->webpage() || media->photo()) {\n\t\treturn true;\n\t} else if (const auto document = media->document()) {\n\t\treturn !document->isVideoMessage() && !document->sticker();\n\t}\n\treturn false;\n}\n\nbool Factchecks::canEdit() const {\n\treturn _session->appConfig().get<bool>(u\"can_edit_factcheck\"_q, false);\n}\n\nint Factchecks::lengthLimit() const {\n\treturn _session->appConfig().get<int>(u\"factcheck_length_limit\"_q, 1024);\n}\n\nvoid Factchecks::save(\n\t\tFullMsgId itemId,\n\t\tTextWithEntities text,\n\t\tFn<void(QString)> done) {\n\tconst auto item = _session->data().message(itemId);\n\tif (!item) {\n\t\treturn;\n\t} else if (text.empty()) {\n\t\t_session->api().request(MTPmessages_DeleteFactCheck(\n\t\t\titem->history()->peer->input(),\n\t\t\tMTP_int(item->id.bare)\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t_session->api().applyUpdates(result);\n\t\t\tdone(QString());\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tdone(error.type());\n\t\t}).send();\n\t} else {\n\t\t_session->api().request(MTPmessages_EditFactCheck(\n\t\t\titem->history()->peer->input(),\n\t\t\tMTP_int(item->id.bare),\n\t\t\tMTP_textWithEntities(\n\t\t\t\tMTP_string(text.text),\n\t\t\t\tApi::EntitiesToMTP(\n\t\t\t\t\t_session,\n\t\t\t\t\ttext.entities,\n\t\t\t\t\tApi::ConvertOption::SkipLocal))\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t_session->api().applyUpdates(result);\n\t\t\tdone(QString());\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tdone(error.type());\n\t\t}).send();\n\t}\n}\n\nvoid Factchecks::save(\n\t\tFullMsgId itemId,\n\t\tconst TextWithEntities &was,\n\t\tTextWithEntities text,\n\t\tstd::shared_ptr<Ui::Show> show) {\n\tconst auto wasEmpty = was.empty();\n\tconst auto textEmpty = text.empty();\n\tsave(itemId, std::move(text), [=](QString error) {\n\t\tshow->showToast(!error.isEmpty()\n\t\t\t? error\n\t\t\t: textEmpty\n\t\t\t? tr::lng_factcheck_remove_done(tr::now)\n\t\t\t: wasEmpty\n\t\t\t? tr::lng_factcheck_add_done(tr::now)\n\t\t\t: tr::lng_factcheck_edit_done(tr::now));\n\t});\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/factchecks.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n\nclass HistoryItem;\nstruct HistoryMessageFactcheck;\n\nnamespace HistoryView {\nclass Message;\nclass WebPage;\n} // namespace HistoryView\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass Show;\n} // namespace Ui\n\nnamespace Data {\n\nclass Factchecks final {\npublic:\n\texplicit Factchecks(not_null<Main::Session*> session);\n\n\tvoid requestFor(not_null<HistoryItem*> item);\n\t[[nodiscard]] std::unique_ptr<HistoryView::WebPage> makeMedia(\n\t\tnot_null<HistoryView::Message*> view,\n\t\tnot_null<HistoryMessageFactcheck*> factcheck);\n\n\t[[nodiscard]] bool canEdit(not_null<HistoryItem*> item) const;\n\t[[nodiscard]] int lengthLimit() const;\n\n\tvoid save(\n\t\tFullMsgId itemId,\n\t\tTextWithEntities text,\n\t\tFn<void(QString)> done);\n\tvoid save(\n\t\tFullMsgId itemId,\n\t\tconst TextWithEntities &was,\n\t\tTextWithEntities text,\n\t\tstd::shared_ptr<Ui::Show> show);\n\nprivate:\n\t[[nodiscard]] bool canEdit() const;\n\n\tvoid subscribeIfNotYet();\n\tvoid request();\n\n\tconst not_null<Main::Session*> _session;\n\n\tbase::Timer _requestTimer;\n\tbase::flat_set<not_null<HistoryItem*>> _pending;\n\tstd::vector<HistoryItem*> _requested;\n\tmtpRequestId _requestId = 0;\n\tbool _subscribed = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/gift_auctions.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/components/gift_auctions.h\"\n\n#include \"api/api_hash.h\"\n#include \"api/api_premium.h\"\n#include \"api/api_text_entities.h\"\n#include \"apiwrap.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n\nnamespace Data {\n\nGiftAuctions::GiftAuctions(not_null<Main::Session*> session)\n: _session(session)\n, _timer([=] { checkSubscriptions(); }) {\n\tcrl::on_main(_session, [=] {\n\t\trpl::merge(\n\t\t\t_session->data().chatsListChanges(),\n\t\t\t_session->data().chatsListLoadedEvents()\n\t\t) | rpl::filter(\n\t\t\t!rpl::mappers::_1\n\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\trequestActive();\n\t\t}, _lifetime);\n\t});\n}\n\nGiftAuctions::~GiftAuctions() = default;\n\nvoid GiftAuctions::resolveSlug(const QString &slug, Fn<void(uint64)> done) {\n\tconst auto it = _slugToGiftId.find(slug);\n\tif (it != _slugToGiftId.end()) {\n\t\tdone(it->second);\n\t\treturn;\n\t}\n\n\tauto &request = _slugRequests[slug];\n\trequest.callbacks.push_back(std::move(done));\n\tif (request.callbacks.size() > 1) {\n\t\treturn;\n\t}\n\n\t_session->api().request(MTPpayments_GetStarGiftAuctionState(\n\t\tMTP_inputStarGiftAuctionSlug(MTP_string(slug)),\n\t\tMTP_int(0)\n\t)).done([=](const MTPpayments_StarGiftAuctionState &result) {\n\t\tconst auto &data = result.data();\n\n\t\t_session->data().processUsers(data.vusers());\n\t\t_session->data().processChats(data.vchats());\n\n\t\tauto gift = Api::FromTL(_session, data.vgift());\n\t\tif (!gift || !gift->id) {\n\t\t\t_slugRequests.erase(slug);\n\t\t\treturn;\n\t\t}\n\t\tconst auto giftId = gift->id;\n\n\t\t_slugToGiftId.emplace(slug, giftId);\n\n\t\tauto &entry = _map[giftId];\n\t\tif (!entry) {\n\t\t\tentry = std::make_unique<Entry>();\n\t\t}\n\t\tconst auto raw = entry.get();\n\t\traw->state.gift = std::move(gift);\n\n\t\tapplyStateResponse(raw, result);\n\n\t\tauto callbacks = std::move(_slugRequests[slug].callbacks);\n\t\t_slugRequests.erase(slug);\n\t\tfor (const auto &callback : callbacks) {\n\t\t\tcallback(giftId);\n\t\t}\n\t}).fail([=] {\n\t\t_slugRequests.erase(slug);\n\t}).send();\n}\n\nvoid GiftAuctions::applyStateResponse(\n\t\tnot_null<Entry*> entry,\n\t\tconst MTPpayments_StarGiftAuctionState &result) {\n\tconst auto &data = result.data();\n\n\tconst auto timeout = data.vtimeout().v;\n\tconst auto ms = timeout * crl::time(1000);\n\tentry->state.subscribedTill = ms ? (crl::now() + ms) : -1;\n\n\tconst auto was = myStateKey(entry->state);\n\tapply(entry, data.vstate());\n\tapply(entry, data.vuser_state());\n\n\tif (entry->changes.has_consumers()) {\n\t\tentry->changes.fire({});\n\t\tif (ms && (!_timer.isActive() || _timer.remainingTime() > ms)) {\n\t\t\t_timer.callOnce(ms);\n\t\t}\n\t}\n\tif (was != myStateKey(entry->state)) {\n\t\t_activeChanged.fire({});\n\t}\n}\n\nrpl::producer<GiftAuctionState> GiftAuctions::state(uint64 giftId) {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tauto &entry = _map[giftId];\n\t\tif (!entry) {\n\t\t\tentry = std::make_unique<Entry>();\n\t\t}\n\t\tconst auto raw = entry.get();\n\n\t\traw->changes.events() | rpl::on_next([=] {\n\t\t\tconsumer.put_next_copy(raw->state);\n\t\t}, lifetime);\n\n\t\tconst auto now = crl::now();\n\t\tif (raw->state.subscribedTill < 0\n\t\t\t|| raw->state.subscribedTill >= now) {\n\t\t\tconsumer.put_next_copy(raw->state);\n\t\t} else if (raw->state.subscribedTill >= 0) {\n\t\t\trequest(giftId);\n\t\t}\n\n\t\treturn lifetime;\n\t};\n}\n\nvoid GiftAuctions::apply(const MTPDupdateStarGiftAuctionState &data) {\n\tif (const auto entry = find(data.vgift_id().v)) {\n\t\tconst auto was = myStateKey(entry->state);\n\t\tapply(entry, data.vstate());\n\t\tentry->changes.fire({});\n\t\tif (was != myStateKey(entry->state)) {\n\t\t\t_activeChanged.fire({});\n\t\t}\n\t} else {\n\t\trequestActive();\n\t}\n}\n\nvoid GiftAuctions::apply(const MTPDupdateStarGiftAuctionUserState &data) {\n\tif (const auto entry = find(data.vgift_id().v)) {\n\t\tconst auto was = myStateKey(entry->state);\n\t\tapply(entry, data.vuser_state());\n\t\tentry->changes.fire({});\n\t\tif (was != myStateKey(entry->state)) {\n\t\t\t_activeChanged.fire({});\n\t\t}\n\t} else {\n\t\trequestActive();\n\t}\n}\n\nvoid GiftAuctions::requestAcquired(\n\t\tuint64 giftId,\n\t\tFn<void(std::vector<Data::GiftAcquired>)> done) {\n\tExpects(done != nullptr);\n\n\t_session->api().request(MTPpayments_GetStarGiftAuctionAcquiredGifts(\n\t\tMTP_long(giftId)\n\t)).done([=](const MTPpayments_StarGiftAuctionAcquiredGifts &result) {\n\t\tconst auto &data = result.data();\n\n\t\tconst auto owner = &_session->data();\n\t\towner->processUsers(data.vusers());\n\t\towner->processChats(data.vchats());\n\n\t\tconst auto &list = data.vgifts().v;\n\t\tauto gifts = std::vector<Data::GiftAcquired>();\n\t\tgifts.reserve(list.size());\n\t\tfor (const auto &gift : list) {\n\t\t\tconst auto &data = gift.data();\n\t\t\tgifts.push_back({\n\t\t\t\t.to = owner->peer(peerFromMTP(data.vpeer())),\n\t\t\t\t.message = (data.vmessage()\n\t\t\t\t\t? Api::ParseTextWithEntities(_session, *data.vmessage())\n\t\t\t\t\t: TextWithEntities()),\n\t\t\t\t.date = data.vdate().v,\n\t\t\t\t.bidAmount = int64(data.vbid_amount().v),\n\t\t\t\t.round = data.vround().v,\n\t\t\t\t.number = data.vgift_num().value_or_empty(),\n\t\t\t\t.position = data.vpos().v,\n\t\t\t\t.nameHidden = data.is_name_hidden(),\n\t\t\t});\n\t\t}\n\t\tif (const auto entry = find(giftId)) {\n\t\t\tconst auto count = int(gifts.size());\n\t\t\tif (entry->state.my.gotCount != count) {\n\t\t\t\tentry->state.my.gotCount = count;\n\t\t\t\tentry->changes.fire({});\n\t\t\t}\n\t\t}\n\t\tdone(std::move(gifts));\n\t}).fail([=] {\n\t\tdone({});\n\t}).send();\n}\n\nstd::optional<Data::UniqueGiftAttributes> GiftAuctions::attributes(\n\t\tuint64 giftId) const {\n\tconst auto i = _attributes.find(giftId);\n\treturn (i != end(_attributes) && i->second.waiters.empty())\n\t\t? i->second.lists\n\t\t: std::optional<Data::UniqueGiftAttributes>();\n}\n\nvoid GiftAuctions::requestAttributes(uint64 giftId, Fn<void()> ready) {\n\tauto &entry = _attributes[giftId];\n\tentry.waiters.push_back(std::move(ready));\n\tif (entry.waiters.size() > 1) {\n\t\treturn;\n\t}\n\t_session->api().request(MTPpayments_GetStarGiftUpgradeAttributes(\n\t\tMTP_long(giftId)\n\t)).done([=](const MTPpayments_StarGiftUpgradeAttributes &result) {\n\t\tconst auto &attributes = result.data().vattributes().v;\n\t\tauto &entry = _attributes[giftId];\n\t\tauto &info = entry.lists;\n\t\tinfo.models.reserve(attributes.size());\n\t\tinfo.patterns.reserve(attributes.size());\n\t\tinfo.backdrops.reserve(attributes.size());\n\t\tfor (const auto &attribute : attributes) {\n\t\t\tattribute.match([&](const MTPDstarGiftAttributeModel &data) {\n\t\t\t\tinfo.models.push_back(Api::FromTL(_session, data));\n\t\t\t}, [&](const MTPDstarGiftAttributePattern &data) {\n\t\t\t\tinfo.patterns.push_back(Api::FromTL(_session, data));\n\t\t\t}, [&](const MTPDstarGiftAttributeBackdrop &data) {\n\t\t\t\tinfo.backdrops.push_back(Api::FromTL(data));\n\t\t\t}, [](const MTPDstarGiftAttributeOriginalDetails &data) {\n\t\t\t});\n\t\t}\n\t\tfor (const auto &ready : base::take(entry.waiters)) {\n\t\t\tif (ready) ready();\n\t\t}\n\t}).fail([=] {\n\t\tfor (const auto &ready : base::take(_attributes[giftId].waiters)) {\n\t\t\tif (ready) ready();\n\t\t}\n\t}).send();\n}\n\nrpl::producer<ActiveAuctions> GiftAuctions::active() const {\n\treturn _activeChanged.events_starting_with_copy(\n\t\trpl::empty\n\t) | rpl::map([=] {\n\t\treturn collectActive();\n\t});\n}\n\nrpl::producer<bool> GiftAuctions::hasActiveChanges() const {\n\tconst auto has = hasActive();\n\treturn _activeChanged.events(\n\t) | rpl::map([=] {\n\t\treturn hasActive();\n\t}) | rpl::combine_previous(\n\t\thas\n\t) | rpl::filter([=](bool previous, bool current) {\n\t\treturn previous != current;\n\t}) | rpl::map([=](bool previous, bool current) {\n\t\treturn current;\n\t});\n}\n\nbool GiftAuctions::hasActive() const {\n\tfor (const auto &[giftId, entry] : _map) {\n\t\tif (myStateKey(entry->state)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid GiftAuctions::checkSubscriptions() {\n\tconst auto now = crl::now();\n\tauto next = crl::time();\n\tfor (const auto &[giftId, entry] : _map) {\n\t\tconst auto raw = entry.get();\n\t\tconst auto till = raw->state.subscribedTill;\n\t\tif (till <= 0 || !raw->changes.has_consumers()) {\n\t\t\tcontinue;\n\t\t} else if (till <= now) {\n\t\t\trequest(giftId);\n\t\t} else {\n\t\t\tconst auto timeout = till - now;\n\t\t\tif (!next || timeout < next) {\n\t\t\t\tnext = timeout;\n\t\t\t}\n\t\t}\n\t}\n\tif (next) {\n\t\t_timer.callOnce(next);\n\t}\n}\n\nauto GiftAuctions::myStateKey(const GiftAuctionState &state) const\n-> MyStateKey {\n\tif (!state.my.bid) {\n\t\treturn {};\n\t}\n\tauto min = 0;\n\tfor (const auto &level : state.bidLevels) {\n\t\tif (level.position > state.gift->auctionGiftsPerRound) {\n\t\t\tbreak;\n\t\t} else if (!min || min > level.amount) {\n\t\t\tmin = level.amount;\n\t\t}\n\t}\n\treturn {\n\t\t.bid = int(state.my.bid),\n\t\t.position = MyAuctionPosition(state),\n\t\t.version = state.version,\n\t};\n}\n\nActiveAuctions GiftAuctions::collectActive() const {\n\tauto result = ActiveAuctions();\n\tresult.list.reserve(_map.size());\n\tfor (const auto &[giftId, entry] : _map) {\n\t\tconst auto raw = &entry->state;\n\t\tif (raw->gift && raw->my.date) {\n\t\t\tresult.list.push_back(raw);\n\t\t}\n\t}\n\treturn result;\n}\n\nuint64 GiftAuctions::countActiveHash() const {\n\tauto result = Api::HashInit();\n\tfor (const auto &active : collectActive().list) {\n\t\tApi::HashUpdate(result, active->version);\n\t\tApi::HashUpdate(result, active->my.date);\n\t}\n\treturn Api::HashFinalize(result);\n}\n\nvoid GiftAuctions::requestActive() {\n\tif (_activeRequestId) {\n\t\treturn;\n\t}\n\t_activeRequestId = _session->api().request(\n\t\tMTPpayments_GetStarGiftActiveAuctions(MTP_long(countActiveHash()))\n\t).done([=](const MTPpayments_StarGiftActiveAuctions &result) {\n\t\t_activeRequestId = 0;\n\t\tresult.match([=](const MTPDpayments_starGiftActiveAuctions &data) {\n\t\t\tconst auto owner = &_session->data();\n\t\t\towner->processUsers(data.vusers());\n\t\t\towner->processChats(data.vchats());\n\n\t\t\tauto giftsFound = base::flat_set<uint64>();\n\t\t\tconst auto &list = data.vauctions().v;\n\t\t\tgiftsFound.reserve(list.size());\n\t\t\tfor (const auto &auction : list) {\n\t\t\t\tconst auto &data = auction.data();\n\t\t\t\tauto gift = Api::FromTL(_session, data.vgift());\n\t\t\t\tif (!gift || !gift->id) {\n\t\t\t\t\tLOG((\"Api Error: Bad auction gift.\"));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto giftId = gift->id;\n\t\t\t\tauto &entry = _map[giftId];\n\t\t\t\tif (!entry) {\n\t\t\t\t\tentry = std::make_unique<Entry>();\n\t\t\t\t}\n\t\t\t\tconst auto raw = entry.get();\n\t\t\t\tif (!raw->state.gift) {\n\t\t\t\t\traw->state.gift = std::move(gift);\n\t\t\t\t}\n\t\t\t\tapply(raw, data.vstate());\n\t\t\t\tapply(raw, data.vuser_state());\n\t\t\t\tgiftsFound.emplace(giftId);\n\t\t\t}\n\t\t\tfor (const auto &[giftId, entry] : _map) {\n\t\t\t\tconst auto my = &entry->state.my;\n\t\t\t\tif (my->date && !giftsFound.contains(giftId)) {\n\t\t\t\t\tmy->to = nullptr;\n\t\t\t\t\tmy->minBidAmount = 0;\n\t\t\t\t\tmy->bid = 0;\n\t\t\t\t\tmy->date = 0;\n\t\t\t\t\tmy->returned = false;\n\t\t\t\t\tgiftsFound.emplace(giftId);\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const auto &giftId : giftsFound) {\n\t\t\t\t_map[giftId]->changes.fire({});\n\t\t\t}\n\t\t\t_activeChanged.fire({});\n\t\t}, [](const MTPDpayments_starGiftActiveAuctionsNotModified &) {\n\t\t});\n\t}).send();\n}\n\nvoid GiftAuctions::request(uint64 giftId) {\n\tauto it = _map.find(giftId);\n\tif (it == _map.end()) {\n\t\treturn;\n\t}\n\tconst auto raw = it->second.get();\n\tif (raw->requested) {\n\t\treturn;\n\t}\n\traw->requested = true;\n\t_session->api().request(MTPpayments_GetStarGiftAuctionState(\n\t\tMTP_inputStarGiftAuction(MTP_long(giftId)),\n\t\tMTP_int(raw->state.version)\n\t)).done([=](const MTPpayments_StarGiftAuctionState &result) {\n\t\tauto it = _map.find(giftId);\n\t\tif (it == _map.end()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto raw = it->second.get();\n\t\traw->requested = false;\n\n\t\tconst auto &data = result.data();\n\n\t\t_session->data().processUsers(data.vusers());\n\t\t_session->data().processChats(data.vchats());\n\n\t\traw->state.gift = Api::FromTL(_session, data.vgift());\n\t\tif (!raw->state.gift) {\n\t\t\treturn;\n\t\t}\n\n\t\tapplyStateResponse(raw, result);\n\t}).fail([=] {\n\t\tauto it = _map.find(giftId);\n\t\tif (it != _map.end()) {\n\t\t\tit->second->requested = false;\n\t\t}\n\t}).send();\n}\n\nGiftAuctions::Entry *GiftAuctions::find(uint64 giftId) const {\n\tconst auto it = _map.find(giftId);\n\treturn (it != _map.end()) ? it->second.get() : nullptr;\n}\n\nvoid GiftAuctions::apply(\n\t\tnot_null<Entry*> entry,\n\t\tconst MTPStarGiftAuctionState &state) {\n\tapply(&entry->state, state);\n}\n\nvoid GiftAuctions::apply(\n\t\tnot_null<GiftAuctionState*> entry,\n\t\tconst MTPStarGiftAuctionState &state) {\n\tExpects(entry->gift.has_value());\n\n\tstate.match([&](const MTPDstarGiftAuctionState &data) {\n\t\tconst auto version = data.vversion().v;\n\t\tif (entry->version >= version) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto owner = &_session->data();\n\t\tentry->startDate = data.vstart_date().v;\n\t\tentry->endDate = data.vend_date().v;\n\t\tentry->minBidAmount = data.vmin_bid_amount().v;\n\t\tconst auto &levels = data.vbid_levels().v;\n\t\tentry->bidLevels.clear();\n\t\tentry->bidLevels.reserve(levels.size());\n\t\tfor (const auto &level : levels) {\n\t\t\tauto &bid = entry->bidLevels.emplace_back();\n\t\t\tconst auto &data = level.data();\n\t\t\tbid.amount = data.vamount().v;\n\t\t\tbid.position = data.vpos().v;\n\t\t\tbid.date = data.vdate().v;\n\t\t}\n\t\tconst auto &top = data.vtop_bidders().v;\n\t\tentry->topBidders.clear();\n\t\tentry->topBidders.reserve(top.size());\n\t\tfor (const auto &user : top) {\n\t\t\tentry->topBidders.push_back(owner->user(UserId(user.v)));\n\t\t}\n\t\tentry->nextRoundAt = data.vnext_round_at().v;\n\t\tentry->giftsLeft = data.vgifts_left().v;\n\t\tentry->currentRound = data.vcurrent_round().v;\n\t\tentry->totalRounds = data.vtotal_rounds().v;\n\t\tconst auto &rounds = data.vrounds().v;\n\t\tentry->roundParameters.clear();\n\t\tentry->roundParameters.reserve(rounds.size());\n\t\tfor (const auto &round : rounds) {\n\t\t\tround.match([&](const MTPDstarGiftAuctionRound &data) {\n\t\t\t\tentry->roundParameters.push_back({\n\t\t\t\t\t.number = data.vnum().v,\n\t\t\t\t\t.duration = data.vduration().v,\n\t\t\t\t});\n\t\t\t}, [&](const MTPDstarGiftAuctionRoundExtendable &data) {\n\t\t\t\tentry->roundParameters.push_back({\n\t\t\t\t\t.number = data.vnum().v,\n\t\t\t\t\t.duration = data.vduration().v,\n\t\t\t\t\t.extendTop = data.vextend_top().v,\n\t\t\t\t\t.extendDuration = data.vextend_window().v,\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t\tentry->averagePrice = 0;\n\t}, [&](const MTPDstarGiftAuctionStateFinished &data) {\n\t\tentry->averagePrice = data.vaverage_price().v;\n\t\tentry->startDate = data.vstart_date().v;\n\t\tentry->endDate = data.vend_date().v;\n\t\tentry->minBidAmount = 0;\n\t\tentry->nextRoundAt\n\t\t\t= entry->currentRound\n\t\t\t= entry->totalRounds\n\t\t\t= entry->giftsLeft\n\t\t\t= entry->version\n\t\t\t= 0;\n\t}, [&](const MTPDstarGiftAuctionStateNotModified &data) {\n\t});\n}\n\nvoid GiftAuctions::apply(\n\t\tnot_null<Entry*> entry,\n\t\tconst MTPStarGiftAuctionUserState &state) {\n\tapply(&entry->state.my, state);\n}\n\nvoid GiftAuctions::apply(\n\t\tnot_null<StarGiftAuctionMyState*> entry,\n\t\tconst MTPStarGiftAuctionUserState &state) {\n\tconst auto &data = state.data();\n\tentry->to = data.vbid_peer()\n\t\t? _session->data().peer(peerFromMTP(*data.vbid_peer())).get()\n\t\t: nullptr;\n\tentry->minBidAmount = data.vmin_bid_amount().value_or(0);\n\tentry->bid = data.vbid_amount().value_or(0);\n\tentry->date = data.vbid_date().value_or(0);\n\tentry->gotCount = data.vacquired_count().v;\n\tentry->returned = data.is_returned();\n}\n\nint MyAuctionPosition(const GiftAuctionState &state) {\n\tconst auto &levels = state.bidLevels;\n\tfor (auto i = begin(levels), e = end(levels); i != e; ++i) {\n\t\tif (i->amount < state.my.bid\n\t\t\t|| (i->amount == state.my.bid && i->date >= state.my.date)) {\n\t\t\treturn i->position;\n\t\t}\n\t}\n\treturn (levels.empty() ? 0 : levels.back().position) + 1;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/gift_auctions.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"data/data_star_gift.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nstruct GiftAuctionBidLevel {\n\tint64 amount = 0;\n\tint position = 0;\n\tTimeId date = 0;\n};\n\nstruct StarGiftAuctionMyState {\n\tPeerData *to = nullptr;\n\tint64 minBidAmount = 0;\n\tint64 bid = 0;\n\tTimeId date = 0;\n\tint gotCount = 0;\n\tbool returned = false;\n};\n\nstruct GiftAuctionRound {\n\tint number = 0;\n\tTimeId duration = 0;\n\tint extendTop = 0;\n\tTimeId extendDuration = 0;\n};\n\nstruct GiftAuctionState {\n\tstd::optional<StarGift> gift;\n\tStarGiftAuctionMyState my;\n\tstd::vector<GiftAuctionBidLevel> bidLevels;\n\tstd::vector<not_null<UserData*>> topBidders;\n\tstd::vector<GiftAuctionRound> roundParameters;\n\tcrl::time subscribedTill = 0;\n\tint64 minBidAmount = 0;\n\tint64 averagePrice = 0;\n\tTimeId startDate = 0;\n\tTimeId endDate = 0;\n\tTimeId nextRoundAt = 0;\n\tint currentRound = 0;\n\tint totalRounds = 0;\n\tint giftsLeft = 0;\n\tint version = 0;\n\n\t[[nodiscard]] bool finished() const {\n\t\treturn (averagePrice != 0);\n\t}\n};\n\nstruct GiftAcquired {\n\tnot_null<PeerData*> to;\n\tTextWithEntities message;\n\tTimeId date = 0;\n\tint64 bidAmount = 0;\n\tint round = 0;\n\tint number = 0;\n\tint position = 0;\n\tbool nameHidden = false;\n};\n\nstruct ActiveAuctions {\n\tstd::vector<not_null<GiftAuctionState*>> list;\n};\n\nclass GiftAuctions final {\npublic:\n\texplicit GiftAuctions(not_null<Main::Session*> session);\n\t~GiftAuctions();\n\n\t[[nodiscard]] rpl::producer<GiftAuctionState> state(uint64 giftId);\n\tvoid resolveSlug(const QString &slug, Fn<void(uint64)> done);\n\n\tvoid apply(const MTPDupdateStarGiftAuctionState &data);\n\tvoid apply(const MTPDupdateStarGiftAuctionUserState &data);\n\n\tvoid requestAcquired(\n\t\tuint64 giftId,\n\t\tFn<void(std::vector<Data::GiftAcquired>)> done);\n\n\t[[nodiscard]] std::optional<Data::UniqueGiftAttributes> attributes(\n\t\tuint64 giftId) const;\n\tvoid requestAttributes(uint64 giftId, Fn<void()> ready = nullptr);\n\n\t[[nodiscard]] rpl::producer<ActiveAuctions> active() const;\n\t[[nodiscard]] rpl::producer<bool> hasActiveChanges() const;\n\t[[nodiscard]] bool hasActive() const;\n\nprivate:\n\tstruct Entry {\n\t\tGiftAuctionState state;\n\t\trpl::event_stream<> changes;\n\t\tbool requested = false;\n\t};\n\tstruct MyStateKey {\n\t\tint bid = 0;\n\t\tint position = 0;\n\t\tint version = 0;\n\n\t\texplicit operator bool() const {\n\t\t\treturn bid != 0;\n\t\t}\n\t\tfriend inline bool operator==(MyStateKey, MyStateKey) = default;\n\t};\n\tstruct Attributes {\n\t\tData::UniqueGiftAttributes lists;\n\t\tstd::vector<Fn<void()>> waiters;\n\t};\n\tstruct SlugRequest {\n\t\tstd::vector<Fn<void(uint64)>> callbacks;\n\t};\n\n\tvoid request(uint64 giftId);\n\tEntry *find(uint64 giftId) const;\n\tvoid applyStateResponse(\n\t\tnot_null<Entry*> entry,\n\t\tconst MTPpayments_StarGiftAuctionState &result);\n\tvoid apply(\n\t\tnot_null<Entry*> entry,\n\t\tconst MTPStarGiftAuctionState &state);\n\tvoid apply(\n\t\tnot_null<GiftAuctionState*> entry,\n\t\tconst MTPStarGiftAuctionState &state);\n\tvoid apply(\n\t\tnot_null<Entry*> entry,\n\t\tconst MTPStarGiftAuctionUserState &state);\n\tvoid apply(\n\t\tnot_null<StarGiftAuctionMyState*> entry,\n\t\tconst MTPStarGiftAuctionUserState &state);\n\tvoid checkSubscriptions();\n\n\t[[nodiscard]] MyStateKey myStateKey(const GiftAuctionState &state) const;\n\t[[nodiscard]] ActiveAuctions collectActive() const;\n\t[[nodiscard]] uint64 countActiveHash() const;\n\tvoid requestActive();\n\n\tconst not_null<Main::Session*> _session;\n\n\tbase::Timer _timer;\n\tbase::flat_map<uint64, std::unique_ptr<Entry>> _map;\n\tbase::flat_map<QString, SlugRequest> _slugRequests;\n\tbase::flat_map<QString, uint64> _slugToGiftId;\n\tbase::flat_map<uint64, Attributes> _attributes;\n\n\trpl::event_stream<> _activeChanged;\n\tmtpRequestId _activeRequestId = 0;\n\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] int MyAuctionPosition(const GiftAuctionState &state);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/location_pickers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/components/location_pickers.h\"\n\n#include \"api/api_common.h\"\n#include \"ui/controls/location_picker.h\"\n\nnamespace Data {\n\nstruct LocationPickers::Entry {\n\tApi::SendAction action;\n\tbase::weak_ptr<Ui::LocationPicker> picker;\n};\n\nLocationPickers::LocationPickers() = default;\n\nLocationPickers::~LocationPickers() = default;\n\nUi::LocationPicker *LocationPickers::lookup(const Api::SendAction &action) {\n\tfor (auto i = begin(_pickers); i != end(_pickers);) {\n\t\tif (const auto strong = i->picker.get()) {\n\t\t\tif (i->action == action) {\n\t\t\t\treturn strong;\n\t\t\t}\n\t\t\t++i;\n\t\t} else {\n\t\t\ti = _pickers.erase(i);\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid LocationPickers::emplace(\n\t\tconst Api::SendAction &action,\n\t\tnot_null<Ui::LocationPicker*> picker) {\n\t_pickers.push_back({ action, picker });\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/location_pickers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n\nnamespace Api {\nstruct SendAction;\n} // namespace Api\n\nnamespace Ui {\nclass LocationPicker;\n} // namespace Ui\n\nnamespace Data {\n\nclass LocationPickers final {\npublic:\n\tLocationPickers();\n\t~LocationPickers();\n\n\tUi::LocationPicker *lookup(const Api::SendAction &action);\n\tvoid emplace(\n\t\tconst Api::SendAction &action,\n\t\tnot_null<Ui::LocationPicker*> picker);\n\nprivate:\n\tstruct Entry;\n\n\tstd::vector<Entry> _pickers;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/passkeys.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/components/passkeys.h\"\n\n#include \"apiwrap.h\"\n#include \"data/data_passkey_deserialize.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"platform/platform_webauthn.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kTimeoutMs = 5000;\n\n[[nodiscard]] PasskeyEntry FromTL(const MTPDpasskey &data) {\n\treturn PasskeyEntry{\n\t\t.id = qs(data.vid()),\n\t\t.name = qs(data.vname()),\n\t\t.date = data.vdate().v,\n\t\t.softwareEmojiId = data.vsoftware_emoji_id().value_or(0),\n\t\t.lastUsageDate = data.vlast_usage_date().value_or(0),\n\t};\n}\n\n} // namespace\n\nPasskeys::Passkeys(not_null<Main::Session*> session)\n: _session(session) {\n}\n\nPasskeys::~Passkeys() = default;\n\nvoid Passkeys::initRegistration(\n\t\tFn<void(const Data::Passkey::RegisterData&)> done) {\n\t_session->api().request(MTPaccount_InitPasskeyRegistration(\n\t)).done([=](const MTPaccount_PasskeyRegistrationOptions &result) {\n\t\tconst auto &data = result.data();\n\t\tconst auto jsonData = data.voptions().data().vdata().v;\n\t\tif (const auto p = Data::Passkey::DeserializeRegisterData(jsonData)) {\n\t\t\tdone(*p);\n\t\t}\n\t}).send();\n}\n\nvoid Passkeys::registerPasskey(\n\t\tconst Platform::WebAuthn::RegisterResult &result,\n\t\tFn<void()> done) {\n\tconst auto credentialIdBase64 = QString::fromUtf8(\n\t\tresult.credentialId.toBase64(QByteArray::Base64UrlEncoding));\n\t_session->api().request(MTPaccount_RegisterPasskey(\n\t\tMTP_inputPasskeyCredentialPublicKey(\n\t\t\tMTP_string(credentialIdBase64),\n\t\t\tMTP_string(credentialIdBase64),\n\t\t\tMTP_inputPasskeyResponseRegister(\n\t\t\t\tMTP_dataJSON(MTP_bytes(result.clientDataJSON)),\n\t\t\t\tMTP_bytes(result.attestationObject)))\n\t)).done([=](const MTPPasskey &result) {\n\t\t_passkeys.emplace_back(FromTL(result.data()));\n\t\t_listUpdated.fire({});\n\t\tdone();\n\t}).send();\n}\n\nvoid Passkeys::deletePasskey(\n\t\tconst QString &id,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail) {\n\t_session->api().request(MTPaccount_DeletePasskey(\n\t\tMTP_string(id)\n\t)).done([=] {\n\t\t_lastRequestTime = 0;\n\t\t_listKnown = false;\n\t\tloadList();\n\t\tdone();\n\t}).fail([=](const MTP::Error &error) {\n\t\tfail(error.type());\n\t}).send();\n}\n\nrpl::producer<> Passkeys::requestList() {\n\tif (crl::now() - _lastRequestTime > kTimeoutMs) {\n\t\tif (!_listRequestId) {\n\t\t\tloadList();\n\t\t}\n\t\treturn _listUpdated.events();\n\t} else {\n\t\treturn _listUpdated.events_starting_with(rpl::empty_value());\n\t}\n}\n\nconst std::vector<PasskeyEntry> &Passkeys::list() const {\n\treturn _passkeys;\n}\n\nbool Passkeys::listKnown() const {\n\treturn _listKnown;\n}\n\nvoid Passkeys::loadList() {\n\t_lastRequestTime = crl::now();\n\t_listRequestId = _session->api().request(MTPaccount_GetPasskeys(\n\t)).done([=](const MTPaccount_Passkeys &result) {\n\t\t_listRequestId = 0;\n\t\t_listKnown = true;\n\t\tconst auto &data = result.data();\n\t\t_passkeys.clear();\n\t\t_passkeys.reserve(data.vpasskeys().v.size());\n\t\tfor (const auto &passkey : data.vpasskeys().v) {\n\t\t\t_passkeys.emplace_back(FromTL(passkey.data()));\n\t\t}\n\t\t_listUpdated.fire({});\n\t}).fail([=] {\n\t\t_listRequestId = 0;\n\t}).send();\n}\n\nbool Passkeys::canRegister() const {\n\tconst auto max = _session->appConfig().passkeysAccountPasskeysMax();\n\treturn Platform::WebAuthn::IsSupported() && _passkeys.size() < max;\n}\n\nbool Passkeys::possible() const {\n\treturn _session->appConfig().settingsDisplayPasskeys();\n}\n\nvoid InitPasskeyLogin(\n\t\tMTP::Sender &api,\n\t\tFn<void(const Data::Passkey::LoginData&)> done) {\n\tapi.request(MTPauth_InitPasskeyLogin(\n\t\tMTP_int(ApiId),\n\t\tMTP_string(ApiHash)\n\t)).done([=](const MTPauth_PasskeyLoginOptions &result) {\n\t\tconst auto &data = result.data();\n\t\tif (const auto p = Passkey::DeserializeLoginData(\n\t\t\t\tdata.voptions().data().vdata().v)) {\n\t\t\tdone(*p);\n\t\t}\n\t}).send();\n}\n\nvoid FinishPasskeyLogin(\n\t\tMTP::Sender &api,\n\t\tint initialDc,\n\t\tconst Platform::WebAuthn::LoginResult &result,\n\t\tFn<void(const MTPauth_Authorization&)> done,\n\t\tFn<void(QString)> fail) {\n\tconst auto userHandleStr = QString::fromUtf8(result.userHandle);\n\tconst auto parts = userHandleStr.split(':');\n\tif (parts.size() != 2) {\n\t\treturn;\n\t}\n\tconst auto userDc = parts[0].toInt();\n\tconst auto credentialIdBase64 = result.credentialId.toBase64(\n\t\tQByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);\n\tconst auto credential = MTP_inputPasskeyCredentialPublicKey(\n\t\tMTP_string(credentialIdBase64.toStdString()),\n\t\tMTP_string(credentialIdBase64.toStdString()),\n\t\tMTP_inputPasskeyResponseLogin(\n\t\t\tMTP_dataJSON(MTP_bytes(result.clientDataJSON)),\n\t\t\tMTP_bytes(result.authenticatorData),\n\t\t\tMTP_bytes(result.signature),\n\t\t\tMTP_string(userHandleStr.toStdString())\n\t\t)\n\t);\n\tconst auto flags = (userDc != initialDc)\n\t\t? MTPauth_finishPasskeyLogin::Flag::f_from_dc_id\n\t\t: MTPauth_finishPasskeyLogin::Flags(0);\n\tapi.request(MTPauth_FinishPasskeyLogin(\n\t\tMTP_flags(flags),\n\t\tcredential,\n\t\tMTP_int(initialDc),\n\t\tMTP_long(0)\n\t)).toDC(\n\t\tuserDc\n\t).done(done).fail([=](const MTP::Error &error) {\n\t\tfail(error.type());\n\t}).send();\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/passkeys.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data::Passkey {\nstruct RegisterData;\nstruct LoginData;\n} // namespace Data::Passkey\nnamespace Platform::WebAuthn {\nstruct RegisterResult;\nstruct LoginResult;\n} // namespace Platform::WebAuthn\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace MTP {\nclass Sender;\n} // namespace MTP\n\nnamespace Data {\n\nstruct PasskeyEntry {\n\tQString id;\n\tQString name;\n\tTimeId date = 0;\n\tDocumentId softwareEmojiId = 0;\n\tTimeId lastUsageDate = 0;\n};\n\nclass Passkeys final {\npublic:\n\texplicit Passkeys(not_null<Main::Session*> session);\n\t~Passkeys();\n\n\tvoid initRegistration(Fn<void(const Data::Passkey::RegisterData&)> done);\n\tvoid registerPasskey(\n\t\tconst Platform::WebAuthn::RegisterResult &result,\n\t\tFn<void()> done);\n\tvoid deletePasskey(\n\t\tconst QString &id,\n\t\tFn<void()> done,\n\t\tFn<void(QString)> fail);\n\t[[nodiscard]] rpl::producer<> requestList();\n\t[[nodiscard]] const std::vector<PasskeyEntry> &list() const;\n\t[[nodiscard]] bool listKnown() const;\n\t[[nodiscard]] bool canRegister() const;\n\t[[nodiscard]] bool possible() const;\n\nprivate:\n\tvoid loadList();\n\n\tconst not_null<Main::Session*> _session;\n\tstd::vector<PasskeyEntry> _passkeys;\n\trpl::event_stream<> _listUpdated;\n\tcrl::time _lastRequestTime = 0;\n\tmtpRequestId _listRequestId = 0;\n\tbool _listKnown = false;\n\n};\n\nvoid InitPasskeyLogin(\n\tMTP::Sender &api,\n\tFn<void(const Data::Passkey::LoginData&)> done);\n\nvoid FinishPasskeyLogin(\n\tMTP::Sender &api,\n\tint initialDc,\n\tconst Platform::WebAuthn::LoginResult &result,\n\tFn<void(const MTPauth_Authorization&)> done,\n\tFn<void(QString)> fail);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/promo_suggestions.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/components/promo_suggestions.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n\nnamespace Data {\nnamespace {\n\nusing UserIds = std::vector<UserId>;\n\nconstexpr auto kTopPromotionInterval = TimeId(60 * 60);\nconstexpr auto kTopPromotionMinDelay = TimeId(10);\n\n[[nodiscard]] CustomSuggestion CustomFromTL(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPPendingSuggestion &r) {\n\treturn CustomSuggestion({\n\t\t.suggestion = qs(r.data().vsuggestion()),\n\t\t.title = Api::ParseTextWithEntities(session, r.data().vtitle()),\n\t\t.description = Api::ParseTextWithEntities(\n\t\t\tsession,\n\t\t\tr.data().vdescription()),\n\t\t.url = qs(r.data().vurl()),\n\t});\n}\n\n} // namespace\n\nPromoSuggestions::PromoSuggestions(\n\tnot_null<Main::Session*> session,\n\tFn<void()> firstPromoLoaded)\n: _session(session)\n, _topPromotionTimer([=] { refreshTopPromotion(); })\n, _firstPromoLoaded(std::move(firstPromoLoaded)) {\n\tCore::App().settings().proxy().connectionTypeValue(\n\t) | rpl::on_next([=] {\n\t\trefreshTopPromotion();\n\t}, _lifetime);\n}\n\nPromoSuggestions::~PromoSuggestions() = default;\n\nvoid PromoSuggestions::refreshTopPromotion() {\n\tconst auto now = base::unixtime::now();\n\tconst auto next = (_topPromotionNextRequestTime != 0)\n\t\t? _topPromotionNextRequestTime\n\t\t: now;\n\tif (_topPromotionRequestId) {\n\t\ttopPromotionDelayed(now, next);\n\t\treturn;\n\t}\n\tconst auto key = [&]() -> std::pair<QString, uint32> {\n\t\tif (!Core::App().settings().proxy().isEnabled()) {\n\t\t\treturn {};\n\t\t}\n\t\tconst auto &proxy = Core::App().settings().proxy().selected();\n\t\tif (proxy.type != MTP::ProxyData::Type::Mtproto) {\n\t\t\treturn {};\n\t\t}\n\t\treturn { proxy.host, proxy.port };\n\t}();\n\tif (_topPromotionKey == key && now < next) {\n\t\ttopPromotionDelayed(now, next);\n\t\treturn;\n\t}\n\t_topPromotionKey = key;\n\t_topPromotionRequestId = _session->api().request(MTPhelp_GetPromoData(\n\t)).done([=](const MTPhelp_PromoData &result) {\n\t\t_topPromotionRequestId = 0;\n\n\t\t_topPromotionNextRequestTime = result.match([&](const auto &data) {\n\t\t\treturn data.vexpires().v;\n\t\t});\n\t\ttopPromotionDelayed(\n\t\t\tbase::unixtime::now(),\n\t\t\t_topPromotionNextRequestTime);\n\n\t\tresult.match([&](const MTPDhelp_promoDataEmpty &data) {\n\t\t\tsetTopPromoted(nullptr, QString(), QString());\n\t\t}, [&](const MTPDhelp_promoData &data) {\n\t\t\t_session->data().processChats(data.vchats());\n\t\t\t_session->data().processUsers(data.vusers());\n\n\t\t\tauto changedPendingSuggestions = false;\n\t\t\tauto pendingSuggestions = ranges::views::all(\n\t\t\t\tdata.vpending_suggestions().v\n\t\t\t) | ranges::views::transform([](const auto &suggestion) {\n\t\t\t\treturn qs(suggestion);\n\t\t\t}) | ranges::to_vector;\n\t\t\tfor (const auto &suggestion : pendingSuggestions) {\n\t\t\t\tif (suggestion == u\"SETUP_LOGIN_EMAIL_NOSKIP\"_q) {\n\t\t\t\t\t_setupEmailState = SetupEmailState::SetupNoSkip;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (suggestion == u\"SETUP_LOGIN_EMAIL\"_q) {\n\t\t\t\t\t_setupEmailState = SetupEmailState::Setup;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!ranges::equal(_pendingSuggestions, pendingSuggestions)) {\n\t\t\t\t_pendingSuggestions = std::move(pendingSuggestions);\n\t\t\t\tchangedPendingSuggestions = true;\n\t\t\t}\n\n\t\t\tauto changedDismissedSuggestions = false;\n\t\t\tfor (const auto &suggestion : data.vdismissed_suggestions().v) {\n\t\t\t\tchangedDismissedSuggestions\n\t\t\t\t\t|= _dismissedSuggestions.emplace(qs(suggestion)).second;\n\t\t\t}\n\n\t\t\tif (const auto peer = data.vpeer()) {\n\t\t\t\tconst auto peerId = peerFromMTP(*peer);\n\t\t\t\tconst auto history = _session->data().history(peerId);\n\t\t\t\tsetTopPromoted(\n\t\t\t\t\thistory,\n\t\t\t\t\tdata.vpsa_type().value_or_empty(),\n\t\t\t\t\tdata.vpsa_message().value_or_empty());\n\t\t\t} else {\n\t\t\t\tsetTopPromoted(nullptr, QString(), QString());\n\t\t\t}\n\n\t\t\tauto changedCustom = false;\n\t\t\tauto custom = data.vcustom_pending_suggestion()\n\t\t\t\t? std::make_optional(\n\t\t\t\t\tCustomFromTL(\n\t\t\t\t\t\t_session,\n\t\t\t\t\t\t*data.vcustom_pending_suggestion()))\n\t\t\t\t: std::nullopt;\n\t\t\tif (_custom != custom) {\n\t\t\t\t_custom = std::move(custom);\n\t\t\t\tchangedCustom = true;\n\t\t\t}\n\n\t\t\tconst auto changedContactBirthdaysLastDayRequest =\n\t\t\t\t_contactBirthdaysLastDayRequest != -1\n\t\t\t\t\t&& _contactBirthdaysLastDayRequest\n\t\t\t\t\t\t!= QDate::currentDate().day();\n\n\t\t\tif (changedPendingSuggestions\n\t\t\t\t|| changedDismissedSuggestions\n\t\t\t\t|| changedCustom\n\t\t\t\t|| changedContactBirthdaysLastDayRequest) {\n\t\t\t\t_refreshed.fire({});\n\t\t\t}\n\t\t});\n\t\tif (_firstPromoLoaded) {\n\t\t\tbase::take(_firstPromoLoaded)();\n\t\t}\n\t}).fail([=] {\n\t\t_topPromotionRequestId = 0;\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto next = _topPromotionNextRequestTime = now\n\t\t\t+ kTopPromotionInterval;\n\t\tif (!_topPromotionTimer.isActive()) {\n\t\t\ttopPromotionDelayed(now, next);\n\t\t}\n\t}).send();\n}\n\nvoid PromoSuggestions::topPromotionDelayed(TimeId now, TimeId next) {\n\t_topPromotionTimer.callOnce(std::min(\n\t\tstd::max(next - now, kTopPromotionMinDelay),\n\t\tkTopPromotionInterval) * crl::time(1000));\n};\n\nrpl::producer<> PromoSuggestions::value() const {\n\treturn _refreshed.events_starting_with({});\n}\n\nvoid PromoSuggestions::setTopPromoted(\n\t\tHistory *promoted,\n\t\tconst QString &type,\n\t\tconst QString &message) {\n\tconst auto changed = (_topPromoted != promoted);\n\tif (!changed\n\t\t&& (!promoted || promoted->topPromotionMessage() == message)) {\n\t\treturn;\n\t}\n\tif (changed) {\n\t\tif (_topPromoted) {\n\t\t\t_topPromoted->cacheTopPromotion(false, QString(), QString());\n\t\t}\n\t}\n\tconst auto old = std::exchange(_topPromoted, promoted);\n\tif (_topPromoted) {\n\t\t_session->data().histories().requestDialogEntry(_topPromoted);\n\t\t_topPromoted->cacheTopPromotion(true, type, message);\n\t\t_topPromoted->requestChatListMessage();\n\t\t_session->changes().historyUpdated(\n\t\t\t_topPromoted,\n\t\t\tHistoryUpdate::Flag::TopPromoted);\n\t}\n\tif (changed && old) {\n\t\t_session->changes().historyUpdated(\n\t\t\told,\n\t\t\tHistoryUpdate::Flag::TopPromoted);\n\t}\n}\n\nbool PromoSuggestions::current(const QString &key) const {\n\tif (key == u\"BIRTHDAY_CONTACTS_TODAY\"_q) {\n\t\tif (_dismissedSuggestions.contains(key)) {\n\t\t\treturn false;\n\t\t} else {\n\t\t\tconst auto known\n\t\t\t\t= PromoSuggestions::knownBirthdaysToday();\n\t\t\tif (!known) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn !known->empty();\n\t\t}\n\t}\n\treturn !_dismissedSuggestions.contains(key)\n\t\t&& ranges::contains(_pendingSuggestions, key);\n}\n\nrpl::producer<> PromoSuggestions::requested(const QString &key) const {\n\treturn value() | rpl::filter([=] { return current(key); });\n}\n\nvoid PromoSuggestions::dismiss(const QString &key) {\n\tif (!_dismissedSuggestions.emplace(key).second) {\n\t\treturn;\n\t}\n\t_session->api().request(MTPhelp_DismissSuggestion(\n\t\tMTP_inputPeerEmpty(),\n\t\tMTP_string(key)\n\t)).send();\n}\n\nvoid PromoSuggestions::dismissSetupEmail(Fn<void()> done) {\n\tauto key = QString();\n\tif (_setupEmailState == SetupEmailState::SettingUp) {\n\t\tkey = u\"SETUP_LOGIN_EMAIL\"_q;\n\t} else if (_setupEmailState == SetupEmailState::SettingUpNoSkip) {\n\t\tkey = u\"SETUP_LOGIN_EMAIL_NOSKIP\"_q;\n\t} else {\n\t\treturn;\n\t}\n\t_session->api().request(MTPhelp_DismissSuggestion(\n\t\tMTP_inputPeerEmpty(),\n\t\tMTP_string(key)\n\t)).done([=](const MTPBool &) {\n\t\t_setupEmailState = SetupEmailState::None;\n\t\tdone();\n\t}).send();\n}\n\nvoid PromoSuggestions::invalidate() {\n\tif (_topPromotionRequestId) {\n\t\t_session->api().request(_topPromotionRequestId).cancel();\n\t}\n\t_topPromotionNextRequestTime = 0;\n\t_topPromotionTimer.callOnce(crl::time(200));\n}\n\nstd::optional<CustomSuggestion> PromoSuggestions::custom() const {\n\treturn (_custom && !_dismissedSuggestions.contains(_custom->suggestion))\n\t\t? _custom\n\t\t: std::nullopt;\n}\n\nvoid PromoSuggestions::requestContactBirthdays(Fn<void()> done, bool force) {\n\tif ((_contactBirthdaysLastDayRequest != -1)\n\t\t&& (_contactBirthdaysLastDayRequest == QDate::currentDate().day())\n\t\t&& !force) {\n\t\treturn done();\n\t}\n\tif (_contactBirthdaysRequestId) {\n\t\t_session->api().request(_contactBirthdaysRequestId).cancel();\n\t}\n\t_contactBirthdaysRequestId = _session->api().request(\n\t\tMTPcontacts_GetBirthdays()\n\t).done([=](const MTPcontacts_ContactBirthdays &result) {\n\t\t_contactBirthdaysRequestId = 0;\n\t\t_contactBirthdaysLastDayRequest = QDate::currentDate().day();\n\t\tauto users = UserIds();\n\t\tauto today = UserIds();\n\t\t_session->data().processUsers(result.data().vusers());\n\t\tfor (const auto &tlContact : result.data().vcontacts().v) {\n\t\t\tconst auto peerId = tlContact.data().vcontact_id().v;\n\t\t\tif (const auto user = _session->data().user(peerId)) {\n\t\t\t\tconst auto &data = tlContact.data().vbirthday().data();\n\t\t\t\tuser->setBirthday(Data::Birthday(\n\t\t\t\t\tdata.vday().v,\n\t\t\t\t\tdata.vmonth().v,\n\t\t\t\t\tdata.vyear().value_or_empty()));\n\t\t\t\tif (user->isSelf()\n\t\t\t\t\t|| user->isInaccessible()\n\t\t\t\t\t|| user->isBlocked()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (Data::IsBirthdayToday(user->birthday())) {\n\t\t\t\t\ttoday.push_back(peerToUser(user->id));\n\t\t\t\t}\n\t\t\t\tusers.push_back(peerToUser(user->id));\n\t\t\t}\n\t\t}\n\t\t_contactBirthdays = std::move(users);\n\t\t_contactBirthdaysToday = std::move(today);\n\t\tdone();\n\t}).fail([=](const MTP::Error &error) {\n\t\t_contactBirthdaysRequestId = 0;\n\t\t_contactBirthdaysLastDayRequest = QDate::currentDate().day();\n\t\t_contactBirthdays = {};\n\t\t_contactBirthdaysToday = {};\n\t\tdone();\n\t}).send();\n}\n\nstd::optional<UserIds> PromoSuggestions::knownContactBirthdays() const {\n\tif ((_contactBirthdaysLastDayRequest == -1)\n\t\t|| (_contactBirthdaysLastDayRequest != QDate::currentDate().day())) {\n\t\treturn std::nullopt;\n\t}\n\treturn _contactBirthdays;\n}\n\nstd::optional<UserIds> PromoSuggestions::knownBirthdaysToday() const {\n\tif ((_contactBirthdaysLastDayRequest == -1)\n\t\t|| (_contactBirthdaysLastDayRequest != QDate::currentDate().day())) {\n\t\treturn std::nullopt;\n\t}\n\treturn _contactBirthdaysToday;\n}\n\nQString PromoSuggestions::SugValidatePassword() {\n\tstatic const auto key = u\"VALIDATE_PASSWORD\"_q;\n\treturn key;\n}\n\nvoid PromoSuggestions::setSetupEmailState(SetupEmailState state) {\n\tif (_setupEmailState != state) {\n\t\t_setupEmailState = state;\n\t\t_setupEmailStateChanges.fire_copy(state);\n\t}\n}\n\nSetupEmailState PromoSuggestions::setupEmailState() const {\n\treturn _setupEmailState;\n}\n\nrpl::producer<SetupEmailState> PromoSuggestions::setupEmailStateValue() const {\n\treturn _setupEmailStateChanges.events_starting_with_copy(_setupEmailState);\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/promo_suggestions.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n\nclass History;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nenum class SetupEmailState {\n\tNone,\n\tSetup,\n\tSetupNoSkip,\n\tSettingUp,\n\tSettingUpNoSkip,\n};\n\nstruct CustomSuggestion final {\n\tQString suggestion;\n\tTextWithEntities title;\n\tTextWithEntities description;\n\tQString url;\n\n\tfriend inline auto operator<=>(\n\t\tconst CustomSuggestion &,\n\t\tconst CustomSuggestion &) = default;\n};\n\nclass PromoSuggestions final {\npublic:\n\texplicit PromoSuggestions(\n\t\tnot_null<Main::Session*> session,\n\t\tFn<void()> firstPromoLoaded = nullptr);\n\t~PromoSuggestions();\n\n\t[[nodiscard]] bool current(const QString &key) const;\n\t[[nodiscard]] std::optional<CustomSuggestion> custom() const;\n\t[[nodiscard]] rpl::producer<> requested(const QString &key) const;\n\tvoid dismiss(const QString &key);\n\tvoid dismissSetupEmail(Fn<void()> done);\n\n\tvoid refreshTopPromotion();\n\n\tvoid invalidate();\n\n\trpl::producer<> value() const;\n\t// Create rpl::producer<> refreshed() const; on memand.\n\n\tvoid requestContactBirthdays(Fn<void()> done, bool force = false);\n\t[[nodiscard]] auto knownContactBirthdays() const\n\t\t-> std::optional<std::vector<UserId>>;\n\t[[nodiscard]] auto knownBirthdaysToday() const\n\t\t-> std::optional<std::vector<UserId>>;\n\n\t[[nodiscard]] static QString SugValidatePassword();\n\n\tvoid setSetupEmailState(SetupEmailState state);\n\t[[nodiscard]] SetupEmailState setupEmailState() const;\n\t[[nodiscard]] rpl::producer<SetupEmailState> setupEmailStateValue() const;\n\nprivate:\n\tvoid setTopPromoted(\n\t\tHistory *promoted,\n\t\tconst QString &type,\n\t\tconst QString &message);\n\n\tvoid topPromotionDelayed(TimeId now, TimeId next);\n\n\tconst not_null<Main::Session*> _session;\n\tbase::flat_set<QString> _dismissedSuggestions;\n\tstd::vector<QString> _pendingSuggestions;\n\tstd::optional<CustomSuggestion> _custom;\n\n\tHistory *_topPromoted = nullptr;\n\n\tmtpRequestId _contactBirthdaysRequestId = 0;\n\tint _contactBirthdaysLastDayRequest = -1;\n\tstd::vector<UserId> _contactBirthdays;\n\tstd::vector<UserId> _contactBirthdaysToday;\n\n\tmtpRequestId _topPromotionRequestId = 0;\n\tstd::pair<QString, uint32> _topPromotionKey;\n\tTimeId _topPromotionNextRequestTime = TimeId(0);\n\tbase::Timer _topPromotionTimer;\n\n\tSetupEmailState _setupEmailState = SetupEmailState::None;\n\n\trpl::event_stream<> _refreshed;\n\trpl::event_stream<SetupEmailState> _setupEmailStateChanges;\n\n\tFn<void()> _firstPromoLoaded;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/recent_peers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/components/recent_peers.h\"\n\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n#include \"storage/serialize_common.h\"\n#include \"storage/serialize_peer.h\"\n#include \"storage/storage_account.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kLimit = 48;\nconstexpr auto kMaxRememberedOpenChats = 32;\n\n} // namespace\n\nRecentPeers::RecentPeers(not_null<Main::Session*> session)\n: _session(session) {\n}\n\nRecentPeers::~RecentPeers() = default;\n\nconst std::vector<not_null<PeerData*>> &RecentPeers::list() const {\n\t_session->local().readSearchSuggestions();\n\n\treturn _list;\n}\n\nrpl::producer<> RecentPeers::updates() const {\n\treturn _updates.events();\n}\n\nvoid RecentPeers::remove(not_null<PeerData*> peer) {\n\tconst auto i = ranges::find(_list, peer);\n\tif (i != end(_list)) {\n\t\t_list.erase(i);\n\t\t_updates.fire({});\n\t}\n\t_session->local().writeSearchSuggestionsDelayed();\n}\n\nvoid RecentPeers::bump(not_null<PeerData*> peer) {\n\t_session->local().readSearchSuggestions();\n\n\tif (!_list.empty() && _list.front() == peer) {\n\t\treturn;\n\t}\n\tauto i = ranges::find(_list, peer);\n\tif (i == end(_list)) {\n\t\t_list.push_back(peer);\n\t\ti = end(_list) - 1;\n\t}\n\tranges::rotate(begin(_list), i, i + 1);\n\t_updates.fire({});\n\n\t_session->local().writeSearchSuggestionsDelayed();\n}\n\nvoid RecentPeers::clear() {\n\t_session->local().readSearchSuggestions();\n\n\t_list.clear();\n\t_updates.fire({});\n\n\t_session->local().writeSearchSuggestionsDelayed();\n}\n\nQByteArray RecentPeers::serialize() const {\n\t_session->local().readSearchSuggestions();\n\n\tif (_list.empty()) {\n\t\treturn {};\n\t}\n\tauto size = 2 * sizeof(quint32); // AppVersion, count\n\tconst auto count = std::min(int(_list.size()), kLimit);\n\tauto &&list = _list | ranges::views::take(count);\n\tfor (const auto &peer : list) {\n\t\tsize += Serialize::peerSize(peer);\n\t}\n\tauto stream = Serialize::ByteArrayWriter(size);\n\tstream\n\t\t<< quint32(AppVersion)\n\t\t<< quint32(count);\n\tfor (const auto &peer : list) {\n\t\tSerialize::writePeer(stream, peer);\n\t}\n\treturn std::move(stream).result();\n}\n\nvoid RecentPeers::applyLocal(QByteArray serialized) {\n\t_list.clear();\n\tif (serialized.isEmpty()) {\n\t\tDEBUG_LOG((\"Suggestions: Bad RecentPeers local, empty.\"));\n\t\treturn;\n\t}\n\tauto stream = Serialize::ByteArrayReader(serialized);\n\tauto streamAppVersion = quint32();\n\tauto count = quint32();\n\tstream >> streamAppVersion >> count;\n\tif (!stream.ok()) {\n\t\tDEBUG_LOG((\"Suggestions: Bad RecentPeers local, not ok.\"));\n\t\treturn;\n\t}\n\tDEBUG_LOG((\"Suggestions: \"\n\t\t\"Start RecentPeers read, count: %1, version: %2.\"\n\t\t).arg(count\n\t\t).arg(streamAppVersion));\n\t_list.reserve(count);\n\tfor (auto i = 0; i != int(count); ++i) {\n\t\tconst auto streamPosition = stream.underlying().device()->pos();\n\t\tconst auto peer = Serialize::readPeer(\n\t\t\t_session,\n\t\t\tstreamAppVersion,\n\t\t\tstream);\n\t\tif (stream.ok() && peer) {\n\t\t\t_list.push_back(peer);\n\t\t} else {\n\t\t\t_list.clear();\n\t\t\tDEBUG_LOG((\"Suggestions: Failed RecentPeers reading %1 / %2.\"\n\t\t\t\t).arg(i + 1\n\t\t\t\t).arg(count));\n\t\t\tDEBUG_LOG((\"Failed bytes: %1.\").arg(\n\t\t\t\tQString::fromUtf8(serialized.mid(streamPosition).toHex())));\n\t\t\treturn;\n\t\t}\n\t}\n\tDEBUG_LOG(\n\t\t(\"Suggestions: RecentPeers read OK, count: %1\").arg(_list.size()));\n}\n\nstd::vector<not_null<Thread*>> RecentPeers::collectChatOpenHistory() const {\n\t_session->local().readSearchSuggestions();\n\treturn _opens;\n}\n\nvoid RecentPeers::chatOpenPush(not_null<Thread*> thread) {\n\tconst auto i = ranges::find(_opens, thread);\n\tif (i == end(_opens)) {\n\t\twhile (_opens.size() >= kMaxRememberedOpenChats) {\n\t\t\t_opens.pop_back();\n\t\t}\n\t\t_opens.insert(begin(_opens), thread);\n\t} else if (i != begin(_opens)) {\n\t\tranges::rotate(begin(_opens), i, i + 1);\n\t}\n}\n\nvoid RecentPeers::chatOpenRemove(not_null<Thread*> thread) {\n\t_opens.erase(ranges::remove(_opens, thread), end(_opens));\n}\n\nvoid RecentPeers::chatOpenKeepUserpics(\n\t\tbase::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> userpics) {\n\t_chatOpenUserpicsCache = std::move(userpics);\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/recent_peers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/userpic_view.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nclass Thread;\n\nclass RecentPeers final {\npublic:\n\texplicit RecentPeers(not_null<Main::Session*> session);\n\t~RecentPeers();\n\n\t[[nodiscard]] const std::vector<not_null<PeerData*>> &list() const;\n\t[[nodiscard]] rpl::producer<> updates() const;\n\n\tvoid remove(not_null<PeerData*> peer);\n\tvoid bump(not_null<PeerData*> peer);\n\tvoid clear();\n\n\t[[nodiscard]] QByteArray serialize() const;\n\tvoid applyLocal(QByteArray serialized);\n\n\t[[nodiscard]] auto collectChatOpenHistory() const\n\t\t-> std::vector<not_null<Thread*>>;\n\tvoid chatOpenPush(not_null<Thread*> thread);\n\tvoid chatOpenRemove(not_null<Thread*> thread);\n\tvoid chatOpenKeepUserpics(\n\t\tbase::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> userpics);\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\n\tstd::vector<not_null<PeerData*>> _list;\n\tstd::vector<not_null<Thread*>> _opens;\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tUi::PeerUserpicView> _chatOpenUserpicsCache;\n\n\trpl::event_stream<> _updates;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/recent_shared_media_gifts.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/components/recent_shared_media_gifts.h\"\n\n#include \"api/api_credits.h\" // InputSavedStarGiftId\n#include \"api/api_premium.h\"\n#include \"apiwrap.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"data/data_document.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kReloadThreshold = 60 * crl::time(1000);\nconstexpr auto kMaxGifts = 3;\nconstexpr auto kMaxPinnedGifts = 6;\n\n} // namespace\n\nRecentSharedMediaGifts::RecentSharedMediaGifts(\n\tnot_null<Main::Session*> session)\n: _session(session) {\n}\n\nRecentSharedMediaGifts::~RecentSharedMediaGifts() = default;\n\nstd::vector<Data::SavedStarGift> RecentSharedMediaGifts::filterGifts(\n\t\tconst std::deque<Data::SavedStarGift> &gifts,\n\t\tbool onlyPinnedToTop) {\n\tauto result = std::vector<Data::SavedStarGift>();\n\tconst auto maxCount = onlyPinnedToTop ? kMaxPinnedGifts : kMaxGifts;\n\tfor (const auto &gift : gifts) {\n\t\tif (!onlyPinnedToTop || gift.pinned) {\n\t\t\tresult.push_back(gift);\n\t\t\tif (result.size() >= maxCount) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid RecentSharedMediaGifts::request(\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void(std::vector<SavedStarGift>)> done,\n\t\tbool onlyPinnedToTop) {\n\tconst auto it = _recent.find(peer->id);\n\tif (it != _recent.end()) {\n\t\tauto &entry = it->second;\n\t\tif (entry.lastRequestTime\n\t\t\t&& entry.lastRequestTime + kReloadThreshold > crl::now()) {\n\t\t\tdone(filterGifts(entry.gifts, onlyPinnedToTop));\n\t\t\treturn;\n\t\t}\n\t\tif (entry.requestId) {\n\t\t\tentry.pendingCallbacks.push_back([=] {\n\t\t\t\tconst auto it = _recent.find(peer->id);\n\t\t\t\tif (it != _recent.end()) {\n\t\t\t\t\tdone(filterGifts(it->second.gifts, onlyPinnedToTop));\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t}\n\n\t_recent[peer->id].requestId = peer->session().api().request(\n\t\tMTPpayments_GetSavedStarGifts(\n\t\t\tMTP_flags(0),\n\t\t\tpeer->input(),\n\t\t\tMTP_int(0), // collection_id\n\t\t\tMTP_string(QString()),\n\t\t\tMTP_int(kMaxPinnedGifts)\n\t)).done([=](const MTPpayments_SavedStarGifts &result) {\n\t\tconst auto &data = result.data();\n\t\tconst auto owner = &peer->owner();\n\t\towner->processUsers(data.vusers());\n\t\towner->processChats(data.vchats());\n\t\tauto &entry = _recent[peer->id];\n\t\tentry.lastRequestTime = crl::now();\n\t\tentry.requestId = 0;\n\t\tentry.gifts.clear();\n\n\t\tfor (const auto &gift : data.vgifts().v) {\n\t\t\tif (auto parsed = Api::FromTL(peer, gift)) {\n\t\t\t\tentry.gifts.push_back(std::move(*parsed));\n\t\t\t}\n\t\t}\n\n\t\tdone(filterGifts(entry.gifts, onlyPinnedToTop));\n\t\tfor (const auto &callback : entry.pendingCallbacks) {\n\t\t\tcallback();\n\t\t}\n\t\tentry.pendingCallbacks.clear();\n\t}).send();\n}\n\nvoid RecentSharedMediaGifts::clearLastRequestTime(\n\t\tnot_null<PeerData*> peer) {\n\tconst auto it = _recent.find(peer->id);\n\tif (it != _recent.end()) {\n\t\tit->second.lastRequestTime = 0;\n\t}\n}\n\nvoid RecentSharedMediaGifts::updatePinnedOrder(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tconst std::vector<SavedStarGift> &gifts,\n\t\tconst std::vector<Data::SavedStarGiftId> &manageIds,\n\t\tFn<void()> done) {\n\tauto inputs = QVector<MTPInputSavedStarGift>();\n\tinputs.reserve(manageIds.size());\n\tfor (const auto &id : manageIds) {\n\t\tinputs.push_back(Api::InputSavedStarGiftId(id));\n\t}\n\n\t_session->api().request(MTPpayments_ToggleStarGiftsPinnedToTop(\n\t\tpeer->input(),\n\t\tMTP_vector<MTPInputSavedStarGift>(std::move(inputs))\n\t)).done([=] {\n\t\tauto result = std::deque<SavedStarGift>();\n\t\tfor (const auto &id : manageIds) {\n\t\t\tfor (const auto &gift : gifts) {\n\t\t\t\tif (gift.manageId == id) {\n\t\t\t\t\tresult.push_back(gift);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t_recent[peer->id].gifts = std::move(result);\n\t\tif (done) {\n\t\t\tdone();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (!Ui::ShowGiftErrorToast(show, error)) {\n\t\t\tshow->showToast(error.type());\n\t\t}\n\t}).send();\n}\n\nvoid RecentSharedMediaGifts::togglePinned(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tconst Data::SavedStarGiftId &manageId,\n\t\tbool pinned,\n\t\tstd::shared_ptr<Data::UniqueGift> uniqueData,\n\t\tstd::shared_ptr<Data::UniqueGift> replacingData) {\n\tconst auto performToggle = [=](const std::vector<SavedStarGift> &gifts) {\n\t\tconst auto limit = _session->appConfig().pinnedGiftsLimit();\n\t\tauto manageIds = std::vector<Data::SavedStarGiftId>();\n\n\t\tif (pinned) {\n\t\t\tfor (const auto &gift : gifts) {\n\t\t\t\tif (gift.pinned && gift.manageId != manageId) {\n\t\t\t\t\tmanageIds.push_back(gift.manageId);\n\t\t\t\t\tif (manageIds.size() >= limit - 1) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tmanageIds.push_back(manageId);\n\t\t} else {\n\t\t\tfor (const auto &gift : gifts) {\n\t\t\t\tif (gift.pinned && gift.manageId != manageId) {\n\t\t\t\t\tmanageIds.push_back(gift.manageId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst auto updateLocal = [=] {\n\t\t\t\tusing GiftAction = Data::GiftUpdate::Action;\n\t\t\t\t_session->data().notifyGiftUpdate({\n\t\t\t\t\t.id = manageId,\n\t\t\t\t\t.action = (pinned ? GiftAction::Pin : GiftAction::Unpin),\n\t\t\t\t});\n\t\t\t\tif (pinned) {\n\t\t\t\t\tshow->showToast({\n\t\t\t\t\t\t.title = (uniqueData\n\t\t\t\t\t\t\t? tr::lng_gift_pinned_done_title(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_gift,\n\t\t\t\t\t\t\t\tData::UniqueGiftName(*uniqueData))\n\t\t\t\t\t\t\t: QString()),\n\t\t\t\t\t\t.text = (replacingData\n\t\t\t\t\t\t\t? tr::lng_gift_pinned_done_replaced(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_gift,\n\t\t\t\t\t\t\t\tTextWithEntities{\n\t\t\t\t\t\t\t\t\tData::UniqueGiftName(*replacingData),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t\t\t: tr::lng_gift_pinned_done(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\ttr::marked)),\n\t\t\t\t\t\t.duration = Ui::Toast::kDefaultDuration * 2,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tif (!pinned) {\n\t\t\t\tupdatePinnedOrder(show, peer, gifts, manageIds, updateLocal);\n\t\t\t} else {\n\t\t\t\t_session->api().request(MTPpayments_GetSavedStarGift(\n\t\t\t\t\tMTP_vector<MTPInputSavedStarGift>(\n\t\t\t\t\t\t1,\n\t\t\t\t\t\tApi::InputSavedStarGiftId(manageId))\n\t\t\t\t)).done([=](const MTPpayments_SavedStarGifts &result) {\n\t\t\t\t\tconst auto &tlGift = result.data().vgifts().v.front();\n\t\t\t\t\tif (auto parsed = Api::FromTL(peer, tlGift)) {\n\t\t\t\t\t\tauto updatedGifts = std::vector<SavedStarGift>();\n\t\t\t\t\t\tfor (const auto &gift : gifts) {\n\t\t\t\t\t\t\tif (gift.pinned && gift.manageId != manageId) {\n\t\t\t\t\t\t\t\tupdatedGifts.push_back(gift);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tparsed->pinned = true;\n\t\t\t\t\t\tupdatedGifts.push_back(*parsed);\n\t\t\t\t\t\tupdatePinnedOrder(\n\t\t\t\t\t\t\tshow,\n\t\t\t\t\t\t\tpeer,\n\t\t\t\t\t\t\tupdatedGifts,\n\t\t\t\t\t\t\tmanageIds,\n\t\t\t\t\t\t\tupdateLocal);\n\t\t\t\t\t}\n\t\t\t\t}).send();\n\t\t\t}\n\t};\n\n\trequest(peer, performToggle, true);\n}\n\nvoid RecentSharedMediaGifts::reorderPinned(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tint oldPosition,\n\t\tint newPosition) {\n\tconst auto performReorder = [=](const std::vector<SavedStarGift> &gifts) {\n\t\tif (oldPosition < 0 || oldPosition >= gifts.size()\n\t\t\t|| newPosition < 0 || newPosition >= gifts.size()\n\t\t\t|| oldPosition == newPosition) {\n\t\t\treturn;\n\t\t}\n\n\t\tauto manageIds = std::vector<Data::SavedStarGiftId>();\n\t\tmanageIds.reserve(gifts.size());\n\t\tfor (const auto &gift : gifts) {\n\t\t\tmanageIds.push_back(gift.manageId);\n\t\t}\n\t\tbase::reorder(manageIds, oldPosition, newPosition);\n\n\t\tupdatePinnedOrder(show, peer, gifts, manageIds, nullptr);\n\t};\n\n\trequest(peer, performReorder, true);\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/recent_shared_media_gifts.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_star_gift.h\"\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nclass RecentSharedMediaGifts final {\npublic:\n\texplicit RecentSharedMediaGifts(not_null<Main::Session*> session);\n\t~RecentSharedMediaGifts();\n\n\tvoid request(\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void(std::vector<Data::SavedStarGift>)> done,\n\t\tbool onlyPinnedToTop = false);\n\n\tvoid clearLastRequestTime(not_null<PeerData*> peer);\n\n\tvoid togglePinned(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tconst Data::SavedStarGiftId &manageId,\n\t\tbool pinned,\n\t\tstd::shared_ptr<Data::UniqueGift> uniqueData,\n\t\tstd::shared_ptr<Data::UniqueGift> replacingData = nullptr);\n\n\tvoid reorderPinned(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tint oldPosition,\n\t\tint newPosition);\n\nprivate:\n\tvoid updatePinnedOrder(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tconst std::vector<SavedStarGift> &gifts,\n\t\tconst std::vector<Data::SavedStarGiftId> &manageIds,\n\t\tFn<void()> done);\n\n\t[[nodiscard]] std::vector<Data::SavedStarGift> filterGifts(\n\t\tconst std::deque<SavedStarGift> &gifts,\n\t\tbool onlyPinnedToTop);\n\n\tstruct Entry {\n\t\tstd::deque<SavedStarGift> gifts;\n\t\tcrl::time lastRequestTime = 0;\n\t\tmtpRequestId requestId = 0;\n\t\tstd::vector<Fn<void()>> pendingCallbacks;\n\t};\n\n\tconst not_null<Main::Session*> _session;\n\n\tbase::flat_map<PeerId, Entry> _recent;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/scheduled_messages.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/components/scheduled_messages.h\"\n\n#include \"base/unixtime.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"api/api_hash.h\"\n#include \"api/api_text_entities.h\"\n#include \"main/main_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"apiwrap.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kRequestTimeLimit = 60 * crl::time(1000);\n\n[[nodiscard]] MsgId RemoteToLocalMsgId(MsgId id) {\n\tExpects(IsServerMsgId(id));\n\n\treturn ServerMaxMsgId + id + 1;\n}\n\n[[nodiscard]] MsgId LocalToRemoteMsgId(MsgId id) {\n\tExpects(IsScheduledMsgId(id));\n\n\treturn (id - ServerMaxMsgId - 1);\n}\n\n[[nodiscard]] bool TooEarlyForRequest(crl::time received) {\n\treturn (received > 0) && (received + kRequestTimeLimit > crl::now());\n}\n\n[[nodiscard]] bool HasScheduledDate(not_null<HistoryItem*> item) {\n\treturn (item->date() != Api::kScheduledUntilOnlineTimestamp)\n\t\t&& (item->date() > base::unixtime::now());\n}\n\n[[nodiscard]] MTPMessage PrepareMessage(const MTPMessage &message) {\n\treturn message.match([&](const MTPDmessageEmpty &data) {\n\t\treturn MTP_messageEmpty(\n\t\t\tdata.vflags(),\n\t\t\tdata.vid(),\n\t\t\tdata.vpeer_id() ? *data.vpeer_id() : MTPPeer());\n\t}, [&](const MTPDmessageService &data) {\n\t\treturn MTP_messageService(\n\t\t\tMTP_flags(data.vflags().v\n\t\t\t\t| MTPDmessageService::Flag(\n\t\t\t\t\tMTPDmessage::Flag::f_from_scheduled)),\n\t\t\tdata.vid(),\n\t\t\tdata.vfrom_id() ? *data.vfrom_id() : MTPPeer(),\n\t\t\tdata.vpeer_id(),\n\t\t\tdata.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(),\n\t\t\tdata.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(),\n\t\t\tdata.vdate(),\n\t\t\tdata.vaction(),\n\t\t\tdata.vreactions() ? *data.vreactions() : MTPMessageReactions(),\n\t\t\tMTP_int(data.vttl_period().value_or_empty()));\n\t}, [&](const MTPDmessage &data) {\n\t\treturn MTP_message(\n\t\t\tMTP_flags(data.vflags().v | MTPDmessage::Flag::f_from_scheduled),\n\t\t\tdata.vid(),\n\t\t\tdata.vfrom_id() ? *data.vfrom_id() : MTPPeer(),\n\t\t\tMTPint(), // from_boosts_applied\n\t\t\tMTPstring(), // from_rank\n\t\t\tdata.vpeer_id(),\n\t\t\tdata.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(),\n\t\t\tdata.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(),\n\t\t\tMTP_long(data.vvia_bot_id().value_or_empty()),\n\t\t\tMTP_long(data.vvia_business_bot_id().value_or_empty()),\n\t\t\tdata.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(),\n\t\t\tdata.vdate(),\n\t\t\tdata.vmessage(),\n\t\t\tdata.vmedia() ? *data.vmedia() : MTPMessageMedia(),\n\t\t\tdata.vreply_markup() ? *data.vreply_markup() : MTPReplyMarkup(),\n\t\t\t(data.ventities()\n\t\t\t\t? *data.ventities()\n\t\t\t\t: MTPVector<MTPMessageEntity>()),\n\t\t\tMTP_int(data.vviews().value_or_empty()),\n\t\t\tMTP_int(data.vforwards().value_or_empty()),\n\t\t\tdata.vreplies() ? *data.vreplies() : MTPMessageReplies(),\n\t\t\tMTP_int(data.vedit_date().value_or_empty()),\n\t\t\tMTP_bytes(data.vpost_author().value_or_empty()),\n\t\t\tMTP_long(data.vgrouped_id().value_or_empty()),\n\t\t\tMTPMessageReactions(),\n\t\t\tMTPVector<MTPRestrictionReason>(),\n\t\t\tMTP_int(data.vttl_period().value_or_empty()),\n\t\t\tMTPint(), // quick_reply_shortcut_id\n\t\t\tMTP_long(data.veffect().value_or_empty()), // effect\n\t\t\tdata.vfactcheck() ? *data.vfactcheck() : MTPFactCheck(),\n\t\t\tMTP_int(data.vreport_delivery_until_date().value_or_empty()),\n\t\t\tMTP_long(data.vpaid_message_stars().value_or_empty()),\n\t\t\t(data.vsuggested_post()\n\t\t\t\t? *data.vsuggested_post()\n\t\t\t\t: MTPSuggestedPost()),\n\t\t\tMTP_int(data.vschedule_repeat_period().value_or_empty()),\n\t\t\tMTP_string(qs(data.vsummary_from_language().value_or_empty())));\n\t});\n}\n\n} // namespace\n\nbool IsScheduledMsgId(MsgId id) {\n\treturn (id > ServerMaxMsgId) && (id < ScheduledMaxMsgId);\n}\n\nScheduledMessages::ScheduledMessages(not_null<Main::Session*> session)\n: _session(session)\n, _clearTimer([=] { clearOldRequests(); }) {\n\t_session->data().itemRemoved(\n\t) | rpl::filter([](not_null<const HistoryItem*> item) {\n\t\treturn item->isScheduled();\n\t}) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\tremove(item);\n\t}, _lifetime);\n}\n\nScheduledMessages::~ScheduledMessages() {\n\tExpects(_data.empty());\n\tExpects(_requests.empty());\n}\n\nvoid ScheduledMessages::clear() {\n\t_lifetime.destroy();\n\tfor (const auto &request : base::take(_requests)) {\n\t\t_session->api().request(request.second.requestId).cancel();\n\t}\n\tbase::take(_data);\n}\n\nvoid ScheduledMessages::clearOldRequests() {\n\tconst auto now = crl::now();\n\twhile (true) {\n\t\tconst auto i = ranges::find_if(_requests, [&](const auto &value) {\n\t\t\tconst auto &request = value.second;\n\t\t\treturn !request.requestId\n\t\t\t\t&& (request.lastReceived + kRequestTimeLimit <= now);\n\t\t});\n\t\tif (i == end(_requests)) {\n\t\t\tbreak;\n\t\t}\n\t\t_requests.erase(i);\n\t}\n}\n\nMsgId ScheduledMessages::localMessageId(MsgId remoteId) const {\n\treturn RemoteToLocalMsgId(remoteId);\n}\n\nMsgId ScheduledMessages::lookupId(not_null<const HistoryItem*> item) const {\n\tExpects(item->isScheduled());\n\tExpects(!item->isSending());\n\tExpects(!item->hasFailed());\n\n\treturn LocalToRemoteMsgId(item->id);\n}\n\nHistoryItem *ScheduledMessages::lookupItem(PeerId peer, MsgId msg) const {\n\tconst auto history = _session->data().historyLoaded(peer);\n\tif (!history) {\n\t\treturn nullptr;\n\t}\n\n\tconst auto i = _data.find(history);\n\tif (i == end(_data)) {\n\t\treturn nullptr;\n\t}\n\n\tconst auto &items = i->second.items;\n\tconst auto j = ranges::find_if(items, [&](auto &item) {\n\t\treturn item->id == msg;\n\t});\n\tif (j == end(items)) {\n\t\treturn nullptr;\n\t}\n\treturn (*j).get();\n}\n\nHistoryItem *ScheduledMessages::lookupItem(FullMsgId itemId) const {\n\treturn lookupItem(itemId.peer, itemId.msg);\n}\n\nint ScheduledMessages::count(not_null<History*> history) const {\n\tconst auto i = _data.find(history);\n\treturn (i != end(_data)) ? i->second.items.size() : 0;\n}\n\nbool ScheduledMessages::hasFor(not_null<Data::ForumTopic*> topic) const {\n\tconst auto i = _data.find(topic->owningHistory());\n\tif (i == end(_data)) {\n\t\treturn false;\n\t}\n\treturn ranges::any_of(i->second.items, [&](const OwnedItem &item) {\n\t\treturn item->topic() == topic;\n\t});\n}\n\nvoid ScheduledMessages::sendNowSimpleMessage(\n\t\tconst MTPDupdateShortSentMessage &update,\n\t\tnot_null<HistoryItem*> local) {\n\tExpects(local->isSending());\n\tExpects(local->isScheduled());\n\n\tif (HasScheduledDate(local)) {\n\t\tLOG((\"Error: trying to put to history a new local message, \"\n\t\t\t\"that has scheduled date.\"));\n\t\treturn;\n\t}\n\n\t// When the user sends a text message scheduled until online\n\t// while the recipient is already online, the server sends\n\t// updateShortSentMessage to the client and the client calls this method.\n\t// Since such messages can only be sent to recipients,\n\t// we know for sure that a message can't have fields such as the author,\n\t// views count, etc.\n\n\tconst auto history = local->history();\n\tauto action = Api::SendAction(history);\n\taction.replyTo = local->replyTo();\n\tconst auto replyHeader = NewMessageReplyHeader(action);\n\tconst auto localFlags = NewMessageFlags(history->peer)\n\t\t& ~MessageFlag::BeingSent;\n\tconst auto flags = MTPDmessage::Flag::f_entities\n\t\t| MTPDmessage::Flag::f_from_id\n\t\t| (action.replyTo\n\t\t\t? MTPDmessage::Flag::f_reply_to\n\t\t\t: MTPDmessage::Flag(0))\n\t\t| (update.vttl_period()\n\t\t\t? MTPDmessage::Flag::f_ttl_period\n\t\t\t: MTPDmessage::Flag(0))\n\t\t| ((localFlags & MessageFlag::Outgoing)\n\t\t\t? MTPDmessage::Flag::f_out\n\t\t\t: MTPDmessage::Flag(0))\n\t\t| (local->effectId()\n\t\t\t? MTPDmessage::Flag::f_effect\n\t\t\t: MTPDmessage::Flag(0));\n\tconst auto views = 1;\n\tconst auto forwards = 0;\n\thistory->addNewMessage(\n\t\tupdate.vid().v,\n\t\tMTP_message(\n\t\t\tMTP_flags(flags),\n\t\t\tupdate.vid(),\n\t\t\tpeerToMTP(local->from()->id),\n\t\t\tMTPint(), // from_boosts_applied\n\t\t\tMTPstring(), // from_rank\n\t\t\tpeerToMTP(history->peer->id),\n\t\t\tMTPPeer(), // saved_peer_id\n\t\t\tMTPMessageFwdHeader(),\n\t\t\tMTPlong(), // via_bot_id\n\t\t\tMTPlong(), // via_business_bot_id\n\t\t\treplyHeader,\n\t\t\tupdate.vdate(),\n\t\t\tMTP_string(local->originalText().text),\n\t\t\tMTP_messageMediaEmpty(),\n\t\t\tMTPReplyMarkup(),\n\t\t\tApi::EntitiesToMTP(\n\t\t\t\t&history->session(),\n\t\t\t\tlocal->originalText().entities),\n\t\t\tMTP_int(views),\n\t\t\tMTP_int(forwards),\n\t\t\tMTPMessageReplies(),\n\t\t\tMTPint(), // edit_date\n\t\t\tMTP_string(),\n\t\t\tMTPlong(),\n\t\t\tMTPMessageReactions(),\n\t\t\tMTPVector<MTPRestrictionReason>(),\n\t\t\tMTP_int(update.vttl_period().value_or_empty()),\n\t\t\tMTPint(), // quick_reply_shortcut_id\n\t\t\tMTP_long(local->effectId()), // effect\n\t\t\tMTPFactCheck(),\n\t\t\tMTPint(), // report_delivery_until_date\n\t\t\tMTPlong(), // paid_message_stars\n\t\t\tMTPSuggestedPost(),\n\t\t\tMTPint(), // schedule_repeat_period\n\t\t\tMTPstring()), // summary_from_language\n\t\tlocalFlags,\n\t\tNewMessageType::Unread);\n\n\tlocal->destroy();\n}\n\nvoid ScheduledMessages::apply(const MTPDupdateNewScheduledMessage &update) {\n\tconst auto &message = update.vmessage();\n\tconst auto peer = PeerFromMessage(message);\n\tif (!peer) {\n\t\treturn;\n\t}\n\tconst auto history = _session->data().historyLoaded(peer);\n\tif (!history) {\n\t\treturn;\n\t}\n\tauto &list = _data[history];\n\tappend(history, list, message);\n\tsort(list);\n\t_updates.fire_copy(history);\n}\n\nvoid ScheduledMessages::checkEntitiesAndUpdate(const MTPDmessage &data) {\n\t// When the user sends a message with a media scheduled until online\n\t// while the recipient is already online, or scheduled message\n\t// is already due and is sent immediately, the server sends\n\t// updateNewMessage or updateNewChannelMessage to the client\n\t// and the client calls this method.\n\n\tconst auto peer = peerFromMTP(data.vpeer_id());\n\tconst auto history = _session->data().historyLoaded(peer);\n\tif (!history) {\n\t\treturn;\n\t}\n\n\tconst auto i = _data.find(history);\n\tif (i == end(_data)) {\n\t\treturn;\n\t}\n\n\tconst auto &itemMap = i->second.itemById;\n\tconst auto j = itemMap.find(data.vid().v);\n\tif (j == end(itemMap)) {\n\t\treturn;\n\t}\n\n\tconst auto existing = j->second;\n\tif (!HasScheduledDate(existing)) {\n\t\t// Destroy a local message, that should be in history.\n\t\texisting->updateSentContent({\n\t\t\tqs(data.vmessage()),\n\t\t\tApi::EntitiesFromMTP(_session, data.ventities().value_or_empty())\n\t\t}, data.vmedia());\n\t\texisting->updateReplyMarkup(\n\t\t\tHistoryMessageMarkupData(data.vreply_markup()));\n\t\texisting->updateForwardedInfo(data.vfwd_from());\n\t\t_session->data().requestItemTextRefresh(existing);\n\n\t\texisting->destroy();\n\t}\n}\n\nvoid ScheduledMessages::apply(\n\t\tconst MTPDupdateDeleteScheduledMessages &update) {\n\tconst auto peer = peerFromMTP(update.vpeer());\n\tif (!peer) {\n\t\treturn;\n\t}\n\tconst auto history = _session->data().historyLoaded(peer);\n\tif (!history) {\n\t\treturn;\n\t}\n\tauto i = _data.find(history);\n\tif (i == end(_data)) {\n\t\treturn;\n\t}\n\tconst auto sent = update.vsent_messages();\n\tconst auto &ids = update.vmessages().v;\n\tfor (auto k = 0, count = int(ids.size()); k != count; ++k) {\n\t\tconst auto id = ids[k].v;\n\t\tconst auto &list = i->second;\n\t\tconst auto j = list.itemById.find(id);\n\t\tif (j != end(list.itemById)) {\n\t\t\tif (sent && k < sent->v.size()) {\n\t\t\t\tconst auto &sentId = sent->v[k];\n\t\t\t\t_session->data().sentFromScheduled({\n\t\t\t\t\t.item = j->second,\n\t\t\t\t\t.sentId = sentId.v,\n\t\t\t\t});\n\t\t\t}\n\t\t\tj->second->destroy();\n\t\t\ti = _data.find(history);\n\t\t\tif (i == end(_data)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\t_updates.fire_copy(history);\n}\n\nvoid ScheduledMessages::apply(\n\t\tconst MTPDupdateMessageID &update,\n\t\tnot_null<HistoryItem*> local) {\n\tconst auto id = update.vid().v;\n\tconst auto i = _data.find(local->history());\n\tAssert(i != end(_data));\n\tauto &list = i->second;\n\tconst auto j = list.itemById.find(id);\n\tif (j != end(list.itemById) || !IsServerMsgId(id)) {\n\t\tlocal->destroy();\n\t} else {\n\t\tAssert(!list.itemById.contains(local->id));\n\t\tlocal->setRealId(localMessageId(id));\n\t\tlist.itemById.emplace(id, local);\n\t}\n}\n\nvoid ScheduledMessages::appendSending(not_null<HistoryItem*> item) {\n\tExpects(item->isSending());\n\tExpects(item->isScheduled());\n\n\tconst auto history = item->history();\n\tauto &list = _data[history];\n\tlist.items.emplace_back(item);\n\tsort(list);\n\t_updates.fire_copy(history);\n}\n\nvoid ScheduledMessages::removeSending(not_null<HistoryItem*> item) {\n\tExpects(item->isSending() || item->hasFailed());\n\tExpects(item->isScheduled());\n\n\titem->destroy();\n}\n\nrpl::producer<> ScheduledMessages::updates(not_null<History*> history) {\n\trequest(history);\n\n\treturn _updates.events(\n\t) | rpl::filter([=](not_null<History*> value) {\n\t\treturn (value == history);\n\t}) | rpl::to_empty;\n}\n\nData::MessagesSlice ScheduledMessages::list(\n\t\tnot_null<History*> history) const {\n\tauto result = Data::MessagesSlice();\n\tconst auto i = _data.find(history);\n\tif (i == end(_data)) {\n\t\tconst auto i = _requests.find(history);\n\t\tif (i == end(_requests)) {\n\t\t\treturn result;\n\t\t}\n\t\tresult.fullCount = result.skippedAfter = result.skippedBefore = 0;\n\t\treturn result;\n\t}\n\tconst auto &list = i->second.items;\n\tresult.skippedAfter = result.skippedBefore = 0;\n\tresult.fullCount = int(list.size());\n\tresult.ids = ranges::views::all(\n\t\tlist\n\t) | ranges::views::transform(\n\t\t&HistoryItem::fullId\n\t) | ranges::to_vector;\n\treturn result;\n}\n\nData::MessagesSlice ScheduledMessages::list(\n\t\tnot_null<const Data::ForumTopic*> topic) const {\n\tauto result = Data::MessagesSlice();\n\tconst auto i = _data.find(topic->Data::Thread::owningHistory());\n\tif (i == end(_data)) {\n\t\tconst auto i = _requests.find(topic->Data::Thread::owningHistory());\n\t\tif (i == end(_requests)) {\n\t\t\treturn result;\n\t\t}\n\t\tresult.fullCount = result.skippedAfter = result.skippedBefore = 0;\n\t\treturn result;\n\t}\n\tconst auto &list = i->second.items;\n\tresult.skippedAfter = result.skippedBefore = 0;\n\tresult.fullCount = int(list.size());\n\tresult.ids = ranges::views::all(\n\t\tlist\n\t) | ranges::views::filter([&](const OwnedItem &item) {\n\t\treturn item->topic() == topic;\n\t}) | ranges::views::transform(\n\t\t&HistoryItem::fullId\n\t) | ranges::to_vector;\n\treturn result;\n}\n\nvoid ScheduledMessages::request(not_null<History*> history) {\n\tconst auto peer = history->peer;\n\tif (peer->isBroadcast() && !Data::CanSendAnything(peer)) {\n\t\treturn;\n\t}\n\tauto &request = _requests[history];\n\tif (request.requestId || TooEarlyForRequest(request.lastReceived)) {\n\t\treturn;\n\t}\n\tconst auto i = _data.find(history);\n\tconst auto hash = (i != end(_data))\n\t\t? countListHash(i->second)\n\t\t: uint64(0);\n\trequest.requestId = _session->api().request(\n\t\tMTPmessages_GetScheduledHistory(peer->input(), MTP_long(hash))\n\t).done([=](const MTPmessages_Messages &result) {\n\t\tparse(history, result);\n\t}).fail([=] {\n\t\t_requests.remove(history);\n\t}).send();\n}\n\nvoid ScheduledMessages::parse(\n\t\tnot_null<History*> history,\n\t\tconst MTPmessages_Messages &list) {\n\tauto &request = _requests[history];\n\trequest.lastReceived = crl::now();\n\trequest.requestId = 0;\n\tif (!_clearTimer.isActive()) {\n\t\t_clearTimer.callOnce(kRequestTimeLimit * 2);\n\t}\n\n\tlist.match([&](const MTPDmessages_messagesNotModified &data) {\n\t}, [&](const auto &data) {\n\t\t_session->data().processUsers(data.vusers());\n\t\t_session->data().processChats(data.vchats());\n\n\t\tconst auto &messages = data.vmessages().v;\n\t\tif (messages.isEmpty()) {\n\t\t\tclearNotSending(history);\n\t\t\treturn;\n\t\t}\n\t\tauto received = base::flat_set<not_null<HistoryItem*>>();\n\t\tauto clear = base::flat_set<not_null<HistoryItem*>>();\n\t\tauto &list = _data.emplace(history, List()).first->second;\n\t\tfor (const auto &message : messages) {\n\t\t\tif (const auto item = append(history, list, message)) {\n\t\t\t\treceived.emplace(item);\n\t\t\t}\n\t\t}\n\t\tfor (const auto &owned : list.items) {\n\t\t\tconst auto item = owned.get();\n\t\t\tif (!item->isSending() && !received.contains(item)) {\n\t\t\t\tclear.emplace(item);\n\t\t\t}\n\t\t}\n\t\tupdated(history, received, clear);\n\t});\n}\n\nHistoryItem *ScheduledMessages::append(\n\t\tnot_null<History*> history,\n\t\tList &list,\n\t\tconst MTPMessage &message) {\n\tconst auto id = message.match([&](const auto &data) {\n\t\treturn data.vid().v;\n\t});\n\tconst auto i = list.itemById.find(id);\n\tif (i != end(list.itemById)) {\n\t\tconst auto existing = i->second;\n\t\tmessage.match([&](const MTPDmessage &data) {\n\t\t\t// Scheduled messages never have an edit date,\n\t\t\t// so if we receive a flag about it,\n\t\t\t// probably this message was edited.\n\t\t\tif (data.is_edit_hide()) {\n\t\t\t\texisting->applyEdition(HistoryMessageEdition(_session, data));\n\t\t\t} else {\n\t\t\t\texisting->updateSentContent({\n\t\t\t\t\tqs(data.vmessage()),\n\t\t\t\t\tApi::EntitiesFromMTP(\n\t\t\t\t\t\t_session,\n\t\t\t\t\t\tdata.ventities().value_or_empty())\n\t\t\t\t}, data.vmedia());\n\t\t\t\texisting->updateReplyMarkup(\n\t\t\t\t\tHistoryMessageMarkupData(data.vreply_markup()));\n\t\t\t\texisting->updateForwardedInfo(data.vfwd_from());\n\t\t\t}\n\t\t\texisting->updateDate(data.vdate().v);\n\t\t\thistory->owner().requestItemTextRefresh(existing);\n\t\t}, [&](const auto &data) {});\n\t\treturn existing;\n\t}\n\n\tif (!IsServerMsgId(id)) {\n\t\tLOG((\"API Error: Bad id in scheduled messages: %1.\").arg(id));\n\t\treturn nullptr;\n\t}\n\tconst auto item = _session->data().addNewMessage(\n\t\tlocalMessageId(id),\n\t\tPrepareMessage(message),\n\t\tMessageFlags(), // localFlags\n\t\tNewMessageType::Existing);\n\tif (!item || item->history() != history) {\n\t\tLOG((\"API Error: Bad data received in scheduled messages.\"));\n\t\treturn nullptr;\n\t}\n\tlist.items.emplace_back(item);\n\tlist.itemById.emplace(id, item);\n\treturn item;\n}\n\nvoid ScheduledMessages::clearNotSending(not_null<History*> history) {\n\tconst auto i = _data.find(history);\n\tif (i == end(_data)) {\n\t\treturn;\n\t}\n\tauto clear = base::flat_set<not_null<HistoryItem*>>();\n\tfor (const auto &owned : i->second.items) {\n\t\tif (!owned->isSending() && !owned->hasFailed()) {\n\t\t\tclear.emplace(owned.get());\n\t\t}\n\t}\n\tupdated(history, {}, clear);\n}\n\nvoid ScheduledMessages::updated(\n\t\tnot_null<History*> history,\n\t\tconst base::flat_set<not_null<HistoryItem*>> &added,\n\t\tconst base::flat_set<not_null<HistoryItem*>> &clear) {\n\tif (!clear.empty()) {\n\t\tfor (const auto &item : clear) {\n\t\t\titem->destroy();\n\t\t}\n\t}\n\tconst auto i = _data.find(history);\n\tif (i != end(_data)) {\n\t\tsort(i->second);\n\t}\n\tif (!added.empty() || !clear.empty()) {\n\t\t_updates.fire_copy(history);\n\t}\n}\n\nvoid ScheduledMessages::sort(List &list) {\n\tranges::sort(list.items, ranges::less(), &HistoryItem::position);\n}\n\nvoid ScheduledMessages::remove(not_null<const HistoryItem*> item) {\n\tconst auto history = item->history();\n\tconst auto i = _data.find(history);\n\tAssert(i != end(_data));\n\tauto &list = i->second;\n\n\tif (!item->isSending() && !item->hasFailed()) {\n\t\tlist.itemById.remove(lookupId(item));\n\t}\n\tconst auto k = ranges::find(list.items, item, &OwnedItem::get);\n\tAssert(k != list.items.end());\n\tk->release();\n\tlist.items.erase(k);\n\n\tif (list.items.empty()) {\n\t\t_data.erase(i);\n\t}\n\t_updates.fire_copy(history);\n}\n\nuint64 ScheduledMessages::countListHash(const List &list) const {\n\tusing namespace Api;\n\n\tauto hash = HashInit();\n\tauto &&serverside = ranges::views::all(\n\t\tlist.items\n\t) | ranges::views::filter([](const OwnedItem &item) {\n\t\treturn !item->isSending() && !item->hasFailed();\n\t}) | ranges::views::reverse;\n\tfor (const auto &item : serverside) {\n\t\tHashUpdate(hash, lookupId(item.get()).bare);\n\t\tif (const auto edited = item->Get<HistoryMessageEdited>()) {\n\t\t\tHashUpdate(hash, edited->date);\n\t\t} else {\n\t\t\tHashUpdate(hash, TimeId(0));\n\t\t}\n\t\tHashUpdate(hash, item->date());\n\t}\n\treturn HashFinalize(hash);\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/scheduled_messages.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/history_item.h\"\n#include \"base/timer.h\"\n\nclass History;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nstruct MessagesSlice;\n\n[[nodiscard]] bool IsScheduledMsgId(MsgId id);\n\nclass ScheduledMessages final {\npublic:\n\texplicit ScheduledMessages(not_null<Main::Session*> session);\n\tScheduledMessages(const ScheduledMessages &other) = delete;\n\tScheduledMessages &operator=(const ScheduledMessages &other) = delete;\n\t~ScheduledMessages();\n\n\t[[nodiscard]] MsgId lookupId(not_null<const HistoryItem*> item) const;\n\t[[nodiscard]] HistoryItem *lookupItem(PeerId peer, MsgId msg) const;\n\t[[nodiscard]] HistoryItem *lookupItem(FullMsgId itemId) const;\n\t[[nodiscard]] int count(not_null<History*> history) const;\n\t[[nodiscard]] bool hasFor(not_null<Data::ForumTopic*> topic) const;\n\t[[nodiscard]] MsgId localMessageId(MsgId remoteId) const;\n\n\tvoid checkEntitiesAndUpdate(const MTPDmessage &data);\n\tvoid apply(const MTPDupdateNewScheduledMessage &update);\n\tvoid apply(const MTPDupdateDeleteScheduledMessages &update);\n\tvoid apply(\n\t\tconst MTPDupdateMessageID &update,\n\t\tnot_null<HistoryItem*> local);\n\n\tvoid appendSending(not_null<HistoryItem*> item);\n\tvoid removeSending(not_null<HistoryItem*> item);\n\n\tvoid sendNowSimpleMessage(\n\t\tconst MTPDupdateShortSentMessage &update,\n\t\tnot_null<HistoryItem*> local);\n\n\t[[nodiscard]] rpl::producer<> updates(not_null<History*> history);\n\t[[nodiscard]] Data::MessagesSlice list(not_null<History*> history) const;\n\t[[nodiscard]] Data::MessagesSlice list(\n\t\tnot_null<const Data::ForumTopic*> topic) const;\n\n\tvoid clear();\n\nprivate:\n\tusing OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;\n\tstruct List {\n\t\tstd::vector<OwnedItem> items;\n\t\tbase::flat_map<MsgId, not_null<HistoryItem*>> itemById;\n\t};\n\tstruct Request {\n\t\tmtpRequestId requestId = 0;\n\t\tcrl::time lastReceived = 0;\n\t};\n\n\tvoid request(not_null<History*> history);\n\tvoid parse(\n\t\tnot_null<History*> history,\n\t\tconst MTPmessages_Messages &list);\n\tHistoryItem *append(\n\t\tnot_null<History*> history,\n\t\tList &list,\n\t\tconst MTPMessage &message);\n\tvoid clearNotSending(not_null<History*> history);\n\tvoid updated(\n\t\tnot_null<History*> history,\n\t\tconst base::flat_set<not_null<HistoryItem*>> &added,\n\t\tconst base::flat_set<not_null<HistoryItem*>> &clear);\n\tvoid sort(List &list);\n\tvoid remove(not_null<const HistoryItem*> item);\n\t[[nodiscard]] uint64 countListHash(const List &list) const;\n\tvoid clearOldRequests();\n\n\tconst not_null<Main::Session*> _session;\n\n\tbase::Timer _clearTimer;\n\tbase::flat_map<not_null<History*>, List> _data;\n\tbase::flat_map<not_null<History*>, Request> _requests;\n\trpl::event_stream<not_null<History*>> _updates;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/sponsored_messages.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/components/sponsored_messages.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"api/api_peer_search.h\" // SponsoredSearchResult\n#include \"apiwrap.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_media_preload.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_element.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/sponsored_message_bar.h\"\n#include \"ui/text/text_utilities.h\" // tr::rich.\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kMs = crl::time(1000);\nconstexpr auto kRequestTimeLimit = 5 * 60 * crl::time(1000);\n\nconst auto kFlaggedPreload = ((MediaPreload*)quintptr(0x01));\n\n[[nodiscard]] bool TooEarlyForRequest(crl::time received) {\n\treturn (received > 0) && (received + kRequestTimeLimit > crl::now());\n}\n\ntemplate <typename Fields>\n[[nodiscard]] std::vector<TextWithEntities> Prepare(const Fields &fields) {\n\tusing InfoList = std::vector<TextWithEntities>;\n\treturn (!fields.sponsorInfo.text.isEmpty()\n\t\t&& !fields.additionalInfo.text.isEmpty())\n\t\t? InfoList{ fields.sponsorInfo, fields.additionalInfo }\n\t\t: !fields.sponsorInfo.text.isEmpty()\n\t\t? InfoList{ fields.sponsorInfo }\n\t\t: !fields.additionalInfo.text.isEmpty()\n\t\t? InfoList{ fields.additionalInfo }\n\t\t: InfoList{};\n}\n\n} // namespace\n\nSponsoredMessages::SponsoredMessages(not_null<Main::Session*> session)\n: _session(session)\n, _clearTimer([=] { clearOldRequests(); }) {\n\tData::AmPremiumValue(\n\t\t_session\n\t) | rpl::on_next([=](bool premium) {\n\t\tif (premium) {\n\t\t\tclear();\n\t\t}\n\t}, _lifetime);\n}\n\nSponsoredMessages::~SponsoredMessages() {\n\tExpects(_data.empty());\n\tExpects(_requests.empty());\n\tExpects(_viewRequests.empty());\n}\n\nvoid SponsoredMessages::clear() {\n\t_lifetime.destroy();\n\tfor (const auto &request : base::take(_requests)) {\n\t\t_session->api().request(request.second.requestId).cancel();\n\t}\n\tfor (const auto &request : base::take(_viewRequests)) {\n\t\t_session->api().request(request.second.requestId).cancel();\n\t}\n\tbase::take(_data);\n}\n\nvoid SponsoredMessages::clearOldRequests() {\n\tconst auto now = crl::now();\n\tconst auto clear = [&](auto &requests) {\n\t\twhile (true) {\n\t\t\tconst auto i = ranges::find_if(requests, [&](const auto &value) {\n\t\t\t\tconst auto &request = value.second;\n\t\t\t\treturn !request.requestId\n\t\t\t\t\t&& (request.lastReceived + kRequestTimeLimit <= now);\n\t\t\t});\n\t\t\tif (i == end(requests)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\trequests.erase(i);\n\t\t}\n\t};\n\tclear(_requests);\n\tclear(_requestsForVideo);\n}\n\nSponsoredMessages::AppendResult SponsoredMessages::append(\n\t\tnot_null<History*> history) {\n\tif (isTopBarFor(history)) {\n\t\treturn SponsoredMessages::AppendResult::None;\n\t}\n\tconst auto it = _data.find(history);\n\tif (it == end(_data)) {\n\t\treturn SponsoredMessages::AppendResult::None;\n\t}\n\tauto &list = it->second;\n\tif (list.showedAll\n\t\t|| !TooEarlyForRequest(list.received)\n\t\t|| list.postsBetween) {\n\t\treturn SponsoredMessages::AppendResult::None;\n\t}\n\n\tconst auto entryIt = ranges::find_if(list.entries, [](const Entry &e) {\n\t\treturn e.item == nullptr;\n\t});\n\tif (entryIt == end(list.entries)) {\n\t\tlist.showedAll = true;\n\t\treturn SponsoredMessages::AppendResult::None;\n\t} else if (entryIt->preload) {\n\t\treturn SponsoredMessages::AppendResult::MediaLoading;\n\t}\n\tentryIt->item.reset(history->addSponsoredMessage(\n\t\tentryIt->itemFullId.msg,\n\t\tentryIt->sponsored.from,\n\t\tentryIt->sponsored.textWithEntities));\n\n\treturn SponsoredMessages::AppendResult::Appended;\n}\n\nvoid SponsoredMessages::inject(\n\t\tnot_null<History*> history,\n\t\tMsgId injectAfterMsgId,\n\t\tint betweenHeight,\n\t\tint fallbackWidth) {\n\tif (!canHaveFor(history)) {\n\t\treturn;\n\t}\n\tconst auto it = _data.find(history);\n\tif (it == end(_data)) {\n\t\treturn;\n\t}\n\tauto &list = it->second;\n\tif (!list.postsBetween || (list.entries.size() == list.injectedCount)) {\n\t\treturn;\n\t}\n\n\twhile (true) {\n\t\tconst auto entryIt = ranges::find_if(list.entries, [](const auto &e) {\n\t\t\treturn e.item == nullptr;\n\t\t});\n\t\tif (entryIt == end(list.entries)) {\n\t\t\tlist.showedAll = true;\n\t\t\treturn;\n\t\t}\n\t\tconst auto lastView = (entryIt != begin(list.entries))\n\t\t\t? (entryIt - 1)->item->mainView()\n\t\t\t: (injectAfterMsgId == ShowAtUnreadMsgId)\n\t\t\t? history->firstUnreadMessage()\n\t\t\t: [&] {\n\t\t\t\tconst auto message = history->peer->owner().message(\n\t\t\t\t\thistory->peer->id,\n\t\t\t\t\tinjectAfterMsgId);\n\t\t\t\treturn message ? message->mainView() : nullptr;\n\t\t\t}();\n\t\tif (!lastView || !lastView->block()) {\n\t\t\treturn;\n\t\t}\n\n\t\tauto summaryBetween = 0;\n\t\tauto summaryHeight = 0;\n\n\t\tusing BlockPtr = std::unique_ptr<HistoryBlock>;\n\t\tusing ViewPtr = std::unique_ptr<HistoryView::Element>;\n\t\tauto blockIt = ranges::find(\n\t\t\thistory->blocks,\n\t\t\tlastView->block(),\n\t\t\t&BlockPtr::get);\n\t\tif (blockIt == end(history->blocks)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto messages = [&]() -> const std::vector<ViewPtr>& {\n\t\t\treturn (*blockIt)->messages;\n\t\t};\n\t\tauto lastViewIt = ranges::find(messages(), lastView, &ViewPtr::get);\n\t\tauto appendAtLeastToEnd = false;\n\t\twhile ((summaryBetween < list.postsBetween)\n\t\t\t|| (summaryHeight < betweenHeight)) {\n\t\t\tlastViewIt++;\n\t\t\tif (lastViewIt == end(messages())) {\n\t\t\t\tblockIt++;\n\t\t\t\tif (blockIt != end(history->blocks)) {\n\t\t\t\t\tlastViewIt = begin(messages());\n\t\t\t\t} else {\n\t\t\t\t\tif (!list.injectedCount) {\n\t\t\t\t\t\tappendAtLeastToEnd = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tsummaryBetween++;\n\t\t\tconst auto viewHeight = (*lastViewIt)->height();\n\t\t\tsummaryHeight += viewHeight\n\t\t\t\t? viewHeight\n\t\t\t\t: (*lastViewIt)->resizeGetHeight(fallbackWidth);\n\t\t}\n\t\t// SponsoredMessages::Details can be requested within\n\t\t// the constructor of HistoryItem, so itemFullId is used as a key.\n\t\tentryIt->itemFullId = FullMsgId(\n\t\t\thistory->peer->id,\n\t\t\t_session->data().nextLocalMessageId());\n\t\tif (appendAtLeastToEnd) {\n\t\t\tentryIt->item.reset(history->addSponsoredMessage(\n\t\t\t\tentryIt->itemFullId.msg,\n\t\t\t\tentryIt->sponsored.from,\n\t\t\t\tentryIt->sponsored.textWithEntities));\n\t\t} else {\n\t\t\tconst auto makedMessage = history->makeMessage(\n\t\t\t\tentryIt->itemFullId.msg,\n\t\t\t\tentryIt->sponsored.from,\n\t\t\t\tentryIt->sponsored.textWithEntities,\n\t\t\t\t(*lastViewIt)->data());\n\t\t\tentryIt->item.reset(makedMessage.get());\n\t\t\thistory->addNewInTheMiddle(\n\t\t\t\tmakedMessage.get(),\n\t\t\t\tstd::distance(begin(history->blocks), blockIt),\n\t\t\t\tstd::distance(begin(messages()), lastViewIt) + 1);\n\t\t\tmessages().back().get()->setPendingResize();\n\t\t}\n\t\tlist.injectedCount++;\n\t}\n}\n\nbool SponsoredMessages::canHaveFor(not_null<History*> history) const {\n\tif (history->peer->isChannel()) {\n\t\treturn true;\n\t} else if (const auto user = history->peer->asUser()) {\n\t\treturn user->isBot();\n\t}\n\treturn false;\n}\n\nbool SponsoredMessages::canHaveFor(not_null<HistoryItem*> item) const {\n\treturn item->history()->peer->isBroadcast()\n\t\t&& item->isRegular();\n}\n\nbool SponsoredMessages::isTopBarFor(not_null<History*> history) const {\n\tif (peerIsUser(history->peer->id)) {\n\t\tif (const auto user = history->peer->asUser()) {\n\t\t\treturn user->isBot();\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid SponsoredMessages::request(not_null<History*> history, Fn<void()> done) {\n\tif (!canHaveFor(history)) {\n\t\treturn;\n\t}\n\tauto &request = _requests[history];\n\tif (request.requestId || TooEarlyForRequest(request.lastReceived)) {\n\t\treturn;\n\t}\n\t{\n\t\tconst auto it = _data.find(history);\n\t\tif (it != end(_data)) {\n\t\t\tauto &list = it->second;\n\t\t\t// Don't rebuild currently displayed messages.\n\t\t\tconst auto proj = [](const Entry &e) {\n\t\t\t\treturn e.item != nullptr;\n\t\t\t};\n\t\t\tif (ranges::any_of(list.entries, proj)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\trequest.requestId = _session->api().request(\n\t\tMTPmessages_GetSponsoredMessages(\n\t\t\tMTP_flags(0),\n\t\t\thistory->peer->input(),\n\t\t\tMTPint()) // msg_id\n\t).done([=](const MTPmessages_sponsoredMessages &result) {\n\t\tparse(history, result);\n\t\tif (done) {\n\t\t\tdone();\n\t\t}\n\t}).fail([=] {\n\t\t_requests.remove(history);\n\t}).send();\n}\n\nvoid SponsoredMessages::requestForVideo(\n\t\tnot_null<HistoryItem*> item,\n\t\tFn<void(SponsoredForVideo)> done) {\n\tExpects(done != nullptr);\n\n\tif (!canHaveFor(item)) {\n\t\tdone({});\n\t\treturn;\n\t}\n\tconst auto peer = item->history()->peer;\n\tauto &request = _requestsForVideo[peer];\n\tif (TooEarlyForRequest(request.lastReceived)) {\n\t\tauto prepared = prepareForVideo(peer);\n\t\tif (prepared.list.empty()\n\t\t\t|| prepared.state.itemIndex < prepared.list.size()\n\t\t\t|| prepared.state.leftTillShow > 0) {\n\t\t\tdone(std::move(prepared));\n\t\t\treturn;\n\t\t}\n\t}\n\trequest.callbacks.push_back(std::move(done));\n\tif (request.requestId) {\n\t\treturn;\n\t}\n\t{\n\t\tconst auto it = _dataForVideo.find(peer);\n\t\tif (it != end(_dataForVideo)) {\n\t\t\tauto &list = it->second;\n\t\t\t// Don't rebuild currently displayed messages.\n\t\t\tconst auto proj = [](const Entry &e) {\n\t\t\t\treturn e.item != nullptr;\n\t\t\t};\n\t\t\tif (ranges::any_of(list.entries, proj)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto finish = [=] {\n\t\tconst auto i = _requestsForVideo.find(peer);\n\t\tif (i != end(_requestsForVideo)) {\n\t\t\tfor (const auto &callback : base::take(i->second.callbacks)) {\n\t\t\t\tcallback(prepareForVideo(peer));\n\t\t\t}\n\t\t}\n\t};\n\tusing Flag = MTPmessages_GetSponsoredMessages::Flag;\n\trequest.requestId = _session->api().request(\n\t\tMTPmessages_GetSponsoredMessages(\n\t\t\tMTP_flags(Flag::f_msg_id),\n\t\t\tpeer->input(),\n\t\t\tMTP_int(item->id.bare))\n\t).done([=](const MTPmessages_sponsoredMessages &result) {\n\t\tparseForVideo(peer, result);\n\t\tfinish();\n\t}).fail([=] {\n\t\t_requestsForVideo.remove(peer);\n\t\tfinish();\n\t}).send();\n}\n\nvoid SponsoredMessages::updateForVideo(\n\t\tFullMsgId itemId,\n\t\tSponsoredForVideoState state) {\n\tif (state.initial()) {\n\t\treturn;\n\t}\n\tconst auto i = _dataForVideo.find(_session->data().peer(itemId.peer));\n\tif (i != end(_dataForVideo)) {\n\t\ti->second.state = state;\n\t}\n}\n\nvoid SponsoredMessages::parse(\n\t\tnot_null<History*> history,\n\t\tconst MTPmessages_sponsoredMessages &list) {\n\tauto &request = _requests[history];\n\trequest.lastReceived = crl::now();\n\trequest.requestId = 0;\n\tif (!_clearTimer.isActive()) {\n\t\t_clearTimer.callOnce(kRequestTimeLimit * 2);\n\t}\n\n\tlist.match([&](const MTPDmessages_sponsoredMessages &data) {\n\t\t_session->data().processUsers(data.vusers());\n\t\t_session->data().processChats(data.vchats());\n\n\t\tconst auto &messages = data.vmessages().v;\n\t\tauto &list = _data.emplace(history).first->second;\n\t\tlist.entries.clear();\n\t\tlist.received = crl::now();\n\t\tif (const auto postsBetween = data.vposts_between()) {\n\t\t\tlist.postsBetween = postsBetween->v;\n\t\t\tlist.state = State::InjectToMiddle;\n\t\t} else {\n\t\t\tlist.state = history->peer->isChannel()\n\t\t\t\t? State::AppendToEnd\n\t\t\t\t: State::AppendToTopBar;\n\t\t}\n\t\tfor (const auto &message : messages) {\n\t\t\tappend([=] {\n\t\t\t\treturn &_data[history].entries;\n\t\t\t}, history, message);\n\t\t}\n\t}, [](const MTPDmessages_sponsoredMessagesEmpty &) {\n\t});\n}\n\nvoid SponsoredMessages::parseForVideo(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPmessages_sponsoredMessages &list) {\n\tauto &request = _requestsForVideo[peer];\n\trequest.lastReceived = crl::now();\n\trequest.requestId = 0;\n\tif (!_clearTimer.isActive()) {\n\t\t_clearTimer.callOnce(kRequestTimeLimit * 2);\n\t}\n\n\tlist.match([&](const MTPDmessages_sponsoredMessages &data) {\n\t\t_session->data().processUsers(data.vusers());\n\t\t_session->data().processChats(data.vchats());\n\n\t\tconst auto history = _session->data().history(peer);\n\t\tconst auto &messages = data.vmessages().v;\n\t\tauto &list = _dataForVideo.emplace(peer).first->second;\n\t\tlist.entries.clear();\n\t\tlist.received = crl::now();\n\t\tlist.startDelay = data.vstart_delay().value_or_empty() * kMs;\n\t\tlist.betweenDelay = data.vbetween_delay().value_or_empty() * kMs;\n\t\tfor (const auto &message : messages) {\n\t\t\tappend([=] {\n\t\t\t\treturn &_dataForVideo[peer].entries;\n\t\t\t}, history, message);\n\t\t}\n\t}, [](const MTPDmessages_sponsoredMessagesEmpty &) {\n\t});\n}\n\nSponsoredForVideo SponsoredMessages::prepareForVideo(\n\t\tnot_null<PeerData*> peer) {\n\tconst auto i = _dataForVideo.find(peer);\n\tif (i == end(_dataForVideo) || i->second.entries.empty()) {\n\t\treturn {};\n\t}\n\treturn SponsoredForVideo{\n\t\t.list = i->second.entries | ranges::views::transform(\n\t\t\t&Entry::sponsored\n\t\t) | ranges::to_vector,\n\t\t.startDelay = i->second.startDelay,\n\t\t.betweenDelay = i->second.betweenDelay,\n\t\t.state = i->second.state,\n\t};\n}\n\nFullMsgId SponsoredMessages::fillTopBar(\n\t\tnot_null<History*> history,\n\t\tnot_null<Ui::RpWidget*> widget) {\n\tconst auto it = _data.find(history);\n\tif (it != end(_data)) {\n\t\tauto &list = it->second;\n\t\tif (!list.entries.empty()) {\n\t\t\tconst auto &entry = list.entries.front();\n\t\t\tconst auto fullId = entry.itemFullId;\n\t\t\tUi::FillSponsoredMessageBar(\n\t\t\t\twidget,\n\t\t\t\t_session,\n\t\t\t\tfullId,\n\t\t\t\tentry.sponsored.from,\n\t\t\t\tentry.sponsored.textWithEntities);\n\t\t\treturn fullId;\n\t\t}\n\t}\n\treturn {};\n}\n\nrpl::producer<> SponsoredMessages::itemRemoved(const FullMsgId &fullId) {\n\tif (IsServerMsgId(fullId.msg) || !fullId) {\n\t\treturn rpl::never<>();\n\t}\n\tconst auto history = _session->data().history(fullId.peer);\n\tconst auto it = _data.find(history);\n\tif (it == end(_data)) {\n\t\treturn rpl::never<>();\n\t}\n\tauto &list = it->second;\n\tconst auto entryIt = ranges::find_if(list.entries, [&](const Entry &e) {\n\t\treturn e.itemFullId == fullId;\n\t});\n\tif (entryIt == end(list.entries)) {\n\t\treturn rpl::never<>();\n\t}\n\tif (!entryIt->optionalDestructionNotifier) {\n\t\tentryIt->optionalDestructionNotifier\n\t\t\t= std::make_unique<rpl::lifetime>();\n\t\tentryIt->optionalDestructionNotifier->add([this, fullId] {\n\t\t\t_itemRemoved.fire_copy(fullId);\n\t\t});\n\t}\n\treturn _itemRemoved.events(\n\t) | rpl::filter(rpl::mappers::_1 == fullId) | rpl::to_empty;\n}\n\nvoid SponsoredMessages::append(\n\t\tFn<not_null<std::vector<Entry>*>()> entries,\n\t\tnot_null<History*> history,\n\t\tconst MTPSponsoredMessage &message) {\n\tconst auto &data = message.data();\n\tconst auto randomId = data.vrandom_id().v;\n\tauto mediaPhoto = (PhotoData*)nullptr;\n\tauto mediaDocument = (DocumentData*)nullptr;\n\t{\n\t\tif (data.vmedia()) {\n\t\t\tdata.vmedia()->match([&](const MTPDmessageMediaPhoto &media) {\n\t\t\t\tif (const auto tlPhoto = media.vphoto()) {\n\t\t\t\t\ttlPhoto->match([&](const MTPDphoto &data) {\n\t\t\t\t\t\tmediaPhoto = _session->data().processPhoto(data);\n\t\t\t\t\t}, [](const MTPDphotoEmpty &) {\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}, [&](const MTPDmessageMediaDocument &media) {\n\t\t\t\tif (const auto tlDocument = media.vdocument()) {\n\t\t\t\t\ttlDocument->match([&](const MTPDdocument &data) {\n\t\t\t\t\t\tconst auto d = _session->data().processDocument(\n\t\t\t\t\t\t\tdata,\n\t\t\t\t\t\t\tmedia.valt_documents());\n\t\t\t\t\t\tif (d->isVideoFile()\n\t\t\t\t\t\t\t|| d->isSilentVideo()\n\t\t\t\t\t\t\t|| d->isAnimation()\n\t\t\t\t\t\t\t|| d->isGifv()) {\n\t\t\t\t\t\t\tmediaDocument = d;\n\t\t\t\t\t\t}\n\t\t\t\t\t}, [](const MTPDdocumentEmpty &) {\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}, [](const auto &) {\n\t\t\t});\n\t\t}\n\t};\n\tconst auto from = SponsoredFrom{\n\t\t.title = qs(data.vtitle()),\n\t\t.link = qs(data.vurl()),\n\t\t.buttonText = qs(data.vbutton_text()),\n\t\t.photoId = data.vphoto()\n\t\t\t? _session->data().processPhoto(*data.vphoto())->id\n\t\t\t: PhotoId(0),\n\t\t.mediaPhotoId = (mediaPhoto ? mediaPhoto->id : 0),\n\t\t.mediaDocumentId = (mediaDocument ? mediaDocument->id : 0),\n\t\t.backgroundEmojiId = BackgroundEmojiIdFromColor(data.vcolor()),\n\t\t.colorIndex = ColorIndexFromColor(data.vcolor()).value_or(0),\n\t\t.isLinkInternal = !UrlRequiresConfirmation(qs(data.vurl())),\n\t\t.isRecommended = data.is_recommended(),\n\t\t.canReport = data.is_can_report(),\n\t};\n\tauto sponsorInfo = data.vsponsor_info()\n\t\t? tr::lng_sponsored_info_submenu(\n\t\t\ttr::now,\n\t\t\tlt_text,\n\t\t\t{ .text = qs(*data.vsponsor_info()) },\n\t\t\ttr::rich)\n\t\t: TextWithEntities();\n\tauto additionalInfo = TextWithEntities::Simple(\n\t\tdata.vadditional_info() ? qs(*data.vadditional_info()) : QString());\n\tauto sharedMessage = SponsoredMessage{\n\t\t.randomId = randomId,\n\t\t.from = from,\n\t\t.textWithEntities = {\n\t\t\t.text = qs(data.vmessage()),\n\t\t\t.entities = Api::EntitiesFromMTP(\n\t\t\t\t_session,\n\t\t\t\tdata.ventities().value_or_empty()),\n\t\t},\n\t\t.history = history,\n\t\t.link = from.link,\n\t\t.sponsorInfo = std::move(sponsorInfo),\n\t\t.additionalInfo = std::move(additionalInfo),\n\t\t.durationMin = data.vmin_display_duration().value_or_empty() * kMs,\n\t\t.durationMax = data.vmax_display_duration().value_or_empty() * kMs,\n\t};\n\tconst auto itemId = FullMsgId(\n\t\thistory->peer->id,\n\t\t_session->data().nextLocalMessageId());\n\tconst auto list = entries();\n\tlist->push_back({\n\t\t.itemFullId = itemId,\n\t\t.sponsored = std::move(sharedMessage),\n\t});\n\tauto &entry = list->back();\n\tconst auto fileOrigin = FileOrigin(); // No way to refresh in ads.\n\n\tconst auto preloaded = [=] {\n\t\tconst auto list = entries();\n\t\tconst auto j = ranges::find(*list, itemId, &Entry::itemFullId);\n\t\tif (j == end(*list)) {\n\t\t\treturn;\n\t\t}\n\t\tauto &entry = *j;\n\t\tif (entry.preload.get() == kFlaggedPreload) {\n\t\t\tentry.preload.release();\n\t\t} else {\n\t\t\tentry.preload = nullptr;\n\t\t}\n\t};\n\n\tauto preload = std::unique_ptr<MediaPreload>();\n\tentry.preload.reset(kFlaggedPreload);\n\tif (mediaPhoto) {\n\t\tpreload = std::make_unique<PhotoPreload>(\n\t\t\tmediaPhoto,\n\t\t\tfileOrigin,\n\t\t\tpreloaded);\n\t} else if (mediaDocument && VideoPreload::Can(mediaDocument)) {\n\t\tpreload = std::make_unique<VideoPreload>(\n\t\t\tmediaDocument,\n\t\t\tfileOrigin,\n\t\t\tpreloaded);\n\t}\n\t// Preload constructor may have called preloaded(), which zero-ed\n\t// entry.preload, that way we're ready and don't need to save it.\n\t// Otherwise we're preloading and need to save the task.\n\tif (entry.preload.get() == kFlaggedPreload) {\n\t\tentry.preload.release();\n\t\tif (preload) {\n\t\t\tentry.preload = std::move(preload);\n\t\t}\n\t}\n}\n\nvoid SponsoredMessages::clearItems(not_null<History*> history) {\n\tconst auto it = _data.find(history);\n\tif (it == end(_data)) {\n\t\treturn;\n\t}\n\tauto &list = it->second;\n\tfor (auto &entry : list.entries) {\n\t\tentry.item.reset();\n\t}\n\tlist.showedAll = false;\n\tlist.injectedCount = 0;\n}\n\nconst SponsoredMessages::Entry *SponsoredMessages::find(\n\t\tconst FullMsgId &fullId) const {\n\tif (!peerIsChannel(fullId.peer) && !peerIsUser(fullId.peer)) {\n\t\treturn nullptr;\n\t}\n\tconst auto history = _session->data().history(fullId.peer);\n\tconst auto it = _data.find(history);\n\tif (it == end(_data)) {\n\t\treturn nullptr;\n\t}\n\tauto &list = it->second;\n\tconst auto entryIt = ranges::find_if(list.entries, [&](const Entry &e) {\n\t\treturn e.itemFullId == fullId;\n\t});\n\tif (entryIt == end(list.entries)) {\n\t\treturn nullptr;\n\t}\n\treturn &*entryIt;\n}\n\nvoid SponsoredMessages::view(const FullMsgId &fullId) {\n\tconst auto entryPtr = find(fullId);\n\tif (!entryPtr) {\n\t\treturn;\n\t}\n\tview(entryPtr->sponsored.randomId);\n}\n\nvoid SponsoredMessages::view(const QByteArray &randomId) {\n\tauto &request = _viewRequests[randomId];\n\tif (request.requestId || TooEarlyForRequest(request.lastReceived)) {\n\t\treturn;\n\t}\n\trequest.requestId = _session->api().request(\n\t\tMTPmessages_ViewSponsoredMessage(MTP_bytes(randomId))\n\t).done([=] {\n\t\tauto &request = _viewRequests[randomId];\n\t\trequest.lastReceived = crl::now();\n\t\trequest.requestId = 0;\n\t}).fail([=] {\n\t\t_viewRequests.remove(randomId);\n\t}).send();\n}\n\nSponsoredMessages::Details SponsoredMessages::lookupDetails(\n\t\tconst FullMsgId &fullId) const {\n\tconst auto entryPtr = find(fullId);\n\tif (!entryPtr) {\n\t\treturn {};\n\t}\n\treturn lookupDetails(entryPtr->sponsored);\n}\n\nSponsoredMessages::Details SponsoredMessages::lookupDetails(\n\t\tconst SponsoredMessage &data) const {\n\treturn {\n\t\t.info = Prepare(data),\n\t\t.link = data.link,\n\t\t.buttonText = data.from.buttonText,\n\t\t.photoId = data.from.photoId,\n\t\t.mediaPhotoId = data.from.mediaPhotoId,\n\t\t.mediaDocumentId = data.from.mediaDocumentId,\n\t\t.backgroundEmojiId = data.from.backgroundEmojiId,\n\t\t.colorIndex = data.from.colorIndex,\n\t\t.isLinkInternal = data.from.isLinkInternal,\n\t\t.canReport = data.from.canReport,\n\t};\n}\n\nSponsoredMessages::Details SponsoredMessages::lookupDetails(\n\t\tconst Api::SponsoredSearchResult &data) const {\n\treturn {\n\t\t.info = Prepare(data),\n\t\t.canReport = true,\n\t};\n}\n\nvoid SponsoredMessages::clicked(\n\t\tconst FullMsgId &fullId,\n\t\tbool isMedia,\n\t\tbool isFullscreen) {\n\tconst auto entryPtr = find(fullId);\n\tif (!entryPtr) {\n\t\treturn;\n\t}\n\tclicked(entryPtr->sponsored.randomId, isMedia, isFullscreen);\n}\n\nvoid SponsoredMessages::clicked(\n\t\tconst QByteArray &randomId,\n\t\tbool isMedia,\n\t\tbool isFullscreen) {\n\tusing Flag = MTPmessages_ClickSponsoredMessage::Flag;\n\t_session->api().request(MTPmessages_ClickSponsoredMessage(\n\t\tMTP_flags(Flag(0)\n\t\t\t| (isMedia ? Flag::f_media : Flag(0))\n\t\t\t| (isFullscreen ? Flag::f_fullscreen : Flag(0))),\n\t\tMTP_bytes(randomId)\n\t)).send();\n}\n\nSponsoredReportAction SponsoredMessages::createReportCallback(\n\t\tconst FullMsgId &fullId) {\n\tconst auto entry = find(fullId);\n\tif (!entry) {\n\t\treturn { .callback = [=](const auto &...) {} };\n\t}\n\tconst auto history = _session->data().history(fullId.peer);\n\tconst auto erase = [=] {\n\t\tconst auto it = _data.find(history);\n\t\tif (it != end(_data)) {\n\t\t\tauto &list = it->second.entries;\n\t\t\tconst auto proj = [&](const Entry &e) {\n\t\t\t\treturn e.itemFullId == fullId;\n\t\t\t};\n\t\t\tlist.erase(ranges::remove_if(list, proj), end(list));\n\t\t}\n\t};\n\treturn createReportCallback(entry->sponsored.randomId, erase);\n}\n\nSponsoredReportAction SponsoredMessages::createReportCallback(\n\t\tconst QByteArray &randomId,\n\t\tFn<void()> erase) {\n\tusing TLChoose = MTPDchannels_sponsoredMessageReportResultChooseOption;\n\tusing TLAdsHidden = MTPDchannels_sponsoredMessageReportResultAdsHidden;\n\tusing TLReported = MTPDchannels_sponsoredMessageReportResultReported;\n\tusing Result = SponsoredReportResult;\n\n\tstruct State final {\n#ifdef _DEBUG\n\t\t~State() {\n\t\t\tqDebug() << \"SponsoredMessages Report ~State().\";\n\t\t}\n#endif\n\t\tmtpRequestId requestId = 0;\n\t};\n\tconst auto state = std::make_shared<State>();\n\n\treturn { .callback = [=](Result::Id optionId, Fn<void(Result)> done) {\n\t\tif (optionId == Result::Id(\"-1\")) {\n\t\t\terase();\n\t\t\treturn;\n\t\t}\n\n\t\tstate->requestId = _session->api().request(\n\t\t\tMTPmessages_ReportSponsoredMessage(\n\t\t\t\tMTP_bytes(randomId),\n\t\t\t\tMTP_bytes(optionId))\n\t\t).done([=](\n\t\t\t\tconst MTPchannels_SponsoredMessageReportResult &result,\n\t\t\t\tmtpRequestId requestId) {\n\t\t\tif (state->requestId != requestId) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->requestId = 0;\n\t\t\tdone(result.match([&](const TLChoose &data) {\n\t\t\t\tconst auto t = qs(data.vtitle());\n\t\t\t\tauto list = Result::Options();\n\t\t\t\tlist.reserve(data.voptions().v.size());\n\t\t\t\tfor (const auto &tl : data.voptions().v) {\n\t\t\t\t\tlist.emplace_back(Result::Option{\n\t\t\t\t\t\t.id = tl.data().voption().v,\n\t\t\t\t\t\t.text = qs(tl.data().vtext()),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn Result{ .options = std::move(list), .title = t };\n\t\t\t}, [](const TLAdsHidden &data) -> Result {\n\t\t\t\treturn { .result = Result::FinalStep::Hidden };\n\t\t\t}, [&](const TLReported &data) -> Result {\n\t\t\t\terase();\n\t\t\t\tif (optionId == Result::Id(\"1\")) { // I don't like it.\n\t\t\t\t\treturn { .result = Result::FinalStep::Silence };\n\t\t\t\t}\n\t\t\t\treturn { .result = Result::FinalStep::Reported };\n\t\t\t}));\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tstate->requestId = 0;\n\t\t\tif (error.type() == u\"PREMIUM_ACCOUNT_REQUIRED\"_q) {\n\t\t\t\tdone({ .result = Result::FinalStep::Premium });\n\t\t\t} else {\n\t\t\t\tdone({ .error = error.type() });\n\t\t\t}\n\t\t}).send();\n\t} };\n}\n\nSponsoredMessages::State SponsoredMessages::state(\n\t\tnot_null<History*> history) const {\n\tconst auto it = _data.find(history);\n\treturn (it == end(_data)) ? State::None : it->second.state;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/sponsored_messages.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"history/history_item.h\"\n#include \"ui/image/image_location.h\"\n#include \"window/window_session_controller_link_info.h\"\n\nclass History;\n\nnamespace Api {\nstruct SponsoredSearchResult;\n} // namespace Api\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Data {\n\nclass MediaPreload;\n\nstruct SponsoredReportResult final {\n\tusing Id = QByteArray;\n\tstruct Option final {\n\t\tId id = 0;\n\t\tQString text;\n\t};\n\tusing Options = std::vector<Option>;\n\tenum class FinalStep {\n\t\tHidden,\n\t\tReported,\n\t\tPremium,\n\t\tSilence,\n\t};\n\tOptions options;\n\tQString title;\n\tQString error;\n\tFinalStep result;\n};\n\nstruct SponsoredFrom {\n\tQString title;\n\tQString link;\n\tQString buttonText;\n\tPhotoId photoId = PhotoId(0);\n\tPhotoId mediaPhotoId = PhotoId(0);\n\tDocumentId mediaDocumentId = DocumentId(0);\n\tuint64 backgroundEmojiId = 0;\n\tuint8 colorIndex : 6 = 0;\n\tbool isLinkInternal = false;\n\tbool isRecommended = false;\n\tbool canReport = false;\n};\n\nstruct SponsoredMessage {\n\tQByteArray randomId;\n\tSponsoredFrom from;\n\tTextWithEntities textWithEntities;\n\tnot_null<History*> history;\n\tQString link;\n\tTextWithEntities sponsorInfo;\n\tTextWithEntities additionalInfo;\n\tcrl::time durationMin = 0;\n\tcrl::time durationMax = 0;\n};\n\nstruct SponsoredMessageDetails {\n\tstd::vector<TextWithEntities> info;\n\tQString link;\n\tQString buttonText;\n\tPhotoId photoId = PhotoId(0);\n\tPhotoId mediaPhotoId = PhotoId(0);\n\tDocumentId mediaDocumentId = DocumentId(0);\n\tuint64 backgroundEmojiId = 0;\n\tuint8 colorIndex : 6 = 0;\n\tbool isLinkInternal = false;\n\tbool canReport = false;\n};\n\nstruct SponsoredReportAction {\n\tFn<void(\n\t\tData::SponsoredReportResult::Id,\n\t\tFn<void(Data::SponsoredReportResult)>)> callback;\n};\n\nstruct SponsoredForVideoState {\n\tint itemIndex = 0;\n\tcrl::time leftTillShow = 0;\n\n\t[[nodiscard]] bool initial() const {\n\t\treturn !itemIndex && !leftTillShow;\n\t}\n};\n\nstruct SponsoredForVideo {\n\tstd::vector<SponsoredMessage> list;\n\tcrl::time startDelay = 0;\n\tcrl::time betweenDelay = 0;\n\n\tSponsoredForVideoState state;\n};\n\nclass SponsoredMessages final {\npublic:\n\tenum class AppendResult {\n\t\tNone,\n\t\tAppended,\n\t\tMediaLoading,\n\t};\n\tenum class State {\n\t\tNone,\n\t\tAppendToEnd,\n\t\tInjectToMiddle,\n\t\tAppendToTopBar,\n\t};\n\tusing Details = SponsoredMessageDetails;\n\tusing RandomId = QByteArray;\n\texplicit SponsoredMessages(not_null<Main::Session*> session);\n\t~SponsoredMessages();\n\n\t[[nodiscard]] bool canHaveFor(not_null<History*> history) const;\n\t[[nodiscard]] bool canHaveFor(not_null<HistoryItem*> item) const;\n\t[[nodiscard]] bool isTopBarFor(not_null<History*> history) const;\n\tvoid request(not_null<History*> history, Fn<void()> done);\n\tvoid requestForVideo(\n\t\tnot_null<HistoryItem*> item,\n\t\tFn<void(SponsoredForVideo)> done);\n\tvoid updateForVideo(\n\t\tFullMsgId itemId,\n\t\tSponsoredForVideoState state);\n\tvoid clearItems(not_null<History*> history);\n\t[[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const;\n\t[[nodiscard]] Details lookupDetails(const SponsoredMessage &data) const;\n\t[[nodiscard]] Details lookupDetails(\n\t\tconst Api::SponsoredSearchResult &data) const;\n\tvoid clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen);\n\tvoid clicked(\n\t\tconst QByteArray &randomId,\n\t\tbool isMedia,\n\t\tbool isFullscreen);\n\t[[nodiscard]] FullMsgId fillTopBar(\n\t\tnot_null<History*> history,\n\t\tnot_null<Ui::RpWidget*> widget);\n\t[[nodiscard]] rpl::producer<> itemRemoved(const FullMsgId &);\n\n\t[[nodiscard]] AppendResult append(not_null<History*> history);\n\tvoid inject(\n\t\tnot_null<History*> history,\n\t\tMsgId injectAfterMsgId,\n\t\tint betweenHeight,\n\t\tint fallbackWidth);\n\n\tvoid view(const FullMsgId &fullId);\n\tvoid view(const QByteArray &randomId);\n\n\t[[nodiscard]] State state(not_null<History*> history) const;\n\n\t[[nodiscard]] SponsoredReportAction createReportCallback(\n\t\tconst FullMsgId &fullId);\n\t[[nodiscard]] SponsoredReportAction createReportCallback(\n\t\tconst QByteArray &randomId,\n\t\tFn<void()> erase);\n\n\tvoid clear();\n\nprivate:\n\tusing OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;\n\tstruct Entry {\n\t\tOwnedItem item;\n\t\tFullMsgId itemFullId;\n\t\tSponsoredMessage sponsored;\n\t\tstd::unique_ptr<MediaPreload> preload;\n\t\tstd::unique_ptr<rpl::lifetime> optionalDestructionNotifier;\n\t};\n\tstruct List {\n\t\tstd::vector<Entry> entries;\n\t\t// Data between history displays.\n\t\tsize_t injectedCount = 0;\n\t\tbool showedAll = false;\n\t\t//\n\t\tcrl::time received = 0;\n\t\tint postsBetween = 0;\n\t\tState state = State::None;\n\t};\n\tstruct ListForVideo {\n\t\tstd::vector<Entry> entries;\n\t\tcrl::time received = 0;\n\t\tcrl::time startDelay = 0;\n\t\tcrl::time betweenDelay = 0;\n\t\tSponsoredForVideoState state;\n\t};\n\tstruct Request {\n\t\tmtpRequestId requestId = 0;\n\t\tcrl::time lastReceived = 0;\n\t};\n\tstruct RequestForVideo {\n\t\tstd::vector<Fn<void(SponsoredForVideo)>> callbacks;\n\t\tmtpRequestId requestId = 0;\n\t\tcrl::time lastReceived = 0;\n\t};\n\n\tvoid parse(\n\t\tnot_null<History*> history,\n\t\tconst MTPmessages_sponsoredMessages &list);\n\tvoid parseForVideo(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPmessages_sponsoredMessages &list);\n\tvoid append(\n\t\tFn<not_null<std::vector<Entry>*>()> entries,\n\t\tnot_null<History*> history,\n\t\tconst MTPSponsoredMessage &message);\n\t[[nodiscard]] SponsoredForVideo prepareForVideo(\n\t\tnot_null<PeerData*> peer);\n\tvoid clearOldRequests();\n\n\tconst Entry *find(const FullMsgId &fullId) const;\n\n\tconst not_null<Main::Session*> _session;\n\n\tbase::Timer _clearTimer;\n\tbase::flat_map<not_null<History*>, List> _data;\n\tbase::flat_map<not_null<History*>, Request> _requests;\n\tbase::flat_map<RandomId, Request> _viewRequests;\n\n\tbase::flat_map<not_null<PeerData*>, ListForVideo> _dataForVideo;\n\tbase::flat_map<not_null<PeerData*>, RequestForVideo> _requestsForVideo;\n\n\trpl::event_stream<FullMsgId> _itemRemoved;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/top_peers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/components/top_peers.h\"\n\n#include \"api/api_hash.h\"\n#include \"apiwrap.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"storage/serialize_common.h\"\n#include \"storage/serialize_peer.h\"\n#include \"storage/storage_account.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kLimit = 64;\nconstexpr auto kRequestTimeLimit = 10 * crl::time(1000);\n\n[[nodiscard]] float64 RatingDelta(TimeId now, TimeId was, int decay) {\n\treturn std::exp((now - was) * 1. / decay);\n}\n\n[[nodiscard]] quint64 SerializeRating(float64 rating) {\n\treturn quint64(\n\t\tbase::SafeRound(std::clamp(rating, 0., 1'000'000.) * 1'000'000.));\n}\n\n[[nodiscard]] float64 DeserializeRating(quint64 rating) {\n\treturn std::clamp(\n\t\trating,\n\t\tquint64(),\n\t\tquint64(1'000'000'000'000ULL)\n\t) / 1'000'000.;\n}\n\n[[nodiscard]] MTPTopPeerCategory TypeToCategory(TopPeerType type) {\n\tswitch (type) {\n\tcase TopPeerType::Chat: return MTP_topPeerCategoryCorrespondents();\n\tcase TopPeerType::BotApp: return MTP_topPeerCategoryBotsApp();\n\t}\n\tUnexpected(\"Type in TypeToCategory.\");\n}\n\n[[nodiscard]] auto TypeToGetFlags(TopPeerType type) {\n\tusing Flag = MTPcontacts_GetTopPeers::Flag;\n\tswitch (type) {\n\tcase TopPeerType::Chat: return Flag::f_correspondents;\n\tcase TopPeerType::BotApp: return Flag::f_bots_app;\n\t}\n\tUnexpected(\"Type in TypeToGetFlags.\");\n}\n\n} // namespace\n\nTopPeers::TopPeers(not_null<Main::Session*> session, TopPeerType type)\n: _session(session)\n, _type(type) {\n\tif (_type == TopPeerType::Chat) {\n\t\tloadAfterChats();\n\t}\n}\n\nvoid TopPeers::loadAfterChats() {\n\tusing namespace rpl::mappers;\n\tcrl::on_main(_session, [=] {\n\t\t_session->data().chatsListLoadedEvents(\n\t\t) | rpl::filter(_1 == nullptr) | rpl::on_next([=] {\n\t\t\tcrl::on_main(_session, [=] {\n\t\t\t\trequest();\n\t\t\t});\n\t\t}, _session->lifetime());\n\t});\n}\n\nTopPeers::~TopPeers() = default;\n\nstd::vector<not_null<PeerData*>> TopPeers::list() const {\n\t_session->local().readSearchSuggestions();\n\n\treturn _list\n\t\t| ranges::view::transform(&TopPeer::peer)\n\t\t| ranges::to_vector;\n}\n\nbool TopPeers::disabled() const {\n\t_session->local().readSearchSuggestions();\n\n\treturn _disabled;\n}\n\nrpl::producer<> TopPeers::updates() const {\n\treturn _updates.events();\n}\n\nvoid TopPeers::remove(not_null<PeerData*> peer) {\n\tconst auto i = ranges::find(_list, peer, &TopPeer::peer);\n\tif (i != end(_list)) {\n\t\t_list.erase(i);\n\t\tupdated();\n\t}\n\n\t_requestId = _session->api().request(MTPcontacts_ResetTopPeerRating(\n\t\tTypeToCategory(_type),\n\t\tpeer->input()\n\t)).send();\n}\n\nvoid TopPeers::increment(not_null<PeerData*> peer, TimeId date) {\n\t_session->local().readSearchSuggestions();\n\n\tif (_disabled || date <= _lastReceivedDate) {\n\t\treturn;\n\t}\n\tif (const auto user = peer->asUser(); user && !user->isBot()) {\n\t\tauto changed = false;\n\t\tauto i = ranges::find(_list, peer, &TopPeer::peer);\n\t\tif (i == end(_list)) {\n\t\t\t_list.push_back({ .peer = peer });\n\t\t\ti = end(_list) - 1;\n\t\t\tchanged = true;\n\t\t}\n\t\tconst auto &config = peer->session().mtp().config();\n\t\tconst auto decay = config.values().ratingDecay;\n\t\ti->rating += RatingDelta(date, _lastReceivedDate, decay);\n\t\tfor (; i != begin(_list); --i) {\n\t\t\tif (i->rating >= (i - 1)->rating) {\n\t\t\t\tchanged = true;\n\t\t\t\tstd::swap(*i, *(i - 1));\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (changed) {\n\t\t\tupdated();\n\t\t} else {\n\t\t\t_session->local().writeSearchSuggestionsDelayed();\n\t\t}\n\t}\n}\n\nvoid TopPeers::reload() {\n\tif (_requestId\n\t\t|| (_lastReceived\n\t\t\t&& _lastReceived + kRequestTimeLimit > crl::now())) {\n\t\treturn;\n\t}\n\trequest();\n}\n\nvoid TopPeers::toggleDisabled(bool disabled) {\n\t_session->local().readSearchSuggestions();\n\n\tif (disabled) {\n\t\tif (!_disabled || !_list.empty()) {\n\t\t\t_disabled = true;\n\t\t\t_list.clear();\n\t\t\tupdated();\n\t\t}\n\t} else if (_disabled) {\n\t\t_disabled = false;\n\t\tupdated();\n\t}\n\n\t_session->api().request(MTPcontacts_ToggleTopPeers(\n\t\tMTP_bool(!disabled)\n\t)).done([=] {\n\t\tif (!_disabled) {\n\t\t\trequest();\n\t\t}\n\t}).send();\n}\n\nvoid TopPeers::request() {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\n\t_requestId = _session->api().request(MTPcontacts_GetTopPeers(\n\t\tMTP_flags(TypeToGetFlags(_type)),\n\t\tMTP_int(0),\n\t\tMTP_int(kLimit),\n\t\tMTP_long(countHash())\n\t)).done([=](\n\t\t\tconst MTPcontacts_TopPeers &result,\n\t\t\tconst MTP::Response &response) {\n\t\t_lastReceivedDate = TimeId(response.outerMsgId >> 32);\n\t\t_lastReceived = crl::now();\n\t\t_requestId = 0;\n\n\t\tresult.match([&](const MTPDcontacts_topPeers &data) {\n\t\t\t_disabled = false;\n\t\t\tconst auto owner = &_session->data();\n\t\t\towner->processUsers(data.vusers());\n\t\t\towner->processChats(data.vchats());\n\t\t\tfor (const auto &category : data.vcategories().v) {\n\t\t\t\tconst auto &data = category.data();\n\t\t\t\tconst auto cons = (_type == TopPeerType::Chat)\n\t\t\t\t\t? mtpc_topPeerCategoryCorrespondents\n\t\t\t\t\t: mtpc_topPeerCategoryBotsApp;\n\t\t\t\tif (data.vcategory().type() != cons) {\n\t\t\t\t\tLOG((\"API Error: Unexpected top peer category.\"));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t_list = ranges::views::all(\n\t\t\t\t\tdata.vpeers().v\n\t\t\t\t) | ranges::views::transform([&](\n\t\t\t\t\t\tconst MTPTopPeer &top) {\n\t\t\t\t\treturn TopPeer{\n\t\t\t\t\t\towner->peer(peerFromMTP(top.data().vpeer())),\n\t\t\t\t\t\ttop.data().vrating().v,\n\t\t\t\t\t};\n\t\t\t\t}) | ranges::to_vector;\n\t\t\t}\n\t\t\tupdated();\n\t\t}, [&](const MTPDcontacts_topPeersDisabled &) {\n\t\t\tif (!_disabled) {\n\t\t\t\t_list.clear();\n\t\t\t\t_disabled = true;\n\t\t\t\tupdated();\n\t\t\t}\n\t\t}, [](const MTPDcontacts_topPeersNotModified &) {\n\t\t});\n\t}).fail([=] {\n\t\t_lastReceived = crl::now();\n\t\t_requestId = 0;\n\t}).send();\n}\n\nuint64 TopPeers::countHash() const {\n\tusing namespace Api;\n\tauto hash = HashInit();\n\tfor (const auto &top : _list | ranges::views::take(kLimit)) {\n\t\tHashUpdate(hash, peerToUser(top.peer->id).bare);\n\t}\n\treturn HashFinalize(hash);\n}\n\nvoid TopPeers::updated() {\n\t_updates.fire({});\n\t_session->local().writeSearchSuggestionsDelayed();\n}\n\nQByteArray TopPeers::serialize() const {\n\t_session->local().readSearchSuggestions();\n\n\tif (!_disabled && _list.empty()) {\n\t\treturn {};\n\t}\n\tauto size = 3 * sizeof(quint32); // AppVersion, disabled, count\n\tconst auto count = std::min(int(_list.size()), kLimit);\n\tauto &&list = _list | ranges::views::take(count);\n\tfor (const auto &top : list) {\n\t\tsize += Serialize::peerSize(top.peer) + sizeof(quint64);\n\t}\n\tauto stream = Serialize::ByteArrayWriter(size);\n\tstream\n\t\t<< quint32(AppVersion)\n\t\t<< quint32(_disabled ? 1 : 0)\n\t\t<< quint32(count);\n\tfor (const auto &top : list) {\n\t\tSerialize::writePeer(stream, top.peer);\n\t\tstream << SerializeRating(top.rating);\n\t}\n\treturn std::move(stream).result();\n}\n\nvoid TopPeers::applyLocal(QByteArray serialized) {\n\tif (_lastReceived) {\n\t\tDEBUG_LOG((\"Suggestions: Skipping TopPeers local, got already.\"));\n\t\treturn;\n\t}\n\t_list.clear();\n\t_disabled = false;\n\tif (serialized.isEmpty()) {\n\t\tDEBUG_LOG((\"Suggestions: Bad TopPeers local, empty.\"));\n\t\treturn;\n\t}\n\tauto stream = Serialize::ByteArrayReader(serialized);\n\tauto streamAppVersion = quint32();\n\tauto disabled = quint32();\n\tauto count = quint32();\n\tstream >> streamAppVersion >> disabled >> count;\n\tif (!stream.ok()) {\n\t\tDEBUG_LOG((\"Suggestions: Bad TopPeers local, not ok.\"));\n\t\treturn;\n\t}\n\tDEBUG_LOG((\"Suggestions: \"\n\t\t\"Start TopPeers read, count: %1, version: %2, disabled: %3.\"\n\t\t).arg(count\n\t\t).arg(streamAppVersion\n\t\t).arg(disabled));\n\t_list.reserve(count);\n\tfor (auto i = 0; i != int(count); ++i) {\n\t\tauto rating = quint64();\n\t\tconst auto streamPosition = stream.underlying().device()->pos();\n\t\tconst auto peer = Serialize::readPeer(\n\t\t\t_session,\n\t\t\tstreamAppVersion,\n\t\t\tstream);\n\t\tstream >> rating;\n\t\tif (stream.ok() && peer) {\n\t\t\t_list.push_back({\n\t\t\t\t.peer = peer,\n\t\t\t\t.rating = DeserializeRating(rating),\n\t\t\t});\n\t\t} else {\n\t\t\tDEBUG_LOG((\"Suggestions: \"\n\t\t\t\t\"Failed TopPeers reading %1 / %2.\").arg(i + 1).arg(count));\n\t\t\tDEBUG_LOG((\"Failed bytes: %1.\").arg(\n\t\t\t\tQString::fromUtf8(serialized.mid(streamPosition).toHex())));\n\t\t\t_list.clear();\n\t\t\treturn;\n\t\t}\n\t}\n\t_disabled = (disabled == 1);\n\tDEBUG_LOG(\n\t\t(\"Suggestions: TopPeers read OK, count: %1\").arg(_list.size()));\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/components/top_peers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nenum class TopPeerType {\n\tChat,\n\tBotApp,\n};\n\nclass TopPeers final {\npublic:\n\tTopPeers(not_null<Main::Session*> session, TopPeerType type);\n\t~TopPeers();\n\n\t[[nodiscard]] std::vector<not_null<PeerData*>> list() const;\n\t[[nodiscard]] bool disabled() const;\n\t[[nodiscard]] rpl::producer<> updates() const;\n\n\tvoid remove(not_null<PeerData*> peer);\n\tvoid increment(not_null<PeerData*> peer, TimeId date);\n\tvoid reload();\n\tvoid toggleDisabled(bool disabled);\n\n\t[[nodiscard]] QByteArray serialize() const;\n\tvoid applyLocal(QByteArray serialized);\n\nprivate:\n\tstruct TopPeer {\n\t\tnot_null<PeerData*> peer;\n\t\tfloat64 rating = 0.;\n\t};\n\n\tvoid loadAfterChats();\n\tvoid request();\n\t[[nodiscard]] uint64 countHash() const;\n\tvoid updated();\n\n\tconst not_null<Main::Session*> _session;\n\tconst TopPeerType _type = {};\n\n\tstd::vector<TopPeer> _list;\n\trpl::event_stream<> _updates;\n\tcrl::time _lastReceived = 0;\n\tTimeId _lastReceivedDate = 0;\n\n\tmtpRequestId _requestId = 0;\n\n\tbool _disabled = false;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_abstract_sparse_ids.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\ntemplate <typename IdsContainer>\nclass AbstractSparseIds {\npublic:\n\tusing Id = typename IdsContainer::value_type;\n\n\tAbstractSparseIds() = default;\n\tAbstractSparseIds(\n\t\tconst IdsContainer &ids,\n\t\tstd::optional<int> fullCount,\n\t\tstd::optional<int> skippedBefore,\n\t\tstd::optional<int> skippedAfter)\n\t: _ids(ids)\n\t, _fullCount(fullCount)\n\t, _skippedBefore(skippedBefore)\n\t, _skippedAfter(skippedAfter) {\n\t}\n\n\t[[nodiscard]] std::optional<int> fullCount() const {\n\t\treturn _fullCount;\n\t}\n\t[[nodiscard]] std::optional<int> skippedBefore() const {\n\t\treturn _skippedBefore;\n\t}\n\t[[nodiscard]] std::optional<int> skippedAfter() const {\n\t\treturn _skippedAfter;\n\t}\n\t[[nodiscard]] std::optional<int> indexOf(Id id) const {\n\t\tconst auto it = ranges::find(_ids, id);\n\t\tif (it != _ids.end()) {\n\t\t\treturn (it - _ids.begin());\n\t\t}\n\t\treturn std::nullopt;\n\t}\n\t[[nodiscard]] int size() const {\n\t\treturn _ids.size();\n\t}\n\t[[nodiscard]] Id operator[](int index) const {\n\t\tExpects(index >= 0 && index < size());\n\n\t\treturn *(_ids.begin() + index);\n\t}\n\t[[nodiscard]] std::optional<int> distance(Id a, Id b) const {\n\t\tif (const auto i = indexOf(a)) {\n\t\t\tif (const auto j = indexOf(b)) {\n\t\t\t\treturn *j - *i;\n\t\t\t}\n\t\t}\n\t\treturn std::nullopt;\n\t}\n\t[[nodiscard]] std::optional<Id> nearest(Id id) const {\n\t\tstatic_assert(std::is_same_v<IdsContainer, base::flat_set<Id>>);\n\t\tif (const auto it = ranges::lower_bound(_ids, id); it != _ids.end()) {\n\t\t\treturn *it;\n\t\t} else if (_ids.empty()) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\treturn _ids.back();\n\t}\n\tvoid reverse() {\n\t\tranges::reverse(_ids);\n\t\tstd::swap(_skippedBefore, _skippedAfter);\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst AbstractSparseIds&,\n\t\tconst AbstractSparseIds&) = default;\n\nprivate:\n\tIdsContainer _ids;\n\tstd::optional<int> _fullCount;\n\tstd::optional<int> _skippedBefore;\n\tstd::optional<int> _skippedAfter;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_abstract_structure.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_abstract_structure.h\"\n\n#include \"base/never_freed_pointer.h\"\n\nnamespace Data {\nnamespace {\n\nusing DataStructures = OrderedSet<AbstractStructure**>;\nbase::NeverFreedPointer<DataStructures> structures;\n\n} // namespace\n\nnamespace internal {\n\nvoid registerAbstractStructure(AbstractStructure **p) {\n\tstructures.createIfNull();\n\tstructures->insert(p);\n}\n\n} // namespace internal\n\nvoid clearGlobalStructures() {\n\tif (!structures) return;\n\tfor (auto &p : *structures) {\n\t\tdelete (*p);\n\t\t*p = nullptr;\n\t}\n\tstructures.clear();\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_abstract_structure.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\n// This module suggests a way to hold global data structures, that are\n// created on demand and deleted at the end of the app launch.\n//\n// Usage:\n//\n// class MyData : public Data::AbstractStruct { .. data .. };\n// Data::GlobalStructurePointer<MyData> myData;\n// .. somewhere when needed ..\n// myData.createIfNull();\n\nclass AbstractStructure {\npublic:\n\tvirtual ~AbstractStructure() = 0;\n};\ninline AbstractStructure::~AbstractStructure() = default;\n\nnamespace internal {\n\nvoid registerAbstractStructure(AbstractStructure **p);\n\n} // namespace\n\n  // Must be created in global scope!\n  // Structure is derived from AbstractStructure.\ntemplate <typename Structure>\nclass GlobalStructurePointer {\npublic:\n\tGlobalStructurePointer() = default;\n\tGlobalStructurePointer(const GlobalStructurePointer<Structure> &other) = delete;\n\tGlobalStructurePointer &operator=(const GlobalStructurePointer<Structure> &other) = delete;\n\n\tvoid createIfNull() {\n\t\tif (!_p) {\n\t\t\t_p = new Structure();\n\t\t\tinternal::registerAbstractStructure(&_p);\n\t\t}\n\t}\n\tStructure *operator->() {\n\t\tAssert(_p != nullptr);\n\t\treturn static_cast<Structure*>(_p);\n\t}\n\tconst Structure *operator->() const {\n\t\tAssert(_p != nullptr);\n\t\treturn static_cast<const Structure*>(_p);\n\t}\n\texplicit operator bool() const {\n\t\treturn _p != nullptr;\n\t}\n\nprivate:\n\tAbstractStructure *_p;\n\n};\n\n// This method should be called at the end of the app launch.\n// It will destroy all data structures created by Data::GlobalStructurePointer.\nvoid clearGlobalStructures();\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_audio_msg_id.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_audio_msg_id.h\"\n\n#include \"data/data_document.h\"\n\nnamespace {\n\nconstexpr auto kMinLengthForChangeablePlaybackSpeed = 20 * TimeId(60); // 20 minutes.\n\n} // namespace\n\nAudioMsgId::AudioMsgId() {\n}\n\nAudioMsgId::AudioMsgId(\n\tnot_null<DocumentData*> audio,\n\tFullMsgId msgId,\n\tuint32 externalPlayId)\n: _audio(audio)\n, _contextId(msgId)\n, _externalPlayId(externalPlayId)\n, _changeablePlaybackSpeed(_audio->isVoiceMessage()\n\t|| _audio->isVideoMessage()\n\t|| (_audio->duration() >= kMinLengthForChangeablePlaybackSpeed)) {\n\tsetTypeFromAudio();\n}\n\nuint32 AudioMsgId::CreateExternalPlayId() {\n\tstatic auto Result = uint32(0);\n\treturn ++Result ? Result : ++Result;\n}\n\nAudioMsgId AudioMsgId::ForVideo() {\n\tauto result = AudioMsgId();\n\tresult._externalPlayId = CreateExternalPlayId();\n\tresult._type = Type::Video;\n\treturn result;\n}\n\nvoid AudioMsgId::setTypeFromAudio() {\n\tif (_audio->isVoiceMessage() || _audio->isVideoMessage()) {\n\t\t_type = Type::Voice;\n\t} else if (_audio->isVideoFile()) {\n\t\t_type = Type::Video;\n\t} else if (_audio->isAudioFile()) {\n\t\t_type = Type::Song;\n\t} else {\n\t\t_type = Type::Unknown;\n\t}\n}\n\nAudioMsgId::Type AudioMsgId::type() const {\n\treturn _type;\n}\n\nDocumentData *AudioMsgId::audio() const {\n\treturn _audio;\n}\n\nFullMsgId AudioMsgId::contextId() const {\n\treturn _contextId;\n}\n\nuint32 AudioMsgId::externalPlayId() const {\n\treturn _externalPlayId;\n}\n\nbool AudioMsgId::changeablePlaybackSpeed() const {\n\treturn _changeablePlaybackSpeed;\n}\n\nAudioMsgId::operator bool() const {\n\treturn (_audio != nullptr) || (_externalPlayId != 0);\n}\n\nbool AudioMsgId::operator<(const AudioMsgId &other) const {\n\tif (quintptr(audio()) < quintptr(other.audio())) {\n\t\treturn true;\n\t} else if (quintptr(other.audio()) < quintptr(audio())) {\n\t\treturn false;\n\t} else if (contextId() < other.contextId()) {\n\t\treturn true;\n\t} else if (other.contextId() < contextId()) {\n\t\treturn false;\n\t}\n\treturn (externalPlayId() < other.externalPlayId());\n}\n\nbool AudioMsgId::operator==(const AudioMsgId &other) const {\n\treturn (audio() == other.audio())\n\t\t&& (contextId() == other.contextId())\n\t\t&& (externalPlayId() == other.externalPlayId());\n}\n\nbool AudioMsgId::operator!=(const AudioMsgId &other) const {\n\treturn !(*this == other);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_audio_msg_id.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass DocumentData;\n\nclass AudioMsgId final {\npublic:\n\tenum class Type {\n\t\tUnknown,\n\t\tVoice,\n\t\tSong,\n\t\tVideo,\n\t};\n\n\tAudioMsgId();\n\tAudioMsgId(\n\t\tnot_null<DocumentData*> audio,\n\t\tFullMsgId msgId,\n\t\tuint32 externalPlayId = 0);\n\n\t[[nodiscard]] static uint32 CreateExternalPlayId();\n\t[[nodiscard]] static AudioMsgId ForVideo();\n\n\t[[nodiscard]] Type type() const;\n\t[[nodiscard]] DocumentData *audio() const;\n\t[[nodiscard]] FullMsgId contextId() const;\n\t[[nodiscard]] uint32 externalPlayId() const;\n\t[[nodiscard]] bool changeablePlaybackSpeed() const;\n\t[[nodiscard]] explicit operator bool() const;\n\n\tbool operator<(const AudioMsgId &other) const;\n\tbool operator==(const AudioMsgId &other) const;\n\tbool operator!=(const AudioMsgId &other) const;\n\nprivate:\n\tvoid setTypeFromAudio();\n\n\tDocumentData *_audio = nullptr;\n\tType _type = Type::Unknown;\n\tFullMsgId _contextId;\n\tuint32 _externalPlayId = 0;\n\tbool _changeablePlaybackSpeed = false;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_authorization.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\nstruct UnreviewedAuth {\n\tuint64 hash = 0;\n\tbool unconfirmed = false;\n\tTimeId date = 0;\n\tQString device;\n\tQString location;\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_auto_download.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_auto_download.h\"\n\n#include \"data/data_peer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_document.h\"\n\n#include <QtCore/QBuffer>\n\nnamespace Data {\nnamespace AutoDownload {\nnamespace {\n\nconstexpr auto kDefaultMaxSize = 8 * int64(1024 * 1024);\nconstexpr auto kDefaultAutoPlaySize = 50 * int64(1024 * 1024);\nconstexpr auto kVersion1 = char(1);\nconstexpr auto kVersion = char(2);\n\ntemplate <typename Enum>\nauto enums_view(int from, int till) {\n\tusing namespace ranges::views;\n\treturn ints(from, till) | transform([](int index) {\n\t\treturn static_cast<Enum>(index);\n\t});\n}\n\ntemplate <typename Enum>\nauto enums_view(int till) {\n\treturn enums_view<Enum>(0, till);\n}\n\nvoid SetDefaultsForSource(Full &data, Source source) {\n\tdata.setBytesLimit(source, Type::Photo, kDefaultMaxSize);\n\tdata.setBytesLimit(source, Type::VoiceMessage, kDefaultMaxSize);\n\tdata.setBytesLimit(\n\t\tsource,\n\t\tType::AutoPlayVideoMessage,\n\t\tkDefaultAutoPlaySize);\n\tdata.setBytesLimit(source, Type::AutoPlayGIF, kDefaultAutoPlaySize);\n\tconst auto channelsFileLimit = (source == Source::Channel)\n\t\t? 0\n\t\t: kDefaultMaxSize;\n\tdata.setBytesLimit(source, Type::File, channelsFileLimit);\n\tdata.setBytesLimit(source, Type::AutoPlayVideo, kDefaultAutoPlaySize);\n\tdata.setBytesLimit(source, Type::Music, channelsFileLimit);\n}\n\nconst Full &Defaults() {\n\tstatic auto Result = [] {\n\t\tauto result = Full::FullDisabled();\n\t\tfor (const auto source : enums_view<Source>(kSourcesCount)) {\n\t\t\tSetDefaultsForSource(result, source);\n\t\t}\n\t\treturn result;\n\t}();\n\treturn Result;\n}\n\nSource SourceFromPeer(not_null<PeerData*> peer) {\n\tif (peer->isUser()) {\n\t\treturn Source::User;\n\t} else if (peer->isChat() || peer->isMegagroup()) {\n\t\treturn Source::Group;\n\t} else {\n\t\treturn Source::Channel;\n\t}\n}\n\nType AutoPlayTypeFromDocument(not_null<DocumentData*> document) {\n\treturn document->isVideoFile()\n\t\t? Type::AutoPlayVideo\n\t\t: document->isVideoMessage()\n\t\t? Type::AutoPlayVideoMessage\n\t\t: Type::AutoPlayGIF;\n}\n\n} // namespace\n\nvoid Single::setBytesLimit(int64 bytesLimit) {\n\tExpects(bytesLimit >= 0 && bytesLimit <= kMaxBytesLimit);\n\n\t_limit = int32(uint32(bytesLimit));\n\n\tEnsures(hasValue());\n}\n\nbool Single::hasValue() const {\n\treturn (_limit != -1);\n}\n\nbool Single::shouldDownload(int64 fileSize) const {\n\tExpects(hasValue());\n\n\tconst auto realLimit = bytesLimit();\n\treturn (realLimit > 0) && (fileSize <= realLimit);\n}\n\nint64 Single::bytesLimit() const {\n\tExpects(hasValue());\n\n\treturn uint32(_limit);\n}\n\nqint32 Single::serialize() const {\n\treturn _limit;\n}\n\nbool Single::setFromSerialized(qint32 serialized) {\n\tauto realLimit = quint32(serialized);\n\tif (serialized != -1 && int64(realLimit) > kMaxBytesLimit) {\n\t\treturn false;\n\t}\n\t_limit = serialized;\n\treturn true;\n}\n\nconst Single &Set::single(Type type) const {\n\tExpects(static_cast<int>(type) >= 0\n\t\t&& static_cast<int>(type) < kTypesCount);\n\n\treturn _data[static_cast<int>(type)];\n}\n\nSingle &Set::single(Type type) {\n\treturn const_cast<Single&>(static_cast<const Set*>(this)->single(type));\n}\n\nvoid Set::setBytesLimit(Type type, int64 bytesLimit) {\n\tsingle(type).setBytesLimit(bytesLimit);\n}\n\nbool Set::hasValue(Type type) const {\n\treturn single(type).hasValue();\n}\n\nbool Set::shouldDownload(Type type, int64 fileSize) const {\n\treturn single(type).shouldDownload(fileSize);\n}\n\nint64 Set::bytesLimit(Type type) const {\n\treturn single(type).bytesLimit();\n}\n\nqint32 Set::serialize(Type type) const {\n\treturn single(type).serialize();\n}\n\nbool Set::setFromSerialized(Type type, qint32 serialized) {\n\tif (static_cast<int>(type) < 0\n\t\t|| static_cast<int>(type) >= kTypesCount) {\n\t\treturn false;\n\t}\n\treturn single(type).setFromSerialized(serialized);\n}\n\nconst Set &Full::set(Source source) const {\n\tExpects(static_cast<int>(source) >= 0\n\t\t&& static_cast<int>(source) < kSourcesCount);\n\n\treturn _data[static_cast<int>(source)];\n}\n\nSet &Full::set(Source source) {\n\treturn const_cast<Set&>(static_cast<const Full*>(this)->set(source));\n}\n\nconst Set &Full::setOrDefault(Source source, Type type) const {\n\tconst auto &my = set(source);\n\tconst auto &result = my.hasValue(type) ? my : Defaults().set(source);\n\n\tEnsures(result.hasValue(type));\n\treturn result;\n}\n\nvoid Full::setBytesLimit(Source source, Type type, int64 bytesLimit) {\n\tset(source).setBytesLimit(type, bytesLimit);\n}\n\nbool Full::shouldDownload(Source source, Type type, int64 fileSize) const {\n\tif (ranges::find(kStreamedTypes, type) != end(kStreamedTypes)) {\n\t\t// With streaming we disable autodownload and hide them in Settings.\n\t\treturn false;\n\t}\n\treturn setOrDefault(source, type).shouldDownload(type, fileSize);\n}\n\nint64 Full::bytesLimit(Source source, Type type) const {\n\treturn setOrDefault(source, type).bytesLimit(type);\n}\n\nQByteArray Full::serialize() const {\n\tauto result = QByteArray();\n\tauto size = sizeof(qint8);\n\tsize += kSourcesCount * kTypesCount * sizeof(qint32);\n\tresult.reserve(size);\n\t{\n\t\tauto buffer = QBuffer(&result);\n\t\tbuffer.open(QIODevice::WriteOnly);\n\t\tauto stream = QDataStream(&buffer);\n\t\tstream << qint8(kVersion);\n\t\tfor (const auto source : enums_view<Source>(kSourcesCount)) {\n\t\t\tfor (const auto type : enums_view<Type>(kTypesCount)) {\n\t\t\t\tstream << set(source).serialize(type);\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nbool Full::setFromSerialized(const QByteArray &serialized) {\n\tif (serialized.isEmpty()) {\n\t\treturn false;\n\t}\n\n\tauto stream = QDataStream(serialized);\n\tauto version = qint8();\n\tstream >> version;\n\tif (stream.status() != QDataStream::Ok) {\n\t\treturn false;\n\t} else if (version != kVersion && version != kVersion1) {\n\t\treturn false;\n\t}\n\tauto temp = Full();\n\tfor (const auto source : enums_view<Source>(kSourcesCount)) {\n\t\tfor (const auto type : enums_view<Type>(kTypesCount)) {\n\t\t\tauto value = qint32();\n\t\t\tstream >> value;\n\t\t\tif (!temp.set(source).setFromSerialized(type, value)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\tif (version == kVersion1) {\n\t\tfor (const auto source : enums_view<Source>(kSourcesCount)) {\n\t\t\tfor (const auto type : kAutoPlayTypes) {\n\t\t\t\ttemp.setBytesLimit(source, type, std::max(\n\t\t\t\t\ttemp.bytesLimit(source, type),\n\t\t\t\t\tkDefaultAutoPlaySize));\n\t\t\t}\n\t\t}\n\t}\n\t_data = temp._data;\n\treturn true;\n}\n\nFull Full::FullDisabled() {\n\tauto result = Full();\n\tfor (const auto source : enums_view<Source>(kSourcesCount)) {\n\t\tfor (const auto type : enums_view<Type>(kTypesCount)) {\n\t\t\tresult.setBytesLimit(source, type, 0);\n\t\t}\n\t}\n\treturn result;\n}\n\nbool Should(\n\t\tconst Full &data,\n\t\tSource source,\n\t\tnot_null<DocumentData*> document) {\n\tif (document->sticker() || document->isGifv()) {\n\t\treturn true;\n\t} else if (document->isVoiceMessage()\n\t\t|| document->isVideoMessage()\n\t\t|| document->isSong()\n\t\t|| document->isVideoFile()) {\n\t\treturn false;\n\t}\n\treturn data.shouldDownload(source, Type::File, document->size);\n}\n\nbool Should(\n\t\tconst Full &data,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<DocumentData*> document) {\n\treturn Should(data, SourceFromPeer(peer), document);\n}\n\nbool Should(\n\t\tconst Full &data,\n\t\tnot_null<DocumentData*> document) {\n\tif (document->sticker()) {\n\t\treturn true;\n\t}\n\treturn Should(data, Source::User, document)\n\t\t|| Should(data, Source::Group, document)\n\t\t|| Should(data, Source::Channel, document);\n}\n\nbool Should(\n\t\tconst Full &data,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<PhotoData*> photo) {\n\treturn data.shouldDownload(\n\t\tSourceFromPeer(peer),\n\t\tType::Photo,\n\t\tphoto->imageByteSize(PhotoSize::Large));\n}\n\nbool ShouldAutoPlay(\n\t\tconst Full &data,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<DocumentData*> document) {\n\treturn document->sticker() || data.shouldDownload(\n\t\tSourceFromPeer(peer),\n\t\tAutoPlayTypeFromDocument(document),\n\t\tdocument->size);\n}\n\nbool ShouldAutoPlay(\n\t\tconst Full &data,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<PhotoData*> photo) {\n\tconst auto source = SourceFromPeer(peer);\n\tconst auto size = photo->videoByteSize(PhotoSize::Large);\n\treturn photo->hasVideo()\n\t\t&& (data.shouldDownload(source, Type::AutoPlayGIF, size)\n\t\t\t|| data.shouldDownload(source, Type::AutoPlayVideo, size)\n\t\t\t|| data.shouldDownload(source, Type::AutoPlayVideoMessage, size));\n}\n\nFull WithDisabledAutoPlay(const Full &data) {\n\tauto result = data;\n\tfor (const auto source : enums_view<Source>(kSourcesCount)) {\n\t\tfor (const auto type : kAutoPlayTypes) {\n\t\t\tresult.setBytesLimit(source, type, 0);\n\t\t}\n\t}\n\treturn result;\n}\n\n} // namespace AutoDownload\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_auto_download.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <array>\n\nnamespace Data {\nnamespace AutoDownload {\n\nconstexpr auto kMaxBytesLimit = 8000 * int64(512 * 1024);\n\nenum class Source {\n\tUser    = 0x00,\n\tGroup   = 0x01,\n\tChannel = 0x02,\n};\n\nconstexpr auto kSourcesCount = 3;\n\nenum class Type {\n\tPhoto                = 0x00,\n\tAutoPlayVideo        = 0x01,\n\tVoiceMessage         = 0x02,\n\tAutoPlayVideoMessage = 0x03,\n\tMusic                = 0x04,\n\tAutoPlayGIF          = 0x05,\n\tFile                 = 0x06,\n};\n\ninline constexpr auto kAutoPlayTypes = {\n\tType::AutoPlayVideo,\n\tType::AutoPlayVideoMessage,\n\tType::AutoPlayGIF,\n};\n\ninline constexpr auto kStreamedTypes = {\n\tType::VoiceMessage,\n\tType::Music,\n};\n\nconstexpr auto kTypesCount = 7;\n\nclass Single {\npublic:\n\tvoid setBytesLimit(int64 bytesLimit);\n\n\tbool hasValue() const;\n\tbool shouldDownload(int64 fileSize) const;\n\tint64 bytesLimit() const;\n\n\tqint32 serialize() const;\n\tbool setFromSerialized(qint32 serialized);\n\nprivate:\n\tint _limit = -1; // FileSize: Right now any file size fits 32 bit.\n\n};\n\nclass Set {\npublic:\n\tvoid setBytesLimit(Type type, int64 bytesLimit);\n\n\tbool hasValue(Type type) const;\n\tbool shouldDownload(Type type, int64 fileSize) const;\n\tint64 bytesLimit(Type type) const;\n\n\tqint32 serialize(Type type) const;\n\tbool setFromSerialized(Type type, qint32 serialized);\n\nprivate:\n\tconst Single &single(Type type) const;\n\tSingle &single(Type type);\n\n\tstd::array<Single, kTypesCount> _data;\n\n};\n\nclass Full {\npublic:\n\tvoid setBytesLimit(Source source, Type type, int64 bytesLimit);\n\n\t[[nodiscard]] bool shouldDownload(\n\t\tSource source,\n\t\tType type,\n\t\tint64 fileSize) const;\n\t[[nodiscard]] int64 bytesLimit(Source source, Type type) const;\n\n\t[[nodiscard]] QByteArray serialize() const;\n\tbool setFromSerialized(const QByteArray &serialized);\n\n\t[[nodiscard]] static Full FullDisabled();\n\nprivate:\n\t[[nodiscard]] const Set &set(Source source) const;\n\t[[nodiscard]] Set &set(Source source);\n\t[[nodiscard]] const Set &setOrDefault(Source source, Type type) const;\n\n\tstd::array<Set, kSourcesCount> _data;\n\n};\n\n[[nodiscard]] bool Should(\n\tconst Full &data,\n\tnot_null<PeerData*> peer,\n\tnot_null<DocumentData*> document);\n[[nodiscard]] bool Should(\n\tconst Full &data,\n\tnot_null<DocumentData*> document);\n[[nodiscard]] bool Should(\n\tconst Full &data,\n\tnot_null<PeerData*> peer,\n\tnot_null<PhotoData*> photo);\n\n[[nodiscard]] bool ShouldAutoPlay(\n\tconst Full &data,\n\tnot_null<PeerData*> peer,\n\tnot_null<DocumentData*> document);\n[[nodiscard]] bool ShouldAutoPlay(\n\tconst Full &data,\n\tnot_null<PeerData*> peer,\n\tnot_null<PhotoData*> photo);\n\n[[nodiscard]] Full WithDisabledAutoPlay(const Full &data);\n\n} // namespace AutoDownload\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_birthday.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_birthday.h\"\n\n#include \"base/timer_rpl.h\"\n#include \"lang/lang_keys.h\"\n\n#include <QtCore/QDate>\n\nnamespace Data {\nnamespace {\n\n[[nodiscard]] bool Validate(int day, int month, int year) {\n\tif (year != 0\n\t\t&& (year < Birthday::kYearMin || year > Birthday::kYearMax)) {\n\t\treturn false;\n\t} else if (day < 1) {\n\t\treturn false;\n\t} else if (month == 2) {\n\t\tif (day == 29) {\n\t\t\treturn !year\n\t\t\t\t|| (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));\n\t\t}\n\t\treturn day <= 28;\n\t} else if (month == 4 || month == 6 || month == 9 || month == 11) {\n\t\treturn day <= 30;\n\t} else if (month > 0 && month <= 12) {\n\t\treturn day <= 31;\n\t}\n\treturn false;\n}\n\n[[nodiscard]] int Serialize(int day, int month, int year) {\n\treturn day + month * 100 + year * 10000;\n}\n\n} // namespace\n\nBirthday::Birthday(int day, int month, int year)\n: _value(Validate(day, month, year) ? Serialize(day, month, year) : 0) {\n}\n\nBirthday Birthday::FromSerialized(int value) {\n\treturn Birthday(value % 100, (value / 100) % 100, value / 10000);\n}\n\nint Birthday::serialize() const {\n\treturn _value;\n}\n\nbool Birthday::valid() const {\n\treturn _value != 0;\n}\n\nint Birthday::day() const {\n\treturn _value % 100;\n}\n\nint Birthday::month() const {\n\treturn (_value / 100) % 100;\n}\n\nint Birthday::year() const {\n\treturn _value / 10000;\n}\n\nQString BirthdayText(Birthday date, bool fullMonth) {\n\tconst auto wrapMonth = fullMonth\n\t\t? Lang::MonthDay\n\t\t: Lang::MonthSmall;\n\tif (const auto year = date.year()) {\n\t\treturn tr::lng_month_day_year(\n\t\t\ttr::now,\n\t\t\tlt_month,\n\t\t\twrapMonth(date.month())(tr::now),\n\t\t\tlt_day,\n\t\t\tQString::number(date.day()),\n\t\t\tlt_year,\n\t\t\tQString::number(year));\n\t} else if (date) {\n\t\treturn tr::lng_month_day(\n\t\t\ttr::now,\n\t\t\tlt_month,\n\t\t\twrapMonth(date.month())(tr::now),\n\t\t\tlt_day,\n\t\t\tQString::number(date.day()));\n\t}\n\treturn QString();\n}\n\nQString BirthdayCake() {\n\treturn QString::fromUtf8(\"\\xf0\\x9f\\x8e\\x82\");\n}\n\nint BirthdayAge(Birthday date) {\n\tif (!date.year()) {\n\t\treturn 0;\n\t}\n\tconst auto now = QDate::currentDate();\n\tconst auto day = QDate(date.year(), date.month(), date.day());\n\tif (!day.isValid() || day >= now) {\n\t\treturn 0;\n\t}\n\tauto age = now.year() - date.year();\n\tif (now < QDate(date.year() + age, date.month(), date.day())) {\n\t\t--age;\n\t}\n\treturn age;\n}\n\nbool IsBirthdayToday(Birthday date) {\n\tif (!date) {\n\t\treturn false;\n\t}\n\tconst auto now = QDate::currentDate();\n\treturn date.day() == now.day() && date.month() == now.month();\n}\n\nrpl::producer<bool> IsBirthdayTodayValue(Birthday date) {\n\treturn rpl::single() | rpl::then(base::timer_each(\n\t\t60 * crl::time(1000)\n\t)) | rpl::map([=] {\n\t\treturn IsBirthdayToday(date);\n\t}) | rpl::distinct_until_changed();\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_birthday.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\nclass Birthday final {\npublic:\n\tconstexpr Birthday() = default;\n\tBirthday(int day, int month, int year = 0);\n\n\t[[nodiscard]] static Birthday FromSerialized(int value);\n\t[[nodiscard]] int serialize() const;\n\n\t[[nodiscard]] bool valid() const;\n\n\t[[nodiscard]] int day() const;\n\t[[nodiscard]] int month() const;\n\t[[nodiscard]] int year() const;\n\n\texplicit operator bool() const {\n\t\treturn valid();\n\t}\n\n\tfriend inline constexpr auto operator<=>(Birthday, Birthday) = default;\n\tfriend inline constexpr bool operator==(Birthday, Birthday) = default;\n\n\tstatic constexpr auto kYearMin = 1875;\n\tstatic constexpr auto kYearMax = 2100;\n\nprivate:\n\tint _value = 0;\n\n};\n\n[[nodiscard]] QString BirthdayText(Birthday date, bool fullMonth = false);\n[[nodiscard]] QString BirthdayCake();\n[[nodiscard]] int BirthdayAge(Birthday date);\n[[nodiscard]] bool IsBirthdayToday(Birthday date);\n[[nodiscard]] rpl::producer<bool> IsBirthdayTodayValue(Birthday date);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_boosts.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\nstruct BoostsOverview final {\n\tbool group = false;\n\tint mine = 0;\n\tint level = 0;\n\tint boostCount = 0;\n\tint currentLevelBoostCount = 0;\n\tint nextLevelBoostCount = 0;\n\tint premiumMemberCount = 0;\n\tfloat64 premiumMemberPercentage = 0;\n};\n\nstruct GiftCodeLink final {\n\tQString text;\n\tQString link;\n\tQString slug;\n};\n\nstruct Boost final {\n\tQString id;\n\tUserId userId = UserId(0);\n\tFullMsgId giveawayMessage;\n\tQDateTime date;\n\tQDateTime expiresAt;\n\tint expiresAfterMonths = 0;\n\tGiftCodeLink giftCodeLink;\n\tint multiplier = 0;\n\tuint64 credits = 0;\n\n\tbool isGift = false;\n\tbool isGiveaway = false;\n\tbool isUnclaimed = false;\n};\n\nstruct BoostsListSlice final {\n\tstruct OffsetToken final {\n\t\tQString next;\n\t\tbool gifts = false;\n\t};\n\tstd::vector<Boost> list;\n\tint multipliedTotal = 0;\n\tbool allLoaded = false;\n\tOffsetToken token;\n};\n\nstruct BoostPrepaidGiveaway final {\n\tQDateTime date;\n\tuint64 id = 0;\n\tuint64 credits = 0;\n\tint months = 0;\n\tint quantity = 0;\n\tint boosts = 0;\n};\n\nstruct BoostStatus final {\n\tBoostsOverview overview;\n\tBoostsListSlice firstSliceBoosts;\n\tBoostsListSlice firstSliceGifts;\n\tstd::vector<BoostPrepaidGiveaway> prepaidGiveaway;\n\tQString link;\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_bot_app.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_bot_app.h\"\n\nBotAppData::BotAppData(not_null<Data::Session*> owner, const BotAppId &id)\n: owner(owner)\n, id(id) {\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_bot_app.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_photo.h\"\n#include \"data/data_document.h\"\n\nstruct BotAppData {\n\tBotAppData(not_null<Data::Session*> owner, const BotAppId &id);\n\n\tconst not_null<Data::Session*> owner;\n\tBotAppId id = 0;\n\tPeerId botId = 0;\n\tQString shortName;\n\tQString title;\n\tQString description;\n\tPhotoData *photo = nullptr;\n\tDocumentData *document = nullptr;\n\n\tuint64 accessHash = 0;\n\tuint64 hash = 0;\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_changes.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_changes.h\"\n\n#include \"main/main_session.h\"\n\nnamespace Data {\n\ntemplate <typename DataType, typename UpdateType>\nvoid Changes::Manager<DataType, UpdateType>::updated(\n\t\tnot_null<DataType*> data,\n\t\tFlags flags,\n\t\tbool dropScheduled) {\n\tsendRealtimeNotifications(data, flags);\n\tif (dropScheduled) {\n\t\tconst auto i = _updates.find(data);\n\t\tif (i != _updates.end()) {\n\t\t\tflags |= i->second;\n\t\t\t_updates.erase(i);\n\t\t}\n\t\t_stream.fire({ data, flags });\n\t} else {\n\t\t_updates[data] |= flags;\n\t}\n}\n\ntemplate <typename DataType, typename UpdateType>\nvoid Changes::Manager<DataType, UpdateType>::sendRealtimeNotifications(\n\t\tnot_null<DataType*> data,\n\t\tFlags flags) {\n\tfor (auto i = 0; i != kCount; ++i) {\n\t\tconst auto flag = static_cast<Flag>(1U << i);\n\t\tif (flags & flag) {\n\t\t\t_realtimeStreams[i].fire({ data, flags });\n\t\t}\n\t}\n}\n\ntemplate <typename DataType, typename UpdateType>\nrpl::producer<UpdateType> Changes::Manager<DataType, UpdateType>::updates(\n\t\tFlags flags) const {\n\treturn _stream.events(\n\t) | rpl::filter([=](const UpdateType &update) {\n\t\treturn (update.flags & flags);\n\t});\n}\n\ntemplate <typename DataType, typename UpdateType>\nrpl::producer<UpdateType> Changes::Manager<DataType, UpdateType>::updates(\n\t\tnot_null<DataType*> data,\n\t\tFlags flags) const {\n\treturn _stream.events(\n\t) | rpl::filter([=](const UpdateType &update) {\n\t\tconst auto &[updateData, updateFlags] = update;\n\t\treturn (updateData == data) && (updateFlags & flags);\n\t});\n}\n\ntemplate <typename DataType, typename UpdateType>\nauto Changes::Manager<DataType, UpdateType>::realtimeUpdates(Flag flag) const\n-> rpl::producer<UpdateType> {\n\treturn _realtimeStreams[details::CountBit(flag)].events();\n}\n\ntemplate <typename DataType, typename UpdateType>\nrpl::producer<UpdateType> Changes::Manager<DataType, UpdateType>::flagsValue(\n\t\tnot_null<DataType*> data,\n\t\tFlags flags) const {\n\treturn rpl::single(\n\t\tUpdateType{ data, flags }\n\t) | rpl::then(updates(data, flags));\n}\n\ntemplate <typename DataType, typename UpdateType>\nvoid Changes::Manager<DataType, UpdateType>::drop(not_null<DataType*> data) {\n\t_updates.remove(data);\n}\n\ntemplate <typename DataType, typename UpdateType>\nvoid Changes::Manager<DataType, UpdateType>::sendNotifications() {\n\tfor (const auto &[data, flags] : base::take(_updates)) {\n\t\t_stream.fire({ data, flags });\n\t}\n}\n\nChanges::Changes(not_null<Main::Session*> session) : _session(session) {\n}\n\nMain::Session &Changes::session() const {\n\treturn *_session;\n}\n\nvoid Changes::nameUpdated(\n\t\tnot_null<PeerData*> peer,\n\t\tbase::flat_set<QChar> oldFirstLetters) {\n\t_nameStream.fire({ peer, std::move(oldFirstLetters) });\n}\n\nrpl::producer<NameUpdate> Changes::realtimeNameUpdates() const {\n\treturn _nameStream.events();\n}\n\nrpl::producer<NameUpdate> Changes::realtimeNameUpdates(\n\t\tnot_null<PeerData*> peer) const {\n\treturn _nameStream.events() | rpl::filter([=](const NameUpdate &update) {\n\t\treturn (update.peer == peer);\n\t});\n}\n\nvoid Changes::peerUpdated(not_null<PeerData*> peer, PeerUpdate::Flags flags) {\n\t_peerChanges.updated(peer, flags);\n\tscheduleNotifications();\n}\n\nrpl::producer<PeerUpdate> Changes::peerUpdates(\n\t\tPeerUpdate::Flags flags) const {\n\treturn _peerChanges.updates(flags);\n}\n\nrpl::producer<PeerUpdate> Changes::peerUpdates(\n\t\tnot_null<PeerData*> peer,\n\t\tPeerUpdate::Flags flags) const {\n\treturn _peerChanges.updates(peer, flags);\n}\n\nrpl::producer<PeerUpdate> Changes::peerFlagsValue(\n\t\tnot_null<PeerData*> peer,\n\t\tPeerUpdate::Flags flags) const {\n\treturn _peerChanges.flagsValue(peer, flags);\n}\n\nrpl::producer<PeerUpdate> Changes::realtimePeerUpdates(\n\t\tPeerUpdate::Flag flag) const {\n\treturn _peerChanges.realtimeUpdates(flag);\n}\n\nvoid Changes::historyUpdated(\n\t\tnot_null<History*> history,\n\t\tHistoryUpdate::Flags flags) {\n\t_historyChanges.updated(history, flags);\n\tscheduleNotifications();\n}\n\nrpl::producer<HistoryUpdate> Changes::historyUpdates(\n\t\tHistoryUpdate::Flags flags) const {\n\treturn _historyChanges.updates(flags);\n}\n\nrpl::producer<HistoryUpdate> Changes::historyUpdates(\n\t\tnot_null<History*> history,\n\t\tHistoryUpdate::Flags flags) const {\n\treturn _historyChanges.updates(history, flags);\n}\n\nrpl::producer<HistoryUpdate> Changes::historyFlagsValue(\n\t\tnot_null<History*> history,\n\t\tHistoryUpdate::Flags flags) const {\n\treturn _historyChanges.flagsValue(history, flags);\n}\n\nrpl::producer<HistoryUpdate> Changes::realtimeHistoryUpdates(\n\t\tHistoryUpdate::Flag flag) const {\n\treturn _historyChanges.realtimeUpdates(flag);\n}\n\nvoid Changes::topicUpdated(\n\t\tnot_null<ForumTopic*> topic,\n\t\tTopicUpdate::Flags flags) {\n\tconst auto drop = (flags & TopicUpdate::Flag::Destroyed);\n\t_topicChanges.updated(topic, flags, drop);\n\tif (!drop) {\n\t\tscheduleNotifications();\n\t}\n}\n\nrpl::producer<TopicUpdate> Changes::topicUpdates(\n\t\tTopicUpdate::Flags flags) const {\n\treturn _topicChanges.updates(flags);\n}\n\nrpl::producer<TopicUpdate> Changes::topicUpdates(\n\t\tnot_null<ForumTopic*> topic,\n\t\tTopicUpdate::Flags flags) const {\n\treturn _topicChanges.updates(topic, flags);\n}\n\nrpl::producer<TopicUpdate> Changes::topicFlagsValue(\n\t\tnot_null<ForumTopic*> topic,\n\t\tTopicUpdate::Flags flags) const {\n\treturn _topicChanges.flagsValue(topic, flags);\n}\n\nrpl::producer<TopicUpdate> Changes::realtimeTopicUpdates(\n\t\tTopicUpdate::Flag flag) const {\n\treturn _topicChanges.realtimeUpdates(flag);\n}\n\nvoid Changes::topicRemoved(not_null<ForumTopic*> topic) {\n\t_topicChanges.drop(topic);\n}\n\nvoid Changes::sublistUpdated(\n\t\tnot_null<SavedSublist*> sublist,\n\t\tSublistUpdate::Flags flags) {\n\tconst auto drop = (flags & SublistUpdate::Flag::Destroyed);\n\t_sublistChanges.updated(sublist, flags, drop);\n\tif (!drop) {\n\t\tscheduleNotifications();\n\t}\n}\n\nrpl::producer<SublistUpdate> Changes::sublistUpdates(\n\t\tSublistUpdate::Flags flags) const {\n\treturn _sublistChanges.updates(flags);\n}\n\nrpl::producer<SublistUpdate> Changes::sublistUpdates(\n\t\tnot_null<SavedSublist*> sublist,\n\t\tSublistUpdate::Flags flags) const {\n\treturn _sublistChanges.updates(sublist, flags);\n}\n\nrpl::producer<SublistUpdate> Changes::sublistFlagsValue(\n\t\tnot_null<SavedSublist*> sublist,\n\t\tSublistUpdate::Flags flags) const {\n\treturn _sublistChanges.flagsValue(sublist, flags);\n}\n\nrpl::producer<SublistUpdate> Changes::realtimeSublistUpdates(\n\t\tSublistUpdate::Flag flag) const {\n\treturn _sublistChanges.realtimeUpdates(flag);\n}\n\nvoid Changes::sublistRemoved(not_null<SavedSublist*> sublist) {\n\t_sublistChanges.drop(sublist);\n}\n\nvoid Changes::messageUpdated(\n\t\tnot_null<HistoryItem*> item,\n\t\tMessageUpdate::Flags flags) {\n\tconst auto drop = (flags & MessageUpdate::Flag::Destroyed);\n\t_messageChanges.updated(item, flags, drop);\n\tif (!drop) {\n\t\tscheduleNotifications();\n\t}\n}\n\nrpl::producer<MessageUpdate> Changes::messageUpdates(\n\t\tMessageUpdate::Flags flags) const {\n\treturn _messageChanges.updates(flags);\n}\n\nrpl::producer<MessageUpdate> Changes::messageUpdates(\n\t\tnot_null<HistoryItem*> item,\n\t\tMessageUpdate::Flags flags) const {\n\treturn _messageChanges.updates(item, flags);\n}\n\nrpl::producer<MessageUpdate> Changes::messageFlagsValue(\n\t\tnot_null<HistoryItem*> item,\n\t\tMessageUpdate::Flags flags) const {\n\treturn _messageChanges.flagsValue(item, flags);\n}\n\nrpl::producer<MessageUpdate> Changes::realtimeMessageUpdates(\n\t\tMessageUpdate::Flag flag) const {\n\treturn _messageChanges.realtimeUpdates(flag);\n}\n\nvoid Changes::entryUpdated(\n\t\tnot_null<Dialogs::Entry*> entry,\n\t\tEntryUpdate::Flags flags) {\n\tconst auto drop = (flags & EntryUpdate::Flag::Destroyed);\n\t_entryChanges.updated(entry, flags, drop);\n\tif (!drop) {\n\t\tscheduleNotifications();\n\t}\n}\n\nrpl::producer<EntryUpdate> Changes::entryUpdates(\n\t\tEntryUpdate::Flags flags) const {\n\treturn _entryChanges.updates(flags);\n}\n\nrpl::producer<EntryUpdate> Changes::entryUpdates(\n\t\tnot_null<Dialogs::Entry*> entry,\n\t\tEntryUpdate::Flags flags) const {\n\treturn _entryChanges.updates(entry, flags);\n}\n\nrpl::producer<EntryUpdate> Changes::entryFlagsValue(\n\t\tnot_null<Dialogs::Entry*> entry,\n\t\tEntryUpdate::Flags flags) const {\n\treturn _entryChanges.flagsValue(entry, flags);\n}\n\nrpl::producer<EntryUpdate> Changes::realtimeEntryUpdates(\n\t\tEntryUpdate::Flag flag) const {\n\treturn _entryChanges.realtimeUpdates(flag);\n}\n\nvoid Changes::entryRemoved(not_null<Dialogs::Entry*> entry) {\n\t_entryChanges.drop(entry);\n}\n\nvoid Changes::storyUpdated(\n\t\tnot_null<Story*> story,\n\t\tStoryUpdate::Flags flags) {\n\tconst auto drop = (flags & StoryUpdate::Flag::Destroyed);\n\t_storyChanges.updated(story, flags, drop);\n\tif (!drop) {\n\t\tscheduleNotifications();\n\t}\n}\n\nrpl::producer<StoryUpdate> Changes::storyUpdates(\n\t\tStoryUpdate::Flags flags) const {\n\treturn _storyChanges.updates(flags);\n}\n\nrpl::producer<StoryUpdate> Changes::storyUpdates(\n\t\tnot_null<Story*> story,\n\t\tStoryUpdate::Flags flags) const {\n\treturn _storyChanges.updates(story, flags);\n}\n\nrpl::producer<StoryUpdate> Changes::storyFlagsValue(\n\t\tnot_null<Story*> story,\n\t\tStoryUpdate::Flags flags) const {\n\treturn _storyChanges.flagsValue(story, flags);\n}\n\nrpl::producer<StoryUpdate> Changes::realtimeStoryUpdates(\n\t\tStoryUpdate::Flag flag) const {\n\treturn _storyChanges.realtimeUpdates(flag);\n}\n\nvoid Changes::chatAdminChanged(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user,\n\t\tChatAdminRights rights,\n\t\tQString rank) {\n\t_chatAdminChanges.fire({\n\t\t.peer = peer,\n\t\t.user = user,\n\t\t.rights = rights,\n\t\t.rank = std::move(rank),\n\t});\n}\n\nrpl::producer<ChatAdminChange> Changes::chatAdminChanges() const {\n\treturn _chatAdminChanges.events();\n}\n\nvoid Changes::chatMemberRankChanged(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user,\n\t\tQString rank) {\n\t_chatMemberRankChanges.fire({ peer, user, std::move(rank) });\n}\n\nrpl::producer<ChatMemberRankChange> Changes::chatMemberRankChanges() const {\n\treturn _chatMemberRankChanges.events();\n}\n\nvoid Changes::scheduleNotifications() {\n\tif (!_notify) {\n\t\t_notify = true;\n\t\tcrl::on_main(&session(), [=] {\n\t\t\tsendNotifications();\n\t\t});\n\t}\n}\n\nvoid Changes::sendNotifications() {\n\tif (!_notify) {\n\t\treturn;\n\t}\n\t_notify = false;\n\t_peerChanges.sendNotifications();\n\t_historyChanges.sendNotifications();\n\t_messageChanges.sendNotifications();\n\t_entryChanges.sendNotifications();\n\t_topicChanges.sendNotifications();\n\t_sublistChanges.sendNotifications();\n\t_storyChanges.sendNotifications();\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_changes.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n#include \"data/data_chat_participant_status.h\"\n\nclass History;\nclass PeerData;\nclass HistoryItem;\n\nnamespace Dialogs {\nclass Entry;\n} // namespace Dialogs\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data::details {\n\ntemplate <typename Flag>\ninline constexpr int CountBit(Flag Last = Flag::LastUsedBit) {\n\tauto i = 0;\n\twhile ((1ULL << i) != static_cast<uint64>(Last)) {\n\t\t++i;\n\t\tAssert(i != 64);\n\t}\n\treturn i;\n}\n\n} // namespace Data::details\n\nnamespace Data {\n\nclass ForumTopic;\nclass SavedSublist;\nclass Story;\n\nstruct NameUpdate {\n\tNameUpdate(\n\t\tnot_null<PeerData*> peer,\n\t\tbase::flat_set<QChar> oldFirstLetters)\n\t: peer(peer)\n\t, oldFirstLetters(std::move(oldFirstLetters)) {\n\t}\n\n\tnot_null<PeerData*> peer;\n\tbase::flat_set<QChar> oldFirstLetters;\n};\n\nstruct PeerUpdate {\n\tenum class Flag : uint64 {\n\t\tNone = 0,\n\n\t\t// Common flags\n\t\tName                = (1ULL << 0),\n\t\tUsername            = (1ULL << 1),\n\t\tPhoto               = (1ULL << 2),\n\t\tAbout               = (1ULL << 3),\n\t\tNotifications       = (1ULL << 4),\n\t\tMigration           = (1ULL << 5),\n\t\tUnavailableReason   = (1ULL << 6),\n\t\tChatThemeToken      = (1ULL << 7),\n\t\tChatWallPaper       = (1ULL << 8),\n\t\tIsBlocked           = (1ULL << 9),\n\t\tMessagesTTL         = (1ULL << 10),\n\t\tFullInfo            = (1ULL << 11),\n\t\tUsernames           = (1ULL << 12),\n\t\tTranslationDisabled = (1ULL << 13),\n\t\tColor               = (1ULL << 14),\n\t\tColorProfile        = (1ULL << 15),\n\t\tBackgroundEmoji     = (1ULL << 16),\n\t\tStoriesState        = (1ULL << 17),\n\t\tVerifyInfo          = (1ULL << 18),\n\t\tStarsPerMessage     = (1ULL << 19),\n\n\t\t// For users\n\t\tCanShareContact     = (1ULL << 20),\n\t\tIsContact           = (1ULL << 21),\n\t\tPhoneNumber         = (1ULL << 22),\n\t\tOnlineStatus        = (1ULL << 23),\n\t\tBotCommands         = (1ULL << 24),\n\t\tBotCanBeInvited     = (1ULL << 25),\n\t\tBotStartToken       = (1ULL << 26),\n\t\tCommonChats         = (1ULL << 27),\n\t\tPeerGifts           = (1ULL << 28),\n\t\tHasCalls            = (1ULL << 29),\n\t\tSupportInfo         = (1ULL << 30),\n\t\tIsBot               = (1ULL << 31),\n\t\tEmojiStatus         = (1ULL << 32),\n\t\tBusinessDetails     = (1ULL << 33),\n\t\tBirthday            = (1ULL << 34),\n\t\tPersonalChannel     = (1ULL << 35),\n\t\tStarRefProgram      = (1ULL << 36),\n\t\tPaysPerMessage      = (1ULL << 37),\n\t\tGiftSettings        = (1ULL << 38),\n\t\tStarsRating         = (1ULL << 39),\n\t\tContactNote         = (1ULL << 40),\n\n\t\t// For chats and channels\n\t\tInviteLinks         = (1ULL << 41),\n\t\tMembers             = (1ULL << 42),\n\t\tAdmins              = (1ULL << 43),\n\t\tBannedUsers         = (1ULL << 44),\n\t\tRights              = (1ULL << 45),\n\t\tPendingRequests     = (1ULL << 46),\n\t\tReactions           = (1ULL << 47),\n\n\t\t// For channels\n\t\tChannelAmIn         = (1ULL << 48),\n\t\tStickersSet         = (1ULL << 49),\n\t\tEmojiSet            = (1ULL << 50),\n\t\tDiscussionLink      = (1ULL << 51),\n\t\tMonoforumLink       = (1ULL << 52),\n\t\tChannelLocation     = (1ULL << 53),\n\t\tSlowmode            = (1ULL << 54),\n\t\tGroupCall           = (1ULL << 55),\n\t\tManagedBot          = (1ULL << 56),\n\n\t\t// For iteration\n\t\tLastUsedBit         = (1ULL << 56),\n\t};\n\tusing Flags = base::flags<Flag>;\n\tfriend inline constexpr auto is_flag_type(Flag) { return true; }\n\n\tnot_null<PeerData*> peer;\n\tFlags flags = 0;\n\n};\n\nstruct HistoryUpdate {\n\tenum class Flag : uint32 {\n\t\tNone = 0,\n\n\t\tIsPinned           = (1U << 0),\n\t\tUnreadView         = (1U << 1),\n\t\tTopPromoted        = (1U << 2),\n\t\tFolder             = (1U << 3),\n\t\tUnreadMentions     = (1U << 4),\n\t\tUnreadReactions    = (1U << 5),\n\t\tClientSideMessages = (1U << 6),\n\t\tChatOccupied       = (1U << 7),\n\t\tMessageSent        = (1U << 8),\n\t\tScheduledSent      = (1U << 9),\n\t\tOutboxRead         = (1U << 10),\n\t\tBotKeyboard        = (1U << 11),\n\t\tCloudDraft         = (1U << 12),\n\t\tTranslateFrom      = (1U << 13),\n\t\tTranslatedTo       = (1U << 14),\n\t\tUnreadPollVotes    = (1U << 15),\n\n\t\tLastUsedBit        = (1U << 15),\n\t};\n\tusing Flags = base::flags<Flag>;\n\tfriend inline constexpr auto is_flag_type(Flag) { return true; }\n\n\tnot_null<History*> history;\n\tFlags flags = 0;\n\n};\n\nstruct TopicUpdate {\n\tenum class Flag : uint32 {\n\t\tNone = 0,\n\n\t\tUnreadView      = (1U << 1),\n\t\tUnreadMentions  = (1U << 2),\n\t\tUnreadReactions = (1U << 3),\n\t\tNotifications   = (1U << 4),\n\t\tTitle           = (1U << 5),\n\t\tIconId          = (1U << 6),\n\t\tColorId         = (1U << 7),\n\t\tCloudDraft      = (1U << 8),\n\t\tClosed          = (1U << 9),\n\t\tCreator         = (1U << 10),\n\t\tDestroyed       = (1U << 11),\n\t\tUnreadPollVotes = (1U << 12),\n\n\t\tLastUsedBit     = (1U << 12),\n\t};\n\tusing Flags = base::flags<Flag>;\n\tfriend inline constexpr auto is_flag_type(Flag) { return true; }\n\n\tnot_null<ForumTopic*> topic;\n\tFlags flags = 0;\n\n};\n\nstruct SublistUpdate {\n\tenum class Flag : uint32 {\n\t\tNone = 0,\n\n\t\tUnreadView = (1U << 1),\n\t\tUnreadReactions = (1U << 2),\n\t\tCloudDraft = (1U << 3),\n\t\tDestroyed = (1U << 4),\n\t\tUnreadPollVotes = (1U << 5),\n\n\t\tLastUsedBit = (1U << 5),\n\t};\n\tusing Flags = base::flags<Flag>;\n\tfriend inline constexpr auto is_flag_type(Flag) { return true; }\n\n\tnot_null<SavedSublist*> sublist;\n\tFlags flags = 0;\n\n};\n\nstruct MessageUpdate {\n\tenum class Flag : uint32 {\n\t\tNone = 0,\n\n\t\tEdited             = (1U << 0),\n\t\tDestroyed          = (1U << 1),\n\t\tDialogRowRepaint   = (1U << 2),\n\t\tDialogRowRefresh   = (1U << 3),\n\t\tNewAdded           = (1U << 4),\n\t\tReplyMarkup        = (1U << 5),\n\t\tBotCallbackSent    = (1U << 6),\n\t\tNewMaybeAdded      = (1U << 7),\n\t\tReplyToTopAdded    = (1U << 8),\n\t\tNewUnreadReaction  = (1U << 9),\n\n\t\tLastUsedBit        = (1U << 9),\n\t};\n\tusing Flags = base::flags<Flag>;\n\tfriend inline constexpr auto is_flag_type(Flag) { return true; }\n\n\tnot_null<HistoryItem*> item;\n\tFlags flags = 0;\n\n};\n\nstruct EntryUpdate {\n\tenum class Flag : uint32 {\n\t\tNone = 0,\n\n\t\tRepaint           = (1U << 0),\n\t\tHasPinnedMessages = (1U << 1),\n\t\tForwardDraft      = (1U << 2),\n\t\tLocalDraftSet     = (1U << 3),\n\t\tHeight            = (1U << 4),\n\t\tDestroyed         = (1U << 5),\n\n\t\tLastUsedBit       = (1U << 5),\n\t};\n\tusing Flags = base::flags<Flag>;\n\tfriend inline constexpr auto is_flag_type(Flag) { return true; }\n\n\tnot_null<Dialogs::Entry*> entry;\n\tFlags flags = 0;\n\n};\n\nstruct StoryUpdate {\n\tenum class Flag : uint32 {\n\t\tNone = 0,\n\n\t\tEdited       = (1U << 0),\n\t\tDestroyed    = (1U << 1),\n\t\tNewAdded     = (1U << 2),\n\t\tViewsChanged = (1U << 3),\n\t\tMarkRead     = (1U << 4),\n\t\tReaction     = (1U << 5),\n\n\t\tLastUsedBit  = (1U << 5),\n\t};\n\tusing Flags = base::flags<Flag>;\n\tfriend inline constexpr auto is_flag_type(Flag) { return true; }\n\n\tnot_null<Story*> story;\n\tFlags flags = 0;\n\n};\n\nstruct ChatAdminChange {\n\tnot_null<PeerData*> peer;\n\tnot_null<UserData*> user;\n\tChatAdminRights rights;\n\tQString rank;\n};\n\nstruct ChatMemberRankChange {\n\tnot_null<PeerData*> peer;\n\tnot_null<UserData*> user;\n\tQString rank;\n};\n\nclass Changes final {\npublic:\n\texplicit Changes(not_null<Main::Session*> session);\n\n\t[[nodiscard]] Main::Session &session() const;\n\n\tvoid nameUpdated(\n\t\tnot_null<PeerData*> peer,\n\t\tbase::flat_set<QChar> oldFirstLetters);\n\t[[nodiscard]] rpl::producer<NameUpdate> realtimeNameUpdates() const;\n\t[[nodiscard]] rpl::producer<NameUpdate> realtimeNameUpdates(\n\t\tnot_null<PeerData*> peer) const;\n\n\tvoid peerUpdated(not_null<PeerData*> peer, PeerUpdate::Flags flags);\n\t[[nodiscard]] rpl::producer<PeerUpdate> peerUpdates(\n\t\tPeerUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<PeerUpdate> peerUpdates(\n\t\tnot_null<PeerData*> peer,\n\t\tPeerUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<PeerUpdate> peerFlagsValue(\n\t\tnot_null<PeerData*> peer,\n\t\tPeerUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<PeerUpdate> realtimePeerUpdates(\n\t\tPeerUpdate::Flag flag) const;\n\n\tvoid historyUpdated(\n\t\tnot_null<History*> history,\n\t\tHistoryUpdate::Flags flags);\n\t[[nodiscard]] rpl::producer<HistoryUpdate> historyUpdates(\n\t\tHistoryUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<HistoryUpdate> historyUpdates(\n\t\tnot_null<History*> history,\n\t\tHistoryUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<HistoryUpdate> historyFlagsValue(\n\t\tnot_null<History*> history,\n\t\tHistoryUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<HistoryUpdate> realtimeHistoryUpdates(\n\t\tHistoryUpdate::Flag flag) const;\n\n\tvoid topicUpdated(\n\t\tnot_null<ForumTopic*> topic,\n\t\tTopicUpdate::Flags flags);\n\t[[nodiscard]] rpl::producer<TopicUpdate> topicUpdates(\n\t\tTopicUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<TopicUpdate> topicUpdates(\n\t\tnot_null<ForumTopic*> topic,\n\t\tTopicUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<TopicUpdate> topicFlagsValue(\n\t\tnot_null<ForumTopic*> topic,\n\t\tTopicUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<TopicUpdate> realtimeTopicUpdates(\n\t\tTopicUpdate::Flag flag) const;\n\tvoid topicRemoved(not_null<ForumTopic*> topic);\n\n\tvoid sublistUpdated(\n\t\tnot_null<SavedSublist*> sublist,\n\t\tSublistUpdate::Flags flags);\n\t[[nodiscard]] rpl::producer<SublistUpdate> sublistUpdates(\n\t\tSublistUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<SublistUpdate> sublistUpdates(\n\t\tnot_null<SavedSublist*> sublist,\n\t\tSublistUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<SublistUpdate> sublistFlagsValue(\n\t\tnot_null<SavedSublist*> sublist,\n\t\tSublistUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<SublistUpdate> realtimeSublistUpdates(\n\t\tSublistUpdate::Flag flag) const;\n\tvoid sublistRemoved(not_null<SavedSublist*> sublist);\n\n\tvoid messageUpdated(\n\t\tnot_null<HistoryItem*> item,\n\t\tMessageUpdate::Flags flags);\n\t[[nodiscard]] rpl::producer<MessageUpdate> messageUpdates(\n\t\tMessageUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<MessageUpdate> messageUpdates(\n\t\tnot_null<HistoryItem*> item,\n\t\tMessageUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<MessageUpdate> messageFlagsValue(\n\t\tnot_null<HistoryItem*> item,\n\t\tMessageUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<MessageUpdate> realtimeMessageUpdates(\n\t\tMessageUpdate::Flag flag) const;\n\n\tvoid entryUpdated(\n\t\tnot_null<Dialogs::Entry*> entry,\n\t\tEntryUpdate::Flags flags);\n\t[[nodiscard]] rpl::producer<EntryUpdate> entryUpdates(\n\t\tEntryUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<EntryUpdate> entryUpdates(\n\t\tnot_null<Dialogs::Entry*> entry,\n\t\tEntryUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<EntryUpdate> entryFlagsValue(\n\t\tnot_null<Dialogs::Entry*> entry,\n\t\tEntryUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<EntryUpdate> realtimeEntryUpdates(\n\t\tEntryUpdate::Flag flag) const;\n\tvoid entryRemoved(not_null<Dialogs::Entry*> entry);\n\n\tvoid storyUpdated(\n\t\tnot_null<Story*> story,\n\t\tStoryUpdate::Flags flags);\n\t[[nodiscard]] rpl::producer<StoryUpdate> storyUpdates(\n\t\tStoryUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<StoryUpdate> storyUpdates(\n\t\tnot_null<Story*> story,\n\t\tStoryUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<StoryUpdate> storyFlagsValue(\n\t\tnot_null<Story*> story,\n\t\tStoryUpdate::Flags flags) const;\n\t[[nodiscard]] rpl::producer<StoryUpdate> realtimeStoryUpdates(\n\t\tStoryUpdate::Flag flag) const;\n\n\tvoid chatAdminChanged(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user,\n\t\tChatAdminRights rights,\n\t\tQString rank);\n\t[[nodiscard]] rpl::producer<ChatAdminChange> chatAdminChanges() const;\n\n\tvoid chatMemberRankChanged(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user,\n\t\tQString rank);\n\t[[nodiscard]] rpl::producer<ChatMemberRankChange> chatMemberRankChanges() const;\n\n\tvoid sendNotifications();\n\nprivate:\n\ttemplate <typename DataType, typename UpdateType>\n\tclass Manager final {\n\tpublic:\n\t\tusing Flag = typename UpdateType::Flag;\n\t\tusing Flags = typename UpdateType::Flags;\n\n\t\tvoid updated(\n\t\t\tnot_null<DataType*> data,\n\t\t\tFlags flags,\n\t\t\tbool dropScheduled = false);\n\t\t[[nodiscard]] rpl::producer<UpdateType> updates(Flags flags) const;\n\t\t[[nodiscard]] rpl::producer<UpdateType> updates(\n\t\t\tnot_null<DataType*> data,\n\t\t\tFlags flags) const;\n\t\t[[nodiscard]] rpl::producer<UpdateType> flagsValue(\n\t\t\tnot_null<DataType*> data,\n\t\t\tFlags flags) const;\n\t\t[[nodiscard]] rpl::producer<UpdateType> realtimeUpdates(\n\t\t\tFlag flag) const;\n\n\t\tvoid drop(not_null<DataType*> data);\n\n\t\tvoid sendNotifications();\n\n\tprivate:\n\t\tstatic constexpr auto kCount = details::CountBit<Flag>() + 1;\n\n\t\tvoid sendRealtimeNotifications(\n\t\t\tnot_null<DataType*> data,\n\t\t\tFlags flags);\n\n\t\tstd::array<rpl::event_stream<UpdateType>, kCount> _realtimeStreams;\n\t\tbase::flat_map<not_null<DataType*>, Flags> _updates;\n\t\trpl::event_stream<UpdateType> _stream;\n\n\t};\n\n\tvoid scheduleNotifications();\n\n\tconst not_null<Main::Session*> _session;\n\n\trpl::event_stream<NameUpdate> _nameStream;\n\tManager<PeerData, PeerUpdate> _peerChanges;\n\tManager<History, HistoryUpdate> _historyChanges;\n\tManager<ForumTopic, TopicUpdate> _topicChanges;\n\tManager<SavedSublist, SublistUpdate> _sublistChanges;\n\tManager<HistoryItem, MessageUpdate> _messageChanges;\n\tManager<Dialogs::Entry, EntryUpdate> _entryChanges;\n\tManager<Story, StoryUpdate> _storyChanges;\n\trpl::event_stream<ChatAdminChange> _chatAdminChanges;\n\trpl::event_stream<ChatMemberRankChange> _chatMemberRankChanges;\n\n\tbool _notify = false;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_channel.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_channel.h\"\n\n#include \"api/api_credits.h\"\n#include \"api/api_global_privacy.h\"\n#include \"api/api_statistics.h\"\n#include \"base/timer_rpl.h\"\n#include \"data/components/credits.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel_admins.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_icons.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_wall_paper.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"main/main_session.h\"\n#include \"main/session/send_as_peers.h\"\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"api/api_chat_invite.h\"\n#include \"api/api_invite_links.h\"\n#include \"apiwrap.h\"\n#include \"storage/storage_account.h\"\n#include \"ui/unread_badge.h\"\n#include \"window/notifications_manager.h\"\n\nnamespace {\n\nusing UpdateFlag = Data::PeerUpdate::Flag;\n\n} // namespace\n\nMegagroupInfo::MegagroupInfo() = default;\n\nMegagroupInfo::~MegagroupInfo() = default;\n\nChatData *MegagroupInfo::getMigrateFromChat() const {\n\treturn _migratedFrom;\n}\n\nvoid MegagroupInfo::setMigrateFromChat(ChatData *chat) {\n\t_migratedFrom = chat;\n}\n\nconst ChannelLocation *MegagroupInfo::getLocation() const {\n\treturn _location.address.isEmpty() ? nullptr : &_location;\n}\n\nvoid MegagroupInfo::setLocation(const ChannelLocation &location) {\n\t_location = location;\n}\n\nData::ChatBotCommands::Changed MegagroupInfo::setBotCommands(\n\t\tconst std::vector<Data::BotCommands> &list) {\n\treturn _botCommands.update(list);\n}\n\nvoid MegagroupInfo::ensureForum(not_null<ChannelData*> that) {\n\tif (!_forum) {\n\t\tconst auto history = that->owner().history(that);\n\t\t_forum = std::make_unique<Data::Forum>(history);\n\t\thistory->forumChanged(nullptr);\n\t}\n}\n\nData::Forum *MegagroupInfo::forum() const {\n\treturn _forum.get();\n}\n\nstd::unique_ptr<Data::Forum> MegagroupInfo::takeForumData() {\n\tif (auto result = base::take(_forum)) {\n\t\tresult->history()->forumChanged(result.get());\n\t\treturn result;\n\t}\n\treturn nullptr;\n}\n\nvoid MegagroupInfo::ensureMonoforum(not_null<ChannelData*> that) {\n\tif (!_monoforum) {\n\t\tconst auto history = that->owner().history(that);\n\t\t_monoforum = std::make_unique<Data::SavedMessages>(\n\t\t\t&that->owner(),\n\t\t\tthat);\n\t\thistory->monoforumChanged(nullptr);\n\t}\n}\n\nData::SavedMessages *MegagroupInfo::monoforum() const {\n\treturn _monoforum.get();\n}\n\nstd::unique_ptr<Data::SavedMessages> MegagroupInfo::takeMonoforumData() {\n\tif (auto result = base::take(_monoforum)) {\n\t\tconst auto history = result->owner().history(result->parentChat());\n\t\thistory->monoforumChanged(result.get());\n\t\treturn result;\n\t}\n\treturn nullptr;\n}\n\nChannelData::ChannelData(not_null<Data::Session*> owner, PeerId id)\n: PeerData(owner, id)\n, _ptsWaiter(&owner->session().updates()) {\n}\n\nvoid ChannelData::setPhoto(const MTPChatPhoto &photo) {\n\tphoto.match([&](const MTPDchatPhoto & data) {\n\t\tupdateUserpic(\n\t\t\tdata.vphoto_id().v,\n\t\t\tdata.vdc_id().v,\n\t\t\tdata.is_has_video());\n\t}, [&](const MTPDchatPhotoEmpty &) {\n\t\tclearUserpic();\n\t});\n}\n\nvoid ChannelData::setName(\n\t\tconst QString &newName,\n\t\tconst QString &newUsername) {\n\tupdateNameDelayed(newName.isEmpty() ? name() : newName, {}, newUsername);\n}\n\nvoid ChannelData::setUsername(const QString &username) {\n\t_username.setUsername(username);\n}\n\nvoid ChannelData::setUsernames(const Data::Usernames &newUsernames) {\n\tconst auto wasUsername = username();\n\tconst auto wasUsernames = usernames();\n\t_username.setUsernames(newUsernames);\n\tconst auto nowUsername = username();\n\tconst auto nowUsernames = usernames();\n\tsession().changes().peerUpdated(\n\t\tthis,\n\t\tUpdateFlag()\n\t\t| ((wasUsername != nowUsername)\n\t\t\t? UpdateFlag::Username\n\t\t\t: UpdateFlag())\n\t\t| (!ranges::equal(wasUsernames, nowUsernames)\n\t\t\t? UpdateFlag::Usernames\n\t\t\t: UpdateFlag()));\n}\n\nQString ChannelData::username() const {\n\treturn _username.username();\n}\n\nQString ChannelData::editableUsername() const {\n\treturn _username.editableUsername();\n}\n\nconst std::vector<QString> &ChannelData::usernames() const {\n\treturn _username.usernames();\n}\n\nbool ChannelData::isUsernameEditable(QString username) const {\n\treturn _username.isEditable(username);\n}\n\nvoid ChannelData::setAccessHash(uint64 accessHash) {\n\t_accessHash = accessHash;\n}\n\nvoid ChannelData::setFlags(ChannelDataFlags which) {\n\tif (which & Flag::MonoforumAdmin) {\n\t\twhich |= Flag::Monoforum;\n\t}\n\tif (which & (Flag::Forum | Flag::Monoforum)) {\n\t\twhich |= Flag::Megagroup;\n\t}\n\tif (which & Flag::Monoforum) {\n\t\twhich &= ~Flag::Forum;\n\t}\n\tconst auto diff = flags() ^ which;\n\tif ((which & Flag::Megagroup) && !mgInfo) {\n\t\tmgInfo = std::make_unique<MegagroupInfo>();\n\t}\n\n\t// Let Data::Forum live till the end of _flags.set.\n\t// That way the data can be used in changes handler.\n\t// Example: render frame for forum auto-closing animation.\n\tconst auto takenForum = ((diff & Flag::Forum) && !(which & Flag::Forum))\n\t\t? mgInfo->takeForumData()\n\t\t: nullptr;\n\tconst auto takenMonoforum = ((diff & Flag::MonoforumAdmin)\n\t\t&& !(which & Flag::MonoforumAdmin))\n\t\t? mgInfo->takeMonoforumData()\n\t\t: nullptr;\n\tconst auto wasIn = amIn();\n\tif ((diff & Flag::MonoforumAdmin) && (which & Flag::MonoforumAdmin)) {\n\t\tmgInfo->ensureMonoforum(this);\n\t} else if ((diff & Flag::Forum) && (which & Flag::Forum)) {\n\t\tmgInfo->ensureForum(this);\n\t}\n\t_flags.set(which);\n\tif (diff & (Flag::Left | Flag::Forbidden)) {\n\t\tif (const auto chat = getMigrateFromChat()) {\n\t\t\tsession().changes().peerUpdated(chat, UpdateFlag::Migration);\n\t\t\tsession().changes().peerUpdated(this, UpdateFlag::Migration);\n\t\t}\n\n\t\tif (wasIn && !amIn()) {\n\t\t\tcrl::on_main(&session(), [=] {\n\t\t\t\tif (!amIn()) {\n\t\t\t\t\tCore::App().closeChatFromWindows(this);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\tif (diff & (Flag::Forum\n\t\t| Flag::MonoforumAdmin\n\t\t| Flag::CallNotEmpty\n\t\t| Flag::SimilarExpanded\n\t\t| Flag::Signatures\n\t\t| Flag::SignatureProfiles\n\t\t| Flag::ForumTabs)) {\n\t\tif (const auto history = this->owner().historyLoaded(this)) {\n\t\t\tif (diff & Flag::CallNotEmpty) {\n\t\t\t\thistory->updateChatListEntry();\n\t\t\t}\n\t\t\tif (diff & (Flag::Forum | Flag::MonoforumAdmin)) {\n\t\t\t\tCore::App().notifications().clearFromHistory(history);\n\t\t\t\thistory->updateChatListEntryHeight();\n\t\t\t\tif (history->inChatList()) {\n\t\t\t\t\tif (const auto forum = this->forum()) {\n\t\t\t\t\t\tforum->preloadTopics();\n\t\t\t\t\t} else if (const auto monoforum = this->monoforum()) {\n\t\t\t\t\t\tmonoforum->preloadSublists();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (diff & Flag::SimilarExpanded) {\n\t\t\t\tif (const auto item = history->joinedMessageInstance()) {\n\t\t\t\t\thistory->owner().requestItemResize(item);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (diff & Flag::SignatureProfiles) {\n\t\t\t\thistory->forceFullResize();\n\t\t\t}\n\t\t\tif (diff & (Flag::Signatures | Flag::SignatureProfiles)) {\n\t\t\t\tsession().changes().peerUpdated(this, UpdateFlag::Rights);\n\t\t\t}\n\t\t\tif (diff & Flag::ForumTabs) {\n\t\t\t\thistory->forumTabsChanged(which & Flag::ForumTabs);\n\t\t\t}\n\t\t}\n\t}\n\tif (const auto raw = takenForum.get()) {\n\t\towner().forumIcons().clearUserpicsReset(raw);\n\t}\n}\n\nvoid ChannelData::addFlags(ChannelDataFlags which) {\n\tsetFlags(flags() | which);\n}\n\nvoid ChannelData::removeFlags(ChannelDataFlags which) {\n\tsetFlags(flags() & ~which);\n}\n\nvoid ChannelData::setInviteLink(const QString &newInviteLink) {\n\t_inviteLink = newInviteLink;\n}\n\nbool ChannelData::canHaveInviteLink() const {\n\treturn amCreator()\n\t\t|| (adminRights() & AdminRight::InviteByLinkOrAdd);\n}\n\nvoid ChannelData::setLocation(const MTPChannelLocation &data) {\n\tif (!mgInfo) {\n\t\treturn;\n\t}\n\tconst auto was = mgInfo->getLocation();\n\tconst auto wasValue = was ? *was : ChannelLocation();\n\tdata.match([&](const MTPDchannelLocation &data) {\n\t\tdata.vgeo_point().match([&](const MTPDgeoPoint &point) {\n\t\t\tmgInfo->setLocation({\n\t\t\t\tqs(data.vaddress()),\n\t\t\t\tData::LocationPoint(point)\n\t\t\t});\n\t\t}, [&](const MTPDgeoPointEmpty &) {\n\t\t\tmgInfo->setLocation(ChannelLocation());\n\t\t});\n\t}, [&](const MTPDchannelLocationEmpty &) {\n\t\tmgInfo->setLocation(ChannelLocation());\n\t});\n\tconst auto now = mgInfo->getLocation();\n\tconst auto nowValue = now ? *now : ChannelLocation();\n\tif (was != now || (was && wasValue != nowValue)) {\n\t\tsession().changes().peerUpdated(\n\t\t\tthis,\n\t\t\tUpdateFlag::ChannelLocation);\n\t}\n}\n\nconst ChannelLocation *ChannelData::getLocation() const {\n\treturn mgInfo ? mgInfo->getLocation() : nullptr;\n}\n\nvoid ChannelData::setDiscussionLink(ChannelData *linked) {\n\tif (_discussionLink != linked || !_discussionLinkKnown) {\n\t\t_discussionLink = linked;\n\t\t_discussionLinkKnown = true;\n\t\tif (const auto history = owner().historyLoaded(this)) {\n\t\t\thistory->forceFullResize();\n\t\t}\n\t\tsession().changes().peerUpdated(this, UpdateFlag::DiscussionLink);\n\t}\n}\n\nChannelData *ChannelData::discussionLink() const {\n\treturn _discussionLink;\n}\n\nbool ChannelData::discussionLinkKnown() const {\n\treturn _discussionLinkKnown;\n}\n\nvoid ChannelData::setMonoforumLink(ChannelData *link) {\n\tif (_monoforumLink) {\n\t\tif (isBroadcast()) {\n\t\t\t_monoforumLink->setMonoforumLink(link ? this : nullptr);\n\t\t} else if (isMonoforum()) {\n\t\t\tif (!link && !monoforumDisabled()) {\n\t\t\t\tsetFlags(flags() | Flag::MonoforumDisabled);\n\t\t\t} else if (link && monoforumDisabled()) {\n\t\t\t\tsetFlags(flags() & ~Flag::MonoforumDisabled);\n\t\t\t}\n\t\t}\n\t\treturn;\n\t} else if (!link) {\n\t\treturn;\n\t}\n\t_monoforumLink = link;\n\tlink->setMonoforumLink(this);\n\tsession().changes().peerUpdated(this, UpdateFlag::MonoforumLink);\n\tif (isMegagroup() && link->canAccessMonoforum()) {\n\t\tsetFlags(flags() | Flag::MonoforumAdmin);\n\t}\n}\n\nChannelData *ChannelData::monoforumLink() const {\n\treturn _monoforumLink;\n}\n\nbool ChannelData::monoforumDisabled() const {\n\treturn flags() & Flag::MonoforumDisabled;\n}\n\nvoid ChannelData::setMembersCount(int newMembersCount) {\n\tif (_membersCount != newMembersCount) {\n\t\tif (isMegagroup()\n\t\t\t&& canViewMembers()\n\t\t\t&& !mgInfo->lastParticipants.empty()) {\n\t\t\tmgInfo->lastParticipantsStatus\n\t\t\t\t|= MegagroupInfo::LastParticipantsCountOutdated;\n\t\t\tmgInfo->lastParticipantsCount = membersCount();\n\t\t}\n\t\t_membersCount = newMembersCount;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::Members);\n\t}\n}\n\nvoid ChannelData::setAdminsCount(int newAdminsCount) {\n\tif (_adminsCount != newAdminsCount) {\n\t\t_adminsCount = newAdminsCount;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::Admins);\n\t}\n}\n\nvoid ChannelData::setRestrictedCount(int newRestrictedCount) {\n\tif (_restrictedCount != newRestrictedCount) {\n\t\t_restrictedCount = newRestrictedCount;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::BannedUsers);\n\t}\n}\n\nvoid ChannelData::setKickedCount(int newKickedCount) {\n\tif (_kickedCount != newKickedCount) {\n\t\t_kickedCount = newKickedCount;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::BannedUsers);\n\t}\n}\n\nvoid ChannelData::setPendingRequestsCount(\n\t\tint count,\n\t\tconst QVector<MTPlong> &recentRequesters) {\n\tsetPendingRequestsCount(count, ranges::views::all(\n\t\trecentRequesters\n\t) | ranges::views::transform([&](const MTPlong &value) {\n\t\treturn UserId(value);\n\t}) | ranges::to_vector);\n}\n\nvoid ChannelData::setPendingRequestsCount(\n\t\tint count,\n\t\tstd::vector<UserId> recentRequesters) {\n\tif (_pendingRequestsCount != count\n\t\t|| _recentRequesters != recentRequesters) {\n\t\t_pendingRequestsCount = count;\n\t\t_recentRequesters = std::move(recentRequesters);\n\t\tsession().changes().peerUpdated(this, UpdateFlag::PendingRequests);\n\t}\n}\n\nbool ChannelData::useSubsectionTabs() const {\n\treturn amMonoforumAdmin()\n\t\t|| (isForum() && (flags() & ChannelDataFlag::ForumTabs));\n}\n\nChatRestrictionsInfo ChannelData::KickedRestrictedRights(\n\t\tnot_null<PeerData*> participant) {\n\tusing Flag = ChatRestriction;\n\tconst auto flags = Flag::ViewMessages\n\t\t| Flag::SendStickers\n\t\t| Flag::SendGifs\n\t\t| Flag::SendGames\n\t\t| Flag::SendInline\n\t\t| Flag::SendPhotos\n\t\t| Flag::SendVideos\n\t\t| Flag::SendVideoMessages\n\t\t| Flag::SendMusic\n\t\t| Flag::SendVoiceMessages\n\t\t| Flag::SendFiles\n\t\t| Flag::SendOther\n\t\t| Flag::EmbedLinks;\n\treturn ChatRestrictionsInfo(\n\t\t(participant->isUser() ? flags : Flag::ViewMessages),\n\t\tstd::numeric_limits<int32>::max());\n}\n\nvoid ChannelData::applyEditAdmin(\n\t\tnot_null<UserData*> user,\n\t\tChatAdminRightsInfo oldRights,\n\t\tChatAdminRightsInfo newRights,\n\t\tconst QString &rank) {\n\tif (mgInfo) {\n\t\t// If rights are empty - still add participant? TODO check\n\t\tif (!base::contains(mgInfo->lastParticipants, user)) {\n\t\t\tmgInfo->lastParticipants.push_front(user);\n\t\t\tsetMembersCount(membersCount() + 1);\n\t\t\tif (user->isBot() && !mgInfo->bots.contains(user)) {\n\t\t\t\tmgInfo->bots.insert(user);\n\t\t\t\tif (mgInfo->botStatus == Data::BotStatus::NoBots) {\n\t\t\t\t\tmgInfo->botStatus = Data::BotStatus::HasBots;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// If rights are empty - still remove restrictions? TODO check\n\t\tif (mgInfo->lastRestricted.contains(user)) {\n\t\t\tmgInfo->lastRestricted.remove(user);\n\t\t\tif (restrictedCount() > 0) {\n\t\t\t\tsetRestrictedCount(restrictedCount() - 1);\n\t\t\t}\n\t\t}\n\n\t\tauto userId = peerToUser(user->id);\n\t\tauto it = mgInfo->lastAdmins.find(user);\n\t\tif (newRights.flags) {\n\t\t\tauto lastAdmin = MegagroupInfo::Admin { newRights };\n\t\t\tlastAdmin.canEdit = true;\n\t\t\tif (it == mgInfo->lastAdmins.cend()) {\n\t\t\t\tmgInfo->lastAdmins.emplace(user, lastAdmin);\n\t\t\t\tsetAdminsCount(adminsCount() + 1);\n\t\t\t} else {\n\t\t\t\tit->second = lastAdmin;\n\t\t\t}\n\t\t\tData::ChannelAdminChanges(this).add(userId, rank);\n\t\t} else {\n\t\t\tif (it != mgInfo->lastAdmins.cend()) {\n\t\t\t\tmgInfo->lastAdmins.erase(it);\n\t\t\t\tif (adminsCount() > 0) {\n\t\t\t\t\tsetAdminsCount(adminsCount() - 1);\n\t\t\t\t}\n\t\t\t}\n\t\t\tData::ChannelAdminChanges(this).remove(userId);\n\t\t}\n\t}\n\tif (oldRights.flags && !newRights.flags) {\n\t\t// We removed an admin.\n\t\tif (adminsCount() > 1) {\n\t\t\tsetAdminsCount(adminsCount() - 1);\n\t\t}\n\t\tif (!isMegagroup() && user->isBot() && membersCount() > 1) {\n\t\t\t// Removing bot admin removes it from channel.\n\t\t\tsetMembersCount(membersCount() - 1);\n\t\t}\n\t} else if (!oldRights.flags && newRights.flags) {\n\t\t// We added an admin.\n\t\tsetAdminsCount(adminsCount() + 1);\n\t\tupdateFullForced();\n\t}\n\tsession().changes().peerUpdated(this, UpdateFlag::Admins);\n}\n\nvoid ChannelData::applyEditMemberRank(\n\t\tnot_null<UserData*> user,\n\t\tconst QString &rank) {\n\tif (!mgInfo) {\n\t\treturn;\n\t}\n\tconst auto userId = peerToUser(user->id);\n\tData::ChannelMemberRankChanges changes(this);\n\tchanges.feed(userId, rank);\n}\n\nvoid ChannelData::applyEditBanned(\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo oldRights,\n\t\tChatRestrictionsInfo newRights) {\n\tauto flags = UpdateFlag::BannedUsers | UpdateFlag::None;\n\tauto isKicked = newRights.flags & ChatRestriction::ViewMessages;\n\tauto isRestricted = !isKicked && newRights.flags;\n\tconst auto user = participant->asUser();\n\tif (mgInfo && user) {\n\t\t// If rights are empty - still remove admin? TODO check\n\t\tif (mgInfo->lastAdmins.contains(user)) {\n\t\t\tmgInfo->lastAdmins.remove(user);\n\t\t\tif (adminsCount() > 1) {\n\t\t\t\tsetAdminsCount(adminsCount() - 1);\n\t\t\t} else {\n\t\t\t\tflags |= UpdateFlag::Admins;\n\t\t\t}\n\t\t}\n\t\tauto it = mgInfo->lastRestricted.find(user);\n\t\tif (isRestricted) {\n\t\t\tif (it == mgInfo->lastRestricted.cend()) {\n\t\t\t\tmgInfo->lastRestricted.emplace(\n\t\t\t\t\tuser,\n\t\t\t\t\tMegagroupInfo::Restricted { newRights });\n\t\t\t\tsetRestrictedCount(restrictedCount() + 1);\n\t\t\t} else {\n\t\t\t\tit->second.rights = newRights;\n\t\t\t}\n\t\t} else {\n\t\t\tif (it != mgInfo->lastRestricted.cend()) {\n\t\t\t\tmgInfo->lastRestricted.erase(it);\n\t\t\t\tif (restrictedCount() > 0) {\n\t\t\t\t\tsetRestrictedCount(restrictedCount() - 1);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (isKicked) {\n\t\t\t\tauto i = ranges::find(\n\t\t\t\t\tmgInfo->lastParticipants,\n\t\t\t\t\tnot_null{ user });\n\t\t\t\tif (i != mgInfo->lastParticipants.end()) {\n\t\t\t\t\tmgInfo->lastParticipants.erase(i);\n\t\t\t\t\tif (membersCount() > 1) {\n\t\t\t\t\t\tsetMembersCount(membersCount() - 1);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmgInfo->lastParticipantsStatus |= MegagroupInfo::LastParticipantsCountOutdated;\n\t\t\t\t\t\tmgInfo->lastParticipantsCount = 0;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tmgInfo->lastParticipantsStatus |= MegagroupInfo::LastParticipantsCountOutdated;\n\t\t\t\t}\n\t\t\t\tflags |= UpdateFlag::Members;\n\t\t\t\towner().removeMegagroupParticipant(this, user);\n\t\t\t\tsetKickedCount(kickedCount() + 1);\n\t\t\t\tif (mgInfo->bots.contains(user)) {\n\t\t\t\t\tmgInfo->bots.remove(user);\n\t\t\t\t\tif (mgInfo->bots.empty() && mgInfo->botStatus == Data::BotStatus::HasBots) {\n\t\t\t\t\t\tmgInfo->botStatus = Data::BotStatus::NoBots;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tData::ChannelAdminChanges(this).remove(peerToUser(user->id));\n\t} else if (!mgInfo) {\n\t\tif (isKicked) {\n\t\t\tsetKickedCount(kickedCount() + 1);\n\t\t}\n\t}\n\tsession().changes().peerUpdated(this, flags);\n}\n\nvoid ChannelData::setViewAsMessagesFlag(bool enabled) {\n\tif (viewForumAsMessages() == enabled) {\n\t\treturn;\n\t}\n\tsetFlags((flags() & ~Flag::ViewAsMessages)\n\t\t| (enabled ? Flag::ViewAsMessages : Flag()));\n}\n\nvoid ChannelData::markForbidden() {\n\towner().processChat(MTP_channelForbidden(\n\t\tMTP_flags(isMegagroup()\n\t\t\t? MTPDchannelForbidden::Flag::f_megagroup\n\t\t\t: MTPDchannelForbidden::Flag::f_broadcast),\n\t\tMTP_long(peerToChannel(id).bare),\n\t\tMTP_long(_accessHash),\n\t\tMTP_string(name()),\n\t\tMTPint()));\n}\n\nbool ChannelData::isGroupAdmin(not_null<UserData*> user) const {\n\tif (auto info = mgInfo.get()) {\n\t\treturn info->admins.contains(peerToUser(user->id));\n\t}\n\treturn false;\n}\n\nbool ChannelData::lastParticipantsRequestNeeded() const {\n\tif (!mgInfo || !canViewMembers()) {\n\t\treturn false;\n\t} else if (mgInfo->lastParticipantsCount == membersCount()) {\n\t\tmgInfo->lastParticipantsStatus\n\t\t\t&= ~MegagroupInfo::LastParticipantsCountOutdated;\n\t}\n\treturn mgInfo->lastParticipants.empty()\n\t\t|| !(mgInfo->lastParticipantsStatus\n\t\t\t& MegagroupInfo::LastParticipantsOnceReceived)\n\t\t|| (mgInfo->lastParticipantsStatus\n\t\t\t& MegagroupInfo::LastParticipantsCountOutdated);\n}\n\nauto ChannelData::unavailableReasons() const\n-> const std::vector<Data::UnavailableReason> & {\n\treturn _unavailableReasons;\n}\n\nvoid ChannelData::setUnavailableReasonsList(\n\t\tstd::vector<Data::UnavailableReason> &&reasons) {\n\t_unavailableReasons = std::move(reasons);\n}\n\nvoid ChannelData::setAvailableMinId(MsgId availableMinId) {\n\tif (_availableMinId != availableMinId) {\n\t\t_availableMinId = availableMinId;\n\t}\n}\n\nbool ChannelData::canBanMembers() const {\n\treturn amCreator() || (adminRights() & AdminRight::BanUsers);\n}\n\nbool ChannelData::canPostMessages() const {\n\treturn amCreator() || (adminRights() & AdminRight::PostMessages);\n}\n\nbool ChannelData::canEditMessages() const {\n\treturn amCreator() || (adminRights() & AdminRight::EditMessages);\n}\n\nbool ChannelData::canDeleteMessages() const {\n\treturn amCreator() || (adminRights() & AdminRight::DeleteMessages);\n}\n\nbool ChannelData::canPostStories() const {\n\treturn amCreator() || (adminRights() & AdminRight::PostStories);\n}\n\nbool ChannelData::canEditStories() const {\n\tif (isMonoforum()) {\n\t\treturn false;\n\t}\n\treturn amCreator() || (adminRights() & AdminRight::EditStories);\n}\n\nbool ChannelData::canDeleteStories() const {\n\treturn amCreator() || (adminRights() & AdminRight::DeleteStories);\n}\n\nbool ChannelData::canAccessMonoforum() const {\n\treturn amCreator() || (adminRights() & AdminRight::ManageDirect);\n}\n\nbool ChannelData::canPostPaidMedia() const {\n\treturn canPostMessages() && (flags() & Flag::PaidMediaAllowed);\n}\n\nbool ChannelData::anyoneCanAddMembers() const {\n\treturn !(defaultRestrictions() & Restriction::AddParticipants);\n}\n\nbool ChannelData::hiddenPreHistory() const {\n\treturn (flags() & Flag::PreHistoryHidden);\n}\n\nbool ChannelData::canAddMembers() const {\n\treturn isMonoforum()\n\t\t? false\n\t\t: isMegagroup()\n\t\t? !amRestricted(ChatRestriction::AddParticipants)\n\t\t: ((adminRights() & AdminRight::InviteByLinkOrAdd) || amCreator());\n}\n\nbool ChannelData::canAddAdmins() const {\n\treturn amCreator() || (adminRights() & AdminRight::AddAdmins);\n}\n\nbool ChannelData::allowsForwarding() const {\n\treturn !(flags() & Flag::NoForwards);\n}\n\nbool ChannelData::canViewMembers() const {\n\treturn (flags() & Flag::CanViewParticipants)\n\t\t&& (!(flags() & Flag::ParticipantsHidden)\n\t\t\t|| amCreator()\n\t\t\t|| hasAdminRights());\n}\n\nbool ChannelData::canViewAdmins() const {\n\treturn (isMegagroup() || hasAdminRights() || amCreator());\n}\n\nbool ChannelData::canViewBanned() const {\n\treturn (hasAdminRights() || amCreator());\n}\n\nbool ChannelData::canEditInformation() const {\n\treturn isMegagroup()\n\t\t? !amRestricted(Restriction::ChangeInfo)\n\t\t: ((adminRights() & AdminRight::ChangeInfo) || amCreator());\n}\n\nbool ChannelData::canEditPermissions() const {\n\treturn isMegagroup()\n\t\t&& !isGigagroup()\n\t\t&& ((adminRights() & AdminRight::BanUsers) || amCreator());\n}\n\nbool ChannelData::canEditSignatures() const {\n\treturn isBroadcast() && canEditInformation();\n}\n\nbool ChannelData::canEditAutoTranslate() const {\n\treturn isBroadcast() && canEditInformation();\n}\n\nbool ChannelData::canEditPreHistoryHidden() const {\n\treturn isMegagroup()\n\t\t&& ((adminRights() & AdminRight::BanUsers) || amCreator())\n\t\t&& (!isPublic() || canEditUsername());\n}\n\nbool ChannelData::canEditUsername() const {\n\treturn amCreator()\n\t\t&& (flags() & Flag::CanSetUsername);\n}\n\nbool ChannelData::canEditStickers() const {\n\treturn (flags() & Flag::CanSetStickers);\n}\n\nbool ChannelData::canEditEmoji() const {\n\treturn amCreator() || (adminRights() & ChatAdminRight::ChangeInfo);\n}\n\nbool ChannelData::canDelete() const {\n\treturn amCreator();\n}\n\nbool ChannelData::canEditLastAdmin(not_null<UserData*> user) const {\n\t// Duplicated in ParticipantsAdditionalData::canEditAdmin :(\n\tif (mgInfo) {\n\t\tauto i = mgInfo->lastAdmins.find(user);\n\t\tif (i != mgInfo->lastAdmins.cend()) {\n\t\t\treturn i->second.canEdit;\n\t\t}\n\t\treturn (user != mgInfo->creator);\n\t}\n\treturn false;\n}\n\nbool ChannelData::canEditAdmin(not_null<UserData*> user) const {\n\t// Duplicated in ParticipantsAdditionalData::canEditAdmin :(\n\tif (user->isSelf()) {\n\t\treturn false;\n\t} else if (amCreator()) {\n\t\treturn true;\n\t} else if (!canEditLastAdmin(user)) {\n\t\treturn false;\n\t}\n\treturn adminRights() & AdminRight::AddAdmins;\n}\n\nbool ChannelData::canRestrictParticipant(\n\t\tnot_null<PeerData*> participant) const {\n\t// Duplicated in ParticipantsAdditionalData::canRestrictParticipant :(\n\tif (participant->isSelf()) {\n\t\treturn false;\n\t} else if (amCreator()) {\n\t\treturn true;\n\t} else if (const auto user = participant->asUser()) {\n\t\tif (!canEditLastAdmin(user)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn adminRights() & AdminRight::BanUsers;\n}\n\nvoid ChannelData::setBotVerifyDetails(Ui::BotVerifyDetails details) {\n\tif (!details) {\n\t\tif (_botVerifyDetails) {\n\t\t\t_botVerifyDetails = nullptr;\n\t\t\tsession().changes().peerUpdated(this, UpdateFlag::VerifyInfo);\n\t\t}\n\t} else if (!_botVerifyDetails) {\n\t\t_botVerifyDetails = std::make_unique<Ui::BotVerifyDetails>(details);\n\t\tsession().changes().peerUpdated(this, UpdateFlag::VerifyInfo);\n\t} else if (*_botVerifyDetails != details) {\n\t\t*_botVerifyDetails = details;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::VerifyInfo);\n\t}\n}\n\nvoid ChannelData::setBotVerifyDetailsIcon(DocumentId iconId) {\n\tif (!iconId) {\n\t\tsetBotVerifyDetails({});\n\t} else {\n\t\tauto info = _botVerifyDetails\n\t\t\t? *_botVerifyDetails\n\t\t\t: Ui::BotVerifyDetails();\n\t\tinfo.iconId = iconId;\n\t\tsetBotVerifyDetails(info);\n\t}\n}\n\nvoid ChannelData::setAdminRights(ChatAdminRights rights) {\n\tif (rights == adminRights()) {\n\t\treturn;\n\t}\n\t_adminRights.set(rights);\n\tif (!canHaveInviteLink()) {\n\t\tsetPendingRequestsCount(0, std::vector<UserId>{});\n\t}\n\tif (isMegagroup()) {\n\t\tconst auto self = session().user();\n\t\tif (hasAdminRights()) {\n\t\t\tif (!amCreator()) {\n\t\t\t\tauto me = MegagroupInfo::Admin{\n\t\t\t\t\tChatAdminRightsInfo{ rights } };\n\t\t\t\tme.canEdit = false;\n\t\t\t\tmgInfo->lastAdmins.emplace(self, me);\n\t\t\t}\n\t\t\tmgInfo->lastRestricted.remove(self);\n\t\t} else {\n\t\t\tmgInfo->lastAdmins.remove(self);\n\t\t}\n\t}\n\tsession().changes().peerUpdated(\n\t\tthis,\n\t\tUpdateFlag::Rights | UpdateFlag::Admins | UpdateFlag::BannedUsers);\n\tif (isBroadcast() && _monoforumLink) {\n\t\tconst auto flags = _monoforumLink->flags();\n\t\t_monoforumLink->setFlags((flags & ~Flag::MonoforumAdmin)\n\t\t\t| (canAccessMonoforum() ? Flag::MonoforumAdmin : Flag()));\n\t}\n}\n\nvoid ChannelData::setRestrictions(ChatRestrictionsInfo rights) {\n\tif (rights.flags == restrictions() && rights.until == _restrictedUntil) {\n\t\treturn;\n\t}\n\t_restrictedUntil = rights.until;\n\t_restrictions.set(rights.flags);\n\tif (isMegagroup()) {\n\t\tconst auto self = session().user();\n\t\tif (hasRestrictions()) {\n\t\t\tif (!amCreator()) {\n\t\t\t\tauto me = MegagroupInfo::Restricted { rights };\n\t\t\t\tmgInfo->lastRestricted.emplace(self, me);\n\t\t\t}\n\t\t\tmgInfo->lastAdmins.remove(self);\n\t\t\tData::ChannelAdminChanges(this).remove(session().userId());\n\t\t} else {\n\t\t\tmgInfo->lastRestricted.remove(self);\n\t\t}\n\t}\n\tsession().changes().peerUpdated(\n\t\tthis,\n\t\tUpdateFlag::Rights | UpdateFlag::Admins | UpdateFlag::BannedUsers);\n}\n\nvoid ChannelData::setDefaultRestrictions(ChatRestrictions rights) {\n\tif (rights == defaultRestrictions()) {\n\t\treturn;\n\t}\n\t_defaultRestrictions.set(rights);\n\tsession().changes().peerUpdated(this, UpdateFlag::Rights);\n}\n\nChatData *ChannelData::getMigrateFromChat() const {\n\tif (const auto info = mgInfo.get()) {\n\t\treturn info->getMigrateFromChat();\n\t}\n\treturn nullptr;\n}\n\nvoid ChannelData::setMigrateFromChat(ChatData *chat) {\n\tExpects(mgInfo != nullptr);\n\n\tconst auto info = mgInfo.get();\n\tif (chat != info->getMigrateFromChat()) {\n\t\tinfo->setMigrateFromChat(chat);\n\t\tif (amIn()) {\n\t\t\tsession().changes().peerUpdated(this, UpdateFlag::Migration);\n\t\t}\n\t}\n}\n\nint ChannelData::slowmodeSeconds() const {\n\tif (const auto info = mgInfo.get()) {\n\t\treturn info->slowmodeSeconds;\n\t}\n\treturn 0;\n}\n\nvoid ChannelData::setSlowmodeSeconds(int seconds) {\n\tif (!mgInfo || slowmodeSeconds() == seconds) {\n\t\treturn;\n\t}\n\tmgInfo->slowmodeSeconds = seconds;\n\tsession().changes().peerUpdated(this, UpdateFlag::Slowmode);\n}\n\nTimeId ChannelData::slowmodeLastMessage() const {\n\treturn (hasAdminRights()\n\t\t|| amCreator()\n\t\t|| unrestrictedByBoosts()\n\t\t|| !mgInfo)\n\t\t? 0\n\t\t: mgInfo->slowmodeLastMessage;\n}\n\nvoid ChannelData::growSlowmodeLastMessage(TimeId when) {\n\tconst auto info = mgInfo.get();\n\tconst auto now = base::unixtime::now();\n\taccumulate_min(when, now);\n\tif (!info) {\n\t\treturn;\n\t} else if (info->slowmodeLastMessage > now) {\n\t\tinfo->slowmodeLastMessage = when;\n\t} else if (info->slowmodeLastMessage >= when) {\n\t\treturn;\n\t} else {\n\t\tinfo->slowmodeLastMessage = when;\n\t}\n\tsession().changes().peerUpdated(this, UpdateFlag::Slowmode);\n}\n\nint ChannelData::starsPerMessage() const {\n\treturn _starsPerMessage;\n}\n\nint ChannelData::commonStarsPerMessage() const {\n\treturn owner().commonStarsPerMessage(this);\n}\n\nvoid ChannelData::setStarsPerMessage(int stars) {\n\tif (_starsPerMessage != stars) {\n\t\t_starsPerMessage = stars;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::StarsPerMessage);\n\t}\n\tcheckTrustedPayForMessage();\n}\n\nint ChannelData::peerGiftsCount() const {\n\treturn _peerGiftsCount;\n}\n\nvoid ChannelData::setPeerGiftsCount(int count) {\n\tif (_peerGiftsCount != count) {\n\t\t_peerGiftsCount = count;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::PeerGifts);\n\t}\n}\n\nint ChannelData::boostsApplied() const {\n\tif (const auto info = mgInfo.get()) {\n\t\treturn info->boostsApplied;\n\t}\n\treturn 0;\n}\n\nint ChannelData::boostsUnrestrict() const {\n\tif (const auto info = mgInfo.get()) {\n\t\treturn info->boostsUnrestrict;\n\t}\n\treturn 0;\n}\n\nbool ChannelData::unrestrictedByBoosts() const {\n\tif (const auto info = mgInfo.get()) {\n\t\treturn (info->boostsUnrestrict > 0)\n\t\t\t&& (info->boostsApplied >= info->boostsUnrestrict);\n\t}\n\treturn 0;\n}\n\nrpl::producer<bool> ChannelData::unrestrictedByBoostsValue() const {\n\treturn mgInfo\n\t\t? mgInfo->unrestrictedByBoostsChanges.events_starting_with(\n\t\t\tunrestrictedByBoosts())\n\t\t: (rpl::single(false) | rpl::type_erased);\n}\n\nvoid ChannelData::setBoostsUnrestrict(int applied, int unrestrict) {\n\tif (const auto info = mgInfo.get()) {\n\t\tif (info->boostsApplied == applied\n\t\t\t&& info->boostsUnrestrict == unrestrict) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto wasUnrestricted = unrestrictedByBoosts();\n\t\tinfo->boostsApplied = applied;\n\t\tinfo->boostsUnrestrict = unrestrict;\n\t\tconst auto nowUnrestricted = unrestrictedByBoosts();\n\t\tif (wasUnrestricted != nowUnrestricted) {\n\t\t\tinfo->unrestrictedByBoostsChanges.fire_copy(nowUnrestricted);\n\t\t\tsession().changes().peerUpdated(\n\t\t\t\tthis,\n\t\t\t\tUpdateFlag::Rights | UpdateFlag::Slowmode);\n\t\t}\n\t}\n}\n\nvoid ChannelData::setInvitePeek(const QString &hash, TimeId expires) {\n\tif (!_invitePeek) {\n\t\t_invitePeek = std::make_unique<InvitePeek>();\n\t}\n\t_invitePeek->hash = hash;\n\t_invitePeek->expires = expires;\n}\n\nvoid ChannelData::clearInvitePeek() {\n\t_invitePeek = nullptr;\n}\n\nTimeId ChannelData::invitePeekExpires() const {\n\treturn _invitePeek ? _invitePeek->expires : 0;\n}\n\nQString ChannelData::invitePeekHash() const {\n\treturn _invitePeek ? _invitePeek->hash : QString();\n}\n\nvoid ChannelData::privateErrorReceived() {\n\tif (invitePeekExpires()) {\n\t\tconst auto hash = invitePeekHash();\n\t\tfor (const auto &window : session().windows()) {\n\t\t\tclearInvitePeek();\n\t\t\tApi::CheckChatInvite(window, hash, this);\n\t\t\treturn;\n\t\t}\n\t\t_invitePeek->expires = base::unixtime::now();\n\t} else {\n\t\tmarkForbidden();\n\t}\n}\n\nvoid ChannelData::migrateCall(std::unique_ptr<Data::GroupCall> call) {\n\tExpects(_call == nullptr);\n\tExpects(call != nullptr);\n\n\t_call = std::move(call);\n\t_call->setPeer(this);\n\tsession().changes().peerUpdated(this, UpdateFlag::GroupCall);\n\taddFlags(Flag::CallActive);\n}\n\nvoid ChannelData::setGroupCall(\n\t\tconst MTPInputGroupCall &call,\n\t\tTimeId scheduleDate,\n\t\tbool rtmp) {\n\tcall.match([&](const MTPDinputGroupCall &data) {\n\t\tif (_call && _call->id() == data.vid().v) {\n\t\t\treturn;\n\t\t} else if (!_call && !data.vid().v) {\n\t\t\treturn;\n\t\t} else if (!data.vid().v) {\n\t\t\tclearGroupCall();\n\t\t\treturn;\n\t\t}\n\t\tconst auto hasCall = (_call != nullptr);\n\t\tif (hasCall) {\n\t\t\towner().unregisterGroupCall(_call.get());\n\t\t}\n\t\t_call = std::make_unique<Data::GroupCall>(\n\t\t\tthis,\n\t\t\tdata.vid().v,\n\t\t\tdata.vaccess_hash().v,\n\t\t\tscheduleDate,\n\t\t\trtmp,\n\t\t\tData::GroupCallOrigin::Group);\n\t\towner().registerGroupCall(_call.get());\n\t\tsession().changes().peerUpdated(this, UpdateFlag::GroupCall);\n\t\taddFlags(Flag::CallActive);\n\t}, [&](const auto &) {\n\t\tclearGroupCall();\n\t});\n}\n\nvoid ChannelData::clearGroupCall() {\n\tif (!_call) {\n\t\treturn;\n\t}\n\towner().unregisterGroupCall(_call.get());\n\t_call = nullptr;\n\tsession().changes().peerUpdated(this, UpdateFlag::GroupCall);\n\tremoveFlags(Flag::CallActive | Flag::CallNotEmpty);\n}\n\nvoid ChannelData::setGroupCallDefaultJoinAs(PeerId peerId) {\n\t_callDefaultJoinAs = peerId;\n}\n\nPeerId ChannelData::groupCallDefaultJoinAs() const {\n\treturn _callDefaultJoinAs;\n}\n\nvoid ChannelData::setAllowedReactions(Data::AllowedReactions value) {\n\tif (_allowedReactions != value) {\n\t\tif (value.paidEnabled) {\n\t\t\tsession().api().globalPrivacy().loadPaidReactionShownPeer();\n\t\t}\n\t\tconst auto enabled = [](const Data::AllowedReactions &allowed) {\n\t\t\treturn (allowed.type != Data::AllowedReactionsType::Some)\n\t\t\t\t|| !allowed.some.empty()\n\t\t\t\t|| allowed.paidEnabled;\n\t\t};\n\t\tconst auto was = enabled(_allowedReactions);\n\t\t_allowedReactions = std::move(value);\n\t\tconst auto now = enabled(_allowedReactions);\n\t\tif (was != now) {\n\t\t\towner().reactions().updateAllInHistory(this, now);\n\t\t}\n\t\tsession().changes().peerUpdated(this, UpdateFlag::Reactions);\n\t}\n}\n\nconst Data::AllowedReactions &ChannelData::allowedReactions() const {\n\treturn _allowedReactions;\n}\n\nbool ChannelData::hasActiveStories() const {\n\treturn flags() & Flag::HasActiveStories;\n}\n\nbool ChannelData::hasUnreadStories() const {\n\treturn flags() & Flag::HasUnreadStories;\n}\n\nbool ChannelData::hasActiveVideoStream() const {\n\treturn flags() & Flag::HasActiveVideoStream;\n}\n\nvoid ChannelData::setStoriesState(StoriesState state) {\n\tExpects(state != StoriesState::Unknown);\n\n\tconst auto was = flags();\n\tswitch (state) {\n\tcase StoriesState::None:\n\t\t_flags.remove(Flag::HasActiveStories\n\t\t\t| Flag::HasUnreadStories\n\t\t\t| Flag::HasActiveVideoStream);\n\t\tbreak;\n\tcase StoriesState::HasRead:\n\t\t_flags.set(Flag::HasActiveStories\n\t\t\t| (was\n\t\t\t\t& ~(Flag::HasUnreadStories | Flag::HasActiveVideoStream)));\n\t\tbreak;\n\tcase StoriesState::HasUnread:\n\t\t_flags.set((was & ~Flag::HasActiveVideoStream)\n\t\t\t| Flag::HasActiveStories\n\t\t\t| Flag::HasUnreadStories);\n\t\tbreak;\n\tcase StoriesState::HasVideoStream:\n\t\t_flags.set((was & ~Flag::HasUnreadStories)\n\t\t\t| Flag::HasActiveStories\n\t\t\t| Flag::HasActiveVideoStream);\n\t\tbreak;\n\t}\n\tif (flags() != was) {\n\t\tif (const auto history = owner().historyLoaded(this)) {\n\t\t\thistory->updateChatListEntryPostponed();\n\t\t}\n\t\tsession().changes().peerUpdated(this, UpdateFlag::StoriesState);\n\t}\n}\n\nint ChannelData::levelHint() const {\n\treturn _levelHint;\n}\n\nvoid ChannelData::updateLevelHint(int levelHint) {\n\t_levelHint = levelHint;\n}\n\nTimeId ChannelData::subscriptionUntilDate() const {\n\treturn _subscriptionUntilDate;\n}\n\nvoid ChannelData::updateSubscriptionUntilDate(TimeId subscriptionUntilDate) {\n\t_subscriptionUntilDate = subscriptionUntilDate;\n}\n\nMTPInputChannel ChannelData::inputChannel() const {\n\tconst auto item = isLoaded() ? nullptr : owner().messageWithPeer(id);\n\tif (item) {\n\t\treturn MTP_inputChannelFromMessage(\n\t\t\titem->history()->peer->input(),\n\t\t\tMTP_int(item->id.bare),\n\t\t\tMTP_long(peerToChannel(id).bare));\n\t}\n\treturn MTP_inputChannel(\n\t\tMTP_long(peerToChannel(id).bare),\n\t\tMTP_long(_accessHash));\n}\n\nnamespace Data {\n\nvoid ApplyMigration(\n\t\tnot_null<ChatData*> chat,\n\t\tnot_null<ChannelData*> channel) {\n\tExpects(channel->isMegagroup());\n\n\tchat->setMigrateToChannel(channel);\n\tchannel->setMigrateFromChat(chat);\n}\n\nvoid ApplyChannelUpdate(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst MTPDupdateChatDefaultBannedRights &update) {\n\tchannel->setDefaultRestrictions(ChatRestrictionsInfo(\n\t\tupdate.vdefault_banned_rights()).flags);\n}\n\nvoid ApplyChannelUpdate(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst MTPDchannelFull &update) {\n\tconst auto session = &channel->session();\n\n\tif (channel->isMegagroup()) {\n\t\tconst auto suggestions = update.vpending_suggestions().value_or_empty();\n\t\tchannel->owner().setSuggestToGigagroup(\n\t\t\tchannel,\n\t\t\tranges::contains(\n\t\t\t\tsuggestions,\n\t\t\t\t\"convert_to_gigagroup\"_q,\n\t\t\t\t&MTPstring::v));\n\t}\n\n\tchannel->setAvailableMinId(update.vavailable_min_id().value_or_empty());\n\tauto canViewAdmins = channel->canViewAdmins();\n\tauto canViewMembers = channel->canViewMembers();\n\tauto canEditStickers = channel->canEditStickers();\n\n\tif (const auto call = update.vcall()) {\n\t\tchannel->setGroupCall(*call);\n\t} else {\n\t\tchannel->clearGroupCall();\n\t}\n\tif (const auto as = update.vgroupcall_default_join_as()) {\n\t\tchannel->setGroupCallDefaultJoinAs(peerFromMTP(*as));\n\t} else {\n\t\tchannel->setGroupCallDefaultJoinAs(0);\n\t}\n\n\tchannel->setMessagesTTL(update.vttl_period().value_or_empty());\n\tchannel->setStarsPerMessage(\n\t\tupdate.vsend_paid_messages_stars().value_or_empty());\n\tusing Flag = ChannelDataFlag;\n\tconst auto mask = Flag::CanSetUsername\n\t\t| Flag::CanViewParticipants\n\t\t| Flag::CanSetStickers\n\t\t| Flag::PreHistoryHidden\n\t\t| Flag::AntiSpam\n\t\t| Flag::Location\n\t\t| Flag::ParticipantsHidden\n\t\t| Flag::CanGetStatistics\n\t\t| Flag::ViewAsMessages\n\t\t| Flag::CanViewRevenue\n\t\t| Flag::PaidMediaAllowed\n\t\t| Flag::CanViewCreditsRevenue\n\t\t| Flag::StargiftsAvailable\n\t\t| Flag::PaidMessagesAvailable\n\t\t| (channel->starsPerMessage() ? Flag::HasStarsPerMessage : Flag())\n\t\t| Flag::StarsPerMessageKnown;\n\tchannel->setFlags((channel->flags() & ~mask)\n\t\t| (update.is_can_set_username() ? Flag::CanSetUsername : Flag())\n\t\t| (update.is_can_view_participants()\n\t\t\t? Flag::CanViewParticipants\n\t\t\t: Flag())\n\t\t| (update.is_can_set_stickers() ? Flag::CanSetStickers : Flag())\n\t\t| (update.is_hidden_prehistory() ? Flag::PreHistoryHidden : Flag())\n\t\t| (update.is_antispam() ? Flag::AntiSpam : Flag())\n\t\t| (update.vlocation() ? Flag::Location : Flag())\n\t\t| (update.is_participants_hidden()\n\t\t\t? Flag::ParticipantsHidden\n\t\t\t: Flag())\n\t\t| (update.is_can_view_stats() ? Flag::CanGetStatistics : Flag())\n\t\t| (update.is_view_forum_as_messages()\n\t\t\t? Flag::ViewAsMessages\n\t\t\t: Flag())\n\t\t| (update.is_paid_media_allowed() ? Flag::PaidMediaAllowed : Flag())\n\t\t| (update.is_can_view_revenue() ? Flag::CanViewRevenue : Flag())\n\t\t| (update.is_can_view_stars_revenue()\n\t\t\t? Flag::CanViewCreditsRevenue\n\t\t\t: Flag())\n\t\t| (update.is_stargifts_available()\n\t\t\t? Flag::StargiftsAvailable\n\t\t\t: Flag())\n\t\t| (update.is_paid_messages_available()\n\t\t\t? Flag::PaidMessagesAvailable\n\t\t\t: Flag())\n\t\t| (channel->starsPerMessage() ? Flag::HasStarsPerMessage : Flag())\n\t\t| Flag::StarsPerMessageKnown);\n\tchannel->setUserpicPhoto(update.vchat_photo());\n\tif (const auto migratedFrom = update.vmigrated_from_chat_id()) {\n\t\tchannel->addFlags(Flag::Megagroup);\n\t\tconst auto chat = channel->owner().chat(migratedFrom->v);\n\t\tData::ApplyMigration(chat, channel);\n\t}\n\tchannel->setAbout(qs(update.vabout()));\n\tchannel->setMembersCount(update.vparticipants_count().value_or_empty());\n\tchannel->setAdminsCount(update.vadmins_count().value_or_empty());\n\tchannel->setRestrictedCount(update.vbanned_count().value_or_empty());\n\tchannel->setKickedCount(update.vkicked_count().value_or_empty());\n\tchannel->setSlowmodeSeconds(update.vslowmode_seconds().value_or_empty());\n\tchannel->setPeerGiftsCount(update.vstargifts_count().value_or_empty());\n\tif (const auto next = update.vslowmode_next_send_date()) {\n\t\tchannel->growSlowmodeLastMessage(\n\t\t\tnext->v - channel->slowmodeSeconds());\n\t}\n\tif (const auto invite = update.vexported_invite()) {\n\t\tchannel->session().api().inviteLinks().setMyPermanent(\n\t\t\tchannel,\n\t\t\t*invite);\n\t} else {\n\t\tchannel->session().api().inviteLinks().clearMyPermanent(channel);\n\t}\n\tif (const auto location = update.vlocation()) {\n\t\tchannel->setLocation(*location);\n\t} else {\n\t\tchannel->setLocation(MTP_channelLocationEmpty());\n\t}\n\tif (const auto chat = update.vlinked_chat_id()) {\n\t\tchannel->setDiscussionLink(channel->owner().channelLoaded(chat->v));\n\t} else {\n\t\tchannel->setDiscussionLink(nullptr);\n\t}\n\tif (const auto history = channel->owner().historyLoaded(channel)) {\n\t\tif (const auto available = update.vavailable_min_id()) {\n\t\t\thistory->clearUpTill(available->v);\n\t\t}\n\t\tconst auto folderId = update.vfolder_id().value_or_empty();\n\t\tconst auto folder = folderId\n\t\t\t? channel->owner().folderLoaded(folderId)\n\t\t\t: nullptr;\n\t\tauto &histories = channel->owner().histories();\n\t\tif (folder && history->folder() != folder) {\n\t\t\t// If history folder is unknown or not synced, request both.\n\t\t\thistories.requestDialogEntry(history);\n\t\t\thistories.requestDialogEntry(folder);\n\t\t} else if (!history->folderKnown()\n\t\t\t|| channel->pts() != update.vpts().v) {\n\t\t\thistories.requestDialogEntry(history);\n\t\t} else {\n\t\t\thistory->applyDialogFields(\n\t\t\t\thistory->folder(),\n\t\t\t\tupdate.vunread_count().v,\n\t\t\t\tupdate.vread_inbox_max_id().v,\n\t\t\t\tupdate.vread_outbox_max_id().v);\n\t\t}\n\t}\n\tif (const auto pinned = update.vpinned_msg_id()) {\n\t\tSetTopPinnedMessageId(channel, pinned->v);\n\t}\n\tif (channel->isMegagroup()) {\n\t\tauto commands = ranges::views::all(\n\t\t\tupdate.vbot_info().v\n\t\t) | ranges::views::transform(\n\t\t\tData::BotCommandsFromTL\n\t\t) | ranges::to_vector;\n\n\t\tif (channel->mgInfo->setBotCommands(std::move(commands))) {\n\t\t\tchannel->owner().botCommandsChanged(channel);\n\t\t}\n\t\tconst auto stickerSet = update.vstickerset();\n\t\tconst auto sset = stickerSet ? &stickerSet->c_stickerSet() : nullptr;\n\t\tconst auto newStickerSetId = (sset ? sset->vid().v : 0);\n\t\tconst auto oldStickerSetId = channel->mgInfo->stickerSet.id;\n\t\tconst auto stickersChanged = (canEditStickers != channel->canEditStickers())\n\t\t\t|| (oldStickerSetId != newStickerSetId);\n\t\tif (oldStickerSetId != newStickerSetId) {\n\t\t\tchannel->mgInfo->stickerSet = StickerSetIdentifier{\n\t\t\t\t.id = sset ? sset->vid().v : 0,\n\t\t\t\t.accessHash = sset ? sset->vaccess_hash().v : 0,\n\t\t\t};\n\t\t}\n\t\tif (stickersChanged) {\n\t\t\tsession->changes().peerUpdated(channel, UpdateFlag::StickersSet);\n\t\t}\n\t\tconst auto emojiSet = update.vemojiset();\n\t\tconst auto eset = emojiSet ? &emojiSet->c_stickerSet() : nullptr;\n\t\tconst auto newEmojiSetId = (eset ? eset->vid().v : 0);\n\t\tconst auto oldEmojiSetId = channel->mgInfo->emojiSet.id;\n\t\tconst auto emojiChanged = (oldEmojiSetId != newEmojiSetId);\n\t\tif (oldEmojiSetId != newEmojiSetId) {\n\t\t\tchannel->mgInfo->emojiSet = StickerSetIdentifier{\n\t\t\t\t.id = eset ? eset->vid().v : 0,\n\t\t\t\t.accessHash = eset ? eset->vaccess_hash().v : 0,\n\t\t\t};\n\t\t}\n\t\tif (emojiChanged) {\n\t\t\tsession->changes().peerUpdated(channel, UpdateFlag::EmojiSet);\n\t\t}\n\t\tchannel->setBoostsUnrestrict(\n\t\t\tupdate.vboosts_applied().value_or_empty(),\n\t\t\tupdate.vboosts_unrestrict().value_or_empty());\n\t}\n\tchannel->setThemeToken(qs(update.vtheme_emoticon().value_or_empty()));\n\tchannel->setTranslationDisabled(update.is_translations_disabled());\n\n\tconst auto reactionsLimit = update.vreactions_limit().value_or_empty();\n\tif (const auto allowed = update.vavailable_reactions()) {\n\t\tauto parsed = Data::Parse(\n\t\t\t*allowed,\n\t\t\treactionsLimit,\n\t\t\tupdate.is_paid_reactions_available());\n\t\tchannel->setAllowedReactions(std::move(parsed));\n\t} else {\n\t\tchannel->setAllowedReactions({\n\t\t\t.maxCount = reactionsLimit,\n\t\t\t.paidEnabled = update.is_paid_reactions_available(),\n\t\t});\n\t}\n\tchannel->setBotVerifyDetails(\n\t\tParseBotVerifyDetails(update.vbot_verification()));\n\tchannel->owner().stories().apply(channel, update.vstories());\n\tchannel->fullUpdated();\n\tchannel->setPendingRequestsCount(\n\t\tupdate.vrequests_pending().value_or_empty(),\n\t\tupdate.vrecent_requesters().value_or_empty());\n\n\tif (canViewAdmins != channel->canViewAdmins()\n\t\t|| canViewMembers != channel->canViewMembers()) {\n\t\tsession->changes().peerUpdated(channel, UpdateFlag::Rights);\n\t}\n\n\tchannel->owner().notifySettings().apply(\n\t\tchannel,\n\t\tupdate.vnotify_settings());\n\n\tif (update.vstats_dc()) {\n\t\tchannel->owner().applyStatsDcId(channel, update.vstats_dc()->v);\n\t}\n\n\tif (const auto sendAs = update.vdefault_send_as()) {\n\t\tsession->sendAsPeers().setChosen(channel, peerFromMTP(*sendAs));\n\t} else {\n\t\tsession->sendAsPeers().setChosen(channel, PeerId());\n\t}\n\n\tif (const auto paper = update.vwallpaper()) {\n\t\tchannel->setWallPaper(\n\t\t\tData::WallPaper::Create(&channel->session(), *paper));\n\t} else {\n\t\tchannel->setWallPaper({});\n\t}\n\n\tif ((channel->flags() & Flag::CanViewRevenue)\n\t\t|| (channel->flags() & Flag::CanViewCreditsRevenue)) {\n\t\tstatic constexpr auto kTimeout = crl::time(60000);\n\t\tconst auto id = channel->id;\n\t\tconst auto weak = base::make_weak(&channel->session());\n\t\tconst auto creditsLoadLifetime = std::make_shared<rpl::lifetime>();\n\t\tconst auto creditsLoad\n\t\t\t= creditsLoadLifetime->make_state<Api::CreditsStatus>(channel);\n\t\tcreditsLoad->request({}, [=](Data::CreditsStatusSlice slice) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->credits().apply(id, slice.balance);\n\t\t\t}\n\t\t\tcreditsLoadLifetime->destroy();\n\t\t});\n\t\tbase::timer_once(kTimeout) | rpl::on_next([=] {\n\t\t\tcreditsLoadLifetime->destroy();\n\t\t}, *creditsLoadLifetime);\n\t\tconst auto currencyLoadLifetime = std::make_shared<rpl::lifetime>();\n\t\tconst auto currencyLoad\n\t\t\t= currencyLoadLifetime->make_state<Api::EarnStatistics>(channel);\n\t\tconst auto apply = [=](const CreditsAmount &balance) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->credits().applyCurrency(id, balance);\n\t\t\t}\n\t\t\tcurrencyLoadLifetime->destroy();\n\t\t};\n\t\tcurrencyLoad->request() | rpl::on_error_done(\n\t\t\t[=](const QString &error) {\n\t\t\t\tapply(CreditsAmount(0, CreditsType::Ton));\n\t\t\t},\n\t\t\t[=] { apply(currencyLoad->data().currentBalance); },\n\t\t\t*currencyLoadLifetime);\n\t\tbase::timer_once(kTimeout) | rpl::on_next([=] {\n\t\t\tcurrencyLoadLifetime->destroy();\n\t\t}, *currencyLoadLifetime);\n\t}\n\n\t// For clearUpTill() call.\n\tchannel->owner().sendHistoryChangeNotifications();\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_channel.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_peer.h\"\n#include \"data/data_pts_waiter.h\"\n#include \"data/data_location.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"data/data_peer_bot_commands.h\"\n#include \"data/data_user_names.h\"\n\nclass ChannelData;\n\nnamespace Data {\nclass Forum;\nclass SavedMessages;\n} // namespace Data\n\nstruct ChannelLocation {\n\tQString address;\n\tData::LocationPoint point;\n\n\tfriend inline bool operator==(\n\t\t\tconst ChannelLocation &a,\n\t\t\tconst ChannelLocation &b) {\n\t\treturn a.address.isEmpty()\n\t\t\t? b.address.isEmpty()\n\t\t\t: (a.address == b.address && a.point == b.point);\n\t}\n\tfriend inline bool operator!=(\n\t\t\tconst ChannelLocation &a,\n\t\t\tconst ChannelLocation &b) {\n\t\treturn !(a == b);\n\t}\n};\n\nenum class ChannelDataFlag : uint64 {\n\tLeft = (1ULL << 0),\n\tCreator = (1ULL << 1),\n\tForbidden = (1ULL << 2),\n\tCallActive = (1ULL << 3),\n\tCallNotEmpty = (1ULL << 4),\n\tSignatures = (1ULL << 5),\n\tVerified = (1ULL << 6),\n\tScam = (1ULL << 7),\n\tFake = (1ULL << 8),\n\tMegagroup = (1ULL << 9),\n\tBroadcast = (1ULL << 10),\n\tGigagroup = (1ULL << 11),\n\tUsername = (1ULL << 12),\n\tLocation = (1ULL << 13),\n\tCanSetUsername = (1ULL << 14),\n\tCanSetStickers = (1ULL << 15),\n\tPreHistoryHidden = (1ULL << 16),\n\tCanViewParticipants = (1ULL << 17),\n\tHasLink = (1ULL << 18),\n\tSlowmodeEnabled = (1ULL << 19),\n\tNoForwards = (1ULL << 20),\n\tJoinToWrite = (1ULL << 21),\n\tRequestToJoin = (1ULL << 22),\n\tForum = (1ULL << 23),\n\tAntiSpam = (1ULL << 24),\n\tParticipantsHidden = (1ULL << 25),\n\tStoriesHidden = (1ULL << 26),\n\tHasActiveStories = (1ULL << 27),\n\tHasUnreadStories = (1ULL << 28),\n\tCanGetStatistics = (1ULL << 29),\n\tViewAsMessages = (1ULL << 30),\n\tSimilarExpanded = (1ULL << 31),\n\tCanViewRevenue = (1ULL << 32),\n\tPaidMediaAllowed = (1ULL << 33),\n\tCanViewCreditsRevenue = (1ULL << 34),\n\tSignatureProfiles = (1ULL << 35),\n\tStargiftsAvailable = (1ULL << 36),\n\tPaidMessagesAvailable = (1ULL << 37),\n\tAutoTranslation = (1ULL << 38),\n\tMonoforum = (1ULL << 39),\n\tMonoforumAdmin = (1ULL << 40),\n\tMonoforumDisabled = (1ULL << 41),\n\tForumTabs = (1ULL << 42),\n\tHasStarsPerMessage = (1ULL << 43),\n\tStarsPerMessageKnown = (1ULL << 44),\n\tHasActiveVideoStream = (1ULL << 45),\n};\ninline constexpr bool is_flag_type(ChannelDataFlag) { return true; };\nusing ChannelDataFlags = base::flags<ChannelDataFlag>;\n\nclass MegagroupInfo {\npublic:\n\tMegagroupInfo();\n\t~MegagroupInfo();\n\n\tstruct Admin {\n\t\texplicit Admin(ChatAdminRightsInfo rights)\n\t\t: rights(rights) {\n\t\t}\n\t\tAdmin(ChatAdminRightsInfo rights, bool canEdit)\n\t\t: rights(rights)\n\t\t, canEdit(canEdit) {\n\t\t}\n\t\tChatAdminRightsInfo rights;\n\t\tbool canEdit = false;\n\t};\n\n\tstruct Restricted {\n\t\texplicit Restricted(ChatRestrictionsInfo rights)\n\t\t: rights(rights) {\n\t\t}\n\t\tChatRestrictionsInfo rights;\n\t};\n\n\tChatData *getMigrateFromChat() const;\n\tvoid setMigrateFromChat(ChatData *chat);\n\n\tconst ChannelLocation *getLocation() const;\n\tvoid setLocation(const ChannelLocation &location);\n\n\tData::ChatBotCommands::Changed setBotCommands(\n\t\tconst std::vector<Data::BotCommands> &commands);\n\t[[nodiscard]] const Data::ChatBotCommands &botCommands() const {\n\t\treturn _botCommands;\n\t}\n\n\tvoid ensureForum(not_null<ChannelData*> that);\n\t[[nodiscard]] Data::Forum *forum() const;\n\t[[nodiscard]] std::unique_ptr<Data::Forum> takeForumData();\n\n\tvoid ensureMonoforum(not_null<ChannelData*> that);\n\t[[nodiscard]] Data::SavedMessages *monoforum() const;\n\t[[nodiscard]] std::unique_ptr<Data::SavedMessages> takeMonoforumData();\n\n\tstd::deque<not_null<UserData*>> lastParticipants;\n\tbase::flat_map<not_null<UserData*>, Admin> lastAdmins;\n\tbase::flat_map<not_null<UserData*>, Restricted> lastRestricted;\n\tbase::flat_set<not_null<PeerData*>> markupSenders;\n\tbase::flat_set<not_null<UserData*>> bots;\n\trpl::event_stream<bool> unrestrictedByBoostsChanges;\n\n\tbase::flat_set<UserId> admins;\n\tbase::flat_map<UserId, QString> memberRanks;\n\n\tUserData *creator = nullptr; // nullptr means unknown\n\tData::BotStatus botStatus = Data::BotStatus::Unknown;\n\tbool joinedMessageFound = false;\n\tbool adminsLoaded = false;\n\tStickerSetIdentifier stickerSet;\n\tStickerSetIdentifier emojiSet;\n\n\tenum LastParticipantsStatus {\n\t\tLastParticipantsUpToDate       = 0x00,\n\t\tLastParticipantsOnceReceived   = 0x01,\n\t\tLastParticipantsCountOutdated  = 0x02,\n\t};\n\tmutable int lastParticipantsStatus = LastParticipantsUpToDate;\n\tint lastParticipantsCount = 0;\n\tint boostsApplied = 0;\n\tint boostsUnrestrict = 0;\n\n\tint slowmodeSeconds = 0;\n\tTimeId slowmodeLastMessage = 0;\n\nprivate:\n\tChatData *_migratedFrom = nullptr;\n\tChannelLocation _location;\n\tData::ChatBotCommands _botCommands;\n\tstd::unique_ptr<Data::Forum> _forum;\n\tstd::unique_ptr<Data::SavedMessages> _monoforum;\n\n\tfriend class ChannelData;\n\n};\n\nclass ChannelData final : public PeerData {\npublic:\n\tusing Flag = ChannelDataFlag;\n\tusing Flags = Data::Flags<ChannelDataFlags>;\n\n\tusing AdminRight = ChatAdminRight;\n\tusing Restriction = ChatRestriction;\n\tusing AdminRights = ChatAdminRights;\n\tusing Restrictions = ChatRestrictions;\n\tusing AdminRightFlags = Data::Flags<AdminRights>;\n\tusing RestrictionFlags = Data::Flags<Restrictions>;\n\n\tChannelData(not_null<Data::Session*> owner, PeerId id);\n\n\tvoid setName(const QString &name, const QString &username);\n\tvoid setUsername(const QString &username);\n\tvoid setUsernames(const Data::Usernames &newUsernames);\n\tvoid setPhoto(const MTPChatPhoto &photo);\n\n\t[[nodiscard]] uint64 accessHash() const {\n\t\treturn _accessHash;\n\t}\n\tvoid setAccessHash(uint64 accessHash);\n\n\tvoid setFlags(ChannelDataFlags which);\n\tvoid addFlags(ChannelDataFlags which);\n\tvoid removeFlags(ChannelDataFlags which);\n\t[[nodiscard]] auto flags() const {\n\t\treturn _flags.current();\n\t}\n\t[[nodiscard]] auto flagsValue() const {\n\t\treturn _flags.value();\n\t}\n\n\t[[nodiscard]] QString username() const;\n\t[[nodiscard]] QString editableUsername() const;\n\t[[nodiscard]] const std::vector<QString> &usernames() const;\n\t[[nodiscard]] bool isUsernameEditable(QString username) const;\n\n\t[[nodiscard]] int membersCount() const {\n\t\treturn std::max(_membersCount, 1);\n\t}\n\tvoid setMembersCount(int newMembersCount);\n\t[[nodiscard]] bool membersCountKnown() const {\n\t\treturn (_membersCount >= 0);\n\t}\n\n\t[[nodiscard]] int adminsCount() const {\n\t\treturn _adminsCount;\n\t}\n\tvoid setAdminsCount(int newAdminsCount);\n\n\t[[nodiscard]] int restrictedCount() const {\n\t\treturn _restrictedCount;\n\t}\n\tvoid setRestrictedCount(int newRestrictedCount);\n\n\t[[nodiscard]] int kickedCount() const {\n\t\treturn _kickedCount;\n\t}\n\tvoid setKickedCount(int newKickedCount);\n\n\t[[nodiscard]] int pendingRequestsCount() const {\n\t\treturn _pendingRequestsCount;\n\t}\n\t[[nodiscard]] const std::vector<UserId> &recentRequesters() const {\n\t\treturn _recentRequesters;\n\t}\n\tvoid setPendingRequestsCount(\n\t\tint count,\n\t\tconst QVector<MTPlong> &recentRequesters);\n\tvoid setPendingRequestsCount(\n\t\tint count,\n\t\tstd::vector<UserId> recentRequesters);\n\n\t[[nodiscard]] bool haveLeft() const {\n\t\treturn flags() & Flag::Left;\n\t}\n\t[[nodiscard]] bool amIn() const {\n\t\treturn !isForbidden() && !haveLeft();\n\t}\n\t[[nodiscard]] bool addsSignature() const {\n\t\treturn flags() & Flag::Signatures;\n\t}\n\t[[nodiscard]] bool signatureProfiles() const {\n\t\treturn flags() & Flag::SignatureProfiles;\n\t}\n\t[[nodiscard]] bool isForbidden() const {\n\t\treturn flags() & Flag::Forbidden;\n\t}\n\t[[nodiscard]] bool isVerified() const {\n\t\treturn flags() & Flag::Verified;\n\t}\n\t[[nodiscard]] bool isScam() const {\n\t\treturn flags() & Flag::Scam;\n\t}\n\t[[nodiscard]] bool isFake() const {\n\t\treturn flags() & Flag::Fake;\n\t}\n\t[[nodiscard]] bool hasStoriesHidden() const {\n\t\treturn flags() & Flag::StoriesHidden;\n\t}\n\t[[nodiscard]] bool viewForumAsMessages() const {\n\t\treturn flags() & Flag::ViewAsMessages;\n\t}\n\t[[nodiscard]] bool stargiftsAvailable() const {\n\t\treturn flags() & Flag::StargiftsAvailable;\n\t}\n\t[[nodiscard]] bool paidMessagesAvailable() const {\n\t\treturn flags() & Flag::PaidMessagesAvailable;\n\t}\n\t[[nodiscard]] bool hasStarsPerMessage() const {\n\t\treturn flags() & Flag::HasStarsPerMessage;\n\t}\n\t[[nodiscard]] bool starsPerMessageKnown() const {\n\t\treturn flags() & Flag::StarsPerMessageKnown;\n\t}\n\t[[nodiscard]] bool useSubsectionTabs() const;\n\n\t[[nodiscard]] static ChatRestrictionsInfo KickedRestrictedRights(\n\t\tnot_null<PeerData*> participant);\n\tstatic constexpr auto kRestrictUntilForever = TimeId(INT_MAX);\n\t[[nodiscard]] static bool IsRestrictedForever(TimeId until) {\n\t\treturn !until || (until == kRestrictUntilForever);\n\t}\n\tvoid applyEditAdmin(\n\t\tnot_null<UserData*> user,\n\t\tChatAdminRightsInfo oldRights,\n\t\tChatAdminRightsInfo newRights,\n\t\tconst QString &rank);\n\tvoid applyEditMemberRank(\n\t\tnot_null<UserData*> user,\n\t\tconst QString &rank);\n\tvoid applyEditBanned(\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo oldRights,\n\t\tChatRestrictionsInfo newRights);\n\tvoid setViewAsMessagesFlag(bool enabled);\n\n\tvoid markForbidden();\n\n\t[[nodiscard]] bool isGroupAdmin(not_null<UserData*> user) const;\n\t[[nodiscard]] bool lastParticipantsRequestNeeded() const;\n\t[[nodiscard]] bool isMegagroup() const {\n\t\treturn flags() & Flag::Megagroup;\n\t}\n\t[[nodiscard]] bool isBroadcast() const {\n\t\treturn flags() & Flag::Broadcast;\n\t}\n\t[[nodiscard]] bool isGigagroup() const {\n\t\treturn flags() & Flag::Gigagroup;\n\t}\n\t[[nodiscard]] bool isForum() const {\n\t\treturn flags() & Flag::Forum;\n\t}\n\t[[nodiscard]] bool isMonoforum() const {\n\t\treturn flags() & Flag::Monoforum;\n\t}\n\t[[nodiscard]] bool hasUsername() const {\n\t\treturn flags() & Flag::Username;\n\t}\n\t[[nodiscard]] bool hasLocation() const {\n\t\treturn flags() & Flag::Location;\n\t}\n\t[[nodiscard]] bool isPublic() const {\n\t\treturn hasUsername() || hasLocation();\n\t}\n\t[[nodiscard]] bool amCreator() const {\n\t\treturn flags() & Flag::Creator;\n\t}\n\t[[nodiscard]] bool joinToWrite() const {\n\t\treturn flags() & Flag::JoinToWrite;\n\t}\n\t[[nodiscard]] bool requestToJoin() const {\n\t\treturn flags() & Flag::RequestToJoin;\n\t}\n\t[[nodiscard]] bool antiSpamMode() const {\n\t\treturn flags() & Flag::AntiSpam;\n\t}\n\t[[nodiscard]] bool autoTranslation() const {\n\t\treturn flags() & Flag::AutoTranslation;\n\t}\n\t[[nodiscard]] auto adminRights() const {\n\t\treturn _adminRights.current();\n\t}\n\t[[nodiscard]] auto adminRightsValue() const {\n\t\treturn _adminRights.value();\n\t}\n\tvoid setAdminRights(ChatAdminRights rights);\n\t[[nodiscard]] bool hasAdminRights() const {\n\t\treturn (adminRights() != 0);\n\t}\n\n\t[[nodiscard]] auto restrictions() const {\n\t\treturn _restrictions.current();\n\t}\n\t[[nodiscard]] auto restrictionsValue() const {\n\t\treturn _restrictions.value();\n\t}\n\t[[nodiscard]] TimeId restrictedUntil() const {\n\t\treturn _restrictedUntil;\n\t}\n\tvoid setRestrictions(ChatRestrictionsInfo rights);\n\t[[nodiscard]] bool hasRestrictions() const {\n\t\treturn (restrictions() != 0);\n\t}\n\t[[nodiscard]] bool hasRestrictions(TimeId now) const {\n\t\treturn hasRestrictions()\n\t\t\t&& (restrictedUntil() > now);\n\t}\n\n\t[[nodiscard]] auto defaultRestrictions() const {\n\t\treturn _defaultRestrictions.current();\n\t}\n\t[[nodiscard]] auto defaultRestrictionsValue() const {\n\t\treturn _defaultRestrictions.value();\n\t}\n\tvoid setDefaultRestrictions(ChatRestrictions rights);\n\n\t// Like in ChatData.\n\t[[nodiscard]] bool allowsForwarding() const;\n\t[[nodiscard]] bool canEditInformation() const;\n\t[[nodiscard]] bool canEditPermissions() const;\n\t[[nodiscard]] bool canEditUsername() const;\n\t[[nodiscard]] bool canEditPreHistoryHidden() const;\n\t[[nodiscard]] bool canAddMembers() const;\n\t[[nodiscard]] bool canAddAdmins() const;\n\t[[nodiscard]] bool canBanMembers() const;\n\t[[nodiscard]] bool anyoneCanAddMembers() const;\n\n\t[[nodiscard]] bool canPostMessages() const;\n\t[[nodiscard]] bool canEditMessages() const;\n\t[[nodiscard]] bool canDeleteMessages() const;\n\t[[nodiscard]] bool canPostStories() const;\n\t[[nodiscard]] bool canEditStories() const;\n\t[[nodiscard]] bool canDeleteStories() const;\n\t[[nodiscard]] bool canPostPaidMedia() const;\n\t[[nodiscard]] bool canAccessMonoforum() const;\n\t[[nodiscard]] bool hiddenPreHistory() const;\n\t[[nodiscard]] bool canViewMembers() const;\n\t[[nodiscard]] bool canViewAdmins() const;\n\t[[nodiscard]] bool canViewBanned() const;\n\t[[nodiscard]] bool canEditSignatures() const;\n\t[[nodiscard]] bool canEditAutoTranslate() const;\n\t[[nodiscard]] bool canEditStickers() const;\n\t[[nodiscard]] bool canEditEmoji() const;\n\t[[nodiscard]] bool canDelete() const;\n\t[[nodiscard]] bool canEditAdmin(not_null<UserData*> user) const;\n\t[[nodiscard]] bool canRestrictParticipant(\n\t\tnot_null<PeerData*> participant) const;\n\n\tvoid setBotVerifyDetails(Ui::BotVerifyDetails details);\n\tvoid setBotVerifyDetailsIcon(DocumentId iconId);\n\t[[nodiscard]] Ui::BotVerifyDetails *botVerifyDetails() const {\n\t\treturn _botVerifyDetails.get();\n\t}\n\n\tvoid setInviteLink(const QString &newInviteLink);\n\t[[nodiscard]] QString inviteLink() const {\n\t\treturn _inviteLink;\n\t}\n\t[[nodiscard]] bool canHaveInviteLink() const;\n\n\tvoid setLocation(const MTPChannelLocation &data);\n\t[[nodiscard]] const ChannelLocation *getLocation() const;\n\n\tvoid setDiscussionLink(ChannelData *link);\n\t[[nodiscard]] ChannelData *discussionLink() const;\n\t[[nodiscard]] bool discussionLinkKnown() const;\n\n\tvoid setMonoforumLink(ChannelData *link);\n\t[[nodiscard]] ChannelData *monoforumLink() const;\n\t[[nodiscard]] bool monoforumDisabled() const;\n\n\tvoid ptsInit(int32 pts) {\n\t\t_ptsWaiter.init(pts);\n\t}\n\tvoid ptsReceived(int32 pts) {\n\t\t_ptsWaiter.updateAndApply(this, pts, 0);\n\t}\n\tbool ptsUpdateAndApply(int32 pts, int32 count) {\n\t\treturn _ptsWaiter.updateAndApply(this, pts, count);\n\t}\n\tbool ptsUpdateAndApply(\n\t\tint32 pts,\n\t\tint32 count,\n\t\tconst MTPUpdate &update) {\n\t\treturn _ptsWaiter.updateAndApply(this, pts, count, update);\n\t}\n\tbool ptsUpdateAndApply(\n\t\t\tint32 pts,\n\t\t\tint32 count,\n\t\t\tconst MTPUpdates &updates) {\n\t\treturn _ptsWaiter.updateAndApply(this, pts, count, updates);\n\t}\n\t[[nodiscard]] int32 pts() const {\n\t\treturn _ptsWaiter.current();\n\t}\n\t[[nodiscard]] bool ptsInited() const {\n\t\treturn _ptsWaiter.inited();\n\t}\n\t[[nodiscard]] bool ptsRequesting() const {\n\t\treturn _ptsWaiter.requesting();\n\t}\n\tvoid ptsSetRequesting(bool isRequesting) {\n\t\treturn _ptsWaiter.setRequesting(isRequesting);\n\t}\n\t// < 0 - not waiting\n\tvoid ptsSetWaitingForShortPoll(int32 ms) {\n\t\treturn _ptsWaiter.setWaitingForShortPoll(this, ms);\n\t}\n\t[[nodiscard]] bool ptsWaitingForSkipped() const {\n\t\treturn _ptsWaiter.waitingForSkipped();\n\t}\n\t[[nodiscard]] bool ptsWaitingForShortPoll() const {\n\t\treturn _ptsWaiter.waitingForShortPoll();\n\t}\n\n\t[[nodiscard]] MsgId availableMinId() const {\n\t\treturn _availableMinId;\n\t}\n\tvoid setAvailableMinId(MsgId availableMinId);\n\n\t[[nodiscard]] ChatData *getMigrateFromChat() const;\n\tvoid setMigrateFromChat(ChatData *chat);\n\n\t[[nodiscard]] int slowmodeSeconds() const;\n\tvoid setSlowmodeSeconds(int seconds);\n\t[[nodiscard]] TimeId slowmodeLastMessage() const;\n\tvoid growSlowmodeLastMessage(TimeId when);\n\n\tvoid setStarsPerMessage(int stars);\n\t[[nodiscard]] int starsPerMessage() const;\n\t[[nodiscard]] int commonStarsPerMessage() const;\n\n\t[[nodiscard]] int peerGiftsCount() const;\n\tvoid setPeerGiftsCount(int count);\n\n\t[[nodiscard]] int boostsApplied() const;\n\t[[nodiscard]] int boostsUnrestrict() const;\n\t[[nodiscard]] bool unrestrictedByBoosts() const;\n\t[[nodiscard]] rpl::producer<bool> unrestrictedByBoostsValue() const;\n\tvoid setBoostsUnrestrict(int applied, int unrestrict);\n\n\tvoid setInvitePeek(const QString &hash, TimeId expires);\n\tvoid clearInvitePeek();\n\t[[nodiscard]] TimeId invitePeekExpires() const;\n\t[[nodiscard]] QString invitePeekHash() const;\n\tvoid privateErrorReceived();\n\n\t[[nodiscard]] Data::GroupCall *groupCall() const {\n\t\treturn _call.get();\n\t}\n\tvoid migrateCall(std::unique_ptr<Data::GroupCall> call);\n\tvoid setGroupCall(\n\t\tconst MTPInputGroupCall &call,\n\t\tTimeId scheduleDate = 0,\n\t\tbool rtmp = false);\n\tvoid clearGroupCall();\n\tvoid setGroupCallDefaultJoinAs(PeerId peerId);\n\t[[nodiscard]] PeerId groupCallDefaultJoinAs() const;\n\n\tvoid setAllowedReactions(Data::AllowedReactions value);\n\t[[nodiscard]] const Data::AllowedReactions &allowedReactions() const;\n\n\t[[nodiscard]] bool hasActiveStories() const;\n\t[[nodiscard]] bool hasUnreadStories() const;\n\t[[nodiscard]] bool hasActiveVideoStream() const;\n\tvoid setStoriesState(StoriesState state);\n\n\t[[nodiscard]] Data::Forum *forum() const {\n\t\treturn mgInfo ? mgInfo->forum() : nullptr;\n\t}\n\t[[nodiscard]] Data::SavedMessages *monoforum() const {\n\t\treturn mgInfo ? mgInfo->monoforum() : nullptr;\n\t}\n\n\t[[nodiscard]] int levelHint() const;\n\tvoid updateLevelHint(int levelHint);\n\n\t[[nodiscard]] TimeId subscriptionUntilDate() const;\n\tvoid updateSubscriptionUntilDate(TimeId subscriptionUntilDate);\n\n\t[[nodiscard]] MTPInputChannel inputChannel() const;\n\n\t// Still public data members.\n\tint32 date = 0;\n\tstd::unique_ptr<MegagroupInfo> mgInfo;\n\n\t// > 0 - user who invited me to channel, < 0 - not in channel.\n\tUserId inviter = 0;\n\tTimeId inviteDate = 0;\n\tbool inviteViaRequest = false;\n\nprivate:\n\tstruct InvitePeek {\n\t\tQString hash;\n\t\tTimeId expires = 0;\n\t};\n\n\tauto unavailableReasons() const\n\t\t-> const std::vector<Data::UnavailableReason> & override;\n\tbool canEditLastAdmin(not_null<UserData*> user) const;\n\n\tvoid setUnavailableReasonsList(\n\t\tstd::vector<Data::UnavailableReason> &&reasons) override;\n\n\tFlags _flags = ChannelDataFlags(Flag::Forbidden);\n\n\tPtsWaiter _ptsWaiter;\n\n\tData::UsernamesInfo _username;\n\n\tstd::vector<UserId> _recentRequesters;\n\tMsgId _availableMinId = 0;\n\n\tuint64 _accessHash = 0;\n\n\tRestrictionFlags _defaultRestrictions;\n\tAdminRightFlags _adminRights;\n\tRestrictionFlags _restrictions;\n\tTimeId _restrictedUntil;\n\tTimeId _subscriptionUntilDate;\n\n\tstd::vector<Data::UnavailableReason> _unavailableReasons;\n\tstd::unique_ptr<InvitePeek> _invitePeek;\n\tQString _inviteLink;\n\n\tChannelData *_discussionLink = nullptr;\n\tChannelData *_monoforumLink = nullptr;\n\tbool _discussionLinkKnown = false;\n\n\tint _peerGiftsCount = 0;\n\tint _membersCount = -1;\n\tint _adminsCount = 1;\n\tint _restrictedCount = 0;\n\tint _kickedCount = 0;\n\tint _pendingRequestsCount = 0;\n\tint _levelHint = 0;\n\tint _starsPerMessage = 0;\n\n\tData::AllowedReactions _allowedReactions;\n\n\tstd::unique_ptr<Data::GroupCall> _call;\n\tPeerId _callDefaultJoinAs = 0;\n\n\tstd::unique_ptr<Ui::BotVerifyDetails> _botVerifyDetails;\n\n};\n\nnamespace Data {\n\nvoid ApplyMigration(\n\tnot_null<ChatData*> chat,\n\tnot_null<ChannelData*> channel);\n\nvoid ApplyChannelUpdate(\n\tnot_null<ChannelData*> channel,\n\tconst MTPDupdateChatDefaultBannedRights &update);\n\nvoid ApplyChannelUpdate(\n\tnot_null<ChannelData*> channel,\n\tconst MTPDchannelFull &update);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_channel_admins.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_channel_admins.h\"\n\n#include \"history/history.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"main/main_session.h\"\n\nnamespace Data {\n\nChannelAdminChanges::ChannelAdminChanges(not_null<ChannelData*> channel)\n: _channel(channel)\n, _admins(_channel->mgInfo->admins)\n, _oldCreator(_channel->mgInfo->creator) {\n}\n\nvoid ChannelAdminChanges::add(UserId userId, const QString &rank) {\n\tif (_admins.emplace(userId).second) {\n\t\t_changes.emplace(userId);\n\t}\n\tauto &ranks = _channel->mgInfo->memberRanks;\n\tif (!rank.isEmpty()) {\n\t\tconst auto i = ranks.find(userId);\n\t\tif (i == end(ranks) || i->second != rank) {\n\t\t\tranks[userId] = rank;\n\t\t\t_changes.emplace(userId);\n\t\t}\n\t} else {\n\t\tif (ranks.remove(userId)) {\n\t\t\t_changes.emplace(userId);\n\t\t}\n\t}\n}\n\nvoid ChannelAdminChanges::remove(UserId userId) {\n\tif (_admins.remove(userId)) {\n\t\t_changes.emplace(userId);\n\t}\n}\n\nChannelAdminChanges::~ChannelAdminChanges() {\n\tconst auto creator = _channel->mgInfo->creator;\n\tif (creator != _oldCreator) {\n\t\tif (creator) {\n\t\t\t_changes.emplace(peerToUser(creator->id));\n\t\t}\n\t\tif (_oldCreator) {\n\t\t\t_changes.emplace(peerToUser(_oldCreator->id));\n\t\t}\n\t}\n\tif (_changes.size() > 1\n\t\t|| (!_changes.empty()\n\t\t\t&& _changes.front() != _channel->session().userId())) {\n\t\tif (const auto history = _channel->owner().historyLoaded(_channel)) {\n\t\t\thistory->applyGroupAdminChanges(_changes);\n\t\t}\n\t}\n}\n\nChannelMemberRankChanges::ChannelMemberRankChanges(\n\t\tnot_null<ChannelData*> channel)\n: _channel(channel)\n, _memberRanks(_channel->mgInfo->memberRanks) {\n}\n\nvoid ChannelMemberRankChanges::feed(\n\t\tUserId userId,\n\t\tconst QString &rank) {\n\tif (rank.isEmpty()) {\n\t\tif (_memberRanks.remove(userId)) {\n\t\t\t_changes.emplace(userId);\n\t\t}\n\t} else {\n\t\tconst auto i = _memberRanks.find(userId);\n\t\tif (i == end(_memberRanks) || i->second != rank) {\n\t\t\t_memberRanks[userId] = rank;\n\t\t\t_changes.emplace(userId);\n\t\t}\n\t}\n}\n\nChannelMemberRankChanges::~ChannelMemberRankChanges() {\n\tif (_changes.empty()) {\n\t\treturn;\n\t}\n\tconst auto info = _channel->mgInfo.get();\n\tauto adminChanges = base::flat_set<UserId>();\n\tfor (const auto &userId : _changes) {\n\t\tif (info->admins.contains(userId)\n\t\t\t|| (info->creator\n\t\t\t\t&& peerToUser(info->creator->id) == userId)) {\n\t\t\tadminChanges.emplace(userId);\n\t\t}\n\t}\n\tif (adminChanges.size() > 1\n\t\t|| (!adminChanges.empty()\n\t\t\t&& adminChanges.front() != _channel->session().userId())) {\n\t\tif (const auto history\n\t\t\t= _channel->owner().historyLoaded(_channel)) {\n\t\t\thistory->applyGroupAdminChanges(adminChanges);\n\t\t}\n\t}\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_channel_admins.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\nclass ChannelAdminChanges {\npublic:\n\tChannelAdminChanges(not_null<ChannelData*> channel);\n\n\tvoid add(UserId userId, const QString &rank);\n\tvoid remove(UserId userId);\n\n\t~ChannelAdminChanges();\n\nprivate:\n\tnot_null<ChannelData*> _channel;\n\tbase::flat_set<UserId> &_admins;\n\tbase::flat_set<UserId> _changes;\n\tUserData *_oldCreator = nullptr;\n\n};\n\nclass ChannelMemberRankChanges {\npublic:\n\tChannelMemberRankChanges(not_null<ChannelData*> channel);\n\n\tvoid feed(UserId userId, const QString &rank);\n\n\t~ChannelMemberRankChanges();\n\nprivate:\n\tnot_null<ChannelData*> _channel;\n\tbase::flat_map<UserId, QString> &_memberRanks;\n\tbase::flat_set<UserId> _changes;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_channel_earn.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QDateTime>\n\n#include \"data/data_credits.h\"\n#include \"data/data_statistics_chart.h\"\n\nnamespace Data {\n\nusing EarnInt = uint64;\n\nstruct EarnHistorySlice final {\n\tusing OffsetToken = QString;\n\tstd::vector<CreditsHistoryEntry> list;\n\tint total = 0;\n\tbool allLoaded = false;\n\tOffsetToken token;\n};\n\nstruct EarnStatistics final {\n\texplicit operator bool() const {\n\t\treturn !!usdRate;\n\t}\n\tData::StatisticalGraph topHoursGraph;\n\tData::StatisticalGraph revenueGraph;\n\tCreditsAmount currentBalance;\n\tCreditsAmount availableBalance;\n\tCreditsAmount overallRevenue;\n\tfloat64 usdRate = 0.;\n\tbool switchedOff = false;\n\n\tEarnHistorySlice firstHistorySlice;\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_chat.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_chat.h\"\n\n#include \"core/application.h\"\n#include \"data/data_user.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"api/api_invite_links.h\"\n\nnamespace {\n\nusing UpdateFlag = Data::PeerUpdate::Flag;\n\n} // namespace\n\nChatData::ChatData(not_null<Data::Session*> owner, PeerId id)\n: PeerData(owner, id) {\n\t_flags.changes(\n\t) | rpl::on_next([=](const Flags::Change &change) {\n\t\tif (change.diff & Flag::CallNotEmpty) {\n\t\t\tif (const auto history = this->owner().historyLoaded(this)) {\n\t\t\t\thistory->updateChatListEntry();\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n}\n\nvoid ChatData::setPhoto(const MTPChatPhoto &photo) {\n\tphoto.match([&](const MTPDchatPhoto &data) {\n\t\tupdateUserpic(\n\t\t\tdata.vphoto_id().v,\n\t\t\tdata.vdc_id().v,\n\t\t\tdata.is_has_video());\n\t}, [&](const MTPDchatPhotoEmpty &) {\n\t\tclearUserpic();\n\t});\n}\n\nChatAdminRightsInfo ChatData::defaultAdminRights(not_null<UserData*> user) {\n\tconst auto isCreator = (creator == peerToUser(user->id))\n\t\t|| (user->isSelf() && amCreator());\n\tusing Flag = ChatAdminRight;\n\treturn ChatAdminRightsInfo(Flag::Other\n\t\t| Flag::ChangeInfo\n\t\t| Flag::DeleteMessages\n\t\t| Flag::BanUsers\n\t\t| Flag::InviteByLinkOrAdd\n\t\t| Flag::PinMessages\n\t\t| Flag::ManageCall\n\t\t| (isCreator ? Flag::AddAdmins : Flag(0)));\n}\n\nbool ChatData::allowsForwarding() const {\n\treturn !(flags() & Flag::NoForwards);\n}\n\nbool ChatData::canEditInformation() const {\n\treturn amIn() && !amRestricted(ChatRestriction::ChangeInfo);\n}\n\nbool ChatData::canEditPermissions() const {\n\treturn amIn()\n\t\t&& (amCreator() || (adminRights() & ChatAdminRight::BanUsers));\n}\n\nbool ChatData::canEditUsername() const {\n\treturn amCreator()\n\t\t&& (flags() & Flag::CanSetUsername);\n}\n\nbool ChatData::canEditPreHistoryHidden() const {\n\treturn amCreator();\n}\n\nbool ChatData::canDeleteMessages() const {\n\treturn amCreator()\n\t\t|| (adminRights() & ChatAdminRight::DeleteMessages);\n}\n\nbool ChatData::canAddMembers() const {\n\treturn amIn() && !amRestricted(ChatRestriction::AddParticipants);\n}\n\nbool ChatData::canAddAdmins() const {\n\treturn amIn() && amCreator();\n}\n\nbool ChatData::canBanMembers() const {\n\treturn amCreator()\n\t\t|| (adminRights() & ChatAdminRight::BanUsers);\n}\n\nbool ChatData::anyoneCanAddMembers() const {\n\treturn !(defaultRestrictions() & ChatRestriction::AddParticipants);\n}\n\nvoid ChatData::setName(const QString &newName) {\n\tupdateNameDelayed(newName.isEmpty() ? name() : newName, {}, {});\n}\n\nvoid ChatData::applyEditAdmin(not_null<UserData*> user, bool isAdmin) {\n\tif (isAdmin) {\n\t\tadmins.emplace(user);\n\t} else {\n\t\tadmins.remove(user);\n\t}\n\tsession().changes().peerUpdated(this, UpdateFlag::Admins);\n}\n\nvoid ChatData::invalidateParticipants() {\n\tparticipants.clear();\n\tadmins.clear();\n\tsetAdminRights(ChatAdminRights());\n\t//setDefaultRestrictions(ChatRestrictions());\n\tinvitedByMe.clear();\n\tbotStatus = Data::BotStatus::Unknown;\n\tsession().changes().peerUpdated(\n\t\tthis,\n\t\tUpdateFlag::Members | UpdateFlag::Admins);\n}\n\nvoid ChatData::setFlags(ChatDataFlags which) {\n\tconst auto wasIn = amIn();\n\t_flags.set(which);\n\tif (wasIn && !amIn()) {\n\t\tcrl::on_main(&session(), [=] {\n\t\t\tif (!amIn()) {\n\t\t\t\tCore::App().closeChatFromWindows(this);\n\t\t\t}\n\t\t});\n\t}\n}\n\nvoid ChatData::setInviteLink(const QString &newInviteLink) {\n\t_inviteLink = newInviteLink;\n}\n\nbool ChatData::canHaveInviteLink() const {\n\treturn amCreator()\n\t\t|| (adminRights() & ChatAdminRight::InviteByLinkOrAdd);\n}\n\nvoid ChatData::setAdminRights(ChatAdminRights rights) {\n\tif (rights == adminRights()) {\n\t\treturn;\n\t}\n\t_adminRights.set(rights);\n\tif (!canHaveInviteLink()) {\n\t\tsetPendingRequestsCount(0, std::vector<UserId>{});\n\t}\n\tsession().changes().peerUpdated(\n\t\tthis,\n\t\tUpdateFlag::Rights | UpdateFlag::Admins | UpdateFlag::BannedUsers);\n}\n\nvoid ChatData::setDefaultRestrictions(ChatRestrictions rights) {\n\tif (rights == defaultRestrictions()) {\n\t\treturn;\n\t}\n\t_defaultRestrictions.set(rights);\n\tsession().changes().peerUpdated(this, UpdateFlag::Rights);\n}\n\nvoid ChatData::refreshBotStatus() {\n\tif (participants.empty()) {\n\t\tbotStatus = Data::BotStatus::Unknown;\n\t} else {\n\t\tconst auto noBots = ranges::none_of(\n\t\t\tparticipants,\n\t\t\t&UserData::isBot);\n\t\tbotStatus = noBots\n\t\t\t? Data::BotStatus::NoBots\n\t\t\t: Data::BotStatus::HasBots;\n\t}\n}\n\nauto ChatData::applyUpdateVersion(int version) -> UpdateStatus {\n\tif (_version > version) {\n\t\treturn UpdateStatus::TooOld;\n\t} else if (_version + 1 < version) {\n\t\tinvalidateParticipants();\n\t\tsession().api().requestFullPeer(this);\n\t\treturn UpdateStatus::Skipped;\n\t}\n\tsetVersion(version);\n\treturn UpdateStatus::Good;\n}\n\nChannelData *ChatData::getMigrateToChannel() const {\n\treturn _migratedTo;\n}\n\nvoid ChatData::setMigrateToChannel(ChannelData *channel) {\n\tif (_migratedTo != channel) {\n\t\t_migratedTo = channel;\n\t\tif (channel->amIn()) {\n\t\t\tsession().changes().peerUpdated(this, UpdateFlag::Migration);\n\t\t}\n\t}\n}\n\nvoid ChatData::setGroupCall(\n\t\tconst MTPInputGroupCall &call,\n\t\tTimeId scheduleDate,\n\t\tbool rtmp) {\n\tif (migrateTo()) {\n\t\treturn;\n\t}\n\tcall.match([&](const MTPDinputGroupCall &data) {\n\t\tif (_call && _call->id() == data.vid().v) {\n\t\t\treturn;\n\t\t} else if (!_call && !data.vid().v) {\n\t\t\treturn;\n\t\t} else if (!data.vid().v) {\n\t\t\tclearGroupCall();\n\t\t\treturn;\n\t\t}\n\t\tconst auto hasCall = (_call != nullptr);\n\t\tif (hasCall) {\n\t\t\towner().unregisterGroupCall(_call.get());\n\t\t}\n\t\t_call = std::make_unique<Data::GroupCall>(\n\t\t\tthis,\n\t\t\tdata.vid().v,\n\t\t\tdata.vaccess_hash().v,\n\t\t\tscheduleDate,\n\t\t\trtmp,\n\t\t\tData::GroupCallOrigin::Group);\n\t\towner().registerGroupCall(_call.get());\n\t\tsession().changes().peerUpdated(this, UpdateFlag::GroupCall);\n\t\taddFlags(Flag::CallActive);\n\t}, [&](const auto &) {\n\t\tclearGroupCall();\n\t});\n}\n\nvoid ChatData::clearGroupCall() {\n\tif (!_call) {\n\t\treturn;\n\t} else if (const auto group = migrateTo(); group && !group->groupCall()) {\n\t\tgroup->migrateCall(base::take(_call));\n\t} else {\n\t\towner().unregisterGroupCall(_call.get());\n\t\t_call = nullptr;\n\t}\n\tsession().changes().peerUpdated(this, UpdateFlag::GroupCall);\n\tremoveFlags(Flag::CallActive | Flag::CallNotEmpty);\n}\n\nvoid ChatData::setGroupCallDefaultJoinAs(PeerId peerId) {\n\t_callDefaultJoinAs = peerId;\n}\n\nPeerId ChatData::groupCallDefaultJoinAs() const {\n\treturn _callDefaultJoinAs;\n}\n\nvoid ChatData::setBotCommands(const std::vector<Data::BotCommands> &list) {\n\tif (_botCommands.update(list)) {\n\t\towner().botCommandsChanged(this);\n\t}\n}\n\nvoid ChatData::setPendingRequestsCount(\n\t\tint count,\n\t\tconst QVector<MTPlong> &recentRequesters) {\n\tsetPendingRequestsCount(count, ranges::views::all(\n\t\trecentRequesters\n\t) | ranges::views::transform([&](const MTPlong &value) {\n\t\treturn UserId(value);\n\t}) | ranges::to_vector);\n}\n\nvoid ChatData::setPendingRequestsCount(\n\t\tint count,\n\t\tstd::vector<UserId> recentRequesters) {\n\tif (_pendingRequestsCount != count\n\t\t|| _recentRequesters != recentRequesters) {\n\t\t_pendingRequestsCount = count;\n\t\t_recentRequesters = std::move(recentRequesters);\n\t\tsession().changes().peerUpdated(this, UpdateFlag::PendingRequests);\n\t}\n}\n\nvoid ChatData::setAllowedReactions(Data::AllowedReactions value) {\n\tif (_allowedReactions != value) {\n\t\tconst auto enabled = [](const Data::AllowedReactions &allowed) {\n\t\t\treturn (allowed.type != Data::AllowedReactionsType::Some)\n\t\t\t\t|| !allowed.some.empty()\n\t\t\t\t|| allowed.paidEnabled;\n\t\t};\n\t\tconst auto was = enabled(_allowedReactions);\n\t\t_allowedReactions = std::move(value);\n\t\tconst auto now = enabled(_allowedReactions);\n\t\tif (was != now) {\n\t\t\towner().reactions().updateAllInHistory(this, now);\n\t\t}\n\t\tsession().changes().peerUpdated(this, UpdateFlag::Reactions);\n\t}\n}\n\nconst Data::AllowedReactions &ChatData::allowedReactions() const {\n\treturn _allowedReactions;\n}\n\nMTPlong ChatData::inputChat() const {\n\treturn MTP_long(peerToChat(id).bare);\n}\n\nnamespace Data {\n\nvoid ApplyChatUpdate(\n\t\tnot_null<ChatData*> chat,\n\t\tconst MTPDupdateChatParticipants &update) {\n\tApplyChatUpdate(chat, update.vparticipants());\n}\n\nvoid ApplyChatUpdate(\n\t\tnot_null<ChatData*> chat,\n\t\tconst MTPDupdateChatParticipantAdd &update) {\n\tif (chat->applyUpdateVersion(update.vversion().v)\n\t\t!= ChatData::UpdateStatus::Good) {\n\t\treturn;\n\t} else if (chat->count < 0) {\n\t\treturn;\n\t}\n\tconst auto user = chat->owner().userLoaded(update.vuser_id().v);\n\tconst auto session = &chat->session();\n\tif (!user\n\t\t|| (!chat->participants.empty()\n\t\t\t&& chat->participants.contains(user))) {\n\t\tchat->invalidateParticipants();\n\t\t++chat->count;\n\t\treturn;\n\t}\n\tif (chat->participants.empty()) {\n\t\tif (chat->count > 0) { // If the count is known.\n\t\t\t++chat->count;\n\t\t}\n\t\tchat->botStatus = Data::BotStatus::Unknown;\n\t} else {\n\t\tchat->participants.emplace(user);\n\t\tif (UserId(update.vinviter_id()) == session->userId()) {\n\t\t\tchat->invitedByMe.insert(user);\n\t\t} else {\n\t\t\tchat->invitedByMe.remove(user);\n\t\t}\n\t\t++chat->count;\n\t\tif (user->isBot()) {\n\t\t\tchat->botStatus = Data::BotStatus::HasBots;\n\t\t\tif (!user->botInfo->inited) {\n\t\t\t\tsession->api().requestFullPeer(user);\n\t\t\t}\n\t\t}\n\t}\n\tsession->changes().peerUpdated(chat, UpdateFlag::Members);\n}\n\nvoid ApplyChatUpdate(\n\t\tnot_null<ChatData*> chat,\n\t\tconst MTPDupdateChatParticipantDelete &update) {\n\tif (chat->applyUpdateVersion(update.vversion().v)\n\t\t!= ChatData::UpdateStatus::Good) {\n\t\treturn;\n\t} else if (chat->count <= 0) {\n\t\treturn;\n\t}\n\tconst auto user = chat->owner().userLoaded(update.vuser_id().v);\n\tif (!user\n\t\t|| (!chat->participants.empty()\n\t\t\t&& !chat->participants.contains(user))) {\n\t\tchat->invalidateParticipants();\n\t\t--chat->count;\n\t\treturn;\n\t}\n\tif (chat->participants.empty()) {\n\t\tif (chat->count > 0) {\n\t\t\tchat->count--;\n\t\t}\n\t\tchat->botStatus = Data::BotStatus::Unknown;\n\t} else {\n\t\tchat->participants.erase(user);\n\t\tchat->count--;\n\t\tchat->invitedByMe.remove(user);\n\t\tchat->admins.remove(user);\n\t\tif (user->isSelf()) {\n\t\t\tchat->setAdminRights(ChatAdminRights());\n\t\t}\n\t\tif (const auto history = chat->owner().historyLoaded(chat)) {\n\t\t\tif (history->lastKeyboardFrom == user->id) {\n\t\t\t\thistory->clearLastKeyboard();\n\t\t\t}\n\t\t}\n\t\tif (chat->botStatus == Data::BotStatus::HasBots && user->isBot()) {\n\t\t\tchat->refreshBotStatus();\n\t\t}\n\t}\n\tchat->session().changes().peerUpdated(chat, UpdateFlag::Members);\n}\n\nvoid ApplyChatUpdate(\n\t\tnot_null<ChatData*> chat,\n\t\tconst MTPDupdateChatParticipantAdmin &update) {\n\tif (chat->applyUpdateVersion(update.vversion().v)\n\t\t!= ChatData::UpdateStatus::Good) {\n\t\treturn;\n\t}\n\tconst auto session = &chat->session();\n\tconst auto user = chat->owner().userLoaded(update.vuser_id().v);\n\tif (!user) {\n\t\tchat->invalidateParticipants();\n\t\treturn;\n\t}\n\tif (user->isSelf()) {\n\t\tchat->setAdminRights(mtpIsTrue(update.vis_admin())\n\t\t\t? chat->defaultAdminRights(user).flags\n\t\t\t: ChatAdminRights());\n\t}\n\tif (mtpIsTrue(update.vis_admin())) {\n\t\tif (chat->noParticipantInfo()) {\n\t\t\tsession->api().requestFullPeer(chat);\n\t\t} else {\n\t\t\tchat->admins.emplace(user);\n\t\t}\n\t} else {\n\t\tchat->admins.erase(user);\n\t}\n\tsession->changes().peerUpdated(chat, UpdateFlag::Admins);\n}\n\nvoid ApplyChatUpdate(\n\t\tnot_null<ChatData*> chat,\n\t\tconst MTPDupdateChatParticipantRank &update) {\n\tif (chat->applyUpdateVersion(update.vversion().v)\n\t\t!= ChatData::UpdateStatus::Good) {\n\t\treturn;\n\t}\n\tconst auto rank = qs(update.vrank().v);\n\tconst auto userId = UserId(update.vuser_id().v);\n\tif (rank.isEmpty()) {\n\t\tchat->memberRanks.remove(userId);\n\t} else {\n\t\tchat->memberRanks[userId] = rank;\n\t}\n\tif (userId != chat->session().userId()) {\n\t\tif (const auto history = chat->owner().historyLoaded(chat)) {\n\t\t\tauto changes = base::flat_set<UserId>();\n\t\t\tchanges.emplace(userId);\n\t\t\thistory->applyGroupAdminChanges(changes);\n\t\t}\n\t}\n\tchat->session().changes().peerUpdated(\n\t\tchat,\n\t\tData::PeerUpdate::Flag::Members);\n}\n\nvoid ApplyChatUpdate(\n\t\tnot_null<ChatData*> chat,\n\t\tconst MTPDupdateChatDefaultBannedRights &update) {\n\tif (chat->applyUpdateVersion(update.vversion().v)\n\t\t!= ChatData::UpdateStatus::Good) {\n\t\treturn;\n\t}\n\tchat->setDefaultRestrictions(ChatRestrictionsInfo(\n\t\tupdate.vdefault_banned_rights()).flags);\n}\n\nvoid ApplyChatUpdate(not_null<ChatData*> chat, const MTPDchatFull &update) {\n\tApplyChatUpdate(chat, update.vparticipants());\n\n\tif (const auto call = update.vcall()) {\n\t\tchat->setGroupCall(*call);\n\t} else {\n\t\tchat->clearGroupCall();\n\t}\n\tif (const auto as = update.vgroupcall_default_join_as()) {\n\t\tchat->setGroupCallDefaultJoinAs(peerFromMTP(*as));\n\t} else {\n\t\tchat->setGroupCallDefaultJoinAs(0);\n\t}\n\n\tchat->setMessagesTTL(update.vttl_period().value_or_empty());\n\tif (const auto info = update.vbot_info()) {\n\t\tauto &&commands = ranges::views::all(\n\t\t\tinfo->v\n\t\t) | ranges::views::transform(Data::BotCommandsFromTL);\n\t\tchat->setBotCommands(std::move(commands) | ranges::to_vector);\n\t} else {\n\t\tchat->setBotCommands({});\n\t}\n\tusing Flag = ChatDataFlag;\n\tconst auto mask = Flag::CanSetUsername;\n\tchat->setFlags((chat->flags() & ~mask)\n\t\t| (update.is_can_set_username() ? Flag::CanSetUsername : Flag()));\n\tif (const auto photo = update.vchat_photo()) {\n\t\tchat->setUserpicPhoto(*photo);\n\t} else {\n\t\tchat->setUserpicPhoto(MTP_photoEmpty(MTP_long(0)));\n\t}\n\tif (const auto invite = update.vexported_invite()) {\n\t\tchat->session().api().inviteLinks().setMyPermanent(chat, *invite);\n\t} else {\n\t\tchat->session().api().inviteLinks().clearMyPermanent(chat);\n\t}\n\tif (const auto pinned = update.vpinned_msg_id()) {\n\t\tSetTopPinnedMessageId(chat, pinned->v);\n\t}\n\tchat->checkFolder(update.vfolder_id().value_or_empty());\n\tchat->setThemeToken(qs(update.vtheme_emoticon().value_or_empty()));\n\tchat->setTranslationDisabled(update.is_translations_disabled());\n\tconst auto reactionsLimit = update.vreactions_limit().value_or_empty();\n\tif (const auto allowed = update.vavailable_reactions()) {\n\t\tconst auto paidEnabled = false;\n\t\tauto parsed = Data::Parse(*allowed, reactionsLimit, paidEnabled);\n\t\tchat->setAllowedReactions(std::move(parsed));\n\t} else {\n\t\tchat->setAllowedReactions({ .maxCount = reactionsLimit });\n\t}\n\tchat->fullUpdated();\n\tchat->setAbout(qs(update.vabout()));\n\tchat->setPendingRequestsCount(\n\t\tupdate.vrequests_pending().value_or_empty(),\n\t\tupdate.vrecent_requesters().value_or_empty());\n\n\tchat->owner().notifySettings().apply(chat, update.vnotify_settings());\n}\n\nvoid ApplyChatUpdate(\n\t\tnot_null<ChatData*> chat,\n\t\tconst MTPChatParticipants &participants) {\n\tconst auto session = &chat->session();\n\tparticipants.match([&](const MTPDchatParticipantsForbidden &data) {\n\t\tif (const auto self = data.vself_participant()) {\n\t\t\t// self->\n\t\t}\n\t\tchat->count = -1;\n\t\tchat->invalidateParticipants();\n\t}, [&](const MTPDchatParticipants &data) {\n\t\tconst auto status = chat->applyUpdateVersion(data.vversion().v);\n\t\tif (status == ChatData::UpdateStatus::TooOld) {\n\t\t\treturn;\n\t\t}\n\t\t// Even if we skipped some updates, we got current participants\n\t\t// and we've requested peer from API to have current rights.\n\t\tchat->setVersion(data.vversion().v);\n\n\t\tconst auto &list = data.vparticipants().v;\n\t\tchat->count = list.size();\n\t\tchat->participants.clear();\n\t\tchat->invitedByMe.clear();\n\t\tchat->admins.clear();\n\t\tchat->memberRanks.clear();\n\t\tchat->setAdminRights(ChatAdminRights());\n\t\tconst auto selfUserId = session->userId();\n\t\tfor (const auto &participant : list) {\n\t\t\tconst auto userId = participant.match([&](const auto &data) {\n\t\t\t\treturn data.vuser_id().v;\n\t\t\t});\n\t\t\tconst auto user = chat->owner().userLoaded(userId);\n\t\t\tif (!user) {\n\t\t\t\tchat->invalidateParticipants();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tchat->participants.emplace(user);\n\n\t\t\tconst auto inviterId = participant.match([&](\n\t\t\t\t\tconst MTPDchatParticipantCreator &data) {\n\t\t\t\treturn UserId(0);\n\t\t\t}, [&](const auto &data) {\n\t\t\t\treturn UserId(data.vinviter_id());\n\t\t\t});\n\t\t\tif (inviterId == selfUserId) {\n\t\t\t\tchat->invitedByMe.insert(user);\n\t\t\t}\n\n\t\t\tparticipant.match([&](const MTPDchatParticipantCreator &data) {\n\t\t\t\tchat->creator = userId;\n\t\t\t\tconst auto rank = qs(data.vrank().value_or_empty());\n\t\t\t\tif (!rank.isEmpty()) {\n\t\t\t\t\tchat->memberRanks[userId] = rank;\n\t\t\t\t}\n\t\t\t}, [&](const MTPDchatParticipantAdmin &data) {\n\t\t\t\tchat->admins.emplace(user);\n\t\t\t\tif (user->isSelf()) {\n\t\t\t\t\tchat->setAdminRights(\n\t\t\t\t\t\tchat->defaultAdminRights(user).flags);\n\t\t\t\t}\n\t\t\t\tconst auto rank = qs(data.vrank().value_or_empty());\n\t\t\t\tif (!rank.isEmpty()) {\n\t\t\t\t\tchat->memberRanks[userId] = rank;\n\t\t\t\t}\n\t\t\t}, [&](const MTPDchatParticipant &data) {\n\t\t\t\tconst auto rank = qs(data.vrank().value_or_empty());\n\t\t\t\tif (!rank.isEmpty()) {\n\t\t\t\t\tchat->memberRanks[userId] = rank;\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\tif (chat->participants.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tif (const auto history = chat->owner().historyLoaded(chat)) {\n\t\t\tif (history->lastKeyboardFrom) {\n\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\tchat->participants,\n\t\t\t\t\thistory->lastKeyboardFrom,\n\t\t\t\t\t&UserData::id);\n\t\t\t\tif (i == end(chat->participants)) {\n\t\t\t\t\thistory->clearLastKeyboard();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tchat->refreshBotStatus();\n\t\tsession->changes().peerUpdated(\n\t\t\tchat,\n\t\t\tUpdateFlag::Members | UpdateFlag::Admins);\n\t});\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_chat.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_peer.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"data/data_peer_bot_commands.h\"\n\nenum class ChatAdminRight;\n\nenum class ChatDataFlag {\n\tLeft = (1 << 0),\n\t//Kicked = (1 << 1),\n\tCreator = (1 << 2),\n\tDeactivated = (1 << 3),\n\tForbidden = (1 << 4),\n\tCallActive = (1 << 5),\n\tCallNotEmpty = (1 << 6),\n\tCanSetUsername = (1 << 7),\n\tNoForwards = (1 << 8),\n};\ninline constexpr bool is_flag_type(ChatDataFlag) { return true; };\nusing ChatDataFlags = base::flags<ChatDataFlag>;\n\nclass ChatData final : public PeerData {\npublic:\n\tusing Flag = ChatDataFlag;\n\tusing Flags = Data::Flags<ChatDataFlags>;\n\n\tChatData(not_null<Data::Session*> owner, PeerId id);\n\n\tvoid setName(const QString &newName);\n\tvoid setPhoto(const MTPChatPhoto &photo);\n\n\tvoid invalidateParticipants();\n\t[[nodiscard]] bool noParticipantInfo() const {\n\t\treturn (count > 0 || amIn()) && participants.empty();\n\t}\n\n\tvoid setFlags(ChatDataFlags which);\n\tvoid addFlags(ChatDataFlags which) {\n\t\t_flags.add(which);\n\t}\n\tvoid removeFlags(ChatDataFlags which) {\n\t\t_flags.remove(which);\n\t}\n\t[[nodiscard]] auto flags() const {\n\t\treturn _flags.current();\n\t}\n\t[[nodiscard]] auto flagsValue() const {\n\t\treturn _flags.value();\n\t}\n\n\t[[nodiscard]] auto adminRights() const {\n\t\treturn _adminRights.current();\n\t}\n\t[[nodiscard]] auto adminRightsValue() const {\n\t\treturn _adminRights.value();\n\t}\n\tvoid setAdminRights(ChatAdminRights rights);\n\t[[nodiscard]] bool hasAdminRights() const {\n\t\treturn (adminRights() != 0);\n\t}\n\n\t[[nodiscard]] auto defaultRestrictions() const {\n\t\treturn _defaultRestrictions.current();\n\t}\n\t[[nodiscard]] auto defaultRestrictionsValue() const {\n\t\treturn _defaultRestrictions.value();\n\t}\n\tvoid setDefaultRestrictions(ChatRestrictions rights);\n\n\t[[nodiscard]] bool isForbidden() const {\n\t\treturn flags() & Flag::Forbidden;\n\t}\n\t[[nodiscard]] bool amIn() const {\n\t\treturn !isForbidden() && !isDeactivated() && !haveLeft();\n\t}\n\t[[nodiscard]] bool haveLeft() const {\n\t\treturn flags() & ChatDataFlag::Left;\n\t}\n\t[[nodiscard]] bool amCreator() const {\n\t\treturn flags() & ChatDataFlag::Creator;\n\t}\n\t[[nodiscard]] bool isDeactivated() const {\n\t\treturn flags() & ChatDataFlag::Deactivated;\n\t}\n\t[[nodiscard]] bool isMigrated() const {\n\t\treturn (_migratedTo != nullptr);\n\t}\n\n\t[[nodiscard]] ChatAdminRightsInfo defaultAdminRights(\n\t\tnot_null<UserData*> user);\n\n\t// Like in ChannelData.\n\t[[nodiscard]] bool allowsForwarding() const;\n\t[[nodiscard]] bool canEditInformation() const;\n\t[[nodiscard]] bool canEditPermissions() const;\n\t[[nodiscard]] bool canEditUsername() const;\n\t[[nodiscard]] bool canEditPreHistoryHidden() const;\n\t[[nodiscard]] bool canDeleteMessages() const;\n\t[[nodiscard]] bool canAddMembers() const;\n\t[[nodiscard]] bool canAddAdmins() const;\n\t[[nodiscard]] bool canBanMembers() const;\n\t[[nodiscard]] bool anyoneCanAddMembers() const;\n\n\tvoid applyEditAdmin(not_null<UserData*> user, bool isAdmin);\n\n\tvoid setInviteLink(const QString &newInviteLink);\n\t[[nodiscard]] QString inviteLink() const {\n\t\treturn _inviteLink;\n\t}\n\t[[nodiscard]] bool canHaveInviteLink() const;\n\tvoid refreshBotStatus();\n\n\tenum class UpdateStatus {\n\t\tGood,\n\t\tTooOld,\n\t\tSkipped,\n\t};\n\tint version() const {\n\t\treturn _version;\n\t}\n\tvoid setVersion(int version) {\n\t\t_version = version;\n\t}\n\tUpdateStatus applyUpdateVersion(int version);\n\n\tChannelData *getMigrateToChannel() const;\n\tvoid setMigrateToChannel(ChannelData *channel);\n\n\t[[nodiscard]] Data::GroupCall *groupCall() const {\n\t\treturn _call.get();\n\t}\n\tvoid setGroupCall(\n\t\tconst MTPInputGroupCall &call,\n\t\tTimeId scheduleDate = 0,\n\t\tbool rtmp = false);\n\tvoid clearGroupCall();\n\tvoid setGroupCallDefaultJoinAs(PeerId peerId);\n\t[[nodiscard]] PeerId groupCallDefaultJoinAs() const;\n\n\tvoid setBotCommands(const std::vector<Data::BotCommands> &commands);\n\t[[nodiscard]] const Data::ChatBotCommands &botCommands() const {\n\t\treturn _botCommands;\n\t}\n\n\t[[nodiscard]] int pendingRequestsCount() const {\n\t\treturn _pendingRequestsCount;\n\t}\n\t[[nodiscard]] const std::vector<UserId> &recentRequesters() const {\n\t\treturn _recentRequesters;\n\t}\n\tvoid setPendingRequestsCount(\n\t\tint count,\n\t\tconst QVector<MTPlong> &recentRequesters);\n\tvoid setPendingRequestsCount(\n\t\tint count,\n\t\tstd::vector<UserId> recentRequesters);\n\n\tvoid setAllowedReactions(Data::AllowedReactions value);\n\t[[nodiscard]] const Data::AllowedReactions &allowedReactions() const;\n\n\t[[nodiscard]] MTPlong inputChat() const;\n\n\t// Still public data members.\n\tint count = 0;\n\tTimeId date = 0;\n\tUserId creator = 0;\n\n\tbase::flat_set<not_null<UserData*>> participants;\n\tbase::flat_set<not_null<UserData*>> invitedByMe;\n\tbase::flat_set<not_null<UserData*>> admins;\n\tstd::deque<not_null<UserData*>> lastAuthors;\n\tbase::flat_set<not_null<PeerData*>> markupSenders;\n\tbase::flat_map<UserId, QString> memberRanks;\n\tData::BotStatus botStatus = Data::BotStatus::Unknown;\n\nprivate:\n\tFlags _flags;\n\tQString _inviteLink;\n\n\tData::Flags<ChatRestrictions> _defaultRestrictions;\n\tData::Flags<ChatAdminRights> _adminRights;\n\tint _version = 0;\n\tint _pendingRequestsCount = 0;\n\tstd::vector<UserId> _recentRequesters;\n\n\tData::AllowedReactions _allowedReactions;\n\n\tstd::unique_ptr<Data::GroupCall> _call;\n\tPeerId _callDefaultJoinAs = 0;\n\tData::ChatBotCommands _botCommands;\n\n\tChannelData *_migratedTo = nullptr;\n\trpl::lifetime _lifetime;\n\n};\n\nnamespace Data {\n\nvoid ApplyChatUpdate(\n\tnot_null<ChatData*> chat,\n\tconst MTPDupdateChatParticipants &update);\nvoid ApplyChatUpdate(\n\tnot_null<ChatData*> chat,\n\tconst MTPDupdateChatParticipantAdd &update);\nvoid ApplyChatUpdate(\n\tnot_null<ChatData*> chat,\n\tconst MTPDupdateChatParticipantDelete &update);\nvoid ApplyChatUpdate(\n\tnot_null<ChatData*> chat,\n\tconst MTPDupdateChatParticipantAdmin &update);\nvoid ApplyChatUpdate(\n\tnot_null<ChatData*> chat,\n\tconst MTPDupdateChatParticipantRank &update);\nvoid ApplyChatUpdate(\n\tnot_null<ChatData*> chat,\n\tconst MTPDupdateChatDefaultBannedRights &update);\nvoid ApplyChatUpdate(\n\tnot_null<ChatData*> chat,\n\tconst MTPDchatFull &update);\nvoid ApplyChatUpdate(\n\tnot_null<ChatData*> chat,\n\tconst MTPChatParticipants &update);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_chat_filters.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_chat_filters.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"history/history.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_histories.h\"\n#include \"dialogs/dialogs_main_list.h\"\n#include \"history/history.h\"\n#include \"history/history_unread_things.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/chat/more_chats_bar.h\"\n#include \"main/main_session.h\"\n#include \"main/main_app_config.h\"\n#include \"apiwrap.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kRefreshSuggestedTimeout = 7200 * crl::time(1000);\nconstexpr auto kLoadExceptionsAfter = 100;\nconstexpr auto kLoadExceptionsPerRequest = 100;\n\n[[nodiscard]] crl::time RequestUpdatesEach(not_null<Session*> owner) {\n\tconst auto appConfig = &owner->session().appConfig();\n\treturn appConfig->get<int>(u\"chatlist_update_period\"_q, 3600)\n\t\t* crl::time(1000);\n}\n\n} // namespace\n\nTextWithEntities ForceCustomEmojiStatic(TextWithEntities text) {\n\tfor (auto &entity : text.entities) {\n\t\tif (entity.type() == EntityType::CustomEmoji) {\n\t\t\tentity = EntityInText(\n\t\t\t\tEntityType::CustomEmoji,\n\t\t\t\tentity.offset(),\n\t\t\t\tentity.length(),\n\t\t\t\tu\"force-static:\"_q + entity.data());\n\t\t}\n\t}\n\treturn text;\n}\n\nChatFilter::ChatFilter(\n\tFilterId id,\n\tChatFilterTitle title,\n\tQString iconEmoji,\n\tstd::optional<uint8> colorIndex,\n\tFlags flags,\n\tbase::flat_set<not_null<History*>> always,\n\tstd::vector<not_null<History*>> pinned,\n\tbase::flat_set<not_null<History*>> never)\n: _id(id)\n, _title(std::move(title.text))\n, _iconEmoji(std::move(iconEmoji))\n, _colorIndex(colorIndex)\n, _always(std::move(always))\n, _pinned(std::move(pinned))\n, _never(std::move(never))\n, _flags(title.isStatic\n\t? (flags | Flag::StaticTitle)\n\t: (flags & ~Flag::StaticTitle)) {\n}\n\nChatFilter ChatFilter::FromTL(\n\t\tconst MTPDialogFilter &data,\n\t\tnot_null<Session*> owner) {\n\treturn data.match([&](const MTPDdialogFilter &data) {\n\t\tconst auto flags = (data.is_contacts() ? Flag::Contacts : Flag(0))\n\t\t\t| (data.is_non_contacts() ? Flag::NonContacts : Flag(0))\n\t\t\t| (data.is_groups() ? Flag::Groups : Flag(0))\n\t\t\t| (data.is_broadcasts() ? Flag::Channels : Flag(0))\n\t\t\t| (data.is_bots() ? Flag::Bots : Flag(0))\n\t\t\t| (data.is_exclude_muted() ? Flag::NoMuted : Flag(0))\n\t\t\t| (data.is_exclude_read() ? Flag::NoRead : Flag(0))\n\t\t\t| (data.is_exclude_archived() ? Flag::NoArchived : Flag(0))\n\t\t\t| (data.is_title_noanimate() ? Flag::StaticTitle : Flag(0));\n\t\tauto &&to_histories = ranges::views::transform([&](\n\t\t\t\tconst MTPInputPeer &input) {\n\t\t\tconst auto peer = Data::PeerFromInputMTP(owner, input);\n\t\t\treturn peer ? owner->history(peer).get() : nullptr;\n\t\t}) | ranges::views::filter([](History *history) {\n\t\t\treturn history != nullptr;\n\t\t}) | ranges::views::transform([](History *history) {\n\t\t\treturn not_null<History*>(history);\n\t\t});\n\t\tauto &&always = ranges::views::concat(\n\t\t\tdata.vinclude_peers().v\n\t\t) | to_histories;\n\t\tauto pinned = ranges::views::all(\n\t\t\tdata.vpinned_peers().v\n\t\t) | to_histories | ranges::to_vector;\n\t\tauto &&never = ranges::views::all(\n\t\t\tdata.vexclude_peers().v\n\t\t) | to_histories;\n\t\tauto &&all = ranges::views::concat(always, pinned);\n\t\tauto list = base::flat_set<not_null<History*>>{\n\t\t\tall.begin(),\n\t\t\tall.end()\n\t\t};\n\t\treturn ChatFilter(\n\t\t\tdata.vid().v,\n\t\t\t{\n\t\t\t\tApi::ParseTextWithEntities(&owner->session(), data.vtitle()),\n\t\t\t\tdata.is_title_noanimate(),\n\t\t\t},\n\t\t\tqs(data.vemoticon().value_or_empty()),\n\t\t\tdata.vcolor()\n\t\t\t\t? std::make_optional(data.vcolor()->v)\n\t\t\t\t: std::nullopt,\n\t\t\tflags,\n\t\t\tstd::move(list),\n\t\t\tstd::move(pinned),\n\t\t\t{ never.begin(), never.end() });\n\t}, [](const MTPDdialogFilterDefault &) {\n\t\treturn ChatFilter();\n\t}, [&](const MTPDdialogFilterChatlist &data) {\n\t\tauto &&to_histories = ranges::views::transform([&](\n\t\t\t\tconst MTPInputPeer &data) {\n\t\t\tconst auto peer = data.match([&](const MTPDinputPeerUser &data) {\n\t\t\t\tconst auto user = owner->user(data.vuser_id().v);\n\t\t\t\tuser->setAccessHash(data.vaccess_hash().v);\n\t\t\t\treturn (PeerData*)user;\n\t\t\t}, [&](const MTPDinputPeerChat &data) {\n\t\t\t\treturn (PeerData*)owner->chat(data.vchat_id().v);\n\t\t\t}, [&](const MTPDinputPeerChannel &data) {\n\t\t\t\tconst auto channel = owner->channel(data.vchannel_id().v);\n\t\t\t\tchannel->setAccessHash(data.vaccess_hash().v);\n\t\t\t\treturn (PeerData*)channel;\n\t\t\t}, [&](const MTPDinputPeerSelf &data) {\n\t\t\t\treturn (PeerData*)owner->session().user();\n\t\t\t}, [&](const auto &data) {\n\t\t\t\treturn (PeerData*)nullptr;\n\t\t\t});\n\t\t\treturn peer ? owner->history(peer).get() : nullptr;\n\t\t}) | ranges::views::filter([](History *history) {\n\t\t\treturn history != nullptr;\n\t\t}) | ranges::views::transform([](History *history) {\n\t\t\treturn not_null<History*>(history);\n\t\t});\n\t\tauto &&always = ranges::views::concat(\n\t\t\tdata.vinclude_peers().v\n\t\t) | to_histories;\n\t\tauto pinned = ranges::views::all(\n\t\t\tdata.vpinned_peers().v\n\t\t) | to_histories | ranges::to_vector;\n\t\tauto &&all = ranges::views::concat(always, pinned);\n\t\tauto list = base::flat_set<not_null<History*>>{\n\t\t\tall.begin(),\n\t\t\tall.end()\n\t\t};\n\t\treturn ChatFilter(\n\t\t\tdata.vid().v,\n\t\t\t{\n\t\t\t\tApi::ParseTextWithEntities(&owner->session(), data.vtitle()),\n\t\t\t\tdata.is_title_noanimate(),\n\t\t\t},\n\t\t\tqs(data.vemoticon().value_or_empty()),\n\t\t\tdata.vcolor()\n\t\t\t\t? std::make_optional(data.vcolor()->v)\n\t\t\t\t: std::nullopt,\n\t\t\t(Flag::Chatlist\n\t\t\t\t| (data.is_has_my_invites() ? Flag::HasMyLinks : Flag())\n\t\t\t\t| (data.is_title_noanimate() ? Flag::StaticTitle : Flag(0))),\n\t\t\tstd::move(list),\n\t\t\tstd::move(pinned),\n\t\t\t{});\n\t});\n}\n\nChatFilter ChatFilter::withId(FilterId id) const {\n\tauto result = *this;\n\tresult._id = id;\n\treturn result;\n}\n\nChatFilter ChatFilter::withTitle(ChatFilterTitle title) const {\n\tauto result = *this;\n\tresult._title = std::move(title.text);\n\tif (title.isStatic) {\n\t\tresult._flags |= Flag::StaticTitle;\n\t} else {\n\t\tresult._flags &= ~Flag::StaticTitle;\n\t}\n\treturn result;\n}\n\nChatFilter ChatFilter::withColorIndex(std::optional<uint8> c) const {\n\tauto result = *this;\n\tresult._colorIndex = c;\n\treturn result;\n}\n\nChatFilter ChatFilter::withChatlist(bool chatlist, bool hasMyLinks) const {\n\tauto result = *this;\n\tresult._flags &= Flag::RulesMask;\n\tif (chatlist) {\n\t\tresult._flags |= Flag::Chatlist;\n\t\tif (hasMyLinks) {\n\t\t\tresult._flags |= Flag::HasMyLinks;\n\t\t} else {\n\t\t\tresult._flags &= ~Flag::HasMyLinks;\n\t\t}\n\t}\n\treturn result;\n}\n\nChatFilter ChatFilter::withoutAlways(not_null<History*> history) const {\n\tauto result = *this;\n\tif (CanRemoveFromChatFilter(result, history)) {\n\t\tresult._always.remove(history);\n\t}\n\treturn result;\n}\n\nMTPDialogFilter ChatFilter::tl(FilterId replaceId) const {\n\tauto always = _always;\n\tauto pinned = QVector<MTPInputPeer>();\n\tpinned.reserve(_pinned.size());\n\tfor (const auto &history : _pinned) {\n\t\tpinned.push_back(history->peer->input());\n\t\talways.remove(history);\n\t}\n\tauto include = QVector<MTPInputPeer>();\n\tinclude.reserve(always.size());\n\tfor (const auto &history : always) {\n\t\tinclude.push_back(history->peer->input());\n\t}\n\tauto title = MTP_textWithEntities(\n\t\tMTP_string(_title.text),\n\t\tApi::EntitiesToMTP(\n\t\t\tnullptr,\n\t\t\t_title.entities,\n\t\t\tApi::ConvertOption::SkipLocal));\n\tif (_flags & Flag::Chatlist) {\n\t\tusing TLFlag = MTPDdialogFilterChatlist::Flag;\n\t\tconst auto flags = TLFlag::f_emoticon\n\t\t\t| (_colorIndex ? TLFlag::f_color : TLFlag(0))\n\t\t\t| (staticTitle() ? TLFlag::f_title_noanimate : TLFlag(0));\n\t\treturn MTP_dialogFilterChatlist(\n\t\t\tMTP_flags(flags),\n\t\t\tMTP_int(replaceId ? replaceId : _id),\n\t\t\tstd::move(title),\n\t\t\tMTP_string(_iconEmoji),\n\t\t\tMTP_int(_colorIndex.value_or(0)),\n\t\t\tMTP_vector<MTPInputPeer>(pinned),\n\t\t\tMTP_vector<MTPInputPeer>(include));\n\t}\n\tusing TLFlag = MTPDdialogFilter::Flag;\n\tconst auto flags = TLFlag::f_emoticon\n\t\t| (_colorIndex ? TLFlag::f_color : TLFlag(0))\n\t\t| (staticTitle() ? TLFlag::f_title_noanimate : TLFlag(0))\n\t\t| ((_flags & Flag::Contacts) ? TLFlag::f_contacts : TLFlag(0))\n\t\t| ((_flags & Flag::NonContacts) ? TLFlag::f_non_contacts : TLFlag(0))\n\t\t| ((_flags & Flag::Groups) ? TLFlag::f_groups : TLFlag(0))\n\t\t| ((_flags & Flag::Channels) ? TLFlag::f_broadcasts : TLFlag(0))\n\t\t| ((_flags & Flag::Bots) ? TLFlag::f_bots : TLFlag(0))\n\t\t| ((_flags & Flag::NoMuted) ? TLFlag::f_exclude_muted : TLFlag(0))\n\t\t| ((_flags & Flag::NoRead) ? TLFlag::f_exclude_read : TLFlag(0))\n\t\t| ((_flags & Flag::NoArchived)\n\t\t\t? TLFlag::f_exclude_archived\n\t\t\t: TLFlag(0));\n\tauto never = QVector<MTPInputPeer>();\n\tnever.reserve(_never.size());\n\tfor (const auto &history : _never) {\n\t\tnever.push_back(history->peer->input());\n\t}\n\treturn MTP_dialogFilter(\n\t\tMTP_flags(flags),\n\t\tMTP_int(replaceId ? replaceId : _id),\n\t\tstd::move(title),\n\t\tMTP_string(_iconEmoji),\n\t\tMTP_int(_colorIndex.value_or(0)),\n\t\tMTP_vector<MTPInputPeer>(pinned),\n\t\tMTP_vector<MTPInputPeer>(include),\n\t\tMTP_vector<MTPInputPeer>(never));\n}\n\nFilterId ChatFilter::id() const {\n\treturn _id;\n}\n\nconst TextWithEntities &ChatFilter::titleText() const {\n\treturn _title;\n}\n\nChatFilterTitle ChatFilter::title() const {\n\treturn { _title, !!(_flags & Flag::StaticTitle) };\n}\n\nQString ChatFilter::iconEmoji() const {\n\treturn _iconEmoji;\n}\n\nstd::optional<uint8> ChatFilter::colorIndex() const {\n\treturn _colorIndex;\n}\n\nChatFilter::Flags ChatFilter::flags() const {\n\treturn _flags;\n}\n\nbool ChatFilter::staticTitle() const {\n\treturn _flags & Flag::StaticTitle;\n}\n\nbool ChatFilter::chatlist() const {\n\treturn _flags & Flag::Chatlist;\n}\n\nbool ChatFilter::hasMyLinks() const {\n\treturn _flags & Flag::HasMyLinks;\n}\n\nconst base::flat_set<not_null<History*>> &ChatFilter::always() const {\n\treturn _always;\n}\n\nconst std::vector<not_null<History*>> &ChatFilter::pinned() const {\n\treturn _pinned;\n}\n\nconst base::flat_set<not_null<History*>> &ChatFilter::never() const {\n\treturn _never;\n}\n\nbool ChatFilter::contains(\n\t\tnot_null<History*> history,\n\t\tbool ignoreFakeUnread) const {\n\tconst auto flag = [&] {\n\t\tconst auto peer = history->peer;\n\t\tif (const auto user = peer->asUser()) {\n\t\t\treturn user->isBot()\n\t\t\t\t? Flag::Bots\n\t\t\t\t: user->isContact()\n\t\t\t\t? Flag::Contacts\n\t\t\t\t: Flag::NonContacts;\n\t\t} else if (peer->isChat()) {\n\t\t\treturn Flag::Groups;\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\tif (channel->isBroadcast()) {\n\t\t\t\treturn Flag::Channels;\n\t\t\t} else {\n\t\t\t\treturn Flag::Groups;\n\t\t\t}\n\t\t} else {\n\t\t\tUnexpected(\"Peer type in ChatFilter::contains.\");\n\t\t}\n\t}();\n\tif (_never.contains(history)) {\n\t\treturn false;\n\t}\n\tconst auto state = (_flags & (Flag::NoMuted | Flag::NoRead))\n\t\t? history->chatListBadgesState()\n\t\t: Dialogs::BadgesState();\n\treturn false\n\t\t|| ((_flags & flag)\n\t\t\t&& (!(_flags & Flag::NoMuted)\n\t\t\t\t|| !history->muted()\n\t\t\t\t|| (state.mention\n\t\t\t\t\t&& history->folderKnown()\n\t\t\t\t\t&& !history->folder()))\n\t\t\t&& (!(_flags & Flag::NoRead)\n\t\t\t\t|| state.unread\n\t\t\t\t|| state.mention\n\t\t\t\t|| (!ignoreFakeUnread && history->fakeUnreadWhileOpened()))\n\t\t\t&& (!(_flags & Flag::NoArchived)\n\t\t\t\t|| (history->folderKnown() && !history->folder())))\n\t\t|| _always.contains(history);\n}\n\nChatFilters::ChatFilters(not_null<Session*> owner)\n: _owner(owner)\n, _moreChatsTimer([=] { checkLoadMoreChatsLists(); }) {\n\t_list.emplace_back();\n\tcrl::on_main(&owner->session(), [=] { load(); });\n}\n\nChatFilters::~ChatFilters() = default;\n\nnot_null<Dialogs::MainList*> ChatFilters::chatsList(FilterId filterId) {\n\tauto &pointer = _chatsLists[filterId];\n\tif (!pointer) {\n\t\tauto limit = rpl::single(rpl::empty_value()) | rpl::then(\n\t\t\t_owner->session().appConfig().refreshed()\n\t\t) | rpl::map([=] {\n\t\t\treturn _owner->pinnedChatsLimit(filterId);\n\t\t});\n\t\tpointer = std::make_unique<Dialogs::MainList>(\n\t\t\t&_owner->session(),\n\t\t\tfilterId,\n\t\t\t_owner->maxPinnedChatsLimitValue(filterId));\n\t}\n\treturn pointer.get();\n}\n\nvoid ChatFilters::clear() {\n\t_chatsLists.clear();\n\t_list.clear();\n}\n\nvoid ChatFilters::setPreloaded(\n\t\tconst QVector<MTPDialogFilter> &result,\n\t\tbool tagsEnabled) {\n\t_loadRequestId = -1;\n\t_tagsEnabled = tagsEnabled;\n\treceived(result);\n\tcrl::on_main(&_owner->session(), [=] {\n\t\tif (_loadRequestId == -1) {\n\t\t\t_loadRequestId = 0;\n\t\t}\n\t});\n}\n\nvoid ChatFilters::load() {\n\tload(false);\n}\n\nvoid ChatFilters::reload() {\n\t_reloading = true;\n\tload();\n}\n\nvoid ChatFilters::load(bool force) {\n\tif (_loadRequestId && !force) {\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\tapi.request(_loadRequestId).cancel();\n\t_loadRequestId = api.request(MTPmessages_GetDialogFilters(\n\t)).done([=](const MTPmessages_DialogFilters &result) {\n\t\t_tagsEnabled = result.data().is_tags_enabled();\n\t\treceived(result.data().vfilters().v);\n\t\t_loadRequestId = 0;\n\t}).fail([=] {\n\t\t_loadRequestId = 0;\n\t\tif (_reloading) {\n\t\t\t_reloading = false;\n\t\t\t_listChanged.fire({});\n\t\t}\n\t}).send();\n}\n\nbool ChatFilters::tagsEnabled() const {\n\treturn _tagsEnabled.current();\n}\n\nrpl::producer<bool> ChatFilters::tagsEnabledValue() const {\n\treturn _tagsEnabled.value();\n}\n\nrpl::producer<bool> ChatFilters::tagsEnabledChanges() const {\n\treturn _tagsEnabled.changes();\n}\n\nvoid ChatFilters::requestToggleTags(bool value, Fn<void()> fail) {\n\tif (_toggleTagsRequestId) {\n\t\treturn;\n\t}\n\t_toggleTagsRequestId = _owner->session().api().request(\n\t\tMTPmessages_ToggleDialogFilterTags(MTP_bool(value))\n\t).done([=](const MTPBool &result) {\n\t\t_tagsEnabled = value;\n\t\t_toggleTagsRequestId = 0;\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto message = error.type();\n\t\t_toggleTagsRequestId = 0;\n\t\tLOG((\"API Error: Toggle Tags - %1\").arg(message));\n\t\tfail();\n\t}).send();\n}\n\nvoid ChatFilters::received(const QVector<MTPDialogFilter> &list) {\n\tauto position = 0;\n\tauto changed = false;\n\tfor (const auto &filter : list) {\n\t\tauto parsed = ChatFilter::FromTL(filter, _owner);\n\t\tconst auto b = begin(_list) + position;\n\t\tconst auto e = end(_list);\n\t\tconst auto i = ranges::find(b, e, parsed.id(), &ChatFilter::id);\n\t\tif (i == e) {\n\t\t\tapplyInsert(std::move(parsed), position);\n\t\t\tchanged = true;\n\t\t} else if (i == b) {\n\t\t\tif (applyChange(*b, std::move(parsed))) {\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t} else {\n\t\t\tstd::swap(*i, *b);\n\t\t\tapplyChange(*b, std::move(parsed));\n\t\t\tchanged = true;\n\t\t}\n\t\t++position;\n\t}\n\twhile (position < _list.size()) {\n\t\tapplyRemove(position);\n\t\tchanged = true;\n\t}\n\tif (!ranges::contains(begin(_list), end(_list), 0, &ChatFilter::id)) {\n\t\t_list.insert(begin(_list), ChatFilter());\n\t}\n\tif (changed || !_loaded || _reloading) {\n\t\t_loaded = true;\n\t\t_reloading = false;\n\t\t_listChanged.fire({});\n\t}\n}\n\nvoid ChatFilters::apply(const MTPUpdate &update) {\n\tupdate.match([&](const MTPDupdateDialogFilter &data) {\n\t\tif (const auto filter = data.vfilter()) {\n\t\t\tset(ChatFilter::FromTL(*filter, _owner));\n\t\t} else {\n\t\t\tremove(data.vid().v);\n\t\t}\n\t}, [&](const MTPDupdateDialogFilters &data) {\n\t\tload(true);\n\t}, [&](const MTPDupdateDialogFilterOrder &data) {\n\t\tif (applyOrder(data.vorder().v)) {\n\t\t\t_listChanged.fire({});\n\t\t} else {\n\t\t\tload(true);\n\t\t}\n\t}, [](auto&&) {\n\t\tUnexpected(\"Update in ChatFilters::apply.\");\n\t});\n}\n\nChatFilterLink ChatFilters::add(\n\t\tFilterId id,\n\t\tconst MTPExportedChatlistInvite &update) {\n\tconst auto i = ranges::find(_list, id, &ChatFilter::id);\n\tif (i == end(_list) || !i->chatlist()) {\n\t\tLOG((\"Api Error: \"\n\t\t\t\"Attempt to add chatlist link to a non-chatlist filter: %1\"\n\t\t\t).arg(id));\n\t\treturn {};\n\t}\n\tauto &links = _chatlistLinks[id];\n\tconst auto &data = update.data();\n\tconst auto url = qs(data.vurl());\n\tconst auto title = qs(data.vtitle());\n\tauto chats = data.vpeers().v | ranges::views::transform([&](\n\t\t\tconst MTPPeer &peer) {\n\t\treturn _owner->history(peerFromMTP(peer));\n\t}) | ranges::to_vector;\n\tconst auto j = ranges::find(links, url, &ChatFilterLink::url);\n\tif (j != end(links)) {\n\t\tif (j->title != title || j->chats != chats) {\n\t\t\tj->title = title;\n\t\t\tj->chats = std::move(chats);\n\t\t\t_chatlistLinksUpdated.fire_copy(id);\n\t\t}\n\t\treturn *j;\n\t}\n\tlinks.push_back({\n\t\t.id = id,\n\t\t.url = url,\n\t\t.title = title,\n\t\t.chats = std::move(chats),\n\t});\n\t_chatlistLinksUpdated.fire_copy(id);\n\treturn links.back();\n}\n\nvoid ChatFilters::edit(\n\t\tFilterId id,\n\t\tconst QString &url,\n\t\tconst QString &title) {\n\tauto &links = _chatlistLinks[id];\n\tconst auto i = ranges::find(links, url, &ChatFilterLink::url);\n\tif (i != end(links)) {\n\t\ti->title = title;\n\t\t_chatlistLinksUpdated.fire_copy(id);\n\n\t\t_owner->session().api().request(MTPchatlists_EditExportedInvite(\n\t\t\tMTP_flags(MTPchatlists_EditExportedInvite::Flag::f_title),\n\t\t\tMTP_inputChatlistDialogFilter(MTP_int(id)),\n\t\t\tMTP_string(url),\n\t\t\tMTP_string(title),\n\t\t\tMTPVector<MTPInputPeer>() // peers\n\t\t)).done([=](const MTPExportedChatlistInvite &result) {\n\t\t\t//const auto &data = result.data();\n\t\t\t//const auto link = _owner->chatsFilters().add(id, result);\n\t\t\t//done(link);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t//done({ .id = id });\n\t\t}).send();\n\t}\n}\n\nvoid ChatFilters::destroy(FilterId id, const QString &url) {\n\tauto &links = _chatlistLinks[id];\n\tconst auto i = ranges::find(links, url, &ChatFilterLink::url);\n\tif (i != end(links)) {\n\t\tlinks.erase(i);\n\t\t_chatlistLinksUpdated.fire_copy(id);\n\n\t\tconst auto api = &_owner->session().api();\n\t\tapi->request(_linksRequestId).cancel();\n\t\t_linksRequestId = api->request(MTPchatlists_DeleteExportedInvite(\n\t\t\tMTP_inputChatlistDialogFilter(MTP_int(id)),\n\t\t\tMTP_string(url)\n\t\t)).send();\n\t}\n}\n\nrpl::producer<std::vector<ChatFilterLink>> ChatFilters::chatlistLinks(\n\t\tFilterId id) const {\n\treturn _chatlistLinksUpdated.events_starting_with_copy(\n\t\tid\n\t) | rpl::filter(rpl::mappers::_1 == id) | rpl::map([=] {\n\t\tconst auto i = _chatlistLinks.find(id);\n\t\treturn (i != end(_chatlistLinks))\n\t\t\t? i->second\n\t\t\t: std::vector<ChatFilterLink>();\n\t});\n}\n\nvoid ChatFilters::reloadChatlistLinks(FilterId id) {\n\tconst auto api = &_owner->session().api();\n\tapi->request(_linksRequestId).cancel();\n\t_linksRequestId = api->request(MTPchatlists_GetExportedInvites(\n\t\tMTP_inputChatlistDialogFilter(MTP_int(id))\n\t)).done([=](const MTPchatlists_ExportedInvites &result) {\n\t\tconst auto &data = result.data();\n\t\t_owner->processUsers(data.vusers());\n\t\t_owner->processChats(data.vchats());\n\t\t_chatlistLinks[id].clear();\n\t\tfor (const auto &link : data.vinvites().v) {\n\t\t\tadd(id, link);\n\t\t}\n\t\t_chatlistLinksUpdated.fire_copy(id);\n\t}).send();\n}\n\nvoid ChatFilters::set(ChatFilter filter) {\n\tif (!filter.id()) {\n\t\treturn;\n\t}\n\tconst auto i = ranges::find(_list, filter.id(), &ChatFilter::id);\n\tif (i == end(_list)) {\n\t\tapplyInsert(std::move(filter), _list.size());\n\t\t_listChanged.fire({});\n\t} else if (applyChange(*i, std::move(filter))) {\n\t\t_listChanged.fire({});\n\t}\n}\n\nvoid ChatFilters::applyInsert(ChatFilter filter, int position) {\n\tExpects(position >= 0 && position <= _list.size());\n\n\t_list.insert(\n\t\tbegin(_list) + position,\n\t\tChatFilter(filter.id(), {}, {}, {}, {}, {}, {}, {}));\n\tapplyChange(*(begin(_list) + position), std::move(filter));\n}\n\nvoid ChatFilters::remove(FilterId id) {\n\tconst auto i = ranges::find(_list, id, &ChatFilter::id);\n\tif (i == end(_list)) {\n\t\treturn;\n\t}\n\tapplyRemove(i - begin(_list));\n\t_listChanged.fire({});\n}\n\nvoid ChatFilters::moveAllToFront() {\n\tconst auto i = ranges::find(_list, FilterId(), &ChatFilter::id);\n\tif (!_list.empty() && i == begin(_list)) {\n\t\treturn;\n\t} else if (i != end(_list)) {\n\t\t_list.erase(i);\n\t}\n\t_list.insert(begin(_list), ChatFilter());\n}\n\nvoid ChatFilters::applyRemove(int position) {\n\tExpects(position >= 0 && position < _list.size());\n\n\tconst auto i = begin(_list) + position;\n\tapplyChange(*i, ChatFilter(i->id(), {}, {}, {}, {}, {}, {}, {}));\n\t_list.erase(i);\n}\n\nbool ChatFilters::applyChange(ChatFilter &filter, ChatFilter &&updated) {\n\tExpects(filter.id() == updated.id());\n\n\tusing Flag = ChatFilter::Flag;\n\n\tconst auto id = filter.id();\n\tconst auto exceptionsChanged = filter.always() != updated.always();\n\tconst auto rulesMask = Flag() | Flag::RulesMask;\n\tconst auto rulesChanged = exceptionsChanged\n\t\t|| ((filter.flags() & rulesMask) != (updated.flags() & rulesMask))\n\t\t|| (filter.never() != updated.never());\n\tconst auto pinnedChanged = (filter.pinned() != updated.pinned());\n\tconst auto chatlistChanged = (filter.chatlist() != updated.chatlist())\n\t\t|| (filter.hasMyLinks() != updated.hasMyLinks());\n\tconst auto listUpdated = rulesChanged\n\t\t|| pinnedChanged\n\t\t|| (filter.titleText() != updated.titleText())\n\t\t|| (filter.staticTitle() != updated.staticTitle())\n\t\t|| (filter.iconEmoji() != updated.iconEmoji());\n\tconst auto colorChanged = filter.colorIndex() != updated.colorIndex();\n\tconst auto colorExistenceChanged = (!filter.colorIndex())\n\t\t!= (!updated.colorIndex());\n\tif (!listUpdated && !chatlistChanged && !colorChanged) {\n\t\treturn false;\n\t}\n\tconst auto wasFilter = std::move(filter);\n\tfilter = std::move(updated);\n\tauto entryToRefreshHeight = (Dialogs::Entry*)(nullptr);\n\tif (rulesChanged) {\n\t\tconst auto filterList = _owner->chatsFilters().chatsList(id);\n\t\tconst auto areTagsEnabled = tagsEnabled();\n\t\tconst auto tagsExistence = [&](not_null<Dialogs::Row*> row) {\n\t\t\treturn (!areTagsEnabled || entryToRefreshHeight)\n\t\t\t\t? false\n\t\t\t\t: row->entry()->hasChatsFilterTags(0);\n\t\t};\n\t\tconst auto feedHistory = [&](not_null<History*> history) {\n\t\t\tconst auto now = filter.contains(history);\n\t\t\tconst auto was = wasFilter.contains(history);\n\t\t\tif (now != was) {\n\t\t\t\tif (now) {\n\t\t\t\t\thistory->addToChatList(id, filterList);\n\t\t\t\t} else {\n\t\t\t\t\thistory->removeFromChatList(id, filterList);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tconst auto feedList = [&](not_null<const Dialogs::MainList*> list) {\n\t\t\tfor (const auto &entry : *list->indexed()) {\n\t\t\t\tif (const auto history = entry->history()) {\n\t\t\t\t\tconst auto wasTags = tagsExistence(entry);\n\t\t\t\t\tfeedHistory(history);\n\t\t\t\t\tif (wasTags != tagsExistence(entry)) {\n\t\t\t\t\t\tentryToRefreshHeight = entry->entry();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tfeedList(_owner->chatsList());\n\t\tif (const auto folder = _owner->folderLoaded(Data::Folder::kId)) {\n\t\t\tfeedList(folder->chatsList());\n\t\t}\n\t\tif (exceptionsChanged && !filter.always().empty()) {\n\t\t\t_exceptionsToLoad.push_back(id);\n\t\t\tUi::PostponeCall(&_owner->session(), [=] {\n\t\t\t\t_owner->session().api().requestMoreDialogsIfNeeded();\n\t\t\t});\n\t\t}\n\t}\n\tif (pinnedChanged) {\n\t\tconst auto filterList = _owner->chatsFilters().chatsList(id);\n\t\tfilterList->pinned()->applyList(filter.pinned());\n\t}\n\tif (chatlistChanged) {\n\t\t_isChatlistChanged.fire_copy(id);\n\t}\n\tif (colorChanged) {\n\t\t_tagColorChanged.fire_copy(TagColorChanged{\n\t\t\t.filterId = id,\n\t\t\t.colorExistenceChanged = colorExistenceChanged,\n\t\t});\n\t}\n\tif (entryToRefreshHeight) {\n\t\t// Trigger a full refresh of height for the main list.\n\t\tentryToRefreshHeight->updateChatListEntryHeight();\n\t}\n\treturn listUpdated;\n}\n\nbool ChatFilters::applyOrder(const QVector<MTPint> &order) {\n\tif (order.size() != _list.size()) {\n\t\treturn false;\n\t} else if (_list.empty()) {\n\t\treturn true;\n\t}\n\tauto indices = ranges::views::all(\n\t\t_list\n\t) | ranges::views::transform(\n\t\t&ChatFilter::id\n\t) | ranges::to_vector;\n\tauto b = indices.begin(), e = indices.end();\n\tfor (const auto &id : order) {\n\t\tconst auto i = ranges::find(b, e, id.v);\n\t\tif (i == e) {\n\t\t\treturn false;\n\t\t} else if (i != b) {\n\t\t\tstd::swap(*i, *b);\n\t\t}\n\t\t++b;\n\t}\n\tauto changed = false;\n\tauto begin = _list.begin(), end = _list.end();\n\tfor (const auto &id : order) {\n\t\tconst auto i = ranges::find(begin, end, id.v, &ChatFilter::id);\n\t\tAssert(i != end);\n\t\tif (i != begin) {\n\t\t\tchanged = true;\n\t\t\tstd::swap(*i, *begin);\n\t\t}\n\t\t++begin;\n\t}\n\tif (changed) {\n\t\t_listChanged.fire({});\n\t}\n\treturn true;\n}\n\nconst ChatFilter &ChatFilters::applyUpdatedPinned(\n\t\tFilterId id,\n\t\tconst std::vector<Dialogs::Key> &dialogs) {\n\tconst auto i = ranges::find(_list, id, &ChatFilter::id);\n\tAssert(i != end(_list));\n\n\tconst auto limit = _owner->pinnedChatsLimit(id);\n\tauto always = i->always();\n\tauto pinned = std::vector<not_null<History*>>();\n\tpinned.reserve(dialogs.size());\n\tfor (const auto &row : dialogs) {\n\t\tif (const auto history = row.history()) {\n\t\t\tif (always.contains(history)) {\n\t\t\t\tpinned.push_back(history);\n\t\t\t} else if (always.size() < limit) {\n\t\t\t\talways.insert(history);\n\t\t\t\tpinned.push_back(history);\n\t\t\t}\n\t\t}\n\t}\n\tset(ChatFilter(\n\t\tid,\n\t\ti->title(),\n\t\ti->iconEmoji(),\n\t\ti->colorIndex(),\n\t\ti->flags(),\n\t\tstd::move(always),\n\t\tstd::move(pinned),\n\t\ti->never()));\n\treturn *i;\n}\n\nvoid ChatFilters::saveOrder(\n\t\tconst std::vector<FilterId> &order,\n\t\tmtpRequestId after) {\n\tif (after) {\n\t\t_saveOrderAfterId = after;\n\t}\n\tconst auto api = &_owner->session().api();\n\tapi->request(_saveOrderRequestId).cancel();\n\n\tauto ids = QVector<MTPint>();\n\tids.reserve(order.size());\n\tfor (const auto id : order) {\n\t\tids.push_back(MTP_int(id));\n\t}\n\tconst auto wrapped = MTP_vector<MTPint>(ids);\n\n\tapply(MTP_updateDialogFilterOrder(wrapped));\n\t_saveOrderRequestId = api->request(MTPmessages_UpdateDialogFiltersOrder(\n\t\twrapped\n\t)).afterRequest(_saveOrderAfterId).send();\n}\n\nbool ChatFilters::archiveNeeded() const {\n\tfor (const auto &filter : _list) {\n\t\tif (!(filter.flags() & ChatFilter::Flag::NoArchived)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nconst std::vector<ChatFilter> &ChatFilters::list() const {\n\treturn _list;\n}\n\nFilterId ChatFilters::defaultId() const {\n\treturn lookupId(0);\n}\n\nFilterId ChatFilters::lookupId(int index) const {\n\tExpects(index >= 0 && index < _list.size());\n\n\tif (_owner->session().user()->isPremium() || !_list.front().id()) {\n\t\treturn _list[index].id();\n\t}\n\tconst auto i = ranges::find(_list, FilterId(0), &ChatFilter::id);\n\treturn !index\n\t\t? FilterId()\n\t\t: (index <= int(i - begin(_list)))\n\t\t? _list[index - 1].id()\n\t\t: _list[index].id();\n}\n\nbool ChatFilters::loaded() const {\n\treturn _loaded;\n}\n\nbool ChatFilters::has() const {\n\treturn _list.size() > 1;\n}\n\nrpl::producer<> ChatFilters::changed() const {\n\treturn _listChanged.events();\n}\n\nrpl::producer<FilterId> ChatFilters::isChatlistChanged() const {\n\treturn _isChatlistChanged.events();\n}\n\nrpl::producer<TagColorChanged> ChatFilters::tagColorChanged() const {\n\treturn _tagColorChanged.events();\n}\n\nbool ChatFilters::loadNextExceptions(bool chatsListLoaded) {\n\tif (_exceptionsLoadRequestId) {\n\t\treturn true;\n\t} else if (!chatsListLoaded\n\t\t&& (_owner->chatsList()->fullSize().current()\n\t\t\t< kLoadExceptionsAfter)) {\n\t\treturn false;\n\t}\n\tauto inputs = QVector<MTPInputDialogPeer>();\n\tconst auto collectExceptions = [&](FilterId id) {\n\t\tauto result = QVector<MTPInputDialogPeer>();\n\t\tconst auto i = ranges::find(_list, id, &ChatFilter::id);\n\t\tif (i != end(_list)) {\n\t\t\tresult.reserve(i->always().size());\n\t\t\tfor (const auto &history : i->always()) {\n\t\t\t\tif (!history->folderKnown()) {\n\t\t\t\t\tinputs.push_back(\n\t\t\t\t\t\tMTP_inputDialogPeer(history->peer->input()));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\twhile (!_exceptionsToLoad.empty()) {\n\t\tconst auto id = _exceptionsToLoad.front();\n\t\tconst auto exceptions = collectExceptions(id);\n\t\tif (inputs.size() + exceptions.size() > kLoadExceptionsPerRequest) {\n\t\t\tAssert(!inputs.isEmpty());\n\t\t\tbreak;\n\t\t}\n\t\t_exceptionsToLoad.pop_front();\n\t\tinputs.append(exceptions);\n\t}\n\tif (inputs.isEmpty()) {\n\t\treturn false;\n\t}\n\tconst auto api = &_owner->session().api();\n\t_exceptionsLoadRequestId = api->request(MTPmessages_GetPeerDialogs(\n\t\tMTP_vector(inputs)\n\t)).done([=](const MTPmessages_PeerDialogs &result) {\n\t\t_exceptionsLoadRequestId = 0;\n\t\t_owner->session().data().histories().applyPeerDialogs(result);\n\t\t_owner->session().api().requestMoreDialogsIfNeeded();\n\t}).fail([=] {\n\t\t_exceptionsLoadRequestId = 0;\n\t\t_owner->session().api().requestMoreDialogsIfNeeded();\n\t}).send();\n\treturn true;\n}\n\nvoid ChatFilters::refreshHistory(not_null<History*> history) {\n\tif (history->inChatList() && !list().empty()) {\n\t\t_owner->refreshChatListEntry(history);\n\t}\n}\n\nvoid ChatFilters::requestSuggested() {\n\tif (_suggestedRequestId) {\n\t\treturn;\n\t}\n\tif (_suggestedLastReceived > 0\n\t\t&& crl::now() - _suggestedLastReceived < kRefreshSuggestedTimeout) {\n\t\treturn;\n\t}\n\tconst auto api = &_owner->session().api();\n\t_suggestedRequestId = api->request(MTPmessages_GetSuggestedDialogFilters(\n\t)).done([=](const MTPVector<MTPDialogFilterSuggested> &data) {\n\t\t_suggestedRequestId = 0;\n\t\t_suggestedLastReceived = crl::now();\n\n\t\t_suggested = ranges::views::all(\n\t\t\tdata.v\n\t\t) | ranges::views::transform([&](const MTPDialogFilterSuggested &f) {\n\t\t\treturn f.match([&](const MTPDdialogFilterSuggested &data) {\n\t\t\t\treturn SuggestedFilter{\n\t\t\t\t\tData::ChatFilter::FromTL(data.vfilter(), _owner),\n\t\t\t\t\tqs(data.vdescription())\n\t\t\t\t};\n\t\t\t});\n\t\t}) | ranges::to_vector;\n\n\t\t_suggestedUpdated.fire({});\n\t}).fail([=] {\n\t\t_suggestedRequestId = 0;\n\t\t_suggestedLastReceived = crl::now() + kRefreshSuggestedTimeout / 2;\n\n\t\t_suggestedUpdated.fire({});\n\t}).send();\n}\n\nbool ChatFilters::suggestedLoaded() const {\n\treturn (_suggestedLastReceived > 0);\n}\n\nconst std::vector<SuggestedFilter> &ChatFilters::suggestedFilters() const {\n\treturn _suggested;\n}\n\nrpl::producer<> ChatFilters::suggestedUpdated() const {\n\treturn _suggestedUpdated.events();\n}\n\nrpl::producer<Ui::MoreChatsBarContent> ChatFilters::moreChatsContent(\n\t\tFilterId id) {\n\tif (!id) {\n\t\treturn rpl::single(Ui::MoreChatsBarContent{ .count = 0 });\n\t}\n\treturn [=](auto consumer) {\n\t\tauto result = rpl::lifetime();\n\n\t\tauto &entry = _moreChatsData[id];\n\t\tauto watching = entry.watching.lock();\n\t\tif (!watching) {\n\t\t\twatching = std::make_shared<bool>(true);\n\t\t\tentry.watching = watching;\n\t\t}\n\t\tresult.add([watching] {});\n\n\t\t_moreChatsUpdated.events_starting_with_copy(\n\t\t\tid\n\t\t) | rpl::on_next([=] {\n\t\t\tconsumer.put_next(Ui::MoreChatsBarContent{\n\t\t\t\t.count = int(moreChats(id).size()),\n\t\t\t});\n\t\t}, result);\n\t\tloadMoreChatsList(id);\n\n\t\treturn result;\n\t};\n}\n\nconst std::vector<not_null<PeerData*>> &ChatFilters::moreChats(\n\t\tFilterId id) const {\n\tstatic const auto kEmpty = std::vector<not_null<PeerData*>>();\n\tif (!id) {\n\t\treturn kEmpty;\n\t}\n\tconst auto i = _moreChatsData.find(id);\n\treturn (i != end(_moreChatsData)) ? i->second.missing : kEmpty;\n}\n\nvoid ChatFilters::moreChatsHide(FilterId id, bool localOnly) {\n\tif (!localOnly) {\n\t\tconst auto api = &_owner->session().api();\n\t\tapi->request(MTPchatlists_HideChatlistUpdates(\n\t\t\tMTP_inputChatlistDialogFilter(MTP_int(id))\n\t\t)).send();\n\t}\n\n\tconst auto i = _moreChatsData.find(id);\n\tif (i != end(_moreChatsData)) {\n\t\tif (const auto requestId = base::take(i->second.requestId)) {\n\t\t\t_owner->session().api().request(requestId).cancel();\n\t\t}\n\t\ti->second.missing = {};\n\t\ti->second.lastUpdate = crl::now();\n\t\t_moreChatsUpdated.fire_copy(id);\n\t}\n}\n\nvoid ChatFilters::loadMoreChatsList(FilterId id) {\n\tExpects(id != 0);\n\n\tconst auto i = ranges::find(_list, id, &ChatFilter::id);\n\tif (i == end(_list) || !i->chatlist()) {\n\t\treturn;\n\t}\n\n\tauto &entry = _moreChatsData[id];\n\tconst auto now = crl::now();\n\tif (!entry.watching.lock() || entry.requestId) {\n\t\treturn;\n\t}\n\tconst auto last = entry.lastUpdate;\n\tconst auto next = last ? (last + RequestUpdatesEach(_owner)) : 0;\n\tif (next > now) {\n\t\tif (!_moreChatsTimer.isActive()) {\n\t\t\t_moreChatsTimer.callOnce(next - now);\n\t\t}\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\tentry.requestId = api.request(MTPchatlists_GetChatlistUpdates(\n\t\tMTP_inputChatlistDialogFilter(MTP_int(id))\n\t)).done([=](const MTPchatlists_ChatlistUpdates &result) {\n\t\tconst auto &data = result.data();\n\t\t_owner->processUsers(data.vusers());\n\t\t_owner->processChats(data.vchats());\n\t\tauto list = ranges::views::all(\n\t\t\tdata.vmissing_peers().v\n\t\t) | ranges::views::transform([&](const MTPPeer &peer) {\n\t\t\treturn _owner->peer(peerFromMTP(peer));\n\t\t}) | ranges::to_vector;\n\n\t\tauto &entry = _moreChatsData[id];\n\t\tentry.requestId = 0;\n\t\tentry.lastUpdate = crl::now();\n\t\tif (!_moreChatsTimer.isActive()) {\n\t\t\t_moreChatsTimer.callOnce(RequestUpdatesEach(_owner));\n\t\t}\n\t\tif (entry.missing != list) {\n\t\t\tentry.missing = std::move(list);\n\t\t\t_moreChatsUpdated.fire_copy(id);\n\t\t}\n\t}).fail([=] {\n\t\tauto &entry = _moreChatsData[id];\n\t\tentry.requestId = 0;\n\t\tentry.lastUpdate = crl::now();\n\t}).send();\n}\n\nvoid ChatFilters::checkLoadMoreChatsLists() {\n\tfor (const auto &[id, entry] : _moreChatsData) {\n\t\tloadMoreChatsList(id);\n\t}\n}\n\nbool CanRemoveFromChatFilter(\n\t\tconst ChatFilter &filter,\n\t\tnot_null<History*> history) {\n\tusing Flag = ChatFilter::Flag;\n\tconst auto flagsWithoutNoReadNoArchivedNoMuted = filter.flags()\n\t\t& ~(Flag::NoRead | Flag::NoArchived | Flag::NoMuted);\n\treturn (filter.always().size() > 1 || flagsWithoutNoReadNoArchivedNoMuted)\n\t\t&& filter.contains(history);\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_chat_filters.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n#include \"base/timer.h\"\n\nclass History;\n\nnamespace Dialogs {\nclass MainList;\nclass Key;\n} // namespace Dialogs\n\nnamespace Ui {\nstruct MoreChatsBarContent;\n} // namespace Ui\n\nnamespace Data {\n\nclass Session;\n\nstruct ChatFilterTitle {\n\tTextWithEntities text;\n\tbool isStatic = false;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn text.empty();\n\t}\n};\n\n[[nodiscard]] TextWithEntities ForceCustomEmojiStatic(TextWithEntities text);\n\nclass ChatFilter final {\npublic:\n\tenum class Flag : ushort {\n\t\tContacts    = (1 << 0),\n\t\tNonContacts = (1 << 1),\n\t\tGroups      = (1 << 2),\n\t\tChannels    = (1 << 3),\n\t\tBots        = (1 << 4),\n\t\tNoMuted     = (1 << 5),\n\t\tNoRead      = (1 << 6),\n\t\tNoArchived  = (1 << 7),\n\t\tRulesMask   = ((1 << 8) - 1),\n\n\t\tChatlist    = (1 << 8),\n\t\tHasMyLinks  = (1 << 9),\n\t\tStaticTitle = (1 << 10),\n\n\t\tNewChats      = (1 << 11), // Telegram Business exceptions.\n\t\tExistingChats = (1 << 12),\n\t};\n\tfriend constexpr inline bool is_flag_type(Flag) { return true; };\n\tusing Flags = base::flags<Flag>;\n\n\tChatFilter() = default;\n\tChatFilter(\n\t\tFilterId id,\n\t\tChatFilterTitle title,\n\t\tQString iconEmoji,\n\t\tstd::optional<uint8> colorIndex,\n\t\tFlags flags,\n\t\tbase::flat_set<not_null<History*>> always,\n\t\tstd::vector<not_null<History*>> pinned,\n\t\tbase::flat_set<not_null<History*>> never);\n\n\t[[nodiscard]] ChatFilter withId(FilterId id) const;\n\t[[nodiscard]] ChatFilter withTitle(ChatFilterTitle title) const;\n\t[[nodiscard]] ChatFilter withColorIndex(std::optional<uint8>) const;\n\t[[nodiscard]] ChatFilter withChatlist(\n\t\tbool chatlist,\n\t\tbool hasMyLinks) const;\n\t[[nodiscard]] ChatFilter withoutAlways(not_null<History*>) const;\n\n\t[[nodiscard]] static ChatFilter FromTL(\n\t\tconst MTPDialogFilter &data,\n\t\tnot_null<Session*> owner);\n\t[[nodiscard]] MTPDialogFilter tl(FilterId replaceId = 0) const;\n\n\t[[nodiscard]] FilterId id() const;\n\t[[nodiscard]] ChatFilterTitle title() const;\n\t[[nodiscard]] const TextWithEntities &titleText() const;\n\t[[nodiscard]] QString iconEmoji() const;\n\t[[nodiscard]] std::optional<uint8> colorIndex() const;\n\t[[nodiscard]] Flags flags() const;\n\t[[nodiscard]] bool staticTitle() const;\n\t[[nodiscard]] bool chatlist() const;\n\t[[nodiscard]] bool hasMyLinks() const;\n\t[[nodiscard]] const base::flat_set<not_null<History*>> &always() const;\n\t[[nodiscard]] const std::vector<not_null<History*>> &pinned() const;\n\t[[nodiscard]] const base::flat_set<not_null<History*>> &never() const;\n\n\t[[nodiscard]] bool contains(\n\t\tnot_null<History*> history,\n\t\tbool ignoreFakeUnread = false) const;\n\nprivate:\n\tFilterId _id = 0;\n\tTextWithEntities _title;\n\tQString _iconEmoji;\n\tstd::optional<uint8> _colorIndex;\n\tbase::flat_set<not_null<History*>> _always;\n\tstd::vector<not_null<History*>> _pinned;\n\tbase::flat_set<not_null<History*>> _never;\n\tFlags _flags;\n\n};\n\ninline bool operator==(const ChatFilter &a, const ChatFilter &b) {\n\treturn (a.titleText() == b.titleText())\n\t\t&& (a.iconEmoji() == b.iconEmoji())\n\t\t&& (a.colorIndex() == b.colorIndex())\n\t\t&& (a.flags() == b.flags())\n\t\t&& (a.always() == b.always())\n\t\t&& (a.never() == b.never());\n}\n\ninline bool operator!=(const ChatFilter &a, const ChatFilter &b) {\n\treturn !(a == b);\n}\n\nstruct ChatFilterLink {\n\tFilterId id = 0;\n\tQString url;\n\tQString title;\n\tstd::vector<not_null<History*>> chats;\n\n\tfriend inline bool operator==(\n\t\tconst ChatFilterLink &a,\n\t\tconst ChatFilterLink &b) = default;\n};\n\nstruct SuggestedFilter {\n\tChatFilter filter;\n\tQString description;\n};\n\nstruct TagColorChanged final {\n\tFilterId filterId = 0;\n\tbool colorExistenceChanged = false;\n};\n\nclass ChatFilters final {\npublic:\n\texplicit ChatFilters(not_null<Session*> owner);\n\t~ChatFilters();\n\n\tvoid setPreloaded(\n\t\tconst QVector<MTPDialogFilter> &result,\n\t\tbool tagsEnabled);\n\n\tvoid load();\n\tvoid reload();\n\tvoid apply(const MTPUpdate &update);\n\tvoid set(ChatFilter filter);\n\tvoid remove(FilterId id);\n\tvoid moveAllToFront();\n\t[[nodiscard]] const std::vector<ChatFilter> &list() const;\n\t[[nodiscard]] rpl::producer<> changed() const;\n\t[[nodiscard]] rpl::producer<FilterId> isChatlistChanged() const;\n\t[[nodiscard]] rpl::producer<TagColorChanged> tagColorChanged() const;\n\t[[nodiscard]] bool loaded() const;\n\t[[nodiscard]] bool has() const;\n\n\t[[nodiscard]] FilterId defaultId() const;\n\t[[nodiscard]] FilterId lookupId(int index) const;\n\n\tbool loadNextExceptions(bool chatsListLoaded);\n\n\tvoid refreshHistory(not_null<History*> history);\n\n\t[[nodiscard]] not_null<Dialogs::MainList*> chatsList(FilterId filterId);\n\tvoid clear();\n\n\tconst ChatFilter &applyUpdatedPinned(\n\t\tFilterId id,\n\t\tconst std::vector<Dialogs::Key> &dialogs);\n\tvoid saveOrder(\n\t\tconst std::vector<FilterId> &order,\n\t\tmtpRequestId after = 0);\n\n\t[[nodiscard]] bool archiveNeeded() const;\n\n\tvoid requestSuggested();\n\t[[nodiscard]] bool suggestedLoaded() const;\n\t[[nodiscard]] auto suggestedFilters() const\n\t\t-> const std::vector<SuggestedFilter> &;\n\t[[nodiscard]] rpl::producer<> suggestedUpdated() const;\n\n\tChatFilterLink add(\n\t\tFilterId id,\n\t\tconst MTPExportedChatlistInvite &update);\n\tvoid edit(\n\t\tFilterId id,\n\t\tconst QString &url,\n\t\tconst QString &title);\n\tvoid destroy(FilterId id, const QString &url);\n\trpl::producer<std::vector<ChatFilterLink>> chatlistLinks(\n\t\tFilterId id) const;\n\tvoid reloadChatlistLinks(FilterId id);\n\n\t[[nodiscard]] rpl::producer<Ui::MoreChatsBarContent> moreChatsContent(\n\t\tFilterId id);\n\t[[nodiscard]] const std::vector<not_null<PeerData*>> &moreChats(\n\t\tFilterId id) const;\n\tvoid moreChatsHide(FilterId id, bool localOnly = false);\n\n\t[[nodiscard]] bool tagsEnabled() const;\n\t[[nodiscard]] rpl::producer<bool> tagsEnabledValue() const;\n\t[[nodiscard]] rpl::producer<bool> tagsEnabledChanges() const;\n\tvoid requestToggleTags(bool value, Fn<void()> fail);\n\nprivate:\n\tstruct MoreChatsData {\n\t\tstd::vector<not_null<PeerData*>> missing;\n\t\tcrl::time lastUpdate = 0;\n\t\tmtpRequestId requestId = 0;\n\t\tstd::weak_ptr<bool> watching;\n\t};\n\n\tvoid load(bool force);\n\tvoid received(const QVector<MTPDialogFilter> &list);\n\tbool applyOrder(const QVector<MTPint> &order);\n\tbool applyChange(ChatFilter &filter, ChatFilter &&updated);\n\tvoid applyInsert(ChatFilter filter, int position);\n\tvoid applyRemove(int position);\n\n\tvoid checkLoadMoreChatsLists();\n\tvoid loadMoreChatsList(FilterId id);\n\n\tconst not_null<Session*> _owner;\n\n\tstd::vector<ChatFilter> _list;\n\tbase::flat_map<FilterId, std::unique_ptr<Dialogs::MainList>> _chatsLists;\n\trpl::event_stream<> _listChanged;\n\trpl::event_stream<FilterId> _isChatlistChanged;\n\trpl::event_stream<TagColorChanged> _tagColorChanged;\n\tmtpRequestId _loadRequestId = 0;\n\tmtpRequestId _saveOrderRequestId = 0;\n\tmtpRequestId _saveOrderAfterId = 0;\n\tmtpRequestId _toggleTagsRequestId = 0;\n\tbool _loaded = false;\n\tbool _reloading = false;\n\n\tmtpRequestId _suggestedRequestId = 0;\n\tstd::vector<SuggestedFilter> _suggested;\n\trpl::event_stream<> _suggestedUpdated;\n\tcrl::time _suggestedLastReceived = 0;\n\n\trpl::variable<bool> _tagsEnabled = false;\n\n\tstd::deque<FilterId> _exceptionsToLoad;\n\tmtpRequestId _exceptionsLoadRequestId = 0;\n\n\tbase::flat_map<FilterId, std::vector<ChatFilterLink>> _chatlistLinks;\n\trpl::event_stream<FilterId> _chatlistLinksUpdated;\n\tmtpRequestId _linksRequestId = 0;\n\n\tbase::flat_map<FilterId, MoreChatsData> _moreChatsData;\n\trpl::event_stream<FilterId> _moreChatsUpdated;\n\tbase::Timer _moreChatsTimer;\n\n};\n\n[[nodiscard]] bool CanRemoveFromChatFilter(\n\tconst ChatFilter &filter,\n\tnot_null<History*> history);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_chat_participant_status.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_chat_participant_status.h\"\n\n#include \"base/unixtime.h\"\n#include \"boxes/peers/edit_peer_permissions_box.h\"\n#include \"boxes/premium_limits_box.h\" // FileSizeLimitBox.\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/format_values.h\" // FormatDurationWordsSlowmode.\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace {\n\n[[nodiscard]] ChatAdminRights ChatAdminRightsFlags(\n\t\tconst MTPChatAdminRights &rights) {\n\treturn rights.match([](const MTPDchatAdminRights &data) {\n\t\tusing Flag = ChatAdminRight;\n\t\treturn (data.is_change_info() ? Flag::ChangeInfo : Flag())\n\t\t\t| (data.is_post_messages() ? Flag::PostMessages : Flag())\n\t\t\t| (data.is_edit_messages() ? Flag::EditMessages : Flag())\n\t\t\t| (data.is_delete_messages() ? Flag::DeleteMessages : Flag())\n\t\t\t| (data.is_ban_users() ? Flag::BanUsers : Flag())\n\t\t\t| (data.is_invite_users() ? Flag::InviteByLinkOrAdd : Flag())\n\t\t\t| (data.is_pin_messages() ? Flag::PinMessages : Flag())\n\t\t\t| (data.is_add_admins() ? Flag::AddAdmins : Flag())\n\t\t\t| (data.is_anonymous() ? Flag::Anonymous : Flag())\n\t\t\t| (data.is_manage_call() ? Flag::ManageCall : Flag())\n\t\t\t| (data.is_other() ? Flag::Other : Flag())\n\t\t\t| (data.is_manage_topics() ? Flag::ManageTopics : Flag())\n\t\t\t| (data.is_post_stories() ? Flag::PostStories : Flag())\n\t\t\t| (data.is_edit_stories() ? Flag::EditStories : Flag())\n\t\t\t| (data.is_delete_stories() ? Flag::DeleteStories : Flag())\n\t\t\t| (data.is_manage_direct_messages()\n\t\t\t\t? Flag::ManageDirect\n\t\t\t\t: Flag())\n\t\t\t| (data.is_manage_ranks()\n\t\t\t\t? Flag::ManageRanks\n\t\t\t\t: Flag());\n\t});\n}\n\n[[nodiscard]] ChatRestrictions ChatBannedRightsFlags(\n\t\tconst MTPChatBannedRights &rights) {\n\treturn rights.match([](const MTPDchatBannedRights &data) {\n\t\tusing Flag = ChatRestriction;\n\t\treturn (data.is_view_messages() ? Flag::ViewMessages : Flag())\n\t\t\t| (data.is_send_stickers() ? Flag::SendStickers : Flag())\n\t\t\t| (data.is_send_gifs() ? Flag::SendGifs : Flag())\n\t\t\t| (data.is_send_games() ? Flag::SendGames : Flag())\n\t\t\t| (data.is_send_inline() ? Flag::SendInline : Flag())\n\t\t\t| (data.is_send_polls() ? Flag::SendPolls : Flag())\n\t\t\t| (data.is_send_photos() ? Flag::SendPhotos : Flag())\n\t\t\t| (data.is_send_videos() ? Flag::SendVideos : Flag())\n\t\t\t| (data.is_send_roundvideos() ? Flag::SendVideoMessages : Flag())\n\t\t\t| (data.is_send_audios() ? Flag::SendMusic : Flag())\n\t\t\t| (data.is_send_voices() ? Flag::SendVoiceMessages : Flag())\n\t\t\t| (data.is_send_docs() ? Flag::SendFiles : Flag())\n\t\t\t| (data.is_send_plain() ? Flag::SendOther : Flag())\n\t\t\t| (data.is_embed_links() ? Flag::EmbedLinks : Flag())\n\t\t\t| (data.is_change_info() ? Flag::ChangeInfo : Flag())\n\t\t\t| (data.is_invite_users() ? Flag::AddParticipants : Flag())\n\t\t\t| (data.is_pin_messages() ? Flag::PinMessages : Flag())\n\t\t\t| (data.is_manage_topics() ? Flag::CreateTopics : Flag())\n\t\t\t| (data.is_edit_rank() ? Flag::EditRank : Flag());\n\t});\n}\n\n[[nodiscard]] TimeId ChatBannedRightsUntilDate(\n\t\tconst MTPChatBannedRights &rights) {\n\treturn rights.match([](const MTPDchatBannedRights &data) {\n\t\treturn data.vuntil_date().v;\n\t});\n}\n\n} // namespace\n\nChatAdminRightsInfo::ChatAdminRightsInfo(const MTPChatAdminRights &rights)\n: flags(ChatAdminRightsFlags(rights)) {\n}\n\nMTPChatAdminRights AdminRightsToMTP(ChatAdminRightsInfo info) {\n\tusing Flag = MTPDchatAdminRights::Flag;\n\tusing R = ChatAdminRight;\n\tconst auto flags = info.flags;\n\treturn MTP_chatAdminRights(MTP_flags(Flag()\n\t\t| ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag())\n\t\t| ((flags & R::PostMessages) ? Flag::f_post_messages : Flag())\n\t\t| ((flags & R::EditMessages) ? Flag::f_edit_messages : Flag())\n\t\t| ((flags & R::DeleteMessages) ? Flag::f_delete_messages : Flag())\n\t\t| ((flags & R::BanUsers) ? Flag::f_ban_users : Flag())\n\t\t| ((flags & R::InviteByLinkOrAdd) ? Flag::f_invite_users : Flag())\n\t\t| ((flags & R::PinMessages) ? Flag::f_pin_messages : Flag())\n\t\t| ((flags & R::AddAdmins) ? Flag::f_add_admins : Flag())\n\t\t| ((flags & R::Anonymous) ? Flag::f_anonymous : Flag())\n\t\t| ((flags & R::ManageCall) ? Flag::f_manage_call : Flag())\n\t\t| ((flags & R::Other) ? Flag::f_other : Flag())\n\t\t| ((flags & R::ManageTopics) ? Flag::f_manage_topics : Flag())\n\t\t| ((flags & R::PostStories) ? Flag::f_post_stories : Flag())\n\t\t| ((flags & R::EditStories) ? Flag::f_edit_stories : Flag())\n\t\t| ((flags & R::DeleteStories) ? Flag::f_delete_stories : Flag())\n\t\t| ((flags & R::ManageDirect)\n\t\t\t? Flag::f_manage_direct_messages\n\t\t\t: Flag())\n\t\t| ((flags & R::ManageRanks)\n\t\t\t? Flag::f_manage_ranks\n\t\t\t: Flag())));\n}\n\nChatRestrictionsInfo::ChatRestrictionsInfo(const MTPChatBannedRights &rights)\n: flags(ChatBannedRightsFlags(rights))\n, until(ChatBannedRightsUntilDate(rights)) {\n}\n\nMTPChatBannedRights RestrictionsToMTP(ChatRestrictionsInfo info) {\n\tusing Flag = MTPDchatBannedRights::Flag;\n\tusing R = ChatRestriction;\n\tconst auto flags = info.flags;\n\treturn MTP_chatBannedRights(\n\t\tMTP_flags(Flag()\n\t\t\t| ((flags & R::ViewMessages) ? Flag::f_view_messages : Flag())\n\t\t\t| ((flags & R::SendStickers) ? Flag::f_send_stickers : Flag())\n\t\t\t| ((flags & R::SendGifs) ? Flag::f_send_gifs : Flag())\n\t\t\t| ((flags & R::SendGames) ? Flag::f_send_games : Flag())\n\t\t\t| ((flags & R::SendInline) ? Flag::f_send_inline : Flag())\n\t\t\t| ((flags & R::SendPolls) ? Flag::f_send_polls : Flag())\n\t\t\t| ((flags & R::SendPhotos) ? Flag::f_send_photos : Flag())\n\t\t\t| ((flags & R::SendVideos) ? Flag::f_send_videos : Flag())\n\t\t\t| ((flags & R::SendVideoMessages) ? Flag::f_send_roundvideos : Flag())\n\t\t\t| ((flags & R::SendMusic) ? Flag::f_send_audios : Flag())\n\t\t\t| ((flags & R::SendVoiceMessages) ? Flag::f_send_voices : Flag())\n\t\t\t| ((flags & R::SendFiles) ? Flag::f_send_docs : Flag())\n\t\t\t| ((flags & R::SendOther) ? Flag::f_send_plain : Flag())\n\t\t\t| ((flags & R::EmbedLinks) ? Flag::f_embed_links : Flag())\n\t\t\t| ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag())\n\t\t\t| ((flags & R::AddParticipants) ? Flag::f_invite_users : Flag())\n\t\t\t| ((flags & R::PinMessages) ? Flag::f_pin_messages : Flag())\n\t\t\t| ((flags & R::CreateTopics) ? Flag::f_manage_topics : Flag())\n\t\t\t| ((flags & R::EditRank) ? Flag::f_edit_rank : Flag())),\n\t\tMTP_int(info.until));\n}\n\nnamespace Data {\n\nstd::vector<ChatRestrictions> ListOfRestrictions(\n\t\tRestrictionsSetOptions options) {\n\tauto labels = RestrictionLabels(options);\n\treturn ranges::views::all(labels)\n\t\t| ranges::views::transform(&RestrictionLabel::flags)\n\t\t| ranges::to_vector;\n}\n\nChatRestrictions AllSendRestrictions() {\n\tconstexpr auto result = [] {\n\t\tauto result = ChatRestrictions();\n\t\tfor (const auto right : AllSendRestrictionsList()) {\n\t\t\tresult |= right;\n\t\t}\n\t\treturn result;\n\t}();\n\treturn result;\n}\n\nChatRestrictions FilesSendRestrictions() {\n\tconstexpr auto result = [] {\n\t\tauto result = ChatRestrictions();\n\t\tfor (const auto right : FilesSendRestrictionsList()) {\n\t\t\tresult |= right;\n\t\t}\n\t\treturn result;\n\t}();\n\treturn result;\n}\n\nChatRestrictions TabbedPanelSendRestrictions() {\n\tconstexpr auto result = [] {\n\t\tauto result = ChatRestrictions();\n\t\tfor (const auto right : TabbedPanelSendRestrictionsList()) {\n\t\t\tresult |= right;\n\t\t}\n\t\treturn result;\n\t}();\n\treturn result;\n}\n\n// Duplicated in CanSendAnyOfValue().\nbool CanSendAnyOf(\n\t\tnot_null<const Thread*> thread,\n\t\tChatRestrictions rights,\n\t\tbool forbidInForums) {\n\tconst auto peer = thread->peer();\n\tconst auto topic = thread->asTopic();\n\treturn CanSendAnyOf(peer, rights, forbidInForums && !topic)\n\t\t&& (!topic || !topic->closed() || topic->canToggleClosed());\n}\n\n// Duplicated in CanSendAnyOfValue().\nbool CanSendAnyOf(\n\t\tnot_null<const PeerData*> peer,\n\t\tChatRestrictions rights,\n\t\tbool forbidInForums) {\n\tif (peer->session().frozen()\n\t\t&& !peer->isFreezeAppealChat()) {\n\t\treturn false;\n\t} else if (const auto user = peer->asUser()) {\n\t\tif (user->isInaccessible()\n\t\t\t|| user->isRepliesChat()\n\t\t\t|| user->isVerifyCodes()) {\n\t\t\treturn false;\n\t\t} else if (user->requiresPremiumToWrite()\n\t\t\t&& !user->session().premium()) {\n\t\t\treturn false;\n\t\t} else if (rights\n\t\t\t& ~(ChatRestriction::SendVoiceMessages\n\t\t\t\t| ChatRestriction::SendVideoMessages\n\t\t\t\t| ChatRestriction::SendPolls)) {\n\t\t\treturn true;\n\t\t}\n\t\tfor (const auto right : {\n\t\t\tChatRestriction::SendVoiceMessages,\n\t\t\tChatRestriction::SendVideoMessages,\n\t\t\tChatRestriction::SendPolls,\n\t\t}) {\n\t\t\tif ((rights & right) && !user->amRestricted(right)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t} else if (const auto chat = peer->asChat()) {\n\t\tif (!chat->amIn()) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (const auto right : AllSendRestrictionsList()) {\n\t\t\tif ((rights & right) && !chat->amRestricted(right)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tif (channel->monoforumDisabled()) {\n\t\t\treturn false;\n\t\t}\n\t\tusing Flag = ChannelDataFlag;\n\t\tconst auto allowed = channel->amIn()\n\t\t\t|| ((channel->flags() & Flag::HasLink)\n\t\t\t\t&& !(channel->flags() & Flag::JoinToWrite))\n\t\t\t|| channel->isMonoforum();\n\t\tif (!allowed || (forbidInForums && channel->isForum())) {\n\t\t\treturn false;\n\t\t} else if (channel->canPostMessages()) {\n\t\t\treturn true;\n\t\t} else if (channel->isBroadcast()) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (const auto right : AllSendRestrictionsList()) {\n\t\t\tif ((rights & right) && !channel->amRestricted(right)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\tUnexpected(\"Peer type in CanSendAnyOf.\");\n}\n\nSendError RestrictionError(\n\t\tnot_null<PeerData*> peer,\n\t\tChatRestriction restriction) {\n\tusing Flag = ChatRestriction;\n\tif (peer->session().frozen()\n\t\t&& !peer->isFreezeAppealChat()) {\n\t\treturn SendError({\n\t\t\t.text = tr::lng_frozen_restrict_title(tr::now),\n\t\t\t.frozen = true,\n\t\t});\n\t} else if (const auto restricted = peer->amRestricted(restriction)) {\n\t\tif (const auto user = peer->asUser()) {\n\t\t\tif (user->requiresPremiumToWrite()\n\t\t\t\t&& !user->session().premium()) {\n\t\t\t\treturn SendError({\n\t\t\t\t\t.text = tr::lng_restricted_send_non_premium(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\tuser->shortName()),\n\t\t\t\t\t.premiumToLift = true,\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst auto result = (restriction == Flag::SendVoiceMessages)\n\t\t\t\t? tr::lng_restricted_send_voice_messages(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tuser->name())\n\t\t\t\t: (restriction == Flag::SendVideoMessages)\n\t\t\t\t? tr::lng_restricted_send_video_messages(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tuser->name())\n\t\t\t\t: (restriction == Flag::SendPolls)\n\t\t\t\t? u\"can't send polls :(\"_q\n\t\t\t\t: (restriction == Flag::PinMessages)\n\t\t\t\t? u\"can't pin :(\"_q\n\t\t\t\t: SendError();\n\n\t\t\tEnsures(result.has_value());\n\t\t\treturn result;\n\t\t}\n\t\tconst auto all = restricted.isWithEveryone();\n\t\tconst auto channel = peer->asChannel();\n\t\tif (channel && channel->monoforumDisabled()) {\n\t\t\treturn tr::lng_action_direct_messages_disabled(tr::now);\n\t\t}\n\t\tif (!all && channel) {\n\t\t\tauto restrictedUntil = channel->restrictedUntil();\n\t\t\tif (restrictedUntil > 0\n\t\t\t\t&& !ChannelData::IsRestrictedForever(restrictedUntil)) {\n\t\t\t\tauto restrictedUntilDateTime = base::unixtime::parse(\n\t\t\t\t\tchannel->restrictedUntil());\n\t\t\t\tauto date = QLocale().toString(\n\t\t\t\t\trestrictedUntilDateTime.date(),\n\t\t\t\t\tQLocale::ShortFormat);\n\t\t\t\tauto time = QLocale().toString(\n\t\t\t\t\trestrictedUntilDateTime.time(),\n\t\t\t\t\tQLocale::ShortFormat);\n\n\t\t\t\tswitch (restriction) {\n\t\t\t\tcase Flag::SendPolls:\n\t\t\t\t\treturn tr::lng_restricted_send_polls_until(\n\t\t\t\t\t\ttr::now, lt_date, date, lt_time, time);\n\t\t\t\tcase Flag::SendOther:\n\t\t\t\t\treturn tr::lng_restricted_send_message_until(\n\t\t\t\t\t\ttr::now, lt_date, date, lt_time, time);\n\t\t\t\tcase Flag::SendPhotos:\n\t\t\t\t\treturn tr::lng_restricted_send_photos_until(\n\t\t\t\t\t\ttr::now, lt_date, date, lt_time, time);\n\t\t\t\tcase Flag::SendVideos:\n\t\t\t\t\treturn tr::lng_restricted_send_videos_until(\n\t\t\t\t\t\ttr::now, lt_date, date, lt_time, time);\n\t\t\t\tcase Flag::SendMusic:\n\t\t\t\t\treturn tr::lng_restricted_send_music_until(\n\t\t\t\t\t\ttr::now, lt_date, date, lt_time, time);\n\t\t\t\tcase Flag::SendFiles:\n\t\t\t\t\treturn tr::lng_restricted_send_files_until(\n\t\t\t\t\t\ttr::now, lt_date, date, lt_time, time);\n\t\t\t\tcase Flag::SendVideoMessages:\n\t\t\t\t\treturn tr::lng_restricted_send_video_messages_until(\n\t\t\t\t\t\ttr::now, lt_date, date, lt_time, time);\n\t\t\t\tcase Flag::SendVoiceMessages:\n\t\t\t\t\treturn tr::lng_restricted_send_voice_messages_until(\n\t\t\t\t\t\ttr::now, lt_date, date, lt_time, time);\n\t\t\t\tcase Flag::SendStickers:\n\t\t\t\t\treturn tr::lng_restricted_send_stickers_until(\n\t\t\t\t\t\ttr::now, lt_date, date, lt_time, time);\n\t\t\t\tcase Flag::SendGifs:\n\t\t\t\t\treturn tr::lng_restricted_send_gifs_until(\n\t\t\t\t\t\ttr::now, lt_date, date, lt_time, time);\n\t\t\t\tcase Flag::SendInline:\n\t\t\t\tcase Flag::SendGames:\n\t\t\t\t\treturn tr::lng_restricted_send_inline_until(\n\t\t\t\t\t\ttr::now, lt_date, date, lt_time, time);\n\t\t\t\t}\n\t\t\t\tUnexpected(\"Restriction in Data::RestrictionErrorKey.\");\n\t\t\t}\n\t\t}\n\t\tif (all\n\t\t\t&& channel\n\t\t\t&& channel->boostsUnrestrict()\n\t\t\t&& !channel->unrestrictedByBoosts()) {\n\t\t\treturn SendError({\n\t\t\t\t.text = tr::lng_restricted_boost_group(tr::now),\n\t\t\t\t.boostsToLift = (channel->boostsUnrestrict()\n\t\t\t\t\t- channel->boostsApplied()),\n\t\t\t});\n\t\t}\n\t\tswitch (restriction) {\n\t\tcase Flag::SendPolls:\n\t\t\treturn all\n\t\t\t\t? tr::lng_restricted_send_polls_all(tr::now)\n\t\t\t\t: tr::lng_restricted_send_polls(tr::now);\n\t\tcase Flag::SendOther:\n\t\t\treturn all\n\t\t\t\t? tr::lng_restricted_send_message_all(tr::now)\n\t\t\t\t: tr::lng_restricted_send_message(tr::now);\n\t\tcase Flag::SendPhotos:\n\t\t\treturn all\n\t\t\t\t? tr::lng_restricted_send_photos_all(tr::now)\n\t\t\t\t: tr::lng_restricted_send_photos(tr::now);\n\t\tcase Flag::SendVideos:\n\t\t\treturn all\n\t\t\t\t? tr::lng_restricted_send_videos_all(tr::now)\n\t\t\t\t: tr::lng_restricted_send_videos(tr::now);\n\t\tcase Flag::SendMusic:\n\t\t\treturn all\n\t\t\t\t? tr::lng_restricted_send_music_all(tr::now)\n\t\t\t\t: tr::lng_restricted_send_music(tr::now);\n\t\tcase Flag::SendFiles:\n\t\t\treturn all\n\t\t\t\t? tr::lng_restricted_send_files_all(tr::now)\n\t\t\t\t: tr::lng_restricted_send_files(tr::now);\n\t\tcase Flag::SendVideoMessages:\n\t\t\treturn all\n\t\t\t\t? tr::lng_restricted_send_video_messages_all(tr::now)\n\t\t\t\t: tr::lng_restricted_send_video_messages_group(tr::now);\n\t\tcase Flag::SendVoiceMessages:\n\t\t\treturn all\n\t\t\t\t? tr::lng_restricted_send_voice_messages_all(tr::now)\n\t\t\t\t: tr::lng_restricted_send_voice_messages_group(tr::now);\n\t\tcase Flag::SendStickers:\n\t\t\treturn all\n\t\t\t\t? tr::lng_restricted_send_stickers_all(tr::now)\n\t\t\t\t: tr::lng_restricted_send_stickers(tr::now);\n\t\tcase Flag::SendGifs:\n\t\t\treturn all\n\t\t\t\t? tr::lng_restricted_send_gifs_all(tr::now)\n\t\t\t\t: tr::lng_restricted_send_gifs(tr::now);\n\t\tcase Flag::SendInline:\n\t\tcase Flag::SendGames:\n\t\t\treturn all\n\t\t\t\t? tr::lng_restricted_send_inline_all(tr::now)\n\t\t\t\t: tr::lng_restricted_send_inline(tr::now);\n\t\t}\n\t\tUnexpected(\"Restriction in Data::RestrictionErrorKey.\");\n\t}\n\treturn SendError();\n}\n\nSendError AnyFileRestrictionError(not_null<PeerData*> peer) {\n\tusing Restriction = ChatRestriction;\n\tfor (const auto right : FilesSendRestrictionsList()) {\n\t\tif (!RestrictionError(peer, right)) {\n\t\t\treturn {};\n\t\t}\n\t}\n\treturn RestrictionError(peer, Restriction::SendFiles);\n}\n\nSendError FileRestrictionError(\n\t\tnot_null<PeerData*> peer,\n\t\tconst Ui::PreparedList &list,\n\t\tstd::optional<bool> compress) {\n\tconst auto slowmode = peer->slowmodeApplied();\n\tif (slowmode) {\n\t\tif (!list.canBeSentInSlowmode()) {\n\t\t\treturn tr::lng_slowmode_no_many(tr::now);\n\t\t} else if (list.files.size() > 1 && list.hasSticker()) {\n\t\t\tif (compress == false) {\n\t\t\t\treturn tr::lng_slowmode_no_many(tr::now);\n\t\t\t} else {\n\t\t\t\tcompress = true;\n\t\t\t}\n\t\t}\n\t}\n\tfor (const auto &file : list.files) {\n\t\tif (const auto error = FileRestrictionError(peer, file, compress)) {\n\t\t\treturn error;\n\t\t}\n\t}\n\treturn {};\n}\n\nSendError FileRestrictionError(\n\t\tnot_null<PeerData*> peer,\n\t\tconst Ui::PreparedFile &file,\n\t\tstd::optional<bool> compress) {\n\tusing Type = Ui::PreparedFile::Type;\n\tusing Restriction = ChatRestriction;\n\tconst auto stickers = RestrictionError(peer, Restriction::SendStickers);\n\tconst auto gifs = RestrictionError(peer, Restriction::SendGifs);\n\tconst auto photos = RestrictionError(peer, Restriction::SendPhotos);\n\tconst auto videos = RestrictionError(peer, Restriction::SendVideos);\n\tconst auto music = RestrictionError(peer, Restriction::SendMusic);\n\tconst auto files = RestrictionError(peer, Restriction::SendFiles);\n\tif (!stickers && !gifs && !photos && !videos && !music && !files) {\n\t\treturn {};\n\t}\n\tswitch (file.type) {\n\tcase Type::Photo:\n\t\tif (compress == true && photos) {\n\t\t\treturn photos;\n\t\t} else if (const auto other = file.isSticker() ? stickers : files) {\n\t\t\tif ((compress == false || photos) && other) {\n\t\t\t\treturn (file.isSticker() || !photos) ? other : photos;\n\t\t\t}\n\t\t}\n\t\tbreak;\n\tcase Type::Video:\n\t\tif (const auto error = file.isGifv() ? gifs : videos) {\n\t\t\treturn error;\n\t\t}\n\t\tbreak;\n\tcase Type::Music:\n\t\tif (music) {\n\t\t\treturn music;\n\t\t}\n\t\tbreak;\n\tcase Type::File:\n\t\tif (files) {\n\t\t\treturn files;\n\t\t}\n\t\tbreak;\n\t}\n\treturn {};\n}\n\nvoid ShowSendErrorToast(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tData::SendError error) {\n\treturn ShowSendErrorToast(navigation->uiShow(), peer, error);\n}\n\nvoid ShowSendErrorToast(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tData::SendError error) {\n\tif (!error.boostsToLift) {\n\t\tshow->showToast(*error);\n\t\treturn;\n\t}\n\tconst auto boost = [=] {\n\t\tconst auto window = show->resolveWindow();\n\t\twindow->resolveBoostState(peer->asChannel(), error.boostsToLift);\n\t};\n\tshow->showToast({\n\t\t.text = tr::link(*error),\n\t\t.filter = [=](const auto &...) { boost(); return false; },\n\t});\n}\n\nbool ShowSendError(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tconst Ui::PreparedList &list,\n\t\tstd::optional<bool> compress,\n\t\tbool ignoreSlowmodeLeft) {\n\tconst auto error = [&]() -> Data::SendError {\n\t\tconst auto error = Data::FileRestrictionError(peer, list, compress);\n\t\tif (error) {\n\t\t\treturn error;\n\t\t} else if (const auto left = peer->slowmodeSecondsLeft()) {\n\t\t\tif (!ignoreSlowmodeLeft) {\n\t\t\t\treturn tr::lng_slowmode_enabled(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_left,\n\t\t\t\t\tUi::FormatDurationWordsSlowmode(left));\n\t\t\t}\n\t\t}\n\t\tusing Error = Ui::PreparedList::Error;\n\t\tswitch (list.error) {\n\t\tcase Error::None: return QString();\n\t\tcase Error::EmptyFile:\n\t\tcase Error::Directory:\n\t\tcase Error::NonLocalUrl: return tr::lng_send_image_empty(\n\t\t\ttr::now,\n\t\t\tlt_name,\n\t\t\tlist.errorData);\n\t\tcase Error::TooLargeFile: return u\"(toolarge)\"_q;\n\t\t}\n\t\treturn tr::lng_forward_send_files_cant(tr::now);\n\t}();\n\tif (!error) {\n\t\treturn false;\n\t} else if (error.text == u\"(toolarge)\"_q) {\n\t\tconst auto max = ranges::max_element(\n\t\t\tlist.files,\n\t\t\t{},\n\t\t\t&Ui::PreparedFile::size);\n\t\tconst auto session = &show->session();\n\t\tshow->show(Box(FileSizeLimitBox, session, max->size, nullptr));\n\t\treturn true;\n\t}\n\tShowSendErrorToast(show, peer, error);\n\treturn true;\n}\n\nbool ShowSendError(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tconst Ui::PreparedBundle &bundle,\n\t\tbool ignoreSlowmodeLeft) {\n\tif (peer->slowmodeApplied() && bundle.groups.size() > 1) {\n\t\tData::ShowSendErrorToast(\n\t\t\tshow,\n\t\t\tpeer,\n\t\t\ttr::lng_slowmode_no_many(tr::now));\n\t\treturn true;\n\t}\n\tconst auto ignore = ignoreSlowmodeLeft;\n\tconst auto compress = bundle.way.sendImagesAsPhotos();\n\tfor (const auto &group : bundle.groups) {\n\t\tif (ShowSendError(show, peer, group.list, compress, ignore)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_chat_participant_status.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Ui {\nstruct PreparedList;\nstruct PreparedFile;\nstruct PreparedBundle;\n} // namespace Ui\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nenum class ChatAdminRight {\n\tChangeInfo = (1 << 0),\n\tPostMessages = (1 << 1),\n\tEditMessages = (1 << 2),\n\tDeleteMessages = (1 << 3),\n\tBanUsers = (1 << 4),\n\tInviteByLinkOrAdd = (1 << 5),\n\tPinMessages = (1 << 7),\n\tAddAdmins = (1 << 9),\n\tAnonymous = (1 << 10),\n\tManageCall = (1 << 11),\n\tOther = (1 << 12),\n\tManageTopics = (1 << 13),\n\tPostStories = (1 << 14),\n\tEditStories = (1 << 15),\n\tDeleteStories = (1 << 16),\n\tManageDirect = (1 << 17),\n\tManageRanks = (1 << 18),\n};\ninline constexpr bool is_flag_type(ChatAdminRight) { return true; }\nusing ChatAdminRights = base::flags<ChatAdminRight>;\n\nenum class ChatRestriction {\n\tViewMessages = (1 << 0),\n\n\tSendStickers = (1 << 3),\n\tSendGifs = (1 << 4),\n\tSendGames = (1 << 5),\n\tSendInline = (1 << 6),\n\tSendPolls = (1 << 8),\n\tSendPhotos = (1 << 19),\n\tSendVideos = (1 << 20),\n\tSendVideoMessages = (1 << 21),\n\tSendMusic = (1 << 22),\n\tSendVoiceMessages = (1 << 23),\n\tSendFiles = (1 << 24),\n\tSendOther = (1 << 25),\n\n\tEmbedLinks = (1 << 7),\n\n\tChangeInfo = (1 << 10),\n\tAddParticipants = (1 << 15),\n\tPinMessages = (1 << 17),\n\tCreateTopics = (1 << 18),\n\tEditRank = (1 << 26),\n};\ninline constexpr bool is_flag_type(ChatRestriction) { return true; }\nusing ChatRestrictions = base::flags<ChatRestriction>;\n\nstruct ChatAdminRightsInfo {\n\tChatAdminRightsInfo() = default;\n\texplicit ChatAdminRightsInfo(ChatAdminRights flags) : flags(flags) {\n\t}\n\texplicit ChatAdminRightsInfo(const MTPChatAdminRights &rights);\n\n\tChatAdminRights flags;\n};\n\n[[nodiscard]] MTPChatAdminRights AdminRightsToMTP(ChatAdminRightsInfo info);\n\nstruct ChatRestrictionsInfo {\n\tChatRestrictionsInfo() = default;\n\tChatRestrictionsInfo(ChatRestrictions flags, TimeId until)\n\t: flags(flags)\n\t, until(until) {\n\t}\n\texplicit ChatRestrictionsInfo(const MTPChatBannedRights &rights);\n\n\tChatRestrictions flags;\n\tTimeId until = 0;\n};\n\n[[nodiscard]] MTPChatBannedRights RestrictionsToMTP(\n\tChatRestrictionsInfo info);\n\nnamespace Data {\n\nclass Thread;\n\nstruct AdminRightsSetOptions {\n\tbool isGroup : 1 = false;\n\tbool isForum : 1 = false;\n\tbool anyoneCanAddMembers : 1 = false;\n};\n\nstruct RestrictionsSetOptions {\n\tbool isForum = false;\n\tbool isUserSpecific = false;\n};\n\n[[nodiscard]] std::vector<ChatRestrictions> ListOfRestrictions(\n\tRestrictionsSetOptions options);\n\n[[nodiscard]] inline constexpr auto AllSendRestrictionsList() {\n\treturn std::array{\n\t\tChatRestriction::SendOther,\n\t\tChatRestriction::SendStickers,\n\t\tChatRestriction::SendGifs,\n\t\tChatRestriction::SendGames,\n\t\tChatRestriction::SendInline,\n\t\tChatRestriction::SendPolls,\n\t\tChatRestriction::SendPhotos,\n\t\tChatRestriction::SendVideos,\n\t\tChatRestriction::SendVideoMessages,\n\t\tChatRestriction::SendMusic,\n\t\tChatRestriction::SendVoiceMessages,\n\t\tChatRestriction::SendFiles,\n\t};\n}\n[[nodiscard]] inline constexpr auto FilesSendRestrictionsList() {\n\treturn std::array{\n\t\tChatRestriction::SendStickers,\n\t\tChatRestriction::SendGifs,\n\t\tChatRestriction::SendPhotos,\n\t\tChatRestriction::SendVideos,\n\t\tChatRestriction::SendMusic,\n\t\tChatRestriction::SendFiles,\n\t};\n}\n[[nodiscard]] inline constexpr auto TabbedPanelSendRestrictionsList() {\n\treturn std::array{\n\t\tChatRestriction::SendStickers,\n\t\tChatRestriction::SendGifs,\n\t\tChatRestriction::SendOther,\n\t};\n}\n[[nodiscard]] ChatRestrictions AllSendRestrictions();\n[[nodiscard]] ChatRestrictions FilesSendRestrictions();\n[[nodiscard]] ChatRestrictions TabbedPanelSendRestrictions();\n\n[[nodiscard]] bool CanSendAnyOf(\n\tnot_null<const Thread*> thread,\n\tChatRestrictions rights,\n\tbool forbidInForums = true);\n[[nodiscard]] bool CanSendAnyOf(\n\tnot_null<const PeerData*> peer,\n\tChatRestrictions rights,\n\tbool forbidInForums = true);\n\n[[nodiscard]] inline bool CanSend(\n\t\tnot_null<const Thread*> thread,\n\t\tChatRestriction right,\n\t\tbool forbidInForums = true) {\n\treturn CanSendAnyOf(thread, right, forbidInForums);\n}\n[[nodiscard]] inline bool CanSend(\n\t\tnot_null<const PeerData*> peer,\n\t\tChatRestriction right,\n\t\tbool forbidInForums = true) {\n\treturn CanSendAnyOf(peer, right, forbidInForums);\n}\n[[nodiscard]] inline bool CanSendTexts(\n\t\tnot_null<const Thread*> thread,\n\t\tbool forbidInForums = true) {\n\treturn CanSend(thread, ChatRestriction::SendOther, forbidInForums);\n}\n[[nodiscard]] inline bool CanSendTexts(\n\t\tnot_null<const PeerData*> peer,\n\t\tbool forbidInForums = true) {\n\treturn CanSend(peer, ChatRestriction::SendOther, forbidInForums);\n}\n[[nodiscard]] inline bool CanSendAnything(\n\t\tnot_null<const Thread*> thread,\n\t\tbool forbidInForums = true) {\n\treturn CanSendAnyOf(thread, AllSendRestrictions(), forbidInForums);\n}\n[[nodiscard]] inline bool CanSendAnything(\n\t\tnot_null<const PeerData*> peer,\n\t\tbool forbidInForums = true) {\n\treturn CanSendAnyOf(peer, AllSendRestrictions(), forbidInForums);\n}\n\nstruct SendError {\n\tSendError(QString text = QString()) : text(std::move(text)) {\n\t}\n\n\tstruct Args {\n\t\tQString text;\n\t\tint boostsToLift = 0;\n\t\tbool monoforumAdmin = false;\n\t\tbool premiumToLift = false;\n\t\tbool frozen = false;\n\t};\n\tSendError(Args &&args)\n\t: text(std::move(args.text))\n\t, boostsToLift(args.boostsToLift)\n\t, monoforumAdmin(args.monoforumAdmin)\n\t, premiumToLift(args.premiumToLift)\n\t, frozen(args.frozen) {\n\t}\n\n\tQString text;\n\tint boostsToLift = 0;\n\tbool monoforumAdmin = false;\n\tbool premiumToLift = false;\n\tbool frozen = false;\n\n\t[[nodiscard]] SendError value_or(SendError other) const {\n\t\treturn *this ? *this : other;\n\t}\n\n\texplicit operator bool() const {\n\t\treturn monoforumAdmin || !text.isEmpty();\n\t}\n\t[[nodiscard]] bool has_value() const {\n\t\treturn !text.isEmpty();\n\t}\n\t[[nodiscard]] const QString &operator*() const {\n\t\treturn text;\n\t}\n};\n\nstruct SendErrorWithThread {\n\tSendError error;\n\tThread *thread = nullptr;\n};\n\n[[nodiscard]] SendError RestrictionError(\n\tnot_null<PeerData*> peer,\n\tChatRestriction restriction);\n[[nodiscard]] SendError AnyFileRestrictionError(not_null<PeerData*> peer);\n[[nodiscard]] SendError FileRestrictionError(\n\tnot_null<PeerData*> peer,\n\tconst Ui::PreparedList &list,\n\tstd::optional<bool> compress);\n[[nodiscard]] SendError FileRestrictionError(\n\tnot_null<PeerData*> peer,\n\tconst Ui::PreparedFile &file,\n\tstd::optional<bool> compress);\n\nvoid ShowSendErrorToast(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tSendError error);\nvoid ShowSendErrorToast(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<PeerData*> peer,\n\tSendError error);\n\nbool ShowSendError(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<PeerData*> peer,\n\tconst Ui::PreparedList &list,\n\tstd::optional<bool> compress,\n\tbool ignoreSlowmodeLeft = false);\nbool ShowSendError(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<PeerData*> peer,\n\tconst Ui::PreparedBundle &bundle,\n\tbool ignoreSlowmodeLeft = false);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_cloud_file.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_cloud_file.h\"\n\n#include \"data/data_file_origin.h\"\n#include \"data/data_session.h\"\n#include \"storage/cache/storage_cache_database.h\"\n#include \"storage/file_download.h\"\n#include \"ui/image/image.h\"\n#include \"main/main_session.h\"\n\nnamespace Data {\n\nCloudFile::~CloudFile() {\n\t// Destroy loader with still alive CloudFile with already zero '.loader'.\n\t// Otherwise in ~FileLoader it tries to clear file.loader and crashes.\n\tbase::take(loader);\n}\n\nvoid CloudFile::clear() {\n\tlocation = {};\n\tbase::take(loader);\n\tbyteSize = 0;\n\tprogressivePartSize = 0;\n\tflags = {};\n}\n\nCloudImage::CloudImage() = default;\n\nCloudImage::CloudImage(\n\t\tnot_null<Main::Session*> session,\n\t\tconst ImageWithLocation &data) {\n\tupdate(session, data);\n}\n\nvoid CloudImage::set(\n\t\tnot_null<Main::Session*> session,\n\t\tconst ImageWithLocation &data) {\n\tconst auto &was = _file.location.file().data;\n\tconst auto &now = data.location.file().data;\n\tif (!data.location.valid()) {\n\t\t_file.flags |= CloudFile::Flag::Cancelled;\n\t\t_file.loader = nullptr;\n\t\t_file.location = ImageLocation();\n\t\t_file.byteSize = 0;\n\t\t_file.flags = CloudFile::Flag();\n\t\t_view = std::weak_ptr<QImage>();\n\t} else if (was != now\n\t\t&& (!v::is<InMemoryLocation>(was) || v::is<InMemoryLocation>(now))) {\n\t\t_file.location = ImageLocation();\n\t\t_view = std::weak_ptr<QImage>();\n\t}\n\tUpdateCloudFile(\n\t\t_file,\n\t\tdata,\n\t\tsession->data().cache(),\n\t\tkImageCacheTag,\n\t\t[=](FileOrigin origin) { load(session, origin); },\n\t\t[=](QImage preloaded, QByteArray) {\n\t\t\tsetToActive(session, std::move(preloaded));\n\t\t});\n}\n\nvoid CloudImage::update(\n\t\tnot_null<Main::Session*> session,\n\t\tconst ImageWithLocation &data) {\n\tUpdateCloudFile(\n\t\t_file,\n\t\tdata,\n\t\tsession->data().cache(),\n\t\tkImageCacheTag,\n\t\t[=](FileOrigin origin) { load(session, origin); },\n\t\t[=](QImage preloaded, QByteArray) {\n\t\t\tsetToActive(session, std::move(preloaded));\n\t\t});\n}\n\nbool CloudImage::empty() const {\n\treturn !_file.location.valid();\n}\n\nbool CloudImage::loading() const {\n\treturn (_file.loader != nullptr);\n}\n\nbool CloudImage::failed() const {\n\treturn (_file.flags & CloudFile::Flag::Failed);\n}\n\nbool CloudImage::loadedOnce() const {\n\treturn (_file.flags & CloudFile::Flag::Loaded);\n}\n\nvoid CloudImage::load(not_null<Main::Session*> session, FileOrigin origin) {\n\tconst auto autoLoading = false;\n\tconst auto finalCheck = [=] {\n\t\tif (const auto active = activeView()) {\n\t\t\treturn active->isNull();\n\t\t} else if (_file.flags & CloudFile::Flag::Loaded) {\n\t\t\treturn false;\n\t\t}\n\t\treturn !(_file.flags & CloudFile::Flag::Loaded);\n\t};\n\tconst auto done = [=](QImage result, QByteArray) {\n\t\tsetToActive(session, std::move(result));\n\t};\n\tLoadCloudFile(\n\t\tsession,\n\t\t_file,\n\t\torigin,\n\t\tLoadFromCloudOrLocal,\n\t\tautoLoading,\n\t\tkImageCacheTag,\n\t\tfinalCheck,\n\t\tdone);\n}\n\nconst ImageLocation &CloudImage::location() const {\n\treturn _file.location;\n}\n\nint CloudImage::byteSize() const {\n\treturn _file.byteSize;\n}\n\nstd::shared_ptr<QImage> CloudImage::createView() {\n\tif (auto active = activeView()) {\n\t\treturn active;\n\t}\n\tauto view = std::make_shared<QImage>();\n\t_view = view;\n\treturn view;\n}\n\nstd::shared_ptr<QImage> CloudImage::activeView() const {\n\treturn _view.lock();\n}\n\nbool CloudImage::isCurrentView(const std::shared_ptr<QImage> &view) const {\n\tif (!view) {\n\t\treturn empty();\n\t}\n\treturn !view.owner_before(_view) && !_view.owner_before(view);\n}\n\nvoid CloudImage::setToActive(\n\t\tnot_null<Main::Session*> session,\n\t\tQImage image) {\n\tif (const auto view = activeView()) {\n\t\t*view = image.isNull()\n\t\t\t? Image::Empty()->original()\n\t\t\t: std::move(image);\n\t\tsession->notifyDownloaderTaskFinished();\n\t}\n}\n\nvoid UpdateCloudFile(\n\t\tCloudFile &file,\n\t\tconst ImageWithLocation &data,\n\t\tStorage::Cache::Database &cache,\n\t\tuint8 cacheTag,\n\t\tFn<void(FileOrigin)> restartLoader,\n\t\tFn<void(QImage, QByteArray)> usePreloaded) {\n\tif (!data.location.valid()) {\n\t\tif (data.progressivePartSize && !file.location.valid()) {\n\t\t\tfile.progressivePartSize = data.progressivePartSize;\n\t\t}\n\t\tif (data.location.width()\n\t\t\t&& data.location.height()\n\t\t\t&& !file.location.valid()\n\t\t\t&& !file.location.width()) {\n\t\t\tfile.location = data.location;\n\t\t}\n\t\treturn;\n\t}\n\n\tconst auto needStickerThumbnailUpdate = [&] {\n\t\tconst auto was = std::get_if<StorageFileLocation>(\n\t\t\t&file.location.file().data);\n\t\tconst auto now = std::get_if<StorageFileLocation>(\n\t\t\t&data.location.file().data);\n\t\tusing Type = StorageFileLocation::Type;\n\t\tif (!was || !now || was->type() != Type::StickerSetThumb) {\n\t\t\treturn false;\n\t\t}\n\t\treturn now->valid()\n\t\t\t&& (now->type() != Type::StickerSetThumb\n\t\t\t\t|| now->cacheKey() != was->cacheKey());\n\t};\n\tconst auto update = !file.location.valid()\n\t\t|| (data.location.file().cacheKey()\n\t\t\t&& (!file.location.file().cacheKey()\n\t\t\t\t|| (file.location.width() < data.location.width())\n\t\t\t\t|| (file.location.height() < data.location.height())\n\t\t\t\t|| needStickerThumbnailUpdate()));\n\tif (!update) {\n\t\treturn;\n\t}\n\tauto cacheBytes = !data.bytes.isEmpty()\n\t\t? data.bytes\n\t\t: v::is<InMemoryLocation>(file.location.file().data)\n\t\t? v::get<InMemoryLocation>(file.location.file().data).bytes\n\t\t: QByteArray();\n\tif (!cacheBytes.isEmpty()) {\n\t\tif (const auto cacheKey = data.location.file().cacheKey()) {\n\t\t\tcache.putIfEmpty(\n\t\t\t\tcacheKey,\n\t\t\t\tStorage::Cache::Database::TaggedValue(\n\t\t\t\t\tstd::move(cacheBytes),\n\t\t\t\t\tcacheTag));\n\t\t}\n\t}\n\tfile.location = data.location;\n\tfile.byteSize = data.bytesCount;\n\tif (!data.preloaded.isNull()) {\n\t\tfile.loader = nullptr;\n\t\tif (usePreloaded) {\n\t\t\tusePreloaded(data.preloaded, data.bytes);\n\t\t}\n\t} else if (file.loader) {\n\t\tconst auto origin = base::take(file.loader)->fileOrigin();\n\t\trestartLoader(origin);\n\t} else if (file.flags & CloudFile::Flag::Failed) {\n\t\tfile.flags &= ~CloudFile::Flag::Failed;\n\t}\n}\n\nvoid LoadCloudFile(\n\t\tnot_null<Main::Session*> session,\n\t\tCloudFile &file,\n\t\tFileOrigin origin,\n\t\tLoadFromCloudSetting fromCloud,\n\t\tbool autoLoading,\n\t\tuint8 cacheTag,\n\t\tFn<bool()> finalCheck,\n\t\tFn<void(CloudFile&)> done,\n\t\tFn<void(bool)> fail,\n\t\tFn<void()> progress,\n\t\tint downloadFrontPartSize = 0) {\n\tconst auto loadSize = downloadFrontPartSize\n\t\t? std::min(downloadFrontPartSize, file.byteSize)\n\t\t: file.byteSize;\n\tif (file.loader) {\n\t\tif (fromCloud == LoadFromCloudOrLocal) {\n\t\t\tfile.loader->permitLoadFromCloud();\n\t\t}\n\t\tif (file.loader->loadSize() < loadSize) {\n\t\t\tfile.loader->increaseLoadSize(loadSize, autoLoading);\n\t\t}\n\t\treturn;\n\t} else if ((file.flags & CloudFile::Flag::Failed)\n\t\t|| !file.location.valid()\n\t\t|| (finalCheck && !finalCheck())) {\n\t\treturn;\n\t}\n\tfile.flags &= ~CloudFile::Flag::Cancelled;\n\tfile.loader = CreateFileLoader(\n\t\tsession,\n\t\tfile.location.file(),\n\t\torigin,\n\t\tQString(),\n\t\tloadSize,\n\t\tfile.byteSize,\n\t\tUnknownFileLocation,\n\t\tLoadToCacheAsWell,\n\t\tfromCloud,\n\t\tautoLoading,\n\t\tcacheTag);\n\n\tconst auto finish = [done](CloudFile &file) {\n\t\tif (!file.loader || file.loader->cancelled()) {\n\t\t\tfile.flags |= CloudFile::Flag::Cancelled;\n\t\t} else {\n\t\t\tfile.flags |= CloudFile::Flag::Loaded;\n\t\t\tdone(file);\n\t\t}\n\t\t// NB! file.loader may be in ~FileLoader() already.\n\t\tif (const auto loader = base::take(file.loader)) {\n\t\t\tif ((file.flags & CloudFile::Flag::Cancelled)\n\t\t\t\t&& !loader->cancelled()) {\n\t\t\t\tloader->cancel();\n\t\t\t}\n\t\t}\n\t};\n\n\tfile.loader->updates(\n\t) | rpl::on_next_error_done([=] {\n\t\tif (const auto onstack = progress) {\n\t\t\tonstack();\n\t\t}\n\t}, [=, &file](FileLoader::Error error) {\n\t\tfinish(file);\n\t\tfile.flags |= CloudFile::Flag::Failed;\n\t\tif (const auto onstack = fail) {\n\t\t\tonstack(error.started);\n\t\t}\n\t}, [=, &file] {\n\t\tfinish(file);\n\t}, file.loader->lifetime());\n\n\tfile.loader->start();\n}\n\nvoid LoadCloudFile(\n\t\tnot_null<Main::Session*> session,\n\t\tCloudFile &file,\n\t\tFileOrigin origin,\n\t\tLoadFromCloudSetting fromCloud,\n\t\tbool autoLoading,\n\t\tuint8 cacheTag,\n\t\tFn<bool()> finalCheck,\n\t\tFn<void(QImage, QByteArray)> done,\n\t\tFn<void(bool)> fail,\n\t\tFn<void()> progress,\n\t\tint downloadFrontPartSize) {\n\tconst auto callback = [=](CloudFile &file) {\n\t\tif (auto read = file.loader->imageData(); read.isNull()) {\n\t\t\tfile.flags |= CloudFile::Flag::Failed;\n\t\t\tif (const auto onstack = fail) {\n\t\t\t\tonstack(true);\n\t\t\t}\n\t\t} else if (const auto onstack = done) {\n\t\t\tonstack(std::move(read), file.loader->bytes());\n\t\t}\n\t};\n\tLoadCloudFile(\n\t\tsession,\n\t\tfile,\n\t\torigin,\n\t\tfromCloud,\n\t\tautoLoading,\n\t\tcacheTag,\n\t\tfinalCheck,\n\t\tcallback,\n\t\tstd::move(fail),\n\t\tstd::move(progress),\n\t\tdownloadFrontPartSize);\n}\n\nvoid LoadCloudFile(\n\t\tnot_null<Main::Session*> session,\n\t\tCloudFile &file,\n\t\tFileOrigin origin,\n\t\tLoadFromCloudSetting fromCloud,\n\t\tbool autoLoading,\n\t\tuint8 cacheTag,\n\t\tFn<bool()> finalCheck,\n\t\tFn<void(QByteArray)> done,\n\t\tFn<void(bool)> fail,\n\t\tFn<void()> progress) {\n\tconst auto callback = [=](CloudFile &file) {\n\t\tif (auto bytes = file.loader->bytes(); bytes.isEmpty()) {\n\t\t\tfile.flags |= CloudFile::Flag::Failed;\n\t\t\tif (const auto onstack = fail) {\n\t\t\t\tonstack(true);\n\t\t\t}\n\t\t} else if (const auto onstack = done) {\n\t\t\tonstack(std::move(bytes));\n\t\t}\n\t};\n\tLoadCloudFile(\n\t\tsession,\n\t\tfile,\n\t\torigin,\n\t\tfromCloud,\n\t\tautoLoading,\n\t\tcacheTag,\n\t\tfinalCheck,\n\t\tcallback,\n\t\tstd::move(fail),\n\t\tstd::move(progress));\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_cloud_file.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n#include \"ui/image/image.h\"\n#include \"ui/image/image_location.h\"\n\nclass FileLoader;\n\nnamespace Storage {\nnamespace Cache {\nclass Database;\n} // namespace Cache\n} // namespace Storage\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nstruct FileOrigin;\n\nstruct CloudFile final {\n\tenum class Flag : uchar {\n\t\tCancelled = 0x01,\n\t\tFailed = 0x02,\n\t\tLoaded = 0x04,\n\t};\n\tfriend inline constexpr bool is_flag_type(Flag) { return true; };\n\n\t~CloudFile();\n\n\tvoid clear();\n\n\tImageLocation location;\n\tstd::unique_ptr<FileLoader> loader;\n\tint byteSize = 0;\n\tint progressivePartSize = 0;\n\tbase::flags<Flag> flags;\n};\n\nclass CloudImage final {\npublic:\n\tCloudImage();\n\tCloudImage(\n\t\tnot_null<Main::Session*> session,\n\t\tconst ImageWithLocation &data);\n\n\t// This method will replace the location and zero the _view pointer.\n\tvoid set(\n\t\tnot_null<Main::Session*> session,\n\t\tconst ImageWithLocation &data);\n\n\tvoid update(\n\t\tnot_null<Main::Session*> session,\n\t\tconst ImageWithLocation &data);\n\n\t[[nodiscard]] bool empty() const;\n\t[[nodiscard]] bool loading() const;\n\t[[nodiscard]] bool failed() const;\n\t[[nodiscard]] bool loadedOnce() const;\n\tvoid load(not_null<Main::Session*> session, FileOrigin origin);\n\t[[nodiscard]] const ImageLocation &location() const;\n\t[[nodiscard]] int byteSize() const;\n\n\t[[nodiscard]] std::shared_ptr<QImage> createView();\n\t[[nodiscard]] std::shared_ptr<QImage> activeView() const;\n\t[[nodiscard]] bool isCurrentView(\n\t\tconst std::shared_ptr<QImage> &view) const;\n\nprivate:\n\tvoid setToActive(not_null<Main::Session*> session, QImage image);\n\n\tCloudFile _file;\n\tstd::weak_ptr<QImage> _view;\n\n};\n\nvoid UpdateCloudFile(\n\tCloudFile &file,\n\tconst ImageWithLocation &data,\n\tStorage::Cache::Database &cache,\n\tuint8 cacheTag,\n\tFn<void(FileOrigin)> restartLoader,\n\tFn<void(QImage, QByteArray)> usePreloaded = nullptr);\n\nvoid LoadCloudFile(\n\tnot_null<Main::Session*> session,\n\tCloudFile &file,\n\tFileOrigin origin,\n\tLoadFromCloudSetting fromCloud,\n\tbool autoLoading,\n\tuint8 cacheTag,\n\tFn<bool()> finalCheck,\n\tFn<void(QImage, QByteArray)> done,\n\tFn<void(bool)> fail = nullptr,\n\tFn<void()> progress = nullptr,\n\tint downloadFrontPartSize = 0);\n\nvoid LoadCloudFile(\n\tnot_null<Main::Session*> session,\n\tCloudFile &file,\n\tFileOrigin origin,\n\tLoadFromCloudSetting fromCloud,\n\tbool autoLoading,\n\tuint8 cacheTag,\n\tFn<bool()> finalCheck,\n\tFn<void(QByteArray)> done,\n\tFn<void(bool)> fail = nullptr,\n\tFn<void()> progress = nullptr);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_cloud_themes.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_cloud_themes.h\"\n\n#include \"api/api_premium.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/themes/window_theme_preview.h\"\n#include \"window/themes/window_theme_editor_box.h\"\n#include \"window/window_controller.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_document_media.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"media/view/media_view_open_common.h\"\n#include \"lang/lang_keys.h\"\n#include \"apiwrap.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kFirstReloadTimeout = 10 * crl::time(1000);\nconstexpr auto kReloadTimeout = 3600 * crl::time(1000);\nconstexpr auto kGiftThemesLimit = 24;\n\nbool IsTestingColors/* = false*/;\n\n} // namespace\n\nCloudTheme CloudTheme::Parse(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDtheme &data,\n\t\tbool parseSettings) {\n\tconst auto document = data.vdocument();\n\tconst auto paper = [&](const MTPThemeSettings &settings) {\n\t\tconst auto &data = settings.data();\n\t\treturn data.vwallpaper()\n\t\t\t? WallPaper::Create(session, *data.vwallpaper())\n\t\t\t: std::nullopt;\n\t};\n\tconst auto outgoingMessagesColors = [&](\n\t\t\tconst MTPThemeSettings &settings) {\n\t\tauto result = std::vector<QColor>();\n\t\tconst auto &data = settings.data();\n\t\tif (const auto colors = data.vmessage_colors()) {\n\t\t\tfor (const auto &color : colors->v) {\n\t\t\t\tresult.push_back(Ui::ColorFromSerialized(color));\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\tconst auto accentColor = [&](const MTPThemeSettings &settings) {\n\t\tconst auto &data = settings.data();\n\t\treturn Ui::ColorFromSerialized(data.vaccent_color());\n\t};\n\tconst auto outgoingAccentColor = [&](const MTPThemeSettings &settings) {\n\t\tconst auto &data = settings.data();\n\t\treturn Ui::MaybeColorFromSerialized(data.voutbox_accent_color());\n\t};\n\tconst auto basedOnDark = [&](const MTPThemeSettings &settings) {\n\t\tconst auto &data = settings.data();\n\t\treturn data.vbase_theme().match([](const MTPDbaseThemeNight &) {\n\t\t\treturn true;\n\t\t}, [](const MTPDbaseThemeTinted &) {\n\t\t\treturn true;\n\t\t}, [](const auto &) {\n\t\t\treturn false;\n\t\t});\n\t};\n\tconst auto settings = [&] {\n\t\tauto result = base::flat_map<Type, Settings>();\n\t\tconst auto settings = data.vsettings();\n\t\tif (!settings) {\n\t\t\treturn result;\n\t\t}\n\t\tfor (const auto &fields : settings->v) {\n\t\t\tconst auto type = basedOnDark(fields) ? Type::Dark : Type::Light;\n\t\t\tresult.emplace(type, Settings{\n\t\t\t\t.paper = paper(fields),\n\t\t\t\t.accentColor = accentColor(fields),\n\t\t\t\t.outgoingAccentColor = outgoingAccentColor(fields),\n\t\t\t\t.outgoingMessagesColors = outgoingMessagesColors(fields),\n\t\t\t});\n\t\t}\n\t\treturn result;\n\t};\n\treturn {\n\t\t.id = data.vid().v,\n\t\t.accessHash = data.vaccess_hash().v,\n\t\t.slug = qs(data.vslug()),\n\t\t.title = qs(data.vtitle()),\n\t\t.documentId = (document\n\t\t\t? session->data().processDocument(*document)->id\n\t\t\t: DocumentId(0)),\n\t\t.createdBy = data.is_creator() ? session->userId() : UserId(0),\n\t\t.usersCount = data.vinstalls_count().value_or_empty(),\n\t\t.emoticon = qs(data.vemoticon().value_or_empty()),\n\t\t.settings = (parseSettings\n\t\t\t? settings()\n\t\t\t: base::flat_map<Type, Settings>()),\n\t};\n}\n\nCloudTheme CloudTheme::Parse(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDchatThemeUniqueGift &data,\n\t\tbool parseSettings) {\n\tconst auto gift = Api::FromTL(session, data.vgift());\n\tif (!gift || !gift->unique) {\n\t\treturn {};\n\t}\n\tconst auto paper = [&](const MTPThemeSettings &settings) {\n\t\tconst auto &data = settings.data();\n\t\treturn data.vwallpaper()\n\t\t\t? WallPaper::Create(session, *data.vwallpaper())\n\t\t\t: std::nullopt;\n\t};\n\tconst auto basedOnDark = [&](const MTPThemeSettings &settings) {\n\t\tconst auto &data = settings.data();\n\t\treturn data.vbase_theme().match([](const MTPDbaseThemeNight &) {\n\t\t\treturn true;\n\t\t}, [](const MTPDbaseThemeTinted &) {\n\t\t\treturn true;\n\t\t}, [](const auto &) {\n\t\t\treturn false;\n\t\t});\n\t};\n\tconst auto outgoingMessagesColors = [&](\n\t\t\tconst MTPThemeSettings &settings) {\n\t\tauto result = std::vector<QColor>();\n\t\tconst auto &data = settings.data();\n\t\tif (const auto colors = data.vmessage_colors()) {\n\t\t\tfor (const auto &color : colors->v) {\n\t\t\t\tresult.push_back(Ui::ColorFromSerialized(color));\n\t\t\t}\n\t\t//} else if (basedOnDark(settings)) {\n\t\t//\tresult.push_back(gift->unique->backdrop.edgeColor);\n\t\t//} else {\n\t\t//\tresult.push_back(anim::color(\n\t\t//\t\tgift->unique->backdrop.patternColor,\n\t\t//\t\tQColor(255, 255, 255, 255),\n\t\t//\t\t0.75));\n\t\t}\n\t\treturn result;\n\t};\n\tconst auto accentColor = [&](const MTPThemeSettings &settings) {\n\t\tconst auto &data = settings.data();\n\t\treturn Ui::ColorFromSerialized(data.vaccent_color());\n\t};\n\tconst auto outgoingAccentColor = [&](const MTPThemeSettings &settings) {\n\t\tconst auto &data = settings.data();\n\t\treturn Ui::MaybeColorFromSerialized(\n\t\t\tdata.voutbox_accent_color()\n\t\t);// .value_or(gift->unique->backdrop.patternColor);\n\t};\n\tconst auto settings = [&] {\n\t\tauto result = base::flat_map<Type, Settings>();\n\t\tfor (const auto &fields : data.vtheme_settings().v) {\n\t\t\tconst auto type = basedOnDark(fields) ? Type::Dark : Type::Light;\n\t\t\tresult.emplace(type, Settings{\n\t\t\t\t.paper = paper(fields),\n\t\t\t\t.accentColor = accentColor(fields),\n\t\t\t\t.outgoingAccentColor = outgoingAccentColor(fields),\n\t\t\t\t.outgoingMessagesColors = outgoingMessagesColors(fields),\n\t\t\t});\n\t\t}\n\t\treturn result;\n\t};\n\treturn {\n\t\t.id = gift->unique->id,\n\t\t.unique = gift->unique,\n\t\t.settings = (parseSettings\n\t\t\t? settings()\n\t\t\t: base::flat_map<Type, Settings>()),\n\t};\n}\n\nCloudTheme CloudTheme::Parse(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPTheme &data,\n\t\tbool parseSettings) {\n\treturn data.match([&](const MTPDtheme &data) {\n\t\treturn CloudTheme::Parse(session, data, parseSettings);\n\t});\n}\n\nQString CloudThemes::Format() {\n\tstatic const auto kResult = QString::fromLatin1(\"tdesktop\");\n\treturn kResult;\n}\n\nCloudThemes::CloudThemes(not_null<Main::Session*> session)\n: _session(session)\n, _reloadCurrentTimer([=] { reloadCurrent(); }) {\n\tsetupReload();\n}\n\nvoid CloudThemes::setupReload() {\n\tusing namespace Window::Theme;\n\n\tif (needReload()) {\n\t\t_reloadCurrentTimer.callOnce(kFirstReloadTimeout);\n\t}\n\tBackground()->updates(\n\t) | rpl::filter([](const BackgroundUpdate &update) {\n\t\treturn (update.type == BackgroundUpdate::Type::ApplyingTheme);\n\t}) | rpl::map([=] {\n\t\treturn needReload();\n\t}) | rpl::on_next([=](bool need) {\n\t\tinstall();\n\t\tif (need) {\n\t\t\tscheduleReload();\n\t\t} else {\n\t\t\t_reloadCurrentTimer.cancel();\n\t\t}\n\t}, _lifetime);\n}\n\nbool CloudThemes::needReload() const {\n\tconst auto &fields = Window::Theme::Background()->themeObject().cloud;\n\treturn fields.id && fields.documentId;\n}\n\nvoid CloudThemes::install() {\n\tusing namespace Window::Theme;\n\n\tconst auto &fields = Background()->themeObject().cloud;\n\tauto &themeId = IsNightMode()\n\t\t? _installedNightThemeId\n\t\t: _installedDayThemeId;\n\tconst auto cloudId = fields.documentId ? fields.id : uint64(0);\n\tif (themeId == cloudId) {\n\t\treturn;\n\t}\n\tthemeId = cloudId;\n\tusing Flag = MTPaccount_InstallTheme::Flag;\n\tconst auto flags = (IsNightMode() ? Flag::f_dark : Flag(0))\n\t\t| Flag::f_format\n\t\t| (themeId ? Flag::f_theme : Flag(0));\n\t_session->api().request(MTPaccount_InstallTheme(\n\t\tMTP_flags(flags),\n\t\tMTP_inputTheme(MTP_long(cloudId), MTP_long(fields.accessHash)),\n\t\tMTP_string(Format()),\n\t\tMTPBaseTheme()\n\t)).send();\n}\n\nvoid CloudThemes::reloadCurrent() {\n\tif (!needReload()) {\n\t\treturn;\n\t}\n\tconst auto &fields = Window::Theme::Background()->themeObject().cloud;\n\t_session->api().request(MTPaccount_GetTheme(\n\t\tMTP_string(Format()),\n\t\tMTP_inputTheme(MTP_long(fields.id), MTP_long(fields.accessHash))\n\t)).done([=](const MTPTheme &result) {\n\t\tapplyUpdate(result);\n\t}).fail([=] {\n\t\t_reloadCurrentTimer.callOnce(kReloadTimeout);\n\t}).send();\n}\n\nvoid CloudThemes::applyUpdate(const MTPTheme &theme) {\n\ttheme.match([&](const MTPDtheme &data) {\n\t\tconst auto cloud = CloudTheme::Parse(_session, data);\n\t\tconst auto &object = Window::Theme::Background()->themeObject();\n\t\tif ((cloud.id != object.cloud.id)\n\t\t\t|| (cloud.documentId == object.cloud.documentId)\n\t\t\t|| !cloud.documentId) {\n\t\t\treturn;\n\t\t}\n\t\tapplyFromDocument(cloud);\n\t});\n\tscheduleReload();\n}\n\nvoid CloudThemes::resolve(\n\t\tnot_null<Window::Controller*> controller,\n\t\tconst QString &slug,\n\t\tconst FullMsgId &clickFromMessageId) {\n\t_session->api().request(_resolveRequestId).cancel();\n\t_resolveRequestId = _session->api().request(MTPaccount_GetTheme(\n\t\tMTP_string(Format()),\n\t\tMTP_inputThemeSlug(MTP_string(slug))\n\t)).done([=](const MTPTheme &result) {\n\t\tshowPreview(controller, result);\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (error.type() == u\"THEME_FORMAT_INVALID\"_q) {\n\t\t\tcontroller->show(Ui::MakeInformBox(tr::lng_theme_no_desktop()));\n\t\t}\n\t}).send();\n}\n\nvoid CloudThemes::showPreview(\n\t\tnot_null<Window::Controller*> controller,\n\t\tconst MTPTheme &data) {\n\tdata.match([&](const MTPDtheme &data) {\n\t\tshowPreview(controller, CloudTheme::Parse(_session, data));\n\t});\n}\n\nvoid CloudThemes::showPreview(\n\t\tnot_null<Window::Controller*> controller,\n\t\tconst CloudTheme &cloud) {\n\tif (cloud.documentId) {\n\t\tpreviewFromDocument(controller, cloud);\n\t} else if (cloud.createdBy == _session->userId()) {\n\t\tcontroller->show(Box(\n\t\t\tWindow::Theme::CreateForExistingBox,\n\t\t\tcontroller,\n\t\t\tcloud));\n\t} else {\n\t\tcontroller->show(Ui::MakeInformBox(tr::lng_theme_no_desktop()));\n\t}\n}\n\nvoid CloudThemes::applyFromDocument(const CloudTheme &cloud) {\n\tconst auto document = _session->data().document(cloud.documentId);\n\tloadDocumentAndInvoke(_updatingFrom, cloud, document, [=](\n\t\t\tstd::shared_ptr<Data::DocumentMedia> media) {\n\t\tconst auto document = media->owner();\n\t\tauto preview = Window::Theme::PreviewFromFile(\n\t\t\tmedia->bytes(),\n\t\t\tdocument->location().name(),\n\t\t\tcloud);\n\t\tif (preview) {\n\t\t\tWindow::Theme::Apply(std::move(preview));\n\t\t\tWindow::Theme::KeepApplied();\n\t\t}\n\t});\n}\n\nvoid CloudThemes::previewFromDocument(\n\t\tnot_null<Window::Controller*> controller,\n\t\tconst CloudTheme &cloud) {\n\tconst auto sessionController = controller->sessionController();\n\tif (!sessionController) {\n\t\treturn;\n\t}\n\tconst auto document = _session->data().document(cloud.documentId);\n\tloadDocumentAndInvoke(_previewFrom, cloud, document, [=](\n\t\t\tstd::shared_ptr<Data::DocumentMedia> media) {\n\t\tconst auto document = media->owner();\n\t\tusing Open = Media::View::OpenRequest;\n\t\tcontroller->openInMediaView(Open(sessionController, document, cloud));\n\t});\n}\n\nvoid CloudThemes::loadDocumentAndInvoke(\n\t\tLoadingDocument &value,\n\t\tconst CloudTheme &cloud,\n\t\tnot_null<DocumentData*> document,\n\t\tFn<void(std::shared_ptr<Data::DocumentMedia>)> callback) {\n\tconst auto alreadyWaiting = (value.document != nullptr);\n\tif (alreadyWaiting) {\n\t\tvalue.document->cancel();\n\t}\n\tvalue.document = document;\n\tvalue.documentMedia = document->createMediaView();\n\tvalue.document->save(\n\t\tData::FileOriginTheme(cloud.id, cloud.accessHash),\n\t\tQString());\n\tvalue.callback = std::move(callback);\n\tif (value.documentMedia->loaded()) {\n\t\tinvokeForLoaded(value);\n\t\treturn;\n\t}\n\tif (!alreadyWaiting) {\n\t\t_session->downloaderTaskFinished(\n\t\t) | rpl::filter([=, &value] {\n\t\t\treturn value.documentMedia->loaded();\n\t\t}) | rpl::on_next([=, &value] {\n\t\t\tinvokeForLoaded(value);\n\t\t}, value.subscription);\n\t}\n}\n\nvoid CloudThemes::invokeForLoaded(LoadingDocument &value) {\n\tconst auto onstack = std::move(value.callback);\n\tauto media = std::move(value.documentMedia);\n\tvalue = LoadingDocument();\n\tonstack(std::move(media));\n}\n\nvoid CloudThemes::scheduleReload() {\n\tif (needReload()) {\n\t\t_reloadCurrentTimer.callOnce(kReloadTimeout);\n\t} else {\n\t\t_reloadCurrentTimer.cancel();\n\t}\n}\n\nvoid CloudThemes::refresh() {\n\tif (_refreshRequestId) {\n\t\treturn;\n\t}\n\t_refreshRequestId = _session->api().request(MTPaccount_GetThemes(\n\t\tMTP_string(Format()),\n\t\tMTP_long(_hash)\n\t)).done([=](const MTPaccount_Themes &result) {\n\t\t_refreshRequestId = 0;\n\t\tresult.match([&](const MTPDaccount_themes &data) {\n\t\t\t_hash = data.vhash().v;\n\t\t\tparseThemes(data.vthemes().v);\n\t\t\t_updates.fire({});\n\t\t}, [](const MTPDaccount_themesNotModified &) {\n\t\t});\n\t}).fail([=] {\n\t\t_refreshRequestId = 0;\n\t}).send();\n}\n\nvoid CloudThemes::parseThemes(const QVector<MTPTheme> &list) {\n\t_list.clear();\n\t_list.reserve(list.size());\n\tfor (const auto &theme : list) {\n\t\t_list.push_back(CloudTheme::Parse(_session, theme));\n\t}\n\tcheckCurrentTheme();\n}\n\nvoid CloudThemes::refreshChatThemes() {\n\tif (_chatThemesRequestId) {\n\t\treturn;\n\t}\n\t_chatThemesRequestId = _session->api().request(MTPaccount_GetChatThemes(\n\t\tMTP_long(_chatThemesHash)\n\t)).done([=](const MTPaccount_Themes &result) {\n\t\t_chatThemesRequestId = 0;\n\t\tresult.match([&](const MTPDaccount_themes &data) {\n\t\t\t_chatThemesHash = data.vhash().v;\n\t\t\tparseChatThemes(data.vthemes().v);\n\t\t\t_chatThemesUpdates.fire({});\n\t\t}, [](const MTPDaccount_themesNotModified &) {\n\t\t});\n\t}).fail([=] {\n\t\t_chatThemesRequestId = 0;\n\t}).send();\n}\n\nconst std::vector<CloudTheme> &CloudThemes::chatThemes() const {\n\treturn _chatThemes;\n}\n\nrpl::producer<> CloudThemes::chatThemesUpdated() const {\n\treturn _chatThemesUpdates.events();\n}\n\nstd::optional<CloudTheme> CloudThemes::themeForToken(\n\t\tconst QString &token) const {\n\tif (token.startsWith(u\"gift:\"_q)) {\n\t\tconst auto id = QStringView(token).mid(5).toULongLong();\n\t\tconst auto i = _giftThemes.find(id);\n\t\treturn (i != end(_giftThemes))\n\t\t\t? i->second\n\t\t\t: std::optional<CloudTheme>();\n\t}\n\tconst auto emoji = Ui::Emoji::Find(token);\n\tif (!emoji) {\n\t\treturn {};\n\t}\n\tconst auto i = ranges::find(_chatThemes, emoji, [](const CloudTheme &v) {\n\t\treturn Ui::Emoji::Find(v.emoticon);\n\t});\n\treturn (i != end(_chatThemes)) ? std::make_optional(*i) : std::nullopt;\n}\n\nrpl::producer<std::optional<CloudTheme>> CloudThemes::themeForTokenValue(\n\t\tconst QString &token) {\n\tif (token.startsWith(u\"gift:\"_q)) {\n\t\treturn rpl::single(themeForToken(token));\n\t}\n\tconst auto testing = TestingColors();\n\tif (!Ui::Emoji::Find(token)) {\n\t\treturn rpl::single<std::optional<CloudTheme>>(std::nullopt);\n\t} else if (auto result = themeForToken(token)) {\n\t\tif (testing) {\n\t\t\treturn rpl::single(\n\t\t\t\tstd::move(result)\n\t\t\t) | rpl::then(chatThemesUpdated(\n\t\t\t) | rpl::map([=] {\n\t\t\t\treturn themeForToken(token);\n\t\t\t}) | rpl::filter([](const std::optional<CloudTheme> &theme) {\n\t\t\t\treturn theme.has_value();\n\t\t\t}));\n\t\t}\n\t\treturn rpl::single(std::move(result));\n\t}\n\trefreshChatThemes();\n\tconst auto limit = testing ? (1 << 20) : 1;\n\treturn rpl::single<std::optional<CloudTheme>>(\n\t\tstd::nullopt\n\t) | rpl::then(chatThemesUpdated(\n\t) | rpl::map([=] {\n\t\treturn themeForToken(token);\n\t}) | rpl::filter([](const std::optional<CloudTheme> &theme) {\n\t\treturn theme.has_value();\n\t}) | rpl::take(limit));\n}\n\nvoid CloudThemes::myGiftThemesLoadMore(bool reload) {\n\tif (reload && !_myGiftThemesTokens.empty()) {\n\t\t_session->api().request(base::take(_myGiftThemesRequestId)).cancel();\n\t}\n\tif (_myGiftThemesRequestId || (!reload && _myGiftThemesLoaded)) {\n\t\treturn;\n\t}\n\t_myGiftThemesRequestId = _session->api().request(\n\t\tMTPaccount_GetUniqueGiftChatThemes(\n\t\t\tMTP_string(reload ? QString() : _myGiftThemesNextOffset),\n\t\t\tMTP_int(kGiftThemesLimit),\n\t\t\tMTP_long(_myGiftThemesHash))\n\t).done([=](const MTPaccount_ChatThemes &result) {\n\t\t_myGiftThemesRequestId = 0;\n\t\tresult.match([&](const MTPDaccount_chatThemes &data) {\n\t\t\tif (reload || _myGiftThemesTokens.empty()) {\n\t\t\t\t_myGiftThemesHash = data.vhash().v;\n\t\t\t\t_myGiftThemesTokens.clear();\n\t\t\t\t_myGiftThemesLoaded = false;\n\t\t\t}\n\t\t\t_session->data().processUsers(data.vusers());\n\t\t\t_session->data().processChats(data.vchats());\n\t\t\tconst auto &list = data.vthemes().v;\n\t\t\tconst auto got = int(list.size());\n\t\t\t_myGiftThemesTokens.reserve(_myGiftThemesTokens.size() + got);\n\t\t\tfor (const auto &theme : list) {\n\t\t\t\ttheme.match([](const MTPDchatTheme &) {\n\t\t\t\t}, [&](const MTPDchatThemeUniqueGift &data) {\n\t\t\t\t\t_myGiftThemesTokens.push_back(\n\t\t\t\t\t\tprocessGiftThemeGetToken(data));\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (const auto next = data.vnext_offset()) {\n\t\t\t\t_myGiftThemesNextOffset = qs(*next);\n\t\t\t} else {\n\t\t\t\t_myGiftThemesLoaded = true;\n\t\t\t}\n\t\t\t_myGiftThemesUpdates.fire({});\n\t\t}, [&](const MTPDaccount_chatThemesNotModified &) {\n\t\t\tif (!reload) {\n\t\t\t\t_myGiftThemesLoaded = true;\n\t\t\t\t_myGiftThemesUpdates.fire({});\n\t\t\t}\n\t\t});\n\t}).fail([=] {\n\t\t_myGiftThemesRequestId = 0;\n\t\t_myGiftThemesLoaded = true;\n\t}).send();\n}\n\nconst std::vector<QString> &CloudThemes::myGiftThemesTokens() const {\n\treturn _myGiftThemesTokens;\n}\n\nbool CloudThemes::myGiftThemesReady() const {\n\treturn !_myGiftThemesTokens.empty() || _myGiftThemesLoaded;\n}\n\nrpl::producer<> CloudThemes::myGiftThemesUpdated() const {\n\treturn _myGiftThemesUpdates.events();\n}\n\nQString CloudThemes::processGiftThemeGetToken(\n\t\tconst MTPDchatThemeUniqueGift &data) {\n\tauto parsed = CloudTheme::Parse(_session, data, true);\n\tif (parsed.unique) {\n\t\tconst auto id = parsed.unique->id;\n\t\t_giftThemes[id] = std::move(parsed);\n\t\treturn u\"gift:%1\"_q.arg(id);\n\t}\n\treturn QString();\n}\n\nvoid CloudThemes::refreshChatThemesFor(const QString &token) {\n\tif (token.startsWith(u\"gift:\"_q)) {\n\t\treturn;\n\t}\n\trefreshChatThemes();\n}\n\nbool CloudThemes::TestingColors() {\n\treturn IsTestingColors;\n}\n\nvoid CloudThemes::SetTestingColors(bool testing) {\n\tIsTestingColors = testing;\n}\n\nQString CloudThemes::prepareTestingLink(const CloudTheme &theme) const {\n\tconst auto hex = [](int value) {\n\t\treturn QChar((value < 10) ? ('0' + value) : ('a' + (value - 10)));\n\t};\n\tconst auto hex2 = [&](int value) {\n\t\treturn QString() + hex(value / 16) + hex(value % 16);\n\t};\n\tconst auto color = [&](const QColor &color) {\n\t\treturn hex2(color.red()) + hex2(color.green()) + hex2(color.blue());\n\t};\n\tconst auto colors = [&](const std::vector<QColor> &colors) {\n\t\tauto list = QStringList();\n\t\tfor (const auto &c : colors) {\n\t\t\tlist.push_back(color(c));\n\t\t}\n\t\treturn list.join(\",\");\n\t};\n\tauto arguments = QStringList();\n\tfor (const auto &[type, settings] : theme.settings) {\n\t\tconst auto add = [&, type = type](const QString &value) {\n\t\t\tconst auto prefix = (type == CloudTheme::Type::Dark)\n\t\t\t\t? u\"dark_\"_q\n\t\t\t\t: u\"\"_q;\n\t\t\targuments.push_back(prefix + value);\n\t\t};\n\t\tadd(\"accent=\" + color(settings.accentColor));\n\t\tif (settings.paper && !settings.paper->backgroundColors().empty()) {\n\t\t\tadd(\"bg=\" + colors(settings.paper->backgroundColors()));\n\t\t}\n\t\tif (settings.paper/* && settings.paper->hasShareUrl()*/) {\n\t\t\tadd(\"intensity=\"\n\t\t\t\t+ QString::number(settings.paper->patternIntensity()));\n\t\t\t//const auto url = settings.paper->shareUrl(_session);\n\t\t\t//const auto from = url.indexOf(\"bg/\");\n\t\t\t//const auto till = url.indexOf(\"?\");\n\t\t\t//if (from > 0 && till > from) {\n\t\t\t//\tadd(\"slug=\" + url.mid(from + 3, till - from - 3));\n\t\t\t//}\n\t\t}\n\t\tif (settings.outgoingAccentColor) {\n\t\t\tadd(\"out_accent\" + color(*settings.outgoingAccentColor));\n\t\t}\n\t\tif (!settings.outgoingMessagesColors.empty()) {\n\t\t\tadd(\"out_bg=\" + colors(settings.outgoingMessagesColors));\n\t\t}\n\t}\n\treturn arguments.isEmpty()\n\t\t? QString()\n\t\t: (\"tg://test_chat_theme?\" + arguments.join(\"&\"));\n}\n\nstd::optional<CloudTheme> CloudThemes::updateThemeFromLink(\n\t\tconst QString &emoticon,\n\t\tconst QMap<QString, QString> &params) {\n\tconst auto emoji = Ui::Emoji::Find(emoticon);\n\tif (!TestingColors() || !emoji) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto i = ranges::find(_chatThemes, emoji, [](const CloudTheme &v) {\n\t\treturn Ui::Emoji::Find(v.emoticon);\n\t});\n\tif (i == end(_chatThemes)) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto hex = [](const QString &value) {\n\t\treturn (value.size() != 1)\n\t\t\t? std::nullopt\n\t\t\t: (value[0] >= 'a' && value[0] <= 'f')\n\t\t\t? std::make_optional(10 + int(value[0].unicode() - 'a'))\n\t\t\t: (value[0] >= 'A' && value[0] <= 'F')\n\t\t\t? std::make_optional(10 + int(value[0].unicode() - 'A'))\n\t\t\t: (value[0] >= '0' && value[0] <= '9')\n\t\t\t? std::make_optional(int(value[0].unicode() - '0'))\n\t\t\t: std::nullopt;\n\t};\n\tconst auto hex2 = [&](const QString &value) {\n\t\tconst auto first = hex(value.mid(0, 1));\n\t\tconst auto second = hex(value.mid(1, 1));\n\t\treturn (first && second)\n\t\t\t? std::make_optional((*first) * 16 + (*second))\n\t\t\t: std::nullopt;\n\t};\n\tconst auto color = [&](const QString &value) {\n\t\tconst auto red = hex2(value.mid(0, 2));\n\t\tconst auto green = hex2(value.mid(2, 2));\n\t\tconst auto blue = hex2(value.mid(4, 2));\n\t\treturn (red && green && blue)\n\t\t\t? std::make_optional(QColor(*red, *green, *blue))\n\t\t\t: std::nullopt;\n\t};\n\tconst auto colors = [&](const QString &value) {\n\t\tauto list = value.split(\",\");\n\t\tauto result = std::vector<QColor>();\n\t\tfor (const auto &single : list) {\n\t\t\tif (const auto c = color(single)) {\n\t\t\t\tresult.push_back(*c);\n\t\t\t} else {\n\t\t\t\treturn std::vector<QColor>();\n\t\t\t}\n\t\t}\n\t\treturn (result.size() > 4) ? std::vector<QColor>() : result;\n\t};\n\n\tconst auto parse = [&](CloudThemeType type, const QString &prefix = {}) {\n\t\tconst auto accent = color(params[\"accent\"]);\n\t\tif (!accent) {\n\t\t\treturn;\n\t\t}\n\t\tauto &settings = i->settings[type];\n\t\tsettings.accentColor = *accent;\n\t\tconst auto bg = colors(params[\"bg\"]);\n\t\tsettings.paper = (settings.paper && !bg.empty())\n\t\t\t? std::make_optional(settings.paper->withBackgroundColors(bg))\n\t\t\t: settings.paper;\n\t\tsettings.paper = (settings.paper && params[\"intensity\"].toInt())\n\t\t\t? std::make_optional(\n\t\t\t\tsettings.paper->withPatternIntensity(\n\t\t\t\t\tparams[\"intensity\"].toInt()))\n\t\t\t: settings.paper;\n\t\tsettings.outgoingAccentColor = color(params[\"out_accent\"]);\n\t\tsettings.outgoingMessagesColors = colors(params[\"out_bg\"]);\n\t};\n\tif (params.contains(\"dark_accent\")) {\n\t\tparse(CloudThemeType::Dark, \"dark_\");\n\t}\n\tif (params.contains(\"accent\")) {\n\t\tparse(params[\"dark\"].isEmpty()\n\t\t\t? CloudThemeType::Light\n\t\t\t: CloudThemeType::Dark);\n\t}\n\t_chatThemesUpdates.fire({});\n\treturn *i;\n}\n\nvoid CloudThemes::parseChatThemes(const QVector<MTPTheme> &list) {\n\t_chatThemes.clear();\n\t_chatThemes.reserve(list.size());\n\tfor (const auto &theme : list) {\n\t\t_chatThemes.push_back(CloudTheme::Parse(_session, theme, true));\n\t}\n}\n\nvoid CloudThemes::checkCurrentTheme() {\n\tconst auto &object = Window::Theme::Background()->themeObject();\n\tif (!object.cloud.id || !object.cloud.documentId) {\n\t\treturn;\n\t}\n\tconst auto i = ranges::find(_list, object.cloud.id, &CloudTheme::id);\n\tif (i == end(_list)) {\n\t\tinstall();\n\t}\n}\n\nrpl::producer<> CloudThemes::updated() const {\n\treturn _updates.events();\n}\n\nconst std::vector<CloudTheme> &CloudThemes::list() const {\n\treturn _list;\n}\n\nvoid CloudThemes::savedFromEditor(const CloudTheme &theme) {\n\tconst auto i = ranges::find(_list, theme.id, &CloudTheme::id);\n\tif (i != end(_list)) {\n\t\t*i = theme;\n\t\t_updates.fire({});\n\t} else {\n\t\t_list.insert(begin(_list), theme);\n\t\t_updates.fire({});\n\t}\n}\n\nvoid CloudThemes::remove(uint64 cloudThemeId) {\n\tconst auto i = ranges::find(_list, cloudThemeId, &CloudTheme::id);\n\tif (i == end(_list)) {\n\t\treturn;\n\t}\n\t_session->api().request(MTPaccount_SaveTheme(\n\t\tMTP_inputTheme(\n\t\t\tMTP_long(i->id),\n\t\t\tMTP_long(i->accessHash)),\n\t\tMTP_bool(true)\n\t)).send();\n\t_list.erase(i);\n\t_updates.fire({});\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_cloud_themes.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"data/data_wall_paper.h\"\n\nclass DocumentData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass Controller;\n} // namespace Window\n\nnamespace Data {\n\nstruct UniqueGift;\nclass DocumentMedia;\n\nenum class CloudThemeType {\n\tDark,\n\tLight,\n};\n\nstruct CloudTheme {\n\tuint64 id = 0;\n\tuint64 accessHash = 0;\n\tQString slug;\n\tQString title;\n\tDocumentId documentId = 0;\n\tUserId createdBy = 0;\n\tint usersCount = 0;\n\tQString emoticon;\n\tstd::shared_ptr<UniqueGift> unique;\n\n\tusing Type = CloudThemeType;\n\tstruct Settings {\n\t\tstd::optional<WallPaper> paper;\n\t\tQColor accentColor;\n\t\tstd::optional<QColor> outgoingAccentColor;\n\t\tstd::vector<QColor> outgoingMessagesColors;\n\t};\n\tbase::flat_map<Type, Settings> settings;\n\n\tstatic CloudTheme Parse(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDtheme &data,\n\t\tbool parseSettings = false);\n\tstatic CloudTheme Parse(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDchatThemeUniqueGift &data,\n\t\tbool parseSettings = false);\n\tstatic CloudTheme Parse(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPTheme &data,\n\t\tbool parseSettings = false);\n};\n\nclass CloudThemes final {\npublic:\n\texplicit CloudThemes(not_null<Main::Session*> session);\n\n\t[[nodiscard]] static QString Format();\n\n\tvoid refresh();\n\t[[nodiscard]] rpl::producer<> updated() const;\n\t[[nodiscard]] const std::vector<CloudTheme> &list() const;\n\tvoid savedFromEditor(const CloudTheme &data);\n\tvoid remove(uint64 cloudThemeId);\n\n\tvoid refreshChatThemes();\n\t[[nodiscard]] const std::vector<CloudTheme> &chatThemes() const;\n\t[[nodiscard]] rpl::producer<> chatThemesUpdated() const;\n\n\tvoid myGiftThemesLoadMore(bool reload = false);\n\t[[nodiscard]] const std::vector<QString> &myGiftThemesTokens() const;\n\t[[nodiscard]] rpl::producer<> myGiftThemesUpdated() const;\n\t[[nodiscard]] QString processGiftThemeGetToken(\n\t\tconst MTPDchatThemeUniqueGift &data);\n\t[[nodiscard]] bool myGiftThemesReady() const;\n\n\tvoid refreshChatThemesFor(const QString &token);\n\t[[nodiscard]] std::optional<CloudTheme> themeForToken(\n\t\tconst QString &token) const;\n\t[[nodiscard]] auto themeForTokenValue(const QString &token)\n\t\t-> rpl::producer<std::optional<CloudTheme>>;\n\n\t[[nodiscard]] static bool TestingColors();\n\tstatic void SetTestingColors(bool testing);\n\t[[nodiscard]] QString prepareTestingLink(const CloudTheme &theme) const;\n\t[[nodiscard]] std::optional<CloudTheme> updateThemeFromLink(\n\t\tconst QString &emoticon,\n\t\tconst QMap<QString, QString> &params);\n\n\tvoid applyUpdate(const MTPTheme &theme);\n\n\tvoid resolve(\n\t\tnot_null<Window::Controller*> controller,\n\t\tconst QString &slug,\n\t\tconst FullMsgId &clickFromMessageId);\n\tvoid showPreview(\n\t\tnot_null<Window::Controller*> controller,\n\t\tconst MTPTheme &data);\n\tvoid showPreview(\n\t\tnot_null<Window::Controller*> controller,\n\t\tconst CloudTheme &cloud);\n\tvoid applyFromDocument(const CloudTheme &cloud);\n\nprivate:\n\tstruct LoadingDocument {\n\t\tCloudTheme theme;\n\t\tDocumentData *document = nullptr;\n\t\tstd::shared_ptr<Data::DocumentMedia> documentMedia;\n\t\trpl::lifetime subscription;\n\t\tFn<void(std::shared_ptr<Data::DocumentMedia>)> callback;\n\t};\n\n\tvoid parseThemes(const QVector<MTPTheme> &list);\n\tvoid checkCurrentTheme();\n\n\tvoid install();\n\tvoid setupReload();\n\t[[nodiscard]] bool needReload() const;\n\tvoid scheduleReload();\n\tvoid reloadCurrent();\n\tvoid previewFromDocument(\n\t\tnot_null<Window::Controller*> controller,\n\t\tconst CloudTheme &cloud);\n\tvoid loadDocumentAndInvoke(\n\t\tLoadingDocument &value,\n\t\tconst CloudTheme &cloud,\n\t\tnot_null<DocumentData*> document,\n\t\tFn<void(std::shared_ptr<Data::DocumentMedia>)> callback);\n\tvoid invokeForLoaded(LoadingDocument &value);\n\n\tvoid parseChatThemes(const QVector<MTPTheme> &list);\n\n\tconst not_null<Main::Session*> _session;\n\tuint64 _hash = 0;\n\tmtpRequestId _refreshRequestId = 0;\n\tmtpRequestId _resolveRequestId = 0;\n\tstd::vector<CloudTheme> _list;\n\trpl::event_stream<> _updates;\n\n\tuint64 _chatThemesHash = 0;\n\tmtpRequestId _chatThemesRequestId = 0;\n\tstd::vector<CloudTheme> _chatThemes;\n\trpl::event_stream<> _chatThemesUpdates;\n\n\tmtpRequestId _myGiftThemesRequestId = 0;\n\tbase::flat_map<uint64, CloudTheme> _giftThemes;\n\tstd::vector<QString> _myGiftThemesTokens;\n\trpl::event_stream<> _myGiftThemesUpdates;\n\tuint64 _myGiftThemesHash = 0;\n\tQString _myGiftThemesNextOffset;\n\tbool _myGiftThemesLoaded = false;\n\n\tbase::Timer _reloadCurrentTimer;\n\tLoadingDocument _updatingFrom;\n\tLoadingDocument _previewFrom;\n\tuint64 _installedDayThemeId = 0;\n\tuint64 _installedNightThemeId = 0;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_credits.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_subscriptions.h\"\n\nnamespace Data {\n\nstruct UniqueGift;\nstruct UniqueGiftValue;\n\nstruct CreditTopupOption final {\n\tuint64 credits = 0;\n\tQString product;\n\tQString currency;\n\tuint64 amount = 0;\n\tbool extended = false;\n\tuint64 giftBarePeerId = 0;\n};\n\nusing CreditTopupOptions = std::vector<CreditTopupOption>;\n\nenum class CreditsHistoryMediaType {\n\tPhoto,\n\tVideo,\n};\n\nstruct CreditsHistoryMedia {\n\tCreditsHistoryMediaType type = CreditsHistoryMediaType::Photo;\n\tuint64 id = 0;\n};\n\nstruct CreditsHistoryEntry final {\n\texplicit operator bool() const {\n\t\treturn !id.isEmpty();\n\t}\n\n\t[[nodiscard]] bool isLiveStoryReaction() const {\n\t\treturn paidMessagesCount && reaction && !bareMsgId;\n\t}\n\n\tusing PhotoId = uint64;\n\tenum class PeerType {\n\t\tPeer,\n\t\tAppStore,\n\t\tPlayMarket,\n\t\tFragment,\n\t\tUnsupported,\n\t\tPremiumBot,\n\t\tAds,\n\t\tAPI,\n\t};\n\n\tQString id;\n\tQString title;\n\tTextWithEntities description;\n\tQDateTime date;\n\tQDateTime firstSaleDate;\n\tQDateTime lastSaleDate;\n\tPhotoId photoId = 0;\n\tstd::vector<CreditsHistoryMedia> extended;\n\tCreditsAmount credits;\n\tuint64 bareMsgId = 0;\n\tuint64 barePeerId = 0;\n\tuint64 bareGiveawayMsgId = 0;\n\tuint64 bareGiftStickerId = 0;\n\tuint64 bareGiftOwnerId = 0;\n\tuint64 bareGiftHostId = 0;\n\tuint64 bareGiftReleasedById = 0;\n\tuint64 bareGiftResaleRecipientId = 0;\n\tuint64 bareActorId = 0;\n\tuint64 bareEntryOwnerId = 0;\n\tuint64 giftChannelSavedId = 0;\n\tuint64 stargiftId = 0;\n\tQString giftPrepayUpgradeHash;\n\tQString giftTitle;\n\tstd::shared_ptr<UniqueGift> uniqueGift;\n\tFn<std::vector<CreditsHistoryEntry>()> pinnedSavedGifts;\n\tuint64 nextToUpgradeStickerId = 0;\n\tFn<void()> nextToUpgradeShow;\n\tFn<void()> craftAnotherCallback;\n\tCreditsAmount starrefAmount;\n\tint starrefCommission = 0;\n\tuint64 starrefRecipientId = 0;\n\tPeerType peerType;\n\tQDateTime subscriptionUntil;\n\n\t// Currency properties.\n\tQDateTime adsProceedsToDate;\n\tQString provider; // Unused.\n\n\tQDateTime successDate;\n\tQString successLink;\n\tint paidMessagesCount = 0;\n\tCreditsAmount paidMessagesAmount;\n\tint paidMessagesCommission = 0;\n\tint limitedCount = 0;\n\tint limitedLeft = 0;\n\tint starsConverted = 0;\n\tint starsToUpgrade = 0;\n\tint starsUpgradedBySender = 0;\n\tint starsForDetailsRemove = 0;\n\tint premiumMonthsForStars = 0;\n\tint floodSkip = 0;\n\tint giftNumber = 0;\n\tbool converted : 1 = false;\n\tbool anonymous : 1 = false;\n\tbool stargift : 1 = false;\n\tbool auction : 1 = false;\n\tbool postsSearch : 1 = false;\n\tbool giftTransferred : 1 = false;\n\tbool giftRefunded : 1 = false;\n\tbool giftUpgraded : 1 = false;\n\tbool giftUpgradeSeparate : 1 = false;\n\tbool giftUpgradeGifted : 1 = false;\n\tbool giftResale : 1 = false;\n\tbool giftResaleForceTon : 1 = false;\n\tbool giftPinned : 1 = false;\n\tbool giftCrafted : 1 = false;\n\tbool savedToProfile : 1 = false;\n\tbool fromGiftsList : 1 = false;\n\tbool fromGiftSlug : 1 = false;\n\tbool soldOutInfo : 1 = false;\n\tbool canUpgradeGift : 1 = false;\n\tbool hasGiftComment : 1 = false;\n\tbool reaction : 1 = false;\n\tbool refunded : 1 = false;\n\tbool pending : 1 = false;\n\tbool failed : 1 = false;\n\tbool in : 1 = false;\n\tbool gift : 1 = false;\n};\n\nstruct CreditsStatusSlice final {\n\tusing OffsetToken = QString;\n\tstd::vector<CreditsHistoryEntry> list;\n\tstd::vector<SubscriptionEntry> subscriptions;\n\tCreditsAmount balance;\n\tuint64 subscriptionsMissingBalance = 0;\n\tbool allLoaded = false;\n\tOffsetToken token;\n\tOffsetToken tokenSubscriptions;\n};\n\nstruct CreditsGiveawayOption final {\n\tstruct Winner final {\n\t\tint users = 0;\n\t\tuint64 perUserStars = 0;\n\t\tbool isDefault = false;\n\t};\n\tstd::vector<Winner> winners;\n\tQString storeProduct;\n\tQString currency;\n\tuint64 amount = 0;\n\tuint64 credits = 0;\n\tint yearlyBoosts = 0;\n\tbool isExtended = false;\n\tbool isDefault = false;\n};\n\nusing CreditsGiveawayOptions = std::vector<CreditsGiveawayOption>;\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_credits_earn.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"core/credits_amount.h\"\n#include \"data/data_statistics_chart.h\"\n\n#include <QtCore/QDateTime>\n\nnamespace Data {\n\nstruct CreditsEarnStatistics final {\n\texplicit operator bool() const {\n\t\treturn usdRate\n\t\t\t&& currentBalance\n\t\t\t&& availableBalance\n\t\t\t&& overallRevenue;\n\t}\n\tData::StatisticalGraph revenueGraph;\n\tCreditsAmount currentBalance;\n\tCreditsAmount availableBalance;\n\tCreditsAmount overallRevenue;\n\tfloat64 usdRate = 0.;\n\tbool isWithdrawalEnabled = false;\n\tQDateTime nextWithdrawalAt;\n\tQString buyAdsUrl;\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_document.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_document.h\"\n\n#include \"data/data_document_resolver.h\"\n#include \"data/data_session.h\"\n#include \"data/data_streaming.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_reply_preview.h\"\n#include \"data/data_web_page.h\"\n#include \"lang/lang_keys.h\"\n#include \"inline_bots/inline_bot_layout_item.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"core/file_utilities.h\"\n#include \"core/mime_type.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"media/streaming/media_streaming_loader_mtproto.h\"\n#include \"media/streaming/media_streaming_loader_local.h\"\n#include \"storage/localstorage.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/streamed_file_downloader.h\"\n#include \"storage/file_download_mtproto.h\"\n#include \"storage/file_download_web.h\"\n#include \"base/options.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/media/history_view_gif.h\"\n#include \"window/window_session_controller.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"base/base_file_utilities.h\"\n#include \"mainwindow.h\"\n#include \"core/application.h\"\n#include \"lottie/lottie_animation.h\"\n#include \"boxes/abstract_box.h\" // Ui::hideLayer().\n\n#include <QtCore/QBuffer>\n#include <QtCore/QMimeType>\n#include <QtCore/QMimeDatabase>\n\nnamespace {\n\nconstexpr auto kDefaultCoverThumbnailSize = 100;\nconstexpr auto kMaxAllowedPreloadPrefix = 6 * 1024 * 1024;\nconstexpr auto kDefaultWebmEmojiSize = 100;\nconstexpr auto kDefaultWebmStickerLargerSize = kStickerSideSize;\n\nconst auto kLottieStickerDimensions = QSize(\n\tkStickerSideSize,\n\tkStickerSideSize);\n\nQString JoinStringList(const QStringList &list, const QString &separator) {\n\tconst auto count = list.size();\n\tif (!count) {\n\t\treturn QString();\n\t}\n\n\tauto result = QString();\n\tauto fullsize = separator.size() * (count - 1);\n\tfor (const auto &string : list) {\n\t\tfullsize += string.size();\n\t}\n\tresult.reserve(fullsize);\n\tresult.append(list[0]);\n\tfor (auto i = 1; i != count; ++i) {\n\t\tresult.append(separator).append(list[i]);\n\t}\n\treturn result;\n}\n\nvoid UpdateStickerSetIdentifier(\n\t\tStickerSetIdentifier &now,\n\t\tconst MTPInputStickerSet &from) {\n\tnow = from.match([&](const MTPDinputStickerSetID &data) {\n\t\treturn StickerSetIdentifier{\n\t\t\t.id = data.vid().v,\n\t\t\t.accessHash = data.vaccess_hash().v,\n\t\t};\n\t}, [](const auto &) {\n\t\treturn StickerSetIdentifier();\n\t});\n}\n\n} // namespace\n\nQString FileNameUnsafe(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &title,\n\t\tconst QString &filter,\n\t\tconst QString &prefix,\n\t\tQString name,\n\t\tbool savingAs,\n\t\tconst QDir &dir) {\n\tname = base::FileNameFromUserString(name);\n\tif (Core::App().settings().askDownloadPath() || savingAs) {\n\t\tif (!name.isEmpty() && name.at(0) == QChar::fromLatin1('.')) {\n\t\t\tname = filedialogDefaultName(prefix, name);\n\t\t} else if (dir.path() != u\".\"_q) {\n\t\t\tQString path = dir.absolutePath();\n\t\t\tif (path != cDialogLastPath()) {\n\t\t\t\tcSetDialogLastPath(path);\n\t\t\t\tLocal::writeSettings();\n\t\t\t}\n\t\t}\n\n\t\t// check if extension of filename is present in filter\n\t\t// it should be in first filter section on the first place\n\t\t// place it there, if it is not\n\t\tQString ext = QFileInfo(name).suffix(), fil = filter, sep = u\";;\"_q;\n\t\tif (!ext.isEmpty()) {\n\t\t\tif (QRegularExpression(u\"^[a-zA-Z_0-9]+$\"_q).match(ext).hasMatch()) {\n\t\t\t\tQStringList filters = filter.split(sep);\n\t\t\t\tif (filters.size() > 1) {\n\t\t\t\t\tconst auto &first = filters.at(0);\n\t\t\t\t\tint32 start = first.indexOf(u\"(*.\"_q);\n\t\t\t\t\tif (start >= 0) {\n\t\t\t\t\t\tif (!QRegularExpression(u\"\\\\(\\\\*\\\\.\"_q + ext + u\"[\\\\)\\\\s]\"_q, QRegularExpression::CaseInsensitiveOption).match(first).hasMatch()) {\n\t\t\t\t\t\t\tQRegularExpressionMatch m = QRegularExpression(u\" \\\\*\\\\.\"_q + ext + u\"[\\\\)\\\\s]\"_q, QRegularExpression::CaseInsensitiveOption).match(first);\n\t\t\t\t\t\t\tif (m.hasMatch() && m.capturedStart() > start + 3) {\n\t\t\t\t\t\t\t\tint32 oldpos = m.capturedStart(), oldend = m.capturedEnd();\n\t\t\t\t\t\t\t\tfil = first.mid(0, start + 3) + ext + u\" *.\"_q + first.mid(start + 3, oldpos - start - 3) + first.mid(oldend - 1) + sep + JoinStringList(filters.mid(1), sep);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tfil = first.mid(0, start + 3) + ext + u\" *.\"_q + first.mid(start + 3) + sep + JoinStringList(filters.mid(1), sep);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfil = QString();\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tfil = QString();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfil = QString();\n\t\t\t}\n\t\t}\n\t\treturn filedialogGetSaveFile(name, title, fil, name) ? name : QString();\n\t}\n\n\tauto path = [&] {\n\t\tconst auto path = Core::App().settings().downloadPath();\n\t\tif (path.isEmpty()) {\n\t\t\treturn File::DefaultDownloadPath(session);\n\t\t} else if (path == FileDialog::Tmp()) {\n\t\t\treturn session->local().tempDirectory();\n\t\t} else {\n\t\t\treturn path;\n\t\t}\n\t}();\n\tif (path.isEmpty()) return QString();\n\tif (name.isEmpty()) name = u\".unknown\"_q;\n\tif (name.at(0) == QChar::fromLatin1('.')) {\n\t\tif (!QDir().exists(path)) QDir().mkpath(path);\n\t\treturn filedialogDefaultName(prefix, name, path);\n\t}\n\tif (dir.path() != u\".\"_q) {\n\t\tpath = dir.absolutePath() + '/';\n\t}\n\n\tQString nameStart, extension;\n\tint32 extPos = name.lastIndexOf('.');\n\tif (extPos >= 0) {\n\t\tnameStart = name.mid(0, extPos);\n\t\textension = name.mid(extPos);\n\t} else {\n\t\tnameStart = name;\n\t}\n\tQString nameBase = path + nameStart;\n\tname = nameBase + extension;\n\tfor (int i = 0; QFileInfo::exists(name); ++i) {\n\t\tname = nameBase + u\" (%1)\"_q.arg(i + 2) + extension;\n\t}\n\n\tif (!QDir().exists(path)) QDir().mkpath(path);\n\treturn name;\n}\n\nQString FileNameForSave(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &title,\n\t\tconst QString &filter,\n\t\tconst QString &prefix,\n\t\tQString name,\n\t\tbool savingAs,\n\t\tconst QDir &dir) {\n\tconst auto result = FileNameUnsafe(\n\t\tsession,\n\t\ttitle,\n\t\tfilter,\n\t\tprefix,\n\t\tname,\n\t\tsavingAs,\n\t\tdir);\n#ifdef Q_OS_WIN\n\tconst auto lower = result.trimmed().toLower();\n\tconst auto kBadExtensions = { u\".lnk\"_q, u\".scf\"_q };\n\tconst auto kMaskExtension = u\".download\"_q;\n\tfor (const auto extension : kBadExtensions) {\n\t\tif (lower.endsWith(extension)) {\n\t\t\treturn result + kMaskExtension;\n\t\t}\n\t}\n#endif // Q_OS_WIN\n\treturn result;\n}\n\nQString DocumentFileNameForSave(\n\t\tnot_null<const DocumentData*> data,\n\t\tbool forceSavingAs,\n\t\tconst QString &already,\n\t\tconst QDir &dir) {\n\tauto alreadySavingFilename = data->loadingFilePath();\n\tif (!alreadySavingFilename.isEmpty()) {\n\t\treturn alreadySavingFilename;\n\t}\n\n\tQString name, filter, caption, prefix;\n\tconst auto mimeType = Core::MimeTypeForName(data->mimeString());\n\tQStringList p = mimeType.globPatterns();\n\tQString pattern = p.isEmpty() ? QString() : p.front();\n\tif (data->isVoiceMessage()) {\n\t\tauto mp3 = data->hasMimeType(u\"audio/mp3\"_q);\n\t\tname = already.isEmpty() ? (mp3 ? u\".mp3\"_q : u\".ogg\"_q) : already;\n\t\tfilter = mp3 ? u\"MP3 Audio (*.mp3);;\"_q : u\"OGG Opus Audio (*.ogg);;\"_q;\n\t\tfilter += FileDialog::AllFilesFilter();\n\t\tcaption = tr::lng_save_audio(tr::now);\n\t\tprefix = u\"audio\"_q;\n\t} else if (data->isVideoFile()) {\n\t\tname = already.isEmpty() ? data->filename() : already;\n\t\tif (name.isEmpty()) {\n\t\t\tname = pattern.isEmpty() ? u\".mov\"_q : pattern.replace('*', QString());\n\t\t}\n\t\tif (pattern.isEmpty()) {\n\t\t\tfilter = u\"MOV Video (*.mov);;\"_q + FileDialog::AllFilesFilter();\n\t\t} else {\n\t\t\tfilter = mimeType.filterString() + u\";;\"_q + FileDialog::AllFilesFilter();\n\t\t}\n\t\tcaption = tr::lng_save_video(tr::now);\n\t\tprefix = u\"video\"_q;\n\t} else {\n\t\tname = already.isEmpty() ? data->filename() : already;\n\t\tif (name.isEmpty()) {\n\t\t\tname = pattern.isEmpty() ? u\".unknown\"_q : pattern.replace('*', QString());\n\t\t}\n\t\tif (pattern.isEmpty()) {\n\t\t\tfilter = QString();\n\t\t} else {\n\t\t\tfilter = mimeType.filterString() + u\";;\"_q + FileDialog::AllFilesFilter();\n\t\t}\n\t\tcaption = data->isAudioFile()\n\t\t\t? tr::lng_save_audio_file(tr::now)\n\t\t\t: tr::lng_save_file(tr::now);\n\t\tprefix = u\"doc\"_q;\n\t}\n\n\treturn FileNameForSave(\n\t\t&data->session(),\n\t\tcaption,\n\t\tfilter,\n\t\tprefix,\n\t\tname,\n\t\tforceSavingAs,\n\t\tdir);\n}\n\nData::FileOrigin StickerData::setOrigin() const {\n\treturn set.id\n\t\t? Data::FileOrigin(\n\t\t\tData::FileOriginStickerSet(set.id, set.accessHash))\n\t\t: Data::FileOrigin();\n}\n\nbool StickerData::isStatic() const {\n\treturn (type == StickerType::Webp);\n}\n\nbool StickerData::isLottie() const {\n\treturn (type == StickerType::Tgs);\n}\n\nbool StickerData::isAnimated() const {\n\treturn !isStatic();\n}\n\nbool StickerData::isWebm() const {\n\treturn (type == StickerType::Webm);\n}\n\nVoiceData::~VoiceData() {\n\tif (!waveform.isEmpty()\n\t\t&& waveform[0] == -1\n\t\t&& waveform.size() > int32(sizeof(TaskId))) {\n\t\tauto taskId = TaskId();\n\t\tmemcpy(&taskId, waveform.constData() + 1, sizeof(taskId));\n\t\tLocal::cancelTask(taskId);\n\t}\n}\n\nDocumentData::DocumentData(not_null<Data::Session*> owner, DocumentId id)\n: id(id)\n, _owner(owner) {\n}\n\nDocumentData::~DocumentData() {\n\tbase::take(_thumbnail.loader).reset();\n\tbase::take(_videoThumbnail.loader).reset();\n\tdestroyLoader();\n}\n\nData::Session &DocumentData::owner() const {\n\treturn *_owner;\n}\n\nMain::Session &DocumentData::session() const {\n\treturn _owner->session();\n}\n\nvoid DocumentData::setattributes(\n\t\tconst QVector<MTPDocumentAttribute> &attributes) {\n\t_duration = -1;\n\t_flags &= ~(Flag::ImageType\n\t\t| Flag::HasAttachedStickers\n\t\t| Flag::UseTextColor\n\t\t| Flag::SilentVideo\n\t\t| kStreamingSupportedMask);\n\t_flags |= kStreamingSupportedUnknown;\n\n\tvalidateLottieSticker();\n\n\tauto wasVideoData = isVideoFile() ? std::move(_additional) : nullptr;\n\n\t_videoPreloadPrefix = 0;\n\tfor (const auto &attribute : attributes) {\n\t\tattribute.match([&](const MTPDdocumentAttributeImageSize &data) {\n\t\t\tdimensions = QSize(data.vw().v, data.vh().v);\n\t\t}, [&](const MTPDdocumentAttributeAnimated &data) {\n\t\t\tif (type == FileDocument\n\t\t\t\t|| type == VideoDocument\n\t\t\t\t|| (sticker() && sticker()->type != StickerType::Webm)) {\n\t\t\t\ttype = AnimatedDocument;\n\t\t\t\t_additional = nullptr;\n\t\t\t}\n\t\t}, [&](const MTPDdocumentAttributeSticker &data) {\n\t\t\tconst auto was = type;\n\t\t\tif (type == FileDocument || type == VideoDocument) {\n\t\t\t\ttype = StickerDocument;\n\t\t\t\t_additional = std::make_unique<StickerData>();\n\t\t\t}\n\t\t\tif (const auto info = sticker()) {\n\t\t\t\tinfo->setType = data.is_mask()\n\t\t\t\t\t? Data::StickersType::Masks\n\t\t\t\t\t: Data::StickersType::Stickers;\n\t\t\t\tif (was == VideoDocument) {\n\t\t\t\t\tinfo->type = StickerType::Webm;\n\t\t\t\t}\n\t\t\t\tinfo->alt = qs(data.valt());\n\t\t\t\tUpdateStickerSetIdentifier(info->set, data.vstickerset());\n\t\t\t}\n\t\t}, [&](const MTPDdocumentAttributeCustomEmoji &data) {\n\t\t\tconst auto was = type;\n\t\t\tif (type == FileDocument || type == VideoDocument) {\n\t\t\t\ttype = StickerDocument;\n\t\t\t\t_additional = std::make_unique<StickerData>();\n\t\t\t}\n\t\t\tif (const auto info = sticker()) {\n\t\t\t\tinfo->setType = Data::StickersType::Emoji;\n\t\t\t\tif (was == VideoDocument) {\n\t\t\t\t\tinfo->type = StickerType::Webm;\n\t\t\t\t}\n\t\t\t\tinfo->alt = qs(data.valt());\n\t\t\t\tif (data.is_free()) {\n\t\t\t\t\t_flags &= ~Flag::PremiumSticker;\n\t\t\t\t} else {\n\t\t\t\t\t_flags |= Flag::PremiumSticker;\n\t\t\t\t}\n\t\t\t\tif (data.is_text_color()) {\n\t\t\t\t\t_flags |= Flag::UseTextColor;\n\t\t\t\t}\n\t\t\t\tUpdateStickerSetIdentifier(info->set, data.vstickerset());\n\t\t\t}\n\t\t}, [&](const MTPDdocumentAttributeVideo &data) {\n\t\t\tif (type == FileDocument) {\n\t\t\t\ttype = data.is_round_message()\n\t\t\t\t\t? RoundVideoDocument\n\t\t\t\t\t: VideoDocument;\n\t\t\t\tif (data.is_round_message()) {\n\t\t\t\t\t_additional = std::make_unique<RoundData>();\n\t\t\t\t} else {\n\t\t\t\t\tif (const auto size = data.vpreload_prefix_size()) {\n\t\t\t\t\t\tif (size->v > 0\n\t\t\t\t\t\t\t&& size->v < kMaxAllowedPreloadPrefix) {\n\t\t\t\t\t\t\t_videoPreloadPrefix = size->v;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t_additional = wasVideoData\n\t\t\t\t\t\t? std::move(wasVideoData)\n\t\t\t\t\t\t: std::make_unique<VideoData>();\n\t\t\t\t\tvideo()->codec = qs(\n\t\t\t\t\t\tdata.vvideo_codec().value_or_empty());\n\t\t\t\t}\n\t\t\t} else if (type == VideoDocument && wasVideoData) {\n\t\t\t\t_additional = std::move(wasVideoData);\n\t\t\t} else if (const auto info = sticker()) {\n\t\t\t\tinfo->type = StickerType::Webm;\n\t\t\t}\n\t\t\t_duration = crl::time(\n\t\t\t\tbase::SafeRound(data.vduration().v * 1000));\n\t\t\tsetMaybeSupportsStreaming(data.is_supports_streaming());\n\t\t\tif (data.is_nosound()) {\n\t\t\t\t_flags |= Flag::SilentVideo;\n\t\t\t}\n\t\t\tdimensions = QSize(data.vw().v, data.vh().v);\n\t\t}, [&](const MTPDdocumentAttributeAudio &data) {\n\t\t\tif (type == FileDocument) {\n\t\t\t\tif (data.is_voice()) {\n\t\t\t\t\ttype = VoiceDocument;\n\t\t\t\t\t_additional = std::make_unique<VoiceData>();\n\t\t\t\t} else {\n\t\t\t\t\ttype = SongDocument;\n\t\t\t\t\t_additional = std::make_unique<SongData>();\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (const auto voiceData = voice() ? voice() : round()) {\n\t\t\t\t_duration = data.vduration().v * crl::time(1000);\n\t\t\t\tvoiceData->waveform = documentWaveformDecode(\n\t\t\t\t\tdata.vwaveform().value_or_empty());\n\t\t\t\tvoiceData->wavemax = voiceData->waveform.empty()\n\t\t\t\t\t? uchar(0)\n\t\t\t\t\t: *ranges::max_element(voiceData->waveform);\n\t\t\t} else if (const auto songData = song()) {\n\t\t\t\t_duration = data.vduration().v * crl::time(1000);\n\t\t\t\tsongData->title = qs(data.vtitle().value_or_empty());\n\t\t\t\tsongData->performer = qs(data.vperformer().value_or_empty());\n\t\t\t\trefreshPossibleCoverThumbnail();\n\t\t\t}\n\t\t}, [&](const MTPDdocumentAttributeFilename &data) {\n\t\t\tsetFileName(qs(data.vfile_name()));\n\t\t}, [&](const MTPDdocumentAttributeHasStickers &data) {\n\t\t\t_flags |= Flag::HasAttachedStickers;\n\t\t});\n\t}\n\n\t// Any \"video/webm\" file is treated as a video-sticker.\n\tif (hasMimeType(u\"video/webm\"_q)) {\n\t\tif (type == FileDocument) {\n\t\t\ttype = StickerDocument;\n\t\t\t_additional = std::make_unique<StickerData>();\n\t\t}\n\t\tif (type == StickerDocument) {\n\t\t\tsticker()->type = StickerType::Webm;\n\t\t}\n\t}\n\n\t// If \"video/webm\" sticker without dimensions we set them to default.\n\tif (const auto info = sticker(); info\n\t\t&& info->set\n\t\t&& info->type == StickerType::Webm\n\t\t&& dimensions.isEmpty()) {\n\t\tif (info->setType == Data::StickersType::Emoji) {\n\t\t\t// Always fixed.\n\t\t\tdimensions = { kDefaultWebmEmojiSize, kDefaultWebmEmojiSize };\n\t\t} else if (info->setType == Data::StickersType::Stickers) {\n\t\t\t// May have aspect != 1, so we count it from the thumbnail.\n\t\t\tconst auto thumbnail = QSize(\n\t\t\t\t_thumbnail.location.width(),\n\t\t\t\t_thumbnail.location.height()\n\t\t\t).scaled(\n\t\t\t\tkDefaultWebmStickerLargerSize,\n\t\t\t\tkDefaultWebmStickerLargerSize,\n\t\t\t\tQt::KeepAspectRatio);\n\t\t\tif (!thumbnail.isEmpty()) {\n\t\t\t\tdimensions = thumbnail;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check sticker size/dimensions properties (for sticker of any type).\n\tif (type == StickerDocument\n\t\t&& ((size > Storage::kMaxStickerBytesSize)\n\t\t\t|| (!sticker()->isLottie()\n\t\t\t\t&& !GoodStickerDimensions(\n\t\t\t\t\tdimensions.width(),\n\t\t\t\t\tdimensions.height())))) {\n\t\ttype = FileDocument;\n\t\t_additional = nullptr;\n\t}\n\n\tif (!_filename.isEmpty()) {\n\t\tusing Type = Core::NameType;\n\t\tif (type == VideoDocument\n\t\t\t|| type == AnimatedDocument\n\t\t\t|| type == RoundVideoDocument\n\t\t\t|| isAnimation()) {\n\t\t\tif (!enforceNameType(Type::Video)) {\n\t\t\t\ttype = FileDocument;\n\t\t\t\t_additional = nullptr;\n\t\t\t}\n\t\t}\n\t\tif (type == SongDocument || type == VoiceDocument || isAudioFile()) {\n\t\t\tif (!enforceNameType(Type::Audio)) {\n\t\t\t\ttype = FileDocument;\n\t\t\t\t_additional = nullptr;\n\t\t\t}\n\t\t}\n\t\tif (!Core::NameTypeAllowsThumbnail(_nameType)) {\n\t\t\t_inlineThumbnailBytes = {};\n\t\t\t_flags &= ~Flag::InlineThumbnailIsPath;\n\t\t\t_thumbnail.clear();\n\t\t\t_videoThumbnail.clear();\n\t\t}\n\t}\n\n\tif (isAudioFile()\n\t\t|| isAnimation()\n\t\t|| isVoiceMessage()\n\t\t|| storyMedia()) {\n\t\tsetMaybeSupportsStreaming(true);\n\t}\n}\n\nvoid DocumentData::setVideoQualities(const QVector<MTPDocument> &list) {\n\tauto qualities = std::vector<not_null<DocumentData*>>();\n\tqualities.reserve(list.size());\n\tfor (const auto &document : list) {\n\t\tqualities.push_back(owner().processDocument(document));\n\t}\n\tsetVideoQualities(std::move(qualities));\n}\n\nvoid DocumentData::setVideoQualities(\n\t\tstd::vector<not_null<DocumentData*>> qualities) {\n\tconst auto data = video();\n\tif (!data) {\n\t\treturn;\n\t}\n\tauto count = int(qualities.size());\n\tif (qualities.empty()) {\n\t\treturn;\n\t}\n\tconst auto good = [&](not_null<DocumentData*> document) {\n\t\treturn document->isVideoFile()\n\t\t\t&& !document->dimensions.isEmpty()\n\t\t\t&& !document->inappPlaybackFailed()\n\t\t\t&& document->useStreamingLoader()\n\t\t\t&& document->canBeStreamed();\n\t};\n\tranges::sort(\n\t\tqualities,\n\t\tranges::greater(),\n\t\t&DocumentData::resolveVideoQuality);\n\tfor (auto i = 0; i != count - 1;) {\n\t\tconst auto my = qualities[i];\n\t\tconst auto next = qualities[i + 1];\n\t\tconst auto myQuality = my->resolveVideoQuality();\n\t\tconst auto nextQuality = next->resolveVideoQuality();\n\t\tconst auto myGood = good(my);\n\t\tconst auto nextGood = good(next);\n\t\tif (!myGood || !nextGood || myQuality == nextQuality) {\n\t\t\tconst auto removeMe = !myGood\n\t\t\t\t|| (nextGood && (my->size > next->size));\n\t\t\tconst auto from = i + (removeMe ? 1 : 2);\n\t\t\tfor (auto j = from; j != count; ++j) {\n\t\t\t\tqualities[j - 1] = qualities[j];\n\t\t\t}\n\t\t\t--count;\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tif (!qualities[count - 1]->resolveVideoQuality()) {\n\t\t--count;\n\t}\n\tqualities.erase(qualities.begin() + count, qualities.end());\n\tif (!qualities.empty()) {\n\t\tif (const auto mine = resolveVideoQuality()) {\n\t\t\tif (mine > qualities.front()->resolveVideoQuality()) {\n\t\t\t\tqualities.insert(begin(qualities), this);\n\t\t\t}\n\t\t}\n\t}\n\tdata->qualities = std::move(qualities);\n}\n\nint DocumentData::resolveVideoQuality() const {\n\tconst auto size = isVideoFile() ? dimensions : QSize();\n\treturn size.isEmpty() ? 0 : std::min(size.width(), size.height());\n}\n\nauto DocumentData::resolveQualities(HistoryItem *context) const\n-> const std::vector<not_null<DocumentData*>> & {\n\tstatic const auto empty = std::vector<not_null<DocumentData*>>();\n\tconst auto info = video();\n\tconst auto media = context ? context->media() : nullptr;\n\tif (!info || !media || media->document() != this) {\n\t\treturn empty;\n\t}\n\treturn media->hasQualitiesList() ? info->qualities : empty;\n}\n\nnot_null<DocumentData*> DocumentData::chooseQuality(\n\t\tHistoryItem *context,\n\t\tMedia::VideoQuality request) {\n\tconst auto &list = resolveQualities(context);\n\tif (list.empty() || !request.height) {\n\t\treturn this;\n\t}\n\tconst auto height = int(request.height);\n\tauto closest = this;\n\tauto closestAbs = std::abs(height - resolveVideoQuality());\n\tauto closestSize = size;\n\tfor (const auto &quality : list) {\n\t\tconst auto abs = std::abs(height - quality->resolveVideoQuality());\n\t\tif (abs < closestAbs\n\t\t\t|| (abs == closestAbs && quality->size < closestSize)) {\n\t\t\tclosest = quality;\n\t\t\tclosestAbs = abs;\n\t\t\tclosestSize = quality->size;\n\t\t}\n\t}\n\treturn closest;\n}\n\nvoid DocumentData::validateLottieSticker() {\n\tif (type == FileDocument\n\t\t&& hasMimeType(u\"application/x-tgsticker\"_q)) {\n\t\ttype = StickerDocument;\n\t\t_additional = std::make_unique<StickerData>();\n\t\tsticker()->type = StickerType::Tgs;\n\t\tdimensions = kLottieStickerDimensions;\n\t}\n}\n\nvoid DocumentData::setDataAndCache(const QByteArray &data) {\n\tif (const auto media = activeMediaView()) {\n\t\tmedia->setBytes(data);\n\t}\n\tif (saveToCache() && data.size() <= Storage::kMaxFileInMemory) {\n\t\towner().cache().put(\n\t\t\tcacheKey(),\n\t\t\tStorage::Cache::Database::TaggedValue(\n\t\t\t\tbase::duplicate(data),\n\t\t\t\tcacheTag()));\n\t}\n}\n\nbool DocumentData::checkWallPaperProperties() {\n\tif (type == WallPaperDocument) {\n\t\treturn true;\n\t}\n\tif (type != FileDocument\n\t\t|| !hasThumbnail()\n\t\t|| dimensions.isEmpty()\n\t\t|| dimensions.width() > Storage::kMaxWallPaperDimension\n\t\t|| dimensions.height() > Storage::kMaxWallPaperDimension\n\t\t|| size > Storage::kMaxWallPaperInMemory) {\n\t\treturn false;\n\t}\n\ttype = WallPaperDocument;\n\treturn true;\n}\n\nvoid DocumentData::updateThumbnails(\n\t\tconst InlineImageLocation &inlineThumbnail,\n\t\tconst ImageWithLocation &thumbnail,\n\t\tconst ImageWithLocation &videoThumbnail,\n\t\tbool isPremiumSticker) {\n\tif (!_filename.isEmpty()\n\t\t&& !Core::NameTypeAllowsThumbnail(Core::DetectNameType(_filename))) {\n\t\treturn;\n\t}\n\tif (!inlineThumbnail.bytes.isEmpty()\n\t\t&& _inlineThumbnailBytes.isEmpty()) {\n\t\t_inlineThumbnailBytes = inlineThumbnail.bytes;\n\t\tif (inlineThumbnail.isPath) {\n\t\t\t_flags |= Flag::InlineThumbnailIsPath;\n\t\t} else {\n\t\t\t_flags &= ~Flag::InlineThumbnailIsPath;\n\t\t}\n\t}\n\tif (!sticker() || sticker()->setType != Data::StickersType::Emoji) {\n\t\tif (isPremiumSticker) {\n\t\t\t_flags |= Flag::PremiumSticker;\n\t\t} else {\n\t\t\t_flags &= ~Flag::PremiumSticker;\n\t\t}\n\t}\n\tData::UpdateCloudFile(\n\t\t_thumbnail,\n\t\tthumbnail,\n\t\towner().cache(),\n\t\tData::kImageCacheTag,\n\t\t[&](Data::FileOrigin origin) { loadThumbnail(origin); },\n\t\t[&](QImage preloaded, QByteArray) {\n\t\t\tif (const auto media = activeMediaView()) {\n\t\t\t\tmedia->setThumbnail(std::move(preloaded));\n\t\t\t}\n\t\t});\n\tData::UpdateCloudFile(\n\t\t_videoThumbnail,\n\t\tvideoThumbnail,\n\t\towner().cache(),\n\t\tData::kAnimationCacheTag,\n\t\t[&](Data::FileOrigin origin) { loadVideoThumbnail(origin); });\n}\n\nbool DocumentData::isWallPaper() const {\n\treturn (type == WallPaperDocument);\n}\n\nbool DocumentData::isPatternWallPaper() const {\n\treturn isWallPaper()\n\t\t&& (isPatternWallPaperPNG() || isPatternWallPaperSVG());\n}\n\nbool DocumentData::isPatternWallPaperPNG() const {\n\treturn isWallPaper() && hasMimeType(u\"image/png\"_q);\n}\n\nbool DocumentData::isPatternWallPaperSVG() const {\n\treturn isWallPaper() && hasMimeType(u\"application/x-tgwallpattern\"_q);\n}\n\nbool DocumentData::isSvgImage() const {\n\treturn hasMimeType(u\"image/svg+xml\"_q)\n\t\t|| _filename.endsWith(u\".svg\"_q, Qt::CaseInsensitive);\n}\n\nbool DocumentData::isPremiumSticker() const {\n\tif (!(_flags & Flag::PremiumSticker)) {\n\t\treturn false;\n\t}\n\tconst auto info = sticker();\n\treturn info && info->setType == Data::StickersType::Stickers;\n}\n\nbool DocumentData::isPremiumEmoji() const {\n\tif (!(_flags & Flag::PremiumSticker)) {\n\t\treturn false;\n\t}\n\tconst auto info = sticker();\n\treturn info && info->setType == Data::StickersType::Emoji;\n}\n\nbool DocumentData::emojiUsesTextColor() const {\n\treturn (_flags & Flag::UseTextColor);\n}\n\nvoid DocumentData::overrideEmojiUsesTextColor(bool value) {\n\tif (value) {\n\t\t_flags |= Flag::UseTextColor;\n\t} else {\n\t\t_flags &= ~Flag::UseTextColor;\n\t}\n}\n\nbool DocumentData::hasThumbnail() const {\n\treturn _thumbnail.location.valid()\n\t\t&& !thumbnailFailed()\n\t\t&& !(_flags & Flag::PossibleCoverThumbnail);\n}\n\nbool DocumentData::thumbnailLoading() const {\n\treturn _thumbnail.loader != nullptr;\n}\n\nbool DocumentData::thumbnailFailed() const {\n\treturn (_thumbnail.flags & Data::CloudFile::Flag::Failed);\n}\n\nvoid DocumentData::loadThumbnail(Data::FileOrigin origin) {\n\tconst auto autoLoading = false;\n\tconst auto finalCheck = [=] {\n\t\tif (const auto active = activeMediaView()) {\n\t\t\treturn !active->thumbnail();\n\t\t}\n\t\treturn true;\n\t};\n\tconst auto done = [=](QImage result, QByteArray) {\n\t\t_flags &= ~Flag::PossibleCoverThumbnail;\n\t\tif (const auto active = activeMediaView()) {\n\t\t\tactive->setThumbnail(std::move(result));\n\t\t}\n\t};\n\tData::LoadCloudFile(\n\t\t&session(),\n\t\t_thumbnail,\n\t\torigin,\n\t\tLoadFromCloudOrLocal,\n\t\tautoLoading,\n\t\tData::kImageCacheTag,\n\t\tfinalCheck,\n\t\tdone);\n}\n\nconst ImageLocation &DocumentData::thumbnailLocation() const {\n\treturn _thumbnail.location;\n}\n\nint DocumentData::thumbnailByteSize() const {\n\treturn _thumbnail.byteSize;\n}\n\nbool DocumentData::hasVideoThumbnail() const {\n\treturn _videoThumbnail.location.valid();\n}\n\nbool DocumentData::videoThumbnailLoading() const {\n\treturn _videoThumbnail.loader != nullptr;\n}\n\nbool DocumentData::videoThumbnailFailed() const {\n\treturn (_videoThumbnail.flags & Data::CloudFile::Flag::Failed);\n}\n\nvoid DocumentData::loadVideoThumbnail(Data::FileOrigin origin) {\n\tconst auto autoLoading = false;\n\tconst auto finalCheck = [=] {\n\t\tif (const auto active = activeMediaView()) {\n\t\t\treturn active->videoThumbnailContent().isEmpty();\n\t\t}\n\t\treturn true;\n\t};\n\tconst auto done = [=](QByteArray result) {\n\t\tif (const auto active = activeMediaView()) {\n\t\t\tactive->setVideoThumbnail(std::move(result));\n\t\t}\n\t};\n\tData::LoadCloudFile(\n\t\t&session(),\n\t\t_videoThumbnail,\n\t\torigin,\n\t\tLoadFromCloudOrLocal,\n\t\tautoLoading,\n\t\tData::kAnimationCacheTag,\n\t\tfinalCheck,\n\t\tdone);\n}\n\nconst ImageLocation &DocumentData::videoThumbnailLocation() const {\n\treturn _videoThumbnail.location;\n}\n\nint DocumentData::videoThumbnailByteSize() const {\n\treturn _videoThumbnail.byteSize;\n}\n\nStorage::Cache::Key DocumentData::goodThumbnailCacheKey() const {\n\treturn Data::DocumentThumbCacheKey(_dc, id);\n}\n\nbool DocumentData::goodThumbnailChecked() const {\n\treturn (_goodThumbnailState & GoodThumbnailFlag::Mask)\n\t\t== GoodThumbnailFlag::Checked;\n}\n\nbool DocumentData::goodThumbnailGenerating() const {\n\treturn (_goodThumbnailState & GoodThumbnailFlag::Mask)\n\t\t== GoodThumbnailFlag::Generating;\n}\n\nbool DocumentData::goodThumbnailNoData() const {\n\treturn (_goodThumbnailState & GoodThumbnailFlag::Mask)\n\t\t== GoodThumbnailFlag::NoData;\n}\n\nvoid DocumentData::setGoodThumbnailGenerating() {\n\t_goodThumbnailState = (_goodThumbnailState & ~GoodThumbnailFlag::Mask)\n\t\t| GoodThumbnailFlag::Generating;\n}\n\nvoid DocumentData::setGoodThumbnailDataReady() {\n\t_goodThumbnailState = GoodThumbnailFlag::DataReady\n\t\t| (goodThumbnailNoData()\n\t\t\t? GoodThumbnailFlag(0)\n\t\t\t: (_goodThumbnailState & GoodThumbnailFlag::Mask));\n}\n\nvoid DocumentData::setGoodThumbnailChecked(bool hasData) {\n\tif (!hasData && (_goodThumbnailState & GoodThumbnailFlag::DataReady)) {\n\t\t_goodThumbnailState &= ~GoodThumbnailFlag::DataReady;\n\t\t_goodThumbnailState &= ~GoodThumbnailFlag::Mask;\n\t\tData::DocumentMedia::CheckGoodThumbnail(this);\n\t\treturn;\n\t}\n\t_goodThumbnailState = (_goodThumbnailState & ~GoodThumbnailFlag::Mask)\n\t\t| (hasData\n\t\t\t? GoodThumbnailFlag::Checked\n\t\t\t: GoodThumbnailFlag::NoData);\n}\n\nstd::shared_ptr<Data::DocumentMedia> DocumentData::createMediaView() {\n\tif (auto result = activeMediaView()) {\n\t\treturn result;\n\t}\n\tauto result = std::make_shared<Data::DocumentMedia>(this);\n\t_media = result;\n\treturn result;\n}\n\nstd::shared_ptr<Data::DocumentMedia> DocumentData::activeMediaView() const {\n\treturn _media.lock();\n}\n\nvoid DocumentData::setGoodThumbnailPhoto(not_null<PhotoData*> photo) {\n\t_goodThumbnailPhoto = photo;\n}\n\nPhotoData *DocumentData::goodThumbnailPhoto() const {\n\treturn _goodThumbnailPhoto;\n}\n\nStorage::Cache::Key DocumentData::bigFileBaseCacheKey() const {\n\treturn hasRemoteLocation()\n\t\t? StorageFileLocation(\n\t\t\t_dc,\n\t\t\tsession().userId(),\n\t\t\tMTP_inputDocumentFileLocation(\n\t\t\t\tMTP_long(id),\n\t\t\t\tMTP_long(_access),\n\t\t\t\tMTP_bytes(_fileReference),\n\t\t\t\tMTP_string())).bigFileBaseCacheKey()\n\t\t: Storage::Cache::Key();\n}\n\nvoid DocumentData::forceToCache(bool force) {\n\tif (force) {\n\t\t_flags |= Flag::ForceToCache;\n\t} else {\n\t\t_flags &= ~Flag::ForceToCache;\n\t}\n}\n\nbool DocumentData::saveToCache() const {\n\treturn (size < Storage::kMaxFileInMemory)\n\t\t&& ((type == StickerDocument)\n\t\t\t|| (_flags & Flag::ForceToCache)\n\t\t\t|| isAnimation()\n\t\t\t|| isVoiceMessage()\n\t\t\t|| isWallPaper()\n\t\t\t|| isTheme()\n\t\t\t|| (hasMimeType(u\"image/png\"_q)\n\t\t\t\t&& _filename.startsWith(\"image_\")));\n}\n\nvoid DocumentData::automaticLoadSettingsChanged() {\n\tif (!cancelled() || status != FileReady) {\n\t\treturn;\n\t}\n\t_loader = nullptr;\n\tresetCancelled();\n}\n\nvoid DocumentData::finishLoad() {\n\t// NB! _loader may be in ~FileLoader() already.\n\tconst auto guard = gsl::finally([&] {\n\t\tdestroyLoader();\n\t});\n\tif (!_loader || _loader->cancelled()) {\n\t\t_flags |= Flag::DownloadCancelled;\n\t\treturn;\n\t}\n\tsetLocation(Core::FileLocation(_loader->fileName()));\n\tsetGoodThumbnailDataReady();\n\tif (const auto media = activeMediaView()) {\n\t\tmedia->setBytes(_loader->bytes());\n\t\tmedia->checkStickerLarge(_loader.get());\n\t}\n}\n\nvoid DocumentData::destroyLoader() {\n\tif (!_loader) {\n\t\treturn;\n\t}\n\tconst auto loader = base::take(_loader);\n\tif (cancelled()) {\n\t\tloader->cancel();\n\t}\n}\n\nbool DocumentData::loading() const {\n\treturn (_loader != nullptr);\n}\n\nQString DocumentData::loadingFilePath() const {\n\treturn loading() ? _loader->fileName() : QString();\n}\n\nbool DocumentData::displayLoading() const {\n\treturn loading()\n\t\t? !_loader->loadingLocal()\n\t\t: (uploading() && !waitingForAlbum());\n}\n\nfloat64 DocumentData::progress() const {\n\tif (uploading()) {\n\t\tif (uploadingData->size > 0) {\n\t\t\tconst auto result = float64(uploadingData->offset)\n\t\t\t\t/ float64(uploadingData->size);\n\t\t\treturn std::clamp(result, 0., 1.);\n\t\t}\n\t\treturn 0.;\n\t}\n\treturn loading() ? _loader->currentProgress() : 0.;\n}\n\nint64 DocumentData::loadOffset() const {\n\treturn loading() ? _loader->currentOffset() : 0;\n}\n\nbool DocumentData::uploading() const {\n\treturn (uploadingData != nullptr);\n}\n\nbool DocumentData::loadedInMediaCache() const {\n\treturn (_flags & Flag::LoadedInMediaCache);\n}\n\nvoid DocumentData::setLoadedInMediaCache(bool loaded) {\n\tconst auto flags = loaded\n\t\t? (_flags | Flag::LoadedInMediaCache)\n\t\t: (_flags & ~Flag::LoadedInMediaCache);\n\tif (_flags == flags) {\n\t\treturn;\n\t}\n\t_flags = flags;\n\tif (filepath().isEmpty()) {\n\t\tif (loadedInMediaCache()) {\n\t\t\tsession().local().writeFileLocation(\n\t\t\t\tmediaKey(),\n\t\t\t\tCore::FileLocation::InMediaCacheLocation());\n\t\t} else {\n\t\t\tsession().local().removeFileLocation(mediaKey());\n\t\t}\n\t\towner().requestDocumentViewRepaint(this);\n\t}\n}\n\nChatRestriction DocumentData::requiredSendRight() const {\n\treturn isVideoFile()\n\t\t? ChatRestriction::SendVideos\n\t\t: isSong()\n\t\t? ChatRestriction::SendMusic\n\t\t: isVoiceMessage()\n\t\t? ChatRestriction::SendVoiceMessages\n\t\t: isVideoMessage()\n\t\t? ChatRestriction::SendVideoMessages\n\t\t: sticker()\n\t\t? ChatRestriction::SendStickers\n\t\t: isAnimation()\n\t\t? ChatRestriction::SendGifs\n\t\t: ChatRestriction::SendFiles;\n}\n\nvoid DocumentData::setFileName(const QString &remoteFileName) {\n\t_filename = remoteFileName;\n\n\t// We don't want LTR/RTL mark/embedding/override/isolate chars\n\t// in filenames, because they introduce a security issue, when\n\t// an executable \"Fil[x]gepj.exe\" may look like \"Filexe.jpeg\".\n\tQChar controls[] = {\n\t\tQChar(0x200E), // LTR Mark\n\t\tQChar(0x200F), // RTL Mark\n\t\tQChar(0x202A), // LTR Embedding\n\t\tQChar(0x202B), // RTL Embedding\n\t\tQChar(0x202D), // LTR Override\n\t\tQChar(0x202E), // RTL Override\n\t\tQChar(0x2066), // LTR Isolate\n\t\tQChar(0x2067), // RTL Isolate\n\t};\n\tfor (const auto &ch : controls) {\n\t\t_filename = std::move(_filename).replace(ch, \"_\");\n\t}\n\t_nameType = Core::DetectNameType(_filename);\n}\n\nbool DocumentData::enforceNameType(Core::NameType nameType) {\n\tif (_nameType == nameType) {\n\t\treturn true;\n\t}\n\tconst auto base = _filename.isEmpty() ? u\"file\"_q : _filename;\n\tconst auto mime = Core::MimeTypeForName(mimeString());\n\tconst auto patterns = mime.globPatterns();\n\tfor (const auto &pattern : mime.globPatterns()) {\n\t\tconst auto now = base + QString(pattern).replace('*', QString());\n\t\tif (Core::DetectNameType(now) == nameType) {\n\t\t\t_filename = now;\n\t\t\t_nameType = nameType;\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid DocumentData::setLoadedInMediaCacheLocation() {\n\t_location = Core::FileLocation();\n\t_flags |= Flag::LoadedInMediaCache;\n}\n\nvoid DocumentData::setWaitingForAlbum() {\n\tif (uploading()) {\n\t\tuploadingData->waitingForAlbum = true;\n\t}\n}\n\nbool DocumentData::waitingForAlbum() const {\n\treturn uploading() && uploadingData->waitingForAlbum;\n}\n\nvoid DocumentData::save(\n\t\tData::FileOrigin origin,\n\t\tconst QString &toFile,\n\t\tLoadFromCloudSetting fromCloud,\n\t\tbool autoLoading) {\n\tif (const auto media = activeMediaView(); media && media->loaded(true)) {\n\t\tauto &l = location(true);\n\t\tif (!toFile.isEmpty()) {\n\t\t\tif (!media->bytes().isEmpty()) {\n\t\t\t\tQFile f(toFile);\n\t\t\t\tif (f.open(QIODevice::WriteOnly)) {\n\t\t\t\t\tf.write(media->bytes());\n\t\t\t\t\tf.close();\n\t\t\t\t}\n\n\t\t\t\tsetLocation(Core::FileLocation(toFile));\n\t\t\t\tsession().local().writeFileLocation(\n\t\t\t\t\tmediaKey(),\n\t\t\t\t\tCore::FileLocation(toFile));\n\t\t\t} else if (l.accessEnable()) {\n\t\t\t\tconst auto &alreadyName = l.name();\n\t\t\t\tif (alreadyName != toFile) {\n\t\t\t\t\tQFile(toFile).remove();\n\t\t\t\t\tQFile(alreadyName).copy(toFile);\n\t\t\t\t}\n\t\t\t\tl.accessDisable();\n\t\t\t}\n\t\t}\n\t\treturn;\n\t}\n\n\tif (_loader) {\n\t\tif (!_loader->setFileName(toFile)) {\n\t\t\tcancel();\n\t\t}\n\t}\n\tresetCancelled();\n\n\tif (_loader) {\n\t\tif (fromCloud == LoadFromCloudOrLocal) {\n\t\t\t_loader->permitLoadFromCloud();\n\t\t}\n\t} else {\n\t\tstatus = FileReady;\n\t\tauto reader = owner().streaming().sharedReader(this, origin, true);\n\t\tif (reader) {\n\t\t\t_loader = std::make_unique<Storage::StreamedFileDownloader>(\n\t\t\t\t&session(),\n\t\t\t\tid,\n\t\t\t\t_dc,\n\t\t\t\torigin,\n\t\t\t\tData::DocumentCacheKey(_dc, id),\n\t\t\t\tmediaKey(),\n\t\t\t\tstd::move(reader),\n\t\t\t\ttoFile,\n\t\t\t\tsize,\n\t\t\t\tlocationType(),\n\t\t\t\t(saveToCache() ? LoadToCacheAsWell : LoadToFileOnly),\n\t\t\t\tfromCloud,\n\t\t\t\tautoLoading,\n\t\t\t\tcacheTag());\n\t\t} else if (hasWebLocation()) {\n\t\t\t_loader = std::make_unique<mtpFileLoader>(\n\t\t\t\t&session(),\n\t\t\t\t_urlLocation,\n\t\t\t\tsize,\n\t\t\t\tsize,\n\t\t\t\tfromCloud,\n\t\t\t\tautoLoading,\n\t\t\t\tcacheTag());\n\t\t} else if (!_access && !_url.isEmpty()) {\n\t\t\t_loader = std::make_unique<webFileLoader>(\n\t\t\t\t&session(),\n\t\t\t\t_url,\n\t\t\t\ttoFile,\n\t\t\t\tfromCloud,\n\t\t\t\tautoLoading,\n\t\t\t\tcacheTag());\n\t\t} else {\n\t\t\t_loader = std::make_unique<mtpFileLoader>(\n\t\t\t\t&session(),\n\t\t\t\tStorageFileLocation(\n\t\t\t\t\t_dc,\n\t\t\t\t\tsession().userId(),\n\t\t\t\t\tMTP_inputDocumentFileLocation(\n\t\t\t\t\t\tMTP_long(id),\n\t\t\t\t\t\tMTP_long(_access),\n\t\t\t\t\t\tMTP_bytes(_fileReference),\n\t\t\t\t\t\tMTP_string())),\n\t\t\t\torigin,\n\t\t\t\tlocationType(),\n\t\t\t\ttoFile,\n\t\t\t\tsize,\n\t\t\t\tsize,\n\t\t\t\t(saveToCache() ? LoadToCacheAsWell : LoadToFileOnly),\n\t\t\t\tfromCloud,\n\t\t\t\tautoLoading,\n\t\t\t\tcacheTag());\n\t\t}\n\t\thandleLoaderUpdates();\n\t}\n\tif (loading()) {\n\t\t_loader->start();\n\t}\n\t// This affects a display of tooltips.\n\t// _owner->notifyDocumentLayoutChanged(this);\n}\n\nvoid DocumentData::handleLoaderUpdates() {\n\t_loader->updates(\n\t) | rpl::on_next_error_done([=] {\n\t\t_owner->documentLoadProgress(this);\n\t}, [=](FileLoader::Error error) {\n\t\tusing FailureReason = FileLoader::FailureReason;\n\t\tif (error.started && _loader) {\n\t\t\tconst auto origin = _loader->fileOrigin();\n\t\t\tconst auto failedFileName = _loader->fileName();\n\t\t\tconst auto retry = [=] {\n\t\t\t\tUi::hideLayer();\n\t\t\t\tsave(origin, failedFileName);\n\t\t\t};\n\t\t\tUi::show(Ui::MakeConfirmBox({\n\t\t\t\ttr::lng_download_finish_failed(),\n\t\t\t\tcrl::guard(&session(), retry)\n\t\t\t}));\n\t\t} else if (error.failureReason == FailureReason::FileWriteFailure) {\n\t\t\tif (!Core::App().settings().downloadPath().isEmpty()) {\n\t\t\t\tCore::App().settings().setDownloadPathBookmark(QByteArray());\n\t\t\t\tCore::App().settings().setDownloadPath(QString());\n\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t\tInvokeQueued(qApp, [] {\n\t\t\t\t\tUi::show(\n\t\t\t\t\t\tUi::MakeInformBox(\n\t\t\t\t\t\t\ttr::lng_download_path_failed(tr::now)));\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tfinishLoad();\n\t\tstatus = FileDownloadFailed;\n\t\t_owner->documentLoadFail(this, error.started);\n\t}, [=] {\n\t\tfinishLoad();\n\t\t_owner->documentLoadDone(this);\n\t}, _loader->lifetime());\n\n}\n\nvoid DocumentData::cancel() {\n\tif (!loading()) {\n\t\treturn;\n\t}\n\n\t_flags |= Flag::DownloadCancelled;\n\tdestroyLoader();\n\t_owner->documentLoadDone(this);\n}\n\nbool DocumentData::cancelled() const {\n\treturn (_flags & Flag::DownloadCancelled);\n}\n\nvoid DocumentData::resetCancelled() {\n\t_flags &= ~Flag::DownloadCancelled;\n}\n\nVoiceWaveform documentWaveformDecode(const QByteArray &encoded5bit) {\n\tauto bitsCount = static_cast<int>(encoded5bit.size() * 8);\n\tauto valuesCount = bitsCount / 5;\n\tif (!valuesCount) {\n\t\treturn VoiceWaveform();\n\t}\n\n\t// Read each 5 bit of encoded5bit as 0-31 unsigned char.\n\t// We count the index of the byte in which the desired 5-bit sequence starts.\n\t// And then we read a uint16 starting from that byte to guarantee to get all of those 5 bits.\n\t//\n\t// BUT! if it is the last byte we have, we're not allowed to read a uint16 starting with it.\n\t// Because it will be an overflow (we'll access one byte after the available memory).\n\t// We see, that only the last 5 bits could start in the last available byte and be problematic.\n\t// So we read in a general way all the entries in a general way except the last one.\n\tauto result = VoiceWaveform(valuesCount, 0);\n\tauto bitsData = encoded5bit.constData();\n\tfor (auto i = 0, l = valuesCount - 1; i != l; ++i) {\n\t\tauto byteIndex = (i * 5) / 8;\n\t\tauto bitShift = (i * 5) % 8;\n\t\tauto value = *reinterpret_cast<const uint16*>(bitsData + byteIndex);\n\t\tresult[i] = static_cast<char>((value >> bitShift) & 0x1F);\n\t}\n\tauto lastByteIndex = ((valuesCount - 1) * 5) / 8;\n\tauto lastBitShift = ((valuesCount - 1) * 5) % 8;\n\tauto lastValue = (lastByteIndex == encoded5bit.size() - 1)\n\t\t? static_cast<uint16>(*reinterpret_cast<const uchar*>(bitsData + lastByteIndex))\n\t\t: *reinterpret_cast<const uint16*>(bitsData + lastByteIndex);\n\tresult[valuesCount - 1] = static_cast<char>((lastValue >> lastBitShift) & 0x1F);\n\n\treturn result;\n}\n\nQByteArray documentWaveformEncode5bit(const VoiceWaveform &waveform) {\n\tauto bitsCount = waveform.size() * 5;\n\tauto bytesCount = (bitsCount + 7) / 8;\n\tauto result = QByteArray(bytesCount + 1, 0);\n\tauto bitsData = result.data();\n\n\t// Write each 0-31 unsigned char as 5 bit to result.\n\t// We reserve one extra byte to be able to dereference any of required bytes\n\t// as a uint16 without overflowing, even the byte with index \"bytesCount - 1\".\n\tfor (auto i = 0, l = int(waveform.size()); i < l; ++i) {\n\t\tauto byteIndex = (i * 5) / 8;\n\t\tauto bitShift = (i * 5) % 8;\n\t\tauto value = (static_cast<uint16>(waveform[i]) & 0x1F) << bitShift;\n\t\t*reinterpret_cast<uint16*>(bitsData + byteIndex) |= value;\n\t}\n\tresult.resize(bytesCount);\n\treturn result;\n}\n\nconst Core::FileLocation &DocumentData::location(bool check) const {\n\tif (check && !_location.check()) {\n\t\tconst auto location = session().local().readFileLocation(mediaKey());\n\t\tconst auto that = const_cast<DocumentData*>(this);\n\t\tif (location.inMediaCache()) {\n\t\t\tthat->setLoadedInMediaCacheLocation();\n\t\t} else {\n\t\t\tthat->_location = location;\n\t\t}\n\t}\n\treturn _location;\n}\n\nvoid DocumentData::setLocation(const Core::FileLocation &loc) {\n\tif (loc.inMediaCache()) {\n\t\tsetLoadedInMediaCacheLocation();\n\t} else if (loc.check()) {\n\t\t_location = loc;\n\t}\n}\n\nQString DocumentData::filepath(bool check) const {\n\treturn (check && _location.name().isEmpty())\n\t\t? QString()\n\t\t: location(check).name();\n}\n\nbool DocumentData::saveFromData() {\n\treturn !filepath(true).isEmpty() || saveFromDataChecked();\n}\n\nbool DocumentData::saveFromDataSilent() {\n\treturn !filepath(true).isEmpty()\n\t\t|| (Core::App().canSaveFileWithoutAskingForPath()\n\t\t\t&& saveFromDataChecked());\n}\n\nbool DocumentData::saveFromDataChecked() {\n\tconst auto media = activeMediaView();\n\tif (!media) {\n\t\treturn false;\n\t}\n\tconst auto bytes = media->bytes();\n\tif (bytes.isEmpty()) {\n\t\treturn false;\n\t}\n\tconst auto path = DocumentFileNameForSave(this);\n\tif (path.isEmpty()) {\n\t\treturn false;\n\t}\n\tauto file = QFile(path);\n\tif (!file.open(QIODevice::WriteOnly)\n\t\t|| file.write(bytes) != bytes.size()) {\n\t\treturn false;\n\t}\n\tfile.close();\n\t_location = Core::FileLocation(path);\n\tsession().local().writeFileLocation(mediaKey(), _location);\n\treturn true;\n}\n\nvoid DocumentData::refreshPossibleCoverThumbnail() {\n\tExpects(isSong());\n\n\tif (_thumbnail.location.valid()) {\n\t\treturn;\n\t}\n\tconst auto songData = song();\n\tif (songData->performer.isEmpty()\n\t\t|| songData->title.isEmpty()\n\t\t// Ignore cover for voice chat records.\n\t\t|| hasMimeType(u\"audio/ogg\"_q)) {\n\t\treturn;\n\t}\n\tconst auto size = kDefaultCoverThumbnailSize;\n\tconst auto location = ImageWithLocation{\n\t\t.location = ImageLocation(\n\t\t\t{ AudioAlbumThumbLocation{ id } },\n\t\t\tsize,\n\t\t\tsize)\n\t};\n\t_flags |= Flag::PossibleCoverThumbnail;\n\tupdateThumbnails({}, location, {}, false);\n\tloadThumbnail({});\n}\n\nbool DocumentData::isStickerSetInstalled() const {\n\tExpects(sticker() != nullptr);\n\n\tusing SetFlag = Data::StickersSetFlag;\n\n\tconst auto &sets = _owner->stickers().sets();\n\tif (const auto id = sticker()->set.id) {\n\t\tconst auto i = sets.find(id);\n\t\treturn (i != sets.cend())\n\t\t\t&& !(i->second->flags & SetFlag::Archived)\n\t\t\t&& (i->second->flags & SetFlag::Installed);\n\t} else {\n\t\treturn false;\n\t}\n}\n\nImage *DocumentData::getReplyPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<PeerData*> context,\n\t\tbool spoiler) {\n\tif (v::is<Data::FileOriginMessage>(origin.data)) {\n\t\tif (const auto item = _owner->message(\n\t\t\t\tv::get<FullMsgId>(origin.data))) {\n\t\t\tif (const auto cover = LookupVideoCover(this, item)) {\n\t\t\t\treturn cover->getReplyPreview(\n\t\t\t\t\tstd::move(origin),\n\t\t\t\t\tcontext,\n\t\t\t\t\tspoiler);\n\t\t\t}\n\t\t}\n\t}\n\tif (!hasThumbnail()) {\n\t\treturn nullptr;\n\t} else if (!_replyPreview) {\n\t\t_replyPreview = std::make_unique<Data::ReplyPreview>(this);\n\t}\n\treturn _replyPreview->image(origin, context, spoiler);\n}\n\nImage *DocumentData::getReplyPreview(not_null<HistoryItem*> item) {\n\tconst auto media = item->media();\n\tconst auto spoiler = media && media->hasSpoiler();\n\treturn getReplyPreview(item->fullId(), item->history()->peer, spoiler);\n}\n\nbool DocumentData::replyPreviewLoaded(bool spoiler) const {\n\tif (!hasThumbnail()) {\n\t\treturn true;\n\t} else if (!_replyPreview) {\n\t\treturn false;\n\t}\n\treturn _replyPreview->loaded(spoiler);\n}\n\nStickerData *DocumentData::sticker() const {\n\treturn (type == StickerDocument)\n\t\t? static_cast<StickerData*>(_additional.get())\n\t\t: nullptr;\n}\n\nData::FileOrigin DocumentData::stickerSetOrigin() const {\n\tif (const auto data = sticker()) {\n\t\tif (const auto result = data->setOrigin()) {\n\t\t\treturn result;\n\t\t} else if (owner().stickers().isFaved(this)) {\n\t\t\treturn Data::FileOriginStickerSet(Data::Stickers::FavedSetId, 0);\n\t\t}\n\t}\n\treturn Data::FileOrigin();\n}\n\nData::FileOrigin DocumentData::stickerOrGifOrigin() const {\n\treturn (sticker()\n\t\t? stickerSetOrigin()\n\t\t: isGifv()\n\t\t? Data::FileOriginSavedGifs()\n\t\t: Data::FileOrigin());\n}\n\nSongData *DocumentData::song() {\n\treturn isSong()\n\t\t? static_cast<SongData*>(_additional.get())\n\t\t: nullptr;\n}\n\nconst SongData *DocumentData::song() const {\n\treturn const_cast<DocumentData*>(this)->song();\n}\n\nVoiceData *DocumentData::voice() {\n\treturn isVoiceMessage()\n\t\t? static_cast<VoiceData*>(_additional.get())\n\t\t: nullptr;\n}\n\nconst VoiceData *DocumentData::voice() const {\n\treturn const_cast<DocumentData*>(this)->voice();\n}\n\nRoundData *DocumentData::round() {\n\treturn isVideoMessage()\n\t\t? static_cast<RoundData*>(_additional.get())\n\t\t: nullptr;\n}\n\nconst RoundData *DocumentData::round() const {\n\treturn const_cast<DocumentData*>(this)->round();\n}\n\nVideoData *DocumentData::video() {\n\treturn isVideoFile()\n\t\t? static_cast<VideoData*>(_additional.get())\n\t\t: nullptr;\n}\n\nconst VideoData *DocumentData::video() const {\n\treturn const_cast<DocumentData*>(this)->video();\n}\n\nbool DocumentData::hasRemoteLocation() const {\n\treturn (_dc != 0 && _access != 0);\n}\n\nbool DocumentData::useStreamingLoader() const {\n\tif (size <= 0) {\n\t\treturn false;\n\t} else if (const auto info = sticker()) {\n\t\treturn info->isWebm();\n\t}\n\treturn isAnimation()\n\t\t|| isVideoFile()\n\t\t|| isAudioFile()\n\t\t|| isVoiceMessage();\n}\n\nbool DocumentData::canBeStreamed() const {\n\treturn hasRemoteLocation() && supportsStreaming();\n}\n\nvoid DocumentData::setInappPlaybackFailed() {\n\t_flags |= Flag::StreamingPlaybackFailed;\n}\n\nbool DocumentData::inappPlaybackFailed() const {\n\treturn (_flags & Flag::StreamingPlaybackFailed);\n}\n\nint DocumentData::videoPreloadPrefix() const {\n\treturn _videoPreloadPrefix;\n}\n\nStorageFileLocation DocumentData::videoPreloadLocation() const {\n\treturn hasRemoteLocation()\n\t\t? StorageFileLocation(\n\t\t\t_dc,\n\t\t\tsession().userId(),\n\t\t\tMTP_inputDocumentFileLocation(\n\t\t\t\tMTP_long(id),\n\t\t\t\tMTP_long(_access),\n\t\t\t\tMTP_bytes(_fileReference),\n\t\t\t\tMTP_string()))\n\t\t: StorageFileLocation();\n}\n\nauto DocumentData::createStreamingLoader(\n\tData::FileOrigin origin,\n\tbool forceRemoteLoader) const\n-> std::unique_ptr<Media::Streaming::Loader> {\n\tif (!useStreamingLoader()) {\n\t\treturn nullptr;\n\t}\n\tif (!forceRemoteLoader) {\n\t\tconst auto media = activeMediaView();\n\t\tconst auto &location = this->location(true);\n\t\tif (media && !media->bytes().isEmpty()) {\n\t\t\treturn Media::Streaming::MakeBytesLoader(media->bytes());\n\t\t} else if (!location.isEmpty() && location.accessEnable()) {\n\t\t\tauto result = Media::Streaming::MakeFileLoader(location.name());\n\t\t\tlocation.accessDisable();\n\t\t\treturn result;\n\t\t}\n\t}\n\treturn hasRemoteLocation()\n\t\t? std::make_unique<Media::Streaming::LoaderMtproto>(\n\t\t\t&session().downloader(),\n\t\t\tStorageFileLocation(\n\t\t\t\t_dc,\n\t\t\t\tsession().userId(),\n\t\t\t\tMTP_inputDocumentFileLocation(\n\t\t\t\t\tMTP_long(id),\n\t\t\t\t\tMTP_long(_access),\n\t\t\t\t\tMTP_bytes(_fileReference),\n\t\t\t\t\tMTP_string())),\n\t\t\tsize,\n\t\t\torigin)\n\t\t: nullptr;\n}\n\nbool DocumentData::hasWebLocation() const {\n\treturn !_urlLocation.url().isEmpty();\n}\n\nbool DocumentData::isNull() const {\n\treturn !hasRemoteLocation()\n\t\t&& !hasWebLocation()\n\t\t&& _url.isEmpty()\n\t\t&& !uploading()\n\t\t&& _location.isEmpty();\n}\n\nMTPInputDocument DocumentData::mtpInput() const {\n\tif (_access) {\n\t\treturn MTP_inputDocument(\n\t\t\tMTP_long(id),\n\t\t\tMTP_long(_access),\n\t\t\tMTP_bytes(_fileReference));\n\t}\n\treturn MTP_inputDocumentEmpty();\n}\n\nQByteArray DocumentData::fileReference() const {\n\treturn _fileReference;\n}\n\nvoid DocumentData::refreshFileReference(const QByteArray &value) {\n\t_fileReference = value;\n\t_thumbnail.location.refreshFileReference(value);\n\t_videoThumbnail.location.refreshFileReference(value);\n}\n\nQString DocumentData::filename() const {\n\treturn _filename;\n}\n\nCore::NameType DocumentData::nameType() const {\n\treturn _nameType;\n}\n\nQString DocumentData::mimeString() const {\n\treturn _mimeString;\n}\n\nbool DocumentData::hasMimeType(const QString &mime) const {\n\treturn (_mimeString == mime);\n}\n\nvoid DocumentData::setMimeString(const QString &mime) {\n\t_mimeString = mime;\n\t_mimeString = std::move(_mimeString).toLower();\n}\n\nMediaKey DocumentData::mediaKey() const {\n\treturn ::mediaKey(locationType(), _dc, id);\n}\n\nStorage::Cache::Key DocumentData::cacheKey() const {\n\tif (hasWebLocation()) {\n\t\treturn Data::WebDocumentCacheKey(_urlLocation);\n\t} else if (!_access && !_url.isEmpty()) {\n\t\treturn Data::UrlCacheKey(_url);\n\t} else {\n\t\treturn Data::DocumentCacheKey(_dc, id);\n\t}\n}\n\nuint8 DocumentData::cacheTag() const {\n\tif (type == StickerDocument) {\n\t\treturn Data::kStickerCacheTag;\n\t} else if (isVoiceMessage()) {\n\t\treturn Data::kVoiceMessageCacheTag;\n\t} else if (isVideoMessage()) {\n\t\treturn Data::kVideoMessageCacheTag;\n\t} else if (isAnimation()) {\n\t\treturn Data::kAnimationCacheTag;\n\t} else if (isWallPaper()) {\n\t\treturn Data::kImageCacheTag;\n\t}\n\treturn 0;\n}\n\nLocationType DocumentData::locationType() const {\n\treturn isVoiceMessage()\n\t\t? AudioFileLocation\n\t\t: isVideoFile()\n\t\t? VideoFileLocation\n\t\t: DocumentFileLocation;\n}\n\nvoid DocumentData::forceIsStreamedAnimation() {\n\ttype = AnimatedDocument;\n\t_additional = nullptr;\n\tsetMaybeSupportsStreaming(true);\n}\n\nbool DocumentData::isMusicForProfile() const {\n\treturn isSong();\n}\n\nbool DocumentData::isVoiceMessage() const {\n\treturn (type == VoiceDocument);\n}\n\nbool DocumentData::isVideoMessage() const {\n\treturn (type == RoundVideoDocument);\n}\n\nbool DocumentData::isAnimation() const {\n\treturn (type == AnimatedDocument)\n\t\t|| isVideoMessage()\n\t\t|| ((_filename.isEmpty()\n\t\t\t|| _nameType == Core::NameType::Image\n\t\t\t|| _nameType == Core::NameType::Video)\n\t\t\t&& hasMimeType(u\"image/gif\"_q)\n\t\t\t&& !(_flags & Flag::StreamingPlaybackFailed));\n}\n\nbool DocumentData::isGifv() const {\n\treturn (type == AnimatedDocument)\n\t\t&& hasMimeType(u\"video/mp4\"_q);\n}\n\nbool DocumentData::isTheme() const {\n\treturn _filename.endsWith(u\".tdesktop-theme\"_q, Qt::CaseInsensitive)\n\t\t|| _filename.endsWith(u\".tdesktop-palette\"_q, Qt::CaseInsensitive)\n\t\t|| (hasMimeType(u\"application/x-tgtheme-tdesktop\"_q)\n\t\t\t&& (_filename.isEmpty()\n\t\t\t\t|| !_filename.contains('.')\n\t\t\t\t|| _nameType == Core::NameType::ThemeFile));\n}\n\nbool DocumentData::isSong() const {\n\treturn (type == SongDocument);\n}\n\nbool DocumentData::isSongWithCover() const {\n\treturn isSong() && hasThumbnail();\n}\n\nbool DocumentData::isAudioFile() const {\n\tif (isVoiceMessage() || isVideoFile()) {\n\t\treturn false;\n\t} else if (isSong()) {\n\t\treturn true;\n\t}\n\tconst auto prefix = u\"audio/\"_q;\n\tif (!_mimeString.startsWith(prefix, Qt::CaseInsensitive)) {\n\t\tif (_filename.endsWith(u\".opus\"_q, Qt::CaseInsensitive)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t} else if (!_filename.isEmpty()\n\t\t&& _nameType != Core::NameType::Audio\n\t\t&& _nameType != Core::NameType::Video) {\n\t\treturn false;\n\t}\n\tconst auto left = _mimeString.mid(prefix.size());\n\tconst auto types = { u\"x-wav\"_q, u\"wav\"_q, u\"mp4\"_q };\n\treturn ranges::contains(types, left);\n}\n\nbool DocumentData::isSharedMediaMusic() const {\n\treturn isSong();\n}\n\nbool DocumentData::isVideoFile() const {\n\treturn (type == VideoDocument);\n}\n\nbool DocumentData::isSilentVideo() const {\n\treturn _flags & Flag::SilentVideo;\n}\n\ncrl::time DocumentData::duration() const {\n\treturn std::max(_duration, crl::time());\n}\n\nbool DocumentData::hasDuration() const {\n\treturn _duration >= 0;\n}\n\nbool DocumentData::isImage() const {\n\treturn (_flags & Flag::ImageType);\n}\n\nbool DocumentData::hasAttachedStickers() const {\n\treturn (_flags & Flag::HasAttachedStickers);\n}\n\nbool DocumentData::supportsStreaming() const {\n\treturn (_flags & kStreamingSupportedMask) == kStreamingSupportedMaybeYes;\n}\n\nvoid DocumentData::setNotSupportsStreaming() {\n\t_flags &= ~kStreamingSupportedMask;\n\t_flags |= kStreamingSupportedNo;\n}\n\nvoid DocumentData::setMaybeSupportsStreaming(bool supports) {\n\tif ((_flags & kStreamingSupportedMask) == kStreamingSupportedNo) {\n\t\treturn;\n\t}\n\t_flags &= ~kStreamingSupportedMask;\n\t_flags |= supports\n\t\t? kStreamingSupportedMaybeYes\n\t\t: kStreamingSupportedMaybeNo;\n}\n\nvoid DocumentData::recountIsImage() {\n\tconst auto isImage = !isAnimation()\n\t\t&& !isVideoFile()\n\t\t&& Core::FileIsImage(filename(), mimeString());\n\tif (isImage) {\n\t\t_flags |= Flag::ImageType;\n\t} else {\n\t\t_flags &= ~Flag::ImageType;\n\t}\n}\n\nvoid DocumentData::setRemoteLocation(\n\t\tint32 dc,\n\t\tuint64 access,\n\t\tconst QByteArray &fileReference) {\n\t_fileReference = fileReference;\n\tif (_dc != dc || _access != access) {\n\t\t_dc = dc;\n\t\t_access = access;\n\t\tif (!isNull()) {\n\t\t\tif (_location.check()) {\n\t\t\t\tsession().local().writeFileLocation(mediaKey(), _location);\n\t\t\t} else {\n\t\t\t\t_location = session().local().readFileLocation(mediaKey());\n\t\t\t\tif (_location.inMediaCache()) {\n\t\t\t\t\tsetLoadedInMediaCacheLocation();\n\t\t\t\t} else if (_location.isEmpty() && loadedInMediaCache()) {\n\t\t\t\t\tsession().local().writeFileLocation(\n\t\t\t\t\t\tmediaKey(),\n\t\t\t\t\t\tCore::FileLocation::InMediaCacheLocation());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid DocumentData::setStoryMedia(bool value) {\n\tif (value) {\n\t\t_flags |= Flag::StoryDocument;\n\t\tsetMaybeSupportsStreaming(true);\n\t} else {\n\t\t_flags &= ~Flag::StoryDocument;\n\t}\n}\n\nbool DocumentData::storyMedia() const {\n\treturn (_flags & Flag::StoryDocument);\n}\n\nvoid DocumentData::setContentUrl(const QString &url) {\n\t_url = url;\n}\n\nvoid DocumentData::setWebLocation(const WebFileLocation &location) {\n\t_urlLocation = location;\n}\n\nvoid DocumentData::collectLocalData(not_null<DocumentData*> local) {\n\tif (local == this) {\n\t\treturn;\n\t}\n\n\t_owner->cache().copyIfEmpty(local->cacheKey(), cacheKey());\n\tif (const auto localMedia = local->activeMediaView()) {\n\t\tauto media = createMediaView();\n\t\tmedia->collectLocalData(localMedia.get());\n\t\t_owner->keepAlive(std::move(media));\n\t}\n\tif (!local->_location.inMediaCache() && !local->_location.isEmpty()) {\n\t\t_location = local->_location;\n\t\tsession().local().writeFileLocation(mediaKey(), _location);\n\t}\n}\n\nPhotoData *LookupVideoCover(\n\t\tnot_null<DocumentData*> document,\n\t\tHistoryItem *item) {\n\tconst auto media = item ? item->media() : nullptr;\n\tif (const auto webpage = media ? media->webpage() : nullptr) {\n\t\tif (webpage->document == document && webpage->photoIsVideoCover) {\n\t\t\treturn webpage->photo;\n\t\t}\n\t\treturn nullptr;\n\t}\n\treturn (media && media->document() == document)\n\t\t? media->videoCover()\n\t\t: nullptr;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_document.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n#include \"base/binary_guard.h\"\n#include \"data/data_types.h\"\n#include \"data/data_cloud_file.h\"\n#include \"core/file_location.h\"\n\nclass HistoryItem;\nclass PhotoData;\nenum class ChatRestriction;\nclass mtpFileLoader;\n\nnamespace Images {\nclass Source;\n} // namespace Images\n\nnamespace Core {\nenum class NameType : uchar;\n} // namespace Core\n\nnamespace Storage {\nnamespace Cache {\nstruct Key;\n} // namespace Cache\n} // namespace Storage\n\nnamespace Media {\nstruct VideoQuality;\n} // namespace Media\n\nnamespace Media::Streaming {\nclass Loader;\n} // namespace Media::Streaming\n\nnamespace Data {\nclass Session;\nclass DocumentMedia;\nclass ReplyPreview;\nenum class StickersType : uchar;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\ninline uint64 mediaMix32To64(int32 a, int32 b) {\n\treturn (uint64(*reinterpret_cast<uint32*>(&a)) << 32)\n\t\t| uint64(*reinterpret_cast<uint32*>(&b));\n}\n\n// version field removed from document.\ninline MediaKey mediaKey(LocationType type, int32 dc, const uint64 &id) {\n\treturn MediaKey(mediaMix32To64(type, dc), id);\n}\n\nstruct DocumentAdditionalData {\n\tvirtual ~DocumentAdditionalData() = default;\n\n};\n\nenum class StickerType : uchar {\n\tWebp,\n\tTgs,\n\tWebm,\n};\n\nstruct StickerData : public DocumentAdditionalData {\n\t[[nodiscard]] Data::FileOrigin setOrigin() const;\n\t[[nodiscard]] bool isStatic() const;\n\t[[nodiscard]] bool isLottie() const;\n\t[[nodiscard]] bool isAnimated() const;\n\t[[nodiscard]] bool isWebm() const;\n\n\tQString alt;\n\tStickerSetIdentifier set;\n\tStickerType type = StickerType::Webp;\n\tData::StickersType setType = Data::StickersType();\n};\n\nstruct SongData : public DocumentAdditionalData {\n\tQString title, performer;\n};\n\nstruct VoiceData : public DocumentAdditionalData {\n\t~VoiceData();\n\n\tVoiceWaveform waveform;\n\tchar wavemax = 0;\n};\n\nstruct VideoData : public DocumentAdditionalData {\n\tQString codec;\n\tstd::vector<not_null<DocumentData*>> qualities;\n};\n\nusing RoundData = VoiceData;\n\nnamespace Serialize {\nclass Document;\n} // namespace Serialize;\n\nclass DocumentData final {\npublic:\n\tDocumentData(not_null<Data::Session*> owner, DocumentId id);\n\t~DocumentData();\n\n\t[[nodiscard]] Data::Session &owner() const;\n\t[[nodiscard]] Main::Session &session() const;\n\n\tvoid setattributes(\n\t\tconst QVector<MTPDocumentAttribute> &attributes);\n\tvoid setVideoQualities(const QVector<MTPDocument> &list);\n\n\tvoid automaticLoadSettingsChanged();\n\tvoid setVideoQualities(std::vector<not_null<DocumentData*>> qualities);\n\t[[nodiscard]] int resolveVideoQuality() const;\n\t[[nodiscard]] auto resolveQualities(HistoryItem *context) const\n\t\t-> const std::vector<not_null<DocumentData*>> &;\n\t[[nodiscard]] not_null<DocumentData*> chooseQuality(\n\t\tHistoryItem *context,\n\t\tMedia::VideoQuality request);\n\n\t[[nodiscard]] bool loading() const;\n\t[[nodiscard]] QString loadingFilePath() const;\n\t[[nodiscard]] bool displayLoading() const;\n\tvoid save(\n\t\tData::FileOrigin origin,\n\t\tconst QString &toFile,\n\t\tLoadFromCloudSetting fromCloud = LoadFromCloudOrLocal,\n\t\tbool autoLoading = false);\n\tvoid cancel();\n\t[[nodiscard]] bool cancelled() const;\n\tvoid resetCancelled();\n\t[[nodiscard]] float64 progress() const;\n\t[[nodiscard]] int64 loadOffset() const;\n\t[[nodiscard]] bool uploading() const;\n\t[[nodiscard]] bool loadedInMediaCache() const;\n\tvoid setLoadedInMediaCache(bool loaded);\n\n\t[[nodiscard]] ChatRestriction requiredSendRight() const;\n\n\tvoid setWaitingForAlbum();\n\t[[nodiscard]] bool waitingForAlbum() const;\n\n\t[[nodiscard]] const Core::FileLocation &location(\n\t\tbool check = false) const;\n\tvoid setLocation(const Core::FileLocation &loc);\n\n\tbool saveFromData();\n\tbool saveFromDataSilent();\n\t[[nodiscard]] QString filepath(bool check = false) const;\n\n\tvoid forceToCache(bool force);\n\t[[nodiscard]] bool saveToCache() const;\n\n\t[[nodiscard]] Image *getReplyPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<PeerData*> context,\n\t\tbool spoiler);\n\t[[nodiscard]] Image *getReplyPreview(not_null<HistoryItem*> item);\n\t[[nodiscard]] bool replyPreviewLoaded(bool spoiler) const;\n\n\t[[nodiscard]] StickerData *sticker() const;\n\t[[nodiscard]] Data::FileOrigin stickerSetOrigin() const;\n\t[[nodiscard]] Data::FileOrigin stickerOrGifOrigin() const;\n\t[[nodiscard]] bool isStickerSetInstalled() const;\n\t[[nodiscard]] SongData *song();\n\t[[nodiscard]] const SongData *song() const;\n\t[[nodiscard]] VoiceData *voice();\n\t[[nodiscard]] const VoiceData *voice() const;\n\t[[nodiscard]] RoundData *round();\n\t[[nodiscard]] const RoundData *round() const;\n\t[[nodiscard]] VideoData *video();\n\t[[nodiscard]] const VideoData *video() const;\n\n\tvoid forceIsStreamedAnimation();\n\t[[nodiscard]] bool isMusicForProfile() const;\n\t[[nodiscard]] bool isVoiceMessage() const;\n\t[[nodiscard]] bool isVideoMessage() const;\n\t[[nodiscard]] bool isSong() const;\n\t[[nodiscard]] bool isSongWithCover() const;\n\t[[nodiscard]] bool isAudioFile() const;\n\t[[nodiscard]] bool isVideoFile() const;\n\t[[nodiscard]] bool isSilentVideo() const;\n\t[[nodiscard]] bool isAnimation() const;\n\t[[nodiscard]] bool isGifv() const;\n\t[[nodiscard]] bool isTheme() const;\n\t[[nodiscard]] bool isSharedMediaMusic() const;\n\t[[nodiscard]] crl::time duration() const;\n\t[[nodiscard]] bool hasDuration() const;\n\t[[nodiscard]] bool isImage() const;\n\tvoid recountIsImage();\n\t[[nodiscard]] bool supportsStreaming() const;\n\tvoid setNotSupportsStreaming();\n\tvoid setDataAndCache(const QByteArray &data);\n\tbool checkWallPaperProperties();\n\t[[nodiscard]] bool isWallPaper() const;\n\t[[nodiscard]] bool isPatternWallPaper() const;\n\t[[nodiscard]] bool isPatternWallPaperPNG() const;\n\t[[nodiscard]] bool isPatternWallPaperSVG() const;\n\t[[nodiscard]] bool isSvgImage() const;\n\t[[nodiscard]] bool isPremiumSticker() const;\n\t[[nodiscard]] bool isPremiumEmoji() const;\n\t[[nodiscard]] bool emojiUsesTextColor() const;\n\tvoid overrideEmojiUsesTextColor(bool value);\n\n\t[[nodiscard]] bool hasThumbnail() const;\n\t[[nodiscard]] bool thumbnailLoading() const;\n\t[[nodiscard]] bool thumbnailFailed() const;\n\tvoid loadThumbnail(Data::FileOrigin origin);\n\t[[nodiscard]] const ImageLocation &thumbnailLocation() const;\n\t[[nodiscard]] int thumbnailByteSize() const;\n\n\t[[nodiscard]] bool hasVideoThumbnail() const;\n\t[[nodiscard]] bool videoThumbnailLoading() const;\n\t[[nodiscard]] bool videoThumbnailFailed() const;\n\tvoid loadVideoThumbnail(Data::FileOrigin origin);\n\t[[nodiscard]] const ImageLocation &videoThumbnailLocation() const;\n\t[[nodiscard]] int videoThumbnailByteSize() const;\n\n\tvoid updateThumbnails(\n\t\tconst InlineImageLocation &inlineThumbnail,\n\t\tconst ImageWithLocation &thumbnail,\n\t\tconst ImageWithLocation &videoThumbnail,\n\t\tbool isPremiumSticker);\n\n\t[[nodiscard]] QByteArray inlineThumbnailBytes() const {\n\t\treturn _inlineThumbnailBytes;\n\t}\n\t[[nodiscard]] bool inlineThumbnailIsPath() const {\n\t\treturn (_flags & Flag::InlineThumbnailIsPath);\n\t}\n\tvoid clearInlineThumbnailBytes() {\n\t\t_inlineThumbnailBytes = QByteArray();\n\t}\n\n\t[[nodiscard]] Storage::Cache::Key goodThumbnailCacheKey() const;\n\t[[nodiscard]] bool goodThumbnailChecked() const;\n\t[[nodiscard]] bool goodThumbnailGenerating() const;\n\t[[nodiscard]] bool goodThumbnailNoData() const;\n\tvoid setGoodThumbnailGenerating();\n\tvoid setGoodThumbnailDataReady();\n\tvoid setGoodThumbnailChecked(bool hasData);\n\n\t[[nodiscard]] std::shared_ptr<Data::DocumentMedia> createMediaView();\n\t[[nodiscard]] auto activeMediaView() const\n\t\t-> std::shared_ptr<Data::DocumentMedia>;\n\tvoid setGoodThumbnailPhoto(not_null<PhotoData*> photo);\n\t[[nodiscard]] PhotoData *goodThumbnailPhoto() const;\n\n\t[[nodiscard]] Storage::Cache::Key bigFileBaseCacheKey() const;\n\n\tvoid setStoryMedia(bool value);\n\t[[nodiscard]] bool storyMedia() const;\n\n\tvoid setRemoteLocation(\n\t\tint32 dc,\n\t\tuint64 access,\n\t\tconst QByteArray &fileReference);\n\tvoid setContentUrl(const QString &url);\n\tvoid setWebLocation(const WebFileLocation &location);\n\t[[nodiscard]] bool hasRemoteLocation() const;\n\t[[nodiscard]] bool hasWebLocation() const;\n\t[[nodiscard]] bool isNull() const;\n\t[[nodiscard]] MTPInputDocument mtpInput() const;\n\t[[nodiscard]] QByteArray fileReference() const;\n\tvoid refreshFileReference(const QByteArray &value);\n\n\t// When we have some client-side generated document\n\t// (for example for displaying an external inline bot result)\n\t// and it has downloaded data, we can collect that data from it\n\t// to (this) received from the server \"same\" document.\n\tvoid collectLocalData(not_null<DocumentData*> local);\n\n\t[[nodiscard]] QString filename() const;\n\t[[nodiscard]] Core::NameType nameType() const;\n\t[[nodiscard]] QString mimeString() const;\n\t[[nodiscard]] bool hasMimeType(const QString &mime) const;\n\tvoid setMimeString(const QString &mime);\n\n\t[[nodiscard]] bool hasAttachedStickers() const;\n\n\t[[nodiscard]] MediaKey mediaKey() const;\n\t[[nodiscard]] Storage::Cache::Key cacheKey() const;\n\t[[nodiscard]] uint8 cacheTag() const;\n\n\t[[nodiscard]] bool canBeStreamed() const;\n\t[[nodiscard]] auto createStreamingLoader(\n\t\tData::FileOrigin origin,\n\t\tbool forceRemoteLoader) const\n\t-> std::unique_ptr<Media::Streaming::Loader>;\n\t[[nodiscard]] bool useStreamingLoader() const;\n\n\tvoid setInappPlaybackFailed();\n\t[[nodiscard]] bool inappPlaybackFailed() const;\n\t[[nodiscard]] int videoPreloadPrefix() const;\n\t[[nodiscard]] StorageFileLocation videoPreloadLocation() const;\n\n\tDocumentId id = 0;\n\tint64 size = 0;\n\tQSize dimensions;\n\tint32 date = 0;\n\tDocumentType type = FileDocument;\n\tFileStatus status = FileReady;\n\n\tstd::unique_ptr<Data::UploadState> uploadingData;\n\nprivate:\n\tenum class Flag : ushort {\n\t\tStreamingMaybeYes = 0x0001,\n\t\tStreamingMaybeNo = 0x0002,\n\t\tStreamingPlaybackFailed = 0x0004,\n\t\tImageType = 0x0008,\n\t\tDownloadCancelled = 0x0010,\n\t\tLoadedInMediaCache = 0x0020,\n\t\tHasAttachedStickers = 0x0040,\n\t\tInlineThumbnailIsPath = 0x0080,\n\t\tForceToCache = 0x0100,\n\t\tPremiumSticker = 0x0200,\n\t\tPossibleCoverThumbnail = 0x0400,\n\t\tUseTextColor = 0x0800,\n\t\tStoryDocument = 0x1000,\n\t\tSilentVideo = 0x2000,\n\t};\n\tusing Flags = base::flags<Flag>;\n\tfriend constexpr bool is_flag_type(Flag) { return true; };\n\n\tenum class GoodThumbnailFlag : uchar {\n\t\tChecked = 0x01,\n\t\tGenerating = 0x02,\n\t\tNoData = 0x03,\n\t\tMask = 0x03,\n\n\t\tDataReady = 0x04,\n\t};\n\tusing GoodThumbnailState = base::flags<GoodThumbnailFlag>;\n\tfriend constexpr bool is_flag_type(GoodThumbnailFlag) { return true; };\n\n\tstatic constexpr Flags kStreamingSupportedMask = Flags()\n\t\t| Flag::StreamingMaybeYes\n\t\t| Flag::StreamingMaybeNo;\n\tstatic constexpr Flags kStreamingSupportedUnknown = Flags()\n\t\t| Flag::StreamingMaybeYes\n\t\t| Flag::StreamingMaybeNo;\n\tstatic constexpr Flags kStreamingSupportedMaybeYes = Flags()\n\t\t| Flag::StreamingMaybeYes;\n\tstatic constexpr Flags kStreamingSupportedMaybeNo = Flags()\n\t\t| Flag::StreamingMaybeNo;\n\tstatic constexpr Flags kStreamingSupportedNo = Flags();\n\n\tfriend class Serialize::Document;\n\n\t[[nodiscard]] LocationType locationType() const;\n\tvoid validateLottieSticker();\n\tvoid setMaybeSupportsStreaming(bool supports);\n\tvoid setLoadedInMediaCacheLocation();\n\tvoid setFileName(const QString &remoteFileName);\n\tbool enforceNameType(Core::NameType nameType);\n\n\tvoid finishLoad();\n\tvoid handleLoaderUpdates();\n\tvoid destroyLoader();\n\n\tbool saveFromDataChecked();\n\n\tvoid refreshPossibleCoverThumbnail();\n\n\tconst not_null<Data::Session*> _owner;\n\n\tint _videoPreloadPrefix = 0;\n\t// Two types of location: from MTProto by dc+access or from web by url\n\tint32 _dc = 0;\n\tuint64 _access = 0;\n\tQByteArray _fileReference;\n\tQString _url;\n\tQString _filename;\n\tQString _mimeString;\n\tWebFileLocation _urlLocation;\n\n\tQByteArray _inlineThumbnailBytes;\n\tData::CloudFile _thumbnail;\n\tData::CloudFile _videoThumbnail;\n\tstd::unique_ptr<Data::ReplyPreview> _replyPreview;\n\tstd::weak_ptr<Data::DocumentMedia> _media;\n\tPhotoData *_goodThumbnailPhoto = nullptr;\n\tcrl::time _duration = -1;\n\n\tCore::FileLocation _location;\n\tstd::unique_ptr<DocumentAdditionalData> _additional;\n\tmutable Flags _flags = kStreamingSupportedUnknown;\n\tGoodThumbnailState _goodThumbnailState = GoodThumbnailState();\n\tCore::NameType _nameType = Core::NameType();\n\tstd::unique_ptr<FileLoader> _loader;\n\n};\n\n[[nodiscard]] PhotoData *LookupVideoCover(\n\tnot_null<DocumentData*> document,\n\tHistoryItem *item);\n\nVoiceWaveform documentWaveformDecode(const QByteArray &encoded5bit);\nQByteArray documentWaveformEncode5bit(const VoiceWaveform &waveform);\n\nQString FileNameForSave(\n\tnot_null<Main::Session*> session,\n\tconst QString &title,\n\tconst QString &filter,\n\tconst QString &prefix,\n\tQString name,\n\tbool savingAs,\n\tconst QDir &dir = QDir());\n\nQString DocumentFileNameForSave(\n\tnot_null<const DocumentData*> data,\n\tbool forceSavingAs = false,\n\tconst QString &already = QString(),\n\tconst QDir &dir = QDir());\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_document_media.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_document_media.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_document_resolver.h\"\n#include \"data/data_session.h\"\n#include \"data/data_file_origin.h\"\n#include \"media/clip/media_clip_reader.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"lottie/lottie_animation.h\"\n#include \"lottie/lottie_frame_generator.h\"\n#include \"ffmpeg/ffmpeg_frame_generator.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"window/themes/window_theme_preview.h\"\n#include \"core/core_settings.h\"\n#include \"core/application.h\"\n#include \"core/mime_type.h\"\n#include \"storage/file_download.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/image/svg_preview.h\"\n#include \"ui/rect.h\"\n\n#include <QtCore/QBuffer>\n#include <QtGui/QImageReader>\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kReadAreaLimit = 12'032 * 9'024;\nconstexpr auto kWallPaperThumbnailLimit = 960;\nconstexpr auto kGoodThumbQuality = 87;\n\nenum class FileType {\n\tVideo,\n\tVideoSticker,\n\tAnimatedSticker,\n\tWallPaper,\n\tWallPatternPNG,\n\tWallPatternSVG,\n\tTheme,\n\tSvgImage,\n};\n\n[[nodiscard]] bool MayHaveGoodThumbnail(not_null<DocumentData*> owner) {\n\treturn owner->isVideoFile()\n\t\t|| owner->isAnimation()\n\t\t|| owner->isWallPaper()\n\t\t|| owner->isTheme()\n\t\t|| owner->isSvgImage()\n\t\t|| (owner->sticker() && owner->sticker()->isAnimated());\n}\n\n[[nodiscard]] QImage PrepareGoodThumbnail(\n\t\tconst QString &path,\n\t\tQByteArray data,\n\t\tFileType type) {\n\tif (type == FileType::Video || type == FileType::VideoSticker) {\n\t\tauto result = v::get<Ui::PreparedFileInformation::Video>(\n\t\t\t::Media::Clip::PrepareForSending(path, data).media);\n\t\tif (result.isWebmSticker && type == FileType::Video) {\n\t\t\tresult.thumbnail = Images::Opaque(std::move(result.thumbnail));\n\t\t}\n\t\treturn result.thumbnail;\n\t} else if (type == FileType::AnimatedSticker) {\n\t\treturn Lottie::ReadThumbnail(Lottie::ReadContent(data, path));\n\t} else if (type == FileType::Theme) {\n\t\treturn Window::Theme::GeneratePreview(data, path);\n\t} else if (type == FileType::WallPatternSVG) {\n\t\treturn Images::Read({\n\t\t\t.path = path,\n\t\t\t.content = std::move(data),\n\t\t\t.maxSize = QSize(\n\t\t\t\tkWallPaperThumbnailLimit,\n\t\t\t\tkWallPaperThumbnailLimit),\n\t\t\t.gzipSvg = true,\n\t\t}).image;\n\t} else if (type == FileType::SvgImage) {\n\t\tif (data.isEmpty() && !path.isEmpty()) {\n\t\t\tauto file = QFile(path);\n\t\t\tif (file.open(QIODevice::ReadOnly)) {\n\t\t\t\tconst auto limit = Ui::SvgPreviewBytesLimit();\n\t\t\t\tif (!file.isSequential() && (file.size() > limit)) {\n\t\t\t\t\treturn QImage();\n\t\t\t\t}\n\t\t\t\tdata = file.read(limit + 1);\n\t\t\t\tif (data.size() > limit) {\n\t\t\t\t\treturn QImage();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn Ui::RenderSvgPreview(data, Size(kWallPaperThumbnailLimit));\n\t}\n\tauto buffer = QBuffer(&data);\n\tauto file = QFile(path);\n\tauto device = data.isEmpty() ? static_cast<QIODevice*>(&file) : &buffer;\n\tauto reader = QImageReader(device);\n\tconst auto size = reader.size();\n\tif (!reader.canRead()\n\t\t|| (size.width() * size.height() > kReadAreaLimit)) {\n\t\treturn QImage();\n\t}\n\tauto result = reader.read();\n\tif (!result.width() || !result.height()) {\n\t\treturn QImage();\n\t}\n\treturn (result.width() > kWallPaperThumbnailLimit\n\t\t|| result.height() > kWallPaperThumbnailLimit)\n\t\t? result.scaled(\n\t\t\tkWallPaperThumbnailLimit,\n\t\t\tkWallPaperThumbnailLimit,\n\t\t\tQt::KeepAspectRatio,\n\t\t\tQt::SmoothTransformation)\n\t\t: result;\n}\n\n} // namespace\n\nVideoPreviewState::VideoPreviewState(DocumentMedia *media)\n: _media(media)\n, _usingThumbnail(media ? media->owner()->hasVideoThumbnail() : false) {\n}\n\nvoid VideoPreviewState::automaticLoad(Data::FileOrigin origin) const {\n\tExpects(_media != nullptr);\n\n\tif (_usingThumbnail) {\n\t\t_media->videoThumbnailWanted(origin);\n\t} else {\n\t\t_media->automaticLoad(origin, nullptr);\n\t}\n}\n\n::Media::Clip::ReaderPointer VideoPreviewState::makeAnimation(\n\t\tFn<void(::Media::Clip::Notification)> callback) const {\n\tExpects(_media != nullptr);\n\tExpects(loaded());\n\n\treturn _usingThumbnail\n\t\t? ::Media::Clip::MakeReader(\n\t\t\t_media->videoThumbnailContent(),\n\t\t\tstd::move(callback))\n\t\t: ::Media::Clip::MakeReader(\n\t\t\t_media->owner()->location(),\n\t\t\t_media->bytes(),\n\t\t\tstd::move(callback));\n}\n\nbool VideoPreviewState::usingThumbnail() const {\n\treturn _usingThumbnail;\n}\n\nbool VideoPreviewState::loading() const {\n\treturn _usingThumbnail\n\t\t? _media->owner()->videoThumbnailLoading()\n\t\t: _media\n\t\t? _media->owner()->loading()\n\t\t: false;\n}\n\nbool VideoPreviewState::loaded() const {\n\treturn _usingThumbnail\n\t\t? !_media->videoThumbnailContent().isEmpty()\n\t\t: _media\n\t\t? _media->loaded()\n\t\t: false;\n}\n\nDocumentMedia::DocumentMedia(not_null<DocumentData*> owner)\n: _owner(owner) {\n}\n\n// NB! Right now DocumentMedia can outlive Main::Session!\n// In DocumentData::collectLocalData a shared_ptr is sent on_main.\n// In case this is a problem the ~Gif code should be rewritten.\nDocumentMedia::~DocumentMedia() = default;\n\nnot_null<DocumentData*> DocumentMedia::owner() const {\n\treturn _owner;\n}\n\nvoid DocumentMedia::goodThumbnailWanted() {\n\t_flags |= Flag::GoodThumbnailWanted;\n}\n\nImage *DocumentMedia::goodThumbnail() const {\n\tExpects((_flags & Flag::GoodThumbnailWanted) != 0);\n\n\tif (!_goodThumbnail) {\n\t\tReadOrGenerateThumbnail(_owner);\n\t}\n\treturn _goodThumbnail.get();\n}\n\nvoid DocumentMedia::setGoodThumbnail(QImage thumbnail) {\n\tif (!(_flags & Flag::GoodThumbnailWanted)) {\n\t\treturn;\n\t}\n\t_goodThumbnail = std::make_unique<Image>(std::move(thumbnail));\n\t_owner->session().notifyDownloaderTaskFinished();\n}\n\nImage *DocumentMedia::thumbnailInline() const {\n\tif (!_inlineThumbnail && !_owner->inlineThumbnailIsPath()) {\n\t\tconst auto bytes = _owner->inlineThumbnailBytes();\n\t\tif (!bytes.isEmpty()) {\n\t\t\tauto image = Images::FromInlineBytes(bytes);\n\t\t\tif (image.isNull()) {\n\t\t\t\t_owner->clearInlineThumbnailBytes();\n\t\t\t} else {\n\t\t\t\t_inlineThumbnail = std::make_unique<Image>(std::move(image));\n\t\t\t}\n\t\t}\n\t}\n\treturn _inlineThumbnail.get();\n}\n\nconst QPainterPath &DocumentMedia::thumbnailPath() const {\n\tif (_pathThumbnail.isEmpty() && _owner->inlineThumbnailIsPath()) {\n\t\tconst auto bytes = _owner->inlineThumbnailBytes();\n\t\tif (!bytes.isEmpty()) {\n\t\t\t_pathThumbnail = Images::PathFromInlineBytes(bytes);\n\t\t\tif (_pathThumbnail.isEmpty()) {\n\t\t\t\t_owner->clearInlineThumbnailBytes();\n\t\t\t}\n\t\t}\n\t}\n\treturn _pathThumbnail;\n}\n\nImage *DocumentMedia::thumbnail() const {\n\treturn _thumbnail.get();\n}\n\nvoid DocumentMedia::thumbnailWanted(Data::FileOrigin origin) {\n\tif (!_thumbnail) {\n\t\t_owner->loadThumbnail(origin);\n\t}\n}\n\nQSize DocumentMedia::thumbnailSize() const {\n\tif (const auto image = _thumbnail.get()) {\n\t\treturn image->size();\n\t}\n\tconst auto &location = _owner->thumbnailLocation();\n\treturn { location.width(), location.height() };\n}\n\nvoid DocumentMedia::setThumbnail(QImage thumbnail) {\n\t_thumbnail = std::make_unique<Image>(std::move(thumbnail));\n\t_owner->session().notifyDownloaderTaskFinished();\n}\n\nQByteArray DocumentMedia::videoThumbnailContent() const {\n\treturn _videoThumbnailBytes;\n}\n\nQSize DocumentMedia::videoThumbnailSize() const {\n\tconst auto &location = _owner->videoThumbnailLocation();\n\treturn { location.width(), location.height() };\n}\n\nvoid DocumentMedia::videoThumbnailWanted(Data::FileOrigin origin) {\n\tif (_videoThumbnailBytes.isEmpty()) {\n\t\t_owner->loadVideoThumbnail(origin);\n\t}\n}\n\nvoid DocumentMedia::setVideoThumbnail(QByteArray content) {\n\t_videoThumbnailBytes = std::move(content);\n\t_videoThumbnailBytes.detach();\n}\n\nvoid DocumentMedia::checkStickerLarge() {\n\tif (_sticker) {\n\t\treturn;\n\t}\n\tconst auto data = _owner->sticker();\n\tif (!data) {\n\t\treturn;\n\t}\n\tautomaticLoad(_owner->stickerSetOrigin(), nullptr);\n\tif (data->isAnimated() || !loaded()) {\n\t\treturn;\n\t}\n\tif (_bytes.isEmpty()) {\n\t\tconst auto &loc = _owner->location(true);\n\t\tif (loc.accessEnable()) {\n\t\t\t_sticker = std::make_unique<Image>(loc.name());\n\t\t\tloc.accessDisable();\n\t\t}\n\t} else {\n\t\t_sticker = std::make_unique<Image>(_bytes);\n\t}\n}\n\nvoid DocumentMedia::automaticLoad(\n\t\tData::FileOrigin origin,\n\t\tconst HistoryItem *item) {\n\tif (_owner->status != FileReady\n\t\t|| loaded()\n\t\t|| _owner->uploading()\n\t\t|| _owner->cancelled()) {\n\t\treturn;\n\t} else if (!item && !_owner->sticker() && !_owner->isAnimation()) {\n\t\treturn;\n\t}\n\tconst auto toCache = _owner->saveToCache();\n\tif (!toCache && !Core::App().canSaveFileWithoutAskingForPath()) {\n\t\t// We need a filename, but we're supposed to ask user for it.\n\t\t// No automatic download in this case.\n\t\treturn;\n\t}\n\tconst auto indata = _owner->filename();\n\tconst auto filename = toCache\n\t\t? QString()\n\t\t: DocumentFileNameForSave(_owner);\n\tconst auto shouldLoadFromCloud = (indata.isEmpty()\n\t\t|| Core::DetectNameType(indata) != Core::NameType::Executable)\n\t\t&& (item\n\t\t\t? Data::AutoDownload::Should(\n\t\t\t\t_owner->session().settings().autoDownload(),\n\t\t\t\titem->history()->peer,\n\t\t\t\t_owner)\n\t\t\t: Data::AutoDownload::Should(\n\t\t\t\t_owner->session().settings().autoDownload(),\n\t\t\t\t_owner));\n\tconst auto loadFromCloud = shouldLoadFromCloud\n\t\t? LoadFromCloudOrLocal\n\t\t: LoadFromLocalOnly;\n\t_owner->save(\n\t\torigin,\n\t\tfilename,\n\t\tloadFromCloud,\n\t\ttrue);\n}\n\nvoid DocumentMedia::collectLocalData(not_null<DocumentMedia*> local) {\n\tif (const auto image = local->_goodThumbnail.get()) {\n\t\t_goodThumbnail = std::make_unique<Image>(image->original());\n\t}\n\tif (const auto image = local->_inlineThumbnail.get()) {\n\t\t_inlineThumbnail = std::make_unique<Image>(image->original());\n\t}\n\tif (const auto image = local->_thumbnail.get()) {\n\t\t_thumbnail = std::make_unique<Image>(image->original());\n\t}\n\tif (const auto image = local->_sticker.get()) {\n\t\t_sticker = std::make_unique<Image>(image->original());\n\t}\n\t_bytes = local->_bytes;\n\t_videoThumbnailBytes = local->_videoThumbnailBytes;\n\t_flags = local->_flags;\n}\n\nvoid DocumentMedia::setBytes(const QByteArray &bytes) {\n\tif (!bytes.isEmpty()) {\n\t\t_bytes = bytes;\n\t}\n}\n\nQByteArray DocumentMedia::bytes() const {\n\treturn _bytes;\n}\n\nbool DocumentMedia::loaded(bool check) const {\n\treturn !_bytes.isEmpty() || !_owner->filepath(check).isEmpty();\n}\n\nfloat64 DocumentMedia::progress() const {\n\treturn (_owner->uploading() || _owner->loading())\n\t\t? _owner->progress()\n\t\t: (loaded() ? 1. : 0.);\n}\n\nbool DocumentMedia::canBePlayed() const {\n\treturn !_owner->inappPlaybackFailed()\n\t\t&& _owner->useStreamingLoader()\n\t\t&& (loaded() || _owner->canBeStreamed());\n}\n\nbool DocumentMedia::thumbnailEnoughForSticker() const {\n\tconst auto &location = owner()->thumbnailLocation();\n\tconst auto size = _thumbnail\n\t\t? QSize(_thumbnail->width(), _thumbnail->height())\n\t\t: location.valid()\n\t\t? QSize(location.width(), location.height())\n\t\t: QSize();\n\treturn (size.width() >= 128) || (size.height() >= 128);\n}\n\nvoid DocumentMedia::checkStickerSmall() {\n\tconst auto data = _owner->sticker();\n\tif ((data && data->isAnimated()) || thumbnailEnoughForSticker()) {\n\t\t_owner->loadThumbnail(_owner->stickerSetOrigin());\n\t\tif (data && data->isAnimated()) {\n\t\t\tautomaticLoad(_owner->stickerSetOrigin(), nullptr);\n\t\t}\n\t} else {\n\t\tcheckStickerLarge();\n\t}\n}\n\nImage *DocumentMedia::getStickerLarge() {\n\tcheckStickerLarge();\n\treturn _sticker.get();\n}\n\nImage *DocumentMedia::getStickerSmall() {\n\tconst auto data = _owner->sticker();\n\tif ((data && data->isAnimated()) || thumbnailEnoughForSticker()) {\n\t\treturn thumbnail();\n\t}\n\treturn _sticker.get();\n}\n\nvoid DocumentMedia::checkStickerLarge(not_null<FileLoader*> loader) {\n\tif (_sticker || !_owner->sticker()) {\n\t\treturn;\n\t}\n\tif (auto image = loader->imageData(); !image.isNull()) {\n\t\t_sticker = std::make_unique<Image>(std::move(image));\n\t}\n}\n\nvoid DocumentMedia::GenerateGoodThumbnail(\n\t\tnot_null<DocumentData*> document,\n\t\tQByteArray data) {\n\tconst auto type = document->isPatternWallPaperSVG()\n\t\t? FileType::WallPatternSVG\n\t\t: document->isPatternWallPaperPNG()\n\t\t? FileType::WallPatternPNG\n\t\t: document->isWallPaper()\n\t\t? FileType::WallPaper\n\t\t: document->isTheme()\n\t\t? FileType::Theme\n\t\t: document->isSvgImage()\n\t\t? FileType::SvgImage\n\t\t: !document->sticker()\n\t\t? FileType::Video\n\t\t: document->sticker()->isLottie()\n\t\t? FileType::AnimatedSticker\n\t\t: FileType::VideoSticker;\n\tauto location = document->location().isEmpty()\n\t\t? nullptr\n\t\t: std::make_unique<Core::FileLocation>(document->location());\n\tif (data.isEmpty() && !location) {\n\t\tdocument->setGoodThumbnailChecked(false);\n\t\treturn;\n\t}\n\tconst auto guard = base::make_weak(&document->session());\n\tcrl::async([=, location = std::move(location)] {\n\t\tconst auto filepath = (location && location->accessEnable())\n\t\t\t? location->name()\n\t\t\t: QString();\n\t\tauto result = PrepareGoodThumbnail(filepath, data, type);\n\t\tauto bytes = QByteArray();\n\t\tif (!result.isNull()) {\n\t\t\tauto buffer = QBuffer(&bytes);\n\t\t\tconst auto format = (type == FileType::AnimatedSticker\n\t\t\t\t|| type == FileType::VideoSticker)\n\t\t\t\t? \"WEBP\"\n\t\t\t\t: (type == FileType::WallPatternPNG\n\t\t\t\t\t|| type == FileType::WallPatternSVG\n\t\t\t\t\t|| type == FileType::SvgImage)\n\t\t\t\t? \"PNG\"\n\t\t\t\t: \"JPG\";\n\t\t\tresult.save(&buffer, format, kGoodThumbQuality);\n\t\t}\n\t\tif (!filepath.isEmpty()) {\n\t\t\tlocation->accessDisable();\n\t\t}\n\t\tconst auto cache = bytes.isEmpty() ? QByteArray(\"(failed)\") : bytes;\n\t\tcrl::on_main(guard, [=] {\n\t\t\tdocument->setGoodThumbnailChecked(true);\n\t\t\tif (const auto active = document->activeMediaView()) {\n\t\t\t\tactive->setGoodThumbnail(result);\n\t\t\t}\n\t\t\tdocument->owner().cache().put(\n\t\t\t\tdocument->goodThumbnailCacheKey(),\n\t\t\t\tStorage::Cache::Database::TaggedValue{\n\t\t\t\t\tbase::duplicate(cache),\n\t\t\t\t\tkImageCacheTag });\n\t\t});\n\t});\n}\n\nvoid DocumentMedia::CheckGoodThumbnail(not_null<DocumentData*> document) {\n\tif (!document->goodThumbnailChecked()) {\n\t\tReadOrGenerateThumbnail(document);\n\t}\n}\n\nvoid DocumentMedia::ReadOrGenerateThumbnail(\n\t\tnot_null<DocumentData*> document) {\n\tif (document->goodThumbnailGenerating()\n\t\t|| document->goodThumbnailNoData()\n\t\t|| !MayHaveGoodThumbnail(document)) {\n\t\treturn;\n\t}\n\tdocument->setGoodThumbnailGenerating();\n\n\tconst auto guard = base::make_weak(&document->session());\n\tconst auto active = document->activeMediaView();\n\tconst auto got = [=](QByteArray value) {\n\t\tif (value.isEmpty()) {\n\t\t\tconst auto bytes = active ? active->bytes() : QByteArray();\n\t\t\tcrl::on_main(guard, [=] {\n\t\t\t\tGenerateGoodThumbnail(document, bytes);\n\t\t\t});\n\t\t} else if (active) {\n\t\t\tcrl::async([=] {\n\t\t\t\tauto image = Images::Read({ .content = value }).image;\n\t\t\t\tcrl::on_main(guard, [=, image = std::move(image)]() mutable {\n\t\t\t\t\tdocument->setGoodThumbnailChecked(true);\n\t\t\t\t\tif (const auto active = document->activeMediaView()) {\n\t\t\t\t\t\tactive->setGoodThumbnail(std::move(image));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t});\n\t\t} else {\n\t\t\tcrl::on_main(guard, [=] {\n\t\t\t\tdocument->setGoodThumbnailChecked(true);\n\t\t\t});\n\t\t}\n\t};\n\tdocument->owner().cache().get(document->goodThumbnailCacheKey(), got);\n}\n\nauto DocumentIconFrameGenerator(not_null<DocumentMedia*> media)\n-> FnMut<std::unique_ptr<Ui::FrameGenerator>()> {\n\tif (!media->loaded()) {\n\t\treturn nullptr;\n\t}\n\tusing Type = StickerType;\n\tconst auto document = media->owner();\n\tconst auto content = media->bytes();\n\tconst auto fromFile = content.isEmpty();\n\tconst auto type = document->sticker()\n\t\t? document->sticker()->type\n\t\t: (document->isVideoFile() || document->isAnimation())\n\t\t? Type::Webm\n\t\t: Type::Webp;\n\tconst auto &location = media->owner()->location(true);\n\tif (fromFile && !location.accessEnable()) {\n\t\treturn nullptr;\n\t}\n\treturn [=]() -> std::unique_ptr<Ui::FrameGenerator> {\n\t\tconst auto bytes = Lottie::ReadContent(content, location.name());\n\t\tif (fromFile) {\n\t\t\tlocation.accessDisable();\n\t\t}\n\t\tif (bytes.isEmpty()) {\n\t\t\treturn nullptr;\n\t\t}\n\t\tswitch (type) {\n\t\tcase Type::Tgs:\n\t\t\treturn std::make_unique<Lottie::FrameGenerator>(bytes);\n\t\tcase Type::Webm:\n\t\t\treturn std::make_unique<FFmpeg::FrameGenerator>(bytes);\n\t\tcase Type::Webp:\n\t\t\treturn std::make_unique<Ui::ImageFrameGenerator>(bytes);\n\t\t}\n\t\tUnexpected(\"Document type in DocumentIconFrameGenerator.\");\n\t};\n}\n\nauto DocumentIconFrameGenerator(const std::shared_ptr<DocumentMedia> &media)\n-> FnMut<std::unique_ptr<Ui::FrameGenerator>()> {\n\treturn DocumentIconFrameGenerator(media.get());\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_document_media.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n\nclass Image;\nclass FileLoader;\n\nnamespace Ui {\nclass FrameGenerator;\n} // namespace Ui\n\nnamespace Media {\nnamespace Clip {\nenum class Notification;\nclass ReaderPointer;\n} // namespace Clip\n} // namespace Media\n\nnamespace Data {\n\nclass DocumentMedia;\n\nclass VideoPreviewState final {\npublic:\n\texplicit VideoPreviewState(DocumentMedia *media);\n\n\tvoid automaticLoad(Data::FileOrigin origin) const;\n\t[[nodiscard]] ::Media::Clip::ReaderPointer makeAnimation(\n\t\tFn<void(::Media::Clip::Notification)> callback) const;\n\t[[nodiscard]] bool usingThumbnail() const;\n\t[[nodiscard]] bool loading() const;\n\t[[nodiscard]] bool loaded() const;\n\nprivate:\n\tDocumentMedia *_media = nullptr;\n\tbool _usingThumbnail = false;\n\n};\n\nclass DocumentMedia final {\npublic:\n\texplicit DocumentMedia(not_null<DocumentData*> owner);\n\t~DocumentMedia();\n\n\t[[nodiscard]] not_null<DocumentData*> owner() const;\n\n\tvoid goodThumbnailWanted();\n\t[[nodiscard]] Image *goodThumbnail() const;\n\tvoid setGoodThumbnail(QImage thumbnail);\n\n\t[[nodiscard]] Image *thumbnailInline() const;\n\t[[nodiscard]] const QPainterPath &thumbnailPath() const;\n\n\t[[nodiscard]] Image *thumbnail() const;\n\t[[nodiscard]] QSize thumbnailSize() const;\n\tvoid thumbnailWanted(Data::FileOrigin origin);\n\tvoid setThumbnail(QImage thumbnail);\n\n\t[[nodiscard]] QByteArray videoThumbnailContent() const;\n\t[[nodiscard]] QSize videoThumbnailSize() const;\n\tvoid videoThumbnailWanted(Data::FileOrigin origin);\n\tvoid setVideoThumbnail(QByteArray content);\n\n\tvoid checkStickerLarge();\n\tvoid checkStickerSmall();\n\t[[nodiscard]] Image *getStickerSmall();\n\t[[nodiscard]] Image *getStickerLarge();\n\tvoid checkStickerLarge(not_null<FileLoader*> loader);\n\n\tvoid setBytes(const QByteArray &bytes);\n\t[[nodiscard]] QByteArray bytes() const;\n\t[[nodiscard]] bool loaded(bool check = false) const;\n\t[[nodiscard]] float64 progress() const;\n\t[[nodiscard]] bool canBePlayed() const;\n\n\tvoid automaticLoad(Data::FileOrigin origin, const HistoryItem *item);\n\n\tvoid collectLocalData(not_null<DocumentMedia*> local);\n\n\t// For DocumentData.\n\tstatic void CheckGoodThumbnail(not_null<DocumentData*> document);\n\nprivate:\n\tenum class Flag : uchar {\n\t\tGoodThumbnailWanted = 0x01,\n\t};\n\tinline constexpr bool is_flag_type(Flag) { return true; };\n\tusing Flags = base::flags<Flag>;\n\n\tstatic void ReadOrGenerateThumbnail(not_null<DocumentData*> document);\n\tstatic void GenerateGoodThumbnail(\n\t\tnot_null<DocumentData*> document,\n\t\tQByteArray data);\n\n\t[[nodiscard]] bool thumbnailEnoughForSticker() const;\n\n\t// NB! Right now DocumentMedia can outlive Main::Session!\n\t// In DocumentData::collectLocalData a shared_ptr is sent on_main.\n\t// In case this is a problem the ~Gif code should be rewritten.\n\tconst not_null<DocumentData*> _owner;\n\tstd::unique_ptr<Image> _goodThumbnail;\n\tmutable std::unique_ptr<Image> _inlineThumbnail;\n\tmutable QPainterPath _pathThumbnail;\n\tstd::unique_ptr<Image> _thumbnail;\n\tstd::unique_ptr<Image> _sticker;\n\tQByteArray _bytes;\n\tQByteArray _videoThumbnailBytes;\n\tFlags _flags;\n\n};\n\n[[nodiscard]] auto DocumentIconFrameGenerator(not_null<DocumentMedia*> media)\n-> FnMut<std::unique_ptr<Ui::FrameGenerator>()>;\n\n[[nodiscard]] auto DocumentIconFrameGenerator(\n\tconst std::shared_ptr<DocumentMedia> &media)\n-> FnMut<std::unique_ptr<Ui::FrameGenerator>()>;\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_document_resolver.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_document_resolver.h\"\n\n#include \"base/platform/base_platform_info.h\"\n#include \"boxes/abstract_box.h\" // Ui::show().\n#include \"chat_helpers/ttl_media_layer_widget.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/mime_type.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/media/history_view_gif.h\"\n#include \"lang/lang_keys.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"platform/platform_file_utilities.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n\n#include <QtCore/QBuffer>\n#include <QtCore/QMimeType>\n#include <QtCore/QMimeDatabase>\n\nnamespace Data {\nnamespace {\n\nvoid ConfirmDontWarnBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\trpl::producer<TextWithEntities> &&text,\n\t\trpl::producer<QString> &&check,\n\t\trpl::producer<QString> &&confirm,\n\t\tFn<void(bool)> callback) {\n\tauto checkbox = object_ptr<Ui::Checkbox>(\n\t\tbox.get(),\n\t\tstd::move(check),\n\t\tfalse,\n\t\tst::defaultBoxCheckbox);\n\tconst auto weak = base::make_weak(checkbox.data());\n\tauto confirmed = crl::guard(weak, [=, callback = std::move(callback)] {\n\t\tconst auto checked = weak->checked();\n\t\tbox->closeBox();\n\t\tcallback(checked);\n\t});\n\tUi::ConfirmBox(box, {\n\t\t.text = std::move(text),\n\t\t.confirmed = std::move(confirmed),\n\t\t.confirmText = std::move(confirm),\n\t});\n\tauto padding = st::boxPadding;\n\tpadding.setTop(padding.bottom());\n\tbox->addRow(std::move(checkbox), std::move(padding));\n\tbox->addRow(object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(\n\t\tbox,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_launch_dont_ask_settings(),\n\t\t\tst::boxLabel)\n\t))->toggleOn(weak->checkedValue());\n}\n\nvoid LaunchWithWarning(\n\t\t// not_null<Window::Controller*> controller,\n\t\tconst QString &name,\n\t\tHistoryItem *item) {\n\tconst auto nameType = Core::DetectNameType(name);\n\tconst auto isIpReveal = (nameType != Core::NameType::Executable)\n\t\t&& Core::IsIpRevealingPath(name);\n\tconst auto extension = Core::FileExtension(name).toLower();\n\n\tauto &app = Core::App();\n\tauto &settings = app.settings();\n\tconst auto warn = [&] {\n\t\tif (item && item->history()->peer->isVerified()) {\n\t\t\treturn false;\n\t\t}\n\t\treturn (isIpReveal && settings.ipRevealWarning())\n\t\t\t|| ((nameType == Core::NameType::Executable\n\t\t\t\t|| nameType == Core::NameType::Unknown)\n\t\t\t\t&& !settings.noWarningExtensions().contains(extension));\n\t}();\n\tif (extension.isEmpty()) {\n\t\t// If you launch a file without extension, like \"test\", in case\n\t\t// there is an executable file with the same name in this folder,\n\t\t// like \"test.bat\", the executable file will be launched.\n\t\t//\n\t\t// Now we always force an Open With dialog box for such files.\n\t\t//\n\t\t// Let's force it for all platforms for files without extension.\n\t\tcrl::on_main([=] {\n\t\t\tPlatform::File::UnsafeShowOpenWith(name);\n\t\t});\n\t\treturn;\n\t} else if (!warn) {\n\t\tFile::Launch(name);\n\t\treturn;\n\t}\n\tconst auto callback = [=, &app, &settings](bool checked) {\n\t\tif (checked) {\n\t\t\tif (isIpReveal) {\n\t\t\t\tsettings.setIpRevealWarning(false);\n\t\t\t} else {\n\t\t\t\tauto copy = settings.noWarningExtensions();\n\t\t\t\tcopy.emplace(extension);\n\t\t\t\tsettings.setNoWarningExtensions(std::move(copy));\n\t\t\t}\n\t\t\tapp.saveSettingsDelayed();\n\t\t}\n\t\tFile::Launch(name);\n\t};\n\tauto text = isIpReveal\n\t\t? tr::lng_launch_svg_warning(tr::marked)\n\t\t: ((nameType == Core::NameType::Executable)\n\t\t\t? tr::lng_launch_exe_warning\n\t\t\t: tr::lng_launch_other_warning)(\n\t\t\t\tlt_extension,\n\t\t\t\trpl::single(tr::bold('.' + extension)),\n\t\t\t\ttr::marked);\n\tauto check = (isIpReveal\n\t\t? tr::lng_launch_exe_dont_ask\n\t\t: tr::lng_launch_dont_ask)();\n\tauto confirm = ((nameType == Core::NameType::Executable)\n\t\t? tr::lng_launch_exe_sure\n\t\t: tr::lng_launch_other_sure)();\n\tUi::show(Box(\n\t\tConfirmDontWarnBox,\n\t\tstd::move(text),\n\t\tstd::move(check),\n\t\tstd::move(confirm),\n\t\tcallback));\n}\n\n} // namespace\n\nbase::binary_guard ReadBackgroundImageAsync(\n\t\tnot_null<Data::DocumentMedia*> media,\n\t\tFnMut<QImage(QImage)> postprocess,\n\t\tFnMut<void(QImage&&)> done) {\n\tauto result = base::binary_guard();\n\tconst auto gzipSvg = media->owner()->isPatternWallPaperSVG();\n\tcrl::async([\n\t\tgzipSvg,\n\t\tbytes = media->bytes(),\n\t\tpath = media->owner()->filepath(),\n\t\tpostprocess = std::move(postprocess),\n\t\tguard = result.make_guard(),\n\t\tcallback = std::move(done)\n\t]() mutable {\n\t\tauto image = Ui::ReadBackgroundImage(path, bytes, gzipSvg).image;\n\t\tif (image.isNull()) {\n\t\t\timage = QImage(1, 1, QImage::Format_ARGB32_Premultiplied);\n\t\t\timage.fill(Qt::black);\n\t\t}\n\t\tif (postprocess) {\n\t\t\timage = postprocess(std::move(image));\n\t\t}\n\t\tcrl::on_main(std::move(guard), [\n\t\t\timage = std::move(image),\n\t\t\tcallback = std::move(callback)\n\t\t]() mutable {\n\t\t\tcallback(std::move(image));\n\t\t});\n\t});\n\treturn result;\n}\n\nvoid ResolveDocument(\n\t\tWindow::SessionController *controller,\n\t\tnot_null<DocumentData*> document,\n\t\tHistoryItem *item,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tbool showDrawButton) {\n\tif (document->isNull()) {\n\t\treturn;\n\t}\n\tconst auto msgId = item ? item->fullId() : FullMsgId();\n\n\tconst auto showDocument = [&] {\n\t\tif (controller) {\n\t\t\tcontroller->openDocument(\n\t\t\t\tdocument,\n\t\t\t\ttrue,\n\t\t\t\t{ msgId, topicRootId, monoforumPeerId, showDrawButton });\n\t\t}\n\t};\n\n\tconst auto media = document->createMediaView();\n\tconst auto openImageInApp = [&] {\n\t\tif (document->size >= Images::kReadBytesLimit) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto &location = document->location(true);\n\t\tconst auto mime = u\"image/\"_q;\n\t\tif (!location.isEmpty() && location.accessEnable()) {\n\t\t\tconst auto guard = gsl::finally([&] {\n\t\t\t\tlocation.accessDisable();\n\t\t\t});\n\t\t\tconst auto path = location.name();\n\t\t\tif (Core::MimeTypeForFile(QFileInfo(path)).name().startsWith(mime)\n\t\t\t\t&& QImageReader(path).canRead()) {\n\t\t\t\tshowDocument();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (document->mimeString().startsWith(mime)\n\t\t\t&& !media->bytes().isEmpty()) {\n\t\t\tauto bytes = media->bytes();\n\t\t\tauto buffer = QBuffer(&bytes);\n\t\t\tif (QImageReader(&buffer).canRead()) {\n\t\t\t\tshowDocument();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\tconst auto &location = document->location(true);\n\tif (document->isTheme() && media->loaded(true)) {\n\t\tshowDocument();\n\t\tlocation.accessDisable();\n\t} else if (media->canBePlayed()) {\n\t\tif (document->isAudioFile()\n\t\t\t|| document->isVoiceMessage()\n\t\t\t|| document->isVideoMessage()) {\n\t\t\t::Media::Player::instance()->playPause({ document, msgId });\n\t\t\tif (controller\n\t\t\t\t&& item\n\t\t\t\t&& item->media()\n\t\t\t\t&& item->media()->ttlSeconds()) {\n\t\t\t\tChatHelpers::ShowTTLMediaLayerWidget(controller, item);\n\t\t\t}\n\t\t} else {\n\t\t\tshowDocument();\n\t\t}\n\t} else {\n\t\tdocument->saveFromDataSilent();\n\t\tif (!openImageInApp()) {\n\t\t\tif (!document->filepath(true).isEmpty()) {\n\t\t\t\tLaunchWithWarning(location.name(), item);\n\t\t\t} else if (document->status == FileReady\n\t\t\t\t|| document->status == FileDownloadFailed) {\n\t\t\t\tDocumentSaveClickHandler::Save(\n\t\t\t\t\titem ? item->fullId() : Data::FileOrigin(),\n\t\t\t\t\tdocument);\n\t\t\t}\n\t\t}\n\t}\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_document_resolver.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/binary_guard.h\"\n\nclass DocumentData;\nclass HistoryItem;\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Data {\n\nclass DocumentMedia;\n\nbase::binary_guard ReadBackgroundImageAsync(\n\tnot_null<Data::DocumentMedia*> media,\n\tFnMut<QImage(QImage)> postprocess,\n\tFnMut<void(QImage&&)> done);\n\nvoid ResolveDocument(\n\tWindow::SessionController *controller,\n\tnot_null<DocumentData*> document,\n\tHistoryItem *item,\n\tMsgId topicRootId,\n\tPeerId monoforumPeerId,\n\tbool showDrawButton);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_download_manager.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_download_manager.h\"\n\n#include \"data/data_session.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_web_page.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_user.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_file_origin.h\"\n#include \"base/unixtime.h\"\n#include \"base/random.h\"\n#include \"main/main_session.h\"\n#include \"main/main_account.h\"\n#include \"lang/lang_keys.h\"\n#include \"storage/storage_account.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"core/application.h\"\n#include \"core/mime_type.h\"\n#include \"ui/controls/download_bar.h\"\n#include \"ui/text/format_song_document_name.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/ui_utility.h\"\n#include \"storage/serialize_common.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kClearLoadingTimeout = 5 * crl::time(1000);\nconstexpr auto kMaxFileSize = 4000 * int64(1024 * 1024);\nconstexpr auto kMaxResolvePerAttempt = 100;\n\nconstexpr auto ByItem = [](const auto &entry) {\n\tif constexpr (std::is_same_v<decltype(entry), const DownloadingId&>) {\n\t\treturn entry.object.item;\n\t} else {\n\t\tconst auto resolved = entry.object.get();\n\t\treturn resolved ? resolved->item.get() : nullptr;\n\t}\n};\n\nconstexpr auto ByDocument = [](const auto &entry) {\n\treturn entry.object.document;\n};\n\n[[nodiscard]] uint64 PeerAccessHash(not_null<PeerData*> peer) {\n\tif (const auto user = peer->asUser()) {\n\t\treturn user->accessHash();\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn channel->accessHash();\n\t}\n\treturn 0;\n}\n\n[[nodiscard]] bool ItemContainsMedia(const DownloadObject &object) {\n\tif (const auto photo = object.photo) {\n\t\tif (const auto media = object.item->media()) {\n\t\t\tif (const auto page = media->webpage()) {\n\t\t\t\tif (page->photo == photo) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tfor (const auto &item : page->collage.items) {\n\t\t\t\t\tif (const auto v = std::get_if<PhotoData*>(&item)) {\n\t\t\t\t\t\tif ((*v) == photo) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn (media->photo() == photo);\n\t\t\t}\n\t\t}\n\t} else if (const auto document = object.document) {\n\t\tif (const auto media = object.item->media()) {\n\t\t\tif (const auto page = media->webpage()) {\n\t\t\t\tif (page->document == document) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tfor (const auto &item : page->collage.items) {\n\t\t\t\t\tif (const auto v = std::get_if<DocumentData*>(&item)) {\n\t\t\t\t\t\tif ((*v) == document) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn (media->document() == document);\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nstruct DocumentDescriptor {\n\tuint64 sessionUniqueId = 0;\n\tDocumentId documentId = 0;\n\tFullMsgId itemId;\n};\n\n} // namespace\n\nstruct DownloadManager::DeleteFilesDescriptor {\n\tbase::flat_set<not_null<Main::Session*>> sessions;\n\tbase::flat_map<QString, DocumentDescriptor> files;\n};\n\nDownloadManager::DownloadManager()\n: _clearLoadingTimer([=] { clearLoading(); }) {\n}\n\nDownloadManager::~DownloadManager() = default;\n\nbool DownloadManager::empty() const {\n\tfor (const auto &[session, data] : _sessions) {\n\t\tif (!data.downloading.empty() || !data.downloaded.empty()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid DownloadManager::trackSession(not_null<Main::Session*> session) {\n\tauto &data = _sessions.emplace(session, SessionData()).first->second;\n\tdata.downloaded = deserialize(session);\n\tdata.resolveNeeded = data.downloaded.size();\n\n\tsession->data().documentLoadProgress(\n\t) | rpl::filter([=](not_null<DocumentData*> document) {\n\t\treturn _loadingDocuments.contains(document);\n\t}) | rpl::on_next([=](not_null<DocumentData*> document) {\n\t\tcheck(document);\n\t}, data.lifetime);\n\n\tsession->data().itemLayoutChanged(\n\t) | rpl::filter([=](not_null<const HistoryItem*> item) {\n\t\treturn _loading.contains(item);\n\t}) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\tcheck(item);\n\t}, data.lifetime);\n\n\tsession->data().itemViewRefreshRequest(\n\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\tchanged(item);\n\t}, data.lifetime);\n\n\tsession->changes().messageUpdates(\n\t\tMessageUpdate::Flag::Destroyed\n\t) | rpl::on_next([=](const MessageUpdate &update) {\n\t\tremoved(update.item);\n\t}, data.lifetime);\n\n\tsession->account().sessionChanges(\n\t) | rpl::filter(\n\t\trpl::mappers::_1 != session\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\tuntrack(session);\n\t}, data.lifetime);\n}\n\nvoid DownloadManager::itemVisibilitiesUpdated(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto i = _sessions.find(session);\n\tif (i == end(_sessions)\n\t\t|| i->second.downloading.empty()\n\t\t|| !i->second.downloading.front().hiddenByView) {\n\t\treturn;\n\t}\n\tfor (const auto &id : i->second.downloading) {\n\t\tif (!id.done\n\t\t\t&& !session->data().queryItemVisibility(id.object.item)) {\n\t\t\tfor (auto &id : i->second.downloading) {\n\t\t\t\tid.hiddenByView = false;\n\t\t\t}\n\t\t\t_loadingListChanges.fire({});\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nint64 DownloadManager::computeNextStartDate() {\n\tconst auto now = base::unixtime::now();\n\tif (_lastStartedBase != now) {\n\t\t_lastStartedBase = now;\n\t\t_lastStartedAdded = 0;\n\t} else {\n\t\t++_lastStartedAdded;\n\t}\n\treturn int64(_lastStartedBase) * 1000 + _lastStartedAdded;\n}\n\nvoid DownloadManager::addLoading(DownloadObject object) {\n\tExpects(object.item != nullptr);\n\tExpects(object.document != nullptr);\n\n\tconst auto item = object.item;\n\tauto &data = sessionData(item);\n\n\tconst auto already = ranges::find(data.downloading, item, ByItem);\n\tif (already != end(data.downloading)) {\n\t\tconst auto document = already->object.document;\n\t\tconst auto photo = already->object.photo;\n\t\tif (document == object.document && photo == object.photo) {\n\t\t\tcheck(item);\n\t\t\treturn;\n\t\t}\n\t\tremove(data, already);\n\t}\n\n\tconst auto size = object.document->size;\n\tconst auto path = object.document->loadingFilePath();\n\tif (path.isEmpty()) {\n\t\treturn;\n\t}\n\n\tconst auto shownExists = !data.downloading.empty()\n\t\t&& !data.downloading.front().hiddenByView;\n\tdata.downloading.push_back({\n\t\t.object = object,\n\t\t.started = computeNextStartDate(),\n\t\t.path = path,\n\t\t.total = size,\n\t\t.hiddenByView = (!shownExists\n\t\t\t&& item->history()->owner().queryItemVisibility(item)),\n\t});\n\t_loading.emplace(item);\n\t_loadingDocuments.emplace(object.document);\n\t_loadingProgress = DownloadProgress{\n\t\t.ready = _loadingProgress.current().ready,\n\t\t.total = _loadingProgress.current().total + size,\n\t};\n\t_loadingListChanges.fire({});\n\t_clearLoadingTimer.cancel();\n\n\tcheck(item);\n}\n\nvoid DownloadManager::check(not_null<const HistoryItem*> item) {\n\tauto &data = sessionData(item);\n\tconst auto i = ranges::find(data.downloading, item, ByItem);\n\tAssert(i != end(data.downloading));\n\tcheck(data, i);\n}\n\nvoid DownloadManager::check(not_null<DocumentData*> document) {\n\tauto &data = sessionData(document);\n\tconst auto i = ranges::find(\n\t\tdata.downloading,\n\t\tdocument.get(),\n\t\tByDocument);\n\tAssert(i != end(data.downloading));\n\tcheck(data, i);\n}\n\nvoid DownloadManager::check(\n\t\tSessionData &data,\n\t\tstd::vector<DownloadingId>::iterator i) {\n\tauto &entry = *i;\n\n\tif (!ItemContainsMedia(entry.object)) {\n\t\tcancel(data, i);\n\t\treturn;\n\t}\n\tconst auto document = entry.object.document;\n\n\t// Load with progress only documents for now.\n\tAssert(document != nullptr);\n\n\tconst auto path = document->filepath(true);\n\tif (!path.isEmpty()) {\n\t\tif (_loading.contains(entry.object.item)) {\n\t\t\taddLoaded(entry.object, path, entry.started);\n\t\t}\n\t} else if (!document->loading()) {\n\t\tremove(data, i);\n\t} else {\n\t\tconst auto totalChange = document->size - entry.total;\n\t\tconst auto readyChange = document->loadOffset() - entry.ready;\n\t\tif (!readyChange && !totalChange) {\n\t\t\treturn;\n\t\t}\n\t\tentry.ready += readyChange;\n\t\tentry.total += totalChange;\n\t\t_loadingProgress = DownloadProgress{\n\t\t\t.ready = _loadingProgress.current().ready + readyChange,\n\t\t\t.total = _loadingProgress.current().total + totalChange,\n\t\t};\n\t}\n}\n\nvoid DownloadManager::addLoaded(\n\t\tDownloadObject object,\n\t\tconst QString &path,\n\t\tDownloadDate started) {\n\tExpects(object.item != nullptr);\n\tExpects(object.document || object.photo);\n\n\tconst auto size = QFileInfo(path).size();\n\tif (size <= 0 || size > kMaxFileSize) {\n\t\treturn;\n\t}\n\n\tconst auto item = object.item;\n\tauto &data = sessionData(item);\n\n\tconst auto id = object.document\n\t\t? DownloadId{ object.document->id, DownloadType::Document }\n\t\t: DownloadId{ object.photo->id, DownloadType::Photo };\n\tdata.downloaded.push_back({\n\t\t.download = id,\n\t\t.started = started,\n\t\t.path = path,\n\t\t.size = size,\n\t\t.itemId = item->fullId(),\n\t\t.peerAccessHash = PeerAccessHash(item->history()->peer),\n\t\t.object = std::make_unique<DownloadObject>(object),\n\t});\n\t_loaded.emplace(item);\n\t_loadedAdded.fire(&data.downloaded.back());\n\n\twritePostponed(&item->history()->session());\n\n\tconst auto i = ranges::find(data.downloading, item, ByItem);\n\tif (i != end(data.downloading)) {\n\t\tauto &entry = *i;\n\t\tconst auto document = entry.object.document;\n\t\tif (document) {\n\t\t\t_loadingDocuments.remove(document);\n\t\t}\n\t\tconst auto j = _loading.find(entry.object.item);\n\t\tif (j == end(_loading)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto totalChange = document->size - entry.total;\n\t\tconst auto readyChange = document->size - entry.ready;\n\t\tentry.ready += readyChange;\n\t\tentry.total += totalChange;\n\t\tentry.done = true;\n\t\t_loading.erase(j);\n\t\t_loadingDone.emplace(entry.object.item);\n\t\t_loadingProgress = DownloadProgress{\n\t\t\t.ready = _loadingProgress.current().ready + readyChange,\n\t\t\t.total = _loadingProgress.current().total + totalChange,\n\t\t};\n\t\t_loadingListChanges.fire({});\n\t\tif (_loading.empty()) {\n\t\t\t_clearLoadingTimer.callOnce(kClearLoadingTimeout);\n\t\t}\n\t}\n}\n\nvoid DownloadManager::clearIfFinished() {\n\tif (_clearLoadingTimer.isActive()) {\n\t\t_clearLoadingTimer.cancel();\n\t\tclearLoading();\n\t}\n}\n\nvoid DownloadManager::deleteFiles(const std::vector<GlobalMsgId> &ids) {\n\tauto descriptor = DeleteFilesDescriptor();\n\tfor (const auto &id : ids) {\n\t\tif (const auto item = MessageByGlobalId(id)) {\n\t\t\tconst auto session = &item->history()->session();\n\t\t\tconst auto i = _sessions.find(session);\n\t\t\tif (i == end(_sessions)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tauto &data = i->second;\n\t\t\tconst auto j = ranges::find(\n\t\t\t\tdata.downloading,\n\t\t\t\tnot_null{ item },\n\t\t\t\tByItem);\n\t\t\tif (j != end(data.downloading)) {\n\t\t\t\tcancel(data, j);\n\t\t\t}\n\n\t\t\tconst auto k = ranges::find(data.downloaded, item, ByItem);\n\t\t\tif (k != end(data.downloaded)) {\n\t\t\t\tconst auto document = k->object->document;\n\t\t\t\tdescriptor.files.emplace(k->path, DocumentDescriptor{\n\t\t\t\t\t.sessionUniqueId = id.sessionUniqueId,\n\t\t\t\t\t.documentId = document ? document->id : DocumentId(),\n\t\t\t\t\t.itemId = id.itemId,\n\t\t\t\t});\n\t\t\t\t_loaded.remove(item);\n\t\t\t\t_generated.remove(item);\n\t\t\t\tif (document) {\n\t\t\t\t\t_generatedDocuments.remove(document);\n\t\t\t\t}\n\t\t\t\tdata.downloaded.erase(k);\n\t\t\t\t_loadedRemoved.fire_copy(item);\n\n\t\t\t\tdescriptor.sessions.emplace(session);\n\t\t\t}\n\t\t}\n\t}\n\tfinishFilesDelete(std::move(descriptor));\n}\n\nvoid DownloadManager::deleteAll() {\n\tauto descriptor = DeleteFilesDescriptor();\n\tfor (auto &[session, data] : _sessions) {\n\t\tif (!data.downloaded.empty()) {\n\t\t\tdescriptor.sessions.emplace(session);\n\t\t} else if (data.downloading.empty()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto sessionUniqueId = session->uniqueId();\n\t\twhile (!data.downloading.empty()) {\n\t\t\tcancel(data, data.downloading.end() - 1);\n\t\t}\n\t\tfor (auto &id : base::take(data.downloaded)) {\n\t\t\tconst auto object = id.object.get();\n\t\t\tconst auto document = object ? object->document : nullptr;\n\t\t\tdescriptor.files.emplace(id.path, DocumentDescriptor{\n\t\t\t\t.sessionUniqueId = sessionUniqueId,\n\t\t\t\t.documentId = document ? document->id : DocumentId(),\n\t\t\t\t.itemId = id.itemId,\n\t\t\t});\n\t\t\tif (document) {\n\t\t\t\t_generatedDocuments.remove(document);\n\t\t\t}\n\t\t\tif (const auto item = object ? object->item.get() : nullptr) {\n\t\t\t\t_loaded.remove(item);\n\t\t\t\t_generated.remove(item);\n\t\t\t\t_loadedRemoved.fire_copy(item);\n\t\t\t}\n\t\t}\n\t}\n\tfor (const auto &session : descriptor.sessions) {\n\t\twritePostponed(session);\n\t}\n\tfinishFilesDelete(std::move(descriptor));\n}\n\nvoid DownloadManager::finishFilesDelete(DeleteFilesDescriptor &&descriptor) {\n\tfor (const auto &session : descriptor.sessions) {\n\t\twritePostponed(session);\n\t}\n\tcrl::async([files = std::move(descriptor.files)]{\n\t\tfor (const auto &file : files) {\n\t\t\tQFile(file.first).remove();\n\t\t\tcrl::on_main([descriptor = file.second] {\n\t\t\t\tif (const auto session = SessionByUniqueId(\n\t\t\t\t\t\tdescriptor.sessionUniqueId)) {\n\t\t\t\t\tif (const auto id = descriptor.documentId) {\n\t\t\t\t\t\t[[maybe_unused]] const auto location\n\t\t\t\t\t\t\t= session->data().document(id)->location(true);\n\t\t\t\t\t}\n\t\t\t\t\tconst auto itemId = descriptor.itemId;\n\t\t\t\t\tif (const auto item = session->data().message(itemId)) {\n\t\t\t\t\t\tsession->data().requestItemRepaint(item);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t});\n}\n\nbool DownloadManager::loadedHasNonCloudFile() const {\n\tfor (const auto &[session, data] : _sessions) {\n\t\tfor (const auto &id : data.downloaded) {\n\t\t\tif (const auto object = id.object.get()) {\n\t\t\t\tif (!object->item->isHistoryEntry()) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nauto DownloadManager::loadingList() const\n-> ranges::any_view<const DownloadingId*, ranges::category::input> {\n\treturn ranges::views::all(\n\t\t_sessions\n\t) | ranges::views::transform([=](const auto &pair) {\n\t\treturn ranges::views::all(\n\t\t\tpair.second.downloading\n\t\t) | ranges::views::transform([](const DownloadingId &id) {\n\t\t\treturn &id;\n\t\t});\n\t}) | ranges::views::join;\n}\n\nDownloadProgress DownloadManager::loadingProgress() const {\n\treturn _loadingProgress.current();\n}\n\nrpl::producer<> DownloadManager::loadingListChanges() const {\n\treturn _loadingListChanges.events();\n}\n\nauto DownloadManager::loadingProgressValue() const\n-> rpl::producer<DownloadProgress> {\n\treturn _loadingProgress.value();\n}\n\nbool DownloadManager::loadingInProgress(Main::Session *onlyInSession) const {\n\treturn lookupLoadingItem(onlyInSession) != nullptr;\n}\n\nHistoryItem *DownloadManager::lookupLoadingItem(\n\t\tMain::Session *onlyInSession) const {\n\tconstexpr auto find = [](const SessionData &data) {\n\t\tconstexpr auto proj = &DownloadingId::done;\n\t\tconst auto i = ranges::find(data.downloading, false, proj);\n\t\treturn (i != end(data.downloading)) ? i->object.item.get() : nullptr;\n\t};\n\tif (onlyInSession) {\n\t\tconst auto i = _sessions.find(onlyInSession);\n\t\treturn (i != end(_sessions)) ? find(i->second) : nullptr;\n\t} else {\n\t\tfor (const auto &[session, data] : _sessions) {\n\t\t\tif (const auto result = find(data)) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid DownloadManager::loadingStopWithConfirmation(\n\t\tFn<void()> callback,\n\t\tMain::Session *onlyInSession) {\n\tconst auto item = lookupLoadingItem(onlyInSession);\n\tif (!item) {\n\t\treturn;\n\t}\n\tconst auto window = Core::App().windowFor(\n\t\tnot_null(&item->history()->session().account()));\n\tif (!window) {\n\t\treturn;\n\t}\n\tconst auto weak = base::make_weak(&item->history()->session());\n\tconst auto id = item->fullId();\n\tauto box = Box([=](not_null<Ui::GenericBox*> box) {\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox.get(),\n\t\t\t\ttr::lng_download_sure_stop(),\n\t\t\t\tst::boxLabel),\n\t\t\tst::boxPadding + QMargins(0, 0, 0, st::boxPadding.bottom()));\n\t\tbox->setStyle(st::defaultBox);\n\t\tbox->addButton(tr::lng_selected_upload_stop(), [=] {\n\t\t\tbox->closeBox();\n\n\t\t\tif (!onlyInSession || weak.get()) {\n\t\t\t\tloadingStop(onlyInSession);\n\t\t\t}\n\t\t\tif (callback) {\n\t\t\t\tcallback();\n\t\t\t}\n\t\t}, st::attentionBoxButton);\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t\tbox->addLeftButton(tr::lng_upload_show_file(), [=] {\n\t\t\tbox->closeBox();\n\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tif (const auto item = strong->data().message(id)) {\n\t\t\t\t\tif (const auto window = strong->tryResolveWindow()) {\n\t\t\t\t\t\twindow->showMessage(item);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n\twindow->show(std::move(box));\n\twindow->activate();\n}\n\nvoid DownloadManager::loadingStop(Main::Session *onlyInSession) {\n\tconst auto stopInSession = [&](SessionData &data) {\n\t\twhile (!data.downloading.empty()) {\n\t\t\tcancel(data, data.downloading.end() - 1);\n\t\t}\n\t};\n\tif (onlyInSession) {\n\t\tconst auto i = _sessions.find(onlyInSession);\n\t\tif (i != end(_sessions)) {\n\t\t\tstopInSession(i->second);\n\t\t}\n\t} else {\n\t\tfor (auto &[session, data] : _sessions) {\n\t\t\tstopInSession(data);\n\t\t}\n\t}\n}\n\nvoid DownloadManager::clearLoading() {\n\tExpects(_loading.empty());\n\n\tfor (auto &[session, data] : _sessions) {\n\t\twhile (!data.downloading.empty()) {\n\t\t\tremove(data, data.downloading.end() - 1);\n\t\t}\n\t}\n}\n\nauto DownloadManager::loadedList()\n-> ranges::any_view<const DownloadedId*, ranges::category::input> {\n\tfor (auto &[session, data] : _sessions) {\n\t\tresolve(session, data);\n\t}\n\treturn ranges::views::all(\n\t\t_sessions\n\t) | ranges::views::transform([=](const auto &pair) {\n\t\treturn ranges::views::all(\n\t\t\tpair.second.downloaded\n\t\t) | ranges::views::filter([](const DownloadedId &id) {\n\t\t\treturn (id.object != nullptr);\n\t\t}) | ranges::views::transform([](const DownloadedId &id) {\n\t\t\treturn &id;\n\t\t});\n\t}) | ranges::views::join;\n}\n\nrpl::producer<> DownloadManager::loadedResolveDone() const {\n\tusing namespace rpl::mappers;\n\treturn _loadedResolveDone.value() | rpl::filter(_1) | rpl::to_empty;\n}\n\nvoid DownloadManager::resolve(\n\t\tnot_null<Main::Session*> session,\n\t\tSessionData &data) {\n\tconst auto guard = gsl::finally([&] {\n\t\tcheckFullResolveDone();\n\t});\n\tif (data.resolveSentTotal >= data.resolveNeeded\n\t\t|| data.resolveSentTotal >= kMaxResolvePerAttempt) {\n\t\treturn;\n\t}\n\tstruct Prepared {\n\t\tuint64 peerAccessHash = 0;\n\t\tQVector<MTPInputMessage> ids;\n\t};\n\tauto &owner = session->data();\n\tauto prepared = base::flat_map<PeerId, Prepared>();\n\tauto last = begin(data.downloaded);\n\tauto from = last + (data.resolveNeeded - data.resolveSentTotal);\n\tfor (auto i = from; i != last;) {\n\t\tauto &id = *--i;\n\t\tconst auto msgId = id.itemId.msg;\n\t\tconst auto info = QFileInfo(id.path);\n\t\tif (!info.exists() || info.size() != id.size) {\n\t\t\t// Mark as deleted.\n\t\t\tid.path = QString();\n\t\t} else if (!owner.message(id.itemId) && IsServerMsgId(msgId)) {\n\t\t\tconst auto groupByPeer = peerIsChannel(id.itemId.peer)\n\t\t\t\t? id.itemId.peer\n\t\t\t\t: session->userPeerId();\n\t\t\tauto &perPeer = prepared[groupByPeer];\n\t\t\tif (peerIsChannel(id.itemId.peer) && !perPeer.peerAccessHash) {\n\t\t\t\tperPeer.peerAccessHash = id.peerAccessHash;\n\t\t\t}\n\t\t\tperPeer.ids.push_back(MTP_inputMessageID(MTP_int(msgId.bare)));\n\t\t}\n\t\tif (++data.resolveSentTotal >= kMaxResolvePerAttempt) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tconst auto check = [=] {\n\t\tauto &data = sessionData(session);\n\t\tif (!data.resolveSentRequests) {\n\t\t\tresolveRequestsFinished(session, data);\n\t\t}\n\t};\n\tconst auto requestFinished = [=] {\n\t\t--sessionData(session).resolveSentRequests;\n\t\tcheck();\n\t};\n\tfor (auto &[peer, perPeer] : prepared) {\n\t\tif (const auto channelId = peerToChannel(peer)) {\n\t\t\tsession->api().request(MTPchannels_GetMessages(\n\t\t\t\tMTP_inputChannel(\n\t\t\t\t\tMTP_long(channelId.bare),\n\t\t\t\t\tMTP_long(perPeer.peerAccessHash)),\n\t\t\t\tMTP_vector<MTPInputMessage>(perPeer.ids)\n\t\t\t)).done([=](const MTPmessages_Messages &result) {\n\t\t\t\tsession->data().processExistingMessages(\n\t\t\t\t\tsession->data().channelLoaded(channelId),\n\t\t\t\t\tresult);\n\t\t\t\trequestFinished();\n\t\t\t}).fail(requestFinished).send();\n\t\t} else {\n\t\t\tsession->api().request(MTPmessages_GetMessages(\n\t\t\t\tMTP_vector<MTPInputMessage>(perPeer.ids)\n\t\t\t)).done([=](const MTPmessages_Messages &result) {\n\t\t\t\tsession->data().processExistingMessages(nullptr, result);\n\t\t\t\trequestFinished();\n\t\t\t}).fail(requestFinished).send();\n\t\t}\n\t}\n\tdata.resolveSentRequests += prepared.size();\n\tcheck();\n}\n\nvoid DownloadManager::resolveRequestsFinished(\n\t\tnot_null<Main::Session*> session,\n\t\tSessionData &data) {\n\tauto &owner = session->data();\n\tfor (; data.resolveSentTotal > 0; --data.resolveSentTotal) {\n\t\tconst auto i = begin(data.downloaded) + (--data.resolveNeeded);\n\t\tif (i->path.isEmpty()) {\n\t\t\tdata.downloaded.erase(i);\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto item = owner.message(i->itemId);\n\t\tconst auto media = item ? item->media() : nullptr;\n\t\tconst auto document = media ? media->document() : nullptr;\n\t\tconst auto photo = media ? media->photo() : nullptr;\n\t\tif (i->download.type == DownloadType::Document\n\t\t\t&& (!document || document->id != i->download.objectId)) {\n\t\t\tgenerateEntry(session, *i);\n\t\t} else if (i->download.type == DownloadType::Photo\n\t\t\t&& (!photo || photo->id != i->download.objectId)) {\n\t\t\tgenerateEntry(session, *i);\n\t\t} else {\n\t\t\ti->object = std::make_unique<DownloadObject>(DownloadObject{\n\t\t\t\t.item = item,\n\t\t\t\t.document = document,\n\t\t\t\t.photo = photo,\n\t\t\t});\n\t\t\t_loaded.emplace(item);\n\t\t}\n\t\t_loadedAdded.fire(&*i);\n\t}\n\tcrl::on_main(session, [=] {\n\t\tresolve(session, sessionData(session));\n\t});\n}\n\nvoid DownloadManager::checkFullResolveDone() {\n\tif (_loadedResolveDone.current()) {\n\t\treturn;\n\t}\n\tfor (const auto &[session, data] : _sessions) {\n\t\tif (data.resolveSentTotal < data.resolveNeeded\n\t\t\t|| data.resolveSentRequests > 0) {\n\t\t\treturn;\n\t\t}\n\t}\n\t_loadedResolveDone = true;\n}\n\nvoid DownloadManager::generateEntry(\n\t\tnot_null<Main::Session*> session,\n\t\tDownloadedId &id) {\n\tExpects(!id.object);\n\n\tconst auto info = QFileInfo(id.path);\n\tconst auto document = session->data().document(\n\t\tbase::RandomValue<DocumentId>(),\n\t\t0, // accessHash\n\t\tQByteArray(), // fileReference\n\t\tTimeId(id.started / 1000),\n\t\tQVector<MTPDocumentAttribute>(\n\t\t\t1,\n\t\t\tMTP_documentAttributeFilename(\n\t\t\t\tMTP_string(info.fileName()))),\n\t\tCore::MimeTypeForFile(info).name(),\n\t\tInlineImageLocation(), // inlineThumbnail\n\t\tImageWithLocation(), // thumbnail\n\t\tImageWithLocation(), // videoThumbnail\n\t\tfalse, // isPremiumSticker\n\t\t0, // dc\n\t\tid.size);\n\tdocument->setLocation(Core::FileLocation(info));\n\t_generatedDocuments.emplace(document);\n\n\tid.object = std::make_unique<DownloadObject>(DownloadObject{\n\t\t.item = generateFakeItem(document),\n\t\t.document = document,\n\t});\n\t_loaded.emplace(id.object->item);\n}\n\nauto DownloadManager::loadedAdded() const\n-> rpl::producer<not_null<const DownloadedId*>> {\n\treturn _loadedAdded.events();\n}\n\nauto DownloadManager::loadedRemoved() const\n-> rpl::producer<not_null<const HistoryItem*>> {\n\treturn _loadedRemoved.events();\n}\n\nvoid DownloadManager::remove(\n\t\tSessionData &data,\n\t\tstd::vector<DownloadingId>::iterator i) {\n\tconst auto now = DownloadProgress{\n\t\t.ready = _loadingProgress.current().ready - i->ready,\n\t\t.total = _loadingProgress.current().total - i->total,\n\t};\n\t_loading.remove(i->object.item);\n\t_loadingDone.remove(i->object.item);\n\tif (const auto document = i->object.document) {\n\t\t_loadingDocuments.remove(document);\n\t}\n\tdata.downloading.erase(i);\n\t_loadingListChanges.fire({});\n\t_loadingProgress = now;\n\tif (_loading.empty() && !_loadingDone.empty()) {\n\t\t_clearLoadingTimer.callOnce(kClearLoadingTimeout);\n\t}\n}\n\nvoid DownloadManager::cancel(\n\t\tSessionData &data,\n\t\tstd::vector<DownloadingId>::iterator i) {\n\tconst auto object = i->object;\n\tconst auto item = object.item;\n\tremove(data, i);\n\tif (!item->isAdminLogEntry()) {\n\t\tif (const auto document = object.document) {\n\t\t\tdocument->cancel();\n\t\t} else if (const auto photo = object.photo) {\n\t\t\tphoto->cancel();\n\t\t}\n\t}\n}\n\nvoid DownloadManager::changed(not_null<const HistoryItem*> item) {\n\tif (_loaded.contains(item)) {\n\t\tauto &data = sessionData(item);\n\t\tconst auto i = ranges::find(data.downloaded, item.get(), ByItem);\n\t\tAssert(i != end(data.downloaded));\n\n\t\tconst auto media = item->media();\n\t\tconst auto photo = media ? media->photo() : nullptr;\n\t\tconst auto document = media ? media->document() : nullptr;\n\t\tif (i->object->photo != photo || i->object->document != document) {\n\t\t\tdetach(*i);\n\t\t}\n\t}\n\tif (_loading.contains(item) || _loadingDone.contains(item)) {\n\t\tcheck(item);\n\t}\n}\n\nvoid DownloadManager::removed(not_null<const HistoryItem*> item) {\n\tif (_loaded.contains(item)) {\n\t\tauto &data = sessionData(item);\n\t\tconst auto i = ranges::find(data.downloaded, item.get(), ByItem);\n\t\tAssert(i != end(data.downloaded));\n\t\tdetach(*i);\n\t}\n\tif (_loading.contains(item) || _loadingDone.contains(item)) {\n\t\tauto &data = sessionData(item);\n\t\tconst auto i = ranges::find(data.downloading, item, ByItem);\n\t\tAssert(i != end(data.downloading));\n\n\t\t// We don't want to download files without messages.\n\t\t// For example, there is no way to refresh a file reference for them.\n\t\t//entry.object.item = nullptr;\n\t\tcancel(data, i);\n\t}\n}\n\nnot_null<HistoryItem*> DownloadManager::regenerateItem(\n\t\tconst DownloadObject &previous) {\n\treturn generateItem(previous.item, previous.document, previous.photo);\n}\n\nnot_null<HistoryItem*> DownloadManager::generateFakeItem(\n\t\tnot_null<DocumentData*> document) {\n\treturn generateItem(nullptr, document, nullptr);\n}\n\nnot_null<HistoryItem*> DownloadManager::generateItem(\n\t\tHistoryItem *previousItem,\n\t\tDocumentData *document,\n\t\tPhotoData *photo) {\n\tExpects(document || photo);\n\n\tconst auto session = document\n\t\t? &document->session()\n\t\t: &photo->session();\n\tconst auto history = previousItem\n\t\t? previousItem->history()\n\t\t: session->data().history(session->user());\n\t;\n\tconst auto caption = TextWithEntities();\n\tconst auto make = [&](const auto media) {\n\t\treturn history->makeMessage({\n\t\t\t.id = history->nextNonHistoryEntryId(),\n\t\t\t.flags = MessageFlag::FakeHistoryItem,\n\t\t\t.from = (previousItem\n\t\t\t\t? previousItem->from()->id\n\t\t\t\t: session->userPeerId()),\n\t\t\t.date = base::unixtime::now(),\n\t\t}, media, caption);\n\t};\n\tconst auto result = document ? make(document) : make(photo);\n\t_generated.emplace(result);\n\treturn result;\n}\n\nvoid DownloadManager::detach(DownloadedId &id) {\n\tExpects(id.object != nullptr);\n\tExpects(_loaded.contains(id.object->item));\n\tExpects(!_generated.contains(id.object->item));\n\n\t// Maybe generate new document?\n\tconst auto was = id.object->item;\n\tconst auto now = regenerateItem(*id.object);\n\t_loaded.remove(was);\n\t_loaded.emplace(now);\n\tid.object->item = now;\n\n\t_loadedRemoved.fire_copy(was);\n\t_loadedAdded.fire_copy(&id);\n}\n\nDownloadManager::SessionData &DownloadManager::sessionData(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto i = _sessions.find(session);\n\tAssert(i != end(_sessions));\n\treturn i->second;\n}\n\nconst DownloadManager::SessionData &DownloadManager::sessionData(\n\t\tnot_null<Main::Session*> session) const {\n\tconst auto i = _sessions.find(session);\n\tAssert(i != end(_sessions));\n\treturn i->second;\n}\n\nDownloadManager::SessionData &DownloadManager::sessionData(\n\t\tnot_null<const HistoryItem*> item) {\n\treturn sessionData(&item->history()->session());\n}\n\nDownloadManager::SessionData &DownloadManager::sessionData(\n\t\tnot_null<DocumentData*> document) {\n\treturn sessionData(&document->session());\n}\n\nvoid DownloadManager::writePostponed(not_null<Main::Session*> session) {\n\tsession->account().local().updateDownloads(serializator(session));\n}\n\nFn<std::optional<QByteArray>()> DownloadManager::serializator(\n\t\tnot_null<Main::Session*> session) const {\n\treturn [this, weak = base::make_weak(session)]()\n\t\t-> std::optional<QByteArray> {\n\t\tconst auto strong = weak.get();\n\t\tif (!strong) {\n\t\t\treturn std::nullopt;\n\t\t} else if (!_sessions.contains(strong)) {\n\t\t\treturn QByteArray();\n\t\t}\n\t\tauto result = QByteArray();\n\t\tconst auto &data = sessionData(strong);\n\t\tconst auto count = data.downloaded.size();\n\t\tconst auto constant = sizeof(quint64) // download.objectId\n\t\t\t+ sizeof(qint32) // download.type\n\t\t\t+ sizeof(qint64) // started\n\t\t\t+ sizeof(quint32) // size\n\t\t\t+ sizeof(quint64) // itemId.peer\n\t\t\t+ sizeof(qint64) // itemId.msg\n\t\t\t+ sizeof(quint64); // peerAccessHash\n\t\tauto size = sizeof(qint32) // count\n\t\t\t+ count * constant;\n\t\tfor (const auto &id : data.downloaded) {\n\t\t\tsize += Serialize::stringSize(id.path);\n\t\t}\n\t\tresult.reserve(size);\n\n\t\tauto stream = QDataStream(&result, QIODevice::WriteOnly);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\t\tstream << qint32(count);\n\t\tfor (const auto &id : data.downloaded) {\n\t\t\tstream\n\t\t\t\t<< quint64(id.download.objectId)\n\t\t\t\t<< qint32(id.download.type)\n\t\t\t\t<< qint64(id.started)\n\t\t\t\t// FileSize: Right now any file size fits 32 bit.\n\t\t\t\t<< quint32(id.size)\n\t\t\t\t<< quint64(id.itemId.peer.value)\n\t\t\t\t<< qint64(id.itemId.msg.bare)\n\t\t\t\t<< quint64(id.peerAccessHash)\n\t\t\t\t<< id.path;\n\t\t}\n\t\tstream.device()->close();\n\n\t\treturn result;\n\t};\n}\n\nstd::vector<DownloadedId> DownloadManager::deserialize(\n\t\tnot_null<Main::Session*> session) const {\n\tconst auto serialized = session->account().local().downloadsSerialized();\n\tif (serialized.isEmpty()) {\n\t\treturn {};\n\t}\n\n\tQDataStream stream(serialized);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\n\tauto count = qint32();\n\tstream >> count;\n\tif (stream.status() != QDataStream::Ok || count <= 0 || count > 99'999) {\n\t\treturn {};\n\t}\n\tauto result = std::vector<DownloadedId>();\n\tresult.reserve(count);\n\tfor (auto i = 0; i != count; ++i) {\n\t\tauto downloadObjectId = quint64();\n\t\tauto uncheckedDownloadType = qint32();\n\t\tauto started = qint64();\n\t\t// FileSize: Right now any file size fits 32 bit.\n\t\tauto size = quint32();\n\t\tauto itemIdPeer = quint64();\n\t\tauto itemIdMsg = qint64();\n\t\tauto peerAccessHash = quint64();\n\t\tauto path = QString();\n\t\tstream\n\t\t\t>> downloadObjectId\n\t\t\t>> uncheckedDownloadType\n\t\t\t>> started\n\t\t\t>> size\n\t\t\t>> itemIdPeer\n\t\t\t>> itemIdMsg\n\t\t\t>> peerAccessHash\n\t\t\t>> path;\n\t\tconst auto downloadType = DownloadType(uncheckedDownloadType);\n\t\tif (stream.status() != QDataStream::Ok\n\t\t\t|| path.isEmpty()\n\t\t\t|| size <= 0\n\t\t\t|| size > kMaxFileSize\n\t\t\t|| (downloadType != DownloadType::Document\n\t\t\t\t&& downloadType != DownloadType::Photo)) {\n\t\t\treturn {};\n\t\t}\n\t\tresult.push_back({\n\t\t\t.download = {\n\t\t\t\t.objectId = downloadObjectId,\n\t\t\t\t.type = downloadType,\n\t\t\t},\n\t\t\t.started = started,\n\t\t\t.path = path,\n\t\t\t.size = int64(size),\n\t\t\t.itemId = { PeerId(itemIdPeer), MsgId(itemIdMsg) },\n\t\t\t.peerAccessHash = peerAccessHash,\n\t\t});\n\t}\n\treturn result;\n}\n\nvoid DownloadManager::untrack(not_null<Main::Session*> session) {\n\tconst auto i = _sessions.find(session);\n\tAssert(i != end(_sessions));\n\n\tfor (const auto &entry : i->second.downloaded) {\n\t\tif (const auto resolved = entry.object.get()) {\n\t\t\tconst auto item = resolved->item;\n\t\t\t_loaded.remove(item);\n\t\t\t_generated.remove(item);\n\t\t\tif (const auto document = resolved->document) {\n\t\t\t\t_generatedDocuments.remove(document);\n\t\t\t}\n\t\t}\n\t}\n\twhile (!i->second.downloading.empty()) {\n\t\tremove(i->second, i->second.downloading.end() - 1);\n\t}\n\t_sessions.erase(i);\n}\n\nrpl::producer<Ui::DownloadBarProgress> MakeDownloadBarProgress() {\n\treturn Core::App().downloadManager().loadingProgressValue(\n\t) | rpl::map([=](const DownloadProgress &progress) {\n\t\treturn Ui::DownloadBarProgress{\n\t\t\t.ready = progress.ready,\n\t\t\t.total = progress.total,\n\t\t};\n\t});\n}\n\nrpl::producer<Ui::DownloadBarContent> MakeDownloadBarContent() {\n\treturn [](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tstruct State {\n\t\t\tDocumentData *document = nullptr;\n\t\t\tstd::shared_ptr<Data::DocumentMedia> media;\n\t\t\trpl::lifetime downloadTaskLifetime;\n\t\t\tQImage thumbnail;\n\n\t\t\tbase::has_weak_ptr guard;\n\t\t\tbool scheduled = false;\n\t\t\tFn<void()> push;\n\t\t};\n\n\t\tconst auto state = lifetime.make_state<State>();\n\t\tauto &manager = Core::App().downloadManager();\n\n\t\tconst auto resolveThumbnailRecursive = [=](auto &&self) -> bool {\n\t\t\tif (state->document && !state->document->hasThumbnail()) {\n\t\t\t\tstate->media = nullptr;\n\t\t\t}\n\t\t\tif (!state->media) {\n\t\t\t\tstate->downloadTaskLifetime.destroy();\n\t\t\t\tif (!state->thumbnail.isNull()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tstate->thumbnail = QImage();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (const auto image = state->media->thumbnail()) {\n\t\t\t\tstate->thumbnail = image->original();\n\t\t\t\tstate->downloadTaskLifetime.destroy();\n\t\t\t\tstate->media = nullptr;\n\t\t\t\treturn true;\n\t\t\t} else if (const auto embed = state->media->thumbnailInline()) {\n\t\t\t\tif (!state->thumbnail.isNull()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tstate->thumbnail = Images::Prepare(embed->original(), 0, {\n\t\t\t\t\t.options = Images::Option::Blur,\n\t\t\t\t});\n\t\t\t} else if (!state->downloadTaskLifetime) {\n\t\t\t\tstate->document->session().downloaderTaskFinished(\n\t\t\t\t) | rpl::filter([=] {\n\t\t\t\t\treturn self(self);\n\t\t\t\t}) | rpl::on_next(\n\t\t\t\t\tstate->push,\n\t\t\t\t\tstate->downloadTaskLifetime);\n\t\t\t}\n\t\t\treturn !state->thumbnail.isNull();\n\t\t};\n\t\tconst auto resolveThumbnail = [=] {\n\t\t\treturn resolveThumbnailRecursive(resolveThumbnailRecursive);\n\t\t};\n\n\t\tconst auto notify = [=, &manager] {\n\t\t\tauto content = Ui::DownloadBarContent();\n\t\t\tauto single = (const Data::DownloadObject*) nullptr;\n\t\t\tfor (const auto id : manager.loadingList()) {\n\t\t\t\tif (id->hiddenByView) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (!single) {\n\t\t\t\t\tsingle = &id->object;\n\t\t\t\t}\n\t\t\t\t++content.count;\n\t\t\t\tif (id->done) {\n\t\t\t\t\t++content.done;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (content.count == 1) {\n\t\t\t\tconst auto document = single->document;\n\t\t\t\tconst auto thumbnailed = (single->item\n\t\t\t\t\t&& document->hasThumbnail())\n\t\t\t\t\t? document\n\t\t\t\t\t: nullptr;\n\t\t\t\tif (state->document != thumbnailed) {\n\t\t\t\t\tstate->document = thumbnailed;\n\t\t\t\t\tstate->media = thumbnailed\n\t\t\t\t\t\t? thumbnailed->createMediaView()\n\t\t\t\t\t\t: nullptr;\n\t\t\t\t\tif (const auto raw = state->media.get()) {\n\t\t\t\t\t\traw->thumbnailWanted(single->item->fullId());\n\t\t\t\t\t}\n\t\t\t\t\tstate->thumbnail = QImage();\n\t\t\t\t\tresolveThumbnail();\n\t\t\t\t}\n\t\t\t\tcontent.singleName = Ui::Text::FormatDownloadsName(\n\t\t\t\t\tdocument);\n\t\t\t\tcontent.singleThumbnail = state->thumbnail;\n\t\t\t}\n\t\t\tconsumer.put_next(std::move(content));\n\t\t};\n\t\tstate->push = [=] {\n\t\t\tif (state->scheduled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->scheduled = true;\n\t\t\tUi::PostponeCall(&state->guard, [=] {\n\t\t\t\tstate->scheduled = false;\n\t\t\t\tnotify();\n\t\t\t});\n\t\t};\n\n\t\tmanager.loadingListChanges(\n\t\t) | rpl::filter([=] {\n\t\t\treturn !state->scheduled;\n\t\t}) | rpl::on_next(state->push, lifetime);\n\n\t\tnotify();\n\t\treturn lifetime;\n\t};\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_download_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n\nnamespace Ui {\nstruct DownloadBarProgress;\nstruct DownloadBarContent;\n} // namespace Ui\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\n// Used in serialization!\nenum class DownloadType {\n\tDocument,\n\tPhoto,\n};\n\n// unixtime * 1000.\nusing DownloadDate = int64;\n\n[[nodiscard]] inline TimeId DateFromDownloadDate(DownloadDate date) {\n\treturn date / 1000;\n}\n\nstruct DownloadId {\n\tuint64 objectId = 0;\n\tDownloadType type = DownloadType::Document;\n};\n\nstruct DownloadProgress {\n\tint64 ready = 0;\n\tint64 total = 0;\n};\ninline constexpr bool operator==(\n\t\tconst DownloadProgress &a,\n\t\tconst DownloadProgress &b) {\n\treturn (a.ready == b.ready) && (a.total == b.total);\n}\n\nstruct DownloadObject {\n\tnot_null<HistoryItem*> item;\n\tDocumentData *document = nullptr;\n\tPhotoData *photo = nullptr;\n};\n\nstruct DownloadedId {\n\tDownloadId download;\n\tDownloadDate started = 0;\n\tQString path;\n\tint64 size = 0;\n\tFullMsgId itemId;\n\tuint64 peerAccessHash = 0;\n\n\tstd::unique_ptr<DownloadObject> object;\n};\n\nstruct DownloadingId {\n\tDownloadObject object;\n\tDownloadDate started = 0;\n\tQString path;\n\tint64 ready = 0;\n\tint64 total = 0;\n\tbool hiddenByView = false;\n\tbool done = false;\n};\n\nclass DownloadManager final {\npublic:\n\tDownloadManager();\n\t~DownloadManager();\n\n\t[[nodiscard]] bool empty() const;\n\n\tvoid trackSession(not_null<Main::Session*> session);\n\tvoid itemVisibilitiesUpdated(not_null<Main::Session*> session);\n\n\t[[nodiscard]] DownloadDate computeNextStartDate();\n\n\tvoid addLoading(DownloadObject object);\n\tvoid addLoaded(\n\t\tDownloadObject object,\n\t\tconst QString &path,\n\t\tDownloadDate started);\n\n\tvoid clearIfFinished();\n\tvoid deleteFiles(const std::vector<GlobalMsgId> &ids);\n\tvoid deleteAll();\n\t[[nodiscard]] bool loadedHasNonCloudFile() const;\n\n\t[[nodiscard]] auto loadingList() const\n\t\t-> ranges::any_view<const DownloadingId*, ranges::category::input>;\n\t[[nodiscard]] DownloadProgress loadingProgress() const;\n\t[[nodiscard]] rpl::producer<> loadingListChanges() const;\n\t[[nodiscard]] auto loadingProgressValue() const\n\t\t-> rpl::producer<DownloadProgress>;\n\n\t[[nodiscard]] bool loadingInProgress(\n\t\tMain::Session *onlyInSession = nullptr) const;\n\tvoid loadingStopWithConfirmation(\n\t\tFn<void()> callback,\n\t\tMain::Session *onlyInSession = nullptr);\n\n\t[[nodiscard]] auto loadedList()\n\t\t-> ranges::any_view<const DownloadedId*, ranges::category::input>;\n\t[[nodiscard]] auto loadedAdded() const\n\t\t-> rpl::producer<not_null<const DownloadedId*>>;\n\t[[nodiscard]] auto loadedRemoved() const\n\t\t-> rpl::producer<not_null<const HistoryItem*>>;\n\t[[nodiscard]] rpl::producer<> loadedResolveDone() const;\n\nprivate:\n\tstruct DeleteFilesDescriptor;\n\tstruct SessionData {\n\t\tstd::vector<DownloadedId> downloaded;\n\t\tstd::vector<DownloadingId> downloading;\n\t\tint resolveNeeded = 0;\n\t\tint resolveSentRequests = 0;\n\t\tint resolveSentTotal = 0;\n\t\trpl::lifetime lifetime;\n\t};\n\n\tvoid check(not_null<const HistoryItem*> item);\n\tvoid check(not_null<DocumentData*> document);\n\tvoid check(\n\t\tSessionData &data,\n\t\tstd::vector<DownloadingId>::iterator i);\n\tvoid changed(not_null<const HistoryItem*> item);\n\tvoid removed(not_null<const HistoryItem*> item);\n\tvoid detach(DownloadedId &id);\n\tvoid untrack(not_null<Main::Session*> session);\n\tvoid remove(\n\t\tSessionData &data,\n\t\tstd::vector<DownloadingId>::iterator i);\n\tvoid cancel(\n\t\tSessionData &data,\n\t\tstd::vector<DownloadingId>::iterator i);\n\tvoid clearLoading();\n\n\t[[nodiscard]] SessionData &sessionData(not_null<Main::Session*> session);\n\t[[nodiscard]] const SessionData &sessionData(\n\t\tnot_null<Main::Session*> session) const;\n\t[[nodiscard]] SessionData &sessionData(\n\t\tnot_null<const HistoryItem*> item);\n\t[[nodiscard]] SessionData &sessionData(not_null<DocumentData*> document);\n\n\tvoid resolve(not_null<Main::Session*> session, SessionData &data);\n\tvoid resolveRequestsFinished(\n\t\tnot_null<Main::Session*> session,\n\t\tSessionData &data);\n\tvoid checkFullResolveDone();\n\n\t[[nodiscard]] not_null<HistoryItem*> regenerateItem(\n\t\tconst DownloadObject &previous);\n\t[[nodiscard]] not_null<HistoryItem*> generateFakeItem(\n\t\tnot_null<DocumentData*> document);\n\t[[nodiscard]] not_null<HistoryItem*> generateItem(\n\t\tHistoryItem *previousItem,\n\t\tDocumentData *document,\n\t\tPhotoData *photo);\n\tvoid generateEntry(not_null<Main::Session*> session, DownloadedId &id);\n\n\t[[nodiscard]] HistoryItem *lookupLoadingItem(\n\t\tMain::Session *onlyInSession) const;\n\tvoid loadingStop(Main::Session *onlyInSession);\n\n\tvoid finishFilesDelete(DeleteFilesDescriptor &&descriptor);\n\tvoid writePostponed(not_null<Main::Session*> session);\n\t[[nodiscard]] Fn<std::optional<QByteArray>()> serializator(\n\t\tnot_null<Main::Session*> session) const;\n\t[[nodiscard]] std::vector<DownloadedId> deserialize(\n\t\tnot_null<Main::Session*> session) const;\n\n\tbase::flat_map<not_null<Main::Session*>, SessionData> _sessions;\n\tbase::flat_set<not_null<const HistoryItem*>> _loading;\n\tbase::flat_set<not_null<DocumentData*>> _loadingDocuments;\n\tbase::flat_set<not_null<const HistoryItem*>> _loadingDone;\n\tbase::flat_set<not_null<const HistoryItem*>> _loaded;\n\tbase::flat_set<not_null<HistoryItem*>> _generated;\n\tbase::flat_set<not_null<DocumentData*>> _generatedDocuments;\n\n\tTimeId _lastStartedBase = 0;\n\tint _lastStartedAdded = 0;\n\n\trpl::event_stream<> _loadingListChanges;\n\trpl::variable<DownloadProgress> _loadingProgress;\n\n\trpl::event_stream<not_null<const DownloadedId*>> _loadedAdded;\n\trpl::event_stream<not_null<const HistoryItem*>> _loadedRemoved;\n\trpl::variable<bool> _loadedResolveDone;\n\n\tbase::Timer _clearLoadingTimer;\n\n};\n\n[[nodiscard]] auto MakeDownloadBarProgress()\n-> rpl::producer<Ui::DownloadBarProgress>;\n\n[[nodiscard]] rpl::producer<Ui::DownloadBarContent> MakeDownloadBarContent();\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_drafts.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_drafts.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"history/history.h\"\n#include \"history/history_widget.h\"\n#include \"history/history_item_components.h\"\n#include \"main/main_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_session.h\"\n#include \"data/data_web_page.h\"\n#include \"mainwidget.h\"\n#include \"storage/localstorage.h\"\n\nnamespace Data {\n\nWebPageDraft WebPageDraft::FromItem(not_null<HistoryItem*> item) {\n\tconst auto previewMedia = item->media();\n\tconst auto previewPage = previewMedia\n\t\t? previewMedia->webpage()\n\t\t: nullptr;\n\tusing PageFlag = MediaWebPageFlag;\n\tconst auto previewFlags = previewMedia\n\t\t? previewMedia->webpageFlags()\n\t\t: PageFlag();\n\treturn {\n\t\t.id = previewPage ? previewPage->id : 0,\n\t\t.url = previewPage ? previewPage->url : QString(),\n\t\t.forceLargeMedia = !!(previewFlags & PageFlag::ForceLargeMedia),\n\t\t.forceSmallMedia = !!(previewFlags & PageFlag::ForceSmallMedia),\n\t\t.invert = item->invertMedia(),\n\t\t.manual = !!(previewFlags & PageFlag::Manual),\n\t\t.removed = !previewPage,\n\t};\n}\n\nDraft::Draft(\n\tconst TextWithTags &textWithTags,\n\tFullReplyTo reply,\n\tSuggestOptions suggest,\n\tconst MessageCursor &cursor,\n\tWebPageDraft webpage,\n\tmtpRequestId saveRequestId)\n: textWithTags(textWithTags)\n, reply(std::move(reply))\n, suggest(suggest)\n, cursor(cursor)\n, webpage(webpage)\n, saveRequestId(saveRequestId) {\n}\n\nDraft::Draft(\n\tnot_null<const Ui::InputField*> field,\n\tFullReplyTo reply,\n\tSuggestOptions suggest,\n\tWebPageDraft webpage,\n\tmtpRequestId saveRequestId)\n: textWithTags(field->getTextWithTags())\n, reply(std::move(reply))\n, suggest(suggest)\n, cursor(field)\n, webpage(webpage) {\n}\n\nvoid ApplyPeerCloudDraft(\n\t\tnot_null<Main::Session*> session,\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tconst MTPDdraftMessage &draft) {\n\tconst auto history = session->data().history(peerId);\n\tconst auto date = draft.vdate().v;\n\tif (history->skipCloudDraftUpdate(topicRootId, monoforumPeerId, date)) {\n\t\treturn;\n\t}\n\tconst auto textWithTags = TextWithTags{\n\t\tqs(draft.vmessage()),\n\t\tTextUtilities::ConvertEntitiesToTextTags(\n\t\t\tApi::EntitiesFromMTP(\n\t\t\t\tsession,\n\t\t\t\tdraft.ventities().value_or_empty()))\n\t};\n\tauto replyTo = draft.vreply_to()\n\t\t? ReplyToFromMTP(history, *draft.vreply_to())\n\t\t: FullReplyTo();\n\treplyTo.topicRootId = topicRootId;\n\treplyTo.monoforumPeerId = monoforumPeerId;\n\tauto webpage = WebPageDraft{\n\t\t.invert = draft.is_invert_media(),\n\t\t.removed = draft.is_no_webpage(),\n\t};\n\tif (const auto media = draft.vmedia()) {\n\t\tmedia->match([&](const MTPDmessageMediaWebPage &data) {\n\t\t\tconst auto parsed = session->data().processWebpage(\n\t\t\t\tdata.vwebpage());\n\t\t\tif (!parsed->failed) {\n\t\t\t\twebpage.forceLargeMedia = data.is_force_large_media();\n\t\t\t\twebpage.forceSmallMedia = data.is_force_small_media();\n\t\t\t\twebpage.manual = data.is_manual();\n\t\t\t\twebpage.url = parsed->url;\n\t\t\t\twebpage.id = parsed->id;\n\t\t\t}\n\t\t}, [](const auto &) {});\n\t}\n\tauto suggest = SuggestOptions();\n\tif (!history->suggestDraftAllowed()) {\n\t\t// Don't apply suggest options in unsupported chats.\n\t} else if (const auto suggested = draft.vsuggested_post()) {\n\t\tconst auto &data = suggested->data();\n\t\tsuggest.exists = 1;\n\t\tsuggest.date = data.vschedule_date().value_or_empty();\n\t\tconst auto price = CreditsAmountFromTL(data.vprice());\n\t\tsuggest.priceWhole = price.whole();\n\t\tsuggest.priceNano = price.nano();\n\t\tsuggest.ton = price.ton() ? 1 : 0;\n\t}\n\tauto cloudDraft = std::make_unique<Draft>(\n\t\ttextWithTags,\n\t\treplyTo,\n\t\tsuggest,\n\t\tMessageCursor(Ui::kQFixedMax, Ui::kQFixedMax, Ui::kQFixedMax),\n\t\tstd::move(webpage));\n\tcloudDraft->date = date;\n\n\thistory->setCloudDraft(std::move(cloudDraft));\n\thistory->applyCloudDraft(topicRootId, monoforumPeerId);\n}\n\nvoid ClearPeerCloudDraft(\n\t\tnot_null<Main::Session*> session,\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tTimeId date) {\n\tconst auto history = session->data().history(peerId);\n\tif (history->skipCloudDraftUpdate(topicRootId, monoforumPeerId, date)) {\n\t\treturn;\n\t}\n\n\thistory->clearCloudDraft(topicRootId, monoforumPeerId);\n\thistory->applyCloudDraft(topicRootId, monoforumPeerId);\n}\n\nvoid SetChatLinkDraft(not_null<PeerData*> peer, TextWithEntities draft) {\n\tstatic const auto kInlineStart = QRegularExpression(\"^@[a-zA-Z0-9_]\");\n\tif (kInlineStart.match(draft.text).hasMatch()) {\n\t\tdraft = TextWithEntities().append(' ').append(std::move(draft));\n\t}\n\n\tconst auto textWithTags = TextWithTags{\n\t\tdraft.text,\n\t\tTextUtilities::ConvertEntitiesToTextTags(draft.entities)\n\t};\n\tconst auto cursor = MessageCursor{\n\t\tint(textWithTags.text.size()),\n\t\tint(textWithTags.text.size()),\n\t\tUi::kQFixedMax\n\t};\n\tconst auto history = peer->owner().history(peer->id);\n\tconst auto topicRootId = MsgId();\n\tconst auto monoforumPeerId = PeerId();\n\thistory->setLocalDraft(std::make_unique<Draft>(\n\t\ttextWithTags,\n\t\tFullReplyTo{\n\t\t\t.topicRootId = topicRootId,\n\t\t\t.monoforumPeerId = monoforumPeerId,\n\t\t},\n\t\tSuggestOptions(),\n\t\tcursor,\n\t\tWebPageDraft()));\n\thistory->clearLocalEditDraft(topicRootId, monoforumPeerId);\n\thistory->session().changes().entryUpdated(\n\t\thistory,\n\t\tEntryUpdate::Flag::LocalDraftSet);\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_drafts.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_msg_id.h\"\n\nnamespace Ui {\nclass InputField;\n} // namespace Ui\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nvoid ApplyPeerCloudDraft(\n\tnot_null<Main::Session*> session,\n\tPeerId peerId,\n\tMsgId topicRootId,\n\tPeerId monoforumPeerId,\n\tconst MTPDdraftMessage &draft);\nvoid ClearPeerCloudDraft(\n\tnot_null<Main::Session*> session,\n\tPeerId peerId,\n\tMsgId topicRootId,\n\tPeerId monoforumPeerId,\n\tTimeId date);\n\nstruct WebPageDraft {\n\t[[nodiscard]] static WebPageDraft FromItem(not_null<HistoryItem*> item);\n\n\tWebPageId id = 0;\n\tQString url;\n\tbool forceLargeMedia : 1 = false;\n\tbool forceSmallMedia : 1 = false;\n\tbool invert : 1 = false;\n\tbool manual : 1 = false;\n\tbool removed : 1 = false;\n\n\tfriend inline bool operator==(const WebPageDraft&, const WebPageDraft&)\n\t\t= default;\n};\n\nstruct Draft {\n\tDraft() = default;\n\tDraft(\n\t\tconst TextWithTags &textWithTags,\n\t\tFullReplyTo reply,\n\t\tSuggestOptions suggest,\n\t\tconst MessageCursor &cursor,\n\t\tWebPageDraft webpage,\n\t\tmtpRequestId saveRequestId = 0);\n\tDraft(\n\t\tnot_null<const Ui::InputField*> field,\n\t\tFullReplyTo reply,\n\t\tSuggestOptions suggest,\n\t\tWebPageDraft webpage,\n\t\tmtpRequestId saveRequestId = 0);\n\n\tTimeId date = 0;\n\tTextWithTags textWithTags;\n\tFullReplyTo reply; // reply.messageId.msg is editMsgId for edit draft.\n\tSuggestOptions suggest;\n\tMessageCursor cursor;\n\tWebPageDraft webpage;\n\tmtpRequestId saveRequestId = 0;\n};\n\nclass DraftKey {\npublic:\n\t[[nodiscard]] static constexpr DraftKey None() {\n\t\treturn 0;\n\t}\n\t[[nodiscard]] static constexpr DraftKey Local(\n\t\t\tMsgId topicRootId,\n\t\t\tPeerId monoforumPeerId) {\n\t\treturn Invalid(topicRootId, monoforumPeerId)\n\t\t\t? None()\n\t\t\t: (topicRootId\n\t\t\t\t? topicRootId.bare\n\t\t\t\t: monoforumPeerId\n\t\t\t\t? (monoforumPeerId.value + kMonoforumDraftBit)\n\t\t\t\t: kLocalDraftIndex);\n\t}\n\t[[nodiscard]] static constexpr DraftKey LocalEdit(\n\t\t\tMsgId topicRootId,\n\t\t\tPeerId monoforumPeerId) {\n\t\treturn Invalid(topicRootId, monoforumPeerId)\n\t\t\t? None()\n\t\t\t: (kEditDraftShift\n\t\t\t\t+ (topicRootId\n\t\t\t\t\t? topicRootId.bare\n\t\t\t\t\t: monoforumPeerId\n\t\t\t\t\t? (monoforumPeerId.value + kMonoforumDraftBit)\n\t\t\t\t\t: kLocalDraftIndex));\n\t}\n\t[[nodiscard]] static constexpr DraftKey Cloud(\n\t\t\tMsgId topicRootId,\n\t\t\tPeerId monoforumPeerId) {\n\t\treturn Invalid(topicRootId, monoforumPeerId)\n\t\t\t? None()\n\t\t\t: topicRootId\n\t\t\t? (kCloudDraftShift + topicRootId.bare)\n\t\t\t: monoforumPeerId\n\t\t\t? (kCloudDraftShift + monoforumPeerId.value + kMonoforumDraftBit)\n\t\t\t: kCloudDraftIndex;\n\t}\n\t[[nodiscard]] static constexpr DraftKey Scheduled() {\n\t\treturn kScheduledDraftIndex;\n\t}\n\t[[nodiscard]] static constexpr DraftKey ScheduledEdit() {\n\t\treturn kScheduledDraftIndex + kEditDraftShift;\n\t}\n\t[[nodiscard]] static constexpr DraftKey Shortcut(\n\t\t\tBusinessShortcutId shortcutId) {\n\t\treturn (shortcutId < 0 || shortcutId >= ServerMaxMsgId)\n\t\t\t? None()\n\t\t\t: (kShortcutDraftShift + shortcutId);\n\t}\n\t[[nodiscard]] static constexpr DraftKey ShortcutEdit(\n\t\t\tBusinessShortcutId shortcutId) {\n\t\treturn (shortcutId < 0 || shortcutId >= ServerMaxMsgId)\n\t\t\t? None()\n\t\t\t: (kShortcutDraftShift + kEditDraftShift + shortcutId);\n\t}\n\n\t[[nodiscard]] static constexpr DraftKey FromSerialized(qint64 value) {\n\t\treturn value;\n\t}\n\t[[nodiscard]] constexpr qint64 serialize() const {\n\t\treturn _value;\n\t}\n\n\t[[nodiscard]] static constexpr DraftKey FromSerializedOld(int32 value) {\n\t\treturn !value\n\t\t\t? None()\n\t\t\t: (value == kLocalDraftIndex + kEditDraftShiftOld)\n\t\t\t? LocalEdit(MsgId(), PeerId())\n\t\t\t: (value == kScheduledDraftIndex + kEditDraftShiftOld)\n\t\t\t? ScheduledEdit()\n\t\t\t: (value > 0 && value < 0x4000'0000)\n\t\t\t? Local(MsgId(value), PeerId())\n\t\t\t: (value > kEditDraftShiftOld\n\t\t\t\t&& value < kEditDraftShiftOld + 0x4000'000)\n\t\t\t? LocalEdit(MsgId(int64(value - kEditDraftShiftOld)), PeerId())\n\t\t\t: None();\n\t}\n\t[[nodiscard]] constexpr bool isLocal() const {\n\t\treturn (_value == kLocalDraftIndex)\n\t\t\t|| (_value > 0\n\t\t\t\t&& (_value & kMonoforumDraftMask) < ServerMaxMsgId.bare);\n\t}\n\t[[nodiscard]] constexpr bool isCloud() const {\n\t\treturn (_value == kCloudDraftIndex)\n\t\t\t|| ((_value & kMonoforumDraftMask) > kCloudDraftShift\n\t\t\t\t&& ((_value & kMonoforumDraftMask)\n\t\t\t\t\t< kCloudDraftShift + ServerMaxMsgId.bare));\n\t}\n\n\t[[nodiscard]] constexpr MsgId topicRootId() const {\n\t\tconst auto max = ServerMaxMsgId.bare;\n\t\tif (_value & kMonoforumDraftBit) {\n\t\t\treturn 0;\n\t\t} else if ((_value > kCloudDraftShift)\n\t\t\t&& (_value < kCloudDraftShift + max)) {\n\t\t\treturn (_value - kCloudDraftShift);\n\t\t} else if ((_value > kEditDraftShift)\n\t\t\t&& (_value < kEditDraftShift + max)) {\n\t\t\treturn (_value - kEditDraftShift);\n\t\t} else if (_value > 0 && _value < max) {\n\t\t\treturn _value;\n\t\t}\n\t\treturn 0;\n\t}\n\t[[nodiscard]] constexpr PeerId monoforumPeerId() const {\n\t\tconst auto max = ServerMaxMsgId.bare;\n\t\tconst auto value = _value & kMonoforumDraftMask;\n\t\tif (!(_value & kMonoforumDraftBit)) {\n\t\t\treturn 0;\n\t\t} else if ((value > kCloudDraftShift)\n\t\t\t&& (value < kCloudDraftShift + max)) {\n\t\t\treturn PeerId(UserId(value - kCloudDraftShift));\n\t\t} else if ((value > kEditDraftShift)\n\t\t\t&& (value < kEditDraftShift + max)) {\n\t\t\treturn PeerId(UserId(value - kEditDraftShift));\n\t\t} else if (value > 0 && value < max) {\n\t\t\treturn PeerId(UserId(value));\n\t\t}\n\t\treturn 0;\n\t}\n\n\tfriend inline constexpr auto operator<=>(DraftKey, DraftKey) = default;\n\tfriend inline constexpr bool operator==(DraftKey, DraftKey) = default;\n\n\tinline explicit operator bool() const {\n\t\treturn _value != 0;\n\t}\n\nprivate:\n\tconstexpr DraftKey(int64 value) : _value(value) {\n\t}\n\n\t[[nodiscard]] static constexpr bool Invalid(\n\t\t\tMsgId topicRootId,\n\t\t\tPeerId monoforumPeerId) {\n\t\treturn (topicRootId < 0)\n\t\t\t|| (topicRootId >= ServerMaxMsgId)\n\t\t\t|| !peerIsUser(monoforumPeerId)\n\t\t\t|| (monoforumPeerId.value >= ServerMaxMsgId);\n\t}\n\n\tstatic constexpr auto kLocalDraftIndex = -1;\n\tstatic constexpr auto kCloudDraftIndex = -2;\n\tstatic constexpr auto kScheduledDraftIndex = -3;\n\tstatic constexpr auto kMonoforumDraftBit = (int64(1) << 60);\n\tstatic constexpr auto kMonoforumDraftMask = (kMonoforumDraftBit - 1);\n\tstatic constexpr auto kEditDraftShift = ServerMaxMsgId.bare;\n\tstatic constexpr auto kCloudDraftShift = 2 * ServerMaxMsgId.bare;\n\tstatic constexpr auto kShortcutDraftShift = 3 * ServerMaxMsgId.bare;\n\tstatic constexpr auto kEditDraftShiftOld = 0x3FFF'FFFF;\n\n\tint64 _value = 0;\n\n};\n\nusing HistoryDrafts = base::flat_map<DraftKey, std::unique_ptr<Draft>>;\n\n[[nodiscard]] inline bool DraftStringIsEmpty(const QString &text) {\n\tfor (const auto &ch : text) {\n\t\tif (!ch.isSpace()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n[[nodiscard]] inline bool DraftIsNull(const Draft *draft) {\n\treturn !draft\n\t\t|| (!draft->reply.messageId\n\t\t\t&& !draft->suggest.exists\n\t\t\t&& DraftStringIsEmpty(draft->textWithTags.text));\n}\n\n[[nodiscard]] inline bool DraftsAreEqual(const Draft *a, const Draft *b) {\n\tconst auto aIsNull = DraftIsNull(a);\n\tconst auto bIsNull = DraftIsNull(b);\n\tif (aIsNull) {\n\t\treturn bIsNull;\n\t} else if (bIsNull) {\n\t\treturn false;\n\t}\n\treturn (a->textWithTags == b->textWithTags)\n\t\t&& (a->reply == b->reply)\n\t\t&& (a->suggest == b->suggest)\n\t\t&& (a->webpage == b->webpage);\n}\n\nvoid SetChatLinkDraft(not_null<PeerData*> peer, TextWithEntities draft);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_emoji_statuses.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_emoji_statuses.h\"\n\n#include \"main/main_session.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"data/data_star_gift.h\"\n#include \"data/data_document.h\"\n#include \"data/data_wall_paper.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"base/unixtime.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/call_delayed.h\"\n#include \"apiwrap.h\"\n#include \"ui/controls/tabbed_search.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kRefreshDefaultListEach = 60 * 60 * crl::time(1000);\nconstexpr auto kRecentRequestTimeout = 10 * crl::time(1000);\nconstexpr auto kMaxTimeout = 6 * 60 * 60 * crl::time(1000);\n\n[[nodiscard]] EmojiStatusCollectible ParseEmojiStatusCollectible(\n\t\tconst MTPDemojiStatusCollectible &data) {\n\treturn EmojiStatusCollectible{\n\t\t.id = data.vcollectible_id().v,\n\t\t.documentId = data.vdocument_id().v,\n\t\t.title = qs(data.vtitle()),\n\t\t.slug = qs(data.vslug()),\n\t\t.patternDocumentId = data.vpattern_document_id().v,\n\t\t.centerColor = Ui::ColorFromSerialized(data.vcenter_color()),\n\t\t.edgeColor = Ui::ColorFromSerialized(data.vedge_color()),\n\t\t.patternColor = Ui::ColorFromSerialized(data.vpattern_color()),\n\t\t.textColor = Ui::ColorFromSerialized(data.vtext_color()),\n\t};\n}\n\n} // namespace\n\nEmojiStatuses::EmojiStatuses(not_null<Session*> owner)\n: _owner(owner)\n, _clearingTimer([=] { processClearing(); }) {\n\trefreshDefault();\n\trefreshColored();\n\n\tbase::timer_each(\n\t\tkRefreshDefaultListEach\n\t) | rpl::on_next([=] {\n\t\trefreshDefault();\n\t\trefreshChannelDefault();\n\t}, _lifetime);\n}\n\nEmojiStatuses::~EmojiStatuses() = default;\n\nMain::Session &EmojiStatuses::session() const {\n\treturn _owner->session();\n}\n\nvoid EmojiStatuses::refreshRecent() {\n\trequestRecent();\n}\n\nvoid EmojiStatuses::refreshDefault() {\n\trequestDefault();\n}\n\nvoid EmojiStatuses::refreshColored() {\n\trequestColored();\n}\n\nvoid EmojiStatuses::refreshChannelDefault() {\n\trequestChannelDefault();\n}\n\nvoid EmojiStatuses::refreshChannelColored() {\n\trequestChannelColored();\n}\n\nvoid EmojiStatuses::refreshCollectibles() {\n\trequestCollectibles();\n}\n\nvoid EmojiStatuses::refreshRecentDelayed() {\n\tif (_recentRequestId || _recentRequestScheduled) {\n\t\treturn;\n\t}\n\t_recentRequestScheduled = true;\n\tbase::call_delayed(kRecentRequestTimeout, &_owner->session(), [=] {\n\t\tif (_recentRequestScheduled) {\n\t\t\trequestRecent();\n\t\t}\n\t});\n}\n\nconst std::vector<EmojiStatusId> &EmojiStatuses::list(Type type) const {\n\tswitch (type) {\n\tcase Type::Recent: return _recent;\n\tcase Type::Default: return _default;\n\tcase Type::Colored: return _colored;\n\tcase Type::ChannelDefault: return _channelDefault;\n\tcase Type::ChannelColored: return _channelColored;\n\tcase Type::Collectibles: return _collectibles;\n\t}\n\tUnexpected(\"Type in EmojiStatuses::list.\");\n}\n\nEmojiStatusData EmojiStatuses::parse(const MTPEmojiStatus &status) {\n\treturn status.match([](const MTPDemojiStatus &data) {\n\t\treturn EmojiStatusData{\n\t\t\t.id = { .documentId = data.vdocument_id().v },\n\t\t\t.until = data.vuntil().value_or_empty(),\n\t\t};\n\t}, [&](const MTPDemojiStatusCollectible &data) {\n\t\tconst auto collectibleId = data.vcollectible_id().v;\n\t\tauto &collectible = _collectibleData[collectibleId];\n\t\tif (!collectible) {\n\t\t\tcollectible = std::make_shared<EmojiStatusCollectible>(\n\t\t\t\tParseEmojiStatusCollectible(data));\n\t\t}\n\t\treturn EmojiStatusData{\n\t\t\t.id = { .collectible = collectible },\n\t\t\t.until = data.vuntil().value_or_empty(),\n\t\t};\n\t}, [](const MTPDinputEmojiStatusCollectible &) {\n\t\treturn EmojiStatusData();\n\t}, [](const MTPDemojiStatusEmpty &) {\n\t\treturn EmojiStatusData();\n\t});\n}\n\nrpl::producer<> EmojiStatuses::recentUpdates() const {\n\treturn _recentUpdated.events();\n}\n\nrpl::producer<> EmojiStatuses::defaultUpdates() const {\n\treturn _defaultUpdated.events();\n}\n\nrpl::producer<> EmojiStatuses::channelDefaultUpdates() const {\n\treturn _channelDefaultUpdated.events();\n}\n\nrpl::producer<> EmojiStatuses::collectiblesUpdates() const {\n\treturn _collectiblesUpdated.events();\n}\n\nvoid EmojiStatuses::registerAutomaticClear(\n\t\tnot_null<PeerData*> peer,\n\t\tTimeId until) {\n\tif (!until) {\n\t\t_clearing.remove(peer);\n\t\tif (_clearing.empty()) {\n\t\t\t_clearingTimer.cancel();\n\t\t}\n\t} else if (auto &already = _clearing[peer]; already != until) {\n\t\talready = until;\n\t\tconst auto i = ranges::min_element(_clearing, {}, [](auto &&pair) {\n\t\t\treturn pair.second;\n\t\t});\n\t\tif (i->first == peer) {\n\t\t\tconst auto now = base::unixtime::now();\n\t\t\tif (now < until) {\n\t\t\t\tprocessClearingIn(until - now);\n\t\t\t} else {\n\t\t\t\tprocessClearing();\n\t\t\t}\n\t\t}\n\t}\n}\n\nauto EmojiStatuses::emojiGroupsValue() const -> rpl::producer<Groups> {\n\tconst_cast<EmojiStatuses*>(this)->requestEmojiGroups();\n\treturn _emojiGroups.data.value();\n}\n\nauto EmojiStatuses::statusGroupsValue() const -> rpl::producer<Groups> {\n\tconst_cast<EmojiStatuses*>(this)->requestStatusGroups();\n\treturn _statusGroups.data.value();\n}\n\nauto EmojiStatuses::stickerGroupsValue() const -> rpl::producer<Groups> {\n\tconst_cast<EmojiStatuses*>(this)->requestStickerGroups();\n\treturn _stickerGroups.data.value();\n}\n\nauto EmojiStatuses::profilePhotoGroupsValue() const\n-> rpl::producer<Groups> {\n\tconst_cast<EmojiStatuses*>(this)->requestProfilePhotoGroups();\n\treturn _profilePhotoGroups.data.value();\n}\n\nvoid EmojiStatuses::requestEmojiGroups() {\n\trequestGroups(\n\t\t&_emojiGroups,\n\t\tMTPmessages_GetEmojiGroups(MTP_int(_emojiGroups.hash)));\n\n}\n\nvoid EmojiStatuses::requestStatusGroups() {\n\trequestGroups(\n\t\t&_statusGroups,\n\t\tMTPmessages_GetEmojiStatusGroups(MTP_int(_statusGroups.hash)));\n}\n\nvoid EmojiStatuses::requestStickerGroups() {\n\trequestGroups(\n\t\t&_stickerGroups,\n\t\tMTPmessages_GetEmojiStickerGroups(MTP_int(_stickerGroups.hash)));\n}\n\nvoid EmojiStatuses::requestProfilePhotoGroups() {\n\trequestGroups(\n\t\t&_profilePhotoGroups,\n\t\tMTPmessages_GetEmojiProfilePhotoGroups(\n\t\t\tMTP_int(_profilePhotoGroups.hash)));\n}\n\n[[nodiscard]] std::vector<Ui::EmojiGroup> GroupsFromTL(\n\t\tconst MTPDmessages_emojiGroups &data) {\n\tconst auto &list = data.vgroups().v;\n\tauto result = std::vector<Ui::EmojiGroup>();\n\tresult.reserve(list.size());\n\tfor (const auto &group : list) {\n\t\tgroup.match([&](const MTPDemojiGroupPremium &data) {\n\t\t\tresult.push_back({\n\t\t\t\t.iconId = QString::number(data.vicon_emoji_id().v),\n\t\t\t\t.type = Ui::EmojiGroupType::Premium,\n\t\t\t});\n\t\t}, [&](const auto &data) {\n\t\t\tauto emoticons = ranges::views::all(\n\t\t\t\tdata.vemoticons().v\n\t\t\t) | ranges::views::transform([](const MTPstring &emoticon) {\n\t\t\t\treturn qs(emoticon);\n\t\t\t}) | ranges::to_vector;\n\t\t\tresult.push_back({\n\t\t\t\t.iconId = QString::number(data.vicon_emoji_id().v),\n\t\t\t\t.emoticons = std::move(emoticons),\n\t\t\t\t.type = (MTPDemojiGroupGreeting::Is<decltype(data)>()\n\t\t\t\t\t? Ui::EmojiGroupType::Greeting\n\t\t\t\t\t: Ui::EmojiGroupType::Normal),\n\t\t\t});\n\t\t});\n\t}\n\treturn result;\n}\n\ntemplate <typename Request>\nvoid EmojiStatuses::requestGroups(\n\t\tnot_null<GroupsType*> type,\n\t\tRequest &&request) {\n\tif (type->requestId) {\n\t\treturn;\n\t}\n\ttype->requestId = _owner->session().api().request(\n\t\tstd::forward<Request>(request)\n\t).done([=](const MTPmessages_EmojiGroups &result) {\n\t\ttype->requestId = 0;\n\t\tresult.match([&](const MTPDmessages_emojiGroups &data) {\n\t\t\ttype->hash = data.vhash().v;\n\t\t\ttype->data = GroupsFromTL(data);\n\t\t}, [](const MTPDmessages_emojiGroupsNotModified&) {\n\t\t});\n\t}).fail([=] {\n\t\ttype->requestId = 0;\n\t}).send();\n}\n\nvoid EmojiStatuses::processClearing() {\n\tauto minWait = TimeId(0);\n\tconst auto now = base::unixtime::now();\n\tauto clearing = base::take(_clearing);\n\tfor (auto i = begin(clearing); i != end(clearing);) {\n\t\tconst auto until = i->second;\n\t\tif (now < until) {\n\t\t\tconst auto wait = (until - now);\n\t\t\tif (!minWait || minWait > wait) {\n\t\t\t\tminWait = wait;\n\t\t\t}\n\t\t\t++i;\n\t\t} else {\n\t\t\ti->first->setEmojiStatus(EmojiStatusId());\n\t\t\ti = clearing.erase(i);\n\t\t}\n\t}\n\tif (_clearing.empty()) {\n\t\t_clearing = std::move(clearing);\n\t} else {\n\t\tfor (const auto &[user, until] : clearing) {\n\t\t\t_clearing.emplace(user, until);\n\t\t}\n\t}\n\tif (minWait) {\n\t\tprocessClearingIn(minWait);\n\t} else {\n\t\t_clearingTimer.cancel();\n\t}\n}\n\nstd::vector<EmojiStatusId> EmojiStatuses::parse(\n\t\tconst MTPDaccount_emojiStatuses &data) {\n\tconst auto &list = data.vstatuses().v;\n\tauto result = std::vector<EmojiStatusId>();\n\tresult.reserve(list.size());\n\tfor (const auto &status : list) {\n\t\tconst auto parsed = parse(status);\n\t\tif (!parsed.id) {\n\t\t\tLOG((\"API Error: empty status in account.emojiStatuses.\"));\n\t\t} else {\n\t\t\tresult.push_back(parsed.id);\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid EmojiStatuses::processClearingIn(TimeId wait) {\n\tconst auto waitms = wait * crl::time(1000);\n\t_clearingTimer.callOnce(std::min(waitms, kMaxTimeout));\n}\n\nvoid EmojiStatuses::requestRecent() {\n\tif (_recentRequestId) {\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\t_recentRequestScheduled = false;\n\t_recentRequestId = api.request(MTPaccount_GetRecentEmojiStatuses(\n\t\tMTP_long(_recentHash)\n\t)).done([=](const MTPaccount_EmojiStatuses &result) {\n\t\t_recentRequestId = 0;\n\t\tresult.match([&](const MTPDaccount_emojiStatuses &data) {\n\t\t\tupdateRecent(data);\n\t\t}, [](const MTPDaccount_emojiStatusesNotModified&) {\n\t\t});\n\t}).fail([=] {\n\t\t_recentRequestId = 0;\n\t\t_recentHash = 0;\n\t}).send();\n}\n\nvoid EmojiStatuses::requestDefault() {\n\tif (_defaultRequestId) {\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\t_defaultRequestId = api.request(MTPaccount_GetDefaultEmojiStatuses(\n\t\tMTP_long(_defaultHash)\n\t)).done([=](const MTPaccount_EmojiStatuses &result) {\n\t\t_defaultRequestId = 0;\n\t\tresult.match([&](const MTPDaccount_emojiStatuses &data) {\n\t\t\tupdateDefault(data);\n\t\t}, [&](const MTPDaccount_emojiStatusesNotModified &) {\n\t\t});\n\t}).fail([=] {\n\t\t_defaultRequestId = 0;\n\t\t_defaultHash = 0;\n\t}).send();\n}\n\nvoid EmojiStatuses::requestColored() {\n\tif (_coloredRequestId) {\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\t_coloredRequestId = api.request(MTPmessages_GetStickerSet(\n\t\tMTP_inputStickerSetEmojiDefaultStatuses(),\n\t\tMTP_int(0) // hash\n\t)).done([=](const MTPmessages_StickerSet &result) {\n\t\t_coloredRequestId = 0;\n\t\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\tupdateColored(data);\n\t\t\trefreshCollectibles();\n\t\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t\t});\n\t}).fail([=] {\n\t\t_coloredRequestId = 0;\n\t}).send();\n}\n\nvoid EmojiStatuses::requestChannelDefault() {\n\tif (_channelDefaultRequestId) {\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\t_channelDefaultRequestId = api.request(MTPaccount_GetDefaultEmojiStatuses(\n\t\tMTP_long(_channelDefaultHash)\n\t)).done([=](const MTPaccount_EmojiStatuses &result) {\n\t\t_channelDefaultRequestId = 0;\n\t\tresult.match([&](const MTPDaccount_emojiStatuses &data) {\n\t\t\tupdateChannelDefault(data);\n\t\t}, [&](const MTPDaccount_emojiStatusesNotModified &) {\n\t\t});\n\t}).fail([=] {\n\t\t_channelDefaultRequestId = 0;\n\t\t_channelDefaultHash = 0;\n\t}).send();\n}\n\nvoid EmojiStatuses::requestChannelColored() {\n\tif (_channelColoredRequestId) {\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\t_channelColoredRequestId = api.request(MTPmessages_GetStickerSet(\n\t\tMTP_inputStickerSetEmojiChannelDefaultStatuses(),\n\t\tMTP_int(0) // hash\n\t)).done([=](const MTPmessages_StickerSet &result) {\n\t\t_channelColoredRequestId = 0;\n\t\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\tupdateChannelColored(data);\n\t\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t\t});\n\t}).fail([=] {\n\t\t_channelColoredRequestId = 0;\n\t}).send();\n}\n\nvoid EmojiStatuses::requestCollectibles() {\n\tif (_collectiblesRequestId) {\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\t_collectiblesRequestId = api.request(\n\t\tMTPaccount_GetCollectibleEmojiStatuses(MTP_long(_collectiblesHash))\n\t).done([=](const MTPaccount_EmojiStatuses &result) {\n\t\t_collectiblesRequestId = 0;\n\t\tresult.match([&](const MTPDaccount_emojiStatuses &data) {\n\t\t\tupdateCollectibles(data);\n\t\t}, [&](const MTPDaccount_emojiStatusesNotModified &) {\n\t\t});\n\t}).fail([=] {\n\t\t_collectiblesRequestId = 0;\n\t\t_collectiblesHash = 0;\n\t}).send();\n}\n\nvoid EmojiStatuses::updateRecent(const MTPDaccount_emojiStatuses &data) {\n\t_recentHash = data.vhash().v;\n\t_recent = parse(data);\n\t_recentUpdated.fire({});\n}\n\nvoid EmojiStatuses::updateDefault(const MTPDaccount_emojiStatuses &data) {\n\t_defaultHash = data.vhash().v;\n\t_default = parse(data);\n\t_defaultUpdated.fire({});\n}\n\nvoid EmojiStatuses::updateColored(const MTPDmessages_stickerSet &data) {\n\tconst auto &list = data.vdocuments().v;\n\t_colored.clear();\n\t_colored.reserve(list.size());\n\tfor (const auto &sticker : data.vdocuments().v) {\n\t\t_colored.push_back({\n\t\t\t.documentId = _owner->processDocument(sticker)->id,\n\t\t});\n\t}\n\t_coloredUpdated.fire({});\n}\n\nvoid EmojiStatuses::updateChannelDefault(\n\t\tconst MTPDaccount_emojiStatuses &data) {\n\t_channelDefaultHash = data.vhash().v;\n\t_channelDefault = parse(data);\n\t_channelDefaultUpdated.fire({});\n}\n\nvoid EmojiStatuses::updateChannelColored(\n\t\tconst MTPDmessages_stickerSet &data) {\n\tconst auto &list = data.vdocuments().v;\n\t_channelColored.clear();\n\t_channelColored.reserve(list.size());\n\tfor (const auto &sticker : data.vdocuments().v) {\n\t\t_channelColored.push_back({\n\t\t\t.documentId = _owner->processDocument(sticker)->id,\n\t\t});\n\t}\n\t_channelColoredUpdated.fire({});\n}\n\nvoid EmojiStatuses::updateCollectibles(\n\tconst MTPDaccount_emojiStatuses &data) {\n\t_collectiblesHash = data.vhash().v;\n\t_collectibles = parse(data);\n\t_collectiblesUpdated.fire({});\n}\n\nvoid EmojiStatuses::set(EmojiStatusId id, TimeId until) {\n\tset(_owner->session().user(), id, until);\n}\n\nvoid EmojiStatuses::set(\n\t\tnot_null<PeerData*> peer,\n\t\tEmojiStatusId id,\n\t\tTimeId until) {\n\tauto &api = _owner->session().api();\n\tauto &requestId = _sentRequests[peer];\n\tif (requestId) {\n\t\tapi.request(base::take(requestId)).cancel();\n\t}\n\tpeer->setEmojiStatus(id, until);\n\tconst auto send = [&](auto &&request) {\n\t\trequestId = api.request(\n\t\t\tstd::move(request)\n\t\t).done([=] {\n\t\t\t_sentRequests.remove(peer);\n\t\t}).fail([=] {\n\t\t\t_sentRequests.remove(peer);\n\t\t}).send();\n\t};\n\tusing EFlag = MTPDemojiStatus::Flag;\n\tusing CFlag = MTPDinputEmojiStatusCollectible::Flag;\n\tconst auto status = !id\n\t\t? MTP_emojiStatusEmpty()\n\t\t: id.collectible\n\t\t? MTP_inputEmojiStatusCollectible(\n\t\t\tMTP_flags(until ? CFlag::f_until : CFlag()),\n\t\t\tMTP_long(id.collectible->id),\n\t\t\tMTP_int(until))\n\t\t: MTP_emojiStatus(\n\t\t\tMTP_flags(until ? EFlag::f_until : EFlag()),\n\t\t\tMTP_long(id.documentId),\n\t\t\tMTP_int(until));\n\tif (peer->isSelf()) {\n\t\tsend(MTPaccount_UpdateEmojiStatus(status));\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tsend(MTPchannels_UpdateEmojiStatus(channel->inputChannel(), status));\n\t}\n}\n\nEmojiStatusId EmojiStatuses::fromUniqueGift(\n\t\tconst Data::UniqueGift &gift) {\n\tconst auto collectibleId = gift.id;\n\tauto &collectible = _collectibleData[collectibleId];\n\tif (!collectible) {\n\t\tcollectible = std::make_shared<EmojiStatusCollectible>(\n\t\t\tEmojiStatusCollectible{\n\t\t\t\t.id = gift.id,\n\t\t\t\t.documentId = gift.model.document->id,\n\t\t\t\t.title = Data::UniqueGiftName(gift),\n\t\t\t\t.slug = gift.slug,\n\t\t\t\t.patternDocumentId = gift.pattern.document->id,\n\t\t\t\t.centerColor = gift.backdrop.centerColor,\n\t\t\t\t.edgeColor = gift.backdrop.edgeColor,\n\t\t\t\t.patternColor = gift.backdrop.patternColor,\n\t\t\t\t.textColor = gift.backdrop.textColor,\n\t\t\t});\n\t}\n\treturn { .collectible = collectible };\n}\n\nEmojiStatusCollectible *EmojiStatuses::collectibleInfo(CollectibleId id) {\n\tconst auto i = _collectibleData.find(id);\n\treturn (i != end(_collectibleData)) ? i->second.get() : nullptr;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_emoji_statuses.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nstruct EmojiGroup;\n} // namespace Ui\n\nnamespace Data {\n\nclass DocumentMedia;\nclass Session;\nstruct UniqueGift;\n\nstruct EmojiStatusCollectible {\n\tCollectibleId id = 0;\n\tDocumentId documentId = 0;\n\tQString title;\n\tQString slug;\n\tDocumentId patternDocumentId = 0;\n\tQColor centerColor;\n\tQColor edgeColor;\n\tQColor patternColor;\n\tQColor textColor;\n\n\texplicit operator bool() const {\n\t\treturn id != 0;\n\t}\n};\nstruct EmojiStatusData {\n\tEmojiStatusId id;\n\tTimeId until = 0;\n};\n\nclass EmojiStatuses final {\npublic:\n\texplicit EmojiStatuses(not_null<Session*> owner);\n\t~EmojiStatuses();\n\n\t[[nodiscard]] Session &owner() const {\n\t\treturn *_owner;\n\t}\n\t[[nodiscard]] Main::Session &session() const;\n\n\tvoid refreshRecent();\n\tvoid refreshRecentDelayed();\n\tvoid refreshDefault();\n\tvoid refreshColored();\n\tvoid refreshChannelDefault();\n\tvoid refreshChannelColored();\n\tvoid refreshCollectibles();\n\n\tenum class Type {\n\t\tRecent,\n\t\tDefault,\n\t\tColored,\n\t\tChannelDefault,\n\t\tChannelColored,\n\t\tCollectibles,\n\t};\n\t[[nodiscard]] const std::vector<EmojiStatusId> &list(Type type) const;\n\n\t[[nodiscard]] EmojiStatusData parse(const MTPEmojiStatus &status);\n\n\t[[nodiscard]] rpl::producer<> recentUpdates() const;\n\t[[nodiscard]] rpl::producer<> defaultUpdates() const;\n\t[[nodiscard]] rpl::producer<> channelDefaultUpdates() const;\n\t[[nodiscard]] rpl::producer<> collectiblesUpdates() const;\n\n\tvoid set(EmojiStatusId id, TimeId until = 0);\n\tvoid set(not_null<PeerData*> peer, EmojiStatusId id, TimeId until = 0);\n\t[[nodiscard]] EmojiStatusId fromUniqueGift(const Data::UniqueGift &gift);\n\t[[nodiscard]] EmojiStatusCollectible *collectibleInfo(CollectibleId id);\n\n\tvoid registerAutomaticClear(not_null<PeerData*> peer, TimeId until);\n\n\tusing Groups = std::vector<Ui::EmojiGroup>;\n\t[[nodiscard]] rpl::producer<Groups> emojiGroupsValue() const;\n\t[[nodiscard]] rpl::producer<Groups> statusGroupsValue() const;\n\t[[nodiscard]] rpl::producer<Groups> stickerGroupsValue() const;\n\t[[nodiscard]] rpl::producer<Groups> profilePhotoGroupsValue() const;\n\tvoid requestEmojiGroups();\n\tvoid requestStatusGroups();\n\tvoid requestStickerGroups();\n\tvoid requestProfilePhotoGroups();\n\nprivate:\n\tstruct GroupsType {\n\t\trpl::variable<Groups> data;\n\t\tmtpRequestId requestId = 0;\n\t\tint32 hash = 0;\n\t};\n\n\tvoid requestRecent();\n\tvoid requestDefault();\n\tvoid requestColored();\n\tvoid requestChannelDefault();\n\tvoid requestChannelColored();\n\tvoid requestCollectibles();\n\n\tvoid updateRecent(const MTPDaccount_emojiStatuses &data);\n\tvoid updateDefault(const MTPDaccount_emojiStatuses &data);\n\tvoid updateColored(const MTPDmessages_stickerSet &data);\n\tvoid updateChannelDefault(const MTPDaccount_emojiStatuses &data);\n\tvoid updateChannelColored(const MTPDmessages_stickerSet &data);\n\tvoid updateCollectibles(const MTPDaccount_emojiStatuses &data);\n\n\tvoid processClearingIn(TimeId wait);\n\tvoid processClearing();\n\n\t[[nodiscard]] std::vector<EmojiStatusId> parse(\n\t\tconst MTPDaccount_emojiStatuses &data);\n\n\ttemplate <typename Request>\n\tvoid requestGroups(not_null<GroupsType*> type, Request &&request);\n\n\tconst not_null<Session*> _owner;\n\n\tstd::vector<EmojiStatusId> _recent;\n\tstd::vector<EmojiStatusId> _default;\n\tstd::vector<EmojiStatusId> _colored;\n\tstd::vector<EmojiStatusId> _channelDefault;\n\tstd::vector<EmojiStatusId> _channelColored;\n\tstd::vector<EmojiStatusId> _collectibles;\n\trpl::event_stream<> _recentUpdated;\n\trpl::event_stream<> _defaultUpdated;\n\trpl::event_stream<> _coloredUpdated;\n\trpl::event_stream<> _channelDefaultUpdated;\n\trpl::event_stream<> _channelColoredUpdated;\n\trpl::event_stream<> _collectiblesUpdated;\n\n\tbase::flat_map<\n\t\tCollectibleId,\n\t\tstd::shared_ptr<EmojiStatusCollectible>> _collectibleData;\n\n\tmtpRequestId _recentRequestId = 0;\n\tbool _recentRequestScheduled = false;\n\tuint64 _recentHash = 0;\n\n\tmtpRequestId _defaultRequestId = 0;\n\tuint64 _defaultHash = 0;\n\n\tmtpRequestId _coloredRequestId = 0;\n\n\tmtpRequestId _channelDefaultRequestId = 0;\n\tuint64 _channelDefaultHash = 0;\n\n\tmtpRequestId _channelColoredRequestId = 0;\n\n\tmtpRequestId _collectiblesRequestId = 0;\n\tuint64 _collectiblesHash = 0;\n\n\tbase::flat_map<not_null<PeerData*>, mtpRequestId> _sentRequests;\n\n\tbase::flat_map<not_null<PeerData*>, TimeId> _clearing;\n\tbase::Timer _clearingTimer;\n\n\tGroupsType _emojiGroups;\n\tGroupsType _statusGroups;\n\tGroupsType _stickerGroups;\n\tGroupsType _profilePhotoGroups;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_file_click_handler.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_file_click_handler.h\"\n\n#include \"core/click_handler_types.h\"\n#include \"core/file_utilities.h\"\n#include \"core/application.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_download_manager.h\"\n#include \"data/data_photo.h\"\n#include \"main/main_session.h\"\n\nFileClickHandler::FileClickHandler(FullMsgId context)\n: _context(context) {\n}\n\nvoid FileClickHandler::setMessageId(FullMsgId context) {\n\t_context = context;\n}\n\nFullMsgId FileClickHandler::context() const {\n\treturn _context;\n}\n\nnot_null<DocumentData*> DocumentClickHandler::document() const {\n\treturn _document;\n}\n\nDocumentWrappedClickHandler::DocumentWrappedClickHandler(\n\tClickHandlerPtr wrapped,\n\tnot_null<DocumentData*> document,\n\tFullMsgId context)\n: DocumentClickHandler(document, context)\n, _wrapped(wrapped) {\n}\n\nvoid DocumentWrappedClickHandler::onClickImpl() const {\n\t_wrapped->onClick({ Qt::LeftButton });\n}\n\nDocumentClickHandler::DocumentClickHandler(\n\tnot_null<DocumentData*> document,\n\tFullMsgId context)\n: FileClickHandler(context)\n, _document(document) {\n\tsetProperty(\n\t\tkDocumentLinkMediaProperty,\n\t\treinterpret_cast<qulonglong>(_document.get()));\n}\n\nQString DocumentClickHandler::tooltip() const {\n\treturn property(kDocumentFilenameTooltipProperty).value<QString>();\n}\n\nDocumentOpenClickHandler::DocumentOpenClickHandler(\n\tnot_null<DocumentData*> document,\n\tFn<void(FullMsgId)> &&callback,\n\tFullMsgId context)\n: DocumentClickHandler(document, context)\n, _handler(std::move(callback)) {\n\tExpects(_handler != nullptr);\n}\n\nvoid DocumentOpenClickHandler::onClickImpl() const {\n\t_handler(context());\n}\n\nvoid DocumentSaveClickHandler::Save(\n\t\tData::FileOrigin origin,\n\t\tnot_null<DocumentData*> data,\n\t\tMode mode,\n\t\tFn<void()> started) {\n\tif (data->isNull()) {\n\t\treturn;\n\t}\n\n\tauto savename = QString();\n\tif (mode == Mode::ToCacheOrFile && data->saveToCache()) {\n\t\tdata->save(origin, savename);\n\t\treturn;\n\t}\n\tInvokeQueued(qApp, crl::guard(&data->session(), [=] {\n\t\t// If we call file dialog synchronously, it will stop\n\t\t// background thread timers from working which would\n\t\t// stop audio playback in voice chats / live streams.\n\t\tif (mode != Mode::ToNewFile && data->saveFromData()) {\n\t\t\tif (started) {\n\t\t\t\tstarted();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst auto filepath = data->filepath(true);\n\t\tconst auto fileinfo = QFileInfo(\n\t\t\t);\n\t\tconst auto filedir = filepath.isEmpty()\n\t\t\t? QDir()\n\t\t\t: fileinfo.dir();\n\t\tconst auto filename = filepath.isEmpty()\n\t\t\t? QString()\n\t\t\t: fileinfo.fileName();\n\t\tconst auto savename = DocumentFileNameForSave(\n\t\t\tdata,\n\t\t\t(mode == Mode::ToNewFile),\n\t\t\tfilename,\n\t\t\tfiledir);\n\t\tif (!savename.isEmpty()) {\n\t\t\tdata->save(origin, savename);\n\t\t\tif (started) {\n\t\t\t\tstarted();\n\t\t\t}\n\t\t}\n\t}));\n}\n\nvoid DocumentSaveClickHandler::SaveAndTrack(\n\t\tFullMsgId itemId,\n\t\tnot_null<DocumentData*> document,\n\t\tMode mode,\n\t\tFn<void()> started) {\n\tSave(itemId ? itemId : Data::FileOrigin(), document, mode, [=] {\n\t\tif (document->loading() && !document->loadingFilePath().isEmpty()) {\n\t\t\tif (const auto item = document->owner().message(itemId)) {\n\t\t\t\tCore::App().downloadManager().addLoading({\n\t\t\t\t\t.item = item,\n\t\t\t\t\t.document = document,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tif (started) {\n\t\t\tstarted();\n\t\t}\n\t});\n}\n\nvoid DocumentSaveClickHandler::onClickImpl() const {\n\tSaveAndTrack(context(), document());\n}\n\nDocumentCancelClickHandler::DocumentCancelClickHandler(\n\tnot_null<DocumentData*> document,\n\tFn<void(FullMsgId)> &&callback,\n\tFullMsgId context)\n: DocumentClickHandler(document, context)\n, _handler(std::move(callback)) {\n}\n\nvoid DocumentCancelClickHandler::onClickImpl() const {\n\tconst auto data = document();\n\tif (data->isNull()) {\n\t\treturn;\n\t} else if (data->uploading() && _handler) {\n\t\t_handler(context());\n\t} else {\n\t\tdata->cancel();\n\t}\n}\n\nvoid DocumentOpenWithClickHandler::Open(\n\t\tData::FileOrigin origin,\n\t\tnot_null<DocumentData*> data) {\n\tif (data->isNull()) {\n\t\treturn;\n\t}\n\n\tdata->saveFromDataSilent();\n\tconst auto path = data->filepath(true);\n\tif (!path.isEmpty()) {\n\t\tFile::OpenWith(path);\n\t} else {\n\t\tDocumentSaveClickHandler::Save(\n\t\t\torigin,\n\t\t\tdata,\n\t\t\tDocumentSaveClickHandler::Mode::ToFile);\n\t}\n}\n\nvoid DocumentOpenWithClickHandler::onClickImpl() const {\n\tOpen(context(), document());\n}\n\nPhotoClickHandler::PhotoClickHandler(\n\tnot_null<PhotoData*> photo,\n\tFullMsgId context,\n\tPeerData *peer)\n: FileClickHandler(context)\n, _photo(photo)\n, _peer(peer) {\n\tsetProperty(\n\t\tkPhotoLinkMediaProperty,\n\t\treinterpret_cast<qulonglong>(_photo.get()));\n}\n\nnot_null<PhotoData*> PhotoClickHandler::photo() const {\n\treturn _photo;\n}\n\nPeerData *PhotoClickHandler::peer() const {\n\treturn _peer;\n}\n\nPhotoOpenClickHandler::PhotoOpenClickHandler(\n\tnot_null<PhotoData*> photo,\n\tFn<void(FullMsgId)> &&callback,\n\tFullMsgId context)\n: PhotoClickHandler(photo, context)\n, _handler(std::move(callback)) {\n\tExpects(_handler != nullptr);\n}\n\nvoid PhotoOpenClickHandler::onClickImpl() const {\n\t_handler(context());\n}\n\nvoid PhotoSaveClickHandler::onClickImpl() const {\n\tconst auto data = photo();\n\tif (data->isNull()) {\n\t\treturn;\n\t} else {\n\t\tdata->clearFailed(Data::PhotoSize::Large);\n\t\tdata->load(context());\n\t}\n}\n\nPhotoCancelClickHandler::PhotoCancelClickHandler(\n\tnot_null<PhotoData*> photo,\n\tFn<void(FullMsgId)> &&callback,\n\tFullMsgId context)\n: PhotoClickHandler(photo, context)\n, _handler(std::move(callback)) {\n}\n\nvoid PhotoCancelClickHandler::onClickImpl() const {\n\tconst auto data = photo();\n\tif (data->isNull()) {\n\t\treturn;\n\t} else if (data->uploading() && _handler) {\n\t\t_handler(context());\n\t} else {\n\t\tdata->cancel();\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_file_click_handler.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_file_origin.h\"\n#include \"ui/basic_click_handlers.h\"\n\nclass DocumentData;\nclass HistoryItem;\nclass PhotoData;\n\nclass FileClickHandler : public LeftButtonClickHandler {\npublic:\n\tFileClickHandler(FullMsgId context);\n\n\tvoid setMessageId(FullMsgId context);\n\n\t[[nodiscard]] FullMsgId context() const;\n\nprivate:\n\tFullMsgId _context;\n\n};\n\nclass DocumentClickHandler : public FileClickHandler {\npublic:\n\tDocumentClickHandler(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context = FullMsgId());\n\n\tQString tooltip() const override;\n\n\t[[nodiscard]] not_null<DocumentData*> document() const;\n\nprivate:\n\tconst not_null<DocumentData*> _document;\n\n};\n\nclass DocumentSaveClickHandler : public DocumentClickHandler {\npublic:\n\tenum class Mode {\n\t\tToCacheOrFile,\n\t\tToFile,\n\t\tToNewFile,\n\t};\n\tusing DocumentClickHandler::DocumentClickHandler;\n\tstatic void Save(\n\t\tData::FileOrigin origin,\n\t\tnot_null<DocumentData*> document,\n\t\tMode mode = Mode::ToCacheOrFile,\n\t\tFn<void()> started = nullptr);\n\tstatic void SaveAndTrack(\n\t\tFullMsgId itemId,\n\t\tnot_null<DocumentData*> document,\n\t\tMode mode = Mode::ToCacheOrFile,\n\t\tFn<void()> started = nullptr);\n\nprotected:\n\tvoid onClickImpl() const override;\n\n};\n\nclass DocumentOpenClickHandler : public DocumentClickHandler {\npublic:\n\tDocumentOpenClickHandler(\n\t\tnot_null<DocumentData*> document,\n\t\tFn<void(FullMsgId)> &&callback,\n\t\tFullMsgId context = FullMsgId());\n\nprotected:\n\tvoid onClickImpl() const override;\n\nprivate:\n\tconst Fn<void(FullMsgId)> _handler;\n\n};\n\nclass DocumentCancelClickHandler : public DocumentClickHandler {\npublic:\n\tDocumentCancelClickHandler(\n\t\tnot_null<DocumentData*> document,\n\t\tFn<void(FullMsgId)> &&callback,\n\t\tFullMsgId context = FullMsgId());\n\nprotected:\n\tvoid onClickImpl() const override;\n\nprivate:\n\tconst Fn<void(FullMsgId)> _handler;\n\n};\n\nclass DocumentOpenWithClickHandler : public DocumentClickHandler {\npublic:\n\tusing DocumentClickHandler::DocumentClickHandler;\n\tstatic void Open(\n\t\tData::FileOrigin origin,\n\t\tnot_null<DocumentData*> document);\n\nprotected:\n\tvoid onClickImpl() const override;\n\n};\n\nclass VoiceSeekClickHandler : public DocumentOpenClickHandler {\npublic:\n\tusing DocumentOpenClickHandler::DocumentOpenClickHandler;\n\nprotected:\n\tvoid onClickImpl() const override {\n\t}\n\n};\n\nclass DocumentWrappedClickHandler : public DocumentClickHandler {\npublic:\n\tDocumentWrappedClickHandler(\n\t\tClickHandlerPtr wrapped,\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context = FullMsgId());\n\nprotected:\n\tvoid onClickImpl() const override;\n\nprivate:\n\tClickHandlerPtr _wrapped;\n\n};\n\nclass PhotoClickHandler : public FileClickHandler {\npublic:\n\tPhotoClickHandler(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context = FullMsgId(),\n\t\tPeerData *peer = nullptr);\n\n\t[[nodiscard]] not_null<PhotoData*> photo() const;\n\t[[nodiscard]] PeerData *peer() const;\n\nprivate:\n\tconst not_null<PhotoData*> _photo;\n\tPeerData * const _peer = nullptr;\n\n};\n\nclass PhotoOpenClickHandler : public PhotoClickHandler {\npublic:\n\tPhotoOpenClickHandler(\n\t\tnot_null<PhotoData*> photo,\n\t\tFn<void(FullMsgId)> &&callback,\n\t\tFullMsgId context = FullMsgId());\n\nprotected:\n\tvoid onClickImpl() const override;\n\nprivate:\n\tconst Fn<void(FullMsgId)> _handler;\n\n};\n\nclass PhotoSaveClickHandler : public PhotoClickHandler {\npublic:\n\tusing PhotoClickHandler::PhotoClickHandler;\n\nprotected:\n\tvoid onClickImpl() const override;\n\n};\n\nclass PhotoCancelClickHandler : public PhotoClickHandler {\npublic:\n\tPhotoCancelClickHandler(\n\t\tnot_null<PhotoData*> photo,\n\t\tFn<void(FullMsgId)> &&callback,\n\t\tFullMsgId context = FullMsgId());\n\nprotected:\n\tvoid onClickImpl() const override;\n\nprivate:\n\tconst Fn<void(FullMsgId)> _handler;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_file_origin.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_file_origin.h\"\n\nnamespace Data {\nnamespace {\n\nstruct FileReferenceAccumulator {\n\ttemplate <typename Type>\n\tvoid push(const MTPVector<Type> &data) {\n\t\tfor (const auto &item : data.v) {\n\t\t\tpush(item);\n\t\t}\n\t}\n\ttemplate <typename Type>\n\tvoid push(const tl::conditional<Type> &data) {\n\t\tif (data) {\n\t\t\tpush(*data);\n\t\t}\n\t}\n\tvoid push(const MTPPhoto &data) {\n\t\tdata.match([&](const MTPDphoto &data) {\n\t\t\tresult.data.emplace(\n\t\t\t\tPhotoFileLocationId{ data.vid().v },\n\t\t\t\tdata.vfile_reference().v);\n\t\t}, [](const MTPDphotoEmpty &data) {\n\t\t});\n\t}\n\tvoid push(const MTPDocument &data) {\n\t\tdata.match([&](const MTPDdocument &data) {\n\t\t\tresult.data.emplace(\n\t\t\t\tDocumentFileLocationId{ data.vid().v },\n\t\t\t\tdata.vfile_reference().v);\n\t\t}, [](const MTPDdocumentEmpty &data) {\n\t\t});\n\t}\n\tvoid push(const MTPPage &data) {\n\t\tpush(data.data().vphotos());\n\t\tpush(data.data().vdocuments());\n\t}\n\tvoid push(const MTPWallPaper &data) {\n\t\tdata.match([&](const MTPDwallPaper &data) {\n\t\t\tpush(data.vdocument());\n\t\t}, [&](const MTPDwallPaperNoFile &data) {\n\t\t});\n\t}\n\tvoid push(const MTPTheme &data) {\n\t\tpush(data.data().vdocument());\n\t}\n\tvoid push(const MTPWebPageAttribute &data) {\n\t\tdata.match([&](const MTPDwebPageAttributeStory &data) {\n\t\t\tpush(data.vstory());\n\t\t}, [&](const MTPDwebPageAttributeTheme &data) {\n\t\t\tpush(data.vdocuments());\n\t\t}, [&](const MTPDwebPageAttributeStickerSet &data) {\n\t\t\tpush(data.vstickers());\n\t\t}, [&](const MTPDwebPageAttributeUniqueStarGift &data) {\n\t\t\tpush(data.vgift());\n\t\t}, [&](const MTPDwebPageAttributeStarGiftCollection &data) {\n\t\t\tpush(data.vicons());\n\t\t}, [&](const MTPDwebPageAttributeStarGiftAuction &data) {\n\t\t\tpush(data.vgift());\n\t\t});\n\t}\n\tvoid push(const MTPStarGift &data) {\n\t\tdata.match([&](const MTPDstarGift &data) {\n\t\t\tpush(data.vsticker());\n\t\t}, [&](const MTPDstarGiftUnique &data) {\n\t\t\tpush(data.vattributes());\n\t\t});\n\t}\n\tvoid push(const MTPStarGiftAttribute &data) {\n\t\tdata.match([&](const MTPDstarGiftAttributeModel &data) {\n\t\t\tpush(data.vdocument());\n\t\t}, [&](const MTPDstarGiftAttributePattern &data) {\n\t\t\tpush(data.vdocument());\n\t\t}, [&](const MTPDstarGiftAttributeBackdrop &data) {\n\t\t}, [&](const MTPDstarGiftAttributeOriginalDetails &data) {\n\t\t});\n\t}\n\tvoid push(const MTPWebPage &data) {\n\t\tdata.match([&](const MTPDwebPage &data) {\n\t\t\tpush(data.vdocument());\n\t\t\tpush(data.vattributes());\n\t\t\tpush(data.vphoto());\n\t\t\tpush(data.vcached_page());\n\t\t}, [](const auto &data) {\n\t\t});\n\t}\n\tvoid push(const MTPGame &data) {\n\t\tdata.match([&](const MTPDgame &data) {\n\t\t\tpush(data.vdocument());\n\t\t}, [](const auto &data) {\n\t\t});\n\t}\n\tvoid push(const MTPMessageExtendedMedia &data) {\n\t\tdata.match([&](const MTPDmessageExtendedMediaPreview &data) {\n\t\t}, [&](const MTPDmessageExtendedMedia &data) {\n\t\t\tpush(data.vmedia());\n\t\t});\n\t}\n\tvoid push(const MTPMessageMedia &data) {\n\t\tdata.match([&](const MTPDmessageMediaPhoto &data) {\n\t\t\tpush(data.vphoto());\n\t\t}, [&](const MTPDmessageMediaDocument &data) {\n\t\t\tpush(data.vdocument());\n\t\t\tpush(data.vvideo_cover());\n\t\t\tpush(data.valt_documents());\n\t\t}, [&](const MTPDmessageMediaWebPage &data) {\n\t\t\tpush(data.vwebpage());\n\t\t}, [&](const MTPDmessageMediaGame &data) {\n\t\t\tpush(data.vgame());\n\t\t}, [&](const MTPDmessageMediaInvoice &data) {\n\t\t\tpush(data.vextended_media());\n\t\t}, [&](const MTPDmessageMediaPaidMedia &data) {\n\t\t\tpush(data.vextended_media());\n\t\t}, [&](const MTPDmessageMediaPoll &data) {\n\t\t\tpush(data.vattached_media());\n\t\t\tfor (const auto &answer : data.vpoll().data().vanswers().v) {\n\t\t\t\tanswer.match([&](const MTPDpollAnswer &a) {\n\t\t\t\t\tpush(a.vmedia());\n\t\t\t\t}, [](const auto &) {\n\t\t\t\t});\n\t\t\t}\n\t\t\tpush(data.vresults().data().vsolution_media());\n\t\t}, [](const auto &data) {\n\t\t});\n\t}\n\tvoid push(const MTPMessageReplyHeader &data) {\n\t\tdata.match([&](const MTPDmessageReplyHeader &data) {\n\t\t\tpush(data.vreply_media());\n\t\t}, [](const MTPDmessageReplyStoryHeader &data) {\n\t\t});\n\t}\n\tvoid push(const MTPMessage &data) {\n\t\tdata.match([&](const MTPDmessage &data) {\n\t\t\tpush(data.vmedia());\n\t\t\tpush(data.vreply_to());\n\t\t}, [&](const MTPDmessageService &data) {\n\t\t\tdata.vaction().match(\n\t\t\t[&](const MTPDmessageActionChatEditPhoto &data) {\n\t\t\t\tpush(data.vphoto());\n\t\t\t}, [&](const MTPDmessageActionSuggestProfilePhoto &data) {\n\t\t\t\tpush(data.vphoto());\n\t\t\t}, [&](const MTPDmessageActionSetChatWallPaper &data) {\n\t\t\t\tpush(data.vwallpaper());\n\t\t\t}, [](const auto &data) {\n\t\t\t});\n\t\t\tpush(data.vreply_to());\n\t\t}, [](const MTPDmessageEmpty &data) {\n\t\t});\n\t}\n\tvoid push(const MTPStoryItem &data) {\n\t\tdata.match([&](const MTPDstoryItem &data) {\n\t\t\tpush(data.vmedia());\n\t\t}, [](const MTPDstoryItemDeleted &) {\n\t\t}, [](const MTPDstoryItemSkipped &) {\n\t\t});\n\t}\n\tvoid push(const MTPmessages_Messages &data) {\n\t\tdata.match([](const MTPDmessages_messagesNotModified &) {\n\t\t}, [&](const auto &data) {\n\t\t\tpush(data.vmessages());\n\t\t});\n\t}\n\tvoid push(const MTPphotos_Photos &data) {\n\t\tdata.match([&](const auto &data) {\n\t\t\tpush(data.vphotos());\n\t\t});\n\t}\n\tvoid push(const MTPusers_UserFull &data) {\n\t\tconst auto &full = data.data().vfull_user().data();\n\t\tpush(full.vpersonal_photo());\n\t\tpush(full.vfallback_photo());\n\t\tpush(full.vprofile_photo());\n\t}\n\tvoid push(const MTPChatFull &data) {\n\t\tdata.match([&](const MTPDchatFull &data) {\n\t\t\tpush(data.vchat_photo());\n\t\t}, [&](const MTPDchannelFull &data) {\n\t\t\tpush(data.vchat_photo());\n\t\t});\n\t}\n\tvoid push(const MTPmessages_ChatFull &data) {\n\t\tpush(data.data().vfull_chat());\n\t}\n\tvoid push(const MTPmessages_RecentStickers &data) {\n\t\tdata.match([&](const MTPDmessages_recentStickers &data) {\n\t\t\tpush(data.vstickers());\n\t\t}, [](const MTPDmessages_recentStickersNotModified &data) {\n\t\t});\n\t}\n\tvoid push(const MTPmessages_FavedStickers &data) {\n\t\tdata.match([&](const MTPDmessages_favedStickers &data) {\n\t\t\tpush(data.vstickers());\n\t\t}, [](const MTPDmessages_favedStickersNotModified &data) {\n\t\t});\n\t}\n\tvoid push(const MTPmessages_StickerSet &data) {\n\t\tdata.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\tpush(data.vdocuments());\n\t\t}, [](const MTPDmessages_stickerSetNotModified &data) {\n\t\t});\n\t}\n\tvoid push(const MTPmessages_SavedGifs &data) {\n\t\tdata.match([&](const MTPDmessages_savedGifs &data) {\n\t\t\tpush(data.vgifs());\n\t\t}, [](const MTPDmessages_savedGifsNotModified &data) {\n\t\t});\n\t}\n\tvoid push(const MTPaccount_SavedRingtones &data) {\n\t\tdata.match([&](const MTPDaccount_savedRingtones &data) {\n\t\t\tpush(data.vringtones());\n\t\t}, [](const MTPDaccount_savedRingtonesNotModified &data) {\n\t\t});\n\t}\n\tvoid push(const MTPhelp_PremiumPromo &data) {\n\t\tpush(data.data().vvideos());\n\t}\n\tvoid push(const MTPmessages_WebPage &data) {\n\t\tpush(data.data().vwebpage());\n\t}\n\tvoid push(const MTPstories_Stories &data) {\n\t\tpush(data.data().vstories());\n\t}\n\tvoid push(const MTPusers_SavedMusic &data) {\n\t\tdata.match([&](const MTPDusers_savedMusic &data) {\n\t\t\tpush(data.vdocuments());\n\t\t}, [](const MTPDusers_savedMusicNotModified &data) {\n\t\t});\n\t}\n\n\tUpdatedFileReferences result;\n};\n\ntemplate <typename Type>\nUpdatedFileReferences GetFileReferencesHelper(const Type &data) {\n\tFileReferenceAccumulator result;\n\tresult.push(data);\n\treturn result.result;\n}\n\n} // namespace\n\nUpdatedFileReferences GetFileReferences(const MTPmessages_Messages &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\nUpdatedFileReferences GetFileReferences(const MTPphotos_Photos &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\nUpdatedFileReferences GetFileReferences(const MTPusers_UserFull &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\nUpdatedFileReferences GetFileReferences(const MTPmessages_ChatFull &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\nUpdatedFileReferences GetFileReferences(\n\t\tconst MTPmessages_RecentStickers &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\nUpdatedFileReferences GetFileReferences(\n\t\tconst MTPmessages_FavedStickers &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\nUpdatedFileReferences GetFileReferences(\n\t\tconst MTPmessages_StickerSet &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\nUpdatedFileReferences GetFileReferences(const MTPmessages_SavedGifs &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\nUpdatedFileReferences GetFileReferences(const MTPWallPaper &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\nUpdatedFileReferences GetFileReferences(const MTPTheme &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\nUpdatedFileReferences GetFileReferences(\n\t\tconst MTPaccount_SavedRingtones &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\nUpdatedFileReferences GetFileReferences(const MTPhelp_PremiumPromo &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\nUpdatedFileReferences GetFileReferences(const MTPmessages_WebPage &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\nUpdatedFileReferences GetFileReferences(const MTPstories_Stories &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\nUpdatedFileReferences GetFileReferences(const MTPusers_SavedMusic &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\nUpdatedFileReferences GetFileReferences(const MTPMessageMedia &data) {\n\treturn GetFileReferencesHelper(data);\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_file_origin.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/variant.h\"\n#include \"data/data_types.h\"\n\nnamespace Data {\n\nusing FileOriginMessage = FullMsgId;\nusing FileOriginStory = FullStoryId;\n\nstruct FileOriginUserPhoto {\n\tFileOriginUserPhoto(UserId userId, PhotoId photoId)\n\t: userId(userId)\n\t, photoId(photoId) {\n\t}\n\n\tUserId userId = 0;\n\tPhotoId photoId = 0;\n\n\tinline bool operator<(const FileOriginUserPhoto &other) const {\n\t\treturn std::tie(userId, photoId)\n\t\t\t< std::tie(other.userId, other.photoId);\n\t}\n};\n\nstruct FileOriginFullUser {\n\tFileOriginFullUser(UserId userId)\n\t: userId(userId) {\n\t}\n\n\tUserId userId = 0;\n\n\tinline bool operator<(const FileOriginFullUser &other) const {\n\t\treturn userId < other.userId;\n\t}\n};\n\nstruct FileOriginPeerPhoto {\n\texplicit FileOriginPeerPhoto(PeerId peerId) : peerId(peerId) {\n\t}\n\n\tPeerId peerId = 0;\n\n\tinline bool operator<(const FileOriginPeerPhoto &other) const {\n\t\treturn peerId < other.peerId;\n\t}\n};\n\nstruct FileOriginStickerSet {\n\tFileOriginStickerSet(uint64 setId, uint64 accessHash)\n\t: setId(setId)\n\t, accessHash(accessHash) {\n\t}\n\n\tuint64 setId = 0;\n\tuint64 accessHash = 0;\n\n\tinline bool operator<(const FileOriginStickerSet &other) const {\n\t\treturn setId < other.setId;\n\t}\n};\n\nstruct FileOriginSavedGifs {\n\tinline bool operator<(const FileOriginSavedGifs &) const {\n\t\treturn false;\n\t}\n};\n\nstruct FileOriginWallpaper {\n\tFileOriginWallpaper(\n\t\tuint64 paperId,\n\t\tuint64 accessHash,\n\t\tUserId ownerId,\n\t\tconst QString &slug)\n\t: paperId(paperId)\n\t, accessHash(accessHash)\n\t, ownerId(ownerId)\n\t, slug(slug) {\n\t}\n\n\tuint64 paperId = 0;\n\tuint64 accessHash = 0;\n\tUserId ownerId = 0;\n\tQString slug;\n\n\tinline bool operator<(const FileOriginWallpaper &other) const {\n\t\treturn paperId < other.paperId;\n\t}\n};\n\nstruct FileOriginTheme {\n\tFileOriginTheme(uint64 themeId, uint64 accessHash)\n\t: themeId(themeId)\n\t, accessHash(accessHash) {\n\t}\n\n\tuint64 themeId = 0;\n\tuint64 accessHash = 0;\n\n\tinline bool operator<(const FileOriginTheme &other) const {\n\t\treturn themeId < other.themeId;\n\t}\n};\n\nstruct FileOriginRingtones {\n\tinline bool operator<(const FileOriginRingtones &) const {\n\t\treturn false;\n\t}\n};\n\nstruct FileOriginPremiumPreviews {\n\tinline bool operator<(const FileOriginPremiumPreviews &) const {\n\t\treturn false;\n\t}\n};\n\nstruct FileOriginWebPage {\n\tQString url;\n\n\tinline bool operator<(const FileOriginWebPage &other) const {\n\t\treturn url < other.url;\n\t}\n};\n\nstruct FileOrigin {\n\tusing Variant = std::variant<\n\t\tv::null_t,\n\t\tFileOriginMessage,\n\t\tFileOriginUserPhoto,\n\t\tFileOriginFullUser,\n\t\tFileOriginPeerPhoto,\n\t\tFileOriginStickerSet,\n\t\tFileOriginSavedGifs,\n\t\tFileOriginWallpaper,\n\t\tFileOriginTheme,\n\t\tFileOriginRingtones,\n\t\tFileOriginPremiumPreviews,\n\t\tFileOriginWebPage,\n\t\tFileOriginStory>;\n\n\tFileOrigin() = default;\n\tFileOrigin(FileOriginMessage data) : data(data) {\n\t}\n\tFileOrigin(FileOriginUserPhoto data) : data(data) {\n\t}\n\tFileOrigin(FileOriginFullUser data) : data(data) {\n\t}\n\tFileOrigin(FileOriginPeerPhoto data) : data(data) {\n\t}\n\tFileOrigin(FileOriginStickerSet data) : data(data) {\n\t}\n\tFileOrigin(FileOriginSavedGifs data) : data(data) {\n\t}\n\tFileOrigin(FileOriginWallpaper data) : data(data) {\n\t}\n\tFileOrigin(FileOriginTheme data) : data(data) {\n\t}\n\tFileOrigin(FileOriginRingtones data) : data(data) {\n\t}\n\tFileOrigin(FileOriginPremiumPreviews data) : data(data) {\n\t}\n\tFileOrigin(FileOriginWebPage data) : data(data) {\n\t}\n\tFileOrigin(FileOriginStory data) : data(data) {\n\t}\n\n\texplicit operator bool() const {\n\t\treturn !v::is_null(data);\n\t}\n\tinline bool operator<(const FileOrigin &other) const {\n\t\treturn data < other.data;\n\t}\n\n\tVariant data;\n};\n\nstruct DocumentFileLocationId {\n\tuint64 id = 0;\n};\n\ninline bool operator<(DocumentFileLocationId a, DocumentFileLocationId b) {\n\treturn a.id < b.id;\n}\n\nstruct PhotoFileLocationId {\n\tuint64 id = 0;\n};\n\ninline bool operator<(PhotoFileLocationId a, PhotoFileLocationId b) {\n\treturn a.id < b.id;\n}\n\nusing FileLocationId = std::variant<\n\tDocumentFileLocationId,\n\tPhotoFileLocationId>;\n\nstruct UpdatedFileReferences {\n\tstd::map<FileLocationId, QByteArray> data;\n};\n\nUpdatedFileReferences GetFileReferences(const MTPmessages_Messages &data);\nUpdatedFileReferences GetFileReferences(const MTPphotos_Photos &data);\nUpdatedFileReferences GetFileReferences(const MTPusers_UserFull &data);\nUpdatedFileReferences GetFileReferences(const MTPmessages_ChatFull &data);\nUpdatedFileReferences GetFileReferences(\n\tconst MTPmessages_RecentStickers &data);\nUpdatedFileReferences GetFileReferences(\n\tconst MTPmessages_FavedStickers &data);\nUpdatedFileReferences GetFileReferences(const MTPmessages_StickerSet &data);\nUpdatedFileReferences GetFileReferences(const MTPmessages_SavedGifs &data);\nUpdatedFileReferences GetFileReferences(const MTPWallPaper &data);\nUpdatedFileReferences GetFileReferences(const MTPTheme &data);\nUpdatedFileReferences GetFileReferences(\n\tconst MTPaccount_SavedRingtones &data);\nUpdatedFileReferences GetFileReferences(const MTPhelp_PremiumPromo &data);\nUpdatedFileReferences GetFileReferences(const MTPmessages_WebPage &data);\nUpdatedFileReferences GetFileReferences(const MTPstories_Stories &data);\nUpdatedFileReferences GetFileReferences(const MTPusers_SavedMusic &data);\n\n// Admin Log Event.\nUpdatedFileReferences GetFileReferences(const MTPMessageMedia &data);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_flags.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <rpl/event_stream.h>\n\nnamespace Data {\n\ntemplate <typename FlagsType>\nusing FlagsUnderlying = typename FlagsType::Type;\n\ntemplate <\n\ttypename FlagsType,\n\tFlagsUnderlying<FlagsType> kEssential = FlagsUnderlying<FlagsType>(-1)>\nclass Flags {\npublic:\n\tusing Type = FlagsType;\n\tusing Enum = typename Type::Enum;\n\n\tstruct Change {\n\t\tusing Type = FlagsType;\n\t\tusing Enum = typename Type::Enum;\n\n\t\tChange(Type diff, Type value)\n\t\t: diff(diff)\n\t\t, value(value) {\n\t\t}\n\t\tType diff = 0;\n\t\tType value = 0;\n\t};\n\n\tFlags() = default;\n\tFlags(Type value) : _value(value) {\n\t}\n\n\tvoid set(Type which) {\n\t\tif (auto diff = which ^ _value) {\n\t\t\t_value = which;\n\t\t\tupdated(diff);\n\t\t}\n\t}\n\tvoid add(Type which) {\n\t\tif (auto diff = which & ~_value) {\n\t\t\t_value |= which;\n\t\t\tupdated(diff);\n\t\t}\n\t}\n\tvoid remove(Type which) {\n\t\tif (auto diff = which & _value) {\n\t\t\t_value &= ~which;\n\t\t\tupdated(diff);\n\t\t}\n\t}\n\tauto current() const {\n\t\treturn _value;\n\t}\n\tauto changes() const {\n\t\treturn _changes.events();\n\t}\n\tauto value() const {\n\t\treturn _changes.events_starting_with({\n\t\t\tType::from_raw(kEssential),\n\t\t\t_value });\n\t}\n\nprivate:\n\tvoid updated(Type diff) {\n\t\tif ((diff &= Type::from_raw(kEssential))) {\n\t\t\t_changes.fire({ diff, _value });\n\t\t}\n\t}\n\n\tType _value = 0;\n\trpl::event_stream<Change> _changes;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_folder.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_folder.h\"\n\n#include \"data/data_session.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_changes.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"lang/lang_keys.h\"\n#include \"storage/storage_facade.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"apiwrap.h\"\n#include \"mainwidget.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kLoadedChatsMinCount = 20;\nconstexpr auto kShowChatNamesCount = 8;\n\n[[nodiscard]] TextWithEntities ComposeFolderListEntryText(\n\t\tnot_null<Folder*> folder) {\n\tconst auto &list = folder->lastHistories();\n\tif (list.empty()) {\n\t\tif (const auto storiesUnread = folder->storiesUnreadCount()) {\n\t\t\treturn {\n\t\t\t\ttr::lng_contacts_stories_status_new(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tstoriesUnread),\n\t\t\t};\n\t\t} else if (const auto storiesCount = folder->storiesCount()) {\n\t\t\treturn {\n\t\t\t\ttr::lng_contacts_stories_status(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tstoriesCount),\n\t\t\t};\n\t\t}\n\t\treturn {};\n\t}\n\n\tconst auto count = std::max(\n\t\tint(list.size()),\n\t\tfolder->chatsList()->fullSize().current());\n\n\tconst auto throwAwayLastName = (list.size() > 1)\n\t\t&& (count == list.size() + 1);\n\tauto &&peers = ranges::views::all(\n\t\tlist\n\t) | ranges::views::take(\n\t\tlist.size() - (throwAwayLastName ? 1 : 0)\n\t);\n\tconst auto wrapName = [](not_null<History*> history) {\n\t\tconst auto name = history->peer->name();\n\t\treturn st::wrap_rtl(TextWithEntities{\n\t\t\t.text = name,\n\t\t\t.entities = (history->chatListBadgesState().unread\n\t\t\t\t? EntitiesInText{\n\t\t\t\t\t{ EntityType::Semibold, 0, int(name.size()), QString() },\n\t\t\t\t\t{ EntityType::Colorized, 0, int(name.size()), QString() },\n\t\t\t\t}\n\t\t\t\t: EntitiesInText{}),\n\t\t});\n\t};\n\tconst auto shown = int(peers.size());\n\tconst auto accumulated = [&] {\n\t\tExpects(shown > 0);\n\n\t\tauto i = peers.begin();\n\t\tauto result = wrapName(*i);\n\t\tfor (++i; i != peers.end(); ++i) {\n\t\t\tresult = tr::lng_archived_last_list(\n\t\t\t\ttr::now,\n\t\t\t\tlt_accumulated,\n\t\t\t\tresult,\n\t\t\t\tlt_chat,\n\t\t\t\twrapName(*i),\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t}();\n\treturn (shown < count)\n\t\t? tr::lng_archived_last(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\t(count - shown),\n\t\t\tlt_chats,\n\t\t\taccumulated,\n\t\t\ttr::marked)\n\t\t: accumulated;\n}\n\n} // namespace\n\nFolder::Folder(not_null<Session*> owner, FolderId id)\n: Entry(owner, Type::Folder)\n, _id(id)\n, _chatsList(\n\t&owner->session(),\n\tFilterId(),\n\towner->maxPinnedChatsLimitValue(this))\n, _name(tr::lng_archived_name(tr::now)) {\n\tindexNameParts();\n\n\tsession().changes().peerUpdates(\n\t\tPeerUpdate::Flag::Name\n\t) | rpl::filter([=](const PeerUpdate &update) {\n\t\treturn ranges::contains(_lastHistories, update.peer, &History::peer);\n\t}) | rpl::on_next([=] {\n\t\t++_chatListViewVersion;\n\t\tupdateChatListEntryPostponed();\n\t}, _lifetime);\n\n\t_chatsList.setAllAreMuted(true);\n\n\t_chatsList.unreadStateChanges(\n\t) | rpl::filter([=] {\n\t\treturn inChatList();\n\t}) | rpl::on_next([=](const Dialogs::UnreadState &old) {\n\t\t++_chatListViewVersion;\n\t\tnotifyUnreadStateChange(old);\n\t}, _lifetime);\n\n\t_chatsList.fullSize().changes(\n\t) | rpl::on_next([=] {\n\t\tupdateChatListEntryPostponed();\n\t}, _lifetime);\n}\n\nFolderId Folder::id() const {\n\treturn _id;\n}\n\nvoid Folder::indexNameParts() {\n\t// We don't want archive to be filtered in the chats list.\n}\n\nvoid Folder::registerOne(not_null<History*> history) {\n\tif (_chatsList.indexed()->size() == 1) {\n\t\tupdateChatListSortPosition();\n\t\tif (!_chatsList.cloudUnreadKnown()) {\n\t\t\towner().histories().requestDialogEntry(this);\n\t\t}\n\t} else {\n\t\tupdateChatListEntry();\n\t}\n\treorderLastHistories();\n}\n\nvoid Folder::unregisterOne(not_null<History*> history) {\n\tif (_chatsList.empty()) {\n\t\tupdateChatListExistence();\n\t}\n\treorderLastHistories();\n}\n\nint Folder::chatListNameVersion() const {\n\treturn 1;\n}\n\nvoid Folder::oneListMessageChanged(HistoryItem *from, HistoryItem *to) {\n\tif (from || to) {\n\t\treorderLastHistories();\n\t}\n}\n\nvoid Folder::reorderLastHistories() {\n\t// We want first kShowChatNamesCount histories, by last message date.\n\tconst auto pred = [](not_null<History*> a, not_null<History*> b) {\n\t\tconst auto aItem = a->chatListMessage();\n\t\tconst auto bItem = b->chatListMessage();\n\t\tconst auto aDate = aItem ? aItem->date() : TimeId(0);\n\t\tconst auto bDate = bItem ? bItem->date() : TimeId(0);\n\t\treturn aDate > bDate;\n\t};\n\t_lastHistories.clear();\n\t_lastHistories.reserve(kShowChatNamesCount + 1);\n\tauto &&histories = ranges::views::all(\n\t\t*_chatsList.indexed()\n\t) | ranges::views::transform([](not_null<Dialogs::Row*> row) {\n\t\treturn row->history();\n\t}) | ranges::views::filter([](History *history) {\n\t\treturn (history != nullptr);\n\t});\n\tauto nonPinnedChecked = 0;\n\tfor (const auto history : histories) {\n\t\tconst auto i = ranges::upper_bound(\n\t\t\t_lastHistories,\n\t\t\tnot_null(history),\n\t\t\tpred);\n\t\tif (size(_lastHistories) < kShowChatNamesCount\n\t\t\t|| i != end(_lastHistories)) {\n\t\t\t_lastHistories.insert(i, history);\n\t\t}\n\t\tif (size(_lastHistories) > kShowChatNamesCount) {\n\t\t\t_lastHistories.pop_back();\n\t\t}\n\t\tif (!history->isPinnedDialog(FilterId())\n\t\t\t&& ++nonPinnedChecked >= kShowChatNamesCount) {\n\t\t\tbreak;\n\t\t}\n\t}\n\t++_chatListViewVersion;\n\tupdateChatListEntry();\n}\n\nnot_null<Dialogs::MainList*> Folder::chatsList() {\n\treturn &_chatsList;\n}\n\nvoid Folder::clearChatsList() {\n\t_chatsList.clear();\n}\n\nvoid Folder::chatListPreloadData() {\n}\n\nvoid Folder::paintUserpic(\n\t\tPainter &p,\n\t\tUi::PeerUserpicView &view,\n\t\tconst Dialogs::Ui::PaintContext &context) const {\n\tpaintUserpic(\n\t\tp,\n\t\tcontext.st->padding.left(),\n\t\tcontext.st->padding.top(),\n\t\tcontext.st->photoSize);\n}\n\nvoid Folder::paintUserpic(Painter &p, int x, int y, int size) const {\n\tpaintUserpic(p, x, y, size, nullptr, nullptr);\n}\n\nvoid Folder::paintUserpic(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tconst style::color &bg,\n\t\tconst style::color &fg) const {\n\tpaintUserpic(p, x, y, size, &bg, &fg);\n}\n\nvoid Folder::paintUserpic(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tconst style::color *overrideBg,\n\t\tconst style::color *overrideFg) const {\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(overrideBg ? *overrideBg : st::historyPeerArchiveUserpicBg);\n\t{\n\t\tPainterHighQualityEnabler hq(p);\n\t\tCore::App().settings().fork().squareUserpics()\n\t\t\t? p.drawRect(x, y, size, size)\n\t\t\t: p.drawEllipse(x, y, size, size);\n\t}\n\tif (size == st::defaultDialogRow.photoSize) {\n\t\tconst auto rect = QRect{ x, y, size, size };\n\t\tif (overrideFg) {\n\t\t\tst::dialogsArchiveUserpic.paintInCenter(\n\t\t\t\tp,\n\t\t\t\trect,\n\t\t\t\t(*overrideFg)->c);\n\t\t} else {\n\t\t\tst::dialogsArchiveUserpic.paintInCenter(p, rect);\n\t\t}\n\t} else {\n\t\tp.save();\n\t\tconst auto ratio = size / float64(st::defaultDialogRow.photoSize);\n\t\tp.translate(x + size / 2., y + size / 2.);\n\t\tp.scale(ratio, ratio);\n\t\tconst auto skip = st::defaultDialogRow.photoSize;\n\t\tconst auto rect = QRect{ -skip, -skip, 2 * skip, 2 * skip };\n\t\tif (overrideFg) {\n\t\t\tst::dialogsArchiveUserpic.paintInCenter(\n\t\t\t\tp,\n\t\t\t\trect,\n\t\t\t\t(*overrideFg)->c);\n\t\t} else {\n\t\t\tst::dialogsArchiveUserpic.paintInCenter(p, rect);\n\t\t}\n\t\tp.restore();\n\t}\n}\n\nconst std::vector<not_null<History*>> &Folder::lastHistories() const {\n\treturn _lastHistories;\n}\n\nvoid Folder::validateListEntryCache() {\n\tif (_listEntryCacheVersion == _chatListViewVersion) {\n\t\treturn;\n\t}\n\t_listEntryCacheVersion = _chatListViewVersion;\n\t_listEntryCache.setMarkedText(\n\t\tst::dialogsTextStyle,\n\t\tComposeFolderListEntryText(this),\n\t\t// Use rich options as long as the entry text does not have user text.\n\t\tUi::ItemTextDefaultOptions());\n}\n\nvoid Folder::updateStoriesCount(int count, int unread) {\n\tif (_storiesCount == count && _storiesUnreadCount == unread) {\n\t\treturn;\n\t}\n\tconst auto limit = (1 << 16) - 1;\n\tconst auto was = (_storiesCount > 0);\n\t_storiesCount = std::min(count, limit);\n\t_storiesUnreadCount = std::min(unread, limit);\n\tconst auto now = (_storiesCount > 0);\n\tif (was == now) {\n\t\tupdateChatListEntryPostponed();\n\t} else if (now) {\n\t\tupdateChatListSortPosition();\n\t} else {\n\t\tupdateChatListExistence();\n\t}\n\t++_chatListViewVersion;\n}\n\nint Folder::storiesCount() const {\n\treturn _storiesCount;\n}\n\nint Folder::storiesUnreadCount() const {\n\treturn _storiesUnreadCount;\n}\n\nTimeId Folder::adjustedChatListTimeId() const {\n\treturn chatListTimeId();\n}\n\nvoid Folder::applyDialog(const MTPDdialogFolder &data) {\n\t_chatsList.updateCloudUnread(data);\n\tif (const auto peerId = peerFromMTP(data.vpeer())) {\n\t\tconst auto history = owner().history(peerId);\n\t\tconst auto fullId = FullMsgId(peerId, data.vtop_message().v);\n\t\thistory->setFolder(this, owner().message(fullId));\n\t} else {\n\t\t_chatsList.clear();\n\t\tupdateChatListExistence();\n\t}\n\tif (_chatsList.indexed()->size() < kLoadedChatsMinCount) {\n\t\tsession().api().requestDialogs(this);\n\t}\n}\n\nvoid Folder::applyPinnedUpdate(const MTPDupdateDialogPinned &data) {\n\tconst auto folderId = data.vfolder_id().value_or_empty();\n\tif (folderId != 0) {\n\t\tLOG((\"API Error: Nested folders detected.\"));\n\t}\n\towner().setChatPinned(this, FilterId(), data.is_pinned());\n}\n\nint Folder::fixedOnTopIndex() const {\n\treturn kArchiveFixOnTopIndex;\n}\n\nbool Folder::shouldBeInChatList() const {\n\treturn !_chatsList.empty() || (_storiesCount > 0);\n}\n\nDialogs::UnreadState Folder::chatListUnreadState() const {\n\treturn _chatsList.unreadState();\n}\n\nDialogs::BadgesState Folder::chatListBadgesState() const {\n\tauto result = Dialogs::BadgesForUnread(\n\t\tchatListUnreadState(),\n\t\tDialogs::CountInBadge::Chats,\n\t\tDialogs::IncludeInBadge::All);\n\tresult.unreadMuted\n\t\t= result.mentionMuted\n\t\t= result.reactionMuted\n\t\t= result.pollMuted\n\t\t= true;\n\tif (result.unread && !result.unreadCounter) {\n\t\tresult.unreadCounter = 1;\n\t}\n\treturn result;\n}\n\nHistoryItem *Folder::chatListMessage() const {\n\treturn nullptr;\n}\n\nbool Folder::chatListMessageKnown() const {\n\treturn true;\n}\n\nconst QString &Folder::chatListName() const {\n\treturn _name;\n}\n\nconst base::flat_set<QString> &Folder::chatListNameWords() const {\n\treturn _nameWords;\n}\n\nconst base::flat_set<QChar> &Folder::chatListFirstLetters() const {\n\treturn _nameFirstLetters;\n}\n\nconst QString &Folder::chatListNameSortKey() const {\n\tstatic const auto empty = QString();\n\treturn empty;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_folder.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"dialogs/dialogs_entry.h\"\n#include \"dialogs/dialogs_main_list.h\"\n#include \"data/data_messages.h\"\n#include \"base/weak_ptr.h\"\n\nclass ChannelData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nclass Session;\n\nclass Folder final : public Dialogs::Entry {\npublic:\n\tstatic constexpr auto kId = 1;\n\n\tFolder(not_null<Data::Session*> owner, FolderId id);\n\tFolder(const Folder &) = delete;\n\tFolder &operator=(const Folder &) = delete;\n\n\t[[nodiscard]] FolderId id() const;\n\tvoid registerOne(not_null<History*> history);\n\tvoid unregisterOne(not_null<History*> history);\n\tvoid oneListMessageChanged(HistoryItem *from, HistoryItem *to);\n\n\tvoid clearChatsList();\n\t[[nodiscard]] not_null<Dialogs::MainList*> chatsList();\n\n\tvoid applyDialog(const MTPDdialogFolder &data);\n\tvoid applyPinnedUpdate(const MTPDupdateDialogPinned &data);\n\n\tTimeId adjustedChatListTimeId() const override;\n\n\tint fixedOnTopIndex() const override;\n\tbool shouldBeInChatList() const override;\n\tDialogs::UnreadState chatListUnreadState() const override;\n\tDialogs::BadgesState chatListBadgesState() const override;\n\tHistoryItem *chatListMessage() const override;\n\tbool chatListMessageKnown() const override;\n\tconst QString &chatListName() const override;\n\tconst QString &chatListNameSortKey() const override;\n\tint chatListNameVersion() const override;\n\tconst base::flat_set<QString> &chatListNameWords() const override;\n\tconst base::flat_set<QChar> &chatListFirstLetters() const override;\n\n\tvoid chatListPreloadData() override;\n\tvoid paintUserpic(\n\t\tPainter &p,\n\t\tUi::PeerUserpicView &view,\n\t\tconst Dialogs::Ui::PaintContext &context) const override;\n\tvoid paintUserpic(Painter &p, int x, int y, int size) const;\n\tvoid paintUserpic(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tconst style::color &overrideBg,\n\t\tconst style::color &overrideFg) const;\n\n\tconst std::vector<not_null<History*>> &lastHistories() const;\n\tvoid validateListEntryCache();\n\t[[nodiscard]] const Ui::Text::String &listEntryCache() const {\n\t\treturn _listEntryCache;\n\t}\n\n\tvoid updateStoriesCount(int count, int unread);\n\t[[nodiscard]] int storiesCount() const;\n\t[[nodiscard]] int storiesUnreadCount() const;\n\nprivate:\n\tvoid indexNameParts();\n\n\tvoid reorderLastHistories();\n\n\tvoid paintUserpic(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tconst style::color *overrideBg,\n\t\tconst style::color *overrideFg) const;\n\n\tconst FolderId _id = 0;\n\tDialogs::MainList _chatsList;\n\n\tQString _name;\n\tbase::flat_set<QString> _nameWords;\n\tbase::flat_set<QChar> _nameFirstLetters;\n\n\tstd::vector<not_null<History*>> _lastHistories;\n\n\tUi::Text::String _listEntryCache;\n\tint _listEntryCacheVersion = 0;\n\tint _chatListViewVersion = 0;\n\t//rpl::variable<MessagePosition> _unreadPosition;\n\n\tuint16_t _storiesCount = 0;\n\tuint16_t _storiesUnreadCount = 0;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_forum.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_forum.h\"\n\n#include \"data/components/recent_peers.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_session.h\"\n#include \"data/data_forum_icons.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_replies_list.h\"\n#include \"data/data_user.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_unread_things.h\"\n#include \"main/main_session.h\"\n#include \"base/random.h\"\n#include \"base/unixtime.h\"\n#include \"apiwrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"core/application.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"storage/storage_facade.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/notifications_manager.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kTopicsFirstLoad = 20;\nconstexpr auto kLoadedTopicsMinCount = 20;\nconstexpr auto kTopicsPerPage = 500;\nconstexpr auto kStalePerRequest = 100;\nconstexpr auto kShowTopicNamesCount = 8;\n// constexpr auto kGeneralColorId = 0xA9A9A9;\n\n} // namespace\n\nForum::Forum(not_null<History*> history)\n: _history(history)\n, _topicsList(&session(), {}, owner().maxPinnedChatsLimitValue(this)) {\n\tExpects(_history->peer->isChannel()\n\t\t|| _history->peer->isBot());\n\n\tif (_history->inChatList()) {\n\t\tpreloadTopics();\n\t}\n\tif (peer()->canCreateTopics()) {\n\t\towner().forumIcons().requestDefaultIfUnknown();\n\t}\n}\n\nForum::~Forum() {\n\tfor (const auto &request : _topicRequests) {\n\t\tif (request.second.id != _staleRequestId) {\n\t\t\towner().histories().cancelRequest(request.second.id);\n\t\t}\n\t}\n\tif (_staleRequestId) {\n\t\tsession().api().request(_staleRequestId).cancel();\n\t}\n\tif (_requestId) {\n\t\tsession().api().request(_requestId).cancel();\n\t}\n\tauto &storage = session().storage();\n\tauto &changes = session().changes();\n\tconst auto peerId = _history->peer->id;\n\tfor (const auto &[rootId, topic] : _topics) {\n\t\tstorage.unload(Storage::SharedMediaUnloadThread(\n\t\t\tpeerId,\n\t\t\trootId,\n\t\t\tPeerId()));\n\t\t_history->setForwardDraft(rootId, PeerId(), {});\n\n\t\tconst auto raw = topic.get();\n\t\tchanges.topicRemoved(raw);\n\t\tchanges.entryRemoved(raw);\n\t}\n}\n\nSession &Forum::owner() const {\n\treturn _history->owner();\n}\n\nMain::Session &Forum::session() const {\n\treturn _history->session();\n}\n\nnot_null<History*> Forum::history() const {\n\treturn _history;\n}\n\nnot_null<PeerData*> Forum::peer() const {\n\treturn _history->peer;\n}\n\nUserData *Forum::bot() const {\n\treturn _history->peer->asBot();\n}\n\nChannelData *Forum::channel() const {\n\treturn _history->peer->asChannel();\n}\n\nnot_null<Dialogs::MainList*> Forum::topicsList() {\n\treturn &_topicsList;\n}\n\nrpl::producer<> Forum::destroyed() const {\n\tif (const auto bot = this->bot()) {\n\t\treturn bot->flagsValue(\n\t\t) | rpl::filter([=](const UserData::Flags::Change &update) {\n\t\t\tusing Flag = UserData::Flag;\n\t\t\treturn (update.diff & Flag::Forum)\n\t\t\t\t&& !(update.value & Flag::Forum);\n\t\t}) | rpl::take(1) | rpl::to_empty;\n\t}\n\treturn channel()->flagsValue(\n\t) | rpl::filter([=](const ChannelData::Flags::Change &update) {\n\t\tusing Flag = ChannelData::Flag;\n\t\treturn (update.diff & Flag::Forum) && !(update.value & Flag::Forum);\n\t}) | rpl::take(1) | rpl::to_empty;\n}\n\nrpl::producer<not_null<ForumTopic*>> Forum::topicDestroyed() const {\n\treturn _topicDestroyed.events();\n}\n\nvoid Forum::preloadTopics() {\n\tif (topicsList()->indexed()->size() < kLoadedTopicsMinCount) {\n\t\trequestTopics();\n\t}\n}\n\nvoid Forum::reloadTopics() {\n\t_topicsList.setLoaded(false);\n\tsession().api().request(base::take(_requestId)).cancel();\n\t_offset = {};\n\tfor (const auto &[rootId, topic] : _topics) {\n\t\tif (!topic->creating()) {\n\t\t\t_staleRootIds.emplace(topic->rootId());\n\t\t}\n\t}\n\trequestTopics();\n}\n\nvoid Forum::requestTopics() {\n\tif (_topicsList.loaded() || _requestId) {\n\t\treturn;\n\t}\n\tconst auto firstLoad = !_offset.date;\n\tconst auto loadCount = firstLoad ? kTopicsFirstLoad : kTopicsPerPage;\n\t_requestId = session().api().request(MTPmessages_GetForumTopics(\n\t\tMTP_flags(0),\n\t\tpeer()->input(),\n\t\tMTPstring(), // q\n\t\tMTP_int(_offset.date),\n\t\tMTP_int(_offset.id),\n\t\tMTP_int(_offset.topicId),\n\t\tMTP_int(loadCount)\n\t)).done([=](const MTPmessages_ForumTopics &result) {\n\t\tconst auto previousOffset = _offset;\n\t\tapplyReceivedTopics(result, _offset);\n\t\tconst auto &list = result.data().vtopics().v;\n\t\tif (list.isEmpty()\n\t\t\t|| list.size() == result.data().vcount().v\n\t\t\t|| (_offset == previousOffset)) {\n\t\t\t_topicsList.setLoaded();\n\t\t}\n\t\t_requestId = 0;\n\t\t_chatsListChanges.fire({});\n\t\tif (_topicsList.loaded()) {\n\t\t\t_chatsListLoadedEvents.fire({});\n\t\t}\n\t\treorderLastTopics();\n\t\trequestSomeStale();\n\t}).fail([=](const MTP::Error &error) {\n\t\t_requestId = 0;\n\t\t_topicsList.setLoaded();\n\t\tif (error.type() == u\"CHANNEL_FORUM_MISSING\"_q && channel()) {\n\t\t\tconst auto flags = channel()->flags() & ~ChannelDataFlag::Forum;\n\t\t\tchannel()->setFlags(flags);\n\t\t}\n\t}).send();\n}\n\nvoid Forum::applyTopicDeleted(MsgId rootId) {\n\t_topicsDeleted.emplace(rootId);\n\n\tconst auto i = _topics.find(rootId);\n\tif (i == end(_topics)) {\n\t\treturn;\n\t}\n\tconst auto raw = i->second.get();\n\tCore::App().notifications().clearFromTopic(raw);\n\towner().removeChatListEntry(raw);\n\n\tif (ranges::contains(_lastTopics, not_null(raw))) {\n\t\treorderLastTopics();\n\t}\n\n\tif (_activeSubsectionTopic == raw) {\n\t\t_activeSubsectionTopic = nullptr;\n\t}\n\t_topicDestroyed.fire(raw);\n\t_history->session().recentPeers().chatOpenRemove(raw);\n\tsession().changes().topicUpdated(\n\t\traw,\n\t\tData::TopicUpdate::Flag::Destroyed);\n\tsession().changes().entryUpdated(\n\t\traw,\n\t\tData::EntryUpdate::Flag::Destroyed);\n\t_topics.erase(i);\n\n\t_history->destroyMessagesByTopic(rootId);\n\tsession().storage().unload(Storage::SharedMediaUnloadThread(\n\t\t_history->peer->id,\n\t\trootId,\n\t\tPeerId()));\n\t_history->setForwardDraft(rootId, PeerId(), {});\n}\n\nvoid Forum::reorderLastTopics() {\n\t// We want first kShowTopicNamesCount histories, by last message date.\n\tconst auto pred = [](not_null<ForumTopic*> a, not_null<ForumTopic*> b) {\n\t\tconst auto aItem = a->chatListMessage();\n\t\tconst auto bItem = b->chatListMessage();\n\t\tconst auto aDate = aItem ? aItem->date() : TimeId(0);\n\t\tconst auto bDate = bItem ? bItem->date() : TimeId(0);\n\t\treturn aDate > bDate;\n\t};\n\t_lastTopics.clear();\n\t_lastTopics.reserve(kShowTopicNamesCount + 1);\n\tauto &&topics = ranges::views::all(\n\t\t*_topicsList.indexed()\n\t) | ranges::views::transform([](not_null<Dialogs::Row*> row) {\n\t\treturn row->topic();\n\t});\n\tauto nonPinnedChecked = 0;\n\tfor (const auto topic : topics) {\n\t\tconst auto i = ranges::upper_bound(\n\t\t\t_lastTopics,\n\t\t\tnot_null(topic),\n\t\t\tpred);\n\t\tif (size(_lastTopics) < kShowTopicNamesCount\n\t\t\t|| i != end(_lastTopics)) {\n\t\t\t_lastTopics.insert(i, topic);\n\t\t}\n\t\tif (size(_lastTopics) > kShowTopicNamesCount) {\n\t\t\t_lastTopics.pop_back();\n\t\t}\n\t\tif (!topic->isPinnedDialog(FilterId())\n\t\t\t&& ++nonPinnedChecked >= kShowTopicNamesCount) {\n\t\t\tbreak;\n\t\t}\n\t}\n\t++_lastTopicsVersion;\n\t_history->updateChatListEntry();\n}\n\nint Forum::recentTopicsListVersion() const {\n\treturn _lastTopicsVersion;\n}\n\nvoid Forum::recentTopicsInvalidate(not_null<ForumTopic*> topic) {\n\tif (ranges::contains(_lastTopics, topic)) {\n\t\t++_lastTopicsVersion;\n\t\t_history->updateChatListEntry();\n\t}\n}\n\nconst std::vector<not_null<ForumTopic*>> &Forum::recentTopics() const {\n\treturn _lastTopics;\n}\n\nvoid Forum::saveActiveSubsectionThread(not_null<Thread*> thread) {\n\tif (const auto topic = thread->asTopic()) {\n\t\tAssert(topic->forum() == this);\n\t\t_activeSubsectionTopic = topic->creating() ? nullptr : topic;\n\t} else {\n\t\tAssert(thread == history());\n\t\t_activeSubsectionTopic = nullptr;\n\t}\n}\n\nThread *Forum::activeSubsectionThread() const {\n\treturn _activeSubsectionTopic;\n}\n\nvoid Forum::markUnreadCountsUnknown(MsgId readTillId) {\n\tif (!peer()->useSubsectionTabs()) {\n\t\treturn;\n\t}\n\tfor (const auto &[rootId, topic] : _topics) {\n\t\tconst auto replies = topic->replies();\n\t\tif (replies->unreadCountCurrent() > 0) {\n\t\t\treplies->setInboxReadTill(readTillId, std::nullopt);\n\t\t}\n\t}\n}\n\nvoid Forum::updateUnreadCounts(\n\t\tMsgId readTillId,\n\t\tconst base::flat_map<not_null<ForumTopic*>, int> &counts) {\n\tif (!peer()->useSubsectionTabs()) {\n\t\treturn;\n\t}\n\tfor (const auto &[rootId, topic] : _topics) {\n\t\tconst auto raw = topic.get();\n\t\tconst auto replies = raw->replies();\n\t\tconst auto i = counts.find(raw);\n\t\tconst auto count = (i != end(counts)) ? i->second : 0;\n\t\treplies->setInboxReadTill(readTillId, count);\n\t}\n}\n\nvoid Forum::listMessageChanged(HistoryItem *from, HistoryItem *to) {\n\tif (from || to) {\n\t\treorderLastTopics();\n\t}\n}\n\nvoid Forum::applyReceivedTopics(\n\t\tconst MTPmessages_ForumTopics &topics,\n\t\tForumOffsets &updateOffsets) {\n\tapplyReceivedTopics(topics, [&](not_null<ForumTopic*> topic) {\n\t\tif (const auto last = topic->lastServerMessage()) {\n\t\t\tupdateOffsets.date = last->date();\n\t\t\tupdateOffsets.id = last->id;\n\t\t}\n\t\tupdateOffsets.topicId = topic->rootId();\n\t});\n}\n\nvoid Forum::applyReceivedTopics(\n\t\tconst MTPmessages_ForumTopics &topics,\n\t\tFn<void(not_null<ForumTopic*>)> callback) {\n\tconst auto &data = topics.data();\n\towner().processUsers(data.vusers());\n\towner().processChats(data.vchats());\n\towner().processMessages(data.vmessages(), NewMessageType::Existing);\n\tif (const auto channel = this->channel()) {\n\t\tchannel->ptsReceived(data.vpts().v);\n\t}\n\tapplyReceivedTopics(data.vtopics(), std::move(callback));\n\tif (!_staleRootIds.empty()) {\n\t\trequestSomeStale();\n\t}\n}\n\nvoid Forum::applyReceivedTopics(\n\t\tconst MTPVector<MTPForumTopic> &topics,\n\t\tFn<void(not_null<ForumTopic*>)> callback) {\n\tconst auto &list = topics.v;\n\tfor (const auto &topic : list) {\n\t\tconst auto rootId = topic.match([&](const auto &data) {\n\t\t\treturn data.vid().v;\n\t\t});\n\t\tconst auto apply = [&](const MTPDforumTopic *fields = nullptr) {\n\t\t\t_topicsDeleted.remove(rootId);\n\t\t\tconst auto i = _topics.find(rootId);\n\t\t\tconst auto creating = (i == end(_topics));\n\t\t\tconst auto raw = creating\n\t\t\t\t? _topics.emplace(\n\t\t\t\t\trootId,\n\t\t\t\t\tstd::make_unique<ForumTopic>(this, rootId)\n\t\t\t\t).first->second.get()\n\t\t\t\t: i->second.get();\n\t\t\tif (fields) {\n\t\t\t\traw->applyTopic(*fields);\n\t\t\t}\n\t\t\tif (creating) {\n\t\t\t\tif (const auto last = _history->chatListMessage()\n\t\t\t\t\t; last && last->topicRootId() == rootId) {\n\t\t\t\t\t_history->lastItemDialogsView().itemInvalidated(last);\n\t\t\t\t\t_history->updateChatListEntry();\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (callback) {\n\t\t\t\tcallback(raw);\n\t\t\t}\n\t\t};\n\n\t\t_staleRootIds.remove(rootId);\n\t\ttopic.match([&](const MTPDforumTopicDeleted &data) {\n\t\t\tif (rootId != ForumTopic::kGeneralId) {\n\t\t\t\tapplyTopicDeleted(rootId);\n\t\t\t} else {\n\t\t\t\t// We shouldn't delete general topic in any case.\n\t\t\t\t// Here this happens in bot forums, for example.\n\t\t\t\tapply();\n\t\t\t}\n\t\t}, [&](const MTPDforumTopic &data) {\n\t\t\tapply(&data);\n\t\t});\n\t}\n}\n\nvoid Forum::requestSomeStale() {\n\tif (_staleRequestId\n\t\t|| (!_offset.id && _requestId)\n\t\t|| _staleRootIds.empty()) {\n\t\treturn;\n\t}\n\tconst auto type = Histories::RequestType::History;\n\tauto rootIds = QVector<MTPint>();\n\trootIds.reserve(std::min(int(_staleRootIds.size()), kStalePerRequest));\n\tfor (auto i = begin(_staleRootIds); i != end(_staleRootIds);) {\n\t\tconst auto rootId = *i;\n\t\ti = _staleRootIds.erase(i);\n\n\t\trootIds.push_back(MTP_int(rootId));\n\t\tif (rootIds.size() == kStalePerRequest) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (rootIds.empty()) {\n\t\treturn;\n\t}\n\tconst auto call = [=] {\n\t\tfor (const auto &id : rootIds) {\n\t\t\tfinishTopicRequest(id.v);\n\t\t}\n\t};\n\tauto &histories = owner().histories();\n\t_staleRequestId = histories.sendRequest(_history, type, [=](\n\t\t\tFn<void()> finish) {\n\t\treturn session().api().request(\n\t\t\tMTPmessages_GetForumTopicsByID(\n\t\t\t\tpeer()->input(),\n\t\t\t\tMTP_vector<MTPint>(rootIds))\n\t\t).done([=](const MTPmessages_ForumTopics &result) {\n\t\t\t_staleRequestId = 0;\n\t\t\tapplyReceivedTopics(result);\n\t\t\tcall();\n\t\t\tfinish();\n\t\t}).fail([=] {\n\t\t\t_staleRequestId = 0;\n\t\t\tcall();\n\t\t\tfinish();\n\t\t}).send();\n\t});\n\tfor (const auto &id : rootIds) {\n\t\t_topicRequests[id.v].id = _staleRequestId;\n\t}\n}\n\nvoid Forum::finishTopicRequest(MsgId rootId) {\n\tif (const auto request = _topicRequests.take(rootId)) {\n\t\tfor (const auto &callback : request->callbacks) {\n\t\t\tcallback();\n\t\t}\n\t}\n}\n\nvoid Forum::requestTopic(MsgId rootId, Fn<void()> done) {\n\tauto &request = _topicRequests[rootId];\n\tif (done) {\n\t\trequest.callbacks.push_back(std::move(done));\n\t}\n\tif (!request.id\n\t\t&& _staleRootIds.emplace(rootId).second\n\t\t&& (_staleRootIds.size() == 1)) {\n\t\tcrl::on_main(&session(), [peer = peer()] {\n\t\t\tif (const auto forum = peer->forum()) {\n\t\t\t\tforum->requestSomeStale();\n\t\t\t}\n\t\t});\n\t}\n}\n\nForumTopic *Forum::applyTopicAdded(\n\t\tMsgId rootId,\n\t\tconst QString &title,\n\t\tint32 colorId,\n\t\tDocumentId iconId,\n\t\tPeerId creatorId,\n\t\tTimeId date,\n\t\tbool my) {\n\tExpects(rootId != 0);\n\n\tconst auto i = _topics.find(rootId);\n\tconst auto raw = (i != end(_topics))\n\t\t? i->second.get()\n\t\t: _topics.emplace(\n\t\t\trootId,\n\t\t\tstd::make_unique<ForumTopic>(this, rootId)\n\t\t).first->second.get();\n\traw->applyTitle(title);\n\traw->applyColorId(colorId);\n\traw->applyIconId(iconId);\n\traw->applyCreator(creatorId);\n\traw->applyCreationDate(date);\n\traw->applyIsMy(my);\n\tif (!creating(rootId)) {\n\t\traw->addToChatList(FilterId(), topicsList());\n\t\t_chatsListChanges.fire({});\n\t\treorderLastTopics();\n\t}\n\treturn raw;\n}\n\nMsgId Forum::reserveCreatingId(\n\t\tconst QString &title,\n\t\tint32 colorId,\n\t\tDocumentId iconId) {\n\tconst auto result = owner().nextLocalMessageId();\n\t_creatingRootIds.emplace(result);\n\tapplyTopicAdded(\n\t\tresult,\n\t\ttitle,\n\t\tcolorId,\n\t\ticonId,\n\t\tsession().userPeerId(),\n\t\tbase::unixtime::now(),\n\t\ttrue);\n\treturn result;\n}\n\nForumTopic *Forum::reserveNewBotTopic() {\n\tconst auto &colors = ForumTopicColorIds();\n\tconst auto colorId = colors[base::RandomIndex(colors.size())];\n\treturn topicFor(reserveCreatingId(\n\t\ttr::lng_bot_new_chat(tr::now),\n\t\tcolorId,\n\t\tDocumentId()));\n}\n\nvoid Forum::discardCreatingId(MsgId rootId) {\n\tExpects(creating(rootId));\n\n\tconst auto i = _topics.find(rootId);\n\tif (i != end(_topics)) {\n\t\tAssert(!i->second->inChatList());\n\t\t_topics.erase(i);\n\t}\n\t_creatingRootIds.remove(rootId);\n}\n\nbool Forum::creating(MsgId rootId) const {\n\treturn _creatingRootIds.contains(rootId);\n}\n\nvoid Forum::created(MsgId rootId, MsgId realId) {\n\tif (rootId == realId) {\n\t\treturn;\n\t}\n\t_creatingRootIds.remove(rootId);\n\tconst auto i = _topics.find(rootId);\n\tAssert(i != end(_topics));\n\tauto topic = std::move(i->second);\n\t_topics.erase(i);\n\tconst auto id = FullMsgId(_history->peer->id, realId);\n\tif (!_topics.contains(realId)) {\n\t\t_topics.emplace(\n\t\t\trealId,\n\t\t\tstd::move(topic)\n\t\t).first->second->setRealRootId(realId);\n\n\t\treorderLastTopics();\n\t}\n\towner().notifyItemIdChange({ id, rootId });\n}\n\nvoid Forum::clearAllUnreadMentions() {\n\tfor (const auto &[rootId, topic] : _topics) {\n\t\ttopic->unreadMentions().clear();\n\t}\n}\n\nvoid Forum::clearAllUnreadReactions() {\n\tfor (const auto &[rootId, topic] : _topics) {\n\t\ttopic->unreadReactions().clear();\n\t}\n}\n\nvoid Forum::clearAllUnreadPollVotes() {\n\tfor (const auto &[rootId, topic] : _topics) {\n\t\ttopic->unreadPollVotes().clear();\n\t}\n}\n\nvoid Forum::enumerateTopics(Fn<void(not_null<ForumTopic*>)> action) const {\n\tfor (const auto &[rootId, topic] : _topics) {\n\t\taction(topic.get());\n\t}\n}\n\nForumTopic *Forum::topicFor(MsgId rootId) {\n\tif (!rootId) {\n\t\treturn nullptr;\n\t}\n\tconst auto i = _topics.find(rootId);\n\treturn (i != end(_topics)) ? i->second.get() : nullptr;\n}\n\nForumTopic *Forum::enforceTopicFor(MsgId rootId) {\n\tExpects(rootId != 0);\n\n\tconst auto i = _topics.find(rootId);\n\tif (i != end(_topics)) {\n\t\treturn i->second.get();\n\t}\n\trequestTopic(rootId);\n\treturn applyTopicAdded(rootId, {}, {}, {}, {}, {}, {});\n}\n\nbool Forum::topicDeleted(MsgId rootId) const {\n\treturn _topicsDeleted.contains(rootId)\n\t\t|| (rootId == ForumTopic::kGeneralId && peer()->isBot());\n}\n\nrpl::producer<> Forum::chatsListChanges() const {\n\treturn _chatsListChanges.events();\n}\n\nrpl::producer<> Forum::chatsListLoadedEvents() const {\n\treturn _chatsListLoadedEvents.events();\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_forum.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"dialogs/dialogs_main_list.h\"\n\nclass History;\nclass ChannelData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window;\n\nnamespace Data {\n\nclass Session;\n\nstruct ForumOffsets {\n\tTimeId date = 0;\n\tMsgId id = 0;\n\tMsgId topicId = 0;\n\n\tfriend inline constexpr auto operator<=>(\n\t\tForumOffsets,\n\t\tForumOffsets) = default;\n};\n\nclass Forum final {\npublic:\n\texplicit Forum(not_null<History*> history);\n\t~Forum();\n\n\t[[nodiscard]] Session &owner() const;\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\t[[nodiscard]] not_null<History*> history() const;\n\t[[nodiscard]] UserData *bot() const;\n\t[[nodiscard]] ChannelData *channel() const;\n\t[[nodiscard]] not_null<Dialogs::MainList*> topicsList();\n\t[[nodiscard]] rpl::producer<> destroyed() const;\n\t[[nodiscard]] auto topicDestroyed() const\n\t\t-> rpl::producer<not_null<ForumTopic*>>;\n\n\tvoid preloadTopics();\n\tvoid reloadTopics();\n\tvoid requestTopics();\n\t[[nodiscard]] rpl::producer<> chatsListChanges() const;\n\t[[nodiscard]] rpl::producer<> chatsListLoadedEvents() const;\n\n\tvoid requestTopic(MsgId rootId, Fn<void()> done = nullptr);\n\tForumTopic *applyTopicAdded(\n\t\tMsgId rootId,\n\t\tconst QString &title,\n\t\tint32 colorId,\n\t\tDocumentId iconId,\n\t\tPeerId creatorId,\n\t\tTimeId date,\n\t\tbool my);\n\tvoid applyTopicDeleted(MsgId rootId);\n\t[[nodiscard]] ForumTopic *topicFor(MsgId rootId);\n\t[[nodiscard]] ForumTopic *enforceTopicFor(MsgId rootId);\n\t[[nodiscard]] bool topicDeleted(MsgId rootId) const;\n\n\tvoid applyReceivedTopics(\n\t\tconst MTPmessages_ForumTopics &topics,\n\t\tForumOffsets &updateOffsets);\n\tvoid applyReceivedTopics(\n\t\tconst MTPmessages_ForumTopics &topics,\n\t\tFn<void(not_null<ForumTopic*>)> callback = nullptr);\n\tvoid applyReceivedTopics(\n\t\tconst MTPVector<MTPForumTopic> &topics,\n\t\tFn<void(not_null<ForumTopic*>)> callback = nullptr);\n\n\t[[nodiscard]] MsgId reserveCreatingId(\n\t\tconst QString &title,\n\t\tint32 colorId,\n\t\tDocumentId iconId);\n\tvoid discardCreatingId(MsgId rootId);\n\t[[nodiscard]] bool creating(MsgId rootId) const;\n\tvoid created(MsgId rootId, MsgId realId);\n\t[[nodiscard]] ForumTopic *reserveNewBotTopic();\n\n\tvoid clearAllUnreadMentions();\n\tvoid clearAllUnreadReactions();\n\tvoid clearAllUnreadPollVotes();\n\tvoid enumerateTopics(Fn<void(not_null<ForumTopic*>)> action) const;\n\n\tvoid listMessageChanged(HistoryItem *from, HistoryItem *to);\n\t[[nodiscard]] int recentTopicsListVersion() const;\n\tvoid recentTopicsInvalidate(not_null<ForumTopic*> topic);\n\t[[nodiscard]] auto recentTopics() const\n\t\t-> const std::vector<not_null<ForumTopic*>> &;\n\n\tvoid saveActiveSubsectionThread(not_null<Thread*> thread);\n\t[[nodiscard]] Thread *activeSubsectionThread() const;\n\n\tvoid markUnreadCountsUnknown(MsgId readTillId);\n\tvoid updateUnreadCounts(\n\t\tMsgId readTillId,\n\t\tconst base::flat_map<not_null<ForumTopic*>, int> &counts);\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tstruct TopicRequest {\n\t\tmtpRequestId id = 0;\n\t\tstd::vector<Fn<void()>> callbacks;\n\t};\n\n\tvoid reorderLastTopics();\n\tvoid requestSomeStale();\n\tvoid finishTopicRequest(MsgId rootId);\n\n\tconst not_null<History*> _history;\n\n\tbase::flat_map<MsgId, std::unique_ptr<ForumTopic>> _topics;\n\tbase::flat_set<MsgId> _topicsDeleted;\n\trpl::event_stream<not_null<ForumTopic*>> _topicDestroyed;\n\tDialogs::MainList _topicsList;\n\n\tbase::flat_map<MsgId, TopicRequest> _topicRequests;\n\tbase::flat_set<MsgId> _staleRootIds;\n\tmtpRequestId _staleRequestId = 0;\n\n\tmtpRequestId _requestId = 0;\n\tForumOffsets _offset;\n\n\tbase::flat_set<MsgId> _creatingRootIds;\n\n\tstd::vector<not_null<ForumTopic*>> _lastTopics;\n\tint _lastTopicsVersion = 0;\n\n\tForumTopic *_activeSubsectionTopic = nullptr;\n\n\trpl::event_stream<> _chatsListChanges;\n\trpl::event_stream<> _chatsListLoadedEvents;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_forum_icons.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_forum_icons.h\"\n\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"apiwrap.h\"\n\nnamespace Data {\n\nForumIcons::ForumIcons(not_null<Session*> owner)\n: _owner(owner)\n, _resetUserpicsTimer([=] { resetUserpics(); }) {\n}\n\nForumIcons::~ForumIcons() = default;\n\nMain::Session &ForumIcons::session() const {\n\treturn _owner->session();\n}\n\nvoid ForumIcons::requestDefaultIfUnknown() {\n\tif (_default.empty()) {\n\t\trequestDefault();\n\t}\n}\n\nvoid ForumIcons::refreshDefault() {\n\trequestDefault();\n}\n\nconst std::vector<DocumentId> &ForumIcons::list() const {\n\treturn _default;\n}\n\nrpl::producer<> ForumIcons::defaultUpdates() const {\n\treturn _defaultUpdated.events();\n}\n\nvoid ForumIcons::requestDefault() {\n\tif (_defaultRequestId) {\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\t_defaultRequestId = api.request(MTPmessages_GetStickerSet(\n\t\tMTP_inputStickerSetEmojiDefaultTopicIcons(),\n\t\tMTP_int(0) // hash\n\t)).done([=](const MTPmessages_StickerSet &result) {\n\t\t_defaultRequestId = 0;\n\t\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\tupdateDefault(data);\n\t\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t\t});\n\t}).fail([=] {\n\t\t_defaultRequestId = 0;\n\t}).send();\n}\n\nvoid ForumIcons::updateDefault(const MTPDmessages_stickerSet &data) {\n\tconst auto &list = data.vdocuments().v;\n\t_default.clear();\n\t_default.reserve(list.size());\n\tfor (const auto &sticker : list) {\n\t\t_default.push_back(_owner->processDocument(sticker)->id);\n\t}\n\t_defaultUpdated.fire({});\n}\n\nvoid ForumIcons::scheduleUserpicsReset(not_null<Forum*> forum) {\n\tconst auto duration = crl::time(st::slideDuration);\n\t_resetUserpicsWhen[forum] = crl::now() + duration;\n\tif (!_resetUserpicsTimer.isActive()) {\n\t\t_resetUserpicsTimer.callOnce(duration);\n\t}\n}\n\nvoid ForumIcons::clearUserpicsReset(not_null<Forum*> forum) {\n\t_resetUserpicsWhen.remove(forum);\n}\n\nvoid ForumIcons::resetUserpics() {\n\tauto nearest = crl::time();\n\tauto now = crl::now();\n\tfor (auto i = begin(_resetUserpicsWhen); i != end(_resetUserpicsWhen);) {\n\t\tif (i->second > now) {\n\t\t\tif (!nearest || nearest > i->second) {\n\t\t\t\tnearest = i->second;\n\t\t\t}\n\t\t\t++i;\n\t\t} else {\n\t\t\tconst auto forum = i->first;\n\t\t\ti = _resetUserpicsWhen.erase(i);\n\t\t\tresetUserpicsFor(forum);\n\t\t}\n\t}\n\tif (nearest) {\n\t\t_resetUserpicsTimer.callOnce(\n\t\t\tstd::min(nearest - now, 86400 * crl::time(1000)));\n\t} else {\n\t\t_resetUserpicsTimer.cancel();\n\t}\n}\n\nvoid ForumIcons::resetUserpicsFor(not_null<Forum*> forum) {\n\tforum->enumerateTopics([](not_null<ForumTopic*> topic) {\n\t\ttopic->clearUserpicLoops();\n\t});\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_forum_icons.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nclass DocumentMedia;\nclass Session;\nclass Forum;\n\nclass ForumIcons final {\npublic:\n\texplicit ForumIcons(not_null<Session*> owner);\n\t~ForumIcons();\n\n\t[[nodiscard]] Session &owner() const {\n\t\treturn *_owner;\n\t}\n\t[[nodiscard]] Main::Session &session() const;\n\n\tvoid refreshDefault();\n\tvoid requestDefaultIfUnknown();\n\n\t[[nodiscard]] const std::vector<DocumentId> &list() const;\n\n\t[[nodiscard]] rpl::producer<> defaultUpdates() const;\n\n\tvoid scheduleUserpicsReset(not_null<Forum*> forum);\n\tvoid clearUserpicsReset(not_null<Forum*> forum);\n\nprivate:\n\tvoid requestDefault();\n\tvoid resetUserpics();\n\tvoid resetUserpicsFor(not_null<Forum*> forum);\n\n\tvoid updateDefault(const MTPDmessages_stickerSet &data);\n\n\tconst not_null<Session*> _owner;\n\n\tstd::vector<DocumentId> _default;\n\trpl::event_stream<> _defaultUpdated;\n\n\tmtpRequestId _defaultRequestId = 0;\n\n\tbase::flat_map<not_null<Forum*>, crl::time> _resetUserpicsWhen;\n\tbase::Timer _resetUserpicsTimer;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_forum_topic.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_forum_topic.h\"\n\n#include \"data/data_channel.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_replies_list.h\"\n#include \"data/data_send_action.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"dialogs/dialogs_main_list.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"apiwrap.h\"\n#include \"api/api_unread_things.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_unread_things.h\"\n#include \"history/view/history_view_item_preview.h\"\n#include \"history/view/history_view_chat_section.h\"\n#include \"main/main_session.h\"\n#include \"base/unixtime.h\"\n#include \"ui/painter.h\"\n#include \"ui/color_int_conversion.h\"\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_chat_helpers.h\"\n\n#include <QtSvg/QSvgRenderer>\n\nnamespace Data {\nnamespace {\n\nusing UpdateFlag = TopicUpdate::Flag;\n\nconstexpr auto kUserpicLoopsCount = 1;\n\n} // namespace\n\nconst base::flat_map<int32, QString> &ForumTopicIcons() {\n\tstatic const auto Result = base::flat_map<int32, QString>{\n\t\t{ 0x6FB9F0, u\"blue\"_q },\n\t\t{ 0xFFD67E, u\"yellow\"_q },\n\t\t{ 0xCB86DB, u\"violet\"_q },\n\t\t{ 0x8EEE98, u\"green\"_q },\n\t\t{ 0xFF93B2, u\"rose\"_q },\n\t\t{ 0xFB6F5F, u\"red\"_q },\n\t};\n\treturn Result;\n}\n\nconst std::vector<int32> &ForumTopicColorIds() {\n\tstatic const auto Result = ForumTopicIcons(\n\t) | ranges::views::transform([](const auto &pair) {\n\t\treturn pair.first;\n\t}) | ranges::to_vector;\n\treturn Result;\n}\n\nconst QString &ForumTopicDefaultIcon() {\n\tstatic const auto Result = u\"gray\"_q;\n\treturn Result;\n}\n\nconst QString &ForumTopicIcon(int32 colorId) {\n\tconst auto &icons = ForumTopicIcons();\n\tconst auto i = icons.find(colorId);\n\treturn (i != end(icons)) ? i->second : ForumTopicDefaultIcon();\n}\n\nQString ForumTopicIconPath(const QString &name) {\n\treturn u\":/gui/topic_icons/%1.svg\"_q.arg(name);\n}\n\nQImage ForumTopicIconBackground(int32 colorId, int size) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto svg = QSvgRenderer(ForumTopicIconPath(ForumTopicIcon(colorId)));\n\tauto result = QImage(\n\t\tQSize(size, size) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\tresult.fill(Qt::transparent);\n\n\tauto p = QPainter(&result);\n\tsvg.render(&p, QRect(0, 0, size, size));\n\tp.end();\n\n\treturn result;\n}\n\nQString ExtractNonEmojiLetter(const QString &title) {\n\tconst auto begin = title.data();\n\tconst auto end = begin + title.size();\n\tfor (auto ch = begin; ch != end;) {\n\t\tauto length = 0;\n\t\tif (Ui::Emoji::Find(ch, end, &length)) {\n\t\t\tch += length;\n\t\t\tcontinue;\n\t\t}\n\t\tuint ucs4 = ch->unicode();\n\t\tlength = 1;\n\t\tif (QChar::isHighSurrogate(ucs4) && ch + 1 != end) {\n\t\t\tushort low = ch[1].unicode();\n\t\t\tif (QChar::isLowSurrogate(low)) {\n\t\t\t\tucs4 = QChar::surrogateToUcs4(ucs4, low);\n\t\t\t\tlength = 2;\n\t\t\t}\n\t\t}\n\t\tif (!QChar::isLetterOrNumber(ucs4)) {\n\t\t\tch += length;\n\t\t\tcontinue;\n\t\t}\n\t\treturn QString(ch, length);\n\t}\n\treturn QString();\n}\n\nQImage ForumTopicIconFrame(\n\t\tint32 colorId,\n\t\tconst QString &title,\n\t\tconst style::ForumTopicIcon &st) {\n\tauto background = ForumTopicIconBackground(colorId, st.size);\n\n\tif (const auto one = ExtractNonEmojiLetter(title); !one.isEmpty()) {\n\t\tauto p = QPainter(&background);\n\t\tp.setPen(Qt::white);\n\t\tp.setFont(st.font);\n\t\tp.drawText(\n\t\t\tQRect(0, st.textTop, st.size, st.font->height * 2),\n\t\t\tone,\n\t\t\tstyle::al_top);\n\t}\n\n\treturn background;\n}\n\nQImage ForumTopicGeneralIconFrame(int size, const QColor &color) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto svg = QSvgRenderer(ForumTopicIconPath(u\"general\"_q));\n\tauto result = QImage(\n\t\tQSize(size, size) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\tresult.fill(Qt::transparent);\n\n\tconst auto use = size * 1.;\n\tconst auto skip = size * 0.;\n\tauto p = QPainter(&result);\n\tsvg.render(&p, QRectF(skip, skip, use, use));\n\tp.end();\n\n\treturn style::colorizeImage(result, color);\n}\n\nTextWithEntities ForumTopicIconWithTitle(\n\t\tMsgId rootId,\n\t\tDocumentId iconId,\n\t\tconst QString &title) {\n\tconst auto wrapped = st::wrap_rtl(title);\n\treturn (rootId == ForumTopic::kGeneralId)\n\t\t? TextWithEntities{ u\"# \"_q + wrapped }\n\t\t: iconId\n\t\t? Data::SingleCustomEmoji(iconId).append(' ').append(wrapped)\n\t\t: TextWithEntities{ wrapped };\n}\n\nQString ForumGeneralIconTitle() {\n\treturn QChar(0) + u\"general\"_q;\n}\n\nbool IsForumGeneralIconTitle(const QString &title) {\n\treturn !title.isEmpty() && !title[0].unicode();\n}\n\nint32 ForumGeneralIconColor(const QColor &color) {\n\treturn int32(uint32(color.red()) << 16\n\t\t| uint32(color.green()) << 8\n\t\t| uint32(color.blue())\n\t\t| (uint32(color.alpha() == 255 ? 0 : color.alpha()) << 24));\n}\n\nQColor ParseForumGeneralIconColor(int32 value) {\n\tconst auto alpha = uint32(value) >> 24;\n\treturn QColor(\n\t\t(value >> 16) & 0xFF,\n\t\t(value >> 8) & 0xFF,\n\t\tvalue & 0xFF,\n\t\talpha ? alpha : 255);\n}\n\nQString TopicIconEmojiEntity(TopicIconDescriptor descriptor) {\n\treturn IsForumGeneralIconTitle(descriptor.title)\n\t\t? u\"topic_general:\"_q + QString::number(uint32(descriptor.colorId))\n\t\t: (u\"topic_icon:\"_q\n\t\t\t+ QString::number(uint32(descriptor.colorId))\n\t\t\t+ ' '\n\t\t\t+ ExtractNonEmojiLetter(descriptor.title));\n}\n\nTopicIconDescriptor ParseTopicIconEmojiEntity(QStringView entity) {\n\tif (!entity.startsWith(u\"topic_\")) {\n\t\treturn {};\n\t}\n\tconst auto general = u\"topic_general:\"_q;\n\tconst auto normal = u\"topic_icon:\"_q;\n\tif (entity.startsWith(general)) {\n\t\treturn {\n\t\t\t.title = ForumGeneralIconTitle(),\n\t\t\t.colorId = int32(entity.mid(general.size()).toUInt()),\n\t\t};\n\t} else if (entity.startsWith(normal)) {\n\t\tconst auto parts = entity.mid(normal.size()).split(' ');\n\t\tif (parts.size() == 2) {\n\t\t\treturn {\n\t\t\t\t.title = parts[1].isEmpty() ? u\" \"_q : parts[1].toString(),\n\t\t\t\t.colorId = int32(parts[0].toUInt()),\n\t\t\t};\n\t\t}\n\t}\n\treturn {};\n}\n\nForumTopic::ForumTopic(not_null<Forum*> forum, MsgId rootId)\n: Thread(&forum->history()->owner(), Type::ForumTopic)\n, _forum(forum)\n, _list(_forum->topicsList())\n, _replies(std::make_shared<RepliesList>(history(), rootId, this))\n, _sendActionPainter(owner().sendActionManager().repliesPainter(\n\thistory(),\n\trootId))\n, _rootId(rootId)\n, _lastKnownServerMessageId(rootId)\n, _creatorId(creating() ? forum->session().userPeerId() : 0)\n, _creationDate(creating() ? base::unixtime::now() : 0)\n, _flags(creating() ? Flag::My : Flag()) {\n\tThread::setMuted(owner().notifySettings().isMuted(this));\n\n\t_sendActionPainter->setTopic(this);\n\tsubscribeToUnreadChanges();\n\n\tif (isGeneral()) {\n\t\tstyle::PaletteChanged(\n\t\t) | rpl::on_next([=] {\n\t\t\t_defaultIcon = QImage();\n\t\t}, _lifetime);\n\t}\n}\n\nForumTopic::~ForumTopic() {\n\t_sendActionPainter->setTopic(nullptr);\n\tsession().api().unreadThings().cancelRequests(this);\n}\n\nstd::shared_ptr<Data::RepliesList> ForumTopic::replies() const {\n\treturn _replies;\n}\n\nnot_null<PeerData*> ForumTopic::peer() const {\n\treturn _forum->peer();\n}\n\nUserData *ForumTopic::bot() const {\n\treturn _forum->bot();\n}\n\nChannelData *ForumTopic::channel() const {\n\treturn _forum->channel();\n}\n\nnot_null<History*> ForumTopic::history() const {\n\treturn _forum->history();\n}\n\nnot_null<Forum*> ForumTopic::forum() const {\n\treturn _forum;\n}\n\nrpl::producer<> ForumTopic::destroyed() const {\n\tusing namespace rpl::mappers;\n\treturn rpl::merge(\n\t\t_forum->destroyed(),\n\t\t_forum->topicDestroyed() | rpl::filter(_1 == this) | rpl::to_empty);\n}\n\nMsgId ForumTopic::rootId() const {\n\treturn _rootId;\n}\n\nPeerId ForumTopic::creatorId() const {\n\treturn _creatorId;\n}\n\nTimeId ForumTopic::creationDate() const {\n\treturn _creationDate;\n}\n\nnot_null<HistoryView::ListMemento*> ForumTopic::listMemento() {\n\tif (!_listMemento) {\n\t\t_listMemento = std::make_unique<HistoryView::ListMemento>();\n\t}\n\treturn _listMemento.get();\n}\n\nbool ForumTopic::my() const {\n\treturn (_flags & Flag::My);\n}\n\nbool ForumTopic::canEdit() const {\n\treturn my() || peer()->canManageTopics();\n}\n\nbool ForumTopic::canDelete() const {\n\tif (creating() || isGeneral()) {\n\t\treturn false;\n\t} else if (bot()) {\n\t\treturn true;\n\t} else if (const auto channel = this->channel()) {\n\t\tif (channel->canDeleteMessages()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn my() && replies()->canDeleteMyTopic();\n}\n\nbool ForumTopic::canToggleClosed() const {\n\treturn !creating() && canEdit() && !_forum->peer()->isBot();\n}\n\nbool ForumTopic::canTogglePinned() const {\n\treturn !creating() && peer()->canManageTopics();\n}\n\nbool ForumTopic::creating() const {\n\treturn _forum->creating(_rootId);\n}\n\nvoid ForumTopic::discard() {\n\tExpects(creating());\n\n\t_forum->discardCreatingId(_rootId);\n}\n\nvoid ForumTopic::setRealRootId(MsgId realId) {\n\tif (_rootId != realId) {\n\t\t_rootId = realId;\n\t\t_lastKnownServerMessageId = realId;\n\t\t_replies = std::make_shared<RepliesList>(history(), _rootId);\n\t\tif (_sendActionPainter) {\n\t\t\t_sendActionPainter->setTopic(nullptr);\n\t\t}\n\t\t_sendActionPainter = owner().sendActionManager().repliesPainter(\n\t\t\thistory(),\n\t\t\t_rootId);\n\t\t_sendActionPainter->setTopic(this);\n\t\tsubscribeToUnreadChanges();\n\t}\n}\n\nvoid ForumTopic::subscribeToUnreadChanges() {\n\t_replies->unreadCountValue(\n\t) | rpl::map([=](std::optional<int> value) {\n\t\treturn value ? _replies->displayedUnreadCount() : value;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::combine_previous(\n\t) | rpl::filter([=] {\n\t\treturn inChatList();\n\t}) | rpl::on_next([=](\n\t\t\tstd::optional<int> previous,\n\t\t\tstd::optional<int> now) {\n\t\tif (previous.value_or(0) != now.value_or(0)) {\n\t\t\t_forum->recentTopicsInvalidate(this);\n\t\t}\n\t\tnotifyUnreadStateChange(unreadStateFor(\n\t\t\tprevious.value_or(0),\n\t\t\tprevious.has_value()));\n\t}, _lifetime);\n}\n\nvoid ForumTopic::readTillEnd() {\n\t_replies->readTill(_lastKnownServerMessageId);\n}\n\nvoid ForumTopic::applyTopic(const MTPDforumTopic &data) {\n\tExpects(_rootId == data.vid().v);\n\n\tconst auto min = data.is_short();\n\n\tapplyCreator(peerFromMTP(data.vfrom_id()));\n\tapplyCreationDate(data.vdate().v);\n\n\tapplyTitle(qs(data.vtitle()));\n\tif (const auto iconId = data.vicon_emoji_id()) {\n\t\tapplyIconId(iconId->v);\n\t} else {\n\t\tapplyIconId(0);\n\t}\n\tapplyColorId(data.vicon_color().v);\n\n\tapplyIsMy(data.is_my());\n\tsetClosed(data.is_closed());\n\n\tif (!min) {\n\t\towner().setPinnedFromEntryList(this, data.is_pinned());\n\t\towner().notifySettings().apply(this, data.vnotify_settings());\n\n\t\tif (const auto draft = data.vdraft()) {\n\t\t\tdraft->match([&](const MTPDdraftMessage &data) {\n\t\t\t\tData::ApplyPeerCloudDraft(\n\t\t\t\t\t&session(),\n\t\t\t\t\tpeer()->id,\n\t\t\t\t\t_rootId,\n\t\t\t\t\tPeerId(),\n\t\t\t\t\tdata);\n\t\t\t}, [](const MTPDdraftMessageEmpty&) {});\n\t\t}\n\n\t\t_replies->setInboxReadTill(\n\t\t\tdata.vread_inbox_max_id().v,\n\t\t\tdata.vunread_count().v);\n\t\t_replies->setOutboxReadTill(data.vread_outbox_max_id().v);\n\t\tapplyTopicTopMessage(data.vtop_message().v);\n\t\tunreadMentions().setCount(data.vunread_mentions_count().v);\n\t\tunreadReactions().setCount(data.vunread_reactions_count().v);\n\t\tunreadPollVotes().setCount(data.vunread_poll_votes_count().v);\n\t}\n}\n\nvoid ForumTopic::applyCreator(PeerId creatorId) {\n\tif (_creatorId != creatorId) {\n\t\t_creatorId = creatorId;\n\t\tsession().changes().topicUpdated(this, UpdateFlag::Creator);\n\t}\n}\n\nvoid ForumTopic::applyCreationDate(TimeId date) {\n\t_creationDate = date;\n}\n\nvoid ForumTopic::applyIsMy(bool my) {\n\tif (my != this->my()) {\n\t\tif (my) {\n\t\t\t_flags |= Flag::My;\n\t\t} else {\n\t\t\t_flags &= ~Flag::My;\n\t\t}\n\t}\n}\n\nbool ForumTopic::closed() const {\n\treturn _flags & Flag::Closed;\n}\n\nvoid ForumTopic::setClosed(bool closed) {\n\tif (this->closed() == closed) {\n\t\treturn;\n\t} else if (closed) {\n\t\t_flags |= Flag::Closed;\n\t} else {\n\t\t_flags &= ~Flag::Closed;\n\t}\n\tsession().changes().topicUpdated(this, UpdateFlag::Closed);\n}\n\nvoid ForumTopic::setClosedAndSave(bool closed) {\n\tsetClosed(closed);\n\n\tconst auto api = &session().api();\n\tconst auto weak = base::make_weak(this);\n\tapi->request(MTPmessages_EditForumTopic(\n\t\tMTP_flags(MTPmessages_EditForumTopic::Flag::f_closed),\n\t\tpeer()->input(),\n\t\tMTP_int(_rootId),\n\t\tMTPstring(), // title\n\t\tMTPlong(), // icon_emoji_id\n\t\tMTP_bool(closed),\n\t\tMTPBool() // hiddenKO\n\t)).done([=](const MTPUpdates &result) {\n\t\tapi->applyUpdates(result);\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (error.type() != u\"TOPIC_NOT_MODIFIED\") {\n\t\t\tif (const auto topic = weak.get()) {\n\t\t\t\ttopic->forum()->requestTopic(topic->rootId());\n\t\t\t}\n\t\t}\n\t}).send();\n}\n\nbool ForumTopic::hidden() const {\n\treturn (_flags & Flag::Hidden);\n}\n\nvoid ForumTopic::setHidden(bool hidden) {\n\tif (hidden) {\n\t\t_flags |= Flag::Hidden;\n\t} else {\n\t\t_flags &= ~Flag::Hidden;\n\t}\n}\n\nvoid ForumTopic::indexTitleParts() {\n\t_titleWords.clear();\n\t_titleFirstLetters.clear();\n\tauto toIndexList = QStringList();\n\tauto appendToIndex = [&](const QString &value) {\n\t\tif (!value.isEmpty()) {\n\t\t\ttoIndexList.push_back(TextUtilities::RemoveAccents(value));\n\t\t}\n\t};\n\n\tappendToIndex(_title);\n\tconst auto appendTranslit = !toIndexList.isEmpty()\n\t\t&& cRussianLetters().match(toIndexList.front()).hasMatch();\n\tif (appendTranslit) {\n\t\tappendToIndex(translitRusEng(toIndexList.front()));\n\t}\n\tauto toIndex = toIndexList.join(' ');\n\ttoIndex += ' ' + rusKeyboardLayoutSwitch(toIndex);\n\n\tconst auto namesList = TextUtilities::PrepareSearchWords(toIndex);\n\tfor (const auto &name : namesList) {\n\t\t_titleWords.insert(name);\n\t\t_titleFirstLetters.insert(name[0]);\n\t}\n}\n\nint ForumTopic::chatListNameVersion() const {\n\treturn _titleVersion;\n}\n\nvoid ForumTopic::applyTopicTopMessage(MsgId topMessageId) {\n\tif (topMessageId) {\n\t\tgrowLastKnownServerMessageId(topMessageId);\n\t\tconst auto itemId = FullMsgId(peer()->id, topMessageId);\n\t\tif (const auto item = owner().message(itemId)) {\n\t\t\tsetLastServerMessage(item);\n\t\t\tresolveChatListMessageGroup();\n\t\t} else {\n\t\t\tsetLastServerMessage(nullptr);\n\t\t}\n\t} else {\n\t\tsetLastServerMessage(nullptr);\n\t}\n}\n\nvoid ForumTopic::resolveChatListMessageGroup() {\n\tif (!(_flags & Flag::ResolveChatListMessage)) {\n\t\treturn;\n\t}\n\t// If we set a single album part, request the full album.\n\tconst auto item = _lastServerMessage.value_or(nullptr);\n\tif (item && item->groupId() != MessageGroupId()) {\n\t\tif (owner().groups().isGroupOfOne(item)\n\t\t\t&& !item->toPreview({\n\t\t\t\t.hideSender = true,\n\t\t\t\t.hideCaption = true }).images.empty()\n\t\t\t\t&& _requestedGroups.emplace(item->fullId()).second) {\n\t\t\towner().histories().requestGroupAround(item);\n\t\t}\n\t}\n}\n\nvoid ForumTopic::growLastKnownServerMessageId(MsgId id) {\n\t_lastKnownServerMessageId = std::max(_lastKnownServerMessageId, id);\n}\n\nvoid ForumTopic::setLastServerMessage(HistoryItem *item) {\n\tif (item) {\n\t\tgrowLastKnownServerMessageId(item->id);\n\t}\n\t_lastServerMessage = item;\n\tif (_lastMessage\n\t\t&& *_lastMessage\n\t\t&& !(*_lastMessage)->isRegular()\n\t\t&& (!item\n\t\t\t|| (*_lastMessage)->date() > item->date()\n\t\t\t|| (*_lastMessage)->isSending())) {\n\t\treturn;\n\t}\n\tsetLastMessage(item);\n}\n\nvoid ForumTopic::setLastMessage(HistoryItem *item) {\n\tif (_lastMessage && *_lastMessage == item) {\n\t\treturn;\n\t}\n\t_lastMessage = item;\n\tif (!item || item->isRegular()) {\n\t\t_lastServerMessage = item;\n\t\tif (item) {\n\t\t\tgrowLastKnownServerMessageId(item->id);\n\t\t}\n\t}\n\tsetChatListMessage(item);\n}\n\nvoid ForumTopic::setChatListMessage(HistoryItem *item) {\n\tif (_chatListMessage && *_chatListMessage == item) {\n\t\treturn;\n\t}\n\tconst auto was = _chatListMessage.value_or(nullptr);\n\tif (item) {\n\t\tif (item->isSponsored()) {\n\t\t\treturn;\n\t\t}\n\t\tif (_chatListMessage\n\t\t\t&& *_chatListMessage\n\t\t\t&& !(*_chatListMessage)->isRegular()\n\t\t\t&& (*_chatListMessage)->date() > item->date()) {\n\t\t\treturn;\n\t\t}\n\t\t_chatListMessage = item;\n\t\tsetChatListTimeId(item->date());\n\t} else if (!_chatListMessage || *_chatListMessage) {\n\t\t_chatListMessage = nullptr;\n\t\tupdateChatListEntry();\n\t}\n\t_forum->listMessageChanged(was, item);\n}\n\nvoid ForumTopic::chatListPreloadData() {\n\tif (_icon) {\n\t\t[[maybe_unused]] const auto preload = _icon->ready();\n\t}\n\tallowChatListMessageResolve();\n}\n\nvoid ForumTopic::paintUserpic(\n\t\tPainter &p,\n\t\tUi::PeerUserpicView &view,\n\t\tconst Dialogs::Ui::PaintContext &context) const {\n\tconst auto &st = context.st;\n\tauto position = QPoint(st->padding.left(), st->padding.top());\n\tif (_icon) {\n\t\tif (context.narrow) {\n\t\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t\tconst auto tag = Data::CustomEmojiManager::SizeTag::Normal;\n\t\t\tconst auto size = Data::FrameSizeFromTag(tag) / ratio;\n\t\t\tposition = QPoint(\n\t\t\t\t(context.width - size) / 2,\n\t\t\t\t(st->height - size) / 2);\n\t\t}\n\t\t_icon->paint(p, {\n\t\t\t.textColor = (context.active\n\t\t\t\t? st::dialogsNameFgActive\n\t\t\t\t: context.selected\n\t\t\t\t? st::dialogsNameFgOver\n\t\t\t\t: st::dialogsNameFg)->c,\n\t\t\t.now = context.now,\n\t\t\t.position = position,\n\t\t\t.paused = context.paused,\n\t\t});\n\t} else {\n\t\tif (isGeneral()) {\n\t\t\tvalidateGeneralIcon(context);\n\t\t} else {\n\t\t\tvalidateDefaultIcon();\n\t\t}\n\t\tconst auto size = st::defaultForumTopicIcon.size;\n\t\tif (context.narrow) {\n\t\t\tposition = QPoint(\n\t\t\t\t(context.width - size) / 2,\n\t\t\t\t(st->height - size) / 2);\n\t\t} else {\n\t\t\tconst auto esize = st::emojiSize;\n\t\t\tconst auto shift = (esize - size) / 2;\n\t\t\tposition += st::forumTopicIconPosition + QPoint(shift, 0);\n\t\t}\n\t\tp.drawImage(position, _defaultIcon);\n\t}\n}\n\nvoid ForumTopic::clearUserpicLoops() {\n\tif (_icon) {\n\t\t_icon->unload();\n\t}\n}\n\nvoid ForumTopic::validateDefaultIcon() const {\n\tif (!_defaultIcon.isNull()) {\n\t\treturn;\n\t}\n\t_defaultIcon = ForumTopicIconFrame(\n\t\t_colorId,\n\t\t_title,\n\t\tst::defaultForumTopicIcon);\n}\n\nvoid ForumTopic::validateGeneralIcon(\n\t\tconst Dialogs::Ui::PaintContext &context) const {\n\tconst auto mask = Flag::GeneralIconActive | Flag::GeneralIconSelected;\n\tconst auto flags = context.active\n\t\t? Flag::GeneralIconActive\n\t\t: context.selected\n\t\t? Flag::GeneralIconSelected\n\t\t: Flag(0);\n\tif (!_defaultIcon.isNull() && ((_flags & mask) == flags)) {\n\t\treturn;\n\t}\n\tconst auto size = st::defaultForumTopicIcon.size;\n\tconst auto &color = context.active\n\t\t? st::dialogsTextFgActive\n\t\t: context.selected\n\t\t? st::dialogsTextFgOver\n\t\t: st::dialogsTextFg;\n\t_defaultIcon = ForumTopicGeneralIconFrame(size, color->c);\n\t_flags = (_flags & ~mask) | flags;\n}\n\nvoid ForumTopic::requestChatListMessage() {\n\tif (!chatListMessageKnown() && !forum()->creating(_rootId)) {\n\t\tforum()->requestTopic(_rootId);\n\t}\n}\n\nTimeId ForumTopic::adjustedChatListTimeId() const {\n\tconst auto result = chatListTimeId();\n\tif (const auto draft = history()->cloudDraft(_rootId, PeerId())) {\n\t\tif (!Data::DraftIsNull(draft) && !session().supportMode()) {\n\t\t\treturn std::max(result, draft->date);\n\t\t}\n\t}\n\treturn result;\n}\n\nint ForumTopic::fixedOnTopIndex() const {\n\treturn 0;\n}\n\nbool ForumTopic::shouldBeInChatList() const {\n\treturn isPinnedDialog(FilterId())\n\t\t|| !lastMessageKnown()\n\t\t|| (lastMessage() != nullptr);\n}\n\nHistoryItem *ForumTopic::lastMessage() const {\n\treturn _lastMessage.value_or(nullptr);\n}\n\nbool ForumTopic::lastMessageKnown() const {\n\treturn _lastMessage.has_value();\n}\n\nHistoryItem *ForumTopic::lastServerMessage() const {\n\treturn _lastServerMessage.value_or(nullptr);\n}\n\nbool ForumTopic::lastServerMessageKnown() const {\n\treturn _lastServerMessage.has_value();\n}\n\nMsgId ForumTopic::lastKnownServerMessageId() const {\n\treturn _lastKnownServerMessageId;\n}\n\nQString ForumTopic::title() const {\n\treturn _title;\n}\n\nTextWithEntities ForumTopic::titleWithIcon() const {\n\treturn ForumTopicIconWithTitle(_rootId, _iconId, _title);\n}\n\nTextWithEntities ForumTopic::titleWithIconOrLogo() const {\n\tif (_iconId || isGeneral()) {\n\t\treturn titleWithIcon();\n\t}\n\treturn Ui::Text::SingleCustomEmoji(Data::TopicIconEmojiEntity({\n\t\t.title = _title,\n\t\t.colorId = _colorId,\n\t})).append(' ').append(_title);\n}\n\nint ForumTopic::titleVersion() const {\n\treturn _titleVersion;\n}\n\nvoid ForumTopic::applyTitle(const QString &title) {\n\tif (_title == title) {\n\t\treturn;\n\t}\n\t_title = title;\n\tinvalidateTitleWithIcon();\n\t_defaultIcon = QImage();\n\tindexTitleParts();\n\tupdateChatListEntry();\n\tsession().changes().topicUpdated(this, UpdateFlag::Title);\n}\n\nDocumentId ForumTopic::iconId() const {\n\treturn _iconId;\n}\n\nvoid ForumTopic::applyIconId(DocumentId iconId) {\n\tif (_iconId == iconId) {\n\t\treturn;\n\t}\n\t_iconId = iconId;\n\tinvalidateTitleWithIcon();\n\t_icon = iconId\n\t\t? std::make_unique<Ui::Text::LimitedLoopsEmoji>(\n\t\t\towner().customEmojiManager().create(\n\t\t\t\t_iconId,\n\t\t\t\t[=] { updateChatListEntry(); },\n\t\t\t\tData::CustomEmojiManager::SizeTag::Normal),\n\t\t\tkUserpicLoopsCount)\n\t\t: nullptr;\n\tif (iconId) {\n\t\t_defaultIcon = QImage();\n\t}\n\tupdateChatListEntry();\n\tsession().changes().topicUpdated(this, UpdateFlag::IconId);\n}\n\nvoid ForumTopic::invalidateTitleWithIcon() {\n\t++_titleVersion;\n\t_forum->recentTopicsInvalidate(this);\n}\n\nint32 ForumTopic::colorId() const {\n\treturn _colorId;\n}\n\nvoid ForumTopic::applyColorId(int32 colorId) {\n\tif (_colorId != colorId) {\n\t\t_colorId = colorId;\n\t\tsession().changes().topicUpdated(this, UpdateFlag::ColorId);\n\t}\n}\n\nvoid ForumTopic::applyMaybeLast(not_null<HistoryItem*> item) {\n\tif (!_lastServerMessage.value_or(nullptr)\n\t\t|| (*_lastServerMessage)->id < item->id) {\n\t\tsetLastServerMessage(item);\n\t\tresolveChatListMessageGroup();\n\t} else {\n\t\tgrowLastKnownServerMessageId(item->id);\n\t}\n}\n\nvoid ForumTopic::applyItemAdded(not_null<HistoryItem*> item) {\n\tif (item->isRegular()) {\n\t\tsetLastServerMessage(item);\n\t} else {\n\t\tsetLastMessage(item);\n\t}\n}\n\nvoid ForumTopic::maybeSetLastMessage(not_null<HistoryItem*> item) {\n\tExpects(item->topicRootId() == _rootId);\n\n\tif (!_lastMessage\n\t\t|| !(*_lastMessage)\n\t\t|| ((*_lastMessage)->date() < item->date())\n\t\t|| ((*_lastMessage)->date() == item->date()\n\t\t\t&& (*_lastMessage)->id < item->id)) {\n\t\tsetLastMessage(item);\n\t}\n}\n\nvoid ForumTopic::applyItemRemoved(MsgId id) {\n\tif (const auto lastItem = lastMessage()) {\n\t\tif (lastItem->id == id) {\n\t\t\t_lastMessage = std::nullopt;\n\t\t}\n\t}\n\tif (const auto lastServerItem = lastServerMessage()) {\n\t\tif (lastServerItem->id == id) {\n\t\t\t_lastServerMessage = std::nullopt;\n\t\t}\n\t}\n\tif (const auto chatListItem = _chatListMessage.value_or(nullptr)) {\n\t\tif (chatListItem->id == id) {\n\t\t\t_chatListMessage = std::nullopt;\n\t\t\trequestChatListMessage();\n\t\t}\n\t}\n}\n\nbool ForumTopic::isServerSideUnread(\n\t\tnot_null<const HistoryItem*> item) const {\n\treturn _replies->isServerSideUnread(item);\n}\n\nvoid ForumTopic::setMuted(bool muted) {\n\tif (this->muted() == muted) {\n\t\treturn;\n\t}\n\tconst auto state = chatListBadgesState();\n\tconst auto notify = state.unread || state.reaction;\n\tconst auto notifier = unreadStateChangeNotifier(notify);\n\tThread::setMuted(muted);\n\tsession().changes().topicUpdated(this, UpdateFlag::Notifications);\n}\n\nHistoryView::SendActionPainter *ForumTopic::sendActionPainter() {\n\treturn _sendActionPainter.get();\n}\n\nDialogs::UnreadState ForumTopic::chatListUnreadState() const {\n\treturn unreadStateFor(\n\t\t_replies->displayedUnreadCount(),\n\t\t_replies->unreadCountKnown());\n}\n\nDialogs::BadgesState ForumTopic::chatListBadgesState() const {\n\tauto result = Dialogs::BadgesForUnread(\n\t\tchatListUnreadState(),\n\t\tDialogs::CountInBadge::Messages,\n\t\tDialogs::IncludeInBadge::All);\n\tif (!result.unread && _replies->inboxReadTillId() < 2) {\n\t\tresult.unread = (bot() || (channel() && channel()->amIn()))\n\t\t\t&& (_lastKnownServerMessageId > history()->inboxReadTillId());\n\t\tresult.unreadMuted = muted();\n\t}\n\treturn result;\n}\n\nDialogs::UnreadState ForumTopic::unreadStateFor(\n\t\tint count,\n\t\tbool known) const {\n\tauto result = Dialogs::UnreadState();\n\tconst auto muted = this->muted();\n\tresult.messages = count;\n\tresult.chats = count ? 1 : 0;\n\tresult.mentions = unreadMentions().has() ? 1 : 0;\n\tresult.reactions = unreadReactions().has() ? 1 : 0;\n\tresult.messagesMuted = muted ? result.messages : 0;\n\tresult.chatsMuted = muted ? result.chats : 0;\n\tresult.reactionsMuted = muted ? result.reactions : 0;\n\tresult.known = known;\n\treturn result;\n}\n\nvoid ForumTopic::allowChatListMessageResolve() {\n\tif (_flags & Flag::ResolveChatListMessage) {\n\t\treturn;\n\t}\n\t_flags |= Flag::ResolveChatListMessage;\n\tresolveChatListMessageGroup();\n}\n\nHistoryItem *ForumTopic::chatListMessage() const {\n\treturn _lastMessage.value_or(nullptr);\n}\n\nbool ForumTopic::chatListMessageKnown() const {\n\treturn _lastMessage.has_value();\n}\n\nconst QString &ForumTopic::chatListName() const {\n\treturn _title;\n}\n\nconst base::flat_set<QString> &ForumTopic::chatListNameWords() const {\n\treturn _titleWords;\n}\n\nconst base::flat_set<QChar> &ForumTopic::chatListFirstLetters() const {\n\treturn _titleFirstLetters;\n}\n\nvoid ForumTopic::hasUnreadMentionChanged(bool has) {\n\tauto was = chatListUnreadState();\n\tif (has) {\n\t\twas.mentions = 0;\n\t} else {\n\t\twas.mentions = 1;\n\t}\n\tnotifyUnreadStateChange(was);\n}\n\nvoid ForumTopic::hasUnreadReactionChanged(bool has) {\n\tauto was = chatListUnreadState();\n\tif (has) {\n\t\twas.reactions = was.reactionsMuted = 0;\n\t} else {\n\t\twas.reactions = 1;\n\t\twas.reactionsMuted = muted() ? was.reactions : 0;\n\t}\n\tnotifyUnreadStateChange(was);\n}\n\nvoid ForumTopic::hasUnreadPollVoteChanged(bool has) {\n}\n\nconst QString &ForumTopic::chatListNameSortKey() const {\n\tstatic const auto empty = QString();\n\treturn empty;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_forum_topic.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_thread.h\"\n#include \"data/notify/data_peer_notify_settings.h\"\n#include \"base/flags.h\"\n\nclass ChannelData;\nenum class ChatRestriction;\n\nnamespace style {\nstruct ForumTopicIcon;\n} // namespace style\n\nnamespace Dialogs {\nclass MainList;\n} // namespace Dialogs\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace HistoryView {\nclass SendActionPainter;\nclass ListMemento;\n} // namespace HistoryView\n\nnamespace Data {\n\nclass RepliesList;\nclass Session;\nclass Forum;\n\n[[nodiscard]] const base::flat_map<int32, QString> &ForumTopicIcons();\n[[nodiscard]] const std::vector<int32> &ForumTopicColorIds();\n[[nodiscard]] const QString &ForumTopicIcon(int32 colorId);\n[[nodiscard]] QString ForumTopicIconPath(const QString &name);\n[[nodiscard]] QImage ForumTopicIconBackground(int32 colorId, int size);\n[[nodiscard]] QImage ForumTopicIconFrame(\n\tint32 colorId,\n\tconst QString &title,\n\tconst style::ForumTopicIcon &st);\n[[nodiscard]] QImage ForumTopicGeneralIconFrame(\n\tint size,\n\tconst QColor &color);\n[[nodiscard]] TextWithEntities ForumTopicIconWithTitle(\n\tMsgId rootId,\n\tDocumentId iconId,\n\tconst QString &title);\n\n[[nodiscard]] QString ForumGeneralIconTitle();\n[[nodiscard]] bool IsForumGeneralIconTitle(const QString &title);\n[[nodiscard]] int32 ForumGeneralIconColor(const QColor &color);\n[[nodiscard]] QColor ParseForumGeneralIconColor(int32 value);\n\nstruct TopicIconDescriptor {\n\tQString title;\n\tint32 colorId = 0;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn !colorId && title.isEmpty();\n\t}\n\texplicit operator bool() const {\n\t\treturn !empty();\n\t}\n};\n\n[[nodiscard]] QString TopicIconEmojiEntity(TopicIconDescriptor descriptor);\n[[nodiscard]] TopicIconDescriptor ParseTopicIconEmojiEntity(\n\tQStringView entity);\n\nclass ForumTopic final : public Thread {\npublic:\n\tstatic constexpr auto kGeneralId = 1;\n\n\tForumTopic(not_null<Forum*> forum, MsgId rootId);\n\t~ForumTopic();\n\n\tnot_null<History*> owningHistory() override {\n\t\treturn history();\n\t}\n\n\t[[nodiscard]] bool isGeneral() const {\n\t\treturn (_rootId == kGeneralId);\n\t}\n\n\t[[nodiscard]] std::shared_ptr<RepliesList> replies() const;\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\t[[nodiscard]] UserData *bot() const;\n\t[[nodiscard]] ChannelData *channel() const;\n\t[[nodiscard]] not_null<History*> history() const;\n\t[[nodiscard]] not_null<Forum*> forum() const;\n\t[[nodiscard]] rpl::producer<> destroyed() const;\n\t[[nodiscard]] MsgId rootId() const;\n\t[[nodiscard]] PeerId creatorId() const;\n\t[[nodiscard]] TimeId creationDate() const;\n\n\t[[nodiscard]] not_null<HistoryView::ListMemento*> listMemento();\n\n\t[[nodiscard]] bool my() const;\n\t[[nodiscard]] bool canEdit() const;\n\t[[nodiscard]] bool canToggleClosed() const;\n\t[[nodiscard]] bool canTogglePinned() const;\n\t[[nodiscard]] bool canDelete() const;\n\n\t[[nodiscard]] bool closed() const;\n\tvoid setClosed(bool closed);\n\tvoid setClosedAndSave(bool closed);\n\n\t[[nodiscard]] bool hidden() const;\n\tvoid setHidden(bool hidden);\n\n\t[[nodiscard]] bool creating() const;\n\tvoid discard();\n\n\tvoid setRealRootId(MsgId realId);\n\tvoid readTillEnd();\n\tvoid requestChatListMessage();\n\n\tvoid applyTopic(const MTPDforumTopic &data);\n\n\tTimeId adjustedChatListTimeId() const override;\n\n\tint fixedOnTopIndex() const override;\n\tbool shouldBeInChatList() const override;\n\tDialogs::UnreadState chatListUnreadState() const override;\n\tDialogs::BadgesState chatListBadgesState() const override;\n\tHistoryItem *chatListMessage() const override;\n\tbool chatListMessageKnown() const override;\n\tconst QString &chatListName() const override;\n\tconst QString &chatListNameSortKey() const override;\n\tint chatListNameVersion() const override;\n\tconst base::flat_set<QString> &chatListNameWords() const override;\n\tconst base::flat_set<QChar> &chatListFirstLetters() const override;\n\n\tvoid hasUnreadMentionChanged(bool has) override;\n\tvoid hasUnreadReactionChanged(bool has) override;\n\tvoid hasUnreadPollVoteChanged(bool has) override;\n\n\t[[nodiscard]] HistoryItem *lastMessage() const;\n\t[[nodiscard]] HistoryItem *lastServerMessage() const;\n\t[[nodiscard]] bool lastMessageKnown() const;\n\t[[nodiscard]] bool lastServerMessageKnown() const;\n\t[[nodiscard]] MsgId lastKnownServerMessageId() const;\n\n\t[[nodiscard]] QString title() const;\n\t[[nodiscard]] TextWithEntities titleWithIcon() const;\n\t[[nodiscard]] TextWithEntities titleWithIconOrLogo() const;\n\t[[nodiscard]] int titleVersion() const;\n\tvoid applyTitle(const QString &title);\n\t[[nodiscard]] DocumentId iconId() const;\n\tvoid applyIconId(DocumentId iconId);\n\t[[nodiscard]] int32 colorId() const;\n\tvoid applyColorId(int32 colorId);\n\tvoid applyCreator(PeerId creatorId);\n\tvoid applyCreationDate(TimeId date);\n\tvoid applyIsMy(bool my);\n\tvoid applyMaybeLast(not_null<HistoryItem*> item);\n\tvoid applyItemAdded(not_null<HistoryItem*> item);\n\tvoid applyItemRemoved(MsgId id);\n\tvoid maybeSetLastMessage(not_null<HistoryItem*> item);\n\n\t[[nodiscard]] PeerNotifySettings &notify() {\n\t\treturn _notify;\n\t}\n\t[[nodiscard]] const PeerNotifySettings &notify() const {\n\t\treturn _notify;\n\t}\n\n\tvoid chatListPreloadData() override;\n\tvoid paintUserpic(\n\t\tPainter &p,\n\t\tUi::PeerUserpicView &view,\n\t\tconst Dialogs::Ui::PaintContext &context) const override;\n\tvoid clearUserpicLoops();\n\n\t[[nodiscard]] bool isServerSideUnread(\n\t\tnot_null<const HistoryItem*> item) const override;\n\n\tvoid setMuted(bool muted) override;\n\n\t[[nodiscard]] auto sendActionPainter()\n\t\t-> HistoryView::SendActionPainter* override;\n\nprivate:\n\tenum class Flag : uchar {\n\t\tClosed = (1 << 0),\n\t\tHidden = (1 << 1),\n\t\tMy = (1 << 2),\n\t\tHasPinnedMessages = (1 << 3),\n\t\tGeneralIconActive = (1 << 4),\n\t\tGeneralIconSelected = (1 << 5),\n\t\tResolveChatListMessage = (1 << 6),\n\t};\n\tfriend inline constexpr bool is_flag_type(Flag) { return true; }\n\tusing Flags = base::flags<Flag>;\n\n\tvoid indexTitleParts();\n\tvoid validateDefaultIcon() const;\n\tvoid validateGeneralIcon(const Dialogs::Ui::PaintContext &context) const;\n\tvoid applyTopicTopMessage(MsgId topMessageId);\n\tvoid growLastKnownServerMessageId(MsgId id);\n\tvoid invalidateTitleWithIcon();\n\n\tvoid setLastMessage(HistoryItem *item);\n\tvoid setLastServerMessage(HistoryItem *item);\n\tvoid setChatListMessage(HistoryItem *item);\n\tvoid allowChatListMessageResolve();\n\tvoid resolveChatListMessageGroup();\n\n\tvoid subscribeToUnreadChanges();\n\t[[nodiscard]] Dialogs::UnreadState unreadStateFor(\n\t\tint count,\n\t\tbool known) const;\n\n\tconst not_null<Forum*> _forum;\n\tconst not_null<Dialogs::MainList*> _list;\n\tstd::shared_ptr<RepliesList> _replies;\n\tstd::unique_ptr<HistoryView::ListMemento> _listMemento;\n\tstd::shared_ptr<HistoryView::SendActionPainter> _sendActionPainter;\n\tMsgId _rootId = 0;\n\tMsgId _lastKnownServerMessageId = 0;\n\n\tPeerNotifySettings _notify;\n\n\tQString _title;\n\tDocumentId _iconId = 0;\n\tbase::flat_set<QString> _titleWords;\n\tbase::flat_set<QChar> _titleFirstLetters;\n\tPeerId _creatorId = 0;\n\tTimeId _creationDate = 0;\n\tint _titleVersion = 0;\n\tint32 _colorId = 0;\n\tmutable Flags _flags;\n\n\tstd::unique_ptr<Ui::Text::CustomEmoji> _icon;\n\tmutable QImage _defaultIcon; // on-demand\n\n\tstd::optional<HistoryItem*> _lastMessage;\n\tstd::optional<HistoryItem*> _lastServerMessage;\n\tstd::optional<HistoryItem*> _chatListMessage;\n\tbase::flat_set<FullMsgId> _requestedGroups;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_game.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_game.h\"\n\nGameData::GameData(not_null<Data::Session*> owner, const GameId &id)\n: owner(owner)\n, id(id) {\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_game.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_photo.h\"\n#include \"data/data_document.h\"\n\nstruct GameData {\n\tGameData(not_null<Data::Session*> owner, const GameId &id);\n\n\tconst not_null<Data::Session*> owner;\n\tGameId id = 0;\n\tuint64 accessHash = 0;\n\tQString shortName;\n\tQString title;\n\tQString description;\n\tPhotoData *photo = nullptr;\n\tDocumentData *document = nullptr;\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_group_call.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_group_call.h\"\n\n#include \"base/unixtime.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"main/main_session.h\"\n#include \"calls/calls_instance.h\"\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_common.h\"\n#include \"core/application.h\"\n#include \"apiwrap.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kRequestPerPage = 50;\nconstexpr auto kSpeakingAfterActive = crl::time(6000);\nconstexpr auto kActiveAfterJoined = crl::time(1000);\nconstexpr auto kWaitForUpdatesTimeout = 3 * crl::time(1000);\nconstexpr auto kReloadStaleTimeout = 16 * crl::time(1000);\n\n[[nodiscard]] QString ExtractNextOffset(const MTPphone_GroupCall &call) {\n\treturn call.match([&](const MTPDphone_groupCall &data) {\n\t\treturn qs(data.vparticipants_next_offset());\n\t});\n}\n\n} // namespace\n\nconst std::string &RtmpEndpointId() {\n\tstatic const auto result = std::string(\"unified\");\n\treturn result;\n}\n\nconst std::string &GroupCallParticipant::cameraEndpoint() const {\n\treturn GetCameraEndpoint(videoParams);\n}\n\nconst std::string &GroupCallParticipant::screenEndpoint() const {\n\treturn GetScreenEndpoint(videoParams);\n}\n\nbool GroupCallParticipant::cameraPaused() const {\n\treturn IsCameraPaused(videoParams);\n}\n\nbool GroupCallParticipant::screenPaused() const {\n\treturn IsScreenPaused(videoParams);\n}\n\nGroupCall::GroupCall(\n\tnot_null<PeerData*> peer,\n\tCallId id,\n\tuint64 accessHash,\n\tTimeId scheduleDate,\n\tbool rtmp,\n\tGroupCallOrigin origin)\n: _id(id)\n, _accessHash(accessHash)\n, _peer(peer)\n, _reloadByQueuedUpdatesTimer([=] { reload(); })\n, _speakingByActiveFinishTimer([=] { checkFinishSpeakingByActive(); })\n, _scheduleDate(scheduleDate)\n, _savedSendAs(peer->session().user())\n, _rtmp(rtmp)\n, _conference(origin == GroupCallOrigin::Conference)\n, _videoStream(origin == GroupCallOrigin::VideoStream)\n, _listenersHidden(rtmp) {\n\tif (_conference) {\n\t\tsession().data().registerGroupCall(this);\n\n\t\t_participantUpdates.events(\n\t\t) | rpl::filter([=](const ParticipantUpdate &update) {\n\t\t\treturn !update.now\n\t\t\t\t&& !update.was->peer->isSelf()\n\t\t\t\t&& !_participantsWithAccess.current().empty();\n\t\t}) | rpl::on_next([=](const ParticipantUpdate &update) {\n\t\t\tif (const auto id = peerToUser(update.was->peer->id)) {\n\t\t\t\tif (_participantsWithAccess.current().contains(id)) {\n\t\t\t\t\t_staleParticipantIds.fire({ id });\n\t\t\t\t}\n\t\t\t}\n\t\t}, _checkStaleLifetime);\n\n\t\t_participantsWithAccess.changes(\n\t\t) | rpl::filter([=](const base::flat_set<UserId> &list) {\n\t\t\treturn !list.empty();\n\t\t}) | rpl::on_next([=] {\n\t\t\tif (_allParticipantsLoaded) {\n\t\t\t\tcheckStaleParticipants();\n\t\t\t} else {\n\t\t\t\trequestParticipants();\n\t\t\t}\n\t\t}, _checkStaleLifetime);\n\t} else if (_videoStream) {\n\t\tsession().data().registerGroupCall(this);\n\t}\n}\n\nGroupCall::~GroupCall() {\n\tif (_conference || _videoStream) {\n\t\tsession().data().unregisterGroupCall(this);\n\t}\n\tapi().request(_unknownParticipantPeersRequestId).cancel();\n\tapi().request(_participantsRequestId).cancel();\n\tapi().request(_reloadRequestId).cancel();\n}\n\nMain::Session &GroupCall::session() const {\n\treturn _peer->session();\n}\n\nCallId GroupCall::id() const {\n\treturn _id;\n}\n\nbool GroupCall::loaded() const {\n\treturn _version > 0;\n}\n\nrpl::producer<bool> GroupCall::loadedValue() const {\n\tif (loaded()) {\n\t\treturn rpl::single(true);\n\t}\n\treturn _loadedChanges.events_starting_with(false);\n}\n\nbool GroupCall::rtmp() const {\n\treturn _rtmp;\n}\n\nGroupCallOrigin GroupCall::origin() const {\n\treturn _conference\n\t\t? GroupCallOrigin::Conference\n\t\t: _videoStream\n\t\t? GroupCallOrigin::VideoStream\n\t\t: GroupCallOrigin::Group;\n}\n\nbool GroupCall::creator() const {\n\treturn _creator;\n}\n\nbool GroupCall::canManage() const {\n\treturn _conference ? _creator : _peer->canManageGroupCall();\n}\n\nbool GroupCall::listenersHidden() const {\n\treturn _listenersHidden;\n}\n\nbool GroupCall::blockchainMayBeEmpty() const {\n\treturn _version < 2;\n}\n\nnot_null<PeerData*> GroupCall::peer() const {\n\treturn _peer;\n}\n\nMTPInputGroupCall GroupCall::input() const {\n\treturn MTP_inputGroupCall(MTP_long(_id), MTP_long(_accessHash));\n}\n\nvoid GroupCall::setPeer(not_null<PeerData*> peer) {\n\tExpects(peer->migrateFrom() == _peer);\n\tExpects(_peer->migrateTo() == peer);\n\n\t_peer = peer;\n}\n\nauto GroupCall::participants() const\n-> const std::vector<Participant> & {\n\treturn _participants;\n}\n\nvoid GroupCall::requestParticipants() {\n\tif (!_savedFull) {\n\t\tif (_participantsRequestId || _reloadRequestId) {\n\t\t\treturn;\n\t\t} else if (_allParticipantsLoaded) {\n\t\t\treturn;\n\t\t}\n\t}\n\tapi().request(base::take(_participantsRequestId)).cancel();\n\t_participantsRequestId = api().request(MTPphone_GetGroupParticipants(\n\t\tinput(),\n\t\tMTP_vector<MTPInputPeer>(), // ids\n\t\tMTP_vector<MTPint>(), // ssrcs\n\t\tMTP_string(_savedFull\n\t\t\t? ExtractNextOffset(*_savedFull)\n\t\t\t: _nextOffset),\n\t\tMTP_int(kRequestPerPage)\n\t)).done([=](const MTPphone_GroupParticipants &result) {\n\t\t_participantsRequestId = 0;\n\t\tresult.match([&](const MTPDphone_groupParticipants &data) {\n\t\t\tconst auto reloaded = processSavedFullCall();\n\t\t\t_nextOffset = qs(data.vnext_offset());\n\t\t\t_peer->owner().processUsers(data.vusers());\n\t\t\t_peer->owner().processChats(data.vchats());\n\t\t\tapplyParticipantsSlice(\n\t\t\t\tdata.vparticipants().v,\n\t\t\t\t(reloaded\n\t\t\t\t\t? ApplySliceSource::FullReloaded\n\t\t\t\t\t: ApplySliceSource::SliceLoaded));\n\t\t\tsetServerParticipantsCount(data.vcount().v);\n\t\t\tif (data.vparticipants().v.isEmpty()) {\n\t\t\t\tsetParticipantsLoaded();\n\t\t\t}\n\t\t\tfinishParticipantsSliceRequest();\n\t\t\tif (reloaded) {\n\t\t\t\t_participantsReloaded.fire({});\n\t\t\t}\n\t\t});\n\t}).fail([=] {\n\t\t_participantsRequestId = 0;\n\t\tconst auto reloaded = processSavedFullCall();\n\t\tsetServerParticipantsCount(_participants.size());\n\t\tsetParticipantsLoaded();\n\t\tfinishParticipantsSliceRequest();\n\t\tif (reloaded) {\n\t\t\t_participantsReloaded.fire({});\n\t\t}\n\t}).send();\n}\n\nvoid GroupCall::setParticipantsLoaded() {\n\t_allParticipantsLoaded = true;\n\tcheckStaleParticipants();\n}\n\nvoid GroupCall::checkStaleParticipants() {\n\tconst auto &list = _participantsWithAccess.current();\n\tif (list.empty()) {\n\t\treturn;\n\t}\n\tauto existing = base::flat_set<UserId>();\n\texisting.reserve(_participants.size() + 1);\n\texisting.emplace(session().userId());\n\tfor (const auto &participant : _participants) {\n\t\tif (const auto id = peerToUser(participant.peer->id)) {\n\t\t\texisting.emplace(id);\n\t\t}\n\t}\n\tauto stale = base::flat_set<UserId>();\n\tfor (const auto &id : list) {\n\t\tif (!existing.contains(id)) {\n\t\t\tstale.reserve(list.size());\n\t\t\tstale.emplace(id);\n\t\t}\n\t}\n\tif (!stale.empty()) {\n\t\t_staleParticipantIds.fire(std::move(stale));\n\t}\n}\n\nbool GroupCall::processSavedFullCall() {\n\tif (!_savedFull) {\n\t\treturn false;\n\t}\n\tapi().request(base::take(_reloadRequestId)).cancel();\n\t_reloadLastFinished = crl::now();\n\tprocessFullCallFields(*base::take(_savedFull));\n\treturn true;\n}\n\nvoid GroupCall::finishParticipantsSliceRequest() {\n\tcomputeParticipantsCount();\n\tprocessQueuedUpdates();\n}\n\nvoid GroupCall::setServerParticipantsCount(int count) {\n\t_serverParticipantsCount = count;\n\tchangePeerEmptyCallFlag();\n}\n\nvoid GroupCall::changePeerEmptyCallFlag() {\n\tconst auto chat = _peer->asChat();\n\tconst auto channel = _peer->asChannel();\n\tconstexpr auto chatFlag = ChatDataFlag::CallNotEmpty;\n\tconstexpr auto channelFlag = ChannelDataFlag::CallNotEmpty;\n\tif (_peer->groupCall() != this) {\n\t\treturn;\n\t} else if (_serverParticipantsCount > 0) {\n\t\tif (chat && !(chat->flags() & chatFlag)) {\n\t\t\tchat->addFlags(chatFlag);\n\t\t\tchat->session().changes().peerUpdated(\n\t\t\t\tchat,\n\t\t\t\tData::PeerUpdate::Flag::GroupCall);\n\t\t} else if (channel && !(channel->flags() & channelFlag)) {\n\t\t\tchannel->addFlags(channelFlag);\n\t\t\tchannel->session().changes().peerUpdated(\n\t\t\t\tchannel,\n\t\t\t\tData::PeerUpdate::Flag::GroupCall);\n\t\t}\n\t} else if (chat && (chat->flags() & chatFlag)) {\n\t\tchat->removeFlags(chatFlag);\n\t\tchat->session().changes().peerUpdated(\n\t\t\tchat,\n\t\t\tData::PeerUpdate::Flag::GroupCall);\n\t} else if (channel && (channel->flags() & channelFlag)) {\n\t\tchannel->removeFlags(channelFlag);\n\t\tchannel->session().changes().peerUpdated(\n\t\t\tchannel,\n\t\t\tData::PeerUpdate::Flag::GroupCall);\n\t}\n}\n\nint GroupCall::fullCount() const {\n\treturn _fullCount.current();\n}\n\nrpl::producer<int> GroupCall::fullCountValue() const {\n\treturn _fullCount.value();\n}\n\nQString GroupCall::conferenceInviteLink() const {\n\treturn _conferenceInviteLink;\n}\n\nbool GroupCall::participantsLoaded() const {\n\treturn _allParticipantsLoaded;\n}\n\nPeerData *GroupCall::participantPeerByAudioSsrc(uint32 ssrc) const {\n\tconst auto i = _participantPeerByAudioSsrc.find(ssrc);\n\treturn (i != end(_participantPeerByAudioSsrc))\n\t\t? i->second.get()\n\t\t: nullptr;\n}\n\nconst GroupCallParticipant *GroupCall::participantByPeer(\n\t\tnot_null<PeerData*> peer) const {\n\treturn const_cast<GroupCall*>(this)->findParticipant(peer);\n}\n\nGroupCallParticipant *GroupCall::findParticipant(\n\t\tnot_null<PeerData*> peer) {\n\tconst auto i = ranges::find(_participants, peer, &Participant::peer);\n\treturn (i != end(_participants)) ? &*i : nullptr;\n}\n\nconst GroupCallParticipant *GroupCall::participantByEndpoint(\n\t\tconst std::string &endpoint) const {\n\tif (endpoint.empty()) {\n\t\treturn nullptr;\n\t}\n\tfor (const auto &participant : _participants) {\n\t\tif (GetCameraEndpoint(participant.videoParams) == endpoint\n\t\t\t|| GetScreenEndpoint(participant.videoParams) == endpoint) {\n\t\t\treturn &participant;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nrpl::producer<> GroupCall::participantsReloaded() {\n\treturn _participantsReloaded.events();\n}\n\nauto GroupCall::participantUpdated() const\n-> rpl::producer<ParticipantUpdate> {\n\treturn _participantUpdates.events();\n}\n\nauto GroupCall::participantSpeaking() const\n-> rpl::producer<not_null<Participant*>> {\n\treturn _participantSpeaking.events();\n}\n\nvoid GroupCall::setParticipantsWithAccess(base::flat_set<UserId> list) {\n\t_participantsWithAccess = std::move(list);\n\tif (_allParticipantsLoaded) {\n\t\tcheckStaleParticipants();\n\t} else {\n\t\trequestParticipants();\n\t}\n}\n\nauto GroupCall::participantsWithAccessCurrent() const\n-> const base::flat_set<UserId> & {\n\treturn _participantsWithAccess.current();\n}\n\nauto GroupCall::participantsWithAccessValue() const\n-> rpl::producer<base::flat_set<UserId>> {\n\treturn _participantsWithAccess.value();\n}\n\nauto GroupCall::staleParticipantIds() const\n-> rpl::producer<base::flat_set<UserId>> {\n\treturn _staleParticipantIds.events();\n}\n\nvoid GroupCall::enqueueUpdate(const MTPUpdate &update) {\n\tconst auto initial = !_version;\n\tupdate.match([&](const MTPDupdateGroupCall &updateData) {\n\t\tupdateData.vcall().match([&](const MTPDgroupCall &data) {\n\t\t\tconst auto version = data.vversion().v;\n\t\t\tif (!_applyingQueuedUpdates\n\t\t\t\t&& (!_version || _version == version)) {\n\t\t\t\tDEBUG_LOG((\"Group Call Participants: \"\n\t\t\t\t\t\"Apply updateGroupCall %1 -> %2\"\n\t\t\t\t\t).arg(_version\n\t\t\t\t\t).arg(version));\n\t\t\t\tapplyEnqueuedUpdate(update);\n\t\t\t} else if (!_version || _version <= version) {\n\t\t\t\tDEBUG_LOG((\"Group Call Participants: \"\n\t\t\t\t\t\"Queue updateGroupCall %1 -> %2\"\n\t\t\t\t\t).arg(_version\n\t\t\t\t\t).arg(version));\n\t\t\t\tconst auto type = QueuedType::Call;\n\t\t\t\t_queuedUpdates.emplace(std::pair{ version, type }, update);\n\t\t\t}\n\t\t}, [&](const MTPDgroupCallDiscarded &data) {\n\t\t\tdiscard(data);\n\t\t});\n\t}, [&](const MTPDupdateGroupCallParticipants &updateData) {\n\t\tconst auto version = updateData.vversion().v;\n\t\tconst auto proj = [](const MTPGroupCallParticipant &data) {\n\t\t\treturn data.match([&](const MTPDgroupCallParticipant &data) {\n\t\t\t\treturn data.is_versioned();\n\t\t\t});\n\t\t};\n\t\tconst auto increment = ranges::contains(\n\t\t\tupdateData.vparticipants().v,\n\t\t\ttrue,\n\t\t\tproj);\n\t\tconst auto required = increment ? (version - 1) : version;\n\t\tif (!_applyingQueuedUpdates && (_version == required)) {\n\t\t\tDEBUG_LOG((\"Group Call Participants: \"\n\t\t\t\t\"Apply updateGroupCallParticipant %1 (%2)\"\n\t\t\t\t).arg(_version\n\t\t\t\t).arg(Logs::b(increment)));\n\t\t\tapplyEnqueuedUpdate(update);\n\t\t} else if (_version <= required) {\n\t\t\tDEBUG_LOG((\"Group Call Participants: \"\n\t\t\t\t\"Queue updateGroupCallParticipant %1 -> %2 (%3)\"\n\t\t\t\t).arg(_version\n\t\t\t\t).arg(version\n\t\t\t\t).arg(Logs::b(increment)));\n\t\t\tconst auto type = increment\n\t\t\t\t? QueuedType::VersionedParticipant\n\t\t\t\t: QueuedType::Participant;\n\t\t\t_queuedUpdates.emplace(std::pair{ version, type }, update);\n\t\t}\n\t}, [](const auto &) {\n\t\tUnexpected(\"Type in GroupCall::enqueueUpdate.\");\n\t});\n\tprocessQueuedUpdates(initial);\n}\n\nvoid GroupCall::discard(const MTPDgroupCallDiscarded &data) {\n\tconst auto id = _id;\n\tconst auto peer = _peer;\n\tcrl::on_main(&peer->session(), [=] {\n\t\tif (peer->groupCall() && peer->groupCall()->id() == id) {\n\t\t\tif (const auto chat = peer->asChat()) {\n\t\t\t\tchat->clearGroupCall();\n\t\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\t\tchannel->clearGroupCall();\n\t\t\t}\n\t\t}\n\t});\n\tCore::App().calls().applyGroupCallUpdateChecked(\n\t\t&peer->session(),\n\t\tMTP_updateGroupCall(\n\t\t\tMTP_flags(MTPDupdateGroupCall::Flag::f_peer),\n\t\t\tpeerToMTP(peer->id),\n\t\t\tMTP_groupCallDiscarded(\n\t\t\t\tdata.vid(),\n\t\t\t\tdata.vaccess_hash(),\n\t\t\t\tdata.vduration())));\n}\n\nvoid GroupCall::processFullCallUsersChats(const MTPphone_GroupCall &call) {\n\tcall.match([&](const MTPDphone_groupCall &data) {\n\t\t_peer->owner().processUsers(data.vusers());\n\t\t_peer->owner().processChats(data.vchats());\n\t});\n}\n\nvoid GroupCall::processFullCallFields(const MTPphone_GroupCall &call) {\n\tcall.match([&](const MTPDphone_groupCall &data) {\n\t\tconst auto &participants = data.vparticipants().v;\n\t\tconst auto nextOffset = qs(data.vparticipants_next_offset());\n\t\tdata.vcall().match([&](const MTPDgroupCall &data) {\n\t\t\t_participants.clear();\n\t\t\t_speakingByActiveFinishes.clear();\n\t\t\t_participantPeerByAudioSsrc.clear();\n\t\t\t_allParticipantsLoaded = false;\n\n\t\t\tapplyParticipantsSlice(\n\t\t\t\tparticipants,\n\t\t\t\tApplySliceSource::FullReloaded);\n\t\t\t_nextOffset = nextOffset;\n\n\t\t\tapplyCallFields(data);\n\t\t}, [&](const MTPDgroupCallDiscarded &data) {\n\t\t\tdiscard(data);\n\t\t});\n\t});\n}\n\nvoid GroupCall::processFullCall(const MTPphone_GroupCall &call) {\n\tprocessFullCallUsersChats(call);\n\tprocessFullCallFields(call);\n\tfinishParticipantsSliceRequest();\n\t_participantsReloaded.fire({});\n}\n\nvoid GroupCall::applyCallFields(const MTPDgroupCall &data) {\n\tDEBUG_LOG((\"Group Call Participants: \"\n\t\t\"Set from groupCall %1 -> %2\"\n\t\t).arg(_version\n\t\t).arg(data.vversion().v));\n\tconst auto initial = !_version;\n\t_version = data.vversion().v;\n\tif (!_version) {\n\t\tLOG((\"API Error: Got zero version in groupCall.\"));\n\t\t_version = 1;\n\t}\n\t_rtmp = data.is_rtmp_stream();\n\t_creator = data.is_creator();\n\t_listenersHidden = data.is_listeners_hidden();\n\tif (data.is_conference()) {\n\t\t_canChangeJoinMuted = false;\n\t\t_joinMuted = false;\n\t\tconst auto enabled = data.is_messages_enabled();\n\t\t_canChangeMessagesEnabled = enabled;\n\t\tif (!enabled || initial) {\n\t\t\t_messagesEnabled = enabled;\n\t\t}\n\t} else {\n\t\t_canChangeJoinMuted = data.is_can_change_join_muted();\n\t\t_joinMuted = data.is_join_muted();\n\t\t_canChangeMessagesEnabled = data.is_can_change_messages_enabled();\n\t\t_messagesEnabled = data.is_messages_enabled();\n\t}\n\t_messagesMinPrice = data.vsend_paid_messages_stars().value_or_empty();\n\t_joinedToTop = !data.is_join_date_asc();\n\tsetServerParticipantsCount(data.vparticipants_count().v);\n\tchangePeerEmptyCallFlag();\n\t_title = qs(data.vtitle().value_or_empty());\n\t{\n\t\t_recordVideo = data.is_record_video_active();\n\t\t_recordStartDate = data.vrecord_start_date().value_or_empty();\n\t}\n\t_scheduleDate = data.vschedule_date().value_or_empty();\n\t_scheduleStartSubscribed = data.is_schedule_start_subscribed();\n\t_unmutedVideoLimit = data.vunmuted_video_limit().v;\n\t_allParticipantsLoaded\n\t\t= (_serverParticipantsCount == _participants.size());\n\t_conferenceInviteLink = qs(data.vinvite_link().value_or_empty());\n\tif (const auto as = data.vdefault_send_as()) {\n\t\t_savedSendAs = _peer->owner().peer(peerFromMTP(*as));\n\t}\n\tif (initial) {\n\t\t_loadedChanges.fire(true);\n\t}\n}\n\nvoid GroupCall::applyLocalUpdate(\n\t\tconst MTPDupdateGroupCallParticipants &update) {\n\tapplyParticipantsSlice(\n\t\tupdate.vparticipants().v,\n\t\tApplySliceSource::UpdateConstructed);\n}\n\nvoid GroupCall::applyEnqueuedUpdate(const MTPUpdate &update) {\n\tExpects(!_applyingQueuedUpdates);\n\n\t_applyingQueuedUpdates = true;\n\tconst auto guard = gsl::finally([&] { _applyingQueuedUpdates = false; });\n\n\tupdate.match([&](const MTPDupdateGroupCall &data) {\n\t\tdata.vcall().match([&](const MTPDgroupCall &data) {\n\t\t\tapplyCallFields(data);\n\t\t\tcomputeParticipantsCount();\n\t\t}, [&](const MTPDgroupCallDiscarded &data) {\n\t\t\tdiscard(data);\n\t\t});\n\t}, [&](const MTPDupdateGroupCallParticipants &data) {\n\t\tDEBUG_LOG((\"Group Call Participants: \"\n\t\t\t\"Set from updateGroupCallParticipants %1 -> %2\"\n\t\t\t).arg(_version\n\t\t\t).arg(data.vversion().v));\n\t\t_version = data.vversion().v;\n\t\tif (!_version) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"Got zero version in updateGroupCallParticipants.\"));\n\t\t\t_version = 1;\n\t\t}\n\t\tapplyParticipantsSlice(\n\t\t\tdata.vparticipants().v,\n\t\t\tApplySliceSource::UpdateReceived);\n\t}, [](const auto &) {\n\t\tUnexpected(\"Type in GroupCall::applyEnqueuedUpdate.\");\n\t});\n\tCore::App().calls().applyGroupCallUpdateChecked(&session(), update);\n}\n\nvoid GroupCall::processQueuedUpdates(bool initial) {\n\tif (!_version || _applyingQueuedUpdates) {\n\t\treturn;\n\t}\n\n\tconst auto size = _queuedUpdates.size();\n\twhile (!_queuedUpdates.empty()) {\n\t\tconst auto &entry = _queuedUpdates.front();\n\t\tconst auto version = entry.first.first;\n\t\tconst auto type = entry.first.second;\n\t\tconst auto incremented = (type == QueuedType::VersionedParticipant);\n\t\tif ((version < _version)\n\t\t\t|| (version == _version && incremented && !initial)) {\n\t\t\t// There is a case for a new conference call we receive:\n\t\t\t// - updateGroupCall, version = 2\n\t\t\t// - updateGroupCallParticipants, version = 2, versioned\n\t\t\t// In case we were joining together with creation,\n\t\t\t// in that case we don't want to skip the participants update,\n\t\t\t// so we pass the `initial` flag specifically for that case.\n\t\t\t_queuedUpdates.erase(_queuedUpdates.begin());\n\t\t} else if (version == _version\n\t\t\t|| (version == _version + 1 && incremented)) {\n\t\t\tconst auto update = entry.second;\n\t\t\t_queuedUpdates.erase(_queuedUpdates.begin());\n\t\t\tapplyEnqueuedUpdate(update);\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (_queuedUpdates.empty()) {\n\t\t_reloadByQueuedUpdatesTimer.cancel();\n\t} else if (_queuedUpdates.size() != size\n\t\t|| !_reloadByQueuedUpdatesTimer.isActive()) {\n\t\t_reloadByQueuedUpdatesTimer.callOnce(kWaitForUpdatesTimeout);\n\t}\n}\n\nvoid GroupCall::computeParticipantsCount() {\n\t_fullCount = (_allParticipantsLoaded && !_listenersHidden)\n\t\t? int(_participants.size())\n\t\t: std::max(int(_participants.size()), _serverParticipantsCount);\n}\n\nvoid GroupCall::reloadIfStale() {\n\tif (!fullCount() && !participantsLoaded()) {\n\t\treload();\n\t} else if (!_reloadLastFinished\n\t\t|| crl::now() > _reloadLastFinished + kReloadStaleTimeout) {\n\t\treload();\n\t}\n}\n\nvoid GroupCall::reload() {\n\tif (_reloadRequestId || _applyingQueuedUpdates) {\n\t\treturn;\n\t}\n\tapi().request(base::take(_participantsRequestId)).cancel();\n\n\tDEBUG_LOG((\"Group Call Participants: \"\n\t\t\"Reloading with queued: %1\"\n\t\t).arg(_queuedUpdates.size()));\n\n\twhile (!_queuedUpdates.empty()) {\n\t\tconst auto &entry = _queuedUpdates.front();\n\t\tconst auto update = entry.second;\n\t\t_queuedUpdates.erase(_queuedUpdates.begin());\n\t\tapplyEnqueuedUpdate(update);\n\t}\n\t_reloadByQueuedUpdatesTimer.cancel();\n\n\tconst auto limit = 3;\n\t_reloadRequestId = api().request(\n\t\tMTPphone_GetGroupCall(input(), MTP_int(limit))\n\t).done([=](const MTPphone_GroupCall &result) {\n\t\tif (requestParticipantsAfterReload(result)) {\n\t\t\t_savedFull = result;\n\t\t\tprocessFullCallUsersChats(result);\n\t\t\trequestParticipants();\n\t\t\treturn;\n\t\t}\n\t\t_reloadRequestId = 0;\n\t\t_reloadLastFinished = crl::now();\n\t\tprocessFullCall(result);\n\t}).fail([=] {\n\t\t_reloadRequestId = 0;\n\t\t_reloadLastFinished = crl::now();\n\t}).send();\n}\n\nbool GroupCall::requestParticipantsAfterReload(\n\t\tconst MTPphone_GroupCall &call) const {\n\treturn call.match([&](const MTPDphone_groupCall &data) {\n\t\tconst auto received = data.vparticipants().v.size();\n\t\tconst auto size = data.vcall().match([&](const MTPDgroupCall &data) {\n\t\t\treturn data.vparticipants_count().v;\n\t\t}, [](const auto &) {\n\t\t\treturn 0;\n\t\t});\n\t\treturn (received < size) && (received < _participants.size());\n\t});\n}\n\nvoid GroupCall::applyParticipantsSlice(\n\t\tconst QVector<MTPGroupCallParticipant> &list,\n\t\tApplySliceSource sliceSource) {\n\tfor (const auto &participant : list) {\n\t\tparticipant.match([&](const MTPDgroupCallParticipant &data) {\n\t\t\tconst auto participantPeerId = peerFromMTP(data.vpeer());\n\t\t\tconst auto participantPeer = _peer->owner().peer(\n\t\t\t\tparticipantPeerId);\n\t\t\tconst auto i = ranges::find(\n\t\t\t\t_participants,\n\t\t\t\tparticipantPeer,\n\t\t\t\t&Participant::peer);\n\t\t\tif (data.is_left()) {\n\t\t\t\tif (i != end(_participants)) {\n\t\t\t\t\tauto update = ParticipantUpdate{\n\t\t\t\t\t\t.was = *i,\n\t\t\t\t\t};\n\t\t\t\t\t_participantPeerByAudioSsrc.erase(i->ssrc);\n\t\t\t\t\t_participantPeerByAudioSsrc.erase(\n\t\t\t\t\t\tGetAdditionalAudioSsrc(i->videoParams));\n\t\t\t\t\t_speakingByActiveFinishes.remove(participantPeer);\n\t\t\t\t\t_participants.erase(i);\n\t\t\t\t\tif (sliceSource != ApplySliceSource::FullReloaded) {\n\t\t\t\t\t\t_participantUpdates.fire(std::move(update));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (_serverParticipantsCount > 0) {\n\t\t\t\t\t--_serverParticipantsCount;\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (const auto about = data.vabout()) {\n\t\t\t\tparticipantPeer->setAbout(qs(*about));\n\t\t\t}\n\t\t\tconst auto was = (i != end(_participants))\n\t\t\t\t? std::make_optional(*i)\n\t\t\t\t: std::nullopt;\n\t\t\tconst auto canSelfUnmute = !data.is_muted()\n\t\t\t\t|| data.is_can_self_unmute();\n\t\t\tconst auto lastActive = data.vactive_date().value_or(\n\t\t\t\twas ? was->lastActive : 0);\n\t\t\tconst auto volume = (was\n\t\t\t\t&& !was->applyVolumeFromMin\n\t\t\t\t&& data.is_min())\n\t\t\t\t? was->volume\n\t\t\t\t: data.vvolume().value_or(Calls::Group::kDefaultVolume);\n\t\t\tconst auto applyVolumeFromMin = (was && data.is_min())\n\t\t\t\t? was->applyVolumeFromMin\n\t\t\t\t: (data.is_min() || data.is_volume_by_admin());\n\t\t\tconst auto mutedByMe = (was && data.is_min())\n\t\t\t\t? was->mutedByMe\n\t\t\t\t: data.is_muted_by_you();\n\t\t\tconst auto onlyMinLoaded = data.is_min()\n\t\t\t\t&& (!was || was->onlyMinLoaded);\n\t\t\tconst auto videoJoined = data.is_video_joined();\n\t\t\tconst auto raisedHandRating\n\t\t\t\t= data.vraise_hand_rating().value_or_empty();\n\t\t\tconst auto localUpdate = (sliceSource\n\t\t\t\t== ApplySliceSource::UpdateConstructed);\n\t\t\tconst auto existingVideoParams = (i != end(_participants))\n\t\t\t\t? i->videoParams\n\t\t\t\t: nullptr;\n\t\t\tauto videoParams = localUpdate\n\t\t\t\t? existingVideoParams\n\t\t\t\t: Calls::ParseVideoParams(\n\t\t\t\t\tdata.vvideo(),\n\t\t\t\t\tdata.vpresentation(),\n\t\t\t\t\texistingVideoParams);\n\t\t\tconst auto value = Participant{\n\t\t\t\t.peer = participantPeer,\n\t\t\t\t.videoParams = std::move(videoParams),\n\t\t\t\t.date = data.vdate().v,\n\t\t\t\t.lastActive = lastActive,\n\t\t\t\t.raisedHandRating = raisedHandRating,\n\t\t\t\t.ssrc = uint32(data.vsource().v),\n\t\t\t\t.volume = volume,\n\t\t\t\t.sounding = canSelfUnmute && was && was->sounding,\n\t\t\t\t.speaking = canSelfUnmute && was && was->speaking,\n\t\t\t\t.additionalSounding = (canSelfUnmute\n\t\t\t\t\t&& was\n\t\t\t\t\t&& was->additionalSounding),\n\t\t\t\t.additionalSpeaking = (canSelfUnmute\n\t\t\t\t\t&& was\n\t\t\t\t\t&& was->additionalSpeaking),\n\t\t\t\t.muted = data.is_muted(),\n\t\t\t\t.mutedByMe = mutedByMe,\n\t\t\t\t.canSelfUnmute = canSelfUnmute,\n\t\t\t\t.onlyMinLoaded = onlyMinLoaded,\n\t\t\t\t.videoJoined = videoJoined,\n\t\t\t\t.applyVolumeFromMin = applyVolumeFromMin,\n\t\t\t};\n\t\t\tconst auto adding = (i == end(_participants));\n\t\t\tif (adding) {\n\t\t\t\tif (value.ssrc) {\n\t\t\t\t\t_participantPeerByAudioSsrc.emplace(\n\t\t\t\t\t\tvalue.ssrc,\n\t\t\t\t\t\tparticipantPeer);\n\t\t\t\t}\n\t\t\t\tif (const auto additional = GetAdditionalAudioSsrc(\n\t\t\t\t\t\tvalue.videoParams)) {\n\t\t\t\t\t_participantPeerByAudioSsrc.emplace(\n\t\t\t\t\t\tadditional,\n\t\t\t\t\t\tparticipantPeer);\n\t\t\t\t}\n\t\t\t\t_participants.push_back(value);\n\t\t\t} else {\n\t\t\t\tif (i->ssrc != value.ssrc) {\n\t\t\t\t\t_participantPeerByAudioSsrc.erase(i->ssrc);\n\t\t\t\t\tif (value.ssrc) {\n\t\t\t\t\t\t_participantPeerByAudioSsrc.emplace(\n\t\t\t\t\t\t\tvalue.ssrc,\n\t\t\t\t\t\t\tparticipantPeer);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (GetAdditionalAudioSsrc(i->videoParams)\n\t\t\t\t\t!= GetAdditionalAudioSsrc(value.videoParams)) {\n\t\t\t\t\t_participantPeerByAudioSsrc.erase(\n\t\t\t\t\t\tGetAdditionalAudioSsrc(i->videoParams));\n\t\t\t\t\tif (const auto additional = GetAdditionalAudioSsrc(\n\t\t\t\t\t\tvalue.videoParams)) {\n\t\t\t\t\t\t_participantPeerByAudioSsrc.emplace(\n\t\t\t\t\t\t\tadditional,\n\t\t\t\t\t\t\tparticipantPeer);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t*i = value;\n\t\t\t}\n\t\t\tif (data.is_just_joined()) {\n\t\t\t\t++_serverParticipantsCount;\n\t\t\t}\n\t\t\tif (sliceSource != ApplySliceSource::FullReloaded) {\n\t\t\t\t_participantUpdates.fire({\n\t\t\t\t\t.was = was,\n\t\t\t\t\t.now = value,\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (adding) {\n\t\t\t\tif (const auto user = participantPeer->asUser()) {\n\t\t\t\t\t_peer->owner().unregisterInvitedToCallUser(\n\t\t\t\t\t\t_id,\n\t\t\t\t\t\tuser,\n\t\t\t\t\t\tfalse);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\tif (sliceSource == ApplySliceSource::UpdateReceived) {\n\t\tchangePeerEmptyCallFlag();\n\t\tcomputeParticipantsCount();\n\t}\n}\n\nvoid GroupCall::applyLastSpoke(\n\t\tuint32 ssrc,\n\t\tLastSpokeTimes when,\n\t\tcrl::time now) {\n\tconst auto i = _participantPeerByAudioSsrc.find(ssrc);\n\tif (i == end(_participantPeerByAudioSsrc)) {\n\t\t_unknownSpokenSsrcs[ssrc] = when;\n\t\trequestUnknownParticipants();\n\t\treturn;\n\t}\n\tconst auto participant = findParticipant(i->second);\n\tAssert(participant != nullptr);\n\n\t_speakingByActiveFinishes.remove(participant->peer);\n\tconst auto sounding = (when.anything + kSoundStatusKeptFor >= now)\n\t\t&& participant->canSelfUnmute;\n\tconst auto speaking = sounding\n\t\t&& (when.voice + kSoundStatusKeptFor >= now);\n\tif (speaking) {\n\t\t_participantSpeaking.fire({ participant });\n\t}\n\tconst auto useAdditional = (ssrc != participant->ssrc);\n\tconst auto nowSounding = useAdditional\n\t\t? participant->additionalSounding\n\t\t: participant->sounding;\n\tconst auto nowSpeaking = useAdditional\n\t\t? participant->additionalSpeaking\n\t\t: participant->speaking;\n\tif (nowSounding != sounding || nowSpeaking != speaking) {\n\t\tconst auto was = *participant;\n\t\tif (useAdditional) {\n\t\t\tparticipant->additionalSounding = sounding;\n\t\t\tparticipant->additionalSpeaking = speaking;\n\t\t} else {\n\t\t\tparticipant->sounding = sounding;\n\t\t\tparticipant->speaking = speaking;\n\t\t}\n\t\t_participantUpdates.fire({\n\t\t\t.was = was,\n\t\t\t.now = *participant,\n\t\t});\n\t}\n}\n\nvoid GroupCall::resolveParticipants(const base::flat_set<uint32> &ssrcs) {\n\tif (ssrcs.empty()) {\n\t\treturn;\n\t}\n\tfor (const auto ssrc : ssrcs) {\n\t\t_unknownSpokenSsrcs.emplace(ssrc, LastSpokeTimes());\n\t}\n\trequestUnknownParticipants();\n}\n\nvoid GroupCall::applyActiveUpdate(\n\t\tPeerId participantPeerId,\n\t\tLastSpokeTimes when,\n\t\tPeerData *participantPeerLoaded) {\n\tif (inCall()) {\n\t\treturn;\n\t}\n\tconst auto participant = participantPeerLoaded\n\t\t? findParticipant(participantPeerLoaded)\n\t\t: nullptr;\n\tconst auto loadByUserId = !participant || participant->onlyMinLoaded;\n\tif (loadByUserId) {\n\t\t_unknownSpokenPeerIds[participantPeerId] = when;\n\t\trequestUnknownParticipants();\n\t}\n\tif (!participant || !participant->canSelfUnmute) {\n\t\treturn;\n\t}\n\tconst auto was = std::make_optional(*participant);\n\tconst auto now = crl::now();\n\tconst auto elapsed = TimeId((now - when.anything) / crl::time(1000));\n\tconst auto lastActive = base::unixtime::now() - elapsed;\n\tconst auto finishes = when.anything + kSpeakingAfterActive;\n\tif (lastActive <= participant->lastActive || finishes <= now) {\n\t\treturn;\n\t}\n\t_speakingByActiveFinishes[participant->peer] = finishes;\n\tif (!_speakingByActiveFinishTimer.isActive()) {\n\t\t_speakingByActiveFinishTimer.callOnce(finishes - now);\n\t}\n\n\tparticipant->lastActive = lastActive;\n\tparticipant->speaking = true;\n\tparticipant->canSelfUnmute = true;\n\tif (!was->speaking || !was->canSelfUnmute) {\n\t\t_participantUpdates.fire({\n\t\t\t.was = was,\n\t\t\t.now = *participant,\n\t\t});\n\t}\n}\n\nvoid GroupCall::checkFinishSpeakingByActive() {\n\tconst auto now = crl::now();\n\tauto nearest = crl::time(0);\n\tauto stop = std::vector<not_null<PeerData*>>();\n\tfor (auto i = begin(_speakingByActiveFinishes)\n\t\t; i != end(_speakingByActiveFinishes);) {\n\t\tconst auto when = i->second;\n\t\tif (now >= when) {\n\t\t\tstop.push_back(i->first);\n\t\t\ti = _speakingByActiveFinishes.erase(i);\n\t\t} else {\n\t\t\tif (!nearest || nearest > when) {\n\t\t\t\tnearest = when;\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t}\n\tfor (const auto &participantPeer : stop) {\n\t\tconst auto participant = findParticipant(participantPeer);\n\t\tAssert(participant != nullptr);\n\t\tif (participant->speaking) {\n\t\t\tconst auto was = *participant;\n\t\t\tparticipant->speaking = false;\n\t\t\t_participantUpdates.fire({\n\t\t\t\t.was = was,\n\t\t\t\t.now = *participant,\n\t\t\t});\n\t\t}\n\t}\n\tif (nearest) {\n\t\t_speakingByActiveFinishTimer.callOnce(nearest - now);\n\t}\n}\n\nvoid GroupCall::requestUnknownParticipants() {\n\tif (_unknownParticipantPeersRequestId\n\t\t|| (_unknownSpokenSsrcs.empty() && _unknownSpokenPeerIds.empty())) {\n\t\treturn;\n\t}\n\tconst auto ssrcs = [&] {\n\t\tif (_unknownSpokenSsrcs.size() < kRequestPerPage) {\n\t\t\treturn base::take(_unknownSpokenSsrcs);\n\t\t}\n\t\tauto result = base::flat_map<uint32, LastSpokeTimes>();\n\t\tresult.reserve(kRequestPerPage);\n\t\twhile (result.size() < kRequestPerPage) {\n\t\t\tconst auto &[ssrc, when] = _unknownSpokenSsrcs.back();\n\t\t\tresult.emplace(ssrc, when);\n\t\t\t_unknownSpokenSsrcs.erase(_unknownSpokenSsrcs.end() - 1);\n\t\t}\n\t\treturn result;\n\t}();\n\tconst auto participantPeerIds = [&] {\n\t\tif (_unknownSpokenPeerIds.size() + ssrcs.size() < kRequestPerPage) {\n\t\t\treturn base::take(_unknownSpokenPeerIds);\n\t\t}\n\t\tauto result = base::flat_map<PeerId, LastSpokeTimes>();\n\t\tconst auto available = (kRequestPerPage - int(ssrcs.size()));\n\t\tif (available > 0) {\n\t\t\tresult.reserve(available);\n\t\t\twhile (result.size() < available) {\n\t\t\t\tconst auto &back = _unknownSpokenPeerIds.back();\n\t\t\t\tconst auto &[participantPeerId, when] = back;\n\t\t\t\tresult.emplace(participantPeerId, when);\n\t\t\t\t_unknownSpokenPeerIds.erase(_unknownSpokenPeerIds.end() - 1);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}();\n\tauto ssrcInputs = QVector<MTPint>();\n\tssrcInputs.reserve(ssrcs.size());\n\tfor (const auto &[ssrc, when] : ssrcs) {\n\t\tssrcInputs.push_back(MTP_int(ssrc));\n\t}\n\tauto peerInputs = QVector<MTPInputPeer>();\n\tpeerInputs.reserve(participantPeerIds.size());\n\tfor (const auto &[participantPeerId, when] : participantPeerIds) {\n\t\tif (const auto userId = peerToUser(participantPeerId)) {\n\t\t\tpeerInputs.push_back(\n\t\t\t\tMTP_inputPeerUser(MTP_long(userId.bare), MTP_long(0)));\n\t\t} else if (const auto chatId = peerToChat(participantPeerId)) {\n\t\t\tpeerInputs.push_back(MTP_inputPeerChat(MTP_long(chatId.bare)));\n\t\t} else if (const auto channelId = peerToChannel(participantPeerId)) {\n\t\t\tpeerInputs.push_back(\n\t\t\t\tMTP_inputPeerChannel(MTP_long(channelId.bare), MTP_long(0)));\n\t\t}\n\t}\n\t_unknownParticipantPeersRequestId = api().request(\n\t\tMTPphone_GetGroupParticipants(\n\t\t\tinput(),\n\t\t\tMTP_vector<MTPInputPeer>(peerInputs),\n\t\t\tMTP_vector<MTPint>(ssrcInputs),\n\t\t\tMTP_string(QString()),\n\t\t\tMTP_int(kRequestPerPage)\n\t\t)\n\t).done([=](const MTPphone_GroupParticipants &result) {\n\t\tresult.match([&](const MTPDphone_groupParticipants &data) {\n\t\t\t_peer->owner().processUsers(data.vusers());\n\t\t\t_peer->owner().processChats(data.vchats());\n\t\t\tapplyParticipantsSlice(\n\t\t\t\tdata.vparticipants().v,\n\t\t\t\tApplySliceSource::UnknownLoaded);\n\t\t});\n\t\t_unknownParticipantPeersRequestId = 0;\n\t\tconst auto now = crl::now();\n\t\tfor (const auto &[ssrc, when] : ssrcs) {\n\t\t\tif (when.voice || when.anything) {\n\t\t\t\tapplyLastSpoke(ssrc, when, now);\n\t\t\t}\n\t\t\t_unknownSpokenSsrcs.remove(ssrc);\n\t\t}\n\t\tfor (const auto &[id, when] : participantPeerIds) {\n\t\t\tif (const auto participantPeer = _peer->owner().peerLoaded(id)) {\n\t\t\t\tconst auto isParticipant = ranges::contains(\n\t\t\t\t\t_participants,\n\t\t\t\t\tnot_null{ participantPeer },\n\t\t\t\t\t&Participant::peer);\n\t\t\t\tif (isParticipant) {\n\t\t\t\t\tapplyActiveUpdate(id, when, participantPeer);\n\t\t\t\t}\n\t\t\t}\n\t\t\t_unknownSpokenPeerIds.remove(id);\n\t\t}\n\t\tif (!ssrcs.empty()) {\n\t\t\t_participantsResolved.fire(&ssrcs);\n\t\t}\n\t\trequestUnknownParticipants();\n\t}).fail([=] {\n\t\t_unknownParticipantPeersRequestId = 0;\n\t\tfor (const auto &[ssrc, when] : ssrcs) {\n\t\t\t_unknownSpokenSsrcs.remove(ssrc);\n\t\t}\n\t\tfor (const auto &[participantPeerId, when] : participantPeerIds) {\n\t\t\t_unknownSpokenPeerIds.remove(participantPeerId);\n\t\t}\n\t\trequestUnknownParticipants();\n\t}).send();\n}\n\nvoid GroupCall::setInCall() {\n\t_unknownSpokenPeerIds.clear();\n\tif (_speakingByActiveFinishes.empty()) {\n\t\treturn;\n\t}\n\tauto restartTimer = true;\n\tconst auto latest = crl::now() + kActiveAfterJoined;\n\tfor (auto &[peer, when] : _speakingByActiveFinishes) {\n\t\tif (when > latest) {\n\t\t\twhen = latest;\n\t\t} else {\n\t\t\trestartTimer = false;\n\t\t}\n\t}\n\tif (restartTimer) {\n\t\t_speakingByActiveFinishTimer.callOnce(kActiveAfterJoined);\n\t}\n}\n\nbool GroupCall::inCall() const {\n\tconst auto current = Core::App().calls().currentGroupCall();\n\treturn (current != nullptr)\n\t\t&& (current->id() == _id)\n\t\t&& (current->state() == Calls::GroupCall::State::Joined);\n}\n\nvoid GroupCall::setJoinMutedLocally(bool muted) {\n\t_joinMuted = muted;\n}\n\nbool GroupCall::joinMuted() const {\n\treturn _joinMuted;\n}\n\nbool GroupCall::canChangeJoinMuted() const {\n\treturn _canChangeJoinMuted;\n}\n\nbool GroupCall::joinedToTop() const {\n\treturn _joinedToTop;\n}\n\nvoid GroupCall::setMessagesEnabledLocally(bool enabled) {\n\t_messagesEnabled = enabled;\n}\n\nApiWrap &GroupCall::api() const {\n\treturn session().api();\n}\n\nvoid GroupCall::saveSendAs(not_null<PeerData*> peer) {\n\t_savedSendAs = peer;\n\tapi().request(MTPphone_SaveDefaultSendAs(\n\t\tinput(),\n\t\tpeer->input()\n\t)).send();\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_group_call.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n\nclass PeerData;\n\nclass ApiWrap;\n\nnamespace Calls {\nstruct ParticipantVideoParams;\n} // namespace Calls\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace TdE2E {\nstruct ParticipantState;\nstruct UserId;\n} // namespace TdE2E\n\nnamespace Data {\n\n[[nodiscard]] const std::string &RtmpEndpointId();\n\nstruct LastSpokeTimes {\n\tcrl::time anything = 0;\n\tcrl::time voice = 0;\n};\n\nstruct GroupCallParticipant {\n\tnot_null<PeerData*> peer;\n\tstd::shared_ptr<Calls::ParticipantVideoParams> videoParams;\n\tTimeId date = 0;\n\tTimeId lastActive = 0;\n\tuint64 raisedHandRating = 0;\n\tuint32 ssrc = 0;\n\tint volume = 0;\n\tbool sounding : 1 = false;\n\tbool speaking : 1 = false;\n\tbool additionalSounding : 1 = false;\n\tbool additionalSpeaking : 1 = false;\n\tbool muted : 1 = false;\n\tbool mutedByMe : 1 = false;\n\tbool canSelfUnmute : 1 = false;\n\tbool onlyMinLoaded : 1 = false;\n\tbool videoJoined = false;\n\tbool applyVolumeFromMin = true;\n\n\t[[nodiscard]] const std::string &cameraEndpoint() const;\n\t[[nodiscard]] const std::string &screenEndpoint() const;\n\t[[nodiscard]] bool cameraPaused() const;\n\t[[nodiscard]] bool screenPaused() const;\n};\n\nenum class GroupCallOrigin : uchar {\n\tGroup,\n\tConference,\n\tVideoStream,\n};\n\nclass GroupCall final {\npublic:\n\tGroupCall(\n\t\tnot_null<PeerData*> peer,\n\t\tCallId id,\n\t\tuint64 accessHash,\n\t\tTimeId scheduleDate,\n\t\tbool rtmp,\n\t\tGroupCallOrigin origin);\n\t~GroupCall();\n\n\t[[nodiscard]] Main::Session &session() const;\n\n\t[[nodiscard]] CallId id() const;\n\t[[nodiscard]] bool loaded() const;\n\t[[nodiscard]] rpl::producer<bool> loadedValue() const;\n\t[[nodiscard]] bool rtmp() const;\n\t[[nodiscard]] GroupCallOrigin origin() const;\n\t[[nodiscard]] bool creator() const;\n\t[[nodiscard]] bool canManage() const;\n\t[[nodiscard]] bool listenersHidden() const;\n\t[[nodiscard]] bool blockchainMayBeEmpty() const;\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\t[[nodiscard]] MTPInputGroupCall input() const;\n\t[[nodiscard]] QString title() const {\n\t\treturn _title.current();\n\t}\n\t[[nodiscard]] rpl::producer<QString> titleValue() const {\n\t\treturn _title.value();\n\t}\n\tvoid setTitle(const QString &title) {\n\t\t_title = title;\n\t}\n\t[[nodiscard]] TimeId recordStartDate() const {\n\t\treturn _recordStartDate.current();\n\t}\n\t[[nodiscard]] rpl::producer<TimeId> recordStartDateValue() const {\n\t\treturn _recordStartDate.value();\n\t}\n\t[[nodiscard]] rpl::producer<TimeId> recordStartDateChanges() const {\n\t\treturn _recordStartDate.changes();\n\t}\n\t[[nodiscard]] TimeId scheduleDate() const {\n\t\treturn _scheduleDate.current();\n\t}\n\t[[nodiscard]] rpl::producer<TimeId> scheduleDateValue() const {\n\t\treturn _scheduleDate.value();\n\t}\n\t[[nodiscard]] rpl::producer<TimeId> scheduleDateChanges() const {\n\t\treturn _scheduleDate.changes();\n\t}\n\t[[nodiscard]] bool scheduleStartSubscribed() const {\n\t\treturn _scheduleStartSubscribed.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> scheduleStartSubscribedValue() const {\n\t\treturn _scheduleStartSubscribed.value();\n\t}\n\t[[nodiscard]] int unmutedVideoLimit() const {\n\t\treturn _unmutedVideoLimit.current();\n\t}\n\t[[nodiscard]] bool recordVideo() const {\n\t\treturn _recordVideo;\n\t}\n\n\tvoid setPeer(not_null<PeerData*> peer);\n\n\tusing Participant = GroupCallParticipant;\n\tstruct ParticipantUpdate {\n\t\tstd::optional<Participant> was;\n\t\tstd::optional<Participant> now;\n\t};\n\n\tstatic constexpr auto kSoundStatusKeptFor = crl::time(1500);\n\n\t[[nodiscard]] auto participants() const\n\t\t-> const std::vector<Participant> &;\n\tvoid requestParticipants();\n\t[[nodiscard]] bool participantsLoaded() const;\n\t[[nodiscard]] PeerData *participantPeerByAudioSsrc(uint32 ssrc) const;\n\t[[nodiscard]] const Participant *participantByPeer(\n\t\tnot_null<PeerData*> peer) const;\n\t[[nodiscard]] const Participant *participantByEndpoint(\n\t\tconst std::string &endpoint) const;\n\n\t[[nodiscard]] rpl::producer<> participantsReloaded();\n\t[[nodiscard]] auto participantUpdated() const\n\t\t-> rpl::producer<ParticipantUpdate>;\n\t[[nodiscard]] auto participantSpeaking() const\n\t\t-> rpl::producer<not_null<Participant*>>;\n\n\tvoid setParticipantsWithAccess(base::flat_set<UserId> list);\n\t[[nodiscard]] auto participantsWithAccessCurrent() const\n\t\t-> const base::flat_set<UserId> &;\n\t[[nodiscard]] auto participantsWithAccessValue() const\n\t\t-> rpl::producer<base::flat_set<UserId>>;\n\t[[nodiscard]] auto staleParticipantIds() const\n\t\t-> rpl::producer<base::flat_set<UserId>>;\n\tvoid setParticipantsLoaded();\n\tvoid checkStaleParticipants();\n\n\tvoid enqueueUpdate(const MTPUpdate &update);\n\tvoid applyLocalUpdate(\n\t\tconst MTPDupdateGroupCallParticipants &update);\n\n\tvoid applyLastSpoke(uint32 ssrc, LastSpokeTimes when, crl::time now);\n\tvoid applyActiveUpdate(\n\t\tPeerId participantPeerId,\n\t\tLastSpokeTimes when,\n\t\tPeerData *participantPeerLoaded);\n\n\tvoid resolveParticipants(const base::flat_set<uint32> &ssrcs);\n\t[[nodiscard]] rpl::producer<\n\t\tnot_null<const base::flat_map<\n\t\t\tuint32,\n\t\t\tLastSpokeTimes>*>> participantsResolved() const {\n\t\treturn _participantsResolved.events();\n\t}\n\n\t[[nodiscard]] int fullCount() const;\n\t[[nodiscard]] rpl::producer<int> fullCountValue() const;\n\t[[nodiscard]] QString conferenceInviteLink() const;\n\n\tvoid setInCall();\n\tvoid reload();\n\tvoid reloadIfStale();\n\tvoid processFullCall(const MTPphone_GroupCall &call);\n\n\tvoid setJoinMutedLocally(bool muted);\n\t[[nodiscard]] bool joinMuted() const;\n\t[[nodiscard]] bool canChangeJoinMuted() const;\n\t[[nodiscard]] bool joinedToTop() const;\n\n\tvoid setMessagesEnabledLocally(bool enabled);\n\t[[nodiscard]] bool canChangeMessagesEnabled() const {\n\t\treturn _canChangeMessagesEnabled;\n\t}\n\t[[nodiscard]] bool messagesEnabled() const {\n\t\treturn _messagesEnabled.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> messagesEnabledValue() const {\n\t\treturn _messagesEnabled.value();\n\t}\n\t[[nodiscard]] int messagesMinPrice() const {\n\t\treturn _messagesMinPrice.current();\n\t}\n\t[[nodiscard]] rpl::producer<int> messagesMinPriceValue() const {\n\t\treturn _messagesMinPrice.value();\n\t}\n\n\t[[nodiscard]] not_null<PeerData*> resolveSendAs() const {\n\t\treturn _savedSendAs.current();\n\t}\n\t[[nodiscard]] rpl::producer<not_null<PeerData*>> sendAsValue() const {\n\t\treturn _savedSendAs.value();\n\t}\n\tvoid saveSendAs(not_null<PeerData*> peer);\n\nprivate:\n\tenum class ApplySliceSource {\n\t\tFullReloaded,\n\t\tSliceLoaded,\n\t\tUnknownLoaded,\n\t\tUpdateReceived,\n\t\tUpdateConstructed,\n\t};\n\tenum class QueuedType : uint8 {\n\t\tVersionedParticipant,\n\t\tParticipant,\n\t\tCall,\n\t};\n\t[[nodiscard]] ApiWrap &api() const;\n\n\tvoid discard(const MTPDgroupCallDiscarded &data);\n\t[[nodiscard]] bool inCall() const;\n\tvoid applyParticipantsSlice(\n\t\tconst QVector<MTPGroupCallParticipant> &list,\n\t\tApplySliceSource sliceSource);\n\tvoid requestUnknownParticipants();\n\tvoid changePeerEmptyCallFlag();\n\tvoid checkFinishSpeakingByActive();\n\tvoid applyCallFields(const MTPDgroupCall &data);\n\tvoid applyEnqueuedUpdate(const MTPUpdate &update);\n\tvoid setServerParticipantsCount(int count);\n\tvoid computeParticipantsCount();\n\tvoid processQueuedUpdates(bool initial = false);\n\tvoid processFullCallUsersChats(const MTPphone_GroupCall &call);\n\tvoid processFullCallFields(const MTPphone_GroupCall &call);\n\t[[nodiscard]] bool requestParticipantsAfterReload(\n\t\tconst MTPphone_GroupCall &call) const;\n\t[[nodiscard]] bool processSavedFullCall();\n\tvoid finishParticipantsSliceRequest();\n\t[[nodiscard]] Participant *findParticipant(not_null<PeerData*> peer);\n\n\tconst CallId _id = 0;\n\tconst uint64 _accessHash = 0;\n\n\tnot_null<PeerData*> _peer;\n\tint _version = 0;\n\trpl::event_stream<bool> _loadedChanges;\n\tmtpRequestId _participantsRequestId = 0;\n\tmtpRequestId _reloadRequestId = 0;\n\tcrl::time _reloadLastFinished = 0;\n\trpl::variable<QString> _title;\n\tQString _conferenceInviteLink;\n\n\tbase::flat_multi_map<\n\t\tstd::pair<int, QueuedType>,\n\t\tMTPUpdate> _queuedUpdates;\n\tbase::Timer _reloadByQueuedUpdatesTimer;\n\tstd::optional<MTPphone_GroupCall> _savedFull;\n\n\tstd::vector<Participant> _participants;\n\tbase::flat_map<uint32, not_null<PeerData*>> _participantPeerByAudioSsrc;\n\tbase::flat_map<not_null<PeerData*>, crl::time> _speakingByActiveFinishes;\n\tbase::Timer _speakingByActiveFinishTimer;\n\tQString _nextOffset;\n\tint _serverParticipantsCount = 0;\n\trpl::variable<int> _fullCount = 0;\n\trpl::variable<int> _unmutedVideoLimit = 0;\n\trpl::variable<bool> _messagesEnabled = false;\n\trpl::variable<int> _messagesMinPrice = 0;\n\trpl::variable<TimeId> _recordStartDate = 0;\n\trpl::variable<TimeId> _scheduleDate = 0;\n\trpl::variable<bool> _scheduleStartSubscribed = false;\n\n\tbase::flat_map<uint32, LastSpokeTimes> _unknownSpokenSsrcs;\n\tbase::flat_map<PeerId, LastSpokeTimes> _unknownSpokenPeerIds;\n\trpl::event_stream<\n\t\tnot_null<const base::flat_map<\n\t\t\tuint32,\n\t\t\tLastSpokeTimes>*>> _participantsResolved;\n\tmtpRequestId _unknownParticipantPeersRequestId = 0;\n\n\trpl::event_stream<ParticipantUpdate> _participantUpdates;\n\trpl::event_stream<not_null<Participant*>> _participantSpeaking;\n\trpl::event_stream<> _participantsReloaded;\n\n\trpl::variable<base::flat_set<UserId>> _participantsWithAccess;\n\trpl::event_stream<base::flat_set<UserId>> _staleParticipantIds;\n\trpl::lifetime _checkStaleLifetime;\n\n\trpl::variable<not_null<PeerData*>> _savedSendAs;\n\n\tbool _creator : 1 = false;\n\tbool _joinMuted : 1 = false;\n\tbool _recordVideo : 1 = false;\n\tbool _canChangeJoinMuted : 1 = true;\n\tbool _canChangeMessagesEnabled : 1 = true;\n\tbool _allParticipantsLoaded : 1 = false;\n\tbool _joinedToTop : 1 = false;\n\tbool _applyingQueuedUpdates : 1 = false;\n\tbool _rtmp : 1 = false;\n\tbool _conference : 1 = false;\n\tbool _videoStream : 1 = false;\n\tbool _listenersHidden : 1 = false;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_groups.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_groups.h\"\n\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"dialogs/ui/dialogs_message_view.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_session.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kMaxItemsInGroup = 10;\n\n} // namespace\n\nGroups::Groups(not_null<Session*> data) : _data(data) {\n}\n\nbool Groups::isGrouped(not_null<const HistoryItem*> item) const {\n\tif (!item->groupId()) {\n\t\treturn false;\n\t}\n\tconst auto media = item->media();\n\treturn media && media->canBeGrouped();\n}\n\nbool Groups::isGroupOfOne(not_null<const HistoryItem*> item) const {\n\tif (const auto groupId = item->groupId()) {\n\t\tconst auto i = _groups.find(groupId);\n\t\treturn (i != _groups.end()) && (i->second.items.size() == 1);\n\t}\n\treturn false;\n}\n\nvoid Groups::registerMessage(not_null<HistoryItem*> item) {\n\tif (!isGrouped(item)) {\n\t\treturn;\n\t}\n\tconst auto i = _groups.emplace(item->groupId(), Group()).first;\n\tauto &items = i->second.items;\n\tif (items.size() < kMaxItemsInGroup) {\n\t\titems.insert(findPositionForItem(items, item), item);\n\t\tif (items.size() > 1) {\n\t\t\trefreshViews(items);\n\t\t}\n\t}\n}\n\nvoid Groups::unregisterMessage(not_null<const HistoryItem*> item) {\n\tconst auto groupId = item->groupId();\n\tif (!groupId) {\n\t\treturn;\n\t}\n\tconst auto i = _groups.find(groupId);\n\tif (i != end(_groups)) {\n\t\tauto &items = i->second.items;\n\t\tconst auto removed = ranges::remove(items, item);\n\t\tconst auto last = end(items);\n\t\tif (removed != last) {\n\t\t\titems.erase(removed, last);\n\t\t\tif (!items.empty()) {\n\t\t\t\trefreshViews(items);\n\t\t\t} else {\n\t\t\t\t_groups.erase(i);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Groups::refreshMessage(\n\t\tnot_null<HistoryItem*> item,\n\t\tbool justRefreshViews) {\n\tif (!isGrouped(item)) {\n\t\tunregisterMessage(item);\n\t\t_data->requestItemViewRefresh(item);\n\t\treturn;\n\t}\n\tif (!item->isRegular() && !item->isScheduled() && !item->isUploading()) {\n\t\treturn;\n\t}\n\tconst auto groupId = item->groupId();\n\tconst auto i = _groups.find(groupId);\n\tif (i == end(_groups)) {\n\t\tregisterMessage(item);\n\t\treturn;\n\t}\n\tauto &items = i->second.items;\n\n\tif (justRefreshViews) {\n\t\trefreshViews(items);\n\t\treturn;\n\t}\n\n\tconst auto position = findPositionForItem(items, item);\n\tauto current = ranges::find(items, item);\n\tif (current == end(items)) {\n\t\titems.insert(position, item);\n\t} else if (position == current + 1) {\n\t\treturn;\n\t} else if (position > current + 1) {\n\t\tfor (++current; current != position; ++current) {\n\t\t\tstd::swap(*(current - 1), *current);\n\t\t}\n\t} else if (position < current) {\n\t\tfor (; current != position; --current) {\n\t\t\tstd::swap(*(current - 1), *current);\n\t\t}\n\t} else {\n\t\tUnexpected(\"Position of item in Groups::refreshMessage().\");\n\t}\n\trefreshViews(items);\n}\n\nHistoryItemsList::const_iterator Groups::findPositionForItem(\n\t\tconst HistoryItemsList &group,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto last = end(group);\n\tconst auto itemId = item->id;\n\tfor (auto result = begin(group); result != last; ++result) {\n\t\tif ((*result)->id > itemId) {\n\t\t\treturn result;\n\t\t}\n\t}\n\treturn last;\n}\n\nconst Group *Groups::find(not_null<const HistoryItem*> item) const {\n\tconst auto groupId = item->groupId();\n\tif (!groupId) {\n\t\treturn nullptr;\n\t}\n\tconst auto i = _groups.find(groupId);\n\tif (i != _groups.end()) {\n\t\tconst auto &result = i->second;\n\t\tif (result.items.size() > 1) {\n\t\t\treturn &result;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid Groups::refreshViews(const HistoryItemsList &items) {\n\tif (items.empty()) {\n\t\treturn;\n\t}\n\tfor (const auto &item : items) {\n\t\t_data->requestItemViewRefresh(item);\n\t\titem->invalidateChatListEntry();\n\t}\n}\n\nnot_null<HistoryItem*> Groups::findItemToEdit(\n\t\tnot_null<HistoryItem*> item) const {\n\tconst auto group = find(item);\n\tif (!group) {\n\t\treturn item;\n\t}\n\tconst auto &list = group->items;\n\tconst auto it = ranges::find_if(\n\t\tlist,\n\t\tranges::not_fn(&HistoryItem::emptyText));\n\tif (it == end(list)) {\n\t\treturn list.front();\n\t}\n\treturn (*it);\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_groups.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_types.h\"\n\nnamespace Data {\n\nclass Session;\n\nstruct Group {\n\tHistoryItemsList items;\n};\n\nclass Groups {\npublic:\n\tGroups(not_null<Session*> data);\n\n\t[[nodiscard]] bool isGrouped(not_null<const HistoryItem*> item) const;\n\t[[nodiscard]] bool isGroupOfOne(not_null<const HistoryItem*> item) const;\n\tvoid registerMessage(not_null<HistoryItem*> item);\n\tvoid unregisterMessage(not_null<const HistoryItem*> item);\n\tvoid refreshMessage(\n\t\tnot_null<HistoryItem*> item,\n\t\tbool justRefreshViews = false);\n\n\t[[nodiscard]] const Group *find(not_null<const HistoryItem*> item) const;\n\n\tnot_null<HistoryItem*> findItemToEdit(not_null<HistoryItem*> item) const;\n\nprivate:\n\tHistoryItemsList::const_iterator findPositionForItem(\n\t\tconst HistoryItemsList &group,\n\t\tnot_null<HistoryItem*> item);\n\tvoid refreshViews(const HistoryItemsList &items);\n\n\tnot_null<Session*> _data;\n\tstd::map<MessageGroupId, Group> _groups;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_histories.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_histories.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/components/scheduled_messages.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_document.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_saved_music.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"base/unixtime.h\"\n#include \"base/random.h\"\n#include \"main/main_session.h\"\n#include \"window/notifications_manager.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/view/history_view_element.h\"\n#include \"core/application.h\"\n#include \"apiwrap.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kReadRequestTimeout = 3 * crl::time(1000);\nconstexpr auto kReportDeliveriesPerRequest = 50;\n\n} // namespace\n\nMTPInputReplyTo ReplyToForMTP(\n\t\tnot_null<History*> history,\n\t\tFullReplyTo replyTo) {\n\tconst auto owner = &history->owner();\n\tif (replyTo.storyId) {\n\t\tif (const auto peer = owner->peerLoaded(replyTo.storyId.peer)) {\n\t\t\treturn MTP_inputReplyToStory(\n\t\t\t\tpeer->input(),\n\t\t\t\tMTP_int(replyTo.storyId.story));\n\t\t}\n\t} else if (replyTo.messageId || replyTo.topicRootId) {\n\t\tconst auto to = LookupReplyTo(history, replyTo.messageId);\n\t\tconst auto replyingToTopic = replyTo.topicRootId\n\t\t\t? history->peer->forumTopicFor(replyTo.topicRootId)\n\t\t\t: nullptr;\n\t\tconst auto replyingToTopicId = replyTo.topicRootId\n\t\t\t? (replyingToTopic\n\t\t\t\t? replyingToTopic->rootId()\n\t\t\t\t: Data::ForumTopic::kGeneralId)\n\t\t\t: (to ? to->topicRootId() : Data::ForumTopic::kGeneralId);\n\t\tconst auto replyToTopicId = (to\n\t\t\t&& (to->history() != history || to->id != replyingToTopicId))\n\t\t\t? to->topicRootId()\n\t\t\t: replyingToTopicId;\n\t\tconst auto possibleMonoforumPeerId = (to && to->sublistPeerId())\n\t\t\t? to->sublistPeerId()\n\t\t\t: replyTo.monoforumPeerId\n\t\t\t? replyTo.monoforumPeerId\n\t\t\t: history->session().user()->id;\n\t\tconst auto replyToMonoforumPeerId = history->peer->amMonoforumAdmin()\n\t\t\t? possibleMonoforumPeerId\n\t\t\t: PeerId();\n\t\tconst auto external = replyTo.messageId\n\t\t\t&& (replyTo.messageId.peer != history->peer->id\n\t\t\t\t|| replyingToTopicId != replyToTopicId);\n\t\tconst auto quoteEntities = Api::EntitiesToMTP(\n\t\t\t&history->session(),\n\t\t\treplyTo.quote.entities,\n\t\t\tApi::ConvertOption::SkipLocal);\n\t\tusing Flag = MTPDinputReplyToMessage::Flag;\n\t\treturn MTP_inputReplyToMessage(\n\t\t\tMTP_flags((replyTo.topicRootId ? Flag::f_top_msg_id : Flag())\n\t\t\t\t| (external ? Flag::f_reply_to_peer_id : Flag())\n\t\t\t\t| (replyTo.quote.text.isEmpty()\n\t\t\t\t\t? Flag()\n\t\t\t\t\t: (Flag::f_quote_text | Flag::f_quote_offset))\n\t\t\t\t| (replyToMonoforumPeerId\n\t\t\t\t\t? Flag::f_monoforum_peer_id\n\t\t\t\t\t: Flag())\n\t\t\t\t| (quoteEntities.v.isEmpty()\n\t\t\t\t\t? Flag()\n\t\t\t\t\t: Flag::f_quote_entities)\n\t\t\t\t| (replyTo.todoItemId ? Flag::f_todo_item_id : Flag())\n\t\t\t| (replyTo.pollOption.isEmpty()\n\t\t\t\t? Flag()\n\t\t\t\t: Flag::f_poll_option)),\n\t\t\tMTP_int(replyTo.messageId ? replyTo.messageId.msg : 0),\n\t\t\tMTP_int(replyTo.topicRootId),\n\t\t\t(external\n\t\t\t\t? owner->peer(replyTo.messageId.peer)->input()\n\t\t\t\t: MTPInputPeer()),\n\t\t\tMTP_string(replyTo.quote.text),\n\t\t\tquoteEntities,\n\t\t\tMTP_int(replyTo.quoteOffset),\n\t\t\t(replyToMonoforumPeerId\n\t\t\t\t? history->owner().peer(replyToMonoforumPeerId)->input()\n\t\t\t\t: MTPInputPeer()),\n\t\t\tMTP_int(replyTo.todoItemId),\n\t\t\tMTP_bytes(replyTo.pollOption));\n\t} else if (history->peer->amMonoforumAdmin()\n\t\t&& replyTo.monoforumPeerId) {\n\t\tconst auto replyToMonoforumPeer = replyTo.monoforumPeerId\n\t\t\t? history->owner().peer(replyTo.monoforumPeerId)\n\t\t\t: history->session().user();\n\t\treturn MTP_inputReplyToMonoForum(replyToMonoforumPeer->input());\n\t}\n\treturn MTPInputReplyTo();\n}\n\nMTPInputMedia WebPageForMTP(\n\t\tconst Data::WebPageDraft &draft,\n\t\tbool required) {\n\tusing Flag = MTPDinputMediaWebPage::Flag;\n\treturn MTP_inputMediaWebPage(\n\t\tMTP_flags(((false && required) ? Flag() : Flag::f_optional)\n\t\t\t| (draft.forceLargeMedia ? Flag::f_force_large_media : Flag())\n\t\t\t| (draft.forceSmallMedia ? Flag::f_force_small_media : Flag())),\n\t\tMTP_string(draft.url));\n}\n\nHistories::Histories(not_null<Session*> owner)\n: _owner(owner)\n, _readRequestsTimer([=] { sendReadRequests(); }) {\n}\n\nSession &Histories::owner() const {\n\treturn *_owner;\n}\n\nMain::Session &Histories::session() const {\n\treturn _owner->session();\n}\n\nHistory *Histories::find(PeerId peerId) {\n\tconst auto i = peerId ? _map.find(peerId) : end(_map);\n\treturn (i != end(_map)) ? i->second.get() : nullptr;\n}\n\nnot_null<History*> Histories::findOrCreate(PeerId peerId) {\n\tExpects(peerId != 0);\n\n\tif (const auto result = find(peerId)) {\n\t\treturn result;\n\t}\n\tconst auto &[i, ok] = _map.emplace(\n\t\tpeerId,\n\t\tstd::make_unique<History>(&owner(), peerId));\n\treturn i->second.get();\n}\n\nvoid Histories::unloadAll() {\n\tfor (const auto &[peerId, history] : _map) {\n\t\thistory->clear(History::ClearType::Unload);\n\t}\n}\n\nvoid Histories::clearAll() {\n\t_map.clear();\n}\n\nvoid Histories::readInbox(not_null<History*> history) {\n\tDEBUG_LOG((\"Reading: readInbox called.\"));\n\tif (history->lastServerMessageKnown()) {\n\t\tconst auto last = history->lastServerMessage();\n\t\tDEBUG_LOG((\"Reading: last known, reading till %1.\"\n\t\t\t).arg(last ? last->id.bare : 0));\n\t\treadInboxTill(history, last ? last->id : 0);\n\t\treturn;\n\t} else if (history->loadedAtBottom()) {\n\t\tif (const auto lastId = history->maxMsgId()) {\n\t\t\tDEBUG_LOG((\"Reading: loaded at bottom, maxMsgId %1.\"\n\t\t\t\t).arg(lastId.bare));\n\t\t\treadInboxTill(history, lastId);\n\t\t\treturn;\n\t\t} else if (history->loadedAtTop()) {\n\t\t\tDEBUG_LOG((\"Reading: loaded at bottom, loaded at top.\"));\n\t\t\treadInboxTill(history, 0);\n\t\t\treturn;\n\t\t}\n\t\tDEBUG_LOG((\"Reading: loaded at bottom, but requesting entry.\"));\n\t}\n\trequestDialogEntry(history, [=] {\n\t\tExpects(history->lastServerMessageKnown());\n\n\t\tconst auto last = history->lastServerMessage();\n\t\tDEBUG_LOG((\"Reading: got entry, reading till %1.\"\n\t\t\t).arg(last ? last->id.bare : 0));\n\t\treadInboxTill(history, last ? last->id : 0);\n\t});\n}\n\nvoid Histories::readInboxTill(not_null<HistoryItem*> item) {\n\tconst auto history = item->history();\n\tif (!item->isRegular()) {\n\t\treadClientSideMessage(item);\n\t\tauto view = item->mainView();\n\t\tif (!view) {\n\t\t\treturn;\n\t\t}\n\t\tauto block = view->block();\n\t\tauto blockIndex = block->indexInHistory();\n\t\tauto itemIndex = view->indexInBlock();\n\t\twhile (blockIndex > 0 || itemIndex > 0) {\n\t\t\tif (itemIndex > 0) {\n\t\t\t\tview = block->messages[--itemIndex].get();\n\t\t\t} else {\n\t\t\t\twhile (blockIndex > 0) {\n\t\t\t\t\tblock = history->blocks[--blockIndex].get();\n\t\t\t\t\titemIndex = block->messages.size();\n\t\t\t\t\tif (itemIndex > 0) {\n\t\t\t\t\t\tview = block->messages[--itemIndex].get();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\titem = view->data();\n\t\t\tif (item->isRegular()) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (!item->isRegular()) {\n\t\t\tLOG((\"App Error: \"\n\t\t\t\t\"Can't read history till unknown local message.\"));\n\t\t\treturn;\n\t\t}\n\t}\n\treadInboxTill(history, item->id);\n}\n\nvoid Histories::readInboxTill(not_null<History*> history, MsgId tillId) {\n\treadInboxTill(history, tillId, false);\n}\n\nvoid Histories::readInboxTill(\n\t\tnot_null<History*> history,\n\t\tMsgId tillId,\n\t\tbool force) {\n\tExpects(IsServerMsgId(tillId) || (!tillId && !force));\n\n\tDEBUG_LOG((\"Reading: readInboxTill %1, force %2.\"\n\t\t).arg(tillId.bare\n\t\t).arg(Logs::b(force)));\n\n\tconst auto syncGuard = gsl::finally([&] {\n\t\tDEBUG_LOG((\"Reading: in guard, unread %1.\"\n\t\t\t).arg(history->unreadCount()));\n\t\tif (history->unreadCount() > 0) {\n\t\t\tif (const auto last = history->lastServerMessage()) {\n\t\t\t\tDEBUG_LOG((\"Reading: checking last %1 and %2.\"\n\t\t\t\t\t).arg(last->id.bare\n\t\t\t\t\t).arg(tillId.bare));\n\t\t\t\tif (last->id == tillId) {\n\t\t\t\t\tDEBUG_LOG((\"Reading: locally marked as read.\"));\n\t\t\t\t\thistory->setUnreadCount(0);\n\t\t\t\t\thistory->updateChatListEntry();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\n\tCore::App().notifications().clearIncomingFromHistory(history);\n\n\tconst auto needsRequest = history->readInboxTillNeedsRequest(tillId);\n\tif (!needsRequest && !force) {\n\t\tDEBUG_LOG((\"Reading: readInboxTill finish 1.\"));\n\t\treturn;\n\t} else if (!history->trackUnreadMessages()) {\n\t\tDEBUG_LOG((\"Reading: readInboxTill finish 2.\"));\n\t\treturn;\n\t}\n\tconst auto maybeState = lookup(history);\n\tif (maybeState && maybeState->sentReadTill >= tillId) {\n\t\tDEBUG_LOG((\"Reading: readInboxTill finish 3 with %1.\"\n\t\t\t).arg(maybeState->sentReadTill.bare));\n\t\treturn;\n\t} else if (maybeState && maybeState->willReadTill >= tillId) {\n\t\tDEBUG_LOG((\"Reading: readInboxTill finish 4 with %1 and force %2.\"\n\t\t\t).arg(maybeState->sentReadTill.bare\n\t\t\t).arg(Logs::b(force)));\n\t\tif (force) {\n\t\t\tsendPendingReadInbox(history);\n\t\t}\n\t\treturn;\n\t} else if (!needsRequest\n\t\t&& (!maybeState || !maybeState->willReadTill)) {\n\t\treturn;\n\t}\n\tconst auto stillUnread = history->countStillUnreadLocal(tillId);\n\tif (!force\n\t\t&& stillUnread\n\t\t&& history->unreadCountKnown()\n\t\t&& *stillUnread == history->unreadCount()) {\n\t\tDEBUG_LOG((\"Reading: count didn't change so just update till %1\"\n\t\t\t).arg(tillId.bare));\n\t\thistory->setInboxReadTill(tillId);\n\t\treturn;\n\t}\n\tauto &state = maybeState ? *maybeState : _states[history];\n\tstate.willReadTill = tillId;\n\tif (force || !stillUnread || !*stillUnread) {\n\t\tDEBUG_LOG((\"Reading: will read till %1 with still unread %2\"\n\t\t\t).arg(tillId.bare\n\t\t\t).arg(stillUnread.value_or(-666)));\n\t\tstate.willReadWhen = 0;\n\t\tsendReadRequests();\n\t\tif (!stillUnread) {\n\t\t\treturn;\n\t\t}\n\t} else if (!state.willReadWhen) {\n\t\tDEBUG_LOG((\"Reading: will read till %1 with postponed\"\n\t\t\t).arg(tillId.bare));\n\t\tstate.willReadWhen = crl::now() + kReadRequestTimeout;\n\t\tif (!_readRequestsTimer.isActive()) {\n\t\t\t_readRequestsTimer.callOnce(kReadRequestTimeout);\n\t\t}\n\t} else {\n\t\tDEBUG_LOG((\"Reading: will read till %1 postponed already\"\n\t\t\t).arg(tillId.bare));\n\t}\n\tDEBUG_LOG((\"Reading: marking now with till %1 and still %2\"\n\t\t).arg(tillId.bare\n\t\t).arg(*stillUnread));\n\thistory->setInboxReadTill(tillId);\n\thistory->setUnreadCount(*stillUnread);\n\thistory->updateChatListEntry();\n}\n\nvoid Histories::readInboxOnNewMessage(not_null<HistoryItem*> item) {\n\tif (!item->isRegular()) {\n\t\treadClientSideMessage(item);\n\t} else {\n\t\treadInboxTill(item->history(), item->id, true);\n\t}\n}\n\nvoid Histories::readClientSideMessage(not_null<HistoryItem*> item) {\n\tif (item->out() || !item->unread(item->history())) {\n\t\treturn;\n\t}\n\tconst auto history = item->history();\n\titem->markClientSideAsRead();\n\tif (const auto unread = history->unreadCount()) {\n\t\thistory->setUnreadCount(unread - 1);\n\t}\n}\n\nvoid Histories::requestDialogEntry(not_null<Data::Folder*> folder) {\n\tif (_dialogFolderRequests.contains(folder)) {\n\t\treturn;\n\t}\n\t_dialogFolderRequests.emplace(folder);\n\n\tauto peers = QVector<MTPInputDialogPeer>(\n\t\t1,\n\t\tMTP_inputDialogPeerFolder(MTP_int(folder->id())));\n\tsession().api().request(MTPmessages_GetPeerDialogs(\n\t\tMTP_vector(std::move(peers))\n\t)).done([=](const MTPmessages_PeerDialogs &result) {\n\t\tapplyPeerDialogs(result);\n\t\t_dialogFolderRequests.remove(folder);\n\t}).fail([=] {\n\t\t_dialogFolderRequests.remove(folder);\n\t}).send();\n}\n\nvoid Histories::requestDialogEntry(\n\t\tnot_null<History*> history,\n\t\tFn<void()> callback) {\n\tconst auto i = _dialogRequests.find(history);\n\tif (i != end(_dialogRequests)) {\n\t\tif (callback) {\n\t\t\ti->second.push_back(std::move(callback));\n\t\t}\n\t\treturn;\n\t}\n\n\tconst auto &[j, ok] = _dialogRequestsPending.try_emplace(history);\n\tif (callback) {\n\t\tj->second.push_back(std::move(callback));\n\t}\n\tif (!ok) {\n\t\treturn;\n\t}\n\tpostponeRequestDialogEntries();\n}\n\nvoid Histories::postponeRequestDialogEntries() {\n\tif (_dialogRequestsPending.size() > 1) {\n\t\treturn;\n\t}\n\tCore::App().postponeCall(crl::guard(&session(), [=] {\n\t\tsendDialogRequests();\n\t}));\n}\n\nvoid Histories::sendDialogRequests() {\n\tif (_dialogRequestsPending.empty()) {\n\t\treturn;\n\t}\n\tconst auto histories = ranges::views::all(\n\t\t_dialogRequestsPending\n\t) | ranges::views::transform([](const auto &pair) {\n\t\treturn pair.first;\n\t}) | ranges::views::filter([&](not_null<History*> history) {\n\t\tconst auto state = lookup(history);\n\t\tif (!state) {\n\t\t\treturn true;\n\t\t} else if (!postponeEntryRequest(*state)) {\n\t\t\treturn true;\n\t\t}\n\t\tstate->postponedRequestEntry = true;\n\t\treturn false;\n\t}) | ranges::to_vector;\n\n\tauto peers = QVector<MTPInputDialogPeer>();\n\tconst auto dialogPeer = [](not_null<History*> history) {\n\t\treturn MTP_inputDialogPeer(history->peer->input());\n\t};\n\tranges::transform(\n\t\thistories,\n\t\tranges::back_inserter(peers),\n\t\tdialogPeer);\n\tfor (auto &[history, callbacks] : base::take(_dialogRequestsPending)) {\n\t\t_dialogRequests.emplace(history, std::move(callbacks));\n\t}\n\n\tconst auto finalize = [=] {\n\t\tfor (const auto &history : histories) {\n\t\t\tconst auto state = lookup(history);\n\t\t\tif (!state || !state->postponedRequestEntry) {\n\t\t\t\tdialogEntryApplied(history);\n\t\t\t\thistory->updateChatListExistence();\n\t\t\t}\n\t\t}\n\t};\n\tsession().api().request(MTPmessages_GetPeerDialogs(\n\t\tMTP_vector(std::move(peers))\n\t)).done([=](const MTPmessages_PeerDialogs &result) {\n\t\tapplyPeerDialogs(result);\n\t\tfinalize();\n\t}).fail([=] {\n\t\tfinalize();\n\t}).send();\n}\n\nvoid Histories::dialogEntryApplied(not_null<History*> history) {\n\tconst auto state = lookup(history);\n\tif (state && state->postponedRequestEntry) {\n\t\treturn;\n\t}\n\thistory->dialogEntryApplied();\n\tif (const auto callbacks = _dialogRequestsPending.take(history)) {\n\t\tfor (const auto &callback : *callbacks) {\n\t\t\tcallback();\n\t\t}\n\t}\n\tif (const auto callbacks = _dialogRequests.take(history)) {\n\t\tfor (const auto &callback : *callbacks) {\n\t\t\tcallback();\n\t\t}\n\t}\n\tif (state && state->sentReadTill && state->sentReadDone) {\n\t\thistory->setInboxReadTill(base::take(state->sentReadTill));\n\t\tcheckEmptyState(history);\n\t}\n}\n\nvoid Histories::applyPeerDialogs(const MTPmessages_PeerDialogs &dialogs) {\n\tExpects(dialogs.type() == mtpc_messages_peerDialogs);\n\n\tconst auto &data = dialogs.c_messages_peerDialogs();\n\t_owner->processUsers(data.vusers());\n\t_owner->processChats(data.vchats());\n\t_owner->processMessages(data.vmessages(), NewMessageType::Last);\n\tfor (const auto &dialog : data.vdialogs().v) {\n\t\tdialog.match([&](const MTPDdialog &data) {\n\t\t\tif (const auto peerId = peerFromMTP(data.vpeer())) {\n\t\t\t\t_owner->history(peerId)->applyDialog(nullptr, data);\n\t\t\t}\n\t\t}, [&](const MTPDdialogFolder &data) {\n\t\t\tconst auto folder = _owner->processFolder(data.vfolder());\n\t\t\tfolder->applyDialog(data);\n\t\t});\n\t}\n\t_owner->sendHistoryChangeNotifications();\n}\n\nvoid Histories::changeDialogUnreadMark(\n\t\tnot_null<History*> history,\n\t\tbool unread) {\n\thistory->setUnreadMark(unread);\n\n\tusing Flag = MTPmessages_MarkDialogUnread::Flag;\n\tsession().api().request(MTPmessages_MarkDialogUnread(\n\t\tMTP_flags(unread ? Flag::f_unread : Flag(0)),\n\t\tMTPInputPeer(), // parent_peer\n\t\tMTP_inputDialogPeer(history->peer->input())\n\t)).send();\n}\n\nvoid Histories::changeSublistUnreadMark(\n\t\tnot_null<Data::SavedSublist*> sublist,\n\t\tbool unread) {\n\tconst auto parent = sublist->parentChat();\n\tif (!parent) {\n\t\treturn;\n\t}\n\tsublist->setUnreadMark(unread);\n\n\tusing Flag = MTPmessages_MarkDialogUnread::Flag;\n\tsession().api().request(MTPmessages_MarkDialogUnread(\n\t\tMTP_flags(Flag::f_parent_peer\n\t\t\t| (unread ? Flag::f_unread : Flag(0))),\n\t\tparent->input(),\n\t\tMTP_inputDialogPeer(sublist->sublistPeer()->input())\n\t)).send();\n}\n\nvoid Histories::requestFakeChatListMessage(\n\t\tnot_null<History*> history) {\n\tif (_fakeChatListRequests.contains(history)) {\n\t\treturn;\n\t}\n\n\t_fakeChatListRequests.emplace(history);\n\tsendRequest(history, RequestType::History, [=](Fn<void()> finish) {\n\t\treturn session().api().request(MTPmessages_GetHistory(\n\t\t\thistory->peer->input(),\n\t\t\tMTP_int(0), // offset_id\n\t\t\tMTP_int(0), // offset_date\n\t\t\tMTP_int(0), // add_offset\n\t\t\tMTP_int(2), // limit\n\t\t\tMTP_int(0), // max_id\n\t\t\tMTP_int(0), // min_id\n\t\t\tMTP_long(0) // hash\n\t\t)).done([=](const MTPmessages_Messages &result) {\n\t\t\t_fakeChatListRequests.erase(history);\n\t\t\thistory->setFakeChatListMessageFrom(result);\n\t\t\tfinish();\n\t\t}).fail([=] {\n\t\t\t_fakeChatListRequests.erase(history);\n\t\t\thistory->setFakeChatListMessageFrom(MTP_messages_messages(\n\t\t\t\tMTP_vector<MTPMessage>(0),\n\t\t\t\tMTP_vector<MTPForumTopic>(0),\n\t\t\t\tMTP_vector<MTPChat>(0),\n\t\t\t\tMTP_vector<MTPUser>(0)));\n\t\t\tfinish();\n\t\t}).send();\n\t});\n}\n\nvoid Histories::requestGroupAround(not_null<HistoryItem*> item) {\n\tconst auto history = item->history();\n\tconst auto id = item->id;\n\tconst auto key = GroupRequestKey{ history, item->topicRootId() };\n\tconst auto i = _chatListGroupRequests.find(key);\n\tif (i != end(_chatListGroupRequests)) {\n\t\tif (i->second.aroundId == id) {\n\t\t\treturn;\n\t\t} else {\n\t\t\tcancelRequest(i->second.requestId);\n\t\t\t_chatListGroupRequests.erase(i);\n\t\t}\n\t}\n\tconstexpr auto kMaxAlbumCount = 10;\n\tconst auto requestId = sendRequest(history, RequestType::History, [=](\n\t\t\tFn<void()> finish) {\n\t\treturn session().api().request(MTPmessages_GetHistory(\n\t\t\thistory->peer->input(),\n\t\t\tMTP_int(id),\n\t\t\tMTP_int(0), // offset_date\n\t\t\tMTP_int(-kMaxAlbumCount),\n\t\t\tMTP_int(2 * kMaxAlbumCount - 1),\n\t\t\tMTP_int(0), // max_id\n\t\t\tMTP_int(0), // min_id\n\t\t\tMTP_long(0) // hash\n\t\t)).done([=](const MTPmessages_Messages &result) {\n\t\t\t_owner->processExistingMessages(\n\t\t\t\thistory->peer->asChannel(),\n\t\t\t\tresult);\n\t\t\t_chatListGroupRequests.remove(key);\n\t\t\thistory->migrateToOrMe()->applyChatListGroup(\n\t\t\t\thistory->peer->id,\n\t\t\t\tresult);\n\t\t\tfinish();\n\t\t}).fail([=] {\n\t\t\t_chatListGroupRequests.remove(key);\n\t\t\tfinish();\n\t\t}).send();\n\t});\n\t_chatListGroupRequests.emplace(\n\t\tkey,\n\t\tChatListGroupRequest{ .aroundId = id, .requestId = requestId });\n}\n\nvoid Histories::sendPendingReadInbox(not_null<History*> history) {\n\tif (const auto state = lookup(history)) {\n\t\tDEBUG_LOG((\"Reading: send pending now with till %1 and when %2\"\n\t\t\t).arg(state->willReadTill.bare\n\t\t\t).arg(state->willReadWhen));\n\t\tif (state->willReadTill && state->willReadWhen) {\n\t\t\tstate->willReadWhen = 0;\n\t\t\tsendReadRequests();\n\t\t}\n\t}\n}\n\nvoid Histories::reportDelivery(not_null<HistoryItem*> item) {\n\tauto &set = _pendingDeliveryReport[item->history()->peer];\n\tif (!set.emplace(item->id).second) {\n\t\treturn;\n\t}\n\tcrl::on_main(&session(), [=] {\n\t\treportPendingDeliveries();\n\t});\n}\n\nvoid Histories::reportPendingDeliveries() {\n\tauto &pending = _pendingDeliveryReport;\n\tfor (auto i = begin(pending); i != end(pending);) {\n\t\tauto &[peer, ids] = *i;\n\t\tauto list = QVector<MTPint>();\n\t\tif (_deliveryReportSent.contains(peer)) {\n\t\t\t++i;\n\t\t\tcontinue;\n\t\t} else if (ids.size() > kReportDeliveriesPerRequest) {\n\t\t\tconst auto count = kReportDeliveriesPerRequest;\n\t\t\tlist.reserve(count);\n\t\t\tfor (auto j = begin(ids), till = j + count; j != till; ++j) {\n\t\t\t\tlist.push_back(MTP_int(*j));\n\t\t\t}\n\t\t\tids.erase(begin(ids), begin(ids) + count);\n\t\t} else if (!ids.empty()) {\n\t\t\tlist.reserve(ids.size());\n\t\t\tfor (const auto &id : ids) {\n\t\t\t\tlist.push_back(MTP_int(id));\n\t\t\t}\n\t\t\tids.clear();\n\t\t}\n\t\tif (ids.empty()) {\n\t\t\ti = pending.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t\t_deliveryReportSent.emplace(peer);\n\t\tconst auto finish = [=] {\n\t\t\t_deliveryReportSent.remove(peer);\n\t\t\tif (_pendingDeliveryReport.contains(peer)) {\n\t\t\t\treportPendingDeliveries();\n\t\t\t}\n\t\t};\n\t\tsession().api().request(MTPmessages_ReportMessagesDelivery(\n\t\t\tMTP_flags(0),\n\t\t\tpeer->input(),\n\t\t\tMTP_vector(std::move(list))\n\t\t)).done(finish).fail(finish).send();\n\t}\n}\n\nvoid Histories::sendReadRequests() {\n\tDEBUG_LOG((\"Reading: send requests with count %1.\").arg(_states.size()));\n\tif (_states.empty()) {\n\t\treturn;\n\t}\n\tconst auto now = crl::now();\n\tauto next = std::optional<crl::time>();\n\tfor (auto &[history, state] : _states) {\n\t\tif (!state.willReadTill) {\n\t\t\tDEBUG_LOG((\"Reading: skipping zero till.\"));\n\t\t\tcontinue;\n\t\t} else if (state.willReadWhen <= now) {\n\t\t\tDEBUG_LOG((\"Reading: sending with till %1.\"\n\t\t\t\t).arg(state.willReadTill.bare));\n\t\t\tsendReadRequest(history, state);\n\t\t} else if (!next || *next > state.willReadWhen) {\n\t\t\tDEBUG_LOG((\"Reading: scheduling for later send.\"));\n\t\t\tnext = state.willReadWhen;\n\t\t}\n\t}\n\tif (next.has_value()) {\n\t\t_readRequestsTimer.callOnce(*next - now);\n\t} else {\n\t\t_readRequestsTimer.cancel();\n\t}\n}\n\nvoid Histories::sendReadRequest(not_null<History*> history, State &state) {\n\tExpects(state.willReadTill > state.sentReadTill);\n\n\tconst auto tillId = state.sentReadTill = base::take(state.willReadTill);\n\tstate.willReadWhen = 0;\n\tstate.sentReadDone = false;\n\tDEBUG_LOG((\"Reading: sending request now with till %1.\"\n\t\t).arg(tillId.bare));\n\tsendRequest(history, RequestType::ReadInbox, [=](Fn<void()> finish) {\n\t\tDEBUG_LOG((\"Reading: sending request invoked with till %1.\"\n\t\t\t).arg(tillId.bare));\n\t\tconst auto finished = [=] {\n\t\t\tconst auto state = lookup(history);\n\t\t\tAssert(state != nullptr);\n\n\t\t\tif (state->sentReadTill == tillId) {\n\t\t\t\tstate->sentReadDone = true;\n\t\t\t\tif (history->unreadCountRefreshNeeded(tillId)) {\n\t\t\t\t\trequestDialogEntry(history);\n\t\t\t\t} else {\n\t\t\t\t\tstate->sentReadTill = 0;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tAssert(!state->sentReadTill || state->sentReadTill > tillId);\n\t\t\t}\n\t\t\thistory->validateMonoAndForumUnread(tillId);\n\t\t\tsendReadRequests();\n\t\t\tfinish();\n\t\t};\n\t\tif (const auto channel = history->peer->asChannel()) {\n\t\t\treturn session().api().request(MTPchannels_ReadHistory(\n\t\t\t\tchannel->inputChannel(),\n\t\t\t\tMTP_int(tillId)\n\t\t\t)).done(finished).fail(finished).send();\n\t\t} else {\n\t\t\treturn session().api().request(MTPmessages_ReadHistory(\n\t\t\t\thistory->peer->input(),\n\t\t\t\tMTP_int(tillId)\n\t\t\t)).done([=](const MTPmessages_AffectedMessages &result) {\n\t\t\t\tsession().api().applyAffectedMessages(history->peer, result);\n\t\t\t\tfinished();\n\t\t\t}).fail([=] {\n\t\t\t\tfinished();\n\t\t\t}).send();\n\t\t}\n\t});\n}\n\nvoid Histories::checkEmptyState(not_null<History*> history) {\n\tconst auto empty = [](const State &state) {\n\t\treturn state.postponed.empty()\n\t\t\t&& !state.postponedRequestEntry\n\t\t\t&& state.sent.empty()\n\t\t\t&& (state.willReadTill == 0)\n\t\t\t&& (state.sentReadTill == 0);\n\t};\n\tconst auto i = _states.find(history);\n\tif (i != end(_states) && empty(i->second)) {\n\t\t_states.erase(i);\n\t}\n}\n\nbool Histories::postponeHistoryRequest(const State &state) const {\n\tconst auto proj = [](const auto &pair) {\n\t\treturn pair.second.type;\n\t};\n\tconst auto i = ranges::find(state.sent, RequestType::Delete, proj);\n\treturn (i != end(state.sent));\n}\n\nbool Histories::postponeEntryRequest(const State &state) const {\n\treturn ranges::any_of(state.sent, [](const auto &pair) {\n\t\treturn pair.second.type != RequestType::History;\n\t});\n}\n\nvoid Histories::deleteMessages(\n\t\tnot_null<History*> history,\n\t\tconst QVector<MTPint> &ids,\n\t\tbool revoke) {\n\tsendRequest(history, RequestType::Delete, [=](Fn<void()> finish) {\n\t\tconst auto done = [=](const MTPmessages_AffectedMessages &result) {\n\t\t\tsession().api().applyAffectedMessages(history->peer, result);\n\t\t\tfinish();\n\t\t\thistory->requestChatListMessage();\n\t\t};\n\t\tif (const auto channel = history->peer->asChannel()) {\n\t\t\treturn session().api().request(MTPchannels_DeleteMessages(\n\t\t\t\tchannel->inputChannel(),\n\t\t\t\tMTP_vector<MTPint>(ids)\n\t\t\t)).done(done).fail(finish).send();\n\t\t} else {\n\t\t\tusing Flag = MTPmessages_DeleteMessages::Flag;\n\t\t\treturn session().api().request(MTPmessages_DeleteMessages(\n\t\t\t\tMTP_flags(revoke ? Flag::f_revoke : Flag(0)),\n\t\t\t\tMTP_vector<MTPint>(ids)\n\t\t\t)).done(done).fail(finish).send();\n\t\t}\n\t});\n}\n\nvoid Histories::deleteAllMessages(\n\t\tnot_null<History*> history,\n\t\tMsgId deleteTillId,\n\t\tbool justClear,\n\t\tbool revoke) {\n\tsendRequest(history, RequestType::Delete, [=](Fn<void()> finish) {\n\t\tconst auto peer = history->peer;\n\t\tconst auto chat = peer->asChat();\n\t\tconst auto channel = peer->asChannel();\n\t\tif (!justClear && revoke && channel && channel->canDelete()) {\n\t\t\treturn session().api().request(MTPchannels_DeleteChannel(\n\t\t\t\tchannel->inputChannel()\n\t\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t\tsession().api().applyUpdates(result);\n\t\t\t//}).fail([=](const MTP::Error &error) {\n\t\t\t//\tif (error.type() == u\"CHANNEL_TOO_LARGE\"_q) {\n\t\t\t//\t\tUi::show(Box<Ui::InformBox>(tr::lng_cant_delete_channel(tr::now)));\n\t\t\t//\t}\n\t\t\t}).send();\n\t\t} else if (channel) {\n\t\t\tusing Flag = MTPchannels_DeleteHistory::Flag;\n\t\t\treturn session().api().request(MTPchannels_DeleteHistory(\n\t\t\t\tMTP_flags(revoke ? Flag::f_for_everyone : Flag(0)),\n\t\t\t\tchannel->inputChannel(),\n\t\t\t\tMTP_int(deleteTillId)\n\t\t\t)).done(finish).fail(finish).send();\n\t\t} else if (!justClear && revoke && chat && chat->amCreator()) {\n\t\t\treturn session().api().request(MTPmessages_DeleteChat(\n\t\t\t\tchat->inputChat()\n\t\t\t)).done(finish).fail([=](const MTP::Error &error) {\n\t\t\t\tif (error.type() == \"PEER_ID_INVALID\") {\n\t\t\t\t\t// Try to join and delete,\n\t\t\t\t\t// while delete fails for non-joined.\n\t\t\t\t\tsession().api().request(MTPmessages_AddChatUser(\n\t\t\t\t\t\tchat->inputChat(),\n\t\t\t\t\t\tMTP_inputUserSelf(),\n\t\t\t\t\t\tMTP_int(0)\n\t\t\t\t\t)).done([=](const MTPmessages_InvitedUsers &result) {\n\t\t\t\t\t\tconst auto &data = result.data();\n\t\t\t\t\t\tsession().api().applyUpdates(data.vupdates());\n\t\t\t\t\t\tdeleteAllMessages(\n\t\t\t\t\t\t\thistory,\n\t\t\t\t\t\t\tdeleteTillId,\n\t\t\t\t\t\t\tjustClear,\n\t\t\t\t\t\t\trevoke);\n\t\t\t\t\t}).send();\n\t\t\t\t}\n\t\t\t\tfinish();\n\t\t\t}).send();\n\t\t} else {\n\t\t\tusing Flag = MTPmessages_DeleteHistory::Flag;\n\t\t\tconst auto flags = Flag(0)\n\t\t\t\t| (justClear ? Flag::f_just_clear : Flag(0))\n\t\t\t\t| (revoke ? Flag::f_revoke : Flag(0));\n\t\t\treturn session().api().request(MTPmessages_DeleteHistory(\n\t\t\t\tMTP_flags(flags),\n\t\t\t\tpeer->input(),\n\t\t\t\tMTP_int(0),\n\t\t\t\tMTPint(), // min_date\n\t\t\t\tMTPint() // max_date\n\t\t\t)).done([=](const MTPmessages_AffectedHistory &result) {\n\t\t\t\tconst auto offset = session().api().applyAffectedHistory(\n\t\t\t\t\tpeer,\n\t\t\t\t\tresult);\n\t\t\t\tif (offset > 0) {\n\t\t\t\t\tdeleteAllMessages(\n\t\t\t\t\t\thistory,\n\t\t\t\t\t\tdeleteTillId,\n\t\t\t\t\t\tjustClear,\n\t\t\t\t\t\trevoke);\n\t\t\t\t}\n\t\t\t\tfinish();\n\t\t\t}).fail(finish).send();\n\t\t}\n\t});\n}\n\nvoid Histories::deleteMessagesByDates(\n\t\tnot_null<History*> history,\n\t\tQDate firstDayToDelete,\n\t\tQDate lastDayToDelete,\n\t\tbool revoke) {\n\tconst auto firstSecondToDelete = base::unixtime::serialize(\n\t\t{ firstDayToDelete, QTime(0, 0) }\n\t);\n\tconst auto lastSecondToDelete = base::unixtime::serialize(\n\t\t{ lastDayToDelete, QTime(23, 59, 59) }\n\t);\n\tdeleteMessagesByDates(\n\t\thistory,\n\t\tfirstSecondToDelete - 1,\n\t\tlastSecondToDelete + 1,\n\t\trevoke);\n}\n\nvoid Histories::deleteMessagesByDates(\n\tnot_null<History*> history,\n\tTimeId minDate,\n\tTimeId maxDate,\n\tbool revoke) {\n\tsendRequest(history, RequestType::Delete, [=](Fn<void()> finish) {\n\t\tconst auto peer = history->peer;\n\t\tusing Flag = MTPmessages_DeleteHistory::Flag;\n\t\tconst auto flags = Flag::f_just_clear\n\t\t\t| Flag::f_min_date\n\t\t\t| Flag::f_max_date\n\t\t\t| (revoke ? Flag::f_revoke : Flag(0));\n\t\treturn session().api().request(MTPmessages_DeleteHistory(\n\t\t\tMTP_flags(flags),\n\t\t\tpeer->input(),\n\t\t\tMTP_int(0),\n\t\t\tMTP_int(minDate),\n\t\t\tMTP_int(maxDate)\n\t\t)).done([=](const MTPmessages_AffectedHistory &result) {\n\t\t\tconst auto offset = session().api().applyAffectedHistory(\n\t\t\t\tpeer,\n\t\t\t\tresult);\n\t\t\tif (offset > 0) {\n\t\t\t\tdeleteMessagesByDates(history, minDate, maxDate, revoke);\n\t\t\t}\n\t\t\tfinish();\n\t\t}).fail(finish).send();\n\t});\n\thistory->destroyMessagesByDates(minDate, maxDate);\n}\n\nvoid Histories::deleteMessages(const MessageIdsList &ids, bool revoke) {\n\tauto remove = std::vector<not_null<HistoryItem*>>();\n\tremove.reserve(ids.size());\n\tbase::flat_map<not_null<History*>, QVector<MTPint>> idsByPeer;\n\tbase::flat_map<not_null<PeerData*>, QVector<MTPint>> scheduledIdsByPeer;\n\tbase::flat_map<BusinessShortcutId, QVector<MTPint>> quickIdsByShortcut;\n\tbase::flat_set<not_null<DocumentData*>> savedMusic;\n\tfor (const auto &itemId : ids) {\n\t\tif (const auto item = _owner->message(itemId)) {\n\t\t\tconst auto history = item->history();\n\t\t\tif (item->isSavedMusicItem()) {\n\t\t\t\tsavedMusic.emplace(item->media()->document());\n\t\t\t\tcontinue;\n\t\t\t} else if (item->isScheduled()) {\n\t\t\t\tconst auto wasOnServer = !item->isSending()\n\t\t\t\t\t&& !item->hasFailed();\n\t\t\t\tauto &scheduled = _owner->session().scheduledMessages();\n\t\t\t\tif (wasOnServer) {\n\t\t\t\t\tscheduledIdsByPeer[history->peer].push_back(\n\t\t\t\t\t\tMTP_int(scheduled.lookupId(item)));\n\t\t\t\t} else {\n\t\t\t\t\tscheduled.removeSending(item);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t} else if (item->isBusinessShortcut()) {\n\t\t\t\tconst auto wasOnServer = !item->isSending()\n\t\t\t\t\t&& !item->hasFailed();\n\t\t\t\tif (wasOnServer) {\n\t\t\t\t\tquickIdsByShortcut[item->shortcutId()].push_back(MTP_int(\n\t\t\t\t\t\t_owner->shortcutMessages().lookupId(item)));\n\t\t\t\t} else {\n\t\t\t\t\t_owner->shortcutMessages().removeSending(item);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tremove.push_back(item);\n\t\t\tif (item->isRegular()) {\n\t\t\t\tidsByPeer[history].push_back(MTP_int(itemId.msg));\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const auto &[history, ids] : idsByPeer) {\n\t\thistory->owner().histories().deleteMessages(history, ids, revoke);\n\t}\n\tfor (const auto &[peer, ids] : scheduledIdsByPeer) {\n\t\tpeer->session().api().request(MTPmessages_DeleteScheduledMessages(\n\t\t\tpeer->input(),\n\t\t\tMTP_vector<MTPint>(ids)\n\t\t)).done([peer = peer](const MTPUpdates &result) {\n\t\t\tpeer->session().api().applyUpdates(result);\n\t\t}).send();\n\t}\n\tfor (const auto &[shortcutId, ids] : quickIdsByShortcut) {\n\t\tconst auto api = &_owner->session().api();\n\t\tapi->request(MTPmessages_DeleteQuickReplyMessages(\n\t\t\tMTP_int(shortcutId),\n\t\t\tMTP_vector<MTPint>(ids)\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\tapi->applyUpdates(result);\n\t\t}).send();\n\t}\n\tfor (const auto &document : savedMusic) {\n\t\tdocument->owner().savedMusic().remove(document);\n\t}\n\n\tif (!remove.empty()) {\n\t\t_owner->notifyItemsAboutToBeDestroyed(remove);\n\t}\n\tfor (const auto &item : remove) {\n\t\tconst auto history = item->history();\n\t\tconst auto wasLast = (history->lastMessage() == item);\n\t\tconst auto wasInChats = (history->chatListMessage() == item);\n\t\titem->destroy();\n\n\t\tif (wasLast || wasInChats) {\n\t\t\thistory->requestChatListMessage();\n\t\t}\n\t}\n}\n\nint Histories::sendRequest(\n\t\tnot_null<History*> history,\n\t\tRequestType type,\n\t\tFn<mtpRequestId(Fn<void()> finish)> generator) {\n\tExpects(type != RequestType::None);\n\n\tauto &state = _states[history];\n\tconst auto id = ++_requestAutoincrement;\n\t_historyByRequest.emplace(id, history);\n\tif (type == RequestType::History && postponeHistoryRequest(state)) {\n\t\tstate.postponed.emplace(\n\t\t\tid,\n\t\t\tPostponedHistoryRequest{ std::move(generator) });\n\t\treturn id;\n\t}\n\tconst auto requestId = generator([=] { checkPostponed(history, id); });\n\tstate.sent.emplace(id, SentRequest{\n\t\tstd::move(generator),\n\t\trequestId,\n\t\ttype\n\t});\n\tif (!state.postponedRequestEntry\n\t\t&& postponeEntryRequest(state)\n\t\t&& _dialogRequests.contains(history)) {\n\t\tstate.postponedRequestEntry = true;\n\t}\n\tif (postponeHistoryRequest(state)) {\n\t\tconst auto resendHistoryRequest = [&](auto &pair) {\n\t\t\tauto &[id, sent] = pair;\n\t\t\tif (sent.type != RequestType::History) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tstate.postponed.emplace(\n\t\t\t\tid,\n\t\t\t\tPostponedHistoryRequest{ std::move(sent.generator) });\n\t\t\tsession().api().request(sent.id).cancel();\n\t\t\treturn true;\n\t\t};\n\t\tstate.sent.erase(\n\t\t\tranges::remove_if(state.sent, resendHistoryRequest),\n\t\t\tend(state.sent));\n\t}\n\treturn id;\n}\n\nvoid Histories::sendCreateTopicRequest(\n\t\tnot_null<History*> history,\n\t\tMsgId rootId) {\n\tconst auto forum = history->asForum();\n\tAssert(forum != nullptr);\n\tconst auto topic = forum->topicFor(rootId);\n\tAssert(topic != nullptr);\n\tconst auto randomId = base::RandomValue<uint64>();\n\tsession().data().registerMessageRandomId(\n\t\trandomId,\n\t\t{ history->peer->id, rootId });\n\tconst auto api = &session().api();\n\tusing Flag = MTPmessages_CreateForumTopic::Flag;\n\tapi->request(MTPmessages_CreateForumTopic(\n\t\tMTP_flags(Flag::f_icon_color\n\t\t\t| (topic->iconId() ? Flag::f_icon_emoji_id : Flag())\n\t\t\t| (history->peer->isBot() ? Flag::f_title_missing : Flag())),\n\t\thistory->peer->input(),\n\t\tMTP_string(topic->title()),\n\t\tMTP_int(topic->colorId()),\n\t\tMTP_long(topic->iconId()),\n\t\tMTP_long(randomId),\n\t\tMTPInputPeer() // send_as\n\t)).done([=](const MTPUpdates &result) {\n\t\tapi->applyUpdates(result, randomId);\n\t}).fail([=](const MTP::Error &error) {\n\t\tapi->sendMessageFail(error, history->peer, randomId);\n\t}).send();\n}\n\nbool Histories::isCreatingTopic(\n\t\tnot_null<History*> history,\n\t\tMsgId rootId) const {\n\tconst auto forum = history->asForum();\n\treturn forum && forum->creating(rootId);\n}\n\nint Histories::sendPreparedMessage(\n\t\tnot_null<History*> history,\n\t\tFullReplyTo replyTo,\n\t\tuint64 randomId,\n\t\tFn<PreparedMessage(not_null<History*>, FullReplyTo)> message,\n\t\tFn<void(const MTPUpdates&, const MTP::Response&)> done,\n\t\tFn<void(const MTP::Error&, const MTP::Response&)> fail) {\n\tif (isCreatingTopic(history, replyTo.topicRootId)) {\n\t\tconst auto id = ++_requestAutoincrement;\n\t\tconst auto creatingId = FullMsgId(\n\t\t\thistory->peer->id,\n\t\t\treplyTo.topicRootId);\n\t\tauto i = _creatingTopics.find(creatingId);\n\t\tif (i == end(_creatingTopics)) {\n\t\t\tsendCreateTopicRequest(history, replyTo.topicRootId);\n\t\t\ti = _creatingTopics.emplace(creatingId).first;\n\t\t}\n\t\ti->second.push_back({\n\t\t\t.randomId = randomId,\n\t\t\t.replyTo = replyTo.messageId,\n\t\t\t.message = std::move(message),\n\t\t\t.done = std::move(done),\n\t\t\t.fail = std::move(fail),\n\t\t\t.requestId = id,\n\t\t});\n\t\t_creatingTopicRequests.emplace(id);\n\t\treturn id;\n\t}\n\tauto realReplyTo = replyTo;\n\tconst auto topicReplyToId = [&](const auto &id) {\n\t\treturn convertTopicReplyToId(history, id);\n\t};\n\trealReplyTo.messageId = topicReplyToId(replyTo.messageId);\n\trealReplyTo.topicRootId = topicReplyToId(replyTo.topicRootId);\n\treturn v::match(message(history, realReplyTo), [&](const auto &request) {\n\t\tconst auto type = RequestType::Send;\n\t\treturn sendRequest(history, type, [=](Fn<void()> finish) {\n\t\t\tconst auto session = &_owner->session();\n\t\t\tconst auto api = &session->api();\n\t\t\thistory->sendRequestId = api->request(\n\t\t\t\tbase::duplicate(request)\n\t\t\t).done([=](\n\t\t\t\t\tconst MTPUpdates &result,\n\t\t\t\t\tconst MTP::Response &response) {\n\t\t\t\tapi->applyUpdates(result, randomId);\n\t\t\t\tdone(result, response);\n\t\t\t\tfinish();\n\t\t\t}).fail([=](\n\t\t\t\t\tconst MTP::Error &error,\n\t\t\t\t\tconst MTP::Response &response) {\n\t\t\t\tfail(error, response);\n\t\t\t\tfinish();\n\t\t\t}).afterRequest(\n\t\t\t\thistory->sendRequestId\n\t\t\t).send();\n\t\t\treturn history->sendRequestId;\n\t\t});\n\t});\n}\n\nvoid Histories::checkTopicCreated(FullMsgId rootId, MsgId realRoot) {\n\tconst auto i = _creatingTopics.find(rootId);\n\tif (i != end(_creatingTopics)) {\n\t\tauto scheduled = base::take(i->second);\n\t\t_creatingTopics.erase(i);\n\n\t\t_createdTopicIds.emplace(rootId, realRoot);\n\n\t\tconst auto history = _owner->history(rootId.peer);\n\t\tif (const auto forum = history->asForum()) {\n\t\t\tforum->created(rootId.msg, realRoot);\n\t\t}\n\n\t\tfor (auto &entry : scheduled) {\n\t\t\t_creatingTopicRequests.erase(entry.requestId);\n\t\t\tsendPreparedMessage(\n\t\t\t\thistory,\n\t\t\t\tFullReplyTo{\n\t\t\t\t\t.messageId = entry.replyTo,\n\t\t\t\t\t.topicRootId = realRoot,\n\t\t\t\t},\n\t\t\t\tentry.randomId,\n\t\t\t\tstd::move(entry.message),\n\t\t\t\tstd::move(entry.done),\n\t\t\t\tstd::move(entry.fail));\n\t\t}\n\t\tfor (const auto &item : history->clientSideMessages()) {\n\t\t\tconst auto replace = [&](MsgId nowId) {\n\t\t\t\treturn (nowId == rootId.msg) ? realRoot : nowId;\n\t\t\t};\n\t\t\tif (item->topicRootId() == rootId.msg) {\n\t\t\t\titem->setReplyFields(\n\t\t\t\t\treplace(item->replyToId()),\n\t\t\t\t\trealRoot,\n\t\t\t\t\ttrue);\n\t\t\t}\n\t\t}\n\t}\n}\n\nFullMsgId Histories::convertTopicReplyToId(\n\t\tnot_null<History*> history,\n\t\tFullMsgId replyToId) const {\n\tconst auto id = (history->peer->id == replyToId.peer)\n\t\t? convertTopicReplyToId(history, replyToId.msg)\n\t\t: replyToId.msg;\n\treturn { replyToId.peer, id };\n}\n\nMsgId Histories::convertTopicReplyToId(\n\t\tnot_null<History*> history,\n\t\tMsgId replyToId) const {\n\tif (!replyToId) {\n\t\treturn {};\n\t}\n\tconst auto i = _createdTopicIds.find({ history->peer->id, replyToId });\n\treturn (i != end(_createdTopicIds)) ? i->second : replyToId;\n}\n\nvoid Histories::checkPostponed(not_null<History*> history, int id) {\n\tif (const auto state = lookup(history)) {\n\t\tfinishSentRequest(history, state, id);\n\t}\n}\n\nvoid Histories::cancelRequest(int id) {\n\tif (!id) {\n\t\treturn;\n\t} else if (_creatingTopicRequests.contains(id)) {\n\t\tcancelDelayedByTopicRequest(id);\n\t\treturn;\n\t}\n\tconst auto history = _historyByRequest.take(id);\n\tif (!history) {\n\t\treturn;\n\t}\n\tconst auto state = lookup(*history);\n\tif (!state) {\n\t\treturn;\n\t}\n\tstate->postponed.remove(id);\n\tfinishSentRequest(*history, state, id);\n}\n\nvoid Histories::cancelDelayedByTopicRequest(int id) {\n\tfor (auto &[rootId, messages] : _creatingTopics) {\n\t\tmessages.erase(\n\t\t\tranges::remove(messages, id, &DelayedByTopicMessage::requestId),\n\t\t\tend(messages));\n\t}\n\t_creatingTopicRequests.remove(id);\n}\n\nvoid Histories::finishSentRequest(\n\t\tnot_null<History*> history,\n\t\tnot_null<State*> state,\n\t\tint id) {\n\t_historyByRequest.remove(id);\n\tconst auto i = state->sent.find(id);\n\tif (i != end(state->sent)) {\n\t\tsession().api().request(i->second.id).cancel();\n\t\tstate->sent.erase(i);\n\t}\n\tif (!state->postponed.empty() && !postponeHistoryRequest(*state)) {\n\t\tfor (auto &[id, postponed] : base::take(state->postponed)) {\n\t\t\tconst auto requestId = postponed.generator([=, id=id] {\n\t\t\t\tcheckPostponed(history, id);\n\t\t\t});\n\t\t\tstate->sent.emplace(id, SentRequest{\n\t\t\t\tstd::move(postponed.generator),\n\t\t\t\trequestId,\n\t\t\t\tRequestType::History\n\t\t\t});\n\t\t}\n\t}\n\tif (state->postponedRequestEntry && !postponeEntryRequest(*state)) {\n\t\tconst auto i = _dialogRequests.find(history);\n\t\tAssert(i != end(_dialogRequests));\n\t\tconst auto &[j, ok] = _dialogRequestsPending.emplace(\n\t\t\thistory,\n\t\t\tstd::move(i->second));\n\t\tAssert(ok);\n\t\t_dialogRequests.erase(i);\n\t\tstate->postponedRequestEntry = false;\n\t\tpostponeRequestDialogEntries();\n\t}\n\tcheckEmptyState(history);\n}\n\nHistories::State *Histories::lookup(not_null<History*> history) {\n\tconst auto i = _states.find(history);\n\treturn (i != end(_states)) ? &i->second : nullptr;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_histories.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n\nclass History;\nclass HistoryItem;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace MTP {\nclass Error;\nstruct Response;\n} // namespace MTP\n\nnamespace Data {\n\nclass Session;\nclass Folder;\nstruct WebPageDraft;\nclass SavedSublist;\n\n[[nodiscard]] MTPInputReplyTo ReplyToForMTP(\n\tnot_null<History*> history,\n\tFullReplyTo replyTo);\n[[nodiscard]] MTPInputMedia WebPageForMTP(\n\tconst Data::WebPageDraft &draft,\n\tbool required = false);\n\nclass Histories final {\npublic:\n\tenum class RequestType : uchar {\n\t\tNone,\n\t\tHistory,\n\t\tReadInbox,\n\t\tDelete,\n\t\tSend,\n\t};\n\n\texplicit Histories(not_null<Session*> owner);\n\n\t[[nodiscard]] Session &owner() const;\n\t[[nodiscard]] Main::Session &session() const;\n\n\t[[nodiscard]] History *find(PeerId peerId);\n\t[[nodiscard]] not_null<History*> findOrCreate(PeerId peerId);\n\n\tvoid applyPeerDialogs(const MTPmessages_PeerDialogs &dialogs);\n\n\tvoid unloadAll();\n\tvoid clearAll();\n\n\tvoid readInbox(not_null<History*> history);\n\tvoid readInboxTill(not_null<HistoryItem*> item);\n\tvoid readInboxTill(not_null<History*> history, MsgId tillId);\n\tvoid readInboxOnNewMessage(not_null<HistoryItem*> item);\n\tvoid readClientSideMessage(not_null<HistoryItem*> item);\n\tvoid sendPendingReadInbox(not_null<History*> history);\n\tvoid reportDelivery(not_null<HistoryItem*> item);\n\n\tvoid requestDialogEntry(not_null<Data::Folder*> folder);\n\tvoid requestDialogEntry(\n\t\tnot_null<History*> history,\n\t\tFn<void()> callback = nullptr);\n\tvoid dialogEntryApplied(not_null<History*> history);\n\tvoid changeDialogUnreadMark(not_null<History*> history, bool unread);\n\tvoid changeSublistUnreadMark(\n\t\tnot_null<Data::SavedSublist*> sublist,\n\t\tbool unread);\n\tvoid requestFakeChatListMessage(not_null<History*> history);\n\n\tvoid requestGroupAround(not_null<HistoryItem*> item);\n\n\tvoid deleteMessages(\n\t\tnot_null<History*> history,\n\t\tconst QVector<MTPint> &ids,\n\t\tbool revoke);\n\tvoid deleteAllMessages(\n\t\tnot_null<History*> history,\n\t\tMsgId deleteTillId,\n\t\tbool justClear,\n\t\tbool revoke);\n\n\tvoid deleteMessagesByDates(\n\t\tnot_null<History*> history,\n\t\tQDate firstDayToDelete,\n\t\tQDate lastDayToDelete,\n\t\tbool revoke);\n\tvoid deleteMessagesByDates(\n\t\tnot_null<History*> history,\n\t\tTimeId minDate,\n\t\tTimeId maxDate,\n\t\tbool revoke);\n\n\tvoid deleteMessages(const MessageIdsList &ids, bool revoke);\n\n\tint sendRequest(\n\t\tnot_null<History*> history,\n\t\tRequestType type,\n\t\tFn<mtpRequestId(Fn<void()> finish)> generator);\n\tvoid cancelRequest(int id);\n\n\tusing PreparedMessage = std::variant<\n\t\tMTPmessages_SendMessage,\n\t\tMTPmessages_SendMedia,\n\t\tMTPmessages_SendInlineBotResult,\n\t\tMTPmessages_SendMultiMedia,\n\t\tMTPmessages_ForwardMessages>;\n\tint sendPreparedMessage(\n\t\tnot_null<History*> history,\n\t\tFullReplyTo replyTo,\n\t\tuint64 randomId,\n\t\tFn<PreparedMessage(not_null<History*>, FullReplyTo)> message,\n\t\tFn<void(const MTPUpdates&, const MTP::Response&)> done,\n\t\tFn<void(const MTP::Error&, const MTP::Response&)> fail);\n\n\tstruct ReplyToPlaceholder {\n\t};\n\ttemplate <typename RequestType, typename ...Args>\n\tstatic auto PrepareMessage(const Args &...args)\n\t-> Fn<Histories::PreparedMessage(not_null<History*>, FullReplyTo)> {\n\t\treturn [=](not_null<History*> history, FullReplyTo replyTo)\n\t\t-> RequestType {\n\t\t\treturn { ReplaceReplyIds(history, args, replyTo)... };\n\t\t};\n\t}\n\n\tvoid checkTopicCreated(FullMsgId rootId, MsgId realRoot);\n\t[[nodiscard]] FullMsgId convertTopicReplyToId(\n\t\tnot_null<History*> history,\n\t\tFullMsgId replyToId) const;\n\t[[nodiscard]] MsgId convertTopicReplyToId(\n\t\tnot_null<History*> history,\n\t\tMsgId replyToId) const;\n\nprivate:\n\tstruct PostponedHistoryRequest {\n\t\tFn<mtpRequestId(Fn<void()> finish)> generator;\n\t};\n\tstruct SentRequest {\n\t\tFn<mtpRequestId(Fn<void()> finish)> generator;\n\t\tmtpRequestId id = 0;\n\t\tRequestType type = RequestType::None;\n\t};\n\tstruct State {\n\t\tbase::flat_map<int, PostponedHistoryRequest> postponed;\n\t\tbase::flat_map<int, SentRequest> sent;\n\t\tMsgId willReadTill = 0;\n\t\tMsgId sentReadTill = 0;\n\t\tcrl::time willReadWhen = 0;\n\t\tbool sentReadDone = false;\n\t\tbool postponedRequestEntry = false;\n\t};\n\tstruct ChatListGroupRequest {\n\t\tMsgId aroundId = 0;\n\t\tmtpRequestId requestId = 0;\n\t};\n\tstruct DelayedByTopicMessage {\n\t\tuint64 randomId = 0;\n\t\tFullMsgId replyTo;\n\t\tFn<PreparedMessage(not_null<History*>, FullReplyTo)> message;\n\t\tFn<void(const MTPUpdates&, const MTP::Response&)> done;\n\t\tFn<void(const MTP::Error&, const MTP::Response&)> fail;\n\t\tint requestId = 0;\n\t};\n\tstruct GroupRequestKey {\n\t\tnot_null<History*> history;\n\t\tMsgId rootId = 0;\n\n\t\tfriend inline auto operator<=>(\n\t\t\tGroupRequestKey,\n\t\t\tGroupRequestKey) = default;\n\t};\n\n\ttemplate <typename Arg>\n\tstatic auto ReplaceReplyIds(\n\t\t\tnot_null<History*> history,\n\t\t\tArg arg,\n\t\t\tFullReplyTo replyTo) {\n\t\tif constexpr (std::is_same_v<Arg, ReplyToPlaceholder>) {\n\t\t\treturn ReplyToForMTP(history, replyTo);\n\t\t} else {\n\t\t\treturn arg;\n\t\t}\n\t}\n\n\tvoid readInboxTill(not_null<History*> history, MsgId tillId, bool force);\n\tvoid sendReadRequests();\n\tvoid sendReadRequest(not_null<History*> history, State &state);\n\t[[nodiscard]] State *lookup(not_null<History*> history);\n\tvoid checkEmptyState(not_null<History*> history);\n\tvoid checkPostponed(not_null<History*> history, int id);\n\tvoid finishSentRequest(\n\t\tnot_null<History*> history,\n\t\tnot_null<State*> state,\n\t\tint id);\n\t[[nodiscard]] bool postponeHistoryRequest(const State &state) const;\n\t[[nodiscard]] bool postponeEntryRequest(const State &state) const;\n\tvoid postponeRequestDialogEntries();\n\n\tvoid sendDialogRequests();\n\tvoid reportPendingDeliveries();\n\n\t[[nodiscard]] bool isCreatingTopic(\n\t\tnot_null<History*> history,\n\t\tMsgId rootId) const;\n\tvoid sendCreateTopicRequest(not_null<History*> history, MsgId rootId);\n\tvoid cancelDelayedByTopicRequest(int id);\n\n\tconst not_null<Session*> _owner;\n\n\tstd::unordered_map<PeerId, std::unique_ptr<History>> _map;\n\tbase::flat_map<not_null<History*>, State> _states;\n\tbase::flat_map<int, not_null<History*>> _historyByRequest;\n\tint _requestAutoincrement = 0;\n\tbase::Timer _readRequestsTimer;\n\n\tbase::flat_set<not_null<Data::Folder*>> _dialogFolderRequests;\n\tbase::flat_map<\n\t\tnot_null<History*>,\n\t\tstd::vector<Fn<void()>>> _dialogRequests;\n\tbase::flat_map<\n\t\tnot_null<History*>,\n\t\tstd::vector<Fn<void()>>> _dialogRequestsPending;\n\n\tbase::flat_set<not_null<History*>> _fakeChatListRequests;\n\n\tbase::flat_map<\n\t\tGroupRequestKey,\n\t\tChatListGroupRequest> _chatListGroupRequests;\n\n\tbase::flat_map<\n\t\tFullMsgId,\n\t\tstd::vector<DelayedByTopicMessage>> _creatingTopics;\n\tbase::flat_map<FullMsgId, MsgId> _createdTopicIds;\n\tbase::flat_set<mtpRequestId> _creatingTopicRequests;\n\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tbase::flat_set<MsgId>> _pendingDeliveryReport;\n\tbase::flat_set<not_null<PeerData*>> _deliveryReportSent;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_history_messages.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_history_messages.h\"\n\n#include \"apiwrap.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_sparse_ids.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n\nnamespace Data {\n\nvoid HistoryMessages::addNew(MsgId messageId) {\n\t_chat.addNew(messageId);\n}\n\nvoid HistoryMessages::addExisting(MsgId messageId, MsgRange noSkipRange) {\n\t_chat.addExisting(messageId, noSkipRange);\n}\n\nvoid HistoryMessages::addSlice(\n\t\tstd::vector<MsgId> &&messageIds,\n\t\tMsgRange noSkipRange,\n\t\tstd::optional<int> count) {\n\t_chat.addSlice(std::move(messageIds), noSkipRange, count);\n}\n\nvoid HistoryMessages::removeOne(MsgId messageId) {\n\t_chat.removeOne(messageId);\n\t_oneRemoved.fire_copy(messageId);\n}\n\nvoid HistoryMessages::removeAll() {\n\t_chat.removeAll();\n\t_allRemoved.fire({});\n}\n\nvoid HistoryMessages::invalidateBottom() {\n\t_chat.invalidateBottom();\n\t_bottomInvalidated.fire({});\n}\n\nStorage::SparseIdsListResult HistoryMessages::snapshot(\n\t\tconst Storage::SparseIdsListQuery &query) const {\n\treturn _chat.snapshot(query);\n}\n\nauto HistoryMessages::sliceUpdated() const\n-> rpl::producer<Storage::SparseIdsSliceUpdate> {\n\treturn _chat.sliceUpdated();\n}\n\nrpl::producer<MsgId> HistoryMessages::oneRemoved() const {\n\treturn _oneRemoved.events();\n}\n\nrpl::producer<> HistoryMessages::allRemoved() const {\n\treturn _allRemoved.events();\n}\n\nrpl::producer<> HistoryMessages::bottomInvalidated() const {\n\treturn _bottomInvalidated.events();\n}\n\nrpl::producer<SparseIdsSlice> HistoryViewer(\n\t\tnot_null<History*> history,\n\t\tMsgId aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tExpects(IsServerMsgId(aroundId) || (aroundId == 0));\n\tExpects((aroundId != 0) || (limitBefore == 0 && limitAfter == 0));\n\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tconst auto messages = &history->messages();\n\n\t\tauto builder = lifetime.make_state<SparseIdsSliceBuilder>(\n\t\t\taroundId,\n\t\t\tlimitBefore,\n\t\t\tlimitAfter);\n\t\tusing RequestAroundInfo = SparseIdsSliceBuilder::AroundData;\n\t\tbuilder->insufficientAround(\n\t\t) | rpl::on_next([=](const RequestAroundInfo &info) {\n\t\t\tif (!info.aroundId) {\n\t\t\t\t// Ignore messages-count-only requests, because we perform\n\t\t\t\t// them with non-zero limit of messages and end up adding\n\t\t\t\t// a broken slice with several last messages from the chat\n\t\t\t\t// with a non-skip range starting at zero.\n\t\t\t\treturn;\n\t\t\t}\n\t\t\thistory->session().api().requestHistory(\n\t\t\t\thistory,\n\t\t\t\tinfo.aroundId,\n\t\t\t\tinfo.direction);\n\t\t}, lifetime);\n\n\t\tauto pushNextSnapshot = [=] {\n\t\t\tconsumer.put_next(builder->snapshot());\n\t\t};\n\n\t\tusing SliceUpdate = Storage::SparseIdsSliceUpdate;\n\t\tmessages->sliceUpdated(\n\t\t) | rpl::filter([=](const SliceUpdate &update) {\n\t\t\treturn builder->applyUpdate(update);\n\t\t}) | rpl::on_next(pushNextSnapshot, lifetime);\n\n\t\tmessages->oneRemoved(\n\t\t) | rpl::filter([=](MsgId messageId) {\n\t\t\treturn builder->removeOne(messageId);\n\t\t}) | rpl::on_next(pushNextSnapshot, lifetime);\n\n\t\tmessages->allRemoved(\n\t\t) | rpl::filter([=] {\n\t\t\treturn builder->removeAll();\n\t\t}) | rpl::on_next(pushNextSnapshot, lifetime);\n\n\t\tmessages->bottomInvalidated(\n\t\t) | rpl::filter([=] {\n\t\t\treturn builder->invalidateBottom();\n\t\t}) | rpl::on_next(pushNextSnapshot, lifetime);\n\n\t\tconst auto snapshot = messages->snapshot({\n\t\t\taroundId,\n\t\t\tlimitBefore,\n\t\t\tlimitAfter,\n\t\t});\n\t\tif (snapshot.count || !snapshot.messageIds.empty()) {\n\t\t\tif (builder->applyInitial(snapshot)) {\n\t\t\t\tpushNextSnapshot();\n\t\t\t}\n\t\t}\n\t\tbuilder->checkInsufficient();\n\n\t\treturn lifetime;\n\t};\n}\n\nrpl::producer<SparseIdsMergedSlice> HistoryMergedViewer(\n\t\tnot_null<History*> history,\n\t\t/*Universal*/MsgId universalAroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tconst auto migrateFrom = history->peer->migrateFrom();\n\tauto createSimpleViewer = [=](\n\t\t\tPeerId peerId,\n\t\t\tMsgId topicRootId,\n\t\t\tPeerId monoforumPeerId,\n\t\t\tSparseIdsSlice::Key simpleKey,\n\t\t\tint limitBefore,\n\t\t\tint limitAfter) {\n\t\tconst auto chosen = (history->peer->id == peerId)\n\t\t\t? history\n\t\t\t: history->owner().history(peerId);\n\t\treturn HistoryViewer(chosen, simpleKey, limitBefore, limitAfter);\n\t};\n\tconst auto peerId = history->peer->id;\n\tconst auto migratedPeerId = migrateFrom ? migrateFrom->id : PeerId(0);\n\tusing Key = SparseIdsMergedSlice::Key;\n\treturn SparseIdsMergedSlice::CreateViewer(\n\t\tKey(peerId, MsgId(), PeerId(), migratedPeerId, universalAroundId),\n\t\tlimitBefore,\n\t\tlimitAfter,\n\t\tstd::move(createSimpleViewer));\n}\n\nrpl::producer<MessagesSlice> HistoryMessagesViewer(\n\t\tnot_null<History*> history,\n\t\tMessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tconst auto computeUnreadAroundId = [&] {\n\t\tif (const auto migrated = history->migrateFrom()) {\n\t\t\tif (const auto around = migrated->loadAroundId()) {\n\t\t\t\treturn MsgId(around - ServerMaxMsgId);\n\t\t\t}\n\t\t}\n\t\tif (const auto around = history->loadAroundId()) {\n\t\t\treturn around;\n\t\t}\n\t\treturn MsgId(ServerMaxMsgId - 1);\n\t};\n\tconst auto messageId = (aroundId.fullId.msg == ShowAtUnreadMsgId)\n\t\t? computeUnreadAroundId()\n\t\t: ((aroundId.fullId.msg == ShowAtTheEndMsgId)\n\t\t\t|| (aroundId == MaxMessagePosition))\n\t\t? (ServerMaxMsgId - 1)\n\t\t: (aroundId.fullId.peer == history->peer->id)\n\t\t? aroundId.fullId.msg\n\t\t: (aroundId.fullId.msg - ServerMaxMsgId);\n\treturn HistoryMergedViewer(\n\t\thistory,\n\t\tmessageId,\n\t\tlimitBefore,\n\t\tlimitAfter\n\t) | rpl::map([=](SparseIdsMergedSlice &&slice) {\n\t\tauto result = Data::MessagesSlice();\n\t\tresult.fullCount = slice.fullCount();\n\t\tresult.skippedAfter = slice.skippedAfter();\n\t\tresult.skippedBefore = slice.skippedBefore();\n\t\tconst auto count = slice.size();\n\t\tresult.ids.reserve(count);\n\t\tif (const auto msgId = slice.nearest(messageId)) {\n\t\t\tresult.nearestToAround = *msgId;\n\t\t}\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tresult.ids.push_back(slice[i]);\n\t\t}\n\t\treturn result;\n\t});\n}\n\n} // namespace Data"
  },
  {
    "path": "Telegram/SourceFiles/data/data_history_messages.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"storage/storage_sparse_ids_list.h\"\n\nclass History;\nclass SparseIdsSlice;\nclass SparseIdsMergedSlice;\n\nnamespace Data {\n\nstruct MessagesSlice;\nstruct MessagePosition;\n\nclass HistoryMessages final {\npublic:\n\tvoid addNew(MsgId messageId);\n\tvoid addExisting(MsgId messageId, MsgRange noSkipRange);\n\tvoid addSlice(\n\t\tstd::vector<MsgId> &&messageIds,\n\t\tMsgRange noSkipRange,\n\t\tstd::optional<int> count);\n\tvoid removeOne(MsgId messageId);\n\tvoid removeAll();\n\tvoid invalidateBottom();\n\n\t[[nodiscard]] Storage::SparseIdsListResult snapshot(\n\t\tconst Storage::SparseIdsListQuery &query) const;\n\t[[nodiscard]] auto sliceUpdated() const\n\t\t-> rpl::producer<Storage::SparseIdsSliceUpdate>;\n\t[[nodiscard]] rpl::producer<MsgId> oneRemoved() const;\n\t[[nodiscard]] rpl::producer<> allRemoved() const;\n\t[[nodiscard]] rpl::producer<> bottomInvalidated() const;\n\nprivate:\n\tStorage::SparseIdsList _chat;\n\trpl::event_stream<MsgId> _oneRemoved;\n\trpl::event_stream<> _allRemoved;\n\trpl::event_stream<> _bottomInvalidated;\n\n};\n\n[[nodiscard]] rpl::producer<SparseIdsSlice> HistoryViewer(\n\tnot_null<History*> history,\n\tMsgId aroundId,\n\tint limitBefore,\n\tint limitAfter);\n\n[[nodiscard]] rpl::producer<SparseIdsMergedSlice> HistoryMergedViewer(\n\tnot_null<History*> history,\n\t/*Universal*/MsgId universalAroundId,\n\tint limitBefore,\n\tint limitAfter);\n\n[[nodiscard]] rpl::producer<MessagesSlice> HistoryMessagesViewer(\n\tnot_null<History*> history,\n\tMessagePosition aroundId,\n\tint limitBefore,\n\tint limitAfter);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_lastseen_status.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\ninline constexpr auto kLifeStartDate = 1375315200; // Let it be 01.08.2013.\n\nclass LastseenStatus final {\npublic:\n\tLastseenStatus() = default;\n\n\t[[nodiscard]] static LastseenStatus Recently(bool byMe = false) {\n\t\treturn LastseenStatus(kRecentlyValue, false, byMe);\n\t}\n\t[[nodiscard]] static LastseenStatus WithinWeek(bool byMe = false) {\n\t\treturn LastseenStatus(kWithinWeekValue, false, byMe);\n\t}\n\t[[nodiscard]] static LastseenStatus WithinMonth(bool byMe = false) {\n\t\treturn LastseenStatus(kWithinMonthValue, false, byMe);\n\t}\n\t[[nodiscard]] static LastseenStatus LongAgo(bool byMe = false) {\n\t\treturn LastseenStatus(kLongAgoValue, false, byMe);\n\t}\n\t[[nodiscard]] static LastseenStatus OnlineTill(\n\t\t\tTimeId till,\n\t\t\tbool local = false,\n\t\t\tbool hiddenByMe = false) {\n\t\treturn (till >= kLifeStartDate + kSpecialValueSkip)\n\t\t\t? LastseenStatus(till - kLifeStartDate, !local, hiddenByMe)\n\t\t\t: LongAgo(hiddenByMe);\n\t}\n\n\t[[nodiscard]] bool isHidden() const {\n\t\treturn !_available;\n\t}\n\t[[nodiscard]] bool isRecently() const {\n\t\treturn !_available && (_value == kRecentlyValue);\n\t}\n\t[[nodiscard]] bool isWithinWeek() const {\n\t\treturn !_available && (_value == kWithinWeekValue);\n\t}\n\t[[nodiscard]] bool isWithinMonth() const {\n\t\treturn !_available && (_value == kWithinMonthValue);\n\t}\n\t[[nodiscard]] bool isLongAgo() const {\n\t\treturn !_available && (_value == kLongAgoValue);\n\t}\n\t[[nodiscard]] bool isHiddenByMe() const {\n\t\treturn _hiddenByMe;\n\t}\n\n\t[[nodiscard]] bool isOnline(TimeId now) const {\n\t\treturn (_value >= kSpecialValueSkip)\n\t\t\t&& (kLifeStartDate + _value > now);\n\t}\n\t[[nodiscard]] bool isLocalOnlineValue() const {\n\t\treturn !_available && (_value >= kSpecialValueSkip);\n\t}\n\t[[nodiscard]] TimeId onlineTill() const {\n\t\treturn (_value >= kSpecialValueSkip)\n\t\t\t? (kLifeStartDate + _value)\n\t\t\t: 0;\n\t}\n\n\t[[nodiscard]] uint32 serialize() const {\n\t\treturn (_value & 0x3FFFFFFF)\n\t\t\t| (_available << 30)\n\t\t\t| (_hiddenByMe << 31);\n\t}\n\t[[nodiscard]] static LastseenStatus FromSerialized(uint32 value) {\n\t\tauto result = LastseenStatus();\n\t\tresult._value = value & 0x3FFFFFFF;\n\t\tresult._available = (value >> 30) & 1;\n\t\tresult._hiddenByMe = (value >> 31) & 1;\n\t\treturn result.valid() ? result : LastseenStatus();\n\t}\n\n\t[[nodiscard]] static LastseenStatus FromLegacy(int32 value) {\n\t\tif (value == -2) {\n\t\t\treturn LastseenStatus::Recently();\n\t\t} else if (value == -3) {\n\t\t\treturn LastseenStatus::WithinWeek();\n\t\t} else if (value == -4) {\n\t\t\treturn LastseenStatus::WithinMonth();\n\t\t} else if (value < -30) {\n\t\t\treturn LastseenStatus::OnlineTill(-value, true);\n\t\t} else if (value > 0) {\n\t\t\treturn LastseenStatus::OnlineTill(value);\n\t\t}\n\t\treturn LastseenStatus();\n\t}\n\n\tfriend inline constexpr auto operator<=>(\n\t\tLastseenStatus,\n\t\tLastseenStatus) = default;\n\tfriend inline constexpr bool operator==(\n\t\tLastseenStatus a,\n\t\tLastseenStatus b) = default;\n\nprivate:\n\tstatic constexpr auto kLongAgoValue = uint32(0);\n\tstatic constexpr auto kRecentlyValue = uint32(1);\n\tstatic constexpr auto kWithinWeekValue = uint32(2);\n\tstatic constexpr auto kWithinMonthValue = uint32(3);\n\tstatic constexpr auto kSpecialValueSkip = uint32(4);\n\tstatic constexpr auto kValidAfter = kLifeStartDate + kSpecialValueSkip;\n\n\t[[nodiscard]] bool valid() const {\n\t\tconstexpr auto kMaxSum = uint32(std::numeric_limits<TimeId>::max());\n\t\treturn (kMaxSum - _value > uint32(kLifeStartDate))\n\t\t\t&& (!_available || (_value >= kSpecialValueSkip));\n\t}\n\n\tLastseenStatus(uint32 value, bool available, bool hiddenByMe)\n\t: _value(value)\n\t, _available(available ? 1 : 0)\n\t, _hiddenByMe(hiddenByMe ? 1 : 0) {\n\t}\n\n\tuint32 _value : 30 = 0;\n\tuint32 _available : 1 = 0;\n\tuint32 _hiddenByMe : 1 = 0;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_location.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_location.h\"\n\n#include \"ui/image/image.h\"\n#include \"data/data_file_origin.h\"\n\nnamespace Data {\nnamespace {\n\n[[nodiscard]] QString AsString(float64 value) {\n\tconstexpr auto kPrecision = 6;\n\treturn QString::number(value, 'f', kPrecision);\n}\n\n} // namespace\n\nLocationPoint::LocationPoint(const MTPDgeoPoint &point)\n: _lat(point.vlat().v)\n, _lon(point.vlong().v)\n, _access(point.vaccess_hash().v) {\n}\n\nLocationPoint::LocationPoint(float64 lat, float64 lon, IgnoreAccessHash)\n: _lat(lat)\n, _lon(lon) {\n}\n\nQString LocationPoint::latAsString() const {\n\treturn AsString(_lat);\n}\n\nQString LocationPoint::lonAsString() const {\n\treturn AsString(_lon);\n}\n\nMTPGeoPoint LocationPoint::toMTP() const {\n\treturn MTP_geoPoint(\n\t\tMTP_flags(0),\n\t\tMTP_double(_lon),\n\t\tMTP_double(_lat),\n\t\tMTP_long(_access),\n\t\tMTP_int(0)); // accuracy_radius\n}\n\nfloat64 LocationPoint::lat() const {\n\treturn _lat;\n}\n\nfloat64 LocationPoint::lon() const {\n\treturn _lon;\n}\n\nuint64 LocationPoint::accessHash() const {\n\treturn _access;\n}\n\nsize_t LocationPoint::hash() const {\n\treturn QtPrivate::QHashCombine().operator()(\n\t\tstd::hash<float64>()(_lat),\n\t\t_lon);\n}\n\nGeoPointLocation ComputeLocation(const LocationPoint &point) {\n\tconst auto scale = 1 + (cScale() * style::DevicePixelRatio()) / 200;\n\tconst auto zoom = 13 + (scale - 1);\n\tconst auto w = st::locationSize.width() / scale;\n\tconst auto h = st::locationSize.height() / scale;\n\n\tauto result = GeoPointLocation();\n\tresult.lat = point.lat();\n\tresult.lon = point.lon();\n\tresult.access = point.accessHash();\n\tresult.width = w;\n\tresult.height = h;\n\tresult.zoom = zoom;\n\tresult.scale = scale;\n\treturn result;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_location.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\nstruct FileOrigin;\n\nclass LocationPoint {\npublic:\n\tLocationPoint() = default;\n\texplicit LocationPoint(const MTPDgeoPoint &point);\n\n\tenum IgnoreAccessHash {\n\t\tNoAccessHash,\n\t};\n\tLocationPoint(float64 lat, float64 lon, IgnoreAccessHash);\n\n\t[[nodiscard]] QString latAsString() const;\n\t[[nodiscard]] QString lonAsString() const;\n\t[[nodiscard]] MTPGeoPoint toMTP() const;\n\n\t[[nodiscard]] float64 lat() const;\n\t[[nodiscard]] float64 lon() const;\n\t[[nodiscard]] uint64 accessHash() const;\n\n\t[[nodiscard]] size_t hash() const;\n\n\tfriend inline bool operator==(\n\t\t\tconst LocationPoint &a,\n\t\t\tconst LocationPoint &b) {\n\t\treturn (a._lat == b._lat) && (a._lon == b._lon);\n\t}\n\n\tfriend inline bool operator<(\n\t\t\tconst LocationPoint &a,\n\t\t\tconst LocationPoint &b) {\n\t\treturn (a._lat < b._lat) || ((a._lat == b._lat) && (a._lon < b._lon));\n\t}\n\nprivate:\n\tfloat64 _lat = 0;\n\tfloat64 _lon = 0;\n\tuint64 _access = 0;\n\n};\n\nstruct InputVenue {\n\tfloat64 lat = 0.;\n\tfloat64 lon = 0.;\n\tQString title;\n\tQString address;\n\tQString provider;\n\tQString id;\n\tQString venueType;\n\n\t[[nodiscard]] bool justLocation() const {\n\t\treturn id.isEmpty();\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst InputVenue &,\n\t\tconst InputVenue &) = default;\n};\n\n[[nodiscard]] GeoPointLocation ComputeLocation(const LocationPoint &point);\n\n} // namespace Data\n\nnamespace std {\n\ntemplate <>\nstruct hash<Data::LocationPoint> {\n\tsize_t operator()(const Data::LocationPoint &value) const {\n\t\treturn value.hash();\n\t}\n};\n\n} // namespace std\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_media_preload.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_media_preload.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"media/streaming/media_streaming_reader.h\"\n#include \"storage/file_download.h\" // kMaxFileInMemory.\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kDefaultPreloadPrefix = 4 * 1024 * 1024;\n\n[[nodiscard]] int64 ChoosePreloadPrefix(not_null<DocumentData*> video) {\n\tconst auto result = video->videoPreloadPrefix();\n\treturn result\n\t\t? result\n\t\t: std::min(int64(kDefaultPreloadPrefix), video->size);\n}\n\n} // namespace\n\nMediaPreload::MediaPreload(Fn<void()> done)\n: _done(std::move(done)) {\n}\n\nvoid MediaPreload::callDone() {\n\tif (const auto onstack = _done) {\n\t\tonstack();\n\t}\n}\n\nPhotoPreload::PhotoPreload(\n\tnot_null<PhotoData*> photo,\n\tFileOrigin origin,\n\tFn<void()> done)\n: MediaPreload(std::move(done))\n, _photo(photo->createMediaView()) {\n\tstart(origin);\n}\n\nPhotoPreload::~PhotoPreload() {\n\tif (_photo) {\n\t\tbase::take(_photo)->owner()->cancel();\n\t}\n}\n\nbool PhotoPreload::Should(\n\t\tnot_null<PhotoData*> photo,\n\t\tnot_null<PeerData*> context) {\n\treturn !photo->cancelled()\n\t\t&& AutoDownload::Should(\n\t\t\tphoto->session().settings().autoDownload(),\n\t\t\tcontext,\n\t\t\tphoto);\n}\n\nvoid PhotoPreload::start(FileOrigin origin) {\n\tif (_photo->loaded()) {\n\t\tcallDone();\n\t} else {\n\t\t_photo->owner()->load(origin, LoadFromCloudOrLocal, true);\n\t\t_photo->owner()->session().downloaderTaskFinished(\n\t\t) | rpl::filter([=] {\n\t\t\treturn _photo->loaded();\n\t\t}) | rpl::on_next([=] { callDone(); }, _lifetime);\n\t}\n}\n\nVideoPreload::VideoPreload(\n\tnot_null<DocumentData*> video,\n\tFileOrigin origin,\n\tFn<void()> done)\n: MediaPreload(std::move(done))\n, DownloadMtprotoTask(\n\t&video->session().downloader(),\n\tvideo->videoPreloadLocation(),\n\torigin)\n, _video(video)\n, _full(video->size) {\n\tif (Can(video)) {\n\t\tcheck();\n\t} else {\n\t\tcallDone();\n\t}\n}\n\nvoid VideoPreload::check() {\n\tconst auto key = _video->bigFileBaseCacheKey();\n\tconst auto weak = base::make_weak(static_cast<has_weak_ptr*>(this));\n\t_video->owner().cacheBigFile().get(key, [weak](\n\t\t\tconst QByteArray &result) {\n\t\tif (!result.isEmpty()) {\n\t\t\tcrl::on_main([weak] {\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstatic_cast<VideoPreload*>(strong)->callDone();\n\t\t\t\t}\n\t\t\t});\n\t\t} else {\n\t\t\tcrl::on_main([weak] {\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstatic_cast<VideoPreload*>(strong)->load();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t});\n}\n\nvoid VideoPreload::load() {\n\tif (!Can(_video)) {\n\t\tcallDone();\n\t\treturn;\n\t}\n\tconst auto prefix = ChoosePreloadPrefix(_video);\n\tAssert(prefix > 0 && prefix <= _video->size);\n\tconst auto part = Storage::kDownloadPartSize;\n\tconst auto parts = (prefix + part - 1) / part;\n\tfor (auto i = 0; i != parts; ++i) {\n\t\t_parts.emplace(i * part, QByteArray());\n\t}\n\taddToQueue();\n}\n\nvoid VideoPreload::done(QByteArray result) {\n\tconst auto key = _video->bigFileBaseCacheKey();\n\tif (!result.isEmpty() && key) {\n\t\tAssert(result.size() < Storage::kMaxFileInMemory);\n\t\t_video->owner().cacheBigFile().putIfEmpty(\n\t\t\tkey,\n\t\t\tStorage::Cache::Database::TaggedValue(std::move(result), 0));\n\t}\n\tcallDone();\n}\n\nVideoPreload::~VideoPreload() {\n\tif (!_finished && !_failed) {\n\t\tcancelAllRequests();\n\t}\n}\n\nbool VideoPreload::Can(not_null<DocumentData*> video) {\n\treturn video->canBeStreamed()\n\t\t&& video->videoPreloadLocation().valid()\n\t\t&& video->bigFileBaseCacheKey();\n}\n\nbool VideoPreload::readyToRequest() const {\n\tconst auto part = Storage::kDownloadPartSize;\n\treturn !_failed && (_nextRequestOffset < _parts.size() * part);\n}\n\nint64 VideoPreload::takeNextRequestOffset() {\n\tExpects(readyToRequest());\n\n\t_requestedOffsets.emplace(_nextRequestOffset);\n\t_nextRequestOffset += Storage::kDownloadPartSize;\n\treturn _requestedOffsets.back();\n}\n\nbool VideoPreload::feedPart(\n\t\tint64 offset,\n\t\tconst QByteArray &bytes) {\n\tExpects(offset < _parts.size() * Storage::kDownloadPartSize);\n\tExpects(_requestedOffsets.contains(int(offset)));\n\tExpects(bytes.size() <= Storage::kDownloadPartSize);\n\n\tconst auto part = Storage::kDownloadPartSize;\n\t_requestedOffsets.remove(int(offset));\n\t_parts[offset] = bytes;\n\tif ((_nextRequestOffset + part >= _parts.size() * part)\n\t\t&& _requestedOffsets.empty()) {\n\t\t_finished = true;\n\t\tremoveFromQueue();\n\t\tauto result = ::Media::Streaming::SerializeComplexPartsMap(_parts);\n\t\tif (result.size() == _full) {\n\t\t\t// Make sure it is parsed as a complex map.\n\t\t\tresult.push_back(char(0));\n\t\t}\n\t\tdone(std::move(result));\n\t}\n\treturn true;\n}\n\nvoid VideoPreload::cancelOnFail() {\n\t_failed = true;\n\tcancelAllRequests();\n\tdone({});\n}\n\nbool VideoPreload::setWebFileSizeHook(int64 size) {\n\t_failed = true;\n\tcancelAllRequests();\n\tdone({});\n\treturn false;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_media_preload.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"storage/download_manager_mtproto.h\"\n\nnamespace Data {\n\nclass PhotoMedia;\nstruct FileOrigin;\n\nclass MediaPreload {\npublic:\n\texplicit MediaPreload(Fn<void()> done);\n\tvirtual ~MediaPreload() = default;\n\nprotected:\n\tvoid callDone();\n\nprivate:\n\tFn<void()> _done;\n\n};\n\nclass PhotoPreload final : public MediaPreload {\npublic:\n\t[[nodiscard]] static bool Should(\n\t\tnot_null<PhotoData*> photo,\n\t\tnot_null<PeerData*> context);\n\n\tPhotoPreload(\n\t\tnot_null<PhotoData*> data,\n\t\tFileOrigin origin,\n\t\tFn<void()> done);\n\t~PhotoPreload();\n\nprivate:\n\tvoid start(FileOrigin origin);\n\n\tstd::shared_ptr<PhotoMedia> _photo;\n\trpl::lifetime _lifetime;\n\n};\n\nclass VideoPreload final\n\t: public MediaPreload\n\t, private Storage::DownloadMtprotoTask {\npublic:\n\t[[nodiscard]] static bool Can(not_null<DocumentData*> video);\n\n\tVideoPreload(\n\t\tnot_null<DocumentData*> video,\n\t\tFileOrigin origin,\n\t\tFn<void()> done);\n\t~VideoPreload();\n\nprivate:\n\tvoid check();\n\tvoid load();\n\tvoid done(QByteArray result);\n\n\tbool readyToRequest() const override;\n\tint64 takeNextRequestOffset() override;\n\tbool feedPart(int64 offset, const QByteArray &bytes) override;\n\tvoid cancelOnFail() override;\n\tbool setWebFileSizeHook(int64 size) override;\n\n\tconst not_null<DocumentData*> _video;\n\tbase::flat_map<uint32, QByteArray> _parts;\n\tbase::flat_set<int> _requestedOffsets;\n\tint64 _full = 0;\n\tint _nextRequestOffset = 0;\n\tbool _finished = false;\n\tbool _failed = false;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_media_rotation.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_media_rotation.h\"\n\nnamespace Data {\nnamespace {\n\n[[nodiscard]] int NormalizeRotation(int rotation) {\n\tconst auto result = rotation\n\t\t- ((rotation / 360) - ((rotation < 0) ? 1 : 0)) * 360;\n\n\tEnsures(result >= 0 && result < 360);\n\treturn result;\n}\n\n} // namespace\n\nvoid MediaRotation::set(not_null<PhotoData*> photo, int rotation) {\n\tif (rotation % 360) {\n\t\t_photoRotations[photo] = NormalizeRotation(rotation);\n\t} else {\n\t\t_photoRotations.remove(photo);\n\t}\n}\n\nint MediaRotation::get(not_null<PhotoData*> photo) const {\n\tconst auto i = _photoRotations.find(photo);\n\treturn (i != end(_photoRotations)) ? i->second : 0;\n}\n\nvoid MediaRotation::set(not_null<DocumentData*> document, int rotation) {\n\tif (rotation % 360) {\n\t\t_documentRotations[document] = NormalizeRotation(rotation);\n\t} else {\n\t\t_documentRotations.remove(document);\n\t}\n}\n\nint MediaRotation::get(not_null<DocumentData*> document) const {\n\tconst auto i = _documentRotations.find(document);\n\treturn (i != end(_documentRotations)) ? i->second : 0;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_media_rotation.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PhotoData;\nclass DocumentData;\n\nnamespace Data {\n\nclass MediaRotation final {\npublic:\n\tvoid set(not_null<PhotoData*> photo, int rotation);\n\t[[nodiscard]] int get(not_null<PhotoData*> photo) const;\n\n\tvoid set(not_null<DocumentData*> document, int rotation);\n\t[[nodiscard]] int get(not_null<DocumentData*> document) const;\n\nprivate:\n\tbase::flat_map<not_null<PhotoData*>, int> _photoRotations;\n\tbase::flat_map<not_null<DocumentData*>, int> _documentRotations;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_media_types.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_media_types.h\"\n\n#include \"base/random.h\"\n#include \"boxes/send_credits_box.h\" // CreditsEmoji.\n#include \"history/history.h\"\n#include \"history/history_item.h\" // CreateMedia.\n#include \"history/history_item_components.h\"\n#include \"history/history_location_manager.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_item_preview.h\"\n#include \"history/view/media/history_view_birthday_suggestion.h\"\n#include \"history/view/media/history_view_photo.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"history/view/media/history_view_gif.h\"\n#include \"history/view/media/history_view_document.h\"\n#include \"history/view/media/history_view_contact.h\"\n#include \"history/view/media/history_view_location.h\"\n#include \"history/view/media/history_view_game.h\"\n#include \"history/view/media/history_view_giveaway.h\"\n#include \"history/view/media/history_view_invoice.h\"\n#include \"history/view/media/history_view_media_generic.h\"\n#include \"history/view/media/history_view_media_grouped.h\"\n#include \"history/view/media/history_view_call.h\"\n#include \"history/view/media/history_view_web_page.h\"\n#include \"history/view/media/history_view_poll.h\"\n#include \"history/view/media/history_view_theme_document.h\"\n#include \"history/view/media/history_view_todo_list.h\"\n#include \"history/view/media/history_view_slot_machine.h\"\n#include \"history/view/media/history_view_dice.h\"\n#include \"history/view/media/history_view_service_box.h\"\n#include \"history/view/media/history_view_story_mention.h\"\n#include \"history/view/media/history_view_premium_gift.h\"\n#include \"history/view/media/history_view_unique_gift.h\"\n#include \"history/view/media/history_view_userpic_suggestion.h\"\n#include \"dialogs/ui/dialogs_message_view.h\"\n#include \"ui/boxes/emoji_stake_box.h\"\n#include \"ui/image/image.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/text/format_song_document_name.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_entity.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/emoji_config.h\"\n#include \"api/api_sending.h\"\n#include \"api/api_transcribes.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"storage/localstorage.h\"\n#include \"chat_helpers/stickers_dice_pack.h\" // Stickers::DicePacks::IsSlot.\n#include \"chat_helpers/stickers_gift_box_pack.h\"\n#include \"data/data_session.h\"\n#include \"data/data_auto_download.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_game.h\"\n#include \"data/data_web_page.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_story.h\"\n#include \"data/data_todo_list.h\"\n#include \"data/data_user.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"calls/calls_instance.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\" // ClickHandlerContext\n#include \"lang/lang_keys.h\"\n#include \"storage/file_upload.h\"\n#include \"window/window_session_controller.h\" // SessionController::uiShow.\n#include \"apiwrap.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kFastRevokeRestriction = 24 * 60 * TimeId(60);\nconstexpr auto kMaxPreviewImages = 3;\nconstexpr auto kLoadingStoryPhotoId = PhotoId(0x7FFF'DEAD'FFFF'FFFFULL);\n\nusing ItemPreview = HistoryView::ItemPreview;\nusing ItemPreviewImage = HistoryView::ItemPreviewImage;\n\nstruct AlbumCounts {\n\tint photos = 0;\n\tint videos = 0;\n\tint audios = 0;\n\tint files = 0;\n};\n\n[[nodiscard]] TextWithEntities WithCaptionNotificationText(\n\t\tconst QString &attachType,\n\t\tconst TextWithEntities &caption,\n\t\tbool hasMiniImages = false) {\n\tif (caption.text.isEmpty()) {\n\t\treturn Ui::Text::Colorized(attachType);\n\t}\n\tauto wrapped = st::wrap_rtl(caption);\n\treturn hasMiniImages\n\t\t? wrapped\n\t\t: tr::lng_dialogs_text_media(\n\t\t\ttr::now,\n\t\t\tlt_media_part,\n\t\t\ttr::lng_dialogs_text_media_wrapped(\n\t\t\t\ttr::now,\n\t\t\t\tlt_media,\n\t\t\t\tUi::Text::Colorized(attachType),\n\t\t\t\ttr::marked),\n\t\t\tlt_caption,\n\t\t\twrapped,\n\t\t\ttr::marked);\n}\n\n[[nodiscard]] QImage PreparePreviewImage(\n\t\tnot_null<const Image*> image,\n\t\tImageRoundRadius radius,\n\t\tbool spoiler) {\n\tconst auto original = image->original();\n\tif (original.width() * 20 < original.height()\n\t\t|| original.height() * 20 < original.width()) {\n\t\treturn QImage();\n\t}\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto size = st::dialogsMiniPreview * factor;\n\tconst auto scaled = original.scaled(\n\t\tQSize(size, size),\n\t\tQt::KeepAspectRatioByExpanding,\n\t\tQt::SmoothTransformation);\n\tauto square = scaled.copy(\n\t\t(scaled.width() - size) / 2,\n\t\t(scaled.height() - size) / 2,\n\t\tsize,\n\t\tsize\n\t).convertToFormat(QImage::Format_ARGB32_Premultiplied);\n\tif (spoiler) {\n\t\tsquare = Images::BlurLargeImage(\n\t\t\tstd::move(square),\n\t\t\tstyle::ConvertScale(3) * factor);\n\t}\n\tif (radius == ImageRoundRadius::Small) {\n\t\tstruct Cache {\n\t\t\tbase::flat_map<int, std::array<QImage, 4>> all;\n\t\t\tstd::array<QImage, 4> *lastUsed = nullptr;\n\t\t\tint lastUsedRadius = 0;\n\t\t};\n\t\tstatic auto cache = Cache();\n\t\tconst auto pxRadius = st::dialogsMiniPreviewRadius;\n\t\tif (!cache.lastUsed || cache.lastUsedRadius != pxRadius) {\n\t\t\tcache.lastUsedRadius = pxRadius;\n\t\t\tconst auto i = cache.all.find(pxRadius);\n\t\t\tif (i != end(cache.all)) {\n\t\t\t\tcache.lastUsed = &i->second;\n\t\t\t} else {\n\t\t\t\tcache.lastUsed = &cache.all.emplace(\n\t\t\t\t\tpxRadius,\n\t\t\t\t\tImages::CornersMask(pxRadius)).first->second;\n\t\t\t}\n\t\t}\n\t\tsquare = Images::Round(std::move(square), *cache.lastUsed);\n\t} else {\n\t\tsquare = Images::Round(std::move(square), radius);\n\t}\n\tsquare.setDevicePixelRatio(factor);\n\treturn square;\n}\n\ntemplate <typename MediaType>\n[[nodiscard]] uint64 CountCacheKey(\n\t\tnot_null<MediaType*> data,\n\t\tImageRoundRadius radius,\n\t\tbool spoiler) {\n\treturn (reinterpret_cast<uint64>(data.get()) & ~3)\n\t\t| ((radius == ImageRoundRadius::Ellipse) ? 2 : 0)\n\t\t| (spoiler ? 1 : 0);\n}\n\n[[nodiscard]] uint64 SimpleCacheKey(ImageRoundRadius radius, bool spoiler) {\n\treturn uint64()\n\t\t| ((radius == ImageRoundRadius::Ellipse) ? 2 : 0)\n\t\t| (spoiler ? 1 : 0);\n}\n\n[[nodiscard]] ItemPreviewImage PreparePhotoPreviewImage(\n\t\tnot_null<const HistoryItem*> item,\n\t\tconst std::shared_ptr<PhotoMedia> &media,\n\t\tImageRoundRadius radius,\n\t\tbool spoiler) {\n\tconst auto photo = media->owner();\n\tconst auto counted = CountCacheKey(photo, radius, spoiler);\n\tif (const auto small = media->image(PhotoSize::Small)) {\n\t\treturn { PreparePreviewImage(small, radius, spoiler), counted };\n\t} else if (const auto thumbnail = media->image(PhotoSize::Thumbnail)) {\n\t\treturn { PreparePreviewImage(thumbnail, radius, spoiler), counted };\n\t} else if (const auto large = media->image(PhotoSize::Large)) {\n\t\treturn { PreparePreviewImage(large, radius, spoiler), counted };\n\t}\n\tconst auto allowedToDownload = media->autoLoadThumbnailAllowed(\n\t\titem->history()->peer);\n\tconst auto simple = SimpleCacheKey(radius, spoiler);\n\tconst auto cacheKey = allowedToDownload ? simple : counted;\n\tif (allowedToDownload) {\n\t\tmedia->owner()->load(PhotoSize::Small, item->fullId());\n\t}\n\tif (const auto blurred = media->thumbnailInline()) {\n\t\treturn { PreparePreviewImage(blurred, radius, spoiler), cacheKey };\n\t}\n\treturn { QImage(), allowedToDownload ? simple : cacheKey };\n}\n\n[[nodiscard]] ItemPreviewImage PrepareFilePreviewImage(\n\t\tnot_null<const HistoryItem*> item,\n\t\tconst std::shared_ptr<DocumentMedia> &media,\n\t\tImageRoundRadius radius,\n\t\tbool spoiler) {\n\tExpects(media->owner()->hasThumbnail());\n\n\tconst auto document = media->owner();\n\tif (const auto thumbnail = media->thumbnail()) {\n\t\tconst auto readyCacheKey = CountCacheKey(document, radius, spoiler);\n\t\treturn {\n\t\t\tPreparePreviewImage(thumbnail, radius, spoiler),\n\t\t\treadyCacheKey,\n\t\t};\n\t}\n\tdocument->loadThumbnail(item->fullId());\n\tconst auto simple = SimpleCacheKey(radius, spoiler);\n\tif (const auto blurred = media->thumbnailInline()) {\n\t\treturn { PreparePreviewImage(blurred, radius, spoiler), simple };\n\t}\n\treturn { QImage(), simple };\n}\n\n[[nodiscard]] QImage PutPlayIcon(QImage preview) {\n\tExpects(!preview.isNull());\n\n\t{\n\t\tQPainter p(&preview);\n\t\tst::dialogsMiniPlay.paintInCenter(\n\t\t\tp,\n\t\t\tQRect(QPoint(), preview.size() / preview.devicePixelRatio()));\n\t}\n\treturn preview;\n}\n\n[[nodiscard]] ItemPreviewImage PreparePhotoPreview(\n\t\tnot_null<const HistoryItem*> item,\n\t\tconst std::shared_ptr<PhotoMedia> &media,\n\t\tImageRoundRadius radius,\n\t\tbool spoiler) {\n\tauto result = PreparePhotoPreviewImage(item, media, radius, spoiler);\n\tif (!result.data.isNull()\n\t\t&& (media->owner()->extendedMediaVideoDuration().has_value()\n\t\t\t|| (item->media() && item->media()->videoCover()))) {\n\t\tresult.data = PutPlayIcon(std::move(result.data));\n\t}\n\treturn result;\n}\n\n[[nodiscard]] ItemPreviewImage PrepareFilePreview(\n\t\tnot_null<const HistoryItem*> item,\n\t\tconst std::shared_ptr<DocumentMedia> &media,\n\t\tImageRoundRadius radius,\n\t\tbool spoiler) {\n\tauto result = PrepareFilePreviewImage(item, media, radius, spoiler);\n\tconst auto document = media->owner();\n\tif (!result.data.isNull()\n\t\t&& (document->isVideoFile() || document->isVideoMessage())) {\n\t\tresult.data = PutPlayIcon(std::move(result.data));\n\t}\n\treturn result;\n}\n\n[[nodiscard]] bool TryFilePreview(not_null<DocumentData*> document) {\n\treturn document->hasThumbnail()\n\t\t&& !document->sticker()\n\t\t&& !document->isAudioFile();\n}\n\ntemplate <typename MediaType>\n[[nodiscard]] ItemPreviewImage FindCachedPreview(\n\t\tconst std::vector<ItemPreviewImage> *existing,\n\t\tnot_null<MediaType*> data,\n\t\tImageRoundRadius radius,\n\t\tbool spoiler) {\n\tif (!existing) {\n\t\treturn {};\n\t}\n\tconst auto i = ranges::find(\n\t\t*existing,\n\t\tCountCacheKey(data, radius, spoiler),\n\t\t&ItemPreviewImage::cacheKey);\n\treturn (i != end(*existing)) ? *i : ItemPreviewImage();\n}\n\nbool UpdateExtendedMedia(\n\t\tstd::unique_ptr<Media> &media,\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPMessageExtendedMedia &extended) {\n\treturn extended.match([&](const MTPDmessageExtendedMediaPreview &data) {\n\t\tauto photo = (PhotoData*)nullptr;\n\t\tif (!media) {\n\t\t\tconst auto id = base::RandomValue<PhotoId>();\n\t\t\tphoto = item->history()->owner().photo(id);\n\t\t} else {\n\t\t\tphoto = media->photo();\n\t\t\tif (!photo || !photo->extendedMediaPreview()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\tauto changed = false;\n\t\tauto size = QSize();\n\t\tauto thumbnail = QByteArray();\n\t\tauto videoDuration = std::optional<TimeId>();\n\t\tif (const auto &w = data.vw()) {\n\t\t\tconst auto &h = data.vh();\n\t\t\tAssert(h.has_value());\n\t\t\tsize = QSize(w->v, h->v);\n\t\t\tif (!changed && photo->size(PhotoSize::Large) != size) {\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t}\n\t\tif (const auto &thumb = data.vthumb()) {\n\t\t\tif (thumb->type() == mtpc_photoStrippedSize) {\n\t\t\t\tthumbnail = thumb->c_photoStrippedSize().vbytes().v;\n\t\t\t\tif (!changed && photo->inlineThumbnailBytes() != thumbnail) {\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (const auto &duration = data.vvideo_duration()) {\n\t\t\tvideoDuration = duration->v;\n\t\t\tif (photo->extendedMediaVideoDuration() != videoDuration) {\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t} else if (photo->extendedMediaVideoDuration().has_value()) {\n\t\t\tchanged = true;\n\t\t}\n\t\tif (changed) {\n\t\t\tphoto->setExtendedMediaPreview(size, thumbnail, videoDuration);\n\t\t}\n\t\tif (!media) {\n\t\t\tmedia = std::make_unique<MediaPhoto>(item, photo, true);\n\t\t}\n\t\treturn changed;\n\t}, [&](const MTPDmessageExtendedMedia &data) {\n\t\tmedia = HistoryItem::CreateMedia(item, data.vmedia());\n\t\treturn true;\n\t});\n}\n\nbool UpdateExtendedMedia(\n\t\tInvoice &invoice,\n\t\tnot_null<HistoryItem*> item,\n\t\tconst QVector<MTPMessageExtendedMedia> &media) {\n\tauto changed = false;\n\tconst auto count = int(media.size());\n\tfor (auto i = 0; i != count; ++i) {\n\t\tif (i <= invoice.extendedMedia.size()) {\n\t\t\tinvoice.extendedMedia.emplace_back();\n\t\t\tchanged = true;\n\t\t}\n\t\tUpdateExtendedMedia(invoice.extendedMedia[i], item, media[i]);\n\t}\n\tif (count < invoice.extendedMedia.size()) {\n\t\tinvoice.extendedMedia.resize(count);\n\t\tchanged = true;\n\t}\n\treturn changed;\n}\n\nTextForMimeData WithCaptionClipboardText(\n\t\tconst QString &attachType,\n\t\tTextForMimeData &&caption) {\n\tauto result = TextForMimeData();\n\tif (attachType.isEmpty()) {\n\t\tresult.reserve(1 + caption.expanded.size());\n\t\tif (!caption.empty()) {\n\t\t\tresult.append(std::move(caption));\n\t\t}\n\t} else {\n\t\tresult.reserve(5 + attachType.size() + caption.expanded.size());\n\t\tresult.append(u\"[ \"_q).append(attachType).append(u\" ]\"_q);\n\t\tif (!caption.empty()) {\n\t\t\tresult.append('\\n').append(std::move(caption));\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] QString ComputeAlbumCountsString(AlbumCounts counts) {\n\tconst auto medias = counts.photos + counts.videos;\n\treturn (counts.photos && counts.videos)\n\t\t? tr::lng_in_dlg_media_count(tr::now, lt_count, medias)\n\t\t: (counts.photos > 1)\n\t\t? tr::lng_in_dlg_photo_count(tr::now, lt_count, counts.photos)\n\t\t: counts.photos\n\t\t? tr::lng_in_dlg_photo(tr::now)\n\t\t: (counts.videos > 1)\n\t\t? tr::lng_in_dlg_video_count(tr::now, lt_count, counts.videos)\n\t\t: counts.videos\n\t\t? tr::lng_in_dlg_video(tr::now)\n\t\t: (counts.audios > 1)\n\t\t? tr::lng_in_dlg_audio_count(tr::now, lt_count, counts.audios)\n\t\t: counts.audios\n\t\t? tr::lng_in_dlg_audio(tr::now)\n\t\t: (counts.files > 1)\n\t\t? tr::lng_in_dlg_file_count(tr::now, lt_count, counts.files)\n\t\t: counts.files\n\t\t? tr::lng_in_dlg_file(tr::now)\n\t\t: tr::lng_in_dlg_album(tr::now);\n}\n\n} // namespace\n\nInvoice ComputeInvoiceData(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPDmessageMediaInvoice &data) {\n\tauto description = qs(data.vdescription());\n\tauto result = Invoice{\n\t\t.receiptMsgId = data.vreceipt_msg_id().value_or_empty(),\n\t\t.amount = data.vtotal_amount().v,\n\t\t.currency = qs(data.vcurrency()),\n\t\t.title = TextUtilities::SingleLine(qs(data.vtitle())),\n\t\t.description = TextUtilities::ParseEntities(\n\t\t\tdescription,\n\t\t\tTextParseLinks | TextParseMultiline),\n\t\t.photo = (data.vphoto()\n\t\t\t? item->history()->owner().photoFromWeb(\n\t\t\t\t*data.vphoto(),\n\t\t\t\tImageLocation())\n\t\t\t: nullptr),\n\t\t.isTest = data.is_test(),\n\t};\n\tif (const auto &media = data.vextended_media()) {\n\t\tUpdateExtendedMedia(result, item, { *media });\n\t}\n\treturn result;\n}\n\nInvoice ComputeInvoiceData(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPDmessageMediaPaidMedia &data) {\n\tauto result = Invoice{\n\t\t.amount = data.vstars_amount().v,\n\t\t.currency = Ui::kCreditsCurrency,\n\t\t.isPaidMedia = true,\n\t};\n\tUpdateExtendedMedia(result, item, data.vextended_media().v);\n\treturn result;\n}\n\nCall ComputeCallData(\n\t\tnot_null<Session*> owner,\n\t\tconst MTPDmessageActionPhoneCall &call) {\n\tauto result = Call();\n\tresult.state = [&] {\n\t\tif (const auto reason = call.vreason()) {\n\t\t\treturn reason->match([](const MTPDphoneCallDiscardReasonBusy &) {\n\t\t\t\treturn CallState::Busy;\n\t\t\t}, [](const MTPDphoneCallDiscardReasonDisconnect &) {\n\t\t\t\treturn CallState::Disconnected;\n\t\t\t}, [](const MTPDphoneCallDiscardReasonHangup &) {\n\t\t\t\treturn CallState::Hangup;\n\t\t\t}, [](const MTPDphoneCallDiscardReasonMissed &) {\n\t\t\t\treturn CallState::Missed;\n\t\t\t}, [](const MTPDphoneCallDiscardReasonMigrateConferenceCall &) {\n\t\t\t\treturn CallState::MigrateConferenceCall;\n\t\t\t});\n\t\t\tUnexpected(\"Call reason type.\");\n\t\t}\n\t\treturn CallState::Hangup;\n\t}();\n\tresult.duration = call.vduration().value_or_empty();\n\tresult.video = call.is_video();\n\treturn result;\n}\n\nCall ComputeCallData(\n\t\tnot_null<Session*> owner,\n\t\tconst MTPDmessageActionConferenceCall &call) {\n\tauto participants = std::vector<not_null<PeerData*>>();\n\tif (const auto list = call.vother_participants()) {\n\t\tparticipants.reserve(list->v.size());\n\t\tfor (const auto &participant : list->v) {\n\t\t\tparticipants.push_back(owner->peer(peerFromMTP(participant)));\n\t\t}\n\t}\n\treturn {\n\t\t.otherParticipants = std::move(participants),\n\t\t.conferenceId = call.vcall_id().v,\n\t\t.duration = call.vduration().value_or_empty(),\n\t\t.state = (call.vduration().value_or_empty()\n\t\t\t? CallState::Hangup\n\t\t\t: call.is_missed()\n\t\t\t? CallState::Missed\n\t\t\t: call.is_active()\n\t\t\t? CallState::Active\n\t\t\t: CallState::Invitation),\n\t\t.video = call.is_video(),\n\t};\n}\n\nGiveawayStart ComputeGiveawayStartData(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPDmessageMediaGiveaway &data) {\n\tauto result = GiveawayStart{\n\t\t.untilDate = data.vuntil_date().v,\n\t\t.quantity = data.vquantity().v,\n\t\t.months = data.vmonths().value_or_empty(),\n\t\t.credits = data.vstars().value_or_empty(),\n\t\t.all = !data.is_only_new_subscribers(),\n\t};\n\tresult.channels.reserve(data.vchannels().v.size());\n\tconst auto owner = &item->history()->owner();\n\tfor (const auto &id : data.vchannels().v) {\n\t\tresult.channels.push_back(owner->channel(ChannelId(id)));\n\t}\n\tif (const auto countries = data.vcountries_iso2()) {\n\t\tresult.countries.reserve(countries->v.size());\n\t\tfor (const auto &country : countries->v) {\n\t\t\tresult.countries.push_back(qs(country));\n\t\t}\n\t}\n\tif (const auto additional = data.vprize_description()) {\n\t\tresult.additionalPrize = qs(*additional);\n\t}\n\treturn result;\n}\n\nGiveawayResults ComputeGiveawayResultsData(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPDmessageMediaGiveawayResults &data) {\n\tconst auto additional = data.vadditional_peers_count();\n\tauto result = GiveawayResults{\n\t\t.channel = item->history()->owner().channel(data.vchannel_id()),\n\t\t.untilDate = data.vuntil_date().v,\n\t\t.launchId = data.vlaunch_msg_id().v,\n\t\t.additionalPeersCount = additional.value_or_empty(),\n\t\t.winnersCount = data.vwinners_count().v,\n\t\t.unclaimedCount = data.vunclaimed_count().v,\n\t\t.months = data.vmonths().value_or_empty(),\n\t\t.credits = data.vstars().value_or_empty(),\n\t\t.refunded = data.is_refunded(),\n\t\t.all = !data.is_only_new_subscribers(),\n\t};\n\tresult.winners.reserve(data.vwinners().v.size());\n\tconst auto owner = &item->history()->owner();\n\tfor (const auto &id : data.vwinners().v) {\n\t\tresult.winners.push_back(owner->user(UserId(id)));\n\t}\n\tif (const auto additional = data.vprize_description()) {\n\t\tresult.additionalPrize = qs(*additional);\n\t}\n\treturn result;\n}\n\nbool HasExtendedMedia(const Invoice &invoice) {\n\treturn !invoice.extendedMedia.empty();\n}\n\nbool HasUnpaidMedia(const Invoice &invoice) {\n\tfor (const auto &media : invoice.extendedMedia) {\n\t\tconst auto photo = media->photo();\n\t\treturn photo && photo->extendedMediaPreview();\n\t}\n\treturn false;\n}\n\nbool IsFirstVideo(const Invoice &invoice) {\n\tif (invoice.extendedMedia.empty()) {\n\t\treturn false;\n\t} else if (const auto photo = invoice.extendedMedia.front()->photo()) {\n\t\treturn photo->extendedMediaVideoDuration().has_value();\n\t}\n\treturn true;\n}\n\nMedia::Media(not_null<HistoryItem*> parent) : _parent(parent) {\n}\n\nnot_null<HistoryItem*> Media::parent() const {\n\treturn _parent;\n}\n\nDocumentData *Media::document() const {\n\treturn nullptr;\n}\n\nPhotoData *Media::videoCover() const {\n\treturn nullptr;\n}\n\nTimeId Media::videoTimestamp() const {\n\treturn 0;\n}\n\nbool Media::hasQualitiesList() const {\n\treturn false;\n}\n\nPhotoData *Media::photo() const {\n\treturn nullptr;\n}\n\nWebPageData *Media::webpage() const {\n\treturn nullptr;\n}\n\nMediaWebPageFlags Media::webpageFlags() const {\n\treturn {};\n}\n\nconst SharedContact *Media::sharedContact() const {\n\treturn nullptr;\n}\n\nconst Call *Media::call() const {\n\treturn nullptr;\n}\n\nGameData *Media::game() const {\n\treturn nullptr;\n}\n\nconst Invoice *Media::invoice() const {\n\treturn nullptr;\n}\n\nconst GiftCode *Media::gift() const {\n\treturn nullptr;\n}\n\nCloudImage *Media::location() const {\n\treturn nullptr;\n}\n\nPollData *Media::poll() const {\n\treturn nullptr;\n}\n\nTodoListData *Media::todolist() const {\n\treturn nullptr;\n}\n\nconst WallPaper *Media::paper() const {\n\treturn nullptr;\n}\n\nbool Media::paperForBoth() const {\n\treturn false;\n}\n\nFullStoryId Media::storyId() const {\n\treturn {};\n}\n\nbool Media::storyExpired(bool revalidate) {\n\treturn false;\n}\n\nbool Media::storyUnsupported() const {\n\treturn false;\n}\n\nbool Media::storyMention() const {\n\treturn false;\n}\n\nconst GiveawayStart *Media::giveawayStart() const {\n\treturn nullptr;\n}\n\nconst GiveawayResults *Media::giveawayResults() const {\n\treturn nullptr;\n}\n\nDiceGameOutcome Media::diceGameOutcome() const {\n\treturn {};\n}\n\nbool Media::uploading() const {\n\treturn false;\n}\n\nStorage::SharedMediaTypesMask Media::sharedMediaTypes() const {\n\treturn {};\n}\n\nbool Media::canBeGrouped() const {\n\treturn false;\n}\n\nItemPreview Media::toPreview(ToPreviewOptions options) const {\n\treturn { .text = notificationText() };\n}\n\nbool Media::hasReplyPreview() const {\n\treturn false;\n}\n\nImage *Media::replyPreview() const {\n\treturn nullptr;\n}\n\nbool Media::replyPreviewLoaded() const {\n\treturn true;\n}\n\nbool Media::allowsForward() const {\n\treturn true;\n}\n\nbool Media::allowsEdit() const {\n\treturn allowsEditCaption();\n}\n\nbool Media::allowsEditCaption() const {\n\treturn false;\n}\n\nbool Media::allowsEditMedia() const {\n\treturn false;\n}\n\nbool Media::allowsRevoke(TimeId now) const {\n\treturn true;\n}\n\nbool Media::forwardedBecomesUnread() const {\n\treturn false;\n}\n\nbool Media::dropForwardedInfo() const {\n\treturn false;\n}\n\nbool Media::forceForwardedInfo() const {\n\treturn false;\n}\n\nbool Media::hasSpoiler() const {\n\treturn false;\n}\n\ncrl::time Media::ttlSeconds() const {\n\treturn 0;\n}\n\nbool Media::consumeMessageText(const TextWithEntities &text) {\n\treturn false;\n}\n\nTextWithEntities Media::consumedMessageText() const {\n\treturn {};\n}\n\nstd::unique_ptr<HistoryView::Media> Media::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tHistoryView::Element *replacing) {\n\treturn createView(message, message->data(), replacing);\n}\n\nItemPreview Media::toGroupPreview(\n\t\tconst HistoryItemsList &items,\n\t\tToPreviewOptions options) const {\n\tauto result = ItemPreview();\n\tauto loadingContext = std::vector<std::any>();\n\tauto counts = AlbumCounts();\n\tauto manyCaptions = false;\n\tfor (const auto &item : items) {\n\t\tif (const auto media = item->media()) {\n\t\t\tif (media->photo()) {\n\t\t\t\tcounts.photos++;\n\t\t\t} else if (const auto document = media->document()) {\n\t\t\t\t(document->isVideoFile()\n\t\t\t\t\t? counts.videos\n\t\t\t\t\t: document->isAudioFile()\n\t\t\t\t\t? counts.audios\n\t\t\t\t\t: counts.files)++;\n\t\t\t}\n\t\t\tauto copy = options;\n\t\t\tcopy.ignoreGroup = true;\n\t\t\tconst auto already = int(result.images.size());\n\t\t\tconst auto left = kMaxPreviewImages - already;\n\t\t\tauto single = left ? media->toPreview(copy) : ItemPreview();\n\t\t\tif (!single.images.empty()) {\n\t\t\t\twhile (single.images.size() > left) {\n\t\t\t\t\tsingle.images.pop_back();\n\t\t\t\t}\n\t\t\t\tresult.images.insert(\n\t\t\t\t\tend(result.images),\n\t\t\t\t\tstd::make_move_iterator(begin(single.images)),\n\t\t\t\t\tstd::make_move_iterator(end(single.images)));\n\t\t\t}\n\t\t\tif (single.loadingContext.has_value()) {\n\t\t\t\tloadingContext.push_back(std::move(single.loadingContext));\n\t\t\t}\n\t\t\tconst auto original = item->originalText();\n\t\t\tif (!original.text.isEmpty()) {\n\t\t\t\tif (result.text.text.isEmpty()) {\n\t\t\t\t\tresult.text = original;\n\t\t\t\t} else {\n\t\t\t\t\tmanyCaptions = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (manyCaptions || result.text.text.isEmpty()) {\n\t\tresult.text = Ui::Text::Colorized(ComputeAlbumCountsString(counts));\n\t}\n\tif (!loadingContext.empty()) {\n\t\tresult.loadingContext = std::move(loadingContext);\n\t}\n\treturn result;\n}\n\nMediaPhoto::MediaPhoto(\n\tnot_null<HistoryItem*> parent,\n\tnot_null<PhotoData*> photo,\n\tbool spoiler)\n: Media(parent)\n, _photo(photo)\n, _spoiler(spoiler) {\n\tparent->history()->owner().registerPhotoItem(_photo, parent);\n\n\tif (_spoiler) {\n\t\tUi::PreloadImageSpoiler();\n\t}\n}\n\nMediaPhoto::MediaPhoto(\n\tnot_null<HistoryItem*> parent,\n\tnot_null<PeerData*> chat,\n\tnot_null<PhotoData*> photo)\n: Media(parent)\n, _photo(photo)\n, _chat(chat) {\n\tparent->history()->owner().registerPhotoItem(_photo, parent);\n}\n\nMediaPhoto::~MediaPhoto() {\n\tif (uploading() && !Core::Quitting()) {\n\t\tparent()->history()->session().uploader().cancel(parent()->fullId());\n\t}\n\tparent()->history()->owner().unregisterPhotoItem(_photo, parent());\n}\n\nstd::unique_ptr<Media> MediaPhoto::clone(not_null<HistoryItem*> parent) {\n\treturn _chat\n\t\t? std::make_unique<MediaPhoto>(parent, _chat, _photo)\n\t\t: std::make_unique<MediaPhoto>(parent, _photo, _spoiler);\n}\n\nPhotoData *MediaPhoto::photo() const {\n\treturn _photo;\n}\n\nbool MediaPhoto::uploading() const {\n\treturn _photo->uploading();\n}\n\nStorage::SharedMediaTypesMask MediaPhoto::sharedMediaTypes() const {\n\tusing Type = Storage::SharedMediaType;\n\tif (_chat) {\n\t\treturn Type::ChatPhoto;\n\t}\n\treturn Storage::SharedMediaTypesMask{}\n\t\t.added(Type::Photo)\n\t\t.added(Type::PhotoVideo);\n}\n\nbool MediaPhoto::canBeGrouped() const {\n\treturn true;\n}\n\nbool MediaPhoto::hasReplyPreview() const {\n\treturn !_photo->isNull();\n}\n\nImage *MediaPhoto::replyPreview() const {\n\treturn _photo->getReplyPreview(parent());\n}\n\nbool MediaPhoto::replyPreviewLoaded() const {\n\treturn _photo->replyPreviewLoaded(_spoiler);\n}\n\nTextWithEntities MediaPhoto::notificationText() const {\n\treturn WithCaptionNotificationText(\n\t\ttr::lng_in_dlg_photo(tr::now),\n\t\tparent()->originalText());\n}\n\nItemPreview MediaPhoto::toPreview(ToPreviewOptions options) const {\n\tconst auto item = parent();\n\tif (!options.ignoreGroup && item->groupId()) {\n\t\tif (const auto group = item->history()->owner().groups().find(item)\n\t\t\t; group && group->items.size() > 1) {\n\t\t\treturn toGroupPreview(group->items, options);\n\t\t}\n\t}\n\tauto images = std::vector<ItemPreviewImage>();\n\tauto context = std::any();\n\tconst auto radius = _chat\n\t\t? ImageRoundRadius::Ellipse\n\t\t: ImageRoundRadius::Small;\n\tif (auto found = FindCachedPreview(\n\t\t\toptions.existing,\n\t\t\t_photo,\n\t\t\tradius,\n\t\t\t_spoiler)) {\n\t\timages.push_back(std::move(found));\n\t} else {\n\t\tconst auto media = _photo->createMediaView();\n\t\tif (auto prepared = PreparePhotoPreview(\n\t\t\t\tparent(),\n\t\t\t\tmedia,\n\t\t\t\tradius,\n\t\t\t\t_spoiler)\n\t\t\t; prepared || !prepared.cacheKey) {\n\t\t\timages.push_back(std::move(prepared));\n\t\t\tif (!prepared.cacheKey) {\n\t\t\t\tcontext = media;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto type = tr::lng_in_dlg_photo(tr::now);\n\tconst auto caption = (options.hideCaption || options.ignoreMessageText)\n\t\t? TextWithEntities()\n\t\t: Dialogs::Ui::DialogsPreviewText(options.translated\n\t\t\t? parent()->translatedText()\n\t\t\t: parent()->originalText());\n\tconst auto hasMiniImages = !images.empty();\n\treturn {\n\t\t.text = WithCaptionNotificationText(type, caption, hasMiniImages),\n\t\t.images = std::move(images),\n\t\t.loadingContext = std::move(context),\n\t};\n}\n\nQString MediaPhoto::pinnedTextSubstring() const {\n\treturn tr::lng_action_pinned_media_photo(tr::now);\n}\n\nTextForMimeData MediaPhoto::clipboardText() const {\n\treturn TextForMimeData();\n}\n\nbool MediaPhoto::allowsEditCaption() const {\n\treturn true;\n}\n\nbool MediaPhoto::allowsEditMedia() const {\n\treturn true;\n}\n\nbool MediaPhoto::hasSpoiler() const {\n\treturn _spoiler;\n}\n\nbool MediaPhoto::updateInlineResultMedia(const MTPMessageMedia &media) {\n\tif (media.type() != mtpc_messageMediaPhoto) {\n\t\treturn false;\n\t}\n\tconst auto &data = media.c_messageMediaPhoto();\n\tconst auto content = data.vphoto();\n\tif (content && !data.vttl_seconds()) {\n\t\tconst auto photo = parent()->history()->owner().processPhoto(\n\t\t\t*content);\n\t\tif (photo == _photo) {\n\t\t\treturn true;\n\t\t} else {\n\t\t\tphoto->collectLocalData(_photo);\n\t\t}\n\t} else {\n\t\tLOG((\"API Error: \"\n\t\t\t\"Got MTPMessageMediaPhoto without photo \"\n\t\t\t\"or with ttl_seconds in updateInlineResultMedia()\"));\n\t}\n\treturn false;\n}\n\nbool MediaPhoto::updateSentMedia(const MTPMessageMedia &media) {\n\tif (media.type() != mtpc_messageMediaPhoto) {\n\t\treturn false;\n\t}\n\tconst auto &mediaPhoto = media.c_messageMediaPhoto();\n\tconst auto content = mediaPhoto.vphoto();\n\tif (!content || mediaPhoto.vttl_seconds()) {\n\t\tLOG((\"Api Error: \"\n\t\t\t\"Got MTPMessageMediaPhoto without photo \"\n\t\t\t\"or with ttl_seconds in updateSentMedia()\"));\n\t\treturn false;\n\t}\n\tparent()->history()->owner().photoConvert(_photo, *content);\n\treturn true;\n}\n\nstd::unique_ptr<HistoryView::Media> MediaPhoto::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\tif (_chat) {\n\t\tif (realParent->isUserpicSuggestion()) {\n\t\t\treturn std::make_unique<HistoryView::ServiceBox>(\n\t\t\t\tmessage,\n\t\t\t\tstd::make_unique<HistoryView::UserpicSuggestion>(\n\t\t\t\t\tmessage,\n\t\t\t\t\t_chat,\n\t\t\t\t\t_photo,\n\t\t\t\t\tst::msgServicePhotoWidth));\n\t\t}\n\t\treturn std::make_unique<HistoryView::Photo>(\n\t\t\tmessage,\n\t\t\t_chat,\n\t\t\t_photo,\n\t\t\tst::msgServicePhotoWidth);\n\t}\n\treturn std::make_unique<HistoryView::Photo>(\n\t\tmessage,\n\t\trealParent,\n\t\t_photo,\n\t\t_spoiler);\n}\n\nMediaFile::MediaFile(\n\tnot_null<HistoryItem*> parent,\n\tnot_null<DocumentData*> document,\n\tArgs &&args)\n: Media(parent)\n, _document(document)\n, _videoCover(args.videoCover)\n, _ttlSeconds(args.ttlSeconds)\n, _emoji(document->sticker() ? document->sticker()->alt : QString())\n, _videoTimestamp(args.videoTimestamp)\n, _skipPremiumEffect(args.skipPremiumEffect)\n, _hasQualitiesList(args.hasQualitiesList)\n, _spoiler(args.spoiler) {\n\tparent->history()->owner().registerDocumentItem(_document, parent);\n\n\tif (!_emoji.isEmpty()) {\n\t\tif (const auto emoji = Ui::Emoji::Find(_emoji)) {\n\t\t\t_emoji = emoji->text();\n\t\t}\n\t}\n\n\tif (_spoiler) {\n\t\tUi::PreloadImageSpoiler();\n\t}\n}\n\nMediaFile::~MediaFile() {\n\tif (uploading() && !Core::Quitting()) {\n\t\tparent()->history()->session().uploader().cancel(parent()->fullId());\n\t}\n\tparent()->history()->owner().unregisterDocumentItem(\n\t\t_document,\n\t\tparent());\n}\n\nstd::unique_ptr<Media> MediaFile::clone(not_null<HistoryItem*> parent) {\n\treturn std::make_unique<MediaFile>(parent, _document, MediaFile::Args{\n\t\t.ttlSeconds = _ttlSeconds,\n\t\t.videoCover = _videoCover,\n\t\t.videoTimestamp = _videoTimestamp,\n\t\t.hasQualitiesList = _hasQualitiesList,\n\t\t.skipPremiumEffect = !_document->session().premium(),\n\t\t.spoiler = _spoiler,\n\t});\n}\n\nDocumentData *MediaFile::document() const {\n\treturn _document;\n}\n\nPhotoData *MediaFile::videoCover() const {\n\treturn _videoCover;\n}\n\nTimeId MediaFile::videoTimestamp() const {\n\treturn _videoTimestamp;\n}\n\nbool MediaFile::hasQualitiesList() const {\n\treturn _hasQualitiesList;\n}\n\nbool MediaFile::uploading() const {\n\treturn _document->uploading();\n}\n\nStorage::SharedMediaTypesMask MediaFile::sharedMediaTypes() const {\n\tusing Type = Storage::SharedMediaType;\n\tif (_document->sticker() || ttlSeconds()) {\n\t\treturn {};\n\t} else if (_document->isVideoMessage()) {\n\t\treturn Storage::SharedMediaTypesMask{}\n\t\t\t.added(Type::RoundFile)\n\t\t\t.added(Type::RoundVoiceFile);\n\t} else if (_document->isGifv()) {\n\t\treturn Type::GIF;\n\t} else if (_document->isVideoFile()) {\n\t\treturn Storage::SharedMediaTypesMask{}\n\t\t\t.added(Type::Video)\n\t\t\t.added(Type::PhotoVideo);\n\t} else if (_document->isVoiceMessage()) {\n\t\treturn Storage::SharedMediaTypesMask{}\n\t\t\t.added(Type::VoiceFile)\n\t\t\t.added(Type::RoundVoiceFile);\n\t} else if (_document->isSharedMediaMusic()) {\n\t\treturn Type::MusicFile;\n\t}\n\treturn Type::File;\n}\n\nbool MediaFile::canBeGrouped() const {\n\tif (_document->sticker() || _document->isAnimation()) {\n\t\treturn false;\n\t} else if (_document->isVideoFile()) {\n\t\treturn true;\n\t} else if (_document->isTheme() && _document->hasThumbnail()) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nbool MediaFile::hasReplyPreview() const {\n\treturn _document->hasThumbnail();\n}\n\nImage *MediaFile::replyPreview() const {\n\treturn _document->getReplyPreview(parent());\n}\n\nbool MediaFile::replyPreviewLoaded() const {\n\treturn _document->replyPreviewLoaded(_spoiler);\n}\n\nItemPreview MediaFile::toPreview(ToPreviewOptions options) const {\n\tconst auto item = parent();\n\tif (!options.ignoreGroup && item->groupId()) {\n\t\tif (const auto group = item->history()->owner().groups().find(item)\n\t\t\t; group && group->items.size() > 1) {\n\t\t\treturn toGroupPreview(group->items, options);\n\t\t}\n\t}\n\tif (_document->sticker()) {\n\t\treturn Media::toPreview(options);\n\t}\n\tauto images = std::vector<ItemPreviewImage>();\n\tauto context = std::any();\n\tconst auto existing = options.existing;\n\tconst auto spoilered = _spoiler\n\t\t|| (_document->isVideoMessage() && ttlSeconds());\n\tconst auto radius = _document->isVideoMessage()\n\t\t? ImageRoundRadius::Ellipse\n\t\t: ImageRoundRadius::Small;\n\tif (_videoCover) {\n\t\tif (auto found = FindCachedPreview(\n\t\t\t\texisting,\n\t\t\t\tnot_null{ _videoCover },\n\t\t\t\tradius,\n\t\t\t\tspoilered)) {\n\t\t\timages.push_back(std::move(found));\n\t\t} else {\n\t\t\tconst auto media = _videoCover->createMediaView();\n\t\t\tif (auto prepared = PreparePhotoPreview(\n\t\t\t\t\tparent(),\n\t\t\t\t\tmedia,\n\t\t\t\t\tradius,\n\t\t\t\t\t_spoiler)\n\t\t\t\t; prepared || !prepared.cacheKey) {\n\t\t\t\timages.push_back(std::move(prepared));\n\t\t\t\tif (!prepared.cacheKey) {\n\t\t\t\t\tcontext = media;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if (auto found = FindCachedPreview(\n\t\t\texisting,\n\t\t\t_document,\n\t\t\tradius,\n\t\t\tspoilered)) {\n\t\timages.push_back(std::move(found));\n\t} else if (TryFilePreview(_document)) {\n\t\tconst auto media = _document->createMediaView();\n\t\tif (auto prepared = PrepareFilePreview(\n\t\t\t\tparent(),\n\t\t\t\tmedia,\n\t\t\t\tradius,\n\t\t\t\tspoilered)\n\t\t\t; prepared || !prepared.cacheKey) {\n\t\t\timages.push_back(std::move(prepared));\n\t\t\tif (!prepared.cacheKey) {\n\t\t\t\tcontext = media;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto type = [&] {\n\t\tusing namespace Ui::Text;\n\t\tif (_document->isVideoMessage()) {\n\t\t\treturn (item->media() && item->media()->ttlSeconds())\n\t\t\t\t? tr::lng_in_dlg_video_message_ttl(tr::now)\n\t\t\t\t: tr::lng_in_dlg_video_message(tr::now);\n\t\t} else if (_document->isAnimation()) {\n\t\t\treturn u\"GIF\"_q;\n\t\t} else if (_document->isVideoFile()) {\n\t\t\treturn tr::lng_in_dlg_video(tr::now);\n\t\t} else if (_document->isVoiceMessage()) {\n\t\t\treturn (item->media() && item->media()->ttlSeconds())\n\t\t\t\t? tr::lng_in_dlg_voice_message_ttl(tr::now)\n\t\t\t\t: item->isUnreadMedia()\n\t\t\t\t? tr::lng_in_dlg_audio_unread(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\tQChar(0x25CF))\n\t\t\t\t: tr::lng_in_dlg_audio(tr::now);\n\t\t} else if (const auto name = FormatSongNameFor(_document).string();\n\t\t\t\t!name.isEmpty()) {\n\t\t\treturn name;\n\t\t} else if (_document->isAudioFile()) {\n\t\t\treturn tr::lng_in_dlg_audio_file(tr::now);\n\t\t}\n\t\treturn tr::lng_in_dlg_file(tr::now);\n\t}();\n\tconst auto caption = (options.hideCaption || options.ignoreMessageText)\n\t\t? TextWithEntities()\n\t\t: Dialogs::Ui::DialogsPreviewText(options.translated\n\t\t\t? parent()->translatedText()\n\t\t\t: parent()->originalText());\n\tconst auto hasMiniImages = !images.empty();\n\treturn {\n\t\t.text = WithCaptionNotificationText(type, caption, hasMiniImages),\n\t\t.images = std::move(images),\n\t\t.loadingContext = std::move(context),\n\t};\n}\n\nTextWithEntities MediaFile::notificationText() const {\n\tif (_document->sticker()) {\n\t\tconst auto text = _emoji.isEmpty()\n\t\t\t? tr::lng_in_dlg_sticker(tr::now)\n\t\t\t: tr::lng_in_dlg_sticker_emoji(tr::now, lt_emoji, _emoji);\n\t\treturn Ui::Text::Colorized(text);\n\t}\n\tconst auto type = [&] {\n\t\tif (_document->isVideoMessage()) {\n\t\t\tconst auto media = parent()->media();\n\t\t\treturn (media && media->ttlSeconds())\n\t\t\t\t? tr::lng_in_dlg_video_message_ttl(tr::now)\n\t\t\t\t: tr::lng_in_dlg_video_message(tr::now);\n\t\t} else if (_document->isAnimation()) {\n\t\t\treturn u\"GIF\"_q;\n\t\t} else if (_document->isVideoFile()) {\n\t\t\treturn tr::lng_in_dlg_video(tr::now);\n\t\t} else if (_document->isVoiceMessage()) {\n\t\t\tconst auto media = parent()->media();\n\t\t\treturn (media && media->ttlSeconds())\n\t\t\t\t? tr::lng_in_dlg_voice_message_ttl(tr::now)\n\t\t\t\t: tr::lng_in_dlg_audio(tr::now);\n\t\t} else if (!_document->filename().isEmpty()) {\n\t\t\treturn _document->filename();\n\t\t} else if (_document->isAudioFile()) {\n\t\t\treturn tr::lng_in_dlg_audio_file(tr::now);\n\t\t}\n\t\treturn tr::lng_in_dlg_file(tr::now);\n\t}();\n\treturn WithCaptionNotificationText(type, parent()->originalText());\n}\n\nQString MediaFile::pinnedTextSubstring() const {\n\tif (_document->sticker()) {\n\t\tif (!_emoji.isEmpty()) {\n\t\t\treturn tr::lng_action_pinned_media_emoji_sticker(\n\t\t\t\ttr::now,\n\t\t\t\tlt_emoji,\n\t\t\t\t_emoji);\n\t\t}\n\t\treturn tr::lng_action_pinned_media_sticker(tr::now);\n\t} else if (_document->isAnimation()) {\n\t\tif (_document->isVideoMessage()) {\n\t\t\treturn tr::lng_action_pinned_media_video_message(tr::now);\n\t\t}\n\t\treturn tr::lng_action_pinned_media_gif(tr::now);\n\t} else if (_document->isVideoFile()) {\n\t\treturn tr::lng_action_pinned_media_video(tr::now);\n\t} else if (_document->isVoiceMessage()) {\n\t\treturn tr::lng_action_pinned_media_voice(tr::now);\n\t} else if (_document->isSong()) {\n\t\treturn tr::lng_action_pinned_media_audio(tr::now);\n\t}\n\treturn tr::lng_action_pinned_media_file(tr::now);\n}\n\nTextForMimeData MediaFile::clipboardText() const {\n\tauto caption = parent()->clipboardText();\n\n\tif (_document->isVoiceMessage() || _document->isVideoMessage()) {\n\t\tconst auto &entry = _document->session().api().transcribes().entry(\n\t\t\tparent());\n\t\tif (!entry.requestId\n\t\t\t&& entry.shown\n\t\t\t&& !entry.toolong\n\t\t\t&& !entry.failed\n\t\t\t&& (entry.pending || !entry.result.isEmpty())) {\n\t\t\tconst auto hasCaption = !caption.rich.text.isEmpty();\n\t\t\tconst auto text = (hasCaption ? \"{{\\n\" : \"\")\n\t\t\t\t+ entry.result\n\t\t\t\t+ (entry.result.isEmpty() ? \"\" : \" \")\n\t\t\t\t+ (entry.pending ? \"[...]\" : \"\")\n\t\t\t\t+ (hasCaption ? \"\\n}}\\n\" : \"\");\n\t\t\tcaption = TextForMimeData{ text, { text } }.append(\n\t\t\t\tstd::move(caption));\n\t\t}\n\t}\n\n\treturn caption;\n}\n\nbool MediaFile::allowsEditCaption() const {\n\treturn !_document->isVideoMessage() && !_document->sticker();\n}\n\nbool MediaFile::allowsEditMedia() const {\n\treturn !_document->isVideoMessage()\n\t\t&& !_document->sticker()\n\t\t&& !_document->isVoiceMessage();\n}\n\nbool MediaFile::forwardedBecomesUnread() const {\n\treturn _document->isVoiceMessage()\n\t\t//|| _document->isVideoFile()\n\t\t|| _document->isVideoMessage();\n}\n\nbool MediaFile::dropForwardedInfo() const {\n\treturn _document->isSong();\n}\n\nbool MediaFile::hasSpoiler() const {\n\treturn _spoiler;\n}\n\ncrl::time MediaFile::ttlSeconds() const {\n\treturn _ttlSeconds;\n}\n\nbool MediaFile::allowsForward() const {\n\treturn !ttlSeconds();\n}\n\nbool MediaFile::updateInlineResultMedia(const MTPMessageMedia &media) {\n\tif (media.type() != mtpc_messageMediaDocument) {\n\t\treturn false;\n\t}\n\tconst auto &data = media.c_messageMediaDocument();\n\tconst auto content = data.vdocument();\n\tif (content && !data.vttl_seconds()) {\n\t\tconst auto document = parent()->history()->owner().processDocument(\n\t\t\t*content);\n\t\tif (document == _document) {\n\t\t\treturn false;\n\t\t} else {\n\t\t\tdocument->collectLocalData(_document);\n\t\t}\n\t} else {\n\t\tLOG((\"API Error: \"\n\t\t\t\"Got MTPMessageMediaDocument without document \"\n\t\t\t\"or with ttl_seconds in updateInlineResultMedia()\"));\n\t}\n\treturn false;\n}\n\nbool MediaFile::updateSentMedia(const MTPMessageMedia &media) {\n\tif (media.type() != mtpc_messageMediaDocument) {\n\t\treturn false;\n\t}\n\tconst auto &data = media.c_messageMediaDocument();\n\tconst auto content = data.vdocument();\n\tif (!content || data.vttl_seconds()) {\n\t\tLOG((\"Api Error: \"\n\t\t\t\"Got MTPMessageMediaDocument without document \"\n\t\t\t\"or with ttl_seconds in updateSentMedia()\"));\n\t\treturn false;\n\t}\n\tconst auto owner = &parent()->history()->owner();\n\towner->documentConvert(_document, *content);\n\tif (const auto cover = _videoCover ? data.vvideo_cover() : nullptr) {\n\t\towner->photoConvert(_videoCover, *cover);\n\t}\n\treturn true;\n}\n\nstd::unique_ptr<HistoryView::Media> MediaFile::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\tif (_document->sticker()) {\n\t\treturn std::make_unique<HistoryView::UnwrappedMedia>(\n\t\t\tmessage,\n\t\t\tstd::make_unique<HistoryView::Sticker>(\n\t\t\t\tmessage,\n\t\t\t\t_document,\n\t\t\t\t_skipPremiumEffect,\n\t\t\t\treplacing));\n\t} else if (_document->isVideoMessage()) {\n\t\tconst auto &entry = _document->session().api().transcribes().entry(\n\t\t\tparent());\n\t\tif (!entry.requestId\n\t\t\t&& entry.shown\n\t\t\t&& entry.roundview\n\t\t\t&& !entry.pending) {\n\t\t\treturn std::make_unique<HistoryView::Document>(\n\t\t\t\tmessage,\n\t\t\t\trealParent,\n\t\t\t\t_document);\n\t\t} else {\n\t\t\treturn std::make_unique<HistoryView::Gif>(\n\t\t\t\tmessage,\n\t\t\t\trealParent,\n\t\t\t\t_document,\n\t\t\t\t_spoiler);\n\t\t}\n\t} else if (_document->isAnimation() || _document->isVideoFile()) {\n\t\treturn std::make_unique<HistoryView::Gif>(\n\t\t\tmessage,\n\t\t\trealParent,\n\t\t\t_document,\n\t\t\t_spoiler);\n\t} else if (_document->isTheme() && _document->hasThumbnail()) {\n\t\treturn std::make_unique<HistoryView::ThemeDocument>(\n\t\t\tmessage,\n\t\t\t_document);\n\t}\n\treturn std::make_unique<HistoryView::Document>(\n\t\tmessage,\n\t\trealParent,\n\t\t_document);\n}\n\nSharedContact::VcardItems SharedContact::ParseVcard(const QString &data) {\n\tconst auto decode = [&](const QByteArray &input) -> QString {\n\t\tauto output = QByteArray();\n\t\tfor (auto i = 0; i < input.size(); ++i) {\n\t\t\tif ((input.at(i) == '=') && ((i + 2) < input.size())) {\n\t\t\t\tconst auto value = input.mid((++i)++, 2);\n\t\t\t\tauto converted = false;\n\t\t\t\tconst auto character = char(value.toUInt(&converted, 16));\n\t\t\t\tif (converted) {\n\t\t\t\t\toutput.append(character);\n\t\t\t\t} else {\n\t\t\t\t\toutput.append('=');\n\t\t\t\t\toutput.append(value);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\toutput.append(input.at(i));\n\t\t\t}\n\t\t}\n\n\t\treturn QString::fromUtf8(output);\n\t};\n\n\tusing Type = SharedContact::VcardItemType;\n\tauto items = SharedContact::VcardItems();\n\tfor (const auto &item : data.split('\\n')) {\n\t\tconst auto parts = item.split(':');\n\t\tif (parts.size() == 2) {\n\t\t\tconst auto &type = parts.front();\n\t\t\tconst auto attributes = type.split(';', Qt::SkipEmptyParts);\n\n\t\t\tconst auto c = Qt::CaseInsensitive;\n\t\t\tauto isQuotedPrintable = false;\n\t\t\tfor (const auto &attribute : attributes) {\n\t\t\t\tconst auto parts = attribute.split('=', Qt::SkipEmptyParts);\n\t\t\t\tif (parts.size() == 2) {\n\t\t\t\t\tif (parts.front().startsWith(\"ENCODING\", c)) {\n\t\t\t\t\t\tisQuotedPrintable = parts[1].startsWith(\n\t\t\t\t\t\t\t\"QUOTED-PRINTABLE\",\n\t\t\t\t\t\t\tc);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst auto &value = isQuotedPrintable\n\t\t\t\t? decode(parts[1].toUtf8())\n\t\t\t\t: parts[1];\n\n\t\t\tif (type.startsWith(\"TEL\")) {\n\t\t\t\tconst auto telType = type.contains(\"PREF\")\n\t\t\t\t\t? Type::PhoneMain\n\t\t\t\t\t: type.contains(\"HOME\")\n\t\t\t\t\t? Type::PhoneHome\n\t\t\t\t\t: type.contains(\"WORK\")\n\t\t\t\t\t? Type::PhoneWork\n\t\t\t\t\t: (type.contains(\"CELL\")\n\t\t\t\t\t\t|| type.contains(\"MOBILE\"))\n\t\t\t\t\t? Type::PhoneMobile\n\t\t\t\t\t: type.contains(\"OTHER\")\n\t\t\t\t\t? Type::PhoneOther\n\t\t\t\t\t: Type::Phone;\n\t\t\t\titems[telType] = value;\n\t\t\t} else if (type.startsWith(\"EMAIL\")) {\n\t\t\t\titems[Type::Email] = value;\n\t\t\t} else if (type.startsWith(\"URL\")) {\n\t\t\t\titems[Type::Url] = value;\n\t\t\t} else if (type.startsWith(\"NOTE\")) {\n\t\t\t\titems[Type::Note] = value;\n\t\t\t} else if (type.startsWith(\"ORG\")) {\n\t\t\t\titems[Type::Organization] = base::duplicate(value)\n\t\t\t\t\t.replace(';', ' ')\n\t\t\t\t\t.trimmed();\n\t\t\t} else if (type.startsWith(\"ADR\")) {\n\t\t\t\titems[Type::Address] = value;\n\t\t\t} else if (type.startsWith(\"BDAY\")) {\n\t\t\t\titems[Type::Birthday] = value;\n\t\t\t} else if (type.startsWith(\"N\")) {\n\t\t\t\titems[Type::Name] = base::duplicate(value)\n\t\t\t\t\t.replace(';', ' ')\n\t\t\t\t\t.trimmed();\n\t\t\t}\n\t\t}\n\t}\n\treturn items;\n}\n\nMediaContact::MediaContact(\n\tnot_null<HistoryItem*> parent,\n\tUserId userId,\n\tconst QString &firstName,\n\tconst QString &lastName,\n\tconst QString &phoneNumber,\n\tconst SharedContact::VcardItems &vcardItems)\n: Media(parent)\n, _contact(SharedContact{\n\t.userId = userId,\n\t.firstName = firstName,\n\t.lastName = lastName,\n\t.phoneNumber = phoneNumber,\n\t.vcardItems = vcardItems,\n}) {\n\tparent->history()->owner().registerContactItem(userId, parent);\n}\n\nMediaContact::~MediaContact() {\n\tparent()->history()->owner().unregisterContactItem(\n\t\t_contact.userId,\n\t\tparent());\n}\n\nstd::unique_ptr<Media> MediaContact::clone(not_null<HistoryItem*> parent) {\n\treturn std::make_unique<MediaContact>(\n\t\tparent,\n\t\t_contact.userId,\n\t\t_contact.firstName,\n\t\t_contact.lastName,\n\t\t_contact.phoneNumber,\n\t\t_contact.vcardItems);\n}\n\nconst SharedContact *MediaContact::sharedContact() const {\n\treturn &_contact;\n}\n\nTextWithEntities MediaContact::notificationText() const {\n\treturn Ui::Text::Colorized(tr::lng_in_dlg_contact(tr::now));\n}\n\nQString MediaContact::pinnedTextSubstring() const {\n\treturn tr::lng_action_pinned_media_contact(tr::now);\n}\n\nTextForMimeData MediaContact::clipboardText() const {\n\tconst auto text = u\"[ \"_q\n\t\t+ tr::lng_in_dlg_contact(tr::now)\n\t\t+ u\" ]\\n\"_q\n\t\t+ tr::lng_full_name(\n\t\t\ttr::now,\n\t\t\tlt_first_name,\n\t\t\t_contact.firstName,\n\t\t\tlt_last_name,\n\t\t\t_contact.lastName).trimmed()\n\t\t+ '\\n'\n\t\t+ _contact.phoneNumber;\n\treturn TextForMimeData::Simple(text);\n}\n\nbool MediaContact::updateInlineResultMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nbool MediaContact::updateSentMedia(const MTPMessageMedia &media) {\n\tif (media.type() != mtpc_messageMediaContact) {\n\t\treturn false;\n\t}\n\tconst auto userId = UserId(media.c_messageMediaContact().vuser_id());\n\tif (_contact.userId != userId) {\n\t\tparent()->history()->owner().unregisterContactItem(\n\t\t\t_contact.userId,\n\t\t\tparent());\n\t\t_contact.userId = userId;\n\t\tparent()->history()->owner().registerContactItem(\n\t\t\t_contact.userId,\n\t\t\tparent());\n\t}\n\treturn true;\n}\n\nstd::unique_ptr<HistoryView::Media> MediaContact::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\treturn std::make_unique<HistoryView::Contact>(message, _contact);\n}\n\nMediaLocation::MediaLocation(\n\tnot_null<HistoryItem*> parent,\n\tconst LocationPoint &point,\n\tTimeId livePeriod)\n: MediaLocation({}, parent, point, livePeriod, QString(), QString()) {\n}\n\nMediaLocation::MediaLocation(\n\tnot_null<HistoryItem*> parent,\n\tconst LocationPoint &point,\n\tconst QString &title,\n\tconst QString &description)\n: MediaLocation({}, parent, point, TimeId(), title, description) {\n}\n\nMediaLocation::MediaLocation(\n\tPrivateTag,\n\tnot_null<HistoryItem*> parent,\n\tconst LocationPoint &point,\n\tTimeId livePeriod,\n\tconst QString &title,\n\tconst QString &description)\n: Media(parent)\n, _point(point)\n, _location(parent->history()->owner().location(point))\n, _livePeriod(livePeriod)\n, _title(title)\n, _description(description) {\n}\n\nstd::unique_ptr<Media> MediaLocation::clone(not_null<HistoryItem*> parent) {\n\treturn std::make_unique<MediaLocation>(\n\t\tPrivateTag(),\n\t\tparent,\n\t\t_point,\n\t\t_livePeriod,\n\t\t_title,\n\t\t_description);\n}\n\nCloudImage *MediaLocation::location() const {\n\treturn _location;\n}\n\nQString MediaLocation::typeString() const {\n\treturn _livePeriod\n\t\t? tr::lng_live_location(tr::now)\n\t\t: tr::lng_maps_point(tr::now);\n}\n\nItemPreview MediaLocation::toPreview(ToPreviewOptions options) const {\n\tconst auto type = typeString();\n\tconst auto hasMiniImages = false;\n\tconst auto text = TextWithEntities{ .text = _title };\n\treturn {\n\t\t.text = WithCaptionNotificationText(type, text, hasMiniImages),\n\t};\n}\n\nTextWithEntities MediaLocation::notificationText() const {\n\treturn WithCaptionNotificationText(typeString(), { .text = _title });\n}\n\nQString MediaLocation::pinnedTextSubstring() const {\n\treturn tr::lng_action_pinned_media_location(tr::now);\n}\n\nTextForMimeData MediaLocation::clipboardText() const {\n\tauto result = TextForMimeData::Simple(\n\t\tu\"[ \"_q + typeString() + u\" ]\\n\"_q);\n\tauto titleResult = TextUtilities::ParseEntities(\n\t\t_title,\n\t\tUi::WebpageTextTitleOptions().flags);\n\tauto descriptionResult = TextUtilities::ParseEntities(\n\t\t_description,\n\t\tTextParseLinks | TextParseMultiline);\n\tif (!titleResult.empty()) {\n\t\tresult.append(std::move(titleResult));\n\t}\n\tif (!descriptionResult.text.isEmpty()) {\n\t\tresult.append(std::move(descriptionResult));\n\t}\n\tresult.append(LocationClickHandler(_point).url());\n\treturn result;\n}\n\nbool MediaLocation::updateInlineResultMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nbool MediaLocation::updateSentMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nstd::unique_ptr<HistoryView::Media> MediaLocation::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\treturn _livePeriod\n\t\t? std::make_unique<HistoryView::Location>(\n\t\t\tmessage,\n\t\t\t_location,\n\t\t\t_point,\n\t\t\treplacing,\n\t\t\t_livePeriod)\n\t\t: std::make_unique<HistoryView::Location>(\n\t\t\tmessage,\n\t\t\t_location,\n\t\t\t_point,\n\t\t\t_title,\n\t\t\t_description);\n}\n\nMediaCall::MediaCall(not_null<HistoryItem*> parent, const Call &call)\n: Media(parent)\n, _call(call) {\n\tconst auto peer = parent->history()->peer;\n\tpeer->owner().registerCallItem(parent);\n\tif (const auto user = _call.conferenceId ? peer->asUser() : nullptr) {\n\t\tCore::App().calls().registerConferenceInvite(\n\t\t\t_call.conferenceId,\n\t\t\tuser,\n\t\t\tparent->id,\n\t\t\t!parent->out());\n\t}\n}\n\nMediaCall::~MediaCall() {\n\tconst auto parent = this->parent();\n\tconst auto peer = parent->history()->peer;\n\tpeer->owner().unregisterCallItem(parent);\n\tif (const auto user = _call.conferenceId ? peer->asUser() : nullptr) {\n\t\tCore::App().calls().unregisterConferenceInvite(\n\t\t\t_call.conferenceId,\n\t\t\tuser,\n\t\t\tparent->id,\n\t\t\t!parent->out());\n\t}\n}\n\nstd::unique_ptr<Media> MediaCall::clone(not_null<HistoryItem*> parent) {\n\tUnexpected(\"Clone of call media.\");\n}\n\nconst Call *MediaCall::call() const {\n\treturn &_call;\n}\n\nTextWithEntities MediaCall::notificationText() const {\n\tconst auto conference = (_call.conferenceId != 0);\n\tauto result = Text(parent(), _call.state, conference, _call.video);\n\tif (_call.duration > 0) {\n\t\tresult = tr::lng_call_type_and_duration(\n\t\t\ttr::now,\n\t\t\tlt_type,\n\t\t\tresult,\n\t\t\tlt_duration,\n\t\t\tUi::FormatDurationWords(_call.duration));\n\t}\n\treturn { .text = result };\n}\n\nQString MediaCall::pinnedTextSubstring() const {\n\treturn QString();\n}\n\nTextForMimeData MediaCall::clipboardText() const {\n\treturn { .rich = notificationText() };\n}\n\nbool MediaCall::allowsForward() const {\n\treturn false;\n}\n\nbool MediaCall::updateInlineResultMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nbool MediaCall::updateSentMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nstd::unique_ptr<HistoryView::Media> MediaCall::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\treturn std::make_unique<HistoryView::Call>(message, &_call);\n}\n\nQString MediaCall::Text(\n\t\tnot_null<HistoryItem*> item,\n\t\tCallState state,\n\t\tbool conference,\n\t\tbool video) {\n\tif (state == CallState::Invitation) {\n\t\treturn tr::lng_call_invitation(tr::now);\n\t} else if (state == CallState::Active) {\n\t\treturn tr::lng_call_ongoing(tr::now);\n\t} else if (item->out()) {\n\t\treturn ((state == CallState::Missed)\n\t\t\t? (conference\n\t\t\t\t? tr::lng_call_group_declined\n\t\t\t\t: video\n\t\t\t\t? tr::lng_call_video_cancelled\n\t\t\t\t: tr::lng_call_cancelled)\n\t\t\t: (conference\n\t\t\t\t? tr::lng_call_group_outgoing\n\t\t\t\t: video\n\t\t\t\t? tr::lng_call_video_outgoing\n\t\t\t\t: tr::lng_call_outgoing))(tr::now);\n\t} else if (state == CallState::Missed) {\n\t\treturn (conference\n\t\t\t? tr::lng_call_group_missed\n\t\t\t: video\n\t\t\t? tr::lng_call_video_missed\n\t\t\t: tr::lng_call_missed)(tr::now);\n\t} else if (state == CallState::Busy) {\n\t\treturn (video\n\t\t\t? tr::lng_call_video_declined\n\t\t\t: tr::lng_call_declined)(tr::now);\n\t}\n\treturn (conference\n\t\t? tr::lng_call_group_incoming\n\t\t: video\n\t\t? tr::lng_call_video_incoming\n\t\t: tr::lng_call_incoming)(tr::now);\n}\n\nMediaWebPage::MediaWebPage(\n\tnot_null<HistoryItem*> parent,\n\tnot_null<WebPageData*> page,\n\tMediaWebPageFlags flags)\n: Media(parent)\n, _page(page)\n, _flags(flags) {\n\tparent->history()->owner().registerWebPageItem(_page, parent);\n}\n\nMediaWebPage::~MediaWebPage() {\n\tparent()->history()->owner().unregisterWebPageItem(_page, parent());\n}\n\nstd::unique_ptr<Media> MediaWebPage::clone(not_null<HistoryItem*> parent) {\n\treturn std::make_unique<MediaWebPage>(parent, _page, _flags);\n}\n\nDocumentData *MediaWebPage::document() const {\n\treturn _page->document;\n}\n\nPhotoData *MediaWebPage::photo() const {\n\treturn _page->photo;\n}\n\nWebPageData *MediaWebPage::webpage() const {\n\treturn _page;\n}\n\nMediaWebPageFlags MediaWebPage::webpageFlags() const {\n\treturn _flags;\n}\n\nStorage::SharedMediaTypesMask MediaWebPage::sharedMediaTypes() const {\n\treturn Storage::SharedMediaType::Link;\n}\n\nbool MediaWebPage::hasReplyPreview() const {\n\tif (const auto document = MediaWebPage::document()) {\n\t\treturn document->hasThumbnail()\n\t\t\t&& !document->isPatternWallPaper();\n\t} else if (const auto photo = MediaWebPage::photo()) {\n\t\treturn !photo->isNull();\n\t}\n\treturn false;\n}\n\nImage *MediaWebPage::replyPreview() const {\n\tif (const auto document = MediaWebPage::document()) {\n\t\treturn document->getReplyPreview(parent());\n\t} else if (const auto photo = MediaWebPage::photo()) {\n\t\treturn photo->getReplyPreview(parent());\n\t}\n\treturn nullptr;\n}\n\nbool MediaWebPage::replyPreviewLoaded() const {\n\tconst auto spoiler = false;\n\tif (const auto document = MediaWebPage::document()) {\n\t\treturn document->replyPreviewLoaded(spoiler);\n\t} else if (const auto photo = MediaWebPage::photo()) {\n\t\treturn photo->replyPreviewLoaded(spoiler);\n\t}\n\treturn true;\n}\n\nItemPreview MediaWebPage::toPreview(ToPreviewOptions options) const {\n\tconst auto caption = [&] {\n\t\tconst auto text = options.ignoreMessageText\n\t\t\t? TextWithEntities()\n\t\t\t: options.translated\n\t\t\t? parent()->translatedText()\n\t\t\t: parent()->originalText();\n\t\treturn text.empty() ? Ui::Text::Colorized(_page->url) : text;\n\t}();\n\tconst auto pageTypeWithPreview = _page->type == WebPageType::Photo\n\t\t|| _page->type == WebPageType::Video\n\t\t|| _page->type == WebPageType::Document;\n\tif (pageTypeWithPreview || !_page->collage.items.empty()) {\n\t\tconst auto radius = ImageRoundRadius::Small;\n\t\tif (auto found = FindCachedPreview(\n\t\t\t\toptions.existing,\n\t\t\t\t_page,\n\t\t\t\tradius,\n\t\t\t\tfalse)) {\n\t\t\treturn { .text = caption, .images = { std::move(found) } };\n\t\t}\n\t\tauto context = std::any();\n\t\tauto images = std::vector<ItemPreviewImage>();\n\t\tauto prepared = ItemPreviewImage();\n\t\tif (const auto photo = MediaWebPage::photo()) {\n\t\t\tconst auto media = photo->createMediaView();\n\t\t\tprepared = PreparePhotoPreview(parent(), media, radius, false);\n\t\t\tif (prepared || !prepared.cacheKey) {\n\t\t\t\timages.push_back(std::move(prepared));\n\t\t\t\tif (!prepared.cacheKey) {\n\t\t\t\t\tcontext = media;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto document = MediaWebPage::document();\n\t\t\tif (document\n\t\t\t\t&& document->hasThumbnail()\n\t\t\t\t&& (document->isGifv() || document->isVideoFile())) {\n\t\t\t\tconst auto media = document->createMediaView();\n\t\t\t\tprepared = PrepareFilePreview(parent(), media, radius, false);\n\t\t\t\tif (prepared || !prepared.cacheKey) {\n\t\t\t\t\timages.push_back(std::move(prepared));\n\t\t\t\t\tif (!prepared.cacheKey) {\n\t\t\t\t\t\tcontext = media;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\t.text = caption,\n\t\t\t.images = std::move(images),\n\t\t\t.loadingContext = std::move(context),\n\t\t};\n\t} else {\n\t\treturn { .text = caption };\n\t}\n}\n\nTextWithEntities MediaWebPage::notificationText() const {\n\treturn parent()->originalText();\n}\n\nQString MediaWebPage::pinnedTextSubstring() const {\n\treturn QString();\n}\n\nTextForMimeData MediaWebPage::clipboardText() const {\n\treturn TextForMimeData();\n}\n\nbool MediaWebPage::allowsEdit() const {\n\treturn true;\n}\n\nbool MediaWebPage::updateInlineResultMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nbool MediaWebPage::updateSentMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nstd::unique_ptr<HistoryView::Media> MediaWebPage::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\tif (realParent->hideLinks()) {\n\t\trealParent->setHasHiddenLinks(true);\n\t\treturn nullptr;\n\t}\n\treturn std::make_unique<HistoryView::WebPage>(message, _page, _flags);\n}\n\nMediaGame::MediaGame(\n\tnot_null<HistoryItem*> parent,\n\tnot_null<GameData*> game)\n: Media(parent)\n, _game(game) {\n}\n\nstd::unique_ptr<Media> MediaGame::clone(not_null<HistoryItem*> parent) {\n\treturn std::make_unique<MediaGame>(parent, _game);\n}\n\nbool MediaGame::hasReplyPreview() const {\n\tif (const auto document = _game->document) {\n\t\treturn document->hasThumbnail();\n\t} else if (const auto photo = _game->photo) {\n\t\treturn !photo->isNull();\n\t}\n\treturn false;\n}\n\nImage *MediaGame::replyPreview() const {\n\tif (const auto document = _game->document) {\n\t\treturn document->getReplyPreview(parent());\n\t} else if (const auto photo = _game->photo) {\n\t\treturn photo->getReplyPreview(parent());\n\t}\n\treturn nullptr;\n}\n\nbool MediaGame::replyPreviewLoaded() const {\n\tconst auto spoiler = false;\n\tif (const auto document = _game->document) {\n\t\treturn document->replyPreviewLoaded(spoiler);\n\t} else if (const auto photo = _game->photo) {\n\t\treturn photo->replyPreviewLoaded(spoiler);\n\t}\n\treturn true;\n}\n\nTextWithEntities MediaGame::notificationText() const {\n\t// Add a game controller emoji before game title.\n\tauto result = QString();\n\tresult.reserve(_game->title.size() + 3);\n\tresult.append(\n\t\tQChar(0xD83C)\n\t).append(\n\t\tQChar(0xDFAE)\n\t).append(\n\t\tQChar(' ')\n\t).append(_game->title);\n\treturn { .text = result };\n}\n\nGameData *MediaGame::game() const {\n\treturn _game;\n}\n\nQString MediaGame::pinnedTextSubstring() const {\n\tconst auto title = _game->title;\n\treturn tr::lng_action_pinned_media_game(tr::now, lt_game, title);\n}\n\nTextForMimeData MediaGame::clipboardText() const {\n\treturn TextForMimeData();\n}\n\nbool MediaGame::dropForwardedInfo() const {\n\treturn true;\n}\n\nbool MediaGame::consumeMessageText(const TextWithEntities &text) {\n\t_consumedText = text;\n\treturn true;\n}\n\nTextWithEntities MediaGame::consumedMessageText() const {\n\treturn _consumedText;\n}\n\nbool MediaGame::updateInlineResultMedia(const MTPMessageMedia &media) {\n\treturn updateSentMedia(media);\n}\n\nbool MediaGame::updateSentMedia(const MTPMessageMedia &media) {\n\tif (media.type() != mtpc_messageMediaGame) {\n\t\treturn false;\n\t}\n\tparent()->history()->owner().gameConvert(\n\t\t_game, media.c_messageMediaGame().vgame());\n\treturn true;\n}\n\nstd::unique_ptr<HistoryView::Media> MediaGame::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\treturn std::make_unique<HistoryView::Game>(\n\t\tmessage,\n\t\t_game,\n\t\t_consumedText);\n}\n\nMediaInvoice::MediaInvoice(\n\tnot_null<HistoryItem*> parent,\n\tconst Invoice &data)\n: Media(parent)\n, _invoice{\n\t.receiptMsgId = data.receiptMsgId,\n\t.amount = data.amount,\n\t.currency = data.currency,\n\t.title = data.title,\n\t.description = data.description,\n\t.photo = data.photo,\n\t.isPaidMedia = data.isPaidMedia,\n\t.isTest = data.isTest,\n} {\n\t_invoice.extendedMedia.reserve(data.extendedMedia.size());\n\tfor (auto &item : data.extendedMedia) {\n\t\t_invoice.extendedMedia.push_back(item->clone(parent));\n\t}\n\tif (HasUnpaidMedia(_invoice)) {\n\t\tUi::PreloadImageSpoiler();\n\t}\n}\n\nstd::unique_ptr<Media> MediaInvoice::clone(not_null<HistoryItem*> parent) {\n\treturn std::make_unique<MediaInvoice>(parent, _invoice);\n}\n\nconst Invoice *MediaInvoice::invoice() const {\n\treturn &_invoice;\n}\n\nbool MediaInvoice::hasReplyPreview() const {\n\tif (const auto photo = _invoice.photo) {\n\t\treturn !photo->isNull();\n\t}\n\treturn false;\n}\n\nImage *MediaInvoice::replyPreview() const {\n\tif (const auto photo = _invoice.photo) {\n\t\treturn photo->getReplyPreview(parent());\n\t}\n\treturn nullptr;\n}\n\nbool MediaInvoice::replyPreviewLoaded() const {\n\tconst auto spoiler = false;\n\tif (const auto photo = _invoice.photo) {\n\t\treturn photo->replyPreviewLoaded(spoiler);\n\t}\n\treturn true;\n}\n\nTextWithEntities MediaInvoice::notificationText() const {\n\tif (_invoice.isPaidMedia && !_invoice.extendedMedia.empty()) {\n\t\treturn WithCaptionNotificationText(\n\t\t\t(IsFirstVideo(_invoice)\n\t\t\t\t? tr::lng_in_dlg_video\n\t\t\t\t: tr::lng_in_dlg_photo)(tr::now),\n\t\t\tparent()->originalText());\n\t}\n\treturn { .text = _invoice.title };\n}\n\nItemPreview MediaInvoice::toPreview(ToPreviewOptions options) const {\n\tif (!_invoice.isPaidMedia || _invoice.extendedMedia.empty()) {\n\t\treturn Media::toPreview(options);\n\t}\n\tauto counts = AlbumCounts();\n\tauto images = std::vector<ItemPreviewImage>();\n\tauto context = std::vector<std::any>();\n\tconst auto existing = options.existing;\n\tconst auto spoiler = HasUnpaidMedia(_invoice);\n\tfor (const auto &media : _invoice.extendedMedia) {\n\t\tconst auto raw = media.get();\n\t\tconst auto photo = raw->photo();\n\t\tconst auto document = raw->document();\n\t\tif (!photo && !document) {\n\t\t\tcontinue;\n\t\t} else if (images.size() < kMaxPreviewImages) {\n\t\t\tconst auto radius = ImageRoundRadius::Small;\n\t\t\tauto found = photo\n\t\t\t\t? FindCachedPreview(\n\t\t\t\t\texisting,\n\t\t\t\t\tnot_null(photo),\n\t\t\t\t\tradius,\n\t\t\t\t\tspoiler)\n\t\t\t\t: FindCachedPreview(\n\t\t\t\t\texisting,\n\t\t\t\t\tnot_null(document),\n\t\t\t\t\tradius,\n\t\t\t\t\tspoiler);\n\t\t\tif (found) {\n\t\t\t\timages.push_back(std::move(found));\n\t\t\t} else if (photo) {\n\t\t\t\tconst auto media = photo->createMediaView();\n\t\t\t\tif (auto prepared = PreparePhotoPreview(\n\t\t\t\t\tparent(),\n\t\t\t\t\tmedia,\n\t\t\t\t\tradius,\n\t\t\t\t\tspoiler)\n\t\t\t\t\t; prepared || !prepared.cacheKey) {\n\t\t\t\t\timages.push_back(std::move(prepared));\n\t\t\t\t\tif (!prepared.cacheKey) {\n\t\t\t\t\t\tcontext.push_back(media);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (TryFilePreview(document)) {\n\t\t\t\tconst auto media = document->createMediaView();\n\t\t\t\tif (auto prepared = PrepareFilePreview(\n\t\t\t\t\t\tparent(),\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tradius,\n\t\t\t\t\t\tspoiler)\n\t\t\t\t\t; prepared || !prepared.cacheKey) {\n\t\t\t\t\timages.push_back(std::move(prepared));\n\t\t\t\t\tif (!prepared.cacheKey) {\n\t\t\t\t\t\tcontext.push_back(media);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (photo && !photo->extendedMediaVideoDuration().has_value()) {\n\t\t\t++counts.photos;\n\t\t} else {\n\t\t\t++counts.videos;\n\t\t}\n\t}\n\tconst auto type = ComputeAlbumCountsString(counts);\n\tconst auto caption = (options.hideCaption || options.ignoreMessageText)\n\t\t? TextWithEntities()\n\t\t: Dialogs::Ui::DialogsPreviewText(options.translated\n\t\t\t? parent()->translatedText()\n\t\t\t: parent()->originalText());\n\tconst auto hasMiniImages = !images.empty();\n\tauto nice = Ui::Text::Colorized(Ui::CreditsEmojiSmall());\n\tnice.append(WithCaptionNotificationText(type, caption, hasMiniImages));\n\treturn {\n\t\t.text = std::move(nice),\n\t\t.images = std::move(images),\n\t\t.loadingContext = std::move(context),\n\t};\n}\n\nQString MediaInvoice::pinnedTextSubstring() const {\n\treturn QString::fromUtf8(\"\\xC2\\xAB\")\n\t\t+ _invoice.title\n\t\t+ QString::fromUtf8(\"\\xC2\\xBB\");\n}\n\nTextForMimeData MediaInvoice::clipboardText() const {\n\treturn TextForMimeData();\n}\n\nbool MediaInvoice::updateInlineResultMedia(const MTPMessageMedia &media) {\n\treturn true;\n}\n\nbool MediaInvoice::updateSentMedia(const MTPMessageMedia &media) {\n\treturn true;\n}\n\nbool MediaInvoice::updateExtendedMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst QVector<MTPMessageExtendedMedia> &media) {\n\tExpects(item == parent());\n\n\treturn UpdateExtendedMedia(_invoice, item, media);\n}\n\nstd::unique_ptr<HistoryView::Media> MediaInvoice::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\tif (_invoice.extendedMedia.size() == 1) {\n\t\treturn _invoice.extendedMedia.front()->createView(\n\t\t\tmessage,\n\t\t\trealParent,\n\t\t\treplacing);\n\t} else if (!_invoice.extendedMedia.empty()) {\n\t\treturn std::make_unique<HistoryView::GroupedMedia>(\n\t\t\tmessage,\n\t\t\t_invoice.extendedMedia);\n\t}\n\treturn std::make_unique<HistoryView::Invoice>(message, &_invoice);\n}\n\nMediaPoll::MediaPoll(\n\tnot_null<HistoryItem*> parent,\n\tnot_null<PollData*> poll)\n: Media(parent)\n, _poll(poll) {\n}\n\nMediaPoll::~MediaPoll() {\n}\n\nstd::unique_ptr<Media> MediaPoll::clone(not_null<HistoryItem*> parent) {\n\tauto result = std::make_unique<MediaPoll>(parent, _poll);\n\tresult->_consumedText = _consumedText;\n\treturn result;\n}\n\nPollData *MediaPoll::poll() const {\n\treturn _poll;\n}\n\nStorage::SharedMediaTypesMask MediaPoll::sharedMediaTypes() const {\n\treturn Storage::SharedMediaTypesMask{}\n\t\t.added(Storage::SharedMediaType::Poll);\n}\n\nItemPreview MediaPoll::toPreview(ToPreviewOptions options) const {\n\tconst auto caption = (options.hideCaption || options.ignoreMessageText)\n\t\t? TextWithEntities()\n\t\t: Dialogs::Ui::DialogsPreviewText(options.translated\n\t\t\t? parent()->translatedText()\n\t\t\t: parent()->originalText());\n\tconst auto type = u\"\\xD83D\\xDCCA \"_q + _poll->question.text;\n\treturn {\n\t\t.text = WithCaptionNotificationText(type, caption),\n\t};\n}\n\nTextWithEntities MediaPoll::notificationText() const {\n\treturn TextWithEntities()\n\t\t.append(QChar(0xD83D))\n\t\t.append(QChar(0xDCCA))\n\t\t.append(QChar(' '))\n\t\t.append(Ui::Text::Colorized(_poll->question));\n}\n\nQString MediaPoll::pinnedTextSubstring() const {\n\treturn QChar(171) + _poll->question.text + QChar(187);\n}\n\nTextForMimeData MediaPoll::clipboardText() const {\n\tauto result = TextWithEntities();\n\tif (!_consumedText.text.isEmpty()) {\n\t\tresult.append(_consumedText).append(u\"\\n\"_q);\n\t}\n\tresult\n\t\t.append(u\"[ \"_q)\n\t\t.append(tr::lng_in_dlg_poll(tr::now))\n\t\t.append(u\" : \"_q)\n\t\t.append(_poll->question)\n\t\t.append(u\" ]\"_q);\n\tfor (const auto &answer : _poll->answers) {\n\t\tresult.append(u\"\\n- \"_q).append(answer.text);\n\t}\n\treturn TextForMimeData::Rich(std::move(result));\n}\n\nbool MediaPoll::consumeMessageText(const TextWithEntities &text) {\n\t_consumedText = text;\n\treturn true;\n}\n\nTextWithEntities MediaPoll::consumedMessageText() const {\n\treturn _consumedText;\n}\n\nbool MediaPoll::updateInlineResultMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nbool MediaPoll::updateSentMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nstd::unique_ptr<HistoryView::Media> MediaPoll::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\treturn std::make_unique<HistoryView::Poll>(message, _poll, _consumedText);\n}\n\nMediaTodoList::MediaTodoList(\n\tnot_null<HistoryItem*> parent,\n\tnot_null<TodoListData*> todolist)\n: Media(parent)\n, _todolist(todolist) {\n}\n\nMediaTodoList::~MediaTodoList() {\n}\n\nstd::unique_ptr<Media> MediaTodoList::clone(not_null<HistoryItem*> parent) {\n\tconst auto id = parent->fullId();\n\treturn std::make_unique<MediaTodoList>(\n\t\tparent,\n\t\tparent->history()->owner().duplicateTodoList(id, _todolist));\n}\n\nTodoListData *MediaTodoList::todolist() const {\n\treturn _todolist;\n}\n\nTextWithEntities MediaTodoList::notificationText() const {\n\treturn TextWithEntities()\n\t\t.append(QChar(0x2705))\n\t\t.append(QChar(' '))\n\t\t.append(Ui::Text::Colorized(_todolist->title));\n}\n\nQString MediaTodoList::pinnedTextSubstring() const {\n\treturn QChar(171) + _todolist->title.text + QChar(187);\n}\n\nTextForMimeData MediaTodoList::clipboardText() const {\n\tauto result = TextWithEntities();\n\tresult\n\t\t.append(u\"[ \"_q)\n\t\t.append(tr::lng_in_dlg_todo_list(tr::now))\n\t\t.append(u\" : \"_q)\n\t\t.append(_todolist->title)\n\t\t.append(u\" ]\"_q);\n\tfor (const auto &item : _todolist->items) {\n\t\tresult.append(u\"\\n- \"_q).append(item.text);\n\t}\n\treturn TextForMimeData::Rich(std::move(result));\n}\n\nbool MediaTodoList::allowsEdit() const {\n\treturn parent()->out() || parent()->history()->peer->isSelf();\n}\n\nbool MediaTodoList::updateInlineResultMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nbool MediaTodoList::updateSentMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nstd::unique_ptr<HistoryView::Media> MediaTodoList::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\treturn std::make_unique<HistoryView::TodoList>(\n\t\tmessage,\n\t\t_todolist,\n\t\treplacing);\n}\n\nMediaDice::MediaDice(\n\tnot_null<HistoryItem*> parent,\n\tDiceGameOutcome outcome,\n\tQString emoji,\n\tint value)\n: Media(parent)\n, _outcome(outcome)\n, _emoji(emoji)\n, _value(value) {\n}\n\nstd::unique_ptr<Media> MediaDice::clone(not_null<HistoryItem*> parent) {\n\treturn std::make_unique<MediaDice>(parent, _outcome, _emoji, _value);\n}\n\nQString MediaDice::emoji() const {\n\treturn _emoji;\n}\n\nint MediaDice::value() const {\n\treturn _value;\n}\n\nDiceGameOutcome MediaDice::diceGameOutcome() const {\n\treturn _outcome;\n}\n\nbool MediaDice::allowsRevoke(TimeId now) const {\n\tconst auto peer = parent()->history()->peer;\n\tif (peer->isSelf() || !peer->isUser()) {\n\t\treturn true;\n\t}\n\treturn (now >= parent()->date() + kFastRevokeRestriction);\n}\n\nTextWithEntities MediaDice::notificationText() const {\n\treturn { .text = _emoji };\n}\n\nQString MediaDice::pinnedTextSubstring() const {\n\treturn QChar(171) + notificationText().text + QChar(187);\n}\n\nTextForMimeData MediaDice::clipboardText() const {\n\treturn { .rich = notificationText() };\n}\n\nbool MediaDice::forceForwardedInfo() const {\n\treturn true;\n}\n\nbool MediaDice::updateInlineResultMedia(const MTPMessageMedia &media) {\n\treturn updateSentMedia(media);\n}\n\nbool MediaDice::updateSentMedia(const MTPMessageMedia &media) {\n\tif (media.type() != mtpc_messageMediaDice) {\n\t\treturn false;\n\t}\n\tconst auto &data = media.c_messageMediaDice();\n\t_value = data.vvalue().v;\n\tif (const auto outcome = data.vgame_outcome()) {\n\t\tconst auto &data = outcome->data();\n\t\t_outcome = Data::DiceGameOutcome{\n\t\t\t.nanoTon = int64(data.vton_amount().v),\n\t\t\t.stakeNanoTon = int64(data.vstake_ton_amount().v),\n\t\t\t.seed = data.vseed().v,\n\t\t};\n\t} else {\n\t\t_outcome = {};\n\t}\n\tparent()->history()->owner().notifyItemDataChange(parent());\n\treturn true;\n}\n\nstd::unique_ptr<HistoryView::Media> MediaDice::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\treturn ::Stickers::DicePacks::IsSlot(_emoji)\n\t\t? std::make_unique<HistoryView::UnwrappedMedia>(\n\t\t\tmessage,\n\t\t\tstd::make_unique<HistoryView::SlotMachine>(message, this))\n\t\t: std::make_unique<HistoryView::UnwrappedMedia>(\n\t\t\tmessage,\n\t\t\tstd::make_unique<HistoryView::Dice>(message, this));\n}\n\nClickHandlerPtr MediaDice::makeHandler() const {\n\treturn MakeHandler(parent()->history(), _emoji);\n}\n\nClickHandlerPtr MediaDice::MakeHandler(\n\t\tnot_null<History*> history,\n\t\tconst QString &emoji) {\n\t// TODO support multi-windows.\n\tstatic auto ShownToast = base::weak_ptr<Ui::Toast::Instance>();\n\tstatic const auto HideExisting = [] {\n\t\tif (const auto toast = ShownToast.get()) {\n\t\t\ttoast->hideAnimated();\n\t\t\tShownToast = nullptr;\n\t\t}\n\t};\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto found = Ui::Emoji::Find(emoji);\n\t\tconst auto id = found ? found->id() : QString();\n\t\tconst auto game = (id == QString::fromUtf8(\"\\xf0\\x9f\\x8e\\xb2\"));\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tconst auto weak = my.sessionWindow;\n\t\tconst auto sendWith = [=](const QByteArray &hash, int64 nanoTon) {\n\t\t\tauto message = Api::MessageToSend(\n\t\t\t\tApi::SendAction(history));\n\t\t\tmessage.textWithTags.text = emoji;\n\n\t\t\tauto &action = message.action;\n\t\t\taction.clearDraft = false;\n\n\t\t\tauto &options = action.options;\n\t\t\toptions.stakeNanoTon = nanoTon;\n\t\t\toptions.stakeSeedHash = hash;\n\n\t\t\tApi::SendDice(message);\n\n\t\t\tHideExisting();\n\t\t};\n\t\tconst auto sendAllowed = CanSend(\n\t\t\thistory->peer,\n\t\t\tChatRestriction::SendOther);\n\t\tconst auto showToast = [=](Ui::Toast::Config &&config) {\n\t\t\tHideExisting();\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tShownToast = strong->showToast(std::move(config));\n\t\t\t} else {\n\t\t\t\tShownToast = Ui::Toast::Show(std::move(config));\n\t\t\t}\n\t\t};\n\t\tconst auto showSimple = [=] {\n\t\t\tauto config = Ui::Toast::Config{\n\t\t\t\t.text = { tr::lng_about_random(tr::now, lt_emoji, emoji) },\n\t\t\t\t.st = &st::historyDiceToast,\n\t\t\t\t.duration = Ui::Toast::kDefaultDuration * 2,\n\t\t\t};\n\t\t\tif (sendAllowed) {\n\t\t\t\tauto link = tr::link(tr::lng_about_random_send(tr::now));\n\t\t\t\tlink.entities.push_back(\n\t\t\t\t\tEntityInText(EntityType::Semibold, 0, link.text.size()));\n\t\t\t\tconfig.text.append(' ').append(std::move(link));\n\t\t\t\tconfig.filter = crl::guard(&history->session(), [=](\n\t\t\t\t\t\tconst ClickHandlerPtr &handler,\n\t\t\t\t\t\tQt::MouseButton button) {\n\t\t\t\t\tif (button == Qt::LeftButton && !ShownToast.empty()) {\n\t\t\t\t\t\tsendWith(QByteArray(), 0);\n\t\t\t\t\t}\n\t\t\t\t\treturn false;\n\t\t\t\t});\n\t\t\t}\n\t\t\tshowToast(std::move(config));\n\t\t};\n\t\tif (!game || !sendAllowed) {\n\t\t\tshowSimple();\n\t\t} else {\n\t\t\tconst auto pack = &history->session().diceStickersPacks();\n\t\t\tpack->resolveGameOptions([=](\n\t\t\t\t\tconst Data::DiceGameOptions &options) {\n\t\t\t\tconst auto window = weak.get();\n\t\t\t\tconst auto seedHash = options.seedHash;\n\t\t\t\tconst auto sendWithStake = [=](int64 stakeNanoTon) {\n\t\t\t\t\tsendWith(seedHash, stakeNanoTon);\n\t\t\t\t};\n\t\t\t\tif (!options || !window) {\n\t\t\t\t\tshowSimple();\n\t\t\t\t} else {\n\t\t\t\t\tshowToast(Ui::MakeEmojiGameStakeToast(window->uiShow(), {\n\t\t\t\t\t\t.session = &window->session(),\n\t\t\t\t\t\t.currentStake = options.previousSteakNanoTon,\n\t\t\t\t\t\t.milliRewards = options.milliRewards,\n\t\t\t\t\t\t.jackpotMilliReward = options.jackpotMilliReward,\n\t\t\t\t\t\t.submit = sendWithStake,\n\t\t\t\t\t}));\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t});\n}\n\nMediaGiftBox::MediaGiftBox(\n\tnot_null<HistoryItem*> parent,\n\tnot_null<PeerData*> from,\n\tGiftType type,\n\tint64 count)\n: MediaGiftBox(parent, from, GiftCode{ .count = count, .type = type }) {\n}\n\nMediaGiftBox::MediaGiftBox(\n\tnot_null<HistoryItem*> parent,\n\tnot_null<PeerData*> from,\n\tGiftCode data)\n: Media(parent)\n, _from(from)\n, _data(std::move(data)) {\n}\n\nstd::unique_ptr<Media> MediaGiftBox::clone(not_null<HistoryItem*> parent) {\n\treturn std::make_unique<MediaGiftBox>(parent, _from, _data);\n}\n\nnot_null<PeerData*> MediaGiftBox::from() const {\n\treturn _from;\n}\n\nconst GiftCode *MediaGiftBox::gift() const {\n\treturn &_data;\n}\n\nTextWithEntities MediaGiftBox::notificationText() const {\n\treturn {};\n}\n\nQString MediaGiftBox::pinnedTextSubstring() const {\n\treturn {};\n}\n\nTextForMimeData MediaGiftBox::clipboardText() const {\n\treturn {};\n}\n\nbool MediaGiftBox::updateInlineResultMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nbool MediaGiftBox::updateSentMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nstd::unique_ptr<HistoryView::Media> MediaGiftBox::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\tif (_data.type == GiftType::BirthdaySuggest) {\n\t\treturn std::make_unique<HistoryView::MediaGeneric>(\n\t\t\tmessage,\n\t\t\tHistoryView::GenerateSuggetsBirthdayMedia(\n\t\t\t\tmessage,\n\t\t\t\treplacing,\n\t\t\t\tData::Birthday::FromSerialized(_data.count)),\n\t\t\tHistoryView::MediaGenericDescriptor{\n\t\t\t\t.maxWidth = st::birthdaySuggestStickerWidth,\n\t\t\t\t.service = true,\n\t\t\t\t.hideServiceText = true,\n\t\t\t});\n\t} else if (_data.type == GiftType::ChatTheme\n\t\t|| _data.type == GiftType::GiftOffer) {\n\t\treturn std::make_unique<HistoryView::ServiceBox>(\n\t\t\tmessage,\n\t\t\tstd::make_unique<HistoryView::GiftServiceBox>(message, this));\n\t} else if (const auto &unique = _data.unique) {\n\t\treturn std::make_unique<HistoryView::MediaGeneric>(\n\t\t\tmessage,\n\t\t\tHistoryView::GenerateUniqueGiftMedia(message, replacing, unique),\n\t\t\tHistoryView::MediaGenericDescriptor{\n\t\t\t\t.maxWidth = st::msgServiceGiftBoxSize.width(),\n\t\t\t\t.paintBgFactory = [=] {\n\t\t\t\t\treturn HistoryView::UniqueGiftBg(message, unique);\n\t\t\t\t},\n\t\t\t\t.service = true,\n\t\t\t});\n\t}\n\treturn std::make_unique<HistoryView::ServiceBox>(\n\t\tmessage,\n\t\tstd::make_unique<HistoryView::PremiumGift>(message, this));\n}\n\nMediaWallPaper::MediaWallPaper(\n\tnot_null<HistoryItem*> parent,\n\tconst WallPaper &paper,\n\tbool paperForBoth)\n: Media(parent)\n, _paper(paper)\n, _paperForBoth(paperForBoth) {\n}\n\nMediaWallPaper::~MediaWallPaper() = default;\n\nstd::unique_ptr<Media> MediaWallPaper::clone(\n\t\tnot_null<HistoryItem*> parent) {\n\treturn std::make_unique<MediaWallPaper>(parent, _paper, _paperForBoth);\n}\n\nconst WallPaper *MediaWallPaper::paper() const {\n\treturn &_paper;\n}\n\nbool MediaWallPaper::paperForBoth() const {\n\treturn _paperForBoth;\n}\n\nTextWithEntities MediaWallPaper::notificationText() const {\n\treturn {};\n}\n\nQString MediaWallPaper::pinnedTextSubstring() const {\n\treturn {};\n}\n\nTextForMimeData MediaWallPaper::clipboardText() const {\n\treturn {};\n}\n\nbool MediaWallPaper::updateInlineResultMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nbool MediaWallPaper::updateSentMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nstd::unique_ptr<HistoryView::Media> MediaWallPaper::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\treturn std::make_unique<HistoryView::ServiceBox>(\n\t\tmessage,\n\t\tstd::make_unique<HistoryView::ThemeDocumentBox>(message, _paper));\n}\n\nMediaStory::MediaStory(\n\tnot_null<HistoryItem*> parent,\n\tFullStoryId storyId,\n\tbool mention)\n: Media(parent)\n, _storyId(storyId)\n, _mention(mention) {\n\tconst auto owner = &parent->history()->owner();\n\towner->registerStoryItem(storyId, parent);\n\n\tconst auto stories = &owner->stories();\n\tconst auto maybeStory = stories->lookup(storyId);\n\tif (!maybeStory && maybeStory.error() == NoStory::Unknown) {\n\t\tstories->resolve(storyId, crl::guard(this, [=] {\n\t\t\tif (const auto maybeStory = stories->lookup(storyId)) {\n\t\t\t\tif ((*maybeStory)->unsupported() || (*maybeStory)->call()) {\n\t\t\t\t\t_unsupported = true;\n\t\t\t\t} else if (!_mention && _viewMayExist) {\n\t\t\t\t\tparent->setText((*maybeStory)->caption());\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t_expired = true;\n\t\t\t}\n\t\t\tif (_mention) {\n\t\t\t\tparent->updateStoryMentionText();\n\t\t\t}\n\t\t\tparent->history()->owner().requestItemViewRefresh(parent);\n\t\t}));\n\t} else if (!maybeStory) {\n\t\t_expired = true;\n\t}\n}\n\nMediaStory::~MediaStory() {\n\tconst auto owner = &parent()->history()->owner();\n\towner->unregisterStoryItem(_storyId, parent());\n}\n\nstd::unique_ptr<Media> MediaStory::clone(not_null<HistoryItem*> parent) {\n\treturn std::make_unique<MediaStory>(parent, _storyId, false);\n}\n\nFullStoryId MediaStory::storyId() const {\n\treturn _storyId;\n}\n\nbool MediaStory::storyExpired(bool revalidate) {\n\tif (revalidate) {\n\t\tconst auto stories = &parent()->history()->owner().stories();\n\t\tif (const auto maybeStory = stories->lookup(_storyId)) {\n\t\t\tif ((*maybeStory)->unsupported() || (*maybeStory)->call()) {\n\t\t\t\t_unsupported = true;\n\t\t\t}\n\t\t\t_expired = false;\n\t\t} else if (maybeStory.error() == Data::NoStory::Deleted) {\n\t\t\t_expired = true;\n\t\t}\n\t}\n\treturn _expired;\n}\n\nbool MediaStory::storyUnsupported() const {\n\treturn _unsupported;\n}\n\nbool MediaStory::storyMention() const {\n\treturn _mention;\n}\n\nTextWithEntities MediaStory::notificationText() const {\n\tconst auto stories = &parent()->history()->owner().stories();\n\tconst auto maybeStory = stories->lookup(_storyId);\n\treturn WithCaptionNotificationText(\n\t\t((_expired\n\t\t\t|| (!maybeStory\n\t\t\t\t&& maybeStory.error() == Data::NoStory::Deleted))\n\t\t\t? tr::lng_in_dlg_story_expired\n\t\t\t: tr::lng_in_dlg_story)(tr::now),\n\t\t(maybeStory\n\t\t\t? (*maybeStory)->caption()\n\t\t\t: TextWithEntities()));\n}\n\nQString MediaStory::pinnedTextSubstring() const {\n\treturn tr::lng_action_pinned_media_story(tr::now);\n}\n\nTextForMimeData MediaStory::clipboardText() const {\n\treturn WithCaptionClipboardText(\n\t\t(_expired\n\t\t\t? tr::lng_in_dlg_story_expired\n\t\t\t: tr::lng_in_dlg_story)(tr::now),\n\t\tparent()->clipboardText());\n}\n\nbool MediaStory::dropForwardedInfo() const {\n\treturn true;\n}\n\nbool MediaStory::updateInlineResultMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nbool MediaStory::updateSentMedia(const MTPMessageMedia &media) {\n\treturn false;\n}\n\nnot_null<PhotoData*> MediaStory::LoadingStoryPhoto(\n\t\tnot_null<Session*> owner) {\n\treturn owner->photo(kLoadingStoryPhotoId);\n}\n\nstd::unique_ptr<HistoryView::Media> MediaStory::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\tconst auto spoiler = false;\n\tconst auto stories = &parent()->history()->owner().stories();\n\tconst auto maybeStory = stories->lookup(_storyId);\n\tif (!maybeStory) {\n\t\tif (!_mention) {\n\t\t\trealParent->setText(TextWithEntities());\n\t\t}\n\t\tif (maybeStory.error() == Data::NoStory::Deleted) {\n\t\t\t_expired = true;\n\t\t\treturn nullptr;\n\t\t}\n\t\t_expired = false;\n\t\tif (_mention) {\n\t\t\treturn nullptr;\n\t\t}\n\t\t_viewMayExist = true;\n\t\treturn std::make_unique<HistoryView::Photo>(\n\t\t\tmessage,\n\t\t\trealParent,\n\t\t\tLoadingStoryPhoto(&realParent->history()->owner()),\n\t\t\tspoiler);\n\t}\n\t_expired = false;\n\t_viewMayExist = true;\n\tconst auto story = *maybeStory;\n\tif (story->unsupported() || story->call()) {\n\t\t_unsupported = true;\n\t\treturn nullptr;\n\t} else if (_mention) {\n\t\treturn std::make_unique<HistoryView::ServiceBox>(\n\t\t\tmessage,\n\t\t\tstd::make_unique<HistoryView::StoryMention>(message, story));\n\t} else {\n\t\trealParent->setText(story->caption());\n\t\tif (const auto photo = story->photo()) {\n\t\t\treturn std::make_unique<HistoryView::Photo>(\n\t\t\t\tmessage,\n\t\t\t\trealParent,\n\t\t\t\tphoto,\n\t\t\t\tspoiler);\n\t\t} else if (const auto document = story->document()) {\n\t\t\treturn std::make_unique<HistoryView::Gif>(\n\t\t\t\tmessage,\n\t\t\t\trealParent,\n\t\t\t\tdocument,\n\t\t\t\tspoiler);\n\t\t}\n\t\treturn nullptr;\n\t}\n}\n\nMediaGiveawayStart::MediaGiveawayStart(\n\tnot_null<HistoryItem*> parent,\n\tconst GiveawayStart &data)\n: Media(parent)\n, _data(data) {\n\tparent->history()->session().giftBoxStickersPacks().load();\n}\n\nstd::unique_ptr<Media> MediaGiveawayStart::clone(\n\t\tnot_null<HistoryItem*> parent) {\n\treturn std::make_unique<MediaGiveawayStart>(parent, _data);\n}\n\nconst GiveawayStart *MediaGiveawayStart::giveawayStart() const {\n\treturn &_data;\n}\n\nTextWithEntities MediaGiveawayStart::notificationText() const {\n\treturn {\n\t\t.text = tr::lng_prizes_title(tr::now, lt_count, _data.quantity),\n\t};\n}\n\nQString MediaGiveawayStart::pinnedTextSubstring() const {\n\treturn QString::fromUtf8(\"\\xC2\\xAB\")\n\t\t+ notificationText().text\n\t\t+ QString::fromUtf8(\"\\xC2\\xBB\");\n}\n\nTextForMimeData MediaGiveawayStart::clipboardText() const {\n\treturn TextForMimeData();\n}\n\nbool MediaGiveawayStart::updateInlineResultMedia(const MTPMessageMedia &media) {\n\treturn true;\n}\n\nbool MediaGiveawayStart::updateSentMedia(const MTPMessageMedia &media) {\n\treturn true;\n}\n\nstd::unique_ptr<HistoryView::Media> MediaGiveawayStart::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\treturn std::make_unique<HistoryView::MediaGeneric>(\n\t\tmessage,\n\t\tHistoryView::GenerateGiveawayStart(message, &_data));\n}\n\nMediaGiveawayResults::MediaGiveawayResults(\n\tnot_null<HistoryItem*> parent,\n\tconst GiveawayResults &data)\n: Media(parent)\n, _data(data) {\n}\n\nstd::unique_ptr<Media> MediaGiveawayResults::clone(\n\t\tnot_null<HistoryItem*> parent) {\n\treturn std::make_unique<MediaGiveawayResults>(parent, _data);\n}\n\nconst GiveawayResults *MediaGiveawayResults::giveawayResults() const {\n\treturn &_data;\n}\n\nTextWithEntities MediaGiveawayResults::notificationText() const {\n\treturn Ui::Text::Colorized({\n\t\t((_data.winnersCount == 1)\n\t\t\t? tr::lng_prizes_results_title_one\n\t\t\t: tr::lng_prizes_results_title)(tr::now)\n\t});\n}\n\nQString MediaGiveawayResults::pinnedTextSubstring() const {\n\treturn QString::fromUtf8(\"\\xC2\\xAB\")\n\t\t+ notificationText().text\n\t\t+ QString::fromUtf8(\"\\xC2\\xBB\");\n}\n\nTextForMimeData MediaGiveawayResults::clipboardText() const {\n\treturn TextForMimeData();\n}\n\nbool MediaGiveawayResults::updateInlineResultMedia(const MTPMessageMedia &media) {\n\treturn true;\n}\n\nbool MediaGiveawayResults::updateSentMedia(const MTPMessageMedia &media) {\n\treturn true;\n}\n\nstd::unique_ptr<HistoryView::Media> MediaGiveawayResults::createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing) {\n\treturn std::make_unique<HistoryView::MediaGeneric>(\n\t\tmessage,\n\t\tHistoryView::GenerateGiveawayResults(message, &_data));\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_media_types.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"data/data_location.h\"\n#include \"data/data_wall_paper.h\"\n\nclass Image;\nclass History;\nclass HistoryItem;\n\nnamespace base {\ntemplate <typename Enum>\nclass enum_mask;\n} // namespace base\n\nnamespace Storage {\nenum class SharedMediaType : signed char;\nusing SharedMediaTypesMask = base::enum_mask<SharedMediaType>;\n} // namespace Storage\n\nnamespace HistoryView {\nenum class Context : char;\nclass Element;\nclass Media;\nstruct ItemPreview;\nstruct ItemPreviewImage;\nstruct ToPreviewOptions;\n} // namespace HistoryView\n\nnamespace Data {\n\nclass CloudImage;\nclass WallPaper;\nclass Session;\nstruct UniqueGift;\n\nenum class CallState : char {\n\tMissed,\n\tBusy,\n\tDisconnected,\n\tHangup,\n\tMigrateConferenceCall,\n\tInvitation,\n\tActive,\n};\n\nstruct SharedContact final {\n\tUserId userId = 0;\n\tQString firstName;\n\tQString lastName;\n\tQString phoneNumber;\n\n\tenum class VcardItemType {\n\t\tPhone,\n\t\tPhoneMain,\n\t\tPhoneHome,\n\t\tPhoneMobile,\n\t\tPhoneWork,\n\t\tPhoneOther,\n\t\tEmail,\n\t\tAddress,\n\t\tUrl,\n\t\tNote,\n\t\tBirthday,\n\t\tOrganization,\n\t\tName,\n\t};\n\n\tusing VcardItems = base::flat_map<VcardItemType, QString>;\n\tstatic VcardItems ParseVcard(const QString &);\n\n\tVcardItems vcardItems;\n};\n\nstruct Call {\n\tusing State = CallState;\n\n\tstd::vector<not_null<PeerData*>> otherParticipants;\n\tCallId conferenceId = 0;\n\tint duration = 0;\n\tState state = State::Missed;\n\tbool video = false;\n\n};\n\nclass Media;\n\nstruct Invoice {\n\tMsgId receiptMsgId = 0;\n\tuint64 amount = 0;\n\tQString currency;\n\tQString title;\n\tTextWithEntities description;\n\tstd::vector<std::unique_ptr<Media>> extendedMedia;\n\tPhotoData *photo = nullptr;\n\tbool isPaidMedia = false;\n\tbool isTest = false;\n};\n[[nodiscard]] bool HasExtendedMedia(const Invoice &invoice);\n[[nodiscard]] bool HasUnpaidMedia(const Invoice &invoice);\n[[nodiscard]] bool IsFirstVideo(const Invoice &invoice);\n\nstruct GiveawayStart {\n\tstd::vector<not_null<ChannelData*>> channels;\n\tstd::vector<QString> countries;\n\tQString additionalPrize;\n\tTimeId untilDate = 0;\n\tint quantity = 0;\n\tint months = 0;\n\tuint64 credits = 0;\n\tbool all = false;\n};\n\nstruct GiveawayResults {\n\tnot_null<ChannelData*> channel;\n\tstd::vector<not_null<PeerData*>> winners;\n\tQString additionalPrize;\n\tTimeId untilDate = 0;\n\tMsgId launchId = 0;\n\tint additionalPeersCount = 0;\n\tint winnersCount = 0;\n\tint unclaimedCount = 0;\n\tint months = 0;\n\tuint64 credits = 0;\n\tbool refunded = false;\n\tbool all = false;\n};\n\nstruct DiceGameOptions {\n\tQByteArray seedHash;\n\tint64 previousSteakNanoTon = 0;\n\tstd::array<int, 6> milliRewards;\n\tint jackpotMilliReward = 0;\n\tint currentStreak = 0;\n\tint playsLeft = 0;\n\n\texplicit operator bool() const {\n\t\treturn !seedHash.isEmpty();\n\t}\n};\n\nstruct DiceGameOutcome {\n\tint64 nanoTon = 0;\n\tint64 stakeNanoTon = 0;\n\tQByteArray seed;\n\n\texplicit operator bool() const {\n\t\treturn stakeNanoTon != 0;\n\t}\n};\n\nenum class GiftType : uchar {\n\tPremium, // count - days\n\tCredits, // count - credits\n\tTon, // count - nano tons\n\tStarGift, // count - stars\n\tChatTheme,\n\tBirthdaySuggest,\n\tGiftOffer,\n};\n\nstruct GiftCode {\n\tQString slug;\n\tuint64 stargiftId = 0;\n\tDocumentData *document = nullptr;\n\tPeerData *stargiftReleasedBy = nullptr;\n\tstd::shared_ptr<UniqueGift> unique;\n\tTextWithEntities message;\n\tPeerData *auctionTo = nullptr;\n\tChannelData *channel = nullptr;\n\tPeerData *channelFrom = nullptr;\n\tuint64 channelSavedId = 0;\n\tQString giftPrepayUpgradeHash;\n\tQString giftTitle;\n\tMsgId giveawayMsgId = 0;\n\tMsgId realGiftMsgId = 0;\n\tint starsConverted = 0;\n\tint starsToUpgrade = 0;\n\tint starsUpgradedBySender = 0;\n\tint starsForDetailsRemove = 0;\n\tint starsBid = 0;\n\tint giftNum = 0;\n\tint limitedCount = 0;\n\tint limitedLeft = 0;\n\tint64 count = 0;\n\tGiftType type = GiftType::Premium;\n\tbool viaGiveaway : 1 = false;\n\tbool transferred : 1 = false;\n\tbool upgradeSeparate : 1 = false;\n\tbool upgradeGifted : 1 = false;\n\tbool upgradable : 1 = false;\n\tbool unclaimed : 1 = false;\n\tbool anonymous : 1 = false;\n\tbool converted : 1 = false;\n\tbool upgraded : 1 = false;\n\tbool refunded : 1 = false;\n\tbool upgrade : 1 = false;\n\tbool saved : 1 = false;\n\tbool craft : 1 = false;\n};\n\nclass Media {\npublic:\n\tMedia(not_null<HistoryItem*> parent);\n\tvirtual ~Media() = default;\n\n\tnot_null<HistoryItem*> parent() const;\n\n\tusing ToPreviewOptions = HistoryView::ToPreviewOptions;\n\tusing ItemPreviewImage = HistoryView::ItemPreviewImage;\n\tusing ItemPreview = HistoryView::ItemPreview;\n\n\tvirtual std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) = 0;\n\n\tvirtual DocumentData *document() const;\n\tvirtual PhotoData *videoCover() const;\n\tvirtual TimeId videoTimestamp() const;\n\tvirtual bool hasQualitiesList() const;\n\tvirtual PhotoData *photo() const;\n\tvirtual WebPageData *webpage() const;\n\tvirtual MediaWebPageFlags webpageFlags() const;\n\tvirtual const SharedContact *sharedContact() const;\n\tvirtual const Call *call() const;\n\tvirtual GameData *game() const;\n\tvirtual const Invoice *invoice() const;\n\tvirtual const GiftCode *gift() const;\n\tvirtual CloudImage *location() const;\n\tvirtual PollData *poll() const;\n\tvirtual TodoListData *todolist() const;\n\tvirtual const WallPaper *paper() const;\n\tvirtual bool paperForBoth() const;\n\tvirtual FullStoryId storyId() const;\n\tvirtual bool storyExpired(bool revalidate = false);\n\tvirtual bool storyUnsupported() const;\n\tvirtual bool storyMention() const;\n\tvirtual const GiveawayStart *giveawayStart() const;\n\tvirtual const GiveawayResults *giveawayResults() const;\n\tvirtual DiceGameOutcome diceGameOutcome() const;\n\n\tvirtual bool uploading() const;\n\tvirtual Storage::SharedMediaTypesMask sharedMediaTypes() const;\n\tvirtual bool canBeGrouped() const;\n\tvirtual bool hasReplyPreview() const;\n\tvirtual Image *replyPreview() const;\n\tvirtual bool replyPreviewLoaded() const;\n\t// Returns text with link-start and link-end commands for service-color highlighting.\n\t// Example: \"[link1-start]You:[link1-end] [link1-start]Photo,[link1-end] caption text\"\n\tvirtual ItemPreview toPreview(ToPreviewOptions way) const;\n\tvirtual TextWithEntities notificationText() const = 0;\n\tvirtual QString pinnedTextSubstring() const = 0;\n\tvirtual TextForMimeData clipboardText() const = 0;\n\tvirtual bool allowsForward() const;\n\tvirtual bool allowsEdit() const;\n\tvirtual bool allowsEditCaption() const;\n\tvirtual bool allowsEditMedia() const;\n\tvirtual bool allowsRevoke(TimeId now) const;\n\tvirtual bool forwardedBecomesUnread() const;\n\tvirtual bool dropForwardedInfo() const;\n\tvirtual bool forceForwardedInfo() const;\n\t[[nodiscard]] virtual bool hasSpoiler() const;\n\t[[nodiscard]] virtual crl::time ttlSeconds() const;\n\n\t[[nodiscard]] virtual bool consumeMessageText(\n\t\tconst TextWithEntities &text);\n\t[[nodiscard]] virtual TextWithEntities consumedMessageText() const;\n\n\t// After sending an inline result we may want to completely recreate\n\t// the media (all media that was generated on client side, for example).\n\tvirtual bool updateInlineResultMedia(const MTPMessageMedia &media) = 0;\n\tvirtual bool updateSentMedia(const MTPMessageMedia &media) = 0;\n\tvirtual bool updateExtendedMedia(\n\t\t\tnot_null<HistoryItem*> item,\n\t\t\tconst QVector<MTPMessageExtendedMedia> &media) {\n\t\treturn false;\n\t}\n\tvirtual std::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) = 0;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tHistoryView::Element *replacing = nullptr);\n\nprotected:\n\t[[nodiscard]] ItemPreview toGroupPreview(\n\t\tconst HistoryItemsList &items,\n\t\tToPreviewOptions options) const;\n\nprivate:\n\tconst not_null<HistoryItem*> _parent;\n\n};\n\nclass MediaPhoto final : public Media {\npublic:\n\tMediaPhoto(\n\t\tnot_null<HistoryItem*> parent,\n\t\tnot_null<PhotoData*> photo,\n\t\tbool spoiler);\n\tMediaPhoto(\n\t\tnot_null<HistoryItem*> parent,\n\t\tnot_null<PeerData*> chat,\n\t\tnot_null<PhotoData*> photo);\n\t~MediaPhoto();\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\tPhotoData *photo() const override;\n\n\tbool uploading() const override;\n\tStorage::SharedMediaTypesMask sharedMediaTypes() const override;\n\tbool canBeGrouped() const override;\n\tbool hasReplyPreview() const override;\n\tImage *replyPreview() const override;\n\tbool replyPreviewLoaded() const override;\n\tItemPreview toPreview(ToPreviewOptions options) const override;\n\tTextWithEntities notificationText() const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\tbool allowsEditCaption() const override;\n\tbool allowsEditMedia() const override;\n\tbool hasSpoiler() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\nprivate:\n\tnot_null<PhotoData*> _photo;\n\tPeerData *_chat = nullptr;\n\tbool _spoiler = false;\n\n};\n\nclass MediaFile final : public Media {\npublic:\n\tstruct Args {\n\t\tcrl::time ttlSeconds = 0;\n\t\tPhotoData *videoCover = nullptr;\n\t\tTimeId videoTimestamp = 0;\n\t\tbool hasQualitiesList = false;\n\t\tbool skipPremiumEffect = false;\n\t\tbool spoiler = false;\n\t};\n\n\tMediaFile(\n\t\tnot_null<HistoryItem*> parent,\n\t\tnot_null<DocumentData*> document,\n\t\tArgs &&args);\n\t~MediaFile();\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\tDocumentData *document() const override;\n\tPhotoData *videoCover() const override;\n\tTimeId videoTimestamp() const override;\n\tbool hasQualitiesList() const override;\n\n\tbool uploading() const override;\n\tStorage::SharedMediaTypesMask sharedMediaTypes() const override;\n\tbool canBeGrouped() const override;\n\tbool hasReplyPreview() const override;\n\tImage *replyPreview() const override;\n\tbool replyPreviewLoaded() const override;\n\tItemPreview toPreview(ToPreviewOptions options) const override;\n\tTextWithEntities notificationText() const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\tbool allowsEditCaption() const override;\n\tbool allowsEditMedia() const override;\n\tbool forwardedBecomesUnread() const override;\n\tbool dropForwardedInfo() const override;\n\tbool hasSpoiler() const override;\n\tcrl::time ttlSeconds() const override;\n\tbool allowsForward() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\nprivate:\n\tnot_null<DocumentData*> _document;\n\tPhotoData *_videoCover = nullptr;\n\n\t// Video (unsupported) / Voice / Round.\n\tcrl::time _ttlSeconds = 0;\n\n\tQString _emoji;\n\tTimeId _videoTimestamp = 0;\n\tbool _skipPremiumEffect = false;\n\tbool _hasQualitiesList = false;\n\tbool _spoiler = false;\n\n};\n\nclass MediaContact final : public Media {\npublic:\n\tMediaContact(\n\t\tnot_null<HistoryItem*> parent,\n\t\tUserId userId,\n\t\tconst QString &firstName,\n\t\tconst QString &lastName,\n\t\tconst QString &phoneNumber,\n\t\tconst SharedContact::VcardItems &vcardItems);\n\t~MediaContact();\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\tconst SharedContact *sharedContact() const override;\n\tTextWithEntities notificationText() const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\nprivate:\n\tSharedContact _contact;\n\n};\n\nclass MediaLocation final : public Media {\n\tstruct PrivateTag {\n\t};\n\npublic:\n\tMediaLocation(\n\t\tnot_null<HistoryItem*> parent,\n\t\tconst LocationPoint &point,\n\t\tTimeId livePeriod = 0);\n\tMediaLocation(\n\t\tnot_null<HistoryItem*> parent,\n\t\tconst LocationPoint &point,\n\t\tconst QString &title,\n\t\tconst QString &description);\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\tCloudImage *location() const override;\n\tItemPreview toPreview(ToPreviewOptions options) const override;\n\tTextWithEntities notificationText() const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\n\tMediaLocation(\n\t\tPrivateTag,\n\t\tnot_null<HistoryItem*> parent,\n\t\tconst LocationPoint &point,\n\t\tTimeId livePeriod,\n\t\tconst QString &title,\n\t\tconst QString &description);\n\nprivate:\n\n\t[[nodiscard]] QString typeString() const;\n\n\tLocationPoint _point;\n\tnot_null<CloudImage*> _location;\n\tTimeId _livePeriod = 0;\n\tQString _title;\n\tQString _description;\n\n};\n\nclass MediaCall final : public Media {\npublic:\n\tMediaCall(not_null<HistoryItem*> parent, const Call &call);\n\t~MediaCall();\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\tconst Call *call() const override;\n\tTextWithEntities notificationText() const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\tbool allowsForward() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\n\t[[nodiscard]] static QString Text(\n\t\tnot_null<HistoryItem*> item,\n\t\tCallState state,\n\t\tbool conference,\n\t\tbool video);\n\nprivate:\n\tCall _call;\n\n};\n\nclass MediaWebPage final : public Media {\npublic:\n\tMediaWebPage(\n\t\tnot_null<HistoryItem*> parent,\n\t\tnot_null<WebPageData*> page,\n\t\tMediaWebPageFlags flags);\n\t~MediaWebPage();\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\tDocumentData *document() const override;\n\tPhotoData *photo() const override;\n\tWebPageData *webpage() const override;\n\tMediaWebPageFlags webpageFlags() const override;\n\n\tStorage::SharedMediaTypesMask sharedMediaTypes() const override;\n\n\tbool hasReplyPreview() const override;\n\tImage *replyPreview() const override;\n\tbool replyPreviewLoaded() const override;\n\tItemPreview toPreview(ToPreviewOptions options) const override;\n\tTextWithEntities notificationText() const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\tbool allowsEdit() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\nprivate:\n\tconst not_null<WebPageData*> _page;\n\tconst MediaWebPageFlags _flags;\n\n};\n\nclass MediaGame final : public Media {\npublic:\n\tMediaGame(\n\t\tnot_null<HistoryItem*> parent,\n\t\tnot_null<GameData*> game);\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\tGameData *game() const override;\n\n\tbool hasReplyPreview() const override;\n\tImage *replyPreview() const override;\n\tbool replyPreviewLoaded() const override;\n\tTextWithEntities notificationText() const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\tbool dropForwardedInfo() const override;\n\n\tbool consumeMessageText(const TextWithEntities &text) override;\n\tTextWithEntities consumedMessageText() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\nprivate:\n\tnot_null<GameData*> _game;\n\tTextWithEntities _consumedText;\n\n};\n\nclass MediaInvoice final : public Media {\npublic:\n\tMediaInvoice(\n\t\tnot_null<HistoryItem*> parent,\n\t\tconst Invoice &data);\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\tconst Invoice *invoice() const override;\n\n\tbool hasReplyPreview() const override;\n\tImage *replyPreview() const override;\n\tbool replyPreviewLoaded() const override;\n\tTextWithEntities notificationText() const override;\n\tItemPreview toPreview(ToPreviewOptions way) const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tbool updateExtendedMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst QVector<MTPMessageExtendedMedia> &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\nprivate:\n\tInvoice _invoice;\n\n};\n\nclass MediaPoll final : public Media {\npublic:\n\tMediaPoll(\n\t\tnot_null<HistoryItem*> parent,\n\t\tnot_null<PollData*> poll);\n\t~MediaPoll();\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\tPollData *poll() const override;\n\tStorage::SharedMediaTypesMask sharedMediaTypes() const override;\n\n\tItemPreview toPreview(ToPreviewOptions options) const override;\n\tTextWithEntities notificationText() const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\tbool consumeMessageText(const TextWithEntities &text) override;\n\tTextWithEntities consumedMessageText() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\nprivate:\n\tnot_null<PollData*> _poll;\n\tTextWithEntities _consumedText;\n\n};\n\nclass MediaTodoList final : public Media {\npublic:\n\tMediaTodoList(\n\t\tnot_null<HistoryItem*> parent,\n\t\tnot_null<TodoListData*> todolist);\n\t~MediaTodoList();\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\tTodoListData *todolist() const override;\n\n\tTextWithEntities notificationText() const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\tbool allowsEdit() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\nprivate:\n\tnot_null<TodoListData*> _todolist;\n\n};\n\nclass MediaDice final : public Media {\npublic:\n\tMediaDice(\n\t\tnot_null<HistoryItem*> parent,\n\t\tDiceGameOutcome outcome,\n\t\tQString emoji,\n\t\tint value);\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\t[[nodiscard]] QString emoji() const;\n\t[[nodiscard]] int value() const;\n\n\tbool allowsRevoke(TimeId now) const override;\n\tTextWithEntities notificationText() const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\tbool forceForwardedInfo() const override;\n\tDiceGameOutcome diceGameOutcome() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\n\t[[nodiscard]] ClickHandlerPtr makeHandler() const;\n\t[[nodiscard]] static ClickHandlerPtr MakeHandler(\n\t\tnot_null<History*> history,\n\t\tconst QString &emoji);\n\nprivate:\n\tDiceGameOutcome _outcome;\n\tQString _emoji;\n\tint _value = 0;\n\n};\n\nclass MediaGiftBox final : public Media {\npublic:\n\tMediaGiftBox(\n\t\tnot_null<HistoryItem*> parent,\n\t\tnot_null<PeerData*> from,\n\t\tGiftType type,\n\t\tint64 count);\n\tMediaGiftBox(\n\t\tnot_null<HistoryItem*> parent,\n\t\tnot_null<PeerData*> from,\n\t\tGiftCode data);\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\t[[nodiscard]] not_null<PeerData*> from() const;\n\t[[nodiscard]] const GiftCode *gift() const override;\n\n\tTextWithEntities notificationText() const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\nprivate:\n\tnot_null<PeerData*> _from;\n\tGiftCode _data;\n\n};\n\nclass MediaWallPaper final : public Media {\npublic:\n\tMediaWallPaper(\n\t\tnot_null<HistoryItem*> parent,\n\t\tconst WallPaper &paper,\n\t\tbool paperForBoth);\n\t~MediaWallPaper();\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\tconst WallPaper *paper() const override;\n\tbool paperForBoth() const override;\n\n\tTextWithEntities notificationText() const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\nprivate:\n\tconst WallPaper _paper;\n\tconst bool _paperForBoth = false;\n\n};\n\nclass MediaStory final : public Media, public base::has_weak_ptr {\npublic:\n\tMediaStory(\n\t\tnot_null<HistoryItem*> parent,\n\t\tFullStoryId storyId,\n\t\tbool mention);\n\t~MediaStory();\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\tFullStoryId storyId() const override;\n\tbool storyExpired(bool revalidate = false) override;\n\tbool storyUnsupported() const override;\n\tbool storyMention() const override;\n\n\tTextWithEntities notificationText() const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\tbool dropForwardedInfo() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\n\t[[nodiscard]] static not_null<PhotoData*> LoadingStoryPhoto(\n\t\tnot_null<Session*> owner);\n\nprivate:\n\tconst FullStoryId _storyId;\n\tconst bool _mention = false;\n\tbool _viewMayExist = false;\n\tbool _unsupported = false;\n\tbool _expired = false;\n\n};\n\nclass MediaGiveawayStart final : public Media {\npublic:\n\tMediaGiveawayStart(\n\t\tnot_null<HistoryItem*> parent,\n\t\tconst GiveawayStart &data);\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\tconst GiveawayStart *giveawayStart() const override;\n\n\tTextWithEntities notificationText() const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\nprivate:\n\tGiveawayStart _data;\n\n};\n\nclass MediaGiveawayResults final : public Media {\npublic:\n\tMediaGiveawayResults(\n\t\tnot_null<HistoryItem*> parent,\n\t\tconst GiveawayResults &data);\n\n\tstd::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;\n\n\tconst GiveawayResults *giveawayResults() const override;\n\n\tTextWithEntities notificationText() const override;\n\tQString pinnedTextSubstring() const override;\n\tTextForMimeData clipboardText() const override;\n\n\tbool updateInlineResultMedia(const MTPMessageMedia &media) override;\n\tbool updateSentMedia(const MTPMessageMedia &media) override;\n\tstd::unique_ptr<HistoryView::Media> createView(\n\t\tnot_null<HistoryView::Element*> message,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tHistoryView::Element *replacing = nullptr) override;\n\nprivate:\n\tGiveawayResults _data;\n\n};\n\n[[nodiscard]] Invoice ComputeInvoiceData(\n\tnot_null<HistoryItem*> item,\n\tconst MTPDmessageMediaInvoice &data);\n[[nodiscard]] Invoice ComputeInvoiceData(\n\tnot_null<HistoryItem*> item,\n\tconst MTPDmessageMediaPaidMedia &data);\n\n[[nodiscard]] Call ComputeCallData(\n\tnot_null<Session*> owner,\n\tconst MTPDmessageActionPhoneCall &call);\n[[nodiscard]] Call ComputeCallData(\n\tnot_null<Session*> owner,\n\tconst MTPDmessageActionConferenceCall &call);\n\n[[nodiscard]] GiveawayStart ComputeGiveawayStartData(\n\tnot_null<HistoryItem*> item,\n\tconst MTPDmessageMediaGiveaway &data);\n\n[[nodiscard]] GiveawayResults ComputeGiveawayResultsData(\n\tnot_null<HistoryItem*> item,\n\tconst MTPDmessageMediaGiveawayResults &data);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_message_reaction_id.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_message_reaction_id.h\"\n\n#include \"data/stickers/data_custom_emoji.h\"\n\nnamespace Data {\n\nQString SearchTagToQuery(const ReactionId &tagId) {\n\tif (const auto customId = tagId.custom()) {\n\t\treturn u\"#tag-custom:%1\"_q.arg(customId);\n\t} else if (!tagId) {\n\t\treturn QString();\n\t}\n\treturn u\"#tag-emoji:\"_q + tagId.emoji();\n}\n\nReactionId SearchTagFromQuery(const QString &query) {\n\tconst auto list = query.split(QChar(' '));\n\tconst auto tag = list.isEmpty() ? QString() : list[0];\n\tif (tag.startsWith(u\"#tag-custom:\"_q)) {\n\t\treturn ReactionId{ DocumentId(tag.mid(12).toULongLong()) };\n\t} else if (tag.startsWith(u\"#tag-emoji:\"_q)) {\n\t\treturn ReactionId{ tag.mid(11) };\n\t}\n\treturn {};\n}\n\nstd::vector<ReactionId> SearchTagsFromQuery(\n\t\tconst QString &query) {\n\tauto result = std::vector<ReactionId>();\n\tif (const auto tag = SearchTagFromQuery(query)) {\n\t\tresult.push_back(tag);\n\t}\n\treturn result;\n}\n\nHashtagWithUsername HashtagWithUsernameFromQuery(QStringView query) {\n\tconst auto match = TextUtilities::RegExpHashtag(true).match(query);\n\tif (match.hasMatch()) {\n\t\tconst auto username = match.capturedView(2).mid(1).toString();\n\t\tconst auto offset = int(match.capturedLength(1));\n\t\tconst auto full = int(query.size());\n\t\tconst auto length = full\n\t\t\t- int(username.size())\n\t\t\t- 1\n\t\t\t- offset\n\t\t\t- int(match.capturedLength(3));\n\t\tif (!username.isEmpty() && length > 0 && offset + length <= full) {\n\t\t\tconst auto hashtag = query.mid(offset, length).toString();\n\t\t\treturn { hashtag, username };\n\t\t}\n\t}\n\treturn {};\n}\n\nQString ReactionEntityData(const ReactionId &id) {\n\tif (id.empty()) {\n\t\treturn {};\n\t} else if (const auto custom = id.custom()) {\n\t\treturn SerializeCustomEmojiId(custom);\n\t}\n\treturn u\"default:\"_q + id.emoji();\n}\n\nReactionId ReactionFromMTP(const MTPReaction &reaction) {\n\treturn reaction.match([](MTPDreactionEmpty) {\n\t\treturn ReactionId{ QString() };\n\t}, [](const MTPDreactionEmoji &data) {\n\t\treturn ReactionId{ qs(data.vemoticon()) };\n\t}, [](const MTPDreactionCustomEmoji &data) {\n\t\treturn ReactionId{ DocumentId(data.vdocument_id().v) };\n\t}, [](const MTPDreactionPaid &) {\n\t\treturn ReactionId::Paid();\n\t});\n}\n\nMTPReaction ReactionToMTP(ReactionId id) {\n\tif (!id) {\n\t\treturn MTP_reactionEmpty();\n\t} else if (id.paid()) {\n\t\treturn MTP_reactionPaid();\n\t} else if (const auto custom = id.custom()) {\n\t\treturn MTP_reactionCustomEmoji(MTP_long(custom));\n\t} else {\n\t\treturn MTP_reactionEmoji(MTP_string(id.emoji()));\n\t}\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_message_reaction_id.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/qt/qt_compare.h\"\n\nnamespace Data {\n\nstruct ReactionId {\n\tstd::variant<QString, DocumentId> data;\n\n\t[[nodiscard]] static QChar PaidTag() {\n\t\treturn '*';\n\t}\n\t[[nodiscard]] static ReactionId Paid() {\n\t\treturn { QString(PaidTag()) };\n\t}\n\n\t[[nodiscard]] bool empty() const {\n\t\tconst auto emoji = std::get_if<QString>(&data);\n\t\treturn emoji && emoji->isEmpty();\n\t}\n\t[[nodiscard]] bool paid() const {\n\t\tconst auto emoji = std::get_if<QString>(&data);\n\t\treturn emoji\n\t\t\t&& emoji->size() == 1\n\t\t\t&& emoji->at(0) == PaidTag();\n\t}\n\t[[nodiscard]] QString emoji() const {\n\t\tconst auto emoji = std::get_if<QString>(&data);\n\t\treturn (emoji && (emoji->size() != 1 || emoji->at(0) != PaidTag()))\n\t\t\t? *emoji\n\t\t\t: QString();\n\t}\n\t[[nodiscard]] DocumentId custom() const {\n\t\tconst auto custom = std::get_if<DocumentId>(&data);\n\t\treturn custom ? *custom : DocumentId();\n\t}\n\n\texplicit operator bool() const {\n\t\treturn !empty();\n\t}\n\n\tfriend inline auto operator<=>(\n\t\tconst ReactionId &,\n\t\tconst ReactionId &) = default;\n\tfriend inline bool operator==(\n\t\tconst ReactionId &a,\n\t\tconst ReactionId &b) = default;\n};\n\nstruct MessageReaction {\n\tReactionId id;\n\tint count = 0;\n\tbool my = false;\n};\n\n[[nodiscard]] QString SearchTagToQuery(const ReactionId &tagId);\n[[nodiscard]] ReactionId SearchTagFromQuery(const QString &query);\n[[nodiscard]] std::vector<ReactionId> SearchTagsFromQuery(\n\tconst QString &query);\n\nstruct HashtagWithUsername {\n\tQString hashtag;\n\tQString username;\n};\n[[nodiscard]] HashtagWithUsername HashtagWithUsernameFromQuery(\n\tQStringView query);\n\n[[nodiscard]] QString ReactionEntityData(const ReactionId &id);\n\n[[nodiscard]] ReactionId ReactionFromMTP(const MTPReaction &reaction);\n[[nodiscard]] MTPReaction ReactionToMTP(ReactionId id);\n\n} // namespace Data\n\nQ_DECLARE_METATYPE(Data::ReactionId);\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_message_reactions.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_message_reactions.h\"\n\n#include \"api/api_global_privacy.h\"\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_messages.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"core/application.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"main/main_session.h\"\n#include \"main/main_app_config.h\"\n#include \"main/session/send_as_peers.h\"\n#include \"data/components/credits.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"storage/localimageloader.h\"\n#include \"ui/image/image_location_factory.h\"\n#include \"ui/animated_icon.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/call_delayed.h\"\n#include \"base/unixtime.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_chat.h\"\n\n#include \"base/random.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kRefreshFullListEach = 60 * 60 * crl::time(1000);\nconstexpr auto kPollEach = 20 * crl::time(1000);\nconstexpr auto kSizeForDownscale = 64;\nconstexpr auto kRecentRequestTimeout = 10 * crl::time(1000);\nconstexpr auto kRecentReactionsLimit = 40;\nconstexpr auto kMyTagsRequestTimeout = crl::time(1000);\nconstexpr auto kTopRequestDelay = 60 * crl::time(1000);\nconstexpr auto kTopReactionsLimit = 14;\nconstexpr auto kPaidAccumulatePeriod = 5 * crl::time(1000) + 500;\n\n[[nodiscard]] QString ReactionIdToLog(const ReactionId &id) {\n\tif (const auto custom = id.custom()) {\n\t\treturn \"custom:\" + QString::number(custom);\n\t}\n\treturn id.emoji();\n}\n\n[[nodiscard]] std::vector<ReactionId> ListFromMTP(\n\t\tconst MTPDmessages_reactions &data) {\n\tconst auto &list = data.vreactions().v;\n\tauto result = std::vector<ReactionId>();\n\tresult.reserve(list.size());\n\tfor (const auto &reaction : list) {\n\t\tconst auto id = ReactionFromMTP(reaction);\n\t\tif (id.empty()) {\n\t\t\tLOG((\"API Error: reactionEmpty in messages.reactions.\"));\n\t\t} else {\n\t\t\tresult.push_back(id);\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] std::vector<MyTagInfo> ListFromMTP(\n\t\tconst MTPDmessages_savedReactionTags &data) {\n\tconst auto &list = data.vtags().v;\n\tauto result = std::vector<MyTagInfo>();\n\tresult.reserve(list.size());\n\tfor (const auto &reaction : list) {\n\t\tconst auto &data = reaction.data();\n\t\tconst auto id = ReactionFromMTP(data.vreaction());\n\t\tif (id.empty()) {\n\t\t\tLOG((\"API Error: reactionEmpty in messages.reactions.\"));\n\t\t} else {\n\t\t\tresult.push_back({\n\t\t\t\t.id = id,\n\t\t\t\t.title = qs(data.vtitle().value_or_empty()),\n\t\t\t\t.count = data.vcount().v,\n\t\t\t});\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] Reaction CustomReaction(not_null<DocumentData*> document) {\n\treturn Reaction{\n\t\t.id = { { document->id } },\n\t\t.title = \"Custom reaction\",\n\t\t.appearAnimation = document,\n\t\t.selectAnimation = document,\n\t\t.centerIcon = document,\n\t\t.active = true,\n\t};\n}\n\n[[nodiscard]] int SentReactionsLimit(not_null<HistoryItem*> item) {\n\tconst auto session = &item->history()->session();\n\tconst auto config = &session->appConfig();\n\treturn session->premium()\n\t\t? config->get<int>(\"reactions_user_max_premium\", 3)\n\t\t: config->get<int>(\"reactions_user_max_default\", 1);\n}\n\n[[nodiscard]] bool IsMyRecent(\n\t\tconst MTPDmessagePeerReaction &data,\n\t\tconst ReactionId &id,\n\t\tnot_null<PeerData*> peer,\n\t\tconst base::flat_map<\n\t\t\tReactionId,\n\t\t\tstd::vector<RecentReaction>> &recent,\n\t\tbool min) {\n\tif (peer->isSelf()) {\n\t\treturn true;\n\t} else if (!min) {\n\t\treturn data.is_my();\n\t}\n\tconst auto j = recent.find(id);\n\tif (j == end(recent)) {\n\t\treturn false;\n\t}\n\tconst auto k = ranges::find(\n\t\tj->second,\n\t\tpeer,\n\t\t&RecentReaction::peer);\n\treturn (k != end(j->second)) && k->my;\n}\n\n[[nodiscard]] bool IsMyTop(\n\t\tconst MTPDmessageReactor &data,\n\t\tPeerData *peer,\n\t\tconst std::vector<MessageReactionsTopPaid> &top,\n\t\tbool min) {\n\tif (peer && peer->isSelf()) {\n\t\treturn true;\n\t} else if (!min) {\n\t\treturn data.is_my();\n\t}\n\tconst auto i = ranges::find(top, peer, &MessageReactionsTopPaid::peer);\n\treturn (i != end(top)) && i->my;\n}\n\n[[nodiscard]] std::optional<PeerId> MaybeShownPeer(\n\t\tuint32 privacySet,\n\t\tPeerId shownPeer) {\n\treturn privacySet ? shownPeer : std::optional<PeerId>();\n}\n\n[[nodiscard]] MTPPaidReactionPrivacy PaidReactionShownPeerToTL(\n\t\tnot_null<Main::Session*> session,\n\t\tstd::optional<PeerId> shownPeer) {\n\treturn !shownPeer\n\t\t? MTPPaidReactionPrivacy()\n\t\t: !*shownPeer\n\t\t? MTP_paidReactionPrivacyAnonymous()\n\t\t: (*shownPeer == session->userPeerId())\n\t\t? MTP_paidReactionPrivacyDefault()\n\t\t: MTP_paidReactionPrivacyPeer(\n\t\t\tsession->data().peer(*shownPeer)->input());\n}\n\n} // namespace\n\nPossibleItemReactionsRef LookupPossibleReactions(\n\t\tnot_null<HistoryItem*> item,\n\t\tbool paidInFront) {\n\tif (!item->canReact()) {\n\t\treturn {};\n\t}\n\tauto result = PossibleItemReactionsRef();\n\tauto peer = item->history()->peer;\n\tif (item->isDiscussionPost()) {\n\t\tif (const auto forwarded = item->Get<HistoryMessageForwarded>()) {\n\t\t\tif (forwarded->savedFromPeer) {\n\t\t\t\tpeer = forwarded->savedFromPeer;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto session = &peer->session();\n\tif (const auto channel = peer->asChannel()) {\n\t\tif ((!channel->amCreator())\n\t\t\t&& (channel->adminRights() & ChatAdminRight::Anonymous)\n\t\t\t&& (session->sendAsPeers().resolveChosen(channel) == channel)) {\n\t\t\treturn {};\n\t\t}\n\t}\n\tconst auto reactions = &session->data().reactions();\n\tconst auto &full = reactions->list(Reactions::Type::Active);\n\tconst auto &top = reactions->list(Reactions::Type::Top);\n\tconst auto &recent = reactions->list(Reactions::Type::Recent);\n\tconst auto &myTags = reactions->list(Reactions::Type::MyTags);\n\tconst auto &tags = reactions->list(Reactions::Type::Tags);\n\tconst auto &all = item->reactions();\n\tconst auto &allowed = PeerAllowedReactions(peer);\n\tconst auto limit = UniqueReactionsLimit(peer);\n\tconst auto premiumPossible = session->premiumPossible();\n\tconst auto limited = (all.size() >= limit) && [&] {\n\t\tconst auto my = item->chosenReactions();\n\t\tif (my.empty()) {\n\t\t\treturn true;\n\t\t}\n\t\treturn true; // #TODO reactions\n\t}();\n\tauto added = base::flat_set<ReactionId>();\n\tconst auto add = [&](auto predicate) {\n\t\tauto &&all = ranges::views::concat(top, recent, full);\n\t\tfor (const auto &reaction : all) {\n\t\t\tif (predicate(reaction)) {\n\t\t\t\tif (added.emplace(reaction.id).second) {\n\t\t\t\t\tresult.recent.push_back(&reaction);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\treactions->clearTemporary();\n\tif (item->reactionsAreTags()) {\n\t\tauto &&all = ranges::views::concat(myTags, tags);\n\t\tresult.recent.reserve(myTags.size() + tags.size());\n\t\tfor (const auto &reaction : all) {\n\t\t\tif (premiumPossible\n\t\t\t\t|| ranges::contains(tags, reaction.id, &Reaction::id)) {\n\t\t\t\tif (added.emplace(reaction.id).second) {\n\t\t\t\t\tresult.recent.push_back(&reaction);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tresult.customAllowed = premiumPossible;\n\t\tresult.tags = true;\n\t} else if (limited) {\n\t\tresult.recent.reserve((allowed.paidEnabled ? 1 : 0) + all.size());\n\t\tadd([&](const Reaction &reaction) {\n\t\t\treturn ranges::contains(all, reaction.id, &MessageReaction::id);\n\t\t});\n\t\tfor (const auto &reaction : all) {\n\t\t\tconst auto id = reaction.id;\n\t\t\tif (added.emplace(id).second) {\n\t\t\t\tif (const auto temp = reactions->lookupTemporary(id)) {\n\t\t\t\t\tresult.recent.push_back(temp);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (allowed.paidEnabled\n\t\t\t&& !added.contains(ReactionId::Paid())) {\n\t\t\tresult.recent.push_back(reactions->lookupPaid());\n\t\t}\n\t} else {\n\t\tresult.recent.reserve((allowed.paidEnabled ? 1 : 0)\n\t\t\t+ ((allowed.type == AllowedReactionsType::Some)\n\t\t\t\t? allowed.some.size()\n\t\t\t\t: full.size()));\n\t\tif (allowed.paidEnabled) {\n\t\t\tresult.recent.push_back(reactions->lookupPaid());\n\t\t}\n\t\tadd([&](const Reaction &reaction) {\n\t\t\tconst auto id = reaction.id;\n\t\t\tif (id.custom() && !premiumPossible) {\n\t\t\t\treturn false;\n\t\t\t} else if ((allowed.type == AllowedReactionsType::Some)\n\t\t\t\t&& !ranges::contains(allowed.some, id)) {\n\t\t\t\treturn false;\n\t\t\t} else if (id.custom()\n\t\t\t\t&& allowed.type == AllowedReactionsType::Default) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\t\tif (allowed.type == AllowedReactionsType::Some) {\n\t\t\tfor (const auto &id : allowed.some) {\n\t\t\t\tif (!added.contains(id)) {\n\t\t\t\t\tif (const auto temp = reactions->lookupTemporary(id)) {\n\t\t\t\t\t\tresult.recent.push_back(temp);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tresult.customAllowed = (allowed.type == AllowedReactionsType::All)\n\t\t\t&& premiumPossible;\n\n\t\tconst auto favoriteId = reactions->favoriteId();\n\t\tif (favoriteId.custom()\n\t\t\t&& result.customAllowed\n\t\t\t&& !ranges::contains(result.recent, favoriteId, &Reaction::id)) {\n\t\t\tif (const auto temp = reactions->lookupTemporary(favoriteId)) {\n\t\t\t\tresult.recent.insert(begin(result.recent), temp);\n\t\t\t}\n\t\t}\n\t}\n\tif (!item->reactionsAreTags()) {\n\t\tconst auto toFront = [&](ReactionId id) {\n\t\t\tconst auto i = ranges::find(result.recent, id, &Reaction::id);\n\t\t\tif (i != end(result.recent) && i != begin(result.recent)) {\n\t\t\t\tstd::rotate(begin(result.recent), i, i + 1);\n\t\t\t}\n\t\t};\n\t\ttoFront(reactions->favoriteId());\n\t\tif (paidInFront) {\n\t\t\ttoFront(ReactionId::Paid());\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions(\n\t\tnot_null<Main::Session*> session) {\n\tauto result = PossibleItemReactionsRef();\n\tconst auto reactions = &session->data().reactions();\n\tconst auto &full = reactions->list(Reactions::Type::Active);\n\tconst auto &top = reactions->list(Reactions::Type::Top);\n\tconst auto &recent = reactions->list(Reactions::Type::Recent);\n\tconst auto premiumPossible = session->premiumPossible();\n\tauto added = base::flat_set<ReactionId>();\n\tresult.recent.reserve(full.size());\n\tfor (const auto &reaction : ranges::views::concat(top, recent, full)) {\n\t\tif (premiumPossible || !reaction.id.custom()) {\n\t\t\tif (added.emplace(reaction.id).second) {\n\t\t\t\tresult.recent.push_back(&reaction);\n\t\t\t}\n\t\t}\n\t}\n\tresult.customAllowed = premiumPossible;\n\tconst auto i = ranges::find(\n\t\tresult.recent,\n\t\treactions->favoriteId(),\n\t\t&Reaction::id);\n\tif (i != end(result.recent) && i != begin(result.recent)) {\n\t\tstd::rotate(begin(result.recent), i, i + 1);\n\t}\n\treturn result;\n}\n\nPossibleItemReactions::PossibleItemReactions(\n\tconst PossibleItemReactionsRef &other)\n: recent(other.recent | ranges::views::transform([](const auto &value) {\n\treturn *value;\n}) | ranges::to_vector)\n, stickers(other.stickers | ranges::views::transform([](const auto &value) {\n\treturn *value;\n}) | ranges::to_vector)\n, customAllowed(other.customAllowed)\n, tags(other.tags){\n}\n\nReactions::Reactions(not_null<Session*> owner)\n: _owner(owner)\n, _topRefreshTimer([=] { refreshTop(); })\n, _repaintTimer([=] { repaintCollected(); })\n, _sendPaidTimer([=] { sendPaid(); }) {\n\trefreshDefault();\n\n\t_myTags.emplace(nullptr);\n\n\tbase::timer_each(\n\t\tkRefreshFullListEach\n\t) | rpl::on_next([=] {\n\t\trefreshDefault();\n\t\trequestEffects();\n\t}, _lifetime);\n\n\t_owner->session().changes().messageUpdates(\n\t\tMessageUpdate::Flag::Destroyed\n\t) | rpl::on_next([=](const MessageUpdate &update) {\n\t\tconst auto item = update.item;\n\t\t_pollingItems.remove(item);\n\t\t_pollItems.remove(item);\n\t\t_repaintItems.remove(item);\n\t\t_sendPaidItems.remove(item);\n\t\tif (const auto i = _sendingPaid.find(item)\n\t\t\t; i != end(_sendingPaid)) {\n\t\t\t_sendingPaid.erase(i);\n\t\t\t_owner->session().credits().invalidate();\n\t\t\tcrl::on_main(&_owner->session(), [=] {\n\t\t\t\tsendPaid();\n\t\t\t});\n\t\t}\n\t}, _lifetime);\n\n\tcrl::on_main(&owner->session(), [=] {\n\t\t// applyFavorite accesses not yet constructed parts of session.\n\t\trpl::single(rpl::empty) | rpl::then(\n\t\t\t_owner->session().mtp().config().updates()\n\t\t) | rpl::map([=] {\n\t\t\tconst auto &config = _owner->session().mtp().configValues();\n\t\t\treturn config.reactionDefaultCustom\n\t\t\t\t? ReactionId{ DocumentId(config.reactionDefaultCustom) }\n\t\t\t\t: ReactionId{ config.reactionDefaultEmoji };\n\t\t}) | rpl::filter([=](const ReactionId &id) {\n\t\t\treturn !_saveFaveRequestId;\n\t\t}) | rpl::on_next([=](ReactionId &&id) {\n\t\t\tapplyFavorite(id);\n\t\t}, _lifetime);\n\t});\n}\n\nReactions::~Reactions() = default;\n\nMain::Session &Reactions::session() const {\n\treturn _owner->session();\n}\n\nvoid Reactions::refreshTop() {\n\trequestTop();\n}\n\nvoid Reactions::refreshRecent() {\n\trequestRecent();\n}\n\nvoid Reactions::refreshRecentDelayed() {\n\tif (_recentRequestId || _recentRequestScheduled) {\n\t\treturn;\n\t}\n\t_recentRequestScheduled = true;\n\tbase::call_delayed(kRecentRequestTimeout, &_owner->session(), [=] {\n\t\tif (_recentRequestScheduled) {\n\t\t\trequestRecent();\n\t\t}\n\t});\n}\n\nvoid Reactions::refreshDefault() {\n\trequestDefault();\n}\n\nvoid Reactions::refreshMyTags(SavedSublist *sublist) {\n\trequestMyTags(sublist);\n}\n\nvoid Reactions::refreshMyTagsDelayed() {\n\tauto &my = _myTags[nullptr];\n\tif (my.requestId || my.requestScheduled) {\n\t\treturn;\n\t}\n\tmy.requestScheduled = true;\n\tbase::call_delayed(kMyTagsRequestTimeout, &_owner->session(), [=] {\n\t\tif (_myTags[nullptr].requestScheduled) {\n\t\t\trequestMyTags();\n\t\t}\n\t});\n}\n\nvoid Reactions::refreshTags() {\n\trequestTags();\n}\n\nvoid Reactions::refreshEffects() {\n\tif (_effects.empty()) {\n\t\trequestEffects();\n\t}\n}\n\nconst std::vector<Reaction> &Reactions::list(Type type) const {\n\tswitch (type) {\n\tcase Type::Active: return _active;\n\tcase Type::Recent: return _recent;\n\tcase Type::Top: return _top;\n\tcase Type::All: return _available;\n\tcase Type::MyTags:\n\t\treturn _myTags.find((SavedSublist*)nullptr)->second.tags;\n\tcase Type::Tags: return _tags;\n\tcase Type::Effects: return _effects;\n\t}\n\tUnexpected(\"Type in Reactions::list.\");\n}\n\nconst std::vector<MyTagInfo> &Reactions::myTagsInfo() const {\n\treturn _myTags.find((SavedSublist*)nullptr)->second.info;\n}\n\nconst QString &Reactions::myTagTitle(const ReactionId &id) const {\n\tconst auto i = _myTags.find((SavedSublist*)nullptr);\n\tif (i != end(_myTags)) {\n\t\tconst auto j = ranges::find(i->second.info, id, &MyTagInfo::id);\n\t\tif (j != end(i->second.info)) {\n\t\t\treturn j->title;\n\t\t}\n\t}\n\tstatic const auto kEmpty = QString();\n\treturn kEmpty;\n}\n\nReactionId Reactions::favoriteId() const {\n\treturn _favoriteId;\n}\n\nconst Reaction *Reactions::favorite() const {\n\treturn _favorite ? &*_favorite : nullptr;\n}\n\nvoid Reactions::setFavorite(const ReactionId &id) {\n\tconst auto api = &_owner->session().api();\n\tif (_saveFaveRequestId) {\n\t\tapi->request(_saveFaveRequestId).cancel();\n\t}\n\t_saveFaveRequestId = api->request(MTPmessages_SetDefaultReaction(\n\t\tReactionToMTP(id)\n\t)).done([=] {\n\t\t_saveFaveRequestId = 0;\n\t}).fail([=] {\n\t\t_saveFaveRequestId = 0;\n\t}).send();\n\n\tapplyFavorite(id);\n}\n\nvoid Reactions::incrementMyTag(const ReactionId &id, SavedSublist *sublist) {\n\tif (sublist) {\n\t\tincrementMyTag(id, nullptr);\n\t}\n\tauto &my = _myTags[sublist];\n\tauto i = ranges::find(my.info, id, &MyTagInfo::id);\n\tif (i == end(my.info)) {\n\t\tmy.info.push_back({ .id = id, .count = 0 });\n\t\ti = end(my.info) - 1;\n\t}\n\t++i->count;\n\twhile (i != begin(my.info)) {\n\t\tauto j = i - 1;\n\t\tif (j->count >= i->count) {\n\t\t\tbreak;\n\t\t}\n\t\tstd::swap(*i, *j);\n\t\ti = j;\n\t}\n\tscheduleMyTagsUpdate(sublist);\n}\n\nvoid Reactions::decrementMyTag(const ReactionId &id, SavedSublist *sublist) {\n\tif (sublist) {\n\t\tdecrementMyTag(id, nullptr);\n\t}\n\tauto &my = _myTags[sublist];\n\tauto i = ranges::find(my.info, id, &MyTagInfo::id);\n\tif (i != end(my.info) && i->count > 0) {\n\t\t--i->count;\n\t\twhile (i + 1 != end(my.info)) {\n\t\t\tauto j = i + 1;\n\t\t\tif (j->count <= i->count) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tstd::swap(*i, *j);\n\t\t\ti = j;\n\t\t}\n\t}\n\tscheduleMyTagsUpdate(sublist);\n}\n\nvoid Reactions::renameTag(const ReactionId &id, const QString &name) {\n\tauto changed = false;\n\tfor (auto &[sublist, my] : _myTags) {\n\t\tauto i = ranges::find(my.info, id, &MyTagInfo::id);\n\t\tif (i == end(my.info) || i->title == name) {\n\t\t\tcontinue;\n\t\t}\n\t\ti->title = name;\n\t\tchanged = true;\n\t\tscheduleMyTagsUpdate(sublist);\n\t}\n\tif (!changed) {\n\t\treturn;\n\t}\n\t_myTagRenamed.fire_copy(id);\n\n\tusing Flag = MTPmessages_UpdateSavedReactionTag::Flag;\n\t_owner->session().api().request(MTPmessages_UpdateSavedReactionTag(\n\t\tMTP_flags(name.isEmpty() ? Flag(0) : Flag::f_title),\n\t\tReactionToMTP(id),\n\t\tMTP_string(name)\n\t)).send();\n}\n\nvoid Reactions::scheduleMyTagsUpdate(SavedSublist *sublist) {\n\tauto &my = _myTags[sublist];\n\tmy.updateScheduled = true;\n\tcrl::on_main(&session(), [=] {\n\t\tauto &my = _myTags[sublist];\n\t\tif (!my.updateScheduled) {\n\t\t\treturn;\n\t\t}\n\t\tmy.updateScheduled = false;\n\t\tmy.tags = resolveByInfos(my.info, _unresolvedMyTags, sublist);\n\t\t_myTagsUpdated.fire_copy(sublist);\n\t});\n}\n\nDocumentData *Reactions::chooseGenericAnimation(\n\t\tnot_null<DocumentData*> custom) const {\n\tconst auto sticker = custom->sticker();\n\tconst auto i = sticker\n\t\t? ranges::find(\n\t\t\t_available,\n\t\t\tReactionId{ { sticker->alt } },\n\t\t\t&Reaction::id)\n\t\t: end(_available);\n\tif (i != end(_available) && i->aroundAnimation) {\n\t\tconst auto view = i->aroundAnimation->createMediaView();\n\t\tview->checkStickerLarge();\n\t\tif (view->loaded()) {\n\t\t\treturn i->aroundAnimation;\n\t\t}\n\t}\n\treturn randomLoadedFrom(_genericAnimations);\n}\n\nvoid Reactions::fillPaidReactionAnimations() const {\n\tconst auto generate = [&](int index) {\n\t\tconst auto session = &_owner->session();\n\t\tconst auto name = u\"star_reaction_effect%1\"_q.arg(index + 1);\n\t\treturn ChatHelpers::GenerateLocalTgsSticker(session, name);\n\t};\n\tconst auto kCount = 3;\n\tfor (auto i = 0; i != kCount; ++i) {\n\t\tconst auto document = generate(i);\n\t\t_paidReactionAnimations.push_back(document);\n\t\t_paidReactionCache.emplace(\n\t\t\tdocument,\n\t\t\tdocument->createMediaView());\n\t}\n\t_paidReactionCache.front().second->checkStickerLarge();\n}\n\nDocumentData *Reactions::choosePaidReactionAnimation() const {\n\tif (_paidReactionAnimations.empty()) {\n\t\tfillPaidReactionAnimations();\n\t}\n\treturn randomLoadedFrom(_paidReactionAnimations);\n}\n\nDocumentData *Reactions::randomLoadedFrom(\n\t\tstd::vector<not_null<DocumentData*>> list) const {\n\tif (list.empty()) {\n\t\treturn nullptr;\n\t}\n\tranges::shuffle(list);\n\tconst auto first = list.front();\n\tconst auto view = first->createMediaView();\n\tview->checkStickerLarge();\n\tif (view->loaded()) {\n\t\treturn first;\n\t}\n\tconst auto k = ranges::find_if(list, [&](not_null<DocumentData*> value) {\n\t\treturn value->createMediaView()->loaded();\n\t});\n\treturn (k != end(list)) ? (*k) : first;\n}\n\nvoid Reactions::applyFavorite(const ReactionId &id) {\n\tif (_favoriteId != id) {\n\t\t_favoriteId = id;\n\t\t_favorite = resolveById(_favoriteId);\n\t\tif (!_favorite && _unresolvedFavoriteId != _favoriteId) {\n\t\t\t_unresolvedFavoriteId = _favoriteId;\n\t\t\tresolve(_favoriteId);\n\t\t}\n\t\t_favoriteUpdated.fire({});\n\t}\n}\n\nrpl::producer<> Reactions::topUpdates() const {\n\treturn _topUpdated.events();\n}\n\nrpl::producer<> Reactions::recentUpdates() const {\n\treturn _recentUpdated.events();\n}\n\nrpl::producer<> Reactions::defaultUpdates() const {\n\treturn _defaultUpdated.events();\n}\n\nrpl::producer<> Reactions::favoriteUpdates() const {\n\treturn _favoriteUpdated.events();\n}\n\nrpl::producer<> Reactions::myTagsUpdates() const {\n\treturn _myTagsUpdated.events(\n\t) | rpl::filter(\n\t\t!rpl::mappers::_1\n\t) | rpl::to_empty;\n}\n\nrpl::producer<> Reactions::tagsUpdates() const {\n\treturn _tagsUpdated.events();\n}\n\nrpl::producer<ReactionId> Reactions::myTagRenamed() const {\n\treturn _myTagRenamed.events();\n}\n\nrpl::producer<> Reactions::effectsUpdates() const {\n\treturn _effectsUpdated.events();\n}\n\nvoid Reactions::preloadReactionImageFor(const ReactionId &emoji) {\n\tif (emoji.paid() || !emoji.emoji().isEmpty()) {\n\t\tpreloadImageFor(emoji);\n\t}\n}\n\nvoid Reactions::preloadEffectImageFor(EffectId id) {\n\tif (id != kFakeEffectId) {\n\t\tpreloadImageFor({ DocumentId(id) });\n\t}\n}\n\nvoid Reactions::preloadImageFor(const ReactionId &id) {\n\tif (_images.contains(id)) {\n\t\treturn;\n\t}\n\tauto &set = _images.emplace(id).first->second;\n\tset.effect = (id.custom() != 0);\n\tif (id.paid()) {\n\t\tloadImage(set, lookupPaid()->centerIcon, true);\n\t\treturn;\n\t}\n\tauto &list = set.effect ? _effects : _available;\n\tconst auto i = ranges::find(list, id, &Reaction::id);\n\tconst auto document = (i == end(list))\n\t\t? nullptr\n\t\t: i->centerIcon\n\t\t? i->centerIcon\n\t\t: i->selectAnimation.get();\n\tif (document || (set.effect && i != end(list))) {\n\t\tif (!set.effect || i->centerIcon) {\n\t\t\tloadImage(set, document, !i->centerIcon);\n\t\t} else {\n\t\t\tgenerateImage(set, i->title);\n\t\t}\n\t\tif (set.effect) {\n\t\t\tpreloadEffect(*i);\n\t\t}\n\t} else if (set.effect && !_waitingForEffects) {\n\t\t_waitingForEffects = true;\n\t\trefreshEffects();\n\t} else if (!set.effect && !_waitingForReactions) {\n\t\t_waitingForReactions = true;\n\t\trefreshDefault();\n\t}\n}\n\nvoid Reactions::preloadEffect(const Reaction &effect) {\n\tif (effect.aroundAnimation) {\n\t\teffect.aroundAnimation->createMediaView()->checkStickerLarge();\n\t} else {\n\t\tconst auto premium = effect.selectAnimation;\n\t\tpremium->loadVideoThumbnail(premium->stickerSetOrigin());\n\t}\n}\n\nvoid Reactions::preloadAnimationsFor(const ReactionId &id) {\n\tconst auto preload = [&](DocumentData *document) {\n\t\tconst auto view = document\n\t\t\t? document->activeMediaView()\n\t\t\t: nullptr;\n\t\tif (view) {\n\t\t\tview->checkStickerLarge();\n\t\t}\n\t};\n\tif (id.paid()) {\n\t\tconst auto fake = lookupPaid();\n\t\tpreload(fake->centerIcon);\n\t\tpreload(fake->aroundAnimation);\n\t\treturn;\n\t}\n\tconst auto custom = id.custom();\n\tconst auto document = custom ? _owner->document(custom).get() : nullptr;\n\tconst auto customSticker = document ? document->sticker() : nullptr;\n\tconst auto findId = custom\n\t\t? ReactionId{ { customSticker ? customSticker->alt : QString() } }\n\t\t: id;\n\tconst auto i = ranges::find(_available, findId, &Reaction::id);\n\tif (i == end(_available)) {\n\t\treturn;\n\t}\n\tif (!custom) {\n\t\tpreload(i->centerIcon);\n\t}\n\tpreload(i->aroundAnimation);\n}\n\nQImage Reactions::resolveReactionImageFor(const ReactionId &emoji) {\n\tExpects(!emoji.custom());\n\n\treturn resolveImageFor(emoji);\n}\n\nQImage Reactions::resolveEffectImageFor(EffectId id) {\n\treturn (id == kFakeEffectId)\n\t\t? QImage()\n\t\t: resolveImageFor({ DocumentId(id) });\n}\n\nQImage Reactions::resolveImageFor(const ReactionId &id) {\n\tauto i = _images.find(id);\n\tif (i == end(_images)) {\n\t\tpreloadImageFor(id);\n\t\ti = _images.find(id);\n\t\tAssert(i != end(_images));\n\t}\n\tauto &set = i->second;\n\tset.effect = (id.custom() != 0);\n\n\tconst auto resolve = [&](QImage &image, int size) {\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tconst auto frameSize = set.fromSelectAnimation\n\t\t\t? (size / 2)\n\t\t\t: size;\n\t\t// Must not be colored to text.\n\t\timage = set.icon->frame(QColor()).scaled(\n\t\t\tframeSize * factor,\n\t\t\tframeSize * factor,\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t\tif (set.fromSelectAnimation) {\n\t\t\tauto result = QImage(\n\t\t\t\tsize * factor,\n\t\t\t\tsize * factor,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tresult.fill(Qt::transparent);\n\n\t\t\tauto p = QPainter(&result);\n\t\t\tp.drawImage(\n\t\t\t\t(size - frameSize) * factor / 2,\n\t\t\t\t(size - frameSize) * factor / 2,\n\t\t\t\timage);\n\t\t\tp.end();\n\n\t\t\tstd::swap(result, image);\n\t\t}\n\t\timage.setDevicePixelRatio(factor);\n\t};\n\tif (set.image.isNull() && set.icon) {\n\t\tresolve(\n\t\t\tset.image,\n\t\t\tset.effect ? st::effectInfoImage : st::reactionInlineImage);\n\t\tcrl::async([icon = std::move(set.icon)]{});\n\t}\n\treturn set.image;\n}\n\nvoid Reactions::resolveReactionImages() {\n\tfor (auto &[id, set] : _images) {\n\t\tif (set.effect || !set.image.isNull() || set.icon || set.media) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto i = ranges::find(_available, id, &Reaction::id);\n\t\tconst auto document = (i == end(_available))\n\t\t\t? nullptr\n\t\t\t: i->centerIcon\n\t\t\t? i->centerIcon\n\t\t\t: i->selectAnimation.get();\n\t\tif (document) {\n\t\t\tloadImage(set, document, !i->centerIcon);\n\t\t} else {\n\t\t\tLOG((\"API Error: Reaction '%1' not found!\"\n\t\t\t\t).arg(ReactionIdToLog(id)));\n\t\t}\n\t}\n}\n\nvoid Reactions::resolveEffectImages() {\n\tfor (auto &[id, set] : _images) {\n\t\tif (!set.effect || !set.image.isNull() || set.icon || set.media) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto i = ranges::find(_effects, id, &Reaction::id);\n\t\tconst auto document = (i == end(_effects))\n\t\t\t? nullptr\n\t\t\t: i->centerIcon\n\t\t\t? i->centerIcon\n\t\t\t: nullptr;\n\t\tif (document) {\n\t\t\tloadImage(set, document, false);\n\t\t} else if (i != end(_effects)) {\n\t\t\tgenerateImage(set, i->title);\n\t\t} else {\n\t\t\tLOG((\"API Error: Effect '%1' not found!\"\n\t\t\t\t).arg(ReactionIdToLog(id)));\n\t\t}\n\t\tif (i != end(_effects)) {\n\t\t\tpreloadEffect(*i);\n\t\t}\n\t}\n}\n\nvoid Reactions::loadImage(\n\t\tImageSet &set,\n\t\tnot_null<DocumentData*> document,\n\t\tbool fromSelectAnimation) {\n\tif (!set.image.isNull() || set.icon) {\n\t\treturn;\n\t} else if (!set.media) {\n\t\tif (!set.effect) {\n\t\t\tset.fromSelectAnimation = fromSelectAnimation;\n\t\t}\n\t\tset.media = document->createMediaView();\n\t\tset.media->checkStickerLarge();\n\t}\n\tif (set.media->loaded()) {\n\t\tsetAnimatedIcon(set);\n\t} else if (!_imagesLoadLifetime) {\n\t\tdocument->session().downloaderTaskFinished(\n\t\t) | rpl::on_next([=] {\n\t\t\tdownloadTaskFinished();\n\t\t}, _imagesLoadLifetime);\n\t}\n}\n\nvoid Reactions::generateImage(ImageSet &set, const QString &emoji) {\n\tExpects(set.effect);\n\n\tconst auto e = Ui::Emoji::Find(emoji);\n\tAssert(e != nullptr);\n\n\tconst auto large = Ui::Emoji::GetSizeLarge();\n\tconst auto factor = style::DevicePixelRatio();\n\tauto image = QImage(large, large, QImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(factor);\n\timage.fill(Qt::transparent);\n\t{\n\t\tQPainter p(&image);\n\t\tUi::Emoji::Draw(p, e, large, 0, 0);\n\t}\n\tconst auto size = st::effectInfoImage;\n\tset.image = image.scaled(size * factor, size * factor);\n\tset.image.setDevicePixelRatio(factor);\n}\n\nvoid Reactions::setAnimatedIcon(ImageSet &set) {\n\tconst auto size = style::ConvertScale(kSizeForDownscale);\n\tset.icon = Ui::MakeAnimatedIcon({\n\t\t.generator = DocumentIconFrameGenerator(set.media),\n\t\t.sizeOverride = QSize(size, size),\n\t\t.colorized = set.media->owner()->emojiUsesTextColor(),\n\t});\n\tset.media = nullptr;\n}\n\nvoid Reactions::downloadTaskFinished() {\n\tauto hasOne = false;\n\tfor (auto &[emoji, set] : _images) {\n\t\tif (!set.media) {\n\t\t\tcontinue;\n\t\t} else if (set.media->loaded()) {\n\t\t\tsetAnimatedIcon(set);\n\t\t} else {\n\t\t\thasOne = true;\n\t\t}\n\t}\n\tif (!hasOne) {\n\t\t_imagesLoadLifetime.destroy();\n\t}\n}\n\nvoid Reactions::requestTop() {\n\tif (_topRequestId) {\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\t_topRefreshTimer.cancel();\n\t_topRequestId = api.request(MTPmessages_GetTopReactions(\n\t\tMTP_int(kTopReactionsLimit),\n\t\tMTP_long(_topHash)\n\t)).done([=](const MTPmessages_Reactions &result) {\n\t\t_topRequestId = 0;\n\t\tresult.match([&](const MTPDmessages_reactions &data) {\n\t\t\tupdateTop(data);\n\t\t}, [](const MTPDmessages_reactionsNotModified&) {\n\t\t});\n\t}).fail([=] {\n\t\t_topRequestId = 0;\n\t\t_topHash = 0;\n\t}).send();\n}\n\nvoid Reactions::requestRecent() {\n\tif (_recentRequestId) {\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\t_recentRequestScheduled = false;\n\t_recentRequestId = api.request(MTPmessages_GetRecentReactions(\n\t\tMTP_int(kRecentReactionsLimit),\n\t\tMTP_long(_recentHash)\n\t)).done([=](const MTPmessages_Reactions &result) {\n\t\t_recentRequestId = 0;\n\t\tresult.match([&](const MTPDmessages_reactions &data) {\n\t\t\tupdateRecent(data);\n\t\t}, [](const MTPDmessages_reactionsNotModified&) {\n\t\t});\n\t}).fail([=] {\n\t\t_recentRequestId = 0;\n\t\t_recentHash = 0;\n\t}).send();\n}\n\nvoid Reactions::requestDefault() {\n\tif (_defaultRequestId) {\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\t_defaultRequestId = api.request(MTPmessages_GetAvailableReactions(\n\t\tMTP_int(_defaultHash)\n\t)).done([=](const MTPmessages_AvailableReactions &result) {\n\t\t_defaultRequestId = 0;\n\t\tresult.match([&](const MTPDmessages_availableReactions &data) {\n\t\t\tupdateDefault(data);\n\t\t}, [&](const MTPDmessages_availableReactionsNotModified &) {\n\t\t});\n\t}).fail([=] {\n\t\t_defaultRequestId = 0;\n\t\t_defaultHash = 0;\n\t}).send();\n}\n\nvoid Reactions::requestGeneric() {\n\tif (_genericRequestId) {\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\t_genericRequestId = api.request(MTPmessages_GetStickerSet(\n\t\tMTP_inputStickerSetEmojiGenericAnimations(),\n\t\tMTP_int(0) // hash\n\t)).done([=](const MTPmessages_StickerSet &result) {\n\t\t_genericRequestId = 0;\n\t\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\tupdateGeneric(data);\n\t\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t\t});\n\t}).fail([=] {\n\t\t_genericRequestId = 0;\n\t}).send();\n}\n\nvoid Reactions::requestMyTags(SavedSublist *sublist) {\n\tauto &my = _myTags[sublist];\n\tif (my.requestId) {\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\tmy.requestScheduled = false;\n\tusing Flag = MTPmessages_GetSavedReactionTags::Flag;\n\tmy.requestId = api.request(MTPmessages_GetSavedReactionTags(\n\t\tMTP_flags(sublist ? Flag::f_peer : Flag()),\n\t\t(sublist ? sublist->sublistPeer()->input() : MTP_inputPeerEmpty()),\n\t\tMTP_long(my.hash)\n\t)).done([=](const MTPmessages_SavedReactionTags &result) {\n\t\tauto &my = _myTags[sublist];\n\t\tmy.requestId = 0;\n\t\tresult.match([&](const MTPDmessages_savedReactionTags &data) {\n\t\t\tupdateMyTags(sublist, data);\n\t\t}, [](const MTPDmessages_savedReactionTagsNotModified&) {\n\t\t});\n\t}).fail([=] {\n\t\tauto &my = _myTags[sublist];\n\t\tmy.requestId = 0;\n\t\tmy.hash = 0;\n\t}).send();\n}\n\nvoid Reactions::requestTags() {\n\tif (_tagsRequestId) {\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\t_tagsRequestId = api.request(MTPmessages_GetDefaultTagReactions(\n\t\tMTP_long(_tagsHash)\n\t)).done([=](const MTPmessages_Reactions &result) {\n\t\t_tagsRequestId = 0;\n\t\tresult.match([&](const MTPDmessages_reactions &data) {\n\t\t\tupdateTags(data);\n\t\t}, [](const MTPDmessages_reactionsNotModified&) {\n\t\t});\n\t}).fail([=] {\n\t\t_tagsRequestId = 0;\n\t\t_tagsHash = 0;\n\t}).send();\n\n}\n\nvoid Reactions::requestEffects() {\n\tif (_effectsRequestId) {\n\t\treturn;\n\t}\n\tauto &api = _owner->session().api();\n\t_effectsRequestId = api.request(MTPmessages_GetAvailableEffects(\n\t\tMTP_int(_effectsHash)\n\t)).done([=](const MTPmessages_AvailableEffects &result) {\n\t\t_effectsRequestId = 0;\n\t\tresult.match([&](const MTPDmessages_availableEffects &data) {\n\t\t\tupdateEffects(data);\n\t\t}, [&](const MTPDmessages_availableEffectsNotModified &) {\n\t\t});\n\t}).fail([=] {\n\t\t_effectsRequestId = 0;\n\t\t_effectsHash = 0;\n\t}).send();\n}\n\nvoid Reactions::updateTop(const MTPDmessages_reactions &data) {\n\t_topHash = data.vhash().v;\n\t_topIds = ListFromMTP(data);\n\t_top = resolveByIds(_topIds, _unresolvedTop);\n\t_topUpdated.fire({});\n}\n\nvoid Reactions::updateRecent(const MTPDmessages_reactions &data) {\n\t_recentHash = data.vhash().v;\n\t_recentIds = ListFromMTP(data);\n\t_recent = resolveByIds(_recentIds, _unresolvedRecent);\n\trecentUpdated();\n}\n\nvoid Reactions::updateDefault(const MTPDmessages_availableReactions &data) {\n\t_defaultHash = data.vhash().v;\n\n\tconst auto &list = data.vreactions().v;\n\tconst auto oldCache = base::take(_iconsCache);\n\tconst auto toCache = [&](DocumentData *document) {\n\t\tif (document) {\n\t\t\t_iconsCache.emplace(document, document->createMediaView());\n\t\t}\n\t};\n\t_active.clear();\n\t_available.clear();\n\t_active.reserve(list.size());\n\t_available.reserve(list.size());\n\t_iconsCache.reserve(list.size() * 4);\n\tfor (const auto &reaction : list) {\n\t\tif (const auto parsed = parse(reaction)) {\n\t\t\t_available.push_back(*parsed);\n\t\t\tif (parsed->active) {\n\t\t\t\t_active.push_back(*parsed);\n\t\t\t\ttoCache(parsed->appearAnimation);\n\t\t\t\ttoCache(parsed->selectAnimation);\n\t\t\t\ttoCache(parsed->centerIcon);\n\t\t\t\ttoCache(parsed->aroundAnimation);\n\t\t\t}\n\t\t}\n\t}\n\tif (_waitingForReactions) {\n\t\t_waitingForReactions = false;\n\t\tresolveReactionImages();\n\t}\n\tdefaultUpdated();\n}\n\nvoid Reactions::updateGeneric(const MTPDmessages_stickerSet &data) {\n\tconst auto oldCache = base::take(_genericCache);\n\tconst auto toCache = [&](not_null<DocumentData*> document) {\n\t\tif (document->sticker()) {\n\t\t\t_genericAnimations.push_back(document);\n\t\t\t_genericCache.emplace(document, document->createMediaView());\n\t\t}\n\t};\n\tconst auto &list = data.vdocuments().v;\n\t_genericAnimations.clear();\n\t_genericAnimations.reserve(list.size());\n\t_genericCache.reserve(list.size());\n\tfor (const auto &sticker : data.vdocuments().v) {\n\t\ttoCache(_owner->processDocument(sticker));\n\t}\n\tif (!_genericCache.empty()) {\n\t\t_genericCache.front().second->checkStickerLarge();\n\t}\n}\n\nvoid Reactions::updateMyTags(\n\t\tSavedSublist *sublist,\n\t\tconst MTPDmessages_savedReactionTags &data) {\n\tauto &my = _myTags[sublist];\n\tmy.hash = data.vhash().v;\n\tauto list = ListFromMTP(data);\n\tauto renamed = base::flat_set<ReactionId>();\n\tif (!sublist) {\n\t\tfor (const auto &info : list) {\n\t\t\tconst auto j = ranges::find(my.info, info.id, &MyTagInfo::id);\n\t\t\tconst auto was = (j != end(my.info)) ? j->title : QString();\n\t\t\tif (info.title != was) {\n\t\t\t\trenamed.emplace(info.id);\n\t\t\t}\n\t\t}\n\t}\n\tmy.info = std::move(list);\n\tmy.tags = resolveByInfos(my.info, _unresolvedMyTags, sublist);\n\t_myTagsUpdated.fire_copy(sublist);\n\tfor (const auto &id : renamed) {\n\t\t_myTagRenamed.fire_copy(id);\n\t}\n}\n\nvoid Reactions::updateTags(const MTPDmessages_reactions &data) {\n\t_tagsHash = data.vhash().v;\n\t_tagsIds = ListFromMTP(data);\n\t_tags = resolveByIds(_tagsIds, _unresolvedTags);\n\t_tagsUpdated.fire({});\n}\n\nvoid Reactions::updateEffects(const MTPDmessages_availableEffects &data) {\n\t_effectsHash = data.vhash().v;\n\n\tconst auto &list = data.veffects().v;\n\tconst auto toCache = [&](DocumentData *document) {\n\t\tif (document) {\n\t\t\t_iconsCache.emplace(document, document->createMediaView());\n\t\t}\n\t};\n\tfor (const auto &document : data.vdocuments().v) {\n\t\ttoCache(_owner->processDocument(document));\n\t}\n\t_effects.clear();\n\t_effects.reserve(list.size());\n\tfor (const auto &effect : list) {\n\t\tif (const auto parsed = parse(effect)) {\n\t\t\t_effects.push_back(*parsed);\n\t\t}\n\t}\n\tif (_waitingForEffects) {\n\t\t_waitingForEffects = false;\n\t\tresolveEffectImages();\n\t}\n\teffectsUpdated();\n}\n\nvoid Reactions::recentUpdated() {\n\t_topRefreshTimer.callOnce(kTopRequestDelay);\n\t_recentUpdated.fire({});\n}\n\nvoid Reactions::defaultUpdated() {\n\trefreshTop();\n\trefreshRecent();\n\tif (_genericAnimations.empty()) {\n\t\trequestGeneric();\n\t}\n\trefreshMyTags();\n\trefreshTags();\n\trefreshEffects();\n\t_defaultUpdated.fire({});\n}\n\nvoid Reactions::myTagsUpdated() {\n\tif (_genericAnimations.empty()) {\n\t\trequestGeneric();\n\t}\n\t_myTagsUpdated.fire({});\n}\n\nvoid Reactions::tagsUpdated() {\n\tif (_genericAnimations.empty()) {\n\t\trequestGeneric();\n\t}\n\t_tagsUpdated.fire({});\n}\n\nvoid Reactions::effectsUpdated() {\n\t_effectsUpdated.fire({});\n}\n\nnot_null<CustomEmojiManager::Listener*> Reactions::resolveListener() {\n\treturn static_cast<CustomEmojiManager::Listener*>(this);\n}\n\nvoid Reactions::customEmojiResolveDone(not_null<DocumentData*> document) {\n\tif (!document->sticker()) {\n\t\treturn;\n\t}\n\tconst auto id = ReactionId{ { document->id } };\n\tconst auto favorite = (_unresolvedFavoriteId == id);\n\tconst auto i = _unresolvedTop.find(id);\n\tconst auto top = (i != end(_unresolvedTop));\n\tconst auto j = _unresolvedRecent.find(id);\n\tconst auto recent = (j != end(_unresolvedRecent));\n\tconst auto k = _unresolvedMyTags.find(id);\n\tconst auto myTagSublists = (k != end(_unresolvedMyTags))\n\t\t? base::take(k->second)\n\t\t: base::flat_set<SavedSublist*>();\n\tconst auto l = _unresolvedTags.find(id);\n\tconst auto tag = (l != end(_unresolvedTags));\n\tif (favorite) {\n\t\t_unresolvedFavoriteId = ReactionId();\n\t\t_favorite = resolveById(_favoriteId);\n\t}\n\tif (top) {\n\t\t_unresolvedTop.erase(i);\n\t\t_top = resolveByIds(_topIds, _unresolvedTop);\n\t}\n\tif (recent) {\n\t\t_unresolvedRecent.erase(j);\n\t\t_recent = resolveByIds(_recentIds, _unresolvedRecent);\n\t}\n\tif (!myTagSublists.empty()) {\n\t\t_unresolvedMyTags.erase(k);\n\t\tfor (const auto &sublist : myTagSublists) {\n\t\t\tauto &my = _myTags[sublist];\n\t\t\tmy.tags = resolveByInfos(my.info, _unresolvedMyTags, sublist);\n\t\t}\n\t}\n\tif (tag) {\n\t\t_unresolvedTags.erase(l);\n\t\t_tags = resolveByIds(_tagsIds, _unresolvedTags);\n\t}\n\tif (favorite) {\n\t\t_favoriteUpdated.fire({});\n\t}\n\tif (top) {\n\t\t_topUpdated.fire({});\n\t}\n\tif (recent) {\n\t\t_recentUpdated.fire({});\n\t}\n\tfor (const auto &sublist : myTagSublists) {\n\t\t_myTagsUpdated.fire_copy(sublist);\n\t}\n\tif (tag) {\n\t\t_tagsUpdated.fire({});\n\t}\n}\n\nstd::optional<Reaction> Reactions::resolveById(const ReactionId &id) {\n\tif (const auto emoji = id.emoji(); !emoji.isEmpty()) {\n\t\tconst auto i = ranges::find(_available, id, &Reaction::id);\n\t\tif (i != end(_available)) {\n\t\t\treturn *i;\n\t\t}\n\t} else if (const auto customId = id.custom()) {\n\t\tconst auto document = _owner->document(customId);\n\t\tif (document->sticker()) {\n\t\t\treturn CustomReaction(document);\n\t\t}\n\t}\n\treturn {};\n}\n\nstd::vector<Reaction> Reactions::resolveByIds(\n\t\tconst std::vector<ReactionId> &ids,\n\t\tbase::flat_set<ReactionId> &unresolved) {\n\tauto result = std::vector<Reaction>();\n\tresult.reserve(ids.size());\n\tfor (const auto &id : ids) {\n\t\tif (const auto resolved = resolveById(id)) {\n\t\t\tresult.push_back(*resolved);\n\t\t} else if (unresolved.emplace(id).second) {\n\t\t\tresolve(id);\n\t\t}\n\t}\n\treturn result;\n}\n\nstd::optional<Reaction> Reactions::resolveByInfo(\n\t\tconst MyTagInfo &info,\n\t\tSavedSublist *sublist) {\n\tconst auto withInfo = [&](Reaction reaction) {\n\t\treaction.count = info.count;\n\t\treaction.title = sublist ? myTagTitle(reaction.id) : info.title;\n\t\treturn reaction;\n\t};\n\tif (const auto emoji = info.id.emoji(); !emoji.isEmpty()) {\n\t\tconst auto i = ranges::find(_available, info.id, &Reaction::id);\n\t\tif (i != end(_available)) {\n\t\t\treturn withInfo(*i);\n\t\t}\n\t} else if (const auto customId = info.id.custom()) {\n\t\tconst auto document = _owner->document(customId);\n\t\tif (document->sticker()) {\n\t\t\treturn withInfo(CustomReaction(document));\n\t\t}\n\t}\n\treturn {};\n}\n\nstd::vector<Reaction> Reactions::resolveByInfos(\n\t\tconst std::vector<MyTagInfo> &infos,\n\t\tbase::flat_map<\n\t\t\tReactionId,\n\t\t\tbase::flat_set<SavedSublist*>> &unresolved,\n\t\tSavedSublist *sublist) {\n\tauto result = std::vector<Reaction>();\n\tresult.reserve(infos.size());\n\tfor (const auto &tag : infos) {\n\t\tif (auto resolved = resolveByInfo(tag, sublist)) {\n\t\t\tresult.push_back(*resolved);\n\t\t} else if (const auto i = unresolved.find(tag.id)\n\t\t\t; i != end(unresolved)) {\n\t\t\ti->second.emplace(sublist);\n\t\t} else {\n\t\t\tunresolved[tag.id].emplace(sublist);\n\t\t\tresolve(tag.id);\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid Reactions::resolve(const ReactionId &id) {\n\tif (const auto emoji = id.emoji(); !emoji.isEmpty()) {\n\t\trefreshDefault();\n\t} else if (const auto customId = id.custom()) {\n\t\t_owner->customEmojiManager().resolve(\n\t\t\tcustomId,\n\t\t\tresolveListener());\n\t}\n}\n\nstd::optional<Reaction> Reactions::parse(const MTPAvailableReaction &entry) {\n\tconst auto &data = entry.data();\n\tconst auto emoji = qs(data.vreaction());\n\tconst auto known = (Ui::Emoji::Find(emoji) != nullptr);\n\tif (!known) {\n\t\tLOG((\"API Error: Unknown emoji in reactions: %1\").arg(emoji));\n\t\treturn std::nullopt;\n\t}\n\treturn std::make_optional(Reaction{\n\t\t.id = ReactionId{ emoji },\n\t\t.title = qs(data.vtitle()),\n\t\t//.staticIcon = _owner->processDocument(data.vstatic_icon()),\n\t\t.appearAnimation = _owner->processDocument(\n\t\t\tdata.vappear_animation()),\n\t\t.selectAnimation = _owner->processDocument(\n\t\t\tdata.vselect_animation()),\n\t\t//.activateAnimation = _owner->processDocument(\n\t\t//\tdata.vactivate_animation()),\n\t\t//.activateEffects = _owner->processDocument(\n\t\t//\tdata.veffect_animation()),\n\t\t.centerIcon = (data.vcenter_icon()\n\t\t\t? _owner->processDocument(*data.vcenter_icon()).get()\n\t\t\t: nullptr),\n\t\t.aroundAnimation = (data.varound_animation()\n\t\t\t? _owner->processDocument(*data.varound_animation()).get()\n\t\t\t: nullptr),\n\t\t.active = !data.is_inactive(),\n\t});\n}\n\nstd::optional<Reaction> Reactions::parse(const MTPAvailableEffect &entry) {\n\tconst auto &data = entry.data();\n\tconst auto emoji = qs(data.vemoticon());\n\tconst auto known = (Ui::Emoji::Find(emoji) != nullptr);\n\tif (!known) {\n\t\tLOG((\"API Error: Unknown emoji in effects: %1\").arg(emoji));\n\t\treturn std::nullopt;\n\t}\n\tconst auto id = DocumentId(data.vid().v);\n\tconst auto stickerId = data.veffect_sticker_id().v;\n\tconst auto document = _owner->document(stickerId);\n\tif (!document->sticker()) {\n\t\tLOG((\"API Error: Bad sticker in effects: %1\").arg(stickerId));\n\t\treturn std::nullopt;\n\t}\n\tconst auto aroundId = data.veffect_animation_id().value_or_empty();\n\tconst auto around = aroundId\n\t\t? _owner->document(aroundId).get()\n\t\t: nullptr;\n\tif (around && !around->sticker()) {\n\t\tLOG((\"API Error: Bad sticker in effects around: %1\").arg(aroundId));\n\t\treturn std::nullopt;\n\t}\n\tconst auto iconId = data.vstatic_icon_id().value_or_empty();\n\tconst auto icon = iconId ? _owner->document(iconId).get() : nullptr;\n\tif (icon && !icon->sticker()) {\n\t\tLOG((\"API Error: Bad sticker in effects icon: %1\").arg(iconId));\n\t\treturn std::nullopt;\n\t}\n\treturn std::make_optional(Reaction{\n\t\t.id = ReactionId{ id },\n\t\t.title = emoji,\n\t\t.appearAnimation = document,\n\t\t.selectAnimation = document,\n\t\t.centerIcon = icon,\n\t\t.aroundAnimation = around,\n\t\t.active = true,\n\t\t.effect = true,\n\t\t.premium = data.is_premium_required(),\n\t});\n}\n\nvoid Reactions::send(not_null<HistoryItem*> item, bool addToRecent) {\n\tconst auto id = item->fullId();\n\tauto &api = _owner->session().api();\n\tauto i = _sentRequests.find(id);\n\tif (i != end(_sentRequests)) {\n\t\tapi.request(i->second).cancel();\n\t} else {\n\t\ti = _sentRequests.emplace(id).first;\n\t}\n\tconst auto chosen = item->chosenReactions();\n\tusing Flag = MTPmessages_SendReaction::Flag;\n\tconst auto flags = (chosen.empty() ? Flag(0) : Flag::f_reaction)\n\t\t| (addToRecent ? Flag::f_add_to_recent : Flag(0));\n\ti->second = api.request(MTPmessages_SendReaction(\n\t\tMTP_flags(flags),\n\t\titem->history()->peer->input(),\n\t\tMTP_int(id.msg),\n\t\tMTP_vector<MTPReaction>(chosen | ranges::views::filter([](\n\t\t\t\tconst ReactionId &id) {\n\t\t\treturn !id.paid();\n\t\t}) | ranges::views::transform(\n\t\t\tReactionToMTP\n\t\t) | ranges::to<QVector<MTPReaction>>())\n\t)).done([=](const MTPUpdates &result) {\n\t\t_sentRequests.remove(id);\n\t\t_owner->session().api().applyUpdates(result);\n\t}).fail([=](const MTP::Error &error) {\n\t\t_sentRequests.remove(id);\n\t}).send();\n}\n\nvoid Reactions::poll(not_null<HistoryItem*> item, crl::time now) {\n\t// Group them by one second.\n\tconst auto last = item->lastReactionsRefreshTime();\n\tconst auto grouped = ((last + 999) / 1000) * 1000;\n\tif (!grouped || item->history()->peer->isUser()) {\n\t\t// First reaction always edits message.\n\t\treturn;\n\t} else if (const auto left = grouped + kPollEach - now; left > 0) {\n\t\tif (!_repaintItems.contains(item)) {\n\t\t\t_repaintItems.emplace(item, grouped + kPollEach);\n\t\t\tif (!_repaintTimer.isActive()\n\t\t\t\t|| _repaintTimer.remainingTime() > left) {\n\t\t\t\t_repaintTimer.callOnce(left);\n\t\t\t}\n\t\t}\n\t} else if (!_pollingItems.contains(item)) {\n\t\tif (_pollItems.empty() && !_pollRequestId) {\n\t\t\tcrl::on_main(&_owner->session(), [=] {\n\t\t\t\tpollCollected();\n\t\t\t});\n\t\t}\n\t\t_pollItems.emplace(item);\n\t}\n}\n\nvoid Reactions::updateAllInHistory(not_null<PeerData*> peer, bool enabled) {\n\tif (const auto history = _owner->historyLoaded(peer)) {\n\t\thistory->reactionsEnabledChanged(enabled);\n\t}\n}\n\nvoid Reactions::clearTemporary() {\n\t_temporary.clear();\n}\n\nReaction *Reactions::lookupTemporary(const ReactionId &id) {\n\tif (id.paid()) {\n\t\treturn lookupPaid();\n\t} else if (const auto emoji = id.emoji(); !emoji.isEmpty()) {\n\t\tconst auto i = ranges::find(_available, id, &Reaction::id);\n\t\treturn (i != end(_available)) ? &*i : nullptr;\n\t} else if (const auto customId = id.custom()) {\n\t\tif (const auto i = _temporary.find(customId); i != end(_temporary)) {\n\t\t\treturn &i->second;\n\t\t}\n\t\tconst auto document = _owner->document(customId);\n\t\tif (document->sticker()) {\n\t\t\treturn &_temporary.emplace(\n\t\t\t\tcustomId,\n\t\t\t\tCustomReaction(document)).first->second;\n\t\t}\n\t\t_owner->customEmojiManager().resolve(\n\t\t\tcustomId,\n\t\t\tresolveListener());\n\t\treturn nullptr;\n\t}\n\treturn nullptr;\n}\n\nnot_null<Reaction*> Reactions::lookupPaid() {\n\tif (!_paid) {\n\t\tconst auto generate = [&](const QString &name) {\n\t\t\tconst auto session = &_owner->session();\n\t\t\treturn ChatHelpers::GenerateLocalTgsSticker(session, name);\n\t\t};\n\t\tconst auto appear = generate(u\"star_reaction_appear\"_q);\n\t\tconst auto center = generate(u\"star_reaction_center\"_q);\n\t\tconst auto select = generate(u\"star_reaction_select\"_q);\n\t\t_paid.emplace(Reaction{\n\t\t\t.id = ReactionId::Paid(),\n\t\t\t.title = u\"Telegram Star\"_q,\n\t\t\t.appearAnimation = appear,\n\t\t\t.selectAnimation = select,\n\t\t\t.centerIcon = center,\n\t\t\t.active = true,\n\t\t});\n\t\t_iconsCache.emplace(appear, appear->createMediaView());\n\t\t_iconsCache.emplace(center, center->createMediaView());\n\t\t_iconsCache.emplace(select, select->createMediaView());\n\n\t\tfillPaidReactionAnimations();\n\t}\n\treturn &*_paid;\n}\n\nnot_null<DocumentData*> Reactions::paidToastAnimation() {\n\tif (!_paidToastAnimation) {\n\t\t_paidToastAnimation = ChatHelpers::GenerateLocalTgsSticker(\n\t\t\t&_owner->session(),\n\t\t\tu\"star_reaction_toast\"_q);\n\t}\n\treturn _paidToastAnimation;\n}\n\nrpl::producer<std::vector<Reaction>> Reactions::myTagsValue(\n\t\tSavedSublist *sublist) {\n\trefreshMyTags(sublist);\n\tconst auto list = [=] {\n\t\treturn _myTags[sublist].tags;\n\t};\n\treturn rpl::single(\n\t\tlist()\n\t) | rpl::then(_myTagsUpdated.events(\n\t) | rpl::filter(\n\t\trpl::mappers::_1 == sublist\n\t) | rpl::map(list));\n}\n\nbool Reactions::isQuitPrevent() {\n\tfor (auto i = begin(_sendPaidItems); i != end(_sendPaidItems);) {\n\t\tconst auto item = i->first;\n\t\tif (_sendingPaid.contains(item)) {\n\t\t\t++i;\n\t\t} else {\n\t\t\ti = _sendPaidItems.erase(i);\n\t\t\tsendPaid(item);\n\t\t}\n\t}\n\tif (_sendingPaid.empty()) {\n\t\treturn false;\n\t}\n\tLOG((\"Reactions prevents quit, sending paid...\"));\n\treturn true;\n}\n\nvoid Reactions::schedulePaid(not_null<HistoryItem*> item) {\n\t_sendPaidItems[item] = crl::now() + kPaidAccumulatePeriod;\n\tif (!_sendPaidTimer.isActive()) {\n\t\t_sendPaidTimer.callOnce(kPaidAccumulatePeriod);\n\t}\n}\n\nvoid Reactions::undoScheduledPaid(not_null<HistoryItem*> item) {\n\t_sendPaidItems.remove(item);\n\titem->cancelScheduledPaidReaction();\n}\n\ncrl::time Reactions::sendingScheduledPaidAt(\n\t\tnot_null<HistoryItem*> item) const {\n\tconst auto i = _sendPaidItems.find(item);\n\treturn (i != end(_sendPaidItems)) ? i->second : crl::time();\n}\n\nvoid Reactions::schedulePaid(not_null<Calls::GroupCall*> call) {\n\t_sendPaidCalls[call] = crl::now() + kPaidAccumulatePeriod;\n\tif (!_sendPaidTimer.isActive()) {\n\t\t_sendPaidTimer.callOnce(kPaidAccumulatePeriod);\n\t}\n}\n\nvoid Reactions::undoScheduledPaid(not_null<Calls::GroupCall*> call) {\n\t_sendPaidCalls.remove(call);\n\tcall->messages()->reactionsPaidScheduledCancel();\n}\n\ncrl::time Reactions::sendingScheduledPaidAt(\n\t\tnot_null<Calls::GroupCall*> call) const {\n\tconst auto i = _sendPaidCalls.find(call);\n\treturn (i != end(_sendPaidCalls)) ? i->second : crl::time();\n}\n\ncrl::time Reactions::ScheduledPaidDelay() {\n\treturn kPaidAccumulatePeriod;\n}\n\nvoid Reactions::repaintCollected() {\n\tconst auto now = crl::now();\n\tauto closest = crl::time();\n\tfor (auto i = begin(_repaintItems); i != end(_repaintItems);) {\n\t\tif (i->second <= now) {\n\t\t\t_owner->requestItemRepaint(i->first);\n\t\t\ti = _repaintItems.erase(i);\n\t\t} else {\n\t\t\tif (!closest || i->second < closest) {\n\t\t\t\tclosest = i->second;\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t}\n\tif (closest) {\n\t\t_repaintTimer.callOnce(closest - now);\n\t}\n}\n\nvoid Reactions::pollCollected() {\n\tauto toRequest = base::flat_map<not_null<PeerData*>, QVector<MTPint>>();\n\t_pollingItems = std::move(_pollItems);\n\tfor (const auto &item : _pollingItems) {\n\t\ttoRequest[item->history()->peer].push_back(MTP_int(item->id));\n\t}\n\tauto &api = _owner->session().api();\n\tfor (const auto &[peer, ids] : toRequest) {\n\t\tconst auto finalize = [=] {\n\t\t\tconst auto now = crl::now();\n\t\t\tfor (const auto &item : base::take(_pollingItems)) {\n\t\t\t\tconst auto last = item->lastReactionsRefreshTime();\n\t\t\t\tif (last && last + kPollEach <= now) {\n\t\t\t\t\titem->updateReactions(nullptr);\n\t\t\t\t}\n\t\t\t}\n\t\t\t_pollRequestId = 0;\n\t\t\tif (!_pollItems.empty()) {\n\t\t\t\tcrl::on_main(&_owner->session(), [=] {\n\t\t\t\t\tpollCollected();\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\t\t_pollRequestId = api.request(MTPmessages_GetMessagesReactions(\n\t\t\tpeer->input(),\n\t\t\tMTP_vector<MTPint>(ids)\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t_owner->session().api().applyUpdates(result);\n\t\t\tfinalize();\n\t\t}).fail([=] {\n\t\t\tfinalize();\n\t\t}).send();\n\t}\n}\n\nbool Reactions::sending(not_null<HistoryItem*> item) const {\n\treturn _sentRequests.contains(item->fullId())\n\t\t|| _sendingPaid.contains(item);\n}\n\nbool Reactions::HasUnread(const MTPMessageReactions &data) {\n\treturn data.match([&](const MTPDmessageReactions &data) {\n\t\tif (const auto &recent = data.vrecent_reactions()) {\n\t\t\tfor (const auto &one : recent->v) {\n\t\t\t\tif (one.match([&](const MTPDmessagePeerReaction &data) {\n\t\t\t\t\treturn data.is_unread();\n\t\t\t\t})) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t});\n}\n\nvoid Reactions::CheckUnknownForUnread(\n\t\tnot_null<Session*> owner,\n\t\tconst MTPMessage &message) {\n\tmessage.match([&](const MTPDmessage &data) {\n\t\tif (data.vreactions() && HasUnread(*data.vreactions())) {\n\t\t\tconst auto peerId = peerFromMTP(data.vpeer_id());\n\t\t\tif (const auto history = owner->historyLoaded(peerId)) {\n\t\t\t\towner->histories().requestDialogEntry(history);\n\t\t\t}\n\t\t}\n\t}, [](const auto &) {\n\t});\n}\n\nvoid Reactions::sendPaid() {\n\tauto next = crl::time();\n\tconst auto now = crl::now();\n\tif (_sendingPaid.empty()) {\n\t\tfor (auto i = begin(_sendPaidItems); i != end(_sendPaidItems);) {\n\t\t\tconst auto item = i->first;\n\t\t\tconst auto when = i->second;\n\t\t\tif (when > now) {\n\t\t\t\tif (!next || next > when) {\n\t\t\t\t\tnext = when;\n\t\t\t\t}\n\t\t\t\t++i;\n\t\t\t} else {\n\t\t\t\ti = _sendPaidItems.erase(i);\n\t\t\t\tif (sendPaid(item)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tfor (auto i = begin(_sendPaidCalls); i != end(_sendPaidCalls);) {\n\t\tconst auto call = i->first;\n\t\tconst auto when = i->second;\n\t\tif (when > now) {\n\t\t\tif (!next || next > when) {\n\t\t\t\tnext = when;\n\t\t\t}\n\t\t\t++i;\n\t\t} else {\n\t\t\ti = _sendPaidCalls.erase(i);\n\t\t\tcall->messages()->reactionsPaidSend();\n\t\t}\n\t}\n\tif (next) {\n\t\t_sendPaidTimer.callOnce(next - now);\n\t}\n}\n\nbool Reactions::sendPaid(not_null<HistoryItem*> item) {\n\tconst auto send = item->startPaidReactionSending();\n\tif (!send.valid) {\n\t\treturn false;\n\t}\n\n\tsendPaidRequest(item, send);\n\treturn true;\n}\n\nvoid Reactions::sendPaidPrivacyRequest(\n\t\tnot_null<HistoryItem*> item,\n\t\tPaidReactionSend send) {\n\tExpects(!_sendingPaid.contains(item));\n\tExpects(send.shownPeer.has_value());\n\tExpects(!send.count);\n\n\tconst auto id = item->fullId();\n\tauto &api = _owner->session().api();\n\tconst auto requestId = api.request(\n\t\tMTPmessages_TogglePaidReactionPrivacy(\n\t\t\titem->history()->peer->input(),\n\t\t\tMTP_int(id.msg),\n\t\t\tPaidReactionShownPeerToTL(&_owner->session(), send.shownPeer))\n\t).done([=] {\n\t\tif (const auto item = _owner->message(id)) {\n\t\t\tif (_sendingPaid.remove(item)) {\n\t\t\t\tsendPaidFinish(item, send, true);\n\t\t\t}\n\t\t}\n\t\tcheckQuitPreventFinished();\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (const auto item = _owner->message(id)) {\n\t\t\tif (_sendingPaid.remove(item)) {\n\t\t\t\tsendPaidFinish(item, send, false);\n\t\t\t}\n\t\t}\n\t\tcheckQuitPreventFinished();\n\t}).send();\n\t_sendingPaid[item] = requestId;\n}\n\nvoid Reactions::sendPaidRequest(\n\t\tnot_null<HistoryItem*> item,\n\t\tPaidReactionSend send) {\n\tExpects(!_sendingPaid.contains(item));\n\n\tif (!send.count) {\n\t\tsendPaidPrivacyRequest(item, send);\n\t\treturn;\n\t}\n\n\tconst auto id = item->fullId();\n\tconst auto randomId = base::unixtime::mtproto_msg_id();\n\tauto &api = _owner->session().api();\n\tusing Flag = MTPmessages_SendPaidReaction::Flag;\n\tconst auto requestId = api.request(MTPmessages_SendPaidReaction(\n\t\tMTP_flags(send.shownPeer ? Flag::f_private : Flag()),\n\t\titem->history()->peer->input(),\n\t\tMTP_int(id.msg),\n\t\tMTP_int(send.count),\n\t\tMTP_long(randomId),\n\t\t(!send.shownPeer\n\t\t\t? MTPPaidReactionPrivacy()\n\t\t\t: PaidReactionShownPeerToTL(&_owner->session(), *send.shownPeer))\n\t)).done([=](const MTPUpdates &result) {\n\t\tif (const auto item = _owner->message(id)) {\n\t\t\tif (_sendingPaid.remove(item)) {\n\t\t\t\tsendPaidFinish(item, send, true);\n\t\t\t}\n\t\t}\n\t\t_owner->session().api().applyUpdates(result);\n\t\tcheckQuitPreventFinished();\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (const auto item = _owner->message(id)) {\n\t\t\t_sendingPaid.remove(item);\n\t\t\tif (error.type() == u\"RANDOM_ID_EXPIRED\"_q) {\n\t\t\t\tsendPaidRequest(item, send);\n\t\t\t} else {\n\t\t\t\tsendPaidFinish(item, send, false);\n\t\t\t}\n\t\t}\n\t\tcheckQuitPreventFinished();\n\t}).send();\n\t_sendingPaid[item] = requestId;\n}\n\nvoid Reactions::checkQuitPreventFinished() {\n\tif (_sendingPaid.empty()) {\n\t\tif (Core::Quitting()) {\n\t\t\tLOG((\"Reactions doesn't prevent quit any more.\"));\n\t\t}\n\t\tCore::App().quitPreventFinished();\n\t}\n}\n\nvoid Reactions::sendPaidFinish(\n\t\tnot_null<HistoryItem*> item,\n\t\tPaidReactionSend send,\n\t\tbool success) {\n\titem->finishPaidReactionSending(send, success);\n\tsendPaid();\n}\n\nMessageReactions::MessageReactions(not_null<HistoryItem*> item)\n: _item(item) {\n}\n\nMessageReactions::~MessageReactions() {\n\tcancelScheduledPaid();\n\tif (const auto paid = _paid.get()) {\n\t\tif (paid->sending > 0) {\n\t\t\tfinishPaidSending({\n\t\t\t\t.count = int(paid->sending),\n\t\t\t\t.valid = true,\n\t\t\t\t.shownPeer = MaybeShownPeer(\n\t\t\t\t\tpaid->sendingPrivacySet,\n\t\t\t\t\tpaid->sendingShownPeer),\n\t\t\t}, false);\n\t\t}\n\t}\n}\n\nvoid MessageReactions::add(const ReactionId &id, bool addToRecent) {\n\tExpects(!id.empty());\n\tExpects(!id.paid());\n\n\tconst auto history = _item->history();\n\tconst auto myLimit = SentReactionsLimit(_item);\n\tif (ranges::contains(chosen(), id)) {\n\t\treturn;\n\t}\n\tauto my = 0;\n\tconst auto tags = _item->reactionsAreTags();\n\tif (tags) {\n\t\tconst auto sublist = _item->savedSublist();\n\t\thistory->owner().reactions().incrementMyTag(id, sublist);\n\t}\n\t_list.erase(ranges::remove_if(_list, [&](MessageReaction &one) {\n\t\tif (one.id.paid()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto removing = one.my && (my == myLimit || ++my == myLimit);\n\t\tif (!removing) {\n\t\t\treturn false;\n\t\t}\n\t\tone.my = false;\n\t\tconst auto removed = !--one.count;\n\t\tconst auto j = _recent.find(one.id);\n\t\tif (j != end(_recent)) {\n\t\t\tif (removed) {\n\t\t\t\tj->second.clear();\n\t\t\t\t_recent.erase(j);\n\t\t\t} else {\n\t\t\t\tj->second.erase(\n\t\t\t\t\tranges::remove(j->second, true, &RecentReaction::my),\n\t\t\t\t\tend(j->second));\n\t\t\t\tif (j->second.empty()) {\n\t\t\t\t\t_recent.erase(j);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (tags) {\n\t\t\tconst auto sublist = _item->savedSublist();\n\t\t\thistory->owner().reactions().decrementMyTag(one.id, sublist);\n\t\t}\n\t\treturn removed;\n\t}), end(_list));\n\tconst auto peer = history->peer;\n\tif (_item->canViewReactions() || peer->isUser()) {\n\t\tauto &list = _recent[id];\n\t\tconst auto from = peer->session().sendAsPeers().resolveChosen(peer);\n\t\tlist.insert(begin(list), RecentReaction{\n\t\t\t.peer = from,\n\t\t\t.my = true,\n\t\t});\n\t}\n\tconst auto i = ranges::find(_list, id, &MessageReaction::id);\n\tif (i != end(_list)) {\n\t\ti->my = true;\n\t\t++i->count;\n\t\tstd::rotate(i, i + 1, end(_list));\n\t} else {\n\t\t_list.push_back({ .id = id, .count = 1, .my = true });\n\t}\n\tauto &owner = history->owner();\n\towner.reactions().send(_item, addToRecent);\n\towner.notifyItemDataChange(_item);\n}\n\nvoid MessageReactions::remove(const ReactionId &id) {\n\tExpects(!id.paid());\n\n\tconst auto history = _item->history();\n\tconst auto self = history->session().user();\n\tconst auto i = ranges::find(_list, id, &MessageReaction::id);\n\tconst auto j = _recent.find(id);\n\tif (i == end(_list)) {\n\t\tAssert(j == end(_recent));\n\t\treturn;\n\t} else if (!i->my) {\n\t\tAssert(j == end(_recent)\n\t\t\t|| !ranges::contains(j->second, self, &RecentReaction::peer));\n\t\treturn;\n\t}\n\ti->my = false;\n\tconst auto tags = _item->reactionsAreTags();\n\tconst auto removed = !--i->count;\n\tif (removed) {\n\t\t_list.erase(i);\n\t}\n\tif (j != end(_recent)) {\n\t\tif (removed) {\n\t\t\tj->second.clear();\n\t\t\t_recent.erase(j);\n\t\t} else {\n\t\t\tj->second.erase(\n\t\t\t\tranges::remove(j->second, true, &RecentReaction::my),\n\t\t\t\tend(j->second));\n\t\t\tif (j->second.empty()) {\n\t\t\t\t_recent.erase(j);\n\t\t\t}\n\t\t}\n\t}\n\tif (tags) {\n\t\tconst auto sublist = _item->savedSublist();\n\t\thistory->owner().reactions().decrementMyTag(id, sublist);\n\t}\n\tauto &owner = history->owner();\n\towner.reactions().send(_item, false);\n\towner.notifyItemDataChange(_item);\n}\n\nbool MessageReactions::checkIfChanged(\n\t\tconst QVector<MTPReactionCount> &list,\n\t\tconst QVector<MTPMessagePeerReaction> &recent,\n\t\tbool min) const {\n\tauto &owner = _item->history()->owner();\n\tif (owner.reactions().sending(_item)) {\n\t\t// We'll apply non-stale data from the request response.\n\t\treturn false;\n\t}\n\tauto existing = base::flat_set<ReactionId>();\n\tfor (const auto &count : list) {\n\t\tconst auto changed = count.match([&](const MTPDreactionCount &data) {\n\t\t\tconst auto id = ReactionFromMTP(data.vreaction());\n\t\t\tconst auto nowCount = data.vcount().v;\n\t\t\tconst auto i = ranges::find(_list, id, &MessageReaction::id);\n\t\t\tconst auto wasCount = (i != end(_list)) ? i->count : 0;\n\t\t\tif (wasCount != nowCount) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\texisting.emplace(id);\n\t\t\treturn false;\n\t\t});\n\t\tif (changed) {\n\t\t\treturn true;\n\t\t}\n\t}\n\tfor (const auto &reaction : _list) {\n\t\tif (!existing.contains(reaction.id)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\tauto parsed = base::flat_map<ReactionId, std::vector<RecentReaction>>();\n\tfor (const auto &reaction : recent) {\n\t\treaction.match([&](const MTPDmessagePeerReaction &data) {\n\t\t\tconst auto id = ReactionFromMTP(data.vreaction());\n\t\t\tif (!ranges::contains(_list, id, &MessageReaction::id)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto peerId = peerFromMTP(data.vpeer_id());\n\t\t\tconst auto peer = owner.peer(peerId);\n\t\t\tconst auto my = IsMyRecent(data, id, peer, _recent, min);\n\t\t\tparsed[id].push_back({\n\t\t\t\t.peer = peer,\n\t\t\t\t.unread = data.is_unread(),\n\t\t\t\t.big = data.is_big(),\n\t\t\t\t.my = my,\n\t\t\t});\n\t\t});\n\t}\n\treturn !ranges::equal(_recent, parsed, [](\n\t\t\tconst auto &a,\n\t\t\tconst auto &b) {\n\t\treturn ranges::equal(a.second, b.second, [](\n\t\t\t\tconst RecentReaction &a,\n\t\t\t\tconst RecentReaction &b) {\n\t\t\treturn (a.peer == b.peer) && (a.big == b.big) && (a.my == b.my);\n\t\t});\n\t});\n}\n\nbool MessageReactions::change(\n\t\tconst QVector<MTPReactionCount> &list,\n\t\tconst QVector<MTPMessagePeerReaction> &recent,\n\t\tconst QVector<MTPMessageReactor> &top,\n\t\tbool min) {\n\tauto &owner = _item->history()->owner();\n\tif (owner.reactions().sending(_item)) {\n\t\t// We'll apply non-stale data from the request response.\n\t\treturn false;\n\t}\n\tauto changed = false;\n\tauto existing = base::flat_set<ReactionId>();\n\tauto order = base::flat_map<ReactionId, int>();\n\tfor (const auto &count : list) {\n\t\tcount.match([&](const MTPDreactionCount &data) {\n\t\t\tconst auto id = ReactionFromMTP(data.vreaction());\n\t\t\tconst auto &chosen = data.vchosen_order();\n\t\t\tif (!min && chosen) {\n\t\t\t\torder[id] = chosen->v;\n\t\t\t}\n\t\t\tconst auto i = ranges::find(_list, id, &MessageReaction::id);\n\t\t\tconst auto nowCount = data.vcount().v;\n\t\t\tif (i == end(_list)) {\n\t\t\t\tchanged = true;\n\t\t\t\t_list.push_back({\n\t\t\t\t\t.id = id,\n\t\t\t\t\t.count = nowCount,\n\t\t\t\t\t.my = (!min && chosen)\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconst auto nowMy = min ? i->my : chosen.has_value();\n\t\t\t\tif (i->count != nowCount || i->my != nowMy) {\n\t\t\t\t\ti->count = nowCount;\n\t\t\t\t\ti->my = nowMy;\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\texisting.emplace(id);\n\t\t});\n\t}\n\tif (!min && !order.empty()) {\n\t\tconst auto minimal = std::numeric_limits<int>::min();\n\t\tconst auto proj = [&](const MessageReaction &reaction) {\n\t\t\treturn reaction.my ? order[reaction.id] : minimal;\n\t\t};\n\t\tconst auto correctOrder = [&] {\n\t\t\tauto previousOrder = minimal;\n\t\t\tfor (const auto &reaction : _list) {\n\t\t\t\tconst auto nowOrder = proj(reaction);\n\t\t\t\tif (nowOrder < previousOrder) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tpreviousOrder = nowOrder;\n\t\t\t}\n\t\t\treturn true;\n\t\t}();\n\t\tif (!correctOrder) {\n\t\t\tchanged = true;\n\t\t\tranges::sort(_list, std::less(), proj);\n\t\t}\n\t}\n\tif (_list.size() != existing.size()) {\n\t\tchanged = true;\n\t\tfor (auto i = begin(_list); i != end(_list);) {\n\t\t\tif (!existing.contains(i->id)) {\n\t\t\t\ti = _list.erase(i);\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t}\n\tauto parsed = base::flat_map<ReactionId, std::vector<RecentReaction>>();\n\tfor (const auto &reaction : recent) {\n\t\treaction.match([&](const MTPDmessagePeerReaction &data) {\n\t\t\tconst auto id = ReactionFromMTP(data.vreaction());\n\t\t\tconst auto i = ranges::find(_list, id, &MessageReaction::id);\n\t\t\tif (i == end(_list)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto &list = parsed[id];\n\t\t\tif (list.size() >= i->count) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto peer = owner.peer(peerFromMTP(data.vpeer_id()));\n\t\t\tconst auto my = IsMyRecent(data, id, peer, _recent, min);\n\t\t\tlist.push_back({\n\t\t\t\t.peer = peer,\n\t\t\t\t.unread = data.is_unread(),\n\t\t\t\t.big = data.is_big(),\n\t\t\t\t.my = my,\n\t\t\t});\n\t\t});\n\t}\n\tif (_recent != parsed) {\n\t\t_recent = std::move(parsed);\n\t\tchanged = true;\n\t}\n\n\tauto paidTop = std::vector<TopPaid>();\n\tconst auto &paindTopNow = _paid ? _paid->top : std::vector<TopPaid>();\n\tfor (const auto &reactor : top) {\n\t\tconst auto &data = reactor.data();\n\t\tconst auto peerId = (data.is_anonymous() || !data.vpeer_id())\n\t\t\t? PeerId()\n\t\t\t: peerFromMTP(*data.vpeer_id());\n\t\tconst auto peer = peerId ? owner.peer(peerId).get() : nullptr;\n\t\tpaidTop.push_back({\n\t\t\t.peer = peer,\n\t\t\t.count = uint32(data.vcount().v),\n\t\t\t.top = data.is_top(),\n\t\t\t.my = IsMyTop(data, peer, paindTopNow, min),\n\t\t});\n\t}\n\tif (paidTop.empty()) {\n\t\tif (_paid && !_paid->top.empty()) {\n\t\t\tchanged = true;\n\t\t\tif (localPaidData()) {\n\t\t\t\t_paid->top.clear();\n\t\t\t} else {\n\t\t\t\t_paid = nullptr;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (min && _paid) {\n\t\t\tconst auto mine = [](const TopPaid &entry) {\n\t\t\t\treturn entry.my != 0;\n\t\t\t};\n\t\t\tif (!ranges::contains(paidTop, true, mine)) {\n\t\t\t\tconst auto nonTopMine = [](const TopPaid &entry) {\n\t\t\t\t\treturn entry.my && !entry.top;\n\t\t\t\t};\n\t\t\t\tconst auto i = ranges::find(_paid->top, true, nonTopMine);\n\t\t\t\tif (i != end(_paid->top)) {\n\t\t\t\t\tpaidTop.push_back(*i);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tranges::sort(paidTop, std::greater(), [](const TopPaid &entry) {\n\t\t\treturn entry.count;\n\t\t});\n\t\tif (!_paid) {\n\t\t\t_paid = std::make_unique<Paid>();\n\t\t}\n\t\tif (_paid->top != paidTop) {\n\t\t\t_paid->top = std::move(paidTop);\n\t\t\tchanged = true;\n\t\t}\n\t}\n\treturn changed;\n}\n\nconst std::vector<MessageReaction> &MessageReactions::list() const {\n\treturn _list;\n}\n\nauto MessageReactions::recent() const\n-> const base::flat_map<ReactionId, std::vector<RecentReaction>> & {\n\treturn _recent;\n}\n\nauto MessageReactions::topPaid() const -> const std::vector<TopPaid> & {\n\tstatic const auto kEmpty = std::vector<TopPaid>();\n\treturn _paid ? _paid->top : kEmpty;\n}\n\nbool MessageReactions::empty() const {\n\treturn _list.empty();\n}\n\nbool MessageReactions::hasUnread() const {\n\tfor (auto &[emoji, list] : _recent) {\n\t\tif (ranges::contains(list, true, &RecentReaction::unread)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid MessageReactions::markRead() {\n\tfor (auto &[emoji, list] : _recent) {\n\t\tfor (auto &reaction : list) {\n\t\t\treaction.unread = false;\n\t\t}\n\t}\n}\n\nvoid MessageReactions::scheduleSendPaid(\n\t\tint count,\n\t\tstd::optional<PeerId> shownPeer) {\n\tExpects(count >= 0);\n\n\tif (!_paid) {\n\t\t_paid = std::make_unique<Paid>();\n\t}\n\t_paid->scheduled += count;\n\t_paid->scheduledFlag = 1;\n\tif (shownPeer.has_value()) {\n\t\t_paid->scheduledShownPeer = *shownPeer;\n\t\t_paid->scheduledPrivacySet = true;\n\t}\n\tif (count > 0) {\n\t\t_item->history()->session().credits().lock(CreditsAmount(count));\n\t}\n\t_item->history()->owner().reactions().schedulePaid(_item);\n}\n\nint MessageReactions::scheduledPaid() const {\n\treturn _paid ? _paid->scheduled : 0;\n}\n\nvoid MessageReactions::cancelScheduledPaid() {\n\tif (_paid) {\n\t\tif (_paid->scheduledFlag) {\n\t\t\tif (const auto amount = int(_paid->scheduled)) {\n\t\t\t\t_item->history()->session().credits().unlock(\n\t\t\t\t\tCreditsAmount(amount));\n\t\t\t}\n\t\t\t_paid->scheduled = 0;\n\t\t\t_paid->scheduledFlag = 0;\n\t\t\t_paid->scheduledShownPeer = 0;\n\t\t\t_paid->scheduledPrivacySet = 0;\n\t\t}\n\t\tif (!_paid->sendingFlag && _paid->top.empty()) {\n\t\t\t_paid = nullptr;\n\t\t}\n\t}\n}\n\nPaidReactionSend MessageReactions::startPaidSending() {\n\tif (!_paid || !_paid->scheduledFlag || _paid->sendingFlag) {\n\t\treturn {};\n\t}\n\t_paid->sending = _paid->scheduled;\n\t_paid->sendingFlag = _paid->scheduledFlag;\n\t_paid->sendingShownPeer = _paid->scheduledShownPeer;\n\t_paid->sendingPrivacySet = _paid->scheduledPrivacySet;\n\t_paid->scheduled = 0;\n\t_paid->scheduledFlag = 0;\n\t_paid->scheduledShownPeer = 0;\n\t_paid->scheduledPrivacySet = 0;\n\treturn {\n\t\t.count = int(_paid->sending),\n\t\t.valid = true,\n\t\t.shownPeer = MaybeShownPeer(\n\t\t\t_paid->sendingPrivacySet,\n\t\t\t_paid->sendingShownPeer),\n\t};\n}\n\nvoid MessageReactions::finishPaidSending(\n\t\tPaidReactionSend send,\n\t\tbool success) {\n\tExpects(_paid != nullptr);\n\tExpects(send.count == _paid->sending);\n\tExpects(send.valid == (_paid->sendingFlag == 1));\n\tExpects(send.shownPeer == MaybeShownPeer(\n\t\t_paid->sendingPrivacySet,\n\t\t_paid->sendingShownPeer));\n\n\t_paid->sending = 0;\n\t_paid->sendingFlag = 0;\n\t_paid->sendingShownPeer = 0;\n\t_paid->sendingPrivacySet = 0;\n\tif (!_paid->scheduledFlag && _paid->top.empty()) {\n\t\t_paid = nullptr;\n\t} else if (!send.count) {\n\t\tconst auto i = ranges::find_if(_paid->top, [](const TopPaid &top) {\n\t\t\treturn top.my;\n\t\t});\n\t\tif (i != end(_paid->top)) {\n\t\t\ti->peer = send.shownPeer\n\t\t\t\t? _item->history()->owner().peer(*send.shownPeer).get()\n\t\t\t\t: nullptr;\n\t\t}\n\t}\n\tif (const auto amount = send.count) {\n\t\tconst auto credits = &_item->history()->session().credits();\n\t\tif (success) {\n\t\t\tcredits->withdrawLocked(CreditsAmount(amount));\n\t\t} else {\n\t\t\tcredits->unlock(CreditsAmount(amount));\n\t\t}\n\t}\n}\n\nbool MessageReactions::localPaidData() const {\n\treturn _paid && (_paid->scheduledFlag || _paid->sendingFlag);\n}\n\nint MessageReactions::localPaidCount() const {\n\treturn _paid ? (_paid->scheduled + _paid->sending) : 0;\n}\n\nPeerId MessageReactions::localPaidShownPeer() const {\n\tconst auto minePaidShownPeer = [&] {\n\t\tfor (const auto &entry : _paid->top) {\n\t\t\tif (entry.my) {\n\t\t\t\treturn entry.peer ? entry.peer->id : PeerId();\n\t\t\t}\n\t\t}\n\t\tconst auto api = &_item->history()->session().api();\n\t\treturn api->globalPrivacy().paidReactionShownPeerCurrent();\n\t};\n\treturn !_paid\n\t\t? PeerId()\n\t\t: (_paid->scheduledFlag && _paid->scheduledPrivacySet)\n\t\t? _paid->scheduledShownPeer\n\t\t: (_paid->sendingFlag && _paid->sendingPrivacySet)\n\t\t? _paid->sendingShownPeer\n\t\t: minePaidShownPeer();\n}\n\nbool MessageReactions::clearCloudData() {\n\tconst auto result = !_list.empty();\n\t_recent.clear();\n\t_list.clear();\n\tif (localPaidData()) {\n\t\t_paid->top.clear();\n\t} else {\n\t\t_paid = nullptr;\n\t}\n\treturn result;\n}\n\nstd::vector<ReactionId> MessageReactions::chosen() const {\n\treturn _list\n\t\t| ranges::views::filter(&MessageReaction::my)\n\t\t| ranges::views::transform(&MessageReaction::id)\n\t\t| ranges::to_vector;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_message_reactions.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"data/data_message_reaction_id.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n\nnamespace Calls {\nclass GroupCall;\n} // namespace Calls\n\nnamespace Ui {\nclass AnimatedIcon;\n} // namespace Ui\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nnamespace Data {\n\nclass SavedSublist;\nclass DocumentMedia;\nclass Session;\n\nstruct Reaction {\n\tReactionId id;\n\tQString title;\n\t//not_null<DocumentData*> staticIcon;\n\tnot_null<DocumentData*> appearAnimation;\n\tnot_null<DocumentData*> selectAnimation;\n\t//not_null<DocumentData*> activateAnimation;\n\t//not_null<DocumentData*> activateEffects;\n\tDocumentData *centerIcon = nullptr;\n\tDocumentData *aroundAnimation = nullptr;\n\tint count = 0;\n\tbool active = false;\n\tbool effect = false;\n\tbool premium = false;\n};\n\nstruct PossibleItemReactionsRef {\n\tstd::vector<not_null<const Reaction*>> recent;\n\tstd::vector<not_null<const Reaction*>> stickers;\n\tbool customAllowed = false;\n\tbool tags = false;\n};\n\nstruct PossibleItemReactions {\n\tPossibleItemReactions() = default;\n\texplicit PossibleItemReactions(const PossibleItemReactionsRef &other);\n\n\tstd::vector<Reaction> recent;\n\tstd::vector<Reaction> stickers;\n\tbool customAllowed = false;\n\tbool tags = false;\n};\n\n[[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions(\n\tnot_null<HistoryItem*> item,\n\tbool paidInFront = false);\n[[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions(\n\tnot_null<Main::Session*> session);\n\nstruct MyTagInfo {\n\tReactionId id;\n\tQString title;\n\tint count = 0;\n};\n\nstruct PaidReactionSend {\n\tint count = 0;\n\tbool valid = false;\n\tstd::optional<PeerId> shownPeer = PeerId();\n};\n\nclass Reactions final : private CustomEmojiManager::Listener {\npublic:\n\texplicit Reactions(not_null<Session*> owner);\n\t~Reactions();\n\n\t[[nodiscard]] Session &owner() const {\n\t\treturn *_owner;\n\t}\n\t[[nodiscard]] Main::Session &session() const;\n\n\tvoid refreshTop();\n\tvoid refreshRecent();\n\tvoid refreshRecentDelayed();\n\tvoid refreshDefault();\n\tvoid refreshMyTags(SavedSublist *sublist = nullptr);\n\tvoid refreshMyTagsDelayed();\n\tvoid refreshTags();\n\tvoid refreshEffects();\n\n\tenum class Type {\n\t\tActive,\n\t\tRecent,\n\t\tTop,\n\t\tAll,\n\t\tMyTags,\n\t\tTags,\n\t\tEffects,\n\t};\n\t[[nodiscard]] const std::vector<Reaction> &list(Type type) const;\n\t[[nodiscard]] const std::vector<MyTagInfo> &myTagsInfo() const;\n\t[[nodiscard]] const QString &myTagTitle(const ReactionId &id) const;\n\t[[nodiscard]] ReactionId favoriteId() const;\n\t[[nodiscard]] const Reaction *favorite() const;\n\tvoid setFavorite(const ReactionId &id);\n\tvoid incrementMyTag(const ReactionId &id, SavedSublist *sublist);\n\tvoid decrementMyTag(const ReactionId &id, SavedSublist *sublist);\n\tvoid renameTag(const ReactionId &id, const QString &name);\n\t[[nodiscard]] DocumentData *chooseGenericAnimation(\n\t\tnot_null<DocumentData*> custom) const;\n\t[[nodiscard]] DocumentData *choosePaidReactionAnimation() const;\n\n\t[[nodiscard]] rpl::producer<> topUpdates() const;\n\t[[nodiscard]] rpl::producer<> recentUpdates() const;\n\t[[nodiscard]] rpl::producer<> defaultUpdates() const;\n\t[[nodiscard]] rpl::producer<> favoriteUpdates() const;\n\t[[nodiscard]] rpl::producer<> myTagsUpdates() const;\n\t[[nodiscard]] rpl::producer<> tagsUpdates() const;\n\t[[nodiscard]] rpl::producer<ReactionId> myTagRenamed() const;\n\t[[nodiscard]] rpl::producer<> effectsUpdates() const;\n\n\tvoid preloadReactionImageFor(const ReactionId &emoji);\n\t[[nodiscard]] QImage resolveReactionImageFor(const ReactionId &emoji);\n\n\t// This is used to reserve space for the effect in BottomInfo but not\n\t// actually paint anything, used in case we want to paint icon ourselves.\n\tstatic constexpr auto kFakeEffectId = EffectId(1);\n\n\tvoid preloadEffectImageFor(EffectId id);\n\t[[nodiscard]] QImage resolveEffectImageFor(EffectId id);\n\n\tvoid preloadAnimationsFor(const ReactionId &emoji);\n\n\tvoid send(not_null<HistoryItem*> item, bool addToRecent);\n\t[[nodiscard]] bool sending(not_null<HistoryItem*> item) const;\n\n\tvoid poll(not_null<HistoryItem*> item, crl::time now);\n\n\tvoid updateAllInHistory(not_null<PeerData*> peer, bool enabled);\n\n\tvoid clearTemporary();\n\t[[nodiscard]] Reaction *lookupTemporary(const ReactionId &id);\n\t[[nodiscard]] not_null<Reaction*> lookupPaid();\n\t[[nodiscard]] not_null<DocumentData*> paidToastAnimation();\n\n\t[[nodiscard]] rpl::producer<std::vector<Reaction>> myTagsValue(\n\t\tSavedSublist *sublist = nullptr);\n\n\t[[nodiscard]] bool isQuitPrevent();\n\n\tvoid schedulePaid(not_null<HistoryItem*> item);\n\tvoid undoScheduledPaid(not_null<HistoryItem*> item);\n\t[[nodiscard]] crl::time sendingScheduledPaidAt(\n\t\tnot_null<HistoryItem*> item) const;\n\n\tvoid schedulePaid(not_null<Calls::GroupCall*> call);\n\tvoid undoScheduledPaid(not_null<Calls::GroupCall*> call);\n\t[[nodiscard]] crl::time sendingScheduledPaidAt(\n\t\tnot_null<Calls::GroupCall*> call) const;\n\n\t[[nodiscard]] static crl::time ScheduledPaidDelay();\n\n\t[[nodiscard]] static bool HasUnread(const MTPMessageReactions &data);\n\tstatic void CheckUnknownForUnread(\n\t\tnot_null<Session*> owner,\n\t\tconst MTPMessage &message);\n\nprivate:\n\tstruct ImageSet {\n\t\tQImage image;\n\t\tstd::shared_ptr<DocumentMedia> media;\n\t\tstd::unique_ptr<Ui::AnimatedIcon> icon;\n\t\tbool fromSelectAnimation = false;\n\t\tbool effect = false;\n\t};\n\tstruct TagsBySublist {\n\t\tTagsBySublist() = default;\n\t\tTagsBySublist(TagsBySublist&&) = default;\n\t\tTagsBySublist(const TagsBySublist&) = delete;\n\t\tTagsBySublist &operator=(TagsBySublist&&) = default;\n\t\tTagsBySublist &operator=(const TagsBySublist&) = delete;\n\n\t\tstd::vector<Reaction> tags;\n\t\tstd::vector<MyTagInfo> info;\n\t\tuint64 hash = 0;\n\t\tmtpRequestId requestId = 0;\n\t\tbool requestScheduled = false;\n\t\tbool updateScheduled = false;\n\t};\n\n\t[[nodiscard]] not_null<CustomEmojiManager::Listener*> resolveListener();\n\tvoid customEmojiResolveDone(not_null<DocumentData*> document) override;\n\n\tvoid requestTop();\n\tvoid requestRecent();\n\tvoid requestDefault();\n\tvoid requestGeneric();\n\tvoid requestMyTags(SavedSublist *sublist = nullptr);\n\tvoid requestTags();\n\tvoid requestEffects();\n\n\tvoid updateTop(const MTPDmessages_reactions &data);\n\tvoid updateRecent(const MTPDmessages_reactions &data);\n\tvoid updateDefault(const MTPDmessages_availableReactions &data);\n\tvoid updateGeneric(const MTPDmessages_stickerSet &data);\n\tvoid updateMyTags(\n\t\tSavedSublist *sublist,\n\t\tconst MTPDmessages_savedReactionTags &data);\n\tvoid updateTags(const MTPDmessages_reactions &data);\n\tvoid updateEffects(const MTPDmessages_availableEffects &data);\n\n\tvoid recentUpdated();\n\tvoid defaultUpdated();\n\tvoid myTagsUpdated();\n\tvoid tagsUpdated();\n\tvoid effectsUpdated();\n\n\t[[nodiscard]] std::optional<Reaction> resolveById(const ReactionId &id);\n\t[[nodiscard]] std::vector<Reaction> resolveByIds(\n\t\tconst std::vector<ReactionId> &ids,\n\t\tbase::flat_set<ReactionId> &unresolved);\n\t[[nodiscard]] std::optional<Reaction> resolveByInfo(\n\t\tconst MyTagInfo &info,\n\t\tSavedSublist *sublist);\n\t[[nodiscard]] std::vector<Reaction> resolveByInfos(\n\t\tconst std::vector<MyTagInfo> &infos,\n\t\tbase::flat_map<\n\t\t\tReactionId,\n\t\t\tbase::flat_set<SavedSublist*>> &unresolved,\n\t\tSavedSublist *sublist);\n\tvoid resolve(const ReactionId &id);\n\tvoid applyFavorite(const ReactionId &id);\n\tvoid scheduleMyTagsUpdate(SavedSublist *sublist);\n\n\t[[nodiscard]] std::optional<Reaction> parse(\n\t\tconst MTPAvailableReaction &entry);\n\t[[nodiscard]] std::optional<Reaction> parse(\n\t\tconst MTPAvailableEffect &entry);\n\n\tvoid preloadEffect(const Reaction &effect);\n\tvoid preloadImageFor(const ReactionId &id);\n\t[[nodiscard]] QImage resolveImageFor(const ReactionId &id);\n\tvoid loadImage(\n\t\tImageSet &set,\n\t\tnot_null<DocumentData*> document,\n\t\tbool fromSelectAnimation);\n\tvoid generateImage(ImageSet &set, const QString &emoji);\n\tvoid setAnimatedIcon(ImageSet &set);\n\tvoid resolveReactionImages();\n\tvoid resolveEffectImages();\n\tvoid downloadTaskFinished();\n\n\tvoid fillPaidReactionAnimations() const;\n\t[[nodiscard]] DocumentData *randomLoadedFrom(\n\t\tstd::vector<not_null<DocumentData*>> list) const;\n\n\tvoid repaintCollected();\n\tvoid pollCollected();\n\n\tvoid sendPaid();\n\tbool sendPaid(not_null<HistoryItem*> item);\n\tvoid sendPaidRequest(\n\t\tnot_null<HistoryItem*> item,\n\t\tPaidReactionSend send);\n\tvoid sendPaidPrivacyRequest(\n\t\tnot_null<HistoryItem*> item,\n\t\tPaidReactionSend send);\n\tvoid sendPaidFinish(\n\t\tnot_null<HistoryItem*> item,\n\t\tPaidReactionSend send,\n\t\tbool success);\n\tvoid checkQuitPreventFinished();\n\n\tconst not_null<Session*> _owner;\n\n\tstd::vector<Reaction> _active;\n\tstd::vector<Reaction> _available;\n\tstd::vector<Reaction> _recent;\n\tstd::vector<ReactionId> _recentIds;\n\tbase::flat_set<ReactionId> _unresolvedRecent;\n\tbase::flat_map<SavedSublist*, TagsBySublist> _myTags;\n\tbase::flat_map<\n\t\tReactionId,\n\t\tbase::flat_set<SavedSublist*>> _unresolvedMyTags;\n\tstd::vector<Reaction> _tags;\n\tstd::vector<ReactionId> _tagsIds;\n\tbase::flat_set<ReactionId> _unresolvedTags;\n\tstd::vector<Reaction> _top;\n\tstd::vector<ReactionId> _topIds;\n\tbase::flat_set<ReactionId> _unresolvedTop;\n\tstd::vector<not_null<DocumentData*>> _genericAnimations;\n\tmutable std::vector<not_null<DocumentData*>> _paidReactionAnimations;\n\tstd::vector<Reaction> _effects;\n\tReactionId _favoriteId;\n\tReactionId _unresolvedFavoriteId;\n\tstd::optional<Reaction> _favorite;\n\tbase::flat_map<\n\t\tnot_null<DocumentData*>,\n\t\tstd::shared_ptr<DocumentMedia>> _iconsCache;\n\tbase::flat_map<\n\t\tnot_null<DocumentData*>,\n\t\tstd::shared_ptr<DocumentMedia>> _genericCache;\n\tmutable base::flat_map<\n\t\tnot_null<DocumentData*>,\n\t\tstd::shared_ptr<DocumentMedia>> _paidReactionCache;\n\trpl::event_stream<> _topUpdated;\n\trpl::event_stream<> _recentUpdated;\n\trpl::event_stream<> _defaultUpdated;\n\trpl::event_stream<> _favoriteUpdated;\n\trpl::event_stream<SavedSublist*> _myTagsUpdated;\n\trpl::event_stream<> _tagsUpdated;\n\trpl::event_stream<ReactionId> _myTagRenamed;\n\trpl::event_stream<> _effectsUpdated;\n\n\t// We need &i->second stay valid while inserting new items.\n\t// So we use std::map instead of base::flat_map here.\n\t// Otherwise we could use flat_map<DocumentId, unique_ptr<Reaction>>.\n\tstd::map<DocumentId, Reaction> _temporary;\n\tstd::optional<Reaction> _paid;\n\tDocumentData *_paidToastAnimation = nullptr;\n\n\tbase::Timer _topRefreshTimer;\n\tmtpRequestId _topRequestId = 0;\n\tuint64 _topHash = 0;\n\n\tmtpRequestId _recentRequestId = 0;\n\tbool _recentRequestScheduled = false;\n\tuint64 _recentHash = 0;\n\n\tmtpRequestId _defaultRequestId = 0;\n\tint32 _defaultHash = 0;\n\n\tmtpRequestId _genericRequestId = 0;\n\n\tmtpRequestId _tagsRequestId = 0;\n\tuint64 _tagsHash = 0;\n\n\tmtpRequestId _effectsRequestId = 0;\n\tint32 _effectsHash = 0;\n\n\tbase::flat_map<ReactionId, ImageSet> _images;\n\trpl::lifetime _imagesLoadLifetime;\n\tbool _waitingForReactions = false;\n\tbool _waitingForEffects = false;\n\n\tbase::flat_map<FullMsgId, mtpRequestId> _sentRequests;\n\n\tbase::flat_map<not_null<HistoryItem*>, crl::time> _repaintItems;\n\tbase::Timer _repaintTimer;\n\tbase::flat_set<not_null<HistoryItem*>> _pollItems;\n\tbase::flat_set<not_null<HistoryItem*>> _pollingItems;\n\tmtpRequestId _pollRequestId = 0;\n\n\tbase::flat_map<not_null<HistoryItem*>, crl::time> _sendPaidItems;\n\tbase::flat_map<not_null<HistoryItem*>, mtpRequestId> _sendingPaid;\n\tbase::Timer _sendPaidTimer;\n\n\tbase::flat_map<not_null<Calls::GroupCall*>, crl::time> _sendPaidCalls;\n\n\tmtpRequestId _saveFaveRequestId = 0;\n\n\trpl::lifetime _lifetime;\n\n};\n\nstruct RecentReaction {\n\tnot_null<PeerData*> peer;\n\tbool unread = false;\n\tbool big = false;\n\tbool my = false;\n\n\tfriend inline bool operator==(\n\t\tconst RecentReaction &a,\n\t\tconst RecentReaction &b) = default;\n};\n\nstruct MessageReactionsTopPaid {\n\tPeerData *peer = nullptr;\n\tuint32 count : 30 = 0;\n\tuint32 top : 1 = 0;\n\tuint32 my : 1 = 0;\n\n\tfriend inline bool operator==(\n\t\tconst MessageReactionsTopPaid &a,\n\t\tconst MessageReactionsTopPaid &b) = default;\n};\n\nclass MessageReactions final {\npublic:\n\texplicit MessageReactions(not_null<HistoryItem*> item);\n\t~MessageReactions();\n\n\tusing TopPaid = MessageReactionsTopPaid;\n\n\tvoid add(const ReactionId &id, bool addToRecent);\n\tvoid remove(const ReactionId &id);\n\tbool change(\n\t\tconst QVector<MTPReactionCount> &list,\n\t\tconst QVector<MTPMessagePeerReaction> &recent,\n\t\tconst QVector<MTPMessageReactor> &top,\n\t\tbool min);\n\t[[nodiscard]] bool checkIfChanged(\n\t\tconst QVector<MTPReactionCount> &list,\n\t\tconst QVector<MTPMessagePeerReaction> &recent,\n\t\tbool min) const;\n\t[[nodiscard]] const std::vector<MessageReaction> &list() const;\n\t[[nodiscard]] auto recent() const\n\t\t-> const base::flat_map<ReactionId, std::vector<RecentReaction>> &;\n\t[[nodiscard]] const std::vector<TopPaid> &topPaid() const;\n\t[[nodiscard]] std::vector<ReactionId> chosen() const;\n\t[[nodiscard]] bool empty() const;\n\n\t[[nodiscard]] bool hasUnread() const;\n\tvoid markRead();\n\n\tvoid scheduleSendPaid(int count, std::optional<PeerId> shownPeer);\n\t[[nodiscard]] int scheduledPaid() const;\n\tvoid cancelScheduledPaid();\n\n\t[[nodiscard]] PaidReactionSend startPaidSending();\n\tvoid finishPaidSending(PaidReactionSend send, bool success);\n\n\t[[nodiscard]] bool localPaidData() const;\n\t[[nodiscard]] int localPaidCount() const;\n\t[[nodiscard]] PeerId localPaidShownPeer() const;\n\tbool clearCloudData();\n\nprivate:\n\tstruct Paid {\n\t\tstd::vector<TopPaid> top;\n\t\tPeerId scheduledShownPeer = 0;\n\t\tPeerId sendingShownPeer = 0;\n\t\tuint32 scheduled: 30 = 0;\n\t\tuint32 scheduledFlag : 1 = 0;\n\t\tuint32 scheduledPrivacySet : 1 = 0;\n\t\tuint32 sending : 30 = 0;\n\t\tuint32 sendingFlag : 1 = 0;\n\t\tuint32 sendingPrivacySet : 1 = 0;\n\t};\n\tconst not_null<HistoryItem*> _item;\n\n\tstd::vector<MessageReaction> _list;\n\tbase::flat_map<ReactionId, std::vector<RecentReaction>> _recent;\n\tstd::unique_ptr<Paid> _paid;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_messages.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_messages.h\"\n\nnamespace Data {\n\nMessagesList::Slice::Slice(\n\tbase::flat_set<MessagePosition> &&messages,\n\tMessagesRange range)\n: messages(std::move(messages))\n, range(range) {\n}\n\ntemplate <typename Range>\nvoid MessagesList::Slice::merge(\n\t\tconst Range &moreMessages,\n\t\tMessagesRange moreNoSkipRange) {\n\tExpects(moreNoSkipRange.from <= range.till);\n\tExpects(range.from <= moreNoSkipRange.till);\n\n\tmessages.merge(std::begin(moreMessages), std::end(moreMessages));\n\trange = {\n\t\tqMin(range.from, moreNoSkipRange.from),\n\t\tqMax(range.till, moreNoSkipRange.till)\n\t};\n}\n\ntemplate <typename Range>\nint MessagesList::uniteAndAdd(\n\t\tMessagesSliceUpdate &update,\n\t\tbase::flat_set<Slice>::iterator uniteFrom,\n\t\tbase::flat_set<Slice>::iterator uniteTill,\n\t\tconst Range &messages,\n\t\tMessagesRange noSkipRange) {\n\tauto uniteFromIndex = uniteFrom - _slices.begin();\n\tauto was = uniteFrom->messages.size();\n\t_slices.modify(uniteFrom, [&](Slice &slice) {\n\t\tslice.merge(messages, noSkipRange);\n\t});\n\tauto firstToErase = uniteFrom + 1;\n\tif (firstToErase != uniteTill) {\n\t\tfor (auto it = firstToErase; it != uniteTill; ++it) {\n\t\t\t_slices.modify(uniteFrom, [&](Slice &slice) {\n\t\t\t\tslice.merge(it->messages, it->range);\n\t\t\t});\n\t\t}\n\t\t_slices.erase(firstToErase, uniteTill);\n\t\tuniteFrom = _slices.begin() + uniteFromIndex;\n\t}\n\tupdate.messages = &uniteFrom->messages;\n\tupdate.range = uniteFrom->range;\n\treturn uniteFrom->messages.size() - was;\n}\n\ntemplate <typename Range>\nint MessagesList::addRangeItemsAndCountNew(\n\t\tMessagesSliceUpdate &update,\n\t\tconst Range &messages,\n\t\tMessagesRange noSkipRange) {\n\tExpects(noSkipRange.from <= noSkipRange.till);\n\n\tauto uniteFrom = ranges::lower_bound(\n\t\t_slices,\n\t\tnoSkipRange.from,\n\t\tstd::less<>(),\n\t\t[](const Slice &slice) { return slice.range.till; });\n\tauto uniteTill = ranges::upper_bound(\n\t\t_slices,\n\t\tnoSkipRange.till,\n\t\tstd::less<>(),\n\t\t[](const Slice &slice) { return slice.range.from; });\n\tif (uniteFrom < uniteTill) {\n\t\treturn uniteAndAdd(update, uniteFrom, uniteTill, messages, noSkipRange);\n\t}\n\n\tauto sliceMessages = base::flat_set<MessagePosition> {\n\t\tstd::begin(messages),\n\t\tstd::end(messages) };\n\tauto slice = _slices.emplace(\n\t\tstd::move(sliceMessages),\n\t\tnoSkipRange\n\t).first;\n\tupdate.messages = &slice->messages;\n\tupdate.range = slice->range;\n\treturn slice->messages.size();\n}\n\ntemplate <typename Range>\nvoid MessagesList::addRange(\n\t\tconst Range &messages,\n\t\tMessagesRange noSkipRange,\n\t\tstd::optional<int> count,\n\t\tbool incrementCount) {\n\tExpects(!count || !incrementCount);\n\n\tauto update = MessagesSliceUpdate();\n\tauto result = addRangeItemsAndCountNew(\n\t\tupdate,\n\t\tmessages,\n\t\tnoSkipRange);\n\tif (count) {\n\t\t_count = count;\n\t} else if (incrementCount && _count && result > 0) {\n\t\t*_count += result;\n\t}\n\tif (_slices.size() == 1) {\n\t\tif (_slices.front().range == FullMessagesRange) {\n\t\t\t_count = _slices.front().messages.size();\n\t\t}\n\t}\n\tupdate.count = _count;\n\t_sliceUpdated.fire(std::move(update));\n}\n\nvoid MessagesList::addOne(MessagePosition messageId) {\n\tauto range = { messageId };\n\taddRange(range, { messageId, messageId }, std::nullopt, true);\n}\n\nvoid MessagesList::addNew(MessagePosition messageId) {\n\tauto range = { messageId };\n\taddRange(range, { messageId, MaxMessagePosition }, std::nullopt, true);\n}\n\nvoid MessagesList::addSlice(\n\t\tstd::vector<MessagePosition> &&messageIds,\n\t\tMessagesRange noSkipRange,\n\t\tstd::optional<int> count) {\n\taddRange(messageIds, noSkipRange, count);\n}\n\nvoid MessagesList::removeOne(MessagePosition messageId) {\n\tauto update = MessagesSliceUpdate();\n\tauto slice = ranges::lower_bound(\n\t\t_slices,\n\t\tmessageId,\n\t\tstd::less<>(),\n\t\t[](const Slice &slice) { return slice.range.till; });\n\tif (slice != _slices.end() && slice->range.from <= messageId) {\n\t\t_slices.modify(slice, [&](Slice &slice) {\n\t\t\treturn slice.messages.remove(messageId);\n\t\t});\n\t\tupdate.messages = &slice->messages;\n\t\tupdate.range = slice->range;\n\t}\n\tif (_count) {\n\t\t--*_count;\n\t}\n\tupdate.count = _count;\n\tif (update.messages) {\n\t\t_sliceUpdated.fire(std::move(update));\n\t}\n}\n\nvoid MessagesList::removeLessThan(MessagePosition messageId) {\n\tauto removed = 0;\n\tfor (auto i = begin(_slices); i != end(_slices);) {\n\t\tif (i->range.till <= messageId) {\n\t\t\tremoved += i->messages.size();\n\t\t\ti = _slices.erase(i);\n\t\t\tcontinue;\n\t\t} else if (i->range.from <= messageId) {\n\t\t\t_slices.modify(i, [&](Slice &slice) {\n\t\t\t\tslice.range.from = MinMessagePosition;\n\t\t\t\tauto from = begin(slice.messages);\n\t\t\t\tauto till = ranges::lower_bound(slice.messages, messageId);\n\t\t\t\tif (from != till) {\n\t\t\t\t\tremoved += till - from;\n\t\t\t\t\tslice.messages.erase(from, till);\n\t\t\t\t}\n\t\t\t});\n\t\t\tbreak;\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (removed && _count) {\n\t\t*_count -= removed;\n\t}\n}\n\nvoid MessagesList::invalidate() {\n\t_slices.clear();\n\t_count = std::nullopt;\n}\n\nvoid MessagesList::invalidateBottom() {\n\tif (!_slices.empty()) {\n\t\tconst auto &last = _slices.back();\n\t\tif (last.range.till == MaxMessagePosition) {\n\t\t\t_slices.modify(_slices.end() - 1, [](Slice &slice) {\n\t\t\t\tslice.range.till = slice.messages.empty()\n\t\t\t\t\t? slice.range.from\n\t\t\t\t\t: slice.messages.back();\n\t\t\t});\n\t\t}\n\t}\n\t_count = std::nullopt;\n}\n\nMessagesResult MessagesList::queryCurrent(const MessagesQuery &query) const {\n\tif (!query.aroundId) {\n\t\treturn MessagesResult();\n\t}\n\tconst auto slice = ranges::lower_bound(\n\t\t_slices,\n\t\tquery.aroundId,\n\t\tstd::less<>(),\n\t\t[](const Slice &slice) { return slice.range.till; });\n\treturn (slice != _slices.end() && slice->range.from <= query.aroundId)\n\t\t? queryFromSlice(query, *slice)\n\t\t: MessagesResult();\n}\n\nrpl::producer<MessagesResult> MessagesList::query(\n\t\tMessagesQuery &&query) const {\n\treturn [this, query = std::move(query)](auto consumer) {\n\t\tauto current = queryCurrent(query);\n\t\tif (current.count.has_value() || !current.messageIds.empty()) {\n\t\t\tconsumer.put_next(std::move(current));\n\t\t}\n\t\tconsumer.put_done();\n\t\treturn rpl::lifetime();\n\t};\n}\n\nrpl::producer<MessagesSliceUpdate> MessagesList::sliceUpdated() const {\n\treturn _sliceUpdated.events();\n}\n\nMessagesResult MessagesList::snapshot(MessagesQuery &&query) const {\n\treturn queryCurrent(query);\n}\n\nbool MessagesList::empty() const {\n\tfor (const auto &slice : _slices) {\n\t\tif (!slice.messages.empty()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nrpl::producer<MessagesResult> MessagesList::viewer(\n\t\tMessagesQuery &&query) const {\n\treturn rpl::single(\n\t\tqueryCurrent(query)\n\t) | rpl::then(sliceUpdated() | rpl::map([=] {\n\t\treturn queryCurrent(query);\n\t})) | rpl::filter([=](const MessagesResult &value) {\n\t\treturn value.count.has_value() || !value.messageIds.empty();\n\t});\n}\n\nMessagesResult MessagesList::queryFromSlice(\n\t\tconst MessagesQuery &query,\n\t\tconst Slice &slice) const {\n\tauto result = MessagesResult {};\n\tauto position = ranges::lower_bound(slice.messages, query.aroundId);\n\tauto haveBefore = int(position - begin(slice.messages));\n\tauto haveEqualOrAfter = int(end(slice.messages) - position);\n\tauto before = qMin(haveBefore, query.limitBefore);\n\tauto equalOrAfter = qMin(haveEqualOrAfter, query.limitAfter + 1);\n\tauto ids = std::vector<MessagePosition>(position - before, position + equalOrAfter);\n\tresult.messageIds.merge(ids.begin(), ids.end());\n\tif (slice.range.from == MinMessagePosition) {\n\t\tresult.skippedBefore = haveBefore - before;\n\t}\n\tif (slice.range.till == MaxMessagePosition) {\n\t\tresult.skippedAfter = haveEqualOrAfter - equalOrAfter;\n\t}\n\tif (_count) {\n\t\tresult.count = _count;\n\t\tif (!result.skippedBefore && result.skippedAfter) {\n\t\t\tresult.skippedBefore = *result.count\n\t\t\t\t- *result.skippedAfter\n\t\t\t\t- int(result.messageIds.size());\n\t\t} else if (!result.skippedAfter && result.skippedBefore) {\n\t\t\tresult.skippedAfter = *result.count\n\t\t\t\t- *result.skippedBefore\n\t\t\t\t- int(result.messageIds.size());\n\t\t}\n\t}\n\treturn result;\n}\n\nMessagesSliceBuilder::MessagesSliceBuilder(\n\tKey key,\n\tint limitBefore,\n\tint limitAfter)\n: _key(key)\n, _limitBefore(limitBefore)\n, _limitAfter(limitAfter) {\n}\n\nbool MessagesSliceBuilder::applyInitial(const MessagesResult &result) {\n\tmergeSliceData(\n\t\tresult.count,\n\t\tresult.messageIds,\n\t\tresult.skippedBefore,\n\t\tresult.skippedAfter);\n\treturn true;\n}\n\nbool MessagesSliceBuilder::applyUpdate(const MessagesSliceUpdate &update) {\n\tauto intersects = [](MessagesRange range1, MessagesRange range2) {\n\t\treturn (range1.from <= range2.till)\n\t\t\t&& (range2.from <= range1.till);\n\t};\n\tauto needMergeMessages = (update.messages != nullptr)\n\t\t&& intersects(update.range, {\n\t\t\t_ids.empty() ? _key : _ids.front(),\n\t\t\t_ids.empty() ? _key : _ids.back()\n\t\t});\n\tif (!needMergeMessages && !update.count) {\n\t\treturn false;\n\t}\n\tauto skippedBefore = (update.range.from == MinMessagePosition)\n\t\t? 0\n\t\t: std::optional<int> {};\n\tauto skippedAfter = (update.range.till == MaxMessagePosition)\n\t\t? 0\n\t\t: std::optional<int> {};\n\tmergeSliceData(\n\t\tupdate.count,\n\t\tneedMergeMessages\n\t\t\t? *update.messages\n\t\t\t: base::flat_set<MessagePosition> {},\n\t\tskippedBefore,\n\t\tskippedAfter);\n\treturn true;\n}\n\nbool MessagesSliceBuilder::removeOne(MessagePosition messageId) {\n\tauto changed = false;\n\tif (_fullCount && *_fullCount > 0) {\n\t\t--*_fullCount;\n\t\tchanged = true;\n\t}\n\tif (_ids.contains(messageId)) {\n\t\t_ids.remove(messageId);\n\t\tchanged = true;\n\t} else if (!_ids.empty()) {\n\t\tif (_ids.front() > messageId\n\t\t\t&& _skippedBefore\n\t\t\t&& *_skippedBefore > 0) {\n\t\t\t--*_skippedBefore;\n\t\t\tchanged = true;\n\t\t} else if (_ids.back() < messageId\n\t\t\t&& _skippedAfter\n\t\t\t&& *_skippedAfter > 0) {\n\t\t\t--*_skippedAfter;\n\t\t\tchanged = true;\n\t\t}\n\t}\n\treturn changed;\n}\n\nbool MessagesSliceBuilder::removeAll() {\n\t_ids = {};\n\t_range = FullMessagesRange;\n\t_fullCount = 0;\n\t_skippedBefore = 0;\n\t_skippedAfter = 0;\n\treturn true;\n}\n\nbool MessagesSliceBuilder::invalidated() {\n\t_fullCount = _skippedBefore = _skippedAfter = std::nullopt;\n\t_ids.clear();\n\tcheckInsufficient();\n\treturn false;\n}\n\nbool MessagesSliceBuilder::bottomInvalidated() {\n\t_fullCount = _skippedAfter = std::nullopt;\n\tcheckInsufficient();\n\treturn true;\n}\n\nvoid MessagesSliceBuilder::checkInsufficient() {\n\tsliceToLimits();\n}\n\nvoid MessagesSliceBuilder::mergeSliceData(\n\t\tstd::optional<int> count,\n\t\tconst base::flat_set<MessagePosition> &messageIds,\n\t\tstd::optional<int> skippedBefore,\n\t\tstd::optional<int> skippedAfter) {\n\tif (messageIds.empty()) {\n\t\tif (count && _fullCount != count) {\n\t\t\t_fullCount = count;\n\t\t\tif (*_fullCount <= _ids.size()) {\n\t\t\t\t_fullCount = _ids.size();\n\t\t\t\t_skippedBefore = _skippedAfter = 0;\n\t\t\t}\n\t\t}\n\t\tfillSkippedAndSliceToLimits();\n\t\treturn;\n\t}\n\tif (count) {\n\t\t_fullCount = count;\n\t}\n\tconst auto impossible = MessagePosition{ .fullId = {}, .date = -1 };\n\tauto wasMinId = _ids.empty() ? impossible : _ids.front();\n\tauto wasMaxId = _ids.empty() ? impossible : _ids.back();\n\t_ids.merge(messageIds.begin(), messageIds.end());\n\n\tauto adjustSkippedBefore = [&](MessagePosition oldId, int oldSkippedBefore) {\n\t\tauto it = _ids.find(oldId);\n\t\tAssert(it != _ids.end());\n\t\t_skippedBefore = oldSkippedBefore - (it - _ids.begin());\n\t\taccumulate_max(*_skippedBefore, 0);\n\t};\n\tif (skippedBefore) {\n\t\tadjustSkippedBefore(messageIds.front(), *skippedBefore);\n\t} else if (wasMinId != impossible && _skippedBefore) {\n\t\tadjustSkippedBefore(wasMinId, *_skippedBefore);\n\t} else {\n\t\t_skippedBefore = std::nullopt;\n\t}\n\n\tauto adjustSkippedAfter = [&](MessagePosition oldId, int oldSkippedAfter) {\n\t\tauto it = _ids.find(oldId);\n\t\tAssert(it != _ids.end());\n\t\t_skippedAfter = oldSkippedAfter - (_ids.end() - it - 1);\n\t\taccumulate_max(*_skippedAfter, 0);\n\t};\n\tif (skippedAfter) {\n\t\tadjustSkippedAfter(messageIds.back(), *skippedAfter);\n\t} else if (wasMaxId != impossible && _skippedAfter) {\n\t\tadjustSkippedAfter(wasMaxId, *_skippedAfter);\n\t} else {\n\t\t_skippedAfter = std::nullopt;\n\t}\n\tfillSkippedAndSliceToLimits();\n}\n\nvoid MessagesSliceBuilder::fillSkippedAndSliceToLimits() {\n\tif (_fullCount) {\n\t\tif (_skippedBefore && !_skippedAfter) {\n\t\t\t_skippedAfter = *_fullCount\n\t\t\t\t- *_skippedBefore\n\t\t\t\t- int(_ids.size());\n\t\t} else if (_skippedAfter && !_skippedBefore) {\n\t\t\t_skippedBefore = *_fullCount\n\t\t\t\t- *_skippedAfter\n\t\t\t\t- int(_ids.size());\n\t\t}\n\t}\n\tsliceToLimits();\n}\n\nvoid MessagesSliceBuilder::sliceToLimits() {\n\tif (!_key) {\n\t\tif (!_fullCount) {\n\t\t\trequestMessagesCount();\n\t\t}\n\t\treturn;\n\t}\n\tauto requestedSomething = false;\n\tauto aroundIt = ranges::lower_bound(_ids, _key);\n\tauto removeFromBegin = (aroundIt - _ids.begin() - _limitBefore);\n\tauto removeFromEnd = (_ids.end() - aroundIt - _limitAfter - 1);\n\tif (removeFromBegin > 0) {\n\t\t_ids.erase(_ids.begin(), _ids.begin() + removeFromBegin);\n\t\tif (_skippedBefore) {\n\t\t\t*_skippedBefore += removeFromBegin;\n\t\t}\n\t} else if (removeFromBegin < 0\n\t\t&& (!_skippedBefore || *_skippedBefore > 0)) {\n\t\trequestedSomething = true;\n\t\trequestMessages(RequestDirection::Before);\n\t}\n\tif (removeFromEnd > 0) {\n\t\t_ids.erase(_ids.end() - removeFromEnd, _ids.end());\n\t\tif (_skippedAfter) {\n\t\t\t*_skippedAfter += removeFromEnd;\n\t\t}\n\t} else if (removeFromEnd < 0\n\t\t&& (!_skippedAfter || *_skippedAfter > 0)) {\n\t\trequestedSomething = true;\n\t\trequestMessages(RequestDirection::After);\n\t}\n\tif (!_fullCount && !requestedSomething) {\n\t\trequestMessagesCount();\n\t}\n}\n\nvoid MessagesSliceBuilder::requestMessages(RequestDirection direction) {\n\tauto requestAroundData = [&]() -> AroundData {\n\t\tif (_ids.empty()) {\n\t\t\treturn { _key, Data::LoadDirection::Around };\n\t\t} else if (direction == RequestDirection::Before) {\n\t\t\treturn { _ids.front(), Data::LoadDirection::Before };\n\t\t}\n\t\treturn { _ids.back(), Data::LoadDirection::After };\n\t};\n\t_insufficientAround.fire(requestAroundData());\n}\n\nvoid MessagesSliceBuilder::requestMessagesCount() {\n\t_insufficientAround.fire({\n\t\tMessagePosition(),\n\t\tData::LoadDirection::Around });\n}\n\nMessagesSlice MessagesSliceBuilder::snapshot() const {\n\tauto result = MessagesSlice();\n\tresult.ids.reserve(_ids.size());\n\tauto nearestToAround = std::optional<FullMsgId>();\n\tfor (const auto &position : _ids) {\n\t\tresult.ids.push_back(position.fullId);\n\t\tif (!nearestToAround && position >= _key) {\n\t\t\tnearestToAround = position.fullId;\n\t\t}\n\t}\n\tresult.nearestToAround = nearestToAround.value_or(\n\t\t_ids.empty() ? FullMsgId() : _ids.back().fullId);\n\tresult.skippedBefore = _skippedBefore;\n\tresult.skippedAfter = _skippedAfter;\n\tresult.fullCount = _fullCount;\n\treturn result;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_messages.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\nenum class LoadDirection : char {\n\tAround,\n\tBefore,\n\tAfter,\n};\n\nstruct MessagePosition {\n\tFullMsgId fullId;\n\tTimeId date = 0;\n\n\texplicit operator bool() const {\n\t\treturn (fullId.msg != 0);\n\t}\n\n\tinline constexpr bool operator<(const MessagePosition &other) const {\n\t\tif (date < other.date) {\n\t\t\treturn true;\n\t\t} else if (other.date < date) {\n\t\t\treturn false;\n\t\t}\n\t\treturn (fullId < other.fullId);\n\t}\n\tinline constexpr bool operator>(const MessagePosition &other) const {\n\t\treturn other < *this;\n\t}\n\tinline constexpr bool operator<=(const MessagePosition &other) const {\n\t\treturn !(other < *this);\n\t}\n\tinline constexpr bool operator>=(const MessagePosition &other) const {\n\t\treturn !(*this < other);\n\t}\n\tinline constexpr bool operator==(const MessagePosition &other) const {\n\t\treturn (date == other.date)\n\t\t\t&& (fullId == other.fullId);\n\t}\n\tinline constexpr bool operator!=(const MessagePosition &other) const {\n\t\treturn !(*this == other);\n\t}\n};\n\nstruct MessagesRange {\n\tMessagePosition from;\n\tMessagePosition till;\n\n\tinline constexpr bool operator==(const MessagesRange &other) const {\n\t\treturn (from == other.from)\n\t\t\t&& (till == other.till);\n\t}\n\tinline constexpr bool operator!=(const MessagesRange &other) const {\n\t\treturn !(*this == other);\n\t}\n};\n\nconstexpr auto MinDate = TimeId(0);\nconstexpr auto MaxDate = std::numeric_limits<TimeId>::max();\nconstexpr auto MinMessagePosition = MessagePosition{\n\t.fullId = FullMsgId(PeerId(), 1),\n\t.date = MinDate,\n};\nconstexpr auto MaxMessagePosition = MessagePosition{\n\t.fullId = FullMsgId(PeerId(), ServerMaxMsgId - 1),\n\t.date = MaxDate,\n};\nconstexpr auto FullMessagesRange = MessagesRange{\n\t.from = MinMessagePosition,\n\t.till = MaxMessagePosition,\n};\nconstexpr auto UnreadMessagePosition = MessagePosition{\n\t.fullId = FullMsgId(PeerId(), ShowAtUnreadMsgId),\n\t.date = MinDate,\n};\n\nstruct MessagesSlice {\n\tstd::vector<FullMsgId> ids;\n\tFullMsgId nearestToAround;\n\tstd::optional<int> skippedBefore;\n\tstd::optional<int> skippedAfter;\n\tstd::optional<int> fullCount;\n};\n\nstruct MessagesQuery {\n\tMessagePosition aroundId;\n\tint limitBefore = 0;\n\tint limitAfter = 0;\n};\n\nstruct MessagesResult {\n\tstd::optional<int> count;\n\tstd::optional<int> skippedBefore;\n\tstd::optional<int> skippedAfter;\n\tbase::flat_set<MessagePosition> messageIds;\n};\n\nstruct MessagesSliceUpdate {\n\tconst base::flat_set<MessagePosition> *messages = nullptr;\n\tMessagesRange range;\n\tstd::optional<int> count;\n};\n\nclass MessagesList {\npublic:\n\tvoid addOne(MessagePosition messageId);\n\tvoid addNew(MessagePosition messageId);\n\tvoid addSlice(\n\t\tstd::vector<MessagePosition> &&messageIds,\n\t\tMessagesRange noSkipRange,\n\t\tstd::optional<int> count);\n\tvoid removeOne(MessagePosition messageId);\n\tvoid removeLessThan(MessagePosition messageId);\n\tvoid invalidate();\n\tvoid invalidateBottom();\n\t[[nodiscard]] rpl::producer<MessagesResult> query(\n\t\tMessagesQuery &&query) const;\n\t[[nodiscard]] rpl::producer<MessagesSliceUpdate> sliceUpdated() const;\n\n\t[[nodiscard]] MessagesResult snapshot(MessagesQuery &&query) const;\n\t[[nodiscard]] rpl::producer<MessagesResult> viewer(\n\t\tMessagesQuery &&query) const;\n\n\t[[nodiscard]] bool empty() const;\n\nprivate:\n\tstruct Slice {\n\t\tSlice(\n\t\t\tbase::flat_set<MessagePosition> &&messages,\n\t\t\tMessagesRange range);\n\n\t\ttemplate <typename Range>\n\t\tvoid merge(\n\t\t\tconst Range &moreMessages,\n\t\t\tMessagesRange moreNoSkipRange);\n\n\t\tbase::flat_set<MessagePosition> messages;\n\t\tMessagesRange range;\n\n\t\tinline bool operator<(const Slice &other) const {\n\t\t\treturn range.from < other.range.from;\n\t\t}\n\n\t};\n\n\ttemplate <typename Range>\n\tint uniteAndAdd(\n\t\tMessagesSliceUpdate &update,\n\t\tbase::flat_set<Slice>::iterator uniteFrom,\n\t\tbase::flat_set<Slice>::iterator uniteTill,\n\t\tconst Range &messages,\n\t\tMessagesRange noSkipRange);\n\ttemplate <typename Range>\n\tint addRangeItemsAndCountNew(\n\t\tMessagesSliceUpdate &update,\n\t\tconst Range &messages,\n\t\tMessagesRange noSkipRange);\n\ttemplate <typename Range>\n\tvoid addRange(\n\t\tconst Range &messages,\n\t\tMessagesRange noSkipRange,\n\t\tstd::optional<int> count,\n\t\tbool incrementCount = false);\n\n\tMessagesResult queryFromSlice(\n\t\tconst MessagesQuery &query,\n\t\tconst Slice &slice) const;\n\tMessagesResult queryCurrent(const MessagesQuery &query) const;\n\n\tstd::optional<int> _count;\n\tbase::flat_set<Slice> _slices;\n\n\trpl::event_stream<MessagesSliceUpdate> _sliceUpdated;\n\n};\n\nclass MessagesSliceBuilder {\npublic:\n\tusing Key = MessagePosition;\n\n\tMessagesSliceBuilder(Key key, int limitBefore, int limitAfter);\n\n\tbool applyInitial(const MessagesResult &result);\n\tbool applyUpdate(const MessagesSliceUpdate &update);\n\tbool removeOne(MessagePosition messageId);\n\tbool removeAll();\n\tbool invalidated();\n\tbool bottomInvalidated();\n\n\tvoid checkInsufficient();\n\tstruct AroundData {\n\t\tMessagePosition aroundId;\n\t\tLoadDirection direction = LoadDirection::Around;\n\n\t\tinline bool operator<(const AroundData &other) const {\n\t\t\treturn (aroundId < other.aroundId)\n\t\t\t\t|| ((aroundId == other.aroundId)\n\t\t\t\t\t&& (direction < other.direction));\n\t\t}\n\t};\n\tauto insufficientAround() const {\n\t\treturn _insufficientAround.events();\n\t}\n\n\tMessagesSlice snapshot() const;\n\nprivate:\n\tenum class RequestDirection {\n\t\tBefore,\n\t\tAfter,\n\t};\n\tvoid requestMessages(RequestDirection direction);\n\tvoid requestMessagesCount();\n\tvoid fillSkippedAndSliceToLimits();\n\tvoid sliceToLimits();\n\n\tvoid mergeSliceData(\n\t\tstd::optional<int> count,\n\t\tconst base::flat_set<MessagePosition> &messageIds,\n\t\tstd::optional<int> skippedBefore = std::nullopt,\n\t\tstd::optional<int> skippedAfter = std::nullopt);\n\n\tMessagePosition _key;\n\tbase::flat_set<MessagePosition> _ids;\n\tMessagesRange _range;\n\tstd::optional<int> _fullCount;\n\tstd::optional<int> _skippedBefore;\n\tstd::optional<int> _skippedAfter;\n\tint _limitBefore = 0;\n\tint _limitAfter = 0;\n\n\trpl::event_stream<AroundData> _insufficientAround;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_msg_id.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/qt/qt_compare.h\"\n#include \"data/data_peer_id.h\"\n#include \"ui/text/text_entity.h\"\n\nstruct MsgId {\n\tconstexpr MsgId() noexcept = default;\n\tconstexpr MsgId(int64 value) noexcept : bare(value) {\n\t}\n\n\tfriend inline constexpr auto operator<=>(MsgId, MsgId) = default;\n\n\t[[nodiscard]] constexpr explicit operator bool() const noexcept {\n\t\treturn (bare != 0);\n\t}\n\t[[nodiscard]] constexpr bool operator!() const noexcept {\n\t\treturn !bare;\n\t}\n\t[[nodiscard]] constexpr MsgId operator-() const noexcept {\n\t\treturn -bare;\n\t}\n\tconstexpr MsgId operator++() noexcept {\n\t\treturn ++bare;\n\t}\n\tconstexpr MsgId operator++(int) noexcept {\n\t\treturn bare++;\n\t}\n\tconstexpr MsgId operator--() noexcept {\n\t\treturn --bare;\n\t}\n\tconstexpr MsgId operator--(int) noexcept {\n\t\treturn bare--;\n\t}\n\n\tint64 bare = 0;\n};\n\nQ_DECLARE_METATYPE(MsgId);\n\n[[nodiscard]] inline constexpr MsgId operator+(MsgId a, MsgId b) noexcept {\n\treturn MsgId(a.bare + b.bare);\n}\n\n[[nodiscard]] inline constexpr MsgId operator-(MsgId a, MsgId b) noexcept {\n\treturn MsgId(a.bare - b.bare);\n}\n\nusing StoryId = int32;\nusing BusinessShortcutId = int32;\n\nstruct FullStoryId {\n\tPeerId peer = 0;\n\tStoryId story = 0;\n\n\t[[nodiscard]] bool valid() const {\n\t\treturn peer != 0 && story != 0;\n\t}\n\texplicit operator bool() const {\n\t\treturn valid();\n\t}\n\tfriend inline auto operator<=>(FullStoryId, FullStoryId) = default;\n\tfriend inline bool operator==(FullStoryId, FullStoryId) = default;\n};\n\nconstexpr auto StartClientMsgId = MsgId(0x01 - (1LL << 58));\nconstexpr auto ClientMsgIds = (1LL << 31);\nconstexpr auto EndClientMsgId = MsgId(StartClientMsgId.bare + ClientMsgIds);\nconstexpr auto StartStoryMsgId = MsgId(EndClientMsgId.bare + 1);\nconstexpr auto ServerMaxStoryId = StoryId(1 << 30);\nconstexpr auto StoryMsgIds = int64(ServerMaxStoryId);\nconstexpr auto EndStoryMsgId = MsgId(StartStoryMsgId.bare + StoryMsgIds);\nconstexpr auto ServerMaxMsgId = MsgId(1LL << 56);\nconstexpr auto ScheduledMaxMsgId = MsgId(ServerMaxMsgId + (1LL << 32));\nconstexpr auto ShortcutMaxMsgId = MsgId(ScheduledMaxMsgId + (1LL << 32));\nconstexpr auto ShowAtUnreadMsgId = MsgId(0);\n\nconstexpr auto SpecialMsgIdShift = EndStoryMsgId.bare;\nconstexpr auto ShowAtTheEndMsgId = MsgId(SpecialMsgIdShift + 1);\nconstexpr auto SwitchAtTopMsgId = MsgId(SpecialMsgIdShift + 2);\nconstexpr auto ShowAndStartBotMsgId = MsgId(SpecialMsgIdShift + 4);\nconstexpr auto ShowAndMaybeStartBotMsgId = MsgId(SpecialMsgIdShift + 5);\nconstexpr auto ShowForChooseMessagesMsgId = MsgId(SpecialMsgIdShift + 6);\nconstexpr auto kSearchQueryOffsetHint = -1;\n\nstatic_assert(SpecialMsgIdShift + 0xFF < 0);\nstatic_assert(-(SpecialMsgIdShift + 0xFF) > ServerMaxMsgId);\n\n[[nodiscard]] constexpr inline bool IsClientMsgId(MsgId id) noexcept {\n\treturn (id >= StartClientMsgId && id < EndClientMsgId);\n}\n[[nodiscard]] constexpr inline int32 ClientMsgIndex(MsgId id) noexcept {\n\tExpects(IsClientMsgId(id));\n\n\treturn int(id.bare - StartClientMsgId.bare);\n}\n[[nodiscard]] constexpr inline MsgId ClientMsgByIndex(int32 index) noexcept {\n\tExpects(index >= 0);\n\n\treturn MsgId(StartClientMsgId.bare + index);\n}\n\n[[nodiscard]] constexpr inline bool IsStoryMsgId(MsgId id) noexcept {\n\treturn (id >= StartStoryMsgId && id < EndStoryMsgId);\n}\n[[nodiscard]] constexpr inline StoryId StoryIdFromMsgId(MsgId id) noexcept {\n\tExpects(IsStoryMsgId(id));\n\n\treturn StoryId(id.bare - StartStoryMsgId.bare);\n}\n[[nodiscard]] constexpr inline MsgId StoryIdToMsgId(StoryId id) noexcept {\n\tExpects(id >= 0);\n\n\treturn MsgId(StartStoryMsgId.bare + id);\n}\n\n[[nodiscard]] constexpr inline bool IsServerMsgId(MsgId id) noexcept {\n\treturn (id > 0 && id < ServerMaxMsgId);\n}\n\nstruct MsgRange {\n\tconstexpr MsgRange() noexcept = default;\n\tconstexpr MsgRange(MsgId from, MsgId till) noexcept\n\t: from(from)\n\t, till(till) {\n\t}\n\n\tfriend inline constexpr bool operator==(MsgRange, MsgRange) = default;\n\n\tMsgId from = 0;\n\tMsgId till = 0;\n};\n\nstruct FullMsgId {\n\tconstexpr FullMsgId() noexcept = default;\n\tconstexpr FullMsgId(PeerId peer, MsgId msg) noexcept\n\t: peer(peer), msg(msg) {\n\t}\n\tFullMsgId(ChannelId channelId, MsgId msgId) = delete;\n\n\tfriend inline constexpr auto operator<=>(FullMsgId, FullMsgId) = default;\n\n\tconstexpr explicit operator bool() const noexcept {\n\t\treturn msg != 0;\n\t}\n\tconstexpr bool operator!() const noexcept {\n\t\treturn msg == 0;\n\t}\n\n\tPeerId peer = 0;\n\tMsgId msg = 0;\n};\n\n#ifdef _DEBUG\ninline QDebug operator<<(QDebug debug, const FullMsgId &fullMsgId) {\n\tdebug.nospace()\n\t\t<< \"FullMsgId(peer: \"\n\t\t<< fullMsgId.peer.value\n\t\t<< \", msg: \"\n\t\t<< fullMsgId.msg.bare\n\t\t<< \")\";\n\treturn debug;\n}\n#endif // _DEBUG\n\nQ_DECLARE_METATYPE(FullMsgId);\n\nstruct MessageHighlightId {\n\tTextWithEntities quote;\n\tint quoteOffset = 0;\n\tint todoItemId = 0;\n\tQByteArray pollOption;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn quote.empty() && !todoItemId && pollOption.isEmpty();\n\t}\n\t[[nodiscard]] friend inline bool operator==(\n\t\tconst MessageHighlightId &a,\n\t\tconst MessageHighlightId &b) = default;\n};\n\nstruct FullReplyTo {\n\tFullMsgId messageId;\n\tTextWithEntities quote;\n\tFullStoryId storyId;\n\tMsgId topicRootId = 0;\n\tPeerId monoforumPeerId = 0;\n\tint quoteOffset = 0;\n\tint todoItemId = 0;\n\tQByteArray pollOption;\n\n\t[[nodiscard]] MessageHighlightId highlight() const {\n\t\treturn { quote, quoteOffset, todoItemId, pollOption };\n\t}\n\t[[nodiscard]] bool replying() const {\n\t\treturn messageId || (storyId && storyId.peer);\n\t}\n\texplicit operator bool() const {\n\t\treturn replying() || monoforumPeerId;\n\t}\n\tfriend inline auto operator<=>(FullReplyTo, FullReplyTo) = default;\n\tfriend inline bool operator==(FullReplyTo, FullReplyTo) = default;\n};\n\nstruct SuggestOptions {\n\tuint32 exists : 1 = 0;\n\tuint32 priceWhole : 31 = 0;\n\tuint32 priceNano : 31 = 0;\n\tuint32 ton : 1 = 0;\n\tTimeId date = 0;\n\tTimeId offerDuration = 0;\n\n\t[[nodiscard]] CreditsAmount price() const {\n\t\treturn CreditsAmount(\n\t\t\tpriceWhole,\n\t\t\tpriceNano,\n\t\t\tton ? CreditsType::Ton : CreditsType::Stars);\n\t}\n\n\texplicit operator bool() const {\n\t\treturn exists != 0;\n\t}\n\n\tfriend inline auto operator<=>(\n\t\tSuggestOptions,\n\t\tSuggestOptions) = default;\n\tfriend inline bool operator==(\n\t\tSuggestOptions,\n\t\tSuggestOptions) = default;\n};\n\nstruct GlobalMsgId {\n\tFullMsgId itemId;\n\tuint64 sessionUniqueId = 0;\n\n\tfriend inline constexpr auto operator<=>(\n\t\tGlobalMsgId,\n\t\tGlobalMsgId) = default;\n\n\tconstexpr explicit operator bool() const noexcept {\n\t\treturn itemId && sessionUniqueId;\n\t}\n\tconstexpr bool operator!() const noexcept {\n\t\treturn !itemId || !sessionUniqueId;\n\t}\n};\n\nnamespace std {\n\ntemplate <>\nstruct hash<MsgId> : private hash<int64> {\n\tsize_t operator()(MsgId value) const noexcept {\n\t\treturn hash<int64>::operator()(value.bare);\n\t}\n};\n\ntemplate <>\nstruct hash<FullStoryId> {\n\tsize_t operator()(FullStoryId value) const {\n\t\treturn QtPrivate::QHashCombine().operator()(\n\t\t\tstd::hash<BareId>()(value.peer.value),\n\t\t\tvalue.story);\n\t}\n};\n\ntemplate <>\nstruct hash<FullMsgId> {\n\tsize_t operator()(FullMsgId value) const {\n\t\treturn QtPrivate::QHashCombine().operator()(\n\t\t\tstd::hash<BareId>()(value.peer.value),\n\t\t\tvalue.msg.bare);\n\t}\n};\n\n} // namespace std\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_passkey_deserialize.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\n#include \"data/data_passkey_deserialize.h\"\n\n#include <QJsonDocument>\n#include <QJsonObject>\n#include <QJsonArray>\n\nnamespace Data::Passkey {\nnamespace {\n\n[[nodiscard]] std::string SerializeClientData(\n\t\tconst QByteArray &challenge,\n\t\tconst QString &type) {\n\tauto obj = QJsonObject();\n\tobj[\"type\"] = type;\n\tobj[\"challenge\"] = QString::fromUtf8(\n\t\tchallenge.toBase64(QByteArray::Base64UrlEncoding\n\t\t\t| QByteArray::OmitTrailingEquals));\n\tobj[\"origin\"] = \"https://telegram.org\";\n\tobj[\"crossOrigin\"] = false;\n\treturn QJsonDocument(obj).toJson(QJsonDocument::Compact).toStdString();\n}\n\n} // namespace\n\nstd::optional<RegisterData> DeserializeRegisterData(\n\t\tconst QByteArray &jsonData) {\n\tauto doc = QJsonDocument::fromJson(jsonData);\n\tif (!doc.isObject()) {\n\t\treturn std::nullopt;\n\t}\n\n\tauto root = doc.object();\n\tauto publicKey = root[\"publicKey\"].toObject();\n\tif (publicKey.isEmpty()) {\n\t\treturn std::nullopt;\n\t}\n\n\tauto data = RegisterData();\n\n\tauto rp = publicKey[\"rp\"].toObject();\n\tdata.rp.id = rp[\"id\"].toString();\n\tdata.rp.name = rp[\"name\"].toString();\n\n\tauto user = publicKey[\"user\"].toObject();\n\tdata.user.id = QByteArray::fromBase64(\n\t\tuser[\"id\"].toString().toUtf8());\n\tdata.user.name = user[\"name\"].toString();\n\tdata.user.displayName = user[\"displayName\"].toString();\n\n\tdata.challenge = QByteArray::fromBase64(\n\t\tpublicKey[\"challenge\"].toString().toUtf8(),\n\t\tQByteArray::Base64UrlEncoding);\n\n\tauto params = publicKey[\"pubKeyCredParams\"].toArray();\n\tfor (const auto &param : params) {\n\t\tauto obj = param.toObject();\n\t\tCredentialParameter cp;\n\t\tcp.type = obj[\"type\"].toString();\n\t\tcp.alg = obj[\"alg\"].toInt();\n\t\tdata.pubKeyCredParams.push_back(cp);\n\t}\n\n\tdata.timeout = publicKey[\"timeout\"].toInt(60000);\n\n\treturn data;\n}\n\nstd::string SerializeClientDataCreate(const QByteArray &challenge) {\n\treturn SerializeClientData(challenge, \"webauthn.create\");\n}\n\nstd::string SerializeClientDataGet(const QByteArray &challenge) {\n\treturn SerializeClientData(challenge, \"webauthn.get\");\n}\n\nstd::optional<LoginData> DeserializeLoginData(\n\t\tconst QByteArray &jsonData) {\n\tauto doc = QJsonDocument::fromJson(jsonData);\n\tif (!doc.isObject()) {\n\t\treturn std::nullopt;\n\t}\n\n\tauto root = doc.object();\n\tauto publicKey = root[\"publicKey\"].toObject();\n\tif (publicKey.isEmpty()) {\n\t\treturn std::nullopt;\n\t}\n\n\tauto data = LoginData();\n\tdata.challenge = QByteArray::fromBase64(\n\t\tpublicKey[\"challenge\"].toString().toUtf8(),\n\t\tQByteArray::Base64UrlEncoding);\n\tdata.rpId = publicKey[\"rpId\"].toString();\n\tdata.timeout = publicKey[\"timeout\"].toInt(60000);\n\tdata.userVerification = publicKey[\"userVerification\"].toString();\n\n\tif (publicKey.contains(\"allowCredentials\")) {\n\t\tauto allowList = publicKey[\"allowCredentials\"].toArray();\n\t\tfor (const auto &cred : allowList) {\n\t\t\tauto credObj = cred.toObject();\n\t\t\tCredential credential;\n\t\t\tcredential.id = QByteArray::fromBase64(\n\t\t\t\tcredObj[\"id\"].toString().toUtf8(),\n\t\t\t\tQByteArray::Base64UrlEncoding);\n\t\t\tcredential.type = credObj[\"type\"].toString();\n\t\t\tdata.allowCredentials.push_back(credential);\n\t\t}\n\t}\n\n\treturn data;\n}\n\n} // namespace Data::Passkey\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_passkey_deserialize.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\n#pragma once\n\nnamespace Data::Passkey {\n\nstruct RelyingParty {\n\tQString id;\n\tQString name;\n};\n\nstruct User {\n\tQByteArray id;\n\tQString name;\n\tQString displayName;\n};\n\nstruct CredentialParameter {\n\tQString type;\n\tint alg = 0;\n};\n\nstruct RegisterData {\n\tRelyingParty rp;\n\tUser user;\n\tQByteArray challenge;\n\tstd::vector<CredentialParameter> pubKeyCredParams;\n\tint timeout = 60000;\n};\n\nstruct Credential {\n\tQByteArray id;\n\tQString type;\n};\n\nstruct LoginData {\n\tQByteArray challenge;\n\tQString rpId;\n\tstd::vector<Credential> allowCredentials;\n\tQString userVerification;\n\tint timeout = 60000;\n};\n\n[[nodiscard]] std::optional<RegisterData> DeserializeRegisterData(\n\tconst QByteArray &jsonData);\n\n[[nodiscard]] std::optional<LoginData> DeserializeLoginData(\n\tconst QByteArray &jsonData);\n\n[[nodiscard]] std::string SerializeClientDataCreate(\n\tconst QByteArray &challenge);\n\n[[nodiscard]] std::string SerializeClientDataGet(\n\tconst QByteArray &challenge);\n\n} // namespace Data::Passkey\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_peer.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_peer.h\"\n\n#include \"api/api_sensitive_content.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_message_reaction_id.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_session.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"base/unixtime.h\"\n#include \"base/crc32hash.h\"\n#include \"lang/lang_keys.h\"\n#include \"apiwrap.h\"\n#include \"api/api_chat_participants.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_app_config.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"window/notifications_manager.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/main_window.h\" // Window::LogoNoMargin.\n#include \"ui/image/image.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/painter.h\"\n#include \"ui/unread_badge.h\"\n#include \"ui/ui_utility.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history_item.h\"\n#include \"storage/file_download.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/storage_facade.h\"\n#include \"storage/storage_shared_media.h\"\n\nnamespace {\n\nconstexpr auto kUpdateFullPeerTimeout = crl::time(5000); // Not more than once in 5 seconds.\nconstexpr auto kUserpicSize = 160;\n\nusing UpdateFlag = Data::PeerUpdate::Flag;\n\n[[nodiscard]] const std::vector<QString> &IgnoredReasons(\n\t\tnot_null<Main::Session*> session) {\n\treturn session->appConfig().ignoredRestrictionReasons();\n}\n\n[[nodiscard]] int ParseRegistrationDate(const QString &text) {\n\t// MM.YYYY\n\tif (text.size() != 7 || text[2] != '.') {\n\t\treturn 0;\n\t}\n\tconst auto month = text.mid(0, 2).toInt();\n\tconst auto year = text.mid(3, 4).toInt();\n\treturn (year > 2012 && year < 2100 && month > 0 && month <= 12)\n\t\t? (year * 100) + month\n\t\t: 0;\n}\n\n[[nodiscard]] int RegistrationYear(int date) {\n\tconst auto year = date / 100;\n\treturn (year > 2012 && year < 2100) ? year : 0;\n}\n\n[[nodiscard]] int RegistrationMonth(int date) {\n\tconst auto month = date % 100;\n\treturn (month > 0 && month <= 12) ? month : 0;\n}\n\n} // namespace\n\nnamespace Data {\n\nuint8 DecideColorIndex(PeerId peerId) {\n\treturn Ui::DecideColorIndex(peerId.value & PeerId::kChatTypeMask);\n}\n\nPeerId FakePeerIdForJustName(const QString &name) {\n\tconstexpr auto kShift = (0xFEULL << 32);\n\tconst auto base = name.isEmpty()\n\t\t? 777\n\t\t: base::crc32(name.constData(), name.size() * sizeof(QChar));\n\treturn peerFromUser(kShift + std::abs(base));\n}\n\nbool UnavailableReason::sensitive() const {\n\treturn reason == u\"sensitive\"_q;\n}\n\nUnavailableReason UnavailableReason::Sensitive() {\n\treturn { u\"sensitive\"_q };\n}\n\nQString UnavailableReason::Compute(\n\t\tnot_null<Main::Session*> session,\n\t\tconst std::vector<UnavailableReason> &list) {\n\tconst auto &skip = IgnoredReasons(session);\n\tauto &&filtered = ranges::views::all(\n\t\tlist\n\t) | ranges::views::filter([&](const Data::UnavailableReason &reason) {\n\t\treturn !reason.sensitive()\n\t\t\t&& !ranges::contains(skip, reason.reason);\n\t});\n\tconst auto first = filtered.begin();\n\treturn (first != filtered.end()) ? first->text : QString();\n}\n\nbool UnavailableReason::IgnoreSensitiveMark(\n\t\tnot_null<Main::Session*> session) {\n\treturn ranges::contains(\n\t\t\tIgnoredReasons(session),\n\t\t\tUnavailableReason::Sensitive().reason);\n}\n\n// We should get a full restriction in \"{full}: {reason}\" format and we\n// need to find an \"-all\" tag in {full}, otherwise ignore this restriction.\nstd::vector<UnavailableReason> UnavailableReason::Extract(\n\t\tconst MTPvector<MTPRestrictionReason> *list) {\n\tif (!list) {\n\t\treturn {};\n\t}\n\treturn ranges::views::all(\n\t\tlist->v\n\t) | ranges::views::filter([](const MTPRestrictionReason &restriction) {\n\t\treturn restriction.match([&](const MTPDrestrictionReason &data) {\n\t\t\tconst auto platform = data.vplatform().v;\n\t\t\treturn false\n#ifdef OS_MAC_STORE\n\t\t\t\t|| (platform == \"ios\"_q)\n#elif defined OS_WIN_STORE // OS_MAC_STORE\n\t\t\t\t|| (platform == \"ms\"_q)\n#endif // OS_MAC_STORE || OS_WIN_STORE\n\t\t\t\t|| (platform == \"all\"_q);\n\t\t});\n\t}) | ranges::views::transform([](const MTPRestrictionReason &restriction) {\n\t\treturn restriction.match([&](const MTPDrestrictionReason &data) {\n\t\t\treturn UnavailableReason{ qs(data.vreason()), qs(data.vtext()) };\n\t\t});\n\t}) | ranges::to_vector;\n}\n\nbool ApplyBotMenuButton(\n\t\tnot_null<BotInfo*> info,\n\t\tconst MTPBotMenuButton *button) {\n\tauto text = QString();\n\tauto url = QString();\n\tif (button) {\n\t\tbutton->match([&](const MTPDbotMenuButton &data) {\n\t\t\ttext = qs(data.vtext());\n\t\t\turl = qs(data.vurl());\n\t\t}, [&](const auto &) {\n\t\t});\n\t}\n\tconst auto changed = (info->botMenuButtonText != text)\n\t\t|| (info->botMenuButtonUrl != url);\n\n\tinfo->botMenuButtonText = text;\n\tinfo->botMenuButtonUrl = url;\n\n\treturn changed;\n}\n\nAllowedReactions Parse(\n\t\tconst MTPChatReactions &value,\n\t\tint maxCount,\n\t\tbool paidEnabled) {\n\treturn value.match([&](const MTPDchatReactionsNone &) {\n\t\treturn AllowedReactions{\n\t\t\t.maxCount = maxCount,\n\t\t\t.paidEnabled = paidEnabled,\n\t\t};\n\t}, [&](const MTPDchatReactionsAll &data) {\n\t\treturn AllowedReactions{\n\t\t\t.maxCount = maxCount,\n\t\t\t.type = (data.is_allow_custom()\n\t\t\t\t? AllowedReactionsType::All\n\t\t\t\t: AllowedReactionsType::Default),\n\t\t\t.paidEnabled = paidEnabled,\n\t\t};\n\t}, [&](const MTPDchatReactionsSome &data) {\n\t\treturn AllowedReactions{\n\t\t\t.some = ranges::views::all(\n\t\t\t\tdata.vreactions().v\n\t\t\t) | ranges::views::transform(\n\t\t\t\tReactionFromMTP\n\t\t\t) | ranges::to_vector,\n\t\t\t.maxCount = maxCount,\n\t\t\t.type = AllowedReactionsType::Some,\n\t\t\t.paidEnabled = paidEnabled,\n\t\t};\n\t});\n}\n\nPeerData *PeerFromInputMTP(\n\t\tnot_null<Session*> owner,\n\t\tconst MTPInputPeer &input) {\n\treturn input.match([&](const MTPDinputPeerUser &data) {\n\t\tconst auto user = owner->user(data.vuser_id().v);\n\t\tuser->setAccessHash(data.vaccess_hash().v);\n\t\treturn (PeerData*)user;\n\t}, [&](const MTPDinputPeerChat &data) {\n\t\treturn (PeerData*)owner->chat(data.vchat_id().v);\n\t}, [&](const MTPDinputPeerChannel &data) {\n\t\tconst auto channel = owner->channel(data.vchannel_id().v);\n\t\tchannel->setAccessHash(data.vaccess_hash().v);\n\t\treturn (PeerData*)channel;\n\t}, [&](const MTPDinputPeerSelf &data) {\n\t\treturn (PeerData*)owner->session().user();\n\t}, [&](const auto &data) {\n\t\treturn (PeerData*)nullptr;\n\t});\n}\n\nUserData *UserFromInputMTP(\n\t\tnot_null<Session*> owner,\n\t\tconst MTPInputUser &input) {\n\treturn input.match([&](const MTPDinputUser &data) {\n\t\tconst auto user = owner->user(data.vuser_id().v);\n\t\tuser->setAccessHash(data.vaccess_hash().v);\n\t\treturn user.get();\n\t}, [&](const MTPDinputUserSelf &data) {\n\t\treturn owner->session().user().get();\n\t}, [](const auto &data) {\n\t\treturn (UserData*)nullptr;\n\t});\n}\n\nUi::ColorCollectible ParseColorCollectible(\n\t\tconst MTPDpeerColorCollectible &data) {\n\treturn {\n\t\t.collectibleId = data.vcollectible_id().v,\n\t\t.giftEmojiId = data.vgift_emoji_id().v,\n\t\t.backgroundEmojiId = data.vbackground_emoji_id().v,\n\t\t.accentColor = Ui::ColorFromSerialized(data.vaccent_color()),\n\t\t.strip = ranges::views::all(\n\t\t\tdata.vcolors().v\n\t\t) | ranges::views::transform(\n\t\t\t&Ui::ColorFromSerialized\n\t\t) | ranges::to_vector,\n\t\t.darkAccentColor = Ui::MaybeColorFromSerialized(\n\t\t\tdata.vdark_accent_color()).value_or(QColor(0, 0, 0, 0)),\n\t\t.darkStrip = (data.vdark_colors()\n\t\t\t? ranges::views::all(\n\t\t\t\tdata.vdark_colors()->v\n\t\t\t) | ranges::views::transform(\n\t\t\t\t&Ui::ColorFromSerialized\n\t\t\t) | ranges::to_vector\n\t\t\t: std::vector<QColor>()),\n\t};\n}\n\n} // namespace Data\n\nPeerClickHandler::PeerClickHandler(not_null<PeerData*> peer)\n: _peer(peer) {\n\tsetProperty(kPeerLinkPeerIdProperty, peer->id.value);\n}\n\nvoid PeerClickHandler::onClick(ClickContext context) const {\n\tif (context.button != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tconst auto my = context.other.value<ClickHandlerContext>();\n\tconst auto window = [&]() -> Window::SessionController* {\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\treturn controller;\n\t\t}\n\t\tconst auto &windows = _peer->session().windows();\n\t\tif (windows.empty()) {\n\t\t\t_peer->session().domain().activate(&_peer->session().account());\n\t\t\tif (windows.empty()) {\n\t\t\t\treturn nullptr;\n\t\t\t}\n\t\t}\n\t\treturn windows.front();\n\t}();\n\tif (window) {\n\t\twindow->showPeer(_peer);\n\t}\n}\n\nPeerData::PeerData(not_null<Data::Session*> owner, PeerId id)\n: id(id)\n, _owner(owner)\n, _colorIndex(Data::DecideColorIndex(id)) {\n}\n\nData::Session &PeerData::owner() const {\n\treturn *_owner;\n}\n\nMain::Session &PeerData::session() const {\n\treturn _owner->session();\n}\n\nMain::Account &PeerData::account() const {\n\treturn session().account();\n}\n\nvoid PeerData::updateNameDelayed(\n\t\tconst QString &newName,\n\t\tconst QString &newNameOrPhone,\n\t\tconst QString &newUsername) {\n\tif (_name == newName && _nameVersion > 1) {\n\t\tif (isUser()) {\n\t\t\tif (asUser()->nameOrPhone == newNameOrPhone\n\t\t\t\t&& asUser()->editableUsername() == newUsername) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else if (isChannel()) {\n\t\t\tif (asChannel()->editableUsername() == newUsername) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else if (isChat()) {\n\t\t\treturn;\n\t\t}\n\t}\n\t_name = newName;\n\tinvalidateEmptyUserpic();\n\n\tauto flags = UpdateFlag::None | UpdateFlag::None;\n\tauto oldFirstLetters = base::flat_set<QChar>();\n\tconst auto nameUpdated = (_nameVersion++ > 1);\n\tif (nameUpdated) {\n\t\toldFirstLetters = nameFirstLetters();\n\t\tflags |= UpdateFlag::Name;\n\t}\n\tif (isUser()) {\n\t\tif (asUser()->editableUsername() != newUsername) {\n\t\t\tasUser()->setUsername(newUsername);\n\t\t\tflags |= UpdateFlag::Username;\n\t\t}\n\t\tasUser()->setNameOrPhone(newNameOrPhone);\n\t} else if (isChannel()) {\n\t\tif (asChannel()->editableUsername() != newUsername) {\n\t\t\tasChannel()->setUsername(newUsername);\n\t\t\tif (asChannel()->username().isEmpty()) {\n\t\t\t\tasChannel()->removeFlags(ChannelDataFlag::Username);\n\t\t\t} else {\n\t\t\t\tasChannel()->addFlags(ChannelDataFlag::Username);\n\t\t\t}\n\t\t\tflags |= UpdateFlag::Username;\n\t\t}\n\t}\n\tfillNames();\n\tif (nameUpdated) {\n\t\tsession().changes().nameUpdated(this, std::move(oldFirstLetters));\n\t}\n\tif (flags) {\n\t\tsession().changes().peerUpdated(this, flags);\n\t}\n}\n\nnot_null<Ui::EmptyUserpic*> PeerData::ensureEmptyUserpic() const {\n\tif (!_userpicEmpty) {\n\t\tconst auto user = asUser();\n\t\t_userpicEmpty = std::make_unique<Ui::EmptyUserpic>(\n\t\t\tUi::EmptyUserpic::UserpicColor(colorIndex()),\n\t\t\t((user && user->isInaccessible())\n\t\t\t\t? Ui::EmptyUserpic::InaccessibleName()\n\t\t\t\t: name()));\n\t}\n\treturn _userpicEmpty.get();\n}\n\nvoid PeerData::invalidateEmptyUserpic() {\n\t_userpicEmpty = nullptr;\n}\n\nvoid PeerData::checkTrustedPayForMessage() {\n\tif (!_checkedTrustedPayForMessage\n\t\t&& !starsPerMessage()\n\t\t&& session().local().peerTrustedPayForMessageRead()) {\n\t\t_checkedTrustedPayForMessage = 1;\n\t\tif (session().local().hasPeerTrustedPayForMessageEntry(id)) {\n\t\t\tsession().local().clearPeerTrustedPayForMessage(id);\n\t\t}\n\t}\n}\n\nClickHandlerPtr PeerData::createOpenLink() {\n\treturn std::make_shared<PeerClickHandler>(this);\n}\n\nvoid PeerData::setUserpic(\n\t\tPhotoId photoId,\n\t\tconst ImageLocation &location,\n\t\tbool hasVideo) {\n\t_userpicPhotoId = photoId;\n\t_userpicHasVideo = hasVideo ? 1 : 0;\n\t_userpic.set(&session(), ImageWithLocation{ .location = location });\n}\n\nvoid PeerData::setUserpicPhoto(const MTPPhoto &data) {\n\tconst auto photoId = data.match([&](const MTPDphoto &data) {\n\t\tconst auto photo = owner().processPhoto(data);\n\t\tphoto->peer = this;\n\t\treturn photo->id;\n\t}, [](const MTPDphotoEmpty &data) {\n\t\treturn PhotoId(0);\n\t});\n\tif (_userpicPhotoId != photoId) {\n\t\t_userpicPhotoId = photoId;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::Photo);\n\t}\n}\n\nQImage *PeerData::userpicCloudImage(Ui::PeerUserpicView &view) const {\n\tif (!_userpic.isCurrentView(view.cloud)) {\n\t\tif (!_userpic.empty()) {\n\t\t\tview.cloud = _userpic.createView();\n\t\t\t_userpic.load(&session(), userpicOrigin());\n\t\t} else {\n\t\t\tview.cloud = nullptr;\n\t\t}\n\t\tview.cached = QImage();\n\t}\n\tif (const auto image = view.cloud.get(); image && !image->isNull()) {\n\t\t_userpicEmpty = nullptr;\n\t\treturn image;\n\t} else if (isNotificationsUser()) {\n\t\tstatic auto result = Window::LogoNoMargin().scaledToWidth(\n\t\t\tkUserpicSize,\n\t\t\tQt::SmoothTransformation);\n\t\treturn &result;\n\t}\n\treturn nullptr;\n}\n\nvoid PeerData::paintUserpic(\n\t\tQPainter &p,\n\t\tUi::PeerUserpicView &view,\n\t\tPaintUserpicContext context) const {\n\tif (const auto broadcast = monoforumBroadcast()) {\n\t\tif (context.shape == Ui::PeerUserpicShape::Auto) {\n\t\t\tcontext.shape = Ui::PeerUserpicShape::Monoforum;\n\t\t}\n\t\tbroadcast->paintUserpic(p, view, context);\n\t\treturn;\n\t}\n\tconst auto size = context.size;\n\tconst auto cloud = userpicCloudImage(view);\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (context.shape == Ui::PeerUserpicShape::Auto) {\n\t\tcontext.shape = (isForum() && !isBot())\n\t\t\t? Ui::PeerUserpicShape::Forum\n\t\t\t: isMonoforum()\n\t\t\t? Ui::PeerUserpicShape::Monoforum\n\t\t\t: Ui::PeerUserpicShape::Circle;\n\t}\n\tUi::ValidateUserpicCache(\n\t\tview,\n\t\tcloud,\n\t\tcloud ? nullptr : ensureEmptyUserpic().get(),\n\t\tsize * ratio,\n\t\tcontext.shape);\n\tp.drawImage(QRect(context.position, QSize(size, size)), view.cached);\n}\n\nvoid PeerData::loadUserpic() {\n\t_userpic.load(&session(), userpicOrigin());\n}\n\nbool PeerData::hasUserpic() const {\n\treturn !_userpic.empty();\n}\n\nUi::PeerUserpicView PeerData::activeUserpicView() {\n\treturn { .cloud = _userpic.empty() ? nullptr : _userpic.activeView() };\n}\n\nUi::PeerUserpicView PeerData::createUserpicView() {\n\tif (_userpic.empty()) {\n\t\treturn {};\n\t}\n\tauto result = _userpic.createView();\n\t_userpic.load(&session(), userpicPhotoOrigin());\n\treturn { .cloud = result };\n}\n\nbool PeerData::useEmptyUserpic(Ui::PeerUserpicView &view) const {\n\treturn !userpicCloudImage(view);\n}\n\nInMemoryKey PeerData::userpicUniqueKey(Ui::PeerUserpicView &view) const {\n\treturn useEmptyUserpic(view)\n\t\t? ensureEmptyUserpic()->uniqueKey()\n\t\t: inMemoryKey(_userpic.location());\n}\n\nQImage PeerData::GenerateUserpicImage(\n\t\tnot_null<PeerData*> peer,\n\t\tUi::PeerUserpicView &view,\n\t\tint size,\n\t\tstd::optional<int> radius) {\n\tif (const auto userpic = peer->userpicCloudImage(view)) {\n\t\tauto image = userpic->scaled(\n\t\t\t{ size, size },\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t\tconst auto round = [&](int radius) {\n\t\t\treturn Images::Round(\n\t\t\t\tstd::move(image),\n\t\t\t\tImages::CornersMask(radius / style::DevicePixelRatio()));\n\t\t};\n\t\tif (radius == 0) {\n\t\t\treturn image;\n\t\t} else if (radius) {\n\t\t\treturn round(*radius);\n\t\t} else if (peer->isForum()) {\n\t\t\treturn round(size * Ui::ForumUserpicRadiusMultiplier());\n\t\t} else {\n\t\t\treturn Images::Circle(std::move(image));\n\t\t}\n\t}\n\tauto result = QImage(\n\t\tQSize(size, size),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::transparent);\n\n\tPainter p(&result);\n\tif (radius == 0) {\n\t\tpeer->ensureEmptyUserpic()->paintSquare(p, 0, 0, size, size);\n\t} else if (radius) {\n\t\tconst auto r = *radius;\n\t\tpeer->ensureEmptyUserpic()->paintRounded(p, 0, 0, size, size, r);\n\t} else if (peer->isForum()) {\n\t\tpeer->ensureEmptyUserpic()->paintRounded(\n\t\t\tp,\n\t\t\t0,\n\t\t\t0,\n\t\t\tsize,\n\t\t\tsize,\n\t\t\tsize * Ui::ForumUserpicRadiusMultiplier());\n\t} else {\n\t\tpeer->ensureEmptyUserpic()->paintCircle(p, 0, 0, size, size);\n\t}\n\tp.end();\n\n\treturn result;\n}\n\nImageLocation PeerData::userpicLocation() const {\n\treturn _userpic.location();\n}\n\nbool PeerData::userpicPhotoUnknown() const {\n\treturn (_userpicPhotoId == kUnknownPhotoId);\n}\n\nPhotoId PeerData::userpicPhotoId() const {\n\treturn userpicPhotoUnknown() ? 0 : _userpicPhotoId;\n}\n\nbool PeerData::userpicHasVideo() const {\n\treturn _userpicHasVideo != 0;\n}\n\nData::FileOrigin PeerData::userpicOrigin() const {\n\treturn Data::FileOriginPeerPhoto(id);\n}\n\nData::FileOrigin PeerData::userpicPhotoOrigin() const {\n\treturn (isUser() && userpicPhotoId())\n\t\t? Data::FileOriginFullUser(peerToUser(id))\n\t\t: Data::FileOrigin();\n}\n\nvoid PeerData::updateUserpic(\n\t\tPhotoId photoId,\n\t\tMTP::DcId dcId,\n\t\tbool hasVideo) {\n\tsetUserpicChecked(\n\t\tphotoId,\n\t\tImageLocation(\n\t\t\t{ StorageFileLocation(\n\t\t\t\tdcId,\n\t\t\t\tisSelf() ? peerToUser(id) : UserId(),\n\t\t\t\tMTP_inputPeerPhotoFileLocation(\n\t\t\t\t\tMTP_flags(0),\n\t\t\t\t\tinput(),\n\t\t\t\t\tMTP_long(photoId))) },\n\t\t\tkUserpicSize,\n\t\t\tkUserpicSize),\n\t\thasVideo);\n}\n\nvoid PeerData::clearUserpic() {\n\tsetUserpicChecked(PhotoId(), ImageLocation(), false);\n}\n\nvoid PeerData::setUserpicChecked(\n\t\tPhotoId photoId,\n\t\tconst ImageLocation &location,\n\t\tbool hasVideo) {\n\tif (_userpicPhotoId != photoId\n\t\t|| _userpic.location() != location\n\t\t|| _userpicHasVideo != (hasVideo ? 1 : 0)) {\n\t\tconst auto known = !userpicPhotoUnknown();\n\t\tsetUserpic(photoId, location, hasVideo);\n\t\tsession().changes().peerUpdated(this, UpdateFlag::Photo);\n\t\tif (known && isPremium() && userpicPhotoUnknown()) {\n\t\t\tupdateFull();\n\t\t}\n\t}\n}\n\nauto PeerData::unavailableReasons() const\n-> const std::vector<Data::UnavailableReason> & {\n\tstatic const auto result = std::vector<Data::UnavailableReason>();\n\treturn result;\n}\n\nQString PeerData::computeUnavailableReason() const {\n\treturn Data::UnavailableReason::Compute(\n\t\t&session(),\n\t\tunavailableReasons());\n}\n\nbool PeerData::hasSensitiveContent() const {\n\treturn _sensitiveContent == 1;\n}\n\nvoid PeerData::setUnavailableReasonsList(\n\t\tstd::vector<Data::UnavailableReason> &&reasons) {\n\tUnexpected(\"PeerData::setUnavailableReasonsList.\");\n}\n\nvoid PeerData::setUnavailableReasons(\n\t\tstd::vector<Data::UnavailableReason> &&reasons) {\n\tconst auto i = ranges::find(\n\t\treasons,\n\t\ttrue,\n\t\t&Data::UnavailableReason::sensitive);\n\tconst auto sensitive = (i != end(reasons));\n\tif (sensitive) {\n\t\treasons.erase(i);\n\t}\n\tauto changed = (sensitive != hasSensitiveContent());\n\tif (changed) {\n\t\tsetHasSensitiveContent(sensitive);\n\t}\n\tif (reasons != unavailableReasons()) {\n\t\tsetUnavailableReasonsList(std::move(reasons));\n\t\tchanged = true;\n\t}\n\tif (changed) {\n\t\tsession().changes().peerUpdated(\n\t\t\tthis,\n\t\t\tUpdateFlag::UnavailableReason);\n\t}\n}\n\nvoid PeerData::setHasSensitiveContent(bool has) {\n\t_sensitiveContent = has ? 1 : 0;\n\tif (has) {\n\t\tsession().api().sensitiveContent().preload();\n\t}\n}\n\n// This is duplicated in CanPinMessagesValue().\nbool PeerData::canPinMessages() const {\n\tif (const auto user = asUser()) {\n\t\treturn !user->amRestricted(ChatRestriction::PinMessages);\n\t} else if (const auto chat = asChat()) {\n\t\treturn chat->amIn()\n\t\t\t&& !chat->amRestricted(ChatRestriction::PinMessages);\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->isMegagroup()\n\t\t\t? !channel->amRestricted(ChatRestriction::PinMessages)\n\t\t\t: ((channel->amCreator()\n\t\t\t\t|| channel->adminRights() & ChatAdminRight::EditMessages));\n\t}\n\tUnexpected(\"Peer type in PeerData::canPinMessages.\");\n}\n\nbool PeerData::canCreatePolls() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->isSelf()\n\t\t\t|| (user->isBot()\n\t\t\t\t&& !user->isSupport()\n\t\t\t\t&& !user->isRepliesChat()\n\t\t\t\t&& !user->isVerifyCodes());\n\t} else if (isMonoforum()) {\n\t\treturn false;\n\t}\n\treturn Data::CanSend(this, ChatRestriction::SendPolls);\n}\n\nbool PeerData::canCreateTodoLists() const {\n\tif (isMonoforum() || isBroadcast()) {\n\t\treturn false;\n\t}\n\treturn session().premium()\n\t\t&& (Data::CanSend(this, ChatRestriction::SendPolls) || isUser());\n}\n\nbool PeerData::canCreateTopics() const {\n\tif (const auto bot = asBot()) {\n\t\treturn bot->isForum();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->isForum()\n\t\t\t&& !channel->amRestricted(ChatRestriction::CreateTopics);\n\t}\n\treturn false;\n}\n\nbool PeerData::canManageTopics() const {\n\tif (const auto bot = asBot()) {\n\t\treturn bot->isForum();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->isForum()\n\t\t\t&& (channel->amCreator()\n\t\t\t\t|| (channel->adminRights() & ChatAdminRight::ManageTopics));\n\t}\n\treturn false;\n}\n\nbool PeerData::canPostStories() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel->canPostStories();\n\t}\n\treturn isSelf();\n}\n\nbool PeerData::canEditStories() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel->canEditStories();\n\t}\n\treturn isSelf();\n}\n\nbool PeerData::canDeleteStories() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel->canDeleteStories();\n\t}\n\treturn isSelf();\n}\n\nbool PeerData::canManageGifts() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel->canPostMessages();\n\t}\n\treturn isSelf();\n}\n\nbool PeerData::canTransferGifts() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel->amCreator();\n\t}\n\treturn isSelf();\n}\n\nbool PeerData::canEditMessagesIndefinitely() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->isSelf();\n\t} else if (isChat()) {\n\t\treturn false;\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->isMegagroup()\n\t\t\t? channel->canPinMessages()\n\t\t\t: channel->canEditMessages();\n\t}\n\tUnexpected(\"Peer type in PeerData::canEditMessagesIndefinitely.\");\n}\n\nbool PeerData::canExportChatHistory() const {\n\tif (isRepliesChat() || isVerifyCodes() || !allowsForwarding()) {\n\t\treturn false;\n\t} else if (const auto channel = asChannel()) {\n\t\tif (!channel->amIn() && channel->invitePeekExpires()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\tfor (const auto &block : _owner->history(id)->blocks) {\n\t\tfor (const auto &message : block->messages) {\n\t\t\tif (!message->data()->isService()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\tif (const auto from = migrateFrom()) {\n\t\treturn from->canExportChatHistory();\n\t}\n\treturn false;\n}\n\nbool PeerData::autoTranslation() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel->autoTranslation();\n\t}\n\treturn false;\n}\n\nbool PeerData::setAbout(const QString &newAbout) {\n\tif (_about == newAbout) {\n\t\treturn false;\n\t}\n\t_about = newAbout;\n\tsession().changes().peerUpdated(this, UpdateFlag::About);\n\treturn true;\n}\n\nvoid PeerData::checkFolder(FolderId folderId) {\n\tconst auto folder = folderId\n\t\t? owner().folderLoaded(folderId)\n\t\t: nullptr;\n\tif (const auto history = owner().historyLoaded(this)) {\n\t\tif (folder && history->folder() != folder) {\n\t\t\towner().histories().requestDialogEntry(history);\n\t\t}\n\t}\n}\n\nvoid PeerData::clearBusinessBot() {\n\tif (const auto details = _barDetails.get()) {\n\t\tif (details->requestChatDate\n\t\t\t|| details->paysPerMessage\n\t\t\t|| !details->phoneCountryCode.isEmpty()) {\n\t\t\tdetails->businessBot = nullptr;\n\t\t\tdetails->businessBotManageUrl = QString();\n\t\t} else {\n\t\t\t_barDetails = nullptr;\n\t\t}\n\t}\n\tif (const auto settings = barSettings()) {\n\t\tsetBarSettings(*settings\n\t\t\t& ~PeerBarSetting::BusinessBotPaused\n\t\t\t& ~PeerBarSetting::BusinessBotCanReply\n\t\t\t& ~PeerBarSetting::HasBusinessBot);\n\t}\n}\n\nvoid PeerData::setTranslationDisabled(bool disabled) {\n\tconst auto flag = disabled\n\t\t? TranslationFlag::Disabled\n\t\t: TranslationFlag::Enabled;\n\tif (_translationFlag != flag) {\n\t\t_translationFlag = flag;\n\t\tsession().changes().peerUpdated(\n\t\t\tthis,\n\t\t\tUpdateFlag::TranslationDisabled);\n\t}\n}\n\nPeerData::TranslationFlag PeerData::translationFlag() const {\n\treturn _translationFlag;\n}\n\nvoid PeerData::saveTranslationDisabled(bool disabled) {\n\tsetTranslationDisabled(disabled);\n\n\tusing Flag = MTPmessages_TogglePeerTranslations::Flag;\n\tsession().api().request(MTPmessages_TogglePeerTranslations(\n\t\tMTP_flags(disabled ? Flag::f_disabled : Flag()),\n\t\tinput()\n\t)).send();\n}\n\nvoid PeerData::setBarSettings(const MTPPeerSettings &data) {\n\tdata.match([&](const MTPDpeerSettings &data) {\n\t\tconst auto wasPaysPerMessage = paysPerMessage();\n\t\tif (!data.vbusiness_bot_id()\n\t\t\t&& !data.vrequest_chat_title()\n\t\t\t&& !data.vcharge_paid_message_stars()\n\t\t\t&& !data.vphone_country()\n\t\t\t&& !data.vregistration_month()\n\t\t\t&& !data.vname_change_date()\n\t\t\t&& !data.vphoto_change_date()) {\n\t\t\t_barDetails = nullptr;\n\t\t} else if (!_barDetails) {\n\t\t\t_barDetails = std::make_unique<PeerBarDetails>();\n\t\t}\n\t\tif (_barDetails) {\n\t\t\t_barDetails->phoneCountryCode\n\t\t\t\t= qs(data.vphone_country().value_or_empty());\n\t\t\t_barDetails->registrationDate = ParseRegistrationDate(\n\t\t\t\tdata.vregistration_month().value_or_empty());\n\t\t\t_barDetails->nameChangeDate\n\t\t\t\t= data.vname_change_date().value_or_empty();\n\t\t\t_barDetails->photoChangeDate\n\t\t\t\t= data.vphoto_change_date().value_or_empty();\n\t\t\t_barDetails->requestChatTitle\n\t\t\t\t= qs(data.vrequest_chat_title().value_or_empty());\n\t\t\t_barDetails->requestChatDate\n\t\t\t\t= data.vrequest_chat_date().value_or_empty();\n\t\t\t_barDetails->businessBot = data.vbusiness_bot_id()\n\t\t\t\t? _owner->user(data.vbusiness_bot_id()->v).get()\n\t\t\t\t: nullptr;\n\t\t\t_barDetails->businessBotManageUrl\n\t\t\t\t= qs(data.vbusiness_bot_manage_url().value_or_empty());\n\t\t\t_barDetails->paysPerMessage\n\t\t\t\t= data.vcharge_paid_message_stars().value_or_empty();\n\t\t}\n\t\tusing Flag = PeerBarSetting;\n\t\tsetBarSettings((data.is_add_contact() ? Flag::AddContact : Flag())\n\t\t\t| (data.is_autoarchived() ? Flag::AutoArchived : Flag())\n\t\t\t| (data.is_block_contact() ? Flag::BlockContact : Flag())\n\t\t\t//| (data.is_invite_members() ? Flag::InviteMembers : Flag())\n\t\t\t| (data.is_need_contacts_exception()\n\t\t\t\t? Flag::NeedContactsException\n\t\t\t\t: Flag())\n\t\t\t//| (data.is_report_geo() ? Flag::ReportGeo : Flag())\n\t\t\t| (data.is_report_spam() ? Flag::ReportSpam : Flag())\n\t\t\t| (data.is_share_contact() ? Flag::ShareContact : Flag())\n\t\t\t| (data.vrequest_chat_title() ? Flag::RequestChat : Flag())\n\t\t\t| (data.vbusiness_bot_id() ? Flag::HasBusinessBot : Flag())\n\t\t\t| (data.is_request_chat_broadcast()\n\t\t\t\t? Flag::RequestChatIsBroadcast\n\t\t\t\t: Flag())\n\t\t\t| (data.is_business_bot_paused()\n\t\t\t\t? Flag::BusinessBotPaused\n\t\t\t\t: Flag())\n\t\t\t| (data.is_business_bot_can_reply()\n\t\t\t\t? Flag::BusinessBotCanReply\n\t\t\t\t: Flag()));\n\t\tif (wasPaysPerMessage != paysPerMessage()) {\n\t\t\tsession().changes().peerUpdated(\n\t\t\t\tthis,\n\t\t\t\tUpdateFlag::PaysPerMessage);\n\t\t}\n\t});\n}\n\nvoid PeerData::setBarSettings(PeerBarSettings which) {\n\tconst auto was = hideLinks();\n\t_barSettings.set(which);\n\tif (was && !hideLinks()) {\n\t\tif (const auto history = owner().historyLoaded(this)) {\n\t\t\tcrl::on_main(&history->session(), [=] {\n\t\t\t\thistory->refreshHiddenLinksItems();\n\t\t\t});\n\t\t}\n\t\tif (const auto from = migrateFrom()) {\n\t\t\tif (const auto history = owner().historyLoaded(from)) {\n\t\t\t\tcrl::on_main(&history->session(), [=] {\n\t\t\t\t\thistory->refreshHiddenLinksItems();\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n}\n\nint PeerData::paysPerMessage() const {\n\treturn _barDetails ? _barDetails->paysPerMessage : 0;\n}\n\nvoid PeerData::clearPaysPerMessage() {\n\tif (const auto details = _barDetails.get()) {\n\t\tif (details->paysPerMessage) {\n\t\t\tif (details->businessBot\n\t\t\t\t|| details->requestChatDate\n\t\t\t\t|| !details->phoneCountryCode.isEmpty()) {\n\t\t\t\tdetails->paysPerMessage = 0;\n\t\t\t} else {\n\t\t\t\t_barDetails = nullptr;\n\t\t\t}\n\t\t\tsession().changes().peerUpdated(\n\t\t\t\tthis,\n\t\t\t\tUpdateFlag::PaysPerMessage);\n\t\t}\n\t}\n}\n\nbool PeerData::hideLinks() const {\n\t//if (!isUser()) {\n\t//\treturn false;\n\t//}\n\tif (const auto to = migrateTo()) {\n\t\treturn to->hideLinks();\n\t}\n\tconst auto settings = barSettings();\n\treturn !settings || (*settings & PeerBarSetting::ReportSpam);\n}\n\nQString PeerData::requestChatTitle() const {\n\treturn _barDetails ? _barDetails->requestChatTitle : QString();\n}\n\nTimeId PeerData::requestChatDate() const {\n\treturn _barDetails ? _barDetails->requestChatDate : 0;\n}\n\nUserData *PeerData::businessBot() const {\n\treturn _barDetails ? _barDetails->businessBot : nullptr;\n}\n\nQString PeerData::businessBotManageUrl() const {\n\treturn _barDetails ? _barDetails->businessBotManageUrl : QString();\n}\n\nQString PeerData::phoneCountryCode() const {\n\treturn _barDetails ? _barDetails->phoneCountryCode : QString();\n}\n\nint PeerData::registrationMonth() const {\n\treturn _barDetails\n\t\t? RegistrationMonth(_barDetails->registrationDate)\n\t\t: 0;\n}\n\nint PeerData::registrationYear() const {\n\treturn _barDetails ? RegistrationYear(_barDetails->registrationDate) : 0;\n}\n\nTimeId PeerData::nameChangeDate() const {\n\treturn _barDetails ? _barDetails->nameChangeDate : 0;\n}\n\nTimeId PeerData::photoChangeDate() const {\n\treturn _barDetails ? _barDetails->photoChangeDate : 0;\n}\n\nbool PeerData::changeBackgroundEmojiId(\n\t\tconst tl::conditional<MTPlong> &cloudBackgroundEmoji) {\n\treturn changeBackgroundEmojiId(cloudBackgroundEmoji\n\t\t? cloudBackgroundEmoji->v\n\t\t: DocumentId());\n}\n\nbool PeerData::changeColorCollectible(\n\t\tconst tl::conditional<MTPPeerColor> &cloudColor) {\n\tif (!cloudColor) {\n\t\treturn clearColorCollectible();\n\t}\n\treturn cloudColor->match([&](const MTPDpeerColorCollectible &data) {\n\t\treturn changeColorCollectible(Data::ParseColorCollectible(data));\n\t}, [&](const MTPDpeerColor &) {\n\t\treturn clearColorCollectible();\n\t}, [&](const MTPDinputPeerColorCollectible &) {\n\t\treturn clearColorCollectible();\n\t});\n}\n\nbool PeerData::changeColor(\n\t\tconst tl::conditional<MTPPeerColor> &cloudColor) {\n\tconst auto maybeColorIndex = Data::ColorIndexFromColor(cloudColor);\n\tconst auto changed1 = maybeColorIndex\n\t\t? changeColorIndex(*maybeColorIndex)\n\t\t: clearColorIndex();\n\tconst auto changed2 = changeBackgroundEmojiId(\n\t\tData::BackgroundEmojiIdFromColor(cloudColor));\n\tconst auto changed3 = changeColorCollectible(cloudColor);\n\treturn changed1 || changed2 || changed3;\n}\n\nbool PeerData::changeColorProfile(\n\t\tconst tl::conditional<MTPPeerColor> &cloudColor) {\n\tconst auto maybeColorIndex = Data::ColorIndexFromColor(cloudColor);\n\tconst auto changed1 = maybeColorIndex\n\t\t? changeColorProfileIndex(*maybeColorIndex)\n\t\t: clearColorProfileIndex();\n\tconst auto changed2 = changeProfileBackgroundEmojiId(\n\t\tData::BackgroundEmojiIdFromColor(cloudColor));\n\tconst auto changed3 = changeColorProfileCollectible(cloudColor);\n\treturn changed1 || changed2 || changed3;\n}\n\nvoid PeerData::fillNames() {\n\t_nameWords.clear();\n\t_nameFirstLetters.clear();\n\tauto toIndexList = QStringList();\n\tauto appendToIndex = [&](const QString &value) {\n\t\tif (!value.isEmpty()) {\n\t\t\ttoIndexList.push_back(TextUtilities::RemoveAccents(value));\n\t\t}\n\t};\n\n\tappendToIndex(name());\n\tconst auto appendTranslit = !toIndexList.isEmpty()\n\t\t&& cRussianLetters().match(toIndexList.front()).hasMatch();\n\tif (appendTranslit) {\n\t\tappendToIndex(translitRusEng(toIndexList.front()));\n\t}\n\tif (const auto user = asUser()) {\n\t\tif (user->nameOrPhone != name()) {\n\t\t\tappendToIndex(user->nameOrPhone);\n\t\t}\n\t\tappendToIndex(user->username());\n\t\tif (isSelf()) {\n\t\t\tconst auto english = u\"Saved messages\"_q;\n\t\t\tconst auto localized = tr::lng_saved_messages(tr::now);\n\t\t\tappendToIndex(english);\n\t\t\tif (localized != english) {\n\t\t\t\tappendToIndex(localized);\n\t\t\t}\n\t\t} else if (isRepliesChat()) {\n\t\t\tconst auto english = u\"Replies\"_q;\n\t\t\tconst auto localized = tr::lng_replies_messages(tr::now);\n\t\t\tappendToIndex(english);\n\t\t\tif (localized != english) {\n\t\t\t\tappendToIndex(localized);\n\t\t\t}\n\t\t} else if (isVerifyCodes()) {\n\t\t\tconst auto english = u\"Verification Codes\"_q;\n\t\t\tconst auto localized = tr::lng_verification_codes(tr::now);\n\t\t\tappendToIndex(english);\n\t\t\tif (localized != english) {\n\t\t\t\tappendToIndex(localized);\n\t\t\t}\n\t\t}\n\t} else if (const auto channel = asChannel()) {\n\t\tappendToIndex(channel->username());\n\t}\n\tauto toIndex = toIndexList.join(' ');\n\ttoIndex += ' ' + rusKeyboardLayoutSwitch(toIndex);\n\n\tconst auto namesList = TextUtilities::PrepareSearchWords(toIndex);\n\tfor (const auto &name : namesList) {\n\t\t_nameWords.insert(name);\n\t\t_nameFirstLetters.insert(name[0]);\n\t}\n}\n\nPeerData::~PeerData() = default;\n\nvoid PeerData::updateFull() {\n\tif (!_lastFullUpdate\n\t\t|| crl::now() > _lastFullUpdate + kUpdateFullPeerTimeout) {\n\t\tupdateFullForced();\n\t}\n}\n\nvoid PeerData::updateFullForced() {\n\tsession().api().requestFullPeer(this);\n\tif (const auto channel = asChannel()) {\n\t\tif (!channel->amCreator() && !channel->inviter) {\n\t\t\tsession().api().chatParticipants().requestSelf(channel);\n\t\t}\n\t}\n}\n\nvoid PeerData::fullUpdated() {\n\t_lastFullUpdate = crl::now();\n\tsetLoadedStatus(LoadedStatus::Full);\n}\n\nUserData *PeerData::asBot() {\n\treturn isBot() ? static_cast<UserData*>(this) : nullptr;\n}\n\nconst UserData *PeerData::asBot() const {\n\treturn isBot()\n\t\t? static_cast<const UserData*>(this)\n\t\t: nullptr;\n}\n\nUserData *PeerData::asUser() {\n\treturn isUser() ? static_cast<UserData*>(this) : nullptr;\n}\n\nconst UserData *PeerData::asUser() const {\n\treturn isUser() ? static_cast<const UserData*>(this) : nullptr;\n}\n\nChatData *PeerData::asChat() {\n\treturn isChat() ? static_cast<ChatData*>(this) : nullptr;\n}\n\nconst ChatData *PeerData::asChat() const {\n\treturn isChat() ? static_cast<const ChatData*>(this) : nullptr;\n}\n\nChannelData *PeerData::asChannel() {\n\treturn isChannel() ? static_cast<ChannelData*>(this) : nullptr;\n}\n\nconst ChannelData *PeerData::asChannel() const {\n\treturn isChannel()\n\t\t? static_cast<const ChannelData*>(this)\n\t\t: nullptr;\n}\n\nChannelData *PeerData::asMegagroup() {\n\treturn isMegagroup() ? static_cast<ChannelData*>(this) : nullptr;\n}\n\nconst ChannelData *PeerData::asMegagroup() const {\n\treturn isMegagroup()\n\t\t? static_cast<const ChannelData*>(this)\n\t\t: nullptr;\n}\n\nChannelData *PeerData::asBroadcast() {\n\treturn isBroadcast() ? static_cast<ChannelData*>(this) : nullptr;\n}\n\nconst ChannelData *PeerData::asBroadcast() const {\n\treturn isBroadcast()\n\t\t? static_cast<const ChannelData*>(this)\n\t\t: nullptr;\n}\n\nChatData *PeerData::asChatNotMigrated() {\n\tif (const auto chat = asChat()) {\n\t\treturn chat->migrateTo() ? nullptr : chat;\n\t}\n\treturn nullptr;\n}\n\nconst ChatData *PeerData::asChatNotMigrated() const {\n\tif (const auto chat = asChat()) {\n\t\treturn chat->migrateTo() ? nullptr : chat;\n\t}\n\treturn nullptr;\n}\n\nChannelData *PeerData::asChannelOrMigrated() {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel;\n\t}\n\treturn migrateTo();\n}\n\nconst ChannelData *PeerData::asChannelOrMigrated() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel;\n\t}\n\treturn migrateTo();\n}\n\nChannelData *PeerData::asMonoforum() {\n\tconst auto channel = asMegagroup();\n\treturn (channel && channel->isMonoforum()) ? channel : nullptr;\n}\n\nconst ChannelData *PeerData::asMonoforum() const {\n\tconst auto channel = asMegagroup();\n\treturn (channel && channel->isMonoforum()) ? channel : nullptr;\n}\n\nChatData *PeerData::migrateFrom() const {\n\tif (const auto megagroup = asMegagroup()) {\n\t\treturn megagroup->amIn()\n\t\t\t? megagroup->getMigrateFromChat()\n\t\t\t: nullptr;\n\t}\n\treturn nullptr;\n}\n\nChannelData *PeerData::migrateTo() const {\n\tif (const auto chat = asChat()) {\n\t\tif (const auto result = chat->getMigrateToChannel()) {\n\t\t\treturn result->amIn() ? result : nullptr;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nnot_null<PeerData*> PeerData::migrateToOrMe() {\n\tif (const auto channel = migrateTo()) {\n\t\treturn channel;\n\t}\n\treturn this;\n}\n\nnot_null<const PeerData*> PeerData::migrateToOrMe() const {\n\tif (const auto channel = migrateTo()) {\n\t\treturn channel;\n\t}\n\treturn this;\n}\n\nnot_null<PeerData*> PeerData::userpicPaintingPeer() {\n\tif (const auto broadcast = monoforumBroadcast()) {\n\t\treturn broadcast;\n\t}\n\treturn this;\n}\n\nnot_null<const PeerData*> PeerData::userpicPaintingPeer() const {\n\treturn const_cast<PeerData*>(this)->userpicPaintingPeer();\n}\n\nUi::PeerUserpicShape PeerData::userpicShape() const {\n\treturn isForum() && !isBot()\n\t\t? Ui::PeerUserpicShape::Forum\n\t\t: isMonoforum()\n\t\t? Ui::PeerUserpicShape::Monoforum\n\t\t: Ui::PeerUserpicShape::Circle;\n}\n\nChannelData *PeerData::monoforumBroadcast() const {\n\tconst auto monoforum = asMonoforum();\n\treturn monoforum ? monoforum->monoforumLink() : nullptr;\n}\n\nChannelData *PeerData::broadcastMonoforum() const {\n\tconst auto broadcast = asBroadcast();\n\treturn broadcast ? broadcast->monoforumLink() : nullptr;\n}\n\nconst QString &PeerData::topBarNameText() const {\n\tif (const auto to = migrateTo()) {\n\t\treturn to->topBarNameText();\n\t} else if (const auto user = asUser()) {\n\t\tif (!user->nameOrPhone.isEmpty()) {\n\t\t\treturn user->nameOrPhone;\n\t\t}\n\t}\n\treturn _name;\n}\n\nint PeerData::nameVersion() const {\n\treturn _nameVersion;\n}\n\nconst QString &PeerData::name() const {\n\tif (const auto to = migrateTo()) {\n\t\treturn to->name();\n\t} else if (const auto broadcast = monoforumBroadcast()) {\n\t\treturn broadcast->name();\n\t}\n\treturn _name;\n}\n\nconst QString &PeerData::shortName() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->firstName.isEmpty() ? user->lastName : user->firstName;\n\t} else if (const auto to = migrateTo()) {\n\t\treturn to->shortName();\n\t} else if (const auto broadcast = monoforumBroadcast()) {\n\t\treturn broadcast->shortName();\n\t}\n\treturn _name;\n}\n\nQString PeerData::username() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->username();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->username();\n\t}\n\treturn QString();\n}\n\nQString PeerData::editableUsername() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->editableUsername();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->editableUsername();\n\t}\n\treturn QString();\n}\n\nconst std::vector<QString> &PeerData::usernames() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->usernames();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->usernames();\n\t}\n\tstatic const auto kEmpty = std::vector<QString>();\n\treturn kEmpty;\n}\n\nbool PeerData::isUsernameEditable(QString username) const {\n\tif (const auto user = asUser()) {\n\t\treturn user->isUsernameEditable(username);\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->isUsernameEditable(username);\n\t}\n\treturn false;\n}\n\nbool PeerData::changeColorCollectible(Ui::ColorCollectible data) {\n\tif (!_colorCollectible || (*_colorCollectible != data)) {\n\t\t// We don't reuse allocated object because in ChatStyle we\n\t\t// cache colors using std::weak_ptr as a key.\n\t\t_colorCollectible = std::make_shared<Ui::ColorCollectible>(\n\t\t\tstd::move(data));\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool PeerData::clearColorCollectible() {\n\tif (!_colorCollectible) {\n\t\treturn false;\n\t}\n\t_colorCollectible = nullptr;\n\treturn true;\n}\n\nbool PeerData::changeColorIndex(uint8 index) {\n\tindex %= Ui::kColorIndexCount;\n\tif (_colorIndexCloud && _colorIndex == index) {\n\t\treturn false;\n\t}\n\t_colorIndexCloud = 1;\n\t_colorIndex = index;\n\treturn true;\n}\n\nbool PeerData::clearColorIndex() {\n\tif (!_colorIndexCloud) {\n\t\treturn false;\n\t}\n\t_colorIndexCloud = 0;\n\t_colorIndex = Data::DecideColorIndex(id);\n\treturn true;\n}\n\nDocumentId PeerData::backgroundEmojiId() const {\n\treturn _backgroundEmojiId;\n}\n\nbool PeerData::changeBackgroundEmojiId(DocumentId id) {\n\tif (_backgroundEmojiId == id) {\n\t\treturn false;\n\t}\n\t_backgroundEmojiId = id;\n\treturn true;\n}\n\nbool PeerData::changeColorProfileCollectible(Ui::ColorCollectible data) {\n\tif (!_colorProfileCollectible || (*_colorProfileCollectible != data)) {\n\t\t_colorProfileCollectible = std::make_shared<Ui::ColorCollectible>(\n\t\t\tstd::move(data));\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool PeerData::changeColorProfileCollectible(\n\t\tconst tl::conditional<MTPPeerColor> &cloudColor) {\n\tif (!cloudColor) {\n\t\treturn clearColorProfileCollectible();\n\t}\n\treturn cloudColor->match([&](const MTPDpeerColorCollectible &data) {\n\t\treturn changeColorProfileCollectible(Data::ParseColorCollectible(data));\n\t}, [&](const MTPDpeerColor &) {\n\t\treturn clearColorProfileCollectible();\n\t}, [&](const MTPDinputPeerColorCollectible &) {\n\t\treturn clearColorProfileCollectible();\n\t});\n}\n\nbool PeerData::clearColorProfileCollectible() {\n\tif (!_colorProfileCollectible) {\n\t\treturn false;\n\t}\n\t_colorProfileCollectible = nullptr;\n\treturn true;\n}\n\nbool PeerData::changeColorProfileIndex(uint8 index) {\n\tindex %= Ui::kColorIndexCount;\n\tif (_colorProfileIndex == index) {\n\t\treturn false;\n\t}\n\t_colorProfileIndex = index;\n\treturn true;\n}\n\nbool PeerData::clearColorProfileIndex() {\n\tif (!_colorProfileIndex.has_value()) {\n\t\treturn false;\n\t}\n\t_colorProfileIndex = std::nullopt;\n\treturn true;\n}\n\nDocumentId PeerData::profileBackgroundEmojiId() const {\n\treturn _profileBackgroundEmojiId;\n}\n\nbool PeerData::changeProfileBackgroundEmojiId(DocumentId id) {\n\tif (_profileBackgroundEmojiId == id) {\n\t\treturn false;\n\t}\n\t_profileBackgroundEmojiId = id;\n\treturn true;\n}\n\nvoid PeerData::setEmojiStatus(const MTPEmojiStatus &status) {\n\tconst auto parsed = owner().emojiStatuses().parse(status);\n\tsetEmojiStatus(parsed.id, parsed.until);\n}\n\nvoid PeerData::setEmojiStatus(EmojiStatusId emojiStatusId, TimeId until) {\n\tif (_emojiStatusId != emojiStatusId) {\n\t\t_emojiStatusId = emojiStatusId;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::EmojiStatus);\n\t}\n\towner().emojiStatuses().registerAutomaticClear(this, until);\n}\n\nEmojiStatusId PeerData::emojiStatusId() const {\n\treturn _emojiStatusId;\n}\n\nbool PeerData::isBot() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->isBot();\n\t}\n\treturn false;\n}\n\nbool PeerData::isSelf() const {\n\tif (const auto user = asUser()) {\n\t\treturn (user->flags() & UserDataFlag::Self);\n\t}\n\treturn false;\n}\n\nbool PeerData::isVerified() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->isVerified();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->isVerified();\n\t}\n\treturn false;\n}\n\nbool PeerData::isPremium() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->isPremium();\n\t}\n\treturn false;\n}\n\nbool PeerData::isScam() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->isScam();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->isScam();\n\t}\n\treturn false;\n}\n\nbool PeerData::isFake() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->isFake();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->isFake();\n\t}\n\treturn false;\n}\n\nbool PeerData::isMegagroup() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel->isMegagroup();\n\t}\n\treturn false;\n}\n\nbool PeerData::isBroadcast() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel->isBroadcast();\n\t}\n\treturn false;\n}\n\nbool PeerData::isForum() const {\n\tif (const auto bot = asBot()) {\n\t\treturn bot->isForum();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->isForum();\n\t}\n\treturn false;\n}\n\nbool PeerData::isMonoforum() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel->isMonoforum();\n\t}\n\treturn false;\n}\n\nbool PeerData::isGigagroup() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel->isGigagroup();\n\t}\n\treturn false;\n}\n\nbool PeerData::isRepliesChat() const {\n\tconstexpr auto kProductionId = peerFromUser(1271266957);\n\tconstexpr auto kTestId = peerFromUser(708513);\n\tif (id != kTestId && id != kProductionId) {\n\t\treturn false;\n\t}\n\treturn ((session().mtp().environment() == MTP::Environment::Production)\n\t\t? kProductionId\n\t\t: kTestId) == id;\n}\n\nbool PeerData::isVerifyCodes() const {\n\tconstexpr auto kVerifyCodesId = peerFromUser(489000);\n\treturn (id == kVerifyCodesId);\n}\n\nbool PeerData::isFreezeAppealChat() const {\n\treturn username().compare(u\"spambot\"_q, Qt::CaseInsensitive) == 0;\n}\n\nbool PeerData::sharedMediaInfo() const {\n\treturn isSelf() || isRepliesChat();\n}\n\nbool PeerData::savedSublistsInfo() const {\n\treturn isSelf() && owner().savedMessages().supported();\n}\n\nbool PeerData::hasStoriesHidden() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->hasStoriesHidden();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->hasStoriesHidden();\n\t}\n\treturn false;\n}\n\nvoid PeerData::setStoriesHidden(bool hidden) {\n\tif (const auto user = asUser()) {\n\t\tuser->setFlags(hidden\n\t\t\t? (user->flags() | UserDataFlag::StoriesHidden)\n\t\t\t: (user->flags() & ~UserDataFlag::StoriesHidden));\n\t} else if (const auto channel = asChannel()) {\n\t\tchannel->setFlags(hidden\n\t\t\t? (channel->flags() | ChannelDataFlag::StoriesHidden)\n\t\t\t: (channel->flags() & ~ChannelDataFlag::StoriesHidden));\n\t} else {\n\t\tUnexpected(\"PeerData::setStoriesHidden for non-user/non-channel.\");\n\t}\n}\n\nUi::BotVerifyDetails *PeerData::botVerifyDetails() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->botVerifyDetails();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->botVerifyDetails();\n\t}\n\treturn nullptr;\n}\n\nData::Forum *PeerData::forum() const {\n\tif (const auto bot = asBot()) {\n\t\treturn bot->forum();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->forum();\n\t}\n\treturn nullptr;\n}\n\nData::ForumTopic *PeerData::forumTopicFor(MsgId rootId) const {\n\tif (!rootId) {\n\t\treturn nullptr;\n\t} else if (const auto forum = this->forum()) {\n\t\treturn forum->topicFor(rootId);\n\t}\n\treturn nullptr;\n}\n\nData::SavedMessages *PeerData::monoforum() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel->monoforum();\n\t}\n\treturn nullptr;\n}\n\nData::SavedSublist *PeerData::monoforumSublistFor(\n\t\tPeerId sublistPeerId) const {\n\tif (!sublistPeerId) {\n\t\treturn nullptr;\n\t} else if (const auto monoforum = this->monoforum()) {\n\t\treturn monoforum->sublistLoaded(owner().peer(sublistPeerId));\n\t}\n\treturn nullptr;\n}\n\nbool PeerData::useSubsectionTabs() const {\n\tif (const auto bot = asBot()) {\n\t\treturn bot->isForum();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->useSubsectionTabs();\n\t}\n\treturn false;\n}\n\nbool PeerData::viewForumAsMessages() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel->viewForumAsMessages();\n\t}\n\treturn false;\n}\n\nvoid PeerData::processTopics(const MTPVector<MTPForumTopic> &topics) {\n\tif (const auto forum = this->forum()) {\n\t\tforum->applyReceivedTopics(topics);\n\t}\n}\n\nbool PeerData::allowsForwarding() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->allowsForwarding();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->allowsForwarding();\n\t} else if (const auto chat = asChat()) {\n\t\treturn chat->allowsForwarding();\n\t}\n\treturn false;\n}\n\nData::RestrictionCheckResult PeerData::amRestricted(\n\t\tChatRestriction right) const {\n\tusing Result = Data::RestrictionCheckResult;\n\tconst auto allowByAdminRights = [](auto right, auto chat) -> bool {\n\t\tif (right == ChatRestriction::AddParticipants) {\n\t\t\treturn chat->adminRights() & ChatAdminRight::InviteByLinkOrAdd;\n\t\t} else if (right == ChatRestriction::ChangeInfo) {\n\t\t\treturn chat->adminRights() & ChatAdminRight::ChangeInfo;\n\t\t} else if (right == ChatRestriction::CreateTopics) {\n\t\t\treturn chat->adminRights() & ChatAdminRight::ManageTopics;\n\t\t} else if (right == ChatRestriction::PinMessages) {\n\t\t\treturn chat->adminRights() & ChatAdminRight::PinMessages;\n\t\t} else {\n\t\t\treturn chat->hasAdminRights();\n\t\t}\n\t};\n\tif (const auto user = asUser()) {\n\t\tif (user->requiresPremiumToWrite() && !user->session().premium()) {\n\t\t\treturn Result::Explicit();\n\t\t}\n\t\treturn (right == ChatRestriction::SendVoiceMessages\n\t\t\t|| right == ChatRestriction::SendVideoMessages)\n\t\t\t? ((user->flags() & UserDataFlag::VoiceMessagesForbidden)\n\t\t\t\t? Result::Explicit()\n\t\t\t\t: Result::Allowed())\n\t\t\t: (right == ChatRestriction::PinMessages)\n\t\t\t? ((user->flags() & UserDataFlag::CanPinMessages)\n\t\t\t\t? Result::Allowed()\n\t\t\t\t: Result::Explicit())\n\t\t\t: Result::Allowed();\n\t} else if (const auto channel = asChannel()) {\n\t\tif (channel->monoforumDisabled()) {\n\t\t\treturn Result::WithEveryone();\n\t\t}\n\t\tconst auto defaultRestrictions = channel->defaultRestrictions()\n\t\t\t| (channel->isPublic()\n\t\t\t\t? (ChatRestriction::PinMessages\n\t\t\t\t\t| ChatRestriction::ChangeInfo)\n\t\t\t\t: ChatRestrictions(0));\n\t\treturn (channel->amCreator() || allowByAdminRights(right, channel))\n\t\t\t? Result::Allowed()\n\t\t\t: ((defaultRestrictions & right)\n\t\t\t\t&& !channel->unrestrictedByBoosts())\n\t\t\t? Result::WithEveryone()\n\t\t\t: (channel->restrictions() & right)\n\t\t\t? Result::Explicit()\n\t\t\t: Result::Allowed();\n\t} else if (const auto chat = asChat()) {\n\t\treturn (chat->amCreator() || allowByAdminRights(right, chat))\n\t\t\t? Result::Allowed()\n\t\t\t: (chat->defaultRestrictions() & right)\n\t\t\t? Result::WithEveryone()\n\t\t\t: Result::Allowed();\n\t}\n\treturn Result::Allowed();\n}\n\nbool PeerData::amAnonymous() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel->isBroadcast()\n\t\t\t? !channel->signatureProfiles()\n\t\t\t: (channel->adminRights() & ChatAdminRight::Anonymous);\n\t}\n\treturn false;\n}\n\nbool PeerData::canRevokeFullHistory() const {\n\tif (const auto user = asUser()) {\n\t\treturn !isSelf()\n\t\t\t&& (!user->isBot() || user->isSupport())\n\t\t\t&& !user->isInaccessible()\n\t\t\t&& session().serverConfig().revokePrivateInbox\n\t\t\t&& (session().serverConfig().revokePrivateTimeLimit == 0x7FFFFFFF);\n\t} else if (const auto chat = asChat()) {\n\t\treturn chat->amCreator();\n\t} else if (const auto megagroup = asMegagroup()) {\n\t\treturn megagroup->amCreator()\n\t\t\t&& megagroup->membersCountKnown()\n\t\t\t&& megagroup->canDelete()\n\t\t\t&& !megagroup->isMonoforum();\n\t}\n\treturn false;\n}\n\nbool PeerData::slowmodeApplied() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn !channel->amCreator()\n\t\t\t&& !channel->hasAdminRights()\n\t\t\t&& (channel->flags() & ChannelDataFlag::SlowmodeEnabled);\n\t}\n\treturn false;\n}\n\nrpl::producer<bool> PeerData::slowmodeAppliedValue() const {\n\tusing namespace rpl::mappers;\n\tconst auto channel = asChannel();\n\tif (!channel) {\n\t\treturn rpl::single(false);\n\t}\n\n\tauto hasAdminRights = channel->adminRightsValue(\n\t) | rpl::map([=] {\n\t\treturn channel->hasAdminRights();\n\t}) | rpl::distinct_until_changed();\n\n\tauto slowmodeEnabled = channel->flagsValue(\n\t) | rpl::filter([=](const ChannelData::Flags::Change &change) {\n\t\treturn (change.diff & ChannelDataFlag::SlowmodeEnabled) != 0;\n\t}) | rpl::map([=](const ChannelData::Flags::Change &change) {\n\t\treturn (change.value & ChannelDataFlag::SlowmodeEnabled) != 0;\n\t}) | rpl::distinct_until_changed();\n\n\treturn rpl::combine(\n\t\tstd::move(hasAdminRights),\n\t\tstd::move(slowmodeEnabled),\n\t\t!_1 && _2);\n}\n\nint PeerData::slowmodeSecondsLeft() const {\n\tif (const auto channel = asChannel()) {\n\t\tif (const auto seconds = channel->slowmodeSeconds()) {\n\t\t\tif (const auto last = channel->slowmodeLastMessage()) {\n\t\t\t\tconst auto now = base::unixtime::now();\n\t\t\t\treturn std::max(seconds - (now - last), 0);\n\t\t\t}\n\t\t}\n\t}\n\treturn 0;\n}\n\nbool PeerData::canManageGroupCall() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->isSelf();\n\t} else if (const auto chat = asChat()) {\n\t\treturn chat->amCreator()\n\t\t\t|| (chat->adminRights() & ChatAdminRight::ManageCall);\n\t} else if (const auto group = asChannel()) {\n\t\tif (group->isMonoforum()) {\n\t\t\treturn false;\n\t\t}\n\t\treturn group->amCreator()\n\t\t\t|| (group->adminRights() & ChatAdminRight::ManageCall);\n\t}\n\tUnexpected(\"Peer type in PeerData::canManageGroupCall.\");\n}\n\nbool PeerData::canManageRanks() const {\n\tif (const auto chat = asChat()) {\n\t\treturn chat->amCreator()\n\t\t\t|| (chat->adminRights() & ChatAdminRight::ManageRanks);\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->amCreator()\n\t\t\t|| (channel->adminRights() & ChatAdminRight::ManageRanks);\n\t}\n\treturn false;\n}\n\nbool PeerData::amMonoforumAdmin() const {\n\tif (const auto channel = asChannel()) {\n\t\treturn channel->flags() & ChannelDataFlag::MonoforumAdmin;\n\t}\n\treturn false;\n}\n\nint PeerData::starsPerMessage() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->starsPerMessage();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->starsPerMessage();\n\t}\n\treturn 0;\n}\n\nint PeerData::starsPerMessageChecked() const {\n\tif (const auto channel = asChannel()) {\n\t\tif (channel->adminRights()\n\t\t\t|| channel->amCreator()\n\t\t\t|| amMonoforumAdmin()) {\n\t\t\treturn 0;\n\t\t}\n\t}\n\treturn starsPerMessage();\n}\n\nData::StarsRating PeerData::starsRating() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->starsRating();\n\t}\n\treturn {};\n}\n\nData::GroupCall *PeerData::groupCall() const {\n\tif (const auto chat = asChat()) {\n\t\treturn chat->groupCall();\n\t} else if (const auto group = asChannel()) {\n\t\treturn group->groupCall();\n\t}\n\treturn nullptr;\n}\n\nPeerId PeerData::groupCallDefaultJoinAs() const {\n\tif (const auto chat = asChat()) {\n\t\treturn chat->groupCallDefaultJoinAs();\n\t} else if (const auto group = asChannel()) {\n\t\treturn group->groupCallDefaultJoinAs();\n\t}\n\treturn 0;\n}\n\nvoid PeerData::setThemeToken(const QString &token) {\n\tif (_themeToken == token) {\n\t\treturn;\n\t} else if (!token.startsWith(u\"gift:\"_q)\n\t\t&& Ui::Emoji::Find(_themeToken) == Ui::Emoji::Find(token)) {\n\t\t_themeToken = token;\n\t\treturn;\n\t}\n\t_themeToken = token;\n\tif (!token.isEmpty() && !owner().cloudThemes().themeForToken(token)) {\n\t\towner().cloudThemes().refreshChatThemesFor(token);\n\t}\n\tsession().changes().peerUpdated(this, UpdateFlag::ChatThemeToken);\n}\n\nconst QString &PeerData::themeToken() const {\n\treturn _themeToken;\n}\n\nvoid PeerData::setWallPaper(\n\t\tstd::optional<Data::WallPaper> paper,\n\t\tbool overriden) {\n\tconst auto paperChanged = (paper || _wallPaper)\n\t\t&& (!paper || !_wallPaper || !_wallPaper->equals(*paper));\n\tif (paperChanged) {\n\t\t_wallPaper = paper\n\t\t\t? std::make_unique<Data::WallPaper>(std::move(*paper))\n\t\t\t: nullptr;\n\t}\n\n\tconst auto overridenValue = overriden ? 1 : 0;\n\tconst auto overridenChanged = (_wallPaperOverriden != overridenValue);\n\tif (overridenChanged) {\n\t\t_wallPaperOverriden = overridenValue;\n\t}\n\n\tif (paperChanged || overridenChanged) {\n\t\tsession().changes().peerUpdated(this, UpdateFlag::ChatWallPaper);\n\t}\n}\n\nbool PeerData::wallPaperOverriden() const {\n\treturn _wallPaperOverriden != 0;\n}\n\nconst Data::WallPaper *PeerData::wallPaper() const {\n\treturn _wallPaper.get();\n}\n\nbool PeerData::hasActiveStories() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->hasActiveStories();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->hasActiveStories();\n\t}\n\treturn false;\n}\n\nbool PeerData::hasUnreadStories() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->hasUnreadStories();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->hasUnreadStories();\n\t}\n\treturn false;\n}\n\nbool PeerData::hasActiveVideoStream() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->hasActiveVideoStream();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->hasActiveVideoStream();\n\t}\n\treturn false;\n}\n\nvoid PeerData::setStoriesState(StoriesState state) {\n\tif (const auto user = asUser()) {\n\t\treturn user->setStoriesState(state);\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->setStoriesState(state);\n\t} else {\n\t\tUnexpected(\"PeerData::setStoriesState for non-user/non-channel.\");\n\t}\n}\n\nint PeerData::peerGiftsCount() const {\n\tif (const auto user = asUser()) {\n\t\treturn user->peerGiftsCount();\n\t} else if (const auto channel = asChannel()) {\n\t\treturn channel->peerGiftsCount();\n\t}\n\treturn 0;\n}\n\nMTPInputPeer PeerData::input() const {\n\tif (const auto user = asUser()) {\n\t\tconst auto specific = user->inputUser();\n\t\treturn specific.match([](const MTPDinputUser &data) {\n\t\t\treturn MTP_inputPeerUser(data.vuser_id(), data.vaccess_hash());\n\t\t}, [](const MTPDinputUserFromMessage &data) {\n\t\t\treturn MTP_inputPeerUserFromMessage(\n\t\t\t\tdata.vpeer(),\n\t\t\t\tdata.vmsg_id(),\n\t\t\t\tdata.vuser_id());\n\t\t}, [](const MTPDinputUserEmpty &) {\n\t\t\treturn MTP_inputPeerEmpty();\n\t\t}, [](const MTPDinputUserSelf &) {\n\t\t\treturn MTP_inputPeerSelf();\n\t\t});\n\t} else if (const auto chat = asChat()) {\n\t\treturn MTP_inputPeerChat(chat->inputChat());\n\t} else if (const auto channel = asChannel()) {\n\t\tconst auto &specific = channel->inputChannel();\n\t\treturn specific.match([](const MTPDinputChannel &data) {\n\t\t\treturn MTP_inputPeerChannel(\n\t\t\t\tdata.vchannel_id(),\n\t\t\t\tdata.vaccess_hash());\n\t\t}, [](const MTPDinputChannelFromMessage &data) {\n\t\t\treturn MTP_inputPeerChannelFromMessage(\n\t\t\t\tdata.vpeer(),\n\t\t\t\tdata.vmsg_id(),\n\t\t\t\tdata.vchannel_id());\n\t\t}, [](const MTPDinputChannelEmpty &) {\n\t\t\treturn MTP_inputPeerEmpty();\n\t\t});\n\t}\n\treturn MTP_inputPeerEmpty();\n}\n\nvoid PeerData::setIsBlocked(bool is) {\n\tconst auto status = is\n\t\t? BlockStatus::Blocked\n\t\t: BlockStatus::NotBlocked;\n\tif (_blockStatus != status) {\n\t\t_blockStatus = status;\n\t\tif (const auto user = asUser()) {\n\t\t\tconst auto flags = user->flags();\n\t\t\tif (is) {\n\t\t\t\tuser->setFlags(flags | UserDataFlag::Blocked);\n\t\t\t} else {\n\t\t\t\tuser->setFlags(flags & ~UserDataFlag::Blocked);\n\t\t\t}\n\t\t}\n\t\tsession().changes().peerUpdated(this, UpdateFlag::IsBlocked);\n\t\tCore::App().notifications().checkDelayed();\n\t}\n}\n\nvoid PeerData::setLoadedStatus(LoadedStatus status) {\n\t_loadedStatus = status;\n}\n\nTimeId PeerData::messagesTTL() const {\n\treturn _ttlPeriod;\n}\n\nvoid PeerData::setMessagesTTL(TimeId period) {\n\tif (_ttlPeriod != period) {\n\t\t_ttlPeriod = period;\n\t\tsession().changes().peerUpdated(\n\t\t\tthis,\n\t\t\tData::PeerUpdate::Flag::MessagesTTL);\n\t}\n}\n\nnamespace Data {\n\nvoid SetTopPinnedMessageId(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId messageId) {\n\tif (const auto channel = peer->asChannel()) {\n\t\tif (messageId <= channel->availableMinId()) {\n\t\t\treturn;\n\t\t}\n\t}\n\tauto &session = peer->session();\n\tconst auto hiddenId = session.settings().hiddenPinnedMessageId(peer->id);\n\tif (hiddenId != 0 && hiddenId != messageId) {\n\t\tsession.settings().setHiddenPinnedMessageId(\n\t\t\tpeer->id,\n\t\t\tMsgId(0), // topicRootId\n\t\t\tPeerId(0), // monoforumPeerId\n\t\t\t0);\n\t\tsession.saveSettingsDelayed();\n\t}\n\tsession.storage().add(Storage::SharedMediaAddExisting(\n\t\tpeer->id,\n\t\tMsgId(0), // topicRootId\n\t\tPeerId(0), // monoforumPeerId\n\t\tStorage::SharedMediaType::Pinned,\n\t\tmessageId,\n\t\t{ messageId, ServerMaxMsgId }));\n\tpeer->owner().history(peer)->setHasPinnedMessages(true);\n}\n\nFullMsgId ResolveTopPinnedId(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tPeerData *migrated) {\n\tconst auto slice = peer->session().storage().snapshot(\n\t\tStorage::SharedMediaQuery(\n\t\t\tStorage::SharedMediaKey(\n\t\t\t\tpeer->id,\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tStorage::SharedMediaType::Pinned,\n\t\t\t\tServerMaxMsgId - 1),\n\t\t\t1,\n\t\t\t1));\n\tconst auto old = (!topicRootId && !monoforumPeerId && migrated)\n\t\t? migrated->session().storage().snapshot(\n\t\t\tStorage::SharedMediaQuery(\n\t\t\t\tStorage::SharedMediaKey(\n\t\t\t\t\tmigrated->id,\n\t\t\t\t\tMsgId(0), // topicRootId\n\t\t\t\t\tPeerId(0), // monoforumPeerId\n\t\t\t\t\tStorage::SharedMediaType::Pinned,\n\t\t\t\t\tServerMaxMsgId - 1),\n\t\t\t\t1,\n\t\t\t\t1))\n\t\t: Storage::SharedMediaResult{\n\t\t\t.count = 0,\n\t\t\t.skippedBefore = 0,\n\t\t\t.skippedAfter = 0,\n\t\t};\n\tif (!slice.messageIds.empty()) {\n\t\treturn FullMsgId(peer->id, slice.messageIds.back());\n\t} else if (!migrated || slice.count != 0 || old.messageIds.empty()) {\n\t\treturn FullMsgId();\n\t} else {\n\t\treturn FullMsgId(migrated->id, old.messageIds.back());\n\t}\n}\n\nFullMsgId ResolveMinPinnedId(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tPeerData *migrated) {\n\tconst auto slice = peer->session().storage().snapshot(\n\t\tStorage::SharedMediaQuery(\n\t\t\tStorage::SharedMediaKey(\n\t\t\t\tpeer->id,\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tStorage::SharedMediaType::Pinned,\n\t\t\t\t1),\n\t\t\t1,\n\t\t\t1));\n\tconst auto old = (!topicRootId && !monoforumPeerId && migrated)\n\t\t? migrated->session().storage().snapshot(\n\t\t\tStorage::SharedMediaQuery(\n\t\t\t\tStorage::SharedMediaKey(\n\t\t\t\t\tmigrated->id,\n\t\t\t\t\tMsgId(0), // topicRootId\n\t\t\t\t\tPeerId(0), // monoforumPeerId\n\t\t\t\t\tStorage::SharedMediaType::Pinned,\n\t\t\t\t\t1),\n\t\t\t\t1,\n\t\t\t\t1))\n\t\t: Storage::SharedMediaResult{\n\t\t\t.count = 0,\n\t\t\t.skippedBefore = 0,\n\t\t\t.skippedAfter = 0,\n\t\t};\n\tif (!old.messageIds.empty()) {\n\t\treturn FullMsgId(migrated->id, old.messageIds.front());\n\t} else if (old.count == 0 && !slice.messageIds.empty()) {\n\t\treturn FullMsgId(peer->id, slice.messageIds.front());\n\t} else {\n\t\treturn FullMsgId();\n\t}\n}\n\nuint64 BackgroundEmojiIdFromColor(const MTPPeerColor *color) {\n\tif (!color) {\n\t\treturn 0;\n\t}\n\treturn color->match([](const MTPDpeerColor &data) -> uint64 {\n\t\treturn data.vbackground_emoji_id().value_or_empty();\n\t}, [](const MTPDpeerColorCollectible &data) -> uint64 {\n\t\treturn data.vbackground_emoji_id().v;\n\t}, [](const MTPDinputPeerColorCollectible &data) -> uint64 {\n\t\treturn 0;\n\t});\n}\n\nstd::optional<uint8> ColorIndexFromColor(const MTPPeerColor *color) {\n\tif (!color) {\n\t\treturn std::nullopt;\n\t}\n\treturn color->match([](const MTPDpeerColor &d) -> std::optional<uint8> {\n\t\treturn d.vcolor() ? std::make_optional(d.vcolor()->v) : std::nullopt;\n\t}, [](const auto &) -> std::optional<uint8> {\n\t\treturn std::nullopt;\n\t});\n}\n\nbool IsBotUserCreatesTopics(not_null<PeerData*> peer) {\n\tif (const auto user = peer->asUser()) {\n\t\treturn user->botInfo && user->botInfo->userCreatesTopics;\n\t}\n\treturn false;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_peer.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/notify/data_peer_notify_settings.h\"\n#include \"data/data_types.h\"\n#include \"data/data_flags.h\"\n#include \"data/data_cloud_file.h\"\n#include \"data/data_peer_common.h\"\n#include \"ui/userpic_view.h\"\n\nstruct BotInfo;\nclass PeerData;\nclass UserData;\nclass ChatData;\nclass ChannelData;\n\nenum class ChatRestriction;\n\nnamespace Ui {\nclass EmptyUserpic;\nstruct BotVerifyDetails;\nstruct ColorCollectible;\n} // namespace Ui\n\nnamespace Main {\nclass Account;\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nclass Forum;\nclass ForumTopic;\nclass Session;\nclass GroupCall;\nclass SavedMessages;\nclass SavedSublist;\nstruct ReactionId;\nclass WallPaper;\n\n[[nodiscard]] uint8 DecideColorIndex(PeerId peerId);\n\n// Must be used only for PeerColor-s.\n[[nodiscard]] PeerId FakePeerIdForJustName(const QString &name);\n\nclass RestrictionCheckResult {\npublic:\n\t[[nodiscard]] static RestrictionCheckResult Allowed() {\n\t\treturn { 0 };\n\t}\n\t[[nodiscard]] static RestrictionCheckResult WithEveryone() {\n\t\treturn { 1 };\n\t}\n\t[[nodiscard]] static RestrictionCheckResult Explicit() {\n\t\treturn { 2 };\n\t}\n\n\texplicit operator bool() const {\n\t\treturn (_value != 0);\n\t}\n\n\tbool operator==(const RestrictionCheckResult &other) const {\n\t\treturn (_value == other._value);\n\t}\n\tbool operator!=(const RestrictionCheckResult &other) const {\n\t\treturn !(*this == other);\n\t}\n\n\t[[nodiscard]] bool isAllowed() const {\n\t\treturn (*this == Allowed());\n\t}\n\t[[nodiscard]] bool isWithEveryone() const {\n\t\treturn (*this == WithEveryone());\n\t}\n\t[[nodiscard]] bool isExplicit() const {\n\t\treturn (*this == Explicit());\n\t}\n\nprivate:\n\tRestrictionCheckResult(int value) : _value(value) {\n\t}\n\n\tint _value = 0;\n\n};\n\nstruct UnavailableReason {\n\tQString reason;\n\tQString text;\n\n\tfriend inline bool operator==(\n\t\tconst UnavailableReason &,\n\t\tconst UnavailableReason &) = default;\n\n\t[[nodiscard]] bool sensitive() const;\n\t[[nodiscard]] static UnavailableReason Sensitive();\n\n\t[[nodiscard]] static QString Compute(\n\t\tnot_null<Main::Session*> session,\n\t\tconst std::vector<UnavailableReason> &list);\n\t[[nodiscard]] static bool IgnoreSensitiveMark(\n\t\tnot_null<Main::Session*> session);\n\n\t[[nodiscard]] static std::vector<UnavailableReason> Extract(\n\t\tconst MTPvector<MTPRestrictionReason> *list);\n};\n\nbool ApplyBotMenuButton(\n\tnot_null<BotInfo*> info,\n\tconst MTPBotMenuButton *button);\n\nenum class AllowedReactionsType : uchar {\n\tAll,\n\tDefault,\n\tSome,\n};\n\nstruct AllowedReactions {\n\tstd::vector<ReactionId> some;\n\tint maxCount = 0;\n\tAllowedReactionsType type = AllowedReactionsType::Some;\n\tbool paidEnabled = false;\n\n\tfriend inline bool operator==(\n\t\tconst AllowedReactions &,\n\t\tconst AllowedReactions &) = default;\n};\n\n[[nodiscard]] AllowedReactions Parse(\n\tconst MTPChatReactions &value,\n\tint maxCount,\n\tbool paidEnabled);\n[[nodiscard]] PeerData *PeerFromInputMTP(\n\tnot_null<Session*> owner,\n\tconst MTPInputPeer &input);\n[[nodiscard]] UserData *UserFromInputMTP(\n\tnot_null<Session*> owner,\n\tconst MTPInputUser &input);\n\n[[nodiscard]] Ui::ColorCollectible ParseColorCollectible(\n\tconst MTPDpeerColorCollectible &data);\n\n} // namespace Data\n\nclass PeerClickHandler : public ClickHandler {\npublic:\n\tPeerClickHandler(not_null<PeerData*> peer);\n\tvoid onClick(ClickContext context) const override;\n\n\tnot_null<PeerData*> peer() const {\n\t\treturn _peer;\n\t}\n\nprivate:\n\tnot_null<PeerData*> _peer;\n\n};\n\nenum class PeerBarSetting {\n\tReportSpam = (1 << 0),\n\tAddContact = (1 << 1),\n\tBlockContact = (1 << 2),\n\tShareContact = (1 << 3),\n\tNeedContactsException = (1 << 4),\n\tAutoArchived = (1 << 5),\n\tRequestChat = (1 << 6),\n\tRequestChatIsBroadcast = (1 << 7),\n\tHasBusinessBot = (1 << 8),\n\tBusinessBotPaused = (1 << 9),\n\tBusinessBotCanReply = (1 << 10),\n\tUnknown = (1 << 11),\n};\ninline constexpr bool is_flag_type(PeerBarSetting) { return true; };\nusing PeerBarSettings = base::flags<PeerBarSetting>;\n\nstruct PeerBarDetails {\n\tQString phoneCountryCode;\n\tint registrationDate = 0; // YYYYMM or 0, YYYY > 2012, MM > 0.\n\tTimeId nameChangeDate = 0;\n\tTimeId photoChangeDate = 0;\n\tQString requestChatTitle;\n\tTimeId requestChatDate;\n\tUserData *businessBot = nullptr;\n\tQString businessBotManageUrl;\n\tint paysPerMessage = 0;\n};\n\nstruct PaintUserpicContext {\n\tQPoint position;\n\tint size = 0;\n\tUi::PeerUserpicShape shape = Ui::PeerUserpicShape::Auto;\n};\n\nclass PeerData {\nprotected:\n\tPeerData(not_null<Data::Session*> owner, PeerId id);\n\tPeerData(const PeerData &other) = delete;\n\tPeerData &operator=(const PeerData &other) = delete;\n\npublic:\n\tusing BarSettings = Data::Flags<PeerBarSettings>;\n\n\tvirtual ~PeerData();\n\n\tstatic constexpr auto kServiceNotificationsId = peerFromUser(777000);\n\tstatic constexpr auto kSavedHiddenAuthorId = peerFromUser(2666000);\n\n\t[[nodiscard]] Data::Session &owner() const;\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] Main::Account &account() const;\n\n\t[[nodiscard]] uint8 colorIndex() const {\n\t\treturn _colorIndex;\n\t}\n\t[[nodiscard]] auto colorCollectible() const\n\t-> const std::shared_ptr<Ui::ColorCollectible> & {\n\t\treturn _colorCollectible;\n\t}\n\tbool changeColorCollectible(Ui::ColorCollectible data);\n\tbool clearColorCollectible();\n\tbool changeColorIndex(uint8 index);\n\tbool clearColorIndex();\n\t[[nodiscard]] DocumentId backgroundEmojiId() const;\n\tbool changeBackgroundEmojiId(DocumentId id);\n\n\t[[nodiscard]] std::optional<uint8> colorProfileIndex() const {\n\t\treturn _colorProfileIndex;\n\t}\n\t[[nodiscard]] auto colorProfileCollectible() const\n\t-> const std::shared_ptr<Ui::ColorCollectible> & {\n\t\treturn _colorProfileCollectible;\n\t}\n\tbool changeColorProfileCollectible(Ui::ColorCollectible data);\n\tbool changeColorProfileCollectible(\n\t\tconst tl::conditional<MTPPeerColor> &cloudColor);\n\tbool clearColorProfileCollectible();\n\tbool changeColorProfileIndex(uint8 index);\n\tbool clearColorProfileIndex();\n\t[[nodiscard]] DocumentId profileBackgroundEmojiId() const;\n\tbool changeProfileBackgroundEmojiId(DocumentId id);\n\n\tvoid setEmojiStatus(const MTPEmojiStatus &status);\n\tvoid setEmojiStatus(EmojiStatusId emojiStatusId, TimeId until = 0);\n\t[[nodiscard]] EmojiStatusId emojiStatusId() const;\n\n\t[[nodiscard]] bool isUser() const {\n\t\treturn peerIsUser(id);\n\t}\n\t[[nodiscard]] bool isChat() const {\n\t\treturn peerIsChat(id);\n\t}\n\t[[nodiscard]] bool isChannel() const {\n\t\treturn peerIsChannel(id);\n\t}\n\t[[nodiscard]] bool isBot() const;\n\t[[nodiscard]] bool isSelf() const;\n\t[[nodiscard]] bool isVerified() const;\n\t[[nodiscard]] bool isPremium() const;\n\t[[nodiscard]] bool isScam() const;\n\t[[nodiscard]] bool isFake() const;\n\t[[nodiscard]] bool isMegagroup() const;\n\t[[nodiscard]] bool isBroadcast() const;\n\t[[nodiscard]] bool isForum() const;\n\t[[nodiscard]] bool isMonoforum() const;\n\t[[nodiscard]] bool isGigagroup() const;\n\t[[nodiscard]] bool isRepliesChat() const;\n\t[[nodiscard]] bool isVerifyCodes() const;\n\t[[nodiscard]] bool isFreezeAppealChat() const;\n\t[[nodiscard]] bool sharedMediaInfo() const;\n\t[[nodiscard]] bool savedSublistsInfo() const;\n\t[[nodiscard]] bool hasStoriesHidden() const;\n\tvoid setStoriesHidden(bool hidden);\n\n\t[[nodiscard]] Ui::BotVerifyDetails *botVerifyDetails() const;\n\n\t[[nodiscard]] bool isNotificationsUser() const {\n\t\treturn (id == peerFromUser(333000))\n\t\t\t|| (id == kServiceNotificationsId);\n\t}\n\t[[nodiscard]] bool isServiceUser() const {\n\t\treturn isUser() && !(id.value % 1000);\n\t}\n\t[[nodiscard]] bool isSavedHiddenAuthor() const {\n\t\treturn (id == kSavedHiddenAuthorId);\n\t}\n\n\t[[nodiscard]] Data::Forum *forum() const;\n\t[[nodiscard]] Data::ForumTopic *forumTopicFor(MsgId rootId) const;\n\n\t[[nodiscard]] Data::SavedMessages *monoforum() const;\n\t[[nodiscard]] Data::SavedSublist *monoforumSublistFor(\n\t\tPeerId sublistPeerId) const;\n\n\t[[nodiscard]] bool useSubsectionTabs() const;\n\t[[nodiscard]] bool viewForumAsMessages() const;\n\tvoid processTopics(const MTPVector<MTPForumTopic> &topics);\n\n\t[[nodiscard]] Data::PeerNotifySettings &notify() {\n\t\treturn _notify;\n\t}\n\t[[nodiscard]] const Data::PeerNotifySettings &notify() const {\n\t\treturn _notify;\n\t}\n\n\t[[nodiscard]] bool allowsForwarding() const;\n\t[[nodiscard]] Data::RestrictionCheckResult amRestricted(\n\t\tChatRestriction right) const;\n\t[[nodiscard]] bool amAnonymous() const;\n\t[[nodiscard]] bool canRevokeFullHistory() const;\n\t[[nodiscard]] bool slowmodeApplied() const;\n\t[[nodiscard]] rpl::producer<bool> slowmodeAppliedValue() const;\n\t[[nodiscard]] int slowmodeSecondsLeft() const;\n\t[[nodiscard]] bool canManageGroupCall() const;\n\t[[nodiscard]] bool canManageRanks() const;\n\t[[nodiscard]] bool amMonoforumAdmin() const;\n\n\t[[nodiscard]] int starsPerMessage() const;\n\t[[nodiscard]] int starsPerMessageChecked() const;\n\t[[nodiscard]] Data::StarsRating starsRating() const;\n\n\t[[nodiscard]] UserData *asBot();\n\t[[nodiscard]] const UserData *asBot() const;\n\t[[nodiscard]] UserData *asUser();\n\t[[nodiscard]] const UserData *asUser() const;\n\t[[nodiscard]] ChatData *asChat();\n\t[[nodiscard]] const ChatData *asChat() const;\n\t[[nodiscard]] ChannelData *asChannel();\n\t[[nodiscard]] const ChannelData *asChannel() const;\n\t[[nodiscard]] ChannelData *asMegagroup();\n\t[[nodiscard]] const ChannelData *asMegagroup() const;\n\t[[nodiscard]] ChannelData *asBroadcast();\n\t[[nodiscard]] const ChannelData *asBroadcast() const;\n\t[[nodiscard]] ChatData *asChatNotMigrated();\n\t[[nodiscard]] const ChatData *asChatNotMigrated() const;\n\t[[nodiscard]] ChannelData *asChannelOrMigrated();\n\t[[nodiscard]] const ChannelData *asChannelOrMigrated() const;\n\t[[nodiscard]] ChannelData *asMonoforum();\n\t[[nodiscard]] const ChannelData *asMonoforum() const;\n\n\t[[nodiscard]] ChatData *migrateFrom() const;\n\t[[nodiscard]] ChannelData *migrateTo() const;\n\t[[nodiscard]] not_null<PeerData*> migrateToOrMe();\n\t[[nodiscard]] not_null<const PeerData*> migrateToOrMe() const;\n\t[[nodiscard]] not_null<PeerData*> userpicPaintingPeer();\n\t[[nodiscard]] not_null<const PeerData*> userpicPaintingPeer() const;\n\t[[nodiscard]] Ui::PeerUserpicShape userpicShape() const;\n\n\t// isMonoforum() ? monoforumLink() : nullptr\n\t[[nodiscard]] ChannelData *monoforumBroadcast() const;\n\n\t// isMonoforum() ? nullptr : monoforumLink()\n\t[[nodiscard]] ChannelData *broadcastMonoforum() const;\n\n\tvoid updateFull();\n\tvoid updateFullForced();\n\tvoid fullUpdated();\n\t[[nodiscard]] bool wasFullUpdated() const {\n\t\treturn (_lastFullUpdate != 0);\n\t}\n\n\t[[nodiscard]] int nameVersion() const;\n\t[[nodiscard]] const QString &name() const;\n\t[[nodiscard]] const QString &shortName() const;\n\t[[nodiscard]] const QString &topBarNameText() const;\n\n\t[[nodiscard]] QString username() const;\n\t[[nodiscard]] QString editableUsername() const;\n\t[[nodiscard]] const std::vector<QString> &usernames() const;\n\t[[nodiscard]] bool isUsernameEditable(QString username) const;\n\n\t[[nodiscard]] const base::flat_set<QString> &nameWords() const {\n\t\treturn _nameWords;\n\t}\n\t[[nodiscard]] const base::flat_set<QChar> &nameFirstLetters() const {\n\t\treturn _nameFirstLetters;\n\t}\n\n\tvoid setUserpic(\n\t\tPhotoId photoId,\n\t\tconst ImageLocation &location,\n\t\tbool hasVideo);\n\tvoid setUserpicPhoto(const MTPPhoto &data);\n\n\tvoid paintUserpic(\n\t\tQPainter &p,\n\t\tUi::PeerUserpicView &view,\n\t\tPaintUserpicContext context) const;\n\tvoid paintUserpic(\n\t\t\tQPainter &p,\n\t\t\tUi::PeerUserpicView &view,\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tint size,\n\t\t\tbool forceCircle = false) const {\n\t\tpaintUserpic(p, view, {\n\t\t\t.position = { x, y },\n\t\t\t.size = size,\n\t\t\t.shape = (forceCircle\n\t\t\t\t? Ui::PeerUserpicShape::Circle\n\t\t\t\t: Ui::PeerUserpicShape::Auto),\n\t\t});\n\t}\n\tvoid paintUserpicLeft(\n\t\t\tQPainter &p,\n\t\t\tUi::PeerUserpicView &view,\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tint w,\n\t\t\tint size,\n\t\t\tbool forceCircle = false) const {\n\t\tpaintUserpic(\n\t\t\tp,\n\t\t\tview,\n\t\t\trtl() ? (w - x - size) : x,\n\t\t\ty,\n\t\t\tsize,\n\t\t\tforceCircle);\n\t}\n\tvoid loadUserpic();\n\t[[nodiscard]] bool hasUserpic() const;\n\t[[nodiscard]] Ui::PeerUserpicView activeUserpicView();\n\t[[nodiscard]] Ui::PeerUserpicView createUserpicView();\n\t[[nodiscard]] bool useEmptyUserpic(Ui::PeerUserpicView &view) const;\n\t[[nodiscard]] InMemoryKey userpicUniqueKey(Ui::PeerUserpicView &view) const;\n\t[[nodiscard]] static QImage GenerateUserpicImage(\n\t\tnot_null<PeerData*> peer,\n\t\tUi::PeerUserpicView &view,\n\t\tint size,\n\t\tstd::optional<int> radius = {});\n\t[[nodiscard]] ImageLocation userpicLocation() const;\n\n\tstatic constexpr auto kUnknownPhotoId = PhotoId(0xFFFFFFFFFFFFFFFFULL);\n\t[[nodiscard]] bool userpicPhotoUnknown() const;\n\t[[nodiscard]] PhotoId userpicPhotoId() const;\n\t[[nodiscard]] bool userpicHasVideo() const;\n\t[[nodiscard]] Data::FileOrigin userpicOrigin() const;\n\t[[nodiscard]] Data::FileOrigin userpicPhotoOrigin() const;\n\n\t// If this string is not empty we must not allow to open the\n\t// conversation and we must show this string instead.\n\t[[nodiscard]] QString computeUnavailableReason() const;\n\t[[nodiscard]] bool hasSensitiveContent() const;\n\tvoid setUnavailableReasons(\n\t\tstd::vector<Data::UnavailableReason> &&reason);\n\n\t[[nodiscard]] ClickHandlerPtr createOpenLink();\n\t[[nodiscard]] const ClickHandlerPtr &openLink() {\n\t\tif (!_openLink) {\n\t\t\t_openLink = createOpenLink();\n\t\t}\n\t\treturn _openLink;\n\t}\n\n\t[[nodiscard]] QImage *userpicCloudImage(Ui::PeerUserpicView &view) const;\n\n\t[[nodiscard]] bool canPinMessages() const;\n\t[[nodiscard]] bool canEditMessagesIndefinitely() const;\n\t[[nodiscard]] bool canCreatePolls() const;\n\t[[nodiscard]] bool canCreateTodoLists() const;\n\t[[nodiscard]] bool canCreateTopics() const;\n\t[[nodiscard]] bool canManageTopics() const;\n\t[[nodiscard]] bool canPostStories() const;\n\t[[nodiscard]] bool canEditStories() const;\n\t[[nodiscard]] bool canDeleteStories() const;\n\t[[nodiscard]] bool canManageGifts() const;\n\t[[nodiscard]] bool canTransferGifts() const;\n\t[[nodiscard]] bool canExportChatHistory() const;\n\t[[nodiscard]] bool autoTranslation() const;\n\n\t// Returns true if about text was changed.\n\tbool setAbout(const QString &newAbout);\n\t[[nodiscard]] const QString &about() const {\n\t\treturn _about;\n\t}\n\n\tvoid checkFolder(FolderId folderId);\n\n\tvoid setBarSettings(PeerBarSettings which);\n\t[[nodiscard]] auto barSettings() const {\n\t\treturn (_barSettings.current() & PeerBarSetting::Unknown)\n\t\t\t? std::nullopt\n\t\t\t: std::make_optional(_barSettings.current());\n\t}\n\t[[nodiscard]] auto barSettingsValue() const {\n\t\treturn (_barSettings.current() & PeerBarSetting::Unknown)\n\t\t\t? _barSettings.changes()\n\t\t\t: (_barSettings.value() | rpl::type_erased);\n\t}\n\t[[nodiscard]] int paysPerMessage() const;\n\tvoid clearPaysPerMessage();\n\t[[nodiscard]] bool hideLinks() const;\n\t[[nodiscard]] QString requestChatTitle() const;\n\t[[nodiscard]] TimeId requestChatDate() const;\n\t[[nodiscard]] UserData *businessBot() const;\n\t[[nodiscard]] QString businessBotManageUrl() const;\n\tvoid clearBusinessBot();\n\t[[nodiscard]] QString phoneCountryCode() const;\n\t[[nodiscard]] int registrationMonth() const;\n\t[[nodiscard]] int registrationYear() const;\n\t[[nodiscard]] TimeId nameChangeDate() const;\n\t[[nodiscard]] TimeId photoChangeDate() const;\n\n\tenum class TranslationFlag : uchar {\n\t\tUnknown,\n\t\tDisabled,\n\t\tEnabled,\n\t};\n\tvoid setTranslationDisabled(bool disabled);\n\t[[nodiscard]] TranslationFlag translationFlag() const;\n\tvoid saveTranslationDisabled(bool disabled);\n\n\tvoid setBarSettings(const MTPPeerSettings &data);\n\tbool changeBackgroundEmojiId(\n\t\tconst tl::conditional<MTPlong> &cloudBackgroundEmoji);\n\tbool changeColorCollectible(\n\t\tconst tl::conditional<MTPPeerColor> &cloudColor);\n\tbool changeColor(const tl::conditional<MTPPeerColor> &cloudColor);\n\tbool changeColorProfile(const tl::conditional<MTPPeerColor> &cloudColor);\n\n\tenum class BlockStatus : char {\n\t\tUnknown,\n\t\tBlocked,\n\t\tNotBlocked,\n\t};\n\t[[nodiscard]] BlockStatus blockStatus() const {\n\t\treturn _blockStatus;\n\t}\n\t[[nodiscard]] bool isBlocked() const {\n\t\treturn (blockStatus() == BlockStatus::Blocked);\n\t}\n\tvoid setIsBlocked(bool is);\n\n\tenum class LoadedStatus : char {\n\t\tNot,\n\t\tMinimal,\n\t\tNormal,\n\t\tFull,\n\t};\n\t[[nodiscard]] LoadedStatus loadedStatus() const {\n\t\treturn _loadedStatus;\n\t}\n\t[[nodiscard]] bool isMinimalLoaded() const {\n\t\treturn (loadedStatus() != LoadedStatus::Not);\n\t}\n\t[[nodiscard]] bool isLoaded() const {\n\t\treturn (loadedStatus() == LoadedStatus::Normal) || isFullLoaded();\n\t}\n\t[[nodiscard]] bool isFullLoaded() const {\n\t\treturn (loadedStatus() == LoadedStatus::Full);\n\t}\n\tvoid setLoadedStatus(LoadedStatus status);\n\n\t[[nodiscard]] TimeId messagesTTL() const;\n\tvoid setMessagesTTL(TimeId period);\n\n\t[[nodiscard]] Data::GroupCall *groupCall() const;\n\t[[nodiscard]] PeerId groupCallDefaultJoinAs() const;\n\n\tvoid setThemeToken(const QString &token);\n\t[[nodiscard]] const QString &themeToken() const;\n\n\tvoid setWallPaper(\n\t\tstd::optional<Data::WallPaper> paper,\n\t\tbool overriden = false);\n\t[[nodiscard]] bool wallPaperOverriden() const;\n\t[[nodiscard]] const Data::WallPaper *wallPaper() const;\n\n\tenum class StoriesState {\n\t\tUnknown,\n\t\tNone,\n\t\tHasRead,\n\t\tHasUnread,\n\t\tHasVideoStream,\n\t};\n\t[[nodiscard]] bool hasActiveStories() const;\n\t[[nodiscard]] bool hasUnreadStories() const;\n\t[[nodiscard]] bool hasActiveVideoStream() const;\n\tvoid setStoriesState(StoriesState state);\n\n\t[[nodiscard]] int peerGiftsCount() const;\n\n\t[[nodiscard]] MTPInputPeer input() const;\n\n\tconst PeerId id;\n\nprotected:\n\tvoid updateNameDelayed(\n\t\tconst QString &newName,\n\t\tconst QString &newNameOrPhone,\n\t\tconst QString &newUsername);\n\tvoid updateUserpic(PhotoId photoId, MTP::DcId dcId, bool hasVideo);\n\tvoid clearUserpic();\n\tvoid invalidateEmptyUserpic();\n\tvoid checkTrustedPayForMessage();\n\nprivate:\n\tvoid fillNames();\n\t[[nodiscard]] not_null<Ui::EmptyUserpic*> ensureEmptyUserpic() const;\n\t[[nodiscard]] virtual auto unavailableReasons() const\n\t\t-> const std::vector<Data::UnavailableReason> &;\n\n\tvoid setUserpicChecked(\n\t\tPhotoId photoId,\n\t\tconst ImageLocation &location,\n\t\tbool hasVideo);\n\n\tvirtual void setUnavailableReasonsList(\n\t\tstd::vector<Data::UnavailableReason> &&reasons);\n\tvoid setHasSensitiveContent(bool has);\n\n\tconst not_null<Data::Session*> _owner;\n\n\tmutable Data::CloudImage _userpic;\n\tPhotoId _userpicPhotoId = kUnknownPhotoId;\n\n\tmutable std::unique_ptr<Ui::EmptyUserpic> _userpicEmpty;\n\n\tData::PeerNotifySettings _notify;\n\n\tClickHandlerPtr _openLink;\n\tbase::flat_set<QString> _nameWords; // for filtering\n\tbase::flat_set<QChar> _nameFirstLetters;\n\n\tEmojiStatusId _emojiStatusId;\n\tDocumentId _backgroundEmojiId = 0;\n\tDocumentId _profileBackgroundEmojiId = 0;\n\tcrl::time _lastFullUpdate = 0;\n\n\tQString _name;\n\tuint32 _nameVersion : 16 = 1;\n\tuint32 _sensitiveContent : 1 = 0;\n\tuint32 _wallPaperOverriden : 1 = 0;\n\tuint32 _checkedTrustedPayForMessage : 1 = 0;\n\n\tTimeId _ttlPeriod = 0;\n\n\tBarSettings _barSettings = PeerBarSettings(PeerBarSetting::Unknown);\n\tstd::unique_ptr<PeerBarDetails> _barDetails;\n\tstd::shared_ptr<Ui::ColorCollectible> _colorCollectible;\n\tstd::shared_ptr<Ui::ColorCollectible> _colorProfileCollectible;\n\n\tBlockStatus _blockStatus = BlockStatus::Unknown;\n\tLoadedStatus _loadedStatus = LoadedStatus::Not;\n\tTranslationFlag _translationFlag = TranslationFlag::Unknown;\n\tuint8 _colorIndex : 6 = 0;\n\tuint8 _colorIndexCloud : 1 = 0;\n\tstd::optional<uint8> _colorProfileIndex;\n\tuint8 _userpicHasVideo : 1 = 0;\n\n\tQString _about;\n\tQString _themeToken;\n\tstd::unique_ptr<Data::WallPaper> _wallPaper;\n\n};\n\nnamespace Data {\n\nvoid SetTopPinnedMessageId(\n\tnot_null<PeerData*> peer,\n\tMsgId messageId);\n[[nodiscard]] FullMsgId ResolveTopPinnedId(\n\tnot_null<PeerData*> peer,\n\tMsgId topicRootId,\n\tPeerId monoforumPeerId,\n\tPeerData *migrated = nullptr);\n[[nodiscard]] FullMsgId ResolveMinPinnedId(\n\tnot_null<PeerData*> peer,\n\tMsgId topicRootId,\n\tPeerId monoforumPeerId,\n\tPeerData *migrated = nullptr);\n\n[[nodiscard]] uint64 BackgroundEmojiIdFromColor(const MTPPeerColor *color);\n[[nodiscard]] std::optional<uint8> ColorIndexFromColor(const MTPPeerColor *);\n\n[[nodiscard]] bool IsBotUserCreatesTopics(not_null<PeerData*>);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_peer_bot_command.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_peer_bot_command.h\"\n\nnamespace Data {\n\nBotCommand BotCommandFromTL(const MTPBotCommand &result) {\n\treturn result.match([](const MTPDbotCommand &data) {\n\t\treturn BotCommand {\n\t\t\t.command = qs(data.vcommand().v),\n\t\t\t.description = qs(data.vdescription().v),\n\t\t};\n\t});\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_peer_bot_command.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\nstruct BotCommand final {\n\tQString command;\n\tQString description;\n\n\tfriend inline bool operator==(\n\t\tconst BotCommand &,\n\t\tconst BotCommand &) = default;\n};\n\n[[nodiscard]] BotCommand BotCommandFromTL(const MTPBotCommand &result);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_peer_bot_commands.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_peer_bot_commands.h\"\n\nnamespace Data {\n\nChatBotCommands::Changed ChatBotCommands::update(\n\t\tconst std::vector<BotCommands> &list) {\n\tauto changed = false;\n\tif (list.empty()) {\n\t\tchanged = (list.empty() != empty());\n\t\tclear();\n\t} else {\n\t\tfor (const auto &commands : list) {\n\t\t\tif (commands.commands.empty()) {\n\t\t\t\tchanged |= remove(commands.userId);\n\t\t\t} else {\n\t\t\t\tauto &value = operator[](commands.userId);\n\t\t\t\tconst auto isEqual = ranges::equal(value, commands.commands);\n\t\t\t\tchanged |= !isEqual;\n\t\t\t\tif (!isEqual) {\n\t\t\t\t\tvalue = commands.commands;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn changed;\n}\n\nBotCommands BotCommandsFromTL(const MTPBotInfo &result) {\n\treturn result.match([](const MTPDbotInfo &data) {\n\t\tconst auto userId = data.vuser_id()\n\t\t\t? UserId(*data.vuser_id())\n\t\t\t: UserId();\n\t\tif (!data.vcommands()) {\n\t\t\treturn BotCommands{ .userId = userId };\n\t\t}\n\t\tauto commands = ranges::views::all(\n\t\t\tdata.vcommands()->v\n\t\t) | ranges::views::transform(BotCommandFromTL) | ranges::to_vector;\n\t\treturn BotCommands{\n\t\t\t.userId = userId,\n\t\t\t.commands = std::move(commands),\n\t\t};\n\t});\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_peer_bot_commands.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_peer_bot_command.h\"\n\nnamespace Data {\n\nenum class BotStatus : int {\n\tNoBots = -1,\n\tUnknown = 0,\n\tHasBots = 2,\n};\n\nstruct BotCommands final {\n\tUserId userId;\n\tstd::vector<BotCommand> commands;\n};\n\nstruct ChatBotCommands final : public base::flat_map<\n\tUserId,\n\tstd::vector<BotCommand>> {\npublic:\n\tusing Changed = bool;\n\n\tusing base::flat_map<UserId, std::vector<BotCommand>>::flat_map;\n\n\tChanged update(const std::vector<BotCommands> &list);\n};\n\n[[nodiscard]] BotCommands BotCommandsFromTL(const MTPBotInfo &result);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_peer_colors.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\nstruct ColorProfileSet {\n\tstd::vector<QColor> palette;\n\tstd::vector<QColor> bg;\n\tstd::vector<QColor> story;\n};\n\nstruct ColorProfileData {\n\tColorProfileSet light;\n\tColorProfileSet dark;\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_peer_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\nstruct StarsRating {\n\tint level = 0;\n\tint stars = 0;\n\tint thisLevelStars = 0;\n\tint nextLevelStars = 0;\n\n\texplicit operator bool() const {\n\t\treturn level != 0 || thisLevelStars != 0;\n\t}\n\n\tfriend inline bool operator==(StarsRating, StarsRating) = default;\n};\n\nstruct StarsRatingPending {\n\tStarsRating value;\n\tTimeId date = 0;\n\n\texplicit operator bool() const {\n\t\treturn value && date;\n\t}\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_peer_id.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_peer_id.h\"\n\nPeerId peerFromMTP(const MTPPeer &peer) {\n\treturn peer.match([](const MTPDpeerUser &data) {\n\t\treturn peerFromUser(data.vuser_id());\n\t}, [](const MTPDpeerChat &data) {\n\t\treturn peerFromChat(data.vchat_id());\n\t}, [](const MTPDpeerChannel &data) {\n\t\treturn peerFromChannel(data.vchannel_id());\n\t});\n}\n\nMTPpeer peerToMTP(PeerId id) {\n\tif (peerIsUser(id)) {\n\t\treturn MTP_peerUser(peerToBareMTPInt(id));\n\t} else if (peerIsChat(id)) {\n\t\treturn MTP_peerChat(peerToBareMTPInt(id));\n\t} else if (peerIsChannel(id)) {\n\t\treturn MTP_peerChannel(peerToBareMTPInt(id));\n\t}\n\treturn MTP_peerUser(MTP_long(0));\n}\n\nPeerId DeserializePeerId(quint64 serialized) {\n\tconst auto flag = (UserId::kReservedBit << 48);\n\tconst auto legacy = !(serialized & (UserId::kReservedBit << 48));\n\tif (!legacy) {\n\t\treturn PeerId(serialized & (~flag));\n\t}\n\tconstexpr auto PeerIdMask = uint64(0xFFFFFFFFULL);\n\tconstexpr auto PeerIdTypeMask = uint64(0xF00000000ULL);\n\tconstexpr auto PeerIdUserShift = uint64(0x000000000ULL);\n\tconstexpr auto PeerIdChatShift = uint64(0x100000000ULL);\n\tconstexpr auto PeerIdChannelShift = uint64(0x200000000ULL);\n\tconstexpr auto PeerIdFakeShift = uint64(0xF00000000ULL);\n\treturn ((serialized & PeerIdTypeMask) == PeerIdUserShift)\n\t\t? peerFromUser(UserId(serialized & PeerIdMask))\n\t\t: ((serialized & PeerIdTypeMask) == PeerIdChatShift)\n\t\t? peerFromChat(ChatId(serialized & PeerIdMask))\n\t\t: ((serialized & PeerIdTypeMask) == PeerIdChannelShift)\n\t\t? peerFromChannel(ChannelId(serialized & PeerIdMask))\n\t\t: ((serialized & PeerIdTypeMask) == PeerIdFakeShift)\n\t\t? PeerId(FakeChatId(serialized & PeerIdMask))\n\t\t: PeerId(0);\n}\n\nquint64 SerializePeerId(PeerId id) {\n\tExpects(!(id.value & (UserId::kReservedBit << 48)));\n\n\treturn id.value | (UserId::kReservedBit << 48);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_peer_id.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nusing BareId = uint64;\n\nstruct PeerIdZeroHelper {\n};\nusing PeerIdZero = void(PeerIdZeroHelper::*)();\n\ntemplate <uint8 Shift>\nstruct ChatIdType {\n\tBareId bare = 0;\n\n\tstatic constexpr BareId kShift = Shift;\n\tstatic constexpr BareId kReservedBit = BareId(0x80);\n\tstatic_assert((Shift & kReservedBit) == 0, \"Last bit is reserved.\");\n\n\tconstexpr ChatIdType() noexcept = default;\n\t//constexpr ChatIdType(PeerIdZero) noexcept { // UserId id = 0;\n\t//}\n\tconstexpr ChatIdType(BareId value) noexcept : bare(value) {\n\t}\n\tconstexpr ChatIdType(MTPlong value) noexcept : bare(value.v) {\n\t}\n\n\tfriend inline constexpr auto operator<=>(\n\t\tChatIdType,\n\t\tChatIdType) = default;\n\n\t[[nodiscard]] constexpr explicit operator bool() const noexcept {\n\t\treturn (bare != 0);\n\t}\n\t[[nodiscard]] constexpr bool operator!() const noexcept {\n\t\treturn !bare;\n\t}\n\n};\n\ntemplate <uchar Shift>\n[[nodiscard]] inline constexpr bool operator==(\n\t\tChatIdType<Shift> a,\n\t\tPeerIdZero) noexcept {\n\treturn (a.bare == 0);\n}\n\ntemplate <uchar Shift>\n[[nodiscard]] inline constexpr bool operator==(\n\t\tPeerIdZero,\n\t\tChatIdType<Shift> a) noexcept {\n\treturn (0 == a.bare);\n}\n\ntemplate <uchar Shift>\n[[nodiscard]] inline constexpr bool operator!=(\n\t\tChatIdType<Shift> a,\n\t\tPeerIdZero) noexcept {\n\treturn (a.bare != 0);\n}\n\ntemplate <uchar Shift>\n[[nodiscard]] inline constexpr bool operator!=(\n\t\tPeerIdZero,\n\t\tChatIdType<Shift> a) noexcept {\n\treturn (0 != a.bare);\n}\n\ntemplate <uchar Shift>\nbool operator<(ChatIdType<Shift>, PeerIdZero) = delete;\n\ntemplate <uchar Shift>\nbool operator<(PeerIdZero, ChatIdType<Shift>) = delete;\n\ntemplate <uchar Shift>\nbool operator>(ChatIdType<Shift>, PeerIdZero) = delete;\n\ntemplate <uchar Shift>\nbool operator>(PeerIdZero, ChatIdType<Shift>) = delete;\n\ntemplate <uchar Shift>\nbool operator<=(ChatIdType<Shift>, PeerIdZero) = delete;\n\ntemplate <uchar Shift>\nbool operator<=(PeerIdZero, ChatIdType<Shift>) = delete;\n\ntemplate <uchar Shift>\nbool operator>=(ChatIdType<Shift>, PeerIdZero) = delete;\n\ntemplate <uchar Shift>\nbool operator>=(PeerIdZero, ChatIdType<Shift>) = delete;\n\nusing UserId = ChatIdType<0>;\nusing ChatId = ChatIdType<1>;\nusing ChannelId = ChatIdType<2>;\nusing FakeChatId = ChatIdType<0x7F>;\n\nstruct PeerIdHelper {\n\tBareId value = 0;\n\tconstexpr PeerIdHelper(BareId value) noexcept : value(value) {\n\t}\n};\n\nstruct PeerId {\n\tBareId value = 0;\n\tstatic constexpr BareId kChatTypeMask = BareId(0xFFFFFFFFFFFFULL);\n\n\tconstexpr PeerId() noexcept = default;\n\tconstexpr PeerId(PeerIdZero) noexcept { // PeerId id = 0;\n\t}\n\ttemplate <uchar Shift>\n\tconstexpr PeerId(ChatIdType<Shift> id) noexcept\n\t: value(id.bare | (BareId(Shift) << 48)) {\n\t}\n\t// This instead of explicit PeerId(BareId) allows to use both\n\t// PeerId(uint64(..)) and PeerId(0).\n\tconstexpr PeerId(PeerIdHelper value) noexcept : value(value.value) {\n\t}\n\n\tfriend inline constexpr auto operator<=>(PeerId, PeerId) = default;\n\n\ttemplate <typename SomeChatIdType, BareId = SomeChatIdType::kShift>\n\t[[nodiscard]] constexpr bool is() const noexcept {\n\t\treturn ((value >> 48) & BareId(0xFF)) == SomeChatIdType::kShift;\n\t}\n\n\ttemplate <typename SomeChatIdType, BareId = SomeChatIdType::kShift>\n\t[[nodiscard]] constexpr SomeChatIdType to() const noexcept {\n\t\treturn is<SomeChatIdType>() ? (value & kChatTypeMask) : 0;\n\t}\n\n\t[[nodiscard]] constexpr explicit operator bool() const noexcept {\n\t\treturn (value != 0);\n\t}\n\t[[nodiscard]] constexpr bool operator!() const noexcept {\n\t\treturn !value;\n\t}\n\n};\n\n[[nodiscard]] inline constexpr bool operator==(\n\t\tPeerId a,\n\t\tPeerIdZero) noexcept {\n\treturn (a.value == 0);\n}\n\n[[nodiscard]] inline constexpr bool operator==(\n\t\tPeerIdZero,\n\t\tPeerId a) noexcept {\n\treturn (0 == a.value);\n}\n\n[[nodiscard]] inline constexpr bool operator!=(\n\t\tPeerId a,\n\t\tPeerIdZero) noexcept {\n\treturn (a.value != 0);\n}\n\n[[nodiscard]] inline constexpr bool operator!=(\n\t\tPeerIdZero,\n\t\tPeerId a) noexcept {\n\treturn (0 != a.value);\n}\n\nbool operator<(PeerId, PeerIdZero) = delete;\nbool operator<(PeerIdZero, PeerId) = delete;\nbool operator>(PeerId, PeerIdZero) = delete;\nbool operator>(PeerIdZero, PeerId) = delete;\nbool operator<=(PeerId, PeerIdZero) = delete;\nbool operator<=(PeerIdZero, PeerId) = delete;\nbool operator>=(PeerId, PeerIdZero) = delete;\nbool operator>=(PeerIdZero, PeerId) = delete;\n\n[[nodiscard]] inline constexpr bool peerIsUser(PeerId id) noexcept {\n\treturn id.is<UserId>();\n}\n\n[[nodiscard]] inline constexpr bool peerIsChat(PeerId id) noexcept {\n\treturn id.is<ChatId>();\n}\n\n[[nodiscard]] inline constexpr bool peerIsChannel(PeerId id) noexcept {\n\treturn id.is<ChannelId>();\n}\n\n[[nodiscard]] inline constexpr PeerId peerFromUser(UserId userId) noexcept {\n\treturn userId;\n}\n\n[[nodiscard]] inline constexpr PeerId peerFromChat(ChatId chatId) noexcept {\n\treturn chatId;\n}\n\n[[nodiscard]] inline constexpr PeerId peerFromChannel(\n\t\tChannelId channelId) noexcept {\n\treturn channelId;\n}\n\n[[nodiscard]] inline constexpr PeerId peerFromUser(MTPlong userId) noexcept {\n\treturn peerFromUser(userId.v);\n}\n\n[[nodiscard]] inline constexpr PeerId peerFromChat(MTPint chatId) noexcept {\n\treturn peerFromChat(chatId.v);\n}\n\n[[nodiscard]] inline constexpr PeerId peerFromChannel(\n\t\tMTPint channelId) noexcept {\n\treturn peerFromChannel(channelId.v);\n}\n\n[[nodiscard]] inline constexpr UserId peerToUser(PeerId id) noexcept {\n\treturn id.to<UserId>();\n}\n\n[[nodiscard]] inline constexpr ChatId peerToChat(PeerId id) noexcept {\n\treturn id.to<ChatId>();\n}\n\n[[nodiscard]] inline constexpr ChannelId peerToChannel(PeerId id) noexcept {\n\treturn id.to<ChannelId>();\n}\n\n[[nodiscard]] inline MTPlong peerToBareMTPInt(PeerId id) {\n\treturn MTP_long(id.value & PeerId::kChatTypeMask);\n}\n\n[[nodiscard]] PeerId peerFromMTP(const MTPPeer &peer);\n[[nodiscard]] MTPpeer peerToMTP(PeerId id);\n\n// Supports both modern and legacy serializations.\n[[nodiscard]] PeerId DeserializePeerId(quint64 serialized);\n[[nodiscard]] quint64 SerializePeerId(PeerId id);\n\nnamespace std {\n\ntemplate <uchar Shift>\nstruct hash<ChatIdType<Shift>> : private hash<BareId> {\n\tsize_t operator()(ChatIdType<Shift> value) const noexcept {\n\t\treturn hash<BareId>::operator()(value.bare);\n\t}\n};\n\ntemplate <>\nstruct hash<PeerId> : private hash<BareId> {\n\tsize_t operator()(PeerId value) const noexcept {\n\t\treturn hash<BareId>::operator()(value.value);\n\t}\n};\n\n} // namespace std\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_peer_values.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_peer_values.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_session.h\"\n#include \"data/data_message_reactions.h\"\n#include \"main/main_session.h\"\n#include \"main/main_app_config.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"base/unixtime.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kMinOnlineChangeTimeout = crl::time(1000);\nconstexpr auto kMaxOnlineChangeTimeout = 86400 * crl::time(1000);\nconstexpr auto kSecondsInDay = 86400;\n\nint OnlinePhraseChangeInSeconds(LastseenStatus status, TimeId now) {\n\tconst auto till = status.onlineTill();\n\tif (till > now) {\n\t\treturn till - now;\n\t} else if (status.isHidden()) {\n\t\treturn std::numeric_limits<int>::max();\n\t}\n\tconst auto minutes = (now - till) / 60;\n\tif (minutes < 60) {\n\t\treturn (minutes + 1) * 60 - (now - till);\n\t}\n\tconst auto hours = (now - till) / 3600;\n\tif (hours < 12) {\n\t\treturn (hours + 1) * 3600 - (now - till);\n\t}\n\tconst auto nowFull = base::unixtime::parse(now);\n\tconst auto tomorrow = nowFull.date().addDays(1).startOfDay();\n\treturn std::max(static_cast<TimeId>(nowFull.secsTo(tomorrow)), 0);\n}\n\nstd::optional<QString> OnlineTextSpecial(not_null<UserData*> user) {\n\tif (user->isNotificationsUser()) {\n\t\treturn tr::lng_status_service_notifications(tr::now);\n\t} else if (user->isSupport()) {\n\t\treturn tr::lng_status_support(tr::now);\n\t} else if (user->isBot()) {\n\t\tif (const auto count = user->botInfo->activeUsers) {\n\t\t\treturn tr::lng_bot_status_users(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\tcount);\n\t\t}\n\t\treturn tr::lng_status_bot(tr::now);\n\t} else if (user->isServiceUser()) {\n\t\treturn tr::lng_status_support(tr::now);\n\t}\n\treturn std::nullopt;\n}\n\nstd::optional<QString> OnlineTextCommon(LastseenStatus status, TimeId now) {\n\tif (status.isOnline(now)) {\n\t\treturn tr::lng_status_online(tr::now);\n\t} else if (status.isLongAgo()) {\n\t\treturn tr::lng_status_offline(tr::now);\n\t} else if (status.isRecently()) {\n\t\treturn tr::lng_status_recently(tr::now);\n\t} else if (status.isWithinWeek()) {\n\t\treturn tr::lng_status_last_week(tr::now);\n\t} else if (status.isWithinMonth()) {\n\t\treturn tr::lng_status_last_month(tr::now);\n\t} else if (status.isHidden()) {\n\t\treturn tr::lng_status_recently(tr::now);\n\t}\n\treturn std::nullopt;\n}\n\n[[nodiscard]] int UniqueReactionsLimit(not_null<Main::AppConfig*> config) {\n\treturn config->get<int>(\"reactions_uniq_max\", 11);\n}\n\n} // namespace\n\ninline auto AdminRightsValue(not_null<ChannelData*> channel) {\n\treturn channel->adminRightsValue();\n}\n\ninline auto AdminRightsValue(\n\t\tnot_null<ChannelData*> channel,\n\t\tChatAdminRights mask) {\n\treturn FlagsValueWithMask(AdminRightsValue(channel), mask);\n}\n\ninline auto AdminRightValue(\n\t\tnot_null<ChannelData*> channel,\n\t\tChatAdminRight flag) {\n\treturn SingleFlagValue(AdminRightsValue(channel), flag);\n}\n\ninline auto AdminRightsValue(not_null<ChatData*> chat) {\n\treturn chat->adminRightsValue();\n}\n\ninline auto AdminRightsValue(\n\t\tnot_null<ChatData*> chat,\n\t\tChatAdminRights mask) {\n\treturn FlagsValueWithMask(AdminRightsValue(chat), mask);\n}\n\ninline auto AdminRightValue(\n\t\tnot_null<ChatData*> chat,\n\t\tChatAdminRight flag) {\n\treturn SingleFlagValue(AdminRightsValue(chat), flag);\n}\n\ninline auto RestrictionsValue(not_null<ChannelData*> channel) {\n\treturn channel->restrictionsValue();\n}\n\ninline auto RestrictionsValue(\n\t\tnot_null<ChannelData*> channel,\n\t\tChatRestrictions mask) {\n\treturn FlagsValueWithMask(RestrictionsValue(channel), mask);\n}\n\ninline auto RestrictionValue(\n\t\tnot_null<ChannelData*> channel,\n\t\tChatRestriction flag) {\n\treturn SingleFlagValue(RestrictionsValue(channel), flag);\n}\n\ninline auto DefaultRestrictionsValue(not_null<ChannelData*> channel) {\n\treturn channel->defaultRestrictionsValue();\n}\n\ninline auto DefaultRestrictionsValue(\n\t\tnot_null<ChannelData*> channel,\n\t\tChatRestrictions mask) {\n\treturn FlagsValueWithMask(DefaultRestrictionsValue(channel), mask);\n}\n\ninline auto DefaultRestrictionValue(\n\t\tnot_null<ChannelData*> channel,\n\t\tChatRestriction flag) {\n\treturn SingleFlagValue(DefaultRestrictionsValue(channel), flag);\n}\n\ninline auto DefaultRestrictionsValue(not_null<ChatData*> chat) {\n\treturn chat->defaultRestrictionsValue();\n}\n\ninline auto DefaultRestrictionsValue(\n\t\tnot_null<ChatData*> chat,\n\t\tChatRestrictions mask) {\n\treturn FlagsValueWithMask(DefaultRestrictionsValue(chat), mask);\n}\n\ninline auto DefaultRestrictionValue(\n\t\tnot_null<ChatData*> chat,\n\t\tChatRestriction flag) {\n\treturn SingleFlagValue(DefaultRestrictionsValue(chat), flag);\n}\n\n// Duplicated in CanSendAnyOf().\n[[nodiscard]] rpl::producer<bool> CanSendAnyOfValue(\n\t\tnot_null<Thread*> thread,\n\t\tChatRestrictions rights,\n\t\tbool forbidInForums) {\n\tif (const auto topic = thread->asTopic()) {\n\t\tusing Flag = ChannelDataFlag;\n\t\tconst auto mask = Flag()\n\t\t\t| Flag::Left\n\t\t\t| Flag::JoinToWrite\n\t\t\t| Flag::HasLink\n\t\t\t| Flag::Forbidden\n\t\t\t| Flag::Creator;\n\t\tif (const auto channel = topic->channel()) {\n\t\t\treturn rpl::combine(\n\t\t\t\tPeerFlagsValue(channel, mask),\n\t\t\t\tRestrictionsValue(channel, rights),\n\t\t\t\tDefaultRestrictionsValue(channel, rights),\n\t\t\t\tAdminRightsValue(channel, ChatAdminRight::ManageTopics),\n\t\t\t\ttopic->session().changes().topicFlagsValue(\n\t\t\t\t\ttopic,\n\t\t\t\t\tTopicUpdate::Flag::Closed),\n\t\t\t\t[=](\n\t\t\t\t\t\tChannelDataFlags flags,\n\t\t\t\t\t\tChatRestrictions sendRestriction,\n\t\t\t\t\t\tChatRestrictions defaultSendRestriction,\n\t\t\t\t\t\tauto,\n\t\t\t\t\t\tauto) {\n\t\t\t\t\tconst auto notAmInFlags = Flag::Left | Flag::Forbidden;\n\t\t\t\t\tconst auto allowed = !(flags & notAmInFlags)\n\t\t\t\t\t\t|| ((flags & Flag::HasLink)\n\t\t\t\t\t\t\t&& !(flags & Flag::JoinToWrite));\n\t\t\t\t\treturn allowed\n\t\t\t\t\t\t&& ((flags & Flag::Creator)\n\t\t\t\t\t\t\t|| (!sendRestriction && !defaultSendRestriction))\n\t\t\t\t\t\t&& (!topic->closed() || topic->canToggleClosed());\n\t\t\t\t});\n\t\t}\n\t}\n\treturn CanSendAnyOfValue(thread->peer(), rights, forbidInForums);\n}\n\n// Duplicated in CanSendAnyOf().\n[[nodiscard]] rpl::producer<bool> CanSendAnyOfValue(\n\t\tnot_null<PeerData*> peer,\n\t\tChatRestrictions rights,\n\t\tbool forbidInForums) {\n\tif (const auto user = peer->asUser()) {\n\t\tif (user->isRepliesChat() || user->isVerifyCodes()) {\n\t\t\treturn rpl::single(false);\n\t\t}\n\t\tusing namespace rpl::mappers;\n\t\tconst auto other = rights & ~(ChatRestriction::SendVoiceMessages\n\t\t\t| ChatRestriction::SendVideoMessages);\n\t\tauto allowedAny = PeerFlagsValue(\n\t\t\tuser,\n\t\t\t(UserDataFlag::Deleted | UserDataFlag::RequiresPremiumToWrite)\n\t\t) | rpl::map([=](UserDataFlags flags) {\n\t\t\treturn (flags & UserDataFlag::Deleted)\n\t\t\t\t? rpl::single(false)\n\t\t\t\t: !(flags & UserDataFlag::RequiresPremiumToWrite)\n\t\t\t\t? rpl::single(true)\n\t\t\t\t: AmPremiumValue(&user->session());\n\t\t}) | rpl::flatten_latest();\n\t\tif (other) {\n\t\t\treturn allowedAny;\n\t\t}\n\t\tconst auto mask = UserDataFlag::VoiceMessagesForbidden;\n\t\treturn rpl::combine(\n\t\t\tstd::move(allowedAny),\n\t\t\tPeerFlagValue(user, mask),\n\t\t\t_1 && !_2);\n\t} else if (const auto chat = peer->asChat()) {\n\t\tconst auto mask = ChatDataFlag()\n\t\t\t| ChatDataFlag::Deactivated\n\t\t\t| ChatDataFlag::Forbidden\n\t\t\t| ChatDataFlag::Left\n\t\t\t| ChatDataFlag::Creator;\n\t\treturn rpl::combine(\n\t\t\tPeerFlagsValue(chat, mask),\n\t\t\tAdminRightsValue(chat),\n\t\t\tDefaultRestrictionsValue(chat, rights),\n\t\t\t[rights](\n\t\t\t\tChatDataFlags flags,\n\t\t\t\tData::Flags<ChatAdminRights>::Change adminRights,\n\t\t\t\tChatRestrictions defaultSendRestrictions) {\n\t\t\tconst auto amOutFlags = ChatDataFlag()\n\t\t\t\t| ChatDataFlag::Deactivated\n\t\t\t\t| ChatDataFlag::Forbidden\n\t\t\t\t| ChatDataFlag::Left;\n\t\treturn !(flags & amOutFlags)\n\t\t\t&& ((flags & ChatDataFlag::Creator)\n\t\t\t\t|| (adminRights.value != ChatAdminRights(0))\n\t\t\t\t|| (rights & ~defaultSendRestrictions));\n\t\t});\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tusing Flag = ChannelDataFlag;\n\t\tconst auto mask = Flag()\n\t\t\t| Flag::Left\n\t\t\t| Flag::Forum\n\t\t\t| Flag::JoinToWrite\n\t\t\t| Flag::Monoforum\n\t\t\t| Flag::HasLink\n\t\t\t| Flag::Forbidden\n\t\t\t| Flag::Creator\n\t\t\t| Flag::Broadcast\n\t\t\t| Flag::MonoforumDisabled;\n\t\treturn rpl::combine(\n\t\t\tPeerFlagsValue(channel, mask),\n\t\t\tAdminRightValue(\n\t\t\t\tchannel,\n\t\t\t\tChatAdminRight::PostMessages),\n\t\t\tchannel->unrestrictedByBoostsValue(),\n\t\t\tRestrictionsValue(channel, rights),\n\t\t\tDefaultRestrictionsValue(channel, rights),\n\t\t\t[=](\n\t\t\t\t\tChannelDataFlags flags,\n\t\t\t\t\tbool postMessagesRight,\n\t\t\t\t\tbool unrestrictedByBoosts,\n\t\t\t\t\tChatRestrictions sendRestriction,\n\t\t\t\t\tChatRestrictions defaultSendRestriction) {\n\t\t\t\tif (flags & Flag::MonoforumDisabled) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst auto notAmInFlags = Flag::Left | Flag::Forbidden;\n\t\t\t\tconst auto forumRestriction = forbidInForums\n\t\t\t\t\t&& (flags & Flag::Forum);\n\t\t\t\tconst auto allowed = !(flags & notAmInFlags)\n\t\t\t\t\t|| ((flags & Flag::HasLink)\n\t\t\t\t\t\t&& !(flags & Flag::JoinToWrite))\n\t\t\t\t\t|| (flags & Flag::Monoforum);\n\t\t\t\tconst auto restricted = sendRestriction\n\t\t\t\t\t| (defaultSendRestriction && !unrestrictedByBoosts);\n\t\t\t\treturn allowed\n\t\t\t\t\t&& !forumRestriction\n\t\t\t\t\t&& (postMessagesRight\n\t\t\t\t\t\t|| (flags & Flag::Creator)\n\t\t\t\t\t\t|| (!(flags & Flag::Broadcast)\n\t\t\t\t\t\t\t&& (rights & ~restricted)));\n\t\t\t});\n\t}\n\tUnexpected(\"Peer type in Data::CanSendAnyOfValue.\");\n}\n\n// This is duplicated in PeerData::canPinMessages().\nrpl::producer<bool> CanPinMessagesValue(not_null<PeerData*> peer) {\n\tusing namespace rpl::mappers;\n\tif (const auto user = peer->asUser()) {\n\t\treturn PeerFlagsValue(\n\t\t\tuser,\n\t\t\tUserDataFlag::CanPinMessages\n\t\t) | rpl::map(_1 != UserDataFlag(0));\n\t} else if (const auto chat = peer->asChat()) {\n\t\tconst auto mask = 0\n\t\t\t| ChatDataFlag::Deactivated\n\t\t\t| ChatDataFlag::Forbidden\n\t\t\t| ChatDataFlag::Left\n\t\t\t| ChatDataFlag::Creator;\n\t\treturn rpl::combine(\n\t\t\tPeerFlagsValue(chat, mask),\n\t\t\tAdminRightValue(chat, ChatAdminRight::PinMessages),\n\t\t\tDefaultRestrictionValue(chat, ChatRestriction::PinMessages),\n\t\t[](\n\t\t\t\tChatDataFlags flags,\n\t\t\t\tbool adminRightAllows,\n\t\t\t\tbool defaultRestriction) {\n\t\t\tconst auto amOutFlags = 0\n\t\t\t\t| ChatDataFlag::Deactivated\n\t\t\t\t| ChatDataFlag::Forbidden\n\t\t\t\t| ChatDataFlag::Left;\n\t\t\treturn !(flags & amOutFlags)\n\t\t\t\t&& ((flags & ChatDataFlag::Creator)\n\t\t\t\t\t|| adminRightAllows\n\t\t\t\t\t|| !defaultRestriction);\n\t\t});\n\t} else if (const auto megagroup = peer->asMegagroup()) {\n\t\tif (megagroup->amCreator()) {\n\t\t\treturn rpl::single(true);\n\t\t}\n\t\treturn rpl::combine(\n\t\t\tAdminRightValue(megagroup, ChatAdminRight::PinMessages),\n\t\t\tDefaultRestrictionValue(megagroup, ChatRestriction::PinMessages),\n\t\t\tPeerFlagsValue(\n\t\t\t\tmegagroup,\n\t\t\t\tChannelDataFlag::Username | ChannelDataFlag::Location),\n\t\t\tmegagroup->restrictionsValue()\n\t\t) | rpl::map([=](\n\t\t\t\tbool adminRightAllows,\n\t\t\t\tbool defaultRestriction,\n\t\t\t\tChannelDataFlags usernameOrLocation,\n\t\t\t\tData::Flags<ChatRestrictions>::Change restrictions) {\n\t\t\treturn adminRightAllows\n\t\t\t\t|| (!usernameOrLocation\n\t\t\t\t\t&& !defaultRestriction\n\t\t\t\t\t&& !(restrictions.value & ChatRestriction::PinMessages));\n\t\t});\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tif (channel->amCreator()) {\n\t\t\treturn rpl::single(true);\n\t\t}\n\t\treturn AdminRightValue(channel, ChatAdminRight::EditMessages);\n\t}\n\tUnexpected(\"Peer type in CanPinMessagesValue.\");\n}\n\nrpl::producer<bool> CanManageGroupCallValue(not_null<PeerData*> peer) {\n\tconst auto flag = ChatAdminRight::ManageCall;\n\tif (const auto user = peer->asUser()) {\n\t\treturn rpl::single(user->isSelf());\n\t} else if (const auto chat = peer->asChat()) {\n\t\treturn chat->amCreator()\n\t\t\t? (rpl::single(true) | rpl::type_erased)\n\t\t\t: AdminRightValue(chat, flag);\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn channel->amCreator()\n\t\t\t? (rpl::single(true) | rpl::type_erased)\n\t\t\t: AdminRightValue(channel, flag);\n\t}\n\treturn rpl::single(false);\n}\n\nrpl::producer<bool> PeerPremiumValue(not_null<PeerData*> peer) {\n\tconst auto user = peer->asUser();\n\tif (!user) {\n\t\treturn rpl::single(false);\n\t}\n\treturn user->flagsValue(\n\t) | rpl::filter([=](UserData::Flags::Change change) {\n\t\treturn (change.diff & UserDataFlag::Premium);\n\t}) | rpl::map([=] {\n\t\treturn user->isPremium();\n\t});\n}\n\nrpl::producer<bool> AmPremiumValue(not_null<Main::Session*> session) {\n\treturn PeerPremiumValue(session->user());\n}\n\nTimeId SortByOnlineValue(not_null<UserData*> user, TimeId now) {\n\tif (user->isServiceUser() || user->isBot()) {\n\t\treturn -1;\n\t}\n\tconst auto lastseen = user->lastseen();\n\tif (const auto till = lastseen.onlineTill()) {\n\t\treturn till;\n\t} else if (lastseen.isRecently()) {\n\t\treturn now - 3 * kSecondsInDay;\n\t} else if (lastseen.isWithinWeek()) {\n\t\treturn now - 7 * kSecondsInDay;\n\t} else if (lastseen.isWithinMonth()) {\n\t\treturn now - 30 * kSecondsInDay;\n\t} else {\n\t\treturn 0;\n\t}\n}\n\ncrl::time OnlineChangeTimeout(Data::LastseenStatus status, TimeId now) {\n\tconst auto result = OnlinePhraseChangeInSeconds(status, now);\n\tAssert(result >= 0);\n\treturn std::clamp(\n\t\tresult * crl::time(1000),\n\t\tkMinOnlineChangeTimeout,\n\t\tkMaxOnlineChangeTimeout);\n}\n\ncrl::time OnlineChangeTimeout(not_null<UserData*> user, TimeId now) {\n\tif (user->isServiceUser() || user->isBot()) {\n\t\treturn kMaxOnlineChangeTimeout;\n\t}\n\treturn OnlineChangeTimeout(user->lastseen(), now);\n}\n\nQString OnlineText(Data::LastseenStatus status, TimeId now) {\n\tif (const auto common = OnlineTextCommon(status, now)) {\n\t\treturn *common;\n\t}\n\tconst auto till = status.onlineTill();\n\tAssert(till > 0);\n\tconst auto minutes = (now - till) / 60;\n\tif (!minutes) {\n\t\treturn tr::lng_status_lastseen_now(tr::now);\n\t} else if (minutes < 60) {\n\t\treturn tr::lng_status_lastseen_minutes(tr::now, lt_count, minutes);\n\t}\n\tconst auto hours = (now - till) / 3600;\n\tif (hours < 12) {\n\t\treturn tr::lng_status_lastseen_hours(tr::now, lt_count, hours);\n\t}\n\tconst auto onlineFull = base::unixtime::parse(till);\n\tconst auto nowFull = base::unixtime::parse(now);\n\tconst auto locale = QLocale();\n\tif (onlineFull.date() == nowFull.date()) {\n\t\tconst auto onlineTime = locale.toString(onlineFull.time(), QLocale::ShortFormat);\n\t\treturn tr::lng_status_lastseen_today(tr::now, lt_time, onlineTime);\n\t} else if (onlineFull.date().addDays(1) == nowFull.date()) {\n\t\tconst auto onlineTime = locale.toString(onlineFull.time(), QLocale::ShortFormat);\n\t\treturn tr::lng_status_lastseen_yesterday(tr::now, lt_time, onlineTime);\n\t}\n\tconst auto date = locale.toString(onlineFull.date(), QLocale::ShortFormat);\n\treturn tr::lng_status_lastseen_date(tr::now, lt_date, date);\n}\n\nQString OnlineText(not_null<UserData*> user, TimeId now) {\n\tif (const auto special = OnlineTextSpecial(user)) {\n\t\treturn *special;\n\t}\n\treturn OnlineText(user->lastseen(), now);\n}\n\nQString OnlineTextFull(not_null<UserData*> user, TimeId now) {\n\tif (const auto special = OnlineTextSpecial(user)) {\n\t\treturn *special;\n\t} else if (const auto common = OnlineTextCommon(user->lastseen(), now)) {\n\t\treturn *common;\n\t}\n\tconst auto till = user->lastseen().onlineTill();\n\tconst auto onlineFull = base::unixtime::parse(till);\n\tconst auto nowFull = base::unixtime::parse(now);\n\n\t//check minutes\n\tint32 minutes = (now - user->lastseen().onlineTill()) / 60;\n\tif (!minutes) {\n\t\treturn tr::lng_status_lastseen_now(tr::now);\n\t} else if (minutes < 60) {\n\t\treturn tr::lng_status_lastseen_minutes(tr::now, lt_count, minutes);\n\t}\n\n\tconst auto locale = QLocale();\n\tif (onlineFull.date() == nowFull.date()) {\n\t\tconst auto onlineTime = locale.toString(onlineFull.time(), QLocale::ShortFormat);\n\t\treturn tr::lng_status_lastseen_today(tr::now, lt_time, onlineTime);\n\t} else if (onlineFull.date().addDays(1) == nowFull.date()) {\n\t\tconst auto onlineTime = locale.toString(onlineFull.time(), QLocale::ShortFormat);\n\t\treturn tr::lng_status_lastseen_yesterday(tr::now, lt_time, onlineTime);\n\t}\n\tconst auto date = locale.toString(onlineFull.date(), QLocale::ShortFormat);\n\tconst auto time = locale.toString(onlineFull.time(), QLocale::ShortFormat);\n\treturn tr::lng_status_lastseen_date_time(tr::now, lt_date, date, lt_time, time);\n}\n\nbool OnlineTextActive(not_null<UserData*> user, TimeId now) {\n\treturn !user->isServiceUser()\n\t\t&& !user->isBot()\n\t\t&& user->lastseen().isOnline(now);\n}\n\nbool IsUserOnline(not_null<UserData*> user, TimeId now) {\n\tif (!now) {\n\t\tnow = base::unixtime::now();\n\t}\n\treturn OnlineTextActive(user, now);\n}\n\nbool ChannelHasActiveCall(not_null<ChannelData*> channel) {\n\treturn (channel->flags() & ChannelDataFlag::CallNotEmpty);\n}\n\nbool ChannelHasSubscriptionUntilDate(ChannelData *channel) {\n\treturn channel && channel->subscriptionUntilDate() > 0;\n}\n\nrpl::producer<Data::StarsRating> StarsRatingValue(\n\t\tnot_null<PeerData*> peer) {\n\tif (const auto user = peer->asUser()) {\n\t\treturn user->session().changes().peerFlagsValue(\n\t\t\tuser,\n\t\t\tData::PeerUpdate::Flag::StarsRating\n\t\t) | rpl::map([=] {\n\t\t\tauto result = user->starsRating();\n\t\t\tif (!user->isSelf() && result.level < 0) {\n\t\t\t\tresult.stars = 0;\n\t\t\t}\n\t\t\treturn result;\n\t\t});\n\t}\n\treturn rpl::single(Data::StarsRating());\n}\n\nrpl::producer<QImage> PeerUserpicImageValue(\n\t\tnot_null<PeerData*> peer,\n\t\tint size,\n\t\tstd::optional<int> radius) {\n\treturn [=](auto consumer) {\n\t\tauto result = rpl::lifetime();\n\t\tstruct State {\n\t\t\tUi::PeerUserpicView view;\n\t\t\trpl::lifetime waiting;\n\t\t\tInMemoryKey key = {};\n\t\t\tbool empty = true;\n\t\t\tFn<void()> push;\n\t\t};\n\t\tconst auto state = result.make_state<State>();\n\t\tstate->push = [=] {\n\t\t\tconst auto key = peer->userpicUniqueKey(state->view);\n\t\t\tconst auto loading = Ui::PeerUserpicLoading(state->view);\n\n\t\t\tif (loading && !state->waiting) {\n\t\t\t\tpeer->session().downloaderTaskFinished(\n\t\t\t\t) | rpl::on_next(state->push, state->waiting);\n\t\t\t} else if (!loading && state->waiting) {\n\t\t\t\tstate->waiting.destroy();\n\t\t\t}\n\n\t\t\tif (!state->empty && (loading || key == state->key)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->key = key;\n\t\t\tstate->empty = false;\n\t\t\tconsumer.put_next(\n\t\t\t\tPeerData::GenerateUserpicImage(\n\t\t\t\t\tpeer,\n\t\t\t\t\tstate->view,\n\t\t\t\t\tsize,\n\t\t\t\t\tradius));\n\t\t};\n\t\tpeer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tPeerUpdate::Flag::Photo\n\t\t) | rpl::on_next(state->push, result);\n\t\treturn result;\n\t};\n}\n\nconst AllowedReactions &PeerAllowedReactions(not_null<PeerData*> peer) {\n\tif (const auto chat = peer->asChat()) {\n\t\treturn chat->allowedReactions();\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn channel->allowedReactions();\n\t} else {\n\t\tstatic const auto result = AllowedReactions{\n\t\t\t.type = AllowedReactionsType::All,\n\t\t};\n\t\treturn result;\n\t}\n}\n\n rpl::producer<AllowedReactions> PeerAllowedReactionsValue(\n\t\tnot_null<PeerData*> peer) {\n\treturn peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::Reactions\n\t) | rpl::map([=]{\n\t\treturn PeerAllowedReactions(peer);\n\t});\n}\n\nint UniqueReactionsLimit(not_null<PeerData*> peer) {\n\tif (const auto channel = peer->asChannel()) {\n\t\tif (const auto limit = channel->allowedReactions().maxCount) {\n\t\t\treturn limit;\n\t\t}\n\t} else if (const auto chat = peer->asChat()) {\n\t\tif (const auto limit = chat->allowedReactions().maxCount) {\n\t\t\treturn limit;\n\t\t}\n\t}\n\treturn UniqueReactionsLimit(&peer->session().appConfig());\n}\n\nrpl::producer<int> UniqueReactionsLimitValue(\n\t\tnot_null<PeerData*> peer) {\n\tauto configValue = peer->session().appConfig().value(\n\t) | rpl::map([config = &peer->session().appConfig()] {\n\t\treturn UniqueReactionsLimit(config);\n\t}) | rpl::distinct_until_changed();\n\tif (peer->isChannel()) {\n\t\treturn rpl::combine(\n\t\t\tPeerAllowedReactionsValue(peer),\n\t\t\tstd::move(configValue)\n\t\t) | rpl::map([=](const auto &allowedReactions, int limit) {\n\t\t\treturn allowedReactions.maxCount\n\t\t\t\t? allowedReactions.maxCount\n\t\t\t\t: limit;\n\t\t});\n\t} else if (peer->isChat()) {\n\t\treturn rpl::combine(\n\t\t\tPeerAllowedReactionsValue(peer),\n\t\t\tstd::move(configValue)\n\t\t) | rpl::map([=](const auto &allowedReactions, int limit) {\n\t\t\treturn allowedReactions.maxCount\n\t\t\t\t? allowedReactions.maxCount\n\t\t\t\t: limit;\n\t\t});\n\t}\n\treturn configValue;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_peer_values.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_peer.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_chat_participant_status.h\"\n\nenum class ImageRoundRadius;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nstruct Reaction;\nclass ForumTopic;\nclass LastseenStatus;\n\ntemplate <typename ChangeType, typename Error, typename Generator>\ninline auto FlagsValueWithMask(\n\t\trpl::producer<ChangeType, Error, Generator> &&value,\n\t\ttypename ChangeType::Type mask) {\n\treturn std::move(\n\t\tvalue\n\t) | rpl::filter([mask](const ChangeType &change) {\n\t\treturn change.diff & mask;\n\t}) | rpl::map([mask](const ChangeType &change) {\n\t\treturn change.value & mask;\n\t});\n}\n\ntemplate <typename ChangeType, typename Error, typename Generator>\ninline auto SingleFlagValue(\n\t\trpl::producer<ChangeType, Error, Generator> &&value,\n\t\ttypename ChangeType::Enum flag) {\n\treturn FlagsValueWithMask(\n\t\tstd::move(value),\n\t\tflag\n\t) | rpl::map([](typename ChangeType::Type value) {\n\t\treturn !!value;\n\t});\n}\n\ntemplate <\n\ttypename PeerType,\n\ttypename ChangeType = typename PeerType::Flags::Change>\ninline auto PeerFlagsValue(PeerType *peer) {\n\tExpects(peer != nullptr);\n\n\treturn peer->flagsValue();\n}\n\ntemplate <\n\ttypename PeerType,\n\ttypename ChangeType = typename PeerType::Flags::Change>\ninline auto PeerFlagsValue(\n\t\tPeerType *peer,\n\t\ttypename PeerType::Flags::Type mask) {\n\treturn FlagsValueWithMask(PeerFlagsValue(peer), mask);\n}\n\ntemplate <\n\ttypename PeerType,\n\ttypename ChangeType = typename PeerType::Flags::Change>\ninline auto PeerFlagValue(\n\t\tPeerType *peer,\n\t\ttypename PeerType::Flags::Enum flag) {\n\treturn SingleFlagValue(PeerFlagsValue(peer), flag);\n}\n\ntemplate <\n\ttypename PeerType,\n\ttypename = typename PeerType::FullFlags::Change>\ninline auto PeerFullFlagsValue(PeerType *peer) {\n\tExpects(peer != nullptr);\n\n\treturn peer->fullFlagsValue();\n}\n\ntemplate <\n\ttypename PeerType,\n\ttypename = typename PeerType::FullFlags::Change>\ninline auto PeerFullFlagsValue(\n\t\tPeerType *peer,\n\t\ttypename PeerType::FullFlags::Type mask) {\n\treturn FlagsValueWithMask(PeerFullFlagsValue(peer), mask);\n}\n\ntemplate <\n\ttypename PeerType,\n\ttypename = typename PeerType::FullFlags::Change>\ninline auto PeerFullFlagValue(\n\t\tPeerType *peer,\n\t\ttypename PeerType::FullFlags::Enum flag) {\n\treturn SingleFlagValue(PeerFullFlagsValue(peer), flag);\n}\n\n[[nodiscard]] rpl::producer<bool> CanSendAnyOfValue(\n\tnot_null<Data::Thread*> thread,\n\tChatRestrictions rights,\n\tbool forbidInForums = true);\n[[nodiscard]] rpl::producer<bool> CanSendAnyOfValue(\n\tnot_null<PeerData*> peer,\n\tChatRestrictions rights,\n\tbool forbidInForums = true);\n\n[[nodiscard]] inline rpl::producer<bool> CanSendValue(\n\t\tnot_null<Thread*> thread,\n\t\tChatRestriction right,\n\t\tbool forbidInForums = true) {\n\treturn CanSendAnyOfValue(thread, right, forbidInForums);\n}\n[[nodiscard]] inline rpl::producer<bool> CanSendValue(\n\t\tnot_null<PeerData*> peer,\n\t\tChatRestriction right,\n\t\tbool forbidInForums = true) {\n\treturn CanSendAnyOfValue(peer, right, forbidInForums);\n}\n[[nodiscard]] inline rpl::producer<bool> CanSendTextsValue(\n\t\tnot_null<Thread*> thread,\n\t\tbool forbidInForums = true) {\n\treturn CanSendValue(thread, ChatRestriction::SendOther, forbidInForums);\n}\n[[nodiscard]] inline rpl::producer<bool> CanSendTextsValue(\n\t\tnot_null<PeerData*> peer,\n\t\tbool forbidInForums = true) {\n\treturn CanSendValue(peer, ChatRestriction::SendOther, forbidInForums);\n}\n[[nodiscard]] inline rpl::producer<bool> CanSendAnythingValue(\n\t\tnot_null<Thread*> thread,\n\t\tbool forbidInForums = true) {\n\treturn CanSendAnyOfValue(thread, AllSendRestrictions(), forbidInForums);\n}\n[[nodiscard]] inline rpl::producer<bool> CanSendAnythingValue(\n\t\tnot_null<PeerData*> peer,\n\t\tbool forbidInForums = true) {\n\treturn CanSendAnyOfValue(peer, AllSendRestrictions(), forbidInForums);\n}\n\n[[nodiscard]] rpl::producer<bool> CanPinMessagesValue(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<bool> CanManageGroupCallValue(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<bool> PeerPremiumValue(not_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<bool> AmPremiumValue(\n\tnot_null<Main::Session*> session);\n\n[[nodiscard]] TimeId SortByOnlineValue(not_null<UserData*> user, TimeId now);\n[[nodiscard]] crl::time OnlineChangeTimeout(\n\tLastseenStatus status,\n\tTimeId now);\n[[nodiscard]] crl::time OnlineChangeTimeout(\n\tnot_null<UserData*> user,\n\tTimeId now);\n[[nodiscard]] QString OnlineText(LastseenStatus status, TimeId now);\n[[nodiscard]] QString OnlineText(not_null<UserData*> user, TimeId now);\n[[nodiscard]] QString OnlineTextFull(not_null<UserData*> user, TimeId now);\n[[nodiscard]] bool OnlineTextActive(not_null<UserData*> user, TimeId now);\n[[nodiscard]] bool IsUserOnline(not_null<UserData*> user, TimeId now = 0);\n[[nodiscard]] bool ChannelHasActiveCall(not_null<ChannelData*> channel);\n[[nodiscard]] bool ChannelHasSubscriptionUntilDate(ChannelData *channel);\n\n[[nodiscard]] rpl::producer<Data::StarsRating> StarsRatingValue(\n\tnot_null<PeerData*> peer);\n\n[[nodiscard]] rpl::producer<QImage> PeerUserpicImageValue(\n\tnot_null<PeerData*> peer,\n\tint size,\n\tstd::optional<int> radius = {});\n\n[[nodiscard]] const AllowedReactions &PeerAllowedReactions(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<AllowedReactions> PeerAllowedReactionsValue(\n\tnot_null<PeerData*> peer);\n\n[[nodiscard]] int UniqueReactionsLimit(not_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<int> UniqueReactionsLimitValue(\n\tnot_null<PeerData*> peer);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_photo.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_photo.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_reply_preview.h\"\n#include \"data/data_photo_media.h\"\n#include \"main/main_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"media/streaming/media_streaming_loader_local.h\"\n#include \"media/streaming/media_streaming_loader_mtproto.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/file_download.h\"\n#include \"core/application.h\"\n\nnamespace {\n\nconstexpr auto kPhotoSideLimit = 2560;\n\nusing Data::PhotoMedia;\nusing Data::PhotoSize;\nusing Data::PhotoSizeIndex;\nusing Data::kPhotoSizeCount;\n\n[[nodiscard]] QImage ValidatePhotoImage(\n\t\tQImage image,\n\t\tconst Data::CloudFile &file) {\n\treturn (v::is<WebFileLocation>(file.location.file().data)\n\t\t&& image.format() == QImage::Format_ARGB32)\n\t\t? Images::Opaque(std::move(image))\n\t\t: image;\n}\n\n} // namespace\n\nPhotoData::PhotoData(not_null<Data::Session*> owner, PhotoId id)\n: id(id)\n, _owner(owner) {\n}\n\nPhotoData::~PhotoData() {\n\tfor (auto &image : _images) {\n\t\tbase::take(image.loader).reset();\n\t}\n\tbase::take(_videoSizes);\n}\n\nvoid PhotoData::setFields(TimeId date, bool hasAttachedStickers) {\n\t_dateOrExtendedVideoDuration = date;\n\t_hasStickers = hasAttachedStickers;\n\t_extendedMediaPreview = false;\n}\n\nvoid PhotoData::setExtendedMediaPreview(\n\t\tQSize dimensions,\n\t\tconst QByteArray &inlineThumbnailBytes,\n\t\tstd::optional<TimeId> videoDuration) {\n\t_extendedMediaPreview = true;\n\tupdateImages(\n\t\tinlineThumbnailBytes,\n\t\t{},\n\t\t{},\n\t\t{ .location = { {}, dimensions.width(), dimensions.height() } },\n\t\t{},\n\t\t{},\n\t\t{});\n\t_dateOrExtendedVideoDuration = videoDuration ? (*videoDuration + 1) : 0;\n}\n\nbool PhotoData::extendedMediaPreview() const {\n\treturn _extendedMediaPreview;\n}\n\nstd::optional<TimeId> PhotoData::extendedMediaVideoDuration() const {\n\treturn (_extendedMediaPreview && _dateOrExtendedVideoDuration)\n\t\t? TimeId(_dateOrExtendedVideoDuration - 1)\n\t\t: std::optional<TimeId>();\n}\n\nData::Session &PhotoData::owner() const {\n\treturn *_owner;\n}\n\nMain::Session &PhotoData::session() const {\n\treturn _owner->session();\n}\n\nvoid PhotoData::automaticLoadSettingsChanged() {\n\tconst auto index = PhotoSizeIndex(PhotoSize::Large);\n\tif (!(_images[index].flags & Data::CloudFile::Flag::Cancelled)) {\n\t\treturn;\n\t}\n\t_images[index].loader = nullptr;\n\t_images[index].flags &= ~Data::CloudFile::Flag::Cancelled;\n}\n\nvoid PhotoData::load(\n\t\tData::FileOrigin origin,\n\t\tLoadFromCloudSetting fromCloud,\n\t\tbool autoLoading) {\n\tload(PhotoSize::Large, origin, fromCloud, autoLoading);\n}\n\nTimeId PhotoData::date() const {\n\treturn _extendedMediaPreview ? 0 : _dateOrExtendedVideoDuration;\n}\n\nbool PhotoData::loading() const {\n\treturn loading(PhotoSize::Large);\n}\n\nint PhotoData::validSizeIndex(PhotoSize size) const {\n\tconst auto index = PhotoSizeIndex(size);\n\tfor (auto i = index; i != kPhotoSizeCount; ++i) {\n\t\tif (_images[i].location.valid()) {\n\t\t\treturn i;\n\t\t}\n\t}\n\treturn PhotoSizeIndex(PhotoSize::Large);\n}\n\nint PhotoData::existingSizeIndex(PhotoSize size) const {\n\tconst auto index = PhotoSizeIndex(size);\n\tfor (auto i = index; i != kPhotoSizeCount; ++i) {\n\t\tif (_images[i].location.valid() || _images[i].progressivePartSize) {\n\t\t\treturn i;\n\t\t}\n\t}\n\treturn PhotoSizeIndex(PhotoSize::Large);\n}\n\nbool PhotoData::hasExact(PhotoSize size) const {\n\treturn _images[PhotoSizeIndex(size)].location.valid();\n}\n\nbool PhotoData::loading(PhotoSize size) const {\n\tconst auto valid = validSizeIndex(size);\n\tconst auto existing = existingSizeIndex(size);\n\tif (!_images[valid].loader) {\n\t\treturn false;\n\t} else if (valid == existing) {\n\t\treturn true;\n\t}\n\treturn (_images[valid].loader->loadSize()\n\t\t>= _images[existing].progressivePartSize);\n}\n\nbool PhotoData::failed(PhotoSize size) const {\n\tconst auto flags = _images[validSizeIndex(size)].flags;\n\treturn (flags & Data::CloudFile::Flag::Failed);\n}\n\nvoid PhotoData::clearFailed(PhotoSize size) {\n\t_images[validSizeIndex(size)].flags &= ~Data::CloudFile::Flag::Failed;\n}\n\nconst ImageLocation &PhotoData::location(PhotoSize size) const {\n\treturn _images[validSizeIndex(size)].location;\n}\n\nint PhotoData::SideLimit() {\n\treturn kPhotoSideLimit;\n}\n\nstd::optional<QSize> PhotoData::size(PhotoSize size) const {\n\tconst auto &provided = location(size);\n\tconst auto result = QSize{ provided.width(), provided.height() };\n\tconst auto limit = SideLimit();\n\tif (result.isEmpty()) {\n\t\treturn std::nullopt;\n\t} else if (result.width() <= limit && result.height() <= limit) {\n\t\treturn result;\n\t}\n\tconst auto scaled = result.scaled(limit, limit, Qt::KeepAspectRatio);\n\treturn QSize(std::max(scaled.width(), 1), std::max(scaled.height(), 1));\n}\n\nint PhotoData::imageByteSize(PhotoSize size) const {\n\tconst auto existing = existingSizeIndex(size);\n\tif (const auto result = _images[existing].progressivePartSize) {\n\t\treturn result;\n\t}\n\treturn _images[validSizeIndex(size)].byteSize;\n}\n\nbool PhotoData::displayLoading() const {\n\tconst auto index = PhotoSizeIndex(PhotoSize::Large);\n\tif (const auto loader = _images[index].loader.get()) {\n\t\treturn !loader->finished()\n\t\t\t&& (!loader->loadingLocal() || !loader->autoLoading());\n\t}\n\treturn (uploading() && !waitingForAlbum());\n}\n\nvoid PhotoData::cancel() {\n\tif (loading()) {\n\t\t_images[PhotoSizeIndex(PhotoSize::Large)].loader->cancel();\n\t}\n}\n\nfloat64 PhotoData::progress() const {\n\tif (uploading()) {\n\t\tif (uploadingData->size > 0) {\n\t\t\tconst auto result = float64(uploadingData->offset)\n\t\t\t\t/ uploadingData->size;\n\t\t\treturn std::clamp(result, 0., 1.);\n\t\t}\n\t\treturn 0.;\n\t}\n\tconst auto index = PhotoSizeIndex(PhotoSize::Large);\n\treturn loading() ? _images[index].loader->currentProgress() : 0.;\n}\n\nbool PhotoData::cancelled() const {\n\tconst auto index = PhotoSizeIndex(PhotoSize::Large);\n\treturn (_images[index].flags & Data::CloudFile::Flag::Cancelled);\n}\n\nvoid PhotoData::setWaitingForAlbum() {\n\tif (uploading()) {\n\t\tuploadingData->waitingForAlbum = true;\n\t}\n}\n\nbool PhotoData::waitingForAlbum() const {\n\treturn uploading() && uploadingData->waitingForAlbum;\n}\n\nint32 PhotoData::loadOffset() const {\n\tconst auto index = PhotoSizeIndex(PhotoSize::Large);\n\treturn loading() ? _images[index].loader->currentOffset() : 0;\n}\n\nbool PhotoData::uploading() const {\n\treturn (uploadingData != nullptr);\n}\n\nImage *PhotoData::getReplyPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<PeerData*> context,\n\t\tbool spoiler) {\n\tif (!_replyPreview) {\n\t\t_replyPreview = std::make_unique<Data::ReplyPreview>(this);\n\t}\n\treturn _replyPreview->image(origin, context, spoiler);\n}\n\nImage *PhotoData::getReplyPreview(not_null<HistoryItem*> item) {\n\tconst auto media = item->media();\n\tconst auto spoiler = (media && media->hasSpoiler());\n\treturn getReplyPreview(item->fullId(), item->history()->peer, spoiler);\n}\n\nbool PhotoData::replyPreviewLoaded(bool spoiler) const {\n\tif (!_replyPreview) {\n\t\treturn false;\n\t}\n\treturn _replyPreview->loaded(spoiler);\n}\n\nvoid PhotoData::setRemoteLocation(\n\t\tint32 dc,\n\t\tuint64 access,\n\t\tconst QByteArray &fileReference) {\n\t_fileReference = fileReference;\n\tif (_dc != dc || _access != access) {\n\t\t_dc = dc;\n\t\t_access = access;\n\t}\n}\n\nMTPInputPhoto PhotoData::mtpInput() const {\n\treturn MTP_inputPhoto(\n\t\tMTP_long(id),\n\t\tMTP_long(_access),\n\t\tMTP_bytes(_fileReference));\n}\n\nQByteArray PhotoData::fileReference() const {\n\treturn _fileReference;\n}\n\nvoid PhotoData::refreshFileReference(const QByteArray &value) {\n\t_fileReference = value;\n\tfor (auto &image : _images) {\n\t\timage.location.refreshFileReference(value);\n\t}\n}\n\nvoid PhotoData::collectLocalData(not_null<PhotoData*> local) {\n\tif (local == this) {\n\t\treturn;\n\t}\n\n\tfor (auto i = 0; i != kPhotoSizeCount; ++i) {\n\t\tif (const auto from = local->_images[i].location.file().cacheKey()) {\n\t\t\tif (const auto to = _images[i].location.file().cacheKey()) {\n\t\t\t\t_owner->cache().copyIfEmpty(from, to);\n\t\t\t}\n\t\t}\n\t}\n\tif (const auto localMedia = local->activeMediaView()) {\n\t\tauto media = createMediaView();\n\t\tmedia->collectLocalData(localMedia.get());\n\t\t_owner->keepAlive(std::move(media));\n\t}\n}\n\nbool PhotoData::isNull() const {\n\treturn !_images[PhotoSizeIndex(PhotoSize::Large)].location.valid();\n}\n\nvoid PhotoData::load(\n\t\tPhotoSize size,\n\t\tData::FileOrigin origin,\n\t\tLoadFromCloudSetting fromCloud,\n\t\tbool autoLoading) {\n\tconst auto valid = validSizeIndex(size);\n\tconst auto existing = existingSizeIndex(size);\n\n\t// Could've changed, if the requested size didn't have a location.\n\tconst auto validSize = static_cast<PhotoSize>(valid);\n\tconst auto finalCheck = [=] {\n\t\tif (const auto active = activeMediaView()) {\n\t\t\treturn !active->image(size);\n\t\t}\n\t\treturn true;\n\t};\n\tconst auto done = [=](QImage result, QByteArray bytes) {\n\t\tExpects(_images[valid].loader != nullptr);\n\n\t\t// Find out what progressive photo size have we loaded exactly.\n\t\tauto goodFor = validSize;\n\t\tconst auto loadSize = _images[valid].loader->loadSize();\n\t\tif (valid > 0 && _images[valid].byteSize > loadSize) {\n\t\t\tfor (auto i = valid; i != 0;) {\n\t\t\t\t--i;\n\t\t\t\tconst auto required = _images[i].progressivePartSize;\n\t\t\t\tif (required > 0 && required <= loadSize) {\n\t\t\t\t\tgoodFor = static_cast<PhotoSize>(i);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (const auto active = activeMediaView()) {\n\t\t\tactive->set(\n\t\t\t\tvalidSize,\n\t\t\t\tgoodFor,\n\t\t\t\tValidatePhotoImage(std::move(result), _images[valid]),\n\t\t\t\tstd::move(bytes));\n\t\t}\n\t\tif (validSize == PhotoSize::Large && goodFor == validSize) {\n\t\t\t_owner->photoLoadDone(this);\n\t\t}\n\t};\n\tconst auto fail = [=](bool started) {\n\t\tif (validSize == PhotoSize::Large) {\n\t\t\t_owner->photoLoadFail(this, started);\n\t\t}\n\t};\n\tconst auto progress = [=] {\n\t\tif (validSize == PhotoSize::Large) {\n\t\t\t_owner->photoLoadProgress(this);\n\t\t}\n\t};\n\tData::LoadCloudFile(\n\t\t&session(),\n\t\t_images[valid],\n\t\torigin,\n\t\tfromCloud,\n\t\tautoLoading,\n\t\tData::kImageCacheTag,\n\t\tfinalCheck,\n\t\tdone,\n\t\tfail,\n\t\tprogress,\n\t\t_images[existing].progressivePartSize);\n\n\tif (size == PhotoSize::Large) {\n\t\t_owner->notifyPhotoLayoutChanged(this);\n\t}\n}\n\nstd::shared_ptr<PhotoMedia> PhotoData::createMediaView() {\n\tif (auto result = activeMediaView()) {\n\t\treturn result;\n\t}\n\tauto result = std::make_shared<PhotoMedia>(this);\n\t_media = result;\n\treturn result;\n}\n\nstd::shared_ptr<PhotoMedia> PhotoData::activeMediaView() const {\n\treturn _media.lock();\n}\n\nvoid PhotoData::updateImages(\n\t\tconst QByteArray &inlineThumbnailBytes,\n\t\tconst ImageWithLocation &small,\n\t\tconst ImageWithLocation &thumbnail,\n\t\tconst ImageWithLocation &large,\n\t\tconst ImageWithLocation &videoSmall,\n\t\tconst ImageWithLocation &videoLarge,\n\t\tcrl::time videoStartTime) {\n\tif (!inlineThumbnailBytes.isEmpty()\n\t\t&& _inlineThumbnailBytes.isEmpty()) {\n\t\t_inlineThumbnailBytes = inlineThumbnailBytes;\n\t}\n\tconst auto update = [&](PhotoSize size, const ImageWithLocation &data) {\n\t\tconst auto index = PhotoSizeIndex(size);\n\t\tData::UpdateCloudFile(\n\t\t\t_images[index],\n\t\t\tdata,\n\t\t\towner().cache(),\n\t\t\tData::kImageCacheTag,\n\t\t\t[=](Data::FileOrigin origin) { load(size, origin); },\n\t\t\t[=](QImage preloaded, QByteArray bytes) {\n\t\t\t\tif (const auto media = activeMediaView()) {\n\t\t\t\t\tmedia->set(\n\t\t\t\t\t\tsize,\n\t\t\t\t\t\tsize,\n\t\t\t\t\t\tValidatePhotoImage(\n\t\t\t\t\t\t\tstd::move(preloaded),\n\t\t\t\t\t\t\t_images[index]),\n\t\t\t\t\t\tstd::move(bytes));\n\t\t\t\t}\n\t\t\t});\n\t};\n\tupdate(PhotoSize::Small, small);\n\tupdate(PhotoSize::Thumbnail, thumbnail);\n\tupdate(PhotoSize::Large, large);\n\n\tif (!videoLarge.location.valid()) {\n\t\t_videoSizes = nullptr;\n\t} else {\n\t\tif (!_videoSizes) {\n\t\t\t_videoSizes = std::make_unique<VideoSizes>();\n\t\t}\n\t\t_videoSizes->startTime = videoStartTime;\n\t\tconstexpr auto large = PhotoSize::Large;\n\t\tconstexpr auto small = PhotoSize::Small;\n\t\tData::UpdateCloudFile(\n\t\t\t_videoSizes->large,\n\t\t\tvideoLarge,\n\t\t\towner().cache(),\n\t\t\tData::kAnimationCacheTag,\n\t\t\t[&](Data::FileOrigin origin) { loadVideo(large, origin); });\n\t\tData::UpdateCloudFile(\n\t\t\t_videoSizes->small,\n\t\t\tvideoSmall,\n\t\t\towner().cache(),\n\t\t\tData::kAnimationCacheTag,\n\t\t\t[&](Data::FileOrigin origin) { loadVideo(small, origin); });\n\t}\n}\n\n[[nodiscard]] bool PhotoData::hasAttachedStickers() const {\n\treturn _hasStickers;\n}\n\nvoid PhotoData::setHasAttachedStickers(bool value) {\n\t_hasStickers = value;\n}\n\nint PhotoData::width() const {\n\treturn _images[PhotoSizeIndex(PhotoSize::Large)].location.width();\n}\n\nint PhotoData::height() const {\n\treturn _images[PhotoSizeIndex(PhotoSize::Large)].location.height();\n}\n\nMediaKey PhotoData::mediaKey() const {\n\treturn ::mediaKey(UnknownFileLocation, _dc, id);\n}\n\nconst Core::FileLocation &PhotoData::location(bool check) const {\n\tif (check && !_location.check()) {\n\t\tconst auto location = session().local().readFileLocation(mediaKey());\n\t\tconst auto that = const_cast<PhotoData*>(this);\n\t\tif (!location.inMediaCache()) {\n\t\t\tthat->_location = location;\n\t\t}\n\t}\n\treturn _location;\n}\n\nvoid PhotoData::setLocation(const Core::FileLocation &loc) {\n\tif (!loc.inMediaCache() && loc.check()) {\n\t\t_location = loc;\n\t\tsession().local().writeFileLocation(mediaKey(), _location);\n\t}\n}\n\nData::CloudFile &PhotoData::videoFile(PhotoSize size) {\n\tExpects(_videoSizes != nullptr);\n\n\treturn (size == PhotoSize::Small && hasVideoSmall())\n\t\t? _videoSizes->small\n\t\t: _videoSizes->large;\n}\n\nconst Data::CloudFile &PhotoData::videoFile(PhotoSize size) const {\n\tExpects(_videoSizes != nullptr);\n\n\treturn (size == PhotoSize::Small && hasVideoSmall())\n\t\t? _videoSizes->small\n\t\t: _videoSizes->large;\n}\n\n\nbool PhotoData::hasVideo() const {\n\treturn _videoSizes != nullptr;\n}\n\nbool PhotoData::hasVideoSmall() const {\n\treturn hasVideo() && _videoSizes->small.location.valid();\n}\n\nbool PhotoData::videoLoading(Data::PhotoSize size) const {\n\treturn _videoSizes && videoFile(size).loader != nullptr;\n}\n\nbool PhotoData::videoFailed(Data::PhotoSize size) const {\n\treturn _videoSizes\n\t\t&& (videoFile(size).flags & Data::CloudFile::Flag::Failed);\n}\n\nvoid PhotoData::loadVideo(Data::PhotoSize size, Data::FileOrigin origin) {\n\tif (!_videoSizes) {\n\t\treturn;\n\t}\n\tconst auto autoLoading = false;\n\tconst auto finalCheck = [=] {\n\t\tif (const auto active = activeMediaView()) {\n\t\t\treturn active->videoContent(size).isEmpty();\n\t\t}\n\t\treturn true;\n\t};\n\tconst auto done = [=](QByteArray result) {\n\t\tif (const auto active = activeMediaView()) {\n\t\t\tactive->setVideo(size, std::move(result));\n\t\t}\n\t};\n\tData::LoadCloudFile(\n\t\t&session(),\n\t\tvideoFile(size),\n\t\torigin,\n\t\tLoadFromCloudOrLocal,\n\t\tautoLoading,\n\t\tData::kAnimationCacheTag,\n\t\tfinalCheck,\n\t\tdone);\n}\n\nconst ImageLocation &PhotoData::videoLocation(Data::PhotoSize size) const {\n\tstatic const auto empty = ImageLocation();\n\treturn _videoSizes ? videoFile(size).location : empty;\n}\n\nint PhotoData::videoByteSize(Data::PhotoSize size) const {\n\treturn _videoSizes ? videoFile(size).byteSize : 0;\n}\n\ncrl::time PhotoData::videoStartPosition() const {\n\treturn _videoSizes ? _videoSizes->startTime : crl::time(0);\n}\n\nvoid PhotoData::setVideoPlaybackFailed() {\n\tif (_videoSizes) {\n\t\t_videoSizes->playbackFailed = true;\n\t}\n}\n\nbool PhotoData::videoPlaybackFailed() const {\n\treturn _videoSizes && _videoSizes->playbackFailed;\n}\n\nbool PhotoData::videoCanBePlayed() const {\n\treturn hasVideo() && !videoPlaybackFailed();\n}\n\nauto PhotoData::createStreamingLoader(\n\tData::FileOrigin origin,\n\tbool forceRemoteLoader) const\n-> std::unique_ptr<Media::Streaming::Loader> {\n\tif (!hasVideo()) {\n\t\treturn nullptr;\n\t}\n\tconstexpr auto large = PhotoSize::Large;\n\tif (!forceRemoteLoader) {\n\t\tconst auto media = activeMediaView();\n\t\tconst auto bytes = media ? media->videoContent(large) : QByteArray();\n\t\tif (media && !bytes.isEmpty()) {\n\t\t\treturn Media::Streaming::MakeBytesLoader(bytes);\n\t\t}\n\t}\n\treturn v::is<StorageFileLocation>(videoLocation(large).file().data)\n\t\t? std::make_unique<Media::Streaming::LoaderMtproto>(\n\t\t\t&session().downloader(),\n\t\t\tv::get<StorageFileLocation>(videoLocation(large).file().data),\n\t\t\tvideoByteSize(large),\n\t\t\torigin)\n\t\t: nullptr;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_photo.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_types.h\"\n#include \"data/data_cloud_file.h\"\n#include \"core/file_location.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Media {\nnamespace Streaming {\nclass Loader;\n} // namespace Streaming\n} // namespace Media\n\nnamespace Data {\n\nclass Session;\nclass ReplyPreview;\nclass PhotoMedia;\n\ninline constexpr auto kPhotoSizeCount = 3;\n\nenum class PhotoSize : uchar {\n\tSmall,\n\tThumbnail,\n\tLarge,\n};\n\n[[nodiscard]] inline int PhotoSizeIndex(PhotoSize size) {\n\tExpects(static_cast<int>(size) < kPhotoSizeCount);\n\n\treturn static_cast<int>(size);\n}\n\n} // namespace Data\n\nclass PhotoData final {\npublic:\n\tPhotoData(not_null<Data::Session*> owner, PhotoId id);\n\t~PhotoData();\n\n\t[[nodiscard]] Data::Session &owner() const;\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] bool isNull() const;\n\n\tvoid automaticLoadSettingsChanged();\n\n\t[[nodiscard]] TimeId date() const;\n\t[[nodiscard]] bool loading() const;\n\t[[nodiscard]] bool displayLoading() const;\n\tvoid cancel();\n\t[[nodiscard]] float64 progress() const;\n\t[[nodiscard]] int32 loadOffset() const;\n\t[[nodiscard]] bool uploading() const;\n\t[[nodiscard]] bool cancelled() const;\n\n\tvoid setWaitingForAlbum();\n\t[[nodiscard]] bool waitingForAlbum() const;\n\n\t[[nodiscard]] Image *getReplyPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<PeerData*> context,\n\t\tbool spoiler);\n\t[[nodiscard]] Image *getReplyPreview(not_null<HistoryItem*> item);\n\t[[nodiscard]] bool replyPreviewLoaded(bool spoiler) const;\n\n\tvoid setRemoteLocation(\n\t\tint32 dc,\n\t\tuint64 access,\n\t\tconst QByteArray &fileReference);\n\t[[nodiscard]] MTPInputPhoto mtpInput() const;\n\t[[nodiscard]] QByteArray fileReference() const;\n\tvoid refreshFileReference(const QByteArray &value);\n\n\t// When we have some client-side generated photo\n\t// (for example for displaying an external inline bot result)\n\t// and it has downloaded full image, we can collect image from it\n\t// to (this) received from the server \"same\" photo.\n\tvoid collectLocalData(not_null<PhotoData*> local);\n\n\t[[nodiscard]] std::shared_ptr<Data::PhotoMedia> createMediaView();\n\t[[nodiscard]] auto activeMediaView() const\n\t\t-> std::shared_ptr<Data::PhotoMedia>;\n\n\tvoid setFields(TimeId date, bool hasAttachedStickers);\n\tvoid setExtendedMediaPreview(\n\t\tQSize dimensions,\n\t\tconst QByteArray &inlineThumbnailBytes,\n\t\tstd::optional<TimeId> videoDuration);\n\t[[nodiscard]] bool extendedMediaPreview() const;\n\t[[nodiscard]] std::optional<TimeId> extendedMediaVideoDuration() const;\n\n\tvoid updateImages(\n\t\tconst QByteArray &inlineThumbnailBytes,\n\t\tconst ImageWithLocation &small,\n\t\tconst ImageWithLocation &thumbnail,\n\t\tconst ImageWithLocation &large,\n\t\tconst ImageWithLocation &videoSmall,\n\t\tconst ImageWithLocation &videoLarge,\n\t\tcrl::time videoStartTime);\n\t[[nodiscard]] int validSizeIndex(Data::PhotoSize size) const;\n\t[[nodiscard]] int existingSizeIndex(Data::PhotoSize size) const;\n\n\t[[nodiscard]] QByteArray inlineThumbnailBytes() const {\n\t\treturn _inlineThumbnailBytes;\n\t}\n\tvoid clearInlineThumbnailBytes() {\n\t\t_inlineThumbnailBytes = QByteArray();\n\t}\n\n\tvoid load(\n\t\tData::FileOrigin origin,\n\t\tLoadFromCloudSetting fromCloud = LoadFromCloudOrLocal,\n\t\tbool autoLoading = false);\n\n\t[[nodiscard]] static int SideLimit();\n\n\t[[nodiscard]] bool hasExact(Data::PhotoSize size) const;\n\t[[nodiscard]] bool loading(Data::PhotoSize size) const;\n\t[[nodiscard]] bool failed(Data::PhotoSize size) const;\n\tvoid clearFailed(Data::PhotoSize size);\n\tvoid load(\n\t\tData::PhotoSize size,\n\t\tData::FileOrigin origin,\n\t\tLoadFromCloudSetting fromCloud = LoadFromCloudOrLocal,\n\t\tbool autoLoading = false);\n\t[[nodiscard]] const ImageLocation &location(Data::PhotoSize size) const;\n\t[[nodiscard]] std::optional<QSize> size(Data::PhotoSize size) const;\n\t[[nodiscard]] int imageByteSize(Data::PhotoSize size) const;\n\n\t[[nodiscard]] bool hasVideo() const;\n\t[[nodiscard]] bool hasVideoSmall() const;\n\t[[nodiscard]] bool videoLoading(Data::PhotoSize size) const;\n\t[[nodiscard]] bool videoFailed(Data::PhotoSize size) const;\n\tvoid loadVideo(Data::PhotoSize size, Data::FileOrigin origin);\n\t[[nodiscard]] const ImageLocation &videoLocation(\n\t\tData::PhotoSize size) const;\n\t[[nodiscard]] int videoByteSize(Data::PhotoSize size) const;\n\t[[nodiscard]] crl::time videoStartPosition() const;\n\tvoid setVideoPlaybackFailed();\n\t[[nodiscard]] bool videoPlaybackFailed() const;\n\t[[nodiscard]] bool videoCanBePlayed() const;\n\t[[nodiscard]] auto createStreamingLoader(\n\t\tData::FileOrigin origin,\n\t\tbool forceRemoteLoader) const\n\t-> std::unique_ptr<Media::Streaming::Loader>;\n\n\t[[nodiscard]] bool hasAttachedStickers() const;\n\tvoid setHasAttachedStickers(bool value);\n\n\t// For now they return size of the 'large' image.\n\t[[nodiscard]] int width() const;\n\t[[nodiscard]] int height() const;\n\n\t[[nodiscard]] MediaKey mediaKey() const;\n\t[[nodiscard]] const Core::FileLocation &location(bool check) const;\n\tvoid setLocation(const Core::FileLocation &loc);\n\n\tPhotoId id = 0;\n\n\tPeerData *peer = nullptr; // for chat and channel photos connection\n\t// geo, caption\n\n\tstd::unique_ptr<Data::UploadState> uploadingData;\n\nprivate:\n\t[[nodiscard]] Data::CloudFile &videoFile(Data::PhotoSize size);\n\t[[nodiscard]] const Data::CloudFile &videoFile(\n\t\tData::PhotoSize size) const;\n\n\tTimeId _dateOrExtendedVideoDuration = 0;\n\n\tstruct VideoSizes {\n\t\tData::CloudFile small;\n\t\tData::CloudFile large;\n\t\tcrl::time startTime = 0;\n\t\tbool playbackFailed = false;\n\t};\n\tQByteArray _inlineThumbnailBytes;\n\tstd::array<Data::CloudFile, Data::kPhotoSizeCount> _images;\n\tstd::unique_ptr<VideoSizes> _videoSizes;\n\n\tint32 _dc = 0;\n\tuint64 _access = 0;\n\tbool _hasStickers = false;\n\tbool _extendedMediaPreview = false;\n\n\tQByteArray _fileReference;\n\tstd::unique_ptr<Data::ReplyPreview> _replyPreview;\n\tstd::weak_ptr<Data::PhotoMedia> _media;\n\n\tnot_null<Data::Session*> _owner;\n\n\tCore::FileLocation _location;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_photo_media.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_photo_media.h\"\n\n#include \"data/data_file_origin.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"storage/file_download.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QClipboard>\n\nnamespace Data {\n\nPhotoMedia::PhotoMedia(not_null<PhotoData*> owner)\n: _owner(owner) {\n}\n\n// NB! Right now DocumentMedia can outlive Main::Session!\n// In DocumentData::collectLocalData a shared_ptr is sent on_main.\n// In case this is a problem the ~Gif code should be rewritten.\nPhotoMedia::~PhotoMedia() = default;\n\nnot_null<PhotoData*> PhotoMedia::owner() const {\n\treturn _owner;\n}\n\nImage *PhotoMedia::thumbnailInline() const {\n\tif (!_inlineThumbnail) {\n\t\tconst auto bytes = _owner->inlineThumbnailBytes();\n\t\tif (!bytes.isEmpty()) {\n\t\t\tauto image = Images::FromInlineBytes(bytes);\n\t\t\tif (image.isNull()) {\n\t\t\t\t_owner->clearInlineThumbnailBytes();\n\t\t\t} else {\n\t\t\t\t_inlineThumbnail = std::make_unique<Image>(std::move(image));\n\t\t\t}\n\t\t}\n\t}\n\treturn _inlineThumbnail.get();\n}\n\nImage *PhotoMedia::image(PhotoSize size) const {\n\tif (const auto resolved = resolveLoadedImage(size)) {\n\t\treturn resolved->data.get();\n\t}\n\treturn nullptr;\n}\n\nQByteArray PhotoMedia::imageBytes(PhotoSize size) const {\n\tif (const auto resolved = resolveLoadedImage(size)) {\n\t\treturn resolved->bytes;\n\t}\n\treturn QByteArray();\n}\n\nauto PhotoMedia::resolveLoadedImage(PhotoSize size) const\n-> const PhotoImage * {\n\tconst auto &original = _images[PhotoSizeIndex(size)];\n\tif (original.data) {\n\t\tif (original.goodFor >= size) {\n\t\t\treturn &original;\n\t\t}\n\t}\n\tconst auto &valid = _images[_owner->validSizeIndex(size)];\n\tif (valid.data.get()) {\n\t\tif (valid.goodFor >= size) {\n\t\t\treturn &valid;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid PhotoMedia::wanted(PhotoSize size, Data::FileOrigin origin) {\n\tconst auto index = _owner->validSizeIndex(size);\n\tif (!_images[index].data || _images[index].goodFor < size) {\n\t\t_owner->load(size, origin);\n\t}\n}\n\nQSize PhotoMedia::size(PhotoSize size) const {\n\tconst auto index = PhotoSizeIndex(size);\n\tif (const auto image = _images[index].data.get()) {\n\t\treturn image->size();\n\t}\n\tconst auto &location = _owner->location(size);\n\treturn { location.width(), location.height() };\n}\n\nvoid PhotoMedia::set(\n\t\tPhotoSize size,\n\t\tPhotoSize goodFor,\n\t\tQImage image,\n\t\tQByteArray bytes) {\n\tconst auto index = PhotoSizeIndex(size);\n\tconst auto limit = PhotoData::SideLimit();\n\tif (image.width() > limit || image.height() > limit) {\n\t\timage = image.scaled(\n\t\t\tlimit,\n\t\t\tlimit,\n\t\t\tQt::KeepAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t}\n\t_images[index] = PhotoImage{\n\t\t.data = std::make_unique<Image>(std::move(image)),\n\t\t.bytes = std::move(bytes),\n\t\t.goodFor = goodFor,\n\t};\n\t_owner->session().notifyDownloaderTaskFinished();\n}\n\nQByteArray PhotoMedia::videoContent(PhotoSize size) const {\n\tconst auto small = (size == PhotoSize::Small) && _owner->hasVideoSmall();\n\treturn small ? _videoBytesSmall : _videoBytesLarge;\n}\n\nQSize PhotoMedia::videoSize(PhotoSize size) const {\n\tconst auto &location = _owner->videoLocation(size);\n\treturn { location.width(), location.height() };\n}\n\nvoid PhotoMedia::videoWanted(PhotoSize size, Data::FileOrigin origin) {\n\tif (videoContent(size).isEmpty()) {\n\t\t_owner->loadVideo(size, origin);\n\t}\n}\n\nvoid PhotoMedia::setVideo(PhotoSize size, QByteArray content) {\n\tconst auto small = (size == PhotoSize::Small) && _owner->hasVideoSmall();\n\t(small ? _videoBytesSmall : _videoBytesLarge) = std::move(content);\n}\n\nbool PhotoMedia::loaded() const {\n\tconst auto index = PhotoSizeIndex(PhotoSize::Large);\n\treturn (_images[index].data != nullptr)\n\t\t&& (_images[index].goodFor >= PhotoSize::Large);\n}\n\nfloat64 PhotoMedia::progress() const {\n\treturn (_owner->uploading() || _owner->loading())\n\t\t? _owner->progress()\n\t\t: (loaded() ? 1. : 0.);\n}\n\nbool PhotoMedia::autoLoadThumbnailAllowed(not_null<PeerData*> peer) const {\n\tif (loaded() || _owner->cancelled()) {\n\t\treturn false;\n\t}\n\treturn _owner->hasExact(PhotoSize::Small)\n\t\t|| _owner->hasExact(PhotoSize::Thumbnail)\n\t\t|| AutoDownload::Should(\n\t\t\t_owner->session().settings().autoDownload(),\n\t\t\tpeer,\n\t\t\t_owner);\n}\n\nvoid PhotoMedia::automaticLoad(\n\t\tFileOrigin origin,\n\t\tconst HistoryItem *item) {\n\tif (item) {\n\t\tautomaticLoad(origin, item->history()->peer);\n\t}\n}\n\nvoid PhotoMedia::automaticLoad(\n\t\tFileOrigin origin,\n\t\tnot_null<PeerData*> peer) {\n\tif (loaded() || _owner->cancelled()) {\n\t\treturn;\n\t}\n\tconst auto loadFromCloud = Data::AutoDownload::Should(\n\t\t_owner->session().settings().autoDownload(),\n\t\tpeer,\n\t\t_owner);\n\t_owner->load(\n\t\torigin,\n\t\tloadFromCloud ? LoadFromCloudOrLocal : LoadFromLocalOnly,\n\t\ttrue);\n}\n\nvoid PhotoMedia::collectLocalData(not_null<PhotoMedia*> local) {\n\tif (const auto image = local->_inlineThumbnail.get()) {\n\t\t_inlineThumbnail = std::make_unique<Image>(image->original());\n\t}\n\tfor (auto i = 0; i != kPhotoSizeCount; ++i) {\n\t\tif (const auto image = local->_images[i].data.get()) {\n\t\t\t_images[i] = PhotoImage{\n\t\t\t\t.data = std::make_unique<Image>(image->original()),\n\t\t\t\t.goodFor = local->_images[i].goodFor\n\t\t\t};\n\t\t}\n\t}\n}\n\nbool PhotoMedia::saveToFile(const QString &path) {\n\tconstexpr auto large = PhotoSize::Large;\n\tif (const auto video = videoContent(large); !video.isEmpty()) {\n\t\tQFile f(path);\n\t\treturn f.open(QIODevice::WriteOnly)\n\t\t\t&& (f.write(video) == video.size());\n\t} else if (const auto photo = imageBytes(large); !photo.isEmpty()) {\n\t\tQFile f(path);\n\t\treturn f.open(QIODevice::WriteOnly)\n\t\t\t&& (f.write(photo) == photo.size());\n\t} else if (const auto fallback = image(large)->original()\n\t\t; !fallback.isNull()) {\n\t\treturn fallback.save(path, \"JPG\");\n\t}\n\treturn false;\n}\n\nbool PhotoMedia::setToClipboard() {\n\tconstexpr auto large = PhotoSize::Large;\n\tif (const auto video = videoContent(large); !video.isEmpty()) {\n\t\treturn false;\n\t}\n\tauto fallback = image(large)->original();\n\tif (fallback.isNull()) {\n\t\treturn false;\n\t}\n\tauto mime = std::make_unique<QMimeData>();\n\tmime->setImageData(std::move(fallback));\n\tif (auto bytes = imageBytes(large); !bytes.isEmpty()) {\n\t\tmime->setData(u\"image/jpeg\"_q, std::move(bytes));\n\t}\n\tmime->setData(u\"application/x-td-use-jpeg\"_q, \"1\");\n\tQGuiApplication::clipboard()->setMimeData(mime.release());\n\treturn true;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_photo_media.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_photo.h\"\n\nclass FileLoader;\n\nnamespace Data {\n\nclass PhotoMedia final {\npublic:\n\texplicit PhotoMedia(not_null<PhotoData*> owner);\n\t~PhotoMedia();\n\n\t[[nodiscard]] not_null<PhotoData*> owner() const;\n\n\t[[nodiscard]] Image *thumbnailInline() const;\n\n\t[[nodiscard]] Image *image(PhotoSize size) const;\n\t[[nodiscard]] QByteArray imageBytes(PhotoSize size) const;\n\t[[nodiscard]] QSize size(PhotoSize size) const;\n\tvoid wanted(PhotoSize size, Data::FileOrigin origin);\n\tvoid set(\n\t\tPhotoSize size,\n\t\tPhotoSize goodFor,\n\t\tQImage image,\n\t\tQByteArray bytes);\n\n\t[[nodiscard]] QByteArray videoContent(PhotoSize size) const;\n\t[[nodiscard]] QSize videoSize(PhotoSize size) const;\n\tvoid videoWanted(PhotoSize size, Data::FileOrigin origin);\n\tvoid setVideo(PhotoSize size, QByteArray content);\n\n\t[[nodiscard]] bool loaded() const;\n\t[[nodiscard]] float64 progress() const;\n\n\t[[nodiscard]] bool autoLoadThumbnailAllowed(\n\t\tnot_null<PeerData*> peer) const;\n\tvoid automaticLoad(FileOrigin origin, const HistoryItem *item);\n\tvoid automaticLoad(FileOrigin origin, not_null<PeerData*> peer);\n\n\tvoid collectLocalData(not_null<PhotoMedia*> local);\n\n\tbool saveToFile(const QString &path);\n\tbool setToClipboard();\n\nprivate:\n\tstruct PhotoImage {\n\t\tstd::unique_ptr<Image> data;\n\t\tQByteArray bytes;\n\t\tPhotoSize goodFor = PhotoSize();\n\t};\n\n\tconst PhotoImage *resolveLoadedImage(PhotoSize size) const;\n\n\t// NB! Right now DocumentMedia can outlive Main::Session!\n\t// In DocumentData::collectLocalData a shared_ptr is sent on_main.\n\t// In case this is a problem the ~Gif code should be rewritten.\n\tconst not_null<PhotoData*> _owner;\n\tmutable std::unique_ptr<Image> _inlineThumbnail;\n\tstd::array<PhotoImage, kPhotoSizeCount> _images;\n\tQByteArray _videoBytesSmall;\n\tQByteArray _videoBytesLarge;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_poll.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_poll.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"data/data_document.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"base/call_delayed.h\"\n#include \"main/main_session.h\"\n#include \"ui/text/text_options.h\"\n\nnamespace {\n\nconstexpr auto kShortPollTimeout = 30 * crl::time(1000);\nconstexpr auto kReloadAfterAutoCloseDelay = crl::time(1000);\n\nvoid ProcessPollMedia(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst MTPMessageMedia &media) {\n\tmedia.match([&](const MTPDmessageMediaPhoto &media) {\n\t\tif (const auto photo = media.vphoto()) {\n\t\t\tphoto->match([&](const MTPDphoto &) {\n\t\t\t\towner->processPhoto(*photo);\n\t\t\t}, [](const auto &) {\n\t\t\t});\n\t\t}\n\t}, [&](const MTPDmessageMediaDocument &media) {\n\t\tif (const auto document = media.vdocument()) {\n\t\t\tdocument->match([&](const MTPDdocument &) {\n\t\t\t\towner->processDocument(*document);\n\t\t\t}, [](const auto &) {\n\t\t\t});\n\t\t}\n\t}, [](const auto &) {\n\t});\n}\n\nconst PollAnswer *AnswerByOption(\n\t\tconst std::vector<PollAnswer> &list,\n\t\tconst QByteArray &option) {\n\tconst auto i = ranges::find(\n\t\tlist,\n\t\toption,\n\t\t[](const PollAnswer &a) { return a.option; });\n\treturn (i != end(list)) ? &*i : nullptr;\n}\n\nPollAnswer *AnswerByOption(\n\t\tstd::vector<PollAnswer> &list,\n\t\tconst QByteArray &option) {\n\treturn const_cast<PollAnswer*>(AnswerByOption(\n\t\tstd::as_const(list),\n\t\toption));\n}\n\n} // namespace\n\nPollData::PollData(not_null<Data::Session*> owner, PollId id)\n: id(id)\n, _owner(owner) {\n}\n\nData::Session &PollData::owner() const {\n\treturn *_owner;\n}\n\nMain::Session &PollData::session() const {\n\treturn _owner->session();\n}\n\nbool PollData::closeByTimer() {\n\tif (closed()) {\n\t\treturn false;\n\t}\n\t_flags |= Flag::Closed;\n\t++version;\n\tbase::call_delayed(kReloadAfterAutoCloseDelay, &_owner->session(), [=] {\n\t\t_lastResultsUpdate = -1; // Force reload results.\n\t\t++version;\n\t\t_owner->notifyPollUpdateDelayed(this);\n\t});\n\treturn true;\n}\n\nbool PollData::applyChanges(const MTPDpoll &poll) {\n\tExpects(poll.vid().v == id);\n\n\tconst auto newQuestion = Api::ParseTextWithEntities(\n\t\t&session(),\n\t\tpoll.vquestion());\n\tconst auto newFlags = (poll.is_closed() ? Flag::Closed : Flag(0))\n\t\t| (poll.is_public_voters() ? Flag::PublicVotes : Flag(0))\n\t\t| (poll.is_multiple_choice() ? Flag::MultiChoice : Flag(0))\n\t\t| (poll.is_quiz() ? Flag::Quiz : Flag(0))\n\t\t| (poll.is_shuffle_answers() ? Flag::ShuffleAnswers : Flag(0))\n\t\t| (poll.is_revoting_disabled() ? Flag::RevotingDisabled : Flag(0))\n\t\t| (poll.is_open_answers() ? Flag::OpenAnswers : Flag(0))\n\t\t| (poll.is_hide_results_until_close()\n\t\t\t? Flag::HideResultsUntilClose\n\t\t\t: Flag(0))\n\t\t| (poll.is_creator() ? Flag::Creator : Flag(0));\n\tconst auto newCloseDate = poll.vclose_date().value_or_empty();\n\tconst auto newClosePeriod = poll.vclose_period().value_or_empty();\n\tauto newAnswers = ranges::views::all(\n\t\tpoll.vanswers().v\n\t) | ranges::views::transform([&](const MTPPollAnswer &data) {\n\t\treturn data.match([&](const MTPDpollAnswer &answer) {\n\t\t\tauto result = PollAnswer();\n\t\t\tresult.option = answer.voption().v;\n\t\t\tresult.text = Api::ParseTextWithEntities(\n\t\t\t\t&session(),\n\t\t\t\tanswer.vtext());\n\t\t\tif (const auto media = answer.vmedia()) {\n\t\t\t\tProcessPollMedia(_owner, *media);\n\t\t\t\tresult.media = PollMediaFromMTP(_owner, *media);\n\t\t\t}\n\t\t\tif (const auto addedBy = answer.vadded_by()) {\n\t\t\t\tresult.addedBy = _owner->peer(peerFromMTP(*addedBy));\n\t\t\t\tresult.addedDate = answer.vdate().value_or_empty();\n\t\t\t}\n\t\t\treturn result;\n\t\t}, [&](const MTPDinputPollAnswer &answer) {\n\t\t\tauto result = PollAnswer();\n\t\t\tresult.text = Api::ParseTextWithEntities(\n\t\t\t\t&session(),\n\t\t\t\tanswer.vtext());\n\t\t\tif (const auto media = answer.vmedia()) {\n\t\t\t\tresult.media = PollMediaFromInputMTP(_owner, *media);\n\t\t\t}\n\t\t\treturn result;\n\t\t}, [](const auto &) {\n\t\t\treturn PollAnswer();\n\t\t});\n\t}) | ranges::views::take(\n\t\tkMaxOptions\n\t) | ranges::to_vector;\n\n\tconst auto changed1 = (question != newQuestion)\n\t\t|| (closeDate != newCloseDate)\n\t\t|| (closePeriod != newClosePeriod)\n\t\t|| (_flags != newFlags);\n\tconst auto changed2 = (answers != newAnswers);\n\tif (!changed1 && !changed2) {\n\t\treturn false;\n\t}\n\tif (changed1) {\n\t\tquestion = newQuestion;\n\t\tcloseDate = newCloseDate;\n\t\tclosePeriod = newClosePeriod;\n\t\t_flags = newFlags;\n\t}\n\tif (changed2) {\n\t\tstd::swap(answers, newAnswers);\n\t\tfor (const auto &old : newAnswers) {\n\t\t\tif (const auto current = answerByOption(old.option)) {\n\t\t\t\tcurrent->votes = old.votes;\n\t\t\t\tcurrent->chosen = old.chosen;\n\t\t\t\tcurrent->correct = old.correct;\n\t\t\t}\n\t\t}\n\t}\n\thash = poll.vhash().v;\n\t++version;\n\treturn true;\n}\n\nbool PollData::applyResults(const MTPPollResults &results) {\n\treturn results.match([&](const MTPDpollResults &results) {\n\t\t_lastResultsUpdate = crl::now();\n\n\t\tconst auto newTotalVoters\n\t\t\t= results.vtotal_voters().value_or(totalVoters);\n\t\tauto changed = (newTotalVoters != totalVoters);\n\t\tif (const auto list = results.vresults()) {\n\t\t\tfor (const auto &result : list->v) {\n\t\t\t\tif (applyResultToAnswers(result, results.is_min())) {\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (!results.is_min()\n\t\t\t&& newTotalVoters == 0\n\t\t\t&& voted()) {\n\t\t\tfor (auto &answer : answers) {\n\t\t\t\tif (answer.chosen) {\n\t\t\t\t\tanswer.chosen = false;\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (const auto recent = results.vrecent_voters()) {\n\t\t\tconst auto recentChanged = !ranges::equal(\n\t\t\t\trecentVoters,\n\t\t\t\trecent->v,\n\t\t\t\tranges::equal_to(),\n\t\t\t\t&PeerData::id,\n\t\t\t\tpeerFromMTP);\n\t\t\tif (recentChanged) {\n\t\t\t\tchanged = true;\n\t\t\t\trecentVoters = ranges::views::all(\n\t\t\t\t\trecent->v\n\t\t\t\t) | ranges::views::transform([&](MTPPeer peerId) {\n\t\t\t\t\tconst auto peer = _owner->peer(peerFromMTP(peerId));\n\t\t\t\t\treturn peer->isMinimalLoaded() ? peer.get() : nullptr;\n\t\t\t\t}) | ranges::views::filter([](PeerData *peer) {\n\t\t\t\t\treturn peer != nullptr;\n\t\t\t\t}) | ranges::views::transform([](PeerData *peer) {\n\t\t\t\t\treturn not_null(peer);\n\t\t\t\t}) | ranges::to_vector;\n\t\t\t}\n\t\t}\n\t\tif (results.vsolution()) {\n\t\t\tauto newSolution = TextWithEntities{\n\t\t\t\tresults.vsolution().value_or_empty(),\n\t\t\t\tApi::EntitiesFromMTP(\n\t\t\t\t\t&_owner->session(),\n\t\t\t\t\tresults.vsolution_entities().value_or_empty())\n\t\t\t};\n\t\t\tif (solution != newSolution) {\n\t\t\t\tsolution = std::move(newSolution);\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t}\n\t\tif (const auto media = results.vsolution_media()) {\n\t\t\tProcessPollMedia(_owner, *media);\n\t\t\tconst auto parsed = PollMediaFromMTP(_owner, *media);\n\t\t\tif (solutionMedia != parsed) {\n\t\t\t\tsolutionMedia = parsed;\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t}\n\t\tif (!changed) {\n\t\t\treturn false;\n\t\t}\n\t\ttotalVoters = newTotalVoters;\n\t\t++version;\n\t\treturn changed;\n\t});\n}\n\nbool PollData::checkResultsReload(crl::time now) {\n\tif (_lastResultsUpdate > 0\n\t\t&& _lastResultsUpdate + kShortPollTimeout > now) {\n\t\treturn false;\n\t} else if (closed() && _lastResultsUpdate >= 0) {\n\t\treturn false;\n\t}\n\t_lastResultsUpdate = now;\n\treturn true;\n}\n\nPollAnswer *PollData::answerByOption(const QByteArray &option) {\n\treturn AnswerByOption(answers, option);\n}\n\nconst PollAnswer *PollData::answerByOption(const QByteArray &option) const {\n\treturn AnswerByOption(answers, option);\n}\n\nbool PollData::applyResultToAnswers(\n\t\tconst MTPPollAnswerVoters &result,\n\t\tbool isMinResults) {\n\treturn result.match([&](const MTPDpollAnswerVoters &voters) {\n\t\tconst auto &option = voters.voption().v;\n\t\tconst auto answer = answerByOption(option);\n\t\tif (!answer) {\n\t\t\treturn false;\n\t\t}\n\t\tauto changed = false;\n\t\tif (const auto count = voters.vvoters()) {\n\t\t\tif (answer->votes != count->v) {\n\t\t\t\tanswer->votes = count->v;\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t}\n\t\tif (!isMinResults) {\n\t\t\tif (answer->chosen != voters.is_chosen()) {\n\t\t\t\tanswer->chosen = voters.is_chosen();\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t}\n\t\tif (voters.is_correct() && !answer->correct) {\n\t\t\tanswer->correct = voters.is_correct();\n\t\t\tchanged = true;\n\t\t}\n\t\tif (const auto recent = voters.vrecent_voters()) {\n\t\t\tconst auto recentChanged = !ranges::equal(\n\t\t\t\tanswer->recentVoters,\n\t\t\t\trecent->v,\n\t\t\t\tranges::equal_to(),\n\t\t\t\t&PeerData::id,\n\t\t\t\tpeerFromMTP);\n\t\t\tif (recentChanged) {\n\t\t\t\tchanged = true;\n\t\t\t\tanswer->recentVoters = ranges::views::all(\n\t\t\t\t\trecent->v\n\t\t\t\t) | ranges::views::transform([&](MTPPeer peerId) {\n\t\t\t\t\tconst auto peer = _owner->peer(\n\t\t\t\t\t\tpeerFromMTP(peerId));\n\t\t\t\t\treturn peer->isMinimalLoaded()\n\t\t\t\t\t\t? peer.get()\n\t\t\t\t\t\t: nullptr;\n\t\t\t\t}) | ranges::views::filter([](PeerData *peer) {\n\t\t\t\t\treturn peer != nullptr;\n\t\t\t\t}) | ranges::views::transform([](PeerData *peer) {\n\t\t\t\t\treturn not_null(peer);\n\t\t\t\t}) | ranges::to_vector;\n\t\t\t}\n\t\t}\n\t\treturn changed;\n\t});\n}\n\nvoid PollData::setFlags(Flags flags) {\n\tif (_flags != flags) {\n\t\t_flags = flags;\n\t\t++version;\n\t}\n}\n\nPollData::Flags PollData::flags() const {\n\treturn _flags;\n}\n\nbool PollData::voted() const {\n\treturn ranges::contains(answers, true, &PollAnswer::chosen);\n}\n\nbool PollData::closed() const {\n\treturn (_flags & Flag::Closed);\n}\n\nbool PollData::publicVotes() const {\n\treturn (_flags & Flag::PublicVotes);\n}\n\nbool PollData::multiChoice() const {\n\treturn (_flags & Flag::MultiChoice);\n}\n\nbool PollData::quiz() const {\n\treturn (_flags & Flag::Quiz);\n}\n\nbool PollData::shuffleAnswers() const {\n\treturn (_flags & Flag::ShuffleAnswers);\n}\n\nbool PollData::revotingDisabled() const {\n\treturn (_flags & Flag::RevotingDisabled);\n}\n\nbool PollData::openAnswers() const {\n\treturn (_flags & Flag::OpenAnswers);\n}\n\nbool PollData::hideResultsUntilClose() const {\n\treturn (_flags & Flag::HideResultsUntilClose);\n}\n\nbool PollData::creator() const {\n\treturn (_flags & Flag::Creator);\n}\n\nQString PollData::debugString() const {\n\tauto result = QString();\n\tresult += u\"Poll #\"_q + QString::number(id) + u'\\n';\n\tresult += u\"Q: \"_q + question.text + u'\\n';\n\tif (quiz()) {\n\t\tresult += u\"[Quiz]\"_q;\n\t}\n\tif (multiChoice()) {\n\t\tresult += u\"[MultiChoice]\"_q;\n\t}\n\tif (closed()) {\n\t\tresult += u\"[Closed]\"_q;\n\t}\n\tif (publicVotes()) {\n\t\tresult += u\"[PublicVotes]\"_q;\n\t}\n\tif (!result.endsWith(u'\\n')) {\n\t\tresult += u'\\n';\n\t}\n\tresult += u\"Total voters: \"_q + QString::number(totalVoters) + u'\\n';\n\tfor (const auto &answer : answers) {\n\t\tresult += u\"  - \"_q + answer.text.text\n\t\t\t+ u\" [\"_q + QString::number(answer.votes) + u\" votes\"_q;\n\t\tif (answer.chosen) {\n\t\t\tresult += u\", chosen\"_q;\n\t\t}\n\t\tif (answer.correct) {\n\t\t\tresult += u\", correct\"_q;\n\t\t}\n\t\tresult += u\"]\\n\"_q;\n\t}\n\tif (!solution.text.isEmpty()) {\n\t\tresult += u\"Solution: \"_q + solution.text + u'\\n';\n\t}\n\treturn result;\n}\n\nMTPInputMedia PollMediaToMTP(const PollMedia &media) {\n\tif (media.photo) {\n\t\treturn MTP_inputMediaPhoto(\n\t\t\tMTP_flags(MTPDinputMediaPhoto::Flag(0)),\n\t\t\tmedia.photo->mtpInput(),\n\t\t\tMTP_int(0),\n\t\t\tMTPInputDocument());\n\t} else if (media.document) {\n\t\treturn MTP_inputMediaDocument(\n\t\t\tMTP_flags(MTPDinputMediaDocument::Flag(0)),\n\t\t\tmedia.document->mtpInput(),\n\t\t\tMTPInputPhoto(),\n\t\t\tMTP_int(0),\n\t\t\tMTP_int(0),\n\t\t\tMTPstring());\n\t} else if (media.geo) {\n\t\treturn MTP_inputMediaGeoPoint(\n\t\t\tMTP_inputGeoPoint(\n\t\t\t\tMTP_flags(0),\n\t\t\t\tMTP_double(media.geo->lat()),\n\t\t\t\tMTP_double(media.geo->lon()),\n\t\t\t\tMTPint())); // accuracy_radius\n\t}\n\treturn MTPInputMedia();\n}\n\nPollMedia PollMediaFromMTP(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst MTPMessageMedia &media) {\n\tauto result = PollMedia();\n\tmedia.match([&](const MTPDmessageMediaPhoto &data) {\n\t\tif (const auto photo = data.vphoto()) {\n\t\t\tphoto->match([&](const MTPDphoto &) {\n\t\t\t\tresult.photo = owner->processPhoto(*photo);\n\t\t\t}, [](const auto &) {\n\t\t\t});\n\t\t}\n\t}, [&](const MTPDmessageMediaDocument &data) {\n\t\tif (const auto document = data.vdocument()) {\n\t\t\tdocument->match([&](const MTPDdocument &) {\n\t\t\t\tresult.document = owner->processDocument(*document);\n\t\t\t}, [](const auto &) {\n\t\t\t});\n\t\t}\n\t}, [&](const MTPDmessageMediaGeo &data) {\n\t\tdata.vgeo().match([&](const MTPDgeoPoint &point) {\n\t\t\tresult.geo = Data::LocationPoint(point);\n\t\t}, [](const MTPDgeoPointEmpty &) {\n\t\t});\n\t}, [&](const MTPDmessageMediaVenue &data) {\n\t\tdata.vgeo().match([&](const MTPDgeoPoint &point) {\n\t\t\tresult.geo = Data::LocationPoint(point);\n\t\t}, [](const MTPDgeoPointEmpty &) {\n\t\t});\n\t}, [](const auto &) {\n\t});\n\treturn result;\n}\n\nPollMedia PollMediaFromInputMTP(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst MTPInputMedia &media) {\n\tauto result = PollMedia();\n\tmedia.match([&](const MTPDinputMediaPhoto &data) {\n\t\tdata.vid().match([&](const MTPDinputPhoto &photo) {\n\t\t\tresult.photo = owner->photo(photo.vid().v);\n\t\t}, [](const auto &) {\n\t\t});\n\t}, [&](const MTPDinputMediaDocument &data) {\n\t\tdata.vid().match([&](const MTPDinputDocument &document) {\n\t\t\tresult.document = owner->document(document.vid().v);\n\t\t}, [](const auto &) {\n\t\t});\n\t}, [&](const MTPDinputMediaGeoPoint &data) {\n\t\tdata.vgeo_point().match([&](const MTPDinputGeoPoint &point) {\n\t\t\tresult.geo.emplace(\n\t\t\t\tpoint.vlat().v,\n\t\t\t\tpoint.vlong().v,\n\t\t\t\tData::LocationPoint::NoAccessHash);\n\t\t}, [](const auto &) {\n\t\t});\n\t}, [](const auto &) {\n\t});\n\treturn result;\n}\n\nQByteArray PollOptionFromLink(const QString &value) {\n\treturn QByteArray::fromBase64(value.toLatin1());\n}\n\nQString PollOptionToLink(const QByteArray &option) {\n\treturn QString::fromLatin1(\n\t\toption.toBase64(QByteArray::OmitTrailingEquals));\n}\n\nMTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) {\n\tconst auto convert = [&](const PollAnswer &answer) {\n\t\tconst auto flags = answer.media\n\t\t\t? MTPDinputPollAnswer::Flag::f_media\n\t\t\t: MTPDinputPollAnswer::Flag(0);\n\t\treturn MTP_inputPollAnswer(\n\t\t\tMTP_flags(flags),\n\t\t\tMTP_textWithEntities(\n\t\t\t\tMTP_string(answer.text.text),\n\t\t\t\tApi::EntitiesToMTP(&poll->session(), answer.text.entities)),\n\t\t\tanswer.media\n\t\t\t\t? PollMediaToMTP(answer.media)\n\t\t\t\t: MTPInputMedia());\n\t};\n\tauto answers = QVector<MTPPollAnswer>();\n\tanswers.reserve(poll->answers.size());\n\tranges::transform(\n\t\tpoll->answers,\n\t\tranges::back_inserter(answers),\n\t\tconvert);\n\tusing Flag = MTPDpoll::Flag;\n\tconst auto flags = ((poll->closed() || close) ? Flag::f_closed : Flag(0))\n\t\t| (poll->multiChoice() ? Flag::f_multiple_choice : Flag(0))\n\t\t| (poll->publicVotes() ? Flag::f_public_voters : Flag(0))\n\t\t| (poll->quiz() ? Flag::f_quiz : Flag(0))\n\t\t| (poll->shuffleAnswers() ? Flag::f_shuffle_answers : Flag(0))\n\t\t| (poll->revotingDisabled() ? Flag::f_revoting_disabled : Flag(0))\n\t\t| (poll->openAnswers() ? Flag::f_open_answers : Flag(0))\n\t\t| (poll->hideResultsUntilClose()\n\t\t\t? Flag::f_hide_results_until_close\n\t\t\t: Flag(0))\n\t\t| (poll->closePeriod > 0 ? Flag::f_close_period : Flag(0))\n\t\t| (poll->closeDate > 0 ? Flag::f_close_date : Flag(0));\n\treturn MTP_poll(\n\t\tMTP_long(poll->id),\n\t\tMTP_flags(flags),\n\t\tMTP_textWithEntities(\n\t\t\tMTP_string(poll->question.text),\n\t\t\tApi::EntitiesToMTP(&poll->session(), poll->question.entities)),\n\t\tMTP_vector<MTPPollAnswer>(answers),\n\t\tMTP_int(poll->closePeriod),\n\t\tMTP_int(poll->closeDate),\n\t\tMTP_long(0));\n}\n\nMTPInputMedia PollDataToInputMedia(\n\t\tnot_null<const PollData*> poll,\n\t\tbool close) {\n\tauto inputFlags = MTPDinputMediaPoll::Flag(0)\n\t\t| (poll->quiz()\n\t\t\t? MTPDinputMediaPoll::Flag::f_correct_answers\n\t\t\t: MTPDinputMediaPoll::Flag(0));\n\tauto correct = QVector<MTPint>();\n\tfor (auto i = 0, count = int(poll->answers.size()); i < count; ++i) {\n\t\tif (poll->answers[i].correct) {\n\t\t\tcorrect.push_back(MTP_int(i));\n\t\t}\n\t}\n\n\tauto solution = poll->solution;\n\tconst auto prepareFlags = Ui::ItemTextDefaultOptions().flags;\n\tTextUtilities::PrepareForSending(solution, prepareFlags);\n\tTextUtilities::Trim(solution);\n\tconst auto sentEntities = Api::EntitiesToMTP(\n\t\t&poll->session(),\n\t\tsolution.entities,\n\t\tApi::ConvertOption::SkipLocal);\n\tif (!solution.text.isEmpty()) {\n\t\tinputFlags |= MTPDinputMediaPoll::Flag::f_solution;\n\t}\n\tif (!sentEntities.v.isEmpty()) {\n\t\tinputFlags |= MTPDinputMediaPoll::Flag::f_solution_entities;\n\t}\n\tif (poll->attachedMedia) {\n\t\tinputFlags |= MTPDinputMediaPoll::Flag::f_attached_media;\n\t}\n\tif (poll->solutionMedia) {\n\t\tinputFlags |= MTPDinputMediaPoll::Flag::f_solution_media;\n\t}\n\treturn MTP_inputMediaPoll(\n\t\tMTP_flags(inputFlags),\n\t\tPollDataToMTP(poll, close),\n\t\tMTP_vector<MTPint>(correct),\n\t\tpoll->attachedMedia\n\t\t\t? PollMediaToMTP(poll->attachedMedia)\n\t\t\t: MTPInputMedia(),\n\t\tMTP_string(solution.text),\n\t\tsentEntities,\n\t\tpoll->solutionMedia\n\t\t\t? PollMediaToMTP(poll->solutionMedia)\n\t\t\t: MTPInputMedia());\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_poll.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_location.h\"\n\nnamespace Data {\nclass Session;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nclass PeerData;\nclass PhotoData;\nclass DocumentData;\n\nstruct PollMedia {\n\tPhotoData *photo = nullptr;\n\tDocumentData *document = nullptr;\n\tstd::optional<Data::LocationPoint> geo;\n\n\texplicit operator bool() const { return photo || document || geo; }\n};\n\ninline bool operator==(const PollMedia &a, const PollMedia &b) {\n\treturn (a.photo == b.photo)\n\t\t&& (a.document == b.document)\n\t\t&& (a.geo == b.geo);\n}\n\ninline bool operator!=(const PollMedia &a, const PollMedia &b) {\n\treturn !(a == b);\n}\n\nstruct PollAnswer {\n\tTextWithEntities text;\n\tQByteArray option;\n\tPollMedia media;\n\tstd::vector<not_null<PeerData*>> recentVoters;\n\tPeerData *addedBy = nullptr;\n\tTimeId addedDate = 0;\n\tint votes = 0;\n\tbool chosen = false;\n\tbool correct = false;\n};\n\ninline bool operator==(const PollAnswer &a, const PollAnswer &b) {\n\treturn (a.text == b.text)\n\t\t&& (a.option == b.option);\n}\n\ninline bool operator!=(const PollAnswer &a, const PollAnswer &b) {\n\treturn !(a == b);\n}\n\nstruct PollData {\n\tPollData(not_null<Data::Session*> owner, PollId id);\n\n\t[[nodiscard]] Data::Session &owner() const;\n\t[[nodiscard]] Main::Session &session() const;\n\n\tenum class Flag {\n\t\tClosed                = 0x001,\n\t\tPublicVotes           = 0x002,\n\t\tMultiChoice           = 0x004,\n\t\tQuiz                  = 0x008,\n\t\tShuffleAnswers        = 0x010,\n\t\tRevotingDisabled      = 0x020,\n\t\tOpenAnswers           = 0x040,\n\t\tHideResultsUntilClose = 0x080,\n\t\tCreator               = 0x100,\n\t};\n\tfriend inline constexpr bool is_flag_type(Flag) { return true; };\n\tusing Flags = base::flags<Flag>;\n\n\tbool closeByTimer();\n\tbool applyChanges(const MTPDpoll &poll);\n\tbool applyResults(const MTPPollResults &results);\n\t[[nodiscard]] bool checkResultsReload(crl::time now);\n\n\t[[nodiscard]] PollAnswer *answerByOption(const QByteArray &option);\n\t[[nodiscard]] const PollAnswer *answerByOption(\n\t\tconst QByteArray &option) const;\n\n\tvoid setFlags(Flags flags);\n\t[[nodiscard]] Flags flags() const;\n\t[[nodiscard]] bool voted() const;\n\t[[nodiscard]] bool closed() const;\n\t[[nodiscard]] bool publicVotes() const;\n\t[[nodiscard]] bool multiChoice() const;\n\t[[nodiscard]] bool quiz() const;\n\t[[nodiscard]] bool shuffleAnswers() const;\n\t[[nodiscard]] bool revotingDisabled() const;\n\t[[nodiscard]] bool openAnswers() const;\n\t[[nodiscard]] bool hideResultsUntilClose() const;\n\t[[nodiscard]] bool creator() const;\n\n\t[[nodiscard]] QString debugString() const;\n\n\tPollId id = 0;\n\tTextWithEntities question;\n\tstd::vector<PollAnswer> answers;\n\tstd::vector<not_null<PeerData*>> recentVoters;\n\tstd::vector<QByteArray> sendingVotes;\n\tTextWithEntities solution;\n\tPollMedia attachedMedia;\n\tPollMedia solutionMedia;\n\tTimeId closePeriod = 0;\n\tTimeId closeDate = 0;\n\tint totalVoters = 0;\n\tint version = 0;\n\tuint64 hash = 0;\n\n\tstatic constexpr auto kMaxOptions = 32;\n\nprivate:\n\tbool applyResultToAnswers(\n\t\tconst MTPPollAnswerVoters &result,\n\t\tbool isMinResults);\n\n\tconst not_null<Data::Session*> _owner;\n\tFlags _flags = Flags();\n\tcrl::time _lastResultsUpdate = 0; // < 0 means force reload.\n\n};\n\ninline constexpr auto kDefaultPollCreateFlags = PollData::Flag::PublicVotes\n\t| PollData::Flag::MultiChoice\n\t| PollData::Flag::OpenAnswers\n\t| PollData::Flag::ShuffleAnswers;\n\n[[nodiscard]] QByteArray PollOptionFromLink(const QString &value);\n[[nodiscard]] QString PollOptionToLink(const QByteArray &option);\n\n[[nodiscard]] MTPPoll PollDataToMTP(\n\tnot_null<const PollData*> poll,\n\tbool close = false);\n[[nodiscard]] MTPInputMedia PollDataToInputMedia(\n\tnot_null<const PollData*> poll,\n\tbool close = false);\n[[nodiscard]] MTPInputMedia PollMediaToMTP(const PollMedia &media);\n[[nodiscard]] PollMedia PollMediaFromMTP(\n\tnot_null<Data::Session*> owner,\n\tconst MTPMessageMedia &media);\n[[nodiscard]] PollMedia PollMediaFromInputMTP(\n\tnot_null<Data::Session*> owner,\n\tconst MTPInputMedia &media);\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_poll_messages.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_poll_messages.h\"\n\n#include \"data/data_messages.h\"\n#include \"data/data_shared_media.h\"\n#include \"data/data_sparse_ids.h\"\n#include \"data/data_chat.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n#include \"storage/storage_shared_media.h\"\n\nnamespace Data {\n\nrpl::producer<MessagesSlice> PollMessagesViewer(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<History*> history,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tMessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tconst auto peerId = history->peer->id;\n\tconst auto migrateFrom = history->peer->migrateFrom();\n\tconst auto migratedPeerId = migrateFrom ? migrateFrom->id : PeerId(0);\n\tconst auto messageId = ((aroundId.fullId.msg == ShowAtTheEndMsgId)\n\t\t\t|| (aroundId == MaxMessagePosition))\n\t\t? (ServerMaxMsgId - 1)\n\t\t: (aroundId.fullId.peer == peerId)\n\t\t? aroundId.fullId.msg\n\t\t: (aroundId.fullId.msg\n\t\t\t? (aroundId.fullId.msg - ServerMaxMsgId)\n\t\t\t: (ServerMaxMsgId - 1));\n\tconst auto key = SharedMediaMergedKey(\n\t\tSparseIdsMergedSlice::Key(\n\t\t\tpeerId,\n\t\t\ttopicRootId,\n\t\t\tmonoforumPeerId,\n\t\t\tmigratedPeerId,\n\t\t\tmessageId),\n\t\tStorage::SharedMediaType::Poll);\n\treturn SharedMediaMergedViewer(\n\t\tsession,\n\t\tkey,\n\t\tlimitBefore,\n\t\tlimitAfter\n\t) | rpl::map([=](SparseIdsMergedSlice &&slice) {\n\t\tauto result = MessagesSlice();\n\t\tresult.fullCount = slice.fullCount();\n\t\tresult.skippedAfter = slice.skippedAfter();\n\t\tresult.skippedBefore = slice.skippedBefore();\n\t\tconst auto count = slice.size();\n\t\tresult.ids.reserve(count);\n\t\tif (const auto msgId = slice.nearest(messageId)) {\n\t\t\tresult.nearestToAround = *msgId;\n\t\t}\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tresult.ids.push_back(slice[i]);\n\t\t}\n\t\treturn result;\n\t});\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_poll_messages.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass History;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nstruct MessagesSlice;\nstruct MessagePosition;\n\n[[nodiscard]] rpl::producer<MessagesSlice> PollMessagesViewer(\n\tnot_null<Main::Session*> session,\n\tnot_null<History*> history,\n\tMsgId topicRootId,\n\tPeerId monoforumPeerId,\n\tMessagePosition aroundId,\n\tint limitBefore,\n\tint limitAfter);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_premium_limits.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_premium_limits.h\"\n\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n\nnamespace Data {\n\nPremiumLimits::PremiumLimits(not_null<Main::Session*> session)\n: _session(session) {\n}\n\nint PremiumLimits::channelsDefault() const {\n\treturn appConfigLimit(\"channels_limit_default\", 500);\n}\nint PremiumLimits::channelsPremium() const {\n\treturn appConfigLimit(\"channels_limit_premium\", 1000);\n}\nint PremiumLimits::channelsCurrent() const {\n\treturn isPremium()\n\t\t? channelsPremium()\n\t\t: channelsDefault();\n}\n\nint PremiumLimits::similarChannelsDefault() const {\n\treturn appConfigLimit(\"recommended_channels_limit_default\", 10);\n}\nint PremiumLimits::similarChannelsPremium() const {\n\treturn appConfigLimit(\"recommended_channels_limit_premium\", 100);\n}\nint PremiumLimits::similarChannelsCurrent() const {\n\treturn isPremium()\n\t\t? channelsPremium()\n\t\t: channelsDefault();\n}\n\nint PremiumLimits::gifsDefault() const {\n\treturn appConfigLimit(\"saved_gifs_limit_default\", 200);\n}\nint PremiumLimits::gifsPremium() const {\n\treturn appConfigLimit(\"saved_gifs_limit_premium\", 400);\n}\nint PremiumLimits::gifsCurrent() const {\n\treturn isPremium()\n\t\t? gifsPremium()\n\t\t: gifsDefault();\n}\n\nint PremiumLimits::stickersFavedDefault() const {\n\treturn appConfigLimit(\"stickers_faved_limit_default\", 5);\n}\nint PremiumLimits::stickersFavedPremium() const {\n\treturn appConfigLimit(\"stickers_faved_limit_premium\", 10);\n}\nint PremiumLimits::stickersFavedCurrent() const {\n\treturn isPremium()\n\t\t? stickersFavedPremium()\n\t\t: stickersFavedDefault();\n}\n\nint PremiumLimits::dialogFiltersDefault() const {\n\treturn appConfigLimit(\"dialog_filters_limit_default\", 10);\n}\nint PremiumLimits::dialogFiltersPremium() const {\n\treturn appConfigLimit(\"dialog_filters_limit_premium\", 20);\n}\nint PremiumLimits::dialogFiltersCurrent() const {\n\treturn isPremium()\n\t\t? dialogFiltersPremium()\n\t\t: dialogFiltersDefault();\n}\n\nint PremiumLimits::dialogShareableFiltersDefault() const {\n\treturn appConfigLimit(\"chatlists_joined_limit_default\", 2);\n}\nint PremiumLimits::dialogShareableFiltersPremium() const {\n\treturn appConfigLimit(\"chatlists_joined_limit_premium\", 20);\n}\nint PremiumLimits::dialogShareableFiltersCurrent() const {\n\treturn isPremium()\n\t\t? dialogShareableFiltersPremium()\n\t\t: dialogShareableFiltersDefault();\n}\n\nint PremiumLimits::dialogFiltersChatsDefault() const {\n\treturn appConfigLimit(\"dialog_filters_chats_limit_default\", 100);\n}\nint PremiumLimits::dialogFiltersChatsPremium() const {\n\treturn appConfigLimit(\"dialog_filters_chats_limit_premium\", 200);\n}\nint PremiumLimits::dialogFiltersChatsCurrent() const {\n\treturn isPremium()\n\t\t? dialogFiltersChatsPremium()\n\t\t: dialogFiltersChatsDefault();\n}\n\nint PremiumLimits::dialogFiltersLinksDefault() const {\n\treturn appConfigLimit(\"chatlist_invites_limit_default\", 3);\n}\nint PremiumLimits::dialogFiltersLinksPremium() const {\n\treturn appConfigLimit(\"chatlist_invites_limit_premium\", 20);\n}\nint PremiumLimits::dialogFiltersLinksCurrent() const {\n\treturn isPremium()\n\t\t? dialogFiltersLinksPremium()\n\t\t: dialogFiltersLinksDefault();\n}\n\nint PremiumLimits::dialogsPinnedDefault() const {\n\treturn appConfigLimit(\"dialogs_pinned_limit_default\", 5);\n}\nint PremiumLimits::dialogsPinnedPremium() const {\n\treturn appConfigLimit(\"dialogs_pinned_limit_premium\", 10);\n}\nint PremiumLimits::dialogsPinnedCurrent() const {\n\treturn isPremium()\n\t\t? dialogsPinnedPremium()\n\t\t: dialogsPinnedDefault();\n}\n\nint PremiumLimits::dialogsFolderPinnedDefault() const {\n\treturn appConfigLimit(\"dialogs_folder_pinned_limit_default\", 100);\n}\nint PremiumLimits::dialogsFolderPinnedPremium() const {\n\treturn appConfigLimit(\"dialogs_folder_pinned_limit_premium\", 200);\n}\nint PremiumLimits::dialogsFolderPinnedCurrent() const {\n\treturn isPremium()\n\t\t? dialogsFolderPinnedPremium()\n\t\t: dialogsFolderPinnedDefault();\n}\n\nint PremiumLimits::topicsPinnedCurrent() const {\n\treturn appConfigLimit(\"topics_pinned_limit\", 5);\n}\n\nint PremiumLimits::savedSublistsPinnedDefault() const {\n\treturn appConfigLimit(\"saved_dialogs_pinned_limit_default\", 5);\n}\nint PremiumLimits::savedSublistsPinnedPremium() const {\n\treturn appConfigLimit(\"saved_dialogs_pinned_limit_premium\", 100);\n}\nint PremiumLimits::savedSublistsPinnedCurrent() const {\n\treturn isPremium()\n\t\t? savedSublistsPinnedPremium()\n\t\t: savedSublistsPinnedDefault();\n}\n\nint PremiumLimits::channelsPublicDefault() const {\n\treturn appConfigLimit(\"channels_public_limit_default\", 10);\n}\nint PremiumLimits::channelsPublicPremium() const {\n\treturn appConfigLimit(\"channels_public_limit_premium\", 20);\n}\nint PremiumLimits::channelsPublicCurrent() const {\n\treturn isPremium()\n\t\t? channelsPublicPremium()\n\t\t: channelsPublicDefault();\n}\n\nint PremiumLimits::captionLengthDefault() const {\n\treturn appConfigLimit(\"caption_length_limit_default\", 1024);\n}\nint PremiumLimits::captionLengthPremium() const {\n\treturn appConfigLimit(\"caption_length_limit_premium\", 2048);\n}\nint PremiumLimits::captionLengthCurrent() const {\n\treturn isPremium()\n\t\t? captionLengthPremium()\n\t\t: captionLengthDefault();\n}\n\nint PremiumLimits::uploadMaxDefault() const {\n\treturn appConfigLimit(\"upload_max_fileparts_default\", 4000);\n}\nint PremiumLimits::uploadMaxPremium() const {\n\treturn appConfigLimit(\"upload_max_fileparts_premium\", 8000);\n}\nint PremiumLimits::uploadMaxCurrent() const {\n\treturn isPremium()\n\t\t? uploadMaxPremium()\n\t\t: uploadMaxDefault();\n}\n\nint PremiumLimits::aboutLengthDefault() const {\n\treturn appConfigLimit(\"about_length_limit_default\", 70);\n}\nint PremiumLimits::aboutLengthPremium() const {\n\treturn appConfigLimit(\"about_length_limit_premium\", 140);\n}\nint PremiumLimits::aboutLengthCurrent() const {\n\treturn isPremium()\n\t\t? aboutLengthPremium()\n\t\t: aboutLengthDefault();\n}\n\nint PremiumLimits::contactNoteLengthCurrent() const {\n\treturn appConfigLimit(\"contact_note_length_limit\", 128);\n}\n\nint PremiumLimits::maxBoostLevel() const {\n\treturn appConfigLimit(\n\t\tu\"boosts_channel_level_max\"_q,\n\t\t_session->isTestMode() ? 9 : 99);\n}\n\nint PremiumLimits::botsCreateDefault() const {\n\treturn appConfigLimit(\"bots_create_limit_default\", 20);\n}\nint PremiumLimits::botsCreatePremium() const {\n\treturn appConfigLimit(\"bots_create_limit_premium\", 40);\n}\n\nint PremiumLimits::appConfigLimit(\n\t\tconst QString &key,\n\t\tint fallback) const {\n\treturn _session->appConfig().get<int>(key, fallback);\n}\n\nbool PremiumLimits::isPremium() const {\n\treturn _session->premium();\n}\n\nLevelLimits::LevelLimits(not_null<Main::Session*> session)\n: _session(session) {\n}\n\nint LevelLimits::channelColorLevelMin() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"channel_color_level_min\"_q,\n\t\t5);\n}\n\nint LevelLimits::channelBgIconLevelMin() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"channel_bg_icon_level_min\"_q,\n\t\t4);\n}\n\nint LevelLimits::channelProfileBgIconLevelMin() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"channel_profile_bg_icon_level_min\"_q,\n\t\t7);\n}\n\nint LevelLimits::channelEmojiStatusLevelMin() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"channel_emoji_status_level_min\"_q,\n\t\t8);\n}\n\nint LevelLimits::channelWallpaperLevelMin() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"channel_wallpaper_level_min\"_q,\n\t\t9);\n}\n\nint LevelLimits::channelCustomWallpaperLevelMin() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"channel_custom_wallpaper_level_min\"_q,\n\t\t10);\n}\n\nint LevelLimits::channelRestrictSponsoredLevelMin() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"channel_restrict_sponsored_level_min\"_q,\n\t\t20);\n}\n\nint LevelLimits::channelAutoTranslateLevelMin() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"channel_autotranslation_level_min\"_q,\n\t\t3);\n}\n\nint LevelLimits::groupTranscribeLevelMin() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"group_transcribe_level_min\"_q,\n\t\t6);\n}\n\nint LevelLimits::groupEmojiStickersLevelMin() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"group_emoji_stickers_level_min\"_q,\n\t\t4);\n}\n\nint LevelLimits::groupProfileBgIconLevelMin() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"group_profile_bg_icon_level_min\"_q,\n\t\t5);\n}\n\nint LevelLimits::groupEmojiStatusLevelMin() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"group_emoji_status_level_min\"_q,\n\t\t8);\n}\n\nint LevelLimits::groupWallpaperLevelMin() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"group_wallpaper_level_min\"_q,\n\t\t9);\n}\n\nint LevelLimits::groupCustomWallpaperLevelMin() const {\n\treturn _session->appConfig().get<int>(\n\t\tu\"group_custom_wallpaper_level_min\"_q,\n\t\t10);\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_premium_limits.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nclass PremiumLimits final {\npublic:\n\tPremiumLimits(not_null<Main::Session*> session);\n\n\t[[nodiscard]] int channelsDefault() const;\n\t[[nodiscard]] int channelsPremium() const;\n\t[[nodiscard]] int channelsCurrent() const;\n\n\t[[nodiscard]] int similarChannelsDefault() const;\n\t[[nodiscard]] int similarChannelsPremium() const;\n\t[[nodiscard]] int similarChannelsCurrent() const;\n\n\t[[nodiscard]] int gifsDefault() const;\n\t[[nodiscard]] int gifsPremium() const;\n\t[[nodiscard]] int gifsCurrent() const;\n\n\t[[nodiscard]] int stickersFavedDefault() const;\n\t[[nodiscard]] int stickersFavedPremium() const;\n\t[[nodiscard]] int stickersFavedCurrent() const;\n\n\t[[nodiscard]] int dialogFiltersDefault() const;\n\t[[nodiscard]] int dialogFiltersPremium() const;\n\t[[nodiscard]] int dialogFiltersCurrent() const;\n\n\t[[nodiscard]] int dialogShareableFiltersDefault() const;\n\t[[nodiscard]] int dialogShareableFiltersPremium() const;\n\t[[nodiscard]] int dialogShareableFiltersCurrent() const;\n\n\t[[nodiscard]] int dialogFiltersChatsDefault() const;\n\t[[nodiscard]] int dialogFiltersChatsPremium() const;\n\t[[nodiscard]] int dialogFiltersChatsCurrent() const;\n\n\t[[nodiscard]] int dialogFiltersLinksDefault() const;\n\t[[nodiscard]] int dialogFiltersLinksPremium() const;\n\t[[nodiscard]] int dialogFiltersLinksCurrent() const;\n\n\t[[nodiscard]] int dialogsPinnedDefault() const;\n\t[[nodiscard]] int dialogsPinnedPremium() const;\n\t[[nodiscard]] int dialogsPinnedCurrent() const;\n\n\t[[nodiscard]] int dialogsFolderPinnedDefault() const;\n\t[[nodiscard]] int dialogsFolderPinnedPremium() const;\n\t[[nodiscard]] int dialogsFolderPinnedCurrent() const;\n\n\t[[nodiscard]] int topicsPinnedCurrent() const;\n\n\t[[nodiscard]] int savedSublistsPinnedDefault() const;\n\t[[nodiscard]] int savedSublistsPinnedPremium() const;\n\t[[nodiscard]] int savedSublistsPinnedCurrent() const;\n\n\t[[nodiscard]] int channelsPublicDefault() const;\n\t[[nodiscard]] int channelsPublicPremium() const;\n\t[[nodiscard]] int channelsPublicCurrent() const;\n\n\t[[nodiscard]] int captionLengthDefault() const;\n\t[[nodiscard]] int captionLengthPremium() const;\n\t[[nodiscard]] int captionLengthCurrent() const;\n\n\t[[nodiscard]] int uploadMaxDefault() const;\n\t[[nodiscard]] int uploadMaxPremium() const;\n\t[[nodiscard]] int uploadMaxCurrent() const;\n\n\t[[nodiscard]] int aboutLengthDefault() const;\n\t[[nodiscard]] int aboutLengthPremium() const;\n\t[[nodiscard]] int aboutLengthCurrent() const;\n\n\t[[nodiscard]] int contactNoteLengthCurrent() const;\n\n\t[[nodiscard]] int maxBoostLevel() const;\n\n\t[[nodiscard]] int botsCreateDefault() const;\n\t[[nodiscard]] int botsCreatePremium() const;\n\nprivate:\n\t[[nodiscard]] int appConfigLimit(\n\t\tconst QString &key,\n\t\tint fallback) const;\n\t[[nodiscard]] bool isPremium() const;\n\n\tconst not_null<Main::Session*> _session;\n\n};\n\nclass LevelLimits final {\npublic:\n\tLevelLimits(not_null<Main::Session*> session);\n\n\t[[nodiscard]] int channelColorLevelMin() const;\n\t[[nodiscard]] int channelBgIconLevelMin() const;\n\t[[nodiscard]] int channelProfileBgIconLevelMin() const;\n\t[[nodiscard]] int channelEmojiStatusLevelMin() const;\n\t[[nodiscard]] int channelWallpaperLevelMin() const;\n\t[[nodiscard]] int channelCustomWallpaperLevelMin() const;\n\t[[nodiscard]] int channelRestrictSponsoredLevelMin() const;\n\t[[nodiscard]] int channelAutoTranslateLevelMin() const;\n\t[[nodiscard]] int groupTranscribeLevelMin() const;\n\t[[nodiscard]] int groupEmojiStickersLevelMin() const;\n\t[[nodiscard]] int groupProfileBgIconLevelMin() const;\n\t[[nodiscard]] int groupEmojiStatusLevelMin() const;\n\t[[nodiscard]] int groupWallpaperLevelMin() const;\n\t[[nodiscard]] int groupCustomWallpaperLevelMin() const;\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_premium_subscription_option.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\nstruct PremiumSubscriptionOption {\n\tint months = 0;\n\tQString duration;\n\tQString discount;\n\tQString costPerMonth;\n\tQString costNoDiscount;\n\tQString costPerYear;\n\tQString currency;\n\tQString total;\n\tQString botUrl;\n};\nusing PremiumSubscriptionOptions = std::vector<PremiumSubscriptionOption>;\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_pts_waiter.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_pts_waiter.h\"\n\n#include \"api/api_updates.h\"\n\nPtsWaiter::PtsWaiter(not_null<Api::Updates*> owner) : _owner(owner) {\n}\n\nuint64 PtsWaiter::ptsKey(PtsSkippedQueue queue, int32 pts) {\n\treturn _queue.emplace(\n\t\tuint64(uint32(pts)) << 32 | (++_skippedKey),\n\t\tqueue\n\t).first->first;\n}\n\nvoid PtsWaiter::setWaitingForSkipped(ChannelData *channel, crl::time ms) {\n\tif (ms >= 0) {\n\t\t_owner->ptsWaiterStartTimerFor(channel, ms);\n\t\t_waitingForSkipped = true;\n\t} else {\n\t\t_waitingForSkipped = false;\n\t\tcheckForWaiting(channel);\n\t}\n}\n\nvoid PtsWaiter::setWaitingForShortPoll(ChannelData *channel, crl::time ms) {\n\tif (ms >= 0) {\n\t\t_owner->ptsWaiterStartTimerFor(channel, ms);\n\t\t_waitingForShortPoll = true;\n\t} else {\n\t\t_waitingForShortPoll = false;\n\t\tcheckForWaiting(channel);\n\t}\n}\n\nvoid PtsWaiter::checkForWaiting(ChannelData *channel) {\n\tif (!_waitingForSkipped && !_waitingForShortPoll) {\n\t\t_owner->ptsWaiterStartTimerFor(channel, -1);\n\t}\n}\n\nvoid PtsWaiter::applySkippedUpdates(ChannelData *channel) {\n\tif (!_waitingForSkipped) {\n\t\treturn;\n\t}\n\n\tsetWaitingForSkipped(channel, -1);\n\n\tif (_queue.empty()) {\n\t\treturn;\n\t}\n\n\t++_applySkippedLevel;\n\tfor (auto i = _queue.cbegin(), e = _queue.cend(); i != e; ++i) {\n\t\tswitch (i->second) {\n\t\tcase SkippedUpdate: {\n\t\t\t_owner->applyUpdateNoPtsCheck(_updateQueue[i->first]);\n\t\t} break;\n\t\tcase SkippedUpdates: {\n\t\t\t_owner->applyUpdatesNoPtsCheck(_updatesQueue[i->first]);\n\t\t} break;\n\t\t}\n\t}\n\t--_applySkippedLevel;\n\tclearSkippedUpdates();\n}\n\nvoid PtsWaiter::clearSkippedUpdates() {\n\t_queue.clear();\n\t_updateQueue.clear();\n\t_updatesQueue.clear();\n\t_applySkippedLevel = 0;\n}\n\nbool PtsWaiter::updated(\n\t\tChannelData *channel,\n\t\tint32 pts,\n\t\tint32 count,\n\t\tconst MTPUpdates &updates) {\n\tif (_requesting || _applySkippedLevel) {\n\t\treturn true;\n\t} else if (pts <= _good && count > 0) {\n\t\treturn false;\n\t} else if (check(channel, pts, count)) {\n\t\treturn true;\n\t}\n\t_updatesQueue.emplace(ptsKey(SkippedUpdates, pts), updates);\n\treturn false;\n}\n\nbool PtsWaiter::updated(\n\t\tChannelData *channel,\n\t\tint32 pts,\n\t\tint32 count,\n\t\tconst MTPUpdate &update) {\n\tif (_requesting || _applySkippedLevel) {\n\t\treturn true;\n\t} else if (pts <= _good && count > 0) {\n\t\treturn false;\n\t} else if (check(channel, pts, count)) {\n\t\treturn true;\n\t}\n\t_updateQueue.emplace(ptsKey(SkippedUpdate, pts), update);\n\treturn false;\n}\n\nbool PtsWaiter::updated(ChannelData *channel, int32 pts, int32 count) {\n\tif (_requesting || _applySkippedLevel) {\n\t\treturn true;\n\t} else if (pts <= _good && count > 0) {\n\t\treturn false;\n\t}\n\treturn check(channel, pts, count);\n}\n\nbool PtsWaiter::updateAndApply(\n\t\tChannelData *channel,\n\t\tint32 pts,\n\t\tint32 count,\n\t\tconst MTPUpdates &updates) {\n\tif (!updated(channel, pts, count, updates)) {\n\t\treturn false;\n\t}\n\tif (!_waitingForSkipped || _queue.empty()) {\n\t\t// Optimization - no need to put in queue and back.\n\t\t_owner->applyUpdatesNoPtsCheck(updates);\n\t} else {\n\t\t_updatesQueue.emplace(ptsKey(SkippedUpdates, pts), updates);\n\t\tapplySkippedUpdates(channel);\n\t}\n\treturn true;\n}\n\nbool PtsWaiter::updateAndApply(\n\t\tChannelData *channel,\n\t\tint32 pts,\n\t\tint32 count,\n\t\tconst MTPUpdate &update) {\n\tif (!updated(channel, pts, count, update)) {\n\t\treturn false;\n\t}\n\tif (!_waitingForSkipped || _queue.empty()) {\n\t\t// Optimization - no need to put in queue and back.\n\t\t_owner->applyUpdateNoPtsCheck(update);\n\t} else {\n\t\t_updateQueue.emplace(ptsKey(SkippedUpdate, pts), update);\n\t\tapplySkippedUpdates(channel);\n\t}\n\treturn true;\n}\n\nbool PtsWaiter::updateAndApply(\n\t\tChannelData *channel,\n\t\tint32 pts,\n\t\tint32 count) {\n\tif (!updated(channel, pts, count)) {\n\t\treturn false;\n\t}\n\tapplySkippedUpdates(channel);\n\treturn true;\n}\n\n// Return false if need to save that update and apply later.\nbool PtsWaiter::check(ChannelData *channel, int32 pts, int32 count) {\n\tif (!inited()) {\n\t\tinit(pts);\n\t\treturn true;\n\t}\n\n\t_last = qMax(_last, pts);\n\t_count += count;\n\tif (_last == _count) {\n\t\t_good = _last;\n\t\treturn true;\n\t} else if (_last < _count) {\n\t\tsetWaitingForSkipped(channel, 1);\n\t} else {\n\t\tsetWaitingForSkipped(channel, kWaitForSkippedTimeout);\n\t}\n\treturn !count;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_pts_waiter.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Api {\nclass Updates;\n} // namespace Api\n\nenum PtsSkippedQueue {\n\tSkippedUpdate,\n\tSkippedUpdates,\n};\n\nclass PtsWaiter {\npublic:\n\texplicit PtsWaiter(not_null<Api::Updates*> owner);\n\n\t// 1s wait for skipped seq or pts in updates.\n\tstatic constexpr auto kWaitForSkippedTimeout = 1000;\n\n\tvoid init(int32 pts) {\n\t\t_good = _last = _count = pts;\n\t\tclearSkippedUpdates();\n\t}\n\tbool inited() const {\n\t\treturn _good > 0;\n\t}\n\tvoid setRequesting(bool isRequesting) {\n\t\t_requesting = isRequesting;\n\t\tif (_requesting) {\n\t\t\tclearSkippedUpdates();\n\t\t}\n\t}\n\tbool requesting() const {\n\t\treturn _requesting;\n\t}\n\tbool waitingForSkipped() const {\n\t\treturn _waitingForSkipped;\n\t}\n\tbool waitingForShortPoll() const {\n\t\treturn _waitingForShortPoll;\n\t}\n\tvoid setWaitingForSkipped(ChannelData *channel, crl::time ms); // < 0 - not waiting\n\tvoid setWaitingForShortPoll(ChannelData *channel, crl::time ms); // < 0 - not waiting\n\tint32 current() const{\n\t\treturn _good;\n\t}\n\tbool updated(\n\t\tChannelData *channel,\n\t\tint32 pts,\n\t\tint32 count,\n\t\tconst MTPUpdates &updates);\n\tbool updated(\n\t\tChannelData *channel,\n\t\tint32 pts,\n\t\tint32 count,\n\t\tconst MTPUpdate &update);\n\tbool updated(\n\t\tChannelData *channel,\n\t\tint32 pts,\n\t\tint32 count);\n\tbool updateAndApply(\n\t\tChannelData *channel,\n\t\tint32 pts,\n\t\tint32 count,\n\t\tconst MTPUpdates &updates);\n\tbool updateAndApply(\n\t\tChannelData *channel,\n\t\tint32 pts,\n\t\tint32 count,\n\t\tconst MTPUpdate &update);\n\tbool updateAndApply(\n\t\tChannelData *channel,\n\t\tint32 pts,\n\t\tint32 count);\n\tvoid applySkippedUpdates(ChannelData *channel);\n\tvoid clearSkippedUpdates();\n\nprivate:\n\t// Return false if need to save that update and apply later.\n\tbool check(ChannelData *channel, int32 pts, int32 count);\n\n\tuint64 ptsKey(PtsSkippedQueue queue, int32 pts);\n\tvoid checkForWaiting(ChannelData *channel);\n\n\tconst not_null<Api::Updates*> _owner;\n\tbase::flat_map<uint64, PtsSkippedQueue> _queue;\n\tbase::flat_map<uint64, MTPUpdate> _updateQueue;\n\tbase::flat_map<uint64, MTPUpdates> _updatesQueue;\n\tint32 _good = 0;\n\tint32 _last = 0;\n\tint32 _count = 0;\n\tint32 _applySkippedLevel = 0;\n\tbool _requesting = false;\n\tbool _waitingForSkipped = false;\n\tbool _waitingForShortPoll = false;\n\tuint32 _skippedKey = 0;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_replies_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_replies_list.h\"\n\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"main/main_session.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_messages.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"window/notifications_manager.h\"\n#include \"core/application.h\"\n#include \"lang/lang_keys.h\"\n#include \"apiwrap.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kMessagesPerPage = 50;\nconstexpr auto kReadRequestTimeout = 3 * crl::time(1000);\nconstexpr auto kMaxMessagesToDeleteMyTopic = 10;\n\n[[nodiscard]] HistoryItem *GenerateDivider(\n\t\tnot_null<History*> history,\n\t\tTimeId date,\n\t\tconst QString &text) {\n\treturn history->makeMessage({\n\t\t.id = history->nextNonHistoryEntryId(),\n\t\t.flags = MessageFlag::FakeHistoryItem,\n\t\t.date = date,\n\t}, PreparedServiceText{ { .text = text } });\n}\n\n[[nodiscard]] bool IsCreating(not_null<History*> history, MsgId rootId) {\n\tif (const auto forum = history->asForum()) {\n\t\treturn forum->creating(rootId);\n\t}\n\treturn false;\n}\n\n} // namespace\n\nstruct RepliesList::Viewer {\n\tMessagesSlice slice;\n\tMsgId around = 0;\n\tint limitBefore = 0;\n\tint limitAfter = 0;\n\tint injectedForRoot = 0;\n\tbase::has_weak_ptr guard;\n\tbool scheduled = false;\n};\n\nRepliesList::RepliesList(\n\tnot_null<History*> history,\n\tMsgId rootId,\n\tForumTopic *owningTopic)\n: _history(history)\n, _owningTopic(owningTopic)\n, _rootId(rootId)\n, _creating(IsCreating(history, rootId))\n, _readRequestTimer([=] { sendReadTillRequest(); }) {\n\tif (_owningTopic) {\n\t\t_owningTopic->destroyed(\n\t\t) | rpl::on_next([=] {\n\t\t\t_owningTopic = nullptr;\n\t\t\tsubscribeToUpdates();\n\t\t}, _lifetime);\n\t} else {\n\t\tsubscribeToUpdates();\n\t}\n}\n\nRepliesList::~RepliesList() {\n\thistories().cancelRequest(base::take(_beforeId));\n\thistories().cancelRequest(base::take(_afterId));\n\tif (_readRequestTimer.isActive()) {\n\t\tsendReadTillRequest();\n\t}\n\tif (_divider) {\n\t\t_divider->destroy();\n\t}\n}\n\nvoid RepliesList::subscribeToUpdates() {\n\t_history->owner().repliesReadTillUpdates(\n\t) | rpl::filter([=](const RepliesReadTillUpdate &update) {\n\t\treturn (update.id.msg == _rootId)\n\t\t\t&& (update.id.peer == _history->peer->id);\n\t}) | rpl::on_next([=](const RepliesReadTillUpdate &update) {\n\t\tapply(update);\n\t}, _lifetime);\n\n\t_history->session().changes().messageUpdates(\n\t\tMessageUpdate::Flag::NewAdded\n\t\t| MessageUpdate::Flag::NewMaybeAdded\n\t\t| MessageUpdate::Flag::ReplyToTopAdded\n\t\t| MessageUpdate::Flag::Destroyed\n\t) | rpl::on_next([=](const MessageUpdate &update) {\n\t\tapply(update);\n\t}, _lifetime);\n\n\t_history->session().changes().topicUpdates(\n\t\tTopicUpdate::Flag::Creator\n\t) | rpl::on_next([=](const TopicUpdate &update) {\n\t\tapply(update);\n\t}, _lifetime);\n\n\t_history->owner().channelDifferenceTooLong(\n\t) | rpl::on_next([=](not_null<ChannelData*> channel) {\n\t\tif (channel == _history->peer) {\n\t\t\tapplyDifferenceTooLong();\n\t\t}\n\t}, _lifetime);\n}\n\nvoid RepliesList::apply(const RepliesReadTillUpdate &update) {\n\tif (update.out) {\n\t\tsetOutboxReadTill(update.readTillId);\n\t} else if (update.readTillId >= _inboxReadTillId) {\n\t\tsetInboxReadTill(\n\t\t\tupdate.readTillId,\n\t\t\tcomputeUnreadCountLocally(update.readTillId));\n\t}\n}\n\nvoid RepliesList::apply(const MessageUpdate &update) {\n\tif (applyUpdate(update)) {\n\t\t_instantChanges.fire({});\n\t}\n}\n\nvoid RepliesList::apply(const TopicUpdate &update) {\n\tif (update.topic->history() == _history\n\t\t&& update.topic->rootId() == _rootId) {\n\t\tif (update.flags & TopicUpdate::Flag::Creator) {\n\t\t\tapplyTopicCreator(update.topic->creatorId());\n\t\t}\n\t}\n}\n\nvoid RepliesList::applyTopicCreator(PeerId creatorId) {\n\tconst auto owner = &_history->owner();\n\tconst auto peerId = _history->peer->id;\n\tfor (const auto &id : _list) {\n\t\tif (const auto item = owner->message(peerId, id)) {\n\t\t\tif (item->from()->id == creatorId) {\n\t\t\t\towner->requestItemResize(item);\n\t\t\t}\n\t\t}\n\t}\n}\n\nrpl::producer<MessagesSlice> RepliesList::source(\n\t\tMessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tconst auto around = aroundId.fullId.msg;\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tconst auto viewer = lifetime.make_state<Viewer>();\n\t\tconst auto push = [=] {\n\t\t\tif (viewer->scheduled) {\n\t\t\t\tviewer->scheduled = false;\n\t\t\t\tif (buildFromData(viewer)) {\n\t\t\t\t\tappendClientSideMessages(viewer->slice);\n\t\t\t\t\tconsumer.put_next_copy(viewer->slice);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tconst auto pushInstant = [=] {\n\t\t\tviewer->scheduled = true;\n\t\t\tpush();\n\t\t};\n\t\tconst auto pushDelayed = [=] {\n\t\t\tif (!viewer->scheduled) {\n\t\t\t\tviewer->scheduled = true;\n\t\t\t\tcrl::on_main(&viewer->guard, push);\n\t\t\t}\n\t\t};\n\t\tviewer->around = around;\n\t\tviewer->limitBefore = limitBefore;\n\t\tviewer->limitAfter = limitAfter;\n\n\t\t_history->session().changes().historyUpdates(\n\t\t\t_history,\n\t\t\tHistoryUpdate::Flag::ClientSideMessages\n\t\t) | rpl::on_next(pushDelayed, lifetime);\n\n\t\t_history->session().changes().messageUpdates(\n\t\t\tMessageUpdate::Flag::Destroyed\n\t\t) | rpl::filter([=](const MessageUpdate &update) {\n\t\t\treturn applyItemDestroyed(viewer, update.item);\n\t\t}) | rpl::on_next(pushDelayed, lifetime);\n\n\t\t_listChanges.events(\n\t\t) | rpl::on_next(pushDelayed, lifetime);\n\n\t\t_instantChanges.events(\n\t\t) | rpl::on_next(pushInstant, lifetime);\n\n\t\tpushInstant();\n\t\treturn lifetime;\n\t};\n}\n\nvoid RepliesList::appendClientSideMessages(MessagesSlice &slice) {\n\tconst auto &messages = _history->clientSideMessages();\n\tif (messages.empty()) {\n\t\treturn;\n\t} else if (slice.ids.empty()) {\n\t\tif (slice.skippedBefore != 0 || slice.skippedAfter != 0) {\n\t\t\treturn;\n\t\t}\n\t\tslice.ids.reserve(messages.size());\n\t\tfor (const auto &item : messages) {\n\t\t\tif (!item->inThread(_rootId)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tslice.ids.push_back(item->fullId());\n\t\t}\n\t\tranges::sort(slice.ids);\n\t\treturn;\n\t}\n\tauto &owner = _history->owner();\n\tauto dates = std::vector<TimeId>();\n\tdates.reserve(slice.ids.size());\n\tfor (const auto &id : slice.ids) {\n\t\tconst auto message = owner.message(id);\n\t\tAssert(message != nullptr);\n\n\t\tdates.push_back(message->date());\n\t}\n\tfor (const auto &item : messages) {\n\t\tif (!item->inThread(_rootId)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto date = item->date();\n\t\tif (date < dates.front()) {\n\t\t\tif (slice.skippedBefore != 0) {\n\t\t\t\tif (slice.skippedBefore) {\n\t\t\t\t\t++*slice.skippedBefore;\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tdates.insert(dates.begin(), date);\n\t\t\tslice.ids.insert(slice.ids.begin(), item->fullId());\n\t\t} else {\n\t\t\tauto to = dates.size();\n\t\t\tfor (; to != 0; --to) {\n\t\t\t\tconst auto checkId = slice.ids[to - 1].msg;\n\t\t\t\tif (dates[to - 1] > date) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (dates[to - 1] < date\n\t\t\t\t\t|| IsServerMsgId(checkId)\n\t\t\t\t\t|| checkId < item->id) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tdates.insert(dates.begin() + to, date);\n\t\t\tslice.ids.insert(slice.ids.begin() + to, item->fullId());\n\t\t}\n\t}\n}\n\nrpl::producer<int> RepliesList::fullCount() const {\n\treturn _fullCount.value() | rpl::filter_optional();\n}\n\nrpl::producer<std::optional<int>> RepliesList::maybeFullCount() const {\n\treturn _fullCount.value();\n}\n\nbool RepliesList::unreadCountKnown() const {\n\treturn _unreadCount.current().has_value();\n}\n\nint RepliesList::unreadCountCurrent() const {\n\treturn _unreadCount.current().value_or(0);\n}\n\nrpl::producer<std::optional<int>> RepliesList::unreadCountValue() const {\n\treturn _unreadCount.value();\n}\n\nvoid RepliesList::injectRootMessageAndReverse(not_null<Viewer*> viewer) {\n\tinjectRootMessage(viewer);\n\tranges::reverse(viewer->slice.ids);\n}\n\nvoid RepliesList::injectRootMessage(not_null<Viewer*> viewer) {\n\tconst auto slice = &viewer->slice;\n\tviewer->injectedForRoot = 0;\n\tif (slice->skippedBefore != 0) {\n\t\treturn;\n\t}\n\tconst auto root = lookupRoot();\n\tif (!root\n\t\t|| (_rootId == Data::ForumTopic::kGeneralId)\n\t\t|| (root->topicRootId() != Data::ForumTopic::kGeneralId)) {\n\t\treturn;\n\t}\n\tinjectRootDivider(root, slice);\n\n\tif (const auto group = _history->owner().groups().find(root)) {\n\t\tfor (const auto &item : ranges::views::reverse(group->items)) {\n\t\t\tslice->ids.push_back(item->fullId());\n\t\t}\n\t\tviewer->injectedForRoot = group->items.size();\n\t\tif (slice->fullCount) {\n\t\t\t*slice->fullCount += group->items.size();\n\t\t}\n\t} else {\n\t\tslice->ids.push_back(root->fullId());\n\t\tviewer->injectedForRoot = 1;\n\t}\n\tif (slice->fullCount) {\n\t\t*slice->fullCount += viewer->injectedForRoot;\n\t}\n}\n\nvoid RepliesList::injectRootDivider(\n\t\tnot_null<HistoryItem*> root,\n\t\tnot_null<MessagesSlice*> slice) {\n\tconst auto withComments = !slice->ids.empty();\n\tconst auto text = [&] {\n\t\treturn withComments\n\t\t\t? tr::lng_replies_discussion_started(tr::now)\n\t\t\t: tr::lng_replies_no_comments(tr::now);\n\t};\n\tif (!_divider) {\n\t\t_dividerWithComments = withComments;\n\t\t_divider = GenerateDivider(\n\t\t\t_history,\n\t\t\troot->date(),\n\t\t\ttext());\n\t} else if (_dividerWithComments != withComments) {\n\t\t_dividerWithComments = withComments;\n\t\t_divider->updateServiceText(PreparedServiceText{ { text() } });\n\t}\n\tslice->ids.push_back(_divider->fullId());\n}\n\nbool RepliesList::buildFromData(not_null<Viewer*> viewer) {\n\tif (_creating\n\t\t|| (_list.empty() && _skippedBefore == 0 && _skippedAfter == 0)) {\n\t\tviewer->slice.ids.clear();\n\t\tviewer->slice.nearestToAround = FullMsgId();\n\t\tviewer->slice.fullCount\n\t\t\t= viewer->slice.skippedBefore\n\t\t\t= viewer->slice.skippedAfter\n\t\t\t= 0;\n\t\tviewer->injectedForRoot = 0;\n\t\tinjectRootMessageAndReverse(viewer);\n\t\treturn true;\n\t}\n\tconst auto around = [&] {\n\t\tif (viewer->around != ShowAtUnreadMsgId) {\n\t\t\treturn viewer->around;\n\t\t} else if (lookupRoot()) {\n\t\t\treturn computeInboxReadTillFull();\n\t\t} else if (_owningTopic) {\n\t\t\t// Somehow we don't want always to jump to computed inboxReadTill\n\t\t\t// (this was in the code before, but I don't remember why).\n\t\t\t// Maybe in case we \"View Thread\" from a group we don't really\n\t\t\t// want to jump to unread inside thread, cause it isn't defined.\n\t\t\t//\n\t\t\t// But in case of topics we definitely want to support jumping\n\t\t\t// to the first unread, even if it is General topic without the\n\t\t\t// actual root message or it is a broken topic without root.\n\t\t\treturn computeInboxReadTillFull();\n\t\t}\n\t\treturn viewer->around;\n\t}();\n\tif (_list.empty()\n\t\t|| (!around && _skippedAfter != 0)\n\t\t|| (around > _list.front() && _skippedAfter != 0)\n\t\t|| (around > 0 && around < _list.back() && _skippedBefore != 0)) {\n\t\tloadAround(around);\n\t\treturn false;\n\t}\n\tconst auto i = around\n\t\t? ranges::lower_bound(_list, around, std::greater<>())\n\t\t: end(_list);\n\tconst auto availableBefore = int(end(_list) - i);\n\tconst auto availableAfter = int(i - begin(_list));\n\tconst auto useBefore = std::min(availableBefore, viewer->limitBefore + 1);\n\tconst auto useAfter = std::min(availableAfter, viewer->limitAfter);\n\tconst auto slice = &viewer->slice;\n\tif (_skippedBefore.has_value()) {\n\t\tslice->skippedBefore\n\t\t\t= (*_skippedBefore + (availableBefore - useBefore));\n\t}\n\tif (_skippedAfter.has_value()) {\n\t\tslice->skippedAfter\n\t\t\t= (*_skippedAfter + (availableAfter - useAfter));\n\t}\n\n\tconst auto peerId = _history->peer->id;\n\tslice->ids.clear();\n\tauto nearestToAround = std::optional<MsgId>();\n\tslice->ids.reserve(useAfter + useBefore);\n\tfor (auto j = i - useAfter, e = i + useBefore; j != e; ++j) {\n\t\tconst auto id = *j;\n\t\tif (id == _rootId) {\n\t\t\tcontinue;\n\t\t} else if (!nearestToAround && id < around) {\n\t\t\tnearestToAround = (j == i - useAfter)\n\t\t\t\t? id\n\t\t\t\t: *(j - 1);\n\t\t}\n\t\tslice->ids.emplace_back(peerId, id);\n\t}\n\tslice->nearestToAround = FullMsgId(\n\t\tpeerId,\n\t\tnearestToAround.value_or(\n\t\t\tslice->ids.empty() ? 0 : slice->ids.back().msg));\n\tslice->fullCount = _fullCount.current();\n\n\tinjectRootMessageAndReverse(viewer);\n\n\tif (_skippedBefore != 0 && useBefore < viewer->limitBefore + 1) {\n\t\tloadBefore();\n\t}\n\tif (_skippedAfter != 0 && useAfter < viewer->limitAfter) {\n\t\tloadAfter();\n\t}\n\n\treturn true;\n}\n\nbool RepliesList::applyItemDestroyed(\n\t\tnot_null<Viewer*> viewer,\n\t\tnot_null<HistoryItem*> item) {\n\tif (item->history() != _history || !item->isRegular()) {\n\t\treturn false;\n\t}\n\tconst auto fullId = item->fullId();\n\tfor (auto i = 0; i != viewer->injectedForRoot; ++i) {\n\t\tif (viewer->slice.ids[i] == fullId) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool RepliesList::applyUpdate(const MessageUpdate &update) {\n\tusing Flag = MessageUpdate::Flag;\n\n\tif (update.item->history() != _history || !update.item->isRegular()) {\n\t\treturn false;\n\t}\n\n\tconst auto id = update.item->id;\n\tconst auto inThread = update.item->inThread(_rootId);\n\tconst auto added = (update.flags & Flag::ReplyToTopAdded);\n\tconst auto i = ranges::lower_bound(_list, id, std::greater<>());\n\tif (update.flags & Flag::Destroyed) {\n\t\tif (!added && inThread) {\n\t\t\tchangeUnreadCountByPost(id, -1);\n\t\t}\n\t\tif (i == end(_list) || *i != id) {\n\t\t\treturn false;\n\t\t}\n\t\t_list.erase(i);\n\t\tif (_skippedBefore && _skippedAfter) {\n\t\t\t_fullCount = *_skippedBefore + _list.size() + *_skippedAfter;\n\t\t} else if (const auto known = _fullCount.current()) {\n\t\t\tif (*known > 0) {\n\t\t\t\t_fullCount = (*known - 1);\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t} else if (!inThread) {\n\t\treturn false;\n\t}\n\tif (added) {\n\t\tchangeUnreadCountByPost(id, 1);\n\t}\n\tif (_skippedAfter != 0\n\t\t|| (i != end(_list) && *i == id)) {\n\t\treturn false;\n\t}\n\t_list.insert(i, id);\n\tif (_skippedBefore && _skippedAfter) {\n\t\t_fullCount = *_skippedBefore + _list.size() + *_skippedAfter;\n\t} else if (const auto known = _fullCount.current()) {\n\t\t_fullCount = *known + 1;\n\t}\n\treturn true;\n}\n\nvoid RepliesList::applyDifferenceTooLong() {\n\tif (!_creating && _skippedAfter.has_value()) {\n\t\t_skippedAfter = std::nullopt;\n\t\t_listChanges.fire({});\n\t}\n}\n\nvoid RepliesList::changeUnreadCountByPost(MsgId id, int delta) {\n\tif (!_inboxReadTillId) {\n\t\tsetUnreadCount(std::nullopt);\n\t\treturn;\n\t}\n\tconst auto count = _unreadCount.current();\n\tif (count.has_value() && (id > _inboxReadTillId)) {\n\t\tsetUnreadCount(std::max(*count + delta, 0));\n\t}\n}\n\nHistories &RepliesList::histories() {\n\treturn _history->owner().histories();\n}\n\nHistoryItem *RepliesList::lookupRoot() {\n\treturn _history->owner().message(_history->peer->id, _rootId);\n}\n\nvoid RepliesList::loadAround(MsgId id) {\n\tExpects(!_creating);\n\n\tif (_loadingAround && *_loadingAround == id) {\n\t\treturn;\n\t}\n\thistories().cancelRequest(base::take(_beforeId));\n\thistories().cancelRequest(base::take(_afterId));\n\n\tconst auto send = [=](Fn<void()> finish) {\n\t\treturn _history->session().api().request(MTPmessages_GetReplies(\n\t\t\t_history->peer->input(),\n\t\t\tMTP_int(_rootId),\n\t\t\tMTP_int(id), // offset_id\n\t\t\tMTP_int(0), // offset_date\n\t\t\tMTP_int(id ? (-kMessagesPerPage / 2) : 0), // add_offset\n\t\t\tMTP_int(kMessagesPerPage), // limit\n\t\t\tMTP_int(0), // max_id\n\t\t\tMTP_int(0), // min_id\n\t\t\tMTP_long(0) // hash\n\t\t)).done([=](const MTPmessages_Messages &result) {\n\t\t\t_beforeId = 0;\n\t\t\t_loadingAround = std::nullopt;\n\t\t\tfinish();\n\n\t\t\tif (!id) {\n\t\t\t\t_skippedAfter = 0;\n\t\t\t} else {\n\t\t\t\t_skippedAfter = std::nullopt;\n\t\t\t}\n\t\t\t_skippedBefore = std::nullopt;\n\t\t\t_list.clear();\n\t\t\tif (processMessagesIsEmpty(result)) {\n\t\t\t\t_fullCount = _skippedBefore = _skippedAfter = 0;\n\t\t\t} else if (id) {\n\t\t\t\tAssert(!_list.empty());\n\t\t\t\tif (_list.front() <= id) {\n\t\t\t\t\t_skippedAfter = 0;\n\t\t\t\t} else if (_list.back() >= id) {\n\t\t\t\t\t_skippedBefore = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t\tcheckReadTillEnd();\n\t\t}).fail([=] {\n\t\t\t_beforeId = 0;\n\t\t\t_loadingAround = std::nullopt;\n\t\t\tfinish();\n\t\t}).send();\n\t};\n\t_loadingAround = id;\n\t_beforeId = histories().sendRequest(\n\t\t_history,\n\t\tHistories::RequestType::History,\n\t\tsend);\n}\n\nvoid RepliesList::loadBefore() {\n\tExpects(!_list.empty());\n\n\tif (_loadingAround) {\n\t\thistories().cancelRequest(base::take(_beforeId));\n\t} else if (_beforeId) {\n\t\treturn;\n\t}\n\n\tconst auto last = _list.back();\n\tconst auto send = [=](Fn<void()> finish) {\n\t\treturn _history->session().api().request(MTPmessages_GetReplies(\n\t\t\t_history->peer->input(),\n\t\t\tMTP_int(_rootId),\n\t\t\tMTP_int(last), // offset_id\n\t\t\tMTP_int(0), // offset_date\n\t\t\tMTP_int(0), // add_offset\n\t\t\tMTP_int(kMessagesPerPage), // limit\n\t\t\tMTP_int(0), // min_id\n\t\t\tMTP_int(0), // max_id\n\t\t\tMTP_long(0) // hash\n\t\t)).done([=](const MTPmessages_Messages &result) {\n\t\t\t_beforeId = 0;\n\t\t\tfinish();\n\n\t\t\tif (_list.empty()) {\n\t\t\t\treturn;\n\t\t\t} else if (_list.back() != last) {\n\t\t\t\tloadBefore();\n\t\t\t} else if (processMessagesIsEmpty(result)) {\n\t\t\t\t_skippedBefore = 0;\n\t\t\t\tif (_skippedAfter == 0) {\n\t\t\t\t\t_fullCount = _list.size();\n\t\t\t\t}\n\t\t\t}\n\t\t}).fail([=] {\n\t\t\t_beforeId = 0;\n\t\t\tfinish();\n\t\t}).send();\n\t};\n\t_beforeId = histories().sendRequest(\n\t\t_history,\n\t\tHistories::RequestType::History,\n\t\tsend);\n}\n\nvoid RepliesList::loadAfter() {\n\tExpects(!_list.empty());\n\n\tif (_afterId) {\n\t\treturn;\n\t}\n\n\tconst auto first = _list.front();\n\tconst auto send = [=](Fn<void()> finish) {\n\t\treturn _history->session().api().request(MTPmessages_GetReplies(\n\t\t\t_history->peer->input(),\n\t\t\tMTP_int(_rootId),\n\t\t\tMTP_int(first + 1), // offset_id\n\t\t\tMTP_int(0), // offset_date\n\t\t\tMTP_int(-kMessagesPerPage), // add_offset\n\t\t\tMTP_int(kMessagesPerPage), // limit\n\t\t\tMTP_int(0), // min_id\n\t\t\tMTP_int(0), // max_id\n\t\t\tMTP_long(0) // hash\n\t\t)).done([=](const MTPmessages_Messages &result) {\n\t\t\t_afterId = 0;\n\t\t\tfinish();\n\n\t\t\tif (_list.empty()) {\n\t\t\t\treturn;\n\t\t\t} else if (_list.front() != first) {\n\t\t\t\tloadAfter();\n\t\t\t} else if (processMessagesIsEmpty(result)) {\n\t\t\t\t_skippedAfter = 0;\n\t\t\t\tif (_skippedBefore == 0) {\n\t\t\t\t\t_fullCount = _list.size();\n\t\t\t\t}\n\t\t\t\tcheckReadTillEnd();\n\t\t\t}\n\t\t}).fail([=] {\n\t\t\t_afterId = 0;\n\t\t\tfinish();\n\t\t}).send();\n\t};\n\t_afterId = histories().sendRequest(\n\t\t_history,\n\t\tHistories::RequestType::History,\n\t\tsend);\n}\n\nbool RepliesList::processMessagesIsEmpty(const MTPmessages_Messages &result) {\n\tconst auto guard = gsl::finally([&] { _listChanges.fire({}); });\n\n\tauto &owner = _history->owner();\n\tconst auto list = result.match([&](\n\t\t\tconst MTPDmessages_messagesNotModified &) {\n\t\tLOG((\"API Error: received messages.messagesNotModified! \"\n\t\t\t\"(HistoryWidget::messagesReceived)\"));\n\t\treturn QVector<MTPMessage>();\n\t}, [&](const auto &data) {\n\t\towner.processUsers(data.vusers());\n\t\towner.processChats(data.vchats());\n\t\treturn data.vmessages().v;\n\t});\n\n\tconst auto fullCount = result.match([&](\n\t\t\tconst MTPDmessages_messagesNotModified &) {\n\t\tLOG((\"API Error: received messages.messagesNotModified! \"\n\t\t\t\"(HistoryWidget::messagesReceived)\"));\n\t\treturn 0;\n\t}, [&](const MTPDmessages_messages &data) {\n\t\t_history->peer->processTopics(data.vtopics());\n\t\treturn int(data.vmessages().v.size());\n\t}, [&](const MTPDmessages_messagesSlice &data) {\n\t\t_history->peer->processTopics(data.vtopics());\n\t\treturn data.vcount().v;\n\t}, [&](const MTPDmessages_channelMessages &data) {\n\t\tif (const auto channel = _history->peer->asChannel()) {\n\t\t\tchannel->ptsReceived(data.vpts().v);\n\t\t} else {\n\t\t\tLOG((\"API Error: received messages.channelMessages when \"\n\t\t\t\t\"no channel was passed! (HistoryWidget::messagesReceived)\"));\n\t\t}\n\t\t_history->peer->processTopics(data.vtopics());\n\t\treturn data.vcount().v;\n\t});\n\n\tif (list.isEmpty()) {\n\t\treturn true;\n\t}\n\n\tconst auto maxId = IdFromMessage(list.front());\n\tconst auto wasSize = int(_list.size());\n\tconst auto toFront = (wasSize > 0) && (maxId > _list.front());\n\tconst auto localFlags = MessageFlags();\n\tconst auto type = NewMessageType::Existing;\n\tauto refreshed = std::vector<MsgId>();\n\tif (toFront) {\n\t\trefreshed.reserve(_list.size() + list.size());\n\t}\n\tauto skipped = 0;\n\tfor (const auto &message : list) {\n\t\tif (const auto item = owner.addNewMessage(message, localFlags, type)) {\n\t\t\tif (item->inThread(_rootId)) {\n\t\t\t\tif (toFront && item->id > _list.front()) {\n\t\t\t\t\trefreshed.push_back(item->id);\n\t\t\t\t} else if (_list.empty() || item->id < _list.back()) {\n\t\t\t\t\t_list.push_back(item->id);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t++skipped;\n\t\t\t}\n\t\t} else {\n\t\t\t++skipped;\n\t\t}\n\t}\n\tif (toFront) {\n\t\trefreshed.insert(refreshed.end(), _list.begin(), _list.end());\n\t\t_list = std::move(refreshed);\n\t}\n\n\tconst auto nowSize = int(_list.size());\n\tauto &decrementFrom = toFront ? _skippedAfter : _skippedBefore;\n\tif (decrementFrom.has_value()) {\n\t\t*decrementFrom = std::max(\n\t\t\t*decrementFrom - (nowSize - wasSize),\n\t\t\t0);\n\t}\n\n\tconst auto checkedCount = std::max(fullCount - skipped, nowSize);\n\tif (_skippedBefore && _skippedAfter) {\n\t\tauto &correct = toFront ? _skippedBefore : _skippedAfter;\n\t\t*correct = std::max(\n\t\t\tcheckedCount - *decrementFrom - nowSize,\n\t\t\t0);\n\t\t*decrementFrom = checkedCount - *correct - nowSize;\n\t\tAssert(*decrementFrom >= 0);\n\t} else if (_skippedBefore) {\n\t\t*_skippedBefore = std::min(*_skippedBefore, checkedCount - nowSize);\n\t\t_skippedAfter = checkedCount - *_skippedBefore - nowSize;\n\t} else if (_skippedAfter) {\n\t\t*_skippedAfter = std::min(*_skippedAfter, checkedCount - nowSize);\n\t\t_skippedBefore = checkedCount - *_skippedAfter - nowSize;\n\t}\n\t_fullCount = checkedCount;\n\n\tcheckReadTillEnd();\n\n\tif (const auto item = lookupRoot()) {\n\t\tif (const auto original = item->lookupDiscussionPostOriginal()) {\n\t\t\tif (_skippedAfter == 0 && !_list.empty()) {\n\t\t\t\toriginal->setCommentsMaxId(_list.front());\n\t\t\t} else {\n\t\t\t\toriginal->setCommentsPossibleMaxId(maxId);\n\t\t\t}\n\t\t}\n\t}\n\n\tEnsures(list.size() >= skipped);\n\treturn (list.size() == skipped);\n}\n\nvoid RepliesList::setInboxReadTill(\n\t\tMsgId readTillId,\n\t\tstd::optional<int> unreadCount) {\n\tconst auto newReadTillId = std::max(readTillId.bare, int64(1));\n\tconst auto ignore = (newReadTillId < _inboxReadTillId);\n\tif (ignore) {\n\t\treturn;\n\t}\n\tconst auto changed = (newReadTillId > _inboxReadTillId);\n\tif (changed) {\n\t\t_inboxReadTillId = newReadTillId;\n\t}\n\tif (_skippedAfter == 0\n\t\t&& !_list.empty()\n\t\t&& _inboxReadTillId >= _list.front()) {\n\t\tunreadCount = 0;\n\t}\n\tconst auto wasUnreadCount = _unreadCount;\n\tif (_unreadCount.current() != unreadCount\n\t\t&& (changed || unreadCount.has_value())) {\n\t\tsetUnreadCount(unreadCount);\n\t}\n}\n\nMsgId RepliesList::inboxReadTillId() const {\n\treturn _inboxReadTillId;\n}\n\nMsgId RepliesList::computeInboxReadTillFull() const {\n\tconst auto local = _inboxReadTillId;\n\tif (const auto megagroup = _history->peer->asMegagroup()) {\n\t\tif (!megagroup->isForum() && megagroup->amIn()) {\n\t\t\treturn std::max(local, _history->inboxReadTillId());\n\t\t}\n\t}\n\treturn local;\n}\n\nvoid RepliesList::setOutboxReadTill(MsgId readTillId) {\n\tconst auto newReadTillId = std::max(readTillId.bare, int64(1));\n\tif (newReadTillId > _outboxReadTillId) {\n\t\t_outboxReadTillId = newReadTillId;\n\t\t_history->session().changes().historyUpdated(\n\t\t\t_history,\n\t\t\tHistoryUpdate::Flag::OutboxRead);\n\t}\n}\n\nMsgId RepliesList::computeOutboxReadTillFull() const {\n\tconst auto local = _outboxReadTillId;\n\tif (const auto megagroup = _history->peer->asMegagroup()) {\n\t\tif (!megagroup->isForum() && megagroup->amIn()) {\n\t\t\treturn std::max(local, _history->outboxReadTillId());\n\t\t}\n\t}\n\treturn local;\n}\n\nvoid RepliesList::setUnreadCount(std::optional<int> count) {\n\t_unreadCount = count;\n\tif (!count && !_readRequestTimer.isActive() && !_readRequestId) {\n\t\treloadUnreadCountIfNeeded();\n\t}\n}\n\nint RepliesList::displayedUnreadCount() const {\n\treturn (_inboxReadTillId > 1) ? unreadCountCurrent() : 0;\n}\n\nbool RepliesList::isServerSideUnread(\n\t\tnot_null<const HistoryItem*> item) const {\n\tconst auto till = item->out()\n\t\t? computeOutboxReadTillFull()\n\t\t: computeInboxReadTillFull();\n\treturn (item->id > till);\n}\n\nvoid RepliesList::checkReadTillEnd() {\n\tif (_unreadCount.current() != 0\n\t\t&& _skippedAfter == 0\n\t\t&& !_list.empty()\n\t\t&& _inboxReadTillId >= _list.front()) {\n\t\tsetUnreadCount(0);\n\t}\n}\n\nstd::optional<int> RepliesList::computeUnreadCountLocally(\n\t\tMsgId afterId) const {\n\tExpects(afterId >= _inboxReadTillId);\n\n\tconst auto currentUnreadCountAfter = _unreadCount.current();\n\tconst auto startingMarkingAsRead = (currentUnreadCountAfter == 0)\n\t\t&& (_inboxReadTillId == 1)\n\t\t&& (afterId > 1);\n\tconst auto wasUnreadCountAfter = startingMarkingAsRead\n\t\t? _fullCount.current().value_or(0)\n\t\t: currentUnreadCountAfter;\n\tconst auto readTillId = std::max(afterId, _rootId);\n\tconst auto wasReadTillId = _inboxReadTillId;\n\tconst auto backLoaded = (_skippedBefore == 0);\n\tconst auto frontLoaded = (_skippedAfter == 0);\n\tconst auto fullLoaded = backLoaded && frontLoaded;\n\tconst auto allUnread = (readTillId == _rootId)\n\t\t|| (fullLoaded && _list.empty());\n\tif (allUnread && fullLoaded) {\n\t\t// Should not happen too often unless the list is empty.\n\t\treturn int(_list.size());\n\t} else if (frontLoaded && !_list.empty() && readTillId >= _list.front()) {\n\t\t// Always \"count by local data\" if read till the end.\n\t\treturn 0;\n\t} else if (wasReadTillId == readTillId) {\n\t\t// Otherwise don't recount the same value over and over.\n\t\treturn wasUnreadCountAfter;\n\t} else if (frontLoaded && !_list.empty() && readTillId >= _list.back()) {\n\t\t// And count by local data if it is available and read-till changed.\n\t\treturn int(ranges::lower_bound(_list, readTillId, std::greater<>())\n\t\t\t- begin(_list));\n\t} else if (_list.empty()) {\n\t\treturn std::nullopt;\n\t} else if (wasUnreadCountAfter.has_value()\n\t\t&& (frontLoaded || readTillId <= _list.front())\n\t\t&& (backLoaded || wasReadTillId >= _list.back())) {\n\t\t// Count how many were read since previous value.\n\t\tconst auto from = ranges::lower_bound(\n\t\t\t_list,\n\t\t\treadTillId,\n\t\t\tstd::greater<>());\n\t\tconst auto till = ranges::lower_bound(\n\t\t\tfrom,\n\t\t\tend(_list),\n\t\t\twasReadTillId,\n\t\t\tstd::greater<>());\n\t\treturn std::max(*wasUnreadCountAfter - int(till - from), 0);\n\t}\n\treturn std::nullopt;\n}\n\nvoid RepliesList::requestUnreadCount() {\n\tif (_reloadUnreadCountRequestId) {\n\t\treturn;\n\t}\n\tconst auto weak = base::make_weak(this);\n\tconst auto session = &_history->session();\n\tconst auto fullId = FullMsgId(_history->peer->id, _rootId);\n\tconst auto apply = [weak, session, fullId](\n\t\t\tMsgId readTill,\n\t\t\tint unreadCount) {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->setInboxReadTill(readTill, unreadCount);\n\t\t}\n\t\tif (const auto root = session->data().message(fullId)) {\n\t\t\tif (const auto post = root->lookupDiscussionPostOriginal()) {\n\t\t\t\tpost->setCommentsInboxReadTill(readTill);\n\t\t\t}\n\t\t}\n\t};\n\t_reloadUnreadCountRequestId = session->api().request(\n\t\tMTPmessages_GetDiscussionMessage(\n\t\t\t_history->peer->input(),\n\t\t\tMTP_int(_rootId))\n\t).done([=](const MTPmessages_DiscussionMessage &result) {\n\t\tif (weak) {\n\t\t\t_reloadUnreadCountRequestId = 0;\n\t\t}\n\t\tresult.match([&](const MTPDmessages_discussionMessage &data) {\n\t\t\tsession->data().processUsers(data.vusers());\n\t\t\tsession->data().processChats(data.vchats());\n\t\t\tapply(\n\t\t\t\tdata.vread_inbox_max_id().value_or_empty(),\n\t\t\t\tdata.vunread_count().v);\n\t\t});\n\t}).send();\n}\n\nvoid RepliesList::readTill(not_null<HistoryItem*> item) {\n\treadTill(item->id, item);\n}\n\nvoid RepliesList::readTill(MsgId tillId) {\n\treadTill(tillId, _history->owner().message(_history->peer->id, tillId));\n}\n\nvoid RepliesList::readTill(\n\t\tMsgId tillId,\n\t\tHistoryItem *tillIdItem) {\n\tif (!IsServerMsgId(tillId)) {\n\t\treturn;\n\t}\n\tconst auto was = computeInboxReadTillFull();\n\tconst auto now = tillId;\n\tif (now < was) {\n\t\treturn;\n\t}\n\tconst auto unreadCount = computeUnreadCountLocally(now);\n\tconst auto fast = (tillIdItem && tillIdItem->out()) || !unreadCount.has_value();\n\tif (was < now || (fast && now == was)) {\n\t\tsetInboxReadTill(now, unreadCount);\n\t\tconst auto rootFullId = FullMsgId(_history->peer->id, _rootId);\n\t\tif (const auto root = _history->owner().message(rootFullId)) {\n\t\t\tif (const auto post = root->lookupDiscussionPostOriginal()) {\n\t\t\t\tpost->setCommentsInboxReadTill(now);\n\t\t\t}\n\t\t}\n\t\tif (!_readRequestTimer.isActive()) {\n\t\t\t_readRequestTimer.callOnce(fast ? 0 : kReadRequestTimeout);\n\t\t} else if (fast && _readRequestTimer.remainingTime() > 0) {\n\t\t\t_readRequestTimer.callOnce(0);\n\t\t}\n\t}\n\tif (const auto topic = _history->peer->forumTopicFor(_rootId)) {\n\t\tCore::App().notifications().clearIncomingFromTopic(topic);\n\t}\n}\n\nvoid RepliesList::sendReadTillRequest() {\n\tif (_readRequestTimer.isActive()) {\n\t\t_readRequestTimer.cancel();\n\t}\n\tconst auto api = &_history->session().api();\n\tapi->request(base::take(_readRequestId)).cancel();\n\n\t_readRequestId = api->request(MTPmessages_ReadDiscussion(\n\t\t_history->peer->input(),\n\t\tMTP_int(_rootId),\n\t\tMTP_int(computeInboxReadTillFull())\n\t)).done(crl::guard(this, [=] {\n\t\t_readRequestId = 0;\n\t\treloadUnreadCountIfNeeded();\n\t})).send();\n}\n\nvoid RepliesList::reloadUnreadCountIfNeeded() {\n\tif (unreadCountKnown()) {\n\t\treturn;\n\t} else if (inboxReadTillId() < computeInboxReadTillFull()) {\n\t\t_readRequestTimer.callOnce(0);\n\t} else {\n\t\trequestUnreadCount();\n\t}\n}\n\nbool RepliesList::canDeleteMyTopic() const {\n\tif (_skippedBefore != 0 || _skippedAfter != 0) {\n\t\treturn false;\n\t}\n\tauto counter = 0;\n\tconst auto owner = &_history->owner();\n\tconst auto peerId = _history->peer->id;\n\tfor (const auto &id : _list) {\n\t\tif (id == _rootId) {\n\t\t\tcontinue;\n\t\t} else if (const auto item = owner->message(peerId, id)) {\n\t\t\tif (!item->out() || ++counter > kMaxMessagesToDeleteMyTopic) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_replies_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"base/timer.h\"\n\nclass History;\n\nnamespace Data {\n\nclass ForumTopic;\nclass Histories;\nstruct MessagePosition;\nstruct MessagesSlice;\nstruct MessageUpdate;\nstruct TopicUpdate;\nstruct RepliesReadTillUpdate;\n\nclass RepliesList final : public base::has_weak_ptr {\npublic:\n\tRepliesList(\n\t\tnot_null<History*> history,\n\t\tMsgId rootId,\n\t\tForumTopic *owningTopic = nullptr);\n\t~RepliesList();\n\n\tvoid apply(const RepliesReadTillUpdate &update);\n\tvoid apply(const MessageUpdate &update);\n\tvoid apply(const TopicUpdate &update);\n\tvoid applyDifferenceTooLong();\n\n\t[[nodiscard]] rpl::producer<MessagesSlice> source(\n\t\tMessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter);\n\n\t[[nodiscard]] rpl::producer<int> fullCount() const;\n\t[[nodiscard]] rpl::producer<std::optional<int>> maybeFullCount() const;\n\n\t[[nodiscard]] bool unreadCountKnown() const;\n\t[[nodiscard]] int unreadCountCurrent() const;\n\t[[nodiscard]] int displayedUnreadCount() const;\n\t[[nodiscard]] rpl::producer<std::optional<int>> unreadCountValue() const;\n\n\tvoid setInboxReadTill(MsgId readTillId, std::optional<int> unreadCount);\n\t[[nodiscard]] MsgId inboxReadTillId() const;\n\t[[nodiscard]] MsgId computeInboxReadTillFull() const;\n\n\tvoid setOutboxReadTill(MsgId readTillId);\n\t[[nodiscard]] MsgId computeOutboxReadTillFull() const;\n\n\t[[nodiscard]] bool isServerSideUnread(\n\t\tnot_null<const HistoryItem*> item) const;\n\n\tvoid requestUnreadCount();\n\n\tvoid readTill(not_null<HistoryItem*> item);\n\tvoid readTill(MsgId tillId);\n\n\t[[nodiscard]] bool canDeleteMyTopic() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tstruct Viewer;\n\n\tHistoryItem *lookupRoot();\n\t[[nodiscard]] Histories &histories();\n\n\tvoid subscribeToUpdates();\n\tvoid appendClientSideMessages(MessagesSlice &slice);\n\t[[nodiscard]] std::optional<int> computeUnreadCountLocally(\n\t\tMsgId afterId) const;\n\n\t[[nodiscard]] bool buildFromData(not_null<Viewer*> viewer);\n\t[[nodiscard]] bool applyItemDestroyed(\n\t\tnot_null<Viewer*> viewer,\n\t\tnot_null<HistoryItem*> item);\n\t[[nodiscard]] bool applyUpdate(const MessageUpdate &update);\n\tvoid applyTopicCreator(PeerId creatorId);\n\tvoid injectRootMessageAndReverse(not_null<Viewer*> viewer);\n\tvoid injectRootMessage(not_null<Viewer*> viewer);\n\tvoid injectRootDivider(\n\t\tnot_null<HistoryItem*> root,\n\t\tnot_null<MessagesSlice*> slice);\n\tbool processMessagesIsEmpty(const MTPmessages_Messages &result);\n\tvoid loadAround(MsgId id);\n\tvoid loadBefore();\n\tvoid loadAfter();\n\n\tvoid changeUnreadCountByPost(MsgId id, int delta);\n\tvoid setUnreadCount(std::optional<int> count);\n\tvoid readTill(MsgId tillId, HistoryItem *tillIdItem);\n\tvoid checkReadTillEnd();\n\tvoid sendReadTillRequest();\n\tvoid reloadUnreadCountIfNeeded();\n\n\tconst not_null<History*> _history;\n\tForumTopic *_owningTopic = nullptr;\n\tconst MsgId _rootId = 0;\n\tconst bool _creating = false;\n\n\tstd::vector<MsgId> _list;\n\tstd::optional<int> _skippedBefore;\n\tstd::optional<int> _skippedAfter;\n\trpl::variable<std::optional<int>> _fullCount;\n\trpl::event_stream<> _listChanges;\n\trpl::event_stream<> _instantChanges;\n\tstd::optional<MsgId> _loadingAround;\n\trpl::variable<std::optional<int>> _unreadCount;\n\tMsgId _inboxReadTillId = 0;\n\tMsgId _outboxReadTillId = 0;\n\tHistoryItem *_divider = nullptr;\n\tbool _dividerWithComments = false;\n\tint _beforeId = 0;\n\tint _afterId = 0;\n\n\tbase::Timer _readRequestTimer;\n\tmtpRequestId _readRequestId = 0;\n\n\tmtpRequestId _reloadUnreadCountRequestId = 0;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_reply_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_reply_preview.h\"\n\n#include \"data/data_file_origin.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"ui/image/image.h\"\n#include \"styles/style_chat.h\"\n\nnamespace Data {\n\nReplyPreview::ReplyPreview(not_null<DocumentData*> document)\n: _document(document) {\n}\n\nReplyPreview::ReplyPreview(not_null<PhotoData*> photo)\n: _photo(photo) {\n}\n\nReplyPreview::~ReplyPreview() = default;\n\nvoid ReplyPreview::prepare(\n\t\tnot_null<Image*> image,\n\t\tImages::Options options,\n\t\tbool spoiler) {\n\tusing namespace Images;\n\tif (image->isNull()) {\n\t\treturn;\n\t}\n\tint w = image->width(), h = image->height();\n\tif (w <= 0) w = 1;\n\tif (h <= 0) h = 1;\n\tauto thumbSize = (w > h)\n\t\t? QSize(\n\t\t\tw * st::historyReplyPreview / h,\n\t\t\tst::historyReplyPreview)\n\t\t: QSize(\n\t\t\tst::historyReplyPreview,\n\t\t\th * st::historyReplyPreview / w);\n\tthumbSize *= style::DevicePixelRatio();\n\toptions |= Option::TransparentBackground;\n\tauto outerSize = st::historyReplyPreview;\n\tauto original = spoiler\n\t\t? image->original().scaled(\n\t\t\t{ 40, 40 },\n\t\t\tQt::KeepAspectRatio,\n\t\t\tQt::SmoothTransformation)\n\t\t: image->original();\n\tauto prepared = Prepare(std::move(original), thumbSize, {\n\t\t.options = options | (spoiler ? Option::Blur : Option()),\n\t\t.outer = { outerSize, outerSize },\n\t});\n\t(spoiler ? _spoilered : _regular) = std::make_unique<Image>(\n\t\tstd::move(prepared));\n\t_good = spoiler || ((options & Option::Blur) == 0);\n}\n\nImage *ReplyPreview::image(\n\t\tData::FileOrigin origin,\n\t\tnot_null<PeerData*> context,\n\t\tbool spoiler) {\n\tauto &image = spoiler ? _spoilered : _regular;\n\tauto &checked = spoiler ? _checkedSpoilered : _checkedRegular;\n\tif (checked) {\n\t\treturn image.get();\n\t} else if (_document) {\n\t\tif (!image || (!_good && _document->hasThumbnail())) {\n\t\t\tif (!_documentMedia) {\n\t\t\t\t_documentMedia = _document->createMediaView();\n\t\t\t\t_documentMedia->thumbnailWanted(origin);\n\t\t\t}\n\t\t\tconst auto thumbnail = _documentMedia->thumbnail();\n\t\t\tconst auto option = _document->isVideoMessage()\n\t\t\t\t? Images::Option::RoundCircle\n\t\t\t\t: Images::Option::None;\n\t\t\tif (spoiler) {\n\t\t\t\tif (const auto image = _documentMedia->thumbnailInline()) {\n\t\t\t\t\tprepare(image, option, true);\n\t\t\t\t} else if (thumbnail) {\n\t\t\t\t\tprepare(thumbnail, option, true);\n\t\t\t\t}\n\t\t\t} else if (thumbnail) {\n\t\t\t\tprepare(thumbnail, option);\n\t\t\t} else if (!image) {\n\t\t\t\tif (const auto image = _documentMedia->thumbnailInline()) {\n\t\t\t\t\tprepare(image, option | Images::Option::Blur);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_good || !_document->hasThumbnail()) {\n\t\t\t\tchecked = true;\n\t\t\t\t_documentMedia = nullptr;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tAssert(_photo != nullptr);\n\t\tif (!image || !_good) {\n\t\t\tconst auto inlineThumbnailBytes = _photo->inlineThumbnailBytes();\n\t\t\tif (!_photoMedia) {\n\t\t\t\t_photoMedia = _photo->createMediaView();\n\t\t\t}\n\t\t\tusing Size = PhotoSize;\n\t\t\tconst auto loadThumbnail = inlineThumbnailBytes.isEmpty()\n\t\t\t\t|| (!spoiler\n\t\t\t\t\t&& _photoMedia->autoLoadThumbnailAllowed(context));\n\t\t\tif (loadThumbnail) {\n\t\t\t\t_photoMedia->wanted(Size::Small, origin);\n\t\t\t}\n\t\t\tif (spoiler) {\n\t\t\t\tif (const auto blurred = _photoMedia->thumbnailInline()) {\n\t\t\t\t\tprepare(blurred, {}, true);\n\t\t\t\t} else if (const auto small = _photoMedia->image(Size::Small)) {\n\t\t\t\t\tprepare(small, {}, true);\n\t\t\t\t} else if (const auto large = _photoMedia->image(Size::Large)) {\n\t\t\t\t\tprepare(large, {}, true);\n\t\t\t\t}\n\t\t\t} else if (const auto small = _photoMedia->image(Size::Small)) {\n\t\t\t\tprepare(small, {});\n\t\t\t} else if (const auto large = _photoMedia->image(Size::Large)) {\n\t\t\t\tprepare(large, {});\n\t\t\t} else if (!image) {\n\t\t\t\tif (const auto blurred = _photoMedia->thumbnailInline()) {\n\t\t\t\t\tprepare(blurred, Images::Option::Blur);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_good) {\n\t\t\t\tchecked = true;\n\t\t\t\t_photoMedia = nullptr;\n\t\t\t}\n\t\t}\n\t}\n\treturn image.get();\n}\n\nbool ReplyPreview::loaded(bool spoiler) const {\n\treturn spoiler ? _checkedSpoilered : _checkedRegular;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_reply_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass Image;\nclass DocumentData;\nclass PhotoData;\n\nnamespace Data {\n\nclass PhotoMedia;\nclass DocumentMedia;\nstruct FileOrigin;\n\nclass ReplyPreview {\npublic:\n\texplicit ReplyPreview(not_null<DocumentData*> document);\n\texplicit ReplyPreview(not_null<PhotoData*> photo);\n\t~ReplyPreview();\n\n\t[[nodiscard]] Image *image(\n\t\tData::FileOrigin origin,\n\t\tnot_null<PeerData*> context,\n\t\tbool spoiler);\n\t[[nodiscard]] bool loaded(bool spoiler) const;\n\nprivate:\n\tvoid prepare(\n\t\tnot_null<Image*> image,\n\t\tImages::Options options,\n\t\tbool spoiler = false);\n\n\tstd::unique_ptr<Image> _regular;\n\tstd::unique_ptr<Image> _spoilered;\n\tPhotoData *_photo = nullptr;\n\tDocumentData *_document = nullptr;\n\tstd::shared_ptr<PhotoMedia> _photoMedia;\n\tstd::shared_ptr<DocumentMedia> _documentMedia;\n\tbool _good = false;\n\tbool _checkedRegular = false;\n\tbool _checkedSpoilered = false;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_report.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\nstruct ReportInput final {\n\tQByteArray optionId;\n\tQString optionText;\n\tQString comment;\n\tstd::vector<MsgId> ids;\n\tstd::vector<StoryId> stories;\n\n\tinline bool operator==(const ReportInput &other) const {\n\t\treturn optionId == other.optionId && comment == other.comment;\n\t}\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_saved_messages.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_saved_messages.h\"\n\n#include \"apiwrap.h\"\n#include \"core/application.h\"\n#include \"data/components/recent_peers.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_user.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_unread_things.h\"\n#include \"main/main_session.h\"\n#include \"storage/storage_facade.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"window/notifications_manager.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kPerPage = 50;\nconstexpr auto kFirstPerPage = 10;\nconstexpr auto kListPerPage = 100;\nconstexpr auto kListFirstPerPage = 20;\nconstexpr auto kLoadedSublistsMinCount = 20;\nconstexpr auto kShowSublistNamesCount = 5;\nconstexpr auto kStalePerRequest = 100;\n\n} // namespace\n\nSavedMessages::SavedMessages(\n\tnot_null<Session*> owner,\n\tChannelData *parentChat)\n: _owner(owner)\n, _parentChat(parentChat)\n, _owningHistory(parentChat ? owner->history(parentChat).get() : nullptr)\n, _chatsList(\n\t&_owner->session(),\n\tFilterId(),\n\t_owner->maxPinnedChatsLimitValue(this))\n, _loadMore([=] { sendLoadMoreRequests(); }) {\n\t// We don't assign _owningHistory for my Saved Messages here,\n\t// because the data structures are not ready yet.\n\tif (_owningHistory && _owningHistory->inChatList()) {\n\t\tpreloadSublists();\n\t}\n}\n\nvoid SavedMessages::clear() {\n\tfor (const auto &request : base::take(_sublistRequests)) {\n\t\tif (request.second.id != _staleRequestId) {\n\t\t\towner().histories().cancelRequest(request.second.id);\n\t\t}\n\t}\n\tif (const auto requestId = base::take(_staleRequestId)) {\n\t\tsession().api().request(requestId).cancel();\n\t}\n\n\tauto &storage = session().storage();\n\tauto &changes = session().changes();\n\tif (_owningHistory) {\n\t\tfor (const auto &[peer, sublist] : base::take(_sublists)) {\n\t\t\tstorage.unload(Storage::SharedMediaUnloadThread(\n\t\t\t\t_owningHistory->peer->id,\n\t\t\t\tMsgId(),\n\t\t\t\tpeer->id));\n\t\t\t_owningHistory->setForwardDraft(MsgId(), peer->id, {});\n\n\t\t\tconst auto raw = sublist.get();\n\t\t\tchanges.sublistRemoved(raw);\n\t\t\tchanges.entryRemoved(raw);\n\t\t}\n\t}\n\t_owningHistory = nullptr;\n}\n\nvoid SavedMessages::saveActiveSubsectionThread(not_null<Thread*> thread) {\n\tif (const auto sublist = thread->asSublist()) {\n\t\tAssert(sublist->parent() == this);\n\t\t_activeSubsectionSublist = sublist;\n\t} else {\n\t\tAssert(thread == _owningHistory);\n\t\t_activeSubsectionSublist = nullptr;\n\t}\n}\n\nThread *SavedMessages::activeSubsectionThread() const {\n\treturn _activeSubsectionSublist;\n}\n\nSavedMessages::~SavedMessages() {\n\tclear();\n}\n\nbool SavedMessages::supported() const {\n\treturn !_unsupported;\n}\n\nvoid SavedMessages::markUnsupported() {\n\t_unsupported = true;\n}\n\nChannelData *SavedMessages::parentChat() const {\n\treturn _parentChat;\n}\n\nnot_null<History*> SavedMessages::owningHistory() const {\n\tif (!_owningHistory) {\n\t\tconst_cast<SavedMessages*>(this)->_owningHistory\n\t\t\t= _owner->history(_owner->session().user());\n\t}\n\treturn _owningHistory;\n}\n\nSession &SavedMessages::owner() const {\n\treturn *_owner;\n}\n\nMain::Session &SavedMessages::session() const {\n\treturn _owner->session();\n}\n\nnot_null<Dialogs::MainList*> SavedMessages::chatsList() {\n\treturn &_chatsList;\n}\n\nnot_null<SavedSublist*> SavedMessages::sublist(not_null<PeerData*> peer) {\n\tif (const auto loaded = sublistLoaded(peer)) {\n\t\treturn loaded;\n\t}\n\treturn _sublists.emplace(\n\t\tpeer,\n\t\tstd::make_unique<SavedSublist>(this, peer)).first->second.get();\n}\n\nSavedSublist *SavedMessages::sublistLoaded(not_null<PeerData*> peer) {\n\tconst auto i = _sublists.find(peer);\n\treturn (i != end(_sublists)) ? i->second.get() : nullptr;\n}\n\nvoid SavedMessages::requestSomeStale() {\n\tif (_staleRequestId\n\t\t|| (!_offset.id && _loadMoreRequestId)\n\t\t|| _stalePeers.empty()\n\t\t|| !_parentChat) {\n\t\treturn;\n\t}\n\tconst auto type = Histories::RequestType::History;\n\tauto peers = std::vector<not_null<PeerData*>>();\n\tauto peerIds = QVector<MTPInputPeer>();\n\tpeers.reserve(std::min(int(_stalePeers.size()), kStalePerRequest));\n\tpeerIds.reserve(std::min(int(_stalePeers.size()), kStalePerRequest));\n\tfor (auto i = begin(_stalePeers); i != end(_stalePeers);) {\n\t\tconst auto peer = *i;\n\t\ti = _stalePeers.erase(i);\n\n\t\tpeers.push_back(peer);\n\t\tpeerIds.push_back(peer->input());\n\t\tif (peerIds.size() == kStalePerRequest) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (peerIds.empty()) {\n\t\treturn;\n\t}\n\tconst auto call = [=] {\n\t\tfor (const auto &peer : peers) {\n\t\t\tfinishSublistRequest(peer);\n\t\t}\n\t\tfor (const auto &peer : peers) {\n\t\t\tif (const auto sublist = sublistLoaded(peer)) {\n\t\t\t\tif (!sublist->lastMessage()\n\t\t\t\t\t&& !sublist->lastServerMessage()) {\n\t\t\t\t\tapplySublistDeleted(peer);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\tauto &histories = owner().histories();\n\t_staleRequestId = histories.sendRequest(_owningHistory, type, [=](\n\t\t\tFn<void()> finish) {\n\t\tusing Flag = MTPmessages_GetSavedDialogsByID::Flag;\n\t\treturn session().api().request(\n\t\t\tMTPmessages_GetSavedDialogsByID(\n\t\t\t\tMTP_flags(Flag::f_parent_peer),\n\t\t\t\t_parentChat->input(),\n\t\t\t\tMTP_vector<MTPInputPeer>(peerIds))\n\t\t).done([=](const MTPmessages_SavedDialogs &result) {\n\t\t\t_staleRequestId = 0;\n\t\t\tapplyReceivedSublists(result);\n\t\t\tcall();\n\t\t\tfinish();\n\t\t}).fail([=] {\n\t\t\t_staleRequestId = 0;\n\t\t\tcall();\n\t\t\tfinish();\n\t\t}).send();\n\t});\n\tfor (const auto &peer : peers) {\n\t\t_sublistRequests[peer].id = _staleRequestId;\n\t}\n}\n\nvoid SavedMessages::finishSublistRequest(not_null<PeerData*> peer) {\n\tif (const auto request = _sublistRequests.take(peer)) {\n\t\tfor (const auto &callback : request->callbacks) {\n\t\t\tcallback();\n\t\t}\n\t}\n}\n\nvoid SavedMessages::requestSublist(\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> done) {\n\tif (!_parentChat) {\n\t\treturn;\n\t}\n\tauto &request = _sublistRequests[peer];\n\tif (done) {\n\t\trequest.callbacks.push_back(std::move(done));\n\t}\n\tif (!request.id\n\t\t&& _stalePeers.emplace(peer).second\n\t\t&& (_stalePeers.size() == 1)) {\n\t\tcrl::on_main(&session(), [peer = _parentChat] {\n\t\t\tif (const auto monoforum = peer->monoforum()) {\n\t\t\t\tmonoforum->requestSomeStale();\n\t\t\t}\n\t\t});\n\t}\n}\n\nvoid SavedMessages::refreshPinned() {\n\tif (parentChat()) {\n\t\treturn;\n\t}\n\tif (_pinnedRequestId) {\n\t\t_refreshPinnedAfterRequest = true;\n\t\treturn;\n\t}\n\tloadPinned();\n}\n\nrpl::producer<> SavedMessages::chatsListChanges() const {\n\treturn _chatsListChanges.events();\n}\n\nrpl::producer<> SavedMessages::chatsListLoadedEvents() const {\n\treturn _chatsListLoadedEvents.events();\n}\n\nvoid SavedMessages::preloadSublists() {\n\tif (parentChat()\n\t\t&& chatsList()->indexed()->size() < kLoadedSublistsMinCount) {\n\t\tloadMore();\n\t}\n}\n\nvoid SavedMessages::loadMore() {\n\t_loadMoreScheduled = true;\n\t_loadMore.call();\n}\n\nvoid SavedMessages::clearAllUnreadReactions() {\n\tfor (const auto &[peer, sublist] : _sublists) {\n\t\tsublist->unreadReactions().clear();\n\t}\n}\n\nvoid SavedMessages::sendLoadMore() {\n\tif (_loadMoreRequestId) {\n\t\treturn;\n\t}\n\tif (!_pinnedLoaded) {\n\t\tloadPinned();\n\t}\n\tif (_chatsList.loaded()) {\n\t\treturn;\n\t}\n\tusing Flag = MTPmessages_GetSavedDialogs::Flag;\n\t_loadMoreRequestId = _owner->session().api().request(\n\t\tMTPmessages_GetSavedDialogs(\n\t\t\tMTP_flags(Flag::f_exclude_pinned\n\t\t\t\t| (_parentChat ? Flag::f_parent_peer : Flag(0))),\n\t\t\t_parentChat ? _parentChat->input() : MTPInputPeer(),\n\t\t\tMTP_int(_offset.date),\n\t\t\tMTP_int(_offset.id),\n\t\t\t_offset.peer ? _offset.peer->input() : MTP_inputPeerEmpty(),\n\t\t\tMTP_int(_offset.id ? kListPerPage : kListFirstPerPage),\n\t\t\tMTP_long(0)) // hash\n\t).done([=](const MTPmessages_SavedDialogs &result) {\n\t\tconst auto applied = applyReceivedSublists(result);\n\t\tif (applied.allLoaded || _offset == applied.offset) {\n\t\t\t_chatsList.setLoaded();\n\t\t} else if (_offset.date > 0 && applied.offset.date > _offset.date) {\n\t\t\tLOG((\"API Error: Bad order in messages.savedDialogs.\"));\n\t\t\t_chatsList.setLoaded();\n\t\t} else {\n\t\t\t_offset = applied.offset;\n\t\t}\n\t\t_loadMoreRequestId = 0;\n\t\t_chatsListChanges.fire({});\n\t\tif (_chatsList.loaded()) {\n\t\t\t_chatsListLoadedEvents.fire({});\n\t\t}\n\t\treorderLastSublists();\n\t\trequestSomeStale();\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (error.type() == u\"SAVED_DIALOGS_UNSUPPORTED\"_q) {\n\t\t\tmarkUnsupported();\n\t\t}\n\t\t_chatsList.setLoaded();\n\t\t_loadMoreRequestId = 0;\n\t}).send();\n}\n\nvoid SavedMessages::loadPinned() {\n\tif (_pinnedRequestId || parentChat()) {\n\t\treturn;\n\t}\n\t_pinnedRequestId = _owner->session().api().request(\n\t\tMTPmessages_GetPinnedSavedDialogs()\n\t).done([=](const MTPmessages_SavedDialogs &result) {\n\t\t_pinnedRequestId = 0;\n\t\t_pinnedLoaded = true;\n\t\tapplyReceivedSublists(result, true);\n\t\t_chatsListChanges.fire({});\n\t\tif (_refreshPinnedAfterRequest) {\n\t\t\t_refreshPinnedAfterRequest = false;\n\t\t\tloadPinned();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (error.type() == u\"SAVED_DIALOGS_UNSUPPORTED\"_q) {\n\t\t\tmarkUnsupported();\n\t\t} else {\n\t\t\t_pinnedLoaded = true;\n\t\t}\n\t\t_pinnedRequestId = 0;\n\t\tif (_refreshPinnedAfterRequest) {\n\t\t\t_refreshPinnedAfterRequest = false;\n\t\t\tloadPinned();\n\t\t}\n\t}).send();\n}\n\nSavedMessages::ApplyResult SavedMessages::applyReceivedSublists(\n\t\tconst MTPmessages_SavedDialogs &dialogs,\n\t\tbool pinned) {\n\tauto list = (const QVector<MTPSavedDialog>*)nullptr;\n\tdialogs.match([](const MTPDmessages_savedDialogsNotModified &) {\n\t\tLOG((\"API Error: messages.savedDialogsNotModified.\"));\n\t}, [&](const auto &data) {\n\t\t_owner->processUsers(data.vusers());\n\t\t_owner->processChats(data.vchats());\n\t\t_owner->processMessages(\n\t\t\tdata.vmessages(),\n\t\t\tNewMessageType::Existing);\n\t\tlist = &data.vdialogs().v;\n\t});\n\tif (!list) {\n\t\treturn { .allLoaded = true };\n\t}\n\tauto lastValid = false;\n\tauto result = ApplyResult();\n\tauto serverPinnedPeers = base::flat_set<not_null<PeerData*>>();\n\tconst auto parentPeerId = _parentChat\n\t\t? _parentChat->id\n\t\t: _owner->session().userPeerId();\n\tfor (const auto &dialog : *list) {\n\t\tdialog.match([&](const MTPDsavedDialog &data) {\n\t\t\tconst auto peer = _owner->peer(peerFromMTP(data.vpeer()));\n\t\t\tconst auto entryPinned = pinned || data.is_pinned();\n\t\t\tconst auto topId = MsgId(data.vtop_message().v);\n\t\t\tif (entryPinned) {\n\t\t\t\tserverPinnedPeers.emplace(peer);\n\t\t\t}\n\t\t\tif (entryPinned) {\n\t\t\t\tif (const auto loaded = sublistLoaded(peer)) {\n\t\t\t\t\t_owner->setPinnedFromEntryList(loaded, true);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (const auto item = _owner->message(parentPeerId, topId);\n\t\t\t\titem\n\t\t\t\t&& item->isRegular()\n\t\t\t\t&& !item->isService()) {\n\t\t\t\tresult.offset.peer = peer;\n\t\t\t\tresult.offset.date = item->date();\n\t\t\t\tresult.offset.id = topId;\n\t\t\t\tlastValid = true;\n\t\t\t\tconst auto entry = sublist(peer);\n\t\t\t\tentry->applyMaybeLast(item);\n\t\t\t\t_owner->setPinnedFromEntryList(entry, entryPinned);\n\t\t\t} else {\n\t\t\t\tlastValid = false;\n\t\t\t}\n\t\t}, [&](const MTPDmonoForumDialog &data) {\n\t\t\tconst auto peer = _owner->peer(peerFromMTP(data.vpeer()));\n\t\t\tif (pinned) {\n\t\t\t\tserverPinnedPeers.emplace(peer);\n\t\t\t}\n\t\t\tconst auto topId = MsgId(data.vtop_message().v);\n\t\t\tif (const auto item = _owner->message(parentPeerId, topId);\n\t\t\t\titem\n\t\t\t\t&& item->isRegular()\n\t\t\t\t&& !item->isService()) {\n\t\t\t\tresult.offset.peer = peer;\n\t\t\t\tresult.offset.date = item->date();\n\t\t\t\tresult.offset.id = topId;\n\t\t\t\tlastValid = true;\n\t\t\t\tsublist(peer)->applyMonoforumDialog(data, item);\n\t\t\t} else {\n\t\t\t\tlastValid = false;\n\t\t\t}\n\t\t});\n\t}\n\tif (pinned) {\n\t\tfor (const auto &[peer, holder] : _sublists) {\n\t\t\tconst auto entry = holder.get();\n\t\t\tif (entry->isPinnedDialog(FilterId())\n\t\t\t\t&& !serverPinnedPeers.contains(peer)) {\n\t\t\t\tif (!entry->parentChat() && !entry->chatListMessage()) {\n\t\t\t\t\tentry->setRestorePinnedWhenNonEmpty(true);\n\t\t\t\t}\n\t\t\t\t_owner->setChatPinned(entry, FilterId(), false);\n\t\t\t}\n\t\t\tentry->updateChatListExistence();\n\t\t}\n\t} else if (!lastValid) {\n\t\tLOG((\"API Error: Unknown message in the end of a slice.\"));\n\t\tresult.allLoaded = true;\n\t} else if (dialogs.type() == mtpc_messages_savedDialogs) {\n\t\tresult.allLoaded = true;\n\t}\n\tif (!_stalePeers.empty()) {\n\t\trequestSomeStale();\n\t}\n\treturn result;\n}\n\nvoid SavedMessages::sendLoadMoreRequests() {\n\tif (_loadMoreScheduled) {\n\t\tsendLoadMore();\n\t}\n}\n\nvoid SavedMessages::apply(const MTPDupdatePinnedSavedDialogs &update) {\n\tExpects(!parentChat());\n\n\tconst auto list = update.vorder();\n\tif (!list) {\n\t\tloadPinned();\n\t\treturn;\n\t}\n\tconst auto &order = list->v;\n\tconst auto notLoaded = [&](const MTPDialogPeer &peer) {\n\t\treturn peer.match([&](const MTPDdialogPeer &data) {\n\t\t\tconst auto peer = _owner->peer(peerFromMTP(data.vpeer()));\n\t\t\treturn !_sublists.contains(peer);\n\t\t}, [&](const MTPDdialogPeerFolder &data) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"updatePinnedSavedDialogs has folders.\"));\n\t\t\treturn false;\n\t\t});\n\t};\n\tif (!ranges::none_of(order, notLoaded)) {\n\t\tloadPinned();\n\t} else {\n\t\t_chatsList.pinned()->applyList(this, order);\n\t\t_owner->notifyPinnedDialogsOrderUpdated();\n\t}\n}\n\nvoid SavedMessages::apply(const MTPDupdateSavedDialogPinned &update) {\n\tExpects(!parentChat());\n\n\tupdate.vpeer().match([&](const MTPDdialogPeer &data) {\n\t\tconst auto peer = _owner->peer(peerFromMTP(data.vpeer()));\n\t\tconst auto i = _sublists.find(peer);\n\t\tif (i != end(_sublists)) {\n\t\t\tconst auto entry = i->second.get();\n\t\t\t_owner->setChatPinned(entry, FilterId(), update.is_pinned());\n\t\t} else {\n\t\t\tloadPinned();\n\t\t}\n\t}, [&](const MTPDdialogPeerFolder &data) {\n\t\tDEBUG_LOG((\"API Error: Folder in updateSavedDialogPinned.\"));\n\t});\n}\n\nvoid SavedMessages::applySublistDeleted(not_null<PeerData*> sublistPeer) {\n\tconst auto i = _sublists.find(sublistPeer);\n\tif (i == end(_sublists)) {\n\t\treturn;\n\t}\n\tconst auto raw = i->second.get();\n\tCore::App().notifications().clearFromSublist(raw);\n\towner().removeChatListEntry(raw);\n\n\tif (ranges::contains(_lastSublists, not_null(raw))) {\n\t\treorderLastSublists();\n\t}\n\tif (_activeSubsectionSublist == raw) {\n\t\t_activeSubsectionSublist = nullptr;\n\t}\n\n\t_sublistDestroyed.fire(raw);\n\t_owner->session().recentPeers().chatOpenRemove(raw);\n\tsession().changes().sublistUpdated(\n\t\traw,\n\t\tData::SublistUpdate::Flag::Destroyed);\n\tsession().changes().entryUpdated(\n\t\traw,\n\t\tData::EntryUpdate::Flag::Destroyed);\n\t_sublists.erase(i);\n\n\tconst auto history = owningHistory();\n\thistory->destroyMessagesBySublist(sublistPeer);\n\tsession().storage().unload(Storage::SharedMediaUnloadThread(\n\t\t_owningHistory->peer->id,\n\t\tMsgId(),\n\t\tsublistPeer->id));\n\thistory->setForwardDraft(MsgId(), sublistPeer->id, {});\n}\n\nvoid SavedMessages::reorderLastSublists() {\n\tif (!_parentChat) {\n\t\treturn;\n\t}\n\n\t// We want first kShowChatNamesCount histories, by last message date.\n\tconst auto pred = [](\n\t\t\tnot_null<SavedSublist*> a,\n\t\t\tnot_null<SavedSublist*> b) {\n\t\tconst auto aItem = a->chatListMessage();\n\t\tconst auto bItem = b->chatListMessage();\n\t\tconst auto aDate = aItem ? aItem->date() : TimeId(0);\n\t\tconst auto bDate = bItem ? bItem->date() : TimeId(0);\n\t\treturn aDate > bDate;\n\t};\n\t_lastSublists.clear();\n\t_lastSublists.reserve(kShowSublistNamesCount + 1);\n\tauto &&sublists = ranges::views::all(\n\t\t*_chatsList.indexed()\n\t) | ranges::views::transform([](not_null<Dialogs::Row*> row) {\n\t\treturn row->sublist();\n\t});\n\tauto nonPinnedChecked = 0;\n\tfor (const auto sublist : sublists) {\n\t\tconst auto i = ranges::upper_bound(\n\t\t\t_lastSublists,\n\t\t\tnot_null(sublist),\n\t\t\tpred);\n\t\tif (size(_lastSublists) < kShowSublistNamesCount\n\t\t\t|| i != end(_lastSublists)) {\n\t\t\t_lastSublists.insert(i, sublist);\n\t\t}\n\t\tif (size(_lastSublists) > kShowSublistNamesCount) {\n\t\t\t_lastSublists.pop_back();\n\t\t}\n\t\tif (!sublist->isPinnedDialog(FilterId())\n\t\t\t&& ++nonPinnedChecked >= kShowSublistNamesCount) {\n\t\t\tbreak;\n\t\t}\n\t}\n\t++_lastSublistsVersion;\n\towningHistory()->updateChatListEntry();\n}\n\nvoid SavedMessages::listMessageChanged(HistoryItem *from, HistoryItem *to) {\n\tif (from || to) {\n\t\treorderLastSublists();\n\t}\n}\n\nint SavedMessages::recentSublistsListVersion() const {\n\treturn _lastSublistsVersion;\n}\n\nvoid SavedMessages::recentSublistsInvalidate(\n\t\tnot_null<SavedSublist*> sublist) {\n\tExpects(_parentChat != nullptr);\n\n\tif (ranges::contains(_lastSublists, sublist)) {\n\t\t++_lastSublistsVersion;\n\t\towningHistory()->updateChatListEntry();\n\t}\n}\n\nauto SavedMessages::recentSublists() const\n-> const std::vector<not_null<SavedSublist*>> & {\n\treturn _lastSublists;\n}\n\nvoid SavedMessages::markUnreadCountsUnknown(MsgId readTillId) {\n\tfor (const auto &[peer, sublist] : _sublists) {\n\t\tif (sublist->unreadCountCurrent() > 0) {\n\t\t\tsublist->setInboxReadTill(readTillId, std::nullopt);\n\t\t}\n\t}\n}\n\nvoid SavedMessages::updateUnreadCounts(\n\t\tMsgId readTillId,\n\t\tconst base::flat_map<not_null<SavedSublist*>, int> &counts) {\n\tfor (const auto &[peer, sublist] : _sublists) {\n\t\tconst auto raw = sublist.get();\n\t\tconst auto i = counts.find(raw);\n\t\tconst auto count = (i != end(counts)) ? i->second : 0;\n\t\tif (raw->unreadCountCurrent() != count) {\n\t\t\traw->setInboxReadTill(readTillId, count);\n\t\t}\n\t}\n}\n\nrpl::producer<> SavedMessages::destroyed() const {\n\tif (!_parentChat) {\n\t\treturn rpl::never<>();\n\t}\n\treturn _parentChat->flagsValue(\n\t) | rpl::filter([=](const ChannelData::Flags::Change &update) {\n\t\tusing Flag = ChannelData::Flag;\n\t\treturn (update.diff & Flag::MonoforumAdmin)\n\t\t\t&& !(update.value & Flag::MonoforumAdmin);\n\t}) | rpl::take(1) | rpl::to_empty;\n}\n\nauto SavedMessages::sublistDestroyed() const\n-> rpl::producer<not_null<SavedSublist*>> {\n\treturn _sublistDestroyed.events();\n}\n\nrpl::lifetime &SavedMessages::lifetime() {\n\treturn _lifetime;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_saved_messages.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"dialogs/dialogs_main_list.h\"\n\nnamespace Dialogs {\nstruct UnreadState;\n} // namespace Dialogs\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nclass Session;\nclass SavedSublist;\n\nstruct SavedMessagesOffsets {\n\tTimeId date = 0;\n\tMsgId id = 0;\n\tPeerData *peer = nullptr;\n\n\tfriend inline constexpr auto operator<=>(\n\t\tSavedMessagesOffsets,\n\t\tSavedMessagesOffsets) = default;\n};\n\nclass SavedMessages final {\npublic:\n\texplicit SavedMessages(\n\t\tnot_null<Session*> owner,\n\t\tChannelData *parentChat = nullptr);\n\t~SavedMessages();\n\n\t[[nodiscard]] bool supported() const;\n\tvoid markUnsupported();\n\n\t[[nodiscard]] ChannelData *parentChat() const;\n\t[[nodiscard]] not_null<History*> owningHistory() const;\n\n\t[[nodiscard]] Session &owner() const;\n\t[[nodiscard]] Main::Session &session() const;\n\n\t[[nodiscard]] not_null<Dialogs::MainList*> chatsList();\n\t[[nodiscard]] not_null<SavedSublist*> sublist(not_null<PeerData*> peer);\n\t[[nodiscard]] SavedSublist *sublistLoaded(not_null<PeerData*> peer);\n\tvoid requestSublist(not_null<PeerData*> peer, Fn<void()> done = nullptr);\n\tvoid refreshPinned();\n\n\t[[nodiscard]] rpl::producer<> chatsListChanges() const;\n\t[[nodiscard]] rpl::producer<> chatsListLoadedEvents() const;\n\n\t[[nodiscard]] rpl::producer<> destroyed() const;\n\t[[nodiscard]] auto sublistDestroyed() const\n\t\t-> rpl::producer<not_null<SavedSublist*>>;\n\n\tvoid preloadSublists();\n\tvoid loadMore();\n\tvoid clearAllUnreadReactions();\n\n\tvoid apply(const MTPDupdatePinnedSavedDialogs &update);\n\tvoid apply(const MTPDupdateSavedDialogPinned &update);\n\tvoid applySublistDeleted(not_null<PeerData*> sublistPeer);\n\n\tvoid listMessageChanged(HistoryItem *from, HistoryItem *to);\n\t[[nodiscard]] int recentSublistsListVersion() const;\n\tvoid recentSublistsInvalidate(not_null<SavedSublist*> sublist);\n\t[[nodiscard]] auto recentSublists() const\n\t\t-> const std::vector<not_null<SavedSublist*>> &;\n\n\tvoid markUnreadCountsUnknown(MsgId readTillId);\n\tvoid updateUnreadCounts(\n\t\tMsgId readTillId,\n\t\tconst base::flat_map<not_null<SavedSublist*>, int> &counts);\n\n\tvoid clear();\n\n\tvoid saveActiveSubsectionThread(not_null<Thread*> thread);\n\tThread *activeSubsectionThread() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tstruct SublistRequest {\n\t\tmtpRequestId id = 0;\n\t\tstd::vector<Fn<void()>> callbacks;\n\t};\n\tstruct ApplyResult {\n\t\tSavedMessagesOffsets offset;\n\t\tbool allLoaded = false;\n\t};\n\n\tvoid loadPinned();\n\tApplyResult applyReceivedSublists(\n\t\tconst MTPmessages_SavedDialogs &result,\n\t\tSavedMessagesOffsets &updateOffsets);\n\tApplyResult applyReceivedSublists(\n\t\tconst MTPmessages_SavedDialogs &result,\n\t\tbool pinned = false);\n\n\tvoid reorderLastSublists();\n\tvoid requestSomeStale();\n\tvoid finishSublistRequest(not_null<PeerData*> peer);\n\n\tvoid sendLoadMore();\n\tvoid sendLoadMoreRequests();\n\n\tconst not_null<Session*> _owner;\n\tChannelData *_parentChat = nullptr;\n\tHistory *_owningHistory = nullptr;\n\n\trpl::event_stream<not_null<SavedSublist*>> _sublistDestroyed;\n\n\tDialogs::MainList _chatsList;\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tstd::unique_ptr<SavedSublist>> _sublists;\n\tbase::flat_map<not_null<PeerData*>, SublistRequest> _sublistRequests;\n\tbase::flat_set<not_null<PeerData*>> _stalePeers;\n\tmtpRequestId _staleRequestId = 0;\n\n\tmtpRequestId _loadMoreRequestId = 0;\n\tmtpRequestId _pinnedRequestId = 0;\n\tbool _refreshPinnedAfterRequest = false;\n\n\tSavedMessagesOffsets _offset;\n\n\tSingleQueuedInvokation _loadMore;\n\tbool _loadMoreScheduled = false;\n\n\tstd::vector<not_null<SavedSublist*>> _lastSublists;\n\tint _lastSublistsVersion = 0;\n\n\trpl::event_stream<> _chatsListChanges;\n\trpl::event_stream<> _chatsListLoadedEvents;\n\n\tSavedSublist *_activeSubsectionSublist = nullptr;\n\n\tbool _pinnedLoaded = false;\n\tbool _unsupported = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_saved_music.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_saved_music.h\"\n\n#include \"api/api_hash.h\"\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n#include \"ui/ui_utility.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kPerPage = 50;\nconstexpr auto kReloadIdsEvery = 30 * crl::time(1000);\n\n[[nodiscard]] not_null<DocumentData*> ItemDocument(\n\t\tnot_null<HistoryItem*> item) {\n\treturn item->media()->document();\n}\n\n} // namespace\n\nSavedMusic::SavedMusic(not_null<Session*> owner)\n: _owner(owner) {\n}\n\nSavedMusic::~SavedMusic() {\n\tExpects(_entries.empty());\n}\n\nvoid SavedMusic::clear() {\n\tbase::take(_entries);\n}\n\nbool SavedMusic::Supported(PeerId peerId) {\n\treturn peerId && peerIsUser(peerId);\n}\n\nnot_null<HistoryItem*> SavedMusic::musicIdToMsg(\n\t\tPeerId peerId,\n\t\tEntry &entry,\n\t\tnot_null<DocumentData*> id) {\n\tconst auto i = entry.musicIdToMsg.find(id);\n\tif (i != end(entry.musicIdToMsg)) {\n\t\treturn i->second.get();\n\t} else if (!entry.history) {\n\t\tentry.history = _owner->history(peerId);\n\t}\n\treturn entry.musicIdToMsg.emplace(id, entry.history->makeMessage({\n\t\t.id = entry.history->nextNonHistoryEntryId(),\n\t\t.flags = (MessageFlag::FakeHistoryItem\n\t\t\t| MessageFlag::HasFromId\n\t\t\t| MessageFlag::SavedMusicItem),\n\t\t.from = entry.history->peer->id,\n\t\t.date = base::unixtime::now(),\n\t}, id, TextWithEntities())).first->second.get();\n}\n\nvoid SavedMusic::loadIds() {\n\tif (_loadIdsRequest\n\t\t|| (_lastReceived\n\t\t\t&& (crl::now() - _lastReceived < kReloadIdsEvery))) {\n\t\treturn;\n\t}\n\t_loadIdsRequest = _owner->session().api().request(\n\t\tMTPaccount_GetSavedMusicIds(MTP_long(Api::CountHash(_myIds)))\n\t).done([=](const MTPaccount_SavedMusicIds &result) {\n\t\t_loadIdsRequest = 0;\n\t\t_lastReceived = crl::now();\n\t\tresult.match([&](const MTPDaccount_savedMusicIds &data) {\n\t\t\t_myIds = data.vids().v\n\t\t\t\t| ranges::views::transform(&MTPlong::v)\n\t\t\t\t| ranges::to_vector;\n\t\t}, [](const MTPDaccount_savedMusicIdsNotModified &) {\n\t\t});\n\t}).fail([=] {\n\t\t_loadIdsRequest = 0;\n\t\t_lastReceived = crl::now();\n\t}).send();\n}\n\nbool SavedMusic::has(not_null<DocumentData*> document) const {\n\treturn ranges::contains(_myIds, document->id);\n}\n\nvoid SavedMusic::save(\n\t\tnot_null<DocumentData*> document,\n\t\tData::FileOrigin origin) {\n\tconst auto peerId = _owner->session().userPeerId();\n\tauto &entry = _entries[peerId];\n\tif (entry.list.empty() && !entry.loaded) {\n\t\tloadMore(peerId);\n\t}\n\tif (has(document)) {\n\t\treturn;\n\t}\n\tconst auto item = musicIdToMsg(peerId, entry, document);\n\tentry.list.insert(begin(entry.list), item);\n\tif (entry.total >= 0) {\n\t\t++entry.total;\n\t}\n\t_myIds.insert(begin(_myIds), document->id);\n\n\tconst auto send = [=](auto resend) -> void {\n\t\tconst auto usedFileReference = document->fileReference();\n\t\t_owner->session().api().request(MTPaccount_SaveMusic(\n\t\t\tMTP_flags(0),\n\t\t\tdocument->mtpInput(),\n\t\t\tMTPInputDocument()\n\t\t)).fail([=](const MTP::Error &error) {\n\t\t\tif (error.code() == 400\n\t\t\t\t&& error.type().startsWith(u\"FILE_REFERENCE_\"_q)) {\n\t\t\t\tdocument->session().api().refreshFileReference(origin, [=](\n\t\t\t\t\t\tconst auto &) {\n\t\t\t\t\tif (document->fileReference() != usedFileReference) {\n\t\t\t\t\t\tresend(resend);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t}).send();\n\t};\n\tsend(send);\n\n\t_changed.fire_copy(peerId);\n}\n\nvoid SavedMusic::remove(not_null<DocumentData*> document) {\n\tconst auto peerId = _owner->session().userPeerId();\n\tauto &entry = _entries[peerId];\n\tconst auto i = ranges::find(entry.list, document, ItemDocument);\n\tif (i != end(entry.list)) {\n\t\tentry.musicIdFromMsgId.remove((*i)->id);\n\t\tentry.list.erase(i);\n\t\tif (entry.total > 0) {\n\t\t\tentry.total = std::max(entry.total - 1, 0);\n\t\t}\n\t}\n\tentry.musicIdToMsg.remove(document);\n\t_myIds.erase(ranges::remove(_myIds, document->id), end(_myIds));\n\t_owner->session().api().request(MTPaccount_SaveMusic(\n\t\tMTP_flags(MTPaccount_SaveMusic::Flag::f_unsave),\n\t\tdocument->mtpInput(),\n\t\tMTPInputDocument()\n\t)).send();\n\t_changed.fire_copy(peerId);\n}\n\nvoid SavedMusic::reorder(int oldPosition, int newPosition) {\n\tconst auto peerId = _owner->session().userPeerId();\n\tauto &entry = _entries[peerId];\n\tif (oldPosition < 0 || newPosition < 0\n\t\t|| oldPosition >= entry.list.size()\n\t\t|| newPosition >= entry.list.size()\n\t\t|| oldPosition == newPosition) {\n\t\treturn;\n\t}\n\n\tconst auto item = entry.list[oldPosition];\n\tconst auto document = ItemDocument(item);\n\n\tbase::reorder(entry.list, oldPosition, newPosition);\n\n\tconst auto afterDocument = (newPosition > 0)\n\t\t? ItemDocument(entry.list[newPosition - 1]).get()\n\t\t: nullptr;\n\n\t_owner->session().api().request(MTPaccount_SaveMusic(\n\t\tMTP_flags(afterDocument\n\t\t\t? MTPaccount_SaveMusic::Flag::f_after_id\n\t\t\t: MTPaccount_SaveMusic::Flags(0)),\n\t\tdocument->mtpInput(),\n\t\tafterDocument ? afterDocument->mtpInput() : MTPInputDocument()\n\t)).done([=] {\n\t}).fail([=](const MTP::Error &error) {\n\t}).send();\n\n\t_changed.fire_copy(peerId);\n}\n\nvoid SavedMusic::apply(not_null<UserData*> user, const MTPDocument *last) {\n\tconst auto peerId = user->id;\n\tauto &entry = _entries[peerId];\n\tif (!last) {\n\t\tif (const auto requestId = base::take(entry.requestId)) {\n\t\t\t_owner->session().api().request(requestId).cancel();\n\t\t}\n\t\tentry = Entry{ .total = 0, .loaded = true };\n\t\t_changed.fire_copy(peerId);\n\t\treturn;\n\t}\n\tconst auto document = _owner->processDocument(*last);\n\tconst auto i = ranges::find(entry.list, document, ItemDocument);\n\tif (i != end(entry.list)) {\n\t\tif (i == begin(entry.list)) {\n\t\t\treturn;\n\t\t}\n\t\tranges::rotate(begin(entry.list), i, i + 1);\n\t\t_changed.fire_copy(peerId);\n\t\tloadMore(peerId, true);\n\t\treturn;\n\t}\n\tentry.list.insert(\n\t\tbegin(entry.list),\n\t\tmusicIdToMsg(peerId, entry, document));\n\t_changed.fire_copy(peerId);\n\tif (entry.loaded) {\n\t\tloadMore(peerId, true);\n\t}\n}\n\nbool SavedMusic::countKnown(PeerId peerId) const {\n\tif (!Supported(peerId)) {\n\t\treturn true;\n\t}\n\tconst auto entry = lookupEntry(peerId);\n\treturn entry && entry->total >= 0;\n}\n\nint SavedMusic::count(PeerId peerId) const {\n\tif (!Supported(peerId)) {\n\t\treturn 0;\n\t}\n\tconst auto entry = lookupEntry(peerId);\n\treturn entry ? std::max(entry->total, 0) : 0;\n}\n\nconst std::vector<not_null<HistoryItem*>> &SavedMusic::list(\n\t\tPeerId peerId) const {\n\tstatic const auto empty = std::vector<not_null<HistoryItem*>>();\n\tif (!Supported(peerId)) {\n\t\treturn empty;\n\t}\n\n\tconst auto entry = lookupEntry(peerId);\n\treturn entry ? entry->list : empty;\n}\n\nvoid SavedMusic::loadMore(PeerId peerId) {\n\tloadMore(peerId, false);\n}\n\nvoid SavedMusic::loadMore(PeerId peerId, bool reload) {\n\tif (!Supported(peerId)) {\n\t\treturn;\n\t}\n\n\tauto &entry = _entries[peerId];\n\tif (!entry.reloading && reload) {\n\t\t_owner->session().api().request(\n\t\t\tbase::take(entry.requestId)).cancel();\n\t}\n\tif ((!reload && entry.loaded) || entry.requestId) {\n\t\treturn;\n\t}\n\tconst auto user = _owner->peer(peerId)->asUser();\n\tAssert(user != nullptr);\n\n\tentry.reloading = reload;\n\tentry.requestId = _owner->session().api().request(MTPusers_GetSavedMusic(\n\t\tuser->inputUser(),\n\t\tMTP_int(reload ? 0 : entry.list.size()),\n\t\tMTP_int(kPerPage),\n\t\tMTP_long(reload ? firstPageHash(entry) : 0)\n\t)).done([=](const MTPusers_SavedMusic &result) {\n\t\tauto &entry = _entries[peerId];\n\t\tentry.requestId = 0;\n\t\tconst auto reloaded = base::take(entry.reloading);\n\t\tresult.match([&](const MTPDusers_savedMusicNotModified &) {\n\t\t}, [&](const MTPDusers_savedMusic &data) {\n\t\t\tconst auto list = data.vdocuments().v;\n\t\t\tconst auto count = int(list.size());\n\t\t\tentry.total = std::max(count, data.vcount().v);\n\t\t\tif (reloaded) {\n\t\t\t\tentry.list.clear();\n\t\t\t}\n\t\t\tfor (const auto &item : list) {\n\t\t\t\tconst auto document = _owner->processDocument(item);\n\t\t\t\tif (!ranges::contains(entry.list, document, ItemDocument)) {\n\t\t\t\t\tentry.list.push_back(\n\t\t\t\t\t\tmusicIdToMsg(peerId, entry, document));\n\t\t\t\t}\n\t\t\t}\n\t\t\tentry.loaded = list.empty()\n\t\t\t\t|| (entry.total == entry.list.size());\n\t\t});\n\t\t_changed.fire_copy(peerId);\n\t}).fail([=](const MTP::Error &error) {\n\t\tauto &entry = _entries[peerId];\n\t\tentry.requestId = 0;\n\t\tentry.total = int(entry.list.size());\n\t\tentry.loaded = true;\n\t\t_changed.fire_copy(peerId);\n\t}).send();\n}\n\nuint64 SavedMusic::firstPageHash(const Entry &entry) const {\n\treturn Api::CountHash(entry.list\n\t\t| ranges::views::transform(ItemDocument)\n\t\t| ranges::views::transform(&DocumentData::id)\n\t\t| ranges::views::take(kPerPage));\n}\n\nrpl::producer<PeerId> SavedMusic::changed() const {\n\treturn _changed.events();\n}\n\nSavedMusic::Entry *SavedMusic::lookupEntry(PeerId peerId) {\n\tif (!Supported(peerId)) {\n\t\treturn nullptr;\n\t}\n\n\tauto it = _entries.find(peerId);\n\tif (it == end(_entries)) {\n\t\treturn nullptr;\n\t}\n\treturn &it->second;\n}\n\nconst SavedMusic::Entry *SavedMusic::lookupEntry(PeerId peerId) const {\n\treturn const_cast<SavedMusic*>(this)->lookupEntry(peerId);\n}\n\nrpl::producer<SavedMusicSlice> SavedMusicList(\n\t\tnot_null<PeerData*> peer,\n\t\tHistoryItem *aroundId,\n\t\tint limit) {\n\tif (!peer->isUser()) {\n\t\treturn rpl::single(SavedMusicSlice({}, 0, 0, 0));\n\t}\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tstruct State {\n\t\t\tSavedMusicSlice slice;\n\t\t\tbase::has_weak_ptr guard;\n\t\t\tbool scheduled = false;\n\t\t};\n\t\tconst auto state = lifetime.make_state<State>();\n\n\t\tconst auto push = [=] {\n\t\t\tstate->scheduled = false;\n\n\t\t\tconst auto peerId = peer->id;\n\t\t\tconst auto savedMusic = &peer->owner().savedMusic();\n\t\t\tif (!savedMusic->countKnown(peerId)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto &loaded = savedMusic->list(peerId);\n\t\t\tconst auto count = savedMusic->count(peerId);\n\t\t\tauto i = aroundId\n\t\t\t\t? ranges::find(loaded, not_null(aroundId))\n\t\t\t\t: begin(loaded);\n\t\t\tif (i == end(loaded)) {\n\t\t\t\ti = begin(loaded);\n\t\t\t}\n\t\t\tconst auto hasAfter = int(i - begin(loaded));\n\t\t\tconst auto hasBefore = int(end(loaded) - i);\n\t\t\tif (hasBefore < limit) {\n\t\t\t\tsavedMusic->loadMore(peerId);\n\t\t\t}\n\t\t\tconst auto takeAfter = std::min(hasAfter, limit);\n\t\t\tconst auto takeBefore = std::min(hasBefore, limit);\n\t\t\tauto ids = std::vector<not_null<HistoryItem*>>();\n\t\t\tids.reserve(takeAfter + takeBefore);\n\t\t\tfor (auto j = i - takeAfter; j != i + takeBefore; ++j) {\n\t\t\t\tids.push_back(*j);\n\t\t\t}\n\t\t\tconst auto added = int(ids.size());\n\t\t\tstate->slice = SavedMusicSlice(\n\t\t\t\tstd::move(ids),\n\t\t\t\tcount,\n\t\t\t\tcount - (hasAfter - takeAfter) - added,\n\t\t\t\thasAfter - takeAfter);\n\t\t\tconsumer.put_next_copy(state->slice);\n\t\t};\n\t\tconst auto schedule = [=] {\n\t\t\tif (state->scheduled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->scheduled = true;\n\t\t\tUi::PostponeCall(&state->guard, [=] {\n\t\t\t\tif (state->scheduled) {\n\t\t\t\t\tpush();\n\t\t\t\t}\n\t\t\t});\n\t\t};\n\n\t\tconst auto peerId = peer->id;\n\t\tconst auto savedMusic = &peer->owner().savedMusic();\n\t\tsavedMusic->changed(\n\t\t) | rpl::filter(\n\t\t\trpl::mappers::_1 == peerId\n\t\t) | rpl::on_next(schedule, lifetime);\n\n\t\tif (!savedMusic->countKnown(peerId)) {\n\t\t\tsavedMusic->loadMore(peerId);\n\t\t}\n\n\t\tpush();\n\n\t\treturn lifetime;\n\t};\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_saved_music.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_abstract_sparse_ids.h\"\n\n#include \"history/history_item.h\"\n\nclass PeerData;\n\nnamespace Data {\n\nclass Session;\nstruct FileOrigin;\n\nclass SavedMusic final {\npublic:\n\texplicit SavedMusic(not_null<Session*> owner);\n\t~SavedMusic();\n\n\t[[nodiscard]] static bool Supported(PeerId peerId);\n\n\t[[nodiscard]] bool countKnown(PeerId peerId) const;\n\t[[nodiscard]] int count(PeerId peerId) const;\n\t[[nodiscard]] const std::vector<not_null<HistoryItem*>> &list(\n\t\tPeerId peerId) const;\n\tvoid loadMore(PeerId peerId);\n\n\t[[nodiscard]] rpl::producer<PeerId> changed() const;\n\n\tvoid loadIds();\n\t[[nodiscard]] bool has(not_null<DocumentData*> document) const;\n\tvoid save(not_null<DocumentData*> document, FileOrigin origin);\n\tvoid remove(not_null<DocumentData*> document);\n\tvoid reorder(int oldPosition, int newPosition);\n\n\tvoid apply(not_null<UserData*> user, const MTPDocument *last);\n\n\tvoid clear();\n\nprivate:\n\tusing OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;\n\n\tstruct Entry {\n\t\tbase::flat_map<MsgId, not_null<DocumentData*>> musicIdFromMsgId;\n\t\tbase::flat_map<not_null<DocumentData*>, OwnedItem> musicIdToMsg;\n\t\tstd::vector<not_null<HistoryItem*>> list;\n\t\tHistory *history = nullptr;\n\t\tmtpRequestId requestId = 0;\n\t\tint total = -1;\n\t\tbool loaded = false;\n\t\tbool reloading = false;\n\t};\n\n\tvoid loadMore(PeerId peerId, bool reload);\n\t[[nodiscard]] Entry *lookupEntry(PeerId peerId);\n\t[[nodiscard]] const Entry *lookupEntry(PeerId peerId) const;\n\t[[nodiscard]] uint64 firstPageHash(const Entry &entry) const;\n\t[[nodiscard]] not_null<HistoryItem*> musicIdToMsg(\n\t\tPeerId peerId,\n\t\tEntry &entry,\n\t\tnot_null<DocumentData*> id);\n\n\tconst not_null<Session*> _owner;\n\n\tstd::vector<DocumentId> _myIds;\n\tcrl::time _lastReceived = 0;\n\tmtpRequestId _loadIdsRequest = 0;\n\n\tstd::unordered_map<PeerId, Entry> _entries;\n\trpl::event_stream<PeerId> _changed;\n\n};\n\nusing SavedMusicSlice = AbstractSparseIds<\n\tstd::vector<not_null<HistoryItem*>>>;\n\n[[nodiscard]] rpl::producer<SavedMusicSlice> SavedMusicList(\n\tnot_null<PeerData*> peer,\n\tHistoryItem *aroundId,\n\tint limit);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_saved_sublist.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_saved_sublist.h\"\n\n#include \"api/api_unread_things.h\"\n#include \"apiwrap.h\"\n#include \"core/application.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_drafts.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_messages.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/view/history_view_item_preview.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_unread_things.h\"\n#include \"main/main_session.h\"\n#include \"window/notifications_manager.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kMessagesPerPage = 50;\nconstexpr auto kReadRequestTimeout = 3 * crl::time(1000);\n\n} // namespace\n\nstruct SavedSublist::Viewer {\n\tMessagesSlice slice;\n\tMsgId around = 0;\n\tint limitBefore = 0;\n\tint limitAfter = 0;\n\tbase::has_weak_ptr guard;\n\tbool scheduled = false;\n};\n\nSavedSublist::SavedSublist(\n\tnot_null<SavedMessages*> parent,\n\tnot_null<PeerData*> sublistPeer)\n: Thread(&sublistPeer->owner(), Dialogs::Entry::Type::SavedSublist)\n, _parent(parent)\n, _sublistHistory(sublistPeer->owner().history(sublistPeer))\n, _readRequestTimer([=] { sendReadTillRequest(); }) {\n\tif (parent->parentChat()) {\n\t\t_flags |= Flag::InMonoforum;\n\t}\n\tsubscribeToUnreadChanges();\n}\n\nSavedSublist::~SavedSublist() {\n\thistories().cancelRequest(base::take(_beforeId));\n\thistories().cancelRequest(base::take(_afterId));\n\tif (_readRequestTimer.isActive()) {\n\t\tsendReadTillRequest();\n\t}\n\tsession().api().unreadThings().cancelRequests(this);\n}\n\nbool SavedSublist::inMonoforum() const {\n\treturn (_flags & Flag::InMonoforum) != 0;\n}\n\nvoid SavedSublist::apply(const SublistReadTillUpdate &update) {\n\tif (update.out) {\n\t\tsetOutboxReadTill(update.readTillId);\n\t} else if (update.readTillId >= _inboxReadTillId) {\n\t\tsetInboxReadTill(\n\t\t\tupdate.readTillId,\n\t\t\tcomputeUnreadCountLocally(update.readTillId));\n\t}\n}\n\nvoid SavedSublist::apply(const MessageUpdate &update) {\n\tif (applyUpdate(update)) {\n\t\t_instantChanges.fire({});\n\t}\n}\n\nvoid SavedSublist::applyDifferenceTooLong() {\n\tif (_skippedAfter.has_value()) {\n\t\t_skippedAfter = std::nullopt;\n\t\t_listChanges.fire({});\n\t}\n}\n\nbool SavedSublist::removeOne(not_null<HistoryItem*> item) {\n\tconst auto id = item->id;\n\tconst auto i = ranges::lower_bound(_list, id, std::greater<>());\n\tchangeUnreadCountByMessage(id, -1);\n\tif (i == end(_list) || *i != id) {\n\t\tif (const auto known = _fullCount.current()) {\n\t\t\tif (*known > 0) {\n\t\t\t\t_fullCount = (*known - 1);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\t_list.erase(i);\n\tif (_skippedBefore && _skippedAfter) {\n\t\t_fullCount = *_skippedBefore + _list.size() + *_skippedAfter;\n\t} else if (const auto known = _fullCount.current()) {\n\t\tif (*known > 0) {\n\t\t\t_fullCount = (*known - 1);\n\t\t}\n\t}\n\treturn true;\n}\n\nrpl::producer<MessagesSlice> SavedSublist::source(\n\t\tMessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tconst auto around = aroundId.fullId.msg;\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tconst auto viewer = lifetime.make_state<Viewer>();\n\t\tconst auto push = [=] {\n\t\t\tif (viewer->scheduled) {\n\t\t\t\tviewer->scheduled = false;\n\t\t\t\tif (buildFromData(viewer)) {\n\t\t\t\t\tappendClientSideMessages(viewer->slice);\n\t\t\t\t\tconsumer.put_next_copy(viewer->slice);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tconst auto pushInstant = [=] {\n\t\t\tviewer->scheduled = true;\n\t\t\tpush();\n\t\t};\n\t\tconst auto pushDelayed = [=] {\n\t\t\tif (!viewer->scheduled) {\n\t\t\t\tviewer->scheduled = true;\n\t\t\t\tcrl::on_main(&viewer->guard, push);\n\t\t\t}\n\t\t};\n\t\tviewer->around = around;\n\t\tviewer->limitBefore = limitBefore;\n\t\tviewer->limitAfter = limitAfter;\n\n\t\tconst auto history = owningHistory();\n\t\thistory->session().changes().historyUpdates(\n\t\t\thistory,\n\t\t\tHistoryUpdate::Flag::ClientSideMessages\n\t\t) | rpl::on_next(pushDelayed, lifetime);\n\n\t\t_listChanges.events(\n\t\t) | rpl::on_next(pushDelayed, lifetime);\n\n\t\t_instantChanges.events(\n\t\t) | rpl::on_next(pushInstant, lifetime);\n\n\t\tpushInstant();\n\t\treturn lifetime;\n\t};\n}\n\nnot_null<SavedMessages*> SavedSublist::parent() const {\n\treturn _parent;\n}\n\nnot_null<History*> SavedSublist::owningHistory() {\n\treturn _parent->owningHistory();\n}\n\nChannelData *SavedSublist::parentChat() const {\n\treturn _parent->parentChat();\n}\n\nnot_null<PeerData*> SavedSublist::sublistPeer() const {\n\treturn _sublistHistory->peer;\n}\n\nbool SavedSublist::isHiddenAuthor() const {\n\treturn sublistPeer()->isSavedHiddenAuthor();\n}\n\nrpl::producer<> SavedSublist::destroyed() const {\n\tusing namespace rpl::mappers;\n\treturn rpl::merge(\n\t\t_parent->destroyed(),\n\t\t_parent->sublistDestroyed() | rpl::filter(\n\t\t\t_1 == this\n\t\t) | rpl::to_empty);\n}\n\nvoid SavedSublist::applyMaybeLast(not_null<HistoryItem*> item) {\n\tif (!item->isRegular() || item->isService()) {\n\t\treturn;\n\t}\n\tif (!_lastServerMessage.value_or(nullptr)\n\t\t|| (*_lastServerMessage)->id < item->id) {\n\t\tsetLastServerMessage(item);\n\t\tresolveChatListMessageGroup();\n\t} else {\n\t\tgrowLastKnownServerMessageId(item->id);\n\t}\n}\n\nvoid SavedSublist::applyItemAdded(not_null<HistoryItem*> item) {\n\tif (item->isService()) {\n\t\treturn;\n\t}\n\tconst auto wasInChatList = shouldBeInChatList();\n\tif (item->isRegular()) {\n\t\tsetLastServerMessage(item);\n\t} else {\n\t\tsetLastMessage(item);\n\t}\n\tif (!_parent->parentChat() && !isPinnedDialog(FilterId())) {\n\t\tif (_restorePinnedWhenNonEmpty) {\n\t\t\towner().setChatPinned(this, FilterId(), true);\n\t\t\t_restorePinnedWhenNonEmpty = false;\n\t\t} else if (!wasInChatList && shouldBeInChatList()) {\n\t\t\t_parent->refreshPinned();\n\t\t}\n\t}\n}\n\nvoid SavedSublist::applyItemRemoved(MsgId id) {\n\tif (const auto lastItem = lastMessage()) {\n\t\tif (lastItem->id == id) {\n\t\t\t_lastMessage = std::nullopt;\n\t\t}\n\t}\n\tif (const auto lastServerItem = lastServerMessage()) {\n\t\tif (lastServerItem->id == id) {\n\t\t\t_lastServerMessage = std::nullopt;\n\t\t}\n\t}\n\tif (const auto chatListItem = _chatListMessage.value_or(nullptr)) {\n\t\tif (chatListItem->id == id) {\n\t\t\t_chatListMessage = std::nullopt;\n\t\t\tcrl::on_main(this, [=] {\n\t\t\t\tconst auto locallyKnownEmpty = _list.empty()\n\t\t\t\t\t&& (_fullCount.current() == 0);\n\t\t\t\tif (_chatListMessage.value_or(nullptr)) {\n\t\t\t\t\treturn;\n\t\t\t\t} else if ((_skippedAfter == 0) || locallyKnownEmpty) {\n\t\t\t\t\tif (!_list.empty()) {\n\t\t\t\t\t\tapplyMaybeLast(owner().message(\n\t\t\t\t\t\t\towningHistory()->peer,\n\t\t\t\t\t\t\t_list.front()));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t} else if ((_skippedBefore == 0) || locallyKnownEmpty) {\n\t\t\t\t\t\tif (!_parent->parentChat()\n\t\t\t\t\t\t\t&& isPinnedDialog(FilterId())) {\n\t\t\t\t\t\t\t_restorePinnedWhenNonEmpty = true;\n\t\t\t\t\t\t\towner().setChatPinned(this, FilterId(), false);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsetLastServerMessage(nullptr);\n\t\t\t\t\t\tupdateChatListExistence();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (_parent->parentChat()) {\n\t\t\t\t\trequestChatListMessage();\n\t\t\t\t} else {\n\t\t\t\t\tloadAround(0);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n}\n\nvoid SavedSublist::requestChatListMessage() {\n\tif (!chatListMessageKnown()) {\n\t\tparent()->requestSublist(sublistPeer());\n\t}\n}\n\nvoid SavedSublist::setRestorePinnedWhenNonEmpty(bool restore) {\n\t_restorePinnedWhenNonEmpty = restore;\n}\n\nvoid SavedSublist::readTillEnd() {\n\treadTill(_lastKnownServerMessageId);\n}\n\nbool SavedSublist::buildFromData(not_null<Viewer*> viewer) {\n\tif (_list.empty() && _skippedBefore == 0 && _skippedAfter == 0) {\n\t\tviewer->slice.ids.clear();\n\t\tviewer->slice.nearestToAround = FullMsgId();\n\t\tviewer->slice.fullCount\n\t\t\t= viewer->slice.skippedBefore\n\t\t\t= viewer->slice.skippedAfter\n\t\t\t= 0;\n\t\tranges::reverse(viewer->slice.ids);\n\t\treturn true;\n\t}\n\tconst auto around = (viewer->around != ShowAtUnreadMsgId)\n\t\t? viewer->around\n\t\t: computeInboxReadTillFull();\n\tif (_list.empty()\n\t\t|| (!around && _skippedAfter != 0)\n\t\t|| (around > _list.front() && _skippedAfter != 0)\n\t\t|| (around > 0 && around < _list.back() && _skippedBefore != 0)) {\n\t\tloadAround(around);\n\t\treturn false;\n\t}\n\tconst auto i = around\n\t\t? ranges::lower_bound(_list, around, std::greater<>())\n\t\t: end(_list);\n\tconst auto availableBefore = int(end(_list) - i);\n\tconst auto availableAfter = int(i - begin(_list));\n\tconst auto useBefore = std::min(availableBefore, viewer->limitBefore + 1);\n\tconst auto useAfter = std::min(availableAfter, viewer->limitAfter);\n\tconst auto slice = &viewer->slice;\n\tif (_skippedBefore.has_value()) {\n\t\tslice->skippedBefore\n\t\t\t= (*_skippedBefore + (availableBefore - useBefore));\n\t}\n\tif (_skippedAfter.has_value()) {\n\t\tslice->skippedAfter\n\t\t\t= (*_skippedAfter + (availableAfter - useAfter));\n\t}\n\n\tconst auto peerId = owningHistory()->peer->id;\n\tslice->ids.clear();\n\tauto nearestToAround = std::optional<MsgId>();\n\tslice->ids.reserve(useAfter + useBefore);\n\tfor (auto j = i - useAfter, e = i + useBefore; j != e; ++j) {\n\t\tconst auto id = *j;\n\t\tif (!nearestToAround && id < around) {\n\t\t\tnearestToAround = (j == i - useAfter)\n\t\t\t\t? id\n\t\t\t\t: *(j - 1);\n\t\t}\n\t\tslice->ids.emplace_back(peerId, id);\n\t}\n\tslice->nearestToAround = FullMsgId(\n\t\tpeerId,\n\t\tnearestToAround.value_or(\n\t\t\tslice->ids.empty() ? 0 : slice->ids.back().msg));\n\tslice->fullCount = _fullCount.current();\n\n\tranges::reverse(viewer->slice.ids);\n\n\tif (_skippedBefore != 0 && useBefore < viewer->limitBefore + 1) {\n\t\tloadBefore();\n\t}\n\tif (_skippedAfter != 0 && useAfter < viewer->limitAfter) {\n\t\tloadAfter();\n\t}\n\n\treturn true;\n}\n\nbool SavedSublist::applyUpdate(const MessageUpdate &update) {\n\tusing Flag = MessageUpdate::Flag;\n\n\tif (update.item->history() != owningHistory()\n\t\t|| !update.item->isRegular()\n\t\t|| update.item->isService()\n\t\t|| update.item->sublistPeerId() != sublistPeer()->id) {\n\t\treturn false;\n\t} else if (update.flags & Flag::Destroyed) {\n\t\treturn removeOne(update.item);\n\t}\n\tconst auto id = update.item->id;\n\tif (update.flags & Flag::NewAdded) {\n\t\tchangeUnreadCountByMessage(id, 1);\n\t}\n\tconst auto i = ranges::lower_bound(_list, id, std::greater<>());\n\tif (_skippedAfter != 0\n\t\t|| (i != end(_list) && *i == id)) {\n\t\treturn false;\n\t}\n\t_list.insert(i, id);\n\tif (_skippedBefore && _skippedAfter) {\n\t\t_fullCount = *_skippedBefore + _list.size() + *_skippedAfter;\n\t} else if (const auto known = _fullCount.current()) {\n\t\t_fullCount = *known + 1;\n\t}\n\treturn true;\n}\n\nbool SavedSublist::processMessagesIsEmpty(\n\t\tconst MTPmessages_Messages &result) {\n\tconst auto guard = gsl::finally([&] { _listChanges.fire({}); });\n\n\tconst auto list = result.match([&](\n\t\t\tconst MTPDmessages_messagesNotModified &) {\n\t\tLOG((\"API Error: received messages.messagesNotModified! \"\n\t\t\t\"(HistoryWidget::messagesReceived)\"));\n\t\treturn QVector<MTPMessage>();\n\t}, [&](const auto &data) {\n\t\towner().processUsers(data.vusers());\n\t\towner().processChats(data.vchats());\n\t\treturn data.vmessages().v;\n\t});\n\n\tconst auto fullCount = result.match([&](\n\t\t\tconst MTPDmessages_messagesNotModified &) {\n\t\tLOG((\"API Error: received messages.messagesNotModified! \"\n\t\t\t\"(HistoryWidget::messagesReceived)\"));\n\t\treturn 0;\n\t}, [&](const MTPDmessages_messages &data) {\n\t\towningHistory()->peer->processTopics(data.vtopics());\n\t\treturn int(data.vmessages().v.size());\n\t}, [&](const MTPDmessages_messagesSlice &data) {\n\t\towningHistory()->peer->processTopics(data.vtopics());\n\t\treturn data.vcount().v;\n\t}, [&](const MTPDmessages_channelMessages &data) {\n\t\tif (const auto channel = owningHistory()->peer->asChannel()) {\n\t\t\tchannel->ptsReceived(data.vpts().v);\n\t\t} else {\n\t\t\tLOG((\"API Error: received messages.channelMessages when \"\n\t\t\t\t\"no channel was passed! (HistoryWidget::messagesReceived)\"));\n\t\t}\n\t\towningHistory()->peer->processTopics(data.vtopics());\n\t\treturn data.vcount().v;\n\t});\n\n\tif (list.isEmpty()) {\n\t\treturn true;\n\t}\n\n\tconst auto maxId = IdFromMessage(list.front());\n\tconst auto wasSize = int(_list.size());\n\tconst auto toFront = (wasSize > 0) && (maxId > _list.front());\n\tconst auto localFlags = MessageFlags();\n\tconst auto type = NewMessageType::Existing;\n\tauto refreshed = std::vector<MsgId>();\n\tif (toFront) {\n\t\trefreshed.reserve(_list.size() + list.size());\n\t}\n\tauto skipped = 0;\n\tfor (const auto &message : list) {\n\t\tif (const auto item = owner().addNewMessage(message, localFlags, type)) {\n\t\t\tif (item->sublistPeerId() == sublistPeer()->id\n\t\t\t\t&& item->isRegular()\n\t\t\t\t&& !item->isService()) {\n\t\t\t\tif (toFront && item->id > _list.front()) {\n\t\t\t\t\trefreshed.push_back(item->id);\n\t\t\t\t} else if (_list.empty() || item->id < _list.back()) {\n\t\t\t\t\t_list.push_back(item->id);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t++skipped;\n\t\t\t}\n\t\t} else {\n\t\t\t++skipped;\n\t\t}\n\t}\n\tif (toFront) {\n\t\trefreshed.insert(refreshed.end(), _list.begin(), _list.end());\n\t\t_list = std::move(refreshed);\n\t}\n\n\tconst auto nowSize = int(_list.size());\n\tauto &decrementFrom = toFront ? _skippedAfter : _skippedBefore;\n\tif (decrementFrom.has_value()) {\n\t\t*decrementFrom = std::max(\n\t\t\t*decrementFrom - (nowSize - wasSize),\n\t\t\t0);\n\t}\n\n\tconst auto checkedCount = std::max(fullCount - skipped, nowSize);\n\tif (_skippedBefore && _skippedAfter) {\n\t\tauto &correct = toFront ? _skippedBefore : _skippedAfter;\n\t\t*correct = std::max(\n\t\t\tcheckedCount - *decrementFrom - nowSize,\n\t\t\t0);\n\t\t*decrementFrom = checkedCount - *correct - nowSize;\n\t\tAssert(*decrementFrom >= 0);\n\t} else if (_skippedBefore) {\n\t\t*_skippedBefore = std::min(*_skippedBefore, checkedCount - nowSize);\n\t\t_skippedAfter = checkedCount - *_skippedBefore - nowSize;\n\t} else if (_skippedAfter) {\n\t\t*_skippedAfter = std::min(*_skippedAfter, checkedCount - nowSize);\n\t\t_skippedBefore = checkedCount - *_skippedAfter - nowSize;\n\t}\n\t_fullCount = checkedCount;\n\n\tcheckReadTillEnd();\n\n\tEnsures(list.size() >= skipped);\n\treturn (list.size() == skipped);\n}\n\nvoid SavedSublist::setInboxReadTill(\n\t\tMsgId readTillId,\n\t\tstd::optional<int> unreadCount) {\n\tconst auto newReadTillId = std::max(readTillId.bare, int64(1));\n\tconst auto ignore = (newReadTillId < _inboxReadTillId);\n\tif (ignore) {\n\t\treturn;\n\t}\n\tconst auto changed = (newReadTillId > _inboxReadTillId);\n\tif (changed) {\n\t\t_inboxReadTillId = newReadTillId;\n\t}\n\tif (_skippedAfter == 0\n\t\t&& !_list.empty()\n\t\t&& _inboxReadTillId >= _list.front()) {\n\t\tunreadCount = 0;\n\t} else if (_lastServerMessage.value_or(nullptr)\n\t\t&& (*_lastServerMessage)->id <= newReadTillId) {\n\t\tunreadCount = 0;\n\t}\n\tif (_unreadCount.current() != unreadCount\n\t\t&& (changed || unreadCount.has_value())) {\n\t\tsetUnreadCount(unreadCount);\n\t}\n}\n\nMsgId SavedSublist::inboxReadTillId() const {\n\treturn _inboxReadTillId;\n}\n\nMsgId SavedSublist::computeInboxReadTillFull() const {\n\treturn _inboxReadTillId;\n}\n\nvoid SavedSublist::setOutboxReadTill(MsgId readTillId) {\n\tconst auto newReadTillId = std::max(readTillId.bare, int64(1));\n\tif (newReadTillId > _outboxReadTillId) {\n\t\t_outboxReadTillId = newReadTillId;\n\t\tconst auto history = owningHistory();\n\t\thistory->session().changes().historyUpdated(\n\t\t\thistory,\n\t\t\tHistoryUpdate::Flag::OutboxRead);\n\t}\n}\n\nMsgId SavedSublist::computeOutboxReadTillFull() const {\n\treturn _outboxReadTillId;\n}\n\nvoid SavedSublist::setUnreadCount(std::optional<int> count) {\n\t_unreadCount = count;\n\tif (!count && !_readRequestTimer.isActive() && !_readRequestId) {\n\t\treloadUnreadCountIfNeeded();\n\t}\n}\n\nvoid SavedSublist::setUnreadMark(bool unread) {\n\tif (unreadMark() == unread) {\n\t\treturn;\n\t}\n\tconst auto notifier = unreadStateChangeNotifier(\n\t\t!unreadCountCurrent());\n\tThread::setUnreadMarkFlag(unread);\n}\n\nbool SavedSublist::unreadCountKnown() const {\n\treturn !inMonoforum() || _unreadCount.current().has_value();\n}\n\nint SavedSublist::unreadCountCurrent() const {\n\treturn _unreadCount.current().value_or(0);\n}\n\nrpl::producer<std::optional<int>> SavedSublist::unreadCountValue() const {\n\tif (!inMonoforum()) {\n\t\treturn rpl::single(std::optional<int>(0));\n\t}\n\treturn _unreadCount.value();\n}\n\nint SavedSublist::displayedUnreadCount() const {\n\treturn (_inboxReadTillId > 1) ? unreadCountCurrent() : 0;\n}\n\nvoid SavedSublist::changeUnreadCountByMessage(MsgId id, int delta) {\n\tif (!inMonoforum() || !_inboxReadTillId) {\n\t\tsetUnreadCount(std::nullopt);\n\t\treturn;\n\t}\n\tconst auto count = _unreadCount.current();\n\tif (count.has_value() && (id > _inboxReadTillId)) {\n\t\tsetUnreadCount(std::max(*count + delta, 0));\n\t}\n}\n\nbool SavedSublist::isServerSideUnread(\n\t\tnot_null<const HistoryItem*> item) const {\n\tif (!inMonoforum()) {\n\t\treturn false;\n\t}\n\tconst auto till = item->out()\n\t\t? computeOutboxReadTillFull()\n\t\t: computeInboxReadTillFull();\n\treturn (item->id > till);\n}\n\nvoid SavedSublist::checkReadTillEnd() {\n\tif (_unreadCount.current() != 0\n\t\t&& _skippedAfter == 0\n\t\t&& !_list.empty()\n\t\t&& _inboxReadTillId >= _list.front()) {\n\t\tsetUnreadCount(0);\n\t}\n}\n\nstd::optional<int> SavedSublist::computeUnreadCountLocally(\n\t\tMsgId afterId) const {\n\tExpects(afterId >= _inboxReadTillId);\n\n\tconst auto currentUnreadCountAfter = _unreadCount.current();\n\tconst auto startingMarkingAsRead = (currentUnreadCountAfter == 0)\n\t\t&& (_inboxReadTillId == 1)\n\t\t&& (afterId > 1);\n\tconst auto wasUnreadCountAfter = startingMarkingAsRead\n\t\t? _fullCount.current().value_or(0)\n\t\t: currentUnreadCountAfter;\n\tconst auto readTillId = std::max(afterId, MsgId(1));\n\tconst auto wasReadTillId = _inboxReadTillId;\n\tconst auto backLoaded = (_skippedBefore == 0);\n\tconst auto frontLoaded = (_skippedAfter == 0);\n\tconst auto fullLoaded = backLoaded && frontLoaded;\n\tconst auto allUnread = (readTillId == MsgId(1))\n\t\t|| (fullLoaded && _list.empty());\n\tif (allUnread && fullLoaded) {\n\t\t// Should not happen too often unless the list is empty.\n\t\treturn int(_list.size());\n\t} else if (frontLoaded && !_list.empty() && readTillId >= _list.front()) {\n\t\t// Always \"count by local data\" if read till the end.\n\t\treturn 0;\n\t} else if (wasReadTillId == readTillId) {\n\t\t// Otherwise don't recount the same value over and over.\n\t\treturn wasUnreadCountAfter;\n\t} else if (frontLoaded && !_list.empty() && readTillId >= _list.back()) {\n\t\t// And count by local data if it is available and read-till changed.\n\t\treturn int(ranges::lower_bound(_list, readTillId, std::greater<>())\n\t\t\t- begin(_list));\n\t} else if (_list.empty()) {\n\t\treturn std::nullopt;\n\t} else if (wasUnreadCountAfter.has_value()\n\t\t&& (frontLoaded || readTillId <= _list.front())\n\t\t&& (backLoaded || wasReadTillId >= _list.back())) {\n\t\t// Count how many were read since previous value.\n\t\tconst auto from = ranges::lower_bound(\n\t\t\t_list,\n\t\t\treadTillId,\n\t\t\tstd::greater<>());\n\t\tconst auto till = ranges::lower_bound(\n\t\t\tfrom,\n\t\t\tend(_list),\n\t\t\twasReadTillId,\n\t\t\tstd::greater<>());\n\t\treturn std::max(*wasUnreadCountAfter - int(till - from), 0);\n\t}\n\treturn std::nullopt;\n}\n\nvoid SavedSublist::requestUnreadCount() {\n\tparent()->requestSublist(sublistPeer());\n}\n\nvoid SavedSublist::readTill(not_null<HistoryItem*> item) {\n\treadTill(item->id, item);\n}\n\nvoid SavedSublist::readTill(MsgId tillId) {\n\tconst auto parentChat = _parent->parentChat();\n\tif (!parentChat) {\n\t\treturn;\n\t}\n\treadTill(tillId, owner().message(parentChat->id, tillId));\n}\n\nvoid SavedSublist::readTill(\n\t\tMsgId tillId,\n\t\tHistoryItem *tillIdItem) {\n\tif (!IsServerMsgId(tillId)) {\n\t\treturn;\n\t}\n\tif (unreadMark()) {\n\t\towner().histories().changeSublistUnreadMark(this, false);\n\t}\n\tconst auto was = computeInboxReadTillFull();\n\tconst auto now = tillId;\n\tif (now < was) {\n\t\treturn;\n\t}\n\tconst auto unreadCount = computeUnreadCountLocally(now);\n\tconst auto fast = (tillIdItem && tillIdItem->out())\n\t\t|| !unreadCount.has_value();\n\tif (was < now || (fast && now == was)) {\n\t\tsetInboxReadTill(now, unreadCount);\n\t\tif (!_readRequestTimer.isActive()) {\n\t\t\t_readRequestTimer.callOnce(fast ? 0 : kReadRequestTimeout);\n\t\t} else if (fast && _readRequestTimer.remainingTime() > 0) {\n\t\t\t_readRequestTimer.callOnce(0);\n\t\t}\n\t}\n\tCore::App().notifications().clearIncomingFromSublist(this);\n}\n\nvoid SavedSublist::sendReadTillRequest() {\n\tconst auto parentChat = _parent->parentChat();\n\tif (!parentChat) {\n\t\treturn;\n\t}\n\tif (_readRequestTimer.isActive()) {\n\t\t_readRequestTimer.cancel();\n\t}\n\tconst auto api = &_parent->session().api();\n\tapi->request(base::take(_readRequestId)).cancel();\n\n\t_sentReadTill = computeInboxReadTillFull();\n\t_readRequestId = api->request(MTPmessages_ReadSavedHistory(\n\t\tparentChat->input(),\n\t\tsublistPeer()->input(),\n\t\tMTP_int(_sentReadTill.bare)\n\t)).done(crl::guard(this, [=] {\n\t\t_readRequestId = 0;\n\t\treloadUnreadCountIfNeeded();\n\t})).send();\n}\n\nvoid SavedSublist::reloadUnreadCountIfNeeded() {\n\tif (unreadCountKnown()) {\n\t\treturn;\n\t} else if (inboxReadTillId() < computeInboxReadTillFull()) {\n\t\t_readRequestTimer.callOnce(0);\n\t} else {\n\t\trequestUnreadCount();\n\t}\n}\n\nvoid SavedSublist::subscribeToUnreadChanges() {\n\tif (!inMonoforum()) {\n\t\treturn;\n\t}\n\t_unreadCount.value(\n\t) | rpl::map([=](std::optional<int> value) {\n\t\treturn value ? displayedUnreadCount() : value;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::combine_previous(\n\t) | rpl::filter([=] {\n\t\treturn inChatList();\n\t}) | rpl::on_next([=](\n\t\t\tstd::optional<int> previous,\n\t\t\tstd::optional<int> now) {\n\t\tif (previous.value_or(0) != now.value_or(0)) {\n\t\t\t_parent->recentSublistsInvalidate(this);\n\t\t}\n\t\tnotifyUnreadStateChange(unreadStateFor(\n\t\t\tprevious.value_or(0),\n\t\t\tprevious.has_value()));\n\t}, _lifetime);\n}\n\nvoid SavedSublist::applyMonoforumDialog(\n\t\tconst MTPDmonoForumDialog &data,\n\t\tnot_null<HistoryItem*> topItem) {\n\tif (const auto parent = parentChat()) {\n\t\tif (const auto draft = data.vdraft()) {\n\t\t\tdraft->match([&](const MTPDdraftMessage &data) {\n\t\t\t\tData::ApplyPeerCloudDraft(\n\t\t\t\t\t&session(),\n\t\t\t\t\tparent->id,\n\t\t\t\t\tMsgId(),\n\t\t\t\t\tsublistPeer()->id,\n\t\t\t\t\tdata);\n\t\t\t}, [](const MTPDdraftMessageEmpty&) {});\n\t\t}\n\t}\n\n\tsetInboxReadTill(\n\t\tdata.vread_inbox_max_id().v,\n\t\tdata.vunread_count().v);\n\tif (!unreadCountKnown() && !_readRequestId) {\n\t\t// We got read_inbox_max_id < than our current inboxReadTillId,\n\t\t// we need either to send a read request with this new value,\n\t\t// or to downgrade inboxReadTillId locally.\n\t\tif (_sentReadTill < computeInboxReadTillFull()) {\n\t\t\tsendReadTillRequest();\n\t\t} else {\n\t\t\t// Just if nothing else helps.\n\t\t\t_inboxReadTillId = 0;\n\t\t\tsetInboxReadTill(\n\t\t\t\tdata.vread_inbox_max_id().v,\n\t\t\t\tdata.vunread_count().v);\n\t\t}\n\t}\n\tsetOutboxReadTill(data.vread_outbox_max_id().v);\n\tunreadReactions().setCount(data.vunread_reactions_count().v);\n\tsetUnreadMark(data.is_unread_mark());\n\tapplyMaybeLast(topItem);\n\n\tif (data.is_nopaid_messages_exception()) {\n\t\t_flags |= Flag::FeeRemoved;\n\t} else {\n\t\t_flags &= ~Flag::FeeRemoved;\n\t}\n}\n\nbool SavedSublist::isFeeRemoved() const {\n\treturn (_flags & Flag::FeeRemoved);\n}\n\nvoid SavedSublist::toggleFeeRemoved(bool feeRemoved) {\n\tif (feeRemoved) {\n\t\t_flags |= Flag::FeeRemoved;\n\t} else {\n\t\t_flags &= ~Flag::FeeRemoved;\n\t}\n}\n\nTimeId SavedSublist::adjustedChatListTimeId() const {\n\tconst auto result = chatListTimeId();\n\tconst auto monoforumPeerId = sublistPeer()->id;\n\tconst auto history = _parent->owningHistory();\n\tif (const auto draft = history->cloudDraft(MsgId(), monoforumPeerId)) {\n\t\tif (!Data::DraftIsNull(draft) && !session().supportMode()) {\n\t\t\treturn std::max(result, draft->date);\n\t\t}\n\t}\n\treturn result;\n}\n\nrpl::producer<> SavedSublist::changes() const {\n\treturn _listChanges.events();\n}\n\nvoid SavedSublist::loadFullCount() {\n\tif (!_fullCount.current() && !_loadingAround) {\n\t\tloadAround(0);\n\t}\n}\n\nvoid SavedSublist::appendClientSideMessages(MessagesSlice &slice) {\n\tconst auto &messages = owningHistory()->clientSideMessages();\n\tif (messages.empty()) {\n\t\treturn;\n\t} else if (slice.ids.empty()) {\n\t\tif (slice.skippedBefore != 0 || slice.skippedAfter != 0) {\n\t\t\treturn;\n\t\t}\n\t\tslice.ids.reserve(messages.size());\n\t\tconst auto sublistPeerId = sublistPeer()->id;\n\t\tfor (const auto &item : messages) {\n\t\t\tif (item->sublistPeerId() != sublistPeerId) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tslice.ids.push_back(item->fullId());\n\t\t}\n\t\tranges::sort(slice.ids);\n\t\treturn;\n\t}\n\tconst auto sublistPeerId = sublistPeer()->id;\n\tauto dates = std::vector<TimeId>();\n\tdates.reserve(slice.ids.size());\n\tfor (const auto &id : slice.ids) {\n\t\tconst auto message = owner().message(id);\n\t\tAssert(message != nullptr);\n\n\t\tdates.push_back(message->date());\n\t}\n\tfor (const auto &item : messages) {\n\t\tif (item->sublistPeerId() != sublistPeerId) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto date = item->date();\n\t\tif (date < dates.front()) {\n\t\t\tif (slice.skippedBefore != 0) {\n\t\t\t\tif (slice.skippedBefore) {\n\t\t\t\t\t++*slice.skippedBefore;\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tdates.insert(dates.begin(), date);\n\t\t\tslice.ids.insert(slice.ids.begin(), item->fullId());\n\t\t} else {\n\t\t\tauto to = dates.size();\n\t\t\tfor (; to != 0; --to) {\n\t\t\t\tconst auto checkId = slice.ids[to - 1].msg;\n\t\t\t\tif (dates[to - 1] > date) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (dates[to - 1] < date\n\t\t\t\t\t|| IsServerMsgId(checkId)\n\t\t\t\t\t|| checkId < item->id) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tdates.insert(dates.begin() + to, date);\n\t\t\tslice.ids.insert(slice.ids.begin() + to, item->fullId());\n\t\t}\n\t}\n}\n\nstd::optional<int> SavedSublist::fullCount() const {\n\treturn _fullCount.current();\n}\n\nrpl::producer<int> SavedSublist::fullCountValue() const {\n\treturn _fullCount.value() | rpl::filter_optional();\n}\n\nint SavedSublist::fixedOnTopIndex() const {\n\treturn 0;\n}\n\nbool SavedSublist::shouldBeInChatList() const {\n\tconst auto monoforum = _parent->parentChat();\n\tif (monoforum && (monoforum == sublistPeer())) {\n\t\treturn false;\n\t}\n\tconst auto last = lastMessage();\n\tconst auto hasDisplayableLast = last && !last->isService();\n\tif (!monoforum) {\n\t\treturn hasDisplayableLast;\n\t}\n\treturn isPinnedDialog(FilterId())\n\t\t|| !lastMessageKnown()\n\t\t|| hasDisplayableLast;\n}\n\nHistoryItem *SavedSublist::lastMessage() const {\n\treturn _lastMessage.value_or(nullptr);\n}\n\nbool SavedSublist::lastMessageKnown() const {\n\treturn _lastMessage.has_value();\n}\n\nHistoryItem *SavedSublist::lastServerMessage() const {\n\treturn _lastServerMessage.value_or(nullptr);\n}\n\nbool SavedSublist::lastServerMessageKnown() const {\n\treturn _lastServerMessage.has_value();\n}\n\nMsgId SavedSublist::lastKnownServerMessageId() const {\n\treturn _lastKnownServerMessageId;\n}\n\nDialogs::UnreadState SavedSublist::chatListUnreadState() const {\n\tif (!inMonoforum()) {\n\t\treturn {};\n\t}\n\treturn unreadStateFor(displayedUnreadCount(), unreadCountKnown());\n}\n\nDialogs::BadgesState SavedSublist::chatListBadgesState() const {\n\tif (!inMonoforum()) {\n\t\treturn {};\n\t}\n\tauto result = Dialogs::BadgesForUnread(\n\t\tchatListUnreadState(),\n\t\tDialogs::CountInBadge::Messages,\n\t\tDialogs::IncludeInBadge::All);\n\tif (!result.unread && inboxReadTillId() < 2) {\n\t\tresult.unread = (_lastKnownServerMessageId\n\t\t\t> _parent->owningHistory()->inboxReadTillId());\n\t\tresult.unreadMuted = muted();\n\t}\n\tif (_parent->owningHistory()->muted()) {\n\t\tresult.unreadMuted\n\t\t\t= result.mentionMuted\n\t\t\t= result.reactionMuted\n\t\t\t= result.pollMuted\n\t\t\t= true;\n\t}\n\treturn result;\n}\n\nHistoryItem *SavedSublist::chatListMessage() const {\n\treturn _lastMessage.value_or(nullptr);\n}\n\nbool SavedSublist::chatListMessageKnown() const {\n\treturn _lastMessage.has_value();\n}\n\nconst QString &SavedSublist::chatListName() const {\n\treturn _sublistHistory->chatListName();\n}\n\nconst base::flat_set<QString> &SavedSublist::chatListNameWords() const {\n\treturn _sublistHistory->chatListNameWords();\n}\n\nconst base::flat_set<QChar> &SavedSublist::chatListFirstLetters() const {\n\treturn _sublistHistory->chatListFirstLetters();\n}\n\nconst QString &SavedSublist::chatListNameSortKey() const {\n\treturn _sublistHistory->chatListNameSortKey();\n}\n\nint SavedSublist::chatListNameVersion() const {\n\treturn _sublistHistory->chatListNameVersion();\n}\n\nvoid SavedSublist::paintUserpic(\n\t\tPainter &p,\n\t\tUi::PeerUserpicView &view,\n\t\tconst Dialogs::Ui::PaintContext &context) const {\n\t_sublistHistory->paintUserpic(p, view, context);\n}\n\nHistoryView::SendActionPainter *SavedSublist::sendActionPainter() {\n\treturn nullptr;\n}\n\nvoid SavedSublist::hasUnreadMentionChanged(bool has) {\n\tauto was = chatListUnreadState();\n\tif (has) {\n\t\twas.mentions = 0;\n\t} else {\n\t\twas.mentions = 1;\n\t}\n\tnotifyUnreadStateChange(was);\n}\n\nvoid SavedSublist::hasUnreadReactionChanged(bool has) {\n\tauto was = chatListUnreadState();\n\tif (has) {\n\t\twas.reactions = was.reactionsMuted = 0;\n\t} else {\n\t\twas.reactions = 1;\n\t\twas.reactionsMuted = muted() ? was.reactions : 0;\n\t}\n\tnotifyUnreadStateChange(was);\n}\n\nvoid SavedSublist::hasUnreadPollVoteChanged(bool has) {\n}\n\nvoid SavedSublist::allowChatListMessageResolve() {\n\tif (_flags & Flag::ResolveChatListMessage) {\n\t\treturn;\n\t}\n\t_flags |= Flag::ResolveChatListMessage;\n\tresolveChatListMessageGroup();\n}\n\nvoid SavedSublist::resolveChatListMessageGroup() {\n\tif (!(_flags & Flag::ResolveChatListMessage)) {\n\t\treturn;\n\t}\n\t// If we set a single album part, request the full album.\n\tconst auto item = _lastServerMessage.value_or(nullptr);\n\tif (item && item->groupId() != MessageGroupId()) {\n\t\tif (owner().groups().isGroupOfOne(item)\n\t\t\t&& !item->toPreview({\n\t\t\t\t.hideSender = true,\n\t\t\t\t.hideCaption = true }).images.empty()\n\t\t\t\t&& _requestedGroups.emplace(item->fullId()).second) {\n\t\t\towner().histories().requestGroupAround(item);\n\t\t}\n\t}\n}\n\nvoid SavedSublist::growLastKnownServerMessageId(MsgId id) {\n\t_lastKnownServerMessageId = std::max(_lastKnownServerMessageId, id);\n}\n\nvoid SavedSublist::setLastServerMessage(HistoryItem *item) {\n\tif (item) {\n\t\tgrowLastKnownServerMessageId(item->id);\n\t}\n\t_lastServerMessage = item;\n\tif (_lastMessage\n\t\t&& *_lastMessage\n\t\t&& !(*_lastMessage)->isRegular()\n\t\t&& (!item\n\t\t\t|| (*_lastMessage)->date() > item->date()\n\t\t\t|| (*_lastMessage)->isSending())) {\n\t\treturn;\n\t}\n\tsetLastMessage(item);\n}\n\nvoid SavedSublist::setLastMessage(HistoryItem *item) {\n\tif (_lastMessage && *_lastMessage == item) {\n\t\treturn;\n\t}\n\t_lastMessage = item;\n\tif (!item || item->isRegular()) {\n\t\t_lastServerMessage = item;\n\t\tif (item) {\n\t\t\tgrowLastKnownServerMessageId(item->id);\n\t\t}\n\t}\n\tsetChatListMessage(item);\n}\n\nvoid SavedSublist::setChatListMessage(HistoryItem *item) {\n\tif (_chatListMessage && *_chatListMessage == item) {\n\t\treturn;\n\t}\n\tconst auto was = _chatListMessage.value_or(nullptr);\n\tif (item) {\n\t\tif (item->isSponsored()) {\n\t\t\treturn;\n\t\t}\n\t\tif (_chatListMessage\n\t\t\t&& *_chatListMessage\n\t\t\t&& !(*_chatListMessage)->isRegular()\n\t\t\t&& (*_chatListMessage)->date() > item->date()) {\n\t\t\treturn;\n\t\t}\n\t\t_chatListMessage = item;\n\t\tsetChatListTimeId(item->date());\n\t\tupdateChatListExistence();\n\t} else if (!_chatListMessage || *_chatListMessage) {\n\t\t_chatListMessage = nullptr;\n\t\tupdateChatListExistence();\n\t}\n\t_parent->listMessageChanged(was, item);\n}\n\nvoid SavedSublist::chatListPreloadData() {\n\tsublistPeer()->loadUserpic();\n\tallowChatListMessageResolve();\n}\n\nDialogs::UnreadState SavedSublist::unreadStateFor(\n\t\tint count,\n\t\tbool known) const {\n\tauto result = Dialogs::UnreadState();\n\tconst auto mark = !count && unreadMark();\n\tconst auto muted = this->muted();\n\tresult.messages = count;\n\tresult.chats = count ? 1 : 0;\n\tresult.marks = mark ? 1 : 0;\n\tresult.reactions = unreadReactions().has() ? 1 : 0;\n\tresult.messagesMuted = muted ? result.messages : 0;\n\tresult.chatsMuted = muted ? result.chats : 0;\n\tresult.marksMuted = muted ? result.marks : 0;\n\tresult.reactionsMuted = muted ? result.reactions : 0;\n\tresult.known = known;\n\treturn result;\n}\n\nHistories &SavedSublist::histories() {\n\treturn owner().histories();\n}\n\nvoid SavedSublist::loadAround(MsgId id) {\n\tif (_loadingAround && *_loadingAround == id) {\n\t\t_loadingAroundRetry = id;\n\t\treturn;\n\t}\n\t_loadingAroundRetry = std::nullopt;\n\thistories().cancelRequest(base::take(_beforeId));\n\thistories().cancelRequest(base::take(_afterId));\n\n\tconst auto send = [=](Fn<void()> finish) {\n\t\tusing Flag = MTPmessages_GetSavedHistory::Flag;\n\t\tconst auto parentChat = _parent->parentChat();\n\t\treturn session().api().request(MTPmessages_GetSavedHistory(\n\t\t\tMTP_flags(parentChat ? Flag::f_parent_peer : Flag(0)),\n\t\t\tparentChat ? parentChat->input() : MTPInputPeer(),\n\t\t\tsublistPeer()->input(),\n\t\t\tMTP_int(id), // offset_id\n\t\t\tMTP_int(0), // offset_date\n\t\t\tMTP_int(id ? (-kMessagesPerPage / 2) : 0), // add_offset\n\t\t\tMTP_int(kMessagesPerPage), // limit\n\t\t\tMTP_int(0), // max_id\n\t\t\tMTP_int(0), // min_id\n\t\t\tMTP_long(0)) // hash\n\t\t).done([=](const MTPmessages_Messages &result) {\n\t\t\t_beforeId = 0;\n\t\t\t_loadingAround = std::nullopt;\n\t\t\tfinish();\n\n\t\t\tif (!id) {\n\t\t\t\t_skippedAfter = 0;\n\t\t\t} else {\n\t\t\t\t_skippedAfter = std::nullopt;\n\t\t\t}\n\t\t\t_skippedBefore = std::nullopt;\n\t\t\t_list.clear();\n\t\t\tif (processMessagesIsEmpty(result)) {\n\t\t\t\t_fullCount = _skippedBefore = _skippedAfter = 0;\n\t\t\t\tif (!_parent->parentChat()\n\t\t\t\t\t&& !_chatListMessage.value_or(nullptr)) {\n\t\t\t\t\tif (isPinnedDialog(FilterId())) {\n\t\t\t\t\t\t_restorePinnedWhenNonEmpty = true;\n\t\t\t\t\t\towner().setChatPinned(this, FilterId(), false);\n\t\t\t\t\t}\n\t\t\t\t\tsetLastServerMessage(nullptr);\n\t\t\t\t\tupdateChatListExistence();\n\t\t\t\t}\n\t\t\t} else if (id) {\n\t\t\t\tAssert(!_list.empty());\n\t\t\t\tif (_list.front() <= id) {\n\t\t\t\t\t_skippedAfter = 0;\n\t\t\t\t} else if (_list.back() >= id) {\n\t\t\t\t\t_skippedBefore = 0;\n\t\t\t\t}\n\t\t\t} else if (!_parent->parentChat()\n\t\t\t\t&& !_chatListMessage.value_or(nullptr)) {\n\t\t\t\tAssert(!_list.empty());\n\t\t\t\tapplyMaybeLast(owner().message(\n\t\t\t\t\towningHistory()->peer,\n\t\t\t\t\t_list.front()));\n\t\t\t}\n\t\t\tcheckReadTillEnd();\n\t\t\tif (const auto retry = base::take(_loadingAroundRetry)) {\n\t\t\t\tloadAround(*retry);\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tif (error.type() == u\"SAVED_DIALOGS_UNSUPPORTED\"_q) {\n\t\t\t\t_parent->markUnsupported();\n\t\t\t}\n\t\t\t_beforeId = 0;\n\t\t\t_loadingAround = std::nullopt;\n\t\t\tfinish();\n\t\t\tif (const auto retry = base::take(_loadingAroundRetry)) {\n\t\t\t\tloadAround(*retry);\n\t\t\t}\n\t\t}).send();\n\t};\n\t_loadingAround = id;\n\t_beforeId = histories().sendRequest(\n\t\towningHistory(),\n\t\tHistories::RequestType::History,\n\t\tsend);\n}\n\nvoid SavedSublist::loadBefore() {\n\tExpects(!_list.empty());\n\n\tif (_loadingAround) {\n\t\thistories().cancelRequest(base::take(_beforeId));\n\t} else if (_beforeId) {\n\t\treturn;\n\t}\n\n\tconst auto last = _list.back();\n\tconst auto send = [=](Fn<void()> finish) {\n\t\tusing Flag = MTPmessages_GetSavedHistory::Flag;\n\t\tconst auto parentChat = _parent->parentChat();\n\t\treturn session().api().request(MTPmessages_GetSavedHistory(\n\t\t\tMTP_flags(parentChat ? Flag::f_parent_peer : Flag(0)),\n\t\t\tparentChat ? parentChat->input() : MTPInputPeer(),\n\t\t\tsublistPeer()->input(),\n\t\t\tMTP_int(last), // offset_id\n\t\t\tMTP_int(0), // offset_date\n\t\t\tMTP_int(0), // add_offset\n\t\t\tMTP_int(kMessagesPerPage), // limit\n\t\t\tMTP_int(0), // min_id\n\t\t\tMTP_int(0), // max_id\n\t\t\tMTP_long(0) // hash\n\t\t)).done([=](const MTPmessages_Messages &result) {\n\t\t\t_beforeId = 0;\n\t\t\tfinish();\n\n\t\t\tif (_list.empty()) {\n\t\t\t\treturn;\n\t\t\t} else if (_list.back() != last) {\n\t\t\t\tloadBefore();\n\t\t\t} else if (processMessagesIsEmpty(result)) {\n\t\t\t\t_skippedBefore = 0;\n\t\t\t\tif (_skippedAfter == 0) {\n\t\t\t\t\t_fullCount = _list.size();\n\t\t\t\t}\n\t\t\t}\n\t\t}).fail([=] {\n\t\t\t_beforeId = 0;\n\t\t\tfinish();\n\t\t}).send();\n\t};\n\t_beforeId = histories().sendRequest(\n\t\towningHistory(),\n\t\tHistories::RequestType::History,\n\t\tsend);\n}\n\nvoid SavedSublist::loadAfter() {\n\tExpects(!_list.empty());\n\n\tif (_afterId) {\n\t\treturn;\n\t}\n\n\tconst auto first = _list.front();\n\tconst auto send = [=](Fn<void()> finish) {\n\t\tusing Flag = MTPmessages_GetSavedHistory::Flag;\n\t\tconst auto parentChat = _parent->parentChat();\n\t\treturn session().api().request(MTPmessages_GetSavedHistory(\n\t\t\tMTP_flags(parentChat ? Flag::f_parent_peer : Flag(0)),\n\t\t\tparentChat ? parentChat->input() : MTPInputPeer(),\n\t\t\tsublistPeer()->input(),\n\t\t\tMTP_int(first + 1), // offset_id\n\t\t\tMTP_int(0), // offset_date\n\t\t\tMTP_int(-kMessagesPerPage), // add_offset\n\t\t\tMTP_int(kMessagesPerPage), // limit\n\t\t\tMTP_int(0), // min_id\n\t\t\tMTP_int(0), // max_id\n\t\t\tMTP_long(0) // hash\n\t\t)).done([=](const MTPmessages_Messages &result) {\n\t\t\t_afterId = 0;\n\t\t\tfinish();\n\n\t\t\tif (_list.empty()) {\n\t\t\t\treturn;\n\t\t\t} else if (_list.front() != first) {\n\t\t\t\tloadAfter();\n\t\t\t} else if (processMessagesIsEmpty(result)) {\n\t\t\t\t_skippedAfter = 0;\n\t\t\t\tif (_skippedBefore == 0) {\n\t\t\t\t\t_fullCount = _list.size();\n\t\t\t\t}\n\t\t\t\tcheckReadTillEnd();\n\t\t\t}\n\t\t}).fail([=] {\n\t\t\t_afterId = 0;\n\t\t\tfinish();\n\t\t}).send();\n\t};\n\t_afterId = histories().sendRequest(\n\t\towningHistory(),\n\t\tHistories::RequestType::History,\n\t\tsend);\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_saved_sublist.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"data/data_thread.h\"\n#include \"dialogs/ui/dialogs_message_view.h\"\n\nclass PeerData;\nclass History;\n\nnamespace Data {\n\nclass Session;\nclass Histories;\nclass SavedMessages;\nstruct MessagePosition;\nstruct MessageUpdate;\nstruct SublistReadTillUpdate;\nstruct MessagesSlice;\n\nclass SavedSublist final : public Data::Thread {\npublic:\n\tSavedSublist(\n\t\tnot_null<SavedMessages*> parent,\n\t\tnot_null<PeerData*> sublistPeer);\n\t~SavedSublist();\n\n\t[[nodiscard]] bool inMonoforum() const;\n\t[[nodiscard]] bool isFeeRemoved() const;\n\tvoid toggleFeeRemoved(bool feeRemoved);\n\n\tvoid apply(const SublistReadTillUpdate &update);\n\tvoid apply(const MessageUpdate &update);\n\tvoid applyDifferenceTooLong();\n\tbool removeOne(not_null<HistoryItem*> item);\n\n\t[[nodiscard]] rpl::producer<MessagesSlice> source(\n\t\tMessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter);\n\n\t[[nodiscard]] not_null<SavedMessages*> parent() const;\n\t[[nodiscard]] not_null<History*> owningHistory() override;\n\t[[nodiscard]] ChannelData *parentChat() const;\n\t[[nodiscard]] not_null<PeerData*> sublistPeer() const;\n\t[[nodiscard]] bool isHiddenAuthor() const;\n\t[[nodiscard]] rpl::producer<> destroyed() const;\n\n\tvoid applyMaybeLast(not_null<HistoryItem*> item);\n\tvoid applyItemAdded(not_null<HistoryItem*> item);\n\tvoid applyItemRemoved(MsgId id);\n\n\t[[nodiscard]] rpl::producer<> changes() const;\n\t[[nodiscard]] std::optional<int> fullCount() const;\n\t[[nodiscard]] rpl::producer<int> fullCountValue() const;\n\tvoid loadFullCount();\n\n\t[[nodiscard]] bool unreadCountKnown() const;\n\t[[nodiscard]] int unreadCountCurrent() const;\n\t[[nodiscard]] int displayedUnreadCount() const;\n\t[[nodiscard]] rpl::producer<std::optional<int>> unreadCountValue() const;\n\tvoid setUnreadMark(bool unread);\n\n\tvoid applyMonoforumDialog(\n\t\tconst MTPDmonoForumDialog &dialog,\n\t\tnot_null<HistoryItem*> topItem);\n\tvoid readTillEnd();\n\tvoid requestChatListMessage();\n\tvoid setRestorePinnedWhenNonEmpty(bool restore);\n\n\tTimeId adjustedChatListTimeId() const override;\n\n\tint fixedOnTopIndex() const override;\n\tbool shouldBeInChatList() const override;\n\tDialogs::UnreadState chatListUnreadState() const override;\n\tDialogs::BadgesState chatListBadgesState() const override;\n\tHistoryItem *chatListMessage() const override;\n\tbool chatListMessageKnown() const override;\n\tconst QString &chatListName() const override;\n\tconst QString &chatListNameSortKey() const override;\n\tint chatListNameVersion() const override;\n\tconst base::flat_set<QString> &chatListNameWords() const override;\n\tconst base::flat_set<QChar> &chatListFirstLetters() const override;\n\n\tvoid hasUnreadMentionChanged(bool has) override;\n\tvoid hasUnreadReactionChanged(bool has) override;\n\tvoid hasUnreadPollVoteChanged(bool has) override;\n\n\t[[nodiscard]] HistoryItem *lastMessage() const;\n\t[[nodiscard]] HistoryItem *lastServerMessage() const;\n\t[[nodiscard]] bool lastMessageKnown() const;\n\t[[nodiscard]] bool lastServerMessageKnown() const;\n\t[[nodiscard]] MsgId lastKnownServerMessageId() const;\n\n\tvoid setInboxReadTill(MsgId readTillId, std::optional<int> unreadCount);\n\t[[nodiscard]] MsgId inboxReadTillId() const;\n\t[[nodiscard]] MsgId computeInboxReadTillFull() const;\n\n\tvoid setOutboxReadTill(MsgId readTillId);\n\t[[nodiscard]] MsgId computeOutboxReadTillFull() const;\n\n\t[[nodiscard]] bool isServerSideUnread(\n\t\tnot_null<const HistoryItem*> item) const override;\n\n\tvoid requestUnreadCount();\n\n\tvoid readTill(not_null<HistoryItem*> item);\n\tvoid readTill(MsgId tillId);\n\n\tvoid chatListPreloadData() override;\n\tvoid paintUserpic(\n\t\tPainter &p,\n\t\tUi::PeerUserpicView &view,\n\t\tconst Dialogs::Ui::PaintContext &context) const override;\n\n\t[[nodiscard]] auto sendActionPainter()\n\t\t-> HistoryView::SendActionPainter* override;\n\nprivate:\n\tstruct Viewer;\n\n\tenum class Flag : uchar {\n\t\tResolveChatListMessage = (1 << 0),\n\t\tInMonoforum = (1 << 1),\n\t\tFeeRemoved = (1 << 2),\n\t};\n\tfriend inline constexpr bool is_flag_type(Flag) { return true; }\n\tusing Flags = base::flags<Flag>;\n\n\t[[nodiscard]] Histories &histories();\n\n\tvoid subscribeToUnreadChanges();\n\t[[nodiscard]] Dialogs::UnreadState unreadStateFor(\n\t\tint count,\n\t\tbool known) const;\n\tvoid setLastMessage(HistoryItem *item);\n\tvoid setLastServerMessage(HistoryItem *item);\n\tvoid setChatListMessage(HistoryItem *item);\n\tvoid allowChatListMessageResolve();\n\tvoid resolveChatListMessageGroup();\n\tvoid growLastKnownServerMessageId(MsgId id);\n\n\tvoid changeUnreadCountByMessage(MsgId id, int delta);\n\tvoid setUnreadCount(std::optional<int> count);\n\tvoid readTill(MsgId tillId, HistoryItem *tillIdItem);\n\tvoid checkReadTillEnd();\n\tvoid sendReadTillRequest();\n\tvoid reloadUnreadCountIfNeeded();\n\n\t[[nodiscard]] bool buildFromData(not_null<Viewer*> viewer);\n\t[[nodiscard]] bool applyUpdate(const MessageUpdate &update);\n\tvoid appendClientSideMessages(MessagesSlice &slice);\n\t[[nodiscard]] std::optional<int> computeUnreadCountLocally(\n\t\tMsgId afterId) const;\n\tbool processMessagesIsEmpty(const MTPmessages_Messages &result);\n\tvoid loadAround(MsgId id);\n\tvoid loadBefore();\n\tvoid loadAfter();\n\n\tconst not_null<SavedMessages*> _parent;\n\tconst not_null<History*> _sublistHistory;\n\n\tMsgId _lastKnownServerMessageId = 0;\n\n\tstd::vector<MsgId> _list;\n\tstd::optional<int> _skippedBefore;\n\tstd::optional<int> _skippedAfter;\n\trpl::variable<std::optional<int>> _fullCount;\n\trpl::event_stream<> _listChanges;\n\trpl::event_stream<> _instantChanges;\n\tstd::optional<MsgId> _loadingAround;\n\tstd::optional<MsgId> _loadingAroundRetry;\n\trpl::variable<std::optional<int>> _unreadCount;\n\tMsgId _inboxReadTillId = 0;\n\tMsgId _outboxReadTillId = 0;\n\tFlags _flags;\n\n\tstd::optional<HistoryItem*> _lastMessage;\n\tstd::optional<HistoryItem*> _lastServerMessage;\n\tstd::optional<HistoryItem*> _chatListMessage;\n\tbase::flat_set<FullMsgId> _requestedGroups;\n\tint _beforeId = 0;\n\tint _afterId = 0;\n\n\tbase::Timer _readRequestTimer;\n\tmtpRequestId _readRequestId = 0;\n\tMsgId _sentReadTill = 0;\n\n\tbool _restorePinnedWhenNonEmpty = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_search_calendar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_search_calendar.h\"\n\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"data/data_document.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_search_controller.h\" // PrepareSearchFilter\n#include \"data/data_session.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n#include \"ui/dynamic_thumbnails.h\"\n\nnamespace Api {\n\nSearchCalendarController::SearchCalendarController(\n\tnot_null<Main::Session*> session,\n\tPeerId peerId,\n\tStorage::SharedMediaType type)\n: _session(session)\n, _peerId(peerId)\n, _type(type)\n, _api(&session->mtp()) {\n}\n\nvoid SearchCalendarController::monthThumbnails(\n\t\tTimeId date,\n\t\tFn<void(std::vector<DayThumbnail>)> onFinish) {\n\tconst auto parsed = base::unixtime::parse(date).date();\n\tconst auto key = MonthKey{\n\t\t.peerId = _peerId,\n\t\t.year = parsed.year(),\n\t\t.month = parsed.month(),\n\t};\n\n\tif (const auto it = _months.find(key); it != _months.end()) {\n\t\tif (!it->second.cache.empty()) {\n\t\t\tonFinish(it->second.cache);\n\t\t\treturn;\n\t\t}\n\t}\n\n\t_months[key].callbacks.push_back(std::move(onFinish));\n\n\tif (!_months[key].requestId) {\n\t\tperformMonthRequest(key);\n\t}\n}\n\nvoid SearchCalendarController::performMonthRequest(const MonthKey &key) {\n\tconst auto peer = _session->data().peer(key.peerId);\n\tconst auto filter = PrepareSearchFilter(_type);\n\n\tconst auto parsed = QDate(key.year, key.month, 1);\n\tconst auto endDate = base::unixtime::serialize(QDateTime(\n\t\tparsed.addMonths(1).addDays(-1),\n\t\tQTime(23, 59, 59)));\n\n\tauto &state = _months[key].state;\n\n\t_months[key].requestId = _api.request(\n\t\tMTPmessages_GetSearchResultsCalendar(\n\t\t\tMTP_flags(0),\n\t\t\tpeer->input(),\n\t\t\tMTPInputPeer(),\n\t\t\tfilter,\n\t\t\tMTP_int(state.offsetId),\n\t\t\tMTP_int(state.offsetDate ? state.offsetDate : endDate)\n\t)).done([=](const MTPmessages_SearchResultsCalendar &result) {\n\t\t_months[key].requestId = 0;\n\t\tconst auto &data = result.data();\n\t\t_session->data().processUsers(data.vusers());\n\t\t_session->data().processChats(data.vchats());\n\t\t_session->data().processMessages(\n\t\t\tdata.vmessages(),\n\t\t\tNewMessageType::Existing);\n\n\t\tauto messageIds = std::vector<FullMsgId>();\n\t\tmessageIds.reserve(data.vmessages().v.size());\n\t\tfor (const auto &message : data.vmessages().v) {\n\t\t\tmessageIds.push_back(\n\t\t\t\tFullMsgId(key.peerId, IdFromMessage(message)));\n\t\t}\n\n\t\tauto &monthState = _months[key].state;\n\t\tconst auto prevOffsetId = monthState.offsetId;\n\t\tconst auto prevOffsetDate = monthState.offsetDate;\n\t\tmonthState.offsetId = data.vmin_msg_id().v;\n\t\tmonthState.offsetDate = data.vmin_date().v;\n\n\t\tconst auto noMoreData = (prevOffsetId == monthState.offsetId\n\t\t\t&& prevOffsetDate == monthState.offsetDate\n\t\t\t&& prevOffsetId != 0);\n\n\t\tprocessMonthMessages(\n\t\t\tkey,\n\t\t\tmessageIds,\n\t\t\tdata.vmin_date().v,\n\t\t\tdata.vmin_msg_id().v,\n\t\t\tnoMoreData);\n\t}).fail([=] {\n\t\tauto &data = _months[key];\n\t\tdata.requestId = 0;\n\t\tdata.cache = {};\n\t\tfor (const auto &callback : data.callbacks) {\n\t\t\tcallback({});\n\t\t}\n\t\tdata.callbacks.clear();\n\t}).send();\n}\n\nvoid SearchCalendarController::processMonthMessages(\n\t\tconst MonthKey &key,\n\t\tconst std::vector<FullMsgId> &messages,\n\t\tTimeId minDate,\n\t\tMsgId minMsgId,\n\t\tbool noMoreData) {\n\tauto result = std::vector<DayThumbnail>();\n\tauto seenDays = base::flat_set<TimeId>();\n\n\tconst auto targetMonth = QDate(key.year, key.month, 1);\n\tconst auto targetStart = base::unixtime::serialize(\n\t\tQDateTime(targetMonth, QTime()));\n\tconst auto targetEnd = base::unixtime::serialize(QDateTime(\n\t\ttargetMonth.addMonths(1).addDays(-1),\n\t\tQTime(23, 59, 59)));\n\n\tfor (const auto &fullId : messages) {\n\t\tconst auto item = _session->data().message(fullId);\n\t\tif (!item) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto date = item->date();\n\t\tif (date < targetStart || date > targetEnd) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto parsed = base::unixtime::parse(date).date();\n\t\tconst auto dayStart = base::unixtime::serialize(\n\t\t\tQDateTime(parsed, QTime()));\n\n\t\tif (seenDays.contains(dayStart)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto media = item->media();\n\t\tif (!media) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto image = std::shared_ptr<Ui::DynamicImage>();\n\n\t\tif (const auto photo = media->photo()) {\n\t\t\timage = Ui::MakePhotoThumbnail(photo, item->fullId());\n\t\t} else if (const auto document = media->document()) {\n\t\t\tif (document->isVideoFile()) {\n\t\t\t\timage = Ui::MakeDocumentThumbnail(document, item->fullId());\n\t\t\t}\n\t\t}\n\n\t\tif (image) {\n\t\t\tseenDays.insert(dayStart);\n\t\t\tresult.push_back(DayThumbnail{\n\t\t\t\t.date = dayStart,\n\t\t\t\t.image = std::move(image),\n\t\t\t\t.msgId = fullId.msg,\n\t\t\t});\n\t\t}\n\t}\n\n\tif (result.empty()\n\t\t&& minDate < targetStart\n\t\t&& !_months[key].requestId\n\t\t&& !noMoreData) {\n\t\tperformMonthRequest(key);\n\t} else {\n\t\tauto &data = _months[key];\n\t\tdata.cache = result;\n\t\tfor (const auto &callback : data.callbacks) {\n\t\t\tcallback(result);\n\t\t}\n\t\tdata.callbacks.clear();\n\t}\n}\n\nstd::optional<MsgId> SearchCalendarController::resolveMsgIdByDate(\n\t\tTimeId date) const {\n\tconst auto parsed = base::unixtime::parse(date).date();\n\tconst auto key = MonthKey{\n\t\t.peerId = _peerId,\n\t\t.year = parsed.year(),\n\t\t.month = parsed.month(),\n\t};\n\n\tconst auto it = _months.find(key);\n\tif (it == _months.end() || it->second.cache.empty()) {\n\t\treturn std::nullopt;\n\t}\n\n\tconst auto dayStart = base::unixtime::serialize(\n\t\tQDateTime(parsed, QTime()));\n\n\tfor (const auto &thumb : it->second.cache) {\n\t\tif (thumb.date == dayStart) {\n\t\t\treturn thumb.msgId;\n\t\t}\n\t}\n\n\treturn std::nullopt;\n}\n\n} // namespace Api"
  },
  {
    "path": "Telegram/SourceFiles/data/data_search_calendar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n#include \"storage/storage_shared_media.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass DynamicImage;\n} // namespace Ui\n\nnamespace Api {\n\nstruct CalendarPeriod {\n\tTimeId date = 0;\n\tMsgId minMsgId = 0;\n\tMsgId maxMsgId = 0;\n\tint count = 0;\n};\n\nstruct CalendarResult {\n\tstd::vector<CalendarPeriod> periods;\n\tint count = 0;\n\tTimeId minDate = 0;\n\tMsgId minMsgId = 0;\n};\n\nstruct DayThumbnail {\n\tTimeId date = 0;\n\tstd::shared_ptr<Ui::DynamicImage> image;\n\tMsgId msgId = 0;\n};\n\nclass SearchCalendarController final {\npublic:\n\tSearchCalendarController(\n\t\tnot_null<Main::Session*> session,\n\t\tPeerId peerId,\n\t\tStorage::SharedMediaType type);\n\n\tvoid monthThumbnails(\n\t\tTimeId date,\n\t\tFn<void(std::vector<DayThumbnail>)> onFinish);\n\n\t[[nodiscard]] std::optional<MsgId> resolveMsgIdByDate(TimeId date) const;\n\nprivate:\n\tstruct MonthKey {\n\t\tPeerId peerId = 0;\n\t\tint year = 0;\n\t\tint month = 0;\n\n\t\tfriend inline auto operator<=>(\n\t\t\tconst MonthKey &,\n\t\t\tconst MonthKey &) = default;\n\t};\n\n\tstruct MonthState {\n\t\tMsgId offsetId = 0;\n\t\tTimeId offsetDate = 0;\n\t};\n\n\tstruct MonthData {\n\t\tstd::vector<DayThumbnail> cache;\n\t\tstd::vector<Fn<void(std::vector<DayThumbnail>)>> callbacks;\n\t\tmtpRequestId requestId = 0;\n\t\tMonthState state;\n\t};\n\n\tvoid performMonthRequest(const MonthKey &key);\n\tvoid processMonthMessages(\n\t\tconst MonthKey &key,\n\t\tconst std::vector<FullMsgId> &messages,\n\t\tTimeId minDate,\n\t\tMsgId minMsgId,\n\t\tbool noMoreData);\n\n\tconst not_null<Main::Session*> _session;\n\tconst PeerId _peerId;\n\tconst Storage::SharedMediaType _type;\n\n\tMTP::Sender _api;\n\n\tbase::flat_map<MonthKey, MonthData> _months;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_search_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_search_controller.h\"\n\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"data/data_messages.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_histories.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"apiwrap.h\"\n\nnamespace Api {\nnamespace {\n\nconstexpr auto kSharedMediaLimit = 100;\nconstexpr auto kFirstSharedMediaLimit = 0;\nconstexpr auto kHistoryLimit = 50;\nconstexpr auto kDefaultSearchTimeoutMs = crl::time(200);\n\n} // namespace\n\nMTPMessagesFilter PrepareSearchFilter(Storage::SharedMediaType type) {\n\tusing Type = Storage::SharedMediaType;\n\tswitch (type) {\n\tcase Type::Photo:\n\t\treturn MTP_inputMessagesFilterPhotos();\n\tcase Type::Video:\n\t\treturn MTP_inputMessagesFilterVideo();\n\tcase Type::PhotoVideo:\n\t\treturn MTP_inputMessagesFilterPhotoVideo();\n\tcase Type::MusicFile:\n\t\treturn MTP_inputMessagesFilterMusic();\n\tcase Type::File:\n\t\treturn MTP_inputMessagesFilterDocument();\n\tcase Type::VoiceFile:\n\t\treturn MTP_inputMessagesFilterVoice();\n\tcase Type::RoundVoiceFile:\n\t\treturn MTP_inputMessagesFilterRoundVoice();\n\tcase Type::RoundFile:\n\t\treturn MTP_inputMessagesFilterRoundVideo();\n\tcase Type::GIF:\n\t\treturn MTP_inputMessagesFilterGif();\n\tcase Type::Link:\n\t\treturn MTP_inputMessagesFilterUrl();\n\tcase Type::ChatPhoto:\n\t\treturn MTP_inputMessagesFilterChatPhotos();\n\tcase Type::Pinned:\n\t\treturn MTP_inputMessagesFilterPinned();\n\tcase Type::Poll:\n\t\treturn MTP_inputMessagesFilterPoll();\n\t}\n\treturn MTP_inputMessagesFilterEmpty();\n}\n\nstd::optional<GlobalMediaRequest> PrepareGlobalMediaRequest(\n\t\tnot_null<Main::Session*> session,\n\t\tint32 offsetRate,\n\t\tData::MessagePosition offsetPosition,\n\t\tStorage::SharedMediaType type,\n\t\tconst QString &query) {\n\tconst auto filter = PrepareSearchFilter(type);\n\tif (query.isEmpty() && filter.type() == mtpc_inputMessagesFilterEmpty) {\n\t\treturn std::nullopt;\n\t}\n\n\tconst auto minDate = 0;\n\tconst auto maxDate = 0;\n\tconst auto folderId = 0;\n\tconst auto limit = offsetPosition.fullId.peer\n\t\t? kSharedMediaLimit\n\t\t: kFirstSharedMediaLimit;\n\treturn MTPmessages_SearchGlobal(\n\t\tMTP_flags(MTPmessages_SearchGlobal::Flag::f_folder_id), // No archive\n\t\tMTP_int(folderId),\n\t\tMTP_string(query),\n\t\tfilter,\n\t\tMTP_int(minDate),\n\t\tMTP_int(maxDate),\n\t\tMTP_int(offsetRate),\n\t\t(offsetPosition.fullId.peer\n\t\t\t? session->data().peer(PeerId(offsetPosition.fullId.peer))->input()\n\t\t\t: MTP_inputPeerEmpty()),\n\t\tMTP_int(offsetPosition.fullId.msg),\n\t\tMTP_int(limit));\n}\n\nGlobalMediaResult ParseGlobalMediaResult(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPmessages_Messages &data) {\n\tauto result = GlobalMediaResult();\n\n\tauto messages = (const QVector<MTPMessage>*)nullptr;\n\tdata.match([&](const MTPDmessages_messagesNotModified &) {\n\t}, [&](const auto &data) {\n\t\tsession->data().processUsers(data.vusers());\n\t\tsession->data().processChats(data.vchats());\n\t\tmessages = &data.vmessages().v;\n\t});\n\tdata.match([&](const MTPDmessages_messagesNotModified &) {\n\t}, [&](const MTPDmessages_messages &data) {\n\t\tresult.fullCount = data.vmessages().v.size();\n\t}, [&](const MTPDmessages_messagesSlice &data) {\n\t\tresult.fullCount = data.vcount().v;\n\t\tresult.offsetRate = data.vnext_rate().value_or_empty();\n\t}, [&](const MTPDmessages_channelMessages &data) {\n\t\tresult.fullCount = data.vcount().v;\n\t});\n\tdata.match([&](const MTPDmessages_channelMessages &data) {\n\t\tLOG((\"API Error: received messages.channelMessages when \"\n\t\t\t\"no channel was passed! (ParseSearchResult)\"));\n\t}, [](const auto &) {});\n\n\tconst auto addType = NewMessageType::Existing;\n\tresult.messageIds.reserve(messages->size());\n\tfor (const auto &message : *messages) {\n\t\tconst auto item = session->data().addNewMessage(\n\t\t\tmessage,\n\t\t\tMessageFlags(),\n\t\t\taddType);\n\t\tif (item) {\n\t\t\tresult.messageIds.push_back(item->position());\n\t\t}\n\t}\n\treturn result;\n}\n\nstd::optional<SearchRequest> PrepareSearchRequest(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t    PeerId monoforumPeerId,\n\t\tStorage::SharedMediaType type,\n\t\tconst QString &query,\n\t\tMsgId messageId,\n\t\tData::LoadDirection direction) {\n\tconst auto filter = PrepareSearchFilter(type);\n\tif (query.isEmpty() && filter.type() == mtpc_inputMessagesFilterEmpty) {\n\t\treturn std::nullopt;\n\t}\n\n\tconst auto minId = 0;\n\tconst auto maxId = 0;\n\tconst auto limit = messageId ? kSharedMediaLimit : kFirstSharedMediaLimit;\n\tconst auto offsetId = [&] {\n\t\tswitch (direction) {\n\t\tcase Data::LoadDirection::Before:\n\t\tcase Data::LoadDirection::Around: return messageId;\n\t\tcase Data::LoadDirection::After: return messageId + 1;\n\t\t}\n\t\tUnexpected(\"Direction in PrepareSearchRequest\");\n\t}();\n\tconst auto addOffset = [&] {\n\t\tswitch (direction) {\n\t\tcase Data::LoadDirection::Before: return 0;\n\t\tcase Data::LoadDirection::Around: return -limit / 2;\n\t\tcase Data::LoadDirection::After: return -limit;\n\t\t}\n\t\tUnexpected(\"Direction in PrepareSearchRequest\");\n\t}();\n\tconst auto hash = uint64(0);\n\n\tconst auto mtpOffsetId = int(std::clamp(\n\t\toffsetId.bare,\n\t\tint64(0),\n\t\tint64(0x3FFFFFFF)));\n\tusing Flag = MTPmessages_Search::Flag;\n\treturn MTPmessages_Search(\n\t\tMTP_flags((topicRootId ? Flag::f_top_msg_id : Flag(0))\n\t\t\t| (monoforumPeerId ? Flag::f_saved_peer_id : Flag(0))),\n\t\tpeer->input(),\n\t\tMTP_string(query),\n\t\tMTP_inputPeerEmpty(),\n\t\t(monoforumPeerId\n\t\t\t? peer->owner().peer(monoforumPeerId)->input()\n\t\t\t: MTPInputPeer()),\n\t\tMTPVector<MTPReaction>(), // saved_reaction\n\t\tMTP_int(topicRootId),\n\t\tfilter,\n\t\tMTP_int(0), // min_date\n\t\tMTP_int(0), // max_date\n\t\tMTP_int(mtpOffsetId),\n\t\tMTP_int(addOffset),\n\t\tMTP_int(limit),\n\t\tMTP_int(maxId),\n\t\tMTP_int(minId),\n\t\tMTP_long(hash));\n}\n\nSearchResult ParseSearchResult(\n\t\tnot_null<PeerData*> peer,\n\t\tStorage::SharedMediaType type,\n\t\tMsgId messageId,\n\t\tData::LoadDirection direction,\n\t\tconst SearchRequestResult &data) {\n\tauto result = SearchResult();\n\tresult.noSkipRange = MsgRange{ messageId, messageId };\n\n\tauto messages = [&] {\n\t\tswitch (data.type()) {\n\t\tcase mtpc_messages_messages: {\n\t\t\tauto &d = data.c_messages_messages();\n\t\t\tpeer->owner().processUsers(d.vusers());\n\t\t\tpeer->owner().processChats(d.vchats());\n\t\t\tpeer->processTopics(d.vtopics());\n\t\t\tresult.fullCount = d.vmessages().v.size();\n\t\t\treturn &d.vmessages().v;\n\t\t} break;\n\n\t\tcase mtpc_messages_messagesSlice: {\n\t\t\tauto &d = data.c_messages_messagesSlice();\n\t\t\tpeer->owner().processUsers(d.vusers());\n\t\t\tpeer->owner().processChats(d.vchats());\n\t\t\tpeer->processTopics(d.vtopics());\n\t\t\tresult.fullCount = d.vcount().v;\n\t\t\treturn &d.vmessages().v;\n\t\t} break;\n\n\t\tcase mtpc_messages_channelMessages: {\n\t\t\tconst auto &d = data.c_messages_channelMessages();\n\t\t\tpeer->owner().processUsers(d.vusers());\n\t\t\tpeer->owner().processChats(d.vchats());\n\t\t\tif (const auto channel = peer->asChannel()) {\n\t\t\t\tchannel->ptsReceived(d.vpts().v);\n\t\t\t} else {\n\t\t\t\tLOG((\"API Error: received messages.channelMessages when \"\n\t\t\t\t\t\"no channel was passed! (ParseSearchResult)\"));\n\t\t\t}\n\t\t\tpeer->processTopics(d.vtopics());\n\t\t\tresult.fullCount = d.vcount().v;\n\t\t\treturn &d.vmessages().v;\n\t\t} break;\n\n\t\tcase mtpc_messages_messagesNotModified: {\n\t\t\tLOG((\"API Error: received messages.messagesNotModified! \"\n\t\t\t\t\"(ParseSearchResult)\"));\n\t\t\treturn (const QVector<MTPMessage>*)nullptr;\n\t\t} break;\n\t\t}\n\t\tUnexpected(\"messages.Messages type in ParseSearchResult()\");\n\t}();\n\n\tif (!messages) {\n\t\treturn result;\n\t}\n\n\tconst auto addType = NewMessageType::Existing;\n\tresult.messageIds.reserve(messages->size());\n\tfor (const auto &message : *messages) {\n\t\tconst auto item = peer->owner().addNewMessage(\n\t\t\tmessage,\n\t\t\tMessageFlags(),\n\t\t\taddType);\n\t\tif (item) {\n\t\t\tconst auto itemId = item->id;\n\t\t\tif ((type == Storage::SharedMediaType::kCount)\n\t\t\t\t|| item->sharedMediaTypes().test(type)) {\n\t\t\t\tresult.messageIds.push_back(itemId);\n\t\t\t}\n\t\t\taccumulate_min(result.noSkipRange.from, itemId);\n\t\t\taccumulate_max(result.noSkipRange.till, itemId);\n\t\t}\n\t}\n\tif (messageId && result.messageIds.empty()) {\n\t\tresult.noSkipRange = [&]() -> MsgRange {\n\t\t\tswitch (direction) {\n\t\t\tcase Data::LoadDirection::Before: // All old loaded.\n\t\t\t\treturn { 0, result.noSkipRange.till };\n\t\t\tcase Data::LoadDirection::Around: // All loaded.\n\t\t\t\treturn { 0, ServerMaxMsgId };\n\t\t\tcase Data::LoadDirection::After: // All new loaded.\n\t\t\t\treturn { result.noSkipRange.from, ServerMaxMsgId };\n\t\t\t}\n\t\t\tUnexpected(\"Direction in ParseSearchResult\");\n\t\t}();\n\t}\n\treturn result;\n}\n\nHistoryRequest PrepareHistoryRequest(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId messageId,\n\t\tData::LoadDirection direction) {\n\tconst auto minId = 0;\n\tconst auto maxId = 0;\n\tconst auto limit = kHistoryLimit;\n\tconst auto offsetId = [&] {\n\t\tswitch (direction) {\n\t\tcase Data::LoadDirection::Before:\n\t\tcase Data::LoadDirection::Around: return messageId;\n\t\tcase Data::LoadDirection::After: return messageId + 1;\n\t\t}\n\t\tUnexpected(\"Direction in PrepareSearchRequest\");\n\t}();\n\tconst auto addOffset = [&] {\n\t\tswitch (direction) {\n\t\tcase Data::LoadDirection::Before: return 0;\n\t\tcase Data::LoadDirection::Around: return -limit / 2;\n\t\tcase Data::LoadDirection::After: return -limit;\n\t\t}\n\t\tUnexpected(\"Direction in PrepareSearchRequest\");\n\t}();\n\tconst auto hash = uint64(0);\n\tconst auto offsetDate = int32(0);\n\n\tconst auto mtpOffsetId = int(std::clamp(\n\t\toffsetId.bare,\n\t\tint64(0),\n\t\tint64(0x3FFFFFFF)));\n\treturn MTPmessages_GetHistory(\n\t\tpeer->input(),\n\t\tMTP_int(mtpOffsetId),\n\t\tMTP_int(offsetDate),\n\t\tMTP_int(addOffset),\n\t\tMTP_int(limit),\n\t\tMTP_int(maxId),\n\t\tMTP_int(minId),\n\t\tMTP_long(hash));\n}\n\nHistoryResult ParseHistoryResult(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId messageId,\n\t\tData::LoadDirection direction,\n\t\tconst HistoryRequestResult &data) {\n\treturn ParseSearchResult(\n\t\tpeer,\n\t\tStorage::SharedMediaType::kCount,\n\t\tmessageId,\n\t\tdirection,\n\t\tdata);\n}\n\nSearchController::CacheEntry::CacheEntry(\n\tnot_null<Main::Session*> session,\n\tconst Query &query)\n: peerData(session->data().peer(query.peerId))\n, migratedData(query.migratedPeerId\n\t? base::make_optional(Data(session->data().peer(query.migratedPeerId)))\n\t: std::nullopt) {\n}\n\nSearchController::SearchController(not_null<Main::Session*> session)\n: _session(session) {\n}\n\nbool SearchController::hasInCache(const Query &query) const {\n\treturn query.query.isEmpty() || _cache.contains(query);\n}\n\nvoid SearchController::setQuery(const Query &query) {\n\tif (query.query.isEmpty()) {\n\t\t_cache.clear();\n\t\t_current = _cache.end();\n\t} else {\n\t\t_current = _cache.find(query);\n\t}\n\tif (_current == _cache.end()) {\n\t\t_current = _cache.emplace(\n\t\t\tquery,\n\t\t\tstd::make_unique<CacheEntry>(_session, query)).first;\n\t}\n}\n\nrpl::producer<SparseIdsMergedSlice> SearchController::idsSlice(\n\t\tSparseIdsMergedSlice::UniversalMsgId aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tExpects(_current != _cache.cend());\n\n\tauto query = (const Query&)_current->first;\n\tauto createSimpleViewer = [=](\n\t\t\tPeerId peerId,\n\t\t\tMsgId topicRootId,\n\t\t\tPeerId monoforumPeerId,\n\t\t\tSparseIdsSlice::Key simpleKey,\n\t\t\tint limitBefore,\n\t\t\tint limitAfter) {\n\t\treturn simpleIdsSlice(\n\t\t\tpeerId,\n\t\t\ttopicRootId,\n\t\t\tmonoforumPeerId,\n\t\t\tsimpleKey,\n\t\t\tquery,\n\t\t\tlimitBefore,\n\t\t\tlimitAfter);\n\t};\n\treturn SparseIdsMergedSlice::CreateViewer(\n\t\tSparseIdsMergedSlice::Key(\n\t\t\tquery.peerId,\n\t\t\tquery.topicRootId,\n\t\t\tquery.monoforumPeerId,\n\t\t\tquery.migratedPeerId,\n\t\t\taroundId),\n\t\tlimitBefore,\n\t\tlimitAfter,\n\t\tstd::move(createSimpleViewer));\n}\n\nrpl::producer<SparseIdsSlice> SearchController::simpleIdsSlice(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tMsgId aroundId,\n\t\tconst Query &query,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tExpects(peerId != 0);\n\tExpects(IsServerMsgId(aroundId) || (aroundId == 0));\n\tExpects((aroundId != 0)\n\t\t|| (limitBefore == 0 && limitAfter == 0));\n\tExpects((query.peerId == peerId\n\t\t&& query.topicRootId == topicRootId\n\t\t&& query.monoforumPeerId == monoforumPeerId)\n\t\t|| (query.migratedPeerId == peerId\n\t\t\t&& MsgId(0) == topicRootId\n\t\t\t&& PeerId(0) == monoforumPeerId));\n\n\tauto it = _cache.find(query);\n\tif (it == _cache.end()) {\n\t\treturn [=](auto) { return rpl::lifetime(); };\n\t}\n\n\tauto listData = (peerId == query.peerId)\n\t\t? &it->second->peerData\n\t\t: &*it->second->migratedData;\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tauto builder = lifetime.make_state<SparseIdsSliceBuilder>(\n\t\t\taroundId,\n\t\t\tlimitBefore,\n\t\t\tlimitAfter);\n\t\tbuilder->insufficientAround(\n\t\t) | rpl::on_next([=](\n\t\t\t\tconst SparseIdsSliceBuilder::AroundData &data) {\n\t\t\trequestMore(data, query, listData);\n\t\t}, lifetime);\n\n\t\tauto pushNextSnapshot = [=] {\n\t\t\tconsumer.put_next(builder->snapshot());\n\t\t};\n\n\t\tlistData->list.sliceUpdated(\n\t\t) | rpl::filter([=](const SliceUpdate &update) {\n\t\t\treturn builder->applyUpdate(update);\n\t\t}) | rpl::on_next(pushNextSnapshot, lifetime);\n\n\t\t_session->data().itemRemoved(\n\t\t) | rpl::filter([=](not_null<const HistoryItem*> item) {\n\t\t\treturn (item->history()->peer->id == peerId)\n\t\t\t\t&& (!topicRootId || item->topicRootId() == topicRootId)\n\t\t\t\t&& (!monoforumPeerId\n\t\t\t\t\t|| item->sublistPeerId() == monoforumPeerId);\n\t\t}) | rpl::filter([=](not_null<const HistoryItem*> item) {\n\t\t\treturn builder->removeOne(item->id);\n\t\t}) | rpl::on_next(pushNextSnapshot, lifetime);\n\n\t\t_session->data().historyCleared(\n\t\t) | rpl::filter([=](not_null<const History*> history) {\n\t\t\treturn (history->peer->id == peerId);\n\t\t}) | rpl::filter([=] {\n\t\t\treturn builder->removeAll();\n\t\t}) | rpl::on_next(pushNextSnapshot, lifetime);\n\n\t\tusing Result = Storage::SparseIdsListResult;\n\t\tlistData->list.query(Storage::SparseIdsListQuery(\n\t\t\taroundId,\n\t\t\tlimitBefore,\n\t\t\tlimitAfter\n\t\t)) | rpl::filter([=](const Result &result) {\n\t\t\treturn builder->applyInitial(result);\n\t\t}) | rpl::on_next_done(\n\t\t\tpushNextSnapshot,\n\t\t\t[=] { builder->checkInsufficient(); },\n\t\t\tlifetime);\n\n\t\treturn lifetime;\n\t};\n}\n\nauto SearchController::saveState() -> SavedState {\n\tauto result = SavedState();\n\tif (_current != _cache.end()) {\n\t\tresult.query = _current->first;\n\t\tresult.peerList = std::move(_current->second->peerData.list);\n\t\tif (auto &migrated = _current->second->migratedData) {\n\t\t\tresult.migratedList = std::move(migrated->list);\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid SearchController::restoreState(SavedState &&state) {\n\tif (!state.query.peerId) {\n\t\treturn;\n\t}\n\n\tauto it = _cache.find(state.query);\n\tif (it == _cache.end()) {\n\t\tit = _cache.emplace(\n\t\t\tstate.query,\n\t\t\tstd::make_unique<CacheEntry>(_session, state.query)).first;\n\t}\n\tauto replace = Data(it->second->peerData.peer);\n\treplace.list = std::move(state.peerList);\n\tit->second->peerData = std::move(replace);\n\tif (auto &migrated = state.migratedList) {\n\t\tAssert(it->second->migratedData.has_value());\n\t\tauto replace = Data(it->second->migratedData->peer);\n\t\treplace.list = std::move(*migrated);\n\t\tit->second->migratedData = std::move(replace);\n\t}\n\t_current = it;\n}\n\nvoid SearchController::requestMore(\n\t\tconst SparseIdsSliceBuilder::AroundData &key,\n\t\tconst Query &query,\n\t\tData *listData) {\n\tif (listData->requests.contains(key)) {\n\t\treturn;\n\t}\n\tauto prepared = PrepareSearchRequest(\n\t\tlistData->peer,\n\t\tquery.topicRootId,\n\t\tquery.monoforumPeerId,\n\t\tquery.type,\n\t\tquery.query,\n\t\tkey.aroundId,\n\t\tkey.direction);\n\tif (!prepared) {\n\t\treturn;\n\t}\n\tauto &histories = _session->data().histories();\n\tconst auto type = ::Data::Histories::RequestType::History;\n\tconst auto history = _session->data().history(listData->peer);\n\tauto requestId = histories.sendRequest(history, type, [=](Fn<void()> finish) {\n\t\treturn _session->api().request(\n\t\t\tstd::move(*prepared)\n\t\t).done([=](const SearchRequestResult &result) {\n\t\t\tlistData->requests.remove(key);\n\t\t\tauto parsed = ParseSearchResult(\n\t\t\t\tlistData->peer,\n\t\t\t\tquery.type,\n\t\t\t\tkey.aroundId,\n\t\t\t\tkey.direction,\n\t\t\t\tresult);\n\t\t\tlistData->list.addSlice(\n\t\t\t\tstd::move(parsed.messageIds),\n\t\t\t\tparsed.noSkipRange,\n\t\t\t\tparsed.fullCount);\n\t\t\tfinish();\n\t\t}).fail([=] {\n\t\t\tfinish();\n\t\t}).send();\n\t});\n\tlistData->requests.emplace(key, [=] {\n\t\t_session->data().histories().cancelRequest(requestId);\n\t});\n}\n\nDelayedSearchController::DelayedSearchController(\n\tnot_null<Main::Session*> session)\n: _controller(session) {\n\t_timer.setCallback([this] { setQueryFast(_nextQuery); });\n}\n\nvoid DelayedSearchController::setQuery(const Query &query) {\n\tsetQuery(query, kDefaultSearchTimeoutMs);\n}\n\nvoid DelayedSearchController::setQuery(\n\t\tconst Query &query,\n\t\tcrl::time delay) {\n\tif (currentQuery() == query) {\n\t\t_timer.cancel();\n\t\treturn;\n\t}\n\tif (_controller.hasInCache(query)) {\n\t\tsetQueryFast(query);\n\t} else {\n\t\t_nextQuery = query;\n\t\t_timer.callOnce(delay);\n\t}\n}\n\nvoid DelayedSearchController::setQueryFast(const Query &query) {\n\t_controller.setQuery(query);\n\t_currentQueryChanges.fire_copy(query.query);\n}\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_search_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_sparse_ids.h\"\n#include \"storage/storage_sparse_ids_list.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"base/timer.h\"\n#include \"base/qt/qt_compare.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\nenum class LoadDirection : char;\nstruct MessagePosition;\n} // namespace Data\n\nnamespace Api {\n\nstruct SearchResult {\n\tstd::vector<MsgId> messageIds;\n\tMsgRange noSkipRange;\n\tint fullCount = 0;\n};\n\nusing SearchRequest = MTPmessages_Search;\nusing SearchRequestResult = MTPmessages_Messages;\n\nusing HistoryResult = SearchResult;\nusing HistoryRequest = MTPmessages_GetHistory;\nusing HistoryRequestResult = MTPmessages_Messages;\n\nusing GlobalMediaRequest = MTPmessages_SearchGlobal;\nstruct GlobalMediaResult {\n\tstd::vector<Data::MessagePosition> messageIds;\n\tint32 offsetRate = 0;\n\tint fullCount = 0;\n};\n\n[[nodiscard]] MTPMessagesFilter PrepareSearchFilter(\n\tStorage::SharedMediaType type);\n\n[[nodiscard]] std::optional<GlobalMediaRequest> PrepareGlobalMediaRequest(\n\tnot_null<Main::Session*> session,\n\tint32 offsetRate,\n\tData::MessagePosition offsetPosition,\n\tStorage::SharedMediaType type,\n\tconst QString &query);\n\n[[nodiscard]] GlobalMediaResult ParseGlobalMediaResult(\n\tnot_null<Main::Session*> session,\n\tconst MTPmessages_Messages &data);\n\n[[nodiscard]] std::optional<SearchRequest> PrepareSearchRequest(\n\tnot_null<PeerData*> peer,\n\tMsgId topicRootId,\n\tPeerId monoforumPeerId,\n\tStorage::SharedMediaType type,\n\tconst QString &query,\n\tMsgId messageId,\n\tData::LoadDirection direction);\n\n[[nodiscard]] SearchResult ParseSearchResult(\n\tnot_null<PeerData*> peer,\n\tStorage::SharedMediaType type,\n\tMsgId messageId,\n\tData::LoadDirection direction,\n\tconst SearchRequestResult &data);\n\n[[nodiscard]] HistoryRequest PrepareHistoryRequest(\n\tnot_null<PeerData*> peer,\n\tMsgId messageId,\n\tData::LoadDirection direction);\n\n[[nodiscard]] HistoryResult ParseHistoryResult(\n\tnot_null<PeerData*> peer,\n\tMsgId messageId,\n\tData::LoadDirection direction,\n\tconst HistoryRequestResult &data);\n\nclass SearchController final {\npublic:\n\tusing IdsList = Storage::SparseIdsList;\n\tstruct Query {\n\t\tusing MediaType = Storage::SharedMediaType;\n\n\t\tPeerId peerId = 0;\n\t\tMsgId topicRootId = 0;\n\t\tPeerId monoforumPeerId = 0;\n\t\tPeerId migratedPeerId = 0;\n\t\tMediaType type = MediaType::kCount;\n\t\tQString query;\n\t\t// from_id, min_date, max_date\n\n\t\tfriend inline std::strong_ordering operator<=>(\n\t\t\tconst Query &a,\n\t\t\tconst Query &b) noexcept = default;\n\n\t};\n\tstruct SavedState {\n\t\tQuery query;\n\t\tIdsList peerList;\n\t\tstd::optional<IdsList> migratedList;\n\t};\n\n\texplicit SearchController(not_null<Main::Session*> session);\n\tvoid setQuery(const Query &query);\n\tbool hasInCache(const Query &query) const;\n\n\tQuery query() const {\n\t\tExpects(_current != _cache.cend());\n\n\t\treturn _current->first;\n\t}\n\n\trpl::producer<SparseIdsMergedSlice> idsSlice(\n\t\tSparseIdsMergedSlice::UniversalMsgId aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter);\n\n\tSavedState saveState();\n\tvoid restoreState(SavedState &&state);\n\nprivate:\n\tstruct Data {\n\t\texplicit Data(not_null<PeerData*> peer) : peer(peer) {\n\t\t}\n\n\t\tnot_null<PeerData*> peer;\n\t\tIdsList list;\n\t\tbase::flat_map<\n\t\t\tSparseIdsSliceBuilder::AroundData,\n\t\t\trpl::lifetime> requests;\n\t};\n\tusing SliceUpdate = Storage::SparseIdsSliceUpdate;\n\n\tstruct CacheEntry {\n\t\tCacheEntry(not_null<Main::Session*> session, const Query &query);\n\n\t\tData peerData;\n\t\tstd::optional<Data> migratedData;\n\t};\n\n\tusing Cache = base::flat_map<Query, std::unique_ptr<CacheEntry>>;\n\n\trpl::producer<SparseIdsSlice> simpleIdsSlice(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tMsgId aroundId,\n\t\tconst Query &query,\n\t\tint limitBefore,\n\t\tint limitAfter);\n\tvoid requestMore(\n\t\tconst SparseIdsSliceBuilder::AroundData &key,\n\t\tconst Query &query,\n\t\tData *listData);\n\n\tconst not_null<Main::Session*> _session;\n\tCache _cache;\n\tCache::iterator _current = _cache.end();\n\n};\n\nclass DelayedSearchController {\npublic:\n\texplicit DelayedSearchController(not_null<Main::Session*> session);\n\n\tusing Query = SearchController::Query;\n\tusing SavedState = SearchController::SavedState;\n\n\tvoid setQuery(const Query &query);\n\tvoid setQuery(const Query &query, crl::time delay);\n\tvoid setQueryFast(const Query &query);\n\n\tQuery currentQuery() const {\n\t\treturn _controller.query();\n\t}\n\n\trpl::producer<SparseIdsMergedSlice> idsSlice(\n\t\t\tSparseIdsMergedSlice::UniversalMsgId aroundId,\n\t\t\tint limitBefore,\n\t\t\tint limitAfter) {\n\t\treturn _controller.idsSlice(\n\t\t\taroundId,\n\t\t\tlimitBefore,\n\t\t\tlimitAfter);\n\t}\n\n\trpl::producer<QString> currentQueryValue() const {\n\t\treturn _currentQueryChanges.events_starting_with(\n\t\t\tcurrentQuery().query);\n\t}\n\n\tSavedState saveState() {\n\t\treturn _controller.saveState();\n\t}\n\n\tvoid restoreState(SavedState &&state) {\n\t\t_controller.restoreState(std::move(state));\n\t}\n\nprivate:\n\tSearchController _controller;\n\tQuery _nextQuery;\n\tbase::Timer _timer;\n\trpl::event_stream<QString> _currentQueryChanges;\n\n};\n\n} // namespace Api\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_send_action.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_send_action.h\"\n\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_send_action.h\"\n\nnamespace Data {\n\nSendActionManager::SendActionManager()\n: _animation([=](crl::time now) { return callback(now); }) {\n}\n\nHistoryView::SendActionPainter *SendActionManager::lookupPainter(\n\t\tnot_null<History*> history,\n\t\tMsgId rootId) {\n\tif (!rootId) {\n\t\treturn history->sendActionPainter();\n\t}\n\tconst auto i = _painters.find(history);\n\tif (i == end(_painters)) {\n\t\treturn nullptr;\n\t}\n\tconst auto j = i->second.find(rootId);\n\tif (j == end(i->second)) {\n\t\treturn nullptr;\n\t}\n\tconst auto result = j->second.lock();\n\tif (!result) {\n\t\ti->second.erase(j);\n\t\tif (i->second.empty()) {\n\t\t\t_painters.erase(i);\n\t\t}\n\t\treturn nullptr;\n\t}\n\tcrl::on_main([copy = result] {\n\t});\n\treturn result.get();\n}\n\nvoid SendActionManager::registerFor(\n\t\tnot_null<History*> history,\n\t\tMsgId rootId,\n\t\tnot_null<UserData*> user,\n\t\tconst MTPSendMessageAction &action,\n\t\tTimeId when) {\n\tif (history->peer->isSelf()) {\n\t\treturn;\n\t}\n\tconst auto sendAction = lookupPainter(history, rootId);\n\tif (!sendAction) {\n\t\treturn;\n\t}\n\tif (sendAction->updateNeedsAnimating(user, action)) {\n\t\tuser->madeAction(when);\n\n\t\tif (!_sendActions.contains(std::pair{ history, rootId })) {\n\t\t\t_sendActions.emplace(std::pair{ history, rootId }, crl::now());\n\t\t\t_animation.start();\n\t\t}\n\t}\n}\n\nauto SendActionManager::repliesPainter(\n\tnot_null<History*> history,\n\tMsgId rootId)\n-> std::shared_ptr<SendActionPainter> {\n\tauto &weak = _painters[history][rootId];\n\tif (auto strong = weak.lock()) {\n\t\treturn strong;\n\t}\n\tauto result = std::make_shared<SendActionPainter>(history, rootId);\n\tweak = result;\n\treturn result;\n}\n\nvoid SendActionManager::repliesPainterRemoved(\n\t\tnot_null<History*> history,\n\t\tMsgId rootId) {\n\tconst auto i = _painters.find(history);\n\tif (i == end(_painters)) {\n\t\treturn;\n\t}\n\tconst auto j = i->second.find(rootId);\n\tif (j == end(i->second) || j->second.lock()) {\n\t\treturn;\n\t}\n\ti->second.erase(j);\n\tif (i->second.empty()) {\n\t\t_painters.erase(i);\n\t}\n}\n\nvoid SendActionManager::repliesPaintersClear(\n\t\tnot_null<History*> history,\n\t\tnot_null<UserData*> user) {\n\tauto &map = _painters[history];\n\tfor (auto i = map.begin(); i != map.end();) {\n\t\tif (auto strong = i->second.lock()) {\n\t\t\tstrong->clear(user);\n\t\t\t++i;\n\t\t} else {\n\t\t\ti = map.erase(i);\n\t\t}\n\t}\n\tif (map.empty()) {\n\t\t_painters.erase(history);\n\t}\n}\n\nbool SendActionManager::callback(crl::time now) {\n\tfor (auto i = begin(_sendActions); i != end(_sendActions);) {\n\t\tconst auto sendAction = lookupPainter(\n\t\t\ti->first.first,\n\t\t\ti->first.second);\n\t\tif (sendAction && sendAction->updateNeedsAnimating(now)) {\n\t\t\t++i;\n\t\t} else {\n\t\t\ti = _sendActions.erase(i);\n\t\t}\n\t}\n\treturn !_sendActions.empty();\n}\n\nauto SendActionManager::animationUpdated() const\n-> rpl::producer<SendActionManager::AnimationUpdate> {\n\treturn _animationUpdate.events();\n}\n\nvoid SendActionManager::updateAnimation(AnimationUpdate &&update) {\n\t_animationUpdate.fire(std::move(update));\n}\n\nauto SendActionManager::speakingAnimationUpdated() const\n-> rpl::producer<not_null<History*>> {\n\treturn _speakingAnimationUpdate.events();\n}\n\nvoid SendActionManager::updateSpeakingAnimation(not_null<History*> history) {\n\t_speakingAnimationUpdate.fire_copy(history);\n}\n\nvoid SendActionManager::clear() {\n\t_sendActions.clear();\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_send_action.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n\nclass History;\n\nnamespace HistoryView {\nclass SendActionPainter;\n} // namespace HistoryView\n\nnamespace Data {\n\nclass Thread;\n\nclass SendActionManager final {\npublic:\n\tstruct AnimationUpdate {\n\t\tnot_null<Thread*> thread;\n\t\tQRect rect;\n\t\tbool textUpdated = false;\n\t};\n\texplicit SendActionManager();\n\n\tvoid registerFor(\n\t\tnot_null<History*> history,\n\t\tMsgId rootId,\n\t\tnot_null<UserData*> user,\n\t\tconst MTPSendMessageAction &action,\n\t\tTimeId when);\n\n\t[[nodiscard]] auto animationUpdated() const\n\t\t-> rpl::producer<AnimationUpdate>;\n\tvoid updateAnimation(AnimationUpdate &&update);\n\t[[nodiscard]] auto speakingAnimationUpdated() const\n\t\t-> rpl::producer<not_null<History*>>;\n\tvoid updateSpeakingAnimation(not_null<History*> history);\n\n\tusing SendActionPainter = HistoryView::SendActionPainter;\n\t[[nodiscard]] std::shared_ptr<SendActionPainter> repliesPainter(\n\t\tnot_null<History*> history,\n\t\tMsgId rootId);\n\tvoid repliesPainterRemoved(\n\t\tnot_null<History*> history,\n\t\tMsgId rootId);\n\tvoid repliesPaintersClear(\n\t\tnot_null<History*> history,\n\t\tnot_null<UserData*> user);\n\n\tvoid clear();\n\nprivate:\n\tbool callback(crl::time now);\n\t[[nodiscard]] SendActionPainter *lookupPainter(\n\t\tnot_null<History*> history,\n\t\tMsgId rootId);\n\n\t// When typing in this history started.\n\tbase::flat_map<\n\t\tstd::pair<not_null<History*>, MsgId>,\n\t\tcrl::time> _sendActions;\n\tUi::Animations::Basic _animation;\n\n\trpl::event_stream<AnimationUpdate> _animationUpdate;\n\trpl::event_stream<not_null<History*>> _speakingAnimationUpdate;\n\n\tbase::flat_map<\n\t\tnot_null<History*>,\n\t\tbase::flat_map<\n\t\t\tMsgId,\n\t\t\tstd::weak_ptr<SendActionPainter>>> _painters;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_session.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_session.h\"\n\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"main/main_app_config.h\"\n#include \"apiwrap.h\"\n#include \"mainwidget.h\"\n#include \"api/api_bot.h\"\n#include \"api/api_premium.h\"\n#include \"api/api_text_entities.h\"\n#include \"api/api_user_names.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/mime_type.h\" // Core::IsMimeSticker\n#include \"ui/image/image_location_factory.h\" // Images::FromPhotoSize\n#include \"ui/text/format_values.h\" // Ui::FormatPhone\n#include \"ui/color_int_conversion.h\"\n#include \"export/export_manager.h\"\n#include \"export/view/export_view_panel_controller.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"window/notifications_manager.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_streamed_drafts.h\"\n#include \"history/view/media/history_view_media.h\"\n#include \"history/view/history_view_element.h\"\n#include \"inline_bots/inline_bot_layout_item.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/storage_encrypted_file.h\"\n#include \"media/player/media_player_instance.h\" // instance()->play()\n#include \"media/audio/media_audio.h\"\n#include \"boxes/abstract_box.h\"\n#include \"passport/passport_form_controller.h\"\n#include \"iv/iv_data.h\"\n#include \"lang/lang_keys.h\" // tr::lng_deleted(tr::now) in user name\n#include \"data/business/data_business_chatbots.h\"\n#include \"data/business/data_business_info.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/components/scheduled_messages.h\"\n#include \"data/components/sponsored_messages.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/data_bot_app.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_download_manager.h\"\n#include \"data/data_web_page.h\"\n#include \"data/data_game.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_replies_list.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_send_action.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_forum_icons.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_music.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_streaming.h\"\n#include \"data/data_media_rotation.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_todo_list.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/unixtime.h\"\n#include \"base/call_delayed.h\"\n#include \"base/random.h\"\n#include \"spellcheck/spellcheck_highlight_syntax.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kNextForUpgradeGiftTimeout = 5 * crl::time(1000);\n\nusing ViewElement = HistoryView::Element;\n\n// s: box 100x100\n// m: box 320x320\n// x: box 800x800\n// y: box 1280x1280\n// w: box 2560x2560 // if loading this fix HistoryPhoto::updateFrom\n// a: crop 160x160\n// b: crop 320x320\n// c: crop 640x640\n// d: crop 1280x1280\nconst auto InlineLevels = \"i\"_q;\nconst auto SmallLevels = \"sa\"_q;\nconst auto ThumbnailLevels = \"mbsa\"_q;\nconst auto LargeLevels = \"ydxcwmbsa\"_q;\n\nvoid CheckForSwitchInlineButton(not_null<HistoryItem*> item) {\n\tif (item->out() || !item->hasSwitchInlineButton()) {\n\t\treturn;\n\t}\n\tif (const auto user = item->history()->peer->asUser()) {\n\t\tif (!user->isBot() || !user->botInfo->inlineReturnTo.key) {\n\t\t\treturn;\n\t\t}\n\t\tif (const auto markup = item->Get<HistoryMessageReplyMarkup>()) {\n\t\t\tfor (const auto &row : markup->data.rows) {\n\t\t\t\tfor (const auto &button : row) {\n\t\t\t\t\tusing ButtonType = HistoryMessageMarkupButton::Type;\n\t\t\t\t\tif (button.type == ButtonType::SwitchInline) {\n\t\t\t\t\t\tconst auto session = &item->history()->session();\n\t\t\t\t\t\tconst auto &windows = session->windows();\n\t\t\t\t\t\tif (!windows.empty()) {\n\t\t\t\t\t\t\tApi::SwitchInlineBotButtonReceived(\n\t\t\t\t\t\t\t\twindows.front(),\n\t\t\t\t\t\t\t\tbutton.data);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n[[nodiscard]] InlineImageLocation FindInlineThumbnail(\n\t\tconst QVector<MTPPhotoSize> &sizes) {\n\tconst auto i = ranges::find(\n\t\tsizes,\n\t\tmtpc_photoStrippedSize,\n\t\t&MTPPhotoSize::type);\n\tconst auto j = ranges::find(\n\t\tsizes,\n\t\tmtpc_photoPathSize,\n\t\t&MTPPhotoSize::type);\n\treturn (i != sizes.end())\n\t\t? InlineImageLocation{ i->c_photoStrippedSize().vbytes().v, false }\n\t\t: (j != sizes.end())\n\t\t? InlineImageLocation{ j->c_photoPathSize().vbytes().v, true }\n\t\t: InlineImageLocation();\n}\n\n[[nodiscard]] InlineImageLocation FindDocumentInlineThumbnail(\n\t\tconst MTPDdocument &data) {\n\treturn FindInlineThumbnail(data.vthumbs().value_or_empty());\n}\n\n[[nodiscard]] MTPPhotoSize FindDocumentThumbnail(const MTPDdocument &data) {\n\tconst auto area = [](const MTPPhotoSize &size) {\n\t\tstatic constexpr auto kInvalid = 0;\n\t\treturn size.match([](const MTPDphotoSizeEmpty &) {\n\t\t\treturn kInvalid;\n\t\t}, [](const MTPDphotoStrippedSize &) {\n\t\t\treturn kInvalid;\n\t\t}, [](const MTPDphotoPathSize &) {\n\t\t\treturn kInvalid;\n\t\t}, [](const auto &data) {\n\t\t\treturn (data.vw().v * data.vh().v);\n\t\t});\n\t};\n\tconst auto thumbs = data.vthumbs();\n\tif (!thumbs) {\n\t\treturn MTP_photoSizeEmpty(MTP_string());\n\t}\n\tconst auto &list = thumbs->v;\n\tconst auto i = ranges::max_element(list, std::less<>(), area);\n\treturn (i != list.end() && area(*i) > 0)\n\t\t? (*i)\n\t\t: MTPPhotoSize(MTP_photoSizeEmpty(MTP_string()));\n}\n\n[[nodiscard]] std::optional<MTPVideoSize> FindDocumentVideoThumbnail(\n\t\tconst MTPDdocument &data) {\n\tconst auto area = [](const MTPVideoSize &size) {\n\t\treturn size.match([](const MTPDvideoSize &data) {\n\t\t\treturn (data.vw().v * data.vh().v);\n\t\t}, [](const MTPDvideoSizeEmojiMarkup &) {\n\t\t\treturn 0;\n\t\t}, [](const MTPDvideoSizeStickerMarkup &) {\n\t\t\treturn 0;\n\t\t});\n\t};\n\tconst auto thumbs = data.vvideo_thumbs();\n\tif (!thumbs) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto &list = thumbs->v;\n\tconst auto i = ranges::max_element(list, std::less<>(), area);\n\treturn (i != list.end() && area(*i) > 0)\n\t\t? std::make_optional(*i)\n\t\t: std::nullopt;\n}\n\n[[nodiscard]] QByteArray FindPhotoInlineThumbnail(const MTPDphoto &data) {\n\tconst auto thumbnail = FindInlineThumbnail(data.vsizes().v);\n\treturn !thumbnail.isPath ? thumbnail.bytes : QByteArray();\n}\n\n[[nodiscard]] int VideoStartTime(const MTPDvideoSize &data) {\n\treturn int(\n\t\tstd::clamp(\n\t\t\tstd::floor(data.vvideo_start_ts().value_or_empty() * 1000),\n\t\t\t0.,\n\t\t\tdouble(std::numeric_limits<int>::max())));\n}\n\n} // namespace\n\nSession::Session(not_null<Main::Session*> session)\n: _session(session)\n, _cache(Core::App().databases().get(\n\t_session->local().cachePath(),\n\t_session->local().cacheSettings()))\n, _bigFileCache(Core::App().databases().get(\n\t_session->local().cacheBigFilePath(),\n\t_session->local().cacheBigFileSettings()))\n, _groupFreeTranscribeLevel(session->appConfig().value(\n) | rpl::map([limits = Data::LevelLimits(session)] {\n\treturn limits.groupTranscribeLevelMin();\n}))\n, _chatsList(\n\tsession,\n\tFilterId(),\n\tmaxPinnedChatsLimitValue(nullptr))\n, _contactsList(Dialogs::SortMode::Name)\n, _contactsNoChatsList(Dialogs::SortMode::Name)\n, _ttlCheckTimer([=] { checkTTLs(); })\n, _formattedDateTimer([=] { checkFormattedDateUpdates(); })\n, _selfDestructTimer([=] { checkSelfDestructItems(); })\n, _pollsClosingTimer([=] { checkPollsClosings(); })\n, _watchForOfflineTimer([=] { checkLocalUsersWentOffline(); })\n, _groups(this)\n, _chatsFilters(std::make_unique<ChatFilters>(this))\n, _cloudThemes(std::make_unique<CloudThemes>(session))\n, _sendActionManager(std::make_unique<SendActionManager>())\n, _streaming(std::make_unique<Streaming>(this))\n, _mediaRotation(std::make_unique<MediaRotation>())\n, _histories(std::make_unique<Histories>(this))\n, _stickers(std::make_unique<Stickers>(this))\n, _reactions(std::make_unique<Reactions>(this))\n, _emojiStatuses(std::make_unique<EmojiStatuses>(this))\n, _forumIcons(std::make_unique<ForumIcons>(this))\n, _notifySettings(std::make_unique<NotifySettings>(this))\n, _customEmojiManager(std::make_unique<CustomEmojiManager>(this))\n, _stories(std::make_unique<Stories>(this))\n, _savedMusic(std::make_unique<SavedMusic>(this))\n, _savedMessages(std::make_unique<SavedMessages>(this))\n, _chatbots(std::make_unique<Chatbots>(this))\n, _businessInfo(std::make_unique<BusinessInfo>(this))\n, _shortcutMessages(std::make_unique<ShortcutMessages>(this)) {\n\t_cache->open(_session->local().cacheKey());\n\t_bigFileCache->open(_session->local().cacheBigFileKey());\n\n\tif constexpr (Platform::IsLinux()) {\n\t\tconst auto wasVersion = _session->local().oldMapVersion();\n\t\tif (wasVersion >= 1007011 && wasVersion < 1007015) {\n\t\t\t_bigFileCache->clear();\n\t\t\t_cache->clearByTag(Data::kImageCacheTag);\n\t\t}\n\t}\n\n\tsetupMigrationViewer();\n\tsetupChannelLeavingViewer();\n\tsetupPeerNameViewer();\n\tsetupUserIsContactViewer();\n\n\t_chatsList.unreadStateChanges(\n\t) | rpl::on_next([=] {\n\t\tnotifyUnreadBadgeChanged();\n\t}, _lifetime);\n\n\t_chatsFilters->changed(\n\t) | rpl::on_next([=] {\n\t\tconst auto enabled = _chatsFilters->has();\n\t\tif (enabled != session->settings().dialogsFiltersEnabled()) {\n\t\t\tsession->settings().setDialogsFiltersEnabled(enabled);\n\t\t\tsession->saveSettingsDelayed();\n\t\t}\n\t}, _lifetime);\n\n\t_reactions->myTagRenamed(\n\t) | rpl::on_next([=](const ReactionId &id) {\n\t\tconst auto i = _viewsByTag.find(id);\n\t\tif (i != end(_viewsByTag)) {\n\t\t\tfor (const auto &view : i->second) {\n\t\t\t\tnotifyItemDataChange(view->data());\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n\n\tSpellchecker::HighlightReady(\n\t) | rpl::on_next([=](uint64 processId) {\n\t\thighlightProcessDone(processId);\n\t}, _lifetime);\n\n\tsubscribeForTopicRepliesLists();\n\n\tcrl::on_main(_session, [=] {\n\t\tAmPremiumValue(\n\t\t\t_session\n\t\t) | rpl::on_next([=] {\n\t\t\tfor (const auto &[document, items] : _documentItems) {\n\t\t\t\tif (document->isVoiceMessage()) {\n\t\t\t\t\tfor (const auto &item : items) {\n\t\t\t\t\t\trequestItemResize(item);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}, _lifetime);\n\n\t\t_stories->loadMore(Data::StorySourcesList::NotHidden);\n\t});\n\n\tsession->appConfig().ignoredRestrictionReasonsChanges(\n\t) | rpl::on_next([=](std::vector<QString> &&changed) {\n\t\tauto refresh = std::vector<not_null<const HistoryItem*>>();\n\t\tfor (const auto &[item, reasons] : _possiblyRestricted) {\n\t\t\tfor (const auto &reason : changed) {\n\t\t\t\tif (reasons.contains(reason)) {\n\t\t\t\t\trefresh.push_back(item);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor (const auto &item : refresh) {\n\t\t\trequestItemViewRefresh(item);\n\t\t}\n\t}, _lifetime);\n}\n\nvoid Session::subscribeForTopicRepliesLists() {\n\trepliesReadTillUpdates(\n\t) | rpl::on_next([=](const RepliesReadTillUpdate &update) {\n\t\tif (const auto peer = peerLoaded(update.id.peer)) {\n\t\t\tif (const auto topic = peer->forumTopicFor(update.id.msg)) {\n\t\t\t\ttopic->replies()->apply(update);\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n\n\tsublistReadTillUpdates(\n\t) | rpl::on_next([=](const SublistReadTillUpdate &update) {\n\t\tif (const auto parentChat = channelLoaded(update.parentChatId)) {\n\t\t\tif (const auto monoforum = parentChat->monoforum()) {\n\t\t\t\tconst auto sublistPeerId = update.sublistPeerId;\n\t\t\t\tconst auto peer = monoforum->owner().peer(sublistPeerId);\n\t\t\t\tif (const auto sublist = monoforum->sublistLoaded(peer)) {\n\t\t\t\t\tsublist->apply(update);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n\n\tsession().changes().messageUpdates(\n\t\tMessageUpdate::Flag::NewAdded\n\t\t| MessageUpdate::Flag::NewMaybeAdded\n\t\t| MessageUpdate::Flag::ReplyToTopAdded\n\t\t| MessageUpdate::Flag::Destroyed\n\t) | rpl::on_next([=](const MessageUpdate &update) {\n\t\tif (const auto topic = update.item->topic()) {\n\t\t\ttopic->replies()->apply(update);\n\t\t} else if (update.flags == MessageUpdate::Flag::ReplyToTopAdded) {\n\t\t\t// Not interested in this one for sublist.\n\t\t\treturn;\n\t\t} else if (const auto sublist = update.item->savedSublist()) {\n\t\t\tsublist->apply(update);\n\t\t}\n\t}, _lifetime);\n\n\tsession().changes().topicUpdates(\n\t\tTopicUpdate::Flag::Creator\n\t) | rpl::on_next([=](const TopicUpdate &update) {\n\t\tupdate.topic->replies()->apply(update);\n\t}, _lifetime);\n\n\tchannelDifferenceTooLong(\n\t) | rpl::on_next([=](not_null<ChannelData*> channel) {\n\t\tif (const auto forum = channel->forum()) {\n\t\t\tforum->enumerateTopics([](not_null<ForumTopic*> topic) {\n\t\t\t\ttopic->replies()->applyDifferenceTooLong();\n\t\t\t});\n\t\t}\n\t}, _lifetime);\n}\n\nvoid Session::clear() {\n\t// Optimization: clear notifications before destroying items.\n\tCore::App().notifications().clearFromSession(_session);\n\n\t// We must clear all [mono]forums before clearing customEmojiManager.\n\t// Because in Data::ForumTopic an Ui::Text::CustomEmoji is cached.\n\tauto botForums = base::flat_set<not_null<UserData*>>();\n\tauto channelForums = base::flat_set<not_null<ChannelData*>>();\n\tfor (const auto &[peerId, peer] : _peers) {\n\t\tif (const auto bot = peer->asBot()) {\n\t\t\tif (bot->isForum()) {\n\t\t\t\tbotForums.emplace(bot);\n\t\t\t}\n\t\t}\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\tif (channel->isForum() || channel->amMonoforumAdmin()) {\n\t\t\t\tchannelForums.emplace(channel);\n\t\t\t}\n\t\t}\n\t}\n\tfor (const auto &bot : botForums) {\n\t\tbot->setFlags(bot->flags() & ~UserDataFlag::Forum);\n\t}\n\tfor (const auto &channel : channelForums) {\n\t\tchannel->setFlags(channel->flags()\n\t\t\t& ~(ChannelDataFlag::Forum | ChannelDataFlag::MonoforumAdmin));\n\t}\n\t_savedMusic->clear();\n\t_savedMessages->clear();\n\n\t_sendActionManager->clear();\n\n\t_histories->unloadAll();\n\t_shortcutMessages = nullptr;\n\t_session->scheduledMessages().clear();\n\t_session->sponsoredMessages().clear();\n\t_dependentMessages.clear();\n\tbase::take(_messages);\n\tbase::take(_nonChannelMessages);\n\t_messageByRandomId.clear();\n\t_sentMessagesData.clear();\n\tcSetRecentInlineBots(RecentInlineBots());\n\tcSetRecentStickers(RecentStickerPack());\n\tHistoryView::Element::ClearGlobal();\n\t_contactsNoChatsList.clear();\n\t_contactsList.clear();\n\t_chatsList.clear();\n\tfor (const auto &[id, folder] : _folders) {\n\t\tfolder->clearChatsList();\n\t}\n\t_chatsFilters->clear();\n\t_histories->clearAll();\n\t_webpages.clear();\n\t_locations.clear();\n\t_polls.clear();\n\t_games.clear();\n\t_documents.clear();\n\t_photos.clear();\n}\n\nvoid Session::keepAlive(std::shared_ptr<PhotoMedia> media) {\n\t// NB! This allows PhotoMedia to outlive Main::Session!\n\t// In case this is a problem this code should be rewritten.\n\tcrl::on_main(&session(), [media = std::move(media)]{});\n}\n\nvoid Session::keepAlive(std::shared_ptr<DocumentMedia> media) {\n\t// NB! This allows DocumentMedia to outlive Main::Session!\n\t// In case this is a problem this code should be rewritten.\n\tcrl::on_main(&session(), [media = std::move(media)] {});\n}\n\nnot_null<PeerData*> Session::peer(PeerId id) {\n\tconst auto i = _peers.find(id);\n\tif (i != _peers.cend()) {\n\t\treturn i->second.get();\n\t}\n\tauto result = [&]() -> std::unique_ptr<PeerData> {\n\t\tif (peerIsUser(id)) {\n\t\t\treturn std::make_unique<UserData>(this, id);\n\t\t} else if (peerIsChat(id)) {\n\t\t\treturn std::make_unique<ChatData>(this, id);\n\t\t} else if (peerIsChannel(id)) {\n\t\t\treturn std::make_unique<ChannelData>(this, id);\n\t\t}\n\t\tUnexpected(\"Peer id type.\");\n\t}();\n\treturn _peers.emplace(id, std::move(result)).first->second.get();\n}\n\nnot_null<UserData*> Session::user(UserId id) {\n\treturn peer(peerFromUser(id))->asUser();\n}\n\nnot_null<ChatData*> Session::chat(ChatId id) {\n\treturn peer(peerFromChat(id))->asChat();\n}\n\nnot_null<ChannelData*> Session::channel(ChannelId id) {\n\treturn peer(peerFromChannel(id))->asChannel();\n}\n\nPeerData *Session::peerLoaded(PeerId id) const {\n\tconst auto i = _peers.find(id);\n\tif (i == end(_peers)) {\n\t\treturn nullptr;\n\t} else if (!i->second->isLoaded()) {\n\t\treturn nullptr;\n\t}\n\treturn i->second.get();\n}\n\nUserData *Session::userLoaded(UserId id) const {\n\tif (const auto peer = peerLoaded(peerFromUser(id))) {\n\t\treturn peer->asUser();\n\t}\n\treturn nullptr;\n}\n\nChatData *Session::chatLoaded(ChatId id) const {\n\tif (const auto peer = peerLoaded(peerFromChat(id))) {\n\t\treturn peer->asChat();\n\t}\n\treturn nullptr;\n}\n\nChannelData *Session::channelLoaded(ChannelId id) const {\n\tif (const auto peer = peerLoaded(peerFromChannel(id))) {\n\t\treturn peer->asChannel();\n\t}\n\treturn nullptr;\n}\n\nnot_null<UserData*> Session::processUser(const MTPUser &data) {\n\tconst auto result = user(data.match([](const auto &data) {\n\t\treturn data.vid().v;\n\t}));\n\tauto minimal = false;\n\tconst MTPUserStatus *status = nullptr;\n\tconst MTPUserStatus emptyStatus = MTP_userStatusEmpty();\n\n\tusing UpdateFlag = PeerUpdate::Flag;\n\tauto flags = UpdateFlag::None | UpdateFlag::None;\n\tdata.match([&](const MTPDuserEmpty &data) {\n\t\tconst auto canShareThisContact = result->canShareThisContactFast();\n\n\t\tresult->setName(tr::lng_deleted(tr::now), QString(), QString(), QString());\n\t\tresult->setPhoto(MTP_userProfilePhotoEmpty());\n\t\tresult->setFlags(UserDataFlag::Deleted);\n\t\tif (!result->phone().isEmpty()) {\n\t\t\tresult->setPhone(QString());\n\t\t\tflags |= UpdateFlag::PhoneNumber;\n\t\t}\n\t\tresult->setBotInfoVersion(-1);\n\t\tstatus = &emptyStatus;\n\t\tresult->setIsContact(false);\n\t\tif (canShareThisContact != result->canShareThisContactFast()) {\n\t\t\tflags |= UpdateFlag::CanShareContact;\n\t\t}\n\t}, [&](const MTPDuser &data) {\n\t\tminimal = data.is_min();\n\n\t\tconst auto canShareThisContact = result->canShareThisContactFast();\n\n\t\tconst auto hasRequirePremiumToWrite\n\t\t\t= data.is_contact_require_premium();\n\t\tconst auto hasStarsPerMessage\n\t\t\t= data.vsend_paid_messages_stars().has_value();\n\t\tif (!hasStarsPerMessage) {\n\t\t\tresult->setStarsPerMessage(0);\n\t\t}\n\t\tresult->setBotInfoVersion(data.vbot_info_version().value_or(-1));\n\n\t\tif (!minimal) {\n\t\t\tif (const auto info = result->botInfo.get()) {\n\t\t\t\tinfo->readsAllHistory = data.is_bot_chat_history();\n\t\t\t\tif (info->cantJoinGroups != data.is_bot_nochats()) {\n\t\t\t\t\tinfo->cantJoinGroups = data.is_bot_nochats();\n\t\t\t\t\tflags |= UpdateFlag::BotCanBeInvited;\n\t\t\t\t}\n\t\t\t\tif (const auto value = data.vbot_inline_placeholder()) {\n\t\t\t\t\tinfo->inlinePlaceholder = '_' + qs(*value);\n\t\t\t\t} else {\n\t\t\t\t\tinfo->inlinePlaceholder = QString();\n\t\t\t\t}\n\t\t\t\tinfo->supportsAttachMenu = data.is_bot_attach_menu();\n\t\t\t\tinfo->supportsBusiness = data.is_bot_business();\n\t\t\t\tconst auto canEditInformation = data.is_bot_can_edit();\n\t\t\t\tif (info->canEditInformation != canEditInformation) {\n\t\t\t\t\tinfo->canEditInformation = canEditInformation;\n\t\t\t\t\tflags |= UpdateFlag::ManagedBot;\n\t\t\t\t}\n\t\t\t\tinfo->activeUsers = data.vbot_active_users().value_or_empty();\n\t\t\t\tinfo->hasMainApp = data.is_bot_has_main_app();\n\t\t\t\tinfo->userCreatesTopics = data.is_bot_forum_can_manage_topics();\n\t\t\t\tinfo->canManageBots = data.is_bot_can_manage_bots();\n\t\t\t}\n\t\t}\n\n\t\tusing Flag = UserDataFlag;\n\t\tconst auto flagsMask = Flag::Deleted\n\t\t\t| Flag::Verified\n\t\t\t| Flag::Scam\n\t\t\t| Flag::Fake\n\t\t\t| Flag::BotInlineGeo\n\t\t\t| Flag::Forum\n\t\t\t| Flag::Premium\n\t\t\t| Flag::Support\n\t\t\t| Flag::HasRequirePremiumToWrite\n\t\t\t| Flag::HasStarsPerMessage\n\t\t\t| Flag::MessageMoneyRestrictionsKnown\n\t\t\t| (!hasRequirePremiumToWrite\n\t\t\t\t? Flag::RequiresPremiumToWrite\n\t\t\t\t: Flag())\n\t\t\t| (!minimal\n\t\t\t\t? Flag::Contact\n\t\t\t\t| Flag::MutualContact\n\t\t\t\t| Flag::DiscardMinPhoto\n\t\t\t\t| Flag::StoriesHidden\n\t\t\t\t: Flag());\n\t\tconst auto storiesState = minimal\n\t\t\t? std::optional<Data::Stories::PeerSourceState>()\n\t\t\t: data.is_stories_unavailable()\n\t\t\t? Data::Stories::PeerSourceState()\n\t\t\t: !data.vstories_max_id()\n\t\t\t? std::optional<Data::Stories::PeerSourceState>()\n\t\t\t: stories().peerSourceState(result, *data.vstories_max_id());\n\t\tconst auto flagsSet = (data.is_deleted() ? Flag::Deleted : Flag())\n\t\t\t| (data.is_verified() ? Flag::Verified : Flag())\n\t\t\t| (data.is_scam() ? Flag::Scam : Flag())\n\t\t\t| (data.is_fake() ? Flag::Fake : Flag())\n\t\t\t| (data.is_bot_inline_geo() ? Flag::BotInlineGeo : Flag())\n\t\t\t| (data.is_bot_forum_view() ? Flag::Forum : Flag())\n\t\t\t| (data.is_premium() ? Flag::Premium : Flag())\n\t\t\t| (data.is_support() ? Flag::Support : Flag())\n\t\t\t| (hasRequirePremiumToWrite\n\t\t\t\t? (Flag::HasRequirePremiumToWrite\n\t\t\t\t\t| (result->hasRequirePremiumToWrite()\n\t\t\t\t\t\t? (result->messageMoneyRestrictionsKnown()\n\t\t\t\t\t\t\t? Flag::MessageMoneyRestrictionsKnown\n\t\t\t\t\t\t\t: Flag())\n\t\t\t\t\t\t: Flag()))\n\t\t\t\t: Flag())\n\t\t\t| (hasStarsPerMessage\n\t\t\t\t? (Flag::HasStarsPerMessage\n\t\t\t\t\t| (result->hasStarsPerMessage()\n\t\t\t\t\t\t? (result->messageMoneyRestrictionsKnown()\n\t\t\t\t\t\t\t? Flag::MessageMoneyRestrictionsKnown\n\t\t\t\t\t\t\t: Flag())\n\t\t\t\t\t\t: Flag()))\n\t\t\t\t: Flag())\n\t\t\t| ((!hasRequirePremiumToWrite && !hasStarsPerMessage)\n\t\t\t\t? Flag::MessageMoneyRestrictionsKnown\n\t\t\t\t: Flag())\n\t\t\t| (!minimal\n\t\t\t\t? (data.is_contact() ? Flag::Contact : Flag())\n\t\t\t\t| (data.is_mutual_contact() ? Flag::MutualContact : Flag())\n\t\t\t\t| (data.is_apply_min_photo()\n\t\t\t\t\t? Flag()\n\t\t\t\t\t: Flag::DiscardMinPhoto)\n\t\t\t\t| (data.is_stories_hidden() ? Flag::StoriesHidden : Flag())\n\t\t\t\t: Flag());\n\t\tresult->setFlags((result->flags() & ~flagsMask) | flagsSet);\n\t\tresult->setBotVerifyDetailsIcon(\n\t\t\tdata.vbot_verification_icon().value_or_empty());\n\t\tif (!minimal) {\n\t\t\tif (storiesState) {\n\t\t\t\tresult->setStoriesState(storiesState->hasVideoStream\n\t\t\t\t\t? PeerData::StoriesState::HasVideoStream\n\t\t\t\t\t: !storiesState->maxId\n\t\t\t\t\t? PeerData::StoriesState::None\n\t\t\t\t\t: (storiesState->maxId > storiesState->readTill)\n\t\t\t\t\t? PeerData::StoriesState::HasUnread\n\t\t\t\t\t: PeerData::StoriesState::HasRead);\n\t\t\t}\n\t\t\tresult->setUnavailableReasons(Data::UnavailableReason::Extract(\n\t\t\t\tdata.vrestriction_reason()));\n\t\t}\n\n\t\tif (const auto accessHash = data.vaccess_hash()) {\n\t\t\tif (!minimal || !result->accessHash()) {\n\t\t\t\tresult->setAccessHash(accessHash->v);\n\t\t\t}\n\t\t}\n\t\tif (data.is_deleted()) {\n\t\t\tif (!result->phone().isEmpty()) {\n\t\t\t\tresult->setPhone(QString());\n\t\t\t\tflags |= UpdateFlag::PhoneNumber;\n\t\t\t}\n\t\t\tresult->setName(tr::lng_deleted(tr::now), QString(), QString(), QString());\n\t\t\tresult->setPhoto(MTP_userProfilePhotoEmpty());\n\t\t\tstatus = &emptyStatus;\n\t\t} else {\n\t\t\t// apply first_name and last_name from minimal user only if we don't have\n\t\t\t// local values for first name and last name already, otherwise skip\n\t\t\tconst auto noLocalName = result->firstName.isEmpty()\n\t\t\t\t&& result->lastName.isEmpty();\n\t\t\tconst auto fname = (!minimal || noLocalName)\n\t\t\t\t? TextUtilities::SingleLine(\n\t\t\t\t\tqs(data.vfirst_name().value_or_empty()))\n\t\t\t\t: result->firstName;\n\t\t\tconst auto lname = (!minimal || noLocalName)\n\t\t\t\t? TextUtilities::SingleLine(\n\t\t\t\t\tqs(data.vlast_name().value_or_empty()))\n\t\t\t\t: result->lastName;\n\n\t\t\tconst auto phone = minimal\n\t\t\t\t? result->phone()\n\t\t\t\t: qs(data.vphone().value_or_empty());\n\t\t\tconst auto uname = minimal\n\t\t\t\t? result->username()\n\t\t\t\t: TextUtilities::SingleLine(\n\t\t\t\t\tqs(data.vusername().value_or_empty()));\n\n\t\t\tconst auto phoneChanged = (result->phone() != phone);\n\t\t\tif (phoneChanged) {\n\t\t\t\tresult->setPhone(phone);\n\t\t\t\tflags |= UpdateFlag::PhoneNumber;\n\t\t\t}\n\t\t\tconst auto nameChanged = (result->firstName != fname)\n\t\t\t\t|| (result->lastName != lname);\n\n\t\t\tauto showPhone = !result->isServiceUser()\n\t\t\t\t&& !data.is_support()\n\t\t\t\t&& !data.is_self()\n\t\t\t\t&& !data.is_contact()\n\t\t\t\t&& !data.is_mutual_contact();\n\t\t\tauto showPhoneChanged = !result->isServiceUser()\n\t\t\t\t&& !data.is_self()\n\t\t\t\t&& ((showPhone && result->isContact())\n\t\t\t\t\t|| (!showPhone\n\t\t\t\t\t\t&& !result->isContact()\n\t\t\t\t\t\t&& !result->phone().isEmpty()));\n\t\t\tif (minimal) {\n\t\t\t\tshowPhoneChanged = false;\n\t\t\t\tshowPhone = !result->isServiceUser()\n\t\t\t\t\t&& !result->isContact()\n\t\t\t\t\t&& !result->phone().isEmpty()\n\t\t\t\t\t&& (result->id != _session->userPeerId());\n\t\t\t}\n\n\t\t\t// see also Serialize::readPeer\n\n\t\t\tconst auto pname = (showPhoneChanged || phoneChanged || nameChanged)\n\t\t\t\t? ((showPhone && !phone.isEmpty())\n\t\t\t\t\t? Ui::FormatPhone(phone)\n\t\t\t\t\t: QString())\n\t\t\t\t: result->nameOrPhone;\n\n\t\t\tresult->setName(fname, lname, pname, uname);\n\t\t\tif (!minimal || result->applyMinPhoto()) {\n\t\t\t\tif (const auto photo = data.vphoto()) {\n\t\t\t\t\tresult->setPhoto(*photo);\n\t\t\t\t} else {\n\t\t\t\t\tresult->setPhoto(MTP_userProfilePhotoEmpty());\n\t\t\t\t}\n\t\t\t}\n\t\t\tstatus = data.vstatus();\n\t\t\tif (!minimal) {\n\t\t\t\tconst auto newUsername = uname;\n\t\t\t\tif (data.vusernames()) {\n\t\t\t\t\tresult->setUsernames(\n\t\t\t\t\t\tApi::Usernames::FromTL(*data.vusernames()));\n\t\t\t\t} else if (!newUsername.isEmpty()) {\n\t\t\t\t\tresult->setUsernames({{ newUsername, true, true }});\n\t\t\t\t} else {\n\t\t\t\t\tresult->setUsernames({});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (const auto &status = data.vemoji_status()) {\n\t\t\tresult->setEmojiStatus(*status);\n\t\t} else {\n\t\t\tresult->setEmojiStatus(EmojiStatusId());\n\t\t}\n\t\tif (!minimal) {\n\t\t\tresult->setIsContact(data.is_contact()\n\t\t\t\t|| data.is_mutual_contact());\n\t\t}\n\n\t\tif (canShareThisContact != result->canShareThisContactFast()) {\n\t\t\tflags |= UpdateFlag::CanShareContact;\n\t\t}\n\n\t\tif (result->changeColor(data.vcolor())) {\n\t\t\tflags |= UpdateFlag::Color;\n\t\t\tif (result->isMinimalLoaded()) {\n\t\t\t\t_peerDecorationsUpdated.fire_copy(result);\n\t\t\t}\n\t\t}\n\t\tif (result->changeColorProfile(data.vprofile_color())) {\n\t\t\tflags |= UpdateFlag::ColorProfile;\n\t\t}\n\t});\n\n\tif (minimal) {\n\t\tif (!result->isMinimalLoaded()) {\n\t\t\tresult->setLoadedStatus(PeerData::LoadedStatus::Minimal);\n\t\t}\n\t} else if (!result->isLoaded()\n\t\t&& (!result->isSelf() || !result->phone().isEmpty())) {\n\t\tresult->setLoadedStatus(PeerData::LoadedStatus::Normal);\n\t}\n\n\tif (!minimal) {\n\t\tconst auto lastseen = status\n\t\t\t? LastseenFromMTP(*status, result->lastseen())\n\t\t\t: Data::LastseenStatus::LongAgo(false);\n\t\tif (result->updateLastseen(lastseen)) {\n\t\t\tflags |= UpdateFlag::OnlineStatus;\n\t\t}\n\t}\n\n\tif (flags) {\n\t\tsession().changes().peerUpdated(result, flags);\n\t}\n\treturn result;\n}\n\nnot_null<PeerData*> Session::processChat(const MTPChat &data) {\n\tconst auto result = data.match([&](const MTPDchat &data) {\n\t\treturn peer(peerFromChat(data.vid().v));\n\t}, [&](const MTPDchatForbidden &data) {\n\t\treturn peer(peerFromChat(data.vid().v));\n\t}, [&](const MTPDchatEmpty &data) {\n\t\treturn peer(peerFromChat(data.vid().v));\n\t}, [&](const MTPDchannel &data) {\n\t\treturn peer(peerFromChannel(data.vid().v));\n\t}, [&](const MTPDchannelForbidden &data) {\n\t\treturn peer(peerFromChannel(data.vid().v));\n\t});\n\tauto minimal = false;\n\n\tusing UpdateFlag = Data::PeerUpdate::Flag;\n\tauto flags = UpdateFlag::None | UpdateFlag::None;\n\tdata.match([&](const MTPDchat &data) {\n\t\tconst auto chat = result->asChat();\n\n\t\tconst auto canAddMembers = chat->canAddMembers();\n\t\tif (chat->version() < data.vversion().v) {\n\t\t\tchat->setVersion(data.vversion().v);\n\t\t\tchat->invalidateParticipants();\n\t\t}\n\n\t\tchat->setName(qs(data.vtitle()));\n\t\tchat->setPhoto(data.vphoto());\n\t\tchat->date = data.vdate().v;\n\n\t\tif (const auto rights = data.vadmin_rights()) {\n\t\t\tchat->setAdminRights(ChatAdminRightsInfo(*rights).flags);\n\t\t} else {\n\t\t\tchat->setAdminRights(ChatAdminRights());\n\t\t}\n\t\tif (const auto rights = data.vdefault_banned_rights()) {\n\t\t\tchat->setDefaultRestrictions(ChatRestrictionsInfo(*rights).flags);\n\t\t} else {\n\t\t\tchat->setDefaultRestrictions(ChatRestrictions());\n\t\t}\n\n\t\tif (const auto migratedTo = data.vmigrated_to()) {\n\t\t\tmigratedTo->match([&](const MTPDinputChannel &input) {\n\t\t\t\tconst auto channel = this->channel(input.vchannel_id().v);\n\t\t\t\tchannel->addFlags(ChannelDataFlag::Megagroup);\n\t\t\t\tif (!channel->accessHash()) {\n\t\t\t\t\tchannel->setAccessHash(input.vaccess_hash().v);\n\t\t\t\t}\n\t\t\t\tApplyMigration(chat, channel);\n\t\t\t}, [](const MTPDinputChannelFromMessage &) {\n\t\t\t\tLOG((\"API Error: \"\n\t\t\t\t\t\"migrated_to contains channel from message.\"));\n\t\t\t}, [](const MTPDinputChannelEmpty &) {\n\t\t\t});\n\t\t}\n\n\t\tusing Flag = ChatDataFlag;\n\t\tconst auto flagsMask = Flag::Left\n\t\t\t| Flag::Creator\n\t\t\t| Flag::Deactivated\n\t\t\t| Flag::Forbidden\n\t\t\t| Flag::CallActive\n\t\t\t| Flag::CallNotEmpty\n\t\t\t| Flag::NoForwards;\n\t\tconst auto flagsSet = (data.is_left() ? Flag::Left : Flag())\n\t\t\t| (data.is_creator() ? Flag::Creator : Flag())\n\t\t\t| (data.is_deactivated() ? Flag::Deactivated : Flag())\n\t\t\t| (data.is_call_active() ? Flag::CallActive : Flag())\n\t\t\t| ((data.is_call_not_empty()\n\t\t\t\t|| (chat->groupCall()\n\t\t\t\t\t&& chat->groupCall()->fullCount() > 0))\n\t\t\t\t? Flag::CallNotEmpty\n\t\t\t\t: Flag())\n\t\t\t| (data.is_noforwards() ? Flag::NoForwards : Flag());\n\t\tchat->setFlags((chat->flags() & ~flagsMask) | flagsSet);\n\t\tchat->count = data.vparticipants_count().v;\n\n\t\tif (canAddMembers != chat->canAddMembers()) {\n\t\t\tflags |= UpdateFlag::Rights;\n\t\t}\n\t}, [&](const MTPDchatForbidden &data) {\n\t\tconst auto chat = result->asChat();\n\n\t\tconst auto canAddMembers = chat->canAddMembers();\n\n\t\tchat->setName(qs(data.vtitle()));\n\t\tchat->setPhoto(MTP_chatPhotoEmpty());\n\t\tchat->date = 0;\n\t\tchat->count = -1;\n\t\tchat->invalidateParticipants();\n\t\tchat->setFlags(ChatDataFlag::Forbidden);\n\t\tchat->setAdminRights(ChatAdminRights());\n\t\tchat->setDefaultRestrictions(ChatRestrictions());\n\n\t\tif (canAddMembers != chat->canAddMembers()) {\n\t\t\tflags |= UpdateFlag::Rights;\n\t\t}\n\t}, [&](const MTPDchannel &data) {\n\t\tconst auto channel = result->asChannel();\n\n\t\tminimal = data.is_min();\n\t\tif (minimal && !result->isLoaded()) {\n\t\t\tLOG((\"API Warning: not loaded minimal channel applied.\"));\n\t\t}\n\n\t\tif (const auto accessHash = data.vaccess_hash()) {\n\t\t\tif (!minimal || !channel->accessHash()) {\n\t\t\t\tchannel->setAccessHash(accessHash->v);\n\t\t\t}\n\t\t}\n\n\t\tconst auto wasInChannel = channel->amIn();\n\t\tconst auto canViewAdmins = channel->canViewAdmins();\n\t\tconst auto canViewMembers = channel->canViewMembers();\n\t\tconst auto canAddMembers = channel->canAddMembers();\n\n\t\tconst auto wasCallNotEmpty = Data::ChannelHasActiveCall(channel);\n\n\t\tchannel->updateLevelHint(data.vlevel().value_or_empty());\n\t\tchannel->updateSubscriptionUntilDate(\n\t\t\tdata.vsubscription_until_date().value_or_empty());\n\t\tif (const auto count = data.vparticipants_count()) {\n\t\t\tchannel->setMembersCount(count->v);\n\t\t}\n\t\tif (const auto rights = data.vdefault_banned_rights()) {\n\t\t\tchannel->setDefaultRestrictions(ChatRestrictionsInfo(*rights).flags);\n\t\t} else {\n\t\t\tchannel->setDefaultRestrictions(ChatRestrictions());\n\t\t}\n\n\t\tif (const auto &status = data.vemoji_status()) {\n\t\t\tchannel->setEmojiStatus(*status);\n\t\t} else {\n\t\t\tchannel->setEmojiStatus(EmojiStatusId());\n\t\t}\n\t\tif (!minimal) {\n\t\t\tif (const auto rights = data.vadmin_rights()) {\n\t\t\t\tchannel->setAdminRights(ChatAdminRightsInfo(*rights).flags);\n\t\t\t} else if (channel->hasAdminRights()) {\n\t\t\t\tchannel->setAdminRights(ChatAdminRights());\n\t\t\t}\n\t\t\tif (const auto rights = data.vbanned_rights()) {\n\t\t\t\tchannel->setRestrictions(ChatRestrictionsInfo(*rights));\n\t\t\t} else if (channel->hasRestrictions()) {\n\t\t\t\tchannel->setRestrictions(ChatRestrictionsInfo());\n\t\t\t}\n\t\t\tchannel->date = data.vdate().v;\n\t\t\tchannel->setUnavailableReasons(Data::UnavailableReason::Extract(\n\t\t\t\tdata.vrestriction_reason()));\n\t\t}\n\n\t\t{\n\t\t\tconst auto newUsername = qs(data.vusername().value_or_empty());\n\t\t\tchannel->setName(\n\t\t\t\tqs(data.vtitle()),\n\t\t\t\tTextUtilities::SingleLine(newUsername));\n\t\t\tif (data.vusernames()) {\n\t\t\t\tchannel->setUsernames(\n\t\t\t\t\tApi::Usernames::FromTL(*data.vusernames()));\n\t\t\t} else if (!newUsername.isEmpty()) {\n\t\t\t\tchannel->setUsernames({ { newUsername, true, true } });\n\t\t\t} else {\n\t\t\t\tchannel->setUsernames({});\n\t\t\t}\n\t\t}\n\t\tconst auto hasUsername = !channel->username().isEmpty();\n\n\t\tusing Flag = ChannelDataFlag;\n\t\tconst auto flagsMask = Flag::Broadcast\n\t\t\t| Flag::Verified\n\t\t\t| Flag::Scam\n\t\t\t| Flag::Fake\n\t\t\t| Flag::Megagroup\n\t\t\t| Flag::Gigagroup\n\t\t\t| Flag::Username\n\t\t\t| Flag::Signatures\n\t\t\t| Flag::SignatureProfiles\n\t\t\t| Flag::HasLink\n\t\t\t| Flag::SlowmodeEnabled\n\t\t\t| Flag::CallActive\n\t\t\t| Flag::CallNotEmpty\n\t\t\t| Flag::Forbidden\n\t\t\t| (!minimal\n\t\t\t\t? (Flag::Left | Flag::Creator)\n\t\t\t\t: Flag())\n\t\t\t| Flag::NoForwards\n\t\t\t| Flag::JoinToWrite\n\t\t\t| Flag::RequestToJoin\n\t\t\t| Flag::Forum\n\t\t\t| Flag::ForumTabs\n\t\t\t| ((!minimal && !data.is_stories_hidden_min())\n\t\t\t\t? Flag::StoriesHidden\n\t\t\t\t: Flag())\n\t\t\t| Flag::AutoTranslation\n\t\t\t| Flag::Monoforum\n\t\t\t| Flag::HasStarsPerMessage\n\t\t\t| Flag::StarsPerMessageKnown;\n\t\tconst auto hasStarsPerMessage\n\t\t\t= data.vsend_paid_messages_stars().has_value();\n\t\tif (!hasStarsPerMessage) {\n\t\t\tchannel->setStarsPerMessage(0);\n\t\t\t_commonStarsPerMessage.remove(channel);\n\t\t} else if (const auto count = data.vsend_paid_messages_stars()->v) {\n\t\t\t_commonStarsPerMessage[channel] = count;\n\t\t} else {\n\t\t\t_commonStarsPerMessage.remove(channel);\n\t\t}\n\t\tconst auto storiesState = minimal\n\t\t\t? std::optional<Data::Stories::PeerSourceState>()\n\t\t\t: data.is_stories_unavailable()\n\t\t\t? Data::Stories::PeerSourceState()\n\t\t\t: !data.vstories_max_id()\n\t\t\t? std::optional<Data::Stories::PeerSourceState>()\n\t\t\t: stories().peerSourceState(channel, *data.vstories_max_id());\n\t\tconst auto flagsSet = (data.is_broadcast() ? Flag::Broadcast : Flag())\n\t\t\t| (data.is_verified() ? Flag::Verified : Flag())\n\t\t\t| (data.is_scam() ? Flag::Scam : Flag())\n\t\t\t| (data.is_fake() ? Flag::Fake : Flag())\n\t\t\t| (data.is_megagroup() ? Flag::Megagroup : Flag())\n\t\t\t| (data.is_gigagroup() ? Flag::Gigagroup : Flag())\n\t\t\t| (hasUsername ? Flag::Username : Flag())\n\t\t\t| (data.is_signatures() ? Flag::Signatures : Flag())\n\t\t\t| (data.is_signature_profiles() ? Flag::SignatureProfiles : Flag())\n\t\t\t| (data.is_has_link() ? Flag::HasLink : Flag())\n\t\t\t| (data.is_slowmode_enabled() ? Flag::SlowmodeEnabled : Flag())\n\t\t\t| (data.is_call_active() ? Flag::CallActive : Flag())\n\t\t\t| ((data.is_call_not_empty()\n\t\t\t\t|| (channel->groupCall()\n\t\t\t\t\t&& channel->groupCall()->fullCount() > 0))\n\t\t\t\t? Flag::CallNotEmpty\n\t\t\t\t: Flag())\n\t\t\t| (!minimal\n\t\t\t\t? ((data.is_left() ? Flag::Left : Flag())\n\t\t\t\t\t| (data.is_creator() ? Flag::Creator : Flag()))\n\t\t\t\t: Flag())\n\t\t\t| (data.is_noforwards() ? Flag::NoForwards : Flag())\n\t\t\t| (data.is_join_to_send() ? Flag::JoinToWrite : Flag())\n\t\t\t| (data.is_join_request() ? Flag::RequestToJoin : Flag())\n\t\t\t| ((data.is_forum() && data.is_megagroup())\n\t\t\t\t? Flag::Forum\n\t\t\t\t: Flag())\n\t\t\t| (data.is_forum_tabs() ? Flag::ForumTabs : Flag())\n\t\t\t| ((!minimal\n\t\t\t\t&& !data.is_stories_hidden_min()\n\t\t\t\t&& data.is_stories_hidden())\n\t\t\t\t? Flag::StoriesHidden\n\t\t\t\t: Flag())\n\t\t\t| (data.is_autotranslation() ? Flag::AutoTranslation : Flag())\n\t\t\t| (data.is_monoforum() ? Flag::Monoforum : Flag())\n\t\t\t| (hasStarsPerMessage\n\t\t\t\t? (Flag::HasStarsPerMessage\n\t\t\t\t\t| (channel->starsPerMessageKnown()\n\t\t\t\t\t\t? Flag::StarsPerMessageKnown\n\t\t\t\t\t\t: Flag()))\n\t\t\t\t: Flag::StarsPerMessageKnown);\n\t\tchannel->setFlags((channel->flags() & ~flagsMask) | flagsSet);\n\t\tchannel->setBotVerifyDetailsIcon(\n\t\t\tdata.vbot_verification_icon().value_or_empty());\n\t\tif (!minimal && storiesState) {\n\t\t\tresult->setStoriesState(storiesState->hasVideoStream\n\t\t\t\t? PeerData::StoriesState::HasVideoStream\n\t\t\t\t: !storiesState->maxId\n\t\t\t\t? PeerData::StoriesState::None\n\t\t\t\t: (storiesState->maxId > storiesState->readTill)\n\t\t\t\t? PeerData::StoriesState::HasUnread\n\t\t\t\t: PeerData::StoriesState::HasRead);\n\t\t}\n\n\t\tchannel->setPhoto(data.vphoto());\n\t\tapplyMonoforumLinkedId(\n\t\t\tchannel,\n\t\t\tdata.vlinked_monoforum_id().value_or_empty());\n\n\t\tif (wasInChannel != channel->amIn()) {\n\t\t\tflags |= UpdateFlag::ChannelAmIn;\n\t\t}\n\t\tif (canViewAdmins != channel->canViewAdmins()\n\t\t\t|| canViewMembers != channel->canViewMembers()\n\t\t\t|| canAddMembers != channel->canAddMembers()) {\n\t\t\tflags |= UpdateFlag::Rights;\n\t\t}\n\t\tif (wasCallNotEmpty != Data::ChannelHasActiveCall(channel)) {\n\t\t\tflags |= UpdateFlag::GroupCall;\n\t\t}\n\t\tif (result->changeColor(data.vcolor())) {\n\t\t\tflags |= UpdateFlag::Color;\n\t\t\tif (result->isMinimalLoaded()) {\n\t\t\t\t_peerDecorationsUpdated.fire_copy(result);\n\t\t\t}\n\t\t}\n\t\tif (result->changeColorProfile(data.vprofile_color())) {\n\t\t\tflags |= UpdateFlag::ColorProfile;\n\t\t}\n\t}, [&](const MTPDchannelForbidden &data) {\n\t\tconst auto channel = result->asChannel();\n\n\t\tauto wasInChannel = channel->amIn();\n\t\tauto canViewAdmins = channel->canViewAdmins();\n\t\tauto canViewMembers = channel->canViewMembers();\n\t\tauto canAddMembers = channel->canAddMembers();\n\n\t\tusing Flag = ChannelDataFlag;\n\t\tconst auto flagsMask = Flag::Broadcast\n\t\t\t| Flag::Megagroup\n\t\t\t| Flag::Forbidden\n\t\t\t| Flag::Monoforum;\n\t\tconst auto flagsSet = (data.is_broadcast() ? Flag::Broadcast : Flag())\n\t\t\t| (data.is_megagroup() ? Flag::Megagroup : Flag())\n\t\t\t| (data.is_monoforum() ? Flag::Monoforum : Flag())\n\t\t\t| Flag::Forbidden;\n\t\tchannel->setFlags((channel->flags() & ~flagsMask) | flagsSet);\n\n\t\tif (channel->hasAdminRights()) {\n\t\t\tchannel->setAdminRights(ChatAdminRights());\n\t\t}\n\t\tif (channel->hasRestrictions()) {\n\t\t\tchannel->setRestrictions(ChatRestrictionsInfo());\n\t\t}\n\n\t\tchannel->setName(qs(data.vtitle()), QString());\n\n\t\tchannel->setAccessHash(data.vaccess_hash().v);\n\t\tchannel->setPhoto(MTP_chatPhotoEmpty());\n\t\tchannel->date = 0;\n\t\tchannel->setMembersCount(0);\n\n\t\tif (wasInChannel != channel->amIn()) {\n\t\t\tflags |= UpdateFlag::ChannelAmIn;\n\t\t}\n\t\tif (canViewAdmins != channel->canViewAdmins()\n\t\t\t|| canViewMembers != channel->canViewMembers()\n\t\t\t|| canAddMembers != channel->canAddMembers()) {\n\t\t\tflags |= UpdateFlag::Rights;\n\t\t}\n\t}, [](const MTPDchatEmpty &) {\n\t});\n\n\tif (minimal) {\n\t\tif (!result->isMinimalLoaded()) {\n\t\t\tresult->setLoadedStatus(PeerData::LoadedStatus::Minimal);\n\t\t}\n\t} else if (!result->isLoaded()) {\n\t\tresult->setLoadedStatus(PeerData::LoadedStatus::Normal);\n\t}\n\tif (flags) {\n\t\tsession().changes().peerUpdated(result, flags);\n\t}\n\treturn result;\n}\n\nUserData *Session::processUsers(const MTPVector<MTPUser> &data) {\n\tauto result = (UserData*)nullptr;\n\tfor (const auto &user : data.v) {\n\t\tresult = processUser(user);\n\t}\n\treturn result;\n}\n\nPeerData *Session::processChats(const MTPVector<MTPChat> &data) {\n\tauto result = (PeerData*)nullptr;\n\t_postponedMonoforumLinkedIds.emplace();\n\tfor (const auto &chat : data.v) {\n\t\tresult = processChat(chat);\n\t}\n\tconst auto ids = base::take(_postponedMonoforumLinkedIds);\n\tfor (const auto &[channel, linkedId] : *ids) {\n\t\tapplyMonoforumLinkedId(channel, linkedId);\n\t}\n\treturn result;\n}\n\nvoid Session::applyMonoforumLinkedId(\n\t\tnot_null<ChannelData*> channel,\n\t\tChannelId linkedId) {\n\tif (!linkedId) {\n\t\tchannel->setMonoforumLink(nullptr);\n\t} else if (_postponedMonoforumLinkedIds) {\n\t\t_postponedMonoforumLinkedIds->emplace(channel, linkedId);\n\t} else {\n\t\tconst auto loaded = channel->isLoaded();\n\t\tconst auto linked = this->channel(linkedId);\n\t\tconst auto good = loaded\n\t\t\t? linked->isLoaded()\n\t\t\t: linked->isMinimalLoaded();\n\t\tif (good) {\n\t\t\tchannel->setMonoforumLink(linked);\n\t\t} else {\n\t\t\tchannel->updateFull();\n\t\t}\n\t}\n}\n\nvoid Session::applyMaximumChatVersions(const MTPVector<MTPChat> &data) {\n\tfor (const auto &chat : data.v) {\n\t\tchat.match([&](const MTPDchat &data) {\n\t\t\tif (const auto chat = chatLoaded(data.vid().v)) {\n\t\t\t\tif (data.vversion().v < chat->version()) {\n\t\t\t\t\tchat->setVersion(data.vversion().v);\n\t\t\t\t}\n\t\t\t}\n\t\t}, [](const auto &) {\n\t\t});\n\t}\n}\n\nvoid Session::registerGroupCall(not_null<GroupCall*> call) {\n\t_groupCalls.emplace(call->id(), call);\n}\n\nvoid Session::unregisterGroupCall(not_null<GroupCall*> call) {\n\t_groupCalls.remove(call->id());\n}\n\nGroupCall *Session::groupCall(CallId callId) const {\n\tconst auto i = _groupCalls.find(callId);\n\treturn (i != end(_groupCalls)) ? i->second.get() : nullptr;\n}\n\nstd::shared_ptr<GroupCall> Session::sharedConferenceCall(\n\t\tCallId id,\n\t\tuint64 accessHash) {\n\tconst auto i = _conferenceCalls.find(id);\n\tif (i != end(_conferenceCalls)) {\n\t\tif (auto result = i->second.lock()) {\n\t\t\treturn result;\n\t\t}\n\t}\n\tauto result = std::make_shared<GroupCall>(\n\t\tsession().user(),\n\t\tid,\n\t\taccessHash,\n\t\tTimeId(), // scheduledDate\n\t\tfalse, // rtmp\n\t\tGroupCallOrigin::Conference);\n\tif (i != end(_conferenceCalls)) {\n\t\ti->second = result;\n\t} else {\n\t\t_conferenceCalls.emplace(id, result);\n\t}\n\treturn result;\n}\n\nstd::shared_ptr<GroupCall> Session::sharedConferenceCallFind(\n\t\tconst MTPUpdates &response) {\n\tconst auto list = response.match([&](const MTPDupdates &data) {\n\t\treturn &data.vupdates().v;\n\t}, [&](const MTPDupdatesCombined &data) {\n\t\treturn &data.vupdates().v;\n\t}, [](const auto &) {\n\t\treturn (const QVector<MTPUpdate>*)nullptr;\n\t});\n\tconst auto empty = std::shared_ptr<GroupCall>();\n\tif (!list) {\n\t\treturn empty;\n\t}\n\tfor (const auto &update : *list) {\n\t\tconst auto call = update.match([&](const MTPDupdateGroupCall &data) {\n\t\t\treturn data.vcall().match([&](const MTPDgroupCall &data) {\n\t\t\t\treturn data.is_conference()\n\t\t\t\t\t? sharedConferenceCall(\n\t\t\t\t\t\tdata.vid().v,\n\t\t\t\t\t\tdata.vaccess_hash().v)\n\t\t\t\t\t: nullptr;\n\t\t\t}, [&](const auto &) { return empty; });\n\t\t}, [&](const auto &) { return empty; });\n\t\tif (call) {\n\t\t\treturn call;\n\t\t}\n\t}\n\treturn empty;\n}\n\nvoid Session::watchForOffline(not_null<UserData*> user, TimeId now) {\n\tif (!now) {\n\t\tnow = base::unixtime::now();\n\t}\n\tif (!Data::IsUserOnline(user, now)) {\n\t\treturn;\n\t}\n\tconst auto lastseen = user->lastseen();\n\tconst auto till = lastseen.onlineTill();\n\tconst auto &[i, ok] = _watchingForOffline.emplace(user, till);\n\tif (!ok) {\n\t\tif (i->second == till) {\n\t\t\treturn;\n\t\t}\n\t\ti->second = till;\n\t}\n\tconst auto timeout = Data::OnlineChangeTimeout(lastseen, now);\n\tconst auto fires = _watchForOfflineTimer.isActive()\n\t\t? _watchForOfflineTimer.remainingTime()\n\t\t: -1;\n\tif (fires >= 0 && fires <= timeout) {\n\t\treturn;\n\t}\n\t_watchForOfflineTimer.callOnce(std::max(timeout, crl::time(1)));\n}\n\nvoid Session::maybeStopWatchForOffline(not_null<UserData*> user) {\n\tif (Data::IsUserOnline(user)) {\n\t\treturn;\n\t} else if (_watchingForOffline.remove(user)\n\t\t&& _watchingForOffline.empty()) {\n\t\t_watchForOfflineTimer.cancel();\n\t}\n}\n\nvoid Session::recordSharingDisabledTime(not_null<UserData*> user) {\n\t_sharingDisabledTimes[user] = base::unixtime::now();\n}\n\nbool Session::sharingRecentlyDisabledByMe(\n\t\tnot_null<UserData*> user) const {\n\tconst auto i = _sharingDisabledTimes.find(user);\n\treturn (i != end(_sharingDisabledTimes))\n\t\t&& (base::unixtime::now() - i->second < 86400);\n}\n\nvoid Session::clearSharingDisabledTime(not_null<UserData*> user) {\n\t_sharingDisabledTimes.remove(user);\n}\n\nvoid Session::checkLocalUsersWentOffline() {\n\t_watchForOfflineTimer.cancel();\n\n\tauto minimal = 86400 * crl::time(1000);\n\tconst auto now = base::unixtime::now();\n\tfor (auto i = begin(_watchingForOffline)\n\t\t; i != end(_watchingForOffline);) {\n\t\tconst auto user = i->first;\n\t\tif (!Data::IsUserOnline(user, now)) {\n\t\t\ti = _watchingForOffline.erase(i);\n\t\t\tsession().changes().peerUpdated(\n\t\t\t\tuser,\n\t\t\t\tPeerUpdate::Flag::OnlineStatus);\n\t\t} else {\n\t\t\tconst auto timeout = Data::OnlineChangeTimeout(user, now);\n\t\t\taccumulate_min(minimal, timeout);\n\t\t\t++i;\n\t\t}\n\t}\n\tif (!_watchingForOffline.empty()) {\n\t\t_watchForOfflineTimer.callOnce(std::max(minimal, crl::time(1)));\n\t}\n}\n\nauto Session::invitedToCallUsers(CallId callId) const\n-> const base::flat_map<not_null<UserData*>, bool> & {\n\tstatic const base::flat_map<not_null<UserData*>, bool> kEmpty;\n\tconst auto i = _invitedToCallUsers.find(callId);\n\treturn (i != _invitedToCallUsers.end()) ? i->second : kEmpty;\n}\n\nvoid Session::registerInvitedToCallUser(\n\t\tCallId callId,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user,\n\t\tbool calling) {\n\tregisterInvitedToCallUser(callId, peer->groupCall(), user, calling);\n}\n\nvoid Session::registerInvitedToCallUser(\n\t\tCallId callId,\n\t\tGroupCall *call,\n\t\tnot_null<UserData*> user,\n\t\tbool calling) {\n\tif (call && call->id() == callId) {\n\t\tconst auto inCall = ranges::contains(\n\t\t\tcall->participants(),\n\t\t\tuser,\n\t\t\t&Data::GroupCallParticipant::peer);\n\t\tif (inCall) {\n\t\t\treturn;\n\t\t}\n\t}\n\t_invitedToCallUsers[callId][user] = calling;\n\t_invitesToCalls.fire({ callId, user, calling });\n}\n\nvoid Session::unregisterInvitedToCallUser(\n\t\tCallId callId,\n\t\tnot_null<UserData*> user,\n\t\tbool onlyStopCalling) {\n\tconst auto i = _invitedToCallUsers.find(callId);\n\tif (i != _invitedToCallUsers.end()) {\n\t\tconst auto j = i->second.find(user);\n\t\tif (j != end(i->second)) {\n\t\t\tif (onlyStopCalling) {\n\t\t\t\tif (!j->second) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tj->second = false;\n\t\t\t} else {\n\t\t\t\ti->second.erase(j);\n\t\t\t\tif (i->second.empty()) {\n\t\t\t\t\t_invitedToCallUsers.erase(i);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto calling = false;\n\t\t\tconst auto removed = !onlyStopCalling;\n\t\t\t_invitesToCalls.fire({ callId, user, calling, removed });\n\t\t}\n\t}\n}\n\nUserData *Session::userByPhone(const QString &phone) const {\n\tconst auto pname = phone.trimmed();\n\tfor (const auto &[peerId, peer] : _peers) {\n\t\tif (const auto user = peer->asUser()) {\n\t\t\tif (user->phone() == pname) {\n\t\t\t\treturn user;\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nPeerData *Session::peerByUsername(const QString &username) const {\n\tconst auto uname = username.trimmed();\n\tif (uname.isEmpty()) {\n\t\treturn nullptr;\n\t}\n\tfor (const auto &[peerId, peer] : _peers) {\n\t\tif (peer->isLoaded()\n\t\t\t&& !peer->username().compare(uname, Qt::CaseInsensitive)) {\n\t\t\treturn peer.get();\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid Session::enumerateUsers(Fn<void(not_null<UserData*>)> action) const {\n\tfor (const auto &[peerId, peer] : _peers) {\n\t\tif (const auto user = peer->asUser()) {\n\t\t\taction(user);\n\t\t}\n\t}\n}\n\nvoid Session::enumerateGroups(Fn<void(not_null<PeerData*>)> action) const {\n\tfor (const auto &[peerId, peer] : _peers) {\n\t\tif (peer->isChat() || peer->isMegagroup()) {\n\t\t\taction(peer.get());\n\t\t}\n\t}\n}\n\nvoid Session::enumerateBroadcasts(\n\t\tFn<void(not_null<ChannelData*>)> action) const {\n\tfor (const auto &[peerId, peer] : _peers) {\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\tif (!channel->isMegagroup()) {\n\t\t\t\taction(channel);\n\t\t\t}\n\t\t}\n\t}\n}\n\nnot_null<History*> Session::history(PeerId peerId) {\n\treturn _histories->findOrCreate(peerId);\n}\n\nHistory *Session::historyLoaded(PeerId peerId) const {\n\treturn _histories->find(peerId);\n}\n\nnot_null<History*> Session::history(not_null<const PeerData*> peer) {\n\treturn history(peer->id);\n}\n\nHistory *Session::historyLoaded(const PeerData *peer) {\n\treturn peer ? historyLoaded(peer->id) : nullptr;\n}\n\nvoid Session::deleteConversationLocally(not_null<PeerData*> peer) {\n\tconst auto markLeft = [&] {\n\t\tif (const auto channel = peer->asMegagroup()) {\n\t\t\tchannel->addFlags(ChannelDataFlag::Left);\n\t\t\tif (const auto from = channel->getMigrateFromChat()) {\n\t\t\t\tif (const auto migrated = historyLoaded(from)) {\n\t\t\t\t\tmigrated->updateChatListExistence();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\tif (const auto history = historyLoaded(peer)) {\n\t\tif (history->folderKnown()) {\n\t\t\tsetChatPinned(history, FilterId(), false);\n\t\t}\n\t\tremoveChatListEntry(history);\n\t\thistory->clearFolder();\n\n\t\t// We want to mark the channel as left before unloading the history,\n\t\t// otherwise some parts of updating may return us to the chats list.\n\t\tmarkLeft();\n\t\thistory->clear(peer->isChannel()\n\t\t\t? History::ClearType::Unload\n\t\t\t: History::ClearType::DeleteChat);\n\t} else {\n\t\tmarkLeft();\n\t}\n}\n\nbool Session::chatsListLoaded(Data::Folder *folder) {\n\treturn chatsList(folder)->loaded();\n}\n\nvoid Session::chatsListChanged(FolderId folderId) {\n\tchatsListChanged(folderId ? folder(folderId).get() : nullptr);\n}\n\nvoid Session::chatsListChanged(Data::Folder *folder) {\n\t_chatsListChanged.fire_copy(folder);\n}\n\nvoid Session::chatsListDone(Data::Folder *folder) {\n\tif (folder) {\n\t\tfolder->chatsList()->setLoaded();\n\t} else {\n\t\t_chatsList.setLoaded();\n\t}\n\t_chatsListLoadedEvents.fire_copy(folder);\n}\n\nvoid Session::userIsBotChanged(not_null<UserData*> user) {\n\tif (const auto history = this->history(user)) {\n\t\tchatsFilters().refreshHistory(history);\n\t}\n\t_userIsBotChanges.fire_copy(user);\n}\n\nrpl::producer<not_null<UserData*>> Session::userIsBotChanges() const {\n\treturn _userIsBotChanges.events();\n}\n\nvoid Session::botCommandsChanged(not_null<PeerData*> peer) {\n\t_botCommandsChanges.fire_copy(peer);\n}\n\nrpl::producer<not_null<PeerData*>> Session::botCommandsChanges() const {\n\treturn _botCommandsChanges.events();\n}\n\nStorage::Cache::Database &Session::cache() {\n\treturn *_cache;\n}\n\nStorage::Cache::Database &Session::cacheBigFile() {\n\treturn *_bigFileCache;\n}\n\nvoid Session::suggestStartExport(TimeId availableAt) {\n\t_exportAvailableAt = availableAt;\n\tsuggestStartExport();\n}\n\nvoid Session::clearExportSuggestion() {\n\t_exportAvailableAt = 0;\n\tif (_exportSuggestion) {\n\t\t_exportSuggestion->closeBox();\n\t}\n}\n\nvoid Session::suggestStartExport() {\n\tif (_exportAvailableAt <= 0) {\n\t\treturn;\n\t}\n\n\tconst auto now = base::unixtime::now();\n\tconst auto left = (_exportAvailableAt <= now)\n\t\t? 0\n\t\t: (_exportAvailableAt - now);\n\tif (left) {\n\t\tbase::call_delayed(\n\t\t\tstd::min(left + 5, 3600) * crl::time(1000),\n\t\t\t_session,\n\t\t\t[=] { suggestStartExport(); });\n\t} else if (Core::App().exportManager().inProgress()) {\n\t\tExport::View::ClearSuggestStart(&session());\n\t} else {\n\t\t_exportSuggestion = Export::View::SuggestStart(&session());\n\t}\n}\n\nconst Passport::SavedCredentials *Session::passportCredentials() const {\n\treturn _passportCredentials ? &_passportCredentials->first : nullptr;\n}\n\nvoid Session::rememberPassportCredentials(\n\t\tPassport::SavedCredentials data,\n\t\tcrl::time rememberFor) {\n\tExpects(rememberFor > 0);\n\n\tstatic auto generation = 0;\n\t_passportCredentials = std::make_unique<CredentialsWithGeneration>(\n\t\tstd::move(data),\n\t\t++generation);\n\tbase::call_delayed(rememberFor, _session, [=, check = generation] {\n\t\tif (_passportCredentials && _passportCredentials->second == check) {\n\t\t\tforgetPassportCredentials();\n\t\t}\n\t});\n}\n\nvoid Session::forgetPassportCredentials() {\n\t_passportCredentials = nullptr;\n}\n\nvoid Session::setupMigrationViewer() {\n\tsession().changes().peerUpdates(\n\t\tPeerUpdate::Flag::Migration\n\t) | rpl::map([](const PeerUpdate &update) {\n\t\treturn update.peer->asChat();\n\t}) | rpl::filter([=](ChatData *chat) {\n\t\treturn (chat != nullptr);\n\t}) | rpl::on_next([=](not_null<ChatData*> chat) {\n\t\tconst auto channel = chat->migrateTo();\n\t\tif (!channel) {\n\t\t\treturn;\n\t\t}\n\n\t\tchat->clearGroupCall();\n\t\tif (const auto from = historyLoaded(chat)) {\n\t\t\tif (const auto to = historyLoaded(channel)) {\n\t\t\t\tif (to->inChatList() && from->inChatList()) {\n\t\t\t\t\tremoveChatListEntry(from);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n}\n\nvoid Session::setupChannelLeavingViewer() {\n\tsession().changes().peerUpdates(\n\t\tPeerUpdate::Flag::ChannelAmIn\n\t) | rpl::map([](const PeerUpdate &update) {\n\t\treturn update.peer->asChannel();\n\t}) | rpl::on_next([=](not_null<ChannelData*> channel) {\n\t\tif (channel->amIn()) {\n\t\t\tchannel->clearInvitePeek();\n\t\t} else {\n\t\t\tif (const auto history = historyLoaded(channel->id)) {\n\t\t\t\thistory->removeJoinedMessage();\n\t\t\t\thistory->updateChatListExistence();\n\t\t\t\thistory->updateChatListSortPosition();\n\t\t\t\tif (!history->inChatList()) {\n\t\t\t\t\thistory->clearFolder();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n}\n\nvoid Session::setupPeerNameViewer() {\n\tsession().changes().realtimeNameUpdates(\n\t) | rpl::on_next([=](const NameUpdate &update) {\n\t\tconst auto peer = update.peer;\n\t\tif (const auto history = historyLoaded(peer)) {\n\t\t\thistory->refreshChatListNameSortKey();\n\t\t}\n\t\tconst auto &oldLetters = update.oldFirstLetters;\n\t\t_contactsNoChatsList.peerNameChanged(peer, oldLetters);\n\t\t_contactsList.peerNameChanged(peer, oldLetters);\n\t}, _lifetime);\n}\n\nvoid Session::setupUserIsContactViewer() {\n\tsession().changes().peerUpdates(\n\t\tPeerUpdate::Flag::IsContact\n\t) | rpl::map([](const PeerUpdate &update) {\n\t\treturn update.peer->asUser();\n\t}) | rpl::on_next([=](not_null<UserData*> user) {\n\t\tconst auto i = _contactViews.find(peerToUser(user->id));\n\t\tif (i != _contactViews.end()) {\n\t\t\tfor (const auto &view : i->second) {\n\t\t\t\trequestViewResize(view);\n\t\t\t}\n\t\t}\n\t\tif (!user->isLoaded()) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"userIsContactChanged() called for a not loaded user!\"));\n\t\t\treturn;\n\t\t}\n\t\tif (user->isContact()) {\n\t\t\tconst auto history = this->history(user->id);\n\t\t\t_contactsList.addByName(history);\n\t\t\tif (!history->inChatList()) {\n\t\t\t\t_contactsNoChatsList.addByName(history);\n\t\t\t}\n\t\t} else if (const auto history = historyLoaded(user)) {\n\t\t\t_contactsNoChatsList.remove(history);\n\t\t\t_contactsList.remove(history);\n\t\t}\n\t}, _lifetime);\n}\n\nSession::~Session() = default;\n\ntemplate <typename Method>\nvoid Session::enumerateItemViews(\n\t\tnot_null<const HistoryItem*> item,\n\t\tMethod method) {\n\tif (const auto i = _views.find(item); i != _views.end()) {\n\t\tfor (const auto &view : i->second) {\n\t\t\tmethod(view);\n\t\t}\n\t}\n}\n\nvoid Session::photoLoadSettingsChanged() {\n\tfor (const auto &[id, photo] : _photos) {\n\t\tphoto->automaticLoadSettingsChanged();\n\t}\n}\n\nvoid Session::documentLoadSettingsChanged() {\n\tfor (const auto &[id, document] : _documents) {\n\t\tdocument->automaticLoadSettingsChanged();\n\t}\n}\n\nvoid Session::notifyPhotoLayoutChanged(not_null<const PhotoData*> photo) {\n\tif (const auto i = _photoItems.find(photo); i != end(_photoItems)) {\n\t\tfor (const auto &item : i->second) {\n\t\t\tnotifyItemLayoutChange(item);\n\t\t}\n\t}\n}\n\nvoid Session::requestPhotoViewRepaint(not_null<const PhotoData*> photo) {\n\tconst auto i = _photoItems.find(photo);\n\tif (i != end(_photoItems)) {\n\t\tfor (const auto &item : i->second) {\n\t\t\trequestItemRepaint(item);\n\t\t}\n\t}\n}\n\nvoid Session::notifyDocumentLayoutChanged(\n\t\tnot_null<const DocumentData*> document) {\n\tconst auto i = _documentItems.find(document);\n\tif (i != end(_documentItems)) {\n\t\tfor (const auto &item : i->second) {\n\t\t\tnotifyItemLayoutChange(item);\n\t\t}\n\t}\n\tif (const auto items = InlineBots::Layout::documentItems()) {\n\t\tif (const auto i = items->find(document); i != items->end()) {\n\t\t\tfor (const auto &item : i->second) {\n\t\t\t\titem->layoutChanged();\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Session::requestDocumentViewRepaint(\n\t\tnot_null<const DocumentData*> document) {\n\tconst auto i = _documentItems.find(document);\n\tif (i != end(_documentItems)) {\n\t\tfor (const auto &item : i->second) {\n\t\t\trequestItemRepaint(item);\n\t\t}\n\t}\n}\n\nvoid Session::requestPollViewRepaint(not_null<const PollData*> poll) {\n\tif (const auto i = _pollViews.find(poll); i != _pollViews.end()) {\n\t\tfor (const auto &view : i->second) {\n\t\t\trequestViewResize(view);\n\t\t}\n\t}\n}\n\nvoid Session::requestTodoListViewRepaint(\n\t\tnot_null<const TodoListData*> todolist) {\n\tif (const auto i = _todoListViews.find(todolist)\n\t\t; i != _todoListViews.end()) {\n\t\tfor (const auto &view : i->second) {\n\t\t\trequestViewResize(view);\n\t\t}\n\t}\n}\n\nvoid Session::documentLoadProgress(not_null<DocumentData*> document) {\n\trequestDocumentViewRepaint(document);\n\t_documentLoadProgress.fire_copy(document);\n}\n\nvoid Session::documentLoadDone(not_null<DocumentData*> document) {\n\tnotifyDocumentLayoutChanged(document);\n\t_documentLoadProgress.fire_copy(document);\n}\n\nvoid Session::documentLoadFail(\n\t\tnot_null<DocumentData*> document,\n\t\tbool started) {\n\tnotifyDocumentLayoutChanged(document);\n\t_documentLoadProgress.fire_copy(document);\n}\n\nvoid Session::photoLoadProgress(not_null<PhotoData*> photo) {\n\trequestPhotoViewRepaint(photo);\n}\n\nvoid Session::photoLoadDone(not_null<PhotoData*> photo) {\n\tnotifyPhotoLayoutChanged(photo);\n}\n\nvoid Session::photoLoadFail(\n\t\tnot_null<PhotoData*> photo,\n\t\tbool started) {\n\tnotifyPhotoLayoutChanged(photo);\n}\n\nvoid Session::markMediaRead(not_null<const DocumentData*> document) {\n\tconst auto i = _documentItems.find(document);\n\tif (i != end(_documentItems)) {\n\t\tauto items = base::flat_set<not_null<HistoryItem*>>();\n\t\titems.reserve(i->second.size());\n\t\tfor (const auto &item : i->second) {\n\t\t\tif (item->isUnreadMention() || item->isIncomingUnreadMedia()) {\n\t\t\t\titems.emplace(item);\n\t\t\t}\n\t\t}\n\t\t_session->api().markContentsRead(items);\n\t}\n}\n\nvoid Session::notifyItemLayoutChange(not_null<const HistoryItem*> item) {\n\t_itemLayoutChanges.fire_copy(item);\n\tenumerateItemViews(item, [&](not_null<ViewElement*> view) {\n\t\tnotifyViewLayoutChange(view);\n\t});\n}\n\nrpl::producer<not_null<const HistoryItem*>> Session::itemLayoutChanged() const {\n\treturn _itemLayoutChanges.events();\n}\n\nvoid Session::notifyViewLayoutChange(not_null<const ViewElement*> view) {\n\t_viewLayoutChanges.fire_copy(view);\n}\n\nrpl::producer<not_null<const ViewElement*>> Session::viewLayoutChanged() const {\n\treturn _viewLayoutChanges.events();\n}\n\nvoid Session::notifyNewItemAdded(not_null<HistoryItem*> item) {\n\t_newItemAdded.fire_copy(item);\n}\n\nrpl::producer<not_null<HistoryItem*>> Session::newItemAdded() const {\n\treturn _newItemAdded.events();\n}\n\nvoid Session::notifyGiftUpdate(GiftUpdate &&update) {\n\t_giftUpdates.fire(std::move(update));\n}\n\nrpl::producer<GiftUpdate> Session::giftUpdates() const {\n\treturn _giftUpdates.events();\n}\n\nvoid Session::notifyGiftsUpdate(GiftsUpdate &&update) {\n\t_giftsUpdates.fire(std::move(update));\n}\n\nrpl::producer<GiftsUpdate> Session::giftsUpdates() const {\n\treturn _giftsUpdates.events();\n}\n\nvoid Session::notifyGiftAuctionGot(GiftAuctionGot &&update) {\n\t_giftAuctionGots.fire(std::move(update));\n}\n\nrpl::producer<GiftAuctionGot> Session::giftAuctionGots() const {\n\treturn _giftAuctionGots.events();\n}\n\nHistoryItem *Session::changeMessageId(PeerId peerId, MsgId wasId, MsgId nowId) {\n\tconst auto list = messagesListForInsert(peerId);\n\tconst auto i = list->find(wasId);\n\tif (i == list->end()) {\n\t\treturn nullptr;\n\t}\n\tconst auto item = i->second;\n\tlist->erase(i);\n\tconst auto &[j, ok] = list->emplace(nowId, item);\n\n\tif (!peerIsChannel(peerId)) {\n\t\tif (IsServerMsgId(wasId)) {\n\t\t\tconst auto k = _nonChannelMessages.find(wasId);\n\t\t\tAssert(k != end(_nonChannelMessages));\n\t\t\t_nonChannelMessages.erase(k);\n\t\t}\n\t\tif (IsServerMsgId(nowId)) {\n\t\t\t_nonChannelMessages.emplace(nowId, item);\n\t\t}\n\t}\n\n\tEnsures(ok);\n\treturn item;\n}\n\nbool Session::queryItemVisibility(not_null<HistoryItem*> item) const {\n\tauto result = false;\n\t_itemVisibilityQueries.fire({ item, &result });\n\treturn result;\n}\n\nbool Session::queryDocumentVisibility(\n\t\tnot_null<DocumentData*> document) const {\n\tconst auto i = _documentItems.find(document);\n\tif (i != end(_documentItems)) {\n\t\tfor (const auto &item : i->second) {\n\t\t\tif (queryItemVisibility(item)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\n\n[[nodiscard]] auto Session::itemVisibilityQueries() const\n-> rpl::producer<Session::ItemVisibilityQuery> {\n\treturn _itemVisibilityQueries.events();\n}\n\nvoid Session::itemVisibilitiesUpdated() {\n\t// This could be rewritten in a more generic form, like:\n\t// rpl::producer<> itemVisibilitiesUpdates()\n\t// if someone else requires those methods, using fast for now.\n\tCore::App().downloadManager().itemVisibilitiesUpdated(_session);\n}\n\nvoid Session::notifyItemIdChange(IdChange event) {\n\tconst auto item = changeMessageId(\n\t\tevent.newId.peer,\n\t\tevent.oldId,\n\t\tevent.newId.msg);\n\n\t_itemIdChanges.fire_copy(event);\n\n\tif (item) {\n\t\tconst auto refreshViewDataId = [](not_null<ViewElement*> view) {\n\t\t\tview->refreshDataId();\n\t\t};\n\t\tenumerateItemViews(item, refreshViewDataId);\n\t\tif (const auto group = groups().find(item)) {\n\t\t\tconst auto leader = group->items.front();\n\t\t\tif (leader != item) {\n\t\t\t\tenumerateItemViews(leader, refreshViewDataId);\n\t\t\t}\n\t\t}\n\t}\n}\n\nrpl::producer<Session::IdChange> Session::itemIdChanged() const {\n\treturn _itemIdChanges.events();\n}\n\nvoid Session::requestItemRepaint(not_null<const HistoryItem*> item, QRect r) {\n\t_itemRepaintRequest.fire_copy(item);\n\tauto repaintGroupLeader = false;\n\tauto repaintView = [&](not_null<const ViewElement*> view) {\n\t\tif (view->isHiddenByGroup()) {\n\t\t\trepaintGroupLeader = true;\n\t\t} else {\n\t\t\trequestViewRepaint(view, r);\n\t\t}\n\t};\n\tenumerateItemViews(item, repaintView);\n\tif (repaintGroupLeader) {\n\t\tif (const auto group = groups().find(item)) {\n\t\t\tconst auto leader = group->items.front();\n\t\t\tif (leader != item) {\n\t\t\t\tenumerateItemViews(leader, repaintView);\n\t\t\t}\n\t\t}\n\t}\n\tconst auto history = item->history();\n\tif (history->lastItemDialogsView().dependsOn(item)) {\n\t\thistory->updateChatListEntry();\n\t}\n\tif (const auto topic = item->topic()) {\n\t\tif (topic->lastItemDialogsView().dependsOn(item)) {\n\t\t\ttopic->updateChatListEntry();\n\t\t}\n\t}\n\tif (const auto sublist = item->savedSublist()) {\n\t\tif (sublist->lastItemDialogsView().dependsOn(item)) {\n\t\t\tsublist->updateChatListEntry();\n\t\t}\n\t}\n}\n\nrpl::producer<not_null<const HistoryItem*>> Session::itemRepaintRequest() const {\n\treturn _itemRepaintRequest.events();\n}\n\nvoid Session::requestDrawToReply(DrawToReplyRequest request) {\n\t_drawToReplyRequests.fire_copy(std::move(request));\n}\n\nrpl::producer<DrawToReplyRequest> Session::drawToReplyRequests() const {\n\treturn _drawToReplyRequests.events();\n}\n\nvoid Session::requestViewRepaint(not_null<const ViewElement*> view, QRect r) {\n\t_viewRepaintRequest.fire_copy({ view, r });\n}\n\nrpl::producer<RequestViewRepaint> Session::viewRepaintRequest() const {\n\treturn _viewRepaintRequest.events();\n}\n\nvoid Session::requestItemResize(not_null<const HistoryItem*> item) {\n\t_itemResizeRequest.fire_copy(item);\n\tenumerateItemViews(item, [&](not_null<ViewElement*> view) {\n\t\trequestViewResize(view);\n\t});\n}\n\nrpl::producer<not_null<const HistoryItem*>> Session::itemResizeRequest() const {\n\treturn _itemResizeRequest.events();\n}\n\nvoid Session::requestViewResize(not_null<ViewElement*> view) {\n\tview->setPendingResize();\n\t_viewResizeRequest.fire_copy(view);\n\tnotifyViewLayoutChange(view);\n}\n\nrpl::producer<not_null<ViewElement*>> Session::viewResizeRequest() const {\n\treturn _viewResizeRequest.events();\n}\n\nvoid Session::notifyViewHeightAdjusted(\n\t\tnot_null<ViewElement*> view,\n\t\tint delta) {\n\t_viewHeightAdjusted.fire({ view, delta });\n}\n\nrpl::producer<Session::ViewHeightAdjusted> Session::viewHeightAdjusted() const {\n\treturn _viewHeightAdjusted.events();\n}\n\nvoid Session::requestItemShowHighlight(not_null<HistoryItem*> item) {\n\t_itemShowHighlightRequest.fire_copy(item);\n}\n\nrpl::producer<not_null<HistoryItem*>> Session::itemShowHighlightRequest() const {\n\treturn _itemShowHighlightRequest.events();\n}\n\nvoid Session::requestItemViewRefresh(not_null<const HistoryItem*> item) {\n\tif (const auto view = item->mainView()) {\n\t\tnotifyHistoryChangeDelayed(item->history());\n\t\tview->refreshInBlock();\n\t}\n\t_itemViewRefreshRequest.fire_copy(item);\n}\n\nrpl::producer<not_null<const HistoryItem*>> Session::itemViewRefreshRequest() const {\n\treturn _itemViewRefreshRequest.events();\n}\n\nvoid Session::notifyItemDataChange(not_null<HistoryItem*> item) {\n\t_itemDataChanges.fire_copy(item);\n}\n\nrpl::producer<not_null<HistoryItem*>> Session::itemDataChanges() const {\n\treturn _itemDataChanges.events();\n}\n\nvoid Session::requestItemTextRefresh(not_null<HistoryItem*> item) {\n\tconst auto call = [&](not_null<HistoryItem*> item) {\n\t\tenumerateItemViews(item, [&](not_null<ViewElement*> view) {\n\t\t\tview->itemTextUpdated();\n\t\t});\n\t\trequestItemResize(item);\n\t\tif (item->textAppearing()) {\n\t\t\tenumerateItemViews(item, [&](not_null<ViewElement*> view) {\n\t\t\t\tview->skipInactiveTextAppearing();\n\t\t\t});\n\t\t}\n\t};\n\tif (const auto group = groups().find(item)) {\n\t\tcall(group->items.front());\n\t} else {\n\t\tcall(item);\n\t}\n}\n\nvoid Session::registerRestricted(\n\t\tnot_null<const HistoryItem*> item,\n\t\tconst QString &reason) {\n\tExpects(item->hasPossibleRestrictions());\n\n\t_possiblyRestricted[item].emplace(reason);\n}\n\nvoid Session::registerRestricted(\n\t\tnot_null<const HistoryItem*> item,\n\t\tconst std::vector<UnavailableReason> &reasons) {\n\tExpects(item->hasPossibleRestrictions());\n\n\tauto &list = _possiblyRestricted[item];\n\tif (list.empty()) {\n\t\tauto &&simple = reasons\n\t\t\t| ranges::views::transform(&UnavailableReason::reason);\n\t\tlist = { begin(simple), end(simple) };\n\t} else {\n\t\tfor (const auto &reason : reasons) {\n\t\t\tlist.emplace(reason.reason);\n\t\t}\n\t}\n}\n\nvoid Session::registerHighlightProcess(\n\t\tuint64 processId,\n\t\tnot_null<HistoryItem*> item) {\n\tExpects(item->inHighlightProcess());\n\n\tconst auto &[i, ok] = _highlightings.emplace(processId, item);\n\n\tEnsures(ok);\n}\n\nvoid Session::highlightProcessDone(uint64 processId) {\n\tif (const auto done = _highlightings.take(processId)) {\n\t\tfor (const auto &[id, item] : _highlightings) {\n\t\t\tif (item == *done) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\t(*done)->highlightProcessDone();\n\t}\n}\n\nvoid Session::requestUnreadReactionsAnimation(not_null<HistoryItem*> item) {\n\tenumerateItemViews(item, [&](not_null<ViewElement*> view) {\n\t\tview->animateUnreadReactions();\n\t});\n}\n\nrpl::producer<not_null<const HistoryItem*>> Session::itemRemoved() const {\n\treturn _itemRemoved.events();\n}\n\nrpl::producer<not_null<const HistoryItem*>> Session::itemRemoved(\n\t\tFullMsgId itemId) const {\n\treturn itemRemoved(\n\t) | rpl::filter([=](not_null<const HistoryItem*> item) {\n\t\treturn (itemId == item->fullId());\n\t});\n}\n\nvoid Session::notifyItemsAboutToBeDestroyed(\n\t\tconst std::vector<not_null<HistoryItem*>> &items) {\n\t_itemsAboutToBeDestroyed.fire_copy(items);\n}\n\nauto Session::itemsAboutToBeDestroyed() const\n-> rpl::producer<std::vector<not_null<HistoryItem*>>> {\n\treturn _itemsAboutToBeDestroyed.events();\n}\n\nvoid Session::notifyViewAboutToBeRemoved(\n\t\tnot_null<const ViewElement*> view) {\n\t_viewAboutToBeRemoved.fire_copy(view);\n}\n\nrpl::producer<not_null<const ViewElement*>>\nSession::viewAboutToBeRemoved() const {\n\treturn _viewAboutToBeRemoved.events();\n}\n\nvoid Session::notifyViewRemoved(not_null<const ViewElement*> view) {\n\t_viewRemoved.fire_copy(view);\n}\n\nrpl::producer<not_null<const ViewElement*>> Session::viewRemoved() const {\n\treturn _viewRemoved.events();\n}\n\nvoid Session::notifyViewPaidReactionSent(not_null<const ViewElement*> view) {\n\t_viewPaidReactionSent.fire_copy(view);\n}\n\nrpl::producer<not_null<const ViewElement*>> Session::viewPaidReactionSent() const {\n\treturn _viewPaidReactionSent.events();\n}\n\nvoid Session::notifyCallPaidReactionSent(not_null<Calls::GroupCall*> call) {\n\t_callPaidReactionSent.fire_copy(call);\n}\n\nrpl::producer<not_null<Calls::GroupCall*>> Session::callPaidReactionSent() const {\n\treturn _callPaidReactionSent.events();\n}\n\nvoid Session::notifyHistoryUnloaded(not_null<const History*> history) {\n\t_historyUnloaded.fire_copy(history);\n}\n\nrpl::producer<not_null<const History*>> Session::historyUnloaded() const {\n\treturn _historyUnloaded.events();\n}\n\nvoid Session::notifyHistoryCleared(not_null<const History*> history) {\n\t_historyCleared.fire_copy(history);\n}\n\nrpl::producer<not_null<const History*>> Session::historyCleared() const {\n\treturn _historyCleared.events();\n}\n\nvoid Session::notifyHistoryChangeDelayed(not_null<History*> history) {\n\thistory->setHasPendingResizedItems();\n\t_historiesChanged.insert(history);\n}\n\nrpl::producer<not_null<History*>> Session::historyChanged() const {\n\treturn _historyChanged.events();\n}\n\nvoid Session::sendHistoryChangeNotifications() {\n\tfor (const auto &history : base::take(_historiesChanged)) {\n\t\t_historyChanged.fire_copy(history);\n\t}\n}\n\nvoid Session::notifyPinnedDialogsOrderUpdated() {\n\t_pinnedDialogsOrderUpdated.fire({});\n}\n\nrpl::producer<> Session::pinnedDialogsOrderUpdated() const {\n\treturn _pinnedDialogsOrderUpdated.events();\n}\n\nvoid Session::nextForUpgradeGiftInvalidate(not_null<PeerData*> owner) {\n\t_nextForUpgradeGifts.remove(owner);\n}\n\nvoid Session::nextForUpgradeGiftRequest(\n\t\tnot_null<PeerData*> owner,\n\t\tFn<void(std::optional<Data::SavedStarGift>)> done) {\n\tauto &entry = _nextForUpgradeGifts[owner];\n\tif (entry.requestId) {\n\t\tentry.done = std::move(done);\n\t\treturn;\n\t} else if (crl::now() - entry.received < kNextForUpgradeGiftTimeout) {\n\t\tdone(entry.gift);\n\t\treturn;\n\t}\n\tentry.done = std::move(done);\n\n\tconst auto finishWith = [=](std::optional<Data::SavedStarGift> gift) {\n\t\tauto &entry = _nextForUpgradeGifts[owner];\n\t\tentry.requestId = 0;\n\t\tentry.gift = std::move(gift);\n\t\tentry.received = crl::now();\n\t\tbase::take(entry.done)(entry.gift);\n\t};\n\tusing Flag = MTPpayments_GetSavedStarGifts::Flag;\n\tentry.requestId = _session->api().request(\n\t\tMTPpayments_GetSavedStarGifts(\n\t\t\tMTP_flags(Flag::f_exclude_unique\n\t\t\t\t| Flag::f_exclude_unlimited\n\t\t\t\t| Flag::f_exclude_unupgradable),\n\t\t\towner->input(),\n\t\t\tMTPint(), // collection_id\n\t\t\tMTP_string(), // offset\n\t\t\tMTP_int(1)) // limit\n\t).done([=](const MTPpayments_SavedStarGifts &result) {\n\t\tconst auto &data = result.data();\n\t\tprocessUsers(data.vusers());\n\t\tprocessChats(data.vchats());\n\t\tconst auto &list = data.vgifts().v;\n\t\tif (list.empty()) {\n\t\t\tfinishWith(std::nullopt);\n\t\t} else if (auto parsed = Api::FromTL(owner, list[0])) {\n\t\t\tfinishWith(std::move(*parsed));\n\t\t} else {\n\t\t\tfinishWith(std::nullopt);\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tfinishWith(std::nullopt);\n\t}).send();\n}\n\nSession::CreditsSubsRebuilderPtr Session::createCreditsSubsRebuilder() {\n\tif (auto result = activeCreditsSubsRebuilder()) {\n\t\treturn result;\n\t}\n\tauto result = std::make_shared<CreditsSubsRebuilder>();\n\t_creditsSubsRebuilder = result;\n\treturn result;\n}\n\nSession::CreditsSubsRebuilderPtr Session::activeCreditsSubsRebuilder() const {\n\treturn _creditsSubsRebuilder.lock();\n}\n\nvoid Session::registerHeavyViewPart(not_null<ViewElement*> view) {\n\t_heavyViewParts.emplace(view);\n}\n\nvoid Session::unregisterHeavyViewPart(not_null<ViewElement*> view) {\n\t_heavyViewParts.remove(view);\n}\n\nvoid Session::unloadHeavyViewParts(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate) {\n\tif (_heavyViewParts.empty()) {\n\t\treturn;\n\t}\n\tconst auto remove = ranges::count(\n\t\t_heavyViewParts,\n\t\tdelegate,\n\t\t[](not_null<ViewElement*> element) { return element->delegate(); });\n\tif (remove == _heavyViewParts.size()) {\n\t\tfor (const auto &view : base::take(_heavyViewParts)) {\n\t\t\tview->unloadHeavyPart();\n\t\t}\n\t} else {\n\t\tauto remove = std::vector<not_null<ViewElement*>>();\n\t\tfor (const auto &view : _heavyViewParts) {\n\t\t\tif (view->delegate() == delegate) {\n\t\t\t\tremove.push_back(view);\n\t\t\t}\n\t\t}\n\t\tfor (const auto &view : remove) {\n\t\t\tview->unloadHeavyPart();\n\t\t}\n\t}\n}\n\nvoid Session::unloadHeavyViewParts(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate,\n\t\tint from,\n\t\tint till) {\n\tif (_heavyViewParts.empty()) {\n\t\treturn;\n\t}\n\tauto remove = std::vector<not_null<ViewElement*>>();\n\tfor (const auto &view : _heavyViewParts) {\n\t\tif (view->delegate() == delegate\n\t\t\t&& !delegate->elementIntersectsRange(view, from, till)) {\n\t\t\tremove.push_back(view);\n\t\t}\n\t}\n\tfor (const auto &view : remove) {\n\t\tview->unloadHeavyPart();\n\t}\n}\n\nvoid Session::registerShownSpoiler(not_null<ViewElement*> view) {\n\t_shownSpoilers.emplace(view);\n}\n\nvoid Session::hideShownSpoilers() {\n\tfor (const auto &view : base::take(_shownSpoilers)) {\n\t\tview->hideSpoilers();\n\t\trequestViewRepaint(view);\n\t}\n}\n\nvoid Session::removeMegagroupParticipant(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<UserData*> user) {\n\t_megagroupParticipantRemoved.fire({ channel, user });\n}\n\nauto Session::megagroupParticipantRemoved() const\n-> rpl::producer<MegagroupParticipant> {\n\treturn _megagroupParticipantRemoved.events();\n}\n\nrpl::producer<not_null<UserData*>> Session::megagroupParticipantRemoved(\n\t\tnot_null<ChannelData*> channel) const {\n\treturn megagroupParticipantRemoved(\n\t) | rpl::filter([channel](auto updateChannel, auto user) {\n\t\treturn (updateChannel == channel);\n\t}) | rpl::map([](auto updateChannel, auto user) {\n\t\treturn user;\n\t});\n}\n\nvoid Session::addNewMegagroupParticipant(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<UserData*> user) {\n\t_megagroupParticipantAdded.fire({ channel, user });\n}\n\nauto Session::megagroupParticipantAdded() const\n-> rpl::producer<MegagroupParticipant> {\n\treturn _megagroupParticipantAdded.events();\n}\n\nrpl::producer<not_null<UserData*>> Session::megagroupParticipantAdded(\n\t\tnot_null<ChannelData*> channel) const {\n\treturn megagroupParticipantAdded(\n\t) | rpl::filter([channel](auto updateChannel, auto user) {\n\t\treturn (updateChannel == channel);\n\t}) | rpl::map([](auto updateChannel, auto user) {\n\t\treturn user;\n\t});\n}\n\nHistoryItemsList Session::idsToItems(\n\t\tconst MessageIdsList &ids) const {\n\treturn ranges::views::all(\n\t\tids\n\t) | ranges::views::transform([&](const FullMsgId &fullId) {\n\t\treturn message(fullId);\n\t}) | ranges::views::filter([](HistoryItem *item) {\n\t\treturn item != nullptr;\n\t}) | ranges::views::transform([](HistoryItem *item) {\n\t\treturn not_null<HistoryItem*>(item);\n\t}) | ranges::to_vector;\n}\n\nMessageIdsList Session::itemsToIds(\n\t\tconst HistoryItemsList &items) const {\n\treturn ranges::views::all(\n\t\titems\n\t) | ranges::views::transform([](not_null<HistoryItem*> item) {\n\t\treturn item->fullId();\n\t}) | ranges::to_vector;\n}\n\nMessageIdsList Session::itemOrItsGroup(not_null<HistoryItem*> item) const {\n\tif (const auto group = groups().find(item)) {\n\t\treturn itemsToIds(group->items);\n\t}\n\treturn { 1, item->fullId() };\n}\n\nvoid Session::setChatPinned(\n\t\tDialogs::Key key,\n\t\tFilterId filterId,\n\t\tbool pinned) {\n\tExpects(key.entry()->folderKnown());\n\n\tconst auto list = (filterId\n\t\t? chatsFilters().chatsList(filterId)\n\t\t: chatsListFor(key.entry()))->pinned();\n\tlist->setPinned(key, pinned);\n\tnotifyPinnedDialogsOrderUpdated();\n}\n\nvoid Session::setPinnedFromEntryList(Dialogs::Key key, bool pinned) {\n\tExpects(key.entry()->folderKnown());\n\n\tconst auto list = chatsListFor(key.entry())->pinned();\n\tif (pinned) {\n\t\tlist->addPinned(key);\n\t} else {\n\t\tlist->setPinned(key, false);\n\t}\n}\n\nvoid Session::applyPinnedChats(\n\t\tData::Folder *folder,\n\t\tconst QVector<MTPDialogPeer> &list) {\n\tfor (const auto &peer : list) {\n\t\tpeer.match([&](const MTPDdialogPeer &data) {\n\t\t\tconst auto history = this->history(peerFromMTP(data.vpeer()));\n\t\t\tif (folder) {\n\t\t\t\thistory->setFolder(folder);\n\t\t\t} else {\n\t\t\t\thistory->clearFolder();\n\t\t\t}\n\t\t}, [&](const MTPDdialogPeerFolder &data) {\n\t\t\tif (folder) {\n\t\t\t\tLOG((\"API Error: Nested folders detected.\"));\n\t\t\t}\n\t\t});\n\t}\n\tchatsList(folder)->pinned()->applyList(this, list);\n\tnotifyPinnedDialogsOrderUpdated();\n}\n\nvoid Session::applyPinnedTopics(\n\t\tnot_null<Data::Forum*> forum,\n\t\tconst QVector<MTPint> &list) {\n\tforum->topicsList()->pinned()->applyList(forum, list);\n\tnotifyPinnedDialogsOrderUpdated();\n}\n\nvoid Session::applyDialogs(\n\t\tData::Folder *requestFolder,\n\t\tconst QVector<MTPMessage> &messages,\n\t\tconst QVector<MTPDialog> &dialogs,\n\t\tstd::optional<int> count) {\n\tprocessMessages(messages, NewMessageType::Last);\n\tfor (const auto &dialog : dialogs) {\n\t\tdialog.match([&](const auto &data) {\n\t\t\tapplyDialog(requestFolder, data);\n\t\t});\n\t}\n\tif (requestFolder && count) {\n\t\trequestFolder->chatsList()->setCloudListSize(*count);\n\t}\n}\n\nvoid Session::applyDialog(\n\t\tData::Folder *requestFolder,\n\t\tconst MTPDdialog &data) {\n\tconst auto peerId = peerFromMTP(data.vpeer());\n\tif (!peerId) {\n\t\treturn;\n\t}\n\n\tconst auto history = this->history(peerId);\n\thistory->applyDialog(requestFolder, data);\n\tsetPinnedFromEntryList(history, data.is_pinned());\n\n\tif (const auto from = history->peer->migrateFrom()) {\n\t\tif (const auto historyFrom = historyLoaded(from)) {\n\t\t\tremoveChatListEntry(historyFrom);\n\t\t}\n\t} else if (const auto to = history->peer->migrateTo()) {\n\t\tif (to->amIn()) {\n\t\t\tremoveChatListEntry(history);\n\t\t}\n\t}\n}\n\nvoid Session::applyDialog(\n\t\tData::Folder *requestFolder,\n\t\tconst MTPDdialogFolder &data) {\n\tif (requestFolder) {\n\t\tLOG((\"API Error: requestFolder != nullptr for dialogFolder.\"));\n\t}\n\tconst auto folder = processFolder(data.vfolder());\n\tfolder->applyDialog(data);\n\tsetPinnedFromEntryList(folder, data.is_pinned());\n}\n\nbool Session::pinnedCanPin(not_null<Dialogs::Entry*> entry) const {\n\tif ([[maybe_unused]] const auto sublist = entry->asSublist()) {\n\t\tif (sublist->parentChat()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto saved = &savedMessages();\n\t\treturn pinnedChatsOrder(saved).size() < pinnedChatsLimit(saved);\n\t} else if (const auto topic = entry->asTopic()) {\n\t\tconst auto forum = topic->forum();\n\t\treturn pinnedChatsOrder(forum).size() < pinnedChatsLimit(forum);\n\t} else {\n\t\tconst auto folder = entry->folder();\n\t\treturn pinnedChatsOrder(folder).size() < pinnedChatsLimit(folder);\n\t}\n}\n\nbool Session::pinnedCanPin(\n\t\tFilterId filterId,\n\t\tnot_null<History*> history) const {\n\tExpects(filterId != 0);\n\n\tconst auto &list = chatsFilters().list();\n\tconst auto i = ranges::find(list, filterId, &Data::ChatFilter::id);\n\treturn (i == end(list))\n\t\t|| (i->always().contains(history))\n\t\t|| (i->always().size() < pinnedChatsLimit(filterId));\n}\n\nint Session::pinnedChatsLimit(Data::Folder *folder) const {\n\tconst auto limits = Data::PremiumLimits(_session);\n\treturn folder\n\t\t? limits.dialogsFolderPinnedCurrent()\n\t\t: limits.dialogsPinnedCurrent();\n}\n\nint Session::pinnedChatsLimit(FilterId filterId) const {\n\tconst auto limits = Data::PremiumLimits(_session);\n\treturn limits.dialogFiltersChatsCurrent();\n}\n\nint Session::pinnedChatsLimit(not_null<Data::Forum*> forum) const {\n\tconst auto limits = Data::PremiumLimits(_session);\n\treturn limits.topicsPinnedCurrent();\n}\n\nint Session::pinnedChatsLimit(not_null<Data::SavedMessages*> saved) const {\n\tif (saved->parentChat()) {\n\t\treturn 0;\n\t}\n\tconst auto limits = Data::PremiumLimits(_session);\n\treturn limits.savedSublistsPinnedCurrent();\n}\n\nrpl::producer<int> Session::maxPinnedChatsLimitValue(\n\t\tData::Folder *folder) const {\n\t// Premium limit from appconfig.\n\t// We always use premium limit in the MainList limit producer,\n\t// because it slices the list to that limit. We don't want to slice\n\t// premium-ly added chats from the pinned list because of sync issues.\n\treturn _session->appConfig().value(\n\t) | rpl::map([folder, limits = Data::PremiumLimits(_session)] {\n\t\treturn folder\n\t\t\t? limits.dialogsFolderPinnedPremium()\n\t\t\t: limits.dialogsPinnedPremium();\n\t});\n}\n\nrpl::producer<int> Session::maxPinnedChatsLimitValue(\n\t\tFilterId filterId) const {\n\t// Premium limit from appconfig.\n\t// We always use premium limit in the MainList limit producer,\n\t// because it slices the list to that limit. We don't want to slice\n\t// premium-ly added chats from the pinned list because of sync issues.\n\treturn _session->appConfig().value(\n\t) | rpl::map([limits = Data::PremiumLimits(_session)] {\n\t\treturn limits.dialogFiltersChatsPremium();\n\t});\n}\n\nrpl::producer<int> Session::maxPinnedChatsLimitValue(\n\t\tnot_null<Data::Forum*> forum) const {\n\treturn _session->appConfig().value(\n\t) | rpl::map([limits = Data::PremiumLimits(_session)] {\n\t\treturn limits.topicsPinnedCurrent();\n\t});\n}\n\nrpl::producer<int> Session::maxPinnedChatsLimitValue(\n\t\tnot_null<SavedMessages*> saved) const {\n\tif (saved->parentChat()) {\n\t\treturn rpl::single(0);\n\t}\n\t// Premium limit from appconfig.\n\t// We always use premium limit in the MainList limit producer,\n\t// because it slices the list to that limit. We don't want to slice\n\t// premium-ly added chats from the pinned list because of sync issues.\n\treturn _session->appConfig().value(\n\t) | rpl::map([limits = Data::PremiumLimits(_session)] {\n\t\treturn limits.savedSublistsPinnedPremium();\n\t});\n}\n\nint Session::groupFreeTranscribeLevel() const {\n\treturn _groupFreeTranscribeLevel.current();\n}\n\nconst std::vector<Dialogs::Key> &Session::pinnedChatsOrder(\n\t\tData::Folder *folder) const {\n\treturn chatsList(folder)->pinned()->order();\n}\n\nconst std::vector<Dialogs::Key> &Session::pinnedChatsOrder(\n\t\tFilterId filterId) const {\n\treturn chatsFilters().chatsList(filterId)->pinned()->order();\n}\n\nconst std::vector<Dialogs::Key> &Session::pinnedChatsOrder(\n\t\tnot_null<Data::Forum*> forum) const {\n\treturn forum->topicsList()->pinned()->order();\n}\n\nconst std::vector<Dialogs::Key> &Session::pinnedChatsOrder(\n\t\tnot_null<Data::SavedMessages*> saved) const {\n\treturn saved->chatsList()->pinned()->order();\n}\n\nvoid Session::clearPinnedChats(Data::Folder *folder) {\n\tchatsList(folder)->pinned()->clear();\n}\n\nvoid Session::reorderTwoPinnedChats(\n\t\tFilterId filterId,\n\t\tDialogs::Key key1,\n\t\tDialogs::Key key2) {\n\tExpects(key1.entry()->folderKnown() && key2.entry()->folderKnown());\n\tExpects(filterId || (key1.entry()->folder() == key2.entry()->folder()));\n\n\tconst auto topic = key1.topic();\n\tconst auto list = topic\n\t\t? topic->forum()->topicsList()\n\t\t: filterId\n\t\t? chatsFilters().chatsList(filterId)\n\t\t: chatsListFor(key1.entry());\n\tlist->pinned()->reorder(key1, key2);\n\tnotifyPinnedDialogsOrderUpdated();\n}\n\nbool Session::updateExistingMessage(const MTPDmessage &data) {\n\tconst auto peer = peerFromMTP(data.vpeer_id());\n\tconst auto existing = message(peer, data.vid().v);\n\tif (!existing) {\n\t\treturn false;\n\t}\n\texisting->applySentMessage(data);\n\tconst auto result = (existing->mainView() != nullptr);\n\tif (result) {\n\t\tstickers().checkSavedGif(existing);\n\t}\n\tsession().changes().messageUpdated(\n\t\texisting,\n\t\tData::MessageUpdate::Flag::NewMaybeAdded);\n\treturn result;\n}\n\nvoid Session::updateEditedMessage(const MTPMessage &data) {\n\tconst auto existing = data.match([](const MTPDmessageEmpty &)\n\t\t\t-> HistoryItem* {\n\t\treturn nullptr;\n\t}, [&](const auto &data) {\n\t\treturn message(peerFromMTP(data.vpeer_id()), data.vid().v);\n\t});\n\tif (!existing) {\n\t\tReactions::CheckUnknownForUnread(this, data);\n\t\treturn;\n\t}\n\tif (existing->isLocalUpdateMedia() && data.type() == mtpc_message) {\n\t\tupdateExistingMessage(data.c_message());\n\t}\n\tdata.match([](const MTPDmessageEmpty &) {\n\t}, [&](const MTPDmessageService &data) {\n\t\texisting->applyEdition(data);\n\t}, [&](const auto &data) {\n\t\texisting->applyEdition(HistoryMessageEdition(_session, data));\n\t});\n}\n\nvoid Session::processMessages(\n\t\tconst QVector<MTPMessage> &data,\n\t\tNewMessageType type) {\n\tauto indices = base::flat_map<uint64, int>();\n\tfor (int i = 0, l = data.size(); i != l; ++i) {\n\t\tconst auto &message = data[i];\n\t\tif (message.type() == mtpc_message) {\n\t\t\tconst auto &data = message.c_message();\n\t\t\t// new message, index my forwarded messages to links overview\n\t\t\tif ((type == NewMessageType::Unread)\n\t\t\t\t&& updateExistingMessage(data)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tconst auto id = IdFromMessage(message); // Only 32 bit values here.\n\t\tindices.emplace((uint64(uint32(id.bare)) << 32) | uint64(i), i);\n\t}\n\tfor (const auto &[position, index] : indices) {\n\t\taddNewMessage(\n\t\t\tdata[index],\n\t\t\tMessageFlags(),\n\t\t\ttype);\n\t}\n}\n\nvoid Session::processMessages(\n\t\tconst MTPVector<MTPMessage> &data,\n\t\tNewMessageType type) {\n\tprocessMessages(data.v, type);\n}\n\nvoid Session::processExistingMessages(\n\t\tChannelData *channel,\n\t\tconst MTPmessages_Messages &data) {\n\tdata.match([&](const MTPDmessages_messagesNotModified&) {\n\t\tLOG((\"API Error: received messages.messagesNotModified!\"));\n\t}, [&](const auto &data) {\n\t\tprocessUsers(data.vusers());\n\t\tprocessChats(data.vchats());\n\t\tprocessMessages(data.vmessages(), NewMessageType::Existing);\n\t});\n\tdata.match([&](const MTPDmessages_channelMessages &data) {\n\t\tif (channel) {\n\t\t\tchannel->ptsReceived(data.vpts().v);\n\t\t} else {\n\t\t\tLOG((\"App Error: received messages.channelMessages!\"));\n\t\t}\n\t}, [](const auto &) {});\n\tdata.match([&](const MTPDmessages_messagesNotModified&) {\n\t}, [&](const auto &data) {\n\t\tif (channel) {\n\t\t\tchannel->processTopics(data.vtopics());\n\t\t}\n\t});\n}\n\nconst Session::Messages *Session::messagesList(PeerId peerId) const {\n\tconst auto i = _messages.find(peerId);\n\treturn (i != end(_messages)) ? &i->second : nullptr;\n}\n\nauto Session::messagesListForInsert(PeerId peerId)\n-> not_null<Messages*> {\n\treturn &_messages[peerId];\n}\n\nvoid Session::registerMessage(not_null<HistoryItem*> item) {\n\tconst auto peerId = item->history()->peer->id;\n\tconst auto list = messagesListForInsert(peerId);\n\tconst auto itemId = item->id;\n\tconst auto i = list->find(itemId);\n\tif (i != list->end()) {\n\t\tLOG((\"App Error: Trying to re-registerMessage().\"));\n\t\ti->second->destroy();\n\t}\n\tlist->emplace(itemId, item);\n\n\tif (!peerIsChannel(peerId) && IsServerMsgId(itemId)) {\n\t\t_nonChannelMessages.emplace(itemId, item);\n\t}\n}\n\nvoid Session::registerMessageTTL(TimeId when, not_null<HistoryItem*> item) {\n\tExpects(when > 0);\n\n\tauto &list = _ttlMessages[when];\n\tlist.emplace(item);\n\n\tconst auto nearest = _ttlMessages.begin()->first;\n\tif (nearest < when && _ttlCheckTimer.isActive()) {\n\t\treturn;\n\t}\n\tscheduleNextTTLs();\n}\n\nvoid Session::scheduleNextTTLs() {\n\tif (_ttlMessages.empty()) {\n\t\treturn;\n\t}\n\tconst auto nearest = _ttlMessages.begin()->first;\n\tconst auto now = base::unixtime::now();\n\n\t// Set timer not more than for 24 hours.\n\tconst auto maxTimeout = TimeId(86400);\n\tconst auto timeout = std::min(std::max(now, nearest) - now, maxTimeout);\n\t_ttlCheckTimer.callOnce(timeout * crl::time(1000));\n}\n\nvoid Session::unregisterMessageTTL(\n\t\tTimeId when,\n\t\tnot_null<HistoryItem*> item) {\n\tExpects(when > 0);\n\n\tconst auto i = _ttlMessages.find(when);\n\tif (i == end(_ttlMessages)) {\n\t\treturn;\n\t}\n\tauto &list = i->second;\n\tlist.erase(item);\n\tif (list.empty()) {\n\t\t_ttlMessages.erase(i);\n\t}\n}\n\nvoid Session::checkTTLs() {\n\t_ttlCheckTimer.cancel();\n\tconst auto now = base::unixtime::now();\n\twhile (!_ttlMessages.empty() && _ttlMessages.begin()->first <= now) {\n\t\t_ttlMessages.begin()->second.front()->destroy();\n\t}\n\tscheduleNextTTLs();\n}\n\nvoid Session::registerFormattedDateUpdate(\n\t\tTimeId when,\n\t\tnot_null<HistoryView::Element*> view) {\n\t_formattedDateUpdates[when].push_back(\n\t\tbase::make_weak(view.get()));\n\tconst auto nearest = _formattedDateUpdates.begin()->first;\n\tif (nearest < when && _formattedDateTimer.isActive()) {\n\t\treturn;\n\t}\n\tscheduleNextFormattedDateUpdate();\n}\n\nvoid Session::scheduleNextFormattedDateUpdate() {\n\tif (_formattedDateUpdates.empty()) {\n\t\treturn;\n\t}\n\tconst auto nearest = _formattedDateUpdates.begin()->first;\n\tconst auto now = base::unixtime::now();\n\tconst auto maxTimeout = TimeId(86400);\n\tconst auto timeout = std::min(\n\t\tstd::max(now, nearest) - now,\n\t\tmaxTimeout);\n\t_formattedDateTimer.callOnce(timeout * crl::time(1000));\n}\n\nvoid Session::checkFormattedDateUpdates() {\n\t_formattedDateTimer.cancel();\n\tconst auto now = base::unixtime::now();\n\tauto expired = std::vector<base::weak_ptr<HistoryView::Element>>();\n\twhile (!_formattedDateUpdates.empty()\n\t\t&& _formattedDateUpdates.begin()->first <= now) {\n\t\tauto &list = _formattedDateUpdates.begin()->second;\n\t\texpired.insert(expired.end(), list.begin(), list.end());\n\t\t_formattedDateUpdates.erase(_formattedDateUpdates.begin());\n\t}\n\tfor (const auto &weak : expired) {\n\t\tif (const auto strong = weak.get()) {\n\t\t\trequestItemTextRefresh(strong->data());\n\t\t}\n\t}\n\tscheduleNextFormattedDateUpdate();\n}\n\nvoid Session::processMessagesDeleted(\n\t\tPeerId peerId,\n\t\tconst QVector<MTPint> &data) {\n\tconst auto list = messagesList(peerId);\n\tconst auto affected = historyLoaded(peerId);\n\tif (!list && !affected) {\n\t\treturn;\n\t}\n\n\tauto historiesToCheck = base::flat_set<not_null<History*>>();\n\tfor (const auto &messageId : data) {\n\t\tconst auto i = list ? list->find(messageId.v) : Messages::iterator();\n\t\tif (list && i != list->end()) {\n\t\t\tconst auto history = i->second->history();\n\t\t\ti->second->destroy();\n\t\t\tif (!history->chatListMessageKnown()) {\n\t\t\t\thistoriesToCheck.emplace(history);\n\t\t\t}\n\t\t} else if (affected) {\n\t\t\taffected->unknownMessageDeleted(messageId.v);\n\t\t}\n\t}\n\tfor (const auto &history : historiesToCheck) {\n\t\thistory->requestChatListMessage();\n\t}\n}\n\nvoid Session::processNonChannelMessagesDeleted(const QVector<MTPint> &data) {\n\tauto historiesToCheck = base::flat_set<not_null<History*>>();\n\tfor (const auto &messageId : data) {\n\t\tif (const auto item = nonChannelMessage(messageId.v)) {\n\t\t\tconst auto history = item->history();\n\t\t\titem->destroy();\n\t\t\tif (!history->chatListMessageKnown()) {\n\t\t\t\thistoriesToCheck.emplace(history);\n\t\t\t}\n\t\t}\n\t}\n\tfor (const auto &history : historiesToCheck) {\n\t\thistory->requestChatListMessage();\n\t}\n}\n\nvoid Session::removeDependencyMessage(not_null<HistoryItem*> item) {\n\tconst auto i = _dependentMessages.find(item);\n\tif (i != end(_dependentMessages)) {\n\t\tconst auto items = std::move(i->second);\n\t\t_dependentMessages.erase(i);\n\n\t\tfor (const auto &dependent : items) {\n\t\t\tdependent->dependencyItemRemoved(item);\n\t\t}\n\t}\n\tif (item->groupId()) {\n\t\tif (const auto group = groups().find(item)) {\n\t\t\tfor (const auto &groupedItem : group->items) {\n\t\t\t\tupdateDependentMessages(groupedItem);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Session::unregisterMessage(not_null<HistoryItem*> item) {\n\tconst auto peerId = item->history()->peer->id;\n\tconst auto itemId = item->id;\n\t_itemRemoved.fire_copy(item);\n\tif (item->hasPossibleRestrictions()) {\n\t\t_possiblyRestricted.remove(item);\n\t}\n\tsession().changes().messageUpdated(\n\t\titem,\n\t\tData::MessageUpdate::Flag::Destroyed);\n\tgroups().unregisterMessage(item);\n\tremoveDependencyMessage(item);\n\tfor (auto i = begin(_highlightings); i != end(_highlightings);) {\n\t\tif (i->second == item) {\n\t\t\ti = _highlightings.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tmessagesListForInsert(peerId)->erase(itemId);\n\n\tif (!peerIsChannel(peerId) && IsServerMsgId(itemId)) {\n\t\t_nonChannelMessages.erase(itemId);\n\t}\n}\n\nMsgId Session::nextLocalMessageId() {\n\tExpects(_localMessageIdCounter < EndClientMsgId);\n\n\treturn _localMessageIdCounter++;\n}\n\nvoid Session::setSuggestToGigagroup(\n\t\tnot_null<ChannelData*> group,\n\t\tbool suggest) {\n\tif (suggest) {\n\t\t_suggestToGigagroup.emplace(group);\n\t} else {\n\t\t_suggestToGigagroup.remove(group);\n\t}\n}\n\nbool Session::suggestToGigagroup(not_null<ChannelData*> group) const {\n\treturn _suggestToGigagroup.contains(group);\n}\n\nHistoryItem *Session::message(PeerId peerId, MsgId itemId) const {\n\tif (!itemId) {\n\t\treturn nullptr;\n\t}\n\n\tconst auto data = messagesList(peerId);\n\tif (!data) {\n\t\treturn nullptr;\n\t}\n\n\tconst auto i = data->find(itemId);\n\treturn (i != data->end()) ? i->second.get() : nullptr;\n}\n\nHistoryItem *Session::message(\n\t\tnot_null<const PeerData*> peer,\n\t\tMsgId itemId) const {\n\treturn message(peer->id, itemId);\n}\n\nHistoryItem *Session::message(FullMsgId itemId) const {\n\treturn message(itemId.peer, itemId.msg);\n}\n\nHistoryItem *Session::nonChannelMessage(MsgId itemId) const {\n\tif (!IsServerMsgId(itemId)) {\n\t\treturn nullptr;\n\t}\n\tconst auto i = _nonChannelMessages.find(itemId);\n\treturn (i != end(_nonChannelMessages)) ? i->second.get() : nullptr;\n}\n\nvoid Session::updateDependentMessages(not_null<HistoryItem*> item) {\n\tconst auto i = _dependentMessages.find(item);\n\tif (i != end(_dependentMessages)) {\n\t\tfor (const auto &dependent : i->second) {\n\t\t\tdependent->updateDependencyItem();\n\t\t}\n\t}\n\tsession().changes().messageUpdated(\n\t\titem,\n\t\tData::MessageUpdate::Flag::Edited);\n}\n\nvoid Session::registerDependentMessage(\n\t\tnot_null<HistoryItem*> dependent,\n\t\tnot_null<HistoryItem*> dependency) {\n\t_dependentMessages[dependency].emplace(dependent);\n}\n\nvoid Session::unregisterDependentMessage(\n\t\tnot_null<HistoryItem*> dependent,\n\t\tnot_null<HistoryItem*> dependency) {\n\tconst auto i = _dependentMessages.find(dependency);\n\tif (i != end(_dependentMessages)) {\n\t\tif (i->second.remove(dependent) && i->second.empty()) {\n\t\t\t_dependentMessages.erase(i);\n\t\t}\n\t}\n}\n\nvoid Session::registerMessageRandomId(uint64 randomId, FullMsgId itemId) {\n\t_messageByRandomId.emplace(randomId, itemId);\n}\n\nvoid Session::unregisterMessageRandomId(uint64 randomId) {\n\t_messageByRandomId.remove(randomId);\n}\n\nFullMsgId Session::messageIdByRandomId(uint64 randomId) const {\n\tconst auto i = _messageByRandomId.find(randomId);\n\treturn (i != end(_messageByRandomId)) ? i->second : FullMsgId();\n}\n\nvoid Session::registerMessageSentData(\n\t\tuint64 randomId,\n\t\tPeerId peerId,\n\t\tconst QString &text) {\n\t_sentMessagesData.emplace(randomId, SentData{ peerId, text });\n}\n\nvoid Session::unregisterMessageSentData(uint64 randomId) {\n\t_sentMessagesData.remove(randomId);\n}\n\nSession::SentData Session::messageSentData(uint64 randomId) const {\n\tconst auto i = _sentMessagesData.find(randomId);\n\treturn (i != end(_sentMessagesData)) ? i->second : SentData();\n}\n\nHistoryItem *Session::addNewMessage(\n\t\tconst MTPMessage &data,\n\t\tMessageFlags localFlags,\n\t\tNewMessageType type) {\n\treturn addNewMessage(IdFromMessage(data), data, localFlags, type);\n}\n\nHistoryItem *Session::addNewMessage(\n\t\tMsgId id,\n\t\tconst MTPMessage &data,\n\t\tMessageFlags localFlags,\n\t\tNewMessageType type) {\n\tconst auto peerId = PeerFromMessage(data);\n\tif (!peerId || data.type() == mtpc_messageEmpty) {\n\t\treturn nullptr;\n\t}\n\n\tif (data.type() == mtpc_message) {\n\t\tif (const auto h = historyLoaded(peerId)) {\n\t\t\tif (const auto streamed = h->streamedDraftsIfExists()) {\n\t\t\t\tif (const auto adopted = streamed->adoptIncoming(\n\t\t\t\t\t\tdata.c_message())) {\n\t\t\t\t\tif (type == NewMessageType::Unread) {\n\t\t\t\t\t\tCheckForSwitchInlineButton(adopted);\n\t\t\t\t\t}\n\t\t\t\t\treturn adopted;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tconst auto result = history(peerId)->addNewMessage(\n\t\tid,\n\t\tdata,\n\t\tlocalFlags,\n\t\ttype);\n\tif (type == NewMessageType::Unread) {\n\t\tCheckForSwitchInlineButton(result);\n\t}\n\treturn result;\n}\n\nint Session::unreadBadge() const {\n\treturn computeUnreadBadge(_chatsList.unreadState());\n}\n\nint Session::unreadWithMentionsBadge() const {\n\tauto state = _chatsList.unreadState();\n\tif (state.mentions) {\n\t\tstate.messages -= state.mentions;\n\t}\n\treturn computeUnreadBadge(state) + state.mentions;\n}\n\nbool Session::unreadBadgeMuted() const {\n\treturn computeUnreadBadgeMuted(_chatsList.unreadState());\n}\n\nbool Session::unreadWithMentionsBadgeMuted() const {\n\tconst auto state = _chatsList.unreadState();\n\treturn !state.mentions && computeUnreadBadgeMuted(state);\n}\n\nint Session::unreadBadgeIgnoreOne(Dialogs::Key key) const {\n\tconst auto remove = (key && key.entry()->inChatList())\n\t\t? key.entry()->chatListUnreadState()\n\t\t: Dialogs::UnreadState();\n\treturn computeUnreadBadge(_chatsList.unreadState() - remove);\n}\n\nbool Session::unreadBadgeMutedIgnoreOne(Dialogs::Key key) const {\n\tif (!Core::App().settings().includeMutedCounter()) {\n\t\treturn false;\n\t}\n\tconst auto remove = (key && key.entry()->inChatList())\n\t\t? key.entry()->chatListUnreadState()\n\t\t: Dialogs::UnreadState();\n\treturn computeUnreadBadgeMuted(_chatsList.unreadState() - remove);\n}\n\nint Session::unreadOnlyMutedBadge() const {\n\tconst auto state = _chatsList.unreadState();\n\treturn Core::App().settings().countUnreadMessages()\n\t\t? state.messagesMuted\n\t\t: state.chatsMuted;\n}\n\nrpl::producer<> Session::unreadBadgeChanges() const {\n\treturn _unreadBadgeChanges.events();\n}\n\nvoid Session::notifyUnreadBadgeChanged() {\n\t_unreadBadgeChanges.fire({});\n}\n\nvoid Session::updateRepliesReadTill(RepliesReadTillUpdate update) {\n\t_repliesReadTillUpdates.fire(std::move(update));\n}\n\nauto Session::repliesReadTillUpdates() const\n-> rpl::producer<RepliesReadTillUpdate> {\n\treturn _repliesReadTillUpdates.events();\n}\n\nvoid Session::updateSublistReadTill(SublistReadTillUpdate update) {\n\t_sublistReadTillUpdates.fire(std::move(update));\n}\n\nauto Session::sublistReadTillUpdates() const\n-> rpl::producer<SublistReadTillUpdate> {\n\treturn _sublistReadTillUpdates.events();\n}\n\nint Session::computeUnreadBadge(const Dialogs::UnreadState &state) const {\n\tconst auto all = Core::App().settings().includeMutedCounter();\n\treturn std::max(state.marks - (all ? 0 : state.marksMuted), 0)\n\t\t+ (Core::App().settings().countUnreadMessages()\n\t\t\t? std::max(state.messages - (all ? 0 : state.messagesMuted), 0)\n\t\t\t: std::max(state.chats - (all ? 0 : state.chatsMuted), 0));\n}\n\nbool Session::computeUnreadBadgeMuted(\n\t\tconst Dialogs::UnreadState &state) const {\n\tif (!Core::App().settings().includeMutedCounter()) {\n\t\treturn false;\n\t}\n\treturn (state.marksMuted >= state.marks)\n\t\t&& (Core::App().settings().countUnreadMessages()\n\t\t\t? (state.messagesMuted >= state.messages)\n\t\t\t: (state.chatsMuted >= state.chats));\n}\n\nvoid Session::selfDestructIn(not_null<HistoryItem*> item, crl::time delay) {\n\t_selfDestructItems.push_back(item->fullId());\n\tif (!_selfDestructTimer.isActive()\n\t\t|| _selfDestructTimer.remainingTime() > delay) {\n\t\t_selfDestructTimer.callOnce(delay);\n\t}\n}\n\nvoid Session::checkSelfDestructItems() {\n\tconst auto now = crl::now();\n\tauto nextDestructIn = crl::time(0);\n\tfor (auto i = _selfDestructItems.begin(); i != _selfDestructItems.cend();) {\n\t\tif (const auto item = message(*i)) {\n\t\t\tif (const auto destructIn = item->getSelfDestructIn(now)) {\n\t\t\t\tif (nextDestructIn > 0) {\n\t\t\t\t\taccumulate_min(nextDestructIn, destructIn);\n\t\t\t\t} else {\n\t\t\t\t\tnextDestructIn = destructIn;\n\t\t\t\t}\n\t\t\t\t++i;\n\t\t\t} else {\n\t\t\t\ti = _selfDestructItems.erase(i);\n\t\t\t}\n\t\t} else {\n\t\t\ti = _selfDestructItems.erase(i);\n\t\t}\n\t}\n\tif (nextDestructIn > 0) {\n\t\t_selfDestructTimer.callOnce(nextDestructIn);\n\t}\n}\n\nnot_null<PhotoData*> Session::photo(PhotoId id) {\n\tauto i = _photos.find(id);\n\tif (i == _photos.end()) {\n\t\ti = _photos.emplace(\n\t\t\tid,\n\t\t\tstd::make_unique<PhotoData>(this, id)).first;\n\t}\n\treturn i->second.get();\n}\n\nnot_null<PhotoData*> Session::processPhoto(const MTPPhoto &data) {\n\treturn data.match([&](const MTPDphoto &data) {\n\t\treturn processPhoto(data);\n\t}, [&](const MTPDphotoEmpty &data) {\n\t\treturn photo(data.vid().v);\n\t});\n}\n\nnot_null<PhotoData*> Session::processPhoto(const MTPDphoto &data) {\n\tconst auto result = photo(data.vid().v);\n\tphotoApplyFields(result, data);\n\treturn result;\n}\n\nnot_null<PhotoData*> Session::processPhoto(\n\t\tconst MTPPhoto &data,\n\t\tconst PreparedPhotoThumbs &thumbs) {\n\tExpects(!thumbs.empty());\n\n\tconst auto find = [&](const QByteArray &levels) {\n\t\tconst auto kInvalidIndex = int(levels.size());\n\t\tconst auto level = [&](const auto &pair) {\n\t\t\tconst auto letter = pair.first;\n\t\t\tconst auto index = levels.indexOf(letter);\n\t\t\treturn (index >= 0) ? index : kInvalidIndex;\n\t\t};\n\t\tconst auto result = ranges::max_element(\n\t\t\tthumbs,\n\t\t\tstd::greater<>(),\n\t\t\tlevel);\n\t\treturn (level(*result) == kInvalidIndex) ? thumbs.end() : result;\n\t};\n\tconst auto image = [&](const QByteArray &levels) {\n\t\tconst auto i = find(levels);\n\t\treturn (i == thumbs.end())\n\t\t\t? ImageWithLocation()\n\t\t\t: Images::FromImageInMemory(\n\t\t\t\ti->second.image,\n\t\t\t\t\"JPG\",\n\t\t\t\ti->second.bytes);\n\t};\n\tconst auto small = image(SmallLevels);\n\tconst auto thumbnail = image(ThumbnailLevels);\n\tconst auto large = image(LargeLevels);\n\treturn data.match([&](const MTPDphoto &data) {\n\t\treturn photo(\n\t\t\tdata.vid().v,\n\t\t\tdata.vaccess_hash().v,\n\t\t\tdata.vfile_reference().v,\n\t\t\tdata.vdate().v,\n\t\t\tdata.vdc_id().v,\n\t\t\tdata.is_has_stickers(),\n\t\t\tQByteArray(),\n\t\t\tsmall,\n\t\t\tthumbnail,\n\t\t\tlarge,\n\t\t\tImageWithLocation{},\n\t\t\tImageWithLocation{},\n\t\t\tcrl::time(0));\n\t}, [&](const MTPDphotoEmpty &data) {\n\t\treturn photo(data.vid().v);\n\t});\n}\n\nnot_null<PhotoData*> Session::photo(\n\t\tPhotoId id,\n\t\tconst uint64 &access,\n\t\tconst QByteArray &fileReference,\n\t\tTimeId date,\n\t\tint32 dc,\n\t\tbool hasStickers,\n\t\tconst QByteArray &inlineThumbnailBytes,\n\t\tconst ImageWithLocation &small,\n\t\tconst ImageWithLocation &thumbnail,\n\t\tconst ImageWithLocation &large,\n\t\tconst ImageWithLocation &videoSmall,\n\t\tconst ImageWithLocation &videoLarge,\n\t\tcrl::time videoStartTime) {\n\tconst auto result = photo(id);\n\tphotoApplyFields(\n\t\tresult,\n\t\taccess,\n\t\tfileReference,\n\t\tdate,\n\t\tdc,\n\t\thasStickers,\n\t\tinlineThumbnailBytes,\n\t\tsmall,\n\t\tthumbnail,\n\t\tlarge,\n\t\tvideoSmall,\n\t\tvideoLarge,\n\t\tvideoStartTime);\n\treturn result;\n}\n\nvoid Session::photoConvert(\n\t\tnot_null<PhotoData*> original,\n\t\tconst MTPPhoto &data) {\n\tconst auto id = data.match([](const auto &data) {\n\t\treturn data.vid().v;\n\t});\n\tconst auto idChanged = (original->id != id);\n\tif (idChanged) {\n\t\tauto i = _photos.find(id);\n\t\tif (i == _photos.end()) {\n\t\t\tconst auto j = _photos.find(original->id);\n\t\t\tAssert(j != _photos.end());\n\t\t\tauto owned = std::move(j->second);\n\t\t\t_photos.erase(j);\n\t\t\ti = _photos.emplace(id, std::move(owned)).first;\n\t\t}\n\n\t\toriginal->id = id;\n\t\toriginal->uploadingData = nullptr;\n\n\t\tif (i->second.get() != original) {\n\t\t\tphotoApplyFields(i->second.get(), data);\n\t\t}\n\t}\n\tphotoApplyFields(original, data);\n}\n\nPhotoData *Session::photoFromWeb(\n\t\tconst MTPWebDocument &data,\n\t\tconst ImageLocation &thumbnailLocation) {\n\tconst auto large = Images::FromWebDocument(data);\n\tif (!large.valid()) {\n\t\treturn nullptr;\n\t}\n\treturn photo(\n\t\tbase::RandomValue<PhotoId>(),\n\t\tuint64(0),\n\t\tQByteArray(),\n\t\tbase::unixtime::now(),\n\t\t0,\n\t\tfalse,\n\t\tQByteArray(),\n\t\tImageWithLocation{},\n\t\tImageWithLocation{ .location = thumbnailLocation },\n\t\tImageWithLocation{ .location = large },\n\t\tImageWithLocation{},\n\t\tImageWithLocation{},\n\t\tcrl::time(0));\n}\n\nvoid Session::photoApplyFields(\n\t\tnot_null<PhotoData*> photo,\n\t\tconst MTPPhoto &data) {\n\tif (data.type() == mtpc_photo) {\n\t\tphotoApplyFields(photo, data.c_photo());\n\t}\n}\n\nvoid Session::photoApplyFields(\n\t\tnot_null<PhotoData*> photo,\n\t\tconst MTPDphoto &data) {\n\tconst auto &sizes = data.vsizes().v;\n\tconst auto progressive = [&] {\n\t\tconst auto area = [&](const MTPPhotoSize &size) {\n\t\t\treturn size.match([](const MTPDphotoSizeProgressive &data) {\n\t\t\t\treturn data.vw().v * data.vh().v;\n\t\t\t}, [](const auto &) {\n\t\t\t\treturn 0;\n\t\t\t});\n\t\t};\n\t\tconst auto found = ranges::max_element(sizes, std::less<>(), area);\n\t\treturn (found == sizes.end()\n\t\t\t|| found->type() != mtpc_photoSizeProgressive)\n\t\t\t? sizes.end()\n\t\t\t: found;\n\t}();\n\tconst auto find = [&](const QByteArray &levels) {\n\t\tconst auto kInvalidIndex = int(levels.size());\n\t\tconst auto level = [&](const MTPPhotoSize &size) {\n\t\t\tconst auto letter = size.match([](const MTPDphotoSizeEmpty &) {\n\t\t\t\treturn char(0);\n\t\t\t}, [](const auto &size) {\n\t\t\t\treturn size.vtype().v.isEmpty() ? char(0) : size.vtype().v[0];\n\t\t\t});\n\t\t\tconst auto index = levels.indexOf(letter);\n\t\t\treturn (index >= 0) ? index : kInvalidIndex;\n\t\t};\n\t\tconst auto result = ranges::max_element(\n\t\t\tsizes,\n\t\t\tstd::greater<>(),\n\t\t\tlevel);\n\t\treturn (level(*result) == kInvalidIndex) ? sizes.end() : result;\n\t};\n\tconst auto image = [&](const QByteArray &levels) {\n\t\tconst auto i = find(levels);\n\t\treturn (i == sizes.end())\n\t\t\t? ImageWithLocation()\n\t\t\t: Images::FromPhotoSize(_session, data, *i);\n\t};\n\tconst auto findVideoSize = [&](PhotoSize size)\n\t-> std::optional<MTPVideoSize> {\n\t\tconst auto sizes = data.vvideo_sizes();\n\t\tif (!sizes) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\tconst auto area = [](const MTPVideoSize &size) {\n\t\t\treturn size.match([](const MTPDvideoSize &data) {\n\t\t\t\treturn data.vsize().v ? (data.vw().v * data.vh().v) : 0;\n\t\t\t}, [](const MTPDvideoSizeEmojiMarkup &) {\n\t\t\t\treturn 0;\n\t\t\t}, [](const MTPDvideoSizeStickerMarkup &) {\n\t\t\t\treturn 0;\n\t\t\t});\n\t\t};\n\t\tconst auto type = [](const MTPVideoSize &size) {\n\t\t\treturn size.match([](const MTPDvideoSize &data) {\n\t\t\t\treturn data.vtype().v.isEmpty()\n\t\t\t\t\t? char(0)\n\t\t\t\t\t: data.vtype().v.front();\n\t\t\t}, [](const auto &) {\n\t\t\t\treturn char(0);\n\t\t\t});\n\t\t};\n\t\tconst auto result = (size == PhotoSize::Small)\n\t\t\t? ranges::find(sizes->v, 'p', type)\n\t\t\t: ranges::max_element(sizes->v, std::less<>(), area);\n\t\tif (result == sizes->v.end() || area(*result) <= 0) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\treturn std::make_optional(*result);\n\t};\n\tconst auto useProgressive = (progressive != sizes.end());\n\tconst auto large = useProgressive\n\t\t? Images::FromPhotoSize(_session, data, *progressive)\n\t\t: image(LargeLevels);\n\tif (large.location.valid()) {\n\t\tconst auto videoSmall = findVideoSize(PhotoSize::Small);\n\t\tconst auto videoLarge = findVideoSize(PhotoSize::Large);\n\t\tphotoApplyFields(\n\t\t\tphoto,\n\t\t\tdata.vaccess_hash().v,\n\t\t\tdata.vfile_reference().v,\n\t\t\tdata.vdate().v,\n\t\t\tdata.vdc_id().v,\n\t\t\tdata.is_has_stickers(),\n\t\t\tFindPhotoInlineThumbnail(data),\n\t\t\t(useProgressive\n\t\t\t\t? ImageWithLocation()\n\t\t\t\t: image(SmallLevels)),\n\t\t\t(useProgressive\n\t\t\t\t? Images::FromProgressiveSize(_session, *progressive, 1)\n\t\t\t\t: image(ThumbnailLevels)),\n\t\t\tlarge,\n\t\t\t(videoSmall\n\t\t\t\t? Images::FromVideoSize(_session, data, *videoSmall)\n\t\t\t\t: ImageWithLocation()),\n\t\t\t(videoLarge\n\t\t\t\t? Images::FromVideoSize(_session, data, *videoLarge)\n\t\t\t\t: ImageWithLocation()),\n\t\t\t(videoLarge\n\t\t\t\t? videoLarge->match([](const MTPDvideoSize &data) {\n\t\t\t\t\treturn VideoStartTime(data);\n\t\t\t\t}, [](const MTPDvideoSizeEmojiMarkup &) { return 0;\n\t\t\t\t}, [](const MTPDvideoSizeStickerMarkup &) { return 0; })\n\t\t\t\t: 0));\n\t}\n}\n\nvoid Session::photoApplyFields(\n\t\tnot_null<PhotoData*> photo,\n\t\tconst uint64 &access,\n\t\tconst QByteArray &fileReference,\n\t\tTimeId date,\n\t\tint32 dc,\n\t\tbool hasStickers,\n\t\tconst QByteArray &inlineThumbnailBytes,\n\t\tconst ImageWithLocation &small,\n\t\tconst ImageWithLocation &thumbnail,\n\t\tconst ImageWithLocation &large,\n\t\tconst ImageWithLocation &videoSmall,\n\t\tconst ImageWithLocation &videoLarge,\n\t\tcrl::time videoStartTime) {\n\tif (!date) {\n\t\treturn;\n\t}\n\tphoto->setRemoteLocation(dc, access, fileReference);\n\tphoto->setFields(date, hasStickers);\n\tphoto->updateImages(\n\t\tinlineThumbnailBytes,\n\t\tsmall,\n\t\tthumbnail,\n\t\tlarge,\n\t\tvideoSmall,\n\t\tvideoLarge,\n\t\tvideoStartTime);\n}\n\nnot_null<DocumentData*> Session::document(DocumentId id) {\n\tauto i = _documents.find(id);\n\tif (i == _documents.cend()) {\n\t\ti = _documents.emplace(\n\t\t\tid,\n\t\t\tstd::make_unique<DocumentData>(this, id)).first;\n\t}\n\treturn i->second.get();\n}\n\nnot_null<DocumentData*> Session::processDocument(\n\t\tconst MTPDocument &data,\n\t\tconst MTPVector<MTPDocument> *qualities) {\n\treturn data.match([&](const MTPDdocument &data) {\n\t\treturn processDocument(data, qualities);\n\t}, [&](const MTPDdocumentEmpty &data) {\n\t\treturn document(data.vid().v);\n\t});\n}\n\nnot_null<DocumentData*> Session::processDocument(\n\t\tconst MTPDdocument &data,\n\t\tconst MTPVector<MTPDocument> *qualities) {\n\tconst auto result = document(data.vid().v);\n\tdocumentApplyFields(result, data);\n\tif (qualities) {\n\t\tresult->setVideoQualities(qualities->v);\n\t}\n\treturn result;\n}\n\nnot_null<DocumentData*> Session::processDocument(\n\t\tconst MTPdocument &data,\n\t\tconst ImageWithLocation &thumbnail) {\n\treturn data.match([&](const MTPDdocument &data) {\n\t\treturn document(\n\t\t\tdata.vid().v,\n\t\t\tdata.vaccess_hash().v,\n\t\t\tdata.vfile_reference().v,\n\t\t\tdata.vdate().v,\n\t\t\tdata.vattributes().v,\n\t\t\tqs(data.vmime_type()),\n\t\t\tInlineImageLocation(),\n\t\t\tthumbnail,\n\t\t\tImageWithLocation(), // videoThumbnail\n\t\t\tfalse, // isPremiumSticker\n\t\t\tdata.vdc_id().v,\n\t\t\tdata.vsize().v);\n\t}, [&](const MTPDdocumentEmpty &data) {\n\t\treturn document(data.vid().v);\n\t});\n}\n\nnot_null<DocumentData*> Session::document(\n\t\tDocumentId id,\n\t\tconst uint64 &access,\n\t\tconst QByteArray &fileReference,\n\t\tTimeId date,\n\t\tconst QVector<MTPDocumentAttribute> &attributes,\n\t\tconst QString &mime,\n\t\tconst InlineImageLocation &inlineThumbnail,\n\t\tconst ImageWithLocation &thumbnail,\n\t\tconst ImageWithLocation &videoThumbnail,\n\t\tbool isPremiumSticker,\n\t\tint32 dc,\n\t\tint64 size) {\n\tconst auto result = document(id);\n\tdocumentApplyFields(\n\t\tresult,\n\t\taccess,\n\t\tfileReference,\n\t\tdate,\n\t\tattributes,\n\t\tmime,\n\t\tinlineThumbnail,\n\t\tthumbnail,\n\t\tvideoThumbnail,\n\t\tisPremiumSticker,\n\t\tdc,\n\t\tsize);\n\treturn result;\n}\n\nvoid Session::documentConvert(\n\t\tnot_null<DocumentData*> original,\n\t\tconst MTPDocument &data) {\n\tconst auto id = data.match([](const auto &data) {\n\t\treturn data.vid().v;\n\t});\n\tconst auto oldCacheKey = original->cacheKey();\n\tconst auto oldGoodKey = original->goodThumbnailCacheKey();\n\tconst auto idChanged = (original->id != id);\n\tif (idChanged) {\n\t\tauto i = _documents.find(id);\n\t\tif (i == _documents.end()) {\n\t\t\tconst auto j = _documents.find(original->id);\n\t\t\tAssert(j != _documents.end());\n\t\t\tauto owned = std::move(j->second);\n\t\t\t_documents.erase(j);\n\t\t\ti = _documents.emplace(id, std::move(owned)).first;\n\t\t}\n\n\t\toriginal->id = id;\n\t\toriginal->status = FileReady;\n\t\toriginal->uploadingData = nullptr;\n\n\t\tif (i->second.get() != original) {\n\t\t\tdocumentApplyFields(i->second.get(), data);\n\t\t}\n\t}\n\tdocumentApplyFields(original, data);\n\tif (idChanged) {\n\t\tcache().moveIfEmpty(oldCacheKey, original->cacheKey());\n\t\tcache().moveIfEmpty(oldGoodKey, original->goodThumbnailCacheKey());\n\t\tif (stickers().savedGifs().indexOf(original) >= 0) {\n\t\t\t_session->local().writeSavedGifs();\n\t\t}\n\t}\n}\n\nDocumentData *Session::documentFromWeb(\n\t\tconst MTPWebDocument &data,\n\t\tconst ImageLocation &thumbnailLocation,\n\t\tconst ImageLocation &videoThumbnailLocation) {\n\treturn data.match([&](const auto &data) {\n\t\treturn documentFromWeb(\n\t\t\tdata,\n\t\t\tthumbnailLocation,\n\t\t\tvideoThumbnailLocation);\n\t});\n}\n\nDocumentData *Session::documentFromWeb(\n\t\tconst MTPDwebDocument &data,\n\t\tconst ImageLocation &thumbnailLocation,\n\t\tconst ImageLocation &videoThumbnailLocation) {\n\tconst auto result = document(\n\t\tbase::RandomValue<DocumentId>(),\n\t\tuint64(0),\n\t\tQByteArray(),\n\t\tbase::unixtime::now(),\n\t\tdata.vattributes().v,\n\t\tdata.vmime_type().v,\n\t\tInlineImageLocation(),\n\t\tImageWithLocation{ .location = thumbnailLocation },\n\t\tImageWithLocation{ .location = videoThumbnailLocation },\n\t\tfalse, // isPremiumSticker\n\t\tsession().mainDcId(),\n\t\tint64(0)); // data.vsize().v\n\tresult->setWebLocation(WebFileLocation(\n\t\tdata.vurl().v,\n\t\tdata.vaccess_hash().v));\n\treturn result;\n}\n\nDocumentData *Session::documentFromWeb(\n\t\tconst MTPDwebDocumentNoProxy &data,\n\t\tconst ImageLocation &thumbnailLocation,\n\t\tconst ImageLocation &videoThumbnailLocation) {\n\tconst auto result = document(\n\t\tbase::RandomValue<DocumentId>(),\n\t\tuint64(0),\n\t\tQByteArray(),\n\t\tbase::unixtime::now(),\n\t\tdata.vattributes().v,\n\t\tdata.vmime_type().v,\n\t\tInlineImageLocation(),\n\t\tImageWithLocation{ .location = thumbnailLocation },\n\t\tImageWithLocation{ .location = videoThumbnailLocation },\n\t\tfalse, // isPremiumSticker\n\t\tsession().mainDcId(),\n\t\tint64(0)); // data.vsize().v\n\tresult->setContentUrl(qs(data.vurl()));\n\treturn result;\n}\n\nvoid Session::documentApplyFields(\n\t\tnot_null<DocumentData*> document,\n\t\tconst MTPDocument &data) {\n\tif (data.type() == mtpc_document) {\n\t\tdocumentApplyFields(document, data.c_document());\n\t}\n}\n\nvoid Session::documentApplyFields(\n\t\tnot_null<DocumentData*> document,\n\t\tconst MTPDdocument &data) {\n\tconst auto inlineThumbnail = FindDocumentInlineThumbnail(data);\n\tconst auto thumbnailSize = FindDocumentThumbnail(data);\n\tconst auto videoThumbnailSize = FindDocumentVideoThumbnail(data);\n\tconst auto prepared = Images::FromPhotoSize(\n\t\t_session,\n\t\tdata,\n\t\tthumbnailSize);\n\tconst auto videoThumbnail = videoThumbnailSize\n\t\t? Images::FromVideoSize(_session, data, *videoThumbnailSize)\n\t\t: ImageWithLocation();\n\tconst auto isPremiumSticker = videoThumbnailSize\n\t\t&& (videoThumbnailSize->c_videoSize().vtype().v == \"f\");\n\tdocumentApplyFields(\n\t\tdocument,\n\t\tdata.vaccess_hash().v,\n\t\tdata.vfile_reference().v,\n\t\tdata.vdate().v,\n\t\tdata.vattributes().v,\n\t\tqs(data.vmime_type()),\n\t\tinlineThumbnail,\n\t\tprepared,\n\t\tvideoThumbnail,\n\t\tisPremiumSticker,\n\t\tdata.vdc_id().v,\n\t\tdata.vsize().v);\n}\n\nvoid Session::documentApplyFields(\n\t\tnot_null<DocumentData*> document,\n\t\tconst uint64 &access,\n\t\tconst QByteArray &fileReference,\n\t\tTimeId date,\n\t\tconst QVector<MTPDocumentAttribute> &attributes,\n\t\tconst QString &mime,\n\t\tconst InlineImageLocation &inlineThumbnail,\n\t\tconst ImageWithLocation &thumbnail,\n\t\tconst ImageWithLocation &videoThumbnail,\n\t\tbool isPremiumSticker,\n\t\tint32 dc,\n\t\tint64 size) {\n\tif (!date) {\n\t\treturn;\n\t}\n\tdocument->date = date;\n\tdocument->setMimeString(mime);\n\tdocument->updateThumbnails(\n\t\tinlineThumbnail,\n\t\tthumbnail,\n\t\tvideoThumbnail,\n\t\tisPremiumSticker);\n\tdocument->size = size;\n\tdocument->setattributes(attributes);\n\n\t// Uses 'type' that is computed from attributes.\n\tdocument->recountIsImage();\n\tif (dc != 0 && access != 0) {\n\t\tdocument->setRemoteLocation(dc, access, fileReference);\n\t}\n}\n\nnot_null<DocumentData*> Session::venueIconDocument(const QString &icon) {\n\tconst auto i = _venueIcons.find(icon);\n\tif (i != end(_venueIcons)) {\n\t\treturn i->second;\n\t}\n\tconst auto result = documentFromWeb(MTP_webDocumentNoProxy(\n\t\tMTP_string(u\"https://ss3.4sqi.net/img/categories_v2/\"_q\n\t\t\t+ icon\n\t\t\t+ u\"_64.png\"_q),\n\t\tMTP_int(0),\n\t\tMTP_string(\"image/png\"),\n\t\tMTP_vector<MTPDocumentAttribute>()), {}, {});\n\t_venueIcons.emplace(icon, result);\n\treturn result;\n}\n\nnot_null<WebPageData*> Session::webpage(WebPageId id) {\n\tauto i = _webpages.find(id);\n\tif (i == _webpages.cend()) {\n\t\ti = _webpages.emplace(\n\t\t\tid,\n\t\t\tstd::make_unique<WebPageData>(this, id)).first;\n\t}\n\treturn i->second.get();\n}\n\nnot_null<WebPageData*> Session::processWebpage(const MTPWebPage &data) {\n\tswitch (data.type()) {\n\tcase mtpc_webPage:\n\t\treturn processWebpage(data.c_webPage());\n\tcase mtpc_webPageEmpty: {\n\t\tconst auto result = webpage(data.c_webPageEmpty().vid().v);\n\t\tresult->type = WebPageType::None;\n\t\tif (result->pendingTill > 0) {\n\t\t\tresult->pendingTill = 0;\n\t\t\tresult->failed = 1;\n\t\t\tnotifyWebPageUpdateDelayed(result);\n\t\t}\n\t\treturn result;\n\t} break;\n\tcase mtpc_webPagePending:\n\t\treturn processWebpage(data.c_webPagePending());\n\tcase mtpc_webPageNotModified:\n\t\tLOG((\"API Error: \"\n\t\t\t\"webPageNotModified is unexpected in Session::webpage().\"));\n\t\treturn webpage(0);\n\t}\n\tUnexpected(\"Type in Session::webpage().\");\n}\n\nnot_null<WebPageData*> Session::processWebpage(const MTPDwebPage &data) {\n\tconst auto result = webpage(data.vid().v);\n\twebpageApplyFields(result, data);\n\treturn result;\n}\n\nnot_null<WebPageData*> Session::processWebpage(\n\t\tconst MTPDwebPagePending &data) {\n\tconstexpr auto kDefaultPendingTimeout = 60;\n\tconst auto result = webpage(data.vid().v);\n\twebpageApplyFields(\n\t\tresult,\n\t\tWebPageType::None,\n\t\tQString(),\n\t\tQString(),\n\t\tQString(),\n\t\tQString(),\n\t\tTextWithEntities(),\n\t\tFullStoryId(),\n\t\tnullptr,\n\t\tnullptr,\n\t\tWebPageCollage(),\n\t\tnullptr,\n\t\tnullptr,\n\t\tnullptr,\n\t\tnullptr,\n\t\t0,\n\t\tQString(),\n\t\tfalse,\n\t\tfalse,\n\t\tdata.vdate().v\n\t\t\t? data.vdate().v\n\t\t\t: (base::unixtime::now() + kDefaultPendingTimeout));\n\treturn result;\n}\n\nnot_null<WebPageData*> Session::webpage(\n\t\tWebPageId id,\n\t\tconst QString &siteName,\n\t\tconst TextWithEntities &content) {\n\treturn webpage(\n\t\tid,\n\t\tWebPageType::Article,\n\t\tQString(),\n\t\tQString(),\n\t\tsiteName,\n\t\tQString(),\n\t\tcontent,\n\t\tnullptr,\n\t\tnullptr,\n\t\tWebPageCollage(),\n\t\tnullptr,\n\t\tnullptr,\n\t\tnullptr,\n\t\t0,\n\t\tQString(),\n\t\tfalse,\n\t\tfalse,\n\t\tTimeId(0));\n}\n\nnot_null<WebPageData*> Session::webpage(\n\t\tWebPageId id,\n\t\tWebPageType type,\n\t\tconst QString &url,\n\t\tconst QString &displayUrl,\n\t\tconst QString &siteName,\n\t\tconst QString &title,\n\t\tconst TextWithEntities &description,\n\t\tPhotoData *photo,\n\t\tDocumentData *document,\n\t\tWebPageCollage &&collage,\n\t\tstd::unique_ptr<Iv::Data> iv,\n\t\tstd::unique_ptr<WebPageStickerSet> stickerSet,\n\t\tstd::shared_ptr<UniqueGift> uniqueGift,\n\t\tint duration,\n\t\tconst QString &author,\n\t\tbool hasLargeMedia,\n\t\tbool photoIsVideoCover,\n\t\tTimeId pendingTill) {\n\tconst auto result = webpage(id);\n\twebpageApplyFields(\n\t\tresult,\n\t\ttype,\n\t\turl,\n\t\tdisplayUrl,\n\t\tsiteName,\n\t\ttitle,\n\t\tdescription,\n\t\tFullStoryId(),\n\t\tphoto,\n\t\tdocument,\n\t\tstd::move(collage),\n\t\tstd::move(iv),\n\t\tstd::move(stickerSet),\n\t\tstd::move(uniqueGift),\n\t\tnullptr,\n\t\tduration,\n\t\tauthor,\n\t\thasLargeMedia,\n\t\tphotoIsVideoCover,\n\t\tpendingTill);\n\treturn result;\n}\n\nvoid Session::webpageApplyFields(\n\t\tnot_null<WebPageData*> page,\n\t\tconst MTPDwebPage &data) {\n\tauto description = TextWithEntities{\n\t\tqs(data.vdescription().value_or_empty())\n\t};\n\tconst auto siteName = qs(data.vsite_name().value_or_empty());\n\tauto parseFlags = TextParseLinks | TextParseMultiline;\n\tif (siteName == u\"Twitter\"_q || siteName == u\"Instagram\"_q) {\n\t\tparseFlags |= TextParseHashtags | TextParseMentions;\n\t}\n\tTextUtilities::ParseEntities(description, parseFlags);\n\tconst auto pendingTill = TimeId(0);\n\tconst auto photo = data.vphoto();\n\tconst auto document = data.vdocument();\n\tconst auto lookupInAttribute = [&](\n\t\t\tconst MTPDwebPageAttributeTheme &data) -> DocumentData* {\n\t\tif (const auto documents = data.vdocuments()) {\n\t\t\tfor (const auto &document : documents->v) {\n\t\t\t\tconst auto processed = processDocument(document);\n\t\t\t\tif (processed->isTheme()) {\n\t\t\t\t\treturn processed;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t};\n\n\tconst auto lookupThemeDocument = [&]() -> DocumentData* {\n\t\tif (const auto attributes = data.vattributes()) {\n\t\t\tfor (const auto &attribute : attributes->v) {\n\t\t\t\tconst auto result = attribute.match([&](\n\t\t\t\t\t\tconst MTPDwebPageAttributeTheme &data) {\n\t\t\t\t\treturn lookupInAttribute(data);\n\t\t\t\t}, [](const MTPDwebPageAttributeStory &) {\n\t\t\t\t\treturn (DocumentData*)nullptr;\n\t\t\t\t}, [](const MTPDwebPageAttributeStickerSet &) {\n\t\t\t\t\treturn (DocumentData*)nullptr;\n\t\t\t\t}, [](const MTPDwebPageAttributeUniqueStarGift &) {\n\t\t\t\t\treturn (DocumentData*)nullptr;\n\t\t\t\t}, [](const MTPDwebPageAttributeStarGiftCollection &) {\n\t\t\t\t\treturn (DocumentData*)nullptr;\n\t\t\t\t}, [](const MTPDwebPageAttributeStarGiftAuction &) {\n\t\t\t\t\treturn (DocumentData*)nullptr;\n\t\t\t\t});\n\t\t\t\tif (result) {\n\t\t\t\t\treturn result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t};\n\n\tusing WebPageStickerSetPtr = std::unique_ptr<WebPageStickerSet>;\n\tconst auto lookupStickerSet = [&]() -> WebPageStickerSetPtr {\n\t\tif (const auto attributes = data.vattributes()) {\n\t\t\tfor (const auto &attribute : attributes->v) {\n\t\t\t\tauto result = attribute.match([&](\n\t\t\t\t\t\tconst MTPDwebPageAttributeStickerSet &data) {\n\t\t\t\t\tauto result = std::make_unique<WebPageStickerSet>();\n\t\t\t\t\tresult->isEmoji = data.is_emojis();\n\t\t\t\t\tresult->isTextColor = data.is_text_color();\n\t\t\t\t\tfor (const auto &tl : data.vstickers().v) {\n\t\t\t\t\t\tresult->items.push_back(processDocument(tl));\n\t\t\t\t\t}\n\t\t\t\t\treturn result;\n\t\t\t\t}, [&](const MTPDwebPageAttributeStarGiftCollection &data) {\n\t\t\t\t\tauto result = std::make_unique<WebPageStickerSet>();\n\t\t\t\t\tresult->isEmoji = false;\n\t\t\t\t\tresult->isTextColor = false;\n\t\t\t\t\tfor (const auto &tl : data.vicons().v) {\n\t\t\t\t\t\tresult->items.push_back(processDocument(tl));\n\t\t\t\t\t}\n\t\t\t\t\treturn result;\n\t\t\t\t}, [](const auto &) {\n\t\t\t\t\treturn WebPageStickerSetPtr(nullptr);\n\t\t\t\t});\n\t\t\t\tif (result && !result->items.empty()) {\n\t\t\t\t\treturn result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t};\n\n\tusing UniqueGiftPtr = std::shared_ptr<UniqueGift>;\n\tconst auto lookupUniqueGift = [&]() -> UniqueGiftPtr {\n\t\tif (const auto attributes = data.vattributes()) {\n\t\t\tfor (const auto &attribute : attributes->v) {\n\t\t\t\treturn attribute.match([&](\n\t\t\t\t\t\tconst MTPDwebPageAttributeUniqueStarGift &data) {\n\t\t\t\t\tconst auto gift = Api::FromTL(_session, data.vgift());\n\t\t\t\t\treturn gift ? gift->unique : nullptr;\n\t\t\t\t}, [](const auto &) -> UniqueGiftPtr { return nullptr; });\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t};\n\n\tusing WebPageAuctionPtr = std::unique_ptr<WebPageAuction>;\n\tconst auto lookupAuction = [&]() -> WebPageAuctionPtr {\n\t\tif (const auto attributes = data.vattributes()) {\n\t\t\tfor (const auto &attribute : attributes->v) {\n\t\t\t\treturn attribute.match([&](\n\t\t\t\t\t\tconst MTPDwebPageAttributeStarGiftAuction &data) {\n\t\t\t\t\tconst auto gift = Api::FromTL(_session, data.vgift());\n\t\t\t\t\tif (!gift) {\n\t\t\t\t\t\treturn WebPageAuctionPtr(nullptr);\n\t\t\t\t\t}\n\t\t\t\t\tauto auction = std::make_unique<WebPageAuction>();\n\t\t\t\t\tauction->auctionGift = std::make_shared<StarGift>(*gift);\n\t\t\t\t\tauction->endDate = data.vend_date().v;\n\t\t\t\t\treturn auction;\n\t\t\t\t}, [](const auto &) -> WebPageAuctionPtr { return nullptr; });\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t};\n\n\tauto story = (Data::Story*)nullptr;\n\tauto storyId = FullStoryId();\n\tif (const auto attributes = data.vattributes()) {\n\t\tfor (const auto &attribute : attributes->v) {\n\t\t\tattribute.match([&](const MTPDwebPageAttributeStory &data) {\n\t\t\t\tstoryId = FullStoryId{\n\t\t\t\t\tpeerFromMTP(data.vpeer()),\n\t\t\t\t\tdata.vid().v,\n\t\t\t\t};\n\t\t\t\tif (const auto embed = data.vstory()) {\n\t\t\t\t\tstory = stories().applySingle(\n\t\t\t\t\t\tpeerFromMTP(data.vpeer()),\n\t\t\t\t\t\t*embed);\n\t\t\t\t} else if (const auto maybe = stories().lookup(storyId)) {\n\t\t\t\t\tstory = *maybe;\n\t\t\t\t} else if (maybe.error() == Data::NoStory::Unknown) {\n\t\t\t\t\tstories().resolve(storyId, [=] {\n\t\t\t\t\t\tif (const auto maybe = stories().lookup(storyId)) {\n\t\t\t\t\t\t\tconst auto story = *maybe;\n\t\t\t\t\t\t\tpage->document = story->document();\n\t\t\t\t\t\t\tpage->photo = story->photo();\n\t\t\t\t\t\t\tpage->description = story->caption();\n\t\t\t\t\t\t\tpage->type = WebPageType::Story;\n\t\t\t\t\t\t\tnotifyWebPageUpdateDelayed(page);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}, [](const auto &) {});\n\t\t}\n\t}\n\tif (const auto page = data.vcached_page()) {\n\t\tfor (const auto &photo : page->data().vphotos().v) {\n\t\t\tprocessPhoto(photo);\n\t\t}\n\t\tfor (const auto &document : page->data().vdocuments().v) {\n\t\t\tprocessDocument(document);\n\t\t}\n\t\tconst auto process = [&](\n\t\t\t\tconst MTPPageBlock &block,\n\t\t\t\tconst auto &self) -> void {\n\t\t\tblock.match([&](const MTPDpageBlockChannel &data) {\n\t\t\t\tprocessChat(data.vchannel());\n\t\t\t}, [&](const MTPDpageBlockCover &data) {\n\t\t\t\tself(data.vcover(), self);\n\t\t\t}, [&](const MTPDpageBlockEmbedPost &data) {\n\t\t\t\tfor (const auto &block : data.vblocks().v) {\n\t\t\t\t\tself(block, self);\n\t\t\t\t}\n\t\t\t}, [&](const MTPDpageBlockCollage &data) {\n\t\t\t\tfor (const auto &block : data.vitems().v) {\n\t\t\t\t\tself(block, self);\n\t\t\t\t}\n\t\t\t}, [&](const MTPDpageBlockSlideshow &data) {\n\t\t\t\tfor (const auto &block : data.vitems().v) {\n\t\t\t\t\tself(block, self);\n\t\t\t\t}\n\t\t\t}, [&](const MTPDpageBlockDetails &data) {\n\t\t\t\tfor (const auto &block : data.vblocks().v) {\n\t\t\t\t\tself(block, self);\n\t\t\t\t}\n\t\t\t}, [](const auto &) {});\n\t\t};\n\t\tfor (const auto &block : page->data().vblocks().v) {\n\t\t\tprocess(block, process);\n\t\t}\n\t}\n\tconst auto type = story ? WebPageType::Story : ParseWebPageType(data);\n\tauto iv = (data.vcached_page() && !IgnoreIv(type))\n\t\t? std::make_unique<Iv::Data>(data, *data.vcached_page())\n\t\t: nullptr;\n\twebpageApplyFields(\n\t\tpage,\n\t\ttype,\n\t\tqs(data.vurl()),\n\t\tqs(data.vdisplay_url()),\n\t\tsiteName,\n\t\tqs(data.vtitle().value_or_empty()),\n\t\t(story ? story->caption() : description),\n\t\tstoryId,\n\t\t(story\n\t\t\t? story->photo()\n\t\t\t: photo\n\t\t\t? processPhoto(*photo).get()\n\t\t\t: nullptr),\n\t\t(story\n\t\t\t? story->document()\n\t\t\t: document\n\t\t\t? processDocument(*document).get()\n\t\t\t: lookupThemeDocument()),\n\t\tWebPageCollage(this, data),\n\t\tstd::move(iv),\n\t\tlookupStickerSet(),\n\t\tlookupUniqueGift(),\n\t\tlookupAuction(),\n\t\tdata.vduration().value_or_empty(),\n\t\tqs(data.vauthor().value_or_empty()),\n\t\tdata.is_has_large_media(),\n\t\tdata.is_video_cover_photo(),\n\t\tpendingTill);\n}\n\nvoid Session::webpageApplyFields(\n\t\tnot_null<WebPageData*> page,\n\t\tWebPageType type,\n\t\tconst QString &url,\n\t\tconst QString &displayUrl,\n\t\tconst QString &siteName,\n\t\tconst QString &title,\n\t\tconst TextWithEntities &description,\n\t\tFullStoryId storyId,\n\t\tPhotoData *photo,\n\t\tDocumentData *document,\n\t\tWebPageCollage &&collage,\n\t\tstd::unique_ptr<Iv::Data> iv,\n\t\tstd::unique_ptr<WebPageStickerSet> stickerSet,\n\t\tstd::shared_ptr<UniqueGift> uniqueGift,\n\t\tstd::unique_ptr<WebPageAuction> auction,\n\t\tint duration,\n\t\tconst QString &author,\n\t\tbool hasLargeMedia,\n\t\tbool photoIsVideoCover,\n\t\tTimeId pendingTill) {\n\tconst auto requestPending = (!page->pendingTill && pendingTill > 0);\n\tconst auto changed = page->applyChanges(\n\t\ttype,\n\t\turl,\n\t\tdisplayUrl,\n\t\tsiteName,\n\t\ttitle,\n\t\tdescription,\n\t\tstoryId,\n\t\tphoto,\n\t\tdocument,\n\t\tstd::move(collage),\n\t\tstd::move(iv),\n\t\tstd::move(stickerSet),\n\t\tstd::move(uniqueGift),\n\t\tstd::move(auction),\n\t\tduration,\n\t\tauthor,\n\t\thasLargeMedia,\n\t\tphotoIsVideoCover,\n\t\tpendingTill);\n\tif (requestPending) {\n\t\t_session->api().requestWebPageDelayed(page);\n\t}\n\tif (changed) {\n\t\tnotifyWebPageUpdateDelayed(page);\n\t}\n}\n\nnot_null<GameData*> Session::game(GameId id) {\n\tauto i = _games.find(id);\n\tif (i == _games.cend()) {\n\t\ti = _games.emplace(id, std::make_unique<GameData>(this, id)).first;\n\t}\n\treturn i->second.get();\n}\n\nnot_null<GameData*> Session::processGame(const MTPDgame &data) {\n\tconst auto result = game(data.vid().v);\n\tgameApplyFields(result, data);\n\treturn result;\n}\n\nnot_null<GameData*> Session::game(\n\t\tGameId id,\n\t\tconst uint64 &accessHash,\n\t\tconst QString &shortName,\n\t\tconst QString &title,\n\t\tconst QString &description,\n\t\tPhotoData *photo,\n\t\tDocumentData *document) {\n\tconst auto result = game(id);\n\tgameApplyFields(\n\t\tresult,\n\t\taccessHash,\n\t\tshortName,\n\t\ttitle,\n\t\tdescription,\n\t\tphoto,\n\t\tdocument);\n\treturn result;\n}\n\nvoid Session::gameConvert(\n\t\tnot_null<GameData*> original,\n\t\tconst MTPGame &data) {\n\tExpects(data.type() == mtpc_game);\n\n\tconst auto id = data.c_game().vid().v;\n\tif (original->id != id) {\n\t\tauto i = _games.find(id);\n\t\tif (i == _games.end()) {\n\t\t\tconst auto j = _games.find(original->id);\n\t\t\tAssert(j != _games.end());\n\t\t\tauto owned = std::move(j->second);\n\t\t\t_games.erase(j);\n\t\t\ti = _games.emplace(id, std::move(owned)).first;\n\t\t}\n\n\t\toriginal->id = id;\n\t\toriginal->accessHash = 0;\n\n\t\tif (i->second.get() != original) {\n\t\t\tgameApplyFields(i->second.get(), data.c_game());\n\t\t}\n\t}\n\tgameApplyFields(original, data.c_game());\n}\n\nvoid Session::gameApplyFields(\n\t\tnot_null<GameData*> game,\n\t\tconst MTPDgame &data) {\n\tconst auto document = data.vdocument();\n\tgameApplyFields(\n\t\tgame,\n\t\tdata.vaccess_hash().v,\n\t\tqs(data.vshort_name()),\n\t\tqs(data.vtitle()),\n\t\tqs(data.vdescription()),\n\t\tprocessPhoto(data.vphoto()),\n\t\tdocument ? processDocument(*document).get() : nullptr);\n}\n\nvoid Session::gameApplyFields(\n\t\tnot_null<GameData*> game,\n\t\tconst uint64 &accessHash,\n\t\tconst QString &shortName,\n\t\tconst QString &title,\n\t\tconst QString &description,\n\t\tPhotoData *photo,\n\t\tDocumentData *document) {\n\tif (game->accessHash) {\n\t\treturn;\n\t}\n\tgame->accessHash = accessHash;\n\tgame->shortName = shortName;\n\tgame->title = TextUtilities::SingleLine(title);\n\tgame->description = description;\n\tgame->photo = photo;\n\tgame->document = document;\n\tnotifyGameUpdateDelayed(game);\n}\n\nnot_null<BotAppData*> Session::botApp(BotAppId id) {\n\tconst auto i = _botApps.find(id);\n\treturn (i != end(_botApps))\n\t\t? i->second.get()\n\t\t: _botApps.emplace(\n\t\t\tid,\n\t\t\tstd::make_unique<BotAppData>(this, id)).first->second.get();\n}\n\nBotAppData *Session::findBotApp(PeerId botId, const QString &appName) const {\n\tfor (const auto &[id, app] : _botApps) {\n\t\tif (app->botId == botId && app->shortName == appName) {\n\t\t\treturn app.get();\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nBotAppData *Session::processBotApp(\n\t\tPeerId botId,\n\t\tconst MTPBotApp &data) {\n\treturn data.match([&](const MTPDbotApp &data) {\n\t\tconst auto result = botApp(data.vid().v);\n\t\tresult->botId = botId;\n\t\tresult->shortName = qs(data.vshort_name());\n\t\tresult->title = qs(data.vtitle());\n\t\tresult->description = qs(data.vdescription());\n\t\tresult->photo = processPhoto(data.vphoto());\n\t\tresult->document = data.vdocument()\n\t\t\t? processDocument(*data.vdocument()).get()\n\t\t\t: nullptr;\n\t\tresult->accessHash = data.vaccess_hash().v;\n\t\tresult->hash = data.vhash().v;\n\t\treturn result.get();\n\t}, [](const MTPDbotAppNotModified &) {\n\t\treturn (BotAppData*)nullptr;\n\t});\n}\n\nnot_null<PollData*> Session::poll(PollId id) {\n\tauto i = _polls.find(id);\n\tif (i == _polls.cend()) {\n\t\ti = _polls.emplace(id, std::make_unique<PollData>(this, id)).first;\n\t}\n\treturn i->second.get();\n}\n\nHistoryItem *Session::findItemForPoll(PollId id) const {\n\tconst auto i = _polls.find(id);\n\tif (i == _polls.end()) {\n\t\treturn nullptr;\n\t}\n\tconst auto j = _pollViews.find(i->second.get());\n\tif (j == _pollViews.end() || j->second.empty()) {\n\t\treturn nullptr;\n\t}\n\treturn j->second.front()->data();\n}\n\nstd::vector<not_null<PeerData*>> Session::pollRecentVoters(PollId id) const {\n\tauto result = std::vector<not_null<PeerData*>>();\n\tconst auto i = _polls.find(id);\n\tif (i == _polls.end()) {\n\t\treturn result;\n\t}\n\tfor (const auto &answer : i->second->answers) {\n\t\tfor (const auto &voter : answer.recentVoters) {\n\t\t\tif (!ranges::contains(result, voter)) {\n\t\t\t\tresult.push_back(voter);\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nnot_null<PollData*> Session::processPoll(const MTPPoll &data) {\n\treturn data.match([&](const MTPDpoll &data) {\n\t\tconst auto id = data.vid().v;\n\t\tconst auto result = poll(id);\n\t\tconst auto changed = result->applyChanges(data);\n\t\tif (changed) {\n\t\t\tnotifyPollUpdateDelayed(result);\n\t\t}\n\t\tif (result->closeDate > 0 && !result->closed()) {\n\t\t\t_pollsClosings.emplace(result->closeDate, result);\n\t\t\tcheckPollsClosings();\n\t\t}\n\t\treturn result;\n\t});\n}\n\nnot_null<PollData*> Session::processPoll(const MTPDmessageMediaPoll &data) {\n\tconst auto result = processPoll(data.vpoll());\n\tauto media = PollMedia();\n\tif (const auto attached = data.vattached_media()) {\n\t\tattached->match([&](const MTPDmessageMediaPhoto &data) {\n\t\t\tconst auto photo = data.vphoto();\n\t\t\tif (photo && photo->type() == mtpc_photo) {\n\t\t\t\tmedia.photo = processPhoto(*photo);\n\t\t\t}\n\t\t}, [&](const MTPDmessageMediaDocument &data) {\n\t\t\tconst auto document = data.vdocument();\n\t\t\tif (document && document->type() == mtpc_document) {\n\t\t\t\tmedia.document = processDocument(*document);\n\t\t\t}\n\t\t}, [&](const MTPDmessageMediaGeo &data) {\n\t\t\tdata.vgeo().match([&](const MTPDgeoPoint &point) {\n\t\t\t\tmedia.geo = LocationPoint(point);\n\t\t\t}, [](const MTPDgeoPointEmpty &) {\n\t\t\t});\n\t\t}, [&](const MTPDmessageMediaVenue &data) {\n\t\t\tdata.vgeo().match([&](const MTPDgeoPoint &point) {\n\t\t\t\tmedia.geo = LocationPoint(point);\n\t\t\t}, [](const MTPDgeoPointEmpty &) {\n\t\t\t});\n\t\t}, [](const auto &) {\n\t\t});\n\t}\n\tif (result->attachedMedia != media) {\n\t\tresult->attachedMedia = media;\n\t\t++result->version;\n\t\tnotifyPollUpdateDelayed(result);\n\t}\n\tconst auto changed = result->applyResults(data.vresults());\n\tif (changed) {\n\t\tnotifyPollUpdateDelayed(result);\n\t}\n\treturn result;\n}\n\nnot_null<TodoListData*> Session::todoList(TodoListId id) {\n\tauto i = _todoLists.find(id);\n\tif (i == _todoLists.cend()) {\n\t\ti = _todoLists.emplace(\n\t\t\tid,\n\t\t\tstd::make_unique<TodoListData>(this, id)).first;\n\t}\n\treturn i->second.get();\n}\n\nnot_null<TodoListData*> Session::processTodoList(\n\t\tTodoListId id,\n\t\tconst MTPTodoList &todolist) {\n\tconst auto &data = todolist.data();\n\tconst auto result = todoList(id);\n\tconst auto changed = result->applyChanges(data);\n\tif (changed) {\n\t\tnotifyTodoListUpdateDelayed(result);\n\t}\n\treturn result;\n}\n\nnot_null<TodoListData*> Session::processTodoList(\n\t\tTodoListId id,\n\t\tconst MTPDmessageMediaToDo &data) {\n\tconst auto result = processTodoList(id, data.vtodo());\n\tconst auto changed = result->applyCompletions(data.vcompletions());\n\tif (changed) {\n\t\tnotifyTodoListUpdateDelayed(result);\n\t}\n\treturn result;\n}\n\nnot_null<TodoListData*> Session::duplicateTodoList(\n\t\tTodoListId id,\n\t\tnot_null<TodoListData*> existing) {\n\tconst auto result = todoList(id);\n\tresult->title = existing->title;\n\tresult->items = existing->items;\n\t++result->version;\n\treturn result;\n}\n\nvoid Session::checkPollsClosings() {\n\tconst auto now = base::unixtime::now();\n\tauto closest = 0;\n\tfor (auto i = _pollsClosings.begin(); i != _pollsClosings.end();) {\n\t\tif (i->first <= now) {\n\t\t\tif (i->second->closeByTimer()) {\n\t\t\t\tnotifyPollUpdateDelayed(i->second);\n\t\t\t}\n\t\t\ti = _pollsClosings.erase(i);\n\t\t} else {\n\t\t\tif (!closest) {\n\t\t\t\tclosest = i->first;\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t}\n\tif (closest) {\n\t\tconstexpr auto kMaxTimeout = 24 * 3600 * crl::time(1000);\n\t\t_pollsClosingTimer.callOnce(\n\t\t\tstd::min((closest - now) * crl::time(1000), kMaxTimeout));\n\t} else {\n\t\t_pollsClosingTimer.cancel();\n\t}\n}\n\nvoid Session::applyUpdate(const MTPDupdateMessagePoll &update) {\n\tconst auto updated = [&] {\n\t\tconst auto poll = update.vpoll();\n\t\tconst auto i = _polls.find(update.vpoll_id().v);\n\t\treturn (i == end(_polls))\n\t\t\t? nullptr\n\t\t\t: poll\n\t\t\t? processPoll(*poll).get()\n\t\t\t: i->second.get();\n\t}();\n\tif (updated && updated->applyResults(update.vresults())) {\n\t\tnotifyPollUpdateDelayed(updated);\n\t}\n\tif (const auto tlPeer = update.vpeer()) {\n\t\tconst auto peer = peerFromMTP(*tlPeer);\n\t\tif (const auto history = historyLoaded(peer)) {\n\t\t\thistories().requestDialogEntry(history);\n\t\t}\n\t} else if (updated) {\n\t\tif (const auto i = _pollViews.find(updated)\n\t\t\t; i != _pollViews.end()) {\n\t\t\tfor (const auto &view : i->second) {\n\t\t\t\tif (view->data()->hasUnreadPollVote()) {\n\t\t\t\t\thistories().requestDialogEntry(\n\t\t\t\t\t\tview->data()->history());\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Session::applyUpdate(const MTPDupdateChatParticipants &update) {\n\tconst auto chatId = update.vparticipants().match([](const auto &update) {\n\t\treturn update.vchat_id().v;\n\t});\n\tif (const auto chat = chatLoaded(chatId)) {\n\t\tApplyChatUpdate(chat, update);\n\t\tfor (const auto &user : chat->participants) {\n\t\t\tif (user->isBot() && !user->botInfo->inited) {\n\t\t\t\t_session->api().requestFullPeer(user);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Session::applyUpdate(const MTPDupdateChatParticipantAdd &update) {\n\tif (const auto chat = chatLoaded(update.vchat_id().v)) {\n\t\tApplyChatUpdate(chat, update);\n\t}\n}\n\nvoid Session::applyUpdate(const MTPDupdateChatParticipantDelete &update) {\n\tif (const auto chat = chatLoaded(update.vchat_id().v)) {\n\t\tApplyChatUpdate(chat, update);\n\t}\n}\n\nvoid Session::applyUpdate(const MTPDupdateChatParticipantAdmin &update) {\n\tif (const auto chat = chatLoaded(update.vchat_id().v)) {\n\t\tApplyChatUpdate(chat, update);\n\t}\n}\n\nvoid Session::applyUpdate(const MTPDupdateChatParticipantRank &update) {\n\tif (const auto chat = chatLoaded(update.vchat_id().v)) {\n\t\tApplyChatUpdate(chat, update);\n\t}\n}\n\nvoid Session::applyUpdate(const MTPDupdateChatDefaultBannedRights &update) {\n\tif (const auto peer = peerLoaded(peerFromMTP(update.vpeer()))) {\n\t\tif (const auto chat = peer->asChat()) {\n\t\t\tApplyChatUpdate(chat, update);\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\tApplyChannelUpdate(channel, update);\n\t\t} else {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"User received in updateChatDefaultBannedRights.\"));\n\t\t}\n\t}\n}\n\nnot_null<Data::CloudImage*> Session::location(const LocationPoint &point) {\n\tconst auto i = _locations.find(point);\n\tif (i != _locations.cend()) {\n\t\treturn i->second.get();\n\t}\n\tconst auto location = Data::ComputeLocation(point);\n\tconst auto prepared = ImageWithLocation{\n\t\t.location = ImageLocation(\n\t\t\t{ location },\n\t\t\tlocation.width,\n\t\t\tlocation.height)\n\t};\n\treturn _locations.emplace(\n\t\tpoint,\n\t\tstd::make_unique<Data::CloudImage>(\n\t\t\t_session,\n\t\t\tprepared)).first->second.get();\n}\n\nvoid Session::registerPhotoItem(\n\t\tnot_null<const PhotoData*> photo,\n\t\tnot_null<HistoryItem*> item) {\n\t_photoItems[photo].insert(item);\n}\n\nvoid Session::unregisterPhotoItem(\n\t\tnot_null<const PhotoData*> photo,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto i = _photoItems.find(photo);\n\tif (i != _photoItems.end()) {\n\t\tauto &items = i->second;\n\t\tif (items.remove(item) && items.empty()) {\n\t\t\t_photoItems.erase(i);\n\t\t}\n\t}\n}\n\nvoid Session::registerDocumentItem(\n\t\tnot_null<const DocumentData*> document,\n\t\tnot_null<HistoryItem*> item) {\n\tif (document->isMusicForProfile()) {\n\t\tdocument->owner().savedMusic().loadIds();\n\t}\n\t_documentItems[document].insert(item);\n}\n\nvoid Session::unregisterDocumentItem(\n\t\tnot_null<const DocumentData*> document,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto i = _documentItems.find(document);\n\tif (i != _documentItems.end()) {\n\t\tauto &items = i->second;\n\t\tif (items.remove(item) && items.empty()) {\n\t\t\t_documentItems.erase(i);\n\t\t}\n\t}\n}\n\nvoid Session::registerWebPageView(\n\t\tnot_null<const WebPageData*> page,\n\t\tnot_null<ViewElement*> view) {\n\t_webpageViews[page].insert(view);\n}\n\nvoid Session::unregisterWebPageView(\n\t\tnot_null<const WebPageData*> page,\n\t\tnot_null<ViewElement*> view) {\n\tconst auto i = _webpageViews.find(page);\n\tif (i != _webpageViews.end()) {\n\t\tauto &items = i->second;\n\t\tif (items.remove(view) && items.empty()) {\n\t\t\t_webpageViews.erase(i);\n\t\t}\n\t}\n}\n\nvoid Session::registerWebPageItem(\n\t\tnot_null<const WebPageData*> page,\n\t\tnot_null<HistoryItem*> item) {\n\t_webpageItems[page].insert(item);\n}\n\nvoid Session::unregisterWebPageItem(\n\t\tnot_null<const WebPageData*> page,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto i = _webpageItems.find(page);\n\tif (i != _webpageItems.end()) {\n\t\tauto &items = i->second;\n\t\tif (items.remove(item) && items.empty()) {\n\t\t\t_webpageItems.erase(i);\n\t\t}\n\t}\n}\n\nvoid Session::registerGameView(\n\t\tnot_null<const GameData*> game,\n\t\tnot_null<ViewElement*> view) {\n\t_gameViews[game].insert(view);\n}\n\nvoid Session::unregisterGameView(\n\t\tnot_null<const GameData*> game,\n\t\tnot_null<ViewElement*> view) {\n\tconst auto i = _gameViews.find(game);\n\tif (i != _gameViews.end()) {\n\t\tauto &items = i->second;\n\t\tif (items.remove(view) && items.empty()) {\n\t\t\t_gameViews.erase(i);\n\t\t}\n\t}\n}\n\nvoid Session::registerPollView(\n\t\tnot_null<const PollData*> poll,\n\t\tnot_null<ViewElement*> view) {\n\t_pollViews[poll].insert(view);\n}\n\nvoid Session::unregisterPollView(\n\t\tnot_null<const PollData*> poll,\n\t\tnot_null<ViewElement*> view) {\n\tconst auto i = _pollViews.find(poll);\n\tif (i != _pollViews.end()) {\n\t\tauto &items = i->second;\n\t\tif (items.remove(view) && items.empty()) {\n\t\t\t_pollViews.erase(i);\n\t\t}\n\t}\n}\n\nvoid Session::registerTodoListView(\n\t\tnot_null<const TodoListData*> todolist,\n\t\tnot_null<ViewElement*> view) {\n\t_todoListViews[todolist].insert(view);\n}\n\nvoid Session::unregisterTodoListView(\n\t\tnot_null<const TodoListData*> todolist,\n\t\tnot_null<ViewElement*> view) {\n\tconst auto i = _todoListViews.find(todolist);\n\tif (i != _todoListViews.end()) {\n\t\tauto &items = i->second;\n\t\tif (items.remove(view) && items.empty()) {\n\t\t\t_todoListViews.erase(i);\n\t\t}\n\t}\n}\n\nvoid Session::registerContactView(\n\t\tUserId contactId,\n\t\tnot_null<ViewElement*> view) {\n\tif (!contactId) {\n\t\treturn;\n\t}\n\t_contactViews[contactId].insert(view);\n}\n\nvoid Session::unregisterContactView(\n\t\tUserId contactId,\n\t\tnot_null<ViewElement*> view) {\n\tif (!contactId) {\n\t\treturn;\n\t}\n\tconst auto i = _contactViews.find(contactId);\n\tif (i != _contactViews.end()) {\n\t\tauto &items = i->second;\n\t\tif (items.remove(view) && items.empty()) {\n\t\t\t_contactViews.erase(i);\n\t\t}\n\t}\n}\n\nvoid Session::registerContactItem(\n\t\tUserId contactId,\n\t\tnot_null<HistoryItem*> item) {\n\tif (!contactId) {\n\t\treturn;\n\t}\n\tconst auto contact = userLoaded(contactId);\n\tconst auto canShare = contact ? contact->canShareThisContact() : false;\n\n\t_contactItems[contactId].insert(item);\n\n\tif (contact && canShare != contact->canShareThisContact()) {\n\t\tsession().changes().peerUpdated(\n\t\t\tcontact,\n\t\t\tPeerUpdate::Flag::CanShareContact);\n\t}\n\n\tif (const auto i = _views.find(item); i != _views.end()) {\n\t\tfor (const auto &view : i->second) {\n\t\t\tif (const auto media = view->media()) {\n\t\t\t\tmedia->updateSharedContactUserId(contactId);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Session::unregisterContactItem(\n\t\tUserId contactId,\n\t\tnot_null<HistoryItem*> item) {\n\tif (!contactId) {\n\t\treturn;\n\t}\n\tconst auto contact = userLoaded(contactId);\n\tconst auto canShare = contact ? contact->canShareThisContact() : false;\n\n\tconst auto i = _contactItems.find(contactId);\n\tif (i != _contactItems.end()) {\n\t\tauto &items = i->second;\n\t\tif (items.remove(item) && items.empty()) {\n\t\t\t_contactItems.erase(i);\n\t\t}\n\t}\n\n\tif (contact && canShare != contact->canShareThisContact()) {\n\t\tsession().changes().peerUpdated(\n\t\t\tcontact,\n\t\t\tPeerUpdate::Flag::CanShareContact);\n\t}\n}\n\nvoid Session::registerCallItem(not_null<HistoryItem*> item) {\n\t_callItems.emplace(item);\n}\n\nvoid Session::unregisterCallItem(not_null<HistoryItem*> item) {\n\t_callItems.erase(item);\n}\n\nvoid Session::destroyAllCallItems() {\n\twhile (!_callItems.empty()) {\n\t\t(*_callItems.begin())->destroy();\n\t}\n}\n\nvoid Session::registerStoryItem(\n\t\tFullStoryId id,\n\t\tnot_null<HistoryItem*> item) {\n\t_storyItems[id].emplace(item);\n}\n\nvoid Session::unregisterStoryItem(\n\t\tFullStoryId id,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto i = _storyItems.find(id);\n\tif (i != _storyItems.end()) {\n\t\tauto &items = i->second;\n\t\tif (items.remove(item) && items.empty()) {\n\t\t\t_storyItems.erase(i);\n\t\t}\n\t}\n}\n\nvoid Session::refreshStoryItemViews(FullStoryId id) {\n\tconst auto i = _storyItems.find(id);\n\tif (i != _storyItems.end()) {\n\t\tfor (const auto &item : i->second) {\n\t\t\tif (const auto media = item->media()) {\n\t\t\t\tif (media->storyMention()) {\n\t\t\t\t\titem->updateStoryMentionText();\n\t\t\t\t}\n\t\t\t}\n\t\t\trequestItemViewRefresh(item);\n\t\t}\n\t}\n}\n\nvoid Session::documentMessageRemoved(not_null<DocumentData*> document) {\n\tif (_documentItems.find(document) != _documentItems.end()) {\n\t\treturn;\n\t}\n\tif (document->loading()) {\n\t\tdocument->cancel();\n\t}\n}\n\nvoid Session::checkPlayingAnimations() {\n\tauto check = base::flat_set<not_null<ViewElement*>>();\n\tfor (const auto &view : _heavyViewParts) {\n\t\tif (const auto media = view->media()) {\n\t\t\tif (const auto document = media->getDocument()) {\n\t\t\t\tif (document->isAnimation() || document->isVideoFile()) {\n\t\t\t\t\tcheck.emplace(view);\n\t\t\t\t}\n\t\t\t} else if (const auto photo = media->getPhoto()) {\n\t\t\t\tif (photo->hasVideo()) {\n\t\t\t\t\tcheck.emplace(view);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tfor (const auto &view : check) {\n\t\tview->media()->checkAnimation();\n\t}\n}\n\nHistoryItem *Session::findWebPageItem(not_null<WebPageData*> page) const {\n\tconst auto i = _webpageItems.find(page);\n\tif (i != _webpageItems.end()) {\n\t\tfor (const auto &item : i->second) {\n\t\t\tif (item->isRegular()) {\n\t\t\t\treturn item;\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nQString Session::findContactPhone(not_null<UserData*> contact) const {\n\tconst auto result = contact->phone();\n\treturn result.isEmpty()\n\t\t? findContactPhone(peerToUser(contact->id))\n\t\t: Ui::FormatPhone(result);\n}\n\nQString Session::findContactPhone(UserId contactId) const {\n\tconst auto i = _contactItems.find(contactId);\n\tif (i != _contactItems.end()) {\n\t\tif (const auto media = (*begin(i->second))->media()) {\n\t\t\tif (const auto contact = media->sharedContact()) {\n\t\t\t\treturn contact->phoneNumber;\n\t\t\t}\n\t\t}\n\t}\n\treturn QString();\n}\n\nbool Session::hasPendingWebPageGamePollTodoListNotification() const {\n\treturn !_webpagesUpdated.empty()\n\t\t|| !_gamesUpdated.empty()\n\t\t|| !_pollsUpdated.empty()\n\t\t|| !_todoListsUpdated.empty();\n}\n\nvoid Session::notifyWebPageUpdateDelayed(not_null<WebPageData*> page) {\n\tconst auto invoke = !hasPendingWebPageGamePollTodoListNotification();\n\t_webpagesUpdated.insert(page);\n\tif (invoke) {\n\t\tcrl::on_main(_session, [=] {\n\t\t\tsendWebPageGamePollTodoListNotifications();\n\t\t});\n\t}\n}\n\nvoid Session::notifyGameUpdateDelayed(not_null<GameData*> game) {\n\tconst auto invoke = !hasPendingWebPageGamePollTodoListNotification();\n\t_gamesUpdated.insert(game);\n\tif (invoke) {\n\t\tcrl::on_main(_session, [=] {\n\t\t\tsendWebPageGamePollTodoListNotifications();\n\t\t});\n\t}\n}\n\nvoid Session::notifyPollUpdateDelayed(not_null<PollData*> poll) {\n\tconst auto invoke = !hasPendingWebPageGamePollTodoListNotification();\n\t_pollsUpdated.insert(poll);\n\tif (invoke) {\n\t\tcrl::on_main(_session, [=] {\n\t\t\tsendWebPageGamePollTodoListNotifications();\n\t\t});\n\t}\n}\n\nvoid Session::notifyTodoListUpdateDelayed(not_null<TodoListData*> todolist) {\n\tconst auto invoke = !hasPendingWebPageGamePollTodoListNotification();\n\t_todoListsUpdated.insert(todolist);\n\tif (invoke) {\n\t\tcrl::on_main(_session, [=] {\n\t\t\tsendWebPageGamePollTodoListNotifications();\n\t\t});\n\t}\n}\n\nvoid Session::sendWebPageGamePollTodoListNotifications() {\n\tauto resize = std::vector<not_null<ViewElement*>>();\n\tfor (const auto &page : base::take(_webpagesUpdated)) {\n\t\t_webpageUpdates.fire_copy(page);\n\t\tif (const auto i = _webpageViews.find(page)\n\t\t\t; i != _webpageViews.end()) {\n\t\t\tresize.insert(end(resize), begin(i->second), end(i->second));\n\t\t}\n\t}\n\tfor (const auto &game : base::take(_gamesUpdated)) {\n\t\tif (const auto i = _gameViews.find(game); i != _gameViews.end()) {\n\t\t\tresize.insert(end(resize), begin(i->second), end(i->second));\n\t\t}\n\t}\n\tfor (const auto &poll : base::take(_pollsUpdated)) {\n\t\t_pollUpdates.fire_copy(poll);\n\t\tif (const auto i = _pollViews.find(poll); i != _pollViews.end()) {\n\t\t\tresize.insert(end(resize), begin(i->second), end(i->second));\n\t\t}\n\t}\n\tfor (const auto &todolist : base::take(_todoListsUpdated)) {\n\t\tif (const auto i = _todoListViews.find(todolist)\n\t\t\t; i != _todoListViews.end()) {\n\t\t\tresize.insert(end(resize), begin(i->second), end(i->second));\n\t\t}\n\t}\n\tfor (const auto &view : resize) {\n\t\trequestViewResize(view);\n\t}\n}\n\nrpl::producer<not_null<WebPageData*>> Session::webPageUpdates() const {\n\treturn _webpageUpdates.events();\n}\n\nrpl::producer<not_null<PollData*>> Session::pollUpdates() const {\n\treturn _pollUpdates.events();\n}\n\nvoid Session::channelDifferenceTooLong(not_null<ChannelData*> channel) {\n\t_channelDifferenceTooLong.fire_copy(channel);\n}\n\nrpl::producer<not_null<ChannelData*>> Session::channelDifferenceTooLong() const {\n\treturn _channelDifferenceTooLong.events();\n}\n\nvoid Session::registerItemView(not_null<ViewElement*> view) {\n\t_views[view->data()].push_back(view);\n}\n\nvoid Session::unregisterItemView(not_null<ViewElement*> view) {\n\tExpects(!_heavyViewParts.contains(view));\n\n\t_shownSpoilers.remove(view);\n\n\tconst auto i = _views.find(view->data());\n\tif (i != end(_views)) {\n\t\tauto &list = i->second;\n\t\tlist.erase(ranges::remove(list, view), end(list));\n\t\tif (list.empty()) {\n\t\t\t_views.erase(i);\n\t\t}\n\t}\n\n\tusing namespace HistoryView;\n\tif (Element::Hovered() == view) {\n\t\tElement::Hovered(nullptr);\n\t}\n\tif (Element::Pressed() == view) {\n\t\tElement::Pressed(nullptr);\n\t}\n\tif (Element::HoveredLink() == view) {\n\t\tElement::HoveredLink(nullptr);\n\t}\n\tif (Element::PressedLink() == view) {\n\t\tElement::PressedLink(nullptr);\n\t}\n\tif (Element::Moused() == view) {\n\t\tElement::Moused(nullptr);\n\t}\n}\n\nnot_null<Folder*> Session::folder(FolderId id) {\n\tif (const auto result = folderLoaded(id)) {\n\t\treturn result;\n\t}\n\tconst auto &[it, ok] = _folders.emplace(\n\t\tid,\n\t\tstd::make_unique<Folder>(this, id));\n\treturn it->second.get();\n}\n\nFolder *Session::folderLoaded(FolderId id) const {\n\tconst auto it = _folders.find(id);\n\treturn (it == end(_folders)) ? nullptr : it->second.get();\n}\n\nnot_null<Folder*> Session::processFolder(const MTPFolder &data) {\n\treturn data.match([&](const MTPDfolder &data) {\n\t\treturn processFolder(data);\n\t});\n}\n\nnot_null<Folder*> Session::processFolder(const MTPDfolder &data) {\n\treturn folder(data.vid().v);\n}\n\nnot_null<Dialogs::MainList*> Session::chatsListFor(\n\t\tnot_null<Dialogs::Entry*> entry) {\n\tif (const auto topic = entry->asTopic()) {\n\t\treturn topic->forum()->topicsList();\n\t} else if (const auto sublist = entry->asSublist()) {\n\t\treturn sublist->parent()->chatsList();\n\t}\n\treturn chatsList(entry->folder());\n}\n\nnot_null<Dialogs::MainList*> Session::chatsList(Data::Folder *folder) {\n\treturn folder ? folder->chatsList().get() : &_chatsList;\n}\n\nnot_null<const Dialogs::MainList*> Session::chatsList(\n\t\tData::Folder *folder) const {\n\treturn folder ? folder->chatsList() : &_chatsList;\n}\n\nnot_null<Dialogs::IndexedList*> Session::contactsList() {\n\treturn &_contactsList;\n}\n\nnot_null<Dialogs::IndexedList*> Session::contactsNoChatsList() {\n\treturn &_contactsNoChatsList;\n}\n\nvoid Session::refreshChatListEntry(Dialogs::Key key) {\n\tExpects(key.entry()->folderKnown());\n\n\tusing namespace Dialogs;\n\n\tconst auto entry = key.entry();\n\tconst auto history = entry->asHistory();\n\tconst auto topic = entry->asTopic();\n\tconst auto mainList = chatsListFor(entry);\n\tauto event = ChatListEntryRefresh{ .key = key };\n\tconst auto creating = event.existenceChanged = !entry->inChatList();\n\tif (creating && topic && topic->creating()) {\n\t\treturn;\n\t} else if (event.existenceChanged) {\n\t\tconst auto mainRow = entry->addToChatList(0, mainList);\n\t\t_contactsNoChatsList.remove(key, mainRow);\n\t} else {\n\t\tevent.moved = entry->adjustByPosInChatList(0, mainList);\n\t}\n\tif (event) {\n\t\t_chatListEntryRefreshes.fire(std::move(event));\n\t}\n\tif (!history) {\n\t\treturn;\n\t}\n\tfor (const auto &filter : _chatsFilters->list()) {\n\t\tconst auto id = filter.id();\n\t\tif (!id) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto filterList = chatsFilters().chatsList(id);\n\t\tauto event = ChatListEntryRefresh{ .key = key, .filterId = id };\n\t\tif (filter.contains(history)) {\n\t\t\tevent.existenceChanged = !entry->inChatList(id);\n\t\t\tif (event.existenceChanged) {\n\t\t\t\tentry->addToChatList(id, filterList);\n\t\t\t} else {\n\t\t\t\tevent.moved = entry->adjustByPosInChatList(id, filterList);\n\t\t\t}\n\t\t} else if (entry->inChatList(id)) {\n\t\t\tentry->removeFromChatList(id, filterList);\n\t\t\tevent.existenceChanged = true;\n\t\t}\n\t\tif (event) {\n\t\t\t_chatListEntryRefreshes.fire(std::move(event));\n\t\t}\n\t}\n\n\tif (creating) {\n\t\tif (const auto from = history->peer->migrateFrom()) {\n\t\t\tif (const auto migrated = historyLoaded(from)) {\n\t\t\t\tremoveChatListEntry(migrated);\n\t\t\t}\n\t\t}\n\t\tif (const auto forum = history->peer->forum()) {\n\t\t\tforum->preloadTopics();\n\t\t} else if (const auto monoforum = history->peer->monoforum()) {\n\t\t\tmonoforum->preloadSublists();\n\t\t}\n\t\t//if (const auto broadcast = history->peer->monoforumBroadcast()) {\n\t\t//\tif (!broadcast->isFullLoaded()) {\n\t\t//\t\tbroadcast->updateFull();\n\t\t//\t}\n\t\t//}\n\t}\n}\n\nvoid Session::removeChatListEntry(Dialogs::Key key) {\n\tusing namespace Dialogs;\n\n\tconst auto entry = key.entry();\n\tif (!entry->inChatList()) {\n\t\treturn;\n\t}\n\tAssert(entry->folderKnown());\n\n\tfor (const auto &filter : _chatsFilters->list()) {\n\t\tconst auto id = filter.id();\n\t\tif (id && entry->inChatList(id)) {\n\t\t\tentry->removeFromChatList(id, chatsFilters().chatsList(id));\n\t\t\t_chatListEntryRefreshes.fire(ChatListEntryRefresh{\n\t\t\t\t.key = key,\n\t\t\t\t.filterId = id,\n\t\t\t\t.existenceChanged = true\n\t\t\t});\n\t\t}\n\t}\n\tconst auto mainList = chatsListFor(entry);\n\tentry->removeFromChatList(0, mainList);\n\t_chatListEntryRefreshes.fire(ChatListEntryRefresh{\n\t\t.key = key,\n\t\t.existenceChanged = true\n\t});\n\tif (_contactsList.contains(key)) {\n\t\tif (!_contactsNoChatsList.contains(key)) {\n\t\t\t_contactsNoChatsList.addByName(key);\n\t\t}\n\t}\n\tif (const auto topic = key.topic()) {\n\t\tCore::App().notifications().clearFromTopic(topic);\n\t} else if (const auto history = key.history()) {\n\t\tCore::App().notifications().clearFromHistory(history);\n\t}\n}\n\nauto Session::chatListEntryRefreshes() const\n-> rpl::producer<ChatListEntryRefresh> {\n\treturn _chatListEntryRefreshes.events();\n}\n\nvoid Session::dialogsRowReplaced(DialogsRowReplacement replacement) {\n\t_dialogsRowReplacements.fire(std::move(replacement));\n}\n\nauto Session::dialogsRowReplacements() const\n-> rpl::producer<DialogsRowReplacement> {\n\treturn _dialogsRowReplacements.events();\n}\n\nvoid Session::serviceNotification(\n\t\tconst TextWithEntities &message,\n\t\tconst MTPMessageMedia &media,\n\t\tbool invertMedia) {\n\tconst auto date = base::unixtime::now();\n\tif (!peerLoaded(PeerData::kServiceNotificationsId)) {\n\t\tprocessUser(MTP_user(\n\t\t\tMTP_flags(\n\t\t\t\tMTPDuser::Flag::f_first_name\n\t\t\t\t| MTPDuser::Flag::f_phone\n\t\t\t\t| MTPDuser::Flag::f_status\n\t\t\t\t| MTPDuser::Flag::f_verified),\n\t\t\tMTP_long(peerToUser(PeerData::kServiceNotificationsId).bare),\n\t\t\tMTPlong(), // access_hash\n\t\t\tMTP_string(\"Telegram\"),\n\t\t\tMTPstring(), // last_name\n\t\t\tMTPstring(), // username\n\t\t\tMTP_string(\"42777\"),\n\t\t\tMTP_userProfilePhotoEmpty(),\n\t\t\tMTP_userStatusRecently(MTP_flags(0)),\n\t\t\tMTPint(), // bot_info_version\n\t\t\tMTPVector<MTPRestrictionReason>(),\n\t\t\tMTPstring(), // bot_inline_placeholder\n\t\t\tMTPstring(), // lang_code\n\t\t\tMTPEmojiStatus(),\n\t\t\tMTPVector<MTPUsername>(),\n\t\t\tMTPRecentStory(),\n\t\t\tMTPPeerColor(), // color\n\t\t\tMTPPeerColor(), // profile_color\n\t\t\tMTPint(), // bot_active_users\n\t\t\tMTPlong(), // bot_verification_icon\n\t\t\tMTPlong())); // send_paid_messages_stars\n\t}\n\tconst auto history = this->history(PeerData::kServiceNotificationsId);\n\tconst auto insert = [=] {\n\t\tinsertCheckedServiceNotification(message, media, date, invertMedia);\n\t};\n\tif (!history->folderKnown()) {\n\t\thistories().requestDialogEntry(history, insert);\n\t} else {\n\t\tinsert();\n\t}\n}\n\nvoid Session::insertCheckedServiceNotification(\n\t\tconst TextWithEntities &message,\n\t\tconst MTPMessageMedia &media,\n\t\tTimeId date,\n\t\tbool invertMedia) {\n\tconst auto flags = MTPDmessage::Flag::f_entities\n\t\t| MTPDmessage::Flag::f_from_id\n\t\t| MTPDmessage::Flag::f_media\n\t\t| (invertMedia\n\t\t\t? MTPDmessage::Flag::f_invert_media\n\t\t\t: MTPDmessage::Flag());\n\tconst auto localFlags = MessageFlag::ClientSideUnread\n\t\t| MessageFlag::Local;\n\tauto sending = TextWithEntities(), left = message;\n\twhile (TextUtilities::CutPart(sending, left, MaxMessageSize)) {\n\t\tconst auto id = nextLocalMessageId();\n\t\taddNewMessage(\n\t\t\tid,\n\t\t\tMTP_message(\n\t\t\t\tMTP_flags(flags),\n\t\t\t\tMTP_int(0), // Not used (would've been trimmed to 32 bits).\n\t\t\t\tpeerToMTP(PeerData::kServiceNotificationsId),\n\t\t\t\tMTPint(), // from_boosts_applied\n\t\t\t\tMTPstring(), // from_rank\n\t\t\t\tpeerToMTP(PeerData::kServiceNotificationsId),\n\t\t\t\tMTPPeer(), // saved_peer_id\n\t\t\t\tMTPMessageFwdHeader(),\n\t\t\t\tMTPlong(), // via_bot_id\n\t\t\t\tMTPlong(), // via_business_bot_id\n\t\t\t\tMTPMessageReplyHeader(),\n\t\t\t\tMTP_int(date),\n\t\t\t\tMTP_string(sending.text),\n\t\t\t\tmedia,\n\t\t\t\tMTPReplyMarkup(),\n\t\t\t\tApi::EntitiesToMTP(&session(), sending.entities),\n\t\t\t\tMTPint(), // views\n\t\t\t\tMTPint(), // forwards\n\t\t\t\tMTPMessageReplies(),\n\t\t\t\tMTPint(), // edit_date\n\t\t\t\tMTPstring(),\n\t\t\t\tMTPlong(),\n\t\t\t\tMTPMessageReactions(),\n\t\t\t\tMTPVector<MTPRestrictionReason>(),\n\t\t\t\tMTPint(), // ttl_period\n\t\t\t\tMTPint(), // quick_reply_shortcut_id\n\t\t\t\tMTPlong(), // effect\n\t\t\t\tMTPFactCheck(),\n\t\t\t\tMTPint(), // report_delivery_until_date\n\t\t\t\tMTPlong(), // paid_message_stars\n\t\t\t\tMTPSuggestedPost(),\n\t\t\t\tMTPint(), // schedule_repeat_period\n\t\t\t\tMTPstring()), // summary_from_language\n\t\t\tlocalFlags,\n\t\t\tNewMessageType::Unread);\n\t}\n\tsendHistoryChangeNotifications();\n}\n\nvoid Session::setMimeForwardIds(MessageIdsList &&list) {\n\t_mimeForwardIds = std::move(list);\n}\n\nMessageIdsList Session::takeMimeForwardIds() {\n\treturn std::move(_mimeForwardIds);\n}\n\nbool Session::updateWallpapers(const MTPaccount_WallPapers &data) {\n\treturn data.match([&](const MTPDaccount_wallPapers &data) {\n\t\tsetWallpapers(data.vwallpapers().v, data.vhash().v);\n\t\treturn true;\n\t}, [&](const MTPDaccount_wallPapersNotModified &) {\n\t\treturn false;\n\t});\n}\n\nvoid Session::setWallpapers(const QVector<MTPWallPaper> &data, uint64 hash) {\n\t_wallpapersHash = hash;\n\n\t_wallpapers.clear();\n\t_wallpapers.reserve(data.size() + 2);\n\n\t_wallpapers.push_back(Data::Legacy1DefaultWallPaper());\n\t_wallpapers.back().setLocalImageAsThumbnail(std::make_shared<Image>(\n\t\tu\":/gui/art/bg_initial.jpg\"_q));\n\tfor (const auto &paper : data) {\n\t\tif (const auto parsed = Data::WallPaper::Create(&session(), paper)) {\n\t\t\t_wallpapers.push_back(*parsed);\n\t\t}\n\t}\n\n\t// Put the legacy2 (flowers) wallpaper to the front of the list.\n\tconst auto legacy2 = ranges::find_if(\n\t\t_wallpapers,\n\t\tData::IsLegacy2DefaultWallPaper);\n\tif (legacy2 != end(_wallpapers)) {\n\t\tranges::rotate(begin(_wallpapers), legacy2, legacy2 + 1);\n\t}\n\n\t// Put the legacy3 (static gradient) wallpaper to the front of the list.\n\tconst auto legacy3 = ranges::find_if(\n\t\t_wallpapers,\n\t\tData::IsLegacy3DefaultWallPaper);\n\tif (legacy3 != end(_wallpapers)) {\n\t\tranges::rotate(begin(_wallpapers), legacy3, legacy3 + 1);\n\t}\n\n\tif (ranges::none_of(_wallpapers, Data::IsDefaultWallPaper)) {\n\t\t_wallpapers.push_back(Data::DefaultWallPaper());\n\t\t_wallpapers.back().setLocalImageAsThumbnail(std::make_shared<Image>(\n\t\t\tu\":/gui/art/bg_thumbnail.png\"_q));\n\t}\n}\n\nvoid Session::removeWallpaper(const WallPaper &paper) {\n\tconst auto i = ranges::find(_wallpapers, paper.id(), &WallPaper::id);\n\tif (i != end(_wallpapers)) {\n\t\t_wallpapers.erase(i);\n\t}\n}\n\nconst std::vector<WallPaper> &Session::wallpapers() const {\n\treturn _wallpapers;\n}\n\nuint64 Session::wallpapersHash() const {\n\treturn _wallpapersHash;\n}\n\nMTP::DcId Session::statsDcId(not_null<PeerData*> peer) {\n\tconst auto it = _peerStatsDcIds.find(peer);\n\treturn (it == end(_peerStatsDcIds)) ? MTP::DcId(0) : it->second;\n}\n\nvoid Session::applyStatsDcId(\n\t\tnot_null<PeerData*> peer,\n\t\tMTP::DcId dcId) {\n\tif (dcId != peer->session().mainDcId()) {\n\t\t_peerStatsDcIds[peer] = dcId;\n\t}\n}\n\nvoid Session::saveViewAsMessages(\n\t\tnot_null<Forum*> forum,\n\t\tbool viewAsMessages) {\n\tconst auto channel = forum->channel();\n\tif (!channel) {\n\t\treturn;\n\t}\n\tif (const auto requestId = _viewAsMessagesRequests.take(channel)) {\n\t\t_session->api().request(*requestId).cancel();\n\t}\n\t_viewAsMessagesRequests[channel] = _session->api().request(\n\t\tMTPchannels_ToggleViewForumAsMessages(\n\t\t\tchannel->inputChannel(),\n\t\t\tMTP_bool(viewAsMessages))\n\t).done([=] {\n\t\t_viewAsMessagesRequests.remove(channel);\n\t}).fail([=] {\n\t\t_viewAsMessagesRequests.remove(channel);\n\t}).send();\n\tchannel->setViewAsMessagesFlag(viewAsMessages);\n}\n\nvoid Session::webViewResultSent(WebViewResultSent &&sent) {\n\treturn _webViewResultSent.fire(std::move(sent));\n}\n\nauto Session::webViewResultSent() const -> rpl::producer<WebViewResultSent> {\n\treturn _webViewResultSent.events();\n}\n\nrpl::producer<not_null<PeerData*>> Session::peerDecorationsUpdated() const {\n\treturn _peerDecorationsUpdated.events();\n}\n\nvoid Session::viewTagsChanged(\n\t\tnot_null<ViewElement*> view,\n\t\tstd::vector<Data::ReactionId> &&was,\n\t\tstd::vector<Data::ReactionId> &&now) {\n\tfor (const auto &id : now) {\n\t\tconst auto i = ranges::remove(was, id);\n\t\tif (i != end(was)) {\n\t\t\twas.erase(i, end(was));\n\t\t} else {\n\t\t\t_viewsByTag[id].emplace(view);\n\t\t}\n\t}\n\tfor (const auto &id : was) {\n\t\tconst auto i = _viewsByTag.find(id);\n\t\tif (i != end(_viewsByTag)\n\t\t\t&& i->second.remove(view)\n\t\t\t&& i->second.empty()) {\n\t\t\t_viewsByTag.erase(i);\n\t\t}\n\t}\n}\n\nvoid Session::sentToScheduled(SentToScheduled value) {\n\t_sentToScheduled.fire(std::move(value));\n}\n\nrpl::producer<SentToScheduled> Session::sentToScheduled() const {\n\treturn _sentToScheduled.events();\n}\n\nvoid Session::sentFromScheduled(SentFromScheduled value) {\n\t_sentFromScheduled.fire(std::move(value));\n}\n\nrpl::producer<SentFromScheduled> Session::sentFromScheduled() const {\n\treturn _sentFromScheduled.events();\n}\n\nvoid Session::editStarsPerMessage(\n\t\tnot_null<ChannelData*> channel,\n\t\tint count) {\n\t// For admin it's zero, we're admin if we can edit it.\n\tchannel->setStarsPerMessage(0);\n\tif (count) {\n\t\t_commonStarsPerMessage[channel] = count;\n\t} else {\n\t\t_commonStarsPerMessage.remove(channel);\n\t}\n}\n\nint Session::commonStarsPerMessage(\n\t\tnot_null<const ChannelData*> channel) const {\n\tconst auto i = _commonStarsPerMessage.find(channel);\n\treturn (i != end(_commonStarsPerMessage)) ? i->second : 0;\n}\n\nvoid Session::setPendingStarsRating(StarsRatingPending value) {\n\t_pendingStarsRating = value\n\t\t? std::make_unique<StarsRatingPending>(value)\n\t\t: nullptr;\n}\n\nStarsRatingPending Session::pendingStarsRating() const {\n\treturn _pendingStarsRating ? *_pendingStarsRating : StarsRatingPending();\n}\n\nvoid Session::addRecentSelfForwards(const RecentSelfForwards &data) {\n\t_recentSelfForwards.fire_copy(data);\n}\n\nrpl::producer<RecentSelfForwards> Session::recentSelfForwards() const {\n\treturn _recentSelfForwards.events();\n}\n\nvoid Session::addRecentJoinChat(const RecentJoinChat &data) {\n\t_recentJoinChat.fire_copy(data);\n}\n\nrpl::producer<RecentJoinChat> Session::recentJoinChat() const {\n\treturn _recentJoinChat.events();\n}\n\nvoid Session::clearLocalStorage() {\n\t_cache->close();\n\t_cache->clear();\n\t_bigFileCache->close();\n\t_bigFileCache->clear();\n}\n\nvoid Session::fillMessagePeer(FullMsgId fullId, PeerId peerId) {\n\tif (!peerLoaded(peerId) && fullId.peer != peerId) {\n\t\t_messagesWithPeer[peerId].push_back(fullId);\n\t}\n}\n\nvoid Session::fillForwardedInfo(\n\t\tFullMsgId fullId,\n\t\tconst MTPMessageFwdHeader &header) {\n\treturn header.match([&](const MTPDmessageFwdHeader &data) {\n\t\tif (const auto fromId = data.vfrom_id()) {\n\t\t\tfillMessagePeer(fullId, peerFromMTP(*fromId));\n\t\t}\n\t});\n}\n\nvoid Session::fillMentionUsers(\n\t\tFullMsgId fullId,\n\t\tconst MTPVector<MTPMessageEntity> &entities) {\n\tfor (const auto &entity : entities.v) {\n\t\tentity.match([&](const MTPDmessageEntityMentionName &data) {\n\t\t\tfillMessagePeer(fullId, peerFromUser(data.vuser_id()));\n\t\t}, [&](const MTPDinputMessageEntityMentionName &data) {\n\t\t\tdata.vuser_id().match([&](const MTPDinputUser &data) {\n\t\t\t\tfillMessagePeer(fullId, peerFromUser(data.vuser_id()));\n\t\t\t}, [](const auto &) {});\n\t\t}, [](const auto &) {});\n\t}\n}\n\nvoid Session::fillMessagePeers(PeerId peerId, const MTPMessage &message) {\n\tconst auto id = IdFromMessage(message);\n\tif (!IsServerMsgId(id)) {\n\t\treturn;\n\t}\n\tconst auto fullId = FullMsgId(peerId, id);\n\tfillMessagePeer(fullId, peerId);\n\treturn message.match([&](const MTPDmessage &data) {\n\t\tif (const auto fromId = data.vfrom_id()) {\n\t\t\tfillMessagePeer(fullId, peerFromMTP(*fromId));\n\t\t}\n\t\tif (const auto viaBotId = data.vvia_bot_id()) {\n\t\t\tfillMessagePeer(fullId, peerFromUser(*viaBotId));\n\t\t}\n\t\tif (const auto fwd = data.vfwd_from()) {\n\t\t\tfillForwardedInfo(fullId, *fwd);\n\t\t}\n\t\tif (const auto entities = data.ventities()) {\n\t\t\tfillMentionUsers(fullId, *entities);\n\t\t}\n\t}, [&](const MTPDmessageService &data) {\n\t\tif (const auto fromId = data.vfrom_id()) {\n\t\t\tfillMessagePeer(fullId, peerFromMTP(*fromId));\n\t\t}\n\t\treturn data.vaction().match(\n\t\t[&](const MTPDmessageActionChatAddUser &data) {\n\t\t\tfor (const auto &userId : data.vusers().v) {\n\t\t\t\tfillMessagePeer(fullId, peerFromUser(userId));\n\t\t\t}\n\t\t}, [&](const MTPDmessageActionChatJoinedByLink &data) {\n\t\t\tfillMessagePeer(fullId, peerFromUser(data.vinviter_id()));\n\t\t}, [&](const MTPDmessageActionChatDeleteUser &data) {\n\t\t\tfillMessagePeer(fullId, peerFromUser(data.vuser_id()));\n\t\t}, [](const auto &) {\n\t\t});\n\t}, [](const MTPDmessageEmpty &) {\n\t});\n}\n\nvoid Session::fillMessagePeers(const MTPDupdateShortMessage &data) {\n\tconst auto id = MsgId(data.vid().v);\n\tconst auto peerId = peerFromUser(data.vuser_id());\n\tif (!id || !peerId) {\n\t\treturn;\n\t}\n\tconst auto fullId = FullMsgId(peerId, id);\n\tfillMessagePeer(fullId, peerId);\n\tif (const auto viaBotId = data.vvia_bot_id()) {\n\t\tfillMessagePeer(fullId, peerFromUser(*viaBotId));\n\t}\n\tif (const auto fwd = data.vfwd_from()) {\n\t\tfillForwardedInfo(fullId, *fwd);\n\t}\n\tif (const auto entities = data.ventities()) {\n\t\tfillMentionUsers(fullId, *entities);\n\t}\n}\n\nvoid Session::fillMessagePeers(const MTPDupdateShortChatMessage &data) {\n\tconst auto id = MsgId(data.vid().v);\n\tconst auto peerId = peerFromChat(data.vchat_id());\n\tif (!id || !peerId) {\n\t\treturn;\n\t}\n\tconst auto fullId = FullMsgId(peerId, id);\n\tfillMessagePeer(fullId, peerId);\n\tfillMessagePeer(fullId, peerFromUser(data.vfrom_id()));\n\tif (const auto viaBotId = data.vvia_bot_id()) {\n\t\tfillMessagePeer(fullId, peerFromUser(*viaBotId));\n\t}\n\tif (const auto fwd = data.vfwd_from()) {\n\t\tfillForwardedInfo(fullId, *fwd);\n\t}\n\tif (const auto entities = data.ventities()) {\n\t\tfillMentionUsers(fullId, *entities);\n\t}\n}\n\nvoid Session::fillMessagePeers(\n\t\tFullMsgId fullId,\n\t\tconst MTPDupdateShortSentMessage &data) {\n\tif (const auto entities = data.ventities()) {\n\t\tfillMentionUsers(fullId, *entities);\n\t}\n}\n\nHistoryItem *Session::messageWithPeer(PeerId id) const {\n\tif (id == _session->userPeerId()) {\n\t\treturn nullptr;\n\t}\n\tconst auto i = _messagesWithPeer.find(id);\n\tif (i == end(_messagesWithPeer)) {\n\t\treturn nullptr;\n\t}\n\tauto &list = i->second;\n\tfor (auto j = begin(list); j != end(list);) {\n\t\tif (const auto item = message(*j)) {\n\t\t\treturn item;\n\t\t}\n\t\tj = list.erase(j);\n\t}\n\t_messagesWithPeer.erase(i);\n\treturn nullptr;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_session.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"storage/storage_databases.h\"\n#include \"dialogs/dialogs_main_list.h\"\n#include \"data/data_groups.h\"\n#include \"data/data_cloud_file.h\"\n#include \"data/data_star_gift.h\"\n#include \"history/history_location_manager.h\"\n#include \"base/timer.h\"\n\nclass Image;\nclass HistoryItem;\nstruct WebPageCollage;\nstruct WebPageStickerSet;\nstruct WebPageAuction;\nenum class WebPageType : uint8;\nenum class NewMessageType;\n\nnamespace Calls {\nclass GroupCall;\n} // namespace Calls\n\nnamespace HistoryView {\nstruct Group;\nclass Element;\nclass ElementDelegate;\n} // namespace HistoryView\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass BoxContent;\n} // namespace Ui\n\nnamespace Passport {\nstruct SavedCredentials;\n} // namespace Passport\n\nnamespace Iv {\nclass Data;\n} // namespace Iv\n\nnamespace Data {\n\nclass Folder;\nclass LocationPoint;\nclass WallPaper;\nclass ShortcutMessages;\nclass SendActionManager;\nclass Reactions;\nclass EmojiStatuses;\nclass ForumIcons;\nclass ChatFilters;\nclass CloudThemes;\nclass Streaming;\nclass MediaRotation;\nclass Histories;\nclass DocumentMedia;\nclass PhotoMedia;\nclass Stickers;\nclass GroupCall;\nclass NotifySettings;\nclass CustomEmojiManager;\nclass Stories;\nclass SavedMusic;\nclass SavedMessages;\nclass Chatbots;\nclass BusinessInfo;\nstruct ReactionId;\nstruct UnavailableReason;\nstruct CreditsStatusSlice;\nstruct StarsRatingPending;\nstruct UniqueGift;\n\nstruct RepliesReadTillUpdate {\n\tFullMsgId id;\n\tMsgId readTillId;\n\tbool out = false;\n};\n\nstruct SublistReadTillUpdate {\n\tChannelId parentChatId;\n\tPeerId sublistPeerId;\n\tMsgId readTillId;\n\tbool out = false;\n};\n\nstruct GiftUpdate {\n\tenum class Action : uchar {\n\t\tSave,\n\t\tUnsave,\n\t\tConvert,\n\t\tTransfer,\n\t\tDelete,\n\t\tPin,\n\t\tUnpin,\n\t\tResaleChange,\n\t\tUpgraded,\n\t};\n\n\tData::SavedStarGiftId id;\n\tQString slug;\n\tAction action = {};\n};\nstruct GiftsUpdate {\n\tnot_null<PeerData*> peer;\n\tint collectionId = 0;\n\tstd::vector<Data::SavedStarGiftId> added;\n\tstd::vector<Data::SavedStarGiftId> removed;\n};\nstruct GiftAuctionGot {\n\tuint64 giftId = 0;\n\tnot_null<PeerData*> to;\n};\n\nstruct SentToScheduled {\n\tnot_null<History*> history;\n\tMsgId scheduledId = 0;\n};\nstruct SentFromScheduled {\n\tnot_null<HistoryItem*> item;\n\tMsgId sentId = 0;\n};\n\nstruct RecentSelfForwards {\n\tPeerId fromPeerId = 0;\n\tMessageIdsList ids;\n};\n\nstruct RecentJoinChat {\n\tPeerId fromPeerId = 0;\n\tPeerId joinedPeerId = 0;\n};\n\nstruct DrawToReplyRequest {\n\tFullMsgId messageId;\n\tuint64 photoId = 0;\n\tuint64 documentId = 0;\n};\n\nstruct RequestViewRepaint {\n\tnot_null<const HistoryView::Element*> view;\n\tQRect rect;\n};\n\nclass Session final {\npublic:\n\tusing ViewElement = HistoryView::Element;\n\n\tstruct SentData {\n\t\tPeerId peerId = 0;\n\t\tQString text;\n\t};\n\n\texplicit Session(not_null<Main::Session*> session);\n\t~Session();\n\n\t[[nodiscard]] Main::Session &session() const {\n\t\treturn *_session;\n\t}\n\n\t[[nodiscard]] Groups &groups() {\n\t\treturn _groups;\n\t}\n\t[[nodiscard]] const Groups &groups() const {\n\t\treturn _groups;\n\t}\n\t[[nodiscard]] ChatFilters &chatsFilters() const {\n\t\treturn *_chatsFilters;\n\t}\n\t[[nodiscard]] ShortcutMessages &shortcutMessages() const {\n\t\treturn *_shortcutMessages;\n\t}\n\t[[nodiscard]] SendActionManager &sendActionManager() const {\n\t\treturn *_sendActionManager;\n\t}\n\t[[nodiscard]] CloudThemes &cloudThemes() const {\n\t\treturn *_cloudThemes;\n\t}\n\t[[nodiscard]] Streaming &streaming() const {\n\t\treturn *_streaming;\n\t}\n\t[[nodiscard]] MediaRotation &mediaRotation() const {\n\t\treturn *_mediaRotation;\n\t}\n\t[[nodiscard]] Histories &histories() const {\n\t\treturn *_histories;\n\t}\n\t[[nodiscard]] Stickers &stickers() const {\n\t\treturn *_stickers;\n\t}\n\t[[nodiscard]] Reactions &reactions() const {\n\t\treturn *_reactions;\n\t}\n\t[[nodiscard]] EmojiStatuses &emojiStatuses() const {\n\t\treturn *_emojiStatuses;\n\t}\n\t[[nodiscard]] ForumIcons &forumIcons() const {\n\t\treturn *_forumIcons;\n\t}\n\t[[nodiscard]] NotifySettings &notifySettings() const {\n\t\treturn *_notifySettings;\n\t}\n\t[[nodiscard]] CustomEmojiManager &customEmojiManager() const {\n\t\treturn *_customEmojiManager;\n\t}\n\t[[nodiscard]] Stories &stories() const {\n\t\treturn *_stories;\n\t}\n\t[[nodiscard]] SavedMusic &savedMusic() const {\n\t\treturn *_savedMusic;\n\t}\n\t[[nodiscard]] SavedMessages &savedMessages() const {\n\t\treturn *_savedMessages;\n\t}\n\t[[nodiscard]] Chatbots &chatbots() const {\n\t\treturn *_chatbots;\n\t}\n\t[[nodiscard]] BusinessInfo &businessInfo() const {\n\t\treturn *_businessInfo;\n\t}\n\n\t[[nodiscard]] MsgId nextNonHistoryEntryId() {\n\t\treturn ++_nonHistoryEntryId;\n\t}\n\n\tvoid subscribeForTopicRepliesLists();\n\tvoid clear();\n\n\tvoid keepAlive(std::shared_ptr<PhotoMedia> media);\n\tvoid keepAlive(std::shared_ptr<DocumentMedia> media);\n\n\tvoid suggestStartExport(TimeId availableAt);\n\tvoid clearExportSuggestion();\n\n\t[[nodiscard]] auto passportCredentials() const\n\t-> const Passport::SavedCredentials*;\n\tvoid rememberPassportCredentials(\n\t\tPassport::SavedCredentials data,\n\t\tcrl::time rememberFor);\n\tvoid forgetPassportCredentials();\n\n\t[[nodiscard]] Storage::Cache::Database &cache();\n\t[[nodiscard]] Storage::Cache::Database &cacheBigFile();\n\n\t[[nodiscard]] not_null<PeerData*> peer(PeerId id);\n\t[[nodiscard]] not_null<PeerData*> peer(UserId id) = delete;\n\t[[nodiscard]] not_null<UserData*> user(UserId id);\n\t[[nodiscard]] not_null<ChatData*> chat(ChatId id);\n\t[[nodiscard]] not_null<ChannelData*> channel(ChannelId id);\n\t[[nodiscard]] not_null<UserData*> user(PeerId id) = delete;\n\t[[nodiscard]] not_null<ChatData*> chat(PeerId id) = delete;\n\t[[nodiscard]] not_null<ChannelData*> channel(PeerId id) = delete;\n\n\t[[nodiscard]] PeerData *peerLoaded(PeerId id) const;\n\t[[nodiscard]] PeerData *peerLoaded(UserId id) const = delete;\n\t[[nodiscard]] UserData *userLoaded(UserId id) const;\n\t[[nodiscard]] ChatData *chatLoaded(ChatId id) const;\n\t[[nodiscard]] ChannelData *channelLoaded(ChannelId id) const;\n\t[[nodiscard]] UserData *userLoaded(PeerId id) const = delete;\n\t[[nodiscard]] ChatData *chatLoaded(PeerId id) const = delete;\n\t[[nodiscard]] ChannelData *channelLoaded(PeerId id) const = delete;\n\n\tnot_null<UserData*> processUser(const MTPUser &data);\n\tnot_null<PeerData*> processChat(const MTPChat &data);\n\n\t// Returns last user, if there were any.\n\tUserData *processUsers(const MTPVector<MTPUser> &data);\n\tPeerData *processChats(const MTPVector<MTPChat> &data);\n\n\tvoid applyMaximumChatVersions(const MTPVector<MTPChat> &data);\n\n\tvoid registerGroupCall(not_null<GroupCall*> call);\n\tvoid unregisterGroupCall(not_null<GroupCall*> call);\n\t[[nodiscard]] GroupCall *groupCall(CallId callId) const;\n\n\t[[nodiscard]] std::shared_ptr<GroupCall> sharedConferenceCall(\n\t\tCallId id,\n\t\tuint64 accessHash);\n\t[[nodiscard]] std::shared_ptr<GroupCall> sharedConferenceCallFind(\n\t\tconst MTPUpdates &response);\n\n\tvoid watchForOffline(not_null<UserData*> user, TimeId now = 0);\n\tvoid maybeStopWatchForOffline(not_null<UserData*> user);\n\n\tvoid recordSharingDisabledTime(not_null<UserData*> user);\n\t[[nodiscard]] bool sharingRecentlyDisabledByMe(\n\t\tnot_null<UserData*> user) const;\n\tvoid clearSharingDisabledTime(not_null<UserData*> user);\n\n\t[[nodiscard]] auto invitedToCallUsers(CallId callId) const\n\t\t-> const base::flat_map<not_null<UserData*>, bool> &;\n\tvoid registerInvitedToCallUser(\n\t\tCallId callId,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user,\n\t\tbool calling);\n\tvoid registerInvitedToCallUser(\n\t\tCallId callId,\n\t\tGroupCall *call,\n\t\tnot_null<UserData*> user,\n\t\tbool calling);\n\tvoid unregisterInvitedToCallUser(\n\t\tCallId callId,\n\t\tnot_null<UserData*> user,\n\t\tbool onlyStopCalling);\n\n\tstruct InviteToCall {\n\t\tCallId id = 0;\n\t\tnot_null<UserData*> user;\n\t\tbool calling = false;\n\t\tbool removed = false;\n\t};\n\t[[nodiscard]] rpl::producer<InviteToCall> invitesToCalls() const {\n\t\treturn _invitesToCalls.events();\n\t}\n\n\tvoid enumerateUsers(Fn<void(not_null<UserData*>)> action) const;\n\tvoid enumerateGroups(Fn<void(not_null<PeerData*>)> action) const;\n\tvoid enumerateBroadcasts(Fn<void(not_null<ChannelData*>)> action) const;\n\t[[nodiscard]] UserData *userByPhone(const QString &phone) const;\n\t[[nodiscard]] PeerData *peerByUsername(const QString &username) const;\n\n\t[[nodiscard]] not_null<History*> history(PeerId peerId);\n\t[[nodiscard]] History *historyLoaded(PeerId peerId) const;\n\t[[nodiscard]] not_null<History*> history(UserId userId) = delete;\n\t[[nodiscard]] History *historyLoaded(UserId userId) const = delete;\n\t[[nodiscard]] not_null<History*> history(not_null<const PeerData*> peer);\n\t[[nodiscard]] History *historyLoaded(const PeerData *peer);\n\n\tvoid deleteConversationLocally(not_null<PeerData*> peer);\n\n\t[[nodiscard]] rpl::variable<bool> &contactsLoaded() {\n\t\treturn _contactsLoaded;\n\t}\n\t[[nodiscard]] rpl::producer<Folder*> chatsListChanges() const {\n\t\treturn _chatsListChanged.events();\n\t}\n\t[[nodiscard]] bool chatsListLoaded(Folder *folder = nullptr);\n\t[[nodiscard]] rpl::producer<Folder*> chatsListLoadedEvents() const {\n\t\treturn _chatsListLoadedEvents.events();\n\t}\n\tvoid chatsListChanged(FolderId folderId);\n\tvoid chatsListChanged(Folder *folder);\n\tvoid chatsListDone(Folder *folder);\n\n\tvoid userIsBotChanged(not_null<UserData*> user);\n\t[[nodiscard]] rpl::producer<not_null<UserData*>> userIsBotChanges() const;\n\tvoid botCommandsChanged(not_null<PeerData*> peer);\n\t[[nodiscard]] rpl::producer<not_null<PeerData*>> botCommandsChanges() const;\n\n\tstruct ItemVisibilityQuery {\n\t\tnot_null<HistoryItem*> item;\n\t\tnot_null<bool*> isVisible;\n\t};\n\t[[nodiscard]] bool queryItemVisibility(not_null<HistoryItem*> item) const;\n\t[[nodiscard]] bool queryDocumentVisibility(not_null<DocumentData*> document) const;\n\t[[nodiscard]] rpl::producer<ItemVisibilityQuery> itemVisibilityQueries() const;\n\tvoid itemVisibilitiesUpdated();\n\n\tstruct IdChange {\n\t\tFullMsgId newId;\n\t\tMsgId oldId = 0;\n\t};\n\tvoid notifyItemIdChange(IdChange event);\n\t[[nodiscard]] rpl::producer<IdChange> itemIdChanged() const;\n\tvoid notifyItemLayoutChange(not_null<const HistoryItem*> item);\n\t[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemLayoutChanged() const;\n\tvoid notifyViewLayoutChange(not_null<const ViewElement*> view);\n\t[[nodiscard]] rpl::producer<not_null<const ViewElement*>> viewLayoutChanged() const;\n\tvoid notifyNewItemAdded(not_null<HistoryItem*> item);\n\t[[nodiscard]] rpl::producer<not_null<HistoryItem*>> newItemAdded() const;\n\tvoid notifyGiftUpdate(GiftUpdate &&update);\n\t[[nodiscard]] rpl::producer<GiftUpdate> giftUpdates() const;\n\tvoid notifyGiftsUpdate(GiftsUpdate &&update);\n\t[[nodiscard]] rpl::producer<GiftsUpdate> giftsUpdates() const;\n\tvoid notifyGiftAuctionGot(GiftAuctionGot &&update);\n\t[[nodiscard]] rpl::producer<GiftAuctionGot> giftAuctionGots() const;\n\tvoid requestItemRepaint(not_null<const HistoryItem*> item, QRect r = QRect());\n\t[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemRepaintRequest() const;\n\tvoid requestDrawToReply(DrawToReplyRequest request);\n\t[[nodiscard]] rpl::producer<DrawToReplyRequest> drawToReplyRequests() const;\n\tvoid requestViewRepaint(not_null<const ViewElement*> view, QRect r = QRect());\n\t[[nodiscard]] rpl::producer<RequestViewRepaint> viewRepaintRequest() const;\n\tvoid requestItemResize(not_null<const HistoryItem*> item);\n\t[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemResizeRequest() const;\n\tvoid requestViewResize(not_null<ViewElement*> view);\n\t[[nodiscard]] rpl::producer<not_null<ViewElement*>> viewResizeRequest() const;\n\tstruct ViewHeightAdjusted {\n\t\tnot_null<ViewElement*> view;\n\t\tint delta = 0;\n\t};\n\tvoid notifyViewHeightAdjusted(not_null<ViewElement*> view, int delta);\n\t[[nodiscard]] rpl::producer<ViewHeightAdjusted> viewHeightAdjusted() const;\n\tvoid requestItemShowHighlight(not_null<HistoryItem*> item);\n\t[[nodiscard]] rpl::producer<not_null<HistoryItem*>> itemShowHighlightRequest() const;\n\tvoid requestItemViewRefresh(not_null<const HistoryItem*> item);\n\t[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemViewRefreshRequest() const;\n\tvoid requestItemTextRefresh(not_null<HistoryItem*> item);\n\tvoid requestUnreadReactionsAnimation(not_null<HistoryItem*> item);\n\tvoid notifyHistoryUnloaded(not_null<const History*> history);\n\t[[nodiscard]] rpl::producer<not_null<const History*>> historyUnloaded() const;\n\tvoid notifyItemDataChange(not_null<HistoryItem*> item);\n\t[[nodiscard]] rpl::producer<not_null<HistoryItem*>> itemDataChanges() const;\n\n\t[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemRemoved() const;\n\t[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemRemoved(\n\t\tFullMsgId itemId) const;\n\tvoid notifyItemsAboutToBeDestroyed(\n\t\tconst std::vector<not_null<HistoryItem*>> &items);\n\t[[nodiscard]] auto itemsAboutToBeDestroyed() const\n\t\t-> rpl::producer<std::vector<not_null<HistoryItem*>>>;\n\tvoid notifyViewAboutToBeRemoved(not_null<const ViewElement*> view);\n\t[[nodiscard]] rpl::producer<not_null<const ViewElement*>> viewAboutToBeRemoved() const;\n\tvoid notifyViewRemoved(not_null<const ViewElement*> view);\n\t[[nodiscard]] rpl::producer<not_null<const ViewElement*>> viewRemoved() const;\n\tvoid notifyHistoryCleared(not_null<const History*> history);\n\t[[nodiscard]] rpl::producer<not_null<const History*>> historyCleared() const;\n\tvoid notifyHistoryChangeDelayed(not_null<History*> history);\n\t[[nodiscard]] rpl::producer<not_null<History*>> historyChanged() const;\n\tvoid notifyViewPaidReactionSent(not_null<const ViewElement*> view);\n\t[[nodiscard]] rpl::producer<not_null<const ViewElement*>> viewPaidReactionSent() const;\n\tvoid notifyCallPaidReactionSent(not_null<Calls::GroupCall*> call);\n\t[[nodiscard]] rpl::producer<not_null<Calls::GroupCall*>> callPaidReactionSent() const;\n\tvoid sendHistoryChangeNotifications();\n\n\tvoid notifyPinnedDialogsOrderUpdated();\n\t[[nodiscard]] rpl::producer<> pinnedDialogsOrderUpdated() const;\n\n\tvoid nextForUpgradeGiftInvalidate(not_null<PeerData*> owner);\n\tvoid nextForUpgradeGiftRequest(\n\t\tnot_null<PeerData*> owner,\n\t\tFn<void(std::optional<Data::SavedStarGift>)> done);\n\n\tusing CreditsSubsRebuilder = rpl::event_stream<CreditsStatusSlice>;\n\tusing CreditsSubsRebuilderPtr = std::shared_ptr<CreditsSubsRebuilder>;\n\t[[nodiscard]] CreditsSubsRebuilderPtr createCreditsSubsRebuilder();\n\t[[nodiscard]] CreditsSubsRebuilderPtr activeCreditsSubsRebuilder() const;\n\n\tvoid registerRestricted(\n\t\tnot_null<const HistoryItem*> item,\n\t\tconst QString &reason);\n\tvoid registerRestricted(\n\t\tnot_null<const HistoryItem*> item,\n\t\tconst std::vector<UnavailableReason> &reasons);\n\n\tvoid registerHighlightProcess(\n\t\tuint64 processId,\n\t\tnot_null<HistoryItem*> item);\n\n\tvoid registerHeavyViewPart(not_null<ViewElement*> view);\n\tvoid unregisterHeavyViewPart(not_null<ViewElement*> view);\n\tvoid unloadHeavyViewParts(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate);\n\tvoid unloadHeavyViewParts(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate,\n\t\tint from,\n\t\tint till);\n\n\tvoid registerShownSpoiler(not_null<ViewElement*> view);\n\tvoid hideShownSpoilers();\n\n\tusing MegagroupParticipant = std::tuple<\n\t\tnot_null<ChannelData*>,\n\t\tnot_null<UserData*>>;\n\tvoid removeMegagroupParticipant(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<UserData*> user);\n\t[[nodiscard]] rpl::producer<MegagroupParticipant> megagroupParticipantRemoved() const;\n\t[[nodiscard]] rpl::producer<not_null<UserData*>> megagroupParticipantRemoved(\n\t\tnot_null<ChannelData*> channel) const;\n\tvoid addNewMegagroupParticipant(\n\t\tnot_null<ChannelData*> channel,\n\t\tnot_null<UserData*> user);\n\t[[nodiscard]] rpl::producer<MegagroupParticipant> megagroupParticipantAdded() const;\n\t[[nodiscard]] rpl::producer<not_null<UserData*>> megagroupParticipantAdded(\n\t\tnot_null<ChannelData*> channel) const;\n\n\tHistoryItemsList idsToItems(const MessageIdsList &ids) const;\n\tMessageIdsList itemsToIds(const HistoryItemsList &items) const;\n\tMessageIdsList itemOrItsGroup(not_null<HistoryItem*> item) const;\n\n\tvoid applyUpdate(const MTPDupdateMessagePoll &update);\n\tvoid applyUpdate(const MTPDupdateChatParticipants &update);\n\tvoid applyUpdate(const MTPDupdateChatParticipantAdd &update);\n\tvoid applyUpdate(const MTPDupdateChatParticipantDelete &update);\n\tvoid applyUpdate(const MTPDupdateChatParticipantAdmin &update);\n\tvoid applyUpdate(const MTPDupdateChatParticipantRank &update);\n\tvoid applyUpdate(const MTPDupdateChatDefaultBannedRights &update);\n\n\tvoid applyDialogs(\n\t\tFolder *requestFolder,\n\t\tconst QVector<MTPMessage> &messages,\n\t\tconst QVector<MTPDialog> &dialogs,\n\t\tstd::optional<int> count = std::nullopt);\n\n\t[[nodiscard]] bool pinnedCanPin(not_null<Dialogs::Entry*> entry) const;\n\t[[nodiscard]] bool pinnedCanPin(\n\t\tFilterId filterId,\n\t\tnot_null<History*> history) const;\n\t[[nodiscard]] int pinnedChatsLimit(Folder *folder) const;\n\t[[nodiscard]] int pinnedChatsLimit(FilterId filterId) const;\n\t[[nodiscard]] int pinnedChatsLimit(not_null<Forum*> forum) const;\n\t[[nodiscard]] int pinnedChatsLimit(\n\t\tnot_null<SavedMessages*> saved) const;\n\t[[nodiscard]] rpl::producer<int> maxPinnedChatsLimitValue(\n\t\tFolder *folder) const;\n\t[[nodiscard]] rpl::producer<int> maxPinnedChatsLimitValue(\n\t\tFilterId filterId) const;\n\t[[nodiscard]] rpl::producer<int> maxPinnedChatsLimitValue(\n\t\tnot_null<Forum*> forum) const;\n\t[[nodiscard]] rpl::producer<int> maxPinnedChatsLimitValue(\n\t\tnot_null<SavedMessages*> saved) const;\n\t[[nodiscard]] int groupFreeTranscribeLevel() const;\n\t[[nodiscard]] const std::vector<Dialogs::Key> &pinnedChatsOrder(\n\t\tFolder *folder) const;\n\t[[nodiscard]] const std::vector<Dialogs::Key> &pinnedChatsOrder(\n\t\tnot_null<Forum*> forum) const;\n\t[[nodiscard]] const std::vector<Dialogs::Key> &pinnedChatsOrder(\n\t\tFilterId filterId) const;\n\t[[nodiscard]] const std::vector<Dialogs::Key> &pinnedChatsOrder(\n\t\tnot_null<SavedMessages*> saved) const;\n\tvoid setChatPinned(Dialogs::Key key, FilterId filterId, bool pinned);\n\tvoid setPinnedFromEntryList(Dialogs::Key key, bool pinned);\n\tvoid clearPinnedChats(Folder *folder);\n\tvoid applyPinnedChats(\n\t\tFolder *folder,\n\t\tconst QVector<MTPDialogPeer> &list);\n\tvoid applyPinnedTopics(\n\t\tnot_null<Forum*> forum,\n\t\tconst QVector<MTPint> &list);\n\tvoid reorderTwoPinnedChats(\n\t\tFilterId filterId,\n\t\tDialogs::Key key1,\n\t\tDialogs::Key key2);\n\n\tvoid setSuggestToGigagroup(not_null<ChannelData*> group, bool suggest);\n\t[[nodiscard]] bool suggestToGigagroup(\n\t\tnot_null<ChannelData*> group) const;\n\n\tvoid registerMessage(not_null<HistoryItem*> item);\n\tvoid unregisterMessage(not_null<HistoryItem*> item);\n\n\tvoid registerMessageTTL(TimeId when, not_null<HistoryItem*> item);\n\tvoid unregisterMessageTTL(TimeId when, not_null<HistoryItem*> item);\n\n\tvoid registerFormattedDateUpdate(\n\t\tTimeId when,\n\t\tnot_null<HistoryView::Element*> view);\n\n\t// Returns true if item found and it is not detached.\n\tbool updateExistingMessage(const MTPDmessage &data);\n\tvoid updateEditedMessage(const MTPMessage &data);\n\tvoid processMessages(\n\t\tconst QVector<MTPMessage> &data,\n\t\tNewMessageType type);\n\tvoid processMessages(\n\t\tconst MTPVector<MTPMessage> &data,\n\t\tNewMessageType type);\n\tvoid processExistingMessages(\n\t\tChannelData *channel,\n\t\tconst MTPmessages_Messages &data);\n\tvoid processNonChannelMessagesDeleted(const QVector<MTPint> &data);\n\tvoid processMessagesDeleted(\n\t\tPeerId peerId,\n\t\tconst QVector<MTPint> &data);\n\n\t[[nodiscard]] MsgId nextLocalMessageId();\n\t[[nodiscard]] HistoryItem *message(\n\t\tPeerId peerId,\n\t\tMsgId itemId) const;\n\t[[nodiscard]] HistoryItem *message(\n\t\tnot_null<const PeerData*> peer,\n\t\tMsgId itemId) const;\n\t[[nodiscard]] HistoryItem *message(FullMsgId itemId) const;\n\n\t[[nodiscard]] HistoryItem *nonChannelMessage(MsgId itemId) const;\n\n\tvoid updateDependentMessages(not_null<HistoryItem*> item);\n\tvoid registerDependentMessage(\n\t\tnot_null<HistoryItem*> dependent,\n\t\tnot_null<HistoryItem*> dependency);\n\tvoid unregisterDependentMessage(\n\t\tnot_null<HistoryItem*> dependent,\n\t\tnot_null<HistoryItem*> dependency);\n\n\tvoid destroyAllCallItems();\n\n\tvoid registerMessageRandomId(uint64 randomId, FullMsgId itemId);\n\tvoid unregisterMessageRandomId(uint64 randomId);\n\t[[nodiscard]] FullMsgId messageIdByRandomId(uint64 randomId) const;\n\tvoid registerMessageSentData(\n\t\tuint64 randomId,\n\t\tPeerId peerId,\n\t\tconst QString &text);\n\tvoid unregisterMessageSentData(uint64 randomId);\n\t[[nodiscard]] SentData messageSentData(uint64 randomId) const;\n\n\tvoid photoLoadSettingsChanged();\n\tvoid documentLoadSettingsChanged();\n\n\tvoid notifyPhotoLayoutChanged(not_null<const PhotoData*> photo);\n\tvoid requestPhotoViewRepaint(not_null<const PhotoData*> photo);\n\tvoid notifyDocumentLayoutChanged(\n\t\tnot_null<const DocumentData*> document);\n\tvoid requestDocumentViewRepaint(not_null<const DocumentData*> document);\n\tvoid markMediaRead(not_null<const DocumentData*> document);\n\tvoid requestPollViewRepaint(not_null<const PollData*> poll);\n\tvoid requestTodoListViewRepaint(not_null<const TodoListData*> todolist);\n\n\tvoid photoLoadProgress(not_null<PhotoData*> photo);\n\tvoid photoLoadDone(not_null<PhotoData*> photo);\n\tvoid photoLoadFail(not_null<PhotoData*> photo, bool started);\n\n\tvoid documentLoadProgress(not_null<DocumentData*> document);\n\tvoid documentLoadDone(not_null<DocumentData*> document);\n\tvoid documentLoadFail(not_null<DocumentData*> document, bool started);\n\n\t[[nodiscard]] auto documentLoadProgress() const\n\t-> rpl::producer<not_null<DocumentData*>> {\n\t\treturn _documentLoadProgress.events();\n\t}\n\n\tHistoryItem *addNewMessage(\n\t\tconst MTPMessage &data,\n\t\tMessageFlags localFlags,\n\t\tNewMessageType type);\n\tHistoryItem *addNewMessage( // Override message id.\n\t\tMsgId id,\n\t\tconst MTPMessage &data,\n\t\tMessageFlags localFlags,\n\t\tNewMessageType type);\n\n\t[[nodiscard]] int unreadBadge() const;\n\t[[nodiscard]] int unreadWithMentionsBadge() const;\n\t[[nodiscard]] bool unreadBadgeMuted() const;\n\t[[nodiscard]] bool unreadWithMentionsBadgeMuted() const;\n\t[[nodiscard]] int unreadBadgeIgnoreOne(Dialogs::Key key) const;\n\t[[nodiscard]] bool unreadBadgeMutedIgnoreOne(Dialogs::Key key) const;\n\t[[nodiscard]] int unreadOnlyMutedBadge() const;\n\t[[nodiscard]] rpl::producer<> unreadBadgeChanges() const;\n\tvoid notifyUnreadBadgeChanged();\n\n\tvoid updateRepliesReadTill(RepliesReadTillUpdate update);\n\t[[nodiscard]] auto repliesReadTillUpdates() const\n\t\t-> rpl::producer<RepliesReadTillUpdate>;\n\n\tvoid updateSublistReadTill(SublistReadTillUpdate update);\n\t[[nodiscard]] auto sublistReadTillUpdates() const\n\t\t-> rpl::producer<SublistReadTillUpdate>;\n\n\tvoid selfDestructIn(not_null<HistoryItem*> item, crl::time delay);\n\n\t[[nodiscard]] not_null<PhotoData*> photo(PhotoId id);\n\tnot_null<PhotoData*> processPhoto(const MTPPhoto &data);\n\tnot_null<PhotoData*> processPhoto(const MTPDphoto &data);\n\tnot_null<PhotoData*> processPhoto(\n\t\tconst MTPPhoto &data,\n\t\tconst PreparedPhotoThumbs &thumbs);\n\t[[nodiscard]] not_null<PhotoData*> photo(\n\t\tPhotoId id,\n\t\tconst uint64 &access,\n\t\tconst QByteArray &fileReference,\n\t\tTimeId date,\n\t\tint32 dc,\n\t\tbool hasStickers,\n\t\tconst QByteArray &inlineThumbnailBytes,\n\t\tconst ImageWithLocation &small,\n\t\tconst ImageWithLocation &thumbnail,\n\t\tconst ImageWithLocation &large,\n\t\tconst ImageWithLocation &videoSmall,\n\t\tconst ImageWithLocation &videoLarge,\n\t\tcrl::time videoStartTime);\n\tvoid photoConvert(\n\t\tnot_null<PhotoData*> original,\n\t\tconst MTPPhoto &data);\n\t[[nodiscard]] PhotoData *photoFromWeb(\n\t\tconst MTPWebDocument &data,\n\t\tconst ImageLocation &thumbnailLocation);\n\n\t[[nodiscard]] not_null<DocumentData*> document(DocumentId id);\n\tnot_null<DocumentData*> processDocument(\n\t\tconst MTPDocument &data,\n\t\tconst MTPVector<MTPDocument> *qualities = nullptr);\n\tnot_null<DocumentData*> processDocument(\n\t\tconst MTPDdocument &data,\n\t\tconst MTPVector<MTPDocument> *qualities = nullptr);\n\tnot_null<DocumentData*> processDocument(\n\t\tconst MTPdocument &data,\n\t\tconst ImageWithLocation &thumbnail);\n\t[[nodiscard]] not_null<DocumentData*> document(\n\t\tDocumentId id,\n\t\tconst uint64 &access,\n\t\tconst QByteArray &fileReference,\n\t\tTimeId date,\n\t\tconst QVector<MTPDocumentAttribute> &attributes,\n\t\tconst QString &mime,\n\t\tconst InlineImageLocation &inlineThumbnail,\n\t\tconst ImageWithLocation &thumbnail,\n\t\tconst ImageWithLocation &videoThumbnail,\n\t\tbool isPremiumSticker,\n\t\tint32 dc,\n\t\tint64 size);\n\tvoid documentConvert(\n\t\tnot_null<DocumentData*> original,\n\t\tconst MTPDocument &data);\n\t[[nodiscard]] DocumentData *documentFromWeb(\n\t\tconst MTPWebDocument &data,\n\t\tconst ImageLocation &thumbnailLocation,\n\t\tconst ImageLocation &videoThumbnailLocation);\n\t[[nodiscard]] not_null<DocumentData*> venueIconDocument(\n\t\tconst QString &icon);\n\n\t[[nodiscard]] not_null<WebPageData*> webpage(WebPageId id);\n\tnot_null<WebPageData*> processWebpage(const MTPWebPage &data);\n\tnot_null<WebPageData*> processWebpage(const MTPDwebPage &data);\n\tnot_null<WebPageData*> processWebpage(const MTPDwebPagePending &data);\n\t[[nodiscard]] not_null<WebPageData*> webpage(\n\t\tWebPageId id,\n\t\tconst QString &siteName,\n\t\tconst TextWithEntities &content);\n\t[[nodiscard]] not_null<WebPageData*> webpage(\n\t\tWebPageId id,\n\t\tWebPageType type,\n\t\tconst QString &url,\n\t\tconst QString &displayUrl,\n\t\tconst QString &siteName,\n\t\tconst QString &title,\n\t\tconst TextWithEntities &description,\n\t\tPhotoData *photo,\n\t\tDocumentData *document,\n\t\tWebPageCollage &&collage,\n\t\tstd::unique_ptr<Iv::Data> iv,\n\t\tstd::unique_ptr<WebPageStickerSet> stickerSet,\n\t\tstd::shared_ptr<UniqueGift> uniqueGift,\n\t\tint duration,\n\t\tconst QString &author,\n\t\tbool hasLargeMedia,\n\t\tbool photoIsVideoCover,\n\t\tTimeId pendingTill);\n\n\t[[nodiscard]] not_null<GameData*> game(GameId id);\n\tnot_null<GameData*> processGame(const MTPDgame &data);\n\t[[nodiscard]] not_null<GameData*> game(\n\t\tGameId id,\n\t\tconst uint64 &accessHash,\n\t\tconst QString &shortName,\n\t\tconst QString &title,\n\t\tconst QString &description,\n\t\tPhotoData *photo,\n\t\tDocumentData *document);\n\tvoid gameConvert(\n\t\tnot_null<GameData*> original,\n\t\tconst MTPGame &data);\n\n\t[[nodiscard]] not_null<BotAppData*> botApp(BotAppId id);\n\tBotAppData *findBotApp(PeerId botId, const QString &appName) const;\n\tBotAppData *processBotApp(\n\t\tPeerId botId,\n\t\tconst MTPBotApp &data);\n\n\t[[nodiscard]] not_null<PollData*> poll(PollId id);\n\t[[nodiscard]] HistoryItem *findItemForPoll(PollId id) const;\n\t[[nodiscard]] std::vector<not_null<PeerData*>> pollRecentVoters(\n\t\tPollId id) const;\n\tnot_null<PollData*> processPoll(const MTPPoll &data);\n\tnot_null<PollData*> processPoll(const MTPDmessageMediaPoll &data);\n\n\t[[nodiscard]] not_null<TodoListData*> todoList(TodoListId id);\n\tnot_null<TodoListData*> processTodoList(\n\t\tTodoListId id,\n\t\tconst MTPTodoList &todolist);\n\tnot_null<TodoListData*> processTodoList(\n\t\tTodoListId id,\n\t\tconst MTPDmessageMediaToDo &data);\n\t[[nodiscard]] not_null<TodoListData*> duplicateTodoList(\n\t\tTodoListId id,\n\t\tnot_null<TodoListData*> existing);\n\n\t[[nodiscard]] not_null<CloudImage*> location(\n\t\tconst LocationPoint &point);\n\n\tvoid registerPhotoItem(\n\t\tnot_null<const PhotoData*> photo,\n\t\tnot_null<HistoryItem*> item);\n\tvoid unregisterPhotoItem(\n\t\tnot_null<const PhotoData*> photo,\n\t\tnot_null<HistoryItem*> item);\n\tvoid registerDocumentItem(\n\t\tnot_null<const DocumentData*> document,\n\t\tnot_null<HistoryItem*> item);\n\tvoid unregisterDocumentItem(\n\t\tnot_null<const DocumentData*> document,\n\t\tnot_null<HistoryItem*> item);\n\tvoid registerWebPageView(\n\t\tnot_null<const WebPageData*> page,\n\t\tnot_null<ViewElement*> view);\n\tvoid unregisterWebPageView(\n\t\tnot_null<const WebPageData*> page,\n\t\tnot_null<ViewElement*> view);\n\tvoid registerWebPageItem(\n\t\tnot_null<const WebPageData*> page,\n\t\tnot_null<HistoryItem*> item);\n\tvoid unregisterWebPageItem(\n\t\tnot_null<const WebPageData*> page,\n\t\tnot_null<HistoryItem*> item);\n\tvoid registerGameView(\n\t\tnot_null<const GameData*> game,\n\t\tnot_null<ViewElement*> view);\n\tvoid unregisterGameView(\n\t\tnot_null<const GameData*> game,\n\t\tnot_null<ViewElement*> view);\n\tvoid registerPollView(\n\t\tnot_null<const PollData*> poll,\n\t\tnot_null<ViewElement*> view);\n\tvoid unregisterPollView(\n\t\tnot_null<const PollData*> poll,\n\t\tnot_null<ViewElement*> view);\n\tvoid registerTodoListView(\n\t\tnot_null<const TodoListData*> todolist,\n\t\tnot_null<ViewElement*> view);\n\tvoid unregisterTodoListView(\n\t\tnot_null<const TodoListData*> todolist,\n\t\tnot_null<ViewElement*> view);\n\tvoid registerContactView(\n\t\tUserId contactId,\n\t\tnot_null<ViewElement*> view);\n\tvoid unregisterContactView(\n\t\tUserId contactId,\n\t\tnot_null<ViewElement*> view);\n\tvoid registerContactItem(\n\t\tUserId contactId,\n\t\tnot_null<HistoryItem*> item);\n\tvoid unregisterContactItem(\n\t\tUserId contactId,\n\t\tnot_null<HistoryItem*> item);\n\tvoid registerCallItem(not_null<HistoryItem*> item);\n\tvoid unregisterCallItem(not_null<HistoryItem*> item);\n\tvoid registerStoryItem(FullStoryId id, not_null<HistoryItem*> item);\n\tvoid unregisterStoryItem(FullStoryId id, not_null<HistoryItem*> item);\n\tvoid refreshStoryItemViews(FullStoryId id);\n\n\tvoid documentMessageRemoved(not_null<DocumentData*> document);\n\n\tvoid checkPlayingAnimations();\n\n\tHistoryItem *findWebPageItem(not_null<WebPageData*> page) const;\n\tQString findContactPhone(not_null<UserData*> contact) const;\n\tQString findContactPhone(UserId contactId) const;\n\n\tvoid notifyWebPageUpdateDelayed(not_null<WebPageData*> page);\n\tvoid notifyGameUpdateDelayed(not_null<GameData*> game);\n\tvoid notifyPollUpdateDelayed(not_null<PollData*> poll);\n\tvoid notifyTodoListUpdateDelayed(not_null<TodoListData*> todolist);\n\t[[nodiscard]] bool hasPendingWebPageGamePollTodoListNotification() const;\n\tvoid sendWebPageGamePollTodoListNotifications();\n\t[[nodiscard]] rpl::producer<not_null<WebPageData*>> webPageUpdates() const;\n\t[[nodiscard]] rpl::producer<not_null<PollData*>> pollUpdates() const;\n\n\tvoid channelDifferenceTooLong(not_null<ChannelData*> channel);\n\t[[nodiscard]] rpl::producer<not_null<ChannelData*>> channelDifferenceTooLong() const;\n\n\tvoid registerItemView(not_null<ViewElement*> view);\n\tvoid unregisterItemView(not_null<ViewElement*> view);\n\n\t[[nodiscard]] not_null<Folder*> folder(FolderId id);\n\t[[nodiscard]] Folder *folderLoaded(FolderId id) const;\n\tnot_null<Folder*> processFolder(const MTPFolder &data);\n\tnot_null<Folder*> processFolder(const MTPDfolder &data);\n\n\t[[nodiscard]] not_null<Dialogs::MainList*> chatsListFor(\n\t\tnot_null<Dialogs::Entry*> entry);\n\t[[nodiscard]] not_null<Dialogs::MainList*> chatsList(\n\t\tFolder *folder = nullptr);\n\t[[nodiscard]] not_null<const Dialogs::MainList*> chatsList(\n\t\tFolder *folder = nullptr) const;\n\t[[nodiscard]] not_null<Dialogs::IndexedList*> contactsList();\n\t[[nodiscard]] not_null<Dialogs::IndexedList*> contactsNoChatsList();\n\n\tstruct ChatListEntryRefresh {\n\t\tDialogs::Key key;\n\t\tDialogs::PositionChange moved;\n\t\tFilterId filterId = 0;\n\t\tbool existenceChanged = false;\n\n\t\texplicit operator bool() const {\n\t\t\treturn existenceChanged || (moved.from != moved.to);\n\t\t}\n\t};\n\tvoid refreshChatListEntry(Dialogs::Key key);\n\tvoid removeChatListEntry(Dialogs::Key key);\n\t[[nodiscard]] auto chatListEntryRefreshes() const\n\t\t-> rpl::producer<ChatListEntryRefresh>;\n\n\tstruct DialogsRowReplacement {\n\t\tnot_null<Dialogs::Row*> old;\n\t\tDialogs::Row *now = nullptr;\n\t};\n\tvoid dialogsRowReplaced(DialogsRowReplacement replacement);\n\trpl::producer<DialogsRowReplacement> dialogsRowReplacements() const;\n\n\tvoid serviceNotification(\n\t\tconst TextWithEntities &message,\n\t\tconst MTPMessageMedia &media = MTP_messageMediaEmpty(),\n\t\tbool invertMedia = false);\n\n\tvoid setMimeForwardIds(MessageIdsList &&list);\n\tMessageIdsList takeMimeForwardIds();\n\n\tbool updateWallpapers(const MTPaccount_WallPapers &data);\n\tvoid removeWallpaper(const WallPaper &paper);\n\tconst std::vector<WallPaper> &wallpapers() const;\n\tuint64 wallpapersHash() const;\n\n\tstruct WebViewResultSent {\n\t\tuint64 queryId = 0;\n\t};\n\tvoid webViewResultSent(WebViewResultSent &&sent);\n\t[[nodiscard]] rpl::producer<WebViewResultSent> webViewResultSent() const;\n\n\tvoid saveViewAsMessages(not_null<Forum*> forum, bool viewAsMessages);\n\n\t[[nodiscard]] auto peerDecorationsUpdated() const\n\t\t-> rpl::producer<not_null<PeerData*>>;\n\n\tvoid applyStatsDcId(not_null<PeerData*>, MTP::DcId);\n\t[[nodiscard]] MTP::DcId statsDcId(not_null<PeerData*>);\n\n\tvoid viewTagsChanged(\n\t\tnot_null<ViewElement*> view,\n\t\tstd::vector<ReactionId> &&was,\n\t\tstd::vector<ReactionId> &&now);\n\n\tvoid sentToScheduled(SentToScheduled value);\n\t[[nodiscard]] rpl::producer<SentToScheduled> sentToScheduled() const;\n\tvoid sentFromScheduled(SentFromScheduled value);\n\t[[nodiscard]] rpl::producer<SentFromScheduled> sentFromScheduled() const;\n\n\tvoid editStarsPerMessage(not_null<ChannelData*> channel, int count);\n\t[[nodiscard]] int commonStarsPerMessage(\n\t\tnot_null<const ChannelData*> channel) const;\n\n\tvoid setPendingStarsRating(StarsRatingPending value);\n\t[[nodiscard]] StarsRatingPending pendingStarsRating() const;\n\n\tvoid addRecentSelfForwards(const RecentSelfForwards &data);\n\t[[nodiscard]] rpl::producer<RecentSelfForwards> recentSelfForwards() const;\n\n\tvoid addRecentJoinChat(const RecentJoinChat &data);\n\t[[nodiscard]] rpl::producer<RecentJoinChat> recentJoinChat() const;\n\n\tvoid clearLocalStorage();\n\n\tvoid fillMessagePeers(PeerId peerId, const MTPMessage &message);\n\tvoid fillMessagePeers(const MTPDupdateShortMessage &data);\n\tvoid fillMessagePeers(const MTPDupdateShortChatMessage &data);\n\tvoid fillMessagePeers(\n\t\tFullMsgId fullId,\n\t\tconst MTPDupdateShortSentMessage &data);\n\t[[nodiscard]] HistoryItem *messageWithPeer(PeerId id) const;\n\nprivate:\n\tusing Messages = std::unordered_map<MsgId, not_null<HistoryItem*>>;\n\n\tstruct NextToUpgradeGift {\n\t\tstd::optional<Data::SavedStarGift> gift;\n\t\tFn<void(std::optional<Data::SavedStarGift>)> done;\n\t\tcrl::time received = 0;\n\t\tmtpRequestId requestId = 0;\n\t};\n\n\tvoid suggestStartExport();\n\n\tvoid setupMigrationViewer();\n\tvoid setupChannelLeavingViewer();\n\tvoid setupPeerNameViewer();\n\tvoid setupUserIsContactViewer();\n\n\tvoid checkSelfDestructItems();\n\tvoid checkLocalUsersWentOffline();\n\n\tvoid scheduleNextTTLs();\n\tvoid checkTTLs();\n\n\tvoid scheduleNextFormattedDateUpdate();\n\tvoid checkFormattedDateUpdates();\n\n\tint computeUnreadBadge(const Dialogs::UnreadState &state) const;\n\tbool computeUnreadBadgeMuted(const Dialogs::UnreadState &state) const;\n\n\tvoid applyDialog(Folder *requestFolder, const MTPDdialog &data);\n\tvoid applyDialog(\n\t\tFolder *requestFolder,\n\t\tconst MTPDdialogFolder &data);\n\n\tconst Messages *messagesList(PeerId peerId) const;\n\tnot_null<Messages*> messagesListForInsert(PeerId peerId);\n\tnot_null<HistoryItem*> registerMessage(\n\t\tstd::unique_ptr<HistoryItem> item);\n\tHistoryItem *changeMessageId(PeerId peerId, MsgId wasId, MsgId nowId);\n\tvoid removeDependencyMessage(not_null<HistoryItem*> item);\n\n\tvoid photoApplyFields(\n\t\tnot_null<PhotoData*> photo,\n\t\tconst MTPPhoto &data);\n\tvoid photoApplyFields(\n\t\tnot_null<PhotoData*> photo,\n\t\tconst MTPDphoto &data);\n\tvoid photoApplyFields(\n\t\tnot_null<PhotoData*> photo,\n\t\tconst uint64 &access,\n\t\tconst QByteArray &fileReference,\n\t\tTimeId date,\n\t\tint32 dc,\n\t\tbool hasStickers,\n\t\tconst QByteArray &inlineThumbnailBytes,\n\t\tconst ImageWithLocation &small,\n\t\tconst ImageWithLocation &thumbnail,\n\t\tconst ImageWithLocation &large,\n\t\tconst ImageWithLocation &videoSmall,\n\t\tconst ImageWithLocation &videoLarge,\n\t\tcrl::time videoStartTime);\n\n\tvoid documentApplyFields(\n\t\tnot_null<DocumentData*> document,\n\t\tconst MTPDocument &data);\n\tvoid documentApplyFields(\n\t\tnot_null<DocumentData*> document,\n\t\tconst MTPDdocument &data);\n\tvoid documentApplyFields(\n\t\tnot_null<DocumentData*> document,\n\t\tconst uint64 &access,\n\t\tconst QByteArray &fileReference,\n\t\tTimeId date,\n\t\tconst QVector<MTPDocumentAttribute> &attributes,\n\t\tconst QString &mime,\n\t\tconst InlineImageLocation &inlineThumbnail,\n\t\tconst ImageWithLocation &thumbnail,\n\t\tconst ImageWithLocation &videoThumbnail,\n\t\tbool isPremiumSticker,\n\t\tint32 dc,\n\t\tint64 size);\n\tDocumentData *documentFromWeb(\n\t\tconst MTPDwebDocument &data,\n\t\tconst ImageLocation &thumbnailLocation,\n\t\tconst ImageLocation &videoThumbnailLocation);\n\tDocumentData *documentFromWeb(\n\t\tconst MTPDwebDocumentNoProxy &data,\n\t\tconst ImageLocation &thumbnailLocation,\n\t\tconst ImageLocation &videoThumbnailLocation);\n\n\tvoid webpageApplyFields(\n\t\tnot_null<WebPageData*> page,\n\t\tconst MTPDwebPage &data);\n\tvoid webpageApplyFields(\n\t\tnot_null<WebPageData*> page,\n\t\tWebPageType type,\n\t\tconst QString &url,\n\t\tconst QString &displayUrl,\n\t\tconst QString &siteName,\n\t\tconst QString &title,\n\t\tconst TextWithEntities &description,\n\t\tFullStoryId storyId,\n\t\tPhotoData *photo,\n\t\tDocumentData *document,\n\t\tWebPageCollage &&collage,\n\t\tstd::unique_ptr<Iv::Data> iv,\n\t\tstd::unique_ptr<WebPageStickerSet> stickerSet,\n\t\tstd::shared_ptr<UniqueGift> uniqueGift,\n\t\tstd::unique_ptr<WebPageAuction> auction,\n\t\tint duration,\n\t\tconst QString &author,\n\t\tbool hasLargeMedia,\n\t\tbool photoIsVideoCover,\n\t\tTimeId pendingTill);\n\n\tvoid gameApplyFields(\n\t\tnot_null<GameData*> game,\n\t\tconst MTPDgame &data);\n\tvoid gameApplyFields(\n\t\tnot_null<GameData*> game,\n\t\tconst uint64 &accessHash,\n\t\tconst QString &shortName,\n\t\tconst QString &title,\n\t\tconst QString &description,\n\t\tPhotoData *photo,\n\t\tDocumentData *document);\n\n\ttemplate <typename Method>\n\tvoid enumerateItemViews(\n\t\tnot_null<const HistoryItem*> item,\n\t\tMethod method);\n\n\tvoid insertCheckedServiceNotification(\n\t\tconst TextWithEntities &message,\n\t\tconst MTPMessageMedia &media,\n\t\tTimeId date,\n\t\tbool invertMedia);\n\n\tvoid setWallpapers(const QVector<MTPWallPaper> &data, uint64 hash);\n\tvoid highlightProcessDone(uint64 processId);\n\n\tvoid applyMonoforumLinkedId(\n\t\tnot_null<ChannelData*> channel,\n\t\tChannelId linkedId);\n\n\tvoid checkPollsClosings();\n\n\tvoid fillMessagePeer(FullMsgId fullId, PeerId peerId);\n\tvoid fillForwardedInfo(\n\t\tFullMsgId fullId,\n\t\tconst MTPMessageFwdHeader &header);\n\tvoid fillMentionUsers(\n\t\tFullMsgId fullId,\n\t\tconst MTPVector<MTPMessageEntity> &entities);\n\n\tconst not_null<Main::Session*> _session;\n\n\tStorage::DatabasePointer _cache;\n\tStorage::DatabasePointer _bigFileCache;\n\n\tTimeId _exportAvailableAt = 0;\n\tbase::weak_qptr<Ui::BoxContent> _exportSuggestion;\n\n\trpl::variable<bool> _contactsLoaded = false;\n\trpl::variable<int> _groupFreeTranscribeLevel;\n\trpl::event_stream<Folder*> _chatsListLoadedEvents;\n\trpl::event_stream<Folder*> _chatsListChanged;\n\trpl::event_stream<not_null<UserData*>> _userIsBotChanges;\n\trpl::event_stream<not_null<PeerData*>> _botCommandsChanges;\n\trpl::event_stream<ItemVisibilityQuery> _itemVisibilityQueries;\n\trpl::event_stream<IdChange> _itemIdChanges;\n\trpl::event_stream<not_null<const HistoryItem*>> _itemLayoutChanges;\n\trpl::event_stream<not_null<const ViewElement*>> _viewLayoutChanges;\n\trpl::event_stream<not_null<HistoryItem*>> _newItemAdded;\n\trpl::event_stream<GiftUpdate> _giftUpdates;\n\trpl::event_stream<GiftsUpdate> _giftsUpdates;\n\trpl::event_stream<GiftAuctionGot> _giftAuctionGots;\n\trpl::event_stream<not_null<const HistoryItem*>> _itemRepaintRequest;\n\trpl::event_stream<RequestViewRepaint> _viewRepaintRequest;\n\trpl::event_stream<not_null<const HistoryItem*>> _itemResizeRequest;\n\trpl::event_stream<not_null<ViewElement*>> _viewResizeRequest;\n\trpl::event_stream<ViewHeightAdjusted> _viewHeightAdjusted;\n\trpl::event_stream<not_null<HistoryItem*>> _itemShowHighlightRequest;\n\trpl::event_stream<not_null<const HistoryItem*>> _itemViewRefreshRequest;\n\trpl::event_stream<not_null<HistoryItem*>> _itemTextRefreshRequest;\n\trpl::event_stream<DrawToReplyRequest> _drawToReplyRequests;\n\trpl::event_stream<not_null<HistoryItem*>> _itemDataChanges;\n\trpl::event_stream<not_null<const HistoryItem*>> _itemRemoved;\n\trpl::event_stream<std::vector<not_null<HistoryItem*>>> _itemsAboutToBeDestroyed;\n\trpl::event_stream<not_null<const ViewElement*>> _viewAboutToBeRemoved;\n\trpl::event_stream<not_null<const ViewElement*>> _viewRemoved;\n\trpl::event_stream<not_null<const ViewElement*>> _viewPaidReactionSent;\n\trpl::event_stream<not_null<Calls::GroupCall*>> _callPaidReactionSent;\n\trpl::event_stream<not_null<const History*>> _historyUnloaded;\n\trpl::event_stream<not_null<const History*>> _historyCleared;\n\tbase::flat_set<not_null<History*>> _historiesChanged;\n\trpl::event_stream<not_null<History*>> _historyChanged;\n\trpl::event_stream<MegagroupParticipant> _megagroupParticipantRemoved;\n\trpl::event_stream<MegagroupParticipant> _megagroupParticipantAdded;\n\trpl::event_stream<DialogsRowReplacement> _dialogsRowReplacements;\n\trpl::event_stream<ChatListEntryRefresh> _chatListEntryRefreshes;\n\trpl::event_stream<> _unreadBadgeChanges;\n\trpl::event_stream<RepliesReadTillUpdate> _repliesReadTillUpdates;\n\trpl::event_stream<SublistReadTillUpdate> _sublistReadTillUpdates;\n\trpl::event_stream<SentToScheduled> _sentToScheduled;\n\trpl::event_stream<SentFromScheduled> _sentFromScheduled;\n\n\tDialogs::MainList _chatsList;\n\tDialogs::IndexedList _contactsList;\n\tDialogs::IndexedList _contactsNoChatsList;\n\n\tMsgId _localMessageIdCounter = StartClientMsgId;\n\tstd::unordered_map<PeerId, Messages> _messages;\n\tstd::map<\n\t\tnot_null<HistoryItem*>,\n\t\tbase::flat_set<not_null<HistoryItem*>>> _dependentMessages;\n\tstd::map<TimeId, base::flat_set<not_null<HistoryItem*>>> _ttlMessages;\n\tbase::Timer _ttlCheckTimer;\n\n\tstd::map<TimeId, std::vector<base::weak_ptr<HistoryView::Element>>> _formattedDateUpdates;\n\tbase::Timer _formattedDateTimer;\n\n\tstd::unordered_map<MsgId, not_null<HistoryItem*>> _nonChannelMessages;\n\n\tbase::flat_map<uint64, FullMsgId> _messageByRandomId;\n\tbase::flat_map<uint64, SentData> _sentMessagesData;\n\n\tbase::Timer _selfDestructTimer;\n\tstd::vector<FullMsgId> _selfDestructItems;\n\n\tstd::unordered_map<\n\t\tPhotoId,\n\t\tstd::unique_ptr<PhotoData>> _photos;\n\tstd::unordered_map<\n\t\tnot_null<const PhotoData*>,\n\t\tbase::flat_set<not_null<HistoryItem*>>> _photoItems;\n\tstd::unordered_map<\n\t\tDocumentId,\n\t\tstd::unique_ptr<DocumentData>> _documents;\n\tstd::unordered_map<\n\t\tnot_null<const DocumentData*>,\n\t\tbase::flat_set<not_null<HistoryItem*>>> _documentItems;\n\tstd::unordered_map<\n\t\tWebPageId,\n\t\tstd::unique_ptr<WebPageData>> _webpages;\n\tstd::unordered_map<\n\t\tnot_null<const WebPageData*>,\n\t\tbase::flat_set<not_null<HistoryItem*>>> _webpageItems;\n\tstd::unordered_map<\n\t\tnot_null<const WebPageData*>,\n\t\tbase::flat_set<not_null<ViewElement*>>> _webpageViews;\n\tstd::unordered_map<\n\t\tLocationPoint,\n\t\tstd::unique_ptr<CloudImage>> _locations;\n\tstd::unordered_map<\n\t\tPollId,\n\t\tstd::unique_ptr<PollData>> _polls;\n\tstd::map<\n\t\tTodoListId,\n\t\tstd::unique_ptr<TodoListData>> _todoLists;\n\tstd::unordered_map<\n\t\tGameId,\n\t\tstd::unique_ptr<GameData>> _games;\n\tstd::unordered_map<\n\t\tBotAppId,\n\t\tstd::unique_ptr<BotAppData>> _botApps;\n\tstd::unordered_map<\n\t\tnot_null<const GameData*>,\n\t\tbase::flat_set<not_null<ViewElement*>>> _gameViews;\n\tstd::unordered_map<\n\t\tnot_null<const PollData*>,\n\t\tbase::flat_set<not_null<ViewElement*>>> _pollViews;\n\tstd::unordered_map<\n\t\tnot_null<const TodoListData*>,\n\t\tbase::flat_set<not_null<ViewElement*>>> _todoListViews;\n\tstd::unordered_map<\n\t\tUserId,\n\t\tbase::flat_set<not_null<HistoryItem*>>> _contactItems;\n\tstd::unordered_map<\n\t\tUserId,\n\t\tbase::flat_set<not_null<ViewElement*>>> _contactViews;\n\tstd::unordered_set<not_null<HistoryItem*>> _callItems;\n\tstd::unordered_map<\n\t\tFullStoryId,\n\t\tbase::flat_set<not_null<HistoryItem*>>> _storyItems;\n\tbase::flat_map<uint64, not_null<HistoryItem*>> _highlightings;\n\tbase::flat_map<QString, not_null<DocumentData*>> _venueIcons;\n\n\tbase::flat_set<not_null<WebPageData*>> _webpagesUpdated;\n\tbase::flat_set<not_null<GameData*>> _gamesUpdated;\n\tbase::flat_set<not_null<PollData*>> _pollsUpdated;\n\tbase::flat_set<not_null<TodoListData*>> _todoListsUpdated;\n\n\trpl::event_stream<not_null<WebPageData*>> _webpageUpdates;\n\trpl::event_stream<not_null<PollData*>> _pollUpdates;\n\trpl::event_stream<not_null<ChannelData*>> _channelDifferenceTooLong;\n\trpl::event_stream<not_null<DocumentData*>> _documentLoadProgress;\n\tbase::flat_set<not_null<ChannelData*>> _suggestToGigagroup;\n\n\tbase::flat_multi_map<TimeId, not_null<PollData*>> _pollsClosings;\n\tbase::Timer _pollsClosingTimer;\n\n\tbase::flat_map<\n\t\tnot_null<const HistoryItem*>,\n\t\tbase::flat_set<QString>> _possiblyRestricted;\n\n\tbase::flat_map<FolderId, std::unique_ptr<Folder>> _folders;\n\n\tstd::unordered_map<\n\t\tnot_null<const HistoryItem*>,\n\t\tstd::vector<not_null<ViewElement*>>> _views;\n\n\trpl::event_stream<> _pinnedDialogsOrderUpdated;\n\n\tbase::flat_set<not_null<ViewElement*>> _heavyViewParts;\n\n\tbase::flat_map<CallId, not_null<GroupCall*>> _groupCalls;\n\tbase::flat_map<CallId, std::weak_ptr<GroupCall>> _conferenceCalls;\n\trpl::event_stream<InviteToCall> _invitesToCalls;\n\tbase::flat_map<\n\t\tCallId,\n\t\tbase::flat_map<not_null<UserData*>, bool>> _invitedToCallUsers;\n\n\tbase::flat_set<not_null<ViewElement*>> _shownSpoilers;\n\tbase::flat_map<\n\t\tReactionId,\n\t\tbase::flat_set<not_null<ViewElement*>>> _viewsByTag;\n\n\tstd::unordered_map<PeerId, std::unique_ptr<PeerData>> _peers;\n\n\tstd::optional<base::flat_map<\n\t\tnot_null<ChannelData*>,\n\t\tChannelId>> _postponedMonoforumLinkedIds;\n\n\t// This one from `channel`, not `channelFull`.\n\tbase::flat_map<not_null<const ChannelData*>, int> _commonStarsPerMessage;\n\n\tMessageIdsList _mimeForwardIds;\n\n\tstd::weak_ptr<CreditsSubsRebuilder> _creditsSubsRebuilder;\n\n\tusing CredentialsWithGeneration = std::pair<\n\t\tconst Passport::SavedCredentials,\n\t\tint>;\n\tstd::unique_ptr<CredentialsWithGeneration> _passportCredentials;\n\n\tstd::vector<WallPaper> _wallpapers;\n\tuint64 _wallpapersHash = 0;\n\n\tbase::flat_map<not_null<UserData*>, TimeId> _watchingForOffline;\n\tbase::Timer _watchForOfflineTimer;\n\tbase::flat_map<not_null<UserData*>, TimeId> _sharingDisabledTimes;\n\n\tbase::flat_map<not_null<PeerData*>, MTP::DcId> _peerStatsDcIds;\n\n\trpl::event_stream<WebViewResultSent> _webViewResultSent;\n\n\trpl::event_stream<not_null<PeerData*>> _peerDecorationsUpdated;\n\tbase::flat_map<\n\t\tnot_null<ChannelData*>,\n\t\tmtpRequestId> _viewAsMessagesRequests;\n\n\tmutable base::flat_map<PeerId, std::vector<FullMsgId>> _messagesWithPeer;\n\n\tGroups _groups;\n\tconst std::unique_ptr<ChatFilters> _chatsFilters;\n\tconst std::unique_ptr<CloudThemes> _cloudThemes;\n\tconst std::unique_ptr<SendActionManager> _sendActionManager;\n\tconst std::unique_ptr<Streaming> _streaming;\n\tconst std::unique_ptr<MediaRotation> _mediaRotation;\n\tconst std::unique_ptr<Histories> _histories;\n\tconst std::unique_ptr<Stickers> _stickers;\n\tconst std::unique_ptr<Reactions> _reactions;\n\tconst std::unique_ptr<EmojiStatuses> _emojiStatuses;\n\tconst std::unique_ptr<ForumIcons> _forumIcons;\n\tconst std::unique_ptr<NotifySettings> _notifySettings;\n\tconst std::unique_ptr<CustomEmojiManager> _customEmojiManager;\n\tconst std::unique_ptr<Stories> _stories;\n\tconst std::unique_ptr<SavedMusic> _savedMusic;\n\tconst std::unique_ptr<SavedMessages> _savedMessages;\n\tconst std::unique_ptr<Chatbots> _chatbots;\n\tconst std::unique_ptr<BusinessInfo> _businessInfo;\n\tstd::unique_ptr<ShortcutMessages> _shortcutMessages;\n\n\tMsgId _nonHistoryEntryId = ShortcutMaxMsgId;\n\n\tstd::unique_ptr<StarsRatingPending> _pendingStarsRating;\n\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tNextToUpgradeGift> _nextForUpgradeGifts;\n\n\trpl::event_stream<RecentSelfForwards> _recentSelfForwards;\n\trpl::event_stream<RecentJoinChat> _recentJoinChat;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_shared_media.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_shared_media.h\"\n\n#include \"apiwrap.h\"\n#include \"core/crash_reports.h\"\n#include \"data/components/scheduled_messages.h\"\n#include \"data/data_document.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_saved_music.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"main/main_session.h\"\n#include \"storage/storage_facade.h\"\n\nnamespace {\n\nusing Type = Storage::SharedMediaType;\n\nbool IsItemGoodForType(const not_null<HistoryItem*> item, Type type) {\n\tconst auto media = item->media();\n\tif (!media || media->webpage()) {\n\t\treturn false;\n\t}\n\tconst auto photo = media->photo();\n\tconst auto photoType = (type == Type::Photo);\n\tconst auto photoVideoType = (type == Type::PhotoVideo);\n\tif ((photoType || photoVideoType) && photo) {\n\t\treturn true;\n\t}\n\n\tconst auto document = media->document();\n\tif (!document) {\n\t\treturn false;\n\t}\n\tconst auto voiceType = (type == Type::VoiceFile);\n\tconst auto voiceDoc = document->isVoiceMessage();\n\n\tconst auto roundType = (type == Type::RoundFile);\n\tconst auto roundDoc = document->isVideoMessage();\n\n\tconst auto audioType = (type == Type::MusicFile);\n\tconst auto audioDoc = document->isAudioFile();\n\n\tconst auto gifType = (type == Type::GIF);\n\tconst auto gifDoc = document->isGifv();\n\n\tconst auto videoType = (type == Type::Video);\n\tconst auto videoDoc = document->isVideoFile();\n\n\tconst auto voiceRoundType = (type == Type::RoundVoiceFile);\n\tconst auto fileType = (type == Type::File);\n\n\treturn (audioType && audioDoc)\n\t\t|| (voiceType && voiceDoc)\n\t\t|| (roundType && roundDoc)\n\t\t|| (voiceRoundType && (roundDoc || voiceDoc))\n\t\t|| (gifType && gifDoc)\n\t\t|| ((videoType || photoVideoType) && videoDoc)\n\t\t|| (fileType && (document->isTheme()\n\t\t\t|| document->isImage()\n\t\t\t|| !document->canBeStreamed()));\n}\n\n} // namespace\n\nstd::optional<Storage::SharedMediaType> SharedMediaOverviewType(\n\t\tStorage::SharedMediaType type) {\n\tswitch (type) {\n\tcase Type::Photo:\n\tcase Type::Video:\n\tcase Type::MusicFile:\n\tcase Type::File:\n\tcase Type::RoundVoiceFile:\n\tcase Type::Link:\n\tcase Type::Poll: return type;\n\t}\n\treturn std::nullopt;\n}\n\nbool SharedMediaAllowSearch(Storage::SharedMediaType type) {\n\tswitch (type) {\n\tcase Type::MusicFile:\n\tcase Type::File:\n\tcase Type::Link:\n\tcase Type::Poll: return true;\n\tdefault: return false;\n\t}\n}\n\nrpl::producer<SparseIdsSlice> SharedMediaViewer(\n\t\tnot_null<Main::Session*> session,\n\t\tStorage::SharedMediaKey key,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tExpects(IsServerMsgId(key.messageId) || (key.messageId == 0));\n\tExpects((key.messageId != 0) || (limitBefore == 0 && limitAfter == 0));\n\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tauto builder = lifetime.make_state<SparseIdsSliceBuilder>(\n\t\t\tkey.messageId,\n\t\t\tlimitBefore,\n\t\t\tlimitAfter);\n\t\tauto requestMediaAround = [\n\t\t\tpeer = session->data().peer(key.peerId),\n\t\t\ttopicRootId = key.topicRootId,\n\t\t\tmonoforumPeerId = key.monoforumPeerId,\n\t\t\ttype = key.type\n\t\t](const SparseIdsSliceBuilder::AroundData &data) {\n\t\t\tpeer->session().api().requestSharedMedia(\n\t\t\t\tpeer,\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\ttype,\n\t\t\t\tdata.aroundId,\n\t\t\t\tdata.direction);\n\t\t};\n\t\tbuilder->insufficientAround(\n\t\t) | rpl::on_next(requestMediaAround, lifetime);\n\n\t\tauto pushNextSnapshot = [=] {\n\t\t\tconsumer.put_next(builder->snapshot());\n\t\t};\n\n\t\tusing SliceUpdate = Storage::SharedMediaSliceUpdate;\n\t\tsession->storage().sharedMediaSliceUpdated(\n\t\t) | rpl::filter([=](const SliceUpdate &update) {\n\t\t\treturn (update.peerId == key.peerId)\n\t\t\t\t&& (update.topicRootId == key.topicRootId)\n\t\t\t\t&& (update.monoforumPeerId == key.monoforumPeerId)\n\t\t\t\t&& (update.type == key.type);\n\t\t}) | rpl::filter([=](const SliceUpdate &update) {\n\t\t\treturn builder->applyUpdate(update.data);\n\t\t}) | rpl::on_next(pushNextSnapshot, lifetime);\n\n\t\tusing OneRemoved = Storage::SharedMediaRemoveOne;\n\t\tsession->storage().sharedMediaOneRemoved(\n\t\t) | rpl::filter([=](const OneRemoved &update) {\n\t\t\treturn (update.peerId == key.peerId)\n\t\t\t\t&& update.types.test(key.type);\n\t\t}) | rpl::filter([=](const OneRemoved &update) {\n\t\t\treturn builder->removeOne(update.messageId);\n\t\t}) | rpl::on_next(pushNextSnapshot, lifetime);\n\n\t\tusing AllRemoved = Storage::SharedMediaRemoveAll;\n\t\tsession->storage().sharedMediaAllRemoved(\n\t\t) | rpl::filter([=](const AllRemoved &update) {\n\t\t\treturn (update.peerId == key.peerId)\n\t\t\t\t&& (!update.topicRootId\n\t\t\t\t\t|| update.topicRootId == key.topicRootId)\n\t\t\t\t&& (!update.monoforumPeerId\n\t\t\t\t\t|| update.monoforumPeerId == key.monoforumPeerId)\n\t\t\t\t&& update.types.test(key.type);\n\t\t}) | rpl::filter([=] {\n\t\t\treturn builder->removeAll();\n\t\t}) | rpl::on_next(pushNextSnapshot, lifetime);\n\n\t\tusing InvalidateBottom = Storage::SharedMediaInvalidateBottom;\n\t\tsession->storage().sharedMediaBottomInvalidated(\n\t\t) | rpl::filter([=](const InvalidateBottom &update) {\n\t\t\treturn (update.peerId == key.peerId);\n\t\t}) | rpl::filter([=] {\n\t\t\treturn builder->invalidateBottom();\n\t\t}) | rpl::on_next(pushNextSnapshot, lifetime);\n\n\t\tusing Result = Storage::SharedMediaResult;\n\t\tsession->storage().query(Storage::SharedMediaQuery(\n\t\t\tkey,\n\t\t\tlimitBefore,\n\t\t\tlimitAfter\n\t\t)) | rpl::filter([=](const Result &result) {\n\t\t\treturn builder->applyInitial(result);\n\t\t}) | rpl::on_next_done(\n\t\t\tpushNextSnapshot,\n\t\t\t[=] { builder->checkInsufficient(); },\n\t\t\tlifetime);\n\n\t\treturn lifetime;\n\t};\n}\n\nrpl::producer<SparseIdsMergedSlice> SharedScheduledMediaViewer(\n\t\tnot_null<Main::Session*> session,\n\t\tSharedMediaMergedKey key,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tExpects(!key.mergedKey.universalId\n\t\t|| Data::IsScheduledMsgId(key.mergedKey.universalId));\n\tExpects((key.mergedKey.universalId != 0)\n\t\t|| (limitBefore == 0 && limitAfter == 0));\n\n\tconst auto history = session->data().history(key.mergedKey.peerId);\n\n\treturn rpl::single(rpl::empty) | rpl::then(\n\t\tsession->scheduledMessages().updates(history)\n\t) | rpl::map([=] {\n\t\tconst auto list = session->scheduledMessages().list(history);\n\n\t\tauto items = ranges::views::all(\n\t\t\tlist.ids\n\t\t) | ranges::views::transform([=](const FullMsgId &fullId) {\n\t\t\treturn session->data().message(fullId);\n\t\t}) | ranges::views::filter([=](HistoryItem *item) {\n\t\t\treturn item\n\t\t\t\t? IsItemGoodForType(item, key.type)\n\t\t\t\t: false;\n\t\t}) | ranges::to_vector;\n\n\t\tranges::sort(items, ranges::less(), &HistoryItem::position);\n\n\t\tauto finishMsgIds = ranges::views::all(\n\t\t\titems\n\t\t) | ranges::views::transform([=](not_null<HistoryItem*> item) {\n\t\t\treturn item->fullId().msg;\n\t\t}) | ranges::to_vector;\n\n\t\tconst auto fullCount = finishMsgIds.size();\n\n\t\tauto unsorted = SparseUnsortedIdsSlice(\n\t\t\tstd::move(finishMsgIds),\n\t\t\tfullCount,\n\t\t\tlist.skippedBefore,\n\t\t\tlist.skippedAfter);\n\t\treturn SparseIdsMergedSlice(\n\t\t\tkey.mergedKey,\n\t\t\tstd::move(unsorted));\n\t});\n}\n\nrpl::producer<SparseIdsMergedSlice> SavedMusicMediaViewer(\n\t\tnot_null<Main::Session*> session,\n\t\tSharedMediaMergedKey key,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tExpects((key.mergedKey.universalId != 0)\n\t\t|| (limitBefore == 0 && limitAfter == 0));\n\n\tconst auto peerId = key.mergedKey.peerId;\n\tconst auto item = key.mergedKey.universalId\n\t\t? session->data().message(peerId, key.mergedKey.universalId)\n\t\t: nullptr;\n\n\treturn Data::SavedMusicList(\n\t\tsession->data().peer(peerId),\n\t\titem,\n\t\tstd::max(limitBefore, limitAfter)\n\t) | rpl::map([=](const Data::SavedMusicSlice &slice) {\n\t\tauto list = std::vector<MsgId>();\n\t\tlist.reserve(slice.size());\n\t\tfor (auto i = 0, count = int(slice.size()); i != count; ++i) {\n\t\t\tlist.push_back(slice[i]->id);\n\t\t}\n\t\treturn SparseIdsMergedSlice(\n\t\t\tkey.mergedKey,\n\t\t\tSparseUnsortedIdsSlice(\n\t\t\t\tstd::move(list),\n\t\t\t\tslice.fullCount(),\n\t\t\t\tslice.skippedBefore(),\n\t\t\t\tslice.skippedAfter()));\n\t});\n}\n\nrpl::producer<SparseIdsMergedSlice> SharedMediaMergedViewer(\n\t\tnot_null<Main::Session*> session,\n\t\tSharedMediaMergedKey key,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tauto createSimpleViewer = [=](\n\t\t\tPeerId peerId,\n\t\t\tMsgId topicRootId,\n\t\t\tPeerId monoforumPeerId,\n\t\t\tSparseIdsSlice::Key simpleKey,\n\t\t\tint limitBefore,\n\t\t\tint limitAfter) {\n\t\treturn SharedMediaViewer(\n\t\t\tsession,\n\t\t\tStorage::SharedMediaKey(\n\t\t\t\tpeerId,\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tkey.type,\n\t\t\t\tsimpleKey),\n\t\t\tlimitBefore,\n\t\t\tlimitAfter\n\t\t);\n\t};\n\treturn SparseIdsMergedSlice::CreateViewer(\n\t\tkey.mergedKey,\n\t\tlimitBefore,\n\t\tlimitAfter,\n\t\tstd::move(createSimpleViewer));\n}\n\nSharedMediaWithLastSlice::SharedMediaWithLastSlice(\n\tnot_null<Main::Session*> session,\n\tKey key)\n: SharedMediaWithLastSlice(\n\tsession,\n\tkey,\n\tSparseIdsMergedSlice(ViewerKey(key)),\n\tEndingSlice(key)) {\n}\n\nSharedMediaWithLastSlice::SharedMediaWithLastSlice(\n\tnot_null<Main::Session*> session,\n\tKey key,\n\tSparseIdsMergedSlice slice,\n\tstd::optional<SparseIdsMergedSlice> ending)\n: _session(session)\n, _key(key)\n, _slice(std::move(slice))\n, _ending(std::move(ending))\n, _lastPhotoId(LastPeerPhotoId(session, key.peerId))\n, _isolatedLastPhoto(_key.type == Type::ChatPhoto\n\t? IsLastIsolated(session, _slice, _ending, _lastPhotoId)\n\t: false) {\n}\n\nstd::optional<int> SharedMediaWithLastSlice::fullCount() const {\n\treturn Add(\n\t\t_slice.fullCount(),\n\t\t_isolatedLastPhoto | [](bool isolated) { return isolated ? 1 : 0; });\n}\n\nstd::optional<int> SharedMediaWithLastSlice::skippedBeforeImpl() const {\n\treturn _slice.skippedBefore();\n}\n\nstd::optional<int> SharedMediaWithLastSlice::skippedBefore() const {\n\treturn _reversed ? skippedAfterImpl() : skippedBeforeImpl();\n}\n\nstd::optional<int> SharedMediaWithLastSlice::skippedAfterImpl() const {\n\treturn isolatedInSlice()\n\t\t? Add(\n\t\t\t_slice.skippedAfter(),\n\t\t\tlastPhotoSkip())\n\t\t: (lastPhotoSkip() | [](int) { return 0; });\n}\n\nstd::optional<int> SharedMediaWithLastSlice::skippedAfter() const {\n\treturn _reversed ? skippedBeforeImpl() : skippedAfterImpl();\n}\n\nstd::optional<int> SharedMediaWithLastSlice::indexOfImpl(Value value) const {\n\treturn std::get_if<FullMsgId>(&value)\n\t\t? _slice.indexOf(*std::get_if<FullMsgId>(&value))\n\t\t: (isolatedInSlice()\n\t\t\t|| !_lastPhotoId\n\t\t\t|| (*std::get_if<not_null<PhotoData*>>(&value))->id != *_lastPhotoId)\n\t\t\t? std::nullopt\n\t\t\t: Add(_slice.size() - 1, lastPhotoSkip());\n}\n\nstd::optional<int> SharedMediaWithLastSlice::indexOf(Value value) const {\n\tconst auto result = indexOfImpl(value);\n\tif (result && (*result < 0 || *result >= size())) {\n\t\t// Should not happen.\n\t\tauto info = QStringList();\n\t\tinfo.push_back(\"slice:\" + QString::number(_slice.size()));\n\t\tinfo.push_back(_slice.fullCount()\n\t\t\t? QString::number(*_slice.fullCount())\n\t\t\t: QString(\"-\"));\n\t\tinfo.push_back(_slice.skippedBefore()\n\t\t\t? QString::number(*_slice.skippedBefore())\n\t\t\t: QString(\"-\"));\n\t\tinfo.push_back(_slice.skippedAfter()\n\t\t\t? QString::number(*_slice.skippedAfter())\n\t\t\t: QString(\"-\"));\n\t\tinfo.push_back(\"ending:\" + (_ending\n\t\t\t? QString::number(_ending->size())\n\t\t\t: QString(\"-\")));\n\t\tinfo.push_back((_ending && _ending->fullCount())\n\t\t\t? QString::number(*_ending->fullCount())\n\t\t\t: QString(\"-\"));\n\t\tinfo.push_back((_ending && _ending->skippedBefore())\n\t\t\t? QString::number(*_ending->skippedBefore())\n\t\t\t: QString(\"-\"));\n\t\tinfo.push_back((_ending && _ending->skippedAfter())\n\t\t\t? QString::number(*_ending->skippedAfter())\n\t\t\t: QString(\"-\"));\n\t\tif (const auto msgId = std::get_if<FullMsgId>(&value)) {\n\t\t\tinfo.push_back(\"value:\" + QString::number(msgId->peer.value));\n\t\t\tinfo.push_back(QString::number(msgId->msg.bare));\n\t\t\tconst auto index = _slice.indexOf(*std::get_if<FullMsgId>(&value));\n\t\t\tinfo.push_back(\"index:\" + (index\n\t\t\t\t? QString::number(*index)\n\t\t\t\t: QString(\"-\")));\n\t\t} else if (const auto photo = std::get_if<not_null<PhotoData*>>(&value)) {\n\t\t\tinfo.push_back(\"value:\" + QString::number((*photo)->id));\n\t\t} else {\n\t\t\tinfo.push_back(\"value:bad\");\n\t\t}\n\t\tinfo.push_back(\"isolated:\" + QString(Logs::b(isolatedInSlice())));\n\t\tinfo.push_back(\"last:\" + (_lastPhotoId\n\t\t\t? QString::number(*_lastPhotoId)\n\t\t\t: QString(\"-\")));\n\t\tinfo.push_back(\"isolated_last:\" + (_isolatedLastPhoto\n\t\t\t? QString(Logs::b(*_isolatedLastPhoto))\n\t\t\t: QString(\"-\")));\n\t\tinfo.push_back(\"skip:\" + (lastPhotoSkip()\n\t\t\t? QString::number(*lastPhotoSkip())\n\t\t\t: QString(\"-\")));\n\t\tCrashReports::SetAnnotation(\"DebugInfo\", info.join(','));\n\t\tUnexpected(\"Result in SharedMediaWithLastSlice::indexOf\");\n\t}\n\treturn _reversed\n\t\t? (result | func::negate | func::add(size() - 1))\n\t\t: result;\n}\n\nint SharedMediaWithLastSlice::size() const {\n\treturn _slice.size()\n\t\t+ ((!isolatedInSlice() && lastPhotoSkip() == 1) ? 1 : 0);\n}\n\nSharedMediaWithLastSlice::Value SharedMediaWithLastSlice::operator[](int index) const {\n\tExpects(index >= 0 && index < size());\n\n\tif (_reversed) {\n\t\tindex = size() - index - 1;\n\t}\n\treturn (index < _slice.size())\n\t\t? Value(_slice[index])\n\t\t: Value(_session->data().photo(*_lastPhotoId));\n}\n\nstd::optional<int> SharedMediaWithLastSlice::distance(\n\t\tconst Key &a,\n\t\tconst Key &b) const {\n\tif (auto i = indexOf(ComputeId(a))) {\n\t\tif (auto j = indexOf(ComputeId(b))) {\n\t\t\treturn *j - *i;\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nvoid SharedMediaWithLastSlice::reverse() {\n\t_reversed = !_reversed;\n}\n\nstd::optional<PhotoId> SharedMediaWithLastSlice::LastPeerPhotoId(\n\t\tnot_null<Main::Session*> session,\n\t\tPeerId peerId) {\n\tif (const auto peer = session->data().peerLoaded(peerId)) {\n\t\treturn peer->userpicPhotoUnknown()\n\t\t\t? std::nullopt\n\t\t\t: base::make_optional(peer->userpicPhotoId());\n\t}\n\treturn std::nullopt;\n}\n\nstd::optional<bool> SharedMediaWithLastSlice::IsLastIsolated(\n\t\tnot_null<Main::Session*> session,\n\t\tconst SparseIdsMergedSlice &slice,\n\t\tconst std::optional<SparseIdsMergedSlice> &ending,\n\t\tstd::optional<PhotoId> lastPeerPhotoId) {\n\tif (!lastPeerPhotoId) {\n\t\treturn std::nullopt;\n\t} else if (!*lastPeerPhotoId) {\n\t\treturn false;\n\t}\n\treturn LastFullMsgId(ending ? *ending : slice)\n\t\t| [&](FullMsgId msgId) { return session->data().message(msgId); }\n\t\t| [](HistoryItem *item) { return item ? item->media() : nullptr; }\n\t\t| [](Data::Media *media) { return media ? media->photo() : nullptr; }\n\t\t| [](PhotoData *photo) { return photo ? photo->id : 0; }\n\t\t| [&](PhotoId photoId) { return *lastPeerPhotoId != photoId; };\n}\n\nstd::optional<FullMsgId> SharedMediaWithLastSlice::LastFullMsgId(\n\t\tconst SparseIdsMergedSlice &slice) {\n\tif (slice.fullCount() == 0) {\n\t\treturn FullMsgId();\n\t} else if (slice.size() == 0 || slice.skippedAfter() != 0) {\n\t\treturn std::nullopt;\n\t}\n\treturn slice[slice.size() - 1];\n}\n\nrpl::producer<SharedMediaWithLastSlice> SharedMediaWithLastViewer(\n\t\tnot_null<Main::Session*> session,\n\t\tSharedMediaWithLastSlice::Key key,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\treturn [=](auto consumer) {\n\t\tauto viewerKey = SharedMediaMergedKey(\n\t\t\tSharedMediaWithLastSlice::ViewerKey(key),\n\t\t\tkey.type);\n\n\t\tif (std::get_if<not_null<PhotoData*>>(&key.universalId)) {\n\t\t\treturn SharedMediaMergedViewer(\n\t\t\t\tsession,\n\t\t\t\tstd::move(viewerKey),\n\t\t\t\tlimitBefore,\n\t\t\t\tlimitAfter\n\t\t\t) | rpl::on_next([=](SparseIdsMergedSlice &&update) {\n\t\t\t\tconsumer.put_next(SharedMediaWithLastSlice(\n\t\t\t\t\tsession,\n\t\t\t\t\tkey,\n\t\t\t\t\tstd::move(update),\n\t\t\t\t\tstd::nullopt));\n\t\t\t});\n\t\t}\n\n\t\tif (key.topicRootId == SharedMediaWithLastSlice::kScheduledTopicId) {\n\t\t\treturn SharedScheduledMediaViewer(\n\t\t\t\tsession,\n\t\t\t\tstd::move(viewerKey),\n\t\t\t\tlimitBefore,\n\t\t\t\tlimitAfter\n\t\t\t) | rpl::on_next([=](SparseIdsMergedSlice &&update) {\n\t\t\t\tconsumer.put_next(SharedMediaWithLastSlice(\n\t\t\t\t\tsession,\n\t\t\t\t\tkey,\n\t\t\t\t\tstd::move(update),\n\t\t\t\t\tstd::nullopt));\n\t\t\t});\n\t\t} else if (key.topicRootId == SharedMediaWithLastSlice::kSavedMusicTopicId) {\n\t\t\treturn SavedMusicMediaViewer(\n\t\t\t\tsession,\n\t\t\t\tstd::move(viewerKey),\n\t\t\t\tlimitBefore,\n\t\t\t\tlimitAfter\n\t\t\t) | rpl::on_next([=](SparseIdsMergedSlice &&update) {\n\t\t\t\tconsumer.put_next(SharedMediaWithLastSlice(\n\t\t\t\t\tsession,\n\t\t\t\t\tkey,\n\t\t\t\t\tstd::move(update),\n\t\t\t\t\tstd::nullopt));\n\t\t\t});\n\t\t}\n\t\treturn rpl::combine(\n\t\t\tSharedMediaMergedViewer(\n\t\t\t\tsession,\n\t\t\t\tstd::move(viewerKey),\n\t\t\t\tlimitBefore,\n\t\t\t\tlimitAfter),\n\t\t\tSharedMediaMergedViewer(\n\t\t\t\tsession,\n\t\t\t\tSharedMediaMergedKey(\n\t\t\t\t\tSharedMediaWithLastSlice::EndingKey(key),\n\t\t\t\t\tkey.type),\n\t\t\t\t1,\n\t\t\t\t1)\n\t\t) | rpl::on_next([=](\n\t\t\t\tSparseIdsMergedSlice &&viewer,\n\t\t\t\tSparseIdsMergedSlice &&ending) {\n\t\t\tconsumer.put_next(SharedMediaWithLastSlice(\n\t\t\t\tsession,\n\t\t\t\tkey,\n\t\t\t\tstd::move(viewer),\n\t\t\t\tstd::move(ending)));\n\t\t});\n\t};\n}\n\nrpl::producer<SharedMediaWithLastSlice> SharedMediaWithLastReversedViewer(\n\t\tnot_null<Main::Session*> session,\n\t\tSharedMediaWithLastSlice::Key key,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\treturn SharedMediaWithLastViewer(\n\t\tsession,\n\t\tkey,\n\t\tlimitBefore,\n\t\tlimitAfter\n\t) | rpl::map([](SharedMediaWithLastSlice &&slice) {\n\t\tslice.reverse();\n\t\treturn std::move(slice);\n\t});\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_shared_media.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"storage/storage_shared_media.h\"\n#include \"base/weak_ptr.h\"\n#include \"base/qt/qt_compare.h\"\n#include \"data/data_sparse_ids.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\n[[nodiscard]] std::optional<Storage::SharedMediaType> SharedMediaOverviewType(\n\tStorage::SharedMediaType type);\nbool SharedMediaAllowSearch(Storage::SharedMediaType type);\n\nrpl::producer<SparseIdsSlice> SharedMediaViewer(\n\tnot_null<Main::Session*> session,\n\tStorage::SharedMediaKey key,\n\tint limitBefore,\n\tint limitAfter);\n\nstruct SharedMediaMergedKey {\n\tusing Type = Storage::SharedMediaType;\n\n\tSharedMediaMergedKey(\n\t\tSparseIdsMergedSlice::Key mergedKey,\n\t\tType type)\n\t: mergedKey(mergedKey)\n\t, type(type) {\n\t}\n\n\tbool operator==(const SharedMediaMergedKey &other) const {\n\t\treturn (mergedKey == other.mergedKey)\n\t\t\t&& (type == other.type);\n\t}\n\n\tSparseIdsMergedSlice::Key mergedKey;\n\tType type = Type::kCount;\n\n};\n\nrpl::producer<SparseIdsMergedSlice> SharedScheduledMediaViewer(\n\tnot_null<Main::Session*> session,\n\tSharedMediaMergedKey key,\n\tint limitBefore,\n\tint limitAfter);\n\nrpl::producer<SparseIdsMergedSlice> SavedMusicMediaViewer(\n\tnot_null<Main::Session*> session,\n\tSharedMediaMergedKey key,\n\tint limitBefore,\n\tint limitAfter);\n\nrpl::producer<SparseIdsMergedSlice> SharedMediaMergedViewer(\n\tnot_null<Main::Session*> session,\n\tSharedMediaMergedKey key,\n\tint limitBefore,\n\tint limitAfter);\n\nclass SharedMediaWithLastSlice {\npublic:\n\tusing Type = Storage::SharedMediaType;\n\n\tusing Value = std::variant<FullMsgId, not_null<PhotoData*>>;\n\tusing MessageId = SparseIdsMergedSlice::UniversalMsgId;\n\tusing UniversalMsgId = std::variant<\n\t\tMessageId,\n\t\tnot_null<PhotoData*>>;\n\n\tstatic constexpr auto kScheduledTopicId\n\t\t= SparseIdsMergedSlice::kScheduledTopicId;\n\tstatic constexpr auto kSavedMusicTopicId\n\t\t= SparseIdsMergedSlice::kSavedMusicTopicId;\n\tstruct Key {\n\t\tKey(\n\t\t\tPeerId peerId,\n\t\t\tMsgId topicRootId,\n\t\t\tPeerId monoforumPeerId,\n\t\t\tPeerId migratedPeerId,\n\t\t\tType type,\n\t\t\tUniversalMsgId universalId)\n\t\t: peerId(peerId)\n\t\t, topicRootId(topicRootId)\n\t\t, monoforumPeerId(monoforumPeerId)\n\t\t, migratedPeerId(migratedPeerId)\n\t\t, type(type)\n\t\t, universalId(universalId) {\n\t\t\tExpects(v::is<MessageId>(universalId) || type == Type::ChatPhoto);\n\t\t}\n\n\t\tfriend inline constexpr auto operator<=>(\n\t\t\tconst Key&,\n\t\t\tconst Key&) = default;\n\n\t\tPeerId peerId = 0;\n\t\tMsgId topicRootId = 0;\n\t\tPeerId monoforumPeerId = 0;\n\t\tPeerId migratedPeerId = 0;\n\t\tType type = Type::kCount;\n\t\tUniversalMsgId universalId;\n\n\t};\n\n\tSharedMediaWithLastSlice(\n\t\tnot_null<Main::Session*> session,\n\t\tKey key);\n\tSharedMediaWithLastSlice(\n\t\tnot_null<Main::Session*> session,\n\t\tKey key,\n\t\tSparseIdsMergedSlice slice,\n\t\tstd::optional<SparseIdsMergedSlice> ending);\n\n\tstd::optional<int> fullCount() const;\n\tstd::optional<int> skippedBefore() const;\n\tstd::optional<int> skippedAfter() const;\n\tstd::optional<int> indexOf(Value fullId) const;\n\tint size() const;\n\tValue operator[](int index) const;\n\tstd::optional<int> distance(const Key &a, const Key &b) const;\n\n\tvoid reverse();\n\n\tstatic SparseIdsMergedSlice::Key ViewerKey(const Key &key) {\n\t\treturn {\n\t\t\tkey.peerId,\n\t\t\tkey.topicRootId,\n\t\t\tkey.monoforumPeerId,\n\t\t\tkey.migratedPeerId,\n\t\t\tv::is<MessageId>(key.universalId)\n\t\t\t\t? v::get<MessageId>(key.universalId)\n\t\t\t\t: ServerMaxMsgId - 1\n\t\t};\n\t}\n\tstatic SparseIdsMergedSlice::Key EndingKey(const Key &key) {\n\t\treturn {\n\t\t\tkey.peerId,\n\t\t\tkey.topicRootId,\n\t\t\tkey.monoforumPeerId,\n\t\t\tkey.migratedPeerId,\n\t\t\tServerMaxMsgId - 1\n\t\t};\n\t}\n\nprivate:\n\tstatic std::optional<SparseIdsMergedSlice> EndingSlice(const Key &key) {\n\t\treturn v::is<MessageId>(key.universalId)\n\t\t\t? base::make_optional(SparseIdsMergedSlice(EndingKey(key)))\n\t\t\t: std::nullopt;\n\t}\n\n\tstatic std::optional<PhotoId> LastPeerPhotoId(\n\t\tnot_null<Main::Session*> session,\n\t\tPeerId peerId);\n\tstatic std::optional<bool> IsLastIsolated(\n\t\tnot_null<Main::Session*> session,\n\t\tconst SparseIdsMergedSlice &slice,\n\t\tconst std::optional<SparseIdsMergedSlice> &ending,\n\t\tstd::optional<PhotoId> lastPeerPhotoId);\n\tstatic std::optional<FullMsgId> LastFullMsgId(\n\t\tconst SparseIdsMergedSlice &slice);\n\tstatic std::optional<int> Add(\n\t\t\tconst std::optional<int> &a,\n\t\t\tconst std::optional<int> &b) {\n\t\treturn (a && b) ? base::make_optional(*a + *b) : std::nullopt;\n\t}\n\tstatic Value ComputeId(PeerId peerId, MsgId msgId) {\n\t\treturn FullMsgId(peerId, msgId);\n\t}\n\tstatic Value ComputeId(const Key &key) {\n\t\tif (const auto messageId = std::get_if<MessageId>(&key.universalId)) {\n\t\t\treturn (*messageId >= 0)\n\t\t\t\t? ComputeId(key.peerId, *messageId)\n\t\t\t\t: ComputeId(key.migratedPeerId, ServerMaxMsgId + *messageId);\n\t\t}\n\t\treturn v::get<not_null<PhotoData*>>(key.universalId);\n\t}\n\n\tbool isolatedInSlice() const {\n\t\treturn (_slice.skippedAfter() != 0);\n\t}\n\tstd::optional<int> lastPhotoSkip() const {\n\t\treturn _isolatedLastPhoto\n\t\t\t| [](bool isolated) { return isolated ? 1 : 0; };\n\t}\n\n\tstd::optional<int> skippedBeforeImpl() const;\n\tstd::optional<int> skippedAfterImpl() const;\n\tstd::optional<int> indexOfImpl(Value fullId) const;\n\n\tnot_null<Main::Session*> _session;\n\tKey _key;\n\tSparseIdsMergedSlice _slice;\n\tstd::optional<SparseIdsMergedSlice> _ending;\n\tstd::optional<PhotoId> _lastPhotoId;\n\tstd::optional<bool> _isolatedLastPhoto;\n\tbool _reversed = false;\n\n};\n\nrpl::producer<SharedMediaWithLastSlice> SharedMediaWithLastViewer(\n\tnot_null<Main::Session*> session,\n\tSharedMediaWithLastSlice::Key key,\n\tint limitBefore,\n\tint limitAfter);\n\nrpl::producer<SharedMediaWithLastSlice> SharedMediaWithLastReversedViewer(\n\tnot_null<Main::Session*> session,\n\tSharedMediaWithLastSlice::Key key,\n\tint limitBefore,\n\tint limitAfter);\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_sparse_ids.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_sparse_ids.h\"\n\n#include <rpl/combine.h>\n#include \"storage/storage_sparse_ids_list.h\"\n\nSparseIdsMergedSlice::SparseIdsMergedSlice(Key key)\n: SparseIdsMergedSlice(\n\tkey,\n\tSparseIdsSlice(),\n\tMigratedSlice(key)) {\n}\n\nSparseIdsMergedSlice::SparseIdsMergedSlice(\n\tKey key,\n\tSparseIdsSlice part,\n\tstd::optional<SparseIdsSlice> migrated)\n: _key(key)\n, _part(std::move(part))\n, _migrated(std::move(migrated)) {\n}\n\nSparseIdsMergedSlice::SparseIdsMergedSlice(\n\tKey key,\n\tSparseUnsortedIdsSlice unsorted)\n: _key(key)\n, _unsorted(std::move(unsorted)) {\n}\n\nstd::optional<int> SparseIdsMergedSlice::fullCount() const {\n\treturn _unsorted\n\t\t? _unsorted->fullCount()\n\t\t: Add(\n\t\t\t_part.fullCount(),\n\t\t\t_migrated ? _migrated->fullCount() : 0);\n}\n\nstd::optional<int> SparseIdsMergedSlice::skippedBefore() const {\n\treturn _unsorted\n\t\t? _unsorted->skippedBefore()\n\t\t: Add(\n\t\t\tisolatedInMigrated() ? 0 : _part.skippedBefore(),\n\t\t\t_migrated\n\t\t\t\t? (isolatedInPart()\n\t\t\t\t\t? _migrated->fullCount()\n\t\t\t\t\t: _migrated->skippedBefore())\n\t\t\t\t: 0\n\t\t);\n}\n\nstd::optional<int> SparseIdsMergedSlice::skippedAfter() const {\n\treturn _unsorted\n\t\t? _unsorted->skippedAfter()\n\t\t: Add(\n\t\t\tisolatedInMigrated() ? _part.fullCount() : _part.skippedAfter(),\n\t\t\tisolatedInPart() ? 0 : _migrated->skippedAfter()\n\t\t);\n}\n\nstd::optional<int> SparseIdsMergedSlice::indexOf(\n\t\tFullMsgId fullId) const {\n\treturn _unsorted\n\t\t? _unsorted->indexOf(fullId.msg)\n\t\t: isFromPart(fullId)\n\t\t? (_part.indexOf(fullId.msg) | func::add(migratedSize()))\n\t\t: isolatedInPart()\n\t\t\t? std::nullopt\n\t\t\t: isFromMigrated(fullId)\n\t\t\t\t? _migrated->indexOf(fullId.msg)\n\t\t\t\t: std::nullopt;\n}\n\nint SparseIdsMergedSlice::size() const {\n\treturn _unsorted\n\t\t? _unsorted->size()\n\t\t: (isolatedInPart() ? 0 : migratedSize())\n\t\t\t+ (isolatedInMigrated() ? 0 : _part.size());\n}\n\nFullMsgId SparseIdsMergedSlice::operator[](int index) const {\n\tExpects(index >= 0 && index < size());\n\n\tif (_unsorted) {\n\t\treturn ComputeId(_key.peerId, (*_unsorted)[index]);\n\t}\n\n\tif (const auto size = migratedSize()) {\n\t\tif (index < size) {\n\t\t\treturn ComputeId(_key.migratedPeerId, (*_migrated)[index]);\n\t\t}\n\t\tindex -= size;\n\t}\n\treturn ComputeId(_key.peerId, _part[index]);\n}\n\nstd::optional<int> SparseIdsMergedSlice::distance(\n\t\tconst Key &a,\n\t\tconst Key &b) const {\n\tif (const auto i = indexOf(ComputeId(a))) {\n\t\tif (const auto j = indexOf(ComputeId(b))) {\n\t\t\treturn *j - *i;\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nauto SparseIdsMergedSlice::nearest(\n\t\tUniversalMsgId id) const -> std::optional<FullMsgId> {\n\tif (_unsorted) {\n\t\tif (_unsorted->indexOf(id).has_value()) {\n\t\t\treturn ComputeId(_key.peerId, id);\n\t\t} else if (const auto count = _unsorted->size()) {\n\t\t\treturn ComputeId(_key.peerId, (*_unsorted)[count / 2]);\n\t\t}\n\t\treturn std::nullopt;\n\t}\n\tconst auto convertFromPartNearest = [&](MsgId result) {\n\t\treturn ComputeId(_key.peerId, result);\n\t};\n\tconst auto convertFromMigratedNearest = [&](MsgId result) {\n\t\treturn ComputeId(_key.migratedPeerId, result);\n\t};\n\tif (IsServerMsgId(id)) {\n\t\tif (auto partNearestId = _part.nearest(id)) {\n\t\t\treturn partNearestId\n\t\t\t\t| convertFromPartNearest;\n\t\t} else if (isolatedInPart()) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\treturn _migrated->nearest(ServerMaxMsgId - 1)\n\t\t\t| convertFromMigratedNearest;\n\t}\n\tif (auto migratedNearestId = _migrated\n\t\t? _migrated->nearest(id + ServerMaxMsgId)\n\t\t: std::nullopt) {\n\t\treturn migratedNearestId\n\t\t\t| convertFromMigratedNearest;\n\t} else if (isolatedInMigrated()) {\n\t\treturn std::nullopt;\n\t}\n\treturn _part.nearest(0)\n\t\t| convertFromPartNearest;\n}\n\nSparseIdsSliceBuilder::SparseIdsSliceBuilder(\n\tKey key,\n\tint limitBefore,\n\tint limitAfter)\n: _key(key)\n, _limitBefore(limitBefore)\n, _limitAfter(limitAfter) {\n}\n\nbool SparseIdsSliceBuilder::applyInitial(\n\t\tconst Storage::SparseIdsListResult &result) {\n\tmergeSliceData(\n\t\tresult.count,\n\t\tresult.messageIds,\n\t\tresult.skippedBefore,\n\t\tresult.skippedAfter);\n\treturn true;\n}\n\nbool SparseIdsSliceBuilder::applyUpdate(\n\t\tconst Storage::SparseIdsSliceUpdate &update) {\n\tauto intersects = [](MsgRange range1, MsgRange range2) {\n\t\treturn (range1.from <= range2.till)\n\t\t\t&& (range2.from <= range1.till);\n\t};\n\tauto needMergeMessages = (update.messages != nullptr)\n\t\t&& intersects(update.range, {\n\t\t\t_ids.empty() ? _key : _ids.front(),\n\t\t\t_ids.empty() ? _key : _ids.back()\n\t\t});\n\tif (!needMergeMessages && !update.count) {\n\t\treturn false;\n\t}\n\tauto skippedBefore = (update.range.from == 0)\n\t\t? 0\n\t\t: std::optional<int> {};\n\tauto skippedAfter = (update.range.till == ServerMaxMsgId)\n\t\t? 0\n\t\t: std::optional<int> {};\n\tmergeSliceData(\n\t\tupdate.count,\n\t\tneedMergeMessages\n\t\t\t? *update.messages\n\t\t\t: base::flat_set<MsgId> {},\n\t\tskippedBefore,\n\t\tskippedAfter);\n\treturn true;\n}\n\nbool SparseIdsSliceBuilder::removeOne(MsgId messageId) {\n\tauto changed = false;\n\tif (_fullCount && *_fullCount > 0) {\n\t\t--*_fullCount;\n\t\tchanged = true;\n\t}\n\tif (_ids.contains(messageId)) {\n\t\t_ids.remove(messageId);\n\t\tchanged = true;\n\t} else if (!_ids.empty()) {\n\t\tif (_ids.front() > messageId\n\t\t\t&& _skippedBefore\n\t\t\t&& *_skippedBefore > 0) {\n\t\t\t--*_skippedBefore;\n\t\t\tchanged = true;\n\t\t} else if (_ids.back() < messageId\n\t\t\t&& _skippedAfter\n\t\t\t&& *_skippedAfter > 0) {\n\t\t\t--*_skippedAfter;\n\t\t\tchanged = true;\n\t\t}\n\t}\n\tif (changed) {\n\t\tcheckInsufficient();\n\t}\n\treturn changed;\n}\n\nbool SparseIdsSliceBuilder::removeAll() {\n\t_ids = {};\n\t_fullCount = 0;\n\t_skippedBefore = 0;\n\t_skippedAfter = 0;\n\treturn true;\n}\n\nbool SparseIdsSliceBuilder::invalidateBottom() {\n\t_fullCount = _skippedAfter = std::nullopt;\n\tcheckInsufficient();\n\treturn true;\n}\n\nvoid SparseIdsSliceBuilder::checkInsufficient() {\n\tsliceToLimits();\n}\n\nvoid SparseIdsSliceBuilder::mergeSliceData(\n\t\tstd::optional<int> count,\n\t\tconst base::flat_set<MsgId> &messageIds,\n\t\tstd::optional<int> skippedBefore,\n\t\tstd::optional<int> skippedAfter) {\n\tif (messageIds.empty()) {\n\t\tif (count && _fullCount != count) {\n\t\t\t_fullCount = count;\n\t\t\tif (*_fullCount <= _ids.size()) {\n\t\t\t\t_fullCount = _ids.size();\n\t\t\t\t_skippedBefore = _skippedAfter = 0;\n\t\t\t}\n\t\t}\n\t\tfillSkippedAndSliceToLimits();\n\t\treturn;\n\t}\n\tif (count) {\n\t\t_fullCount = count;\n\t}\n\tauto wasMinId = _ids.empty() ? -1 : _ids.front();\n\tauto wasMaxId = _ids.empty() ? -1 : _ids.back();\n\t_ids.merge(messageIds.begin(), messageIds.end());\n\n\tauto adjustSkippedBefore = [&](MsgId oldId, int oldSkippedBefore) {\n\t\tauto it = _ids.find(oldId);\n\t\tAssert(it != _ids.end());\n\t\t_skippedBefore = oldSkippedBefore - (it - _ids.begin());\n\t\taccumulate_max(*_skippedBefore, 0);\n\t};\n\tif (skippedBefore) {\n\t\tadjustSkippedBefore(messageIds.front(), *skippedBefore);\n\t} else if (wasMinId >= 0 && _skippedBefore) {\n\t\tadjustSkippedBefore(wasMinId, *_skippedBefore);\n\t} else {\n\t\t_skippedBefore = std::nullopt;\n\t}\n\n\tauto adjustSkippedAfter = [&](MsgId oldId, int oldSkippedAfter) {\n\t\tauto it = _ids.find(oldId);\n\t\tAssert(it != _ids.end());\n\t\t_skippedAfter = oldSkippedAfter - (_ids.end() - it - 1);\n\t\taccumulate_max(*_skippedAfter, 0);\n\t};\n\tif (skippedAfter) {\n\t\tadjustSkippedAfter(messageIds.back(), *skippedAfter);\n\t} else if (wasMaxId >= 0 && _skippedAfter) {\n\t\tadjustSkippedAfter(wasMaxId, *_skippedAfter);\n\t} else {\n\t\t_skippedAfter = std::nullopt;\n\t}\n\tfillSkippedAndSliceToLimits();\n}\n\nvoid SparseIdsSliceBuilder::fillSkippedAndSliceToLimits() {\n\tif (_fullCount) {\n\t\tif (_skippedBefore && !_skippedAfter) {\n\t\t\t_skippedAfter = *_fullCount\n\t\t\t\t- *_skippedBefore\n\t\t\t\t- int(_ids.size());\n\t\t} else if (_skippedAfter && !_skippedBefore) {\n\t\t\t_skippedBefore = *_fullCount\n\t\t\t\t- *_skippedAfter\n\t\t\t\t- int(_ids.size());\n\t\t}\n\t}\n\tsliceToLimits();\n}\n\nvoid SparseIdsSliceBuilder::sliceToLimits() {\n\tif (!_key) {\n\t\tif (!_fullCount) {\n\t\t\trequestMessagesCount();\n\t\t}\n\t\treturn;\n\t}\n\tauto requestedSomething = false;\n\tauto aroundIt = ranges::lower_bound(_ids, _key);\n\tauto removeFromBegin = (aroundIt - _ids.begin() - _limitBefore);\n\tauto removeFromEnd = (_ids.end() - aroundIt - _limitAfter - 1);\n\tif (removeFromBegin > 0) {\n\t\t_ids.erase(_ids.begin(), _ids.begin() + removeFromBegin);\n\t\tif (_skippedBefore) {\n\t\t\t*_skippedBefore += removeFromBegin;\n\t\t}\n\t} else if (removeFromBegin < 0\n\t\t&& (!_skippedBefore || *_skippedBefore > 0)) {\n\t\trequestedSomething = true;\n\t\trequestMessages(RequestDirection::Before);\n\t}\n\tif (removeFromEnd > 0) {\n\t\t_ids.erase(_ids.end() - removeFromEnd, _ids.end());\n\t\tif (_skippedAfter) {\n\t\t\t*_skippedAfter += removeFromEnd;\n\t\t}\n\t} else if (removeFromEnd < 0\n\t\t&& (!_skippedAfter || *_skippedAfter > 0)) {\n\t\trequestedSomething = true;\n\t\trequestMessages(RequestDirection::After);\n\t}\n\tif (!_fullCount && !requestedSomething) {\n\t\trequestMessagesCount();\n\t}\n}\n\nvoid SparseIdsSliceBuilder::requestMessages(\n\t\tRequestDirection direction) {\n\tauto requestAroundData = [&]() -> AroundData {\n\t\tif (_ids.empty()) {\n\t\t\treturn { _key, Data::LoadDirection::Around };\n\t\t} else if (direction == RequestDirection::Before) {\n\t\t\treturn { _ids.front(), Data::LoadDirection::Before };\n\t\t}\n\t\treturn { _ids.back(), Data::LoadDirection::After };\n\t};\n\t_insufficientAround.fire(requestAroundData());\n}\n\nvoid SparseIdsSliceBuilder::requestMessagesCount() {\n\t_insufficientAround.fire({ 0, Data::LoadDirection::Around });\n}\n\nSparseIdsSlice SparseIdsSliceBuilder::snapshot() const {\n\treturn SparseIdsSlice(\n\t\t_ids,\n\t\t_fullCount,\n\t\t_skippedBefore,\n\t\t_skippedAfter);\n}\n\nrpl::producer<SparseIdsMergedSlice> SparseIdsMergedSlice::CreateViewer(\n\t\tSparseIdsMergedSlice::Key key,\n\t\tint limitBefore,\n\t\tint limitAfter,\n\t\tFn<SimpleViewerFunction> simpleViewer) {\n\tExpects(!key.topicRootId\n\t\t|| (!key.monoforumPeerId && !key.migratedPeerId));\n\tExpects(!key.monoforumPeerId\n\t\t|| (!key.topicRootId && !key.migratedPeerId));\n\tExpects(IsServerMsgId(key.universalId)\n\t\t|| (key.universalId == 0)\n\t\t|| (IsServerMsgId(ServerMaxMsgId + key.universalId) && key.migratedPeerId != 0));\n\tExpects((key.universalId != 0)\n\t\t|| (limitBefore == 0 && limitAfter == 0));\n\n\treturn [=](auto consumer) {\n\t\tauto partViewer = simpleViewer(\n\t\t\tkey.peerId,\n\t\t\tkey.topicRootId,\n\t\t\tkey.monoforumPeerId,\n\t\t\tSparseIdsMergedSlice::PartKey(key),\n\t\t\tlimitBefore,\n\t\t\tlimitAfter\n\t\t);\n\t\tif (!key.migratedPeerId) {\n\t\t\treturn std::move(\n\t\t\t\tpartViewer\n\t\t\t) | rpl::on_next([=](SparseIdsSlice &&part) {\n\t\t\t\tconsumer.put_next(SparseIdsMergedSlice(\n\t\t\t\t\tkey,\n\t\t\t\t\tstd::move(part),\n\t\t\t\t\tstd::nullopt));\n\t\t\t});\n\t\t}\n\t\tauto migratedViewer = simpleViewer(\n\t\t\tkey.migratedPeerId,\n\t\t\tMsgId(0), // topicRootId\n\t\t\tPeerId(0), // monoforumPeerId\n\t\t\tSparseIdsMergedSlice::MigratedKey(key),\n\t\t\tlimitBefore,\n\t\t\tlimitAfter);\n\t\treturn rpl::combine(\n\t\t\tstd::move(partViewer),\n\t\t\tstd::move(migratedViewer)\n\t\t) | rpl::on_next([=](\n\t\t\t\tSparseIdsSlice &&part,\n\t\t\t\tSparseIdsSlice &&migrated) {\n\t\t\tconsumer.put_next(SparseIdsMergedSlice(\n\t\t\t\tkey,\n\t\t\t\tstd::move(part),\n\t\t\t\tstd::move(migrated)));\n\t\t});\n\t};\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_sparse_ids.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_abstract_sparse_ids.h\"\n#include \"data/data_messages.h\"\n\nnamespace Storage {\nstruct SparseIdsListResult;\nstruct SparseIdsSliceUpdate;\n} // namespace Storage\n\nclass SparseIdsSlice final : public AbstractSparseIds<base::flat_set<MsgId>> {\npublic:\n\tusing Key = MsgId;\n\tusing AbstractSparseIds<base::flat_set<MsgId>>::AbstractSparseIds;\n\n};\n\nusing SparseUnsortedIdsSlice = AbstractSparseIds<std::vector<MsgId>>;\n\nclass SparseIdsMergedSlice {\npublic:\n\tusing UniversalMsgId = MsgId;\n\tstatic constexpr MsgId kScheduledTopicId = ScheduledMaxMsgId;\n\tstatic constexpr MsgId kSavedMusicTopicId = ScheduledMaxMsgId + 1;\n\n\tstruct Key {\n\t\tKey(\n\t\t\tPeerId peerId,\n\t\t\tMsgId topicRootId,\n\t\t\tPeerId monoforumPeerId,\n\t\t\tPeerId migratedPeerId,\n\t\t\tUniversalMsgId universalId)\n\t\t: peerId(peerId)\n\t\t, topicRootId(topicRootId)\n\t\t, monoforumPeerId(monoforumPeerId)\n\t\t, migratedPeerId((topicRootId || monoforumPeerId) ? 0 : migratedPeerId)\n\t\t, universalId(universalId) {\n\t\t}\n\n\t\tfriend inline constexpr bool operator==(\n\t\t\tconst Key &,\n\t\t\tconst Key &) = default;\n\n\t\tPeerId peerId = 0;\n\t\tMsgId topicRootId = 0;\n\t\tPeerId monoforumPeerId = 0;\n\t\tPeerId migratedPeerId = 0;\n\t\tUniversalMsgId universalId = 0;\n\t};\n\n\tSparseIdsMergedSlice(Key key);\n\tSparseIdsMergedSlice(\n\t\tKey key,\n\t\tSparseIdsSlice part,\n\t\tstd::optional<SparseIdsSlice> migrated);\n\tSparseIdsMergedSlice(\n\t\tKey key,\n\t\tSparseUnsortedIdsSlice unsorted);\n\n\tstd::optional<int> fullCount() const;\n\tstd::optional<int> skippedBefore() const;\n\tstd::optional<int> skippedAfter() const;\n\tstd::optional<int> indexOf(FullMsgId fullId) const;\n\tint size() const;\n\tFullMsgId operator[](int index) const;\n\tstd::optional<int> distance(const Key &a, const Key &b) const;\n\tstd::optional<FullMsgId> nearest(UniversalMsgId id) const;\n\n\tusing SimpleViewerFunction = rpl::producer<SparseIdsSlice>(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tSparseIdsSlice::Key simpleKey,\n\t\tint limitBefore,\n\t\tint limitAfter);\n\tstatic rpl::producer<SparseIdsMergedSlice> CreateViewer(\n\t\tSparseIdsMergedSlice::Key key,\n\t\tint limitBefore,\n\t\tint limitAfter,\n\t\tFn<SimpleViewerFunction> simpleViewer);\n\nprivate:\n\tstatic SparseIdsSlice::Key PartKey(const Key &key) {\n\t\treturn (key.universalId < 0) ? 1 : key.universalId;\n\t}\n\tstatic SparseIdsSlice::Key MigratedKey(const Key &key) {\n\t\treturn (key.universalId < 0)\n\t\t\t? (ServerMaxMsgId + key.universalId)\n\t\t\t: (key.universalId > 0) ? (ServerMaxMsgId - 1) : 0;\n\t}\n\tstatic std::optional<SparseIdsSlice> MigratedSlice(const Key &key) {\n\t\treturn key.migratedPeerId\n\t\t\t? base::make_optional(SparseIdsSlice())\n\t\t\t: std::nullopt;\n\t}\n\n\tstatic bool IsFromSlice(PeerId peerId, FullMsgId fullId) {\n\t\treturn (peerId == fullId.peer);\n\t}\n\tstatic FullMsgId ComputeId(PeerId peerId, MsgId msgId) {\n\t\treturn FullMsgId(peerId, msgId);\n\t}\n\tstatic FullMsgId ComputeId(const Key &key) {\n\t\treturn (key.universalId >= 0)\n\t\t\t? ComputeId(key.peerId, key.universalId)\n\t\t\t: ComputeId(key.migratedPeerId, ServerMaxMsgId + key.universalId);\n\t}\n\tstatic std::optional<int> Add(\n\t\t\tconst std::optional<int> &a,\n\t\t\tconst std::optional<int> &b) {\n\t\treturn (a && b) ? base::make_optional(*a + *b) : std::nullopt;\n\t}\n\n\tbool isFromPart(FullMsgId fullId) const {\n\t\treturn IsFromSlice(_key.peerId, fullId);\n\t}\n\tbool isFromMigrated(FullMsgId fullId) const {\n\t\treturn _migrated\n\t\t\t? IsFromSlice(_key.migratedPeerId, fullId)\n\t\t\t: false;\n\t}\n\tint migratedSize() const {\n\t\treturn isolatedInPart() ? 0 : _migrated->size();\n\t}\n\tbool isolatedInPart() const {\n\t\treturn IsServerMsgId(_key.universalId)\n\t\t\t&& (!_migrated || _part.skippedBefore() != 0);\n\t}\n\tbool isolatedInMigrated() const {\n\t\treturn IsServerMsgId(ServerMaxMsgId + _key.universalId)\n\t\t\t&& (_migrated->skippedAfter() != 0);\n\t}\n\n\tKey _key;\n\tSparseIdsSlice _part;\n\tstd::optional<SparseIdsSlice> _migrated;\n\tstd::optional<SparseUnsortedIdsSlice> _unsorted;\n\n};\n\nclass SparseIdsSliceBuilder {\npublic:\n\tusing Key = SparseIdsSlice::Key;\n\n\tSparseIdsSliceBuilder(Key key, int limitBefore, int limitAfter);\n\n\tbool applyInitial(const Storage::SparseIdsListResult &result);\n\tbool applyUpdate(const Storage::SparseIdsSliceUpdate &update);\n\tbool removeOne(MsgId messageId);\n\tbool removeAll();\n\tbool invalidateBottom();\n\n\tvoid checkInsufficient();\n\tstruct AroundData {\n\t\tMsgId aroundId = 0;\n\t\tData::LoadDirection direction = Data::LoadDirection::Around;\n\n\t\tinline bool operator<(const AroundData &other) const {\n\t\t\treturn (aroundId < other.aroundId)\n\t\t\t\t|| ((aroundId == other.aroundId)\n\t\t\t\t\t&& (direction < other.direction));\n\t\t}\n\t};\n\tauto insufficientAround() const {\n\t\treturn _insufficientAround.events();\n\t}\n\n\tSparseIdsSlice snapshot() const;\n\nprivate:\n\tenum class RequestDirection {\n\t\tBefore,\n\t\tAfter,\n\t};\n\tvoid requestMessages(RequestDirection direction);\n\tvoid requestMessagesCount();\n\tvoid fillSkippedAndSliceToLimits();\n\tvoid sliceToLimits();\n\n\tvoid mergeSliceData(\n\t\tstd::optional<int> count,\n\t\tconst base::flat_set<MsgId> &messageIds,\n\t\tstd::optional<int> skippedBefore = std::nullopt,\n\t\tstd::optional<int> skippedAfter = std::nullopt);\n\n\tKey _key;\n\tbase::flat_set<MsgId> _ids;\n\tstd::optional<int> _fullCount;\n\tstd::optional<int> _skippedBefore;\n\tstd::optional<int> _skippedAfter;\n\tint _limitBefore = 0;\n\tint _limitAfter = 0;\n\n\trpl::event_stream<AroundData> _insufficientAround;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_star_gift.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_star_gift.h\"\n\n#include \"api/api_premium.h\"\n#include \"apiwrap.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"lang/lang_tag.h\"\n#include \"main/main_session.h\"\n#include \"ui/controls/ton_common.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"styles/style_credits.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kMyGiftsPerPage = 50;\nconstexpr auto kResaleGiftsPerPage = 50;\nconstexpr auto kCraftGiftsPerPage = 50;\n\n[[nodiscard]] MTPStarGiftAttributeId AttributeToTL(GiftAttributeId id) {\n\tswitch (id.type) {\n\tcase GiftAttributeIdType::Backdrop:\n\t\treturn MTP_starGiftAttributeIdBackdrop(\n\t\t\tMTP_int(int32(uint32(id.value))));\n\tcase GiftAttributeIdType::Model:\n\t\treturn MTP_starGiftAttributeIdModel(MTP_long(id.value));\n\tcase GiftAttributeIdType::Pattern:\n\t\treturn MTP_starGiftAttributeIdPattern(MTP_long(id.value));\n\t}\n\tUnexpected(\"Invalid attribute id type\");\n}\n\n[[nodiscard]] GiftAttributeId FromTL(const MTPStarGiftAttributeId &id) {\n\treturn id.match([&](const MTPDstarGiftAttributeIdBackdrop &data) {\n\t\treturn GiftAttributeId{\n\t\t\t.value = uint64(uint32(data.vbackdrop_id().v)),\n\t\t\t.type = GiftAttributeIdType::Backdrop,\n\t\t};\n\t}, [&](const MTPDstarGiftAttributeIdModel &data) {\n\t\treturn GiftAttributeId{\n\t\t\t.value = data.vdocument_id().v,\n\t\t\t.type = GiftAttributeIdType::Model,\n\t\t};\n\t}, [&](const MTPDstarGiftAttributeIdPattern &data) {\n\t\treturn GiftAttributeId{\n\t\t\t.value = data.vdocument_id().v,\n\t\t\t.type = GiftAttributeIdType::Pattern,\n\t\t};\n\t});\n}\n\n} // namespace\n\nQString UniqueGiftName(const UniqueGift &gift) {\n\treturn UniqueGiftName(gift.title, gift.number);\n}\n\nQString UniqueGiftName(const QString &title, int number) {\n\treturn title + u\" #\"_q + Lang::FormatCountDecimal(number);\n}\n\nQString UniqueGiftRarityText(UniqueGiftRarity type) {\n\tswitch (type) {\n\tcase UniqueGiftRarity::Uncommon:\n\t\treturn tr::lng_gift_uncommon_tag(tr::now);\n\tcase UniqueGiftRarity::Rare:\n\t\treturn tr::lng_gift_rare_tag(tr::now);\n\tcase UniqueGiftRarity::Epic:\n\t\treturn tr::lng_gift_epic_tag(tr::now);\n\tcase UniqueGiftRarity::Legendary:\n\t\treturn tr::lng_gift_legendary_tag(tr::now);\n\t}\n\treturn QString();\n}\n\nUniqueGiftRarityColors UniqueGiftRarityBadgeColors(UniqueGiftRarity type) {\n\tconstexpr auto kBgOpacity = 0.15;\n\tconst auto base = [&] {\n\t\tswitch (type) {\n\t\tcase UniqueGiftRarity::Uncommon: return QColor(0x40, 0xA9, 0x20);\n\t\tcase UniqueGiftRarity::Rare: return QColor(0x11, 0xAA, 0xBE);\n\t\tcase UniqueGiftRarity::Epic: return QColor(0x95, 0x5C, 0xDB);\n\t\tcase UniqueGiftRarity::Legendary: return QColor(0xBF, 0x76, 0x00);\n\t\t}\n\t\tUnexpected(\"Invalid rarity type.\");\n\t}();\n\tauto bg = base;\n\tbg.setAlphaF(kBgOpacity);\n\treturn { bg, base };\n}\n\nQString UniqueGiftAttributeText(const UniqueGiftAttribute &attribute) {\n\tconst auto type = attribute.rarityType();\n\tif (type != UniqueGiftRarity::Default) {\n\t\treturn UniqueGiftRarityText(type);\n\t}\n\tconst auto permille = attribute.rarityPermille();\n\treturn (permille > 0)\n\t\t? (QString::number(permille / 10.) + '%')\n\t\t: u\"< 0.1%\"_q;\n}\n\nbool UniqueGiftAttributeHasSpecialRarity(const UniqueGiftAttribute &attribute) {\n\treturn attribute.rarityType() != UniqueGiftRarity::Default;\n}\n\nCreditsAmount UniqueGiftResaleStars(const UniqueGift &gift) {\n\treturn CreditsAmount(gift.starsForResale);\n}\n\nCreditsAmount UniqueGiftResaleTon(const UniqueGift &gift) {\n\treturn CreditsAmount(\n\t\tgift.nanoTonForResale / Ui::kNanosInOne,\n\t\tgift.nanoTonForResale % Ui::kNanosInOne,\n\t\tCreditsType::Ton);\n}\n\nCreditsAmount UniqueGiftResaleAsked(const UniqueGift &gift) {\n\treturn gift.onlyAcceptTon\n\t\t? UniqueGiftResaleTon(gift)\n\t\t: UniqueGiftResaleStars(gift);\n}\n\nTextWithEntities FormatGiftResaleStars(const UniqueGift &gift) {\n\treturn Ui::Text::IconEmoji(\n\t\t&st::starIconEmoji\n\t).append(Lang::FormatCountDecimal(gift.starsForResale));\n}\n\nTextWithEntities FormatGiftResaleTon(const UniqueGift &gift) {\n\treturn Ui::Text::IconEmoji(\n\t\t&st::tonIconEmoji\n\t).append(Lang::FormatCreditsAmountDecimal(UniqueGiftResaleTon(gift)));\n}\n\nTextWithEntities FormatGiftResaleAsked(const UniqueGift &gift) {\n\treturn gift.onlyAcceptTon\n\t\t? FormatGiftResaleTon(gift)\n\t\t: FormatGiftResaleStars(gift);\n}\n\nGiftAttributeId IdFor(const UniqueGiftBackdrop &value) {\n\treturn {\n\t\t.value = uint64(uint32(value.id)),\n\t\t.type = GiftAttributeIdType::Backdrop,\n\t};\n}\n\nGiftAttributeId IdFor(const UniqueGiftModel &value) {\n\treturn {\n\t\t.value = value.document->id,\n\t\t.type = GiftAttributeIdType::Model,\n\t};\n}\n\nGiftAttributeId IdFor(const UniqueGiftPattern &value) {\n\treturn {\n\t\t.value = value.document->id,\n\t\t.type = GiftAttributeIdType::Pattern,\n\t};\n}\n\nrpl::producer<MyGiftsDescriptor> MyUniqueGiftsSlice(\n\t\tnot_null<Main::Session*> session,\n\t\tMyUniqueType type,\n\t\tQString offset) {\n\treturn [=](auto consumer) {\n\t\tusing Flag = MTPpayments_GetSavedStarGifts::Flag;\n\t\tconst auto user = session->user();\n\t\tconst auto requestId = session->api().request(\n\t\t\tMTPpayments_GetSavedStarGifts(\n\t\t\tMTP_flags(Flag::f_exclude_upgradable\n\t\t\t\t| Flag::f_exclude_unupgradable\n\t\t\t\t| Flag::f_exclude_unlimited\n\t\t\t\t| ((type == MyUniqueType::OnlyOwned)\n\t\t\t\t\t? Flag::f_exclude_hosted\n\t\t\t\t\t: Flag())),\n\t\t\tuser->input(),\n\t\t\tMTP_int(0), // collection_id\n\t\t\tMTP_string(offset),\n\t\t\tMTP_int(kMyGiftsPerPage)\n\t\t)).done([=](const MTPpayments_SavedStarGifts &result) {\n\t\t\tauto gifts = MyGiftsDescriptor();\n\t\t\tconst auto &data = result.data();\n\t\t\tif (const auto next = data.vnext_offset()) {\n\t\t\t\tgifts.offset = qs(*next);\n\t\t\t}\n\n\t\t\tconst auto owner = &session->data();\n\t\t\towner->processUsers(data.vusers());\n\t\t\towner->processChats(data.vchats());\n\n\t\t\tgifts.list.reserve(data.vgifts().v.size());\n\t\t\tfor (const auto &gift : data.vgifts().v) {\n\t\t\t\tif (auto parsed = Api::FromTL(user, gift)) {\n\t\t\t\t\tgifts.list.push_back(std::move(*parsed));\n\t\t\t\t}\n\t\t\t}\n\t\t\tconsumer.put_next(std::move(gifts));\n\t\t\tconsumer.put_done();\n\t\t}).fail([=] {\n\t\t\tconsumer.put_next({});\n\t\t\tconsumer.put_done();\n\t\t}).send();\n\n\t\tauto lifetime = rpl::lifetime();\n\t\tlifetime.add([=] { session->api().request(requestId).cancel(); });\n\t\treturn lifetime;\n\t};\n}\n\nrpl::producer<ResaleGiftsDescriptor> ResaleGiftsSlice(\n\t\tnot_null<Main::Session*> session,\n\t\tuint64 giftId,\n\t\tResaleGiftsFilter filter,\n\t\tQString offset) {\n\treturn [=](auto consumer) {\n\t\tusing Flag = MTPpayments_GetResaleStarGifts::Flag;\n\t\tconst auto requestId = session->api().request(\n\t\t\tMTPpayments_GetResaleStarGifts(\n\t\t\t\tMTP_flags(Flag::f_attributes_hash\n\t\t\t\t\t| ((filter.sort == ResaleGiftsSort::Price)\n\t\t\t\t\t\t? Flag::f_sort_by_price\n\t\t\t\t\t\t: (filter.sort == ResaleGiftsSort::Number)\n\t\t\t\t\t\t? Flag::f_sort_by_num\n\t\t\t\t\t\t: Flag())\n\t\t\t\t\t| (filter.attributes.empty()\n\t\t\t\t\t\t? Flag()\n\t\t\t\t\t\t: Flag::f_attributes)\n\t\t\t\t\t| (filter.forCraft ? Flag::f_for_craft : Flag())\n\t\t\t\t\t| (filter.starsOnly ? Flag::f_stars_only : Flag())),\n\t\t\t\tMTP_long(filter.attributesHash),\n\t\t\t\tMTP_long(giftId),\n\t\t\t\tMTP_vector_from_range(filter.attributes\n\t\t\t\t\t| ranges::views::transform(AttributeToTL)),\n\t\t\t\tMTP_string(offset),\n\t\t\t\tMTP_int(kResaleGiftsPerPage)\n\t\t\t)).done([=](const MTPpayments_ResaleStarGifts &result) {\n\t\t\tconst auto &data = result.data();\n\t\t\tsession->data().processUsers(data.vusers());\n\t\t\tsession->data().processChats(data.vchats());\n\n\t\t\tauto info = ResaleGiftsDescriptor{\n\t\t\t\t.giftId = giftId,\n\t\t\t\t.offset = qs(data.vnext_offset().value_or_empty()),\n\t\t\t\t.count = data.vcount().v,\n\t\t\t};\n\t\t\tconst auto &list = data.vgifts().v;\n\t\t\tinfo.list.reserve(list.size());\n\t\t\tfor (const auto &entry : list) {\n\t\t\t\tif (auto gift = Api::FromTL(session, entry)) {\n\t\t\t\t\tinfo.list.push_back(std::move(*gift));\n\t\t\t\t}\n\t\t\t}\n\t\t\tinfo.attributesHash = data.vattributes_hash().value_or_empty();\n\t\t\tconst auto &attributes = data.vattributes()\n\t\t\t\t? data.vattributes()->v\n\t\t\t\t: QVector<MTPStarGiftAttribute>();\n\t\t\tconst auto &counters = data.vcounters()\n\t\t\t\t? data.vcounters()->v\n\t\t\t\t: QVector<MTPStarGiftAttributeCounter>();\n\t\t\tauto counts = base::flat_map<GiftAttributeId, int>();\n\t\t\tcounts.reserve(counters.size());\n\t\t\tfor (const auto &counter : counters) {\n\t\t\t\tconst auto &data = counter.data();\n\t\t\t\tcounts.emplace(FromTL(data.vattribute()), data.vcount().v);\n\t\t\t}\n\t\t\tconst auto count = [&](GiftAttributeId id) {\n\t\t\t\tconst auto i = counts.find(id);\n\t\t\t\treturn i != end(counts) ? i->second : 0;\n\t\t\t};\n\t\t\tinfo.models.reserve(attributes.size());\n\t\t\tinfo.patterns.reserve(attributes.size());\n\t\t\tinfo.backdrops.reserve(attributes.size());\n\t\t\tfor (const auto &attribute : attributes) {\n\t\t\t\tattribute.match([&](const MTPDstarGiftAttributeModel &data) {\n\t\t\t\t\tconst auto parsed = Api::FromTL(session, data);\n\t\t\t\t\tinfo.models.push_back({ parsed, count(IdFor(parsed)) });\n\t\t\t\t}, [&](const MTPDstarGiftAttributePattern &data) {\n\t\t\t\t\tconst auto parsed = Api::FromTL(session, data);\n\t\t\t\t\tinfo.patterns.push_back({ parsed, count(IdFor(parsed)) });\n\t\t\t\t}, [&](const MTPDstarGiftAttributeBackdrop &data) {\n\t\t\t\t\tconst auto parsed = Api::FromTL(data);\n\t\t\t\t\tinfo.backdrops.push_back({ parsed, count(IdFor(parsed)) });\n\t\t\t\t}, [](const MTPDstarGiftAttributeOriginalDetails &data) {\n\t\t\t\t});\n\t\t\t}\n\t\t\tconsumer.put_next(std::move(info));\n\t\t\tconsumer.put_done();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_next({});\n\t\t\tconsumer.put_done();\n\t\t}).send();\n\n\t\tauto lifetime = rpl::lifetime();\n\t\tlifetime.add([=] { session->api().request(requestId).cancel(); });\n\t\treturn lifetime;\n\t};\n}\n\nrpl::producer<CraftGiftsDescriptor> CraftGiftsSlice(\n\t\tnot_null<Main::Session*> session,\n\t\tuint64 giftId,\n\t\tQString offset) {\n\treturn [=](auto consumer) {\n\t\tconst auto requestId = session->api().request(\n\t\t\tMTPpayments_GetCraftStarGifts(\n\t\t\t\tMTP_long(giftId),\n\t\t\t\tMTP_string(offset),\n\t\t\t\tMTP_int(kCraftGiftsPerPage))\n\t\t).done([=](const MTPpayments_SavedStarGifts &result) {\n\t\t\tconst auto user = session->user();\n\t\t\tconst auto owner = &session->data();\n\t\t\tconst auto &data = result.data();\n\t\t\towner->processUsers(data.vusers());\n\t\t\towner->processChats(data.vchats());\n\n\t\t\tauto info = CraftGiftsDescriptor{\n\t\t\t\t.giftId = giftId,\n\t\t\t\t.offset = qs(data.vnext_offset().value_or_empty()),\n\t\t\t\t.count = data.vcount().v,\n\t\t\t};\n\t\t\tinfo.list.reserve(data.vgifts().v.size());\n\t\t\tfor (const auto &gift : data.vgifts().v) {\n\t\t\t\tif (auto parsed = Api::FromTL(user, gift)) {\n\t\t\t\t\tinfo.list.push_back(std::move(*parsed));\n\t\t\t\t}\n\t\t\t}\n\t\t\tconsumer.put_next(std::move(info));\n\t\t\tconsumer.put_done();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tconsumer.put_next({});\n\t\t\tconsumer.put_done();\n\t\t}).send();\n\n\t\tauto lifetime = rpl::lifetime();\n\t\tlifetime.add([=] { session->api().request(requestId).cancel(); });\n\t\treturn lifetime;\n\t};\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_star_gift.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nstruct ColorCollectible;\n} // namespace Ui\n\nnamespace Data {\n\nenum class UniqueGiftRarity : int {\n\tDefault = 0,\n\tUncommon = -1,\n\tRare = -2,\n\tEpic = -3,\n\tLegendary = -4,\n};\n\nstruct UniqueGiftAttribute {\n\tQString name;\n\tint rarityValue = 0;\n\n\t[[nodiscard]] UniqueGiftRarity rarityType() const {\n\t\treturn (rarityValue >= 0)\n\t\t\t? UniqueGiftRarity::Default\n\t\t\t: UniqueGiftRarity(rarityValue);\n\t}\n\t[[nodiscard]] int rarityPermille() const {\n\t\treturn (rarityValue >= 0) ? rarityValue : 0;\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst UniqueGiftAttribute &,\n\t\tconst UniqueGiftAttribute &) = default;\n};\n\nstruct UniqueGiftRarityColors {\n\tQColor bg;\n\tQColor fg;\n};\n\n[[nodiscard]] QString UniqueGiftRarityText(UniqueGiftRarity type);\n[[nodiscard]] UniqueGiftRarityColors UniqueGiftRarityBadgeColors(\n\tUniqueGiftRarity type);\n[[nodiscard]] QString UniqueGiftAttributeText(\n\tconst UniqueGiftAttribute &attribute);\n[[nodiscard]] bool UniqueGiftAttributeHasSpecialRarity(\n\tconst UniqueGiftAttribute &attribute);\n\nstruct UniqueGiftModel : UniqueGiftAttribute {\n\tnot_null<DocumentData*> document;\n\n\tfriend inline bool operator==(\n\t\tconst UniqueGiftModel &,\n\t\tconst UniqueGiftModel &) = default;\n};\n\nstruct UniqueGiftPattern : UniqueGiftAttribute {\n\tnot_null<DocumentData*> document;\n\n\tfriend inline bool operator==(\n\t\tconst UniqueGiftPattern &,\n\t\tconst UniqueGiftPattern &) = default;\n};\n\nstruct UniqueGiftBackdrop : UniqueGiftAttribute {\n\tQColor centerColor;\n\tQColor edgeColor;\n\tQColor patternColor;\n\tQColor textColor;\n\tint id = 0;\n\n\tfriend inline bool operator==(\n\t\tconst UniqueGiftBackdrop &,\n\t\tconst UniqueGiftBackdrop &) = default;\n};\n\nstruct UniqueGiftAttributes {\n\tstd::vector<UniqueGiftModel> models;\n\tstd::vector<UniqueGiftBackdrop> backdrops;\n\tstd::vector<UniqueGiftPattern> patterns;\n};\n\nstruct UniqueGiftOriginalDetails {\n\tPeerId senderId = 0;\n\tPeerId recipientId = 0;\n\tTimeId date = 0;\n\tTextWithEntities message;\n};\n\nstruct UniqueGiftValue {\n\tQString currency;\n\tint64 valuePrice = 0;\n\tint64 valuePriceUsd = 0;\n\tCreditsAmount initialPriceStars;\n\tint64 initialSalePrice = 0;\n\tTimeId initialSaleDate = 0;\n\tint64 lastSalePrice = 0;\n\tTimeId lastSaleDate = 0;\n\tint64 averagePrice = 0;\n\tint64 minimumPrice = 0;\n\tint forSaleOnTelegram = 0;\n\tint forSaleOnFragment = 0;\n\tQString fragmentUrl;\n\tbool lastSaleFragment = false;\n};\n\nstruct UniqueGift {\n\tCollectibleId id = 0;\n\tuint64 initialGiftId = 0;\n\tQString slug;\n\tQString title;\n\tQString giftAddress;\n\tQString ownerAddress;\n\tQString ownerName;\n\tPeerId ownerId = 0;\n\tPeerId hostId = 0;\n\tPeerData *releasedBy = nullptr;\n\tPeerData *themeUser = nullptr;\n\tint64 nanoTonForResale = -1;\n\tint craftChancePermille = 0;\n\tint starsForResale = -1;\n\tint starsForTransfer = -1;\n\tint starsMinOffer = -1;\n\tint number = 0;\n\tbool onlyAcceptTon = false;\n\tbool canBeTheme = false;\n\tbool crafted = false;\n\tbool burned = false;\n\tTimeId exportAt = 0;\n\tTimeId canTransferAt = 0;\n\tTimeId canResellAt = 0;\n\tTimeId canCraftAt = 0;\n\tUniqueGiftModel model;\n\tUniqueGiftPattern pattern;\n\tUniqueGiftBackdrop backdrop;\n\tUniqueGiftOriginalDetails originalDetails;\n\tstd::shared_ptr<UniqueGiftValue> value;\n\tstd::shared_ptr<Ui::ColorCollectible> peerColor;\n};\n\n[[nodiscard]] QString UniqueGiftName(const UniqueGift &gift);\n[[nodiscard]] QString UniqueGiftName(const QString &title, int number);\n\n[[nodiscard]] CreditsAmount UniqueGiftResaleStars(const UniqueGift &gift);\n[[nodiscard]] CreditsAmount UniqueGiftResaleTon(const UniqueGift &gift);\n[[nodiscard]] CreditsAmount UniqueGiftResaleAsked(const UniqueGift &gift);\n\n[[nodiscard]] TextWithEntities FormatGiftResaleStars(const UniqueGift &gift);\n[[nodiscard]] TextWithEntities FormatGiftResaleTon(const UniqueGift &gift);\n[[nodiscard]] TextWithEntities FormatGiftResaleAsked(const UniqueGift &gift);\n\nstruct StarGiftBackground {\n\tQColor center;\n\tQColor edge;\n\tQColor text;\n\n\t[[nodiscard]] UniqueGiftBackdrop backdrop() const {\n\t\treturn {\n\t\t\t.centerColor = center,\n\t\t\t.edgeColor = edge,\n\t\t\t.patternColor = edge,\n\t\t\t.textColor = text,\n\t\t};\n\t}\n};\n\nstruct StarGift {\n\tuint64 id = 0;\n\tstd::shared_ptr<UniqueGift> unique;\n\tstd::shared_ptr<StarGiftBackground> background;\n\tint64 stars = 0;\n\tint64 starsConverted = 0;\n\tint64 starsToUpgrade = 0;\n\tint64 starsResellMin = 0;\n\tnot_null<DocumentData*> document;\n\tPeerData *releasedBy = nullptr;\n\tQString resellTitle;\n\tint resellCount = 0;\n\tQString auctionSlug;\n\tint auctionGiftsPerRound = 0;\n\tTimeId auctionStartDate = 0;\n\tint limitedLeft = 0;\n\tint limitedCount = 0;\n\tint perUserTotal = 0;\n\tint perUserRemains = 0;\n\tint upgradeVariants = 0;\n\tTimeId firstSaleDate = 0;\n\tTimeId lastSaleDate = 0;\n\tTimeId lockedUntilDate = 0;\n\tbool resellTonOnly : 1 = false;\n\tbool requirePremium : 1 = false;\n\tbool peerColorAvailable : 1 = false;\n\tbool upgradable : 1 = false;\n\tbool birthday : 1 = false;\n\tbool soldOut : 1 = false;\n\n\t[[nodiscard]] bool auction() const {\n\t\treturn !auctionSlug.isEmpty();\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst StarGift &,\n\t\tconst StarGift &) = default;\n};\n\nclass SavedStarGiftId {\npublic:\n\t[[nodiscard]] static SavedStarGiftId User(MsgId messageId) {\n\t\tauto result = SavedStarGiftId();\n\t\tresult.entityId = uint64(messageId.bare);\n\t\treturn result;\n\t}\n\t[[nodiscard]] static SavedStarGiftId Chat(\n\t\t\tnot_null<PeerData*> peer,\n\t\t\tuint64 savedId) {\n\t\tauto result = SavedStarGiftId();\n\t\tresult.peer = peer;\n\t\tresult.entityId = savedId;\n\t\treturn result;\n\t}\n\n\t[[nodiscard]] bool isUser() const {\n\t\treturn !peer;\n\t}\n\t[[nodiscard]] bool isChat() const {\n\t\treturn peer != nullptr;\n\t}\n\n\t[[nodiscard]] MsgId userMessageId() const {\n\t\treturn peer ? MsgId(0) : MsgId(entityId);\n\t}\n\t[[nodiscard]] PeerData *chat() const {\n\t\treturn peer;\n\t}\n\t[[nodiscard]] uint64 chatSavedId() const {\n\t\treturn peer ? entityId : 0;\n\t}\n\n\texplicit operator bool() const {\n\t\treturn entityId != 0;\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst SavedStarGiftId &,\n\t\tconst SavedStarGiftId &) = default;\n\tfriend inline auto operator<=>(\n\t\tconst SavedStarGiftId &,\n\t\tconst SavedStarGiftId &) = default;\n\nprivate:\n\tPeerData *peer = nullptr;\n\tuint64 entityId = 0;\n\n};\n\nstruct SavedStarGift {\n\tStarGift info;\n\tSavedStarGiftId manageId;\n\tstd::vector<int> collectionIds;\n\tTextWithEntities message;\n\tint64 starsConverted = 0;\n\tint64 starsUpgradedBySender = 0;\n\tint64 starsForDetailsRemove = 0;\n\tQString giftPrepayUpgradeHash;\n\tPeerId fromId = 0;\n\tTimeId date = 0;\n\tint giftNum = 0;\n\tbool upgradeSeparate = false;\n\tbool upgradable = false;\n\tbool anonymous = false;\n\tbool pinned = false;\n\tbool hidden = false;\n\tbool mine = false;\n};\n\nstruct GiftUpgradeSpinner {\n\tenum class State {\n\t\tInitial, // Didn't start the upgrade.\n\t\tPreparing, // Upgraded, requested Cover to prepare the spinners.\n\t\tLoading, // Cover started preparing the spinners.\n\t\tPrepared, // Cover prepared the spinners, waiting for nextToUpgrade.\n\t\tStarted, // Started spinning animations.\n\t\tFinishBackdrop, // Ready to animate to target backdrop.\n\t\tFinishedBackdrop, // Backdrop finishes spinning.\n\t\tFinishPattern, // Ready to animate to target pattern.\n\t\tFinishedPattern, // Pattern finishes spinning.\n\t\tFinishModel, // Ready to animate to target model.\n\t\tFinishedModel, // All spinners finish animating or user pressed Skip.\n\t\tFinished, // Animations finished, show fireworks.\n\t\tTimeout, // Waited for Prepared for too long, skipping animations.\n\t};\n\n\tData::UniqueGiftAttributes attributes;\n\tstd::shared_ptr<Data::UniqueGift> target;\n\trpl::variable<State> state;\n};\n\nstruct GiftUpgradeResult {\n\tStarGift info;\n\tSavedStarGiftId manageId;\n\tTimeId date = 0;\n\tint starsForDetailsRemove = 0;\n\tbool saved = false;\n};\n\nstruct GiftCollection {\n\tint id = 0;\n\tint count = 0;\n\tQString title;\n\tDocumentData *icon = nullptr;\n\tuint64 hash = 0;\n};\n\nstruct UniqueGiftModelCount {\n\tUniqueGiftModel model;\n\tint count = 0;\n};\n\nstruct UniqueGiftBackdropCount {\n\tUniqueGiftBackdrop backdrop;\n\tint count = 0;\n};\n\nstruct UniqueGiftPatternCount {\n\tUniqueGiftPattern pattern;\n\tint count = 0;\n};\n\nenum class ResaleGiftsSort {\n\tDate,\n\tPrice,\n\tNumber,\n};\n\nenum class GiftAttributeIdType {\n\tModel,\n\tPattern,\n\tBackdrop,\n};\n\nstruct GiftAttributeId {\n\tuint64 value = 0;\n\tGiftAttributeIdType type = GiftAttributeIdType::Model;\n\n\tfriend inline auto operator<=>(\n\t\tGiftAttributeId,\n\t\tGiftAttributeId) = default;\n\tfriend inline bool operator==(\n\t\tGiftAttributeId,\n\t\tGiftAttributeId) = default;\n};\n\n[[nodiscard]] GiftAttributeId IdFor(const UniqueGiftBackdrop &value);\n[[nodiscard]] GiftAttributeId IdFor(const UniqueGiftModel &value);\n[[nodiscard]] GiftAttributeId IdFor(const UniqueGiftPattern &value);\n\nstruct MyGiftsDescriptor {\n\tstd::vector<SavedStarGift> list;\n\tQString offset;\n};\n\nenum class MyUniqueType {\n\tOwnedAndHosted,\n\tOnlyOwned,\n};\n\n[[nodiscard]] rpl::producer<MyGiftsDescriptor> MyUniqueGiftsSlice(\n\tnot_null<Main::Session*> session,\n\tMyUniqueType type,\n\tQString offset = QString());\n\nstruct ResaleGiftsDescriptor {\n\tuint64 giftId = 0;\n\tQString title;\n\tQString offset;\n\tstd::vector<StarGift> list;\n\tstd::vector<UniqueGiftModelCount> models;\n\tstd::vector<UniqueGiftBackdropCount> backdrops;\n\tstd::vector<UniqueGiftPatternCount> patterns;\n\tuint64 attributesHash = 0;\n\tint count = 0;\n\tResaleGiftsSort sort = ResaleGiftsSort::Date;\n};\n\nstruct ResaleGiftsFilter {\n\tuint64 attributesHash = 0;\n\tbase::flat_set<GiftAttributeId> attributes;\n\tResaleGiftsSort sort = ResaleGiftsSort::Price;\n\tbool forCraft = false;\n\tbool starsOnly = false;\n\n\tfriend inline bool operator==(\n\t\tconst ResaleGiftsFilter &,\n\t\tconst ResaleGiftsFilter &) = default;\n};\n\n[[nodiscard]] rpl::producer<ResaleGiftsDescriptor> ResaleGiftsSlice(\n\tnot_null<Main::Session*> session,\n\tuint64 giftId,\n\tResaleGiftsFilter filter = {},\n\tQString offset = QString());\n\nstruct CraftGiftsDescriptor {\n\tuint64 giftId = 0;\n\tQString offset;\n\tstd::vector<SavedStarGift> list;\n\tint count = 0;\n};\n\n[[nodiscard]] rpl::producer<CraftGiftsDescriptor> CraftGiftsSlice(\n\tnot_null<Main::Session*> session,\n\tuint64 giftId,\n\tQString offset = QString());\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_statistics.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_statistics_chart.h\"\n\nnamespace Data {\n\nstruct StatisticsMessageInteractionInfo final {\n\tMsgId messageId;\n\tStoryId storyId = StoryId(0);\n\tint viewsCount = 0;\n\tint forwardsCount = 0;\n\tint reactionsCount = 0;\n};\n\nstruct StatisticsMessageSenderInfo final {\n\tUserId userId = UserId(0);\n\tint sentMessageCount = 0;\n\tint averageCharacterCount = 0;\n};\n\nstruct StatisticsAdministratorActionsInfo final {\n\tUserId userId = UserId(0);\n\tint deletedMessageCount = 0;\n\tint bannedUserCount = 0;\n\tint restrictedUserCount = 0;\n};\n\nstruct StatisticsInviterInfo final {\n\tUserId userId = UserId(0);\n\tint addedMemberCount = 0;\n};\n\nstruct StatisticalValue final {\n\tfloat64 value = 0.;\n\tfloat64 previousValue = 0.;\n\tfloat64 growthRatePercentage = 0.;\n};\n\nstruct ChannelStatistics final {\n\t[[nodiscard]] bool empty() const {\n\t\treturn !startDate || !endDate;\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n\n\tint startDate = 0;\n\tint endDate = 0;\n\n\tStatisticalValue memberCount;\n\tStatisticalValue meanViewCount;\n\tStatisticalValue meanShareCount;\n\tStatisticalValue meanReactionCount;\n\tStatisticalValue meanStoryViewCount;\n\tStatisticalValue meanStoryShareCount;\n\tStatisticalValue meanStoryReactionCount;\n\n\tfloat64 enabledNotificationsPercentage = 0.;\n\n\tStatisticalGraph memberCountGraph;\n\tStatisticalGraph joinGraph;\n\tStatisticalGraph muteGraph;\n\tStatisticalGraph viewCountByHourGraph;\n\tStatisticalGraph viewCountBySourceGraph;\n\tStatisticalGraph joinBySourceGraph;\n\tStatisticalGraph languageGraph;\n\tStatisticalGraph messageInteractionGraph;\n\tStatisticalGraph instantViewInteractionGraph;\n\tStatisticalGraph reactionsByEmotionGraph;\n\tStatisticalGraph storyInteractionsGraph;\n\tStatisticalGraph storyReactionsByEmotionGraph;\n\n\tstd::vector<StatisticsMessageInteractionInfo> recentMessageInteractions;\n\n};\n\nstruct SupergroupStatistics final {\n\t[[nodiscard]] bool empty() const {\n\t\treturn !startDate || !endDate;\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n\n\tint startDate = 0;\n\tint endDate = 0;\n\n\tStatisticalValue memberCount;\n\tStatisticalValue messageCount;\n\tStatisticalValue viewerCount;\n\tStatisticalValue senderCount;\n\n\tStatisticalGraph memberCountGraph;\n\tStatisticalGraph joinGraph;\n\tStatisticalGraph joinBySourceGraph;\n\tStatisticalGraph languageGraph;\n\tStatisticalGraph messageContentGraph;\n\tStatisticalGraph actionGraph;\n\tStatisticalGraph dayGraph;\n\tStatisticalGraph weekGraph;\n\n\tstd::vector<StatisticsMessageSenderInfo> topSenders;\n\tstd::vector<StatisticsAdministratorActionsInfo> topAdministrators;\n\tstd::vector<StatisticsInviterInfo> topInviters;\n\n};\n\nstruct MessageStatistics final {\n\texplicit operator bool() const {\n\t\treturn !messageInteractionGraph.chart.empty() || views;\n\t}\n\tData::StatisticalGraph messageInteractionGraph;\n\tData::StatisticalGraph reactionsByEmotionGraph;\n\tint publicForwards = 0;\n\tint privateForwards = 0;\n\tint views = 0;\n\tint reactions = 0;\n};\n// At the moment, the structures are identical.\nusing StoryStatistics = MessageStatistics;\n\nstruct AnyStatistics final {\n\tData::ChannelStatistics channel;\n\tData::SupergroupStatistics supergroup;\n\tData::MessageStatistics message;\n\tData::StoryStatistics story;\n};\n\nstruct RecentPostId final {\n\tFullMsgId messageId;\n\tFullStoryId storyId;\n\n\t[[nodiscard]] bool valid() const {\n\t\treturn messageId || storyId;\n\t}\n\texplicit operator bool() const {\n\t\treturn valid();\n\t}\n\tfriend inline auto operator<=>(RecentPostId, RecentPostId) = default;\n\tfriend inline bool operator==(RecentPostId, RecentPostId) = default;\n};\n\nstruct PublicForwardsSlice final {\n\tusing OffsetToken = QString;\n\tQVector<RecentPostId> list;\n\tint total = 0;\n\tbool allLoaded = false;\n\tOffsetToken token;\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_statistics_chart.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_statistics_chart.h\"\n\n#include \"statistics/statistics_format_values.h\"\n#include \"styles/style_basic.h\"\n#include \"styles/style_statistics.h\"\n\n#include <QtCore/QDateTime>\n#include <QtCore/QLocale>\n\nnamespace Data {\n\nvoid StatisticalChart::measure() {\n\tif (x.empty()) {\n\t\treturn;\n\t}\n\tconst auto n = x.size();\n\tconst auto start = x.front();\n\tconst auto end = x.back();\n\n\txPercentage.clear();\n\txPercentage.resize(n);\n\tif (n == 1) {\n\t\txPercentage[0] = 1;\n\t} else {\n\t\tfor (auto i = 0; i < n; i++) {\n\t\t\txPercentage[i] = (x[i] - start) / float64(end - start);\n\t\t}\n\t}\n\n\tfor (auto &line : lines) {\n\t\tif (line.maxValue > maxValue) {\n\t\t\tmaxValue = line.maxValue;\n\t\t}\n\t\tif (line.minValue < minValue) {\n\t\t\tminValue = line.minValue;\n\t\t}\n\t\tline.segmentTree = Statistic::SegmentTree(line.y);\n\t}\n\n\tdaysLookup.clear();\n\tconst auto dateCount = int((end - start) / timeStep) + 10;\n\tdaysLookup.reserve(dateCount);\n\tconstexpr auto kOneDay = 3600 * 24 * 1000;\n\n\t// View data.\n\tauto maxWidth = 0;\n\tconst auto &defaultFont = st::statisticsDetailsBottomCaptionStyle.font;\n\n\tfor (auto i = 0; i < dateCount; i++) {\n\t\tconst auto r = (start + (i * timeStep)) / 1000;\n\t\tif (timeStep == 1) {\n\t\t\tdaysLookup.push_back(\n\t\t\t\tQString(((i < 10) ? u\"0%1:00\"_q : u\"%1:00\"_q).arg(i)));\n\t\t} else if (timeStep < kOneDay) {\n\t\t\tconst auto dateTime = QDateTime::fromSecsSinceEpoch(r);\n\t\t\tdaysLookup.push_back(u\"%1:%2\"_q\n\t\t\t\t.arg(dateTime.time().hour(), 2, 10, QChar('0'))\n\t\t\t\t.arg(dateTime.time().minute(), 2, 10, QChar('0')));\n\t\t} else {\n\t\t\tdaysLookup.push_back(Statistic::LangDayMonth(r));\n\t\t}\n\t\tmaxWidth = std::max(maxWidth, defaultFont->width(daysLookup.back()));\n\t}\n\tdayStringMaxWidth = maxWidth;\n\n\toneDayPercentage = timeStep / float64(end - start);\n}\n\nQString StatisticalChart::getDayString(int i) const {\n\treturn daysLookup[int((x[i] - x[0]) / timeStep)];\n}\n\nint StatisticalChart::findStartIndex(float64 v) const {\n\tif (!v) {\n\t\treturn 0;\n\t}\n\tconst auto n = int(xPercentage.size());\n\n\tif (n < 2) {\n\t\treturn 0;\n\t}\n\tauto left = 0;\n\tauto right = n - 1;\n\n\twhile (left <= right) {\n\t\tconst auto middle = (right + left) >> 1;\n\t\tif (v < xPercentage[middle]\n\t\t\t&& (!middle || (v > xPercentage[middle - 1]))) {\n\t\t\treturn middle;\n\t\t}\n\t\tif (v == xPercentage[middle]) {\n\t\t\treturn middle;\n\t\t}\n\t\tif (v < xPercentage[middle]) {\n\t\t\tright = middle - 1;\n\t\t} else if (v > xPercentage[middle]) {\n\t\t\tleft = middle + 1;\n\t\t}\n\t}\n\treturn left;\n}\n\nint StatisticalChart::findEndIndex(int left, float64 v) const {\n\tconst auto wasLeft = left;\n\tconst auto n = int(xPercentage.size());\n\tif (v == 1.) {\n\t\treturn n - 1;\n\t}\n\tauto right = n - 1;\n\n\twhile (left <= right) {\n\t\tconst auto middle = (right + left) >> 1;\n\t\tif (v > xPercentage[middle]\n\t\t\t&& ((middle == n - 1) || (v < xPercentage[middle + 1]))) {\n\t\t\treturn middle;\n\t\t}\n\t\tif (v == xPercentage[middle]) {\n\t\t\treturn middle;\n\t\t}\n\t\tif (v < xPercentage[middle]) {\n\t\t\tright = middle - 1;\n\t\t} else if (v > xPercentage[middle]) {\n\t\t\tleft = middle + 1;\n\t\t}\n\t}\n\treturn std::max(wasLeft, right);\n}\n\n\nint StatisticalChart::findIndex(int left, int right, float64 v) const {\n\tconst auto n = int(xPercentage.size());\n\n\tif (v <= xPercentage[left]) {\n\t\treturn left;\n\t}\n\tif (v >= xPercentage[right]) {\n\t\treturn right;\n\t}\n\n\twhile (left <= right) {\n\t\tconst auto middle = (right + left) >> 1;\n\t\tif (v > xPercentage[middle]\n\t\t\t&& ((middle == n - 1) || (v < xPercentage[middle + 1]))) {\n\t\t\treturn middle;\n\t\t}\n\n\t\tif (v == xPercentage[middle]) {\n\t\t\treturn middle;\n\t\t}\n\t\tif (v < xPercentage[middle]) {\n\t\t\tright = middle - 1;\n\t\t} else if (v > xPercentage[middle]) {\n\t\t\tleft = middle + 1;\n\t\t}\n\t}\n\treturn right;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_statistics_chart.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"statistics/segment_tree.h\"\n\nnamespace Data {\n\nenum class StatisticalCurrency {\n\tNone,\n\tTon,\n\tCredits,\n};\n\nstruct StatisticalChart {\n\tStatisticalChart() = default;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn lines.empty();\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n\n\tvoid measure();\n\n\t[[nodiscard]] QString getDayString(int i) const;\n\n\t[[nodiscard]] int findStartIndex(float64 v) const;\n\t[[nodiscard]] int findEndIndex(int left, float64 v) const;\n\t[[nodiscard]] int findIndex(int left, int right, float64 v) const;\n\n\tstruct Line final {\n\t\tstd::vector<Statistic::ChartValue> y;\n\n\t\tStatistic::SegmentTree segmentTree;\n\t\tint id = 0;\n\t\tQString idString;\n\t\tQString name;\n\t\tStatistic::ChartValue maxValue = 0;\n\t\tStatistic::ChartValue minValue\n\t\t\t= std::numeric_limits<Statistic::ChartValue>::max();\n\t\tQString colorKey;\n\t\tQColor color;\n\t\tQColor colorDark;\n\t\tbool isHiddenOnStart = false;\n\t};\n\n\tstd::vector<float64> x;\n\tstd::vector<float64> xPercentage;\n\tstd::vector<QString> daysLookup;\n\n\tstd::vector<Line> lines;\n\n\tstruct {\n\t\tfloat64 min = 0.;\n\t\tfloat64 max = 0.;\n\t} defaultZoomXIndex;\n\n\tStatistic::ChartValue maxValue = 0;\n\tStatistic::ChartValue minValue\n\t\t= std::numeric_limits<Statistic::ChartValue>::max();\n\n\tfloat64 oneDayPercentage = 0.;\n\n\tfloat64 timeStep = 1.;\n\n\tbool isFooterHidden = false;\n\tbool hasPercentages = false;\n\tbool weekFormat = false;\n\n\tStatisticalCurrency currency = StatisticalCurrency::None;\n\tfloat64 currencyRate = 0.;\n\n\t// View data.\n\tint dayStringMaxWidth = 0;\n\n};\n\nstruct StatisticalGraph final {\n\tStatisticalChart chart;\n\tQString zoomToken;\n\tQString error;\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_stories.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_stories.h\"\n\n#include \"base/unixtime.h\"\n#include \"apiwrap.h\"\n#include \"core/application.h\"\n#include \"data/components/top_peers.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"ui/layers/show.h\"\n#include \"ui/text/text_utilities.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kMaxResolveTogether = 100;\nconstexpr auto kIgnorePreloadAroundIfLoaded = 15;\nconstexpr auto kPreloadAroundCount = 30;\nconstexpr auto kMarkAsReadDelay = 3 * crl::time(1000);\nconstexpr auto kIncrementViewsDelay = 5 * crl::time(1000);\nconstexpr auto kArchiveFirstPerPage = 30;\nconstexpr auto kArchivePerPage = 100;\nconstexpr auto kSavedFirstPerPage = 30;\nconstexpr auto kSavedPerPage = 100;\nconstexpr auto kMaxPreloadSources = 10;\nconstexpr auto kStillPreloadFromFirst = 3;\nconstexpr auto kMaxSegmentsCount = 180;\nconstexpr auto kPollingIntervalChat = 5 * TimeId(60);\nconstexpr auto kPollingIntervalViewer = 1 * TimeId(60);\nconstexpr auto kPollViewsInterval = 10 * crl::time(1000);\nconstexpr auto kPollingViewsPerPage = Story::kRecentViewersMax;\n\nusing UpdateFlag = StoryUpdate::Flag;\n\n[[nodiscard]] std::optional<StoryMedia> ParseMedia(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPMessageMedia &media,\n\t\tconst std::shared_ptr<GroupCall> &existingCall) {\n\treturn media.match([&](const MTPDmessageMediaPhoto &data)\n\t\t-> std::optional<StoryMedia> {\n\t\tif (const auto photo = data.vphoto()) {\n\t\t\tconst auto result = peer->owner().processPhoto(*photo);\n\t\t\tif (!result->isNull()) {\n\t\t\t\treturn StoryMedia{ result };\n\t\t\t}\n\t\t}\n\t\treturn {};\n\t}, [&](const MTPDmessageMediaDocument &data)\n\t\t-> std::optional<StoryMedia> {\n\t\tif (const auto document = data.vdocument()) {\n\t\t\tconst auto result = peer->owner().processDocument(\n\t\t\t\t*document,\n\t\t\t\tdata.valt_documents());\n\t\t\tif (!result->isNull()\n\t\t\t\t&& (result->isGifv() || result->isVideoFile())) {\n\t\t\t\tresult->setStoryMedia(true);\n\t\t\t\treturn StoryMedia{ result };\n\t\t\t}\n\t\t}\n\t\treturn {};\n\t}, [&](const MTPDmessageMediaVideoStream &data) {\n\t\tconst auto rtmp = data.is_rtmp_stream();\n\t\treturn std::make_optional(data.vcall().match([&](\n\t\t\t\tconst MTPDinputGroupCall &data) {\n\t\t\tauto call = (existingCall\n\t\t\t\t&& existingCall->peer() == peer\n\t\t\t\t&& existingCall->id() == data.vid().v)\n\t\t\t\t? existingCall\n\t\t\t\t: std::make_shared<Data::GroupCall>(\n\t\t\t\t\tpeer,\n\t\t\t\t\tdata.vid().v,\n\t\t\t\t\tdata.vaccess_hash().v,\n\t\t\t\t\tTimeId(),\n\t\t\t\t\trtmp,\n\t\t\t\t\tGroupCallOrigin::VideoStream);\n\t\t\treturn StoryMedia{ std::move(call) };\n\t\t}, [](const auto &) {\n\t\t\treturn StoryMedia();\n\t\t}));\n\t}, [&](const MTPDmessageMediaUnsupported &data) {\n\t\treturn std::make_optional(StoryMedia{ v::null });\n\t}, [](const auto &) { return std::optional<StoryMedia>(); });\n}\n\n[[nodiscard]] StoryAlbum FromTL(\n\t\tnot_null<Session*> owner,\n\t\tconst MTPStoryAlbum &album) {\n\tconst auto &data = album.data();\n\treturn {\n\t\t.id = data.valbum_id().v,\n\t\t.title = qs(data.vtitle()),\n\t\t.iconPhoto = (data.vicon_photo()\n\t\t\t? owner->processPhoto(*data.vicon_photo()).get()\n\t\t\t: nullptr),\n\t\t.iconVideo = (data.vicon_video()\n\t\t\t? owner->processDocument(*data.vicon_video()).get()\n\t\t\t: nullptr),\n\t};\n}\n\nbool InsertSorted(std::vector<StoryId> &list, StoryId id) {\n\tconst auto i = ranges::lower_bound(list, id, std::greater<>());\n\tif (i != end(list) && *i == id) {\n\t\treturn false;\n\t}\n\tlist.insert(i, id);\n\treturn true;\n}\n\nbool RemoveSorted(std::vector<StoryId> &list, StoryId id) {\n\tconst auto i = ranges::lower_bound(list, id, std::greater<>());\n\tif (i != end(list) && *i == id) {\n\t\tlist.erase(i);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool RemoveUnsorted(std::vector<StoryId> &list, StoryId id) {\n\tconst auto i = ranges::find(list, id);\n\tif (i != end(list)) {\n\t\tlist.erase(i);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\n} // namespace\n\nstd::vector<StoryId> RespectingPinned(const StoriesIds &ids) {\n\tif (ids.pinnedToTop.empty()) {\n\t\treturn ids.list;\n\t}\n\tauto result = std::vector<StoryId>();\n\tresult.reserve(ids.list.size());\n\tresult.insert(end(result), begin(ids.pinnedToTop), end(ids.pinnedToTop));\n\tfor (const auto &id : ids.list) {\n\t\tif (!ranges::contains(ids.pinnedToTop, id)) {\n\t\t\tresult.push_back(id);\n\t\t}\n\t}\n\treturn result;\n}\n\nStoriesSourceInfo StoriesSource::info() const {\n\treturn {\n\t\t.id = peer->id,\n\t\t.last = ids.empty() ? 0 : ids.back().date,\n\t\t.count = uint32(std::min(int(ids.size()), kMaxSegmentsCount)),\n\t\t.unreadCount = uint32(std::min(unreadCount(), kMaxSegmentsCount)),\n\t\t.premium = (peer->isUser() && peer->asUser()->isPremium()) ? 1U : 0,\n\t\t.hasVideoStream = hasVideoStream ? 1U : 0,\n\t};\n}\n\nint StoriesSource::unreadCount() const {\n\tconst auto i = ids.lower_bound(StoryIdDates{ .id = readTill + 1 });\n\treturn int(end(ids) - i);\n}\n\nStoryIdDates StoriesSource::toOpen() const {\n\tif (ids.empty()) {\n\t\treturn {};\n\t}\n\tconst auto i = ids.lower_bound(StoryIdDates{ readTill + 1 });\n\treturn (i != end(ids)) ? *i : ids.front();\n}\n\nStories::Stories(not_null<Session*> owner)\n: _owner(owner)\n, _expireTimer([=] { processExpired(); })\n, _markReadTimer([=] { sendMarkAsReadRequests(); })\n, _incrementViewsTimer([=] { sendIncrementViewsRequests(); })\n, _pollingTimer([=] { sendPollingRequests(); })\n, _pollingViewsTimer([=] { sendPollingViewsRequests(); }) {\n\tcrl::on_main(this, [=] {\n\t\tsession().changes().peerUpdates(\n\t\t\tData::PeerUpdate::Flag::Rights\n\t\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\t\tconst auto channel = update.peer->asChannel();\n\t\t\tif (!channel) {\n\t\t\t\treturn;\n\t\t\t} else if (!channel->canEditStories()) {\n\t\t\t\tconst auto peerId = channel->id;\n\t\t\t\tconst auto i = _peersWithDeletedStories.find(peerId);\n\t\t\t\tif (i != end(_peersWithDeletedStories)) {\n\t\t\t\t\t_peersWithDeletedStories.erase(i);\n\t\t\t\t\tfor (auto j = begin(_deleted); j != end(_deleted);) {\n\t\t\t\t\t\tif (j->peer == peerId) {\n\t\t\t\t\t\t\tj = _deleted.erase(j);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t++j;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tclearArchive(channel);\n\t\t\t}\n\t\t}, _lifetime);\n\t});\n}\n\nStories::~Stories() {\n\tExpects(_pollingSettings.empty());\n\tExpects(_pollingViews.empty());\n}\n\nSession &Stories::owner() const {\n\treturn *_owner;\n}\n\nMain::Session &Stories::session() const {\n\treturn _owner->session();\n}\n\nvoid Stories::apply(const MTPDupdateStory &data) {\n\tconst auto peerId = peerFromMTP(data.vpeer());\n\tconst auto peer = _owner->peer(peerId);\n\tconst auto now = base::unixtime::now();\n\tconst auto idDates = parseAndApply(peer, data.vstory(), now);\n\tif (!idDates) {\n\t\treturn;\n\t}\n\tconst auto expired = (idDates.expires <= now);\n\tif (expired) {\n\t\tapplyExpired({ peerId, idDates.id });\n\t\treturn;\n\t}\n\tconst auto i = _all.find(peerId);\n\tif (i == end(_all)) {\n\t\trequestPeerStories(peer);\n\t\treturn;\n\t} else if (i->second.ids.contains(idDates)) {\n\t\treturn;\n\t}\n\tconst auto wasInfo = i->second.info();\n\ti->second.ids.emplace(idDates);\n\tconst auto nowInfo = i->second.info();\n\tif (peer->isSelf() && i->second.readTill < idDates.id) {\n\t\t_readTill[peerId] = i->second.readTill = idDates.id;\n\t}\n\tif (wasInfo == nowInfo) {\n\t\treturn;\n\t}\n\tconst auto refreshInList = [&](StorySourcesList list) {\n\t\tauto &sources = _sources[static_cast<int>(list)];\n\t\tconst auto i = ranges::find(\n\t\t\tsources,\n\t\t\tpeerId,\n\t\t\t&StoriesSourceInfo::id);\n\t\tif (i != end(sources)) {\n\t\t\t*i = nowInfo;\n\t\t\tsort(list);\n\t\t}\n\t};\n\tif (peer->hasStoriesHidden()) {\n\t\trefreshInList(StorySourcesList::Hidden);\n\t} else {\n\t\trefreshInList(StorySourcesList::NotHidden);\n\t}\n\t_sourceChanged.fire_copy(peerId);\n\tupdatePeerStoriesState(peer);\n}\n\nvoid Stories::apply(const MTPDupdateReadStories &data) {\n\tbumpReadTill(peerFromMTP(data.vpeer()), data.vmax_id().v);\n}\n\nvoid Stories::apply(const MTPStoriesStealthMode &stealthMode) {\n\tconst auto &data = stealthMode.data();\n\t_stealthMode = StealthMode{\n\t\t.enabledTill = data.vactive_until_date().value_or_empty(),\n\t\t.cooldownTill = data.vcooldown_until_date().value_or_empty(),\n\t};\n}\n\nvoid Stories::apply(not_null<PeerData*> peer, const MTPPeerStories *data) {\n\tif (!data) {\n\t\tapplyDeletedFromSources(peer->id, StorySourcesList::NotHidden);\n\t\tapplyDeletedFromSources(peer->id, StorySourcesList::Hidden);\n\t\t_all.erase(peer->id);\n\t\t_sourceChanged.fire_copy(peer->id);\n\t\tupdatePeerStoriesState(peer);\n\t} else {\n\t\tparseAndApply(*data, ParseSource::DirectRequest);\n\t}\n}\n\nStory *Stories::applySingle(PeerId peerId, const MTPstoryItem &story) {\n\tconst auto idDates = parseAndApply(\n\t\t_owner->peer(peerId),\n\t\tstory,\n\t\tbase::unixtime::now());\n\tconst auto value = idDates\n\t\t? lookup({ peerId, idDates.id })\n\t\t: base::make_unexpected(NoStory::Deleted);\n\treturn value ? value->get() : nullptr;\n}\n\nvoid Stories::requestPeerStories(\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> done) {\n\tconst auto &[i, ok] = _requestingPeerStories.emplace(peer);\n\tif (done) {\n\t\ti->second.push_back(std::move(done));\n\t}\n\tif (!ok) {\n\t\treturn;\n\t}\n\tconst auto finish = [=] {\n\t\tif (const auto callbacks = _requestingPeerStories.take(peer)) {\n\t\t\tfor (const auto &callback : *callbacks) {\n\t\t\t\tcallback();\n\t\t\t}\n\t\t}\n\t};\n\t_owner->session().api().request(MTPstories_GetPeerStories(\n\t\tpeer->input()\n\t)).done([=](const MTPstories_PeerStories &result) {\n\t\tconst auto &data = result.data();\n\t\t_owner->processUsers(data.vusers());\n\t\t_owner->processChats(data.vchats());\n\t\tparseAndApply(data.vstories(), ParseSource::DirectRequest);\n\t\tfinish();\n\t}).fail([=] {\n\t\tapplyDeletedFromSources(peer->id, StorySourcesList::NotHidden);\n\t\tapplyDeletedFromSources(peer->id, StorySourcesList::Hidden);\n\t\tfinish();\n\t}).send();\n}\n\nvoid Stories::registerExpiring(TimeId expires, FullStoryId id) {\n\tfor (auto i = _expiring.findFirst(expires)\n\t\t; (i != end(_expiring)) && (i->first == expires)\n\t\t; ++i) {\n\t\tif (i->second == id) {\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto reschedule = _expiring.empty()\n\t\t|| (_expiring.front().first > expires);\n\t_expiring.emplace(expires, id);\n\tif (reschedule) {\n\t\tscheduleExpireTimer();\n\t}\n}\n\nvoid Stories::scheduleExpireTimer() {\n\tif (_expireSchedulePosted) {\n\t\treturn;\n\t}\n\t_expireSchedulePosted = true;\n\tcrl::on_main(this, [=] {\n\t\tif (!_expireSchedulePosted) {\n\t\t\treturn;\n\t\t}\n\t\t_expireSchedulePosted = false;\n\t\tif (_expiring.empty()) {\n\t\t\t_expireTimer.cancel();\n\t\t} else {\n\t\t\tconst auto nearest = _expiring.front().first;\n\t\t\tconst auto now = base::unixtime::now();\n\t\t\tconst auto delay = (nearest > now)\n\t\t\t\t? std::min(nearest - now, 86'400)\n\t\t\t\t: 0;\n\t\t\t_expireTimer.callOnce(delay * crl::time(1000));\n\t\t}\n\t});\n}\n\nvoid Stories::processExpired() {\n\tconst auto now = base::unixtime::now();\n\tauto expired = base::flat_set<FullStoryId>();\n\tauto i = begin(_expiring);\n\tfor (; i != end(_expiring) && i->first <= now; ++i) {\n\t\texpired.emplace(i->second);\n\t}\n\t_expiring.erase(begin(_expiring), i);\n\tfor (const auto &id : expired) {\n\t\tapplyExpired(id);\n\t}\n\tif (!_expiring.empty()) {\n\t\tscheduleExpireTimer();\n\t}\n}\n\nStories::Set *Stories::lookupArchive(not_null<PeerData*> peer) {\n\treturn albumIdsSet(peer->id, kStoriesAlbumIdArchive);\n}\n\nvoid Stories::clearArchive(not_null<PeerData*> peer) {\n\tconst auto peerId = peer->id;\n\tconst auto i = _archive.find(peerId);\n\tif (i == end(_archive)) {\n\t\treturn;\n\t}\n\tauto archive = base::take(i->second);\n\t_archive.erase(i);\n\tfor (const auto &id : archive.ids.list) {\n\t\tif (const auto story = lookup({ peerId, id })) {\n\t\t\tif ((*story)->expired() && !(*story)->inProfile()) {\n\t\t\t\tapplyDeleted(peer, id);\n\t\t\t}\n\t\t}\n\t}\n\t_albumIdsChanged.fire({ peerId, kStoriesAlbumIdArchive });\n}\n\nvoid Stories::parseAndApply(\n\t\tconst MTPPeerStories &stories,\n\t\tParseSource source) {\n\tconst auto &data = stories.data();\n\tconst auto peerId = peerFromMTP(data.vpeer());\n\tconst auto already = _readTill.find(peerId);\n\tconst auto readTill = std::max(\n\t\tdata.vmax_read_id().value_or_empty(),\n\t\t(already != end(_readTill) ? already->second : 0));\n\tconst auto peer = _owner->peer(peerId);\n\tauto result = StoriesSource{\n\t\t.peer = peer,\n\t\t.readTill = readTill,\n\t\t.hidden = peer->hasStoriesHidden(),\n\t};\n\tconst auto &list = data.vstories().v;\n\tconst auto now = base::unixtime::now();\n\tresult.ids.reserve(list.size());\n\tfor (const auto &story : list) {\n\t\tif (const auto id = parseAndApply(result.peer, story, now)) {\n\t\t\tresult.ids.emplace(id);\n\n\t\t\tif (story.type() == mtpc_storyItem) {\n\t\t\t\tconst auto &media = story.c_storyItem().vmedia();\n\t\t\t\tif (media.type() == mtpc_messageMediaVideoStream) {\n\t\t\t\t\tresult.hasVideoStream = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (result.ids.empty()) {\n\t\tapplyDeletedFromSources(peerId, StorySourcesList::NotHidden);\n\t\tapplyDeletedFromSources(peerId, StorySourcesList::Hidden);\n\t\tpeer->setStoriesState(PeerData::StoriesState::None);\n\t\treturn;\n\t} else if (peer->isSelf()) {\n\t\tresult.readTill = result.ids.back().id;\n\t}\n\t_readTill[peerId] = result.readTill;\n\tconst auto info = result.info();\n\tconst auto i = _all.find(peerId);\n\tif (i != end(_all)) {\n\t\tif (i->second != result) {\n\t\t\ti->second = std::move(result);\n\t\t}\n\t} else {\n\t\t_all.emplace(peerId, std::move(result));\n\t}\n\tconst auto add = [&](StorySourcesList list) {\n\t\tauto &sources = _sources[static_cast<int>(list)];\n\t\tconst auto i = ranges::find(\n\t\t\tsources,\n\t\t\tpeerId,\n\t\t\t&StoriesSourceInfo::id);\n\t\tif (i == end(sources)) {\n\t\t\tsources.push_back(info);\n\t\t} else if (*i == info) {\n\t\t\treturn;\n\t\t} else {\n\t\t\t*i = info;\n\t\t}\n\t\tsort(list);\n\t};\n\tconst auto appendToStrip = [](not_null<PeerData*> peer, ParseSource source) {\n\t\tif (peer->isSelf()) {\n\t\t\treturn true;\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\treturn channel->amIn();\n\t\t} else if (const auto user = peer->asUser()) {\n\t\t\treturn (source == ParseSource::MyStrip)\n\t\t\t\t|| user->storiesCorrespondent()\n\t\t\t\t|| user->isServiceUser()\n\t\t\t\t|| user->isContact()\n\t\t\t\t|| user->isBot();\n\t\t}\n\t\treturn false;\n\t};\n\tif (appendToStrip(result.peer, source)) {\n\t\tif (source == ParseSource::MyStrip\n\t\t\t&& !appendToStrip(result.peer, ParseSource::DirectRequest)) {\n\t\t\tresult.peer->asUser()->setStoriesCorrespondent(true);\n\t\t}\n\t\tconst auto hidden = result.peer->hasStoriesHidden();\n\t\tusing List = StorySourcesList;\n\t\tadd(hidden ? List::Hidden : List::NotHidden);\n\t\tapplyDeletedFromSources(\n\t\t\tpeerId,\n\t\t\thidden ? List::NotHidden : List::Hidden);\n\t} else {\n\t\tapplyDeletedFromSources(peerId, StorySourcesList::NotHidden);\n\t\tapplyDeletedFromSources(peerId, StorySourcesList::Hidden);\n\t}\n\t_sourceChanged.fire_copy(peerId);\n\tupdatePeerStoriesState(result.peer);\n}\n\nStory *Stories::parseAndApply(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPDstoryItem &data,\n\t\tTimeId now) {\n\tconst auto id = data.vid().v;\n\tconst auto fullId = FullStoryId{ peer->id, id };\n\tauto &stories = _stories[peer->id];\n\tconst auto i = stories.find(id);\n\tconst auto &existingCall = (i != end(stories))\n\t\t? i->second->call()\n\t\t: nullptr;\n\tconst auto media = ParseMedia(peer, data.vmedia(), existingCall);\n\tif (!media) {\n\t\treturn nullptr;\n\t}\n\tconst auto expires = data.vexpire_date().v;\n\tconst auto expired = (expires <= now);\n\tif (expired && !data.is_pinned() && !hasArchive(peer)) {\n\t\treturn nullptr;\n\t}\n\tauto albumInfo = (Albums*)nullptr;\n\tauto list = std::optional<base::flat_set<int>>();\n\tif (const auto albums = data.valbums()) {\n\t\tlist.emplace();\n\t\tif (!albums->v.empty()) {\n\t\t\talbumInfo = &_albums[peer->id];\n\t\t\tlist->reserve(albums->v.size());\n\t\t\tfor (const auto &albumId : albums->v) {\n\t\t\t\talbumInfo->sets[albumId.v].albumKnownInArchive.emplace(id);\n\t\t\t\tlist->emplace(albumId.v);\n\t\t\t}\n\t\t}\n\t}\n\tif (i != end(stories)) {\n\t\tconst auto result = i->second.get();\n\t\tconst auto mediaChanged = (result->media() != *media);\n\t\tresult->applyChanges(*media, data, now);\n\t\tif (list) {\n\t\t\tconst auto &was = result->albumIds();\n\t\t\tif (*list != was) {\n\t\t\t\tif (!albumInfo && !was.empty()) {\n\t\t\t\t\talbumInfo = &_albums[peer->id];\n\t\t\t\t}\n\t\t\t\tfor (const auto wasId : result->albumIds()) {\n\t\t\t\t\tif (!list->contains(wasId)) {\n\t\t\t\t\t\talbumInfo->sets[wasId].albumKnownInArchive.remove(id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tresult->setAlbumIds(*base::take(list));\n\t\t\t}\n\t\t}\n\n\t\tconst auto j = _pollingSettings.find(result);\n\t\tif (j != end(_pollingSettings)) {\n\t\t\tmaybeSchedulePolling(result, j->second, now);\n\t\t}\n\t\tif (mediaChanged) {\n\t\t\t_preloaded.remove(fullId);\n\t\t\tif (_preloading && _preloading->id() == fullId) {\n\t\t\t\t_preloading = nullptr;\n\t\t\t\trebuildPreloadSources(StorySourcesList::NotHidden);\n\t\t\t\trebuildPreloadSources(StorySourcesList::Hidden);\n\t\t\t\tcontinuePreloading();\n\t\t\t}\n\t\t\t_owner->refreshStoryItemViews(fullId);\n\t\t}\n\t\treturn result;\n\t}\n\tconst auto wasDeleted = _deleted.remove(fullId);\n\tconst auto result = stories.emplace(id, std::make_unique<Story>(\n\t\tid,\n\t\tpeer,\n\t\tStoryMedia{ *media },\n\t\tdata,\n\t\tnow\n\t)).first->second.get();\n\tif (list) {\n\t\tresult->setAlbumIds(*base::take(list));\n\t}\n\n\tif (const auto archive = lookupArchive(peer)) {\n\t\tconst auto added = InsertSorted(archive->ids.list, id);\n\t\tif (added) {\n\t\t\tif (archive->total >= 0 && id > archive->lastId) {\n\t\t\t\t++archive->total;\n\t\t\t}\n\t\t\t_albumIdsChanged.fire({ peer->id, kStoriesAlbumIdArchive });\n\t\t}\n\t}\n\n\tif (expired) {\n\t\t_expiring.remove(expires, fullId);\n\t\tapplyExpired(fullId);\n\t} else {\n\t\tregisterExpiring(expires, fullId);\n\t}\n\n\tif (wasDeleted) {\n\t\t_owner->refreshStoryItemViews(fullId);\n\t}\n\n\treturn result;\n}\n\nStoryIdDates Stories::parseAndApply(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPstoryItem &story,\n\t\tTimeId now) {\n\treturn story.match([&](const MTPDstoryItem &data) {\n\t\tif (const auto story = parseAndApply(peer, data, now)) {\n\t\t\treturn story->idDates();\n\t\t}\n\t\tapplyDeleted(peer, data.vid().v);\n\t\treturn StoryIdDates();\n\t}, [&](const MTPDstoryItemSkipped &data) {\n\t\tconst auto expires = data.vexpire_date().v;\n\t\tconst auto expired = (expires <= now);\n\t\tconst auto fullId = FullStoryId{ peer->id, data.vid().v };\n\t\tif (!expired) {\n\t\t\tregisterExpiring(expires, fullId);\n\t\t} else if (!hasArchive(peer)) {\n\t\t\tapplyDeleted(peer, data.vid().v);\n\t\t\treturn StoryIdDates();\n\t\t} else {\n\t\t\t_expiring.remove(expires, fullId);\n\t\t\tapplyExpired(fullId);\n\t\t}\n\t\treturn StoryIdDates{\n\t\t\tdata.vid().v,\n\t\t\tdata.vdate().v,\n\t\t\tdata.vexpire_date().v,\n\t\t};\n\t}, [&](const MTPDstoryItemDeleted &data) {\n\t\tapplyDeleted(peer, data.vid().v);\n\t\treturn StoryIdDates();\n\t});\n}\n\nvoid Stories::updateDependentMessages(not_null<Data::Story*> story) {\n\tconst auto i = _dependentMessages.find(story);\n\tif (i != end(_dependentMessages)) {\n\t\tfor (const auto &dependent : i->second) {\n\t\t\tdependent->updateDependencyItem();\n\t\t}\n\t}\n\tsession().changes().storyUpdated(\n\t\tstory,\n\t\tData::StoryUpdate::Flag::Edited);\n}\n\nvoid Stories::registerDependentMessage(\n\t\tnot_null<HistoryItem*> dependent,\n\t\tnot_null<Data::Story*> dependency) {\n\t_dependentMessages[dependency].emplace(dependent);\n}\n\nvoid Stories::unregisterDependentMessage(\n\t\tnot_null<HistoryItem*> dependent,\n\t\tnot_null<Data::Story*> dependency) {\n\tconst auto i = _dependentMessages.find(dependency);\n\tif (i != end(_dependentMessages)) {\n\t\tif (i->second.remove(dependent) && i->second.empty()) {\n\t\t\t_dependentMessages.erase(i);\n\t\t}\n\t}\n}\n\nvoid Stories::savedStateChanged(not_null<Story*> story) {\n\tconst auto id = story->id();\n\tconst auto peer = story->peer()->id;\n\tconst auto inProfile = story->inProfile();\n\tif (inProfile) {\n\t\tauto &saved = _saved[peer];\n\t\tconst auto added = InsertSorted(saved.ids.list, id);\n\t\tif (added) {\n\t\t\tif (saved.total >= 0 && id > saved.lastId) {\n\t\t\t\t++saved.total;\n\t\t\t}\n\t\t\t_albumIdsChanged.fire({ peer, kStoriesAlbumIdSaved });\n\t\t}\n\t} else if (const auto i = _saved.find(peer); i != end(_saved)) {\n\t\tauto &saved = i->second;\n\t\tif (RemoveSorted(saved.ids.list, id)) {\n\t\t\tif (saved.total > 0) {\n\t\t\t\t--saved.total;\n\t\t\t}\n\t\t\t_albumIdsChanged.fire({ peer, kStoriesAlbumIdSaved });\n\t\t}\n\t}\n}\n\nvoid Stories::loadMore(StorySourcesList list) {\n\tconst auto index = static_cast<int>(list);\n\tif (_loadMoreRequestId[index] || _sourcesLoaded[index]) {\n\t\treturn;\n\t}\n\tconst auto hidden = (list == StorySourcesList::Hidden);\n\tconst auto api = &_owner->session().api();\n\tusing Flag = MTPstories_GetAllStories::Flag;\n\t_loadMoreRequestId[index] = api->request(MTPstories_GetAllStories(\n\t\tMTP_flags((hidden ? Flag::f_hidden : Flag())\n\t\t\t| (_sourcesStates[index].isEmpty()\n\t\t\t\t? Flag(0)\n\t\t\t\t: (Flag::f_next | Flag::f_state))),\n\t\tMTP_string(_sourcesStates[index])\n\t)).done([=](const MTPstories_AllStories &result) {\n\t\t_loadMoreRequestId[index] = 0;\n\n\t\tresult.match([&](const MTPDstories_allStories &data) {\n\t\t\t_owner->processUsers(data.vusers());\n\t\t\t_owner->processChats(data.vchats());\n\t\t\t_sourcesStates[index] = qs(data.vstate());\n\t\t\t_sourcesLoaded[index] = !data.is_has_more();\n\t\t\tfor (const auto &single : data.vpeer_stories().v) {\n\t\t\t\tparseAndApply(single, ParseSource::MyStrip);\n\t\t\t}\n\t\t}, [](const MTPDstories_allStoriesNotModified &) {\n\t\t});\n\n\t\tresult.match([&](const auto &data) {\n\t\t\tapply(data.vstealth_mode());\n\t\t});\n\n\t\tpreloadListsMore();\n\t}).fail([=] {\n\t\t_loadMoreRequestId[index] = 0;\n\t}).send();\n}\n\nvoid Stories::preloadListsMore() {\n\tif (_loadMoreRequestId[static_cast<int>(StorySourcesList::NotHidden)]\n\t\t|| _loadMoreRequestId[static_cast<int>(StorySourcesList::Hidden)]) {\n\t\treturn;\n\t}\n\tconst auto loading = [&](StorySourcesList list) {\n\t\treturn _loadMoreRequestId[static_cast<int>(list)] != 0;\n\t};\n\tconst auto countLoaded = [&](StorySourcesList list) {\n\t\tconst auto index = static_cast<int>(list);\n\t\treturn _sourcesLoaded[index] || !_sourcesStates[index].isEmpty();\n\t};\n\tconst auto selfId = _owner->session().userPeerId();\n\tconstexpr auto archive = kStoriesAlbumIdArchive;\n\tif (loading(StorySourcesList::NotHidden)\n\t\t|| loading(StorySourcesList::Hidden)) {\n\t\treturn;\n\t} else if (!countLoaded(StorySourcesList::NotHidden)) {\n\t\tloadMore(StorySourcesList::NotHidden);\n\t} else if (!countLoaded(StorySourcesList::Hidden)) {\n\t\tloadMore(StorySourcesList::Hidden);\n\t} else if (!albumIdsCountKnown(selfId, archive)) {\n\t\talbumIdsLoadMore(selfId, archive);\n\t}\n}\n\nvoid Stories::notifySourcesChanged(StorySourcesList list) {\n\t_sourcesChanged[static_cast<int>(list)].fire({});\n\tif (list == StorySourcesList::Hidden) {\n\t\tpushHiddenCountsToFolder();\n\t}\n}\n\nvoid Stories::pushHiddenCountsToFolder() {\n\tconst auto &list = sources(StorySourcesList::Hidden);\n\tif (list.empty()) {\n\t\tif (_folderForHidden) {\n\t\t\t_folderForHidden->updateStoriesCount(0, 0);\n\t\t}\n\t\treturn;\n\t}\n\tif (!_folderForHidden) {\n\t\t_folderForHidden = _owner->folder(Folder::kId);\n\t}\n\tconst auto count = int(list.size());\n\tconst auto unread = ranges::count_if(\n\t\tlist,\n\t\t[](const StoriesSourceInfo &info) { return info.unreadCount > 0; });\n\t_folderForHidden->updateStoriesCount(count, unread);\n}\n\nvoid Stories::sendResolveRequests() {\n\tif (!_resolveSent.empty()) {\n\t\treturn;\n\t}\n\tauto leftToSend = kMaxResolveTogether;\n\tauto byPeer = base::flat_map<PeerId, QVector<MTPint>>();\n\tfor (auto i = begin(_resolvePending); i != end(_resolvePending);) {\n\t\tconst auto peerId = i->first;\n\t\tauto &ids = i->second;\n\t\tauto &sent = _resolveSent[peerId];\n\t\tif (ids.size() <= leftToSend) {\n\t\t\tsent = base::take(ids);\n\t\t\ti = _resolvePending.erase(i); // Invalidates `ids`.\n\t\t\tleftToSend -= int(sent.size());\n\t\t} else {\n\t\t\tsent = {\n\t\t\t\tstd::make_move_iterator(begin(ids)),\n\t\t\t\tstd::make_move_iterator(begin(ids) + leftToSend)\n\t\t\t};\n\t\t\tids.erase(begin(ids), begin(ids) + leftToSend);\n\t\t\tleftToSend = 0;\n\t\t}\n\t\tauto &prepared = byPeer[peerId];\n\t\tfor (auto &[storyId, callbacks] : sent) {\n\t\t\tprepared.push_back(MTP_int(storyId));\n\t\t}\n\t\tif (!leftToSend) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tconst auto api = &_owner->session().api();\n\tfor (auto &entry : byPeer) {\n\t\tconst auto peerId = entry.first;\n\t\tauto &prepared = entry.second;\n\t\tconst auto finish = [=](PeerId peerId) {\n\t\t\tconst auto sent = _resolveSent.take(peerId);\n\t\t\tAssert(sent.has_value());\n\t\t\tfor (const auto &[storyId, list] : *sent) {\n\t\t\t\tfinalizeResolve({ peerId, storyId });\n\t\t\t\tfor (const auto &callback : list) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}\n\t\t\t_itemsChanged.fire_copy(peerId);\n\t\t\tif (_resolveSent.empty() && !_resolvePending.empty()) {\n\t\t\t\tcrl::on_main(&session(), [=] { sendResolveRequests(); });\n\t\t\t}\n\t\t};\n\t\tconst auto peer = _owner->session().data().peer(peerId);\n\t\tapi->request(MTPstories_GetStoriesByID(\n\t\t\tpeer->input(),\n\t\t\tMTP_vector<MTPint>(prepared)\n\t\t)).done([=](const MTPstories_Stories &result) {\n\t\t\towner().processUsers(result.data().vusers());\n\t\t\towner().processChats(result.data().vchats());\n\t\t\tprocessResolvedStories(peer, result.data().vstories().v);\n\t\t\tfinish(peer->id);\n\t\t}).fail([=] {\n\t\t\tfinish(peerId);\n\t\t}).send();\n\t }\n}\n\nvoid Stories::processResolvedStories(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QVector<MTPStoryItem> &list) {\n\tconst auto now = base::unixtime::now();\n\tfor (const auto &item : list) {\n\t\titem.match([&](const MTPDstoryItem &data) {\n\t\t\tif (!parseAndApply(peer, data, now)) {\n\t\t\t\tapplyDeleted(peer, data.vid().v);\n\t\t\t}\n\t\t}, [&](const MTPDstoryItemSkipped &data) {\n\t\t\tLOG((\"API Error: Unexpected storyItemSkipped in resolve.\"));\n\t\t}, [&](const MTPDstoryItemDeleted &data) {\n\t\t\tapplyDeleted(peer, data.vid().v);\n\t\t});\n\t}\n}\n\nvoid Stories::finalizeResolve(FullStoryId id) {\n\tconst auto already = lookup(id);\n\tif (!already.has_value() && already.error() == NoStory::Unknown) {\n\t\tLOG((\"API Error: Could not resolve story %1_%2\"\n\t\t\t).arg(id.peer.value\n\t\t\t).arg(id.story));\n\t\tapplyDeleted(_owner->peer(id.peer), id.story);\n\t}\n}\n\nvoid Stories::applyDeleted(not_null<PeerData*> peer, StoryId id) {\n\tconst auto fullId = FullStoryId{ peer->id, id };\n\tapplyRemovedFromActive(fullId);\n\n\tif (const auto channel = peer->asChannel()) {\n\t\tif (!hasArchive(channel)) {\n\t\t\t_peersWithDeletedStories.emplace(channel->id);\n\t\t}\n\t}\n\n\t_deleted.emplace(fullId);\n\tconst auto peerId = peer->id;\n\tconst auto i = _stories.find(peerId);\n\tif (i != end(_stories)) {\n\t\tconst auto j = i->second.find(id);\n\t\tif (j != end(i->second)) {\n\t\t\tconst auto &story\n\t\t\t\t= _deletingStories[fullId]\n\t\t\t\t= std::move(j->second);\n\t\t\t_expiring.remove(story->expires(), story->fullId());\n\t\t\ti->second.erase(j);\n\n\t\t\tsession().changes().storyUpdated(\n\t\t\t\tstory.get(),\n\t\t\t\tUpdateFlag::Destroyed);\n\t\t\tremoveDependencyStory(story.get());\n\t\t\tconst auto removeFromAlbum = [&](int albumId) {\n\t\t\t\tif (const auto set = albumIdsSet(peerId, albumId, true)) {\n\t\t\t\t\tset->albumKnownInArchive.remove(id);\n\t\t\t\t\tRemoveUnsorted(set->ids.pinnedToTop, id);\n\n\t\t\t\t\tconst auto sorted = (albumId == kStoriesAlbumIdSaved)\n\t\t\t\t\t\t|| (albumId == kStoriesAlbumIdArchive);\n\t\t\t\t\tconst auto removed = sorted\n\t\t\t\t\t\t? RemoveSorted(set->ids.list, id)\n\t\t\t\t\t\t: RemoveUnsorted(set->ids.list, id);\n\t\t\t\t\tif (removed) {\n\t\t\t\t\t\tif (set->total > 0) {\n\t\t\t\t\t\t\t--set->total;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t_albumIdsChanged.fire({ peerId, albumId });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\tif (hasArchive(story->peer())) {\n\t\t\t\tremoveFromAlbum(kStoriesAlbumIdArchive);\n\t\t\t}\n\t\t\tif (story->inProfile()) {\n\t\t\t\tremoveFromAlbum(kStoriesAlbumIdSaved);\n\t\t\t}\n\t\t\tfor (const auto id : story->albumIds()) {\n\t\t\t\tremoveFromAlbum(id);\n\t\t\t}\n\t\t\tif (_preloading && _preloading->id() == fullId) {\n\t\t\t\t_preloading = nullptr;\n\t\t\t\tpreloadFinished(fullId);\n\t\t\t}\n\t\t\t_owner->refreshStoryItemViews(fullId);\n\t\t\tAssert(!_pollingSettings.contains(story.get()));\n\t\t\tif (const auto j = _items.find(peerId); j != end(_items)) {\n\t\t\t\tconst auto k = j->second.find(id);\n\t\t\t\tif (k != end(j->second)) {\n\t\t\t\t\tAssert(!k->second.lock());\n\t\t\t\t\tj->second.erase(k);\n\t\t\t\t\tif (j->second.empty()) {\n\t\t\t\t\t\t_items.erase(j);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (i->second.empty()) {\n\t\t\t\t_stories.erase(i);\n\t\t\t}\n\t\t\t_deletingStories.remove(fullId);\n\t\t}\n\t}\n}\n\nvoid Stories::applyExpired(FullStoryId id) {\n\tif (const auto maybeStory = lookup(id)) {\n\t\tconst auto story = *maybeStory;\n\t\tif (!hasArchive(story->peer()) && !story->inProfile()) {\n\t\t\tapplyDeleted(story->peer(), id.story);\n\t\t\treturn;\n\t\t}\n\t}\n\tapplyRemovedFromActive(id);\n}\n\nvoid Stories::applyRemovedFromActive(FullStoryId id) {\n\tconst auto removeFromList = [&](StorySourcesList list) {\n\t\tconst auto index = static_cast<int>(list);\n\t\tauto &sources = _sources[index];\n\t\tconst auto i = ranges::find(\n\t\t\tsources,\n\t\t\tid.peer,\n\t\t\t&StoriesSourceInfo::id);\n\t\tif (i != end(sources)) {\n\t\t\tsources.erase(i);\n\t\t\tnotifySourcesChanged(list);\n\t\t}\n\t};\n\tconst auto i = _all.find(id.peer);\n\tif (i != end(_all)) {\n\t\tconst auto j = i->second.ids.lower_bound(StoryIdDates{ id.story });\n\t\tif (j != end(i->second.ids) && j->id == id.story) {\n\t\t\ti->second.ids.erase(j);\n\t\t\tconst auto peer = i->second.peer;\n\t\t\tif (i->second.ids.empty()) {\n\t\t\t\t_all.erase(i);\n\t\t\t\tremoveFromList(StorySourcesList::NotHidden);\n\t\t\t\tremoveFromList(StorySourcesList::Hidden);\n\t\t\t}\n\t\t\t_sourceChanged.fire_copy(id.peer);\n\t\t\tupdatePeerStoriesState(peer);\n\t\t}\n\t}\n}\n\nvoid Stories::applyDeletedFromSources(PeerId id, StorySourcesList list) {\n\tauto &sources = _sources[static_cast<int>(list)];\n\tconst auto i = ranges::find(\n\t\tsources,\n\t\tid,\n\t\t&StoriesSourceInfo::id);\n\tif (i != end(sources)) {\n\t\tsources.erase(i);\n\t}\n\tnotifySourcesChanged(list);\n}\n\nvoid Stories::removeDependencyStory(not_null<Story*> story) {\n\tconst auto i = _dependentMessages.find(story);\n\tif (i != end(_dependentMessages)) {\n\t\tconst auto items = std::move(i->second);\n\t\t_dependentMessages.erase(i);\n\n\t\tfor (const auto &dependent : items) {\n\t\t\tdependent->dependencyStoryRemoved(story);\n\t\t}\n\t}\n}\n\nvoid Stories::sort(StorySourcesList list) {\n\tconst auto index = static_cast<int>(list);\n\tauto &sources = _sources[index];\n\tconst auto self = _owner->session().userPeerId();\n\tconst auto changelogSenderId = UserData::kServiceNotificationsId;\n\tconst auto proj = [&](const StoriesSourceInfo &info) {\n\t\tconst auto key = int64(info.last)\n\t\t\t+ (info.premium ? (int64(1) << 47) : 0)\n\t\t\t+ ((info.id == changelogSenderId) ? (int64(1) << 47) : 0)\n\t\t\t+ ((info.unreadCount > 0) ? (int64(1) << 49) : 0)\n\t\t\t+ ((info.id == self) ? (int64(1) << 50) : 0);\n\t\treturn std::make_pair(key, info.id);\n\t};\n\tranges::sort(sources, ranges::greater(), proj);\n\tnotifySourcesChanged(list);\n\tpreloadSourcesChanged(list);\n}\n\nstd::shared_ptr<HistoryItem> Stories::lookupItem(not_null<Story*> story) {\n\tconst auto i = _items.find(story->peer()->id);\n\tif (i == end(_items)) {\n\t\treturn nullptr;\n\t}\n\tconst auto j = i->second.find(story->id());\n\tif (j == end(i->second)) {\n\t\treturn nullptr;\n\t}\n\treturn j->second.lock();\n}\n\nStealthMode Stories::stealthMode() const {\n\treturn _stealthMode.current();\n}\n\nrpl::producer<StealthMode> Stories::stealthModeValue() const {\n\treturn _stealthMode.value();\n}\n\nvoid Stories::activateStealthMode(Fn<void()> done) {\n\tconst auto api = &session().api();\n\tusing Flag = MTPstories_ActivateStealthMode::Flag;\n\tapi->request(MTPstories_ActivateStealthMode(\n\t\tMTP_flags(Flag::f_past | Flag::f_future)\n\t)).done([=](const MTPUpdates &result) {\n\t\tapi->applyUpdates(result);\n\t\tif (done) done();\n\t}).fail([=] {\n\t\tif (done) done();\n\t}).send();\n}\n\nvoid Stories::sendReaction(FullStoryId id, Data::ReactionId reaction) {\n\tif (const auto maybeStory = lookup(id)) {\n\t\tconst auto story = *maybeStory;\n\t\tstory->setReactionId(reaction);\n\n\t\tconst auto api = &session().api();\n\t\tapi->request(MTPstories_SendReaction(\n\t\t\tMTP_flags(0),\n\t\t\tstory->peer()->input(),\n\t\t\tMTP_int(id.story),\n\t\t\tReactionToMTP(reaction)\n\t\t)).send();\n\t}\n}\n\nstd::shared_ptr<HistoryItem> Stories::resolveItem(not_null<Story*> story) {\n\tauto &items = _items[story->peer()->id];\n\tauto i = items.find(story->id());\n\tif (i == end(items)) {\n\t\ti = items.emplace(story->id()).first;\n\t} else if (const auto result = i->second.lock()) {\n\t\treturn result;\n\t}\n\tconst auto history = _owner->history(story->peer());\n\tauto result = std::shared_ptr<HistoryItem>(\n\t\thistory->makeMessage(StoryIdToMsgId(story->id()), story).get(),\n\t\tHistoryItem::Destroyer());\n\ti->second = result;\n\treturn result;\n}\n\nstd::shared_ptr<HistoryItem> Stories::resolveItem(FullStoryId id) {\n\tconst auto story = lookup(id);\n\treturn story ? resolveItem(*story) : std::shared_ptr<HistoryItem>();\n}\n\nconst StoriesSource *Stories::source(PeerId id) const {\n\tconst auto i = _all.find(id);\n\treturn (i != end(_all)) ? &i->second : nullptr;\n}\n\nconst std::vector<StoriesSourceInfo> &Stories::sources(\n\t\tStorySourcesList list) const {\n\treturn _sources[static_cast<int>(list)];\n}\n\nbool Stories::sourcesLoaded(StorySourcesList list) const {\n\treturn _sourcesLoaded[static_cast<int>(list)];\n}\n\nrpl::producer<> Stories::sourcesChanged(StorySourcesList list) const {\n\treturn _sourcesChanged[static_cast<int>(list)].events();\n}\n\nrpl::producer<PeerId> Stories::sourceChanged() const {\n\treturn _sourceChanged.events();\n}\n\nrpl::producer<PeerId> Stories::itemsChanged() const {\n\treturn _itemsChanged.events();\n}\n\nbase::expected<not_null<Story*>, NoStory> Stories::lookup(\n\t\tFullStoryId id) const {\n\tconst auto i = _stories.find(id.peer);\n\tif (i != end(_stories)) {\n\t\tconst auto j = i->second.find(id.story);\n\t\tif (j != end(i->second)) {\n\t\t\treturn j->second.get();\n\t\t}\n\t}\n\treturn base::make_unexpected(\n\t\t_deleted.contains(id) ? NoStory::Deleted : NoStory::Unknown);\n}\n\nvoid Stories::resolve(FullStoryId id, Fn<void()> done, bool force) {\n\tif (!force) {\n\t\tconst auto already = lookup(id);\n\t\tif (already.has_value() || already.error() != NoStory::Unknown) {\n\t\t\tif (done) {\n\t\t\t\tdone();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t}\n\tif (const auto i = _resolveSent.find(id.peer); i != end(_resolveSent)) {\n\t\tif (const auto j = i->second.find(id.story); j != end(i->second)) {\n\t\t\tif (done) {\n\t\t\t\tj->second.push_back(std::move(done));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t}\n\tauto &ids = _resolvePending[id.peer];\n\tif (ids.empty()) {\n\t\tcrl::on_main(&session(), [=] {\n\t\t\tsendResolveRequests();\n\t\t});\n\t}\n\tauto &callbacks = ids[id.story];\n\tif (done) {\n\t\tcallbacks.push_back(std::move(done));\n\t}\n}\n\nvoid Stories::loadAround(FullStoryId id, StoriesContext context) {\n\tif (v::is<StoriesContextSingle>(context.data)\n\t\t|| v::is<StoriesContextAlbum>(context.data)) {\n\t\treturn;\n\t}\n\tconst auto i = _all.find(id.peer);\n\tif (i == end(_all)) {\n\t\treturn;\n\t}\n\tconst auto j = i->second.ids.lower_bound(StoryIdDates{ id.story });\n\tif (j == end(i->second.ids) || j->id != id.story) {\n\t\treturn;\n\t}\n\tconst auto ignore = [&] {\n\t\tconst auto side = kIgnorePreloadAroundIfLoaded;\n\t\tconst auto left = ranges::min(int(j - begin(i->second.ids)), side);\n\t\tconst auto right = ranges::min(int(end(i->second.ids) - j), side);\n\t\tfor (auto k = j - left; k != j + right; ++k) {\n\t\t\tconst auto maybeStory = lookup({ id.peer, k->id });\n\t\t\tif (!maybeStory && maybeStory.error() == NoStory::Unknown) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}();\n\tif (ignore) {\n\t\treturn;\n\t}\n\tconst auto side = kPreloadAroundCount;\n\tconst auto left = ranges::min(int(j - begin(i->second.ids)), side);\n\tconst auto right = ranges::min(int(end(i->second.ids) - j), side);\n\tconst auto from = j - left;\n\tconst auto till = j + right;\n\tfor (auto k = from; k != till; ++k) {\n\t\tresolve({ id.peer, k->id }, nullptr);\n\t}\n}\n\nvoid Stories::markAsRead(FullStoryId id, bool viewed) {\n\tif (id.peer == _owner->session().userPeerId()) {\n\t\treturn;\n\t}\n\tconst auto maybeStory = lookup(id);\n\tif (!maybeStory) {\n\t\treturn;\n\t}\n\tconst auto story = *maybeStory;\n\tif (story->expired() && story->inProfile()) {\n\t\t_incrementViewsPending[id.peer].emplace(id.story);\n\t\tif (!_incrementViewsTimer.isActive()) {\n\t\t\t_incrementViewsTimer.callOnce(kIncrementViewsDelay);\n\t\t}\n\t}\n\tif (!bumpReadTill(id.peer, id.story)) {\n\t\treturn;\n\t}\n\tif (!_markReadPending.contains(id.peer)) {\n\t\tsendMarkAsReadRequests();\n\t}\n\t_markReadPending.emplace(id.peer);\n\t_markReadTimer.callOnce(kMarkAsReadDelay);\n}\n\nbool Stories::bumpReadTill(PeerId peerId, StoryId maxReadTill) {\n\tauto &till = _readTill[peerId];\n\tauto refreshItems = std::vector<StoryId>();\n\tconst auto guard = gsl::finally([&] {\n\t\tfor (const auto id : refreshItems) {\n\t\t\t_owner->refreshStoryItemViews({ peerId, id });\n\t\t}\n\t});\n\tif (till < maxReadTill) {\n\t\tconst auto from = till;\n\t\ttill = maxReadTill;\n\t\tupdatePeerStoriesState(_owner->peer(peerId));\n\t\tconst auto i = _stories.find(peerId);\n\t\tif (i != end(_stories)) {\n\t\t\trefreshItems = ranges::make_subrange(\n\t\t\t\ti->second.lower_bound(from + 1),\n\t\t\t\ti->second.lower_bound(till + 1)\n\t\t\t) | ranges::views::transform([=](const auto &pair) {\n\t\t\t\t_owner->session().changes().storyUpdated(\n\t\t\t\t\tpair.second.get(),\n\t\t\t\t\tStoryUpdate::Flag::MarkRead);\n\t\t\t\treturn pair.first;\n\t\t\t}) | ranges::to_vector;\n\t\t}\n\t}\n\tconst auto i = _all.find(peerId);\n\tif (i == end(_all) || i->second.readTill >= maxReadTill) {\n\t\treturn false;\n\t}\n\tconst auto wasUnreadCount = i->second.unreadCount();\n\ti->second.readTill = maxReadTill;\n\tconst auto nowUnreadCount = i->second.unreadCount();\n\tif (wasUnreadCount != nowUnreadCount) {\n\t\tconst auto refreshInList = [&](StorySourcesList list) {\n\t\t\tauto &sources = _sources[static_cast<int>(list)];\n\t\t\tconst auto i = ranges::find(\n\t\t\t\tsources,\n\t\t\t\tpeerId,\n\t\t\t\t&StoriesSourceInfo::id);\n\t\t\tif (i != end(sources)) {\n\t\t\t\ti->unreadCount = nowUnreadCount;\n\t\t\t\tsort(list);\n\t\t\t}\n\t\t};\n\t\trefreshInList(StorySourcesList::NotHidden);\n\t\trefreshInList(StorySourcesList::Hidden);\n\t}\n\treturn true;\n}\n\nvoid Stories::toggleHidden(\n\t\tPeerId peerId,\n\t\tbool hidden,\n\t\tstd::shared_ptr<Ui::Show> show) {\n\tconst auto peer = _owner->peer(peerId);\n\tconst auto byHints = peer->isUser()\n\t\t&& !peer->asUser()->isBot()\n\t\t&& !peer->asUser()->isContact()\n\t\t&& !peer->asUser()->isServiceUser();\n\tconst auto justRemove = (byHints || peer->isServiceUser()) && hidden;\n\tif (peer->hasStoriesHidden() != hidden) {\n\t\tif (byHints && hidden) {\n\t\t\tpeer->asUser()->setStoriesCorrespondent(false);\n\t\t} else if (!justRemove) {\n\t\t\tpeer->setStoriesHidden(hidden);\n\t\t}\n\t\tsession().api().request(MTPstories_TogglePeerStoriesHidden(\n\t\t\tpeer->input(),\n\t\t\tMTP_bool(hidden)\n\t\t)).send();\n\t\tif (byHints) {\n\t\t\tpeer->session().topPeers().remove(peer);\n\t\t}\n\t}\n\n\tconst auto name = peer->shortName();\n\tconst auto guard = gsl::finally([&] {\n\t\tif (show && !justRemove) {\n\t\t\tconst auto phrase = hidden\n\t\t\t\t? tr::lng_stories_hidden_to_contacts\n\t\t\t\t: tr::lng_stories_shown_in_chats;\n\t\t\tshow->showToast(phrase(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\ttr::bold(name),\n\t\t\t\ttr::rich));\n\t\t}\n\t});\n\n\tif (justRemove) {\n\t\tapply(peer, nullptr);\n\t\treturn;\n\t}\n\n\tconst auto i = _all.find(peerId);\n\tif (i == end(_all)) {\n\t\treturn;\n\t}\n\ti->second.hidden = hidden;\n\tconst auto info = i->second.info();\n\tconst auto main = static_cast<int>(StorySourcesList::NotHidden);\n\tconst auto other = static_cast<int>(StorySourcesList::Hidden);\n\tconst auto proj = &StoriesSourceInfo::id;\n\tif (hidden) {\n\t\tconst auto i = ranges::find(_sources[main], peerId, proj);\n\t\tif (i != end(_sources[main])) {\n\t\t\t_sources[main].erase(i);\n\t\t\tnotifySourcesChanged(StorySourcesList::NotHidden);\n\t\t\tpreloadSourcesChanged(StorySourcesList::NotHidden);\n\t\t}\n\t\tconst auto j = ranges::find(_sources[other], peerId, proj);\n\t\tif (j == end(_sources[other])) {\n\t\t\t_sources[other].push_back(info);\n\t\t} else {\n\t\t\t*j = info;\n\t\t}\n\t\tsort(StorySourcesList::Hidden);\n\t} else {\n\t\tconst auto i = ranges::find(_sources[other], peerId, proj);\n\t\tif (i != end(_sources[other])) {\n\t\t\t_sources[other].erase(i);\n\t\t\tnotifySourcesChanged(StorySourcesList::Hidden);\n\t\t\tpreloadSourcesChanged(StorySourcesList::Hidden);\n\t\t}\n\t\tconst auto j = ranges::find(_sources[main], peerId, proj);\n\t\tif (j == end(_sources[main])) {\n\t\t\t_sources[main].push_back(info);\n\t\t} else {\n\t\t\t*j = info;\n\t\t}\n\t\tsort(StorySourcesList::NotHidden);\n\t}\n}\n\nvoid Stories::sendMarkAsReadRequest(\n\t\tnot_null<PeerData*> peer,\n\t\tStoryId tillId) {\n\tconst auto peerId = peer->id;\n\t_markReadRequests.emplace(peerId);\n\tconst auto finish = [=] {\n\t\t_markReadRequests.remove(peerId);\n\t\tif (!_markReadTimer.isActive()\n\t\t\t&& _markReadPending.contains(peerId)) {\n\t\t\tsendMarkAsReadRequests();\n\t\t}\n\t\tcheckQuitPreventFinished();\n\t};\n\n\tconst auto api = &_owner->session().api();\n\tapi->request(MTPstories_ReadStories(\n\t\tpeer->input(),\n\t\tMTP_int(tillId)\n\t)).done(finish).fail(finish).send();\n}\n\nvoid Stories::checkQuitPreventFinished() {\n\tif (_markReadRequests.empty() && _incrementViewsRequests.empty()) {\n\t\tif (Core::Quitting()) {\n\t\t\tLOG((\"Stories doesn't prevent quit any more.\"));\n\t\t}\n\t\tCore::App().quitPreventFinished();\n\t}\n}\n\nvoid Stories::sendMarkAsReadRequests() {\n\t_markReadTimer.cancel();\n\tfor (auto i = begin(_markReadPending); i != end(_markReadPending);) {\n\t\tconst auto peerId = *i;\n\t\tif (_markReadRequests.contains(peerId)) {\n\t\t\t++i;\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto j = _all.find(peerId);\n\t\tif (j != end(_all)) {\n\t\t\tsendMarkAsReadRequest(j->second.peer, j->second.readTill);\n\t\t}\n\t\ti = _markReadPending.erase(i);\n\t}\n}\n\nvoid Stories::sendIncrementViewsRequests() {\n\tif (_incrementViewsPending.empty()) {\n\t\treturn;\n\t}\n\tstruct Prepared {\n\t\tPeerId peer = 0;\n\t\tQVector<MTPint> ids;\n\t};\n\tauto prepared = std::vector<Prepared>();\n\tfor (const auto &[peer, ids] : _incrementViewsPending) {\n\t\tif (_incrementViewsRequests.contains(peer)) {\n\t\t\tcontinue;\n\t\t}\n\t\tprepared.push_back({ .peer = peer });\n\t\tfor (const auto &id : ids) {\n\t\t\tprepared.back().ids.push_back(MTP_int(id));\n\t\t}\n\t}\n\n\tconst auto api = &_owner->session().api();\n\tfor (auto &[peer, ids] : prepared) {\n\t\t_incrementViewsRequests.emplace(peer);\n\t\tconst auto finish = [=, peer = peer] {\n\t\t\t_incrementViewsRequests.remove(peer);\n\t\t\tif (!_incrementViewsTimer.isActive()\n\t\t\t\t&& _incrementViewsPending.contains(peer)) {\n\t\t\t\tsendIncrementViewsRequests();\n\t\t\t}\n\t\t\tcheckQuitPreventFinished();\n\t\t};\n\t\tapi->request(MTPstories_IncrementStoryViews(\n\t\t\t_owner->peer(peer)->input(),\n\t\t\tMTP_vector<MTPint>(std::move(ids))\n\t\t)).done(finish).fail(finish).send();\n\t\t_incrementViewsPending.remove(peer);\n\t}\n}\n\nvoid Stories::loadViewsSlice(\n\t\tnot_null<PeerData*> peer,\n\t\tStoryId id,\n\t\tQString offset,\n\t\tFn<void(StoryViews)> done) {\n\tExpects(peer->isSelf() || !done);\n\n\tif (_viewsStoryPeer == peer\n\t\t&& _viewsStoryId == id\n\t\t&& _viewsOffset == offset\n\t\t&& (!offset.isEmpty() || _viewsRequestId)) {\n\t\tif (_viewsRequestId) {\n\t\t\t_viewsDone = std::move(done);\n\t\t}\n\t\treturn;\n\t}\n\t_viewsStoryPeer = peer;\n\t_viewsStoryId = id;\n\t_viewsOffset = offset;\n\t_viewsDone = std::move(done);\n\n\tif (peer->isSelf()) {\n\t\tsendViewsSliceRequest();\n\t} else {\n\t\tsendViewsCountsRequest();\n\t}\n}\n\nvoid Stories::loadReactionsSlice(\n\t\tnot_null<PeerData*> peer,\n\t\tStoryId id,\n\t\tQString offset,\n\t\tFn<void(StoryViews)> done) {\n\tExpects(peer->isChannel());\n\n\tif (_reactionsStoryPeer == peer\n\t\t&& _reactionsStoryId == id\n\t\t&& _reactionsOffset == offset) {\n\t\tif (_reactionsRequestId) {\n\t\t\t_reactionsDone = std::move(done);\n\t\t}\n\t\treturn;\n\t}\n\t_reactionsStoryPeer = peer;\n\t_reactionsStoryId = id;\n\t_reactionsOffset = offset;\n\t_reactionsDone = std::move(done);\n\n\tusing Flag = MTPstories_GetStoryReactionsList::Flag;\n\tconst auto api = &_owner->session().api();\n\t_owner->session().api().request(_reactionsRequestId).cancel();\n\t_reactionsRequestId = api->request(MTPstories_GetStoryReactionsList(\n\t\tMTP_flags(offset.isEmpty() ? Flag() : Flag::f_offset),\n\t\t_reactionsStoryPeer->input(),\n\t\tMTP_int(_reactionsStoryId),\n\t\tMTPReaction(),\n\t\tMTP_string(_reactionsOffset),\n\t\tMTP_int(kViewsPerPage)\n\t)).done([=](const MTPstories_StoryReactionsList &result) {\n\t\t_reactionsRequestId = 0;\n\n\t\tconst auto &data = result.data();\n\t\tauto slice = StoryViews{\n\t\t\t.nextOffset = data.vnext_offset().value_or_empty(),\n\t\t\t.reactions = data.vcount().v,\n\t\t\t.total = data.vcount().v,\n\t\t};\n\t\t_owner->processUsers(data.vusers());\n\t\t_owner->processChats(data.vchats());\n\t\tslice.list.reserve(data.vreactions().v.size());\n\t\tfor (const auto &reaction : data.vreactions().v) {\n\t\t\treaction.match([&](const MTPDstoryReaction &data) {\n\t\t\t\tslice.list.push_back({\n\t\t\t\t\t.peer = _owner->peer(peerFromMTP(data.vpeer_id())),\n\t\t\t\t\t.reaction = ReactionFromMTP(data.vreaction()),\n\t\t\t\t\t.date = data.vdate().v,\n\t\t\t\t});\n\t\t\t}, [&](const MTPDstoryReactionPublicRepost &data) {\n\t\t\t\tconst auto story = applySingle(\n\t\t\t\t\tpeerFromMTP(data.vpeer_id()),\n\t\t\t\t\tdata.vstory());\n\t\t\t\tif (story) {\n\t\t\t\t\tslice.list.push_back({\n\t\t\t\t\t\t.peer = story->peer(),\n\t\t\t\t\t\t.repostId = story->id(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}, [&](const MTPDstoryReactionPublicForward &data) {\n\t\t\t\tconst auto item = _owner->addNewMessage(\n\t\t\t\t\tdata.vmessage(),\n\t\t\t\t\t{},\n\t\t\t\t\tNewMessageType::Existing);\n\t\t\t\tif (item) {\n\t\t\t\t\tslice.list.push_back({\n\t\t\t\t\t\t.peer = item->history()->peer,\n\t\t\t\t\t\t.forwardId = item->id,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\tconst auto fullId = FullStoryId{\n\t\t\t.peer = _reactionsStoryPeer->id,\n\t\t\t.story = _reactionsStoryId,\n\t\t};\n\t\tif (const auto story = lookup(fullId)) {\n\t\t\t(*story)->applyChannelReactionsSlice(_reactionsOffset, slice);\n\t\t}\n\t\tif (const auto done = base::take(_reactionsDone)) {\n\t\t\tdone(std::move(slice));\n\t\t}\n\t}).fail([=] {\n\t\t_reactionsRequestId = 0;\n\t\tif (const auto done = base::take(_reactionsDone)) {\n\t\t\tdone({});\n\t\t}\n\t}).send();\n}\n\nvoid Stories::sendViewsSliceRequest() {\n\tExpects(_viewsStoryPeer != nullptr);\n\tExpects(_viewsStoryPeer->isSelf());\n\n\tusing Flag = MTPstories_GetStoryViewsList::Flag;\n\tconst auto api = &_owner->session().api();\n\t_owner->session().api().request(_viewsRequestId).cancel();\n\t_viewsRequestId = api->request(MTPstories_GetStoryViewsList(\n\t\tMTP_flags(Flag::f_reactions_first),\n\t\t_viewsStoryPeer->input(),\n\t\tMTPstring(), // q\n\t\tMTP_int(_viewsStoryId),\n\t\tMTP_string(_viewsOffset),\n\t\tMTP_int(_viewsDone ? kViewsPerPage : kPollingViewsPerPage)\n\t)).done([=](const MTPstories_StoryViewsList &result) {\n\t\t_viewsRequestId = 0;\n\n\t\tconst auto &data = result.data();\n\t\tauto slice = StoryViews{\n\t\t\t.nextOffset = data.vnext_offset().value_or_empty(),\n\t\t\t.reactions = data.vreactions_count().v,\n\t\t\t.forwards = data.vforwards_count().v,\n\t\t\t.views = data.vviews_count().v,\n\t\t\t.total = data.vcount().v,\n\t\t};\n\t\t_owner->processUsers(data.vusers());\n\t\t_owner->processChats(data.vchats());\n\t\tslice.list.reserve(data.vviews().v.size());\n\t\tfor (const auto &view : data.vviews().v) {\n\t\t\tview.match([&](const MTPDstoryView &data) {\n\t\t\t\tslice.list.push_back({\n\t\t\t\t\t.peer = _owner->peer(peerFromUser(data.vuser_id())),\n\t\t\t\t\t.reaction = (data.vreaction()\n\t\t\t\t\t\t? ReactionFromMTP(*data.vreaction())\n\t\t\t\t\t\t: Data::ReactionId()),\n\t\t\t\t\t.date = data.vdate().v,\n\t\t\t\t});\n\t\t\t}, [&](const MTPDstoryViewPublicRepost &data) {\n\t\t\t\tconst auto story = applySingle(\n\t\t\t\t\tpeerFromMTP(data.vpeer_id()),\n\t\t\t\t\tdata.vstory());\n\t\t\t\tif (story) {\n\t\t\t\t\tslice.list.push_back({\n\t\t\t\t\t\t.peer = story->peer(),\n\t\t\t\t\t\t.repostId = story->id(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}, [&](const MTPDstoryViewPublicForward &data) {\n\t\t\t\tconst auto item = _owner->addNewMessage(\n\t\t\t\t\tdata.vmessage(),\n\t\t\t\t\t{},\n\t\t\t\t\tNewMessageType::Existing);\n\t\t\t\tif (item) {\n\t\t\t\t\tslice.list.push_back({\n\t\t\t\t\t\t.peer = item->history()->peer,\n\t\t\t\t\t\t.forwardId = item->id,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\tconst auto fullId = FullStoryId{\n\t\t\t.peer = _owner->session().userPeerId(),\n\t\t\t.story = _viewsStoryId,\n\t\t};\n\t\tif (const auto story = lookup(fullId)) {\n\t\t\t(*story)->applyViewsSlice(_viewsOffset, slice);\n\t\t}\n\t\tif (const auto done = base::take(_viewsDone)) {\n\t\t\tdone(std::move(slice));\n\t\t}\n\t}).fail([=] {\n\t\t_viewsRequestId = 0;\n\t\tif (const auto done = base::take(_viewsDone)) {\n\t\t\tdone({});\n\t\t}\n\t}).send();\n}\n\nvoid Stories::sendViewsCountsRequest() {\n\tExpects(_viewsStoryPeer != nullptr);\n\tExpects(!_viewsDone);\n\n\tconst auto api = &_owner->session().api();\n\t_owner->session().api().request(_viewsRequestId).cancel();\n\t_viewsRequestId = api->request(MTPstories_GetStoriesViews(\n\t\t_viewsStoryPeer->input(),\n\t\tMTP_vector<MTPint>(1, MTP_int(_viewsStoryId))\n\t)).done([=](const MTPstories_StoryViews &result) {\n\t\t_viewsRequestId = 0;\n\n\t\tconst auto &data = result.data();\n\t\t_owner->processUsers(data.vusers());\n\t\tif (data.vviews().v.size() == 1) {\n\t\t\tconst auto fullId = FullStoryId{\n\t\t\t\t_viewsStoryPeer->id,\n\t\t\t\t_viewsStoryId,\n\t\t\t};\n\t\t\tif (const auto story = lookup(fullId)) {\n\t\t\t\t(*story)->applyViewsCounts(data.vviews().v.front().data());\n\t\t\t}\n\t\t}\n\t}).fail([=] {\n\t\t_viewsRequestId = 0;\n\t}).send();\n}\n\nbool Stories::hasArchive(not_null<PeerData*> peer) const {\n\tif (peer->isSelf()) {\n\t\treturn true;\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn channel->canEditStories();\n\t}\n\treturn false;\n}\n\nconst Stories::Set *Stories::albumIdsSet(PeerId peerId, int albumId) const {\n\treturn const_cast<Stories*>(this)->albumIdsSet(peerId, albumId, true);\n}\n\nStories::Set *Stories::albumIdsSet(PeerId peerId, int albumId, bool lazy) {\n\tif (albumId == kStoriesAlbumIdArchive) {\n\t\tconst auto peer = _owner->peer(peerId);\n\t\tif (!hasArchive(peer)) {\n\t\t\tclearArchive(peer);\n\t\t\treturn nullptr;\n\t\t}\n\t\tconst auto i = _archive.find(peerId);\n\t\treturn (i != end(_archive))\n\t\t\t? &i->second\n\t\t\t: lazy\n\t\t\t? nullptr\n\t\t\t: &_archive.emplace(peerId, Set()).first->second;\n\t} else if (albumId == kStoriesAlbumIdSaved) {\n\t\tconst auto i = _saved.find(peerId);\n\t\treturn (i != end(_saved))\n\t\t\t? &i->second\n\t\t\t: lazy\n\t\t\t? nullptr\n\t\t\t: &_saved.emplace(peerId, Set()).first->second;\n\t}\n\tauto i = _albums.find(peerId);\n\tif (i == end(_albums)) {\n\t\tif (lazy) {\n\t\t\treturn nullptr;\n\t\t}\n\t\ti = _albums.emplace(peerId, Albums()).first;\n\t}\n\tconst auto j = i->second.sets.find(albumId);\n\treturn (j != end(i->second.sets))\n\t\t? &j->second\n\t\t: lazy\n\t\t? nullptr\n\t\t: &i->second.sets.emplace(albumId).first->second;\n}\n\nconst StoriesIds &Stories::albumIds(PeerId peerId, int albumId) const {\n\tstatic const auto empty = StoriesIds();\n\tconst auto set = albumIdsSet(peerId, albumId);\n\treturn set ? set->ids : empty;\n}\n\nrpl::producer<StoryAlbumIdsKey> Stories::albumIdsChanged() const {\n\treturn _albumIdsChanged.events();\n}\n\nint Stories::albumIdsCount(PeerId peerId, int albumId) const {\n\tconst auto set = albumIdsSet(peerId, albumId);\n\treturn set ? std::max(set->total, 0) : 0;\n}\n\nbool Stories::albumIdsCountKnown(PeerId peerId, int albumId) const {\n\tconst auto set = albumIdsSet(peerId, albumId);\n\treturn set && (set->total >= 0);\n}\n\nbool Stories::albumIdsLoaded(PeerId peerId, int albumId) const {\n\tconst auto set = albumIdsSet(peerId, albumId);\n\treturn set && set->loaded;\n}\n\nvoid Stories::albumIdsLoadMore(PeerId peerId, int albumId) {\n\talbumIdsLoadMore(peerId, albumId, false);\n}\n\nvoid Stories::albumIdsLoadMore(PeerId peerId, int albumId, bool reload) {\n\tExpects(!reload || albumId > 0);\n\n\tconst auto peer = _owner->peer(peerId);\n\tconst auto set = albumIdsSet(peerId, albumId);\n\tif (set && reload) {\n\t\t_owner->session().api().request(base::take(set->requestId)).cancel();\n\t}\n\tif (!set || set->requestId || (!reload && set->loaded)) {\n\t\treturn;\n\t}\n\tconst auto api = &_owner->session().api();\n\tconst auto done = [=](const MTPstories_Stories &result) {\n\t\tconst auto set = albumIdsSet(peerId, albumId);\n\t\tif (!set) {\n\t\t\treturn;\n\t\t}\n\t\tset->requestId = 0;\n\t\tif (reload) {\n\t\t\tset->ids.list.clear();\n\t\t\tset->ids.pinnedToTop.clear();\n\t\t\tset->lastId = StoryId();\n\t\t}\n\t\tconst auto &data = result.data();\n\t\tconst auto now = base::unixtime::now();\n\t\tauto pinnedToTopIds = data.vpinned_to_top().value_or_empty();\n\t\tauto pinnedToTop = pinnedToTopIds\n\t\t\t| ranges::views::transform(&MTPint::v)\n\t\t\t| ranges::to_vector;\n\t\tconst auto ordered = (albumId == kStoriesAlbumIdSaved)\n\t\t\t|| (albumId == kStoriesAlbumIdArchive);\n\t\tset->total = data.vcount().v;\n\t\tfor (const auto &story : data.vstories().v) {\n\t\t\tconst auto id = story.match([&](const auto &id) {\n\t\t\t\treturn id.vid().v;\n\t\t\t});\n\t\t\tif (ordered) {\n\t\t\t\tInsertSorted(set->ids.list, id);\n\t\t\t} else {\n\t\t\t\tset->ids.list.push_back(id);\n\t\t\t}\n\t\t\tset->lastId = id;\n\t\t\tif (!parseAndApply(peer, story, now)) {\n\t\t\t\tif (ordered) {\n\t\t\t\t\tRemoveSorted(set->ids.list, id);\n\t\t\t\t} else {\n\t\t\t\t\tset->ids.list.pop_back();\n\t\t\t\t}\n\t\t\t\tif (set->total > 0) {\n\t\t\t\t\t--set->total;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst auto count = int(set->ids.list.size());\n\t\tset->loaded = data.vstories().v.empty();\n\t\tset->total = set->loaded ? count : std::max(set->total, count);\n\t\tif (albumId == kStoriesAlbumIdSaved) {\n\t\t\tsetPinnedToTop(peerId, std::move(pinnedToTop));\n\t\t}\n\t\t_albumIdsChanged.fire({ peerId, albumId });\n\t};\n\tconst auto fail = [=] {\n\t\tconst auto set = albumIdsSet(peerId, albumId);\n\t\tif (!set) {\n\t\t\treturn;\n\t\t}\n\t\tset->requestId = 0;\n\t\tset->loaded = true;\n\t\tset->total = int(set->ids.list.size());\n\t\t_albumIdsChanged.fire({ peerId, albumId });\n\t};\n\tset->requestId = (albumId == kStoriesAlbumIdArchive)\n\t\t? api->request(MTPstories_GetStoriesArchive(\n\t\t\tpeer->input(),\n\t\t\tMTP_int(set->lastId),\n\t\t\tMTP_int(set->lastId ? kArchivePerPage : kArchiveFirstPerPage)\n\t\t)).done(done).fail(fail).send()\n\t\t: (albumId == kStoriesAlbumIdSaved)\n\t\t? api->request(MTPstories_GetPinnedStories(\n\t\t\tpeer->input(),\n\t\t\tMTP_int(set->lastId),\n\t\t\tMTP_int(set->lastId ? kSavedPerPage : kSavedFirstPerPage)\n\t\t)).done(done).fail(fail).send()\n\t\t: api->request(MTPstories_GetAlbumStories(\n\t\t\tpeer->input(),\n\t\t\tMTP_int(albumId),\n\t\t\tMTP_int(reload ? 0 : set->ids.list.size()),\n\t\t\tMTP_int((reload || set->ids.list.empty())\n\t\t\t\t? kSavedFirstPerPage\n\t\t\t\t: kSavedPerPage)\n\t\t)).done(done).fail(fail).send();\n}\n\nconst base::flat_set<StoryId> &Stories::albumKnownInArchive(\n\t\tPeerId peerId,\n\t\tint albumId) const {\n\tstatic const auto empty = base::flat_set<StoryId>();\n\n\tconst auto i = _albums.find(peerId);\n\tif (i == end(_albums)) {\n\t\treturn empty;\n\t}\n\tconst auto j = i->second.sets.find(albumId);\n\treturn (j != end(i->second.sets))\n\t\t? j->second.albumKnownInArchive\n\t\t: empty;\n}\n\nauto Stories::albumsListValue(PeerId peerId)\n-> rpl::producer<std::vector<Data::StoryAlbum>> {\n\tauto &albums = _albums[peerId];\n\tif (!albums.requestId) {\n\t\tloadAlbums(_owner->peer(peerId), albums);\n\t}\n\treturn albums.list.value();\n}\n\nvoid Stories::loadAlbums(not_null<PeerData*> peer, Albums &albums) {\n\tconst auto api = &_owner->session().api();\n\tapi->request(base::take(albums.requestId)).cancel();\n\talbums.requestId = api->request(MTPstories_GetAlbums(\n\t\tpeer->input(),\n\t\tMTP_long(albums.hash)\n\t)).done([=](const MTPstories_Albums &result) {\n\t\tauto &albums = _albums[peer->id];\n\t\talbums.requestId = 0;\n\t\tresult.match([&](const MTPDstories_albums &data) {\n\t\t\talbums.hash = data.vhash().v;\n\t\t\tauto parsed = std::vector<Data::StoryAlbum>();\n\t\t\tconst auto &list = data.valbums().v;\n\t\t\tparsed.reserve(list.size());\n\t\t\tfor (const auto &album : list) {\n\t\t\t\tparsed.push_back(FromTL(_owner, album));\n\t\t\t}\n\t\t\talbums.list = std::move(parsed);\n\t\t}, [](const MTPDstories_albumsNotModified &) {\n\t\t});\n\t}).fail([=] {\n\t\tauto &albums = _albums[peer->id];\n\t\talbums.requestId = 0;\n\t\talbums.hash = 0;\n\t}).send();\n}\n\nvoid Stories::albumCreate(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &title,\n\t\tStoryId addId,\n\t\tFn<void(StoryAlbum)> done,\n\t\tFn<void(QString)> fail) {\n\tauto ids = QVector<MTPint>();\n\tif (addId) {\n\t\tids.push_back(MTP_int(addId));\n\t}\n\t_owner->session().api().request(MTPstories_CreateAlbum(\n\t\tpeer->input(),\n\t\tMTP_string(title),\n\t\tMTP_vector<MTPint>(ids)\n\t)).done([=](const MTPStoryAlbum &result) {\n\t\tauto &albums = _albums[peer->id];\n\t\tauto current = albums.list.current();\n\t\tauto parsed = FromTL(_owner, result);\n\t\tcurrent.push_back(parsed);\n\t\talbums.list = std::move(current);\n\t\tloadAlbums(peer, albums);\n\t\tif (const auto onstack = done) {\n\t\t\tonstack(std::move(parsed));\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (const auto onstack = fail) {\n\t\t\tonstack(error.type());\n\t\t}\n\t}).send();\n}\n\nvoid Stories::albumRename(\n\t\tnot_null<PeerData*> peer,\n\t\tint id,\n\t\tconst QString &title,\n\t\tFn<void(StoryAlbum)> done,\n\t\tFn<void(QString)> fail) {\n\tusing Flag = MTPstories_UpdateAlbum::Flag;\n\t_owner->session().api().request(MTPstories_UpdateAlbum(\n\t\tMTP_flags(Flag::f_title),\n\t\tpeer->input(),\n\t\tMTP_int(id),\n\t\tMTP_string(title),\n\t\tMTPVector<MTPint>(),\n\t\tMTPVector<MTPint>(),\n\t\tMTPVector<MTPint>()\n\t)).done([=](const MTPStoryAlbum &result) {\n\t\tauto &albums = _albums[peer->id];\n\t\tauto current = albums.list.current();\n\t\tauto parsed = FromTL(_owner, result);\n\t\tcurrent.push_back(parsed);\n\t\talbums.list = std::move(current);\n\t\tloadAlbums(peer, albums);\n\t\tif (const auto onstack = done) {\n\t\t\tonstack(std::move(parsed));\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (const auto onstack = fail) {\n\t\t\tonstack(error.type());\n\t\t}\n\t}).send();\n}\n\nvoid Stories::albumDelete(not_null<PeerData*> peer, int id) {\n\t_owner->session().api().request(MTPstories_DeleteAlbum(\n\t\tpeer->input(),\n\t\tMTP_int(id)\n\t)).send();\n\n\tauto &albums = _albums[peer->id];\n\tauto current = albums.list.current();\n\tcurrent.erase(\n\t\tranges::remove(current, id, &StoryAlbum::id),\n\t\tend(current));\n\talbums.list = std::move(current);\n\n\tconst auto i = albums.sets.find(id);\n\tif (i != end(albums.sets)) {\n\t\t_owner->session().api().request(\n\t\t\tbase::take(i->second.requestId)).cancel();\n\t\tfor (const auto storyId : i->second.albumKnownInArchive) {\n\t\t\tif (const auto story = lookup({ peer->id, storyId })) {\n\t\t\t\tauto now = (*story)->albumIds();\n\t\t\t\tif (now.remove(id)) {\n\t\t\t\t\t(*story)->setAlbumIds(std::move(now));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\talbums.sets.erase(i);\n\t}\n}\n\nvoid Stories::albumReorderStories(\n\t\tnot_null<PeerData*> peer,\n\t\tint albumId,\n\t\tint oldPosition,\n\t\tint newPosition,\n\t\tFn<void()> done,\n\t\tFn<void()> fail) {\n\tconst auto ids = albumIds(peer->id, albumId);\n\tconst auto list = RespectingPinned(ids);\n\n\tif (oldPosition < 0 || newPosition < 0\n\t\t|| oldPosition >= list.size() || newPosition >= list.size()) {\n\t\tfail();\n\t\treturn;\n\t}\n\n\tif (_reorderStoriesRequestId) {\n\t\t_owner->session().api().request(\n\t\t\tbase::take(_reorderStoriesRequestId)).cancel();\n\t}\n\n\tauto reorderedList = list;\n\tbase::reorder(reorderedList, oldPosition, newPosition);\n\n\tauto order = QVector<MTPint>();\n\torder.reserve(reorderedList.size());\n\tfor (const auto id : reorderedList) {\n\t\torder.push_back(MTP_int(id));\n\t}\n\n\t_reorderStoriesRequestId = _owner->session().api().request(\n\t\tMTPstories_UpdateAlbum(\n\t\t\tMTP_flags(MTPstories_UpdateAlbum::Flag::f_order),\n\t\t\tpeer->input(),\n\t\t\tMTP_int(albumId),\n\t\t\tMTPstring(),\n\t\t\tMTPVector<MTPint>(),\n\t\t\tMTPVector<MTPint>(),\n\t\t\tMTP_vector<MTPint>(order)\n\t)).done([=](const MTPStoryAlbum &result) {\n\t\t_reorderStoriesRequestId = 0;\n\t\tif (const auto set = albumIdsSet(peer->id, albumId)) {\n\t\t\tset->ids.list = reorderedList;\n\t\t\t_albumIdsChanged.fire({ peer->id, albumId });\n\t\t}\n\t\tdone();\n\t}).fail([=] {\n\t\t_reorderStoriesRequestId = 0;\n\t\tfail();\n\t}).send();\n}\n\nvoid Stories::notifyAlbumUpdate(StoryAlbumUpdate &&update) {\n\tconst auto peerId = update.peer->id;\n\tconst auto i = _albums.find(peerId);\n\tif (i != end(_albums)) {\n\t\tconst auto albumId = update.albumId;\n\t\tconst auto j = i->second.sets.find(albumId);\n\t\tif (j != end(i->second.sets)) {\n\t\t\tfor (const auto &id : update.added) {\n\t\t\t\tj->second.albumKnownInArchive.emplace(id);\n\t\t\t\tif (const auto story = lookup({ peerId, id })) {\n\t\t\t\t\tauto now = (*story)->albumIds();\n\t\t\t\t\tif (now.emplace(albumId).second) {\n\t\t\t\t\t\t(*story)->setAlbumIds(std::move(now));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const auto &id : update.removed) {\n\t\t\t\tj->second.albumKnownInArchive.remove(id);\n\t\t\t\tif (const auto story = lookup({ peerId, id })) {\n\t\t\t\t\tauto now = (*story)->albumIds();\n\t\t\t\t\tif (now.remove(albumId)) {\n\t\t\t\t\t\t(*story)->setAlbumIds(std::move(now));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\talbumIdsLoadMore(peerId, albumId, true);\n\t\t}\n\t}\n\t_albumUpdates.fire(std::move(update));\n}\n\nrpl::producer<StoryAlbumUpdate> Stories::albumUpdates() const {\n\treturn _albumUpdates.events();\n}\n\nvoid Stories::setPinnedToTop(\n\t\tPeerId peerId,\n\t\tstd::vector<StoryId> &&pinnedToTop) {\n\tconst auto i = _saved.find(peerId);\n\tif (i == end(_saved) && pinnedToTop.empty()) {\n\t\treturn;\n\t}\n\tauto &saved = (i == end(_saved)) ? _saved[peerId] : i->second;\n\tif (saved.ids.pinnedToTop != pinnedToTop) {\n\t\tfor (const auto id : saved.ids.pinnedToTop) {\n\t\t\tif (!ranges::contains(pinnedToTop, id)) {\n\t\t\t\tif (const auto maybeStory = lookup({ peerId, id })) {\n\t\t\t\t\t(*maybeStory)->setPinnedToTop(false);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor (const auto id : pinnedToTop) {\n\t\t\tif (!ranges::contains(saved.ids.pinnedToTop, id)) {\n\t\t\t\tif (const auto maybeStory = lookup({ peerId, id })) {\n\t\t\t\t\t(*maybeStory)->setPinnedToTop(true);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tsaved.ids.pinnedToTop = std::move(pinnedToTop);\n\t}\n}\n\nvoid Stories::deleteList(const std::vector<FullStoryId> &ids) {\n\tif (ids.empty()) {\n\t\treturn;\n\t}\n\tconst auto peer = session().data().peer(ids.front().peer);\n\tauto list = QVector<MTPint>();\n\tlist.reserve(ids.size());\n\tfor (const auto &id : ids) {\n\t\tif (id.peer == peer->id) {\n\t\t\tlist.push_back(MTP_int(id.story));\n\t\t}\n\t}\n\tconst auto api = &_owner->session().api();\n\tapi->request(MTPstories_DeleteStories(\n\t\tpeer->input(),\n\t\tMTP_vector<MTPint>(list)\n\t)).done([=](const MTPVector<MTPint> &result) {\n\t\tfor (const auto &id : result.v) {\n\t\t\tapplyDeleted(peer, id.v);\n\t\t}\n\t}).send();\n}\n\nvoid Stories::toggleInProfileList(\n\t\tconst std::vector<FullStoryId> &ids,\n\t\tbool inProfile) {\n\tif (ids.empty()) {\n\t\treturn;\n\t}\n\tconst auto peer = session().data().peer(ids.front().peer);\n\tauto list = QVector<MTPint>();\n\tlist.reserve(ids.size());\n\tfor (const auto &id : ids) {\n\t\tif (id.peer == peer->id) {\n\t\t\tlist.push_back(MTP_int(id.story));\n\t\t}\n\t}\n\tif (list.empty()) {\n\t\treturn;\n\t}\n\tconst auto api = &_owner->session().api();\n\tapi->request(MTPstories_TogglePinned(\n\t\tpeer->input(),\n\t\tMTP_vector<MTPint>(list),\n\t\tMTP_bool(inProfile)\n\t)).done([=](const MTPVector<MTPint> &result) {\n\t\tconst auto peerId = peer->id;\n\t\tauto &saved = _saved[peerId];\n\t\tconst auto loaded = saved.loaded;\n\t\tconst auto lastId = !saved.ids.list.empty()\n\t\t\t? saved.ids.list.back()\n\t\t\t: saved.lastId\n\t\t\t? saved.lastId\n\t\t\t: std::numeric_limits<StoryId>::max();\n\t\tauto dirty = false;\n\t\tfor (const auto &id : result.v) {\n\t\t\tif (const auto maybeStory = lookup({ peerId, id.v })) {\n\t\t\t\tconst auto story = *maybeStory;\n\t\t\t\tstory->setInProfile(inProfile);\n\t\t\t\tif (inProfile) {\n\t\t\t\t\tconst auto add = loaded || (id.v >= lastId);\n\t\t\t\t\tif (!add) {\n\t\t\t\t\t\tdirty = true;\n\t\t\t\t\t} else if (InsertSorted(saved.ids.list, id.v)) {\n\t\t\t\t\t\tif (saved.total >= 0) {\n\t\t\t\t\t\t\t++saved.total;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (RemoveSorted(saved.ids.list, id.v)) {\n\t\t\t\t\tif (saved.total > 0) {\n\t\t\t\t\t\t--saved.total;\n\t\t\t\t\t}\n\t\t\t\t} else if (!loaded) {\n\t\t\t\t\tdirty = true;\n\t\t\t\t}\n\t\t\t} else if (!loaded) {\n\t\t\t\tdirty = true;\n\t\t\t}\n\t\t}\n\t\tif (dirty) {\n\t\t\talbumIdsLoadMore(peerId, kStoriesAlbumIdSaved);\n\t\t} else {\n\t\t\t_albumIdsChanged.fire({ peerId, kStoriesAlbumIdSaved });\n\t\t}\n\t}).send();\n}\n\nbool Stories::canTogglePinnedList(\n\t\tconst std::vector<FullStoryId> &ids,\n\t\tbool pin) const {\n\tExpects(!ids.empty());\n\n\tif (!pin) {\n\t\treturn true;\n\t}\n\n\tconst auto peerId = ids.front().peer;\n\tconst auto i = _saved.find(peerId);\n\tif (i == end(_saved)) {\n\t\treturn false;\n\t}\n\n\tauto &already = i->second.ids.pinnedToTop;\n\tauto count = int(already.size());\n\tfor (const auto &id : ids) {\n\t\tif (!ranges::contains(already, id.story)) {\n\t\t\t++count;\n\t\t}\n\t}\n\treturn count <= maxPinnedCount();\n}\n\nint Stories::maxPinnedCount() const {\n\tconst auto appConfig = &_owner->session().appConfig();\n\treturn appConfig->get<int>(u\"stories_pinned_to_top_count_max\"_q, 3);\n}\n\nvoid Stories::togglePinnedList(\n\t\tconst std::vector<FullStoryId> &ids,\n\t\tbool pin) {\n\tif (ids.empty()) {\n\t\treturn;\n\t}\n\tconst auto peerId = ids.front().peer;\n\tauto &saved = _saved[peerId];\n\tauto list = QVector<MTPint>();\n\tlist.reserve(maxPinnedCount());\n\tif (pin) {\n\t\tauto copy = ids;\n\t\tranges::sort(copy, ranges::greater());\n\t\tfor (const auto &id : copy) {\n\t\t\tif (id.peer == peerId\n\t\t\t\t&& !ranges::contains(saved.ids.pinnedToTop, id.story)) {\n\t\t\t\tlist.push_back(MTP_int(id.story));\n\t\t\t}\n\t\t}\n\t}\n\tfor (const auto &id : saved.ids.pinnedToTop) {\n\t\tif (pin || !ranges::contains(ids, FullStoryId{ peerId, id })) {\n\t\t\tlist.push_back(MTP_int(id));\n\t\t}\n\t}\n\tconst auto api = &_owner->session().api();\n\tconst auto peer = session().data().peer(peerId);\n\tapi->request(MTPstories_TogglePinnedToTop(\n\t\tpeer->input(),\n\t\tMTP_vector<MTPint>(list)\n\t)).done([=] {\n\t\tsetPinnedToTop(peerId, list\n\t\t\t| ranges::views::transform(&MTPint::v)\n\t\t\t| ranges::to_vector);\n\t\t_albumIdsChanged.fire({ peerId, kStoriesAlbumIdSaved });\n\t}).send();\n\n}\n\nbool Stories::isQuitPrevent() {\n\tif (!_markReadPending.empty()) {\n\t\tsendMarkAsReadRequests();\n\t}\n\tif (!_incrementViewsPending.empty()) {\n\t\tsendIncrementViewsRequests();\n\t}\n\tif (_markReadRequests.empty() && _incrementViewsRequests.empty()) {\n\t\treturn false;\n\t}\n\tLOG((\"Stories prevents quit, marking as read...\"));\n\treturn true;\n}\n\nvoid Stories::incrementPreloadingMainSources() {\n\tExpects(_preloadingMainSourcesCounter >= 0);\n\n\tif (++_preloadingMainSourcesCounter == 1\n\t\t&& rebuildPreloadSources(StorySourcesList::NotHidden)) {\n\t\tcontinuePreloading();\n\t}\n}\n\nvoid Stories::decrementPreloadingMainSources() {\n\tExpects(_preloadingMainSourcesCounter > 0);\n\n\tif (!--_preloadingMainSourcesCounter\n\t\t&& rebuildPreloadSources(StorySourcesList::NotHidden)) {\n\t\tcontinuePreloading();\n\t}\n}\n\nvoid Stories::incrementPreloadingHiddenSources() {\n\tExpects(_preloadingHiddenSourcesCounter >= 0);\n\n\tif (++_preloadingHiddenSourcesCounter == 1\n\t\t&& rebuildPreloadSources(StorySourcesList::Hidden)) {\n\t\tcontinuePreloading();\n\t}\n}\n\nvoid Stories::decrementPreloadingHiddenSources() {\n\tExpects(_preloadingHiddenSourcesCounter > 0);\n\n\tif (!--_preloadingHiddenSourcesCounter\n\t\t&& rebuildPreloadSources(StorySourcesList::Hidden)) {\n\t\tcontinuePreloading();\n\t}\n}\n\nvoid Stories::setPreloadingInViewer(std::vector<FullStoryId> ids) {\n\tids.erase(ranges::remove_if(ids, [&](FullStoryId id) {\n\t\treturn _preloaded.contains(id);\n\t}), end(ids));\n\tif (_toPreloadViewer != ids) {\n\t\t_toPreloadViewer = std::move(ids);\n\t\tcontinuePreloading();\n\t}\n}\n\nstd::optional<Stories::PeerSourceState> Stories::peerSourceState(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPRecentStory &recent) {\n\tconst auto &data = recent.data();\n\tconst auto maxId = data.vmax_id().value_or_empty();\n\tconst auto live = data.is_live();\n\tconst auto i = _readTill.find(peer->id);\n\tif (_readTillReceived || (i != end(_readTill))) {\n\t\treturn PeerSourceState{\n\t\t\t.maxId = maxId,\n\t\t\t.readTill = std::min(\n\t\t\t\tmaxId,\n\t\t\t\t(i != end(_readTill)) ? i->second : 0),\n\t\t\t.hasVideoStream = live,\n\t\t};\n\t}\n\trequestReadTills();\n\t_pendingPeerRecentState[peer] = RecentState{\n\t\t.maxId = maxId,\n\t\t.hasVideoStream = live,\n\t};\n\treturn std::nullopt;\n}\n\nvoid Stories::requestReadTills() {\n\tif (_readTillReceived || _readTillsRequestId) {\n\t\treturn;\n\t}\n\tconst auto api = &_owner->session().api();\n\t_readTillsRequestId = api->request(MTPstories_GetAllReadPeerStories(\n\t)).done([=](const MTPUpdates &result) {\n\t\t_readTillReceived = true;\n\t\tapi->applyUpdates(result);\n\t\tfor (auto &[peer, recent] : base::take(_pendingPeerRecentState)) {\n\t\t\tupdatePeerStoriesState(peer, recent);\n\t\t}\n\t\tfor (const auto &storyId : base::take(_pendingReadTillItems)) {\n\t\t\t_owner->refreshStoryItemViews(storyId);\n\t\t}\n\t}).send();\n}\n\nbool Stories::isUnread(not_null<Story*> story) {\n\tconst auto till = _readTill.find(story->peer()->id);\n\tif (till == end(_readTill) && !_readTillReceived) {\n\t\trequestReadTills();\n\t\t_pendingReadTillItems.emplace(story->fullId());\n\t\treturn false;\n\t}\n\tconst auto readTill = (till != end(_readTill)) ? till->second : 0;\n\treturn (story->id() > readTill);\n}\n\nvoid Stories::registerPolling(not_null<Story*> story, Polling polling) {\n\tauto &settings = _pollingSettings[story];\n\tswitch (polling) {\n\tcase Polling::Chat: ++settings.chat; break;\n\tcase Polling::Viewer:\n\t\t++settings.viewer;\n\t\tif ((story->peer()->isSelf() || story->peer()->isChannel())\n\t\t\t&& _pollingViews.emplace(story).second) {\n\t\t\tsendPollingViewsRequests();\n\t\t}\n\t\tbreak;\n\t}\n\tmaybeSchedulePolling(story, settings, base::unixtime::now());\n}\n\nvoid Stories::unregisterPolling(not_null<Story*> story, Polling polling) {\n\tconst auto i = _pollingSettings.find(story);\n\tAssert(i != end(_pollingSettings));\n\n\tswitch (polling) {\n\tcase Polling::Chat:\n\t\tAssert(i->second.chat > 0);\n\t\t--i->second.chat;\n\t\tbreak;\n\tcase Polling::Viewer:\n\t\tAssert(i->second.viewer > 0);\n\t\tif (!--i->second.viewer) {\n\t\t\t_pollingViews.remove(story);\n\t\t\tif (_pollingViews.empty()) {\n\t\t\t\t_pollingViewsTimer.cancel();\n\t\t\t}\n\t\t}\n\t\tbreak;\n\t}\n\tif (!i->second.chat && !i->second.viewer) {\n\t\t_pollingSettings.erase(i);\n\t}\n}\n\nbool Stories::registerPolling(FullStoryId id, Polling polling) {\n\tif (const auto maybeStory = lookup(id)) {\n\t\tregisterPolling(*maybeStory, polling);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Stories::unregisterPolling(FullStoryId id, Polling polling) {\n\tif (const auto maybeStory = lookup(id)) {\n\t\tunregisterPolling(*maybeStory, polling);\n\t} else if (const auto i = _deletingStories.find(id)\n\t\t; i != end(_deletingStories)) {\n\t\tunregisterPolling(i->second.get(), polling);\n\t} else {\n\t\tUnexpected(\"Couldn't find story for unregistering polling.\");\n\t}\n}\n\nint Stories::pollingInterval(const PollingSettings &settings) const {\n\treturn settings.viewer ? kPollingIntervalViewer : kPollingIntervalChat;\n}\n\nvoid Stories::maybeSchedulePolling(\n\t\tnot_null<Story*> story,\n\t\tconst PollingSettings &settings,\n\t\tTimeId now) {\n\tconst auto last = story->lastUpdateTime();\n\tconst auto next = last + pollingInterval(settings);\n\tconst auto left = std::max(next - now, 0) * crl::time(1000) + 1;\n\tif (!_pollingTimer.isActive() || _pollingTimer.remainingTime() > left) {\n\t\t_pollingTimer.callOnce(left);\n\t}\n}\n\nvoid Stories::sendPollingRequests() {\n\tauto min = 0;\n\tconst auto now = base::unixtime::now();\n\tfor (const auto &[story, settings] : _pollingSettings) {\n\t\tconst auto last = story->lastUpdateTime();\n\t\tconst auto next = last + pollingInterval(settings);\n\t\tif (now >= next) {\n\t\t\tresolve(story->fullId(), nullptr, true);\n\t\t} else {\n\t\t\tconst auto left = (next - now) * crl::time(1000) + 1;\n\t\t\tif (!min || left < min) {\n\t\t\t\tmin = left;\n\t\t\t}\n\t\t}\n\t}\n\tif (min > 0) {\n\t\t_pollingTimer.callOnce(min);\n\t}\n}\n\nvoid Stories::sendPollingViewsRequests() {\n\tif (_pollingViews.empty()) {\n\t\treturn;\n\t} else if (!_viewsRequestId) {\n\t\tAssert(_viewsDone == nullptr);\n\t\tconst auto story = _pollingViews.front();\n\t\tloadViewsSlice(story->peer(), story->id(), QString(), nullptr);\n\t}\n\t_pollingViewsTimer.callOnce(kPollViewsInterval);\n}\n\nvoid Stories::updatePeerStoriesState(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::optional<RecentState> cachedRecentState) {\n\tconst auto till = _readTill.find(peer->id);\n\tconst auto readTill = (till != end(_readTill)) ? till->second : 0;\n\tconst auto pendingRecentState = [&] {\n\t\tif (cachedRecentState) {\n\t\t\treturn *cachedRecentState;\n\t\t}\n\t\tconst auto j = _pendingPeerRecentState.find(peer);\n\t\treturn (j != end(_pendingPeerRecentState))\n\t\t\t? j->second\n\t\t\t: RecentState();\n\t};\n\tconst auto i = _all.find(peer->id);\n\tconst auto recent = (i != end(_all))\n\t\t? RecentState{\n\t\t\t(i->second.ids.empty() ? 0 : i->second.ids.back().id),\n\t\t\t(!i->second.ids.empty() && i->second.ids.back().videoStream),\n\t\t}\n\t\t: pendingRecentState();\n\tpeer->setStoriesState(recent.hasVideoStream\n\t\t? PeerData::StoriesState::HasVideoStream\n\t\t: !recent.maxId\n\t\t? PeerData::StoriesState::None\n\t\t: (recent.maxId <= readTill)\n\t\t? PeerData::StoriesState::HasRead\n\t\t: PeerData::StoriesState::HasUnread);\n}\n\nvoid Stories::preloadSourcesChanged(StorySourcesList list) {\n\tif (rebuildPreloadSources(list)) {\n\t\tcontinuePreloading();\n\t}\n}\n\nbool Stories::rebuildPreloadSources(StorySourcesList list) {\n\tconst auto index = static_cast<int>(list);\n\tconst auto &counter = (list == StorySourcesList::Hidden)\n\t\t? _preloadingHiddenSourcesCounter\n\t\t: _preloadingMainSourcesCounter;\n\tif (!counter) {\n\t\treturn !base::take(_toPreloadSources[index]).empty();\n\t}\n\tauto now = std::vector<FullStoryId>();\n\tauto processed = 0;\n\tfor (const auto &source : _sources[index]) {\n\t\tconst auto i = _all.find(source.id);\n\t\tif (i != end(_all)) {\n\t\t\tif (const auto id = i->second.toOpen().id) {\n\t\t\t\tconst auto fullId = FullStoryId{ source.id, id };\n\t\t\t\tif (!_preloaded.contains(fullId)) {\n\t\t\t\t\tnow.push_back(fullId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (++processed >= kMaxPreloadSources) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (now != _toPreloadSources[index]) {\n\t\t_toPreloadSources[index] = std::move(now);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Stories::continuePreloading() {\n\tconst auto now = _preloading ? _preloading->id() : FullStoryId();\n\tif (now) {\n\t\tif (shouldContinuePreload(now)) {\n\t\t\treturn;\n\t\t}\n\t\t_preloading = nullptr;\n\t}\n\tconst auto id = nextPreloadId();\n\tif (!id) {\n\t\treturn;\n\t} else if (const auto maybeStory = lookup(id)) {\n\t\tstartPreloading(*maybeStory);\n\t}\n}\n\nbool Stories::shouldContinuePreload(FullStoryId id) const {\n\tconst auto first = ranges::views::concat(\n\t\t_toPreloadViewer,\n\t\t_toPreloadSources[static_cast<int>(StorySourcesList::Hidden)],\n\t\t_toPreloadSources[static_cast<int>(StorySourcesList::NotHidden)]\n\t) | ranges::views::take(kStillPreloadFromFirst);\n\treturn ranges::contains(first, id);\n}\n\nFullStoryId Stories::nextPreloadId() const {\n\tconst auto hidden = static_cast<int>(StorySourcesList::Hidden);\n\tconst auto main = static_cast<int>(StorySourcesList::NotHidden);\n\tconst auto result = !_toPreloadViewer.empty()\n\t\t? _toPreloadViewer.front()\n\t\t: !_toPreloadSources[hidden].empty()\n\t\t? _toPreloadSources[hidden].front()\n\t\t: !_toPreloadSources[main].empty()\n\t\t? _toPreloadSources[main].front()\n\t\t: FullStoryId();\n\n\tEnsures(!_preloaded.contains(result));\n\treturn result;\n}\n\nvoid Stories::startPreloading(not_null<Story*> story) {\n\tExpects(!_preloaded.contains(story->fullId()));\n\n\tconst auto id = story->fullId();\n\tauto preloading = std::make_unique<StoryPreload>(story, [=] {\n\t\t_preloading = nullptr;\n\t\tpreloadFinished(id, true);\n\t});\n\tif (!_preloaded.contains(id)) {\n\t\t_preloading = std::move(preloading);\n\t}\n}\n\nvoid Stories::preloadFinished(FullStoryId id, bool markAsPreloaded) {\n\tfor (auto &sources : _toPreloadSources) {\n\t\tsources.erase(ranges::remove(sources, id), end(sources));\n\t}\n\t_toPreloadViewer.erase(\n\t\tranges::remove(_toPreloadViewer, id),\n\t\tend(_toPreloadViewer));\n\tif (markAsPreloaded) {\n\t\t_preloaded.emplace(id);\n\t}\n\tcrl::on_main(this, [=] {\n\t\tcontinuePreloading();\n\t});\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_stories.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/qt/qt_compare.h\"\n#include \"base/expected.h\"\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n#include \"data/data_story.h\"\n\nnamespace Data {\nclass GroupCall;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass Show;\n} // namespace Ui\n\nnamespace Data {\n\ninline constexpr auto kStoriesAlbumIdSaved = 0;\ninline constexpr auto kStoriesAlbumIdArchive = -1;\n\nclass Folder;\nclass Session;\nstruct StoryView;\nstruct StoryIdDates;\nclass Story;\nstruct StoryAlbum;\nclass StoryPreload;\n\nstruct StoriesIds {\n\tstd::vector<StoryId> list; // flat_set<int, greater> for saved/archive.\n\tstd::vector<StoryId> pinnedToTop;\n\n\tfriend inline bool operator==(\n\t\tconst StoriesIds&,\n\t\tconst StoriesIds&) = default;\n};\n\n[[nodiscard]] std::vector<StoryId> RespectingPinned(const StoriesIds &ids);\n\nstruct StoriesSourceInfo {\n\tPeerId id = 0;\n\tTimeId last = 0;\n\tuint32 count : 15 = 0;\n\tuint32 unreadCount : 15 = 0;\n\tuint32 premium : 1 = 0;\n\tuint32 hasVideoStream : 1 = 0;\n\n\tfriend inline bool operator==(\n\t\tStoriesSourceInfo,\n\t\tStoriesSourceInfo) = default;\n};\n\nstruct StoriesSource {\n\tnot_null<PeerData*> peer;\n\tbase::flat_set<StoryIdDates> ids;\n\tStoryId readTill = 0;\n\tbool hidden = false;\n\tbool hasVideoStream = false;\n\n\t[[nodiscard]] StoriesSourceInfo info() const;\n\t[[nodiscard]] int unreadCount() const;\n\t[[nodiscard]] StoryIdDates toOpen() const;\n\n\tfriend inline bool operator==(StoriesSource, StoriesSource) = default;\n};\n\nenum class NoStory : uchar {\n\tUnknown,\n\tDeleted,\n};\n\nenum class StorySourcesList : uchar {\n\tNotHidden,\n\tHidden,\n};\n\nstruct StoriesContextSingle {\n\tfriend inline auto operator<=>(\n\t\tStoriesContextSingle,\n\t\tStoriesContextSingle) = default;\n\tfriend inline bool operator==(StoriesContextSingle, StoriesContextSingle) = default;\n};\n\nstruct StoriesContextPeer {\n\tfriend inline auto operator<=>(\n\t\tStoriesContextPeer,\n\t\tStoriesContextPeer) = default;\n\tfriend inline bool operator==(StoriesContextPeer, StoriesContextPeer) = default;\n};\n\nstruct StoriesContextAlbum {\n\tint id = 0;\n\n\tfriend inline auto operator<=>(\n\t\tStoriesContextAlbum,\n\t\tStoriesContextAlbum) = default;\n\tfriend inline bool operator==(StoriesContextAlbum, StoriesContextAlbum) = default;\n};\n\nstruct StoriesContext {\n\tstd::variant<\n\t\tStoriesContextSingle,\n\t\tStoriesContextPeer,\n\t\tStoriesContextAlbum,\n\t\tStorySourcesList> data;\n\n\tfriend inline auto operator<=>(\n\t\tStoriesContext,\n\t\tStoriesContext) = default;\n\tfriend inline bool operator==(StoriesContext, StoriesContext) = default;\n};\n\nstruct StealthMode {\n\tTimeId enabledTill = 0;\n\tTimeId cooldownTill = 0;\n\n\tfriend inline auto operator<=>(StealthMode, StealthMode) = default;\n\tfriend inline bool operator==(StealthMode, StealthMode) = default;\n};\n\nstruct StoryAlbumUpdate {\n\tnot_null<PeerData*> peer;\n\tint albumId = 0;\n\tstd::vector<StoryId> added;\n\tstd::vector<StoryId> removed;\n};\n\ninline constexpr auto kStorySourcesListCount = 2;\n\nstruct StoryAlbumIdsKey {\n\tPeerId peerId;\n\tint albumId = 0;\n\n\tfriend inline auto operator<=>(\n\t\tStoryAlbumIdsKey,\n\t\tStoryAlbumIdsKey) = default;\n\tfriend inline bool operator==(\n\t\tStoryAlbumIdsKey,\n\t\tStoryAlbumIdsKey) = default;\n};\n\nclass Stories final : public base::has_weak_ptr {\npublic:\n\texplicit Stories(not_null<Session*> owner);\n\t~Stories();\n\n\tstatic constexpr auto kInProfileToastDuration = 4 * crl::time(1000);\n\n\t[[nodiscard]] Session &owner() const;\n\t[[nodiscard]] Main::Session &session() const;\n\n\tvoid updateDependentMessages(not_null<Data::Story*> story);\n\tvoid registerDependentMessage(\n\t\tnot_null<HistoryItem*> dependent,\n\t\tnot_null<Data::Story*> dependency);\n\tvoid unregisterDependentMessage(\n\t\tnot_null<HistoryItem*> dependent,\n\t\tnot_null<Data::Story*> dependency);\n\n\tvoid loadMore(StorySourcesList list);\n\tvoid apply(const MTPDupdateStory &data);\n\tvoid apply(const MTPDupdateReadStories &data);\n\tvoid apply(const MTPStoriesStealthMode &stealthMode);\n\tvoid apply(not_null<PeerData*> peer, const MTPPeerStories *data);\n\tStory *applySingle(PeerId peerId, const MTPstoryItem &story);\n\tvoid loadAround(FullStoryId id, StoriesContext context);\n\n\tconst StoriesSource *source(PeerId id) const;\n\t[[nodiscard]] const std::vector<StoriesSourceInfo> &sources(\n\t\tStorySourcesList list) const;\n\t[[nodiscard]] bool sourcesLoaded(StorySourcesList list) const;\n\t[[nodiscard]] rpl::producer<> sourcesChanged(\n\t\tStorySourcesList list) const;\n\t[[nodiscard]] rpl::producer<PeerId> sourceChanged() const;\n\t[[nodiscard]] rpl::producer<PeerId> itemsChanged() const;\n\n\t[[nodiscard]] base::expected<not_null<Story*>, NoStory> lookup(\n\t\tFullStoryId id) const;\n\tvoid resolve(FullStoryId id, Fn<void()> done, bool force = false);\n\t[[nodiscard]] std::shared_ptr<HistoryItem> resolveItem(FullStoryId id);\n\t[[nodiscard]] std::shared_ptr<HistoryItem> resolveItem(\n\t\tnot_null<Story*> story);\n\n\t[[nodiscard]] bool isQuitPrevent();\n\tvoid markAsRead(FullStoryId id, bool viewed);\n\n\tvoid toggleHidden(\n\t\tPeerId peerId,\n\t\tbool hidden,\n\t\tstd::shared_ptr<Ui::Show> show);\n\n\tstatic constexpr auto kViewsPerPage = 50;\n\tvoid loadViewsSlice(\n\t\tnot_null<PeerData*> peer,\n\t\tStoryId id,\n\t\tQString offset,\n\t\tFn<void(StoryViews)> done);\n\tvoid loadReactionsSlice(\n\t\tnot_null<PeerData*> peer,\n\t\tStoryId id,\n\t\tQString offset,\n\t\tFn<void(StoryViews)> done);\n\n\t[[nodiscard]] bool hasArchive(not_null<PeerData*> peer) const;\n\n\t[[nodiscard]] const StoriesIds &albumIds(\n\t\tPeerId peerId,\n\t\tint albumId) const;\n\t[[nodiscard]] rpl::producer<StoryAlbumIdsKey> albumIdsChanged() const;\n\t[[nodiscard]] int albumIdsCount(PeerId peerId, int albumId) const;\n\t[[nodiscard]] bool albumIdsCountKnown(PeerId peerId, int albumId) const;\n\t[[nodiscard]] bool albumIdsLoaded(PeerId peerId, int albumId) const;\n\tvoid albumIdsLoadMore(PeerId peerId, int albumId);\n\t[[nodiscard]] const base::flat_set<StoryId> &albumKnownInArchive(\n\t\tPeerId peerId,\n\t\tint albumId) const;\n\n\t[[nodiscard]] auto albumsListValue(PeerId peerId)\n\t\t-> rpl::producer<std::vector<Data::StoryAlbum>>;\n\tvoid albumCreate(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &title,\n\t\tStoryId addId,\n\t\tFn<void(StoryAlbum)> done,\n\t\tFn<void(QString)> fail);\n\tvoid albumRename(\n\t\tnot_null<PeerData*> peer,\n\t\tint id,\n\t\tconst QString &title,\n\t\tFn<void(StoryAlbum)> done,\n\t\tFn<void(QString)> fail);\n\tvoid albumDelete(not_null<PeerData*> peer, int id);\n\tvoid albumReorderStories(\n\t\tnot_null<PeerData*> peer,\n\t\tint albumId,\n\t\tint oldPosition,\n\t\tint newPosition,\n\t\tFn<void()> done,\n\t\tFn<void()> fail);\n\tvoid notifyAlbumUpdate(StoryAlbumUpdate &&update);\n\t[[nodiscard]] rpl::producer<StoryAlbumUpdate> albumUpdates() const;\n\n\tvoid deleteList(const std::vector<FullStoryId> &ids);\n\tvoid toggleInProfileList(\n\t\tconst std::vector<FullStoryId> &ids,\n\t\tbool inProfile);\n\t[[nodiscard]] bool canTogglePinnedList(\n\t\tconst std::vector<FullStoryId> &ids,\n\t\tbool pin) const;\n\t[[nodiscard]] int maxPinnedCount() const;\n\tvoid togglePinnedList(const std::vector<FullStoryId> &ids, bool pin);\n\n\tvoid incrementPreloadingMainSources();\n\tvoid decrementPreloadingMainSources();\n\tvoid incrementPreloadingHiddenSources();\n\tvoid decrementPreloadingHiddenSources();\n\tvoid setPreloadingInViewer(std::vector<FullStoryId> ids);\n\n\tstruct PeerSourceState {\n\t\tStoryId maxId = 0;\n\t\tStoryId readTill = 0;\n\t\tbool hasVideoStream = false;\n\t};\n\t[[nodiscard]] std::optional<PeerSourceState> peerSourceState(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPRecentStory &recent);\n\t[[nodiscard]] bool isUnread(not_null<Story*> story);\n\n\tenum class Polling {\n\t\tChat,\n\t\tViewer,\n\t};\n\tvoid registerPolling(not_null<Story*> story, Polling polling);\n\tvoid unregisterPolling(not_null<Story*> story, Polling polling);\n\n\t[[nodiscard]] bool registerPolling(FullStoryId id, Polling polling);\n\tvoid unregisterPolling(FullStoryId id, Polling polling);\n\tvoid requestPeerStories(\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> done = nullptr);\n\n\tvoid savedStateChanged(not_null<Story*> story);\n\t[[nodiscard]] std::shared_ptr<HistoryItem> lookupItem(\n\t\tnot_null<Story*> story);\n\n\t[[nodiscard]] StealthMode stealthMode() const;\n\t[[nodiscard]] rpl::producer<StealthMode> stealthModeValue() const;\n\tvoid activateStealthMode(Fn<void()> done = nullptr);\n\n\tvoid sendReaction(FullStoryId id, Data::ReactionId reaction);\n\nprivate:\n\tstruct Set {\n\t\tStoriesIds ids;\n\t\tbase::flat_set<StoryId> albumKnownInArchive;\n\t\tint total = -1;\n\t\tStoryId lastId = 0;\n\t\tbool loaded = false;\n\t\tmtpRequestId requestId = 0;\n\t};\n\tstruct Albums {\n\t\trpl::variable<std::vector<Data::StoryAlbum>> list;\n\t\tbase::flat_map<int, Set> sets;\n\t\tuint64 hash = 0;\n\t\tmtpRequestId requestId = 0;\n\t};\n\tstruct RecentState {\n\t\tStoryId maxId = 0;\n\t\tbool hasVideoStream = false;\n\t};\n\tstruct PollingSettings {\n\t\tint chat = 0;\n\t\tint viewer = 0;\n\t};\n\tenum class ParseSource : uchar {\n\t\tMyStrip,\n\t\tDirectRequest,\n\t};\n\n\tvoid albumIdsLoadMore(PeerId peerId, int albumId, bool reload);\n\tvoid parseAndApply(const MTPPeerStories &stories, ParseSource source);\n\t[[nodiscard]] Story *parseAndApply(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPDstoryItem &data,\n\t\tTimeId now);\n\tStoryIdDates parseAndApply(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPstoryItem &story,\n\t\tTimeId now);\n\tvoid processResolvedStories(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QVector<MTPStoryItem> &list);\n\tvoid sendResolveRequests();\n\tvoid finalizeResolve(FullStoryId id);\n\tvoid updatePeerStoriesState(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::optional<RecentState> cachedRecentState = std::nullopt);\n\n\t[[nodiscard]] Set *lookupArchive(not_null<PeerData*> peer);\n\tvoid clearArchive(not_null<PeerData*> peer);\n\n\tconst Set *albumIdsSet(PeerId peerId, int albumId) const;\n\tSet *albumIdsSet(PeerId peerId, int albumId, bool lazy = false);\n\n\tvoid applyDeleted(not_null<PeerData*> peer, StoryId id);\n\tvoid applyExpired(FullStoryId id);\n\tvoid applyRemovedFromActive(FullStoryId id);\n\tvoid applyDeletedFromSources(PeerId id, StorySourcesList list);\n\tvoid removeDependencyStory(not_null<Story*> story);\n\tvoid sort(StorySourcesList list);\n\tbool bumpReadTill(PeerId peerId, StoryId maxReadTill);\n\tvoid requestReadTills();\n\n\tvoid sendMarkAsReadRequests();\n\tvoid sendMarkAsReadRequest(not_null<PeerData*> peer, StoryId tillId);\n\tvoid sendIncrementViewsRequests();\n\tvoid checkQuitPreventFinished();\n\n\tvoid registerExpiring(TimeId expires, FullStoryId id);\n\tvoid scheduleExpireTimer();\n\tvoid processExpired();\n\n\tvoid preloadSourcesChanged(StorySourcesList list);\n\tbool rebuildPreloadSources(StorySourcesList list);\n\tvoid continuePreloading();\n\t[[nodiscard]] bool shouldContinuePreload(FullStoryId id) const;\n\t[[nodiscard]] FullStoryId nextPreloadId() const;\n\tvoid startPreloading(not_null<Story*> story);\n\tvoid preloadFinished(FullStoryId id, bool markAsPreloaded = false);\n\tvoid preloadListsMore();\n\n\tvoid notifySourcesChanged(StorySourcesList list);\n\tvoid pushHiddenCountsToFolder();\n\tvoid setPinnedToTop(\n\t\tPeerId peerId,\n\t\tstd::vector<StoryId> &&pinnedToTop);\n\n\t[[nodiscard]] int pollingInterval(\n\t\tconst PollingSettings &settings) const;\n\tvoid maybeSchedulePolling(\n\t\tnot_null<Story*> story,\n\t\tconst PollingSettings &settings,\n\t\tTimeId now);\n\tvoid sendPollingRequests();\n\tvoid sendPollingViewsRequests();\n\tvoid sendViewsSliceRequest();\n\tvoid sendViewsCountsRequest();\n\n\tvoid loadAlbums(not_null<PeerData*> peer, Albums &albums);\n\n\tconst not_null<Session*> _owner;\n\tstd::unordered_map<\n\t\tPeerId,\n\t\tbase::flat_map<StoryId, std::unique_ptr<Story>>> _stories;\n\tbase::flat_map<FullStoryId, std::unique_ptr<Story>> _deletingStories;\n\tstd::unordered_map<\n\t\tPeerId,\n\t\tbase::flat_map<StoryId, std::weak_ptr<HistoryItem>>> _items;\n\tbase::flat_multi_map<TimeId, FullStoryId> _expiring;\n\tbase::flat_set<PeerId> _peersWithDeletedStories;\n\tbase::flat_set<FullStoryId> _deleted;\n\tbase::Timer _expireTimer;\n\tbool _expireSchedulePosted = false;\n\n\tbase::flat_map<\n\t\tPeerId,\n\t\tbase::flat_map<StoryId, std::vector<Fn<void()>>>> _resolvePending;\n\tbase::flat_map<\n\t\tPeerId,\n\t\tbase::flat_map<StoryId, std::vector<Fn<void()>>>> _resolveSent;\n\n\tstd::unordered_map<\n\t\tnot_null<Data::Story*>,\n\t\tbase::flat_set<not_null<HistoryItem*>>> _dependentMessages;\n\n\tstd::unordered_map<PeerId, StoriesSource> _all;\n\tstd::vector<StoriesSourceInfo> _sources[kStorySourcesListCount];\n\trpl::event_stream<> _sourcesChanged[kStorySourcesListCount];\n\tbool _sourcesLoaded[kStorySourcesListCount] = { false };\n\tQString _sourcesStates[kStorySourcesListCount];\n\tFolder *_folderForHidden = nullptr;\n\n\tmtpRequestId _loadMoreRequestId[kStorySourcesListCount] = { 0 };\n\n\trpl::event_stream<PeerId> _sourceChanged;\n\trpl::event_stream<PeerId> _itemsChanged;\n\n\tstd::unordered_map<PeerId, Set> _archive;\n\tstd::unordered_map<PeerId, Set> _saved;\n\tstd::unordered_map<PeerId, Albums> _albums;\n\trpl::event_stream<StoryAlbumUpdate> _albumUpdates;\n\trpl::event_stream<StoryAlbumIdsKey> _albumIdsChanged;\n\n\tbase::flat_set<PeerId> _markReadPending;\n\tbase::Timer _markReadTimer;\n\tbase::flat_set<PeerId> _markReadRequests;\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tstd::vector<Fn<void()>>> _requestingPeerStories;\n\n\tbase::flat_map<PeerId, base::flat_set<StoryId>> _incrementViewsPending;\n\tbase::Timer _incrementViewsTimer;\n\tbase::flat_set<PeerId> _incrementViewsRequests;\n\n\tPeerData *_viewsStoryPeer = nullptr;\n\tStoryId _viewsStoryId = 0;\n\tQString _viewsOffset;\n\tFn<void(StoryViews)> _viewsDone;\n\tmtpRequestId _viewsRequestId = 0;\n\n\tPeerData *_reactionsStoryPeer = nullptr;\n\tStoryId _reactionsStoryId = 0;\n\tQString _reactionsOffset;\n\tFn<void(StoryViews)> _reactionsDone;\n\tmtpRequestId _reactionsRequestId = 0;\n\n\tbase::flat_set<FullStoryId> _preloaded;\n\tstd::vector<FullStoryId> _toPreloadSources[kStorySourcesListCount];\n\tstd::vector<FullStoryId> _toPreloadViewer;\n\tstd::unique_ptr<StoryPreload> _preloading;\n\tint _preloadingHiddenSourcesCounter = 0;\n\tint _preloadingMainSourcesCounter = 0;\n\n\tbase::flat_map<PeerId, StoryId> _readTill;\n\tbase::flat_set<FullStoryId> _pendingReadTillItems;\n\tbase::flat_map<not_null<PeerData*>, RecentState> _pendingPeerRecentState;\n\tmtpRequestId _readTillsRequestId = 0;\n\tbool _readTillReceived = false;\n\n\tbase::flat_map<not_null<Story*>, PollingSettings> _pollingSettings;\n\tbase::flat_set<not_null<Story*>> _pollingViews;\n\tbase::Timer _pollingTimer;\n\tbase::Timer _pollingViewsTimer;\n\n\tmtpRequestId _reorderStoriesRequestId = 0;\n\n\trpl::variable<StealthMode> _stealthMode;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_stories_ids.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_stories_ids.h\"\n\n#include \"data/data_changes.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"main/main_session.h\"\n#include \"ui/ui_utility.h\"\n\nnamespace Data {\n\nrpl::producer<StoriesIdsSlice> AlbumStoriesIds(\n\t\tnot_null<PeerData*> peer,\n\t\tint albumId,\n\t\tStoryId aroundId,\n\t\tint limit) {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tstruct State {\n\t\t\tStoriesIdsSlice slice;\n\t\t\tbase::has_weak_ptr guard;\n\t\t\tbool scheduled = false;\n\t\t};\n\t\tconst auto state = lifetime.make_state<State>();\n\n\t\tconst auto push = [=] {\n\t\t\tstate->scheduled = false;\n\n\t\t\tconst auto peerId = peer->id;\n\t\t\tconst auto stories = &peer->owner().stories();\n\t\t\tif (!stories->albumIdsCountKnown(peerId, albumId)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto &loaded = stories->albumIds(peerId, albumId);\n\t\t\tconst auto sorted = RespectingPinned(loaded);\n\t\t\tconst auto count = stories->albumIdsCount(peerId, albumId);\n\t\t\tauto i = ranges::find(sorted, aroundId);\n\t\t\tif (i == end(sorted)) {\n\t\t\t\ti = begin(sorted);\n\t\t\t}\n\t\t\tconst auto hasBefore = int(i - begin(sorted));\n\t\t\tconst auto hasAfter = int(end(sorted) - i);\n\t\t\tif (hasAfter < limit) {\n\t\t\t\tstories->albumIdsLoadMore(peerId, albumId);\n\t\t\t}\n\t\t\tconst auto takeBefore = std::min(hasBefore, limit);\n\t\t\tconst auto takeAfter = std::min(hasAfter, limit);\n\t\t\tauto ids = std::vector<StoryId>();\n\t\t\tids.reserve(takeBefore + takeAfter);\n\t\t\tfor (auto j = i - takeBefore; j != i + takeAfter; ++j) {\n\t\t\t\tids.push_back(*j);\n\t\t\t}\n\t\t\tconst auto added = int(ids.size());\n\t\t\tstate->slice = StoriesIdsSlice(\n\t\t\t\tstd::move(ids),\n\t\t\t\tcount,\n\t\t\t\t(hasBefore - takeBefore),\n\t\t\t\tcount - (hasBefore - takeBefore) - added);\n\t\t\tconsumer.put_next_copy(state->slice);\n\t\t};\n\t\tconst auto schedule = [=] {\n\t\t\tif (state->scheduled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->scheduled = true;\n\t\t\tUi::PostponeCall(&state->guard, [=] {\n\t\t\t\tif (state->scheduled) {\n\t\t\t\t\tpush();\n\t\t\t\t}\n\t\t\t});\n\t\t};\n\n\t\tconst auto peerId = peer->id;\n\t\tconst auto stories = &peer->owner().stories();\n\t\tstories->albumIdsChanged(\n\t\t) | rpl::filter(\n\t\t\trpl::mappers::_1 == Data::StoryAlbumIdsKey{ peerId, albumId }\n\t\t) | rpl::on_next(schedule, lifetime);\n\n\t\tif (!stories->albumIdsCountKnown(peerId, albumId)) {\n\t\t\tstories->albumIdsLoadMore(peerId, albumId);\n\t\t}\n\n\t\tpush();\n\n\t\treturn lifetime;\n\t};\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_stories_ids.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_abstract_sparse_ids.h\"\n\nclass PeerData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nusing StoriesIdsSlice = AbstractSparseIds<std::vector<StoryId>>;\n\n[[nodiscard]] rpl::producer<StoriesIdsSlice> AlbumStoriesIds(\n\tnot_null<PeerData*> peer,\n\tint albumId,\n\tStoryId aroundId,\n\tint limit);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_story.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_story.h\"\n\n#include \"base/unixtime.h\"\n#include \"api/api_text_entities.h\"\n#include \"data/data_document.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_media_preload.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_thread.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"media/streaming/media_streaming_reader.h\"\n#include \"storage/download_manager_mtproto.h\"\n#include \"storage/file_download.h\" // kMaxFileInMemory\n#include \"ui/text/text_utilities.h\"\n#include \"ui/color_int_conversion.h\"\n\nnamespace Data {\nnamespace {\n\nusing UpdateFlag = StoryUpdate::Flag;\n\n[[nodiscard]] StoryArea ParseArea(const MTPMediaAreaCoordinates &area) {\n\tconst auto &data = area.data();\n\tconst auto center = QPointF(data.vx().v, data.vy().v);\n\tconst auto size = QSizeF(data.vw().v, data.vh().v);\n\tconst auto corner = center - QPointF(size.width(), size.height()) / 2.;\n\treturn {\n\t\t.geometry = { corner / 100., size / 100. },\n\t\t.rotation = data.vrotation().v,\n\t\t.radius = data.vradius().value_or_empty(),\n\t};\n}\n\n[[nodiscard]] TextWithEntities StripLinks(TextWithEntities text) {\n\tconst auto link = [&](const EntityInText &entity) {\n\t\treturn (entity.type() == EntityType::CustomUrl)\n\t\t\t|| (entity.type() == EntityType::Url)\n\t\t\t|| (entity.type() == EntityType::Mention)\n\t\t\t|| (entity.type() == EntityType::Hashtag);\n\t};\n\ttext.entities.erase(\n\t\tranges::remove_if(text.entities, link),\n\t\ttext.entities.end());\n\treturn text;\n}\n\n[[nodiscard]] auto ParseLocation(const MTPMediaArea &area)\n-> std::optional<StoryLocation> {\n\tauto result = std::optional<StoryLocation>();\n\tarea.match([&](const MTPDmediaAreaVenue &data) {\n\t\tdata.vgeo().match([&](const MTPDgeoPoint &geo) {\n\t\t\tresult.emplace(StoryLocation{\n\t\t\t\t.area = ParseArea(data.vcoordinates()),\n\t\t\t\t.point = Data::LocationPoint(geo),\n\t\t\t\t.title = qs(data.vtitle()),\n\t\t\t\t.address = qs(data.vaddress()),\n\t\t\t\t.provider = qs(data.vprovider()),\n\t\t\t\t.venueId = qs(data.vvenue_id()),\n\t\t\t\t.venueType = qs(data.vvenue_type()),\n\t\t\t});\n\t\t}, [](const MTPDgeoPointEmpty &) {\n\t\t});\n\t}, [&](const MTPDmediaAreaGeoPoint &data) {\n\t\tdata.vgeo().match([&](const MTPDgeoPoint &geo) {\n\t\t\tresult.emplace(StoryLocation{\n\t\t\t\t.area = ParseArea(data.vcoordinates()),\n\t\t\t\t.point = Data::LocationPoint(geo),\n\t\t\t});\n\t\t}, [](const MTPDgeoPointEmpty &) {\n\t\t});\n\t}, [&](const MTPDmediaAreaSuggestedReaction &data) {\n\t}, [&](const MTPDmediaAreaChannelPost &data) {\n\t}, [&](const MTPDmediaAreaUrl &data) {\n\t}, [&](const MTPDmediaAreaWeather &data) {\n\t}, [&](const MTPDmediaAreaStarGift &data) {\n\t}, [&](const MTPDinputMediaAreaChannelPost &data) {\n\t\tLOG((\"API Error: Unexpected inputMediaAreaChannelPost from API.\"));\n\t}, [&](const MTPDinputMediaAreaVenue &data) {\n\t\tLOG((\"API Error: Unexpected inputMediaAreaVenue from API.\"));\n\t});\n\treturn result;\n}\n\n[[nodiscard]] auto ParseSuggestedReaction(const MTPMediaArea &area)\n-> std::optional<SuggestedReaction> {\n\tauto result = std::optional<SuggestedReaction>();\n\tarea.match([&](const MTPDmediaAreaVenue &data) {\n\t}, [&](const MTPDmediaAreaGeoPoint &data) {\n\t}, [&](const MTPDmediaAreaSuggestedReaction &data) {\n\t\tresult.emplace(SuggestedReaction{\n\t\t\t.area = ParseArea(data.vcoordinates()),\n\t\t\t.reaction = Data::ReactionFromMTP(data.vreaction()),\n\t\t\t.flipped = data.is_flipped(),\n\t\t\t.dark = data.is_dark(),\n\t\t});\n\t}, [&](const MTPDmediaAreaChannelPost &data) {\n\t}, [&](const MTPDmediaAreaUrl &data) {\n\t}, [&](const MTPDmediaAreaWeather &data) {\n\t}, [&](const MTPDmediaAreaStarGift &data) {\n\t}, [&](const MTPDinputMediaAreaChannelPost &data) {\n\t\tLOG((\"API Error: Unexpected inputMediaAreaChannelPost from API.\"));\n\t}, [&](const MTPDinputMediaAreaVenue &data) {\n\t\tLOG((\"API Error: Unexpected inputMediaAreaVenue from API.\"));\n\t});\n\treturn result;\n}\n\n[[nodiscard]] auto ParseChannelPost(const MTPMediaArea &area)\n-> std::optional<ChannelPost> {\n\tauto result = std::optional<ChannelPost>();\n\tarea.match([&](const MTPDmediaAreaVenue &data) {\n\t}, [&](const MTPDmediaAreaGeoPoint &data) {\n\t}, [&](const MTPDmediaAreaSuggestedReaction &data) {\n\t}, [&](const MTPDmediaAreaChannelPost &data) {\n\t\tresult.emplace(ChannelPost{\n\t\t\t.area = ParseArea(data.vcoordinates()),\n\t\t\t.itemId = FullMsgId(\n\t\t\t\tpeerFromChannel(data.vchannel_id()),\n\t\t\t\tdata.vmsg_id().v),\n\t\t});\n\t}, [&](const MTPDmediaAreaUrl &data) {\n\t}, [&](const MTPDmediaAreaWeather &data) {\n\t}, [&](const MTPDmediaAreaStarGift &data) {\n\t}, [&](const MTPDinputMediaAreaChannelPost &data) {\n\t\tLOG((\"API Error: Unexpected inputMediaAreaChannelPost from API.\"));\n\t}, [&](const MTPDinputMediaAreaVenue &data) {\n\t\tLOG((\"API Error: Unexpected inputMediaAreaVenue from API.\"));\n\t});\n\treturn result;\n}\n\n[[nodiscard]] auto ParseUrlArea(const MTPMediaArea &area)\n-> std::optional<UrlArea> {\n\tauto result = std::optional<UrlArea>();\n\tarea.match([&](const MTPDmediaAreaVenue &data) {\n\t}, [&](const MTPDmediaAreaGeoPoint &data) {\n\t}, [&](const MTPDmediaAreaSuggestedReaction &data) {\n\t}, [&](const MTPDmediaAreaChannelPost &data) {\n\t}, [&](const MTPDmediaAreaUrl &data) {\n\t\tresult.emplace(UrlArea{\n\t\t\t.area = ParseArea(data.vcoordinates()),\n\t\t\t.url = qs(data.vurl()),\n\t\t});\n\t}, [&](const MTPDmediaAreaWeather &data) {\n\t}, [&](const MTPDmediaAreaStarGift &data) {\n\t\tresult.emplace(UrlArea{\n\t\t\t.area = ParseArea(data.vcoordinates()),\n\t\t\t.url = u\"tg://nft?slug=\"_q + qs(data.vslug()),\n\t\t});\n\t}, [&](const MTPDinputMediaAreaChannelPost &data) {\n\t\tLOG((\"API Error: Unexpected inputMediaAreaChannelPost from API.\"));\n\t}, [&](const MTPDinputMediaAreaVenue &data) {\n\t\tLOG((\"API Error: Unexpected inputMediaAreaVenue from API.\"));\n\t});\n\treturn result;\n}\n\n[[nodiscard]] auto ParseWeatherArea(const MTPMediaArea &area)\n-> std::optional<WeatherArea> {\n\tauto result = std::optional<WeatherArea>();\n\tarea.match([&](const MTPDmediaAreaVenue &data) {\n\t}, [&](const MTPDmediaAreaGeoPoint &data) {\n\t}, [&](const MTPDmediaAreaSuggestedReaction &data) {\n\t}, [&](const MTPDmediaAreaChannelPost &data) {\n\t}, [&](const MTPDmediaAreaUrl &data) {\n\t}, [&](const MTPDmediaAreaWeather &data) {\n\t\tresult.emplace(WeatherArea{\n\t\t\t.area = ParseArea(data.vcoordinates()),\n\t\t\t.emoji = qs(data.vemoji()),\n\t\t\t.color = Ui::Color32FromSerialized(data.vcolor().v),\n\t\t\t.millicelsius = int(1000. * std::clamp(\n\t\t\t\tdata.vtemperature_c().v,\n\t\t\t\t-274.,\n\t\t\t\t1'000'000.)),\n\t\t});\n\t}, [&](const MTPDmediaAreaStarGift &data) {\n\t}, [&](const MTPDinputMediaAreaChannelPost &data) {\n\t\tLOG((\"API Error: Unexpected inputMediaAreaChannelPost from API.\"));\n\t}, [&](const MTPDinputMediaAreaVenue &data) {\n\t\tLOG((\"API Error: Unexpected inputMediaAreaVenue from API.\"));\n\t});\n\treturn result;\n}\n\n[[nodiscard]] PeerData *RepostSourcePeer(\n\t\tnot_null<Session*> owner,\n\t\tconst MTPDstoryItem &data) {\n\tif (const auto forwarded = data.vfwd_from()) {\n\t\tif (const auto from = forwarded->data().vfrom()) {\n\t\t\treturn owner->peer(peerFromMTP(*from));\n\t\t}\n\t}\n\treturn nullptr;\n}\n\n[[nodiscard]] QString RepostSourceName(const MTPDstoryItem &data) {\n\tif (const auto forwarded = data.vfwd_from()) {\n\t\treturn qs(forwarded->data().vfrom_name().value_or_empty());\n\t}\n\treturn {};\n}\n\n[[nodiscard]] StoryId RepostSourceId(const MTPDstoryItem &data) {\n\tif (const auto forwarded = data.vfwd_from()) {\n\t\treturn forwarded->data().vstory_id().value_or_empty();\n\t}\n\treturn {};\n}\n\n[[nodiscard]] bool RepostModified(const MTPDstoryItem &data) {\n\tif (const auto forwarded = data.vfwd_from()) {\n\t\treturn forwarded->data().is_modified();\n\t}\n\treturn false;\n}\n\n[[nodiscard]] PeerData *FromPeer(\n\t\tnot_null<Session*> owner,\n\t\tconst MTPDstoryItem &data) {\n\tif (const auto from = data.vfrom_id()) {\n\t\treturn owner->peer(peerFromMTP(*from));\n\t}\n\treturn nullptr;\n}\n\n} // namespace\n\nStory::Story(\n\tStoryId id,\n\tnot_null<PeerData*> peer,\n\tStoryMedia media,\n\tconst MTPDstoryItem &data,\n\tTimeId now)\n: _id(id)\n, _peer(peer)\n, _repostSourcePeer(RepostSourcePeer(&peer->owner(), data))\n, _repostSourceName(RepostSourceName(data))\n, _repostSourceId(RepostSourceId(data))\n, _fromPeer(FromPeer(&peer->owner(), data))\n, _date(data.vdate().v)\n, _expires(data.vexpire_date().v)\n, _repostModified(RepostModified(data)) {\n\tapplyFields(std::move(media), data, now, true);\n}\n\nSession &Story::owner() const {\n\treturn _peer->owner();\n}\n\nMain::Session &Story::session() const {\n\treturn _peer->session();\n}\n\nnot_null<PeerData*> Story::peer() const {\n\treturn _peer;\n}\n\nStoryId Story::id() const {\n\treturn _id;\n}\n\nbool Story::mine() const {\n\treturn _peer->isSelf();\n}\n\nStoryIdDates Story::idDates() const {\n\treturn {\n\t\t_id,\n\t\t_date,\n\t\t_expires,\n\t\tstd::holds_alternative<std::shared_ptr<GroupCall>>(_media.data),\n\t};\n}\n\nFullStoryId Story::fullId() const {\n\treturn { _peer->id, _id };\n}\n\nTimeId Story::date() const {\n\treturn _date;\n}\n\nTimeId Story::expires() const {\n\treturn _expires;\n}\n\nbool Story::expired(TimeId now) const {\n\treturn _expires <= (now ? now : base::unixtime::now());\n}\n\nbool Story::unsupported() const {\n\treturn v::is_null(_media.data);\n}\n\nconst StoryMedia &Story::media() const {\n\treturn _media;\n}\n\nPhotoData *Story::photo() const {\n\tconst auto result = std::get_if<not_null<PhotoData*>>(&_media.data);\n\treturn result ? result->get() : nullptr;\n}\n\nDocumentData *Story::document() const {\n\tconst auto result = std::get_if<not_null<DocumentData*>>(&_media.data);\n\treturn result ? result->get() : nullptr;\n}\n\nconst std::shared_ptr<GroupCall> &Story::call() const {\n\tconst auto result = std::get_if<std::shared_ptr<GroupCall>>(\n\t\t&_media.data);\n\tstatic const auto empty = std::shared_ptr<GroupCall>();\n\treturn result ? *result : empty;\n}\n\nbool Story::hasReplyPreview() const {\n\treturn v::match(_media.data, [](not_null<PhotoData*> photo) {\n\t\treturn !photo->isNull();\n\t}, [](not_null<DocumentData*> document) {\n\t\treturn document->hasThumbnail();\n\t}, [](const std::shared_ptr<GroupCall> &call) {\n\t\treturn false;\n\t}, [](v::null_t) {\n\t\treturn false;\n\t});\n}\n\nImage *Story::replyPreview() const {\n\treturn v::match(_media.data, [&](not_null<PhotoData*> photo) {\n\t\treturn photo->getReplyPreview(fullId(), _peer, false);\n\t}, [&](not_null<DocumentData*> document) {\n\t\treturn document->getReplyPreview(fullId(), _peer, false);\n\t}, [](const std::shared_ptr<GroupCall> &call) {\n\t\treturn (Image*)nullptr;\n\t}, [](v::null_t) {\n\t\treturn (Image*)nullptr;\n\t});\n}\n\nTextWithEntities Story::inReplyText() const {\n\tconst auto type = tr::lng_in_dlg_story(tr::now);\n\treturn _caption.text.isEmpty()\n\t\t? Ui::Text::Colorized(type)\n\t\t: tr::lng_dialogs_text_media(\n\t\t\ttr::now,\n\t\t\tlt_media_part,\n\t\t\ttr::lng_dialogs_text_media_wrapped(\n\t\t\t\ttr::now,\n\t\t\t\tlt_media,\n\t\t\t\tUi::Text::Colorized(type),\n\t\t\t\ttr::marked),\n\t\t\tlt_caption,\n\t\t\t_caption,\n\t\t\ttr::marked);\n}\n\nvoid Story::setPinnedToTop(bool pinned) {\n\tif (_pinnedToTop != pinned) {\n\t\t_pinnedToTop = pinned;\n\t\tif (const auto item = _peer->owner().stories().lookupItem(this)) {\n\t\t\titem->setIsPinned(pinned);\n\t\t}\n\t}\n}\n\nbool Story::pinnedToTop() const {\n\treturn _pinnedToTop;\n}\n\nvoid Story::setInProfile(bool value) {\n\tif (_inProfile != value) {\n\t\t_inProfile = value;\n\t\tif (const auto item = _peer->owner().stories().lookupItem(this)) {\n\t\t\titem->setStoryInProfile(value);\n\t\t}\n\t}\n}\n\nbool Story::inProfile() const {\n\treturn _inProfile;\n}\n\nStoryPrivacy Story::privacy() const {\n\treturn _privacyPublic\n\t\t? StoryPrivacy::Public\n\t\t: _privacyCloseFriends\n\t\t? StoryPrivacy::CloseFriends\n\t\t: _privacyContacts\n\t\t? StoryPrivacy::Contacts\n\t\t: _privacySelectedContacts\n\t\t? StoryPrivacy::SelectedContacts\n\t\t: StoryPrivacy::Other;\n}\n\nbool Story::forbidsForward() const {\n\treturn _noForwards;\n}\n\nbool Story::edited() const {\n\treturn _edited;\n}\n\nbool Story::out() const {\n\treturn _out;\n}\n\nbool Story::canDownloadIfPremium() const {\n\treturn !forbidsForward() || _peer->isSelf();\n}\n\nbool Story::canDownloadChecked() const {\n\treturn _peer->isSelf()\n\t\t|| (canDownloadIfPremium() && _peer->session().premium());\n}\n\nbool Story::canShare() const {\n\treturn _privacyPublic\n\t\t&& !forbidsForward()\n\t\t&& (inProfile() || !expired());\n}\n\nbool Story::canDelete() const {\n\treturn _peer->canDeleteStories() || (out() && _peer->canPostStories());\n}\n\nbool Story::canReport() const {\n\treturn !_peer->isSelf();\n}\n\nbool Story::hasDirectLink() const {\n\tif (!_privacyPublic || (!_inProfile && expired())) {\n\t\treturn false;\n\t}\n\treturn !_peer->username().isEmpty();\n}\n\nData::SendError Story::errorTextForForward(\n\t\tnot_null<Thread*> to) const {\n\tconst auto peer = to->peer();\n\tconst auto holdsPhoto = v::is<not_null<PhotoData*>>(_media.data);\n\tconst auto first = holdsPhoto\n\t\t? ChatRestriction::SendPhotos\n\t\t: ChatRestriction::SendVideos;\n\tconst auto second = holdsPhoto\n\t\t? ChatRestriction::SendVideos\n\t\t: ChatRestriction::SendPhotos;\n\tif (const auto one = Data::RestrictionError(peer, first)) {\n\t\treturn one;\n\t} else if (const auto two = Data::RestrictionError(peer, second)) {\n\t\treturn two;\n\t} else if (!Data::CanSend(to, first, false)\n\t\t|| !Data::CanSend(to, second, false)) {\n\t\treturn tr::lng_forward_cant(tr::now);\n\t}\n\treturn {};\n}\n\nvoid Story::setCaption(TextWithEntities &&caption) {\n\t_caption = std::move(caption);\n}\n\nconst TextWithEntities &Story::caption() const {\n\tstatic const auto empty = TextWithEntities();\n\treturn unsupported() ? empty : _caption;\n}\n\nData::ReactionId Story::sentReactionId() const {\n\treturn _sentReactionId;\n}\n\nvoid Story::setReactionId(Data::ReactionId id) {\n\tif (_sentReactionId != id) {\n\t\tconst auto wasEmpty = _sentReactionId.empty();\n\t\tchangeSuggestedReactionCount(_sentReactionId, -1);\n\t\t_sentReactionId = id;\n\t\tchangeSuggestedReactionCount(id, 1);\n\n\t\tif (_views.known && _sentReactionId.empty() != wasEmpty) {\n\t\t\tconst auto delta = wasEmpty ? 1 : -1;\n\t\t\tif (_views.reactions + delta >= 0) {\n\t\t\t\t_views.reactions += delta;\n\t\t\t}\n\t\t}\n\t\tsession().changes().storyUpdated(this, UpdateFlag::Reaction);\n\t}\n}\n\nvoid Story::changeSuggestedReactionCount(Data::ReactionId id, int delta) {\n\tif (id.empty() || !_peer->isChannel()) {\n\t\treturn;\n\t}\n\tfor (auto &suggested : _suggestedReactions) {\n\t\tif (suggested.reaction == id && suggested.count + delta >= 0) {\n\t\t\tsuggested.count += delta;\n\t\t}\n\t}\n}\n\nconst std::vector<not_null<PeerData*>> &Story::recentViewers() const {\n\treturn _recentViewers;\n}\n\nconst StoryViews &Story::viewsList() const {\n\treturn _views;\n}\n\nconst StoryViews &Story::channelReactionsList() const {\n\treturn _channelReactions;\n}\n\nint Story::interactions() const {\n\treturn _views.total;\n}\n\nint Story::views() const {\n\treturn _views.views;\n}\n\nint Story::forwards() const {\n\treturn _views.forwards;\n}\n\nint Story::reactions() const {\n\treturn _views.reactions;\n}\n\nvoid Story::applyViewsSlice(\n\t\tconst QString &offset,\n\t\tconst StoryViews &slice) {\n\tconst auto changed = (_views.reactions != slice.reactions)\n\t\t|| (_views.views != slice.views)\n\t\t|| (_views.forwards != slice.forwards)\n\t\t|| (_views.total != slice.total);\n\t_views.reactions = slice.reactions;\n\t_views.forwards = slice.forwards;\n\t_views.views = slice.views;\n\t_views.total = slice.total;\n\t_views.known = true;\n\tif (offset.isEmpty()) {\n\t\t_views = slice;\n\t\tif (!_channelReactions.total) {\n\t\t\t_channelReactions.total = _views.reactions + _views.forwards;\n\t\t}\n\t} else if (_views.nextOffset == offset) {\n\t\t_views.list.insert(\n\t\t\tend(_views.list),\n\t\t\tbegin(slice.list),\n\t\t\tend(slice.list));\n\t\t_views.nextOffset = slice.nextOffset;\n\t\tif (_views.nextOffset.isEmpty()) {\n\t\t\t_views.total = int(_views.list.size());\n\t\t\t_views.reactions = _views.total\n\t\t\t\t- ranges::count(\n\t\t\t\t\t_views.list,\n\t\t\t\t\tData::ReactionId(),\n\t\t\t\t\t&StoryView::reaction);\n\t\t\t_views.forwards = _views.total\n\t\t\t\t- ranges::count(\n\t\t\t\t\t_views.list,\n\t\t\t\t\t0,\n\t\t\t\t\t[](const StoryView &view) {\n\t\t\t\t\t\treturn view.repostId\n\t\t\t\t\t\t\t? view.repostId\n\t\t\t\t\t\t\t: view.forwardId.bare;\n\t\t\t\t\t});\n\t\t}\n\t}\n\tconst auto known = int(_views.list.size());\n\tif (known >= _recentViewers.size()) {\n\t\tconst auto take = std::min(known, kRecentViewersMax);\n\t\tauto viewers = _views.list\n\t\t\t| ranges::views::take(take)\n\t\t\t| ranges::views::transform(&StoryView::peer)\n\t\t\t| ranges::to_vector;\n\t\tif (_recentViewers != viewers) {\n\t\t\t_recentViewers = std::move(viewers);\n\t\t\tif (!changed) {\n\t\t\t\t// Count not changed, but list of recent viewers changed.\n\t\t\t\t_peer->session().changes().storyUpdated(\n\t\t\t\t\tthis,\n\t\t\t\t\tUpdateFlag::ViewsChanged);\n\t\t\t}\n\t\t}\n\t}\n\tif (changed) {\n\t\t_peer->session().changes().storyUpdated(\n\t\t\tthis,\n\t\t\tUpdateFlag::ViewsChanged);\n\t}\n}\n\nvoid Story::applyChannelReactionsSlice(\n\t\tconst QString &offset,\n\t\tconst StoryViews &slice) {\n\tconst auto changed = (_channelReactions.reactions != slice.reactions)\n\t\t|| (_channelReactions.total != slice.total);\n\t_channelReactions.reactions = slice.reactions;\n\t_channelReactions.total = slice.total;\n\t_channelReactions.known = true;\n\tif (offset.isEmpty()) {\n\t\t_channelReactions = slice;\n\t} else if (_channelReactions.nextOffset == offset) {\n\t\t_channelReactions.list.insert(\n\t\t\tend(_channelReactions.list),\n\t\t\tbegin(slice.list),\n\t\t\tend(slice.list));\n\t\t_channelReactions.nextOffset = slice.nextOffset;\n\t\tif (_channelReactions.nextOffset.isEmpty()) {\n\t\t\t_channelReactions.total = int(_channelReactions.list.size());\n\t\t}\n\t}\n\tif (changed) {\n\t\t_peer->session().changes().storyUpdated(\n\t\t\tthis,\n\t\t\tUpdateFlag::ViewsChanged);\n\t}\n}\n\nconst std::vector<StoryLocation> &Story::locations() const {\n\treturn _locations;\n}\n\nconst std::vector<SuggestedReaction> &Story::suggestedReactions() const {\n\treturn _suggestedReactions;\n}\n\nconst std::vector<ChannelPost> &Story::channelPosts() const {\n\treturn _channelPosts;\n}\n\nconst std::vector<UrlArea> &Story::urlAreas() const {\n\treturn _urlAreas;\n}\n\nconst std::vector<WeatherArea> &Story::weatherAreas() const {\n\treturn _weatherAreas;\n}\n\nvoid Story::applyChanges(\n\t\tStoryMedia media,\n\t\tconst MTPDstoryItem &data,\n\t\tTimeId now) {\n\tapplyFields(std::move(media), data, now, false);\n}\n\nStory::ViewsCounts Story::parseViewsCounts(\n\t\tconst MTPDstoryViews &data,\n\t\tconst Data::ReactionId &mine) {\n\tauto result = ViewsCounts{\n\t\t.views = data.vviews_count().v,\n\t\t.forwards = data.vforwards_count().value_or_empty(),\n\t\t.reactions = data.vreactions_count().value_or_empty(),\n\t};\n\tif (const auto list = data.vrecent_viewers()) {\n\t\tresult.viewers.reserve(list->v.size());\n\t\tauto &owner = _peer->owner();\n\t\tauto &&cut = list->v\n\t\t\t| ranges::views::take(kRecentViewersMax);\n\t\tfor (const auto &id : cut) {\n\t\t\tresult.viewers.push_back(owner.peer(peerFromUser(id)));\n\t\t}\n\t}\n\tauto total = 0;\n\tif (const auto list = data.vreactions()\n\t\t; list && _peer->isChannel()) {\n\t\tresult.reactionsCounts.reserve(list->v.size());\n\t\tfor (const auto &reaction : list->v) {\n\t\t\tconst auto &data = reaction.data();\n\t\t\tconst auto id = Data::ReactionFromMTP(data.vreaction());\n\t\t\tconst auto count = data.vcount().v;\n\t\t\tresult.reactionsCounts[id] = count;\n\t\t\ttotal += count;\n\t\t}\n\t}\n\tif (!mine.empty()) {\n\t\tif (auto &count = result.reactionsCounts[mine]; !count) {\n\t\t\tcount = 1;\n\t\t\t++total;\n\t\t}\n\t}\n\tif (result.reactions < total) {\n\t\tresult.reactions = total;\n\t}\n\treturn result;\n}\n\nvoid Story::applyFields(\n\t\tStoryMedia media,\n\t\tconst MTPDstoryItem &data,\n\t\tTimeId now,\n\t\tbool initial) {\n\t_lastUpdateTime = now;\n\n\tconst auto reaction = data.is_min()\n\t\t? _sentReactionId\n\t\t: data.vsent_reaction()\n\t\t? Data::ReactionFromMTP(*data.vsent_reaction())\n\t\t: Data::ReactionId();\n\tconst auto inProfile = data.is_pinned();\n\tconst auto edited = data.is_edited();\n\tconst auto privacy = data.is_public()\n\t\t? StoryPrivacy::Public\n\t\t: data.is_close_friends()\n\t\t? StoryPrivacy::CloseFriends\n\t\t: data.is_contacts()\n\t\t? StoryPrivacy::Contacts\n\t\t: data.is_selected_contacts()\n\t\t? StoryPrivacy::SelectedContacts\n\t\t: StoryPrivacy::Other;\n\tconst auto noForwards = data.is_noforwards();\n\tconst auto out = data.is_min() ? _out : data.is_out();\n\tauto caption = TextWithEntities{\n\t\tdata.vcaption().value_or_empty(),\n\t\tApi::EntitiesFromMTP(\n\t\t\t&session(),\n\t\t\tdata.ventities().value_or_empty()),\n\t};\n\tif (const auto user = _peer->asUser()) {\n\t\tif (!user->isVerified() && !user->isPremium()) {\n\t\t\tcaption = StripLinks(std::move(caption));\n\t\t}\n\t}\n\tauto counts = ViewsCounts();\n\tauto viewsKnown = _views.known;\n\tif (const auto info = data.vviews()) {\n\t\tcounts = parseViewsCounts(info->data(), reaction);\n\t\tviewsKnown = true;\n\t} else {\n\t\tcounts.views = _views.total;\n\t\tcounts.forwards = _views.forwards;\n\t\tcounts.reactions = _views.reactions;\n\t\tcounts.viewers = _recentViewers;\n\t\tfor (const auto &suggested : _suggestedReactions) {\n\t\t\tif (const auto count = suggested.count) {\n\t\t\t\tcounts.reactionsCounts[suggested.reaction] = count;\n\t\t\t}\n\t\t}\n\t}\n\tauto locations = std::vector<StoryLocation>();\n\tauto suggestedReactions = std::vector<SuggestedReaction>();\n\tauto channelPosts = std::vector<ChannelPost>();\n\tauto urlAreas = std::vector<UrlArea>();\n\tauto weatherAreas = std::vector<WeatherArea>();\n\tif (const auto areas = data.vmedia_areas()) {\n\t\tfor (const auto &area : areas->v) {\n\t\t\tif (const auto location = ParseLocation(area)) {\n\t\t\t\tlocations.push_back(*location);\n\t\t\t} else if (auto reaction = ParseSuggestedReaction(area)) {\n\t\t\t\tconst auto i = counts.reactionsCounts.find(\n\t\t\t\t\treaction->reaction);\n\t\t\t\tif (i != end(counts.reactionsCounts)) {\n\t\t\t\t\treaction->count = i->second;\n\t\t\t\t}\n\t\t\t\tsuggestedReactions.push_back(*reaction);\n\t\t\t} else if (auto post = ParseChannelPost(area)) {\n\t\t\t\tchannelPosts.push_back(*post);\n\t\t\t} else if (auto url = ParseUrlArea(area)) {\n\t\t\t\turlAreas.push_back(*url);\n\t\t\t} else if (auto weather = ParseWeatherArea(area)) {\n\t\t\t\tweatherAreas.push_back(*weather);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst auto inProfileChanged = (_inProfile != inProfile);\n\tconst auto editedChanged = (_edited != edited);\n\tconst auto mediaChanged = (_media != media);\n\tconst auto captionChanged = (_caption != caption);\n\tconst auto locationsChanged = (_locations != locations);\n\tconst auto suggestedReactionsChanged\n\t\t= (_suggestedReactions != suggestedReactions);\n\tconst auto channelPostsChanged = (_channelPosts != channelPosts);\n\tconst auto urlAreasChanged = (_urlAreas != urlAreas);\n\tconst auto weatherAreasChanged = (_weatherAreas != weatherAreas);\n\tconst auto reactionChanged = (_sentReactionId != reaction);\n\n\t_out = out;\n\t_privacyPublic = (privacy == StoryPrivacy::Public);\n\t_privacyCloseFriends = (privacy == StoryPrivacy::CloseFriends);\n\t_privacyContacts = (privacy == StoryPrivacy::Contacts);\n\t_privacySelectedContacts = (privacy == StoryPrivacy::SelectedContacts);\n\t_edited = edited;\n\t_inProfile = inProfile;\n\t_noForwards = noForwards;\n\tif (mediaChanged) {\n\t\t_media = std::move(media);\n\t}\n\tif (captionChanged) {\n\t\t_caption = std::move(caption);\n\t}\n\tif (locationsChanged) {\n\t\t_locations = std::move(locations);\n\t}\n\tif (suggestedReactionsChanged) {\n\t\t_suggestedReactions = std::move(suggestedReactions);\n\t}\n\tif (channelPostsChanged) {\n\t\t_channelPosts = std::move(channelPosts);\n\t}\n\tif (urlAreasChanged) {\n\t\t_urlAreas = std::move(urlAreas);\n\t}\n\tif (weatherAreasChanged) {\n\t\t_weatherAreas = std::move(weatherAreas);\n\t}\n\tif (reactionChanged) {\n\t\t_sentReactionId = reaction;\n\t}\n\tupdateViewsCounts(std::move(counts), viewsKnown, initial);\n\n\tconst auto changed = editedChanged\n\t\t|| captionChanged\n\t\t|| mediaChanged\n\t\t|| locationsChanged\n\t\t|| channelPostsChanged\n\t\t|| urlAreasChanged\n\t\t|| weatherAreasChanged;\n\tconst auto reactionsChanged = reactionChanged\n\t\t|| suggestedReactionsChanged;\n\tif (!initial && (changed || reactionsChanged)) {\n\t\t_peer->session().changes().storyUpdated(this, UpdateFlag()\n\t\t\t| (changed ? UpdateFlag::Edited : UpdateFlag())\n\t\t\t| (reactionsChanged ? UpdateFlag::Reaction : UpdateFlag()));\n\t}\n\tif (!initial && (captionChanged || mediaChanged)) {\n\t\tif (const auto item = _peer->owner().stories().lookupItem(this)) {\n\t\t\titem->applyChanges(this);\n\t\t}\n\t\t_peer->owner().refreshStoryItemViews(fullId());\n\t}\n\tif (inProfileChanged) {\n\t\t_peer->owner().stories().savedStateChanged(this);\n\t}\n}\n\nvoid Story::updateViewsCounts(ViewsCounts &&counts, bool known, bool initial) {\n\tconst auto total = _views.total\n\t\t? _views.total\n\t\t: (counts.views + counts.forwards);\n\tconst auto viewsChanged = (_views.total != total)\n\t\t|| (_views.forwards != counts.forwards)\n\t\t|| (_views.reactions != counts.reactions)\n\t\t|| (_recentViewers != counts.viewers);\n\tif (_views.reactions != counts.reactions\n\t\t|| _views.forwards != counts.forwards\n\t\t|| _views.total != total\n\t\t|| _views.known != known) {\n\t\t_views = StoryViews{\n\t\t\t.reactions = counts.reactions,\n\t\t\t.forwards = counts.forwards,\n\t\t\t.views = counts.views,\n\t\t\t.total = total,\n\t\t\t.known = known,\n\t\t};\n\t\tif (!_channelReactions.total) {\n\t\t\t_channelReactions.total = _views.reactions + _views.forwards;\n\t\t}\n\t}\n\tif (viewsChanged) {\n\t\t_recentViewers = std::move(counts.viewers);\n\t\t_peer->session().changes().storyUpdated(\n\t\t\tthis,\n\t\t\tUpdateFlag::ViewsChanged);\n\t}\n}\n\nvoid Story::applyViewsCounts(const MTPDstoryViews &data) {\n\tauto counts = parseViewsCounts(data, _sentReactionId);\n\tauto suggestedCountsChanged = false;\n\tfor (auto &suggested : _suggestedReactions) {\n\t\tconst auto i = counts.reactionsCounts.find(suggested.reaction);\n\t\tconst auto v = (i != end(counts.reactionsCounts)) ? i->second : 0;\n\t\tif (suggested.count != v) {\n\t\t\tsuggested.count = v;\n\t\t\tsuggestedCountsChanged = true;\n\t\t}\n\t}\n\tupdateViewsCounts(std::move(counts), true, false);\n\tif (suggestedCountsChanged) {\n\t\t_peer->session().changes().storyUpdated(this, UpdateFlag::Reaction);\n\t}\n}\n\nTimeId Story::lastUpdateTime() const {\n\treturn _lastUpdateTime;\n}\n\nbool Story::repost() const {\n\treturn _repostSourcePeer || !_repostSourceName.isEmpty();\n}\n\nbool Story::repostModified() const {\n\treturn _repostModified;\n}\n\nPeerData *Story::repostSourcePeer() const {\n\treturn _repostSourcePeer;\n}\n\nQString Story::repostSourceName() const {\n\treturn _repostSourceName;\n}\n\nStoryId Story::repostSourceId() const {\n\treturn _repostSourceId;\n}\n\nconst base::flat_set<int> &Story::albumIds() const {\n\treturn _albumIds;\n}\n\nvoid Story::setAlbumIds(base::flat_set<int> ids) {\n\t_albumIds = std::move(ids);\n}\n\nPeerData *Story::fromPeer() const {\n\treturn _fromPeer;\n}\n\nStoryPreload::StoryPreload(not_null<Story*> story, Fn<void()> done)\n: _story(story) {\n\tif (const auto photo = _story->photo()) {\n\t\tif (PhotoPreload::Should(photo, story->peer())) {\n\t\t\t_task = std::make_unique<PhotoPreload>(\n\t\t\t\tphoto,\n\t\t\t\tstory->fullId(),\n\t\t\t\tstd::move(done));\n\t\t} else {\n\t\t\tdone();\n\t\t}\n\t} else if (const auto video = _story->document()) {\n\t\tif (VideoPreload::Can(video)) {\n\t\t\t_task = std::make_unique<VideoPreload>(\n\t\t\t\tvideo,\n\t\t\t\tstory->fullId(),\n\t\t\t\tstd::move(done));\n\t\t} else {\n\t\t\tdone();\n\t\t}\n\t} else {\n\t\tdone();\n\t}\n}\n\nStoryPreload::~StoryPreload() = default;\n\nFullStoryId StoryPreload::id() const {\n\treturn _story->fullId();\n}\n\nnot_null<Story*> StoryPreload::story() const {\n\treturn _story;\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_story.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"data/data_location.h\"\n#include \"data/data_message_reaction_id.h\"\n\nclass Image;\nclass PhotoData;\nclass DocumentData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nclass GroupCall;\nclass Session;\nclass Thread;\nclass MediaPreload;\nstruct SendError;\n\nenum class StoryPrivacy : uchar {\n\tPublic,\n\tCloseFriends,\n\tContacts,\n\tSelectedContacts,\n\tOther,\n};\n\nstruct StoryIdDates {\n\tStoryId id = 0;\n\tTimeId date = 0;\n\tTimeId expires = 0;\n\tbool videoStream = false;\n\n\t[[nodiscard]] bool valid() const {\n\t\treturn id != 0;\n\t}\n\texplicit operator bool() const {\n\t\treturn valid();\n\t}\n\n\tfriend inline auto operator<=>(StoryIdDates, StoryIdDates) = default;\n\tfriend inline bool operator==(StoryIdDates, StoryIdDates) = default;\n};\n\nstruct StoryMedia {\n\tstd::variant<\n\t\tv::null_t,\n\t\tnot_null<PhotoData*>,\n\t\tnot_null<DocumentData*>,\n\t\tstd::shared_ptr<GroupCall>> data;\n\n\tfriend inline bool operator==(StoryMedia, StoryMedia) = default;\n};\n\nstruct StoryView {\n\tnot_null<PeerData*> peer;\n\tData::ReactionId reaction;\n\tStoryId repostId = 0;\n\tMsgId forwardId = 0;\n\tTimeId date = 0;\n\n\tfriend inline bool operator==(StoryView, StoryView) = default;\n};\n\nstruct StoryViews {\n\tstd::vector<StoryView> list;\n\tQString nextOffset;\n\tint reactions = 0;\n\tint forwards = 0;\n\tint views = 0;\n\tint total = 0;\n\tbool known = false;\n};\n\nstruct StoryArea {\n\tQRectF geometry;\n\tfloat64 rotation = 0;\n\tfloat64 radius = 0;\n\n\tfriend inline bool operator==(\n\t\tconst StoryArea &,\n\t\tconst StoryArea &) = default;\n};\n\nstruct StoryLocation {\n\tStoryArea area;\n\tData::LocationPoint point;\n\tQString title;\n\tQString address;\n\tQString provider;\n\tQString venueId;\n\tQString venueType;\n\n\tfriend inline bool operator==(\n\t\tconst StoryLocation &,\n\t\tconst StoryLocation &) = default;\n};\n\nstruct SuggestedReaction {\n\tStoryArea area;\n\tData::ReactionId reaction;\n\tint count = 0;\n\tbool flipped = false;\n\tbool dark = false;\n\n\tfriend inline bool operator==(\n\t\tconst SuggestedReaction &,\n\t\tconst SuggestedReaction &) = default;\n};\n\nstruct ChannelPost {\n\tStoryArea area;\n\tFullMsgId itemId;\n\n\tfriend inline bool operator==(\n\t\tconst ChannelPost &,\n\t\tconst ChannelPost &) = default;\n};\n\nstruct UrlArea {\n\tStoryArea area;\n\tQString url;\n\n\tfriend inline bool operator==(\n\t\tconst UrlArea &,\n\t\tconst UrlArea &) = default;\n};\n\nstruct WeatherArea {\n\tStoryArea area;\n\tQString emoji;\n\tQColor color;\n\tint millicelsius = 0;\n\n\tfriend inline bool operator==(\n\t\tconst WeatherArea &,\n\t\tconst WeatherArea &) = default;\n};\n\nclass Story final {\npublic:\n\tStory(\n\t\tStoryId id,\n\t\tnot_null<PeerData*> peer,\n\t\tStoryMedia media,\n\t\tconst MTPDstoryItem &data,\n\t\tTimeId now);\n\n\tstatic constexpr int kRecentViewersMax = 3;\n\n\t[[nodiscard]] Session &owner() const;\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\n\t[[nodiscard]] StoryId id() const;\n\t[[nodiscard]] bool mine() const;\n\t[[nodiscard]] StoryIdDates idDates() const;\n\t[[nodiscard]] FullStoryId fullId() const;\n\t[[nodiscard]] TimeId date() const;\n\t[[nodiscard]] TimeId expires() const;\n\t[[nodiscard]] bool unsupported() const;\n\t[[nodiscard]] bool expired(TimeId now = 0) const;\n\t[[nodiscard]] const StoryMedia &media() const;\n\t[[nodiscard]] PhotoData *photo() const;\n\t[[nodiscard]] DocumentData *document() const;\n\t[[nodiscard]] const std::shared_ptr<GroupCall> &call() const;\n\n\t[[nodiscard]] bool hasReplyPreview() const;\n\t[[nodiscard]] Image *replyPreview() const;\n\t[[nodiscard]] TextWithEntities inReplyText() const;\n\n\tvoid setPinnedToTop(bool pinned);\n\tbool pinnedToTop() const;\n\n\tvoid setInProfile(bool value);\n\t[[nodiscard]] bool inProfile() const;\n\t[[nodiscard]] StoryPrivacy privacy() const;\n\t[[nodiscard]] bool forbidsForward() const;\n\t[[nodiscard]] bool edited() const;\n\t[[nodiscard]] bool out() const;\n\n\t[[nodiscard]] bool canDownloadIfPremium() const;\n\t[[nodiscard]] bool canDownloadChecked() const;\n\t[[nodiscard]] bool canShare() const;\n\t[[nodiscard]] bool canDelete() const;\n\t[[nodiscard]] bool canReport() const;\n\n\t[[nodiscard]] bool hasDirectLink() const;\n\t[[nodiscard]] Data::SendError errorTextForForward(\n\t\tnot_null<Thread*> to) const;\n\n\tvoid setCaption(TextWithEntities &&caption);\n\t[[nodiscard]] const TextWithEntities &caption() const;\n\n\t[[nodiscard]] Data::ReactionId sentReactionId() const;\n\tvoid setReactionId(Data::ReactionId id);\n\n\t[[nodiscard]] auto recentViewers() const\n\t\t-> const std::vector<not_null<PeerData*>> &;\n\t[[nodiscard]] const StoryViews &viewsList() const;\n\t[[nodiscard]] const StoryViews &channelReactionsList() const;\n\t[[nodiscard]] int interactions() const;\n\t[[nodiscard]] int views() const;\n\t[[nodiscard]] int forwards() const;\n\t[[nodiscard]] int reactions() const;\n\tvoid applyViewsSlice(const QString &offset, const StoryViews &slice);\n\tvoid applyChannelReactionsSlice(\n\t\tconst QString &offset,\n\t\tconst StoryViews &slice);\n\n\t[[nodiscard]] const std::vector<StoryLocation> &locations() const;\n\t[[nodiscard]] auto suggestedReactions() const\n\t\t-> const std::vector<SuggestedReaction> &;\n\t[[nodiscard]] auto channelPosts() const\n\t\t-> const std::vector<ChannelPost> &;\n\t[[nodiscard]] auto urlAreas() const\n\t\t-> const std::vector<UrlArea> &;\n\t[[nodiscard]] auto weatherAreas() const\n\t\t-> const std::vector<WeatherArea> &;\n\n\tvoid applyChanges(\n\t\tStoryMedia media,\n\t\tconst MTPDstoryItem &data,\n\t\tTimeId now);\n\tvoid applyViewsCounts(const MTPDstoryViews &data);\n\t[[nodiscard]] TimeId lastUpdateTime() const;\n\n\t[[nodiscard]] bool repost() const;\n\t[[nodiscard]] bool repostModified() const;\n\t[[nodiscard]] PeerData *repostSourcePeer() const;\n\t[[nodiscard]] QString repostSourceName() const;\n\t[[nodiscard]] StoryId repostSourceId() const;\n\n\t[[nodiscard]] const base::flat_set<int> &albumIds() const;\n\tvoid setAlbumIds(base::flat_set<int> ids);\n\n\t[[nodiscard]] PeerData *fromPeer() const;\n\nprivate:\n\tstruct ViewsCounts {\n\t\tint views = 0;\n\t\tint forwards = 0;\n\t\tint reactions = 0;\n\t\tbase::flat_map<Data::ReactionId, int> reactionsCounts;\n\t\tstd::vector<not_null<PeerData*>> viewers;\n\t};\n\n\tvoid changeSuggestedReactionCount(Data::ReactionId id, int delta);\n\tvoid applyFields(\n\t\tStoryMedia media,\n\t\tconst MTPDstoryItem &data,\n\t\tTimeId now,\n\t\tbool initial);\n\n\tvoid updateViewsCounts(ViewsCounts &&counts, bool known, bool initial);\n\t[[nodiscard]] ViewsCounts parseViewsCounts(\n\t\tconst MTPDstoryViews &data,\n\t\tconst Data::ReactionId &mine);\n\n\tconst StoryId _id = 0;\n\tconst not_null<PeerData*> _peer;\n\tPeerData * const _repostSourcePeer = nullptr;\n\tconst QString _repostSourceName;\n\tconst StoryId _repostSourceId = 0;\n\tbase::flat_set<int> _albumIds;\n\tPeerData * const _fromPeer = nullptr;\n\tData::ReactionId _sentReactionId;\n\tStoryMedia _media;\n\tTextWithEntities _caption;\n\tstd::vector<not_null<PeerData*>> _recentViewers;\n\tstd::vector<StoryLocation> _locations;\n\tstd::vector<SuggestedReaction> _suggestedReactions;\n\tstd::vector<ChannelPost> _channelPosts;\n\tstd::vector<UrlArea> _urlAreas;\n\tstd::vector<WeatherArea> _weatherAreas;\n\tStoryViews _views;\n\tStoryViews _channelReactions;\n\tconst TimeId _date = 0;\n\tconst TimeId _expires = 0;\n\tTimeId _lastUpdateTime = 0;\n\tbool _out : 1 = false;\n\tbool _inProfile : 1 = false;\n\tbool _pinnedToTop : 1 = false;\n\tbool _privacyPublic : 1 = false;\n\tbool _privacyCloseFriends : 1 = false;\n\tbool _privacyContacts : 1 = false;\n\tbool _privacySelectedContacts : 1 = false;\n\tconst bool _repostModified : 1 = false;\n\tbool _noForwards : 1 = false;\n\tbool _edited : 1 = false;\n\n};\n\nclass StoryPreload final : public base::has_weak_ptr {\npublic:\n\tStoryPreload(not_null<Story*> story, Fn<void()> done);\n\t~StoryPreload();\n\n\t[[nodiscard]] FullStoryId id() const;\n\t[[nodiscard]] not_null<Story*> story() const;\n\nprivate:\n\tconst not_null<Story*> _story;\n\n\tstd::unique_ptr<MediaPreload> _task;\n\n};\n\nstruct StoryAlbum {\n\tint id = 0;\n\tQString title;\n\tPhotoData *iconPhoto = nullptr;\n\tDocumentData *iconVideo = nullptr;\n\n\tfriend inline bool operator==(\n\t\tconst StoryAlbum &,\n\t\tconst StoryAlbum &) = default;\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_streaming.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_streaming.h\"\n\n#include \"data/data_photo.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_file_origin.h\"\n#include \"media/streaming/media_streaming_loader.h\"\n#include \"media/streaming/media_streaming_reader.h\"\n#include \"media/streaming/media_streaming_document.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kKeepAliveTimeout = 5 * crl::time(1000);\n\ntemplate <typename Object, typename Data>\nbool PruneDestroyedAndSet(\n\t\tbase::flat_map<\n\t\t\tnot_null<Data*>,\n\t\t\tstd::weak_ptr<Object>> &objects,\n\t\tnot_null<Data*> data,\n\t\tconst std::shared_ptr<Object> &object) {\n\tauto result = false;\n\tfor (auto i = begin(objects); i != end(objects);) {\n\t\tif (i->first == data) {\n\t\t\t(i++)->second = object;\n\t\t\tresult = true;\n\t\t} else if (i->second.lock() != nullptr) {\n\t\t\t++i;\n\t\t} else {\n\t\t\ti = objects.erase(i);\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] auto LookupOtherQualities(\n\tDocumentData *original,\n\tnot_null<DocumentData*> quality,\n\tHistoryItem *context)\n-> std::vector<Media::Streaming::QualityDescriptor> {\n\tif (!original || !context) {\n\t\treturn {};\n\t}\n\tauto qualities = original->resolveQualities(context);\n\tif (qualities.empty()) {\n\t\treturn {};\n\t}\n\tauto result = std::vector<Media::Streaming::QualityDescriptor>();\n\tresult.reserve(qualities.size());\n\tfor (const auto &video : qualities) {\n\t\tif (video != quality) {\n\t\t\tif (const auto height = video->resolveVideoQuality()) {\n\t\t\t\tresult.push_back({\n\t\t\t\t\t.sizeInBytes = uint32(video->size),\n\t\t\t\t\t.height = uint32(height),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] auto LookupOtherQualities(\n\tDocumentData *original,\n\tnot_null<PhotoData*> quality,\n\tHistoryItem *context)\n-> std::vector<Media::Streaming::QualityDescriptor> {\n\tExpects(!original);\n\n\treturn {};\n}\n\n} // namespace\n\nStreaming::Streaming(not_null<Session*> owner)\n: _owner(owner)\n, _keptAliveTimer([=] { clearKeptAlive(); }) {\n}\n\nStreaming::~Streaming() = default;\n\ntemplate <typename Data>\n[[nodiscard]] std::shared_ptr<Streaming::Reader> Streaming::sharedReader(\n\t\tbase::flat_map<not_null<Data*>, std::weak_ptr<Reader>> &readers,\n\t\tnot_null<Data*> data,\n\t\tFileOrigin origin,\n\t\tbool forceRemoteLoader) {\n\tconst auto i = readers.find(data);\n\tif (i != end(readers)) {\n\t\tif (auto result = i->second.lock()) {\n\t\t\tif (!forceRemoteLoader || result->isRemoteLoader()) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\tauto loader = data->createStreamingLoader(origin, forceRemoteLoader);\n\tif (!loader) {\n\t\treturn nullptr;\n\t}\n\tauto result = std::make_shared<Reader>(\n\t\tstd::move(loader),\n\t\t&_owner->cacheBigFile());\n\tif (!PruneDestroyedAndSet(readers, data, result)) {\n\t\treaders.emplace_or_assign(data, result);\n\t}\n\treturn result;\n\n}\n\ntemplate <typename Data>\n[[nodiscard]] std::shared_ptr<Streaming::Document> Streaming::sharedDocument(\n\t\tbase::flat_map<not_null<Data*>, std::weak_ptr<Document>> &documents,\n\t\tbase::flat_map<not_null<Data*>, std::weak_ptr<Reader>> &readers,\n\t\tnot_null<Data*> data,\n\t\tDocumentData *original,\n\t\tHistoryItem *context,\n\t\tFileOrigin origin) {\n\tauto otherQualities = LookupOtherQualities(original, data, context);\n\tconst auto i = documents.find(data);\n\tif (i != end(documents)) {\n\t\tif (auto result = i->second.lock()) {\n\t\t\tif (!otherQualities.empty()) {\n\t\t\t\tresult->setOtherQualities(std::move(otherQualities));\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\t}\n\tauto reader = sharedReader(readers, data, origin);\n\tif (!reader) {\n\t\treturn nullptr;\n\t}\n\tauto result = std::make_shared<Document>(\n\t\tdata,\n\t\tstd::move(reader),\n\t\tstd::move(otherQualities));\n\tif (!PruneDestroyedAndSet(documents, data, result)) {\n\t\tdocuments.emplace_or_assign(data, result);\n\t}\n\treturn result;\n}\n\ntemplate <typename Data>\nvoid Streaming::keepAlive(\n\t\tbase::flat_map<not_null<Data*>, std::weak_ptr<Document>> &documents,\n\t\tnot_null<Data*> data) {\n\tconst auto i = documents.find(data);\n\tif (i == end(documents)) {\n\t\treturn;\n\t}\n\tauto shared = i->second.lock();\n\tif (!shared) {\n\t\treturn;\n\t}\n\tconst auto till = crl::now() + kKeepAliveTimeout;\n\tconst auto j = _keptAlive.find(shared);\n\tif (j != end(_keptAlive)) {\n\t\tj->second = till;\n\t} else {\n\t\t_keptAlive.emplace(std::move(shared), till);\n\t}\n\tif (!_keptAliveTimer.isActive()) {\n\t\t_keptAliveTimer.callOnce(kKeepAliveTimeout);\n\t}\n}\n\nstd::shared_ptr<Streaming::Reader> Streaming::sharedReader(\n\t\tnot_null<DocumentData*> document,\n\t\tFileOrigin origin,\n\t\tbool forceRemoteLoader) {\n\treturn sharedReader(_fileReaders, document, origin, forceRemoteLoader);\n}\n\nstd::shared_ptr<Streaming::Document> Streaming::sharedDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFileOrigin origin) {\n\treturn sharedDocument(\n\t\t_fileDocuments,\n\t\t_fileReaders,\n\t\tdocument,\n\t\tnullptr,\n\t\tnullptr,\n\t\torigin);\n}\n\nstd::shared_ptr<Streaming::Document> Streaming::sharedDocument(\n\t\tnot_null<DocumentData*> quality,\n\t\tnot_null<DocumentData*> original,\n\t\tHistoryItem *context,\n\t\tFileOrigin origin) {\n\treturn sharedDocument(\n\t\t_fileDocuments,\n\t\t_fileReaders,\n\t\tquality,\n\t\toriginal,\n\t\tcontext,\n\t\torigin);\n}\n\nstd::shared_ptr<Streaming::Reader> Streaming::sharedReader(\n\t\tnot_null<PhotoData*> photo,\n\t\tFileOrigin origin,\n\t\tbool forceRemoteLoader) {\n\treturn sharedReader(_photoReaders, photo, origin, forceRemoteLoader);\n}\n\nstd::shared_ptr<Streaming::Document> Streaming::sharedDocument(\n\t\tnot_null<PhotoData*> photo,\n\t\tFileOrigin origin) {\n\treturn sharedDocument(\n\t\t_photoDocuments,\n\t\t_photoReaders,\n\t\tphoto,\n\t\tnullptr,\n\t\tnullptr,\n\t\torigin);\n}\n\nvoid Streaming::keepAlive(not_null<DocumentData*> document) {\n\tkeepAlive(_fileDocuments, document);\n}\n\nvoid Streaming::keepAlive(not_null<PhotoData*> photo) {\n\tkeepAlive(_photoDocuments, photo);\n}\n\nvoid Streaming::clearKeptAlive() {\n\tconst auto now = crl::now();\n\tauto min = std::numeric_limits<crl::time>::max();\n\tfor (auto i = begin(_keptAlive); i != end(_keptAlive);) {\n\t\tconst auto wait = (i->second - now);\n\t\tif (wait <= 0) {\n\t\t\ti = _keptAlive.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t\tif (min > wait) {\n\t\t\t\tmin = wait;\n\t\t\t}\n\t\t}\n\t}\n\tif (!_keptAlive.empty()) {\n\t\t_keptAliveTimer.callOnce(min);\n\t}\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_streaming.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n\nclass PhotoData;\nclass DocumentData;\n\nnamespace Media::Streaming {\nclass Reader;\nclass Document;\n} // namespace Media::Streaming\n\nnamespace Data {\n\nclass Session;\nstruct FileOrigin;\n\nclass Streaming final {\npublic:\n\texplicit Streaming(not_null<Session*> owner);\n\tStreaming(const Streaming &other) = delete;\n\tStreaming &operator=(const Streaming &other) = delete;\n\t~Streaming();\n\n\tusing Reader = ::Media::Streaming::Reader;\n\tusing Document = ::Media::Streaming::Document;\n\n\t[[nodiscard]] std::shared_ptr<Reader> sharedReader(\n\t\tnot_null<DocumentData*> document,\n\t\tFileOrigin origin,\n\t\tbool forceRemoteLoader = false);\n\t[[nodiscard]] std::shared_ptr<Document> sharedDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFileOrigin origin);\n\t[[nodiscard]] std::shared_ptr<Document> sharedDocument(\n\t\tnot_null<DocumentData*> quality,\n\t\tnot_null<DocumentData*> original,\n\t\tHistoryItem *context,\n\t\tFileOrigin origin);\n\n\t[[nodiscard]] std::shared_ptr<Reader> sharedReader(\n\t\tnot_null<PhotoData*> photo,\n\t\tFileOrigin origin,\n\t\tbool forceRemoteLoader = false);\n\t[[nodiscard]] std::shared_ptr<Document> sharedDocument(\n\t\tnot_null<PhotoData*> photo,\n\t\tFileOrigin origin);\n\n\tvoid keepAlive(not_null<DocumentData*> document);\n\tvoid keepAlive(not_null<PhotoData*> photo);\n\nprivate:\n\tvoid clearKeptAlive();\n\n\ttemplate <typename Data>\n\t[[nodiscard]] std::shared_ptr<Reader> sharedReader(\n\t\tbase::flat_map<not_null<Data*>, std::weak_ptr<Reader>> &readers,\n\t\tnot_null<Data*> data,\n\t\tFileOrigin origin,\n\t\tbool forceRemoteLoader = false);\n\n\ttemplate <typename Data>\n\t[[nodiscard]] std::shared_ptr<Document> sharedDocument(\n\t\tbase::flat_map<not_null<Data*>, std::weak_ptr<Document>> &documents,\n\t\tbase::flat_map<not_null<Data*>, std::weak_ptr<Reader>> &readers,\n\t\tnot_null<Data*> data,\n\t\tDocumentData *original,\n\t\tHistoryItem *context,\n\t\tFileOrigin origin);\n\n\ttemplate <typename Data>\n\tvoid keepAlive(\n\t\tbase::flat_map<not_null<Data*>, std::weak_ptr<Document>> &documents,\n\t\tnot_null<Data*> data);\n\n\tconst not_null<Session*> _owner;\n\n\tbase::flat_map<\n\t\tnot_null<DocumentData*>,\n\t\tstd::weak_ptr<Reader>> _fileReaders;\n\tbase::flat_map<\n\t\tnot_null<DocumentData*>,\n\t\tstd::weak_ptr<Document>> _fileDocuments;\n\n\tbase::flat_map<not_null<PhotoData*>, std::weak_ptr<Reader>> _photoReaders;\n\tbase::flat_map<\n\t\tnot_null<PhotoData*>,\n\t\tstd::weak_ptr<Document>> _photoDocuments;\n\n\tbase::flat_map<std::shared_ptr<Document>, crl::time> _keptAlive;\n\tbase::Timer _keptAliveTimer;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_subscriptions.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\nstruct PeerSubscription final {\n\tuint64 credits = 0;\n\tint period = 0;\n\n\texplicit operator bool() const {\n\t\treturn credits > 0 && period > 0;\n\t}\n};\n\nstruct SubscriptionEntry final {\n\texplicit operator bool() const {\n\t\treturn !id.isEmpty();\n\t}\n\n\tQString id;\n\tQString inviteHash;\n\tQString title;\n\tQString slug;\n\tQDateTime until;\n\tPeerSubscription subscription;\n\tuint64 barePeerId = 0;\n\tuint64 photoId = 0;\n\tbool cancelled = false;\n\tbool cancelledByBot = false;\n\tbool expired = false;\n\tbool canRefulfill = false;\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_thread.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_thread.h\"\n\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_unread_things.h\"\n#include \"main/main_session.h\"\n\nnamespace Data {\n\nThread::~Thread() = default;\n\nnot_null<Thread*> Thread::migrateToOrMe() const {\n\tconst auto history = asHistory();\n\treturn history ? history->migrateToOrMe() : const_cast<Thread*>(this);\n}\n\nMsgId Thread::topicRootId() const {\n\tif (const auto topic = asTopic()) {\n\t\treturn topic->rootId();\n\t}\n\treturn MsgId();\n}\n\nPeerId Thread::monoforumPeerId() const {\n\tif (const auto sublist = asSublist()) {\n\t\treturn sublist->sublistPeer()->id;\n\t}\n\treturn PeerId();\n}\n\nPeerData *Thread::maybeSublistPeer() const {\n\tif (const auto sublist = asSublist()) {\n\t\treturn sublist->sublistPeer();\n\t}\n\treturn nullptr;\n}\n\nnot_null<PeerData*> Thread::peer() const {\n\treturn owningHistory()->peer;\n}\n\nPeerNotifySettings &Thread::notify() {\n\tconst auto topic = asTopic();\n\treturn topic ? topic->notify() : peer()->notify();\n}\n\nconst PeerNotifySettings &Thread::notify() const {\n\treturn const_cast<Thread*>(this)->notify();\n}\n\nvoid Thread::setUnreadThingsKnown() {\n\t_flags |= Flag::UnreadThingsKnown;\n}\n\nHistoryUnreadThings::Proxy Thread::unreadMentions() {\n\treturn {\n\t\tthis,\n\t\t_unreadThings,\n\t\tHistoryUnreadThings::Type::Mentions,\n\t\t!!(_flags & Flag::UnreadThingsKnown),\n\t};\n}\n\nHistoryUnreadThings::ConstProxy Thread::unreadMentions() const {\n\treturn {\n\t\t_unreadThings ? &_unreadThings->mentions : nullptr,\n\t\t!!(_flags & Flag::UnreadThingsKnown),\n\t};\n}\n\nHistoryUnreadThings::Proxy Thread::unreadReactions() {\n\treturn {\n\t\tthis,\n\t\t_unreadThings,\n\t\tHistoryUnreadThings::Type::Reactions,\n\t\t!!(_flags & Flag::UnreadThingsKnown),\n\t};\n}\n\nHistoryUnreadThings::ConstProxy Thread::unreadReactions() const {\n\treturn {\n\t\t_unreadThings ? &_unreadThings->reactions : nullptr,\n\t\t!!(_flags & Flag::UnreadThingsKnown),\n\t};\n}\n\nHistoryUnreadThings::Proxy Thread::unreadPollVotes() {\n\treturn {\n\t\tthis,\n\t\t_unreadThings,\n\t\tHistoryUnreadThings::Type::PollVotes,\n\t\t!!(_flags & Flag::UnreadThingsKnown),\n\t};\n}\n\nHistoryUnreadThings::ConstProxy Thread::unreadPollVotes() const {\n\treturn {\n\t\t_unreadThings ? &_unreadThings->pollVotes : nullptr,\n\t\t!!(_flags & Flag::UnreadThingsKnown),\n\t};\n}\n\nbool Thread::canToggleUnread(bool nowUnread) const {\n\tif ((asTopic() || asForum()) && !nowUnread) {\n\t\treturn false;\n\t} else if (asSublist() && owningHistory()->peer->isSelf()) {\n\t\treturn false;\n\t} else if (asHistory() && peer()->amMonoforumAdmin()) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nconst base::flat_set<MsgId> &Thread::unreadMentionsIds() const {\n\tif (!_unreadThings) {\n\t\tstatic const auto Result = base::flat_set<MsgId>();\n\t\treturn Result;\n\t}\n\treturn _unreadThings->mentions.ids();\n}\n\nconst base::flat_set<MsgId> &Thread::unreadReactionsIds() const {\n\tif (!_unreadThings) {\n\t\tstatic const auto Result = base::flat_set<MsgId>();\n\t\treturn Result;\n\t}\n\treturn _unreadThings->reactions.ids();\n}\n\nconst base::flat_set<MsgId> &Thread::unreadPollVotesIds() const {\n\tif (!_unreadThings) {\n\t\tstatic const auto Result = base::flat_set<MsgId>();\n\t\treturn Result;\n\t}\n\treturn _unreadThings->pollVotes.ids();\n}\n\nvoid Thread::clearNotifications() {\n\t_notifications.clear();\n}\n\nvoid Thread::clearIncomingNotifications() {\n\tif (!peer()->isSelf()) {\n\t\tconst auto proj = [](ItemNotification notification) {\n\t\t\treturn notification.item->out();\n\t\t};\n\t\t_notifications.erase(\n\t\t\tranges::remove(_notifications, false, proj),\n\t\t\tend(_notifications));\n\t}\n}\n\nvoid Thread::removeNotification(not_null<HistoryItem*> item) {\n\t_notifications.erase(\n\t\tranges::remove(_notifications, item, &ItemNotification::item),\n\t\tend(_notifications));\n}\n\nstd::optional<ItemNotification> Thread::currentNotification() const {\n\treturn empty(_notifications)\n\t\t? std::nullopt\n\t\t: std::make_optional(_notifications.front());\n}\n\nbool Thread::hasNotification() const {\n\treturn !empty(_notifications);\n}\n\nvoid Thread::skipNotification() {\n\tif (!empty(_notifications)) {\n\t\t_notifications.pop_front();\n\t}\n}\n\nvoid Thread::pushNotification(ItemNotification notification) {\n\t_notifications.push_back(notification);\n}\n\nvoid Thread::popNotification(ItemNotification notification) {\n\tif (!empty(_notifications) && (_notifications.back() == notification)) {\n\t\t_notifications.pop_back();\n\t}\n}\n\nvoid Thread::setMuted(bool muted) {\n\tif (muted) {\n\t\t_flags |= Flag::Muted;\n\t} else {\n\t\t_flags &= ~Flag::Muted;\n\t}\n}\n\nvoid Thread::setUnreadMarkFlag(bool unread) {\n\tif (unread) {\n\t\t_flags |= Flag::UnreadMark;\n\t} else {\n\t\t_flags &= ~Flag::UnreadMark;\n\t}\n}\n\n[[nodiscard]] bool Thread::hasPinnedMessages() const {\n\treturn (_flags & Flag::HasPinnedMessages);\n}\n\nvoid Thread::setHasPinnedMessages(bool has) {\n\tif (hasPinnedMessages() == has) {\n\t\treturn;\n\t} else if (has) {\n\t\t_flags |= Flag::HasPinnedMessages;\n\t} else {\n\t\t_flags &= ~Flag::HasPinnedMessages;\n\t}\n\tsession().changes().entryUpdated(\n\t\tthis,\n\t\tEntryUpdate::Flag::HasPinnedMessages);\n}\n\nvoid Thread::saveMeAsActiveSubsectionThread() {\n\tif (const auto peer = owningHistory()->peer; peer->useSubsectionTabs()) {\n\t\tif (const auto forum = peer->forum()) {\n\t\t\tforum->saveActiveSubsectionThread(this);\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\tif (const auto monoforum = channel->monoforum()) {\n\t\t\t\tmonoforum->saveActiveSubsectionThread(this);\n\t\t\t}\n\t\t}\n\t}\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_thread.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n#include \"dialogs/dialogs_entry.h\"\n#include \"dialogs/ui/dialogs_message_view.h\"\n#include \"ui/text/text.h\"\n\n#include <deque>\n\nenum class ChatRestriction;\nusing ChatRestrictions = base::flags<ChatRestriction>;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace HistoryUnreadThings {\nenum class AddType;\nstruct All;\nclass Proxy;\nclass ConstProxy;\n} // namespace HistoryUnreadThings\n\nnamespace HistoryView {\nclass SendActionPainter;\n} // namespace HistoryView\n\nnamespace st {\nextern const int &dialogsTextWidthMin;\n} // namespace st\n\nnamespace Data {\n\nclass PeerNotifySettings;\n\nenum class ItemNotificationType {\n\tMessage,\n\tReaction,\n\tPollVote,\n};\n\nstruct ItemNotification {\n\tnot_null<HistoryItem*> item;\n\tUserData *reactionOrVoteSender = nullptr;\n\tItemNotificationType type = ItemNotificationType::Message;\n\n\tfriend inline auto operator<=>(\n\t\tItemNotification a,\n\t\tItemNotification b) = default;\n};\n\nclass Thread : public Dialogs::Entry {\npublic:\n\tusing Entry::Entry;\n\t~Thread();\n\n\t[[nodiscard]] virtual not_null<History*> owningHistory() = 0;\n\n\t[[nodiscard]] not_null<Thread*> migrateToOrMe() const;\n\t[[nodiscard]] not_null<const History*> owningHistory() const {\n\t\treturn const_cast<Thread*>(this)->owningHistory();\n\t}\n\t[[nodiscard]] MsgId topicRootId() const;\n\t[[nodiscard]] PeerId monoforumPeerId() const;\n\t[[nodiscard]] PeerData *maybeSublistPeer() const;\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\t[[nodiscard]] PeerNotifySettings &notify();\n\t[[nodiscard]] const PeerNotifySettings &notify() const;\n\n\tvoid setUnreadThingsKnown();\n\t[[nodiscard]] HistoryUnreadThings::Proxy unreadMentions();\n\t[[nodiscard]] HistoryUnreadThings::ConstProxy unreadMentions() const;\n\t[[nodiscard]] HistoryUnreadThings::Proxy unreadReactions();\n\t[[nodiscard]] HistoryUnreadThings::ConstProxy unreadReactions() const;\n\t[[nodiscard]] HistoryUnreadThings::Proxy unreadPollVotes();\n\t[[nodiscard]] HistoryUnreadThings::ConstProxy unreadPollVotes() const;\n\tvirtual void hasUnreadMentionChanged(bool has) = 0;\n\tvirtual void hasUnreadReactionChanged(bool has) = 0;\n\tvirtual void hasUnreadPollVoteChanged(bool has) = 0;\n\tbool canToggleUnread(bool nowUnread) const;\n\n\tvoid removeNotification(not_null<HistoryItem*> item);\n\tvoid clearNotifications();\n\tvoid clearIncomingNotifications();\n\t[[nodiscard]] auto currentNotification() const\n\t\t-> std::optional<ItemNotification>;\n\tbool hasNotification() const;\n\tvoid skipNotification();\n\tvoid pushNotification(ItemNotification notification);\n\tvoid popNotification(ItemNotification notification);\n\n\t[[nodiscard]] bool muted() const {\n\t\treturn (_flags & Flag::Muted);\n\t}\n\tvirtual void setMuted(bool muted);\n\n\t[[nodiscard]] bool unreadMark() const {\n\t\treturn (_flags & Flag::UnreadMark);\n\t}\n\n\t[[nodiscard]] virtual bool isServerSideUnread(\n\t\tnot_null<const HistoryItem*> item) const = 0;\n\n\t[[nodiscard]] const base::flat_set<MsgId> &unreadMentionsIds() const;\n\t[[nodiscard]] const base::flat_set<MsgId> &unreadReactionsIds() const;\n\t[[nodiscard]] const base::flat_set<MsgId> &unreadPollVotesIds() const;\n\n\t[[nodiscard]] Ui::Text::String &cloudDraftTextCache() {\n\t\treturn _cloudDraftTextCache;\n\t}\n\t[[nodiscard]] Dialogs::Ui::MessageView &lastItemDialogsView() {\n\t\treturn _lastItemDialogsView;\n\t}\n\n\t[[nodiscard]] virtual auto sendActionPainter()\n\t\t-> HistoryView::SendActionPainter* = 0;\n\n\t[[nodiscard]] bool hasPinnedMessages() const;\n\tvoid setHasPinnedMessages(bool has);\n\n\tvoid saveMeAsActiveSubsectionThread();\n\nprotected:\n\tvoid setUnreadMarkFlag(bool unread);\n\nprivate:\n\tenum class Flag : uchar {\n\t\tUnreadMark = (1 << 0),\n\t\tMuted = (1 << 1),\n\t\tUnreadThingsKnown = (1 << 2),\n\t\tHasPinnedMessages = (1 << 3),\n\t};\n\tfriend inline constexpr bool is_flag_type(Flag) { return true; }\n\n\tUi::Text::String _cloudDraftTextCache = { st::dialogsTextWidthMin };\n\tDialogs::Ui::MessageView _lastItemDialogsView;\n\tstd::unique_ptr<HistoryUnreadThings::All> _unreadThings;\n\tstd::deque<ItemNotification> _notifications;\n\n\tbase::flags<Flag> _flags;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_todo_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_todo_list.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"base/call_delayed.h\"\n#include \"history/history_item.h\"\n#include \"main/main_session.h\"\n#include \"api/api_text_entities.h\"\n#include \"ui/text/text_options.h\"\n\nnamespace {\n\nconstexpr auto kShortPollTimeout = 30 * crl::time(1000);\n\nconst TodoListItem *ItemById(const std::vector<TodoListItem> &list, int id) {\n\tconst auto i = ranges::find(list, id, &TodoListItem::id);\n\treturn (i != end(list)) ? &*i : nullptr;\n}\n\nTodoListItem *ItemById(std::vector<TodoListItem> &list, int id) {\n\treturn const_cast<TodoListItem*>(ItemById(std::as_const(list), id));\n}\n\n} // namespace\n\nTodoListData::TodoListData(not_null<Data::Session*> owner, TodoListId id)\n: id(id)\n, _owner(owner) {\n}\n\nData::Session &TodoListData::owner() const {\n\treturn *_owner;\n}\n\nMain::Session &TodoListData::session() const {\n\treturn _owner->session();\n}\n\nbool TodoListData::applyChanges(const MTPDtodoList &todolist) {\n\tconst auto newTitle = Api::ParseTextWithEntities(\n\t\t&session(),\n\t\ttodolist.vtitle());\n\tconst auto newFlags = (todolist.is_others_can_append()\n\t\t? Flag::OthersCanAppend\n\t\t: Flag())\n\t\t| (todolist.is_others_can_complete() ? Flag::OthersCanComplete\n\t\t\t: Flag());\n\tauto newItems = ranges::views::all(\n\t\ttodolist.vlist().v\n\t) | ranges::views::transform([&](const MTPTodoItem &item) {\n\t\treturn TodoListItemFromMTP(&session(), item);\n\t}) | ranges::views::take(\n\t\tkMaxOptions\n\t) | ranges::to_vector;\n\n\tconst auto changed1 = (title != newTitle) || (_flags != newFlags);\n\tconst auto changed2 = (items != newItems);\n\tif (!changed1 && !changed2) {\n\t\treturn false;\n\t}\n\tif (changed1) {\n\t\ttitle = newTitle;\n\t\t_flags = newFlags;\n\t}\n\tif (changed2) {\n\t\tstd::swap(items, newItems);\n\t\tfor (const auto &old : newItems) {\n\t\t\tif (const auto current = itemById(old.id)) {\n\t\t\t\tcurrent->completedBy = old.completedBy;\n\t\t\t\tcurrent->completionDate = old.completionDate;\n\t\t\t}\n\t\t}\n\t}\n\t++version;\n\treturn true;\n}\n\nbool TodoListData::applyCompletions(\n\t\tconst MTPVector<MTPTodoCompletion> *completions) {\n\tauto changed = false;\n\tconst auto lookup = [&](int id) {\n\t\tif (!completions) {\n\t\t\treturn (const MTPDtodoCompletion*)nullptr;\n\t\t}\n\t\tconst auto proj = [](const MTPTodoCompletion &completion) {\n\t\t\treturn completion.data().vid().v;\n\t\t};\n\t\tconst auto i = ranges::find(completions->v, id, proj);\n\t\treturn (i != completions->v.end()) ? &i->data() : nullptr;\n\t};\n\tfor (auto &item : items) {\n\t\tconst auto completion = lookup(item.id);\n\t\tconst auto by = completion\n\t\t\t? owner().peer(peerFromMTP(completion->vcompleted_by())).get()\n\t\t\t: nullptr;\n\t\tconst auto date = completion ? completion->vdate().v : TimeId();\n\t\tif (item.completedBy != by || item.completionDate != date) {\n\t\t\titem.completedBy = by;\n\t\t\titem.completionDate = date;\n\t\t\tchanged = true;\n\t\t}\n\t}\n\tif (changed) {\n\t\t++version;\n\t}\n\treturn changed;\n}\n\nvoid TodoListData::apply(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPDmessageActionTodoCompletions &data) {\n\tfor (const auto &id : data.vcompleted().v) {\n\t\tif (const auto task = itemById(id.v)) {\n\t\t\ttask->completedBy = item->from();\n\t\t\ttask->completionDate = item->date();\n\t\t}\n\t}\n\tfor (const auto &id : data.vincompleted().v) {\n\t\tif (const auto task = itemById(id.v)) {\n\t\t\ttask->completedBy = nullptr;\n\t\t\ttask->completionDate = TimeId();\n\t\t}\n\t}\n\towner().notifyTodoListUpdateDelayed(this);\n}\n\nvoid TodoListData::apply(const MTPDmessageActionTodoAppendTasks &data) {\n\tconst auto limit = TodoListData::kMaxOptions;\n\tfor (const auto &task : data.vlist().v) {\n\t\tif (items.size() < limit) {\n\t\t\tconst auto parsed = TodoListItemFromMTP(\n\t\t\t\t&session(),\n\t\t\t\ttask);\n\t\t\tif (!itemById(parsed.id)) {\n\t\t\t\titems.push_back(std::move(parsed));\n\t\t\t}\n\t\t}\n\t}\n\towner().notifyTodoListUpdateDelayed(this);\n}\n\nTodoListItem *TodoListData::itemById(int id) {\n\treturn ItemById(items, id);\n}\n\nconst TodoListItem *TodoListData::itemById(int id) const {\n\treturn ItemById(items, id);\n}\n\nvoid TodoListData::setFlags(Flags flags) {\n\tif (_flags != flags) {\n\t\t_flags = flags;\n\t\t++version;\n\t}\n}\n\nTodoListData::Flags TodoListData::flags() const {\n\treturn _flags;\n}\n\nbool TodoListData::othersCanAppend() const {\n\treturn (_flags & Flag::OthersCanAppend);\n}\n\nbool TodoListData::othersCanComplete() const {\n\treturn (_flags & Flag::OthersCanComplete);\n}\n\nMTPVector<MTPTodoItem> TodoListItemsToMTP(\n\t\tnot_null<Main::Session*> session,\n\t\tconst std::vector<TodoListItem> &tasks) {\n\tconst auto convert = [&](const TodoListItem &item) {\n\t\treturn MTP_todoItem(\n\t\t\tMTP_int(item.id),\n\t\t\tMTP_textWithEntities(\n\t\t\t\tMTP_string(item.text.text),\n\t\t\t\tApi::EntitiesToMTP(session, item.text.entities)));\n\t};\n\tauto items = QVector<MTPTodoItem>();\n\titems.reserve(tasks.size());\n\tranges::transform(tasks, ranges::back_inserter(items), convert);\n\treturn MTP_vector<MTPTodoItem>(items);\n}\n\nMTPTodoList TodoListDataToMTP(not_null<const TodoListData*> todolist) {\n\tusing Flag = MTPDtodoList::Flag;\n\tconst auto flags = Flag()\n\t\t| (todolist->othersCanAppend()\n\t\t\t? Flag::f_others_can_append\n\t\t\t: Flag())\n\t\t| (todolist->othersCanComplete()\n\t\t\t? Flag::f_others_can_complete\n\t\t\t: Flag());\n\treturn MTP_todoList(\n\t\tMTP_flags(flags),\n\t\tMTP_textWithEntities(\n\t\t\tMTP_string(todolist->title.text),\n\t\t\tApi::EntitiesToMTP(\n\t\t\t\t&todolist->session(),\n\t\t\t\ttodolist->title.entities)),\n\t\tTodoListItemsToMTP(&todolist->session(), todolist->items));\n}\n\nMTPInputMedia TodoListDataToInputMedia(\n\t\tnot_null<const TodoListData*> todolist) {\n\treturn MTP_inputMediaTodo(TodoListDataToMTP(todolist));\n}\n\nTodoListItem TodoListItemFromMTP(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPTodoItem &item) {\n\tconst auto &data = item.data();\n\treturn {\n\t\t.text = Api::ParseTextWithEntities(session, data.vtitle()),\n\t\t.id = data.vid().v,\n\t};\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_todo_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nclass Session;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nstruct TodoListItem {\n\tTextWithEntities text;\n\tPeerData *completedBy = nullptr;\n\tTimeId completionDate = 0;\n\tint id = 0;\n\n\tfriend inline bool operator==(\n\t\tconst TodoListItem &,\n\t\tconst TodoListItem &) = default;\n};\n\nstruct TodoListData {\n\tTodoListData(not_null<Data::Session*> owner, TodoListId id);\n\n\t[[nodiscard]] Data::Session &owner() const;\n\t[[nodiscard]] Main::Session &session() const;\n\n\tenum class Flag {\n\t\tOthersCanAppend   = 0x01,\n\t\tOthersCanComplete = 0x02,\n\t};\n\tfriend inline constexpr bool is_flag_type(Flag) { return true; };\n\tusing Flags = base::flags<Flag>;\n\n\tbool applyChanges(const MTPDtodoList &todolist);\n\tbool applyCompletions(const MTPVector<MTPTodoCompletion> *completions);\n\n\tvoid apply(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPDmessageActionTodoCompletions &data);\n\tvoid apply(const MTPDmessageActionTodoAppendTasks &data);\n\n\t[[nodiscard]] TodoListItem *itemById(int id);\n\t[[nodiscard]] const TodoListItem *itemById(int id) const;\n\n\tvoid setFlags(Flags flags);\n\t[[nodiscard]] Flags flags() const;\n\t[[nodiscard]] bool othersCanAppend() const;\n\t[[nodiscard]] bool othersCanComplete() const;\n\n\tTodoListId id;\n\tTextWithEntities title;\n\tstd::vector<TodoListItem> items;\n\tint version = 0;\n\n\tstatic constexpr auto kMaxOptions = 32;\n\nprivate:\n\tconst not_null<Data::Session*> _owner;\n\tFlags _flags = Flags();\n\n};\n\n[[nodiscard]] MTPVector<MTPTodoItem> TodoListItemsToMTP(\n\tnot_null<Main::Session*> session,\n\tconst std::vector<TodoListItem> &tasks);\n[[nodiscard]] MTPTodoList TodoListDataToMTP(\n\tnot_null<const TodoListData*> todolist);\n[[nodiscard]] MTPInputMedia TodoListDataToInputMedia(\n\tnot_null<const TodoListData*> todolist);\n[[nodiscard]] TodoListItem TodoListItemFromMTP(\n\tnot_null<Main::Session*> session,\n\tconst MTPTodoItem &item);\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_types.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_types.h\"\n\n#include \"ui/widgets/fields/input_field.h\"\n#include \"storage/cache/storage_cache_types.h\"\n#include \"base/openssl_help.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kDocumentCacheTag = 0x0000000000000100ULL;\nconstexpr auto kDocumentCacheMask = 0x00000000000000FFULL;\nconstexpr auto kDocumentThumbCacheTag = 0x0000000000000200ULL;\nconstexpr auto kDocumentThumbCacheMask = 0x00000000000000FFULL;\nconstexpr auto kAudioAlbumThumbCacheTag = 0x0000000000000300ULL;\nconstexpr auto kWebDocumentCacheTag = 0x0000020000000000ULL;\nconstexpr auto kUrlCacheTag = 0x0000030000000000ULL;\nconstexpr auto kGeoPointCacheTag = 0x0000040000000000ULL;\n\n} // namespace\n\nStorage::Cache::Key DocumentCacheKey(int32 dcId, uint64 id) {\n\treturn Storage::Cache::Key{\n\t\tData::kDocumentCacheTag | (uint64(dcId) & Data::kDocumentCacheMask),\n\t\tid\n\t};\n}\n\nStorage::Cache::Key DocumentThumbCacheKey(int32 dcId, uint64 id) {\n\tconst auto part = (uint64(dcId) & Data::kDocumentThumbCacheMask);\n\treturn Storage::Cache::Key{\n\t\tData::kDocumentThumbCacheTag | part,\n\t\tid\n\t};\n}\n\nStorage::Cache::Key WebDocumentCacheKey(const WebFileLocation &location) {\n\tconst auto CacheDcId = 4; // The default production value. Doesn't matter.\n\tconst auto dcId = uint64(CacheDcId) & 0xFFULL;\n\tconst auto &url = location.url();\n\tconst auto hash = openssl::Sha256(bytes::make_span(url));\n\tconst auto bytes = bytes::make_span(hash);\n\tconst auto bytes1 = bytes.subspan(0, sizeof(uint32));\n\tconst auto bytes2 = bytes.subspan(sizeof(uint32), sizeof(uint64));\n\tconst auto part1 = *reinterpret_cast<const uint32*>(bytes1.data());\n\tconst auto part2 = *reinterpret_cast<const uint64*>(bytes2.data());\n\treturn Storage::Cache::Key{\n\t\tData::kWebDocumentCacheTag | (dcId << 32) | part1,\n\t\tpart2\n\t};\n}\n\nStorage::Cache::Key UrlCacheKey(const QString &location) {\n\tconst auto url = location.toUtf8();\n\tconst auto hash = openssl::Sha256(bytes::make_span(url));\n\tconst auto bytes = bytes::make_span(hash);\n\tconst auto bytes1 = bytes.subspan(0, sizeof(uint32));\n\tconst auto bytes2 = bytes.subspan(sizeof(uint32), sizeof(uint64));\n\tconst auto bytes3 = bytes.subspan(\n\t\tsizeof(uint32) + sizeof(uint64),\n\t\tsizeof(uint16));\n\tconst auto part1 = *reinterpret_cast<const uint32*>(bytes1.data());\n\tconst auto part2 = *reinterpret_cast<const uint64*>(bytes2.data());\n\tconst auto part3 = *reinterpret_cast<const uint16*>(bytes3.data());\n\treturn Storage::Cache::Key{\n\t\tData::kUrlCacheTag | (uint64(part3) << 32) | part1,\n\t\tpart2\n\t};\n}\n\nStorage::Cache::Key GeoPointCacheKey(const GeoPointLocation &location) {\n\tconst auto zoomscale = ((uint32(location.zoom) & 0x0FU) << 8)\n\t\t| (uint32(location.scale) & 0x0FU);\n\tconst auto widthheight = ((uint32(location.width) & 0xFFFFU) << 16)\n\t\t| (uint32(location.height) & 0xFFFFU);\n\treturn Storage::Cache::Key{\n\t\tData::kGeoPointCacheTag | (uint64(zoomscale) << 32) | widthheight,\n\t\t(uint64(base::SafeRound(\n\t\t\tstd::abs(location.lat + 360.) * 1000000)) << 32)\n\t\t| uint64(base::SafeRound(std::abs(location.lon + 360.) * 1000000))\n\t};\n}\n\nStorage::Cache::Key AudioAlbumThumbCacheKey(\n\t\tconst AudioAlbumThumbLocation &location) {\n\treturn Storage::Cache::Key{\n\t\tData::kAudioAlbumThumbCacheTag,\n\t\tlocation.documentId,\n\t};\n}\n\n} // namespace Data\n\nvoid MessageCursor::fillFrom(not_null<const Ui::InputField*> field) {\n\tconst auto cursor = field->textCursor();\n\tposition = cursor.position();\n\tanchor = cursor.anchor();\n\tconst auto top = field->scrollTop().current();\n\tscroll = (top != field->scrollTopMax()) ? top : Ui::kQFixedMax;\n}\n\nvoid MessageCursor::applyTo(not_null<Ui::InputField*> field) {\n\tauto cursor = field->textCursor();\n\tcursor.setPosition(anchor, QTextCursor::MoveAnchor);\n\tcursor.setPosition(position, QTextCursor::KeepAnchor);\n\tfield->setTextCursor(cursor);\n\tfield->scrollTo(scroll);\n}\n\nPeerId PeerFromMessage(const MTPmessage &message) {\n\treturn message.match([](const MTPDmessageEmpty &data) {\n\t\treturn data.vpeer_id() ? peerFromMTP(*data.vpeer_id()) : PeerId(0);\n\t}, [](const auto &data) {\n\t\treturn peerFromMTP(data.vpeer_id());\n\t});\n}\n\nMTPDmessage::Flags FlagsFromMessage(const MTPmessage &message) {\n\treturn message.match([](const MTPDmessageEmpty &) {\n\t\treturn MTPDmessage::Flags(0);\n\t}, [](const MTPDmessage &data) {\n\t\treturn data.vflags().v;\n\t}, [](const MTPDmessageService &data) {\n\t\treturn mtpCastFlags(data.vflags().v);\n\t});\n}\n\nMsgId IdFromMessage(const MTPmessage &message) {\n\treturn message.match([](const auto &data) {\n\t\treturn data.vid().v;\n\t});\n}\n\nTimeId DateFromMessage(const MTPmessage &message) {\n\treturn message.match([](const MTPDmessageEmpty &) {\n\t\treturn TimeId(0);\n\t}, [](const auto &message) {\n\t\treturn message.vdate().v;\n\t});\n}\n\nBusinessShortcutId BusinessShortcutIdFromMessage(\n\t\tconst MTPmessage &message) {\n\treturn message.match([](const MTPDmessage &data) {\n\t\treturn data.vquick_reply_shortcut_id().value_or_empty();\n\t}, [](const auto &) {\n\t\treturn BusinessShortcutId();\n\t});\n}\n\nbool GoodStickerDimensions(int width, int height) {\n\t// Show all .webp (except very large ones) as stickers,\n\t// allow to open them in media viewer to see details.\n\tconstexpr auto kLargetsStickerSide = 2560;\n\treturn (width > 0)\n\t\t&& (height > 0)\n\t\t&& (width * height <= kLargetsStickerSide * kLargetsStickerSide);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_types.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/text/text.h\" // Ui::kQFixedMax.\n#include \"data/data_peer_id.h\"\n#include \"data/data_msg_id.h\"\n#include \"base/qt/qt_compare.h\"\n\nclass HistoryItem;\nusing HistoryItemsList = std::vector<not_null<HistoryItem*>>;\n\nclass StorageImageLocation;\nclass WebFileLocation;\nstruct GeoPointLocation;\n\nnamespace Storage {\nnamespace Cache {\nstruct Key;\n} // namespace Cache\n} // namespace Storage\n\nnamespace Ui {\nclass InputField;\n} // namespace Ui\n\nnamespace Images {\nenum class Option;\nusing Options = base::flags<Option>;\n} // namespace Images\n\nnamespace Data {\n\nstruct FileOrigin;\nstruct EmojiStatusCollectible;\n\nstruct UploadState {\n\texplicit UploadState(int64 size) : size(size) {\n\t}\n\tint64 offset = 0;\n\tint64 size = 0;\n\tbool waitingForAlbum = false;\n};\n\nStorage::Cache::Key DocumentCacheKey(int32 dcId, uint64 id);\nStorage::Cache::Key DocumentThumbCacheKey(int32 dcId, uint64 id);\nStorage::Cache::Key WebDocumentCacheKey(const WebFileLocation &location);\nStorage::Cache::Key UrlCacheKey(const QString &location);\nStorage::Cache::Key GeoPointCacheKey(const GeoPointLocation &location);\nStorage::Cache::Key AudioAlbumThumbCacheKey(\n\tconst AudioAlbumThumbLocation &location);\n\nconstexpr auto kImageCacheTag = uint8(0x01);\nconstexpr auto kStickerCacheTag = uint8(0x02);\nconstexpr auto kVoiceMessageCacheTag = uint8(0x03);\nconstexpr auto kVideoMessageCacheTag = uint8(0x04);\nconstexpr auto kAnimationCacheTag = uint8(0x05);\n\n} // namespace Data\n\nstruct MessageGroupId {\n\tuint64 peerAndScheduledFlag = 0;\n\tuint64 value = 0;\n\n\tMessageGroupId() = default;\n\tstatic MessageGroupId FromRaw(\n\t\t\tPeerId peer,\n\t\t\tuint64 value,\n\t\t\tbool scheduled) {\n\t\tauto result = MessageGroupId();\n\t\tresult.peerAndScheduledFlag = peer.value\n\t\t\t| (scheduled ? (1ULL << 55) : 0);\n\t\tresult.value = value;\n\t\treturn result;\n\t}\n\n\tbool empty() const {\n\t\treturn !value;\n\t}\n\texplicit operator bool() const {\n\t\treturn !empty();\n\t}\n\tuint64 raw() const {\n\t\treturn value;\n\t}\n\n\tfriend inline constexpr auto operator<=>(\n\t\tMessageGroupId,\n\t\tMessageGroupId) noexcept = default;\n\n};\n\nclass PeerData;\nclass UserData;\nclass ChatData;\nclass ChannelData;\nstruct BotInfo;\n\nnamespace Data {\nclass Folder;\n} // namespace Data\n\nusing FolderId = int32;\nusing FilterId = int32;\n\nusing MessageIdsList = std::vector<FullMsgId>;\n\n[[nodiscard]] PeerId PeerFromMessage(const MTPmessage &message);\n[[nodiscard]] MTPDmessage::Flags FlagsFromMessage(\n\tconst MTPmessage &message);\n[[nodiscard]] MsgId IdFromMessage(const MTPmessage &message);\n[[nodiscard]] TimeId DateFromMessage(const MTPmessage &message);\n[[nodiscard]] BusinessShortcutId BusinessShortcutIdFromMessage(\n\tconst MTPmessage &message);\n\n[[nodiscard]] inline MTPint MTP_int(MsgId id) noexcept {\n\treturn MTP_int(id.bare);\n}\n\nclass DocumentData;\nclass PhotoData;\nstruct WebPageData;\nstruct GameData;\nstruct BotAppData;\nstruct PollData;\nstruct TodoListData;\n\nusing PhotoId = uint64;\nusing VideoId = uint64;\nusing AudioId = uint64;\nusing DocumentId = uint64;\nusing WebPageId = uint64;\nusing GameId = uint64;\nusing PollId = uint64;\nusing TodoListId = FullMsgId;\nusing WallPaperId = uint64;\nusing CallId = uint64;\nusing BotAppId = uint64;\nusing EffectId = uint64;\nusing CollectibleId = uint64;\n\nstruct EmojiStatusId {\n\tDocumentId documentId = 0;\n\tstd::shared_ptr<Data::EmojiStatusCollectible> collectible;\n\n\texplicit operator bool() const {\n\t\treturn documentId || collectible;\n\t}\n\n\tfriend inline auto operator<=>(\n\t\tconst EmojiStatusId &,\n\t\tconst EmojiStatusId &) = default;\n\tfriend inline bool operator==(\n\t\tconst EmojiStatusId &,\n\t\tconst EmojiStatusId &) = default;\n};\n\nconstexpr auto CancelledWebPageId = WebPageId(0xFFFFFFFFFFFFFFFFULL);\n\nstruct PreparedPhotoThumb {\n\tQImage image;\n\tQByteArray bytes;\n};\nusing PreparedPhotoThumbs = base::flat_map<char, PreparedPhotoThumb>;\n\n// [0] == -1 -- counting, [0] == -2 -- could not count\nusing VoiceWaveform = QVector<signed char>;\n\nenum LocationType {\n\tUnknownFileLocation = 0,\n\t// 1, 2, etc are used as \"version\" value in mediaKey() method.\n\n\tDocumentFileLocation = 0x4e45abe9, // mtpc_inputDocumentFileLocation\n\tAudioFileLocation = 0x74dc404d, // mtpc_inputAudioFileLocation\n\tVideoFileLocation = 0x3d0364ec, // mtpc_inputVideoFileLocation\n\tSecureFileLocation = 0xcbc7ee28, // mtpc_inputSecureFileLocation\n};\n\nenum FileStatus : signed char {\n\tFileDownloadFailed = -2,\n\tFileUploadFailed = -1,\n\tFileReady = 1,\n};\n\n// Don't change the values. This type is used for serialization.\nenum DocumentType {\n\tFileDocument = 0,\n\tVideoDocument = 1,\n\tSongDocument = 2,\n\tStickerDocument = 3,\n\tAnimatedDocument = 4,\n\tVoiceDocument = 5,\n\tRoundVideoDocument = 6,\n\tWallPaperDocument = 7,\n};\n\ninline constexpr auto kStickerSideSize = 512;\n[[nodiscard]] bool GoodStickerDimensions(int width, int height);\n\nusing MediaKey = QPair<uint64, uint64>;\n\nstruct MessageCursor {\n\tMessageCursor() = default;\n\tMessageCursor(int position, int anchor, int scroll)\n\t: position(position)\n\t, anchor(anchor)\n\t, scroll(scroll) {\n\t}\n\tMessageCursor(not_null<const Ui::InputField*> field) {\n\t\tfillFrom(field);\n\t}\n\n\tvoid fillFrom(not_null<const Ui::InputField*> field);\n\tvoid applyTo(not_null<Ui::InputField*> field);\n\n\tint position = 0;\n\tint anchor = 0;\n\tint scroll = Ui::kQFixedMax;\n\n};\n\ninline bool operator==(\n\t\tconst MessageCursor &a,\n\t\tconst MessageCursor &b) {\n\treturn (a.position == b.position)\n\t\t&& (a.anchor == b.anchor)\n\t\t&& (a.scroll == b.scroll);\n}\n\ninline bool operator!=(\n\t\tconst MessageCursor &a,\n\t\tconst MessageCursor &b) {\n\treturn !(a == b);\n}\n\nstruct StickerSetIdentifier {\n\tuint64 id = 0;\n\tuint64 accessHash = 0;\n\tQString shortName;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn !id && shortName.isEmpty();\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n};\n\nenum class MessageFlag : uint64 {\n\tHideEdited            = (1ULL << 0),\n\tLegacy                = (1ULL << 1),\n\tHasReplyMarkup        = (1ULL << 2),\n\tHasFromId             = (1ULL << 3),\n\tHasPostAuthor         = (1ULL << 4),\n\tHasViews              = (1ULL << 5),\n\tHasReplyInfo          = (1ULL << 6),\n\tCanViewReactions      = (1ULL << 7),\n\tAdminLogEntry         = (1ULL << 8),\n\tPost                  = (1ULL << 9),\n\tSilent                = (1ULL << 10),\n\tOutgoing              = (1ULL << 11),\n\tPinned                = (1ULL << 12),\n\tMediaIsUnread         = (1ULL << 13),\n\tHasUnreadReaction     = (1ULL << 14),\n\tMentionsMe            = (1ULL << 15),\n\tIsOrWasScheduled      = (1ULL << 16),\n\tNoForwards            = (1ULL << 17),\n\tInvertMedia           = (1ULL << 18),\n\n\t// Needs to return back to inline mode.\n\tHasSwitchInlineButton = (1ULL << 19),\n\n\t// For \"shared links\" indexing.\n\tHasTextLinks          = (1ULL << 20),\n\n\t// Group / channel create or migrate service message.\n\tIsGroupEssential      = (1ULL << 21),\n\n\t// Edited media is generated on the client\n\t// and should not update media from server.\n\tIsLocalUpdateMedia    = (1ULL << 22),\n\n\t// Sent from inline bot, need to re-set media when sent.\n\tFromInlineBot         = (1ULL << 23),\n\n\t// Generated on the client side and should be unread.\n\tClientSideUnread      = (1ULL << 24),\n\n\t// In a supergroup.\n\tHasAdminBadge         = (1ULL << 25),\n\n\t// Outgoing message that is being sent.\n\tBeingSent             = (1ULL << 26),\n\n\t// Outgoing message and failed to be sent.\n\tSendingFailed         = (1ULL << 27),\n\n\t// No media and only a several emoji or an only custom emoji text.\n\tSpecialOnlyEmoji      = (1ULL << 28),\n\n\t// Message existing in the message history.\n\tHistoryEntry          = (1ULL << 29),\n\n\t// Local message, not existing on the server.\n\tLocal                 = (1ULL << 30),\n\n\t// Fake message for some UI element.\n\tFakeHistoryItem       = (1ULL << 31),\n\n\t// Contact sign-up message, notification should be skipped for Silent.\n\tIsContactSignUp       = (1ULL << 32),\n\n\t// Optimization for item text custom emoji repainting.\n\tCustomEmojiRepainting = (1ULL << 33),\n\n\t// Profile photo suggestion, views have special media type.\n\tIsUserpicSuggestion   = (1ULL << 34),\n\n\tOnlyEmojiAndSpaces    = (1ULL << 35),\n\tOnlyEmojiAndSpacesSet = (1ULL << 36),\n\n\t// Fake message with some info, like bot cover and information.\n\tFakeAboutView         = (1ULL << 37),\n\n\tStoryItem             = (1ULL << 38),\n\n\tInHighlightProcess    = (1ULL << 39),\n\n\t// If not set then we need to refresh _displayFrom value.\n\tDisplayFromChecked    = (1ULL << 40),\n\tDisplayFromProfiles   = (1ULL << 41),\n\n\tShowSimilarChannels   = (1ULL << 42),\n\n\tSponsored             = (1ULL << 43),\n\n\tReactionsAreTags      = (1ULL << 44),\n\n\tShortcutMessage       = (1ULL << 45),\n\n\tEffectWatched         = (1ULL << 46),\n\n\tSensitiveContent      = (1ULL << 47),\n\tHasRestrictions       = (1ULL << 48),\n\n\tEstimatedDate         = (1ULL << 49),\n\n\tReactionsAllowed      = (1ULL << 50),\n\n\tHideDisplayDate       = (1ULL << 51),\n\n\tStarsPaidSuggested    = (1ULL << 52),\n\tTonPaidSuggested      = (1ULL << 53),\n\n\tStoryInProfile        = (1ULL << 54),\n\tSavedMusicItem        = (1ULL << 55),\n\n\tHasHiddenLinks        = (1ULL << 56),\n\tHasSummaryEntry       = (1ULL << 57),\n\tCanBeSummarized       = (1ULL << 58),\n\tHasUnreadPollVote     = (1ULL << 59),\n\n\tTextAppearing         = (1ULL << 60),\n\tTextAppearingStarted  = (1ULL << 61),\n};\ninline constexpr bool is_flag_type(MessageFlag) { return true; }\nusing MessageFlags = base::flags<MessageFlag>;\n\nenum class MediaWebPageFlag : uint8 {\n\tForceLargeMedia = (1 << 0),\n\tForceSmallMedia = (1 << 1),\n\tManual = (1 << 2),\n\tSafe = (1 << 3),\n\tSponsored = (1 << 4),\n};\ninline constexpr bool is_flag_type(MediaWebPageFlag) { return true; }\nusing MediaWebPageFlags = base::flags<MediaWebPageFlag>;\n\nnamespace Data {\n\nenum class ForwardOptions {\n\tPreserveInfo,\n\tNoSenderNames,\n\tNoNamesAndCaptions,\n};\n\nstruct ForwardDraft {\n\tMessageIdsList ids;\n\tForwardOptions options = ForwardOptions::PreserveInfo;\n\n\tfriend inline auto operator<=>(\n\t\tconst ForwardDraft&,\n\t\tconst ForwardDraft&) = default;\n};\n\nstruct ResolvedForwardDraft {\n\tHistoryItemsList items;\n\tForwardOptions options = ForwardOptions::PreserveInfo;\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_unread_value.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_unread_value.h\"\n\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"window/notifications_manager.h\"\n\nnamespace Data {\nnamespace {\n\nrpl::producer<Dialogs::UnreadState> MainListUnreadState(\n\t\tnot_null<Dialogs::MainList*> list) {\n\treturn rpl::single(rpl::empty) | rpl::then(\n\t\tlist->unreadStateChanges() | rpl::to_empty\n\t) | rpl::map([=] {\n\t\treturn list->unreadState();\n\t});\n}\n\n} // namespace\n\n[[nodiscard]] Dialogs::UnreadState MainListMapUnreadState(\n\t\tnot_null<Main::Session*> session,\n\t\tconst Dialogs::UnreadState &state) {\n\tconst auto folderId = Data::Folder::kId;\n\tif (const auto folder = session->data().folderLoaded(folderId)) {\n\t\treturn state - folder->chatsList()->unreadState();\n\t}\n\treturn state;\n}\n\nrpl::producer<Dialogs::UnreadState> UnreadStateValue(\n\t\tnot_null<Main::Session*> session,\n\t\tFilterId filterId) {\n\tif (filterId > 0) {\n\t\tconst auto filters = &session->data().chatsFilters();\n\t\treturn MainListUnreadState(filters->chatsList(filterId));\n\t}\n\treturn MainListUnreadState(\n\t\tsession->data().chatsList()\n\t) | rpl::map([=](const Dialogs::UnreadState &state) {\n\t\treturn MainListMapUnreadState(session, state);\n\t});\n}\n\nrpl::producer<bool> IncludeMutedCounterFoldersValue() {\n\tusing namespace Window::Notifications;\n\treturn rpl::single(rpl::empty_value()) | rpl::then(\n\t\tCore::App().notifications().settingsChanged(\n\t\t) | rpl::filter(\n\t\t\trpl::mappers::_1 == ChangeType::IncludeMuted\n\t\t) | rpl::to_empty\n\t) | rpl::map([] {\n\t\treturn Core::App().settings().includeMutedCounterFolders();\n\t});\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_unread_value.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Dialogs {\nstruct UnreadState;\n} // namespace Dialogs\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\n[[nodiscard]] Dialogs::UnreadState MainListMapUnreadState(\n\tnot_null<Main::Session*> session,\n\tconst Dialogs::UnreadState &state);\n\n[[nodiscard]] rpl::producer<Dialogs::UnreadState> UnreadStateValue(\n\tnot_null<Main::Session*> session,\n\tFilterId filterId);\n\n[[nodiscard]] rpl::producer<bool> IncludeMutedCounterFoldersValue();\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_user.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_user.h\"\n\n#include \"api/api_credits.h\"\n#include \"api/api_global_privacy.h\"\n#include \"api/api_sensitive_content.h\"\n#include \"api/api_statistics.h\"\n#include \"api/api_text_entities.h\"\n#include \"base/timer_rpl.h\"\n#include \"core/application.h\"\n#include \"storage/localstorage.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/storage_user_photos.h\"\n#include \"main/main_session.h\"\n#include \"data/business/data_business_common.h\"\n#include \"data/business/data_business_info.h\"\n#include \"data/components/credits.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_icons.h\"\n#include \"data/data_saved_music.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_peer_bot_command.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_wall_paper.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"api/api_peer_photo.h\"\n#include \"apiwrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"window/notifications_manager.h\"\n#include \"styles/style_chat.h\"\n\nnamespace {\n\n// User with hidden last seen stays online in UI for such amount of seconds.\nconstexpr auto kSetOnlineAfterActivity = TimeId(30);\n\nusing UpdateFlag = Data::PeerUpdate::Flag;\n\nbool ApplyBotVerifierSettings(\n\t\tnot_null<BotInfo*> info,\n\t\tconst MTPBotVerifierSettings *settings) {\n\tif (!settings) {\n\t\tconst auto taken = base::take(info->verifierSettings);\n\t\treturn taken != nullptr;\n\t}\n\tconst auto &data = settings->data();\n\tconst auto parsed = BotVerifierSettings{\n\t\t.iconId = DocumentId(data.vicon().v),\n\t\t.company = qs(data.vcompany()),\n\t\t.customDescription = qs(data.vcustom_description().value_or_empty()),\n\t\t.canModifyDescription = data.is_can_modify_custom_description(),\n\t};\n\tif (!info->verifierSettings) {\n\t\tinfo->verifierSettings = std::make_unique<BotVerifierSettings>(\n\t\t\tparsed);\n\t\treturn true;\n\t} else if (*info->verifierSettings != parsed) {\n\t\t*info->verifierSettings = parsed;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\n[[nodiscard]] Data::StarsRating ParseStarsRating(\n\t\tconst MTPStarsRating *rating) {\n\tif (!rating) {\n\t\treturn {};\n\t}\n\tconst auto &data = rating->data();\n\treturn {\n\t\t.level = data.vlevel().v,\n\t\t.stars = int(data.vstars().v),\n\t\t.thisLevelStars = int(data.vcurrent_level_stars().v),\n\t\t.nextLevelStars = int(data.vnext_level_stars().value_or_empty()),\n\t};\n}\n\n} // namespace\n\nBotInfo::BotInfo() = default;\n\nBotInfo::~BotInfo() = default;\n\nvoid BotInfo::ensureForum(not_null<UserData*> that) {\n\tif (!_forum) {\n\t\tconst auto history = that->owner().history(that);\n\t\t_forum = std::make_unique<Data::Forum>(history);\n\t\thistory->forumChanged(nullptr);\n\t}\n}\n\nData::Forum *BotInfo::forum() const {\n\treturn _forum.get();\n}\n\nstd::unique_ptr<Data::Forum> BotInfo::takeForumData() {\n\tif (auto result = base::take(_forum)) {\n\t\tresult->history()->forumChanged(result.get());\n\t\treturn result;\n\t}\n\treturn nullptr;\n}\n\nData::LastseenStatus LastseenFromMTP(\n\t\tconst MTPUserStatus &status,\n\t\tData::LastseenStatus currentStatus) {\n\treturn status.match([](const MTPDuserStatusEmpty &data) {\n\t\treturn Data::LastseenStatus::LongAgo();\n\t}, [&](const MTPDuserStatusRecently &data) {\n\t\treturn currentStatus.isLocalOnlineValue()\n\t\t\t? Data::LastseenStatus::OnlineTill(\n\t\t\t\tcurrentStatus.onlineTill(),\n\t\t\t\ttrue,\n\t\t\t\tdata.is_by_me())\n\t\t\t: Data::LastseenStatus::Recently(data.is_by_me());\n\t}, [](const MTPDuserStatusLastWeek &data) {\n\t\treturn Data::LastseenStatus::WithinWeek(data.is_by_me());\n\t}, [](const MTPDuserStatusLastMonth &data) {\n\t\treturn Data::LastseenStatus::WithinMonth(data.is_by_me());\n\t}, [](const MTPDuserStatusOnline& data) {\n\t\treturn Data::LastseenStatus::OnlineTill(data.vexpires().v);\n\t}, [](const MTPDuserStatusOffline &data) {\n\t\treturn Data::LastseenStatus::OnlineTill(data.vwas_online().v);\n\t});\n}\n\nUserData::UserData(not_null<Data::Session*> owner, PeerId id)\n: PeerData(owner, id)\n, _flags((id == owner->session().userPeerId()) ? Flag::Self : Flag(0)) {\n}\n\nUserData::~UserData() = default;\n\nbool UserData::canShareThisContact() const {\n\treturn canShareThisContactFast()\n\t\t|| !owner().findContactPhone(peerToUser(id)).isEmpty();\n}\n\nvoid UserData::setIsContact(bool is) {\n\tconst auto status = is\n\t\t? ContactStatus::Contact\n\t\t: ContactStatus::NotContact;\n\tif (_contactStatus != status) {\n\t\t_contactStatus = status;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::IsContact);\n\t}\n}\n\nData::LastseenStatus UserData::lastseen() const {\n\treturn _lastseen;\n}\n\nbool UserData::updateLastseen(Data::LastseenStatus value) {\n\tif (_lastseen == value) {\n\t\treturn false;\n\t}\n\t_lastseen = value;\n\towner().maybeStopWatchForOffline(this);\n\treturn true;\n}\n\n// see Serialize::readPeer as well\nvoid UserData::setPhoto(const MTPUserProfilePhoto &photo) {\n\tphoto.match([&](const MTPDuserProfilePhoto &data) {\n\t\tif (data.is_personal()) {\n\t\t\taddFlags(UserDataFlag::PersonalPhoto);\n\t\t} else {\n\t\t\tremoveFlags(UserDataFlag::PersonalPhoto);\n\t\t}\n\t\tupdateUserpic(\n\t\t\tdata.vphoto_id().v,\n\t\t\tdata.vdc_id().v,\n\t\t\tdata.is_has_video());\n\t}, [&](const MTPDuserProfilePhotoEmpty &) {\n\t\tremoveFlags(UserDataFlag::PersonalPhoto);\n\t\tclearUserpic();\n\t});\n}\n\nauto UserData::unavailableReasons() const\n-> const std::vector<Data::UnavailableReason> & {\n\treturn _unavailableReasons;\n}\n\nvoid UserData::setUnavailableReasonsList(\n\t\tstd::vector<Data::UnavailableReason> &&reasons) {\n\t_unavailableReasons = std::move(reasons);\n}\n\nvoid UserData::setCommonChatsCount(int count) {\n\tif (_commonChatsCount != count) {\n\t\t_commonChatsCount = count;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::CommonChats);\n\t}\n}\n\nint UserData::peerGiftsCount() const {\n\treturn _peerGiftsCount;\n}\n\nvoid UserData::setPeerGiftsCount(int count) {\n\tif (_peerGiftsCount != count) {\n\t\t_peerGiftsCount = count;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::PeerGifts);\n\t}\n}\n\nbool UserData::hasPrivateForwardName() const {\n\treturn !_privateForwardName.isEmpty();\n}\n\nQString UserData::privateForwardName() const {\n\treturn _privateForwardName;\n}\n\nvoid UserData::setPrivateForwardName(const QString &name) {\n\t_privateForwardName = name;\n}\n\nbool UserData::hasActiveStories() const {\n\treturn flags() & Flag::HasActiveStories;\n}\n\nbool UserData::hasUnreadStories() const {\n\treturn flags() & Flag::HasUnreadStories;\n}\n\nbool UserData::hasActiveVideoStream() const {\n\treturn flags() & Flag::HasActiveVideoStream;\n}\n\nvoid UserData::setStoriesState(StoriesState state) {\n\tExpects(state != StoriesState::Unknown);\n\n\tconst auto was = flags();\n\tswitch (state) {\n\tcase StoriesState::None:\n\t\t_flags.remove(Flag::HasActiveStories\n\t\t\t| Flag::HasUnreadStories\n\t\t\t| Flag::HasActiveVideoStream);\n\t\tbreak;\n\tcase StoriesState::HasRead:\n\t\t_flags.set(Flag::HasActiveStories\n\t\t\t| (was\n\t\t\t\t& ~(Flag::HasUnreadStories | Flag::HasActiveVideoStream)));\n\t\tbreak;\n\tcase StoriesState::HasUnread:\n\t\t_flags.set((was & ~Flag::HasActiveVideoStream)\n\t\t\t| Flag::HasActiveStories\n\t\t\t| Flag::HasUnreadStories);\n\t\tbreak;\n\tcase StoriesState::HasVideoStream:\n\t\t_flags.set((was & ~Flag::HasUnreadStories)\n\t\t\t| Flag::HasActiveStories\n\t\t\t| Flag::HasActiveVideoStream);\n\t\tbreak;\n\t}\n\tif (flags() != was) {\n\t\tif (const auto history = owner().historyLoaded(this)) {\n\t\t\thistory->updateChatListEntryPostponed();\n\t\t}\n\t\tsession().changes().peerUpdated(this, UpdateFlag::StoriesState);\n\t}\n}\n\nconst Data::BusinessDetails &UserData::businessDetails() const {\n\tstatic const auto empty = Data::BusinessDetails();\n\treturn _businessDetails ? *_businessDetails : empty;\n}\n\nvoid UserData::setBusinessDetails(Data::BusinessDetails details) {\n\tdetails.hours = details.hours.normalized();\n\tif ((!details && !_businessDetails)\n\t\t|| (details && _businessDetails && details == *_businessDetails)) {\n\t\treturn;\n\t}\n\t_businessDetails = details\n\t\t? std::make_unique<Data::BusinessDetails>(std::move(details))\n\t\t: nullptr;\n\tsession().changes().peerUpdated(this, UpdateFlag::BusinessDetails);\n}\n\nvoid UserData::setStarRefProgram(StarRefProgram program) {\n\tconst auto info = botInfo.get();\n\tif (info && info->starRefProgram != program) {\n\t\tinfo->starRefProgram = program;\n\t\tsession().changes().peerUpdated(\n\t\t\tthis,\n\t\t\tData::PeerUpdate::Flag::StarRefProgram);\n\t}\n}\n\nChannelId UserData::personalChannelId() const {\n\treturn _personalChannelId;\n}\n\nMsgId UserData::personalChannelMessageId() const {\n\treturn _personalChannelMessageId;\n}\n\nvoid UserData::setPersonalChannel(ChannelId channelId, MsgId messageId) {\n\tif (_personalChannelId != channelId\n\t\t|| _personalChannelMessageId != messageId) {\n\t\t_personalChannelId = channelId;\n\t\t_personalChannelMessageId = messageId;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::PersonalChannel);\n\t}\n}\n\nUserId UserData::botManagerId() const {\n\treturn _botManagerId;\n}\n\nvoid UserData::setBotManagerId(UserId managerId) {\n\tconst auto changed = (_botManagerId != managerId);\n\t_botManagerId = managerId;\n\tif (changed) {\n\t\tsession().changes().peerUpdated(this, UpdateFlag::ManagedBot);\n\t}\n}\n\nMTPInputUser UserData::inputUser() const {\n\tconst auto item = isLoaded() ? nullptr : owner().messageWithPeer(id);\n\tif (item) {\n\t\tconst auto peer = item->history()->peer;\n\t\tAssert(peer.get() != this);\n\n\t\treturn MTP_inputUserFromMessage(\n\t\t\titem->history()->peer->input(),\n\t\t\tMTP_int(item->id.bare),\n\t\t\tMTP_long(peerToUser(id).bare));\n\t} else if (isSelf()) {\n\t\treturn MTP_inputUserSelf();\n\t}\n\treturn MTP_inputUser(\n\t\tMTP_long(peerToUser(id).bare),\n\t\tMTP_long(_accessHash));\n}\n\nvoid UserData::setName(\n\t\tconst QString &newFirstName,\n\t\tconst QString &newLastName,\n\t\tconst QString &newPhoneName,\n\t\tconst QString &newUsername) {\n\tbool changeName = !newFirstName.isEmpty() || !newLastName.isEmpty();\n\n\tQString newFullName;\n\tif (changeName && newFirstName.trimmed().isEmpty()) {\n\t\tfirstName = newLastName;\n\t\tlastName = QString();\n\t\tnewFullName = firstName;\n\t} else {\n\t\tif (changeName) {\n\t\t\tfirstName = newFirstName;\n\t\t\tlastName = newLastName;\n\t\t}\n\t\tnewFullName = lastName.isEmpty()\n\t\t\t? firstName\n\t\t\t: tr::lng_full_name(\n\t\t\t\ttr::now,\n\t\t\t\tlt_first_name,\n\t\t\t\tfirstName,\n\t\t\t\tlt_last_name,\n\t\t\t\tlastName);\n\t}\n\tupdateNameDelayed(newFullName, newPhoneName, newUsername);\n}\n\nvoid UserData::setUsernames(const Data::Usernames &newUsernames) {\n\tconst auto wasUsername = username();\n\tconst auto wasUsernames = usernames();\n\t_username.setUsernames(newUsernames);\n\tconst auto nowUsername = username();\n\tconst auto nowUsernames = usernames();\n\tsession().changes().peerUpdated(\n\t\tthis,\n\t\tUpdateFlag()\n\t\t| ((wasUsername != nowUsername)\n\t\t\t? UpdateFlag::Username\n\t\t\t: UpdateFlag())\n\t\t| (!ranges::equal(wasUsernames, nowUsernames)\n\t\t\t? UpdateFlag::Usernames\n\t\t\t: UpdateFlag()));\n}\n\nvoid UserData::setUsername(const QString &username) {\n\t_username.setUsername(username);\n}\n\nvoid UserData::setPhone(const QString &newPhone) {\n\tif (_phone != newPhone) {\n\t\t_phone = newPhone;\n\t}\n}\n\nvoid UserData::setBotInfoVersion(int version) {\n\tif (version < 0) {\n\t\t// We don't support bots becoming non-bots.\n\t\tif (botInfo) {\n\t\t\tbotInfo->version = -1;\n\t\t}\n\t} else if (!botInfo) {\n\t\tbotInfo = std::make_unique<BotInfo>();\n\t\tbotInfo->version = version;\n\t\towner().userIsBotChanged(this);\n\t} else if (botInfo->version < version) {\n\t\tif (!botInfo->commands.empty()) {\n\t\t\tbotInfo->commands.clear();\n\t\t\towner().botCommandsChanged(this);\n\t\t}\n\t\tbotInfo->description.clear();\n\t\tbotInfo->version = version;\n\t\tbotInfo->inited = false;\n\t}\n}\n\nvoid UserData::setBotInfo(const MTPBotInfo &info) {\n\tswitch (info.type()) {\n\tcase mtpc_botInfo: {\n\t\tconst auto &d = info.c_botInfo();\n\t\tif (!isBot()) {\n\t\t\treturn;\n\t\t} else if (d.vuser_id() && peerFromUser(*d.vuser_id()) != id) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto description = qs(d.vdescription().value_or_empty());\n\t\tif (botInfo->description != description) {\n\t\t\tbotInfo->description = description;\n\t\t\t++botInfo->descriptionVersion;\n\t\t}\n\t\tif (const auto photo = d.vdescription_photo()) {\n\t\t\tconst auto parsed = owner().processPhoto(*photo);\n\t\t\tif (botInfo->photo != parsed) {\n\t\t\t\tbotInfo->photo = parsed;\n\t\t\t\t++botInfo->descriptionVersion;\n\t\t\t}\n\t\t} else if (botInfo->photo) {\n\t\t\tbotInfo->photo = nullptr;\n\t\t\t++botInfo->descriptionVersion;\n\t\t}\n\t\tif (const auto document = d.vdescription_document()) {\n\t\t\tconst auto parsed = owner().processDocument(*document);\n\t\t\tif (botInfo->document != parsed) {\n\t\t\t\tbotInfo->document = parsed;\n\t\t\t\t++botInfo->descriptionVersion;\n\t\t\t}\n\t\t} else if (botInfo->document) {\n\t\t\tbotInfo->document = nullptr;\n\t\t\t++botInfo->descriptionVersion;\n\t\t}\n\n\t\tauto commands = d.vcommands()\n\t\t\t? ranges::views::all(\n\t\t\t\td.vcommands()->v\n\t\t\t) | ranges::views::transform(\n\t\t\t\tData::BotCommandFromTL\n\t\t\t) | ranges::to_vector\n\t\t\t: std::vector<Data::BotCommand>();\n\t\tconst auto changedCommands = !ranges::equal(\n\t\t\tbotInfo->commands,\n\t\t\tcommands);\n\t\tbotInfo->commands = std::move(commands);\n\n\t\tconst auto changedButton = Data::ApplyBotMenuButton(\n\t\t\tbotInfo.get(),\n\t\t\td.vmenu_button());\n\t\tbotInfo->inited = true;\n\n\t\tconst auto privacy = qs(d.vprivacy_policy_url().value_or_empty());\n\t\tconst auto privacyChanged = (botInfo->privacyPolicyUrl != privacy);\n\t\tbotInfo->privacyPolicyUrl = privacy;\n\n\t\tif (const auto settings = d.vapp_settings()) {\n\t\t\tconst auto &data = settings->data();\n\t\t\tbotInfo->botAppColorTitleDay = Ui::MaybeColorFromSerialized(\n\t\t\t\tdata.vheader_color()).value_or(QColor(0, 0, 0, 0));\n\t\t\tbotInfo->botAppColorTitleNight = Ui::MaybeColorFromSerialized(\n\t\t\t\tdata.vheader_dark_color()).value_or(QColor(0, 0, 0, 0));\n\t\t\tbotInfo->botAppColorBodyDay = Ui::MaybeColorFromSerialized(\n\t\t\t\tdata.vbackground_color()).value_or(QColor(0, 0, 0, 0));\n\t\t\tbotInfo->botAppColorBodyNight = Ui::MaybeColorFromSerialized(\n\t\t\t\tdata.vbackground_dark_color()).value_or(QColor(0, 0, 0, 0));\n\t\t} else {\n\t\t\tbotInfo->botAppColorTitleDay\n\t\t\t\t= botInfo->botAppColorTitleNight\n\t\t\t\t= botInfo->botAppColorBodyDay\n\t\t\t\t= botInfo->botAppColorBodyNight\n\t\t\t\t= QColor(0, 0, 0, 0);\n\t\t}\n\t\tconst auto changedVerifierSettings = ApplyBotVerifierSettings(\n\t\t\tbotInfo.get(),\n\t\t\td.vverifier_settings());\n\n\t\tif (changedCommands\n\t\t\t|| changedButton\n\t\t\t|| privacyChanged\n\t\t\t|| changedVerifierSettings) {\n\t\t\towner().botCommandsChanged(this);\n\t\t}\n\t} break;\n\t}\n}\n\nvoid UserData::setNameOrPhone(const QString &newNameOrPhone) {\n\tnameOrPhone = newNameOrPhone;\n}\n\nvoid UserData::madeAction(TimeId when) {\n\tif (isBot() || isServiceUser() || when <= 0) {\n\t\treturn;\n\t}\n\tconst auto till = lastseen().onlineTill();\n\tif (till < when + 1\n\t\t&& updateLastseen(\n\t\t\tData::LastseenStatus::OnlineTill(\n\t\t\t\twhen + kSetOnlineAfterActivity,\n\t\t\t\t!till || lastseen().isLocalOnlineValue(),\n\t\t\t\tlastseen().isHiddenByMe()))) {\n\t\tsession().changes().peerUpdated(this, UpdateFlag::OnlineStatus);\n\t}\n}\n\nvoid UserData::setAccessHash(uint64 accessHash) {\n\tif (accessHash == kInaccessibleAccessHashOld) {\n\t\t_accessHash = 0;\n\t\t_flags.add(Flag::Deleted);\n\t\tinvalidateEmptyUserpic();\n\t} else {\n\t\t_accessHash = accessHash;\n\t}\n}\n\nvoid UserData::setFlags(UserDataFlags which) {\n\tif (!isBot()) {\n\t\twhich &= ~Flag::Forum;\n\t}\n\tconst auto diff = flags() ^ which;\n\tif (diff & Flag::Deleted) {\n\t\tinvalidateEmptyUserpic();\n\t}\n\t// Let Data::Forum live till the end of _flags.set.\n\t// That way the data can be used in changes handler.\n\t// Example: render frame for forum auto-closing animation.\n\tconst auto takenForum = (botInfo\n\t\t&& (diff & Flag::Forum)\n\t\t&& !(which & Flag::Forum))\n\t\t? botInfo->takeForumData()\n\t\t: nullptr;\n\tif ((diff & Flag::Forum) && (which & Flag::Forum)) {\n\t\tif (const auto info = botInfo.get()) {\n\t\t\tinfo->ensureForum(this);\n\t\t}\n\t}\n\t_flags.set((flags() & Flag::Self) | (which & ~Flag::Self));\n\tif (diff & Flag::Forum) {\n\t\tif (const auto history = this->owner().historyLoaded(this)) {\n\t\t\tif (diff & Flag::Forum) {\n\t\t\t\tCore::App().notifications().clearFromHistory(history);\n\t\t\t\thistory->updateChatListEntryHeight();\n\t\t\t\tif (history->inChatList()) {\n\t\t\t\t\tif (const auto forum = this->forum()) {\n\t\t\t\t\t\tforum->preloadTopics();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (const auto raw = takenForum.get()) {\n\t\towner().forumIcons().clearUserpicsReset(raw);\n\t}\n}\n\nvoid UserData::addFlags(UserDataFlags which) {\n\tsetFlags(flags() | which);\n}\n\nvoid UserData::removeFlags(UserDataFlags which) {\n\tsetFlags(flags() & ~which);\n}\n\nbool UserData::isVerified() const {\n\treturn flags() & UserDataFlag::Verified;\n}\n\nbool UserData::isScam() const {\n\treturn flags() & UserDataFlag::Scam;\n}\n\nbool UserData::isFake() const {\n\treturn flags() & UserDataFlag::Fake;\n}\n\nbool UserData::isPremium() const {\n\treturn flags() & UserDataFlag::Premium;\n}\n\nbool UserData::isBotInlineGeo() const {\n\treturn flags() & UserDataFlag::BotInlineGeo;\n}\n\nbool UserData::isBot() const {\n\treturn botInfo != nullptr;\n}\n\nbool UserData::isSupport() const {\n\treturn flags() & UserDataFlag::Support;\n}\n\nbool UserData::isInaccessible() const {\n\treturn flags() & UserDataFlag::Deleted;\n}\n\nbool UserData::applyMinPhoto() const {\n\treturn !(flags() & UserDataFlag::DiscardMinPhoto);\n}\n\nbool UserData::hasPersonalPhoto() const {\n\treturn (flags() & UserDataFlag::PersonalPhoto);\n}\n\nbool UserData::hasStoriesHidden() const {\n\treturn (flags() & UserDataFlag::StoriesHidden);\n}\n\nbool UserData::hasRequirePremiumToWrite() const {\n\treturn (flags() & UserDataFlag::HasRequirePremiumToWrite);\n}\n\nbool UserData::hasStarsPerMessage() const {\n\treturn (flags() & UserDataFlag::HasStarsPerMessage);\n}\n\nbool UserData::requiresPremiumToWrite() const {\n\treturn !isSelf() && (flags() & UserDataFlag::RequiresPremiumToWrite);\n}\n\nbool UserData::messageMoneyRestrictionsKnown() const {\n\treturn (flags() & UserDataFlag::MessageMoneyRestrictionsKnown);\n}\n\nbool UserData::canSendIgnoreMoneyRestrictions() const {\n\treturn !isInaccessible() && !isRepliesChat() && !isVerifyCodes();\n}\n\nbool UserData::readDatesPrivate() const {\n\treturn (flags() & UserDataFlag::ReadDatesPrivate);\n}\n\nbool UserData::allowsForwarding() const {\n\treturn !(flags() & Flag::NoForwardsMyEnabled)\n\t\t&& !(flags() & Flag::NoForwardsPeerEnabled);\n}\n\nvoid UserData::setNoForwardsFlags(bool myEnabled, bool peerEnabled) {\n\tconst auto mask = Flag::NoForwardsMyEnabled | Flag::NoForwardsPeerEnabled;\n\tsetFlags((flags() & ~mask)\n\t\t| (myEnabled ? Flag::NoForwardsMyEnabled : Flag())\n\t\t| (peerEnabled ? Flag::NoForwardsPeerEnabled : Flag()));\n\tif (!myEnabled && !peerEnabled) {\n\t\towner().clearSharingDisabledTime(this);\n\t}\n}\n\nint UserData::starsPerMessage() const {\n\treturn _starsPerMessage;\n}\n\nvoid UserData::setStoriesCorrespondent(bool is) {\n\tif (is) {\n\t\t_flags.add(UserDataFlag::StoriesCorrespondent);\n\t} else {\n\t\t_flags.remove(UserDataFlag::StoriesCorrespondent);\n\t}\n}\n\nbool UserData::storiesCorrespondent() const {\n\treturn (_flags.current() & UserDataFlag::StoriesCorrespondent);\n}\n\nvoid UserData::setStarsPerMessage(int stars) {\n\tif (_starsPerMessage != stars) {\n\t\t_starsPerMessage = stars;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::StarsPerMessage);\n\t}\n\tcheckTrustedPayForMessage();\n}\n\nvoid UserData::setStarsRating(Data::StarsRating value) {\n\tif (_starsRating != value) {\n\t\t_starsRating = value;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::StarsRating);\n\t}\n}\n\nData::StarsRating UserData::starsRating() const {\n\treturn _starsRating;\n}\n\nbool UserData::canAddContact() const {\n\treturn canShareThisContact() && !isContact();\n}\n\nbool UserData::canShareThisContactFast() const {\n\treturn !_phone.isEmpty();\n}\n\nQString UserData::username() const {\n\treturn _username.username();\n}\n\nQString UserData::editableUsername() const {\n\treturn _username.editableUsername();\n}\n\nconst std::vector<QString> &UserData::usernames() const {\n\treturn _username.usernames();\n}\n\nbool UserData::isUsernameEditable(QString username) const {\n\treturn _username.isEditable(username);\n}\n\nvoid UserData::setBotVerifyDetails(Ui::BotVerifyDetails details) {\n\tif (!details) {\n\t\tif (_botVerifyDetails) {\n\t\t\t_botVerifyDetails = nullptr;\n\t\t\tsession().changes().peerUpdated(this, UpdateFlag::VerifyInfo);\n\t\t}\n\t} else if (!_botVerifyDetails) {\n\t\t_botVerifyDetails = std::make_unique<Ui::BotVerifyDetails>(details);\n\t\tsession().changes().peerUpdated(this, UpdateFlag::VerifyInfo);\n\t} else if (*_botVerifyDetails != details) {\n\t\t*_botVerifyDetails = details;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::VerifyInfo);\n\t}\n}\n\nvoid UserData::setBotVerifyDetailsIcon(DocumentId iconId) {\n\tif (!iconId) {\n\t\tsetBotVerifyDetails({});\n\t} else {\n\t\tauto info = _botVerifyDetails\n\t\t\t? *_botVerifyDetails\n\t\t\t: Ui::BotVerifyDetails();\n\t\tinfo.iconId = iconId;\n\t\tsetBotVerifyDetails(info);\n\t}\n}\n\nconst QString &UserData::phone() const {\n\treturn _phone;\n}\n\nUserData::ContactStatus UserData::contactStatus() const {\n\treturn _contactStatus;\n}\n\nbool UserData::isContact() const {\n\treturn (contactStatus() == ContactStatus::Contact);\n}\n\nUserData::CallsStatus UserData::callsStatus() const {\n\treturn _callsStatus;\n}\n\nint UserData::commonChatsCount() const {\n\treturn _commonChatsCount;\n}\n\nvoid UserData::setCallsStatus(CallsStatus callsStatus) {\n\tif (callsStatus != _callsStatus) {\n\t\t_callsStatus = callsStatus;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::HasCalls);\n\t}\n}\n\nData::Birthday UserData::birthday() const {\n\treturn _birthday;\n}\n\nvoid UserData::setBirthday(Data::Birthday value) {\n\tif (_birthday != value) {\n\t\t_birthday = value;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::Birthday);\n\n\t\tif (isSelf()) {\n\t\t\tsession().api().sensitiveContent().reload(true);\n\t\t}\n\t}\n}\n\nvoid UserData::setBirthday(const tl::conditional<MTPBirthday> &value) {\n\tif (!value) {\n\t\tsetBirthday(Data::Birthday());\n\t} else {\n\t\tconst auto &data = value->data();\n\t\tsetBirthday(Data::Birthday(\n\t\t\tdata.vday().v,\n\t\t\tdata.vmonth().v,\n\t\t\tdata.vyear().value_or_empty()));\n\t}\n}\n\nbool UserData::hasCalls() const {\n\treturn (callsStatus() != CallsStatus::Disabled)\n\t\t&& (callsStatus() != CallsStatus::Unknown);\n}\n\nvoid UserData::setDisallowedGiftTypes(Api::DisallowedGiftTypes types) {\n\tif (_disallowedGiftTypes != types) {\n\t\t_disallowedGiftTypes = types;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::GiftSettings);\n\t}\n}\n\nconst TextWithEntities &UserData::note() const {\n\treturn _note;\n}\n\nvoid UserData::setNote(const TextWithEntities &note) {\n\tif (_note != note) {\n\t\t_note = note;\n\t\tsession().changes().peerUpdated(this, UpdateFlag::ContactNote);\n\t}\n}\n\nnamespace Data {\n\nvoid ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) {\n\tconst auto profilePhoto = update.vprofile_photo()\n\t\t? user->owner().processPhoto(*update.vprofile_photo()).get()\n\t\t: nullptr;\n\tconst auto personalPhoto = update.vpersonal_photo()\n\t\t? user->owner().processPhoto(*update.vpersonal_photo()).get()\n\t\t: nullptr;\n\tif (personalPhoto && profilePhoto) {\n\t\tuser->session().api().peerPhoto().registerNonPersonalPhoto(\n\t\t\tuser,\n\t\t\tprofilePhoto);\n\t} else {\n\t\tuser->session().api().peerPhoto().unregisterNonPersonalPhoto(user);\n\t}\n\tif (const auto photo = update.vfallback_photo()) {\n\t\tconst auto data = user->owner().processPhoto(*photo);\n\t\tif (!data->isNull()) { // Sometimes there is photoEmpty :shrug:\n\t\t\tuser->session().storage().add(Storage::UserPhotosSetBack(\n\t\t\t\tpeerToUser(user->id),\n\t\t\t\tdata->id\n\t\t\t));\n\t\t}\n\t}\n\tuser->setBarSettings(update.vsettings());\n\tuser->owner().notifySettings().apply(user, update.vnotify_settings());\n\n\tuser->setMessagesTTL(update.vttl_period().value_or_empty());\n\tif (const auto info = update.vbot_info()) {\n\t\tuser->setBotInfo(*info);\n\t} else {\n\t\tuser->setBotInfoVersion(-1);\n\t}\n\tif (const auto info = user->botInfo.get()) {\n\t\tinfo->canManageEmojiStatus = update.is_bot_can_manage_emoji_status();\n\t\tuser->setStarRefProgram(\n\t\t\tData::ParseStarRefProgram(update.vstarref_program()));\n\t}\n\tif (const auto pinned = update.vpinned_msg_id()) {\n\t\tSetTopPinnedMessageId(user, pinned->v);\n\t}\n\tuser->setStarsPerMessage(\n\t\tupdate.vsend_paid_messages_stars().value_or_empty());\n\tusing Flag = UserDataFlag;\n\tconst auto mask = Flag::Blocked\n\t\t| Flag::HasPhoneCalls\n\t\t| Flag::PhoneCallsPrivate\n\t\t| Flag::CanPinMessages\n\t\t| Flag::VoiceMessagesForbidden\n\t\t| Flag::ReadDatesPrivate\n\t\t| (update.is_contact_require_premium()\n\t\t\t? Flag::HasRequirePremiumToWrite\n\t\t\t: Flag())\n\t\t| (user->starsPerMessage() ? Flag::HasStarsPerMessage : Flag())\n\t\t| Flag::MessageMoneyRestrictionsKnown\n\t\t| Flag::RequiresPremiumToWrite\n\t\t| Flag::UnofficialSecurityRisk;\n\tuser->setFlags((user->flags() & ~mask)\n\t\t| (update.is_phone_calls_private()\n\t\t\t? Flag::PhoneCallsPrivate\n\t\t\t: Flag())\n\t\t| (update.is_phone_calls_available() ? Flag::HasPhoneCalls : Flag())\n\t\t| (update.is_can_pin_message() ? Flag::CanPinMessages : Flag())\n\t\t| (update.is_blocked() ? Flag::Blocked : Flag())\n\t\t| (update.is_voice_messages_forbidden()\n\t\t\t? Flag::VoiceMessagesForbidden\n\t\t\t: Flag())\n\t\t| (update.is_read_dates_private() ? Flag::ReadDatesPrivate : Flag())\n\t\t| (user->starsPerMessage() ? Flag::HasStarsPerMessage : Flag())\n\t\t| Flag::MessageMoneyRestrictionsKnown\n\t\t| (update.is_contact_require_premium()\n\t\t\t? (Flag::RequiresPremiumToWrite | Flag::HasRequirePremiumToWrite)\n\t\t\t: Flag())\n\t\t| (update.is_unofficial_security_risk()\n\t\t\t? Flag::UnofficialSecurityRisk\n\t\t\t: Flag()));\n\tuser->setIsBlocked(update.is_blocked());\n\tuser->setCallsStatus(update.is_phone_calls_private()\n\t\t? UserData::CallsStatus::Private\n\t\t: update.is_phone_calls_available()\n\t\t? UserData::CallsStatus::Enabled\n\t\t: UserData::CallsStatus::Disabled);\n\tuser->setAbout(qs(update.vabout().value_or_empty()));\n\tuser->setCommonChatsCount(update.vcommon_chats_count().v);\n\tuser->setPeerGiftsCount(update.vstargifts_count().value_or_empty());\n\tuser->checkFolder(update.vfolder_id().value_or_empty());\n\tif (const auto theme = update.vtheme()) {\n\t\ttheme->match([&](const MTPDchatTheme &data) {\n\t\t\tuser->setThemeToken(qs(data.vemoticon()));\n\t\t}, [&](const MTPDchatThemeUniqueGift &data) {\n\t\t\tuser->setThemeToken(\n\t\t\t\tuser->owner().cloudThemes().processGiftThemeGetToken(data));\n\t\t});\n\t} else {\n\t\tuser->setThemeToken(QString());\n\t}\n\tuser->setTranslationDisabled(update.is_translations_disabled());\n\tuser->setPrivateForwardName(\n\t\tupdate.vprivate_forward_name().value_or_empty());\n\n\tif (const auto info = user->botInfo.get()) {\n\t\tconst auto group = update.vbot_group_admin_rights()\n\t\t\t? ChatAdminRightsInfo(*update.vbot_group_admin_rights()).flags\n\t\t\t: ChatAdminRights();\n\t\tconst auto channel = update.vbot_broadcast_admin_rights()\n\t\t\t? ChatAdminRightsInfo(\n\t\t\t\t*update.vbot_broadcast_admin_rights()).flags\n\t\t\t: ChatAdminRights();\n\t\tif (info->groupAdminRights != group\n\t\t\t|| info->channelAdminRights != channel) {\n\t\t\tinfo->groupAdminRights = group;\n\t\t\tinfo->channelAdminRights = channel;\n\t\t\tuser->session().changes().peerUpdated(\n\t\t\t\tuser,\n\t\t\t\tData::PeerUpdate::Flag::Rights);\n\t\t}\n\t\tif (info->canEditInformation) {\n\t\t\tstatic constexpr auto kTimeout = crl::time(60000);\n\t\t\tconst auto id = user->id;\n\t\t\tconst auto weak = base::make_weak(&user->session());\n\t\t\tconst auto creditsLoadLifetime\n\t\t\t\t= std::make_shared<rpl::lifetime>();\n\t\t\tconst auto creditsLoad\n\t\t\t\t= creditsLoadLifetime->make_state<Api::CreditsStatus>(user);\n\t\t\tcreditsLoad->request({}, [=](Data::CreditsStatusSlice slice) {\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->credits().apply(id, slice.balance);\n\t\t\t\t}\n\t\t\t\tcreditsLoadLifetime->destroy();\n\t\t\t});\n\t\t\tbase::timer_once(kTimeout) | rpl::on_next([=] {\n\t\t\t\tcreditsLoadLifetime->destroy();\n\t\t\t}, *creditsLoadLifetime);\n\t\t\tconst auto currencyLoadLifetime\n\t\t\t\t= std::make_shared<rpl::lifetime>();\n\t\t\tconst auto currencyLoad\n\t\t\t\t= currencyLoadLifetime->make_state<Api::EarnStatistics>(user);\n\t\t\tconst auto apply = [=](const CreditsAmount &balance) {\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->credits().applyCurrency(id, balance);\n\t\t\t\t}\n\t\t\t\tcurrencyLoadLifetime->destroy();\n\t\t\t};\n\t\t\tcurrencyLoad->request() | rpl::on_error_done(\n\t\t\t\t[=](const QString &error) {\n\t\t\t\t\tapply(CreditsAmount(0, CreditsType::Ton));\n\t\t\t\t},\n\t\t\t\t[=] { apply(currencyLoad->data().currentBalance); },\n\t\t\t\t*currencyLoadLifetime);\n\t\t\tbase::timer_once(kTimeout) | rpl::on_next([=] {\n\t\t\t\tcurrencyLoadLifetime->destroy();\n\t\t\t}, *currencyLoadLifetime);\n\t\t}\n\t}\n\n\tif (const auto paper = update.vwallpaper()) {\n\t\tuser->setWallPaper(\n\t\t\tData::WallPaper::Create(&user->session(), *paper),\n\t\t\tupdate.is_wallpaper_overridden());\n\t} else {\n\t\tuser->setWallPaper({});\n\t}\n\n\tuser->setBusinessDetails(FromMTP(\n\t\t&user->owner(),\n\t\tupdate.vbusiness_work_hours(),\n\t\tupdate.vbusiness_location(),\n\t\tupdate.vbusiness_intro()));\n\tuser->setBirthday(update.vbirthday());\n\tuser->setPersonalChannel(\n\t\tupdate.vpersonal_channel_id().value_or_empty(),\n\t\tupdate.vpersonal_channel_message().value_or_empty());\n\tuser->setBotManagerId(update.vbot_manager_id().value_or_empty());\n\tif (user->isSelf()) {\n\t\tuser->owner().businessInfo().applyAwaySettings(\n\t\t\tFromMTP(&user->owner(), update.vbusiness_away_message()));\n\t\tuser->owner().businessInfo().applyGreetingSettings(\n\t\t\tFromMTP(&user->owner(), update.vbusiness_greeting_message()));\n\t}\n\tuser->setBotVerifyDetails(\n\t\tParseBotVerifyDetails(update.vbot_verification()));\n\tuser->setStarsRating(ParseStarsRating(update.vstars_rating()));\n\tif (user->isSelf()) {\n\t\tuser->owner().setPendingStarsRating({\n\t\t\t.value = ParseStarsRating(update.vstars_my_pending_rating()),\n\t\t\t.date = update.vstars_my_pending_rating_date().value_or_empty(),\n\t\t});\n\t}\n\n\tif (const auto gifts = update.vdisallowed_gifts()) {\n\t\tconst auto &data = gifts->data();\n\t\tuser->setDisallowedGiftTypes(Api::DisallowedGiftType()\n\t\t\t| (data.is_disallow_unlimited_stargifts()\n\t\t\t\t? Api::DisallowedGiftType::Unlimited\n\t\t\t\t: Api::DisallowedGiftType())\n\t\t\t| (data.is_disallow_limited_stargifts()\n\t\t\t\t? Api::DisallowedGiftType::Limited\n\t\t\t\t: Api::DisallowedGiftType())\n\t\t\t| (data.is_disallow_unique_stargifts()\n\t\t\t\t? Api::DisallowedGiftType::Unique\n\t\t\t\t: Api::DisallowedGiftType())\n\t\t\t| (data.is_disallow_premium_gifts()\n\t\t\t\t? Api::DisallowedGiftType::Premium\n\t\t\t\t: Api::DisallowedGiftType())\n\t\t\t| (data.is_disallow_stargifts_from_channels()\n\t\t\t\t? Api::DisallowedGiftType::FromChannels\n\t\t\t\t: Api::DisallowedGiftType())\n\t\t\t| (update.is_display_gifts_button()\n\t\t\t\t? Api::DisallowedGiftType::SendHide\n\t\t\t\t: Api::DisallowedGiftType()));\n\t} else {\n\t\tuser->setDisallowedGiftTypes(Api::DisallowedGiftTypes()\n\t\t\t| (update.is_display_gifts_button()\n\t\t\t\t? Api::DisallowedGiftType::SendHide\n\t\t\t\t: Api::DisallowedGiftType()));\n\t}\n\n\tuser->owner().stories().apply(user, update.vstories());\n\tuser->owner().savedMusic().apply(user, update.vsaved_music());\n\n\tif (const auto note = update.vnote()) {\n\t\tuser->setNote(TextWithEntities{\n\t\t\tqs(note->data().vtext()),\n\t\t\tApi::EntitiesFromMTP(\n\t\t\t\t&user->session(),\n\t\t\t\tnote->data().ventities().v)\n\t\t});\n\t} else {\n\t\tuser->setNote(TextWithEntities());\n\t}\n\n\tuser->setNoForwardsFlags(\n\t\tupdate.is_noforwards_my_enabled(),\n\t\tupdate.is_noforwards_peer_enabled());\n\n\tuser->fullUpdated();\n}\n\nStarRefProgram ParseStarRefProgram(const MTPStarRefProgram *program) {\n\tif (!program) {\n\t\treturn {};\n\t}\n\tauto result = StarRefProgram();\n\tconst auto &data = program->data();\n\tresult.commission = data.vcommission_permille().v;\n\tresult.durationMonths = data.vduration_months().value_or_empty();\n\tresult.revenuePerUser = CreditsAmountFromTL(\n\t\tdata.vdaily_revenue_per_user());\n\tresult.endDate = data.vend_date().value_or_empty();\n\treturn result;\n}\n\nUi::BotVerifyDetails ParseBotVerifyDetails(const MTPBotVerification *info) {\n\tif (!info) {\n\t\treturn {};\n\t}\n\tconst auto &data = info->data();\n\tconst auto description = qs(data.vdescription());\n\tconst auto flags = TextParseLinks;\n\treturn {\n\t\t.botId = UserId(data.vbot_id().v),\n\t\t.iconId = DocumentId(data.vicon().v),\n\t\t.description = TextUtilities::ParseEntities(description, flags),\n\t};\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_user.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"core/credits_amount.h\"\n#include \"data/components/credits.h\"\n#include \"data/data_birthday.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"data/data_lastseen_status.h\"\n#include \"data/data_user_names.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"base/flags.h\"\n\nnamespace Data {\nclass Forum;\nstruct BotCommand;\nstruct BusinessDetails;\n} // namespace Data\n\nnamespace Api {\nenum class DisallowedGiftType : uchar;\nusing DisallowedGiftTypes = base::flags<DisallowedGiftType>;\n} // namespace Api\n\nstruct StarRefProgram {\n\tCreditsAmount revenuePerUser;\n\tTimeId endDate = 0;\n\tushort commission = 0;\n\tuint8 durationMonths = 0;\n\n\tfriend inline constexpr bool operator==(\n\t\tStarRefProgram,\n\t\tStarRefProgram) = default;\n};\n\nstruct BotVerifierSettings {\n\tDocumentId iconId = 0;\n\tQString company;\n\tQString customDescription;\n\tbool canModifyDescription = false;\n\n\texplicit operator bool() const {\n\t\treturn iconId != 0;\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst BotVerifierSettings &a,\n\t\tconst BotVerifierSettings &b) = default;\n};\n\nstruct BotInfo {\n\tenum class SetBotPhotoOpenState : uchar {\n\t\tUnknown,\n\t\tOpenedWithHistory,\n\t\tOpenedEmpty,\n\t};\n\n\tBotInfo();\n\t~BotInfo();\n\n\tvoid ensureForum(not_null<UserData*> that);\n\t[[nodiscard]] Data::Forum *forum() const;\n\t[[nodiscard]] std::unique_ptr<Data::Forum> takeForumData();\n\n\tQString description;\n\tQString inlinePlaceholder;\n\tstd::vector<Data::BotCommand> commands;\n\n\tPhotoData *photo = nullptr;\n\tDocumentData *document = nullptr;\n\n\tQString botMenuButtonText;\n\tQString botMenuButtonUrl;\n\tQString privacyPolicyUrl;\n\n\tQColor botAppColorTitleDay = QColor(0, 0, 0, 0);\n\tQColor botAppColorTitleNight = QColor(0, 0, 0, 0);\n\tQColor botAppColorBodyDay = QColor(0, 0, 0, 0);\n\tQColor botAppColorBodyNight = QColor(0, 0, 0, 0);\n\n\tQString startToken;\n\tDialogs::EntryState inlineReturnTo;\n\n\tChatAdminRights groupAdminRights;\n\tChatAdminRights channelAdminRights;\n\n\tStarRefProgram starRefProgram;\n\tstd::unique_ptr<BotVerifierSettings> verifierSettings;\n\n\tint version = 0;\n\tint descriptionVersion = 0;\n\tint activeUsers = 0;\n\tSetBotPhotoOpenState setBotPhotoOpenState = SetBotPhotoOpenState::Unknown;\n\tbool inited : 1 = false;\n\tbool readsAllHistory : 1 = false;\n\tbool cantJoinGroups : 1 = false;\n\tbool supportsAttachMenu : 1 = false;\n\tbool canEditInformation : 1 = false;\n\tbool canManageEmojiStatus : 1 = false;\n\tbool supportsBusiness : 1 = false;\n\tbool hasMainApp : 1 = false;\n\tbool userCreatesTopics : 1 = false;\n\tbool setBotPhotoHidden : 1 = false;\n\tbool canManageBots : 1 = false;\n\nprivate:\n\tstd::unique_ptr<Data::Forum> _forum;\n\n};\n\nenum class UserDataFlag : uint32 {\n\tContact = (1 << 0),\n\tMutualContact = (1 << 1),\n\tDeleted = (1 << 2),\n\tVerified = (1 << 3),\n\tScam = (1 << 4),\n\tFake = (1 << 5),\n\tBotInlineGeo = (1 << 6),\n\tBlocked = (1 << 7),\n\tHasPhoneCalls = (1 << 8),\n\tPhoneCallsPrivate = (1 << 9),\n\tSupport = (1 << 10),\n\tCanPinMessages = (1 << 11),\n\tDiscardMinPhoto = (1 << 12),\n\tSelf = (1 << 13),\n\tPremium = (1 << 14),\n\tUnofficialSecurityRisk = (1 << 15),\n\tVoiceMessagesForbidden = (1 << 16),\n\tPersonalPhoto = (1 << 17),\n\tStoriesHidden = (1 << 18),\n\tHasActiveStories = (1 << 19),\n\tHasUnreadStories = (1 << 20),\n\tRequiresPremiumToWrite = (1 << 21),\n\tHasRequirePremiumToWrite = (1 << 22),\n\tHasStarsPerMessage = (1 << 23),\n\tMessageMoneyRestrictionsKnown = (1 << 24),\n\tReadDatesPrivate = (1 << 25),\n\tStoriesCorrespondent = (1 << 26),\n\tForum = (1 << 27),\n\tHasActiveVideoStream = (1 << 28),\n\tNoForwardsMyEnabled = (1 << 29),\n\tNoForwardsPeerEnabled = (1 << 30),\n};\ninline constexpr bool is_flag_type(UserDataFlag) { return true; };\nusing UserDataFlags = base::flags<UserDataFlag>;\n\n[[nodiscard]] Data::LastseenStatus LastseenFromMTP(\n\tconst MTPUserStatus &status,\n\tData::LastseenStatus currentStatus);\n\nclass UserData final : public PeerData {\npublic:\n\tusing Flag = UserDataFlag;\n\tusing Flags = Data::Flags<UserDataFlags>;\n\n\tUserData(not_null<Data::Session*> owner, PeerId id);\n\t~UserData();\n\n\tvoid setPhoto(const MTPUserProfilePhoto &photo);\n\n\tvoid setName(\n\t\tconst QString &newFirstName,\n\t\tconst QString &newLastName,\n\t\tconst QString &newPhoneName,\n\t\tconst QString &newUsername);\n\tvoid setUsernames(const Data::Usernames &newUsernames);\n\n\tvoid setUsername(const QString &username);\n\tvoid setPhone(const QString &newPhone);\n\tvoid setBotInfoVersion(int version);\n\tvoid setBotInfo(const MTPBotInfo &info);\n\n\tvoid setNameOrPhone(const QString &newNameOrPhone);\n\n\tvoid madeAction(TimeId when); // pseudo-online\n\n\t[[nodiscard]] uint64 accessHash() const {\n\t\treturn _accessHash;\n\t}\n\tvoid setAccessHash(uint64 accessHash);\n\n\tauto flags() const {\n\t\treturn _flags.current();\n\t}\n\tauto flagsValue() const {\n\t\treturn _flags.value();\n\t}\n\tvoid setFlags(UserDataFlags which);\n\tvoid addFlags(UserDataFlags which);\n\tvoid removeFlags(UserDataFlags which);\n\n\t[[nodiscard]] bool isVerified() const;\n\t[[nodiscard]] bool isScam() const;\n\t[[nodiscard]] bool isFake() const;\n\t[[nodiscard]] bool isPremium() const;\n\t[[nodiscard]] bool isBotInlineGeo() const;\n\t[[nodiscard]] bool isBot() const;\n\t[[nodiscard]] bool isSupport() const;\n\t[[nodiscard]] bool isInaccessible() const;\n\t[[nodiscard]] bool applyMinPhoto() const;\n\t[[nodiscard]] bool hasPersonalPhoto() const;\n\t[[nodiscard]] bool hasStoriesHidden() const;\n\t[[nodiscard]] bool hasRequirePremiumToWrite() const;\n\t[[nodiscard]] bool hasStarsPerMessage() const;\n\t[[nodiscard]] bool requiresPremiumToWrite() const;\n\t[[nodiscard]] bool messageMoneyRestrictionsKnown() const;\n\t[[nodiscard]] bool canSendIgnoreMoneyRestrictions() const;\n\t[[nodiscard]] bool readDatesPrivate() const;\n\t[[nodiscard]] bool allowsForwarding() const;\n\tvoid setNoForwardsFlags(bool myEnabled, bool peerEnabled);\n\t[[nodiscard]] bool isForum() const {\n\t\treturn flags() & Flag::Forum;\n\t}\n\t[[nodiscard]] Data::Forum *forum() const {\n\t\treturn botInfo ? botInfo->forum() : nullptr;\n\t}\n\n\tvoid setStoriesCorrespondent(bool is);\n\t[[nodiscard]] bool storiesCorrespondent() const;\n\n\tvoid setStarsPerMessage(int stars);\n\t[[nodiscard]] int starsPerMessage() const;\n\n\tvoid setStarsRating(Data::StarsRating value);\n\t[[nodiscard]] Data::StarsRating starsRating() const;\n\n\t[[nodiscard]] bool canShareThisContact() const;\n\t[[nodiscard]] bool canAddContact() const;\n\n\t// In Data::Session::processUsers() we check only that.\n\t// When actually trying to share contact we perform\n\t// a full check by canShareThisContact() call.\n\t[[nodiscard]] bool canShareThisContactFast() const;\n\n\t[[nodiscard]] const QString &phone() const;\n\t[[nodiscard]] QString username() const;\n\t[[nodiscard]] QString editableUsername() const;\n\t[[nodiscard]] const std::vector<QString> &usernames() const;\n\t[[nodiscard]] bool isUsernameEditable(QString username) const;\n\n\tvoid setBotVerifyDetails(Ui::BotVerifyDetails details);\n\tvoid setBotVerifyDetailsIcon(DocumentId iconId);\n\t[[nodiscard]] Ui::BotVerifyDetails *botVerifyDetails() const {\n\t\treturn _botVerifyDetails.get();\n\t}\n\n\tenum class ContactStatus : char {\n\t\tUnknown,\n\t\tContact,\n\t\tNotContact,\n\t};\n\t[[nodiscard]] ContactStatus contactStatus() const;\n\t[[nodiscard]] bool isContact() const;\n\tvoid setIsContact(bool is);\n\n\t[[nodiscard]] Data::LastseenStatus lastseen() const;\n\tbool updateLastseen(Data::LastseenStatus value);\n\n\tenum class CallsStatus : char {\n\t\tUnknown,\n\t\tEnabled,\n\t\tDisabled,\n\t\tPrivate,\n\t};\n\tCallsStatus callsStatus() const;\n\tbool hasCalls() const;\n\tvoid setCallsStatus(CallsStatus callsStatus);\n\n\t[[nodiscard]] Data::Birthday birthday() const;\n\tvoid setBirthday(Data::Birthday value);\n\tvoid setBirthday(const tl::conditional<MTPBirthday> &value);\n\n\t[[nodiscard]] int commonChatsCount() const;\n\tvoid setCommonChatsCount(int count);\n\n\t[[nodiscard]] int peerGiftsCount() const;\n\tvoid setPeerGiftsCount(int count);\n\n\t[[nodiscard]] bool hasPrivateForwardName() const;\n\t[[nodiscard]] QString privateForwardName() const;\n\tvoid setPrivateForwardName(const QString &name);\n\n\t[[nodiscard]] bool hasActiveStories() const;\n\t[[nodiscard]] bool hasUnreadStories() const;\n\t[[nodiscard]] bool hasActiveVideoStream() const;\n\tvoid setStoriesState(StoriesState state);\n\n\t[[nodiscard]] const Data::BusinessDetails &businessDetails() const;\n\tvoid setBusinessDetails(Data::BusinessDetails details);\n\n\tvoid setStarRefProgram(StarRefProgram program);\n\n\t[[nodiscard]] ChannelId personalChannelId() const;\n\t[[nodiscard]] MsgId personalChannelMessageId() const;\n\tvoid setPersonalChannel(ChannelId channelId, MsgId messageId);\n\n\t[[nodiscard]] UserId botManagerId() const;\n\tvoid setBotManagerId(UserId managerId);\n\n\t[[nodiscard]] bool unofficialSecurityRisk() const {\n\t\treturn flags() & Flag::UnofficialSecurityRisk;\n\t}\n\n\t[[nodiscard]] MTPInputUser inputUser() const;\n\n\tQString firstName;\n\tQString lastName;\n\tQString nameOrPhone;\n\n\tstd::unique_ptr<BotInfo> botInfo;\n\n\t[[nodiscard]] Api::DisallowedGiftTypes disallowedGiftTypes() const {\n\t\treturn _disallowedGiftTypes;\n\t}\n\tvoid setDisallowedGiftTypes(Api::DisallowedGiftTypes types);\n\n\t[[nodiscard]] const TextWithEntities &note() const;\n\tvoid setNote(const TextWithEntities &note);\n\nprivate:\n\tauto unavailableReasons() const\n\t\t-> const std::vector<Data::UnavailableReason> & override;\n\n\tvoid setUnavailableReasonsList(\n\t\tstd::vector<Data::UnavailableReason> &&reasons) override;\n\n\tFlags _flags;\n\tData::LastseenStatus _lastseen;\n\tData::Birthday _birthday;\n\tint _commonChatsCount = 0;\n\tint _peerGiftsCount = 0;\n\tint _starsPerMessage = 0;\n\tContactStatus _contactStatus = ContactStatus::Unknown;\n\tCallsStatus _callsStatus = CallsStatus::Unknown;\n\n\tData::UsernamesInfo _username;\n\n\tstd::unique_ptr<Data::BusinessDetails> _businessDetails;\n\tstd::vector<Data::UnavailableReason> _unavailableReasons;\n\tQString _phone;\n\tQString _privateForwardName;\n\tstd::unique_ptr<Ui::BotVerifyDetails> _botVerifyDetails;\n\tData::StarsRating _starsRating;\n\n\tChannelId _personalChannelId = 0;\n\tMsgId _personalChannelMessageId = 0;\n\tUserId _botManagerId = 0;\n\n\tuint64 _accessHash = 0;\n\tstatic constexpr auto kInaccessibleAccessHashOld\n\t\t= 0xFFFFFFFFFFFFFFFFULL;\n\n\tApi::DisallowedGiftTypes _disallowedGiftTypes;\n\tTextWithEntities _note;\n\n};\n\nnamespace Data {\n\nvoid ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update);\n\n[[nodiscard]] StarRefProgram ParseStarRefProgram(\n\tconst MTPStarRefProgram *program);\n\n[[nodiscard]] Ui::BotVerifyDetails ParseBotVerifyDetails(\n\tconst MTPBotVerification *info);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_user_names.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_user_names.h\"\n\nnamespace Data {\n\nUsernamesInfo::UsernamesInfo() = default;\n\nvoid UsernamesInfo::setUsername(const QString &username) {\n\tif (_usernames.empty()) {\n\t\tif (username.isEmpty()) {\n\t\t\t_indexEditableUsername = -1;\n\t\t} else {\n\t\t\t_usernames.push_back(username);\n\t\t\t_indexEditableUsername = 0;\n\t\t}\n\t} else if ((_indexEditableUsername < 0)\n\t\t\t|| (_indexEditableUsername >= _usernames.size())) {\n\t\tif (username.isEmpty()) {\n\t\t\t_indexEditableUsername = -1;\n\t\t} else {\n\t\t\t_usernames.push_back(username);\n\t\t\t_indexEditableUsername = 0;\n\t\t}\n\t} else if (_usernames[_indexEditableUsername] != username) {\n\t\tif (username.isEmpty()) {\n\t\t\t_usernames.erase(begin(_usernames) + _indexEditableUsername);\n\t\t\t_indexEditableUsername = -1;\n\t\t} else {\n\t\t\t_usernames[_indexEditableUsername] = username;\n\t\t}\n\t}\n}\n\nvoid UsernamesInfo::setUsernames(const Usernames &usernames) {\n\tauto editableUsername = QString();\n\tauto newUsernames = ranges::views::all(\n\t\tusernames\n\t) | ranges::views::filter([&](const Data::Username &username) {\n\t\tif (username.editable) {\n\t\t\teditableUsername = username.username;\n\t\t\treturn username.active;\n\t\t}\n\t\treturn username.active;\n\t}) | ranges::views::transform([](const Data::Username &username) {\n\t\treturn username.username;\n\t}) | ranges::to_vector;\n\n\tif (!ranges::equal(_usernames, newUsernames)) {\n\t\t_usernames = std::move(newUsernames);\n\t}\n\tif (!editableUsername.isEmpty()) {\n\t\tfor (auto i = 0; i < _usernames.size(); i++) {\n\t\t\tif (_usernames[i] == editableUsername) {\n\t\t\t\t_indexEditableUsername = i;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} else {\n\t\t_indexEditableUsername = -1;\n\t}\n}\n\nQString UsernamesInfo::username() const {\n\treturn _usernames.empty() ? QString() : _usernames.front();\n}\n\nQString UsernamesInfo::editableUsername() const {\n\treturn (_indexEditableUsername < 0)\n\t\t? QString()\n\t\t: _usernames[_indexEditableUsername];\n}\n\nconst std::vector<QString> &UsernamesInfo::usernames() const {\n\treturn _usernames;\n}\n\nbool UsernamesInfo::isEditable(const QString &username) const {\n\treturn (_indexEditableUsername >= 0)\n\t\t&& (_indexEditableUsername < _usernames.size())\n\t\t&& (_usernames[_indexEditableUsername] == username);\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_user_names.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\nstruct Username final {\n\tQString username;\n\tbool active = false;\n\tbool editable = false;\n};\n\nusing Usernames = std::vector<Username>;\n\nclass UsernamesInfo final {\npublic:\n\tUsernamesInfo();\n\n\tvoid setUsername(const QString &username);\n\tvoid setUsernames(const Usernames &usernames);\n\n\t[[nodiscard]] QString username() const;\n\t[[nodiscard]] QString editableUsername() const;\n\t[[nodiscard]] const std::vector<QString> &usernames() const;\n\t[[nodiscard]] bool isEditable(const QString &username) const;\n\nprivate:\n\tstd::vector<QString> _usernames;\n\tint _indexEditableUsername = -1;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_user_photos.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_user_photos.h\"\n\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"api/api_peer_photo.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"storage/storage_facade.h\"\n#include \"storage/storage_user_photos.h\"\n\nclass UserPhotosSliceBuilder {\npublic:\n\tusing Key = UserPhotosSlice::Key;\n\n\tUserPhotosSliceBuilder(Key key, int limitBefore, int limitAfter);\n\n\tbool applyUpdate(const Storage::UserPhotosResult &update);\n\tbool applyUpdate(const Storage::UserPhotosSliceUpdate &update);\n\tvoid checkInsufficientPhotos();\n\tauto insufficientPhotosAround() const {\n\t\treturn _insufficientPhotosAround.events();\n\t}\n\n\tUserPhotosSlice snapshot() const;\n\nprivate:\n\tvoid mergeSliceData(\n\t\tstd::optional<int> count,\n\t\tconst std::deque<PhotoId> &photoIds,\n\t\tstd::optional<int> skippedBefore,\n\t\tint skippedAfter);\n\tvoid sliceToLimits();\n\n\tKey _key;\n\tstd::deque<PhotoId> _ids;\n\tstd::optional<int> _fullCount;\n\tstd::optional<int> _skippedBefore;\n\tint _skippedAfter = 0;\n\tint _limitBefore = 0;\n\tint _limitAfter = 0;\n\n\trpl::event_stream<Api::PeerPhoto::UserPhotoId> _insufficientPhotosAround;\n\n};\n\nUserPhotosSlice::UserPhotosSlice(Key key)\n: UserPhotosSlice(\n\tkey,\n\t{},\n\tstd::nullopt,\n\tstd::nullopt,\n\tstd::nullopt) {\n}\n\nUserPhotosSlice::UserPhotosSlice(\n\tKey key,\n\tstd::deque<PhotoId> &&ids,\n\tstd::optional<int> fullCount,\n\tstd::optional<int> skippedBefore,\n\tstd::optional<int> skippedAfter)\n: AbstractSparseIds<std::deque<PhotoId>>(\n\tids,\n\tfullCount,\n\tskippedBefore,\n\tskippedAfter)\n, _key(key) {\n}\n\nstd::optional<int> UserPhotosSlice::distance(\n\t\tconst Key &a,\n\t\tconst Key &b) const {\n\tif (a.userId != _key.userId\n\t\t|| b.userId != _key.userId) {\n\t\treturn std::nullopt;\n\t}\n\tif (const auto i = indexOf(a.photoId)) {\n\t\tif (const auto j = indexOf(b.photoId)) {\n\t\t\treturn *j - *i;\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nUserPhotosSliceBuilder::UserPhotosSliceBuilder(\n\tKey key,\n\tint limitBefore,\n\tint limitAfter)\n: _key(key)\n, _limitBefore(limitBefore)\n, _limitAfter(limitAfter) {\n}\n\nbool UserPhotosSliceBuilder::applyUpdate(const Storage::UserPhotosResult &update) {\n\tmergeSliceData(\n\t\tupdate.count,\n\t\tupdate.photoIds,\n\t\tupdate.skippedBefore,\n\t\tupdate.skippedAfter);\n\treturn true;\n}\n\nbool UserPhotosSliceBuilder::applyUpdate(const Storage::UserPhotosSliceUpdate &update) {\n\tif (update.userId != _key.userId) {\n\t\treturn false;\n\t}\n\tconst auto idsCount = update.photoIds ? int(update.photoIds->size()) : 0;\n\tmergeSliceData(\n\t\tupdate.count,\n\t\tupdate.photoIds ? *update.photoIds : std::deque<PhotoId> {},\n\t\tupdate.count | func::add(-idsCount),\n\t\t0);\n\treturn true;\n}\n\nvoid UserPhotosSliceBuilder::checkInsufficientPhotos() {\n\tsliceToLimits();\n}\n\nvoid UserPhotosSliceBuilder::mergeSliceData(\n\t\tstd::optional<int> count,\n\t\tconst std::deque<PhotoId> &photoIds,\n\t\tstd::optional<int> skippedBefore,\n\t\tint skippedAfter) {\n\tif (photoIds.empty()) {\n\t\tif (_fullCount != count) {\n\t\t\t_fullCount = count;\n\t\t\tif (_fullCount && *_fullCount <= _ids.size()) {\n\t\t\t\t_fullCount = _ids.size();\n\t\t\t\t_skippedBefore = _skippedAfter = 0;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (count) {\n\t\t\t_fullCount = count;\n\t\t}\n\t\t_skippedAfter = skippedAfter;\n\t\t_ids = photoIds;\n\n\t\tif (_fullCount) {\n\t\t\t_skippedBefore = *_fullCount\n\t\t\t\t- _skippedAfter\n\t\t\t\t- int(_ids.size());\n\t\t}\n\t}\n\tsliceToLimits();\n}\n\nvoid UserPhotosSliceBuilder::sliceToLimits() {\n\tconst auto aroundIt = ranges::find(_ids, _key.photoId);\n\tconst auto removeFromBegin = (aroundIt - _ids.begin() - _limitBefore);\n\tconst auto removeFromEnd = (_ids.end() - aroundIt - _limitAfter - 1);\n\tif (removeFromEnd > 0) {\n\t\t_ids.erase(_ids.end() - removeFromEnd, _ids.end());\n\t\t_skippedAfter += removeFromEnd;\n\t}\n\tif (removeFromBegin > 0) {\n\t\t_ids.erase(_ids.begin(), _ids.begin() + removeFromBegin);\n\t\tif (_skippedBefore) {\n\t\t\t*_skippedBefore += removeFromBegin;\n\t\t}\n\t} else if (removeFromBegin < 0\n\t\t&& (!_skippedBefore || *_skippedBefore > 0)) {\n\t\t_insufficientPhotosAround.fire(_ids.empty() ? 0 : _ids.front());\n\t}\n}\n\nUserPhotosSlice UserPhotosSliceBuilder::snapshot() const {\n\treturn UserPhotosSlice(\n\t\t_key,\n\t\tbase::duplicate(_ids),\n\t\t_fullCount,\n\t\t_skippedBefore,\n\t\t_skippedAfter);\n}\n\nrpl::producer<UserPhotosSlice> UserPhotosViewer(\n\t\tnot_null<Main::Session*> session,\n\t\tUserPhotosSlice::Key key,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tconst auto builder = lifetime.make_state<UserPhotosSliceBuilder>(\n\t\t\tkey,\n\t\t\tlimitBefore,\n\t\t\tlimitAfter);\n\t\tconst auto applyUpdate = [=](auto &&update) {\n\t\t\tif (builder->applyUpdate(std::forward<decltype(update)>(update))) {\n\t\t\t\tconsumer.put_next(builder->snapshot());\n\t\t\t}\n\t\t};\n\t\tauto requestPhotosAround = [user = session->data().user(key.userId)](\n\t\t\t\tApi::PeerPhoto::UserPhotoId photoId) {\n\t\t\tuser->session().api().peerPhoto().requestUserPhotos(\n\t\t\t\tuser,\n\t\t\t\tphotoId);\n\t\t};\n\t\tbuilder->insufficientPhotosAround()\n\t\t\t| rpl::on_next(std::move(requestPhotosAround), lifetime);\n\n\t\tsession->storage().userPhotosSliceUpdated()\n\t\t\t| rpl::on_next(applyUpdate, lifetime);\n\n\t\tsession->storage().query(Storage::UserPhotosQuery(\n\t\t\tkey,\n\t\t\tlimitBefore,\n\t\t\tlimitAfter\n\t\t)) | rpl::on_next_done(\n\t\t\tapplyUpdate,\n\t\t\t[=] { builder->checkInsufficientPhotos(); },\n\t\t\tlifetime);\n\n\t\treturn lifetime;\n\t};\n}\n\n\nrpl::producer<UserPhotosSlice> UserPhotosReversedViewer(\n\t\tnot_null<Main::Session*> session,\n\t\tUserPhotosSlice::Key key,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\treturn UserPhotosViewer(\n\t\tsession,\n\t\tkey,\n\t\tlimitBefore,\n\t\tlimitAfter\n\t) | rpl::map([](UserPhotosSlice &&slice) {\n\t\tslice.reverse();\n\t\treturn std::move(slice);\n\t});\n}\n\nstd::optional<PhotoId> SyncUserFallbackPhotoViewer(not_null<UserData*> user) {\n\tauto syncLifetime = rpl::lifetime();\n\tauto result = std::optional<PhotoId>(std::nullopt);\n\n\tconstexpr auto kFallbackCount = 1;\n\tuser->session().storage().query(Storage::UserPhotosQuery(\n\t\tStorage::UserPhotosKey(peerToUser(user->id), true),\n\t\tkFallbackCount,\n\t\tkFallbackCount\n\t)) | rpl::on_next([&](Storage::UserPhotosResult &&slice) {\n\t\tif (slice.photoIds.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tresult = slice.photoIds.front();\n\t}, syncLifetime);\n\treturn result;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_user_photos.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_abstract_sparse_ids.h\"\n#include \"storage/storage_user_photos.h\"\n#include \"base/weak_ptr.h\"\n\nclass UserData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nclass UserPhotosSlice final : public AbstractSparseIds<std::deque<PhotoId>> {\npublic:\n\tusing Key = Storage::UserPhotosKey;\n\n\tUserPhotosSlice(Key key);\n\tUserPhotosSlice(\n\t\tKey key,\n\t\tstd::deque<PhotoId> &&ids,\n\t\tstd::optional<int> fullCount,\n\t\tstd::optional<int> skippedBefore,\n\t\tstd::optional<int> skippedAfter);\n\n\tstd::optional<int> distance(const Key &a, const Key &b) const;\n\tconst Key &key() const { return _key; }\n\nprivate:\n\tKey _key;\n\n\tfriend class UserPhotosSliceBuilder;\n\n};\n\nrpl::producer<UserPhotosSlice> UserPhotosViewer(\n\tnot_null<Main::Session*> session,\n\tUserPhotosSlice::Key key,\n\tint limitBefore,\n\tint limitAfter);\n\nrpl::producer<UserPhotosSlice> UserPhotosReversedViewer(\n\tnot_null<Main::Session*> session,\n\tUserPhotosSlice::Key key,\n\tint limitBefore,\n\tint limitAfter);\n\n[[nodiscard]] std::optional<PhotoId> SyncUserFallbackPhotoViewer(\n\tnot_null<UserData*> user);\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_wall_paper.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_wall_paper.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_session.h\"\n#include \"storage/serialize_common.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/color_int_conversion.h\"\n#include \"core/application.h\"\n#include \"main/main_session.h\"\n\nnamespace Ui {\n\nQColor ColorFromSerialized(MTPint serialized) {\n\treturn ColorFromSerialized(serialized.v);\n}\n\nstd::optional<QColor> MaybeColorFromSerialized(\n\t\tconst tl::conditional<MTPint> &mtp) {\n\treturn mtp ? ColorFromSerialized(*mtp) : std::optional<QColor>();\n}\n\n} // namespace Ui\n\nnamespace Data {\nnamespace {\n\nconstexpr auto FromLegacyBackgroundId(int32 legacyId) -> WallPaperId {\n\treturn uint64(0xFFFFFFFF00000000ULL) | uint64(uint32(legacyId));\n}\n\nconstexpr auto kUninitializedBackground = FromLegacyBackgroundId(-999);\nconstexpr auto kTestingThemeBackground = FromLegacyBackgroundId(-666);\nconstexpr auto kTestingDefaultBackground = FromLegacyBackgroundId(-665);\nconstexpr auto kTestingEditorBackground = FromLegacyBackgroundId(-664);\nconstexpr auto kThemeBackground = FromLegacyBackgroundId(-2);\nconstexpr auto kCustomBackground = FromLegacyBackgroundId(-1);\nconstexpr auto kLegacy1DefaultBackground = FromLegacyBackgroundId(0);\nconstexpr auto kLegacy2DefaultBackground = 5947530738516623361;\nconstexpr auto kLegacy3DefaultBackground = 5778236420632084488;\nconstexpr auto kLegacy4DefaultBackground = 5945087215657811969;\nconstexpr auto kDefaultBackground = 5933856211186221059;\nconstexpr auto kIncorrectDefaultBackground = FromLegacyBackgroundId(105);\n\nconstexpr auto kVersionTag = qint32(0x7FFFFFFF);\nconstexpr auto kVersion = 1;\n\nusing Ui::MaybeColorFromSerialized;\n\n[[nodiscard]] quint32 SerializeColor(const QColor &color) {\n\treturn (quint32(std::clamp(color.red(), 0, 255)) << 16)\n\t\t| (quint32(std::clamp(color.green(), 0, 255)) << 8)\n\t\t| quint32(std::clamp(color.blue(), 0, 255));\n}\n\n[[nodiscard]] quint32 SerializeMaybeColor(std::optional<QColor> color) {\n\treturn color ? SerializeColor(*color) : quint32(-1);\n}\n\n[[nodiscard]] std::vector<QColor> ColorsFromMTP(\n\t\tconst MTPDwallPaperSettings &data) {\n\tauto result = std::vector<QColor>();\n\tconst auto c1 = MaybeColorFromSerialized(data.vbackground_color());\n\tif (!c1) {\n\t\treturn result;\n\t}\n\tresult.reserve(4);\n\tresult.push_back(*c1);\n\tconst auto c2 = MaybeColorFromSerialized(\n\t\tdata.vsecond_background_color());\n\tif (!c2) {\n\t\treturn result;\n\t}\n\tresult.push_back(*c2);\n\tconst auto c3 = MaybeColorFromSerialized(data.vthird_background_color());\n\tif (!c3) {\n\t\treturn result;\n\t}\n\tresult.push_back(*c3);\n\tconst auto c4 = MaybeColorFromSerialized(\n\t\tdata.vfourth_background_color());\n\tif (!c4) {\n\t\treturn result;\n\t}\n\tresult.push_back(*c4);\n\treturn result;\n}\n\n[[nodiscard]] std::optional<QColor> ColorFromString(QStringView string) {\n\tif (string.size() != 6) {\n\t\treturn {};\n\t} else if (ranges::any_of(string, [](QChar ch) {\n\t\treturn (ch < 'a' || ch > 'f')\n\t\t\t&& (ch < 'A' || ch > 'F')\n\t\t\t&& (ch < '0' || ch > '9');\n\t})) {\n\t\treturn {};\n\t}\n\tconst auto component = [](QStringView text, int index) {\n\t\tconst auto decimal = [](QChar hex) {\n\t\t\tconst auto code = hex.unicode();\n\t\t\treturn (code >= '0' && code <= '9')\n\t\t\t\t? int(code - '0')\n\t\t\t\t: (code >= 'a' && code <= 'f')\n\t\t\t\t? int(code - 'a' + 0x0a)\n\t\t\t\t: int(code - 'A' + 0x0a);\n\t\t};\n\t\tindex *= 2;\n\t\treturn decimal(text[index]) * 0x10 + decimal(text[index + 1]);\n\t};\n\treturn QColor(\n\t\tcomponent(string, 0),\n\t\tcomponent(string, 1),\n\t\tcomponent(string, 2),\n\t\t255);\n}\n\n[[nodiscard]] std::vector<QColor> ColorsFromString(const QString &string) {\n\tconstexpr auto kMaxColors = 4;\n\tconst auto view = QStringView(string);\n\tconst auto count = int(view.size() / 6);\n\tif (!count || count > kMaxColors || view.size() != count * 7 - 1) {\n\t\treturn {};\n\t}\n\tauto result = std::vector<QColor>();\n\tresult.reserve(count);\n\tfor (auto i = 0; i != count; ++i) {\n\t\tif (i + 1 < count\n\t\t\t&& view[i * 7 + 6] != '~'\n\t\t\t&& (count > 2 || view[i * 7 + 6] != '-')) {\n\t\t\treturn {};\n\t\t} else if (const auto parsed = ColorFromString(view.mid(i * 7, 6))) {\n\t\t\tresult.push_back(*parsed);\n\t\t} else {\n\t\t\treturn {};\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] QString StringFromColor(QColor color) {\n\tconst auto component = [](int value) {\n\t\tconst auto hex = [](int value) {\n\t\t\tvalue = std::clamp(value, 0, 15);\n\t\t\treturn (value > 9)\n\t\t\t\t? QChar('a' + (value - 10))\n\t\t\t\t: QChar('0' + value);\n\t\t};\n\t\treturn QString() + hex(value / 16) + hex(value % 16);\n\t};\n\treturn component(color.red())\n\t\t+ component(color.green())\n\t\t+ component(color.blue());\n}\n\n[[nodiscard]] QString StringFromColors(const std::vector<QColor> &colors) {\n\tExpects(!colors.empty());\n\n\tauto strings = QStringList();\n\tstrings.reserve(colors.size());\n\tfor (const auto &color : colors) {\n\t\tstrings.push_back(StringFromColor(color));\n\t}\n\tconst auto separator = (colors.size() > 2) ? '~' : '-';\n\treturn strings.join(separator);\n}\n\n[[nodiscard]] qint32 RawFromLegacyFlags(qint32 legacyFlags) {\n\tusing Flag = WallPaperFlag;\n\treturn ((legacyFlags & (1 << 0)) ? qint32(Flag::Creator) : 0)\n\t\t| ((legacyFlags & (1 << 1)) ? qint32(Flag::Default) : 0)\n\t\t| ((legacyFlags & (1 << 3)) ? qint32(Flag::Pattern) : 0)\n\t\t| ((legacyFlags & (1 << 4)) ? qint32(Flag::Dark) : 0);\n}\n\n} // namespace\n\nWallPaper::WallPaper(WallPaperId id) : _id(id) {\n}\n\nvoid WallPaper::setLocalImageAsThumbnail(std::shared_ptr<Image> image) {\n\tExpects(IsDefaultWallPaper(*this)\n\t\t|| IsLegacy1DefaultWallPaper(*this)\n\t\t|| IsCustomWallPaper(*this));\n\tExpects(_thumbnail == nullptr);\n\n\t_thumbnail = std::move(image);\n}\n\nWallPaperId WallPaper::id() const {\n\treturn _id;\n}\n\nQString WallPaper::emojiId() const {\n\treturn _emojiId;\n}\n\nbool WallPaper::equals(const WallPaper &paper) const {\n\treturn (_flags == paper._flags)\n\t\t&& (_slug == paper._slug)\n\t\t&& (_emojiId == paper._emojiId)\n\t\t&& (_backgroundColors == paper._backgroundColors)\n\t\t&& (_rotation == paper._rotation)\n\t\t&& (_intensity == paper._intensity)\n\t\t&& (_blurred == paper._blurred)\n\t\t&& (_document == paper._document);\n}\n\nconst std::vector<QColor> WallPaper::backgroundColors() const {\n\treturn _backgroundColors;\n}\n\nDocumentData *WallPaper::document() const {\n\treturn _document;\n}\n\nImage *WallPaper::localThumbnail() const {\n\treturn _thumbnail.get();\n}\n\nbool WallPaper::isPattern() const {\n\treturn _flags & WallPaperFlag::Pattern;\n}\n\nbool WallPaper::isDefault() const {\n\treturn _flags & WallPaperFlag::Default;\n}\n\nbool WallPaper::isCreator() const {\n\treturn _flags & WallPaperFlag::Creator;\n}\n\nbool WallPaper::isDark() const {\n\treturn _flags & WallPaperFlag::Dark;\n}\n\nbool WallPaper::isLocal() const {\n\treturn !document() && _thumbnail;\n}\n\nbool WallPaper::isBlurred() const {\n\treturn _blurred;\n}\n\nint WallPaper::patternIntensity() const {\n\treturn _intensity;\n}\n\nfloat64 WallPaper::patternOpacity() const {\n\treturn _intensity / 100.;\n}\n\nint WallPaper::gradientRotation() const {\n\t// In case of complex gradients rotation value is dynamic.\n\treturn (_backgroundColors.size() < 3) ? _rotation : 0;\n}\n\nbool WallPaper::hasShareUrl() const {\n\treturn !_slug.isEmpty();\n}\n\nQStringList WallPaper::collectShareParams() const {\n\tauto result = QStringList();\n\tif (isPattern()) {\n\t\tif (!backgroundColors().empty()) {\n\t\t\tresult.push_back(\n\t\t\t\t\"bg_color=\" + StringFromColors(backgroundColors()));\n\t\t}\n\t\tif (_intensity) {\n\t\t\tresult.push_back(\"intensity=\" + QString::number(_intensity));\n\t\t}\n\t}\n\tif (_rotation && backgroundColors().size() == 2) {\n\t\tresult.push_back(\"rotation=\" + QString::number(_rotation));\n\t}\n\tauto mode = QStringList();\n\tif (_blurred) {\n\t\tmode.push_back(\"blur\");\n\t}\n\tif (!mode.isEmpty()) {\n\t\tresult.push_back(\"mode=\" + mode.join('+'));\n\t}\n\treturn result;\n}\n\nbool WallPaper::isNull() const {\n\treturn !_id && _slug.isEmpty() && _backgroundColors.empty();\n}\n\nQString WallPaper::key() const {\n\tif (isNull()) {\n\t\treturn QString();\n\t}\n\tconst auto base = _slug.isEmpty()\n\t\t? (_id\n\t\t\t? QString::number(_id)\n\t\t\t: StringFromColors(backgroundColors()))\n\t\t: (\"bg/\" + _slug);\n\tauto params = collectShareParams();\n\tif (_document && !isPattern()) {\n\t\tparams += u\"&intensity=\"_q + QString::number(_intensity);\n\t}\n\treturn params.isEmpty() ? base : (base + '?' + params.join('&'));\n}\n\nQString WallPaper::shareUrl(not_null<Main::Session*> session) const {\n\tif (!hasShareUrl()) {\n\t\treturn QString();\n\t}\n\tconst auto base = session->createInternalLinkFull(\"bg/\" + _slug);\n\tconst auto params = collectShareParams();\n\treturn params.isEmpty() ? base : (base + '?' + params.join('&'));\n}\n\nvoid WallPaper::loadDocumentThumbnail() const {\n\tif (_document) {\n\t\t_document->loadThumbnail(fileOrigin());\n\t}\n}\n\nvoid WallPaper::loadDocument() const {\n\tif (_document) {\n\t\t_document->save(fileOrigin(), QString());\n\t}\n}\n\nFileOrigin WallPaper::fileOrigin() const {\n\treturn FileOriginWallpaper(_id, _accessHash, _ownerId, _slug);\n}\n\nUserId WallPaper::ownerId() const {\n\treturn _ownerId;\n}\n\nMTPInputWallPaper WallPaper::mtpInput(not_null<Main::Session*> session) const {\n\treturn (_ownerId && _ownerId != session->userId() && !_slug.isEmpty())\n\t\t? MTP_inputWallPaperSlug(MTP_string(_slug))\n\t\t: MTP_inputWallPaper(MTP_long(_id), MTP_long(_accessHash));\n}\n\nMTPWallPaperSettings WallPaper::mtpSettings() const {\n\tconst auto serializeForIndex = [&](int index) {\n\t\treturn (_backgroundColors.size() > index)\n\t\t\t? MTP_int(SerializeColor(_backgroundColors[index]))\n\t\t\t: MTP_int(0);\n\t};\n\tusing Flag = MTPDwallPaperSettings::Flag;\n\tconst auto flagForIndex = [&](int index) {\n\t\treturn (_backgroundColors.size() <= index)\n\t\t\t? Flag(0)\n\t\t\t: (index == 0)\n\t\t\t? Flag::f_background_color\n\t\t\t: (index == 1)\n\t\t\t? Flag::f_second_background_color\n\t\t\t: (index == 2)\n\t\t\t? Flag::f_third_background_color\n\t\t\t: Flag::f_fourth_background_color;\n\t};\n\treturn MTP_wallPaperSettings(\n\t\tMTP_flags((_blurred ? Flag::f_blur : Flag(0))\n\t\t\t| Flag::f_intensity\n\t\t\t| Flag::f_rotation\n\t\t\t| (_emojiId.isEmpty() ? Flag() : Flag::f_emoticon)\n\t\t\t| flagForIndex(0)\n\t\t\t| flagForIndex(1)\n\t\t\t| flagForIndex(2)\n\t\t\t| flagForIndex(3)),\n\t\tserializeForIndex(0),\n\t\tserializeForIndex(1),\n\t\tserializeForIndex(2),\n\t\tserializeForIndex(3),\n\t\tMTP_int(_intensity),\n\t\tMTP_int(_rotation),\n\t\tMTP_string(_emojiId));\n}\n\nWallPaper WallPaper::withUrlParams(\n\t\tconst QMap<QString, QString> &params) const {\n\tauto result = *this;\n\tresult._blurred = false;\n\tresult._backgroundColors = ColorsFromString(_slug);\n\tresult._intensity = kDefaultIntensity;\n\tif (auto mode = params.value(\"mode\"); !mode.isEmpty()) {\n\t\tconst auto list = mode.replace('+', ' ').split(' ');\n\t\tfor (const auto &change : list) {\n\t\t\tif (change == u\"blur\"_q) {\n\t\t\t\tresult._blurred = true;\n\t\t\t}\n\t\t}\n\t}\n\tif (result._backgroundColors.empty()) {\n\t\tresult._backgroundColors = ColorsFromString(params.value(\"bg_color\"));\n\t}\n\tif (result._backgroundColors.empty()) {\n\t\tresult._backgroundColors = ColorsFromString(params.value(\"gradient\"));\n\t}\n\tif (result._backgroundColors.empty()) {\n\t\tresult._backgroundColors = ColorsFromString(params.value(\"color\"));\n\t}\n\tif (result._backgroundColors.empty()) {\n\t\tresult._backgroundColors = ColorsFromString(params.value(\"slug\"));\n\t}\n\tif (const auto string = params.value(\"intensity\"); !string.isEmpty()) {\n\t\tauto ok = false;\n\t\tconst auto intensity = string.toInt(&ok);\n\t\tif (ok && base::in_range(intensity, -100, 101)) {\n\t\t\tresult._intensity = intensity;\n\t\t}\n\t}\n\tresult._rotation = params.value(\"rotation\").toInt();\n\tresult._rotation = (std::clamp(result._rotation, 0, 315) / 45) * 45;\n\n\treturn result;\n}\n\nWallPaper WallPaper::withBlurred(bool blurred) const {\n\tauto result = *this;\n\tresult._blurred = blurred;\n\treturn result;\n}\n\nWallPaper WallPaper::withPatternIntensity(int intensity) const {\n\tauto result = *this;\n\tresult._intensity = intensity;\n\treturn result;\n}\n\nWallPaper WallPaper::withGradientRotation(int rotation) const {\n\tauto result = *this;\n\tresult._rotation = rotation;\n\treturn result;\n}\n\nWallPaper WallPaper::withBackgroundColors(std::vector<QColor> colors) const {\n\tauto result = *this;\n\tresult._backgroundColors = std::move(colors);\n\tif (!ColorsFromString(_slug).empty()) {\n\t\tresult._slug = StringFromColors(result._backgroundColors);\n\t}\n\treturn result;\n}\n\nWallPaper WallPaper::withParamsFrom(const WallPaper &other) const {\n\tauto result = *this;\n\tresult._blurred = other._blurred;\n\tif (!other._backgroundColors.empty()) {\n\t\tresult._backgroundColors = other._backgroundColors;\n\t\tif (!ColorsFromString(_slug).empty()) {\n\t\t\tresult._slug = StringFromColors(result._backgroundColors);\n\t\t}\n\t}\n\tresult._intensity = other._intensity;\n\tif (other.isPattern()) {\n\t\tresult._flags |= WallPaperFlag::Pattern;\n\t}\n\treturn result;\n}\n\nWallPaper WallPaper::withoutImageData() const {\n\tauto result = *this;\n\tresult._thumbnail = nullptr;\n\treturn result;\n}\n\nstd::optional<WallPaper> WallPaper::Create(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPWallPaper &data) {\n\treturn data.match([&](const MTPDwallPaper &data) {\n\t\treturn Create(session, data);\n\t}, [](const MTPDwallPaperNoFile &data) {\n\t\treturn Create(data);\n\t});\n}\n\nstd::optional<WallPaper> WallPaper::Create(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDwallPaper &data) {\n\tconst auto document = session->data().processDocument(\n\t\tdata.vdocument());\n\tif (!document->checkWallPaperProperties()) {\n\t\treturn std::nullopt;\n\t}\n\tauto result = WallPaper(data.vid().v);\n\tresult._accessHash = data.vaccess_hash().v;\n\tresult._ownerId = session->userId();\n\tresult._flags = (data.is_dark() ? WallPaperFlag::Dark : WallPaperFlag(0))\n\t\t| (data.is_pattern() ? WallPaperFlag::Pattern : WallPaperFlag(0))\n\t\t| (data.is_default() ? WallPaperFlag::Default : WallPaperFlag(0))\n\t\t| (data.is_creator() ? WallPaperFlag::Creator : WallPaperFlag(0));\n\tresult._slug = qs(data.vslug());\n\tresult._document = document;\n\tif (const auto settings = data.vsettings()) {\n\t\tsettings->match([&](const MTPDwallPaperSettings &data) {\n\t\t\tresult._blurred = data.is_blur();\n\t\t\tif (const auto intensity = data.vintensity()) {\n\t\t\t\tresult._intensity = intensity->v;\n\t\t\t}\n\t\t\tif (result.isPattern()) {\n\t\t\t\tresult._backgroundColors = ColorsFromMTP(data);\n\t\t\t\tif (const auto rotation = data.vrotation()) {\n\t\t\t\t\tresult._rotation = rotation->v;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\treturn result;\n}\n\nstd::optional<WallPaper> WallPaper::Create(const MTPDwallPaperNoFile &data) {\n\tauto result = WallPaper(data.vid().v);\n\tresult._flags = (data.is_dark() ? WallPaperFlag::Dark : WallPaperFlag(0))\n\t\t| (data.is_default() ? WallPaperFlag::Default : WallPaperFlag(0));\n\tresult._blurred = false;\n\tresult._backgroundColors.clear();\n\tif (const auto settings = data.vsettings()) {\n\t\tsettings->match([&](const MTPDwallPaperSettings &data) {\n\t\t\tresult._blurred = data.is_blur();\n\t\t\tresult._backgroundColors = ColorsFromMTP(data);\n\t\t\tif (const auto rotation = data.vrotation()) {\n\t\t\t\tresult._rotation = rotation->v;\n\t\t\t}\n\t\t\tresult._emojiId = qs(data.vemoticon().value_or_empty());\n\t\t});\n\t}\n\treturn result;\n}\n\nQByteArray WallPaper::serialize() const {\n\tauto size = sizeof(quint64) // _id\n\t\t+ sizeof(quint64) // _accessHash\n\t\t+ sizeof(qint32) // version tag\n\t\t+ sizeof(qint32) // version\n\t\t+ sizeof(qint32) // _flags\n\t\t+ Serialize::stringSize(_slug)\n\t\t+ sizeof(qint32) // _settings\n\t\t+ sizeof(qint32) // _backgroundColors.size()\n\t\t+ (_backgroundColors.size() * sizeof(quint32)) // _backgroundColors\n\t\t+ sizeof(qint32) // _intensity\n\t\t+ sizeof(qint32) // _rotation\n\t\t+ sizeof(quint64); // ownerId\n\n\tauto result = QByteArray();\n\tresult.reserve(size);\n\t{\n\t\tauto stream = QDataStream(&result, QIODevice::WriteOnly);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\t\tstream\n\t\t\t<< quint64(_id)\n\t\t\t<< quint64(_accessHash)\n\t\t\t<< qint32(kVersionTag)\n\t\t\t<< qint32(kVersion)\n\t\t\t<< qint32(_flags)\n\t\t\t<< _slug\n\t\t\t<< qint32(_blurred ? 1 : 0)\n\t\t\t<< qint32(_backgroundColors.size());\n\t\tfor (const auto &color : _backgroundColors) {\n\t\t\tstream << SerializeMaybeColor(color);\n\t\t}\n\t\tstream\n\t\t\t<< qint32(_intensity)\n\t\t\t<< qint32(_rotation)\n\t\t\t<< quint64(_ownerId.bare);\n\t}\n\treturn result;\n}\n\nstd::optional<WallPaper> WallPaper::FromSerialized(\n\t\tconst QByteArray &serialized) {\n\tif (serialized.isEmpty()) {\n\t\treturn std::nullopt;\n\t}\n\n\tauto id = quint64();\n\tauto accessHash = quint64();\n\tauto versionTag = qint32();\n\tauto version = qint32(0);\n\n\tauto stream = QDataStream(serialized);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\tstream\n\t\t>> id\n\t\t>> accessHash\n\t\t>> versionTag;\n\n\tauto flags = qint32();\n\tauto ownerId = UserId();\n\tauto slug = QString();\n\tauto blurred = qint32();\n\tauto backgroundColors = std::vector<QColor>();\n\tauto intensity = qint32();\n\tauto rotation = qint32();\n\tif (versionTag == kVersionTag) {\n\t\tauto bareOwnerId = quint64();\n\t\tauto backgroundColorsCount = qint32();\n\t\tstream\n\t\t\t>> version\n\t\t\t>> flags\n\t\t\t>> slug\n\t\t\t>> blurred\n\t\t\t>> backgroundColorsCount;\n\t\tif (backgroundColorsCount < 0 || backgroundColorsCount > 4) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\tbackgroundColors.reserve(backgroundColorsCount);\n\t\tfor (auto i = 0; i != backgroundColorsCount; ++i) {\n\t\t\tauto serialized = quint32();\n\t\t\tstream >> serialized;\n\t\t\tconst auto color = MaybeColorFromSerialized(serialized);\n\t\t\tif (!color) {\n\t\t\t\treturn std::nullopt;\n\t\t\t}\n\t\t\tbackgroundColors.push_back(*color);\n\t\t}\n\t\tstream\n\t\t\t>> intensity\n\t\t\t>> rotation\n\t\t\t>> bareOwnerId;\n\t\townerId = UserId(BareId(bareOwnerId));\n\t} else {\n\t\tauto settings = qint32();\n\t\tauto backgroundColor = quint32();\n\t\tstream\n\t\t\t>> slug\n\t\t\t>> settings\n\t\t\t>> backgroundColor\n\t\t\t>> intensity;\n\t\tif (!stream.atEnd()) {\n\t\t\tauto field1 = qint32();\n\t\t\tauto field2 = qint32();\n\t\t\tstream >> field1;\n\t\t\tif (!stream.atEnd()) {\n\t\t\t\tstream >> field2;\n\t\t\t}\n\t\t\townerId = UserId(\n\t\t\t\tBareId(uint32(field1)) | (BareId(uint32(field2)) << 32));\n\t\t}\n\t\tflags = RawFromLegacyFlags(versionTag);\n\t\tblurred = (settings & qint32(1U << 1)) ? 1 : 0;\n\t\tif (const auto color = MaybeColorFromSerialized(backgroundColor)) {\n\t\t\tbackgroundColors.push_back(*color);\n\t\t}\n\t}\n\tif (stream.status() != QDataStream::Ok) {\n\t\treturn std::nullopt;\n\t} else if (intensity < -100 || intensity > 100) {\n\t\treturn std::nullopt;\n\t}\n\tauto result = WallPaper(id);\n\tresult._accessHash = accessHash;\n\tresult._ownerId = ownerId;\n\tresult._flags = WallPaperFlags::from_raw(flags);\n\tresult._slug = slug;\n\tresult._blurred = (blurred == 1);\n\tresult._backgroundColors = std::move(backgroundColors);\n\tresult._intensity = intensity;\n\tresult._rotation = rotation;\n\treturn result;\n}\n\nstd::optional<WallPaper> WallPaper::FromLegacySerialized(\n\t\tquint64 id,\n\t\tquint64 accessHash,\n\t\tquint32 flags,\n\t\tQString slug) {\n\tauto result = WallPaper(id);\n\tresult._accessHash = accessHash;\n\tresult._flags = WallPaperFlags::from_raw(RawFromLegacyFlags(flags));\n\tresult._slug = slug;\n\tif (const auto color = ColorFromString(slug)) {\n\t\tresult._backgroundColors.push_back(*color);\n\t}\n\treturn result;\n}\n\nstd::optional<WallPaper> WallPaper::FromLegacyId(qint32 legacyId) {\n\tauto result = WallPaper(FromLegacyBackgroundId(legacyId));\n\tif (!IsCustomWallPaper(result)) {\n\t\tresult._flags = WallPaperFlag::Default;\n\t}\n\treturn result;\n}\n\nstd::optional<WallPaper> WallPaper::FromColorsSlug(const QString &slug) {\n\tauto colors = ColorsFromString(slug);\n\tif (colors.empty()) {\n\t\treturn std::nullopt;\n\t}\n\tauto result = CustomWallPaper();\n\tresult._slug = slug;\n\tresult._backgroundColors = std::move(colors);\n\treturn result;\n}\n\nWallPaper WallPaper::FromEmojiId(const QString &emojiId) {\n\tauto result = WallPaper(0);\n\tresult._emojiId = emojiId;\n\treturn result;\n}\n\nWallPaper WallPaper::ConstructDefault() {\n\tauto result = WallPaper(\n\t\tkDefaultBackground\n\t).withPatternIntensity(50).withBackgroundColors({\n\t\tQColor(219, 221, 187),\n\t\tQColor(107, 165, 135),\n\t\tQColor(213, 216, 141),\n\t\tQColor(136, 184, 132),\n\t});\n\tresult._flags |= WallPaperFlag::Default | WallPaperFlag::Pattern;\n\treturn result;\n}\n\nWallPaper ThemeWallPaper() {\n\treturn WallPaper(kThemeBackground);\n}\n\nbool IsThemeWallPaper(const WallPaper &paper) {\n\treturn (paper.id() == kThemeBackground);\n}\n\nWallPaper CustomWallPaper() {\n\treturn WallPaper(kCustomBackground);\n}\n\nbool IsCustomWallPaper(const WallPaper &paper) {\n\treturn (paper.id() == kCustomBackground);\n}\n\nWallPaper Legacy1DefaultWallPaper() {\n\treturn WallPaper(kLegacy1DefaultBackground);\n}\n\nbool IsLegacy1DefaultWallPaper(const WallPaper &paper) {\n\treturn (paper.id() == kLegacy1DefaultBackground);\n}\n\nbool IsLegacy2DefaultWallPaper(const WallPaper &paper) {\n\treturn (paper.id() == kLegacy2DefaultBackground)\n\t\t|| (paper.id() == kIncorrectDefaultBackground);\n}\n\nbool IsLegacy3DefaultWallPaper(const WallPaper &paper) {\n\treturn (paper.id() == kLegacy3DefaultBackground);\n}\n\nbool IsLegacy4DefaultWallPaper(const WallPaper &paper) {\n\treturn (paper.id() == kLegacy4DefaultBackground);\n}\n\nWallPaper DefaultWallPaper() {\n\treturn WallPaper::ConstructDefault();\n}\n\nbool IsDefaultWallPaper(const WallPaper &paper) {\n\treturn (paper.id() == kDefaultBackground);\n}\n\nbool IsCloudWallPaper(const WallPaper &paper) {\n\treturn (paper.id() != kIncorrectDefaultBackground)\n\t\t&& !IsThemeWallPaper(paper)\n\t\t&& !IsCustomWallPaper(paper)\n\t\t&& !IsLegacy1DefaultWallPaper(paper)\n\t\t&& !details::IsUninitializedWallPaper(paper)\n\t\t&& !details::IsTestingThemeWallPaper(paper)\n\t\t&& !details::IsTestingDefaultWallPaper(paper)\n\t\t&& !details::IsTestingEditorWallPaper(paper);\n}\n\nQImage GenerateDitheredGradient(const Data::WallPaper &paper) {\n\treturn Ui::GenerateDitheredGradient(\n\t\tpaper.backgroundColors(),\n\t\tpaper.gradientRotation());\n}\n\nnamespace details {\n\nWallPaper UninitializedWallPaper() {\n\treturn WallPaper(kUninitializedBackground);\n}\n\nbool IsUninitializedWallPaper(const WallPaper &paper) {\n\treturn (paper.id() == kUninitializedBackground);\n}\n\nWallPaper TestingThemeWallPaper() {\n\treturn WallPaper(kTestingThemeBackground);\n}\n\nbool IsTestingThemeWallPaper(const WallPaper &paper) {\n\treturn (paper.id() == kTestingThemeBackground);\n}\n\nWallPaper TestingDefaultWallPaper() {\n\treturn WallPaper(\n\t\tkTestingDefaultBackground\n\t).withParamsFrom(DefaultWallPaper());\n}\n\nbool IsTestingDefaultWallPaper(const WallPaper &paper) {\n\treturn (paper.id() == kTestingDefaultBackground);\n}\n\nWallPaper TestingEditorWallPaper() {\n\treturn WallPaper(kTestingEditorBackground);\n}\n\nbool IsTestingEditorWallPaper(const WallPaper &paper) {\n\treturn (paper.id() == kTestingEditorBackground);\n}\n\n} // namespace details\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_wall_paper.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n\nclass Image;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\n\n[[nodiscard]] QColor ColorFromSerialized(MTPint serialized);\n[[nodiscard]] std::optional<QColor> MaybeColorFromSerialized(\n\tconst tl::conditional<MTPint> &mtp);\n\n} // namespace Ui\n\nnamespace Data {\n\nstruct FileOrigin;\n\nenum class WallPaperFlag {\n\tPattern = (1 << 0),\n\tDefault = (1 << 1),\n\tCreator = (1 << 2),\n\tDark = (1 << 3),\n};\ninline constexpr bool is_flag_type(WallPaperFlag) { return true; };\nusing WallPaperFlags = base::flags<WallPaperFlag>;\n\nclass WallPaper {\npublic:\n\texplicit WallPaper(WallPaperId id);\n\n\tvoid setLocalImageAsThumbnail(std::shared_ptr<Image> image);\n\n\t[[nodiscard]] bool equals(const WallPaper &paper) const;\n\n\t[[nodiscard]] WallPaperId id() const;\n\t[[nodiscard]] QString emojiId() const;\n\t[[nodiscard]] bool isNull() const;\n\t[[nodiscard]] QString key() const;\n\t[[nodiscard]] const std::vector<QColor> backgroundColors() const;\n\t[[nodiscard]] DocumentData *document() const;\n\t[[nodiscard]] Image *localThumbnail() const;\n\t[[nodiscard]] bool isPattern() const;\n\t[[nodiscard]] bool isDefault() const;\n\t[[nodiscard]] bool isCreator() const;\n\t[[nodiscard]] bool isDark() const;\n\t[[nodiscard]] bool isLocal() const;\n\t[[nodiscard]] bool isBlurred() const;\n\t[[nodiscard]] int patternIntensity() const;\n\t[[nodiscard]] float64 patternOpacity() const;\n\t[[nodiscard]] int gradientRotation() const;\n\t[[nodiscard]] bool hasShareUrl() const;\n\t[[nodiscard]] QString shareUrl(not_null<Main::Session*> session) const;\n\n\tvoid loadDocument() const;\n\tvoid loadDocumentThumbnail() const;\n\t[[nodiscard]] FileOrigin fileOrigin() const;\n\n\t[[nodiscard]] UserId ownerId() const;\n\t[[nodiscard]] MTPInputWallPaper mtpInput(\n\t\tnot_null<Main::Session*> session) const;\n\t[[nodiscard]] MTPWallPaperSettings mtpSettings() const;\n\n\t[[nodiscard]] WallPaper withUrlParams(\n\t\tconst QMap<QString, QString> &params) const;\n\t[[nodiscard]] WallPaper withBlurred(bool blurred) const;\n\t[[nodiscard]] WallPaper withPatternIntensity(int intensity) const;\n\t[[nodiscard]] WallPaper withGradientRotation(int rotation) const;\n\t[[nodiscard]] WallPaper withBackgroundColors(\n\t\tstd::vector<QColor> colors) const;\n\t[[nodiscard]] WallPaper withParamsFrom(const WallPaper &other) const;\n\t[[nodiscard]] WallPaper withoutImageData() const;\n\n\t[[nodiscard]] static std::optional<WallPaper> Create(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPWallPaper &data);\n\t[[nodiscard]] static std::optional<WallPaper> Create(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDwallPaper &data);\n\t[[nodiscard]] static std::optional<WallPaper> Create(\n\t\tconst MTPDwallPaperNoFile &data);\n\n\t[[nodiscard]] QByteArray serialize() const;\n\t[[nodiscard]] static std::optional<WallPaper> FromSerialized(\n\t\tconst QByteArray &serialized);\n\t[[nodiscard]] static std::optional<WallPaper> FromLegacySerialized(\n\t\tquint64 id,\n\t\tquint64 accessHash,\n\t\tquint32 flags,\n\t\tQString slug);\n\t[[nodiscard]] static std::optional<WallPaper> FromLegacyId(\n\t\tqint32 legacyId);\n\t[[nodiscard]] static std::optional<WallPaper> FromColorsSlug(\n\t\tconst QString &slug);\n\t[[nodiscard]] static WallPaper FromEmojiId(const QString &emojiId);\n\t[[nodiscard]] static WallPaper ConstructDefault();\n\nprivate:\n\tstatic constexpr auto kDefaultIntensity = 50;\n\n\t[[nodiscard]] QStringList collectShareParams() const;\n\n\tWallPaperId _id = WallPaperId();\n\tuint64 _accessHash = 0;\n\tUserId _ownerId = 0;\n\tWallPaperFlags _flags;\n\tQString _slug;\n\tQString _emojiId;\n\n\tstd::vector<QColor> _backgroundColors;\n\tint _rotation = 0;\n\tint _intensity = kDefaultIntensity;\n\tbool _blurred = false;\n\n\tDocumentData *_document = nullptr;\n\tstd::shared_ptr<Image> _thumbnail;\n\n};\n\n[[nodiscard]] WallPaper ThemeWallPaper();\n[[nodiscard]] bool IsThemeWallPaper(const WallPaper &paper);\n[[nodiscard]] WallPaper CustomWallPaper();\n[[nodiscard]] bool IsCustomWallPaper(const WallPaper &paper);\n[[nodiscard]] WallPaper Legacy1DefaultWallPaper();\n[[nodiscard]] bool IsLegacy1DefaultWallPaper(const WallPaper &paper);\n[[nodiscard]] bool IsLegacy2DefaultWallPaper(const WallPaper &paper);\n[[nodiscard]] bool IsLegacy3DefaultWallPaper(const WallPaper &paper);\n[[nodiscard]] bool IsLegacy4DefaultWallPaper(const WallPaper &paper);\n[[nodiscard]] WallPaper DefaultWallPaper();\n[[nodiscard]] bool IsDefaultWallPaper(const WallPaper &paper);\n[[nodiscard]] bool IsCloudWallPaper(const WallPaper &paper);\n\n[[nodiscard]] QImage GenerateDitheredGradient(const WallPaper &paper);\n\nnamespace details {\n\n[[nodiscard]] WallPaper UninitializedWallPaper();\n[[nodiscard]] bool IsUninitializedWallPaper(const WallPaper &paper);\n[[nodiscard]] WallPaper TestingThemeWallPaper();\n[[nodiscard]] bool IsTestingThemeWallPaper(const WallPaper &paper);\n[[nodiscard]] WallPaper TestingDefaultWallPaper();\n[[nodiscard]] bool IsTestingDefaultWallPaper(const WallPaper &paper);\n[[nodiscard]] WallPaper TestingEditorWallPaper();\n[[nodiscard]] bool IsTestingEditorWallPaper(const WallPaper &paper);\n\n} // namespace details\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_web_page.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/data_web_page.h\"\n\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"mainwidget.h\"\n#include \"data/data_session.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_star_gift.h\"\n#include \"core/local_url_handlers.h\"\n#include \"lang/lang_keys.h\"\n#include \"iv/iv_data.h\"\n#include \"ui/image/image.h\"\n#include \"ui/text/text_entity.h\"\n\nnamespace {\n\n[[nodiscard]] WebPageCollage ExtractCollage(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst QVector<MTPPageBlock> &items,\n\t\tconst QVector<MTPPhoto> &photos,\n\t\tconst QVector<MTPDocument> &documents) {\n\tconst auto count = items.size();\n\tif (count < 2) {\n\t\treturn {};\n\t}\n\tconst auto bad = ranges::find_if(items, [](mtpTypeId type) {\n\t\treturn (type != mtpc_pageBlockPhoto && type != mtpc_pageBlockVideo);\n\t}, [](const MTPPageBlock &item) {\n\t\treturn item.type();\n\t});\n\tif (bad != items.end()) {\n\t\treturn {};\n\t}\n\n\tfor (const auto &photo : photos) {\n\t\towner->processPhoto(photo);\n\t}\n\tfor (const auto &document : documents) {\n\t\towner->processDocument(document);\n\t}\n\tauto result = WebPageCollage();\n\tresult.items.reserve(count);\n\tfor (const auto &item : items) {\n\t\tconst auto good = item.match([&](const MTPDpageBlockPhoto &data) {\n\t\t\tconst auto photo = owner->photo(data.vphoto_id().v);\n\t\t\tif (photo->isNull()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tresult.items.emplace_back(photo);\n\t\t\treturn true;\n\t\t}, [&](const MTPDpageBlockVideo &data) {\n\t\t\tconst auto document = owner->document(data.vvideo_id().v);\n\t\t\tif (!document->isVideoFile()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tresult.items.emplace_back(document);\n\t\t\treturn true;\n\t\t}, [](const auto &) -> bool {\n\t\t\tUnexpected(\"Type of block in Collage.\");\n\t\t});\n\t\tif (!good) {\n\t\t\treturn {};\n\t\t}\n\t}\n\treturn result;\n}\n\nWebPageCollage ExtractCollage(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst MTPDwebPage &data) {\n\tconst auto page = data.vcached_page();\n\tif (!page) {\n\t\treturn {};\n\t}\n\tconst auto processMedia = [&] {\n\t\tif (const auto photo = data.vphoto()) {\n\t\t\towner->processPhoto(*photo);\n\t\t}\n\t\tif (const auto document = data.vdocument()) {\n\t\t\towner->processDocument(*document);\n\t\t}\n\t};\n\treturn page->match([&](const auto &page) {\n\t\tfor (const auto &block : page.vblocks().v) {\n\t\t\tswitch (block.type()) {\n\t\t\tcase mtpc_pageBlockPhoto:\n\t\t\tcase mtpc_pageBlockVideo:\n\t\t\tcase mtpc_pageBlockCover:\n\t\t\tcase mtpc_pageBlockEmbed:\n\t\t\tcase mtpc_pageBlockEmbedPost:\n\t\t\tcase mtpc_pageBlockAudio:\n\t\t\t\treturn WebPageCollage();\n\t\t\tcase mtpc_pageBlockSlideshow:\n\t\t\t\tprocessMedia();\n\t\t\t\treturn ExtractCollage(\n\t\t\t\t\towner,\n\t\t\t\t\tblock.c_pageBlockSlideshow().vitems().v,\n\t\t\t\t\tpage.vphotos().v,\n\t\t\t\t\tpage.vdocuments().v);\n\t\t\tcase mtpc_pageBlockCollage:\n\t\t\t\tprocessMedia();\n\t\t\t\treturn ExtractCollage(\n\t\t\t\t\towner,\n\t\t\t\t\tblock.c_pageBlockCollage().vitems().v,\n\t\t\t\t\tpage.vphotos().v,\n\t\t\t\t\tpage.vdocuments().v);\n\t\t\tdefault: break;\n\t\t\t}\n\t\t}\n\t\treturn WebPageCollage();\n\t});\n}\n\n} // namespace\n\nWebPageType ParseWebPageType(\n\t\tconst QString &type,\n\t\tconst QString &embedUrl,\n\t\tbool hasIV) {\n\tif (type == u\"video\"_q || type == u\"gif\"_q || !embedUrl.isEmpty()) {\n\t\treturn WebPageType::Video;\n\t} else if (type == u\"photo\"_q) {\n\t\treturn WebPageType::Photo;\n\t} else if (type == u\"document\"_q) {\n\t\treturn WebPageType::Document;\n\t} else if (type == u\"profile\"_q) {\n\t\treturn WebPageType::Profile;\n\t} else if (type == u\"telegram_background\"_q) {\n\t\treturn WebPageType::WallPaper;\n\t} else if (type == u\"telegram_theme\"_q) {\n\t\treturn WebPageType::Theme;\n\t} else if (type == u\"telegram_story\"_q) {\n\t\treturn WebPageType::Story;\n\t} else if (type == u\"telegram_channel\"_q) {\n\t\treturn WebPageType::Channel;\n\t} else if (type == u\"telegram_channel_request\"_q) {\n\t\treturn WebPageType::ChannelWithRequest;\n\t} else if (type == u\"telegram_megagroup\"_q\n\t\t|| type == u\"telegram_chat\"_q) {\n\t\treturn WebPageType::Group;\n\t} else if (type == u\"telegram_megagroup_request\"_q\n\t\t|| type == u\"telegram_chat_request\"_q) {\n\t\treturn WebPageType::GroupWithRequest;\n\t} else if (type == u\"telegram_album\"_q) {\n\t\treturn WebPageType::Album;\n\t} else if (type == u\"telegram_message\"_q) {\n\t\treturn WebPageType::Message;\n\t} else if (type == u\"telegram_bot\"_q) {\n\t\treturn WebPageType::Bot;\n\t} else if (type == u\"telegram_voicechat\"_q) {\n\t\treturn WebPageType::VoiceChat;\n\t} else if (type == u\"telegram_livestream\"_q) {\n\t\treturn WebPageType::Livestream;\n\t} else if (type == u\"telegram_call\"_q) {\n\t\treturn WebPageType::ConferenceCall;\n\t} else if (type == u\"telegram_user\"_q) {\n\t\treturn WebPageType::User;\n\t} else if (type == u\"telegram_botapp\"_q) {\n\t\treturn WebPageType::BotApp;\n\t} else if (type == u\"telegram_channel_boost\"_q) {\n\t\treturn WebPageType::ChannelBoost;\n\t} else if (type == u\"telegram_group_boost\"_q) {\n\t\treturn WebPageType::GroupBoost;\n\t} else if (type == u\"telegram_giftcode\"_q) {\n\t\treturn WebPageType::Giftcode;\n\t} else if (type == u\"telegram_stickerset\"_q) {\n\t\treturn WebPageType::StickerSet;\n\t} else if (type == u\"telegram_story_album\"_q) {\n\t\treturn WebPageType::StoryAlbum;\n\t} else if (type == u\"telegram_collection\"_q) {\n\t\treturn WebPageType::GiftCollection;\n\t} else if (type == u\"telegram_auction\"_q) {\n\t\treturn WebPageType::Auction;\n\t} else if (type == u\"telegram_newbot\"_q) {\n\t\treturn WebPageType::NewBot;\n\t} else if (hasIV) {\n\t\treturn WebPageType::ArticleWithIV;\n\t} else {\n\t\treturn WebPageType::Article;\n\t}\n}\n\nbool IgnoreIv(WebPageType type) {\n\treturn !Iv::ShowButton()\n\t\t|| (type == WebPageType::Message)\n\t\t|| (type == WebPageType::Album);\n}\n\nWebPageType ParseWebPageType(const MTPDwebPage &page) {\n\treturn ParseWebPageType(\n\t\tqs(page.vtype().value_or_empty()),\n\t\tpage.vembed_url().value_or_empty(),\n\t\t!!page.vcached_page());\n}\n\nWebPageCollage::WebPageCollage(\n\tnot_null<Data::Session*> owner,\n\tconst MTPDwebPage &data)\n: WebPageCollage(ExtractCollage(owner, data)) {\n}\n\nWebPageData::WebPageData(not_null<Data::Session*> owner, const WebPageId &id)\n: id(id)\n, _owner(owner) {\n}\n\nWebPageData::~WebPageData() = default;\n\nData::Session &WebPageData::owner() const {\n\treturn *_owner;\n}\n\nMain::Session &WebPageData::session() const {\n\treturn _owner->session();\n}\n\nbool WebPageData::applyChanges(\n\t\tWebPageType newType,\n\t\tconst QString &newUrl,\n\t\tconst QString &newDisplayUrl,\n\t\tconst QString &newSiteName,\n\t\tconst QString &newTitle,\n\t\tconst TextWithEntities &newDescription,\n\t\tFullStoryId newStoryId,\n\t\tPhotoData *newPhoto,\n\t\tDocumentData *newDocument,\n\t\tWebPageCollage &&newCollage,\n\t\tstd::unique_ptr<Iv::Data> newIv,\n\t\tstd::unique_ptr<WebPageStickerSet> newStickerSet,\n\t\tstd::shared_ptr<Data::UniqueGift> newUniqueGift,\n\t\tstd::unique_ptr<WebPageAuction> newAuction,\n\t\tint newDuration,\n\t\tconst QString &newAuthor,\n\t\tbool newHasLargeMedia,\n\t\tbool newPhotoIsVideoCover,\n\t\tint newPendingTill) {\n\tif (newPendingTill != 0\n\t\t&& (!url.isEmpty() || failed)\n\t\t&& (!pendingTill\n\t\t\t|| pendingTill == newPendingTill\n\t\t\t|| newPendingTill < -1)) {\n\t\treturn false;\n\t}\n\n\tconst auto resultUrl = newUrl;\n\tconst auto resultDisplayUrl = newDisplayUrl;\n\tconst auto possibleSiteName = newSiteName;\n\tconst auto resultTitle = TextUtilities::SingleLine(newTitle);\n\tconst auto resultAuthor = newAuthor;\n\n\tconst auto viewTitleText = resultTitle.isEmpty()\n\t\t? TextUtilities::SingleLine(resultAuthor)\n\t\t: resultTitle;\n\tconst auto resultSiteName = [&] {\n\t\tif (!possibleSiteName.isEmpty()) {\n\t\t\treturn possibleSiteName;\n\t\t} else if (!newDescription.text.isEmpty()\n\t\t\t&& viewTitleText.isEmpty()\n\t\t\t&& !resultUrl.isEmpty()) {\n\t\t\treturn Iv::SiteNameFromUrl(resultUrl);\n\t\t}\n\t\treturn QString();\n\t}();\n\tconst auto hasSiteName = !resultSiteName.isEmpty() ? 1 : 0;\n\tconst auto hasTitle = !resultTitle.isEmpty() ? 1 : 0;\n\tconst auto hasDescription = !newDescription.text.isEmpty() ? 1 : 0;\n\tconst auto allowLargeMediaDocument = newDocument\n\t\t&& newDocument->isVideoFile()\n\t\t&& newPhoto;\n\tif ((!allowLargeMediaDocument && newDocument)\n\t\t|| !newCollage.items.empty()\n\t\t|| !newPhoto\n\t\t|| (hasSiteName + hasTitle + hasDescription < 2)) {\n\t\tnewHasLargeMedia = false;\n\t}\n\tif (!newDocument || !newDocument->isVideoFile() || !newPhoto) {\n\t\tnewPhotoIsVideoCover = false;\n\t}\n\n\tif (type == newType\n\t\t&& url == resultUrl\n\t\t&& displayUrl == resultDisplayUrl\n\t\t&& siteName == resultSiteName\n\t\t&& title == resultTitle\n\t\t&& description.text == newDescription.text\n\t\t&& storyId == newStoryId\n\t\t&& photo == newPhoto\n\t\t&& document == newDocument\n\t\t&& collage.items == newCollage.items\n\t\t&& (!iv == !newIv)\n\t\t&& (!iv || iv->partial() == newIv->partial())\n\t\t&& (!stickerSet == !newStickerSet)\n\t\t&& (!uniqueGift == !newUniqueGift)\n\t\t&& (!auction == !newAuction)\n\t\t&& duration == newDuration\n\t\t&& author == resultAuthor\n\t\t&& hasLargeMedia == (newHasLargeMedia ? 1 : 0)\n\t\t&& photoIsVideoCover == (newPhotoIsVideoCover ? 1 : 0)\n\t\t&& pendingTill == newPendingTill) {\n\t\treturn false;\n\t}\n\tif (pendingTill > 0 && newPendingTill <= 0) {\n\t\t_owner->session().api().clearWebPageRequest(this);\n\t}\n\ttype = newType;\n\thasLargeMedia = newHasLargeMedia ? 1 : 0;\n\tphotoIsVideoCover = newPhotoIsVideoCover ? 1 : 0;\n\turl = resultUrl;\n\tdisplayUrl = resultDisplayUrl;\n\tsiteName = resultSiteName;\n\ttitle = resultTitle;\n\tdescription = newDescription;\n\tstoryId = newStoryId;\n\tphoto = newPhoto;\n\tdocument = newDocument;\n\tcollage = std::move(newCollage);\n\tiv = std::move(newIv);\n\tstickerSet = std::move(newStickerSet);\n\tuniqueGift = std::move(newUniqueGift);\n\tauction = std::move(newAuction);\n\tduration = newDuration;\n\tauthor = resultAuthor;\n\tpendingTill = newPendingTill;\n\t++version;\n\n\tif (type == WebPageType::WallPaper && document) {\n\t\tdocument->checkWallPaperProperties();\n\t}\n\n\treplaceDocumentGoodThumbnail();\n\n\treturn true;\n}\n\nvoid WebPageData::replaceDocumentGoodThumbnail() {\n\tif (document && photo) {\n\t\tdocument->setGoodThumbnailPhoto(photo);\n\t}\n}\n\nvoid WebPageData::ApplyChanges(\n\t\tnot_null<Main::Session*> session,\n\t\tChannelData *channel,\n\t\tconst MTPmessages_Messages &result) {\n\tconst auto list = result.match([](\n\t\t\tconst MTPDmessages_messagesNotModified &) {\n\t\tLOG((\"API Error: received messages.messagesNotModified! \"\n\t\t\t\"(WebPageData::ApplyChanges)\"));\n\t\treturn static_cast<const QVector<MTPMessage>*>(nullptr);\n\t}, [&](const auto &data) {\n\t\tsession->data().processUsers(data.vusers());\n\t\tsession->data().processChats(data.vchats());\n\t\treturn &data.vmessages().v;\n\t});\n\tresult.match([&](\n\t\t\tconst MTPDmessages_channelMessages &data) {\n\t\tif (channel) {\n\t\t\tchannel->ptsReceived(data.vpts().v);\n\t\t\tchannel->processTopics(data.vtopics());\n\t\t} else {\n\t\t\tLOG((\"API Error: received messages.channelMessages \"\n\t\t\t\t\"when no channel was passed! (WebPageData::ApplyChanges)\"));\n\t\t}\n\t}, [&](const auto &) {\n\t});\n\tresult.match([](\n\t\t\tconst MTPDmessages_messagesNotModified &) {\n\t}, [&](const auto &data) {\n\t\tif (channel) {\n\t\t\tchannel->processTopics(data.vtopics());\n\t\t}\n\t});\n\tif (!list) {\n\t\treturn;\n\t}\n\n\tfor (const auto &message : *list) {\n\t\tmessage.match([&](const MTPDmessage &data) {\n\t\t\tif (const auto media = data.vmedia()) {\n\t\t\t\tmedia->match([&](const MTPDmessageMediaWebPage &data) {\n\t\t\t\t\tsession->data().processWebpage(data.vwebpage());\n\t\t\t\t}, [&](const auto &) {\n\t\t\t\t});\n\t\t\t}\n\t\t}, [&](const auto &) {\n\t\t});\n\t}\n\tsession->data().sendWebPageGamePollTodoListNotifications();\n}\n\nQString WebPageData::displayedSiteName() const {\n\treturn (document && document->isWallPaper())\n\t\t? tr::lng_media_chat_background(tr::now)\n\t\t: (document && document->isTheme())\n\t\t? tr::lng_media_color_theme(tr::now)\n\t\t: siteName;\n}\n\nTimeId WebPageData::extractVideoTimestamp() const {\n\tconst auto take = [&](const QStringList &list, int index) {\n\t\treturn (index >= 0 && index < list.size()) ? list[index] : QString();\n\t};\n\tconst auto hashed = take(url.split('#'), 0);\n\tconst auto params = take(hashed.split('?'), 1);\n\tconst auto parts = params.split('&');\n\tfor (const auto &part : parts) {\n\t\tif (part.startsWith(u\"t=\"_q)) {\n\t\t\treturn Core::ParseVideoTimestamp(part.mid(2));\n\t\t}\n\t}\n\treturn 0;\n}\n\nbool WebPageData::computeDefaultSmallMedia() const {\n\tif (!collage.items.empty()) {\n\t\treturn false;\n\t} else if (siteName.isEmpty()\n\t\t&& title.isEmpty()\n\t\t&& description.empty()\n\t\t&& author.isEmpty()) {\n\t\treturn false;\n\t} else if (!uniqueGift\n\t\t&& !document\n\t\t&& photo\n\t\t&& type != WebPageType::Photo\n\t\t&& type != WebPageType::Document\n\t\t&& type != WebPageType::Story\n\t\t&& type != WebPageType::Video) {\n\t\tif (type == WebPageType::Profile) {\n\t\t\treturn true;\n\t\t} else if (siteName == u\"Twitter\"_q\n\t\t\t|| siteName == u\"Facebook\"_q\n\t\t\t|| type == WebPageType::ArticleWithIV) {\n\t\t\treturn false;\n\t\t} else {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool WebPageData::suggestEnlargePhoto() const {\n\treturn !siteName.isEmpty() || !title.isEmpty() || !description.empty();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/data/data_web_page.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_document.h\"\n\nclass ChannelData;\n\nnamespace Data {\nclass Session;\nstruct UniqueGift;\nstruct StarGift;\n} // namespace Data\n\nnamespace Iv {\nclass Data;\n} // namespace Iv\n\nenum class WebPageType : uint8 {\n\tNone,\n\n\tMessage,\n\tAlbum,\n\n\tGroup,\n\tGroupWithRequest,\n\tGroupBoost,\n\tChannel,\n\tChannelWithRequest,\n\tChannelBoost,\n\tGiftcode,\n\n\tPhoto,\n\tVideo,\n\tDocument,\n\n\tUser,\n\tBot,\n\tProfile,\n\tBotApp,\n\n\tWallPaper,\n\tTheme,\n\tStory,\n\tStickerSet,\n\tStoryAlbum,\n\tGiftCollection,\n\tAuction,\n\tNewBot,\n\n\tArticle,\n\tArticleWithIV,\n\n\tVoiceChat,\n\tLivestream,\n\tConferenceCall,\n\n\tFactcheck,\n};\n[[nodiscard]] WebPageType ParseWebPageType(const MTPDwebPage &type);\n[[nodiscard]] bool IgnoreIv(WebPageType type);\n\nstruct WebPageCollage {\n\tusing Item = std::variant<PhotoData*, DocumentData*>;\n\n\tWebPageCollage() = default;\n\texplicit WebPageCollage(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst MTPDwebPage &data);\n\n\tstd::vector<Item> items;\n\n};\n\nstruct WebPageStickerSet {\n\tWebPageStickerSet() = default;\n\n\tstd::vector<not_null<DocumentData*>> items;\n\tbool isEmoji = false;\n\tbool isTextColor = false;\n\n};\n\nstruct WebPageAuction {\n\tstd::shared_ptr<Data::StarGift> auctionGift;\n\tTimeId endDate = 0;\n};\n\nstruct WebPageData {\n\tWebPageData(not_null<Data::Session*> owner, const WebPageId &id);\n\t~WebPageData();\n\n\t[[nodiscard]] Data::Session &owner() const;\n\t[[nodiscard]] Main::Session &session() const;\n\n\tbool applyChanges(\n\t\tWebPageType newType,\n\t\tconst QString &newUrl,\n\t\tconst QString &newDisplayUrl,\n\t\tconst QString &newSiteName,\n\t\tconst QString &newTitle,\n\t\tconst TextWithEntities &newDescription,\n\t\tFullStoryId newStoryId,\n\t\tPhotoData *newPhoto,\n\t\tDocumentData *newDocument,\n\t\tWebPageCollage &&newCollage,\n\t\tstd::unique_ptr<Iv::Data> newIv,\n\t\tstd::unique_ptr<WebPageStickerSet> newStickerSet,\n\t\tstd::shared_ptr<Data::UniqueGift> newUniqueGift,\n\t\tstd::unique_ptr<WebPageAuction> newAuction,\n\t\tint newDuration,\n\t\tconst QString &newAuthor,\n\t\tbool newHasLargeMedia,\n\t\tbool newPhotoIsVideoCover,\n\t\tint newPendingTill);\n\n\tstatic void ApplyChanges(\n\t\tnot_null<Main::Session*> session,\n\t\tChannelData *channel,\n\t\tconst MTPmessages_Messages &result);\n\n\t[[nodiscard]] QString displayedSiteName() const;\n\t[[nodiscard]] TimeId extractVideoTimestamp() const;\n\t[[nodiscard]] bool computeDefaultSmallMedia() const;\n\t[[nodiscard]] bool suggestEnlargePhoto() const;\n\n\tconst WebPageId id = 0;\n\tWebPageType type = WebPageType::None;\n\tQString url;\n\tQString displayUrl;\n\tQString siteName;\n\tQString title;\n\tTextWithEntities description;\n\tFullStoryId storyId;\n\tQString author;\n\tPhotoData *photo = nullptr;\n\tDocumentData *document = nullptr;\n\tWebPageCollage collage;\n\tstd::unique_ptr<Iv::Data> iv;\n\tstd::unique_ptr<WebPageStickerSet> stickerSet;\n\tstd::shared_ptr<Data::UniqueGift> uniqueGift;\n\tstd::unique_ptr<WebPageAuction> auction;\n\tint duration = 0;\n\tTimeId pendingTill = 0;\n\tuint32 version : 29 = 0;\n\tuint32 photoIsVideoCover : 1 = 0;\n\tuint32 hasLargeMedia : 1 = 0;\n\tuint32 failed : 1 = 0;\n\nprivate:\n\tvoid replaceDocumentGoodThumbnail();\n\n\tconst not_null<Data::Session*> _owner;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/data/notify/data_notify_settings.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/notify/data_notify_settings.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_ringtones.h\"\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"main/main_session.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n#include \"window/notifications_manager.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kMaxNotifyCheckDelay = 24 * 3600 * crl::time(1000);\n\n[[nodiscard]] bool MutedFromUntil(TimeId until, crl::time *changesIn) {\n\tconst auto now = base::unixtime::now();\n\tconst auto result = (until > now) ? (until - now) : 0;\n\tif (changesIn) {\n\t\t*changesIn = (result > 0)\n\t\t\t? std::min(result * crl::time(1000), kMaxNotifyCheckDelay)\n\t\t\t: kMaxNotifyCheckDelay;\n\t}\n\treturn (result > 0);\n}\n\n[[nodiscard]] bool SkipAddException(not_null<PeerData*> peer) {\n\tif (const auto user = peer->asUser()) {\n\t\treturn user->isInaccessible() || user->isSelf();\n\t} else if (const auto chat = peer->asChat()) {\n\t\treturn chat->isDeactivated() || chat->isForbidden();\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn channel->isForbidden();\n\t}\n\treturn false;\n}\n\n} // namespace\n\nDefaultNotify DefaultNotifyType(not_null<const PeerData*> peer) {\n\treturn peer->isUser()\n\t\t? DefaultNotify::User\n\t\t: (peer->isChat() || peer->isMegagroup())\n\t\t? DefaultNotify::Group\n\t\t: DefaultNotify::Broadcast;\n}\n\nMTPInputNotifyPeer DefaultNotifyToMTP(DefaultNotify type) {\n\tswitch (type) {\n\tcase DefaultNotify::User: return MTP_inputNotifyUsers();\n\tcase DefaultNotify::Group: return MTP_inputNotifyChats();\n\tcase DefaultNotify::Broadcast: return MTP_inputNotifyBroadcasts();\n\t}\n\tUnexpected(\"Default notify type in sendNotifySettingsUpdates\");\n}\n\nNotifySettings::NotifySettings(not_null<Session*> owner)\n: _owner(owner)\n, _unmuteByFinishedTimer([=] { unmuteByFinished(); }) {\n}\n\nvoid NotifySettings::request(not_null<PeerData*> peer) {\n\tif (peer->notify().settingsUnknown()) {\n\t\tpeer->session().api().requestNotifySettings(\n\t\t\tMTP_inputNotifyPeer(peer->input()));\n\t}\n\tif (defaultSettings(peer).settingsUnknown()) {\n\t\tpeer->session().api().requestNotifySettings(peer->isUser()\n\t\t\t? MTP_inputNotifyUsers()\n\t\t\t: (peer->isChat() || peer->isMegagroup())\n\t\t\t? MTP_inputNotifyChats()\n\t\t\t: MTP_inputNotifyBroadcasts());\n\t}\n}\n\nvoid NotifySettings::request(not_null<Thread*> thread) {\n\tif (const auto topic = thread->asTopic()) {\n\t\tif (topic->notify().settingsUnknown()) {\n\t\t\ttopic->session().api().requestNotifySettings(\n\t\t\t\tMTP_inputNotifyForumTopic(\n\t\t\t\t\ttopic->peer()->input(),\n\t\t\t\t\tMTP_int(topic->rootId())));\n\t\t}\n\t}\n\trequest(thread->peer());\n}\n\nvoid NotifySettings::apply(\n\t\tconst MTPNotifyPeer &notifyPeer,\n\t\tconst MTPPeerNotifySettings &settings) {\n\tnotifyPeer.match([&](const MTPDnotifyUsers &) {\n\t\tapply(DefaultNotify::User, settings);\n\t}, [&](const MTPDnotifyChats &) {\n\t\tapply(DefaultNotify::Group, settings);\n\t}, [&](const MTPDnotifyBroadcasts &) {\n\t\tapply(DefaultNotify::Broadcast, settings);\n\t}, [&](const MTPDnotifyPeer &data) {\n\t\tapply(peerFromMTP(data.vpeer()), settings);\n\t}, [&](const MTPDnotifyForumTopic &data) {\n\t\tapply(peerFromMTP(data.vpeer()), data.vtop_msg_id().v, settings);\n\t});\n}\n\nvoid NotifySettings::apply(\n\t\tconst MTPInputNotifyPeer &notifyPeer,\n\t\tconst MTPPeerNotifySettings &settings) {\n\tconst auto peerFromInput = [&](const MTPInputPeer &peer) {\n\t\treturn peer.match([&](const MTPDinputPeerSelf &) {\n\t\t\treturn _owner->session().userPeerId();\n\t\t}, [](const MTPDinputPeerUser &data) {\n\t\t\treturn peerFromUser(data.vuser_id());\n\t\t}, [](const MTPDinputPeerChat &data) {\n\t\t\treturn peerFromChat(data.vchat_id());\n\t\t}, [](const MTPDinputPeerChannel &data) {\n\t\t\treturn peerFromChannel(data.vchannel_id());\n\t\t}, [](const MTPDinputPeerUserFromMessage &data) -> PeerId {\n\t\t\treturn peerFromUser(data.vuser_id());\n\t\t}, [](const MTPDinputPeerChannelFromMessage &data) -> PeerId {\n\t\t\treturn peerFromChannel(data.vchannel_id());\n\t\t}, [](const MTPDinputPeerEmpty &) -> PeerId {\n\t\t\tUnexpected(\"Empty peer in NotifySettings::apply.\");\n\t\t});\n\t};\n\tnotifyPeer.match([&](const MTPDinputNotifyUsers &) {\n\t\tapply(DefaultNotify::User, settings);\n\t}, [&](const MTPDinputNotifyChats &) {\n\t\tapply(DefaultNotify::Group, settings);\n\t}, [&](const MTPDinputNotifyBroadcasts &) {\n\t\tapply(DefaultNotify::Broadcast, settings);\n\t}, [&](const MTPDinputNotifyPeer &data) {\n\t\tapply(peerFromInput(data.vpeer()), settings);\n\t}, [&](const MTPDinputNotifyForumTopic &data) {\n\t\tapply(peerFromInput(data.vpeer()), data.vtop_msg_id().v, settings);\n\t});\n}\n\nvoid NotifySettings::apply(\n\t\tDefaultNotify type,\n\t\tconst MTPPeerNotifySettings &settings) {\n\tif (defaultValue(type).settings.change(settings)) {\n\t\tupdateLocal(type);\n\t\tCore::App().notifications().checkDelayed();\n\t}\n}\n\nvoid NotifySettings::apply(\n\t\tPeerId peerId,\n\t\tconst MTPPeerNotifySettings &settings) {\n\tif (const auto peer = _owner->peerLoaded(peerId)) {\n\t\tapply(peer, settings);\n\t}\n}\n\nvoid NotifySettings::apply(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPPeerNotifySettings &settings) {\n\tif (peer->notify().change(settings)) {\n\t\tupdateException(peer);\n\t\tupdateLocal(peer);\n\t\tCore::App().notifications().checkDelayed();\n\t}\n}\n\nvoid NotifySettings::apply(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tconst MTPPeerNotifySettings &settings) {\n\tif (const auto peer = _owner->peerLoaded(peerId)) {\n\t\tif (const auto topic = peer->forumTopicFor(topicRootId)) {\n\t\t\tapply(topic, settings);\n\t\t}\n\t}\n}\n\nvoid NotifySettings::apply(\n\t\tnot_null<ForumTopic*> topic,\n\t\tconst MTPPeerNotifySettings &settings) {\n\tif (topic->notify().change(settings)) {\n\t\tupdateLocal(topic);\n\t\tCore::App().notifications().checkDelayed();\n\t}\n}\n\nvoid NotifySettings::update(\n\t\tnot_null<Thread*> thread,\n\t\tMuteValue muteForSeconds,\n\t\tstd::optional<bool> silentPosts,\n\t\tstd::optional<NotifySound> sound,\n\t\tstd::optional<bool> storiesMuted) {\n\tif (thread->notify().change(\n\t\t\tmuteForSeconds,\n\t\t\tsilentPosts,\n\t\t\tsound,\n\t\t\tstoriesMuted)) {\n\t\tif (const auto history = thread->asHistory()) {\n\t\t\tupdateException(history->peer);\n\t\t}\n\t\tupdateLocal(thread);\n\t\tthread->session().api().updateNotifySettingsDelayed(thread);\n\t}\n}\n\nvoid NotifySettings::resetToDefault(not_null<Thread*> thread) {\n\t// Duplicated in clearExceptions(type) and resetToDefault(peer).\n\tif (thread->notify().resetToDefault()) {\n\t\tif (const auto history = thread->asHistory()) {\n\t\t\tupdateException(history->peer);\n\t\t}\n\t\tupdateLocal(thread);\n\t\tthread->session().api().updateNotifySettingsDelayed(thread);\n\t\tCore::App().notifications().checkDelayed();\n\t}\n}\n\nvoid NotifySettings::update(\n\t\tnot_null<PeerData*> peer,\n\t\tMuteValue muteForSeconds,\n\t\tstd::optional<bool> silentPosts,\n\t\tstd::optional<NotifySound> sound,\n\t\tstd::optional<bool> storiesMuted) {\n\tif (peer->notify().change(\n\t\t\tmuteForSeconds,\n\t\t\tsilentPosts,\n\t\t\tsound,\n\t\t\tstoriesMuted)) {\n\t\tupdateException(peer);\n\t\tupdateLocal(peer);\n\t\tpeer->session().api().updateNotifySettingsDelayed(peer);\n\t}\n}\n\nvoid NotifySettings::resetToDefault(not_null<PeerData*> peer) {\n\t// Duplicated in clearExceptions(type) and resetToDefault(thread).\n\tif (peer->notify().resetToDefault()) {\n\t\tupdateException(peer);\n\t\tupdateLocal(peer);\n\t\tpeer->session().api().updateNotifySettingsDelayed(peer);\n\t\tCore::App().notifications().checkDelayed();\n\t}\n}\n\nvoid NotifySettings::forumParentMuteUpdated(not_null<Forum*> forum) {\n\tforum->enumerateTopics([&](not_null<ForumTopic*> topic) {\n\t\tif (!topic->notify().settingsUnknown()) {\n\t\t\tupdateLocal(topic);\n\t\t}\n\t});\n}\n\nauto NotifySettings::defaultValue(DefaultNotify type)\n-> DefaultValue & {\n\tconst auto index = static_cast<int>(type);\n\tAssert(index >= 0 && index < base::array_size(_defaultValues));\n\treturn _defaultValues[index];\n}\n\nauto NotifySettings::defaultValue(DefaultNotify type) const\n-> const DefaultValue & {\n\tconst auto index = static_cast<int>(type);\n\tAssert(index >= 0 && index < base::array_size(_defaultValues));\n\treturn _defaultValues[index];\n}\n\nconst PeerNotifySettings &NotifySettings::defaultSettings(\n\t\tnot_null<const PeerData*> peer) const {\n\treturn defaultSettings(DefaultNotifyType(peer));\n}\n\nconst PeerNotifySettings &NotifySettings::defaultSettings(\n\t\tDefaultNotify type) const {\n\treturn defaultValue(type).settings;\n}\n\nbool NotifySettings::isMuted(DefaultNotify type) const {\n\tif (const auto until = defaultSettings(type).muteUntil()) {\n\t\treturn MutedFromUntil(*until, nullptr);\n\t}\n\treturn true;\n}\n\nvoid NotifySettings::defaultUpdate(\n\t\tDefaultNotify type,\n\t\tMuteValue muteForSeconds,\n\t\tstd::optional<bool> silentPosts,\n\t\tstd::optional<NotifySound> sound,\n\t\tstd::optional<bool> storiesMuted) {\n\tauto &settings = defaultValue(type).settings;\n\tif (settings.change(muteForSeconds, silentPosts, sound, storiesMuted)) {\n\t\tupdateLocal(type);\n\t\t_owner->session().api().updateNotifySettingsDelayed(type);\n\t}\n}\n\nvoid NotifySettings::updateLocal(not_null<Thread*> thread) {\n\tconst auto topic = thread->asTopic();\n\tif (!topic) {\n\t\treturn updateLocal(thread->peer());\n\t}\n\tauto changesIn = crl::time(0);\n\tconst auto muted = isMuted(topic, &changesIn);\n\ttopic->setMuted(muted);\n\tif (muted) {\n\t\tauto &lifetime = _mutedTopics.emplace(\n\t\t\ttopic,\n\t\t\trpl::lifetime()).first->second;\n\t\ttopic->destroyed() | rpl::on_next([=] {\n\t\t\t_mutedTopics.erase(topic);\n\t\t}, lifetime);\n\t\tunmuteByFinishedDelayed(changesIn);\n\t\tCore::App().notifications().clearIncomingFromTopic(topic);\n\t} else {\n\t\t_mutedTopics.erase(topic);\n\t}\n\tcacheSound(topic->notify().sound());\n}\n\nvoid NotifySettings::updateLocal(not_null<PeerData*> peer) {\n\tconst auto history = _owner->historyLoaded(peer->id);\n\tauto changesIn = crl::time(0);\n\tconst auto muted = isMuted(peer, &changesIn);\n\tconst auto changeInHistory = history && (history->muted() != muted);\n\tif (changeInHistory) {\n\t\thistory->setMuted(muted);\n\t\t// Notification already sent.\n\t} else {\n\t\tpeer->session().changes().peerUpdated(\n\t\t\tpeer,\n\t\t\tPeerUpdate::Flag::Notifications);\n\t}\n\n\tif (muted) {\n\t\t_mutedPeers.emplace(peer);\n\t\tunmuteByFinishedDelayed(changesIn);\n\t\tif (history) {\n\t\t\tCore::App().notifications().clearIncomingFromHistory(history);\n\t\t}\n\t} else {\n\t\t_mutedPeers.erase(peer);\n\t}\n\tcacheSound(peer->notify().sound());\n}\n\nvoid NotifySettings::cacheSound(DocumentId id) {\n\tcacheSound(_owner->document(id));\n}\n\nvoid NotifySettings::cacheSound(not_null<DocumentData*> document) {\n\tif (document->isNull()) {\n\t\treturn;\n\t}\n\tconst auto view = document->createMediaView();\n\t_ringtones.views.emplace(document->id, view);\n\tdocument->forceToCache(true);\n\tdocument->save(FileOriginRingtones(), QString());\n}\n\nvoid NotifySettings::cacheSound(const std::optional<NotifySound> &sound) {\n\tif (!sound || !sound->id) {\n\t\treturn;\n\t} else if (const auto doc = _owner->document(sound->id); !doc->isNull()) {\n\t\tcacheSound(doc);\n\t\treturn;\n\t}\n\t_ringtones.pendingIds.push_back(sound->id);\n\tif (_ringtones.pendingLifetime) {\n\t\treturn;\n\t}\n\t// Not requested yet.\n\t_owner->session().api().ringtones().listUpdates(\n\t) | rpl::on_next([=] {\n\t\tfor (const auto id : base::take(_ringtones.pendingIds)) {\n\t\t\tcacheSound(id);\n\t\t}\n\t\t_ringtones.pendingLifetime.destroy();\n\t}, _ringtones.pendingLifetime);\n\t_owner->session().api().ringtones().requestList();\n}\n\nvoid NotifySettings::updateLocal(DefaultNotify type) {\n\tdefaultValue(type).updates.fire({});\n\n\tconst auto goodForUpdate = [&](\n\t\t\tnot_null<const PeerData*> peer,\n\t\t\tconst PeerNotifySettings &settings) {\n\t\tauto &peers = peer->notify();\n\t\treturn !peers.settingsUnknown()\n\t\t\t&& ((!peers.muteUntil() && settings.muteUntil())\n\t\t\t\t|| (!peers.silentPosts() && settings.silentPosts())\n\t\t\t\t|| (!peers.sound() && settings.sound()));\n\t};\n\n\tconst auto callback = [&](not_null<PeerData*> peer) {\n\t\tif (goodForUpdate(peer, defaultSettings(type))) {\n\t\t\tupdateLocal(peer);\n\t\t}\n\t};\n\tswitch (type) {\n\tcase DefaultNotify::User: _owner->enumerateUsers(callback); break;\n\tcase DefaultNotify::Group: _owner->enumerateGroups(callback); break;\n\tcase DefaultNotify::Broadcast:\n\t\t_owner->enumerateBroadcasts(callback);\n\t\tbreak;\n\t}\n\tcacheSound(defaultValue(type).settings.sound());\n}\n\nstd::shared_ptr<DocumentMedia> NotifySettings::lookupRingtone(\n\t\tDocumentId id) const {\n\tif (!id) {\n\t\treturn nullptr;\n\t}\n\tconst auto it = _ringtones.views.find(id);\n\treturn (it == end(_ringtones.views)) ? nullptr : it->second;\n}\n\nvoid NotifySettings::unmuteByFinishedDelayed(crl::time delay) {\n\taccumulate_min(delay, kMaxNotifyCheckDelay);\n\tif (!_unmuteByFinishedTimer.isActive()\n\t\t|| _unmuteByFinishedTimer.remainingTime() > delay) {\n\t\t_unmuteByFinishedTimer.callOnce(delay);\n\t}\n}\n\nvoid NotifySettings::unmuteByFinished() {\n\tauto changesInMin = crl::time(0);\n\tfor (auto i = begin(_mutedPeers); i != end(_mutedPeers);) {\n\t\tconst auto history = _owner->historyLoaded((*i)->id);\n\t\tauto changesIn = crl::time(0);\n\t\tconst auto muted = isMuted(*i, &changesIn);\n\t\tif (history) {\n\t\t\thistory->setMuted(muted);\n\t\t}\n\t\tif (muted) {\n\t\t\tif (!changesInMin || changesInMin > changesIn) {\n\t\t\t\tchangesInMin = changesIn;\n\t\t\t}\n\t\t\t++i;\n\t\t} else {\n\t\t\ti = _mutedPeers.erase(i);\n\t\t}\n\t}\n\tfor (auto i = begin(_mutedTopics); i != end(_mutedTopics);) {\n\t\tauto changesIn = crl::time(0);\n\t\tconst auto topic = i->first;\n\t\tconst auto muted = isMuted(topic, &changesIn);\n\t\ttopic->setMuted(muted);\n\t\tif (muted) {\n\t\t\tif (!changesInMin || changesInMin > changesIn) {\n\t\t\t\tchangesInMin = changesIn;\n\t\t\t}\n\t\t\t++i;\n\t\t} else {\n\t\t\ti = _mutedTopics.erase(i);\n\t\t}\n\t}\n\tif (changesInMin) {\n\t\tunmuteByFinishedDelayed(changesInMin);\n\t}\n}\n\nbool NotifySettings::isMuted(\n\t\tnot_null<const Thread*> thread,\n\t\tcrl::time *changesIn) const {\n\tconst auto topic = thread->asTopic();\n\tconst auto until = topic ? topic->notify().muteUntil() : std::nullopt;\n\treturn until\n\t\t? MutedFromUntil(*until, changesIn)\n\t\t: isMuted(thread->peer(), changesIn);\n}\n\nbool NotifySettings::isMuted(not_null<const Thread*> thread) const {\n\treturn isMuted(thread, nullptr);\n}\n\nNotifySound NotifySettings::sound(not_null<const Thread*> thread) const {\n\tconst auto topic = thread->asTopic();\n\tconst auto sound = topic ? topic->notify().sound() : std::nullopt;\n\treturn sound ? *sound : this->sound(thread->peer());\n}\n\nbool NotifySettings::muteUnknown(not_null<const Thread*> thread) const {\n\tconst auto topic = thread->asTopic();\n\treturn (topic && topic->notify().settingsUnknown())\n\t\t|| ((!topic || !topic->notify().muteUntil().has_value())\n\t\t\t&& muteUnknown(thread->peer()));\n}\n\nbool NotifySettings::soundUnknown(not_null<const Thread*> thread) const {\n\tconst auto topic = thread->asTopic();\n\treturn (topic && topic->notify().settingsUnknown())\n\t\t|| ((!topic || !topic->notify().sound().has_value())\n\t\t\t&& soundUnknown(topic->peer()));\n}\n\nbool NotifySettings::isMuted(\n\t\tnot_null<const PeerData*> peer,\n\t\tcrl::time *changesIn) const {\n\tif (const auto until = peer->notify().muteUntil()) {\n\t\treturn MutedFromUntil(*until, changesIn);\n\t} else if (const auto until = defaultSettings(peer).muteUntil()) {\n\t\treturn MutedFromUntil(*until, changesIn);\n\t}\n\treturn true;\n}\n\nbool NotifySettings::isMuted(not_null<const PeerData*> peer) const {\n\treturn isMuted(peer, nullptr);\n}\n\nbool NotifySettings::silentPosts(not_null<const PeerData*> peer) const {\n\tif (const auto silent = peer->notify().silentPosts()) {\n\t\treturn *silent;\n\t} else if (const auto silent = defaultSettings(peer).silentPosts()) {\n\t\treturn *silent;\n\t}\n\treturn false;\n}\n\nNotifySound NotifySettings::sound(not_null<const PeerData*> peer) const {\n\t// Explicitly ignore a notify sound for Saved Messages\n\t// to follow the global notify sound.\n\tif (const auto sound = peer->notify().sound(); !peer->isSelf() && sound) {\n\t\treturn *sound;\n\t} else if (const auto sound = defaultSettings(peer).sound()) {\n\t\treturn *sound;\n\t}\n\treturn {};\n}\n\nbool NotifySettings::muteUnknown(not_null<const PeerData*> peer) const {\n\treturn peer->notify().settingsUnknown()\n\t\t|| (!peer->notify().muteUntil().has_value()\n\t\t\t&& defaultSettings(peer).settingsUnknown());\n}\n\nbool NotifySettings::silentPostsUnknown(\n\t\tnot_null<const PeerData*> peer) const {\n\treturn peer->notify().settingsUnknown()\n\t\t|| (!peer->notify().silentPosts().has_value()\n\t\t\t&& defaultSettings(peer).settingsUnknown());\n}\n\nbool NotifySettings::soundUnknown(not_null<const PeerData*> peer) const {\n\treturn peer->notify().settingsUnknown()\n\t\t|| (!peer->notify().sound().has_value()\n\t\t\t&& defaultSettings(peer).settingsUnknown());\n}\n\nbool NotifySettings::settingsUnknown(not_null<const PeerData*> peer) const {\n\treturn muteUnknown(peer)\n\t\t|| silentPostsUnknown(peer)\n\t\t|| soundUnknown(peer);\n}\n\nbool NotifySettings::settingsUnknown(not_null<const Thread*> thread) const {\n\tconst auto topic = thread->asTopic();\n\treturn muteUnknown(thread)\n\t\t|| soundUnknown(thread)\n\t\t|| (!topic && silentPostsUnknown(thread->peer()));\n}\n\nrpl::producer<> NotifySettings::defaultUpdates(DefaultNotify type) const {\n\treturn defaultValue(type).updates.events();\n}\n\nrpl::producer<> NotifySettings::defaultUpdates(\n\t\tnot_null<const PeerData*> peer) const {\n\treturn defaultUpdates(peer->isUser()\n\t\t? DefaultNotify::User\n\t\t: (peer->isChat() || peer->isMegagroup())\n\t\t? DefaultNotify::Group\n\t\t: DefaultNotify::Broadcast);\n}\n\nvoid NotifySettings::loadExceptions() {\n\tfor (auto i = 0; i != kDefaultNotifyTypes; ++i) {\n\t\tif (_exceptionsRequestId[i]) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto type = static_cast<DefaultNotify>(i);\n\t\tconst auto api = &_owner->session().api();\n\t\tconst auto requestId = api->request(MTPaccount_GetNotifyExceptions(\n\t\t\tMTP_flags(MTPaccount_GetNotifyExceptions::Flag::f_peer),\n\t\t\tDefaultNotifyToMTP(type)\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\tapi->applyUpdates(result);\n\t\t}).send();\n\t\t_exceptionsRequestId[i] = requestId;\n\t}\n}\n\nvoid NotifySettings::updateException(not_null<PeerData*> peer) {\n\tconst auto type = DefaultNotifyType(peer);\n\tconst auto index = static_cast<int>(type);\n\tconst auto exception = peer->notify().muteUntil().has_value();\n\tif (!exception) {\n\t\tif (_exceptions[index].remove(peer)) {\n\t\t\texceptionsUpdated(type);\n\t\t}\n\t} else if (SkipAddException(peer)) {\n\t\treturn;\n\t} else if (_exceptions[index].emplace(peer).second) {\n\t\texceptionsUpdated(type);\n\t}\n}\n\nvoid NotifySettings::exceptionsUpdated(DefaultNotify type) {\n\tif (!ranges::contains(_exceptionsUpdatesScheduled, true)) {\n\t\tcrl::on_main(&_owner->session(), [=] {\n\t\t\tconst auto scheduled = base::take(_exceptionsUpdatesScheduled);\n\t\t\tfor (auto i = 0; i != kDefaultNotifyTypes; ++i) {\n\t\t\t\tif (scheduled[i]) {\n\t\t\t\t\t_exceptionsUpdates.fire(static_cast<DefaultNotify>(i));\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\t_exceptionsUpdatesScheduled[static_cast<int>(type)] = true;\n\t_exceptionsUpdatesRealtime.fire_copy(type);\n}\n\nrpl::producer<DefaultNotify> NotifySettings::exceptionsUpdates() const {\n\treturn _exceptionsUpdates.events();\n}\n\nauto NotifySettings::exceptionsUpdatesRealtime() const\n-> rpl::producer<DefaultNotify> {\n\treturn _exceptionsUpdatesRealtime.events();\n}\n\nconst base::flat_set<not_null<PeerData*>> &NotifySettings::exceptions(\n\t\tDefaultNotify type) const {\n\tconst auto index = static_cast<int>(type);\n\tAssert(index >= 0 && index < kDefaultNotifyTypes);\n\n\treturn _exceptions[index];\n}\n\nvoid NotifySettings::clearExceptions(DefaultNotify type) {\n\tconst auto index = static_cast<int>(type);\n\tconst auto list = base::take(_exceptions[index]);\n\tif (list.empty()) {\n\t\treturn;\n\t}\n\tfor (const auto &peer : list) {\n\t\t// Duplicated in resetToDefault(peer / thread).\n\t\tif (peer->notify().resetToDefault()) {\n\t\t\tupdateLocal(peer);\n\t\t\tpeer->session().api().updateNotifySettingsDelayed(peer);\n\t\t}\n\t}\n\tCore::App().notifications().checkDelayed();\n\texceptionsUpdated(type);\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/notify/data_notify_settings.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/notify/data_peer_notify_settings.h\"\n\n#include \"base/timer.h\"\n\nclass PeerData;\n\nnamespace Data {\n\nclass DocumentMedia;\nclass Session;\nclass Thread;\nclass Forum;\nclass ForumTopic;\n\n[[nodiscard]] DefaultNotify DefaultNotifyType(\n\tnot_null<const PeerData*> peer);\n\n[[nodiscard]] MTPInputNotifyPeer DefaultNotifyToMTP(DefaultNotify type);\n\nclass NotifySettings final {\npublic:\n\tNotifySettings(not_null<Session*> owner);\n\n\tvoid request(not_null<PeerData*> peer);\n\tvoid request(not_null<Thread*> thread);\n\n\tvoid apply(\n\t\tconst MTPNotifyPeer &notifyPeer,\n\t\tconst MTPPeerNotifySettings &settings);\n\tvoid apply(\n\t\tconst MTPInputNotifyPeer &notifyPeer,\n\t\tconst MTPPeerNotifySettings &settings);\n\tvoid apply(DefaultNotify type, const MTPPeerNotifySettings &settings);\n\tvoid apply(PeerId peerId, const MTPPeerNotifySettings &settings);\n\tvoid apply(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPPeerNotifySettings &settings);\n\tvoid apply(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tconst MTPPeerNotifySettings &settings);\n\tvoid apply(\n\t\tnot_null<ForumTopic*> topic,\n\t\tconst MTPPeerNotifySettings &settings);\n\n\tvoid update(\n\t\tnot_null<Thread*> thread,\n\t\tMuteValue muteForSeconds,\n\t\tstd::optional<bool> silentPosts = std::nullopt,\n\t\tstd::optional<NotifySound> sound = std::nullopt,\n\t\tstd::optional<bool> storiesMuted = std::nullopt);\n\tvoid resetToDefault(not_null<Thread*> thread);\n\tvoid update(\n\t\tnot_null<PeerData*> peer,\n\t\tMuteValue muteForSeconds,\n\t\tstd::optional<bool> silentPosts = std::nullopt,\n\t\tstd::optional<NotifySound> sound = std::nullopt,\n\t\tstd::optional<bool> storiesMuted = std::nullopt);\n\tvoid resetToDefault(not_null<PeerData*> peer);\n\n\tvoid forumParentMuteUpdated(not_null<Forum*> forum);\n\n\tvoid cacheSound(DocumentId id);\n\tvoid cacheSound(not_null<DocumentData*> document);\n\t[[nodiscard]] std::shared_ptr<DocumentMedia> lookupRingtone(\n\t\tDocumentId id) const;\n\n\t[[nodiscard]] rpl::producer<> defaultUpdates(DefaultNotify type) const;\n\t[[nodiscard]] rpl::producer<> defaultUpdates(\n\t\tnot_null<const PeerData*> peer) const;\n\n\t[[nodiscard]] const PeerNotifySettings &defaultSettings(\n\t\tDefaultNotify type) const;\n\t[[nodiscard]] bool isMuted(DefaultNotify type) const;\n\n\tvoid defaultUpdate(\n\t\tDefaultNotify type,\n\t\tMuteValue muteForSeconds,\n\t\tstd::optional<bool> silentPosts = std::nullopt,\n\t\tstd::optional<NotifySound> sound = std::nullopt,\n\t\tstd::optional<bool> storiesMuted = std::nullopt);\n\n\t[[nodiscard]] bool isMuted(not_null<const Thread*> thread) const;\n\t[[nodiscard]] NotifySound sound(not_null<const Thread*> thread) const;\n\t[[nodiscard]] bool muteUnknown(not_null<const Thread*> thread) const;\n\t[[nodiscard]] bool soundUnknown(not_null<const Thread*> thread) const;\n\n\t[[nodiscard]] bool isMuted(not_null<const PeerData*> peer) const;\n\t[[nodiscard]] bool silentPosts(not_null<const PeerData*> peer) const;\n\t[[nodiscard]] NotifySound sound(not_null<const PeerData*> peer) const;\n\t[[nodiscard]] bool muteUnknown(not_null<const PeerData*> peer) const;\n\t[[nodiscard]] bool silentPostsUnknown(\n\t\tnot_null<const PeerData*> peer) const;\n\t[[nodiscard]] bool soundUnknown(not_null<const PeerData*> peer) const;\n\n\tvoid loadExceptions();\n\t[[nodiscard]] rpl::producer<DefaultNotify> exceptionsUpdates() const;\n\t[[nodiscard]] auto exceptionsUpdatesRealtime() const\n\t\t-> rpl::producer<DefaultNotify>;\n\t[[nodiscard]] const base::flat_set<not_null<PeerData*>> &exceptions(\n\t\tDefaultNotify type) const;\n\tvoid clearExceptions(DefaultNotify type);\n\nprivate:\n\tstatic constexpr auto kDefaultNotifyTypes = 3;\n\n\tstruct DefaultValue {\n\t\tPeerNotifySettings settings;\n\t\trpl::event_stream<> updates;\n\t};\n\n\tvoid cacheSound(const std::optional<NotifySound> &sound);\n\n\t[[nodiscard]] bool isMuted(\n\t\tnot_null<const Thread*> thread,\n\t\tcrl::time *changesIn) const;\n\t[[nodiscard]] bool isMuted(\n\t\tnot_null<const PeerData*> peer,\n\t\tcrl::time *changesIn) const;\n\n\t[[nodiscard]] DefaultValue &defaultValue(DefaultNotify type);\n\t[[nodiscard]] const DefaultValue &defaultValue(DefaultNotify type) const;\n\t[[nodiscard]] const PeerNotifySettings &defaultSettings(\n\t\tnot_null<const PeerData*> peer) const;\n\t[[nodiscard]] bool settingsUnknown(not_null<const PeerData*> peer) const;\n\t[[nodiscard]] bool settingsUnknown(\n\t\tnot_null<const Thread*> thread) const;\n\n\tvoid unmuteByFinished();\n\tvoid unmuteByFinishedDelayed(crl::time delay);\n\tvoid updateLocal(not_null<Thread*> thread);\n\tvoid updateLocal(not_null<PeerData*> peer);\n\tvoid updateLocal(DefaultNotify type);\n\n\tvoid updateException(not_null<PeerData*> peer);\n\tvoid exceptionsUpdated(DefaultNotify type);\n\n\tconst not_null<Session*> _owner;\n\n\tDefaultValue _defaultValues[3];\n\tstd::unordered_set<not_null<const PeerData*>> _mutedPeers;\n\tstd::unordered_map<not_null<ForumTopic*>, rpl::lifetime> _mutedTopics;\n\tbase::Timer _unmuteByFinishedTimer;\n\n\tstruct {\n\t\tbase::flat_map<\n\t\t\tDocumentId,\n\t\t\tstd::shared_ptr<DocumentMedia>> views;\n\t\tstd::vector<DocumentId> pendingIds;\n\t\trpl::lifetime pendingLifetime;\n\t} _ringtones;\n\n\trpl::event_stream<DefaultNotify> _exceptionsUpdates;\n\trpl::event_stream<DefaultNotify> _exceptionsUpdatesRealtime;\n\tstd::array<\n\t\tbase::flat_set<not_null<PeerData*>>,\n\t\tkDefaultNotifyTypes> _exceptions;\n\tstd::array<mtpRequestId, kDefaultNotifyTypes> _exceptionsRequestId = {};\n\tstd::array<bool, kDefaultNotifyTypes> _exceptionsUpdatesScheduled = {};\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/notify/data_peer_notify_settings.h\"\n\n#include \"base/unixtime.h\"\n\nnamespace Data {\nnamespace {\n\n[[nodiscard]] MTPinputPeerNotifySettings DefaultSettings() {\n\treturn MTP_inputPeerNotifySettings(\n\t\tMTP_flags(0),\n\t\tMTPBool(),\n\t\tMTPBool(),\n\t\tMTPint(),\n\t\tMTPNotificationSound(),\n\t\tMTPBool(),\n\t\tMTPBool(),\n\t\tMTPNotificationSound());\n}\n\n[[nodiscard]] NotifySound ParseSound(const MTPNotificationSound &sound) {\n\treturn sound.match([&](const MTPDnotificationSoundDefault &data) {\n\t\treturn NotifySound();\n\t}, [&](const MTPDnotificationSoundNone &data) {\n\t\treturn NotifySound{ .none = true };\n\t}, [&](const MTPDnotificationSoundLocal &data) {\n\t\treturn NotifySound{\n\t\t\t.title = qs(data.vtitle()),\n\t\t\t.data = qs(data.vdata()),\n\t\t};\n\t}, [&](const MTPDnotificationSoundRingtone &data) {\n\t\treturn NotifySound{ .id = data.vid().v };\n\t});\n}\n\n[[nodiscard]] MTPNotificationSound SerializeSound(\n\t\tconst std::optional<NotifySound> &sound) {\n\treturn !sound\n\t\t? MTPNotificationSound()\n\t\t: sound->none\n\t\t? MTP_notificationSoundNone()\n\t\t: sound->id\n\t\t? MTP_notificationSoundRingtone(MTP_long(sound->id))\n\t\t: !sound->title.isEmpty()\n\t\t? MTP_notificationSoundLocal(\n\t\t\tMTP_string(sound->title),\n\t\t\tMTP_string(sound->data))\n\t\t: MTP_notificationSoundDefault();\n}\n\n} // namespace\n\nint MuteValue::until() const {\n\tconstexpr auto kMax = std::numeric_limits<int>::max();\n\n\treturn forever\n\t\t? kMax\n\t\t: (period > 0)\n\t\t? int(std::min(int64(base::unixtime::now()) + period, int64(kMax)))\n\t\t: unmute\n\t\t? 0\n\t\t: -1;\n}\n\nclass NotifyPeerSettingsValue {\npublic:\n\tNotifyPeerSettingsValue(const MTPDpeerNotifySettings &data);\n\n\tbool change(const MTPDpeerNotifySettings &data);\n\tbool change(\n\t\tMuteValue muteForSeconds,\n\t\tstd::optional<bool> silentPosts,\n\t\tstd::optional<NotifySound> sound,\n\t\tstd::optional<bool> storiesMuted);\n\n\tstd::optional<TimeId> muteUntil() const;\n\tstd::optional<bool> silentPosts() const;\n\tstd::optional<NotifySound> sound() const;\n\tMTPinputPeerNotifySettings serialize() const;\n\nprivate:\n\tbool change(\n\t\tstd::optional<int> mute,\n\t\tstd::optional<NotifySound> sound,\n\t\tstd::optional<bool> showPreviews,\n\t\tstd::optional<bool> silentPosts,\n\t\tstd::optional<bool> storiesMuted);\n\n\tstd::optional<TimeId> _mute;\n\tstd::optional<NotifySound> _sound;\n\tstd::optional<bool> _silent;\n\tstd::optional<bool> _showPreviews;\n\tstd::optional<bool> _storiesMuted;\n\n};\n\nNotifyPeerSettingsValue::NotifyPeerSettingsValue(\n\t\tconst MTPDpeerNotifySettings &data) {\n\tchange(data);\n}\n\nbool NotifyPeerSettingsValue::change(const MTPDpeerNotifySettings &data) {\n\tconst auto mute = data.vmute_until();\n\tconst auto sound = data.vother_sound();\n\tconst auto showPreviews = data.vshow_previews();\n\tconst auto silent = data.vsilent();\n\tconst auto storiesMuted = data.vstories_muted();\n\treturn change(\n\t\tmute ? std::make_optional(mute->v) : std::nullopt,\n\t\tsound ? std::make_optional(ParseSound(*sound)) : std::nullopt,\n\t\t(showPreviews\n\t\t\t? std::make_optional(mtpIsTrue(*showPreviews))\n\t\t\t: std::nullopt),\n\t\tsilent ? std::make_optional(mtpIsTrue(*silent)) : std::nullopt,\n\t\t(storiesMuted\n\t\t\t? std::make_optional(mtpIsTrue(*storiesMuted))\n\t\t\t: std::nullopt));\n}\n\nbool NotifyPeerSettingsValue::change(\n\t\tMuteValue muteForSeconds,\n\t\tstd::optional<bool> silentPosts,\n\t\tstd::optional<NotifySound> sound,\n\t\tstd::optional<bool> storiesMuted) {\n\tconst auto newMute = muteForSeconds\n\t\t? base::make_optional(muteForSeconds.until())\n\t\t: _mute;\n\tconst auto newSilentPosts = silentPosts\n\t\t? base::make_optional(*silentPosts)\n\t\t: _silent;\n\tconst auto newSound = sound\n\t\t? base::make_optional(*sound)\n\t\t: _sound;\n\tconst auto newStoriesMuted = storiesMuted\n\t\t? base::make_optional(*storiesMuted)\n\t\t: _storiesMuted;\n\treturn change(\n\t\tnewMute,\n\t\tnewSound,\n\t\t_showPreviews,\n\t\tnewSilentPosts,\n\t\tnewStoriesMuted);\n}\n\nbool NotifyPeerSettingsValue::change(\n\t\tstd::optional<int> mute,\n\t\tstd::optional<NotifySound> sound,\n\t\tstd::optional<bool> showPreviews,\n\t\tstd::optional<bool> silentPosts,\n\t\tstd::optional<bool> storiesMuted) {\n\tif (_mute == mute\n\t\t&& _sound == sound\n\t\t&& _showPreviews == showPreviews\n\t\t&& _silent == silentPosts\n\t\t&& _storiesMuted == storiesMuted) {\n\t\treturn false;\n\t}\n\t_mute = mute;\n\t_sound = sound;\n\t_showPreviews = showPreviews;\n\t_silent = silentPosts;\n\t_storiesMuted = storiesMuted;\n\treturn true;\n}\n\nstd::optional<TimeId> NotifyPeerSettingsValue::muteUntil() const {\n\treturn _mute;\n}\n\nstd::optional<bool> NotifyPeerSettingsValue::silentPosts() const {\n\treturn _silent;\n}\n\nstd::optional<NotifySound> NotifyPeerSettingsValue::sound() const {\n\treturn _sound;\n}\n\nMTPinputPeerNotifySettings NotifyPeerSettingsValue::serialize() const {\n\tusing Flag = MTPDinputPeerNotifySettings::Flag;\n\tconst auto flag = [](auto &&optional, Flag flag) {\n\t\treturn optional.has_value() ? flag : Flag(0);\n\t};\n\treturn MTP_inputPeerNotifySettings(\n\t\tMTP_flags(flag(_mute, Flag::f_mute_until)\n\t\t\t| flag(_sound, Flag::f_sound)\n\t\t\t| flag(_silent, Flag::f_silent)\n\t\t\t| flag(_showPreviews, Flag::f_show_previews)\n\t\t\t| flag(_storiesMuted, Flag::f_stories_muted)),\n\t\tMTP_bool(_showPreviews.value_or(true)),\n\t\tMTP_bool(_silent.value_or(false)),\n\t\tMTP_int(_mute.value_or(false)),\n\t\tSerializeSound(_sound),\n\t\tMTP_bool(_storiesMuted.value_or(false)),\n\t\tMTP_bool(false), // stories_hide_sender\n\t\tSerializeSound(std::nullopt)); // stories_sound\n}\n\nPeerNotifySettings::PeerNotifySettings() = default;\n\nbool PeerNotifySettings::change(const MTPPeerNotifySettings &settings) {\n\tauto &data = settings.data();\n\tconst auto empty = !data.vflags().v;\n\tif (empty) {\n\t\tif (!_known || _value) {\n\t\t\t_known = true;\n\t\t\t_value = nullptr;\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\tif (_value) {\n\t\treturn _value->change(data);\n\t}\n\t_known = true;\n\t_value = std::make_unique<NotifyPeerSettingsValue>(data);\n\treturn true;\n}\n\nbool PeerNotifySettings::change(\n\t\tMuteValue muteForSeconds,\n\t\tstd::optional<bool> silentPosts,\n\t\tstd::optional<NotifySound> sound,\n\t\tstd::optional<bool> storiesMuted) {\n\tif (!muteForSeconds && !silentPosts && !sound && !storiesMuted) {\n\t\treturn false;\n\t} else if (_value) {\n\t\treturn _value->change(\n\t\t\tmuteForSeconds,\n\t\t\tsilentPosts,\n\t\t\tsound,\n\t\t\tstoriesMuted);\n\t}\n\tusing Flag = MTPDpeerNotifySettings::Flag;\n\tconst auto flags = (muteForSeconds ? Flag::f_mute_until : Flag(0))\n\t\t| (silentPosts ? Flag::f_silent : Flag(0))\n\t\t| (sound ? Flag::f_other_sound : Flag(0))\n\t\t| (storiesMuted ? Flag::f_stories_muted : Flag(0));\n\treturn change(MTP_peerNotifySettings(\n\t\tMTP_flags(flags),\n\t\tMTPBool(),\n\t\tsilentPosts ? MTP_bool(*silentPosts) : MTPBool(),\n\t\tMTP_int(muteForSeconds.until()),\n\t\tMTPNotificationSound(),\n\t\tMTPNotificationSound(),\n\t\tSerializeSound(sound),\n\t\tstoriesMuted ? MTP_bool(*storiesMuted) : MTPBool(),\n\t\tMTPBool(), // stories_hide_sender\n\t\tMTPNotificationSound(),\n\t\tMTPNotificationSound(),\n\t\tSerializeSound(std::nullopt))); // stories_sound\n}\n\nbool PeerNotifySettings::resetToDefault() {\n\tif (_known && !_value) {\n\t\treturn false;\n\t}\n\t_known = true;\n\t_value = nullptr;\n\treturn true;\n}\n\nstd::optional<TimeId> PeerNotifySettings::muteUntil() const {\n\treturn _value\n\t\t? _value->muteUntil()\n\t\t: std::nullopt;\n}\n\nbool PeerNotifySettings::settingsUnknown() const {\n\treturn !_known;\n}\n\nstd::optional<bool> PeerNotifySettings::silentPosts() const {\n\treturn _value\n\t\t? _value->silentPosts()\n\t\t: std::nullopt;\n}\n\nstd::optional<NotifySound> PeerNotifySettings::sound() const {\n\treturn _value\n\t\t? _value->sound()\n\t\t: std::nullopt;\n}\n\nMTPinputPeerNotifySettings PeerNotifySettings::serialize() const {\n\treturn _value\n\t\t? _value->serialize()\n\t\t: DefaultSettings();\n}\n\nPeerNotifySettings::~PeerNotifySettings() = default;\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/notify/data_peer_notify_settings.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\n\nclass NotifyPeerSettingsValue;\n\nenum class DefaultNotify : uint8_t {\n\tUser,\n\tGroup,\n\tBroadcast,\n};\n\nstruct NotifySound {\n\tQString\ttitle;\n\tQString data;\n\tDocumentId id = 0;\n\tbool none = false;\n};\n\nstruct MuteValue {\n\tbool unmute = false;\n\tbool forever = false;\n\tint period = 0;\n\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn unmute || forever || period;\n\t}\n\t[[nodiscard]] int until() const;\n};\n\ninline bool operator==(const NotifySound &a, const NotifySound &b) {\n\treturn (a.id == b.id)\n\t\t&& (a.none == b.none)\n\t\t&& (a.title == b.title)\n\t\t&& (a.data == b.data);\n}\n\nclass PeerNotifySettings {\npublic:\n\tPeerNotifySettings();\n\n\tbool change(const MTPPeerNotifySettings &settings);\n\tbool change(\n\t\tMuteValue muteForSeconds,\n\t\tstd::optional<bool> silentPosts,\n\t\tstd::optional<NotifySound> sound,\n\t\tstd::optional<bool> storiesMuted);\n\tbool resetToDefault();\n\n\tbool settingsUnknown() const;\n\tstd::optional<TimeId> muteUntil() const;\n\tstd::optional<bool> silentPosts() const;\n\tstd::optional<NotifySound> sound() const;\n\tMTPinputPeerNotifySettings serialize() const;\n\n\t~PeerNotifySettings();\n\nprivate:\n\tbool _known = false;\n\tstd::unique_ptr<NotifyPeerSettingsValue> _value;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/notify/data_peer_notify_volume.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/notify/data_peer_notify_volume.h\"\n\n#include \"data/data_peer.h\"\n#include \"data/data_thread.h\"\n#include \"data/notify/data_peer_notify_settings.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"core/application.h\"\n#include \"window/notifications_manager.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Data {\n\nData::VolumeController DefaultRingtonesVolumeController(\n\t\tnot_null<Main::Session*> session,\n\t\tData::DefaultNotify defaultNotify) {\n\treturn Data::VolumeController{\n\t\t.volume = [=]() -> ushort {\n\t\t\tconst auto volume = session->settings().ringtoneVolume(\n\t\t\t\tdefaultNotify);\n\t\t\treturn volume ? volume : 100;\n\t\t},\n\t\t.saveVolume = [=](ushort volume) {\n\t\t\tsession->settings().setRingtoneVolume(defaultNotify, volume);\n\t\t\tsession->saveSettingsDelayed();\n\t\t}};\n}\n\nData::VolumeController ThreadRingtonesVolumeController(\n\t\tnot_null<Data::Thread*> thread) {\n\treturn Data::VolumeController{\n\t\t.volume = [=]() -> ushort {\n\t\t\tconst auto volume = thread->session().settings().ringtoneVolume(\n\t\t\t\tthread->peer()->id,\n\t\t\t\tthread->topicRootId(),\n\t\t\t\tthread->monoforumPeerId());\n\t\t\treturn volume ? volume : 100;\n\t\t},\n\t\t.saveVolume = [=](ushort volume) {\n\t\t\tthread->session().settings().setRingtoneVolume(\n\t\t\t\tthread->peer()->id,\n\t\t\t\tthread->topicRootId(),\n\t\t\t\tthread->monoforumPeerId(),\n\t\t\t\tvolume);\n\t\t\tthread->session().saveSettingsDelayed();\n\t\t}};\n}\n\n} // namespace Data\n\nnamespace Ui {\n\nvoid AddRingtonesVolumeSlider(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<bool> toggleOn,\n\t\trpl::producer<QString> subtitle,\n\t\tData::VolumeController volumeController) {\n\tExpects(volumeController.volume && volumeController.saveVolume);\n\n\tconst auto volumeWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tvolumeWrap->toggleOn(\n\t\trpl::combine(\n\t\t\tCore::App().notifications().volumeSupportedValue(),\n\t\t\tstd::move(toggleOn)\n\t\t) | rpl::map(\n\t\t\trpl::mappers::_1 && rpl::mappers::_2\n\t\t) | rpl::distinct_until_changed(),\n\t\tanim::type::normal);\n\tvolumeWrap->finishAnimating();\n\n\tUi::AddSubsectionTitle(volumeWrap->entity(), std::move(subtitle));\n\tauto sliderWithLabel = Settings::MakeSliderWithLabel(\n\t\tvolumeWrap->entity(),\n\t\tst::settingsScale,\n\t\tst::settingsScaleLabel,\n\t\tst::normalFont->spacew * 2,\n\t\tst::settingsScaleLabel.style.font->width(\"100%\"),\n\t\ttrue);\n\tconst auto slider = sliderWithLabel.slider;\n\tconst auto label = sliderWithLabel.label;\n\n\tvolumeWrap->entity()->add(\n\t\tstd::move(sliderWithLabel.widget),\n\t\tst::settingsBigScalePadding);\n\n\tconst auto updateLabel = [=](int volume) {\n\t\tlabel->setText(QString::number(volume) + '%');\n\t};\n\n\tslider->setPseudoDiscrete(\n\t\t100,\n\t\t[=](int index) { return index + 1; },\n\t\tint(volumeController.volume()),\n\t\tupdateLabel,\n\t\t[saveVolume = volumeController.saveVolume](int volume) {\n\t\t\tsaveVolume(volume);\n\t\t});\n\tupdateLabel(volumeController.volume());\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/data/notify/data_peer_notify_volume.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Data {\n\nenum class DefaultNotify : uint8_t;\nclass Thread;\n\nstruct VolumeController {\n\tFn<ushort()> volume = nullptr;\n\tFn<void(ushort)> saveVolume = nullptr;\n};\n\n[[nodiscard]] VolumeController DefaultRingtonesVolumeController(\n\tnot_null<Main::Session*> session,\n\tData::DefaultNotify defaultNotify);\n\n[[nodiscard]] VolumeController ThreadRingtonesVolumeController(\n\tnot_null<Data::Thread*> thread);\n\n} // namespace Data\n\nnamespace Ui {\n\nvoid AddRingtonesVolumeSlider(\n\tnot_null<Ui::VerticalLayout*> container,\n\trpl::producer<bool> toggleOn,\n\trpl::producer<QString> subtitle,\n\tData::VolumeController volumeController);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/data/raw/raw_countries_bounds.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/raw/raw_countries_bounds.h\"\n\n// Source: https://github.com/sandstrom/country-bounding-boxes\n\nnamespace Raw {\n\nconst base::flat_map<QString, GeoBounds> &CountryBounds() {\n\tstatic const auto result = base::flat_map<QString, GeoBounds>{\n\t\t{ u\"AF\"_q, GeoBounds{ 60.53, 29.32, 75.16, 38.49 } },\n\t\t{ u\"AO\"_q, GeoBounds{ 11.64, -17.93, 24.08, -4.44 } },\n\t\t{ u\"AL\"_q, GeoBounds{ 19.3, 39.62, 21.02, 42.69 } },\n\t\t{ u\"AE\"_q, GeoBounds{ 51.58, 22.5, 56.4, 26.06 } },\n\t\t{ u\"AR\"_q, GeoBounds{ -73.42, -55.25, -53.63, -21.83 } },\n\t\t{ u\"AM\"_q, GeoBounds{ 43.58, 38.74, 46.51, 41.25 } },\n\t\t{ u\"AQ\"_q, GeoBounds{ -180.0, -90.0, 180.0, -63.27 } },\n\t\t{ u\"TF\"_q, GeoBounds{ 68.72, -49.78, 70.56, -48.63 } },\n\t\t{ u\"AU\"_q, GeoBounds{ 113.34, -43.63, 153.57, -10.67 } },\n\t\t{ u\"AT\"_q, GeoBounds{ 9.48, 46.43, 16.98, 49.04 } },\n\t\t{ u\"AZ\"_q, GeoBounds{ 44.79, 38.27, 50.39, 41.86 } },\n\t\t{ u\"BI\"_q, GeoBounds{ 29.02, -4.5, 30.75, -2.35 } },\n\t\t{ u\"BE\"_q, GeoBounds{ 2.51, 49.53, 6.16, 51.48 } },\n\t\t{ u\"BJ\"_q, GeoBounds{ 0.77, 6.14, 3.8, 12.24 } },\n\t\t{ u\"BF\"_q, GeoBounds{ -5.47, 9.61, 2.18, 15.12 } },\n\t\t{ u\"BD\"_q, GeoBounds{ 88.08, 20.67, 92.67, 26.45 } },\n\t\t{ u\"BG\"_q, GeoBounds{ 22.38, 41.23, 28.56, 44.23 } },\n\t\t{ u\"BS\"_q, GeoBounds{ -78.98, 23.71, -77.0, 27.04 } },\n\t\t{ u\"BA\"_q, GeoBounds{ 15.75, 42.65, 19.6, 45.23 } },\n\t\t{ u\"BY\"_q, GeoBounds{ 23.2, 51.32, 32.69, 56.17 } },\n\t\t{ u\"BZ\"_q, GeoBounds{ -89.23, 15.89, -88.11, 18.5 } },\n\t\t{ u\"BO\"_q, GeoBounds{ -69.59, -22.87, -57.5, -9.76 } },\n\t\t{ u\"BR\"_q, GeoBounds{ -73.99, -33.77, -34.73, 5.24 } },\n\t\t{ u\"BN\"_q, GeoBounds{ 114.2, 4.01, 115.45, 5.45 } },\n\t\t{ u\"BT\"_q, GeoBounds{ 88.81, 26.72, 92.1, 28.3 } },\n\t\t{ u\"BW\"_q, GeoBounds{ 19.9, -26.83, 29.43, -17.66 } },\n\t\t{ u\"CF\"_q, GeoBounds{ 14.46, 2.27, 27.37, 11.14 } },\n\t\t{ u\"CA\"_q, GeoBounds{ -141.0, 41.68, -52.65, 73.23 } },\n\t\t{ u\"CH\"_q, GeoBounds{ 6.02, 45.78, 10.44, 47.83 } },\n\t\t{ u\"CL\"_q, GeoBounds{ -75.64, -55.61, -66.96, -17.58 } },\n\t\t{ u\"CN\"_q, GeoBounds{ 73.68, 18.2, 135.03, 53.46 } },\n\t\t{ u\"CI\"_q, GeoBounds{ -8.6, 4.34, -2.56, 10.52 } },\n\t\t{ u\"CM\"_q, GeoBounds{ 8.49, 1.73, 16.01, 12.86 } },\n\t\t{ u\"CD\"_q, GeoBounds{ 12.18, -13.26, 31.17, 5.26 } },\n\t\t{ u\"CG\"_q, GeoBounds{ 11.09, -5.04, 18.45, 3.73 } },\n\t\t{ u\"CO\"_q, GeoBounds{ -78.99, -4.3, -66.88, 12.44 } },\n\t\t{ u\"CR\"_q, GeoBounds{ -85.94, 8.23, -82.55, 11.22 } },\n\t\t{ u\"CU\"_q, GeoBounds{ -84.97, 19.86, -74.18, 23.19 } },\n\t\t{ u\"CY\"_q, GeoBounds{ 32.26, 34.57, 34.0, 35.17 } },\n\t\t{ u\"CZ\"_q, GeoBounds{ 12.24, 48.56, 18.85, 51.12 } },\n\t\t{ u\"DE\"_q, GeoBounds{ 5.99, 47.3, 15.02, 54.98 } },\n\t\t{ u\"DJ\"_q, GeoBounds{ 41.66, 10.93, 43.32, 12.7 } },\n\t\t{ u\"DK\"_q, GeoBounds{ 8.09, 54.8, 12.69, 57.73 } },\n\t\t{ u\"DO\"_q, GeoBounds{ -71.95, 17.6, -68.32, 19.88 } },\n\t\t{ u\"DZ\"_q, GeoBounds{ -8.68, 19.06, 12.0, 37.12 } },\n\t\t{ u\"EC\"_q, GeoBounds{ -80.97, -4.96, -75.23, 1.38 } },\n\t\t{ u\"EG\"_q, GeoBounds{ 24.7, 22.0, 36.87, 31.59 } },\n\t\t{ u\"ER\"_q, GeoBounds{ 36.32, 12.46, 43.08, 18.0 } },\n\t\t{ u\"ES\"_q, GeoBounds{ -9.39, 35.95, 3.04, 43.75 } },\n\t\t{ u\"EE\"_q, GeoBounds{ 23.34, 57.47, 28.13, 59.61 } },\n\t\t{ u\"ET\"_q, GeoBounds{ 32.95, 3.42, 47.79, 14.96 } },\n\t\t{ u\"FI\"_q, GeoBounds{ 20.65, 59.85, 31.52, 70.16 } },\n\t\t{ u\"FJ\"_q, GeoBounds{ -180.0, -18.29, 180.0, -16.02 } },\n\t\t{ u\"FK\"_q, GeoBounds{ -61.2, -52.3, -57.75, -51.1 } },\n\t\t{ u\"FR\"_q, GeoBounds{ -5.0, 42.5, 9.56, 51.15 } },\n\t\t{ u\"GA\"_q, GeoBounds{ 8.8, -3.98, 14.43, 2.33 } },\n\t\t{ u\"GB\"_q, GeoBounds{ -7.57, 49.96, 1.68, 58.64 } },\n\t\t{ u\"GE\"_q, GeoBounds{ 39.96, 41.06, 46.64, 43.55 } },\n\t\t{ u\"GH\"_q, GeoBounds{ -3.24, 4.71, 1.06, 11.1 } },\n\t\t{ u\"GN\"_q, GeoBounds{ -15.13, 7.31, -7.83, 12.59 } },\n\t\t{ u\"GM\"_q, GeoBounds{ -16.84, 13.13, -13.84, 13.88 } },\n\t\t{ u\"GW\"_q, GeoBounds{ -16.68, 11.04, -13.7, 12.63 } },\n\t\t{ u\"GQ\"_q, GeoBounds{ 9.31, 1.01, 11.29, 2.28 } },\n\t\t{ u\"GR\"_q, GeoBounds{ 20.15, 34.92, 26.6, 41.83 } },\n\t\t{ u\"GL\"_q, GeoBounds{ -73.3, 60.04, -12.21, 83.65 } },\n\t\t{ u\"GT\"_q, GeoBounds{ -92.23, 13.74, -88.23, 17.82 } },\n\t\t{ u\"GY\"_q, GeoBounds{ -61.41, 1.27, -56.54, 8.37 } },\n\t\t{ u\"HN\"_q, GeoBounds{ -89.35, 12.98, -83.15, 16.01 } },\n\t\t{ u\"HR\"_q, GeoBounds{ 13.66, 42.48, 19.39, 46.5 } },\n\t\t{ u\"HT\"_q, GeoBounds{ -74.46, 18.03, -71.62, 19.92 } },\n\t\t{ u\"HU\"_q, GeoBounds{ 16.2, 45.76, 22.71, 48.62 } },\n\t\t{ u\"ID\"_q, GeoBounds{ 95.29, -10.36, 141.03, 5.48 } },\n\t\t{ u\"IN\"_q, GeoBounds{ 68.18, 7.97, 97.4, 35.49 } },\n\t\t{ u\"IE\"_q, GeoBounds{ -9.98, 51.67, -6.03, 55.13 } },\n\t\t{ u\"IR\"_q, GeoBounds{ 44.11, 25.08, 63.32, 39.71 } },\n\t\t{ u\"IQ\"_q, GeoBounds{ 38.79, 29.1, 48.57, 37.39 } },\n\t\t{ u\"IS\"_q, GeoBounds{ -24.33, 63.5, -13.61, 66.53 } },\n\t\t{ u\"IL\"_q, GeoBounds{ 34.27, 29.5, 35.84, 33.28 } },\n\t\t{ u\"IT\"_q, GeoBounds{ 6.75, 36.62, 18.48, 47.12 } },\n\t\t{ u\"JM\"_q, GeoBounds{ -78.34, 17.7, -76.2, 18.52 } },\n\t\t{ u\"JO\"_q, GeoBounds{ 34.92, 29.2, 39.2, 33.38 } },\n\t\t{ u\"JP\"_q, GeoBounds{ 129.41, 31.03, 145.54, 45.55 } },\n\t\t{ u\"KZ\"_q, GeoBounds{ 46.47, 40.66, 87.36, 55.39 } },\n\t\t{ u\"KE\"_q, GeoBounds{ 33.89, -4.68, 41.86, 5.51 } },\n\t\t{ u\"KG\"_q, GeoBounds{ 69.46, 39.28, 80.26, 43.3 } },\n\t\t{ u\"KH\"_q, GeoBounds{ 102.35, 10.49, 107.61, 14.57 } },\n\t\t{ u\"KR\"_q, GeoBounds{ 126.12, 34.39, 129.47, 38.61 } },\n\t\t{ u\"KW\"_q, GeoBounds{ 46.57, 28.53, 48.42, 30.06 } },\n\t\t{ u\"LA\"_q, GeoBounds{ 100.12, 13.88, 107.56, 22.46 } },\n\t\t{ u\"LB\"_q, GeoBounds{ 35.13, 33.09, 36.61, 34.64 } },\n\t\t{ u\"LR\"_q, GeoBounds{ -11.44, 4.36, -7.54, 8.54 } },\n\t\t{ u\"LY\"_q, GeoBounds{ 9.32, 19.58, 25.16, 33.14 } },\n\t\t{ u\"LK\"_q, GeoBounds{ 79.7, 5.97, 81.79, 9.82 } },\n\t\t{ u\"LS\"_q, GeoBounds{ 27.0, -30.65, 29.33, -28.65 } },\n\t\t{ u\"LT\"_q, GeoBounds{ 21.06, 53.91, 26.59, 56.37 } },\n\t\t{ u\"LU\"_q, GeoBounds{ 5.67, 49.44, 6.24, 50.13 } },\n\t\t{ u\"LV\"_q, GeoBounds{ 21.06, 55.62, 28.18, 57.97 } },\n\t\t{ u\"MA\"_q, GeoBounds{ -17.02, 21.42, -1.12, 35.76 } },\n\t\t{ u\"MD\"_q, GeoBounds{ 26.62, 45.49, 30.02, 48.47 } },\n\t\t{ u\"MG\"_q, GeoBounds{ 43.25, -25.6, 50.48, -12.04 } },\n\t\t{ u\"MX\"_q, GeoBounds{ -117.13, 14.54, -86.81, 32.72 } },\n\t\t{ u\"MK\"_q, GeoBounds{ 20.46, 40.84, 22.95, 42.32 } },\n\t\t{ u\"ML\"_q, GeoBounds{ -12.17, 10.1, 4.27, 24.97 } },\n\t\t{ u\"MM\"_q, GeoBounds{ 92.3, 9.93, 101.18, 28.34 } },\n\t\t{ u\"ME\"_q, GeoBounds{ 18.45, 41.88, 20.34, 43.52 } },\n\t\t{ u\"MN\"_q, GeoBounds{ 87.75, 41.6, 119.77, 52.05 } },\n\t\t{ u\"MZ\"_q, GeoBounds{ 30.18, -26.74, 40.78, -10.32 } },\n\t\t{ u\"MR\"_q, GeoBounds{ -17.06, 14.62, -4.92, 27.4 } },\n\t\t{ u\"MW\"_q, GeoBounds{ 32.69, -16.8, 35.77, -9.23 } },\n\t\t{ u\"MY\"_q, GeoBounds{ 100.09, 0.77, 119.18, 6.93 } },\n\t\t{ u\"NA\"_q, GeoBounds{ 11.73, -29.05, 25.08, -16.94 } },\n\t\t{ u\"NC\"_q, GeoBounds{ 164.03, -22.4, 167.12, -20.11 } },\n\t\t{ u\"NE\"_q, GeoBounds{ 0.3, 11.66, 15.9, 23.47 } },\n\t\t{ u\"NG\"_q, GeoBounds{ 2.69, 4.24, 14.58, 13.87 } },\n\t\t{ u\"NI\"_q, GeoBounds{ -87.67, 10.73, -83.15, 15.02 } },\n\t\t{ u\"NL\"_q, GeoBounds{ 3.31, 50.8, 7.09, 53.51 } },\n\t\t{ u\"NO\"_q, GeoBounds{ 4.99, 58.08, 31.29, 70.92 } },\n\t\t{ u\"NP\"_q, GeoBounds{ 80.09, 26.4, 88.17, 30.42 } },\n\t\t{ u\"NZ\"_q, GeoBounds{ 166.51, -46.64, 178.52, -34.45 } },\n\t\t{ u\"OM\"_q, GeoBounds{ 52.0, 16.65, 59.81, 26.4 } },\n\t\t{ u\"PK\"_q, GeoBounds{ 60.87, 23.69, 77.84, 37.13 } },\n\t\t{ u\"PA\"_q, GeoBounds{ -82.97, 7.22, -77.24, 9.61 } },\n\t\t{ u\"PE\"_q, GeoBounds{ -81.41, -18.35, -68.67, -0.06 } },\n\t\t{ u\"PH\"_q, GeoBounds{ 117.17, 5.58, 126.54, 18.51 } },\n\t\t{ u\"PG\"_q, GeoBounds{ 141.0, -10.65, 156.02, -2.5 } },\n\t\t{ u\"PL\"_q, GeoBounds{ 14.07, 49.03, 24.03, 54.85 } },\n\t\t{ u\"PR\"_q, GeoBounds{ -67.24, 17.95, -65.59, 18.52 } },\n\t\t{ u\"KP\"_q, GeoBounds{ 124.27, 37.67, 130.78, 42.99 } },\n\t\t{ u\"PT\"_q, GeoBounds{ -9.53, 36.84, -6.39, 42.28 } },\n\t\t{ u\"PY\"_q, GeoBounds{ -62.69, -27.55, -54.29, -19.34 } },\n\t\t{ u\"QA\"_q, GeoBounds{ 50.74, 24.56, 51.61, 26.11 } },\n\t\t{ u\"RO\"_q, GeoBounds{ 20.22, 43.69, 29.63, 48.22 } },\n\t\t{ u\"RU\"_q, GeoBounds{ -18.0, 41.15, 180.0, 81.25 } },\n\t\t{ u\"RW\"_q, GeoBounds{ 29.02, -2.92, 30.82, -1.13 } },\n\t\t{ u\"SA\"_q, GeoBounds{ 34.63, 16.35, 55.67, 32.16 } },\n\t\t{ u\"SD\"_q, GeoBounds{ 21.94, 8.62, 38.41, 22.0 } },\n\t\t{ u\"SS\"_q, GeoBounds{ 23.89, 3.51, 35.3, 12.25 } },\n\t\t{ u\"SN\"_q, GeoBounds{ -17.63, 12.33, -11.47, 16.6 } },\n\t\t{ u\"SB\"_q, GeoBounds{ 156.49, -10.83, 162.4, -6.6 } },\n\t\t{ u\"SL\"_q, GeoBounds{ -13.25, 6.79, -10.23, 10.05 } },\n\t\t{ u\"SV\"_q, GeoBounds{ -90.1, 13.15, -87.72, 14.42 } },\n\t\t{ u\"SO\"_q, GeoBounds{ 40.98, -1.68, 51.13, 12.02 } },\n\t\t{ u\"RS\"_q, GeoBounds{ 18.83, 42.25, 22.99, 46.17 } },\n\t\t{ u\"SR\"_q, GeoBounds{ -58.04, 1.82, -53.96, 6.03 } },\n\t\t{ u\"SK\"_q, GeoBounds{ 16.88, 47.76, 22.56, 49.57 } },\n\t\t{ u\"SI\"_q, GeoBounds{ 13.7, 45.45, 16.56, 46.85 } },\n\t\t{ u\"SE\"_q, GeoBounds{ 11.03, 55.36, 23.9, 69.11 } },\n\t\t{ u\"SZ\"_q, GeoBounds{ 30.68, -27.29, 32.07, -25.66 } },\n\t\t{ u\"SY\"_q, GeoBounds{ 35.7, 32.31, 42.35, 37.23 } },\n\t\t{ u\"TD\"_q, GeoBounds{ 13.54, 7.42, 23.89, 23.41 } },\n\t\t{ u\"TG\"_q, GeoBounds{ -0.05, 5.93, 1.87, 11.02 } },\n\t\t{ u\"TH\"_q, GeoBounds{ 97.38, 5.69, 105.59, 20.42 } },\n\t\t{ u\"TJ\"_q, GeoBounds{ 67.44, 36.74, 74.98, 40.96 } },\n\t\t{ u\"TM\"_q, GeoBounds{ 52.5, 35.27, 66.55, 42.75 } },\n\t\t{ u\"TL\"_q, GeoBounds{ 124.97, -9.39, 127.34, -8.27 } },\n\t\t{ u\"TT\"_q, GeoBounds{ -61.95, 10.0, -60.9, 10.89 } },\n\t\t{ u\"TN\"_q, GeoBounds{ 7.52, 30.31, 11.49, 37.35 } },\n\t\t{ u\"TR\"_q, GeoBounds{ 26.04, 35.82, 44.79, 42.14 } },\n\t\t{ u\"TW\"_q, GeoBounds{ 120.11, 21.97, 121.95, 25.3 } },\n\t\t{ u\"TZ\"_q, GeoBounds{ 29.34, -11.72, 40.32, -0.95 } },\n\t\t{ u\"UG\"_q, GeoBounds{ 29.58, -1.44, 35.04, 4.25 } },\n\t\t{ u\"UA\"_q, GeoBounds{ 22.09, 44.36, 40.08, 52.34 } },\n\t\t{ u\"UY\"_q, GeoBounds{ -58.43, -34.95, -53.21, -30.11 } },\n\t\t{ u\"US\"_q, GeoBounds{ -125.0, 25.0, -66.96, 49.5 } },\n\t\t{ u\"UZ\"_q, GeoBounds{ 55.93, 37.14, 73.06, 45.59 } },\n\t\t{ u\"VE\"_q, GeoBounds{ -73.3, 0.72, -59.76, 12.16 } },\n\t\t{ u\"VN\"_q, GeoBounds{ 102.17, 8.6, 109.34, 23.35 } },\n\t\t{ u\"VU\"_q, GeoBounds{ 166.63, -16.6, 167.84, -14.63 } },\n\t\t{ u\"PS\"_q, GeoBounds{ 34.93, 31.35, 35.55, 32.53 } },\n\t\t{ u\"YE\"_q, GeoBounds{ 42.6, 12.59, 53.11, 19.0 } },\n\t\t{ u\"ZA\"_q, GeoBounds{ 16.34, -34.82, 32.83, -22.09 } },\n\t\t{ u\"ZM\"_q, GeoBounds{ 21.89, -17.96, 33.49, -8.24 } },\n\t\t{ u\"ZW\"_q, GeoBounds{ 25.26, -22.27, 32.85, -15.51 } }\n\t};\n\treturn result;\n}\n\n} // namespace Raw\n"
  },
  {
    "path": "Telegram/SourceFiles/data/raw/raw_countries_bounds.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QString>\n\nnamespace Raw {\n\nstruct GeoBounds {\n\tdouble minLat = 0.;\n\tdouble minLon = 0.;\n\tdouble maxLat = 0.;\n\tdouble maxLon = 0.;\n};\n\n[[nodiscard]] const base::flat_map<QString, GeoBounds> &CountryBounds();\n\n} // namespace Raw\n"
  },
  {
    "path": "Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/stickers/data_custom_emoji.h\"\n\n#include \"boxes/peers/edit_forum_topic_box.h\" // MakeTopicIconEmoji.\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_forum_topic.h\" // ParseTopicIconEmojiEntity.\n#include \"data/data_peer.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"dialogs/ui/dialogs_stories_content.h\"\n#include \"dialogs/ui/dialogs_stories_content.h\"\n#include \"lottie/lottie_common.h\"\n#include \"lottie/lottie_frame_generator.h\"\n#include \"ffmpeg/ffmpeg_frame_generator.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"storage/file_download.h\" // kMaxFileInMemory\n#include \"ui/chat/chats_filter_tag.h\"\n#include \"ui/effects/premium_stars_colored.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/text/custom_emoji_instance.h\"\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/ui_utility.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kMaxPerRequest = 100;\n#if 0 // inject-to-on_main\nconstexpr auto kUnsubscribeUpdatesDelay = 3 * crl::time(1000);\n#endif\n\nusing SizeTag = CustomEmojiManager::SizeTag;\n\nclass CallbackListener final : public CustomEmojiManager::Listener {\npublic:\n\texplicit CallbackListener(Fn<void(not_null<DocumentData*>)> callback)\n\t: _callback(std::move(callback)) {\n\t\tExpects(_callback != nullptr);\n\t}\n\nprivate:\n\tvoid customEmojiResolveDone(not_null<DocumentData*> document) {\n\t\t_callback(document);\n\t}\n\n\tFn<void(not_null<DocumentData*>)> _callback;\n\n};\n\n[[nodiscard]] ChatHelpers::StickerLottieSize LottieSizeFromTag(SizeTag tag) {\n\t// NB! onlyCustomEmoji dimensions caching uses last ::EmojiInteraction-s.\n\tusing LottieSize = ChatHelpers::StickerLottieSize;\n\tswitch (tag) {\n\tcase SizeTag::Normal: return LottieSize::EmojiInteraction;\n\tcase SizeTag::Large: return LottieSize::EmojiInteractionReserved1;\n\tcase SizeTag::Isolated: return LottieSize::EmojiInteractionReserved2;\n\tcase SizeTag::SetIcon: return LottieSize::EmojiInteractionReserved3;\n\t}\n\tUnexpected(\"SizeTag value in CustomEmojiManager-LottieSizeFromTag.\");\n}\n\n[[nodiscard]] int EmojiSizeFromTag(SizeTag tag) {\n\tswitch (tag) {\n\tcase SizeTag::Normal: return Ui::Emoji::GetSizeNormal();\n\tcase SizeTag::Large: return Ui::Emoji::GetSizeLarge();\n\tcase SizeTag::Isolated:\n\t\treturn (st::largeEmojiSize + 2 * st::largeEmojiOutline)\n\t\t\t* style::DevicePixelRatio();\n\tcase SizeTag::SetIcon:\n\t\treturn int(style::ConvertScale(18 * 7 / 6., style::Scale()))\n\t\t\t* style::DevicePixelRatio();\n\t}\n\tUnexpected(\"SizeTag value in CustomEmojiManager-SizeFromTag.\");\n}\n\n[[nodiscard]] int FrameSizeFromTag(SizeTag tag, int sizeOverride) {\n\treturn sizeOverride\n\t\t? (sizeOverride * style::DevicePixelRatio())\n\t\t: FrameSizeFromTag(tag);\n}\n\n[[nodiscard]] QString UserpicEmojiPrefix() {\n\treturn u\"userpic:\"_q;\n}\n\n[[nodiscard]] QString ScaledSimplePrefix() {\n\treturn u\"scaled-simple:\"_q;\n}\n\n[[nodiscard]] QString ScaledCustomPrefix() {\n\treturn u\"scaled-custom:\"_q;\n}\n\n[[nodiscard]] QString ForceStaticPrefix() {\n\treturn u\"force-static:\"_q;\n}\n\n[[nodiscard]] QString CollectiblePrefix() {\n\treturn u\"collectible:\"_q;\n}\n\n[[nodiscard]] QString InternalPadding(QMargins value) {\n\treturn value.isNull() ? QString() : QString(\",%1,%2,%3,%4\"\n\t).arg(value.left()\n\t).arg(value.top()\n\t).arg(value.right()\n\t).arg(value.bottom());\n}\n\n} // namespace\n\nclass CustomEmojiLoader final\n\t: public Ui::CustomEmoji::Loader\n\t, public base::has_weak_ptr {\npublic:\n\tCustomEmojiLoader(\n\t\tnot_null<Session*> owner,\n\t\tDocumentId id,\n\t\tSizeTag tag,\n\t\tint sizeOverride);\n\tCustomEmojiLoader(\n\t\tnot_null<DocumentData*> document,\n\t\tSizeTag tag,\n\t\tint sizeOverride);\n\n\t[[nodiscard]] DocumentData *document() const;\n\tvoid resolved(not_null<DocumentData*> document);\n\n\tQString entityData() override;\n\n\tvoid load(Fn<void(LoadResult)> loaded) override;\n\tbool loading() override;\n\tvoid cancel() override;\n\tUi::CustomEmoji::Preview preview() override;\n\nprivate:\n\tstruct Resolve {\n\t\tFn<void(LoadResult)> requested;\n\t\tQString entityData;\n\t};\n\tstruct Process {\n\t\tstd::shared_ptr<DocumentMedia> media;\n\t\tFn<void(LoadResult)> loaded;\n\t\tbase::has_weak_ptr guard;\n\t\trpl::lifetime lifetime;\n\t};\n\tstruct Requested {\n\t\tnot_null<DocumentData*> document;\n\t\tstd::unique_ptr<Process> process;\n\t};\n\tstruct Lookup : Requested {\n\t};\n\tstruct Load : Requested {\n\t};\n\n\tvoid check();\n\t[[nodiscard]] Storage::Cache::Key cacheKey(\n\t\tnot_null<DocumentData*> document) const;\n\tvoid startCacheLookup(\n\t\tnot_null<Lookup*> lookup,\n\t\tFn<void(LoadResult)> loaded);\n\tvoid lookupDone(\n\t\tnot_null<Lookup*> lookup,\n\t\tstd::optional<Ui::CustomEmoji::Cache> result);\n\tvoid loadNoCache(\n\t\tnot_null<DocumentData*> document,\n\t\tFn<void(LoadResult)> loaded);\n\n\t[[nodiscard]] static std::variant<Resolve, Lookup, Load> InitialState(\n\t\tnot_null<Session*> owner,\n\t\tDocumentId id);\n\n\tstd::variant<Resolve, Lookup, Load> _state;\n\tushort _sizeOverride = 0;\n\tSizeTag _tag = SizeTag::Normal;\n\n};\n\nCustomEmojiLoader::CustomEmojiLoader(\n\tnot_null<Session*> owner,\n\tDocumentId id,\n\tSizeTag tag,\n\tint sizeOverride)\n: _state(InitialState(owner, id))\n, _sizeOverride(sizeOverride)\n, _tag(tag) {\n\tExpects(sizeOverride >= 0\n\t\t&& sizeOverride <= std::numeric_limits<ushort>::max());\n}\n\nCustomEmojiLoader::CustomEmojiLoader(\n\tnot_null<DocumentData*> document,\n\tSizeTag tag,\n\tint sizeOverride)\n: _state(Lookup{ document })\n, _sizeOverride(sizeOverride)\n, _tag(tag) {\n\tExpects(sizeOverride >= 0\n\t\t&& sizeOverride <= std::numeric_limits<ushort>::max());\n}\n\nDocumentData *CustomEmojiLoader::document() const {\n\treturn v::match(_state, [](const Resolve &) {\n\t\treturn (DocumentData*)nullptr;\n\t}, [](const auto &data) {\n\t\treturn data.document.get();\n\t});\n}\n\nvoid CustomEmojiLoader::resolved(not_null<DocumentData*> document) {\n\tExpects(v::is<Resolve>(_state));\n\n\tauto requested = std::move(v::get<Resolve>(_state).requested);\n\t_state = Lookup{ document };\n\tif (requested) {\n\t\tload(std::move(requested));\n\t}\n}\n\nvoid CustomEmojiLoader::load(Fn<void(LoadResult)> loaded) {\n\tif (const auto resolve = std::get_if<Resolve>(&_state)) {\n\t\tresolve->requested = std::move(loaded);\n\t} else if (const auto lookup = std::get_if<Lookup>(&_state)) {\n\t\tif (!lookup->process) {\n\t\t\tstartCacheLookup(lookup, std::move(loaded));\n\t\t} else {\n\t\t\tlookup->process->loaded = std::move(loaded);\n\t\t}\n\t} else if (const auto load = std::get_if<Load>(&_state)) {\n\t\tif (!load->process) {\n\t\t\tload->process = std::make_unique<Process>(Process{\n\t\t\t\t.media = load->document->createMediaView(),\n\t\t\t\t.loaded = std::move(loaded),\n\t\t\t});\n\t\t\tload->process->media->owner()->resetCancelled();\n\t\t\tload->process->media->checkStickerLarge();\n\t\t\tif (load->process->media->loaded()) {\n\t\t\t\tcheck();\n\t\t\t} else {\n\t\t\t\tload->document->session().downloaderTaskFinished(\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\tcheck();\n\t\t\t\t}, load->process->lifetime);\n\t\t\t}\n\t\t} else {\n\t\t\tload->process->loaded = std::move(loaded);\n\t\t}\n\t}\n}\n\nQString CustomEmojiLoader::entityData() {\n\tif (const auto resolve = std::get_if<Resolve>(&_state)) {\n\t\treturn resolve->entityData;\n\t} else if (const auto lookup = std::get_if<Lookup>(&_state)) {\n\t\treturn SerializeCustomEmojiId(lookup->document);\n\t} else if (const auto load = std::get_if<Load>(&_state)) {\n\t\treturn SerializeCustomEmojiId(load->document);\n\t}\n\tUnexpected(\"State in CustomEmojiLoader::entityData.\");\n}\n\nbool CustomEmojiLoader::loading() {\n\tif (const auto resolve = std::get_if<Resolve>(&_state)) {\n\t\treturn (resolve->requested != nullptr);\n\t} else if (const auto lookup = std::get_if<Lookup>(&_state)) {\n\t\treturn (lookup->process != nullptr);\n\t} else if (const auto load = std::get_if<Load>(&_state)) {\n\t\treturn (load->process != nullptr);\n\t}\n\treturn false;\n}\n\nStorage::Cache::Key CustomEmojiLoader::cacheKey(\n\t\tnot_null<DocumentData*> document) const {\n\tconst auto baseKey = document->bigFileBaseCacheKey();\n\tif (!baseKey) {\n\t\treturn {};\n\t}\n\treturn Storage::Cache::Key{\n\t\tbaseKey.high,\n\t\tbaseKey.low + ChatHelpers::LottieCacheKeyShift(\n\t\t\t0x0F,\n\t\t\tLottieSizeFromTag(_tag)),\n\t};\n}\n\nvoid CustomEmojiLoader::startCacheLookup(\n\t\tnot_null<Lookup*> lookup,\n\t\tFn<void(LoadResult)> loaded) {\n\tconst auto document = lookup->document;\n\tconst auto key = cacheKey(document);\n\tif (!key) {\n\t\tloadNoCache(document, std::move(loaded));\n\t\treturn;\n\t}\n\tlookup->process = std::make_unique<Process>(Process{\n\t\t.loaded = std::move(loaded),\n\t});\n\tconst auto size = FrameSizeFromTag(_tag, _sizeOverride);\n\tconst auto weak = base::make_weak(&lookup->process->guard);\n\tdocument->owner().cacheBigFile().get(key, [=](QByteArray value) {\n\t\tauto cache = Ui::CustomEmoji::Cache::FromSerialized(value, size);\n\t\tcrl::on_main(weak, [=, result = std::move(cache)]() mutable {\n\t\t\tlookupDone(lookup, std::move(result));\n\t\t});\n\t});\n}\n\nvoid CustomEmojiLoader::lookupDone(\n\t\tnot_null<Lookup*> lookup,\n\t\tstd::optional<Ui::CustomEmoji::Cache> result) {\n\tconst auto document = lookup->document;\n\tif (!result) {\n\t\tloadNoCache(document, std::move(lookup->process->loaded));\n\t\treturn;\n\t}\n\tconst auto tag = _tag;\n\tconst auto sizeOverride = int(_sizeOverride);\n\tauto loader = [=] {\n\t\treturn std::make_unique<CustomEmojiLoader>(\n\t\t\tdocument,\n\t\t\ttag,\n\t\t\tsizeOverride);\n\t};\n\tauto done = std::move(lookup->process->loaded);\n\tdone(Ui::CustomEmoji::Cached(\n\t\tSerializeCustomEmojiId(document),\n\t\tstd::move(loader),\n\t\tstd::move(*result)));\n}\n\nvoid CustomEmojiLoader::loadNoCache(\n\t\tnot_null<DocumentData*> document,\n\t\tFn<void(LoadResult)> loaded) {\n\t_state = Load{ document };\n\tload(std::move(loaded));\n}\n\nvoid CustomEmojiLoader::check() {\n\tusing namespace Ui::CustomEmoji;\n\n\tconst auto load = std::get_if<Load>(&_state);\n\tAssert(load != nullptr);\n\tAssert(load->process != nullptr);\n\n\tconst auto media = load->process->media.get();\n\tconst auto document = media->owner();\n\tconst auto data = media->bytes();\n\tconst auto filepath = document->filepath();\n\tif (data.isEmpty() && filepath.isEmpty()) {\n\t\treturn;\n\t}\n\tload->process->lifetime.destroy();\n\n\tconst auto tag = _tag;\n\tconst auto sizeOverride = int(_sizeOverride);\n\tconst auto size = FrameSizeFromTag(_tag, _sizeOverride);\n\tauto loader = [=] {\n\t\treturn std::make_unique<CustomEmojiLoader>(\n\t\t\tdocument,\n\t\t\ttag,\n\t\t\tsizeOverride);\n\t};\n\tauto put = [=, key = cacheKey(document)](QByteArray value) {\n\t\tconst auto size = value.size();\n\t\tif (size <= Storage::kMaxFileInMemory) {\n\t\t\tdocument->owner().cacheBigFile().put(key, std::move(value));\n\t\t} else {\n\t\t\tLOG((\"Data Error: Cached emoji size too big: %1.\").arg(size));\n\t\t}\n\t};\n\tconst auto type = document->sticker()->type;\n\tauto generator = [=, bytes = Lottie::ReadContent(data, filepath)]()\n\t-> std::unique_ptr<Ui::FrameGenerator> {\n\t\tswitch (type) {\n\t\tcase StickerType::Tgs:\n\t\t\treturn std::make_unique<Lottie::FrameGenerator>(bytes);\n\t\tcase StickerType::Webm:\n\t\t\treturn std::make_unique<FFmpeg::FrameGenerator>(bytes);\n\t\tcase StickerType::Webp:\n\t\t\treturn std::make_unique<Ui::ImageFrameGenerator>(bytes);\n\t\t}\n\t\tUnexpected(\"Type in custom emoji sticker frame generator.\");\n\t};\n\tauto renderer = std::make_unique<Renderer>(RendererDescriptor{\n\t\t.generator = std::move(generator),\n\t\t.put = std::move(put),\n\t\t.loader = std::move(loader),\n\t\t.size = size,\n\t});\n\tbase::take(load->process)->loaded(Caching{\n\t\tstd::move(renderer),\n\t\tSerializeCustomEmojiId(document),\n\t});\n}\n\nauto CustomEmojiLoader::InitialState(\n\tnot_null<Session*> owner,\n\tDocumentId id)\n-> std::variant<Resolve, Lookup, Load> {\n\tconst auto document = owner->document(id);\n\tif (document->sticker()) {\n\t\treturn Lookup{ document };\n\t}\n\treturn Resolve{ .entityData = SerializeCustomEmojiId(id) };\n}\n\nvoid CustomEmojiLoader::cancel() {\n\tif (const auto lookup = std::get_if<Lookup>(&_state)) {\n\t\tbase::take(lookup->process);\n\t} else if (const auto load = std::get_if<Load>(&_state)) {\n\t\tif (base::take(load->process)) {\n\t\t\tload->document->cancel();\n\t\t}\n\t}\n}\n\nUi::CustomEmoji::Preview CustomEmojiLoader::preview() {\n\tusing Preview = Ui::CustomEmoji::Preview;\n\tconst auto make = [&](not_null<DocumentData*> document) -> Preview {\n\t\tconst auto dimensions = document->dimensions;\n\t\tif (!document->inlineThumbnailIsPath()\n\t\t\t|| dimensions.isEmpty()) {\n\t\t\treturn {};\n\t\t}\n\t\tconst auto scale = (FrameSizeFromTag(_tag, _sizeOverride) * 1.)\n\t\t\t/ (style::DevicePixelRatio() * dimensions.width());\n\t\treturn { document->createMediaView()->thumbnailPath(), scale };\n\t};\n\tif (const auto lookup = std::get_if<Lookup>(&_state)) {\n\t\treturn make(lookup->document);\n\t} else if (const auto load = std::get_if<Load>(&_state)) {\n\t\treturn make(load->document);\n\t}\n\treturn {};\n}\n\nCustomEmojiManager::CustomEmojiManager(not_null<Session*> owner)\n: _owner(owner)\n, _repaintTimer([=] { invokeRepaints(); }) {\n\tconst auto appConfig = &owner->session().appConfig();\n\tappConfig->value(\n\t) | rpl::take_while([=] {\n\t\treturn !_coloredSetId;\n\t}) | rpl::on_next([=] {\n\t\tconst auto setId = appConfig->get<QString>(\n\t\t\t\"default_emoji_statuses_stickerset_id\",\n\t\t\tQString()).toULongLong();\n\t\tif (setId) {\n\t\t\t_coloredSetId = setId;\n\t\t}\n\t}, _lifetime);\n}\n\nCustomEmojiManager::~CustomEmojiManager() = default;\n\ntemplate <typename LoaderFactory>\nstd::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(\n\t\tDocumentId documentId,\n\t\tFn<void()> update,\n\t\tSizeTag tag,\n\t\tint sizeOverride,\n\t\tLoaderFactory factory) {\n\tauto &instances = _instances[SizeIndex(tag)];\n\tauto i = instances.find(documentId);\n\tif (i == end(instances)) {\n\t\tusing Loading = Ui::CustomEmoji::Loading;\n\t\tconst auto repaint = [=](\n\t\t\t\tnot_null<Ui::CustomEmoji::Instance*> instance,\n\t\t\t\tUi::CustomEmoji::RepaintRequest request) {\n\t\t\trepaintLater(instance, request);\n\t\t};\n\t\tauto [loader, setId, colored] = factory();\n\t\ti = instances.emplace(\n\t\t\tdocumentId,\n\t\t\tstd::make_unique<Ui::CustomEmoji::Instance>(Loading{\n\t\t\t\tstd::move(loader),\n\t\t\t\tprepareNonExactPreview(documentId, tag, sizeOverride)\n\t\t\t}, std::move(repaint))).first;\n\t\tif (colored) {\n\t\t\ti->second->setColored();\n\t\t}\n\t} else if (!i->second->hasImagePreview()) {\n\t\tauto preview = prepareNonExactPreview(documentId, tag, sizeOverride);\n\t\tif (preview.isImage()) {\n\t\t\ti->second->updatePreview(std::move(preview));\n\t\t}\n\t}\n\treturn std::make_unique<Ui::CustomEmoji::Object>(\n\t\ti->second.get(),\n\t\tstd::move(update));\n}\n\nUi::Text::CustomEmojiFactory CustomEmojiManager::factory(\n\t\tSizeTag tag,\n\t\tint sizeOverride) {\n\treturn [=](QStringView data, const Ui::Text::MarkedContext &context) {\n\t\treturn create(data, context.repaint, tag, sizeOverride);\n\t};\n}\n\nUi::CustomEmoji::Preview CustomEmojiManager::prepareNonExactPreview(\n\t\tDocumentId documentId,\n\t\tSizeTag tag,\n\t\tint sizeOverride) const {\n\tfor (auto i = _instances.size(); i != 0;) {\n\t\tif (SizeIndex(tag) == --i) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto &other = _instances[i];\n\t\tconst auto j = other.find(documentId);\n\t\tif (j == end(other)) {\n\t\t\tcontinue;\n\t\t} else if (const auto nonExact = j->second->imagePreview()) {\n\t\t\tconst auto size = FrameSizeFromTag(tag, sizeOverride);\n\t\t\treturn {\n\t\t\t\tnonExact.image().scaled(\n\t\t\t\t\tsize,\n\t\t\t\t\tsize,\n\t\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\t\tQt::SmoothTransformation),\n\t\t\t\tfalse,\n\t\t\t};\n\t\t}\n\t}\n\treturn {};\n}\n\nstd::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(\n\t\tQStringView data,\n\t\tFn<void()> update,\n\t\tSizeTag tag,\n\t\tint sizeOverride) {\n\tif (data.startsWith(ScaledSimplePrefix())) {\n\t\tconst auto text = data.mid(ScaledSimplePrefix().size());\n\t\tconst auto emoji = Ui::Emoji::Find(text);\n\t\tAssert(emoji != nullptr);\n\t\treturn Ui::MakeScaledSimpleEmoji(emoji);\n\t} else if (data.startsWith(ScaledCustomPrefix())) {\n\t\tconst auto original = data.mid(ScaledCustomPrefix().size());\n\t\treturn Ui::MakeScaledCustomEmoji(\n\t\t\tcreate(original, std::move(update), SizeTag::Large));\n\t} else if (data.startsWith(ForceStaticPrefix())) {\n\t\tconst auto original = data.mid(ForceStaticPrefix().size());\n\t\treturn std::make_unique<Ui::Text::FirstFrameEmoji>(\n\t\t\tcreate(original, std::move(update), tag, sizeOverride));\n\t} else if (data.startsWith(UserpicEmojiPrefix())) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto size = EmojiSizeFromTag(tag) / ratio;\n\t\treturn userpic(data, std::move(update), size);\n\t} else if (data.startsWith(CollectiblePrefix())) {\n\t\tconst auto id = data.mid(CollectiblePrefix().size()).toULongLong();\n\t\tconst auto emojiStatuses = &session().data().emojiStatuses();\n\t\tauto info = emojiStatuses->collectibleInfo(id);\n\t\tAssert(info != nullptr);\n\t\tconst auto documentId = info->documentId;\n\t\tauto inner = create(documentId, base::duplicate(update), tag);\n\t\treturn Ui::Premium::MakeCollectibleEmoji(\n\t\t\tdata,\n\t\t\tinfo->centerColor,\n\t\t\tinfo->edgeColor,\n\t\t\tstd::move(inner),\n\t\t\tstd::move(update),\n\t\t\tFrameSizeFromTag(tag) / style::DevicePixelRatio());\n\t} else if (const auto parsed = Data::ParseTopicIconEmojiEntity(data)) {\n\t\treturn MakeTopicIconEmoji(parsed, std::move(update), tag);\n\t}\n\tconst auto parsed = ParseCustomEmojiData(data);\n\treturn parsed\n\t\t? create(parsed, std::move(update), tag, sizeOverride)\n\t\t: nullptr;\n}\n\nstd::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(\n\t\tDocumentId documentId,\n\t\tFn<void()> update,\n\t\tSizeTag tag,\n\t\tint sizeOverride) {\n\treturn create(documentId, std::move(update), tag, sizeOverride, [&] {\n\t\treturn createLoaderWithSetId(documentId, tag, sizeOverride);\n\t});\n}\n\nstd::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(\n\t\tnot_null<DocumentData*> document,\n\t\tFn<void()> update,\n\t\tSizeTag tag,\n\t\tint sizeOverride) {\n\treturn create(document->id, std::move(update), tag, sizeOverride, [&] {\n\t\treturn createLoaderWithSetId(document, tag, sizeOverride);\n\t});\n}\n\nstd::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::userpic(\n\t\tQStringView data,\n\t\tFn<void()> update,\n\t\tint size) {\n\tconst auto v = data.mid(UserpicEmojiPrefix().size()).split(',');\n\tif (v.size() != 5 && v.size() != 1) {\n\t\treturn nullptr;\n\t}\n\tauto image = std::shared_ptr<Ui::DynamicImage>();\n\tif (v[0] == u\"self\"_q) {\n\t\timage = Ui::MakeSavedMessagesThumbnail();\n\t} else if (v[0] == u\"replies\"_q) {\n\t\timage = Ui::MakeRepliesThumbnail();\n\t} else {\n\t\tconst auto id = PeerId(v[0].toULongLong());\n\t\timage = Ui::MakeUserpicThumbnail(_owner->peer(id));\n\t}\n\tconst auto padding = (v.size() == 5)\n\t\t? QMargins(v[1].toInt(), v[2].toInt(), v[3].toInt(), v[4].toInt())\n\t\t: QMargins();\n\treturn std::make_unique<Ui::CustomEmoji::DynamicImageEmoji>(\n\t\tdata.toString(),\n\t\tstd::move(image),\n\t\tstd::move(update),\n\t\tpadding,\n\t\tsize);\n}\n\nvoid CustomEmojiManager::resolve(\n\t\tQStringView data,\n\t\tnot_null<Listener*> listener) {\n\tresolve(ParseCustomEmojiData(data), listener);\n}\n\nvoid CustomEmojiManager::resolve(\n\t\tDocumentId documentId,\n\t\tnot_null<Listener*> listener) {\n\tif (_owner->document(documentId)->sticker()) {\n\t\treturn;\n\t}\n\t_resolvers[documentId].emplace(listener);\n\t_listeners[listener].emplace(documentId);\n\t_pendingForRequest.emplace(documentId);\n\tif (!_requestId && _pendingForRequest.size() == 1) {\n\t\tcrl::on_main(this, [=] { request(); });\n\t}\n}\n\nvoid CustomEmojiManager::unregisterListener(not_null<Listener*> listener) {\n\tif (const auto list = _listeners.take(listener)) {\n\t\tfor (const auto id : *list) {\n\t\t\tconst auto i = _resolvers.find(id);\n\t\t\tif (i != end(_resolvers)\n\t\t\t\t&& i->second.remove(listener)\n\t\t\t\t&& i->second.empty()) {\n\t\t\t\t_resolvers.erase(i);\n\t\t\t}\n\t\t}\n\t}\n}\n\nauto CustomEmojiManager::resolve(DocumentId documentId)\n-> rpl::producer<not_null<DocumentData*>, rpl::empty_error> {\n\treturn [=](auto consumer) {\n\t\tauto result = rpl::lifetime();\n\t\tconst auto put = [=](\n\t\t\t\tnot_null<DocumentData*> document,\n\t\t\t\tbool resolved = true) {\n\t\t\tif (!document->sticker()) {\n\t\t\t\tif (resolved) {\n\t\t\t\t\tconsumer.put_error({});\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconsumer.put_next_copy(document);\n\t\t\treturn true;\n\t\t};\n\t\tif (!put(owner().document(documentId), false)) {\n\t\t\tconst auto listener = result.make_state<CallbackListener>(put);\n\t\t\tresolve(documentId, listener);\n\t\t\tresult.add([=] {\n\t\t\t\tunregisterListener(listener);\n\t\t\t});\n\t\t}\n\t\treturn result;\n\t};\n}\n\nstd::unique_ptr<Ui::CustomEmoji::Loader> CustomEmojiManager::createLoader(\n\t\tnot_null<DocumentData*> document,\n\t\tSizeTag tag,\n\t\tint sizeOverride) {\n\treturn createLoaderWithSetId(document, tag, sizeOverride).loader;\n}\n\nstd::unique_ptr<Ui::CustomEmoji::Loader> CustomEmojiManager::createLoader(\n\t\tDocumentId documentId,\n\t\tSizeTag tag,\n\t\tint sizeOverride) {\n\treturn createLoaderWithSetId(documentId, tag, sizeOverride).loader;\n}\n\nauto CustomEmojiManager::createLoaderWithSetId(\n\tnot_null<DocumentData*> document,\n\tSizeTag tag,\n\tint sizeOverride\n) -> LoaderWithSetId {\n\tif (const auto sticker = document->sticker()) {\n\t\treturn {\n\t\t\tstd::make_unique<CustomEmojiLoader>(document, tag, sizeOverride),\n\t\t\tsticker->set.id,\n\t\t\tdocument->emojiUsesTextColor(),\n\t\t};\n\t}\n\treturn createLoaderWithSetId(document->id, tag, sizeOverride);\n}\n\nauto CustomEmojiManager::createLoaderWithSetId(\n\tDocumentId documentId,\n\tSizeTag tag,\n\tint sizeOverride\n) -> LoaderWithSetId {\n\tauto result = std::make_unique<CustomEmojiLoader>(\n\t\t_owner,\n\t\tdocumentId,\n\t\ttag,\n\t\tsizeOverride);\n\tif (const auto document = result->document()) {\n\t\tif (const auto sticker = document->sticker()) {\n\t\t\treturn {\n\t\t\t\tstd::move(result),\n\t\t\t\tsticker->set.id,\n\t\t\t\tdocument->emojiUsesTextColor(),\n\t\t\t};\n\t\t}\n\t} else {\n\t\tconst auto i = SizeIndex(tag);\n\t\t_loaders[i][documentId].push_back(base::make_weak(result));\n\t\t_pendingForRequest.emplace(documentId);\n\t\tif (!_requestId && _pendingForRequest.size() == 1) {\n\t\t\tcrl::on_main(this, [=] { request(); });\n\t\t}\n\t}\n\treturn { std::move(result), uint64(), false };\n}\n\nQString CustomEmojiManager::lookupSetName(uint64 setId) {\n\tconst auto &sets = _owner->stickers().sets();\n\tconst auto i = sets.find(setId);\n\treturn (i != end(sets)) ? i->second->title : QString();\n}\n\nvoid CustomEmojiManager::request() {\n\tauto ids = QVector<MTPlong>();\n\tids.reserve(std::min(kMaxPerRequest, int(_pendingForRequest.size())));\n\twhile (!_pendingForRequest.empty() && ids.size() < kMaxPerRequest) {\n\t\tconst auto i = _pendingForRequest.end() - 1;\n\t\tids.push_back(MTP_long(*i));\n\t\t_pendingForRequest.erase(i);\n\t}\n\tif (ids.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto api = &_owner->session().api();\n\t_requestId = api->request(MTPmessages_GetCustomEmojiDocuments(\n\t\tMTP_vector<MTPlong>(ids)\n\t)).done([=](const MTPVector<MTPDocument> &result) {\n\t\tfor (const auto &entry : result.v) {\n\t\t\tconst auto document = _owner->processDocument(entry);\n\t\t\tfillColoredFlags(document);\n\t\t\tprocessLoaders(document);\n\t\t\tprocessListeners(document);\n\t\t\trequestSetFor(document);\n\t\t}\n\t\trequestFinished();\n\t}).fail([=] {\n\t\tLOG((\"API Error: Failed to get documents for emoji.\"));\n\t\tfor (const auto &id : ids) {\n\t\t\tprocessListeners(_owner->document(id.v));\n\t\t}\n\t\trequestFinished();\n\t}).send();\n}\n\nvoid CustomEmojiManager::fillColoredFlags(not_null<DocumentData*> document) {\n\tif (document->emojiUsesTextColor()) {\n\t\tconst auto id = document->id;\n\t\tfor (auto &instances : _instances) {\n\t\t\tconst auto i = instances.find(id);\n\t\t\tif (i != end(instances)) {\n\t\t\t\ti->second->setColored();\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid CustomEmojiManager::processLoaders(not_null<DocumentData*> document) {\n\tconst auto id = document->id;\n\tfor (auto &loaders : _loaders) {\n\t\tif (const auto list = loaders.take(id)) {\n\t\t\tfor (const auto &weak : *list) {\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->resolved(document);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid CustomEmojiManager::processListeners(\n\t\tnot_null<DocumentData*> document) {\n\tconst auto id = document->id;\n\tif (const auto listeners = _resolvers.take(id)) {\n\t\tfor (const auto &listener : *listeners) {\n\t\t\tconst auto i = _listeners.find(listener);\n\t\t\tif (i != end(_listeners) && i->second.remove(id)) {\n\t\t\t\tif (i->second.empty()) {\n\t\t\t\t\t_listeners.erase(i);\n\t\t\t\t}\n\t\t\t\tlistener->customEmojiResolveDone(document);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid CustomEmojiManager::requestSetFor(not_null<DocumentData*> document) {\n\tconst auto sticker = document->sticker();\n\tif (!sticker || !sticker->set.id) {\n\t\treturn;\n\t}\n\tconst auto &sets = document->owner().stickers().sets();\n\tconst auto i = sets.find(sticker->set.id);\n\tif (i != end(sets)) {\n\t\treturn;\n\t}\n\tconst auto session = &document->session();\n\tsession->api().scheduleStickerSetRequest(\n\t\tsticker->set.id,\n\t\tsticker->set.accessHash);\n\tif (_requestSetsScheduled) {\n\t\treturn;\n\t}\n\t_requestSetsScheduled = true;\n\tcrl::on_main(this, [=] {\n\t\t_requestSetsScheduled = false;\n\t\tsession->api().requestStickerSets();\n\t});\n}\n\nint CustomEmojiManager::SizeIndex(SizeTag tag) {\n\tconst auto result = static_cast<int>(tag);\n\n\tEnsures(result >= 0 && result < kSizeCount);\n\treturn result;\n}\n\nvoid CustomEmojiManager::requestFinished() {\n\t_requestId = 0;\n\tif (!_pendingForRequest.empty()) {\n\t\trequest();\n\t}\n}\n\nvoid CustomEmojiManager::repaintLater(\n\t\tnot_null<Ui::CustomEmoji::Instance*> instance,\n\t\tUi::CustomEmoji::RepaintRequest request) {\n\tauto &bunch = _repaints[request.duration];\n\tif (bunch.when > 0) {\n\t\tfor (const auto &already : bunch.instances) {\n\t\t\tif (already.get() == instance) {\n\t\t\t\t// Still waiting for full bunch repaint, don't bump.\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\tif (bunch.when < request.when) {\n\t\tbunch.when = request.when;\n#if 0 // inject-to-on_main\n\t\t_repaintsLastAdded = request.when;\n#endif\n\t}\n\tbunch.instances.emplace_back(instance);\n\tscheduleRepaintTimer();\n}\n\nbool CustomEmojiManager::checkEmptyRepaints() {\n\tif (!_repaints.empty()) {\n\t\treturn false;\n#if 0 // inject-to-on_main\n\t} else if (_repaintsLifetime\n\t\t&& crl::now() >= _repaintsLastAdded + kUnsubscribeUpdatesDelay) {\n\t\t_repaintsLifetime.destroy();\n#endif\n\t}\n\treturn true;\n}\n\nvoid CustomEmojiManager::scheduleRepaintTimer() {\n\tif (checkEmptyRepaints() || _repaintTimerScheduled) {\n\t\treturn;\n\t}\n\n#if 0 // inject-to-on_main\n\tif (!_repaintsLifetime) {\n\t\tcrl::on_main_update_requests(\n\t\t) | rpl::on_next([=] {\n\t\t\tinvokeRepaints();\n\t\t}, _repaintsLifetime);\n\t}\n#endif\n\n\t_repaintTimerScheduled = true;\n\tUi::PostponeCall(this, [=] {\n\t\t_repaintTimerScheduled = false;\n\n\t\tauto next = crl::time();\n\t\tfor (const auto &[duration, bunch] : _repaints) {\n\t\t\tif (!next || next > bunch.when) {\n\t\t\t\tnext = bunch.when;\n\t\t\t}\n\t\t}\n\t\tif (next && (!_repaintNext || _repaintNext > next)) {\n\t\t\tconst auto now = crl::now();\n\t\t\tif (now >= next) {\n\t\t\t\t_repaintNext = 0;\n\t\t\t\t_repaintTimer.cancel();\n\t\t\t\tinvokeRepaints();\n\t\t\t} else {\n\t\t\t\t_repaintNext = next;\n\t\t\t\t_repaintTimer.callOnce(next - now);\n\t\t\t}\n\t\t}\n\t});\n}\n\nvoid CustomEmojiManager::invokeRepaints() {\n\t_repaintNext = 0;\n\tif (checkEmptyRepaints()) {\n\t\treturn;\n\t}\n\tconst auto now = crl::now();\n\tauto repaint = std::vector<base::weak_ptr<Ui::CustomEmoji::Instance>>();\n\tfor (auto i = begin(_repaints); i != end(_repaints);) {\n\t\tif (i->second.when > now) {\n\t\t\t++i;\n\t\t\tcontinue;\n\t\t}\n\t\tauto &list = i->second.instances;\n\t\tif (repaint.empty()) {\n\t\t\trepaint = std::move(list);\n\t\t} else {\n\t\t\trepaint.insert(\n\t\t\t\tend(repaint),\n\t\t\t\tstd::make_move_iterator(begin(list)),\n\t\t\t\tstd::make_move_iterator(end(list)));\n\t\t}\n\t\ti = _repaints.erase(i);\n\t}\n\tif (!repaint.empty()) {\n\t\tfor (const auto &weak : repaint) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->repaint();\n\t\t\t}\n\t\t}\n\t} else if (_repaintTimer.isActive()) {\n\t\treturn;\n\t}\n\tscheduleRepaintTimer();\n}\n\nMain::Session &CustomEmojiManager::session() const {\n\treturn _owner->session();\n}\n\nSession &CustomEmojiManager::owner() const {\n\treturn *_owner;\n}\n\nuint64 CustomEmojiManager::coloredSetId() const {\n\treturn _coloredSetId;\n}\n\n[[nodiscard]] QString CustomEmojiManager::peerUserpicEmojiData(\n\t\tnot_null<PeerData*> peer,\n\t\tQMargins padding,\n\t\tbool respectSavedRepliesEtc) {\n\tconst auto id = !respectSavedRepliesEtc\n\t\t? QString::number(peer->id.value)\n\t\t: peer->isSelf()\n\t\t? u\"self\"_q\n\t\t: peer->isRepliesChat()\n\t\t? u\"replies\"_q\n\t\t: QString::number(peer->id.value);\n\treturn UserpicEmojiPrefix() + id + InternalPadding(padding);\n}\n\nint FrameSizeFromTag(SizeTag tag) {\n\tconst auto emoji = EmojiSizeFromTag(tag);\n\tconst auto factor = style::DevicePixelRatio();\n\treturn Ui::Text::AdjustCustomEmojiSize(emoji / factor) * factor;\n}\n\nQString SerializeCustomEmojiId(DocumentId id) {\n\treturn QString::number(id);\n}\n\nQString SerializeCustomEmojiId(not_null<DocumentData*> document) {\n\treturn SerializeCustomEmojiId(document->id);\n}\n\nDocumentId ParseCustomEmojiData(QStringView data) {\n\treturn data.toULongLong();\n}\n\nTextWithEntities SingleCustomEmoji(DocumentId id) {\n\treturn Ui::Text::SingleCustomEmoji(SerializeCustomEmojiId(id));\n}\n\nTextWithEntities SingleCustomEmoji(not_null<DocumentData*> document) {\n\tconst auto sticker = document->sticker();\n\treturn Ui::Text::SingleCustomEmoji(\n\t\tSerializeCustomEmojiId(document),\n\t\tsticker ? sticker->alt : QString());\n}\n\nbool AllowEmojiWithoutPremium(\n\t\tnot_null<PeerData*> peer,\n\t\tDocumentData *exactEmoji) {\n\tif (peer->isSelf()) {\n\t\treturn true;\n\t} else if (!exactEmoji) {\n\t\treturn false;\n\t} else if (const auto sticker = exactEmoji->sticker()) {\n\t\tif (const auto channel = peer->asMegagroup()) {\n\t\t\tif (channel->mgInfo->emojiSet.id == sticker->set.id) {\n\t\t\t\treturn (sticker->set.id != 0);\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid InsertCustomEmoji(\n\t\tnot_null<Ui::InputField*> field,\n\t\tnot_null<DocumentData*> document) {\n\tconst auto sticker = document->sticker();\n\tif (!sticker || sticker->alt.isEmpty()) {\n\t\treturn;\n\t}\n\tUi::InsertCustomEmojiAtCursor(\n\t\tfield,\n\t\tfield->textCursor(),\n\t\tsticker->alt,\n\t\tUi::InputField::CustomEmojiLink(SerializeCustomEmojiId(document)));\n}\n\nUi::Text::CustomEmojiFactory ReactedMenuFactory(\n\t\tnot_null<Main::Session*> session) {\n\treturn [owner = &session->data()](\n\t\tQStringView data,\n\t\tconst Ui::Text::MarkedContext &context\n\t) -> std::unique_ptr<Ui::Text::CustomEmoji> {\n\t\tconst auto prefix = u\"default:\"_q;\n\t\tif (data.startsWith(prefix)) {\n\t\t\tconst auto &list = owner->reactions().list(\n\t\t\t\tData::Reactions::Type::All);\n\t\t\tconst auto emoji = data.mid(prefix.size()).toString();\n\t\t\tconst auto id = Data::ReactionId{ { emoji } };\n\t\t\tconst auto i = ranges::find(list, id, &Data::Reaction::id);\n\t\t\tif (i != end(list)) {\n\t\t\t\tconst auto document = i->centerIcon\n\t\t\t\t\t? not_null(i->centerIcon)\n\t\t\t\t\t: i->selectAnimation;\n\t\t\t\tconst auto size = st::emojiSize * (i->centerIcon ? 2 : 1);\n\t\t\t\tconst auto tag = Data::CustomEmojiManager::SizeTag::Normal;\n\t\t\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t\t\tconst auto skip = (Data::FrameSizeFromTag(tag) / ratio - size) / 2;\n\t\t\t\treturn std::make_unique<Ui::Text::FirstFrameEmoji>(\n\t\t\t\t\tstd::make_unique<Ui::Text::ShiftedEmoji>(\n\t\t\t\t\t\towner->customEmojiManager().create(\n\t\t\t\t\t\t\tdocument,\n\t\t\t\t\t\t\tcontext.repaint,\n\t\t\t\t\t\t\ttag,\n\t\t\t\t\t\t\tsize),\n\t\t\t\t\t\tQPoint(skip, skip)));\n\t\t\t}\n\t\t}\n\t\treturn owner->customEmojiManager().create(data, context.repaint);\n\t};\n}\n\nQString CollectibleCustomEmojiId(Data::EmojiStatusCollectible &data) {\n\treturn CollectiblePrefix() + QString::number(data.id);\n}\n\nQString EmojiStatusCustomId(const EmojiStatusId &id) {\n\treturn id.collectible\n\t\t? CollectibleCustomEmojiId(*id.collectible)\n\t\t: SerializeCustomEmojiId(id.documentId);\n}\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/stickers/data_custom_emoji.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/stickers/data_stickers_set.h\"\n#include \"ui/text/custom_emoji_instance.h\"\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nclass Session;\nclass CustomEmojiLoader;\n\nenum class CustomEmojiSizeTag : uchar {\n\tNormal,\n\tLarge,\n\tIsolated,\n\tSetIcon,\n\n\tkCount,\n};\n\nclass CustomEmojiManager final : public base::has_weak_ptr {\npublic:\n\tusing SizeTag = CustomEmojiSizeTag;\n\n\tCustomEmojiManager(not_null<Session*> owner);\n\t~CustomEmojiManager();\n\n\t[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> create(\n\t\tQStringView data,\n\t\tFn<void()> update,\n\t\tSizeTag tag = SizeTag::Normal,\n\t\tint sizeOverride = 0);\n\t[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> create(\n\t\tDocumentId documentId,\n\t\tFn<void()> update,\n\t\tSizeTag tag = SizeTag::Normal,\n\t\tint sizeOverride = 0);\n\t[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> create(\n\t\tnot_null<DocumentData*> document,\n\t\tFn<void()> update,\n\t\tSizeTag tag = SizeTag::Normal,\n\t\tint sizeOverride = 0);\n\n\t[[nodiscard]] Ui::Text::CustomEmojiFactory factory(\n\t\tSizeTag tag = SizeTag::Normal,\n\t\tint sizeOverride = 0);\n\n\tclass Listener {\n\tpublic:\n\t\tvirtual void customEmojiResolveDone(\n\t\t\tnot_null<DocumentData*> document) = 0;\n\t};\n\tvoid resolve(QStringView data, not_null<Listener*> listener);\n\tvoid resolve(DocumentId documentId, not_null<Listener*> listener);\n\tvoid unregisterListener(not_null<Listener*> listener);\n\n\t[[nodiscard]] auto resolve(DocumentId documentId)\n\t\t-> rpl::producer<not_null<DocumentData*>, rpl::empty_error>;\n\n\t[[nodiscard]] std::unique_ptr<Ui::CustomEmoji::Loader> createLoader(\n\t\tnot_null<DocumentData*> document,\n\t\tSizeTag tag,\n\t\tint sizeOverride = 0);\n\t[[nodiscard]] std::unique_ptr<Ui::CustomEmoji::Loader> createLoader(\n\t\tDocumentId documentId,\n\t\tSizeTag tag,\n\t\tint sizeOverride = 0);\n\n\t[[nodiscard]] QString lookupSetName(uint64 setId);\n\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] Session &owner() const;\n\n\t[[nodiscard]] QString peerUserpicEmojiData(\n\t\tnot_null<PeerData*> peer,\n\t\tQMargins padding = {},\n\t\tbool respectSavedRepliesEtc = false);\n\n\t[[nodiscard]] uint64 coloredSetId() const;\n\nprivate:\n\tstatic constexpr auto kSizeCount = int(SizeTag::kCount);\n\n\tstruct InternalEmojiData {\n\t\tQImage image;\n\t\tbool textColor = true;\n\t};\n\tstruct RepaintBunch {\n\t\tcrl::time when = 0;\n\t\tstd::vector<base::weak_ptr<Ui::CustomEmoji::Instance>> instances;\n\t};\n\tstruct LoaderWithSetId {\n\t\tstd::unique_ptr<Ui::CustomEmoji::Loader> loader;\n\t\tuint64 setId = 0;\n\t\tbool colored = false;\n\t};\n\n\t[[nodiscard]] LoaderWithSetId createLoaderWithSetId(\n\t\tnot_null<DocumentData*> document,\n\t\tSizeTag tag,\n\t\tint sizeOverride = 0);\n\t[[nodiscard]] LoaderWithSetId createLoaderWithSetId(\n\t\tDocumentId documentId,\n\t\tSizeTag tag,\n\t\tint sizeOverride = 0);\n\n\tvoid request();\n\tvoid requestFinished();\n\tvoid repaintLater(\n\t\tnot_null<Ui::CustomEmoji::Instance*> instance,\n\t\tUi::CustomEmoji::RepaintRequest request);\n\tvoid scheduleRepaintTimer();\n\tbool checkEmptyRepaints();\n\tvoid invokeRepaints();\n\tvoid fillColoredFlags(not_null<DocumentData*> document);\n\tvoid processLoaders(not_null<DocumentData*> document);\n\tvoid processListeners(not_null<DocumentData*> document);\n\tvoid requestSetFor(not_null<DocumentData*> document);\n\n\t[[nodiscard]] Ui::CustomEmoji::Preview prepareNonExactPreview(\n\t\tDocumentId documentId,\n\t\tSizeTag tag,\n\t\tint sizeOverride) const;\n\ttemplate <typename LoaderFactory>\n\t[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> create(\n\t\tDocumentId documentId,\n\t\tFn<void()> update,\n\t\tSizeTag tag,\n\t\tint sizeOverride,\n\t\tLoaderFactory factory);\n\t[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> userpic(\n\t\tQStringView data,\n\t\tFn<void()> update,\n\t\tint size);\n\t[[nodiscard]] static int SizeIndex(SizeTag tag);\n\n\tconst not_null<Session*> _owner;\n\n\tstd::array<\n\t\tstd::unordered_map<\n\t\t\tDocumentId,\n\t\t\tstd::unique_ptr<Ui::CustomEmoji::Instance>>,\n\t\tkSizeCount> _instances;\n\tstd::array<\n\t\tbase::flat_map<\n\t\t\tDocumentId,\n\t\t\tstd::vector<base::weak_ptr<CustomEmojiLoader>>>,\n\t\tkSizeCount> _loaders;\n\tbase::flat_map<\n\t\tDocumentId,\n\t\tbase::flat_set<not_null<Listener*>>> _resolvers;\n\tbase::flat_map<\n\t\tnot_null<Listener*>,\n\t\tbase::flat_set<DocumentId>> _listeners;\n\tbase::flat_set<DocumentId> _pendingForRequest;\n\n\tmtpRequestId _requestId = 0;\n\n\tuint64 _coloredSetId = 0;\n\n\tbase::flat_map<crl::time, RepaintBunch> _repaints;\n\tcrl::time _repaintNext = 0;\n\tbase::Timer _repaintTimer;\n\tbool _repaintTimerScheduled = false;\n\tbool _requestSetsScheduled = false;\n\n#if 0 // inject-to-on_main\n\tcrl::time _repaintsLastAdded = 0;\n\trpl::lifetime _repaintsLifetime;\n#endif\n\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] int FrameSizeFromTag(CustomEmojiManager::SizeTag tag);\n\n[[nodiscard]] QString SerializeCustomEmojiId(DocumentId id);\n[[nodiscard]] QString SerializeCustomEmojiId(\n\tnot_null<DocumentData*> document);\n[[nodiscard]] DocumentId ParseCustomEmojiData(QStringView data);\n\n[[nodiscard]] TextWithEntities SingleCustomEmoji(DocumentId id);\n[[nodiscard]] TextWithEntities SingleCustomEmoji(\n\tnot_null<DocumentData*> document);\n\n[[nodiscard]] bool AllowEmojiWithoutPremium(\n\tnot_null<PeerData*> peer,\n\tDocumentData *exactEmoji = nullptr);\n\nvoid InsertCustomEmoji(\n\tnot_null<Ui::InputField*> field,\n\tnot_null<DocumentData*> document);\n\n[[nodiscard]] Ui::Text::CustomEmojiFactory ReactedMenuFactory(\n\tnot_null<Main::Session*> session);\n\n[[nodiscard]] QString CollectibleCustomEmojiId(\n\tData::EmojiStatusCollectible &data);\n[[nodiscard]] QString EmojiStatusCustomId(const EmojiStatusId &id);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/stickers/data_stickers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/stickers/data_stickers.h\"\n\n#include \"api/api_hash.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"lang/lang_keys.h\"\n#include \"data/data_premium_limits.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"apiwrap.h\"\n#include \"storage/storage_account.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"main/main_session.h\"\n#include \"main/main_app_config.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/image/image_location_factory.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"mainwindow.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/abstract_box.h\" // Ui::show().\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Data {\nnamespace {\n\nconstexpr auto kPremiumToastDuration = 5 * crl::time(1000);\n\nusing SetFlag = StickersSetFlag;\n\n[[nodiscard]] TextWithEntities SavedGifsToast(\n\t\tconst Data::PremiumLimits &limits) {\n\tconst auto defaultLimit = limits.gifsDefault();\n\tconst auto premiumLimit = limits.gifsPremium();\n\treturn tr::bold(\n\t\ttr::lng_saved_gif_limit_title(tr::now, lt_count, defaultLimit)\n\t).append('\\n').append(\n\t\ttr::lng_saved_gif_limit_more(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tpremiumLimit,\n\t\t\tlt_link,\n\t\t\ttr::link(tr::lng_saved_gif_limit_link(tr::now)),\n\t\t\ttr::marked));\n}\n\n[[nodiscard]] TextWithEntities FaveStickersToast(\n\t\tconst Data::PremiumLimits &limits) {\n\tconst auto defaultLimit = limits.stickersFavedDefault();\n\tconst auto premiumLimit = limits.stickersFavedPremium();\n\treturn tr::bold(\n\t\ttr::lng_fave_sticker_limit_title(tr::now, lt_count, defaultLimit)\n\t).append('\\n').append(\n\t\ttr::lng_fave_sticker_limit_more(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tpremiumLimit,\n\t\t\tlt_link,\n\t\t\ttr::link(tr::lng_fave_sticker_limit_link(tr::now)),\n\t\t\ttr::marked));\n}\n\nvoid MaybeShowPremiumToast(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tTextWithEntities text,\n\t\tconst QString &ref) {\n\tif (!show) {\n\t\treturn;\n\t}\n\tconst auto session = &show->session();\n\tif (session->user()->isPremium()) {\n\t\treturn;\n\t}\n\tconst auto filter = [=](const auto ...) {\n\t\tif (const auto controller = show->resolveWindow()) {\n\t\t\tSettings::ShowPremium(controller, ref);\n\t\t}\n\t\treturn false;\n\t};\n\tshow->showToast({\n\t\t.text = std::move(text),\n\t\t.filter = filter,\n\t\t.duration = kPremiumToastDuration,\n\t});\n}\n\nvoid RemoveFromSet(\n\t\tStickersSets &sets,\n\t\tnot_null<DocumentData*> document,\n\t\tuint64 setId) {\n\tconst auto it = sets.find(setId);\n\tif (it == sets.end()) {\n\t\treturn;\n\t}\n\tconst auto set = it->second.get();\n\tconst auto index = set->stickers.indexOf(document);\n\tif (index < 0) {\n\t\treturn;\n\t}\n\tset->stickers.removeAt(index);\n\tif (!set->dates.empty()) {\n\t\tset->dates.erase(set->dates.begin() + index);\n\t}\n\tfor (auto i = set->emoji.begin(); i != set->emoji.end();) {\n\t\tconst auto index = i->second.indexOf(document);\n\t\tif (index >= 0) {\n\t\t\ti->second.removeAt(index);\n\t\t\tif (i->second.empty()) {\n\t\t\t\ti = set->emoji.erase(i);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\t++i;\n\t}\n\tif (set->stickers.empty()) {\n\t\tsets.erase(it);\n\t}\n}\n\n} // namespace\n\nStickers::Stickers(not_null<Session*> owner) : _owner(owner) {\n}\n\nSession &Stickers::owner() const {\n\treturn *_owner;\n}\n\nMain::Session &Stickers::session() const {\n\treturn _owner->session();\n}\n\nvoid Stickers::notifyUpdated(StickersType type) {\n\t_updated.fire_copy(type);\n}\n\nrpl::producer<StickersType> Stickers::updated() const {\n\treturn _updated.events();\n}\n\nrpl::producer<> Stickers::updated(StickersType type) const {\n\tusing namespace rpl::mappers;\n\treturn updated() | rpl::filter(_1 == type) | rpl::to_empty;\n}\n\nvoid Stickers::notifyRecentUpdated(StickersType type) {\n\t_recentUpdated.fire_copy(type);\n}\n\nrpl::producer<StickersType> Stickers::recentUpdated() const {\n\treturn _recentUpdated.events();\n}\n\nrpl::producer<> Stickers::recentUpdated(StickersType type) const {\n\tusing namespace rpl::mappers;\n\treturn recentUpdated() | rpl::filter(_1 == type) | rpl::to_empty;\n}\n\nvoid Stickers::notifySavedGifsUpdated() {\n\t_savedGifsUpdated.fire({});\n}\n\nrpl::producer<> Stickers::savedGifsUpdated() const {\n\treturn _savedGifsUpdated.events();\n}\n\nvoid Stickers::notifyStickerSetInstalled(uint64 setId) {\n\t_stickerSetInstalled.fire(std::move(setId));\n}\n\nrpl::producer<uint64> Stickers::stickerSetInstalled() const {\n\treturn _stickerSetInstalled.events();\n}\n\nvoid Stickers::notifyGifWithCaptionSent() {\n\t_gifWithCaptionSent.fire({});\n}\n\nrpl::producer<> Stickers::gifWithCaptionSent() const {\n\treturn _gifWithCaptionSent.events();\n}\n\nvoid Stickers::notifyEmojiSetInstalled(uint64 setId) {\n\t_emojiSetInstalled.fire(std::move(setId));\n}\n\nrpl::producer<uint64> Stickers::emojiSetInstalled() const {\n\treturn _emojiSetInstalled.events();\n}\n\nvoid Stickers::incrementSticker(not_null<DocumentData*> document) {\n\tif (!document->sticker() || !document->sticker()->set) {\n\t\treturn;\n\t}\n\n\tbool writeRecentStickers = false;\n\tauto &sets = setsRef();\n\tauto it = sets.find(Data::Stickers::CloudRecentSetId);\n\tif (it == sets.cend()) {\n\t\tit = sets.emplace(\n\t\t\tData::Stickers::CloudRecentSetId,\n\t\t\tstd::make_unique<Data::StickersSet>(\n\t\t\t\t&session().data(),\n\t\t\t\tData::Stickers::CloudRecentSetId,\n\t\t\t\tuint64(0), // accessHash\n\t\t\t\tuint64(0), // hash\n\t\t\t\ttr::lng_recent_stickers(tr::now),\n\t\t\t\tQString(),\n\t\t\t\t0, // count\n\t\t\t\tSetFlag::Special,\n\t\t\t\tTimeId(0))).first;\n\t} else {\n\t\tit->second->title = tr::lng_recent_stickers(tr::now);\n\t}\n\tconst auto set = it->second.get();\n\tauto removedFromEmoji = std::vector<not_null<EmojiPtr>>();\n\tauto index = set->stickers.indexOf(document);\n\tif (index > 0) {\n\t\tif (set->dates.empty()) {\n\t\t\tsession().api().requestSpecialStickersForce(false, true, false);\n\t\t} else {\n\t\t\tAssert(set->dates.size() == set->stickers.size());\n\t\t\tset->dates.erase(set->dates.begin() + index);\n\t\t}\n\t\tset->stickers.removeAt(index);\n\t\tfor (auto i = set->emoji.begin(); i != set->emoji.end();) {\n\t\t\tif (const auto index = i->second.indexOf(document); index >= 0) {\n\t\t\t\tremovedFromEmoji.emplace_back(i->first);\n\t\t\t\ti->second.removeAt(index);\n\t\t\t\tif (i->second.empty()) {\n\t\t\t\t\ti = set->emoji.erase(i);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t}\n\tif (index) {\n\t\tif (set->dates.size() == set->stickers.size()) {\n\t\t\tset->dates.insert(set->dates.begin(), base::unixtime::now());\n\t\t}\n\t\tset->stickers.push_front(document);\n\t\tif (const auto emojiList = getEmojiListFromSet(document)) {\n\t\t\tfor (const auto &emoji : *emojiList) {\n\t\t\t\tset->emoji[emoji].push_front(document);\n\t\t\t}\n\t\t} else if (!removedFromEmoji.empty()) {\n\t\t\tfor (const auto &emoji : removedFromEmoji) {\n\t\t\t\tset->emoji[emoji].push_front(document);\n\t\t\t}\n\t\t} else {\n\t\t\tsession().api().requestSpecialStickersForce(false, true, false);\n\t\t}\n\n\t\twriteRecentStickers = true;\n\t}\n\n\t// Remove that sticker from old recent, now it is in cloud recent stickers.\n\tbool writeOldRecent = false;\n\tauto &recent = getRecentPack();\n\tfor (auto i = recent.begin(), e = recent.end(); i != e; ++i) {\n\t\tif (i->first == document) {\n\t\t\twriteOldRecent = true;\n\t\t\trecent.erase(i);\n\t\t\tbreak;\n\t\t}\n\t}\n\twhile (!recent.isEmpty()\n\t\t&& (set->stickers.size() + recent.size()\n\t\t\t> session().serverConfig().stickersRecentLimit)) {\n\t\twriteOldRecent = true;\n\t\trecent.pop_back();\n\t}\n\n\tif (writeOldRecent) {\n\t\tsession().saveSettings();\n\t}\n\n\t// Remove that sticker from custom stickers, now it is in cloud recent stickers.\n\tbool writeInstalledStickers = false;\n\tauto customIt = sets.find(Data::Stickers::CustomSetId);\n\tif (customIt != sets.cend()) {\n\t\tconst auto custom = customIt->second.get();\n\t\tint removeIndex = custom->stickers.indexOf(document);\n\t\tif (removeIndex >= 0) {\n\t\t\tcustom->stickers.removeAt(removeIndex);\n\t\t\tif (custom->stickers.isEmpty()) {\n\t\t\t\tsets.erase(customIt);\n\t\t\t}\n\t\t\twriteInstalledStickers = true;\n\t\t}\n\t}\n\n\tif (writeInstalledStickers) {\n\t\tsession().local().writeInstalledStickers();\n\t}\n\tif (writeRecentStickers) {\n\t\tsession().local().writeRecentStickers();\n\t}\n\tnotifyRecentUpdated(StickersType::Stickers);\n}\n\nvoid Stickers::addSavedGif(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document) {\n\tconst auto index = _savedGifs.indexOf(document);\n\tif (!index) {\n\t\treturn;\n\t}\n\tif (index > 0) {\n\t\t_savedGifs.remove(index);\n\t}\n\t_savedGifs.push_front(document);\n\tconst auto session = &document->session();\n\tconst auto limits = Data::PremiumLimits(session);\n\tif (_savedGifs.size() > limits.gifsCurrent()) {\n\t\t_savedGifs.pop_back();\n\t\tMaybeShowPremiumToast(\n\t\t\tshow,\n\t\t\tSavedGifsToast(limits),\n\t\t\tLimitsPremiumRef(\"saved_gifs\"));\n\t}\n\tsession->local().writeSavedGifs();\n\n\tnotifySavedGifsUpdated();\n\tsetLastSavedGifsUpdate(0);\n\tsession->api().updateSavedGifs();\n}\n\nvoid Stickers::checkSavedGif(not_null<HistoryItem*> item) {\n\tif (item->Has<HistoryMessageForwarded>()\n\t\t|| (!item->out()\n\t\t\t&& item->history()->peer != session().user())) {\n\t\treturn;\n\t}\n\tif (const auto media = item->media()) {\n\t\tif (const auto document = media->document()) {\n\t\t\tif (document->isGifv()) {\n\t\t\t\taddSavedGif(nullptr, document);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Stickers::applyArchivedResult(\n\t\tconst MTPDmessages_stickerSetInstallResultArchive &d) {\n\tauto &v = d.vsets().v;\n\tStickersSetsOrder archived;\n\tarchived.reserve(v.size());\n\tQMap<uint64, uint64> setsToRequest;\n\n\tauto masksCount = 0;\n\tauto stickersCount = 0;\n\tfor (const auto &data : v) {\n\t\tconst auto set = feedSet(data);\n\t\tif (set->flags & SetFlag::NotLoaded) {\n\t\t\tsetsToRequest.insert(set->id, set->accessHash);\n\t\t}\n\t\tif (set->type() == StickersType::Emoji) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto isMasks = (set->type() == StickersType::Masks);\n\t\t(isMasks ? masksCount : stickersCount)++;\n\t\tauto &order = isMasks ? maskSetsOrderRef() : setsOrderRef();\n\t\tconst auto index = order.indexOf(set->id);\n\t\tif (index >= 0) {\n\t\t\torder.removeAt(index);\n\t\t}\n\t\tarchived.push_back(set->id);\n\t}\n\tif (!setsToRequest.isEmpty()) {\n\t\tfor (auto i = setsToRequest.cbegin(), e = setsToRequest.cend(); i != e; ++i) {\n\t\t\tsession().api().scheduleStickerSetRequest(i.key(), i.value());\n\t\t}\n\t\tsession().api().requestStickerSets();\n\t}\n\tif (stickersCount) {\n\t\tsession().local().writeInstalledStickers();\n\t\tsession().local().writeArchivedStickers();\n\t}\n\tif (masksCount) {\n\t\tsession().local().writeInstalledMasks();\n\t\tsession().local().writeArchivedMasks();\n\t}\n\n\t// TODO async toast.\n\tUi::Toast::Show(Ui::Toast::Config{\n\t\t.text = { tr::lng_stickers_packs_archived(tr::now) },\n\t\t.st = &st::stickersToast,\n\t});\n\t//Ui::show(\n\t//\tBox<StickersBox>(archived, &session()),\n\t//\tUi::LayerOption::KeepOther);\n\tif (stickersCount) {\n\t\tnotifyUpdated(StickersType::Stickers);\n\t}\n\tif (masksCount) {\n\t\tnotifyUpdated(StickersType::Masks);\n\t}\n}\n\nvoid Stickers::installLocally(uint64 setId) {\n\tauto &sets = setsRef();\n\tauto it = sets.find(setId);\n\tif (it == sets.end()) {\n\t\treturn;\n\t}\n\n\tconst auto set = it->second.get();\n\tconst auto flags = set->flags;\n\tset->flags &= ~(SetFlag::Archived | SetFlag::Unread);\n\tset->flags |= SetFlag::Installed;\n\tset->installDate = base::unixtime::now();\n\tauto changedFlags = flags ^ set->flags;\n\n\tconst auto isMasks = (set->type() == StickersType::Masks);\n\tconst auto isEmoji = (set->type() == StickersType::Emoji);\n\tauto &order = isEmoji\n\t\t? emojiSetsOrderRef()\n\t\t: isMasks\n\t\t? maskSetsOrderRef()\n\t\t: setsOrderRef();\n\tint insertAtIndex = 0, currentIndex = order.indexOf(setId);\n\tif (currentIndex != insertAtIndex) {\n\t\tif (currentIndex > 0) {\n\t\t\torder.removeAt(currentIndex);\n\t\t}\n\t\torder.insert(insertAtIndex, setId);\n\t}\n\n\tauto customIt = sets.find(CustomSetId);\n\tif (customIt != sets.cend()) {\n\t\tconst auto custom = customIt->second.get();\n\t\tfor (const auto sticker : std::as_const(set->stickers)) {\n\t\t\tint removeIndex = custom->stickers.indexOf(sticker);\n\t\t\tif (removeIndex >= 0) custom->stickers.removeAt(removeIndex);\n\t\t}\n\t\tif (custom->stickers.isEmpty()) {\n\t\t\tsets.erase(customIt);\n\t\t}\n\t}\n\tsession().local().writeInstalledStickers();\n\tif (!isMasks && (changedFlags & SetFlag::Unread)) {\n\t\tif (isEmoji) {\n\t\t\tsession().local().writeFeaturedCustomEmoji();\n\t\t} else {\n\t\t\tsession().local().writeFeaturedStickers();\n\t\t}\n\t}\n\tif (!isEmoji && (changedFlags & SetFlag::Archived)) {\n\t\tauto &archivedOrder = isMasks\n\t\t\t? archivedMaskSetsOrderRef()\n\t\t\t: archivedSetsOrderRef();\n\t\tconst auto index = archivedOrder.indexOf(setId);\n\t\tif (index >= 0) {\n\t\t\tarchivedOrder.removeAt(index);\n\t\t\tif (isMasks) {\n\t\t\t\tsession().local().writeArchivedMasks();\n\t\t\t} else {\n\t\t\t\tsession().local().writeArchivedStickers();\n\t\t\t}\n\t\t}\n\t}\n\tnotifyUpdated(set->type());\n}\n\nvoid Stickers::undoInstallLocally(uint64 setId) {\n\tconst auto &sets = this->sets();\n\tconst auto it = sets.find(setId);\n\tif (it == sets.end()) {\n\t\treturn;\n\t}\n\n\tconst auto set = it->second.get();\n\tset->flags &= ~SetFlag::Installed;\n\tset->installDate = TimeId(0);\n\n\tauto &order = setsOrderRef();\n\tint currentIndex = order.indexOf(setId);\n\tif (currentIndex >= 0) {\n\t\torder.removeAt(currentIndex);\n\t}\n\n\tsession().local().writeInstalledStickers();\n\tnotifyUpdated(set->type());\n\n\tUi::show(\n\t\tUi::MakeInformBox(tr::lng_stickers_not_found()),\n\t\tUi::LayerOption::KeepOther);\n}\n\nbool Stickers::isFaved(not_null<const DocumentData*> document) const {\n\tconst auto &sets = this->sets();\n\tconst auto it = sets.find(FavedSetId);\n\tif (it == sets.cend()) {\n\t\treturn false;\n\t}\n\tfor (const auto sticker : std::as_const(it->second->stickers)) {\n\t\tif (sticker == document) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Stickers::checkFavedLimit(\n\t\tStickersSet &set,\n\t\tstd::shared_ptr<ChatHelpers::Show> show) {\n\tconst auto session = &_owner->session();\n\tconst auto limits = Data::PremiumLimits(session);\n\tif (set.stickers.size() <= limits.stickersFavedCurrent()) {\n\t\treturn;\n\t}\n\tauto removing = set.stickers.back();\n\tset.stickers.pop_back();\n\tfor (auto i = set.emoji.begin(); i != set.emoji.end();) {\n\t\tauto index = i->second.indexOf(removing);\n\t\tif (index >= 0) {\n\t\t\ti->second.removeAt(index);\n\t\t\tif (i->second.empty()) {\n\t\t\t\ti = set.emoji.erase(i);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\t++i;\n\t}\n\tMaybeShowPremiumToast(\n\t\tstd::move(show),\n\t\tFaveStickersToast(limits),\n\t\tLimitsPremiumRef(\"stickers_faved\"));\n}\n\nvoid Stickers::pushFavedToFront(\n\t\tStickersSet &set,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document,\n\t\tconst std::vector<not_null<EmojiPtr>> &emojiList) {\n\tset.stickers.push_front(document);\n\tfor (auto emoji : emojiList) {\n\t\tset.emoji[emoji].push_front(document);\n\t}\n\tcheckFavedLimit(set, std::move(show));\n}\n\nvoid Stickers::moveFavedToFront(StickersSet &set, int index) {\n\tExpects(index > 0 && index < set.stickers.size());\n\n\tauto document = set.stickers[index];\n\twhile (index-- != 0) {\n\t\tset.stickers[index + 1] = set.stickers[index];\n\t}\n\tset.stickers[0] = document;\n\tfor (auto &[emoji, list] : set.emoji) {\n\t\tauto index = list.indexOf(document);\n\t\tif (index > 0) {\n\t\t\twhile (index-- != 0) {\n\t\t\t\tlist[index + 1] = list[index];\n\t\t\t}\n\t\t\tlist[0] = document;\n\t\t}\n\t}\n}\n\nvoid Stickers::setIsFaved(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document,\n\t\tstd::optional<std::vector<not_null<EmojiPtr>>> emojiList) {\n\tauto &sets = setsRef();\n\tauto it = sets.find(FavedSetId);\n\tif (it == sets.end()) {\n\t\tit = sets.emplace(FavedSetId, std::make_unique<StickersSet>(\n\t\t\t&document->owner(),\n\t\t\tFavedSetId,\n\t\t\tuint64(0), // accessHash\n\t\t\tuint64(0), // hash\n\t\t\tLang::Hard::FavedSetTitle(),\n\t\t\tQString(),\n\t\t\t0, // count\n\t\t\tSetFlag::Special,\n\t\t\tTimeId(0))).first;\n\t}\n\tconst auto set = it->second.get();\n\tauto index = set->stickers.indexOf(document);\n\tif (index == 0) {\n\t\treturn;\n\t}\n\tif (index > 0) {\n\t\tmoveFavedToFront(*set, index);\n\t} else if (emojiList) {\n\t\tpushFavedToFront(*set, show, document, *emojiList);\n\t} else if (auto list = getEmojiListFromSet(document)) {\n\t\tpushFavedToFront(*set, show, document, *list);\n\t} else {\n\t\trequestSetToPushFaved(show, document);\n\t\treturn;\n\t}\n\tsession().local().writeFavedStickers();\n\tnotifyUpdated(StickersType::Stickers);\n\tnotifyStickerSetInstalled(FavedSetId);\n}\n\nvoid Stickers::requestSetToPushFaved(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document) {\n\tauto addAnyway = [=](std::vector<not_null<EmojiPtr>> list) {\n\t\tif (list.empty()) {\n\t\t\tif (auto sticker = document->sticker()) {\n\t\t\t\tif (auto emoji = Ui::Emoji::Find(sticker->alt)) {\n\t\t\t\t\tlist.push_back(emoji);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tsetIsFaved(nullptr, document, std::move(list));\n\t};\n\tsession().api().request(MTPmessages_GetStickerSet(\n\t\tData::InputStickerSet(document->sticker()->set),\n\t\tMTP_int(0) // hash\n\t)).done([=](const MTPmessages_StickerSet &result) {\n\t\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\tauto list = std::vector<not_null<EmojiPtr>>();\n\t\t\tlist.reserve(data.vpacks().v.size());\n\t\t\tfor (const auto &mtpPack : data.vpacks().v) {\n\t\t\t\tauto &pack = mtpPack.c_stickerPack();\n\t\t\t\tfor (const auto &documentId : pack.vdocuments().v) {\n\t\t\t\t\tif (documentId.v == document->id) {\n\t\t\t\t\t\tif (const auto emoji = Ui::Emoji::Find(qs(mtpPack.c_stickerPack().vemoticon()))) {\n\t\t\t\t\t\t\tlist.emplace_back(emoji);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\taddAnyway(std::move(list));\n\t\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\t\tLOG((\"API Error: Unexpected messages.stickerSetNotModified.\"));\n\t\t});\n\t}).fail([=] {\n\t\t// Perhaps this is a deleted sticker pack. Add anyway.\n\t\taddAnyway({});\n\t}).send();\n}\n\nvoid Stickers::removeFromRecentSet(not_null<DocumentData*> document) {\n\tRemoveFromSet(setsRef(), document, CloudRecentSetId);\n\tsession().local().writeRecentStickers();\n\tnotifyRecentUpdated(StickersType::Stickers);\n}\n\nvoid Stickers::setIsNotFaved(not_null<DocumentData*> document) {\n\tRemoveFromSet(setsRef(), document, FavedSetId);\n\tsession().local().writeFavedStickers();\n\tnotifyUpdated(StickersType::Stickers);\n}\n\nvoid Stickers::setFaved(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document,\n\t\tbool faved) {\n\tif (faved) {\n\t\tsetIsFaved(std::move(show), document);\n\t} else {\n\t\tsetIsNotFaved(document);\n\t}\n}\n\nvoid Stickers::setsReceived(\n\t\tconst QVector<MTPStickerSet> &data,\n\t\tuint64 hash) {\n\tsomethingReceived(data, hash, StickersType::Stickers);\n}\n\nvoid Stickers::masksReceived(\n\t\tconst QVector<MTPStickerSet> &data,\n\t\tuint64 hash) {\n\tsomethingReceived(data, hash, StickersType::Masks);\n}\n\nvoid Stickers::emojiReceived(\n\t\tconst QVector<MTPStickerSet> &data,\n\t\tuint64 hash) {\n\tsomethingReceived(data, hash, StickersType::Emoji);\n}\n\nvoid Stickers::somethingReceived(\n\t\tconst QVector<MTPStickerSet> &list,\n\t\tuint64 hash,\n\t\tStickersType type) {\n\tauto &setsOrder = (type == StickersType::Emoji)\n\t\t? emojiSetsOrderRef()\n\t\t: (type == StickersType::Masks)\n\t\t? maskSetsOrderRef()\n\t\t: setsOrderRef();\n\tsetsOrder.clear();\n\n\tauto &sets = setsRef();\n\tQMap<uint64, uint64> setsToRequest;\n\tfor (auto &[id, set] : sets) {\n\t\tconst auto archived = !!(set->flags & SetFlag::Archived);\n\t\tif (!archived && (type == set->type())) {\n\t\t\t// Mark for removing.\n\t\t\tset->flags &= ~SetFlag::Installed;\n\t\t\tset->installDate = 0;\n\t\t}\n\t}\n\tfor (const auto &info : list) {\n\t\tconst auto set = feedSet(info);\n\t\tif (!(set->flags & SetFlag::Archived)\n\t\t\t|| (set->flags & SetFlag::Official)) {\n\t\t\tsetsOrder.push_back(set->id);\n\t\t\tif (set->stickers.isEmpty()\n\t\t\t\t|| (set->flags & SetFlag::NotLoaded)) {\n\t\t\t\tsetsToRequest.insert(set->id, set->accessHash);\n\t\t\t}\n\t\t}\n\t}\n\tauto writeRecent = false;\n\tauto &recent = getRecentPack();\n\tfor (auto it = sets.begin(); it != sets.end();) {\n\t\tconst auto set = it->second.get();\n\t\tconst auto installed = !!(set->flags & SetFlag::Installed);\n\t\tconst auto featured = !!(set->flags & SetFlag::Featured);\n\t\tconst auto special = !!(set->flags & SetFlag::Special);\n\t\tconst auto archived = !!(set->flags & SetFlag::Archived);\n\t\tconst auto emoji = !!(set->flags & SetFlag::Emoji);\n\t\tconst auto locked = (set->locked > 0);\n\t\tif (!installed) { // remove not mine sets from recent stickers\n\t\t\tfor (auto i = recent.begin(); i != recent.cend();) {\n\t\t\t\tif (set->stickers.indexOf(i->first) >= 0) {\n\t\t\t\t\ti = recent.erase(i);\n\t\t\t\t\twriteRecent = true;\n\t\t\t\t} else {\n\t\t\t\t\t++i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (installed || featured || special || archived || emoji || locked) {\n\t\t\t++it;\n\t\t} else {\n\t\t\tit = sets.erase(it);\n\t\t}\n\t}\n\n\tif (!setsToRequest.isEmpty()) {\n\t\tauto &api = session().api();\n\t\tfor (auto i = setsToRequest.cbegin(), e = setsToRequest.cend(); i != e; ++i) {\n\t\t\tapi.scheduleStickerSetRequest(i.key(), i.value());\n\t\t}\n\t\tapi.requestStickerSets();\n\t}\n\n\tif (type == StickersType::Emoji) {\n\t\tsession().local().writeInstalledCustomEmoji();\n\t} else if (type == StickersType::Masks) {\n\t\tsession().local().writeInstalledMasks();\n\t} else {\n\t\tsession().local().writeInstalledStickers();\n\t}\n\tif (writeRecent) {\n\t\tsession().saveSettings();\n\t}\n\n\tconst auto counted = (type == StickersType::Emoji)\n\t\t? Api::CountCustomEmojiHash(&session())\n\t\t: (type == StickersType::Masks)\n\t\t? Api::CountMasksHash(&session())\n\t\t: Api::CountStickersHash(&session());\n\tif (counted != hash) {\n\t\tLOG((\"API Error: received %1 hash %2 while counted hash is %3\"\n\t\t\t).arg((type == StickersType::Emoji)\n\t\t\t\t? \"custom-emoji\"\n\t\t\t\t: (type == StickersType::Masks)\n\t\t\t\t? \"masks\"\n\t\t\t\t: \"stickers\"\n\t\t\t).arg(hash\n\t\t\t).arg(counted));\n\t}\n\n\tnotifyUpdated(type);\n}\n\nvoid Stickers::setPackAndEmoji(\n\t\tStickersSet &set,\n\t\tStickersPack &&pack,\n\t\tstd::vector<TimeId> &&dates,\n\t\tconst QVector<MTPStickerPack> &packs) {\n\tset.stickers = std::move(pack);\n\tset.dates = std::move(dates);\n\tset.emoji.clear();\n\tfor (const auto &mtpPack : packs) {\n\t\tAssert(mtpPack.type() == mtpc_stickerPack);\n\t\tauto &pack = mtpPack.c_stickerPack();\n\t\tif (auto emoji = Ui::Emoji::Find(qs(pack.vemoticon()))) {\n\t\t\temoji = emoji->original();\n\t\t\tauto &stickers = pack.vdocuments().v;\n\n\t\t\tauto p = StickersPack();\n\t\t\tp.reserve(stickers.size());\n\t\t\tfor (auto j = 0, c = int(stickers.size()); j != c; ++j) {\n\t\t\t\tauto document = owner().document(stickers[j].v);\n\t\t\t\tif (!document || !document->sticker()) continue;\n\n\t\t\t\tp.push_back(document);\n\t\t\t}\n\t\t\tset.emoji[emoji] = std::move(p);\n\t\t}\n\t}\n}\n\nnot_null<StickersSet*> Stickers::collectibleSet() {\n\tconst auto setId = CollectibleSetId;\n\tauto &sets = setsRef();\n\tauto it = sets.find(setId);\n\tif (it == sets.cend()) {\n\t\tit = sets.emplace(setId, std::make_unique<StickersSet>(\n\t\t\t\t&owner(),\n\t\t\t\tsetId,\n\t\t\t\tuint64(0), // accessHash\n\t\t\t\tuint64(0), // hash\n\t\t\t\ttr::lng_collectible_emoji(tr::now),\n\t\t\t\tQString(),\n\t\t\t\t0, // count\n\t\t\t\tSetFlag::Special,\n\t\t\t\tTimeId(0))).first;\n\t}\n\treturn it->second.get();\n}\n\nvoid Stickers::specialSetReceived(\n\t\tuint64 setId,\n\t\tconst QString &setTitle,\n\t\tconst QVector<MTPDocument> &items,\n\t\tuint64 hash,\n\t\tconst QVector<MTPStickerPack> &packs,\n\t\tconst QVector<MTPint> &usageDates) {\n\tauto &sets = setsRef();\n\tauto it = sets.find(setId);\n\n\tif (items.isEmpty()) {\n\t\tif (it != sets.cend()) {\n\t\t\tsets.erase(it);\n\t\t}\n\t} else {\n\t\tif (it == sets.cend()) {\n\t\t\tit = sets.emplace(setId, std::make_unique<StickersSet>(\n\t\t\t\t&owner(),\n\t\t\t\tsetId,\n\t\t\t\tuint64(0), // accessHash\n\t\t\t\tuint64(0), // hash\n\t\t\t\tsetTitle,\n\t\t\t\tQString(),\n\t\t\t\t0, // count\n\t\t\t\tSetFlag::Special,\n\t\t\t\tTimeId(0))).first;\n\t\t} else {\n\t\t\tit->second->title = setTitle;\n\t\t}\n\t\tconst auto set = it->second.get();\n\t\tset->hash = hash;\n\n\t\tauto dates = std::vector<TimeId>();\n\t\tauto dateIndex = 0;\n\t\tauto datesAvailable = (items.size() == usageDates.size())\n\t\t\t&& ((setId == CloudRecentSetId)\n\t\t\t\t|| (setId == CloudRecentAttachedSetId));\n\n\t\tauto customIt = sets.find(CustomSetId);\n\t\tauto pack = StickersPack();\n\t\tpack.reserve(items.size());\n\t\tfor (const auto &item : items) {\n\t\t\t++dateIndex;\n\t\t\tconst auto document = owner().processDocument(item);\n\t\t\tif (!document->sticker()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tpack.push_back(document);\n\t\t\tif (datesAvailable) {\n\t\t\t\tdates.push_back(TimeId(usageDates[dateIndex - 1].v));\n\t\t\t}\n\t\t\tif (customIt != sets.cend()) {\n\t\t\t\tconst auto custom = customIt->second.get();\n\t\t\t\tauto index = custom->stickers.indexOf(document);\n\t\t\t\tif (index >= 0) {\n\t\t\t\t\tcustom->stickers.removeAt(index);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (customIt != sets.cend()\n\t\t\t&& customIt->second->stickers.isEmpty()) {\n\t\t\tsets.erase(customIt);\n\t\t\tcustomIt = sets.end();\n\t\t}\n\n\t\tauto writeRecent = false;\n\t\tauto &recent = getRecentPack();\n\t\tfor (auto i = recent.begin(); i != recent.cend();) {\n\t\t\tif (set->stickers.indexOf(i->first) >= 0 && pack.indexOf(i->first) < 0) {\n\t\t\t\ti = recent.erase(i);\n\t\t\t\twriteRecent = true;\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\n\t\tif (pack.isEmpty()) {\n\t\t\tsets.erase(it);\n\t\t} else {\n\t\t\tsetPackAndEmoji(*set, std::move(pack), std::move(dates), packs);\n\t\t}\n\n\t\tif (writeRecent) {\n\t\t\tsession().saveSettings();\n\t\t}\n\t}\n\n\tswitch (setId) {\n\tcase CloudRecentSetId: {\n\t\tconst auto counted = Api::CountRecentStickersHash(&session());\n\t\tif (counted != hash) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"received recent stickers hash %1 while counted hash is %2\"\n\t\t\t\t).arg(hash\n\t\t\t\t).arg(counted));\n\t\t}\n\t\tsession().local().writeRecentStickers();\n\t} break;\n\tcase CloudRecentAttachedSetId: {\n\t\tconst auto counted = Api::CountRecentStickersHash(&session(), true);\n\t\tif (counted != hash) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"received recent attached stickers hash %1 \"\n\t\t\t\t\"while counted hash is %2\"\n\t\t\t\t).arg(hash\n\t\t\t\t).arg(counted));\n\t\t}\n\t\tsession().local().writeRecentMasks();\n\t} break;\n\tcase FavedSetId: {\n\t\tconst auto counted = Api::CountFavedStickersHash(&session());\n\t\tif (counted != hash) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"received faved stickers hash %1 while counted hash is %2\"\n\t\t\t\t).arg(hash\n\t\t\t\t).arg(counted));\n\t\t}\n\t\tsession().local().writeFavedStickers();\n\t} break;\n\tdefault: Unexpected(\"setId in SpecialSetReceived()\");\n\t}\n\n\tnotifyUpdated((setId == CloudRecentAttachedSetId)\n\t\t? StickersType::Masks\n\t\t: StickersType::Stickers);\n}\n\nvoid Stickers::featuredSetsReceived(\n\t\tconst MTPmessages_FeaturedStickers &result) {\n\tsetLastFeaturedUpdate(crl::now());\n\tresult.match([](const MTPDmessages_featuredStickersNotModified &) {\n\t}, [&](const MTPDmessages_featuredStickers &data) {\n\t\tfeaturedReceived(data, StickersType::Stickers);\n\t});\n}\n\nvoid Stickers::featuredEmojiSetsReceived(\n\t\tconst MTPmessages_FeaturedStickers &result) {\n\tsetLastFeaturedEmojiUpdate(crl::now());\n\tresult.match([](const MTPDmessages_featuredStickersNotModified &) {\n\t}, [&](const MTPDmessages_featuredStickers &data) {\n\t\tfeaturedReceived(data, StickersType::Emoji);\n\t});\n}\n\nvoid Stickers::featuredReceived(\n\t\tconst MTPDmessages_featuredStickers &data,\n\t\tStickersType type) {\n\tconst auto &list = data.vsets().v;\n\tconst auto &unread = data.vunread().v;\n\tconst auto hash = data.vhash().v;\n\n\tauto &&unreadIds = ranges::views::all(\n\t\tunread\n\t) | ranges::views::transform(&MTPlong::v);\n\tconst auto unreadMap = base::flat_set<uint64>{\n\t\tunreadIds.begin(),\n\t\tunreadIds.end()\n\t};\n\n\tconst auto isEmoji = (type == StickersType::Emoji);\n\tauto &featuredOrder = isEmoji\n\t\t? featuredEmojiSetsOrderRef()\n\t\t: featuredSetsOrderRef();\n\tfeaturedOrder.clear();\n\n\tauto &sets = setsRef();\n\tauto setsToRequest = base::flat_map<uint64, uint64>();\n\tfor (auto &[id, set] : sets) {\n\t\t// Mark for removing.\n\t\tif (set->type() == type) {\n\t\t\tset->flags &= ~SetFlag::Featured;\n\t\t}\n\t}\n\tfor (const auto &entry : list) {\n\t\tconst auto data = entry.match([&](const auto &data) {\n\t\t\treturn data.vset().match([&](const MTPDstickerSet &data) {\n\t\t\t\treturn &data;\n\t\t\t});\n\t\t});\n\t\tauto it = sets.find(data->vid().v);\n\t\tconst auto title = getSetTitle(*data);\n\t\tconst auto installDate = data->vinstalled_date().value_or_empty();\n\t\tauto thumbnailType = StickerType::Webp;\n\t\tconst auto thumbnail = [&] {\n\t\t\tif (const auto thumbs = data->vthumbs()) {\n\t\t\t\tfor (const auto &thumb : thumbs->v) {\n\t\t\t\t\tconst auto result = Images::FromPhotoSize(\n\t\t\t\t\t\t&session(),\n\t\t\t\t\t\t*data,\n\t\t\t\t\t\tthumb);\n\t\t\t\t\tif (result.location.valid()) {\n\t\t\t\t\t\tthumbnailType = ThumbnailTypeFromPhotoSize(thumb);\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ImageWithLocation();\n\t\t}();\n\t\tconst auto setId = data->vid().v;\n\t\tconst auto flags = SetFlag::Featured\n\t\t\t| (unreadMap.contains(setId) ? SetFlag::Unread : SetFlag())\n\t\t\t| ParseStickersSetFlags(*data);\n\t\tif (it == sets.cend()) {\n\t\t\tit = sets.emplace(data->vid().v, std::make_unique<StickersSet>(\n\t\t\t\t&owner(),\n\t\t\t\tsetId,\n\t\t\t\tdata->vaccess_hash().v,\n\t\t\t\tdata->vhash().v,\n\t\t\t\ttitle,\n\t\t\t\tqs(data->vshort_name()),\n\t\t\t\tdata->vcount().v,\n\t\t\t\tflags | SetFlag::NotLoaded,\n\t\t\t\tinstallDate)).first;\n\t\t} else {\n\t\t\tconst auto set = it->second.get();\n\t\t\tset->accessHash = data->vaccess_hash().v;\n\t\t\tset->title = title;\n\t\t\tset->shortName = qs(data->vshort_name());\n\t\t\tset->flags = flags\n\t\t\t\t| (set->flags & (SetFlag::NotLoaded | SetFlag::Special));\n\t\t\tset->installDate = installDate;\n\t\t\tif (set->count != data->vcount().v || set->hash != data->vhash().v || set->emoji.empty()) {\n\t\t\t\tset->count = data->vcount().v;\n\t\t\t\tset->hash = data->vhash().v;\n\t\t\t\tset->flags |= SetFlag::NotLoaded; // need to request this set\n\t\t\t}\n\t\t}\n\t\tit->second->setThumbnail(thumbnail, thumbnailType);\n\t\tit->second->thumbnailDocumentId = data->vthumb_document_id().value_or_empty();\n\t\tfeaturedOrder.push_back(data->vid().v);\n\t\tif (it->second->stickers.isEmpty()\n\t\t\t|| (it->second->flags & SetFlag::NotLoaded)) {\n\t\t\tsetsToRequest.emplace(data->vid().v, data->vaccess_hash().v);\n\t\t}\n\t}\n\n\tauto unreadCount = 0;\n\tfor (auto it = sets.begin(); it != sets.end();) {\n\t\tconst auto set = it->second.get();\n\t\tconst auto installed = (set->flags & SetFlag::Installed);\n\t\tconst auto featured = (set->flags & SetFlag::Featured);\n\t\tconst auto special = (set->flags & SetFlag::Special);\n\t\tconst auto archived = (set->flags & SetFlag::Archived);\n\t\tconst auto emoji = !!(set->flags & SetFlag::Emoji);\n\t\tconst auto locked = (set->locked > 0);\n\t\tif (installed || featured || special || archived || emoji || locked) {\n\t\t\tif (featured && (set->flags & SetFlag::Unread)) {\n\t\t\t\tif (!(set->flags & SetFlag::Emoji)) {\n\t\t\t\t\t++unreadCount;\n\t\t\t\t}\n\t\t\t}\n\t\t\t++it;\n\t\t} else {\n\t\t\tit = sets.erase(it);\n\t\t}\n\t}\n\tsetFeaturedSetsUnreadCount(unreadCount);\n\n\tconst auto counted = isEmoji\n\t\t? Api::CountFeaturedEmojiHash(&session())\n\t\t: Api::CountFeaturedStickersHash(&session());\n\tif (counted != hash) {\n\t\tLOG((\"API Error: \"\n\t\t\t\"received featured stickers hash %1 while counted hash is %2\"\n\t\t\t).arg(hash\n\t\t\t).arg(counted));\n\t}\n\n\tif (!setsToRequest.empty()) {\n\t\tauto &api = session().api();\n\t\tfor (const auto &[setId, accessHash] : setsToRequest) {\n\t\t\tapi.scheduleStickerSetRequest(setId, accessHash);\n\t\t}\n\t\tapi.requestStickerSets();\n\t}\n\tif (isEmoji) {\n\t\tsession().local().writeFeaturedCustomEmoji();\n\t} else {\n\t\tsession().local().writeFeaturedStickers();\n\t}\n\n\tnotifyUpdated(type);\n}\n\nvoid Stickers::gifsReceived(const QVector<MTPDocument> &items, uint64 hash) {\n\tauto &saved = savedGifsRef();\n\tsaved.clear();\n\n\tsaved.reserve(items.size());\n\tfor (const auto &item : items) {\n\t\tconst auto document = owner().processDocument(item);\n\t\tif (!document->isGifv()) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"bad document returned in Stickers::gifsReceived!\"));\n\t\t\tcontinue;\n\t\t}\n\n\t\tsaved.push_back(document);\n\t}\n\tconst auto counted = Api::CountSavedGifsHash(&session());\n\tif (counted != hash) {\n\t\tLOG((\"API Error: \"\n\t\t\t\"received saved gifs hash %1 while counted hash is %2\"\n\t\t\t).arg(hash\n\t\t\t).arg(counted));\n\t}\n\n\tsession().local().writeSavedGifs();\n\n\tnotifySavedGifsUpdated();\n}\n\nstd::vector<not_null<DocumentData*>> Stickers::getPremiumList(uint64 seed) {\n\tstruct StickerWithDate {\n\t\tnot_null<DocumentData*> document;\n\t\tTimeId date = 0;\n\t};\n\tauto result = std::vector<StickerWithDate>();\n\tauto &sets = setsRef();\n\tauto setsToRequest = base::flat_map<uint64, uint64>();\n\n\tconst auto add = [&](not_null<DocumentData*> document, TimeId date) {\n\t\tif (ranges::find(result, document, [](const StickerWithDate &data) {\n\t\t\treturn data.document;\n\t\t}) == result.end()) {\n\t\t\tresult.push_back({ document, date });\n\t\t}\n\t};\n\n\tconstexpr auto kSlice = 65536;\n\tconst auto CreateSortKey = [&](\n\t\t\tnot_null<DocumentData*> document,\n\t\t\tint base) {\n\t\tif (document->sticker() && document->sticker()->isAnimated()) {\n\t\t\tbase += kSlice;\n\t\t}\n\t\treturn TimeId(base + int((document->id ^ seed) % kSlice));\n\t};\n\tconst auto CreateRecentSortKey = [&](not_null<DocumentData*> document) {\n\t\treturn CreateSortKey(document, kSlice * 6);\n\t};\n\tauto myCounter = 0;\n\tconst auto CreateMySortKey = [&](not_null<DocumentData*> document) {\n\t\tauto base = kSlice * 6;\n\t\tif (!document->sticker() || !document->sticker()->isAnimated()) {\n\t\t\tbase -= kSlice;\n\t\t}\n\t\treturn (base - (++myCounter));\n\t};\n\tconst auto CreateFeaturedSortKey = [&](not_null<DocumentData*> document) {\n\t\treturn CreateSortKey(document, kSlice * 2);\n\t};\n\tconst auto InstallDateAdjusted = [&](\n\t\t\tTimeId date,\n\t\t\tnot_null<DocumentData*> document) {\n\t\treturn (document->sticker() && document->sticker()->isAnimated())\n\t\t\t? date\n\t\t\t: date / 2;\n\t};\n\tconst auto RecentInstallDate = [&](not_null<DocumentData*> document) {\n\t\tExpects(document->sticker() != nullptr);\n\n\t\tconst auto sticker = document->sticker();\n\t\tif (sticker->set.id) {\n\t\t\tconst auto setIt = sets.find(sticker->set.id);\n\t\t\tif (setIt != sets.end()) {\n\t\t\t\treturn InstallDateAdjusted(setIt->second->installDate, document);\n\t\t\t}\n\t\t}\n\t\treturn TimeId(0);\n\t};\n\n\tauto recentIt = sets.find(Stickers::CloudRecentSetId);\n\tif (recentIt != sets.cend()) {\n\t\tconst auto recent = recentIt->second.get();\n\t\tconst auto count = int(recent->stickers.size());\n\t\tresult.reserve(count);\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tconst auto document = recent->stickers[i];\n\t\t\tauto index = i;\n\t\t\tif (!document->isPremiumSticker()) {\n\t\t\t\tcontinue;\n\t\t\t} else {\n\t\t\t\tindex = recent->stickers.indexOf(document);\n\t\t\t}\n\t\t\tconst auto usageDate = (recent->dates.empty() || index < 0)\n\t\t\t\t? 0\n\t\t\t\t: recent->dates[index];\n\t\t\tconst auto date = usageDate\n\t\t\t\t? usageDate\n\t\t\t\t: RecentInstallDate(document);\n\t\t\tresult.push_back({\n\t\t\t\tdocument,\n\t\t\t\tdate ? date : CreateRecentSortKey(document) });\n\t\t}\n\t}\n\tconst auto addList = [&](\n\t\t\tconst StickersSetsOrder &order,\n\t\t\tSetFlag skip) {\n\t\tfor (const auto setId : order) {\n\t\t\tauto it = sets.find(setId);\n\t\t\tif (it == sets.cend() || (it->second->flags & skip)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto set = it->second.get();\n\t\t\tif (set->emoji.empty()) {\n\t\t\t\tsetsToRequest.emplace(set->id, set->accessHash);\n\t\t\t\tset->flags |= SetFlag::NotLoaded;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto my = (set->flags & SetFlag::Installed);\n\t\t\tresult.reserve(result.size() + set->stickers.size());\n\t\t\tfor (const auto document : set->stickers) {\n\t\t\t\tif (!document->isPremiumSticker()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto installDate = my ? set->installDate : TimeId(0);\n\t\t\t\tconst auto date = (installDate > 1)\n\t\t\t\t\t? InstallDateAdjusted(installDate, document)\n\t\t\t\t\t: my\n\t\t\t\t\t? CreateMySortKey(document)\n\t\t\t\t\t: CreateFeaturedSortKey(document);\n\t\t\t\tadd(document, date);\n\t\t\t}\n\t\t}\n\t};\n\n\taddList(setsOrder(), SetFlag::Archived);\n\taddList(featuredSetsOrder(), SetFlag::Installed);\n\n\tif (!setsToRequest.empty()) {\n\t\tfor (const auto &[setId, accessHash] : setsToRequest) {\n\t\t\tsession().api().scheduleStickerSetRequest(setId, accessHash);\n\t\t}\n\t\tsession().api().requestStickerSets();\n\t}\n\n\tranges::sort(result, std::greater<>(), &StickerWithDate::date);\n\n\treturn result\n\t\t| ranges::views::transform(&StickerWithDate::document)\n\t\t| ranges::to_vector;\n}\n\nstd::vector<not_null<DocumentData*>> Stickers::getListByEmoji(\n\t\tstd::vector<EmojiPtr> emoji,\n\t\tuint64 seed,\n\t\tbool forceAllResults) {\n\tauto all = base::flat_set<EmojiPtr>();\n\tfor (const auto &one : emoji) {\n\t\tall.emplace(one->original());\n\t}\n\tconst auto single = (all.size() == 1) ? all.front() : nullptr;\n\n\tstruct StickerWithDate {\n\t\tnot_null<DocumentData*> document;\n\t\tTimeId date = 0;\n\t};\n\tauto result = std::vector<StickerWithDate>();\n\tauto &sets = setsRef();\n\tauto setsToRequest = base::flat_map<uint64, uint64>();\n\n\tconst auto add = [&](not_null<DocumentData*> document, TimeId date) {\n\t\tif (ranges::find(result, document, [](const StickerWithDate &data) {\n\t\t\treturn data.document;\n\t\t}) == result.end()) {\n\t\t\tresult.push_back({ document, date });\n\t\t}\n\t};\n\n\tconstexpr auto kSlice = 65536;\n\tconst auto CreateSortKey = [&](\n\t\t\tnot_null<DocumentData*> document,\n\t\t\tint base) {\n\t\tif (document->sticker() && document->sticker()->isAnimated()) {\n\t\t\tbase += kSlice;\n\t\t}\n\t\treturn TimeId(base + int((document->id ^ seed) % kSlice));\n\t};\n\tconst auto CreateRecentSortKey = [&](not_null<DocumentData*> document) {\n\t\treturn CreateSortKey(document, kSlice * 6);\n\t};\n\tauto myCounter = 0;\n\tconst auto CreateMySortKey = [&](not_null<DocumentData*> document) {\n\t\tauto base = kSlice * 6;\n\t\tif (!document->sticker() || !document->sticker()->isAnimated()) {\n\t\t\tbase -= kSlice;\n\t\t}\n\t\treturn (base - (++myCounter));\n\t};\n\tconst auto CreateFeaturedSortKey = [&](not_null<DocumentData*> document) {\n\t\treturn CreateSortKey(document, kSlice * 2);\n\t};\n\tconst auto CreateOtherSortKey = [&](not_null<DocumentData*> document) {\n\t\treturn CreateSortKey(document, 0);\n\t};\n\tconst auto InstallDateAdjusted = [&](\n\t\t\tTimeId date,\n\t\t\tnot_null<DocumentData*> document) {\n\t\treturn (document->sticker() && document->sticker()->isAnimated())\n\t\t\t? date\n\t\t\t: date / 2;\n\t};\n\tconst auto RecentInstallDate = [&](not_null<DocumentData*> document) {\n\t\tExpects(document->sticker() != nullptr);\n\n\t\tconst auto sticker = document->sticker();\n\t\tif (sticker->set.id) {\n\t\t\tconst auto setIt = sets.find(sticker->set.id);\n\t\t\tif (setIt != sets.end()) {\n\t\t\t\treturn InstallDateAdjusted(setIt->second->installDate, document);\n\t\t\t}\n\t\t}\n\t\treturn TimeId(0);\n\t};\n\n\tauto recentIt = sets.find(Stickers::CloudRecentSetId);\n\tif (recentIt != sets.cend()) {\n\t\tconst auto recent = recentIt->second.get();\n\t\tconst auto i = single\n\t\t\t? recent->emoji.find(single)\n\t\t\t: recent->emoji.end();\n\t\tconst auto list = (i != recent->emoji.end())\n\t\t\t? &i->second\n\t\t\t: !single\n\t\t\t? &recent->stickers\n\t\t\t: nullptr;\n\t\tif (list) {\n\t\t\tconst auto count = int(list->size());\n\t\t\tresult.reserve(count);\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tconst auto document = (*list)[i];\n\t\t\t\tconst auto sticker = document->sticker();\n\t\t\t\tauto index = i;\n\t\t\t\tif (!sticker) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (!single) {\n\t\t\t\t\tconst auto main = Ui::Emoji::Find(sticker->alt);\n\t\t\t\t\tif (!main || !all.contains(main)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tindex = recent->stickers.indexOf(document);\n\t\t\t\t}\n\t\t\t\tconst auto usageDate = (recent->dates.empty() || index < 0)\n\t\t\t\t\t? 0\n\t\t\t\t\t: recent->dates[index];\n\t\t\t\tconst auto date = usageDate\n\t\t\t\t\t? usageDate\n\t\t\t\t\t: RecentInstallDate(document);\n\t\t\t\tresult.push_back({\n\t\t\t\t\tdocument,\n\t\t\t\t\tdate ? date : CreateRecentSortKey(document) });\n\t\t\t}\n\t\t}\n\t}\n\tconst auto addList = [&](\n\t\t\tconst StickersSetsOrder &order,\n\t\t\tSetFlag skip) {\n\t\tfor (const auto setId : order) {\n\t\t\tauto it = sets.find(setId);\n\t\t\tif (it == sets.cend() || (it->second->flags & skip)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto set = it->second.get();\n\t\t\tif (set->emoji.empty()) {\n\t\t\t\tsetsToRequest.emplace(set->id, set->accessHash);\n\t\t\t\tset->flags |= SetFlag::NotLoaded;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto my = (set->flags & SetFlag::Installed);\n\t\t\tconst auto i = single\n\t\t\t\t? set->emoji.find(single)\n\t\t\t\t: set->emoji.end();\n\t\t\tconst auto list = (i != set->emoji.end())\n\t\t\t\t? &i->second\n\t\t\t\t: !single\n\t\t\t\t? &set->stickers\n\t\t\t\t: nullptr;\n\t\t\tif (list) {\n\t\t\t\tresult.reserve(result.size() + list->size());\n\t\t\t\tfor (const auto document : *list) {\n\t\t\t\t\tconst auto sticker = document->sticker();\n\t\t\t\t\tif (!sticker) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t} else if (!single) {\n\t\t\t\t\t\tconst auto main = Ui::Emoji::Find(sticker->alt);\n\t\t\t\t\t\tif (!main || !all.contains(main)) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tconst auto installDate = my ? set->installDate : TimeId(0);\n\t\t\t\t\tconst auto date = (installDate > 1)\n\t\t\t\t\t\t? InstallDateAdjusted(installDate, document)\n\t\t\t\t\t\t: my\n\t\t\t\t\t\t? CreateMySortKey(document)\n\t\t\t\t\t\t: CreateFeaturedSortKey(document);\n\t\t\t\t\tadd(document, date);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\taddList(setsOrder(), SetFlag::Archived);\n\t//addList(featuredSetsOrder(), SetFlag::Installed);\n\n\tif (!setsToRequest.empty()) {\n\t\tfor (const auto &[setId, accessHash] : setsToRequest) {\n\t\t\tsession().api().scheduleStickerSetRequest(setId, accessHash);\n\t\t}\n\t\tsession().api().requestStickerSets();\n\t}\n\n\tif (forceAllResults || Core::App().settings().suggestStickersByEmoji()) {\n\t\tconst auto key = ranges::accumulate(\n\t\t\tall,\n\t\t\tQString(),\n\t\t\tranges::plus(),\n\t\t\t&Ui::Emoji::One::text);\n\t\tconst auto others = session().api().stickersByEmoji(key);\n\t\tif (others) {\n\t\t\tresult.reserve(result.size() + others->size());\n\t\t\tfor (const auto &document : *others) {\n\t\t\t\tadd(document, CreateOtherSortKey(document));\n\t\t\t}\n\t\t} else if (!forceAllResults) {\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tranges::sort(result, std::greater<>(), &StickerWithDate::date);\n\n\tconst auto appConfig = &session().appConfig();\n\tauto mixed = std::vector<not_null<DocumentData*>>();\n\tmixed.reserve(result.size());\n\tauto premiumIndex = 0, nonPremiumIndex = 0;\n\tconst auto skipToNext = [&](bool premium) {\n\t\tauto &index = premium ? premiumIndex : nonPremiumIndex;\n\t\twhile (index < result.size()\n\t\t\t&& result[index].document->isPremiumSticker() != premium) {\n\t\t\t++index;\n\t\t}\n\t};\n\tconst auto done = [&](bool premium) {\n\t\tskipToNext(premium);\n\t\tconst auto &index = premium ? premiumIndex : nonPremiumIndex;\n\t\treturn (index == result.size());\n\t};\n\tconst auto take = [&](bool premium) {\n\t\tif (done(premium)) {\n\t\t\treturn false;\n\t\t}\n\t\tauto &index = premium ? premiumIndex : nonPremiumIndex;\n\t\tmixed.push_back(result[index++].document);\n\t\treturn true;\n\t};\n\n\tif (session().premium()) {\n\t\tconst auto normalsPerPremium = appConfig->get<int>(\n\t\t\tu\"stickers_normal_by_emoji_per_premium_num\"_q,\n\t\t\t2);\n\t\tdo {\n\t\t\t// Add \"stickers_normal_by_emoji_per_premium_num\" non-premium.\n\t\t\tfor (auto i = 0; i < normalsPerPremium; ++i) {\n\t\t\t\tif (!take(false)) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Then one premium.\n\t\t} while (!done(false) && take(true));\n\n\t\t// Add what's left.\n\t\twhile (take(false)) {\n\t\t}\n\t\twhile (take(true)) {\n\t\t}\n\t} else {\n\t\t// All non-premium.\n\t\twhile (take(false)) {\n\t\t}\n\n\t\t// In the end add \"stickers_premium_by_emoji_num\" premium.\n\t\tconst auto premiumsToEnd = appConfig->get<int>(\n\t\t\tu\"stickers_premium_by_emoji_num\"_q,\n\t\t\t0);\n\t\tfor (auto i = 0; i < premiumsToEnd; ++i) {\n\t\t\tif (!take(true)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn mixed;\n}\n\nstd::optional<std::vector<not_null<EmojiPtr>>> Stickers::getEmojiListFromSet(\n\t\tnot_null<DocumentData*> document) {\n\tif (auto sticker = document->sticker()) {\n\t\tauto &inputSet = sticker->set;\n\t\tif (!inputSet.id) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\tconst auto &sets = this->sets();\n\t\tauto it = sets.find(inputSet.id);\n\t\tif (it == sets.cend()) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\tconst auto set = it->second.get();\n\t\tauto result = std::vector<not_null<EmojiPtr>>();\n\t\tfor (auto i = set->emoji.cbegin(), e = set->emoji.cend(); i != e; ++i) {\n\t\t\tif (i->second.contains(document)) {\n\t\t\t\tresult.emplace_back(i->first);\n\t\t\t}\n\t\t}\n\t\tif (result.empty()) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\treturn result;\n\t}\n\treturn std::nullopt;\n}\n\nnot_null<StickersSet*> Stickers::feedSet(const MTPStickerSet &info) {\n\tauto &sets = setsRef();\n\tconst auto &data = info.data();\n\tauto it = sets.find(data.vid().v);\n\tauto title = getSetTitle(data);\n\tauto oldFlags = StickersSetFlags(0);\n\tauto thumbnailType = StickerType::Webp;\n\tconst auto thumbnail = [&] {\n\t\tif (const auto thumbs = data.vthumbs()) {\n\t\t\tfor (const auto &thumb : thumbs->v) {\n\t\t\t\tconst auto result = Images::FromPhotoSize(\n\t\t\t\t\t&session(),\n\t\t\t\t\tdata,\n\t\t\t\t\tthumb);\n\t\t\t\tif (result.location.valid()) {\n\t\t\t\t\tthumbnailType = Data::ThumbnailTypeFromPhotoSize(thumb);\n\t\t\t\t\treturn result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn ImageWithLocation();\n\t}();\n\tconst auto flags = ParseStickersSetFlags(data);\n\tif (it == sets.cend()) {\n\t\tit = sets.emplace(data.vid().v, std::make_unique<StickersSet>(\n\t\t\t&owner(),\n\t\t\tdata.vid().v,\n\t\t\tdata.vaccess_hash().v,\n\t\t\tdata.vhash().v,\n\t\t\ttitle,\n\t\t\tqs(data.vshort_name()),\n\t\t\tdata.vcount().v,\n\t\t\tflags | SetFlag::NotLoaded,\n\t\t\tdata.vinstalled_date().value_or_empty())).first;\n\t} else {\n\t\tconst auto set = it->second.get();\n\t\tset->accessHash = data.vaccess_hash().v;\n\t\tset->title = title;\n\t\tset->shortName = qs(data.vshort_name());\n\t\toldFlags = set->flags;\n\t\tconst auto clientFlags = set->flags\n\t\t\t& (SetFlag::Featured\n\t\t\t\t| SetFlag::Unread\n\t\t\t\t| SetFlag::NotLoaded\n\t\t\t\t| SetFlag::Special);\n\t\tset->flags = flags | clientFlags;\n\t\tconst auto installDate = data.vinstalled_date();\n\t\tset->installDate = installDate\n\t\t\t? (installDate->v ? installDate->v : base::unixtime::now())\n\t\t\t: TimeId(0);\n\t\tif (set->count != data.vcount().v\n\t\t\t|| set->hash != data.vhash().v\n\t\t\t|| set->emoji.empty()) {\n\t\t\t// Need to request this data.\n\t\t\tset->count = data.vcount().v;\n\t\t\tset->hash = data.vhash().v;\n\t\t\tset->flags |= SetFlag::NotLoaded;\n\t\t}\n\t}\n\tconst auto set = it->second.get();\n\tset->setThumbnail(thumbnail, thumbnailType);\n\tset->thumbnailDocumentId = data.vthumb_document_id().value_or_empty();\n\tauto changedFlags = (oldFlags ^ set->flags);\n\tif (changedFlags & SetFlag::Archived) {\n\t\tconst auto isMasks = (set->type() == StickersType::Masks);\n\t\tauto &archivedOrder = isMasks\n\t\t\t? archivedMaskSetsOrderRef()\n\t\t\t: archivedSetsOrderRef();\n\t\tconst auto index = archivedOrder.indexOf(set->id);\n\t\tif (set->flags & SetFlag::Archived) {\n\t\t\tif (index < 0) {\n\t\t\t\tarchivedOrder.push_front(set->id);\n\t\t\t}\n\t\t} else if (index >= 0) {\n\t\t\tarchivedOrder.removeAt(index);\n\t\t}\n\t}\n\treturn it->second.get();\n}\n\nnot_null<StickersSet*> Stickers::feedSetFull(\n\t\tconst MTPDmessages_stickerSet &data) {\n\tconst auto set = feedSet(data.vset());\n\tfeedSetStickers(set, data.vdocuments().v, data.vpacks().v);\n\treturn set;\n}\n\nnot_null<StickersSet*> Stickers::feedSet(\n\t\tconst MTPStickerSetCovered &data) {\n\tconst auto set = data.match([&](const auto &data) {\n\t\treturn feedSet(data.vset());\n\t});\n\tdata.match([](const MTPDstickerSetCovered &data) {\n\t}, [&](const MTPDstickerSetNoCovered &data) {\n\t}, [&](const MTPDstickerSetMultiCovered &data) {\n\t\tfeedSetCovers(set, data.vcovers().v);\n\t}, [&](const MTPDstickerSetFullCovered &data) {\n\t\tfeedSetStickers(set, data.vdocuments().v, data.vpacks().v);\n\t});\n\treturn set;\n}\n\nvoid Stickers::feedSetStickers(\n\t\tnot_null<StickersSet*> set,\n\t\tconst QVector<MTPDocument> &documents,\n\t\tconst QVector<MTPStickerPack> &packs) {\n\tset->flags &= ~SetFlag::NotLoaded;\n\n\tauto &sets = setsRef();\n\tconst auto wasArchived = [&] {\n\t\tconst auto it = sets.find(set->id);\n\t\treturn (it != sets.end())\n\t\t\t&& (it->second->flags & SetFlag::Archived);\n\t}();\n\n\tauto customIt = sets.find(Stickers::CustomSetId);\n\tconst auto inputSet = set->identifier();\n\n\tauto pack = StickersPack();\n\tpack.reserve(documents.size());\n\tfor (const auto &item : documents) {\n\t\tconst auto document = owner().processDocument(item);\n\t\tif (!document->sticker()) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tpack.push_back(document);\n\t\tif (!document->sticker()->set.id) {\n\t\t\tdocument->sticker()->set = inputSet;\n\t\t}\n\t\tif (customIt != sets.cend()) {\n\t\t\tconst auto custom = customIt->second.get();\n\t\t\tconst auto index = custom->stickers.indexOf(document);\n\t\t\tif (index >= 0) {\n\t\t\t\tcustom->stickers.removeAt(index);\n\t\t\t}\n\t\t}\n\t}\n\tif (customIt != sets.cend() && customIt->second->stickers.isEmpty()) {\n\t\tsets.erase(customIt);\n\t\tcustomIt = sets.end();\n\t}\n\n\tauto writeRecent = false;\n\tauto &recent = getRecentPack();\n\tfor (auto i = recent.begin(); i != recent.cend();) {\n\t\tif (set->stickers.indexOf(i->first) >= 0\n\t\t\t&& pack.indexOf(i->first) < 0) {\n\t\t\ti = recent.erase(i);\n\t\t\twriteRecent = true;\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\n\tconst auto isEmoji = (set->type() == StickersType::Emoji);\n\tconst auto isMasks = (set->type() == StickersType::Masks);\n\tset->stickers = pack;\n\tset->emoji.clear();\n\tfor (auto i = 0, l = int(packs.size()); i != l; ++i) {\n\t\tconst auto &pack = packs[i].data();\n\t\tif (auto emoji = Ui::Emoji::Find(qs(pack.vemoticon()))) {\n\t\t\temoji = emoji->original();\n\t\t\tauto &stickers = pack.vdocuments().v;\n\n\t\t\tauto p = StickersPack();\n\t\t\tp.reserve(stickers.size());\n\t\t\tfor (auto j = 0, c = int(stickers.size()); j != c; ++j) {\n\t\t\t\tconst auto document = owner().document(stickers[j].v);\n\t\t\t\tif (!document->sticker()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tp.push_back(document);\n\t\t\t}\n\t\t\tset->emoji[emoji] = std::move(p);\n\t\t}\n\t}\n\n\tif (writeRecent) {\n\t\tsession().saveSettings();\n\t}\n\n\tconst auto isArchived = !!(set->flags & SetFlag::Archived);\n\tif ((set->flags & SetFlag::Installed) && !isArchived) {\n\t\tif (isEmoji) {\n\t\t\tsession().local().writeInstalledCustomEmoji();\n\t\t} else if (isMasks) {\n\t\t\tsession().local().writeInstalledMasks();\n\t\t} else {\n\t\t\tsession().local().writeInstalledStickers();\n\t\t}\n\t}\n\tif (set->flags & SetFlag::Featured) {\n\t\tif (isEmoji) {\n\t\t\tsession().local().writeFeaturedCustomEmoji();\n\t\t} else if (isMasks) {\n\t\t} else {\n\t\t\tsession().local().writeFeaturedStickers();\n\t\t}\n\t}\n\tif (wasArchived != isArchived) {\n\t\tif (isEmoji) {\n\t\t} else if (isMasks) {\n\t\t\tsession().local().writeArchivedMasks();\n\t\t} else {\n\t\t\tsession().local().writeArchivedStickers();\n\t\t}\n\t}\n\tnotifyUpdated(set->type());\n}\n\nvoid Stickers::feedSetCovers(\n\t\tnot_null<StickersSet*> set,\n\t\tconst QVector<MTPDocument> &documents) {\n\tset->covers = StickersPack();\n\tfor (const auto &cover : documents) {\n\t\tconst auto document = session().data().processDocument(cover);\n\t\tif (document->sticker()) {\n\t\t\tset->covers.push_back(document);\n\t\t}\n\t}\n}\n\nvoid Stickers::newSetReceived(const MTPDmessages_stickerSet &set) {\n\tconst auto &s = set.vset().c_stickerSet();\n\tif (!s.vinstalled_date()) {\n\t\tLOG((\"API Error: \"\n\t\t\t\"updateNewStickerSet without install_date flag.\"));\n\t\treturn;\n\t} else if (s.is_archived()) {\n\t\tLOG((\"API Error: \"\n\t\t\t\"updateNewStickerSet with archived flag.\"));\n\t\treturn;\n\t}\n\tauto &order = s.is_emojis()\n\t\t? emojiSetsOrderRef()\n\t\t: s.is_masks()\n\t\t? maskSetsOrderRef()\n\t\t: setsOrderRef();\n\tint32 insertAtIndex = 0, currentIndex = order.indexOf(s.vid().v);\n\tif (currentIndex != insertAtIndex) {\n\t\tif (currentIndex > 0) {\n\t\t\torder.removeAt(currentIndex);\n\t\t}\n\t\torder.insert(insertAtIndex, s.vid().v);\n\t}\n\n\tfeedSetFull(set);\n}\n\nQString Stickers::getSetTitle(const MTPDstickerSet &s) {\n\tauto title = qs(s.vtitle());\n\tif ((s.vflags().v & MTPDstickerSet::Flag::f_official)\n\t\t&& !title.compare(u\"Great Minds\"_q, Qt::CaseInsensitive)) {\n\t\treturn tr::lng_stickers_default_set(tr::now);\n\t}\n\treturn title;\n}\n\nRecentStickerPack &Stickers::getRecentPack() const {\n\tif (cRecentStickers().isEmpty() && !cRecentStickersPreload().isEmpty()) {\n\t\tconst auto p = cRecentStickersPreload();\n\t\tcSetRecentStickersPreload(RecentStickerPreload());\n\n\t\tauto &recent = cRefRecentStickers();\n\t\trecent.reserve(p.size());\n\t\tfor (const auto &preloaded : p) {\n\t\t\tconst auto document = owner().document(preloaded.first);\n\t\t\tif (!document || !document->sticker()) continue;\n\n\t\t\trecent.push_back(qMakePair(document, preloaded.second));\n\t\t}\n\t}\n\treturn cRefRecentStickers();\n}\n\nStickerType ThumbnailTypeFromPhotoSize(const MTPPhotoSize &size) {\n\tconst auto &type = size.match([&](const auto &data) {\n\t\treturn data.vtype().v;\n\t});\n\tconst auto ch = type.isEmpty() ? char() : type[0];\n\tswitch (ch) {\n\tcase 's': return StickerType::Webp;\n\tcase 'a': return StickerType::Tgs;\n\tcase 'v': return StickerType::Webm;\n\t}\n\treturn StickerType::Webp;\n}\n\n} // namespace Stickers\n"
  },
  {
    "path": "Telegram/SourceFiles/data/stickers/data_stickers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n#include \"data/stickers/data_stickers_set.h\"\n#include \"settings.h\"\n\nclass HistoryItem;\nclass DocumentData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\n\nclass Session;\nclass DocumentMedia;\n\nenum class StickersType : uchar {\n\tStickers,\n\tMasks,\n\tEmoji,\n};\n[[nodiscard]] StickerType ThumbnailTypeFromPhotoSize(\n\tconst MTPPhotoSize &size);\n\nclass Stickers final {\npublic:\n\texplicit Stickers(not_null<Session*> owner);\n\n\t[[nodiscard]] Session &owner() const;\n\t[[nodiscard]] Main::Session &session() const;\n\n\t// for backward compatibility\n\tstatic constexpr auto DefaultSetId = 0;\n\tstatic constexpr auto CustomSetId = 0xFFFFFFFFFFFFFFFFULL;\n\n\t// For stickers panel, should not appear in Sets.\n\tstatic constexpr auto RecentSetId = 0xFFFFFFFFFFFFFFFEULL;\n\tstatic constexpr auto NoneSetId = 0xFFFFFFFFFFFFFFFDULL;\n\tstatic constexpr auto FeaturedSetId = 0xFFFFFFFFFFFFFFFBULL;\n\n\t// For cloud-stored recent stickers.\n\tstatic constexpr auto CloudRecentSetId = 0xFFFFFFFFFFFFFFFCULL;\n\tstatic constexpr auto CloudRecentAttachedSetId = 0xFFFFFFFFFFFFFFF9ULL;\n\n\t// For cloud-stored faved stickers.\n\tstatic constexpr auto FavedSetId = 0xFFFFFFFFFFFFFFFAULL;\n\n\t// For setting up megagroup sticker set.\n\tstatic constexpr auto MegagroupSetId = 0xFFFFFFFFFFFFFFEFULL;\n\n\t// For collectible emoji statuses.\n\tstatic constexpr auto CollectibleSetId = 0xFFFFFFFFFFFFFFF8ULL;\n\n\tvoid notifyUpdated(StickersType type);\n\t[[nodiscard]] rpl::producer<StickersType> updated() const;\n\t[[nodiscard]] rpl::producer<> updated(StickersType type) const;\n\tvoid notifyRecentUpdated(StickersType type);\n\t[[nodiscard]] rpl::producer<StickersType> recentUpdated() const;\n\t[[nodiscard]] rpl::producer<> recentUpdated(StickersType type) const;\n\tvoid notifySavedGifsUpdated();\n\t[[nodiscard]] rpl::producer<> savedGifsUpdated() const;\n\tvoid notifyStickerSetInstalled(uint64 setId);\n\t[[nodiscard]] rpl::producer<uint64> stickerSetInstalled() const;\n\tvoid notifyGifWithCaptionSent();\n\t[[nodiscard]] rpl::producer<> gifWithCaptionSent() const;\n\tvoid notifyEmojiSetInstalled(uint64 setId);\n\t[[nodiscard]] rpl::producer<uint64> emojiSetInstalled() const;\n\n\tvoid incrementSticker(not_null<DocumentData*> document);\n\n\t[[nodiscard]] bool updateNeeded(crl::time now) const {\n\t\treturn updateNeeded(_lastUpdate, now);\n\t}\n\tvoid setLastUpdate(crl::time update) {\n\t\t_lastUpdate = update;\n\t}\n\t[[nodiscard]] bool recentUpdateNeeded(crl::time now) const {\n\t\treturn updateNeeded(_lastRecentUpdate, now);\n\t}\n\tvoid setLastRecentUpdate(crl::time update) {\n\t\tif (update) {\n\t\t\tnotifyRecentUpdated(StickersType::Stickers);\n\t\t}\n\t\t_lastRecentUpdate = update;\n\t}\n\t[[nodiscard]] bool masksUpdateNeeded(crl::time now) const {\n\t\treturn updateNeeded(_lastMasksUpdate, now);\n\t}\n\tvoid setLastMasksUpdate(crl::time update) {\n\t\t_lastMasksUpdate = update;\n\t}\n\t[[nodiscard]] bool emojiUpdateNeeded(crl::time now) const {\n\t\treturn updateNeeded(_lastEmojiUpdate, now);\n\t}\n\tvoid setLastEmojiUpdate(crl::time update) {\n\t\t_lastEmojiUpdate = update;\n\t}\n\t[[nodiscard]] bool recentAttachedUpdateNeeded(crl::time now) const {\n\t\treturn updateNeeded(_lastRecentAttachedUpdate, now);\n\t}\n\tvoid setLastRecentAttachedUpdate(crl::time update) {\n\t\tif (update) {\n\t\t\tnotifyRecentUpdated(StickersType::Masks);\n\t\t}\n\t\t_lastRecentAttachedUpdate = update;\n\t}\n\t[[nodiscard]] bool favedUpdateNeeded(crl::time now) const {\n\t\treturn updateNeeded(_lastFavedUpdate, now);\n\t}\n\tvoid setLastFavedUpdate(crl::time update) {\n\t\t_lastFavedUpdate = update;\n\t}\n\t[[nodiscard]] bool featuredUpdateNeeded(crl::time now) const {\n\t\treturn updateNeeded(_lastFeaturedUpdate, now);\n\t}\n\tvoid setLastFeaturedUpdate(crl::time update) {\n\t\t_lastFeaturedUpdate = update;\n\t}\n\t[[nodiscard]] bool featuredEmojiUpdateNeeded(crl::time now) const {\n\t\treturn updateNeeded(_lastFeaturedEmojiUpdate, now);\n\t}\n\tvoid setLastFeaturedEmojiUpdate(crl::time update) {\n\t\t_lastFeaturedEmojiUpdate = update;\n\t}\n\t[[nodiscard]] bool savedGifsUpdateNeeded(crl::time now) const {\n\t\treturn updateNeeded(_lastSavedGifsUpdate, now);\n\t}\n\tvoid setLastSavedGifsUpdate(crl::time update) {\n\t\t_lastSavedGifsUpdate = update;\n\t}\n\t[[nodiscard]] int featuredSetsUnreadCount() const {\n\t\treturn _featuredSetsUnreadCount.current();\n\t}\n\tvoid setFeaturedSetsUnreadCount(int count) {\n\t\t_featuredSetsUnreadCount = count;\n\t}\n\t[[nodiscard]] rpl::producer<int> featuredSetsUnreadCountValue() const {\n\t\treturn _featuredSetsUnreadCount.value();\n\t}\n\t[[nodiscard]] const StickersSets &sets() const {\n\t\treturn _sets;\n\t}\n\t[[nodiscard]] StickersSets &setsRef() {\n\t\treturn _sets;\n\t}\n\t[[nodiscard]] const StickersSetsOrder &setsOrder() const {\n\t\treturn _setsOrder;\n\t}\n\t[[nodiscard]] StickersSetsOrder &setsOrderRef() {\n\t\treturn _setsOrder;\n\t}\n\t[[nodiscard]] const StickersSetsOrder &maskSetsOrder() const {\n\t\treturn _maskSetsOrder;\n\t}\n\t[[nodiscard]] StickersSetsOrder &maskSetsOrderRef() {\n\t\treturn _maskSetsOrder;\n\t}\n\t[[nodiscard]] const StickersSetsOrder &emojiSetsOrder() const {\n\t\treturn _emojiSetsOrder;\n\t}\n\t[[nodiscard]] StickersSetsOrder &emojiSetsOrderRef() {\n\t\treturn _emojiSetsOrder;\n\t}\n\t[[nodiscard]] const StickersSetsOrder &featuredSetsOrder() const {\n\t\treturn _featuredSetsOrder;\n\t}\n\t[[nodiscard]] StickersSetsOrder &featuredSetsOrderRef() {\n\t\treturn _featuredSetsOrder;\n\t}\n\t[[nodiscard]] const StickersSetsOrder &featuredEmojiSetsOrder() const {\n\t\treturn _featuredEmojiSetsOrder;\n\t}\n\t[[nodiscard]] StickersSetsOrder &featuredEmojiSetsOrderRef() {\n\t\treturn _featuredEmojiSetsOrder;\n\t}\n\t[[nodiscard]] const StickersSetsOrder &archivedSetsOrder() const {\n\t\treturn _archivedSetsOrder;\n\t}\n\t[[nodiscard]] StickersSetsOrder &archivedSetsOrderRef() {\n\t\treturn _archivedSetsOrder;\n\t}\n\t[[nodiscard]] const StickersSetsOrder &archivedMaskSetsOrder() const {\n\t\treturn _archivedMaskSetsOrder;\n\t}\n\t[[nodiscard]] StickersSetsOrder &archivedMaskSetsOrderRef() {\n\t\treturn _archivedMaskSetsOrder;\n\t}\n\t[[nodiscard]] const SavedGifs &savedGifs() const {\n\t\treturn _savedGifs;\n\t}\n\t[[nodiscard]] SavedGifs &savedGifsRef() {\n\t\treturn _savedGifs;\n\t}\n\tvoid removeFromRecentSet(not_null<DocumentData*> document);\n\n\tvoid addSavedGif(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document);\n\tvoid checkSavedGif(not_null<HistoryItem*> item);\n\n\tvoid applyArchivedResult(\n\t\tconst MTPDmessages_stickerSetInstallResultArchive &d);\n\tvoid installLocally(uint64 setId);\n\tvoid undoInstallLocally(uint64 setId);\n\t[[nodiscard]] bool isFaved(not_null<const DocumentData*> document) const;\n\tvoid setFaved(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document,\n\t\tbool faved);\n\n\tvoid setsReceived(const QVector<MTPStickerSet> &data, uint64 hash);\n\tvoid masksReceived(const QVector<MTPStickerSet> &data, uint64 hash);\n\tvoid emojiReceived(const QVector<MTPStickerSet> &data, uint64 hash);\n\tvoid specialSetReceived(\n\t\tuint64 setId,\n\t\tconst QString &setTitle,\n\t\tconst QVector<MTPDocument> &items,\n\t\tuint64 hash,\n\t\tconst QVector<MTPStickerPack> &packs = QVector<MTPStickerPack>(),\n\t\tconst QVector<MTPint> &usageDates = QVector<MTPint>());\n\tvoid featuredSetsReceived(const MTPmessages_FeaturedStickers &result);\n\tvoid featuredEmojiSetsReceived(\n\t\tconst MTPmessages_FeaturedStickers &result);\n\tvoid gifsReceived(const QVector<MTPDocument> &items, uint64 hash);\n\n\t[[nodiscard]] std::vector<not_null<DocumentData*>> getPremiumList(\n\t\tuint64 seed);\n\t[[nodiscard]] std::vector<not_null<DocumentData*>> getListByEmoji(\n\t\tstd::vector<EmojiPtr> emoji,\n\t\tuint64 seed,\n\t\tbool forceAllResults = false);\n\t[[nodiscard]] auto getEmojiListFromSet(not_null<DocumentData*> document)\n\t\t-> std::optional<std::vector<not_null<EmojiPtr>>>;\n\n\t[[nodiscard]] not_null<StickersSet*> collectibleSet();\n\n\tnot_null<StickersSet*> feedSet(const MTPStickerSet &data);\n\tnot_null<StickersSet*> feedSet(const MTPStickerSetCovered &data);\n\tnot_null<StickersSet*> feedSetFull(const MTPDmessages_stickerSet &data);\n\tvoid feedSetStickers(\n\t\tnot_null<StickersSet*> set,\n\t\tconst QVector<MTPDocument> &documents,\n\t\tconst QVector<MTPStickerPack> &packs);\n\tvoid feedSetCovers(\n\t\tnot_null<StickersSet*> set,\n\t\tconst QVector<MTPDocument> &documents);\n\tvoid newSetReceived(const MTPDmessages_stickerSet &set);\n\n\t[[nodiscard]] QString getSetTitle(const MTPDstickerSet &s);\n\n\t[[nodiscard]] RecentStickerPack &getRecentPack() const;\n\nprivate:\n\t[[nodiscard]] bool updateNeeded(crl::time last, crl::time now) const {\n\t\tconstexpr auto kUpdateTimeout = crl::time(3600'000);\n\t\treturn (last == 0) || (now >= last + kUpdateTimeout);\n\t}\n\tvoid checkFavedLimit(\n\t\tStickersSet &set,\n\t\tstd::shared_ptr<ChatHelpers::Show> show);\n\tvoid setIsFaved(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document,\n\t\tstd::optional<std::vector<not_null<EmojiPtr>>> emojiList\n\t\t\t= std::nullopt);\n\tvoid setIsNotFaved(not_null<DocumentData*> document);\n\tvoid pushFavedToFront(\n\t\tStickersSet &set,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document,\n\t\tconst std::vector<not_null<EmojiPtr>> &emojiList);\n\tvoid moveFavedToFront(StickersSet &set, int index);\n\tvoid requestSetToPushFaved(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document);\n\tvoid setPackAndEmoji(\n\t\tStickersSet &set,\n\t\tStickersPack &&pack,\n\t\tstd::vector<TimeId> &&dates,\n\t\tconst QVector<MTPStickerPack> &packs);\n\tvoid somethingReceived(\n\t\tconst QVector<MTPStickerSet> &list,\n\t\tuint64 hash,\n\t\tStickersType type);\n\tvoid featuredReceived(\n\t\tconst MTPDmessages_featuredStickers &data,\n\t\tStickersType type);\n\n\tconst not_null<Session*> _owner;\n\trpl::event_stream<StickersType> _updated;\n\trpl::event_stream<StickersType> _recentUpdated;\n\trpl::event_stream<> _savedGifsUpdated;\n\trpl::event_stream<uint64> _stickerSetInstalled;\n\trpl::event_stream<> _gifWithCaptionSent;\n\trpl::event_stream<uint64> _emojiSetInstalled;\n\tcrl::time _lastUpdate = 0;\n\tcrl::time _lastRecentUpdate = 0;\n\tcrl::time _lastFavedUpdate = 0;\n\tcrl::time _lastFeaturedUpdate = 0;\n\tcrl::time _lastSavedGifsUpdate = 0;\n\tcrl::time _lastMasksUpdate = 0;\n\tcrl::time _lastEmojiUpdate = 0;\n\tcrl::time _lastFeaturedEmojiUpdate = 0;\n\tcrl::time _lastRecentAttachedUpdate = 0;\n\trpl::variable<int> _featuredSetsUnreadCount = 0;\n\tStickersSets _sets;\n\tStickersSetsOrder _setsOrder;\n\tStickersSetsOrder _maskSetsOrder;\n\tStickersSetsOrder _emojiSetsOrder;\n\tStickersSetsOrder _featuredSetsOrder;\n\tStickersSetsOrder _featuredEmojiSetsOrder;\n\tStickersSetsOrder _archivedSetsOrder;\n\tStickersSetsOrder _archivedMaskSetsOrder;\n\tSavedGifs _savedGifs;\n\n};\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/data/stickers/data_stickers_set.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"data/stickers/data_stickers_set.h\"\n\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_document.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"storage/file_download.h\"\n#include \"ui/image/image.h\"\n\nnamespace Data {\n\nStickersSetThumbnailView::StickersSetThumbnailView(\n\tnot_null<StickersSet*> owner)\n: _owner(owner) {\n}\n\nnot_null<StickersSet*> StickersSetThumbnailView::owner() const {\n\treturn _owner;\n}\n\nvoid StickersSetThumbnailView::set(\n\t\tnot_null<Main::Session*> session,\n\t\tQByteArray content) {\n\tauto image = Images::Read({ .content = content }).image;\n\tif (image.isNull()) {\n\t\t_content = std::move(content);\n\t} else {\n\t\t_image = std::make_unique<Image>(std::move(image));\n\t}\n\tsession->notifyDownloaderTaskFinished();\n}\n\nImage *StickersSetThumbnailView::image() const {\n\treturn _image.get();\n}\n\nQByteArray StickersSetThumbnailView::content() const {\n\treturn _content;\n}\n\nStickersSetFlags ParseStickersSetFlags(const MTPDstickerSet &data) {\n\tusing Flag = StickersSetFlag;\n\treturn (data.is_archived() ? Flag::Archived : Flag())\n\t\t| (data.is_official() ? Flag::Official : Flag())\n\t\t| (data.is_masks() ? Flag::Masks : Flag())\n\t\t| (data.is_emojis() ? Flag::Emoji : Flag())\n\t\t| (data.vinstalled_date() ? Flag::Installed : Flag())\n\t\t//| (data.is_videos() ? Flag::Webm : Flag())\n\t\t| (data.is_text_color() ? Flag::TextColor : Flag())\n\t\t| (data.is_channel_emoji_status() ? Flag::ChannelStatus : Flag())\n\t\t| (data.is_creator() ? Flag::AmCreator : Flag());\n}\n\nStickersSet::StickersSet(\n\tnot_null<Data::Session*> owner,\n\tuint64 id,\n\tuint64 accessHash,\n\tuint64 hash,\n\tconst QString &title,\n\tconst QString &shortName,\n\tint count,\n\tStickersSetFlags flags,\n\tTimeId installDate)\n: id(id)\n, accessHash(accessHash)\n, hash(hash)\n, title(title)\n, shortName(shortName)\n, count(count)\n, flags(flags)\n, installDate(installDate)\n, _owner(owner) {\n}\n\nStickersSet::~StickersSet() = default;\n\nData::Session &StickersSet::owner() const {\n\treturn *_owner;\n}\n\nMain::Session &StickersSet::session() const {\n\treturn _owner->session();\n}\n\nMTPInputStickerSet StickersSet::mtpInput() const {\n\treturn (id && accessHash)\n\t\t? MTP_inputStickerSetID(MTP_long(id), MTP_long(accessHash))\n\t\t: MTP_inputStickerSetShortName(MTP_string(shortName));\n}\n\nStickerSetIdentifier StickersSet::identifier() const {\n\treturn StickerSetIdentifier{\n\t\t.id = id,\n\t\t.accessHash = accessHash,\n\t};\n}\n\nStickersType StickersSet::type() const {\n\treturn (flags & StickersSetFlag::Emoji)\n\t\t? StickersType::Emoji\n\t\t: (flags & StickersSetFlag::Masks)\n\t\t? StickersType::Masks\n\t\t: StickersType::Stickers;\n}\n\nbool StickersSet::textColor() const {\n\treturn flags & StickersSetFlag::TextColor;\n}\n\nbool StickersSet::channelStatus() const {\n\treturn flags & StickersSetFlag::ChannelStatus;\n}\n\nvoid StickersSet::setThumbnail(\n\t\tconst ImageWithLocation &data,\n\t\tStickerType type) {\n\t_thumbnailType = type;\n\tData::UpdateCloudFile(\n\t\t_thumbnail,\n\t\tdata,\n\t\t_owner->cache(),\n\t\tData::kImageCacheTag,\n\t\t[=](Data::FileOrigin origin) { loadThumbnail(); });\n\tif (!data.bytes.isEmpty()) {\n\t\tif (_thumbnail.loader) {\n\t\t\t_thumbnail.loader->cancel();\n\t\t}\n\t\tif (const auto view = activeThumbnailView()) {\n\t\t\tview->set(&_owner->session(), data.bytes);\n\t\t}\n\t}\n}\n\nbool StickersSet::hasThumbnail() const {\n\treturn _thumbnail.location.valid();\n}\n\nStickerType StickersSet::thumbnailType() const {\n\treturn _thumbnailType;\n}\n\nbool StickersSet::thumbnailLoading() const {\n\treturn (_thumbnail.loader != nullptr);\n}\n\nbool StickersSet::thumbnailFailed() const {\n\treturn (_thumbnail.flags & Data::CloudFile::Flag::Failed);\n}\n\nvoid StickersSet::loadThumbnail() {\n\tconst auto autoLoading = false;\n\tconst auto finalCheck = [=] {\n\t\tif (const auto active = activeThumbnailView()) {\n\t\t\treturn !active->image() && active->content().isEmpty();\n\t\t}\n\t\treturn true;\n\t};\n\tconst auto done = [=](QByteArray result) {\n\t\tif (const auto active = activeThumbnailView()) {\n\t\t\tactive->set(&_owner->session(), std::move(result));\n\t\t}\n\t};\n\tData::LoadCloudFile(\n\t\t&_owner->session(),\n\t\t_thumbnail,\n\t\tData::FileOriginStickerSet(id, accessHash),\n\t\tLoadFromCloudOrLocal,\n\t\tautoLoading,\n\t\tData::kImageCacheTag,\n\t\tfinalCheck,\n\t\tdone);\n}\n\nconst ImageLocation &StickersSet::thumbnailLocation() const {\n\treturn _thumbnail.location;\n}\n\nStorage::Cache::Key StickersSet::thumbnailBigFileBaseCacheKey() const {\n\tconst auto &location = _thumbnail.location.file().data;\n\tif (const auto storage = std::get_if<StorageFileLocation>(&location)) {\n\t\treturn storage->bigFileBaseCacheKey();\n\t}\n\treturn {};\n}\n\nint StickersSet::thumbnailByteSize() const {\n\treturn _thumbnail.byteSize;\n}\n\nDocumentData *StickersSet::lookupThumbnailDocument() const {\n\tif (thumbnailDocumentId) {\n\t\tconst auto i = ranges::find(\n\t\t\tstickers,\n\t\t\tthumbnailDocumentId,\n\t\t\t&DocumentData::id);\n\t\tif (i != stickers.end()) {\n\t\t\treturn *i;\n\t\t}\n\t}\n\treturn !stickers.empty()\n\t\t? stickers.front()\n\t\t: !covers.empty()\n\t\t? covers.front()\n\t\t: nullptr;\n}\n\nstd::shared_ptr<StickersSetThumbnailView> StickersSet::createThumbnailView() {\n\tif (auto active = activeThumbnailView()) {\n\t\treturn active;\n\t}\n\tauto view = std::make_shared<StickersSetThumbnailView>(this);\n\t_thumbnailView = view;\n\treturn view;\n}\n\nstd::shared_ptr<StickersSetThumbnailView> StickersSet::activeThumbnailView() {\n\treturn _thumbnailView.lock();\n}\n\nMTPInputStickerSet InputStickerSet(StickerSetIdentifier id) {\n\treturn !id\n\t\t? MTP_inputStickerSetEmpty()\n\t\t: id.id\n\t\t? MTP_inputStickerSetID(MTP_long(id.id), MTP_long(id.accessHash))\n\t\t: MTP_inputStickerSetShortName(MTP_string(id.shortName));\n}\n\nStickerSetIdentifier FromInputSet(const MTPInputStickerSet &id) {\n\treturn id.match([](const MTPDinputStickerSetID &data) {\n\t\treturn StickerSetIdentifier{\n\t\t\t.id = data.vid().v,\n\t\t\t.accessHash = data.vaccess_hash().v,\n\t\t};\n\t}, [](const MTPDinputStickerSetShortName &data) {\n\t\treturn StickerSetIdentifier{ .shortName = qs(data.vshort_name()) };\n\t}, [](const auto &) {\n\t\treturn StickerSetIdentifier();\n\t});\n}\n\n} // namespace Stickers\n"
  },
  {
    "path": "Telegram/SourceFiles/data/stickers/data_stickers_set.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_cloud_file.h\"\n\nclass DocumentData;\nenum class StickerType : uchar;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\n\nclass Session;\n\nusing StickersSetsOrder = QList<uint64>;\nusing SavedGifs = QVector<DocumentData*>;\nusing StickersPack = QVector<DocumentData*>;\n\nenum class StickersType : uchar;\n\nclass StickersSet;\nusing StickersSets = base::flat_map<uint64, std::unique_ptr<StickersSet>>;\n\nclass StickersSetThumbnailView final {\npublic:\n\texplicit StickersSetThumbnailView(not_null<StickersSet*> owner);\n\n\t[[nodiscard]] not_null<StickersSet*> owner() const;\n\n\tvoid set(not_null<Main::Session*> session, QByteArray content);\n\n\t[[nodiscard]] Image *image() const;\n\t[[nodiscard]] QByteArray content() const;\n\nprivate:\n\tconst not_null<StickersSet*> _owner;\n\tstd::unique_ptr<Image> _image;\n\tQByteArray _content;\n\n};\n\nenum class StickersSetFlag : ushort {\n\tInstalled = (1 << 0),\n\tArchived = (1 << 1),\n\tMasks = (1 << 2),\n\tOfficial = (1 << 3),\n\tNotLoaded = (1 << 4),\n\tFeatured = (1 << 5),\n\tUnread = (1 << 6),\n\tSpecial = (1 << 7),\n\tEmoji = (1 << 9),\n\tTextColor = (1 << 10),\n\tChannelStatus = (1 << 11),\n\tAmCreator = (1 << 12),\n};\ninline constexpr bool is_flag_type(StickersSetFlag) { return true; };\nusing StickersSetFlags = base::flags<StickersSetFlag>;\n\n[[nodiscard]] StickersSetFlags ParseStickersSetFlags(\n\tconst MTPDstickerSet &data);\n\nclass StickersSet final {\npublic:\n\tStickersSet(\n\t\tnot_null<Data::Session*> owner,\n\t\tuint64 id,\n\t\tuint64 accessHash,\n\t\tuint64 hash,\n\t\tconst QString &title,\n\t\tconst QString &shortName,\n\t\tint count,\n\t\tStickersSetFlags flags,\n\t\tTimeId installDate);\n\t~StickersSet();\n\n\t[[nodiscard]] Data::Session &owner() const;\n\t[[nodiscard]] Main::Session &session() const;\n\n\t[[nodiscard]] MTPInputStickerSet mtpInput() const;\n\t[[nodiscard]] StickerSetIdentifier identifier() const;\n\t[[nodiscard]] StickersType type() const;\n\t[[nodiscard]] bool textColor() const;\n\t[[nodiscard]] bool channelStatus() const;\n\n\tvoid setThumbnail(const ImageWithLocation &data, StickerType type);\n\n\t[[nodiscard]] bool hasThumbnail() const;\n\t[[nodiscard]] StickerType thumbnailType() const;\n\t[[nodiscard]] bool thumbnailLoading() const;\n\t[[nodiscard]] bool thumbnailFailed() const;\n\tvoid loadThumbnail();\n\t[[nodiscard]] const ImageLocation &thumbnailLocation() const;\n\t[[nodiscard]] Storage::Cache::Key thumbnailBigFileBaseCacheKey() const;\n\t[[nodiscard]] int thumbnailByteSize() const;\n\t[[nodiscard]] DocumentData *lookupThumbnailDocument() const;\n\n\t[[nodiscard]] std::shared_ptr<StickersSetThumbnailView> createThumbnailView();\n\t[[nodiscard]] std::shared_ptr<StickersSetThumbnailView> activeThumbnailView();\n\n\tuint64 id = 0;\n\tuint64 accessHash = 0;\n\tuint64 hash = 0;\n\tDocumentId thumbnailDocumentId = 0;\n\tQString title, shortName;\n\tint count = 0;\n\tint locked = 0;\n\tStickersSetFlags flags;\n\nprivate:\n\tStickerType _thumbnailType = {};\n\npublic:\n\tTimeId installDate = 0;\n\tStickersPack covers;\n\tStickersPack stickers;\n\tstd::vector<TimeId> dates;\n\tbase::flat_map<EmojiPtr, StickersPack> emoji;\n\nprivate:\n\tconst not_null<Data::Session*> _owner;\n\n\tCloudFile _thumbnail;\n\tstd::weak_ptr<StickersSetThumbnailView> _thumbnailView;\n\n};\n\n[[nodiscard]] MTPInputStickerSet InputStickerSet(StickerSetIdentifier id);\n[[nodiscard]] StickerSetIdentifier FromInputSet(const MTPInputStickerSet &id);\n\n} // namespace Data\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\n\nusing \"ui/layers/layers.style\"; // boxRoundShadow\nusing \"ui/widgets/widgets.style\";\n\nDialogRow {\n\theight: pixels;\n\tpadding: margins;\n\tphotoSize: pixels;\n\tnameLeft: pixels;\n\tnameTop: pixels;\n\ttextLeft: pixels;\n\ttextTop: pixels;\n\ttopicsSkip: pixels;\n\ttopicsSkipBig: pixels;\n\ttopicsHeight: pixels;\n\tunreadMarkDiameter: pixels;\n\ttagTop: pixels;\n}\nDialogRightButton {\n\tbutton: RoundButton;\n\tmargin: margins;\n}\n\nThreeStateIcon {\n\ticon: icon;\n\tover: icon;\n\tactive: icon;\n}\n\nVerifiedBadge {\n\tcolor: color;\n\theight: pixels;\n}\n\nForumTopicIcon {\n\tsize: pixels;\n\tfont: font;\n\ttextTop: pixels;\n}\n\nDialogsMiniIcon {\n\ticon: ThreeStateIcon;\n\tskipText: pixels;\n\tskipMedia: pixels;\n}\n\ndefaultForumTopicIcon: ForumTopicIcon {\n\tsize: 21px;\n\tfont: font(bold 11px);\n\ttextTop: 2px;\n}\nnormalForumTopicIcon: ForumTopicIcon {\n\tsize: 19px;\n\tfont: font(bold 10px);\n\ttextTop: 2px;\n}\nlargeForumTopicIcon: ForumTopicIcon {\n\tsize: 26px;\n\tfont: font(bold 13px);\n\ttextTop: 3px;\n}\ninfoForumTopicIcon: ForumTopicIcon {\n\tsize: 32px;\n\tfont: font(bold 15px);\n\ttextTop: 4px;\n}\n\ndialogsUnreadFont: font(12px bold);\ndialogsUnreadHeight: 19px;\ndialogsUnreadPadding: 5px;\n\ndialogsRipple: RippleAnimation(defaultRippleAnimation) {\n\tcolor: dialogsRippleBg;\n}\n\ndialogsTextFont: normalFont;\ndialogsTextStyle: defaultTextStyle;\ndialogsDateFont: font(13px);\ndialogsDateSkip: 5px;\n\ndialogsRowHeight: 62px;\ndialogsFilterPadding: point(7px, 7px);\ndialogsFilterSkip: 4px;\n\ndefaultDialogRow: DialogRow {\n\theight: dialogsRowHeight;\n\tpadding: margins(10px, 8px, 10px, 8px);\n\tphotoSize: 46px;\n\tnameLeft: 68px;\n\tnameTop: 10px;\n\ttextLeft: 68px;\n\ttextTop: 34px;\n}\ntaggedDialogRow: DialogRow(defaultDialogRow) {\n\theight: 72px;\n\ttextTop: 30px;\n\ttagTop: 52px;\n}\nforumDialogRow: DialogRow(defaultDialogRow) {\n\theight: 80px;\n\ttextTop: 32px;\n\ttopicsSkip: 8px;\n\ttopicsSkipBig: 14px;\n\ttopicsHeight: 21px;\n}\ntaggedForumDialogRow: DialogRow(forumDialogRow) {\n\theight: 96px;\n\ttagTop: 77px;\n}\ndialogRowFilterTagSkip: 4px;\ndialogRowFilterTagStyle: TextStyle(defaultTextStyle) {\n\tfont: font(10px);\n}\ndialogRowOpenBot: DialogRightButton {\n\tbutton: RoundButton(defaultActiveButton) {\n\t\theight: 20px;\n\t\ttextTop: 1px;\n\t}\n\tmargin: margins(0px, 32px, 10px, 0px);\n}\ndialogRowOpenBotRecent: DialogRightButton(dialogRowOpenBot) {\n\tmargin: margins(0px, 32px, 16px, 0px);\n}\ndialogsTopBarRightButton: RoundButton(defaultActiveButton) {\n\twidth: -16px;\n\theight: 22px;\n\ttextTop: 2px;\n}\n\nforumDialogJumpArrow: icon{{ \"dialogs/dialogs_topic_arrow\", dialogsTextFg }};\nforumDialogJumpArrowOver: icon{{ \"dialogs/dialogs_topic_arrow\", dialogsTextFgOver }};\nforumDialogJumpArrowSkip: 8px;\nforumDialogJumpArrowPosition: point(3px, 3px);\nforumDialogJumpPadding: margins(8px, 3px, 8px, 3px);\nforumDialogJumpRadius: 11px;\n\ndialogsOnlineBadgeStroke: 2px;\ndialogsOnlineBadgeSize: 10px;\ndialogsOnlineBadgeSkip: point(0px, 2px);\ndialogsOnlineBadgeDuration: 150;\n\ndialogsCallBadgeSize: 16px;\ndialogsCallBadgeSkip: point(-3px, -3px);\n\ndialogsSubscriptionBadgeSize: 16px;\ndialogsSubscriptionBadgeSkip: point(-4px, -4px);\n\ndialogsTTLBadgeSize: 20px;\ndialogsTTLBadgeInnerMargins: margins(2px, 2px, 2px, 2px);\n// Relative to a photo place, not a whole userpic place.\ndialogsTTLBadgeSkip: point(1px, 1px);\n\ndialogsSpeakingStrokeNumerator: 16px;\ndialogsSpeakingDenominator: 8.;\n\ndialogsImportantBarHeight: 37px;\n\ndialogsWidthDuration: universalDuration;\ndialogsTextWidthMin: 150px;\n\ndialogsTextPalette: TextPalette(defaultTextPalette) {\n\tlinkFg: dialogsTextFgService;\n\tmonoFg: dialogsTextFg;\n\tspoilerFg: dialogsTextFg;\n}\ndialogsTextPaletteOver: TextPalette(defaultTextPalette) {\n\tlinkFg: dialogsTextFgServiceOver;\n\tmonoFg: dialogsTextFgOver;\n\tspoilerFg: dialogsTextFgOver;\n}\ndialogsTextPaletteActive: TextPalette(defaultTextPalette) {\n\tlinkFg: dialogsTextFgServiceActive;\n\tmonoFg: dialogsTextFgActive;\n\tspoilerFg: dialogsTextFgActive;\n}\ndialogsTextPaletteDraft: TextPalette(defaultTextPalette) {\n\tlinkFg: dialogsDraftFg;\n\tmonoFg: dialogsTextFg;\n\tspoilerFg: dialogsTextFg;\n}\ndialogsTextPaletteDraftOver: TextPalette(defaultTextPalette) {\n\tlinkFg: dialogsDraftFgOver;\n\tmonoFg: dialogsTextFgOver;\n\tspoilerFg: dialogsTextFgOver;\n}\ndialogsTextPaletteDraftActive: TextPalette(defaultTextPalette) {\n\tlinkFg: dialogsDraftFgActive;\n\tmonoFg: dialogsTextFgActive;\n\tspoilerFg: dialogsTextFgActive;\n}\ndialogsTextPaletteTaken: TextPalette(defaultTextPalette) {\n\tlinkFg: boxTextFgGood;\n\tmonoFg: dialogsTextFg;\n\tspoilerFg: dialogsTextFg;\n}\ndialogsTextPaletteTakenOver: TextPalette(defaultTextPalette) {\n\tlinkFg: boxTextFgGood;\n\tmonoFg: dialogsTextFgOver;\n\tspoilerFg: dialogsTextFgOver;\n}\ndialogsTextPaletteTakenActive: TextPalette(defaultTextPalette) {\n\tlinkFg: dialogsDraftFgActive;\n\tmonoFg: dialogsTextFgActive;\n\tspoilerFg: dialogsTextFgActive;\n}\ndialogsTextPaletteArchive: TextPalette(defaultTextPalette) {\n\tlinkFg: dialogsArchiveFg;\n\tmonoFg: dialogsArchiveFg;\n\tspoilerFg: dialogsArchiveFg;\n}\ndialogsTextPaletteArchiveOver: TextPalette(defaultTextPalette) {\n\tlinkFg: dialogsArchiveFgOver;\n\tmonoFg: dialogsArchiveFgOver;\n\tspoilerFg: dialogsArchiveFgOver;\n}\ndialogsTextPaletteArchiveActive: TextPalette(defaultTextPalette) {\n\tlinkFg: dialogsTextFgActive;\n\tmonoFg: dialogsTextFgActive;\n\tspoilerFg: dialogsTextFgActive;\n}\ndialogsTextPaletteInTopic: TextPalette(defaultTextPalette) {\n\tlinkFg: dialogsNameFg;\n\tmonoFg: dialogsTextFg;\n\tspoilerFg: dialogsTextFg;\n}\ndialogsTextPaletteInTopicOver: TextPalette(defaultTextPalette) {\n\tlinkFg: dialogsNameFgOver;\n\tmonoFg: dialogsTextFgOver;\n\tspoilerFg: dialogsTextFgOver;\n}\ndialogsTextPaletteInTopicActive: TextPalette(defaultTextPalette) {\n\tlinkFg: dialogsNameFgActive;\n\tmonoFg: dialogsTextFgActive;\n\tspoilerFg: dialogsTextFgActive;\n}\n\ndialogsEmptyHeight: 160px;\ndialogsEmptySkip: 2px;\ndialogsEmptyLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 32px;\n\talign: align(top);\n\ttextFg: windowSubTextFg;\n}\ndialogEmptyButton: RoundButton(defaultActiveButton) {\n}\ndialogEmptyButtonSkip: 12px;\ndialogEmptyButtonLabel: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(boxFontSize semibold);\n\t}\n\tminWidth: 32px;\n\talign: align(top);\n\ttextFg: windowFg;\n}\n\ndialogsMenuToggle: IconButton {\n\twidth: 40px;\n\theight: 40px;\n\n\ticon: icon {{ \"dialogs/dialogs_menu\", dialogsMenuIconFg }};\n\ticonOver: icon {{ \"dialogs/dialogs_menu\", dialogsMenuIconFgOver }};\n\ticonPosition: point(-1px, -1px);\n\n\trippleAreaPosition: point(0px, 0px);\n\trippleAreaSize: 40px;\n\tripple: defaultRippleAnimationBgOver;\n}\ndialogsMenuToggleUnread: icon {\n\t{ \"dialogs/dialogs_menu_unread\", dialogsMenuIconFg },\n\t{ \"dialogs/dialogs_menu_unread_dot\", dialogsUnreadBg },\n};\ndialogsMenuToggleUnreadMuted: icon {\n\t{ \"dialogs/dialogs_menu_unread\", dialogsMenuIconFg },\n\t{ \"dialogs/dialogs_menu_unread_dot\", dialogsMenuIconFg },\n};\n\ndialogsLock: IconButton {\n\twidth: 36px;\n\theight: 38px;\n\n\ticon: icon {{ \"dialogs/dialogs_lock_off\", dialogsMenuIconFg }};\n\ticonOver: icon {{ \"dialogs/dialogs_lock_off\", dialogsMenuIconFgOver }};\n\ticonPosition: point(-1px, -1px);\n\n\tripple: emptyRippleAnimation;\n}\ndialogsUnlockIcon: icon {{ \"dialogs/dialogs_lock_on\", dialogsMenuIconFg }};\ndialogsUnlockIconOver: icon {{ \"dialogs/dialogs_lock_on\", dialogsMenuIconFgOver }};\ndialogsCalendar: IconButton {\n\twidth: 32px;\n\theight: 35px;\n\n\ticon: icon {{ \"dialogs/dialogs_calendar\", dialogsMenuIconFg }};\n\ticonOver: icon {{ \"dialogs/dialogs_calendar\", dialogsMenuIconFgOver }};\n\ticonPosition: point(1px, 6px);\n}\ndialogsSearchFrom: IconButton(dialogsCalendar) {\n\twidth: 29px;\n\ticon: icon {{ \"dialogs/dialogs_search_from\", dialogsMenuIconFg }};\n\ticonOver: icon {{ \"dialogs/dialogs_search_from\", dialogsMenuIconFgOver }};\n}\ndialogsSearchForNarrowFilters: IconButton(dialogsMenuToggle) {\n\ticon: icon {{ \"top_bar_search\", menuIconFg }};\n\ticonOver: icon {{ \"top_bar_search\", menuIconFgOver }};\n\ticonPosition: point(4px, 4px);\n}\n\ndialogsFilter: InputField(defaultInputField) {\n\ttextBg: filterInputInactiveBg;\n\ttextBgActive: filterInputActiveBg;\n\ttextMargins: margins(12px, 8px, 30px, 5px);\n\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(5px, 0px, 2px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderShift: -50px;\n\tplaceholderFont: normalFont;\n\n\tborderFg: filterInputInactiveBg;\n\tborderFgActive: windowBgRipple;\n\tborderFgError: activeLineFgError;\n\n\tborder: 3px;\n\tborderActive: 2px;\n\tborderRadius: 18px;\n\tborderDenominator: 2;\n\n\tstyle: defaultTextStyle;\n\n\theightMin: 35px;\n}\ndialogsCancelSearchInPeer: IconButton(dialogsMenuToggle) {\n\ticon: icon {{ \"dialogs/dialogs_cancel_search\", dialogsMenuIconFg }};\n\ticonOver: icon {{ \"dialogs/dialogs_cancel_search\", dialogsMenuIconFgOver }};\n\ticonPosition: point(11px, 11px);\n\trippleAreaPosition: point(3px, 3px);\n\trippleAreaSize: 34px;\n}\ndialogsCancelSearch: CrossButton {\n\twidth: 35px;\n\theight: 35px;\n\n\tcross: CrossAnimation {\n\t\tsize: 35px;\n\t\tskip: 12px;\n\t\tstroke: 1.5;\n\t\tminScale: 0.3;\n\t}\n\tcrossFg: dialogsMenuIconFg;\n\tcrossFgOver: dialogsMenuIconFgOver;\n\tcrossPosition: point(0px, 0px);\n\n\tduration: 150;\n\tloadingPeriod: 1000;\n\tripple: emptyRippleAnimation;\n}\n\ndialogCalendar: IconButton(dialogsMenuToggle) {\n\ticon: icon {{ \"dialogs/dialogs_calendar\", lightButtonFg }};\n\ticonOver: icon {{ \"dialogs/dialogs_calendar\", lightButtonFgOver }};\n\ticonPosition: point(8px, 9px);\n\n\trippleAreaPosition: point(3px, 3px);\n\trippleAreaSize: 34px;\n}\ndialogSearchFrom: IconButton(dialogCalendar) {\n\ticon: icon {{ \"dialogs/dialogs_search_from\", lightButtonFg }};\n\ticonOver: icon {{ \"dialogs/dialogs_search_from\", lightButtonFgOver }};\n\ticonPosition: point(9px, 8px);\n}\n\ndialogsChatTypeSkip: 3px;\ndialogsChatIcon: ThreeStateIcon {\n\ticon: icon {{ \"dialogs/dialogs_chat\", dialogsChatIconFg, point(1px, 4px) }};\n\tover: icon {{ \"dialogs/dialogs_chat\", dialogsChatIconFgOver, point(1px, 4px) }};\n\tactive: icon {{ \"dialogs/dialogs_chat\", dialogsChatIconFgActive, point(1px, 4px) }};\n}\ndialogsChannelIcon: ThreeStateIcon {\n\ticon: icon {{ \"dialogs/dialogs_channel\", dialogsChatIconFg, point(3px, 4px) }};\n\tover: icon {{ \"dialogs/dialogs_channel\", dialogsChatIconFgOver, point(3px, 4px) }};\n\tactive: icon {{ \"dialogs/dialogs_channel\", dialogsChatIconFgActive, point(3px, 4px) }};\n}\ndialogsBotIcon: ThreeStateIcon {\n\ticon: icon {{ \"dialogs/dialogs_bot\", dialogsChatIconFg, point(1px, 3px) }};\n\tover: icon {{ \"dialogs/dialogs_bot\", dialogsChatIconFgOver, point(1px, 3px) }};\n\tactive: icon {{ \"dialogs/dialogs_bot\", dialogsChatIconFgActive, point(1px, 3px) }};\n}\ndialogsForumIcon: ThreeStateIcon {\n\ticon: icon {{ \"dialogs/dialogs_forum\", dialogsChatIconFg, point(1px, 4px) }};\n\tover: icon {{ \"dialogs/dialogs_forum\", dialogsChatIconFgOver, point(1px, 4px) }};\n\tactive: icon {{ \"dialogs/dialogs_forum\", dialogsChatIconFgActive, point(1px, 4px) }};\n}\ndialogsArchiveUserpic: icon {{ \"archive_userpic\", historyPeerUserpicFg }};\ndialogsRepliesUserpic: icon {{ \"replies_userpic\", historyPeerUserpicFg }};\ndialogsInaccessibleUserpic: icon {{ \"dialogs/inaccessible_userpic\", historyPeerUserpicFg }};\ndialogsHiddenAuthorUserpic: icon {{ \"dialogs/avatar_hidden\", premiumButtonFg }};\ndialogsMyNotesUserpic: icon {{ \"dialogs/avatar_notes\", historyPeerUserpicFg }};\n\ndialogsSendStateSkip: 20px;\ndialogsSendingIcon: ThreeStateIcon {\n\ticon: icon {{ \"dialogs/dialogs_sending\", dialogsSendingIconFg, point(8px, 4px) }};\n\tover: icon {{ \"dialogs/dialogs_sending\", dialogsSendingIconFgOver, point(8px, 4px) }};\n\tactive: icon {{ \"dialogs/dialogs_sending\", dialogsSendingIconFgActive, point(8px, 4px) }};\n}\ndialogsSentIcon: ThreeStateIcon {\n\ticon: icon {{ \"dialogs/dialogs_sent\", dialogsSentIconFg, point(10px, 4px) }};\n\tover: icon {{ \"dialogs/dialogs_sent\", dialogsSentIconFgOver, point(10px, 4px) }};\n\tactive: icon {{ \"dialogs/dialogs_sent\", dialogsSentIconFgActive, point(10px, 4px) }};\n}\ndialogsReceivedIcon: ThreeStateIcon {\n\ticon: icon {{ \"dialogs/dialogs_received\", dialogsSentIconFg, point(5px, 4px) }};\n\tover: icon {{ \"dialogs/dialogs_received\", dialogsSentIconFgOver, point(5px, 4px) }};\n\tactive: icon {{ \"dialogs/dialogs_received\", dialogsSentIconFgActive, point(5px, 4px) }};\n}\ndialogsPinnedIcon: ThreeStateIcon {\n\ticon: icon {{ \"dialogs/dialogs_pinned\", dialogsUnreadBgMuted }};\n\tover: icon {{ \"dialogs/dialogs_pinned\", dialogsUnreadBgMutedOver }};\n\tactive: icon {{ \"dialogs/dialogs_pinned\", dialogsUnreadBgMutedActive }};\n}\ndialogsLockIcon: ThreeStateIcon {\n\ticon: icon {{ \"emoji/premium_lock\", dialogsUnreadBgMuted, point(4px, 0px) }};\n\tover: icon {{ \"emoji/premium_lock\", dialogsUnreadBgMutedOver, point(4px, 0px) }};\n\tactive: icon {{ \"emoji/premium_lock\", dialogsUnreadBgMutedActive, point(4px, 0px) }};\n}\n\ndialogsVerifiedColors: VerifiedBadge {\n\theight: 20px;\n\tcolor: dialogsVerifiedIconBg;\n}\ndialogsVerifiedColorsOver: VerifiedBadge(dialogsVerifiedColors) {\n\tcolor: dialogsVerifiedIconBgOver;\n}\ndialogsVerifiedColorsActive: VerifiedBadge(dialogsVerifiedColors) {\n\tcolor: dialogsVerifiedIconBgActive;\n}\n\ndialogsVerifiedIcon: icon {\n\t{ \"dialogs/dialogs_verified_star\", dialogsVerifiedIconBg },\n\t{ \"dialogs/dialogs_verified_check\", dialogsVerifiedIconFg },\n};\ndialogsVerifiedIconOver: icon {\n\t{ \"dialogs/dialogs_verified_star\", dialogsVerifiedIconBgOver },\n\t{ \"dialogs/dialogs_verified_check\", dialogsVerifiedIconFgOver },\n};\ndialogsVerifiedIconActive: icon {\n\t{ \"dialogs/dialogs_verified_star\", dialogsVerifiedIconBgActive },\n\t{ \"dialogs/dialogs_verified_check\", dialogsVerifiedIconFgActive },\n};\ndialogsPremiumIcon: ThreeStateIcon {\n\ticon: icon {{ \"dialogs/dialogs_premium\", dialogsVerifiedIconBg }};\n\tover: icon {{ \"dialogs/dialogs_premium\", dialogsVerifiedIconBgOver }};\n\tactive: icon {{ \"dialogs/dialogs_premium\", dialogsVerifiedIconBgActive }};\n}\n\nhistorySendingIcon: icon {{ \"dialogs/dialogs_sending\", historySendingOutIconFg, point(5px, 5px) }};\nhistorySendingInvertedIcon: icon {{ \"dialogs/dialogs_sending\", historySendingInvertedIconFg, point(5px, 5px) }};\nhistoryViewsSendingIcon: icon {{ \"dialogs/dialogs_sending\", historySendingInIconFg, point(3px, 0px) }};\nhistoryViewsSendingInvertedIcon: icon {{ \"dialogs/dialogs_sending\", historySendingInvertedIconFg, point(3px, 0px) }};\n\ndialogsUpdateButton: FlatButton {\n\tcolor: activeButtonFg;\n\toverColor: activeButtonFgOver;\n\n\tbgColor: activeButtonBg;\n\toverBgColor: activeButtonBgOver;\n\n\twidth: -34px;\n\theight: 46px;\n\n\ttextTop: 14px;\n\n\tfont: semiboldFont;\n\toverFont: semiboldFont;\n\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: activeButtonBgRipple;\n\t}\n}\ndialogsInstallUpdate: icon {{ \"install_update\", activeButtonFg }};\ndialogsInstallUpdateOver: icon {{ \"install_update\", activeButtonFgOver }};\ndialogsInstallUpdateIconSkip: 7px;\ndialogsInstallUpdateIconSize: 20px;\ndialogsInstallUpdateIconInnerMargin: 5px;\ndialogsInstallUpdateIconSide1: 5px;\ndialogsInstallUpdateIconSide2: 3px;\n\ndialogsLoadMoreButton: FlatButton(dialogsUpdateButton) {\n\tcolor: lightButtonFg;\n\toverColor: lightButtonFg;\n\tbgColor: lightButtonBg;\n\toverBgColor: lightButtonBgOver;\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: lightButtonBgRipple;\n\t}\n\n\theight: 36px;\n\ttextTop: 9px;\n\tfont: semiboldFont;\n\toverFont: semiboldFont;\n}\ndialogsLoadMore: icon {{ \"install_update-flip_vertical\", lightButtonFg }};\ndialogsLoadMoreLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {\n\tcolor: lightButtonFg;\n\tthickness: 3px;\n\tsize: size(12px, 12px);\n}\n\ndialogsSearchInHeight: 38px;\ndialogsSearchInPhotoSize: 28px;\ndialogsSearchInPhotoPadding: 10px;\ndialogsSearchInSkip: 10px;\ndialogsSearchInNameTop: 9px;\ndialogsSearchInDownTop: 15px;\ndialogsSearchInDown: icon {{ \"intro_country_dropdown\", windowBoldFg }};\ndialogsSearchInDownSkip: 4px;\ndialogsSearchInMenu: PopupMenu(defaultPopupMenu) {\n\tscrollPadding: margins(0px, 0px, 0px, 0px);\n\tradius: 8px;\n\tmenu: menuWithIcons;\n}\ndialogsSearchInCheck: icon {{ \"player/player_check\", mediaPlayerActiveFg }};\ndialogsSearchInCheckSkip: 8px;\ndialogsSearchFromStyle: defaultTextStyle;\ndialogsSearchFromPalette: TextPalette(defaultTextPalette) {\n\tlinkFg: dialogsNameFg;\n}\n\ndialogsScamPadding: margins(2px, 0px, 2px, 0px);\ndialogsScamFont: font(9px semibold);\ndialogsScamSkip: 4px;\ndialogsScamRadius: 2px;\n\ndialogsMiniPreviewTop: 1px;\ndialogsMiniPreview: 16px;\ndialogsMiniPreviewRadius: 2px;\ndialogsMiniPreviewSkip: 2px;\ndialogsMiniPreviewRight: 3px;\ndialogsMiniPlay: icon{{ \"dialogs/dialogs_mini_play\", videoPlayIconFg }};\n\ndialogsMiniForward: DialogsMiniIcon {\n\ticon: ThreeStateIcon {\n\t\ticon: icon {{ \"mini_forward\", dialogsTextFg, point(0px, 1px) }};\n\t\tover: icon {{ \"mini_forward\", dialogsTextFgOver, point(0px, 1px) }};\n\t\tactive: icon {{ \"mini_forward\", dialogsTextFgActive, point(0px, 1px) }};\n\t}\n\tskipText: 1px;\n\tskipMedia: 2px;\n}\n\ndialogsMiniReplyIcon: IconEmoji {\n\ticon: icon {{ \"mini_forward-flip_horizontal\", attentionButtonFg }};\n\tpadding: margins(0px, 2px, 0px, 0px);\n}\n\ndialogsMiniReplyStory: DialogsMiniIcon {\n\ticon: ThreeStateIcon {\n\t\ticon: icon {{ \"mini_reply_story\", dialogsTextFg, point(0px, 1px) }};\n\t\tover: icon {{ \"mini_reply_story\", dialogsTextFgOver, point(0px, 1px) }};\n\t\tactive: icon {{ \"mini_reply_story\", dialogsTextFgActive, point(0px, 1px) }};\n\t}\n\tskipText: 4px;\n\tskipMedia: 5px;\n}\n\ndialogsUnreadMention: ThreeStateIcon {\n\ticon: icon{{ \"dialogs/dialogs_chatlist_mention-18x18\", dialogsMentionIconFg }};\n\tover: icon{{ \"dialogs/dialogs_chatlist_mention-18x18\", dialogsMentionIconFg }};\n\tactive: icon{{ \"dialogs/dialogs_chatlist_mention-18x18\", dialogsNameFgActive }};\n}\ndialogsUnreadMentionMuted: ThreeStateIcon {\n\ticon: icon{{ \"dialogs/dialogs_chatlist_mention-18x18\", dialogsUnreadBgMuted }};\n\tover: icon{{ \"dialogs/dialogs_chatlist_mention-18x18\", dialogsUnreadBgMutedOver }};\n\tactive: icon{{ \"dialogs/dialogs_chatlist_mention-18x18\", dialogsUnreadBgMutedActive }};\n}\ndialogsUnreadReaction: ThreeStateIcon {\n\ticon: icon{{ \"dialogs/dialogs_chatlist_reaction-18x18\", dialogsReactionIconFg }};\n\tover: icon{{ \"dialogs/dialogs_chatlist_reaction-18x18\", dialogsReactionIconFg }};\n\tactive: icon{{ \"dialogs/dialogs_chatlist_reaction-18x18\", dialogsNameFgActive }};\n}\ndialogsUnreadReactionMuted: ThreeStateIcon {\n\ticon: icon{{ \"dialogs/dialogs_chatlist_reaction-18x18\", dialogsUnreadBgMuted }};\n\tover: icon{{ \"dialogs/dialogs_chatlist_reaction-18x18\", dialogsUnreadBgMutedOver }};\n\tactive: icon{{ \"dialogs/dialogs_chatlist_reaction-18x18\", dialogsUnreadBgMutedActive }};\n}\ndialogsUnreadPoll: ThreeStateIcon {\n\ticon: icon{{ \"dialogs/dialogs_chatlist_poll-18x18\", dialogsPollIconFg }};\n\tover: icon{{ \"dialogs/dialogs_chatlist_poll-18x18\", dialogsPollIconFg }};\n\tactive: icon{{ \"dialogs/dialogs_chatlist_poll-18x18\", dialogsNameFgActive }};\n}\ndialogsUnreadPollMuted: ThreeStateIcon {\n\ticon: icon{{ \"dialogs/dialogs_chatlist_poll-18x18\", dialogsUnreadBgMuted }};\n\tover: icon{{ \"dialogs/dialogs_chatlist_poll-18x18\", dialogsUnreadBgMutedOver }};\n\tactive: icon{{ \"dialogs/dialogs_chatlist_poll-18x18\", dialogsUnreadBgMutedActive }};\n}\ndialogsUnreadMentionBadge: ThreeStateIcon {\n\ticon: icon{{ \"dialogs/dialogs_chatlist_mention-13x13\", dialogsUnreadFg }};\n\tover: icon{{ \"dialogs/dialogs_chatlist_mention-13x13\", dialogsUnreadFgOver }};\n\tactive: icon{{ \"dialogs/dialogs_chatlist_mention-13x13\", dialogsUnreadFgActive }};\n}\ndialogsUnreadMentionBadgeMuted: ThreeStateIcon {\n\ticon: icon{{ \"dialogs/dialogs_chatlist_mention-13x13\", dialogsUnreadFg }};\n\tover: icon{{ \"dialogs/dialogs_chatlist_mention-13x13\", dialogsUnreadFgOver }};\n\tactive: icon{{ \"dialogs/dialogs_chatlist_mention-13x13\", dialogsUnreadFgActive }};\n}\ndialogsUnreadReactionBadge: ThreeStateIcon {\n\ticon: icon{{ \"dialogs/dialogs_chatlist_reaction-13x13\", dialogsUnreadFg }};\n\tover: icon{{ \"dialogs/dialogs_chatlist_reaction-13x13\", dialogsUnreadFgOver }};\n\tactive: icon{{ \"dialogs/dialogs_chatlist_reaction-13x13\", dialogsUnreadFgActive }};\n}\ndialogsUnreadReactionBadgeMuted: ThreeStateIcon {\n\ticon: icon{{ \"dialogs/dialogs_chatlist_reaction-13x13\", dialogsUnreadFg }};\n\tover: icon{{ \"dialogs/dialogs_chatlist_reaction-13x13\", dialogsUnreadFgOver }};\n\tactive: icon{{ \"dialogs/dialogs_chatlist_reaction-13x13\", dialogsUnreadFgActive }};\n}\ndialogsUnreadPollBadge: ThreeStateIcon {\n\ticon: icon{{ \"dialogs/dialogs_chatlist_poll-13x13\", dialogsUnreadFg }};\n\tover: icon{{ \"dialogs/dialogs_chatlist_poll-13x13\", dialogsUnreadFgOver }};\n\tactive: icon{{ \"dialogs/dialogs_chatlist_poll-13x13\", dialogsUnreadFgActive }};\n}\ndialogsUnreadPollBadgeMuted: ThreeStateIcon {\n\ticon: icon{{ \"dialogs/dialogs_chatlist_poll-13x13\", dialogsUnreadFg }};\n\tover: icon{{ \"dialogs/dialogs_chatlist_poll-13x13\", dialogsUnreadFgOver }};\n\tactive: icon{{ \"dialogs/dialogs_chatlist_poll-13x13\", dialogsUnreadFgActive }};\n}\n\ndialogsMuteIcon: ThreeStateIcon {\n\ticon: icon{{ \"dialogs/dialogs_mute\", dialogsUnreadBgMuted }};\n\tover: icon{{ \"dialogs/dialogs_mute\", dialogsUnreadBgMutedOver }};\n\tactive: icon{{ \"dialogs/dialogs_mute\", dialogsUnreadBgMutedActive }};\n}\ndialogsMuteIconSkip: 4px;\n\ndownloadBarHeight: 46px;\ndownloadArrow: icon{{ \"fast_to_original\", menuIconFg }};\ndownloadArrowOver: icon{{ \"fast_to_original\", menuIconFgOver }};\ndownloadArrowRight: 10px;\ndownloadTitleLeft: 57px;\ndownloadTitleTop: 4px;\ndownloadInfoStyle: TextStyle(defaultTextStyle) {\n\tfont: font(12px);\n}\ndownloadInfoLeft: 57px;\ndownloadInfoTop: 23px;\ndownloadLoadingLeft: 15px;\ndownloadLoadingSize: 24px;\ndownloadLoadingLine: 2px;\ndownloadIconDocument: icon {{ \"dialogs/dialogs_downloads\", windowFgActive }};\ndownloadIconSize: 16px;\ndownloadIconSizeDone: 20px;\n\nforumTopicRow: DialogRow(defaultDialogRow) {\n\theight: 54px;\n\tpadding: margins(8px, 7px, 10px, 7px);\n\tphotoSize: 20px;\n\tnameLeft: 39px;\n\tnameTop: 7px;\n\ttextLeft: 39px;\n\ttextTop: 29px;\n\tunreadMarkDiameter: 8px;\n}\nforumTopicIconPosition: point(2px, 0px);\neditTopicTitleMargin: margins(70px, 2px, 22px, 18px);\neditTopicIconPosition: point(24px, 19px);\neditTopicMaxHeight: 408px;\n\nchooseTopicListItem: PeerListItem(defaultPeerListItem) {\n\theight: 44px;\n\tphotoSize: 20px;\n\tphotoPosition: point(16px, 12px);\n\tnamePosition: point(55px, 11px);\n\tnameStyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px semibold);\n\t}\n}\nchooseTopicList: PeerList(defaultPeerList) {\n\titem: chooseTopicListItem;\n}\n\nDialogsStories {\n\tleft: pixels;\n\theight: pixels;\n\tphoto: pixels;\n\tphotoLeft: pixels;\n\tphotoTop: pixels;\n\tshift: pixels;\n\tlineTwice: pixels;\n\tlineReadTwice: pixels;\n\tnameLeft: pixels;\n\tnameRight: pixels;\n\tnameTop: pixels;\n\tnameStyle: TextStyle;\n}\nDialogsStoriesList {\n\tsmall: DialogsStories;\n\tfull: DialogsStories;\n\tbg: color;\n\treadOpacity: double;\n\tfullClickable: int;\n}\n\ndialogsStories: DialogsStories {\n\tleft: 4px;\n\theight: 35px;\n\tphoto: 21px;\n\tphotoTop: 4px;\n\tphotoLeft: 4px;\n\tshift: 16px;\n\tlineTwice: 3px;\n\tlineReadTwice: 0px;\n\tnameLeft: 11px;\n\tnameRight: 10px;\n\tnameTop: 3px;\n\tnameStyle: semiboldTextStyle;\n}\n\ndialogsStoriesFull: DialogsStories {\n\tleft: 4px;\n\theight: 77px;\n\tphoto: 42px;\n\tphotoLeft: 10px;\n\tphotoTop: 9px;\n\tlineTwice: 4px;\n\tlineReadTwice: 2px;\n\tnameLeft: 0px;\n\tnameRight: 0px;\n\tnameTop: 56px;\n\tnameStyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(11px);\n\t}\n}\ntopPeers: DialogsStories(dialogsStoriesFull) {\n\tphoto: 46px;\n\tphotoLeft: 10px;\n\tphotoTop: 8px;\n\tnameLeft: 6px;\n}\ntopPeersRadius: 4px;\ntopPeersMargin: margins(3px, 3px, 3px, 4px);\n\nrecentPeersEmptySize: 100px;\nrecentPeersEmptyMargin: margins(10px, 10px, 10px, 10px);\nrecentPeersEmptySkip: 10px;\nrecentPeersEmptyHeightMin: 220px;\nrecentPeersItem: PeerListItem(defaultPeerListItem) {\n\theight: 56px;\n\tphotoSize: 42px;\n\tphotoPosition: point(10px, 7px);\n\tnamePosition: point(64px, 9px);\n\tstatusPosition: point(64px, 30px);\n\tbutton: OutlineButton(defaultPeerListButton) {\n\t\ttextBg: contactsBg;\n\t\ttextBgOver: contactsBgOver;\n\t\tripple: defaultRippleAnimation;\n\t}\n\tstatusFg: contactsStatusFg;\n\tstatusFgOver: contactsStatusFgOver;\n\tstatusFgActive: contactsStatusFgOnline;\n}\nrecentPeersList: PeerList(defaultPeerList) {\n\tpadding: margins(0px, 4px, 0px, 4px);\n\titem: recentPeersItem;\n}\nrecentPeersItemActive: PeerListItem(recentPeersItem) {\n\tnameFg: dialogsNameFgActive;\n\tnameFgChecked: dialogsNameFgActive;\n\tbutton: OutlineButton(defaultPeerListButton) {\n\t\ttextBg: dialogsBgActive;\n\t\ttextBgOver: dialogsBgActive;\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: dialogsRippleBgActive;\n\t\t}\n\t}\n\tstatusFg: dialogsTextFgActive;\n\tstatusFgOver: dialogsTextFgActive;\n\tstatusFgActive: dialogsTextFgActive;\n}\nrecentPeersSpecialName: PeerListItem(recentPeersItem) {\n\tnamePosition: point(64px, 19px);\n}\n\ndialogsTabsScroll: ScrollArea(defaultScrollArea) {\n\tbarHidden: true;\n}\ndialogsSearchTabs: SettingsSlider(defaultSettingsSlider) {\n\tpadding: 9px;\n\theight: 33px;\n\tbarTop: 30px;\n\tbarSkip: 0px;\n\tbarStroke: 6px;\n\tbarRadius: 2px;\n\tbarFg: transparent;\n\tbarSnapToLabel: true;\n\tstrictSkip: 18px;\n\tlabelTop: 7px;\n\tlabelStyle: semiboldTextStyle;\n\tlabelFg: windowSubTextFg;\n\tlabelFgActive: lightButtonFg;\n\trippleBottomSkip: 1px;\n\trippleBg: windowBgOver;\n\trippleBgActive: lightButtonBgOver;\n\tripple: defaultRippleAnimation;\n}\n\nchatsFiltersTabs: SettingsSlider(dialogsSearchTabs) {\n\trippleBottomSkip: 0px;\n}\n\ndialogsStoriesList: DialogsStoriesList {\n\tsmall: dialogsStories;\n\tfull: dialogsStoriesFull;\n\tbg: dialogsBg;\n\treadOpacity: 0.6;\n\tfullClickable: 0;\n}\ndialogsStoriesListInfo: DialogsStoriesList(dialogsStoriesList) {\n\tbg: transparent;\n\tfullClickable: 1;\n}\ndialogsStoriesListMine: DialogsStoriesList(dialogsStoriesListInfo) {\n\treadOpacity: 1.;\n}\ndialogsStoriesTooltip: ImportantTooltip(defaultImportantTooltip) {\n\tpadding: margins(0px, 0px, 0px, 0px);\n}\ndialogsStoriesTooltipLabel: defaultImportantTooltipLabel;\ndialogsStoriesTooltipMaxWidth: 200px;\nsearchedBarHeight: 28px;\nsearchedBarFont: normalFont;\nsearchedBarPosition: point(14px, 5px);\nsearchedBarLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: searchedBarFg;\n\tmargin: margins(14px, 5px, 14px, 5px);\n}\nsearchedBarLink: LinkButton(defaultLinkButton) {\n\tcolor: searchedBarFg;\n\toverColor: searchedBarFg;\n\tpadding: margins(14px, 5px, 14px, 5px);\n}\n\ndialogsSearchTagSkip: point(8px, 4px);\ndialogsSearchTagBottom: 10px;\ndialogsSearchTagLocked: icon{{ \"dialogs/mini_tag_lock\", lightButtonFgOver }};\ndialogsSearchTagPromo: defaultTextStyle;\ndialogsSearchTagArrow: IconEmoji {\n\ticon: icon{{ \"dialogs/mini_arrow\", windowSubTextFg }};\n\tpadding: margins(-6px, 3px, 0px, 0px);\n}\ndialogsSearchTagPromoLeft: 6px;\ndialogsSearchTagPromoRight: 1px;\ndialogsSearchTagPromoSkip: 6px;\n\ndialogsPopularAppsPadding: margins(10px, 8px, 10px, 12px);\ndialogsPopularAppsAbout: FlatLabel(boxDividerLabel) {\n\tminWidth: 128px;\n}\n\ndialogsQuickActionSize: 20px;\ndialogsQuickActionRippleSize: 80px;\n\ndialogsSponsoredButton: DialogRightButton(dialogRowOpenBot) {\n\tbutton: RoundButton(defaultLightButton) {\n\t\ttextFg: windowActiveTextFg;\n\t\ttextFgOver: windowActiveTextFg;\n\t\ttextBg: lightButtonBgOver;\n\t\ttextBgOver: lightButtonBgOver;\n\t\theight: 20px;\n\t\ttextTop: 1px;\n\t}\n\tmargin: margins(0px, 9px, 10px, 0px);\n}\n\ndialogsTopBarLeftPadding: 18px;\ndialogsTopBarSuggestionTitleStyle: TextStyle(defaultTextStyle) {\n\tfont: font(semibold 12px);\n}\ndialogsTopBarSuggestionAboutStyle: TextStyle(defaultTextStyle) {\n\tfont: font(11px);\n}\ndialogsSuggestionDeniedAuthLottie: size(70px, 70px);\ndialogsSuggestionDeniedAuthLottieCircle: size(90px, 90px);\ndialogsSuggestionDeniedAuthLottieMargins: margins(10px, 10px, 10px, 10px);\n\ndialogsMiniQuoteIcon: icon{{ \"chat/mini_quote\", dialogsTextFg }};\n\npostsSearchIntroTitle: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowBoldFg;\n\tminWidth: 64px;\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(semibold 16px);\n\t}\n\talign: align(top);\n}\npostsSearchIntroTitleMargin: margins(20px, 0px, 20px, 4px);\npostsSearchIntroSubtitle: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n\tminWidth: 64px;\n\talign: align(top);\n}\npostsSearchIntroSubtitleMargin: margins(20px, 4px, 20px, 16px);\npostsSearchIntroButton: RoundButton(defaultActiveButton) {\n\twidth: 200px;\n\theight: 42px;\n\ttextTop: 12px;\n\tstyle: semiboldTextStyle;\n}\npostsSearchIntroFooter: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n\tminWidth: 64px;\n\talign: align(top);\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px);\n\t}\n}\npostsSearchIntroFooterMargin: margins(20px, 12px, 20px, 0px);\npostsSearchIcon: icon {{ \"box_search\", windowFgActive }};\npostsSearchIconPadding: margins(12px, 1px, 6px, 0px);\npostsSearchArrow: icon {{ \"dialogs/mini_arrow\", windowFgActive }};\npostsSearchArrowPadding: margins(2px, 1px, 8px, 0px);\n\ndialogsUnconfirmedAuthPadding: margins(10px, 0px, 10px, 0px);\ndialogsUnconfirmedAuthTitle: FlatLabel(boxLabel) {\n\tminWidth: 70px;\n\talign: align(top);\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px);\n\t}\n}\ndialogsUnconfirmedAuthAbout: FlatLabel(boxLabel) {\n\tminWidth: 70px;\n\talign: align(top);\n\ttextFg: windowSubTextFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(11px);\n\t}\n}\ndialogsUnconfirmedAuthButton: RoundButton(defaultBoxButton) {\n\twidth: -16px;\n\theight: 26px;\n\ttextTop: 5px;\n\tradius: 10px;\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(11px semibold);\n\t}\n}\ndialogsUnconfirmedAuthButtonNo: RoundButton(dialogsUnconfirmedAuthButton) {\n\ttextFg: attentionButtonFg;\n\ttextFgOver: attentionButtonFgOver;\n\ttextBgOver: attentionButtonBgOver;\n\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: attentionButtonBgRipple;\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#ifdef _DEBUG\n#include <QtCore/QDebug>\n#endif // _DEBUG\n\nnamespace style {\nstruct DialogRightButton;\n} // namespace style\n\nnamespace Ui {\nclass RippleAnimation;\n} // namespace Ui\n\nnamespace Dialogs {\n\nclass Row;\n\nenum class SortMode {\n\tDate    = 0x00,\n\tName    = 0x01,\n\tAdd     = 0x02,\n};\n\nstruct PositionChange {\n\tint from = -1;\n\tint to = -1;\n\tint height = 0;\n};\n\nstruct UnreadState {\n\tint messages = 0;\n\tint messagesMuted = 0;\n\tint chats = 0;\n\tint chatsMuted = 0;\n\tint marks = 0;\n\tint marksMuted = 0;\n\tint reactions = 0;\n\tint reactionsMuted = 0;\n\tint mentions = 0;\n\tint polls = 0;\n\tint pollsMuted = 0;\n\tbool known = false;\n\n\tUnreadState &operator+=(const UnreadState &other) {\n\t\tmessages += other.messages;\n\t\tmessagesMuted += other.messagesMuted;\n\t\tchats += other.chats;\n\t\tchatsMuted += other.chatsMuted;\n\t\tmarks += other.marks;\n\t\tmarksMuted += other.marksMuted;\n\t\treactions += other.reactions;\n\t\treactionsMuted += other.reactionsMuted;\n\t\tmentions += other.mentions;\n\t\tpolls += other.polls;\n\t\tpollsMuted += other.pollsMuted;\n\t\treturn *this;\n\t}\n\tUnreadState &operator-=(const UnreadState &other) {\n\t\tmessages -= other.messages;\n\t\tmessagesMuted -= other.messagesMuted;\n\t\tchats -= other.chats;\n\t\tchatsMuted -= other.chatsMuted;\n\t\tmarks -= other.marks;\n\t\tmarksMuted -= other.marksMuted;\n\t\treactions -= other.reactions;\n\t\treactionsMuted -= other.reactionsMuted;\n\t\tmentions -= other.mentions;\n\t\tpolls -= other.polls;\n\t\tpollsMuted -= other.pollsMuted;\n\t\treturn *this;\n\t}\n};\n\ninline UnreadState operator+(const UnreadState &a, const UnreadState &b) {\n\tauto result = a;\n\tresult += b;\n\treturn result;\n}\n\ninline UnreadState operator-(const UnreadState &a, const UnreadState &b) {\n\tauto result = a;\n\tresult -= b;\n\treturn result;\n}\n\n#ifdef _DEBUG\ninline QDebug operator<<(QDebug debug, const UnreadState &state) {\n\treturn debug.nospace() << \"UnreadState(messages:\" << state.messages\n\t<< \", messagesMuted:\" << state.messagesMuted\n\t<< \", chats:\" << state.chats\n\t<< \", chatsMuted:\" << state.chatsMuted\n\t<< \", marks:\" << state.marks\n\t<< \", marksMuted:\" << state.marksMuted\n\t<< \", reactions:\" << state.reactions\n\t<< \", reactionsMuted:\" << state.reactionsMuted\n\t<< \", mentions:\" << state.mentions\n\t<< \", polls:\" << state.polls\n\t<< \", pollsMuted:\" << state.pollsMuted\n\t<< \", known:\" << state.known << \")\";\n}\n#endif // _DEBUG\n\nstruct BadgesState {\n\tint unreadCounter = 0;\n\tbool unread : 1 = false;\n\tbool unreadMuted : 1 = false;\n\tbool mention : 1 = false;\n\tbool mentionMuted : 1 = false;\n\tbool reaction : 1 = false;\n\tbool reactionMuted : 1 = false;\n\tbool poll : 1 = false;\n\tbool pollMuted : 1 = false;\n\n\tfriend inline constexpr auto operator<=>(\n\t\tBadgesState,\n\t\tBadgesState) = default;\n\tfriend inline constexpr bool operator==(\n\t\tBadgesState,\n\t\tBadgesState) = default;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn !unread && !mention && !reaction && !poll;\n\t}\n};\n\nenum class CountInBadge : uchar {\n\tDefault,\n\tChats,\n\tMessages,\n};\n\nenum class IncludeInBadge : uchar {\n\tDefault,\n\tUnmuted,\n\tAll,\n\tUnmutedOrAll,\n};\n\nstruct RowsByLetter {\n\tnot_null<Row*> main;\n\tbase::flat_map<QChar, not_null<Row*>> letters;\n};\n\nstruct RightButton final {\n\tconst style::DialogRightButton *st = nullptr;\n\tQImage bg;\n\tQImage selectedBg;\n\tQImage activeBg;\n\tUi::Text::String text;\n\tstd::unique_ptr<Ui::RippleAnimation> ripple;\n\n\texplicit operator bool() const {\n\t\treturn st != nullptr;\n\t}\n};\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_entry.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/dialogs_entry.h\"\n\n#include \"dialogs/dialogs_key.h\"\n#include \"dialogs/dialogs_indexed_list.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_session.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"mainwidget.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/ui_utility.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"styles/style_dialogs.h\" // st::dialogsTextWidthMin\n\n#include \"core/fork_settings.h\"\n\nnamespace Dialogs {\nnamespace {\n\nauto DialogsPosToTopShift = 0;\n\nuint64 DialogPosFromDate(TimeId date) {\n\tif (!date) {\n\t\treturn 0;\n\t}\n\treturn (uint64(date) << 32) | (++DialogsPosToTopShift);\n}\n\nuint64 FixedOnTopDialogPos(int index) {\n\treturn 0xFFFFFFFFFFFF000FULL - index;\n}\n\nuint64 PinnedDialogPos(int pinnedIndex) {\n\treturn 0xFFFFFFFF000000FFULL - pinnedIndex;\n}\n\n} // namespace\n\nBadgesState BadgesForUnread(\n\t\tconst UnreadState &state,\n\t\tCountInBadge count,\n\t\tIncludeInBadge include) {\n\tconst auto countMessages = (count == CountInBadge::Messages)\n\t\t|| ((count == CountInBadge::Default)\n\t\t\t&& Core::App().settings().countUnreadMessages());\n\tconst auto counterFull = state.marks\n\t\t+ (countMessages ? state.messages : state.chats);\n\tconst auto counterMuted = state.marksMuted\n\t\t+ (countMessages ? state.messagesMuted : state.chatsMuted);\n\tconst auto unreadMuted = (counterFull <= counterMuted);\n\n\tconst auto includeMuted = (include == IncludeInBadge::All)\n\t\t|| (include == IncludeInBadge::UnmutedOrAll && unreadMuted)\n\t\t|| ((include == IncludeInBadge::Default)\n\t\t\t&& Core::App().settings().includeMutedCounter());\n\n\tconst auto marks = state.marks - (includeMuted ? 0 : state.marksMuted);\n\tconst auto counter = counterFull - (includeMuted ? 0 : counterMuted);\n\tconst auto mark = (counter == 1) && (marks == 1);\n\treturn {\n\t\t.unreadCounter = mark ? 0 : counter,\n\t\t.unread = (counter > 0),\n\t\t.unreadMuted = includeMuted && (counter <= counterMuted),\n\t\t.mention = (state.mentions > 0),\n\t\t.reaction = (state.reactions > 0),\n\t\t.reactionMuted = (state.reactions <= state.reactionsMuted),\n\t\t.poll = (state.polls > 0),\n\t\t.pollMuted = (state.polls <= state.pollsMuted),\n\t};\n}\n\nEntry::Entry(not_null<Data::Session*> owner, Type type)\n: _owner(owner)\n, _flags((type == Type::History)\n\t? (Flag::IsThread | Flag::IsHistory)\n\t: (type == Type::ForumTopic)\n\t? (Flag::IsThread | Flag::IsForumTopic)\n\t: (type == Type::SavedSublist)\n\t? (Flag::IsThread | Flag::IsSavedSublist)\n\t: Flag(0)) {\n}\n\nEntry::~Entry() = default;\n\nData::Session &Entry::owner() const {\n\treturn *_owner;\n}\n\nMain::Session &Entry::session() const {\n\treturn _owner->session();\n}\n\nHistory *Entry::asHistory() {\n\treturn (_flags & Flag::IsHistory)\n\t\t? static_cast<History*>(this)\n\t\t: nullptr;\n}\n\nData::Forum *Entry::asForum() {\n\treturn (_flags & Flag::IsHistory)\n\t\t? static_cast<History*>(this)->peer->forum()\n\t\t: nullptr;\n}\n\nData::Folder *Entry::asFolder() {\n\treturn (_flags & Flag::IsThread)\n\t\t? nullptr\n\t\t: static_cast<Data::Folder*>(this);\n}\n\nData::Thread *Entry::asThread() {\n\treturn (_flags & Flag::IsThread)\n\t\t? static_cast<Data::Thread*>(this)\n\t\t: nullptr;\n}\n\nData::ForumTopic *Entry::asTopic() {\n\treturn (_flags & Flag::IsForumTopic)\n\t\t? static_cast<Data::ForumTopic*>(this)\n\t\t: nullptr;\n}\n\nData::SavedSublist *Entry::asSublist() {\n\treturn (_flags & Flag::IsSavedSublist)\n\t\t? static_cast<Data::SavedSublist*>(this)\n\t\t: nullptr;\n}\n\nconst History *Entry::asHistory() const {\n\treturn const_cast<Entry*>(this)->asHistory();\n}\n\nconst Data::Forum *Entry::asForum() const {\n\treturn const_cast<Entry*>(this)->asForum();\n}\n\nconst Data::Folder *Entry::asFolder() const {\n\treturn const_cast<Entry*>(this)->asFolder();\n}\n\nconst Data::Thread *Entry::asThread() const {\n\treturn const_cast<Entry*>(this)->asThread();\n}\n\nconst Data::ForumTopic *Entry::asTopic() const {\n\treturn const_cast<Entry*>(this)->asTopic();\n}\n\nconst Data::SavedSublist *Entry::asSublist() const {\n\treturn const_cast<Entry*>(this)->asSublist();\n}\n\nvoid Entry::pinnedIndexChanged(FilterId filterId, int was, int now) {\n\tif (!filterId && session().supportMode()) {\n\t\t// Force reorder in support mode.\n\t\t_sortKeyInChatList = 0;\n\t}\n\tupdateChatListSortPosition();\n\tupdateChatListEntry();\n\tif ((was != 0) != (now != 0)) {\n\t\tchangedChatListPinHook();\n\t}\n}\n\nvoid Entry::cachePinnedIndex(FilterId filterId, int index) {\n\tconst auto i = _pinnedIndex.find(filterId);\n\tconst auto was = (i != end(_pinnedIndex)) ? i->second : 0;\n\tif (index == was) {\n\t\treturn;\n\t}\n\tif (!index) {\n\t\t_pinnedIndex.erase(i);\n\t} else if (!was) {\n\t\t_pinnedIndex.emplace(filterId, index);\n\t} else {\n\t\ti->second = index;\n\t}\n\tpinnedIndexChanged(filterId, was, index);\n}\n\nbool Entry::needUpdateInChatList() const {\n\treturn inChatList() || shouldBeInChatList();\n}\n\nvoid Entry::updateChatListSortPosition() {\n\tif (session().supportMode()\n\t\t&& _sortKeyInChatList != 0\n\t\t&& session().settings().supportFixChatsOrder()) {\n\t\tupdateChatListEntry();\n\t\treturn;\n\t}\n\t_sortKeyByDate = DialogPosFromDate(adjustedChatListTimeId());\n\tconst auto fixedIndex = fixedOnTopIndex();\n\t_sortKeyInChatList = fixedIndex\n\t\t? FixedOnTopDialogPos(fixedIndex)\n\t\t: computeSortPosition(0);\n\tif (needUpdateInChatList()) {\n\t\tsetChatListExistence(true);\n\t} else {\n\t\t_sortKeyInChatList = _sortKeyByDate = 0;\n\t}\n}\n\nint Entry::lookupPinnedIndex(FilterId filterId) const {\n\tif (filterId) {\n\t\tconst auto i = _pinnedIndex.find(filterId);\n\t\treturn (i != end(_pinnedIndex)) ? i->second : 0;\n\t} else if (!_pinnedIndex.empty()) {\n\t\treturn _pinnedIndex.front().first\n\t\t\t? 0\n\t\t\t: _pinnedIndex.front().second;\n\t}\n\treturn 0;\n}\n\nuint64 Entry::computeSortPosition(FilterId filterId) const {\n\tconst auto index = lookupPinnedIndex(filterId);\n\tif (!index && Core::ForkSettings::PrimaryUnmutedMessages()) {\n\t\tif (const auto history = asHistory()) {\n\t\t\tconst auto muted = history->muted();\n\t\t\tconst auto unreadCount = history->unreadCount();\n\t\t\tif (!history->isForum() && !muted && unreadCount > 0) {\n\t\t\t\treturn 0xFFFFFFFF000000FFULL - 30;\n\t\t\t}\n\t\t}\n\t}\n\treturn index ? PinnedDialogPos(index) : _sortKeyByDate;\n}\n\nvoid Entry::updateChatListExistence() {\n\tif (const auto history = asHistory()) {\n\t\tif (history->peer->asMonoforum()) {\n\t\t\tif (!folderKnown()) {\n\t\t\t\thistory->clearFolder();\n\t\t\t}\n\t\t}\n\t}\n\tsetChatListExistence(shouldBeInChatList());\n}\n\nvoid Entry::notifyUnreadStateChange(const UnreadState &wasState) {\n\tExpects(folderKnown());\n\tExpects(inChatList());\n\n\tconst auto nowState = chatListUnreadState();\n\towner().chatsListFor(this)->unreadStateChanged(wasState, nowState);\n\tauto &filters = owner().chatsFilters();\n\tfor (const auto &[filterId, links] : _chatListLinks) {\n\t\tif (filterId) {\n\t\t\tfilters.chatsList(filterId)->unreadStateChanged(\n\t\t\t\twasState,\n\t\t\t\tnowState);\n\t\t}\n\t}\n\tif (const auto history = asHistory()) {\n\t\tsession().changes().historyUpdated(\n\t\t\thistory,\n\t\t\tData::HistoryUpdate::Flag::UnreadView);\n\t\tconst auto isForFilters = [](UnreadState state) {\n\t\t\treturn state.messages || state.marks || state.mentions;\n\t\t};\n\t\tif (isForFilters(wasState) != isForFilters(nowState)) {\n\t\t\tconst auto wasTags = _tagColors.size();\n\t\t\towner().chatsFilters().refreshHistory(history);\n\n\t\t\t// Hack for History::fakeUnreadWhileOpened().\n\t\t\tif (!isForFilters(nowState)\n\t\t\t\t&& (wasTags > 0)\n\t\t\t\t&& (wasTags == _tagColors.size())) {\n\t\t\t\tauto updateRequested = false;\n\t\t\t\tfor (const auto &filter : filters.list()) {\n\t\t\t\t\tif (!(filter.flags() & Data::ChatFilter::Flag::NoRead)\n\t\t\t\t\t\t|| !_chatListLinks.contains(filter.id())\n\t\t\t\t\t\t|| filter.contains(history, true)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst auto wasTagsCount = _tagColors.size();\n\t\t\t\t\tsetColorIndexForFilterId(filter.id(), std::nullopt);\n\t\t\t\t\tupdateRequested |= (wasTagsCount != _tagColors.size());\n\t\t\t\t}\n\t\t\t\tif (updateRequested) {\n\t\t\t\t\tupdateChatListEntryHeight();\n\t\t\t\t\tsession().changes().peerUpdated(\n\t\t\t\t\t\thistory->peer,\n\t\t\t\t\t\tData::PeerUpdate::Flag::Name);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if (const auto sublist = asSublist()) {\n\t\tsession().changes().sublistUpdated(\n\t\t\tsublist,\n\t\t\tData::SublistUpdate::Flag::UnreadView);\n\t}\n\tupdateChatListEntryPostponed();\n}\n\nconst Ui::Text::String &Entry::chatListNameText() const {\n\tconst auto version = chatListNameVersion();\n\tif (_chatListNameVersion < version) {\n\t\t_chatListNameVersion = version;\n\t\t_chatListNameText.setText(\n\t\t\tst::semiboldTextStyle,\n\t\t\tchatListName(),\n\t\t\tUi::NameTextOptions());\n\t}\n\treturn _chatListNameText;\n}\n\nvoid Entry::setChatListExistence(bool exists) {\n\tif (exists && _sortKeyInChatList) {\n\t\towner().refreshChatListEntry(this);\n\t\tupdateChatListEntry();\n\t} else {\n\t\towner().removeChatListEntry(this);\n\t}\n}\n\nTimeId Entry::adjustedChatListTimeId() const {\n\treturn chatListTimeId();\n}\n\nvoid Entry::changedChatListPinHook() {\n}\n\nRowsByLetter *Entry::chatListLinks(FilterId filterId) {\n\tconst auto i = _chatListLinks.find(filterId);\n\treturn (i != end(_chatListLinks)) ? &i->second : nullptr;\n}\n\nconst RowsByLetter *Entry::chatListLinks(FilterId filterId) const {\n\tconst auto i = _chatListLinks.find(filterId);\n\treturn (i != end(_chatListLinks)) ? &i->second : nullptr;\n}\n\nnot_null<Row*> Entry::mainChatListLink(FilterId filterId) const {\n\tconst auto links = chatListLinks(filterId);\n\tAssert(links != nullptr);\n\treturn links->main;\n}\n\nRow *Entry::maybeMainChatListLink(FilterId filterId) const {\n\tconst auto links = chatListLinks(filterId);\n\treturn links ? links->main.get() : nullptr;\n}\n\nPositionChange Entry::adjustByPosInChatList(\n\t\tFilterId filterId,\n\t\tnot_null<MainList*> list) {\n\tconst auto links = chatListLinks(filterId);\n\tAssert(links != nullptr);\n\tconst auto from = links->main->top();\n\tlist->indexed()->adjustByDate(*links);\n\tconst auto to = links->main->top();\n\treturn { .from = from, .to = to, .height = links->main->height() };\n}\n\nvoid Entry::setChatListTimeId(TimeId date) {\n\t_timeId = date;\n\tupdateChatListSortPosition();\n\tif (const auto folder = this->folder()) {\n\t\tfolder->updateChatListSortPosition();\n\t}\n}\n\nint Entry::posInChatList(FilterId filterId) const {\n\treturn mainChatListLink(filterId)->index();\n}\n\nvoid Entry::setColorIndexForFilterId(\n\t\tFilterId filterId,\n\t\tstd::optional<uint8> colorIndex) {\n\tif (!filterId) {\n\t\treturn;\n\t}\n\tif (colorIndex) {\n\t\t_tagColors[filterId] = *colorIndex;\n\t} else {\n\t\t_tagColors.remove(filterId);\n\t}\n}\n\nnot_null<Row*> Entry::addToChatList(\n\t\tFilterId filterId,\n\t\tnot_null<MainList*> list) {\n\tif (filterId) {\n\t\tconst auto &list = owner().chatsFilters().list();\n\t\tconst auto it = ranges::find(list, filterId, &Data::ChatFilter::id);\n\t\tif (it != end(list)) {\n\t\t\tsetColorIndexForFilterId(filterId, it->colorIndex());\n\t\t}\n\t}\n\tif (const auto main = maybeMainChatListLink(filterId)) {\n\t\treturn main;\n\t}\n\treturn _chatListLinks.emplace(\n\t\tfilterId,\n\t\tlist->addEntry(this)\n\t).first->second.main;\n}\n\nvoid Entry::removeFromChatList(\n\t\tFilterId filterId,\n\t\tnot_null<MainList*> list) {\n\tif (isPinnedDialog(filterId)) {\n\t\towner().setChatPinned(this, filterId, false);\n\t}\n\tif (filterId) {\n\t\tconst auto it = _tagColors.find(filterId);\n\t\tif (it != end(_tagColors)) {\n\t\t\t_tagColors.erase(it);\n\t\t}\n\t}\n\n\tconst auto i = _chatListLinks.find(filterId);\n\tif (i == end(_chatListLinks)) {\n\t\treturn;\n\t}\n\t_chatListLinks.erase(i);\n\tlist->removeEntry(this);\n}\n\nvoid Entry::removeChatListEntryByLetter(FilterId filterId, QChar letter) {\n\tconst auto i = _chatListLinks.find(filterId);\n\tif (i != end(_chatListLinks)) {\n\t\ti->second.letters.remove(letter);\n\t}\n}\n\nvoid Entry::addChatListEntryByLetter(\n\t\tFilterId filterId,\n\t\tQChar letter,\n\t\tnot_null<Row*> row) {\n\tconst auto i = _chatListLinks.find(filterId);\n\tif (i != end(_chatListLinks)) {\n\t\ti->second.letters.emplace(letter, row);\n\t}\n}\n\nvoid Entry::updateChatListEntry() {\n\t_flags &= ~Flag::UpdatePostponed;\n\tsession().changes().entryUpdated(this, Data::EntryUpdate::Flag::Repaint);\n}\n\nvoid Entry::updateChatListEntryPostponed() {\n\tif (_flags & Flag::UpdatePostponed) {\n\t\treturn;\n\t}\n\t_flags |= Flag::UpdatePostponed;\n\tUi::PostponeCall(this, [=] {\n\t\tif (_flags & Flag::UpdatePostponed) {\n\t\t\tupdateChatListEntry();\n\t\t}\n\t});\n}\n\nvoid Entry::updateChatListEntryHeight() {\n\tsession().changes().entryUpdated(this, Data::EntryUpdate::Flag::Height);\n}\n\nbool Entry::hasChatsFilterTags(FilterId exclude) const {\n\tif (!owner().chatsFilters().tagsEnabled()) {\n\t\treturn false;\n\t}\n\tif (exclude) {\n\t\tif (_tagColors.size() == 1) {\n\t\t\tif (_tagColors.begin()->first == exclude) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\treturn !_tagColors.empty();\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_entry.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flat_map.h\"\n#include \"base/weak_ptr.h\"\n#include \"base/flags.h\"\n#include \"dialogs/dialogs_common.h\"\n#include \"ui/unread_badge.h\"\n\nclass HistoryItem;\nclass History;\nclass UserData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\nclass Session;\nclass Forum;\nclass Folder;\nclass ForumTopic;\nclass SavedSublist;\nclass SavedMessages;\nclass Thread;\n} // namespace Data\n\nnamespace Ui {\nstruct PeerUserpicView;\n} // namespace Ui\n\nnamespace Dialogs::Ui {\nusing namespace ::Ui;\nstruct PaintContext;\n} // namespace Dialogs::Ui\n\nnamespace Dialogs {\n\nstruct UnreadState;\nclass Row;\nclass IndexedList;\nclass MainList;\n\n[[nodiscard]] BadgesState BadgesForUnread(\n\tconst UnreadState &state,\n\tCountInBadge count = CountInBadge::Default,\n\tIncludeInBadge include = IncludeInBadge::Default);\n\nclass Entry : public base::has_weak_ptr {\npublic:\n\tenum class Type : uchar {\n\t\tHistory,\n\t\tFolder,\n\t\tForumTopic,\n\t\tSavedSublist,\n\t};\n\tEntry(not_null<Data::Session*> owner, Type type);\n\tvirtual ~Entry();\n\n\t[[nodiscard]] Data::Session &owner() const;\n\t[[nodiscard]] Main::Session &session() const;\n\n\tHistory *asHistory();\n\tData::Forum *asForum();\n\tData::Folder *asFolder();\n\tData::Thread *asThread();\n\tData::ForumTopic *asTopic();\n\tData::SavedSublist *asSublist();\n\n\tconst History *asHistory() const;\n\tconst Data::Forum *asForum() const;\n\tconst Data::Folder *asFolder() const;\n\tconst Data::Thread *asThread() const;\n\tconst Data::ForumTopic *asTopic() const;\n\tconst Data::SavedSublist *asSublist() const;\n\n\tPositionChange adjustByPosInChatList(\n\t\tFilterId filterId,\n\t\tnot_null<MainList*> list);\n\t[[nodiscard]] bool inChatList(FilterId filterId = 0) const {\n\t\treturn _chatListLinks.contains(filterId);\n\t}\n\tRowsByLetter *chatListLinks(FilterId filterId);\n\tconst RowsByLetter *chatListLinks(FilterId filterId) const;\n\t[[nodiscard]] int posInChatList(FilterId filterId) const;\n\tnot_null<Row*> addToChatList(\n\t\tFilterId filterId,\n\t\tnot_null<MainList*> list);\n\tvoid setColorIndexForFilterId(FilterId, std::optional<uint8>);\n\tvoid removeFromChatList(\n\t\tFilterId filterId,\n\t\tnot_null<MainList*> list);\n\tvoid removeChatListEntryByLetter(FilterId filterId, QChar letter);\n\tvoid addChatListEntryByLetter(\n\t\tFilterId filterId,\n\t\tQChar letter,\n\t\tnot_null<Row*> row);\n\tvoid updateChatListEntry();\n\tvoid updateChatListEntryPostponed();\n\tvoid updateChatListEntryHeight();\n\t[[nodiscard]] bool isPinnedDialog(FilterId filterId) const {\n\t\treturn lookupPinnedIndex(filterId) != 0;\n\t}\n\tvoid cachePinnedIndex(FilterId filterId, int index);\n\t[[nodiscard]] uint64 sortKeyInChatList(FilterId filterId) const {\n\t\treturn filterId\n\t\t\t? computeSortPosition(filterId)\n\t\t\t: _sortKeyInChatList;\n\t}\n\tvoid updateChatListSortPosition();\n\tvoid setChatListTimeId(TimeId date);\n\tvirtual void updateChatListExistence();\n\tbool needUpdateInChatList() const;\n\t[[nodiscard]] virtual TimeId adjustedChatListTimeId() const;\n\n\t[[nodiscard]] virtual int fixedOnTopIndex() const = 0;\n\tstatic constexpr auto kArchiveFixOnTopIndex = 1;\n\tstatic constexpr auto kTopPromotionFixOnTopIndex = 2;\n\n\t[[nodiscard]] virtual bool shouldBeInChatList() const = 0;\n\t[[nodiscard]] virtual UnreadState chatListUnreadState() const = 0;\n\t[[nodiscard]] virtual BadgesState chatListBadgesState() const = 0;\n\t[[nodiscard]] virtual HistoryItem *chatListMessage() const = 0;\n\t[[nodiscard]] virtual bool chatListMessageKnown() const = 0;\n\t[[nodiscard]] virtual const QString &chatListName() const = 0;\n\t[[nodiscard]] virtual const QString &chatListNameSortKey() const = 0;\n\t[[nodiscard]] virtual int chatListNameVersion() const = 0;\n\t[[nodiscard]] virtual auto chatListNameWords() const\n\t\t-> const base::flat_set<QString> & = 0;\n\t[[nodiscard]] virtual auto chatListFirstLetters() const\n\t\t-> const base::flat_set<QChar> & = 0;\n\n\t[[nodiscard]] virtual bool folderKnown() const {\n\t\treturn true;\n\t}\n\t[[nodiscard]] virtual Data::Folder *folder() const {\n\t\treturn nullptr;\n\t}\n\n\tvirtual void chatListPreloadData() = 0;\n\tvirtual void paintUserpic(\n\t\tPainter &p,\n\t\tUi::PeerUserpicView &view,\n\t\tconst Ui::PaintContext &context) const = 0;\n\n\t[[nodiscard]] TimeId chatListTimeId() const {\n\t\treturn _timeId;\n\t}\n\n\t[[nodiscard]] const Ui::Text::String &chatListNameText() const;\n\t[[nodiscard]] Ui::PeerBadge &chatListPeerBadge() const {\n\t\treturn _chatListPeerBadge;\n\t}\n\n\t[[nodiscard]] bool hasChatsFilterTags(FilterId exclude) const;\nprotected:\n\tvoid notifyUnreadStateChange(const UnreadState &wasState);\n\tinline auto unreadStateChangeNotifier(bool required);\n\n\t[[nodiscard]] int lookupPinnedIndex(FilterId filterId) const;\n\nprivate:\n\tenum class Flag : uchar {\n\t\tIsThread = (1 << 0),\n\t\tIsHistory = (1 << 1),\n\t\tIsForumTopic = (1 << 2),\n\t\tIsSavedSublist = (1 << 3),\n\t\tUpdatePostponed = (1 << 4),\n\t\tInUnreadChangeBlock = (1 << 5),\n\t};\n\tfriend inline constexpr bool is_flag_type(Flag) { return true; }\n\tusing Flags = base::flags<Flag>;\n\n\tvirtual void changedChatListPinHook();\n\tvoid pinnedIndexChanged(FilterId filterId, int was, int now);\n\t[[nodiscard]] uint64 computeSortPosition(FilterId filterId) const;\n\n\tvoid setChatListExistence(bool exists);\n\tnot_null<Row*> mainChatListLink(FilterId filterId) const;\n\tRow *maybeMainChatListLink(FilterId filterId) const;\n\n\tconst not_null<Data::Session*> _owner;\n\tbase::flat_map<FilterId, RowsByLetter> _chatListLinks;\n\tuint64 _sortKeyInChatList = 0;\n\tuint64 _sortKeyByDate = 0;\n\tbase::flat_map<FilterId, int> _pinnedIndex;\n\tbase::flat_map<FilterId, uint8> _tagColors;\n\tmutable Ui::PeerBadge _chatListPeerBadge;\n\tmutable Ui::Text::String _chatListNameText;\n\tmutable int _chatListNameVersion = 0;\n\tTimeId _timeId = 0;\n\tFlags _flags;\n\n};\n\nauto Entry::unreadStateChangeNotifier(bool required) {\n\tExpects(!(_flags & Flag::InUnreadChangeBlock));\n\n\t_flags |= Flag::InUnreadChangeBlock;\n\tconst auto notify = required && inChatList();\n\tconst auto wasState = notify ? chatListUnreadState() : UnreadState();\n\treturn gsl::finally([=, this] {\n\t\t_flags &= ~Flag::InUnreadChangeBlock;\n\t\tif (notify) {\n\t\t\tAssert(inChatList());\n\t\t\tnotifyUnreadStateChange(wasState);\n\t\t}\n\t});\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_indexed_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/dialogs_indexed_list.h\"\n\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n\nnamespace Dialogs {\n\nIndexedList::IndexedList(SortMode sortMode, FilterId filterId)\n: _sortMode(sortMode)\n, _filterId(filterId)\n, _list(sortMode, filterId)\n, _empty(sortMode, filterId) {\n}\n\nRowsByLetter IndexedList::addToEnd(Key key) {\n\tif (const auto row = _list.getRow(key)) {\n\t\treturn { row };\n\t}\n\n\tauto result = RowsByLetter{ _list.addToEnd(key) };\n\tfor (const auto &ch : key.entry()->chatListFirstLetters()) {\n\t\tauto j = _index.find(ch);\n\t\tif (j == _index.cend()) {\n\t\t\tj = _index.emplace(ch, _sortMode, _filterId).first;\n\t\t}\n\t\tresult.letters.emplace(ch, j->second.addToEnd(key));\n\t}\n\treturn result;\n}\n\nRow *IndexedList::addByName(Key key) {\n\tif (const auto row = _list.getRow(key)) {\n\t\treturn row;\n\t}\n\n\tconst auto result = _list.addByName(key);\n\tfor (const auto &ch : key.entry()->chatListFirstLetters()) {\n\t\tauto j = _index.find(ch);\n\t\tif (j == _index.cend()) {\n\t\t\tj = _index.emplace(ch, _sortMode, _filterId).first;\n\t\t}\n\t\tj->second.addByName(key);\n\t}\n\treturn result;\n}\n\nvoid IndexedList::adjustByDate(const RowsByLetter &links) {\n\t_list.adjustByDate(links.main);\n\tfor (const auto &[ch, row] : links.letters) {\n\t\tif (auto it = _index.find(ch); it != _index.cend()) {\n\t\t\tit->second.adjustByDate(row);\n\t\t}\n\t}\n}\n\nbool IndexedList::updateHeights(float64 narrowRatio) {\n\treturn _list.updateHeights(narrowRatio);\n}\n\nbool IndexedList::updateHeight(Key key, float64 narrowRatio) {\n\treturn _list.updateHeight(key, narrowRatio);\n}\n\nvoid IndexedList::moveToTop(Key key) {\n\tif (_list.moveToTop(key)) {\n\t\tfor (const auto &ch : key.entry()->chatListFirstLetters()) {\n\t\t\tif (auto it = _index.find(ch); it != _index.cend()) {\n\t\t\t\tit->second.moveToTop(key);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid IndexedList::movePinned(Row *row, int deltaSign) {\n\tauto swapPinnedIndexWith = find(row);\n\tAssert(swapPinnedIndexWith != cend());\n\tif (deltaSign > 0) {\n\t\t++swapPinnedIndexWith;\n\t} else {\n\t\tAssert(swapPinnedIndexWith != cbegin());\n\t\t--swapPinnedIndexWith;\n\t}\n\trow->key().entry()->owner().reorderTwoPinnedChats(\n\t\t_filterId,\n\t\trow->key(),\n\t\t(*swapPinnedIndexWith)->key());\n}\n\nvoid IndexedList::peerNameChanged(\n\t\tnot_null<PeerData*> peer,\n\t\tconst base::flat_set<QChar> &oldLetters) {\n\tExpects(_sortMode != SortMode::Date);\n\n\tif (const auto history = peer->owner().historyLoaded(peer)) {\n\t\tif (_sortMode == SortMode::Name) {\n\t\t\tadjustByName(history, oldLetters);\n\t\t} else {\n\t\t\tadjustNames(FilterId(), history, oldLetters);\n\t\t}\n\t}\n}\n\nvoid IndexedList::peerNameChanged(\n\t\tFilterId filterId,\n\t\tnot_null<PeerData*> peer,\n\t\tconst base::flat_set<QChar> &oldLetters) {\n\tExpects(_sortMode == SortMode::Date);\n\n\tif (const auto history = peer->owner().historyLoaded(peer)) {\n\t\tadjustNames(filterId, history, oldLetters);\n\t}\n}\n\nvoid IndexedList::adjustByName(\n\t\tKey key,\n\t\tconst base::flat_set<QChar> &oldLetters) {\n\tExpects(_sortMode == SortMode::Name);\n\n\tconst auto mainRow = _list.adjustByName(key);\n\tif (!mainRow) return;\n\n\tauto toRemove = oldLetters;\n\tauto toAdd = base::flat_set<QChar>();\n\tfor (const auto &ch : key.entry()->chatListFirstLetters()) {\n\t\tauto j = toRemove.find(ch);\n\t\tif (j == toRemove.cend()) {\n\t\t\ttoAdd.insert(ch);\n\t\t} else {\n\t\t\ttoRemove.erase(j);\n\t\t\tif (auto it = _index.find(ch); it != _index.cend()) {\n\t\t\t\tit->second.adjustByName(key);\n\t\t\t}\n\t\t}\n\t}\n\tfor (auto ch : toRemove) {\n\t\tif (auto it = _index.find(ch); it != _index.cend()) {\n\t\t\tit->second.remove(key, mainRow);\n\t\t}\n\t}\n\tif (!toAdd.empty()) {\n\t\tfor (auto ch : toAdd) {\n\t\t\tauto j = _index.find(ch);\n\t\t\tif (j == _index.cend()) {\n\t\t\t\tj = _index.emplace(ch, _sortMode, _filterId).first;\n\t\t\t}\n\t\t\tj->second.addByName(key);\n\t\t}\n\t}\n}\n\nvoid IndexedList::adjustNames(\n\t\tFilterId filterId,\n\t\tnot_null<History*> history,\n\t\tconst base::flat_set<QChar> &oldLetters) {\n\tconst auto key = Dialogs::Key(history);\n\tauto mainRow = _list.getRow(key);\n\tif (!mainRow) return;\n\n\tauto toRemove = oldLetters;\n\tauto toAdd = base::flat_set<QChar>();\n\tfor (const auto &ch : key.entry()->chatListFirstLetters()) {\n\t\tauto j = toRemove.find(ch);\n\t\tif (j == toRemove.cend()) {\n\t\t\ttoAdd.insert(ch);\n\t\t} else {\n\t\t\ttoRemove.erase(j);\n\t\t}\n\t}\n\tfor (auto ch : toRemove) {\n\t\tif (_sortMode == SortMode::Date) {\n\t\t\thistory->removeChatListEntryByLetter(filterId, ch);\n\t\t}\n\t\tif (auto it = _index.find(ch); it != _index.cend()) {\n\t\t\tit->second.remove(key, mainRow);\n\t\t}\n\t}\n\tfor (auto ch : toAdd) {\n\t\tauto j = _index.find(ch);\n\t\tif (j == _index.cend()) {\n\t\t\tj = _index.emplace(ch, _sortMode, _filterId).first;\n\t\t}\n\t\tauto row = j->second.addToEnd(key);\n\t\tif (_sortMode == SortMode::Date) {\n\t\t\thistory->addChatListEntryByLetter(filterId, ch, row);\n\t\t}\n\t}\n}\n\nvoid IndexedList::freeze() {\n\t_list.freeze();\n\tfor (auto &[ch, list] : _index) {\n\t\tlist.freeze();\n\t}\n}\n\nvoid IndexedList::unfreeze() {\n\t_list.unfreeze();\n\tfor (auto &[ch, list] : _index) {\n\t\tlist.unfreeze();\n\t}\n}\n\nvoid IndexedList::remove(Key key, Row *replacedBy) {\n\tif (_list.remove(key, replacedBy)) {\n\t\tfor (const auto &ch : key.entry()->chatListFirstLetters()) {\n\t\t\tif (const auto it = _index.find(ch); it != _index.cend()) {\n\t\t\t\tit->second.remove(key, replacedBy);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid IndexedList::clear() {\n\t_list.clear();\n\t_index.clear();\n}\n\nstd::vector<not_null<Row*>> IndexedList::filtered(\n\t\tconst QStringList &words) const {\n\tconst auto minimal = [&]() -> const Dialogs::List* {\n\t\tif (empty()) {\n\t\t\treturn nullptr;\n\t\t}\n\t\tauto result = (const Dialogs::List*)nullptr;\n\t\tfor (const auto &word : words) {\n\t\t\tif (word.isEmpty()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto found = filtered(word[0]);\n\t\t\tif (!found || found->empty()) {\n\t\t\t\treturn nullptr;\n\t\t\t} else if (!result || result->size() > found->size()) {\n\t\t\t\tresult = found;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}();\n\tauto result = std::vector<not_null<Row*>>();\n\tif (!minimal || minimal->empty()) {\n\t\treturn result;\n\t}\n\tresult.reserve(minimal->size());\n\tfor (const auto &row : *minimal) {\n\t\tconst auto &nameWords = row->entry()->chatListNameWords();\n\t\tconst auto found = [&](const QString &word) {\n\t\t\tfor (const auto &name : nameWords) {\n\t\t\t\tif (name.startsWith(word)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\t\tconst auto allFound = [&] {\n\t\t\tfor (const auto &word : words) {\n\t\t\t\tif (!found(word)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}();\n\t\tif (allFound) {\n\t\t\tresult.push_back(row);\n\t\t}\n\t}\n\treturn result;\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_indexed_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"dialogs/dialogs_list.h\"\n\nclass History;\n\nnamespace Dialogs {\n\nstruct RowsByLetter;\nclass Row;\n\nclass IndexedList {\npublic:\n\tIndexedList(SortMode sortMode, FilterId filterId = 0);\n\n\tRowsByLetter addToEnd(Key key);\n\tRow *addByName(Key key);\n\tvoid adjustByDate(const RowsByLetter &links);\n\tvoid moveToTop(Key key);\n\tvoid freeze();\n\tvoid unfreeze();\n\tbool updateHeight(Key key, float64 narrowRatio);\n\tbool updateHeights(float64 narrowRatio);\n\n\t// row must belong to this indexed list all().\n\tvoid movePinned(Row *row, int deltaSign);\n\n\t// For sortMode != SortMode::Date && != Complex\n\tvoid peerNameChanged(\n\t\tnot_null<PeerData*> peer,\n\t\tconst base::flat_set<QChar> &oldChars);\n\n\t//For sortMode == SortMode::Date || == Complex\n\tvoid peerNameChanged(\n\t\tFilterId filterId,\n\t\tnot_null<PeerData*> peer,\n\t\tconst base::flat_set<QChar> &oldChars);\n\n\tvoid remove(Key key, Row *replacedBy = nullptr);\n\tvoid clear();\n\n\t[[nodiscard]] const List &all() const {\n\t\treturn _list;\n\t}\n\t[[nodiscard]] const List *filtered(QChar ch) const {\n\t\tconst auto i = _index.find(ch);\n\t\treturn (i != _index.end()) ? &i->second : nullptr;\n\t}\n\t[[nodiscard]] std::vector<not_null<Row*>> filtered(\n\t\tconst QStringList &words) const;\n\n\t// Part of List interface is duplicated here for all() list.\n\t[[nodiscard]] int size() const { return all().size(); }\n\t[[nodiscard]] bool empty() const { return all().empty(); }\n\t[[nodiscard]] int height() const { return all().height(); }\n\t[[nodiscard]] bool contains(Key key) const {\n\t\treturn all().contains(key);\n\t}\n\t[[nodiscard]] Row *getRow(Key key) const { return all().getRow(key); }\n\t[[nodiscard]] Row *rowAtY(int y) const { return all().rowAtY(y); }\n\n\tusing iterator = List::iterator;\n\tusing const_iterator = List::const_iterator;\n\t[[nodiscard]] const_iterator cbegin() const { return all().cbegin(); }\n\t[[nodiscard]] const_iterator cend() const { return all().cend(); }\n\t[[nodiscard]] const_iterator begin() const { return all().cbegin(); }\n\t[[nodiscard]] const_iterator end() const { return all().cend(); }\n\t[[nodiscard]] iterator begin() { return all().begin(); }\n\t[[nodiscard]] iterator end() { return all().end(); }\n\t[[nodiscard]] const_iterator cfind(Row *value) const {\n\t\treturn all().cfind(value);\n\t}\n\t[[nodiscard]] const_iterator find(Row *value) const {\n\t\treturn all().cfind(value);\n\t}\n\t[[nodiscard]] iterator find(Row *value) { return all().find(value); }\n\t[[nodiscard]] const_iterator findByY(int y) const {\n\t\treturn all().findByY(y);\n\t}\n\t[[nodiscard]] iterator findByY(int y) { return all().findByY(y); }\n\nprivate:\n\tvoid adjustByName(\n\t\tKey key,\n\t\tconst base::flat_set<QChar> &oldChars);\n\tvoid adjustNames(\n\t\tFilterId filterId,\n\t\tnot_null<History*> history,\n\t\tconst base::flat_set<QChar> &oldChars);\n\n\tSortMode _sortMode = SortMode();\n\tFilterId _filterId = 0;\n\tList _list, _empty;\n\tbase::flat_map<QChar, List> _index;\n\n};\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/dialogs_inner_widget.h\"\n\n#include \"dialogs/dialogs_three_state_icon.h\"\n#include \"dialogs/ui/chat_search_empty.h\"\n#include \"dialogs/ui/chat_search_in.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"dialogs/ui/dialogs_video_userpic.h\"\n#include \"dialogs/dialogs_indexed_list.h\"\n#include \"dialogs/dialogs_widget.h\"\n#include \"dialogs/dialogs_search_from_controllers.h\"\n#include \"dialogs/dialogs_search_tags.h\"\n#include \"dialogs/dialogs_quick_action.h\"\n#include \"history/view/history_view_context_menu.h\"\n#include \"history/view/history_view_subsection_tabs.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/shortcuts.h\"\n#include \"core/ui_integration.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"data/components/sponsored_messages.h\"\n#include \"data/data_drafts.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_icons.h\"\n#include \"data/data_session.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_stories.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"data/data_send_action.h\"\n#include \"base/unixtime.h\"\n#include \"base/options.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"settings/settings_common.h\"\n#include \"storage/storage_account.h\"\n#include \"apiwrap.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"menu/menu_sponsored.h\"\n#include \"window/notifications_manager.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_peer_menu.h\"\n#include \"ui/chat/chats_filter_tag.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/loading_element.h\"\n#include \"ui/effects/thanos_effect_session.h\"\n#include \"ui/widgets/multi_select.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/unread_badge.h\"\n#include \"boxes/filters/edit_filter_box.h\"\n#include \"boxes/peers/edit_forum_topic_box.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"api/api_chat_filters.h\"\n#include \"base/qt/qt_common_adapters.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\" // popupMenuExpandedSeparator\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_color_indices.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_media_player.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtWidgets/QApplication>\n#include <QtCore/QMimeData>\n\nnamespace Dialogs {\nnamespace {\n\nconstexpr auto kFreezeTimeout = 2 * crl::time(1000);\nconstexpr auto kHashtagResultsLimit = 5;\nconstexpr auto kStartReorderThreshold = 30;\nconstexpr auto kStartDragToFilterThresholdX = kStartReorderThreshold;\nconstexpr auto kStartDragToFilterThresholdY = 75;\nconstexpr auto kQueryPreviewLimit = 32;\nconstexpr auto kPreviewPostsLimit = 3;\n\n[[nodiscard]] InnerWidget::ChatsFilterTagsKey SerializeFilterTagsKey(\n\t\tFilterId filterId,\n\t\tuint8 more,\n\t\tbool active) {\n\treturn (filterId & 0xFFFFFFFF)\n\t\t| (static_cast<int64_t>(more) << 32)\n\t\t| (static_cast<int64_t>(active) << 40);\n}\n\n[[nodiscard]] int FixedOnTopDialogsCount(not_null<Dialogs::IndexedList*> list) {\n\tauto result = 0;\n\tfor (const auto &row : *list) {\n\t\tif (!row->entry()->fixedOnTopIndex()) {\n\t\t\tbreak;\n\t\t}\n\t\t++result;\n\t}\n\treturn result;\n}\n\n[[nodiscard]] int PinnedDialogsCount(\n\t\tFilterId filterId,\n\t\tnot_null<Dialogs::IndexedList*> list) {\n\tauto result = 0;\n\tfor (const auto &row : *list) {\n\t\tif (row->entry()->fixedOnTopIndex()) {\n\t\t\tcontinue;\n\t\t} else if (!row->entry()->isPinnedDialog(filterId)) {\n\t\t\tbreak;\n\t\t}\n\t\t++result;\n\t}\n\treturn result;\n}\n\n[[nodiscard]] UserData *MaybeBotWithApp(Row *row) {\n\tif (row) {\n\t\tif (const auto history = row->key().history()) {\n\t\t\tif (const auto user = history->peer->asUser()) {\n\t\t\t\tif (user->botInfo && user->botInfo->hasMainApp) {\n\t\t\t\t\tif (!history->unreadCount() && !history->unreadMark()) {\n\t\t\t\t\t\treturn user;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\n[[nodiscard]] object_ptr<SearchEmpty> MakeSearchEmpty(\n\t\tQWidget *parent,\n\t\tSearchState state,\n\t\tFn<void()> resetChatTypeFilter) {\n\tconst auto query = state.query.trimmed();\n\tconst auto hashtag = !query.isEmpty() && (query[0] == '#');\n\tconst auto trimmed = hashtag ? query.mid(1).trimmed() : query;\n\tconst auto fromPeer = (state.tab == ChatSearchTab::MyMessages\n\t\t|| state.tab == ChatSearchTab::PublicPosts\n\t\t|| !state.inChat.peer()\n\t\t|| !(state.inChat.peer()->isChat()\n\t\t\t|| state.inChat.peer()->isMegagroup()))\n\t\t? nullptr\n\t\t: state.fromPeer;\n\tconst auto waiting = trimmed.isEmpty()\n\t\t&& state.tags.empty()\n\t\t&& !fromPeer;\n\tconst auto suggestAllChats = !waiting\n\t\t&& state.tab == ChatSearchTab::MyMessages\n\t\t&& state.filter != ChatTypeFilter::All;\n\tconst auto icon = waiting\n\t\t? SearchEmptyIcon::Search\n\t\t: SearchEmptyIcon::NoResults;\n\tauto text = TextWithEntities();\n\tif (waiting) {\n\t\tif (hashtag) {\n\t\t\ttext.append(tr::lng_search_tab_by_hashtag(tr::now));\n\t\t} else {\n\t\t\ttext.append(tr::lng_dlg_search_for_messages(tr::now));\n\t\t}\n\t} else {\n\t\ttext.append(tr::lng_search_tab_no_results(\n\t\t\ttr::now,\n\t\t\ttr::bold));\n\t\tif (!trimmed.isEmpty()) {\n\t\t\tconst auto preview = (trimmed.size() > kQueryPreviewLimit + 3)\n\t\t\t\t? (trimmed.mid(0, kQueryPreviewLimit) + Ui::kQEllipsis)\n\t\t\t\t: trimmed;\n\t\t\ttext.append(\"\\n\").append(\n\t\t\t\ttr::lng_search_tab_no_results_text(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_query,\n\t\t\t\t\ttrimmed.mid(0, kQueryPreviewLimit)));\n\t\t\tif (suggestAllChats) {\n\t\t\t\ttext.append(\"\\n\\n\").append(\n\t\t\t\t\ttr::link(tr::lng_search_tab_try_in_all(tr::now)));\n\t\t\t} else if (hashtag) {\n\t\t\t\ttext.append(\"\\n\").append(\n\t\t\t\t\ttr::lng_search_tab_no_results_retry(tr::now));\n\t\t\t}\n\t\t}\n\t}\n\tauto result = object_ptr<SearchEmpty>(\n\t\tparent,\n\t\ticon,\n\t\trpl::single(std::move(text)));\n\tif (suggestAllChats) {\n\t\tresult->handlerActivated(\n\t\t) | rpl::on_next(resetChatTypeFilter, result->lifetime());\n\t}\n\tresult->show();\n\tresult->resizeToWidth(parent->width());\n\treturn result;\n}\n\n[[nodiscard]] QString ChatTypeFilterLabel(ChatTypeFilter filter) {\n\tswitch (filter) {\n\tcase ChatTypeFilter::All:\n\t\treturn tr::lng_search_filter_all(tr::now);\n\tcase ChatTypeFilter::Private:\n\t\treturn tr::lng_search_filter_private(tr::now);\n\tcase ChatTypeFilter::Groups:\n\t\treturn tr::lng_search_filter_group(tr::now);\n\tcase ChatTypeFilter::Channels:\n\t\treturn tr::lng_search_filter_channel(tr::now);\n\t}\n\tUnexpected(\"Chat type filter in search results.\");\n}\n\n} // namespace\n\nstruct InnerWidget::CollapsedRow {\n\tCollapsedRow(Data::Folder *folder) : folder(folder) {\n\t}\n\n\tData::Folder *folder = nullptr;\n\tBasicRow row;\n};\n\nstruct InnerWidget::HashtagResult {\n\tHashtagResult(const QString &tag) : tag(tag) {\n\t}\n\tQString tag;\n\tBasicRow row;\n};\n\nstruct InnerWidget::SponsoredSearchResult {\n\tApi::SponsoredSearchResult data;\n\tRightButton button;\n};\n\nstruct InnerWidget::PeerSearchResult {\n\texplicit PeerSearchResult(not_null<PeerData*> peer) : peer(peer) {\n\t}\n\n\tnot_null<PeerData*> peer;\n\tstd::unique_ptr<SponsoredSearchResult> sponsored;\n\tmutable Ui::Text::String name;\n\tmutable Ui::PeerBadge badge;\n\tBasicRow row;\n};\n\nstruct InnerWidget::TagCache {\n\tUi::ChatsFilterTagContext context;\n\tQImage frame;\n};\n\nKey InnerWidget::FilterResult::key() const {\n\treturn row->key();\n}\n\nint InnerWidget::FilterResult::bottom() const {\n\treturn top + row->height();\n}\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\trpl::producer<ChildListShown> childListShown)\n: RpWidget(parent)\n, _controller(controller)\n, _shownList(controller->session().data().chatsList()->indexed())\n, _st(&st::defaultDialogRow)\n, _pinnedShiftAnimation([=](crl::time now) {\n\treturn pinnedShiftAnimationCallback(now);\n})\n, _narrowWidth(st::defaultDialogRow.padding.left()\n\t+ st::defaultDialogRow.photoSize\n\t+ st::defaultDialogRow.padding.left())\n, _childListShown(std::move(childListShown))\n, _freezeTimer([=] { _shownList->unfreeze(); update(); }) {\n\tsetAttribute(Qt::WA_OpaquePaintEvent, true);\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_topicJumpCache = nullptr;\n\t\t_chatsFilterTags.clear();\n\t\t_rightButtons.clear();\n\t\t_pressedRightButtonData = nullptr;\n\t\tfor (const auto &result : _peerSearchResults) {\n\t\t\tif (const auto sponsored = result->sponsored.get()) {\n\t\t\t\tsponsored->button = {};\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\tsession().downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\tCore::App().notifications().settingsChanged(\n\t) | rpl::on_next([=](Window::Notifications::ChangeType change) {\n\t\tif (change == Window::Notifications::ChangeType::CountMessages) {\n\t\t\t// Folder rows change their unread badge with this setting.\n\t\t\tupdate();\n\t\t}\n\t}, lifetime());\n\n\tsession().data().contactsLoaded().changes(\n\t) | rpl::on_next([=] {\n\t\trefresh();\n\t\trefreshEmpty();\n\t}, lifetime());\n\n\tsession().data().itemRemoved(\n\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\titemRemoved(item);\n\t}, lifetime());\n\n\tsession().data().dialogsRowReplacements(\n\t) | rpl::on_next([=](Data::Session::DialogsRowReplacement r) {\n\t\tdialogRowReplaced(r.old, r.now);\n\t}, lifetime());\n\n\tsession().data().sendActionManager().animationUpdated(\n\t) | rpl::on_next([=](\n\t\t\tconst Data::SendActionManager::AnimationUpdate &update) {\n\t\tconst auto updateRect = Ui::RowPainter::SendActionAnimationRect(\n\t\t\tupdate.thread,\n\t\t\t_filterId,\n\t\t\tupdate.rect,\n\t\t\twidth(),\n\t\t\tupdate.textUpdated);\n\t\tupdateDialogRow(\n\t\t\tRowDescriptor(update.thread, FullMsgId()),\n\t\t\tupdateRect,\n\t\t\tUpdateRowSection::Default | UpdateRowSection::Filtered);\n\t}, lifetime());\n\n\tsession().data().sendActionManager().speakingAnimationUpdated(\n\t) | rpl::on_next([=](not_null<History*> history) {\n\t\trepaintDialogRowCornerStatus(history);\n\t}, lifetime());\n\n\tsetupOnlineStatusCheck();\n\n\trpl::merge(\n\t\tsession().data().chatsListChanges(),\n\t\tsession().data().chatsListLoadedEvents()\n\t) | rpl::filter([=](Data::Folder *folder) {\n\t\treturn !_savedSublists\n\t\t\t&& !_openedForum\n\t\t\t&& (folder == _openedFolder);\n\t}) | rpl::on_next([=] {\n\t\trefresh();\n\t}, lifetime());\n\n\tUi::ScheduleThanosEffectWarmUp(&session(), lifetime());\n\n\trpl::merge(\n\t\tsession().settings().archiveCollapsedChanges() | rpl::map_to(false),\n\t\tsession().data().chatsFilters().changed() | rpl::map_to(true),\n\t\tsession().data().chatsFilters().tagsEnabledChanges(\n\t\t) | rpl::map_to(true)\n\t) | rpl::on_next([=](bool refreshHeight) {\n\t\tif (refreshHeight) {\n\t\t\t_chatsFilterTags.clear();\n\t\t\t_shownList->updateHeights(_narrowRatio);\n\t\t}\n\t\trefreshWithCollapsedRows();\n\t}, lifetime());\n\n\tsession().data().chatsFilters().tagsEnabledValue(\n\t) | rpl::on_next([=](bool tags) {\n\t\t_handleChatListEntryTagRefreshesLifetime.destroy();\n\t\tif (!tags) {\n\t\t\treturn;\n\t\t}\n\t\tusing Event = Data::Session::ChatListEntryRefresh;\n\t\tsession().data().chatListEntryRefreshes(\n\t\t) | rpl::filter([=](const Event &event) {\n\t\t\tif (_waitingAllChatListEntryRefreshesForTags) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (event.existenceChanged) {\n\t\t\t\tif (event.key.entry()->inChatList(_filterId)) {\n\t\t\t\t\t_waitingAllChatListEntryRefreshesForTags = true;\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t}) | rpl::on_next([=](const Event &event) {\n\t\t\tUi::PostponeCall(crl::guard(this, [=] {\n\t\t\t\t_waitingAllChatListEntryRefreshesForTags = false;\n\t\t\t\tif (_shownList->updateHeights(_narrowRatio)) {\n\t\t\t\t\trefresh();\n\t\t\t\t}\n\t\t\t}));\n\t\t}, _handleChatListEntryTagRefreshesLifetime);\n\n\t\tsession().data().chatsFilters().tagColorChanged(\n\t\t) | rpl::on_next([=](Data::TagColorChanged data) {\n\t\t\tconst auto filterId = data.filterId;\n\t\t\tconst auto key = SerializeFilterTagsKey(filterId, 0, false);\n\t\t\tconst auto activeKey = SerializeFilterTagsKey(filterId, 0, true);\n\t\t\t{\n\t\t\t\tauto &tags = _chatsFilterTags;\n\t\t\t\tif (const auto it = tags.find(key); it != tags.end()) {\n\t\t\t\t\ttags.erase(it);\n\t\t\t\t}\n\t\t\t\tif (const auto it = tags.find(activeKey); it != tags.end()) {\n\t\t\t\t\ttags.erase(it);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (data.colorExistenceChanged) {\n\t\t\t\tauto &filters = session().data().chatsFilters();\n\t\t\t\tfor (const auto &filter : filters.list()) {\n\t\t\t\t\tif (filter.id() != filterId) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst auto c = filter.colorIndex();\n\t\t\t\t\tconst auto list = filters.chatsList(filterId);\n\t\t\t\t\tfor (const auto &row : list->indexed()->all()) {\n\t\t\t\t\t\trow->entry()->setColorIndexForFilterId(filterId, c);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (_shownList->updateHeights(_narrowRatio)) {\n\t\t\t\t\trefresh();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tupdate();\n\t\t\t}\n\t\t}, _handleChatListEntryTagRefreshesLifetime);\n\t}, lifetime());\n\n\tsession().settings().archiveInMainMenuChanges(\n\t) | rpl::on_next([=] {\n\t\trefresh();\n\t}, lifetime());\n\n\tsession().changes().historyUpdates(\n\t\tData::HistoryUpdate::Flag::IsPinned\n\t\t| Data::HistoryUpdate::Flag::ChatOccupied\n\t) | rpl::on_next([=](const Data::HistoryUpdate &update) {\n\t\tif (update.flags & Data::HistoryUpdate::Flag::IsPinned) {\n\t\t\tstopReorderPinned();\n\t\t}\n\t\tif (update.flags & Data::HistoryUpdate::Flag::ChatOccupied) {\n\t\t\tthis->update();\n\t\t\t_updated.fire({});\n\t\t}\n\t}, lifetime());\n\n\tusing UpdateFlag = Data::PeerUpdate::Flag;\n\tsession().changes().peerUpdates(\n\t\tUpdateFlag::Name\n\t\t| UpdateFlag::Photo\n\t\t| UpdateFlag::IsContact\n\t\t| UpdateFlag::FullInfo\n\t\t| UpdateFlag::EmojiStatus\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tif (update.flags\n\t\t\t& (UpdateFlag::Name\n\t\t\t\t| UpdateFlag::Photo\n\t\t\t\t| UpdateFlag::FullInfo\n\t\t\t\t| UpdateFlag::EmojiStatus)) {\n\t\t\tconst auto peer = update.peer;\n\t\t\tconst auto history = peer->owner().historyLoaded(peer);\n\t\t\tif (_state == WidgetState::Default) {\n\t\t\t\tif (history) {\n\t\t\t\t\tupdateDialogRow({ history, FullMsgId() });\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthis->update();\n\t\t\t}\n\t\t\t_updated.fire({});\n\t\t}\n\t\tif (update.flags & UpdateFlag::IsContact) {\n\t\t\t// contactsNoChatsList could've changed.\n\t\t\tUi::PostponeCall(this, [=] { refresh(); });\n\t\t}\n\t}, lifetime());\n\n\tsession().changes().messageUpdates(\n\t\tData::MessageUpdate::Flag::DialogRowRefresh\n\t) | rpl::on_next([=](const Data::MessageUpdate &update) {\n\t\trefreshDialogRow({ update.item->history(), update.item->fullId() });\n\t}, lifetime());\n\n\tsession().changes().entryUpdates(\n\t\tData::EntryUpdate::Flag::Repaint\n\t\t| Data::EntryUpdate::Flag::Height\n\t) | rpl::on_next([=](const Data::EntryUpdate &update) {\n\t\tconst auto entry = update.entry;\n\t\tif (update.flags & Data::EntryUpdate::Flag::Height) {\n\t\t\tif (updateEntryHeight(entry)) {\n\t\t\t\trefresh();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst auto repaintId = (_state == WidgetState::Default)\n\t\t\t? _filterId\n\t\t\t: 0;\n\t\tif (const auto links = entry->chatListLinks(repaintId)) {\n\t\t\trepaintDialogRow(repaintId, links->main);\n\t\t}\n\t\tif (session().supportMode()\n\t\t\t&& !session().settings().supportAllSearchResults()) {\n\t\t\trepaintDialogRow({ entry, FullMsgId() });\n\t\t}\n\t}, lifetime());\n\n\t_controller->activeChatEntryValue(\n\t) | rpl::combine_previous(\n\t) | rpl::on_next([=](\n\t\t\tRowDescriptor previous,\n\t\t\tRowDescriptor next) {\n\t\tconst auto update = [&](const RowDescriptor &descriptor) {\n\t\t\tconst auto msgId = descriptor.fullId;\n\t\t\tif (const auto topic = descriptor.key.topic()) {\n\t\t\t\tif (_openedForum == topic->forum()) {\n\t\t\t\t\tupdateDialogRow(descriptor);\n\t\t\t\t} else {\n\t\t\t\t\tupdateDialogRow({ { topic->owningHistory() }, msgId });\n\t\t\t\t}\n\t\t\t} else if (const auto sublist = descriptor.key.sublist()) {\n\t\t\t\tif (_savedSublists == sublist->parent()) {\n\t\t\t\t\tupdateDialogRow(descriptor);\n\t\t\t\t} else {\n\t\t\t\t\tupdateDialogRow({ { sublist->owningHistory() }, msgId });\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tupdateDialogRow(descriptor);\n\t\t\t}\n\t\t};\n\t\tupdate(previous);\n\t\tupdate(next);\n\t}, lifetime());\n\n\t_controller->activeChatsFilter(\n\t) | rpl::on_next([=](FilterId filterId) {\n\t\tswitchToFilter(filterId);\n\t}, lifetime());\n\n\t_controller->window().widget()->globalForceClicks(\n\t) | rpl::on_next([=](QPoint globalPosition) {\n\t\tprocessGlobalForceClick(globalPosition);\n\t}, lifetime());\n\n\tsession().data().stories().incrementPreloadingMainSources();\n\n\thandleChatListEntryRefreshes();\n\n\trefreshWithCollapsedRows(true);\n\n\tsetupShortcuts();\n}\n\nbool InnerWidget::updateEntryHeight(not_null<Entry*> entry) {\n\tif (!_geometryInited) {\n\t\treturn false;\n\t}\n\tauto changing = false;\n\tauto top = 0;\n\tfor (auto &result : _filterResults) {\n\t\tif (changing) {\n\t\t\tresult.top = top;\n\t\t}\n\t\tif (result.row->key().entry() == entry) {\n\t\t\tresult.row->recountHeight(_narrowRatio, _filterId);\n\t\t\tchanging = true;\n\t\t\ttop = result.top;\n\t\t}\n\t\tif (changing) {\n\t\t\ttop += result.row->height();\n\t\t}\n\t}\n\treturn _shownList->updateHeight(entry, _narrowRatio) || changing;\n}\n\nvoid InnerWidget::setNarrowRatio(float64 narrowRatio) {\n\tif (_geometryInited && _narrowRatio == narrowRatio) {\n\t\treturn;\n\t}\n\t_geometryInited = true;\n\t_narrowRatio = narrowRatio;\n\tif (_shownList->updateHeights(_narrowRatio) || !height()) {\n\t\trefresh();\n\t}\n}\n\nMain::Session &InnerWidget::session() const {\n\treturn _controller->session();\n}\n\nvoid InnerWidget::refreshWithCollapsedRows(bool toTop) {\n\tconst auto pressed = _collapsedPressed;\n\tconst auto selected = _collapsedSelected;\n\n\tsetCollapsedPressed(-1);\n\t_collapsedSelected = -1;\n\n\t_collapsedRows.clear();\n\tconst auto archive = !_shownList->empty()\n\t\t? _shownList->begin()->get()->folder()\n\t\t: nullptr;\n\tconst auto inMainMenu = session().settings().archiveInMainMenu();\n\tif (archive && (session().settings().archiveCollapsed() || inMainMenu)) {\n\t\tif (_selected && _selected->folder() == archive) {\n\t\t\t_selected = nullptr;\n\t\t}\n\t\tif (_pressed && _pressed->folder() == archive) {\n\t\t\tclearPressed();\n\t\t}\n\t\t_skipTopDialog = true;\n\t\tif (!inMainMenu && !_filterId) {\n\t\t\t_collapsedRows.push_back(\n\t\t\t\tstd::make_unique<CollapsedRow>(archive));\n\t\t}\n\t} else {\n\t\t_skipTopDialog = false;\n\t}\n\n\tAssert(!needCollapsedRowsRefresh());\n\trefresh(toTop);\n\n\tif (selected >= 0 && selected < _collapsedRows.size()) {\n\t\t_collapsedSelected = selected;\n\t}\n\tif (pressed >= 0 && pressed < _collapsedRows.size()) {\n\t\tsetCollapsedPressed(pressed);\n\t}\n}\n\nint InnerWidget::skipTopHeight() const {\n\treturn (_skipTopDialog && !_shownList->empty())\n\t\t? _shownList->begin()->get()->height()\n\t\t: 0;\n}\n\nint InnerWidget::collapsedRowsOffset() const {\n\treturn 0;\n}\n\nint InnerWidget::dialogsOffset() const {\n\treturn collapsedRowsOffset()\n\t\t+ (_collapsedRows.size() * st::dialogsImportantBarHeight)\n\t\t- skipTopHeight();\n}\n\nint InnerWidget::fixedOnTopCount() const {\n\tauto result = 0;\n\tfor (const auto &row : *_shownList) {\n\t\tif (row->entry()->fixedOnTopIndex()) {\n\t\t\t++result;\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn result;\n}\n\nint InnerWidget::shownHeight(int till) const {\n\treturn !till\n\t\t? 0\n\t\t: (till > 0 && till < _shownList->size())\n\t\t? (_shownList->begin() + till)->get()->top()\n\t\t: _shownList->height();\n}\n\nint InnerWidget::pinnedOffset() const {\n\treturn dialogsOffset() + shownHeight(fixedOnTopCount());\n}\n\nint InnerWidget::hashtagsOffset() const {\n\treturn searchInChatOffset() + searchInChatSkip();\n}\n\nint InnerWidget::filteredOffset() const {\n\treturn hashtagsOffset() + (_hashtagResults.size() * st::mentionHeight);\n}\n\nint InnerWidget::filteredIndex(int y) const {\n\treturn ranges::lower_bound(\n\t\t_filterResults,\n\t\ty,\n\t\tranges::less(),\n\t\t&FilterResult::bottom\n\t) - begin(_filterResults);\n}\n\nint InnerWidget::filteredHeight(int till) const {\n\treturn (!till || _filterResults.empty())\n\t\t? 0\n\t\t: (till > 0 && till < _filterResults.size())\n\t\t? _filterResults[till].top\n\t\t: (_filterResults.back().top + _filterResults.back().row->height());\n}\n\nint InnerWidget::peerSearchOffset() const {\n\treturn filteredOffset()\n\t\t+ filteredHeight()\n\t\t+ st::searchedBarHeight;\n}\n\nint InnerWidget::searchInChatOffset() const {\n\treturn (_searchTags ? _searchTags->height() : 0);\n}\n\nint InnerWidget::searchInChatSkip() const {\n\treturn _searchIn ? _searchIn->height() : 0;\n}\n\nint InnerWidget::previewOffset() const {\n\tauto result = peerSearchOffset();\n\tif (!_peerSearchResults.empty()) {\n\t\tresult += (_peerSearchResults.size() * st::dialogsRowHeight)\n\t\t\t+ st::searchedBarHeight;\n\t}\n\treturn result;\n}\n\nint InnerWidget::searchedOffset() const {\n\tauto result = previewOffset();\n\tif (!_previewResults.empty()) {\n\t\tresult += (_previewResults.size() * st::dialogsRowHeight)\n\t\t\t+ st::searchedBarHeight;\n\t}\n\treturn result;\n}\n\nvoid InnerWidget::changeOpenedFolder(Data::Folder *folder) {\n\tExpects(!folder || !_savedSublists);\n\n\tif (_openedFolder == folder) {\n\t\treturn;\n\t}\n\tstopReorderPinned();\n\tclearSelection();\n\t_openedFolder = folder;\n\trefreshShownList();\n\trefreshWithCollapsedRows(true);\n\tif (_loadMoreCallback) {\n\t\t_loadMoreCallback();\n\t}\n}\n\nvoid InnerWidget::changeOpenedForum(Data::Forum *forum) {\n\tExpects(!forum || !_savedSublists);\n\n\tif (_openedForum == forum) {\n\t\treturn;\n\t}\n\tstopReorderPinned();\n\tclearSelection();\n\n\tif (forum) {\n\t\tsaveChatsFilterScrollState(_filterId);\n\t}\n\t_filterId = forum\n\t\t? 0\n\t\t: _controller->activeChatsFilterCurrent();\n\tif (_openedForum) {\n\t\t// If we close it inside forum destruction we should not schedule.\n\t\tsession().data().forumIcons().scheduleUserpicsReset(_openedForum);\n\t}\n\t_openedForum = forum;\n\t_st = forum ? &st::forumTopicRow : &st::defaultDialogRow;\n\trefreshShownList();\n\n\t_openedForumLifetime.destroy();\n\tif (forum) {\n\t\trpl::merge(\n\t\t\tforum->chatsListChanges(),\n\t\t\tforum->chatsListLoadedEvents()\n\t\t) | rpl::on_next([=] {\n\t\t\trefresh();\n\t\t}, _openedForumLifetime);\n\t}\n\n\trefreshWithCollapsedRows(true);\n\tif (_loadMoreCallback) {\n\t\t_loadMoreCallback();\n\t}\n\n\tif (!forum) {\n\t\trestoreChatsFilterScrollState(_filterId);\n\t}\n}\n\nvoid InnerWidget::showSavedSublists() {\n\tExpects(!_geometryInited);\n\tExpects(!_savedSublists);\n\n\t_savedSublists = &session().data().savedMessages();\n\n\tstopReorderPinned();\n\tclearSelection();\n\n\t_filterId = 0;\n\t_openedForum = nullptr;\n\t_st = &st::defaultDialogRow;\n\trefreshShownList();\n\n\t_openedForumLifetime.destroy();\n\n\t//session().data().savedMessages().chatsListChanges(\n\t//) | rpl::on_next([=] {\n\t//\trefresh();\n\t//}, lifetime());\n\n\trefreshWithCollapsedRows(true);\n\tif (_loadMoreCallback) {\n\t\t_loadMoreCallback();\n\t}\n}\n\nvoid InnerWidget::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tp.setInactive(\n\t\t_controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any));\n\tif (!_savedSublists && _controller->contentOverlapped(this, e)) {\n\t\treturn;\n\t}\n\tconst auto activeEntry = _controller->activeChatEntryCurrent();\n\tconst auto videoPaused = _controller->isGifPausedAtLeastFor(\n\t\tWindow::GifPauseReason::Any);\n\tauto fullWidth = width();\n\tconst auto r = e->rect();\n\tauto dialogsClip = r;\n\tconst auto ms = crl::now();\n\tconst auto childListShown = _childListShown.current();\n\tauto context = Ui::PaintContext{\n\t\t.st = _st,\n\t\t.topicJumpCache = _topicJumpCache.get(),\n\t\t.folder = _openedFolder,\n\t\t.forum = _openedForum,\n\t\t.currentBg = currentBg(),\n\t\t.filter = _filterId,\n\t\t.now = ms,\n\t\t.width = fullWidth,\n\t\t.paused = videoPaused,\n\t\t.narrow = (fullWidth < st::columnMinimalWidthLeft / 2),\n\t};\n\tconst auto fillGuard = gsl::finally([&] {\n\t\t// We translate painter down, but it'll be cropped below rect.\n\t\tp.fillRect(rect(), context.currentBg);\n\t});\n\tconst auto paintRow = [&](\n\t\t\tnot_null<Row*> row,\n\t\t\tbool selected,\n\t\t\tbool mayBeActive) {\n\t\tconst auto &key = row->key();\n\t\tconst auto active = mayBeActive && isRowActive(row, activeEntry);\n\t\tconst auto history = key.history();\n\t\tconst auto forum = history && history->isForum();\n\t\tconst auto monoforum = history && history->amMonoforumAdmin();\n\t\tif ((forum || monoforum) && !_topicJumpCache) {\n\t\t\t_topicJumpCache = std::make_unique<Ui::TopicJumpCache>();\n\t\t}\n\t\tconst auto expanding = (forum || monoforum)\n\t\t\t&& (history->peer->id == childListShown.peerId);\n\t\tcontext.rightButton = maybeCacheRightButton(row);\n\t\tif (history) {\n\t\t\tif (_activeQuickAction\n\t\t\t\t&& (_activeQuickAction->data.msgBareId\n\t\t\t\t\t== history->peer->id.value)) {\n\t\t\t\tcontext.quickActionContext = _activeQuickAction.get();\n\t\t\t} else if (!_inactiveQuickActions.empty()) {\n\t\t\t\tauto it = _inactiveQuickActions.begin();\n\t\t\t\twhile (it != _inactiveQuickActions.end()) {\n\t\t\t\t\tconst auto raw = it->get();\n\t\t\t\t\tif (raw->finishedAt\n\t\t\t\t\t\t&& (ms - raw->finishedAt\n\t\t\t\t\t\t\t> st::defaultRippleAnimation.hideDuration)) {\n\t\t\t\t\t\tit = _inactiveQuickActions.erase(it);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (raw->data.msgBareId == history->peer->id.value) {\n\t\t\t\t\t\t\tcontext.quickActionContext = raw;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t++it;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tcontext.st = (forum || monoforum) ? &st::forumDialogRow : _st.get();\n\n\t\tauto chatsFilterTags = std::vector<QImage*>();\n\t\tif (context.narrow) {\n\t\t\tcontext.chatsFilterTags = nullptr;\n\t\t} else if (row->entry()->hasChatsFilterTags(context.filter)) {\n\t\t\tconst auto a = active;\n\t\t\tcontext.st = (forum || monoforum)\n\t\t\t\t? &st::taggedForumDialogRow\n\t\t\t\t: &st::taggedDialogRow;\n\t\t\tauto availableWidth = context.width\n\t\t\t\t- context.st->padding.right()\n\t\t\t\t- st::dialogsUnreadPadding\n\t\t\t\t- context.st->nameLeft;\n\t\t\tauto more = uint8(0);\n\t\t\tconst auto &list = session().data().chatsFilters().list();\n\t\t\tfor (const auto &filter : list) {\n\t\t\t\tif (!row->entry()->inChatList(filter.id())\n\t\t\t\t\t|| (filter.id() == context.filter)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (active\n\t\t\t\t\t&& (filter.flags() & Data::ChatFilter::Flag::NoRead)\n\t\t\t\t\t&& !filter.contains(history, true)) {\n\t\t\t\t\t// Hack for History::fakeUnreadWhileOpened().\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (const auto tag = cacheChatsFilterTag(filter, 0, a)) {\n\t\t\t\t\tif (more) {\n\t\t\t\t\t\tmore++;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst auto tagWidth = tag->width()\n\t\t\t\t\t\t/ style::DevicePixelRatio();\n\t\t\t\t\tif (availableWidth < tagWidth) {\n\t\t\t\t\t\tmore++;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchatsFilterTags.push_back(tag);\n\t\t\t\t\t\tavailableWidth -= tagWidth\n\t\t\t\t\t\t\t+ st::dialogRowFilterTagSkip;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (more) {\n\t\t\t\tif (const auto tag = cacheChatsFilterTag({}, more, a)) {\n\t\t\t\t\tconst auto tagWidth = tag->width()\n\t\t\t\t\t\t/ style::DevicePixelRatio();\n\t\t\t\t\tif (availableWidth < tagWidth) {\n\t\t\t\t\t\tmore++;\n\t\t\t\t\t\tif (!chatsFilterTags.empty()) {\n\t\t\t\t\t\t\tconst auto tag = cacheChatsFilterTag({}, more, a);\n\t\t\t\t\t\t\tif (tag) {\n\t\t\t\t\t\t\t\tchatsFilterTags.back() = tag;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchatsFilterTags.push_back(tag);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontext.chatsFilterTags = &chatsFilterTags;\n\t\t} else {\n\t\t\tcontext.chatsFilterTags = nullptr;\n\t\t}\n\n\t\tcontext.topicsExpanded = (expanding && !active)\n\t\t\t? childListShown.shown\n\t\t\t: 0.;\n\t\tcontext.active = active;\n\t\tcontext.selected = _menuRow.key\n\t\t\t? (row->key() == _menuRow.key)\n\t\t\t: _chatPreviewRow.key\n\t\t\t? (row->key() == _chatPreviewRow.key)\n\t\t\t: selected;\n\t\tcontext.topicJumpSelected = selected\n\t\t\t&& _selectedTopicJump\n\t\t\t&& (!_pressed || _pressedTopicJump);\n\t\tUi::RowPainter::Paint(p, row, validateVideoUserpic(row), context);\n\t\tif (context.quickActionContext) {\n\t\t\tcontext.quickActionContext = nullptr;\n\t\t}\n\t};\n\tif (_state == WidgetState::Default) {\n\t\tconst auto collapsedSkip = collapsedRowsOffset();\n\t\tp.translate(0, collapsedSkip);\n\t\tpaintCollapsedRows(p, r.translated(0, -collapsedSkip));\n\n\t\tconst auto &list = _shownList->all();\n\t\tconst auto shownBottom = _shownList->height() - skipTopHeight();\n\t\tconst auto selected = isPressed()\n\t\t\t? (_pressed ? _pressed->key() : Key())\n\t\t\t: (_selected ? _selected->key() : Key());\n\t\tif (shownBottom) {\n\t\t\tconst auto skip = dialogsOffset();\n\t\t\tconst auto promoted = fixedOnTopCount();\n\t\t\tconst auto reorderingPinned = (_aboveIndex >= 0)\n\t\t\t\t&& !_pinnedRows.empty();\n\t\t\tconst auto reorderingIndex = reorderingPinned\n\t\t\t\t? (promoted + _aboveIndex)\n\t\t\t\t: -1;\n\t\t\tconst auto reorderingRow = (reorderingIndex >= 0\n\t\t\t\t&& reorderingIndex < list.size())\n\t\t\t\t? (list.cbegin() + reorderingIndex)->get()\n\t\t\t\t: nullptr;\n\t\t\tif (reorderingRow) {\n\t\t\t\tdialogsClip = dialogsClip.marginsAdded({\n\t\t\t\t\t0,\n\t\t\t\t\treorderingRow->height(),\n\t\t\t\t\t0,\n\t\t\t\t\treorderingRow->height(),\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst auto skippedTop = skipTopHeight();\n\t\t\tconst auto paintDialog = [&](not_null<Row*> row) {\n\t\t\t\tconst auto pinned = row->index() - promoted;\n\t\t\t\tconst auto count = _pinnedRows.size();\n\t\t\t\tconst auto xadd = 0;\n\t\t\t\tconst auto yadd = base::in_range(pinned, 0, count)\n\t\t\t\t\t? qRound(_pinnedRows[pinned].yadd.current())\n\t\t\t\t\t: 0;\n\t\t\t\tif (xadd || yadd) {\n\t\t\t\t\tp.translate(xadd, yadd);\n\t\t\t\t}\n\t\t\t\tpaintRow(row, (row->key() == selected), true);\n\t\t\t\tif (xadd || yadd) {\n\t\t\t\t\tp.translate(-xadd, -yadd);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tauto i = list.findByY(dialogsClip.top() - skip);\n\t\t\tif (_skipTopDialog && i != list.cend() && !(*i)->index()) {\n\t\t\t\t++i;\n\t\t\t}\n\t\t\tif (i != list.cend()) {\n\t\t\t\tauto top = (*i)->top();\n\n\t\t\t\t// If we're reordering pinned chats we need to fill this area background first.\n\t\t\t\tif (reorderingPinned) {\n\t\t\t\t\tconst auto pinnedBottom = shownHeight(promoted + _pinnedRows.size());\n\t\t\t\t\tconst auto pinnedTop = shownHeight(promoted);\n\t\t\t\t\tp.fillRect(0, pinnedTop - skippedTop, fullWidth, pinnedBottom - pinnedTop, currentBg());\n\t\t\t\t}\n\n\t\t\t\tp.translate(0, top - skippedTop);\n\t\t\t\tfor (auto e = list.cend(); i != e; ++i) {\n\t\t\t\t\tauto row = (*i);\n\t\t\t\t\tif (top >= dialogsClip.top() - skip + dialogsClip.height()) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Skip currently dragged chat to paint it above others after.\n\t\t\t\t\tif (row->index() != promoted + _aboveIndex || _aboveIndex < 0) {\n\t\t\t\t\t\tpaintDialog(row);\n\t\t\t\t\t}\n\n\t\t\t\t\tp.translate(0, row->height());\n\t\t\t\t\ttop += row->height();\n\t\t\t\t}\n\n\t\t\t\t// Paint the dragged chat above all others.\n\t\t\t\tif (reorderingRow) {\n\t\t\t\t\tp.translate(0, reorderingRow->top() - top);\n\t\t\t\t\tpaintDialog(reorderingRow);\n\t\t\t\t\tp.translate(0, top - reorderingRow->top());\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tp.fillRect(dialogsClip, currentBg());\n\t\t}\n\t} else if (_state == WidgetState::Filtered) {\n\t\tif (_searchTags) {\n\t\t\tpaintSearchTags(p, {\n\t\t\t\t.st = &st::forumTopicRow,\n\t\t\t\t.currentBg = currentBg(),\n\t\t\t\t.now = ms,\n\t\t\t\t.width = fullWidth,\n\t\t\t\t.paused = videoPaused,\n\t\t\t});\n\t\t\tp.translate(0, _searchTags->height());\n\t\t}\n\t\tif (_searchIn) {\n\t\t\tp.translate(0, searchInChatSkip());\n\t\t\tif (_previewResults.empty() && _searchResults.empty()) {\n\t\t\t\tp.fillRect(0, 0, fullWidth, st::lineWidth, st::shadowFg);\n\t\t\t}\n\t\t}\n\t\tif (!_hashtagResults.empty()) {\n\t\t\tconst auto skip = hashtagsOffset();\n\t\t\tauto from = floorclamp(r.y() - skip, st::mentionHeight, 0, _hashtagResults.size());\n\t\t\tauto to = ceilclamp(r.y() + r.height() - skip, st::mentionHeight, 0, _hashtagResults.size());\n\t\t\tp.translate(0, from * st::mentionHeight);\n\t\t\tif (from < _hashtagResults.size()) {\n\t\t\t\tconst auto htagleft = st::defaultDialogRow.padding.left();\n\t\t\t\tauto htagwidth = fullWidth\n\t\t\t\t\t- htagleft\n\t\t\t\t\t- st::defaultDialogRow.padding.right();\n\n\t\t\t\tp.setFont(st::mentionFont);\n\t\t\t\tfor (; from < to; ++from) {\n\t\t\t\t\tauto &result = _hashtagResults[from];\n\t\t\t\t\tbool selected = (from == (isPressed() ? _hashtagPressed : _hashtagSelected));\n\t\t\t\t\tp.fillRect(0, 0, fullWidth, st::mentionHeight, selected ? st::mentionBgOver : currentBg());\n\t\t\t\t\tresult->row.paintRipple(p, 0, 0, fullWidth);\n\t\t\t\t\tauto &tag = result->tag;\n\t\t\t\t\tif (selected) {\n\t\t\t\t\t\tint skip = (st::mentionHeight - st::smallCloseIconOver.height()) / 2;\n\t\t\t\t\t\tst::smallCloseIconOver.paint(p, QPoint(fullWidth - st::smallCloseIconOver.width() - skip, skip), width());\n\t\t\t\t\t}\n\t\t\t\t\tauto first = (_hashtagFilter.size() < 2) ? QString() : ('#' + tag.mid(0, _hashtagFilter.size() - 1));\n\t\t\t\t\tauto second = (_hashtagFilter.size() < 2) ? ('#' + tag) : tag.mid(_hashtagFilter.size() - 1);\n\t\t\t\t\tauto firstwidth = st::mentionFont->width(first);\n\t\t\t\t\tauto secondwidth = st::mentionFont->width(second);\n\t\t\t\t\tif (htagwidth < firstwidth + secondwidth) {\n\t\t\t\t\t\tif (htagwidth < firstwidth + st::mentionFont->elidew) {\n\t\t\t\t\t\t\tfirst = st::mentionFont->elided(first + second, htagwidth);\n\t\t\t\t\t\t\tsecond = QString();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsecond = st::mentionFont->elided(second, htagwidth - firstwidth);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tp.setFont(st::mentionFont);\n\t\t\t\t\tif (!first.isEmpty()) {\n\t\t\t\t\t\tp.setPen(selected ? st::mentionFgOverActive : st::mentionFgActive);\n\t\t\t\t\t\tp.drawText(htagleft, st::mentionTop + st::mentionFont->ascent, first);\n\t\t\t\t\t}\n\t\t\t\t\tif (!second.isEmpty()) {\n\t\t\t\t\t\tp.setPen(selected ? st::mentionFgOver : st::mentionFg);\n\t\t\t\t\t\tp.drawText(htagleft + firstwidth, st::mentionTop + st::mentionFont->ascent, second);\n\t\t\t\t\t}\n\t\t\t\t\tp.translate(0, st::mentionHeight);\n\t\t\t\t}\n\t\t\t\tif (to < _hashtagResults.size()) {\n\t\t\t\t\tp.translate(0, (_hashtagResults.size() - to) * st::mentionHeight);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!_filterResults.empty()) {\n\t\t\tauto skip = filteredOffset();\n\t\t\tauto from = filteredIndex(r.y() - skip);\n\t\t\tauto to = std::min(\n\t\t\t\tfilteredIndex(r.y() + r.height() - skip) + 1,\n\t\t\t\tint(_filterResults.size()));\n\t\t\tconst auto height = filteredHeight(from);\n\t\t\tp.translate(0, height);\n\t\t\tfor (; from < to; ++from) {\n\t\t\t\tconst auto selected = isPressed()\n\t\t\t\t\t? (from == _filteredPressed)\n\t\t\t\t\t: (from == _filteredSelected);\n\t\t\t\tconst auto row = _filterResults[from].row;\n\t\t\t\tpaintRow(row, selected, !activeEntry.fullId);\n\t\t\t\tp.translate(0, row->height());\n\t\t\t}\n\t\t}\n\n\t\tif (!_peerSearchResults.empty()) {\n\t\t\tp.fillRect(0, 0, fullWidth, st::searchedBarHeight, st::searchedBarBg);\n\t\t\tp.setFont(st::searchedBarFont);\n\t\t\tp.setPen(st::searchedBarFg);\n\t\t\tp.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), tr::lng_search_global_results(tr::now));\n\t\t\tp.translate(0, st::searchedBarHeight);\n\n\t\t\tauto skip = peerSearchOffset();\n\t\t\tauto from = floorclamp(r.y() - skip, st::dialogsRowHeight, 0, _peerSearchResults.size());\n\t\t\tauto to = ceilclamp(r.y() + r.height() - skip, st::dialogsRowHeight, 0, _peerSearchResults.size());\n\t\t\tp.translate(0, from * st::dialogsRowHeight);\n\t\t\tif (from < _peerSearchResults.size()) {\n\t\t\t\tconst auto activePeer = activeEntry.key.peer();\n\t\t\t\tfor (; from < to; ++from) {\n\t\t\t\t\tconst auto &result = _peerSearchResults[from];\n\t\t\t\t\tif (result->sponsored\n\t\t\t\t\t\t&& r.y() <= (skip + from * st::dialogsRowHeight)\n\t\t\t\t\t\t&& r.y() + r.height() >= (skip + (from + 1) * st::dialogsRowHeight)) {\n\t\t\t\t\t\tsession().sponsoredMessages().view(\n\t\t\t\t\t\t\tresult->sponsored->data.randomId);\n\t\t\t\t\t}\n\t\t\t\t\tconst auto peer = result->peer;\n\t\t\t\t\tconst auto active = !activeEntry.fullId\n\t\t\t\t\t\t&& activePeer\n\t\t\t\t\t\t&& ((peer == activePeer)\n\t\t\t\t\t\t\t|| (peer->migrateTo() == activePeer));\n\t\t\t\t\tconst auto selected = (from == ((_peerSearchMenu >= 0)\n\t\t\t\t\t\t? _peerSearchMenu\n\t\t\t\t\t\t: isPressed()\n\t\t\t\t\t\t? _peerSearchPressed\n\t\t\t\t\t\t: _peerSearchSelected));\n\t\t\t\t\tif (result->sponsored\n\t\t\t\t\t\t&& result->sponsored->button.text.isEmpty()) {\n\t\t\t\t\t\tfillRightButton(\n\t\t\t\t\t\t\tresult->sponsored->button,\n\t\t\t\t\t\t\ttr::lng_search_sponsored_button(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\ttr::marked),\n\t\t\t\t\t\t\tst::dialogsSponsoredButton);\n\t\t\t\t\t}\n\n\t\t\t\t\tpaintPeerSearchResult(p, result.get(), {\n\t\t\t\t\t\t.rightButton = (result->sponsored\n\t\t\t\t\t\t\t? &result->sponsored->button\n\t\t\t\t\t\t\t: nullptr),\n\t\t\t\t\t\t.st = &st::defaultDialogRow,\n\t\t\t\t\t\t.currentBg = currentBg(),\n\t\t\t\t\t\t.now = ms,\n\t\t\t\t\t\t.width = fullWidth,\n\t\t\t\t\t\t.active = active,\n\t\t\t\t\t\t.selected = selected,\n\t\t\t\t\t\t.paused = videoPaused,\n\t\t\t\t\t});\n\t\t\t\t\tp.translate(0, st::dialogsRowHeight);\n\t\t\t\t}\n\t\t\t\tif (to < _peerSearchResults.size()) {\n\t\t\t\t\tp.translate(0, (_peerSearchResults.size() - to) * st::dialogsRowHeight);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst auto showUnreadInSearchResults = uniqueSearchResults();\n\t\tif (_previewResults.empty() && _searchResults.empty()) {\n\t\t\tif (_loadingAnimation) {\n\t\t\t\tconst auto text = tr::lng_contacts_loading(tr::now);\n\t\t\t\tp.fillRect(0, 0, fullWidth, st::searchedBarHeight, st::searchedBarBg);\n\t\t\t\tp.setFont(st::searchedBarFont);\n\t\t\t\tp.setPen(st::searchedBarFg);\n\t\t\t\tp.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), text);\n\t\t\t\tp.translate(0, st::searchedBarHeight);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (!_previewResults.empty()) {\n\t\t\tconst auto text = tr::lng_search_tab_public_posts(tr::now);\n\t\t\tp.fillRect(0, 0, fullWidth, st::searchedBarHeight, st::searchedBarBg);\n\t\t\tp.setFont(st::searchedBarFont);\n\t\t\tp.setPen(st::searchedBarFg);\n\t\t\tp.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), text);\n\t\t\tconst auto moreFont = (_selectedMorePosts || _pressedMorePosts)\n\t\t\t\t? st::searchedBarFont->underline()\n\t\t\t\t: st::searchedBarFont;\n\t\t\t{\n\t\t\t\tconst auto text = tr::lng_channels_your_more(tr::now);\n\t\t\t\tif (!_morePostsWidth) {\n\t\t\t\t\t_morePostsWidth = moreFont->width(text);\n\t\t\t\t}\n\t\t\t\tp.setFont(moreFont);\n\t\t\t\tp.drawTextLeft(\n\t\t\t\t\twidth() - st::searchedBarPosition.x() - _morePostsWidth,\n\t\t\t\t\tst::searchedBarPosition.y(),\n\t\t\t\t\twidth(),\n\t\t\t\t\ttext);\n\t\t\t\tp.translate(0, st::searchedBarHeight);\n\t\t\t}\n\t\t\tauto skip = previewOffset();\n\t\t\tauto from = floorclamp(r.y() - skip, _st->height, 0, _previewResults.size());\n\t\t\tauto to = ceilclamp(r.y() + r.height() - skip, _st->height, 0, _previewResults.size());\n\t\t\tp.translate(0, from * _st->height);\n\t\t\tif (from < _previewResults.size()) {\n\t\t\t\tconst auto searchLowerText = (_searchHashOrCashtag == HashOrCashtag::None)\n\t\t\t\t\t? _searchState.query.toLower()\n\t\t\t\t\t: QString();\n\t\t\t\tfor (; from < to; ++from) {\n\t\t\t\t\tconst auto &result = _previewResults[from];\n\t\t\t\t\tconst auto active = isSearchResultActive(result.get(), activeEntry);\n\t\t\t\t\tconst auto selected = _menuRow.key\n\t\t\t\t\t\t? isSearchResultActive(result.get(), _menuRow)\n\t\t\t\t\t\t: _chatPreviewRow.key\n\t\t\t\t\t\t? isSearchResultActive(result.get(), _chatPreviewRow)\n\t\t\t\t\t\t: (from == (isPressed()\n\t\t\t\t\t\t\t? _previewPressed\n\t\t\t\t\t\t\t: _previewSelected));\n\t\t\t\t\tUi::RowPainter::Paint(p, result.get(), {\n\t\t\t\t\t\t.st = _st,\n\t\t\t\t\t\t.folder = _openedFolder,\n\t\t\t\t\t\t.forum = _openedForum,\n\t\t\t\t\t\t.currentBg = currentBg(),\n\t\t\t\t\t\t.filter = _filterId,\n\t\t\t\t\t\t.now = ms,\n\t\t\t\t\t\t.searchLowerText = QStringView(searchLowerText),\n\t\t\t\t\t\t.width = fullWidth,\n\t\t\t\t\t\t.active = active,\n\t\t\t\t\t\t.selected = selected,\n\t\t\t\t\t\t.paused = videoPaused,\n\t\t\t\t\t\t.search = true,\n\t\t\t\t\t\t.narrow = (fullWidth < st::columnMinimalWidthLeft / 2),\n\t\t\t\t\t\t.displayUnreadInfo = showUnreadInSearchResults,\n\t\t\t\t\t});\n\t\t\t\t\tp.translate(0, _st->height);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (to < _previewResults.size()) {\n\t\t\t\tp.translate(0, (_previewResults.size() - to) * _st->height);\n\t\t\t}\n\t\t}\n\n\t\tif (!_searchResults.empty()) {\n\t\t\tconst auto text = showUnreadInSearchResults\n\t\t\t\t? u\"Search results\"_q\n\t\t\t\t: (_searchState.tab == ChatSearchTab::PublicPosts && !_searchIn)\n\t\t\t\t? (_searchState.query.isEmpty()\n\t\t\t\t\t? tr::lng_posts_subtitle_empty(tr::now)\n\t\t\t\t\t: tr::lng_posts_subtitle(tr::now))\n\t\t\t\t: tr::lng_search_found_results(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\t_searchedMigratedCount + _searchedCount);\n\t\t\tconst auto searchLowerText = (_searchHashOrCashtag == HashOrCashtag::None)\n\t\t\t\t? _searchState.query.toLower()\n\t\t\t\t: QString();\n\t\t\tp.fillRect(0, 0, fullWidth, st::searchedBarHeight, st::searchedBarBg);\n\t\t\tp.setFont(st::searchedBarFont);\n\t\t\tp.setPen(st::searchedBarFg);\n\t\t\tp.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), text);\n\t\t\tconst auto filterOver = _selectedChatTypeFilter\n\t\t\t\t|| _pressedChatTypeFilter;\n\t\t\tconst auto filterFont = filterOver\n\t\t\t\t? st::searchedBarFont->underline()\n\t\t\t\t: st::searchedBarFont;\n\t\t\tif (hasChatTypeFilter()) {\n\t\t\t\tconst auto text = ChatTypeFilterLabel(_searchState.filter);\n\t\t\t\tif (!_chatTypeFilterWidth) {\n\t\t\t\t\t_chatTypeFilterWidth = filterFont->width(text);\n\t\t\t\t}\n\t\t\t\tp.setFont(filterFont);\n\t\t\t\tp.drawTextLeft(\n\t\t\t\t\t(width()\n\t\t\t\t\t\t- st::searchedBarPosition.x()\n\t\t\t\t\t\t- _chatTypeFilterWidth),\n\t\t\t\t\tst::searchedBarPosition.y(),\n\t\t\t\t\twidth(),\n\t\t\t\t\ttext);\n\t\t\t}\n\t\t\tp.translate(0, st::searchedBarHeight);\n\n\t\t\tauto skip = searchedOffset();\n\t\t\tauto from = floorclamp(r.y() - skip, _st->height, 0, _searchResults.size());\n\t\t\tauto to = ceilclamp(r.y() + r.height() - skip, _st->height, 0, _searchResults.size());\n\t\t\tp.translate(0, from * _st->height);\n\t\t\tif (from < _searchResults.size()) {\n\t\t\t\tfor (; from < to; ++from) {\n\t\t\t\t\tconst auto &result = _searchResults[from];\n\t\t\t\t\tconst auto active = isSearchResultActive(result.get(), activeEntry);\n\t\t\t\t\tconst auto selected = _menuRow.key\n\t\t\t\t\t\t? isSearchResultActive(result.get(), _menuRow)\n\t\t\t\t\t\t: _chatPreviewRow.key\n\t\t\t\t\t\t? isSearchResultActive(result.get(), _chatPreviewRow)\n\t\t\t\t\t\t: (from == (isPressed()\n\t\t\t\t\t\t\t? _searchedPressed\n\t\t\t\t\t\t\t: _searchedSelected));\n\t\t\t\t\tUi::RowPainter::Paint(p, result.get(), {\n\t\t\t\t\t\t.st = _st,\n\t\t\t\t\t\t.folder = _openedFolder,\n\t\t\t\t\t\t.forum = _openedForum,\n\t\t\t\t\t\t.currentBg = currentBg(),\n\t\t\t\t\t\t.filter = _filterId,\n\t\t\t\t\t\t.now = ms,\n\t\t\t\t\t\t.searchLowerText = QStringView(searchLowerText),\n\t\t\t\t\t\t.width = fullWidth,\n\t\t\t\t\t\t.active = active,\n\t\t\t\t\t\t.selected = selected,\n\t\t\t\t\t\t.paused = videoPaused,\n\t\t\t\t\t\t.search = true,\n\t\t\t\t\t\t.narrow = (fullWidth < st::columnMinimalWidthLeft / 2),\n\t\t\t\t\t\t.displayUnreadInfo = showUnreadInSearchResults,\n\t\t\t\t\t});\n\t\t\t\t\tp.translate(0, _st->height);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid InnerWidget::fillRightButton(\n\t\tRightButton &button,\n\t\tconst TextWithEntities &text,\n\t\tconst style::DialogRightButton &st) {\n\tbutton.st = &st;\n\tbutton.text.setMarkedText(st.button.style, text);\n\tconst auto size = QSize(\n\t\tbutton.text.maxWidth() + button.text.minHeight(),\n\t\tst.button.height);\n\tconst auto generateBg = [&](const style::color &c) {\n\t\tauto bg = QImage(\n\t\t\tstyle::DevicePixelRatio() * size,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tbg.setDevicePixelRatio(style::DevicePixelRatio());\n\t\tbg.fill(Qt::transparent);\n\t\t{\n\t\t\tauto p = QPainter(&bg);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(c);\n\t\t\tconst auto r = size.height() / 2;\n\t\t\tp.drawRoundedRect(Rect(size), r, r);\n\t\t}\n\t\treturn bg;\n\t};\n\tbutton.bg = generateBg(st.button.textBg);\n\tbutton.selectedBg = generateBg(st.button.textBgOver);\n\tbutton.activeBg = generateBg(st.button.textFg);\n}\n\n[[nodiscard]] RightButton *InnerWidget::maybeCacheRightButton(Row *row) {\n\tif (const auto user = MaybeBotWithApp(row)) {\n\t\tconst auto it = _rightButtons.find(user->id);\n\t\tif (it == _rightButtons.end()) {\n\t\t\tauto rightButton = RightButton();\n\t\t\tfillRightButton(\n\t\t\t\trightButton,\n\t\t\t\ttr::lng_profile_open_app_short(\n\t\t\t\t\ttr::now,\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::dialogRowOpenBot);\n\t\t\treturn &(_rightButtons.emplace(\n\t\t\t\tuser->id,\n\t\t\t\tstd::move(rightButton)).first->second);\n\t\t} else {\n\t\t\treturn &(it->second);\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nUi::VideoUserpic *InnerWidget::validateVideoUserpic(not_null<Row*> row) {\n\tconst auto history = row->history();\n\treturn history ? validateVideoUserpic(history) : nullptr;\n}\n\nUi::VideoUserpic *InnerWidget::validateVideoUserpic(\n\t\tnot_null<History*> history) {\n\tconst auto peer = history->peer;\n\tif (!peer->isPremium()\n\t\t|| peer->userpicPhotoUnknown()\n\t\t|| !peer->userpicHasVideo()) {\n\t\t_videoUserpics.remove(peer);\n\t\treturn nullptr;\n\t}\n\tconst auto i = _videoUserpics.find(peer);\n\tif (i != end(_videoUserpics)) {\n\t\treturn i->second.get();\n\t}\n\tconst auto repaint = [=] {\n\t\tupdateDialogRow({ history, FullMsgId() });\n\t\tupdateSearchResult(history->peer);\n\t};\n\treturn _videoUserpics.emplace(peer, std::make_unique<Ui::VideoUserpic>(\n\t\tpeer,\n\t\trepaint\n\t)).first->second.get();\n}\n\nvoid InnerWidget::paintCollapsedRows(Painter &p, QRect clip) const {\n\tauto index = 0;\n\tconst auto rowHeight = st::dialogsImportantBarHeight;\n\tfor (const auto &row : _collapsedRows) {\n\t\tconst auto increment = gsl::finally([&] {\n\t\t\tp.translate(0, rowHeight);\n\t\t\t++index;\n\t\t});\n\n\t\tconst auto y = index * rowHeight;\n\t\tif (!clip.intersects(QRect(0, y, width(), rowHeight))) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto selected = (index == _collapsedSelected)\n\t\t\t|| (index == _collapsedPressed);\n\t\tpaintCollapsedRow(p, row.get(), selected);\n\t}\n}\n\nvoid InnerWidget::paintCollapsedRow(\n\t\tPainter &p,\n\t\tnot_null<const CollapsedRow*> row,\n\t\tbool selected) const {\n\tExpects(row->folder != nullptr);\n\n\tconst auto text = row->folder->chatListName();\n\tconst auto unread = row->folder->chatListBadgesState().unreadCounter;\n\tconst auto fullWidth = width();\n\tUi::PaintCollapsedRow(p, row->row, row->folder, text, unread, {\n\t\t.st = _st,\n\t\t.currentBg = currentBg(),\n\t\t.width = fullWidth,\n\t\t.selected = selected,\n\t\t.narrow = (fullWidth < st::columnMinimalWidthLeft / 2),\n\t});\n}\n\nbool InnerWidget::isRowActive(\n\t\tnot_null<Row*> row,\n\t\tconst RowDescriptor &entry) const {\n\tconst auto key = row->key();\n\tif (entry.key == key) {\n\t\treturn true;\n\t} else if (const auto topic = entry.key.topic()) {\n\t\tif (const auto history = key.history()) {\n\t\t\treturn (history->peer == topic->peer())\n\t\t\t\t&& HistoryView::SubsectionTabs::UsedFor(history);\n\t\t}\n\t\treturn false;\n\t} else if (const auto sublist = entry.key.sublist()) {\n\t\treturn key.history() && key.history() == sublist->owningHistory();\n\t}\n\treturn false;\n}\n\nbool InnerWidget::isSearchResultActive(\n\t\tnot_null<FakeRow*> result,\n\t\tconst RowDescriptor &entry) const {\n\tconst auto item = result->item();\n\tconst auto peer = item->history()->peer;\n\treturn (item->fullId() == entry.fullId)\n\t\t|| (peer->migrateTo()\n\t\t\t&& (peer->migrateTo()->id == entry.fullId.peer)\n\t\t\t&& (item->id == -entry.fullId.msg))\n\t\t|| (uniqueSearchResults() && peer == entry.key.peer());\n}\n\nvoid InnerWidget::paintPeerSearchResult(\n\t\tPainter &p,\n\t\tnot_null<const PeerSearchResult*> result,\n\t\tconst Ui::PaintContext &context) {\n\tQRect fullRect(0, 0, context.width, st::dialogsRowHeight);\n\tp.fillRect(\n\t\tfullRect,\n\t\t(context.active\n\t\t\t? st::dialogsBgActive\n\t\t\t: context.selected\n\t\t\t? st::dialogsBgOver\n\t\t\t: currentBg()));\n\tif (!context.active) {\n\t\tresult->row.paintRipple(p, 0, 0, context.width);\n\t}\n\n\tauto peer = result->peer;\n\tauto userpicPeer = (peer->migrateTo() ? peer->migrateTo() : peer);\n\tuserpicPeer->paintUserpicLeft(\n\t\tp,\n\t\tresult->row.userpicView(),\n\t\tcontext.st->padding.left(),\n\t\tcontext.st->padding.top(),\n\t\twidth(),\n\t\tcontext.st->photoSize);\n\n\tauto nameleft = context.st->nameLeft;\n\tauto available = context.width - nameleft - context.st->padding.right();\n\tauto namewidth = available;\n\tif (const auto used = Ui::PaintRightButton(p, context)) {\n\t\tnamewidth -= used - st::dialogsUnreadPadding;\n\t}\n\tQRect rectForName(nameleft, context.st->nameTop, namewidth, st::semiboldFont->height);\n\n\tif (result->name.isEmpty()) {\n\t\tresult->name.setText(\n\t\t\tst::semiboldTextStyle,\n\t\t\tpeer->name(),\n\t\t\tUi::NameTextOptions());\n\t}\n\n\tif (const auto info = peer->botVerifyDetails()) {\n\t\tif (!result->badge.ready(info)) {\n\t\t\tresult->badge.set(\n\t\t\t\tinfo,\n\t\t\t\tpeer->owner().customEmojiManager().factory(),\n\t\t\t\t[=] { updateSearchResult(peer); });\n\t\t}\n\t\tconst auto &st = Ui::VerifiedStyle(context);\n\t\tconst auto position = rectForName.topLeft();\n\t\tconst auto skip = result->badge.drawVerified(p, position, st);\n\t\trectForName.setLeft(position.x() + skip + st::dialogsChatTypeSkip);\n\t} else if (const auto chatTypeIcon = Ui::ChatTypeIcon(peer, context)) {\n\t\tchatTypeIcon->paint(p, rectForName.topLeft(), context.width);\n\t\trectForName.setLeft(rectForName.left()\n\t\t\t+ chatTypeIcon->width()\n\t\t\t+ st::dialogsChatTypeSkip);\n\t}\n\tconst auto badgeWidth = result->badge.drawGetWidth(p, {\n\t\t.peer = peer,\n\t\t.rectForName = rectForName,\n\t\t.nameWidth = result->name.maxWidth(),\n\t\t.outerWidth = context.width,\n\t\t.verified = (context.active\n\t\t\t? &st::dialogsVerifiedIconActive\n\t\t\t: context.selected\n\t\t\t? &st::dialogsVerifiedIconOver\n\t\t\t: &st::dialogsVerifiedIcon),\n\t\t.premium = &ThreeStateIcon(\n\t\t\tst::dialogsPremiumIcon,\n\t\t\tcontext.active,\n\t\t\tcontext.selected),\n\t\t.scam = (context.active\n\t\t\t? &st::dialogsScamFgActive\n\t\t\t: context.selected\n\t\t\t? &st::dialogsScamFgOver\n\t\t\t: &st::dialogsScamFg),\n\t\t.direct = (context.active\n\t\t\t? &st::dialogsDraftFgActive\n\t\t\t: context.selected\n\t\t\t? &st::windowSubTextFgOver\n\t\t\t: &st::windowSubTextFg),\n\t\t.premiumFg = (context.active\n\t\t\t? &st::dialogsVerifiedIconBgActive\n\t\t\t: context.selected\n\t\t\t? &st::dialogsVerifiedIconBgOver\n\t\t\t: &st::dialogsVerifiedIconBg),\n\t\t.customEmojiRepaint = [=] { updateSearchResult(peer); },\n\t\t.now = context.now,\n\t\t.prioritizeVerification = true,\n\t\t.paused = context.paused,\n\t});\n\trectForName.setWidth(rectForName.width() - badgeWidth);\n\n\tQRect tr(context.st->textLeft, context.st->textTop, namewidth, st::dialogsTextFont->height);\n\tp.setFont(st::dialogsTextFont);\n\tQString username = peer->username();\n\tif (!context.active && username.startsWith(_peerSearchQuery, Qt::CaseInsensitive)) {\n\t\tauto first = '@' + username.mid(0, _peerSearchQuery.size());\n\t\tauto second = username.mid(_peerSearchQuery.size());\n\t\tauto w = st::dialogsTextFont->width(first);\n\t\tif (w >= tr.width()) {\n\t\t\tp.setPen(st::dialogsTextFgService);\n\t\t\tp.drawText(tr.left(), tr.top() + st::dialogsTextFont->ascent, st::dialogsTextFont->elided(first, tr.width()));\n\t\t} else {\n\t\t\tp.setPen(st::dialogsTextFgService);\n\t\t\tp.drawText(tr.left(), tr.top() + st::dialogsTextFont->ascent, first);\n\t\t\tp.setPen(st::dialogsTextFg);\n\t\t\tp.drawText(tr.left() + w, tr.top() + st::dialogsTextFont->ascent, st::dialogsTextFont->elided(second, tr.width() - w));\n\t\t}\n\t} else {\n\t\tp.setPen(context.active ? st::dialogsTextFgActive : st::dialogsTextFgService);\n\t\tp.drawText(tr.left(), tr.top() + st::dialogsTextFont->ascent, st::dialogsTextFont->elided('@' + username, tr.width()));\n\t}\n\n\tp.setPen(context.active ? st::dialogsTextFgActive : st::dialogsNameFg);\n\tresult->name.drawElided(p, rectForName.left(), rectForName.top(), rectForName.width());\n}\n\nQBrush InnerWidget::currentBg() const {\n\treturn anim::brush(\n\t\tst::dialogsBg,\n\t\tst::dialogsBgOver,\n\t\t_childListShown.current().shown);\n}\n\nvoid InnerWidget::paintSearchTags(\n\t\tPainter &p,\n\t\tconst Ui::PaintContext &context) const {\n\tExpects(_searchTags != nullptr);\n\n\tconst auto height = _searchTags->height();\n\tp.fillRect(0, 0, width(), height, currentBg());\n\tconst auto top = st::dialogsSearchTagBottom / 2;\n\tconst auto position = QPoint(_searchTagsLeft, top);\n\t_searchTags->paint(p, position, context.now, context.paused);\n}\n\nvoid InnerWidget::showPeerMenu() {\n\tif (!_selected) {\n\t\treturn;\n\t}\n\tconst auto &padding = st::defaultDialogRow.padding;\n\tconst auto pos = QPoint(\n\t\twidth() - padding.right(),\n\t\t_selected->top() + _selected->height() + padding.bottom());\n\tauto event = QContextMenuEvent(\n\t\tQContextMenuEvent::Keyboard,\n\t\tpos,\n\t\tmapToGlobal(pos));\n\tInnerWidget::contextMenuEvent(&event);\n}\n\nvoid InnerWidget::mouseMoveEvent(QMouseEvent *e) {\n\tif (_chatPreviewTouchGlobal || _touchDragStartGlobal) {\n\t\treturn;\n\t}\n\tconst auto globalPosition = e->globalPos();\n\tif (!_lastMousePosition) {\n\t\t_lastMousePosition = globalPosition;\n\t\treturn;\n\t} else if (!_mouseSelection\n\t\t&& *_lastMousePosition == globalPosition) {\n\t\treturn;\n\t}\n\n\tif (_lastMousePosition && *_lastMousePosition != globalPosition) {\n\t\tif (skipChatsListFreeze()) {\n\t\t\tunfreezeShownList(true);\n\t\t} else {\n\t\t\tif (!_freezeTimer.isActive()) {\n\t\t\t\t_shownList->freeze();\n\t\t\t}\n\t\t\t_freezeTimer.callOnce(kFreezeTimeout);\n\t\t}\n\t}\n\n\tif (_pressed && (e->buttons() & Qt::LeftButton)) {\n\t\tconst auto local = e->pos();\n\t\tconst auto outside = _dragging ? false : true;\n\t\tconst auto delta = local - _dragStart;\n\t\tconst auto thresholdY = _pressed->entry()->isPinnedDialog(_filterId)\n\t\t\t? kStartDragToFilterThresholdY\n\t\t\t: kStartDragToFilterThresholdX;\n\t\tconst auto distanceExceeded = std::abs(delta.x())\n\t\t\t\t>= style::ConvertScale(kStartDragToFilterThresholdX)\n\t\t\t|| std::abs(delta.y()) >= style::ConvertScale(thresholdY);\n\n\t\tif (!_qdragging && outside && distanceExceeded) {\n\t\t\tif (_pressed->history()) {\n\t\t\t\tunfreezeShownList(true);\n\t\t\t\t_dragging = _pressed;\n\t\t\t\t_qdragging = _pressed;\n\t\t\t\tInvokeQueued(this, [=] { performDrag(); });\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else if (!outside && _qdragging) {\n\t\t\t_qdragging = nullptr;\n\t\t}\n\t}\n\n\tselectByMouse(globalPosition);\n\tif (_chatPreviewScheduled && !isUserpicPress()) {\n\t\tcancelChatPreview();\n\t}\n}\n\nbool InnerWidget::skipChatsListFreeze() const {\n\treturn _dragging != nullptr;\n}\n\nvoid InnerWidget::unfreezeShownList(bool updateIfWasFrozen) {\n\tconst auto wasFrozen = _freezeTimer.isActive();\n\t_freezeTimer.cancel();\n\t_shownList->unfreeze();\n\tif (updateIfWasFrozen && wasFrozen) {\n\t\tupdate();\n\t}\n}\n\nvoid InnerWidget::performDrag() {\n\tif (!_qdragging || !session().data().chatsFilters().has()) {\n\t\treturn;\n\t}\n\tconst auto history = _qdragging->history();\n\tif (!history) {\n\t\treturn;\n\t}\n\n\tauto mimeData = std::make_unique<QMimeData>();\n\tauto byteArray = [&] {\n\t\tauto data = QByteArray();\n\t\tauto stream = QDataStream(&data, QIODevice::WriteOnly);\n\t\tstream << history->peer->id.value;\n\t\tstream << history->session().isTestMode();\n\t\treturn data;\n\t}();\n\tmimeData->setData(\n\t\tu\"application/x-telegram-dialog\"_q,\n\t\tstd::move(byteArray));\n\n\tif (const auto u = history->peer->username(); !u.isEmpty()) {\n\t\tmimeData->setText(history->peer->session().createInternalLinkFull(u));\n\t\tmimeData->setData(\n\t\t\tu\"application/x-telegram-input-field\"_q,\n\t\t\t('@' + u).toUtf8());\n\t}\n\n\tconst auto &st = st::defaultDialogRow;\n\tauto pixmap = QPixmap(Size(st.height * style::DevicePixelRatio()));\n\tpixmap.setDevicePixelRatio(style::DevicePixelRatio());\n\tpixmap.fill(Qt::transparent);\n\tif (const auto draw = PaintUserpicCallback(history->peer, true)) {\n\t\tauto p = Painter(&pixmap);\n\t\tp.setOpacity(0.7);\n\t\tconst auto pos = (st.height - st.photoSize) / 2;\n\t\tdraw(p, pos, pos, st.height, st.photoSize);\n\t}\n\n\tUi::Animations::Manager::SetScheduleWithInvokeQueued(true);\n\n\t_controller->cancelScheduledPreview();\n\t// This call enters event loop and can destroy any QObject.\n\t_controller->widget()->launchDrag(\n\t\tstd::move(mimeData),\n\t\t[=, weak = base::make_weak(this)] {\n\t\t\tUi::Animations::Manager::SetScheduleWithInvokeQueued(false);\n\t\t\tif (weak) {\n\t\t\t\t_qdragging = nullptr;\n\t\t\t\tclearPressed();\n\t\t\t\tfinishReorderOnRelease();\n\t\t\t\tselectByMouse(QCursor::pos());\n\t\t\t}\n\t\t},\n\t\tpixmap);\n}\n\nvoid InnerWidget::cancelChatPreview() {\n\t_chatPreviewTouchGlobal = {};\n\t_chatPreviewScheduled = false;\n\tif (_chatPreviewRow.key) {\n\t\tupdateDialogRow(base::take(_chatPreviewRow));\n\t}\n\t_controller->cancelScheduledPreview();\n}\n\nvoid InnerWidget::clearIrrelevantState() {\n\tif (_state == WidgetState::Default) {\n\t\t_hashtagSelected = -1;\n\t\tsetHashtagPressed(-1);\n\t\t_hashtagDeleteSelected = _hashtagDeletePressed = false;\n\t\t_filteredSelected = -1;\n\t\tsetFilteredPressed(-1, false, false);\n\t\t_peerSearchSelected = -1;\n\t\tsetPeerSearchPressed(-1, false);\n\t\t_previewSelected = -1;\n\t\tsetPreviewPressed(-1);\n\t\t_searchedSelected = -1;\n\t\tsetSearchedPressed(-1);\n\t} else if (_state == WidgetState::Filtered) {\n\t\t_collapsedSelected = -1;\n\t\tsetCollapsedPressed(-1);\n\t\t_selected = nullptr;\n\t\tclearPressed();\n\t}\n}\n\nbool InnerWidget::lookupIsInBotAppButton(\n\t\tRow *row,\n\t\tQPoint localPosition) {\n\tif (_narrowRatio) {\n\t\treturn false;\n\t}\n\tif (const auto user = MaybeBotWithApp(row)) {\n\t\tconst auto it = _rightButtons.find(user->id);\n\t\tif (it != _rightButtons.end()) {\n\t\t\treturn lookupIsInRightButton(it->second, localPosition);\n\t\t}\n\t}\n\treturn false;\n}\n\nbool InnerWidget::lookupIsInRightButton(\n\t\tconst RightButton &button,\n\t\tQPoint localPosition) {\n\tif (!button.st) {\n\t\treturn false;\n\t}\n\n\tconst auto s = button.bg.size() / style::DevicePixelRatio();\n\tconst auto r = QRect(\n\t\twidth() - s.width() - button.st->margin.right(),\n\t\tbutton.st->margin.top(),\n\t\ts.width(),\n\t\ts.height());\n\treturn r.contains(localPosition);\n}\n\nvoid InnerWidget::selectByMouse(QPoint globalPosition) {\n\tconst auto local = mapFromGlobal(globalPosition);\n\tif (updateReorderPinned(local)) {\n\t\treturn;\n\t}\n\t_mouseSelection = true;\n\t_lastMousePosition = globalPosition;\n\t_lastRowLocalMouseX = local.x();\n\n\tconst auto tagBase = QPoint(\n\t\t_searchTagsLeft,\n\t\tst::dialogsSearchTagBottom / 2);\n\tconst auto tagPoint = local - tagBase;\n\tconst auto inTags = _searchTags\n\t\t&& QRect(\n\t\t\ttagBase,\n\t\t\tQSize(width() - 2 * _searchTagsLeft, _searchTags->height())\n\t\t).contains(local);\n\tconst auto tagLink = inTags\n\t\t? _searchTags->lookupHandler(tagPoint)\n\t\t: nullptr;\n\tClickHandler::setActive(tagLink);\n\tif (inTags) {\n\t\tsetCursor(tagLink ? style::cur_pointer : style::cur_default);\n\t}\n\n\tconst auto w = width();\n\tconst auto mouseY = local.y();\n\tclearIrrelevantState();\n\tif (_state == WidgetState::Default) {\n\t\tconst auto offset = dialogsOffset();\n\t\tconst auto collapsedSelected = (mouseY >= 0\n\t\t\t&& mouseY < _collapsedRows.size() * st::dialogsImportantBarHeight)\n\t\t\t? (mouseY / st::dialogsImportantBarHeight)\n\t\t\t: -1;\n\t\tconst auto selected = (collapsedSelected >= 0)\n\t\t\t? nullptr\n\t\t\t: (mouseY >= offset)\n\t\t\t? _shownList->rowAtY(mouseY - offset)\n\t\t\t: nullptr;\n\t\tconst auto mappedY = selected ? mouseY - offset - selected->top() : 0;\n\t\tconst auto selectedTopicJump = selected\n\t\t\t&& selected->lookupIsInTopicJump(local.x(), mappedY);\n\t\tconst auto selectedRightButton = selected\n\t\t\t&& lookupIsInBotAppButton(selected, QPoint(local.x(), mappedY));\n\t\tif (_collapsedSelected != collapsedSelected\n\t\t\t|| _selected != selected\n\t\t\t|| _selectedTopicJump != selectedTopicJump\n\t\t\t|| _selectedRightButton != selectedRightButton) {\n\t\t\tupdateSelectedRow();\n\t\t\t_selected = selected;\n\t\t\t_selectedTopicJump = selectedTopicJump;\n\t\t\t_selectedRightButton = selectedRightButton;\n\t\t\t_collapsedSelected = collapsedSelected;\n\t\t\tupdateSelectedRow();\n\t\t\tsetCursor((_selected || _collapsedSelected >= 0)\n\t\t\t\t? style::cur_pointer\n\t\t\t\t: style::cur_default);\n\t\t}\n\t} else if (_state == WidgetState::Filtered) {\n\t\tauto wasSelected = isSelected();\n\t\tif (_hashtagResults.empty()) {\n\t\t\t_hashtagSelected = -1;\n\t\t\t_hashtagDeleteSelected = false;\n\t\t} else {\n\t\t\tauto skip = hashtagsOffset();\n\t\t\tauto hashtagSelected = (mouseY >= skip) ? ((mouseY - skip) / st::mentionHeight) : -1;\n\t\t\tif (hashtagSelected < 0 || hashtagSelected >= _hashtagResults.size()) {\n\t\t\t\thashtagSelected = -1;\n\t\t\t}\n\t\t\tif (_hashtagSelected != hashtagSelected) {\n\t\t\t\tupdateSelectedRow();\n\t\t\t\t_hashtagSelected = hashtagSelected;\n\t\t\t\tupdateSelectedRow();\n\t\t\t}\n\t\t\t_hashtagDeleteSelected = (_hashtagSelected >= 0) && (local.x() >= w - st::mentionHeight);\n\t\t}\n\t\tif (!_filterResults.empty()) {\n\t\t\tauto skip = filteredOffset();\n\t\t\tauto filteredSelected = (mouseY >= skip)\n\t\t\t\t? filteredIndex(mouseY - skip)\n\t\t\t\t: -1;\n\t\t\tif (filteredSelected < 0 || filteredSelected >= _filterResults.size()) {\n\t\t\t\tfilteredSelected = -1;\n\t\t\t}\n\t\t\tconst auto mappedY = (filteredSelected >= 0)\n\t\t\t\t? mouseY - skip - _filterResults[filteredSelected].top\n\t\t\t\t: 0;\n\t\t\tconst auto selectedTopicJump = (filteredSelected >= 0)\n\t\t\t\t&& _filterResults[filteredSelected].row->lookupIsInTopicJump(\n\t\t\t\t\tlocal.x(),\n\t\t\t\t\tmappedY);\n\t\t\tconst auto selectedRightButton = (filteredSelected >= 0)\n\t\t\t\t? lookupIsInBotAppButton(\n\t\t\t\t\t_filterResults[filteredSelected].row,\n\t\t\t\t\tQPoint(local.x(), mappedY))\n\t\t\t\t: _selectedRightButton;\n\t\t\tif (_filteredSelected != filteredSelected\n\t\t\t\t|| _selectedTopicJump != selectedTopicJump\n\t\t\t\t|| _selectedRightButton != selectedRightButton) {\n\t\t\t\tupdateSelectedRow();\n\t\t\t\t_filteredSelected = filteredSelected;\n\t\t\t\t_selectedTopicJump = selectedTopicJump;\n\t\t\t\t_selectedRightButton = selectedRightButton;\n\t\t\t\tupdateSelectedRow();\n\t\t\t}\n\t\t}\n\t\tif (!_peerSearchResults.empty()) {\n\t\t\tconst auto skip = peerSearchOffset();\n\t\t\tauto peerSearchSelected = (mouseY >= skip) ? ((mouseY - skip) / st::dialogsRowHeight) : -1;\n\t\t\tif (peerSearchSelected < 0 || peerSearchSelected >= _peerSearchResults.size()) {\n\t\t\t\tpeerSearchSelected = -1;\n\t\t\t}\n\t\t\tconst auto mappedY = (peerSearchSelected >= 0)\n\t\t\t\t? mouseY - skip - (peerSearchSelected * st::dialogsRowHeight)\n\t\t\t\t: 0;\n\t\t\tconst auto selectedRightButton = (peerSearchSelected >= 0)\n\t\t\t\t? (_peerSearchResults[peerSearchSelected]->sponsored\n\t\t\t\t\t&& lookupIsInRightButton(\n\t\t\t\t\t\t_peerSearchResults[peerSearchSelected]->sponsored->button,\n\t\t\t\t\t\tQPoint(local.x(), mappedY)))\n\t\t\t\t: _selectedRightButton;\n\t\t\tif (_peerSearchSelected != peerSearchSelected\n\t\t\t\t|| _selectedRightButton != selectedRightButton) {\n\t\t\t\tupdateSelectedRow();\n\t\t\t\t_peerSearchSelected = peerSearchSelected;\n\t\t\t\t_selectedRightButton = selectedRightButton;\n\t\t\t\tupdateSelectedRow();\n\t\t\t}\n\t\t}\n\t\tif (!_previewResults.empty()) {\n\t\t\tauto skip = previewOffset();\n\t\t\tauto previewSelected = (mouseY >= skip) ? ((mouseY - skip) / _st->height) : -1;\n\t\t\tif (previewSelected < 0 || previewSelected >= _previewResults.size()) {\n\t\t\t\tpreviewSelected = -1;\n\t\t\t}\n\t\t\tif (_previewSelected != previewSelected) {\n\t\t\t\tupdateSelectedRow();\n\t\t\t\t_previewSelected = previewSelected;\n\t\t\t\tupdateSelectedRow();\n\t\t\t}\n\t\t\tauto selectedMorePosts = false;\n\t\t\tconst auto from = skip - st::searchedBarHeight;\n\t\t\tif (mouseY <= skip && mouseY >= from) {\n\t\t\t\tconst auto left = width()\n\t\t\t\t\t- _morePostsWidth\n\t\t\t\t\t- 2 * st::searchedBarPosition.x();\n\t\t\t\tif (_morePostsWidth > 0 && local.x() >= left) {\n\t\t\t\t\tselectedMorePosts = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_selectedMorePosts != selectedMorePosts) {\n\t\t\t\tupdate(0, from, width(), st::searchedBarHeight);\n\t\t\t\t_selectedMorePosts = selectedMorePosts;\n\t\t\t}\n\t\t}\n\t\tif (!_searchResults.empty()) {\n\t\t\tauto skip = searchedOffset();\n\t\t\tauto searchedSelected = (mouseY >= skip) ? ((mouseY - skip) / _st->height) : -1;\n\t\t\tif (searchedSelected < 0 || searchedSelected >= _searchResults.size()) {\n\t\t\t\tsearchedSelected = -1;\n\t\t\t}\n\t\t\tif (_searchedSelected != searchedSelected) {\n\t\t\t\tupdateSelectedRow();\n\t\t\t\t_searchedSelected = searchedSelected;\n\t\t\t\tupdateSelectedRow();\n\t\t\t}\n\t\t\tauto selectedChatTypeFilter = false;\n\t\t\tconst auto from = skip - st::searchedBarHeight;\n\t\t\tif (hasChatTypeFilter() && mouseY <= skip && mouseY >= from) {\n\t\t\t\tconst auto left = width()\n\t\t\t\t\t- _chatTypeFilterWidth\n\t\t\t\t\t- 2 * st::searchedBarPosition.x();\n\t\t\t\tif (_chatTypeFilterWidth > 0 && local.x() >= left) {\n\t\t\t\t\tselectedChatTypeFilter = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_selectedChatTypeFilter != selectedChatTypeFilter) {\n\t\t\t\tupdate(0, from, width(), st::searchedBarHeight);\n\t\t\t\t_selectedChatTypeFilter = selectedChatTypeFilter;\n\t\t\t}\n\t\t}\n\t\tif (!inTags && wasSelected != isSelected()) {\n\t\t\tsetCursor(wasSelected ? style::cur_default : style::cur_pointer);\n\t\t}\n\t}\n}\n\nRowDescriptor InnerWidget::computeChatPreviewRow() const {\n\tauto result = computeChosenRow();\n\tif (const auto peer = result.key.peer()) {\n\t\tconst auto topicId = _pressedTopicJump\n\t\t\t? _pressedTopicJumpRootId\n\t\t\t: MsgId();\n\t\tconst auto sublistPeerId = _pressedTopicJump\n\t\t\t? _pressedSublistJumpPeerId\n\t\t\t: PeerId();\n\t\tif (const auto sublist = peer->monoforumSublistFor(sublistPeerId)) {\n\t\t\treturn { sublist, FullMsgId() };\n\t\t} else if (const auto topic = peer->forumTopicFor(topicId)) {\n\t\t\treturn { topic, FullMsgId() };\n\t\t}\n\t}\n\treturn { result.key, result.message.fullId };\n}\n\nvoid InnerWidget::processGlobalForceClick(QPoint globalPosition) {\n\tconst auto parent = parentWidget();\n\tif (_pressButton == Qt::LeftButton\n\t\t&& parent->rect().contains(parent->mapFromGlobal(globalPosition))) {\n\t\tshowChatPreview();\n\t}\n}\n\nvoid InnerWidget::mousePressEvent(QMouseEvent *e) {\n\tselectByMouse(e->globalPos());\n\n\t_pressButton = e->button();\n\tsetPressed(_selected, _selectedTopicJump, _selectedRightButton);\n\tsetCollapsedPressed(_collapsedSelected);\n\tsetHashtagPressed(_hashtagSelected);\n\t_hashtagDeletePressed = _hashtagDeleteSelected;\n\tsetFilteredPressed(\n\t\t_filteredSelected,\n\t\t_selectedTopicJump,\n\t\t_selectedRightButton);\n\tsetPeerSearchPressed(_peerSearchSelected, _selectedRightButton);\n\tsetPreviewPressed(_previewSelected);\n\tsetSearchedPressed(_searchedSelected);\n\t_pressedMorePosts = _selectedMorePosts;\n\t_pressedChatTypeFilter = _selectedChatTypeFilter;\n\n\tconst auto alt = (e->modifiers() & Qt::AltModifier);\n\tif (alt && showChatPreview()) {\n\t\treturn;\n\t} else if (!alt && isUserpicPress()) {\n\t\tscheduleChatPreview(e->globalPos());\n\t}\n\n\tif (base::in_range(_collapsedSelected, 0, _collapsedRows.size())) {\n\t\tauto row = &_collapsedRows[_collapsedSelected]->row;\n\t\trow->addRipple(e->pos(), QSize(width(), st::dialogsImportantBarHeight), [this, index = _collapsedSelected] {\n\t\t\tupdate(0, (index * st::dialogsImportantBarHeight), width(), st::dialogsImportantBarHeight);\n\t\t});\n\t} else if (_pressed) {\n\t\tauto row = _pressed;\n\t\tconst auto weakThis = base::make_weak(this);\n\t\tconst auto weakEntry = base::make_weak(row->entry());\n\t\tconst auto updateCallback = [weakThis, weakEntry] {\n\t\t\tconst auto that = weakThis.get();\n\t\t\tif (!that || !that->_pinnedShiftAnimation.animating()) {\n\t\t\t\tif (const auto entry = weakEntry.get()) {\n\t\t\t\t\tentry->updateChatListEntry();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tconst auto origin = e->pos()\n\t\t\t- QPoint(0, dialogsOffset() + _pressed->top());\n\t\tif ((_pressButton == Qt::MiddleButton)\n\t\t\t&& addQuickActionRipple(row, updateCallback)) {\n\t\t} else if (addRightButtonRipple(origin, updateCallback)) {\n\t\t} else if (_pressedTopicJump) {\n\t\t\trow->addTopicJumpRipple(\n\t\t\t\torigin,\n\t\t\t\t_topicJumpCache.get(),\n\t\t\t\tupdateCallback);\n\t\t} else {\n\t\t\trow->clearTopicJumpRipple();\n\t\t\trow->addRipple(\n\t\t\t\torigin,\n\t\t\t\tQSize(width(), _pressed->height()),\n\t\t\t\tupdateCallback);\n\t\t}\n\t\t_dragStart = e->pos();\n\t} else if (base::in_range(_hashtagPressed, 0, _hashtagResults.size()) && !_hashtagDeletePressed) {\n\t\tauto row = &_hashtagResults[_hashtagPressed]->row;\n\t\tconst auto origin = e->pos() - QPoint(0, hashtagsOffset() + _hashtagPressed * st::mentionHeight);\n\t\trow->addRipple(origin, QSize(width(), st::mentionHeight), [this, index = _hashtagPressed] {\n\t\t\tupdate(0, hashtagsOffset() + index * st::mentionHeight, width(), st::mentionHeight);\n\t\t});\n\t} else if (base::in_range(_filteredPressed, 0, _filterResults.size())) {\n\t\tconst auto &result = _filterResults[_filteredPressed];\n\t\tconst auto row = result.row;\n\t\tconst auto filterId = _filterId;\n\t\tconst auto origin = e->pos()\n\t\t\t- QPoint(0, filteredOffset() + result.top);\n\t\tconst auto updateCallback = [=] { repaintDialogRow(filterId, row); };\n\t\tif (addRightButtonRipple(origin, updateCallback)) {\n\t\t} else if (_pressedTopicJump) {\n\t\t\trow->addTopicJumpRipple(\n\t\t\t\torigin,\n\t\t\t\t_topicJumpCache.get(),\n\t\t\t\tupdateCallback);\n\t\t} else {\n\t\t\trow->clearTopicJumpRipple();\n\t\t\trow->addRipple(\n\t\t\t\torigin,\n\t\t\t\tQSize(width(), row->height()),\n\t\t\t\tupdateCallback);\n\t\t}\n\t} else if (base::in_range(_peerSearchPressed, 0, _peerSearchResults.size())) {\n\t\tauto &result = _peerSearchResults[_peerSearchPressed];\n\t\tconst auto row = &result->row;\n\t\tconst auto origin = e->pos()\n\t\t\t- QPoint(0, peerSearchOffset() + _peerSearchPressed * st::dialogsRowHeight);\n\t\tconst auto updateCallback = [this, peer = result->peer] {\n\t\t\tupdateSearchResult(peer);\n\t\t};\n\t\tif (addRightButtonRipple(origin, updateCallback)) {\n\t\t} else {\n\t\t\trow->addRipple(\n\t\t\t\torigin,\n\t\t\t\tQSize(width(), st::dialogsRowHeight),\n\t\t\t\tupdateCallback);\n\t\t}\n\t} else if (base::in_range(_searchedPressed, 0, _searchResults.size())) {\n\t\tauto &row = _searchResults[_searchedPressed];\n\t\trow->addRipple(\n\t\t\te->pos() - QPoint(0, searchedOffset() + _searchedPressed * _st->height),\n\t\t\tQSize(width(), _st->height),\n\t\t\trow->repaint());\n\t}\n\tClickHandler::pressed();\n\tif (anim::Disabled()\n\t\t&& !_chatPreviewScheduled\n\t\t&& (!_pressed || !_pressed->entry()->isPinnedDialog(_filterId))) {\n\t\tmousePressReleased(e->globalPos(), e->button(), e->modifiers());\n\t}\n}\n\nbool InnerWidget::addRightButtonRipple(QPoint origin, Fn<void()> updateCallback) {\n\tif (!(_pressedRightButton && _pressedRightButtonData)) {\n\t\treturn false;\n\t}\n\tconst auto size = _pressedRightButtonData->bg.size()\n\t\t/ style::DevicePixelRatio();\n\tif (!_pressedRightButtonData->ripple) {\n\t\t_pressedRightButtonData->ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t_pressedRightButtonData->st->button.ripple,\n\t\t\tUi::RippleAnimation::RoundRectMask(size, size.height() / 2),\n\t\t\tstd::move(updateCallback));\n\t}\n\tconst auto shift = QPoint(\n\t\twidth() - size.width() - _pressedRightButtonData->st->margin.right(),\n\t\t_pressedRightButtonData->st->margin.top());\n\t_pressedRightButtonData->ripple->add(origin - shift);\n\treturn true;\n}\n\nbool InnerWidget::addQuickActionRipple(\n\t\tnot_null<Row*> row,\n\t\tFn<void()> updateCallback) {\n\tif (_activeQuickAction) {\n\t\treturn false;\n\t}\n\tconst auto action = Core::App().settings().quickDialogAction();\n\tif (action == Dialogs::Ui::QuickDialogAction::Disabled) {\n\t\treturn false;\n\t}\n\tconst auto history = row->history();\n\tif (!history) {\n\t\treturn false;\n\t}\n\tconst auto type = ResolveQuickDialogLabel(history, action, _filterId);\n\tif (type == Dialogs::Ui::QuickDialogActionLabel::Disabled) {\n\t\treturn false;\n\t}\n\tconst auto key = history->peer->id.value;\n\tconst auto context = ensureQuickAction(key);\n\tif (context->data) {\n\t\treturn false;\n\t}\n\n\tauto name = ResolveQuickDialogLottieIconName(type);\n\tconst auto rowHeight = row->height();\n\tcontext->icon = Lottie::MakeIcon({\n\t\t.name = std::move(name),\n\t\t.sizeOverride = Size(st::dialogsQuickActionSize),\n\t});\n\tcontext->action = action;\n\tcontext->icon->jumpTo(context->icon->framesCount() - 1, [=] {\n\t\tconst auto size = QSize(\n\t\t\tst::dialogsQuickActionRippleSize,\n\t\t\trowHeight);\n\t\tif (!context->ripple) {\n\t\t\tcontext->ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\tUi::RippleAnimation::RectMask(size),\n\t\t\t\tupdateCallback);\n\t\t}\n\t\tif (!context->rippleFg) {\n\t\t\tcontext->rippleFg = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\tUi::RippleAnimation::MaskByDrawer(\n\t\t\t\t\tsize,\n\t\t\t\t\ttrue,\n\t\t\t\t\t[&](QPainter &p) {\n\t\t\t\t\t\tp.setCompositionMode(\n\t\t\t\t\t\t\tQPainter::CompositionMode_Source);\n\t\t\t\t\t\tp.fillRect(Rect(size), Qt::transparent);\n\t\t\t\t\t\tDrawQuickAction(\n\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\tRect(size),\n\t\t\t\t\t\t\tcontext->icon.get(),\n\t\t\t\t\t\t\tResolveQuickDialogLabel(\n\t\t\t\t\t\t\t\thistory,\n\t\t\t\t\t\t\t\taction,\n\t\t\t\t\t\t\t\t_filterId));\n\t\t\t\t\t}),\n\t\t\t\tstd::move(updateCallback));\n\t\t}\n\t\tcontext->ripple->add(QPoint(size.width() / 2, size.height() / 2));\n\t\tcontext->rippleFg->add(QPoint(size.width() / 2, size.height() / 2));\n\t});\n\n\treturn true;\n}\n\nconst std::vector<Key> &InnerWidget::pinnedChatsOrder() const {\n\tconst auto owner = &session().data();\n\treturn _savedSublists\n\t\t? owner->pinnedChatsOrder(_savedSublists)\n\t\t: _openedForum\n\t\t? owner->pinnedChatsOrder(_openedForum)\n\t\t: _filterId\n\t\t? owner->pinnedChatsOrder(_filterId)\n\t\t: owner->pinnedChatsOrder(_openedFolder);\n}\n\nvoid InnerWidget::checkReorderPinnedStart(QPoint localPosition) {\n\tif (!_pressed\n\t\t|| _dragging\n\t\t|| (_state != WidgetState::Default)\n\t\t|| _pressedRightButtonData) {\n\t\treturn;\n\t} else if (qAbs(localPosition.y() - _dragStart.y())\n\t\t< style::ConvertScale(kStartReorderThreshold)) {\n\t\treturn;\n\t}\n\tif ((_pressButton == Qt::MiddleButton)\n\t\t&& (Core::App().settings().quickDialogAction()\n\t\t\t!= Dialogs::Ui::QuickDialogAction::Disabled)) {\n\t\treturn;\n\t}\n\tunfreezeShownList(true);\n\t_dragging = _pressed;\n\tstartReorderPinned(localPosition);\n}\n\nvoid InnerWidget::startReorderPinned(QPoint localPosition) {\n\tExpects(_dragging != nullptr);\n\n\tcancelChatPreview();\n\tif (updateReorderIndexGetCount() < 2) {\n\t\t_dragging = nullptr;\n\t} else {\n\t\tconst auto &order = pinnedChatsOrder();\n\t\t_pinnedOnDragStart = base::flat_set<Key>{\n\t\t\torder.begin(),\n\t\t\torder.end()\n\t\t};\n\t\t_pinnedRows[_draggingIndex].yadd = anim::value(0, localPosition.y() - _dragStart.y());\n\t\t_pinnedRows[_draggingIndex].animStartTime = crl::now();\n\t\t_pinnedShiftAnimation.start();\n\t}\n}\n\nint InnerWidget::countPinnedIndex(Row *ofRow) {\n\tif (!ofRow || !ofRow->entry()->isPinnedDialog(_filterId)) {\n\t\treturn -1;\n\t}\n\tauto result = 0;\n\tfor (const auto &row : *_shownList) {\n\t\tif (row->entry()->fixedOnTopIndex()) {\n\t\t\tcontinue;\n\t\t} else if (!row->entry()->isPinnedDialog(_filterId)) {\n\t\t\tbreak;\n\t\t} else if (row == ofRow) {\n\t\t\treturn result;\n\t\t}\n\t\t++result;\n\t}\n\treturn -1;\n}\n\nvoid InnerWidget::savePinnedOrder() {\n\tif (_savedSublists && _savedSublists->parentChat()) {\n\t\treturn;\n\t}\n\tconst auto &newOrder = pinnedChatsOrder();\n\tif (newOrder.size() != _pinnedOnDragStart.size()) {\n\t\treturn; // Something has changed in the set of pinned chats.\n\t}\n\tfor (const auto &key : newOrder) {\n\t\tif (!_pinnedOnDragStart.contains(key)) {\n\t\t\treturn; // Something has changed in the set of pinned chats.\n\t\t}\n\t}\n\tif (_savedSublists) {\n\t\tsession().api().savePinnedOrder(&session().data().savedMessages());\n\t} else if (_openedForum) {\n\t\tsession().api().savePinnedOrder(_openedForum);\n\t} else if (_filterId) {\n\t\tApi::SaveNewFilterPinned(&session(), _filterId);\n\t} else {\n\t\tsession().api().savePinnedOrder(_openedFolder);\n\t}\n}\n\nvoid InnerWidget::finishReorderPinned() {\n\tauto wasDragging = (_dragging != nullptr);\n\tif (wasDragging) {\n\t\tsavePinnedOrder();\n\t\t_dragging = nullptr;\n\t\t_touchDragStartGlobal = {};\n\t\t_qdragging = nullptr;\n\t}\n\n\t_draggingIndex = -1;\n\tif (!_pinnedShiftAnimation.animating()) {\n\t\t_pinnedRows.clear();\n\t\t_aboveIndex = -1;\n\t}\n\tif (wasDragging) {\n\t\t_draggingScroll.cancel();\n\t}\n}\n\nbool InnerWidget::finishReorderOnRelease() {\n\tif (!_dragging) {\n\t\treturn false;\n\t}\n\tupdateReorderIndexGetCount();\n\tif (_draggingIndex >= 0) {\n\t\t_pinnedRows[_draggingIndex].yadd.start(0.);\n\t\t_pinnedRows[_draggingIndex].animStartTime = crl::now();\n\t\tif (!_pinnedShiftAnimation.animating()) {\n\t\t\t_pinnedShiftAnimation.start();\n\t\t}\n\t}\n\tfinishReorderPinned();\n\treturn true;\n}\n\nvoid InnerWidget::stopReorderPinned() {\n\t_pinnedShiftAnimation.stop();\n\tfinishReorderPinned();\n}\n\nint InnerWidget::updateReorderIndexGetCount() {\n\tauto index = countPinnedIndex(_dragging);\n\tif (index < 0) {\n\t\tfinishReorderPinned();\n\t\treturn 0;\n\t}\n\n\tconst auto count = Dialogs::PinnedDialogsCount(_filterId, _shownList);\n\tAssert(index < count);\n\tif (count < 2) {\n\t\tstopReorderPinned();\n\t\treturn 0;\n\t}\n\n\t_draggingIndex = index;\n\t_aboveIndex = _draggingIndex;\n\twhile (count > _pinnedRows.size()) {\n\t\t_pinnedRows.emplace_back();\n\t}\n\twhile (count < _pinnedRows.size()) {\n\t\t_pinnedRows.pop_back();\n\t}\n\treturn count;\n}\n\nbool InnerWidget::updateReorderPinned(QPoint localPosition) {\n\tcheckReorderPinnedStart(localPosition);\n\tauto pinnedCount = updateReorderIndexGetCount();\n\tif (pinnedCount < 2) {\n\t\treturn false;\n\t}\n\n\tconst auto draggingHeight = _dragging->height();\n\tauto yaddWas = _pinnedRows[_draggingIndex].yadd.current();\n\tauto shift = 0;\n\tauto shiftHeight = 0;\n\tauto now = crl::now();\n\tif (_dragStart.y() > localPosition.y() && _draggingIndex > 0) {\n\t\tshift = -floorclamp(_dragStart.y() - localPosition.y() + (draggingHeight / 2), draggingHeight, 0, _draggingIndex);\n\n\t\tfor (auto from = _draggingIndex, to = _draggingIndex + shift; from > to; --from) {\n\t\t\t_shownList->movePinned(_dragging, -1);\n\t\t\tstd::swap(_pinnedRows[from], _pinnedRows[from - 1]);\n\t\t\t_pinnedRows[from].yadd = anim::value(_pinnedRows[from].yadd.current() - draggingHeight, 0);\n\t\t\t_pinnedRows[from].animStartTime = now;\n\t\t\tshiftHeight -= (*(_shownList->cbegin() + from))->height();\n\t\t}\n\t} else if (_dragStart.y() < localPosition.y() && _draggingIndex + 1 < pinnedCount) {\n\t\tshift = floorclamp(localPosition.y() - _dragStart.y() + (draggingHeight / 2), draggingHeight, 0, pinnedCount - _draggingIndex - 1);\n\n\t\tfor (auto from = _draggingIndex, to = _draggingIndex + shift; from < to; ++from) {\n\t\t\t_shownList->movePinned(_dragging, 1);\n\t\t\tstd::swap(_pinnedRows[from], _pinnedRows[from + 1]);\n\t\t\t_pinnedRows[from].yadd = anim::value(_pinnedRows[from].yadd.current() + draggingHeight, 0);\n\t\t\t_pinnedRows[from].animStartTime = now;\n\t\t\tshiftHeight += (*(_shownList->cbegin() + from))->height();\n\t\t}\n\t}\n\tif (shift) {\n\t\t_draggingIndex += shift;\n\t\t_aboveIndex = _draggingIndex;\n\t\t_dragStart.setY(_dragStart.y() + shiftHeight);\n\t\tif (!_pinnedShiftAnimation.animating()) {\n\t\t\t_pinnedShiftAnimation.start();\n\t\t}\n\t}\n\t_aboveTopShift = qCeil(_pinnedRows[_aboveIndex].yadd.current());\n\t_pinnedRows[_draggingIndex].yadd = anim::value(\n\t\tyaddWas - shiftHeight,\n\t\tlocalPosition.y() - _dragStart.y());\n\tif (!_pinnedRows[_draggingIndex].animStartTime) {\n\t\t_pinnedRows[_draggingIndex].yadd.finish();\n\t}\n\tpinnedShiftAnimationCallback(now);\n\n\tconst auto delta = [&] {\n\t\tif (localPosition.y() < _visibleTop) {\n\t\t\treturn localPosition.y() - _visibleTop;\n\t\t} else if ((localPosition.y() > _visibleBottom)\n\t\t\t&& (_savedSublists\n\t\t\t\t|| _openedFolder\n\t\t\t\t|| _openedForum\n\t\t\t\t|| _filterId)) {\n\t\t\treturn localPosition.y() - _visibleBottom;\n\t\t}\n\t\treturn 0;\n\t}();\n\n\t_draggingScroll.checkDeltaScroll(delta);\n\treturn true;\n}\n\nbool InnerWidget::pinnedShiftAnimationCallback(crl::time now) {\n\tif (anim::Disabled()) {\n\t\tnow += st::stickersRowDuration;\n\t}\n\n\tauto animating = false;\n\tauto updateMin = -1;\n\tauto updateMax = 0;\n\tfor (auto i = 0, l = static_cast<int>(_pinnedRows.size()); i != l; ++i) {\n\t\tauto start = _pinnedRows[i].animStartTime;\n\t\tif (start) {\n\t\t\tif (updateMin < 0) updateMin = i;\n\t\t\tupdateMax = i;\n\t\t\tif (start + st::stickersRowDuration > now && now >= start) {\n\t\t\t\t_pinnedRows[i].yadd.update(float64(now - start) / st::stickersRowDuration, anim::sineInOut);\n\t\t\t\tanimating = true;\n\t\t\t} else {\n\t\t\t\t_pinnedRows[i].yadd.finish();\n\t\t\t\t_pinnedRows[i].animStartTime = 0;\n\t\t\t}\n\t\t}\n\t}\n\tupdateReorderIndexGetCount();\n\tif (_draggingIndex >= 0) {\n\t\tif (updateMin < 0 || updateMin > _draggingIndex) {\n\t\t\tupdateMin = _draggingIndex;\n\t\t}\n\t\tif (updateMax < _draggingIndex) updateMax = _draggingIndex;\n\t}\n\tif (updateMin >= 0) {\n\t\tconst auto minHeight = _st->height;\n\t\tconst auto maxHeight = st::taggedForumDialogRow.height;\n\t\tauto top = pinnedOffset();\n\t\tauto updateFrom = top + minHeight * (updateMin - 1);\n\t\tauto updateHeight = maxHeight * (updateMax - updateMin + 3);\n\t\tif (base::in_range(_aboveIndex, 0, _pinnedRows.size())) {\n\t\t\t// Always include currently dragged chat in its current and old positions.\n\t\t\tauto aboveRowBottom = top + (_aboveIndex + 1) * maxHeight;\n\t\t\tauto aboveTopShift = qCeil(_pinnedRows[_aboveIndex].yadd.current());\n\t\t\taccumulate_max(updateHeight, (aboveRowBottom - updateFrom) + _aboveTopShift);\n\t\t\taccumulate_max(updateHeight, (aboveRowBottom - updateFrom) + aboveTopShift);\n\t\t\t_aboveTopShift = aboveTopShift;\n\t\t}\n\t\tupdate(0, updateFrom, width(), updateHeight);\n\t}\n\tif (!animating) {\n\t\t_aboveIndex = _draggingIndex;\n\t}\n\treturn animating;\n}\n\nvoid InnerWidget::mouseReleaseEvent(QMouseEvent *e) {\n\tmousePressReleased(e->globalPos(), e->button(), e->modifiers());\n}\n\nvoid InnerWidget::mousePressReleased(\n\t\tQPoint globalPosition,\n\t\tQt::MouseButton button,\n\t\tQt::KeyboardModifiers modifiers) {\n\tif (_chatPreviewScheduled) {\n\t\t_controller->cancelScheduledPreview();\n\t}\n\tconst auto pressButton = base::take(_pressButton);\n\n\tconst auto wasDragging = finishReorderOnRelease();\n\n\tauto collapsedPressed = _collapsedPressed;\n\tsetCollapsedPressed(-1);\n\tconst auto pressedTopicRootId = _pressedTopicJumpRootId;\n\tconst auto pressedSublistPeerId = _pressedSublistJumpPeerId;\n\tconst auto pressedTopicJump = _pressedTopicJump;\n\tconst auto pressedRightButton = _pressedRightButton;\n\tauto pressed = _pressed;\n\tclearPressed();\n\tauto hashtagPressed = _hashtagPressed;\n\tsetHashtagPressed(-1);\n\tauto hashtagDeletePressed = _hashtagDeletePressed;\n\t_hashtagDeletePressed = false;\n\tauto filteredPressed = _filteredPressed;\n\tsetFilteredPressed(-1, false, false);\n\tauto peerSearchPressed = _peerSearchPressed;\n\tsetPeerSearchPressed(-1, false);\n\tauto previewPressed = _previewPressed;\n\tsetPreviewPressed(-1);\n\tauto searchedPressed = _searchedPressed;\n\tsetSearchedPressed(-1);\n\tconst auto pressedMorePosts = _pressedMorePosts;\n\t_pressedMorePosts = false;\n\tconst auto pressedChatTypeFilter = _pressedChatTypeFilter;\n\t_pressedChatTypeFilter = false;\n\tif (wasDragging) {\n\t\tselectByMouse(globalPosition);\n\t}\n\tif (_pressedRightButtonData && _pressedRightButtonData->ripple) {\n\t\t_pressedRightButtonData->ripple->lastStop();\n\t}\n\tif ((pressButton == Qt::MiddleButton)\n\t\t&& _activeQuickAction\n\t\t&& pressed\n\t\t&& !_activeQuickAction->data) {\n\t\tif (const auto history = pressed->history()) {\n\t\t\tconst auto raw = _activeQuickAction.get();\n\t\t\tif (raw->ripple) {\n\t\t\t\traw->ripple->lastStop();\n\t\t\t}\n\t\t\tif (raw->rippleFg) {\n\t\t\t\traw->rippleFg->lastStop();\n\t\t\t}\n\n\t\t\tif (pressed == _selected) {\n\t\t\t\tPerformQuickDialogAction(\n\t\t\t\t\t_controller,\n\t\t\t\t\thistory->peer,\n\t\t\t\t\traw->action,\n\t\t\t\t\t_filterId);\n\t\t\t}\n\t\t\tdeactivateQuickAction();\n\t\t}\n\t}\n\tupdateSelectedRow();\n\tif (!wasDragging && button == Qt::LeftButton) {\n\t\tif ((collapsedPressed >= 0 && collapsedPressed == _collapsedSelected)\n\t\t\t|| (pressed\n\t\t\t\t&& pressed == _selected\n\t\t\t\t&& pressedTopicJump == _selectedTopicJump\n\t\t\t\t&& pressedRightButton == _selectedRightButton)\n\t\t\t|| (hashtagPressed >= 0\n\t\t\t\t&& hashtagPressed == _hashtagSelected\n\t\t\t\t&& hashtagDeletePressed == _hashtagDeleteSelected)\n\t\t\t|| (filteredPressed >= 0\n\t\t\t\t&& filteredPressed == _filteredSelected\n\t\t\t\t&& pressedRightButton == _selectedRightButton)\n\t\t\t|| (peerSearchPressed >= 0\n\t\t\t\t&& peerSearchPressed == _peerSearchSelected\n\t\t\t\t&& pressedRightButton == _selectedRightButton)\n\t\t\t|| (previewPressed >= 0\n\t\t\t\t&& previewPressed == _previewSelected)\n\t\t\t|| (searchedPressed >= 0\n\t\t\t\t&& searchedPressed == _searchedSelected)\n\t\t\t|| (pressedMorePosts\n\t\t\t\t&& pressedMorePosts == _selectedMorePosts)\n\t\t\t|| (pressedChatTypeFilter\n\t\t\t\t&& pressedChatTypeFilter == _selectedChatTypeFilter)) {\n\t\t\tif (pressedRightButton && (pressed || filteredPressed >= 0)) {\n\t\t\t\tconst auto &row = pressed\n\t\t\t\t\t? pressed\n\t\t\t\t\t: _filterResults[filteredPressed].row.get();\n\t\t\t\tif (const auto user = MaybeBotWithApp(row)) {\n\t\t\t\t\t_openBotMainAppRequests.fire(peerToUser(user->id));\n\t\t\t\t}\n\t\t\t} else if (pressedRightButton && peerSearchPressed >= 0) {\n\t\t\t\tshowSponsoredMenu(peerSearchPressed, globalPosition);\n\t\t\t} else {\n\t\t\t\tchooseRow(\n\t\t\t\t\tmodifiers,\n\t\t\t\t\tpressedTopicRootId,\n\t\t\t\t\tpressedSublistPeerId);\n\t\t\t}\n\t\t}\n\t}\n\tif (auto activated = ClickHandler::unpressed()) {\n\t\tActivateClickHandler(window(), activated, ClickContext{\n\t\t\tbutton,\n\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.sessionWindow = _controller,\n\t\t\t}) });\n\t}\n}\n\nvoid InnerWidget::setCollapsedPressed(int pressed) {\n\tif (_collapsedPressed != pressed) {\n\t\tif (_collapsedPressed >= 0) {\n\t\t\t_collapsedRows[_collapsedPressed]->row.stopLastRipple();\n\t\t}\n\t\t_collapsedPressed = pressed;\n\t}\n}\n\nvoid InnerWidget::setPressed(\n\t\tRow *pressed,\n\t\tbool pressedTopicJump,\n\t\tbool pressedRightButton) {\n\tif ((_pressed != pressed)\n\t\t|| (pressed && _pressedTopicJump != pressedTopicJump)\n\t\t|| (pressed && _pressedRightButton != pressedRightButton)) {\n\t\tif (_pressed) {\n\t\t\t_pressed->stopLastRipple();\n\t\t}\n\t\tif (_pressedRightButtonData && _pressedRightButtonData->ripple) {\n\t\t\t_pressedRightButtonData->ripple->lastStop();\n\t\t}\n\t\t_pressed = pressed;\n\t\tif (pressed || !pressedTopicJump || !pressedRightButton) {\n\t\t\t_pressedTopicJump = pressedTopicJump;\n\t\t\t_pressedRightButton = pressedRightButton;\n\t\t\tif (pressedRightButton) {\n\t\t\t\tif (const auto user = MaybeBotWithApp(pressed)) {\n\t\t\t\t\tconst auto it = _rightButtons.find(user->id);\n\t\t\t\t\tif (it != _rightButtons.end()) {\n\t\t\t\t\t\t_pressedRightButtonData = &(it->second);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto history = pressedTopicJump\n\t\t\t\t? pressed->history()\n\t\t\t\t: nullptr;\n\t\t\tconst auto item = history ? history->chatListMessage() : nullptr;\n\t\t\t_pressedTopicJumpRootId = item ? item->topicRootId() : MsgId();\n\t\t\t_pressedSublistJumpPeerId = item\n\t\t\t\t? item->sublistPeerId()\n\t\t\t\t: PeerId();\n\t\t}\n\t}\n}\n\nvoid InnerWidget::clearPressed() {\n\tsetPressed(nullptr, false, false);\n}\n\nvoid InnerWidget::setHashtagPressed(int pressed) {\n\tif (base::in_range(_hashtagPressed, 0, _hashtagResults.size())) {\n\t\t_hashtagResults[_hashtagPressed]->row.stopLastRipple();\n\t}\n\t_hashtagPressed = pressed;\n}\n\nvoid InnerWidget::setFilteredPressed(\n\t\tint pressed,\n\t\tbool pressedTopicJump,\n\t\tbool pressedRightButton) {\n\tif (_filteredPressed != pressed\n\t\t|| (pressed >= 0 && _pressedTopicJump != pressedTopicJump)\n\t\t|| (pressed >= 0 && _pressedRightButton != pressedRightButton)) {\n\t\tif (base::in_range(_filteredPressed, 0, _filterResults.size())) {\n\t\t\t_filterResults[_filteredPressed].row->stopLastRipple();\n\t\t}\n\t\tif (_pressedRightButtonData && _pressedRightButtonData->ripple) {\n\t\t\t_pressedRightButtonData->ripple->lastStop();\n\t\t}\n\t\t_filteredPressed = pressed;\n\t\tif (pressed >= 0 || !pressedTopicJump || !pressedRightButton) {\n\t\t\t_pressedTopicJump = pressedTopicJump;\n\t\t\t_pressedRightButton = pressedRightButton;\n\t\t\tif (pressed >= 0 && pressedRightButton) {\n\t\t\t\tconst auto &row = _filterResults[pressed].row;\n\t\t\t\tif (const auto history = row->history()) {\n\t\t\t\t\tconst auto it = _rightButtons.find(history->peer->id);\n\t\t\t\t\tif (it != _rightButtons.end()) {\n\t\t\t\t\t\t_pressedRightButtonData = &(it->second);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto history = pressedTopicJump\n\t\t\t\t? _filterResults[pressed].row->history()\n\t\t\t\t: nullptr;\n\t\t\tconst auto item = history ? history->chatListMessage() : nullptr;\n\t\t\t_pressedTopicJumpRootId = item ? item->topicRootId() : MsgId();\n\t\t\t_pressedSublistJumpPeerId = item\n\t\t\t\t? item->sublistPeerId()\n\t\t\t\t: PeerId();\n\t\t}\n\t}\n}\n\nvoid InnerWidget::setPeerSearchPressed(int pressed, bool pressedRightButton) {\n\tif (_peerSearchPressed != pressed\n\t\t|| (pressed >= 0 && _pressedRightButton != pressedRightButton)) {\n\t\tif (base::in_range(_peerSearchPressed, 0, _peerSearchResults.size())) {\n\t\t\t_peerSearchResults[_peerSearchPressed]->row.stopLastRipple();\n\t\t}\n\t\tif (_pressedRightButtonData && _pressedRightButtonData->ripple) {\n\t\t\t_pressedRightButtonData->ripple->lastStop();\n\t\t}\n\t\t_peerSearchPressed = pressed;\n\t\tif (pressed >= 0 || !pressedRightButton) {\n\t\t\t_pressedRightButton = pressedRightButton;\n\t\t\tif (pressed >= 0 && pressedRightButton) {\n\t\t\t\tconst auto &entry = _peerSearchResults[pressed];\n\t\t\t\tif (entry->sponsored) {\n\t\t\t\t\t_pressedRightButtonData = &entry->sponsored->button;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid InnerWidget::setPreviewPressed(int pressed) {\n\tif (base::in_range(_previewPressed, 0, _previewResults.size())) {\n\t\t_previewResults[_previewPressed]->stopLastRipple();\n\t}\n\t_previewPressed = pressed;\n}\n\nvoid InnerWidget::setSearchedPressed(int pressed) {\n\tif (base::in_range(_searchedPressed, 0, _searchResults.size())) {\n\t\t_searchResults[_searchedPressed]->stopLastRipple();\n\t}\n\t_searchedPressed = pressed;\n}\n\nvoid InnerWidget::resizeEvent(QResizeEvent *e) {\n\tif (_searchTags) {\n\t\t_searchTags->resizeToWidth(width() - 2 * _searchTagsLeft);\n\t}\n\tresizeEmpty();\n\tmoveSearchIn();\n}\n\nvoid InnerWidget::moveSearchIn() {\n\tif (!_searchIn) {\n\t\treturn;\n\t}\n\tconst auto searchInWidth = std::max(\n\t\twidth(),\n\t\tst::columnMinimalWidthLeft - _narrowWidth);\n\t_searchIn->resizeToWidth(searchInWidth);\n\t_searchIn->moveToLeft(0, searchInChatOffset());\n}\n\nvoid InnerWidget::dialogRowReplaced(\n\t\tRow *oldRow,\n\t\tRow *newRow) {\n\tauto found = false;\n\tif (_state == WidgetState::Filtered) {\n\t\tauto top = 0;\n\t\tfor (auto i = _filterResults.begin(); i != _filterResults.end();) {\n\t\t\tif (i->row == oldRow) { // this row is shown in filtered and maybe is in contacts!\n\t\t\t\tfound = true;\n\t\t\t\ttop = i->top;\n\t\t\t\tif (!newRow) {\n\t\t\t\t\ti = _filterResults.erase(i);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\ti->row = newRow;\n\t\t\t}\n\t\t\tif (found) {\n\t\t\t\ti->top = top;\n\t\t\t\ttop += i->row->height();\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t}\n\tif (_selected == oldRow) {\n\t\t_selected = newRow;\n\t}\n\tif (_pressed == oldRow) {\n\t\tif (newRow) {\n\t\t\tsetPressed(newRow, _pressedTopicJump, _pressedRightButton);\n\t\t} else {\n\t\t\tclearPressed();\n\t\t}\n\t}\n\tif (_dragging == oldRow) {\n\t\tif (newRow) {\n\t\t\t_dragging = newRow;\n\t\t} else {\n\t\t\tstopReorderPinned();\n\t\t}\n\t}\n\tif (found) {\n\t\trefresh();\n\t}\n}\n\nvoid InnerWidget::handleChatListEntryRefreshes() {\n\tusing Event = Data::Session::ChatListEntryRefresh;\n\tsession().data().chatListEntryRefreshes(\n\t) | rpl::filter([=](const Event &event) {\n\t\tif (event.filterId != _filterId) {\n\t\t\treturn false;\n\t\t} else if (const auto topic = event.key.topic()) {\n\t\t\treturn (topic->forum() == _openedForum);\n\t\t} else if (const auto sublist = event.key.sublist()) {\n\t\t\treturn sublist->parent() == _savedSublists;\n\t\t} else {\n\t\t\treturn !_openedForum;\n\t\t}\n\t}) | rpl::on_next([=](const Event &event) {\n\t\tconst auto offset = dialogsOffset();\n\t\tconst auto from = offset + event.moved.from;\n\t\tconst auto to = offset + event.moved.to;\n\t\tconst auto &key = event.key;\n\t\tconst auto entry = key.entry();\n\n\t\t// Don't jump in chats list scroll position while dragging.\n\t\tif (!_dragging\n\t\t\t&& (from != to)\n\t\t\t&& (_state == WidgetState::Default)\n\t\t\t&& (key.topic()\n\t\t\t\t? (key.topic()->forum() == _openedForum)\n\t\t\t\t: key.sublist()\n\t\t\t\t? (key.sublist()->parent() == _savedSublists)\n\t\t\t\t: (entry->folder() == _openedFolder))) {\n\t\t\t_dialogMoved.fire({ from, to });\n\t\t}\n\n\t\tif (event.existenceChanged) {\n\t\t\tif (!entry->inChatList()) {\n\t\t\t\tif (key == _menuRow.key && _menu) {\n\t\t\t\t\tInvokeQueued(this, [=] { _menu = nullptr; });\n\t\t\t\t}\n\t\t\t\tif (_selected && _selected->key() == key) {\n\t\t\t\t\t_selected = nullptr;\n\t\t\t\t}\n\t\t\t\tif (_pressed && _pressed->key() == key) {\n\t\t\t\t\tclearPressed();\n\t\t\t\t}\n\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\t_filterResults,\n\t\t\t\t\tkey,\n\t\t\t\t\t&FilterResult::key);\n\t\t\t\tif (i != _filterResults.end()) {\n\t\t\t\t\tif (_filteredSelected == (i - _filterResults.begin())\n\t\t\t\t\t\t&& (i + 1) == _filterResults.end()) {\n\t\t\t\t\t\t_filteredSelected = -1;\n\t\t\t\t\t}\n\t\t\t\t\t_filterResults.erase(i);\n\t\t\t\t}\n\t\t\t\t_updated.fire({});\n\t\t\t}\n\t\t\trefresh();\n\t\t} else if (_state == WidgetState::Default && from != to) {\n\t\t\tupdate(\n\t\t\t\t0,\n\t\t\t\tstd::min(from, to),\n\t\t\t\twidth(),\n\t\t\t\tstd::abs(from - to) + event.moved.height);\n\t\t}\n\t}, lifetime());\n}\n\nvoid InnerWidget::repaintCollapsedFolderRow(not_null<Data::Folder*> folder) {\n\tfor (auto i = 0, l = int(_collapsedRows.size()); i != l; ++i) {\n\t\tif (_collapsedRows[i]->folder == folder) {\n\t\t\tupdate(0, i * st::dialogsImportantBarHeight, width(), st::dialogsImportantBarHeight);\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nint InnerWidget::defaultRowTop(not_null<Row*> row) const {\n\tconst auto index = row->index();\n\tauto top = dialogsOffset();\n\tif (base::in_range(index, 0, _pinnedRows.size())) {\n\t\ttop += qRound(_pinnedRows[index].yadd.current());\n\t}\n\treturn top + row->top();\n}\n\nvoid InnerWidget::repaintDialogRow(\n\t\tFilterId filterId,\n\t\tnot_null<Row*> row) {\n\tif (_state == WidgetState::Default) {\n\t\tif (_filterId == filterId) {\n\t\t\tif (const auto folder = row->folder()) {\n\t\t\t\trepaintCollapsedFolderRow(folder);\n\t\t\t}\n\t\t\tupdate(0, defaultRowTop(row), width(), row->height());\n\t\t}\n\t} else if (_state == WidgetState::Filtered) {\n\t\tif (!filterId) {\n\t\t\tfor (auto i = 0, l = int(_filterResults.size()); i != l; ++i) {\n\t\t\t\tconst auto &result = _filterResults[i];\n\t\t\t\tif (result.key() == row->key()) {\n\t\t\t\t\tupdate(\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tfilteredOffset() + result.top,\n\t\t\t\t\t\twidth(),\n\t\t\t\t\t\tresult.row->height());\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid InnerWidget::repaintDialogRow(RowDescriptor row) {\n\tupdateDialogRow(row);\n}\n\nvoid InnerWidget::refreshDialogRow(RowDescriptor row) {\n\tif (row.fullId) {\n\t\tfor (const auto &result : _searchResults) {\n\t\t\tif (result->item()->fullId() == row.fullId) {\n\t\t\t\tresult->itemView().itemInvalidated(result->item());\n\t\t\t}\n\t\t}\n\t}\n\trepaintDialogRow(row);\n}\n\nvoid InnerWidget::updateSearchResult(not_null<PeerData*> peer) {\n\tif (_state == WidgetState::Filtered) {\n\t\tconst auto i = ranges::find(\n\t\t\t_peerSearchResults,\n\t\t\tpeer,\n\t\t\t&PeerSearchResult::peer);\n\t\tif (i != end(_peerSearchResults)) {\n\t\t\tconst auto top = peerSearchOffset();\n\t\t\tconst auto index = (i - begin(_peerSearchResults));\n\t\t\trtlupdate(\n\t\t\t\t0,\n\t\t\t\ttop + index * st::dialogsRowHeight,\n\t\t\t\twidth(),\n\t\t\t\tst::dialogsRowHeight);\n\t\t}\n\t}\n}\n\nvoid InnerWidget::updateDialogRow(\n\t\tRowDescriptor row,\n\t\tQRect updateRect,\n\t\tUpdateRowSections sections) {\n\tif (IsServerMsgId(-row.fullId.msg)) {\n\t\tif (const auto peer = row.key.peer()) {\n\t\t\tif (const auto from = peer->migrateFrom()) {\n\t\t\t\tif (const auto migrated = from->owner().historyLoaded(from)) {\n\t\t\t\t\trow = RowDescriptor(\n\t\t\t\t\t\tmigrated,\n\t\t\t\t\t\tFullMsgId(from->id, -row.fullId.msg));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tconst auto updateRow = [&](int rowTop, int rowHeight) {\n\t\tif (!updateRect.isEmpty()) {\n\t\t\trtlupdate(updateRect.translated(0, rowTop));\n\t\t} else {\n\t\t\trtlupdate(0, rowTop, width(), rowHeight);\n\t\t}\n\t};\n\tif (_state == WidgetState::Default) {\n\t\tif (sections & UpdateRowSection::Default) {\n\t\t\tif (const auto folder = row.key.folder()) {\n\t\t\t\trepaintCollapsedFolderRow(folder);\n\t\t\t}\n\t\t\tif (const auto dialog = _shownList->getRow(row.key)) {\n\t\t\t\tconst auto position = dialog->index();\n\t\t\t\tauto top = dialogsOffset();\n\t\t\t\tif (base::in_range(position, 0, _pinnedRows.size())) {\n\t\t\t\t\ttop += qRound(_pinnedRows[position].yadd.current());\n\t\t\t\t}\n\t\t\t\tupdateRow(top + dialog->top(), dialog->height());\n\t\t\t}\n\t\t}\n\t} else if (_state == WidgetState::Filtered) {\n\t\tif ((sections & UpdateRowSection::Filtered)\n\t\t\t&& !_filterResults.empty()) {\n\t\t\tfor (const auto &result : _filterResults) {\n\t\t\t\tif (result.key() == row.key) {\n\t\t\t\t\tupdateRow(\n\t\t\t\t\t\tfilteredOffset() + result.top,\n\t\t\t\t\t\tresult.row->height());\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif ((sections & UpdateRowSection::PeerSearch)\n\t\t\t&& !_peerSearchResults.empty()) {\n\t\t\tif (const auto peer = row.key.peer()) {\n\t\t\t\tconst auto rowHeight = st::dialogsRowHeight;\n\t\t\t\tauto index = 0;\n\t\t\t\tfor (const auto &result : _peerSearchResults) {\n\t\t\t\t\tif (result->peer == peer) {\n\t\t\t\t\t\tupdateRow(\n\t\t\t\t\t\t\tpeerSearchOffset() + index * rowHeight,\n\t\t\t\t\t\t\trowHeight);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\t++index;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif ((sections & UpdateRowSection::MessageSearch)\n\t\t\t&& !_searchResults.empty()) {\n\t\t\tconst auto add = searchedOffset();\n\t\t\tauto index = 0;\n\t\t\tfor (const auto &result : _searchResults) {\n\t\t\t\tif (isSearchResultActive(result.get(), row)) {\n\t\t\t\t\tupdateRow(add + index * _st->height, _st->height);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t++index;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid InnerWidget::enterEventHook(QEnterEvent *e) {\n\tsetMouseTracking(true);\n}\n\nRow *InnerWidget::shownRowByKey(Key key) {\n\tconst auto entry = key.entry();\n\tif (_savedSublists) {\n\t\tconst auto sublist = entry->asSublist();\n\t\tif (!sublist || sublist->parent() != _savedSublists) {\n\t\t\treturn nullptr;\n\t\t}\n\t} else if (_openedForum) {\n\t\tconst auto topic = entry->asTopic();\n\t\tif (!topic || topic->forum() != _openedForum) {\n\t\t\treturn nullptr;\n\t\t}\n\t} else if (_openedFolder) {\n\t\tconst auto history = entry->asHistory();\n\t\tif (!history || history->folder() != _openedFolder) {\n\t\t\treturn nullptr;\n\t\t}\n\t} else {\n\t\tconst auto history = entry->asHistory();\n\t\tif (!entry->asFolder() && (!history || history->folder())) {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\tconst auto links = entry->chatListLinks(FilterId());\n\treturn links ? links->main.get() : nullptr;\n}\n\nvoid InnerWidget::updateSelectedRow(Key key) {\n\tif (_state == WidgetState::Default) {\n\t\tif (key) {\n\t\t\tconst auto row = shownRowByKey(key);\n\t\t\tif (!row) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto position = row->index();\n\t\t\tauto top = dialogsOffset();\n\t\t\tif (base::in_range(position, 0, _pinnedRows.size())) {\n\t\t\t\ttop += qRound(_pinnedRows[position].yadd.current());\n\t\t\t}\n\t\t\tupdate(0, top + row->top(), width(), row->height());\n\t\t} else if (_selected) {\n\t\t\tupdate(0, dialogsOffset() + _selected->top(), width(), _selected->height());\n\t\t} else if (_collapsedSelected >= 0) {\n\t\t\tupdate(0, _collapsedSelected * st::dialogsImportantBarHeight, width(), st::dialogsImportantBarHeight);\n\t\t}\n\t} else if (_state == WidgetState::Filtered) {\n\t\tif (key) {\n\t\t\tfor (auto i = 0, l = int(_filterResults.size()); i != l; ++i) {\n\t\t\t\tconst auto &result = _filterResults[i];\n\t\t\t\tif (result.key() == key) {\n\t\t\t\t\tupdate(0, filteredOffset() + result.top, width(), result.row->height());\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (_hashtagSelected >= 0) {\n\t\t\tupdate(0, hashtagsOffset() + _hashtagSelected * st::mentionHeight, width(), st::mentionHeight);\n\t\t} else if (_filteredSelected >= 0) {\n\t\t\tif (_filteredSelected < _filterResults.size()) {\n\t\t\t\tconst auto &result = _filterResults[_filteredSelected];\n\t\t\t\tupdate(0, filteredOffset() + result.top, width(), result.row->height());\n\t\t\t}\n\t\t} else if (_peerSearchSelected >= 0) {\n\t\t\tupdate(0, peerSearchOffset() + _peerSearchSelected * st::dialogsRowHeight, width(), st::dialogsRowHeight);\n\t\t} else if (_previewSelected >= 0) {\n\t\t\tupdate(0, previewOffset() + _previewSelected * _st->height, width(), _st->height);\n\t\t} else if (_searchedSelected >= 0) {\n\t\t\tupdate(0, searchedOffset() + _searchedSelected * _st->height, width(), _st->height);\n\t\t}\n\t}\n}\n\nvoid InnerWidget::refreshShownList() {\n\tconst auto list = _savedSublists\n\t\t? _savedSublists->chatsList()->indexed()\n\t\t: _openedForum\n\t\t? _openedForum->topicsList()->indexed()\n\t\t: _filterId\n\t\t? session().data().chatsFilters().chatsList(_filterId)->indexed()\n\t\t: session().data().chatsList(_openedFolder)->indexed();\n\tif (_shownList != list) {\n\t\t_shownList->unfreeze();\n\t\t_shownList = list;\n\t\t_shownList->updateHeights(_narrowRatio);\n\t}\n}\n\nvoid InnerWidget::leaveEventHook(QEvent *e) {\n\tsetMouseTracking(false);\n\tunfreezeShownList(false);\n\tclearSelection();\n\tupdate();\n}\n\nvoid InnerWidget::dragLeft() {\n\tsetMouseTracking(false);\n\tunfreezeShownList(false);\n\tclearSelection();\n\tupdate();\n}\n\nFilterId InnerWidget::filterId() const {\n\treturn _filterId;\n}\n\nvoid InnerWidget::clearSelection() {\n\t_mouseSelection = false;\n\t_lastMousePosition = std::nullopt;\n\t_lastRowLocalMouseX = -1;\n\tif (isSelected()) {\n\t\tupdateSelectedRow();\n\t\t_collapsedSelected = -1;\n\t\t_selectedMorePosts = false;\n\t\t_selectedChatTypeFilter = false;\n\t\t_selected = nullptr;\n\t\t_filteredSelected\n\t\t\t= _searchedSelected\n\t\t\t= _previewSelected\n\t\t\t= _peerSearchSelected\n\t\t\t= _hashtagSelected\n\t\t\t= -1;\n\t\tsetCursor(style::cur_default);\n\t}\n}\n\nvoid InnerWidget::fillSupportSearchMenu(not_null<Ui::PopupMenu*> menu) {\n\tconst auto globalSearch = (_searchState.tab == ChatSearchTab::MyMessages)\n\t\t|| (_searchState.tab == ChatSearchTab::PublicPosts);\n\tif (!globalSearch && _searchState.inChat) {\n\t\treturn;\n\t}\n\tconst auto all = session().settings().supportAllSearchResults();\n\tconst auto text = all ? \"Only one from chat\" : \"Show all messages\";\n\tmenu->addAction(text, [=] {\n\t\tsession().settings().setSupportAllSearchResults(!all);\n\t\tsession().saveSettingsDelayed();\n\t});\n}\n\nvoid InnerWidget::fillArchiveSearchMenu(not_null<Ui::PopupMenu*> menu) {\n\tconst auto folder = session().data().folderLoaded(Data::Folder::kId);\n\tconst auto globalSearch = (_searchState.tab == ChatSearchTab::MyMessages)\n\t\t|| (_searchState.tab == ChatSearchTab::PublicPosts);\n\tif (!folder\n\t\t|| !folder->chatsList()->fullSize().current()\n\t\t|| (!globalSearch && _searchState.inChat)\n\t\t|| (_searchState.tab == ChatSearchTab::PublicPosts)) {\n\t\treturn;\n\t}\n\tconst auto skip = session().settings().skipArchiveInSearch();\n\tconst auto text = skip\n\t\t? tr::lng_dialogs_show_archive_in_search(tr::now)\n\t\t: tr::lng_dialogs_skip_archive_in_search(tr::now);\n\tmenu->addAction(text, [=] {\n\t\tsession().settings().setSkipArchiveInSearch(!skip);\n\t\tsession().saveSettingsDelayed();\n\t});\n}\n\nbool InnerWidget::showChatPreview() {\n\tconst auto row = computeChatPreviewRow();\n\tconst auto callback = crl::guard(this, [=](bool shown) {\n\t\tchatPreviewShown(shown, row);\n\t});\n\treturn _controller->showChatPreview(row, callback);\n}\n\nvoid InnerWidget::chatPreviewShown(bool shown, RowDescriptor row) {\n\t_chatPreviewScheduled = false;\n\tif (shown) {\n\t\tconst auto chosen = computeChosenRow();\n\t\tif (!chosen.sponsoredRandomId.isEmpty() && row.key == chosen.key) {\n\t\t\tauto &messages = session().sponsoredMessages();\n\t\t\tmessages.clicked(chosen.sponsoredRandomId, false, false);\n\t\t}\n\t\t_chatPreviewRow = row;\n\t\tif (base::take(_chatPreviewTouchGlobal)) {\n\t\t\t_touchCancelRequests.fire({});\n\t\t}\n\t\tClickHandler::unpressed();\n\t\tmousePressReleased(QCursor::pos(), Qt::NoButton, Qt::NoModifier);\n\t} else {\n\t\tcancelChatPreview();\n\t\tconst auto globalPosition = QCursor::pos();\n\t\tif (rect().contains(mapFromGlobal(globalPosition))) {\n\t\t\tsetMouseTracking(true);\n\t\t\tselectByMouse(globalPosition);\n\t\t}\n\t}\n}\n\nbool InnerWidget::scheduleChatPreview(QPoint positionOverride) {\n\tconst auto row = computeChatPreviewRow();\n\tconst auto callback = crl::guard(this, [=](bool shown) {\n\t\tchatPreviewShown(shown, row);\n\t});\n\t_chatPreviewScheduled = _controller->scheduleChatPreview(\n\t\trow,\n\t\tcallback,\n\t\tnullptr,\n\t\tpositionOverride);\n\treturn _chatPreviewScheduled;\n}\n\nvoid InnerWidget::contextMenuEvent(QContextMenuEvent *e) {\n\t_menu = nullptr;\n\n\tconst auto fromMouse = e->reason() == QContextMenuEvent::Mouse;\n\n\tif (fromMouse) {\n\t\tselectByMouse(e->globalPos());\n\t}\n\n\tconst auto row = [&]() -> RowDescriptor {\n\t\tif (_state == WidgetState::Default) {\n\t\t\tif (_selected) {\n\t\t\t\treturn { _selected->key(), FullMsgId() };\n\t\t\t} else if (base::in_range(_collapsedSelected, 0, _collapsedRows.size())) {\n\t\t\t\tif (const auto folder = _collapsedRows[_collapsedSelected]->folder) {\n\t\t\t\t\treturn { folder, FullMsgId() };\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (_state == WidgetState::Filtered) {\n\t\t\tif (base::in_range(_filteredSelected, 0, _filterResults.size())) {\n\t\t\t\treturn { _filterResults[_filteredSelected].key(), FullMsgId() };\n\t\t\t} else if (base::in_range(_previewSelected, 0, _previewResults.size())) {\n\t\t\t\treturn {\n\t\t\t\t\t_previewResults[_previewSelected]->item()->history(),\n\t\t\t\t\t_previewResults[_previewSelected]->item()->fullId()\n\t\t\t\t};\n\t\t\t} else if (base::in_range(_searchedSelected, 0, _searchResults.size())) {\n\t\t\t\treturn {\n\t\t\t\t\t_searchResults[_searchedSelected]->item()->history(),\n\t\t\t\t\t_searchResults[_searchedSelected]->item()->fullId()\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\treturn RowDescriptor();\n\t}();\n\tif (!row.key) {\n\t\treturn;\n\t}\n\n\t_menuRow = row;\n\tif (_pressButton != Qt::LeftButton) {\n\t\tmousePressReleased(e->globalPos(), _pressButton, e->modifiers());\n\t}\n\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\trow.fullId ? st::defaultPopupMenu : st::popupMenuExpandedSeparator);\n\tif (row.fullId) {\n\t\tif (session().supportMode()) {\n\t\t\tfillSupportSearchMenu(_menu.get());\n\t\t} else {\n\t\t\tfillArchiveSearchMenu(_menu.get());\n\t\t}\n\t} else {\n\t\tconst auto addAction = Ui::Menu::CreateAddActionCallback(_menu);\n\t\tWindow::FillDialogsEntryMenu(\n\t\t\t_controller,\n\t\t\tDialogs::EntryState{\n\t\t\t\t.key = row.key,\n\t\t\t\t.section = Dialogs::EntryState::Section::ContextMenu,\n\t\t\t\t.filterId = _filterId,\n\t\t\t},\n\t\t\taddAction);\n\t}\n\tQObject::connect(_menu.get(), &QObject::destroyed, [=] {\n\t\tif (_menuRow.key) {\n\t\t\tupdateDialogRow(base::take(_menuRow));\n\t\t}\n\t\tif (!fromMouse) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto globalPosition = QCursor::pos();\n\t\tif (rect().contains(mapFromGlobal(globalPosition))) {\n\t\t\tsetMouseTracking(true);\n\t\t\tselectByMouse(globalPosition);\n\t\t}\n\t});\n\tif (_menu->empty()) {\n\t\t_menu = nullptr;\n\t} else {\n\t\t_menu->popup(e->globalPos());\n\t\te->accept();\n\t}\n}\n\nvoid InnerWidget::showSponsoredMenu(int peerSearchIndex, QPoint globalPos) {\n\t_menu = nullptr;\n\n\tconst auto count = int(_peerSearchResults.size());\n\tconst auto entry = (peerSearchIndex >= 0 && peerSearchIndex < count)\n\t\t? _peerSearchResults[peerSearchIndex].get()\n\t\t: nullptr;\n\tif (!entry || !entry->sponsored) {\n\t\treturn;\n\t}\n\n\t_peerSearchMenu = peerSearchIndex;\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\tst::popupMenuExpandedSeparator);\n\tconst auto peer = entry->peer;\n\tconst auto remove = crl::guard(this, [=] {\n\t\t_sponsoredRemoved.emplace(peer);\n\t\tif (_pressedRightButtonData) {\n\t\t\tfor (const auto &result : _peerSearchResults) {\n\t\t\t\tif (result->peer == peer\n\t\t\t\t\t&& result->sponsored\n\t\t\t\t\t&& _pressedRightButtonData\n\t\t\t\t\t\t== &result->sponsored->button) {\n\t\t\t\t\t_pressedRightButtonData = nullptr;\n\t\t\t\t\t_pressedRightButton = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t_peerSearchResults.erase(\n\t\t\tranges::remove(\n\t\t\t\t_peerSearchResults,\n\t\t\t\tpeer,\n\t\t\t\t&PeerSearchResult::peer),\n\t\t\tend(_peerSearchResults));\n\t\trefresh();\n\t});\n\tMenu::FillSponsored(\n\t\tUi::Menu::CreateAddActionCallback(_menu),\n\t\t_controller->uiShow(),\n\t\tMenu::SponsoredPhrases::Search,\n\t\tsession().sponsoredMessages().lookupDetails(entry->sponsored->data),\n\t\tsession().sponsoredMessages().createReportCallback(\n\t\t\tentry->sponsored->data.randomId,\n\t\t\tremove));\n\tQObject::connect(_menu.get(), &QObject::destroyed, [=] {\n\t\tif (_peerSearchMenu >= 0\n\t\t\t&& _peerSearchMenu < _peerSearchResults.size()) {\n\t\t\tconst auto index = std::exchange(_peerSearchMenu, -1);\n\t\t\tupdateSearchResult(_peerSearchResults[index]->peer);\n\t\t}\n\t\tconst auto globalPosition = QCursor::pos();\n\t\tif (rect().contains(mapFromGlobal(globalPosition))) {\n\t\t\tsetMouseTracking(true);\n\t\t\tselectByMouse(globalPosition);\n\t\t}\n\t});\n\tif (_menu->empty()) {\n\t\t_menu = nullptr;\n\t} else {\n\t\t_menu->popup(globalPos);\n\t}\n}\n\nvoid InnerWidget::parentGeometryChanged() {\n\tconst auto globalPosition = QCursor::pos();\n\tif (rect().contains(mapFromGlobal(globalPosition))) {\n\t\tsetMouseTracking(true);\n\t\tif (_mouseSelection) {\n\t\t\tselectByMouse(globalPosition);\n\t\t}\n\t}\n}\n\nbool InnerWidget::processTouchEvent(not_null<QTouchEvent*> e) {\n\tconst auto point = e->touchPoints().empty()\n\t\t? std::optional<QPoint>()\n\t\t: e->touchPoints().front().screenPos().toPoint();\n\tswitch (e->type()) {\n\tcase QEvent::TouchBegin: {\n\t\tif (!point) {\n\t\t\treturn false;\n\t\t}\n\t\tselectByMouse(*point);\n\t\tif (isUserpicPressOnWide() && scheduleChatPreview(*point)) {\n\t\t\t_chatPreviewTouchGlobal = point;\n\t\t} else if (!_dragging) {\n\t\t\t_touchDragStartGlobal = point;\n\t\t\t_touchDragPinnedTimer.callOnce(QApplication::startDragTime());\n\t\t}\n\t} break;\n\n\tcase QEvent::TouchUpdate: {\n\t\tif (!point) {\n\t\t\treturn false;\n\t\t}\n\t\tif (_chatPreviewTouchGlobal) {\n\t\t\tconst auto delta = (*_chatPreviewTouchGlobal - *point);\n\t\t\tif (delta.manhattanLength() >= QApplication::startDragDistance()) {\n\t\t\t\tcancelChatPreview();\n\t\t\t}\n\t\t}\n\t\tif (_touchDragStartGlobal && _dragging) {\n\t\t\tupdateReorderPinned(mapFromGlobal(*point));\n\t\t\treturn _dragging != nullptr;\n\t\t} else if (_touchDragStartGlobal) {\n\t\t\tconst auto delta = (*_touchDragStartGlobal - *point);\n\t\t\tif (delta.manhattanLength() >= QApplication::startDragDistance()) {\n\t\t\t\tif (_touchDragPinnedTimer.isActive()) {\n\t\t\t\t\t_touchDragPinnedTimer.cancel();\n\t\t\t\t\t_touchDragStartGlobal = {};\n\t\t\t\t\t_touchDragNowGlobal = {};\n\t\t\t\t} else {\n\t\t\t\t\tdragPinnedFromTouch();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t_touchDragNowGlobal = point;\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase QEvent::TouchEnd:\n\tcase QEvent::TouchCancel: {\n\t\tif (_chatPreviewTouchGlobal) {\n\t\t\tcancelChatPreview();\n\t\t}\n\t\tif (_touchDragStartGlobal) {\n\t\t\t_touchDragStartGlobal = {};\n\t\t\treturn finishReorderOnRelease();\n\t\t}\n\t} break;\n\t}\n\treturn false;\n}\n\nvoid InnerWidget::dragPinnedFromTouch() {\n\tExpects(_touchDragStartGlobal.has_value());\n\n\tconst auto global = *_touchDragStartGlobal;\n\t_touchDragPinnedTimer.cancel();\n\tselectByMouse(global);\n\tif (!_selected || _dragging || _state != WidgetState::Default) {\n\t\treturn;\n\t}\n\t_dragStart = mapFromGlobal(global);\n\tunfreezeShownList(true);\n\t_dragging = _selected;\n\tconst auto now = mapFromGlobal(_touchDragNowGlobal.value_or(global));\n\tstartReorderPinned(now);\n\tupdateReorderPinned(now);\n}\n\nbool InnerWidget::hasChatTypeFilter() const {\n\treturn !_searchResults.empty()\n\t\t&& (_searchState.tab == ChatSearchTab::MyMessages);\n}\n\nvoid InnerWidget::searchRequested(bool loading) {\n\t_searchWaiting = false;\n\t_searchLoading = loading;\n\tif (loading) {\n\t\tclearSearchResults(true);\n\t\tclearPreviewResults();\n\t}\n\trefresh(true);\n}\n\nvoid InnerWidget::applySearchState(SearchState state) {\n\tif (_searchState == state) {\n\t\treturn;\n\t}\n\tauto withSameQuery = state;\n\twithSameQuery.query = _searchState.query;\n\tconst auto otherChanged = (_searchState != withSameQuery);\n\n\tconst auto ignoreInChat = (state.tab == ChatSearchTab::MyMessages)\n\t\t|| (state.tab == ChatSearchTab::PublicPosts);\n\tconst auto sublist = ignoreInChat ? nullptr : state.inChat.sublist();\n\tconst auto peer = ignoreInChat ? nullptr : state.inChat.peer();\n\tif (const auto migrateFrom = peer ? peer->migrateFrom() : nullptr) {\n\t\t_searchInMigrated = peer->owner().history(migrateFrom);\n\t} else {\n\t\t_searchInMigrated = nullptr;\n\t}\n\tif (peer && peer->isSelf()) {\n\t\tconst auto reactions = &peer->owner().reactions();\n\t\t_searchTags = std::make_unique<SearchTags>(\n\t\t\t&peer->owner(),\n\t\t\treactions->myTagsValue(sublist),\n\t\t\tstate.tags);\n\n\t\t_searchTags->repaintRequests() | rpl::on_next([=] {\n\t\t\tconst auto height = _searchTags->height();\n\t\t\tupdate(0, 0, width(), height);\n\t\t}, _searchTags->lifetime());\n\n\t\t_searchTags->menuRequests(\n\t\t) | rpl::on_next([=](Data::ReactionId id) {\n\t\t\tHistoryView::ShowTagInListMenu(\n\t\t\t\t&_menu,\n\t\t\t\t_lastMousePosition.value_or(QCursor::pos()),\n\t\t\t\tthis,\n\t\t\t\tid,\n\t\t\t\t_controller);\n\t\t}, _searchTags->lifetime());\n\n\t\t_searchTags->heightValue() | rpl::skip(\n\t\t\t1\n\t\t) | rpl::on_next([=] {\n\t\t\trefresh();\n\t\t\tmoveSearchIn();\n\t\t\tif (_loadingAnimation) {\n\t\t\t\t_loadingAnimation->move(0, searchedOffset());\n\t\t\t}\n\t\t}, _searchTags->lifetime());\n\t} else {\n\t\t_searchTags = nullptr;\n\t\tstate.tags.clear();\n\t}\n\t_searchFromShown = ignoreInChat\n\t\t? nullptr\n\t\t: sublist\n\t\t? sublist->sublistPeer().get()\n\t\t: state.fromPeer;\n\tif (state.inChat) {\n\t\tonHashtagFilterUpdate(QStringView());\n\t}\n\tif (state.filter != _searchState.filter) {\n\t\t_chatTypeFilterWidth = 0;\n\t\tupdate();\n\t}\n\t_searchState = std::move(state);\n\t_searchHashOrCashtag = IsHashOrCashtagSearchQuery(_searchState.query);\n\t_searchWithPostsPreview = computeSearchWithPostsPreview();\n\n\tupdateSearchIn();\n\tmoveSearchIn();\n\n\tauto newFilter = _searchState.query;\n\tconst auto mentionsSearch = (newFilter == u\"@\"_q);\n\tconst auto words = mentionsSearch\n\t\t? QStringList(newFilter)\n\t\t: TextUtilities::PrepareSearchWords(newFilter);\n\tnewFilter = words.isEmpty() ? QString() : words.join(' ');\n\tif (newFilter != _filter || otherChanged) {\n\t\t_filter = newFilter;\n\t\tif (_filter.isEmpty()\n\t\t\t&& !_searchState.fromPeer\n\t\t\t&& _searchState.tags.empty()\n\t\t\t&& _searchState.tab != ChatSearchTab::PublicPosts) {\n\t\t\tclearFilter();\n\t\t} else {\n\t\t\tsetState(WidgetState::Filtered);\n\t\t\t_filterResults.clear();\n\t\t\t_filterResultsGlobal.clear();\n\t\t\tconst auto append = [&](not_null<IndexedList*> list) {\n\t\t\t\tconst auto results = list->filtered(words);\n\t\t\t\tauto top = filteredHeight();\n\t\t\t\tauto i = _filterResults.insert(\n\t\t\t\t\tend(_filterResults),\n\t\t\t\t\tbegin(results),\n\t\t\t\t\tend(results));\n\t\t\t\tfor (const auto e = end(_filterResults); i != e; ++i) {\n\t\t\t\t\ti->top = top;\n\t\t\t\t\ti->row->recountHeight(_narrowRatio, _filterId);\n\t\t\t\t\ttop += i->row->height();\n\t\t\t\t}\n\t\t\t};\n\t\t\tif (_searchState.filterChatsList() && !words.isEmpty()) {\n\t\t\t\tif (_savedSublists) {\n\t\t\t\t\tappend(_savedSublists->chatsList()->indexed());\n\t\t\t\t} else if (_openedForum) {\n\t\t\t\t\tappend(_openedForum->topicsList()->indexed());\n\t\t\t\t} else {\n\t\t\t\t\tconst auto owner = &session().data();\n\t\t\t\t\tappend(owner->chatsList()->indexed());\n\t\t\t\t\tconst auto id = Data::Folder::kId;\n\t\t\t\t\tif (const auto add = owner->folderLoaded(id)) {\n\t\t\t\t\t\tappend(add->chatsList()->indexed());\n\t\t\t\t\t}\n\t\t\t\t\tappend(owner->contactsNoChatsList());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tclearMouseSelection(true);\n\t}\n\tif (_state != WidgetState::Default) {\n\t\t_searchWaiting = true;\n\t\t_searchRequests.fire(otherChanged\n\t\t\t? SearchRequestDelay::Instant\n\t\t\t: SearchRequestDelay::Delayed);\n\t\tif (_searchWaiting) {\n\t\t\trefresh(true);\n\t\t}\n\t}\n}\n\nvoid InnerWidget::onHashtagFilterUpdate(QStringView newFilter) {\n\tif (newFilter.isEmpty()\n\t\t|| newFilter.at(0) != '#'\n\t\t|| _searchState.inChat) {\n\t\t_hashtagFilter = QString();\n\t\tif (!_hashtagResults.empty()) {\n\t\t\t_hashtagResults.clear();\n\t\t\trefresh(true);\n\t\t\tclearMouseSelection(true);\n\t\t}\n\t\treturn;\n\t}\n\t_hashtagFilter = newFilter.toString();\n\tif (cRecentSearchHashtags().isEmpty() && cRecentWriteHashtags().isEmpty()) {\n\t\tsession().local().readRecentHashtagsAndBots();\n\t}\n\tauto &recent = cRecentSearchHashtags();\n\t_hashtagResults.clear();\n\tif (!recent.isEmpty()) {\n\t\t_hashtagResults.reserve(qMin(recent.size(), kHashtagResultsLimit));\n\t\tfor (const auto &tag : recent) {\n\t\t\tif (tag.first.startsWith(base::StringViewMid(_hashtagFilter, 1), Qt::CaseInsensitive)\n\t\t\t\t&& tag.first.size() + 1 != newFilter.size()) {\n\t\t\t\t_hashtagResults.push_back(std::make_unique<HashtagResult>(tag.first));\n\t\t\t\tif (_hashtagResults.size() == kHashtagResultsLimit) break;\n\t\t\t}\n\t\t}\n\t}\n\trefresh(true);\n\tclearMouseSelection(true);\n}\n\nvoid InnerWidget::appendToFiltered(Key key) {\n\tfor (const auto &row : _filterResults) {\n\t\tif (row.key() == key) {\n\t\t\treturn;\n\t\t}\n\t}\n\tauto row = std::make_unique<Row>(key, 0, 0);\n\trow->recountHeight(_narrowRatio, _filterId);\n\tconst auto &[i, ok] = _filterResultsGlobal.emplace(key, std::move(row));\n\tconst auto height = filteredHeight();\n\t_filterResults.emplace_back(i->second.get());\n\t_filterResults.back().top = height;\n\ttrackResultsHistory(key.owningHistory());\n}\n\nInnerWidget::~InnerWidget() {\n\tunfreezeShownList(false);\n\tsession().data().stories().decrementPreloadingMainSources();\n\tclearSearchResults();\n}\n\nvoid InnerWidget::clearSearchResults(bool alsoPeerSearchResults) {\n\tif (alsoPeerSearchResults) {\n\t\tclearPeerSearchResults();\n\t}\n\t_searchResults.clear();\n\t_searchedCount = _searchedMigratedCount = 0;\n}\n\nvoid InnerWidget::clearPeerSearchResults() {\n\tif (_pressedRightButtonData) {\n\t\tfor (const auto &result : _peerSearchResults) {\n\t\t\tif (result->sponsored\n\t\t\t\t&& _pressedRightButtonData == &result->sponsored->button) {\n\t\t\t\t_pressedRightButtonData = nullptr;\n\t\t\t\t_pressedRightButton = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\t_peerSearchResults.clear();\n}\n\nvoid InnerWidget::clearPreviewResults() {\n\t_previewResults.clear();\n\t_previewCount = 0;\n}\n\nvoid InnerWidget::trackResultsHistory(not_null<History*> history) {\n\tif (!_trackedHistories.emplace(history).second) {\n\t\treturn;\n\t}\n\tconst auto peer = history->peer;\n\tif (!peer->isBot() && !peer->isMegagroup()) {\n\t\treturn;\n\t}\n\tauto changes = peer->isBot()\n\t\t? peer->asBot()->flagsValue() | rpl::skip(\n\t\t\t1\n\t\t) | rpl::filter([=](const UserData::Flags::Change &change) {\n\t\t\treturn change.diff & UserDataFlag::Forum;\n\t\t}) | rpl::to_empty | rpl::type_erased\n\t\t: peer->asChannel()->flagsValue() | rpl::skip(\n\t\t\t1\n\t\t) | rpl::filter([=](const ChannelData::Flags::Change &change) {\n\t\t\treturn (change.diff & ChannelDataFlag::Forum);\n\t\t}) | rpl::to_empty | rpl::type_erased;\n\tstd::move(changes) | rpl::on_next([=] {\n\t\tfor (const auto &row : _searchResults) {\n\t\t\tif (row->item()->history()->peer == peer) {\n\t\t\t\trow->invalidateTopic();\n\t\t\t}\n\t\t}\n\t\tauto removed = false;\n\t\tfor (auto i = begin(_filterResultsGlobal)\n\t\t\t; i != end(_filterResultsGlobal);) {\n\t\t\tif (const auto topic = i->first.topic()) {\n\t\t\t\tif (topic->peer() == peer) {\n\t\t\t\t\tremoved = true;\n\t\t\t\t\t_filterResults.erase(\n\t\t\t\t\t\tranges::remove(\n\t\t\t\t\t\t\t_filterResults,\n\t\t\t\t\t\t\ti->first,\n\t\t\t\t\t\t\t&FilterResult::key),\n\t\t\t\t\t\tend(_filterResults));\n\t\t\t\t\ti = _filterResultsGlobal.erase(i);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t\tif (removed) {\n\t\t\trefresh();\n\t\t\tclearMouseSelection(true);\n\t\t}\n\t\tupdate();\n\t}, _trackedLifetime);\n\n\tif (const auto forum = peer->forum()) {\n\t\tforum->topicDestroyed(\n\t\t) | rpl::on_next([=](not_null<Data::ForumTopic*> topic) {\n\t\t\tauto removed = false;\n\t\t\tconst auto sfrom = ranges::remove(\n\t\t\t\t_searchResults,\n\t\t\t\ttopic.get(),\n\t\t\t\t&FakeRow::topic);\n\t\t\tif (sfrom != end(_searchResults)) {\n\t\t\t\t_searchResults.erase(sfrom, end(_searchResults));\n\t\t\t\tremoved = true;\n\t\t\t}\n\t\t\tconst auto ffrom = ranges::remove(\n\t\t\t\t_filterResults,\n\t\t\t\tKey(topic),\n\t\t\t\t&FilterResult::key);\n\t\t\tif (ffrom != end(_filterResults)) {\n\t\t\t\t_filterResults.erase(ffrom, end(_filterResults));\n\t\t\t\tremoved = true;\n\t\t\t}\n\t\t\t_filterResultsGlobal.erase(Key(topic));\n\t\t\tif (removed) {\n\t\t\t\trefresh();\n\t\t\t\tclearMouseSelection(true);\n\t\t\t}\n\t\t\tif (_chatPreviewRow.key.topic() == topic) {\n\t\t\t\t_chatPreviewRow = {};\n\t\t\t}\n\t\t}, _trackedLifetime);\n\t}\n}\n\nData::Thread *InnerWidget::updateFromParentDrag(QPoint globalPosition) {\n\tif (!_freezeTimer.isActive()) {\n\t\t_shownList->freeze();\n\t}\n\t_freezeTimer.callOnce(kFreezeTimeout);\n\tselectByMouse(globalPosition);\n\n\tconst auto fromRow = [](Row *row) {\n\t\treturn row ? row->thread() : nullptr;\n\t};\n\tif (_state == WidgetState::Default) {\n\t\treturn fromRow(_selected);\n\t} else if (_state == WidgetState::Filtered) {\n\t\tif (base::in_range(_filteredSelected, 0, _filterResults.size())) {\n\t\t\treturn fromRow(_filterResults[_filteredSelected].row);\n\t\t} else if (base::in_range(_peerSearchSelected, 0, _peerSearchResults.size())) {\n\t\t\treturn session().data().history(\n\t\t\t\t_peerSearchResults[_peerSearchSelected]->peer);\n\t\t} else if (base::in_range(_previewSelected, 0, _previewResults.size())) {\n\t\t\tif (const auto item = _previewResults[_previewSelected]->item()) {\n\t\t\t\tif (const auto topic = item->topic()) {\n\t\t\t\t\treturn topic;\n\t\t\t\t}\n\t\t\t\treturn item->history();\n\t\t\t}\n\t\t} else if (base::in_range(_searchedSelected, 0, _searchResults.size())) {\n\t\t\tif (const auto item = _searchResults[_searchedSelected]->item()) {\n\t\t\t\tif (const auto topic = item->topic()) {\n\t\t\t\t\treturn topic;\n\t\t\t\t}\n\t\t\t\treturn item->history();\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid InnerWidget::setLoadMoreCallback(Fn<void()> callback) {\n\t_loadMoreCallback = std::move(callback);\n}\n\nvoid InnerWidget::setLoadMoreFilteredCallback(Fn<void()> callback) {\n\t_loadMoreFilteredCallback = std::move(callback);\n}\n\nauto InnerWidget::chosenRow() const -> rpl::producer<ChosenRow> {\n\treturn _chosenRow.events();\n}\n\nrpl::producer<> InnerWidget::updated() const {\n\treturn _updated.events();\n}\n\nrpl::producer<int> InnerWidget::scrollByDeltaRequests() const {\n\treturn _draggingScroll.scrolls();\n}\n\nrpl::producer<> InnerWidget::listBottomReached() const {\n\treturn _listBottomReached.events();\n}\n\nrpl::producer<ChatSearchTab> InnerWidget::changeSearchTabRequests() const {\n\treturn _changeSearchTabRequests.events();\n}\n\nauto InnerWidget::changeSearchFilterRequests() const\n-> rpl::producer<ChatTypeFilter>{\n\treturn _changeSearchFilterRequests.events();\n}\n\nrpl::producer<> InnerWidget::cancelSearchRequests() const {\n\treturn _cancelSearchRequests.events();\n}\n\nrpl::producer<> InnerWidget::cancelSearchFromRequests() const {\n\treturn _cancelSearchFromRequests.events();\n}\n\nrpl::producer<> InnerWidget::changeSearchFromRequests() const {\n\treturn _changeSearchFromRequests.events();\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::mustScrollTo() const {\n\treturn _mustScrollTo.events();\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::dialogMoved() const {\n\treturn _dialogMoved.events();\n}\n\nrpl::producer<SearchRequestDelay> InnerWidget::searchRequests() const {\n\treturn _searchRequests.events();\n}\n\nrpl::producer<QString> InnerWidget::completeHashtagRequests() const {\n\treturn _completeHashtagRequests.events();\n}\n\nrpl::producer<> InnerWidget::refreshHashtagsRequests() const {\n\treturn _refreshHashtagsRequests.events();\n}\n\nvoid InnerWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\t_visibleTop = visibleTop;\n\t_visibleBottom = visibleBottom;\n\tpreloadRowsData();\n\tconst auto loadTill = _visibleTop\n\t\t+ PreloadHeightsCount * (_visibleBottom - _visibleTop);\n\tif (_state == WidgetState::Filtered && loadTill >= peerSearchOffset()) {\n\t\tif (_loadMoreFilteredCallback) {\n\t\t\t_loadMoreFilteredCallback();\n\t\t}\n\t}\n\tif (loadTill >= height()) {\n\t\tif (_loadMoreCallback) {\n\t\t\t_loadMoreCallback();\n\t\t}\n\t}\n}\n\nvoid InnerWidget::itemRemoved(not_null<const HistoryItem*> item) {\n\tint wasCount = _searchResults.size();\n\tfor (auto i = _searchResults.begin(); i != _searchResults.end();) {\n\t\tif ((*i)->item() == item) {\n\t\t\ti = _searchResults.erase(i);\n\t\t\tif (item->history() == _searchInMigrated) {\n\t\t\t\tif (_searchedMigratedCount > 0) --_searchedMigratedCount;\n\t\t\t} else {\n\t\t\t\tif (_searchedCount > 0) --_searchedCount;\n\t\t\t}\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tif (wasCount != _searchResults.size()) {\n\t\trefresh();\n\t}\n}\n\nbool InnerWidget::uniqueSearchResults() const {\n\treturn _controller->uniqueChatsInSearchResults(_searchState);\n}\n\nbool InnerWidget::hasHistoryInResults(not_null<History*> history) const {\n\tusing Result = std::unique_ptr<FakeRow>;\n\tconst auto inSearchResults = ranges::find(\n\t\t_searchResults,\n\t\thistory,\n\t\t[](const Result &result) { return result->item()->history(); }\n\t) != end(_searchResults);\n\tif (inSearchResults) {\n\t\treturn true;\n\t}\n\tconst auto inFilteredResults = ranges::find(\n\t\t_filterResults,\n\t\tKey(history),\n\t\t&FilterResult::key\n\t) != end(_filterResults);\n\tif (inFilteredResults) {\n\t\treturn true;\n\t}\n\tconst auto inPeerSearchResults = ranges::find(\n\t\t_peerSearchResults,\n\t\thistory->peer,\n\t\t[](const auto &result) { return result->peer; }\n\t) != end(_peerSearchResults);\n\tif (inPeerSearchResults) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid InnerWidget::searchReceived(\n\t\tstd::vector<not_null<HistoryItem*>> messages,\n\t\tHistoryItem *inject,\n\t\tSearchRequestType type,\n\t\tint fullCount) {\n\t_searchWaiting = false;\n\t_searchLoading = false;\n\n\tconst auto uniquePeers = uniqueSearchResults();\n\tconst auto withPreview = _searchWithPostsPreview;\n\tconst auto toPreview = withPreview && type.posts;\n\tif (type.start && !type.migrated && (!withPreview || !type.posts)) {\n\t\tclearSearchResults(false);\n\t}\n\tif (!withPreview || toPreview) {\n\t\tclearPreviewResults();\n\t}\n\n\tconst auto globalSearch = (_searchState.tab == ChatSearchTab::MyMessages)\n\t\t|| (_searchState.tab == ChatSearchTab::PublicPosts);\n\tconst auto key = globalSearch\n\t\t? Key()\n\t\t: (!_openedForum || _searchState.inChat.topic())\n\t\t? _searchState.inChat\n\t\t: Key(_openedForum->history());\n\tif (inject\n\t\t&& (globalSearch\n\t\t\t|| !_searchState.inChat\n\t\t\t|| inject->history() == _searchState.inChat.history())) {\n\t\tAssert(_searchResults.empty());\n\t\tAssert(!toPreview);\n\t\tconst auto index = int(_searchResults.size());\n\t\t_searchResults.push_back(\n\t\t\tstd::make_unique<FakeRow>(\n\t\t\t\tkey,\n\t\t\t\tinject,\n\t\t\t\t[=] { repaintSearchResult(index); }));\n\t\ttrackResultsHistory(inject->history());\n\t\t++fullCount;\n\t}\n\tauto &results = toPreview ? _previewResults : _searchResults;\n\tfor (const auto &item : messages) {\n\t\tconst auto history = item->history();\n\t\tif (toPreview || !uniquePeers || !hasHistoryInResults(history)) {\n\t\t\tconst auto index = int(results.size());\n\t\t\tconst auto repaint = toPreview\n\t\t\t\t? Fn<void()>([=] { repaintSearchResult(index); })\n\t\t\t\t: [=] { repaintPreviewResult(index); };\n\t\t\tresults.push_back(\n\t\t\t\tstd::make_unique<FakeRow>(key, item, repaint));\n\t\t\ttrackResultsHistory(history);\n\t\t\tif (!toPreview && uniquePeers && !history->unreadCountKnown()) {\n\t\t\t\thistory->owner().histories().requestDialogEntry(history);\n\t\t\t} else if (toPreview && results.size() >= kPreviewPostsLimit) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\tif (type.migrated) {\n\t\t_searchedMigratedCount = fullCount;\n\t} else if (!withPreview || !toPreview) {\n\t\t_searchedCount = fullCount;\n\t} else {\n\t\t_previewCount = fullCount;\n\t}\n\n\trefresh();\n}\n\nvoid InnerWidget::peerSearchReceived(Api::PeerSearchResult result) {\n\tif (_state != WidgetState::Filtered) {\n\t\treturn;\n\t}\n\n\t_peerSearchQuery = result.query.toLower().trimmed();\n\tclearPeerSearchResults();\n\t_peerSearchResults.reserve(result.peers.size()\n\t\t+ result.sponsored.size());\n\tfor\t(const auto &peer : result.my) {\n\t\tappendToFiltered(peer->owner().history(peer));\n\t}\n\tif (Core::App().settings().fork().globalSearchDisabled()) {\n\t\treturn refresh();\n\t}\n\tconst auto inlist = [&](not_null<PeerData*> peer) {\n\t\tif (const auto history = peer->owner().historyLoaded(peer)) {\n\t\t\t// Skip existing chats.\n\t\t\treturn history->inChatList();\n\t\t}\n\t\treturn false;\n\t};\n\tauto added = base::flat_set<not_null<PeerData*>>();\n\tfor (const auto &sponsored : result.sponsored) {\n\t\tconst auto peer = sponsored.peer;\n\t\tif (inlist(peer) || _sponsoredRemoved.contains(peer)) {\n\t\t\tcontinue;\n\t\t}\n\t\t_peerSearchResults.push_back(\n\t\t\tstd::make_unique<PeerSearchResult>(peer));\n\t\t_peerSearchResults.back()->sponsored\n\t\t\t= std::make_unique<SponsoredSearchResult>(SponsoredSearchResult{\n\t\t\t\t.data = sponsored,\n\t\t\t});\n\t\tadded.emplace(peer);\n\t}\n\tfor (const auto &peer : result.peers) {\n\t\tif (added.contains(peer) || inlist(peer)) {\n\t\t\tcontinue;\n\t\t}\n\t\t_peerSearchResults.push_back(\n\t\t\tstd::make_unique<PeerSearchResult>(peer));\n\t}\n\trefresh();\n}\n\nData::Folder *InnerWidget::shownFolder() const {\n\treturn _openedFolder;\n}\n\nData::Forum *InnerWidget::shownForum() const {\n\treturn _openedForum;\n}\n\nbool InnerWidget::needCollapsedRowsRefresh() const {\n\tconst auto archive = !_shownList->empty()\n\t\t? _shownList->begin()->get()->folder()\n\t\t: nullptr;\n\tconst auto collapsedHasArchive = !_collapsedRows.empty()\n\t\t&& (_collapsedRows.back()->folder != nullptr);\n\tconst auto archiveIsCollapsed = (archive != nullptr)\n\t\t&& session().settings().archiveCollapsed();\n\tconst auto archiveIsInMainMenu = (archive != nullptr)\n\t\t&& session().settings().archiveInMainMenu();\n\treturn archiveIsInMainMenu\n\t\t? (collapsedHasArchive || !_skipTopDialog)\n\t\t: archiveIsCollapsed\n\t\t\t? (!collapsedHasArchive || !_skipTopDialog)\n\t\t\t: (collapsedHasArchive || _skipTopDialog);\n}\n\nvoid InnerWidget::editOpenedFilter() {\n\tif (_filterId > 0) {\n\t\tEditExistingFilter(_controller, _filterId);\n\t}\n}\n\nvoid InnerWidget::refresh(bool toTop) {\n\tif (!_geometryInited) {\n\t\treturn;\n\t} else if (needCollapsedRowsRefresh()) {\n\t\treturn refreshWithCollapsedRows(toTop);\n\t}\n\trefreshEmpty();\n\tif (_searchTags) {\n\t\t_searchTagsLeft = st::dialogsFilterSkip\n\t\t\t+ st::dialogsFilterPadding.x();\n\t\t_searchTags->resizeToWidth(width() - 2 * _searchTagsLeft);\n\t}\n\tauto h = 0;\n\tif (_state == WidgetState::Default) {\n\t\tif (_shownList->empty()) {\n\t\t\th = st::dialogsEmptyHeight;\n\t\t} else {\n\t\t\th = dialogsOffset() + _shownList->height();\n\t\t}\n\t} else if (_state == WidgetState::Filtered) {\n\t\tif (_searchEmpty && !_searchEmpty->isHidden()) {\n\t\t\th = searchedOffset() + st::recentPeersEmptyHeightMin;\n\t\t\t_searchEmpty->setMinimalHeight(st::recentPeersEmptyHeightMin);\n\t\t\t_searchEmpty->move(0, h - st::recentPeersEmptyHeightMin);\n\t\t} else if (_loadingAnimation) {\n\t\t\th = searchedOffset() + _loadingAnimation->height();\n\t\t} else {\n\t\t\th = searchedOffset() + (_searchResults.size() * _st->height);\n\t\t}\n\t}\n\tresize(width(), h);\n\tif (toTop) {\n\t\tstopReorderPinned();\n\t\tjumpToTop();\n\t\tpreloadRowsData();\n\t}\n\tupdate();\n}\n\nvoid InnerWidget::refreshEmpty() {\n\tif (_state == WidgetState::Filtered) {\n\t\tconst auto empty = _filterResults.empty()\n\t\t\t&& _searchResults.empty()\n\t\t\t&& _peerSearchResults.empty()\n\t\t\t&& _hashtagResults.empty();\n\t\tif (_searchLoading || _searchWaiting || !empty) {\n\t\t\tif (_searchEmpty) {\n\t\t\t\t_searchEmpty->hide();\n\t\t\t}\n\t\t} else if (_searchEmptyState != _searchState) {\n\t\t\t_searchEmptyState = _searchState;\n\t\t\t_searchEmpty = MakeSearchEmpty(this, _searchState, [=] {\n\t\t\t\t_changeSearchFilterRequests.fire(ChatTypeFilter::All);\n\t\t\t});\n\t\t\tif (_controller->session().data().chatsListLoaded()) {\n\t\t\t\t_searchEmpty->animate();\n\t\t\t}\n\t\t} else if (_searchEmpty) {\n\t\t\t_searchEmpty->show();\n\t\t}\n\n\t\tif ((!_searchLoading && !_searchWaiting) || !empty) {\n\t\t\t_loadingAnimation.destroy();\n\t\t} else if (!_loadingAnimation) {\n\t\t\t_loadingAnimation = Ui::CreateLoadingDialogRowWidget(\n\t\t\t\tthis,\n\t\t\t\t*_st,\n\t\t\t\t2);\n\t\t\t_loadingAnimation->resizeToWidth(width());\n\t\t\t_loadingAnimation->move(0, searchedOffset());\n\t\t\t_loadingAnimation->show();\n\t\t}\n\t} else {\n\t\t_searchEmpty.destroy();\n\t\t_loadingAnimation.destroy();\n\t\t_searchEmptyState = {};\n\t}\n\n\tconst auto data = &session().data();\n\tconst auto state = !_shownList->empty()\n\t\t? EmptyState::None\n\t\t: _savedSublists\n\t\t? (_savedSublists->chatsList()->loaded()\n\t\t\t? EmptyState::EmptySavedSublists\n\t\t\t: EmptyState::Loading)\n\t\t: _openedForum\n\t\t? (_openedForum->topicsList()->loaded()\n\t\t\t? EmptyState::EmptyForum\n\t\t\t: EmptyState::Loading)\n\t\t: (!_filterId && data->contactsLoaded().current())\n\t\t? EmptyState::NoContacts\n\t\t: (_filterId > 0) && data->chatsList()->loaded()\n\t\t? EmptyState::EmptyFolder\n\t\t: EmptyState::Loading;\n\tif (state == EmptyState::None) {\n\t\t_emptyState = state;\n\t\t_empty.destroy();\n\t\t_emptyList.destroy();\n\t\t_emptyButton.destroy();\n\t\treturn;\n\t} else if (_emptyState == state) {\n\t\tconst auto isEmptyVisible = (_state == WidgetState::Default)\n\t\t\t&& !_openedFolder;\n\t\t_empty->setVisible(isEmptyVisible);\n\t\tif (_emptyList) {\n\t\t\t_emptyList->setVisible(isEmptyVisible);\n\t\t\t_empty->setVisible(false);\n\t\t}\n\t\tif (_emptyButton) {\n\t\t\t_emptyButton->setVisible(isEmptyVisible);\n\t\t}\n\t\treturn;\n\t}\n\t_emptyState = state;\n\tauto phrase = (state == EmptyState::NoContacts)\n\t\t? tr::lng_no_chats()\n\t\t: (state == EmptyState::EmptyFolder)\n\t\t? tr::lng_no_chats_filter()\n\t\t: (state == EmptyState::EmptyForum)\n\t\t? tr::lng_forum_no_topics()\n\t\t: (state == EmptyState::EmptySavedSublists)\n\t\t? tr::lng_no_saved_sublists()\n\t\t: tr::lng_contacts_loading();\n\tauto link = (state == EmptyState::NoContacts)\n\t\t? tr::lng_add_contact_button()\n\t\t: (state == EmptyState::EmptyFolder)\n\t\t? tr::lng_filters_context_edit()\n\t\t: (state == EmptyState::EmptyForum)\n\t\t? tr::lng_forum_create_topic()\n\t\t: rpl::single(QString());\n\tauto full = rpl::combine(\n\t\tstd::move(phrase),\n\t\tstd::move(link)\n\t) | rpl::map([](const QString &phrase, const QString &link) {\n\t\tauto result = tr::marked(phrase);\n\t\tif (!link.isEmpty()) {\n\t\t\tresult.append(\"\\n\\n\").append(tr::link(link));\n\t\t}\n\t\treturn result;\n\t});\n\t_empty.create(this, std::move(full), st::dialogsEmptyLabel);\n\t_empty->overrideLinkClickHandler([=] {\n\t\tif (_emptyState == EmptyState::NoContacts) {\n\t\t\t_controller->showAddContact();\n\t\t} else if (_emptyState == EmptyState::EmptyFolder) {\n\t\t\teditOpenedFilter();\n\t\t} else if (_emptyState == EmptyState::EmptyForum) {\n\t\t\t_controller->show(\n\t\t\t\tBox(NewForumTopicBox, _controller, _openedForum->history()));\n\t\t}\n\t});\n\t_empty->setVisible(_state == WidgetState::Default);\n\n\tif (state == EmptyState::NoContacts) {\n\t\tconst auto isListVisible = (_state == WidgetState::Default)\n\t\t\t&& !_openedFolder;\n\t\tif (_openedFolder) {\n\t\t\tcrl::on_main(this, [=] {\n\t\t\t\tif (_openedFolder) {\n\t\t\t\t\t_controller->closeFolder();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\t_emptyList.create(this);\n\t\t_emptyList->setVisible(isListVisible);\n\n\t\tauto icon = ::Settings::CreateLottieIcon(\n\t\t\t_emptyList,\n\t\t\t{\n\t\t\t\t.name = u\"no_chats\"_q,\n\t\t\t\t.sizeOverride = st::normalBoxLottieSize,\n\t\t\t});\n\t\t_emptyList->add(std::move(icon.widget), style::al_top);\n\t\tUi::AddSkip(_emptyList);\n\t\t_emptyList->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t_emptyList,\n\t\t\t\ttr::lng_no_conversations(),\n\t\t\t\tst::dialogEmptyButtonLabel),\n\t\t\tstyle::al_top);\n\t\tif (_state == WidgetState::Default) {\n\t\t\ticon.animate(anim::repeat::once);\n\t\t}\n\t\t_emptyButton.create(\n\t\t\tthis,\n\t\t\ttr::lng_no_conversations_button(),\n\t\t\tst::dialogEmptyButton);\n\t\t_emptyButton->setVisible(isListVisible);\n\t\t_emptyButton->setClickedCallback([=, window = _controller] {\n\t\t\twindow->show(PrepareContactsBox(window));\n\t\t});\n\t\tgeometryValue() | rpl::on_next([=](const QRect &r) {\n\t\t\tconst auto top = r.height()\n\t\t\t\t- _emptyButton->height()\n\t\t\t\t- st::dialogEmptyButtonSkip;\n\t\t\t_emptyButton->moveToLeft(st::dialogEmptyButtonSkip, top);\n\t\t}, _emptyButton->lifetime());\n\t\tgeometryValue() | rpl::on_next([=](const QRect &r) {\n\t\t\tconst auto bottom = _emptyButton\n\t\t\t\t? (_emptyButton->height() + st::dialogEmptyButtonSkip)\n\t\t\t\t: 0;\n\t\t\t_emptyList->moveToLeft(\n\t\t\t\t0,\n\t\t\t\t((r.height() - bottom) - _emptyList->height()) / 2);\n\t\t}, _emptyList->lifetime());\n\n\t\t_empty->setVisible(!_emptyList->isVisible());\n\t}\n\n\tresizeEmpty();\n}\n\nvoid InnerWidget::resizeEmpty() {\n\tif (_empty) {\n\t\tconst auto skip = st::dialogsEmptySkip;\n\t\t_empty->resizeToWidth(width() - 2 * skip);\n\t\t_empty->move(skip, (st::dialogsEmptyHeight - _empty->height()) / 2);\n\t}\n\tif (_emptyList) {\n\t\t_emptyList->resizeToWidth(width());\n\t}\n\tif (_emptyButton) {\n\t\tconst auto skip = st::dialogEmptyButtonSkip;\n\t\t_emptyButton->resizeToWidth(width() - 2 * skip);\n\t}\n\tif (_searchEmpty) {\n\t\t_searchEmpty->resizeToWidth(width());\n\t\t_searchEmpty->move(0, searchedOffset());\n\t}\n\tif (_loadingAnimation) {\n\t\t_loadingAnimation->resizeToWidth(width());\n\t\t_loadingAnimation->move(0, searchedOffset());\n\t}\n}\n\nvoid InnerWidget::clearMouseSelection(bool clearSelection) {\n\t_mouseSelection = false;\n\t_lastMousePosition = std::nullopt;\n\t_lastRowLocalMouseX = -1;\n\tif (clearSelection) {\n\t\tif (_state == WidgetState::Default) {\n\t\t\t_collapsedSelected = -1;\n\t\t\t_selected = nullptr;\n\t\t} else if (_state == WidgetState::Filtered) {\n\t\t\t_filteredSelected\n\t\t\t\t= _peerSearchSelected\n\t\t\t\t= _previewSelected\n\t\t\t\t= _searchedSelected\n\t\t\t\t= _hashtagSelected = -1;\n\t\t}\n\t\tsetCursor(style::cur_default);\n\t}\n}\n\nWidgetState InnerWidget::state() const {\n\treturn _state;\n}\n\nbool InnerWidget::hasFilteredResults() const {\n\treturn !_filterResults.empty() && _hashtagResults.empty();\n}\n\nauto InnerWidget::searchTagsChanges() const\n-> rpl::producer<std::vector<Data::ReactionId>> {\n\treturn _searchTags\n\t\t? _searchTags->selectedChanges()\n\t\t: rpl::never<std::vector<Data::ReactionId>>();\n}\n\nvoid InnerWidget::updateSearchIn() {\n\tif (!_searchState.inChat\n\t\t&& _searchHashOrCashtag == HashOrCashtag::None) {\n\t\t_searchIn = nullptr;\n\t\treturn;\n\t} else if (!_searchIn) {\n\t\t_searchIn = std::make_unique<ChatSearchIn>(this);\n\t\t_searchIn->show();\n\t\t_searchIn->changeFromRequests() | rpl::start_to_stream(\n\t\t\t_changeSearchFromRequests,\n\t\t\t_searchIn->lifetime());\n\t\t_searchIn->cancelFromRequests() | rpl::start_to_stream(\n\t\t\t_cancelSearchFromRequests,\n\t\t\t_searchIn->lifetime());\n\t\t_searchIn->cancelInRequests() | rpl::start_to_stream(\n\t\t\t_cancelSearchRequests,\n\t\t\t_searchIn->lifetime());\n\t\t_searchIn->tabChanges() | rpl::start_to_stream(\n\t\t\t_changeSearchTabRequests,\n\t\t\t_searchIn->lifetime());\n\t}\n\n\tconst auto sublist = _searchState.inChat.sublist();\n\tconst auto topic = _searchState.inChat.topic();\n\tconst auto peer = _searchState.inChat.owningHistory()\n\t\t? _searchState.inChat.owningHistory()->peer.get()\n\t\t: _openedForum\n\t\t? _openedForum->peer().get()\n\t\t: nullptr;\n\tconst auto paused = [window = _controller] {\n\t\treturn window->isGifPausedAtLeastFor(Window::GifPauseReason::Any);\n\t};\n\tconst auto textFg = [] {\n\t\treturn st::windowSubTextFg->c;\n\t};\n\tconst auto topicIcon = !topic\n\t\t? nullptr\n\t\t: topic->iconId()\n\t\t? Ui::MakeEmojiThumbnail(\n\t\t\t&topic->owner(),\n\t\t\tData::SerializeCustomEmojiId(topic->iconId()),\n\t\t\tpaused,\n\t\t\ttextFg)\n\t\t: Ui::MakeEmojiThumbnail(\n\t\t\t&topic->owner(),\n\t\t\tData::TopicIconEmojiEntity({\n\t\t\t\t.title = (topic->isGeneral()\n\t\t\t\t\t? Data::ForumGeneralIconTitle()\n\t\t\t\t\t: topic->title()),\n\t\t\t\t.colorId = (topic->isGeneral()\n\t\t\t\t\t? Data::ForumGeneralIconColor(st::windowSubTextFg->c)\n\t\t\t\t\t: topic->colorId()),\n\t\t\t}),\n\t\t\tpaused,\n\t\t\ttextFg);\n\tconst auto peerIcon = peer\n\t\t? Ui::MakeUserpicThumbnail(peer)\n\t\t: sublist\n\t\t? Ui::MakeUserpicThumbnail(sublist->sublistPeer())\n\t\t: nullptr;\n\tconst auto myIcon = Ui::MakeIconThumbnail(st::menuIconChats);\n\tconst auto publicIcon = (_searchHashOrCashtag != HashOrCashtag::None)\n\t\t? Ui::MakeIconThumbnail(st::menuIconChannel)\n\t\t: nullptr;\n\tconst auto peerTabType = (peer && peer->isBroadcast())\n\t\t? ChatSearchPeerTabType::Channel\n\t\t: (peer && (peer->isChat() || peer->isMegagroup()))\n\t\t? ChatSearchPeerTabType::Group\n\t\t: ChatSearchPeerTabType::Chat;\n\tconst auto fromImage = _searchFromShown\n\t\t? Ui::MakeUserpicThumbnail(_searchFromShown)\n\t\t: nullptr;\n\tconst auto fromName = _searchFromShown\n\t\t? _searchFromShown->shortName()\n\t\t: QString();\n\t_searchIn->apply({\n\t\t{ ChatSearchTab::ThisTopic, topicIcon },\n\t\t{ ChatSearchTab::ThisPeer, peerIcon },\n\t\t{ ChatSearchTab::MyMessages, myIcon },\n\t\t{ ChatSearchTab::PublicPosts, publicIcon },\n\t}, _searchState.tab, peerTabType, fromImage, fromName);\n}\n\nvoid InnerWidget::repaintSearchResult(int index) {\n\trtlupdate(\n\t\t0,\n\t\tsearchedOffset() + index * _st->height,\n\t\twidth(),\n\t\t_st->height);\n}\n\nvoid InnerWidget::repaintPreviewResult(int index) {\n\trtlupdate(\n\t\t0,\n\t\tpreviewOffset() + index * _st->height,\n\t\twidth(),\n\t\t_st->height);\n}\n\nbool InnerWidget::computeSearchWithPostsPreview() const {\n\treturn \t(_searchHashOrCashtag != HashOrCashtag::None)\n\t\t&& (_searchState.tab == ChatSearchTab::MyMessages)\n\t\t&& !_searchState.inChat;\n}\n\nvoid InnerWidget::clearFilter() {\n\tif (_state == WidgetState::Filtered || _searchState.inChat) {\n\t\tif (_searchState.inChat) {\n\t\t\tsetState(WidgetState::Filtered);\n\t\t} else {\n\t\t\tsetState(WidgetState::Default);\n\t\t}\n\t\t_hashtagResults.clear();\n\t\t_filterResults.clear();\n\t\t_filterResultsGlobal.clear();\n\t\tclearPeerSearchResults();\n\t\t_searchResults.clear();\n\t\t_previewResults.clear();\n\t\t_trackedHistories.clear();\n\t\t_trackedLifetime.destroy();\n\t\t_filter = QString();\n\t\trefresh(true);\n\t}\n}\n\nvoid InnerWidget::setState(WidgetState state) {\n\t_state = state;\n}\n\nvoid InnerWidget::selectSkip(int32 direction) {\n\tclearMouseSelection();\n\tif (_state == WidgetState::Default) {\n\t\tconst auto skip = _skipTopDialog ? 1 : 0;\n\t\tif (_collapsedRows.empty() && _shownList->size() <= skip) {\n\t\t\treturn;\n\t\t}\n\t\tif (_collapsedSelected < 0 && !_selected) {\n\t\t\tif (!_collapsedRows.empty()) {\n\t\t\t\t_collapsedSelected = 0;\n\t\t\t} else {\n\t\t\t\t_selected = (_shownList->cbegin() + skip)->get();\n\t\t\t}\n\t\t} else {\n\t\t\tauto cur = (_collapsedSelected >= 0)\n\t\t\t\t? _collapsedSelected\n\t\t\t\t: int(_collapsedRows.size()\n\t\t\t\t\t+ (_shownList->cfind(_selected)\n\t\t\t\t\t\t- _shownList->cbegin()\n\t\t\t\t\t\t- skip));\n\t\t\tcur = std::clamp(\n\t\t\t\tcur + direction,\n\t\t\t\t0,\n\t\t\t\tstatic_cast<int>(_collapsedRows.size()\n\t\t\t\t\t+ _shownList->size()\n\t\t\t\t\t- skip\n\t\t\t\t\t- 1));\n\t\t\tif (cur < _collapsedRows.size()) {\n\t\t\t\t_collapsedSelected = cur;\n\t\t\t\t_selected = nullptr;\n\t\t\t} else {\n\t\t\t\t_collapsedSelected = -1;\n\t\t\t\t_selected = *(_shownList->cbegin() + skip + cur - _collapsedRows.size());\n\t\t\t}\n\t\t}\n\t\tscrollToDefaultSelected();\n\t} else if (_state == WidgetState::Filtered) {\n\t\tif (_hashtagResults.empty()\n\t\t\t&& _filterResults.empty()\n\t\t\t&& _peerSearchResults.empty()\n\t\t\t&& _previewResults.empty()\n\t\t\t&& _searchResults.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tif ((_hashtagSelected < 0 || _hashtagSelected >= _hashtagResults.size())\n\t\t\t&& (_filteredSelected < 0 || _filteredSelected >= _filterResults.size())\n\t\t\t&& (_peerSearchSelected < 0 || _peerSearchSelected >= _peerSearchResults.size())\n\t\t\t&& (_previewSelected < 0 || _previewSelected >= _previewResults.size())\n\t\t\t&& (_searchedSelected < 0 || _searchedSelected >= _searchResults.size())) {\n\t\t\tif (_hashtagResults.empty() && _filterResults.empty() && _peerSearchResults.empty() && _previewResults.empty()) {\n\t\t\t\t_searchedSelected = 0;\n\t\t\t} else if (_hashtagResults.empty() && _filterResults.empty() && _peerSearchResults.empty()) {\n\t\t\t\t_previewSelected = 0;\n\t\t\t} else if (_hashtagResults.empty() && _filterResults.empty()) {\n\t\t\t\t_peerSearchSelected = 0;\n\t\t\t} else if (_hashtagResults.empty()) {\n\t\t\t\t_filteredSelected = 0;\n\t\t\t} else {\n\t\t\t\t_hashtagSelected = 0;\n\t\t\t}\n\t\t} else {\n\t\t\tint32 cur = base::in_range(_hashtagSelected, 0, _hashtagResults.size())\n\t\t\t\t? _hashtagSelected\n\t\t\t\t: base::in_range(_filteredSelected, 0, _filterResults.size())\n\t\t\t\t? (_hashtagResults.size() + _filteredSelected)\n\t\t\t\t: base::in_range(_peerSearchSelected, 0, _peerSearchResults.size())\n\t\t\t\t? (_peerSearchSelected + _filterResults.size() + _hashtagResults.size())\n\t\t\t\t: base::in_range(_previewSelected, 0, _previewResults.size())\n\t\t\t\t? (_previewSelected + _peerSearchResults.size() + _filterResults.size() + _hashtagResults.size())\n\t\t\t\t: (_searchedSelected + _previewResults.size() + _peerSearchResults.size() + _filterResults.size() + _hashtagResults.size());\n\t\t\tcur = std::clamp(\n\t\t\t\tcur + direction,\n\t\t\t\t0,\n\t\t\t\tstatic_cast<int>(_hashtagResults.size()\n\t\t\t\t\t+ _filterResults.size()\n\t\t\t\t\t+ _peerSearchResults.size()\n\t\t\t\t\t+ _previewResults.size()\n\t\t\t\t\t+ _searchResults.size()) - 1);\n\t\t\tif (cur < _hashtagResults.size()) {\n\t\t\t\t_hashtagSelected = cur;\n\t\t\t\t_filteredSelected = _peerSearchSelected = _previewSelected = _searchedSelected = -1;\n\t\t\t} else if (cur < _hashtagResults.size() + _filterResults.size()) {\n\t\t\t\t_filteredSelected = cur - _hashtagResults.size();\n\t\t\t\t_hashtagSelected = _peerSearchSelected = _previewSelected = _searchedSelected = -1;\n\t\t\t} else if (cur < _hashtagResults.size() + _filterResults.size() + _peerSearchResults.size()) {\n\t\t\t\t_peerSearchSelected = cur - _hashtagResults.size() - _filterResults.size();\n\t\t\t\t_hashtagSelected = _filteredSelected = _previewSelected = _searchedSelected = -1;\n\t\t\t} else if (cur < _hashtagResults.size() + _filterResults.size() + _peerSearchResults.size() + _previewResults.size()) {\n\t\t\t\t_previewSelected = cur - _hashtagResults.size() - _filterResults.size() - _peerSearchResults.size();\n\t\t\t\t_hashtagSelected = _filteredSelected = _peerSearchSelected = _searchedSelected = -1;\n\t\t\t} else {\n\t\t\t\t_searchedSelected = cur - _hashtagResults.size() - _filterResults.size() - _peerSearchResults.size() - _previewResults.size();\n\t\t\t\t_hashtagSelected = _filteredSelected = _peerSearchSelected = _previewSelected = -1;\n\t\t\t}\n\t\t}\n\t\tif (base::in_range(_hashtagSelected, 0, _hashtagResults.size())) {\n\t\t\tconst auto from = _hashtagSelected * st::mentionHeight;\n\t\t\tscrollToItem(from, st::mentionHeight);\n\t\t} else if (base::in_range(_filteredSelected, 0, _filterResults.size())) {\n\t\t\tconst auto &result = _filterResults[_filteredSelected];\n\t\t\tconst auto from = filteredOffset() + result.top;\n\t\t\tscrollToItem(from, result.row->height());\n\t\t} else if (base::in_range(_peerSearchSelected, 0, _peerSearchResults.size())) {\n\t\t\tconst auto from = peerSearchOffset()\n\t\t\t\t+ _peerSearchSelected * st::dialogsRowHeight\n\t\t\t\t+ (_peerSearchSelected ? 0 : -st::searchedBarHeight);\n\t\t\tconst auto height = st::dialogsRowHeight\n\t\t\t\t+ (_peerSearchSelected ? 0 : st::searchedBarHeight);\n\t\t\tscrollToItem(from, height);\n\t\t} else if (base::in_range(_previewSelected, 0, _previewResults.size())) {\n\t\t\tconst auto from = previewOffset()\n\t\t\t\t+ _previewSelected * _st->height\n\t\t\t\t+ (_previewSelected ? 0 : -st::searchedBarHeight);\n\t\t\tconst auto height = _st->height\n\t\t\t\t+ (_previewSelected ? 0 : st::searchedBarHeight);\n\t\t\tscrollToItem(from, height);\n\t\t} else {\n\t\t\tconst auto from = searchedOffset()\n\t\t\t\t+ _searchedSelected * _st->height\n\t\t\t\t+ (_searchedSelected ? 0 : -st::searchedBarHeight);\n\t\t\tconst auto height = _st->height\n\t\t\t\t+ (_searchedSelected ? 0 : st::searchedBarHeight);\n\t\t\tscrollToItem(from, height);\n\t\t}\n\t}\n\tupdate();\n}\n\nvoid InnerWidget::scrollToEntry(const RowDescriptor &entry) {\n\tif (_state == WidgetState::Default) {\n\t\tif (auto row = _shownList->getRow(entry.key)) {\n\t\t\tscrollToItem(dialogsOffset() + row->top(), row->height());\n\t\t}\n\t} else if (_state == WidgetState::Filtered) {\n\t\tfor (auto i = 0, c = int(_previewResults.size()); i != c; ++i) {\n\t\t\tif (isSearchResultActive(_previewResults[i].get(), entry)) {\n\t\t\t\tconst auto from = previewOffset() + i * _st->height;\n\t\t\t\tscrollToItem(from, _st->height);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tfor (auto i = 0, c = int(_searchResults.size()); i != c; ++i) {\n\t\t\tif (isSearchResultActive(_searchResults[i].get(), entry)) {\n\t\t\t\tconst auto from = searchedOffset() + i * _st->height;\n\t\t\t\tscrollToItem(from, _st->height);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tfor (auto i = 0, c = int(_filterResults.size()); i != c; ++i) {\n\t\t\tauto &result = _filterResults[i];\n\t\t\tif (result.key() == entry.key) {\n\t\t\t\tconst auto from = filteredOffset() + result.top;\n\t\t\t\tscrollToItem(from, result.row->height());\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid InnerWidget::selectSkipPage(int32 pixels, int32 direction) {\n\tclearMouseSelection();\n\tint toSkip = pixels / _st->height;\n\tif (_state != WidgetState::Default) {\n\t\tselectSkip(direction * toSkip);\n\t\treturn;\n\t}\n\tconst auto skip = _skipTopDialog ? 1 : 0;\n\tif (!_selected) {\n\t\tif (direction > 0 && _shownList->size() > skip) {\n\t\t\t_selected = (_shownList->cbegin() + skip)->get();\n\t\t\t_collapsedSelected = -1;\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (direction > 0) {\n\t\tfor (auto i = _shownList->cfind(_selected), end = _shownList->cend()\n\t\t\t; i != end && (toSkip--)\n\t\t\t; ++i) {\n\t\t\t_selected = *i;\n\t\t}\n\t} else {\n\t\tfor (auto i = _shownList->cfind(_selected), b = _shownList->cbegin()\n\t\t\t; i != b && (*i)->index() > skip && (toSkip--)\n\t\t\t;) {\n\t\t\t_selected = *(--i);\n\t\t}\n\t\tif (toSkip && !_collapsedRows.empty()) {\n\t\t\t_collapsedSelected = std::max(int(_collapsedRows.size()) - toSkip, 0);\n\t\t\t_selected = nullptr;\n\t\t}\n\t}\n\tscrollToDefaultSelected();\n\tupdate();\n}\n\nvoid InnerWidget::scrollToItem(int top, int height) {\n\t_mustScrollTo.fire({ top, top + height });\n}\n\nvoid InnerWidget::scrollToDefaultSelected() {\n\tExpects(_state == WidgetState::Default);\n\n\tif (_collapsedSelected >= 0) {\n\t\tconst auto from = _collapsedSelected * st::dialogsImportantBarHeight;\n\t\tscrollToItem(from, st::dialogsImportantBarHeight);\n\t} else if (_selected) {\n\t\tconst auto from = dialogsOffset() + _selected->top();\n\t\tscrollToItem(from, _selected->height());\n\t}\n}\n\nvoid InnerWidget::preloadRowsData() {\n\tif (!parentWidget()) {\n\t\treturn;\n\t}\n\n\tauto yFrom = _visibleTop;\n\tauto yTo = _visibleTop + (_visibleBottom - _visibleTop) * (PreloadHeightsCount + 1);\n\tif (_state == WidgetState::Default) {\n\t\tauto otherStart = _shownList->size() * _st->height;\n\t\tif (yFrom < otherStart) {\n\t\t\tfor (auto i = _shownList->findByY(yFrom), end = _shownList->cend()\n\t\t\t\t; i != end\n\t\t\t\t; ++i) {\n\t\t\t\tif (((*i)->index() * _st->height) >= yTo) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t(*i)->entry()->chatListPreloadData();\n\t\t\t}\n\t\t\tyFrom = 0;\n\t\t} else {\n\t\t\tyFrom -= otherStart;\n\t\t}\n\t\tyTo -= otherStart;\n\t} else if (_state == WidgetState::Filtered) {\n\t\tauto from = (yFrom - filteredOffset()) / _st->height;\n\t\tif (from < 0) from = 0;\n\t\tif (from < _filterResults.size()) {\n\t\t\tconst auto to = std::min(\n\t\t\t\t((yTo - filteredOffset()) / _st->height) + 1,\n\t\t\t\tint(_filterResults.size()));\n\t\t\tfor (; from < to; ++from) {\n\t\t\t\t_filterResults[from].key().entry()->chatListPreloadData();\n\t\t\t}\n\t\t}\n\n\t\tfrom = (yFrom - peerSearchOffset()) / st::dialogsRowHeight;\n\t\tif (from < 0) from = 0;\n\t\tif (from < _peerSearchResults.size()) {\n\t\t\tconst auto to = std::min(\n\t\t\t\t((yTo - peerSearchOffset()) / st::dialogsRowHeight) + 1,\n\t\t\t\tint(_peerSearchResults.size()));\n\t\t\tfor (; from < to; ++from) {\n\t\t\t\t_peerSearchResults[from]->peer->loadUserpic();\n\t\t\t}\n\t\t}\n\n\t\tfrom = (yFrom - previewOffset()) / _st->height;\n\t\tif (from < 0) from = 0;\n\t\tif (from < _previewResults.size()) {\n\t\t\tconst auto to = std::min(\n\t\t\t\t((yTo - previewOffset()) / _st->height) + 1,\n\t\t\t\tint(_previewResults.size()));\n\t\t\tfor (; from < to; ++from) {\n\t\t\t\t_previewResults[from]->item()->history()->peer->loadUserpic();\n\t\t\t}\n\t\t}\n\n\t\tfrom = (yFrom - searchedOffset()) / _st->height;\n\t\tif (from < 0) from = 0;\n\t\tif (from < _searchResults.size()) {\n\t\t\tconst auto to = std::min(\n\t\t\t\t((yTo - searchedOffset()) / _st->height) + 1,\n\t\t\t\tint(_searchResults.size()));\n\t\t\tfor (; from < to; ++from) {\n\t\t\t\t_searchResults[from]->item()->history()->peer->loadUserpic();\n\t\t\t}\n\t\t}\n\t}\n}\n\nbool InnerWidget::chooseCollapsedRow(Qt::KeyboardModifiers modifiers) {\n\tif (_state != WidgetState::Default) {\n\t\treturn false;\n\t} else if ((_collapsedSelected < 0)\n\t\t|| (_collapsedSelected >= _collapsedRows.size())) {\n\t\treturn false;\n\t}\n\tconst auto &row = _collapsedRows[_collapsedSelected];\n\tAssert(row->folder != nullptr);\n\t_controller->openFolder(row->folder);\n\treturn true;\n}\n\nvoid InnerWidget::switchToFilter(FilterId filterId) {\n\tif (_controller->windowId().type != Window::SeparateType::Primary) {\n\t\treturn;\n\t}\n\tconst auto &list = session().data().chatsFilters().list();\n\tconst auto filterIt = filterId\n\t\t? ranges::find(list, filterId, &Data::ChatFilter::id)\n\t\t: end(list);\n\tconst auto found = (filterIt != end(list));\n\tif (!found) {\n\t\tfilterId = 0;\n\t}\n\tif (_filterId == filterId) {\n\t\tjumpToTop();\n\t\treturn;\n\t}\n\tsaveChatsFilterScrollState(_filterId);\n\tif (_openedFolder) {\n\t\t_filterId = filterId;\n\t\trefreshShownList();\n\t} else {\n\t\tclearSelection();\n\t\tstopReorderPinned();\n\t\t_filterId = filterId;\n\t\trefreshShownList();\n\t\trefreshWithCollapsedRows(true);\n\t}\n\trefreshEmpty();\n\t{\n\t\tconst auto skip = found\n\t\t\t// Don't save a scroll state for very flexible chat filters.\n\t\t\t&& (filterIt->flags() & (Data::ChatFilter::Flag::NoRead));\n\t\tif (!skip) {\n\t\t\trestoreChatsFilterScrollState(filterId);\n\t\t}\n\t}\n}\n\nvoid InnerWidget::jumpToTop() {\n\t_mustScrollTo.fire({ 0, -1 });\n}\n\nvoid InnerWidget::saveChatsFilterScrollState(FilterId filterId) {\n\t_chatsFilterScrollStates[filterId] = -y();\n}\n\nvoid InnerWidget::restoreChatsFilterScrollState(FilterId filterId) {\n\tconst auto it = _chatsFilterScrollStates.find(filterId);\n\tif (it != end(_chatsFilterScrollStates)) {\n\t\t_mustScrollTo.fire({ std::max(it->second, 0), -1 });\n\t}\n}\n\nQImage *InnerWidget::cacheChatsFilterTag(\n\t\tconst Data::ChatFilter &filter,\n\t\tuint8 more,\n\t\tbool active) {\n\tif (!filter.id() && !more) {\n\t\treturn nullptr;\n\t}\n\tconst auto key = SerializeFilterTagsKey(filter.id(), more, active);\n\tauto &entry = _chatsFilterTags[key];\n\tif (!entry.frame.isNull()) {\n\t\tif (!entry.context.loading) {\n\t\t\treturn &entry.frame;\n\t\t}\n\t\tfor (const auto &[k, emoji] : entry.context.emoji) {\n\t\t\tif (!emoji->ready()) {\n\t\t\t\treturn &entry.frame; // Still waiting for emoji.\n\t\t\t}\n\t\t}\n\t}\n\tauto roundedText = TextWithEntities();\n\tauto colorIndex = -1;\n\tif (filter.id()) {\n\t\troundedText = filter.title().text;\n\t\troundedText.text = roundedText.text.toUpper();\n\t\tif (filter.colorIndex()) {\n\t\t\tcolorIndex = *(filter.colorIndex());\n\t\t}\n\t} else if (more > 0) {\n\t\troundedText.text = QChar('+') + QString::number(more);\n\t\tcolorIndex = st::colorIndexBlue;\n\t}\n\tif (roundedText.empty() || colorIndex < 0) {\n\t\treturn nullptr;\n\t}\n\tconst auto color = Ui::EmptyUserpic::UserpicColor(colorIndex).color2;\n\tentry.context.color = color->c;\n\tentry.context.active = active;\n\tentry.context.textContext = Core::TextContext({ .session = &session() });\n\tentry.frame = Ui::ChatsFilterTag(roundedText, entry.context);\n\treturn &entry.frame;\n}\n\nbool InnerWidget::chooseHashtag() {\n\tif (_state != WidgetState::Filtered) {\n\t\treturn false;\n\t} else if ((_hashtagSelected < 0)\n\t\t|| (_hashtagSelected >= _hashtagResults.size())) {\n\t\treturn false;\n\t}\n\tconst auto &hashtag = _hashtagResults[_hashtagSelected];\n\tif (_hashtagDeleteSelected) {\n\t\tauto recent = cRecentSearchHashtags();\n\t\tfor (auto i = recent.begin(); i != recent.cend();) {\n\t\t\tif (i->first == hashtag->tag) {\n\t\t\t\ti = recent.erase(i);\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t\tcSetRecentSearchHashtags(recent);\n\t\tsession().local().writeRecentHashtagsAndBots();\n\t\t_refreshHashtagsRequests.fire({});\n\t\tselectByMouse(QCursor::pos());\n\t} else {\n\t\tsession().local().saveRecentSearchHashtags('#' + hashtag->tag);\n\t\t_completeHashtagRequests.fire_copy(hashtag->tag);\n\t}\n\treturn true;\n}\n\nChosenRow InnerWidget::computeChosenRow() const {\n\tif (_state == WidgetState::Default) {\n\t\tif ((_collapsedSelected >= 0)\n\t\t\t&& (_collapsedSelected < _collapsedRows.size())) {\n\t\t\tconst auto &row = _collapsedRows[_collapsedSelected];\n\t\t\tAssert(row->folder != nullptr);\n\t\t\treturn {\n\t\t\t\t.key = row->folder,\n\t\t\t\t.message = Data::UnreadMessagePosition,\n\t\t\t};\n\t\t} else if (_selected) {\n\t\t\treturn {\n\t\t\t\t.key = _selected->key(),\n\t\t\t\t.message = Data::UnreadMessagePosition,\n\t\t\t};\n\t\t}\n\t} else if (_state == WidgetState::Filtered) {\n\t\tif (base::in_range(_filteredSelected, 0, _filterResults.size())) {\n\t\t\treturn {\n\t\t\t\t.key = _filterResults[_filteredSelected].key(),\n\t\t\t\t.message = Data::UnreadMessagePosition,\n\t\t\t\t.filteredRow = true,\n\t\t\t};\n\t\t} else if (base::in_range(_peerSearchSelected, 0, _peerSearchResults.size())) {\n\t\t\tconst auto row = _peerSearchResults[_peerSearchSelected].get();\n\t\t\treturn {\n\t\t\t\t.key = session().data().history(row->peer),\n\t\t\t\t.message = Data::UnreadMessagePosition,\n\t\t\t\t.sponsoredRandomId = (row->sponsored\n\t\t\t\t\t? row->sponsored->data.randomId\n\t\t\t\t\t: QByteArray()),\n\t\t\t};\n\t\t} else if (base::in_range(_previewSelected, 0, _previewResults.size())) {\n\t\t\tconst auto result = _previewResults[_previewSelected].get();\n\t\t\tconst auto topic = result->topic();\n\t\t\tconst auto item = result->item();\n\t\t\treturn {\n\t\t\t\t.key = (topic ? (Entry*)topic : (Entry*)item->history()),\n\t\t\t\t.message = item->position()\n\t\t\t};\n\t\t} else if (base::in_range(_searchedSelected, 0, _searchResults.size())) {\n\t\t\tconst auto result = _searchResults[_searchedSelected].get();\n\t\t\tconst auto topic = result->topic();\n\t\t\tconst auto item = result->item();\n\t\t\treturn {\n\t\t\t\t.key = (topic ? (Entry*)topic : (Entry*)item->history()),\n\t\t\t\t.message = item->position()\n\t\t\t};\n\t\t}\n\t}\n\treturn ChosenRow();\n}\n\nbool InnerWidget::isUserpicPress() const {\n\treturn (_lastRowLocalMouseX >= 0)\n\t\t&& (_lastRowLocalMouseX < _st->nameLeft)\n\t\t&& (_collapsedSelected < 0\n\t\t\t|| _collapsedSelected >= _collapsedRows.size());\n}\n\nbool InnerWidget::isUserpicPressOnWide() const {\n\treturn isUserpicPress() && (width() > _narrowWidth);\n}\n\nbool InnerWidget::chooseRow(\n\t\tQt::KeyboardModifiers modifiers,\n\t\tMsgId pressedTopicRootId,\n\t\tPeerId pressedSublistPeerId) {\n\tif (chooseHashtag()) {\n\t\treturn true;\n\t} else if (_selectedMorePosts) {\n\t\tif (_searchHashOrCashtag != HashOrCashtag::None) {\n\t\t\t_changeSearchTabRequests.fire(ChatSearchTab::PublicPosts);\n\t\t}\n\t\treturn true;\n\t} else if (_selectedChatTypeFilter) {\n\t\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tthis,\n\t\t\tst::popupMenuWithIcons);\n\t\tfor (const auto tab : {\n\t\t\tChatTypeFilter::All,\n\t\t\tChatTypeFilter::Private,\n\t\t\tChatTypeFilter::Groups,\n\t\t\tChatTypeFilter::Channels,\n\t\t}) {\n\t\t\t_menu->addAction(ChatTypeFilterLabel(tab), [=] {\n\t\t\t\t_changeSearchFilterRequests.fire_copy(tab);\n\t\t\t}, (tab == _searchState.filter)\n\t\t\t\t? &st::mediaPlayerMenuCheck\n\t\t\t\t: nullptr);\n\t\t}\n\t\t_menu->popup(QCursor::pos());\n\t\treturn true;\n\t}\n\tconst auto modifyChosenRow = [&](\n\t\t\tChosenRow row,\n\t\t\tQt::KeyboardModifiers modifiers) {\n\t\trow.newWindow = (modifiers & Qt::ControlModifier);\n\t\trow.userpicClick = isUserpicPressOnWide();\n\t\treturn row;\n\t};\n\tauto chosen = modifyChosenRow(computeChosenRow(), modifiers);\n\tif (chosen.key) {\n\t\tif (IsServerMsgId(chosen.message.fullId.msg)) {\n\t\t\tsession().local().saveRecentSearchHashtags(_filter);\n\t\t}\n\t\tif (!chosen.message.fullId) {\n\t\t\tif (const auto history = chosen.key.history()) {\n\t\t\t\tif (history->peer->forum()) {\n\t\t\t\t\tchosen.topicJumpRootId = pressedTopicRootId;\n\t\t\t\t} else if (history->peer->amMonoforumAdmin()) {\n\t\t\t\t\tchosen.sublistJumpPeerId = pressedSublistPeerId;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t_chosenRow.fire_copy(chosen);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nRowDescriptor InnerWidget::chatListEntryBefore(\n\t\tconst RowDescriptor &which) const {\n\tif (!which.key) {\n\t\treturn RowDescriptor();\n\t}\n\tif (_state == WidgetState::Default) {\n\t\tif (const auto row = _shownList->getRow(which.key)) {\n\t\t\tconst auto i = _shownList->cfind(row);\n\t\t\tif (i != _shownList->cbegin()) {\n\t\t\t\treturn RowDescriptor(\n\t\t\t\t\t(*(i - 1))->key(),\n\t\t\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t\t\t}\n\t\t}\n\t\treturn RowDescriptor();\n\t}\n\n\tconst auto whichThread = which.key.thread();\n\tif (!whichThread) {\n\t\treturn RowDescriptor();\n\t}\n\tif (!_searchResults.empty()) {\n\t\tfor (auto b = _searchResults.cbegin(), i = b + 1, e = _searchResults.cend(); i != e; ++i) {\n\t\t\tif (isSearchResultActive(i->get(), which)) {\n\t\t\t\tconst auto j = i - 1;\n\t\t\t\treturn RowDescriptor(\n\t\t\t\t\t(*j)->item()->history(),\n\t\t\t\t\t(*j)->item()->fullId());\n\t\t\t}\n\t\t}\n\t\tif (isSearchResultActive(_searchResults[0].get(), which)) {\n\t\t\tif (_peerSearchResults.empty()) {\n\t\t\t\tif (_filterResults.empty()) {\n\t\t\t\t\treturn RowDescriptor();\n\t\t\t\t}\n\t\t\t\treturn RowDescriptor(\n\t\t\t\t\t_filterResults.back().key(),\n\t\t\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t\t\t}\n\t\t\treturn RowDescriptor(\n\t\t\t\tsession().data().history(_peerSearchResults.back()->peer),\n\t\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t\t}\n\t}\n\tif (const auto history = whichThread->asHistory()) {\n\t\tif (!_peerSearchResults.empty()\n\t\t\t&& _peerSearchResults[0]->peer == history->peer) {\n\t\t\tif (_filterResults.empty()) {\n\t\t\t\treturn RowDescriptor();\n\t\t\t}\n\t\t\treturn RowDescriptor(\n\t\t\t\t_filterResults.back().key(),\n\t\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t\t}\n\t\tif (!_peerSearchResults.empty()) {\n\t\t\tfor (auto b = _peerSearchResults.cbegin(), i = b + 1, e = _peerSearchResults.cend(); i != e; ++i) {\n\t\t\t\tif ((*i)->peer == history->peer) {\n\t\t\t\t\treturn RowDescriptor(\n\t\t\t\t\t\tsession().data().history((*(i - 1))->peer),\n\t\t\t\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (_filterResults.empty() || _filterResults[0].key() == which.key) {\n\t\treturn RowDescriptor();\n\t}\n\n\tfor (auto b = _filterResults.cbegin(), i = b + 1, e = _filterResults.cend(); i != e; ++i) {\n\t\tif (i->key() == which.key) {\n\t\t\treturn RowDescriptor(\n\t\t\t\t(i - 1)->key(),\n\t\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t\t}\n\t}\n\treturn RowDescriptor();\n}\n\nRowDescriptor InnerWidget::chatListEntryAfter(\n\t\tconst RowDescriptor &which) const {\n\tif (!which.key) {\n\t\treturn RowDescriptor();\n\t}\n\tif (_state == WidgetState::Default) {\n\t\tif (const auto row = _shownList->getRow(which.key)) {\n\t\t\tconst auto i = _shownList->cfind(row) + 1;\n\t\t\tif (i != _shownList->cend()) {\n\t\t\t\treturn RowDescriptor(\n\t\t\t\t\t(*i)->key(),\n\t\t\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t\t\t}\n\t\t}\n\t\treturn RowDescriptor();\n\t}\n\n\tconst auto whichThread = which.key.thread();\n\tif (!whichThread) {\n\t\treturn RowDescriptor();\n\t}\n\tfor (auto i = _searchResults.cbegin(), e = _searchResults.cend(); i != e; ++i) {\n\t\tif (isSearchResultActive(i->get(), which)) {\n\t\t\tif (++i != e) {\n\t\t\t\treturn RowDescriptor(\n\t\t\t\t\t(*i)->item()->history(),\n\t\t\t\t\t(*i)->item()->fullId());\n\t\t\t}\n\t\t\treturn RowDescriptor();\n\t\t}\n\t}\n\tif (const auto history = whichThread->asHistory()) {\n\t\tfor (auto i = _peerSearchResults.cbegin(), e = _peerSearchResults.cend(); i != e; ++i) {\n\t\t\tif ((*i)->peer == history->peer) {\n\t\t\t\t++i;\n\t\t\t\tif (i != e) {\n\t\t\t\t\treturn RowDescriptor(\n\t\t\t\t\t\tsession().data().history((*i)->peer),\n\t\t\t\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t\t\t\t} else if (!_searchResults.empty()) {\n\t\t\t\t\treturn RowDescriptor(\n\t\t\t\t\t\t_searchResults.front()->item()->history(),\n\t\t\t\t\t\t_searchResults.front()->item()->fullId());\n\t\t\t\t}\n\t\t\t\treturn RowDescriptor();\n\t\t\t}\n\t\t}\n\t}\n\tfor (auto i = _filterResults.cbegin(), e = _filterResults.cend(); i != e; ++i) {\n\t\tif ((*i).key() == which.key) {\n\t\t\t++i;\n\t\t\tif (i != e) {\n\t\t\t\treturn RowDescriptor(\n\t\t\t\t\t(*i).key(),\n\t\t\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t\t\t} else if (!_peerSearchResults.empty()) {\n\t\t\t\treturn RowDescriptor(\n\t\t\t\t\tsession().data().history(_peerSearchResults.front()->peer),\n\t\t\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t\t\t} else if (!_searchResults.empty()) {\n\t\t\t\treturn RowDescriptor(\n\t\t\t\t\t_searchResults.front()->item()->history(),\n\t\t\t\t\t_searchResults.front()->item()->fullId());\n\t\t\t}\n\t\t\treturn RowDescriptor();\n\t\t}\n\t}\n\treturn RowDescriptor();\n}\n\nRowDescriptor InnerWidget::chatListEntryFirst() const {\n\tif (_state == WidgetState::Default) {\n\t\tconst auto i = _shownList->cbegin();\n\t\tif (i != _shownList->cend()) {\n\t\t\treturn RowDescriptor(\n\t\t\t\t(*i)->key(),\n\t\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t\t}\n\t\treturn RowDescriptor();\n\t} else if (!_filterResults.empty()) {\n\t\treturn RowDescriptor(\n\t\t\t_filterResults.front().key(),\n\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t} else if (!_peerSearchResults.empty()) {\n\t\treturn RowDescriptor(\n\t\t\tsession().data().history(_peerSearchResults.front()->peer),\n\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t} else if (!_searchResults.empty()) {\n\t\treturn RowDescriptor(\n\t\t\t_searchResults.front()->item()->history(),\n\t\t\t_searchResults.front()->item()->fullId());\n\t}\n\treturn RowDescriptor();\n}\n\nRowDescriptor InnerWidget::chatListEntryLast() const {\n\tif (_state == WidgetState::Default) {\n\t\tconst auto i = _shownList->cend();\n\t\tif (i != _shownList->cbegin()) {\n\t\t\treturn RowDescriptor(\n\t\t\t\t(*(i - 1))->key(),\n\t\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t\t}\n\t\treturn RowDescriptor();\n\t} else if (!_searchResults.empty()) {\n\t\treturn RowDescriptor(\n\t\t\t_searchResults.back()->item()->history(),\n\t\t\t_searchResults.back()->item()->fullId());\n\t} else if (!_peerSearchResults.empty()) {\n\t\treturn RowDescriptor(\n\t\t\tsession().data().history(_peerSearchResults.back()->peer),\n\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t} else if (!_filterResults.empty()) {\n\t\treturn RowDescriptor(\n\t\t\t_filterResults.back().key(),\n\t\t\tFullMsgId(PeerId(), ShowAtUnreadMsgId));\n\t}\n\treturn RowDescriptor();\n}\n\nvoid InnerWidget::setupOnlineStatusCheck() {\n\tsession().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::OnlineStatus\n\t\t| Data::PeerUpdate::Flag::GroupCall\n\t\t| Data::PeerUpdate::Flag::MessagesTTL\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tconst auto &peer = update.peer;\n\t\tif (const auto user = peer->asUser()) {\n\t\t\tif (user->isSelf()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (const auto history = session().data().historyLoaded(user)) {\n\t\t\t\tupdateRowCornerStatusShown(history);\n\t\t\t}\n\t\t} else if (const auto group = peer->asMegagroup()) {\n\t\t\tif (const auto history = session().data().historyLoaded(group)) {\n\t\t\t\tupdateRowCornerStatusShown(history);\n\t\t\t}\n\t\t} else if (peer->messagesTTL()) {\n\t\t\tif (const auto history = session().data().historyLoaded(peer)) {\n\t\t\t\tupdateRowCornerStatusShown(history);\n\t\t\t}\n\t\t}\n\t}, lifetime());\n}\n\nvoid InnerWidget::repaintDialogRowCornerStatus(not_null<History*> history) {\n\tconst auto user = history->peer->isUser();\n\tconst auto size = user\n\t\t? st::dialogsOnlineBadgeSize\n\t\t: st::dialogsCallBadgeSize;\n\tconst auto stroke = st::dialogsOnlineBadgeStroke;\n\tconst auto skip = user\n\t\t? st::dialogsOnlineBadgeSkip\n\t\t: st::dialogsCallBadgeSkip;\n\tconst auto updateRect = QRect(\n\t\t_st->photoSize - skip.x() - size,\n\t\t_st->photoSize - skip.y() - size,\n\t\tsize,\n\t\tsize\n\t).marginsAdded(\n\t\t{ stroke, stroke, stroke, stroke }\n\t).translated(\n\t\tst::defaultDialogRow.padding.left(),\n\t\tst::defaultDialogRow.padding.top()\n\t);\n\tconst auto ttlUpdateRect = !history->peer->messagesTTL()\n\t\t? QRect()\n\t\t: Dialogs::CornerBadgeTTLRect(\n\t\t\t_st->photoSize\n\t\t).translated(\n\t\t\tst::defaultDialogRow.padding.left(),\n\t\t\tst::defaultDialogRow.padding.top()\n\t\t);\n\tupdateDialogRow(\n\t\tRowDescriptor(\n\t\t\thistory,\n\t\t\tFullMsgId()),\n\t\tupdateRect.united(ttlUpdateRect),\n\t\tUpdateRowSection::Default | UpdateRowSection::Filtered);\n}\n\nvoid InnerWidget::updateRowCornerStatusShown(not_null<History*> history) {\n\tconst auto repaint = [=] {\n\t\trepaintDialogRowCornerStatus(history);\n\t};\n\trepaint();\n\n\tconst auto findRow = [&](not_null<History*> history)\n\t\t-> std::pair<Row*, int> {\n\t\tif (state() == WidgetState::Default) {\n\t\t\tconst auto row = _shownList->getRow({ history });\n\t\t\treturn { row, row ? defaultRowTop(row) : 0 };\n\t\t}\n\t\tconst auto i = ranges::find(\n\t\t\t_filterResults,\n\t\t\tKey(history),\n\t\t\t&FilterResult::key);\n\t\tconst auto index = (i - begin(_filterResults));\n\t\tconst auto row = (i == end(_filterResults)) ? nullptr : i->row.get();\n\t\treturn { row, filteredOffset() + index * _st->height };\n\t};\n\tif (const auto &[row, top] = findRow(history); row != nullptr) {\n\t\tconst auto visible = (top < _visibleBottom)\n\t\t\t&& (top + _st->height > _visibleTop);\n\t\trow->updateCornerBadgeShown(\n\t\t\thistory->peer,\n\t\t\tvisible ? Fn<void()>(crl::guard(this, repaint)) : nullptr);\n\t}\n}\n\nRowDescriptor InnerWidget::resolveChatNext(RowDescriptor from) const {\n\tconst auto row = from.key ? from : _controller->activeChatEntryCurrent();\n\treturn row.key\n\t\t? computeJump(\n\t\t\tchatListEntryAfter(row),\n\t\t\tJumpSkip::NextOrEnd)\n\t\t: row;\n}\n\nRowDescriptor InnerWidget::resolveChatPrevious(RowDescriptor from) const {\n\tconst auto row = from.key ? from : _controller->activeChatEntryCurrent();\n\treturn row.key\n\t\t? computeJump(\n\t\t\tchatListEntryBefore(row),\n\t\t\tJumpSkip::PreviousOrBegin)\n\t\t: row;\n}\n\nvoid InnerWidget::setupShortcuts() {\n\tShortcuts::Requests(\n\t) | rpl::filter([=] {\n\t\treturn isActiveWindow()\n\t\t\t&& !_controller->isLayerShown()\n\t\t\t&& !_controller->window().locked()\n\t\t\t&& !_childListShown.current().shown\n\t\t\t&& !_chatPreviewRow.key;\n\t}) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {\n\t\tusing Command = Shortcuts::Command;\n\n\t\tconst auto row = _controller->activeChatEntryCurrent();\n\t\t// Those should be computed before the call to request->handle.\n\t\tconst auto previous = row.key\n\t\t\t? computeJump(\n\t\t\t\tchatListEntryBefore(row),\n\t\t\t\tJumpSkip::PreviousOrBegin)\n\t\t\t: row;\n\t\tconst auto next = row.key\n\t\t\t? computeJump(\n\t\t\t\tchatListEntryAfter(row),\n\t\t\t\tJumpSkip::NextOrEnd)\n\t\t\t: row;\n\t\tconst auto first = [&] {\n\t\t\tconst auto to = chatListEntryFirst();\n\t\t\tconst auto jump = computeJump(to, JumpSkip::NextOrOriginal);\n\t\t\treturn (to == row || jump == row || to == previous) ? to : jump;\n\t\t}();\n\t\tconst auto last = [&] {\n\t\t\tconst auto to = chatListEntryLast();\n\t\t\tconst auto jump = computeJump(to, JumpSkip::PreviousOrOriginal);\n\t\t\treturn (to == row || jump == row || to == next) ? to : jump;\n\t\t}();\n\t\tif (row.key) {\n\t\t\trequest->check(Command::ChatPrevious) && request->handle([=] {\n\t\t\t\treturn jumpToDialogRow(previous);\n\t\t\t});\n\t\t\trequest->check(Command::ChatNext) && request->handle([=] {\n\t\t\t\treturn jumpToDialogRow(next);\n\t\t\t});\n\t\t} else if (_state == WidgetState::Default\n\t\t\t? !_shownList->empty()\n\t\t\t: !_filterResults.empty()) {\n\t\t\trequest->check(Command::ChatNext) && request->handle([=] {\n\t\t\t\treturn jumpToDialogRow(first);\n\t\t\t});\n\t\t}\n\t\trequest->check(Command::ChatFirst) && request->handle([=] {\n\t\t\treturn jumpToDialogRow(first);\n\t\t});\n\t\trequest->check(Command::ChatLast) && request->handle([=] {\n\t\t\treturn jumpToDialogRow(last);\n\t\t});\n\t\trequest->check(Command::ChatSelf) && request->handle([=] {\n\t\t\t_controller->showThread(\n\t\t\t\tsession().data().history(session().user()),\n\t\t\t\tShowAtUnreadMsgId,\n\t\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\t\treturn true;\n\t\t});\n\t\trequest->check(Command::ShowArchive) && request->handle([=] {\n\t\t\tconst auto folder = session().data().folderLoaded(\n\t\t\t\tData::Folder::kId);\n\t\t\tif (folder && !folder->chatsList()->empty()) {\n\t\t\t\tconst auto controller = _controller;\n\t\t\t\tcontroller->openFolder(folder);\n\n\t\t\t\t// Calling openFolder() could've destroyed this widget.\n\t\t\t\tcontroller->window().hideSettingsAndLayer();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\n\t\tif (session().data().chatsFilters().has()) {\n\t\t\tconst auto filters = &session().data().chatsFilters();\n\t\t\tconst auto filtersCount = int(filters->list().size());\n\t\t\tauto &&folders = ranges::views::zip(\n\t\t\t\tShortcuts::kShowFolder,\n\t\t\t\tranges::views::ints(0, ranges::unreachable));\n\t\t\tfor (const auto &[command, index] : folders) {\n\t\t\t\tconst auto select = (command == Command::ShowFolderLast)\n\t\t\t\t\t? (filtersCount - 1)\n\t\t\t\t\t: std::clamp(index, 0, filtersCount - 1);\n\t\t\t\trequest->check(command) && request->handle([=] {\n\t\t\t\t\tif (select <= filtersCount) {\n\t\t\t\t\t\t_controller->setActiveChatsFilter(\n\t\t\t\t\t\t\tfilters->lookupId(select));\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tstatic const auto kPinned = {\n\t\t\tCommand::ChatPinned1,\n\t\t\tCommand::ChatPinned2,\n\t\t\tCommand::ChatPinned3,\n\t\t\tCommand::ChatPinned4,\n\t\t\tCommand::ChatPinned5,\n\t\t\tCommand::ChatPinned6,\n\t\t\tCommand::ChatPinned7,\n\t\t\tCommand::ChatPinned8,\n\t\t};\n\t\tauto &&pinned = ranges::views::zip(\n\t\t\tkPinned,\n\t\t\tranges::views::ints(0, ranges::unreachable));\n\t\tfor (const auto &[command, index] : pinned) {\n\t\t\trequest->check(command) && request->handle([=, index = index] {\n\t\t\t\tconst auto list = (_filterId\n\t\t\t\t\t? session().data().chatsFilters().chatsList(_filterId)\n\t\t\t\t\t: session().data().chatsList()\n\t\t\t\t)->indexed();\n\t\t\t\tconst auto count = Dialogs::PinnedDialogsCount(\n\t\t\t\t\t_filterId,\n\t\t\t\t\tlist);\n\t\t\t\tif (index >= count) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst auto skip = Dialogs::FixedOnTopDialogsCount(list);\n\t\t\t\tconst auto row = *(list->cbegin() + skip + index);\n\t\t\t\treturn jumpToDialogRow({ row->key(), FullMsgId() });\n\t\t\t});\n\t\t}\n\n\t\trequest->check(Command::FolderNext) && request->handle([=] {\n\t\t\tusing namespace Window;\n\t\t\treturn CheckAndJumpToNearChatsFilter(_controller, true, true);\n\t\t});\n\n\t\trequest->check(Command::FolderPrevious) && request->handle([=] {\n\t\t\tusing namespace Window;\n\t\t\treturn CheckAndJumpToNearChatsFilter(_controller, false, true);\n\t\t});\n\n\t\trequest->check(Command::ReadChat) && request->handle([=] {\n\t\t\tconst auto thread = _selected ? _selected->thread() : nullptr;\n\t\t\tif (!thread) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (Window::IsUnreadThread(thread)) {\n\t\t\t\tWindow::MarkAsReadThread(thread);\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\n\t\t(!_openedForum)\n\t\t\t&& request->check(Command::ArchiveChat)\n\t\t\t&& request->handle([=] {\n\t\t\t\tconst auto thread = _selected ? _selected->thread() : nullptr;\n\t\t\t\tif (!thread) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst auto history = thread->owningHistory();\n\t\t\t\tconst auto isArchived = history->folder()\n\t\t\t\t\t&& (history->folder()->id() == Data::Folder::kId);\n\n\t\t\t\tWindow::ToggleHistoryArchived(\n\t\t\t\t\t_controller->uiShow(),\n\t\t\t\t\thistory,\n\t\t\t\t\t!isArchived);\n\t\t\t\treturn true;\n\t\t\t});\n\n\t\trequest->check(Command::ShowContacts) && request->handle([=] {\n\t\t\t_controller->show(PrepareContactsBox(_controller));\n\t\t\treturn true;\n\t\t});\n\n\t\tif (session().supportMode() && row.key.history()) {\n\t\t\trequest->check(\n\t\t\t\tCommand::SupportScrollToCurrent\n\t\t\t) && request->handle([=] {\n\t\t\t\tscrollToEntry(row);\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\t}, lifetime());\n}\n\nRowDescriptor InnerWidget::computeJump(\n\t\tconst RowDescriptor &to,\n\t\tJumpSkip skip) const {\n\tauto result = to;\n\tif (result.key) {\n\t\tconst auto down = (skip == JumpSkip::NextOrEnd)\n\t\t\t|| (skip == JumpSkip::NextOrOriginal);\n\t\tconst auto needSkip = [&] {\n\t\t\treturn (result.key.folder() != nullptr)\n\t\t\t\t|| (session().supportMode()\n\t\t\t\t\t&& !result.key.entry()->chatListBadgesState().unread);\n\t\t};\n\t\twhile (needSkip()) {\n\t\t\tconst auto next = down\n\t\t\t\t? chatListEntryAfter(result)\n\t\t\t\t: chatListEntryBefore(result);\n\t\t\tif (next.key) {\n\t\t\t\tresult = next;\n\t\t\t} else {\n\t\t\t\tif (skip == JumpSkip::PreviousOrOriginal\n\t\t\t\t\t|| skip == JumpSkip::NextOrOriginal) {\n\t\t\t\t\tresult = to;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nbool InnerWidget::jumpToDialogRow(RowDescriptor to) {\n\tif (to == chatListEntryLast()) {\n\t\t_listBottomReached.fire({});\n\t}\n\tif (uniqueSearchResults()) {\n\t\tto.fullId = FullMsgId();\n\t}\n\treturn _controller->jumpToChatListEntry(to);\n}\n\nrpl::producer<UserId> InnerWidget::openBotMainAppRequests() const {\n\treturn _openBotMainAppRequests.events();\n}\n\nvoid InnerWidget::setSwipeContextData(\n\t\tint64 key,\n\t\tstd::optional<Ui::Controls::SwipeContextData> data) {\n\tif (!key) {\n\t\treturn;\n\t}\n\tif (!data) {\n\t\t_activeQuickAction = nullptr;\n\t\treturn;\n\t}\n\tconst auto context = ensureQuickAction(key);\n\n\tcontext->data = base::take(*data);\n\tif (context->data.msgBareId) {\n\t\tconstexpr auto kStartAnimateThreshold = 0.32;\n\t\tconstexpr auto kResetAnimateThreshold = 0.24;\n\t\tif (context->data.ratio > kStartAnimateThreshold) {\n\t\t\tif (context->icon\n\t\t\t\t&& !context->icon->frameIndex()\n\t\t\t\t&& !context->icon->animating()) {\n\t\t\t\tcontext->icon->animate(\n\t\t\t\t\t[=] { update(); },\n\t\t\t\t\t0,\n\t\t\t\t\tcontext->icon->framesCount());\n\t\t\t}\n\t\t} else if (context->data.ratio < kResetAnimateThreshold) {\n\t\t\tif (context->icon\n\t\t\t\t&& context->icon->frameIndex()) {\n\t\t\t\tcontext->icon->jumpTo(0, [=] { update(); });\n\t\t\t}\n\t\t}\n\t\tupdate();\n\t}\n}\n\nnot_null<Ui::QuickActionContext*> InnerWidget::ensureQuickAction(int64 key) {\n\tExpects(key != 0);\n\n\tif (_activeQuickAction) {\n\t\tif (_activeQuickAction->data.msgBareId == key) {\n\t\t\treturn _activeQuickAction.get();\n\t\t} else {\n\t\t\tdeactivateQuickAction();\n\t\t}\n\t}\n\t_activeQuickAction = std::make_unique<Ui::QuickActionContext>();\n\t_activeQuickAction->data.msgBareId = key;\n\treturn _activeQuickAction.get();\n}\n\nint64 InnerWidget::calcSwipeKey(int top) {\n\ttop -= dialogsOffset();\n\tif (top < 0) {\n\t\treturn 0;\n\t}\n\tfor (auto it = _shownList->begin(); it != _shownList->end(); ++it) {\n\t\tconst auto row = it->get();\n\t\tconst auto from = row->top();\n\t\tconst auto to = from + row->height();\n\t\tif (top >= from && top < to) {\n\t\t\tif (const auto peer = row->key().peer()) {\n\t\t\t\treturn peer->id.value;\n\t\t\t}\n\t\t\treturn 0;\n\t\t}\n\t}\n\treturn 0;\n}\n\nvoid InnerWidget::prepareQuickAction(\n\t\tint64 key,\n\t\tDialogs::Ui::QuickDialogAction action) {\n\tExpects(key != 0);\n\n\tconst auto context = ensureQuickAction(key);\n\tauto name = ResolveQuickDialogLottieIconName(\n\t\tResolveQuickDialogLabel(\n\t\t\tsession().data().history(PeerId(key)),\n\t\t\taction,\n\t\t\t_filterId));\n\tcontext->icon = Lottie::MakeIcon({\n\t\t.name = std::move(name),\n\t\t.sizeOverride = Size(st::dialogsQuickActionSize),\n\t});\n\tcontext->action = action;\n}\n\nvoid InnerWidget::clearQuickActions() {\n\t_inactiveQuickActions.clear();\n}\n\nvoid InnerWidget::deactivateQuickAction() {\n\tif (_activeQuickAction) {\n\t\t_activeQuickAction->finishedAt = crl::now();\n\t\t_inactiveQuickActions.push_back(\n\t\t\tQuickActionPtr{ _activeQuickAction.release() });\n\t}\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_inner_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n#include \"base/object_ptr.h\"\n#include \"base/timer.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"dialogs/ui/dialogs_quick_action_context.h\"\n#include \"data/data_messages.h\"\n#include \"ui/dragging_scroll_manager.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/userpic_view.h\"\n\nnamespace style {\nstruct DialogRow;\nstruct DialogRightButton;\n} // namespace style\n\nnamespace Api {\nstruct PeerSearchResult;\n} // namespace Api\n\nnamespace MTP {\nclass Error;\n} // namespace MTP\n\nnamespace Lottie {\nclass Icon;\n} // namespace Lottie\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass IconButton;\nclass PopupMenu;\nclass FlatLabel;\nclass VerticalLayout;\nclass RoundButton;\nstruct ScrollToRequest;\nnamespace Controls {\nenum class QuickDialogAction;\n} // namespace Controls\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Data {\nclass ChatFilter;\nclass Thread;\nclass Folder;\nclass Forum;\nclass SavedMessages;\nstruct ReactionId;\n} // namespace Data\n\nnamespace Dialogs::Ui {\nusing namespace ::Ui;\nclass VideoUserpic;\nstruct PaintContext;\nstruct TopicJumpCache;\n} // namespace Dialogs::Ui\n\nnamespace Dialogs {\n\nclass Row;\nclass FakeRow;\nclass IndexedList;\nclass SearchTags;\nclass SearchEmpty;\nclass ChatSearchIn;\nenum class HashOrCashtag : uchar;\nstruct RightButton;\nenum class ChatTypeFilter : uchar;\n\nstruct ChosenRow {\n\tKey key;\n\tData::MessagePosition message;\n\tMsgId topicJumpRootId;\n\tPeerId sublistJumpPeerId;\n\tQByteArray sponsoredRandomId;\n\tbool userpicClick : 1 = false;\n\tbool filteredRow : 1 = false;\n\tbool newWindow : 1 = false;\n};\n\nstruct SearchRequestType {\n\tbool migrated : 1 = false;\n\tbool posts : 1 = false;\n\tbool start : 1 = false;\n\tbool peer : 1 = false;\n\n\tfriend inline constexpr auto operator<=>(\n\t\tSearchRequestType a,\n\t\tSearchRequestType b) = default;\n\tfriend inline constexpr bool operator==(\n\t\tSearchRequestType a,\n\t\tSearchRequestType b) = default;\n};\n\nenum class SearchRequestDelay : uchar {\n\tInCache,\n\tInstant,\n\tDelayed,\n};\n\nenum class WidgetState {\n\tDefault,\n\tFiltered,\n};\n\nclass InnerWidget final : public Ui::RpWidget {\npublic:\n\tusing ChatsFilterTagsKey = int64;\n\n\tstruct ChildListShown {\n\t\tPeerId peerId = 0;\n\t\tfloat64 shown = 0.;\n\t};\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\trpl::producer<ChildListShown> childListShown);\n\n\tvoid searchReceived(\n\t\tstd::vector<not_null<HistoryItem*>> result,\n\t\tHistoryItem *inject,\n\t\tSearchRequestType type,\n\t\tint fullCount);\n\tvoid peerSearchReceived(Api::PeerSearchResult result);\n\n\t[[nodiscard]] FilterId filterId() const;\n\n\tvoid clearSelection();\n\n\tvoid changeOpenedFolder(Data::Folder *folder);\n\tvoid changeOpenedForum(Data::Forum *forum);\n\tvoid showSavedSublists();\n\tvoid selectSkip(int32 direction);\n\tvoid selectSkipPage(int32 pixels, int32 direction);\n\n\tvoid dragLeft();\n\tvoid setNarrowRatio(float64 narrowRatio);\n\n\tvoid clearFilter();\n\tvoid refresh(bool toTop = false);\n\tvoid refreshEmpty();\n\tvoid resizeEmpty();\n\n\tvoid showPeerMenu();\n\n\t[[nodiscard]] bool isUserpicPress() const;\n\t[[nodiscard]] bool isUserpicPressOnWide() const;\n\tvoid cancelChatPreview();\n\tbool scheduleChatPreview(QPoint positionOverride);\n\tbool showChatPreview();\n\tvoid chatPreviewShown(bool shown, RowDescriptor row = {});\n\tbool chooseRow(\n\t\tQt::KeyboardModifiers modifiers = {},\n\t\tMsgId pressedTopicRootId = {},\n\t\tPeerId pressedSublistPeerId = {});\n\n\tvoid scrollToEntry(const RowDescriptor &entry);\n\n\t[[nodiscard]] Data::Folder *shownFolder() const;\n\t[[nodiscard]] Data::Forum *shownForum() const;\n\n\t[[nodiscard]] WidgetState state() const;\n\t[[nodiscard]] not_null<const style::DialogRow*> st() const {\n\t\treturn _st;\n\t}\n\t[[nodiscard]] bool hasFilteredResults() const;\n\n\tvoid searchRequested(bool loading);\n\tvoid applySearchState(SearchState state);\n\t[[nodiscard]] auto searchTagsChanges() const\n\t\t-> rpl::producer<std::vector<Data::ReactionId>>;\n\n\tvoid onHashtagFilterUpdate(QStringView newFilter);\n\tvoid appendToFiltered(Key key);\n\n\tData::Thread *updateFromParentDrag(QPoint globalPosition);\n\n\tvoid setLoadMoreCallback(Fn<void()> callback);\n\tvoid setLoadMoreFilteredCallback(Fn<void()> callback);\n\t[[nodiscard]] rpl::producer<> listBottomReached() const;\n\t[[nodiscard]] auto changeSearchTabRequests() const\n\t\t-> rpl::producer<ChatSearchTab>;\n\t[[nodiscard]] auto changeSearchFilterRequests() const\n\t\t-> rpl::producer<ChatTypeFilter>;\n\t[[nodiscard]] rpl::producer<> cancelSearchRequests() const;\n\t[[nodiscard]] rpl::producer<> cancelSearchFromRequests() const;\n\t[[nodiscard]] rpl::producer<> changeSearchFromRequests() const;\n\t[[nodiscard]] rpl::producer<ChosenRow> chosenRow() const;\n\t[[nodiscard]] rpl::producer<> updated() const;\n\n\t[[nodiscard]] rpl::producer<int> scrollByDeltaRequests() const;\n\t[[nodiscard]] rpl::producer<Ui::ScrollToRequest> mustScrollTo() const;\n\t[[nodiscard]] rpl::producer<Ui::ScrollToRequest> dialogMoved() const;\n\t[[nodiscard]] rpl::producer<SearchRequestDelay> searchRequests() const;\n\t[[nodiscard]] rpl::producer<QString> completeHashtagRequests() const;\n\t[[nodiscard]] rpl::producer<> refreshHashtagsRequests() const;\n\n\t[[nodiscard]] RowDescriptor resolveChatNext(RowDescriptor from = {}) const;\n\t[[nodiscard]] RowDescriptor resolveChatPrevious(RowDescriptor from = {}) const;\n\n\t~InnerWidget();\n\n\tvoid parentGeometryChanged();\n\n\tbool processTouchEvent(not_null<QTouchEvent*> e);\n\t[[nodiscard]] rpl::producer<> touchCancelRequests() const {\n\t\treturn _touchCancelRequests.events();\n\t}\n\n\t[[nodiscard]] rpl::producer<UserId> openBotMainAppRequests() const;\n\n\tvoid setSwipeContextData(\n\t\tint64 key,\n\t\tstd::optional<Ui::Controls::SwipeContextData> data);\n\t[[nodiscard]] int64 calcSwipeKey(int top);\n\tvoid prepareQuickAction(int64 key, Dialogs::Ui::QuickDialogAction);\n\tvoid clearQuickActions();\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\nprivate:\n\tstruct CollapsedRow;\n\tstruct HashtagResult;\n\tstruct SponsoredSearchResult;\n\tstruct PeerSearchResult;\n\tstruct TagCache;\n\n\tenum class JumpSkip {\n\t\tPreviousOrBegin,\n\t\tNextOrEnd,\n\t\tPreviousOrOriginal,\n\t\tNextOrOriginal,\n\t};\n\n\tenum class EmptyState : uchar {\n\t\tNone,\n\t\tLoading,\n\t\tNoContacts,\n\t\tEmptyFolder,\n\t\tEmptyForum,\n\t\tEmptySavedSublists,\n\t};\n\n\tstruct PinnedRow {\n\t\tanim::value yadd;\n\t\tcrl::time animStartTime = 0;\n\t};\n\n\tstruct FilterResult {\n\t\tFilterResult(not_null<Row*> row) : row(row) {\n\t\t}\n\n\t\tnot_null<Row*> row;\n\t\tint top = 0;\n\n\t\t[[nodiscard]] Key key() const;\n\t\t[[nodiscard]] int bottom() const;\n\t};\n\n\tMain::Session &session() const;\n\n\tvoid dialogRowReplaced(Row *oldRow, Row *newRow);\n\n\tvoid setState(WidgetState state);\n\tvoid editOpenedFilter();\n\tvoid repaintCollapsedFolderRow(not_null<Data::Folder*> folder);\n\tvoid refreshWithCollapsedRows(bool toTop = false);\n\tbool needCollapsedRowsRefresh() const;\n\tbool chooseCollapsedRow(Qt::KeyboardModifiers modifiers);\n\tvoid switchToFilter(FilterId filterId);\n\tbool chooseHashtag();\n\tChosenRow computeChosenRow() const;\n\tbool isRowActive(not_null<Row*> row, const RowDescriptor &entry) const;\n\tbool isSearchResultActive(\n\t\tnot_null<FakeRow*> result,\n\t\tconst RowDescriptor &entry) const;\n\n\tvoid repaintDialogRow(FilterId filterId, not_null<Row*> row);\n\tvoid repaintDialogRow(RowDescriptor row);\n\tvoid refreshDialogRow(RowDescriptor row);\n\tbool updateEntryHeight(not_null<Entry*> entry);\n\tvoid showSponsoredMenu(int peerSearchIndex, QPoint globalPos);\n\n\tvoid clearMouseSelection(bool clearSelection = false);\n\tvoid mousePressReleased(\n\t\tQPoint globalPosition,\n\t\tQt::MouseButton button,\n\t\tQt::KeyboardModifiers modifiers);\n\tvoid processGlobalForceClick(QPoint globalPosition);\n\tvoid clearIrrelevantState();\n\tvoid selectByMouse(QPoint globalPosition);\n\tvoid preloadRowsData();\n\tvoid scrollToItem(int top, int height);\n\tvoid scrollToDefaultSelected();\n\tvoid setCollapsedPressed(int pressed);\n\tvoid setPressed(\n\t\tRow *pressed,\n\t\tbool pressedTopicJump,\n\t\tbool pressedRightButton);\n\tvoid clearPressed();\n\tvoid setHashtagPressed(int pressed);\n\tvoid setFilteredPressed(\n\t\tint pressed,\n\t\tbool pressedTopicJump,\n\t\tbool pressedRightButton);\n\tvoid setPeerSearchPressed(int pressed, bool pressedRightButton);\n\tvoid setPreviewPressed(int pressed);\n\tvoid setSearchedPressed(int pressed);\n\tbool isPressed() const {\n\t\treturn (_collapsedPressed >= 0)\n\t\t\t|| _pressed\n\t\t\t|| (_hashtagPressed >= 0)\n\t\t\t|| (_filteredPressed >= 0)\n\t\t\t|| (_peerSearchPressed >= 0)\n\t\t\t|| (_previewPressed >= 0)\n\t\t\t|| (_searchedPressed >= 0)\n\t\t\t|| _pressedMorePosts\n\t\t\t|| _pressedChatTypeFilter;\n\t}\n\tbool isSelected() const {\n\t\treturn (_collapsedSelected >= 0)\n\t\t\t|| _selected\n\t\t\t|| (_hashtagSelected >= 0)\n\t\t\t|| (_filteredSelected >= 0)\n\t\t\t|| (_peerSearchSelected >= 0)\n\t\t\t|| (_previewSelected >= 0)\n\t\t\t|| (_searchedSelected >= 0)\n\t\t\t|| _selectedMorePosts\n\t\t\t|| _selectedChatTypeFilter;\n\t}\n\tbool uniqueSearchResults() const;\n\tbool hasHistoryInResults(not_null<History*> history) const;\n\n\tint defaultRowTop(not_null<Row*> row) const;\n\tvoid setupOnlineStatusCheck();\n\tvoid jumpToTop();\n\n\tvoid updateRowCornerStatusShown(not_null<History*> history);\n\tvoid repaintDialogRowCornerStatus(not_null<History*> history);\n\n\tbool addBotAppRipple(QPoint origin, Fn<void()> updateCallback);\n\tbool addQuickActionRipple(not_null<Row*> row, Fn<void()> updateCallback);\n\n\tbool addRightButtonRipple(QPoint origin, Fn<void()> updateCallback);\n\n\tvoid setupShortcuts();\n\tRowDescriptor computeJump(\n\t\tconst RowDescriptor &to,\n\t\tJumpSkip skip) const;\n\tbool jumpToDialogRow(RowDescriptor to);\n\n\tRowDescriptor chatListEntryBefore(const RowDescriptor &which) const;\n\tRowDescriptor chatListEntryAfter(const RowDescriptor &which) const;\n\tRowDescriptor chatListEntryFirst() const;\n\tRowDescriptor chatListEntryLast() const;\n\n\tvoid itemRemoved(not_null<const HistoryItem*> item);\n\tenum class UpdateRowSection {\n\t\tDefault       = (1 << 0),\n\t\tFiltered      = (1 << 1),\n\t\tPeerSearch    = (1 << 2),\n\t\tMessageSearch = (1 << 3),\n\t\tAll           = Default | Filtered | PeerSearch | MessageSearch,\n\t};\n\tusing UpdateRowSections = base::flags<UpdateRowSection>;\n\tfriend inline constexpr auto is_flag_type(UpdateRowSection) { return true; };\n\n\tvoid updateSearchResult(not_null<PeerData*> peer);\n\tvoid updateDialogRow(\n\t\tRowDescriptor row,\n\t\tQRect updateRect = QRect(),\n\t\tUpdateRowSections sections = UpdateRowSection::All);\n\tvoid fillSupportSearchMenu(not_null<Ui::PopupMenu*> menu);\n\tvoid fillArchiveSearchMenu(not_null<Ui::PopupMenu*> menu);\n\n\tvoid refreshShownList();\n\t[[nodiscard]] int skipTopHeight() const;\n\t[[nodiscard]] int collapsedRowsOffset() const;\n\t[[nodiscard]] int dialogsOffset() const;\n\t[[nodiscard]] int shownHeight(int till = -1) const;\n\t[[nodiscard]] int fixedOnTopCount() const;\n\t[[nodiscard]] int pinnedOffset() const;\n\t[[nodiscard]] int filteredOffset() const;\n\t[[nodiscard]] int filteredIndex(int y) const;\n\t[[nodiscard]] int filteredHeight(int till = -1) const;\n\t[[nodiscard]] int peerSearchOffset() const;\n\t[[nodiscard]] int searchInChatOffset() const;\n\t[[nodiscard]] int previewOffset() const;\n\t[[nodiscard]] int searchedOffset() const;\n\t[[nodiscard]] int searchInChatSkip() const;\n\t[[nodiscard]] int hashtagsOffset() const;\n\n\tvoid paintCollapsedRows(\n\t\tPainter &p,\n\t\tQRect clip) const;\n\tvoid paintCollapsedRow(\n\t\tPainter &p,\n\t\tnot_null<const CollapsedRow*> row,\n\t\tbool selected) const;\n\tvoid paintPeerSearchResult(\n\t\tPainter &p,\n\t\tnot_null<const PeerSearchResult*> result,\n\t\tconst Ui::PaintContext &context);\n\tvoid paintSearchTags(\n\t\tPainter &p,\n\t\tconst Ui::PaintContext &context) const;\n\t//void paintSearchInChat(\n\t//\tPainter &p,\n\t//\tconst Ui::PaintContext &context) const;\n\t//void paintSearchInPeer(\n\t//\tPainter &p,\n\t//\tnot_null<PeerData*> peer,\n\t//\tUi::PeerUserpicView &userpic,\n\t//\tint top,\n\t//\tconst Ui::Text::String &text) const;\n\t//void paintSearchInSaved(\n\t//\tPainter &p,\n\t//\tint top,\n\t//\tconst Ui::Text::String &text) const;\n\t//void paintSearchInReplies(\n\t//\tPainter &p,\n\t//\tint top,\n\t//\tconst Ui::Text::String &text) const;\n\t//void paintSearchInTopic(\n\t//\tPainter &p,\n\t//\tconst Ui::PaintContext &context,\n\t//\tnot_null<Data::ForumTopic*> topic,\n\t//\tUi::PeerUserpicView &userpic,\n\t//\tint top,\n\t//\tconst Ui::Text::String &text) const;\n\t//template <typename PaintUserpic>\n\t//void paintSearchInFilter(\n\t//\tPainter &p,\n\t//\tPaintUserpic paintUserpic,\n\t//\tint top,\n\t//\tconst style::icon *icon,\n\t//\tconst Ui::Text::String &text) const;\n\tvoid updateSearchIn();\n\tvoid repaintSearchResult(int index);\n\tvoid repaintPreviewResult(int index);\n\n\t[[nodiscard]] bool computeSearchWithPostsPreview() const;\n\n\tUi::VideoUserpic *validateVideoUserpic(not_null<Row*> row);\n\tUi::VideoUserpic *validateVideoUserpic(not_null<History*> history);\n\n\tRow *shownRowByKey(Key key);\n\tvoid clearSearchResults(bool alsoPeerSearchResults = true);\n\tvoid clearPeerSearchResults();\n\tvoid clearPreviewResults();\n\tvoid updateSelectedRow(Key key = Key());\n\tvoid trackResultsHistory(not_null<History*> history);\n\n\t[[nodiscard]] QBrush currentBg() const;\n\t[[nodiscard]] RowDescriptor computeChatPreviewRow() const;\n\n\t[[nodiscard]] const std::vector<Key> &pinnedChatsOrder() const;\n\tvoid checkReorderPinnedStart(QPoint localPosition);\n\tvoid startReorderPinned(QPoint localPosition);\n\tint updateReorderIndexGetCount();\n\tbool updateReorderPinned(QPoint localPosition);\n\t[[nodiscard]] bool skipChatsListFreeze() const;\n\tvoid unfreezeShownList(bool updateIfWasFrozen);\n\tvoid finishReorderPinned();\n\tbool finishReorderOnRelease();\n\tvoid stopReorderPinned();\n\tint countPinnedIndex(Row *ofRow);\n\tvoid savePinnedOrder();\n\tbool pinnedShiftAnimationCallback(crl::time now);\n\tvoid handleChatListEntryRefreshes();\n\tvoid moveSearchIn();\n\tvoid dragPinnedFromTouch();\n\t[[nodiscard]] bool hasChatTypeFilter() const;\n\n\tvoid saveChatsFilterScrollState(FilterId filterId);\n\tvoid restoreChatsFilterScrollState(FilterId filterId);\n\n\t[[nodiscard]] not_null<Ui::QuickActionContext*> ensureQuickAction(\n\t\tint64 key);\n\tvoid deactivateQuickAction();\n\n\t[[nodiscard]] bool lookupIsInBotAppButton(\n\t\tRow *row,\n\t\tQPoint localPosition);\n\t[[nodiscard]] bool lookupIsInRightButton(\n\t\tconst RightButton &button,\n\t\tQPoint localPosition);\n\t[[nodiscard]] RightButton *maybeCacheRightButton(Row *row);\n\tvoid fillRightButton(\n\t\tRightButton &button,\n\t\tconst TextWithEntities &text,\n\t\tconst style::DialogRightButton &st);\n\n\t[[nodiscard]] QImage *cacheChatsFilterTag(\n\t\tconst Data::ChatFilter &filter,\n\t\tuint8 more,\n\t\tbool active);\n\n\tvoid performDrag();\n\n\tconst not_null<Window::SessionController*> _controller;\n\n\tnot_null<IndexedList*> _shownList;\n\tFilterId _filterId = 0;\n\tbool _mouseSelection = false;\n\tstd::optional<QPoint> _lastMousePosition;\n\tint _lastRowLocalMouseX = -1;\n\tQt::MouseButton _pressButton = Qt::LeftButton;\n\n\tData::Folder *_openedFolder = nullptr;\n\tData::Forum *_openedForum = nullptr;\n\trpl::lifetime _openedForumLifetime;\n\n\tstd::vector<std::unique_ptr<CollapsedRow>> _collapsedRows;\n\tnot_null<const style::DialogRow*> _st;\n\tmutable std::unique_ptr<Ui::TopicJumpCache> _topicJumpCache;\n\tbool _selectedChatTypeFilter = false;\n\tbool _pressedChatTypeFilter = false;\n\tbool _selectedMorePosts = false;\n\tbool _pressedMorePosts = false;\n\tint _collapsedSelected = -1;\n\tint _collapsedPressed = -1;\n\tbool _skipTopDialog = false;\n\tRow *_selected = nullptr;\n\tRow *_pressed = nullptr;\n\tMsgId _pressedTopicJumpRootId;\n\tPeerId _pressedSublistJumpPeerId;\n\tbool _selectedTopicJump = false;\n\tbool _pressedTopicJump = false;\n\n\tRightButton *_pressedRightButtonData = nullptr;\n\tbool _pressedRightButtonSponsored = false;\n\tbool _selectedRightButton = false;\n\tbool _pressedRightButton = false;\n\n\tRow *_qdragging = nullptr;\n\n\tRow *_dragging = nullptr;\n\tint _draggingIndex = -1;\n\tint _aboveIndex = -1;\n\tQPoint _dragStart;\n\tstd::vector<PinnedRow> _pinnedRows;\n\tUi::Animations::Basic _pinnedShiftAnimation;\n\tbase::flat_set<Key> _pinnedOnDragStart;\n\n\t// Remember the last currently dragged row top shift for updating area.\n\tint _aboveTopShift = -1;\n\tint _narrowWidth = 0;\n\n\tint _visibleTop = 0;\n\tint _visibleBottom = 0;\n\tQString _filter, _hashtagFilter;\n\n\tstd::vector<std::unique_ptr<HashtagResult>> _hashtagResults;\n\tint _hashtagSelected = -1;\n\tint _hashtagPressed = -1;\n\tbool _hashtagDeleteSelected = false;\n\tbool _hashtagDeletePressed = false;\n\n\tstd::vector<FilterResult> _filterResults;\n\tbase::flat_map<Key, std::unique_ptr<Row>> _filterResultsGlobal;\n\tint _filteredSelected = -1;\n\tint _filteredPressed = -1;\n\n\tEmptyState _emptyState = EmptyState::None;\n\n\tbase::flat_set<not_null<History*>> _trackedHistories;\n\trpl::lifetime _trackedLifetime;\n\n\tQString _peerSearchQuery;\n\tbase::flat_set<not_null<PeerData*>> _sponsoredRemoved;\n\tstd::vector<std::unique_ptr<PeerSearchResult>> _peerSearchResults;\n\tint _peerSearchSelected = -1;\n\tint _peerSearchPressed = -1;\n\tint _peerSearchMenu = -1;\n\n\tstd::vector<std::unique_ptr<FakeRow>> _previewResults;\n\tint _previewCount = 0;\n\tint _previewSelected = -1;\n\tint _previewPressed = -1;\n\tint _morePostsWidth = 0;\n\tint _chatTypeFilterWidth = 0;\n\n\tstd::vector<std::unique_ptr<FakeRow>> _searchResults;\n\tint _searchedCount = 0;\n\tint _searchedMigratedCount = 0;\n\tint _searchedSelected = -1;\n\tint _searchedPressed = -1;\n\n\tWidgetState _state = WidgetState::Default;\n\n\tstd::unique_ptr<ChatSearchIn> _searchIn;\n\trpl::event_stream<ChatSearchTab> _changeSearchTabRequests;\n\trpl::event_stream<ChatTypeFilter> _changeSearchFilterRequests;\n\trpl::event_stream<> _cancelSearchRequests;\n\trpl::event_stream<> _cancelSearchFromRequests;\n\trpl::event_stream<> _changeSearchFromRequests;\n\tobject_ptr<Ui::RpWidget> _loadingAnimation = { nullptr };\n\tobject_ptr<SearchEmpty> _searchEmpty = { nullptr };\n\tSearchState _searchEmptyState;\n\tobject_ptr<Ui::FlatLabel> _empty = { nullptr };\n\tobject_ptr<Ui::VerticalLayout> _emptyList = { nullptr };\n\tobject_ptr<Ui::RoundButton> _emptyButton = { nullptr };\n\n\tUi::DraggingScrollManager _draggingScroll;\n\n\tSearchState _searchState;\n\tHashOrCashtag _searchHashOrCashtag = {};\n\tbool _searchWithPostsPreview = false;\n\tHistory *_searchInMigrated = nullptr;\n\tPeerData *_searchFromShown = nullptr;\n\tUi::Text::String _searchFromUserText;\n\tstd::unique_ptr<SearchTags> _searchTags;\n\tint _searchTagsLeft = 0;\n\tRowDescriptor _menuRow;\n\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tstd::unique_ptr<Ui::VideoUserpic>> _videoUserpics;\n\n\tbase::flat_map<FilterId, int> _chatsFilterScrollStates;\n\n\tstd::unordered_map<ChatsFilterTagsKey, TagCache> _chatsFilterTags;\n\tbool _waitingAllChatListEntryRefreshesForTags = false;\n\trpl::lifetime _handleChatListEntryTagRefreshesLifetime;\n\n\tstd::unordered_map<PeerId, RightButton> _rightButtons;\n\n\tFn<void()> _loadMoreCallback;\n\tFn<void()> _loadMoreFilteredCallback;\n\trpl::event_stream<> _listBottomReached;\n\trpl::event_stream<ChosenRow> _chosenRow;\n\trpl::event_stream<> _updated;\n\n\trpl::event_stream<Ui::ScrollToRequest> _mustScrollTo;\n\trpl::event_stream<Ui::ScrollToRequest> _dialogMoved;\n\trpl::event_stream<SearchRequestDelay> _searchRequests;\n\trpl::event_stream<QString> _completeHashtagRequests;\n\trpl::event_stream<> _refreshHashtagsRequests;\n\trpl::event_stream<UserId> _openBotMainAppRequests;\n\n\tusing QuickActionPtr = std::unique_ptr<Ui::QuickActionContext>;\n\tQuickActionPtr _activeQuickAction;\n\tstd::vector<QuickActionPtr> _inactiveQuickActions;\n\n\tRowDescriptor _chatPreviewRow;\n\tbool _chatPreviewScheduled = false;\n\tstd::optional<QPoint> _chatPreviewTouchGlobal;\n\tbase::Timer _touchDragPinnedTimer;\n\tstd::optional<QPoint> _touchDragStartGlobal;\n\tstd::optional<QPoint> _touchDragNowGlobal;\n\trpl::event_stream<> _touchCancelRequests;\n\n\trpl::variable<ChildListShown> _childListShown;\n\tbase::Timer _freezeTimer;\n\tfloat64 _narrowRatio = 0.;\n\tbool _geometryInited = false;\n\n\tData::SavedMessages *_savedSublists = nullptr;\n\n\tbool _searchLoading = false;\n\tbool _searchWaiting = false;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n};\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_key.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/dialogs_key.h\"\n\n#include \"data/data_folder.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"dialogs/ui/chat_search_in.h\"\n#include \"history/history.h\"\n\nnamespace Dialogs {\n\nKey::Key(History *history) : _value(history) {\n}\n\nKey::Key(Data::Folder *folder) : _value(folder) {\n}\n\nKey::Key(Data::Thread *thread) : _value(thread) {\n}\n\nKey::Key(Data::ForumTopic *topic) : _value(topic) {\n}\n\nKey::Key(Data::SavedSublist *sublist) : _value(sublist) {\n}\n\nKey::Key(not_null<History*> history) : _value(history) {\n}\n\nKey::Key(not_null<Data::Thread*> thread) : _value(thread) {\n}\n\nKey::Key(not_null<Data::Folder*> folder) : _value(folder) {\n}\n\nKey::Key(not_null<Data::ForumTopic*> topic) : _value(topic) {\n}\n\nKey::Key(not_null<Data::SavedSublist*> sublist) : _value(sublist) {\n}\n\nnot_null<Entry*> Key::entry() const {\n\tExpects(_value != nullptr);\n\n\treturn _value;\n}\n\nHistory *Key::history() const {\n\treturn _value ? _value->asHistory() : nullptr;\n}\n\nData::Folder *Key::folder() const {\n\treturn _value ? _value->asFolder() : nullptr;\n}\n\nData::ForumTopic *Key::topic() const {\n\treturn _value ? _value->asTopic() : nullptr;\n}\n\nData::Thread *Key::thread() const {\n\treturn _value ? _value->asThread() : nullptr;\n}\n\nData::SavedSublist *Key::sublist() const {\n\treturn _value ? _value->asSublist() : nullptr;\n}\n\nHistory *Key::owningHistory() const {\n\tif (const auto thread = this->thread()) {\n\t\treturn thread->owningHistory();\n\t}\n\treturn nullptr;\n}\n\nPeerData *Key::peer() const {\n\tif (const auto history = owningHistory()) {\n\t\treturn history->peer;\n\t}\n\treturn nullptr;\n}\n\n[[nodiscard]] bool SearchState::empty() const {\n\treturn !inChat\n\t\t&& tags.empty()\n\t\t&& QStringView(query).trimmed().isEmpty();\n}\n\nChatSearchTab SearchState::defaultTabForMe() const {\n\treturn inChat.topic()\n\t\t? ChatSearchTab::ThisTopic\n\t\t: (inChat.history() || inChat.sublist())\n\t\t? ChatSearchTab::ThisPeer\n\t\t: ChatSearchTab::MyMessages;\n}\n\nbool SearchState::filterChatsList() const {\n\tusing Tab = ChatSearchTab;\n\treturn !inChat // ThisPeer can be in opened forum.\n\t\t&& (tab == Tab::MyMessages || tab == Tab::ThisPeer);\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_key.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/qt/qt_compare.h\"\n#include \"data/data_message_reaction_id.h\"\n\nclass History;\nclass PeerData;\n\nnamespace Data {\nclass Thread;\nclass Folder;\nclass ForumTopic;\nclass SavedSublist;\nstruct ReactionId;\n} // namespace Data\n\nnamespace Dialogs {\n\nclass Entry;\nenum class ChatSearchTab : uchar;\n\nclass Key {\npublic:\n\tKey() = default;\n\tKey(Entry *entry) : _value(entry) {\n\t}\n\tKey(History *history);\n\tKey(Data::Folder *folder);\n\tKey(Data::Thread *thread);\n\tKey(Data::ForumTopic *topic);\n\tKey(Data::SavedSublist *sublist);\n\tKey(not_null<Entry*> entry) : _value(entry) {\n\t}\n\tKey(not_null<History*> history);\n\tKey(not_null<Data::Thread*> thread);\n\tKey(not_null<Data::Folder*> folder);\n\tKey(not_null<Data::ForumTopic*> topic);\n\tKey(not_null<Data::SavedSublist*> sublist);\n\n\texplicit operator bool() const {\n\t\treturn (_value != nullptr);\n\t}\n\t[[nodiscard]] not_null<Entry*> entry() const;\n\t[[nodiscard]] History *history() const;\n\t[[nodiscard]] Data::Folder *folder() const;\n\t[[nodiscard]] Data::ForumTopic *topic() const;\n\t[[nodiscard]] Data::Thread *thread() const;\n\t[[nodiscard]] History *owningHistory() const;\n\t[[nodiscard]] PeerData *peer() const;\n\t[[nodiscard]] Data::SavedSublist *sublist() const;\n\n\tfriend inline constexpr auto operator<=>(Key, Key) noexcept = default;\n\nprivate:\n\tEntry *_value = nullptr;\n\n};\n\nstruct RowDescriptor {\n\tRowDescriptor() = default;\n\tRowDescriptor(Key key, FullMsgId fullId) : key(key), fullId(fullId) {\n\t}\n\n\tKey key;\n\tFullMsgId fullId;\n\n};\n\ninline bool operator==(const RowDescriptor &a, const RowDescriptor &b) {\n\treturn (a.key == b.key)\n\t\t&& ((a.fullId == b.fullId) || (!a.fullId.msg && !b.fullId.msg));\n}\n\ninline bool operator!=(const RowDescriptor &a, const RowDescriptor &b) {\n\treturn !(a == b);\n}\n\ninline bool operator<(const RowDescriptor &a, const RowDescriptor &b) {\n\tif (a.key < b.key) {\n\t\treturn true;\n\t} else if (a.key > b.key) {\n\t\treturn false;\n\t}\n\treturn a.fullId < b.fullId;\n}\n\ninline bool operator>(const RowDescriptor &a, const RowDescriptor &b) {\n\treturn (b < a);\n}\n\ninline bool operator<=(const RowDescriptor &a, const RowDescriptor &b) {\n\treturn !(b < a);\n}\n\ninline bool operator>=(const RowDescriptor &a, const RowDescriptor &b) {\n\treturn !(a < b);\n}\n\nstruct EntryState {\n\tenum class Section {\n\t\tHistory,\n\t\tProfile,\n\t\tChatsList,\n\t\tScheduled,\n\t\tPinned,\n\t\tReplies,\n\t\tSavedSublist,\n\t\tContextMenu,\n\t\tSubsectionTabsMenu,\n\t\tShortcutMessages,\n\t};\n\n\tKey key;\n\tSection section = Section::History;\n\tFilterId filterId = 0;\n\tFullReplyTo currentReplyTo;\n\tSuggestOptions currentSuggest;\n\n\tfriend inline auto operator<=>(\n\t\tconst EntryState&,\n\t\tconst EntryState&) = default;\n\tfriend inline bool operator==(\n\t\tconst EntryState&,\n\t\tconst EntryState&) = default;\n};\n\nenum class ChatTypeFilter : uchar {\n\tAll,\n\tPrivate,\n\tGroups,\n\tChannels,\n};\n\nstruct SearchState {\n\tKey inChat;\n\tPeerData *fromPeer = nullptr;\n\tstd::vector<Data::ReactionId> tags;\n\tChatSearchTab tab = {};\n\tChatTypeFilter filter = ChatTypeFilter::All;\n\tQString query;\n\n\t[[nodiscard]] bool empty() const;\n\t[[nodiscard]] ChatSearchTab defaultTabForMe() const;\n\t[[nodiscard]] bool filterChatsList() const;\n\n\texplicit operator bool() const {\n\t\treturn !empty();\n\t}\n\n\tfriend inline auto operator<=>(\n\t\tconst SearchState&,\n\t\tconst SearchState&) noexcept = default;\n\tfriend inline bool operator==(\n\t\tconst SearchState&,\n\t\tconst SearchState&) = default;\n};\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/dialogs_list.h\"\n\n#include \"dialogs/dialogs_entry.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"data/data_session.h\"\n\nnamespace Dialogs {\n\nList::List(SortMode sortMode, FilterId filterId)\n: _sortMode(sortMode)\n, _filterId(filterId) {\n}\n\nList::const_iterator List::cfind(Row *value) const {\n\treturn value\n\t\t? (cbegin() + value->index())\n\t\t: cend();\n}\n\nnot_null<Row*> List::addToEnd(Key key) {\n\tif (const auto result = getRow(key)) {\n\t\treturn result;\n\t}\n\tconst auto result = _rowByKey.emplace(\n\t\tkey,\n\t\tstd::make_unique<Row>(key, _rows.size(), height())\n\t).first->second.get();\n\tresult->recountHeight(_narrowRatio, _filterId);\n\t_rows.emplace_back(result);\n\tif (_sortMode == SortMode::Date) {\n\t\tadjustByDate(result);\n\t}\n\treturn result;\n}\n\nRow *List::adjustByName(Key key) {\n\tExpects(_sortMode == SortMode::Name);\n\n\tconst auto row = getRow(key);\n\tif (!row) {\n\t\treturn nullptr;\n\t}\n\tadjustByName(row);\n\treturn row;\n}\n\nnot_null<Row*> List::addByName(Key key) {\n\tExpects(_sortMode == SortMode::Name);\n\n\tconst auto row = addToEnd(key);\n\tadjustByName(key);\n\treturn row;\n}\n\nvoid List::adjustByName(not_null<Row*> row) {\n\tExpects(row->index() >= 0 && row->index() < _rows.size());\n\n\tconst auto &key = row->entry()->chatListNameSortKey();\n\tconst auto index = row->index();\n\tconst auto i = _rows.begin() + index;\n\tconst auto before = std::find_if(i + 1, _rows.end(), [&](Row *row) {\n\t\treturn row->entry()->chatListNameSortKey().compare(key) >= 0;\n\t});\n\tif (before != i + 1) {\n\t\trotate(i, i + 1, before);\n\t} else if (i != _rows.begin()) {\n\t\tconst auto from = std::make_reverse_iterator(i);\n\t\tconst auto after = std::find_if(from, _rows.rend(), [&](Row *row) {\n\t\t\treturn row->entry()->chatListNameSortKey().compare(key) <= 0;\n\t\t}).base();\n\t\tif (after != i) {\n\t\t\trotate(after, i, i + 1);\n\t\t}\n\t}\n}\n\nvoid List::adjustByDate(not_null<Row*> row) {\n\tExpects(_sortMode == SortMode::Date);\n\n\tif (_frozen) {\n\t\tconst auto canAdjustWhileFrozen = _pendingAdjust.empty()\n\t\t\t&& (row->entry()->fixedOnTopIndex()\n\t\t\t\t|| row->entry()->isPinnedDialog(_filterId));\n\t\tif (!canAdjustWhileFrozen) {\n\t\t\t_pendingAdjust.emplace(row);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tconst auto key = row->sortKey(_filterId);\n\tconst auto index = row->index();\n\tconst auto i = _rows.begin() + index;\n\tconst auto before = std::find_if(i + 1, _rows.end(), [&](Row *row) {\n\t\treturn (row->sortKey(_filterId) <= key);\n\t});\n\tif (before != i + 1) {\n\t\trotate(i, i + 1, before);\n\t} else {\n\t\tconst auto from = std::make_reverse_iterator(i);\n\t\tconst auto after = std::find_if(from, _rows.rend(), [&](Row *row) {\n\t\t\treturn (row->sortKey(_filterId) >= key);\n\t\t}).base();\n\t\tif (after != i) {\n\t\t\trotate(after, i, i + 1);\n\t\t}\n\t}\n}\n\nvoid List::freeze() {\n\t_frozen = true;\n}\n\nvoid List::unfreeze() {\n\t_frozen = false;\n\tauto pending = base::take(_pendingAdjust);\n\tif (pending.empty()) {\n\t\treturn;\n\t} else if (pending.size() == 1) {\n\t\tadjustByDate(*pending.begin());\n\t\treturn;\n\t}\n\tfor (const auto &row : pending) {\n\t\tadjustByDate(row);\n\t}\n\tif (!sortedByDate()) {\n\t\tsortByDate();\n\t}\n}\n\nbool List::sortedByDate() const {\n\tExpects(_sortMode == SortMode::Date);\n\n\tfor (auto i = 1, count = int(_rows.size()); i != count; ++i) {\n\t\tif (_rows[i - 1]->sortKey(_filterId) < _rows[i]->sortKey(_filterId)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid List::sortByDate() {\n\tExpects(_sortMode == SortMode::Date);\n\n\tranges::stable_sort(_rows, [&](Row *a, Row *b) {\n\t\treturn a->sortKey(_filterId) > b->sortKey(_filterId);\n\t});\n\tauto top = 0;\n\tfor (auto i = 0, count = int(_rows.size()); i != count; ++i) {\n\t\tconst auto row = _rows[i];\n\t\trow->_index = i;\n\t\trow->_top = top;\n\t\ttop += row->height();\n\t}\n}\n\nbool List::updateHeight(Key key, float64 narrowRatio) {\n\tconst auto i = _rowByKey.find(key);\n\tif (i == _rowByKey.cend()) {\n\t\treturn false;\n\t}\n\tconst auto row = i->second.get();\n\tconst auto index = row->index();\n\tauto top = row->top();\n\tconst auto was = row->height();\n\trow->recountHeight(narrowRatio, _filterId);\n\tif (row->height() == was) {\n\t\treturn false;\n\t}\n\tfor (auto i = _rows.begin() + index, e = _rows.end(); i != e; ++i) {\n\t\t(*i)->_top = top;\n\t\ttop += (*i)->height();\n\t}\n\treturn true;\n}\n\nbool List::updateHeights(float64 narrowRatio) {\n\t_narrowRatio = narrowRatio;\n\tauto was = height();\n\tauto top = 0;\n\tfor (const auto &row : _rows) {\n\t\trow->_top = top;\n\t\trow->recountHeight(narrowRatio, _filterId);\n\t\ttop += row->height();\n\t}\n\treturn (height() != was);\n}\n\nbool List::moveToTop(Key key) {\n\tconst auto i = _rowByKey.find(key);\n\tif (i == _rowByKey.cend()) {\n\t\treturn false;\n\t}\n\tconst auto index = i->second->index();\n\tconst auto begin = _rows.begin();\n\trotate(begin, begin + index, begin + index + 1);\n\treturn true;\n}\n\nvoid List::rotate(\n\t\tstd::vector<not_null<Row*>>::iterator first,\n\t\tstd::vector<not_null<Row*>>::iterator middle,\n\t\tstd::vector<not_null<Row*>>::iterator last) {\n\tauto top = (*first)->top();\n\tstd::rotate(first, middle, last);\n\n\tauto count = (last - first);\n\tauto index = (first - _rows.begin());\n\twhile (count--) {\n\t\tconst auto row = *first++;\n\t\trow->_index = index++;\n\t\trow->_top = top;\n\t\ttop += row->height();\n\t}\n}\n\nbool List::remove(Key key, Row *replacedBy) {\n\tauto i = _rowByKey.find(key);\n\tif (i == _rowByKey.cend()) {\n\t\treturn false;\n\t}\n\n\tconst auto row = i->second.get();\n\t_pendingAdjust.remove(row);\n\trow->entry()->owner().dialogsRowReplaced({ row, replacedBy });\n\n\tauto top = row->top();\n\tconst auto index = row->index();\n\t_rows.erase(_rows.begin() + index);\n\tfor (auto i = index, count = int(_rows.size()); i != count; ++i) {\n\t\tconst auto row = _rows[i];\n\t\trow->_index = i;\n\t\trow->_top = top;\n\t\ttop += row->height();\n\t}\n\t_rowByKey.erase(i);\n\treturn true;\n}\n\nRow *List::rowAtY(int y) const {\n\tconst auto i = findByY(y);\n\tif (i == cend()) {\n\t\treturn nullptr;\n\t}\n\tconst auto row = *i;\n\tconst auto top = row->top();\n\tconst auto bottom = top + row->height();\n\treturn (top <= y && bottom > y) ? row.get() : nullptr;\n}\n\nList::iterator List::findByY(int y) const {\n\treturn ranges::lower_bound(_rows, y, ranges::less(), [](const Row *row) {\n\t\treturn row->top() + row->height() - 1;\n\t});\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"dialogs/dialogs_row.h\"\n\nclass PeerData;\nnamespace Dialogs {\n\nenum class SortMode;\n\nclass List final {\npublic:\n\tList(SortMode sortMode, FilterId filterId = 0);\n\tList(const List &other) = delete;\n\tList &operator=(const List &other) = delete;\n\tList(List &&other) = default;\n\tList &operator=(List &&other) = default;\n\t~List() = default;\n\n\tvoid clear() {\n\t\t_frozen = false;\n\t\t_pendingAdjust.clear();\n\t\t_rows.clear();\n\t\t_rowByKey.clear();\n\t}\n\t[[nodiscard]] int size() const {\n\t\treturn _rows.size();\n\t}\n\t[[nodiscard]] bool empty() const {\n\t\treturn _rows.empty();\n\t}\n\t[[nodiscard]] int height() const {\n\t\treturn _rows.empty()\n\t\t\t? 0\n\t\t\t: (_rows.back()->top() + _rows.back()->height());\n\t}\n\t[[nodiscard]] bool contains(Key key) const {\n\t\treturn _rowByKey.find(key) != _rowByKey.end();\n\t}\n\t[[nodiscard]] Row *getRow(Key key) const {\n\t\tconst auto i = _rowByKey.find(key);\n\t\treturn (i != _rowByKey.end()) ? i->second.get() : nullptr;\n\t}\n\t[[nodiscard]] Row *rowAtY(int y) const;\n\n\tnot_null<Row*> addToEnd(Key key);\n\tRow *adjustByName(Key key);\n\tnot_null<Row*> addByName(Key key);\n\tbool moveToTop(Key key);\n\tvoid adjustByDate(not_null<Row*> row);\n\tvoid freeze();\n\tvoid unfreeze();\n\tbool updateHeight(Key key, float64 narrowRatio);\n\tbool updateHeights(float64 narrowRatio);\n\tbool remove(Key key, Row *replacedBy = nullptr);\n\n\tusing const_iterator = std::vector<not_null<Row*>>::const_iterator;\n\tusing iterator = const_iterator;\n\n\t[[nodiscard]] const_iterator cbegin() const { return _rows.cbegin(); }\n\t[[nodiscard]] const_iterator cend() const { return _rows.cend(); }\n\t[[nodiscard]] const_iterator begin() const { return cbegin(); }\n\t[[nodiscard]] const_iterator end() const { return cend(); }\n\t[[nodiscard]] iterator begin() { return cbegin(); }\n\t[[nodiscard]] iterator end() { return cend(); }\n\t[[nodiscard]] const_iterator cfind(Row *value) const;\n\t[[nodiscard]] const_iterator find(Row *value) const {\n\t\treturn cfind(value);\n\t}\n\t[[nodiscard]] iterator find(Row *value) { return cfind(value); }\n\t[[nodiscard]] iterator findByY(int y) const;\n\nprivate:\n\tvoid adjustByName(not_null<Row*> row);\n\t[[nodiscard]] bool sortedByDate() const;\n\tvoid sortByDate();\n\tvoid rotate(\n\t\tstd::vector<not_null<Row*>>::iterator first,\n\t\tstd::vector<not_null<Row*>>::iterator middle,\n\t\tstd::vector<not_null<Row*>>::iterator last);\n\n\tSortMode _sortMode = SortMode();\n\tFilterId _filterId = 0;\n\tfloat64 _narrowRatio = 0.;\n\tbool _frozen = false;\n\tstd::vector<not_null<Row*>> _rows;\n\tstd::map<Key, std::unique_ptr<Row>> _rowByKey;\n\tbase::flat_set<not_null<Row*>> _pendingAdjust;\n\n};\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_main_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/dialogs_main_list.h\"\n\n#include \"data/data_changes.h\"\n#include \"data/data_session.h\"\n#include \"data/data_chat_filters.h\"\n#include \"main/main_session.h\"\n#include \"history/history_unread_things.h\"\n#include \"history/history.h\"\n\nnamespace Dialogs {\n\nMainList::MainList(\n\tnot_null<Main::Session*> session,\n\tFilterId filterId,\n\trpl::producer<int> pinnedLimit)\n: _filterId(filterId)\n, _all(SortMode::Date, filterId)\n, _pinned(filterId, 1) {\n\t_unreadState.known = true;\n\n\tstd::move(\n\t\tpinnedLimit\n\t) | rpl::on_next([=](int limit) {\n\t\t_pinned.setLimit(std::max(limit, 1));\n\t}, _lifetime);\n\n\tsession->changes().realtimeNameUpdates(\n\t) | rpl::on_next([=](const Data::NameUpdate &update) {\n\t\t_all.peerNameChanged(_filterId, update.peer, update.oldFirstLetters);\n\t}, _lifetime);\n}\n\nbool MainList::empty() const {\n\treturn _all.empty();\n}\n\nbool MainList::loaded() const {\n\treturn _loaded;\n}\n\nvoid MainList::setLoaded(bool loaded) {\n\tif (_loaded == loaded) {\n\t\treturn;\n\t}\n\tconst auto recomputer = gsl::finally([&] {\n\t\trecomputeFullListSize();\n\t});\n\tconst auto notifier = unreadStateChangeNotifier(true);\n\t_loaded = loaded;\n}\n\nvoid MainList::setAllAreMuted(bool allAreMuted) {\n\tif (_allAreMuted == allAreMuted) {\n\t\treturn;\n\t}\n\tconst auto notifier = unreadStateChangeNotifier(true);\n\t_allAreMuted = allAreMuted;\n}\n\nvoid MainList::setCloudListSize(int size) {\n\tif (_cloudListSize == size) {\n\t\treturn;\n\t}\n\t_cloudListSize = size;\n\trecomputeFullListSize();\n}\n\nconst rpl::variable<int> &MainList::fullSize() const {\n\treturn _fullListSize;\n}\n\nvoid MainList::clear() {\n\tconst auto recomputer = gsl::finally([&] {\n\t\trecomputeFullListSize();\n\t});\n\tconst auto notifier = unreadStateChangeNotifier(true);\n\t_pinned.clear();\n\t_all.clear();\n\t_unreadState = UnreadState();\n\t_cloudUnreadState = UnreadState();\n\t_unreadState.known = true;\n\t_cloudUnreadState.known = true;\n\t_cloudListSize = 0;\n}\n\nRowsByLetter MainList::addEntry(Key key) {\n\tconst auto result = _all.addToEnd(key);\n\n\tconst auto unread = key.entry()->chatListUnreadState();\n\tunreadEntryChanged(unread, true);\n\trecomputeFullListSize();\n\n\treturn result;\n}\n\nvoid MainList::removeEntry(Key key) {\n\t_all.remove(key);\n\n\tconst auto unread = key.entry()->chatListUnreadState();\n\tunreadEntryChanged(unread, false);\n\trecomputeFullListSize();\n}\n\nvoid MainList::recomputeFullListSize() {\n\t_fullListSize = std::max(_all.size(), loaded() ? 0 : _cloudListSize);\n}\n\nvoid MainList::unreadStateChanged(\n\t\tconst UnreadState &wasState,\n\t\tconst UnreadState &nowState) {\n\tconst auto useClouded = _cloudUnreadState.known && !loaded();\n\tconst auto updateCloudUnread = _cloudUnreadState.known && wasState.known;\n\tconst auto notify = !useClouded || wasState.known;\n\tconst auto notifier = unreadStateChangeNotifier(notify);\n\t_unreadState += nowState - wasState;\n\tif (_unreadState.chatsMuted > _unreadState.chats\n\t\t|| _unreadState.messagesMuted > _unreadState.messages) {\n\t\t[[maybe_unused]] int a = 0;\n\t}\n\tif (updateCloudUnread) {\n\t\tAssert(nowState.known);\n\t\t_cloudUnreadState += nowState - wasState;\n\t\tfinalizeCloudUnread();\n\t}\n}\n\nvoid MainList::unreadEntryChanged(\n\t\tconst Dialogs::UnreadState &state,\n\t\tbool added) {\n\tif (!state.messages\n\t\t&& !state.chats\n\t\t&& !state.marks\n\t\t&& !state.mentions\n\t\t&& !state.reactions) {\n\t\treturn;\n\t}\n\tconst auto updateCloudUnread = _cloudUnreadState.known && state.known;\n\tconst auto notify = !_cloudUnreadState.known || loaded() || state.known;\n\tconst auto notifier = unreadStateChangeNotifier(notify);\n\tif (added) {\n\t\t_unreadState += state;\n\t} else {\n\t\t_unreadState -= state;\n\t}\n\tif (_unreadState.chatsMuted > _unreadState.chats\n\t\t|| _unreadState.messagesMuted > _unreadState.messages) {\n\t\t[[maybe_unused]] int a = 0;\n\t}\n\tif (updateCloudUnread) {\n\t\tif (added) {\n\t\t\t_cloudUnreadState += state;\n\t\t} else {\n\t\t\t_cloudUnreadState -= state;\n\t\t}\n\t\tfinalizeCloudUnread();\n\t}\n}\n\nvoid MainList::updateCloudUnread(const MTPDdialogFolder &data) {\n\tconst auto notifier = unreadStateChangeNotifier(!loaded());\n\n\t_cloudUnreadState.messages = data.vunread_muted_messages_count().v\n\t\t+ data.vunread_unmuted_messages_count().v;\n\t_cloudUnreadState.chats = data.vunread_muted_peers_count().v\n\t\t+ data.vunread_unmuted_peers_count().v;\n\tfinalizeCloudUnread();\n\n\t_cloudUnreadState.known = true;\n}\n\nbool MainList::cloudUnreadKnown() const {\n\treturn _cloudUnreadState.known;\n}\n\nvoid MainList::finalizeCloudUnread() {\n\t// Cloud state for archive folder always counts everything as muted.\n\t_cloudUnreadState.messagesMuted = _cloudUnreadState.messages;\n\t_cloudUnreadState.chatsMuted = _cloudUnreadState.chats;\n\n\t// We don't know the real value of marked chats counts in cloud unread.\n\t_cloudUnreadState.marksMuted = _cloudUnreadState.marks = 0;\n}\n\nUnreadState MainList::unreadState() const {\n\tconst auto useCloudState = _cloudUnreadState.known && !loaded();\n\tauto result = useCloudState ? _cloudUnreadState : _unreadState;\n\n\t// We don't know the real value of marked chats counts in cloud unread.\n\tif (useCloudState) {\n\t\tresult.marks = _unreadState.marks;\n\t\tresult.marksMuted = _unreadState.marksMuted;\n\t}\n\tif (_allAreMuted) {\n\t\tresult.messagesMuted = result.messages;\n\t\tresult.chatsMuted = result.chats;\n\t\tresult.marksMuted = result.marks;\n\t}\n#ifdef Q_OS_WIN\n\t[[maybe_unused]] volatile auto touch = 0\n\t\t+ _unreadState.marks + _unreadState.marksMuted\n\t\t+ _unreadState.messages + _unreadState.messagesMuted\n\t\t+ _unreadState.chats + _unreadState.chatsMuted\n\t\t+ _unreadState.reactions + _unreadState.reactionsMuted\n\t\t+ _unreadState.mentions;\n#endif // Q_OS_WIN\n\treturn result;\n}\n\nrpl::producer<UnreadState> MainList::unreadStateChanges() const {\n\treturn _unreadStateChanges.events();\n}\n\nnot_null<IndexedList*> MainList::indexed() {\n\treturn &_all;\n}\n\nnot_null<const IndexedList*> MainList::indexed() const {\n\treturn &_all;\n}\n\nnot_null<PinnedList*> MainList::pinned() {\n\treturn &_pinned;\n}\n\nnot_null<const PinnedList*> MainList::pinned() const {\n\treturn &_pinned;\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_main_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"dialogs/dialogs_common.h\"\n#include \"dialogs/dialogs_indexed_list.h\"\n#include \"dialogs/dialogs_pinned_list.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\nclass Thread;\n} // namespace Data\n\nnamespace Dialogs {\n\nclass MainList final {\npublic:\n\tMainList(\n\t\tnot_null<Main::Session*> session,\n\t\tFilterId filterId,\n\t\trpl::producer<int> pinnedLimit);\n\n\tbool empty() const;\n\tbool loaded() const;\n\tvoid setLoaded(bool loaded = true);\n\tvoid setAllAreMuted(bool allAreMuted = true);\n\tvoid clear();\n\n\tRowsByLetter addEntry(Key key);\n\tvoid removeEntry(Key key);\n\n\tvoid unreadStateChanged(\n\t\tconst UnreadState &wasState,\n\t\tconst UnreadState &nowState);\n\tvoid unreadEntryChanged(const UnreadState &state, bool added);\n\tvoid updateCloudUnread(const MTPDdialogFolder &data);\n\t[[nodiscard]] bool cloudUnreadKnown() const;\n\t[[nodiscard]] UnreadState unreadState() const;\n\t[[nodiscard]] rpl::producer<UnreadState> unreadStateChanges() const;\n\n\t[[nodiscard]] not_null<IndexedList*> indexed();\n\t[[nodiscard]] not_null<const IndexedList*> indexed() const;\n\t[[nodiscard]] not_null<PinnedList*> pinned();\n\t[[nodiscard]] not_null<const PinnedList*> pinned() const;\n\n\tvoid setCloudListSize(int size);\n\t[[nodiscard]] const rpl::variable<int> &fullSize() const;\n\nprivate:\n\tvoid finalizeCloudUnread();\n\tvoid recomputeFullListSize();\n\n\tinline auto unreadStateChangeNotifier(bool notify);\n\n\tFilterId _filterId = 0;\n\tIndexedList _all;\n\tPinnedList _pinned;\n\tUnreadState _unreadState;\n\tUnreadState _cloudUnreadState;\n\trpl::event_stream<UnreadState> _unreadStateChanges;\n\trpl::variable<int> _fullListSize = 0;\n\tint _cloudListSize = 0;\n\n\tbool _loaded = false;\n\tbool _allAreMuted = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\nauto MainList::unreadStateChangeNotifier(bool notify) {\n\tconst auto wasState = notify ? unreadState() : UnreadState();\n\treturn gsl::finally([=] {\n\t\tif (notify) {\n\t\t\t_unreadStateChanges.fire_copy(wasState);\n\t\t}\n\t});\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_pinned_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/dialogs_pinned_list.h\"\n\n#include \"data/data_saved_messages.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"dialogs/dialogs_entry.h\"\n#include \"history/history.h\"\n#include \"data/data_session.h\"\n#include \"data/data_forum.h\"\n\nnamespace Dialogs {\n\nPinnedList::PinnedList(FilterId filterId, int limit)\n: _filterId(filterId)\n, _limit(limit) {\n\tExpects(limit > 0);\n}\n\nvoid PinnedList::setLimit(int limit) {\n\tExpects(limit > 0);\n\n\tif (_limit == limit) {\n\t\treturn;\n\t}\n\t_limit = limit;\n\tapplyLimit(_limit);\n}\n\nvoid PinnedList::addPinned(Key key) {\n\tExpects(key.entry()->folderKnown());\n\n\taddPinnedGetPosition(key);\n}\n\nint PinnedList::addPinnedGetPosition(Key key) {\n\tconst auto already = ranges::find(_data, key);\n\tif (already != end(_data)) {\n\t\treturn already - begin(_data);\n\t}\n\tapplyLimit(_limit - 1);\n\tconst auto position = int(_data.size());\n\t_data.push_back(key);\n\tkey.entry()->cachePinnedIndex(_filterId, position + 1);\n\treturn position;\n}\n\nvoid PinnedList::setPinned(Key key, bool pinned) {\n\tExpects(key.entry()->folderKnown() || _filterId != 0);\n\n\tif (pinned) {\n\t\tconst int position = addPinnedGetPosition(key);\n\t\tif (position) {\n\t\t\tconst auto begin = _data.begin();\n\t\t\tstd::rotate(begin, begin + position, begin + position + 1);\n\t\t\tfor (auto i = 0; i != position + 1; ++i) {\n\t\t\t\t_data[i].entry()->cachePinnedIndex(_filterId, i + 1);\n\t\t\t}\n\t\t}\n\t} else if (const auto it = ranges::find(_data, key); it != end(_data)) {\n\t\tconst auto index = int(it - begin(_data));\n\t\t_data.erase(it);\n\t\tkey.entry()->cachePinnedIndex(_filterId, 0);\n\t\tfor (auto i = index, count = int(size(_data)); i != count; ++i) {\n\t\t\t_data[i].entry()->cachePinnedIndex(_filterId, i + 1);\n\t\t}\n\t}\n}\n\nvoid PinnedList::applyLimit(int limit) {\n\tExpects(limit >= 0);\n\n\twhile (_data.size() > limit) {\n\t\tsetPinned(_data.back(), false);\n\t}\n}\n\nvoid PinnedList::clear() {\n\tapplyLimit(0);\n}\n\nvoid PinnedList::applyList(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst QVector<MTPDialogPeer> &list) {\n\tExpects(this != owner->savedMessages().chatsList()->pinned());\n\n\tclear();\n\tfor (const auto &peer : list) {\n\t\tpeer.match([&](const MTPDdialogPeer &data) {\n\t\t\tif (const auto peerId = peerFromMTP(data.vpeer())) {\n\t\t\t\taddPinned(owner->history(peerId));\n\t\t\t}\n\t\t}, [&](const MTPDdialogPeerFolder &data) {\n\t\t\taddPinned(owner->folder(data.vfolder_id().v));\n\t\t});\n\t}\n}\n\nvoid PinnedList::applyList(\n\t\tnot_null<Data::SavedMessages*> sublistsOwner,\n\t\tconst QVector<MTPDialogPeer> &list) {\n\tExpects(this == sublistsOwner->chatsList()->pinned());\n\n\tclear();\n\tfor (const auto &peer : list) {\n\t\tpeer.match([&](const MTPDdialogPeer &data) {\n\t\t\tif (const auto peerId = peerFromMTP(data.vpeer())) {\n\t\t\t\tconst auto peer = sublistsOwner->owner().peer(peerId);\n\t\t\t\taddPinned(sublistsOwner->sublist(peer));\n\t\t\t}\n\t\t}, [](const MTPDdialogPeerFolder &data) {\n\t\t});\n\t}\n}\n\nvoid PinnedList::applyList(\n\t\tnot_null<Data::Forum*> forum,\n\t\tconst QVector<MTPint> &list) {\n\tExpects(this == forum->topicsList()->pinned());\n\n\tclear();\n\tfor (const auto &topicId : list) {\n\t\taddPinned(forum->topicFor(topicId.v));\n\t}\n}\n\nvoid PinnedList::applyList(const std::vector<not_null<History*>> &list) {\n\tExpects(_filterId != 0);\n\n\tconst auto old = base::take(_data);\n\n\tconst auto count = int(list.size());\n\t_data.reserve(count);\n\tfor (auto i = 0; i != count; ++i) {\n\t\tconst auto history = list[i];\n\t\t_data.emplace_back(history);\n\t\thistory->cachePinnedIndex(_filterId, i + 1);\n\t}\n\n\tfor (const auto &key : old) {\n\t\tconst auto history = key.history();\n\t\tif (!history || !ranges::contains(_data, history, &Key::history)) {\n\t\t\tkey.entry()->cachePinnedIndex(_filterId, 0);\n\t\t}\n\t}\n}\n\nvoid PinnedList::reorder(Key key1, Key key2) {\n\tconst auto index1 = ranges::find(_data, key1) - begin(_data);\n\tconst auto index2 = ranges::find(_data, key2) - begin(_data);\n\tAssert(index1 >= 0 && index1 < _data.size());\n\tAssert(index2 >= 0 && index2 < _data.size());\n\tAssert(index1 != index2);\n\tstd::swap(_data[index1], _data[index2]);\n\tkey1.entry()->cachePinnedIndex(_filterId, index2 + 1);\n\tkey2.entry()->cachePinnedIndex(_filterId, index1 + 1);\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_pinned_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass History;\n\nnamespace Data {\nclass SavedMessages;\nclass Session;\nclass Forum;\n} // namespace Data\n\nnamespace Dialogs {\n\nclass Key;\n\nclass PinnedList final {\npublic:\n\tPinnedList(FilterId filterId, int limit);\n\n\tvoid setLimit(int limit);\n\n\t// Places on the last place in the list otherwise.\n\t// Does nothing if already pinned.\n\tvoid addPinned(Key key);\n\n\t// if (pinned) places on the first place in the list.\n\tvoid setPinned(Key key, bool pinned);\n\n\tvoid clear();\n\n\tvoid applyList(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst QVector<MTPDialogPeer> &list);\n\tvoid applyList(\n\t\tnot_null<Data::SavedMessages*> sublistsOwner,\n\t\tconst QVector<MTPDialogPeer> &list);\n\tvoid applyList(\n\t\tnot_null<Data::Forum*> forum,\n\t\tconst QVector<MTPint> &list);\n\tvoid applyList(const std::vector<not_null<History*>> &list);\n\tvoid reorder(Key key1, Key key2);\n\n\tconst std::vector<Key> &order() const {\n\t\treturn _data;\n\t}\n\nprivate:\n\tint addPinnedGetPosition(Key key);\n\tvoid applyLimit(int limit);\n\n\tFilterId _filterId = 0;\n\tint _limit = 0;\n\tstd::vector<Key> _data;\n\n};\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_quick_action.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/dialogs_quick_action.h\"\n\n#include \"dialogs/ui/dialogs_quick_action_context.h\"\n#include \"apiwrap.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"dialogs/dialogs_entry.h\"\n#include \"history/history.h\"\n#include \"lang/lang_instance.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_mute.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Dialogs {\nnamespace {\n\nconst style::font &SwipeActionFont(\n\t\tDialogs::Ui::QuickDialogActionLabel action,\n\t\tint availableWidth) {\n\tstruct Entry final {\n\t\tDialogs::Ui::QuickDialogActionLabel action;\n\t\tQString langId;\n\t\tstyle::font font;\n\t};\n\tstatic auto Fonts = std::vector<Entry>();\n\tfor (auto &entry : Fonts) {\n\t\tif (entry.action == action) {\n\t\t\tif (entry.langId == Lang::GetInstance().id()) {\n\t\t\t\treturn entry.font;\n\t\t\t}\n\t\t}\n\t}\n\tconstexpr auto kNormalFontSize = 13;\n\tconstexpr auto kMinFontSize = 5;\n\tfor (auto i = kNormalFontSize; i >= kMinFontSize; --i) {\n\t\tauto font = style::font(\n\t\t\tstyle::ConvertScale(i, style::Scale()),\n\t\t\tst::semiboldFont->flags(),\n\t\t\tst::semiboldFont->family());\n\t\tif (font->width(ResolveQuickDialogLabel(action)) <= availableWidth\n\t\t\t|| i == kMinFontSize) {\n\t\t\tFonts.emplace_back(Entry{\n\t\t\t\t.action = action,\n\t\t\t\t.langId = Lang::GetInstance().id(),\n\t\t\t\t.font = std::move(font),\n\t\t\t});\n\t\t\treturn Fonts.back().font;\n\t\t}\n\t}\n\tUnexpected(\"SwipeActionFont: can't find font.\");\n}\n\n} // namespace\n\nvoid PerformQuickDialogAction(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tUi::QuickDialogAction action,\n\t\tFilterId filterId) {\n\tconst auto history = peer->owner().history(peer);\n\tif (action == Dialogs::Ui::QuickDialogAction::Mute) {\n\t\tconst auto isMuted = rpl::variable<bool>(\n\t\t\tMuteMenu::ThreadDescriptor(history).isMutedValue()).current();\n\t\tMuteMenu::ThreadDescriptor(history).updateMutePeriod(isMuted\n\t\t\t? 0\n\t\t\t: std::numeric_limits<TimeId>::max());\n\t\tcontroller->showToast(isMuted\n\t\t\t? tr::lng_quick_dialog_action_toast_unmute_success(tr::now)\n\t\t\t: tr::lng_quick_dialog_action_toast_mute_success(tr::now));\n\t} else if (action == Dialogs::Ui::QuickDialogAction::Pin) {\n\t\tconst auto entry = (Dialogs::Entry*)(history);\n\t\tconst auto isPinned = entry->isPinnedDialog(filterId);\n\t\tconst auto onToggled = isPinned\n\t\t\t? Fn<void()>(nullptr)\n\t\t\t: [=] {\n\t\t\t\tcontroller->showToast(\n\t\t\t\t\ttr::lng_quick_dialog_action_toast_pin_success(tr::now));\n\t\t\t};\n\t\tWindow::TogglePinnedThread(controller, entry, filterId, onToggled);\n\t\tif (isPinned) {\n\t\t\tcontroller->showToast(\n\t\t\t\ttr::lng_quick_dialog_action_toast_unpin_success(tr::now));\n\t\t}\n\t} else if (action == Dialogs::Ui::QuickDialogAction::Read) {\n\t\tif (Window::IsUnreadThread(history)) {\n\t\t\tWindow::MarkAsReadThread(history);\n\t\t\tcontroller->showToast(\n\t\t\t\ttr::lng_quick_dialog_action_toast_read_success(tr::now));\n\t\t} else if (history) {\n\t\t\tpeer->owner().histories().changeDialogUnreadMark(history, true);\n\t\t\tcontroller->showToast(\n\t\t\t\ttr::lng_quick_dialog_action_toast_unread_success(tr::now));\n\t\t}\n\t} else if (action == Dialogs::Ui::QuickDialogAction::Archive) {\n\t\tconst auto isArchived = Window::IsArchived(history);\n\t\tcontroller->showToast(isArchived\n\t\t\t? tr::lng_quick_dialog_action_toast_unarchive_success(tr::now)\n\t\t\t: tr::lng_quick_dialog_action_toast_archive_success(tr::now));\n\t\thistory->session().api().toggleHistoryArchived(\n\t\t\thistory,\n\t\t\t!isArchived,\n\t\t\t[] {});\n\t} else if (action == Dialogs::Ui::QuickDialogAction::Delete) {\n\t\tWindow::DeleteAndLeaveHandler(controller, peer)();\n\t}\n}\n\nQString ResolveQuickDialogLottieIconName(Ui::QuickDialogActionLabel action) {\n\tswitch (action) {\n\tcase Ui::QuickDialogActionLabel::Mute:\n\t\treturn u\"swipe_mute\"_q;\n\tcase Ui::QuickDialogActionLabel::Unmute:\n\t\treturn u\"swipe_unmute\"_q;\n\tcase Ui::QuickDialogActionLabel::Pin:\n\t\treturn u\"swipe_pin\"_q;\n\tcase Ui::QuickDialogActionLabel::Unpin:\n\t\treturn u\"swipe_unpin\"_q;\n\tcase Ui::QuickDialogActionLabel::Read:\n\t\treturn u\"swipe_read\"_q;\n\tcase Ui::QuickDialogActionLabel::Unread:\n\t\treturn u\"swipe_unread\"_q;\n\tcase Ui::QuickDialogActionLabel::Archive:\n\t\treturn u\"swipe_archive\"_q;\n\tcase Ui::QuickDialogActionLabel::Unarchive:\n\t\treturn u\"swipe_unarchive\"_q;\n\tcase Ui::QuickDialogActionLabel::Delete:\n\t\treturn u\"swipe_delete\"_q;\n\tdefault:\n\t\treturn u\"swipe_disabled\"_q;\n\t}\n}\n\nUi::QuickDialogActionLabel ResolveQuickDialogLabel(\n\t\tnot_null<History*> history,\n\t\tUi::QuickDialogAction action,\n\t\tFilterId filterId) {\n\tif (action == Dialogs::Ui::QuickDialogAction::Mute) {\n\t\tif (history->peer->isSelf()) {\n\t\t\treturn Ui::QuickDialogActionLabel::Disabled;\n\t\t}\n\t\tconst auto isMuted = rpl::variable<bool>(\n\t\t\tMuteMenu::ThreadDescriptor(history).isMutedValue()).current();\n\t\treturn isMuted\n\t\t\t? Ui::QuickDialogActionLabel::Unmute\n\t\t\t: Ui::QuickDialogActionLabel::Mute;\n\t} else if (action == Dialogs::Ui::QuickDialogAction::Pin) {\n\t\tconst auto entry = (Dialogs::Entry*)(history);\n\t\treturn entry->isPinnedDialog(filterId)\n\t\t\t? Ui::QuickDialogActionLabel::Unpin\n\t\t\t: Ui::QuickDialogActionLabel::Pin;\n\t} else if (action == Dialogs::Ui::QuickDialogAction::Read) {\n\t\tconst auto unread = Window::IsUnreadThread(history);\n\t\tif (history->isForum() && !unread) {\n\t\t\treturn Ui::QuickDialogActionLabel::Disabled;\n\t\t}\n\t\treturn unread\n\t\t\t? Ui::QuickDialogActionLabel::Read\n\t\t\t: Ui::QuickDialogActionLabel::Unread;\n\t} else if (action == Dialogs::Ui::QuickDialogAction::Archive) {\n\t\tif (!Window::CanArchive(history, history->peer)) {\n\t\t\treturn Ui::QuickDialogActionLabel::Disabled;\n\t\t}\n\t\treturn Window::IsArchived(history)\n\t\t\t? Ui::QuickDialogActionLabel::Unarchive\n\t\t\t: Ui::QuickDialogActionLabel::Archive;\n\t} else if (action == Dialogs::Ui::QuickDialogAction::Delete) {\n\t\treturn Ui::QuickDialogActionLabel::Delete;\n\t}\n\treturn Ui::QuickDialogActionLabel::Disabled;\n}\n\nQString ResolveQuickDialogLabel(Ui::QuickDialogActionLabel action) {\n\tswitch (action) {\n\tcase Ui::QuickDialogActionLabel::Mute:\n\t\treturn tr::lng_settings_quick_dialog_action_mute(tr::now);\n\tcase Ui::QuickDialogActionLabel::Unmute:\n\t\treturn tr::lng_settings_quick_dialog_action_unmute(tr::now);\n\tcase Ui::QuickDialogActionLabel::Pin:\n\t\treturn tr::lng_settings_quick_dialog_action_pin(tr::now);\n\tcase Ui::QuickDialogActionLabel::Unpin:\n\t\treturn tr::lng_settings_quick_dialog_action_unpin(tr::now);\n\tcase Ui::QuickDialogActionLabel::Read:\n\t\treturn tr::lng_settings_quick_dialog_action_read(tr::now);\n\tcase Ui::QuickDialogActionLabel::Unread:\n\t\treturn tr::lng_settings_quick_dialog_action_unread(tr::now);\n\tcase Ui::QuickDialogActionLabel::Archive:\n\t\treturn tr::lng_settings_quick_dialog_action_archive(tr::now);\n\tcase Ui::QuickDialogActionLabel::Unarchive:\n\t\treturn tr::lng_settings_quick_dialog_action_unarchive(tr::now);\n\tcase Ui::QuickDialogActionLabel::Delete:\n\t\treturn tr::lng_settings_quick_dialog_action_delete(tr::now);\n\tdefault:\n\t\treturn tr::lng_settings_quick_dialog_action_disabled(tr::now);\n\t};\n}\n\nconst style::color &ResolveQuickActionBg(\n\t\tUi::QuickDialogActionLabel action) {\n\tswitch (action) {\n\tcase Ui::QuickDialogActionLabel::Delete:\n\t\treturn st::attentionButtonFg;\n\tcase Ui::QuickDialogActionLabel::Disabled:\n\t\treturn st::windowSubTextFgOver;\n\tcase Ui::QuickDialogActionLabel::Mute:\n\tcase Ui::QuickDialogActionLabel::Unmute:\n\tcase Ui::QuickDialogActionLabel::Pin:\n\tcase Ui::QuickDialogActionLabel::Unpin:\n\tcase Ui::QuickDialogActionLabel::Read:\n\tcase Ui::QuickDialogActionLabel::Unread:\n\tcase Ui::QuickDialogActionLabel::Archive:\n\tcase Ui::QuickDialogActionLabel::Unarchive:\n\tdefault:\n\t\treturn st::windowBgActive;\n\t};\n}\n\nconst style::color &ResolveQuickActionBgActive(\n\t\tUi::QuickDialogActionLabel action) {\n\treturn st::windowSubTextFgOver;\n}\n\nvoid DrawQuickAction(\n\t\tQPainter &p,\n\t\tconst QRect &rect,\n\t\tnot_null<Lottie::Icon*> icon,\n\t\tUi::QuickDialogActionLabel label,\n\t\tfloat64 iconRatio,\n\t\tbool twoLines) {\n\tconst auto iconSize = st::dialogsQuickActionSize * iconRatio;\n\tconst auto innerHeight = iconSize * 2;\n\tconst auto top = (rect.height() - innerHeight) / 2;\n\ticon->paint(p, rect.x() + (rect.width() - iconSize) / 2, top);\n\tp.setPen(st::premiumButtonFg);\n\tp.setBrush(Qt::NoBrush);\n\tconst auto availableWidth = rect.width();\n\tp.setFont(SwipeActionFont(label, availableWidth));\n\tif (twoLines) {\n\t\tauto text = ResolveQuickDialogLabel(label);\n\t\tconst auto index = text.indexOf(' ');\n\t\tif (index != -1) {\n\t\t\ttext = text.replace(index, 1, '\\n');\n\t\t}\n\t\tp.drawText(\n\t\t\tQRect(rect.x(), top, availableWidth, innerHeight),\n\t\t\tstd::move(text),\n\t\t\tstyle::al_bottom);\n\t} else {\n\t\tp.drawText(\n\t\t\tQRect(rect.x(), top, availableWidth, innerHeight),\n\t\t\tResolveQuickDialogLabel(label),\n\t\t\tstyle::al_bottom);\n\t}\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_quick_action.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass History;\nclass PeerData;\n\nnamespace Dialogs::Ui {\nenum class QuickDialogAction;\nenum class QuickDialogActionLabel;\n} // namespace Dialogs::Ui\n\nnamespace Lottie {\nclass Icon;\n} // namespace Lottie\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Dialogs {\n\nvoid PerformQuickDialogAction(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\tUi::QuickDialogAction action,\n\tFilterId filterId);\n\n[[nodiscard]] QString ResolveQuickDialogLottieIconName(\n\tUi::QuickDialogActionLabel action);\n\n[[nodiscard]] Ui::QuickDialogActionLabel ResolveQuickDialogLabel(\n\tnot_null<History*> history,\n\tUi::QuickDialogAction action,\n\tFilterId filterId);\n\n[[nodiscard]] QString ResolveQuickDialogLabel(Ui::QuickDialogActionLabel);\n\n[[nodiscard]] const style::color &ResolveQuickActionBg(\n\tUi::QuickDialogActionLabel);\n[[nodiscard]] const style::color &ResolveQuickActionBgActive(\n\tUi::QuickDialogActionLabel);\n\nvoid DrawQuickAction(\n\tQPainter &p,\n\tconst QRect &rect,\n\tnot_null<Lottie::Icon*> icon,\n\tUi::QuickDialogActionLabel label,\n\tfloat64 iconRatio = 1.,\n\tbool twoLines = false);\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_row.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/dialogs_row.h\"\n\n#include \"ui/chat/chat_theme.h\" // CountAverageColor.\n#include \"ui/color_contrast.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/effects/outline_segments.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/ttl_icon.h\"\n#include \"ui/effects/round_checkbox.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/text/custom_emoji_text_badge.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"dialogs/dialogs_entry.h\"\n#include \"dialogs/ui/dialogs_video_userpic.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"base/unixtime.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Dialogs {\nnamespace {\n\nconstexpr auto kTopLayer = 2;\nconstexpr auto kBottomLayer = 1;\nconstexpr auto kNoneLayer = 0;\nconstexpr auto kBlurRadius = 24;\n\n[[nodiscard]] const QPainterPath &SubscriptionOutlinePath() {\n\tstatic auto path = QPainterPath();\n\tif (!path.isEmpty()) {\n\t\treturn path;\n\t}\n\tconst auto scaledMoveTo = [&](float64 x, float64 y) {\n\t\tpath.moveTo(style::ConvertFloatScale(x), style::ConvertFloatScale(y));\n\t};\n\tconst auto scaledLineTo = [&](float64 x, float64 y) {\n\t\tpath.lineTo(style::ConvertFloatScale(x), style::ConvertFloatScale(y));\n\t};\n\tconst auto scaledCubicTo = [&](\n\t\t\tfloat64 x1,\n\t\t\tfloat64 y1,\n\t\t\tfloat64 x2,\n\t\t\tfloat64 y2,\n\t\t\tfloat64 x3,\n\t\t\tfloat64 y3) {\n\t\tpath.cubicTo(\n\t\t\tstyle::ConvertFloatScale(x1),\n\t\t\tstyle::ConvertFloatScale(y1),\n\t\t\tstyle::ConvertFloatScale(x2),\n\t\t\tstyle::ConvertFloatScale(y2),\n\t\t\tstyle::ConvertFloatScale(x3),\n\t\t\tstyle::ConvertFloatScale(y3));\n\t};\n\tconst auto scaledTranslate = [&](float64 x, float64 y) {\n\t\tpath.translate(\n\t\t\tstyle::ConvertFloatScale(x),\n\t\t\tstyle::ConvertFloatScale(y));\n\t};\n\n\tscaledMoveTo(42.3009, 18.3345);\n\tscaledLineTo(44.3285, 14.1203);\n\tscaledCubicTo(44.6152, 13.6549, 45.7858, 13.3542, 46.1909, 13.5523);\n\tscaledCubicTo(46.3355, 13.6044, 47.0064, 13.7541, 47.3833, 14.5053);\n\tscaledLineTo(49.3924 * 1.0071, 18.4206 * 0.9905);\n\t// 49.5459 * 1.007, 18.7336 * 0.9897.\n\tscaledCubicTo(49.8927213, 18.5406439, 52.5473, 18.8491, 53.3141, 18.8789);\n\tscaledCubicTo(53.6484, 18.8441, 55.8914, 20.0065, 54.3752, 20.7818);\n\tscaledCubicTo(54.1725, 20.8744, 41.3467, 31.3217, 41.3467, 31.3217);\n\tscaledCubicTo(40.7918, 31.5944, 41.2661, 31.4116, 40.8968, 30.9483);\n\tscaledCubicTo(39.9809, 30.3111, 40.0577, 25.4542, 40.1925, 25.5408);\n\tscaledCubicTo(39.9835, 25.6454, 38.4545, 22.9776, 37.8121, 22.3477);\n\tscaledLineTo(37.3236, 21.4448);\n\tscaledCubicTo(37.0943, 20.8845, 37.2524, 20.4742, 37.4164, 19.7765);\n\tscaledCubicTo(37.4703, 19.4582, 38.1756, 19.0759, 38.4504, 19.0422);\n\tscaledLineTo(41.6566, 18.6449);\n\tscaledCubicTo(41.5344, 18.6041, 42.2622, 18.6087, 42.3009, 18.3345);\n\tscaledTranslate(-42.3009, -18.3345);\n\tscaledTranslate(1.2, 0.4);\n\n\treturn path;\n}\n\n[[nodiscard]] const QImage &SubscriptionIcon() {\n\tstatic auto starImage = QImage();\n\tif (!starImage.isNull()) {\n\t\treturn starImage;\n\t}\n\tstarImage = Ui::GenerateStars(st::dialogsSubscriptionBadgeSize, 1);\n\treturn starImage;\n}\n\n[[nodiscard]] QImage CornerBadgeTTL(\n\t\tnot_null<PeerData*> peer,\n\t\tUi::PeerUserpicView &view,\n\t\tint photoSize) {\n\tconst auto ttl = peer->messagesTTL();\n\tif (!ttl) {\n\t\treturn QImage();\n\t}\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto fullSize = photoSize;\n\tconst auto partRect = CornerBadgeTTLRect(fullSize);\n\tconst auto &partSize = partRect.width();\n\tconst auto partSkip = fullSize - partSize;\n\tauto result = Images::Circle(BlurredDarkenedPart(\n\t\tPeerData::GenerateUserpicImage(peer, view, fullSize * ratio, 0),\n\t\tQRect(\n\t\t\tQPoint(partSkip, partSkip) * ratio,\n\t\t\tQSize(partSize, partSize) * ratio)));\n\tresult.setDevicePixelRatio(ratio);\n\n\tauto q = QPainter(&result);\n\tPainterHighQualityEnabler hq(q);\n\n\tconst auto innerRect = QRect(QPoint(), partRect.size())\n\t\t- st::dialogsTTLBadgeInnerMargins;\n\tconst auto ttlText = Ui::FormatTTLTiny(ttl);\n\n\tUi::PaintTimerIcon(q, innerRect, ttlText, st::premiumButtonFg->c);\n\n\treturn result;\n}\n\n} // namespace\n\nQRect CornerBadgeTTLRect(int photoSize) {\n\tconst auto &partSize = st::dialogsTTLBadgeSize;\n\treturn QRect(\n\t\tphotoSize - partSize + st::dialogsTTLBadgeSkip.x(),\n\t\tphotoSize - partSize + st::dialogsTTLBadgeSkip.y(),\n\t\tpartSize,\n\t\tpartSize);\n}\n\nQImage BlurredDarkenedPart(QImage image, QRect part) {\n\tauto blurred = Images::BlurLargeImage(\n\t\tstd::move(image),\n\t\tkBlurRadius).copy(part);\n\n\tconstexpr auto kMinAcceptableContrast = 4.5;\n\tconst auto averageColor = Ui::CountAverageColor(blurred);\n\tconst auto contrast = Ui::CountContrast(\n\t\taverageColor,\n\t\tst::premiumButtonFg->c);\n\tif (contrast < kMinAcceptableContrast) {\n\t\tconstexpr auto kDarkerBy = 0.2;\n\t\tauto painterPart = QPainter(&blurred);\n\t\tpainterPart.setOpacity(kDarkerBy);\n\t\tpainterPart.fillRect(QRect(QPoint(), part.size()), Qt::black);\n\t}\n\n\tblurred.setDevicePixelRatio(image.devicePixelRatio());\n\treturn blurred;\n}\n\nRow::CornerLayersManager::CornerLayersManager() = default;\n\nbool Row::CornerLayersManager::isSameLayer(Layer layer) const {\n\treturn isFinished() && (_nextLayer == layer);\n}\n\nvoid Row::CornerLayersManager::setLayer(\n\t\tLayer layer,\n\t\tFn<void()> updateCallback) {\n\tif (_nextLayer == layer) {\n\t\treturn;\n\t}\n\t_lastFrameShown = false;\n\t_prevLayer = _nextLayer;\n\t_nextLayer = layer;\n\tif (_animation.animating()) {\n\t\t_animation.change(\n\t\t\t1.,\n\t\t\tst::dialogsOnlineBadgeDuration * (1. - _animation.value(1.)));\n\t} else if (updateCallback) {\n\t\t_animation.start(\n\t\t\tstd::move(updateCallback),\n\t\t\t0.,\n\t\t\t1.,\n\t\t\tst::dialogsOnlineBadgeDuration);\n\t}\n}\n\nfloat64 Row::CornerLayersManager::progressForLayer(Layer layer) const {\n\treturn (_nextLayer == layer)\n\t\t? progress()\n\t\t: (_prevLayer == layer)\n\t\t? (1. - progress())\n\t\t: 0.;\n}\n\nfloat64 Row::CornerLayersManager::progress() const {\n\treturn _animation.value(1.);\n}\n\nbool Row::CornerLayersManager::isFinished() const {\n\treturn (progress() == 1.) && _lastFrameShown;\n}\n\nvoid Row::CornerLayersManager::markFrameShown() {\n\tif (progress() == 1.) {\n\t\t_lastFrameShown = true;\n\t}\n}\n\nbool Row::CornerLayersManager::isDisplayedNone() const {\n\treturn (progress() == 1.) && (_nextLayer == 0);\n}\n\nBasicRow::BasicRow() = default;\nBasicRow::~BasicRow() = default;\n\nvoid BasicRow::addRipple(\n\t\tQPoint origin,\n\t\tQSize size,\n\t\tFn<void()> updateCallback) {\n\tif (!_ripple) {\n\t\taddRippleWithMask(\n\t\t\torigin,\n\t\t\tUi::RippleAnimation::RectMask(size),\n\t\t\tstd::move(updateCallback));\n\t} else {\n\t\t_ripple->add(origin);\n\t}\n}\n\nvoid BasicRow::addRippleWithMask(\n\t\tQPoint origin,\n\t\tQImage mask,\n\t\tFn<void()> updateCallback) {\n\t_ripple = std::make_unique<Ui::RippleAnimation>(\n\t\tst::dialogsRipple,\n\t\tstd::move(mask),\n\t\tstd::move(updateCallback));\n\t_ripple->add(origin);\n}\n\nvoid BasicRow::clearRipple() {\n\t_ripple = nullptr;\n}\n\nvoid BasicRow::stopLastRipple() {\n\tif (_ripple) {\n\t\t_ripple->lastStop();\n\t}\n}\n\nvoid BasicRow::paintRipple(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tconst QColor *colorOverride) const {\n\tif (_ripple) {\n\t\t_ripple->paint(p, x, y, outerWidth, colorOverride);\n\t\tif (_ripple->empty()) {\n\t\t\t_ripple.reset();\n\t\t}\n\t}\n}\n\nvoid BasicRow::paintUserpic(\n\t\tPainter &p,\n\t\tnot_null<Entry*> entry,\n\t\tPeerData *peer,\n\t\tUi::VideoUserpic *videoUserpic,\n\t\tconst Ui::PaintContext &context,\n\t\tbool hasUnreadBadgesAbove) const {\n\tPaintUserpic(p, entry, peer, videoUserpic, _userpic, context);\n}\n\nRow::Row(Key key, int index, int top) : _id(key), _top(top), _index(index) {\n\tif (const auto history = key.history()) {\n\t\tupdateCornerBadgeShown(history->peer);\n\t}\n}\n\nRow::~Row() {\n\tclearTopicJumpRipple();\n}\n\nconst style::DialogRow &Row::ComputeSt(\n\t\tnot_null<const Entry*> entry,\n\t\tFilterId filterId) {\n\tif (const auto history = entry->asHistory()) {\n\t\tconst auto hasTags = entry->hasChatsFilterTags(filterId);\n\t\tconst auto wideRow = history->isForum()\n\t\t\t|| history->amMonoforumAdmin();\n\t\treturn wideRow\n\t\t\t? (hasTags ? st::taggedForumDialogRow : st::forumDialogRow)\n\t\t\t: hasTags\n\t\t\t? st::taggedDialogRow\n\t\t\t: st::defaultDialogRow;\n\t} else if (entry->asTopic()) {\n\t\treturn st::forumTopicRow;\n\t}\n\treturn st::defaultDialogRow;\n}\n\nvoid Row::recountHeight(float64 narrowRatio, FilterId filterId) {\n\tconst auto &st = ComputeSt(_id.entry(), filterId);\n\tif (_id.history() && _id.history()->isTopPromoted()) {\n\t\t_height = 1;\n\t\treturn;\n\t}\n\t_height = ((&st == &st::defaultDialogRow) || !_id.history())\n\t\t? st::defaultDialogRow.height\n\t\t: anim::interpolate(\n\t\t\tst.height,\n\t\t\tst::defaultDialogRow.height,\n\t\t\tnarrowRatio);\n}\n\nuint64 Row::sortKey(FilterId filterId) const {\n\treturn _id.entry()->sortKeyInChatList(filterId);\n}\n\nvoid Row::setCornerBadgeShown(\n\t\tCornerLayersManager::Layer nextLayer,\n\t\tFn<void()> updateCallback) const {\n\tconst auto cornerBadgeShown = (nextLayer ? 1 : 0);\n\tif (_cornerBadgeShown == cornerBadgeShown) {\n\t\tif (!cornerBadgeShown) {\n\t\t\treturn;\n\t\t} else if (_cornerBadgeUserpic\n\t\t\t&& _cornerBadgeUserpic->layersManager.isSameLayer(nextLayer)) {\n\t\t\treturn;\n\t\t}\n\t}\n\tconst_cast<Row*>(this)->_cornerBadgeShown = cornerBadgeShown;\n\tensureCornerBadgeUserpic();\n\t_cornerBadgeUserpic->layersManager.setLayer(\n\t\tnextLayer,\n\t\tstd::move(updateCallback));\n\tif (!_cornerBadgeShown\n\t\t&& _cornerBadgeUserpic\n\t\t&& _cornerBadgeUserpic->layersManager.isDisplayedNone()) {\n\t\t_cornerBadgeUserpic = nullptr;\n\t}\n}\n\nvoid Row::updateCornerBadgeShown(\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> updateCallback,\n\t\tbool hasUnreadBadgesAbove) const {\n\tconst auto user = peer->asUser();\n\tconst auto now = user ? base::unixtime::now() : TimeId();\n\tconst auto channel = user ? nullptr : peer->asChannel();\n\tconst auto nextLayer = [&] {\n\t\tif (hasUnreadBadgesAbove) {\n\t\t\treturn kNoneLayer;\n\t\t} else if (user && Data::IsUserOnline(user, now)) {\n\t\t\treturn kTopLayer;\n\t\t} else if (channel\n\t\t\t&& (Data::ChannelHasActiveCall(channel)\n\t\t\t\t|| Data::ChannelHasSubscriptionUntilDate(channel))) {\n\t\t\treturn kTopLayer;\n\t\t} else if (peer->messagesTTL()) {\n\t\t\treturn kBottomLayer;\n\t\t}\n\t\treturn kNoneLayer;\n\t}();\n\tsetCornerBadgeShown(nextLayer, std::move(updateCallback));\n\tif ((nextLayer == kTopLayer) && user) {\n\t\tpeer->owner().watchForOffline(user, now);\n\t}\n}\n\nvoid Row::ensureCornerBadgeUserpic() const {\n\tif (_cornerBadgeUserpic) {\n\t\treturn;\n\t}\n\t_cornerBadgeUserpic = std::make_unique<CornerBadgeUserpic>();\n}\n\nvoid Row::PaintCornerBadgeFrame(\n\t\tnot_null<CornerBadgeUserpic*> data,\n\t\tint framePadding,\n\t\tnot_null<Entry*> entry,\n\t\tPeerData *peer,\n\t\tUi::VideoUserpic *videoUserpic,\n\t\tUi::PeerUserpicView &view,\n\t\tconst Ui::PaintContext &context,\n\t\tbool subscribed) {\n\tdata->frame.fill(Qt::transparent);\n\n\tPainter q(&data->frame);\n\tq.translate(framePadding, framePadding);\n\tauto hq = std::optional<PainterHighQualityEnabler>();\n\tconst auto photoSize = context.st->photoSize;\n\tconst auto storiesCount = data->storiesCount;\n\tif (storiesCount) {\n\t\thq.emplace(q);\n\t\tconst auto line = st::dialogsStoriesFull.lineTwice / 2.;\n\t\tconst auto skip = line * 3 / 2.;\n\t\tconst auto scale = 1. - (2 * skip / photoSize);\n\t\tconst auto center = photoSize / 2.;\n\t\tq.save();\n\t\tq.translate(center, center);\n\t\tq.scale(scale, scale);\n\t\tq.translate(-center, -center);\n\t}\n\tq.translate(-context.st->padding.left(), -context.st->padding.top());\n\tPaintUserpic(\n\t\tq,\n\t\tentry,\n\t\tpeer,\n\t\tvideoUserpic,\n\t\tview,\n\t\tcontext);\n\tq.translate(context.st->padding.left(), context.st->padding.top());\n\tif (storiesCount) {\n\t\tq.restore();\n\n\t\tconst auto outline = QRectF(0, 0, photoSize, photoSize);\n\t\tconst auto storiesUnread = st::dialogsStoriesFull.lineTwice / 2.;\n\t\tconst auto storiesLine = st::dialogsStoriesFull.lineReadTwice / 2.;\n\t\tauto segments = std::vector<Ui::OutlineSegment>();\n\t\tif (data->storiesHasVideoStream) {\n\t\t\tconst auto storiesVideoStreamBrush = st::attentionButtonFg->b;\n\t\t\tsegments.push_back({ storiesVideoStreamBrush, storiesUnread });\n\t\t} else {\n\t\t\tconst auto storiesUnreadCount = data->storiesUnreadCount;\n\t\t\tconst auto storiesUnreadBrush = [&] {\n\t\t\t\tif (context.active || !storiesUnreadCount) {\n\t\t\t\t\treturn st::dialogsUnreadBgMutedActive->b;\n\t\t\t\t}\n\t\t\t\tauto gradient = Ui::UnreadStoryOutlineGradient(outline);\n\t\t\t\treturn QBrush(gradient);\n\t\t\t}();\n\t\t\tconst auto storiesBrush = context.active\n\t\t\t\t? st::dialogsUnreadBgMutedActive->b\n\t\t\t\t: st::dialogsUnreadBgMuted->b;\n\t\t\tsegments.reserve(storiesCount);\n\t\t\tconst auto storiesReadCount = storiesCount - storiesUnreadCount;\n\t\t\tfor (auto i = 0; i != storiesReadCount; ++i) {\n\t\t\t\tsegments.push_back({ storiesBrush, storiesLine });\n\t\t\t}\n\t\t\tfor (auto i = 0; i != storiesUnreadCount; ++i) {\n\t\t\t\tsegments.push_back({ storiesUnreadBrush, storiesUnread });\n\t\t\t}\n\t\t}\n\t\tif (peer && (peer->forum() || peer->monoforum())) {\n\t\t\tconst auto radius = context.st->photoSize\n\t\t\t\t* Ui::ForumUserpicRadiusMultiplier();\n\t\t\tUi::PaintOutlineSegments(q, outline, radius, segments);\n\t\t} else {\n\t\t\tUi::PaintOutlineSegments(q, outline, segments);\n\t\t}\n\n\t\tif (data->storiesHasVideoStream) {\n\t\t\tUi::PaintLiveBadge(q, 0, 0, photoSize);\n\t\t}\n\t}\n\n\tif (subscribed) {\n\t\tif (!hq) {\n\t\t\thq.emplace(q);\n\t\t}\n\t\t// TODO: Unnecessarily repaints on activating peer.\n\t\tq.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tconst auto &s = st::dialogsSubscriptionBadgeSkip;\n\t\tauto path = SubscriptionOutlinePath();\n\t\tconst auto x = photoSize - s.x() - st::dialogsSubscriptionBadgeSize;\n\t\tconst auto y = photoSize - s.y() - st::dialogsSubscriptionBadgeSize;\n\t\tq.translate(x, y);\n\t\tq.fillPath(path, Qt::transparent);\n\t\tq.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\tq.resetTransform();\n\t\tq.drawImage(x, y, SubscriptionIcon());\n\t\treturn;\n\t}\n\n\tconst auto &manager = data->layersManager;\n\tif (const auto p = manager.progressForLayer(kBottomLayer); p > 0.) {\n\t\tconst auto size = photoSize;\n\t\tif (data->cacheTTL.isNull() && peer && peer->messagesTTL()) {\n\t\t\tdata->cacheTTL = CornerBadgeTTL(peer, view, size);\n\t\t}\n\t\tq.setOpacity(p);\n\t\tconst auto point = CornerBadgeTTLRect(size).topLeft();\n\t\tq.drawImage(point, data->cacheTTL);\n\t\tq.setOpacity(1.);\n\t}\n\tconst auto topLayerProgress = manager.progressForLayer(kTopLayer);\n\tif (!topLayerProgress) {\n\t\treturn;\n\t}\n\n\tif (!hq) {\n\t\thq.emplace(q);\n\t}\n\tq.setCompositionMode(QPainter::CompositionMode_Source);\n\n\tconst auto online = peer && peer->isUser();\n\tconst auto size = online\n\t\t? st::dialogsOnlineBadgeSize\n\t\t: st::dialogsCallBadgeSize;\n\tconst auto stroke = st::dialogsOnlineBadgeStroke;\n\tconst auto skip = online\n\t\t? st::dialogsOnlineBadgeSkip\n\t\t: st::dialogsCallBadgeSkip;\n\tconst auto shrink = (size / 2) * (1. - topLayerProgress);\n\n\tauto pen = QPen(Qt::transparent);\n\tpen.setWidthF(stroke * topLayerProgress);\n\tq.setPen(pen);\n\tq.setBrush(data->active\n\t\t? st::dialogsOnlineBadgeFgActive\n\t\t: st::dialogsOnlineBadgeFg);\n\tq.drawEllipse(QRectF(\n\t\tphotoSize - skip.x() - size,\n\t\tphotoSize - skip.y() - size,\n\t\tsize,\n\t\tsize\n\t).marginsRemoved({ shrink, shrink, shrink, shrink }));\n}\n\nvoid Row::paintUserpic(\n\t\tPainter &p,\n\t\tnot_null<Entry*> entry,\n\t\tPeerData *peer,\n\t\tUi::VideoUserpic *videoUserpic,\n\t\tconst Ui::PaintContext &context,\n\t\tbool hasUnreadBadgesAbove) const {\n\tif (peer) {\n\t\tupdateCornerBadgeShown(peer, nullptr, hasUnreadBadgesAbove);\n\t}\n\n\tconst auto cornerBadgeShown = !_cornerBadgeUserpic\n\t\t? _cornerBadgeShown\n\t\t: !_cornerBadgeUserpic->layersManager.isDisplayedNone();\n\tconst auto storiesPeer = peer\n\t\t? ((peer->isUser() || peer->isChannel()) ? peer : nullptr)\n\t\t: nullptr;\n\tconst auto storiesFolder = peer ? nullptr : _id.folder();\n\tconst auto storiesHas = storiesPeer\n\t\t? storiesPeer->hasActiveStories()\n\t\t: storiesFolder\n\t\t? (storiesFolder->storiesCount() > 0)\n\t\t: false;\n\tif (!cornerBadgeShown && !storiesHas) {\n\t\tBasicRow::paintUserpic(p, entry, peer, videoUserpic, context, false);\n\t\tif (!peer || !_cornerBadgeShown) {\n\t\t\t_cornerBadgeUserpic = nullptr;\n\t\t}\n\t\treturn;\n\t}\n\tensureCornerBadgeUserpic();\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto framePadding = std::max({\n\t\t-st::dialogsCallBadgeSkip.x(),\n\t\t-st::dialogsCallBadgeSkip.y(),\n\t\tst::lineWidth * 2 });\n\tconst auto frameSide = (2 * framePadding + context.st->photoSize)\n\t\t* ratio;\n\tconst auto frameSize = QSize(frameSide, frameSide);\n\tconst auto storiesSource = (storiesHas && storiesPeer)\n\t\t? storiesPeer->owner().stories().source(storiesPeer->id)\n\t\t: nullptr;\n\tconst auto storiesCountReal = storiesSource\n\t\t? int(storiesSource->ids.size())\n\t\t: storiesFolder\n\t\t? storiesFolder->storiesCount()\n\t\t: storiesHas\n\t\t? 1\n\t\t: 0;\n\tconst auto storiesUnreadCountReal = storiesSource\n\t\t? storiesSource->unreadCount()\n\t\t: storiesFolder\n\t\t? storiesFolder->storiesUnreadCount()\n\t\t: (storiesPeer && storiesPeer->hasUnreadStories())\n\t\t? 1\n\t\t: 0;\n\tconst auto storiesHasVideoStream = storiesSource\n\t\t? storiesSource->hasVideoStream\n\t\t: (storiesPeer && storiesPeer->hasActiveVideoStream())\n\t\t? 1\n\t\t: 0;\n\tconst auto limit = Ui::kOutlineSegmentsMax;\n\tconst auto storiesCount = std::min(storiesCountReal, limit);\n\tconst auto storiesUnreadCount = std::min(storiesUnreadCountReal, limit);\n\tif (_cornerBadgeUserpic->frame.size() != frameSize) {\n\t\t_cornerBadgeUserpic->frame = QImage(\n\t\t\tframeSize,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t_cornerBadgeUserpic->frame.setDevicePixelRatio(ratio);\n\t}\n\tauto key = peer ? peer->userpicUniqueKey(userpicView()) : InMemoryKey();\n\tkey.first += peer ? peer->messagesTTL() : 0;\n\tconst auto frameIndex = videoUserpic ? videoUserpic->frameIndex() : -1;\n\tconst auto paletteVersionReal = style::PaletteVersion();\n\tconst auto paletteVersion = (paletteVersionReal & ((1 << 17) - 1));\n\tconst auto active = context.active ? 1 : 0;\n\tconst auto keyChanged = (_cornerBadgeUserpic->key != key)\n\t\t|| (_cornerBadgeUserpic->paletteVersion != paletteVersion);\n\tif (keyChanged) {\n\t\t_cornerBadgeUserpic->cacheTTL = QImage();\n\t}\n\tconst auto subscribed = Data::ChannelHasSubscriptionUntilDate(\n\t\tpeer ? peer->asChannel() : nullptr);\n\tif (keyChanged\n\t\t|| !_cornerBadgeUserpic->layersManager.isFinished()\n\t\t|| _cornerBadgeUserpic->active != active\n\t\t|| _cornerBadgeUserpic->frameIndex != frameIndex\n\t\t|| _cornerBadgeUserpic->storiesCount != storiesCount\n\t\t|| _cornerBadgeUserpic->storiesUnreadCount != storiesUnreadCount\n\t\t|| _cornerBadgeUserpic->storiesHasVideoStream != storiesHasVideoStream\n\t\t|| videoUserpic) {\n\t\t_cornerBadgeUserpic->key = key;\n\t\t_cornerBadgeUserpic->paletteVersion = paletteVersion;\n\t\t_cornerBadgeUserpic->active = active;\n\t\t_cornerBadgeUserpic->storiesCount = storiesCount;\n\t\t_cornerBadgeUserpic->storiesUnreadCount = storiesUnreadCount;\n\t\t_cornerBadgeUserpic->storiesHasVideoStream = storiesHasVideoStream;\n\t\t_cornerBadgeUserpic->frameIndex = frameIndex;\n\t\t_cornerBadgeUserpic->layersManager.markFrameShown();\n\t\tPaintCornerBadgeFrame(\n\t\t\t_cornerBadgeUserpic.get(),\n\t\t\tframePadding,\n\t\t\t_id.entry(),\n\t\t\tpeer,\n\t\t\tvideoUserpic,\n\t\t\tuserpicView(),\n\t\t\tcontext,\n\t\t\tsubscribed);\n\t}\n\tp.drawImage(\n\t\tcontext.st->padding.left() - framePadding,\n\t\tcontext.st->padding.top() - framePadding,\n\t\t_cornerBadgeUserpic->frame);\n\tconst auto history = _id.history();\n\tif (!history || history->peer->isUser() || subscribed) {\n\t\treturn;\n\t}\n\tconst auto actionPainter = history->sendActionPainter();\n\tconst auto bg = context.active\n\t\t? st::dialogsBgActive\n\t\t: st::dialogsBg;\n\tconst auto size = st::dialogsCallBadgeSize;\n\tconst auto skip = st::dialogsCallBadgeSkip;\n\tp.setOpacity(\n\t\t_cornerBadgeUserpic->layersManager.progressForLayer(kTopLayer));\n\tp.translate(context.st->padding.left(), context.st->padding.top());\n\tactionPainter->paintSpeaking(\n\t\tp,\n\t\tcontext.st->photoSize - skip.x() - size,\n\t\tcontext.st->photoSize - skip.y() - size,\n\t\tcontext.width,\n\t\tbg,\n\t\tcontext.now);\n\tp.translate(-context.st->padding.left(), -context.st->padding.top());\n\tp.setOpacity(1.);\n}\n\nbool Row::lookupIsInTopicJump(int x, int y) const {\n\tconst auto history = this->history();\n\treturn history && history->lastItemDialogsView().isInTopicJump(x, y);\n}\n\nvoid Row::stopLastRipple() {\n\tBasicRow::stopLastRipple();\n\tconst auto history = this->history();\n\tconst auto view = history ? &history->lastItemDialogsView() : nullptr;\n\tif (view) {\n\t\tview->stopLastRipple();\n\t}\n}\n\nvoid Row::clearRipple() {\n\tBasicRow::clearRipple();\n\tclearTopicJumpRipple();\n}\n\nvoid Row::addTopicJumpRipple(\n\t\tQPoint origin,\n\t\tnot_null<Ui::TopicJumpCache*> topicJumpCache,\n\t\tFn<void()> updateCallback) {\n\tconst auto history = this->history();\n\tconst auto view = history ? &history->lastItemDialogsView() : nullptr;\n\tif (view) {\n\t\tview->addTopicJumpRipple(\n\t\t\torigin,\n\t\t\ttopicJumpCache,\n\t\t\tstd::move(updateCallback));\n\t\t_topicJumpRipple = 1;\n\t}\n}\n\nvoid Row::clearTopicJumpRipple() {\n\tif (!_topicJumpRipple) {\n\t\treturn;\n\t}\n\tconst auto history = this->history();\n\tconst auto view = history ? &history->lastItemDialogsView() : nullptr;\n\tif (view) {\n\t\tview->clearRipple();\n\t}\n\t_topicJumpRipple = 0;\n}\n\nbool Row::topicJumpRipple() const {\n\treturn _topicJumpRipple != 0;\n}\n\nFakeRow::FakeRow(\n\tKey searchInChat,\n\tnot_null<HistoryItem*> item,\n\tFn<void()> repaint)\n: _searchInChat(searchInChat)\n, _item(item)\n, _repaint(std::move(repaint)) {\n\tinvalidateTopic();\n}\n\nvoid FakeRow::invalidateTopic() {\n\t_topic = _item->topic();\n\tif (_topic) {\n\t\treturn;\n\t} else if (const auto rootId = _item->topicRootId()) {\n\t\tif (const auto forum = _item->history()->asForum()) {\n\t\t\tif (!forum->topicDeleted(rootId)) {\n\t\t\t\tforum->requestTopic(rootId, crl::guard(this, [=] {\n\t\t\t\t\t_topic = _item->topic();\n\t\t\t\t\tif (_topic) {\n\t\t\t\t\t\t_repaint();\n\t\t\t\t\t}\n\t\t\t\t}));\n\t\t\t}\n\t\t}\n\t}\n}\n\nconst Ui::Text::String &FakeRow::name() const {\n\tif (_name.isEmpty()) {\n\t\tconst auto from = _searchInChat\n\t\t\t? _item->displayFrom()\n\t\t\t: nullptr;\n\t\tconst auto peer = from ? from : _item->history()->peer.get();\n\t\t_name.setText(\n\t\t\tst::semiboldTextStyle,\n\t\t\tpeer->name(),\n\t\t\tUi::NameTextOptions());\n\t}\n\treturn _name;\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_row.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n#include \"ui/text/text.h\"\n#include \"ui/unread_badge.h\"\n#include \"ui/userpic_view.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"dialogs/ui/dialogs_message_view.h\"\n\nclass History;\nclass HistoryItem;\n\nnamespace style {\nstruct DialogRow;\n} // namespace style\n\nnamespace Ui {\nclass RippleAnimation;\n} // namespace Ui\n\nnamespace Dialogs::Ui {\nusing namespace ::Ui;\nclass RowPainter;\nclass VideoUserpic;\nstruct PaintContext;\nstruct TopicJumpCache;\n} // namespace Dialogs::Ui\n\nnamespace Dialogs {\n\nclass Entry;\nenum class SortMode;\n\n[[nodiscard]] QRect CornerBadgeTTLRect(int photoSize);\n[[nodiscard]] QImage BlurredDarkenedPart(QImage image, QRect part);\n\nclass BasicRow {\npublic:\n\tBasicRow();\n\tvirtual ~BasicRow();\n\n\tvirtual void paintUserpic(\n\t\tPainter &p,\n\t\tnot_null<Entry*> entry,\n\t\tPeerData *peer,\n\t\tUi::VideoUserpic *videoUserpic,\n\t\tconst Ui::PaintContext &context,\n\t\tbool hasUnreadBadgesAbove) const;\n\n\tvoid addRipple(QPoint origin, QSize size, Fn<void()> updateCallback);\n\tvirtual void stopLastRipple();\n\tvirtual void clearRipple();\n\tvoid addRippleWithMask(\n\t\tQPoint origin,\n\t\tQImage mask,\n\t\tFn<void()> updateCallback);\n\n\tvoid paintRipple(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tconst QColor *colorOverride = nullptr) const;\n\n\t[[nodiscard]] Ui::PeerUserpicView &userpicView() const {\n\t\treturn _userpic;\n\t}\n\nprivate:\n\tmutable Ui::PeerUserpicView _userpic;\n\tmutable std::unique_ptr<Ui::RippleAnimation> _ripple;\n\n};\n\nclass List;\nclass Row final : public BasicRow {\npublic:\n\texplicit Row(std::nullptr_t) {\n\t}\n\tRow(Key key, int index, int top);\n\t~Row();\n\n\t[[nodiscard]] static const style::DialogRow &ComputeSt(\n\t\tnot_null<const Entry*> entry,\n\t\tFilterId filterId);\n\n\t[[nodiscard]] int top() const {\n\t\treturn _top;\n\t}\n\t[[nodiscard]] int height() const {\n\t\tExpects(_height != 0);\n\n\t\treturn _height;\n\t}\n\tvoid recountHeight(float64 narrowRatio, FilterId filterId);\n\n\tvoid updateCornerBadgeShown(\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> updateCallback = nullptr,\n\t\tbool hasUnreadBadgesAbove = false) const;\n\tvoid paintUserpic(\n\t\tPainter &p,\n\t\tnot_null<Entry*> entry,\n\t\tPeerData *peer,\n\t\tUi::VideoUserpic *videoUserpic,\n\t\tconst Ui::PaintContext &context,\n\t\tbool hasUnreadBadgesAbove) const final override;\n\n\t[[nodiscard]] bool lookupIsInTopicJump(int x, int y) const;\n\tvoid stopLastRipple() override;\n\tvoid clearRipple() override;\n\tvoid addTopicJumpRipple(\n\t\tQPoint origin,\n\t\tnot_null<Ui::TopicJumpCache*> topicJumpCache,\n\t\tFn<void()> updateCallback);\n\tvoid clearTopicJumpRipple();\n\t[[nodiscard]] bool topicJumpRipple() const;\n\n\t[[nodiscard]] Key key() const {\n\t\treturn _id;\n\t}\n\t[[nodiscard]] History *history() const {\n\t\treturn _id.history();\n\t}\n\t[[nodiscard]] Data::Folder *folder() const {\n\t\treturn _id.folder();\n\t}\n\t[[nodiscard]] Data::ForumTopic *topic() const {\n\t\treturn _id.topic();\n\t}\n\t[[nodiscard]] Data::Thread *thread() const {\n\t\treturn _id.thread();\n\t}\n\t[[nodiscard]] Data::SavedSublist *sublist() const {\n\t\treturn _id.sublist();\n\t}\n\t[[nodiscard]] not_null<Entry*> entry() const {\n\t\treturn _id.entry();\n\t}\n\t[[nodiscard]] int index() const {\n\t\treturn _index;\n\t}\n\t[[nodiscard]] uint64 sortKey(FilterId filterId) const;\n\n\t// for any attached data, for example View in contacts list\n\tvoid *attached = nullptr;\n\nprivate:\n\tfriend class List;\n\n\tclass CornerLayersManager {\n\tpublic:\n\t\tusing Layer = int;\n\t\tCornerLayersManager();\n\n\t\t[[nodiscard]] bool isSameLayer(Layer layer) const;\n\t\t[[nodiscard]] bool isDisplayedNone() const;\n\t\t[[nodiscard]] float64 progressForLayer(Layer layer) const;\n\t\t[[nodiscard]] float64 progress() const;\n\t\t[[nodiscard]] bool isFinished() const;\n\t\tvoid setLayer(Layer layer, Fn<void()> updateCallback);\n\t\tvoid markFrameShown();\n\n\tprivate:\n\t\tbool _lastFrameShown = false;\n\t\tLayer _prevLayer = 0;\n\t\tLayer _nextLayer = 0;\n\t\tUi::Animations::Simple _animation;\n\n\t};\n\n\tstruct CornerBadgeUserpic {\n\t\tInMemoryKey key;\n\t\tCornerLayersManager layersManager;\n\t\tQImage frame;\n\t\tQImage cacheTTL;\n\t\tint frameIndex = -1;\n\t\tuint32 paletteVersion : 16 = 0;\n\t\tuint32 storiesCount : 7 = 0;\n\t\tuint32 storiesUnreadCount : 7 = 0;\n\t\tuint32 storiesHasVideoStream : 1 = 0;\n\t\tuint32 active : 1 = 0;\n\t};\n\n\tvoid setCornerBadgeShown(\n\t\tCornerLayersManager::Layer nextLayer,\n\t\tFn<void()> updateCallback) const;\n\tvoid ensureCornerBadgeUserpic() const;\n\tstatic void PaintCornerBadgeFrame(\n\t\tnot_null<CornerBadgeUserpic*> data,\n\t\tint framePadding,\n\t\tnot_null<Entry*> entry,\n\t\tPeerData *peer,\n\t\tUi::VideoUserpic *videoUserpic,\n\t\tUi::PeerUserpicView &view,\n\t\tconst Ui::PaintContext &context,\n\t\tbool subscribed);\n\n\tKey _id;\n\tmutable std::unique_ptr<CornerBadgeUserpic> _cornerBadgeUserpic;\n\tint _top = 0;\n\tint _height = 0;\n\tuint32 _index : 30 = 0;\n\tuint32 _cornerBadgeShown : 1 = 0;\n\tuint32 _topicJumpRipple : 1 = 0;\n\n};\n\nclass FakeRow final : public BasicRow, public base::has_weak_ptr {\npublic:\n\tFakeRow(\n\t\tKey searchInChat,\n\t\tnot_null<HistoryItem*> item,\n\t\tFn<void()> repaint);\n\n\t[[nodiscard]] Key searchInChat() const {\n\t\treturn _searchInChat;\n\t}\n\t[[nodiscard]] Data::ForumTopic *topic() const {\n\t\treturn _topic;\n\t}\n\t[[nodiscard]] not_null<HistoryItem*> item() const {\n\t\treturn _item;\n\t}\n\t[[nodiscard]] Ui::MessageView &itemView() const {\n\t\treturn _itemView;\n\t}\n\t[[nodiscard]] Fn<void()> repaint() const {\n\t\treturn _repaint;\n\t}\n\t[[nodiscard]] Ui::PeerBadge &badge() const {\n\t\treturn _badge;\n\t}\n\t[[nodiscard]] const Ui::Text::String &name() const;\n\n\tvoid invalidateTopic();\n\nprivate:\n\tfriend class Ui::RowPainter;\n\n\tconst Key _searchInChat;\n\tconst not_null<HistoryItem*> _item;\n\tData::ForumTopic *_topic = nullptr;\n\tconst Fn<void()> _repaint;\n\tmutable Ui::MessageView _itemView;\n\tmutable Ui::PeerBadge _badge;\n\tmutable Ui::Text::String _name;\n\n};\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_search_from_controllers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/dialogs_search_from_controllers.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n\nnamespace Dialogs {\n\nobject_ptr<Ui::BoxContent> SearchFromBox(\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void(not_null<PeerData*>)> callback,\n\t\tFn<void()> closedCallback) {\n\tauto createController = [\n\t\tpeer,\n\t\tcallback = std::move(callback)\n\t]() -> std::unique_ptr<PeerListController> {\n\t\tif (peer && (peer->isChat() || peer->isMegagroup())) {\n\t\t\treturn std::make_unique<Dialogs::SearchFromController>(\n\t\t\t\tpeer,\n\t\t\t\tstd::move(callback));\n\t\t}\n\t\treturn nullptr;\n\t};\n\tif (auto controller = createController()) {\n\t\tauto subscription = std::make_shared<rpl::lifetime>();\n\t\tauto box = Box<PeerListBox>(\n\t\t\tstd::move(controller),\n\t\t\t[subscription](not_null<PeerListBox*> box) {\n\t\t\t\tbox->addButton(tr::lng_cancel(), [box, subscription] {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t});\n\t\t\t});\n\t\tbox->boxClosing() | rpl::on_next(\n\t\t\tstd::move(closedCallback),\n\t\t\t*subscription);\n\t\treturn box;\n\t}\n\treturn nullptr;\n}\n\nSearchFromController::SearchFromController(\n\tnot_null<PeerData*> peer,\n\tFn<void(not_null<PeerData*>)> callback)\n: AddSpecialBoxController(\n\tpeer,\n\tParticipantsBoxController::Role::Members,\n\tAdminDoneCallback(),\n\tBannedDoneCallback())\n, _callback(std::move(callback)) {\n\t_excludeSelf = false;\n}\n\nvoid SearchFromController::prepare() {\n\tAddSpecialBoxController::prepare();\n\tdelegate()->peerListSetTitle(tr::lng_search_messages_from());\n\tif (const auto megagroup = peer()->asMegagroup()) {\n\t\tif (!delegate()->peerListFindRow(megagroup->id.value)) {\n\t\t\tdelegate()->peerListAppendRow(\n\t\t\t\tstd::make_unique<PeerListRow>(megagroup));\n\t\t\tsetDescriptionText({});\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t}\n}\n\nvoid SearchFromController::rowClicked(not_null<PeerListRow*> row) {\n\tif (const auto onstack = base::duplicate(_callback)) {\n\t\tonstack(row->peer());\n\t}\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_search_from_controllers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peer_list_box.h\"\n#include \"boxes/peers/add_participants_box.h\"\n\nnamespace Dialogs {\n\nobject_ptr<Ui::BoxContent> SearchFromBox(\n\tnot_null<PeerData*> peer,\n\tFn<void(not_null<PeerData*>)> callback,\n\tFn<void()> closedCallback);\n\nclass SearchFromController : public AddSpecialBoxController {\npublic:\n\tSearchFromController(\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void(not_null<PeerData*>)> callback);\n\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\nprivate:\n\tFn<void(not_null<PeerData*>)> _callback;\n\n};\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_search_posts.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/dialogs_search_posts.h\"\n\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"data/data_session.h\"\n#include \"data/data_peer_values.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n\nnamespace Dialogs {\nnamespace {\n\nconstexpr auto kQueryDelay = crl::time(500);\nconstexpr auto kPerPage = 50;\n\n[[nodiscard]] const QRegularExpression &SearchSplitter() {\n\tstatic const auto result = QRegularExpression(QString::fromLatin1(\"\"\n\t\t\"[\\\\s\\\\-\\\\+\\\\(\\\\)\\\\[\\\\]\\\\{\\\\}\\\\<\\\\>\\\\,\\\\.\\\\!\\\\_\\\\;\\\\\\\"\\\\'\\\\x0]\"));\n\treturn result;\n}\n\n} // namespace\n\nPostsSearch::PostsSearch(not_null<Main::Session*> session)\n: _session(session)\n, _api(&_session->api().instance())\n, _timer([=] { applyQuery(); })\n, _recheckTimer([=] { recheck(); }) {\n\tData::AmPremiumValue(_session) | rpl::on_next([=] {\n\t\tmaybePushPremiumUpdate();\n\t}, _lifetime);\n}\n\nrpl::producer<PostsSearchState> PostsSearch::stateUpdates() const {\n\treturn _stateUpdates.events();\n}\n\nrpl::producer<PostsSearchState> PostsSearch::pagesUpdates() const {\n\treturn _pagesUpdates.events();\n}\n\nvoid PostsSearch::requestMore() {\n\tif (!_query) {\n\t\treturn;\n\t}\n\tauto &entry = _entries[*_query];\n\tif (_queryPushed != *_query || !entry.pagesPushed) {\n\t\treturn;\n\t} else if (entry.pagesPushed < entry.pages.size()) {\n\t\t_pagesUpdates.fire(PostsSearchState{\n\t\t\t.page = entry.pages[entry.pagesPushed++],\n\t\t\t.totalCount = entry.totalCount,\n\t\t});\n\t} else {\n\t\trequestSearch(*_query);\n\t}\n}\n\nvoid PostsSearch::setQuery(const QString &query) {\n\tconst auto words = TextUtilities::PrepareSearchWords(\n\t\tquery,\n\t\t&SearchSplitter());\n\tconst auto prepared = words.isEmpty() ? QString() : words.join(' ');\n\tif (_queryExact == query) {\n\t\treturn;\n\t}\n\t_queryExact = query;\n\t_query = prepared;\n\tconst auto i = _entries.find(prepared);\n\tif (i != end(_entries)) {\n\t\tpushStateUpdate(i->second);\n\t} else if (prepared.isEmpty()) {\n\t\tapplyQuery();\n\t} else {\n\t\t_timer.callOnce(kQueryDelay);\n\t}\n}\n\nint PostsSearch::setAllowedStars(int stars) {\n\tif (!_query) {\n\t\treturn 0;\n\t} else if (_floodState) {\n\t\tif (_floodState->freeSearchesLeft > 0) {\n\t\t\tstars = 0;\n\t\t} else if (_floodState->nextFreeSearchTime > 0\n\t\t\t&& _floodState->nextFreeSearchTime <= base::unixtime::now()) {\n\t\t\tstars = 0;\n\t\t} else {\n\t\t\tstars = std::min(int(_floodState->starsPerPaidSearch), stars);\n\t\t}\n\t}\n\t_entries[*_query].allowedStars = stars;\n\trequestSearch(*_query);\n\treturn stars;\n}\n\nvoid PostsSearch::pushStateUpdate(const Entry &entry) {\n\tExpects(_query.has_value());\n\n\tconst auto initial = (_queryPushed != *_query);\n\tif (initial) {\n\t\t_queryPushed = *_query;\n\t\tentry.pagesPushed = 0;\n\t} else if (entry.pagesPushed > 0) {\n\t\tif (entry.pagesPushed < entry.pages.size()) {\n\t\t\t_pagesUpdates.fire(PostsSearchState{\n\t\t\t\t.page = entry.pages[entry.pagesPushed++],\n\t\t\t\t.totalCount = entry.totalCount,\n\t\t\t});\n\t\t}\n\t\treturn;\n\t}\n\tconst auto empty = entry.pages.empty()\n\t\t|| (entry.pages.size() == 1 && entry.pages.front().empty());\n\tif (!empty || (entry.loaded && !_query->isEmpty())) {\n\t\tif (!entry.pages.empty()) {\n\t\t\t++entry.pagesPushed;\n\t\t}\n\t\t_stateUpdates.fire(PostsSearchState{\n\t\t\t.page = (entry.pages.empty()\n\t\t\t\t? std::vector<not_null<HistoryItem*>>()\n\t\t\t\t: entry.pages.front()),\n\t\t\t.totalCount = entry.totalCount,\n\t\t});\n\t} else if (entry.checkId || entry.searchId) {\n\t\t_stateUpdates.fire(PostsSearchState{\n\t\t\t.loading = true,\n\t\t});\n\t} else {\n\t\tAssert(_floodState.has_value());\n\t\tauto copy = _floodState;\n\t\tcopy->query = *_queryExact;\n\t\tcopy->needsPremium = !_session->premium();\n\t\t_stateUpdates.fire(PostsSearchState{\n\t\t\t.intro = std::move(copy),\n\t\t});\n\t}\n}\n\nvoid PostsSearch::maybePushPremiumUpdate() {\n\tif (!_floodState || !_query) {\n\t\treturn;\n\t}\n\tauto &entry = _entries[*_query];\n\tif (!entry.pages.empty()\n\t\t|| entry.loaded\n\t\t|| entry.checkId\n\t\t|| entry.searchId) {\n\t\treturn;\n\t}\n\tpushStateUpdate(entry);\n}\n\nvoid PostsSearch::applyQuery() {\n\tExpects(_query.has_value());\n\n\t_timer.cancel();\n\tif (_query->isEmpty()) {\n\t\trequestSearch(QString());\n\t} else {\n\t\trequestState(*_query);\n\t}\n}\n\nvoid PostsSearch::requestSearch(const QString &query) {\n\tauto &entry = _entries[query];\n\tif (entry.searchId || entry.loaded) {\n\t\treturn;\n\t}\n\n\tconst auto useStars = entry.allowedStars;\n\tentry.allowedStars = 0;\n\n\tusing Flag = MTPchannels_SearchPosts::Flag;\n\tentry.searchId = _api.request(MTPchannels_SearchPosts(\n\t\tMTP_flags(Flag::f_query\n\t\t\t| (useStars ? Flag::f_allow_paid_stars : Flag())),\n\t\tMTP_string(), // hashtag\n\t\tMTP_string(query),\n\t\tMTP_int(entry.offsetRate),\n\t\t(entry.offsetPeer ? entry.offsetPeer->input() : MTP_inputPeerEmpty()),\n\t\tMTP_int(entry.offsetId),\n\t\tMTP_int(kPerPage),\n\t\tMTP_long(useStars)\n\t)).done([=](const MTPmessages_Messages &result) {\n\t\tauto &entry = _entries[query];\n\t\tentry.searchId = 0;\n\n\t\tconst auto initial = !entry.offsetId;\n\t\tconst auto owner = &_session->data();\n\t\tconst auto processList = [&](const MTPVector<MTPMessage> &messages) {\n\t\t\tauto result = std::vector<not_null<HistoryItem*>>();\n\t\t\tfor (const auto &message : messages.v) {\n\t\t\t\tconst auto msgId = IdFromMessage(message);\n\t\t\t\tconst auto peerId = PeerFromMessage(message);\n\t\t\t\tconst auto lastDate = DateFromMessage(message);\n\t\t\t\tif (const auto peer = owner->peerLoaded(peerId)) {\n\t\t\t\t\tif (lastDate) {\n\t\t\t\t\t\tconst auto item = owner->addNewMessage(\n\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\tMessageFlags(),\n\t\t\t\t\t\t\tNewMessageType::Existing);\n\t\t\t\t\t\tresult.push_back(item);\n\t\t\t\t\t}\n\t\t\t\t\tentry.offsetPeer = peer;\n\t\t\t\t} else {\n\t\t\t\t\tLOG((\"API Error: a search results with not loaded peer %1\"\n\t\t\t\t\t\t).arg(peerId.value));\n\t\t\t\t}\n\t\t\t\tentry.offsetId = msgId;\n\t\t\t}\n\t\t\treturn result;\n\t\t};\n\t\tauto totalCount = 0;\n\t\tauto messages = result.match([&](const MTPDmessages_messages &data) {\n\t\t\towner->processUsers(data.vusers());\n\t\t\towner->processChats(data.vchats());\n\t\t\tentry.loaded = true;\n\t\t\tauto list = processList(data.vmessages());\n\t\t\ttotalCount = list.size();\n\t\t\treturn list;\n\t\t}, [&](const MTPDmessages_messagesSlice &data) {\n\t\t\towner->processUsers(data.vusers());\n\t\t\towner->processChats(data.vchats());\n\t\t\tauto list = processList(data.vmessages());\n\t\t\tconst auto nextRate = data.vnext_rate();\n\t\t\tconst auto rateUpdated = nextRate\n\t\t\t\t&& (nextRate->v != entry.offsetRate);\n\t\t\tconst auto finished = list.empty();\n\t\t\tif (rateUpdated) {\n\t\t\t\tentry.offsetRate = nextRate->v;\n\t\t\t}\n\t\t\tif (finished) {\n\t\t\t\tentry.loaded = true;\n\t\t\t}\n\t\t\ttotalCount = data.vcount().v;\n\t\t\tif (const auto flood = data.vsearch_flood()) {\n\t\t\t\tsetFloodStateFrom(flood->data());\n\t\t\t}\n\t\t\treturn list;\n\t\t}, [&](const MTPDmessages_channelMessages &data) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"received messages.channelMessages when no channel \"\n\t\t\t\t\"was passed! (PostsSearch::performSearch)\"));\n\t\t\towner->processUsers(data.vusers());\n\t\t\towner->processChats(data.vchats());\n\t\t\tauto list = processList(data.vmessages());\n\t\t\tif (list.empty()) {\n\t\t\t\tentry.loaded = true;\n\t\t\t}\n\t\t\ttotalCount = data.vcount().v;\n\t\t\treturn list;\n\t\t}, [&](const MTPDmessages_messagesNotModified &) {\n\t\t\tLOG((\"API Error: received messages.messagesNotModified! \"\n\t\t\t\t\"(PostsSearch::performSearch)\"));\n\t\t\tentry.loaded = true;\n\t\t\treturn std::vector<not_null<HistoryItem*>>();\n\t\t});\n\t\tif (initial) {\n\t\t\tentry.pages.clear();\n\t\t}\n\t\tentry.pages.push_back(std::move(messages));\n\t\tconst auto count = int(ranges::accumulate(\n\t\t\tentry.pages,\n\t\t\tsize_type(),\n\t\t\tranges::plus(),\n\t\t\t&std::vector<not_null<HistoryItem*>>::size));\n\t\tconst auto full = entry.loaded ? count : std::max(count, totalCount);\n\t\tentry.totalCount = full;\n\t\tif (_query == query) {\n\t\t\tpushStateUpdate(entry);\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tauto &entry = _entries[query];\n\t\tentry.searchId = 0;\n\n\t\tconst auto initial = !entry.offsetId;\n\t\tconst auto &type = error.type();\n\t\tif (initial && type.startsWith(u\"FLOOD_WAIT_\"_q)) {\n\t\t\trequestState(query);\n\t\t} else {\n\t\t\tentry.loaded = true;\n\t\t}\n\t}).handleFloodErrors().send();\n}\n\nvoid PostsSearch::setFloodStateFrom(const MTPDsearchPostsFlood &data) {\n\t_recheckTimer.cancel();\n\tconst auto left = std::max(data.vremains().v, 0);\n\tconst auto next = data.vwait_till().value_or_empty();\n\tif (!left && next > 0) {\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto delay = std::clamp(next - now, 1, 86401);\n\t\t_recheckTimer.callOnce(delay * crl::time(1000));\n\t}\n\t_floodState = PostsSearchIntroState{\n\t\t.freeSearchesPerDay = data.vtotal_daily().v,\n\t\t.freeSearchesLeft = left,\n\t\t.nextFreeSearchTime = next,\n\t\t.starsPerPaidSearch = uint32(data.vstars_amount().v),\n\t};\n}\n\nvoid PostsSearch::recheck() {\n\trequestState(*_query, true);\n}\n\nvoid PostsSearch::requestState(const QString &query, bool force) {\n\tauto &entry = _entries[query];\n\tif (force) {\n\t\t_api.request(base::take(entry.checkId)).cancel();\n\t} else if (entry.checkId || entry.loaded) {\n\t\treturn;\n\t}\n\n\tusing Flag = MTPchannels_CheckSearchPostsFlood::Flag;\n\tentry.checkId = _api.request(MTPchannels_CheckSearchPostsFlood(\n\t\tMTP_flags(Flag::f_query),\n\t\tMTP_string(query)\n\t)).done([=](const MTPSearchPostsFlood &result) {\n\t\tauto &entry = _entries[query];\n\t\tentry.checkId = 0;\n\n\t\tconst auto &data = result.data();\n\t\tsetFloodStateFrom(data);\n\t\tif (data.is_query_is_free()) {\n\t\t\tif (!entry.loaded) {\n\t\t\t\trequestSearch(query);\n\t\t\t}\n\t\t} else if (_query == query) {\n\t\t\tpushStateUpdate(entry);\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tauto &entry = _entries[query];\n\t\tentry.checkId = 0;\n\t\tentry.loaded = true;\n\t}).handleFloodErrors().send();\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_search_posts.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"dialogs/ui/posts_search_intro.h\"\n#include \"mtproto/sender.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Dialogs {\n\nstruct PostsSearchState {\n\tstd::optional<PostsSearchIntroState> intro;\n\tstd::vector<not_null<HistoryItem*>> page;\n\tint totalCount = 0;\n\tbool loading = false;\n};\n\nclass PostsSearch final {\npublic:\n\texplicit PostsSearch(not_null<Main::Session*> session);\n\n\t[[nodiscard]] rpl::producer<PostsSearchState> stateUpdates() const;\n\t[[nodiscard]] rpl::producer<PostsSearchState> pagesUpdates() const;\n\n\tvoid setQuery(const QString &query);\n\tint setAllowedStars(int stars);\n\tvoid requestMore();\n\nprivate:\n\tstruct Entry {\n\t\tstd::vector<std::vector<not_null<HistoryItem*>>> pages;\n\t\tint totalCount = 0;\n\t\tmtpRequestId searchId = 0;\n\t\tmtpRequestId checkId = 0;\n\t\tPeerData *offsetPeer = nullptr;\n\t\tMsgId offsetId = 0;\n\t\tint offsetRate = 0;\n\t\tint allowedStars = 0;\n\t\tmutable int pagesPushed = 0;\n\t\tbool loaded = false;\n\t};\n\n\tvoid recheck();\n\tvoid applyQuery();\n\tvoid requestSearch(const QString &query);\n\tvoid requestState(const QString &query, bool force = false);\n\tvoid setFloodStateFrom(const MTPDsearchPostsFlood &data);\n\tvoid pushStateUpdate(const Entry &entry);\n\tvoid maybePushPremiumUpdate();\n\n\tconst not_null<Main::Session*> _session;\n\n\tMTP::Sender _api;\n\n\tbase::Timer _timer;\n\tbase::Timer _recheckTimer;\n\tbase::flat_map<QString, Entry> _entries;\n\tstd::optional<QString> _queryExact;\n\tstd::optional<QString> _query;\n\tQString _queryPushed;\n\n\tstd::optional<PostsSearchIntroState> _floodState;\n\n\trpl::event_stream<PostsSearchState> _stateUpdates;\n\trpl::event_stream<PostsSearchState> _pagesUpdates;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/dialogs_search_tags.h\"\n\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_document.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_session.h\"\n#include \"history/view/reactions/history_view_reactions.h\"\n#include \"main/main_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Dialogs {\nnamespace {\n\n[[nodiscard]] QString ComposeText(const Data::Reaction &tag) {\n\tauto result = tag.title;\n\tif (!result.isEmpty() && tag.count > 0) {\n\t\tresult.append(' ');\n\t}\n\tif (tag.count > 0) {\n\t\tresult.append(QString::number(tag.count));\n\t}\n\treturn TextUtilities::SingleLine(result);\n}\n\n[[nodiscard]] ClickHandlerPtr MakePromoLink() {\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tShowPremiumPreviewBox(\n\t\t\t\tcontroller,\n\t\t\t\tPremiumFeature::TagsForMessages);\n\t\t}\n\t});\n}\n\n[[nodiscard]] Ui::Text::String FillAdditionalText(int width) {\n\tauto emoji = Ui::Text::IconEmoji(&st::dialogsSearchTagArrow);\n\tauto result = Ui::Text::String();\n\tconst auto attempt = [&](const auto &phrase) {\n\t\tresult.setMarkedText(\n\t\t\tst::dialogsSearchTagPromo,\n\t\t\tphrase(tr::now, lt_arrow, emoji, tr::marked),\n\t\t\tkMarkupTextOptions);\n\t\treturn result.maxWidth() < width;\n\t};\n\tif (attempt(tr::lng_add_tag_phrase_long)\n\t\t|| attempt(tr::lng_add_tag_phrase)) {\n\t\treturn result;\n\t}\n\treturn {};\n}\n\n} // namespace\n\nstruct SearchTags::Tag {\n\tData::ReactionId id;\n\tstd::unique_ptr<Ui::Text::CustomEmoji> custom;\n\tQString text;\n\tint textWidth = 0;\n\tmutable QImage image;\n\tQRect geometry;\n\tClickHandlerPtr link;\n\tbool selected = false;\n\tbool promo = false;\n};\n\nSearchTags::SearchTags(\n\tnot_null<Data::Session*> owner,\n\trpl::producer<std::vector<Data::Reaction>> tags,\n\tstd::vector<Data::ReactionId> selected)\n: _owner(owner)\n, _added(selected) {\n\trpl::combine(\n\t\tstd::move(tags),\n\t\tData::AmPremiumValue(&owner->session())\n\t) | rpl::on_next([=](\n\t\t\tconst std::vector<Data::Reaction> &list,\n\t\t\tbool premium) {\n\t\tfill(list, premium);\n\t}, _lifetime);\n\n\t// Mark the `selected` reactions as selected in `_tags`.\n\tfor (const auto &id : selected) {\n\t\tconst auto i = ranges::find(_tags, id, &Tag::id);\n\t\tif (i != end(_tags)) {\n\t\t\ti->selected = true;\n\t\t}\n\t}\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_normalBg = _selectedBg = QImage();\n\t}, _lifetime);\n}\n\nSearchTags::~SearchTags() = default;\n\nvoid SearchTags::fill(\n\t\tconst std::vector<Data::Reaction> &list,\n\t\tbool premium) {\n\tconst auto selected = collectSelected();\n\t_tags.clear();\n\t_tags.reserve(list.size());\n\tconst auto link = [&](Data::ReactionId id) {\n\t\treturn std::make_shared<GenericClickHandler>(crl::guard(this, [=](\n\t\t\t\tClickContext context) {\n\t\t\tif (!premium) {\n\t\t\t\tMakePromoLink()->onClick(context);\n\t\t\t\treturn;\n\t\t\t} else if (context.button == Qt::RightButton) {\n\t\t\t\t_menuRequests.fire_copy(id);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto i = ranges::find(_tags, id, &Tag::id);\n\t\t\tif (i != end(_tags)) {\n\t\t\t\tif (!i->selected && !base::IsShiftPressed()) {\n\t\t\t\t\tfor (auto &tag : _tags) {\n\t\t\t\t\t\ttag.selected = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ti->selected = !i->selected;\n\t\t\t\t_selectedChanges.fire({});\n\t\t\t}\n\t\t}));\n\t};\n\tconst auto push = [&](Data::ReactionId id, const QString &text) {\n\t\tconst auto customId = id.custom();\n\t\t_tags.push_back({\n\t\t\t.id = id,\n\t\t\t.custom = (customId\n\t\t\t\t? _owner->customEmojiManager().create(\n\t\t\t\t\tcustomId,\n\t\t\t\t\t[=] { _repaintRequests.fire({}); })\n\t\t\t\t: nullptr),\n\t\t\t.text = text,\n\t\t\t.textWidth = st::reactionInlineTagFont->width(text),\n\t\t\t.link = link(id),\n\t\t\t.selected = ranges::contains(selected, id),\n\t\t});\n\t\tif (!customId) {\n\t\t\t_owner->reactions().preloadReactionImageFor(id);\n\t\t}\n\t};\n\tif (!premium) {\n\t\tconst auto text = (list.empty() && _added.empty())\n\t\t\t? tr::lng_add_tag_button(tr::now)\n\t\t\t: tr::lng_unlock_tags(tr::now);\n\t\t_tags.push_back({\n\t\t\t.id = Data::ReactionId(),\n\t\t\t.text = text,\n\t\t\t.textWidth = st::reactionInlineTagFont->width(text),\n\t\t\t.link = MakePromoLink(),\n\t\t\t.promo = true,\n\t\t});\n\t}\n\tfor (const auto &reaction : list) {\n\t\tif (reaction.count > 0\n\t\t\t|| ranges::contains(_added, reaction.id)\n\t\t\t|| ranges::contains(selected, reaction.id)) {\n\t\t\tpush(reaction.id, ComposeText(reaction));\n\t\t}\n\t}\n\tfor (const auto &reaction : _added) {\n\t\tif (!ranges::contains(_tags, reaction, &Tag::id)) {\n\t\t\tpush(reaction, QString());\n\t\t}\n\t}\n\tif (_width > 0) {\n\t\tlayout();\n\t\t_repaintRequests.fire({});\n\t}\n}\n\nvoid SearchTags::layout() {\n\tExpects(_width > 0);\n\n\tif (_tags.empty()) {\n\t\t_additionalText = {};\n\t\t_height = 0;\n\t\treturn;\n\t}\n\tconst auto &bg = validateBg(false, false);\n\tconst auto skip = st::dialogsSearchTagSkip;\n\tconst auto size = bg.size() / bg.devicePixelRatio();\n\tconst auto xbase = size.width();\n\tconst auto ybase = size.height();\n\tauto x = 0;\n\tauto y = 0;\n\tfor (auto &tag : _tags) {\n\t\tconst auto width = xbase + (tag.promo\n\t\t\t? std::max(0, tag.textWidth - st::dialogsSearchTagPromoLeft - st::dialogsSearchTagPromoRight)\n\t\t\t: tag.textWidth);\n\t\tif (x > 0 && x + width > _width) {\n\t\t\tx = 0;\n\t\t\ty += ybase + skip.y();\n\t\t}\n\t\ttag.geometry = QRect(x, y, width, ybase);\n\t\tx += width + skip.x();\n\t}\n\t_height = y + ybase + st::dialogsSearchTagBottom;\n\tif (_tags.size() == 1 && _tags.front().promo) {\n\t\t_additionalLeft = x - skip.x() + st::dialogsSearchTagPromoSkip;\n\t\tconst auto additionalWidth = _width - _additionalLeft;\n\t\t_additionalText = FillAdditionalText(additionalWidth);\n\t} else {\n\t\t_additionalText = {};\n\t}\n}\n\nvoid SearchTags::resizeToWidth(int width) {\n\tif (_width == width || width <= 0) {\n\t\treturn;\n\t}\n\t_width = width;\n\tlayout();\n}\n\nint SearchTags::height() const {\n\treturn _height.current();\n}\n\nrpl::producer<int> SearchTags::heightValue() const {\n\treturn _height.value();\n}\n\nrpl::producer<> SearchTags::repaintRequests() const {\n\treturn _repaintRequests.events();\n}\n\nClickHandlerPtr SearchTags::lookupHandler(QPoint point) const {\n\tfor (const auto &tag : _tags) {\n\t\tif (tag.geometry.contains(point.x(), point.y())) {\n\t\t\treturn tag.link;\n\t\t} else if (tag.promo\n\t\t\t&& !_additionalText.isEmpty()\n\t\t\t&& tag.geometry.united(QRect(\n\t\t\t\t_additionalLeft,\n\t\t\t\ttag.geometry.y(),\n\t\t\t\t_additionalText.maxWidth(),\n\t\t\t\ttag.geometry.height())).contains(point.x(), point.y())) {\n\t\t\treturn tag.link;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nauto SearchTags::selectedChanges() const\n-> rpl::producer<std::vector<Data::ReactionId>> {\n\treturn _selectedChanges.events() | rpl::map([=] {\n\t\treturn collectSelected();\n\t});\n}\n\nvoid SearchTags::paintCustomFrame(\n\t\tQPainter &p,\n\t\tnot_null<Ui::Text::CustomEmoji*> emoji,\n\t\tQPoint innerTopLeft,\n\t\tcrl::time now,\n\t\tbool paused,\n\t\tconst QColor &textColor) const {\n\tif (_customCache.isNull()) {\n\t\tusing namespace Ui::Text;\n\t\tconst auto size = st::emojiSize;\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tconst auto adjusted = AdjustCustomEmojiSize(size);\n\t\t_customCache = QImage(\n\t\t\tQSize(adjusted, adjusted) * factor,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t_customCache.setDevicePixelRatio(factor);\n\t\t_customSkip = (size - adjusted) / 2;\n\t}\n\t_customCache.fill(Qt::transparent);\n\tauto q = QPainter(&_customCache);\n\temoji->paint(q, {\n\t\t.textColor = textColor,\n\t\t.now = now,\n\t\t.paused = paused || On(PowerSaving::kEmojiChat),\n\t});\n\tq.end();\n\t_customCache = Images::Round(\n\t\tstd::move(_customCache),\n\t\t(Images::Option::RoundLarge\n\t\t\t| Images::Option::RoundSkipTopRight\n\t\t\t| Images::Option::RoundSkipBottomRight));\n\n\tp.drawImage(\n\t\tinnerTopLeft + QPoint(_customSkip, _customSkip),\n\t\t_customCache);\n}\n\nrpl::producer<Data::ReactionId> SearchTags::menuRequests() const {\n\treturn _menuRequests.events();\n}\n\nvoid SearchTags::paint(\n\t\tPainter &p,\n\t\tQPoint position,\n\t\tcrl::time now,\n\t\tbool paused) const {\n\tconst auto size = st::reactionInlineSize;\n\tconst auto skip = (size - st::reactionInlineImage) / 2;\n\tconst auto padding = st::reactionInlinePadding;\n\tfor (const auto &tag : _tags) {\n\t\tconst auto geometry = tag.geometry.translated(position);\n\t\tpaintBackground(p, geometry, tag);\n\t\tpaintText(p, geometry, tag);\n\t\tif (!tag.custom && !tag.promo && tag.image.isNull()) {\n\t\t\ttag.image = _owner->reactions().resolveReactionImageFor(tag.id);\n\t\t}\n\t\tconst auto inner = geometry.marginsRemoved(padding);\n\t\tconst auto image = QRect(\n\t\t\tinner.topLeft() + QPoint(skip, skip),\n\t\t\tQSize(st::reactionInlineImage, st::reactionInlineImage));\n\t\tif (tag.promo) {\n\t\t\tst::dialogsSearchTagLocked.paintInCenter(p, QRect(\n\t\t\t\tinner.x(),\n\t\t\t\tinner.y() + skip,\n\t\t\t\tsize - st::dialogsSearchTagPromoLeft,\n\t\t\t\tst::reactionInlineImage));\n\t\t} else if (const auto custom = tag.custom.get()) {\n\t\t\tconst auto textFg = tag.selected\n\t\t\t\t? st::dialogsNameFgActive->c\n\t\t\t\t: st::dialogsNameFgOver->c;\n\t\t\tpaintCustomFrame(\n\t\t\t\tp,\n\t\t\t\tcustom,\n\t\t\t\tinner.topLeft(),\n\t\t\t\tnow,\n\t\t\t\tpaused,\n\t\t\t\ttextFg);\n\t\t} else if (!tag.image.isNull()) {\n\t\t\tp.drawImage(image.topLeft(), tag.image);\n\t\t}\n\t}\n\tpaintAdditionalText(p, position);\n}\n\nvoid SearchTags::paintAdditionalText(Painter &p, QPoint position) const {\n\tif (_additionalText.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto x = position.x() + _additionalLeft;\n\tconst auto tag = _tags.front().geometry;\n\tconst auto height = st::dialogsSearchTagPromo.font->height;\n\tconst auto y = position.y() + tag.y() + (tag.height() - height) / 2;\n\tp.setPen(st::windowSubTextFg);\n\t_additionalText.drawLeft(p, x, y, _width - _additionalLeft, _width);\n}\n\nvoid SearchTags::paintBackground(\n\t\tQPainter &p,\n\t\tQRect geometry,\n\t\tconst Tag &tag) const {\n\tconst auto &image = validateBg(tag.selected, tag.promo);\n\tconst auto ratio = int(image.devicePixelRatio());\n\tconst auto size = image.size() / ratio;\n\tif (const auto fill = geometry.width() - size.width(); fill > 0) {\n\t\tconst auto left = size.width() / 2;\n\t\tconst auto right = size.width() - left;\n\t\tconst auto x = geometry.x();\n\t\tconst auto y = geometry.y();\n\t\tp.drawImage(\n\t\t\tQRect(x, y, left, size.height()),\n\t\t\timage,\n\t\t\tQRect(QPoint(), QSize(left, size.height()) * ratio));\n\t\tp.fillRect(\n\t\t\tQRect(x + left, y, fill, size.height()),\n\t\t\tbgColor(tag.selected, tag.promo));\n\t\tp.drawImage(\n\t\t\tQRect(x + left + fill, y, right, size.height()),\n\t\t\timage,\n\t\t\tQRect(left * ratio, 0, right * ratio, size.height() * ratio));\n\t} else {\n\t\tp.drawImage(geometry.topLeft(), image);\n\t}\n}\n\nvoid SearchTags::paintText(\n\t\tQPainter &p,\n\t\tQRect geometry,\n\t\tconst Tag &tag) const {\n\tusing namespace HistoryView::Reactions;\n\n\tif (tag.text.isEmpty()) {\n\t\treturn;\n\t}\n\tp.setPen(tag.promo\n\t\t? st::lightButtonFgOver\n\t\t: tag.selected\n\t\t? st::dialogsTextFgActive\n\t\t: st::windowSubTextFg);\n\tp.setFont(st::reactionInlineTagFont);\n\tconst auto position = tag.promo\n\t\t? st::reactionInlineTagPromoPosition\n\t\t: st::reactionInlineTagNamePosition;\n\tconst auto x = geometry.x() + position.x();\n\tconst auto y = geometry.y() + position.y();\n\tp.drawText(x, y + st::reactionInlineTagFont->ascent, tag.text);\n}\n\nQColor SearchTags::bgColor(bool selected, bool promo) const {\n\treturn promo\n\t\t? st::lightButtonBgOver->c\n\t\t: selected\n\t\t? st::dialogsBgActive->c\n\t\t: st::dialogsBgOver->c;\n}\n\nconst QImage &SearchTags::validateBg(bool selected, bool promo) const {\n\tusing namespace HistoryView::Reactions;\n\tauto &image = promo ? _promoBg : selected ? _selectedBg : _normalBg;\n\tif (image.isNull()) {\n\t\tconst auto tagBg = bgColor(selected, promo);\n\t\tconst auto dotBg = st::transparent->c;\n\t\timage = InlineList::PrepareTagBg(tagBg, dotBg);\n\t}\n\treturn image;\n}\n\nstd::vector<Data::ReactionId> SearchTags::collectSelected() const {\n\treturn _tags | ranges::views::filter(\n\t\t&Tag::selected\n\t) | ranges::views::transform(\n\t\t&Tag::id\n\t) | ranges::to_vector;\n}\n\nrpl::lifetime &SearchTags::lifetime() {\n\treturn _lifetime;\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_search_tags.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n\nclass Painter;\n\nnamespace Data {\nclass Session;\nstruct Reaction;\nstruct ReactionId;\n} // namespace Data\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nnamespace Dialogs {\n\nclass SearchTags final : public base::has_weak_ptr {\npublic:\n\tSearchTags(\n\t\tnot_null<Data::Session*> owner,\n\t\trpl::producer<std::vector<Data::Reaction>> tags,\n\t\tstd::vector<Data::ReactionId> selected);\n\t~SearchTags();\n\n\tvoid resizeToWidth(int width);\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] rpl::producer<int> heightValue() const;\n\t[[nodiscard]] rpl::producer<> repaintRequests() const;\n\n\t[[nodiscard]] ClickHandlerPtr lookupHandler(QPoint point) const;\n\t[[nodiscard]] auto selectedChanges() const\n\t\t-> rpl::producer<std::vector<Data::ReactionId>>;\n\n\t[[nodiscard]] rpl::producer<Data::ReactionId> menuRequests() const;\n\n\tvoid paint(\n\t\tPainter &p,\n\t\tQPoint position,\n\t\tcrl::time now,\n\t\tbool paused) const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tstruct Tag;\n\n\tvoid fill(const std::vector<Data::Reaction> &list, bool premium);\n\tvoid paintCustomFrame(\n\t\tQPainter &p,\n\t\tnot_null<Ui::Text::CustomEmoji*> emoji,\n\t\tQPoint innerTopLeft,\n\t\tcrl::time now,\n\t\tbool paused,\n\t\tconst QColor &textColor) const;\n\tvoid layout();\n\t[[nodiscard]] std::vector<Data::ReactionId> collectSelected() const;\n\t[[nodiscard]] QColor bgColor(bool selected, bool promo) const;\n\t[[nodiscard]] const QImage &validateBg(bool selected, bool promo) const;\n\tvoid paintAdditionalText(Painter &p, QPoint position) const;\n\tvoid paintBackground(QPainter &p, QRect geometry, const Tag &tag) const;\n\tvoid paintText(QPainter &p, QRect geometry, const Tag &tag) const;\n\n\tconst not_null<Data::Session*> _owner;\n\tstd::vector<Data::ReactionId> _added;\n\tstd::vector<Tag> _tags;\n\tUi::Text::String _additionalText;\n\trpl::event_stream<> _selectedChanges;\n\trpl::event_stream<> _repaintRequests;\n\trpl::event_stream<Data::ReactionId> _menuRequests;\n\tmutable QImage _normalBg;\n\tmutable QImage _selectedBg;\n\tmutable QImage _promoBg;\n\tmutable QImage _customCache;\n\tmutable int _customSkip = 0;\n\trpl::variable<int> _height;\n\tint _width = 0;\n\tint _additionalLeft = 0;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_three_state_icon.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"styles/style_dialogs.h\"\n\nnamespace Dialogs {\n\n[[nodiscard]] inline const style::icon &ThreeStateIcon(\n\t\tconst style::ThreeStateIcon &icons,\n\t\tbool active,\n\t\tbool over) {\n\treturn active ? icons.active : over ? icons.over : icons.icon;\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/dialogs_top_bar_suggestion.h\"\n\n#include \"api/api_authorizations.h\"\n#include \"api/api_credits.h\"\n#include \"api/api_peer_photo.h\"\n#include \"api/api_premium.h\"\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"boxes/star_gift_box.h\" // ShowStarGiftBox.\n#include \"boxes/star_gift_auction_box.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\"\n#include \"data/components/gift_auctions.h\"\n#include \"data/components/promo_suggestions.h\"\n#include \"data/data_birthday.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_peer_values.h\" // Data::AmPremiumValue.\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"dialogs/ui/dialogs_top_bar_suggestion_content.h\"\n#include \"history/view/history_view_group_call_bar.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_active_sessions.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Dialogs {\nnamespace {\n\n[[nodiscard]] not_null<Window::SessionController*> FindSessionController(\n\t\tnot_null<Ui::RpWidget*> widget) {\n\tconst auto window = Core::App().findWindow(widget);\n\tAssert(window != nullptr);\n\treturn window->sessionController();\n}\n\n[[nodiscard]] QString FormatAuthInfo(const Data::UnreviewedAuth &auth) {\n\tconst auto location = auth.location.isEmpty()\n\t\t? QString()\n\t\t: \"\\U0001F30D \" + auth.location;\n\tconst auto device = auth.device.isEmpty()\n\t\t? QString()\n\t\t: \"\\U0001F4F1 \" + auth.device;\n\n\tif (!location.isEmpty() && !device.isEmpty()) {\n\t\treturn location + \" (\" + device + \")\";\n\t} else if (!location.isEmpty()) {\n\t\treturn location;\n\t} else if (!device.isEmpty()) {\n\t\treturn device;\n\t}\n\treturn QString();\n}\n\nvoid ShowAuthToast(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Main::Session*> session,\n\t\tconst std::vector<Data::UnreviewedAuth> &list,\n\t\tbool confirmed) {\n\tif (confirmed) {\n\t\tauto text = tr::lng_unconfirmed_auth_confirmed_message(\n\t\t\ttr::now,\n\t\t\tlt_link,\n\t\t\ttr::link(tr::lng_settings_sessions_title(tr::now)),\n\t\t\ttr::rich);\n\t\tauto filter = [=](\n\t\t\t\tClickHandlerPtr handler,\n\t\t\t\tQt::MouseButton button) {\n\t\t\tif (const auto controller = FindSessionController(parent)) {\n\t\t\t\tsession->api().authorizations().reload();\n\t\t\t\tcontroller->showSettings(Settings::SessionsId());\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\t\tUi::Toast::Show(parent->window(), Ui::Toast::Config{\n\t\t\t.title = tr::lng_unconfirmed_auth_confirmed(tr::now),\n\t\t\t.text = std::move(text),\n\t\t\t.filter = std::move(filter),\n\t\t\t.duration = crl::time(5000),\n\t\t});\n\t} else {\n\t\tauto messageText = QString();\n\t\tif (list.size() == 1) {\n\t\t\tmessageText = tr::lng_unconfirmed_auth_denied_single(\n\t\t\t\ttr::now,\n\t\t\t\tlt_country,\n\t\t\t\tFormatAuthInfo(list.front()));\n\t\t} else {\n\t\t\tauto authList = QString('\\n');\n\t\t\tfor (auto i = 0; i < std::min(int(list.size()), 10); ++i) {\n\t\t\t\tconst auto info = FormatAuthInfo(list[i]);\n\t\t\t\tif (!info.isEmpty()) {\n\t\t\t\t\tauthList += \"• \" + info + \"\\n\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tmessageText = tr::lng_unconfirmed_auth_denied_multiple(\n\t\t\t\ttr::now,\n\t\t\t\tlt_country,\n\t\t\t\tauthList);\n\t\t}\n\t\tif (const auto controller = FindSessionController(parent)) {\n\t\t\tconst auto count = float64(list.size());\n\t\t\tcontroller->show(Box(ShowAuthDeniedBox, count, messageText));\n\t\t}\n\t}\n}\n\nconstexpr auto kSugSetBirthday = \"BIRTHDAY_SETUP\"_cs;\nconstexpr auto kSugBirthdayContacts = \"BIRTHDAY_CONTACTS_TODAY\"_cs;\nconstexpr auto kSugPremiumAnnual = \"PREMIUM_ANNUAL\"_cs;\nconstexpr auto kSugPremiumUpgrade = \"PREMIUM_UPGRADE\"_cs;\nconstexpr auto kSugPremiumRestore = \"PREMIUM_RESTORE\"_cs;\nconstexpr auto kSugPremiumGrace = \"PREMIUM_GRACE\"_cs;\nconstexpr auto kSugSetUserpic = \"USERPIC_SETUP\"_cs;\nconstexpr auto kSugLowCreditsSubs = \"STARS_SUBSCRIPTION_LOW_BALANCE\"_cs;\n\n\n} // namespace\n\nrpl::producer<Ui::SlideWrap<Ui::RpWidget>*> TopBarSuggestionValue(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<bool> outerWrapToggleValue) {\n\treturn [=, outerWrapToggleValue = rpl::duplicate(outerWrapToggleValue)](\n\t\t\tauto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tstruct Toggle {\n\t\t\tbool value = false;\n\t\t\tanim::type type;\n\t\t};\n\n\t\tstruct State {\n\t\t\tTopBarSuggestionContent *content = nullptr;\n\t\t\tUi::SlideWrap<Ui::VerticalLayout> *unconfirmedWarning = nullptr;\n\t\t\tbase::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> wrap;\n\t\t\trpl::variable<int> leftPadding;\n\t\t\trpl::variable<Toggle> desiredWrapToggle;\n\t\t\trpl::variable<bool> outerWrapToggle;\n\t\t\trpl::lifetime birthdayLifetime;\n\t\t\trpl::lifetime premiumLifetime;\n\t\t\trpl::lifetime userpicLifetime;\n\t\t\trpl::lifetime giftsLifetime;\n\t\t\trpl::lifetime creditsLifetime;\n\t\t\trpl::lifetime auctionsLifetime;\n\t\t\tstd::unique_ptr<Api::CreditsHistory> creditsHistory;\n\t\t};\n\n\t\tconst auto state = lifetime.make_state<State>();\n\t\tstate->outerWrapToggle = rpl::duplicate(outerWrapToggleValue);\n\t\tstate->leftPadding = rpl::variable<int>(\n\t\t\trpl::single(st::dialogsTopBarLeftPadding));\n\t\tconst auto ensureContent = [=] {\n\t\t\tif (!state->content) {\n\t\t\t\tconst auto window = FindSessionController(parent);\n\t\t\t\tstate->content = Ui::CreateChild<TopBarSuggestionContent>(\n\t\t\t\t\tparent,\n\t\t\t\t\t[=] { return window->isGifPausedAtLeastFor(\n\t\t\t\t\t\tWindow::GifPauseReason::Layer); });\n\t\t\t\trpl::combine(\n\t\t\t\t\tparent->widthValue(),\n\t\t\t\t\tstate->content->desiredHeightValue()\n\t\t\t\t) | rpl::on_next([=](int width, int height) {\n\t\t\t\t\tstate->content->resize(width, height);\n\t\t\t\t}, state->content->lifetime());\n\t\t\t}\n\t\t};\n\t\tconst auto ensureWrap = [=](not_null<Ui::RpWidget*> child) {\n\t\t\tif (!state->wrap) {\n\t\t\t\tstate->wrap\n\t\t\t\t\t= base::make_unique_q<Ui::SlideWrap<Ui::RpWidget>>(\n\t\t\t\t\t\tparent,\n\t\t\t\t\t\tobject_ptr<Ui::RpWidget>::fromRaw(child));\n\t\t\t\tstate->desiredWrapToggle.force_assign(\n\t\t\t\t\tToggle{ false, anim::type::instant });\n\t\t\t}\n\t\t};\n\n\t\tconst auto setLeftPaddingRelativeTo = [=](\n\t\t\t\tnot_null<TopBarSuggestionContent*> content,\n\t\t\t\tnot_null<Ui::RpWidget*> relativeTo) {\n\t\t\tcontent->setLeftPadding(state->leftPadding.value(\n\t\t\t\t) | rpl::map([w = relativeTo->width()](int padding) {\n\t\t\t\t\treturn w + padding * 2;\n\t\t\t\t}));\n\t\t};\n\n\t\tconst auto processCurrentSuggestion = [=](auto repeat) -> void {\n\t\t\tstate->birthdayLifetime.destroy();\n\t\t\tstate->premiumLifetime.destroy();\n\t\t\tstate->userpicLifetime.destroy();\n\t\t\tstate->giftsLifetime.destroy();\n\t\t\tstate->creditsLifetime.destroy();\n\t\t\tstate->auctionsLifetime.destroy();\n\n\t\t\tif (!session->api().authorizations().unreviewed().empty()) {\n\t\t\t\tstate->content = nullptr;\n\t\t\t\tstate->wrap = nullptr;\n\t\t\t\tconst auto &list\n\t\t\t\t\t= session->api().authorizations().unreviewed();\n\t\t\t\tconst auto hashes = ranges::views::all(\n\t\t\t\t\tlist\n\t\t\t\t) | ranges::views::transform([](const auto &auth) {\n\t\t\t\t\treturn auth.hash;\n\t\t\t\t}) | ranges::to_vector;\n\n\t\t\t\tconst auto content = CreateUnconfirmedAuthContent(\n\t\t\t\t\tparent,\n\t\t\t\t\tlist,\n\t\t\t\t\t[=](bool confirmed) {\n\t\t\t\t\t\tShowAuthToast(parent, session, list, confirmed);\n\t\t\t\t\t\tsession->api().authorizations().review(\n\t\t\t\t\t\t\thashes,\n\t\t\t\t\t\t\tconfirmed);\n\t\t\t\t\t});\n\t\t\t\tensureWrap(content);\n\t\t\t\tconst auto wasUnconfirmedWarning = state->unconfirmedWarning;\n\t\t\t\tstate->unconfirmedWarning = content;\n\t\t\t\tstate->desiredWrapToggle.force_assign(Toggle{\n\t\t\t\t\ttrue,\n\t\t\t\t\t(state->unconfirmedWarning != wasUnconfirmedWarning)\n\t\t\t\t\t\t? anim::type::instant\n\t\t\t\t\t\t: anim::type::normal,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tif (state->unconfirmedWarning) {\n\t\t\t\t\tstate->unconfirmedWarning = nullptr;\n\t\t\t\t\tstate->wrap = nullptr;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tensureContent();\n\t\t\tensureWrap(state->content);\n\t\t\tconst auto content = state->content;\n\t\t\tconst auto wrap = state->wrap.get();\n\t\t\tusing RightIcon = TopBarSuggestionContent::RightIcon;\n\t\t\tconst auto promo = &session->promoSuggestions();\n\t\t\tconst auto auctions = &session->giftAuctions();\n\t\t\tif (auctions->hasActive()) {\n\t\t\t\tusing namespace Data;\n\t\t\t\tstruct Button {\n\t\t\t\t\trpl::variable<TextWithEntities> text;\n\t\t\t\t\tFn<void()> callback;\n\t\t\t\t\tbase::has_weak_ptr guard;\n\t\t\t\t};\n\t\t\t\tauto &lifetime = state->auctionsLifetime;\n\t\t\t\tconst auto button = lifetime.template make_state<Button>();\n\t\t\t\tconst auto window = FindSessionController(parent);\n\t\t\t\tauctions->active(\n\t\t\t\t) | rpl::on_next([=](ActiveAuctions &&active) {\n\t\t\t\t\tconst auto empty = active.list.empty();\n\t\t\t\t\tstate->desiredWrapToggle.force_assign(\n\t\t\t\t\t\tToggle{ !empty, anim::type::normal });\n\t\t\t\t\tif (empty) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tauto text = Ui::ActiveAuctionsState(active);\n\t\t\t\t\tconst auto textColorOverride = text.someOutbid\n\t\t\t\t\t\t? st::attentionButtonFg->c\n\t\t\t\t\t\t: std::optional<QColor>();\n\t\t\t\t\tcontent->setContent(\n\t\t\t\t\t\tUi::ActiveAuctionsTitle(active),\n\t\t\t\t\t\tstd::move(text.text),\n\t\t\t\t\t\tCore::TextContext({ .session = session }),\n\t\t\t\t\t\ttextColorOverride);\n\t\t\t\t\tbutton->text = Ui::ActiveAuctionsButton(active);\n\t\t\t\t\tbutton->callback = Ui::ActiveAuctionsCallback(\n\t\t\t\t\t\twindow,\n\t\t\t\t\t\tactive);\n\t\t\t\t}, state->auctionsLifetime);\n\t\t\t\tconst auto callback = crl::guard(&button->guard, [=] {\n\t\t\t\t\tbutton->callback();\n\t\t\t\t});\n\t\t\t\tcontent->setRightButton(button->text.value(), callback);\n\t\t\t\tcontent->setClickedCallback(callback);\n\t\t\t\tcontent->setLeftPadding(state->leftPadding.value());\n\t\t\t\tstate->desiredWrapToggle.force_assign(\n\t\t\t\t\tToggle{ true, anim::type::normal });\n\t\t\t\treturn;\n\t\t\t} else if (const auto custom = promo->custom()) {\n\t\t\t\tcontent->setRightIcon(RightIcon::Close);\n\t\t\t\tcontent->setLeftPadding(state->leftPadding.value());\n\t\t\t\tcontent->setClickedCallback([=] {\n\t\t\t\t\tconst auto controller = FindSessionController(parent);\n\t\t\t\t\tUrlClickHandler::Open(\n\t\t\t\t\t\tcustom->url,\n\t\t\t\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t\t\t\t.sessionWindow = base::make_weak(controller),\n\t\t\t\t\t\t}));\n\t\t\t\t});\n\t\t\t\tcontent->setHideCallback([=] {\n\t\t\t\t\tpromo->dismiss(custom->suggestion);\n\t\t\t\t\trepeat(repeat);\n\t\t\t\t});\n\n\t\t\t\tcontent->setContent(\n\t\t\t\t\tcustom->title,\n\t\t\t\t\tcustom->description,\n\t\t\t\t\tCore::TextContext({ .session = session }));\n\t\t\t\tstate->desiredWrapToggle.force_assign(\n\t\t\t\t\tToggle{ true, anim::type::normal });\n\t\t\t\treturn;\n\t\t\t} else if (session->premiumCanBuy()\n\t\t\t\t&& promo->current(kSugPremiumGrace.utf8())) {\n\t\t\t\tcontent->setRightIcon(RightIcon::Close);\n\t\t\t\tcontent->setLeftPadding(state->leftPadding.value());\n\t\t\t\tcontent->setClickedCallback([=] {\n\t\t\t\t\tconst auto controller = FindSessionController(parent);\n\t\t\t\t\tUrlClickHandler::Open(\n\t\t\t\t\t\tu\"https://t.me/premiumbot?start=status\"_q,\n\t\t\t\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t\t\t\t.sessionWindow = base::make_weak(controller),\n\t\t\t\t\t\t}));\n\t\t\t\t});\n\t\t\t\tcontent->setHideCallback([=] {\n\t\t\t\t\tpromo->dismiss(kSugPremiumGrace.utf8());\n\t\t\t\t\trepeat(repeat);\n\t\t\t\t});\n\t\t\t\tcontent->setContent(\n\t\t\t\t\ttr::lng_dialogs_suggestions_premium_grace_title(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::bold),\n\t\t\t\t\ttr::lng_dialogs_suggestions_premium_grace_about(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tTextWithEntities::Simple));\n\t\t\t\tstate->desiredWrapToggle.force_assign(\n\t\t\t\t\tToggle{ true, anim::type::normal });\n\t\t\t\treturn;\n\t\t\t} else if (session->premiumCanBuy()\n\t\t\t\t&& promo->current(kSugLowCreditsSubs.utf8())) {\n\t\t\t\tstate->creditsHistory = std::make_unique<Api::CreditsHistory>(\n\t\t\t\t\tsession->user(),\n\t\t\t\t\tfalse,\n\t\t\t\t\tfalse);\n\t\t\t\tconst auto show = [=](\n\t\t\t\t\t\tconst QString &peers,\n\t\t\t\t\t\tuint64 needed,\n\t\t\t\t\t\tuint64 whole) {\n\t\t\t\t\tif (whole > needed) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tcontent->setRightIcon(RightIcon::Close);\n\t\t\t\t\tcontent->setLeftPadding(state->leftPadding.value());\n\t\t\t\t\tcontent->setClickedCallback([=] {\n\t\t\t\t\t\tconst auto controller = FindSessionController(parent);\n\t\t\t\t\t\tcontroller->uiShow()->show(Box(\n\t\t\t\t\t\t\tSettings::SmallBalanceBox,\n\t\t\t\t\t\t\tcontroller->uiShow(),\n\t\t\t\t\t\t\tneeded,\n\t\t\t\t\t\t\tSettings::SmallBalanceSubscription{ peers },\n\t\t\t\t\t\t\t[=] {\n\t\t\t\t\t\t\t\tpromo->dismiss(kSugLowCreditsSubs.utf8());\n\t\t\t\t\t\t\t\trepeat(repeat);\n\t\t\t\t\t\t\t}));\n\t\t\t\t\t});\n\t\t\t\t\tcontent->setHideCallback([=] {\n\t\t\t\t\t\tpromo->dismiss(kSugLowCreditsSubs.utf8());\n\t\t\t\t\t\trepeat(repeat);\n\t\t\t\t\t});\n\n\t\t\t\t\tcontent->setContent(\n\t\t\t\t\t\ttr::lng_dialogs_suggestions_credits_sub_low_title(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\tfloat64(needed - whole),\n\t\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\t\tUi::MakeCreditsIconEntity(),\n\t\t\t\t\t\t\tlt_channels,\n\t\t\t\t\t\t\t{ peers },\n\t\t\t\t\t\t\ttr::bold),\n\t\t\t\t\t\ttr::lng_dialogs_suggestions_credits_sub_low_about(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tTextWithEntities::Simple),\n\t\t\t\t\t\tUi::MakeCreditsIconContext(\n\t\t\t\t\t\t\tcontent->contentTitleSt().font->height,\n\t\t\t\t\t\t\t1));\n\t\t\t\t\tstate->desiredWrapToggle.force_assign(\n\t\t\t\t\t\tToggle{ true, anim::type::normal });\n\t\t\t\t};\n\t\t\t\tsession->credits().load();\n\t\t\t\tstate->creditsLifetime.destroy();\n\t\t\t\tsession->credits().balanceValue() | rpl::on_next([=] {\n\t\t\t\t\tstate->creditsLifetime.destroy();\n\t\t\t\t\tstate->creditsHistory->requestSubscriptions(\n\t\t\t\t\t\tData::CreditsStatusSlice::OffsetToken(),\n\t\t\t\t\t\t[=](Data::CreditsStatusSlice slice) {\n\t\t\t\t\t\t\tstate->creditsHistory = nullptr;\n\t\t\t\t\t\t\tauto peers = QStringList();\n\t\t\t\t\t\t\tauto credits = uint64(0);\n\t\t\t\t\t\t\tfor (const auto &entry : slice.subscriptions) {\n\t\t\t\t\t\t\t\tif (entry.barePeerId) {\n\t\t\t\t\t\t\t\t\tconst auto peer = session->data().peer(\n\t\t\t\t\t\t\t\t\t\tPeerId(entry.barePeerId));\n\t\t\t\t\t\t\t\t\tpeers.append(peer->name());\n\t\t\t\t\t\t\t\t\tcredits += entry.subscription.credits;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tshow(\n\t\t\t\t\t\t\t\tpeers.join(\", \"),\n\t\t\t\t\t\t\t\tcredits,\n\t\t\t\t\t\t\t\tsession->credits().balance().whole());\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttrue);\n\t\t\t\t}, state->creditsLifetime);\n\n\t\t\t\treturn;\n\t\t\t} else if (session->premiumCanBuy()\n\t\t\t\t&& promo->current(kSugBirthdayContacts.utf8())) {\n\t\t\t\tpromo->requestContactBirthdays(crl::guard(content, [=] {\n\t\t\t\t\tconst auto users = promo->knownBirthdaysToday().value_or(\n\t\t\t\t\t\tstd::vector<UserId>());\n\t\t\t\t\tif (users.empty()) {\n\t\t\t\t\t\trepeat(repeat);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst auto controller = FindSessionController(parent);\n\t\t\t\t\tconst auto isSingle = users.size() == 1;\n\t\t\t\t\tconst auto first = session->data().user(users.front());\n\t\t\t\t\tcontent->setRightIcon(RightIcon::Close);\n\t\t\t\t\tcontent->setClickedCallback([=] {\n\t\t\t\t\t\tif (isSingle) {\n\t\t\t\t\t\t\tUi::ShowStarGiftBox(controller, first);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tUi::ChooseStarGiftRecipient(controller);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t\tcontent->setHideCallback([=] {\n\t\t\t\t\t\tpromo->dismiss(kSugBirthdayContacts.utf8());\n\t\t\t\t\t\tcontroller->showToast(\n\t\t\t\t\t\t\ttr::lng_dialogs_suggestions_birthday_contact_dismiss(\n\t\t\t\t\t\t\t\ttr::now));\n\t\t\t\t\t\trepeat(repeat);\n\t\t\t\t\t});\n\t\t\t\t\tauto title = isSingle\n\t\t\t\t\t\t? tr::lng_dialogs_suggestions_birthday_contact_title(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_text,\n\t\t\t\t\t\t\t{ first->shortName() },\n\t\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t\t: tr::lng_dialogs_suggestions_birthday_contacts_title(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\tusers.size(),\n\t\t\t\t\t\t\ttr::rich);\n\t\t\t\t\tauto text = isSingle\n\t\t\t\t\t\t? tr::lng_dialogs_suggestions_birthday_contact_about(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tTextWithEntities::Simple)\n\t\t\t\t\t\t: tr::lng_dialogs_suggestions_birthday_contacts_about(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tTextWithEntities::Simple);\n\t\t\t\t\tcontent->setContent(std::move(title), std::move(text));\n\t\t\t\t\tstate->giftsLifetime.destroy();\n\t\t\t\t\tif (!isSingle) {\n\t\t\t\t\t\tstruct UserViews {\n\t\t\t\t\t\t\tstd::vector<HistoryView::UserpicInRow> inRow;\n\t\t\t\t\t\t\tQImage userpics;\n\t\t\t\t\t\t\tbase::unique_qptr<Ui::RpWidget> widget;\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst auto s\n\t\t\t\t\t\t\t= state->giftsLifetime.template make_state<\n\t\t\t\t\t\t\t\tUserViews>();\n\t\t\t\t\t\ts->widget = base::make_unique_q<Ui::RpWidget>(\n\t\t\t\t\t\t\tcontent);\n\t\t\t\t\t\tconst auto widget = s->widget.get();\n\t\t\t\t\t\twidget->setAttribute(\n\t\t\t\t\t\t\tQt::WA_TransparentForMouseEvents);\n\t\t\t\t\t\tcontent->sizeValue() | rpl::filter_size(\n\t\t\t\t\t\t) | rpl::on_next([=](const QSize &size) {\n\t\t\t\t\t\t\twidget->resize(size);\n\t\t\t\t\t\t\twidget->show();\n\t\t\t\t\t\t\twidget->raise();\n\t\t\t\t\t\t}, widget->lifetime());\n\t\t\t\t\t\tfor (const auto &id : users) {\n\t\t\t\t\t\t\tif (s->inRow.size() >= 3) {\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (const auto user = session->data().user(id)) {\n\t\t\t\t\t\t\t\ts->inRow.push_back({ .peer = user });\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\twidget->paintRequest() | rpl::on_next([=] {\n\t\t\t\t\t\t\tauto p = QPainter(widget);\n\t\t\t\t\t\t\tif (HistoryView::NeedRegenerateUserpics(\n\t\t\t\t\t\t\t\t\ts->userpics,\n\t\t\t\t\t\t\t\t\ts->inRow)) {\n\t\t\t\t\t\t\t\tconst auto &st = st::historyCommentsUserpics;\n\t\t\t\t\t\t\t\tHistoryView::GenerateUserpicsInRow(\n\t\t\t\t\t\t\t\t\ts->userpics,\n\t\t\t\t\t\t\t\t\ts->inRow,\n\t\t\t\t\t\t\t\t\tst,\n\t\t\t\t\t\t\t\t\t3);\n\t\t\t\t\t\t\t\tconst auto v = int(users.size() * st.size\n\t\t\t\t\t\t\t\t\t- st.shift);\n\t\t\t\t\t\t\t\tcontent->setLeftPadding(\n\t\t\t\t\t\t\t\t\tstate->leftPadding.value(\n\t\t\t\t\t\t\t\t\t) | rpl::map([v](int padding) {\n\t\t\t\t\t\t\t\t\t\treturn padding * 2 + v;\n\t\t\t\t\t\t\t\t\t}));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tp.drawImage(\n\t\t\t\t\t\t\t\tstate->leftPadding.current(),\n\t\t\t\t\t\t\t\t(widget->height()\n\t\t\t\t\t\t\t\t\t- (s->userpics.height()\n\t\t\t\t\t\t\t\t\t\t/ style::DevicePixelRatio())) / 2,\n\t\t\t\t\t\t\t\ts->userpics);\n\t\t\t\t\t\t}, widget->lifetime());\n\t\t\t\t\t} else {\n\t\t\t\t\t\tusing Ptr = base::unique_qptr<Ui::UserpicButton>;\n\t\t\t\t\t\tconst auto ptr\n\t\t\t\t\t\t\t= state->giftsLifetime.template make_state<Ptr>(\n\t\t\t\t\t\t\t\tbase::make_unique_q<Ui::UserpicButton>(\n\t\t\t\t\t\t\t\t\tcontent,\n\t\t\t\t\t\t\t\t\tfirst,\n\t\t\t\t\t\t\t\t\tst::uploadUserpicButton));\n\t\t\t\t\t\tconst auto fake = ptr->get();\n\t\t\t\t\t\tfake->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\t\t\t\trpl::combine(\n\t\t\t\t\t\t\tstate->leftPadding.value(),\n\t\t\t\t\t\t\tcontent->sizeValue() | rpl::filter_size()\n\t\t\t\t\t\t) | rpl::on_next([=](int p, const QSize &s) {\n\t\t\t\t\t\t\tfake->raise();\n\t\t\t\t\t\t\tfake->show();\n\t\t\t\t\t\t\tfake->moveToLeft(\n\t\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\t\t(s.height() - fake->height()) / 2);\n\t\t\t\t\t\t}, fake->lifetime());\n\t\t\t\t\t\tsetLeftPaddingRelativeTo(content, fake);\n\t\t\t\t\t}\n\n\t\t\t\t\tstate->desiredWrapToggle.force_assign(\n\t\t\t\t\t\tToggle{ true, anim::type::normal });\n\t\t\t\t}));\n\t\t\t\treturn;\n\t\t\t} else if (promo->current(kSugSetBirthday.utf8())\n\t\t\t\t&& !Data::IsBirthdayToday(session->user()->birthday())) {\n\t\t\t\tcontent->setRightIcon(RightIcon::Close);\n\t\t\t\tcontent->setLeftPadding(state->leftPadding.value());\n\t\t\t\tcontent->setClickedCallback([=] {\n\t\t\t\t\tconst auto controller = FindSessionController(parent);\n\t\t\t\t\tCore::App().openInternalUrl(\n\t\t\t\t\t\tu\"internal:edit_birthday:add_privacy\"_q,\n\t\t\t\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t\t\t\t.sessionWindow = base::make_weak(controller),\n\t\t\t\t\t\t}));\n\n\t\t\t\t\tstate->birthdayLifetime = Info::Profile::BirthdayValue(\n\t\t\t\t\t\tsession->user()\n\t\t\t\t\t) | rpl::map(\n\t\t\t\t\t\tData::IsBirthdayTodayValue\n\t\t\t\t\t) | rpl::flatten_latest(\n\t\t\t\t\t) | rpl::distinct_until_changed(\n\t\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\t\trepeat(repeat);\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t\tcontent->setHideCallback([=] {\n\t\t\t\t\tpromo->dismiss(kSugSetBirthday.utf8());\n\t\t\t\t\trepeat(repeat);\n\t\t\t\t});\n\t\t\t\tcontent->setContent(\n\t\t\t\t\ttr::lng_dialogs_suggestions_birthday_title(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::bold),\n\t\t\t\t\ttr::lng_dialogs_suggestions_birthday_about(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tTextWithEntities::Simple));\n\t\t\t\tstate->desiredWrapToggle.force_assign(\n\t\t\t\t\tToggle{ true, anim::type::normal });\n\t\t\t\treturn;\n\t\t\t} else if (session->premiumPossible() && !session->premium()) {\n\t\t\t\tconst auto isPremiumAnnual = promo->current(\n\t\t\t\t\tkSugPremiumAnnual.utf8());\n\t\t\t\tconst auto isPremiumRestore = !isPremiumAnnual\n\t\t\t\t\t&& promo->current(kSugPremiumRestore.utf8());\n\t\t\t\tconst auto isPremiumUpgrade = !isPremiumAnnual\n\t\t\t\t\t&& !isPremiumRestore\n\t\t\t\t\t&& promo->current(kSugPremiumUpgrade.utf8());\n\t\t\t\tconst auto set = [=](QString discount) {\n\t\t\t\t\tconstexpr auto kMinus = QChar(0x2212);\n\t\t\t\t\tconst auto &title = isPremiumAnnual\n\t\t\t\t\t\t? tr::lng_dialogs_suggestions_premium_annual_title\n\t\t\t\t\t\t: isPremiumRestore\n\t\t\t\t\t\t? tr::lng_dialogs_suggestions_premium_restore_title\n\t\t\t\t\t\t: tr::lng_dialogs_suggestions_premium_upgrade_title;\n\t\t\t\t\tconst auto &description = isPremiumAnnual\n\t\t\t\t\t\t? tr::lng_dialogs_suggestions_premium_annual_about\n\t\t\t\t\t\t: isPremiumRestore\n\t\t\t\t\t\t? tr::lng_dialogs_suggestions_premium_restore_about\n\t\t\t\t\t\t: tr::lng_dialogs_suggestions_premium_upgrade_about;\n\t\t\t\t\tcontent->setContent(\n\t\t\t\t\t\ttitle(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_text,\n\t\t\t\t\t\t\t{ discount.replace(kMinus, QChar()) },\n\t\t\t\t\t\t\ttr::bold),\n\t\t\t\t\t\tdescription(tr::now, TextWithEntities::Simple));\n\t\t\t\t\tcontent->setClickedCallback([=] {\n\t\t\t\t\t\tconst auto controller = FindSessionController(parent);\n\t\t\t\t\t\tSettings::ShowPremium(controller, \"dialogs_hint\");\n\t\t\t\t\t\tpromo->dismiss(isPremiumAnnual\n\t\t\t\t\t\t\t? kSugPremiumAnnual.utf8()\n\t\t\t\t\t\t\t: isPremiumRestore\n\t\t\t\t\t\t\t? kSugPremiumRestore.utf8()\n\t\t\t\t\t\t\t: kSugPremiumUpgrade.utf8());\n\t\t\t\t\t\trepeat(repeat);\n\t\t\t\t\t});\n\t\t\t\t\tstate->desiredWrapToggle.force_assign(\n\t\t\t\t\t\tToggle{ true, anim::type::normal });\n\t\t\t\t};\n\t\t\t\tif (isPremiumAnnual || isPremiumRestore || isPremiumUpgrade) {\n\t\t\t\t\tcontent->setRightIcon(RightIcon::Arrow);\n\t\t\t\t\tcontent->setLeftPadding(state->leftPadding.value());\n\t\t\t\t\tconst auto api = &session->api().premium();\n\t\t\t\t\tapi->statusTextValue() | rpl::on_next([=] {\n\t\t\t\t\t\tfor (const auto &o : api->subscriptionOptions()) {\n\t\t\t\t\t\t\tif (o.months == 12) {\n\t\t\t\t\t\t\t\tset(o.discount);\n\t\t\t\t\t\t\t\tstate->premiumLifetime.destroy();\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}, state->premiumLifetime);\n\t\t\t\t\tapi->reload();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (promo->current(kSugSetUserpic.utf8())\n\t\t\t\t&& !session->user()->userpicPhotoId()) {\n\t\t\t\tconst auto controller = FindSessionController(parent);\n\t\t\t\tcontent->setRightIcon(RightIcon::Close);\n\t\t\t\tconst auto upload = Ui::CreateChild<Ui::UserpicButton>(\n\t\t\t\t\tcontent,\n\t\t\t\t\t&controller->window(),\n\t\t\t\t\tUi::UserpicButton::Role::ChoosePhoto,\n\t\t\t\t\tst::uploadUserpicButton);\n\t\t\t\trpl::combine(\n\t\t\t\t\tstate->leftPadding.value(),\n\t\t\t\t\tcontent->sizeValue() | rpl::filter_size()\n\t\t\t\t) | rpl::on_next([=](int padding, const QSize &s) {\n\t\t\t\t\tupload->raise();\n\t\t\t\t\tupload->show();\n\t\t\t\t\tupload->moveToLeft(\n\t\t\t\t\t\tpadding,\n\t\t\t\t\t\t(s.height() - upload->height()) / 2);\n\t\t\t\t}, content->lifetime());\n\t\t\t\tsetLeftPaddingRelativeTo(content, upload);\n\t\t\t\tupload->chosenImages() | rpl::on_next([=](\n\t\t\t\t\t\tUi::UserpicButton::ChosenImage &&chosen) {\n\t\t\t\t\tif (chosen.type == Ui::UserpicButton::ChosenType::Set) {\n\t\t\t\t\t\tsession->api().peerPhoto().upload(\n\t\t\t\t\t\t\tsession->user(),\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tstd::move(chosen.image),\n\t\t\t\t\t\t\t\tchosen.markup.documentId,\n\t\t\t\t\t\t\t\tchosen.markup.colors,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}, upload->lifetime());\n\n\t\t\t\tstate->userpicLifetime = session->changes().peerUpdates(\n\t\t\t\t\tsession->user(),\n\t\t\t\t\tData::PeerUpdate::Flag::Photo\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\tif (session->user()->userpicPhotoId()) {\n\t\t\t\t\t\trepeat(repeat);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\tcontent->setHideCallback([=] {\n\t\t\t\t\tpromo->dismiss(kSugSetUserpic.utf8());\n\t\t\t\t\trepeat(repeat);\n\t\t\t\t});\n\n\t\t\t\tcontent->setClickedCallback([=] {\n\t\t\t\t\tconst auto syntetic = [=](QEvent::Type type) {\n\t\t\t\t\t\tUi::SendSynteticMouseEvent(\n\t\t\t\t\t\t\tupload,\n\t\t\t\t\t\t\ttype,\n\t\t\t\t\t\t\tQt::LeftButton,\n\t\t\t\t\t\t\tupload->mapToGlobal(QPoint(0, 0)));\n\t\t\t\t\t};\n\t\t\t\t\tsyntetic(QEvent::MouseMove);\n\t\t\t\t\tsyntetic(QEvent::MouseButtonPress);\n\t\t\t\t\tsyntetic(QEvent::MouseButtonRelease);\n\t\t\t\t});\n\t\t\t\tcontent->setContent(\n\t\t\t\t\ttr::lng_dialogs_suggestions_userpics_title(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::bold),\n\t\t\t\t\ttr::lng_dialogs_suggestions_userpics_about(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tTextWithEntities::Simple));\n\t\t\t\tstate->desiredWrapToggle.force_assign(\n\t\t\t\t\tToggle{ true, anim::type::normal });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->desiredWrapToggle.force_assign(\n\t\t\t\tToggle{ false, anim::type::normal });\n\t\t\tbase::call_delayed(st::slideWrapDuration * 2, wrap, [=] {\n\t\t\t\tstate->content = nullptr;\n\t\t\t\tstate->wrap = nullptr;\n\t\t\t\tconsumer.put_next(nullptr);\n\t\t\t});\n\t\t};\n\n\t\tstate->desiredWrapToggle.value() | rpl::combine_previous(\n\t\t) | rpl::filter([=] {\n\t\t\treturn state->wrap != nullptr;\n\t\t}) | rpl::on_next([=](Toggle was, Toggle now) {\n\t\t\tstate->wrap->toggle(\n\t\t\t\tstate->outerWrapToggle.current() && now.value,\n\t\t\t\t(was.value == now.value)\n\t\t\t\t\t? anim::type::instant\n\t\t\t\t\t: now.type);\n\t\t}, lifetime);\n\n\t\tstate->outerWrapToggle.value() | rpl::combine_previous(\n\t\t) | rpl::filter([=] {\n\t\t\treturn state->wrap != nullptr;\n\t\t}) | rpl::on_next([=](bool was, bool now) {\n\t\t\tconst auto toggle = state->desiredWrapToggle.current();\n\t\t\tstate->wrap->toggle(\n\t\t\t\ttoggle.value && now,\n\t\t\t\t(was == now) ? toggle.type : anim::type::instant);\n\t\t}, lifetime);\n\n\t\trpl::merge(\n\t\t\tsession->promoSuggestions().value(),\n\t\t\tsession->api().authorizations().unreviewedChanges(),\n\t\t\tData::AmPremiumValue(session) | rpl::skip(1) | rpl::to_empty,\n\t\t\tsession->giftAuctions().hasActiveChanges() | rpl::to_empty\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto was = state->wrap.get();\n\t\t\tconst auto weak = base::make_weak(was);\n\t\t\tprocessCurrentSuggestion(processCurrentSuggestion);\n\t\t\tif (was != state->wrap || (was && !weak)) {\n\t\t\t\tconsumer.put_next_copy(state->wrap.get());\n\t\t\t}\n\t\t}, lifetime);\n\n\t\treturn lifetime;\n\t};\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\ntemplate <typename Object>\nclass object_ptr;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass RpWidget;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Dialogs {\n\n[[nodiscard]] auto TopBarSuggestionValue(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Main::Session*>,\n\trpl::producer<bool> outerWrapToggleValue)\n-> rpl::producer<Ui::SlideWrap<Ui::RpWidget>*>;\n\n} // namespace Dialogs\n\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/dialogs_widget.h\"\n\n#include \"core/application.h\"\n#include \"core/fork_settings.h\"\n\n#include \"base/call_delayed.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"base/options.h\"\n#include \"dialogs/ui/chat_search_in.h\"\n#include \"dialogs/ui/dialogs_stories_content.h\"\n#include \"dialogs/ui/dialogs_stories_list.h\"\n#include \"dialogs/ui/dialogs_suggestions.h\"\n#include \"dialogs/dialogs_inner_widget.h\"\n#include \"dialogs/dialogs_search_from_controllers.h\"\n#include \"dialogs/dialogs_top_bar_suggestion.h\"\n#include \"dialogs/dialogs_quick_action.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/history_view_chat_section.h\"\n#include \"history/view/history_view_contact_status.h\"\n#include \"history/view/history_view_group_call_bar.h\"\n#include \"history/view/history_view_requests_bar.h\"\n#include \"history/view/history_view_top_bar_widget.h\"\n#include \"boxes/peers/edit_peer_requests_box.h\"\n#include \"boxes/choose_filter_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/chat_filters_tabs_strip.h\"\n#include \"ui/widgets/elastic_scroll.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/chat/requests_bar.h\"\n#include \"ui/chat/group_call_bar.h\"\n#include \"ui/chat/more_chats_bar.h\"\n#include \"ui/controls/download_bar.h\"\n#include \"ui/controls/jump_down_button.h\"\n#include \"ui/controls/swipe_handler.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"lang/lang_keys.h\"\n#include \"mainwindow.h\"\n#include \"mainwidget.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"api/api_authorizations.h\"\n#include \"api/api_chat_filters.h\"\n#include \"apiwrap.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"core/application.h\"\n#include \"core/ui_integration.h\"\n#include \"core/update_checker.h\"\n#include \"core/shortcuts.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_slide_animation.h\"\n#include \"window/window_connecting_widget.h\"\n#include \"window/window_main_menu.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/storage_domain.h\"\n#include \"data/components/recent_peers.h\"\n#include \"data/components/sponsored_messages.h\"\n#include \"data/data_session.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_user.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_download_manager.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_stories.h\"\n#include \"info/downloads/info_downloads_widget.h\"\n#include \"info/info_memento.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_window.h\"\n#include \"base/qt/qt_common_adapters.h\"\n\n#include <QtCore/QMimeData>\n#include <QtGui/QTextBlock>\n#include <QtWidgets/QScrollBar>\n#include <QtWidgets/QTextEdit>\n\nnamespace Dialogs {\nnamespace {\n\nconstexpr auto kSearchPerPage = 50;\nconstexpr auto kStoriesExpandDuration = crl::time(200);\nconstexpr auto kSearchRequestDelay = crl::time(900);\n\nbase::options::toggle OptionForumHideChatsList({\n\t.id = kOptionForumHideChatsList,\n\t.name = \"Hide chat list in forums\",\n\t.description = \"Don't keep a narrow column of chat list.\",\n});\n\n[[nodiscard]] bool RedirectTextToSearch(const QString &text) {\n\tfor (const auto &ch : text) {\n\t\tif (ch.unicode() >= 32) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n[[nodiscard]] QImage UpdateIcon() {\n\tconst auto iconSize = st::dialogsInstallUpdateIconSize;\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto result = QImage(\n\t\tSize(iconSize) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\tresult.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&result);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tauto path = QPainterPath();\n\n\t\tconst auto fullRect = QRectF(0, 0, iconSize, iconSize);\n\t\tconst auto rect = fullRect\n\t\t\t- Margins(st::dialogsInstallUpdateIconInnerMargin);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(Qt::white);\n\t\tp.drawEllipse(fullRect);\n\n\t\tp.setCompositionMode(QPainter::CompositionMode_Clear);\n\n\t\tauto pen = QPen(Qt::black);\n\t\tpen.setWidthF(style::ConvertFloatScale(2.));\n\t\tpen.setCapStyle(Qt::RoundCap);\n\t\tp.setPen(pen);\n\n\t\tusing namespace arc;\n\t\tconstexpr auto kShift = int(20 * 16);\n\t\tp.drawArc(rect, -kShift, kQuarterLength + kShift);\n\t\tp.drawArc(rect, kHalfLength - kShift, kQuarterLength + kShift);\n\n\t\tconst auto side1 = st::dialogsInstallUpdateIconSide1;\n\t\tconst auto side2 = st::dialogsInstallUpdateIconSide2;\n\t\tconst auto top = rect.y() - side1;\n\t\tconst auto bottom = rect::bottom(rect) - side1;\n\t\tconst auto centerX = rect::center(rect).x();\n\t\tpath.moveTo(centerX, bottom + side1 + side2);\n\t\tpath.lineTo(centerX, bottom + side1 - side2);\n\t\tpath.lineTo(centerX + side2, bottom + side1);\n\t\tpath.closeSubpath();\n\n\t\tpath.moveTo(centerX, top + side1 + side2);\n\t\tpath.lineTo(centerX, top + side1 - side2);\n\t\tpath.lineTo(centerX - side2, top + side1);\n\t\tpath.closeSubpath();\n\n\t\tp.fillPath(path, Qt::black);\n\t}\n\treturn result;\n}\n\n} // namespace\n\nconst char kOptionForumHideChatsList[] = \"forum-hide-chats-list\";\n\nclass Widget::BottomButton : public Ui::RippleButton {\npublic:\n\tBottomButton(\n\t\tQWidget *parent,\n\t\tconst QString &text,\n\t\tconst style::FlatButton &st,\n\t\tconst style::icon &icon,\n\t\tconst style::icon &iconOver,\n\t\tbool hasTextIcon);\n\n\tvoid setText(const QString &text);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid onStateChanged(State was, StateChangeSource source) override;\n\nprivate:\n\tvoid radialAnimationCallback();\n\n\tQString _text;\n\tconst style::FlatButton &_st;\n\tconst style::icon &_icon;\n\tconst style::icon &_iconOver;\n\tconst bool _hasTextIcon;\n\tstd::unique_ptr<Ui::InfiniteRadialAnimation> _loading;\n\n\tQImage _textIcon;\n\n};\n\nWidget::BottomButton::BottomButton(\n\tQWidget *parent,\n\tconst QString &text,\n\tconst style::FlatButton &st,\n\tconst style::icon &icon,\n\tconst style::icon &iconOver,\n\tbool hasTextIcon)\n: RippleButton(parent, st.ripple)\n, _text(text)\n, _st(st)\n, _icon(icon)\n, _iconOver(iconOver)\n, _hasTextIcon(hasTextIcon) {\n\tresize(st::columnMinimalWidthLeft, _st.height);\n\n\tif (_hasTextIcon) {\n\t\trpl::single(rpl::empty_value()) | rpl::then(\n\t\t\tstyle::PaletteChanged()\n\t\t) | rpl::on_next([this] {\n\t\t\t_textIcon = UpdateIcon();\n\t\t}, lifetime());\n\t}\n}\n\nvoid Widget::BottomButton::setText(const QString &text) {\n\t_text = text;\n\tupdate();\n}\n\nvoid Widget::BottomButton::radialAnimationCallback() {\n\tif (!anim::Disabled() && width() < st::columnMinimalWidthLeft) {\n\t\tupdate();\n\t}\n}\n\nvoid Widget::BottomButton::onStateChanged(\n\t\tState was,\n\t\tStateChangeSource source) {\n\tRippleButton::onStateChanged(was, source);\n\tif ((was & StateFlag::Disabled) != (state() & StateFlag::Disabled)) {\n\t\t_loading = isDisabled()\n\t\t\t? std::make_unique<Ui::InfiniteRadialAnimation>(\n\t\t\t\t[=] { radialAnimationCallback(); },\n\t\t\t\tst::dialogsLoadMoreLoading)\n\t\t\t: nullptr;\n\t\tif (_loading) {\n\t\t\t_loading->start();\n\t\t}\n\t}\n\tupdate();\n}\n\nvoid Widget::BottomButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto over = isOver() && !isDisabled();\n\n\tauto r = QRect(0, height() - _st.height, width(), _st.height);\n\n\tif (_hasTextIcon) {\n\t\tauto gradient = QLinearGradient(0, 0, width(), 0);\n\t\tgradient.setStops({\n\t\t\t{ 0., st::groupCallLive1->c },\n\t\t\t{ 1., st::groupCallLive2->c },\n\t\t});\n\t\tp.fillRect(r, QBrush(std::move(gradient)));\n\t\tif (over) {\n\t\t\tp.fillRect(\n\t\t\t\tr,\n\t\t\t\tanim::with_alpha(st::universalRippleAnimation.color->c, .3));\n\t\t}\n\n\t\tif (!isDisabled()) {\n\t\t\tpaintRipple(p, 0, 0, &st::universalRippleAnimation.color->c);\n\t\t}\n\t} else {\n\t\tp.fillRect(r, over ? _st.overBgColor : _st.bgColor);\n\t\tif (!isDisabled()) {\n\t\t\tpaintRipple(p, 0, 0);\n\t\t}\n\t}\n\n\tconst auto &font = over ? _st.overFont : _st.font;\n\tp.setFont(font);\n\tp.setRenderHint(QPainter::TextAntialiasing);\n\tp.setPen(over ? _st.overColor : _st.color);\n\n\tif (width() >= st::columnMinimalWidthLeft) {\n\t\tr.setTop(_st.textTop);\n\t\tif (_hasTextIcon) {\n\t\t\tconst auto &icon = _textIcon;\n\t\t\tconst auto iconSize = icon.size() / style::DevicePixelRatio();\n\t\t\tconst auto skip = st::dialogsInstallUpdateIconSkip;\n\t\t\tconst auto textWidth = font->width(_text);\n\t\t\tconst auto rect = QRect(\n\t\t\t\t(width() - (iconSize.width() + textWidth + skip)) / 2,\n\t\t\t\tr.y(),\n\t\t\t\ttextWidth,\n\t\t\t\tr.height());\n\t\t\tp.drawText(\n\t\t\t\trect.translated(iconSize.width() + skip, 0),\n\t\t\t\t_text,\n\t\t\t\tstyle::al_top);\n\t\t\tp.drawImage(rect.x(), (height() - iconSize.height()) / 2, icon);\n\t\t} else {\n\t\t\tp.drawText(r, _text, style::al_top);\n\t\t}\n\t} else if (isDisabled() && _loading) {\n\t\t_loading->draw(\n\t\t\tp,\n\t\t\tQPoint(\n\t\t\t\t(width() - st::dialogsLoadMoreLoading.size.width()) / 2,\n\t\t\t\t(height() - st::dialogsLoadMoreLoading.size.height()) / 2),\n\t\t\twidth());\n\t} else {\n\t\tif (_hasTextIcon) {\n\t\t\tconst auto size = _textIcon.size() / style::DevicePixelRatio();\n\t\t\tp.drawImage(\n\t\t\t\t(width() - size.width()) / 2,\n\t\t\t\t(height() - size.height()) / 2,\n\t\t\t\t_textIcon);\n\t\t} else {\n\t\t\t(over ? _iconOver : _icon).paintInCenter(p, r);\n\t\t}\n\t}\n}\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tLayout layout)\n: Window::AbstractSectionWidget(parent, controller, nullptr)\n, _api(&controller->session().mtp())\n, _chooseByDragTimer([=] { _inner->chooseRow(); })\n, _layout(layout)\n, _narrowWidth(st::defaultDialogRow.padding.left()\n\t+ st::defaultDialogRow.photoSize\n\t+ st::defaultDialogRow.padding.left())\n, _searchControls(this)\n, _mainMenu({\n\t.toggle = object_ptr<Ui::IconButton>(\n\t\t_searchControls,\n\t\tst::dialogsMenuToggle),\n\t.under = object_ptr<Ui::AbstractButton>(_searchControls),\n})\n, _searchForNarrowLayout(_searchControls, st::dialogsSearchForNarrowFilters)\n, _search(_searchControls, st::dialogsFilter, tr::lng_dlg_filter())\n, _chooseFromUser(\n\t_searchControls,\n\tobject_ptr<Ui::IconButton>(this, st::dialogsSearchFrom))\n, _jumpToDate(\n\t_searchControls,\n\tobject_ptr<Ui::IconButton>(this, st::dialogsCalendar))\n, _cancelSearch(_searchControls, st::dialogsCancelSearch)\n, _lockUnlock(\n\t_searchControls,\n\tobject_ptr<Ui::IconButton>(this, st::dialogsLock))\n, _scroll(this)\n, _scrollToTop(_scroll, st::dialogsToUp)\n, _stories((_layout != Layout::Child)\n\t? std::make_unique<Stories::List>(\n\t\tthis,\n\t\tst::dialogsStoriesList,\n\t\t_storiesContents.events() | rpl::flatten_latest())\n\t: nullptr)\n, _searchTimer([=] { search(); })\n, _peerSearch(&controller->session(), Api::PeerSearch::Type::WithSponsored)\n, _singleMessageSearch(&controller->session()) {\n\tconst auto makeChildListShown = [](PeerId peerId, float64 shown) {\n\t\treturn InnerWidget::ChildListShown{ peerId, shown };\n\t};\n\tusing OverscrollType = Ui::ElasticScroll::OverscrollType;\n\t_scroll->setOverscrollTypes(\n\t\t_stories ? OverscrollType::Virtual : OverscrollType::Real,\n\t\tOverscrollType::Real);\n\t_innerList = _scroll->setOwnedWidget(\n\t\tobject_ptr<Ui::VerticalLayout>(this));\n\t_inner = _innerList->add(object_ptr<InnerWidget>(\n\t\t_innerList,\n\t\tcontroller,\n\t\trpl::combine(\n\t\t\t_childListPeerId.value(),\n\t\t\t_childListShown.value(),\n\t\t\tmakeChildListShown)));\n\trpl::combine(\n\t\t_scroll->heightValue(),\n\t\t_topBarSuggestionHeightChanged.events_starting_with(0)\n\t) | rpl::on_next([=](int height, int topBarHeight) {\n\t\t_innerList->setMinimumHeight(height);\n\t\t_inner->setMinimumHeight(height - topBarHeight);\n\t\t_inner->refresh();\n\t}, _innerList->lifetime());\n\t_scroll->widthValue() | rpl::on_next([=](int width) {\n\t\t_innerList->resizeToWidth(width);\n\t}, _innerList->lifetime());\n\t_scrollToTop->raise();\n\t_lockUnlock->toggle(false, anim::type::instant);\n\n\t_inner->updated(\n\t) | rpl::on_next([=] {\n\t\tlistScrollUpdated();\n\t}, lifetime());\n\n\trpl::combine(\n\t\tsession().api().dialogsLoadMayBlockByDate(),\n\t\tsession().api().dialogsLoadBlockedByDate()\n\t) | rpl::on_next([=](bool mayBlock, bool isBlocked) {\n\t\trefreshLoadMoreButton(mayBlock, isBlocked);\n\t}, lifetime());\n\n\tsession().changes().historyUpdates(\n\t\tData::HistoryUpdate::Flag::MessageSent\n\t) | rpl::filter([=](const Data::HistoryUpdate &update) {\n\t\tif (_openedForum) {\n\t\t\treturn (update.history == _openedForum->history());\n\t\t} else if (_openedFolder) {\n\t\t\treturn (update.history->folder() == _openedFolder)\n\t\t\t\t&& !update.history->isPinnedDialog(FilterId());\n\t\t} else {\n\t\t\treturn !update.history->folder()\n\t\t\t\t&& !update.history->isPinnedDialog(\n\t\t\t\t\tcontroller->activeChatsFilterCurrent());\n\t\t}\n\t}) | rpl::on_next([=](const Data::HistoryUpdate &update) {\n\t\tjumpToTop(true);\n\t}, lifetime());\n\n\tfullSearchRefreshOn(session().settings().skipArchiveInSearchChanges(\n\t) | rpl::to_empty);\n\n\t_inner->scrollByDeltaRequests(\n\t) | rpl::on_next([=](int delta) {\n\t\tif (_scroll) {\n\t\t\t_scroll->scrollToY(_scroll->scrollTop() + delta);\n\t\t}\n\t}, lifetime());\n\n\t_inner->mustScrollTo(\n\t) | rpl::on_next([=](const Ui::ScrollToRequest &data) {\n\t\tif (_scroll) {\n\t\t\t_scroll->scrollToY(data.ymin, data.ymax);\n\t\t}\n\t}, lifetime());\n\t_inner->dialogMoved(\n\t) | rpl::on_next([=](const Ui::ScrollToRequest &data) {\n\t\tconst auto movedFrom = data.ymin;\n\t\tconst auto movedTo = data.ymax;\n\t\tconst auto st = _scroll->scrollTop();\n\t\tif (st > movedTo && st < movedFrom) {\n\t\t\t_scroll->scrollToY(st + _inner->st()->height);\n\t\t}\n\t}, lifetime());\n\t_inner->searchRequests(\n\t) | rpl::on_next([=](SearchRequestDelay delay) {\n\t\tsearchRequested(delay);\n\t}, lifetime());\n\t_inner->completeHashtagRequests(\n\t) | rpl::on_next([=](const QString &tag) {\n\t\tcompleteHashtag(tag);\n\t}, lifetime());\n\t_inner->refreshHashtagsRequests(\n\t) | rpl::on_next([=] {\n\t\tsearchCursorMoved();\n\t}, lifetime());\n\t_inner->changeSearchTabRequests(\n\t) | rpl::filter([=](ChatSearchTab tab) {\n\t\treturn _searchState.tab != tab;\n\t}) | rpl::on_next([=](ChatSearchTab tab) {\n\t\tauto copy = _searchState;\n\t\tcopy.tab = tab;\n\t\tapplySearchState(std::move(copy));\n\t}, lifetime());\n\t_inner->changeSearchFilterRequests(\n\t) | rpl::filter([=](ChatTypeFilter filter) {\n\t\treturn (_searchState.filter != filter)\n\t\t\t&& (_searchState.tab == ChatSearchTab::MyMessages);\n\t}) | rpl::on_next([=](ChatTypeFilter filter) {\n\t\tauto copy = _searchState;\n\t\tcopy.filter = filter;\n\t\tapplySearchState(copy);\n\t}, lifetime());\n\t_inner->cancelSearchRequests(\n\t) | rpl::on_next([=] {\n\t\tcancelSearch({\n\t\t\t.forceFullCancel = true,\n\t\t\t.jumpBackToSearchedChat = true,\n\t\t});\n\t\tcontroller->widget()->setInnerFocus();\n\t}, lifetime());\n\t_inner->cancelSearchFromRequests(\n\t) | rpl::on_next([=] {\n\t\tauto copy = _searchState;\n\t\tcopy.fromPeer = nullptr;\n\t\tif (copy.inChat.sublist()) {\n\t\t\tcopy.inChat = session().data().history(session().user());\n\t\t}\n\t\tapplySearchState(std::move(copy));\n\t}, lifetime());\n\t_inner->changeSearchFromRequests(\n\t) | rpl::on_next([=] {\n\t\tshowSearchFrom();\n\t}, lifetime());\n\t_inner->chosenRow(\n\t) | rpl::on_next([=](const ChosenRow &row) {\n\t\tchosenRow(row);\n\t}, lifetime());\n\t_inner->openBotMainAppRequests(\n\t) | rpl::on_next([=](UserId userId) {\n\t\tif (const auto user = session().data().user(userId)) {\n\t\t\topenBotMainApp(user);\n\t\t}\n\t}, lifetime());\n\n\t_scroll->geometryChanged(\n\t) | rpl::on_next(crl::guard(_inner, [=] {\n\t\t_inner->parentGeometryChanged();\n\t}), lifetime());\n\t_scroll->scrolls(\n\t) | rpl::on_next([=] {\n\t\tlistScrollUpdated();\n\t}, lifetime());\n\n\tsession().data().chatsListChanges(\n\t) | rpl::filter([=](Data::Folder *folder) {\n\t\treturn (folder == _inner->shownFolder());\n\t}) | rpl::on_next([=] {\n\t\tUi::PostponeCall(this, [=] { listScrollUpdated(); });\n\t}, lifetime());\n\n\tsetAttribute(Qt::WA_InputMethodEnabled);\n\tcontroller->widget()->imeCompositionStarts(\n\t) | rpl::filter([=] {\n\t\treturn redirectImeToSearch();\n\t}) | rpl::on_next([=] {\n\t\t_search->setFocusFast();\n\t}, lifetime());\n\n\t_search->changes(\n\t) | rpl::on_next([=] {\n\t\tcrl::on_main(this, [=] { applySearchUpdate(); });\n\t}, _search->lifetime());\n\n\t_search->submits(\n\t) | rpl::on_next([=] { submit(); }, _search->lifetime());\n\n\t_search->setMimeDataHook([=](\n\t\t\tnot_null<const QMimeData*> data,\n\t\t\tUi::InputField::MimeAction action) {\n\t\tif (data->hasFormat(u\"application/x-telegram-dialog\"_q)) {\n\t\t\tif (const auto history = HistoryFromMimeData(data, &session())) {\n\t\t\t\tif (action != Ui::InputField::MimeAction::Check) {\n\t\t\t\t\tcontroller->searchInChat(history);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t});\n\n\tQObject::connect(\n\t\t_search->rawTextEdit().get(),\n\t\t&QTextEdit::cursorPositionChanged,\n\t\tthis,\n\t\t[=] { searchCursorMoved(); },\n\t\tQt::QueuedConnection); // So getLastText() works already.\n\n\tif (!Core::UpdaterDisabled()) {\n\t\tCore::UpdateChecker checker;\n\t\trpl::merge(\n\t\t\trpl::single(rpl::empty),\n\t\t\tchecker.isLatest(),\n\t\t\tchecker.failed(),\n\t\t\tchecker.ready()\n\t\t) | rpl::on_next([=] {\n\t\t\tcheckUpdateStatus();\n\t\t}, lifetime());\n\t}\n\n\t_cancelSearch->setClickedCallback([=] {\n\t\tcancelSearch({ .jumpBackToSearchedChat = true });\n\t});\n\t_cancelSearch->setAccessibleName(tr::lng_sr_cancel_search(tr::now));\n\t_jumpToDate->entity()->setClickedCallback([=] { showCalendar(); });\n\t_jumpToDate->entity()->setAccessibleName(\n\t\ttr::lng_sr_search_date(tr::now));\n\t_chooseFromUser->entity()->setClickedCallback([=] { showSearchFrom(); });\n\t_chooseFromUser->entity()->setAccessibleName(\n\t\ttr::lng_search_messages_from(tr::now));\n\trpl::single(rpl::empty) | rpl::then(\n\t\tsession().domain().local().localPasscodeChanged()\n\t) | rpl::on_next([=] {\n\t\tupdateLockUnlockVisibility();\n\t}, lifetime());\n\tconst auto lockUnlock = _lockUnlock->entity();\n\tlockUnlock->setAccessibleName(tr::lng_shortcuts_lock(tr::now));\n\tlockUnlock->setClickedCallback([=] {\n\t\tlockUnlock->setIconOverride(\n\t\t\t&st::dialogsUnlockIcon,\n\t\t\t&st::dialogsUnlockIconOver);\n\t\tCore::App().maybeLockByPasscode();\n\t\tlockUnlock->setIconOverride(nullptr);\n\t});\n\n\tsetupMainMenuToggle();\n\tsetupShortcuts();\n\tif (_stories) {\n\t\tsetupStories();\n\t}\n\n\t_searchForNarrowLayout->setAccessibleName(tr::lng_dlg_filter(tr::now));\n\t_searchForNarrowLayout->setClickedCallback([=] {\n\t\t_search->setFocusFast();\n\t\tif (_childList) {\n\t\t\tcontroller->closeForum();\n\t\t}\n\t});\n\n\tsetAcceptDrops(true);\n\n\t_inner->setLoadMoreFilteredCallback([=] {\n\t\tconst auto state = _inner->state();\n\t\tif (state == WidgetState::Filtered\n\t\t\t&& !_topicSearchFull\n\t\t\t&& searchForTopicsRequired(_topicSearchQuery)) {\n\t\t\tsearchTopics();\n\t\t}\n\t});\n\t_inner->setLoadMoreCallback([=] {\n\t\tconst auto state = _inner->state();\n\t\tconst auto process = currentSearchProcess();\n\t\tif (state == WidgetState::Filtered\n\t\t\t&& (!process->full\n\t\t\t\t|| (_searchInMigrated && !_migratedProcess.full))) {\n\t\t\tsearchMore();\n\t\t} else if (_openedForum && state == WidgetState::Default) {\n\t\t\t_openedForum->requestTopics();\n\t\t} else {\n\t\t\tconst auto folder = _inner->shownFolder();\n\t\t\tif (!folder || !folder->chatsList()->loaded()) {\n\t\t\t\tsession().api().requestDialogs(folder);\n\t\t\t}\n\t\t}\n\t});\n\t_inner->listBottomReached(\n\t) | rpl::on_next([=] {\n\t\tloadMoreBlockedByDate();\n\t}, lifetime());\n\n\t_search->customUpDown(true);\n\n\tupdateJumpToDateVisibility(true);\n\tupdateSearchFromVisibility(true);\n\tsetupSupportMode();\n\tsetupScrollUpButton();\n\tsetupTouchChatPreview();\n\n\tconst auto overscrollBg = [=] {\n\t\treturn anim::color(\n\t\t\tst::dialogsBg,\n\t\t\tst::dialogsBgOver,\n\t\t\t_childListShown.current());\n\t};\n\t_scroll->setOverscrollBg(overscrollBg());\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_scroll->setOverscrollBg(overscrollBg());\n\t}, lifetime());\n\n\tif (_layout != Layout::Child) {\n\t\tsetupConnectingWidget();\n\n\t\tchangeOpenedFolder(\n\t\t\tcontroller->openedFolder().current(),\n\t\t\tanim::type::instant);\n\n\t\tcontroller->openedFolder().changes(\n\t\t) | rpl::on_next([=](Data::Folder *folder) {\n\t\t\tchangeOpenedFolder(folder, anim::type::normal);\n\t\t}, lifetime());\n\n\t\tcontroller->shownForum().changes(\n\t\t) | rpl::filter(!rpl::mappers::_1) | rpl::on_next([=] {\n\t\t\tif (_openedForum) {\n\t\t\t\tchangeOpenedForum(nullptr, anim::type::normal);\n\t\t\t} else if (_childList) {\n\t\t\t\tcloseChildList(anim::type::normal);\n\t\t\t}\n\t\t}, lifetime());\n\n\t\t_childListShown.changes(\n\t\t) | rpl::on_next([=] {\n\t\t\t_scroll->setOverscrollBg(overscrollBg());\n\t\t\tupdateControlsGeometry();\n\t\t}, lifetime());\n\n\t\t_childListShown.changes(\n\t\t) | rpl::filter((rpl::mappers::_1 == 0.) || (rpl::mappers::_1 == 1.)\n\t\t) | rpl::on_next([=](float64 shown) {\n\t\t\tconst auto color = (shown > 0.) ? &st::dialogsRippleBg : nullptr;\n\t\t\t_mainMenu.toggle->setRippleColorOverride(color);\n\t\t\t_searchForNarrowLayout->setRippleColorOverride(color);\n\t\t}, lifetime());\n\n\t\tsetupMoreChatsBar();\n\t\tsetupDownloadBar();\n\t}\n\tsetupSwipeBack();\n\n\tif (session().settings().dialogsFiltersEnabled()\n\t\t&& (Core::App().settings().chatFiltersHorizontal()\n\t\t\t|| !controller->enoughSpaceForFilters())) {\n\t\ttoggleFiltersMenu(true);\n\t}\n\n\tsetupFrozenAccountBar();\n\tsetupTopBarSuggestions();\n}\n\nvoid Widget::setupSwipeBack() {\n\tconst auto isMainList = [=] {\n\t\tconst auto current = controller()->activeChatsFilterCurrent();\n\t\tconst auto &chatsFilters = session().data().chatsFilters();\n\t\tif (chatsFilters.has()) {\n\t\t\treturn chatsFilters.defaultId() == current;\n\t\t}\n\t\treturn !current;\n\t};\n\n\tauto update = [=](Ui::Controls::SwipeContextData data) {\n\t\tdata.cursorTop -= _inner->y();\n\t\tif (data.translation != 0) {\n\t\t\tif (data.translation < 0\n\t\t\t\t&& _inner\n\t\t\t\t&& (Core::App().settings().quickDialogAction()\n\t\t\t\t\t!= Ui::QuickDialogAction::Disabled)) {\n\t\t\t\t_inner->setSwipeContextData(data.msgBareId, std::move(data));\n\t\t\t} else {\n\t\t\t\tif (!_swipeBackData.callback) {\n\t\t\t\t\t_swipeBackData = Ui::Controls::SetupSwipeBack(\n\t\t\t\t\t\tthis,\n\t\t\t\t\t\t[]() -> std::pair<QColor, QColor> {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tst::historyForwardChooseBg->c,\n\t\t\t\t\t\t\t\tst::historyForwardChooseFg->c,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t},\n\t\t\t\t\t\t_swipeBackMirrored,\n\t\t\t\t\t\t_swipeBackIconMirrored);\n\t\t\t\t}\n\t\t\t\t_swipeBackData.callback(data);\n\t\t\t}\n\t\t\treturn;\n\t\t} else {\n\t\t\tif (_swipeBackData.lifetime) {\n\t\t\t\t_swipeBackData = {};\n\t\t\t}\n\t\t\tif (_inner) {\n\t\t\t\t_inner->setSwipeContextData(data.msgBareId, std::nullopt);\n\t\t\t\t_inner->update();\n\t\t\t}\n\t\t}\n\t};\n\n\tauto init = [=](int top, Qt::LayoutDirection direction) {\n\t\ttop -= _inner->y();\n\t\t_swipeBackIconMirrored = false;\n\t\t_swipeBackMirrored = false;\n\t\tif (_childListShown.current()) {\n\t\t\treturn Ui::Controls::SwipeHandlerFinishData();\n\t\t}\n\t\tconst auto isRightToLeft = direction == Qt::RightToLeft;\n\t\tconst auto action = Core::App().settings().quickDialogAction();\n\t\tconst auto isDisabled = action == Ui::QuickDialogAction::Disabled;\n\t\tif (_inner) {\n\t\t\t_inner->clearQuickActions();\n\t\t\tif (!isRightToLeft) {\n\t\t\t\tif (const auto key = _inner->calcSwipeKey(top);\n\t\t\t\t\t\tkey && !isDisabled) {\n\t\t\t\t\t_inner->prepareQuickAction(key, action);\n\t\t\t\t\treturn Ui::Controls::SwipeHandlerFinishData{\n\t\t\t\t\t\t.callback = [=, session = &session()] {\n\t\t\t\t\t\t\tauto callback = [=, peerId = PeerId(key)] {\n\t\t\t\t\t\t\t\tPerformQuickDialogAction(\n\t\t\t\t\t\t\t\t\tcontroller(),\n\t\t\t\t\t\t\t\t\tsession->data().peer(peerId),\n\t\t\t\t\t\t\t\t\taction,\n\t\t\t\t\t\t\t\t\t_inner->filterId());\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tbase::call_delayed(\n\t\t\t\t\t\t\t\tst::slideWrapDuration,\n\t\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\t\tstd::move(callback));\n\t\t\t\t\t\t},\n\t\t\t\t\t\t.msgBareId = key,\n\t\t\t\t\t\t.speedRatio = 1.,\n\t\t\t\t\t\t.reachRatioDuration = crl::time(\n\t\t\t\t\t\t\tst::slideWrapDuration),\n\t\t\t\t\t\t.provideReachOutRatio = true,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (controller()->openedFolder().current()) {\n\t\t\tif (!isRightToLeft) {\n\t\t\t\treturn Ui::Controls::SwipeHandlerFinishData();\n\t\t\t}\n\t\t\treturn Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {\n\t\t\t\t_swipeBackData = {};\n\t\t\t\tif (controller()->openedFolder().current()) {\n\t\t\t\t\tif (!controller()->windowId().folder()) {\n\t\t\t\t\t\tcontroller()->closeFolder();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\tif (controller()->shownForum().current()) {\n\t\t\tif (!isRightToLeft) {\n\t\t\t\treturn Ui::Controls::SwipeHandlerFinishData();\n\t\t\t}\n\t\t\tconst auto id = controller()->windowId();\n\t\t\tconst auto initial = id.forum();\n\t\t\tif (initial) {\n\t\t\t\treturn Ui::Controls::SwipeHandlerFinishData();\n\t\t\t}\n\t\t\treturn Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {\n\t\t\t\t_swipeBackData = {};\n\t\t\t\tif (controller()->shownForum().current()) {\n\t\t\t\t\tcontroller()->closeForum();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\tif (isRightToLeft && isMainList()) {\n\t\t\t_swipeBackIconMirrored = true;\n\t\t\treturn Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {\n\t\t\t\t_swipeBackIconMirrored = false;\n\t\t\t\t_swipeBackData = {};\n\t\t\t\tif (isMainList()) {\n\t\t\t\t\tshowMainMenu();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\tif (session().data().chatsFilters().has() && isDisabled) {\n\t\t\t_swipeBackMirrored = !isRightToLeft;\n\t\t\tusing namespace Window;\n\t\t\tconst auto next = !isRightToLeft;\n\t\t\tif (CheckAndJumpToNearChatsFilter(controller(), next, false)) {\n\t\t\t\treturn Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {\n\t\t\t\t\t_swipeBackData = {};\n\t\t\t\t\tCheckAndJumpToNearChatsFilter(controller(), next, true);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn Ui::Controls::SwipeHandlerFinishData();\n\t};\n\n\tUi::Controls::SetupSwipeHandler({\n\t\t.widget = _innerList,\n\t\t.scroll = _scroll.data(),\n\t\t.update = std::move(update),\n\t\t.init = std::move(init),\n\t});\n\n}\n\nvoid Widget::chosenRow(const ChosenRow &row) {\n\tstoriesToggleExplicitExpand(false);\n\n\tif (!row.sponsoredRandomId.isEmpty()) {\n\t\tauto &messages = session().sponsoredMessages();\n\t\tmessages.clicked(row.sponsoredRandomId, false, false);\n\t} else if (!_searchState.query.isEmpty()) {\n\t\tif (const auto history = row.key.history()) {\n\t\t\tsession().recentPeers().bump(history->peer);\n\t\t}\n\t}\n\n\tconst auto history = row.key.history();\n\tconst auto topicJump = history\n\t\t? history->peer->forumTopicFor(row.topicJumpRootId)\n\t\t: nullptr;\n\tconst auto sublistJump = history\n\t\t? history->peer->monoforumSublistFor(row.sublistJumpPeerId)\n\t\t: nullptr;\n\n\tif (topicJump) {\n\t\tif (controller()->shownForum().current() == topicJump->forum()) {\n\t\t\tcontroller()->closeForum();\n\t\t} else if (row.newWindow) {\n\t\t\tcontroller()->showInNewWindow(Window::SeparateId(topicJump));\n\t\t} else {\n\t\t\tif (!controller()->adaptive().isOneColumn()\n\t\t\t\t&& !topicJump->peer()->useSubsectionTabs()) {\n\t\t\t\tcontroller()->showForum(\n\t\t\t\t\ttopicJump->forum(),\n\t\t\t\t\tWindow::SectionShow().withChildColumn());\n\t\t\t}\n\t\t\tcontroller()->showThread(\n\t\t\t\ttopicJump,\n\t\t\t\tShowAtUnreadMsgId,\n\t\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\t}\n\t\treturn;\n\t} else if (sublistJump) {\n\t\tif (row.newWindow) {\n\t\t\tcontroller()->showInNewWindow(Window::SeparateId(sublistJump));\n\t\t} else {\n\t\t\tcontroller()->showThread(\n\t\t\t\tsublistJump,\n\t\t\t\tShowAtUnreadMsgId,\n\t\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\t}\n\t\treturn;\n\t} else if (const auto topic = row.key.topic()) {\n\t\tauto params = Window::SectionShow(\n\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\tparams.highlight = Window::SearchHighlightId(_searchState.query);\n\t\tif (row.newWindow) {\n\t\t\tcontroller()->showInNewWindow(\n\t\t\t\tWindow::SeparateId(topic),\n\t\t\t\trow.message.fullId.msg);\n\t\t} else {\n\t\t\tsession().data().saveViewAsMessages(topic->forum(), false);\n\t\t\tcontroller()->showThread(topic, row.message.fullId.msg, params);\n\t\t}\n\t} else if (history\n\t\t&& row.userpicClick\n\t\t&& (row.message.fullId.msg == ShowAtUnreadMsgId)\n\t\t&& history->peer->hasActiveStories()\n\t\t&& !history->peer->isSelf()) {\n\t\tcontroller()->openPeerStories(history->peer->id);\n\t\treturn;\n\t} else if (history\n\t\t&& history->isForum()\n\t\t&& !row.message.fullId\n\t\t&& (!controller()->adaptive().isOneColumn()\n\t\t\t|| !history->peer->viewForumAsMessages()\n\t\t\t|| history->peer->useSubsectionTabs())) {\n\t\tconst auto forum = history->peer->forum();\n\t\tif (controller()->shownForum().current() == forum) {\n\t\t\tcontroller()->closeForum();\n\t\t} else if (row.newWindow) {\n\t\t\tconst auto type = forum->peer()->useSubsectionTabs()\n\t\t\t\t? Window::SeparateType::Chat\n\t\t\t\t: Window::SeparateType::Forum;\n\t\t\tcontroller()->showInNewWindow(Window::SeparateId(type, history));\n\t\t} else {\n\t\t\tcontroller()->showForum(\n\t\t\t\tforum,\n\t\t\t\tWindow::SectionShow(\n\t\t\t\t\tWindow::SectionShow::Way::ClearStack).withChildColumn());\n\t\t\tif (controller()->shownForum().current() == forum\n\t\t\t\t&& forum->peer()->viewForumAsMessages()) {\n\t\t\t\tcontroller()->showThread(\n\t\t\t\t\thistory,\n\t\t\t\t\tShowAtUnreadMsgId,\n\t\t\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\t\t}\n\t\t}\n\t\treturn;\n\t} else if (history\n\t\t&& history->amMonoforumAdmin()\n\t\t&& !row.message.fullId) {\n\t\tconst auto monoforum = history->peer->monoforum();\n\t\tif (row.newWindow) {\n\t\t\tcontroller()->showInNewWindow(\n\t\t\t\tWindow::SeparateId(Window::SeparateType::Chat, history));\n\t\t} else {\n\t\t\tif (const auto active = monoforum->activeSubsectionThread()) {\n\t\t\t\tcontroller()->showThread(\n\t\t\t\t\tactive,\n\t\t\t\t\tShowAtUnreadMsgId,\n\t\t\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\t\t} else {\n\t\t\t\tcontroller()->showPeerHistory(\n\t\t\t\t\thistory,\n\t\t\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\t\t}\n\t\t}\n\t\treturn;\n\t} else if (history) {\n\t\tconst auto peer = history->peer;\n\t\tconst auto showAtMsgId = controller()->uniqueChatsInSearchResults(\n\t\t\t_searchState\n\t\t) ? ShowAtUnreadMsgId : row.message.fullId.msg;\n\t\tauto params = Window::SectionShow(\n\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\tparams.highlight = Window::SearchHighlightId(_searchState.query);\n\t\tif (row.newWindow) {\n\t\t\tcontroller()->showInNewWindow(peer, showAtMsgId);\n\t\t\tcloseSuggestions();\n\t\t} else {\n\t\t\tcontroller()->showThread(history, showAtMsgId, params);\n\t\t\thideChildList();\n\t\t}\n\t} else if (const auto folder = row.key.folder()) {\n\t\tif (row.userpicClick) {\n\t\t\tconst auto list = Data::StorySourcesList::Hidden;\n\t\t\tconst auto &sources = session().data().stories().sources(list);\n\t\t\tif (!sources.empty()) {\n\t\t\t\tcontroller()->openPeerStories(sources.front().id, list);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (row.newWindow) {\n\t\t\tcontroller()->showInNewWindow(Window::SeparateId(\n\t\t\t\tWindow::SeparateType::Archive,\n\t\t\t\t&session()));\n\t\t\treturn;\n\t\t}\n\t\tcontroller()->openFolder(folder);\n\t\thideChildList();\n\t}\n\tif (row.filteredRow && !session().supportMode()) {\n\t\tif (_subsectionTopBar) {\n\t\t\t_subsectionTopBar->toggleSearch(false, anim::type::instant);\n\t\t} else {\n\t\t\tescape();\n\t\t}\n\t}\n\tupdateForceDisplayWide();\n}\n\nvoid Widget::setGeometryWithTopMoved(\n\t\tconst QRect &newGeometry,\n\t\tint topDelta) {\n\t_topDelta = topDelta;\n\tbool willBeResized = (size() != newGeometry.size());\n\tif (geometry() != newGeometry) {\n\t\tauto weak = base::make_weak(this);\n\t\tsetGeometry(newGeometry);\n\t\tif (!weak) {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (!willBeResized) {\n\t\tresizeEvent(nullptr);\n\t}\n\t_topDelta = 0;\n}\n\nvoid Widget::scrollToDefaultChecked(bool verytop) {\n\tif (_scrollToAnimation.animating()) {\n\t\treturn;\n\t}\n\tscrollToDefault(verytop);\n}\n\nvoid Widget::setupScrollUpButton() {\n\t_scrollToTop->setClickedCallback([=] { scrollToDefaultChecked(); });\n\t_scrollToTop->setAccessibleName(tr::lng_sr_scroll_to_top(tr::now));\n\ttrackScroll(_scrollToTop);\n\ttrackScroll(this);\n\tupdateScrollUpVisibility();\n}\n\nvoid Widget::setupTouchChatPreview() {\n\t_scroll->setCustomTouchProcess([=](not_null<QTouchEvent*> e) {\n\t\treturn _inner->processTouchEvent(e);\n\t});\n\t_inner->touchCancelRequests() | rpl::on_next([=] {\n\t\tQTouchEvent ev(QEvent::TouchCancel);\n\t\tev.setTimestamp(crl::now());\n\t\tQGuiApplication::sendEvent(_scroll, &ev);\n\t}, _inner->lifetime());\n}\n\nvoid Widget::setupFrozenAccountBar() {\n\tsession().frozenValue(\n\t) | rpl::on_next([=] {\n\t\tupdateFrozenAccountBar();\n\t\tupdateControlsGeometry();\n\t}, lifetime());\n}\n\nvoid Widget::setupTopBarSuggestions() {\n\tif (_layout == Layout::Child) {\n\t\treturn;\n\t}\n\tusing namespace rpl::mappers;\n\tcrl::on_main(_innerList, [=] {\n\t\tconst auto owner = &session().data();\n\t\tsession().api().authorizations().unreviewedChanges(\n\t\t) | rpl::on_next([=] {\n\t\t\tupdateForceDisplayWide();\n\t\t}, lifetime());\n\t\t(owner->chatsListLoaded(nullptr)\n\t\t\t? rpl::single<Data::Folder*>(nullptr)\n\t\t\t: owner->chatsListLoadedEvents()\n\t\t) | rpl::filter(_1 == nullptr) | rpl::map([=] {\n\t\t\tauto on = rpl::combine(\n\t\t\t\tcontroller()->activeChatsFilter(),\n\t\t\t\t_openedFolderOrForumChanges.events_starting_with(false),\n\t\t\t\twidthValue() | rpl::map(\n\t\t\t\t\t_1 >= st::columnMinimalWidthLeft\n\t\t\t\t) | rpl::distinct_until_changed(),\n\t\t\t\t_searchStateForTopBarSuggestion.events_starting_with(\n\t\t\t\t\t!_searchState.query.isEmpty()),\n\t\t\t\t_jumpToDate->toggledValue()\n\t\t\t) | rpl::map([=](\n\t\t\t\t\tFilterId id,\n\t\t\t\t\tbool folderOrForum,\n\t\t\t\t\tbool wide,\n\t\t\t\t\tbool search,\n\t\t\t\t\tbool searchInPeer) {\n\t\t\t\treturn !folderOrForum\n\t\t\t\t\t&& wide\n\t\t\t\t\t&& !search\n\t\t\t\t\t&& !searchInPeer\n\t\t\t\t\t&& (id == owner->chatsFilters().defaultId());\n\t\t\t});\n\t\t\treturn TopBarSuggestionValue(_innerList, &session(), std::move(on));\n\t\t}) | rpl::flatten_latest() | rpl::on_next([=](\n\t\t\t\tUi::SlideWrap<Ui::RpWidget> *raw) {\n\t\t\tif (raw) {\n\t\t\t\t_topBarSuggestion = _innerList->insert(\n\t\t\t\t\t0,\n\t\t\t\t\tobject_ptr<Ui::SlideWrap<Ui::RpWidget>>::fromRaw(raw));\n\t\t\t\t_topBarSuggestion->heightValue(\n\t\t\t\t) | rpl::start_to_stream(\n\t\t\t\t\t_topBarSuggestionHeightChanged,\n\t\t\t\t\t_topBarSuggestion->entity()->lifetime());\n\t\t\t\trpl::combine(\n\t\t\t\t\t_topBarSuggestion->entity()->desiredHeightValue(),\n\t\t\t\t\t_childListShown.value()\n\t\t\t\t) | rpl::on_next([=](\n\t\t\t\t\t\tint desiredHeight,\n\t\t\t\t\t\tfloat64 shown) {\n\t\t\t\t\tconst auto newHeight = desiredHeight * (1. - shown);\n\t\t\t\t\t_topBarSuggestion->entity()->setMaximumHeight(newHeight);\n\t\t\t\t\t_topBarSuggestion->entity()->setMinimumWidth((shown > 0)\n\t\t\t\t\t\t? width()\n\t\t\t\t\t\t: 0);\n\t\t\t\t\t_topBarSuggestion->entity()->resize(width(), newHeight);\n\t\t\t\t}, _topBarSuggestion->lifetime());\n\t\t\t} else {\n\t\t\t\tif (_topBarSuggestion) {\n\t\t\t\t\tdelete _topBarSuggestion;\n\t\t\t\t}\n\t\t\t\t_topBarSuggestion = nullptr;\n\t\t\t}\n\t\t}, lifetime());\n\t});\n}\n\nvoid Widget::updateFrozenAccountBar() {\n\tif (_layout == Layout::Child\n\t\t|| _openedForum\n\t\t|| _openedFolder\n\t\t|| !session().frozen()) {\n\t\t_frozenAccountBar = nullptr;\n\t} else if (!_frozenAccountBar) {\n\t\t_frozenAccountBar = FrozenWriteRestriction(\n\t\t\tthis,\n\t\t\tcontroller()->uiShow(),\n\t\t\tFrozenWriteRestrictionType::DialogsList);\n\t\t_frozenAccountBar->show();\n\t}\n}\n\nvoid Widget::updateTopBarSuggestions() {\n\tif (_topBarSuggestion) {\n\t\t_openedFolderOrForumChanges.fire(_openedFolder || _openedForum);\n\t}\n}\n\nvoid Widget::setupMoreChatsBar() {\n\tif (_layout == Layout::Child) {\n\t\treturn;\n\t}\n\tcontroller()->activeChatsFilter(\n\t) | rpl::on_next([=](FilterId id) {\n\t\tstoriesToggleExplicitExpand(false);\n\t\tconst auto cancelled = cancelSearch({ .forceFullCancel = true });\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\tif (cancelled) {\n\t\t\t\tcontroller()->content()->dialogsCancelled();\n\t\t\t}\n\t\t});\n\n\t\tif (!id) {\n\t\t\t_moreChatsBar = nullptr;\n\t\t\tupdateControlsGeometry();\n\t\t\treturn;\n\t\t}\n\t\tconst auto filters = &session().data().chatsFilters();\n\t\t_moreChatsBar = std::make_unique<Ui::MoreChatsBar>(\n\t\t\tthis,\n\t\t\tfilters->moreChatsContent(id));\n\n\t\ttrackScroll(_moreChatsBar->wrap());\n\n\t\t_moreChatsBar->barClicks(\n\t\t) | rpl::on_next([=] {\n\t\t\tif (const auto missing = filters->moreChats(id)\n\t\t\t\t; !missing.empty()) {\n\t\t\t\tApi::ProcessFilterUpdate(controller(), id, missing);\n\t\t\t}\n\t\t}, _moreChatsBar->lifetime());\n\n\t\t_moreChatsBar->closeClicks(\n\t\t) | rpl::on_next([=] {\n\t\t\tApi::ProcessFilterUpdate(controller(), id, {});\n\t\t}, _moreChatsBar->lifetime());\n\n\t\tif (_showAnimation) {\n\t\t\t_moreChatsBar->hide();\n\t\t} else {\n\t\t\t_moreChatsBar->show();\n\t\t\t_moreChatsBar->finishAnimating();\n\t\t}\n\n\t\t_moreChatsBar->heightValue(\n\t\t) | rpl::on_next([=] {\n\t\t\tupdateControlsGeometry();\n\t\t}, _moreChatsBar->lifetime());\n\t}, lifetime());\n}\n\nvoid Widget::setupDownloadBar() {\n\tif (_layout == Layout::Child) {\n\t\treturn;\n\t}\n\n\tData::MakeDownloadBarContent(\n\t) | rpl::on_next([=](Ui::DownloadBarContent &&content) {\n\t\tconst auto create = (content.count && !_downloadBar);\n\t\tif (create) {\n\t\t\t_downloadBar = std::make_unique<Ui::DownloadBar>(\n\t\t\t\tthis,\n\t\t\t\tData::MakeDownloadBarProgress());\n\t\t}\n\t\tif (_downloadBar) {\n\t\t\t_downloadBar->show(std::move(content));\n\t\t}\n\t\tif (create) {\n\t\t\t_downloadBar->heightValue(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tupdateControlsGeometry();\n\t\t\t}, _downloadBar->lifetime());\n\n\t\t\t_downloadBar->shownValue(\n\t\t\t) | rpl::filter(\n\t\t\t\t!rpl::mappers::_1\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\t_downloadBar = nullptr;\n\t\t\t\tupdateControlsGeometry();\n\t\t\t}, _downloadBar->lifetime());\n\n\t\t\t_downloadBar->clicks(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tauto &&list = Core::App().downloadManager().loadingList();\n\t\t\t\tconst auto guard = gsl::finally([] {\n\t\t\t\t\tCore::App().downloadManager().clearIfFinished();\n\t\t\t\t});\n\t\t\t\tauto first = (HistoryItem*)nullptr;\n\t\t\t\tfor (const auto id : list) {\n\t\t\t\t\tif (!first) {\n\t\t\t\t\t\tfirst = id->object.item;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcontroller()->showSection(\n\t\t\t\t\t\t\tInfo::Downloads::Make(\n\t\t\t\t\t\t\t\tcontroller()->session().user()));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (first) {\n\t\t\t\t\tcontroller()->showMessage(first);\n\t\t\t\t}\n\t\t\t}, _downloadBar->lifetime());\n\n\t\t\tif (_connecting) {\n\t\t\t\t_connecting->raise();\n\t\t\t}\n\t\t}\n\t}, lifetime());\n}\n\nvoid Widget::updateScrollUpVisibility() {\n\tif (_scrollToAnimation.animating()) {\n\t\treturn;\n\t}\n\n\tstartScrollUpButtonAnimation(\n\t\t(_scroll->scrollTop() > (st::historyToDownShownAfter / 2))\n\t\t&& (_scroll->scrollTop() < _scroll->scrollTopMax()));\n}\n\nvoid Widget::startScrollUpButtonAnimation(bool shown) {\n\tconst auto smallColumn = (width() < st::columnMinimalWidthLeft)\n\t\t|| _childList;\n\tshown &= !smallColumn;\n\tif (_scrollToTopIsShown == shown) {\n\t\treturn;\n\t}\n\t_scrollToTopIsShown = shown;\n\t_scrollToTopShown.start(\n\t\t[=] { updateScrollUpPosition(); },\n\t\t_scrollToTopIsShown ? 0. : 1.,\n\t\t_scrollToTopIsShown ? 1. : 0.,\n\t\tsmallColumn ? 0 : st::historyToDownDuration);\n}\n\nvoid Widget::updateScrollUpPosition() {\n\t// _scrollToTop is a child widget of _scroll, not me.\n\tauto top = anim::interpolate(\n\t\t0,\n\t\t_scrollToTop->height() + st::connectingMargin.top(),\n\t\t_scrollToTopShown.value(_scrollToTopIsShown ? 1. : 0.));\n\t_scrollToTop->moveToRight(\n\t\tst::historyToDownPosition.x(),\n\t\t_scroll->height() - top);\n\tconst auto shouldBeHidden\n\t\t= !_scrollToTopIsShown && !_scrollToTopShown.animating();\n\tif (shouldBeHidden != _scrollToTop->isHidden()) {\n\t\t_scrollToTop->setVisible(!shouldBeHidden);\n\t}\n}\n\nvoid Widget::setupConnectingWidget() {\n\t_connecting = std::make_unique<Window::ConnectionState>(\n\t\tthis,\n\t\t&session().account(),\n\t\tcontroller()->adaptive().oneColumnValue());\n}\n\nvoid Widget::setupSupportMode() {\n\tif (!session().supportMode()) {\n\t\treturn;\n\t}\n\n\tfullSearchRefreshOn(session().settings().supportAllSearchResultsValue(\n\t) | rpl::to_empty);\n}\n\nvoid Widget::setupMainMenuToggle() {\n\t_mainMenu.under->setClickedCallback([=] {\n\t\t_mainMenu.toggle->clicked({}, Qt::LeftButton);\n\t});\n\t_mainMenu.under->stackUnder(_mainMenu.toggle);\n\t_mainMenu.toggle->setClickedCallback([=] { showMainMenu(); });\n\t_mainMenu.toggle->setIsMenuButton(true);\n\t_mainMenu.toggle->setAccessibleName(tr::lng_main_menu(tr::now));\n\t_mainMenu.toggle->setAcceptBoth();\n\tconst auto window = controller();\n\t_mainMenu.toggle->addClickHandler([=](Qt::MouseButton button) {\n\t\tif (button == Qt::LeftButton) {\n\t\t\tshowMainMenu();\n\t\t} else if (button == Qt::MiddleButton) {\n\t\t\tconst auto folder = session().data().folderLoaded(\n\t\t\t\tData::Folder::kId);\n\t\t\tif (folder && !folder->chatsList()->empty()) {\n\t\t\t\tcontroller()->openFolder(folder);\n\t\t\t}\n\t\t} else if (button == Qt::RightButton) {\n\t\t\twindow->showPeerHistory(window->session().user());\n\t\t}\n\t});\n\n\trpl::single(rpl::empty) | rpl::then(\n\t\tcontroller()->filtersMenuChanged()\n\t) | rpl::on_next([=] {\n\t\tconst auto filtersHidden = !controller()->filtersWidth();\n\t\t_mainMenu.toggle->setVisible(filtersHidden);\n\t\t_mainMenu.under->setVisible(filtersHidden);\n\t\t_searchForNarrowLayout->setVisible(!filtersHidden);\n\t\tupdateControlsGeometry();\n\t}, lifetime());\n\n\tWindow::OtherAccountsUnreadState(\n\t\t&controller()->session().account()\n\t) | rpl::on_next([=](const Window::OthersUnreadState &state) {\n\t\tconst auto icon = !state.count\n\t\t\t? nullptr\n\t\t\t: !state.allMuted\n\t\t\t? &st::dialogsMenuToggleUnread\n\t\t\t: &st::dialogsMenuToggleUnreadMuted;\n\t\t_mainMenu.toggle->setIconOverride(icon, icon);\n\t}, _mainMenu.toggle->lifetime());\n}\n\nvoid Widget::setupStories() {\n\t_stories->verticalScrollEvents(\n\t) | rpl::on_next([=](not_null<QWheelEvent*> e) {\n\t\t_scroll->viewportEvent(e);\n\t}, _stories->lifetime());\n\n\tif (!Core::App().settings().storiesClickTooltipHidden()) {\n\t\t// Don't create tooltip\n\t\t// until storiesClickTooltipHidden can be returned to false.\n\t\tconst auto hideTooltip = [=] {\n\t\t\tCore::App().settings().setStoriesClickTooltipHidden(true);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t};\n\t\tInvokeQueued(_stories.get(), [=] {\n\t\t\t_stories->setShowTooltip(\n\t\t\t\tcontroller()->content(),\n\t\t\t\trpl::combine(\n\t\t\t\t\tCore::App().settings().storiesClickTooltipHiddenValue(),\n\t\t\t\t\tshownValue(),\n\t\t\t\t\t!rpl::mappers::_1 && rpl::mappers::_2),\n\t\t\t\thideTooltip);\n\t\t});\n\t}\n\n\t_storiesContents.fire(Stories::ContentForSession(\n\t\t&controller()->session(),\n\t\tData::StorySourcesList::NotHidden));\n\n\tconst auto currentSource = [=] {\n\t\tusing List = Data::StorySourcesList;\n\t\treturn _openedFolder ? List::Hidden : List::NotHidden;\n\t};\n\n\trpl::combine(\n\t\t_scroll->positionValue(),\n\t\t_scroll->movementValue(),\n\t\t_storiesExplicitExpandValue.value()\n\t) | rpl::on_next([=](\n\t\t\tUi::ElasticScrollPosition position,\n\t\t\tUi::ElasticScrollMovement movement,\n\t\t\tint explicitlyExpanded) {\n\t\tif (_stories->isHidden()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto overscrollTop = std::max(-position.overscroll, 0);\n\t\tif (overscrollTop > 0 && _storiesExplicitExpand) {\n\t\t\t_scroll->setOverscrollDefaults(\n\t\t\t\t-st::dialogsStoriesFull.height,\n\t\t\t\t0,\n\t\t\t\ttrue);\n\t\t}\n\t\tif (explicitlyExpanded > 0 && explicitlyExpanded < overscrollTop) {\n\t\t\t_storiesExplicitExpandAnimation.stop();\n\t\t\t_storiesExplicitExpand = false;\n\t\t\t_storiesExplicitExpandValue = 0;\n\t\t\treturn;\n\t\t}\n\t\tconst auto above = std::max(explicitlyExpanded, overscrollTop);\n\t\tif (_aboveScrollAdded != above) {\n\t\t\t_aboveScrollAdded = above;\n\t\t\tif (_updateScrollGeometryCached) {\n\t\t\t\t_updateScrollGeometryCached();\n\t\t\t}\n\t\t}\n\t\tusing Phase = Ui::ElasticScrollMovement;\n\t\t_stories->setExpandedHeight(\n\t\t\t_aboveScrollAdded,\n\t\t\t((movement == Phase::Momentum || movement == Phase::Returning)\n\t\t\t\t&& (explicitlyExpanded < above)));\n\t\tif (position.overscroll > 0\n\t\t\t|| (position.value\n\t\t\t\t> (_storiesExplicitExpandScrollTop\n\t\t\t\t\t+ st::dialogsRowHeight))) {\n\t\t\tstoriesToggleExplicitExpand(false);\n\t\t}\n\t\tupdateLockUnlockPosition();\n\t}, lifetime());\n\n\t_stories->collapsedGeometryChanged(\n\t) | rpl::on_next([=] {\n\t\tupdateLockUnlockPosition();\n\t}, lifetime());\n\n\t_stories->clicks(\n\t) | rpl::on_next([=](uint64 id) {\n\t\tcontroller()->openPeerStories(PeerId(int64(id)), currentSource());\n\t}, lifetime());\n\n\t_stories->showMenuRequests(\n\t) | rpl::on_next([=](const Stories::ShowMenuRequest &request) {\n\t\tFillSourceMenu(controller(), request);\n\t}, lifetime());\n\n\t_stories->loadMoreRequests(\n\t) | rpl::on_next([=] {\n\t\tsession().data().stories().loadMore(currentSource());\n\t}, lifetime());\n\n\t_stories->toggleExpandedRequests(\n\t) | rpl::on_next([=](bool expanded) {\n\t\tconst auto position = _scroll->position();\n\t\tif (!expanded) {\n\t\t\t_scroll->setOverscrollDefaults(0, 0);\n\t\t} else if (position.value > 0 || position.overscroll >= 0) {\n\t\t\tstoriesToggleExplicitExpand(true);\n\t\t\t_scroll->setOverscrollDefaults(0, 0);\n\t\t} else {\n\t\t\t_scroll->setOverscrollDefaults(\n\t\t\t\t-st::dialogsStoriesFull.height,\n\t\t\t\t0);\n\t\t}\n\t}, lifetime());\n\n\t_stories->emptyValue() | rpl::skip(1) | rpl::on_next([=] {\n\t\tupdateStoriesVisibility();\n\t}, lifetime());\n\n\t_stories->widthValue() | rpl::on_next([=] {\n\t\tupdateLockUnlockPosition();\n\t}, lifetime());\n}\n\nvoid Widget::storiesToggleExplicitExpand(bool expand) {\n\tif (_storiesExplicitExpand == expand) {\n\t\treturn;\n\t}\n\t_storiesExplicitExpand = expand;\n\tif (!expand) {\n\t\t_scroll->setOverscrollDefaults(0, 0, true);\n\t}\n\tconst auto height = st::dialogsStoriesFull.height;\n\tconst auto duration = kStoriesExpandDuration;\n\t_storiesExplicitExpandScrollTop = _scroll->position().value;\n\t_storiesExplicitExpandAnimation.start([=](float64 value) {\n\t\t_storiesExplicitExpandValue = int(base::SafeRound(value));\n\t}, expand ? 0 : height, expand ? height : 0, duration, anim::sineInOut);\n}\n\nvoid Widget::trackScroll(not_null<Ui::RpWidget*> widget) {\n\twidget->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::TouchBegin\n\t\t\t|| type == QEvent::TouchUpdate\n\t\t\t|| type == QEvent::TouchEnd\n\t\t\t|| type == QEvent::TouchCancel\n\t\t\t|| type == QEvent::Wheel) {\n\t\t\t_scroll->viewportEvent(e);\n\t\t}\n\t}, widget->lifetime());\n}\n\nvoid Widget::setupShortcuts() {\n\tShortcuts::Requests(\n\t) | rpl::filter([=] {\n\t\treturn isActiveWindow()\n\t\t\t&& Ui::InFocusChain(this)\n\t\t\t&& !_childList\n\t\t\t&& !controller()->isLayerShown()\n\t\t\t&& !controller()->window().locked();\n\t}) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {\n\t\tusing Command = Shortcuts::Command;\n\n\t\tif (!controller()->activeChatCurrent()) {\n\t\t\trequest->check(Command::Search) && request->handle([=] {\n\t\t\t\tif (const auto forum = _openedForum) {\n\t\t\t\t\tconst auto history = forum->history();\n\t\t\t\t\tcontroller()->searchInChat(history);\n\t\t\t\t\treturn true;\n\t\t\t\t} else if (!_openedFolder\n\t\t\t\t\t&& !_childList\n\t\t\t\t\t&& _search->isVisible()) {\n\t\t\t\t\t_search->setFocus();\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t});\n\t\t\trequest->check(Command::ShowChatMenu, 1) && request->handle([=] {\n\t\t\t\tif (_inner) {\n\t\t\t\t\tWindow::ActivateWindow(controller());\n\t\t\t\t\t_inner->showPeerMenu();\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\t\t\trequest->check(Command::ShowChatPreview, 1)\n\t\t\t&& request->handle([=] {\n\t\t\t\tif (_inner) {\n\t\t\t\t\tWindow::ActivateWindow(controller());\n\t\t\t\t\treturn _inner->showChatPreview();\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\t}, lifetime());\n}\n\nvoid Widget::fullSearchRefreshOn(rpl::producer<> events) {\n\tstd::move(\n\t\tevents\n\t) | rpl::filter([=] {\n\t\treturn !_searchQuery.isEmpty();\n\t}) | rpl::on_next([=] {\n\t\t_searchTimer.cancel();\n\t\t_searchProcess.cache.clear();\n\t\tconst auto queries = base::take(_searchProcess.queries);\n\t\tfor (const auto &[requestId, query] : queries) {\n\t\t\tsession().api().request(requestId).cancel();\n\t\t}\n\t\t_singleMessageSearch.clear();\n\t\t_searchQuery = QString();\n\t\t_scroll->scrollToY(0);\n\t\tcancelSearchRequest();\n\t\tsearch();\n\t}, lifetime());\n}\n\nvoid Widget::updateControlsVisibility(bool fast) {\n\tupdateLoadMoreChatsVisibility();\n\t_scroll->setVisible(!_suggestions && _hidingSuggestions.empty());\n\tupdateStoriesVisibility();\n\tif ((_openedFolder || _openedForum) && _searchHasFocus) {\n\t\tsetInnerFocus();\n\t}\n\tif (_updateTelegram) {\n\t\t_updateTelegram->show();\n\t}\n\t_searchControls->setVisible(!_openedFolder && !_openedForum);\n\tif (_moreChatsBar) {\n\t\t_moreChatsBar->show();\n\t}\n\tif (_frozenAccountBar) {\n\t\t_frozenAccountBar->show();\n\t}\n\tif (_chatFilters) {\n\t\t_chatFilters->setVisible(!_openedForum);\n\t}\n\tif (_openedFolder || _openedForum) {\n\t\t_subsectionTopBar->show();\n\t\tif (_forumTopShadow) {\n\t\t\t_forumTopShadow->show();\n\t\t}\n\t\tif (_forumGroupCallBar) {\n\t\t\t_forumGroupCallBar->show();\n\t\t}\n\t\tif (_forumRequestsBar) {\n\t\t\t_forumRequestsBar->show();\n\t\t}\n\t\tif (_forumReportBar) {\n\t\t\t_forumReportBar->show();\n\t\t}\n\t} else {\n\t\tupdateLockUnlockVisibility(fast\n\t\t\t? anim::type::instant\n\t\t\t: anim::type::normal);\n\t\tupdateJumpToDateVisibility(fast);\n\t\tupdateSearchFromVisibility(fast);\n\t}\n\tif (_connecting) {\n\t\t_connecting->setForceHidden(false);\n\t}\n\tif (_childList) {\n\t\t_childList->show();\n\t\t_childListShadow->show();\n\t}\n\tif (_hideChildListCanvas) {\n\t\t_hideChildListCanvas->show();\n\t}\n\tif (_childList && _searchHasFocus) {\n\t\tsetInnerFocus();\n\t}\n\tupdateLockUnlockPosition();\n}\n\nvoid Widget::updateLockUnlockPosition() {\n\tif (_lockUnlock->isHidden()) {\n\t\treturn;\n\t}\n\tconst auto stories = (_stories && !_stories->isHidden())\n\t\t? _stories->collapsedGeometryCurrent()\n\t\t: Stories::List::CollapsedGeometry();\n\tconst auto simple = _search->x() + _search->width();\n\tconst auto right = stories.geometry.isEmpty()\n\t\t? simple\n\t\t: anim::interpolate(stories.geometry.x(), simple, stories.expanded);\n\t_lockUnlock->move(\n\t\tright - _lockUnlock->width(),\n\t\tst::dialogsFilterPadding.y());\n}\n\nvoid Widget::updateHasFocus(not_null<QWidget*> focused) {\n\tconst auto has = (focused == _search.data())\n\t\t|| (focused == _search->rawTextEdit());\n\tif (_searchHasFocus != has) {\n\t\t_searchHasFocus = has;\n\t\tif (_postponeProcessSearchFocusChange) {\n\t\t\treturn;\n\t\t} else if (has) {\n\t\t\tprocessSearchFocusChange();\n\t\t} else {\n\t\t\t// Search field may loose focus from the destructor of some\n\t\t\t// widget, in that case we don't want to destroy _suggestions\n\t\t\t// synchronously, because it may lead to a crash.\n\t\t\tcrl::on_main(this, [=] { processSearchFocusChange(); });\n\t\t}\n\t}\n}\n\nvoid Widget::toggleFiltersMenu(bool enabled) {\n\tif (_layout == Layout::Child) {\n\t\tenabled = false;\n\t}\n\tif (const auto id = controller()->windowId(); id.forum() || id.folder()) {\n\t\tenabled = false;\n\t}\n\tif (!enabled == !_chatFilters) {\n\t\treturn;\n\t} else if (enabled) {\n\t\tclass NoScrollPropagationWidget final : public Ui::RpWidget {\n\t\tpublic:\n\t\t\tusing Ui::RpWidget::RpWidget;\n\n\t\tprotected:\n\t\t\tvoid touchEvent(QTouchEvent *e) {\n\t\t\t\te->accept();\n\t\t\t}\n\t\t\tvoid wheelEvent(QWheelEvent *e) override final {\n\t\t\t\te->accept();\n\t\t\t}\n\n\t\t};\n\n\t\t_chatFilters = base::make_unique_q<NoScrollPropagationWidget>(this);\n\t\tconst auto raw = _chatFilters.get();\n\t\tconst auto idBeforeTabs = controller()->activeChatsFilterCurrent();\n\t\tconst auto inner = Ui::AddChatFiltersTabsStrip(\n\t\t\t_chatFilters.get(),\n\t\t\t&session(),\n\t\t\t[this](FilterId id) {\n\t\t\t\t_scroll->scrollToY(0);\n\t\t\t\tif (controller()->activeChatsFilterCurrent() != id) {\n\t\t\t\t\tcontroller()->setActiveChatsFilter(id);\n\t\t\t\t}\n\t\t\t},\n\t\t\tWindow::GifPauseReason::Any,\n\t\t\tcontroller(),\n\t\t\ttrue);\n\t\tif (controller()->activeChatsFilterCurrent() != idBeforeTabs) {\n\t\t\tcontroller()->setActiveChatsFilter(idBeforeTabs);\n\t\t}\n\t\traw->show();\n\t\traw->stackUnder(_scroll);\n\t\traw->resizeToWidth(width());\n\t\tconst auto shadow = Ui::CreateChild<Ui::PlainShadow>(raw);\n\t\tshadow->show();\n\t\tinner->sizeValue() | rpl::on_next([=, this](const QSize &s) {\n\t\t\traw->resize(s);\n\t\t\tshadow->setGeometry(\n\t\t\t\t0,\n\t\t\t\ts.height() - shadow->height(),\n\t\t\t\ts.width(),\n\t\t\t\tshadow->height());\n\t\t\tupdateControlsGeometry();\n\t\t}, _chatFilters->lifetime());\n\t\tupdateControlsGeometry();\n\t} else {\n\t\t_chatFilters = nullptr;\n\t}\n}\n\nbool Widget::cancelSearchByMouseBack() {\n\treturn _searchHasFocus\n\t\t&& !_searchSuggestionsLocked\n\t\t&& !_searchState.inChat\n\t\t&& cancelSearch({ .jumpBackToSearchedChat = true });\n}\n\nvoid Widget::processSearchFocusChange() {\n\t_searchSuggestionsLocked = _suggestions && _suggestions->persist();\n\tupdateCancelSearch();\n\tupdateForceDisplayWide();\n\tupdateSuggestions(anim::type::normal);\n}\n\nvoid Widget::updateSuggestions(anim::type animated) {\n\tconst auto suggest = (_searchHasFocus || _searchSuggestionsLocked)\n\t\t&& !_searchState.inChat\n\t\t&& (_inner->state() == WidgetState::Default);\n\tif (anim::Disabled() || !session().data().chatsListLoaded()) {\n\t\tanimated = anim::type::instant;\n\t}\n\tif (!suggest && _suggestions) {\n\t\tif (animated == anim::type::normal) {\n\t\t\tauto taken = base::take(_suggestions);\n\t\t\ttaken->setVisible(false);\n\t\t\tstoriesExplicitCollapse();\n\t\t\tupdateStoriesVisibility();\n\t\t\tstartWidthAnimation();\n\t\t\ttaken->setVisible(true);\n\t\t\t_suggestions = base::take(taken);\n\n\t\t\t_suggestions->hide(animated, [=, raw = _suggestions.get()] {\n\t\t\t\tstopWidthAnimation();\n\t\t\t\t_hidingSuggestions.erase(\n\t\t\t\t\tranges::remove(\n\t\t\t\t\t\t_hidingSuggestions,\n\t\t\t\t\t\traw,\n\t\t\t\t\t\t&std::unique_ptr<Suggestions>::get),\n\t\t\t\t\tend(_hidingSuggestions));\n\t\t\t\tupdateControlsVisibility();\n\t\t\t});\n\t\t\t_hidingSuggestions.push_back(std::move(_suggestions));\n\t\t} else {\n\t\t\t_suggestions = nullptr;\n\t\t\t_hidingSuggestions.clear();\n\t\t\tstoriesExplicitCollapse();\n\t\t\tupdateControlsVisibility();\n\t\t\t_scroll->show();\n\t\t}\n\t} else if (suggest && !_suggestions) {\n\t\tif (animated == anim::type::normal) {\n\t\t\tstartWidthAnimation();\n\t\t}\n\t\t// Hides stories and passcode lock.\n\t\tupdateStoriesVisibility();\n\t\t_suggestions = std::make_unique<Suggestions>(\n\t\t\tthis,\n\t\t\tcontroller(),\n\t\t\tTopPeersContent(&session()),\n\t\t\tRecentPeersContent(&session()));\n\t\t_suggestions->clearSearchQueryRequests() | rpl::on_next([=] {\n\t\t\tsetSearchQuery(QString());\n\t\t}, _suggestions->lifetime());\n\t\t_searchSuggestionsLocked = false;\n\n\t\trpl::merge(\n\t\t\t_suggestions->topPeerChosen(),\n\t\t\t_suggestions->recentPeerChosen(),\n\t\t\t_suggestions->myChannelChosen(),\n\t\t\t_suggestions->recommendationChosen()\n\t\t) | rpl::on_next([=](not_null<PeerData*> peer) {\n\t\t\tif (_searchSuggestionsLocked\n\t\t\t\t&& (!_suggestions || !_suggestions->persist())) {\n\t\t\t\tprocessSearchFocusChange();\n\t\t\t}\n\t\t\tchosenRow({\n\t\t\t\t.key = peer->owner().history(peer),\n\t\t\t\t.newWindow = base::IsCtrlPressed(),\n\t\t\t});\n\t\t\tif (!_searchSuggestionsLocked && _searchHasFocus) {\n\t\t\t\tsetFocus();\n\t\t\t\tcontroller()->widget()->setInnerFocus();\n\t\t\t}\n\t\t}, _suggestions->lifetime());\n\n\t\trpl::merge(\n\t\t\t_suggestions->openBotMainAppRequests(),\n\t\t\t_suggestions->recentAppChosen()\n\t\t) | rpl::on_next([=](not_null<PeerData*> peer) {\n\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\tif (const auto info = user->botInfo.get()) {\n\t\t\t\t\tif (info->hasMainApp) {\n\t\t\t\t\t\topenBotMainApp(user);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tchosenRow({\n\t\t\t\t.key = peer->owner().history(peer),\n\t\t\t\t.newWindow = base::IsCtrlPressed(),\n\t\t\t});\n\t\t}, _suggestions->lifetime());\n\n\t\t_suggestions->popularAppChosen(\n\t\t) | rpl::on_next([=](not_null<PeerData*> peer) {\n\t\t\tcontroller()->showPeerInfo(peer);\n\t\t}, _suggestions->lifetime());\n\n\t\t_suggestions->closeRequests() | rpl::on_next([=] {\n\t\t\tcloseSuggestions();\n\t\t}, _suggestions->lifetime());\n\n\t\tupdateControlsGeometry();\n\n\t\t_suggestions->show(animated, [=] {\n\t\t\tstopWidthAnimation();\n\t\t});\n\t\t_scroll->hide();\n\t} else {\n\t\tupdateStoriesVisibility();\n\t}\n}\n\nvoid Widget::closeSuggestions() {\n\t_searchSuggestionsLocked = false;\n\t_searchHasFocus = false;\n\tsetFocus();\n\tupdateForceDisplayWide();\n\tupdateCancelSearch();\n\tupdateSuggestions(anim::type::normal);\n}\n\nvoid Widget::openBotMainApp(not_null<UserData*> bot) {\n\tsession().attachWebView().open({\n\t\t.bot = bot,\n\t\t.context = {\n\t\t\t.controller = controller(),\n\t\t\t.maySkipConfirmation = true,\n\t\t},\n\t\t.source = InlineBots::WebViewSourceBotProfile(),\n\t});\n}\n\nvoid Widget::changeOpenedSubsection(\n\t\tFnMut<void()> change,\n\t\tbool fromRight,\n\t\tanim::type animated) {\n\tif (isHidden()) {\n\t\tanimated = anim::type::instant;\n\t}\n\tauto oldContentCache = QPixmap();\n\tconst auto showDirection = fromRight\n\t\t? Window::SlideDirection::FromRight\n\t\t: Window::SlideDirection::FromLeft;\n\tif (animated == anim::type::normal) {\n\t\tif (_connecting) {\n\t\t\t_connecting->setForceHidden(true);\n\t\t}\n\t\toldContentCache = grabForFolderSlideAnimation();\n\t}\n\t//_scroll->verticalScrollBar()->setMinimum(0);\n\t_showAnimation = nullptr;\n\tdestroyChildListCanvas();\n\tchange();\n\trefreshTopBars();\n\tupdateControlsVisibility(true);\n\t_peerSearch.clear();\n\t_api.request(base::take(_topicSearchRequest)).cancel();\n\tif (animated == anim::type::normal) {\n\t\tif (_connecting) {\n\t\t\t_connecting->setForceHidden(true);\n\t\t}\n\t\tauto newContentCache = grabForFolderSlideAnimation();\n\t\tif (_connecting) {\n\t\t\t_connecting->setForceHidden(false);\n\t\t}\n\t\tstartSlideAnimation(\n\t\t\tstd::move(oldContentCache),\n\t\t\tstd::move(newContentCache),\n\t\t\tshowDirection);\n\t}\n}\n\nvoid Widget::destroyChildListCanvas() {\n\t_childListShown = 0.;\n\t_hideChildListCanvas = nullptr;\n}\n\nvoid Widget::changeOpenedFolder(Data::Folder *folder, anim::type animated) {\n\tif (_openedFolder == folder) {\n\t\treturn;\n\t}\n\tchangeOpenedSubsection([&] {\n\t\tcancelSearch({ .forceFullCancel = true });\n\t\tcloseChildList(anim::type::instant);\n\t\tcontroller()->closeForum();\n\t\t_openedFolder = folder;\n\t\t_inner->changeOpenedFolder(folder);\n\t\tif (_stories) {\n\t\t\tstoriesExplicitCollapse();\n\t\t}\n\t\tupdateFrozenAccountBar();\n\t\tupdateTopBarSuggestions();\n\t}, (folder != nullptr), animated);\n}\n\nvoid Widget::storiesExplicitCollapse() {\n\tif (_storiesExplicitExpand) {\n\t\tstoriesToggleExplicitExpand(false);\n\t} else if (_stories) {\n\t\tusing Type = Ui::ElasticScroll::OverscrollType;\n\t\t_scroll->setOverscrollDefaults(0, 0);\n\t\t_scroll->setOverscrollTypes(Type::None, Type::Real);\n\t\t_scroll->setOverscrollTypes(\n\t\t\t_stories->isHidden() ? Type::Real : Type::Virtual,\n\t\t\tType::Real);\n\t}\n\t_storiesExplicitExpandAnimation.stop();\n\t_storiesExplicitExpandValue = 0;\n\n\tusing List = Data::StorySourcesList;\n\tcollectStoriesUserpicsViews(_openedFolder\n\t\t? List::NotHidden\n\t\t: List::Hidden);\n\t_storiesContents.fire(Stories::ContentForSession(\n\t\t&session(),\n\t\t_openedFolder ? List::Hidden : List::NotHidden));\n}\n\nvoid Widget::collectStoriesUserpicsViews(Data::StorySourcesList list) {\n\tauto &map = (list == Data::StorySourcesList::Hidden)\n\t\t? _storiesUserpicsViewsHidden\n\t\t: _storiesUserpicsViewsShown;\n\tmap.clear();\n\tauto &owner = session().data();\n\tfor (const auto &source : owner.stories().sources(list)) {\n\t\tif (const auto peer = owner.peerLoaded(source.id)) {\n\t\t\tif (auto view = peer->activeUserpicView(); view.cloud) {\n\t\t\t\tmap.emplace(source.id, std::move(view));\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) {\n\tif (_openedForum == forum) {\n\t\treturn;\n\t}\n\tchangeOpenedSubsection([&] {\n\t\tcancelSearch({ .forceFullCancel = true });\n\t\tcloseChildList(anim::type::instant);\n\t\t_openedForum = forum;\n\t\t_searchState.tab = forum\n\t\t\t? ChatSearchTab::ThisPeer\n\t\t\t: ChatSearchTab::MyMessages;\n\t\t_searchWithPostsPreview = computeSearchWithPostsPreview();\n\t\t_api.request(base::take(_topicSearchRequest)).cancel();\n\t\t_inner->changeOpenedForum(forum);\n\t\tstoriesToggleExplicitExpand(false);\n\t\tupdateFrozenAccountBar();\n\t\tupdateTopBarSuggestions();\n\t\tupdateStoriesVisibility();\n\t}, (forum != nullptr), animated);\n}\n\nvoid Widget::hideChildList() {\n\tif (_childList) {\n\t\tcontroller()->closeForum();\n\t}\n}\n\nvoid Widget::refreshTopBars() {\n\tif (_openedFolder || _openedForum) {\n\t\tif (!_subsectionTopBar) {\n\t\t\t_subsectionTopBar.create(this, controller());\n\t\t\tif (_stories) {\n\t\t\t\t_stories->raise();\n\t\t\t}\n\t\t\t_subsectionTopBar->searchCancelled(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tescape();\n\t\t\t}, _subsectionTopBar->lifetime());\n\t\t\t_subsectionTopBar->searchSubmitted(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tsubmit();\n\t\t\t}, _subsectionTopBar->lifetime());\n\t\t\t_subsectionTopBar->searchQuery(\n\t\t\t) | rpl::on_next([=](QString query) {\n\t\t\t\tapplySearchUpdate();\n\t\t\t}, _subsectionTopBar->lifetime());\n\t\t\t_subsectionTopBar->jumpToDateRequest(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tshowCalendar();\n\t\t\t}, _subsectionTopBar->lifetime());\n\t\t\t_subsectionTopBar->chooseFromUserRequest(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tshowSearchFrom();\n\t\t\t}, _subsectionTopBar->lifetime());\n\t\t\tupdateControlsGeometry();\n\t\t}\n\t\tconst auto history = _openedForum\n\t\t\t? _openedForum->history().get()\n\t\t\t: nullptr;\n\t\t_subsectionTopBar->setActiveChat(\n\t\t\tHistoryView::TopBarWidget::ActiveChat{\n\t\t\t\t.key = (_openedForum\n\t\t\t\t\t? Dialogs::Key(history)\n\t\t\t\t\t: Dialogs::Key(_openedFolder)),\n\t\t\t\t.section = Dialogs::EntryState::Section::ChatsList,\n\t\t\t}, history ? history->sendActionPainter() : nullptr);\n\t\tif (_forumSearchRequested) {\n\t\t\tshowSearchInTopBar(anim::type::instant);\n\t\t}\n\t} else if (_subsectionTopBar) {\n\t\tif (_subsectionTopBar->searchHasFocus()) {\n\t\t\tsetFocus();\n\t\t}\n\t\t_subsectionTopBar.destroy();\n\t\tupdateSearchFromVisibility(true);\n\t}\n\t_forumSearchRequested = false;\n\tif (_openedForum && _openedForum->peer()->isChannel()) {\n\t\tconst auto peer = _openedForum->peer();\n\t\tpeer->updateFull();\n\n\t\t_forumReportBar = std::make_unique<HistoryView::ContactStatus>(\n\t\t\tcontroller(),\n\t\t\tthis,\n\t\t\tpeer,\n\t\t\ttrue);\n\t\t_forumRequestsBar = std::make_unique<Ui::RequestsBar>(\n\t\t\tthis,\n\t\t\tHistoryView::RequestsBarContentByPeer(\n\t\t\t\tpeer,\n\t\t\t\tst::historyRequestsUserpics.size,\n\t\t\t\ttrue));\n\t\t_forumGroupCallBar = std::make_unique<Ui::GroupCallBar>(\n\t\t\tthis,\n\t\t\tHistoryView::GroupCallBarContentByPeer(\n\t\t\t\tpeer,\n\t\t\t\tst::historyGroupCallUserpics.size,\n\t\t\t\ttrue),\n\t\t\tCore::App().appDeactivatedValue());\n\t\t_forumTopShadow = std::make_unique<Ui::PlainShadow>(this);\n\n\t\t_forumRequestsBar->barClicks(\n\t\t) | rpl::on_next([=] {\n\t\t\tRequestsBoxController::Start(controller(), peer);\n\t\t}, _forumRequestsBar->lifetime());\n\n\t\trpl::merge(\n\t\t\t_forumGroupCallBar->barClicks(),\n\t\t\t_forumGroupCallBar->joinClicks()\n\t\t) | rpl::on_next([=] {\n\t\t\tif (peer->groupCall()) {\n\t\t\t\tcontroller()->startOrJoinGroupCall(peer);\n\t\t\t}\n\t\t}, _forumGroupCallBar->lifetime());\n\n\t\tif (_showAnimation) {\n\t\t\t_forumTopShadow->hide();\n\t\t\t_forumGroupCallBar->hide();\n\t\t\t_forumRequestsBar->hide();\n\t\t\t_forumReportBar->bar().hide();\n\t\t} else {\n\t\t\t_forumTopShadow->show();\n\t\t\t_forumGroupCallBar->show();\n\t\t\t_forumRequestsBar->show();\n\t\t\t_forumReportBar->show();\n\t\t\t_forumGroupCallBar->finishAnimating();\n\t\t\t_forumRequestsBar->finishAnimating();\n\t\t}\n\n\t\trpl::combine(\n\t\t\t_forumGroupCallBar->heightValue(),\n\t\t\t_forumRequestsBar->heightValue(),\n\t\t\t_forumReportBar->bar().heightValue()\n\t\t) | rpl::on_next([=] {\n\t\t\tupdateControlsGeometry();\n\t\t}, _forumRequestsBar->lifetime());\n\t} else {\n\t\t_forumTopShadow = nullptr;\n\t\t_forumGroupCallBar = nullptr;\n\t\t_forumRequestsBar = nullptr;\n\t\t_forumReportBar = nullptr;\n\t\tupdateControlsGeometry();\n\t}\n}\n\nvoid Widget::showSearchInTopBar(anim::type animated) {\n\tExpects(_subsectionTopBar != nullptr);\n\n\t_subsectionTopBar->toggleSearch(true, animated);\n\tupdateForceDisplayWide();\n}\n\nQPixmap Widget::grabForFolderSlideAnimation() {\n\tconst auto hidden = _scrollToTop->isHidden();\n\tif (!hidden) {\n\t\t_scrollToTop->hide();\n\t}\n\n\tconst auto rect = QRect(0, 0, width(), rect::bottom(_scroll));\n\tauto result = Ui::GrabWidget(this, rect);\n\n\tif (!hidden) {\n\t\t_scrollToTop->show();\n\t}\n\treturn result;\n}\n\nvoid Widget::checkUpdateStatus() {\n\tExpects(!Core::UpdaterDisabled());\n\n\tif (_layout == Layout::Child) {\n\t\treturn;\n\t}\n\n\tusing Checker = Core::UpdateChecker;\n\tif (Checker().state() == Checker::State::Ready) {\n\t\tif (_updateTelegram) {\n\t\t\treturn;\n\t\t}\n\t\t_updateTelegram.create(\n\t\t\tthis,\n\t\t\ttr::lng_update_telegram(tr::now),\n\t\t\tst::dialogsUpdateButton,\n\t\t\tst::dialogsInstallUpdate,\n\t\t\tst::dialogsInstallUpdateOver,\n\t\t\ttrue);\n\t\t_updateTelegram->show();\n\t\t_updateTelegram->setClickedCallback([] {\n\t\t\tCore::checkReadyUpdate();\n\t\t\tCore::Restart();\n\t\t});\n\t\tif (_connecting) {\n\t\t\t_connecting->raise();\n\t\t}\n\t} else {\n\t\tif (!_updateTelegram) {\n\t\t\treturn;\n\t\t}\n\t\t_updateTelegram.destroy();\n\t}\n\tupdateControlsGeometry();\n}\n\nvoid Widget::setInnerFocus(bool unfocusSearch) {\n\tif (_childList) {\n\t\t_childList->setInnerFocus();\n\t} else if (_subsectionTopBar && _subsectionTopBar->searchSetFocus()) {\n\t\treturn;\n\t} else if (!unfocusSearch\n\t\t&& (!_search->getLastText().isEmpty()\n\t\t\t|| _searchState.inChat\n\t\t\t|| _searchHasFocus\n\t\t\t|| _searchSuggestionsLocked)) {\n\t\t_search->setFocus();\n\t} else {\n\t\tsetFocus();\n\t}\n}\n\nbool Widget::searchHasFocus() const {\n\treturn _searchHasFocus;\n}\n\nData::Forum *Widget::openedForum() const {\n\treturn _openedForum;\n}\n\nvoid Widget::jumpToTop(bool belowPinned) {\n\tif (session().supportMode()) {\n\t\treturn;\n\t}\n\tif ((_searchState.query.trimmed().isEmpty() && !_searchState.inChat)) {\n\t\tauto to = 0;\n\t\tif (belowPinned) {\n\t\t\tconst auto list = _openedForum\n\t\t\t\t? _openedForum->topicsList()\n\t\t\t\t: controller()->activeChatsFilterCurrent()\n\t\t\t\t? session().data().chatsFilters().chatsList(\n\t\t\t\t\tcontroller()->activeChatsFilterCurrent())\n\t\t\t\t: session().data().chatsList(_openedFolder);\n\t\t\tconst auto count = int(list->pinned()->order().size());\n\t\t\tconst auto row = _inner->st()->height;\n\t\t\tconst auto min = (row * (count * 2 + 1) - _scroll->height()) / 2;\n\t\t\tif (_scroll->scrollTop() <= min) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Don't jump too high up, below the pinned chats.\n\t\t\tto = std::max(min, to);\n\t\t}\n\t\t_scrollToAnimation.stop();\n\t\t_scroll->scrollToY(to);\n\t}\n}\n\nvoid Widget::raiseWithTooltip() {\n\traise();\n\tif (_stories) {\n\t\tUi::PostponeCall(this, [=] {\n\t\t\t_stories->raiseTooltip();\n\t\t});\n\t}\n}\n\nvoid Widget::scrollToDefault(bool verytop) {\n\tif (verytop) {\n\t\t//_scroll->verticalScrollBar()->setMinimum(0);\n\t}\n\t_scrollToAnimation.stop();\n\tauto scrollTop = _scroll->scrollTop();\n\tconst auto scrollTo = 0;\n\tif (scrollTop == scrollTo) {\n\t\treturn;\n\t}\n\tconst auto maxAnimatedDelta = _scroll->height();\n\tif (scrollTo + maxAnimatedDelta < scrollTop) {\n\t\tscrollTop = scrollTo + maxAnimatedDelta;\n\t\t_scroll->scrollToY(scrollTop);\n\t}\n\n\tstartScrollUpButtonAnimation(false);\n\n\tconst auto scroll = [=] {\n\t\tconst auto animated = qRound(_scrollToAnimation.value(scrollTo));\n\t\tconst auto animatedDelta = animated - scrollTo;\n\t\tconst auto realDelta = _scroll->scrollTop() - scrollTo;\n\t\tif (base::OppositeSigns(realDelta, animatedDelta)) {\n\t\t\t// We scrolled manually to the other side of target 'scrollTo'.\n\t\t\t_scrollToAnimation.stop();\n\t\t} else if (std::abs(realDelta) > std::abs(animatedDelta)) {\n\t\t\t// We scroll by animation only if it gets us closer to target.\n\t\t\t_scroll->scrollToY(animated);\n\t\t}\n\t};\n\n\t_scrollAnimationTo = scrollTo;\n\t_scrollToAnimation.start(\n\t\tscroll,\n\t\tscrollTop,\n\t\tscrollTo,\n\t\tst::slideDuration,\n\t\tanim::sineInOut);\n}\n\n[[nodiscard]] QPixmap Widget::grabNonNarrowScrollFrame() {\n\tauto scrollGeometry = _scroll->geometry();\n\tconst auto top = _searchControls->y() + _searchControls->height();\n\tconst auto skip = scrollGeometry.y() - top;\n\tauto wideGeometry = QRect(\n\t\tscrollGeometry.x(),\n\t\tscrollGeometry.y(),\n\t\tstd::max(scrollGeometry.width(), st::columnMinimalWidthLeft),\n\t\tscrollGeometry.height());\n\t_scroll->setGeometry(wideGeometry);\n\t_inner->resize(wideGeometry.width(), _inner->height());\n\t_inner->setNarrowRatio(0.);\n\tUi::SendPendingMoveResizeEvents(_scroll);\n\tconst auto grabSize = QSize(\n\t\twideGeometry.width(),\n\t\tskip + wideGeometry.height());\n\tauto image = QImage(\n\t\tgrabSize * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\timage.fill(Qt::transparent);\n\t{\n\t\tQPainter p(&image);\n\t\tUi::RenderWidget(\n\t\t\tp,\n\t\t\tthis,\n\t\t\tQPoint(),\n\t\t\tQRect(0, top, wideGeometry.width(), skip));\n\t\tif (_chatFilters) {\n\t\t\tUi::RenderWidget(\n\t\t\t\tp,\n\t\t\t\t_chatFilters,\n\t\t\t\tQPoint(0, skip - _chatFilters->height()));\n\t\t}\n\t\tUi::RenderWidget(p, _scroll, QPoint(0, skip));\n\t}\n\tif (scrollGeometry != wideGeometry) {\n\t\t_scroll->setGeometry(scrollGeometry);\n\t\tupdateControlsGeometry();\n\t}\n\treturn Ui::PixmapFromImage(std::move(image));\n}\n\nvoid Widget::startWidthAnimation() {\n\tif (!_widthAnimationCache.isNull()) {\n\t\treturn;\n\t}\n\t_widthAnimationCache = grabNonNarrowScrollFrame();\n\t_scroll->hide();\n\tif (_frozenAccountBar) {\n\t\t_frozenAccountBar->hide();\n\t}\n\tif (_chatFilters) {\n\t\t_chatFilters->hide();\n\t}\n\tupdateStoriesVisibility();\n}\n\nvoid Widget::stopWidthAnimation() {\n\t_widthAnimationCache = QPixmap();\n\tif (!_showAnimation) {\n\t\t_scroll->setVisible(!_suggestions);\n\t\tif (_frozenAccountBar) {\n\t\t\t_frozenAccountBar->setVisible(!_suggestions);\n\t\t}\n\t\tif (_chatFilters) {\n\t\t\t_chatFilters->setVisible(!_suggestions);\n\t\t}\n\t}\n\tupdateStoriesVisibility();\n\tupdate();\n}\n\nvoid Widget::updateStoriesVisibility() {\n\tupdateLockUnlockVisibility(anim::type::normal);\n\tif (!_stories) {\n\t\treturn;\n\t}\n\tconst auto widthAnimation = !_widthAnimationCache.isNull();\n\tconst auto suggestionsAnimation = widthAnimation\n\t\t&& (!_suggestions || !_hidingSuggestions.empty());\n\tconst auto hiddenInstant = _showAnimation\n\t\t|| _openedForum\n\t\t|| (widthAnimation && !suggestionsAnimation)\n\t\t|| _childList\n\t\t|| (Core::App().settings().fork().archivedStoriesAreHidden()\n\t\t\t? !!_openedFolder\n\t\t\t: false)\n\t\t|| _stories->empty()\n\t\t|| (_scroll->position().overscroll < -st::dialogsFilterSkip);\n\tconst auto hiddenAnimated = _searchHasFocus\n\t\t|| _searchSuggestionsLocked\n\t\t|| !_searchState.query.isEmpty()\n\t\t|| _searchState.inChat\n\t\t|| suggestionsAnimation;\n\tconst auto hidden = hiddenInstant || hiddenAnimated;\n\tconst auto changed = (_stories->toggledHidden() != hidden);\n\t_stories->setToggledHidden(hiddenInstant, hiddenAnimated);\n\tif (changed) {\n\t\tusing Type = Ui::ElasticScroll::OverscrollType;\n\t\tif (hidden) {\n\t\t\t_scroll->setOverscrollDefaults(0, 0);\n\t\t\t_scroll->setOverscrollTypes(Type::Real, Type::Real);\n\t\t\tif (_scroll->position().overscroll < 0) {\n\t\t\t\t_scroll->scrollToY(0);\n\t\t\t}\n\t\t\t_scroll->update();\n\t\t} else {\n\t\t\t_scroll->setOverscrollDefaults(0, 0);\n\t\t\t_scroll->setOverscrollTypes(Type::Virtual, Type::Real);\n\t\t\t_storiesExplicitExpandValue.force_assign(\n\t\t\t\t_storiesExplicitExpandValue.current());\n\t\t}\n\t\tif (_aboveScrollAdded > 0 && _updateScrollGeometryCached) {\n\t\t\t_updateScrollGeometryCached();\n\t\t}\n\t\tupdateLockUnlockPosition();\n\t}\n}\n\nvoid Widget::showFast() {\n\tif (isHidden()) {\n\t\t_inner->clearSelection();\n\t}\n\tshow();\n}\n\nrpl::producer<float64> Widget::shownProgressValue() const {\n\treturn _shownProgressValue.value();\n}\n\nvoid Widget::showAnimated(\n\t\tWindow::SlideDirection direction,\n\t\tconst Window::SectionSlideParams &params) {\n\t_showAnimation = nullptr;\n\n\tauto oldContentCache = params.oldContentCache;\n\tshowFast();\n\tauto newContentCache = Ui::GrabWidget(this);\n\n\tif (_updateTelegram) {\n\t\t_updateTelegram->hide();\n\t}\n\tif (_connecting) {\n\t\t_connecting->setForceHidden(true);\n\t}\n\tif (_childList) {\n\t\t_childList->hide();\n\t\t_childListShadow->hide();\n\t}\n\t_shownProgressValue = 0.;\n\tstartSlideAnimation(\n\t\tstd::move(oldContentCache),\n\t\tstd::move(newContentCache),\n\t\tdirection);\n}\n\nvoid Widget::startSlideAnimation(\n\t\tQPixmap oldContentCache,\n\t\tQPixmap newContentCache,\n\t\tWindow::SlideDirection direction) {\n\t_scroll->hide();\n\tif (_stories) {\n\t\t_stories->setToggledHidden(true, false);\n\t}\n\t_searchControls->hide();\n\tif (_subsectionTopBar) {\n\t\t_subsectionTopBar->hide();\n\t}\n\tif (_moreChatsBar) {\n\t\t_moreChatsBar->hide();\n\t}\n\tif (_frozenAccountBar) {\n\t\t_frozenAccountBar->hide();\n\t}\n\tif (_chatFilters) {\n\t\t_chatFilters->hide();\n\t}\n\tif (_forumTopShadow) {\n\t\t_forumTopShadow->hide();\n\t}\n\tif (_forumGroupCallBar) {\n\t\t_forumGroupCallBar->hide();\n\t}\n\tif (_forumRequestsBar) {\n\t\t_forumRequestsBar->hide();\n\t}\n\tif (_forumReportBar) {\n\t\t_forumReportBar->bar().hide();\n\t}\n\n\t_showAnimation = std::make_unique<Window::SlideAnimation>();\n\t_showAnimation->setDirection(direction);\n\t_showAnimation->setRepaintCallback([=] {\n\t\tif (_shownProgressValue.current() < 1.) {\n\t\t\t_shownProgressValue = _showAnimation->progress();\n\t\t}\n\t\tupdate();\n\t});\n\t_showAnimation->setFinishedCallback([=] { slideFinished(); });\n\t_showAnimation->setPixmaps(oldContentCache, newContentCache);\n\t_showAnimation->start();\n}\n\nbool Widget::floatPlayerHandleWheelEvent(QEvent *e) {\n\treturn _scroll->viewportEvent(e);\n}\n\nQRect Widget::floatPlayerAvailableRect() {\n\treturn mapToGlobal(_scroll->geometry());\n}\n\nvoid Widget::slideFinished() {\n\t_showAnimation = nullptr;\n\t_shownProgressValue = 1.;\n\tupdateControlsVisibility(true);\n\tif ((!_subsectionTopBar || !_subsectionTopBar->searchHasFocus())\n\t\t&& !_searchHasFocus) {\n\t\tcontroller()->widget()->setInnerFocus();\n\t}\n}\n\nvoid Widget::escape() {\n\tif (!cancelSearch({ .jumpBackToSearchedChat = true })) {\n\t\tif (const auto forum = controller()->shownForum().current()) {\n\t\t\tconst auto id = controller()->windowId();\n\t\t\tconst auto initial = id.forum();\n\t\t\tif (!initial) {\n\t\t\t\tcontroller()->closeForum();\n\t\t\t} else if (initial != forum) {\n\t\t\t\tcontroller()->showForum(initial);\n\t\t\t}\n\t\t} else if (controller()->openedFolder().current()) {\n\t\t\tif (!controller()->windowId().folder()) {\n\t\t\t\tcontroller()->closeFolder();\n\t\t\t}\n\t\t} else if (controller()->activeChatEntryCurrent().key) {\n\t\t\tcontroller()->content()->dialogsCancelled();\n\t\t} else if (controller()->isPrimary()) {\n\t\t\tconst auto filters = &session().data().chatsFilters();\n\t\t\tconst auto &list = filters->list();\n\t\t\tconst auto first = list.empty() ? FilterId() : list.front().id();\n\t\t\tif (controller()->activeChatsFilterCurrent() != first) {\n\t\t\t\tcontroller()->setActiveChatsFilter(first);\n\t\t\t}\n\t\t}\n\t} else if (!_searchState.inChat\n\t\t&& controller()->activeChatEntryCurrent().key) {\n\t\tcontroller()->content()->dialogsCancelled();\n\t}\n}\n\nvoid Widget::submit() {\n\tif (_suggestions) {\n\t\t_suggestions->chooseRow();\n\t\treturn;\n\t} else if (_inner->chooseRow()) {\n\t\treturn;\n\t}\n\tconst auto state = _inner->state();\n\tif (state == WidgetState::Default\n\t\t|| (state == WidgetState::Filtered\n\t\t\t&& _inner->hasFilteredResults())) {\n\t\t_inner->selectSkip(1);\n\t\t_inner->chooseRow();\n\t} else {\n\t\tsearch();\n\t}\n}\n\nvoid Widget::refreshLoadMoreButton(bool mayBlock, bool isBlocked) {\n\tif (_layout == Layout::Child) {\n\t\treturn;\n\t}\n\n\tif (!mayBlock) {\n\t\tif (_loadMoreChats) {\n\t\t\t_loadMoreChats.destroy();\n\t\t\tupdateControlsGeometry();\n\t\t}\n\t\treturn;\n\t}\n\tif (!_loadMoreChats) {\n\t\t_loadMoreChats.create(\n\t\t\tthis,\n\t\t\t\"Load more\",\n\t\t\tst::dialogsLoadMoreButton,\n\t\t\tst::dialogsLoadMore,\n\t\t\tst::dialogsLoadMore,\n\t\t\tfalse);\n\t\t_loadMoreChats->show();\n\t\t_loadMoreChats->addClickHandler([=] {\n\t\t\tloadMoreBlockedByDate();\n\t\t});\n\t\tupdateControlsGeometry();\n\t}\n\tconst auto loading = !isBlocked;\n\t_loadMoreChats->setDisabled(loading);\n\t_loadMoreChats->setText(loading ? \"Loading...\" : \"Load more\");\n}\n\nvoid Widget::loadMoreBlockedByDate() {\n\tif (!_loadMoreChats\n\t\t|| _loadMoreChats->isDisabled()\n\t\t|| _loadMoreChats->isHidden()) {\n\t\treturn;\n\t}\n\tsession().api().requestMoreBlockedByDateDialogs();\n}\n\nbool Widget::search(bool inCache, SearchRequestDelay delay) {\n\t_processingSearch = true;\n\tconst auto guard = gsl::finally([&] {\n\t\t_processingSearch = false;\n\t\tlistScrollUpdated();\n\t});\n\n\tauto result = false;\n\tconst auto query = _searchState.query.trimmed();\n\tconst auto trimmed = (query.isEmpty() || query[0] != '#')\n\t\t? query\n\t\t: query.mid(1).trimmed();\n\tconst auto inPeer = searchInPeer();\n\tconst auto fromPeer = searchFromPeer();\n\tconst auto &inTags = searchInTags();\n\tconst auto tab = _searchState.tab;\n\tconst auto filter = _searchState.filter;\n\tconst auto fromStartType = SearchRequestType{\n\t\t.start = true,\n\t\t.peer = (inPeer != nullptr),\n\t};\n\tif (trimmed.isEmpty() && !fromPeer && inTags.empty()) {\n\t\tcancelSearchRequest();\n\n\t\t// Otherwise inside first searchApplyEmpty we call searchMode(),\n\t\t// which tries to load migrated search results for empty query.\n\t\t_migratedProcess.full = true;\n\n\t\tsearchApplyEmpty(fromStartType, currentSearchProcess());\n\t\tif (_searchInMigrated) {\n\t\t\tconst auto type = SearchRequestType{\n\t\t\t\t.migrated = true,\n\t\t\t\t.start = true,\n\t\t\t};\n\t\t\tsearchApplyEmpty(type, &_migratedProcess);\n\t\t}\n\t\tif (_searchWithPostsPreview) {\n\t\t\tsearchApplyEmpty(\n\t\t\t\t{ .posts = true, .start = true },\n\t\t\t\t&_postsProcess);\n\t\t}\n\t\t_peerSearch.clear();\n\t\t_api.request(base::take(_topicSearchRequest)).cancel();\n\t\tpeerSearchReceived({});\n\t\treturn true;\n\t} else if (inCache) {\n\t\tconst auto success = _singleMessageSearch.lookup(query, [=] {\n\t\t\tsearchRequested(delay);\n\t\t});\n\t\tif (!success) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto process = currentSearchProcess();\n\t\tconst auto i = process->cache.find(query);\n\t\tif (i != process->cache.end()) {\n\t\t\t_searchQuery = query;\n\t\t\t_searchQueryFrom = fromPeer;\n\t\t\t_searchQueryTags = inTags;\n\t\t\t_searchQueryTab = tab;\n\t\t\t_searchQueryFilter = filter;\n\t\t\tprocess->nextRate = 0;\n\t\t\tprocess->full = false;\n\t\t\t_migratedProcess.full = false;\n\t\t\tcancelSearchRequest();\n\t\t\tsearchReceived(fromStartType, i->second, process, true);\n\t\t\tresult = true;\n\t\t}\n\t} else if (_searchQuery != query\n\t\t|| _searchQueryFrom != fromPeer\n\t\t|| _searchQueryTags != inTags\n\t\t|| _searchQueryTab != tab\n\t\t|| _searchQueryFilter != filter) {\n\t\tconst auto process = currentSearchProcess();\n\t\t_searchQuery = query;\n\t\t_searchQueryFrom = fromPeer;\n\t\t_searchQueryTags = inTags;\n\t\t_searchQueryTab = tab;\n\t\t_searchQueryFilter = filter;\n\t\tprocess->nextRate = 0;\n\t\tprocess->full = false;\n\t\t_migratedProcess.full = false;\n\t\tcancelSearchRequest();\n\t\tif (inPeer) {\n\t\t\tconst auto topic = searchInTopic();\n\t\t\tauto &histories = session().data().histories();\n\t\t\tconst auto type = Data::Histories::RequestType::History;\n\t\t\tconst auto history = session().data().history(inPeer);\n\t\t\tconst auto sublist = _openedForum\n\t\t\t\t? nullptr\n\t\t\t\t: _searchState.inChat.sublist();\n\t\t\tconst auto fromPeer = sublist ? nullptr : _searchQueryFrom;\n\t\t\tconst auto savedPeer = sublist\n\t\t\t\t? sublist->sublistPeer().get()\n\t\t\t\t: nullptr;\n\t\t\t_historiesRequest = histories.sendRequest(history, type, [=](\n\t\t\t\t\tFn<void()> finish) {\n\t\t\t\tconst auto type = SearchRequestType{\n\t\t\t\t\t.start = true,\n\t\t\t\t\t.peer = true,\n\t\t\t\t};\n\t\t\t\tusing Flag = MTPmessages_Search::Flag;\n\t\t\t\tprocess->requestId = session().api().request(\n\t\t\t\t\tMTPmessages_Search(\n\t\t\t\t\t\tMTP_flags((topic ? Flag::f_top_msg_id : Flag())\n\t\t\t\t\t\t\t| (fromPeer ? Flag::f_from_id : Flag())\n\t\t\t\t\t\t\t| (savedPeer ? Flag::f_saved_peer_id : Flag())\n\t\t\t\t\t\t\t| (_searchQueryTags.empty()\n\t\t\t\t\t\t\t\t? Flag()\n\t\t\t\t\t\t\t\t: Flag::f_saved_reaction)),\n\t\t\t\t\t\tinPeer->input(),\n\t\t\t\t\t\tMTP_string(_searchQuery),\n\t\t\t\t\t\t(fromPeer ? fromPeer->input() : MTP_inputPeerEmpty()),\n\t\t\t\t\t\t(savedPeer ? savedPeer->input() : MTP_inputPeerEmpty()),\n\t\t\t\t\t\tMTP_vector_from_range(\n\t\t\t\t\t\t\t_searchQueryTags | ranges::views::transform(\n\t\t\t\t\t\t\t\tData::ReactionToMTP\n\t\t\t\t\t\t\t)),\n\t\t\t\t\t\tMTP_int(topic ? topic->rootId() : 0),\n\t\t\t\t\t\tMTP_inputMessagesFilterEmpty(),\n\t\t\t\t\t\tMTP_int(0), // min_date\n\t\t\t\t\t\tMTP_int(0), // max_date\n\t\t\t\t\t\tMTP_int(0), // offset_id\n\t\t\t\t\t\tMTP_int(0), // add_offset\n\t\t\t\t\t\tMTP_int(kSearchPerPage),\n\t\t\t\t\t\tMTP_int(0), // max_id\n\t\t\t\t\t\tMTP_int(0), // min_id\n\t\t\t\t\t\tMTP_long(0)) // hash\n\t\t\t\t).done([=](const MTPmessages_Messages &result) {\n\t\t\t\t\t_historiesRequest = 0;\n\t\t\t\t\tsearchReceived(type, result, process);\n\t\t\t\t\tfinish();\n\t\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\t\t_historiesRequest = 0;\n\t\t\t\t\tsearchFailed(type, error, process);\n\t\t\t\t\tfinish();\n\t\t\t\t}).send();\n\t\t\t\tprocess->queries.emplace(process->requestId, _searchQuery);\n\t\t\t\treturn process->requestId;\n\t\t\t});\n\t\t} else if (_searchState.tab == ChatSearchTab::PublicPosts) {\n\t\t\trequestPublicPosts(true);\n\t\t} else {\n\t\t\trequestMessages(true);\n\t\t}\n\t\t_inner->searchRequested(true);\n\t} else {\n\t\t_inner->searchRequested(false);\n\t}\n\tif (peerSearchRequired()) {\n\t\tconst auto requestType = inCache\n\t\t\t? Api::PeerSearch::RequestType::CacheOnly\n\t\t\t: Api::PeerSearch::RequestType::CacheOrRemote;\n\t\t_peerSearch.request(query, [=](Api::PeerSearchResult result) {\n\t\t\tpeerSearchReceived(result);\n\t\t}, requestType);\n\t} else {\n\t\t_peerSearch.clear();\n\t\tpeerSearchReceived({});\n\t}\n\tconst auto peerQuery = Api::ConvertPeerSearchQuery(query);\n\tif (searchForTopicsRequired(peerQuery)) {\n\t\tif (inCache) {\n\t\t\tif (_topicSearchQuery != peerQuery) {\n\t\t\t\tresult = false;\n\t\t\t}\n\t\t} else if (_topicSearchQuery != peerQuery) {\n\t\t\t_topicSearchQuery = peerQuery;\n\t\t\t_topicSearchFull = false;\n\t\t\tsearchTopics();\n\t\t}\n\t} else {\n\t\t_api.request(base::take(_topicSearchRequest)).cancel();\n\t\t_topicSearchQuery = peerQuery;\n\t\t_topicSearchFull = true;\n\t}\n\treturn result;\n}\n\nbool Widget::peerSearchRequired() const {\n\treturn _searchState.filterChatsList() && !_openedForum;\n}\n\nbool Widget::searchForTopicsRequired(const QString &query) const {\n\treturn _searchState.filterChatsList()\n\t\t&& _openedForum\n\t\t&& !query.isEmpty()\n\t\t&& (IsHashOrCashtagSearchQuery(query) == HashOrCashtag::None)\n\t\t&& !_openedForum->topicsList()->loaded();\n}\n\nvoid Widget::searchRequested(SearchRequestDelay delay) {\n\tif (search(true, delay)) {\n\t\treturn;\n\t} else if (delay == SearchRequestDelay::Instant) {\n\t\t_searchTimer.cancel();\n\t\tsearch();\n\t} else {\n\t\t_searchTimer.callOnce(kSearchRequestDelay);\n\t}\n}\n\nvoid Widget::showMainMenu() {\n\tcontroller()->widget()->showMainMenu();\n}\n\nvoid Widget::searchMessages(SearchState state) {\n\tif (const auto peer = state.inChat.peer()) {\n\t\tif (_openedForum && peer->forum() != _openedForum) {\n\t\t\tcontroller()->closeForum();\n\t\t}\n\t} else if (state.query.isEmpty()) {\n\t\tif (_childList) {\n\t\t\thideChildList();\n\t\t}\n\t\tif (_openedForum) {\n\t\t\tcontroller()->closeForum();\n\t\t}\n\t\tif (_layout == Layout::Main) {\n\t\t\tcontroller()->closeFolder();\n\t\t}\n\t}\n\tapplySearchState(std::move(state));\n\tsession().local().saveRecentSearchHashtags(_searchState.query);\n\n\tif (_childList) {\n\t\t_childList->setInnerFocus();\n\t} else if (_subsectionTopBar) {\n\t\tif (!_subsectionTopBar->searchSetFocus()\n\t\t\t&& !_subsectionTopBar->searchHasFocus()) {\n\t\t\t_subsectionTopBar->toggleSearch(true, anim::type::normal);\n\t\t}\n\t} else {\n\t\t_search->setFocus();\n\t}\n}\n\nvoid Widget::searchTopics() {\n\tif (_topicSearchRequest || _topicSearchFull) {\n\t\treturn;\n\t}\n\t_api.request(base::take(_topicSearchRequest)).cancel();\n\t_topicSearchRequest = _api.request(MTPmessages_GetForumTopics(\n\t\tMTP_flags(MTPmessages_GetForumTopics::Flag::f_q),\n\t\t_openedForum->peer()->input(),\n\t\tMTP_string(_topicSearchQuery),\n\t\tMTP_int(_topicSearchOffsetDate),\n\t\tMTP_int(_topicSearchOffsetId),\n\t\tMTP_int(_topicSearchOffsetTopicId),\n\t\tMTP_int(kSearchPerPage)\n\t)).done([=](const MTPmessages_ForumTopics &result) {\n\t\t_topicSearchRequest = 0;\n\t\tconst auto savedTopicId = _topicSearchOffsetTopicId;\n\t\tconst auto byCreation = result.data().is_order_by_create_date();\n\t\t_openedForum->applyReceivedTopics(result, [&](\n\t\t\t\tnot_null<Data::ForumTopic*> topic) {\n\t\t\t_topicSearchOffsetTopicId = topic->rootId();\n\t\t\tif (byCreation) {\n\t\t\t\t_topicSearchOffsetDate = topic->creationDate();\n\t\t\t\tif (const auto last = topic->lastServerMessage()) {\n\t\t\t\t\t_topicSearchOffsetId = last->id;\n\t\t\t\t}\n\t\t\t} else if (const auto last = topic->lastServerMessage()) {\n\t\t\t\t_topicSearchOffsetId = last->id;\n\t\t\t\t_topicSearchOffsetDate = last->date();\n\t\t\t}\n\t\t\t_inner->appendToFiltered(topic);\n\t\t});\n\t\tif (_topicSearchOffsetTopicId != savedTopicId) {\n\t\t\t_inner->refresh();\n\t\t} else {\n\t\t\t_topicSearchFull = true;\n\t\t}\n\t}).fail([=] {\n\t\t_topicSearchFull = true;\n\t}).send();\n}\n\nvoid Widget::searchMore() {\n\tconst auto process = currentSearchProcess();\n\tif (process->requestId\n\t\t|| _historiesRequest\n\t\t|| _searchTimer.isActive()) {\n\t\treturn;\n\t} else if (!process->full) {\n\t\tif (const auto peer = searchInPeer()) {\n\t\t\tauto &histories = session().data().histories();\n\t\t\tconst auto topic = searchInTopic();\n\t\t\tconst auto type = Data::Histories::RequestType::History;\n\t\t\tconst auto history = session().data().history(peer);\n\t\t\tconst auto sublist = _openedForum\n\t\t\t\t? nullptr\n\t\t\t\t: _searchState.inChat.sublist();\n\t\t\tconst auto fromPeer = sublist ? nullptr : _searchQueryFrom;\n\t\t\tconst auto savedPeer = sublist\n\t\t\t\t? sublist->sublistPeer().get()\n\t\t\t\t: nullptr;\n\t\t\t_historiesRequest = histories.sendRequest(history, type, [=](\n\t\t\t\t\tFn<void()> finish) {\n\t\t\t\tconst auto type = SearchRequestType{\n\t\t\t\t\t.start = !process->lastId,\n\t\t\t\t\t.peer = true,\n\t\t\t\t};\n\t\t\t\tusing Flag = MTPmessages_Search::Flag;\n\t\t\t\tprocess->requestId = session().api().request(\n\t\t\t\t\tMTPmessages_Search(\n\t\t\t\t\t\tMTP_flags((topic ? Flag::f_top_msg_id : Flag())\n\t\t\t\t\t\t\t| (fromPeer ? Flag::f_from_id : Flag())\n\t\t\t\t\t\t\t| (savedPeer ? Flag::f_saved_peer_id : Flag())\n\t\t\t\t\t\t\t| (_searchQueryTags.empty()\n\t\t\t\t\t\t\t\t? Flag()\n\t\t\t\t\t\t\t\t: Flag::f_saved_reaction)),\n\t\t\t\t\t\tpeer->input(),\n\t\t\t\t\t\tMTP_string(_searchQuery),\n\t\t\t\t\t\t(fromPeer ? fromPeer->input() : MTP_inputPeerEmpty()),\n\t\t\t\t\t\t(savedPeer\n\t\t\t\t\t\t\t? savedPeer->input()\n\t\t\t\t\t\t\t: MTP_inputPeerEmpty()),\n\t\t\t\t\t\tMTP_vector_from_range(\n\t\t\t\t\t\t\t_searchQueryTags | ranges::views::transform(\n\t\t\t\t\t\t\t\tData::ReactionToMTP\n\t\t\t\t\t\t\t)),\n\t\t\t\t\t\tMTP_int(topic ? topic->rootId() : 0),\n\t\t\t\t\t\tMTP_inputMessagesFilterEmpty(),\n\t\t\t\t\t\tMTP_int(0), // min_date\n\t\t\t\t\t\tMTP_int(0), // max_date\n\t\t\t\t\t\tMTP_int(process->lastId),\n\t\t\t\t\t\tMTP_int(0), // add_offset\n\t\t\t\t\t\tMTP_int(kSearchPerPage),\n\t\t\t\t\t\tMTP_int(0), // max_id\n\t\t\t\t\t\tMTP_int(0), // min_id\n\t\t\t\t\t\tMTP_long(0)) // hash\n\t\t\t\t).done([=](const MTPmessages_Messages &result) {\n\t\t\t\t\tsearchReceived(type, result, process);\n\t\t\t\t\t_historiesRequest = 0;\n\t\t\t\t\tfinish();\n\t\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\t\tsearchFailed(type, error, process);\n\t\t\t\t\t_historiesRequest = 0;\n\t\t\t\t\tfinish();\n\t\t\t\t}).send();\n\t\t\t\tif (!process->lastId) {\n\t\t\t\t\tprocess->queries.emplace(\n\t\t\t\t\t\tprocess->requestId,\n\t\t\t\t\t\t_searchQuery);\n\t\t\t\t}\n\t\t\t\treturn process->requestId;\n\t\t\t});\n\t\t} else if (_searchState.tab == ChatSearchTab::PublicPosts) {\n\t\t\trequestPublicPosts(false);\n\t\t} else {\n\t\t\trequestMessages(false);\n\t\t}\n\t} else if (_searchInMigrated && !_migratedProcess.full) {\n\t\tauto &histories = session().data().histories();\n\t\tconst auto type = Data::Histories::RequestType::History;\n\t\tconst auto history = _searchInMigrated;\n\t\t_historiesRequest = histories.sendRequest(history, type, [=](\n\t\t\t\tFn<void()> finish) {\n\t\t\tconst auto type = SearchRequestType{\n\t\t\t\t.migrated = true,\n\t\t\t\t.start = !_migratedProcess.lastId,\n\t\t\t};\n\t\t\tconst auto flags = _searchQueryFrom\n\t\t\t\t? MTP_flags(MTPmessages_Search::Flag::f_from_id)\n\t\t\t\t: MTP_flags(0);\n\t\t\t_migratedProcess.requestId = session().api().request(\n\t\t\t\tMTPmessages_Search(\n\t\t\t\t\tflags,\n\t\t\t\t\t_searchInMigrated->peer->input(),\n\t\t\t\t\tMTP_string(_searchQuery),\n\t\t\t\t\t(_searchQueryFrom\n\t\t\t\t\t\t? _searchQueryFrom->input()\n\t\t\t\t\t\t: MTP_inputPeerEmpty()),\n\t\t\t\t\tMTPInputPeer(), // saved_peer_id\n\t\t\t\t\tMTPVector<MTPReaction>(), // saved_reaction\n\t\t\t\t\tMTPint(), // top_msg_id\n\t\t\t\t\tMTP_inputMessagesFilterEmpty(),\n\t\t\t\t\tMTP_int(0), // min_date\n\t\t\t\t\tMTP_int(0), // max_date\n\t\t\t\t\tMTP_int(_migratedProcess.lastId),\n\t\t\t\t\tMTP_int(0), // add_offset\n\t\t\t\t\tMTP_int(kSearchPerPage),\n\t\t\t\t\tMTP_int(0), // max_id\n\t\t\t\t\tMTP_int(0), // min_id\n\t\t\t\t\tMTP_long(0)) // hash\n\t\t\t).done([=](const MTPmessages_Messages &result) {\n\t\t\t\tsearchReceived(type, result, &_migratedProcess);\n\t\t\t\t_historiesRequest = 0;\n\t\t\t\tfinish();\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tsearchFailed(type, error, &_migratedProcess);\n\t\t\t\t_historiesRequest = 0;\n\t\t\t\tfinish();\n\t\t\t}).send();\n\t\t\treturn _migratedProcess.requestId;\n\t\t});\n\t}\n}\n\nvoid Widget::requestPublicPosts(bool fromStart) {\n\tif (!_postsProcess.lastId || !_postsProcess.lastPeer) {\n\t\tfromStart = true;\n\t}\n\tconst auto type = SearchRequestType{\n\t\t.posts = true,\n\t\t.start = fromStart,\n\t};\n\tusing Flag = MTPchannels_SearchPosts::Flag;\n\t_postsProcess.requestId = session().api().request(\n\t\tMTPchannels_SearchPosts(\n\t\t\tMTP_flags(Flag::f_hashtag),\n\t\t\tMTP_string(_searchState.query.trimmed().mid(1)),\n\t\t\tMTP_string(), // query\n\t\t\tMTP_int(fromStart ? 0 : _postsProcess.nextRate),\n\t\t\t(fromStart\n\t\t\t\t? MTP_inputPeerEmpty()\n\t\t\t\t: _postsProcess.lastPeer->input()),\n\t\t\tMTP_int(fromStart ? 0 : _postsProcess.lastId),\n\t\t\tMTP_int(kSearchPerPage),\n\t\t\tMTP_long(0)) // allow_paid_stars\n\t).done([=](const MTPmessages_Messages &result) {\n\t\tsearchReceived(type, result, &_postsProcess);\n\t}).fail([=](const MTP::Error &error) {\n\t\tsearchFailed(type, error, &_postsProcess);\n\t}).send();\n\tif (fromStart) {\n\t\t_postsProcess.queries.emplace(_postsProcess.requestId, _searchQuery);\n\t}\n}\n\nvoid Widget::requestMessages(bool fromStart) {\n\tif (!_searchProcess.lastId || !_searchProcess.lastPeer) {\n\t\tfromStart = true;\n\t}\n\tconst auto type = SearchRequestType{\n\t\t.start = fromStart,\n\t};\n\tusing Flag = MTPmessages_SearchGlobal::Flag;\n\tconst auto flags = Flag()\n\t\t| (session().settings().skipArchiveInSearch()\n\t\t\t? Flag::f_folder_id\n\t\t\t: Flag())\n\t\t| (_searchQueryFilter == ChatTypeFilter::Private\n\t\t\t? Flag::f_users_only\n\t\t\t: _searchQueryFilter == ChatTypeFilter::Groups\n\t\t\t? Flag::f_groups_only\n\t\t\t: _searchQueryFilter == ChatTypeFilter::Channels\n\t\t\t? Flag::f_broadcasts_only\n\t\t\t: Flag());\n\tconst auto folderId = 0;\n\t_searchProcess.requestId = session().api().request(\n\t\tMTPmessages_SearchGlobal(\n\t\t\tMTP_flags(flags),\n\t\t\tMTP_int(folderId),\n\t\t\tMTP_string(_searchQuery),\n\t\t\tMTP_inputMessagesFilterEmpty(),\n\t\t\tMTP_int(0), // min_date\n\t\t\tMTP_int(0), // max_date\n\t\t\tMTP_int(fromStart ? 0 : _searchProcess.nextRate),\n\t\t\t(fromStart\n\t\t\t\t? MTP_inputPeerEmpty()\n\t\t\t\t: _searchProcess.lastPeer->input()),\n\t\t\tMTP_int(fromStart ? 0 : _searchProcess.lastId),\n\t\t\tMTP_int(kSearchPerPage))\n\t).done([=](const MTPmessages_Messages &result) {\n\t\tsearchReceived(type, result, &_searchProcess);\n\t}).fail([=](const MTP::Error &error) {\n\t\tsearchFailed(type, error, &_searchProcess);\n\t}).send();\n\tif (!_searchProcess.lastId) {\n\t\t_searchProcess.queries.emplace(\n\t\t\t_searchProcess.requestId,\n\t\t\t_searchQuery);\n\t}\n\tif (fromStart && _searchWithPostsPreview) {\n\t\trequestPublicPosts(true);\n\t}\n}\n\nauto Widget::currentSearchProcess() -> not_null<SearchProcessState*> {\n\treturn (_searchState.tab == ChatSearchTab::PublicPosts)\n\t\t? &_postsProcess\n\t\t: &_searchProcess;\n}\n\nbool Widget::computeSearchWithPostsPreview() const {\n\treturn \t(_searchHashOrCashtag != HashOrCashtag::None)\n\t\t&& (_searchState.tab == ChatSearchTab::MyMessages)\n\t\t&& !_searchState.inChat;\n}\n\nvoid Widget::searchReceived(\n\t\tSearchRequestType type,\n\t\tconst MTPmessages_Messages &result,\n\t\tnot_null<SearchProcessState*> process,\n\t\tbool cacheResults) {\n\tconst auto state = _inner->state();\n\tif (!cacheResults\n\t\t&& (state == WidgetState::Filtered)\n\t\t&& type.start) {\n\t\tconst auto i = process->queries.find(process->requestId);\n\t\tif (i != process->queries.end()) {\n\t\t\tprocess->cache[i->second] = result;\n\t\t\tprocess->queries.erase(i);\n\t\t}\n\t}\n\tconst auto inject = (type.start && !type.posts && !type.migrated)\n\t\t? *_singleMessageSearch.lookup(_searchQuery)\n\t\t: nullptr;\n\tif (cacheResults && process->requestId) {\n\t\treturn;\n\t}\n\tif (type.start) {\n\t\tprocess->lastPeer = nullptr;\n\t\tprocess->lastId = 0;\n\t}\n\tconst auto processList = [&](const MTPVector<MTPMessage> &messages) {\n\t\tauto result = std::vector<not_null<HistoryItem*>>();\n\t\tfor (const auto &message : messages.v) {\n\t\t\tconst auto msgId = IdFromMessage(message);\n\t\t\tconst auto peerId = PeerFromMessage(message);\n\t\t\tconst auto lastDate = DateFromMessage(message);\n\t\t\tif (const auto peer = session().data().peerLoaded(peerId)) {\n\t\t\t\tif (lastDate) {\n\t\t\t\t\tconst auto item = session().data().addNewMessage(\n\t\t\t\t\t\tmessage,\n\t\t\t\t\t\tMessageFlags(),\n\t\t\t\t\t\tNewMessageType::Existing);\n\t\t\t\t\tresult.push_back(item);\n\t\t\t\t}\n\t\t\t\tprocess->lastPeer = peer;\n\t\t\t} else {\n\t\t\t\tLOG((\"API Error: a search results with not loaded peer %1\"\n\t\t\t\t\t).arg(peerId.value));\n\t\t\t}\n\t\t\tprocess->lastId = msgId;\n\t\t}\n\t\treturn result;\n\t};\n\tauto fullCount = 0;\n\tauto messages = result.match([&](const MTPDmessages_messages &data) {\n\t\tif (!cacheResults) {\n\t\t\t// Don't apply cached data!\n\t\t\tsession().data().processUsers(data.vusers());\n\t\t\tsession().data().processChats(data.vchats());\n\t\t\tif (const auto peer = searchInPeer()) {\n\t\t\t\tpeer->processTopics(data.vtopics());\n\t\t\t}\n\t\t}\n\t\tprocess->full = true;\n\t\tauto list = processList(data.vmessages());\n\t\tfullCount = list.size();\n\t\treturn list;\n\t}, [&](const MTPDmessages_messagesSlice &data) {\n\t\tif (!cacheResults) {\n\t\t\t// Don't apply cached data!\n\t\t\tsession().data().processUsers(data.vusers());\n\t\t\tsession().data().processChats(data.vchats());\n\t\t\tif (const auto peer = searchInPeer()) {\n\t\t\t\tpeer->processTopics(data.vtopics());\n\t\t\t}\n\t\t}\n\t\tauto list = processList(data.vmessages());\n\t\tconst auto nextRate = data.vnext_rate();\n\t\tconst auto rateUpdated = nextRate\n\t\t\t&& (nextRate->v != process->nextRate);\n\t\tconst auto finished = (type.peer || type.migrated || type.posts)\n\t\t\t? list.empty()\n\t\t\t: !rateUpdated;\n\t\tif (rateUpdated) {\n\t\t\tprocess->nextRate = nextRate->v;\n\t\t}\n\t\tif (finished) {\n\t\t\tprocess->full = true;\n\t\t}\n\t\tfullCount = data.vcount().v;\n\t\treturn list;\n\t}, [&](const MTPDmessages_channelMessages &data) {\n\t\tif (!cacheResults) {\n\t\t\t// Don't apply cached data!\n\t\t\tsession().data().processUsers(data.vusers());\n\t\t\tsession().data().processChats(data.vchats());\n\t\t\tif (const auto peer = searchInPeer()) {\n\t\t\t\tif (const auto channel = peer->asChannel()) {\n\t\t\t\t\tchannel->ptsReceived(data.vpts().v);\n\t\t\t\t} else {\n\t\t\t\t\tLOG((\"API Error: \"\n\t\t\t\t\t\t\"received messages.channelMessages when no channel \"\n\t\t\t\t\t\t\"was passed! (Widget::searchReceived)\"));\n\t\t\t\t}\n\t\t\t\tpeer->processTopics(data.vtopics());\n\t\t\t} else {\n\t\t\t\tLOG((\"API Error: \"\n\t\t\t\t\t\"received messages.channelMessages when no channel \"\n\t\t\t\t\t\"was passed! (Widget::searchReceived)\"));\n\t\t\t}\n\t\t}\n\t\tauto list = processList(data.vmessages());\n\t\tif (list.empty()) {\n\t\t\tprocess->full = true;\n\t\t}\n\t\tfullCount = data.vcount().v;\n\t\treturn list;\n\t}, [&](const MTPDmessages_messagesNotModified &) {\n\t\tLOG((\"API Error: received messages.messagesNotModified! \"\n\t\t\t\"(Widget::searchReceived)\"));\n\t\tprocess->full = true;\n\t\treturn std::vector<not_null<HistoryItem*>>();\n\t});\n\t_inner->searchReceived(messages, inject, type, fullCount);\n\n\tprocess->requestId = 0;\n\tlistScrollUpdated();\n\tupdate();\n}\n\nvoid Widget::peerSearchReceived(Api::PeerSearchResult result) {\n\t_inner->peerSearchReceived(std::move(result));\n\tlistScrollUpdated();\n\tupdate();\n}\n\nvoid Widget::searchApplyEmpty(\n\t\tSearchRequestType type,\n\t\tnot_null<SearchProcessState*> process) {\n\tprocess->full = true;\n\tsearchReceived(\n\t\ttype,\n\t\tMTP_messages_messages(\n\t\t\tMTP_vector<MTPMessage>(),\n\t\t\tMTP_vector<MTPForumTopic>(),\n\t\t\tMTP_vector<MTPChat>(),\n\t\t\tMTP_vector<MTPUser>()),\n\t\tprocess);\n}\n\nvoid Widget::searchFailed(\n\t\tSearchRequestType type,\n\t\tconst MTP::Error &error,\n\t\tnot_null<SearchProcessState*> process) {\n\tif (error.type() == u\"SEARCH_QUERY_EMPTY\"_q) {\n\t\tsearchApplyEmpty(type, process);\n\t} else {\n\t\tprocess->requestId = 0;\n\t\tprocess->full = true;\n\t}\n}\n\nvoid Widget::dragEnterEvent(QDragEnterEvent *e) {\n\tusing namespace Storage;\n\n\tconst auto data = e->mimeData();\n\t_dragInScroll = false;\n\t_dragForward = !controller()->adaptive().isOneColumn()\n\t\t&& data->hasFormat(u\"application/x-td-forward\"_q);\n\tif (_dragForward) {\n\t\te->setDropAction(Qt::CopyAction);\n\t\te->accept();\n\t\tupdateDragInScroll(_scroll->geometry().contains(e->pos()));\n\t} else if (ComputeMimeDataState(data) != MimeDataState::None) {\n\t\te->setDropAction(Qt::CopyAction);\n\t\te->accept();\n\t}\n\t_chooseByDragTimer.cancel();\n}\n\nvoid Widget::dragMoveEvent(QDragMoveEvent *e) {\n\tif (_scroll->geometry().contains(e->pos())) {\n\t\tif (_dragForward) {\n\t\t\tupdateDragInScroll(true);\n\t\t} else {\n\t\t\t_chooseByDragTimer.callOnce(ChoosePeerByDragTimeout);\n\t\t}\n\t\tconst auto global = mapToGlobal(e->pos());\n\t\tconst auto thread = _suggestions\n\t\t\t? _suggestions->updateFromParentDrag(global)\n\t\t\t: _inner->updateFromParentDrag(global);\n\t\te->setDropAction(thread ? Qt::CopyAction : Qt::IgnoreAction);\n\t} else {\n\t\tif (_dragForward) {\n\t\t\tupdateDragInScroll(false);\n\t\t}\n\t\tif (_suggestions) {\n\t\t\t_suggestions->dragLeft();\n\t\t}\n\t\t_inner->dragLeft();\n\t\te->setDropAction(Qt::IgnoreAction);\n\t}\n\te->accept();\n}\n\nvoid Widget::dragLeaveEvent(QDragLeaveEvent *e) {\n\tif (_dragForward) {\n\t\tupdateDragInScroll(false);\n\t} else {\n\t\t_chooseByDragTimer.cancel();\n\t}\n\tif (_suggestions) {\n\t\t_suggestions->dragLeft();\n\t}\n\t_inner->dragLeft();\n\te->accept();\n}\n\nvoid Widget::updateDragInScroll(bool inScroll) {\n\tif (_dragInScroll != inScroll) {\n\t\t_dragInScroll = inScroll;\n\t\tif (_dragInScroll) {\n\t\t\tcontroller()->content()->showDragForwardInfo();\n\t\t} else {\n\t\t\tcontroller()->content()->dialogsCancelled();\n\t\t}\n\t}\n}\n\nvoid Widget::dropEvent(QDropEvent *e) {\n\t_chooseByDragTimer.cancel();\n\tif (_scroll->geometry().contains(e->pos())) {\n\t\tconst auto globalPosition = mapToGlobal(e->pos());\n\t\tconst auto thread = _suggestions\n\t\t\t? _suggestions->updateFromParentDrag(globalPosition)\n\t\t\t: _inner->updateFromParentDrag(globalPosition);\n\t\tif (thread) {\n\t\t\te->setDropAction(Qt::CopyAction);\n\t\t\te->accept();\n\t\t\tcontroller()->content()->filesOrForwardDrop(\n\t\t\t\tthread,\n\t\t\t\te->mimeData());\n\t\t\tif (!thread->owningHistory()->isForum()) {\n\t\t\t\thideChildList();\n\t\t\t}\n\t\t\tcontroller()->widget()->raise();\n\t\t\tcontroller()->widget()->activateWindow();\n\t\t}\n\t}\n}\n\nvoid Widget::listScrollUpdated() {\n\tconst auto scrollTop = _scroll->scrollTop();\n\t_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());\n\tupdateScrollUpVisibility();\n\n\t// Fix button rendering glitch, Qt bug with WA_OpaquePaintEvent widgets.\n\t_scrollToTop->update();\n}\n\nvoid Widget::updateCancelSearch() {\n\tconst auto shown = !_searchState.query.isEmpty()\n\t\t|| (!_searchState.inChat\n\t\t\t&& (_searchHasFocus || _searchSuggestionsLocked));\n\t_cancelSearch->toggle(shown, anim::type::normal);\n\tif (_searchState.inChat) {\n\t\t_cancelSearch->setAccessibleName(shown\n\t\t\t? tr::lng_sr_clear_search(tr::now)\n\t\t\t: tr::lng_sr_cancel_search(tr::now));\n\t}\n}\n\nQString Widget::validateSearchQuery() {\n\tconst auto query = currentSearchQuery();\n\tif (!_subsectionTopBar\n\t\t&& _suggestions\n\t\t&& _suggestions->consumeSearchQuery(query)) {\n\t\treturn QString();\n\t} else if (_searchState.tab == ChatSearchTab::PublicPosts) {\n\t\tif (_searchHashOrCashtag == HashOrCashtag::None) {\n\t\t\t_searchHashOrCashtag = HashOrCashtag::Hashtag;\n\t\t}\n\t\tconst auto fixed = FixHashtagSearchQuery(\n\t\t\tquery,\n\t\t\tcurrentSearchQueryCursorPosition(),\n\t\t\t_searchHashOrCashtag);\n\t\tif (fixed.text != query) {\n\t\t\tsetSearchQuery(fixed.text, fixed.cursorPosition);\n\t\t}\n\t\treturn fixed.text;\n\t} else {\n\t\t_searchHashOrCashtag = IsHashOrCashtagSearchQuery(query);\n\t}\n\t_searchWithPostsPreview = computeSearchWithPostsPreview();\n\treturn query;\n}\n\nvoid Widget::applySearchUpdate() {\n\tauto copy = _searchState;\n\tcopy.query = validateSearchQuery();\n\tapplySearchState(std::move(copy));\n\n\tif (_chooseFromUser->toggled()\n\t\t|| _searchState.fromPeer\n\t\t|| !_searchState.tags.empty()) {\n\t\tauto switchToChooseFrom = HistoryView::SwitchToChooseFromQuery();\n\t\tif (_lastSearchText != switchToChooseFrom\n\t\t\t&& switchToChooseFrom.startsWith(_lastSearchText)\n\t\t\t&& _searchState.query == switchToChooseFrom) {\n\t\t\tshowSearchFrom();\n\t\t}\n\t}\n\t_lastSearchText = _searchState.query;\n}\n\nvoid Widget::updateForceDisplayWide() {\n\tif (_childList) {\n\t\t_childList->updateForceDisplayWide();\n\t\treturn;\n\t}\n\tcontroller()->setChatsForceDisplayWide(_searchHasFocus\n\t\t|| (_subsectionTopBar && _subsectionTopBar->searchHasFocus())\n\t\t|| _searchSuggestionsLocked\n\t\t|| !_searchState.query.isEmpty()\n\t\t|| _searchState.inChat\n\t\t|| !session().api().authorizations().unreviewed().empty());\n}\n\nvoid Widget::showForum(\n\t\tnot_null<Data::Forum*> forum,\n\t\tconst Window::SectionShow &params) {\n\tif (_openedForum == forum) {\n\t\treturn;\n\t}\n\tconst auto nochat = !controller()->mainSectionShown();\n\tif (!params.childColumn\n\t\t|| (Core::App().settings().dialogsWidthRatio(nochat) == 0.)\n\t\t|| (_layout != Layout::Main)\n\t\t|| OptionForumHideChatsList.value()) {\n\t\tchangeOpenedForum(forum, params.animated);\n\t\treturn;\n\t}\n\tcancelSearch({ .forceFullCancel = true });\n\topenChildList(forum, params);\n}\n\nvoid Widget::openChildList(\n\t\tnot_null<Data::Forum*> forum,\n\t\tconst Window::SectionShow &params) {\n\tauto slide = Window::SectionSlideParams();\n\tconst auto animated = !_childList\n\t\t&& (params.animated == anim::type::normal);\n\tif (animated) {\n\t\tdestroyChildListCanvas();\n\t\tslide.oldContentCache = Ui::GrabWidget(\n\t\t\tthis,\n\t\t\tQRect(_narrowWidth, 0, width() - _narrowWidth, height()));\n\t}\n\tauto copy = params;\n\tcopy.childColumn = false;\n\tcopy.animated = anim::type::instant;\n\t{\n\t\tif (_childList && InFocusChain(_childList.get())) {\n\t\t\tsetFocus();\n\t\t}\n\t\t_childList = std::make_unique<Widget>(\n\t\t\tthis,\n\t\t\tcontroller(),\n\t\t\tLayout::Child);\n\t\t_childList->showForum(forum, copy);\n\t\t_childListPeerId = forum->peer()->id;\n\t}\n\n\t_childListShadow = std::make_unique<Ui::RpWidget>(this);\n\tconst auto shadow = _childListShadow.get();\n\tconst auto opacity = shadow->lifetime().make_state<float64>(0.);\n\tshadow->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tshadow->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(shadow);\n\t\tp.setOpacity(*opacity);\n\t\tp.fillRect(clip, st::shadowFg);\n\t}, shadow->lifetime());\n\t_childListShown.value() | rpl::on_next([=](float64 value) {\n\t\t*opacity = value;\n\t\tupdate();\n\t\t_inner->update();\n\t\t_search->setVisible(value < 1.);\n\t\tif (!value && _childListShadow.get() != shadow) {\n\t\t\tdelete shadow;\n\t\t}\n\t}, shadow->lifetime());\n\n\tupdateControlsGeometry();\n\tupdateControlsVisibility(true);\n\n\tif (animated) {\n\t\t_childList->showAnimated(Window::SlideDirection::FromRight, slide);\n\t\t_childListShown = _childList->shownProgressValue();\n\t} else {\n\t\t_childListShown = 1.;\n\t}\n\tif (hasFocus()) {\n\t\tsetInnerFocus();\n\t}\n\tupdateForceDisplayWide();\n}\n\nvoid Widget::closeChildList(anim::type animated) {\n\tif (!_childList) {\n\t\treturn;\n\t}\n\tconst auto geometry = _childList->geometry();\n\tconst auto shown = _childListShown.current();\n\tauto oldContentCache = QPixmap();\n\tauto animation = (Window::SlideAnimation*)nullptr;\n\tif (animated == anim::type::normal) {\n\t\toldContentCache = Ui::GrabWidget(_childList.get());\n\t\t_hideChildListCanvas = std::make_unique<Ui::RpWidget>(this);\n\t\t_hideChildListCanvas->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t_hideChildListCanvas->setGeometry(geometry);\n\t\tanimation = _hideChildListCanvas->lifetime().make_state<\n\t\t\tWindow::SlideAnimation\n\t\t>();\n\t\t_hideChildListCanvas->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tQPainter p(_hideChildListCanvas.get());\n\t\t\tanimation->paintContents(p);\n\t\t}, _hideChildListCanvas->lifetime());\n\t}\n\tif (InFocusChain(_childList.get())) {\n\t\tsetFocus();\n\t}\n\t_childList = nullptr;\n\t_childListShown = 0.;\n\tif (hasFocus()) {\n\t\tsetInnerFocus();\n\t\t_search->finishAnimating();\n\t}\n\tif (animated == anim::type::normal) {\n\t\t_hideChildListCanvas->hide();\n\t\tauto newContentCache = Ui::GrabWidget(this, geometry);\n\t\t_hideChildListCanvas->show();\n\n\t\t_childListShown = shown;\n\t\t_childListShadow.release();\n\n\t\tanimation->setDirection(Window::SlideDirection::FromLeft);\n\t\tanimation->setRepaintCallback([=] {\n\t\t\t_childListShown = (1. - animation->progress()) * shown;\n\t\t\t_hideChildListCanvas->update();\n\t\t});\n\t\tanimation->setFinishedCallback([=] {\n\t\t\tdestroyChildListCanvas();\n\t\t});\n\t\tanimation->setPixmaps(oldContentCache, newContentCache);\n\t\tanimation->start();\n\t} else {\n\t\t_childListShadow = nullptr;\n\t}\n\tupdateStoriesVisibility();\n\tupdateForceDisplayWide();\n}\n\nbool Widget::applySearchState(SearchState state) {\n\tif (_searchState == state) {\n\t\treturn true;\n\t} else if (_childList) {\n\t\tif (_childList->applySearchState(state)) {\n\t\t\treturn true;\n\t\t}\n\t\thideChildList();\n\t}\n\tif (state.inChat && _layout == Layout::Main) {\n\t\tcontroller()->closeFolder();\n\t}\n\n\t// Adjust state to be consistent.\n\tif (const auto peer = state.inChat.peer()) {\n\t\tif (const auto to = peer->migrateTo()) {\n\t\t\tstate.inChat = peer->owner().history(to);\n\t\t}\n\t}\n\tconst auto peer = state.inChat.peer();\n\tconst auto topic = state.inChat.topic();\n\tconst auto forum = peer ? peer->forum() : nullptr;\n\tif (state.inChat.folder() || (forum && !topic)) {\n\t\tstate.inChat = {};\n\t}\n\tif (!state.inChat && !forum && !_openedForum) {\n\t\tstate.fromPeer = nullptr;\n\t}\n\tif (state.tab == ChatSearchTab::PublicPosts\n\t\t&& IsHashOrCashtagSearchQuery(state.query) == HashOrCashtag::None) {\n\t\tstate.tab = (_openedForum && !state.inChat)\n\t\t\t? ChatSearchTab::ThisPeer\n\t\t\t: ChatSearchTab::MyMessages;\n\t} else if (!state.inChat\n\t\t&& _searchHashOrCashtag == HashOrCashtag::None) {\n\t\tstate.tab = (forum || _openedForum)\n\t\t\t? ChatSearchTab::ThisPeer\n\t\t\t: ChatSearchTab::MyMessages;\n\t}\n\tif (!state.tags.empty()) {\n\t\tstate.inChat = session().data().history(session().user());\n\t}\n\n\tconst auto clearQuery = state.fromPeer\n\t\t&& (_lastSearchText == HistoryView::SwitchToChooseFromQuery());\n\tif (clearQuery) {\n\t\tstate.query = _lastSearchText = QString();\n\t}\n\n\tconst auto inChatChanged = (_searchState.inChat != state.inChat);\n\tconst auto fromPeerChanged = (_searchState.fromPeer != state.fromPeer);\n\tconst auto tagsChanged = (_searchState.tags != state.tags);\n\tconst auto queryChanged = (_searchState.query != state.query);\n\tconst auto tabChanged = (_searchState.tab != state.tab);\n\tconst auto queryEmptyChanged = queryChanged\n\t\t? (_searchState.query.isEmpty() != state.query.isEmpty())\n\t\t: false;\n\tif (queryEmptyChanged || tabChanged) {\n\t\tstate.filter = ChatTypeFilter::All;\n\t}\n\tconst auto filterChanged = (_searchState.filter != state.filter);\n\n\tif (forum) {\n\t\tif (_openedForum == forum) {\n\t\t\tshowSearchInTopBar(anim::type::normal);\n\t\t} else if (_layout == Layout::Main) {\n\t\t\t_forumSearchRequested = true;\n\t\t\tauto params = Window::SectionShow(\n\t\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\t\tparams.forceTopicsList = true;\n\t\t\tcontroller()->showForum(forum, params);\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t} else if (peer && (_layout != Layout::Main)) {\n\t\treturn false;\n\t}\n\n\tif ((state.tab == ChatSearchTab::ThisTopic\n\t\t&& !state.inChat.topic())\n\t\t|| (state.tab == ChatSearchTab::ThisPeer\n\t\t\t&& !state.inChat\n\t\t\t&& !_openedForum)\n\t\t|| (state.tab == ChatSearchTab::PublicPosts\n\t\t\t&& _searchHashOrCashtag == HashOrCashtag::None)) {\n\t\tstate.tab = state.inChat.topic()\n\t\t\t? ChatSearchTab::ThisTopic\n\t\t\t: (state.inChat.owningHistory() || state.inChat.sublist())\n\t\t\t? ChatSearchTab::ThisPeer\n\t\t\t: ChatSearchTab::MyMessages;\n\t}\n\n\tconst auto migrateFrom = (peer\n\t\t&& !topic\n\t\t&& state.tab == ChatSearchTab::ThisPeer)\n\t\t? peer->migrateFrom()\n\t\t: nullptr;\n\t_searchInMigrated = migrateFrom\n\t\t? peer->owner().history(migrateFrom).get()\n\t\t: nullptr;\n\t_searchState = state;\n\tif (_chatFilters && (queryEmptyChanged || inChatChanged)) {\n\t\t_chatFilters->setVisible(_searchState.query.isEmpty()\n\t\t\t&& !_openedForum\n\t\t\t&& !searchInPeer());\n\t\tupdateControlsGeometry();\n\t}\n\tif (_topBarSuggestion && queryEmptyChanged) {\n\t\t_searchStateForTopBarSuggestion.fire(!_searchState.query.isEmpty());\n\t}\n\t_searchWithPostsPreview = computeSearchWithPostsPreview();\n\tif (queryChanged) {\n\t\tupdateLockUnlockVisibility(anim::type::normal);\n\t\tupdateLoadMoreChatsVisibility();\n\t}\n\tif (inChatChanged) {\n\t\tcontroller()->setSearchInChat(_searchState.inChat);\n\t}\n\tif (queryChanged || inChatChanged) {\n\t\tupdateCancelSearch();\n\t\tupdateStoriesVisibility();\n\t}\n\tupdateJumpToDateVisibility();\n\tupdateSearchFromVisibility();\n\tupdateLockUnlockPosition();\n\n\tconst auto searchCleared = state.query.isEmpty()\n\t\t&& !state.fromPeer\n\t\t&& state.tags.empty();\n\tif (searchCleared\n\t\t|| inChatChanged\n\t\t|| fromPeerChanged\n\t\t|| filterChanged\n\t\t|| tagsChanged\n\t\t|| tabChanged) {\n\t\tclearSearchCache(searchCleared);\n\t}\n\tif (state.query.isEmpty()) {\n\t\t_peerSearch.clear();\n\t}\n\n\tif (_searchState.query != currentSearchQuery()) {\n\t\tsetSearchQuery(_searchState.query);\n\t}\n\t_inner->applySearchState(_searchState);\n\n\tif (!_postponeProcessSearchFocusChange) {\n\t\t// Suggestions depend on _inner->state(), not on _searchState.\n\t\tupdateSuggestions(anim::type::instant);\n\t}\n\n\t_searchTagsLifetime = _inner->searchTagsChanges(\n\t) | rpl::on_next([=](std::vector<Data::ReactionId> &&list) {\n\t\tauto copy = _searchState;\n\t\tcopy.tags = std::move(list);\n\t\tapplySearchState(std::move(copy));\n\t});\n\tif (_subsectionTopBar) {\n\t\t_subsectionTopBar->searchEnableJumpToDate(\n\t\t\t_openedForum && _searchState.inChat);\n\t}\n\tif (!_searchState.inChat && _searchState.query.isEmpty()) {\n\t\tif (!_widthAnimationCache.isNull()) {\n\t\t\tstopWidthAnimation();\n\t\t}\n\t\tsetInnerFocus();\n\t} else if (!_subsectionTopBar) {\n\t\t_search->setFocus();\n\t} else if (_openedForum && !_subsectionTopBar->searchSetFocus()) {\n\t\t_subsectionTopBar->toggleSearch(true, anim::type::normal);\n\t}\n\tupdateForceDisplayWide();\n\tapplySearchUpdate();\n\treturn true;\n}\n\nvoid Widget::clearSearchCache(bool clearPosts) {\n\t_searchProcess.cache.clear();\n\t_singleMessageSearch.clear();\n\tconst auto queries = base::take(_searchProcess.queries);\n\tfor (const auto &[requestId, query] : queries) {\n\t\tsession().api().request(requestId).cancel();\n\t}\n\t_searchQuery = QString();\n\t_searchQueryFrom = nullptr;\n\t_searchQueryTags.clear();\n\tif (clearPosts) {\n\t\t_postsProcess.cache.clear();\n\t\tconst auto queries = base::take(_postsProcess.queries);\n\t\tfor (const auto &[requestId, query] : queries) {\n\t\t\tsession().api().request(requestId).cancel();\n\t\t}\n\t}\n\t_topicSearchQuery = QString();\n\t_topicSearchOffsetDate = 0;\n\t_topicSearchOffsetId = _topicSearchOffsetTopicId = 0;\n\t_api.request(base::take(_topicSearchRequest)).cancel();\n\t_peerSearch.clear();\n\tcancelSearchRequest();\n}\n\nvoid Widget::showCalendar() {\n\tif (_searchState.inChat) {\n\t\tcontroller()->showCalendar({ _searchState.inChat });\n\t}\n}\n\nvoid Widget::showSearchFrom() {\n\tif (const auto peer = searchInPeer()) {\n\t\tconst auto weak = base::make_weak(_searchState.inChat.topic());\n\t\tconst auto chat = (!_searchState.inChat && _openedForum)\n\t\t\t? Key(_openedForum->history())\n\t\t\t: _searchState.inChat;\n\t\tauto box = SearchFromBox(\n\t\t\tpeer,\n\t\t\tcrl::guard(this, [=](not_null<PeerData*> from) {\n\t\t\t\tcontroller()->hideLayer();\n\t\t\t\tauto copy = _searchState;\n\t\t\t\tif (!chat.topic()) {\n\t\t\t\t\tcopy.inChat = chat;\n\t\t\t\t\tcopy.fromPeer = from;\n\t\t\t\t\tapplySearchState(std::move(copy));\n\t\t\t\t} else if (const auto strong = weak.get()) {\n\t\t\t\t\tcopy.inChat = strong;\n\t\t\t\t\tcopy.fromPeer = from;\n\t\t\t\t\tapplySearchState(std::move(copy));\n\t\t\t\t}\n\t\t\t}),\n\t\t\tcrl::guard(this, [=] { _search->setFocus(); }));\n\t\tif (box) {\n\t\t\tcontroller()->show(std::move(box));\n\t\t}\n\t}\n}\n\nvoid Widget::searchCursorMoved() {\n\tconst auto to = _search->textCursor().position();\n\tconst auto text = _search->getLastText();\n\tauto hashtag = QStringView();\n\tfor (int start = to; start > 0;) {\n\t\t--start;\n\t\tif (text.size() <= start) {\n\t\t\tbreak;\n\t\t}\n\t\tconst auto ch = text[start];\n\t\tif (ch == '#') {\n\t\t\thashtag = base::StringViewMid(text, start, to - start);\n\t\t\tbreak;\n\t\t} else if (!ch.isLetterOrNumber() && ch != '_') {\n\t\t\tbreak;\n\t\t}\n\t}\n\t_inner->onHashtagFilterUpdate(hashtag);\n}\n\nvoid Widget::completeHashtag(QString tag) {\n\tconst auto t = _search->getLastText();\n\tauto cur = _search->textCursor().position();\n\tauto hashtag = QString();\n\tfor (int start = cur; start > 0;) {\n\t\t--start;\n\t\tif (t.size() <= start) {\n\t\t\tbreak;\n\t\t} else if (t.at(start) == '#') {\n\t\t\tif (cur == start + 1\n\t\t\t\t|| base::StringViewMid(t, start + 1, cur - start - 1)\n\t\t\t\t\t== base::StringViewMid(tag, 0, cur - start - 1)) {\n\t\t\t\twhile (cur < t.size() && cur - start - 1 < tag.size()) {\n\t\t\t\t\tif (t.at(cur) != tag.at(cur - start - 1)) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\t++cur;\n\t\t\t\t}\n\t\t\t\tif (cur - start - 1 == tag.size()\n\t\t\t\t\t&& cur < t.size()\n\t\t\t\t\t&& t.at(cur) == ' ') {\n\t\t\t\t\t++cur;\n\t\t\t\t}\n\t\t\t\thashtag = t.mid(0, start + 1) + tag + ' ' + t.mid(cur);\n\t\t\t\tsetSearchQuery(hashtag, start + 1 + tag.size() + 1);\n\t\t\t\tapplySearchUpdate();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tbreak;\n\t\t} else if (!t.at(start).isLetterOrNumber() && t.at(start) != '_') {\n\t\t\tbreak;\n\t\t}\n\t}\n\tsetSearchQuery(\n\t\tt.mid(0, cur) + '#' + tag + ' ' + t.mid(cur),\n\t\tcur + 1 + tag.size() + 1);\n\tapplySearchUpdate();\n}\n\nvoid Widget::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid Widget::updateLockUnlockVisibility(anim::type animated) {\n\tif (_showAnimation) {\n\t\treturn;\n\t}\n\tconst auto widthAnimation = !_widthAnimationCache.isNull();\n\tconst auto suggestionsAnimation = widthAnimation\n\t\t&& (!_suggestions || !_hidingSuggestions.empty());\n\tconst auto hiddenInstant = _showAnimation\n\t\t|| _openedForum\n\t\t|| (widthAnimation && !suggestionsAnimation)\n\t\t|| _childList\n\t\t|| !session().domain().local().hasLocalPasscode()\n\t\t|| (_stories\n\t\t\t&& !_stories->empty()\n\t\t\t&& _scroll->position().overscroll < -st::dialogsFilterSkip);\n\tconst auto hiddenAnimated = _searchHasFocus\n\t\t|| _searchSuggestionsLocked\n\t\t|| !_searchState.query.isEmpty()\n\t\t|| _searchState.inChat\n\t\t|| suggestionsAnimation;\n\tconst auto hidden = hiddenInstant || hiddenAnimated;\n\tconst auto changed = (_lockUnlock->toggled() == hidden);\n\t_lockUnlock->toggle(\n\t\t!hidden,\n\t\thiddenInstant ? anim::type::instant : animated);\n\tif (changed) {\n\t\tif (!hidden) {\n\t\t\tupdateLockUnlockPosition();\n\t\t}\n\t\tupdateControlsGeometry();\n\t}\n}\n\nvoid Widget::updateLoadMoreChatsVisibility() {\n\tif (_showAnimation || !_loadMoreChats) {\n\t\treturn;\n\t}\n\tconst auto hidden = (_openedFolder != nullptr)\n\t\t|| (_openedForum != nullptr)\n\t\t|| !_searchState.query.isEmpty();\n\tif (_loadMoreChats->isHidden() != hidden) {\n\t\t_loadMoreChats->setVisible(!hidden);\n\t\tupdateControlsGeometry();\n\t}\n}\n\nvoid Widget::updateJumpToDateVisibility(bool fast) {\n\tif (_showAnimation) {\n\t\treturn;\n\t}\n\n\t_jumpToDate->toggle(\n\t\t(searchInPeer() && _searchState.query.isEmpty()),\n\t\tfast ? anim::type::instant : anim::type::normal);\n}\n\nvoid Widget::updateSearchFromVisibility(bool fast) {\n\tauto visible = [&] {\n\t\tif (const auto peer = searchInPeer()) {\n\t\t\tif (peer->isChat() || peer->isMegagroup()) {\n\t\t\t\treturn !_searchState.fromPeer;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}();\n\tconst auto changed = (visible == !_chooseFromUser->toggled());\n\t_chooseFromUser->toggle(\n\t\tvisible,\n\t\tfast ? anim::type::instant : anim::type::normal);\n\tif (_subsectionTopBar) {\n\t\t_subsectionTopBar->searchEnableChooseFromUser(true, visible);\n\t} else if (changed) {\n\t\tauto additional = QMargins();\n\t\tif (visible) {\n\t\t\tadditional.setRight(_chooseFromUser->width());\n\t\t}\n\t\t_search->setAdditionalMargins(additional);\n\t}\n}\n\nvoid Widget::updateControlsGeometry() {\n\tif (width() < _narrowWidth) {\n\t\treturn;\n\t}\n\tauto filterAreaTop = 0;\n\n\tconst auto ratiow = anim::interpolate(\n\t\twidth(),\n\t\t_narrowWidth,\n\t\t_childListShown.current());\n\tconst auto smallw = st::columnMinimalWidthLeft - _narrowWidth;\n\tconst auto narrowRatio = (ratiow < smallw)\n\t\t? ((smallw - ratiow) / float64(smallw - _narrowWidth))\n\t\t: 0.;\n\n\tauto filterLeft = (controller()->filtersWidth()\n\t\t? st::dialogsFilterSkip\n\t\t: (st::dialogsFilterPadding.x() + _mainMenu.toggle->width()))\n\t\t+ st::dialogsFilterPadding.x();\n\tconst auto filterRight = st::dialogsFilterSkip\n\t\t+ st::dialogsFilterPadding.x();\n\tconst auto filterWidth = qMax(ratiow, smallw) - filterLeft - filterRight;\n\tconst auto filterAreaHeight = st::topBarHeight;\n\t_searchControls->setGeometry(0, filterAreaTop, ratiow, filterAreaHeight);\n\tif (_subsectionTopBar) {\n\t\t_subsectionTopBar->setGeometryWithNarrowRatio(\n\t\t\t_searchControls->geometry(),\n\t\t\t_narrowWidth,\n\t\t\tnarrowRatio);\n\t}\n\n\tauto filterTop = (filterAreaHeight - _search->height()) / 2;\n\tfilterLeft = anim::interpolate(filterLeft, _narrowWidth, narrowRatio);\n\t_search->setGeometryToLeft(\n\t\tfilterLeft,\n\t\tfilterTop,\n\t\tfilterWidth,\n\t\t_search->height());\n\n\tauto mainMenuLeft = anim::interpolate(\n\t\tst::dialogsFilterPadding.x(),\n\t\t(_narrowWidth - _mainMenu.toggle->width()) / 2,\n\t\tnarrowRatio);\n\t_mainMenu.toggle->moveToLeft(mainMenuLeft, st::dialogsFilterPadding.y());\n\t_mainMenu.under->setGeometry(\n\t\t0,\n\t\t0,\n\t\tfilterLeft,\n\t\t_mainMenu.toggle->y()\n\t\t\t+ _mainMenu.toggle->height()\n\t\t\t+ st::dialogsFilterPadding.y());\n\tconst auto searchLeft = anim::interpolate(\n\t\t-_searchForNarrowLayout->width(),\n\t\t(_narrowWidth - _searchForNarrowLayout->width()) / 2,\n\t\tnarrowRatio);\n\t_searchForNarrowLayout->moveToLeft(\n\t\tsearchLeft,\n\t\tst::dialogsFilterPadding.y());\n\n\tauto right = filterLeft + filterWidth;\n\t_cancelSearch->moveToLeft(right - _cancelSearch->width(), _search->y());\n\tright -= _jumpToDate->width();\n\t_jumpToDate->moveToLeft(right, _search->y());\n\tright -= _chooseFromUser->width();\n\t_chooseFromUser->moveToLeft(right, _search->y());\n\n\tconst auto barw = width();\n\tconst auto expandedStoriesTop = filterAreaTop + filterAreaHeight;\n\tconst auto storiesHeight = 2 * st::dialogsStories.photoTop\n\t\t+ st::dialogsStories.photo;\n\tconst auto added = (st::dialogsFilter.heightMin - storiesHeight) / 2;\n\tif (_stories) {\n\t\t_stories->setLayoutConstraints(\n\t\t\t{ filterLeft + filterWidth, filterTop + added },\n\t\t\tstyle::al_right,\n\t\t\t{ 0, expandedStoriesTop, barw, st::dialogsStoriesFull.height });\n\t}\n\tif (_forumTopShadow) {\n\t\t_forumTopShadow->setGeometry(\n\t\t\t0,\n\t\t\texpandedStoriesTop,\n\t\t\tbarw,\n\t\t\tst::lineWidth);\n\t}\n\n\tupdateLockUnlockPosition();\n\n\tauto bottomSkip = 0;\n\tconst auto putBottomButton = [&](auto &button) {\n\t\tif (button && !button->isHidden()) {\n\t\t\tconst auto buttonHeight = button->height();\n\t\t\tbottomSkip += buttonHeight;\n\t\t\tbutton->setGeometry(\n\t\t\t\t0,\n\t\t\t\theight() - bottomSkip,\n\t\t\t\tbarw,\n\t\t\t\tbuttonHeight);\n\t\t}\n\t};\n\tputBottomButton(_updateTelegram);\n\tputBottomButton(_downloadBar);\n\tputBottomButton(_loadMoreChats);\n\tif (_connecting) {\n\t\t_connecting->setBottomSkip(bottomSkip);\n\t}\n\tif (_layout != Layout::Child) {\n\t\tcontroller()->setConnectingBottomSkip(bottomSkip);\n\t}\n\n\tconst auto wasScrollTop = _scroll->scrollTop();\n\tconst auto newScrollTop = (wasScrollTop == 0)\n\t\t? wasScrollTop\n\t\t: (_topDelta < 0 && wasScrollTop <= 0)\n\t\t? wasScrollTop\n\t\t: (wasScrollTop + _topDelta);\n\n\tconst auto scrollWidth = _childList ? _narrowWidth : barw;\n\tif (_moreChatsBar) {\n\t\t_moreChatsBar->resizeToWidth(barw);\n\t}\n\tif (_forumGroupCallBar) {\n\t\t_forumGroupCallBar->resizeToWidth(barw);\n\t}\n\tif (_forumRequestsBar) {\n\t\t_forumRequestsBar->resizeToWidth(barw);\n\t}\n\tif (_chatFilters) {\n\t\t_chatFilters->resizeToWidth(barw);\n\t}\n\tif (_frozenAccountBar) {\n\t\t_frozenAccountBar->resize(barw, _frozenAccountBar->height());\n\t}\n\t_updateScrollGeometryCached = [=] {\n\t\tconst auto frozenBarTop = expandedStoriesTop\n\t\t\t+ ((!_stories || _stories->isHidden()) ? 0 : _aboveScrollAdded);\n\t\tif (_frozenAccountBar) {\n\t\t\t_frozenAccountBar->move(0, frozenBarTop);\n\t\t}\n\t\tconst auto moreChatsBarTop = frozenBarTop\n\t\t\t+ (_frozenAccountBar ? _frozenAccountBar->height() : 0);\n\t\tif (_moreChatsBar) {\n\t\t\t_moreChatsBar->move(0, moreChatsBarTop);\n\t\t}\n\t\tconst auto forumGroupCallTop = moreChatsBarTop\n\t\t\t+ (_moreChatsBar ? _moreChatsBar->height() : 0);\n\t\tif (_forumGroupCallBar) {\n\t\t\t_forumGroupCallBar->move(0, forumGroupCallTop);\n\t\t}\n\t\tconst auto forumRequestsTop = forumGroupCallTop\n\t\t\t+ (_forumGroupCallBar ? _forumGroupCallBar->height() : 0);\n\t\tif (_forumRequestsBar) {\n\t\t\t_forumRequestsBar->move(0, forumRequestsTop);\n\t\t}\n\t\tconst auto forumReportTop = forumRequestsTop\n\t\t\t+ (_forumRequestsBar ? _forumRequestsBar->height() : 0);\n\t\tif (_forumReportBar) {\n\t\t\t_forumReportBar->bar().move(0, forumReportTop);\n\t\t}\n\t\tconst auto chatFiltersTop = forumReportTop\n\t\t\t+ (_forumReportBar ? _forumReportBar->bar().height() : 0);\n\t\tif (_chatFilters) {\n\t\t\t_chatFilters->move(0, chatFiltersTop);\n\t\t}\n\t\tconst auto scrollTop = chatFiltersTop\n\t\t\t+ ((_chatFilters\n\t\t\t\t&& _searchState.query.isEmpty()\n\t\t\t\t&& !_openedForum && !searchInPeer())\n\t\t\t\t? (_chatFilters->height() * (1. - narrowRatio))\n\t\t\t\t: 0);\n\t\tconst auto scrollHeight = height() - scrollTop - bottomSkip;\n\t\tconst auto wasScrollHeight = _scroll->height();\n\t\t_scroll->setGeometry(0, scrollTop, scrollWidth, scrollHeight);\n\t\tif (scrollHeight != wasScrollHeight) {\n\t\t\tcontroller()->floatPlayerAreaUpdated();\n\t\t}\n\t};\n\t_updateScrollGeometryCached();\n\n\tif (_suggestions) {\n\t\t_suggestions->setGeometry(\n\t\t\t0,\n\t\t\texpandedStoriesTop,\n\t\t\tscrollWidth,\n\t\t\theight() - expandedStoriesTop - bottomSkip);\n\t}\n\n\t_inner->resize(scrollWidth, _inner->height());\n\t_inner->setNarrowRatio(narrowRatio);\n\tif (newScrollTop != wasScrollTop) {\n\t\t_scroll->scrollToY(newScrollTop);\n\t} else {\n\t\tlistScrollUpdated();\n\t}\n\tif (_scrollToTopIsShown) {\n\t\tupdateScrollUpPosition();\n\t}\n\n\tif (_childList) {\n\t\tconst auto childw = std::max(_narrowWidth, width() - scrollWidth);\n\t\tconst auto childh = _scroll->y() + _scroll->height();\n\t\tconst auto childx = width() - childw;\n\t\t_childList->setGeometryWithTopMoved(\n\t\t\t{ childx, 0, childw, childh },\n\t\t\t_topDelta);\n\t\tconst auto line = st::lineWidth;\n\t\t_childListShadow->setGeometry(childx - line, 0, line, childh);\n\t}\n}\n\nRowDescriptor Widget::resolveChatNext(RowDescriptor from) const {\n\treturn _inner->resolveChatNext(from);\n}\n\nRowDescriptor Widget::resolveChatPrevious(RowDescriptor from) const {\n\treturn _inner->resolveChatPrevious(from);\n}\n\nvoid Widget::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Escape) {\n\t\tescape();\n\t\t//if (_openedForum) {\n\t\t//\tcontroller()->closeForum();\n\t\t//} else if (_openedFolder) {\n\t\t//\tcontroller()->closeFolder();\n\t\t//} else {\n\t\t//\te->ignore();\n\t\t//}\n\t} else if ((e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Tab)\n\t\t&& _searchHasFocus\n\t\t&& !_searchState.inChat\n\t\t&& _searchState.query.isEmpty()) {\n\t\tescape();\n\t} else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {\n\t\tsubmit();\n\t} else if (_suggestions\n\t\t&& (e->key() == Qt::Key_Down\n\t\t\t|| e->key() == Qt::Key_Up\n\t\t\t|| e->key() == Qt::Key_Left\n\t\t\t|| e->key() == Qt::Key_Right)) {\n\t\t_suggestions->selectJump(Qt::Key(e->key()));\n\t} else if (e->key() == Qt::Key_Down) {\n\t\t_inner->selectSkip(1);\n\t} else if (e->key() == Qt::Key_Up) {\n\t\t_inner->selectSkip(-1);\n\t} else if (e->key() == Qt::Key_PageDown) {\n\t\tif (_suggestions) {\n\t\t\t_suggestions->selectJump(Qt::Key_Down, _scroll->height());\n\t\t} else {\n\t\t\t_inner->selectSkipPage(_scroll->height(), 1);\n\t\t}\n\t} else if (e->key() == Qt::Key_PageUp) {\n\t\tif (_suggestions) {\n\t\t\t_suggestions->selectJump(Qt::Key_Up, _scroll->height());\n\t\t} else {\n\t\t\t_inner->selectSkipPage(_scroll->height(), -1);\n\t\t}\n\t} else if (redirectKeyToSearch(e)) {\n\t\t// This delay in search focus processing allows us not to create\n\t\t// _suggestions in case the event inserts some non-whitespace search\n\t\t// query while still show _suggestions animated, if it is a space.\n\t\t_postponeProcessSearchFocusChange = true;\n\t\t_search->setFocusFast();\n\t\tif (e->key() != Qt::Key_Space) {\n\t\t\tQCoreApplication::sendEvent(_search->rawTextEdit(), e);\n\t\t}\n\t\t_postponeProcessSearchFocusChange = false;\n\t\tprocessSearchFocusChange();\n\t} else if (e->key() == Qt::Key_F10) {\n\t\tshowMainMenu();\n\t} else {\n\t\te->ignore();\n\t}\n}\n\nvoid Widget::inputMethodEvent(QInputMethodEvent *e) {\n\tconst auto cursor = _search->rawTextEdit()->textCursor();\n\tbool isGettingInput = !e->commitString().isEmpty()\n\t\t|| e->preeditString() != cursor.block().layout()->preeditAreaText()\n\t\t|| e->replacementLength() > 0;\n\n\tif (!isGettingInput || _postponeProcessSearchFocusChange) {\n\t\tWindow::AbstractSectionWidget::inputMethodEvent(e);\n\t\treturn;\n\t}\n\n\t// This delay in search focus processing allows us not to create\n\t// _suggestions in case the event inserts some non-whitespace search\n\t// query while still show _suggestions animated, if it is a space.\n\t_postponeProcessSearchFocusChange = true;\n\t_search->setFocusFast();\n\tQCoreApplication::sendEvent(_search->rawTextEdit(), e);\n\t_postponeProcessSearchFocusChange = false;\n\tprocessSearchFocusChange();\n}\n\nQVariant Widget::inputMethodQuery(Qt::InputMethodQuery query) const {\n\treturn _search->rawTextEdit()->inputMethodQuery(query);\n}\n\nbool Widget::redirectToSearchPossible() const {\n\treturn !_openedFolder\n\t\t&& !_openedForum\n\t\t&& !_childList\n\t\t&& _search->isVisible()\n\t\t&& !_search->hasFocus()\n\t\t&& hasFocus();\n}\n\nbool Widget::redirectKeyToSearch(QKeyEvent *e) const {\n\tif (!redirectToSearchPossible()) {\n\t\treturn false;\n\t}\n\tconst auto character = !(e->modifiers() & ~Qt::ShiftModifier)\n\t\t&& (e->key() != Qt::Key_Shift)\n\t\t&& RedirectTextToSearch(e->text());\n\tif (character) {\n\t\treturn true;\n\t} else if (e != QKeySequence::Paste) {\n\t\treturn false;\n\t}\n\tconst auto useSelectionMode = (e->key() == Qt::Key_Insert)\n\t\t&& (e->modifiers() == (Qt::CTRL | Qt::SHIFT))\n\t\t&& QGuiApplication::clipboard()->supportsSelection();\n\tconst auto pasteMode = useSelectionMode\n\t\t? QClipboard::Selection\n\t\t: QClipboard::Clipboard;\n\tconst auto data = QGuiApplication::clipboard()->mimeData(pasteMode);\n\treturn data && data->hasText();\n}\n\nbool Widget::redirectImeToSearch() const {\n\treturn redirectToSearchPossible();\n}\n\nvoid Widget::paintEvent(QPaintEvent *e) {\n\tif (controller()->contentOverlapped(this, e)) {\n\t\treturn;\n\t}\n\n\tPainter p(this);\n\tQRect r(e->rect());\n\tif (r != rect()) {\n\t\tp.setClipRect(r);\n\t}\n\tif (_showAnimation) {\n\t\t_showAnimation->paintContents(p);\n\t\treturn;\n\t}\n\tconst auto bg = anim::brush(\n\t\tst::dialogsBg,\n\t\tst::dialogsBgOver,\n\t\t_childListShown.current());\n\tauto above = QRect(0, 0, width(), _scroll->y());\n\tif (above.intersects(r)) {\n\t\tp.fillRect(above.intersected(r), bg);\n\t}\n\n\tauto belowTop = _scroll->y() + _scroll->height();\n\tif (!_widthAnimationCache.isNull()) {\n\t\tconst auto suggestionsShown = _suggestions\n\t\t\t? _suggestions->shownOpacity()\n\t\t\t: !_hidingSuggestions.empty()\n\t\t\t? _hidingSuggestions.back()->shownOpacity()\n\t\t\t: 0.;\n\t\tconst auto suggestionsSkip = suggestionsShown\n\t\t\t* (st::topPeers.height + st::searchedBarHeight);\n\t\tconst auto top = _searchControls->y()\n\t\t\t+ _searchControls->height()\n\t\t\t+ suggestionsSkip;\n\t\tconst auto aboveBottom = above.y() + above.height();\n\t\tif (top > aboveBottom) {\n\t\t\tp.fillRect(0, aboveBottom, width(), top - aboveBottom, bg);\n\t\t}\n\t\tp.drawPixmapLeft(0, top, width(), _widthAnimationCache);\n\t\tbelowTop = top\n\t\t\t+ (_widthAnimationCache.height() / style::DevicePixelRatio());\n\t}\n\n\tauto below = QRect(0, belowTop, width(), height() - belowTop);\n\tif (below.intersects(r)) {\n\t\tp.fillRect(below.intersected(r), bg);\n\t}\n}\n\nvoid Widget::scrollToEntry(const RowDescriptor &entry) {\n\t_inner->scrollToEntry(entry);\n}\n\nvoid Widget::cancelSearchRequest() {\n\tsession().api().request(base::take(_searchProcess.requestId)).cancel();\n\tsession().api().request(base::take(_migratedProcess.requestId)).cancel();\n\tsession().api().request(base::take(_postsProcess.requestId)).cancel();\n\tsession().data().histories().cancelRequest(\n\t\tbase::take(_historiesRequest));\n}\n\nPeerData *Widget::searchInPeer() const {\n\treturn (_searchState.tab == ChatSearchTab::MyMessages\n\t\t|| _searchState.tab == ChatSearchTab::PublicPosts)\n\t\t? nullptr\n\t\t: _openedForum\n\t\t? _openedForum->peer().get()\n\t\t: _searchState.inChat.sublist()\n\t\t? _searchState.inChat.sublist()->owningHistory()->peer.get()\n\t\t: _searchState.inChat.peer();\n}\n\nData::ForumTopic *Widget::searchInTopic() const {\n\treturn (_searchState.tab != ChatSearchTab::ThisTopic)\n\t\t? nullptr\n\t\t: _searchState.inChat.topic();\n}\n\nPeerData *Widget::searchFromPeer() const {\n\tif (const auto peer = searchInPeer()) {\n\t\tif (peer->isChat() || peer->isMegagroup()) {\n\t\t\treturn _searchState.fromPeer;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nconst std::vector<Data::ReactionId> &Widget::searchInTags() const {\n\tif (const auto peer = searchInPeer()) {\n\t\tif (peer->isSelf() && _searchState.tab == ChatSearchTab::ThisPeer) {\n\t\t\treturn _searchState.tags;\n\t\t}\n\t}\n\tstatic const auto kEmpty = std::vector<Data::ReactionId>();\n\treturn kEmpty;\n}\n\nQString Widget::currentSearchQuery() const {\n\treturn _subsectionTopBar\n\t\t? _subsectionTopBar->searchQueryCurrent()\n\t\t: _search->getLastText();\n}\n\nint Widget::currentSearchQueryCursorPosition() const {\n\treturn _subsectionTopBar\n\t\t? _subsectionTopBar->searchQueryCursorPosition()\n\t\t: _search->textCursor().position();\n}\n\nvoid Widget::clearSearchField() {\n\tif (_subsectionTopBar) {\n\t\t_subsectionTopBar->searchClear();\n\t} else {\n\t\t_search->clear();\n\t}\n}\n\nvoid Widget::setSearchQuery(const QString &query, int cursorPosition) {\n\tif (query.isEmpty()) {\n\t\tclearSearchField();\n\t\treturn;\n\t}\n\tif (cursorPosition < 0) {\n\t\tcursorPosition = query.size();\n\t}\n\tif (_subsectionTopBar) {\n\t\t_subsectionTopBar->searchSetText(query, cursorPosition);\n\t} else {\n\t\t_search->setText(query);\n\t\t_search->setCursorPosition(cursorPosition);\n\t}\n}\n\nbool Widget::cancelSearch(CancelSearchOptions options) {\n\tconst auto clearingSuggestionsQuery = _suggestions\n\t\t&& _suggestions->consumeSearchQuery(QString());\n\tif (clearingSuggestionsQuery) {\n\t\tsetSearchQuery(QString());\n\t\tif (!options.forceFullCancel) {\n\t\t\treturn true;\n\t\t}\n\t}\n\tcancelSearchRequest();\n\tauto updatedState = _searchState;\n\tconst auto clearingQuery = clearingSuggestionsQuery\n\t\t|| !updatedState.query.isEmpty();\n\tconst auto forceFullCancel = options.forceFullCancel;\n\tauto clearingInChat = (forceFullCancel || !clearingQuery)\n\t\t&& (updatedState.inChat\n\t\t\t|| updatedState.fromPeer\n\t\t\t|| !updatedState.tags.empty());\n\tif (clearingQuery) {\n\t\tupdatedState.query = QString();\n\t}\n\tif (clearingInChat) {\n\t\tif (options.jumpBackToSearchedChat\n\t\t\t&& updatedState.inChat\n\t\t\t&& controller()->adaptive().isOneColumn()) {\n\t\t\tif (const auto thread = updatedState.inChat.thread()) {\n\t\t\t\tcontroller()->showThread(thread);\n\t\t\t} else {\n\t\t\t\tUnexpected(\"Empty key in cancelSearch().\");\n\t\t\t}\n\t\t}\n\t\tupdatedState.inChat = {};\n\t\tupdatedState.fromPeer = nullptr;\n\t\tupdatedState.tags = {};\n\t}\n\tif (!clearingQuery\n\t\t&& _subsectionTopBar\n\t\t&& _subsectionTopBar->toggleSearch(false, anim::type::normal)) {\n\t\tsetInnerFocus(true);\n\t\tclearingInChat = true;\n\t}\n\tconst auto clearSearchFocus = (forceFullCancel || !updatedState.inChat)\n\t\t&& (_searchHasFocus || _searchSuggestionsLocked);\n\tif (!updatedState.inChat && _suggestions) {\n\t\t_suggestions->clearPersistance();\n\t\t_searchSuggestionsLocked = false;\n\t}\n\tif (!_suggestions && clearSearchFocus) {\n\t\t// Don't create suggestions in unfocus case.\n\t\tsetInnerFocus(true);\n\t}\n\t_searchProcess.lastPeer = nullptr;\n\t_searchProcess.lastId = 0;\n\t_migratedProcess.lastPeer = nullptr;\n\t_migratedProcess.lastId = 0;\n\t_postsProcess.lastPeer = nullptr;\n\t_postsProcess.lastId = 0;\n\t_inner->clearFilter();\n\tapplySearchState(std::move(updatedState));\n\tif (_suggestions && clearSearchFocus) {\n\t\tconst auto clearLockedFocus = !_searchHasFocus;\n\t\tsetInnerFocus(true);\n\t\tif (clearLockedFocus) {\n\t\t\tprocessSearchFocusChange();\n\t\t}\n\t}\n\tupdateForceDisplayWide();\n\tif (clearingInChat) {\n\t\tif (const auto forum = controller()->shownForum().current()) {\n\t\t\tif (forum->peer()->useSubsectionTabs()) {\n\t\t\t\tconst auto id = controller()->windowId();\n\t\t\t\tconst auto initial = id.forum();\n\t\t\t\tif (!initial) {\n\t\t\t\t\tcontroller()->closeForum();\n\t\t\t\t} else if (initial != forum) {\n\t\t\t\t\tcontroller()->showForum(initial);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn clearingQuery || clearingInChat || clearSearchFocus;\n}\n\nWidget::~Widget() {\n\tcancelSearchRequest();\n\n\t// Destructor may hide the bar and attempt to double-destroy it.\n\tbase::take(_downloadBar);\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/dialogs_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_peer_search.h\"\n#include \"base/timer.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"window/section_widget.h\"\n#include \"ui/controls/swipe_handler_data.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/userpic_view.h\"\n#include \"mtproto/sender.h\"\n#include \"api/api_single_message_search.h\"\n\nnamespace MTP {\nclass Error;\n} // namespace MTP\n\nnamespace Data {\nclass Forum;\nenum class StorySourcesList : uchar;\nstruct ReactionId;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace HistoryView {\nclass TopBarWidget;\nclass ContactStatus;\n} // namespace HistoryView\n\nnamespace Ui {\nclass AbstractButton;\nclass IconButton;\nclass PopupMenu;\nclass DropdownMenu;\nclass FlatButton;\nclass InputField;\nclass CrossButton;\nclass PlainShadow;\nclass DownloadBar;\nclass GroupCallBar;\nclass RequestsBar;\nclass MoreChatsBar;\nclass JumpDownButton;\nclass ElasticScroll;\ntemplate <typename Widget>\nclass FadeWrapScaled;\ntemplate <typename Widget>\nclass SlideWrap;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\nclass ConnectionState;\nstruct SectionShow;\nstruct SeparateId;\n} // namespace Window\n\nnamespace Dialogs::Stories {\nclass List;\nstruct Content;\n} // namespace Dialogs::Stories\n\nnamespace Dialogs {\n\nextern const char kOptionForumHideChatsList[];\n\nstruct RowDescriptor;\nclass Row;\nclass FakeRow;\nclass Key;\nstruct ChosenRow;\nclass InnerWidget;\nstruct SearchRequestType;\nenum class SearchRequestDelay : uchar;\nclass Suggestions;\nclass ChatSearchIn;\nenum class ChatSearchTab : uchar;\nenum class HashOrCashtag : uchar;\n\nclass Widget final : public Window::AbstractSectionWidget {\npublic:\n\tenum class Layout {\n\t\tMain,\n\t\tChild,\n\t};\n\tWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tLayout layout);\n\n\t// When resizing the widget with top edge moved up or down and we\n\t// want to add this top movement to the scroll position, so inner\n\t// content will not move.\n\tvoid setGeometryWithTopMoved(const QRect &newGeometry, int topDelta);\n\n\tvoid updateDragInScroll(bool inScroll);\n\n\tvoid showForum(\n\t\tnot_null<Data::Forum*> forum,\n\t\tconst Window::SectionShow &params);\n\tvoid setInnerFocus(bool unfocusSearch = false);\n\t[[nodiscard]] bool searchHasFocus() const;\n\n\t[[nodiscard]] Data::Forum *openedForum() const;\n\n\tvoid jumpToTop(bool belowPinned = false);\n\tvoid raiseWithTooltip();\n\n\t[[nodiscard]] QPixmap grabNonNarrowScrollFrame();\n\tvoid startWidthAnimation();\n\tvoid stopWidthAnimation();\n\n\tbool hasTopBarShadow() const {\n\t\treturn true;\n\t}\n\tvoid showAnimated(\n\t\tWindow::SlideDirection direction,\n\t\tconst Window::SectionSlideParams &params);\n\tvoid showFast();\n\t[[nodiscard]] rpl::producer<float64> shownProgressValue() const;\n\n\tvoid scrollToEntry(const RowDescriptor &entry);\n\n\tvoid searchMessages(SearchState state);\n\n\t[[nodiscard]] RowDescriptor resolveChatNext(RowDescriptor from = {}) const;\n\t[[nodiscard]] RowDescriptor resolveChatPrevious(RowDescriptor from = {}) const;\n\tvoid updateHasFocus(not_null<QWidget*> focused);\n\tvoid toggleFiltersMenu(bool value);\n\n\t// Float player interface.\n\tbool floatPlayerHandleWheelEvent(QEvent *e) override;\n\tQRect floatPlayerAvailableRect() override;\n\n\tbool cancelSearchByMouseBack();\n\n\tQVariant inputMethodQuery(Qt::InputMethodQuery query) const override;\n\n\t~Widget();\n\nprotected:\n\tvoid dragEnterEvent(QDragEnterEvent *e) override;\n\tvoid dragMoveEvent(QDragMoveEvent *e) override;\n\tvoid dragLeaveEvent(QDragLeaveEvent *e) override;\n\tvoid dropEvent(QDropEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid inputMethodEvent(QInputMethodEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tstruct SearchProcessState {\n\t\tbase::flat_map<QString, MTPmessages_Messages> cache;\n\t\tbase::flat_map<mtpRequestId, QString> queries;\n\n\t\tPeerData *lastPeer = nullptr;\n\t\tMsgId lastId = 0;\n\t\tint32 nextRate = 0;\n\t\tmtpRequestId requestId = 0;\n\t\tbool full = false;\n\t};\n\n\tvoid chosenRow(const ChosenRow &row);\n\tvoid listScrollUpdated();\n\tvoid searchCursorMoved();\n\tvoid completeHashtag(QString tag);\n\tvoid requestPublicPosts(bool fromStart);\n\tvoid requestMessages(bool fromStart);\n\t[[nodiscard]] not_null<SearchProcessState*> currentSearchProcess();\n\n\t[[nodiscard]] bool computeSearchWithPostsPreview() const;\n\n\t[[nodiscard]] QString currentSearchQuery() const;\n\t[[nodiscard]] int currentSearchQueryCursorPosition() const;\n\tvoid clearSearchField();\n\tvoid searchRequested(SearchRequestDelay delay);\n\tbool search(bool inCache = false, SearchRequestDelay after = {});\n\tvoid searchTopics();\n\tvoid searchMore();\n\n\tvoid slideFinished();\n\tvoid searchReceived(\n\t\tSearchRequestType type,\n\t\tconst MTPmessages_Messages &result,\n\t\tnot_null<SearchProcessState*> process,\n\t\tbool cacheResults = false);\n\tvoid peerSearchReceived(Api::PeerSearchResult result);\n\tvoid escape();\n\tvoid submit();\n\tvoid cancelSearchRequest();\n\t[[nodiscard]] PeerData *searchInPeer() const;\n\t[[nodiscard]] Data::ForumTopic *searchInTopic() const;\n\t[[nodiscard]] PeerData *searchFromPeer() const;\n\t[[nodiscard]] const std::vector<Data::ReactionId> &searchInTags() const;\n\n\tvoid setupSupportMode();\n\tvoid setupTouchChatPreview();\n\tvoid setupFrozenAccountBar();\n\tvoid setupConnectingWidget();\n\tvoid setupMainMenuToggle();\n\tvoid setupMoreChatsBar();\n\tvoid setupDownloadBar();\n\tvoid setupShortcuts();\n\tvoid setupStories();\n\tvoid setupSwipeBack();\n\tvoid setupTopBarSuggestions();\n\tvoid storiesExplicitCollapse();\n\tvoid collectStoriesUserpicsViews(Data::StorySourcesList list);\n\tvoid storiesToggleExplicitExpand(bool expand);\n\tvoid trackScroll(not_null<Ui::RpWidget*> widget);\n\t[[nodiscard]] bool peerSearchRequired() const;\n\t[[nodiscard]] bool searchForTopicsRequired(const QString &query) const;\n\n\t// Child list may be unable to set specific search state.\n\tbool applySearchState(SearchState state);\n\n\tvoid showCalendar();\n\tvoid showSearchFrom();\n\tvoid showMainMenu();\n\tvoid clearSearchCache(bool clearPosts);\n\tvoid setSearchQuery(const QString &query, int cursorPosition = -1);\n\tvoid updateTopBarSuggestions();\n\tvoid updateFrozenAccountBar();\n\tvoid updateControlsVisibility(bool fast = false);\n\tvoid updateLockUnlockVisibility(\n\t\tanim::type animated = anim::type::instant);\n\tvoid updateLoadMoreChatsVisibility();\n\tvoid updateStoriesVisibility();\n\tvoid updateJumpToDateVisibility(bool fast = false);\n\tvoid updateSearchFromVisibility(bool fast = false);\n\tvoid updateControlsGeometry();\n\tvoid refreshTopBars();\n\tvoid showSearchInTopBar(anim::type animated);\n\tvoid checkUpdateStatus();\n\tvoid openBotMainApp(not_null<UserData*> bot);\n\tvoid changeOpenedSubsection(\n\t\tFnMut<void()> change,\n\t\tbool fromRight,\n\t\tanim::type animated);\n\tvoid changeOpenedFolder(Data::Folder *folder, anim::type animated);\n\tvoid changeOpenedForum(Data::Forum *forum, anim::type animated);\n\tvoid hideChildList();\n\tvoid destroyChildListCanvas();\n\t[[nodiscard]] QPixmap grabForFolderSlideAnimation();\n\tvoid startSlideAnimation(\n\t\tQPixmap oldContentCache,\n\t\tQPixmap newContentCache,\n\t\tWindow::SlideDirection direction);\n\n\tvoid openChildList(\n\t\tnot_null<Data::Forum*> forum,\n\t\tconst Window::SectionShow &params);\n\tvoid closeChildList(anim::type animated);\n\n\tvoid fullSearchRefreshOn(rpl::producer<> events);\n\tvoid updateCancelSearch();\n\t[[nodiscard]] QString validateSearchQuery();\n\tvoid applySearchUpdate();\n\tvoid refreshLoadMoreButton(bool mayBlock, bool isBlocked);\n\tvoid loadMoreBlockedByDate();\n\n\tvoid searchFailed(\n\t\tSearchRequestType type,\n\t\tconst MTP::Error &error,\n\t\tnot_null<SearchProcessState*> process);\n\tvoid searchApplyEmpty(\n\t\tSearchRequestType type,\n\t\tnot_null<SearchProcessState*> process);\n\n\tvoid updateForceDisplayWide();\n\tvoid scrollToDefault(bool verytop = false);\n\tvoid scrollToDefaultChecked(bool verytop = false);\n\tvoid setupScrollUpButton();\n\tvoid updateScrollUpVisibility();\n\tvoid startScrollUpButtonAnimation(bool shown);\n\tvoid updateScrollUpPosition();\n\tvoid updateLockUnlockPosition();\n\tvoid updateSuggestions(anim::type animated);\n\tvoid processSearchFocusChange();\n\tvoid closeSuggestions();\n\n\t[[nodiscard]] bool redirectToSearchPossible() const;\n\t[[nodiscard]] bool redirectKeyToSearch(QKeyEvent *e) const;\n\t[[nodiscard]] bool redirectImeToSearch() const;\n\n\tstruct CancelSearchOptions {\n\t\tbool forceFullCancel = false;\n\t\tbool jumpBackToSearchedChat = false;\n\t};\n\tbool cancelSearch(CancelSearchOptions options);\n\n\tMTP::Sender _api;\n\n\tbool _dragInScroll = false;\n\tbool _dragForward = false;\n\tbase::Timer _chooseByDragTimer;\n\n\tconst Layout _layout = Layout::Main;\n\tconst int _narrowWidth = 0;\n\n\tstd::unique_ptr<Ui::AbstractButton> _frozenAccountBar;\n\n\tobject_ptr<Ui::RpWidget> _searchControls;\n\tobject_ptr<HistoryView::TopBarWidget> _subsectionTopBar = { nullptr };\n\tstruct {\n\t\tobject_ptr<Ui::IconButton> toggle;\n\t\tobject_ptr<Ui::AbstractButton> under;\n\t} _mainMenu;\n\tobject_ptr<Ui::IconButton> _searchForNarrowLayout;\n\tobject_ptr<Ui::InputField> _search;\n\tobject_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _chooseFromUser;\n\tobject_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _jumpToDate;\n\tobject_ptr<Ui::CrossButton> _cancelSearch;\n\tobject_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _lockUnlock;\n\n\tstd::unique_ptr<Ui::MoreChatsBar> _moreChatsBar;\n\n\tstd::unique_ptr<Ui::PlainShadow> _forumTopShadow;\n\tstd::unique_ptr<Ui::GroupCallBar> _forumGroupCallBar;\n\tstd::unique_ptr<Ui::RequestsBar> _forumRequestsBar;\n\tstd::unique_ptr<HistoryView::ContactStatus> _forumReportBar;\n\n\tbase::unique_qptr<Ui::RpWidget> _chatFilters;\n\n\tQPointer<Ui::SlideWrap<Ui::RpWidget>> _topBarSuggestion;\n\trpl::event_stream<int> _topBarSuggestionHeightChanged;\n\trpl::event_stream<bool> _searchStateForTopBarSuggestion;\n\trpl::event_stream<bool> _openedFolderOrForumChanges;\n\n\tobject_ptr<Ui::ElasticScroll> _scroll;\n\tUi::VerticalLayout *_innerList = nullptr;\n\tInnerWidget *_inner = nullptr;\n\tstd::unique_ptr<Suggestions> _suggestions;\n\tstd::vector<std::unique_ptr<Suggestions>> _hidingSuggestions;\n\tclass BottomButton;\n\tobject_ptr<BottomButton> _updateTelegram = { nullptr };\n\tobject_ptr<BottomButton> _loadMoreChats = { nullptr };\n\tstd::unique_ptr<Ui::DownloadBar> _downloadBar;\n\tstd::unique_ptr<Window::ConnectionState> _connecting;\n\n\tUi::Animations::Simple _scrollToAnimation;\n\tint _scrollAnimationTo = 0;\n\tstd::unique_ptr<Window::SlideAnimation> _showAnimation;\n\trpl::variable<float64> _shownProgressValue;\n\n\tUi::Animations::Simple _scrollToTopShown;\n\tobject_ptr<Ui::JumpDownButton> _scrollToTop;\n\tbool _scrollToTopIsShown = false;\n\tbool _forumSearchRequested = false;\n\tHashOrCashtag _searchHashOrCashtag = {};\n\tbool _searchWithPostsPreview = false;\n\n\tData::Folder *_openedFolder = nullptr;\n\tData::Forum *_openedForum = nullptr;\n\tSearchState _searchState;\n\tHistory *_searchInMigrated = nullptr;\n\trpl::lifetime _searchTagsLifetime;\n\tQString _lastSearchText;\n\tbool _searchSuggestionsLocked = false;\n\tbool _searchHasFocus = false;\n\tbool _processingSearch = false;\n\n\trpl::event_stream<rpl::producer<Stories::Content>> _storiesContents;\n\tbase::flat_map<PeerId, Ui::PeerUserpicView> _storiesUserpicsViewsHidden;\n\tbase::flat_map<PeerId, Ui::PeerUserpicView> _storiesUserpicsViewsShown;\n\tFn<void()> _updateScrollGeometryCached;\n\tstd::unique_ptr<Stories::List> _stories;\n\tUi::Animations::Simple _storiesExplicitExpandAnimation;\n\trpl::variable<int> _storiesExplicitExpandValue = 0;\n\tint _storiesExplicitExpandScrollTop = 0;\n\tint _aboveScrollAdded = 0;\n\tbool _storiesExplicitExpand = false;\n\tbool _postponeProcessSearchFocusChange = false;\n\n\tbase::Timer _searchTimer;\n\n\tQString _topicSearchQuery;\n\tTimeId _topicSearchOffsetDate = 0;\n\tMsgId _topicSearchOffsetId = 0;\n\tMsgId _topicSearchOffsetTopicId = 0;\n\tbool _topicSearchFull = false;\n\tmtpRequestId _topicSearchRequest = 0;\n\n\tQString _searchQuery;\n\tPeerData *_searchQueryFrom = nullptr;\n\tstd::vector<Data::ReactionId> _searchQueryTags;\n\tChatSearchTab _searchQueryTab = {};\n\tChatTypeFilter _searchQueryFilter = {};\n\n\tUi::Controls::SwipeBackResult _swipeBackData;\n\tbool _swipeBackMirrored = false;\n\tbool _swipeBackIconMirrored = false;\n\n\tSearchProcessState _searchProcess;\n\tSearchProcessState _migratedProcess;\n\tSearchProcessState _postsProcess;\n\tint _historiesRequest = 0; // Not real mtpRequestId.\n\n\tApi::PeerSearch _peerSearch;\n\tApi::SingleMessageSearch _singleMessageSearch;\n\n\tQPixmap _widthAnimationCache;\n\n\tint _topDelta = 0;\n\n\tstd::unique_ptr<Widget> _childList;\n\tstd::unique_ptr<Ui::RpWidget> _childListShadow;\n\trpl::variable<float64> _childListShown;\n\trpl::variable<PeerId> _childListPeerId;\n\tstd::unique_ptr<Ui::RpWidget> _hideChildListCanvas;\n\n};\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/ui/chat_search_empty.h\"\n\n#include \"base/object_ptr.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/widgets/labels.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Dialogs {\n\nSearchEmpty::SearchEmpty(\n\tQWidget *parent,\n\tIcon icon,\n\trpl::producer<TextWithEntities> text)\n: RpWidget(parent) {\n\tsetup(icon, std::move(text));\n}\n\nvoid SearchEmpty::setMinimalHeight(int minimalHeight) {\n\tconst auto minimal = st::recentPeersEmptyHeightMin;\n\tresize(width(), std::max(minimalHeight, minimal));\n}\n\nvoid SearchEmpty::setup(Icon icon, rpl::producer<TextWithEntities> text) {\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tthis,\n\t\tstd::move(text),\n\t\tst::defaultPeerListAbout);\n\tconst auto size = st::recentPeersEmptySize;\n\tconst auto animation = [&] {\n\t\tswitch (icon) {\n\t\tcase Icon::Search: return u\"search\"_q;\n\t\tcase Icon::NoResults: return u\"noresults\"_q;\n\t\t}\n\t\tUnexpected(\"Icon in SearchEmpty::setup.\");\n\t}();\n\tconst auto [widget, animate] = Settings::CreateLottieIcon(\n\t\tthis,\n\t\t{\n\t\t\t.name = animation,\n\t\t\t.sizeOverride = { size, size },\n\t\t},\n\t\tst::recentPeersEmptyMargin);\n\tconst auto animated = widget.data();\n\n\tsizeValue() | rpl::on_next([=](QSize size) {\n\t\tconst auto padding = st::recentPeersEmptyMargin;\n\t\tconst auto paddings = padding.left() + padding.right();\n\t\tlabel->resizeToWidth(size.width() - paddings);\n\t\tconst auto x = (size.width() - animated->width()) / 2;\n\t\tconst auto y = (size.height() - animated->height()) / 3;\n\t\tconst auto top = y + animated->height() + st::recentPeersEmptySkip;\n\t\tconst auto sub = std::max(top + label->height() - size.height(), 0);\n\t\tanimated->move(x, y - sub);\n\t\tlabel->move((size.width() - label->width()) / 2, top - sub);\n\t}, lifetime());\n\n\tlabel->setClickHandlerFilter([=](\n\t\t\tconst ClickHandlerPtr &handler,\n\t\t\tQt::MouseButton) {\n\t\t_handlerActivated.fire_copy(handler);\n\t\treturn false;\n\t});\n\n\t_animate = [animate] {\n\t\tanimate(anim::repeat::once);\n\t};\n}\n\nvoid SearchEmpty::animate() {\n\tif (const auto onstack = _animate) {\n\t\tonstack();\n\t}\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/chat_search_empty.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace Dialogs {\n\nenum class SearchEmptyIcon {\n\tSearch,\n\tNoResults,\n};\n\nclass SearchEmpty final : public Ui::RpWidget {\npublic:\n\tusing Icon = SearchEmptyIcon;\n\n\tSearchEmpty(\n\t\tQWidget *parent,\n\t\tIcon icon,\n\t\trpl::producer<TextWithEntities> text);\n\n\tvoid setMinimalHeight(int minimalHeight);\n\n\tvoid animate();\n\n\t[[nodiscard]] rpl::producer<ClickHandlerPtr> handlerActivated() const {\n\t\treturn _handlerActivated.events();\n\t}\n\nprivate:\n\tvoid setup(Icon icon, rpl::producer<TextWithEntities> text);\n\n\tFn<void()> _animate;\n\trpl::event_stream<ClickHandlerPtr> _handlerActivated;\n\n};\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/chat_search_in.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/ui/chat_search_in.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/menu/menu_item_base.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_window.h\"\n\nnamespace Dialogs {\nnamespace {\n\nclass Action final : public Ui::Menu::ItemBase {\npublic:\n\tAction(\n\t\tnot_null<Ui::PopupMenu*> parentMenu,\n\t\tstd::shared_ptr<Ui::DynamicImage> icon,\n\t\tconst QString &label,\n\t\tbool chosen);\n\t~Action();\n\n\tbool isEnabled() const override;\n\tnot_null<QAction*> action() const override;\n\n\tvoid handleKeyPress(not_null<QKeyEvent*> e) override;\n\nprotected:\n\tQPoint prepareRippleStartPosition() const override;\n\tQImage prepareRippleMask() const override;\n\n\tint contentHeight() const override;\n\nprivate:\n\tvoid paint(Painter &p);\n\n\tvoid resolveMinWidth();\n\tvoid refreshDimensions();\n\n\tconst not_null<Ui::PopupMenu*> _parentMenu;\n\tconst not_null<QAction*> _dummyAction;\n\tconst style::Menu &_st;\n\tconst int _height = 0;\n\n\tstd::shared_ptr<Ui::DynamicImage> _icon;\n\tUi::Text::String _text;\n\tbool _checked = false;\n\n};\n\n[[nodiscard]] QString TabLabel(\n\t\tChatSearchTab tab,\n\t\tChatSearchPeerTabType type = {}) {\n\tswitch (tab) {\n\tcase ChatSearchTab::MyMessages:\n\t\treturn tr::lng_search_tab_my_messages(tr::now);\n\tcase ChatSearchTab::ThisTopic:\n\t\treturn tr::lng_search_tab_this_topic(tr::now);\n\tcase ChatSearchTab::ThisPeer:\n\t\tswitch (type) {\n\t\tcase ChatSearchPeerTabType::Chat:\n\t\t\treturn tr::lng_search_tab_this_chat(tr::now);\n\t\tcase ChatSearchPeerTabType::Channel:\n\t\t\treturn tr::lng_search_tab_this_channel(tr::now);\n\t\tcase ChatSearchPeerTabType::Group:\n\t\t\treturn tr::lng_search_tab_this_group(tr::now);\n\t\t}\n\t\tUnexpected(\"Type in Dialogs::TabLabel.\");\n\tcase ChatSearchTab::PublicPosts:\n\t\treturn tr::lng_search_tab_public_posts(tr::now);\n\t}\n\tUnexpected(\"Tab in Dialogs::TabLabel.\");\n}\n\nAction::Action(\n\tnot_null<Ui::PopupMenu*> parentMenu,\n\tstd::shared_ptr<Ui::DynamicImage> icon,\n\tconst QString &label,\n\tbool chosen)\n: ItemBase(parentMenu->menu(), parentMenu->menu()->st())\n, _parentMenu(parentMenu)\n, _dummyAction(CreateChild<QAction>(parentMenu->menu().get()))\n, _st(parentMenu->menu()->st())\n, _height(st::dialogsSearchInHeight)\n, _icon(std::move(icon))\n, _checked(chosen) {\n\t_text.setText(st::semiboldTextStyle, label);\n\t_icon->subscribeToUpdates([=] { update(); });\n\n\tfitToMenuWidth();\n\tresolveMinWidth();\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(this);\n\t\tpaint(p);\n\t}, lifetime());\n\n\tenableMouseSelecting();\n}\n\nAction::~Action() {\n\t_icon->subscribeToUpdates(nullptr);\n}\n\nvoid Action::resolveMinWidth() {\n\tconst auto maxWidth = st::dialogsSearchInPhotoPadding\n\t\t+ st::dialogsSearchInPhotoSize\n\t\t+ st::dialogsSearchInSkip\n\t\t+ _text.maxWidth()\n\t\t+ st::dialogsSearchInCheckSkip\n\t\t+ st::dialogsSearchInCheck.width()\n\t\t+ st::dialogsSearchInCheckSkip;\n\tsetMinWidth(maxWidth);\n}\n\nvoid Action::paint(Painter &p) {\n\tconst auto enabled = isEnabled();\n\tconst auto selected = isSelected();\n\tif (selected && _st.itemBgOver->c.alpha() < 255) {\n\t\tp.fillRect(0, 0, width(), _height, _st.itemBg);\n\t}\n\tconst auto &bg = selected ? _st.itemBgOver : _st.itemBg;\n\tp.fillRect(0, 0, width(), _height, bg);\n\tif (enabled) {\n\t\tpaintRipple(p, 0, 0);\n\t}\n\n\tauto x = st::dialogsSearchInPhotoPadding;\n\tconst auto photos = st::dialogsSearchInPhotoSize;\n\tconst auto photoy = (height() - photos) / 2;\n\tp.drawImage(QRect{ x, photoy, photos, photos }, _icon->image(photos));\n\tx += photos + st::dialogsSearchInSkip;\n\tconst auto available = width()\n\t\t- x\n\t\t- st::dialogsSearchInCheckSkip\n\t\t- st::dialogsSearchInCheck.width()\n\t\t- st::dialogsSearchInCheckSkip;\n\n\tp.setPen(!enabled\n\t\t? _st.itemFgDisabled\n\t\t: selected\n\t\t? _st.itemFgOver\n\t\t: _st.itemFg);\n\t_text.drawLeftElided(\n\t\tp,\n\t\tx,\n\t\tst::dialogsSearchInNameTop,\n\t\tavailable,\n\t\twidth());\n\tx += available;\n\tif (_checked) {\n\t\tx += st::dialogsSearchInCheckSkip;\n\t\tconst auto &icon = st::dialogsSearchInCheck;\n\t\tconst auto icony = (height() - icon.height()) / 2;\n\t\ticon.paint(p, x, icony, width());\n\t}\n}\n\nbool Action::isEnabled() const {\n\treturn true;\n}\n\nnot_null<QAction*> Action::action() const {\n\treturn _dummyAction;\n}\n\nQPoint Action::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos());\n}\n\nQImage Action::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::RectMask(size());\n}\n\nint Action::contentHeight() const {\n\treturn _height;\n}\n\nvoid Action::handleKeyPress(not_null<QKeyEvent*> e) {\n\tif (!isSelected()) {\n\t\treturn;\n\t}\n\tconst auto key = e->key();\n\tif (key == Qt::Key_Enter || key == Qt::Key_Return) {\n\t\tsetClicked(Ui::Menu::TriggeredSource::Keyboard);\n\t}\n}\n\n} // namespace\n\nFixedHashtagSearchQuery FixHashtagSearchQuery(\n\t\tconst QString &query,\n\t\tint cursorPosition,\n\t\tHashOrCashtag tag) {\n\tconst auto trimmed = query.trimmed();\n\tconst auto hash = int(trimmed.isEmpty()\n\t\t? query.size()\n\t\t: query.indexOf(trimmed));\n\tconst auto start = std::min(cursorPosition, hash);\n\tconst auto first = QChar(tag == HashOrCashtag::Cashtag ? '$' : '#');\n\tauto result = query.mid(0, start);\n\tfor (const auto &ch : query.mid(start)) {\n\t\tif (ch.isSpace()) {\n\t\t\tif (cursorPosition > result.size()) {\n\t\t\t\t--cursorPosition;\n\t\t\t}\n\t\t\tcontinue;\n\t\t} else if (result.size() == start) {\n\t\t\tresult += first;\n\t\t\tif (ch != first) {\n\t\t\t\t++cursorPosition;\n\t\t\t}\n\t\t}\n\t\tif (ch != first) {\n\t\t\tresult += ch;\n\t\t}\n\t}\n\tif (result.size() == start) {\n\t\tresult += first;\n\t\t++cursorPosition;\n\t}\n\treturn { result, cursorPosition };\n}\n\nHashOrCashtag IsHashOrCashtagSearchQuery(const QString &query) {\n\tconst auto trimmed = query.trimmed();\n\tconst auto first = trimmed.isEmpty() ? QChar() : trimmed[0];\n\tif (first == '#') {\n\t\tfor (const auto &ch : trimmed) {\n\t\t\tif (ch.isSpace()) {\n\t\t\t\treturn HashOrCashtag::None;\n\t\t\t}\n\t\t}\n\t\treturn HashOrCashtag::Hashtag;\n\t} else if (first == '$') {\n\t\tfor (auto it = trimmed.begin() + 1; it != trimmed.end(); ++it) {\n\t\t\tif ((*it) < 'A' || (*it) > 'Z') {\n\t\t\t\treturn HashOrCashtag::None;\n\t\t\t}\n\t\t}\n\t\treturn HashOrCashtag::Cashtag;\n\t}\n\treturn HashOrCashtag::None;\n}\n\nvoid ChatSearchIn::Section::update() {\n\touter->update();\n}\n\nChatSearchIn::ChatSearchIn(QWidget *parent)\n: RpWidget(parent) {\n\t_in.clicks.events() | rpl::on_next([=] {\n\t\tshowMenu();\n\t}, lifetime());\n}\n\nChatSearchIn::~ChatSearchIn() = default;\n\nvoid ChatSearchIn::apply(\n\t\tstd::vector<PossibleTab> tabs,\n\t\tChatSearchTab active,\n\t\tChatSearchPeerTabType peerTabType,\n\t\tstd::shared_ptr<Ui::DynamicImage> fromUserpic,\n\t\tQString fromName) {\n\t_tabs = std::move(tabs);\n\t_peerTabType = peerTabType;\n\t_active = active;\n\tconst auto i = ranges::find(_tabs, active, &PossibleTab::tab);\n\tAssert(i != end(_tabs));\n\tAssert(i->icon != nullptr);\n\tupdateSection(\n\t\t&_in,\n\t\ti->icon->clone(),\n\t\ttr::semibold(TabLabel(active, peerTabType)));\n\n\tauto text = tr::lng_dlg_search_from(\n\t\ttr::now,\n\t\tlt_user,\n\t\ttr::semibold(fromName),\n\t\ttr::marked);\n\tupdateSection(&_from, std::move(fromUserpic), std::move(text));\n\n\tresizeToWidth(width());\n}\n\nrpl::producer<> ChatSearchIn::cancelInRequests() const {\n\treturn _in.cancelRequests.events();\n}\n\nrpl::producer<> ChatSearchIn::cancelFromRequests() const {\n\treturn _from.cancelRequests.events();\n}\n\nrpl::producer<> ChatSearchIn::changeFromRequests() const {\n\treturn _from.clicks.events();\n}\n\nrpl::producer<ChatSearchTab> ChatSearchIn::tabChanges() const {\n\treturn _active.changes();\n}\n\nvoid ChatSearchIn::showMenu() {\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\tst::dialogsSearchInMenu);\n\tconst auto active = _active.current();\n\tauto activeIndex = 0;\n\tfor (const auto &tab : _tabs) {\n\t\tif (!tab.icon) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto value = tab.tab;\n\t\tif (value == active) {\n\t\t\tactiveIndex = _menu->actions().size();\n\t\t}\n\t\tauto action = base::make_unique_q<Action>(\n\t\t\t_menu.get(),\n\t\t\ttab.icon,\n\t\t\tTabLabel(value, _peerTabType),\n\t\t\t(value == active));\n\t\taction->setActionTriggered([=] {\n\t\t\t_active = value;\n\t\t});\n\t\t_menu->addAction(std::move(action));\n\t}\n\tconst auto count = int(_menu->actions().size());\n\tconst auto bottomLeft = (activeIndex * 2 >= count);\n\tconst auto single = st::dialogsSearchInHeight;\n\tconst auto in = mapToGlobal(_in.outer->pos()\n\t\t+ QPoint(0, bottomLeft ? count * single : 0));\n\t_menu->setForcedOrigin(bottomLeft\n\t\t? Ui::PanelAnimation::Origin::BottomLeft\n\t\t: Ui::PanelAnimation::Origin::TopLeft);\n\tif (_menu->prepareGeometryFor(in)) {\n\t\t_menu->move(_menu->pos() - QPoint(_menu->inner().x(), activeIndex * single));\n\t\t_menu->popupPrepared();\n\t}\n}\n\nvoid ChatSearchIn::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\tconst auto top = QRect(0, 0, width(), st::searchedBarHeight);\n\tp.fillRect(top, st::searchedBarBg);\n\tp.fillRect(rect().translated(0, st::searchedBarHeight), st::dialogsBg);\n\n\tp.setFont(st::searchedBarFont);\n\tp.setPen(st::searchedBarFg);\n\tp.drawTextLeft(\n\t\tst::searchedBarPosition.x(),\n\t\tst::searchedBarPosition.y(),\n\t\twidth(),\n\t\ttr::lng_dlg_search_in(tr::now));\n}\n\nint ChatSearchIn::resizeGetHeight(int newWidth) {\n\tauto result = st::searchedBarHeight;\n\tif (const auto raw = _in.outer.get()) {\n\t\traw->resizeToWidth(newWidth);\n\t\traw->move(0, result);\n\t\tresult += raw->height();\n\t\t_in.shadow->setGeometry(0, result, newWidth, st::lineWidth);\n\t\tresult += st::lineWidth;\n\t}\n\tif (const auto raw = _from.outer.get()) {\n\t\traw->resizeToWidth(newWidth);\n\t\traw->move(0, result);\n\t\tresult += raw->height();\n\t\t_from.shadow->setGeometry(0, result, newWidth, st::lineWidth);\n\t\tresult += st::lineWidth;\n\t}\n\treturn result;\n}\n\nvoid ChatSearchIn::updateSection(\n\t\tnot_null<Section*> section,\n\t\tstd::shared_ptr<Ui::DynamicImage> image,\n\t\tTextWithEntities text) {\n\tif (section->subscribed) {\n\t\tsection->image->subscribeToUpdates(nullptr);\n\t\tsection->subscribed = false;\n\t}\n\tif (!image) {\n\t\tif (section->outer) {\n\t\t\tsection->cancel = nullptr;\n\t\t\tsection->shadow = nullptr;\n\t\t\tsection->outer = nullptr;\n\t\t\tsection->subscribed = false;\n\t\t}\n\t\treturn;\n\t} else if (!section->outer) {\n\t\tauto button = std::make_unique<Ui::AbstractButton>(this);\n\t\tconst auto raw = button.get();\n\t\tsection->outer = std::move(button);\n\n\t\traw->resize(\n\t\t\tst::columnMinimalWidthLeft,\n\t\t\tst::dialogsSearchInHeight);\n\n\t\traw->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(raw);\n\t\t\tif (!section->subscribed) {\n\t\t\t\tsection->subscribed = true;\n\t\t\t\tsection->image->subscribeToUpdates([=] {\n\t\t\t\t\traw->update();\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst auto outer = raw->width();\n\t\t\tconst auto size = st::dialogsSearchInPhotoSize;\n\t\t\tconst auto left = st::dialogsSearchInPhotoPadding;\n\t\t\tconst auto top = (st::dialogsSearchInHeight - size) / 2;\n\t\t\tp.drawImage(\n\t\t\t\tQRect{ left, top, size, size },\n\t\t\t\tsection->image->image(size));\n\n\t\t\tconst auto x = left + size + st::dialogsSearchInSkip;\n\t\t\tconst auto available = outer\n\t\t\t\t- st::dialogsSearchInSkip\n\t\t\t\t- section->cancel->width()\n\t\t\t\t- 2 * st::dialogsSearchInDownSkip\n\t\t\t\t- st::dialogsSearchInDown.width()\n\t\t\t\t- x;\n\t\t\tconst auto use = std::min(section->text.maxWidth(), available);\n\t\t\tconst auto iconx = x + use + st::dialogsSearchInDownSkip;\n\t\t\tconst auto icony = st::dialogsSearchInDownTop;\n\t\t\tst::dialogsSearchInDown.paint(p, iconx, icony, outer);\n\t\t\tp.setPen(st::windowBoldFg);\n\t\t\tsection->text.draw(p, {\n\t\t\t\t.position = QPoint(x, st::dialogsSearchInNameTop),\n\t\t\t\t.outerWidth = outer,\n\t\t\t\t.availableWidth = available,\n\t\t\t\t.elisionLines = 1,\n\t\t\t});\n\t\t}, raw->lifetime());\n\n\t\tsection->shadow = std::make_unique<Ui::PlainShadow>(this);\n\t\tsection->shadow->show();\n\n\t\tconst auto st = &st::dialogsCancelSearchInPeer;\n\t\tsection->cancel = std::make_unique<Ui::IconButton>(raw, *st);\n\t\tsection->cancel->setAccessibleName(tr::lng_cancel(tr::now));\n\t\tsection->cancel->show();\n\t\traw->sizeValue() | rpl::on_next([=](QSize size) {\n\t\t\tconst auto left = size.width() - section->cancel->width();\n\t\t\tconst auto top = (size.height() - st->height) / 2;\n\t\t\tsection->cancel->moveToLeft(left, top);\n\t\t}, section->cancel->lifetime());\n\t\tsection->cancel->clicks() | rpl::to_empty | rpl::start_to_stream(\n\t\t\tsection->cancelRequests,\n\t\t\tsection->cancel->lifetime());\n\n\t\traw->clicks() | rpl::to_empty | rpl::start_to_stream(\n\t\t\tsection->clicks,\n\t\t\traw->lifetime());\n\n\t\traw->show();\n\t}\n\tsection->image = std::move(image);\n\tsection->text.setMarkedText(st::dialogsSearchFromStyle, std::move(text));\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/chat_search_in.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\nclass PlainShadow;\nclass DynamicImage;\nclass IconButton;\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Dialogs {\n\nenum class ChatSearchTab : uchar {\n\tMyMessages,\n\tThisTopic,\n\tThisPeer,\n\tPublicPosts,\n};\n\nenum class ChatSearchPeerTabType : uchar {\n\tChat,\n\tChannel,\n\tGroup,\n};\n\nclass ChatSearchIn final : public Ui::RpWidget {\npublic:\n\texplicit ChatSearchIn(QWidget *parent);\n\t~ChatSearchIn();\n\n\tstruct PossibleTab {\n\t\tChatSearchTab tab = {};\n\t\tstd::shared_ptr<Ui::DynamicImage> icon;\n\t};\n\tvoid apply(\n\t\tstd::vector<PossibleTab> tabs,\n\t\tChatSearchTab active,\n\t\tChatSearchPeerTabType peerTabType,\n\t\tstd::shared_ptr<Ui::DynamicImage> fromUserpic,\n\t\tQString fromName);\n\n\t[[nodiscard]] rpl::producer<> cancelInRequests() const;\n\t[[nodiscard]] rpl::producer<> cancelFromRequests() const;\n\t[[nodiscard]] rpl::producer<> changeFromRequests() const;\n\t[[nodiscard]] rpl::producer<ChatSearchTab> tabChanges() const;\n\nprivate:\n\tstruct Section {\n\t\tstd::unique_ptr<Ui::RpWidget> outer;\n\t\tstd::unique_ptr<Ui::IconButton> cancel;\n\t\tstd::unique_ptr<Ui::PlainShadow> shadow;\n\t\tstd::shared_ptr<Ui::DynamicImage> image;\n\t\tUi::Text::String text;\n\t\trpl::event_stream<> clicks;\n\t\trpl::event_stream<> cancelRequests;\n\t\tbool subscribed = false;\n\n\t\tvoid update();\n\t};\n\n\tint resizeGetHeight(int newWidth) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid showMenu();\n\n\tvoid updateSection(\n\t\tnot_null<Section*> section,\n\t\tstd::shared_ptr<Ui::DynamicImage> image,\n\t\tTextWithEntities text);\n\n\tSection _in;\n\tSection _from;\n\trpl::variable<ChatSearchTab> _active;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tstd::vector<PossibleTab> _tabs;\n\tChatSearchPeerTabType _peerTabType = ChatSearchPeerTabType::Chat;\n\n};\n\nenum class HashOrCashtag : uchar {\n\tNone,\n\tHashtag,\n\tCashtag,\n};\n\nstruct FixedHashtagSearchQuery {\n\tQString text;\n\tint cursorPosition = 0;\n};\n[[nodiscard]] FixedHashtagSearchQuery FixHashtagSearchQuery(\n\tconst QString &query,\n\tint cursorPosition,\n\tHashOrCashtag tag);\n\n[[nodiscard]] HashOrCashtag IsHashOrCashtagSearchQuery(const QString &query);\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/ui/dialogs_layout.h\"\n\n#include \"base/options.h\"\n#include \"base/unixtime.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_drafts.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_thread.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"dialogs/dialogs_list.h\"\n#include \"dialogs/dialogs_three_state_icon.h\"\n#include \"dialogs/dialogs_quick_action.h\"\n#include \"dialogs/ui/dialogs_video_userpic.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history_unread_things.h\"\n#include \"history/view/history_view_item_preview.h\"\n#include \"history/view/history_view_send_action.h\"\n#include \"lang/lang_instance.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_session.h\"\n#include \"storage/localstorage.h\"\n#include \"support/support_helper.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/unread_badge.h\"\n#include \"ui/unread_badge_paint.h\"\n#include \"ui/unread_counter_format.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/data_peer_values.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_window.h\"\n\nnamespace Dialogs::Ui {\n\nconst char kOptionDialogsMuteIcon[] = \"dialogs-mute-icon\";\n\nnamespace {\n\nbase::options::toggle DialogsMuteIcon({\n\t.id = kOptionDialogsMuteIcon,\n\t.name = \"Mute icon in dialogs\",\n\t.description = \"Show a small mute icon next to the chat name \"\n\t\t\"for muted chats.\",\n});\n\nconst auto kPsaBadgePrefix = \"cloud_lng_badge_psa_\";\n\n[[nodiscard]] bool ShowUserBotIcon(not_null<UserData*> user) {\n\treturn user->isBot()\n\t\t&& !user->isSupport()\n\t\t&& !user->isRepliesChat()\n\t\t&& !user->isVerifyCodes();\n}\n\n[[nodiscard]] bool ShowSendActionInDialogs(Data::Thread *thread) {\n\tconst auto history = thread ? thread->owningHistory().get() : nullptr;\n\tif (!history || thread->asSublist()) {\n\t\treturn false;\n\t} else if (const auto user = history->peer->asUser()) {\n\t\treturn !user->lastseen().isHidden();\n\t}\n\treturn !history->isForum() && !history->amMonoforumAdmin();\n}\n\nvoid PaintRowTopRight(\n\t\tQPainter &p,\n\t\tconst QString &text,\n\t\tQRect &rectForName,\n\t\tconst PaintContext &context) {\n\tconst auto width = st::dialogsDateFont->width(text);\n\trectForName.setWidth(rectForName.width() - width - st::dialogsDateSkip);\n\tp.setFont(st::dialogsDateFont);\n\tp.setPen(context.active\n\t\t? st::dialogsDateFgActive\n\t\t: context.selected\n\t\t? st::dialogsDateFgOver\n\t\t: st::dialogsDateFg);\n\n\tif (text == tr::lng_status_online(tr::now)) {\n\t\tp.setPen(st::contactsStatusFgOnline);\n\t}\n\n\tp.drawText(\n\t\trectForName.left() + rectForName.width() + st::dialogsDateSkip,\n\t\trectForName.top() + st::semiboldFont->height - st::normalFont->descent,\n\t\ttext);\n}\n\nint PaintRightButtonImpl(QPainter &p, const PaintContext &context) {\n\tif (context.width < st::columnMinimalWidthLeft) {\n\t\treturn 0;\n\t}\n\tif (const auto rightButton = context.rightButton) {\n\t\tAssert(rightButton->st != nullptr);\n\n\t\tconst auto size = rightButton->bg.size() / style::DevicePixelRatio();\n\t\tconst auto left = context.width\n\t\t\t- size.width()\n\t\t\t- rightButton->st->margin.right();\n\t\tconst auto top = rightButton->st->margin.top();\n\t\tp.drawImage(\n\t\t\tleft,\n\t\t\ttop,\n\t\t\tcontext.active\n\t\t\t\t? rightButton->activeBg\n\t\t\t\t: context.selected\n\t\t\t\t? rightButton->selectedBg\n\t\t\t\t: rightButton->bg);\n\t\tif (rightButton->ripple) {\n\t\t\trightButton->ripple->paint(\n\t\t\t\tp,\n\t\t\t\tleft,\n\t\t\t\ttop,\n\t\t\t\tsize.width() - size.height() / 2,\n\t\t\t\t(context.active\n\t\t\t\t\t? &st::universalRippleAnimation.color->c\n\t\t\t\t\t: &rightButton->st->button.ripple.color->c));\n\t\t\tif (rightButton->ripple->empty()) {\n\t\t\t\trightButton->ripple.reset();\n\t\t\t}\n\t\t}\n\t\tp.setPen(context.active\n\t\t\t? rightButton->st->button.textBg\n\t\t\t: context.selected\n\t\t\t? rightButton->st->button.textFgOver\n\t\t\t: rightButton->st->button.textFg);\n\t\trightButton->text.draw(p, {\n\t\t\t.position = QPoint(\n\t\t\t\tleft + size.height() / 2,\n\t\t\t\ttop + rightButton->st->button.textTop),\n\t\t\t.outerWidth = size.width() - size.height() / 2,\n\t\t\t.availableWidth = size.width() - size.height() / 2,\n\t\t\t.elisionLines = 1,\n\t\t});\n\t\treturn size.width() + st::dialogsUnreadPadding;\n\t}\n\treturn 0;\n}\n\nint PaintBadges(\n\t\tQPainter &p,\n\t\tconst PaintContext &context,\n\t\tBadgesState badgesState,\n\t\tint right,\n\t\tint top,\n\t\tbool displayPinnedIcon,\n\t\tint pinnedIconTop,\n\t\tbool narrow) {\n\tconst auto paintIconBadge = [&](\n\t\t\tconst style::ThreeStateIcon &icons,\n\t\t\tbool muted,\n\t\t\tUnreadBadgeSize sizeId) {\n\t\tif (narrow) {\n\t\t\tauto st = UnreadBadgeStyle();\n\t\t\tst.sizeId = sizeId;\n\t\t\tst.active = context.active;\n\t\t\tst.selected = context.selected;\n\t\t\tst.muted = muted;\n\t\t\tst.padding = 0;\n\t\t\tst.textTop = 0;\n\t\t\tconst auto badge = PaintUnreadBadge(p, QString(), right, top, st);\n\t\t\tThreeStateIcon(icons, st.active, st.selected).paintInCenter(\n\t\t\t\tp,\n\t\t\t\tbadge);\n\t\t\tright -= badge.width() + st.padding + st::dialogsUnreadPadding;\n\t\t\treturn;\n\t\t}\n\t\tconst auto &icon = ThreeStateIcon(\n\t\t\ticons,\n\t\t\tcontext.active,\n\t\t\tcontext.selected);\n\t\ticon.paint(p, right - icon.width(), top, context.width);\n\t\tright -= icon.width() + st::dialogsUnreadPadding;\n\t};\n\tauto initial = right;\n\tauto painted = 0;\n\tif (badgesState.unread\n\t\t&& !badgesState.unreadCounter\n\t\t&& context.st->unreadMarkDiameter > 0) {\n\t\tconst auto d = context.st->unreadMarkDiameter;\n\t\tauto st = UnreadBadgeStyle();\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto rect = QRect(\n\t\t\tright - st.size + (st.size - d) / 2,\n\t\t\ttop + (st.size - d) / 2,\n\t\t\td,\n\t\t\td);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(badgesState.unreadMuted\n\t\t\t? (context.active\n\t\t\t\t? st::dialogsUnreadBgMutedActive\n\t\t\t\t: context.selected\n\t\t\t\t? st::dialogsUnreadBgMutedOver\n\t\t\t\t: st::dialogsUnreadBgMuted)\n\t\t\t: (context.active\n\t\t\t\t? st::dialogsUnreadBgActive\n\t\t\t\t: context.selected\n\t\t\t\t? st::dialogsUnreadBgOver\n\t\t\t\t: st::dialogsUnreadBg));\n\t\tp.drawEllipse(rect);\n\t\tright -= st.size + st.padding;\n\t\t++painted;\n\t} else if (badgesState.unread) {\n\t\tUnreadBadgeStyle st;\n\t\tst.active = context.active;\n\t\tst.selected = context.selected;\n\t\tst.muted = badgesState.unreadMuted;\n\t\tconst auto counter = FormatUnreadCounter(\n\t\t\tbadgesState.unreadCounter,\n\t\t\tbadgesState.mention || badgesState.reaction || badgesState.poll,\n\t\t\tnarrow);\n\t\tconst auto badge = PaintUnreadBadge(p, counter, right, top, st);\n\t\tright -= badge.width() + st.padding;\n\t\t++painted;\n\t} else if (const auto used = PaintRightButton(p, context)) {\n\t\treturn used - st::dialogsUnreadPadding;\n\t} else if (displayPinnedIcon) {\n\t\tconst auto &icon = ThreeStateIcon(\n\t\t\tst::dialogsPinnedIcon,\n\t\t\tcontext.active,\n\t\t\tcontext.selected);\n\t\ticon.paint(p, right - icon.width(), pinnedIconTop, context.width);\n\t\tright -= icon.width() + st::dialogsUnreadPadding;\n\t}\n\tif ((!narrow || (painted < 2))\n\t\t&& (badgesState.mention || badgesState.reaction)) {\n\t\tconst auto muted = badgesState.mention\n\t\t\t? badgesState.mentionMuted\n\t\t\t: badgesState.reactionMuted;\n\t\tpaintIconBadge(\n\t\t\tbadgesState.mention\n\t\t\t\t? (narrow\n\t\t\t\t\t? (muted\n\t\t\t\t\t\t? st::dialogsUnreadMentionBadgeMuted\n\t\t\t\t\t\t: st::dialogsUnreadMentionBadge)\n\t\t\t\t\t: (muted\n\t\t\t\t\t\t? st::dialogsUnreadMentionMuted\n\t\t\t\t\t\t: st::dialogsUnreadMention))\n\t\t\t\t: (narrow\n\t\t\t\t\t? (muted\n\t\t\t\t\t\t? st::dialogsUnreadReactionBadgeMuted\n\t\t\t\t\t\t: st::dialogsUnreadReactionBadge)\n\t\t\t\t\t: (muted\n\t\t\t\t\t\t? st::dialogsUnreadReactionMuted\n\t\t\t\t\t\t: st::dialogsUnreadReaction)),\n\t\t\tmuted,\n\t\t\tbadgesState.mention\n\t\t\t\t? UnreadBadgeSize::Dialogs\n\t\t\t\t: UnreadBadgeSize::ReactionInDialogs);\n\t\t++painted;\n\t}\n\tif ((!narrow || (painted < 2)) && badgesState.poll) {\n\t\tpaintIconBadge(\n\t\t\tnarrow\n\t\t\t\t? (badgesState.pollMuted\n\t\t\t\t\t? st::dialogsUnreadPollBadgeMuted\n\t\t\t\t\t: st::dialogsUnreadPollBadge)\n\t\t\t\t: (badgesState.pollMuted\n\t\t\t\t\t? st::dialogsUnreadPollMuted\n\t\t\t\t\t: st::dialogsUnreadPoll),\n\t\t\tbadgesState.pollMuted,\n\t\t\tUnreadBadgeSize::PollInDialogs);\n\t\t++painted;\n\t}\n\treturn (initial - right);\n}\n\nvoid PaintExpandedTopicsBar(QPainter &p, float64 progress) {\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto radius = st::roundRadiusLarge;\n\tconst auto width = st::forumDialogRow.padding.left() / 2;\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(st::dialogsBgActive);\n\tp.drawRoundedRect(\n\t\tQRectF(\n\t\t\t-3. * radius - width * (1. - progress),\n\t\t\tst::forumDialogRow.padding.top(),\n\t\t\t3. * radius + width,\n\t\t\tst::forumDialogRow.photoSize),\n\t\tradius,\n\t\tradius);\n}\n\nvoid PaintNarrowCounter(\n\t\tQPainter &p,\n\t\tconst PaintContext &context,\n\t\tBadgesState badgesState) {\n\tconst auto top = context.st->padding.top()\n\t\t+ context.st->photoSize\n\t\t- st::dialogsUnreadHeight;\n\tPaintBadges(\n\t\tp,\n\t\tcontext,\n\t\tbadgesState,\n\t\tcontext.st->padding.left() + context.st->photoSize,\n\t\ttop,\n\t\tfalse,\n\t\t0,\n\t\ttrue);\n}\n\nint PaintWideCounter(\n\t\tQPainter &p,\n\t\tconst PaintContext &context,\n\t\tBadgesState badgesState,\n\t\tint texttop,\n\t\tint availableWidth,\n\t\tbool displayPinnedIcon) {\n\tconst auto top = texttop\n\t\t+ st::dialogsTextFont->ascent\n\t\t- st::dialogsUnreadFont->ascent\n\t\t- (st::dialogsUnreadHeight - st::dialogsUnreadFont->height) / 2;\n\tconst auto used = PaintBadges(\n\t\tp,\n\t\tcontext,\n\t\tbadgesState,\n\t\tcontext.width - context.st->padding.right(),\n\t\ttop,\n\t\tdisplayPinnedIcon,\n\t\ttexttop,\n\t\tfalse);\n\treturn availableWidth - used;\n}\n\nvoid PaintFolderEntryText(\n\t\tPainter &p,\n\t\tnot_null<Data::Folder*> folder,\n\t\tconst PaintContext &context,\n\t\tQRect rect) {\n\tif (rect.isEmpty()) {\n\t\treturn;\n\t}\n\tfolder->validateListEntryCache();\n\tp.setFont(st::dialogsTextFont);\n\tp.setPen(context.active\n\t\t? st::dialogsTextFgActive\n\t\t: context.selected\n\t\t? st::dialogsTextFgOver\n\t\t: st::dialogsTextFg);\n\tfolder->listEntryCache().draw(p, {\n\t\t.position = rect.topLeft(),\n\t\t.availableWidth = rect.width(),\n\t\t.palette = &(context.active\n\t\t\t? st::dialogsTextPaletteArchiveActive\n\t\t\t: context.selected\n\t\t\t? st::dialogsTextPaletteArchiveOver\n\t\t\t: st::dialogsTextPaletteArchive),\n\t\t.spoiler = Text::DefaultSpoilerCache(),\n\t\t.now = context.now,\n\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t.elisionHeight = rect.height(),\n\t});\n}\n\nenum class Flag {\n\tSavedMessages    = 0x008,\n\tRepliesMessages  = 0x010,\n\tVerifyCodes      = 0x020,\n\tAllowUserOnline  = 0x040,\n\tTopicJumpRipple  = 0x080,\n\tHiddenAuthor     = 0x100,\n\tMyNotes          = 0x200,\n};\ninline constexpr bool is_flag_type(Flag) { return true; }\n\ntemplate <typename PaintItemCallback>\nvoid PaintRow(\n\t\tPainter &p,\n\t\tnot_null<const BasicRow*> row,\n\t\tQRect geometry,\n\t\tnot_null<Entry*> entry,\n\t\tVideoUserpic *videoUserpic,\n\t\tPeerData *from,\n\t\tPeerBadge &rowBadge,\n\t\tFn<void()> customEmojiRepaint,\n\t\tconst Text::String &rowName,\n\t\tconst HiddenSenderInfo *hiddenSenderInfo,\n\t\tHistoryItem *item,\n\t\tconst Data::Draft *draft,\n\t\tQDateTime date,\n\t\tconst PaintContext &context,\n\t\tBadgesState badgesState,\n\t\tbase::flags<Flag> flags,\n\t\tPaintItemCallback &&paintItemCallback) {\n\tconst auto supportMode = entry->session().supportMode();\n\tif (supportMode) {\n\t\tdraft = nullptr;\n\t}\n\n\tconst auto history = entry->asHistory();\n\tconst auto thread = entry->asThread();\n\tconst auto sublist = entry->asSublist();\n\n\tauto bg = context.active\n\t\t? st::dialogsBgActive\n\t\t: context.selected\n\t\t? st::dialogsBgOver\n\t\t: context.currentBg;\n\tauto swipeTranslation = 0;\n\tif (history\n\t\t&& context.quickActionContext\n\t\t&& !context.quickActionContext->ripple\n\t\t&& (history->peer->id.value\n\t\t\t== context.quickActionContext->data.msgBareId)) {\n\t\tif (context.quickActionContext->data.translation != 0) {\n\t\t\tswipeTranslation = context.quickActionContext->data.translation\n\t\t\t\t* -2;\n\t\t}\n\t}\n\tif (swipeTranslation) {\n\t\tp.translate(-swipeTranslation, 0);\n\t}\n\tp.fillRect(geometry, bg);\n\tif (!(flags & Flag::TopicJumpRipple)) {\n\t\tauto ripple = context.active\n\t\t\t? st::dialogsRippleBgActive\n\t\t\t: st::dialogsRippleBg;\n\t\trow->paintRipple(p, 0, 0, context.width, &ripple->c);\n\t}\n\n\tif (flags & Flag::SavedMessages) {\n\t\tEmptyUserpic::PaintSavedMessages(\n\t\t\tp,\n\t\t\tcontext.st->padding.left(),\n\t\t\tcontext.st->padding.top(),\n\t\t\tcontext.width,\n\t\t\tcontext.st->photoSize);\n\t} else if (flags & Flag::RepliesMessages) {\n\t\tEmptyUserpic::PaintRepliesMessages(\n\t\t\tp,\n\t\t\tcontext.st->padding.left(),\n\t\t\tcontext.st->padding.top(),\n\t\t\tcontext.width,\n\t\t\tcontext.st->photoSize);\n\t} else if (flags & Flag::HiddenAuthor) {\n\t\tEmptyUserpic::PaintHiddenAuthor(\n\t\t\tp,\n\t\t\tcontext.st->padding.left(),\n\t\t\tcontext.st->padding.top(),\n\t\t\tcontext.width,\n\t\t\tcontext.st->photoSize);\n\t} else if (flags & Flag::MyNotes) {\n\t\tEmptyUserpic::PaintMyNotes(\n\t\t\tp,\n\t\t\tcontext.st->padding.left(),\n\t\t\tcontext.st->padding.top(),\n\t\t\tcontext.width,\n\t\t\tcontext.st->photoSize);\n\t} else if (!from && hiddenSenderInfo) {\n\t\thiddenSenderInfo->emptyUserpic.paintCircle(\n\t\t\tp,\n\t\t\tcontext.st->padding.left(),\n\t\t\tcontext.st->padding.top(),\n\t\t\tcontext.width,\n\t\t\tcontext.st->photoSize);\n\t} else if (!(flags & Flag::AllowUserOnline)) {\n\t\tPaintUserpic(\n\t\t\tp,\n\t\t\tentry,\n\t\t\tfrom,\n\t\t\tvideoUserpic,\n\t\t\trow->userpicView(),\n\t\t\tcontext);\n\t} else {\n\t\trow->paintUserpic(\n\t\t\tp,\n\t\t\tentry,\n\t\t\tfrom,\n\t\t\tvideoUserpic,\n\t\t\tcontext,\n\t\t\t(context.narrow\n\t\t\t\t&& !badgesState.empty()\n\t\t\t\t&& !draft\n\t\t\t\t&& item\n\t\t\t\t&& !item->isEmpty()));\n\t}\n\n\tconst auto nameleft = context.st->nameLeft;\n\tif (context.topicsExpanded > 0.) {\n\t\tPaintExpandedTopicsBar(p, context.topicsExpanded);\n\t}\n\tif (context.narrow) {\n\t\tif (!draft && item && !item->isEmpty()) {\n\t\t\tPaintNarrowCounter(p, context, badgesState);\n\t\t}\n\t\treturn;\n\t}\n\n\tauto namewidth = context.width - nameleft - context.st->padding.right();\n\tauto rectForName = QRect(\n\t\tnameleft,\n\t\tcontext.st->nameTop,\n\t\tnamewidth,\n\t\tst::semiboldFont->height);\n\n\tconst auto promoted = (history && history->useTopPromotion())\n\t\t&& !context.search;\n\tconst auto verifyInfo = (from\n\t\t&& (!from->isSelf()\n\t\t\t|| (!(flags & Flag::SavedMessages) && !(flags & Flag::MyNotes))))\n\t\t? from->botVerifyDetails()\n\t\t: nullptr;\n\tif (promoted) {\n\t\tconst auto type = history->topPromotionType();\n\t\tconst auto custom = type.isEmpty()\n\t\t\t? QString()\n\t\t\t: Lang::GetNonDefaultValue(kPsaBadgePrefix + type.toUtf8());\n\t\tconst auto text = type.isEmpty()\n\t\t\t? tr::lng_proxy_sponsor(tr::now)\n\t\t\t: custom.isEmpty()\n\t\t\t? tr::lng_badge_psa_default(tr::now)\n\t\t\t: custom;\n\t\tPaintRowTopRight(p, text, rectForName, context);\n\t} else if (verifyInfo) {\n\t\tif (!rowBadge.ready(verifyInfo)) {\n\t\t\trowBadge.set(\n\t\t\t\tverifyInfo,\n\t\t\t\tfrom->owner().customEmojiManager().factory(),\n\t\t\t\tcustomEmojiRepaint);\n\t\t}\n\t\tconst auto &st = Ui::VerifiedStyle(context);\n\t\tconst auto position = rectForName.topLeft();\n\t\tconst auto skip = rowBadge.drawVerified(p, position, st);\n\t\trectForName.setLeft(position.x() + skip + st::dialogsChatTypeSkip);\n\t} else if (from) {\n\t\tif (const auto chatTypeIcon = ChatTypeIcon(from, context)) {\n\t\t\tchatTypeIcon->paint(p, rectForName.topLeft(), context.width);\n\t\t\trectForName.setLeft(rectForName.left()\n\t\t\t\t+ chatTypeIcon->width()\n\t\t\t\t+ st::dialogsChatTypeSkip);\n\t\t}\n\t}\n\tauto texttop = context.st->textTop;\n\tif (const auto folder = entry->asFolder()) {\n\t\tconst auto availableWidth = PaintWideCounter(\n\t\t\tp,\n\t\t\tcontext,\n\t\t\tbadgesState,\n\t\t\ttexttop,\n\t\t\tnamewidth,\n\t\t\tfalse);\n\t\tconst auto rect = QRect(\n\t\t\tnameleft,\n\t\t\ttexttop,\n\t\t\tavailableWidth,\n\t\t\tst::dialogsTextFont->height);\n\t\tPaintFolderEntryText(p, folder, context, rect);\n\t} else if (promoted && !history->topPromotionMessage().isEmpty()) {\n\t\tauto availableWidth = namewidth;\n\t\tp.setFont(st::dialogsTextFont);\n\t\tif (history->cloudDraftTextCache().isEmpty()) {\n\t\t\thistory->cloudDraftTextCache().setText(\n\t\t\t\tst::dialogsTextStyle,\n\t\t\t\thistory->topPromotionMessage(),\n\t\t\t\tDialogTextOptions());\n\t\t}\n\t\tp.setPen(context.active\n\t\t\t? st::dialogsTextFgActive\n\t\t\t: context.selected\n\t\t\t? st::dialogsTextFgOver\n\t\t\t: st::dialogsTextFg);\n\t\thistory->cloudDraftTextCache().draw(p, {\n\t\t\t.position = { nameleft, texttop },\n\t\t\t.availableWidth = availableWidth,\n\t\t\t.spoiler = Text::DefaultSpoilerCache(),\n\t\t\t.now = context.now,\n\t\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t\t.elisionLines = 1,\n\t\t});\n\t} else if (draft\n\t\t|| (supportMode\n\t\t\t&& entry->session().supportHelper().isOccupiedBySomeone(history))) {\n\t\tif (!promoted) {\n\t\t\tconst auto dateString = Ui::FormatDialogsDate(date);\n\t\t\tPaintRowTopRight(p, dateString, rectForName, context);\n\t\t}\n\n\t\tauto availableWidth = namewidth;\n\t\tif (const auto used = PaintRightButton(p, context)) {\n\t\t\tavailableWidth -= used;\n\t\t} else if (entry->isPinnedDialog(context.filter)\n\t\t\t&& (context.filter || !entry->fixedOnTopIndex())) {\n\t\t\tauto &icon = ThreeStateIcon(\n\t\t\t\tst::dialogsPinnedIcon,\n\t\t\t\tcontext.active,\n\t\t\t\tcontext.selected);\n\t\t\ticon.paint(\n\t\t\t\tp,\n\t\t\t\tcontext.width - context.st->padding.right() - icon.width(),\n\t\t\t\ttexttop,\n\t\t\t\tcontext.width);\n\t\t\tavailableWidth -= icon.width() + st::dialogsUnreadPadding;\n\t\t}\n\n\t\tp.setFont(st::dialogsTextFont);\n\t\tauto &color = context.active\n\t\t\t? st::dialogsTextFgServiceActive\n\t\t\t: context.selected\n\t\t\t? st::dialogsTextFgServiceOver\n\t\t\t: st::dialogsTextFgService;\n\t\tif (!ShowSendActionInDialogs(thread)\n\t\t\t|| !thread->sendActionPainter()->paint(\n\t\t\t\tp,\n\t\t\t\tnameleft,\n\t\t\t\ttexttop,\n\t\t\t\tavailableWidth,\n\t\t\t\tcontext.width,\n\t\t\t\tcolor,\n\t\t\t\tcontext.paused)) {\n\t\t\tauto &cache = thread->cloudDraftTextCache();\n\t\t\tif (cache.isEmpty()) {\n\t\t\t\tusing namespace TextUtilities;\n\t\t\t\tauto draftWrapped = Text::Colorized(\n\t\t\t\t\ttr::lng_dialogs_text_from_wrapped(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\ttr::lng_from_draft(tr::now)));\n\t\t\t\tauto draftText = supportMode\n\t\t\t\t\t? Text::Colorized(\n\t\t\t\t\t\tSupport::ChatOccupiedString(history))\n\t\t\t\t\t: tr::lng_dialogs_text_with_from(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_from_part,\n\t\t\t\t\t\tstd::move(draftWrapped),\n\t\t\t\t\t\tlt_message,\n\t\t\t\t\t\tDialogsPreviewText({\n\t\t\t\t\t\t\t.text = draft->textWithTags.text,\n\t\t\t\t\t\t\t.entities = ConvertTextTagsToEntities(\n\t\t\t\t\t\t\t\tdraft->textWithTags.tags),\n\t\t\t\t\t\t}),\n\t\t\t\t\t\ttr::marked);\n\t\t\t\tif (draft && draft->reply) {\n\t\t\t\t\tdraftText = Ui::Text::Colorized(\n\t\t\t\t\t\tUi::Text::IconEmoji(&st::dialogsMiniReplyIcon)\n\t\t\t\t\t).append(std::move(draftText));\n\t\t\t\t}\n\t\t\t\tconst auto context = Core::TextContext({\n\t\t\t\t\t.session = &thread->session(),\n\t\t\t\t\t.repaint = customEmojiRepaint,\n\t\t\t\t});\n\t\t\t\tcache.setMarkedText(\n\t\t\t\t\tst::dialogsTextStyle,\n\t\t\t\t\tstd::move(draftText),\n\t\t\t\t\tDialogTextOptions(),\n\t\t\t\t\tcontext);\n\t\t\t}\n\t\t\tp.setPen(context.active\n\t\t\t\t? st::dialogsTextFgActive\n\t\t\t\t: context.selected\n\t\t\t\t? st::dialogsTextFgOver\n\t\t\t\t: st::dialogsTextFg);\n\t\t\tcache.draw(p, {\n\t\t\t\t.position = { nameleft, texttop },\n\t\t\t\t.availableWidth = availableWidth,\n\t\t\t\t.palette = &(supportMode\n\t\t\t\t\t? (context.active\n\t\t\t\t\t\t? st::dialogsTextPaletteTakenActive\n\t\t\t\t\t\t: context.selected\n\t\t\t\t\t\t? st::dialogsTextPaletteTakenOver\n\t\t\t\t\t\t: st::dialogsTextPaletteTaken)\n\t\t\t\t\t: (context.active\n\t\t\t\t\t\t? st::dialogsTextPaletteDraftActive\n\t\t\t\t\t\t: context.selected\n\t\t\t\t\t\t? st::dialogsTextPaletteDraftOver\n\t\t\t\t\t\t: st::dialogsTextPaletteDraft)),\n\t\t\t\t.spoiler = Text::DefaultSpoilerCache(),\n\t\t\t\t.now = context.now,\n\t\t\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t\t\t.elisionLines = 1,\n\t\t\t});\n\t\t}\n\t} else if (!item) {\n\t\tauto availableWidth = namewidth;\n\t\tif (const auto used = PaintRightButton(p, context)) {\n\t\t\tavailableWidth -= used;\n\t\t} else if (entry->isPinnedDialog(context.filter)\n\t\t\t&& (context.filter || !entry->fixedOnTopIndex())) {\n\t\t\tauto &icon = ThreeStateIcon(\n\t\t\t\tst::dialogsPinnedIcon,\n\t\t\t\tcontext.active,\n\t\t\t\tcontext.selected);\n\t\t\ticon.paint(p, context.width - context.st->padding.right() - icon.width(), texttop, context.width);\n\t\t\tavailableWidth -= icon.width() + st::dialogsUnreadPadding;\n\t\t}\n\n\t\tauto &color = context.active\n\t\t\t? st::dialogsTextFgServiceActive\n\t\t\t: context.selected\n\t\t\t? st::dialogsTextFgServiceOver\n\t\t\t: st::dialogsTextFgService;\n\t\tp.setFont(st::dialogsTextFont);\n\t\tif (!ShowSendActionInDialogs(thread)\n\t\t\t|| !thread->sendActionPainter()->paint(\n\t\t\t\tp,\n\t\t\t\tnameleft,\n\t\t\t\ttexttop,\n\t\t\t\tavailableWidth,\n\t\t\t\tcontext.width,\n\t\t\t\tcolor,\n\t\t\t\tcontext.now)) {\n\t\t\t// Empty history\n\t\t}\n\t} else if (!item->isEmpty()) {\n\t\tif ((thread || sublist) && !promoted) {\n\t\t\tconst auto dateString = Ui::FormatDialogsDate(date);\n\t\t\tPaintRowTopRight(p, dateString, rectForName, context);\n\t\t}\n\n\t\tpaintItemCallback(nameleft, namewidth);\n\t} else if (entry->isPinnedDialog(context.filter)\n\t\t&& (context.filter || !entry->fixedOnTopIndex())) {\n\t\tauto &icon = ThreeStateIcon(\n\t\t\tst::dialogsPinnedIcon,\n\t\t\tcontext.active,\n\t\t\tcontext.selected);\n\t\ticon.paint(\n\t\t\tp,\n\t\t\tcontext.width - context.st->padding.right() - icon.width(),\n\t\t\ttexttop,\n\t\t\tcontext.width);\n\t}\n\tconst auto sendStateIcon = [&]() -> const style::icon* {\n\t\tif (!thread) {\n\t\t\treturn nullptr;\n\t\t} else if (const auto topic = thread->asTopic()\n\t\t\t; !context.search && topic && topic->closed()) {\n\t\t\treturn &ThreeStateIcon(\n\t\t\t\tst::dialogsLockIcon,\n\t\t\t\tcontext.active,\n\t\t\t\tcontext.selected);\n\t\t} else if (draft) {\n\t\t\tif (draft->saveRequestId) {\n\t\t\t\treturn &ThreeStateIcon(\n\t\t\t\t\tst::dialogsSendingIcon,\n\t\t\t\t\tcontext.active,\n\t\t\t\t\tcontext.selected);\n\t\t\t}\n\t\t} else if (item && !item->isEmpty() && item->needCheck()) {\n\t\t\tif (!item->isSending() && !item->hasFailed()) {\n\t\t\t\tif (item->unread(thread)) {\n\t\t\t\t\treturn &ThreeStateIcon(\n\t\t\t\t\t\tst::dialogsSentIcon,\n\t\t\t\t\t\tcontext.active,\n\t\t\t\t\t\tcontext.selected);\n\t\t\t\t}\n\t\t\t\treturn &ThreeStateIcon(\n\t\t\t\t\tst::dialogsReceivedIcon,\n\t\t\t\t\tcontext.active,\n\t\t\t\t\tcontext.selected);\n\t\t\t}\n\t\t\treturn &ThreeStateIcon(\n\t\t\t\tst::dialogsSendingIcon,\n\t\t\t\tcontext.active,\n\t\t\t\tcontext.selected);\n\t\t}\n\t\treturn nullptr;\n\t}();\n\tif (sendStateIcon) {\n\t\trectForName.setWidth(rectForName.width() - st::dialogsSendStateSkip);\n\t\tsendStateIcon->paint(p, rectForName.topLeft() + QPoint(rectForName.width(), 0), context.width);\n\t}\n\tif (from) {\n\t\tif (from->isUser() && Core::App().settings().fork().lastSeenInDialogs()) {\n\t\t\tconst auto user = from->asUser();\n\t\t\tconst auto now = base::unixtime::now();\n\t\t\tconst auto status = user->lastseen();\n\t\t\tif (status.isOnline(now)) {\n\t\t\t\tPaintRowTopRight(p, tr::lng_status_online(tr::now), rectForName, context);\n\t\t\t} else {\n\t\t\t\tconst auto till = status.onlineTill();\n\t\t\t\tif (till > 0) {\n\t\t\t\t\tconst auto elapsed = now - till;\n\t\t\t\t\tconst auto text = Ui::FormatMuteForTiny(elapsed);\n\t\t\t\t\tif (!text.isEmpty()) {\n\t\t\t\t\t\tPaintRowTopRight(p, text, rectForName, context);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tp.setFont(st::semiboldFont);\n\tconst auto paintPeerBadge = [&](int rowNameWidth) {\n\t\tconst auto badgeWidth = rowBadge.drawGetWidth(p, {\n\t\t\t.peer = from,\n\t\t\t.rectForName = rectForName,\n\t\t\t.nameWidth = rowNameWidth,\n\t\t\t.outerWidth = context.width,\n\t\t\t.verified = (context.active\n\t\t\t\t? &st::dialogsVerifiedIconActive\n\t\t\t\t: context.selected\n\t\t\t\t? &st::dialogsVerifiedIconOver\n\t\t\t\t: &st::dialogsVerifiedIcon),\n\t\t\t.premium = &ThreeStateIcon(\n\t\t\t\tst::dialogsPremiumIcon,\n\t\t\t\tcontext.active,\n\t\t\t\tcontext.selected),\n\t\t\t.scam = (context.active\n\t\t\t\t? &st::dialogsScamFgActive\n\t\t\t\t: context.selected\n\t\t\t\t? &st::dialogsScamFgOver\n\t\t\t\t: &st::dialogsScamFg),\n\t\t\t.direct = (context.active\n\t\t\t\t? &st::dialogsDraftFgActive\n\t\t\t\t: context.selected\n\t\t\t\t? &st::windowSubTextFgOver\n\t\t\t\t: &st::windowSubTextFg),\n\t\t\t.premiumFg = (context.active\n\t\t\t\t? &st::dialogsVerifiedIconBgActive\n\t\t\t\t: context.selected\n\t\t\t\t? &st::dialogsVerifiedIconBgOver\n\t\t\t\t: &st::dialogsVerifiedIconBg),\n\t\t\t.customEmojiRepaint = customEmojiRepaint,\n\t\t\t.now = context.now,\n\t\t\t.paused = context.paused,\n\t\t});\n\t\trectForName.setWidth(rectForName.width() - badgeWidth);\n\t};\n\tif (flags\n\t\t& (Flag::SavedMessages\n\t\t\t| Flag::RepliesMessages\n\t\t\t| Flag::VerifyCodes\n\t\t\t| Flag::HiddenAuthor\n\t\t\t| Flag::MyNotes)) {\n\t\tauto text = (flags & Flag::SavedMessages)\n\t\t\t? tr::lng_saved_messages(tr::now)\n\t\t\t: (flags & Flag::RepliesMessages)\n\t\t\t? tr::lng_replies_messages(tr::now)\n\t\t\t: (flags & Flag::VerifyCodes)\n\t\t\t? tr::lng_verification_codes(tr::now)\n\t\t\t: (flags & Flag::MyNotes)\n\t\t\t? tr::lng_my_notes(tr::now)\n\t\t\t: tr::lng_hidden_author_messages(tr::now);\n\t\tconst auto textWidth = st::semiboldFont->width(text);\n\t\tif (!context.search && (flags & Flag::VerifyCodes)) {\n\t\t\tpaintPeerBadge(textWidth);\n\t\t}\n\t\tif (textWidth > rectForName.width()) {\n\t\t\ttext = st::semiboldFont->elided(text, rectForName.width());\n\t\t}\n\t\tp.setPen(context.active\n\t\t\t? st::dialogsNameFgActive\n\t\t\t: context.selected\n\t\t\t? st::dialogsNameFgOver\n\t\t\t: st::dialogsNameFg);\n\t\tp.drawTextLeft(\n\t\t\trectForName.left(),\n\t\t\trectForName.top(),\n\t\t\tcontext.width,\n\t\t\ttext);\n\t} else if (from) {\n\t\tauto badgeWidth = 0;\n\t\tif ((history || sublist) && !context.search) {\n\t\t\tconst auto widthBefore = rectForName.width();\n\t\t\tpaintPeerBadge(rowName.maxWidth());\n\t\t\tbadgeWidth = widthBefore - rectForName.width();\n\t\t}\n\t\tconst auto drawMuteIcon = DialogsMuteIcon.value()\n\t\t\t&& thread\n\t\t\t&& thread->muted();\n\t\tif (drawMuteIcon) {\n\t\t\tconst auto &muteIcon = ThreeStateIcon(\n\t\t\t\tst::dialogsMuteIcon,\n\t\t\t\tcontext.active,\n\t\t\t\tcontext.selected);\n\t\t\trectForName.setWidth(\n\t\t\t\trectForName.width()\n\t\t\t\t\t- muteIcon.width()\n\t\t\t\t\t- st::dialogsMuteIconSkip);\n\t\t}\n\t\tp.setPen(context.active\n\t\t\t? st::dialogsNameFgActive\n\t\t\t: context.selected\n\t\t\t? st::dialogsNameFgOver\n\t\t\t: st::dialogsNameFg);\n\t\trowName.draw(p, {\n\t\t\t.position = rectForName.topLeft(),\n\t\t\t.availableWidth = rectForName.width(),\n\t\t\t.elisionLines = 1,\n\t\t});\n\t\tif (drawMuteIcon) {\n\t\t\tconst auto &muteIcon = ThreeStateIcon(\n\t\t\t\tst::dialogsMuteIcon,\n\t\t\t\tcontext.active,\n\t\t\t\tcontext.selected);\n\t\t\tconst auto nameW = std::min(\n\t\t\t\trowName.maxWidth(),\n\t\t\t\trectForName.width());\n\t\t\tconst auto muteLeft = rectForName.left()\n\t\t\t\t+ nameW\n\t\t\t\t+ badgeWidth\n\t\t\t\t+ st::dialogsMuteIconSkip;\n\t\t\tconst auto muteTop = rectForName.top()\n\t\t\t\t+ (st::semiboldFont->height - muteIcon.height()) / 2;\n\t\t\tmuteIcon.paint(p, muteLeft, muteTop, context.width);\n\t\t}\n\t} else if (hiddenSenderInfo) {\n\t\tp.setPen(context.active\n\t\t\t? st::dialogsNameFgActive\n\t\t\t: context.selected\n\t\t\t? st::dialogsNameFgOver\n\t\t\t: st::dialogsNameFg);\n\t\thiddenSenderInfo->nameText().draw(p, {\n\t\t\t.position = rectForName.topLeft(),\n\t\t\t.availableWidth = rectForName.width(),\n\t\t\t.elisionLines = 1,\n\t\t});\n\t} else {\n\t\tp.setPen(context.active\n\t\t\t? st::dialogsNameFgActive\n\t\t\t: entry->folder()\n\t\t\t? (context.selected\n\t\t\t\t? st::dialogsArchiveFgOver\n\t\t\t\t: st::dialogsArchiveFg)\n\t\t\t: (context.selected\n\t\t\t\t? st::dialogsNameFgOver\n\t\t\t\t: st::dialogsNameFg));\n\t\trowName.draw(p, {\n\t\t\t.position = rectForName.topLeft(),\n\t\t\t.availableWidth = rectForName.width(),\n\t\t\t.elisionLines = 1,\n\t\t});\n\t}\n\n\tif (const auto tags = context.chatsFilterTags) {\n\t\tauto left = nameleft;\n\t\tfor (const auto &tag : *tags) {\n\t\t\tp.drawImage(left, context.st->tagTop, *tag);\n\t\t\tleft += st::dialogRowFilterTagSkip\n\t\t\t\t+ (tag->width() / style::DevicePixelRatio());\n\t\t}\n\t}\n\tif (swipeTranslation) {\n\t\tp.translate(swipeTranslation, 0);\n\t\tconst auto swipeActionRect = QRect(\n\t\t\trect::right(geometry) - swipeTranslation,\n\t\t\tgeometry.y(),\n\t\t\tswipeTranslation,\n\t\t\tgeometry.height());\n\t\tp.setClipRegion(swipeActionRect);\n\t\tconst auto labelType = ResolveQuickDialogLabel(\n\t\t\thistory,\n\t\t\tcontext.quickActionContext->action,\n\t\t\tcontext.filter);\n\t\tp.fillRect(swipeActionRect, ResolveQuickActionBg(labelType));\n\t\tif (context.quickActionContext->data.reachRatio) {\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(ResolveQuickActionBgActive(labelType));\n\t\t\tconst auto r = swipeTranslation\n\t\t\t\t* context.quickActionContext->data.reachRatio;\n\t\t\tconst auto offset = st::dialogsQuickActionSize\n\t\t\t\t+ st::dialogsQuickActionSize / 2.;\n\t\t\tp.drawEllipse(QPointF(geometry.width() - offset, offset), r, r);\n\t\t}\n\t\tconst auto quickWidth = st::dialogsQuickActionSize * 3;\n\t\tif (context.quickActionContext->icon) {\n\t\t\tDrawQuickAction(\n\t\t\t\tp,\n\t\t\t\tQRect(\n\t\t\t\t\trect::right(geometry) - quickWidth,\n\t\t\t\t\tgeometry.y(),\n\t\t\t\t\tquickWidth,\n\t\t\t\t\tgeometry.height()),\n\t\t\t\tcontext.quickActionContext->icon.get(),\n\t\t\t\tlabelType);\n\t\t}\n\t\tp.setClipping(false);\n\t}\n\tif (const auto quick = context.quickActionContext;\n\t\t\tquick && quick->ripple && quick->rippleFg) {\n\t\tconst auto labelType = ResolveQuickDialogLabel(\n\t\t\thistory,\n\t\t\tcontext.quickActionContext->action,\n\t\t\tcontext.filter);\n\t\tconst auto ripple = ResolveQuickActionBg(labelType);\n\t\tconst auto size = st::dialogsQuickActionRippleSize;\n\t\tconst auto x = geometry.width() - size;\n\t\tquick->ripple->paint(p, x, 0, size, &ripple->c);\n\t\tquick->rippleFg->paint(p, x, 0, size, &st::premiumButtonFg->c);\n\t\tif (quick->ripple->empty()) {\n\t\t\tquick->ripple.reset();\n\t\t}\n\t\tif (quick->rippleFg->empty()) {\n\t\t\tquick->rippleFg.reset();\n\t\t}\n\t}\n}\n\n} // namespace\n\nconst style::icon *ChatTypeIcon(not_null<PeerData*> peer) {\n\treturn ChatTypeIcon(peer, {\n\t\t.st = &st::defaultDialogRow,\n\t\t.currentBg = st::windowBg,\n\t});\n}\n\nconst style::icon *ChatTypeIcon(\n\t\tnot_null<PeerData*> peer,\n\t\tconst PaintContext &context) {\n\tif (const auto user = peer->asUser()) {\n\t\tif (ShowUserBotIcon(user)) {\n\t\t\treturn &ThreeStateIcon(\n\t\t\t\tst::dialogsBotIcon,\n\t\t\t\tcontext.active,\n\t\t\t\tcontext.selected);\n\t\t}\n\t} else if (peer->isBroadcast()) {\n\t\treturn &ThreeStateIcon(\n\t\t\tst::dialogsChannelIcon,\n\t\t\tcontext.active,\n\t\t\tcontext.selected);\n\t} else if (peer->isForum()) {\n\t\treturn &ThreeStateIcon(\n\t\t\tst::dialogsForumIcon,\n\t\t\tcontext.active,\n\t\t\tcontext.selected);\n\t} else if (!peer->isMonoforum()) {\n\t\treturn &ThreeStateIcon(\n\t\t\tst::dialogsChatIcon,\n\t\t\tcontext.active,\n\t\t\tcontext.selected);\n\t}\n\treturn nullptr;\n}\n\nconst style::VerifiedBadge &VerifiedStyle(const PaintContext &context) {\n\treturn context.active\n\t\t? st::dialogsVerifiedColorsActive\n\t\t: context.selected\n\t\t? st::dialogsVerifiedColorsOver\n\t\t: st::dialogsVerifiedColors;\n}\n\nvoid RowPainter::Paint(\n\t\tPainter &p,\n\t\tnot_null<const Row*> row,\n\t\tVideoUserpic *videoUserpic,\n\t\tconst PaintContext &context) {\n\tconst auto entry = row->entry();\n\tconst auto history = row->history();\n\tconst auto thread = row->thread();\n\tconst auto sublist = row->sublist();\n\tconst auto peer = history ? history->peer.get() : nullptr;\n\tconst auto badgesState = entry->chatListBadgesState();\n\tentry->chatListPreloadData(); // Allow chat list message resolve.\n\tconst auto item = entry->chatListMessage();\n\tconst auto cloudDraft = [&]() -> const Data::Draft*{\n\t\tif (!thread) {\n\t\t\treturn nullptr;\n\t\t}\n\t\tif ((!peer || (!peer->isForum() && !peer->amMonoforumAdmin()))\n\t\t\t&& (!item || !badgesState.unread)) {\n\t\t\t// Draw item, if there are unread messages.\n\t\t\tconst auto draft = thread->owningHistory()->cloudDraft(\n\t\t\t\tthread->topicRootId(),\n\t\t\t\tthread->monoforumPeerId());\n\t\t\tif (!Data::DraftIsNull(draft)) {\n\t\t\t\treturn draft;\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t}();\n\tconst auto displayDate = [&] {\n\t\tif (item) {\n\t\t\tif (cloudDraft) {\n\t\t\t\treturn (item->date() > cloudDraft->date)\n\t\t\t\t\t? ItemDateTime(item)\n\t\t\t\t\t: base::unixtime::parse(cloudDraft->date);\n\t\t\t}\n\t\t\treturn ItemDateTime(item);\n\t\t}\n\t\treturn cloudDraft\n\t\t\t? base::unixtime::parse(cloudDraft->date)\n\t\t\t: QDateTime();\n\t}();\n\tconst auto displayPinnedIcon = badgesState.empty()\n\t\t&& entry->isPinnedDialog(context.filter)\n\t\t&& (context.filter || !entry->fixedOnTopIndex());\n\n\tconst auto from = history\n\t\t? (history->peer->migrateTo()\n\t\t\t? history->peer->migrateTo()\n\t\t\t: history->peer.get())\n\t\t: sublist\n\t\t? sublist->sublistPeer().get()\n\t\t: nullptr;\n\tconst auto allowUserOnline = true;// !context.narrow || badgesState.empty();\n\tconst auto flags = (allowUserOnline ? Flag::AllowUserOnline : Flag(0))\n\t\t| ((sublist && !sublist->parentChat() && from->isSelf())\n\t\t\t? Flag::MyNotes\n\t\t\t: (peer && peer->isSelf())\n\t\t\t? Flag::SavedMessages\n\t\t\t: Flag(0))\n\t\t| ((from && from->isRepliesChat())\n\t\t\t? Flag::RepliesMessages\n\t\t\t: Flag(0))\n\t\t| ((from && from->isVerifyCodes())\n\t\t\t? Flag::VerifyCodes\n\t\t\t: Flag(0))\n\t\t| ((sublist && from->isSavedHiddenAuthor())\n\t\t\t? Flag::HiddenAuthor\n\t\t\t: Flag(0))\n\t\t| (row->topicJumpRipple() ? Flag::TopicJumpRipple : Flag(0));\n\tconst auto paintItemCallback = [&](int nameleft, int namewidth) {\n\t\tconst auto texttop = context.st->textTop;\n\t\tconst auto availableWidth = PaintWideCounter(\n\t\t\tp,\n\t\t\tcontext,\n\t\t\tbadgesState,\n\t\t\ttexttop,\n\t\t\tnamewidth,\n\t\t\tdisplayPinnedIcon);\n\t\tconst auto &color = context.active\n\t\t\t? st::dialogsTextFgServiceActive\n\t\t\t: context.selected\n\t\t\t? st::dialogsTextFgServiceOver\n\t\t\t: st::dialogsTextFgService;\n\t\tauto rect = QRect(\n\t\t\tnameleft,\n\t\t\ttexttop,\n\t\t\tavailableWidth,\n\t\t\tst::dialogsTextFont->height);\n\t\tconst auto actionWasPainted = ShowSendActionInDialogs(thread)\n\t\t\t? thread->sendActionPainter()->paint(\n\t\t\t\tp,\n\t\t\t\trect.x(),\n\t\t\t\trect.y(),\n\t\t\t\trect.width(),\n\t\t\t\tcontext.width,\n\t\t\t\tcolor,\n\t\t\t\tcontext.now)\n\t\t\t: false;\n\t\tconst auto view = actionWasPainted\n\t\t\t? nullptr\n\t\t\t: thread\n\t\t\t? &thread->lastItemDialogsView()\n\t\t\t: nullptr;\n\t\tif (view) {\n\t\t\tconst auto forum = (peer && context.st->topicsHeight)\n\t\t\t\t? peer->forum()\n\t\t\t\t: nullptr;\n\t\t\tconst auto monoforum = (peer && context.st->topicsHeight)\n\t\t\t\t? peer->monoforum()\n\t\t\t\t: nullptr;\n\t\t\tif (!view->prepared(item, forum, monoforum)) {\n\t\t\t\tview->prepare(\n\t\t\t\t\titem,\n\t\t\t\t\tforum,\n\t\t\t\t\tmonoforum,\n\t\t\t\t\t[=] { entry->updateChatListEntry(); },\n\t\t\t\t\t{});\n\t\t\t}\n\t\t\tif (forum || monoforum) {\n\t\t\t\trect.setHeight(context.st->topicsHeight + rect.height());\n\t\t\t}\n\t\t\tview->paint(p, rect, context);\n\t\t}\n\t};\n\tPaintRow(\n\t\tp,\n\t\trow,\n\t\tQRect(0, 0, context.width, row->height()),\n\t\tentry,\n\t\tvideoUserpic,\n\t\tfrom,\n\t\tentry->chatListPeerBadge(),\n\t\t[=] { entry->updateChatListEntry(); },\n\t\tentry->chatListNameText(),\n\t\tnullptr,\n\t\titem,\n\t\tcloudDraft,\n\t\tdisplayDate,\n\t\tcontext,\n\t\tbadgesState,\n\t\tflags,\n\t\tpaintItemCallback);\n}\n\nvoid RowPainter::Paint(\n\t\tPainter &p,\n\t\tnot_null<const FakeRow*> row,\n\t\tconst PaintContext &context) {\n\tconst auto item = row->item();\n\tconst auto topic = context.forum ? row->topic() : nullptr;\n\tconst auto history = topic ? nullptr : item->history().get();\n\tconst auto entry = topic ? (Entry*)topic : (Entry*)history;\n\tauto cloudDraft = nullptr;\n\tconst auto from = [&] {\n\t\tconst auto in = row->searchInChat();\n\t\treturn (topic && (in.topic() != topic))\n\t\t\t? nullptr\n\t\t\t: in\n\t\t\t? item->displayFrom()\n\t\t\t: history->peer->migrateTo()\n\t\t\t? history->peer->migrateTo()\n\t\t\t: history->peer.get();\n\t}();\n\tconst auto hiddenSenderInfo = [&]() -> const HiddenSenderInfo* {\n\t\tif (const auto searchChat = row->searchInChat()) {\n\t\t\tif (const auto peer = searchChat.peer()) {\n\t\t\t\tif (const auto forwarded = item->Get<HistoryMessageForwarded>()) {\n\t\t\t\t\tif (peer->isSelf() || forwarded->imported) {\n\t\t\t\t\t\treturn forwarded->savedFromHiddenSenderInfo.get()\n\t\t\t\t\t\t\t? forwarded->savedFromHiddenSenderInfo.get()\n\t\t\t\t\t\t\t: forwarded->originalHiddenSenderInfo.get();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t}();\n\tauto previewOptions = [&]() -> HistoryView::ToPreviewOptions {\n\t\tif (topic) {\n\t\t\treturn {};\n\t\t} else if (const auto searchChat = row->searchInChat()) {\n\t\t\tif (const auto peer = searchChat.peer()) {\n\t\t\t\tif (!peer->isChannel() || peer->isMegagroup()) {\n\t\t\t\t\treturn { .hideSender = true };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn {};\n\t}();\n\tpreviewOptions.ignoreGroup = true;\n\tpreviewOptions.searchLowerText = context.searchLowerText;\n\n\tconst auto badgesState = context.displayUnreadInfo\n\t\t? entry->chatListBadgesState()\n\t\t: BadgesState();\n\tconst auto displayPinnedIcon = false;\n\n\tconst auto paintItemCallback = [&](int nameleft, int namewidth) {\n\t\tconst auto texttop = context.st->textTop;\n\t\tconst auto availableWidth = PaintWideCounter(\n\t\t\tp,\n\t\t\tcontext,\n\t\t\tbadgesState,\n\t\t\ttexttop,\n\t\t\tnamewidth,\n\t\t\tdisplayPinnedIcon);\n\n\t\tconst auto itemRect = QRect(\n\t\t\tnameleft,\n\t\t\ttexttop,\n\t\t\tavailableWidth,\n\t\t\tst::dialogsTextFont->height);\n\t\tauto &view = row->itemView();\n\t\tif (!view.prepared(item, nullptr, nullptr)) {\n\t\t\tview.prepare(\n\t\t\t\titem,\n\t\t\t\tnullptr,\n\t\t\t\tnullptr,\n\t\t\t\trow->repaint(),\n\t\t\t\tpreviewOptions);\n\t\t}\n\t\tview.paint(p, itemRect, context);\n\t};\n\tconst auto showSavedMessages = history\n\t\t&& history->peer->isSelf()\n\t\t&& !row->searchInChat();\n\tconst auto showRepliesMessages = history\n\t\t&& history->peer->isRepliesChat()\n\t\t&& !row->searchInChat();\n\tconst auto showVerifyCodes = history\n\t\t&& history->peer->isVerifyCodes()\n\t\t&& !row->searchInChat();\n\tconst auto flags = (showSavedMessages ? Flag::SavedMessages : Flag(0))\n\t\t| (showRepliesMessages ? Flag::RepliesMessages : Flag(0))\n\t\t| (showVerifyCodes ? Flag::VerifyCodes : Flag(0));\n\tPaintRow(\n\t\tp,\n\t\trow,\n\t\tQRect(0, 0, context.width, context.st->height),\n\t\tentry,\n\t\tnullptr,\n\t\tfrom,\n\t\trow->badge(),\n\t\trow->repaint(),\n\t\trow->name(),\n\t\thiddenSenderInfo,\n\t\titem,\n\t\tcloudDraft,\n\t\tItemDateTime(item),\n\t\tcontext,\n\t\tbadgesState,\n\t\tflags,\n\t\tpaintItemCallback);\n}\n\nQRect RowPainter::SendActionAnimationRect(\n\t\tnot_null<const Data::Thread*> thread,\n\t\tFilterId filterId,\n\t\tQRect rect,\n\t\tint fullWidth,\n\t\tbool textUpdated) {\n\tconst auto &st = Row::ComputeSt(thread, filterId);\n\tconst auto nameleft = st.nameLeft;\n\tconst auto namewidth = fullWidth - nameleft - st.padding.right();\n\tconst auto texttop = st.textTop;\n\treturn QRect(\n\t\tnameleft + (textUpdated ? 0 : rect.x()),\n\t\ttexttop + rect.y(),\n\t\ttextUpdated ? namewidth : rect.width(),\n\t\trect.height());\n}\n\nvoid PaintCollapsedRow(\n\t\tPainter &p,\n\t\tconst BasicRow &row,\n\t\tData::Folder *folder,\n\t\tconst QString &text,\n\t\tint unread,\n\t\tconst PaintContext &context) {\n\tp.fillRect(\n\t\tQRect{ 0, 0, context.width, st::dialogsImportantBarHeight },\n\t\tcontext.selected ? st::dialogsBgOver : context.currentBg);\n\n\trow.paintRipple(p, 0, 0, context.width);\n\n\tconst auto unreadTop = (st::dialogsImportantBarHeight - st::dialogsUnreadHeight) / 2;\n\tif (!context.narrow || !folder) {\n\t\tp.setFont(st::semiboldFont);\n\t\tp.setPen(st::dialogsNameFg);\n\n\t\tconst auto textBaseline = unreadTop\n\t\t\t+ (st::dialogsUnreadHeight - st::dialogsUnreadFont->height) / 2\n\t\t\t+ st::dialogsUnreadFont->ascent;\n\t\tconst auto left = context.narrow\n\t\t\t? ((context.width - st::semiboldFont->width(text)) / 2)\n\t\t\t: st::dialogsTopBarLeftPadding;\n\t\t\t// : context.st->padding.left();\n\t\tp.drawText(left, textBaseline, text);\n\t} else {\n\t\tfolder->paintUserpic(\n\t\t\tp,\n\t\t\t(context.width - st::dialogsUnreadHeight) / 2,\n\t\t\tunreadTop,\n\t\t\tst::dialogsUnreadHeight);\n\t}\n\tif (!context.narrow && unread) {\n\t\tconst auto unreadRight = context.width - context.st->padding.right();\n\t\tUnreadBadgeStyle st;\n\t\tst.muted = true;\n\t\tPaintUnreadBadge(\n\t\t\tp,\n\t\t\tQString::number(unread),\n\t\t\tunreadRight,\n\t\t\tunreadTop,\n\t\t\tst);\n\t}\n}\n\nint PaintRightButton(QPainter &p, const PaintContext &context) {\n\treturn PaintRightButtonImpl(p, context);\n}\n\n} // namespace Dialogs::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_layout.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"dialogs/ui/dialogs_quick_action_context.h\"\n#include \"ui/cached_round_corners.h\"\n\nnamespace style {\nstruct DialogRow;\nstruct VerifiedBadge;\n} // namespace style\n\nnamespace st {\nextern const style::DialogRow &defaultDialogRow;\n} // namespace st\n\nnamespace Data {\nclass Forum;\nclass Folder;\nclass Thread;\n} // namespace Data\n\nnamespace Dialogs {\nclass Row;\nclass FakeRow;\nclass BasicRow;\nstruct RightButton;\n} // namespace Dialogs\n\nnamespace Dialogs::Ui {\n\nusing namespace ::Ui;\n\nclass VideoUserpic;\n\nstruct TopicJumpCorners {\n\tCornersPixmaps normal;\n\tCornersPixmaps inverted;\n\tQPixmap small;\n\tint invertedRadius = 0;\n\tint smallKey = 0; // = `-radius` if top right else `radius`.\n};\n\nstruct TopicJumpCache {\n\tTopicJumpCorners corners;\n\tTopicJumpCorners over;\n\tTopicJumpCorners selected;\n\tTopicJumpCorners rippleMask;\n};\n\nstruct PaintContext {\n\tRightButton *rightButton = nullptr;\n\tstd::vector<QImage*> *chatsFilterTags = nullptr;\n\tQuickActionContext *quickActionContext = nullptr;\n\tnot_null<const style::DialogRow*> st;\n\tTopicJumpCache *topicJumpCache = nullptr;\n\tData::Folder *folder = nullptr;\n\tData::Forum *forum = nullptr;\n\trequired<QBrush> currentBg;\n\tFilterId filter = 0;\n\tfloat64 topicsExpanded = 0.;\n\tcrl::time now = 0;\n\tQStringView searchLowerText;\n\tint width = 0;\n\tbool active = false;\n\tbool selected = false;\n\tbool topicJumpSelected = false;\n\tbool paused = false;\n\tbool search = false;\n\tbool narrow = false;\n\tbool displayUnreadInfo = false;\n};\n\nextern const char kOptionDialogsMuteIcon[];\n\n[[nodiscard]] const style::icon *ChatTypeIcon(\n\tnot_null<PeerData*> peer,\n\tconst PaintContext &context);\n[[nodiscard]] const style::icon *ChatTypeIcon(not_null<PeerData*> peer);\n\n[[nodiscard]] const style::VerifiedBadge &VerifiedStyle(\n\tconst PaintContext &context);\n\nclass RowPainter {\npublic:\n\tstatic void Paint(\n\t\tPainter &p,\n\t\tnot_null<const Row*> row,\n\t\tVideoUserpic *videoUserpic,\n\t\tconst PaintContext &context);\n\tstatic void Paint(\n\t\tPainter &p,\n\t\tnot_null<const FakeRow*> row,\n\t\tconst PaintContext &context);\n\tstatic QRect SendActionAnimationRect(\n\t\tnot_null<const Data::Thread*> thread,\n\t\tFilterId filterId,\n\t\tQRect rect,\n\t\tint fullWidth,\n\t\tbool textUpdated);\n};\n\nvoid PaintCollapsedRow(\n\tPainter &p,\n\tconst BasicRow &row,\n\tData::Folder *folder,\n\tconst QString &text,\n\tint unread,\n\tconst PaintContext &context);\n\nint PaintRightButton(QPainter &p, const PaintContext &context);\n\n} // namespace Dialogs::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/ui/dialogs_message_view.h\"\n\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_item_preview.h\"\n#include \"main/main_session.h\"\n#include \"dialogs/dialogs_three_state_icon.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"dialogs/ui/dialogs_topics_view.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/power_saving.h\"\n#include \"core/ui_integration.h\"\n#include \"lang/lang_keys.h\"\n#include \"lang/lang_text_entity.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace {\n\nconstexpr auto kEmojiLoopCount = 2;\n\ntemplate <ushort kTag>\nstruct TextWithTagOffset {\n\tTextWithTagOffset(TextWithEntities text) : text(std::move(text)) {\n\t}\n\tTextWithTagOffset(QString text) : text({ std::move(text) }) {\n\t}\n\tstatic TextWithTagOffset FromString(const QString &text) {\n\t\treturn { { text } };\n\t}\n\n\tTextWithEntities text;\n\tint offset = -1;\n};\n\n} // namespace\n\nnamespace Lang {\n\ntemplate <ushort kTag>\nstruct ReplaceTag<TextWithTagOffset<kTag>> {\n\tstatic TextWithTagOffset<kTag> Call(\n\t\tTextWithTagOffset<kTag> &&original,\n\t\tushort tag,\n\t\tconst TextWithTagOffset<kTag> &replacement);\n};\n\ntemplate <ushort kTag>\nTextWithTagOffset<kTag> ReplaceTag<TextWithTagOffset<kTag>>::Call(\n\t\tTextWithTagOffset<kTag> &&original,\n\t\tushort tag,\n\t\tconst TextWithTagOffset<kTag> &replacement) {\n\tconst auto replacementPosition = FindTagReplacementPosition(\n\t\toriginal.text.text,\n\t\ttag);\n\tif (replacementPosition < 0) {\n\t\treturn std::move(original);\n\t}\n\toriginal.text = ReplaceTag<TextWithEntities>::Replace(\n\t\tstd::move(original.text),\n\t\treplacement.text,\n\t\treplacementPosition);\n\tif (tag == kTag) {\n\t\toriginal.offset = replacementPosition;\n\t} else if (original.offset > replacementPosition) {\n\t\tconstexpr auto kReplaceCommandLength = 4;\n\t\tconst auto replacementSize = replacement.text.text.size();\n\t\toriginal.offset += replacementSize - kReplaceCommandLength;\n\t}\n\treturn std::move(original);\n}\n\n} // namespace Lang\n\nnamespace Dialogs::Ui {\n\nTextWithEntities DialogsPreviewText(TextWithEntities text) {\n\tauto result = Ui::Text::Filtered(\n\t\tstd::move(text),\n\t\t{\n\t\t\tEntityType::Pre,\n\t\t\tEntityType::Code,\n\t\t\tEntityType::Spoiler,\n\t\t\tEntityType::StrikeOut,\n\t\t\tEntityType::Underline,\n\t\t\tEntityType::Italic,\n\t\t\tEntityType::CustomEmoji,\n\t\t\tEntityType::Colorized,\n\t\t});\n\tfor (auto &entity : result.entities) {\n\t\tif (entity.type() == EntityType::Pre) {\n\t\t\tentity = EntityInText(\n\t\t\t\tEntityType::Code,\n\t\t\t\tentity.offset(),\n\t\t\t\tentity.length());\n\t\t} else if (entity.type() == EntityType::Colorized\n\t\t\t&& !entity.data().isEmpty()) {\n\t\t\t// Drop 'data' so that only link-color colorization takes place.\n\t\t\tentity = EntityInText(\n\t\t\t\tEntityType::Colorized,\n\t\t\t\tentity.offset(),\n\t\t\t\tentity.length());\n\t\t}\n\t}\n\treturn result;\n}\n\nstruct MessageView::LoadingContext {\n\tstd::any context;\n\trpl::lifetime lifetime;\n};\n\nMessageView::MessageView()\n: _senderCache(st::dialogsTextWidthMin)\n, _textCache(st::dialogsTextWidthMin) {\n}\n\nMessageView::~MessageView() = default;\n\nvoid MessageView::itemInvalidated(not_null<const HistoryItem*> item) {\n\tif (_textCachedFor == item.get()) {\n\t\t_textCachedFor = nullptr;\n\t}\n}\n\nbool MessageView::dependsOn(not_null<const HistoryItem*> item) const {\n\treturn (_textCachedFor == item.get());\n}\n\nbool MessageView::prepared(\n\t\tnot_null<const HistoryItem*> item,\n\t\tData::Forum *forum,\n\t\tData::SavedMessages *monoforum) const {\n\treturn (_textCachedFor == item.get())\n\t\t&& (_unreadMedia == item->isUnreadMedia())\n\t\t&& ((!forum && !monoforum)\n\t\t\t|| (_topics\n\t\t\t\t&& _topics->forum() == forum\n\t\t\t\t&& _topics->monoforum() == monoforum\n\t\t\t\t&& _topics->prepared()));\n}\n\nvoid MessageView::prepare(\n\t\tnot_null<const HistoryItem*> item,\n\t\tData::Forum *forum,\n\t\tData::SavedMessages *monoforum,\n\t\tFn<void()> customEmojiRepaint,\n\t\tToPreviewOptions options) {\n\tif (!forum && !monoforum) {\n\t\t_topics = nullptr;\n\t} else if (!_topics\n\t\t|| _topics->forum() != forum\n\t\t|| _topics->monoforum() != monoforum) {\n\t\t_topics = std::make_unique<TopicsView>(forum, monoforum);\n\t\tif (forum) {\n\t\t\t_topics->prepare(item->topicRootId(), customEmojiRepaint);\n\t\t} else {\n\t\t\t_topics->prepare(item->sublistPeerId(), customEmojiRepaint);\n\t\t}\n\t} else if (!_topics->prepared()) {\n\t\tif (forum) {\n\t\t\t_topics->prepare(item->topicRootId(), customEmojiRepaint);\n\t\t} else {\n\t\t\t_topics->prepare(item->sublistPeerId(), customEmojiRepaint);\n\t\t}\n\t}\n\tif (_textCachedFor == item.get()) {\n\t\t_unreadMedia = item->isUnreadMedia();\n\t\treturn;\n\t}\n\toptions.existing = &_imagesCache;\n\toptions.ignoreTopic = true;\n\toptions.spoilerLoginCode = true;\n\tauto preview = item->toPreview(options);\n\t_leftIcon = (preview.icon == ItemPreview::Icon::ForwardedMessage)\n\t\t? &st::dialogsMiniForward\n\t\t: (preview.icon == ItemPreview::Icon::ReplyToStory)\n\t\t? &st::dialogsMiniReplyStory\n\t\t: nullptr;\n\tconst auto hasImages = !preview.images.empty();\n\tconst auto history = item->history();\n\tauto context = Core::TextContext({\n\t\t.session = &history->session(),\n\t\t.repaint = customEmojiRepaint,\n\t\t.customEmojiLoopLimit = kEmojiLoopCount,\n\t});\n\tconst auto senderTill = (preview.arrowInTextPosition > 0)\n\t\t? preview.arrowInTextPosition\n\t\t: preview.imagesInTextPosition;\n\tif ((hasImages || _leftIcon) && senderTill > 0) {\n\t\tauto sender = Text::Mid(preview.text, 0, senderTill);\n\t\tTextUtilities::Trim(sender);\n\t\t_senderCache.setMarkedText(\n\t\t\tst::dialogsTextStyle,\n\t\t\tstd::move(sender),\n\t\t\tDialogTextOptions());\n\t\tpreview.text = Text::Mid(preview.text, senderTill);\n\t} else {\n\t\t_senderCache = { st::dialogsTextWidthMin };\n\t}\n\tTextUtilities::Trim(preview.text);\n\tauto textToCache = DialogsPreviewText(std::move(preview.text));\n\n\tif (!options.searchLowerText.isEmpty()) {\n\t\tstatic constexpr auto kLeftShift = 15;\n\t\tauto minFrom = std::numeric_limits<uint16>::max();\n\n\t\tconst auto words = Ui::Text::Words(options.searchLowerText);\n\t\ttextToCache.entities.reserve(textToCache.entities.size()\n\t\t\t+ words.size());\n\n\t\tfor (const auto &word : words) {\n\t\t\tconst auto selection = HistoryView::FindSearchQueryHighlight(\n\t\t\t\ttextToCache.text,\n\t\t\t\tword);\n\t\t\tif (!selection.empty()) {\n\t\t\t\tminFrom = std::min(minFrom, selection.from);\n\t\t\t\ttextToCache.entities.push_back(EntityInText{\n\t\t\t\t\tEntityType::Colorized,\n\t\t\t\t\tselection.from,\n\t\t\t\t\tselection.to - selection.from\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tif (minFrom == std::numeric_limits<uint16>::max()\n\t\t\t&& !item->replyTo().quote.empty()) {\n\t\t\tauto textQuote = TextWithEntities();\n\t\t\tfor (const auto &word : words) {\n\t\t\t\tconst auto selection = HistoryView::FindSearchQueryHighlight(\n\t\t\t\t\titem->replyTo().quote.text,\n\t\t\t\t\tword);\n\t\t\t\tif (!selection.empty()) {\n\t\t\t\t\tminFrom = 0;\n\t\t\t\t\tif (textQuote.empty()) {\n\t\t\t\t\t\ttextQuote = item->replyTo().quote;\n\t\t\t\t\t}\n\t\t\t\t\ttextQuote.entities.push_back(EntityInText{\n\t\t\t\t\t\tEntityType::Colorized,\n\t\t\t\t\t\tselection.from,\n\t\t\t\t\t\tselection.to - selection.from\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!textQuote.empty()) {\n\t\t\t\tauto helper = Ui::Text::CustomEmojiHelper(context);\n\t\t\t\tconst auto factory = Ui::Text::PaletteDependentEmoji{\n\t\t\t\t\t.factory = [=] {\n\t\t\t\t\t\tconst auto &icon = st::dialogsMiniQuoteIcon;\n\t\t\t\t\t\tauto image = QImage(\n\t\t\t\t\t\t\ticon.size() * style::DevicePixelRatio(),\n\t\t\t\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t\t\t\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\t\t\t\timage.fill(Qt::transparent);\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tauto p = Painter(&image);\n\t\t\t\t\t\t\ticon.paintInCenter(\n\t\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\t\tRect(icon.size()),\n\t\t\t\t\t\t\t\tst::dialogsTextFg->c);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn image;\n\t\t\t\t\t},\n\t\t\t\t\t.margin = QMargins(\n\t\t\t\t\t\tst::lineWidth * 2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tst::lineWidth * 2,\n\t\t\t\t\t\t0),\n\t\t\t\t};\n\t\t\t\ttextToCache = textQuote\n\t\t\t\t\t.append(helper.paletteDependent(factory))\n\t\t\t\t\t.append(std::move(textToCache));\n\t\t\t\tcontext = helper.context(customEmojiRepaint);\n\t\t\t}\n\t\t}\n\n\t\tif (!words.empty() && minFrom != std::numeric_limits<uint16>::max()) {\n\t\t\tstd::sort(\n\t\t\t\ttextToCache.entities.begin(),\n\t\t\t\ttextToCache.entities.end(),\n\t\t\t\t[](const auto &a, const auto &b) {\n\t\t\t\t\treturn a.offset() < b.offset();\n\t\t\t\t});\n\n\t\t\tconst auto textSize = textToCache.text.size();\n\t\t\tminFrom = (minFrom > textSize || minFrom < kLeftShift)\n\t\t\t\t? 0\n\t\t\t\t: minFrom - kLeftShift;\n\n\t\t\ttextToCache = (TextWithEntities{\n\t\t\t\tminFrom > 0 ? kQEllipsis : QString()\n\t\t\t}).append(Text::Mid(std::move(textToCache), minFrom));\n\t\t}\n\t}\n\t_hasPlainLinkAtBegin = !textToCache.entities.empty()\n\t\t&& (textToCache.entities.front().type() == EntityType::Colorized);\n\t_textCache.setMarkedText(\n\t\tst::dialogsTextStyle,\n\t\tstd::move(textToCache),\n\t\tDialogTextOptions(),\n\t\tstd::move(context));\n\t_textCachedFor = item;\n\t_unreadMedia = item->isUnreadMedia();\n\t_imagesCache = std::move(preview.images);\n\tif (!ranges::any_of(_imagesCache, &ItemPreviewImage::hasSpoiler)) {\n\t\t_spoiler = nullptr;\n\t} else if (!_spoiler) {\n\t\t_spoiler = std::make_unique<SpoilerAnimation>(customEmojiRepaint);\n\t}\n\tif (preview.loadingContext.has_value()) {\n\t\tif (!_loadingContext) {\n\t\t\t_loadingContext = std::make_unique<LoadingContext>();\n\t\t\titem->history()->session().downloaderTaskFinished(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\t_textCachedFor = nullptr;\n\t\t\t}, _loadingContext->lifetime);\n\t\t}\n\t\t_loadingContext->context = std::move(preview.loadingContext);\n\t} else {\n\t\t_loadingContext = nullptr;\n\t}\n}\n\nbool MessageView::isInTopicJump(int x, int y) const {\n\treturn _topics && _topics->isInTopicJumpArea(x, y);\n}\n\nvoid MessageView::addTopicJumpRipple(\n\t\tQPoint origin,\n\t\tnot_null<TopicJumpCache*> topicJumpCache,\n\t\tFn<void()> updateCallback) {\n\tif (_topics) {\n\t\t_topics->addTopicJumpRipple(\n\t\t\torigin,\n\t\t\ttopicJumpCache,\n\t\t\tstd::move(updateCallback));\n\t}\n}\n\nvoid MessageView::stopLastRipple() {\n\tif (_topics) {\n\t\t_topics->stopLastRipple();\n\t}\n}\n\nvoid MessageView::clearRipple() {\n\tif (_topics) {\n\t\t_topics->clearRipple();\n\t}\n}\n\nint MessageView::countWidth() const {\n\tauto result = 0;\n\tif (!_senderCache.isEmpty()) {\n\t\tresult += _senderCache.maxWidth();\n\t\tif (!_imagesCache.empty() && !_leftIcon) {\n\t\t\tresult += st::dialogsMiniPreviewSkip\n\t\t\t\t+ st::dialogsMiniPreviewRight;\n\t\t}\n\t}\n\tif (_leftIcon) {\n\t\tconst auto w = _leftIcon->icon.icon.width();\n\t\tresult += w\n\t\t\t+ (_imagesCache.empty()\n\t\t\t\t? _leftIcon->skipText\n\t\t\t\t: _leftIcon->skipMedia);\n\t}\n\tif (!_imagesCache.empty()) {\n\t\tresult += (_imagesCache.size()\n\t\t\t* (st::dialogsMiniPreview + st::dialogsMiniPreviewSkip))\n\t\t\t+ st::dialogsMiniPreviewRight;\n\t}\n\treturn result + _textCache.maxWidth();\n}\n\nvoid MessageView::paint(\n\t\tPainter &p,\n\t\tconst QRect &geometry,\n\t\tconst PaintContext &context) const {\n\tif (geometry.isEmpty()) {\n\t\treturn;\n\t}\n\tp.setFont(st::dialogsTextFont);\n\tp.setPen(context.active\n\t\t? st::dialogsTextFgActive\n\t\t: context.selected\n\t\t? st::dialogsTextFgOver\n\t\t: st::dialogsTextFg);\n\tconst auto withTopic = _topics && context.st->topicsHeight;\n\tconst auto palette = &(withTopic\n\t\t? (context.active\n\t\t\t? st::dialogsTextPaletteInTopicActive\n\t\t\t: context.selected\n\t\t\t? st::dialogsTextPaletteInTopicOver\n\t\t\t: st::dialogsTextPaletteInTopic)\n\t\t: (context.active\n\t\t\t? st::dialogsTextPaletteActive\n\t\t\t: context.selected\n\t\t\t? st::dialogsTextPaletteOver\n\t\t\t: st::dialogsTextPalette));\n\n\tauto rect = geometry;\n\tconst auto checkJump = withTopic && !context.active;\n\tconst auto jump1 = checkJump ? _topics->jumpToTopicWidth() : 0;\n\tif (jump1) {\n\t\tpaintJumpToLast(p, rect, context, jump1);\n\t} else if (_topics) {\n\t\t_topics->clearTopicJumpGeometry();\n\t}\n\n\tif (withTopic) {\n\t\t_topics->paint(p, rect, context);\n\t\trect.setTop(rect.top() + context.st->topicsHeight);\n\t}\n\n\tauto finalRight = rect.x() + rect.width();\n\tif (jump1) {\n\t\trect.setWidth(rect.width() - st::forumDialogJumpArrowSkip);\n\t\tfinalRight -= st::forumDialogJumpArrowSkip;\n\t}\n\tconst auto pausedSpoiler = context.paused\n\t\t|| On(PowerSaving::kChatSpoiler);\n\tif (!_senderCache.isEmpty()) {\n\t\t_senderCache.draw(p, {\n\t\t\t.position = rect.topLeft(),\n\t\t\t.availableWidth = rect.width(),\n\t\t\t.palette = palette,\n\t\t\t.elisionHeight = rect.height(),\n\t\t});\n\t\trect.setLeft(rect.x() + _senderCache.maxWidth());\n\t\tif (!_imagesCache.empty() && !_leftIcon) {\n\t\t\tconst auto skip = st::dialogsMiniPreviewSkip\n\t\t\t\t+ st::dialogsMiniPreviewRight;\n\t\t\trect.setLeft(rect.x() + skip);\n\t\t}\n\t}\n\n\tif (_leftIcon) {\n\t\tconst auto &icon = ThreeStateIcon(\n\t\t\t_leftIcon->icon,\n\t\t\tcontext.active,\n\t\t\tcontext.selected);\n\t\tconst auto w = (icon.width());\n\t\tif (rect.width() > w) {\n\t\t\tif (_hasPlainLinkAtBegin && !context.active) {\n\t\t\t\ticon.paint(\n\t\t\t\t\tp,\n\t\t\t\t\trect.topLeft(),\n\t\t\t\t\trect.width(),\n\t\t\t\t\tpalette->linkFg->c);\n\t\t\t} else {\n\t\t\t\ticon.paint(p, rect.topLeft(), rect.width());\n\t\t\t}\n\t\t\trect.setLeft(rect.x()\n\t\t\t\t+ w\n\t\t\t\t+ (_imagesCache.empty()\n\t\t\t\t\t? _leftIcon->skipText\n\t\t\t\t\t: _leftIcon->skipMedia));\n\t\t}\n\t}\n\tfor (const auto &image : _imagesCache) {\n\t\tconst auto w = st::dialogsMiniPreview + st::dialogsMiniPreviewSkip;\n\t\tif (rect.width() < w) {\n\t\t\tbreak;\n\t\t}\n\t\tconst auto mini = QRect(\n\t\t\trect.x(),\n\t\t\trect.y() + st::dialogsMiniPreviewTop,\n\t\t\tst::dialogsMiniPreview,\n\t\t\tst::dialogsMiniPreview);\n\t\tif (!image.data.isNull()) {\n\t\t\tp.drawImage(mini, image.data);\n\t\t\tif (image.hasSpoiler()) {\n\t\t\t\tconst auto frame = DefaultImageSpoiler().frame(\n\t\t\t\t\t_spoiler->index(context.now, pausedSpoiler));\n\t\t\t\tif (image.isEllipse()) {\n\t\t\t\t\tconst auto radius = st::dialogsMiniPreview / 2;\n\t\t\t\t\tstatic auto mask = Images::CornersMask(radius);\n\t\t\t\t\tFillSpoilerRect(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tmini,\n\t\t\t\t\t\tImages::CornersMaskRef(mask),\n\t\t\t\t\t\tframe,\n\t\t\t\t\t\t_cornersCache);\n\t\t\t\t} else {\n\t\t\t\t\tFillSpoilerRect(p, mini, frame);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\trect.setLeft(rect.x() + w);\n\t}\n\tif (!_imagesCache.empty()) {\n\t\trect.setLeft(rect.x() + st::dialogsMiniPreviewRight);\n\t}\n\t// Style of _textCache.\n\tstatic const auto ellipsisWidth = st::dialogsTextStyle.font->width(\n\t\tkQEllipsis);\n\tif (rect.width() > ellipsisWidth) {\n\t\t_textCache.draw(p, {\n\t\t\t.position = rect.topLeft(),\n\t\t\t.availableWidth = rect.width(),\n\t\t\t.palette = palette,\n\t\t\t.spoiler = Text::DefaultSpoilerCache(),\n\t\t\t.now = context.now,\n\t\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t\t.pausedSpoiler = pausedSpoiler,\n\t\t\t.elisionHeight = rect.height(),\n\t\t});\n\t\trect.setLeft(rect.x() + _textCache.maxWidth());\n\t}\n\tif (jump1) {\n\t\tconst auto position = st::forumDialogJumpArrowPosition\n\t\t\t+ QPoint((rect.width() > 0) ? rect.x() : finalRight, rect.y());\n\t\t(context.selected\n\t\t\t? st::forumDialogJumpArrowOver\n\t\t\t: st::forumDialogJumpArrow).paint(p, position, context.width);\n\t}\n}\n\nvoid MessageView::paintJumpToLast(\n\t\tPainter &p,\n\t\tconst QRect &rect,\n\t\tconst PaintContext &context,\n\t\tint width1) const {\n\tif (!context.topicJumpCache) {\n\t\t_topics->clearTopicJumpGeometry();\n\t\treturn;\n\t}\n\tconst auto width2 = countWidth() + st::forumDialogJumpArrowSkip;\n\tconst auto geometry = FillJumpToLastBg(p, {\n\t\t.st = context.st,\n\t\t.corners = (context.selected\n\t\t\t? &context.topicJumpCache->over\n\t\t\t: &context.topicJumpCache->corners),\n\t\t.geometry = rect,\n\t\t.bg = (context.selected\n\t\t\t? st::dialogsRippleBg\n\t\t\t: st::dialogsBgOver),\n\t\t.width1 = width1,\n\t\t.width2 = width2,\n\t});\n\tif (context.topicJumpSelected) {\n\t\tp.setOpacity(0.1);\n\t\tFillJumpToLastPrepared(p, {\n\t\t\t.st = context.st,\n\t\t\t.corners = &context.topicJumpCache->selected,\n\t\t\t.bg = st::dialogsTextFg,\n\t\t\t.prepared = geometry,\n\t\t});\n\t\tp.setOpacity(1.);\n\t}\n\tif (!_topics->changeTopicJumpGeometry(geometry)) {\n\t\tauto color = st::dialogsTextFg->c;\n\t\tcolor.setAlpha(color.alpha() / 10);\n\t\tif (color.alpha() > 0) {\n\t\t\t_topics->paintRipple(p, 0, 0, context.width, &color);\n\t\t}\n\t}\n}\n\nHistoryView::ItemPreview PreviewWithSender(\n\t\tHistoryView::ItemPreview &&preview,\n\t\tconst QString &sender,\n\t\tTextWithEntities topic) {\n\tconst auto wrappedSender = st::wrap_rtl(sender);\n\tauto senderWithOffset = topic.empty()\n\t\t? TextWithTagOffset<lt_from>::FromString(wrappedSender)\n\t\t: tr::lng_dialogs_text_from_in_topic(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\t{ wrappedSender },\n\t\t\tlt_topic,\n\t\t\tstd::move(topic),\n\t\t\tTextWithTagOffset<lt_from>::FromString);\n\tauto wrappedWithOffset = tr::lng_dialogs_text_from_wrapped(\n\t\ttr::now,\n\t\tlt_from,\n\t\tstd::move(senderWithOffset.text),\n\t\tTextWithTagOffset<lt_from>::FromString);\n\tconst auto wrappedSize = wrappedWithOffset.text.text.size();\n\tauto fullWithOffset = tr::lng_dialogs_text_with_from(\n\t\ttr::now,\n\t\tlt_from_part,\n\t\tUi::Text::Colorized(std::move(wrappedWithOffset.text)),\n\t\tlt_message,\n\t\tstd::move(preview.text),\n\t\tTextWithTagOffset<lt_from_part>::FromString);\n\tpreview.text = std::move(fullWithOffset.text);\n\tpreview.arrowInTextPosition = (fullWithOffset.offset < 0\n\t\t|| wrappedWithOffset.offset < 0\n\t\t|| senderWithOffset.offset < 0)\n\t\t? -1\n\t\t: (fullWithOffset.offset\n\t\t\t+ wrappedWithOffset.offset\n\t\t\t+ senderWithOffset.offset\n\t\t\t+ sender.size());\n\tpreview.imagesInTextPosition = (fullWithOffset.offset < 0)\n\t\t? 0\n\t\t: (fullWithOffset.offset + wrappedSize);\n\treturn std::move(preview);\n}\n\n} // namespace Dialogs::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <any>\n\nclass Image;\nclass HistoryItem;\nenum class ImageRoundRadius;\n\nnamespace style {\nstruct DialogRow;\nstruct DialogsMiniIcon;\n} // namespace style\n\nnamespace Ui {\nclass SpoilerAnimation;\n} // namespace Ui\n\nnamespace Data {\nclass Forum;\nclass SavedMessages;\n} // namespace Data\n\nnamespace HistoryView {\nstruct ToPreviewOptions;\nstruct ItemPreviewImage;\nstruct ItemPreview;\n} // namespace HistoryView\n\nnamespace Dialogs::Ui {\n\nusing namespace ::Ui;\n\nstruct PaintContext;\nstruct TopicJumpCache;\nclass TopicsView;\n\n[[nodiscard]] TextWithEntities DialogsPreviewText(TextWithEntities text);\n\nclass MessageView final {\npublic:\n\tMessageView();\n\t~MessageView();\n\n\tusing ToPreviewOptions = HistoryView::ToPreviewOptions;\n\tusing ItemPreviewImage = HistoryView::ItemPreviewImage;\n\tusing ItemPreview = HistoryView::ItemPreview;\n\n\tvoid itemInvalidated(not_null<const HistoryItem*> item);\n\t[[nodiscard]] bool dependsOn(not_null<const HistoryItem*> item) const;\n\n\t[[nodiscard]] bool prepared(\n\t\tnot_null<const HistoryItem*> item,\n\t\tData::Forum *forum,\n\t\tData::SavedMessages *monoforum) const;\n\tvoid prepare(\n\t\tnot_null<const HistoryItem*> item,\n\t\tData::Forum *forum,\n\t\tData::SavedMessages *monoforum,\n\t\tFn<void()> customEmojiRepaint,\n\t\tToPreviewOptions options);\n\n\tvoid paint(\n\t\tPainter &p,\n\t\tconst QRect &geometry,\n\t\tconst PaintContext &context) const;\n\n\t[[nodiscard]] bool isInTopicJump(int x, int y) const;\n\tvoid addTopicJumpRipple(\n\t\tQPoint origin,\n\t\tnot_null<TopicJumpCache*> topicJumpCache,\n\t\tFn<void()> updateCallback);\n\tvoid stopLastRipple();\n\tvoid clearRipple();\n\nprivate:\n\tstruct LoadingContext;\n\n\t[[nodiscard]] int countWidth() const;\n\tvoid paintJumpToLast(\n\t\tPainter &p,\n\t\tconst QRect &rect,\n\t\tconst PaintContext &context,\n\t\tint width1) const;\n\n\tmutable const HistoryItem *_textCachedFor = nullptr;\n\tmutable Text::String _senderCache;\n\tmutable std::unique_ptr<TopicsView> _topics;\n\tmutable Text::String _textCache;\n\tmutable std::vector<ItemPreviewImage> _imagesCache;\n\tmutable std::unique_ptr<SpoilerAnimation> _spoiler;\n\tmutable std::unique_ptr<LoadingContext> _loadingContext;\n\tmutable const style::DialogsMiniIcon *_leftIcon = nullptr;\n\tmutable QImage _cornersCache;\n\tmutable bool _hasPlainLinkAtBegin = false;\n\tmutable bool _unreadMedia = false;\n\n};\n\n[[nodiscard]] HistoryView::ItemPreview PreviewWithSender(\n\tHistoryView::ItemPreview &&preview,\n\tconst QString &sender,\n\tTextWithEntities topic);\n\n} // namespace Dialogs::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_quick_action.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Dialogs::Ui {\n\nusing namespace ::Ui;\n\nenum class QuickDialogAction {\n\tMute,\n\tPin,\n\tRead,\n\tArchive,\n\tDelete,\n\tDisabled,\n};\n\n} // namespace Dialogs::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_quick_action_context.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"dialogs/ui/dialogs_quick_action.h\"\n#include \"ui/controls/swipe_handler_data.h\"\n\nnamespace Lottie {\nclass Icon;\n} // namespace Lottie\n\nnamespace Ui {\nclass RippleAnimation;\n} // namespace Ui\n\nnamespace Dialogs::Ui {\n\nusing namespace ::Ui;\n\nenum class QuickDialogActionLabel {\n\tMute,\n\tUnmute,\n\tPin,\n\tUnpin,\n\tRead,\n\tUnread,\n\tArchive,\n\tUnarchive,\n\tDelete,\n\tDisabled,\n};\n\nstruct QuickActionContext {\n\t::Ui::Controls::SwipeContextData data;\n\tstd::unique_ptr<Lottie::Icon> icon;\n\tstd::unique_ptr<Ui::RippleAnimation> ripple;\n\tstd::unique_ptr<Ui::RippleAnimation> rippleFg;\n\tQuickDialogAction action;\n\tcrl::time finishedAt = 0;\n};\n\n} // namespace Dialogs::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/ui/dialogs_stories_content.h\"\n\n#include \"base/unixtime.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_user.h\"\n#include \"dialogs/ui/dialogs_stories_list.h\"\n#include \"info/stories/info_stories_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"main/main_session.h\"\n#include \"media/stories/media_stories_stealth.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/painter.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_media_stories.h\"\n\nnamespace Dialogs::Stories {\nnamespace {\n\nconstexpr auto kShownLastCount = 3;\n\nclass State final {\npublic:\n\tState(not_null<Data::Stories*> data, Data::StorySourcesList list);\n\n\t[[nodiscard]] Content next();\n\nprivate:\n\tconst not_null<Data::Stories*> _data;\n\tconst Data::StorySourcesList _list;\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tstd::shared_ptr<Ui::DynamicImage>> _userpics;\n\n};\n\nState::State(not_null<Data::Stories*> data, Data::StorySourcesList list)\n: _data(data)\n, _list(list) {\n}\n\nContent State::next() {\n\tconst auto &sources = _data->sources(_list);\n\tauto result = Content{ .total = int(sources.size()) };\n\tresult.elements.reserve(sources.size());\n\tfor (const auto &info : sources) {\n\t\tconst auto source = _data->source(info.id);\n\t\tAssert(source != nullptr);\n\n\t\tauto userpic = std::shared_ptr<Ui::DynamicImage>();\n\t\tconst auto peer = source->peer;\n\t\tif (const auto i = _userpics.find(peer); i != end(_userpics)) {\n\t\t\tuserpic = i->second;\n\t\t} else {\n\t\t\tuserpic = Ui::MakeUserpicThumbnail(peer, true);\n\t\t\t_userpics.emplace(peer, userpic);\n\t\t}\n\t\tresult.elements.push_back({\n\t\t\t.id = uint64(peer->id.value),\n\t\t\t.name = peer->shortName(),\n\t\t\t.thumbnail = std::move(userpic),\n\t\t\t.count = info.count,\n\t\t\t.unreadCount = info.unreadCount,\n\t\t\t.hasVideoStream = info.hasVideoStream ? 1U : 0U,\n\t\t\t.skipSmall = peer->isSelf() ? 1U : 0U,\n\t\t});\n\t}\n\treturn result;\n}\n\n} // namespace\n\nrpl::producer<Content> ContentForSession(\n\t\tnot_null<Main::Session*> session,\n\t\tData::StorySourcesList list) {\n\treturn [=](auto consumer) {\n\t\tauto result = rpl::lifetime();\n\t\tconst auto stories = &session->data().stories();\n\t\tconst auto state = result.make_state<State>(stories, list);\n\t\trpl::single(\n\t\t\trpl::empty\n\t\t) | rpl::then(\n\t\t\tstories->sourcesChanged(list)\n\t\t) | rpl::on_next([=] {\n\t\t\tconsumer.put_next(state->next());\n\t\t}, result);\n\t\treturn result;\n\t};\n}\n\nrpl::producer<Content> LastForPeer(not_null<PeerData*> peer) {\n\tusing namespace rpl::mappers;\n\n\tconst auto stories = &peer->owner().stories();\n\tconst auto peerId = peer->id;\n\n\treturn rpl::single(\n\t\tpeerId\n\t) | rpl::then(\n\t\tstories->sourceChanged() | rpl::filter(_1 == peerId)\n\t) | rpl::map([=] {\n\t\tauto ids = std::vector<StoryId>();\n\t\tauto readTill = StoryId();\n\t\tauto total = 0;\n\t\tif (const auto source = stories->source(peerId)) {\n\t\t\treadTill = source->readTill;\n\t\t\ttotal = int(source->ids.size());\n\t\t\tids = ranges::views::all(source->ids)\n\t\t\t\t| ranges::views::reverse\n\t\t\t\t| ranges::views::take(kShownLastCount)\n\t\t\t\t| ranges::views::transform(&Data::StoryIdDates::id)\n\t\t\t\t| ranges::to_vector;\n\t\t}\n\t\treturn rpl::make_producer<Content>([=](auto consumer) {\n\t\t\tauto lifetime = rpl::lifetime();\n\t\t\tif (ids.empty()) {\n\t\t\t\tconsumer.put_next(Content());\n\t\t\t\tconsumer.put_done();\n\t\t\t\treturn lifetime;\n\t\t\t}\n\n\t\t\tstruct State {\n\t\t\t\tFn<void()> check;\n\t\t\t\tbase::has_weak_ptr guard;\n\t\t\t\tint readTill = StoryId();\n\t\t\t\tbool pushed = false;\n\t\t\t};\n\t\t\tconst auto state = lifetime.make_state<State>();\n\t\t\tstate->readTill = readTill;\n\t\t\tstate->check = [=] {\n\t\t\t\tif (state->pushed) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tauto done = true;\n\t\t\t\tauto resolving = false;\n\t\t\t\tauto result = Content{ .total = total };\n\t\t\t\tfor (const auto id : ids) {\n\t\t\t\t\tconst auto storyId = FullStoryId{ peerId, id };\n\t\t\t\t\tconst auto maybe = stories->lookup(storyId);\n\t\t\t\t\tif (maybe) {\n\t\t\t\t\t\tif (!resolving) {\n\t\t\t\t\t\t\tconst auto stream = (*maybe)->call();\n\t\t\t\t\t\t\tconst auto unread = stream\n\t\t\t\t\t\t\t\t|| (id > state->readTill);\n\t\t\t\t\t\t\tresult.elements.reserve(ids.size());\n\t\t\t\t\t\t\tresult.elements.push_back({\n\t\t\t\t\t\t\t\t.id = uint64(id),\n\t\t\t\t\t\t\t\t.thumbnail = Ui::MakeStoryThumbnail(*maybe),\n\t\t\t\t\t\t\t\t.count = 1U,\n\t\t\t\t\t\t\t\t.unreadCount = unread ? 1U : 0U,\n\t\t\t\t\t\t\t\t.hasVideoStream = stream ? 1U : 0U,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tif (unread) {\n\t\t\t\t\t\t\t\tdone = false;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (maybe.error() == Data::NoStory::Unknown) {\n\t\t\t\t\t\tresolving = true;\n\t\t\t\t\t\tstories->resolve(\n\t\t\t\t\t\t\tstoryId,\n\t\t\t\t\t\t\tcrl::guard(&state->guard, state->check));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (resolving) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tstate->pushed = true;\n\t\t\t\tconsumer.put_next(std::move(result));\n\t\t\t\tif (done) {\n\t\t\t\t\tconsumer.put_done();\n\t\t\t\t}\n\t\t\t};\n\n\t\t\trpl::single(peerId) | rpl::then(\n\t\t\t\tstories->itemsChanged() | rpl::filter(_1 == peerId)\n\t\t\t) | rpl::on_next(state->check, lifetime);\n\n\t\t\tstories->session().changes().storyUpdates(\n\t\t\t\tData::StoryUpdate::Flag::MarkRead\n\t\t\t) | rpl::on_next([=](const Data::StoryUpdate &update) {\n\t\t\t\tif (update.story->peer()->id == peerId) {\n\t\t\t\t\tif (update.story->id() > state->readTill) {\n\t\t\t\t\t\tstate->readTill = update.story->id();\n\t\t\t\t\t\tif (ranges::contains(ids, state->readTill)\n\t\t\t\t\t\t\t|| state->readTill > ids.front()) {\n\t\t\t\t\t\t\tstate->pushed = false;\n\t\t\t\t\t\t\tstate->check();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, lifetime);\n\n\t\t\treturn lifetime;\n\t\t});\n\t}) | rpl::flatten_latest();\n}\n\nvoid FillSourceMenu(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst ShowMenuRequest &request) {\n\tconst auto owner = &controller->session().data();\n\tconst auto peer = owner->peer(PeerId(request.id));\n\tconst auto &add = request.callback;\n\tif (peer->isSelf()) {\n\t\tadd(tr::lng_stories_archive_button(tr::now), [=] {\n\t\t\tcontroller->showSection(Info::Stories::Make(\n\t\t\t\tpeer,\n\t\t\t\tInfo::Stories::ArchiveId()));\n\t\t}, &st::menuIconStoriesArchiveSection);\n\t\tadd(tr::lng_stories_my_title(tr::now), [=] {\n\t\t\tcontroller->showSection(Info::Stories::Make(peer));\n\t\t}, &st::menuIconStoriesSavedSection);\n\t} else {\n\t\tconst auto group = peer->isMegagroup();\n\t\tconst auto channel = peer->isChannel();\n\t\tconst auto showHistoryText = group\n\t\t\t? tr::lng_context_open_group(tr::now)\n\t\t\t: channel\n\t\t\t? tr::lng_context_open_channel(tr::now)\n\t\t\t: tr::lng_profile_send_message(tr::now);\n\t\tadd(showHistoryText, [=] {\n\t\t\tcontroller->showPeerHistory(peer);\n\t\t}, channel ? &st::menuIconChannel : &st::menuIconChatBubble);\n\t\tconst auto viewProfileText = group\n\t\t\t? tr::lng_context_view_group(tr::now)\n\t\t\t: channel\n\t\t\t? tr::lng_context_view_channel(tr::now)\n\t\t\t: tr::lng_context_view_profile(tr::now);\n\t\tadd(viewProfileText, [=] {\n\t\t\tcontroller->showPeerInfo(peer);\n\t\t}, channel ? &st::menuIconInfo : &st::menuIconProfile);\n\t\tif (!peer->hasActiveVideoStream() && peer->hasUnreadStories()) {\n\t\t\tMedia::Stories::AddStealthModeMenu(add, peer, controller);\n\t\t}\n\t\tconst auto in = [&](Data::StorySourcesList list) {\n\t\t\treturn ranges::contains(\n\t\t\t\towner->stories().sources(list),\n\t\t\t\tpeer->id,\n\t\t\t\t&Data::StoriesSourceInfo::id);\n\t\t};\n\t\tconst auto toggle = [=](bool shown) {\n\t\t\towner->stories().toggleHidden(\n\t\t\t\tpeer->id,\n\t\t\t\t!shown,\n\t\t\t\tcontroller->uiShow());\n\t\t};\n\t\tif (in(Data::StorySourcesList::NotHidden)) {\n\t\t\tadd(tr::lng_stories_archive(tr::now), [=] {\n\t\t\t\ttoggle(false);\n\t\t\t}, &st::menuIconArchive);\n\t\t}\n\t\tif (in(Data::StorySourcesList::Hidden)) {\n\t\t\tadd(tr::lng_stories_unarchive(tr::now), [=] {\n\t\t\t\ttoggle(true);\n\t\t\t}, &st::menuIconUnarchive);\n\t\t}\n\t}\n}\n\n} // namespace Dialogs::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nenum class StorySourcesList : uchar;\nclass Story;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Dialogs::Stories {\n\nstruct Content;\nstruct ShowMenuRequest;\n\n[[nodiscard]] rpl::producer<Content> ContentForSession(\n\tnot_null<Main::Session*> session,\n\tData::StorySourcesList list);\n\n[[nodiscard]] rpl::producer<Content> LastForPeer(not_null<PeerData*> peer);\n\nvoid FillSourceMenu(\n\tnot_null<Window::SessionController*> controller,\n\tconst ShowMenuRequest &request);\n\n} // namespace Dialogs::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/ui/dialogs_stories_list.h\"\n\n#include \"base/event_filter.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/round_checkbox.h\"\n#include \"ui/effects/outline_segments.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_dialogs.h\"\n\n#include <QtWidgets/QApplication>\n#include <QtGui/QWindow>\n#include <QtGui/QPainter>\n\n#include \"base/debug_log.h\"\n\nnamespace Dialogs::Stories {\nnamespace {\n\nconstexpr auto kSmallThumbsShown = 3;\nconstexpr auto kPreloadPages = 2;\nconstexpr auto kExpandAfterRatio = 0.72;\nconstexpr auto kCollapseAfterRatio = 0.68;\nconstexpr auto kFrictionRatio = 0.15;\nconstexpr auto kExpandCatchUpDuration = crl::time(200);\nconstexpr auto kMaxTooltipNames = 3;\n\n[[nodiscard]] int AvailableNameWidth(const style::DialogsStoriesList &st) {\n\tconst auto &full = st.full;\n\tconst auto &font = full.nameStyle.font;\n\tconst auto skip = font->spacew;\n\treturn full.photoLeft * 2 + full.photo - 2 * skip;\n}\n\n} // namespace\n\nstruct List::Layout {\n\tint itemsCount = 0;\n\tQPointF geometryShift;\n\tfloat64 expandedRatio = 0.;\n\tfloat64 expandRatio = 0.;\n\tfloat64 ratio = 0.;\n\tfloat64 segmentsSpinProgress = 0.;\n\tfloat64 thumbnailLeft = 0.;\n\tfloat64 photoLeft = 0.;\n\tfloat64 left = 0.;\n\tfloat64 single = 0.;\n\tint smallSkip = 0;\n\tint leftFull = 0;\n\tint leftSmall = 0;\n\tint singleFull = 0;\n\tint singleSmall = 0;\n\tint startIndexSmall = 0;\n\tint endIndexSmall = 0;\n\tint startIndexFull = 0;\n\tint endIndexFull = 0;\n};\n\nList::List(\n\tnot_null<QWidget*> parent,\n\tconst style::DialogsStoriesList &st,\n\trpl::producer<Content> content)\n: RpWidget(parent)\n, _st(st) {\n\tsetCursor(style::cur_default);\n\n\tstd::move(content) | rpl::on_next([=](Content &&content) {\n\t\tshowContent(std::move(content));\n\t}, lifetime());\n\n\tsetMouseTracking(true);\n\tresize(0, _data.empty() ? 0 : st.full.height);\n}\n\nList::~List() = default;\n\nvoid List::showContent(Content &&content) {\n\tif (_content == content) {\n\t\treturn;\n\t}\n\tif (content.elements.empty()) {\n\t\t_data = {};\n\t\t_empty = true;\n\t\treturn;\n\t}\n\tconst auto wasCount = int(_data.items.size());\n\t_content = std::move(content);\n\tauto items = base::take(_data.items);\n\t_data.items.reserve(_content.elements.size());\n\tfor (const auto &element : _content.elements) {\n\t\tconst auto id = element.id;\n\t\tconst auto i = ranges::find(items, id, [](const Item &item) {\n\t\t\treturn item.element.id;\n\t\t});\n\t\tif (i != end(items)) {\n\t\t\t_data.items.push_back(std::move(*i));\n\t\t\tauto &item = _data.items.back();\n\t\t\tif (item.element.thumbnail != element.thumbnail) {\n\t\t\t\titem.element.thumbnail = element.thumbnail;\n\t\t\t\titem.subscribed = false;\n\t\t\t}\n\t\t\tif (item.element.name != element.name) {\n\t\t\t\titem.element.name = element.name;\n\t\t\t\titem.nameCache = QImage();\n\t\t\t}\n\t\t\titem.element.count = element.count;\n\t\t\titem.element.unreadCount = element.unreadCount;\n\t\t\titem.element.hasVideoStream = element.hasVideoStream;\n\t\t} else {\n\t\t\t_data.items.emplace_back(Item{ .element = element });\n\t\t}\n\t}\n\t_lastCollapsedGeometry = {};\n\tif (int(_data.items.size()) != wasCount) {\n\t\tupdateGeometry();\n\t}\n\tupdateScrollMax();\n\tupdate();\n\tif (!wasCount) {\n\t\t_empty = false;\n\t}\n\t_tooltipText = computeTooltipText();\n\tupdateTooltipGeometry();\n}\n\nvoid List::updateScrollMax() {\n\tconst auto &full = _st.full;\n\tconst auto singleFull = full.photoLeft * 2 + full.photo;\n\tconst auto widthFull = full.left + int(_data.items.size()) * singleFull;\n\t_scrollLeftMax = std::max(widthFull - width(), 0);\n\t_scrollLeft = std::clamp(_scrollLeft, 0, _scrollLeftMax);\n\tcheckLoadMore();\n\tupdate();\n}\n\nrpl::producer<uint64> List::clicks() const {\n\treturn _clicks.events();\n}\n\nrpl::producer<ShowMenuRequest> List::showMenuRequests() const {\n\treturn _showMenuRequests.events();\n}\n\nrpl::producer<bool> List::toggleExpandedRequests() const {\n\treturn _toggleExpandedRequests.events();\n}\n\n//rpl::producer<> List::entered() const {\n//\treturn _entered.events();\n//}\n\nrpl::producer<> List::loadMoreRequests() const {\n\treturn _loadMoreRequests.events();\n}\n\nrpl::producer<not_null<QWheelEvent*>> List::verticalScrollEvents() const {\n\treturn _verticalScrollEvents.events();\n}\n\nbool List::toggledHidden() const {\n\treturn _hiddenInstant || _hiddenAnimated;\n}\n\nvoid List::setToggledHidden(bool hiddenInstant, bool hiddenAnimated) {\n\tconst auto hidden = (hiddenInstant || hiddenAnimated);\n\tconst auto hiddenChanged = (hidden != toggledHidden());\n\tconst auto hiddenInstantChanged = (_hiddenInstant != hiddenInstant);\n\tconst auto hiddenAnimatedChanged = (_hiddenAnimated != hiddenAnimated);\n\t_hiddenInstant = hiddenInstant;\n\t_hiddenAnimated = hiddenAnimated;\n\tif (hiddenChanged) {\n\t\tif (_hiddenInstant || !hiddenAnimatedChanged) {\n\t\t\t_hiddenAnimation.stop();\n\t\t\tsetVisible(!toggledHidden());\n\t\t} else {\n\t\t\tconst auto from = hidden ? 0. : 1.;\n\t\t\tconst auto till = hidden ? 1. : 0.;\n\t\t\t_hiddenAnimation.start([=] {\n\t\t\t\tif (!_hiddenAnimation.animating()) {\n\t\t\t\t\tsetVisible(!toggledHidden());\n\t\t\t\t}\n\t\t\t\tupdate();\n\t\t\t}, from, till, st::fadeWrapDuration, anim::linear);\n\t\t\tshow();\n\t\t}\n\t} else if (hiddenInstantChanged && _hiddenInstant) {\n\t\t_hiddenAnimation.stop();\n\t\tsetVisible(!toggledHidden());\n\t}\n}\n\nvoid List::requestExpanded(bool expanded) {\n\tif (_expanded != expanded) {\n\t\t_expanded = expanded;\n\t\tconst auto from = _expanded ? 0. : 1.;\n\t\tconst auto till = _expanded ? 2. : 0.;\n\t\tconst auto duration = (_expanded ? 2 : 1) * st::slideWrapDuration;\n\t\tif (!isHidden() && _expanded) {\n\t\t\ttoggleTooltip(false);\n\t\t}\n\t\t_expandedAnimation.start([=] {\n\t\t\tcheckForFullState();\n\t\t\tupdate();\n\t\t\t_collapsedGeometryChanged.fire({});\n\t\t\tif (!isHidden() && !_expandedAnimation.animating()) {\n\t\t\t\ttoggleTooltip(false);\n\t\t\t}\n\t\t}, from, till, duration, anim::sineInOut);\n\t}\n\t_toggleExpandedRequests.fire_copy(_expanded);\n}\n\n//void List::enterEventHook(QEnterEvent *e) {\n\t//_entered.fire({});\n//}\n\nvoid List::resizeEvent(QResizeEvent *e) {\n\tupdateScrollMax();\n}\n\nvoid List::updateExpanding() {\n\tupdateExpanding(\n\t\t_lastExpandedHeight * _expandCatchUpAnimation.value(1.),\n\t\t_st.full.height);\n}\n\nvoid List::updateExpanding(int expandingHeight, int expandedHeight) {\n\tExpects(!expandingHeight || expandedHeight > 0);\n\n\tconst auto ratio = !expandingHeight\n\t\t? 0.\n\t\t: (float64(expandingHeight) / expandedHeight);\n\tif (_lastRatio == ratio) {\n\t\treturn;\n\t}\n\tconst auto expanding = (ratio > _lastRatio);\n\t_lastRatio = ratio;\n\tconst auto change = _expanded\n\t\t? (!expanding && ratio < kCollapseAfterRatio)\n\t\t: (expanding && ratio > kExpandAfterRatio);\n\tif (change) {\n\t\trequestExpanded(!_expanded);\n\t}\n\tupdateTooltipGeometry();\n}\n\nList::Layout List::computeLayout() {\n\treturn computeLayout(_expandedAnimation.value(_expanded ? 2. : 0.));\n}\n\nList::Layout List::computeLayout(float64 expanded) const {\n\tconst auto segmentsSpinProgress = expanded / 2.;\n\texpanded = std::min(expanded, 1.);\n\n\tconst auto &st = _st.small;\n\tconst auto &full = _st.full;\n\tconst auto expandedRatio = _lastRatio;\n\tconst auto collapsedRatio = expandedRatio * kFrictionRatio;\n\tconst auto ratio = expandedRatio * expanded\n\t\t+ collapsedRatio * (1. - expanded);\n\tconst auto expandRatio = (ratio >= kCollapseAfterRatio)\n\t\t? 1.\n\t\t: (ratio <= kExpandAfterRatio * kFrictionRatio)\n\t\t? 0.\n\t\t: ((ratio - (kExpandAfterRatio * kFrictionRatio))\n\t\t\t/ (kCollapseAfterRatio - (kExpandAfterRatio * kFrictionRatio)));\n\n\tconst auto lerp = [&](float64 a, float64 b) {\n\t\treturn a + (b - a) * ratio;\n\t};\n\tconst auto widthFull = width();\n\tconst auto itemsCount = int(_data.items.size());\n\tconst auto leftFullMin = full.left;\n\tconst auto singleFullMin = full.photoLeft * 2 + full.photo;\n\tconst auto totalFull = leftFullMin + singleFullMin * itemsCount;\n\tconst auto skipSide = (totalFull < widthFull)\n\t\t? (widthFull - totalFull) / (itemsCount + 1)\n\t\t: 0;\n\tconst auto skipBetween = (totalFull < widthFull && itemsCount > 1)\n\t\t? (widthFull - totalFull - 2 * skipSide) / (itemsCount - 1)\n\t\t: skipSide;\n\tconst auto singleFull = singleFullMin + skipBetween;\n\tconst auto smallSkip = (itemsCount > 1\n\t\t&& _data.items[0].element.skipSmall)\n\t\t? 1\n\t\t: 0;\n\tconst auto smallCount = std::min(\n\t\tkSmallThumbsShown,\n\t\titemsCount - smallSkip);\n\tconst auto leftSmall = st.left - (smallSkip ? st.shift : 0);\n\tconst auto leftFull = full.left - _scrollLeft + skipSide;\n\tconst auto startIndexFull = std::max(-leftFull, 0) / singleFull;\n\tconst auto cellLeftFull = leftFull + (startIndexFull * singleFull);\n\tconst auto endIndexFull = std::min(\n\t\t(width() - leftFull + singleFull - 1) / singleFull,\n\t\titemsCount);\n\tconst auto startIndexSmall = std::min(startIndexFull, smallSkip);\n\tconst auto endIndexSmall = smallSkip + smallCount;\n\tconst auto cellLeftSmall = leftSmall + (startIndexSmall * st.shift);\n\tconst auto thumbnailLeftFull = cellLeftFull + full.photoLeft;\n\tconst auto thumbnailLeftSmall = cellLeftSmall + st.photoLeft;\n\tconst auto thumbnailLeft = lerp(thumbnailLeftSmall, thumbnailLeftFull);\n\tconst auto photoLeft = lerp(st.photoLeft, full.photoLeft);\n\treturn Layout{\n\t\t.itemsCount = itemsCount,\n\t\t.geometryShift = QPointF(\n\t\t\t(_state == State::Changing\n\t\t\t\t? (lerp(_changingGeometryFrom.x(), _geometryFull.x()) - x())\n\t\t\t\t: 0.),\n\t\t\t(_state == State::Changing\n\t\t\t\t? (lerp(_changingGeometryFrom.y(), _geometryFull.y()) - y())\n\t\t\t\t: 0.)),\n\t\t.expandedRatio = expandedRatio,\n\t\t.expandRatio = expandRatio,\n\t\t.ratio = ratio,\n\t\t.segmentsSpinProgress = segmentsSpinProgress,\n\t\t.thumbnailLeft = thumbnailLeft,\n\t\t.photoLeft = photoLeft,\n\t\t.left = thumbnailLeft - photoLeft,\n\t\t.single = lerp(st.shift, singleFull),\n\t\t.smallSkip = smallSkip,\n\t\t.leftFull = leftFull,\n\t\t.leftSmall = leftSmall,\n\t\t.singleFull = singleFull,\n\t\t.singleSmall = st.shift,\n\t\t.startIndexSmall = startIndexSmall,\n\t\t.endIndexSmall = endIndexSmall,\n\t\t.startIndexFull = startIndexFull,\n\t\t.endIndexFull = endIndexFull,\n\t};\n}\n\nvoid List::paintEvent(QPaintEvent *e) {\n\tconst auto hidden = _hiddenAnimation.value(toggledHidden() ? 1. : 0.);\n\tif (hidden >= 1.) {\n\t\treturn;\n\t}\n\tconst auto &st = _st.small;\n\tconst auto &full = _st.full;\n\tconst auto layout = computeLayout();\n\tconst auto ratio = layout.ratio;\n\tconst auto expandRatio = layout.expandRatio;\n\tconst auto lerp = [&](float64 a, float64 b) {\n\t\treturn a + (b - a) * ratio;\n\t};\n\tconst auto elerp = [&](float64 a, float64 b) {\n\t\treturn a + (b - a) * expandRatio;\n\t};\n\tconst auto line = elerp(st.lineTwice, full.lineTwice) / 2.;\n\tconst auto photo = lerp(st.photo, full.photo);\n\tconst auto layered = (layout.single < (photo + 4 * line))\n\t\t|| (hidden > 0.);\n\tauto p = QPainter(this);\n\tif (layered) {\n\t\tensureLayer();\n\t\tauto q = QPainter(&_layer);\n\t\tpaint(q, layout, photo, line, true);\n\t\tq.end();\n\n\t\tif (hidden > 0.) {\n\t\t\tp.setOpacity(1. - hidden);\n\t\t}\n\t\tp.drawImage(0, 0, _layer);\n\t} else {\n\t\tpaint(p, layout, photo, line, false);\n\t}\n}\n\nvoid List::ensureLayer() {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto layer = size() * ratio;\n\tif (_layer.size() != layer) {\n\t\t_layer = QImage(layer, QImage::Format_ARGB32_Premultiplied);\n\t\t_layer.setDevicePixelRatio(ratio);\n\t}\n\t_layer.fill(Qt::transparent);\n}\n\nvoid List::paint(\n\t\tQPainter &p,\n\t\tconst Layout &layout,\n\t\tfloat64 photo,\n\t\tfloat64 line,\n\t\tbool layered) {\n\tconst auto &st = _st.small;\n\tconst auto &full = _st.full;\n\tconst auto expandRatio = layout.expandRatio;\n\tconst auto elerp = [&](float64 a, float64 b) {\n\t\treturn a + (b - a) * expandRatio;\n\t};\n\tconst auto lineRead = elerp(st.lineReadTwice, full.lineReadTwice) / 2.;\n\tconst auto photoTopSmall = st.photoTop;\n\tconst auto photoTop = photoTopSmall\n\t\t+ (full.photoTop - photoTopSmall) * layout.expandedRatio;\n\tconst auto nameScale = _lastRatio;\n\tconst auto nameTop = full.nameTop\n\t\t+ (photoTop + photo - full.photoTop - full.photo);\n\tconst auto nameWidth = nameScale * AvailableNameWidth(_st);\n\tconst auto nameHeight = nameScale * full.nameStyle.font->height;\n\tconst auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.;\n\tconst auto readUserpicOpacity = elerp(_st.readOpacity, 1.);\n\tconst auto readUserpicAppearingOpacity = elerp(_st.readOpacity, 0.);\n\tif (_state == State::Changing) {\n\t\tp.translate(layout.geometryShift);\n\t}\n\n\tconst auto drawSmall = (expandRatio < 1.);\n\tconst auto drawFull = (expandRatio > 0.);\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tconst auto count = std::max(\n\t\tlayout.endIndexFull - layout.startIndexFull,\n\t\tlayout.endIndexSmall - layout.startIndexSmall);\n\n\tstruct Single {\n\t\tfloat64 x = 0.;\n\t\tint indexSmall = 0;\n\t\tItem *itemSmall = nullptr;\n\t\tint indexFull = 0;\n\t\tItem *itemFull = nullptr;\n\t\tfloat64 photoTop = 0.;\n\n\t\texplicit operator bool() const {\n\t\t\treturn itemSmall || itemFull;\n\t\t}\n\t};\n\tconst auto lookup = [&](int index) {\n\t\tconst auto indexSmall = layout.startIndexSmall + index;\n\t\tconst auto indexFull = layout.startIndexFull + index;\n\t\tconst auto ySmall = photoTopSmall\n\t\t\t+ ((photoTop - photoTopSmall)\n\t\t\t\t* (kSmallThumbsShown - indexSmall + layout.smallSkip) / 0.5);\n\t\tconst auto y = elerp(ySmall, photoTop);\n\n\t\tconst auto small = (drawSmall\n\t\t\t&& indexSmall < layout.endIndexSmall\n\t\t\t&& indexSmall >= layout.smallSkip)\n\t\t\t? &_data.items[indexSmall]\n\t\t\t: nullptr;\n\t\tconst auto full = (drawFull && indexFull < layout.endIndexFull)\n\t\t\t? &_data.items[indexFull]\n\t\t\t: nullptr;\n\t\tconst auto x = layout.left + layout.single * index;\n\t\treturn Single{ x, indexSmall, small, indexFull, full, y };\n\t};\n\tconst auto hasUnread = [&](const Single &single) {\n\t\tconst auto itemSmall = single.itemSmall;\n\t\tconst auto itemFull = single.itemFull;\n\t\treturn false\n\t\t\t||(itemSmall\n\t\t\t\t&& (itemSmall->element.unreadCount\n\t\t\t\t\t|| itemSmall->element.hasVideoStream))\n\t\t\t|| (itemFull\n\t\t\t\t&& (itemFull->element.unreadCount\n\t\t\t\t\t|| itemFull->element.hasVideoStream));\n\t};\n\tconst auto enumerate = [&](auto &&paintGradient, auto &&paintOther) {\n\t\tauto nextGradientPainted = false;\n\t\tauto skippedPainted = false;\n\t\tconst auto first = layout.smallSkip - layout.startIndexSmall;\n\t\tfor (auto i = count; i != first;) {\n\t\t\t--i;\n\t\t\tconst auto next = (i > 0) ? lookup(i - 1) : Single();\n\t\t\tconst auto gradientPainted = nextGradientPainted;\n\t\t\tnextGradientPainted = false;\n\t\t\tif (const auto current = lookup(i)) {\n\t\t\t\tif (i == first && next && !skippedPainted) {\n\t\t\t\t\tskippedPainted = true;\n\t\t\t\t\tpaintGradient(next);\n\t\t\t\t\tpaintOther(next);\n\t\t\t\t}\n\t\t\t\tif (!gradientPainted) {\n\t\t\t\t\tpaintGradient(current);\n\t\t\t\t}\n\t\t\t\tif (i > first && hasUnread(current) && next) {\n\t\t\t\t\tif (current.itemSmall || !next.itemSmall) {\n\t\t\t\t\t\tif (i - 1 == first\n\t\t\t\t\t\t\t&& first > 0\n\t\t\t\t\t\t\t&& !skippedPainted) {\n\t\t\t\t\t\t\tif (const auto skipped = lookup(i - 2)) {\n\t\t\t\t\t\t\t\tskippedPainted = true;\n\t\t\t\t\t\t\t\tpaintGradient(skipped);\n\t\t\t\t\t\t\t\tpaintOther(skipped);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tnextGradientPainted = true;\n\t\t\t\t\t\tpaintGradient(next);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tpaintOther(current);\n\t\t\t}\n\t\t}\n\t};\n\tauto gradient = Ui::UnreadStoryOutlineGradient();\n\tenumerate([&](Single single) {\n\t\t// Name.\n\t\tif (const auto full = single.itemFull) {\n\t\t\tvalidateName(full);\n\t\t\tif (expandRatio > 0.) {\n\t\t\t\tp.setOpacity(expandRatio);\n\t\t\t\tp.drawImage(QRectF(\n\t\t\t\t\tsingle.x + nameLeft,\n\t\t\t\t\tnameTop,\n\t\t\t\t\tnameWidth,\n\t\t\t\t\tnameHeight\n\t\t\t\t), full->nameCache);\n\t\t\t}\n\t\t}\n\n\t\t// Unread gradient.\n\t\tconst auto x = single.x;\n\t\tconst auto userpic = QRectF(\n\t\t\tx + layout.photoLeft,\n\t\t\tsingle.photoTop,\n\t\t\tphoto,\n\t\t\tphoto);\n\t\tconst auto small = single.itemSmall;\n\t\tconst auto itemFull = single.itemFull;\n\t\tconst auto smallHasVideoStream = small\n\t\t\t&& small->element.hasVideoStream;\n\t\tconst auto smallUnread = smallHasVideoStream\n\t\t\t|| (small && small->element.unreadCount);\n\t\tconst auto fullHasVideoStream = itemFull\n\t\t\t&& itemFull->element.hasVideoStream;\n\t\tconst auto fullUnreadCount = fullHasVideoStream\n\t\t\t? 1\n\t\t\t: itemFull\n\t\t\t? itemFull->element.unreadCount\n\t\t\t: 0;\n\t\tconst auto unreadOpacity = (smallUnread && fullUnreadCount)\n\t\t\t? 1.\n\t\t\t: smallUnread\n\t\t\t? (1. - expandRatio)\n\t\t\t: fullUnreadCount\n\t\t\t? expandRatio\n\t\t\t: 0.;\n\t\tif (unreadOpacity > 0.) {\n\t\t\tp.setOpacity(unreadOpacity);\n\t\t\tconst auto outerAdd = 1.5 * line;\n\t\t\tconst auto outer = userpic.marginsAdded(\n\t\t\t\t{ outerAdd, outerAdd, outerAdd, outerAdd });\n\t\t\tgradient.setStart(userpic.topRight());\n\t\t\tgradient.setFinalStop(userpic.bottomLeft());\n\t\t\tif (!fullUnreadCount) {\n\t\t\t\tif (smallHasVideoStream) {\n\t\t\t\t\tp.setPen(QPen(st::attentionButtonFg->c, line));\n\t\t\t\t} else {\n\t\t\t\t\tp.setPen(QPen(gradient, line));\n\t\t\t\t}\n\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t\tif (style::SquareUserpics()) {\n\t\t\t\tp.drawRect(outer);\n\t\t\t\t} else {\n\t\t\t\tp.drawEllipse(outer);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tvalidateSegments(itemFull, gradient, line, true);\n\t\t\t\tUi::PaintOutlineSegments(\n\t\t\t\t\tp,\n\t\t\t\t\touter,\n\t\t\t\t\titemFull->segments,\n\t\t\t\t\tlayout.segmentsSpinProgress);\n\t\t\t}\n\t\t}\n\t\tp.setOpacity(1.);\n\t}, [&](Single single) {\n\t\tExpects(single.itemSmall || single.itemFull);\n\n\t\tconst auto x = single.x;\n\t\tconst auto userpic = QRectF(\n\t\t\tx + layout.photoLeft,\n\t\t\tsingle.photoTop,\n\t\t\tphoto,\n\t\t\tphoto);\n\t\tconst auto small = single.itemSmall;\n\t\tconst auto itemFull = single.itemFull;\n\t\tconst auto smallUnread = small\n\t\t\t&& (small->element.unreadCount\n\t\t\t\t|| small->element.hasVideoStream);\n\t\tconst auto fullUnreadCount = !itemFull\n\t\t\t? 0\n\t\t\t: itemFull->element.hasVideoStream\n\t\t\t? 1\n\t\t\t: itemFull->element.unreadCount;\n\t\tconst auto fullCount = itemFull ? itemFull->element.count : 0;\n\n\t\t// White circle with possible read gray line.\n\t\tconst auto hasReadLine = (itemFull && fullUnreadCount < fullCount);\n\t\tp.setOpacity((small && itemFull)\n\t\t\t? 1.\n\t\t\t: small\n\t\t\t? (1. - expandRatio)\n\t\t\t: expandRatio);\n\t\tconst auto add = line + (hasReadLine ? (lineRead / 2.) : 0.);\n\t\tconst auto rect = userpic.marginsAdded({ add, add, add, add });\n\t\tif (layered) {\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(st::transparent);\n\t\t\tif (style::SquareUserpics()) {\n\t\t\tp.drawRect(rect);\n\t\t\t} else {\n\t\t\tp.drawEllipse(rect);\n\t\t\t}\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\t}\n\t\tif (hasReadLine) {\n\t\t\tif (small\n\t\t\t\t&& !small->element.unreadCount\n\t\t\t\t&& !small->element.hasVideoStream) {\n\t\t\t\tp.setOpacity(expandRatio);\n\t\t\t}\n\t\t\tvalidateSegments(\n\t\t\t\titemFull,\n\t\t\t\tst::dialogsUnreadBgMuted->b,\n\t\t\t\tlineRead,\n\t\t\t\tfalse);\n\t\t\tUi::PaintOutlineSegments(\n\t\t\t\tp,\n\t\t\t\trect,\n\t\t\t\titemFull->segments,\n\t\t\t\tlayout.segmentsSpinProgress);\n\t\t}\n\n\t\t// Userpic.\n\t\tif (itemFull == small) {\n\t\t\tp.setOpacity(smallUnread ? 1. : readUserpicOpacity);\n\t\t\tvalidateThumbnail(itemFull);\n\t\t\tconst auto size = full.photo;\n\t\t\tp.drawImage(userpic, itemFull->element.thumbnail->image(size));\n\t\t} else {\n\t\t\tif (small) {\n\t\t\t\tp.setOpacity(smallUnread\n\t\t\t\t\t? (itemFull ? 1. : (1. - expandRatio))\n\t\t\t\t\t: (itemFull\n\t\t\t\t\t\t? _st.readOpacity\n\t\t\t\t\t\t: readUserpicAppearingOpacity));\n\t\t\t\tvalidateThumbnail(small);\n\t\t\t\tconst auto size = (expandRatio > 0.)\n\t\t\t\t\t? full.photo\n\t\t\t\t\t: st.photo;\n\t\t\t\tp.drawImage(userpic, small->element.thumbnail->image(size));\n\t\t\t}\n\t\t\tif (itemFull) {\n\t\t\t\tp.setOpacity(expandRatio);\n\t\t\t\tvalidateThumbnail(itemFull);\n\t\t\t\tconst auto size = full.photo;\n\t\t\t\tp.drawImage(\n\t\t\t\t\tuserpic,\n\t\t\t\t\titemFull->element.thumbnail->image(size));\n\t\t\t}\n\t\t}\n\n\t\tif (const auto full = single.itemFull) {\n\t\t\tif (full->element.hasVideoStream && expandRatio > 0.) {\n\t\t\t\tp.setOpacity(expandRatio);\n\t\t\t\tconst auto skip = std::ceil(line + lineRead);\n\t\t\t\tUi::PaintLiveBadge(\n\t\t\t\t\tp,\n\t\t\t\t\tstd::ceil(userpic.x() - skip),\n\t\t\t\t\tstd::ceil(userpic.y() - skip),\n\t\t\t\t\tstd::ceil(userpic.width() + 2 * skip),\n\t\t\t\t\tst::windowBg->c);\n\t\t\t}\n\t\t}\n\t\tp.setOpacity(1.);\n\t});\n}\n\nvoid List::validateThumbnail(not_null<Item*> item) {\n\tif (!item->subscribed) {\n\t\titem->subscribed = true;\n\t\t//const auto id = item.element.id;\n\t\titem->element.thumbnail->subscribeToUpdates([=] {\n\t\t\tupdate();\n\t\t});\n\t}\n}\n\nvoid List::validateSegments(\n\t\tnot_null<Item*> item,\n\t\tconst QBrush &brush,\n\t\tfloat64 line,\n\t\tbool forUnread) {\n\tconst auto count = item->element.count;\n\tconst auto unread = item->element.unreadCount;\n\tif (item->element.hasVideoStream) {\n\t\titem->segments.resize(1);\n\t\tif (forUnread) {\n\t\t\titem->segments[0].width = line;\n\t\t\titem->segments[0].brush = st::attentionButtonFg->b;\n\t\t} else {\n\t\t\titem->segments[0].width = 0.;\n\t\t}\n\t\treturn;\n\t}\n\tif (int(item->segments.size()) != count) {\n\t\titem->segments.resize(count);\n\t}\n\tauto i = 0;\n\tif (forUnread) {\n\t\tfor (; i != count - unread; ++i) {\n\t\t\titem->segments[i].width = 0.;\n\t\t}\n\t\tfor (; i != count; ++i) {\n\t\t\titem->segments[i].brush = brush;\n\t\t\titem->segments[i].width = line;\n\t\t}\n\t} else {\n\t\tfor (; i != count - unread; ++i) {\n\t\t\titem->segments[i].brush = brush;\n\t\t\titem->segments[i].width = line;\n\t\t}\n\t\tfor (; i != count; ++i) {\n\t\t\titem->segments[i].width = 0.;\n\t\t}\n\t}\n}\n\nvoid List::validateName(not_null<Item*> item) {\n\tconst auto &element = item->element;\n\tconst auto &color = (element.unreadCount || element.skipSmall)\n\t\t? st::dialogsNameFg\n\t\t: st::windowSubTextFg;\n\tif (!item->nameCache.isNull() && item->nameCacheColor == color->c) {\n\t\treturn;\n\t}\n\tconst auto &full = _st.full;\n\tconst auto &font = full.nameStyle.font;\n\tconst auto available = AvailableNameWidth(_st);\n\tconst auto my = element.skipSmall\n\t\t? tr::lng_stories_my_name(tr::now)\n\t\t: QString();\n\tconst auto use = (my.isEmpty()\n\t\t|| full.nameStyle.font->width(my) > available)\n\t\t? element.name\n\t\t: my;\n\tconst auto text = Ui::Text::String(full.nameStyle, use);\n\tconst auto ratio = style::DevicePixelRatio();\n\titem->nameCacheColor = color->c;\n\titem->nameCache = QImage(\n\t\tQSize(available, font->height) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\titem->nameCache.setDevicePixelRatio(ratio);\n\titem->nameCache.fill(Qt::transparent);\n\tauto p = Painter(&item->nameCache);\n\tp.setPen(color);\n\ttext.drawElided(p, 0, 0, available, 1, style::al_top);\n}\n\nvoid List::wheelEvent(QWheelEvent *e) {\n\tconst auto phase = e->phase();\n\tconst auto fullDelta = e->pixelDelta().isNull()\n\t\t? e->angleDelta()\n\t\t: e->pixelDelta();\n\tif (phase == Qt::ScrollBegin || phase == Qt::ScrollEnd) {\n\t\t_scrollingLock = Qt::Orientation();\n\t\tif (fullDelta.isNull()) {\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto vertical = qAbs(fullDelta.x()) < qAbs(fullDelta.y());\n\tif (_scrollingLock == Qt::Orientation() && phase != Qt::NoScrollPhase) {\n\t\t_scrollingLock = vertical ? Qt::Vertical : Qt::Horizontal;\n\t}\n\tif (_scrollingLock == Qt::Vertical || (vertical && !_scrollLeftMax)) {\n\t\t_verticalScrollEvents.fire(e);\n\t\treturn;\n\t} else if (_state == State::Small) {\n\t\te->ignore();\n\t\treturn;\n\t}\n\tconst auto delta = vertical\n\t\t? fullDelta.y()\n\t\t: ((style::RightToLeft() ? -1 : 1) * fullDelta.x());\n\n\tconst auto now = _scrollLeft;\n\tconst auto used = now - delta;\n\tconst auto next = std::clamp(used, 0, _scrollLeftMax);\n\tif (next != now) {\n\t\trequestExpanded(true);\n\t\t_scrollLeft = next;\n\t\tupdateSelected();\n\t\tcheckLoadMore();\n\t\tupdate();\n\t}\n\te->accept();\n}\n\nvoid List::mousePressEvent(QMouseEvent *e) {\n\tif (e->button() != Qt::LeftButton) {\n\t\treturn;\n\t} else if (_state == State::Small) {\n\t\trequestExpanded(true);\n\t\tif (const auto onstack = _tooltipHide) {\n\t\t\tonstack();\n\t\t}\n\t\treturn;\n\t} else if (_state != State::Full) {\n\t\treturn;\n\t}\n\t_lastMousePosition = e->globalPos();\n\tupdateSelected();\n\n\t_mouseDownPosition = _lastMousePosition;\n\t_pressed = _selected;\n}\n\nvoid List::mouseMoveEvent(QMouseEvent *e) {\n\t_lastMousePosition = e->globalPos();\n\tupdateSelected();\n\n\tif (!_dragging && _mouseDownPosition && _state == State::Full) {\n\t\tif ((_lastMousePosition - *_mouseDownPosition).manhattanLength()\n\t\t\t>= QApplication::startDragDistance()) {\n\t\t\t_dragging = true;\n\t\t\t_startDraggingLeft = _scrollLeft;\n\t\t}\n\t}\n\tcheckDragging();\n}\n\nvoid List::checkDragging() {\n\tif (_dragging) {\n\t\tconst auto sign = (style::RightToLeft() ? -1 : 1);\n\t\tconst auto newLeft = std::clamp(\n\t\t\t(sign * (_mouseDownPosition->x() - _lastMousePosition.x())\n\t\t\t\t+ _startDraggingLeft),\n\t\t\t0,\n\t\t\t_scrollLeftMax);\n\t\tif (newLeft != _scrollLeft) {\n\t\t\t_scrollLeft = newLeft;\n\t\t\tcheckLoadMore();\n\t\t\tupdate();\n\t\t}\n\t}\n}\n\nvoid List::checkLoadMore() {\n\tif (_scrollLeftMax - _scrollLeft < width() * kPreloadPages) {\n\t\t_loadMoreRequests.fire({});\n\t}\n}\n\nvoid List::mouseReleaseEvent(QMouseEvent *e) {\n\t_lastMousePosition = e->globalPos();\n\tconst auto guard = gsl::finally([&] {\n\t\t_mouseDownPosition = std::nullopt;\n\t});\n\n\tconst auto pressed = std::exchange(_pressed, -1);\n\tif (finishDragging()) {\n\t\treturn;\n\t}\n\tupdateSelected();\n\tif (_selected == pressed) {\n\t\tif (!_expanded) {\n\t\t\trequestExpanded(true);\n\t\t} else if (_selected < _data.items.size()) {\n\t\t\t_clicks.fire_copy(_data.items[_selected].element.id);\n\t\t}\n\t}\n}\n\nvoid List::setExpandedHeight(int height, bool momentum) {\n\theight = std::clamp(height, 0, _st.full.height);\n\tif (_lastExpandedHeight == height) {\n\t\treturn;\n\t} else if (momentum && _expandIgnored) {\n\t\treturn;\n\t} else if (momentum && height > 0 && !_lastExpandedHeight) {\n\t\t_expandIgnored = true;\n\t\treturn;\n\t} else if (!momentum && _expandIgnored && height > 0) {\n\t\t_expandIgnored = false;\n\t\t_expandCatchUpAnimation.start([=] {\n\t\t\tupdateExpanding();\n\t\t\tupdate();\n\t\t\tcheckForFullState();\n\t\t}, 0., 1., kExpandCatchUpDuration);\n\t} else if (!height && _expandCatchUpAnimation.animating()) {\n\t\t_expandCatchUpAnimation.stop();\n\t}\n\t_lastExpandedHeight = height;\n\tupdateExpanding();\n\tif (!checkForFullState()) {\n\t\tsetState(!height ? State::Small : State::Changing);\n\t}\n\tupdate();\n}\n\nbool List::checkForFullState() {\n\tif (_expandCatchUpAnimation.animating()\n\t\t|| _expandedAnimation.animating()\n\t\t|| _lastExpandedHeight < _st.full.height) {\n\t\treturn false;\n\t}\n\tsetState(State::Full);\n\treturn true;\n}\n\nvoid List::setLayoutConstraints(\n\t\tQPoint positionSmall,\n\t\tstyle::align alignSmall,\n\t\tQRect geometryFull) {\n\tif (_positionSmall == positionSmall\n\t\t&& _alignSmall == alignSmall\n\t\t&& _geometryFull == geometryFull) {\n\t\treturn;\n\t}\n\t_positionSmall = positionSmall;\n\t_alignSmall = alignSmall;\n\t_geometryFull = geometryFull;\n\t_lastCollapsedGeometry = {};\n\tupdateGeometry();\n\tupdate();\n}\n\nTextWithEntities List::computeTooltipText() const {\n\tconst auto &list = _data.items;\n\tif (list.empty()) {\n\t\treturn {};\n\t} else if (list.size() == 1 && list.front().element.skipSmall) {\n\t\treturn { tr::lng_stories_click_to_view_mine(tr::now) };\n\t}\n\tauto names = QStringList();\n\tfor (const auto &item : list) {\n\t\tif (item.element.skipSmall) {\n\t\t\tcontinue;\n\t\t}\n\t\tnames.append(item.element.name);\n\t\tif (names.size() >= kMaxTooltipNames) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tauto sequence = tr::bold(names.front());\n\tif (names.size() > 1) {\n\t\tfor (auto i = 1; i + 1 != names.size(); ++i) {\n\t\t\tsequence = tr::lng_stories_click_to_view_and_one(\n\t\t\t\ttr::now,\n\t\t\t\tlt_accumulated,\n\t\t\t\tsequence,\n\t\t\t\tlt_user,\n\t\t\t\ttr::bold(names[i]),\n\t\t\t\ttr::marked);\n\t\t}\n\t\tsequence = tr::lng_stories_click_to_view_and_last(\n\t\t\ttr::now,\n\t\t\tlt_accumulated,\n\t\t\tsequence,\n\t\t\tlt_user,\n\t\t\ttr::bold(names.back()),\n\t\t\ttr::marked);\n\t}\n\treturn tr::lng_stories_click_to_view(\n\t\ttr::now,\n\t\tlt_users,\n\t\tsequence,\n\t\ttr::marked);\n}\n\nvoid List::setShowTooltip(\n\t\tnot_null<Ui::RpWidget*> tooltipParent,\n\t\trpl::producer<bool> shown,\n\t\tFn<void()> hide) {\n\t_tooltip = nullptr;\n\t_tooltipHide = std::move(hide);\n\t_tooltipNotHidden = std::move(shown);\n\t_tooltipText = computeTooltipText();\n\tconst auto notEmpty = [](const TextWithEntities &text) {\n\t\treturn !text.empty();\n\t};\n\t_tooltip = std::make_unique<Ui::ImportantTooltip>(\n\t\ttooltipParent,\n\t\tUi::MakeTooltipWithClose(\n\t\t\ttooltipParent,\n\t\t\t_tooltipText.value() | rpl::filter(notEmpty),\n\t\t\tst::dialogsStoriesTooltipMaxWidth,\n\t\t\tst::dialogsStoriesTooltipLabel,\n\t\t\tst::importantTooltipHide,\n\t\t\tst::defaultImportantTooltip.padding,\n\t\t\t_tooltipHide),\n\t\tst::dialogsStoriesTooltip);\n\tconst auto tooltip = _tooltip.get();\n\tconst auto weak = QPointer<QWidget>(tooltip);\n\ttooltip->toggleFast(false);\n\tupdateTooltipGeometry();\n\n\t{\n\t\tconst auto recompute = [=] {\n\t\t\tupdateTooltipGeometry();\n\t\t\ttooltip->raise();\n\t\t};\n\t\tusing namespace base;\n\t\tusing Event = not_null<QEvent*>;\n\t\tinstall_event_filter(tooltip, tooltipParent, [=](Event e) {\n\t\t\tif (e->type() == QEvent::ChildAdded) {\n\t\t\t\trecompute();\n\t\t\t}\n\t\t\treturn EventFilterResult::Continue;\n\t\t});\n\t}\n\n\trpl::combine(\n\t\t_tooltipNotHidden.value(),\n\t\t_tooltipText.value() | rpl::map(\n\t\t\tnotEmpty\n\t\t) | rpl::distinct_until_changed(),\n\t\ttooltipParent->windowActiveValue()\n\t) | rpl::on_next([=](bool, bool, bool active) {\n\t\t_tooltipWindowActive = active;\n\t\tif (!isHidden()) {\n\t\t\ttoggleTooltip(false);\n\t\t}\n\t}, tooltip->lifetime());\n\n\tshownValue(\n\t) | rpl::skip(1) | rpl::on_next([=](bool shown) {\n\t\ttoggleTooltip(true);\n\t}, tooltip->lifetime());\n}\n\nvoid List::raiseTooltip() {\n\tif (_tooltip) {\n\t\t_tooltip->raise();\n\t}\n}\n\nvoid List::toggleTooltip(bool fast) {\n\tconst auto shown = !_expanded\n\t\t&& !_expandedAnimation.animating()\n\t\t&& !isHidden()\n\t\t&& _tooltipNotHidden.current()\n\t\t&& !_tooltipText.current().empty()\n\t\t&& isActiveWindow();\n\tif (_tooltip) {\n\t\tif (fast) {\n\t\t\t_tooltip->toggleFast(shown);\n\t\t} else {\n\t\t\t_tooltip->toggleAnimated(shown);\n\t\t}\n\t}\n\tif (shown) {\n\t\tupdateTooltipGeometry();\n\t}\n}\n\nvoid List::updateTooltipGeometry() {\n\tif (!_tooltip || _expanded || _expandedAnimation.animating()) {\n\t\treturn;\n\t}\n\tconst auto collapsed = collapsedGeometryCurrent();\n\tconst auto geometry = Ui::MapFrom(\n\t\t_tooltip->parentWidget(),\n\t\tparentWidget(),\n\t\tQRect(\n\t\t\tcollapsed.geometry.x(),\n\t\t\tcollapsed.geometry.y(),\n\t\t\tint(std::ceil(collapsed.singleWidth)),\n\t\t\tcollapsed.geometry.height()));\n\tconst auto weak = QPointer<QWidget>(_tooltip.get());\n\tconst auto countPosition = [=](QSize size) {\n\t\tconst auto left = geometry.x()\n\t\t\t+ (geometry.width() - size.width()) / 2;\n\t\tconst auto right = _tooltip->parentWidget()->width()\n\t\t\t- st::dialogsStoriesTooltip.padding.right();\n\t\treturn QPoint(\n\t\t\tstd::max(std::min(left, right - size.width()), 0),\n\t\t\tgeometry.y() + geometry.height());\n\t};\n\t_tooltip->pointAt(geometry, RectPart::Bottom, countPosition);\n}\n\nList::CollapsedGeometry List::collapsedGeometryCurrent() const {\n\tconst auto expanded = _expandedAnimation.value(_expanded ? 2. : 0.);\n\tif (expanded >= 1.) {\n\t\tconst auto single = 2 * _st.full.photoLeft + _st.full.photo;\n\t\treturn { QRect(), 1., float64(single) };\n\t} else if (_lastCollapsedRatio == _lastRatio\n\t\t&& _lastCollapsedGeometry.expanded == expanded\n\t\t&& !_lastCollapsedGeometry.geometry.isEmpty()) {\n\t\treturn _lastCollapsedGeometry;\n\t}\n\tconst auto layout = computeLayout(0.);\n\tconst auto small = countSmallGeometry();\n\tconst auto index = layout.smallSkip - layout.startIndexSmall;\n\tconst auto shift = x() + layout.geometryShift.x();\n\tconst auto left = int(base::SafeRound(\n\t\tshift + layout.left + layout.single * index));\n\tconst auto width = small.x() + small.width() - left;\n\tconst auto photoTopSmall = _st.small.photoTop;\n\tconst auto photoTop = photoTopSmall\n\t\t+ (_st.full.photoTop - photoTopSmall) * layout.expandedRatio;\n\tconst auto ySmall = photoTopSmall\n\t\t+ ((photoTop - photoTopSmall) * kSmallThumbsShown / 0.5);\n\tconst auto photo = _st.small.photo\n\t\t+ (_st.full.photo - _st.small.photo) * layout.ratio;\n\tconst auto top = y() + layout.geometryShift.y();\n\t_lastCollapsedRatio = _lastRatio;\n\t_lastCollapsedGeometry = {\n\t\tQRect(left, top, width, ySmall + photo + _st.full.photoTop),\n\t\texpanded,\n\t\tlayout.photoLeft * 2 + photo,\n\t};\n\treturn _lastCollapsedGeometry;\n}\n\nrpl::producer<> List::collapsedGeometryChanged() const {\n\treturn _collapsedGeometryChanged.events();\n}\n\nvoid List::updateGeometry() {\n\tswitch (_state) {\n\tcase State::Small: setGeometry(countSmallGeometry()); break;\n\tcase State::Changing: {\n\t\t_changingGeometryFrom = countSmallGeometry();\n\t\tsetGeometry(_geometryFull.united(_changingGeometryFrom));\n\t} break;\n\tcase State::Full: setGeometry(_geometryFull);\n\t}\n\tupdateTooltipGeometry();\n\tupdate();\n}\n\nQRect List::countSmallGeometry() const {\n\tconst auto &st = _st.small;\n\tconst auto layout = computeLayout(0.);\n\tconst auto count = layout.endIndexSmall\n\t\t- std::max(layout.startIndexSmall, layout.smallSkip);\n\tconst auto width = st.left\n\t\t+ st.photoLeft\n\t\t+ st.photo + (count - 1) * st.shift\n\t\t+ st.photoLeft\n\t\t+ st.left;\n\tconst auto left = ((_alignSmall & Qt::AlignRight) == Qt::AlignRight)\n\t\t? (_positionSmall.x() - width)\n\t\t: ((_alignSmall & Qt::AlignCenter) == Qt::AlignCenter)\n\t\t? (_positionSmall.x() - (width / 2))\n\t\t: _positionSmall.x();\n\treturn QRect(\n\t\tleft,\n\t\t_positionSmall.y(),\n\t\twidth,\n\t\tst.photoTop + st.photo + st.photoTop);\n}\n\nvoid List::setState(State state) {\n\tif (_state == state) {\n\t\treturn;\n\t}\n\t_state = state;\n\tupdateGeometry();\n}\n\nvoid List::contextMenuEvent(QContextMenuEvent *e) {\n\t_menu = nullptr;\n\n\tif (e->reason() == QContextMenuEvent::Mouse) {\n\t\t_lastMousePosition = e->globalPos();\n\t\tupdateSelected();\n\t}\n\tif (_selected < 0 || _data.empty() || !_expanded) {\n\t\treturn;\n\t}\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\tst::popupMenuWithIcons);\n\t_showMenuRequests.fire({\n\t\t_data.items[_selected].element.id,\n\t\tUi::Menu::CreateAddActionCallback(_menu),\n\t});\n\tif (_menu->empty()) {\n\t\t_menu = nullptr;\n\t\treturn;\n\t}\n\tconst auto updateAfterMenuDestroyed = [=] {\n\t\tconst auto globalPosition = QCursor::pos();\n\t\tif (rect().contains(mapFromGlobal(globalPosition))) {\n\t\t\t_lastMousePosition = globalPosition;\n\t\t\tupdateSelected();\n\t\t}\n\t};\n\tQObject::connect(\n\t\t_menu.get(),\n\t\t&QObject::destroyed,\n\t\tcrl::guard(&_menuGuard, updateAfterMenuDestroyed));\n\t_menu->popup(e->globalPos());\n\te->accept();\n}\n\nbool List::finishDragging() {\n\tif (!_dragging) {\n\t\treturn false;\n\t}\n\tcheckDragging();\n\t_dragging = false;\n\tupdateSelected();\n\treturn true;\n}\n\nvoid List::updateSelected() {\n\tif (_pressed >= 0) {\n\t\treturn;\n\t}\n\tconst auto &st = _st.small;\n\tconst auto p = mapFromGlobal(_lastMousePosition);\n\tconst auto layout = computeLayout();\n\tconst auto firstRightFull = layout.leftFull\n\t\t+ (layout.startIndexFull + 1) * layout.singleFull;\n\tconst auto secondLeftFull = firstRightFull;\n\tconst auto firstRightSmall = layout.leftSmall\n\t\t+ st.photoLeft\n\t\t+ st.photo;\n\tconst auto secondLeftSmall = layout.smallSkip\n\t\t? (layout.leftSmall + st.photoLeft + st.shift)\n\t\t: firstRightSmall;\n\tconst auto lastRightAddFull = 0;\n\tconst auto lastRightAddSmall = st.photoLeft;\n\tconst auto lerp = [&](float64 a, float64 b) {\n\t\treturn a + (b - a) * layout.ratio;\n\t};\n\tconst auto firstRight = lerp(firstRightSmall, firstRightFull);\n\tconst auto secondLeft = lerp(secondLeftSmall, secondLeftFull);\n\tconst auto lastRightAdd = lerp(lastRightAddSmall, lastRightAddFull);\n\tconst auto activateFull = (layout.ratio >= 0.5);\n\tconst auto startIndex = activateFull\n\t\t? layout.startIndexFull\n\t\t: layout.startIndexSmall;\n\tconst auto endIndex = activateFull\n\t\t? layout.endIndexFull\n\t\t: layout.endIndexSmall;\n\tconst auto x = p.x();\n\tconst auto infiniteIndex = (x < secondLeft)\n\t\t? 0\n\t\t: int(\n\t\t\tstd::floor((std::max(x - firstRight, 0.)) / layout.single) + 1);\n\tconst auto index = (endIndex == startIndex)\n\t\t? -1\n\t\t: (infiniteIndex == endIndex - startIndex\n\t\t\t&& x < firstRight\n\t\t\t\t+ (endIndex - startIndex - 1) * layout.single\n\t\t\t\t+ lastRightAdd)\n\t\t? (infiniteIndex - 1) // Last small part should still be clickable.\n\t\t: (startIndex + infiniteIndex >= endIndex)\n\t\t? (_st.fullClickable ? (endIndex - 1) : -1)\n\t\t: infiniteIndex;\n\tconst auto selected = (index < 0\n\t\t|| startIndex + index >= layout.itemsCount)\n\t\t? -1\n\t\t: (startIndex + index);\n\tif (_selected != selected) {\n\t\tconst auto over = (selected >= 0);\n\t\tif (over != (_selected >= 0)) {\n\t\t\tsetCursor(over ? style::cur_pointer : style::cur_default);\n\t\t}\n\t\t_selected = selected;\n\t}\n}\n\n} // namespace Dialogs::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/qt/qt_compare.h\"\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/rp_widget.h\"\n\nclass QPainter;\n\nnamespace style {\nstruct DialogsStories;\nstruct DialogsStoriesList;\n} // namespace style\n\nnamespace Ui {\nclass PopupMenu;\nclass DynamicImage;\nstruct OutlineSegment;\nclass ImportantTooltip;\n} // namespace Ui\n\nnamespace Dialogs::Stories {\n\nstruct Element {\n\tuint64 id = 0;\n\tQString name;\n\tstd::shared_ptr<Ui::DynamicImage> thumbnail;\n\tuint32 count : 15 = 0;\n\tuint32 unreadCount : 15 = 0;\n\tuint32 hasVideoStream : 1 = 0;\n\tuint32 skipSmall : 1 = 0;\n\n\tfriend inline bool operator==(\n\t\tconst Element &a,\n\t\tconst Element &b) = default;\n};\n\nstruct Content {\n\tstd::vector<Element> elements;\n\tint total = 0;\n\n\tfriend inline bool operator==(\n\t\tconst Content &a,\n\t\tconst Content &b) = default;\n};\n\nstruct ShowMenuRequest {\n\tuint64 id = 0;\n\tUi::Menu::MenuCallback callback;\n};\n\nclass List final : public Ui::RpWidget {\npublic:\n\tList(\n\t\tnot_null<QWidget*> parent,\n\t\tconst style::DialogsStoriesList &st,\n\t\trpl::producer<Content> content);\n\t~List();\n\n\tvoid setExpandedHeight(int height, bool momentum = false);\n\tvoid setLayoutConstraints(\n\t\tQPoint positionSmall,\n\t\tstyle::align alignSmall,\n\t\tQRect geometryFull = QRect());\n\tvoid setShowTooltip(\n\t\tnot_null<Ui::RpWidget*> tooltipParent,\n\t\trpl::producer<bool> shown,\n\t\tFn<void()> hide);\n\tvoid raiseTooltip();\n\n\tstruct CollapsedGeometry {\n\t\tQRect geometry;\n\t\tfloat64 expanded = 0.;\n\t\tfloat64 singleWidth = 0.;\n\t};\n\t[[nodiscard]] CollapsedGeometry collapsedGeometryCurrent() const;\n\t[[nodiscard]] rpl::producer<> collapsedGeometryChanged() const;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn _empty.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> emptyValue() const {\n\t\treturn _empty.value();\n\t}\n\t[[nodiscard]] rpl::producer<uint64> clicks() const;\n\t[[nodiscard]] rpl::producer<ShowMenuRequest> showMenuRequests() const;\n\t[[nodiscard]] rpl::producer<bool> toggleExpandedRequests() const;\n\t//[[nodiscard]] rpl::producer<> entered() const;\n\t[[nodiscard]] rpl::producer<> loadMoreRequests() const;\n\n\t[[nodiscard]] auto verticalScrollEvents() const\n\t\t-> rpl::producer<not_null<QWheelEvent*>>;\n\n\t[[nodiscard]] bool toggledHidden() const;\n\tvoid setToggledHidden(bool hiddenInstant, bool hiddenAnimated);\n\nprivate:\n\tstruct Layout;\n\tenum class State {\n\t\tSmall,\n\t\tChanging,\n\t\tFull,\n\t};\n\tstruct Item {\n\t\tElement element;\n\t\tQImage nameCache;\n\t\tQColor nameCacheColor;\n\t\tstd::vector<Ui::OutlineSegment> segments;\n\t\tbool subscribed = false;\n\t};\n\tstruct Data {\n\t\tstd::vector<Item> items;\n\n\t\t[[nodiscard]] bool empty() const {\n\t\t\treturn items.empty();\n\t\t}\n\t};\n\n\tvoid showContent(Content &&content);\n\t//void enterEventHook(QEnterEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid wheelEvent(QWheelEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\n\tvoid paint(\n\t\tQPainter &p,\n\t\tconst Layout &layout,\n\t\tfloat64 photo,\n\t\tfloat64 line,\n\t\tbool layered);\n\tvoid ensureLayer();\n\tvoid validateThumbnail(not_null<Item*> item);\n\tvoid validateName(not_null<Item*> item);\n\tvoid updateScrollMax();\n\tvoid updateSelected();\n\tvoid checkDragging();\n\tbool finishDragging();\n\tvoid checkLoadMore();\n\tvoid requestExpanded(bool expanded);\n\n\tvoid updateTooltipGeometry();\n\t[[nodiscard]] TextWithEntities computeTooltipText() const;\n\tvoid toggleTooltip(bool fast);\n\n\tbool checkForFullState();\n\tvoid setState(State state);\n\tvoid updateGeometry();\n\t[[nodiscard]] QRect countSmallGeometry() const;\n\tvoid updateExpanding();\n\tvoid updateExpanding(int expandingHeight, int expandedHeight);\n\tvoid validateSegments(\n\t\tnot_null<Item*> item,\n\t\tconst QBrush &brush,\n\t\tfloat64 line,\n\t\tbool forUnread);\n\n\t[[nodiscard]] Layout computeLayout();\n\t[[nodiscard]] Layout computeLayout(float64 expanded) const;\n\n\tconst style::DialogsStoriesList &_st;\n\tContent _content;\n\tData _data;\n\trpl::event_stream<uint64> _clicks;\n\trpl::event_stream<ShowMenuRequest> _showMenuRequests;\n\trpl::event_stream<bool> _toggleExpandedRequests;\n\t//rpl::event_stream<> _entered;\n\trpl::event_stream<> _loadMoreRequests;\n\trpl::event_stream<> _collapsedGeometryChanged;\n\n\tQImage _layer;\n\tQPoint _positionSmall;\n\tstyle::align _alignSmall = {};\n\tQRect _geometryFull;\n\tQRect _changingGeometryFrom;\n\tState _state = State::Small;\n\trpl::variable<bool> _empty = true;\n\n\tQPoint _lastMousePosition;\n\tstd::optional<QPoint> _mouseDownPosition;\n\tint _startDraggingLeft = 0;\n\tint _scrollLeft = 0;\n\tint _scrollLeftMax = 0;\n\tbool _dragging = false;\n\tQt::Orientation _scrollingLock = {};\n\n\tUi::Animations::Simple _expandedAnimation;\n\tUi::Animations::Simple _expandCatchUpAnimation;\n\tUi::Animations::Simple _hiddenAnimation;\n\tfloat64 _lastRatio = 0.;\n\tint _lastExpandedHeight = 0;\n\tbool _hiddenAnimated : 1 = false;\n\tbool _hiddenInstant : 1 = false;\n\tbool _expandIgnored : 1 = false;\n\tbool _expanded : 1 = false;\n\n\tmutable CollapsedGeometry _lastCollapsedGeometry;\n\tmutable float64 _lastCollapsedRatio = 0.;\n\n\tint _selected = -1;\n\tint _pressed = -1;\n\n\trpl::event_stream<not_null<QWheelEvent*>> _verticalScrollEvents;\n\n\trpl::variable<TextWithEntities> _tooltipText;\n\trpl::variable<bool> _tooltipNotHidden;\n\tFn<void()> _tooltipHide;\n\tstd::unique_ptr<Ui::ImportantTooltip> _tooltip;\n\tbool _tooltipWindowActive = false;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\tbase::has_weak_ptr _menuGuard;\n\n};\n\n} // namespace Dialogs::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/ui/dialogs_suggestions.h\"\n\n#include \"api/api_chat_participants.h\"\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"boxes/choose_filter_box.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"core/application.h\"\n#include \"core/local_url_handlers.h\"\n#include \"core/ui_integration.h\"\n#include \"data/components/recent_peers.h\"\n#include \"data/components/top_peers.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_download_manager.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"dialogs/ui/chat_search_empty.h\"\n#include \"dialogs/ui/chat_search_in.h\"\n#include \"dialogs/ui/posts_search_intro.h\"\n#include \"dialogs/dialogs_inner_widget.h\"\n#include \"dialogs/dialogs_search_posts.h\"\n#include \"history/history.h\"\n#include \"info/downloads/info_downloads_widget.h\"\n#include \"info/media/info_media_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"info/info_wrap_widget.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/swipe_handler.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/custom_emoji_text_badge.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/discrete_sliders.h\"\n#include \"ui/widgets/elastic_scroll.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/delayed_activation.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/search_field_controller.h\"\n#include \"ui/unread_badge_paint.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_separate_id.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_peer_menu.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_window.h\"\n\nnamespace Dialogs {\nnamespace {\n\nconstexpr auto kCollapsedChannelsCount = 5;\nconstexpr auto kProbablyMaxChannels = 1000;\nconstexpr auto kCollapsedAppsCount = 5;\nconstexpr auto kProbablyMaxApps = 100;\nconstexpr auto kSearchQueryDelay = crl::time(900);\n\nclass RecentRow final : public PeerListRow {\npublic:\n\texplicit RecentRow(not_null<PeerData*> peer);\n\n\tbool refreshBadge();\n\n\tQSize rightActionSize() const override;\n\tQMargins rightActionMargins() const override;\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\tbool rightActionDisabled() const override;\n\tvoid rightActionAddRipple(\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) override;\n\tvoid rightActionStopLastRipple() override;\n\n\tconst style::PeerListItem &computeSt(\n\t\tconst style::PeerListItem &st) const override;\n\nprivate:\n\tconst not_null<History*> _history;\n\tstd::unique_ptr<Ui::Text::String> _mainAppText;\n\tstd::unique_ptr<Ui::RippleAnimation> _actionRipple;\n\tQString _badgeString;\n\tQSize _badgeSize;\n\tuint32 _counter : 30 = 0;\n\tuint32 _unread : 1 = 0;\n\tuint32 _muted : 1 = 0;\n\n};\n\nclass ChannelRow final : public PeerListRow {\npublic:\n\tusing PeerListRow::PeerListRow;\n\n\tvoid setActive(bool active);\n\n\tconst style::PeerListItem &computeSt(\n\t\tconst style::PeerListItem &st) const override;\n\nprivate:\n\tbool _active = false;\n\n};\n\nstruct EntryMenuDescriptor {\n\tnot_null<Window::SessionController*> controller;\n\tnot_null<PeerData*> peer;\n\tQString removeOneText;\n\tFn<void()> removeOne;\n\tQString removeAllText;\n\tQString removeAllConfirm;\n\tFn<void()> removeAll;\n\tFn<void()> closeCallback;\n};\n\n[[nodiscard]] Fn<void()> RemoveAllConfirm(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tQString removeAllConfirm,\n\t\tFn<void()> removeAll) {\n\treturn [=] {\n\t\tcontroller->show(Ui::MakeConfirmBox({\n\t\t\t.text = removeAllConfirm,\n\t\t\t.confirmed = [=](Fn<void()> close) { removeAll(); close(); }\n\t\t}));\n\t};\n}\n\nvoid FillEntryMenu(\n\t\tconst Ui::Menu::MenuCallback &add,\n\t\tEntryMenuDescriptor &&descriptor) {\n\tconst auto peer = descriptor.peer;\n\tconst auto controller = descriptor.controller;\n\tconst auto group = peer->isMegagroup();\n\tconst auto channel = peer->isChannel();\n\n\tadd(tr::lng_context_new_window(tr::now), [=] {\n\t\tUi::PreventDelayedActivation();\n\t\tcontroller->showInNewWindow(peer);\n\t\tif (descriptor.closeCallback) {\n\t\t\tdescriptor.closeCallback();\n\t\t}\n\t}, &st::menuIconNewWindow);\n\tWindow::AddSeparatorAndShiftUp(add);\n\n\tconst auto showHistoryText = group\n\t\t? tr::lng_context_open_group(tr::now)\n\t\t: channel\n\t\t? tr::lng_context_open_channel(tr::now)\n\t\t: tr::lng_profile_send_message(tr::now);\n\tadd(showHistoryText, [=] {\n\t\tcontroller->showPeerHistory(peer);\n\t}, channel ? &st::menuIconChannel : &st::menuIconChatBubble);\n\n\tconst auto history = peer->owner().historyLoaded(peer);\n\tif (history\n\t\t&& history->owner().chatsFilters().has()\n\t\t&& history->inChatList()) {\n\t\tadd(Ui::Menu::MenuCallback::Args{\n\t\t\t.text = tr::lng_filters_menu_add(tr::now),\n\t\t\t.handler = nullptr,\n\t\t\t.icon = &st::menuIconAddToFolder,\n\t\t\t.fillSubmenu = [&](not_null<Ui::PopupMenu*> menu) {\n\t\t\t\tFillChooseFilterMenu(controller, menu, history);\n\t\t\t},\n\t\t\t.submenuSt = &st::foldersMenu,\n\t\t});\n\t}\n\tconst auto viewProfileText = group\n\t\t? tr::lng_context_view_group(tr::now)\n\t\t: channel\n\t\t? tr::lng_context_view_channel(tr::now)\n\t\t: tr::lng_context_view_profile(tr::now);\n\tadd(viewProfileText, [=] {\n\t\tcontroller->showPeerInfo(peer);\n\t}, channel ? &st::menuIconInfo : &st::menuIconProfile);\n\n\tadd({ .separatorSt = &st::expandedMenuSeparator });\n\n\tadd({\n\t\t.text = descriptor.removeOneText,\n\t\t.handler = descriptor.removeOne,\n\t\t.icon = &st::menuIconDeleteAttention,\n\t\t.isAttention = true,\n\t});\n\tif (!descriptor.removeAllText.isEmpty()) {\n\t\tadd({\n\t\t\t.text = descriptor.removeAllText,\n\t\t\t.handler = RemoveAllConfirm(\n\t\t\t\tdescriptor.controller,\n\t\t\t\tdescriptor.removeAllConfirm,\n\t\t\t\tdescriptor.removeAll),\n\t\t\t.icon = &st::menuIconCancelAttention,\n\t\t\t.isAttention = true,\n\t\t});\n\t}\n}\n\nRecentRow::RecentRow(not_null<PeerData*> peer)\n: PeerListRow(peer)\n, _history(peer->owner().history(peer))\n, _mainAppText([&]() -> std::unique_ptr<Ui::Text::String> {\n\tif (const auto user = peer->asUser()) {\n\t\tif (user->botInfo && user->botInfo->hasMainApp) {\n\t\t\treturn std::make_unique<Ui::Text::String>(\n\t\t\t\tst::dialogRowOpenBotRecent.button.style,\n\t\t\t\ttr::lng_profile_open_app_short(tr::now));\n\t\t}\n\t}\n\treturn nullptr;\n}()) {\n\tif (peer->isSelf() || peer->isRepliesChat() || peer->isVerifyCodes()) {\n\t\tsetCustomStatus(u\" \"_q);\n\t} else if (const auto chat = peer->asChat()) {\n\t\tif (chat->count > 0) {\n\t\t\tsetCustomStatus(\n\t\t\t\ttr::lng_chat_status_members(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\tchat->count));\n\t\t}\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tif (channel->membersCountKnown()) {\n\t\t\tsetCustomStatus((channel->isBroadcast()\n\t\t\t\t? tr::lng_chat_status_subscribers\n\t\t\t\t: tr::lng_chat_status_members)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\tchannel->membersCount()));\n\t\t}\n\t}\n\trefreshBadge();\n}\n\nbool RecentRow::refreshBadge() {\n\tif (_history->peer->isSelf()) {\n\t\treturn false;\n\t}\n\tauto result = false;\n\tconst auto muted = _history->muted() ? 1 : 0;\n\tif (_muted != muted) {\n\t\t_muted = muted;\n\t\tif (_counter || _unread) {\n\t\t\tresult = true;\n\t\t}\n\t}\n\tconst auto badges = _history->chatListBadgesState();\n\tconst auto unread = badges.unread ? 1 : 0;\n\tif (_counter != badges.unreadCounter || _unread != unread) {\n\t\t_counter = badges.unreadCounter;\n\t\t_unread = unread;\n\t\tresult = true;\n\n\t\t_badgeString = !_counter\n\t\t\t? (_unread ? u\" \"_q : QString())\n\t\t\t: (_counter < 1000)\n\t\t\t? QString::number(_counter)\n\t\t\t: (QString::number(_counter / 1000) + 'K');\n\t\tif (_badgeString.isEmpty()) {\n\t\t\t_badgeSize = QSize();\n\t\t} else {\n\t\t\tauto st = Ui::UnreadBadgeStyle();\n\t\t\tconst auto unreadRectHeight = st.size;\n\t\t\tconst auto unreadWidth = st.font->width(_badgeString);\n\t\t\t_badgeSize = QSize(\n\t\t\t\tstd::max(unreadWidth + 2 * st.padding, unreadRectHeight),\n\t\t\t\tunreadRectHeight);\n\t\t}\n\t}\n\treturn result;\n}\n\nQSize RecentRow::rightActionSize() const {\n\tif (_mainAppText && _badgeSize.isEmpty()) {\n\t\treturn QSize(\n\t\t\t_mainAppText->maxWidth() + _mainAppText->minHeight(),\n\t\t\tst::dialogRowOpenBotRecent.button.height);\n\t}\n\treturn _badgeSize;\n}\n\nQMargins RecentRow::rightActionMargins() const {\n\tif (_mainAppText && _badgeSize.isEmpty()) {\n\t\tconst auto &st = st::dialogRowOpenBotRecent;\n\t\tauto margins = st.margin;\n\t\tmargins.setTop((st::recentPeersItem.height - st.button.height) / 2);\n\t\treturn margins;\n\t} else if (_badgeSize.isEmpty()) {\n\t\treturn {};\n\t}\n\tconst auto x = st::recentPeersItem.photoPosition.x();\n\tconst auto y = (st::recentPeersItem.height - _badgeSize.height()) / 2;\n\treturn QMargins(x, y, x, y);\n}\n\nvoid RecentRow::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\tif (_mainAppText && _badgeSize.isEmpty()) {\n\t\tconst auto size = RecentRow::rightActionSize();\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(actionSelected\n\t\t\t? st::activeButtonBgOver\n\t\t\t: st::activeButtonBg);\n\t\tconst auto radius = size.height() / 2;\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.drawRoundedRect(QRect(QPoint(x, y), size), radius, radius);\n\t\tif (_actionRipple) {\n\t\t\t_actionRipple->paint(p, x, y, outerWidth);\n\t\t\tif (_actionRipple->empty()) {\n\t\t\t\t_actionRipple.reset();\n\t\t\t}\n\t\t}\n\t\tp.setPen(actionSelected\n\t\t\t? st::activeButtonFgOver\n\t\t\t: st::activeButtonFg);\n\t\tconst auto top = st::dialogRowOpenBotRecent.button.textTop;\n\t\t_mainAppText->draw(p, {\n\t\t\t.position = QPoint(x + size.height() / 2, y + top),\n\t\t\t.outerWidth = outerWidth,\n\t\t\t.availableWidth = outerWidth,\n\t\t\t.elisionLines = 1,\n\t\t});\n\t}\n\tif (!_counter && !_unread) {\n\t\treturn;\n\t} else if (_badgeString.isEmpty()) {\n\t\t_badgeString = !_counter\n\t\t\t? u\" \"_q\n\t\t\t: (_counter < 1000)\n\t\t\t? QString::number(_counter)\n\t\t\t: (QString::number(_counter / 1000) + 'K');\n\t}\n\tauto st = Ui::UnreadBadgeStyle();\n\tst.selected = selected;\n\tst.muted = _muted;\n\tconst auto &counter = _badgeString;\n\tPaintUnreadBadge(p, counter, x + _badgeSize.width(), y, st);\n}\n\nbool RecentRow::rightActionDisabled() const {\n\treturn !_mainAppText || !_badgeSize.isEmpty();\n}\n\nvoid RecentRow::rightActionAddRipple(\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) {\n\tif (!_mainAppText || !_badgeSize.isEmpty()) {\n\t\treturn;\n\t}\n\tif (!_actionRipple) {\n\t\tconst auto size = rightActionSize();\n\t\tconst auto radius = size.height() / 2;\n\t\tauto mask = Ui::RippleAnimation::RoundRectMask(size, radius);\n\t\t_actionRipple = std::make_unique<Ui::RippleAnimation>(\n\t\t\tst::defaultActiveButton.ripple,\n\t\t\tstd::move(mask),\n\t\t\tstd::move(updateCallback));\n\t}\n\t_actionRipple->add(point);\n}\n\nvoid RecentRow::rightActionStopLastRipple() {\n\tif (_actionRipple) {\n\t\t_actionRipple->lastStop();\n\t}\n}\n\nconst style::PeerListItem &RecentRow::computeSt(\n\t\tconst style::PeerListItem &st) const {\n\treturn (peer()->isSelf()\n\t\t|| peer()->isRepliesChat()\n\t\t|| peer()->isVerifyCodes())\n\t\t? st::recentPeersSpecialName\n\t\t: st;\n}\n\nvoid ChannelRow::setActive(bool active) {\n\t_active = active;\n}\n\nconst style::PeerListItem &ChannelRow::computeSt(\n\t\tconst style::PeerListItem &st) const {\n\treturn _active ? st::recentPeersItemActive : st::recentPeersItem;\n}\n\n} // namespace\n\n\nclass Suggestions::ObjectListController\n\t: public PeerListController\n\t, public base::has_weak_ptr {\npublic:\n\texplicit ObjectListController(\n\t\tnot_null<Window::SessionController*> window);\n\n\t[[nodiscard]] not_null<Window::SessionController*> window() const {\n\t\treturn _window;\n\t}\n\t[[nodiscard]] rpl::producer<int> count() const {\n\t\treturn _count.value();\n\t}\n\t[[nodiscard]] rpl::producer<not_null<PeerData*>> chosen() const {\n\t\treturn _chosen.events();\n\t}\n\n\tvoid setCloseCallback(Fn<void()> callback) {\n\t\t_closeCallback = std::move(callback);\n\t}\n\n\tMain::Session &session() const override {\n\t\treturn _window->session();\n\t}\n\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowMiddleClicked(not_null<PeerListRow*> row) override;\n\tbool rowTrackPress(not_null<PeerListRow*> row) override;\n\tvoid rowTrackPressCancel() override;\n\tbool rowTrackPressSkipMouseSelection() override;\n\n\tbool processTouchEvent(not_null<QTouchEvent*> e);\n\tvoid setupTouchChatPreview(not_null<Ui::ElasticScroll*> scroll);\n\nprotected:\n\t[[nodiscard]] int countCurrent() const;\n\tvoid setCount(int count);\n\n\t[[nodiscard]] bool expandedCurrent() const;\n\t[[nodiscard]] rpl::producer<bool> expanded() const;\n\n\tvoid setupPlainDivider(rpl::producer<QString> title);\n\tvoid setupExpandDivider(rpl::producer<QString> title);\n\n\tFn<void()> _closeCallback;\n\nprivate:\n\tconst not_null<Window::SessionController*> _window;\n\n\tstd::optional<QPoint> _chatPreviewTouchGlobal;\n\trpl::event_stream<> _touchCancelRequests;\n\trpl::event_stream<not_null<PeerData*>> _chosen;\n\trpl::variable<int> _count;\n\trpl::variable<Ui::RpWidget*> _toggleExpanded = nullptr;\n\trpl::variable<bool> _expanded = false;\n\n};\n\nclass RecentsController final : public Suggestions::ObjectListController {\npublic:\n\tusing RightActionCallback = Fn<void(not_null<PeerData*>)>;\n\n\tRecentsController(\n\t\tnot_null<Window::SessionController*> window,\n\t\tRecentPeersList list,\n\t\tRightActionCallback rightActionCallback,\n\t\tFn<void()> closeCallback);\n\n\tvoid prepare() override;\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override;\n\n\tQString savedMessagesChatStatus() const override;\n\nprivate:\n\tvoid setupDivider();\n\tvoid subscribeToEvents();\n\t[[nodiscard]] Fn<void()> removeAllCallback();\n\n\tRecentPeersList _recent;\n\tRightActionCallback _rightActionCallback;\n\trpl::lifetime _lifetime;\n\n};\n\nclass MyChannelsController final\n\t: public Suggestions::ObjectListController {\npublic:\n\texplicit MyChannelsController(\n\t\tnot_null<Window::SessionController*> window);\n\n\tvoid prepare() override;\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\nprivate:\n\tvoid appendRow(not_null<ChannelData*> channel);\n\tvoid fill(bool force = false);\n\n\tstd::vector<not_null<History*>> _channels;\n\trpl::lifetime _lifetime;\n\n};\n\nclass RecommendationsController final\n\t: public Suggestions::ObjectListController {\npublic:\n\texplicit RecommendationsController(\n\t\tnot_null<Window::SessionController*> window);\n\n\tvoid prepare() override;\n\n\tvoid load();\n\nprivate:\n\tvoid fill();\n\tvoid appendRow(not_null<ChannelData*> channel);\n\n\tHistory *_activeHistory = nullptr;\n\tbool _requested = false;\n\trpl::lifetime _lifetime;\n\n};\n\nclass RecentAppsController final\n\t: public Suggestions::ObjectListController {\npublic:\n\texplicit RecentAppsController(\n\t\tnot_null<Window::SessionController*> window);\n\n\tvoid prepare() override;\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\n\tvoid load();\n\n\t[[nodiscard]] rpl::producer<> refreshed() const;\n\t[[nodiscard]] bool shown(not_null<PeerData*> peer) const;\n\nprivate:\n\tvoid appendRow(not_null<UserData*> bot);\n\tvoid fill();\n\n\tstd::vector<not_null<UserData*>> _bots;\n\trpl::event_stream<> _refreshed;\n\trpl::lifetime _lifetime;\n\n};\n\nclass PopularAppsController final\n\t: public Suggestions::ObjectListController {\npublic:\n\tPopularAppsController(\n\t\tnot_null<Window::SessionController*> window,\n\t\tFn<bool(not_null<PeerData*>)> filterOut,\n\t\trpl::producer<> filterOutRefreshes);\n\n\tvoid prepare() override;\n\n\tvoid load();\n\nprivate:\n\tvoid fill();\n\tvoid appendRow(not_null<UserData*> bot);\n\n\tFn<bool(not_null<PeerData*>)> _filterOut;\n\trpl::producer<> _filterOutRefreshes;\n\tbool _requested = false;\n\trpl::lifetime _lifetime;\n\n};\n\nSuggestions::ObjectListController::ObjectListController(\n\tnot_null<Window::SessionController*> window)\n: _window(window) {\n}\n\nbool Suggestions::ObjectListController::rowTrackPress(\n\t\tnot_null<PeerListRow*> row) {\n\tconst auto peer = row->peer();\n\tconst auto history = peer->owner().history(peer);\n\tconst auto callback = crl::guard(this, [=](bool shown) {\n\t\tdelegate()->peerListPressLeftToContextMenu(shown);\n\t});\n\tif (base::IsAltPressed()) {\n\t\t_window->showChatPreview(\n\t\t\t{ history, FullMsgId() },\n\t\t\tcallback,\n\t\t\tnullptr,\n\t\t\t_chatPreviewTouchGlobal);\n\t\treturn false;\n\t}\n\tconst auto point = delegate()->peerListLastRowMousePosition();\n\tconst auto &st = computeListSt().item;\n\tif (point && point->x() < st.photoPosition.x() + st.photoSize) {\n\t\t_window->scheduleChatPreview(\n\t\t\t{ history, FullMsgId() },\n\t\t\tcallback,\n\t\t\tnullptr,\n\t\t\t_chatPreviewTouchGlobal);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Suggestions::ObjectListController::rowTrackPressCancel() {\n\t_chatPreviewTouchGlobal = {};\n\t_window->cancelScheduledPreview();\n}\n\nbool Suggestions::ObjectListController::rowTrackPressSkipMouseSelection() {\n\treturn _chatPreviewTouchGlobal.has_value();\n}\n\nbool Suggestions::ObjectListController::processTouchEvent(\n\t\tnot_null<QTouchEvent*> e) {\n\tconst auto point = e->touchPoints().empty()\n\t\t? std::optional<QPoint>()\n\t\t: e->touchPoints().front().screenPos().toPoint();\n\tswitch (e->type()) {\n\tcase QEvent::TouchBegin: {\n\t\tif (!point) {\n\t\t\treturn false;\n\t\t}\n\t\t_chatPreviewTouchGlobal = point;\n\t\tif (!delegate()->peerListTrackRowPressFromGlobal(*point)) {\n\t\t\t_chatPreviewTouchGlobal = {};\n\t\t}\n\t} break;\n\n\tcase QEvent::TouchUpdate: {\n\t\tif (!point) {\n\t\t\treturn false;\n\t\t}\n\t\tif (_chatPreviewTouchGlobal) {\n\t\t\tconst auto delta = (*_chatPreviewTouchGlobal - *point);\n\t\t\tif (delta.manhattanLength() > computeListSt().item.photoSize) {\n\t\t\t\trowTrackPressCancel();\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase QEvent::TouchEnd:\n\tcase QEvent::TouchCancel: {\n\t\tif (_chatPreviewTouchGlobal) {\n\t\t\trowTrackPressCancel();\n\t\t}\n\t} break;\n\t}\n\treturn false;\n}\n\nvoid Suggestions::ObjectListController::setupTouchChatPreview(\n\t\tnot_null<Ui::ElasticScroll*> scroll) {\n\t_touchCancelRequests.events() | rpl::on_next([=] {\n\t\tQTouchEvent ev(QEvent::TouchCancel);\n\t\tev.setTimestamp(crl::now());\n\t\tQGuiApplication::sendEvent(scroll, &ev);\n\t}, lifetime());\n}\n\nint Suggestions::ObjectListController::countCurrent() const {\n\treturn _count.current();\n}\n\nvoid Suggestions::ObjectListController::setCount(int count) {\n\t_count = count;\n}\n\nbool Suggestions::ObjectListController::expandedCurrent() const {\n\treturn _expanded.current();\n}\n\nrpl::producer<bool> Suggestions::ObjectListController::expanded() const {\n\treturn _expanded.value();\n}\n\nvoid Suggestions::ObjectListController::rowClicked(\n\t\tnot_null<PeerListRow*> row) {\n\t_chosen.fire(row->peer());\n}\n\nvoid Suggestions::ObjectListController::rowMiddleClicked(\n\t\tnot_null<PeerListRow*> row) {\n\twindow()->showInNewWindow(row->peer());\n\tif (_closeCallback) {\n\t\t_closeCallback();\n\t}\n}\n\nvoid Suggestions::ObjectListController::setupPlainDivider(\n\t\trpl::producer<QString> title) {\n\tauto result = object_ptr<Ui::FixedHeightWidget>(\n\t\t(QWidget*)nullptr,\n\t\tst::searchedBarHeight);\n\tconst auto raw = result.data();\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\tstd::move(title),\n\t\tst::searchedBarLabel);\n\traw->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tconst auto x = st::searchedBarPosition.x();\n\t\tconst auto y = st::searchedBarPosition.y();\n\t\tlabel->resizeToWidth(size.width() - x * 2);\n\t\tlabel->moveToLeft(x, y, size.width());\n\t}, raw->lifetime());\n\traw->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tQPainter(raw).fillRect(clip, st::searchedBarBg);\n\t}, raw->lifetime());\n\n\tdelegate()->peerListSetAboveWidget(std::move(result));\n}\n\nvoid Suggestions::ObjectListController::setupExpandDivider(\n\t\trpl::producer<QString> title) {\n\tauto result = object_ptr<Ui::FixedHeightWidget>(\n\t\t(QWidget*)nullptr,\n\t\tst::searchedBarHeight);\n\tconst auto raw = result.data();\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\tstd::move(title),\n\t\tst::searchedBarLabel);\n\tcount(\n\t) | rpl::map(\n\t\trpl::mappers::_1 > kCollapsedChannelsCount\n\t) | rpl::distinct_until_changed() | rpl::on_next([=](bool more) {\n\t\t_expanded = false;\n\t\tif (!more) {\n\t\t\tconst auto toggle = _toggleExpanded.current();\n\t\t\t_toggleExpanded = nullptr;\n\t\t\tdelete toggle;\n\t\t\treturn;\n\t\t} else if (_toggleExpanded.current()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto toggle = Ui::CreateChild<Ui::LinkButton>(\n\t\t\traw,\n\t\t\ttr::lng_channels_your_more(tr::now),\n\t\t\tst::searchedBarLink);\n\t\ttoggle->show();\n\t\ttoggle->setClickedCallback([=] {\n\t\t\tconst auto expand = !_expanded.current();\n\t\t\ttoggle->setText(expand\n\t\t\t\t? tr::lng_channels_your_less(tr::now)\n\t\t\t\t: tr::lng_channels_your_more(tr::now));\n\t\t\t_expanded = expand;\n\t\t});\n\t\trpl::combine(\n\t\t\traw->sizeValue(),\n\t\t\ttoggle->widthValue()\n\t\t) | rpl::on_next([=](QSize size, int width) {\n\t\t\tconst auto x = st::searchedBarPosition.x();\n\t\t\tconst auto y = st::searchedBarPosition.y();\n\t\t\ttoggle->moveToRight(0, 0, size.width());\n\t\t\tlabel->resizeToWidth(size.width() - x - width);\n\t\t\tlabel->moveToLeft(x, y, size.width());\n\t\t}, toggle->lifetime());\n\t\t_toggleExpanded = toggle;\n\t}, raw->lifetime());\n\n\trpl::combine(\n\t\traw->sizeValue(),\n\t\t_toggleExpanded.value()\n\t) | rpl::filter(\n\t\trpl::mappers::_2 == nullptr\n\t) | rpl::on_next([=](QSize size, const auto) {\n\t\tconst auto x = st::searchedBarPosition.x();\n\t\tconst auto y = st::searchedBarPosition.y();\n\t\tlabel->resizeToWidth(size.width() - x * 2);\n\t\tlabel->moveToLeft(x, y, size.width());\n\t}, raw->lifetime());\n\n\traw->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tQPainter(raw).fillRect(clip, st::searchedBarBg);\n\t}, raw->lifetime());\n\n\tdelegate()->peerListSetAboveWidget(std::move(result));\n}\n\nRecentsController::RecentsController(\n\tnot_null<Window::SessionController*> window,\n\tRecentPeersList list,\n\tRightActionCallback rightActionCallback,\n\tFn<void()> closeCallback)\n: ObjectListController(window)\n, _recent(std::move(list))\n, _rightActionCallback(std::move(rightActionCallback)) {\n\t_closeCallback = std::move(closeCallback);\n}\n\nvoid RecentsController::prepare() {\n\tsetupDivider();\n\n\tfor (const auto &peer : _recent.list) {\n\t\tdelegate()->peerListAppendRow(std::make_unique<RecentRow>(peer));\n\t}\n\tdelegate()->peerListRefreshRows();\n\tsetCount(_recent.list.size());\n\n\tsubscribeToEvents();\n}\n\nFn<void()> RecentsController::removeAllCallback() {\n\tconst auto weak = base::make_weak(this);\n\tconst auto session = &this->session();\n\treturn crl::guard(session, [=] {\n\t\tif (weak) {\n\t\t\tsetCount(0);\n\t\t\twhile (delegate()->peerListFullRowsCount() > 0) {\n\t\t\t\tdelegate()->peerListRemoveRow(delegate()->peerListRowAt(0));\n\t\t\t}\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t\tsession->recentPeers().clear();\n\t});\n}\n\nbase::unique_qptr<Ui::PopupMenu> RecentsController::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\tconst auto peer = row->peer();\n\tconst auto weak = base::make_weak(this);\n\tconst auto session = &this->session();\n\tconst auto removeOne = crl::guard(session, [=] {\n\t\tif (weak) {\n\t\t\tconst auto rowId = peer->id.value;\n\t\t\tif (const auto row = delegate()->peerListFindRow(rowId)) {\n\t\t\t\tsetCount(std::max(0, countCurrent() - 1));\n\t\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t\t\tdelegate()->peerListRefreshRows();\n\t\t\t}\n\t\t}\n\t\tsession->recentPeers().remove(peer);\n\t});\n\tFillEntryMenu(Ui::Menu::CreateAddActionCallback(result), {\n\t\t.controller = window(),\n\t\t.peer = peer,\n\t\t.removeOneText = tr::lng_recent_remove(tr::now),\n\t\t.removeOne = removeOne,\n\t\t.removeAllText = tr::lng_recent_clear_all(tr::now),\n\t\t.removeAllConfirm = tr::lng_recent_clear_sure(tr::now),\n\t\t.removeAll = removeAllCallback(),\n\t\t.closeCallback = crl::guard(this, [=] {\n\t\t\tif (_closeCallback) {\n\t\t\t\t_closeCallback();\n\t\t\t}\n\t\t}),\n\t});\n\treturn result;\n}\n\nvoid RecentsController::rowRightActionClicked(not_null<PeerListRow*> row) {\n\tif (_rightActionCallback) {\n\t\tif (const auto peer = row->peer()) {\n\t\t\t_rightActionCallback(peer);\n\t\t}\n\t}\n}\n\nQString RecentsController::savedMessagesChatStatus() const {\n\treturn tr::lng_saved_forward_here(tr::now);\n}\n\nvoid RecentsController::setupDivider() {\n\tauto result = object_ptr<Ui::FixedHeightWidget>(\n\t\t(QWidget*)nullptr,\n\t\tst::searchedBarHeight);\n\tconst auto raw = result.data();\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\ttr::lng_recent_title(),\n\t\tst::searchedBarLabel);\n\tconst auto clear = Ui::CreateChild<Ui::LinkButton>(\n\t\traw,\n\t\ttr::lng_recent_clear(tr::now),\n\t\tst::searchedBarLink);\n\tclear->setClickedCallback(RemoveAllConfirm(\n\t\twindow(),\n\t\ttr::lng_recent_clear_sure(tr::now),\n\t\tremoveAllCallback()));\n\trpl::combine(\n\t\traw->sizeValue(),\n\t\tclear->widthValue()\n\t) | rpl::on_next([=](QSize size, int width) {\n\t\tconst auto x = st::searchedBarPosition.x();\n\t\tconst auto y = st::searchedBarPosition.y();\n\t\tclear->moveToRight(0, 0, size.width());\n\t\tlabel->resizeToWidth(size.width() - x - width);\n\t\tlabel->moveToLeft(x, y, size.width());\n\t}, raw->lifetime());\n\traw->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tQPainter(raw).fillRect(clip, st::searchedBarBg);\n\t}, raw->lifetime());\n\n\tdelegate()->peerListSetAboveWidget(std::move(result));\n}\n\nvoid RecentsController::subscribeToEvents() {\n\tusing Flag = Data::PeerUpdate::Flag;\n\tsession().changes().peerUpdates(\n\t\tFlag::Notifications\n\t\t| Flag::OnlineStatus\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tconst auto peer = update.peer;\n\t\tif (peer->isSelf()) {\n\t\t\treturn;\n\t\t}\n\t\tauto refreshed = false;\n\t\tconst auto row = delegate()->peerListFindRow(update.peer->id.value);\n\t\tif (!row) {\n\t\t\treturn;\n\t\t} else if (update.flags & Flag::Notifications) {\n\t\t\trefreshed = static_cast<RecentRow*>(row)->refreshBadge();\n\t\t}\n\t\tif (!peer->isRepliesChat()\n\t\t\t&& !peer->isVerifyCodes()\n\t\t\t&& (update.flags & Flag::OnlineStatus)) {\n\t\t\trow->clearCustomStatus();\n\t\t\trefreshed = true;\n\t\t}\n\t\tif (refreshed) {\n\t\t\tdelegate()->peerListUpdateRow(row);\n\t\t}\n\t}, _lifetime);\n\n\tsession().data().unreadBadgeChanges(\n\t) | rpl::on_next([=] {\n\t\tfor (auto i = 0; i != countCurrent(); ++i) {\n\t\t\tconst auto row = delegate()->peerListRowAt(i);\n\t\t\tif (static_cast<RecentRow*>(row.get())->refreshBadge()) {\n\t\t\t\tdelegate()->peerListUpdateRow(row);\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n}\n\nMyChannelsController::MyChannelsController(\n\tnot_null<Window::SessionController*> window)\n: ObjectListController(window) {\n}\n\nvoid MyChannelsController::prepare() {\n\tsetupExpandDivider(tr::lng_channels_your_title());\n\n\tsession().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::ChannelAmIn\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tconst auto channel = update.peer->asBroadcast();\n\t\tif (!channel || channel->amIn()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto history = channel->owner().history(channel);\n\t\tconst auto i = ranges::remove(_channels, history);\n\t\tif (i == end(_channels)) {\n\t\t\treturn;\n\t\t}\n\t\t_channels.erase(i, end(_channels));\n\t\tconst auto row = delegate()->peerListFindRow(channel->id.value);\n\t\tif (row) {\n\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t}\n\t\tsetCount(_channels.size());\n\t\tfill(true);\n\t}, _lifetime);\n\n\t_channels.reserve(kProbablyMaxChannels);\n\tconst auto owner = &session().data();\n\tconst auto add = [&](not_null<Dialogs::MainList*> list) {\n\t\tfor (const auto &row : list->indexed()->all()) {\n\t\t\tif (const auto history = row->history()) {\n\t\t\t\tif (history->peer->isBroadcast()) {\n\t\t\t\t\t_channels.push_back(history);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\tadd(owner->chatsList());\n\tif (const auto folder = owner->folderLoaded(Data::Folder::kId)) {\n\t\tadd(owner->chatsList(folder));\n\t}\n\n\tranges::sort(_channels, ranges::greater(), &History::chatListTimeId);\n\tsetCount(_channels.size());\n\n\texpanded() | rpl::on_next([=] {\n\t\tfill();\n\t}, _lifetime);\n\n\tauto loading = owner->chatsListChanges(\n\t) | rpl::take_while([=](Data::Folder *folder) {\n\t\treturn !owner->chatsListLoaded(folder);\n\t});\n\trpl::merge(\n\t\tstd::move(loading),\n\t\towner->chatsListLoadedEvents()\n\t) | rpl::on_next([=](Data::Folder *folder) {\n\t\tconst auto list = owner->chatsList(folder);\n\t\tfor (const auto &row : list->indexed()->all()) {\n\t\t\tif (const auto history = row->history()) {\n\t\t\t\tif (history->peer->isBroadcast()) {\n\t\t\t\t\tif (ranges::contains(_channels, not_null(history))) {\n\t\t\t\t\t\t_channels.push_back(history);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst auto was = countCurrent();\n\t\tconst auto now = int(_channels.size());\n\t\tif (was != now) {\n\t\t\tsetCount(now);\n\t\t\tfill();\n\t\t}\n\t}, _lifetime);\n}\n\nvoid MyChannelsController::fill(bool force) {\n\tconst auto count = countCurrent();\n\tconst auto limit = expandedCurrent()\n\t\t? count\n\t\t: std::min(count, kCollapsedChannelsCount);\n\tconst auto already = delegate()->peerListFullRowsCount();\n\tconst auto delta = limit - already;\n\tif (!delta && !force) {\n\t\treturn;\n\t} else if (delta > 0) {\n\t\tfor (auto i = already; i != limit; ++i) {\n\t\t\tappendRow(_channels[i]->peer->asBroadcast());\n\t\t}\n\t} else if (delta < 0) {\n\t\tfor (auto i = already; i != limit;) {\n\t\t\tdelegate()->peerListRemoveRow(delegate()->peerListRowAt(--i));\n\t\t}\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid MyChannelsController::appendRow(not_null<ChannelData*> channel) {\n\tauto row = std::make_unique<PeerListRow>(channel);\n\tif (channel->membersCountKnown()) {\n\t\trow->setCustomStatus((channel->isBroadcast()\n\t\t\t? tr::lng_chat_status_subscribers\n\t\t\t: tr::lng_chat_status_members)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\tchannel->membersCount()));\n\t}\n\tdelegate()->peerListAppendRow(std::move(row));\n}\n\nbase::unique_qptr<Ui::PopupMenu> MyChannelsController::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\tconst auto peer = row->peer();\n\tconst auto addAction = Ui::Menu::CreateAddActionCallback(result);\n\tWindow::FillDialogsEntryMenu(\n\t\twindow(),\n\t\tDialogs::EntryState{\n\t\t\t.key = peer->owner().history(peer),\n\t\t\t.section = Dialogs::EntryState::Section::ContextMenu,\n\t\t},\n\t\taddAction);\n\treturn result;\n}\n\nRecommendationsController::RecommendationsController(\n\tnot_null<Window::SessionController*> window)\n: ObjectListController(window) {\n}\n\nvoid RecommendationsController::prepare() {\n\tsetupPlainDivider(tr::lng_channels_recommended());\n\tfill();\n}\n\nvoid RecommendationsController::load() {\n\tif (_requested || countCurrent()) {\n\t\treturn;\n\t}\n\t_requested = true;\n\tconst auto participants = &session().api().chatParticipants();\n\tparticipants->loadRecommendations();\n\tparticipants->recommendationsLoaded(\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\tfill();\n\t}, _lifetime);\n}\n\nvoid RecommendationsController::fill() {\n\tconst auto participants = &session().api().chatParticipants();\n\tconst auto &list = participants->recommendations().list;\n\tif (list.empty()) {\n\t\treturn;\n\t}\n\tfor (const auto &peer : list) {\n\t\tif (const auto channel = peer->asBroadcast()) {\n\t\t\tappendRow(channel);\n\t\t}\n\t}\n\tdelegate()->peerListRefreshRows();\n\tsetCount(delegate()->peerListFullRowsCount());\n\n\twindow()->activeChatValue() | rpl::on_next([=](const Key &key) {\n\t\tconst auto history = key.history();\n\t\tif (_activeHistory == history) {\n\t\t\treturn;\n\t\t} else if (_activeHistory) {\n\t\t\tconst auto id = _activeHistory->peer->id.value;\n\t\t\tif (const auto row = delegate()->peerListFindRow(id)) {\n\t\t\t\tstatic_cast<ChannelRow*>(row)->setActive(false);\n\t\t\t\tdelegate()->peerListUpdateRow(row);\n\t\t\t}\n\t\t}\n\t\t_activeHistory = history;\n\t\tif (_activeHistory) {\n\t\t\tconst auto id = _activeHistory->peer->id.value;\n\t\t\tif (const auto row = delegate()->peerListFindRow(id)) {\n\t\t\t\tstatic_cast<ChannelRow*>(row)->setActive(true);\n\t\t\t\tdelegate()->peerListUpdateRow(row);\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n}\n\nvoid RecommendationsController::appendRow(not_null<ChannelData*> channel) {\n\tauto row = std::make_unique<ChannelRow>(channel);\n\tif (channel->membersCountKnown()) {\n\t\trow->setCustomStatus((channel->isBroadcast()\n\t\t\t? tr::lng_chat_status_subscribers\n\t\t\t: tr::lng_chat_status_members)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\tchannel->membersCount()));\n\t}\n\tdelegate()->peerListAppendRow(std::move(row));\n}\n\nRecentAppsController::RecentAppsController(\n\tnot_null<Window::SessionController*> window)\n: ObjectListController(window) {\n}\n\nvoid RecentAppsController::prepare() {\n\tsetupExpandDivider(tr::lng_bot_apps_your());\n\n\t_bots.reserve(kProbablyMaxApps);\n\trpl::single() | rpl::then(\n\t\tsession().topBotApps().updates()\n\t) | rpl::on_next([=] {\n\t\t_bots.clear();\n\t\tfor (const auto &peer : session().topBotApps().list()) {\n\t\t\tif (const auto bot = peer->asUser()) {\n\t\t\t\tif (bot->isBot() && !bot->isInaccessible()) {\n\t\t\t\t\t_bots.push_back(bot);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tsetCount(_bots.size());\n\t\twhile (delegate()->peerListFullRowsCount()) {\n\t\t\tdelegate()->peerListRemoveRow(delegate()->peerListRowAt(0));\n\t\t}\n\t\tfill();\n\t}, _lifetime);\n\n\texpanded() | rpl::skip(1) | rpl::on_next([=] {\n\t\tfill();\n\t}, _lifetime);\n}\n\nbase::unique_qptr<Ui::PopupMenu> RecentAppsController::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\tconst auto peer = row->peer();\n\tconst auto weak = base::make_weak(this);\n\tconst auto session = &this->session();\n\tconst auto removeOne = crl::guard(session, [=] {\n\t\tif (weak) {\n\t\t\tconst auto rowId = peer->id.value;\n\t\t\tif (const auto row = delegate()->peerListFindRow(rowId)) {\n\t\t\t\tsetCount(std::max(0, countCurrent() - 1));\n\t\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t\t\tdelegate()->peerListRefreshRows();\n\t\t\t}\n\t\t}\n\t\tsession->topBotApps().remove(peer);\n\t});\n\tFillEntryMenu(Ui::Menu::CreateAddActionCallback(result), {\n\t\t.controller = window(),\n\t\t.peer = peer,\n\t\t.removeOneText = tr::lng_recent_remove(tr::now),\n\t\t.removeOne = removeOne,\n\t\t.closeCallback = crl::guard(this, [=] {\n\t\t\tif (_closeCallback) {\n\t\t\t\t_closeCallback();\n\t\t\t}\n\t\t}),\n\t});\n\treturn result;\n}\n\nvoid RecentAppsController::load() {\n\tsession().topBotApps().reload();\n}\n\nrpl::producer<> RecentAppsController::refreshed() const {\n\treturn _refreshed.events();\n}\n\nbool RecentAppsController::shown(not_null<PeerData*> peer) const {\n\treturn delegate()->peerListFindRow(peer->id.value) != nullptr;\n}\n\nvoid RecentAppsController::fill() {\n\tconst auto count = countCurrent();\n\tconst auto limit = expandedCurrent()\n\t\t? count\n\t\t: std::min(count, kCollapsedAppsCount);\n\tconst auto already = delegate()->peerListFullRowsCount();\n\tconst auto delta = limit - already;\n\tif (!delta) {\n\t\treturn;\n\t} else if (delta > 0) {\n\t\tfor (auto i = already; i != limit; ++i) {\n\t\t\tappendRow(_bots[i]);\n\t\t}\n\t} else if (delta < 0) {\n\t\tfor (auto i = already; i != limit;) {\n\t\t\tdelegate()->peerListRemoveRow(delegate()->peerListRowAt(--i));\n\t\t}\n\t}\n\tdelegate()->peerListRefreshRows();\n\n\t_refreshed.fire({});\n}\n\nvoid RecentAppsController::appendRow(not_null<UserData*> bot) {\n\tauto row = std::make_unique<PeerListRow>(bot);\n\tif (const auto count = bot->botInfo->activeUsers) {\n\t\trow->setCustomStatus(\n\t\t\ttr::lng_bot_status_users(tr::now, lt_count_decimal, count));\n\t}\n\tdelegate()->peerListAppendRow(std::move(row));\n}\n\nPopularAppsController::PopularAppsController(\n\tnot_null<Window::SessionController*> window,\n\tFn<bool(not_null<PeerData*>)> filterOut,\n\trpl::producer<> filterOutRefreshes)\n: ObjectListController(window)\n, _filterOut(std::move(filterOut))\n, _filterOutRefreshes(std::move(filterOutRefreshes)) {\n}\n\nvoid PopularAppsController::prepare() {\n\tif (_filterOut) {\n\t\tsetupPlainDivider(tr::lng_bot_apps_popular());\n\t}\n\trpl::single() | rpl::then(\n\t\tstd::move(_filterOutRefreshes)\n\t) | rpl::on_next([=] {\n\t\tfill();\n\t}, _lifetime);\n}\n\nvoid PopularAppsController::load() {\n\tif (_requested || countCurrent()) {\n\t\treturn;\n\t}\n\t_requested = true;\n\tconst auto attachWebView = &session().attachWebView();\n\tattachWebView->loadPopularAppBots();\n\tattachWebView->popularAppBotsLoaded(\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\tfill();\n\t}, _lifetime);\n}\n\nvoid PopularAppsController::fill() {\n\twhile (delegate()->peerListFullRowsCount()) {\n\t\tdelegate()->peerListRemoveRow(delegate()->peerListRowAt(0));\n\t}\n\tfor (const auto &bot : session().attachWebView().popularAppBots()) {\n\t\tif (!_filterOut || !_filterOut(bot)) {\n\t\t\tappendRow(bot);\n\t\t}\n\t}\n\tconst auto count = delegate()->peerListFullRowsCount();\n\tsetCount(count);\n\tif (count > 0) {\n\t\tdelegate()->peerListSetBelowWidget(object_ptr<Ui::DividerLabel>(\n\t\t\t(QWidget*)nullptr,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t(QWidget*)nullptr,\n\t\t\t\ttr::lng_bot_apps_which(\n\t\t\t\t\tlt_link,\n\t\t\t\t\ttr::lng_bot_apps_which_link(\n\t\t\t\t\t\ttr::url(u\"internal:about_popular_apps\"_q)),\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::dialogsPopularAppsAbout),\n\t\t\tst::dialogsPopularAppsPadding));\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid PopularAppsController::appendRow(not_null<UserData*> bot) {\n\tauto row = std::make_unique<PeerListRow>(bot);\n\tif (bot->isBot()) {\n\t\tif (!bot->botInfo->activeUsers && !bot->username().isEmpty()) {\n\t\t\trow->setCustomStatus('@' + bot->username());\n\t\t}\n\t}\n\tdelegate()->peerListAppendRow(std::move(row));\n}\n\nSuggestions::Suggestions(\n\tnot_null<QWidget*> parent,\n\tnot_null<Window::SessionController*> controller,\n\trpl::producer<TopPeersList> topPeers,\n\tRecentPeersList recentPeers)\n: RpWidget(parent)\n, _controller(controller)\n, _tabsScroll(\n\tstd::make_unique<Ui::ScrollArea>(this, st::dialogsTabsScroll, true))\n, _tabs(\n\t_tabsScroll->setOwnedWidget(\n\t\tobject_ptr<Ui::SettingsSlider>(this, st::dialogsSearchTabs)))\n, _tabKeys(TabKeysFor(controller))\n, _chatsScroll(std::make_unique<Ui::ElasticScroll>(this))\n, _chatsContent(\n\t_chatsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))\n, _topPeersWrap(\n\t_chatsContent->add(object_ptr<Ui::SlideWrap<TopPeersStrip>>(\n\t\tthis,\n\t\tobject_ptr<TopPeersStrip>(this, std::move(topPeers)))))\n, _topPeers(_topPeersWrap->entity())\n, _recent(setupRecentPeers(std::move(recentPeers)))\n, _emptyRecent(_chatsContent->add(setupEmptyRecent()))\n, _channelsScroll(std::make_unique<Ui::ElasticScroll>(this))\n, _channelsContent(\n\t_channelsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))\n, _myChannels(setupMyChannels())\n, _recommendations(setupRecommendations())\n, _emptyChannels(_channelsContent->add(setupEmptyChannels()))\n, _appsScroll(std::make_unique<Ui::ElasticScroll>(this))\n, _appsContent(\n\t_appsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))\n, _postsScroll(std::make_unique<Ui::ElasticScroll>(this))\n, _postsWrap(_postsScroll->setOwnedWidget(object_ptr<Ui::RpWidget>(this)))\n, _recentApps(setupRecentApps())\n, _popularApps(setupPopularApps())\n, _searchQueryTimer([=] { applySearchQuery(); }) {\n\n\tsetupLocalUrlButton();\n\tsetupTabs();\n\tsetupChats();\n\tsetupChannels();\n\tsetupApps();\n}\n\nSuggestions::~Suggestions() = default;\n\nvoid Suggestions::setupLocalUrlButton() {\n\tconst auto urlFromClipboard = [=] {\n\t\tconst auto maybeUrl = QGuiApplication::clipboard()->text();\n\t\tconst auto local = Core::TryConvertUrlToLocal(maybeUrl);\n\n\t\tconst auto protocol = u\"tg://\"_q;\n\t\tif (local.startsWith(protocol)) {\n\t\t\treturn local;\n\t\t}\n\t\treturn QString();\n\t};\n\tconst auto url = urlFromClipboard();\n\tif (url.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto &st = st::defaultSettingsButton;\n\t_localUrlButton = base::unique_qptr<Ui::AbstractButton>(\n\t\tUi::CreateSimpleSettingsButton(this, st.ripple, st.textBg));\n\t_localUrlButton->resize(width(), st.height + rect::m::sum::v(st.padding));\n\tconst auto text = lifetime().make_state<Ui::Text::String>(\n\t\tst::semiboldTextStyle,\n\t\ttr::lng_animated_emoji_saved_open(tr::now) + QChar(' ') + url);\n\t_localUrlButton->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(_localUrlButton.get());\n\t\tconst auto r = _localUrlButton->rect();\n\t\tconst auto left = st::searchedBarPosition.x();\n\t\tp.setPen(st::windowBgActive);\n\t\ttext->draw(p, {\n\t\t\t.position = QPoint(left, (r.height() - text->minHeight()) / 2),\n\t\t\t.outerWidth = (r.width() - left),\n\t\t\t.availableWidth = (r.width() - left),\n\t\t\t.elisionLines = 1,\n\t\t});\n\t}, _localUrlButton->lifetime());\n\twidthValue() | rpl::on_next([=](int width) {\n\t\t_localUrlButton->resizeToWidth(width);\n\t}, _localUrlButton->lifetime());\n\t_localUrlButton->setClickedCallback([=] {\n\t\tconst auto current = urlFromClipboard();\n\t\tUrlClickHandler::Open(current.isEmpty() ? url : current);\n\t});\n}\n\nvoid Suggestions::setupTabs() {\n\t_tabsScroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) {\n\t\tconst auto pixelDelta = e->pixelDelta();\n\t\tconst auto angleDelta = e->angleDelta();\n\t\tif (std::abs(pixelDelta.x()) + std::abs(angleDelta.x())) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto y = pixelDelta.y() ? pixelDelta.y() : angleDelta.y();\n\t\t_tabsScroll->scrollToX(_tabsScroll->scrollLeft() - y);\n\t\treturn true;\n\t});\n\n\tconst auto scrollToIndex = [=](int index, anim::type type) {\n\t\tconst auto to = index\n\t\t\t? (_tabs->centerOfSection(index) - _tabsScroll->width() / 2)\n\t\t\t: 0;\n\t\t_tabsScrollAnimation.stop();\n\t\tif (type == anim::type::instant) {\n\t\t\t_tabsScroll->scrollToX(to);\n\t\t} else {\n\t\t\t_tabsScrollAnimation.start(\n\t\t\t\t[=](float64 v) { _tabsScroll->scrollToX(v); },\n\t\t\t\t_tabsScroll->scrollLeft(),\n\t\t\t\tstd::min(to, _tabsScroll->scrollLeftMax()),\n\t\t\t\tst::defaultTabsSlider.duration);\n\t\t}\n\t};\n\trpl::single(-1) | rpl::then(\n\t\t_tabs->sectionActivated()\n\t) | rpl::combine_previous(\n\t) | rpl::on_next([=](int was, int index) {\n\t\tif (was != index) {\n\t\t\tscrollToIndex(index, anim::type::normal);\n\t\t}\n\t}, _tabs->lifetime());\n\n\tconst auto shadow = Ui::CreateChild<Ui::PlainShadow>(this);\n\tshadow->lower();\n\n\tconst auto localY = _localUrlButton ? _localUrlButton->height() : 0;\n\t_tabsScroll->move(0, localY);\n\t_tabs->move(0, localY);\n\trpl::combine(\n\t\twidthValue(),\n\t\t_tabs->geometryValue()\n\t) | rpl::on_next([=](int width, const QRect &r) {\n\t\tconst auto line = st::lineWidth;\n\t\tshadow->setGeometry(0, rect::bottom(r) - line, width, line);\n\t}, shadow->lifetime());\n\n\tshadow->showOn(_tabsScroll->shownValue());\n\n\tconst auto labels = base::flat_map<Key, QString>{\n\t\t{ Key{ Tab::Chats }, tr::lng_recent_chats(tr::now) },\n\t\t{ Key{ Tab::Channels }, tr::lng_recent_channels(tr::now) },\n\t\t{ Key{ Tab::Apps }, tr::lng_recent_apps(tr::now) },\n\t\t{ Key{ Tab::Posts }, tr::lng_recent_posts(tr::now) },\n\t\t{ Key{ Tab::Media, MediaType::Photo }, tr::lng_all_photos(tr::now) },\n\t\t{ Key{ Tab::Media, MediaType::Video }, tr::lng_all_videos(tr::now) },\n\t\t{ Key{ Tab::Downloads }, tr::lng_all_downloads(tr::now) },\n\t\t{ Key{ Tab::Media, MediaType::Link }, tr::lng_all_links(tr::now) },\n\t\t{ Key{ Tab::Media, MediaType::File }, tr::lng_all_files(tr::now) },\n\t\t{\n\t\t\tKey{ Tab::Media, MediaType::MusicFile },\n\t\t\ttr::lng_all_music(tr::now),\n\t\t},\n\t\t{\n\t\t\tKey{ Tab::Media, MediaType::RoundVoiceFile },\n\t\t\ttr::lng_all_voice(tr::now),\n\t\t},\n\t};\n\n\tauto sections = std::vector<TextWithEntities>();\n\tfor (const auto key : _tabKeys) {\n\t\tconst auto i = labels.find(key);\n\t\tAssert(i != end(labels));\n\t\tsections.push_back({ i->second });\n\t}\n\t_tabs->setSections(sections);\n\t_tabs->sectionActivated(\n\t) | rpl::on_next([=](int section) {\n\t\tAssert(section >= 0 && section < _tabKeys.size());\n\t\tswitchTab(_tabKeys[section]);\n\t}, _tabs->lifetime());\n}\n\nvoid Suggestions::setupChats() {\n\t_recent->count.value() | rpl::on_next([=](int count) {\n\t\t_recent->wrap->toggle(count > 0, anim::type::instant);\n\t\t_emptyRecent->toggle(count == 0, anim::type::instant);\n\t}, _recent->wrap->lifetime());\n\n\t_topPeers->emptyValue() | rpl::on_next([=](bool empty) {\n\t\t_topPeersWrap->toggle(!empty, anim::type::instant);\n\t}, _topPeers->lifetime());\n\n\t_topPeers->clicks() | rpl::on_next([=](uint64 peerIdRaw) {\n\t\tconst auto peerId = PeerId(peerIdRaw);\n\t\t_topPeerChosen.fire(_controller->session().data().peer(peerId));\n\t}, _topPeers->lifetime());\n\n\t_topPeers->pressed() | rpl::on_next([=](uint64 peerIdRaw) {\n\t\thandlePressForChatPreview(PeerId(peerIdRaw), [=](bool shown) {\n\t\t\t_topPeers->pressLeftToContextMenu(shown);\n\t\t});\n\t}, _topPeers->lifetime());\n\n\t_topPeers->pressCancelled() | rpl::on_next([=] {\n\t\t_controller->cancelScheduledPreview();\n\t}, _topPeers->lifetime());\n\n\t_topPeers->showMenuRequests(\n\t) | rpl::on_next([=](const ShowTopPeerMenuRequest &request) {\n\t\tconst auto weak = base::make_weak(this);\n\t\tconst auto owner = &_controller->session().data();\n\t\tconst auto peer = owner->peer(PeerId(request.id));\n\t\tconst auto removeOne = [=] {\n\t\t\tpeer->session().topPeers().remove(peer);\n\t\t\tif (weak) {\n\t\t\t\t_topPeers->removeLocally(peer->id.value);\n\t\t\t}\n\t\t};\n\t\tconst auto session = &_controller->session();\n\t\tconst auto removeAll = crl::guard(session, [=] {\n\t\t\tsession->topPeers().toggleDisabled(true);\n\t\t\tif (weak) {\n\t\t\t\t_topPeers->removeLocally();\n\t\t\t}\n\t\t});\n\t\tFillEntryMenu(request.callback, {\n\t\t\t.controller = _controller,\n\t\t\t.peer = peer,\n\t\t\t.removeOneText = tr::lng_recent_remove(tr::now),\n\t\t\t.removeOne = removeOne,\n\t\t\t.removeAllText = tr::lng_recent_hide_top(\n\t\t\t\ttr::now,\n\t\t\t\tUi::Text::FixAmpersandInAction),\n\t\t\t.removeAllConfirm = tr::lng_recent_hide_sure(tr::now),\n\t\t\t.removeAll = removeAll,\n\t\t\t.closeCallback = crl::guard(\n\t\t\t\tthis,\n\t\t\t\t[=] { _closeRequests.fire({}); }),\n\t\t\t});\n\t}, _topPeers->lifetime());\n\n\t_topPeers->scrollToRequests(\n\t) | rpl::on_next([this](Ui::ScrollToRequest request) {\n\t\t_chatsScroll->scrollToY(request.ymin, request.ymax);\n\t}, _topPeers->lifetime());\n\n\t_topPeers->verticalScrollEvents(\n\t) | rpl::on_next([=](not_null<QWheelEvent*> e) {\n\t\t_chatsScroll->viewportEvent(e);\n\t}, _topPeers->lifetime());\n\n\t_chatsScroll->setVisible(_key.current().tab == Tab::Chats);\n\t_chatsScroll->setCustomTouchProcess(_recent->processTouch);\n}\n\nvoid Suggestions::handlePressForChatPreview(\n\tPeerId id,\n\tFn<void(bool)> callback) {\n\tcallback = crl::guard(this, callback);\n\tconst auto row = RowDescriptor(\n\t\t_controller->session().data().history(id),\n\t\tFullMsgId());\n\tif (base::IsAltPressed()) {\n\t\t_controller->showChatPreview(row, callback);\n\t} else {\n\t\t_controller->scheduleChatPreview(row, callback);\n\t}\n}\n\nvoid Suggestions::setupChannels() {\n\t_myChannels->count.value() | rpl::on_next([=](int count) {\n\t\t_myChannels->wrap->toggle(count > 0, anim::type::instant);\n\t}, _myChannels->wrap->lifetime());\n\n\t_recommendations->count.value() | rpl::on_next([=](int count) {\n\t\t_recommendations->wrap->toggle(count > 0, anim::type::instant);\n\t}, _recommendations->wrap->lifetime());\n\n\t_emptyChannels->toggleOn(\n\t\trpl::combine(\n\t\t\t_myChannels->count.value(),\n\t\t\t_recommendations->count.value(),\n\t\t\trpl::mappers::_1 + rpl::mappers::_2 == 0),\n\t\tanim::type::instant);\n\n\t_channelsScroll->setVisible(_key.current().tab == Tab::Channels);\n\t_channelsScroll->setCustomTouchProcess([=](not_null<QTouchEvent*> e) {\n\t\tconst auto myChannels = _myChannels->processTouch(e);\n\t\tconst auto recommendations = _recommendations->processTouch(e);\n\t\treturn myChannels || recommendations;\n\t});\n}\n\nvoid Suggestions::setupApps() {\n\t_recentApps->count.value() | rpl::on_next([=](int count) {\n\t\t_recentApps->wrap->toggle(count > 0, anim::type::instant);\n\t}, _recentApps->wrap->lifetime());\n\n\t_popularApps->count.value() | rpl::on_next([=](int count) {\n\t\t_popularApps->wrap->toggle(count > 0, anim::type::instant);\n\t}, _popularApps->wrap->lifetime());\n\n\t_appsScroll->setVisible(_key.current().tab == Tab::Apps);\n\t_appsScroll->setCustomTouchProcess([=](not_null<QTouchEvent*> e) {\n\t\tconst auto recentApps = _recentApps->processTouch(e);\n\t\tconst auto popularApps = _popularApps->processTouch(e);\n\t\treturn recentApps || popularApps;\n\t});\n}\n\nUi::Controls::SwipeHandlerArgs Suggestions::generateIncompleteSwipeArgs() {\n\t_swipeLifetime.destroy();\n\n\tauto update = [=](Ui::Controls::SwipeContextData data) {\n\t\tif (data.translation != 0) {\n\t\t\tif (!_swipeBackData.callback) {\n\t\t\t\t_swipeBackData = Ui::Controls::SetupSwipeBack(\n\t\t\t\t\tthis,\n\t\t\t\t\t[=]() -> std::pair<QColor, QColor> {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tst::historyForwardChooseBg->c,\n\t\t\t\t\t\t\tst::historyForwardChooseFg->c,\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t\tdata.translation < 0);\n\t\t\t}\n\t\t\t_swipeBackData.callback(data);\n\t\t\treturn;\n\t\t} else if (_swipeBackData.lifetime) {\n\t\t\t_swipeBackData = {};\n\t\t}\n\t};\n\tauto init = [=](int, Qt::LayoutDirection direction) {\n\t\tif (!_tabs) {\n\t\t\treturn Ui::Controls::SwipeHandlerFinishData();\n\t\t}\n\t\tconst auto activeSection = _tabs->activeSection();\n\t\tconst auto isToLeft = direction == Qt::RightToLeft;\n\t\tif ((isToLeft && activeSection > 0)\n\t\t\t|| (!isToLeft && activeSection < _tabKeys.size() - 1)) {\n\t\t\treturn Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {\n\t\t\t\tif (_tabs\n\t\t\t\t\t&& _tabs->activeSection() == activeSection) {\n\t\t\t\t\t_swipeBackData = {};\n\t\t\t\t\t_tabs->setActiveSection(isToLeft\n\t\t\t\t\t\t? activeSection - 1\n\t\t\t\t\t\t: activeSection + 1);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\treturn Ui::Controls::SwipeHandlerFinishData();\n\t};\n\treturn { .widget = this, .update = update, .init = init };\n}\n\nvoid Suggestions::reinstallSwipe(not_null<Ui::ElasticScroll*> scroll) {\n\t_swipeLifetime.destroy();\n\n\tauto args = generateIncompleteSwipeArgs();\n\targs.scroll = scroll;\n\targs.onLifetime = &_swipeLifetime;\n\n\tUi::Controls::SetupSwipeHandler(std::move(args));\n}\n\nvoid Suggestions::selectJump(Qt::Key direction, int pageSize) {\n\tswitch (_key.current().tab) {\n\tcase Tab::Chats: selectJumpChats(direction, pageSize); return;\n\tcase Tab::Channels: selectJumpChannels(direction, pageSize); return;\n\tcase Tab::Apps: selectJumpApps(direction, pageSize); return;\n\t}\n}\n\nvoid Suggestions::selectJumpChats(Qt::Key direction, int pageSize) {\n\tconst auto recentHasSelection = [=] {\n\t\treturn _recent->selectJump({}, 0) == JumpResult::Applied;\n\t};\n\tif (pageSize) {\n\t\tif (direction == Qt::Key_Down || direction == Qt::Key_Up) {\n\t\t\t_topPeers->deselectByKeyboard();\n\t\t\tif (!recentHasSelection()) {\n\t\t\t\tif (direction == Qt::Key_Down) {\n\t\t\t\t\t_recent->selectJump(direction, 0);\n\t\t\t\t} else {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_recent->selectJump(direction, pageSize)\n\t\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t\tif (direction == Qt::Key_Up) {\n\t\t\t\t\t_chatsScroll->scrollTo(0);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if (direction == Qt::Key_Up) {\n\t\tif (_recent->selectJump(direction, pageSize)\n\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t_topPeers->selectByKeyboard(direction);\n\t\t} else if (_topPeers->selectedByKeyboard()) {\n\t\t\t_topPeers->selectByKeyboard(direction);\n\t\t}\n\t} else if (direction == Qt::Key_Down) {\n\t\tif (!_topPeersWrap->toggled() || recentHasSelection()) {\n\t\t\t_recent->selectJump(direction, pageSize);\n\t\t} else if (_topPeers->selectedByKeyboard()) {\n\t\t\tif (!_topPeers->selectByKeyboard(direction)\n\t\t\t\t&& _recent->count.current() > 0) {\n\t\t\t\t_topPeers->deselectByKeyboard();\n\t\t\t\t_recent->selectJump(direction, pageSize);\n\t\t\t}\n\t\t} else {\n\t\t\t_topPeers->selectByKeyboard({});\n\t\t\t_chatsScroll->scrollTo(0);\n\t\t}\n\t} else if (direction == Qt::Key_Left || direction == Qt::Key_Right) {\n\t\tif (!recentHasSelection()) {\n\t\t\t_topPeers->selectByKeyboard(direction);\n\t\t}\n\t}\n}\n\nvoid Suggestions::selectJumpChannels(Qt::Key direction, int pageSize) {\n\tconst auto myChannelsHasSelection = [=] {\n\t\treturn _myChannels->selectJump({}, 0) == JumpResult::Applied;\n\t};\n\tconst auto recommendationsHasSelection = [=] {\n\t\treturn _recommendations->selectJump({}, 0) == JumpResult::Applied;\n\t};\n\tif (pageSize) {\n\t\tif (direction == Qt::Key_Down) {\n\t\t\tif (recommendationsHasSelection()) {\n\t\t\t\t_recommendations->selectJump(direction, pageSize);\n\t\t\t} else if (myChannelsHasSelection()) {\n\t\t\t\tif (_myChannels->selectJump(direction, pageSize)\n\t\t\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t\t\t_recommendations->selectJump(direction, 0);\n\t\t\t\t}\n\t\t\t} else if (_myChannels->count.current()) {\n\t\t\t\t_myChannels->selectJump(direction, 0);\n\t\t\t\t_myChannels->selectJump(direction, pageSize);\n\t\t\t} else if (_recommendations->count.current()) {\n\t\t\t\t_recommendations->selectJump(direction, 0);\n\t\t\t\t_recommendations->selectJump(direction, pageSize);\n\t\t\t}\n\t\t} else if (direction == Qt::Key_Up) {\n\t\t\tif (myChannelsHasSelection()) {\n\t\t\t\tif (_myChannels->selectJump(direction, pageSize)\n\t\t\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t\t\t_channelsScroll->scrollTo(0);\n\t\t\t\t}\n\t\t\t} else if (recommendationsHasSelection()) {\n\t\t\t\tif (_recommendations->selectJump(direction, pageSize)\n\t\t\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t\t\t_myChannels->selectJump(direction, -1);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if (direction == Qt::Key_Up) {\n\t\tif (myChannelsHasSelection()) {\n\t\t\t_myChannels->selectJump(direction, 0);\n\t\t} else if (_recommendations->selectJump(direction, 0)\n\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t_myChannels->selectJump(direction, -1);\n\t\t} else if (!recommendationsHasSelection()) {\n\t\t\tif (_myChannels->selectJump(direction, 0)\n\t\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t\t_channelsScroll->scrollTo(0);\n\t\t\t}\n\t\t}\n\t} else if (direction == Qt::Key_Down) {\n\t\tif (recommendationsHasSelection()) {\n\t\t\t_recommendations->selectJump(direction, 0);\n\t\t} else if (_myChannels->selectJump(direction, 0)\n\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t_recommendations->selectJump(direction, 0);\n\t\t} else if (!myChannelsHasSelection()) {\n\t\t\tif (_recommendations->selectJump(direction, 0)\n\t\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t\t_myChannels->selectJump(direction, 0);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Suggestions::selectJumpApps(Qt::Key direction, int pageSize) {\n\tconst auto recentAppsHasSelection = [=] {\n\t\treturn _recentApps->selectJump({}, 0) == JumpResult::Applied;\n\t};\n\tconst auto popularAppsHasSelection = [=] {\n\t\treturn _popularApps->selectJump({}, 0) == JumpResult::Applied;\n\t};\n\tif (pageSize) {\n\t\tif (direction == Qt::Key_Down) {\n\t\t\tif (popularAppsHasSelection()) {\n\t\t\t\t_popularApps->selectJump(direction, pageSize);\n\t\t\t} else if (recentAppsHasSelection()) {\n\t\t\t\tif (_recentApps->selectJump(direction, pageSize)\n\t\t\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t\t\t_popularApps->selectJump(direction, 0);\n\t\t\t\t}\n\t\t\t} else if (_recentApps->count.current()) {\n\t\t\t\t_recentApps->selectJump(direction, 0);\n\t\t\t\t_recentApps->selectJump(direction, pageSize);\n\t\t\t} else if (_popularApps->count.current()) {\n\t\t\t\t_popularApps->selectJump(direction, 0);\n\t\t\t\t_popularApps->selectJump(direction, pageSize);\n\t\t\t}\n\t\t} else if (direction == Qt::Key_Up) {\n\t\t\tif (recentAppsHasSelection()) {\n\t\t\t\tif (_recentApps->selectJump(direction, pageSize)\n\t\t\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t\t\t_channelsScroll->scrollTo(0);\n\t\t\t\t}\n\t\t\t} else if (popularAppsHasSelection()) {\n\t\t\t\tif (_popularApps->selectJump(direction, pageSize)\n\t\t\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t\t\t_recentApps->selectJump(direction, -1);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if (direction == Qt::Key_Up) {\n\t\tif (recentAppsHasSelection()) {\n\t\t\t_recentApps->selectJump(direction, 0);\n\t\t} else if (_popularApps->selectJump(direction, 0)\n\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t_recentApps->selectJump(direction, -1);\n\t\t} else if (!popularAppsHasSelection()) {\n\t\t\tif (_recentApps->selectJump(direction, 0)\n\t\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t\t_channelsScroll->scrollTo(0);\n\t\t\t}\n\t\t}\n\t} else if (direction == Qt::Key_Down) {\n\t\tif (popularAppsHasSelection()) {\n\t\t\t_popularApps->selectJump(direction, 0);\n\t\t} else if (_recentApps->selectJump(direction, 0)\n\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t_popularApps->selectJump(direction, 0);\n\t\t} else if (!recentAppsHasSelection()) {\n\t\t\tif (_popularApps->selectJump(direction, 0)\n\t\t\t\t== JumpResult::AppliedAndOut) {\n\t\t\t\t_recentApps->selectJump(direction, 0);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Suggestions::chooseRow() {\n\tswitch (_key.current().tab) {\n\tcase Tab::Chats:\n\t\tif (!_topPeers->chooseRow()) {\n\t\t\t_recent->choose();\n\t\t}\n\t\tbreak;\n\tcase Tab::Channels:\n\t\tif (!_myChannels->choose()) {\n\t\t\t_recommendations->choose();\n\t\t}\n\t\tbreak;\n\tcase Tab::Apps:\n\t\tif (!_recentApps->choose()) {\n\t\t\t_popularApps->choose();\n\t\t}\n\t\tbreak;\n\t}\n}\n\nbool Suggestions::consumeSearchQuery(const QString &query) {\n\tusing Type = MediaType;\n\tconst auto key = _key.current();\n\tconst auto tab = key.tab;\n\tconst auto type = (key.tab == Tab::Media) ? key.mediaType : Type::kCount;\n\tif (tab == Tab::Posts) {\n\t\tconst auto changed = (_searchQuery != query);\n\t\tsetPostsSearchQuery(query);\n\t\treturn changed || !query.isEmpty();\n\t} else if (tab != Tab::Downloads\n\t\t&& type != Type::File\n\t\t&& type != Type::Link\n\t\t&& type != Type::MusicFile) {\n\t\treturn false;\n\t} else if (_searchQuery == query) {\n\t\treturn false;\n\t}\n\t_searchQuery = query;\n\t_persist = !_searchQuery.isEmpty();\n\tif (query.isEmpty() || tab == Tab::Downloads) {\n\t\t_searchQueryTimer.cancel();\n\t\tapplySearchQuery();\n\t} else {\n\t\t_searchQueryTimer.callOnce(kSearchQueryDelay);\n\t}\n\treturn true;\n}\n\nvoid Suggestions::setupPostsSearch() {\n\t_postsSearch = std::make_unique<PostsSearch>(&_controller->session());\n\n\t_postsSearch->stateUpdates(\n\t) | rpl::on_next([=](const PostsSearchState &state) {\n\t\tif (state.intro) {\n\t\t\tif (!_postsSearchIntro) {\n\t\t\t\tsetupPostsIntro(*state.intro);\n\t\t\t} else {\n\t\t\t\t_postsSearchIntro->update(*state.intro);\n\t\t\t}\n\t\t\treturn;\n\t\t} else if (!_postsContent) {\n\t\t\tsetupPostsResults();\n\t\t}\n\n\t\t_postsContent->applySearchState(SearchState{\n\t\t\t.tab = ChatSearchTab::PublicPosts,\n\t\t\t.query = _searchQuery,\n\t\t});\n\t\tif (state.loading) {\n\t\t\t_postsContent->searchRequested(true);\n\t\t} else {\n\t\t\t_postsContent->searchReceived(\n\t\t\t\tstate.page,\n\t\t\t\tnullptr,\n\t\t\t\t{ .posts = true, .start = true },\n\t\t\t\tstate.totalCount);\n\t\t\t_postsScroll->scrollToY(0);\n\t\t\tupdatePostsSearchVisibleRange();\n\t\t}\n\t}, _postsWrap->lifetime());\n\n\t_postsSearch->pagesUpdates(\n\t) | rpl::on_next([=](const PostsSearchState &state) {\n\t\tExpects(!state.intro && !state.loading);\n\n\t\tif (!_postsContent) {\n\t\t\treturn;\n\t\t}\n\t\t_postsContent->searchReceived(\n\t\t\tstate.page,\n\t\t\tnullptr,\n\t\t\t{ .posts = true },\n\t\t\tstate.totalCount);\n\t\tupdatePostsSearchVisibleRange();\n\t}, _postsWrap->lifetime());\n}\n\nvoid Suggestions::setPostsSearchQuery(const QString &query) {\n\tif (!_postsSearch) {\n\t\tsetupPostsSearch();\n\t}\n\tif (!query.isEmpty()) {\n\t\t_persist = true;\n\t}\n\t_searchQuery = query;\n\t_searchQueryTimer.cancel();\n\t_postsSearch->setQuery(query);\n}\n\nvoid Suggestions::setupPostsResults() {\n\tExpects(!_postsContent);\n\n\tdelete base::take(_postsSearchIntro);\n\t_postsContent = Ui::CreateChild<InnerWidget>(\n\t\t_postsWrap.get(),\n\t\t_controller,\n\t\trpl::single(InnerWidget::ChildListShown()));\n\n\t_postsContent->applySearchState(SearchState{\n\t\t.tab = ChatSearchTab::PublicPosts,\n\t\t.query = _searchQuery,\n\t});\n\t_postsContent->searchRequested(true);\n\n\t_postsContent->chosenRow(\n\t) | rpl::on_next([=](const ChosenRow &row) {\n\t\tconst auto history = row.key.history();\n\t\tif (!history) {\n\t\t\treturn;\n\t\t}\n\t\t_persist = true;\n\t\tconst auto showAtMsgId = row.message.fullId.msg;\n\t\tauto params = Window::SectionShow(\n\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\tparams.highlight = Window::SearchHighlightId(_searchQuery);\n\t\tif (row.newWindow) {\n\t\t\t_controller->showInNewWindow(history->peer, showAtMsgId);\n\t\t\t_closeRequests.fire({});\n\t\t} else {\n\t\t\t_controller->showThread(history, showAtMsgId, params);\n\t\t}\n\t}, _postsContent->lifetime());\n\n\t_postsContent->heightValue() | rpl::on_next([=](int height) {\n\t\t_postsWrap->resize(_postsWrap->width(), height);\n\t}, _postsContent->lifetime());\n\n\trpl::combine(\n\t\trpl::single(rpl::empty) | rpl::then(_postsScroll->scrolls()),\n\t\t_postsScroll->heightValue()\n\t) | rpl::on_next([=] {\n\t\tupdatePostsSearchVisibleRange();\n\t}, _postsContent->lifetime());\n\n\t_postsContent->setLoadMoreCallback([=] {\n\t\t_postsSearch->requestMore();\n\t});\n\n\t_postsContent->setNarrowRatio(0.);\n\t_postsContent->show();\n\tupdateControlsGeometry();\n}\n\nvoid Suggestions::updatePostsSearchVisibleRange() {\n\tExpects(_postsContent != nullptr);\n\n\tconst auto top = _postsScroll->scrollTop();\n\tconst auto height = _postsScroll->height();\n\t_postsContent->setVisibleTopBottom(top, top + height);\n}\n\nvoid Suggestions::setupPostsIntro(const PostsSearchIntroState &intro) {\n\tExpects(!_postsSearchIntro);\n\n\tdelete base::take(_postsContent);\n\t_postsSearchIntro = Ui::CreateChild<PostsSearchIntro>(_postsWrap, intro);\n\n\t_postsSearchIntro->searchWithStars(\n\t) | rpl::on_next([=](int stars) {\n\t\tif (!_controller->session().premium()) {\n\t\t\tSettings::ShowPremium(\n\t\t\t\t_controller,\n\t\t\t\tu\"posts_search\"_q);\n\t\t} else if (!stars) {\n\t\t\t_postsSearch->setAllowedStars(0);\n\t\t} else {\n\t\t\tusing namespace Settings;\n\t\t\tconst auto done = [=](Settings::SmallBalanceResult result) {\n\t\t\t\tif (result == Settings::SmallBalanceResult::Success\n\t\t\t\t\t|| result == Settings::SmallBalanceResult::Already) {\n\t\t\t\t\tconst auto spent = _postsSearch->setAllowedStars(stars);\n\t\t\t\t\tif (spent > 0) {\n\t\t\t\t\t\t_controller->showToast({\n\t\t\t\t\t\t\t.text = tr::lng_posts_paid_spent(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\t\tspent,\n\t\t\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t\t\t.attach = RectPart::Top,\n\t\t\t\t\t\t\t.duration = Ui::Toast::kDefaultDuration * 2,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\tMaybeRequestBalanceIncrease(\n\t\t\t\t_controller->uiShow(),\n\t\t\t\tstars,\n\t\t\t\tSmallBalanceForSearch{},\n\t\t\t\tdone);\n\t\t}\n\t}, _postsSearchIntro->lifetime());\n\n\t_postsScroll->heightValue() | rpl::on_next([=](int height) {\n\t\t_postsWrap->resize(_postsWrap->width(), height);\n\t}, _postsSearchIntro->lifetime());\n\n\t_postsSearchIntro->show();\n\tupdateControlsGeometry();\n}\n\nvoid Suggestions::applySearchQuery() {\n\tconst auto key = _key.current();\n\tconst auto controller = _mediaLists[key].wrap->controller();\n\tconst auto search = controller->searchFieldController();\n\tif (search->query() != _searchQuery) {\n\t\tsearch->setQuery(_searchQuery);\n\t}\n}\n\nrpl::producer<> Suggestions::clearSearchQueryRequests() const {\n\treturn _clearSearchQueryRequests.events();\n}\n\nData::Thread *Suggestions::updateFromParentDrag(QPoint globalPosition) {\n\tswitch (_key.current().tab) {\n\tcase Tab::Chats: return updateFromChatsDrag(globalPosition);\n\tcase Tab::Channels: return updateFromChannelsDrag(globalPosition);\n\t}\n\treturn nullptr;\n}\n\nData::Thread *Suggestions::updateFromChatsDrag(QPoint globalPosition) {\n\tif (const auto top = _topPeers->updateFromParentDrag(globalPosition)) {\n\t\treturn _controller->session().data().history(PeerId(top));\n\t}\n\treturn fromListId(_recent->updateFromParentDrag(globalPosition));\n}\n\nData::Thread *Suggestions::updateFromChannelsDrag(QPoint globalPosition) {\n\tif (const auto id = _myChannels->updateFromParentDrag(globalPosition)) {\n\t\treturn fromListId(id);\n\t}\n\treturn fromListId(_recommendations->updateFromParentDrag(globalPosition));\n}\n\nData::Thread *Suggestions::updateFromAppsDrag(QPoint globalPosition) {\n\tif (const auto id = _recentApps->updateFromParentDrag(globalPosition)) {\n\t\treturn fromListId(id);\n\t}\n\treturn fromListId(_popularApps->updateFromParentDrag(globalPosition));\n}\n\nData::Thread *Suggestions::fromListId(uint64 peerListRowId) {\n\treturn peerListRowId\n\t\t? _controller->session().data().history(PeerId(peerListRowId)).get()\n\t\t: nullptr;\n}\n\nvoid Suggestions::dragLeft() {\n\t_topPeers->dragLeft();\n\t_recent->dragLeft();\n\t_myChannels->dragLeft();\n\t_recommendations->dragLeft();\n\t_recentApps->dragLeft();\n\t_popularApps->dragLeft();\n}\n\nvoid Suggestions::show(anim::type animated, Fn<void()> finish) {\n\tRpWidget::show();\n\n\t_hidden = false;\n\tif (animated == anim::type::instant) {\n\t\tfinishShow();\n\t} else {\n\t\tstartShownAnimation(true, std::move(finish));\n\t}\n}\n\nvoid Suggestions::hide(anim::type animated, Fn<void()> finish) {\n\t_hidden = true;\n\tif (isHidden()) {\n\t\treturn;\n\t} else if (animated == anim::type::instant) {\n\t\tRpWidget::hide();\n\t} else {\n\t\tstartShownAnimation(false, std::move(finish));\n\t}\n}\n\nvoid Suggestions::switchTab(Key key) {\n\tconst auto was = _key.current();\n\tif (was == key) {\n\t\treturn;\n\t}\n\tconsumeSearchQuery(QString());\n\t_key = key;\n\t_persist = false;\n\t_clearSearchQueryRequests.fire({});\n\tif (_tabs->isHidden()) {\n\t\treturn;\n\t}\n\tstartSlideAnimation(was, key);\n}\n\nvoid Suggestions::ensureContent(Key key) {\n\tif (key.tab == Tab::Posts) {\n\t\tsetPostsSearchQuery(QString());\n\t\treturn;\n\t} else if (key.tab != Tab::Downloads && key.tab != Tab::Media) {\n\t\treturn;\n\t}\n\tauto &list = _mediaLists[key];\n\tif (list.wrap) {\n\t\treturn;\n\t}\n\tconst auto self = _controller->session().user();\n\tconst auto memento = (key.tab == Tab::Downloads)\n\t\t? Info::Downloads::Make(self)\n\t\t: std::make_shared<Info::Memento>(\n\t\t\tself,\n\t\t\tInfo::Section(key.mediaType, Info::Section::Type::GlobalMedia));\n\tlist.wrap = Ui::CreateChild<Info::WrapWidget>(\n\t\tthis,\n\t\t_controller,\n\t\tInfo::Wrap::Search,\n\t\tmemento.get());\n\tlist.wrap->show();\n\tupdateControlsGeometry();\n}\n\nvoid Suggestions::startSlideAnimation(Key was, Key now) {\n\tensureContent(now);\n\tconst auto wasIndex = ranges::find(_tabKeys, was);\n\tconst auto nowIndex = ranges::find(_tabKeys, now);\n\tif (!_slideAnimation.animating()) {\n\t\tconst auto find = [&](Key key) -> not_null<QWidget*> {\n\t\t\tswitch (key.tab) {\n\t\t\tcase Tab::Chats: return _chatsScroll.get();\n\t\t\tcase Tab::Channels: return _channelsScroll.get();\n\t\t\tcase Tab::Apps: return _appsScroll.get();\n\t\t\tcase Tab::Posts: return _postsScroll.get();\n\t\t\t}\n\t\t\treturn _mediaLists[key].wrap;\n\t\t};\n\t\tauto left = find(was);\n\t\tauto right = find(now);\n\t\tif (wasIndex > nowIndex) {\n\t\t\tstd::swap(left, right);\n\t\t}\n\t\t_slideLeft = Ui::GrabWidget(left);\n\t\t_slideLeftTop = left->y();\n\t\t_slideRight = Ui::GrabWidget(right);\n\t\t_slideRightTop = right->y();\n\t\tleft->hide();\n\t\tright->hide();\n\t}\n\tconst auto from = (nowIndex > wasIndex) ? 0. : 1.;\n\tconst auto to = (nowIndex > wasIndex) ? 1. : 0.;\n\t_slideAnimation.start([=] {\n\t\tupdate();\n\t\tif (!_slideAnimation.animating() && !_shownAnimation.animating()) {\n\t\t\tfinishShow();\n\t\t}\n\t}, from, to, st::slideDuration, anim::sineInOut);\n}\n\nvoid Suggestions::startShownAnimation(bool shown, Fn<void()> finish) {\n\tconst auto from = shown ? 0. : 1.;\n\tconst auto to = shown ? 1. : 0.;\n\t_shownAnimation.start([=] {\n\t\tupdate();\n\t\tif (!_shownAnimation.animating() && finish) {\n\t\t\tfinish();\n\t\t\tif (shown) {\n\t\t\t\tfinishShow();\n\t\t\t}\n\t\t}\n\t}, from, to, st::slideDuration, anim::easeOutQuint);\n\tif (_cache.isNull()) {\n\t\tconst auto now = width();\n\t\tif (now < st::columnMinimalWidthLeft) {\n\t\t\tresize(st::columnMinimalWidthLeft, height());\n\t\t}\n\t\t_cache = Ui::GrabWidget(this);\n\t\tif (now < st::columnMinimalWidthLeft) {\n\t\t\tresize(now, height());\n\t\t}\n\t}\n\tif (_localUrlButton) {\n\t\t_localUrlButton->hide();\n\t}\n\t_tabsScroll->hide();\n\t_chatsScroll->hide();\n\t_channelsScroll->hide();\n\t_appsScroll->hide();\n\t_postsScroll->hide();\n\tfor (const auto &[key, list] : _mediaLists) {\n\t\tlist.wrap->hide();\n\t}\n\t_slideAnimation.stop();\n}\n\nvoid Suggestions::finishShow() {\n\t_slideAnimation.stop();\n\t_slideLeft = _slideRight = QPixmap();\n\t_slideLeftTop = _slideRightTop = 0;\n\n\t_shownAnimation.stop();\n\t_cache = QPixmap();\n\n\tif (_localUrlButton) {\n\t\t_localUrlButton->show();\n\t}\n\t_tabsScroll->show();\n\tconst auto key = _key.current();\n\t_chatsScroll->setVisible(key == Key{ Tab::Chats });\n\t_channelsScroll->setVisible(key == Key{ Tab::Channels });\n\t_appsScroll->setVisible(key == Key{ Tab::Apps });\n\t_postsScroll->setVisible(key == Key{ Tab::Posts });\n\tfor (const auto &[mediaKey, list] : _mediaLists) {\n\t\tlist.wrap->setVisible(key == mediaKey);\n\t\tif (key == mediaKey) {\n\t\t\t_swipeLifetime.destroy();\n\t\t\tauto incomplete = generateIncompleteSwipeArgs();\n\t\t\tlist.wrap->replaceSwipeHandler(&incomplete);\n\t\t}\n\t}\n\tif (key == Key{ Tab::Chats }) {\n\t\treinstallSwipe(_chatsScroll.get());\n\t} else if (key == Key{ Tab::Channels }) {\n\t\treinstallSwipe(_channelsScroll.get());\n\t} else if (key == Key{ Tab::Apps }) {\n\t\treinstallSwipe(_appsScroll.get());\n\t} else if (key == Key{ Tab::Posts }) {\n\t\treinstallSwipe(_postsScroll.get());\n\t}\n}\n\nfloat64 Suggestions::shownOpacity() const {\n\treturn _shownAnimation.value(_hidden ? 0. : 1.);\n}\n\nstd::vector<Suggestions::Key> Suggestions::TabKeysFor(\n\t\tnot_null<Window::SessionController*> controller) {\n\tauto result = std::vector<Key>{\n\t\t{ Tab::Chats },\n\t\t{ Tab::Channels },\n\t\t{ Tab::Apps },\n\t\t{ Tab::Posts },\n\t\t{ Tab::Media, MediaType::Photo },\n\t\t{ Tab::Media, MediaType::Video },\n\t\t{ Tab::Downloads },\n\t\t{ Tab::Media, MediaType::Link },\n\t\t{ Tab::Media, MediaType::File },\n\t\t{ Tab::Media, MediaType::MusicFile },\n\t\t{ Tab::Media, MediaType::RoundVoiceFile },\n\t};\n\tif (Core::App().downloadManager().empty()) {\n\t\tresult.erase(ranges::find(result, Key{ Tab::Downloads }));\n\t}\n\treturn result;\n}\n\nvoid Suggestions::paintEvent(QPaintEvent *e) {\n\tconst auto opacity = shownOpacity();\n\tauto color = st::windowBg->c;\n\tcolor.setAlphaF(color.alphaF() * opacity);\n\n\tauto p = QPainter(this);\n\tp.fillRect(e->rect(), color);\n\tif (!_cache.isNull()) {\n\t\tconst auto slide = st::topPeers.height + st::searchedBarHeight;\n\t\tp.setOpacity(opacity);\n\t\tp.drawPixmap(0, (opacity - 1.) * slide, _cache);\n\t} else if (!_slideLeft.isNull()) {\n\t\tconst auto slide = st::topPeers.height + st::searchedBarHeight;\n\t\tconst auto right = (_key.current().tab == Tab::Channels);\n\t\tconst auto progress = _slideAnimation.value(right ? 1. : 0.);\n\t\tp.setOpacity(1. - progress);\n\t\tp.drawPixmap(\n\t\t\tanim::interpolate(0, -slide, progress),\n\t\t\t_slideLeftTop,\n\t\t\t_slideLeft);\n\t\tp.setOpacity(progress);\n\t\tp.drawPixmap(\n\t\t\tanim::interpolate(slide, 0, progress),\n\t\t\t_slideRightTop,\n\t\t\t_slideRight);\n\t}\n}\n\nvoid Suggestions::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid Suggestions::updateControlsGeometry() {\n\tconst auto w = std::max(width(), st::columnMinimalWidthLeft);\n\t_tabs->fitWidthToSections();\n\tif (_localUrlButton) {\n\t\t_localUrlButton->resizeToWidth(w);\n\t\t_tabs->move(_tabs->x(), rect::bottom(_localUrlButton.get()));\n\t}\n\tconst auto tabs = rect::bottom(_tabs.get());\n\t_tabsScroll->setGeometry(0, _tabs->y(), w, _tabs->height());\n\n\tconst auto content = QRect(0, tabs, w, height() - tabs);\n\n\t_chatsScroll->setGeometry(content);\n\t_chatsContent->resizeToWidth(w);\n\n\t_channelsScroll->setGeometry(content);\n\t_channelsContent->resizeToWidth(w);\n\n\t_appsScroll->setGeometry(content);\n\t_appsContent->resizeToWidth(w);\n\n\t_postsScroll->setGeometry(content);\n\t_postsWrap->resizeToWidth(w);\n\tif (_postsSearchIntro) {\n\t\t_postsSearchIntro->setGeometry(0, 0, w, height() - tabs);\n\t} else if (_postsContent) {\n\t\t_postsContent->resizeToWidth(w);\n\t\t_postsContent->setMinimumHeight(height() - tabs);\n\t\t_postsContent->refresh();\n\t}\n\n\tconst auto expanding = false;\n\tfor (const auto &[key, list] : _mediaLists) {\n\t\tconst auto full = !list.wrap->scrollBottomSkip();\n\t\tconst auto additionalScroll = (full ? st::boxRadius : 0);\n\t\tconst auto height = content.height() - (full ? 0 : st::boxRadius);\n\t\tconst auto wrapGeometry = QRect{ 0, tabs, w, height};\n\t\tlist.wrap->updateGeometry(\n\t\t\twrapGeometry,\n\t\t\texpanding,\n\t\t\tadditionalScroll,\n\t\t\tcontent.height());\n\t}\n}\n\nauto Suggestions::setupRecentPeers(RecentPeersList recentPeers)\n-> std::unique_ptr<ObjectList> {\n\tconst auto controller = lifetime().make_state<RecentsController>(\n\t\t_controller,\n\t\tstd::move(recentPeers),\n\t\t[=](not_null<PeerData*> p) { _openBotMainAppRequests.fire_copy(p); },\n\t\t[=] { _closeRequests.fire({}); });\n\n\tconst auto addToScroll = [=] {\n\t\treturn _topPeersWrap->toggled() ? _topPeers->height() : 0;\n\t};\n\tauto result = setupObjectList(\n\t\t_chatsScroll.get(),\n\t\t_chatsContent,\n\t\tcontroller,\n\t\taddToScroll);\n\tconst auto raw = result.get();\n\tconst auto list = raw->wrap->entity();\n\n\traw->selectJump = [list](Qt::Key direction, int pageSize) {\n\t\tconst auto had = list->hasSelection();\n\t\tif (direction == Qt::Key()) {\n\t\t\treturn had ? JumpResult::Applied : JumpResult::NotApplied;\n\t\t} else if (direction == Qt::Key_Up && !had) {\n\t\t\treturn JumpResult::NotApplied;\n\t\t} else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {\n\t\t\tconst auto delta = (direction == Qt::Key_Down) ? 1 : -1;\n\t\t\tif (pageSize > 0) {\n\t\t\t\tlist->selectSkipPage(pageSize, delta);\n\t\t\t} else {\n\t\t\t\tlist->selectSkip(delta);\n\t\t\t}\n\t\t\treturn list->hasSelection()\n\t\t\t\t? JumpResult::Applied\n\t\t\t\t: had\n\t\t\t\t? JumpResult::AppliedAndOut\n\t\t\t\t: JumpResult::NotApplied;\n\t\t}\n\t\treturn JumpResult::NotApplied;\n\t};\n\n\traw->chosen.events(\n\t) | rpl::on_next([=](not_null<PeerData*> peer) {\n\t\t_controller->session().recentPeers().bump(peer);\n\t}, list->lifetime());\n\n\treturn result;\n}\n\nobject_ptr<Ui::SlideWrap<>> Suggestions::setupEmptyRecent() {\n\tconst auto icon = SearchEmptyIcon::Search;\n\treturn setupEmpty(_chatsContent, icon, tr::lng_recent_none());\n}\n\nauto Suggestions::setupMyChannels() -> std::unique_ptr<ObjectList> {\n\tconst auto controller = lifetime().make_state<MyChannelsController>(\n\t\t_controller);\n\n\tauto result = setupObjectList(\n\t\t_channelsScroll.get(),\n\t\t_channelsContent,\n\t\tcontroller);\n\tconst auto raw = result.get();\n\tconst auto list = raw->wrap->entity();\n\n\traw->selectJump = [=](Qt::Key direction, int pageSize) {\n\t\tconst auto had = list->hasSelection();\n\t\tif (direction == Qt::Key()) {\n\t\t\treturn had ? JumpResult::Applied : JumpResult::NotApplied;\n\t\t} else if (direction == Qt::Key_Up && !had) {\n\t\t\tif (pageSize < 0) {\n\t\t\t\tlist->selectLast();\n\t\t\t\treturn list->hasSelection()\n\t\t\t\t\t? JumpResult::Applied\n\t\t\t\t\t: JumpResult::NotApplied;\n\t\t\t}\n\t\t\treturn JumpResult::NotApplied;\n\t\t} else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {\n\t\t\tconst auto was = list->selectedIndex();\n\t\t\tconst auto delta = (direction == Qt::Key_Down) ? 1 : -1;\n\t\t\tif (pageSize > 0) {\n\t\t\t\tlist->selectSkipPage(pageSize, delta);\n\t\t\t} else {\n\t\t\t\tlist->selectSkip(delta);\n\t\t\t}\n\t\t\tif (had\n\t\t\t\t&& delta > 0\n\t\t\t\t&& raw->count.current()\n\t\t\t\t&& list->selectedIndex() == was) {\n\t\t\t\tlist->clearSelection();\n\t\t\t\treturn JumpResult::AppliedAndOut;\n\t\t\t}\n\t\t\treturn list->hasSelection()\n\t\t\t\t? JumpResult::Applied\n\t\t\t\t: had\n\t\t\t\t? JumpResult::AppliedAndOut\n\t\t\t\t: JumpResult::NotApplied;\n\t\t}\n\t\treturn JumpResult::NotApplied;\n\t};\n\n\traw->chosen.events(\n\t) | rpl::on_next([=] {\n\t\t_persist = false;\n\t}, list->lifetime());\n\n\treturn result;\n}\n\nauto Suggestions::setupRecommendations() -> std::unique_ptr<ObjectList> {\n\tconst auto controller = lifetime().make_state<RecommendationsController>(\n\t\t_controller);\n\n\tconst auto addToScroll = [=] {\n\t\tconst auto wrap = _myChannels->wrap;\n\t\treturn wrap->toggled() ? wrap->height() : 0;\n\t};\n\tauto result = setupObjectList(\n\t\t_channelsScroll.get(),\n\t\t_channelsContent,\n\t\tcontroller,\n\t\taddToScroll);\n\tconst auto raw = result.get();\n\tconst auto list = raw->wrap->entity();\n\n\traw->selectJump = [list](Qt::Key direction, int pageSize) {\n\t\tconst auto had = list->hasSelection();\n\t\tif (direction == Qt::Key()) {\n\t\t\treturn had ? JumpResult::Applied : JumpResult::NotApplied;\n\t\t} else if (direction == Qt::Key_Up && !had) {\n\t\t\treturn JumpResult::NotApplied;\n\t\t} else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {\n\t\t\tconst auto delta = (direction == Qt::Key_Down) ? 1 : -1;\n\t\t\tif (pageSize > 0) {\n\t\t\t\tlist->selectSkipPage(pageSize, delta);\n\t\t\t} else {\n\t\t\t\tlist->selectSkip(delta);\n\t\t\t}\n\t\t\treturn list->hasSelection()\n\t\t\t\t? JumpResult::Applied\n\t\t\t\t: had\n\t\t\t\t? JumpResult::AppliedAndOut\n\t\t\t\t: JumpResult::NotApplied;\n\t\t}\n\t\treturn JumpResult::NotApplied;\n\t};\n\n\traw->chosen.events(\n\t) | rpl::on_next([=] {\n\t\t_persist = true;\n\t}, list->lifetime());\n\n\t_key.value() | rpl::filter(\n\t\trpl::mappers::_1 == Key{ Tab::Channels }\n\t) | rpl::on_next([=] {\n\t\tcontroller->load();\n\t}, list->lifetime());\n\n\treturn result;\n}\n\nauto Suggestions::setupRecentApps() -> std::unique_ptr<ObjectList> {\n\tconst auto controller = lifetime().make_state<RecentAppsController>(\n\t\t_controller);\n\tcontroller->setCloseCallback([=] {\n\t\t_closeRequests.fire({});\n\t});\n\t_recentAppsShows = [=](not_null<PeerData*> peer) {\n\t\treturn controller->shown(peer);\n\t};\n\t_recentAppsRefreshed = controller->refreshed();\n\n\tauto result = setupObjectList(\n\t\t_appsScroll.get(),\n\t\t_appsContent,\n\t\tcontroller);\n\tconst auto raw = result.get();\n\tconst auto list = raw->wrap->entity();\n\n\traw->selectJump = [=](Qt::Key direction, int pageSize) {\n\t\tconst auto had = list->hasSelection();\n\t\tif (direction == Qt::Key()) {\n\t\t\treturn had ? JumpResult::Applied : JumpResult::NotApplied;\n\t\t} else if (direction == Qt::Key_Up && !had) {\n\t\t\tif (pageSize < 0) {\n\t\t\t\tlist->selectLast();\n\t\t\t\treturn list->hasSelection()\n\t\t\t\t\t? JumpResult::Applied\n\t\t\t\t\t: JumpResult::NotApplied;\n\t\t\t}\n\t\t\treturn JumpResult::NotApplied;\n\t\t} else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {\n\t\t\tconst auto was = list->selectedIndex();\n\t\t\tconst auto delta = (direction == Qt::Key_Down) ? 1 : -1;\n\t\t\tif (pageSize > 0) {\n\t\t\t\tlist->selectSkipPage(pageSize, delta);\n\t\t\t} else {\n\t\t\t\tlist->selectSkip(delta);\n\t\t\t}\n\t\t\tif (had\n\t\t\t\t&& delta > 0\n\t\t\t\t&& raw->count.current()\n\t\t\t\t&& list->selectedIndex() == was) {\n\t\t\t\tlist->clearSelection();\n\t\t\t\treturn JumpResult::AppliedAndOut;\n\t\t\t}\n\t\t\treturn list->hasSelection()\n\t\t\t\t? JumpResult::Applied\n\t\t\t\t: had\n\t\t\t\t? JumpResult::AppliedAndOut\n\t\t\t\t: JumpResult::NotApplied;\n\t\t}\n\t\treturn JumpResult::NotApplied;\n\t};\n\n\traw->chosen.events(\n\t) | rpl::on_next([=] {\n\t\t_persist = false;\n\t}, list->lifetime());\n\n\tcontroller->load();\n\n\treturn result;\n}\n\nauto Suggestions::setupPopularApps() -> std::unique_ptr<ObjectList> {\n\tconst auto controller = lifetime().make_state<PopularAppsController>(\n\t\t_controller,\n\t\t_recentAppsShows,\n\t\trpl::duplicate(_recentAppsRefreshed));\n\n\tconst auto addToScroll = [=] {\n\t\tconst auto wrap = _recentApps->wrap;\n\t\treturn wrap->toggled() ? wrap->height() : 0;\n\t};\n\tauto result = setupObjectList(\n\t\t_appsScroll.get(),\n\t\t_appsContent,\n\t\tcontroller,\n\t\taddToScroll);\n\tconst auto raw = result.get();\n\tconst auto list = raw->wrap->entity();\n\n\traw->selectJump = [list](Qt::Key direction, int pageSize) {\n\t\tconst auto had = list->hasSelection();\n\t\tif (direction == Qt::Key()) {\n\t\t\treturn had ? JumpResult::Applied : JumpResult::NotApplied;\n\t\t} else if (direction == Qt::Key_Up && !had) {\n\t\t\treturn JumpResult::NotApplied;\n\t\t} else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {\n\t\t\tconst auto delta = (direction == Qt::Key_Down) ? 1 : -1;\n\t\t\tif (pageSize > 0) {\n\t\t\t\tlist->selectSkipPage(pageSize, delta);\n\t\t\t} else {\n\t\t\t\tlist->selectSkip(delta);\n\t\t\t}\n\t\t\treturn list->hasSelection()\n\t\t\t\t? JumpResult::Applied\n\t\t\t\t: had\n\t\t\t\t? JumpResult::AppliedAndOut\n\t\t\t\t: JumpResult::NotApplied;\n\t\t}\n\t\treturn JumpResult::NotApplied;\n\t};\n\n\traw->chosen.events(\n\t) | rpl::on_next([=] {\n\t\t_persist = true;\n\t}, list->lifetime());\n\n\t_key.value() | rpl::filter(\n\t\trpl::mappers::_1 == Key{ Tab::Apps }\n\t) | rpl::on_next([=] {\n\t\tcontroller->load();\n\t}, list->lifetime());\n\n\treturn result;\n}\n\nauto Suggestions::setupObjectList(\n\tnot_null<Ui::ElasticScroll*> scroll,\n\tnot_null<Ui::VerticalLayout*> parent,\n\tnot_null<ObjectListController*> controller,\n\tFn<int()> addToScroll)\n-> std::unique_ptr<ObjectList> {\n\tauto &lifetime = parent->lifetime();\n\tconst auto delegate = lifetime.make_state<\n\t\tPeerListContentDelegateSimple\n\t>();\n\tcontroller->setStyleOverrides(&st::recentPeersList);\n\n\tauto content = object_ptr<PeerListContent>(parent, controller);\n\tconst auto list = content.data();\n\n\tauto result = std::make_unique<ObjectList>(ObjectList{\n\t\t.wrap = parent->add(object_ptr<Ui::SlideWrap<PeerListContent>>(\n\t\t\tparent,\n\t\t\tstd::move(content))),\n\t});\n\tconst auto raw = result.get();\n\n\traw->count = controller->count();\n\traw->processTouch = [=](not_null<QTouchEvent*> e) {\n\t\treturn controller->processTouchEvent(e);\n\t};\n\n\tcontroller->chosen(\n\t) | rpl::on_next([=](not_null<PeerData*> peer) {\n\t\traw->chosen.fire_copy(peer);\n\t}, lifetime);\n\n\traw->choose = [=] {\n\t\treturn list->submitted();\n\t};\n\traw->updateFromParentDrag = [=](QPoint globalPosition) {\n\t\treturn list->updateFromParentDrag(globalPosition);\n\t};\n\traw->dragLeft = [=] {\n\t\tlist->dragLeft();\n\t};\n\n\tlist->scrollToRequests(\n\t) | rpl::on_next([=](Ui::ScrollToRequest request) {\n\t\tconst auto add = addToScroll ? addToScroll() : 0;\n\t\tscroll->scrollToY(request.ymin + add, request.ymax + add);\n\t}, list->lifetime());\n\n\tdelegate->setContent(list);\n\tcontroller->setDelegate(delegate);\n\tcontroller->setupTouchChatPreview(scroll);\n\n\treturn result;\n}\n\nobject_ptr<Ui::SlideWrap<>> Suggestions::setupEmptyChannels() {\n\tconst auto icon = SearchEmptyIcon::NoResults;\n\treturn setupEmpty(_channelsContent, icon, tr::lng_channels_none_about());\n}\n\nobject_ptr<Ui::SlideWrap<>> Suggestions::setupEmpty(\n\t\tnot_null<QWidget*> parent,\n\t\tSearchEmptyIcon icon,\n\t\trpl::producer<QString> text) {\n\tauto content = object_ptr<SearchEmpty>(\n\t\tparent,\n\t\ticon,\n\t\tstd::move(text) | rpl::map(tr::marked));\n\n\tconst auto raw = content.data();\n\trpl::combine(\n\t\t_chatsScroll->heightValue(),\n\t\t_topPeersWrap->heightValue()\n\t) | rpl::on_next([=](int height, int top) {\n\t\traw->setMinimalHeight(height - top);\n\t}, raw->lifetime());\n\n\tauto result = object_ptr<Ui::SlideWrap<>>(\n\t\tparent,\n\t\tstd::move(content));\n\tresult->toggle(false, anim::type::instant);\n\n\tresult->toggledValue() | rpl::filter([=](bool shown) {\n\t\treturn shown && _controller->session().data().chatsListLoaded();\n\t}) | rpl::on_next([=] {\n\t\traw->animate();\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\nbool Suggestions::persist() const {\n\treturn _persist;\n}\n\nvoid Suggestions::clearPersistance() {\n\t_persist = false;\n}\n\nrpl::producer<TopPeersList> TopPeersContent(\n\t\tnot_null<Main::Session*> session) {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tstruct Entry {\n\t\t\tnot_null<History*> history;\n\t\t\tint index = 0;\n\t\t};\n\t\tstruct State {\n\t\t\tTopPeersList data;\n\t\t\tbase::flat_map<not_null<PeerData*>, Entry> indices;\n\t\t\tbase::has_weak_ptr guard;\n\t\t\tbool scheduled = true;\n\t\t};\n\t\tauto state = lifetime.make_state<State>();\n\t\tconst auto top = session->topPeers().list();\n\t\tauto &entries = state->data.entries;\n\t\tauto &indices = state->indices;\n\t\tentries.reserve(top.size());\n\t\tindices.reserve(top.size());\n\t\tconst auto now = base::unixtime::now();\n\t\tfor (const auto &peer : top) {\n\t\t\tconst auto user = peer->asUser();\n\t\t\tif (user->isInaccessible()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto self = user && user->isSelf();\n\t\t\tconst auto history = peer->owner().history(peer);\n\t\t\tconst auto badges = history->chatListBadgesState();\n\t\t\tentries.push_back({\n\t\t\t\t.id = peer->id.value,\n\t\t\t\t.name = (self\n\t\t\t\t\t? tr::lng_saved_messages(tr::now)\n\t\t\t\t\t: peer->shortName()),\n\t\t\t\t.userpic = (self\n\t\t\t\t\t? Ui::MakeSavedMessagesThumbnail()\n\t\t\t\t\t: Ui::MakeUserpicThumbnail(peer)),\n\t\t\t\t.badge = uint32(badges.unreadCounter),\n\t\t\t\t.unread = badges.unread,\n\t\t\t\t.muted = !self && history->muted(),\n\t\t\t\t.online = user && !self && Data::IsUserOnline(user, now),\n\t\t\t});\n\t\t\tif (entries.back().online) {\n\t\t\t\tuser->owner().watchForOffline(user, now);\n\t\t\t}\n\t\t\tindices.emplace(peer, Entry{\n\t\t\t\t.history = peer->owner().history(peer),\n\t\t\t\t.index = int(entries.size()) - 1,\n\t\t\t});\n\t\t}\n\n\t\tconst auto push = [=] {\n\t\t\tif (!state->scheduled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->scheduled = false;\n\t\t\tconsumer.put_next_copy(state->data);\n\t\t};\n\t\tconst auto schedule = [=] {\n\t\t\tif (state->scheduled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->scheduled = true;\n\t\t\tcrl::on_main(&state->guard, push);\n\t\t};\n\n\t\tusing Flag = Data::PeerUpdate::Flag;\n\t\tsession->changes().peerUpdates(\n\t\t\tFlag::Name\n\t\t\t| Flag::Photo\n\t\t\t| Flag::Notifications\n\t\t\t| Flag::OnlineStatus\n\t\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\t\tconst auto peer = update.peer;\n\t\t\tif (peer->isSelf()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto i = state->indices.find(peer);\n\t\t\tif (i == end(state->indices)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto changed = false;\n\t\t\tauto &entry = state->data.entries[i->second.index];\n\t\t\tconst auto flags = update.flags;\n\t\t\tif (flags & Flag::Name) {\n\t\t\t\tconst auto now = peer->shortName();\n\t\t\t\tif (entry.name != now) {\n\t\t\t\t\tentry.name = now;\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (flags & Flag::Photo) {\n\t\t\t\tentry.userpic = Ui::MakeUserpicThumbnail(peer);\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t\tif (flags & Flag::Notifications) {\n\t\t\t\tconst auto now = i->second.history->muted();\n\t\t\t\tif (entry.muted != now) {\n\t\t\t\t\tentry.muted = now;\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (flags & Flag::OnlineStatus) {\n\t\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\t\tconst auto now = base::unixtime::now();\n\t\t\t\t\tconst auto value = Data::IsUserOnline(user, now);\n\t\t\t\t\tif (entry.online != value) {\n\t\t\t\t\t\tentry.online = value;\n\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\tif (value) {\n\t\t\t\t\t\t\tuser->owner().watchForOffline(user, now);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (changed) {\n\t\t\t\tschedule();\n\t\t\t}\n\t\t}, lifetime);\n\n\t\tsession->data().unreadBadgeChanges(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto changed = false;\n\t\t\tauto &entries = state->data.entries;\n\t\t\tfor (const auto &[peer, data] : state->indices) {\n\t\t\t\tconst auto badges = data.history->chatListBadgesState();\n\t\t\t\tauto &entry = entries[data.index];\n\t\t\t\tif (entry.badge != badges.unreadCounter\n\t\t\t\t\t|| entry.unread != badges.unread) {\n\t\t\t\t\tentry.badge = badges.unreadCounter;\n\t\t\t\t\tentry.unread = badges.unread;\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (changed) {\n\t\t\t\tschedule();\n\t\t\t}\n\t\t}, lifetime);\n\n\t\tpush();\n\t\treturn lifetime;\n\t};\n}\n\nRecentPeersList RecentPeersContent(not_null<Main::Session*> session) {\n\treturn RecentPeersList{ session->recentPeers().list() };\n}\n\nobject_ptr<Ui::BoxContent> StarsExamplesBox(\n\t\tnot_null<Window::SessionController*> window) {\n\tauto controller = std::make_unique<PopularAppsController>(\n\t\twindow,\n\t\tnullptr,\n\t\tnullptr);\n\tconst auto raw = controller.get();\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tbox->setTitle(tr::lng_credits_box_history_entry_gift_examples());\n\t\tbox->addButton(tr::lng_close(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\n\t\traw->load();\n\t\traw->chosen() | rpl::on_next([=](not_null<PeerData*> peer) {\n\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\tif (const auto info = user->botInfo.get()) {\n\t\t\t\t\tif (info->hasMainApp) {\n\t\t\t\t\t\twindow->session().attachWebView().open({\n\t\t\t\t\t\t\t.bot = user,\n\t\t\t\t\t\t\t.context = {\n\t\t\t\t\t\t\t\t.controller = window,\n\t\t\t\t\t\t\t\t.maySkipConfirmation = true,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t.source = InlineBots::WebViewSourceBotProfile(),\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\twindow->showPeerInfo(peer);\n\t\t}, box->lifetime());\n\t};\n\treturn Box<PeerListBox>(std::move(controller), std::move(initBox));\n}\n\nobject_ptr<Ui::BoxContent> PopularAppsAboutBox(\n\t\tnot_null<Window::SessionController*> window) {\n\treturn Ui::MakeInformBox({\n\t\t.text = tr::lng_popular_apps_info_text(\n\t\t\tlt_bot,\n\t\t\trpl::single(tr::link(\n\t\t\t\tu\"@botfather\"_q,\n\t\t\t\tu\"https://t.me/botfather\"_q)),\n\t\t\tlt_link,\n\t\t\ttr::lng_popular_apps_info_here(\n\t\t\t\ttr::url(tr::lng_popular_apps_info_url(tr::now))),\n\t\t\ttr::rich),\n\t\t.confirmText = tr::lng_popular_apps_info_confirm(),\n\t\t.title = tr::lng_popular_apps_info_title(),\n\t});\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"base/timer.h\"\n#include \"dialogs/ui/top_peers_strip.h\"\n#include \"ui/controls/swipe_handler_data.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/rp_widget.h\"\n\nclass PeerListContent;\n\nnamespace Data {\nclass Thread;\n} // namespace Data\n\nnamespace Info {\nclass WrapWidget;\n} // namespace Info\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Storage {\nenum class SharedMediaType : signed char;\n} // namespace Storage\n\nnamespace Ui::Controls {\nstruct SwipeHandlerArgs;\n} // namespace Ui::Controls\n\nnamespace Ui {\nclass AbstractButton;\nclass BoxContent;\nclass ScrollArea;\nclass ElasticScroll;\nclass SettingsSlider;\nclass VerticalLayout;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Dialogs {\n\nclass InnerWidget;\nclass PostsSearch;\nclass PostsSearchIntro;\nstruct PostsSearchIntroState;\nenum class SearchEmptyIcon;\n\nstruct RecentPeersList {\n\tstd::vector<not_null<PeerData*>> list;\n};\n\nclass Suggestions final : public Ui::RpWidget {\npublic:\n\tSuggestions(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\trpl::producer<TopPeersList> topPeers,\n\t\tRecentPeersList recentPeers);\n\t~Suggestions();\n\n\tvoid selectJump(Qt::Key direction, int pageSize = 0);\n\tvoid chooseRow();\n\n\tbool consumeSearchQuery(const QString &query);\n\t[[nodiscard]] rpl::producer<> clearSearchQueryRequests() const;\n\n\t[[nodiscard]] Data::Thread *updateFromParentDrag(QPoint globalPosition);\n\tvoid dragLeft();\n\n\tvoid show(anim::type animated, Fn<void()> finish);\n\tvoid hide(anim::type animated, Fn<void()> finish);\n\t[[nodiscard]] float64 shownOpacity() const;\n\n\t[[nodiscard]] bool persist() const;\n\tvoid clearPersistance();\n\n\t[[nodiscard]] rpl::producer<not_null<PeerData*>> topPeerChosen() const {\n\t\treturn _topPeerChosen.events();\n\t}\n\t[[nodiscard]] auto recentPeerChosen() const\n\t-> rpl::producer<not_null<PeerData*>> {\n\t\treturn _recent->chosen.events();\n\t}\n\t[[nodiscard]] auto myChannelChosen() const\n\t-> rpl::producer<not_null<PeerData*>> {\n\t\treturn _myChannels->chosen.events();\n\t}\n\t[[nodiscard]] auto recommendationChosen() const\n\t-> rpl::producer<not_null<PeerData*>> {\n\t\treturn _recommendations->chosen.events();\n\t}\n\t[[nodiscard]] auto recentAppChosen() const\n\t-> rpl::producer<not_null<PeerData*>> {\n\t\treturn _recentApps->chosen.events();\n\t}\n\t[[nodiscard]] auto popularAppChosen() const\n\t-> rpl::producer<not_null<PeerData*>> {\n\t\treturn _popularApps->chosen.events();\n\t}\n\t[[nodiscard]] auto openBotMainAppRequests() const\n\t-> rpl::producer<not_null<PeerData*>> {\n\t\treturn _openBotMainAppRequests.events();\n\t}\n\t[[nodiscard]] rpl::producer<> closeRequests() const {\n\t\treturn _closeRequests.events();\n\t}\n\n\tclass ObjectListController;\n\nprivate:\n\tusing MediaType = Storage::SharedMediaType;\n\tenum class Tab : uchar {\n\t\tChats,\n\t\tChannels,\n\t\tApps,\n\t\tPosts,\n\t\tMedia,\n\t\tDownloads,\n\t};\n\tenum class JumpResult : uchar {\n\t\tNotApplied,\n\t\tApplied,\n\t\tAppliedAndOut,\n\t};\n\n\tstruct Key {\n\t\tTab tab = Tab::Chats;\n\t\tMediaType mediaType = {};\n\n\t\tfriend inline auto operator<=>(Key, Key) = default;\n\t\tfriend inline bool operator==(Key, Key) = default;\n\t};\n\n\tstruct ObjectList {\n\t\tnot_null<Ui::SlideWrap<PeerListContent>*> wrap;\n\t\trpl::variable<int> count;\n\t\tFn<bool()> choose;\n\t\tFn<JumpResult(Qt::Key, int)> selectJump;\n\t\tFn<uint64(QPoint)> updateFromParentDrag;\n\t\tFn<void()> dragLeft;\n\t\tFn<bool(not_null<QTouchEvent*>)> processTouch;\n\t\trpl::event_stream<not_null<PeerData*>> chosen;\n\t};\n\n\tstruct MediaList {\n\t\tInfo::WrapWidget *wrap = nullptr;\n\t\trpl::variable<int> count;\n\t};\n\n\t[[nodiscard]] static std::vector<Key> TabKeysFor(\n\t\tnot_null<Window::SessionController*> controller);\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\tvoid setupLocalUrlButton();\n\tvoid setupTabs();\n\tvoid setupChats();\n\tvoid setupChannels();\n\tvoid setupApps();\n\tvoid reinstallSwipe(not_null<Ui::ElasticScroll*>);\n\t[[nodiscard]] auto generateIncompleteSwipeArgs()\n\t-> Ui::Controls::SwipeHandlerArgs;\n\n\tvoid selectJumpChats(Qt::Key direction, int pageSize);\n\tvoid selectJumpChannels(Qt::Key direction, int pageSize);\n\tvoid selectJumpApps(Qt::Key direction, int pageSize);\n\n\t[[nodiscard]] Data::Thread *updateFromChatsDrag(QPoint globalPosition);\n\t[[nodiscard]] Data::Thread *updateFromChannelsDrag(\n\t\tQPoint globalPosition);\n\t[[nodiscard]] Data::Thread *updateFromAppsDrag(QPoint globalPosition);\n\t[[nodiscard]] Data::Thread *fromListId(uint64 peerListRowId);\n\n\t[[nodiscard]] std::unique_ptr<ObjectList> setupRecentPeers(\n\t\tRecentPeersList recentPeers);\n\t[[nodiscard]] auto setupEmptyRecent()\n\t\t-> object_ptr<Ui::SlideWrap<Ui::RpWidget>>;\n\n\t[[nodiscard]] std::unique_ptr<ObjectList> setupMyChannels();\n\t[[nodiscard]] std::unique_ptr<ObjectList> setupRecommendations();\n\t[[nodiscard]] auto setupEmptyChannels()\n\t\t-> object_ptr<Ui::SlideWrap<Ui::RpWidget>>;\n\n\t[[nodiscard]] std::unique_ptr<ObjectList> setupRecentApps();\n\t[[nodiscard]] std::unique_ptr<ObjectList> setupPopularApps();\n\n\t[[nodiscard]] std::unique_ptr<ObjectList> setupObjectList(\n\t\tnot_null<Ui::ElasticScroll*> scroll,\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tnot_null<ObjectListController*> controller,\n\t\tFn<int()> addToScroll = nullptr);\n\n\t[[nodiscard]] object_ptr<Ui::SlideWrap<Ui::RpWidget>> setupEmpty(\n\t\tnot_null<QWidget*> parent,\n\t\tSearchEmptyIcon icon,\n\t\trpl::producer<QString> text);\n\n\tvoid switchTab(Key key);\n\tvoid startShownAnimation(bool shown, Fn<void()> finish);\n\tvoid startSlideAnimation(Key was, Key now);\n\tvoid ensureContent(Key key);\n\tvoid finishShow();\n\n\tvoid handlePressForChatPreview(PeerId id, Fn<void(bool)> callback);\n\tvoid updateControlsGeometry();\n\tvoid applySearchQuery();\n\n\tvoid setupPostsSearch();\n\tvoid setPostsSearchQuery(const QString &query);\n\tvoid setupPostsResults();\n\tvoid setupPostsIntro(const PostsSearchIntroState &intro);\n\tvoid updatePostsSearchVisibleRange();\n\n\tconst not_null<Window::SessionController*> _controller;\n\n\tbase::unique_qptr<Ui::AbstractButton> _localUrlButton;\n\tconst std::unique_ptr<Ui::ScrollArea> _tabsScroll;\n\tconst not_null<Ui::SettingsSlider*> _tabs;\n\tUi::Animations::Simple _tabsScrollAnimation;\n\tconst std::vector<Key> _tabKeys;\n\trpl::variable<Key> _key;\n\n\tconst std::unique_ptr<Ui::ElasticScroll> _chatsScroll;\n\tconst not_null<Ui::VerticalLayout*> _chatsContent;\n\n\tconst not_null<Ui::SlideWrap<TopPeersStrip>*> _topPeersWrap;\n\tconst not_null<TopPeersStrip*> _topPeers;\n\trpl::event_stream<not_null<PeerData*>> _topPeerChosen;\n\trpl::event_stream<not_null<PeerData*>> _openBotMainAppRequests;\n\trpl::event_stream<> _closeRequests;\n\n\tconst std::unique_ptr<ObjectList> _recent;\n\n\tconst not_null<Ui::SlideWrap<Ui::RpWidget>*> _emptyRecent;\n\n\tconst std::unique_ptr<Ui::ElasticScroll> _channelsScroll;\n\tconst not_null<Ui::VerticalLayout*> _channelsContent;\n\n\tconst std::unique_ptr<ObjectList> _myChannels;\n\tconst std::unique_ptr<ObjectList> _recommendations;\n\n\tconst not_null<Ui::SlideWrap<Ui::RpWidget>*> _emptyChannels;\n\n\tconst std::unique_ptr<Ui::ElasticScroll> _appsScroll;\n\tconst not_null<Ui::VerticalLayout*> _appsContent;\n\n\tstd::unique_ptr<PostsSearch> _postsSearch;\n\tconst std::unique_ptr<Ui::ElasticScroll> _postsScroll;\n\tconst not_null<Ui::RpWidget*> _postsWrap;\n\tPostsSearchIntro *_postsSearchIntro = nullptr;\n\tInnerWidget *_postsContent = nullptr;\n\n\trpl::producer<> _recentAppsRefreshed;\n\tFn<bool(not_null<PeerData*>)> _recentAppsShows;\n\tconst std::unique_ptr<ObjectList> _recentApps;\n\tconst std::unique_ptr<ObjectList> _popularApps;\n\n\tbase::flat_map<Key, MediaList> _mediaLists;\n\trpl::event_stream<> _clearSearchQueryRequests;\n\tQString _searchQuery;\n\tbase::Timer _searchQueryTimer;\n\n\tUi::Animations::Simple _shownAnimation;\n\tFn<void()> _showFinished;\n\tbool _hidden = false;\n\tbool _persist = false;\n\tQPixmap _cache;\n\n\tUi::Animations::Simple _slideAnimation;\n\tQPixmap _slideLeft;\n\tQPixmap _slideRight;\n\n\tUi::Controls::SwipeBackResult _swipeBackData;\n\trpl::lifetime _swipeLifetime;\n\n\tint _slideLeftTop = 0;\n\tint _slideRightTop = 0;\n\n};\n\n[[nodiscard]] rpl::producer<TopPeersList> TopPeersContent(\n\tnot_null<Main::Session*> session);\n\n[[nodiscard]] RecentPeersList RecentPeersContent(\n\tnot_null<Main::Session*> session);\n\n[[nodiscard]] object_ptr<Ui::BoxContent> StarsExamplesBox(\n\tnot_null<Window::SessionController*> window);\n\n[[nodiscard]] object_ptr<Ui::BoxContent> PopularAppsAboutBox(\n\tnot_null<Window::SessionController*> window);\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/ui/dialogs_top_bar_suggestion_content.h\"\n\n#include \"base/call_delayed.h\"\n#include \"data/data_authorization.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/ui_rpl_filter.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Dialogs {\n\nclass UnconfirmedAuthWrap : public Ui::SlideWrap<Ui::VerticalLayout> {\npublic:\n\tUnconfirmedAuthWrap(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tobject_ptr<Ui::VerticalLayout> &&child)\n\t: Ui::SlideWrap<Ui::VerticalLayout>(parent, std::move(child)) {\n\t}\n\n\trpl::producer<int> desiredHeightValue() const override {\n\t\treturn entity()->heightValue();\n\t}\n};\n\nnot_null<Ui::SlideWrap<Ui::VerticalLayout>*> CreateUnconfirmedAuthContent(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst std::vector<Data::UnreviewedAuth> &list,\n\t\tFn<void(bool)> callback) {\n\tconst auto wrap = Ui::CreateChild<UnconfirmedAuthWrap>(\n\t\tparent,\n\t\tobject_ptr<Ui::VerticalLayout>(parent));\n\tconst auto content = wrap->entity();\n\tcontent->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(content);\n\t\tp.fillRect(content->rect(), st::dialogsBg);\n\t}, content->lifetime());\n\n\tconst auto padding = st::dialogsUnconfirmedAuthPadding;\n\n\tUi::AddSkip(content);\n\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_unconfirmed_auth_title(),\n\t\t\tst::dialogsUnconfirmedAuthTitle),\n\t\tpadding,\n\t\tstyle::al_top);\n\n\tUi::AddSkip(content);\n\n\tauto messageText = QString();\n\tif (list.size() == 1) {\n\t\tconst auto &auth = list.at(0);\n\t\tmessageText = tr::lng_unconfirmed_auth_single(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tauth.device,\n\t\t\tlt_country,\n\t\t\tauth.location);\n\t} else {\n\t\tauto commonLocation = list.at(0).location;\n\t\tfor (auto i = 1; i < list.size(); ++i) {\n\t\t\tif (commonLocation != list.at(i).location) {\n\t\t\t\tcommonLocation.clear();\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (commonLocation.isEmpty()) {\n\t\t\tmessageText = tr::lng_unconfirmed_auth_multiple(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tlist.size());\n\t\t} else {\n\t\t\tmessageText = tr::lng_unconfirmed_auth_multiple_from(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tlist.size(),\n\t\t\t\tlt_country,\n\t\t\t\tcommonLocation);\n\t\t}\n\t}\n\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\trpl::single(messageText),\n\t\t\tst::dialogsUnconfirmedAuthAbout),\n\t\tpadding,\n\t\tstyle::al_top)->setTryMakeSimilarLines(true);\n\n\tUi::AddSkip(content);\n\tconst auto buttons = content->add(object_ptr<Ui::FixedHeightWidget>(\n\t\tcontent,\n\t\tst::dialogsUnconfirmedAuthButton.height));\n\tconst auto yes = Ui::CreateChild<Ui::RoundButton>(\n\t\tbuttons,\n\t\ttr::lng_unconfirmed_auth_confirm(),\n\t\tst::dialogsUnconfirmedAuthButton);\n\tconst auto no = Ui::CreateChild<Ui::RoundButton>(\n\t\tbuttons,\n\t\ttr::lng_unconfirmed_auth_deny(),\n\t\tst::dialogsUnconfirmedAuthButtonNo);\n\tyes->setClickedCallback([=] {\n\t\twrap->toggle(false, anim::type::normal);\n\t\tbase::call_delayed(st::universalDuration, wrap, [=] {\n\t\t\tcallback(true);\n\t\t});\n\t});\n\tno->setClickedCallback([=] {\n\t\twrap->toggle(false, anim::type::normal);\n\t\tbase::call_delayed(st::universalDuration, wrap, [=] {\n\t\t\tcallback(false);\n\t\t});\n\t});\n\tbuttons->sizeValue(\n\t) | rpl::filter_size(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tconst auto halfWidth = (s.width() - rect::m::sum::h(padding)) / 2;\n\t\tyes->moveToLeft(\n\t\t\tpadding.left() + (halfWidth - yes->width()) / 2,\n\t\t\t0);\n\t\tno->moveToLeft(\n\t\t\tpadding.left() + halfWidth + (halfWidth - no->width()) / 2,\n\t\t\t0);\n\t}, buttons->lifetime());\n\tUi::AddSkip(content);\n\tcontent->add(object_ptr<Ui::FadeShadow>(content));\n\n\treturn wrap;\n}\n\nvoid ShowAuthDeniedBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tfloat64 count,\n\t\tconst QString &messageText) {\n\tbox->setStyle(st::showOrBox);\n\tbox->setWidth(st::boxWideWidth);\n\tconst auto buttonPadding = QMargins(\n\t\tst::showOrBox.buttonPadding.left(),\n\t\t0,\n\t\tst::showOrBox.buttonPadding.right(),\n\t\t0);\n\tauto icon = Settings::CreateLottieIcon(\n\t\tbox,\n\t\t{\n\t\t\t.name = u\"ban\"_q,\n\t\t\t.sizeOverride = st::dialogsSuggestionDeniedAuthLottie,\n\t\t},\n\t\tst::dialogsSuggestionDeniedAuthLottieMargins);\n\tSettings::AddLottieIconWithCircle(\n\t\tbox->verticalLayout(),\n\t\tstd::move(icon.widget),\n\t\tst::settingsBlockedListIconPadding,\n\t\tst::dialogsSuggestionDeniedAuthLottieCircle);\n\tbox->setShowFinishedCallback([=, animate = std::move(icon.animate)] {\n\t\tanimate(anim::repeat::once);\n\t});\n\tUi::AddSkip(box->verticalLayout());\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_unconfirmed_auth_denied_title(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(count)),\n\t\t\tst::boostCenteredTitle),\n\t\tst::showOrTitlePadding + buttonPadding,\n\t\tstyle::al_top);\n\tUi::AddSkip(box->verticalLayout());\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\tmessageText,\n\t\t\tst::boostText),\n\t\tst::showOrAboutPadding + buttonPadding,\n\t\tstyle::al_top);\n\tUi::AddSkip(box->verticalLayout());\n\tconst auto warning = box->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_unconfirmed_auth_denied_warning(tr::bold),\n\t\t\tst::boostText),\n\t\tst::showOrAboutPadding + buttonPadding\n\t\t\t+ QMargins(st::boostTextSkip, 0, st::boostTextSkip, 0),\n\t\tstyle::al_top);\n\twarning->setTextColorOverride(st::attentionButtonFg->c);\n\tconst auto warningBg = Ui::CreateChild<Ui::RpWidget>(\n\t\tbox->verticalLayout());\n\twarning->geometryValue() | rpl::on_next([=](QRect r) {\n\t\twarningBg->setGeometry(r + Margins(st::boostTextSkip));\n\t}, warningBg->lifetime());\n\twarningBg->paintOn([=](QPainter &p) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::attentionButtonBgOver);\n\t\tp.drawRoundedRect(\n\t\t\twarningBg->rect(),\n\t\t\tst::buttonRadius,\n\t\t\tst::buttonRadius);\n\t});\n\twarningBg->show();\n\twarning->raise();\n\twarningBg->stackUnder(warning);\n\tconst auto confirm = box->addButton(\n\t\tobject_ptr<Ui::RoundButton>(\n\t\t\tbox,\n\t\t\trpl::single(QString()),\n\t\t\tst::defaultActiveButton));\n\tconfirm->setClickedCallback([=] {\n\t\tbox->closeBox();\n\t});\n\tconfirm->resize(\n\t\tst::showOrShowButton.width,\n\t\tst::showOrShowButton.height);\n\n\tconst auto textLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\tconfirm,\n\t\ttr::lng_archive_hint_button(),\n\t\tst::defaultSubsectionTitle);\n\ttextLabel->setTextColorOverride(st::defaultActiveButton.textFg->c);\n\ttextLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto timerLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\tconfirm,\n\t\trpl::single(QString()),\n\t\tst::defaultSubsectionTitle);\n\ttimerLabel->setTextColorOverride(\n\t\tanim::with_alpha(st::defaultActiveButton.textFg->c, 0.75));\n\ttimerLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconstexpr auto kTimer = 5;\n\tconst auto remaining = confirm->lifetime().make_state<int>(kTimer);\n\tconst auto timerLifetime\n\t\t= confirm->lifetime().make_state<rpl::lifetime>();\n\tconst auto timer = timerLifetime->make_state<base::Timer>([=] {\n\t\tif ((*remaining) > 0) {\n\t\t\ttimerLabel->setText(QString::number((*remaining)--));\n\t\t} else {\n\t\t\ttimerLabel->hide();\n\t\t\tconfirm->setAttribute(Qt::WA_TransparentForMouseEvents, false);\n\t\t\tbox->setCloseByEscape(true);\n\t\t\tbox->setCloseByOutsideClick(true);\n\t\t\ttimerLifetime->destroy();\n\t\t}\n\t});\n\tbox->setCloseByEscape(false);\n\tbox->setCloseByOutsideClick(false);\n\tconfirm->setAttribute(Qt::WA_TransparentForMouseEvents, true);\n\ttimerLabel->setText(QString::number((*remaining)));\n\ttimer->callEach(1000);\n\n\trpl::combine(\n\t\tconfirm->sizeValue(),\n\t\ttextLabel->sizeValue(),\n\t\ttimerLabel->sizeValue(),\n\t\ttimerLabel->shownValue()\n\t) | rpl::on_next([=](QSize btn, QSize text, QSize timer, bool shown) {\n\t\tconst auto skip = st::normalFont->spacew;\n\t\tconst auto totalWidth = shown\n\t\t\t? (text.width() + skip + timer.width())\n\t\t\t: text.width();\n\t\tconst auto left = (btn.width() - totalWidth) / 2;\n\t\ttextLabel->moveToLeft(left, (btn.height() - text.height()) / 2);\n\t\ttimerLabel->moveToLeft(\n\t\t\tleft + text.width() + skip,\n\t\t\t(btn.height() - timer.height()) / 2);\n\t}, confirm->lifetime());\n}\n\nTopBarSuggestionContent::TopBarSuggestionContent(\n\tnot_null<Ui::RpWidget*> parent,\n\tFn<bool()> emojiPaused)\n: Ui::RippleButton(parent, st::defaultRippleAnimationBgOver)\n, _titleSt(st::semiboldTextStyle)\n, _contentTitleSt(st::dialogsTopBarSuggestionTitleStyle)\n, _contentTextSt(st::dialogsTopBarSuggestionAboutStyle)\n, _emojiPaused(std::move(emojiPaused)) {\n\tsetRightIcon(RightIcon::Close);\n}\n\nvoid TopBarSuggestionContent::setRightIcon(RightIcon icon) {\n\t_rightButton = nullptr;\n\tif (icon == _rightIcon) {\n\t\treturn;\n\t}\n\t_rightHide = nullptr;\n\t_rightArrow = nullptr;\n\t_rightIcon = icon;\n\tif (icon == RightIcon::Close) {\n\t\t_rightHide = base::make_unique_q<Ui::IconButton>(\n\t\t\tthis,\n\t\t\tst::dialogsCancelSearchInPeer);\n\t\tconst auto rightHide = _rightHide.get();\n\t\tsizeValue() | rpl::filter_size(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\trightHide->moveToRight(st::buttonRadius, st::lineWidth);\n\t\t}, rightHide->lifetime());\n\t\trightHide->show();\n\t} else if (icon == RightIcon::Arrow) {\n\t\t_rightArrow = base::make_unique_q<Ui::IconButton>(\n\t\t\tthis,\n\t\t\tst::backButton);\n\t\tconst auto arrow = _rightArrow.get();\n\t\tarrow->setIconOverride(\n\t\t\t&st::settingsPremiumArrow,\n\t\t\t&st::settingsPremiumArrowOver);\n\t\tarrow->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tsizeValue() | rpl::filter_size(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\tconst auto &point = st::settingsPremiumArrowShift;\n\t\t\tarrow->moveToLeft(\n\t\t\t\ts.width() - arrow->width(),\n\t\t\t\tpoint.y() + (s.height() - arrow->height()) / 2);\n\t\t}, arrow->lifetime());\n\t\tarrow->show();\n\t}\n}\n\nvoid TopBarSuggestionContent::setRightButton(\n\t\trpl::producer<TextWithEntities> text,\n\t\tFn<void()> callback) {\n\t_rightHide = nullptr;\n\t_rightArrow = nullptr;\n\t_rightIcon = RightIcon::None;\n\tif (!text) {\n\t\t_rightButton = nullptr;\n\t\treturn;\n\t}\n\tusing namespace Ui;\n\t_rightButton = base::make_unique_q<RoundButton>(\n\t\tthis,\n\t\trpl::single(QString()),\n\t\tst::dialogsTopBarRightButton);\n\t_rightButton->setText(std::move(text));\n\trpl::combine(\n\t\tsizeValue(),\n\t\t_rightButton->sizeValue()\n\t) | rpl::on_next([=](QSize outer, QSize inner) {\n\t\tconst auto top = (outer.height() - inner.height()) / 2;\n\t\t_rightButton->moveToRight(top, top, outer.width());\n\t}, _rightButton->lifetime());\n\t_rightButton->setFullRadius(true);\n\t_rightButton->setClickedCallback(std::move(callback));\n\t_rightButton->show();\n}\n\nvoid TopBarSuggestionContent::draw(QPainter &p) {\n\tconst auto kLinesForPhoto = 3;\n\n\tconst auto r = Ui::RpWidget::rect();\n\tp.fillRect(r, st::historyPinnedBg);\n\tp.fillRect(\n\t\tr.x(),\n\t\tr.y() + r.height() - st::lineWidth,\n\t\tr.width(),\n\t\tst::lineWidth,\n\t\tst::shadowFg);\n\tUi::RippleButton::paintRipple(p, 0, 0);\n\tconst auto leftPadding = _leftPadding;\n\tconst auto rightPadding = 0;\n\tconst auto topPadding = st::msgReplyPadding.top();\n\tconst auto availableWidthNoPhoto = r.width()\n\t\t- (_rightArrow\n\t\t\t? (_rightArrow->width() / 4 * 3) // Takes full height.\n\t\t\t: 0)\n\t\t- leftPadding\n\t\t- rightPadding;\n\tconst auto availableWidth = availableWidthNoPhoto\n\t\t- (_rightHide ? _rightHide->width() : 0);\n\tconst auto titleRight = leftPadding;\n\tconst auto hasSecondLineTitle = availableWidth < _contentTitle.maxWidth();\n\tconst auto paused = On(PowerSaving::kEmojiChat)\n\t\t|| (_emojiPaused && _emojiPaused());\n\tp.setPen(st::windowActiveTextFg);\n\tp.setPen(st::windowFg);\n\t{\n\t\tconst auto left = leftPadding;\n\t\tconst auto top = topPadding;\n\t\t_contentTitle.draw(p, {\n\t\t\t.position = QPoint(left, top),\n\t\t\t.outerWidth = hasSecondLineTitle\n\t\t\t\t? availableWidth\n\t\t\t\t: (availableWidth - titleRight),\n\t\t\t.availableWidth = availableWidth,\n\t\t\t.pausedEmoji = paused,\n\t\t\t.elisionLines = hasSecondLineTitle ? 2 : 1,\n\t\t});\n\t}\n\t{\n\t\tconst auto left = leftPadding;\n\t\tconst auto top = hasSecondLineTitle\n\t\t\t? (topPadding\n\t\t\t\t+ _titleSt.font->height\n\t\t\t\t+ _contentTitleSt.font->height)\n\t\t\t: topPadding + _titleSt.font->height;\n\t\tauto lastContentLineAmount = 0;\n\t\tconst auto lineHeight = _contentTextSt.font->height;\n\t\tconst auto lineLayout = [&](int line) -> Ui::Text::LineGeometry {\n\t\t\tline++;\n\t\t\tlastContentLineAmount = line;\n\t\t\tconst auto diff = (st::sponsoredMessageBarMaxHeight)\n\t\t\t\t- line * lineHeight;\n\t\t\tif (diff < 3 * lineHeight) {\n\t\t\t\treturn {\n\t\t\t\t\t.width = availableWidthNoPhoto,\n\t\t\t\t\t.elided = true,\n\t\t\t\t};\n\t\t\t} else if (diff < 2 * lineHeight) {\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\tline += (hasSecondLineTitle ? 2 : 1) + 1;\n\t\t\treturn {\n\t\t\t\t.width = (line > kLinesForPhoto)\n\t\t\t\t\t? availableWidthNoPhoto\n\t\t\t\t\t: availableWidth,\n\t\t\t};\n\t\t};\n\t\tp.setPen(_descriptionColorOverride.value_or(st::windowSubTextFg->c));\n\t\t_contentText.draw(p, {\n\t\t\t.position = QPoint(left, top),\n\t\t\t.outerWidth = availableWidth,\n\t\t\t.availableWidth = availableWidth,\n\t\t\t.geometry = Ui::Text::GeometryDescriptor{\n\t\t\t\t.layout = std::move(lineLayout),\n\t\t\t},\n\t\t\t.pausedEmoji = paused,\n\t\t});\n\t\t_lastPaintedContentTop = top;\n\t\t_lastPaintedContentLineAmount = lastContentLineAmount;\n\t}\n}\n\nvoid TopBarSuggestionContent::setContent(\n\t\tTextWithEntities title,\n\t\tTextWithEntities description,\n\t\tstd::optional<Ui::Text::MarkedContext> context,\n\t\tstd::optional<QColor> descriptionColorOverride) {\n\t_descriptionColorOverride = descriptionColorOverride;\n\tif (context) {\n\t\tcontext->repaint = [=] { update(); };\n\t\t_contentTitle.setMarkedText(\n\t\t\t_contentTitleSt,\n\t\t\tstd::move(title),\n\t\t\tkMarkupTextOptions,\n\t\t\t*context);\n\t\t_contentText.setMarkedText(\n\t\t\t_contentTextSt,\n\t\t\tstd::move(description),\n\t\t\tkMarkupTextOptions,\n\t\t\tbase::take(*context));\n\t} else {\n\t\t_contentTitle.setMarkedText(_contentTitleSt, std::move(title));\n\t\t_contentText.setMarkedText(_contentTextSt, std::move(description));\n\t}\n\tupdate();\n}\n\nvoid TopBarSuggestionContent::paintEvent(QPaintEvent *) {\n\tauto p = QPainter(this);\n\tdraw(p);\n}\n\nrpl::producer<int> TopBarSuggestionContent::desiredHeightValue() const {\n\treturn rpl::combine(\n\t\t_lastPaintedContentTop.value(),\n\t\t_lastPaintedContentLineAmount.value()\n\t) | rpl::distinct_until_changed() | rpl::map([=](\n\t\t\tint lastTop,\n\t\t\tint lastLines) {\n\t\tconst auto bottomPadding = st::msgReplyPadding.top();\n\t\tconst auto desiredHeight = lastTop\n\t\t\t+ (lastLines * _contentTextSt.font->height)\n\t\t\t+ bottomPadding;\n\t\treturn std::min(desiredHeight, st::sponsoredMessageBarMaxHeight);\n\t});\n}\n\nvoid TopBarSuggestionContent::setHideCallback(Fn<void()> hideCallback) {\n\tExpects(_rightHide != nullptr);\n\t_rightHide->setClickedCallback(std::move(hideCallback));\n}\n\nvoid TopBarSuggestionContent::setLeftPadding(rpl::producer<int> value) {\n\tstd::move(value) | rpl::on_next([=](int padding) {\n\t\t_leftPadding = padding;\n\t\tupdate();\n\t}, lifetime());\n}\n\nconst style::TextStyle & TopBarSuggestionContent::contentTitleSt() const {\n\treturn _contentTitleSt;\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/buttons.h\"\n\nnamespace Ui {\nclass DynamicImage;\nclass GenericBox;\nclass IconButton;\nclass VerticalLayout;\ntemplate<typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Ui::Text {\nstruct MarkedContext;\n} // namespace Ui::Text\n\nnamespace Data {\nstruct UnreviewedAuth;\n} // namespace Data\n\nnamespace Dialogs {\n\nnot_null<Ui::SlideWrap<Ui::VerticalLayout>*> CreateUnconfirmedAuthContent(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst std::vector<Data::UnreviewedAuth> &list,\n\t\tFn<void(bool)> callback);\n\nvoid ShowAuthDeniedBox(\n\tnot_null<Ui::GenericBox*> box,\n\tfloat64 count,\n\tconst QString &messageText);\n\nclass TopBarSuggestionContent : public Ui::RippleButton {\npublic:\n\tenum class RightIcon {\n\t\tNone,\n\t\tClose,\n\t\tArrow,\n\t};\n\n\tTopBarSuggestionContent(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tFn<bool()> emojiPaused = nullptr);\n\n\tvoid setContent(\n\t\tTextWithEntities title,\n\t\tTextWithEntities description,\n\t\tstd::optional<Ui::Text::MarkedContext> context = std::nullopt,\n\t\tstd::optional<QColor> descriptionColorOverride = std::nullopt);\n\n\t[[nodiscard]] rpl::producer<int> desiredHeightValue() const override;\n\n\tvoid setHideCallback(Fn<void()>);\n\tvoid setRightIcon(RightIcon);\n\tvoid setRightButton(\n\t\trpl::producer<TextWithEntities> text,\n\t\tFn<void()> callback);\n\tvoid setLeftPadding(rpl::producer<int>);\n\n\t[[nodiscard]] const style::TextStyle &contentTitleSt() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *) override;\n\nprivate:\n\tvoid draw(QPainter &p);\n\n\tconst style::TextStyle &_titleSt;\n\tconst style::TextStyle &_contentTitleSt;\n\tconst style::TextStyle &_contentTextSt;\n\n\tUi::Text::String _contentTitle;\n\tUi::Text::String _contentText;\n\trpl::variable<int> _lastPaintedContentLineAmount = 0;\n\trpl::variable<int> _lastPaintedContentTop = 0;\n\tstd::optional<QColor> _descriptionColorOverride;\n\n\tbase::unique_qptr<Ui::IconButton> _rightHide;\n\tbase::unique_qptr<Ui::IconButton> _rightArrow;\n\tbase::unique_qptr<Ui::RoundButton> _rightButton;\n\tFn<void()> _hideCallback;\n\tFn<bool()> _emojiPaused;\n\n\tint _leftPadding = 0;\n\n\tRightIcon _rightIcon = RightIcon::None;\n\n\tstd::shared_ptr<Ui::DynamicImage> _rightPhoto;\n\tQImage _rightPhotoImage;\n\n};\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/ui/dialogs_topics_view.h\"\n\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"core/ui_integration.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Dialogs::Ui {\nnamespace {\n\nconstexpr auto kIconLoopCount = 1;\n\n} // namespace\n\nTopicsView::TopicsView(Data::Forum *forum, Data::SavedMessages *monoforum)\n: _forum(forum)\n, _monoforum(monoforum) {\n}\n\nTopicsView::~TopicsView() = default;\n\nbool TopicsView::prepared() const {\n\tconst auto version = _forum\n\t\t? _forum->recentTopicsListVersion()\n\t\t: _monoforum->recentSublistsListVersion();\n\treturn (_version == version);\n}\n\nvoid TopicsView::prepare(MsgId frontRootId, Fn<void()> customEmojiRepaint) {\n\tExpects(_forum != nullptr);\n\n\tconst auto &list = _forum->recentTopics();\n\t_version = _forum->recentTopicsListVersion();\n\t_titles.reserve(list.size());\n\tauto index = 0;\n\tfor (const auto &topic : list) {\n\t\tconst auto from = begin(_titles) + index;\n\t\tconst auto key = topic->rootId().bare;\n\t\tconst auto i = ranges::find(\n\t\t\tfrom,\n\t\t\tend(_titles),\n\t\t\tkey,\n\t\t\t&Title::key);\n\t\tif (i != end(_titles)) {\n\t\t\tif (i != from) {\n\t\t\t\tranges::rotate(from, i, i + 1);\n\t\t\t}\n\t\t} else if (index >= _titles.size()) {\n\t\t\t_titles.emplace_back();\n\t\t}\n\t\tauto &title = _titles[index++];\n\t\tconst auto unread = topic->chatListBadgesState().unread;\n\t\tif (title.key == key\n\t\t\t&& title.unread == unread\n\t\t\t&& title.version == topic->titleVersion()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto context = Core::TextContext({\n\t\t\t.session = &topic->session(),\n\t\t\t.repaint = customEmojiRepaint,\n\t\t\t.customEmojiLoopLimit = kIconLoopCount,\n\t\t});\n\t\tauto topicTitle = topic->titleWithIcon();\n\t\ttitle.key = key;\n\t\ttitle.version = topic->titleVersion();\n\t\ttitle.unread = unread;\n\t\ttitle.title.setMarkedText(\n\t\t\tst::dialogsTextStyle,\n\t\t\t(unread\n\t\t\t\t? Ui::Text::Colorized(\n\t\t\t\t\tUi::Text::Wrapped(\n\t\t\t\t\t\tstd::move(topicTitle),\n\t\t\t\t\t\tEntityType::Bold))\n\t\t\t\t: std::move(topicTitle)),\n\t\t\tDialogTextOptions(),\n\t\t\tcontext);\n\t}\n\twhile (_titles.size() > index) {\n\t\t_titles.pop_back();\n\t}\n\tconst auto i = frontRootId\n\t\t? ranges::find(_titles, frontRootId.bare, &Title::key)\n\t\t: end(_titles);\n\t_jumpToTopic = (i != end(_titles));\n\tif (_jumpToTopic) {\n\t\tif (i != begin(_titles)) {\n\t\t\tranges::rotate(begin(_titles), i, i + 1);\n\t\t}\n\t\tif (!_titles.front().unread) {\n\t\t\t_jumpToTopic = false;\n\t\t}\n\t}\n\t_allLoaded = _forum->topicsList()->loaded();\n}\n\nvoid TopicsView::prepare(PeerId frontPeerId, Fn<void()> customEmojiRepaint) {\n\tExpects(_monoforum != nullptr);\n\n\tconst auto &list = _monoforum->recentSublists();\n\tconst auto manager = &_monoforum->session().data().customEmojiManager();\n\t_version = _monoforum->recentSublistsListVersion();\n\t_titles.reserve(list.size());\n\tauto index = 0;\n\tfor (const auto &sublist : list) {\n\t\tconst auto from = begin(_titles) + index;\n\t\tconst auto peer = sublist->sublistPeer();\n\t\tconst auto key = peer->id.value;\n\t\tconst auto i = ranges::find(\n\t\t\tfrom,\n\t\t\tend(_titles),\n\t\t\tkey,\n\t\t\t&Title::key);\n\t\tif (i != end(_titles)) {\n\t\t\tif (i != from) {\n\t\t\t\tranges::rotate(from, i, i + 1);\n\t\t\t}\n\t\t} else if (index >= _titles.size()) {\n\t\t\t_titles.emplace_back();\n\t\t}\n\t\tauto &title = _titles[index++];\n\t\tconst auto unread = sublist->chatListBadgesState().unread;\n\t\tif (title.key == key\n\t\t\t&& title.unread == unread\n\t\t\t&& title.version == peer->nameVersion()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto context = Core::TextContext({\n\t\t\t.session = &sublist->session(),\n\t\t\t.repaint = customEmojiRepaint,\n\t\t\t.customEmojiLoopLimit = kIconLoopCount,\n\t\t});\n\t\tauto topicTitle = TextWithEntities().append(\n\t\t\tUi::Text::SingleCustomEmoji(\n\t\t\t\tmanager->peerUserpicEmojiData(peer),\n\t\t\t\tu\"@\"_q)\n\t\t).append(' ').append(peer->shortName());\n\t\ttitle.key = key;\n\t\ttitle.version = peer->nameVersion();\n\t\ttitle.unread = unread;\n\t\ttitle.title.setMarkedText(\n\t\t\tst::dialogsTextStyle,\n\t\t\t(unread\n\t\t\t\t? Ui::Text::Colorized(\n\t\t\t\t\tUi::Text::Wrapped(\n\t\t\t\t\t\tstd::move(topicTitle),\n\t\t\t\t\t\tEntityType::Bold))\n\t\t\t\t: std::move(topicTitle)),\n\t\t\tDialogTextOptions(),\n\t\t\tcontext);\n\t}\n\twhile (_titles.size() > index) {\n\t\t_titles.pop_back();\n\t}\n\tconst auto i = frontPeerId\n\t\t? ranges::find(_titles, frontPeerId.value, &Title::key)\n\t\t: end(_titles);\n\t_jumpToTopic = (i != end(_titles));\n\tif (_jumpToTopic) {\n\t\tif (i != begin(_titles)) {\n\t\t\tranges::rotate(begin(_titles), i, i + 1);\n\t\t}\n\t\tif (!_titles.front().unread) {\n\t\t\t_jumpToTopic = false;\n\t\t}\n\t}\n\t_allLoaded = _monoforum->chatsList()->loaded();\n}\n\nint TopicsView::jumpToTopicWidth() const {\n\treturn _jumpToTopic ? _titles.front().title.maxWidth() : 0;\n}\n\nvoid TopicsView::paint(\n\t\tPainter &p,\n\t\tconst QRect &geometry,\n\t\tconst PaintContext &context) const {\n\tp.setFont(st::dialogsTextFont);\n\tp.setPen(context.active\n\t\t? st::dialogsTextFgActive\n\t\t: context.selected\n\t\t? st::dialogsTextFgOver\n\t\t: st::dialogsTextFg);\n\tconst auto palette = &(context.active\n\t\t? st::dialogsTextPaletteArchiveActive\n\t\t: context.selected\n\t\t? st::dialogsTextPaletteArchiveOver\n\t\t: st::dialogsTextPaletteArchive);\n\tauto rect = geometry;\n\trect.setWidth(rect.width() - _lastTopicJumpGeometry.rightCut);\n\tauto skipBig = _jumpToTopic && !context.active;\n\tif (_titles.empty()) {\n\t\tconst auto text = _allLoaded\n\t\t\t? tr::lng_filters_no_chats(tr::now)\n\t\t\t: tr::lng_contacts_loading(tr::now);\n\t\tp.drawText(\n\t\t\trect.x(),\n\t\t\trect.y() + st::normalFont->ascent,\n\t\t\ttext);\n\t\treturn;\n\t}\n\tfor (const auto &title : _titles) {\n\t\tif (rect.width() < title.title.style()->font->elidew) {\n\t\t\tbreak;\n\t\t}\n\t\ttitle.title.draw(p, {\n\t\t\t.position = rect.topLeft(),\n\t\t\t.availableWidth = rect.width(),\n\t\t\t.palette = palette,\n\t\t\t.spoiler = Text::DefaultSpoilerCache(),\n\t\t\t.now = context.now,\n\t\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t\t.elisionLines = 1,\n\t\t});\n\t\tconst auto skip = skipBig\n\t\t\t? context.st->topicsSkipBig\n\t\t\t: context.st->topicsSkip;\n\t\trect.setLeft(rect.left() + title.title.maxWidth() + skip);\n\t\tskipBig = false;\n\t}\n}\n\nbool TopicsView::changeTopicJumpGeometry(JumpToLastGeometry geometry) {\n\tif (_lastTopicJumpGeometry != geometry) {\n\t\t_lastTopicJumpGeometry = geometry;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid TopicsView::clearTopicJumpGeometry() {\n\tchangeTopicJumpGeometry({});\n}\n\nbool TopicsView::isInTopicJumpArea(int x, int y) const {\n\treturn _lastTopicJumpGeometry.area1.contains(x, y)\n\t\t|| _lastTopicJumpGeometry.area2.contains(x, y);\n}\n\nvoid TopicsView::addTopicJumpRipple(\n\t\tQPoint origin,\n\t\tnot_null<TopicJumpCache*> topicJumpCache,\n\t\tFn<void()> updateCallback) {\n\tauto mask = topicJumpRippleMask(topicJumpCache);\n\tif (mask.isNull()) {\n\t\treturn;\n\t}\n\t_ripple = std::make_unique<Ui::RippleAnimation>(\n\t\tst::dialogsRipple,\n\t\tstd::move(mask),\n\t\tstd::move(updateCallback));\n\t_ripple->add(origin);\n}\n\nvoid TopicsView::stopLastRipple() {\n\tif (_ripple) {\n\t\t_ripple->lastStop();\n\t}\n}\n\nvoid TopicsView::clearRipple() {\n\t_ripple = nullptr;\n}\n\nvoid TopicsView::paintRipple(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tconst QColor *colorOverride) const {\n\tif (_ripple) {\n\t\t_ripple->paint(p, x, y, outerWidth, colorOverride);\n\t\tif (_ripple->empty()) {\n\t\t\t_ripple.reset();\n\t\t}\n\t}\n}\n\nQImage TopicsView::topicJumpRippleMask(\n\t\tnot_null<TopicJumpCache*> topicJumpCache) const {\n\tconst auto &st = st::forumDialogRow;\n\tconst auto area1 = _lastTopicJumpGeometry.area1;\n\tif (area1.isEmpty()) {\n\t\treturn QImage();\n\t}\n\tconst auto area2 = _lastTopicJumpGeometry.area2;\n\tconst auto drawer = [&](QPainter &p) {\n\t\tconst auto white = style::complex_color([] { return Qt::white; });\n\t\t// p.setOpacity(.1);\n\t\tFillJumpToLastPrepared(p, {\n\t\t\t.st = &st,\n\t\t\t.corners = &topicJumpCache->rippleMask,\n\t\t\t.bg = white.color(),\n\t\t\t.prepared = _lastTopicJumpGeometry,\n\t\t});\n\t};\n\treturn Ui::RippleAnimation::MaskByDrawer(\n\t\tQRect(0, 0, 1, 1).united(area1).united(area2).size(),\n\t\tfalse,\n\t\tdrawer);\n}\n\nJumpToLastGeometry FillJumpToLastBg(QPainter &p, JumpToLastBg context) {\n\tconst auto padding = st::forumDialogJumpPadding;\n\tconst auto availableWidth = context.geometry.width();\n\tconst auto want1 = std::min(context.width1, availableWidth);\n\tconst auto use1 = std::min(want1, availableWidth - padding.right());\n\tconst auto use2 = std::min(context.width2, availableWidth);\n\tconst auto rightCut = want1 - use1;\n\tconst auto origin = context.geometry.topLeft();\n\tconst auto delta = std::abs(use1 - use2);\n\tif (delta <= context.st->topicsSkip / 2) {\n\t\tconst auto w = std::max(use1, use2);\n\t\tconst auto h = context.st->topicsHeight + st::normalFont->height;\n\t\tconst auto fill = QRect(origin, QSize(w, h));\n\t\tconst auto full = fill.marginsAdded(padding);\n\t\tauto result = JumpToLastGeometry{ rightCut, full };\n\t\tFillJumpToLastPrepared(p, {\n\t\t\t.st = context.st,\n\t\t\t.corners = context.corners,\n\t\t\t.bg = context.bg,\n\t\t\t.prepared = result,\n\t\t});\n\t\treturn result;\n\t}\n\tconst auto h1 = context.st->topicsHeight;\n\tconst auto h2 = st::normalFont->height;\n\tconst auto rect1 = QRect(origin, QSize(use1, h1));\n\tconst auto fill1 = rect1.marginsAdded({\n\t\tpadding.left(),\n\t\tpadding.top(),\n\t\tpadding.right(),\n\t\t(use1 < use2 ? -padding.top() : padding.bottom()),\n\t});\n\tconst auto add = QPoint(0, h1);\n\tconst auto rect2 = QRect(origin + add, QSize(use2, h2));\n\tconst auto fill2 = rect2.marginsAdded({\n\t\tpadding.left(),\n\t\t(use2 < use1 ? -padding.bottom() : padding.top()),\n\t\tpadding.right(),\n\t\tpadding.bottom(),\n\t});\n\tauto result = JumpToLastGeometry{ rightCut, fill1, fill2 };\n\tFillJumpToLastPrepared(p, {\n\t\t.st = context.st,\n\t\t.corners = context.corners,\n\t\t.bg = context.bg,\n\t\t.prepared = result,\n\t});\n\treturn result;\n}\n\nvoid FillJumpToLastPrepared(QPainter &p, JumpToLastPrepared context) {\n\tauto &normal = context.corners->normal;\n\tauto &inverted = context.corners->inverted;\n\tauto &small = context.corners->small;\n\tconst auto radius = st::forumDialogJumpRadius;\n\tconst auto &bg = context.bg;\n\tconst auto area1 = context.prepared.area1;\n\tconst auto area2 = context.prepared.area2;\n\tif (area2.isNull()) {\n\t\tif (normal.p[0].isNull()) {\n\t\t\tnormal = Ui::PrepareCornerPixmaps(radius, bg);\n\t\t}\n\t\tUi::FillRoundRect(p, area1, bg, normal);\n\t\treturn;\n\t}\n\tconst auto width1 = area1.width();\n\tconst auto width2 = area2.width();\n\tconst auto delta = std::abs(width1 - width2);\n\tconst auto h1 = context.st->topicsHeight;\n\tconst auto h2 = st::normalFont->height;\n\tconst auto hmin = std::min(h1, h2);\n\tconst auto wantedInvertedRadius = hmin - radius;\n\tconst auto invertedr = std::min(wantedInvertedRadius, delta / 2);\n\tconst auto smallr = std::min(radius, delta - invertedr);\n\tconst auto smallkey = (width1 < width2) ? smallr : (-smallr);\n\tif (normal.p[0].isNull()) {\n\t\tnormal = Ui::PrepareCornerPixmaps(radius, bg);\n\t}\n\tif (inverted.p[0].isNull()\n\t\t|| context.corners->invertedRadius != invertedr) {\n\t\tcontext.corners->invertedRadius = invertedr;\n\t\tinverted = Ui::PrepareInvertedCornerPixmaps(invertedr, bg);\n\t}\n\tif (smallr != radius\n\t\t&& (small.isNull() || context.corners->smallKey != smallkey)) {\n\t\tcontext.corners->smallKey = smallr;\n\t\tauto pixmaps = Ui::PrepareCornerPixmaps(smallr, bg);\n\t\tsmall = pixmaps.p[(width1 < width2) ? 1 : 3];\n\t}\n\tauto no1 = normal;\n\tno1.p[2] = QPixmap();\n\tif (width1 < width2) {\n\t\tno1.p[3] = QPixmap();\n\t} else if (smallr != radius) {\n\t\tno1.p[3] = small;\n\t}\n\tUi::FillRoundRect(p, area1, bg, no1);\n\tif (width1 < width2) {\n\t\tp.drawPixmap(\n\t\t\tarea1.x() + width1,\n\t\t\tarea1.y() + area1.height() - invertedr,\n\t\t\tinverted.p[3]);\n\t}\n\tauto no2 = normal;\n\tno2.p[0] = QPixmap();\n\tif (width2 < width1) {\n\t\tno2.p[1] = QPixmap();\n\t} else if (smallr != radius) {\n\t\tno2.p[1] = small;\n\t}\n\tUi::FillRoundRect(p, area2, bg, no2);\n\tif (width2 < width1) {\n\t\tp.drawPixmap(\n\t\t\tarea2.x() + width2,\n\t\t\tarea2.y(),\n\t\t\tinverted.p[0]);\n\t}\n}\n\n} // namespace Dialogs::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass Painter;\n\nnamespace style {\nstruct DialogRow;\n} // namespace style\n\nnamespace Data {\nclass Forum;\nclass ForumTopic;\nclass SavedMessages;\nclass SavedSublist;\n} // namespace Data\n\nnamespace Ui {\nclass RippleAnimation;\n} // namespace Ui\n\nnamespace Dialogs::Ui {\n\nusing namespace ::Ui;\n\nstruct PaintContext;\nstruct TopicJumpCache;\nstruct TopicJumpCorners;\n\nstruct JumpToLastBg {\n\tnot_null<const style::DialogRow*> st;\n\tnot_null<TopicJumpCorners*> corners;\n\tQRect geometry;\n\tconst style::color &bg;\n\tint width1 = 0;\n\tint width2 = 0;\n};\nstruct JumpToLastGeometry {\n\tint rightCut = 0;\n\tQRect area1;\n\tQRect area2;\n\n\tfriend inline bool operator==(\n\t\tconst JumpToLastGeometry&,\n\t\tconst JumpToLastGeometry&) = default;\n};\nJumpToLastGeometry FillJumpToLastBg(QPainter &p, JumpToLastBg context);\n\nstruct JumpToLastPrepared {\n\tnot_null<const style::DialogRow*> st;\n\tnot_null<TopicJumpCorners*> corners;\n\tconst style::color &bg;\n\tconst JumpToLastGeometry &prepared;\n};\nvoid FillJumpToLastPrepared(QPainter &p, JumpToLastPrepared context);\n\nclass TopicsView final {\npublic:\n\tTopicsView(Data::Forum *forum, Data::SavedMessages *monoforum);\n\t~TopicsView();\n\n\t[[nodiscard]] Data::Forum *forum() const {\n\t\treturn _forum;\n\t}\n\t[[nodiscard]] Data::SavedMessages *monoforum() const {\n\t\treturn _monoforum;\n\t}\n\n\t[[nodiscard]] bool prepared() const;\n\tvoid prepare(MsgId frontRootId, Fn<void()> customEmojiRepaint);\n\tvoid prepare(PeerId frontPeerId, Fn<void()> customEmojiRepaint);\n\n\t[[nodiscard]] int jumpToTopicWidth() const;\n\n\tvoid paint(\n\t\tPainter &p,\n\t\tconst QRect &geometry,\n\t\tconst PaintContext &context) const;\n\n\tbool changeTopicJumpGeometry(JumpToLastGeometry geometry);\n\tvoid clearTopicJumpGeometry();\n\t[[nodiscard]] bool isInTopicJumpArea(int x, int y) const;\n\tvoid addTopicJumpRipple(\n\t\tQPoint origin,\n\t\tnot_null<TopicJumpCache*> topicJumpCache,\n\t\tFn<void()> updateCallback);\n\tvoid paintRipple(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tconst QColor *colorOverride) const;\n\tvoid stopLastRipple();\n\tvoid clearRipple();\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tstruct Title {\n\t\tText::String title;\n\t\tuint64 key = 0;\n\t\tint version = -1;\n\t\tbool unread = false;\n\t};\n\n\t[[nodiscard]] QImage topicJumpRippleMask(\n\t\tnot_null<TopicJumpCache*> topicJumpCache) const;\n\n\tData::Forum * const _forum = nullptr;\n\tData::SavedMessages * const _monoforum = nullptr;\n\n\tmutable std::vector<Title> _titles;\n\tmutable std::unique_ptr<RippleAnimation> _ripple;\n\tJumpToLastGeometry _lastTopicJumpGeometry;\n\tint _version = -1;\n\tbool _jumpToTopic = false;\n\tbool _allLoaded = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Dialogs::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/ui/dialogs_video_userpic.h\"\n\n#include \"core/file_location.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_session.h\"\n#include \"dialogs/dialogs_entry.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Dialogs::Ui {\n\nVideoUserpic::VideoUserpic(not_null<PeerData*> peer, Fn<void()> repaint)\n: _peer(peer)\n, _repaint(std::move(repaint)) {\n}\n\nVideoUserpic::~VideoUserpic() = default;\n\nint VideoUserpic::frameIndex() const {\n\treturn -1;\n}\n\nvoid VideoUserpic::paintLeft(\n\t\tPainter &p,\n\t\tUi::PeerUserpicView &view,\n\t\tint x,\n\t\tint y,\n\t\tint w,\n\t\tint size,\n\t\tbool paused) {\n\t_lastSize = size;\n\n\tconst auto photoId = _peer->userpicPhotoId();\n\tif (_videoPhotoId != photoId) {\n\t\t_videoPhotoId = photoId;\n\t\t_video = nullptr;\n\t\t_videoPhotoMedia = nullptr;\n\t\tconst auto photo = _peer->owner().photo(photoId);\n\t\tif (photo->isNull()) {\n\t\t\t_peer->updateFullForced();\n\t\t} else {\n\t\t\t_videoPhotoMedia = photo->createMediaView();\n\t\t\t_videoPhotoMedia->videoWanted(\n\t\t\t\tData::PhotoSize::Small,\n\t\t\t\t_peer->userpicPhotoOrigin());\n\t\t}\n\t}\n\tif (!_video) {\n\t\tif (!_videoPhotoMedia) {\n\t\t\tconst auto photo = _peer->owner().photo(photoId);\n\t\t\tif (!photo->isNull()) {\n\t\t\t\t_videoPhotoMedia = photo->createMediaView();\n\t\t\t\t_videoPhotoMedia->videoWanted(\n\t\t\t\t\tData::PhotoSize::Small,\n\t\t\t\t\t_peer->userpicPhotoOrigin());\n\t\t\t}\n\t\t}\n\t\tif (_videoPhotoMedia) {\n\t\t\tauto small = _videoPhotoMedia->videoContent(\n\t\t\t\tData::PhotoSize::Small);\n\t\t\tauto bytes = small.isEmpty()\n\t\t\t\t? _videoPhotoMedia->videoContent(Data::PhotoSize::Large)\n\t\t\t\t: small;\n\t\t\tif (!bytes.isEmpty()) {\n\t\t\t\tauto callback = [=](Media::Clip::Notification notification) {\n\t\t\t\t\tclipCallback(notification);\n\t\t\t\t};\n\t\t\t\t_video = Media::Clip::MakeReader(\n\t\t\t\t\tCore::FileLocation(),\n\t\t\t\t\tstd::move(bytes),\n\t\t\t\t\tstd::move(callback));\n\t\t\t}\n\t\t}\n\t}\n\tif (rtl()) {\n\t\tx = w - x - size;\n\t}\n\tif (_video && _video->ready()) {\n\t\tstartReady();\n\n\t\tconst auto now = paused ? crl::time(0) : crl::now();\n\t\tp.drawImage(x, y, _video->current(request(size), now));\n\t} else {\n\t\t_peer->paintUserpicLeft(p, view, x, y, w, size);\n\t}\n}\n\nMedia::Clip::FrameRequest VideoUserpic::request(int size) const {\n\treturn {\n\t\t.frame = { size, size },\n\t\t.outer = { size, size },\n\t\t.factor = style::DevicePixelRatio(),\n\t\t.radius = ImageRoundRadius::Ellipse,\n\t};\n}\n\nbool VideoUserpic::startReady(int size) {\n\tif (!_video->ready() || _video->started()) {\n\t\treturn false;\n\t} else if (!_lastSize) {\n\t\t_lastSize = size ? size : _video->width();\n\t}\n\t_video->start(request(_lastSize));\n\t_repaint();\n\treturn true;\n}\n\nvoid VideoUserpic::clipCallback(Media::Clip::Notification notification) {\n\tusing namespace Media::Clip;\n\n\tswitch (notification) {\n\tcase Notification::Reinit: {\n\t\tif (_video->state() == State::Error) {\n\t\t\t_video.setBad();\n\t\t} else if (startReady()) {\n\t\t\t_repaint();\n\t\t}\n\t} break;\n\n\tcase Notification::Repaint: _repaint(); break;\n\t}\n}\n\nvoid PaintUserpic(\n\t\tPainter &p,\n\t\tnot_null<Entry*> entry,\n\t\tPeerData *peer,\n\t\tVideoUserpic *videoUserpic,\n\t\tPeerUserpicView &view,\n\t\tconst Ui::PaintContext &context) {\n\tif (peer) {\n\t\tPaintUserpic(\n\t\t\tp,\n\t\t\tpeer,\n\t\t\tvideoUserpic,\n\t\t\tview,\n\t\t\tcontext.st->padding.left(),\n\t\t\tcontext.st->padding.top(),\n\t\t\tcontext.width,\n\t\t\tcontext.st->photoSize,\n\t\t\tcontext.paused);\n\t} else {\n\t\tentry->paintUserpic(p, view, context);\n\t}\n}\n\nvoid PaintUserpic(\n\t\tPainter &p,\n\t\tnot_null<PeerData*> peer,\n\t\tUi::VideoUserpic *videoUserpic,\n\t\tUi::PeerUserpicView &view,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size,\n\t\tbool paused) {\n\tif (videoUserpic) {\n\t\tvideoUserpic->paintLeft(p, view, x, y, outerWidth, size, paused);\n\t} else {\n\t\tpeer->paintUserpicLeft(p, view, x, y, outerWidth, size);\n\t}\n}\n\n} // namespace Dialogs::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/clip/media_clip_reader.h\"\n\nclass Painter;\n\nnamespace Data {\nclass PhotoMedia;\n} // namespace Data\n\nnamespace Ui {\nstruct PeerUserpicView;\n} // namespace Ui\n\nnamespace Dialogs {\nclass Entry;\n} // namespace Dialogs\n\nnamespace Dialogs::Ui {\n\nusing namespace ::Ui;\n\nstruct PaintContext;\n\nclass VideoUserpic final {\npublic:\n\tVideoUserpic(not_null<PeerData*> peer, Fn<void()> repaint);\n\t~VideoUserpic();\n\n\t[[nodiscard]] int frameIndex() const;\n\n\tvoid paintLeft(\n\t\tPainter &p,\n\t\tPeerUserpicView &view,\n\t\tint x,\n\t\tint y,\n\t\tint w,\n\t\tint size,\n\t\tbool paused);\n\nprivate:\n\tvoid clipCallback(Media::Clip::Notification notification);\n\t[[nodiscard]] Media::Clip::FrameRequest request(int size) const;\n\tbool startReady(int size = 0);\n\n\tconst not_null<PeerData*> _peer;\n\tconst Fn<void()> _repaint;\n\n\tMedia::Clip::ReaderPointer _video;\n\tint _lastSize = 0;\n\tstd::shared_ptr<Data::PhotoMedia> _videoPhotoMedia;\n\tPhotoId _videoPhotoId = 0;\n\n};\n\nvoid PaintUserpic(\n\tPainter &p,\n\tnot_null<Entry*> entry,\n\tPeerData *peer,\n\tVideoUserpic *videoUserpic,\n\tPeerUserpicView &view,\n\tconst Ui::PaintContext &context);\n\nvoid PaintUserpic(\n\tPainter &p,\n\tnot_null<PeerData*> peer,\n\tVideoUserpic *videoUserpic,\n\tPeerUserpicView &view,\n\tint x,\n\tint y,\n\tint outerWidth,\n\tint size,\n\tbool paused);\n\n} // namespace Dialogs::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/posts_search_intro.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/ui/posts_search_intro.h\"\n\n#include \"base/timer_rpl.h\"\n#include \"base/unixtime.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/controls/button_labels.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Dialogs {\nnamespace {\n\n[[nodiscard]] rpl::producer<QString> FormatCountdownTill(TimeId when) {\n\treturn rpl::single(rpl::empty) | rpl::then(\n\t\tbase::timer_each(1000)\n\t) | rpl::map([=] {\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto delta = std::max(when - now, 0);\n\t\tconst auto hours = delta / 3600;\n\t\tconst auto minutes = (delta % 3600) / 60;\n\t\tconst auto seconds = delta % 60;\n\t\tconstexpr auto kZero = QChar('0');\n\t\treturn (hours > 0)\n\t\t\t? u\"%1:%2:%3\"_q\n\t\t\t\t.arg(hours)\n\t\t\t\t.arg(minutes, 2, 10, kZero)\n\t\t\t\t.arg(seconds, 2, 10, kZero)\n\t\t\t: u\"%1:%2\"_q\n\t\t\t\t.arg(minutes)\n\t\t\t\t.arg(seconds, 2, 10, kZero);\n\t});\n}\n\nvoid SetSearchButtonLabel(\n\t\tnot_null<Ui::RpWidget*> button,\n\t\trpl::producer<TextWithEntities> text) {\n\tconst auto left = &st::postsSearchIcon;\n\tconst auto leftPadding = st::postsSearchIconPadding;\n\tconst auto right = &st::postsSearchArrow;\n\tconst auto rightPadding = st::postsSearchArrowPadding;\n\tconst auto leftSkip = left->size().grownBy(leftPadding).width();\n\tconst auto rightSkip = right->size().grownBy(rightPadding).width();\n\n\tstruct State {\n\t\tState() : linkFg([] {\n\t\t\tauto copy = st::windowFgActive->c;\n\t\t\tcopy.setAlphaF(0.6);\n\t\t\treturn copy;\n\t\t}), st(st::resaleButtonTitle) {\n\t\t}\n\n\t\tstyle::complex_color linkFg;\n\t\tstyle::FlatLabel st;\n\t};\n\tauto lifetime = rpl::lifetime();\n\tconst auto state = lifetime.make_state<State>();\n\tstate->st.palette.linkFg = state->linkFg.color();\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton,\n\t\trpl::duplicate(text),\n\t\tstate->st);\n\tlabel->lifetime().add(std::move(lifetime));\n\tlabel->show();\n\n\tconst auto icons = Ui::CreateChild<Ui::RpWidget>(button);\n\ticons->show();\n\trpl::combine(\n\t\tbutton->sizeValue(),\n\t\tstd::move(text)\n\t) | rpl::on_next([=](QSize size, const auto &) {\n\t\ticons->setGeometry(QRect(QPoint(), size));\n\t\tconst auto available = size.width() - leftSkip - rightSkip;\n\t\tif (available <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto width = std::min(available, label->textMaxWidth());\n\t\tlabel->resizeToWidth(width);\n\t\tconst auto full = leftSkip + width + rightSkip;\n\t\tconst auto x = (size.width() - full) / 2;\n\t\tconst auto y = (size.height() - label->height()) / 2;\n\t\tlabel->moveToLeft(x + leftSkip, y, size.width());\n\t}, icons->lifetime());\n\n\ticons->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(icons);\n\t\tleft->paint(\n\t\t\tp,\n\t\t\tlabel->x() - leftSkip + leftPadding.left(),\n\t\t\tlabel->y() + leftPadding.top(),\n\t\t\ticons->width());\n\t\tright->paint(\n\t\t\tp,\n\t\t\tlabel->x() + label->width() + rightPadding.left(),\n\t\t\tlabel->y() + rightPadding.top(),\n\t\t\ticons->width());\n\t}, icons->lifetime());\n}\n\n} // namespace\n\nPostsSearchIntro::PostsSearchIntro(\n\tnot_null<Ui::RpWidget*> parent,\n\tPostsSearchIntroState state)\n: RpWidget(parent)\n, _state(std::move(state))\n, _content(std::make_unique<Ui::VerticalLayout>(this)) {\n\tsetup();\n}\n\nPostsSearchIntro::~PostsSearchIntro() = default;\n\nvoid PostsSearchIntro::update(PostsSearchIntroState state) {\n\t_state = std::move(state);\n}\n\nrpl::producer<int> PostsSearchIntro::searchWithStars() const {\n\treturn _button->clicks() | rpl::map([=] {\n\t\tconst auto &now = _state.current();\n\t\treturn (now.needsPremium || now.freeSearchesLeft)\n\t\t\t? 0\n\t\t\t: int(now.starsPerPaidSearch);\n\t});\n}\n\nvoid PostsSearchIntro::setup() {\n\tauto title = _state.value(\n\t) | rpl::map([](const PostsSearchIntroState &state) {\n\t\treturn (state.needsPremium || state.freeSearchesLeft > 0)\n\t\t\t? tr::lng_posts_title()\n\t\t\t: tr::lng_posts_limit_reached();\n\t}) | rpl::flatten_latest();\n\n\tauto subtitle = _state.value(\n\t) | rpl::map([](const PostsSearchIntroState &state) {\n\t\treturn (state.needsPremium || state.freeSearchesLeft > 0)\n\t\t\t? tr::lng_posts_start()\n\t\t\t: tr::lng_posts_limit_about(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(state.freeSearchesPerDay * 1.));\n\t}) | rpl::flatten_latest();\n\n\tauto footer = _state.value(\n\t) | rpl::map([](const PostsSearchIntroState &state)\n\t-> rpl::producer<QString> {\n\t\tif (state.needsPremium) {\n\t\t\treturn tr::lng_posts_need_subscribe();\n\t\t} else if (state.freeSearchesLeft > 0) {\n\t\t\treturn tr::lng_posts_remaining(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(state.freeSearchesLeft * 1.));\n\t\t} else {\n\t\t\treturn rpl::single(QString());\n\t\t}\n\t}) | rpl::flatten_latest();\n\n\t_title = _content->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_content.get(),\n\t\t\tstd::move(title),\n\t\t\tst::postsSearchIntroTitle),\n\t\tst::postsSearchIntroTitleMargin,\n\t\tstyle::al_top);\n\t_title->setTryMakeSimilarLines(true);\n\t_subtitle = _content->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_content.get(),\n\t\t\tstd::move(subtitle),\n\t\t\tst::postsSearchIntroSubtitle),\n\t\tst::postsSearchIntroSubtitleMargin,\n\t\tstyle::al_top);\n\t_subtitle->setTryMakeSimilarLines(true);\n\t_button = _content->add(\n\t\tobject_ptr<Ui::RoundButton>(\n\t\t\t_content.get(),\n\t\t\trpl::single(QString()),\n\t\t\tst::postsSearchIntroButton),\n\t\tstyle::al_top);\n\t_footer = _content->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_content.get(),\n\t\t\tstd::move(footer),\n\t\t\tst::postsSearchIntroFooter),\n\t\tst::postsSearchIntroFooterMargin,\n\t\tstyle::al_top);\n\t_footer->setTryMakeSimilarLines(true);\n\n\t_state.value(\n\t) | rpl::on_next([=](const PostsSearchIntroState &state) {\n\t\tif (state.query.trimmed().isEmpty() && !state.needsPremium) {\n\t\t\t_button->resize(_button->width(), 0);\n\t\t\t_content->resizeToWidth(width());\n\t\t\treturn;\n\t\t}\n\n\t\tauto copy = _button->children();\n\t\tfor (const auto child : copy) {\n\t\t\tdelete child;\n\t\t}\n\t\tif (state.needsPremium) {\n\t\t\t_button->setText(tr::lng_posts_subscribe());\n\t\t} else if (state.freeSearchesLeft > 0) {\n\t\t\t_button->setText(rpl::single(QString()));\n\n\t\t\tSetSearchButtonLabel(_button, tr::lng_posts_search_button(\n\t\t\t\tlt_query,\n\t\t\t\trpl::single(Ui::Text::Colorized(state.query.trimmed())),\n\t\t\t\ttr::marked));\n\t\t} else {\n\t\t\t_button->setText(rpl::single(QString()));\n\n\t\t\tUi::SetButtonTwoLabels(\n\t\t\t\t_button,\n\t\t\t\ttr::lng_posts_limit_search_paid(\n\t\t\t\t\tlt_cost,\n\t\t\t\t\trpl::single(Ui::Text::IconEmoji(\n\t\t\t\t\t\t&st::starIconEmoji\n\t\t\t\t\t).append(\n\t\t\t\t\t\tLang::FormatCountDecimal(state.starsPerPaidSearch))),\n\t\t\t\t\ttr::marked),\n\t\t\t\ttr::lng_posts_limit_unlocks(\n\t\t\t\t\tlt_duration,\n\t\t\t\t\tFormatCountdownTill(\n\t\t\t\t\t\tstate.nextFreeSearchTime\n\t\t\t\t\t) | rpl::map(tr::marked),\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::resaleButtonTitle,\n\t\t\t\tst::resaleButtonSubtitle);\n\t\t}\n\t\t_button->resize(_button->width(), st::postsSearchIntroButton.height);\n\t\t_content->resizeToWidth(width());\n\t}, _button->lifetime());\n}\n\nvoid PostsSearchIntro::resizeEvent(QResizeEvent *e) {\n\t_content->resizeToWidth(width());\n\tconst auto top = std::max(0, (height() - _content->height()) / 3);\n\t_content->move(0, top);\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/posts_search_intro.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\nclass FlatLabel;\nclass RoundButton;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Dialogs {\n\nstruct PostsSearchIntroState {\n\tQString query;\n\tint freeSearchesPerDay = 0;\n\tint freeSearchesLeft = 0;\n\tTimeId nextFreeSearchTime = 0;\n\tuint32 starsPerPaidSearch : 31 = 0;\n\tuint32 needsPremium : 1 = 0;\n\n\tfriend inline bool operator==(\n\t\tPostsSearchIntroState,\n\t\tPostsSearchIntroState) = default;\n};\n\nclass PostsSearchIntro final : public Ui::RpWidget {\npublic:\n\tPostsSearchIntro(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tPostsSearchIntroState state);\n\t~PostsSearchIntro();\n\n\tvoid update(PostsSearchIntroState state);\n\n\t[[nodiscard]] rpl::producer<int> searchWithStars() const;\n\nprivate:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\tvoid setup();\n\n\trpl::variable<PostsSearchIntroState> _state;\n\n\tstd::unique_ptr<Ui::VerticalLayout> _content;\n\tUi::FlatLabel *_title = nullptr;\n\tUi::FlatLabel *_subtitle = nullptr;\n\tUi::RoundButton *_button = nullptr;\n\tUi::FlatLabel *_footer = nullptr;\n\n};\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"dialogs/ui/top_peers_strip.h\"\n\n#include \"base/event_filter.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/text/text.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/painter.h\"\n#include \"ui/unread_badge_paint.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_widgets.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace Dialogs {\n\nstruct TopPeersStrip::Entry {\n\tuint64 id = 0;\n\tUi::Text::String name;\n\tstd::shared_ptr<Ui::DynamicImage> userpic;\n\tstd::unique_ptr<Ui::RippleAnimation> ripple;\n\tUi::Animations::Simple onlineShown;\n\tQImage userpicFrame;\n\tfloat64 userpicFrameOnline = 0.;\n\tQString badgeString;\n\tuint32 badge : 27 = 0;\n\tuint32 userpicFrameDirty : 1 = 0;\n\tuint32 subscribed : 1 = 0;\n\tuint32 unread : 1 = 0;\n\tuint32 online : 1 = 0;\n\tuint32 muted : 1 = 0;\n};\n\nstruct TopPeersStrip::Layout {\n\tint single = 0;\n\tint inrow = 0;\n\tfloat64 fsingle = 0.;\n\tfloat64 added = 0.;\n};\n\nTopPeersStrip::TopPeersStrip(\n\tnot_null<QWidget*> parent,\n\trpl::producer<TopPeersList> content)\n: RpWidget(parent)\n, _header(this)\n, _strip(this)\n, _selection(st::topPeersRadius, st::windowBgOver) {\n\tsetupHeader();\n\tsetupStrip();\n\n\tstd::move(content) | rpl::on_next([=](const TopPeersList &list) {\n\t\tapply(list);\n\t}, lifetime());\n\n\trpl::combine(\n\t\t_count.value(),\n\t\t_expanded.value()\n\t) | rpl::on_next([=] {\n\t\tresizeToWidth(width());\n\t}, _strip.lifetime());\n\n\tresize(0, _header.height() + _strip.height());\n}\n\nvoid TopPeersStrip::setupHeader() {\n\t_header.resize(0, st::searchedBarHeight);\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\t&_header,\n\t\ttr::lng_recent_frequent(),\n\t\tst::searchedBarLabel);\n\tconst auto single = outer().width();\n\n\trpl::combine(\n\t\t_count.value(),\n\t\twidthValue()\n\t) | rpl::map(\n\t\t(rpl::mappers::_1 * single) > (rpl::mappers::_2 + (single * 2) / 3)\n\t) | rpl::distinct_until_changed() | rpl::on_next([=](bool more) {\n\t\tsetExpanded(false);\n\t\tif (!more) {\n\t\t\tconst auto toggle = _toggleExpanded.current();\n\t\t\t_toggleExpanded = nullptr;\n\t\t\tdelete toggle;\n\t\t\treturn;\n\t\t} else if (_toggleExpanded.current()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto toggle = Ui::CreateChild<Ui::LinkButton>(\n\t\t\t&_header,\n\t\t\ttr::lng_channels_your_more(tr::now),\n\t\t\tst::searchedBarLink);\n\t\ttoggle->show();\n\t\ttoggle->setClickedCallback([=] {\n\t\t\tconst auto expand = !_expanded.current();\n\t\t\ttoggle->setText(expand\n\t\t\t\t? tr::lng_recent_frequent_collapse(tr::now)\n\t\t\t\t: tr::lng_recent_frequent_all(tr::now));\n\t\t\tsetExpanded(expand);\n\t\t});\n\t\trpl::combine(\n\t\t\t_header.sizeValue(),\n\t\t\ttoggle->widthValue()\n\t\t) | rpl::on_next([=](QSize size, int width) {\n\t\t\tconst auto x = st::searchedBarPosition.x();\n\t\t\tconst auto y = st::searchedBarPosition.y();\n\t\t\ttoggle->moveToRight(0, 0, size.width());\n\t\t\tlabel->resizeToWidth(size.width() - x - width);\n\t\t\tlabel->moveToLeft(x, y, size.width());\n\t\t}, toggle->lifetime());\n\t\t_toggleExpanded = toggle;\n\t}, _header.lifetime());\n\n\trpl::combine(\n\t\t_header.sizeValue(),\n\t\t_toggleExpanded.value()\n\t) | rpl::filter(\n\t\trpl::mappers::_2 == nullptr\n\t) | rpl::on_next([=](QSize size, const auto) {\n\t\tconst auto x = st::searchedBarPosition.x();\n\t\tconst auto y = st::searchedBarPosition.y();\n\t\tlabel->resizeToWidth(size.width() - x * 2);\n\t\tlabel->moveToLeft(x, y, size.width());\n\t}, _header.lifetime());\n\n\t_header.paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tQPainter(&_header).fillRect(clip, st::searchedBarBg);\n\t}, _header.lifetime());\n}\n\nvoid TopPeersStrip::setExpanded(bool expanded) {\n\tif (_expanded.current() == expanded) {\n\t\treturn;\n\t}\n\tconst auto from = expanded ? 0. : 1.;\n\tconst auto to = expanded ? 1. : 0.;\n\t_expandAnimation.start([=] {\n\t\tif (!_expandAnimation.animating()) {\n\t\t\tupdateScrollMax();\n\t\t}\n\t\tresizeToWidth(width());\n\t\tupdate();\n\t}, from, to, st::slideDuration, anim::easeOutQuint);\n\t_expanded = expanded;\n}\n\nvoid TopPeersStrip::setupStrip() {\n\t_strip.resize(0, st::topPeers.height);\n\n\t_strip.setMouseTracking(true);\n\n\tbase::install_event_filter(&_strip, [=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::Wheel) {\n\t\t\tstripWheelEvent(static_cast<QWheelEvent*>(e.get()));\n\t\t} else if (type == QEvent::MouseButtonPress) {\n\t\t\tstripMousePressEvent(static_cast<QMouseEvent*>(e.get()));\n\t\t} else if (type == QEvent::MouseMove) {\n\t\t\tstripMouseMoveEvent(static_cast<QMouseEvent*>(e.get()));\n\t\t} else if (type == QEvent::MouseButtonRelease) {\n\t\t\tstripMouseReleaseEvent(static_cast<QMouseEvent*>(e.get()));\n\t\t} else if (type == QEvent::ContextMenu) {\n\t\t\tstripContextMenuEvent(static_cast<QContextMenuEvent*>(e.get()));\n\t\t} else if (type == QEvent::Leave) {\n\t\t\tstripLeaveEvent(e.get());\n\t\t} else {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\t\treturn base::EventFilterResult::Cancel;\n\t});\n\n\t_strip.paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tpaintStrip(clip);\n\t}, _strip.lifetime());\n}\n\nTopPeersStrip::~TopPeersStrip() {\n\tunsubscribeUserpics(true);\n}\n\nint TopPeersStrip::resizeGetHeight(int newWidth) {\n\t_header.resize(newWidth, _header.height());\n\tconst auto single = QSize(outer().width(), st::topPeers.height);\n\tconst auto inRow = newWidth / single.width();\n\tconst auto rows = (inRow > 0)\n\t\t? ((std::max(_count.current(), 1) + inRow - 1) / inRow)\n\t\t: 1;\n\tconst auto height = single.height() * rows;\n\tconst auto value = _expandAnimation.value(_expanded.current() ? 1. : 0.);\n\tconst auto result = anim::interpolate(single.height(), height, value);\n\t_strip.setGeometry(0, _header.height(), newWidth, result);\n\tupdateScrollMax(newWidth);\n\treturn _strip.y() + _strip.height();\n}\n\nrpl::producer<not_null<QWheelEvent*>> TopPeersStrip::verticalScrollEvents() const {\n\treturn _verticalScrollEvents.events();\n}\n\nvoid TopPeersStrip::stripWheelEvent(QWheelEvent *e) {\n\tconst auto phase = e->phase();\n\tconst auto fullDelta = e->pixelDelta().isNull()\n\t\t? e->angleDelta()\n\t\t: e->pixelDelta();\n\tif (phase == Qt::ScrollBegin || phase == Qt::ScrollEnd) {\n\t\t_scrollingLock = Qt::Orientation();\n\t\tif (fullDelta.isNull()) {\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto vertical = qAbs(fullDelta.x()) < qAbs(fullDelta.y());\n\tif (_scrollingLock == Qt::Orientation() && phase != Qt::NoScrollPhase) {\n\t\t_scrollingLock = vertical ? Qt::Vertical : Qt::Horizontal;\n\t}\n\tif (_scrollingLock == Qt::Vertical || (vertical && !_scrollLeftMax)) {\n\t\t_verticalScrollEvents.fire(e);\n\t\treturn;\n\t} else if (_expandAnimation.animating()) {\n\t\treturn;\n\t}\n\tconst auto delta = vertical\n\t\t? fullDelta.y()\n\t\t: ((style::RightToLeft() ? -1 : 1) * fullDelta.x());\n\n\tconst auto now = _scrollLeft;\n\tconst auto used = now - delta;\n\tconst auto next = std::clamp(used, 0, _scrollLeftMax);\n\tif (next != now) {\n\t\t_scrollLeft = next;\n\t\tunsubscribeUserpics();\n\t\tupdateSelected();\n\t\tupdate();\n\t}\n\te->accept();\n}\n\nvoid TopPeersStrip::stripLeaveEvent(QEvent *e) {\n\tif (!_selectionByKeyboard) {\n\t\tclearSelection();\n\t}\n\tif (!_dragging) {\n\t\t_lastMousePosition = std::nullopt;\n\t}\n}\n\nvoid TopPeersStrip::stripMousePressEvent(QMouseEvent *e) {\n\tif (e->button() != Qt::LeftButton) {\n\t\treturn;\n\t}\n\t_lastMousePosition = e->globalPos();\n\t_selectionByKeyboard = false;\n\tupdateSelected();\n\n\t_mouseDownPosition = _lastMousePosition;\n\t_pressed = _selected;\n\n\tif (_selected >= 0) {\n\t\tAssert(_selected < _entries.size());\n\t\tauto &entry = _entries[_selected];\n\t\tif (!entry.ripple) {\n\t\t\tentry.ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\tUi::RippleAnimation::RoundRectMask(\n\t\t\t\t\tinnerRounded().size(),\n\t\t\t\t\tst::topPeersRadius),\n\t\t\t\t[=] { update(); });\n\t\t}\n\t\tconst auto layout = currentLayout();\n\t\tconst auto expanded = _expanded.current();\n\t\tconst auto row = expanded ? (_selected / layout.inrow) : 0;\n\t\tconst auto column = (_selected - (row * layout.inrow));\n\t\tconst auto x = layout.added + column * layout.fsingle - scrollLeft();\n\t\tconst auto y = row * st::topPeers.height;\n\t\tentry.ripple->add(e->pos() - QPoint(\n\t\t\tx + st::topPeersMargin.left(),\n\t\t\ty + st::topPeersMargin.top()));\n\n\t\t_presses.fire_copy(entry.id);\n\t}\n}\n\nvoid TopPeersStrip::stripMouseMoveEvent(QMouseEvent *e) {\n\tif (!_lastMousePosition) {\n\t\t_lastMousePosition = e->globalPos();\n\t\tif (_selectionByKeyboard) {\n\t\t\treturn;\n\t\t}\n\t} else if (_selectionByKeyboard\n\t\t&& (_lastMousePosition == e->globalPos())) {\n\t\treturn;\n\t}\n\tselectByMouse(e->globalPos());\n\n\tif (!_dragging && _mouseDownPosition) {\n\t\tif ((*_lastMousePosition - *_mouseDownPosition).manhattanLength()\n\t\t\t>= QApplication::startDragDistance()) {\n\t\t\t_pressCancelled.fire({});\n\t\t\tif (!_expandAnimation.animating()) {\n\t\t\t\t_dragging = true;\n\t\t\t\t_startDraggingLeft = _scrollLeft;\n\t\t\t}\n\t\t}\n\t}\n\tcheckDragging();\n}\n\nvoid TopPeersStrip::selectByMouse(QPoint globalPosition) {\n\t_lastMousePosition = globalPosition;\n\t_selectionByKeyboard = false;\n\tupdateSelected();\n}\n\nvoid TopPeersStrip::checkDragging() {\n\tif (_dragging && !_expandAnimation.animating()) {\n\t\tconst auto sign = (style::RightToLeft() ? -1 : 1);\n\t\tconst auto newLeft = std::clamp(\n\t\t\t(sign * (_mouseDownPosition->x() - _lastMousePosition->x())\n\t\t\t\t+ _startDraggingLeft),\n\t\t\t0,\n\t\t\t_scrollLeftMax);\n\t\tif (newLeft != _scrollLeft) {\n\t\t\t_scrollLeft = newLeft;\n\t\t\tunsubscribeUserpics();\n\t\t\tupdate();\n\t\t}\n\t}\n}\n\nvoid TopPeersStrip::unsubscribeUserpics(bool all) {\n\tif (!all && (_expandAnimation.animating() || _expanded.current())) {\n\t\treturn;\n\t}\n\tconst auto single = outer().width();\n\tauto x = -_scrollLeft;\n\tfor (auto &entry : _entries) {\n\t\tif (all || x + single <= 0 || x >= width()) {\n\t\t\tif (entry.subscribed) {\n\t\t\t\tentry.userpic->subscribeToUpdates(nullptr);\n\t\t\t\tentry.subscribed = false;\n\t\t\t}\n\t\t\tentry.userpicFrame = QImage();\n\t\t\tentry.onlineShown.stop();\n\t\t\tentry.ripple = nullptr;\n\t\t}\n\t\tx += single;\n\t}\n}\n\nvoid TopPeersStrip::subscribeUserpic(Entry &entry) {\n\tconst auto raw = entry.userpic.get();\n\tentry.userpic->subscribeToUpdates([=] {\n\t\tconst auto i = ranges::find(\n\t\t\t_entries,\n\t\t\traw,\n\t\t\t[&](const Entry &entry) { return entry.userpic.get(); });\n\t\tif (i != end(_entries)) {\n\t\t\ti->userpicFrameDirty = 1;\n\t\t}\n\t\tupdate();\n\t});\n\tentry.subscribed = true;\n}\n\nvoid TopPeersStrip::stripMouseReleaseEvent(QMouseEvent *e) {\n\t_pressCancelled.fire({});\n\n\t_lastMousePosition = e->globalPos();\n\tconst auto guard = gsl::finally([&] {\n\t\t_mouseDownPosition = std::nullopt;\n\t});\n\n\tconst auto pressed = clearPressed();\n\tif (finishDragging()) {\n\t\treturn;\n\t}\n\t_selectionByKeyboard = false;\n\tupdateSelected();\n\tif (_selected >= 0 && _selected == pressed) {\n\t\tAssert(_selected < _entries.size());\n\t\t_clicks.fire_copy(_entries[_selected].id);\n\t}\n}\n\nint TopPeersStrip::clearPressed() {\n\tconst auto pressed = std::exchange(_pressed, -1);\n\tif (pressed >= 0) {\n\t\tAssert(pressed < _entries.size());\n\t\tauto &entry = _entries[pressed];\n\t\tif (entry.ripple) {\n\t\t\tentry.ripple->lastStop();\n\t\t}\n\t}\n\treturn pressed;\n}\n\nvoid TopPeersStrip::updateScrollMax(int newWidth) {\n\tif (_expandAnimation.animating()) {\n\t\treturn;\n\t} else if (!newWidth) {\n\t\tnewWidth = width();\n\t}\n\tif (_expanded.current()) {\n\t\t_scrollLeft = 0;\n\t\t_scrollLeftMax = 0;\n\t} else {\n\t\tconst auto single = outer().width();\n\t\tconst auto widthFull = int(_entries.size()) * single;\n\t\t_scrollLeftMax = std::max(widthFull - newWidth, 0);\n\t\t_scrollLeft = std::clamp(_scrollLeft, 0, _scrollLeftMax);\n\t}\n\tunsubscribeUserpics();\n\tupdate();\n}\n\nbool TopPeersStrip::empty() const {\n\treturn !_count.current();\n}\n\nrpl::producer<bool> TopPeersStrip::emptyValue() const {\n\treturn _count.value()\n\t\t| rpl::map(!rpl::mappers::_1)\n\t\t| rpl::distinct_until_changed();\n}\n\nrpl::producer<uint64> TopPeersStrip::clicks() const {\n\treturn _clicks.events();\n}\n\nrpl::producer<uint64> TopPeersStrip::pressed() const {\n\treturn _presses.events();\n}\n\nrpl::producer<> TopPeersStrip::pressCancelled() const {\n\treturn _pressCancelled.events();\n}\n\nvoid TopPeersStrip::pressLeftToContextMenu(bool shown) {\n\tif (!shown) {\n\t\t_contexted = -1;\n\t\tupdate();\n\t\treturn;\n\t}\n\t_contexted = clearPressed();\n\tif (finishDragging()) {\n\t\treturn;\n\t}\n\t_mouseDownPosition = std::nullopt;\n}\n\nauto TopPeersStrip::showMenuRequests() const\n-> rpl::producer<ShowTopPeerMenuRequest> {\n\treturn _showMenuRequests.events();\n}\n\nauto TopPeersStrip::scrollToRequests() const\n-> rpl::producer<Ui::ScrollToRequest> {\n\treturn _scrollToRequests.events();\n}\n\nvoid TopPeersStrip::removeLocally(uint64 id) {\n\tif (!id) {\n\t\tunsubscribeUserpics(true);\n\t\tsetSelected(-1);\n\t\t_pressed = -1;\n\t\t_entries.clear();\n\t\t_hiddenLocally = true;\n\t\t_count = 0;\n\t\treturn;\n\t}\n\t_removed.emplace(id);\n\tconst auto i = ranges::find(_entries, id, &Entry::id);\n\tif (i == end(_entries)) {\n\t\treturn;\n\t} else if (i->subscribed) {\n\t\ti->userpic->subscribeToUpdates(nullptr);\n\t}\n\tconst auto index = int(i - begin(_entries));\n\t_entries.erase(i);\n\tif (_selected > index) {\n\t\t--_selected;\n\t}\n\tif (_pressed > index) {\n\t\t--_pressed;\n\t}\n\tif (_contexted > index) {\n\t\t--_contexted;\n\t}\n\tupdateScrollMax();\n\t_count = int(_entries.size());\n\tupdate();\n}\n\nbool TopPeersStrip::selectedByKeyboard() const {\n\treturn _selectionByKeyboard && _selected >= 0;\n}\n\nbool TopPeersStrip::selectByKeyboard(Qt::Key direction) {\n\tif (_entries.empty()) {\n\t\treturn false;\n\t} else if (direction == Qt::Key()) {\n\t\t_selectionByKeyboard = true;\n\t\tif (_selected < 0) {\n\t\t\tsetSelected(0);\n\t\t\tscrollToSelected();\n\t\t\treturn true;\n\t\t}\n\t} else if (direction == Qt::Key_Left) {\n\t\tif (_selected > 0) {\n\t\t\t_selectionByKeyboard = true;\n\t\t\tsetSelected(_selected - 1);\n\t\t\tscrollToSelected();\n\t\t\treturn true;\n\t\t}\n\t} else if (direction == Qt::Key_Right) {\n\t\tif (_selected + 1 < _entries.size()) {\n\t\t\t_selectionByKeyboard = true;\n\t\t\tsetSelected(_selected + 1);\n\t\t\tscrollToSelected();\n\t\t\treturn true;\n\t\t}\n\t} else if (direction == Qt::Key_Up) {\n\t\tconst auto layout = currentLayout();\n\t\tif (_selected < 0) {\n\t\t\t_selectionByKeyboard = true;\n\t\t\tconst auto rows = _expanded.current()\n\t\t\t\t? ((int(_entries.size()) + layout.inrow - 1) / layout.inrow)\n\t\t\t\t: 1;\n\t\t\tsetSelected((rows - 1) * layout.inrow);\n\t\t\tscrollToSelected();\n\t\t\treturn true;\n\t\t} else if (!_expanded.current()) {\n\t\t\tdeselectByKeyboard();\n\t\t} else if (_selected >= 0) {\n\t\t\tconst auto row = _selected / layout.inrow;\n\t\t\tif (row > 0) {\n\t\t\t\t_selectionByKeyboard = true;\n\t\t\t\tsetSelected(_selected - layout.inrow);\n\t\t\t\tscrollToSelected();\n\t\t\t\treturn true;\n\t\t\t} else {\n\t\t\t\tdeselectByKeyboard();\n\t\t\t}\n\t\t}\n\t} else if (direction == Qt::Key_Down) {\n\t\tif (_selected >= 0 && _expanded.current()) {\n\t\t\tconst auto layout = currentLayout();\n\t\t\tconst auto row = _selected / layout.inrow;\n\t\t\tconst auto rows = (int(_entries.size()) + layout.inrow - 1)\n\t\t\t\t/ layout.inrow;\n\t\t\tif (row + 1 < rows) {\n\t\t\t\t_selectionByKeyboard = true;\n\t\t\t\tsetSelected(std::min(\n\t\t\t\t\t_selected + layout.inrow,\n\t\t\t\t\tint(_entries.size()) - 1));\n\t\t\t\tscrollToSelected();\n\t\t\t\treturn true;\n\t\t\t} else {\n\t\t\t\tdeselectByKeyboard();\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid TopPeersStrip::deselectByKeyboard() {\n\tif (_selectionByKeyboard) {\n\t\tsetSelected(-1);\n\t}\n}\n\nbool TopPeersStrip::chooseRow() {\n\tif (_selected >= 0) {\n\t\tAssert(_selected < _entries.size());\n\t\t_clicks.fire_copy(_entries[_selected].id);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nuint64 TopPeersStrip::updateFromParentDrag(QPoint globalPosition) {\n\tif (!rect().contains(mapFromGlobal(globalPosition))) {\n\t\tdragLeft();\n\t\treturn 0;\n\t}\n\tselectByMouse(globalPosition);\n\n\treturn (_selected >= 0) ? _entries[_selected].id : 0;\n}\n\nvoid TopPeersStrip::dragLeft() {\n\tclearSelection();\n}\n\nvoid TopPeersStrip::apply(const TopPeersList &list) {\n\tif (_hiddenLocally) {\n\t\treturn;\n\t}\n\tauto now = std::vector<Entry>();\n\n\tconst auto selectedId = (_selected >= 0) ? _entries[_selected].id : 0;\n\tconst auto pressedId = (_pressed >= 0) ? _entries[_pressed].id : 0;\n\tconst auto contextedId = (_contexted >= 0) ? _entries[_contexted].id : 0;\n\tconst auto restoreIndex = [&](uint64 id) {\n\t\tif (!id) {\n\t\t\treturn -1;\n\t\t}\n\t\tconst auto i = ranges::find(_entries, id, &Entry::id);\n\t\treturn (i != end(_entries)) ? int(i - begin(_entries)) : -1;\n\t};\n\n\tfor (const auto &entry : list.entries) {\n\t\tif (_removed.contains(entry.id)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto i = ranges::find(_entries, entry.id, &Entry::id);\n\t\tif (i != end(_entries)) {\n\t\t\tnow.push_back(base::take(*i));\n\t\t} else {\n\t\t\tnow.push_back({ .id = entry.id });\n\t\t}\n\t\tapply(now.back(), entry);\n\t}\n\tif (now.empty()) {\n\t\t_count = 0;\n\t}\n\tfor (auto &entry : _entries) {\n\t\tif (entry.subscribed) {\n\t\t\tentry.userpic->subscribeToUpdates(nullptr);\n\t\t\tentry.subscribed = false;\n\t\t}\n\t}\n\t_entries = std::move(now);\n\t_selected = restoreIndex(selectedId);\n\t_pressed = restoreIndex(pressedId);\n\t_contexted = restoreIndex(contextedId);\n\tupdateScrollMax();\n\tunsubscribeUserpics();\n\t_count = int(_entries.size());\n\tupdate();\n}\n\nvoid TopPeersStrip::apply(Entry &entry, const TopPeersEntry &data) {\n\tExpects(entry.id == data.id);\n\tExpects(data.userpic != nullptr);\n\n\tif (entry.name.toString() != data.name) {\n\t\tentry.name.setText(st::topPeers.nameStyle, data.name);\n\t}\n\tif (entry.userpic.get() != data.userpic.get()) {\n\t\tif (entry.subscribed) {\n\t\t\tentry.userpic->subscribeToUpdates(nullptr);\n\t\t}\n\t\tentry.userpic = data.userpic;\n\t\tif (entry.subscribed) {\n\t\t\tsubscribeUserpic(entry);\n\t\t}\n\t}\n\tif (entry.online != data.online) {\n\t\tentry.online = data.online;\n\t\tif (!entry.subscribed) {\n\t\t\tentry.onlineShown.stop();\n\t\t} else {\n\t\t\tentry.onlineShown.start(\n\t\t\t\t[=] { update(); },\n\t\t\t\tentry.online ? 0. : 1.,\n\t\t\t\tentry.online ? 1. : 0.,\n\t\t\t\tst::dialogsOnlineBadgeDuration);\n\t\t}\n\t}\n\tif (entry.badge != data.badge) {\n\t\tentry.badge = data.badge;\n\t\tentry.badgeString = QString();\n\t\tentry.userpicFrameDirty = 1;\n\t}\n\tif (entry.unread != data.unread) {\n\t\tentry.unread = data.unread;\n\t\tif (!entry.badge) {\n\t\t\tentry.userpicFrameDirty = 1;\n\t\t}\n\t}\n\tif (entry.muted != data.muted) {\n\t\tentry.muted = data.muted;\n\t\tif (entry.badge || entry.unread) {\n\t\t\tentry.userpicFrameDirty = 1;\n\t\t}\n\t}\n}\n\nQRect TopPeersStrip::outer() const {\n\tconst auto &st = st::topPeers;\n\tconst auto single = st.photoLeft * 2 + st.photo;\n\treturn QRect(0, 0, single, st::topPeers.height);\n}\n\nQRect TopPeersStrip::innerRounded() const {\n\treturn outer().marginsRemoved(st::topPeersMargin);\n}\n\nint TopPeersStrip::scrollLeft() const {\n\tconst auto value = _expandAnimation.value(_expanded.current() ? 1. : 0.);\n\treturn anim::interpolate(_scrollLeft, 0, value);\n}\n\nvoid TopPeersStrip::paintStrip(QRect clip) {\n\tauto p = Painter(&_strip);\n\n\tconst auto &st = st::topPeers;\n\tconst auto scroll = scrollLeft();\n\n\tconst auto rows = (height() + st.height - 1) / st.height;\n\tconst auto fromrow = std::min(clip.y() / st.height, rows);\n\tconst auto tillrow = std::min(\n\t\t(clip.y() + clip.height() + st.height - 1) / st.height,\n\t\trows);\n\tconst auto layout = currentLayout();\n\tconst auto fsingle = layout.fsingle;\n\tconst auto added = layout.added;\n\n\tfor (auto row = fromrow; row != tillrow; ++row) {\n\t\tconst auto shift = scroll + row * layout.inrow * fsingle;\n\t\tconst auto from = std::min(\n\t\t\tint(std::floor((shift + clip.x()) / fsingle)),\n\t\t\tint(_entries.size()));\n\t\tconst auto till = std::clamp(\n\t\t\tint(std::ceil(\n\t\t\t\t(shift + clip.x() + clip.width() + fsingle - 1) / fsingle + 1\n\t\t\t)),\n\t\t\tfrom,\n\t\t\tint(_entries.size()));\n\n\t\tauto x = int(base::SafeRound(-shift + from * fsingle + added));\n\t\tauto y = row * st.height;\n\t\tconst auto highlighted = (_contexted >= 0)\n\t\t\t? _contexted\n\t\t\t: (_pressed >= 0)\n\t\t\t? _pressed\n\t\t\t: _selected;\n\t\tfor (auto i = from; i != till; ++i) {\n\t\t\tauto &entry = _entries[i];\n\t\t\tconst auto selected = (i == highlighted);\n\t\t\tif (selected) {\n\t\t\t\t_selection.paint(p, innerRounded().translated(x, y));\n\t\t\t}\n\t\t\tif (entry.ripple) {\n\t\t\t\tentry.ripple->paint(\n\t\t\t\t\tp,\n\t\t\t\t\tx + st::topPeersMargin.left(),\n\t\t\t\t\ty + st::topPeersMargin.top(),\n\t\t\t\t\twidth());\n\t\t\t\tif (entry.ripple->empty()) {\n\t\t\t\t\tentry.ripple = nullptr;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!entry.subscribed) {\n\t\t\t\tsubscribeUserpic(entry);\n\t\t\t}\n\t\t\tpaintUserpic(p, x, y, i, selected);\n\n\t\t\tp.setPen(st::dialogsNameFg);\n\t\t\tentry.name.drawElided(\n\t\t\t\tp,\n\t\t\t\tx + st.nameLeft,\n\t\t\t\ty + st.nameTop,\n\t\t\t\tlayout.single - 2 * st.nameLeft,\n\t\t\t\t1,\n\t\t\t\tstyle::al_top);\n\t\t\tx += fsingle;\n\t\t}\n\t}\n}\n\nvoid TopPeersStrip::paintUserpic(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint index,\n\t\tbool selected) {\n\tExpects(index >= 0 && index < _entries.size());\n\n\tauto &entry = _entries[index];\n\tconst auto &st = st::topPeers;\n\tconst auto size = st.photo;\n\tconst auto rect = QRect(x + st.photoLeft, y + st.photoTop, size, size);\n\n\tconst auto online = entry.onlineShown.value(entry.online ? 1. : 0.);\n\tconst auto useFrame = !entry.userpicFrame.isNull()\n\t\t&& !entry.userpicFrameDirty\n\t\t&& (entry.userpicFrameOnline == online);\n\tif (useFrame) {\n\t\tp.drawImage(rect, entry.userpicFrame);\n\t\treturn;\n\t}\n\tconst auto simple = entry.userpic->image(size);\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto renderFrame = (online > 0) || entry.badge || entry.unread;\n\tif (!renderFrame) {\n\t\tentry.userpicFrame = QImage();\n\t\tp.drawImage(rect, simple);\n\t\treturn;\n\t} else if (entry.userpicFrame.size() != QSize(size, size) * ratio) {\n\t\tentry.userpicFrame = QImage(\n\t\t\tQSize(size, size) * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tentry.userpicFrame.setDevicePixelRatio(ratio);\n\t}\n\tentry.userpicFrame.fill(Qt::transparent);\n\tentry.userpicFrameDirty = 0;\n\tentry.userpicFrameOnline = online;\n\n\tauto q = QPainter(&entry.userpicFrame);\n\tconst auto inner = QRect(0, 0, size, size);\n\tq.drawImage(inner, simple);\n\n\tauto hq = PainterHighQualityEnabler(q);\n\n\tif (online > 0) {\n\t\tq.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tconst auto onlineSize = st::dialogsOnlineBadgeSize;\n\t\tconst auto stroke = st::dialogsOnlineBadgeStroke;\n\t\tconst auto skip = st::dialogsOnlineBadgeSkip;\n\t\tconst auto shrink = (onlineSize / 2) * (1. - online);\n\n\t\tauto pen = QPen(Qt::transparent);\n\t\tpen.setWidthF(stroke * online);\n\t\tq.setPen(pen);\n\t\tq.setBrush(st::dialogsOnlineBadgeFg);\n\t\tq.drawEllipse(QRectF(\n\t\t\tsize - skip.x() - onlineSize,\n\t\t\tsize - skip.y() - onlineSize,\n\t\t\tonlineSize,\n\t\t\tonlineSize\n\t\t).marginsRemoved({ shrink, shrink, shrink, shrink }));\n\t\tq.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t}\n\n\tif (entry.badge || entry.unread) {\n\t\tif (entry.badgeString.isEmpty()) {\n\t\t\tentry.badgeString = !entry.badge\n\t\t\t\t? u\" \"_q\n\t\t\t\t: (entry.badge < 1000)\n\t\t\t\t? QString::number(entry.badge)\n\t\t\t\t: (QString::number(entry.badge / 1000) + 'K');\n\t\t}\n\t\tauto st = Ui::UnreadBadgeStyle();\n\t\tst.selected = selected;\n\t\tst.muted = entry.muted;\n\t\tconst auto &counter = entry.badgeString;\n\t\tconst auto badge = PaintUnreadBadge(q, counter, size, 0, st);\n\n\t\tconst auto width = style::ConvertScaleExact(2.);\n\t\tconst auto add = (width - style::ConvertScaleExact(1.)) / 2.;\n\t\tauto pen = QPen(Qt::transparent);\n\t\tpen.setWidthF(width);\n\t\tq.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tq.setPen(pen);\n\t\tq.setBrush(Qt::NoBrush);\n\t\tq.drawEllipse(QRectF(badge).marginsAdded({ add, add, add, add }));\n\t}\n\n\tq.end();\n\n\tp.drawImage(rect, entry.userpicFrame);\n}\n\nvoid TopPeersStrip::stripContextMenuEvent(QContextMenuEvent *e) {\n\t_menu = nullptr;\n\n\tif (e->reason() == QContextMenuEvent::Mouse) {\n\t\t_lastMousePosition = e->globalPos();\n\t\t_selectionByKeyboard = false;\n\t\tupdateSelected();\n\t}\n\tif (_selected < 0 || _entries.empty()) {\n\t\treturn;\n\t}\n\tAssert(_selected < _entries.size());\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\tst::popupMenuWithIcons);\n\t_showMenuRequests.fire({\n\t\t_entries[_selected].id,\n\t\tUi::Menu::CreateAddActionCallback(_menu),\n\t});\n\tif (_menu->empty()) {\n\t\t_menu = nullptr;\n\t\treturn;\n\t}\n\tconst auto updateAfterMenuDestroyed = [=] {\n\t\tconst auto globalPosition = QCursor::pos();\n\t\tif (rect().contains(mapFromGlobal(globalPosition))) {\n\t\t\t_lastMousePosition = globalPosition;\n\t\t\t_selectionByKeyboard = false;\n\t\t\tupdateSelected();\n\t\t}\n\t};\n\tQObject::connect(\n\t\t_menu.get(),\n\t\t&QObject::destroyed,\n\t\tcrl::guard(&_menuGuard, updateAfterMenuDestroyed));\n\t_menu->popup(e->globalPos());\n\te->accept();\n}\n\nbool TopPeersStrip::finishDragging() {\n\tif (!_dragging) {\n\t\treturn false;\n\t}\n\tcheckDragging();\n\t_dragging = false;\n\t_selectionByKeyboard = false;\n\tupdateSelected();\n\treturn true;\n}\n\nTopPeersStrip::Layout TopPeersStrip::currentLayout() const {\n\tconst auto single = outer().width();\n\tconst auto inrow = std::max(width() / single, 1);\n\tconst auto value = _expandAnimation.value(_expanded.current() ? 1. : 0.);\n\tconst auto esingle = (width() / float64(inrow));\n\tconst auto fsingle = single + (esingle - single) * value;\n\n\treturn {\n\t\t.single = single,\n\t\t.inrow = inrow,\n\t\t.fsingle = fsingle,\n\t\t.added = (fsingle - single) / 2.,\n\t};\n}\n\nvoid TopPeersStrip::updateSelected() {\n\tif (_pressed >= 0 || !_lastMousePosition || _selectionByKeyboard) {\n\t\treturn;\n\t}\n\tconst auto p = _strip.mapFromGlobal(*_lastMousePosition);\n\tconst auto expanded = _expanded.current();\n\tconst auto row = expanded ? (p.y() / st::topPeers.height) : 0;\n\tconst auto layout = currentLayout();\n\tconst auto column = (_scrollLeft + p.x()) / layout.fsingle;\n\tconst auto index = row * layout.inrow + int(std::floor(column));\n\tsetSelected((index < 0 || index >= _entries.size()) ? -1 : index);\n}\n\nvoid TopPeersStrip::setSelected(int selected) {\n\tif (_selected != selected) {\n\t\tconst auto over = (selected >= 0);\n\t\tif (over != (_selected >= 0)) {\n\t\t\tsetCursor(over ? style::cur_pointer : style::cur_default);\n\t\t}\n\t\t_selected = selected;\n\t\tupdate();\n\t}\n}\n\nvoid TopPeersStrip::clearSelection() {\n\tsetSelected(-1);\n}\n\nvoid TopPeersStrip::scrollToSelected() {\n\tif (_selected < 0) {\n\t\treturn;\n\t} else if (_expanded.current()) {\n\t\tconst auto layout = currentLayout();\n\t\tconst auto row = _selected / layout.inrow;\n\t\tconst auto header = _header.height();\n\t\tconst auto top = header + row * st::topPeers.height;\n\t\tconst auto bottom = top + st::topPeers.height;\n\t\t_scrollToRequests.fire({ top - (row ? 0 : header), bottom});\n\t} else {\n\t\tconst auto single = outer().width();\n\t\tconst auto left = _selected * single;\n\t\tconst auto right = left + single;\n\t\tif (_scrollLeft > left) {\n\t\t\t_scrollLeft = std::clamp(left, 0, _scrollLeftMax);\n\t\t} else if (_scrollLeft + width() < right) {\n\t\t\t_scrollLeft = std::clamp(right - width(), 0, _scrollLeftMax);\n\t\t}\n\t\tconst auto height = _header.height() + st::topPeers.height;\n\t\t_scrollToRequests.fire({ 0, height });\n\t}\n}\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/dialogs/ui/top_peers_strip.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/round_rect.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\nclass DynamicImage;\nclass LinkButton;\nstruct ScrollToRequest;\n} // namespace Ui\n\nnamespace Dialogs {\n\nstruct TopPeersEntry {\n\tuint64 id = 0;\n\tQString name;\n\tstd::shared_ptr<Ui::DynamicImage> userpic;\n\tuint32 badge : 28 = 0;\n\tuint32 unread : 1 = 0;\n\tuint32 muted : 1 = 0;\n\tuint32 online : 1 = 0;\n};\n\nstruct TopPeersList {\n\tstd::vector<TopPeersEntry> entries;\n};\n\nstruct ShowTopPeerMenuRequest {\n\tuint64 id = 0;\n\tUi::Menu::MenuCallback callback;\n};\n\nclass TopPeersStrip final : public Ui::RpWidget {\npublic:\n\tTopPeersStrip(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<TopPeersList> content);\n\t~TopPeersStrip();\n\n\t[[nodiscard]] bool empty() const;\n\t[[nodiscard]] rpl::producer<bool> emptyValue() const;\n\t[[nodiscard]] rpl::producer<uint64> clicks() const;\n\t[[nodiscard]] rpl::producer<uint64> pressed() const;\n\t[[nodiscard]] rpl::producer<> pressCancelled() const;\n\t[[nodiscard]] auto showMenuRequests() const\n\t\t-> rpl::producer<ShowTopPeerMenuRequest>;\n\t[[nodiscard]] auto scrollToRequests() const\n\t\t-> rpl::producer<Ui::ScrollToRequest>;\n\n\tvoid removeLocally(uint64 id = 0);\n\n\t[[nodiscard]] bool selectedByKeyboard() const;\n\tbool selectByKeyboard(Qt::Key direction);\n\tvoid deselectByKeyboard();\n\tbool chooseRow();\n\tvoid pressLeftToContextMenu(bool shown);\n\n\tuint64 updateFromParentDrag(QPoint globalPosition);\n\tvoid dragLeft();\n\n\t[[nodiscard]] auto verticalScrollEvents() const\n\t\t-> rpl::producer<not_null<QWheelEvent*>>;\n\nprivate:\n\tstruct Entry;\n\tstruct Layout;\n\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid setupHeader();\n\tvoid setupStrip();\n\n\tvoid paintStrip(QRect clip);\n\tvoid stripWheelEvent(QWheelEvent *e);\n\tvoid stripMousePressEvent(QMouseEvent *e);\n\tvoid stripMouseMoveEvent(QMouseEvent *e);\n\tvoid stripMouseReleaseEvent(QMouseEvent *e);\n\tvoid stripContextMenuEvent(QContextMenuEvent *e);\n\tvoid stripLeaveEvent(QEvent *e);\n\n\tvoid updateScrollMax(int newWidth = 0);\n\tvoid updateSelected();\n\tvoid setSelected(int selected);\n\tvoid setExpanded(bool expanded);\n\tvoid scrollToSelected();\n\tvoid checkDragging();\n\tbool finishDragging();\n\tvoid subscribeUserpic(Entry &entry);\n\tvoid unsubscribeUserpics(bool all = false);\n\tvoid paintUserpic(Painter &p, int x, int y, int index, bool selected);\n\tvoid clearSelection();\n\tvoid selectByMouse(QPoint globalPosition);\n\n\t[[nodiscard]] QRect outer() const;\n\t[[nodiscard]] QRect innerRounded() const;\n\t[[nodiscard]] int scrollLeft() const;\n\t[[nodiscard]] Layout currentLayout() const;\n\tint clearPressed();\n\tvoid apply(const TopPeersList &list);\n\tvoid apply(Entry &entry, const TopPeersEntry &data);\n\n\tUi::RpWidget _header;\n\tUi::RpWidget _strip;\n\n\tstd::vector<Entry> _entries;\n\trpl::variable<int> _count = 0;\n\tbase::flat_set<uint64> _removed;\n\trpl::variable<Ui::LinkButton*> _toggleExpanded = nullptr;\n\n\trpl::event_stream<uint64> _clicks;\n\trpl::event_stream<uint64> _presses;\n\trpl::event_stream<> _pressCancelled;\n\trpl::event_stream<ShowTopPeerMenuRequest> _showMenuRequests;\n\trpl::event_stream<not_null<QWheelEvent*>> _verticalScrollEvents;\n\n\tstd::optional<QPoint> _lastMousePosition;\n\tstd::optional<QPoint> _mouseDownPosition;\n\tint _startDraggingLeft = 0;\n\tint _scrollLeft = 0;\n\tint _scrollLeftMax = 0;\n\tbool _dragging = false;\n\tQt::Orientation _scrollingLock = {};\n\n\tint _selected = -1;\n\tint _pressed = -1;\n\tint _contexted = -1;\n\tbool _selectionByKeyboard = false;\n\tbool _hiddenLocally = false;\n\n\tUi::Animations::Simple _expandAnimation;\n\trpl::variable<bool> _expanded = false;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\n\tUi::RoundRect _selection;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\tbase::has_weak_ptr _menuGuard;\n\n};\n\n} // namespace Dialogs\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/color_picker.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/color_picker.h\"\n\n#include \"base/basic_types.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"ui/abstract_button.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/widgets/color_editor.h\"\n#include \"styles/style_editor.h\"\n\n#include <QConicalGradient>\n\n\nnamespace Editor {\nnamespace {\n\nconstexpr auto kMinBrushSize = 0.1;\nconstexpr auto kMinBrushWidth = 1.;\nconstexpr auto kMaxBrushWidth = 25.;\n\nconstexpr auto kCircleDuration = crl::time(200);\nconstexpr auto kSizeControlSwitchDuration = crl::time(140);\nconstexpr auto kColorButtonSwitchDuration = crl::time(140);\n\ninline float64 InterpolateF(float64 a, float64 b, float64 b_ratio) {\n\treturn a + float64(b - a) * b_ratio;\n};\n\ninline float64 InterpolationRatio(int from, int to, int result) {\n\treturn (result - from) / float64(to - from);\n};\n\n[[nodiscard]] int ToolIndex(Brush::Tool tool) {\n\tswitch (tool) {\n\tcase Brush::Tool::Pen: return 0;\n\tcase Brush::Tool::Arrow: return 1;\n\tcase Brush::Tool::Marker: return 2;\n\tcase Brush::Tool::Blur: return 3;\n\tcase Brush::Tool::Eraser: return 4;\n\t}\n\treturn 0;\n}\n\n[[nodiscard]] Brush::Tool ToolFromIndex(int index) {\n\tswitch (index) {\n\tcase 0: return Brush::Tool::Pen;\n\tcase 1: return Brush::Tool::Arrow;\n\tcase 2: return Brush::Tool::Marker;\n\tcase 3: return Brush::Tool::Blur;\n\tcase 4: return Brush::Tool::Eraser;\n\t}\n\treturn Brush::Tool::Pen;\n}\n\n[[nodiscard]] bool FixedColorTool(Brush::Tool tool) {\n\treturn (tool == Brush::Tool::Eraser) || (tool == Brush::Tool::Blur);\n}\n\n[[nodiscard]] QColor FixedToolColor() {\n\treturn QColor(0, 0, 0);\n}\n\nvoid NormalizeBrushColor(Brush &brush) {\n\tif (FixedColorTool(brush.tool)) {\n\t\tbrush.color = FixedToolColor();\n\t}\n}\n\nclass PlusCircle final : public Ui::AbstractButton {\npublic:\n\tusing Ui::AbstractButton::AbstractButton;\n\nprivate:\n\tvoid paintEvent(QPaintEvent *event) override {\n\t\tauto p = QPainter(this);\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tconst auto border = st::photoEditorColorButtonBorder;\n\t\tconst auto half = border / 2.;\n\t\tconst auto rect = QRectF(QWidget::rect())\n\t\t\t.adjusted(half, half, -half, -half);\n\n\t\tp.setPen(border > 0\n\t\t\t? QPen(st::photoEditorColorButtonBorderFg, border)\n\t\t\t: Qt::NoPen);\n\t\tp.setBrush(Qt::NoBrush);\n\n\t\tconst auto lineWidth = st::photoEditorColorPalettePlusLine;\n\t\tauto pen = QPen(st::photoEditorColorPalettePlusFg, lineWidth);\n\t\tpen.setCapStyle(Qt::RoundCap);\n\t\tp.setPen(pen);\n\n\t\tconst auto c = rect::center(rect);\n\t\tconst auto r = rect.width() / 2. - lineWidth * 1.75;\n\t\tp.drawLine(QPointF(c.x() - r, c.y()), QPointF(c.x() + r, c.y()));\n\t\tp.drawLine(QPointF(c.x(), c.y() - r), QPointF(c.x(), c.y() + r));\n\t}\n};\n\nclass ColorButton final : public Ui::AbstractButton {\npublic:\n\tColorButton(\n\t\tnot_null<QWidget*> parent,\n\t\tFn<QColor()> color)\n\t: AbstractButton(parent)\n\t, _color(std::move(color)) {\n\t}\n\nprivate:\n\tvoid paintEvent(QPaintEvent *event) override {\n\t\tauto p = QPainter(this);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tconst auto size = std::min(width(), height());\n\t\tif (size <= 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto left = (width() - size) / 2.;\n\t\tconst auto top = (height() - size) / 2.;\n\t\tconst auto outer = QRectF(left, top, size, size);\n\t\tconst auto ringWidth = float64(std::max(\n\t\t\tst::photoEditorColorButtonBorder,\n\t\t\tst::photoEditorColorPaletteSelectionWidth));\n\t\tconst auto ringHalf = ringWidth / 2.;\n\t\tconst auto ringRect = outer.adjusted(\n\t\t\tringHalf,\n\t\t\tringHalf,\n\t\t\t-ringHalf,\n\t\t\t-ringHalf);\n\n\t\tauto gradient = QConicalGradient(outer.center(), 15.);\n\t\tgradient.setColorAt(0. / 7., QColor(0xEB, 0x4B, 0x4B));\n\t\tgradient.setColorAt(1. / 7., QColor(0xFF, 0xA5, 0x00));\n\t\tgradient.setColorAt(2. / 7., QColor(0xFF, 0xFF, 0x00));\n\t\tgradient.setColorAt(3. / 7., QColor(0x8F, 0xCE, 0x00));\n\t\tgradient.setColorAt(4. / 7., QColor(0x00, 0xFF, 0xFF));\n\t\tgradient.setColorAt(5. / 7., QColor(0x60, 0x80, 0xE4));\n\t\tgradient.setColorAt(6. / 7., QColor(0xEE, 0x82, 0xEE));\n\t\tgradient.setColorAt(7. / 7., QColor(0xEB, 0x4B, 0x4B));\n\n\t\tauto pen = QPen(QBrush(gradient), ringWidth);\n\t\tpen.setCapStyle(Qt::RoundCap);\n\t\tp.setPen(pen);\n\t\tp.setBrush(Qt::NoBrush);\n\t\tp.drawEllipse(ringRect);\n\n\t\tconst auto innerInset = ringWidth\n\t\t\t+ st::photoEditorColorButtonBorder * 1.5;\n\t\tif (outer.width() <= innerInset * 2.\n\t\t\t|| outer.height() <= innerInset * 2.) {\n\t\t\treturn;\n\t\t}\n\t\tauto innerRect = outer.adjusted(\n\t\t\tinnerInset,\n\t\t\tinnerInset,\n\t\t\t-innerInset,\n\t\t\t-innerInset);\n\t\tconst auto innerBorder = float64(st::photoEditorColorButtonBorder);\n\t\tif (innerBorder > 0.) {\n\t\t\tconst auto innerHalf = innerBorder / 2.;\n\t\t\tinnerRect = innerRect.adjusted(\n\t\t\t\tinnerHalf,\n\t\t\t\tinnerHalf,\n\t\t\t\t-innerHalf,\n\t\t\t\t-innerHalf);\n\t\t}\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(_color());\n\t\tp.drawEllipse(innerRect);\n\t}\n\n\tFn<QColor()> _color;\n\n};\n\nclass ToolLottieButton final : public Ui::AbstractButton {\npublic:\n\tToolLottieButton(\n\t\tnot_null<QWidget*> parent,\n\t\tconst QString &path)\n\t: AbstractButton(parent)\n\t, _icon(Lottie::MakeIcon({\n\t\t.path = path,\n\t\t.sizeOverride = Size(st::photoEditorToolButtonIconSize),\n\t\t.frame = 0,\n\t})) {\n\t\tevents(\n\t\t) | rpl::on_next([=](not_null<QEvent*> event) {\n\t\t\tconst auto type = event->type();\n\t\t\tif (type == QEvent::Enter) {\n\t\t\t\tif (_hovered) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t_hovered = true;\n\t\t\t\t_resetPending = false;\n\t\t\t\tif (_icon && _icon->valid() && !_icon->animating()) {\n\t\t\t\t\tif (_icon->frameIndex() != 0) {\n\t\t\t\t\t\t_icon->jumpTo(0, [=] { update(); });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tplayOnce();\n\t\t\t} else if (type == QEvent::Leave) {\n\t\t\t\t_hovered = false;\n\t\t\t\tif (_icon && _icon->animating()) {\n\t\t\t\t\t_resetPending = true;\n\t\t\t\t} else {\n\t\t\t\t\treset();\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime());\n\t}\n\nprivate:\n\tvoid paintEvent(QPaintEvent *event) override {\n\t\tauto p = Painter(this);\n\t\tif (_icon) {\n\t\t\t_icon->paintInCenter(p, rect());\n\t\t\tif (_resetPending && !_icon->animating()) {\n\t\t\t\t_resetPending = false;\n\t\t\t\treset();\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid playOnce() {\n\t\tif (!_icon || !_icon->valid()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto count = _icon->framesCount();\n\t\tif (count <= 0) {\n\t\t\treturn;\n\t\t}\n\t\t_icon->animate(\n\t\t\t[=] { update(); },\n\t\t\t0,\n\t\t\tcount - 1,\n\t\t\tcrl::time(st::photoEditorToolButtonHoverDuration));\n\t}\n\n\tvoid reset() {\n\t\tif (!_icon || !_icon->valid()) {\n\t\t\treturn;\n\t\t}\n\t\tif (_icon->frameIndex() != 0) {\n\t\t\t_icon->jumpTo(0, [=] { update(); });\n\t\t}\n\t}\n\n\tstd::unique_ptr<Lottie::Icon> _icon;\n\tbool _hovered = false;\n\tbool _resetPending = false;\n};\n\nstd::vector<QColor> PaletteColors() {\n\treturn {\n\t\tQColor(0, 0, 0),\n\t\tQColor(255, 255, 255),\n\t\tQColor(234, 39, 57),\n\t\tQColor(252, 150, 77),\n\t\tQColor(252, 222, 101),\n\t\tQColor(128, 200, 100),\n\t\tQColor(73, 197, 237),\n\t\tQColor(48, 81, 227),\n\t\tQColor(219, 58, 210),\n\t\tQColor(255, 114, 169),\n\t};\n}\n\n} // namespace\n\nColorPicker::ColorPicker(\n\tnot_null<Ui::RpWidget*> parent,\n\tstd::shared_ptr<Ui::Show> show,\n\tconst std::array<Brush, 5> &savedBrushes,\n\tBrush::Tool savedTool)\n: _parent(parent)\n, _show(std::move(show))\n, _colorButton(base::make_unique_q<ColorButton>(parent, [=] {\n\treturn colorButtonColor();\n}))\n, _paletteWrap(std::in_place, parent)\n, _sizeControlHoverArea(std::in_place, parent)\n, _sizeControl(std::in_place, parent)\n, _toolSelection(std::in_place, parent)\n, _brush(savedBrushes[ToolIndex(savedTool)])\n, _toolBrushes(savedBrushes) {\n\t_colorButton->resize(Size(st::photoEditorColorButtonSize));\n\n\tfor (auto i = 0; i != int(_toolBrushes.size()); ++i) {\n\t\t_toolBrushes[i].tool = ToolFromIndex(i);\n\t\tNormalizeBrushColor(_toolBrushes[i]);\n\t}\n\t_brush = _toolBrushes[ToolIndex(savedTool)];\n\t_brush.tool = savedTool;\n\tNormalizeBrushColor(_brush);\n\t_colorButtonFrom = _brush.color;\n\t_colorButtonTo = _brush.color;\n\n\t_toolSelection->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_toolSelection->setAttribute(Qt::WA_TranslucentBackground, true);\n\t_toolSelection->setVisible(false);\n\t_toolSelection->paintOn([=](QPainter &p) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setOpacity(st::photoEditorToolButtonSelectedOpacity);\n\t\tauto pen = QPen(\n\t\t\tst::photoEditorToolButtonSelectedFg,\n\t\t\tst::photoEditorToolButtonSelectedWidth);\n\t\tpen.setCapStyle(Qt::RoundCap);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::photoEditorToolButtonSelectedFg);\n\t\tconst auto padding = st::photoEditorToolButtonSelectedPadding;\n\t\tconst auto half = pen.widthF() / 2.;\n\t\tconst auto rect = QRectF(_toolSelection->rect()).adjusted(\n\t\t\tpadding + half,\n\t\t\tpadding + half,\n\t\t\t-padding - half,\n\t\t\t-padding - half);\n\t\tp.drawEllipse(rect);\n\t});\n\n\t_toolButtons.push_back(base::make_unique_q<ToolLottieButton>(\n\t\tparent,\n\t\tu\":/animations/photo_editor_pen.tgs\"_q));\n\t_toolButtons.push_back(base::make_unique_q<ToolLottieButton>(\n\t\tparent,\n\t\tu\":/animations/photo_editor_arrow.tgs\"_q));\n\t_toolButtons.push_back(base::make_unique_q<ToolLottieButton>(\n\t\tparent,\n\t\tu\":/animations/photo_editor_marker.tgs\"_q));\n\t_toolButtons.push_back(base::make_unique_q<ToolLottieButton>(\n\t\tparent,\n\t\tu\":/animations/photo_editor_blur.tgs\"_q));\n\t_toolButtons.push_back(base::make_unique_q<ToolLottieButton>(\n\t\tparent,\n\t\tu\":/animations/photo_editor_eraser.tgs\"_q));\n\tfor (const auto &button : _toolButtons) {\n\t\tbutton->resize(\n\t\t\tst::photoEditorToolButtonSize,\n\t\t\tst::photoEditorToolButtonSize);\n\t\tbutton->show();\n\t}\n\tconst auto setToolRequest = [=](Brush::Tool tool) {\n\t\tsetTool(tool);\n\t};\n\tif (_toolButtons.size() >= 5) {\n\t\t_toolButtons[0]->setClickedCallback([=] {\n\t\t\tsetToolRequest(Brush::Tool::Pen);\n\t\t});\n\t\t_toolButtons[1]->setClickedCallback([=] {\n\t\t\tsetToolRequest(Brush::Tool::Arrow);\n\t\t});\n\t\t_toolButtons[2]->setClickedCallback([=] {\n\t\t\tsetToolRequest(Brush::Tool::Marker);\n\t\t});\n\t\t_toolButtons[3]->setClickedCallback([=] {\n\t\t\tsetToolRequest(Brush::Tool::Blur);\n\t\t});\n\t\t_toolButtons[4]->setClickedCallback([=] {\n\t\t\tsetToolRequest(Brush::Tool::Eraser);\n\t\t});\n\t}\n\tupdateToolSelection(false);\n\t_paletteWrap->setVisible(false);\n\t_sizeControl->resize(\n\t\tst::photoEditorBrushSizeControlHitPadding * 2\n\t\t\t+ st::photoEditorBrushSizeControlExpandShift\n\t\t\t+ st::photoEditorBrushSizeControlExpandedTopWidth,\n\t\tst::photoEditorBrushSizeControlHeight);\n\t_sizeControlHoverArea->setMouseTracking(true);\n\t_sizeControl->setMouseTracking(true);\n\n\tupdateSizeControlPositionFromRatio(false);\n\tmoveSizeControl(_parent->size());\n\n\t_colorButton->setClickedCallback([=] {\n\t\tsetPaletteVisible(!_paletteVisible);\n\t});\n\n\t_sizeControl->paintOn([=](QPainter &p) {\n\t\tpaintSizeControl(p);\n\t});\n\n\t_parent->sizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\tmoveSizeControl(size);\n\t\tif (_paletteVisible) {\n\t\t\trebuildPalette();\n\t\t}\n\t}, _sizeControl->lifetime());\n\n\t_sizeControlHoverArea->events(\n\t) | rpl::on_next([=](not_null<QEvent*> event) {\n\t\tconst auto type = event->type();\n\t\tif (type == QEvent::Enter) {\n\t\t\t_sizeHoverAreaHovered = true;\n\t\t\tupdateSizeControlExpanded();\n\t\t} else if (type == QEvent::Leave) {\n\t\t\t_sizeHoverAreaHovered = false;\n\t\t\tupdateSizeControlExpanded();\n\t\t}\n\t}, _sizeControlHoverArea->lifetime());\n\n\t_sizeControl->events(\n\t) | rpl::on_next([=](not_null<QEvent*> event) {\n\t\tconst auto type = event->type();\n\t\tif (type == QEvent::Enter) {\n\t\t\t_sizeControlHovered = true;\n\t\t\tupdateSizeControlExpanded();\n\t\t\treturn;\n\t\t} else if (type == QEvent::Leave) {\n\t\t\t_sizeControlHovered = false;\n\t\t\tupdateSizeControlExpanded();\n\t\t\treturn;\n\t\t}\n\t\tconst auto isPress = (type == QEvent::MouseButtonPress)\n\t\t\t|| (type == QEvent::MouseButtonDblClick);\n\t\tconst auto isMove = (type == QEvent::MouseMove);\n\t\tconst auto isRelease = (type == QEvent::MouseButtonRelease);\n\t\tif (!isPress && !isMove && !isRelease) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto e = static_cast<QMouseEvent*>(event.get());\n\t\tif (isPress) {\n\t\t\tif (e->button() != Qt::LeftButton) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto progress = _sizeControlAnimation.value(\n\t\t\t\t_sizeControlExpanded ? 1. : 0.);\n\t\t\tconst auto inHandle = sizeControlHandleRect(progress).contains(\n\t\t\t\te->pos());\n\t\t\tconst auto inControl = sizeControlHitRect(progress).contains(\n\t\t\t\te->pos());\n\t\t\tif (!inHandle && !inControl) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_sizeControlPositionAnimation.stop();\n\t\t\t_sizeDown.pressed = true;\n\t\t\tupdateSizeControlMousePosition(e->pos().y());\n\t\t\tupdateSizeControlExpanded();\n\t\t\t_sizeControl->update();\n\t\t\treturn;\n\t\t}\n\t\tif (!_sizeDown.pressed) {\n\t\t\treturn;\n\t\t}\n\t\tif (isMove) {\n\t\t\tupdateSizeControlMousePosition(e->pos().y());\n\t\t\t_sizeControl->update();\n\t\t\treturn;\n\t\t}\n\t\tif (isRelease && (e->button() == Qt::LeftButton)) {\n\t\t\tupdateSizeControlMousePosition(e->pos().y());\n\t\t\t_sizeDown.pressed = false;\n\t\t\tupdateSizeControlExpanded();\n\t\t\t_saveBrushRequests.fire_copy(_brush);\n\t\t\t_sizeControl->update();\n\t\t}\n\t}, _sizeControl->lifetime());\n\n\trebuildPalette();\n}\n\nvoid ColorPicker::moveLine(const QPoint &position) {\n\t_colorButtonCenter = position;\n\tconst auto gap = st::photoEditorToolButtonGap;\n\tconst auto colorWidth = _colorButton->width();\n\tconst auto colorHeight = _colorButton->height();\n\tconst auto toolSize = st::photoEditorToolButtonSize;\n\tconst auto tools = int(_toolButtons.size());\n\tconst auto totalWidth = colorWidth + tools * toolSize + tools * gap;\n\tconst auto left = position.x() - totalWidth / 2;\n\tconst auto top = position.y() - colorHeight / 2;\n\t_colorButton->move(left, top);\n\tupdateToolButtonsGeometry();\n\tupdateToolSelection(false);\n\tupdatePaletteGeometry();\n}\n\nvoid ColorPicker::setCanvasRect(const QRect &rect) {\n\t_canvasRect = rect;\n\tmoveSizeControl(_parent->size());\n}\n\nvoid ColorPicker::updateToolButtonsGeometry() {\n\tconst auto size = st::photoEditorToolButtonSize;\n\tconst auto extra = st::photoEditorToolButtonSelectedExtra;\n\tconst auto hit = size + extra * 2;\n\tconst auto gap = st::photoEditorToolButtonGap;\n\tauto x = _colorButton->x() + _colorButton->width() + gap;\n\tconst auto y = _colorButton->y()\n\t\t+ (_colorButton->height() - size) / 2;\n\tfor (const auto &button : _toolButtons) {\n\t\tbutton->resize(hit, hit);\n\t\tbutton->move(x - extra, y - extra);\n\t\tx += size + gap;\n\t}\n}\n\nvoid ColorPicker::updateToolSelection(bool animated) {\n\tif (_toolButtons.size() < 5) {\n\t\treturn;\n\t}\n\tconst auto index = ToolIndex(_brush.tool);\n\tif (index < 0 || index >= int(_toolButtons.size())) {\n\t\treturn;\n\t}\n\tconst auto &button = _toolButtons[index];\n\tconst auto target = button->pos();\n\t_toolSelection->resize(button->size());\n\t_toolSelection->raise();\n\tif (!animated || !_toolSelection->isVisible()) {\n\t\t_toolSelectionAnimation.stop();\n\t\t_toolSelection->move(target);\n\t\t_toolSelection->update();\n\t\treturn;\n\t}\n\t_toolSelectionFrom = _toolSelection->pos();\n\t_toolSelectionTo = target;\n\t_toolSelectionAnimation.stop();\n\t_toolSelectionAnimation.start([=] {\n\t\tconst auto progress = _toolSelectionAnimation.value(1.);\n\t\t_toolSelection->move(\n\t\t\tanim::interpolate(\n\t\t\t\t_toolSelectionFrom.x(),\n\t\t\t\t_toolSelectionTo.x(),\n\t\t\t\tprogress),\n\t\t\tanim::interpolate(\n\t\t\t\t_toolSelectionFrom.y(),\n\t\t\t\t_toolSelectionTo.y(),\n\t\t\t\tprogress));\n\t\t_toolSelection->update();\n\t}, 0., 1., crl::time(st::photoEditorToolButtonSelectDuration),\n\t\tanim::easeOutCirc);\n}\n\nvoid ColorPicker::setTool(Brush::Tool tool) {\n\tif (_brush.tool == tool) {\n\t\treturn;\n\t}\n\tstoreCurrentBrush();\n\t_brush = _toolBrushes[ToolIndex(tool)];\n\t_brush.tool = tool;\n\tNormalizeBrushColor(_brush);\n\tupdateSizeControlPositionFromRatio(true);\n\tupdateColorButtonColor(_brush.color, true);\n\tif (_paletteVisible) {\n\t\trebuildPalette();\n\t} else {\n\t\t_colorButton->update();\n\t}\n\tupdateToolSelection(true);\n\t_sizeControl->update();\n\t_saveBrushRequests.fire_copy(_brush);\n}\n\nvoid ColorPicker::storeCurrentBrush() {\n\tNormalizeBrushColor(_brush);\n\t_toolBrushes[ToolIndex(_brush.tool)] = _brush;\n}\n\nvoid ColorPicker::updateColorButtonColor(const QColor &color, bool animated) {\n\tconst auto hasValid = _colorButtonFrom.isValid() && _colorButtonTo.isValid();\n\tconst auto from = hasValid ? colorButtonColor() : color;\n\tconst auto to = color.isValid() ? color : from;\n\tif (from == to) {\n\t\t_colorButtonAnimation.stop();\n\t\t_colorButtonFrom = from;\n\t\t_colorButtonTo = to;\n\t\treturn;\n\t}\n\t_colorButtonFrom = from;\n\t_colorButtonTo = to;\n\t_colorButtonAnimation.stop();\n\tif (animated) {\n\t\t_colorButtonAnimation.start(\n\t\t\t[=] { _colorButton->update(); },\n\t\t\t0.,\n\t\t\t1.,\n\t\t\tkColorButtonSwitchDuration,\n\t\t\tanim::easeOutCirc);\n\t} else {\n\t\t_colorButton->update();\n\t}\n}\n\nQColor ColorPicker::colorButtonColor() const {\n\tconst auto progress = _colorButtonAnimation.value(1.);\n\treturn anim::color(_colorButtonFrom, _colorButtonTo, progress);\n}\n\nvoid ColorPicker::paintSizeControl(QPainter &p) {\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tconst auto progress = _sizeControlAnimation.value(_sizeControlExpanded\n\t\t? 1.\n\t\t: 0.);\n\tconst auto path = sizeControlShapePath(progress);\n\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(QColor(255, 255, 255, anim::interpolate(96, 176, progress)));\n\tp.drawPath(path);\n\n\tconst auto handleRect = sizeControlHandleRect(progress);\n\tp.setBrush(QColor(255, 255, 255, 244));\n\tp.drawEllipse(handleRect);\n}\n\nvoid ColorPicker::setVisible(bool visible) {\n\tif (!visible) {\n\t\t_paletteVisible = false;\n\t\t_sizeDown.pressed = false;\n\t\t_sizeHoverAreaHovered = false;\n\t\t_sizeControlHovered = false;\n\t\t_sizeControlExpanded = false;\n\t\t_sizeControlAnimation.stop();\n\t\t_sizeControlPositionAnimation.stop();\n\t\t_toolSelectionAnimation.stop();\n\t}\n\t_colorButton->setVisible(visible && !_paletteVisible);\n\t_paletteWrap->setVisible(visible && _paletteVisible);\n\t_sizeControlHoverArea->setVisible(visible);\n\t_sizeControl->setVisible(visible);\n\t_toolSelection->setVisible(visible && !_paletteVisible);\n\tfor (const auto &button : _toolButtons) {\n\t\tbutton->setVisible(visible && !_paletteVisible);\n\t}\n\tif (visible && !_paletteVisible) {\n\t\tupdateToolSelection(false);\n\t}\n}\n\nrpl::producer<Brush> ColorPicker::saveBrushRequests() const {\n\treturn _saveBrushRequests.events_starting_with_copy(_brush);\n}\n\nbool ColorPicker::preventHandleKeyPress() const {\n\treturn _sizeControl->isVisible()\n\t\t&& (_sizeControlAnimation.animating() || _sizeDown.pressed);\n}\n\nvoid ColorPicker::rebuildPalette() {\n\t_paletteButtons.clear();\n\t_palettePlus = nullptr;\n\n\tauto colors = PaletteColors();\n\tauto hasCurrent = false;\n\tfor (const auto &c : colors) {\n\t\tif (c == _brush.color) {\n\t\t\thasCurrent = true;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (!hasCurrent) {\n\t\tcolors.push_back(_brush.color);\n\n\t\tconst auto &padding = st::photoEditorButtonBarPadding;\n\t\tconst auto size = st::photoEditorColorPaletteItemSize;\n\t\tconst auto gap = st::photoEditorColorPaletteGap;\n\t\tconst auto barWidth = std::min(\n\t\t\t\tst::photoEditorButtonBarWidth,\n\t\t\t\t_parent->width())\n\t\t\t- rect::m::sum::h(padding)\n\t\t\t- st::photoEditorUndoButton.width\n\t\t\t- st::photoEditorRedoButton.width;\n\t\tconst auto count = int(colors.size());\n\t\tconst auto paletteWidth = count * size\n\t\t\t+ (count - 1) * gap\n\t\t\t+ (gap + size);\n\t\tif (paletteWidth > barWidth && colors.size() > 2) {\n\t\t\tcolors.erase(colors.end() - 2);\n\t\t}\n\t}\n\n\tauto index = uint8(0);\n\tfor (const auto &c : colors) {\n\t\tauto button = base::make_unique_q<Ui::ColorSample>(\n\t\t\t_paletteWrap,\n\t\t\t[c](uint8) {\n\t\t\t\tauto set = Data::ColorProfileSet();\n\t\t\t\tset.palette = { c };\n\t\t\t\treturn set;\n\t\t\t},\n\t\t\tindex++,\n\t\t\tc == _brush.color);\n\t\tbutton->setSelectionCutout(true);\n\t\tbutton->setClickedCallback([=] {\n\t\t\t_brush.color = c;\n\t\t\tstoreCurrentBrush();\n\t\t\tupdateColorButtonColor(_brush.color, true);\n\t\t\trebuildPalette();\n\t\t\t_colorButton->update();\n\t\t\t_saveBrushRequests.fire_copy(_brush);\n\t\t\tsetPaletteVisible(false);\n\t\t});\n\t\tbutton->show();\n\t\t_paletteButtons.push_back(std::move(button));\n\t}\n\n\t_palettePlus = base::make_unique_q<PlusCircle>(_paletteWrap);\n\t_palettePlus->setClickedCallback([=] {\n\t\tif (!_show) {\n\t\t\treturn;\n\t\t}\n\t\t_show->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tstruct State {\n\t\t\t\tQColor color;\n\t\t\t};\n\t\t\tconst auto state = box->lifetime().make_state<State>();\n\t\t\tstate->color = _brush.color;\n\t\t\tauto editor = box->addRow(\n\t\t\t\tobject_ptr<ColorEditor>(\n\t\t\t\t\tbox,\n\t\t\t\t\tColorEditor::Mode::HSL,\n\t\t\t\t\t_brush.color),\n\t\t\t\tstyle::margins());\n\t\t\tbox->setWidth(editor->width());\n\t\t\teditor->colorValue(\n\t\t\t) | rpl::on_next([=](QColor c) {\n\t\t\t\tstate->color = c;\n\t\t\t}, editor->lifetime());\n\t\t\tbox->addButton(tr::lng_box_done(), [=] {\n\t\t\t\t_brush.color = state->color;\n\t\t\t\tstoreCurrentBrush();\n\t\t\t\tupdateColorButtonColor(_brush.color, true);\n\t\t\t\trebuildPalette();\n\t\t\t\t_colorButton->update();\n\t\t\t\t_saveBrushRequests.fire_copy(_brush);\n\t\t\t\tsetPaletteVisible(false);\n\t\t\t\tbox->closeBox();\n\t\t\t});\n\t\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t\t}));\n\t});\n\t_palettePlus->show();\n\tupdatePaletteGeometry();\n}\n\nvoid ColorPicker::updatePaletteGeometry() {\n\tif (!_paletteWrap) {\n\t\treturn;\n\t}\n\tconst auto size = st::photoEditorColorPaletteItemSize;\n\tconst auto gap = st::photoEditorColorPaletteGap;\n\tconst auto count = int(_paletteButtons.size());\n\tconst auto plusSize = size;\n\tconst auto width = (count * size)\n\t\t+ ((count > 0) ? (count - 1) * gap : 0)\n\t\t+ gap + plusSize;\n\tconst auto height = std::max(size, plusSize);\n\n\t_paletteWrap->resize(width, height);\n\tauto x = 0;\n\tfor (const auto &button : _paletteButtons) {\n\t\tbutton->resize(size, size);\n\t\tbutton->move(x, (height - size) / 2);\n\t\tx += size + gap;\n\t}\n\tif (_palettePlus) {\n\t\t_palettePlus->resize(plusSize, plusSize);\n\t\t_palettePlus->move(x, (height - plusSize) / 2);\n\t}\n\n\tif (_colorButtonCenter.isNull()) {\n\t\treturn;\n\t}\n\t_paletteWrap->move(_colorButtonCenter - QPoint(width / 2, height / 2));\n}\n\nvoid ColorPicker::setPaletteVisible(bool visible) {\n\tif (_paletteVisible == visible) {\n\t\treturn;\n\t}\n\t_paletteVisible = visible;\n\t_paletteWrap->setVisible(visible);\n\t_colorButton->setVisible(!visible);\n\t_toolSelection->setVisible(!visible);\n\tfor (const auto &button : _toolButtons) {\n\t\tbutton->setVisible(!visible);\n\t}\n\tif (visible) {\n\t\trebuildPalette();\n\t} else {\n\t\tupdateToolSelection(false);\n\t}\n}\n\nvoid ColorPicker::moveSizeControl(const QSize &size) {\n\tif (size.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto areaWidth = std::min(\n\t\tsize.width(),\n\t\tst::photoEditorBrushSizeControlHitPadding);\n\tconst auto areaTop = (_canvasRect.height() > 0)\n\t\t? _canvasRect.y()\n\t\t: (size.height() - _sizeControl->height()) / 2;\n\tconst auto areaHeight = (_canvasRect.height() > 0)\n\t\t? _canvasRect.height()\n\t\t: _sizeControl->height();\n\t_sizeControlHoverArea->setGeometry(\n\t\t0,\n\t\tstd::clamp(areaTop, 0, std::max(0, size.height() - areaHeight)),\n\t\tareaWidth,\n\t\tstd::min(areaHeight, size.height()));\n\n\tconst auto collapsedCenterX = sizeControlCurrentCenterX(0.);\n\tconst auto collapsedLeft = collapsedCenterX\n\t\t- (float64(st::photoEditorBrushSizeControlCollapsedWidth) / 2.);\n\tconst auto y = (_canvasRect.height() > 0)\n\t\t? (rect::center(_canvasRect).y() - _sizeControl->height() / 2)\n\t\t: ((size.height() - _sizeControl->height()) / 2);\n\tconst auto diff = size.height() - _sizeControl->height();\n\t_sizeControl->move(\n\t\t-int(base::SafeRound(collapsedLeft)),\n\t\tstd::clamp(y, 0, std::max(0, diff)));\n}\n\nvoid ColorPicker::updateSizeControlExpanded() {\n\tconst auto expanded = _sizeDown.pressed\n\t\t|| _sizeHoverAreaHovered\n\t\t|| _sizeControlHovered;\n\tif (_sizeControlExpanded == expanded\n\t\t&& !_sizeControlAnimation.animating()) {\n\t\treturn;\n\t}\n\t_sizeControlExpanded = expanded;\n\tconst auto from = _sizeControlAnimation.value(expanded ? 0. : 1.);\n\tconst auto to = expanded ? 1. : 0.;\n\t_sizeControlAnimation.stop();\n\t_sizeControlAnimation.start(\n\t\t[=] { _sizeControl->update(); },\n\t\tfrom,\n\t\tto,\n\t\tkCircleDuration * std::abs(to - from),\n\t\tanim::easeOutCirc);\n\t_sizeControl->update();\n}\n\nvoid ColorPicker::updateSizeControlMousePosition(int y) {\n\t_sizeDown.y = std::clamp(y, sizeControlTop(), sizeControlBottom());\n\t_brush.sizeRatio = sizeControlRatioFromY(_sizeDown.y);\n\tstoreCurrentBrush();\n}\n\nvoid ColorPicker::updateSizeControlPositionFromRatio(bool animated) {\n\tconst auto target = sizeControlYFromRatio(_brush.sizeRatio);\n\tif (!animated || _sizeDown.pressed) {\n\t\t_sizeControlPositionAnimation.stop();\n\t\t_sizeDown.y = target;\n\t\treturn;\n\t}\n\tif (_sizeDown.y == target) {\n\t\treturn;\n\t}\n\t_sizeControlPositionFrom = _sizeDown.y;\n\t_sizeControlPositionTo = target;\n\t_sizeControlPositionAnimation.stop();\n\t_sizeControlPositionAnimation.start([=] {\n\t\tconst auto progress = _sizeControlPositionAnimation.value(1.);\n\t\t_sizeDown.y = anim::interpolate(\n\t\t\t_sizeControlPositionFrom,\n\t\t\t_sizeControlPositionTo,\n\t\t\tprogress);\n\t\t_sizeControl->update();\n\t}, 0., 1., kSizeControlSwitchDuration, anim::easeOutCirc);\n\t_sizeControl->update();\n}\n\nint ColorPicker::sizeControlShapeTop() const {\n\treturn st::photoEditorBrushSizeControlHitPadding;\n}\n\nint ColorPicker::sizeControlShapeBottom() const {\n\treturn _sizeControl->height() - st::photoEditorBrushSizeControlHitPadding;\n}\n\nint ColorPicker::sizeControlTop() const {\n\treturn sizeControlShapeTop()\n\t\t+ (st::photoEditorBrushSizeControlExpandedTopWidth / 2);\n}\n\nint ColorPicker::sizeControlBottom() const {\n\treturn sizeControlShapeBottom()\n\t\t- (st::photoEditorBrushSizeControlExpandedBottomWidth / 2);\n}\n\nfloat ColorPicker::sizeControlRatioFromY(int y) const {\n\tconst auto top = sizeControlTop();\n\tconst auto bottom = sizeControlBottom();\n\tif (bottom <= top) {\n\t\treturn 1.f;\n\t}\n\tconst auto ratio = 1. - InterpolationRatio(top, bottom, y);\n\treturn std::clamp(ratio, kMinBrushSize, 1.0);\n}\n\nint ColorPicker::sizeControlYFromRatio(float ratio) const {\n\tconst auto top = sizeControlTop();\n\tconst auto bottom = sizeControlBottom();\n\tif (bottom <= top) {\n\t\treturn top;\n\t}\n\tconst auto normalized = std::clamp(\n\t\t(ratio - kMinBrushSize) / (1.0 - kMinBrushSize),\n\t\t0.0,\n\t\t1.0);\n\treturn std::clamp(\n\t\tanim::interpolate(bottom, top, normalized),\n\t\ttop,\n\t\tbottom);\n}\n\nQRectF ColorPicker::sizeControlHandleRect(float64 progress) const {\n\tconst auto handleSize = sizeControlHandleSize();\n\tconst auto centerX = sizeControlCurrentCenterX(progress);\n\treturn QRectF(\n\t\tcenterX - handleSize / 2.,\n\t\t_sizeDown.y - handleSize / 2.,\n\t\thandleSize,\n\t\thandleSize);\n}\n\nQRectF ColorPicker::sizeControlHitRect(float64 progress) const {\n\tconst auto collapsed = st::photoEditorBrushSizeControlCollapsedWidth;\n\tconst auto width = float64(anim::interpolate(\n\t\tcollapsed,\n\t\tst::photoEditorBrushSizeControlExpandedTopWidth,\n\t\tprogress));\n\tconst auto centerX = sizeControlCurrentCenterX(progress);\n\tconst auto top = float64(sizeControlShapeTop());\n\tconst auto bottom = float64(sizeControlShapeBottom());\n\treturn QRectF(\n\t\tcenterX - width / 2.,\n\t\ttop,\n\t\twidth,\n\t\tbottom - top);\n}\n\nQPainterPath ColorPicker::sizeControlShapePath(float64 progress) const {\n\tconst auto collapsed = st::photoEditorBrushSizeControlCollapsedWidth;\n\tconst auto topWidth = float64(anim::interpolate(\n\t\tcollapsed,\n\t\tst::photoEditorBrushSizeControlExpandedTopWidth,\n\t\tprogress));\n\tconst auto topInset = st::photoEditorBrushSizeControlTopInset;\n\tconst auto adjustedTopWidth = std::max(\n\t\t0.,\n\t\ttopWidth - float64(topInset * 2));\n\tconst auto bottomWidth = float64(anim::interpolate(\n\t\tcollapsed,\n\t\tst::photoEditorBrushSizeControlExpandedBottomWidth,\n\t\tprogress));\n\tconst auto centerX = sizeControlCurrentCenterX(progress);\n\tconst auto top = float64(sizeControlShapeTop()) + topInset;\n\tconst auto bottom = float64(sizeControlShapeBottom());\n\tconst auto topRadius = adjustedTopWidth / 2.;\n\tconst auto bottomRadius = bottomWidth / 2.;\n\n\tauto path = QPainterPath();\n\tconst auto topRect = QRectF(\n\t\tcenterX - topRadius,\n\t\ttop,\n\t\tadjustedTopWidth,\n\t\tadjustedTopWidth);\n\tconst auto bottomRect = QRectF(\n\t\tcenterX - bottomRadius,\n\t\tbottom - bottomWidth,\n\t\tbottomWidth,\n\t\tbottomWidth);\n\tpath.moveTo(centerX - topRadius, top + topRadius);\n\tpath.arcTo(topRect, 180., -180.);\n\tpath.lineTo(centerX + bottomRadius, bottom - bottomRadius);\n\tpath.arcTo(bottomRect, 0., -180.);\n\tpath.lineTo(centerX - topRadius, top + topRadius);\n\tpath.closeSubpath();\n\treturn path;\n}\n\nfloat64 ColorPicker::sizeControlCurrentCenterX(float64 progress) const {\n\tconst auto from = float64(st::photoEditorBrushSizeControlCollapsedWidth)\n\t\t/ 2.;\n\tconst auto to = float64(st::photoEditorBrushSizeControlExpandShift)\n\t\t+ float64(st::photoEditorBrushSizeControlExpandedTopWidth) / 2.;\n\treturn float64(st::photoEditorBrushSizeControlHitPadding)\n\t\t+ InterpolateF(from, to, progress);\n}\n\nfloat64 ColorPicker::sizeControlHandleSize() const {\n\tconst auto width = kMinBrushWidth\n\t\t+ (kMaxBrushWidth - kMinBrushWidth) * _brush.sizeRatio;\n\treturn std::clamp(\n\t\twidth,\n\t\tfloat64(st::photoEditorBrushSizeControlExpandedBottomWidth),\n\t\tfloat64(st::photoEditorBrushSizeControlExpandedTopWidth));\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/color_picker.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n#include \"editor/photo_editor_inner_common.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/peer/color_sample.h\"\n\nnamespace Ui {\nclass RpWidget;\nclass Show;\n} // namespace Ui\n\nnamespace Editor {\n\nclass ColorPicker final {\npublic:\n\tColorPicker(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tconst std::array<Brush, 5> &savedBrushes,\n\t\tBrush::Tool savedTool);\n\n\tvoid moveLine(const QPoint &position);\n\tvoid setCanvasRect(const QRect &rect);\n\tvoid setVisible(bool visible);\n\tbool preventHandleKeyPress() const;\n\n\trpl::producer<Brush> saveBrushRequests() const;\n\nprivate:\n\tvoid paintSizeControl(QPainter &p);\n\tvoid rebuildPalette();\n\tvoid updateToolButtonsGeometry();\n\tvoid updateToolSelection(bool animated);\n\tvoid setTool(Brush::Tool tool);\n\tvoid storeCurrentBrush();\n\tvoid updateColorButtonColor(const QColor &color, bool animated);\n\t[[nodiscard]] QColor colorButtonColor() const;\n\tvoid updatePaletteGeometry();\n\tvoid setPaletteVisible(bool visible);\n\tvoid moveSizeControl(const QSize &size);\n\tvoid updateSizeControlExpanded();\n\tvoid updateSizeControlMousePosition(int y);\n\tvoid updateSizeControlPositionFromRatio(bool animated);\n\t[[nodiscard]] int sizeControlShapeTop() const;\n\t[[nodiscard]] int sizeControlShapeBottom() const;\n\t[[nodiscard]] int sizeControlTop() const;\n\t[[nodiscard]] int sizeControlBottom() const;\n\t[[nodiscard]] float sizeControlRatioFromY(int y) const;\n\t[[nodiscard]] int sizeControlYFromRatio(float ratio) const;\n\t[[nodiscard]] QRectF sizeControlHandleRect(float64 progress) const;\n\t[[nodiscard]] QRectF sizeControlHitRect(float64 progress) const;\n\t[[nodiscard]] QPainterPath sizeControlShapePath(float64 progress) const;\n\t[[nodiscard]] float64 sizeControlCurrentCenterX(float64 progress) const;\n\t[[nodiscard]] float64 sizeControlHandleSize() const;\n\n\tconst not_null<Ui::RpWidget*> _parent;\n\tconst std::shared_ptr<Ui::Show> _show;\n\n\tconst base::unique_qptr<Ui::AbstractButton> _colorButton;\n\tconst base::unique_qptr<Ui::RpWidget> _paletteWrap;\n\tconst base::unique_qptr<Ui::RpWidget> _sizeControlHoverArea;\n\tconst base::unique_qptr<Ui::RpWidget> _sizeControl;\n\tconst base::unique_qptr<Ui::RpWidget> _toolSelection;\n\tstd::vector<base::unique_qptr<Ui::AbstractButton>> _toolButtons;\n\n\tstruct {\n\t\tint y = 0;\n\t\tbool pressed = false;\n\t} _sizeDown;\n\tbool _sizeHoverAreaHovered = false;\n\tbool _sizeControlHovered = false;\n\tbool _sizeControlExpanded = false;\n\tint _sizeControlPositionFrom = 0;\n\tint _sizeControlPositionTo = 0;\n\tQRect _canvasRect;\n\tQPoint _colorButtonCenter;\n\tbool _paletteVisible = false;\n\tBrush _brush;\n\tQColor _colorButtonFrom;\n\tQColor _colorButtonTo;\n\tstd::array<Brush, 5> _toolBrushes;\n\n\tUi::Animations::Simple _sizeControlAnimation;\n\tUi::Animations::Simple _sizeControlPositionAnimation;\n\tUi::Animations::Simple _colorButtonAnimation;\n\tUi::Animations::Simple _toolSelectionAnimation;\n\n\trpl::event_stream<Brush> _saveBrushRequests;\n\n\tstd::vector<base::unique_qptr<Ui::ColorSample>> _paletteButtons;\n\tbase::unique_qptr<Ui::AbstractButton> _palettePlus;\n\tQPoint _toolSelectionFrom;\n\tQPoint _toolSelectionTo;\n\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/controllers/controllers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"editor/controllers/stickers_panel_controller.h\"\n#include \"editor/controllers/undo_controller.h\"\n#include \"ui/layers/show.h\"\n\nnamespace Editor {\n\nstruct Controllers final {\n\tControllers(\n\t\tstd::unique_ptr<StickersPanelController> stickersPanelController,\n\t\tstd::unique_ptr<UndoController> undoController,\n\t\tstd::shared_ptr<Ui::Show> show)\n\t: stickersPanelController(std::move(stickersPanelController))\n\t, undoController(std::move(undoController))\n\t, show(std::move(show)) {\n\t}\n\t~Controllers() {\n\t};\n\n\tconst std::unique_ptr<StickersPanelController> stickersPanelController;\n\tconst std::unique_ptr<UndoController> undoController;\n\tconst std::shared_ptr<Ui::Show> show;\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/controllers/stickers_panel_controller.h\"\n\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"window/window_session_controller.h\" // Window::GifPauseReason\n\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Editor {\n\nStickersPanelController::StickersPanelController(\n\tnot_null<Ui::RpWidget*> panelContainer,\n\tstd::shared_ptr<ChatHelpers::Show> show)\n: _stickersPanel(\n\tbase::make_unique_q<ChatHelpers::TabbedPanel>(\n\t\tpanelContainer,\n\t\tChatHelpers::TabbedPanelDescriptor{\n\t\t\t.ownedSelector = object_ptr<ChatHelpers::TabbedSelector>(\n\t\t\t\tnullptr,\n\t\t\t\tChatHelpers::TabbedSelectorDescriptor{\n\t\t\t\t\t.show = show,\n\t\t\t\t\t.st = st::storiesComposeControls.tabbed,\n\t\t\t\t\t.level = Window::GifPauseReason::Layer,\n\t\t\t\t\t.mode = ChatHelpers::TabbedSelector::Mode::MediaEditor,\n\t\t\t\t\t.features = {\n\t\t\t\t\t\t.megagroupSet = false,\n\t\t\t\t\t\t.stickersSettings = false,\n\t\t\t\t\t\t.openStickerSets = false,\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t})) {\n\t_stickersPanel->setDesiredHeightValues(\n\t\t1.,\n\t\tst::emojiPanMinHeight / 2,\n\t\tst::emojiPanMinHeight);\n\t_stickersPanel->hide();\n}\n\nauto StickersPanelController::stickerChosen() const\n-> rpl::producer<not_null<DocumentData*>> {\n\treturn _stickersPanel->selector()->fileChosen(\n\t) | rpl::map([](const ChatHelpers::FileChosen &data) {\n\t\treturn data.document;\n\t});\n}\n\nrpl::producer<bool> StickersPanelController::panelShown() const {\n\treturn _stickersPanel->shownValue();\n}\n\nvoid StickersPanelController::setShowRequestChanges(\n\t\trpl::producer<ShowRequest> &&showRequest) {\n\tstd::move(\n\t\tshowRequest\n\t) | rpl::on_next([=](ShowRequest show) {\n\t\tif (show == ShowRequest::ToggleAnimated) {\n\t\t\t_stickersPanel->toggleAnimated();\n\t\t\t_stickersPanel->raise();\n\t\t} else if (show == ShowRequest::ShowAnimated) {\n\t\t\t_stickersPanel->showAnimated();\n\t\t\t_stickersPanel->raise();\n\t\t} else if (show == ShowRequest::HideAnimated) {\n\t\t\t_stickersPanel->hideAnimated();\n\t\t} else if (show == ShowRequest::HideFast) {\n\t\t\t_stickersPanel->hideFast();\n\t\t}\n\t}, _stickersPanel->lifetime());\n}\n\nvoid StickersPanelController::setMoveRequestChanges(\n\t\trpl::producer<QPoint> &&moveRequest) {\n\tstd::move(\n\t\tmoveRequest\n\t) | rpl::on_next([=](const QPoint &point) {\n\t\t_stickersPanel->moveBottomRight(\n\t\t\tpoint.y(),\n\t\t\tpoint.x() + _stickersPanel->width() / 2);\n\t}, _stickersPanel->lifetime());\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n\nnamespace ChatHelpers {\nclass TabbedPanel;\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Editor {\n\nclass StickersPanelController final {\npublic:\n\tenum class ShowRequest {\n\t\tToggleAnimated,\n\t\tShowAnimated,\n\t\tHideAnimated,\n\t\tHideFast,\n\t};\n\n\tStickersPanelController(\n\t\tnot_null<Ui::RpWidget*> panelContainer,\n\t\tstd::shared_ptr<ChatHelpers::Show> show);\n\n\t[[nodiscard]] auto stickerChosen() const\n\t-> rpl::producer<not_null<DocumentData*>>;\n\t[[nodiscard]] rpl::producer<bool> panelShown() const;\n\n\tvoid setShowRequestChanges(rpl::producer<ShowRequest> &&showRequest);\n\t// Middle x and plain y position.\n\tvoid setMoveRequestChanges(rpl::producer<QPoint> &&moveRequest);\n\nprivate:\n\tconst base::unique_qptr<ChatHelpers::TabbedPanel> _stickersPanel;\n\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/controllers/undo_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/controllers/undo_controller.h\"\n\nnamespace Editor {\nnamespace {\nusing EnableRequest = UndoController::EnableRequest;\n} // namespace\n\nUndoController::UndoController() {\n}\n\nvoid UndoController::setCanPerformChanges(\n\t\trpl::producer<EnableRequest> &&command) {\n\tstd::move(\n\t\tcommand\n\t) | rpl::start_to_stream(_enable, _lifetime);\n}\n\nvoid UndoController::setPerformRequestChanges(rpl::producer<Undo> &&command) {\n\tstd::move(\n\t\tcommand\n\t) | rpl::start_to_stream(_perform, _lifetime);\n}\n\nrpl::producer<EnableRequest> UndoController::canPerformChanges() const {\n\treturn _enable.events();\n}\n\nrpl::producer<Undo> UndoController::performRequestChanges() const {\n\treturn _perform.events();\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/controllers/undo_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Editor {\n\nenum class Undo {\n\tUndo,\n\tRedo,\n};\n\nclass UndoController final {\npublic:\n\tstruct EnableRequest {\n\t\tUndo command = Undo::Undo;\n\t\tbool enable = true;\n\t};\n\n\tUndoController();\n\n\tvoid setCanPerformChanges(rpl::producer<EnableRequest> &&command);\n\tvoid setPerformRequestChanges(rpl::producer<Undo> &&command);\n\n\t[[nodiscard]] rpl::producer<EnableRequest> canPerformChanges() const;\n\t[[nodiscard]] rpl::producer<Undo> performRequestChanges() const;\n\nprivate:\n\n\trpl::event_stream<Undo> _perform;\n\trpl::event_stream<EnableRequest> _enable;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/editor.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\n\nusing \"window/window.style\";\nusing \"ui/widgets/widgets.style\";\nusing \"ui/chat/chat.style\";\n\n// photoEditorControlsBottomSkip * 2 + photoEditorControlsCenterSkip + photoEditorButtonBarHeight * 2\nphotoEditorControlsHeight: 146px;\n\nphotoEditorControlsBottomSkip: 20px;\nphotoEditorControlsCenterSkip: 6px;\n\nphotoEditorContentMargins: margins(\n\tphotoEditorControlsBottomSkip,\n\tphotoEditorControlsBottomSkip,\n\tphotoEditorControlsBottomSkip,\n\tphotoEditorControlsHeight);\n\nphotoEditorBarAnimationDuration: 200;\n\nphotoEditorButtonIconFg: mediaviewPipControlsFg;\nphotoEditorButtonIconFgOver: mediaviewPipControlsFgOver;\n\nphotoEditorButtonIconFgActive: lightButtonFg;\nphotoEditorButtonIconFgInactive: mediaviewPipPlaybackInactive;\nphotoEditorEdgeButtonBg: shadowFg;\nphotoEditorEdgeButtonMargins: margins(4px, 4px, 4px, 4px);\n\nphotoEditorButtonBarHeight: 48px;\nphotoEditorButtonBarWidth: 422px;\nphotoEditorButtonBarPadding: margins(2px, 0px, 2px, 0px);\nphotoEditorTextButtonPadding: margins(22px, 0px, 22px, 0px);\n\nphotoEditorButtonStyle: TextStyle(semiboldTextStyle) {\n\tfont: font(14px semibold);\n}\nphotoEditorButtonTextTop: 15px;\n\nphotoEditorAbout: FlatLabel(defaultFlatLabel) {\n\ttextFg: mediaviewCaptionFg;\n\tminWidth: 240px;\n\talign: align(top);\n}\nphotoEditorAboutMargin: margins(10px, 22px, 10px, 0px);\n\nphotoEditorRotateButton: IconButton(defaultIconButton) {\n\twidth: photoEditorButtonBarHeight;\n\theight: photoEditorButtonBarHeight;\n\n\ticon: icon {{ \"photo_editor/rotate-flip_horizontal\", photoEditorButtonIconFg }};\n\ticonOver: icon {{ \"photo_editor/rotate-flip_horizontal\", photoEditorButtonIconFgOver }};\n\n\trippleAreaPosition: point(4px, 4px);\n\trippleAreaSize: 40px;\n\tripple: universalRippleAnimation;\n}\nphotoEditorFlipButton: IconButton(photoEditorRotateButton) {\n\ticon: icon {{ \"photo_editor/flip\", photoEditorButtonIconFg }};\n\ticonOver: icon {{ \"photo_editor/flip\", photoEditorButtonIconFgOver }};\n}\nphotoEditorFlipIconActive: icon {{ \"photo_editor/flip\", photoEditorButtonIconFgActive }};\n\nphotoEditorPaintModeButton: IconButton(photoEditorRotateButton) {\n\ticon: icon {{ \"photo_editor/paint\", photoEditorButtonIconFg }};\n\ticonOver: icon {{ \"photo_editor/paint\", photoEditorButtonIconFgOver }};\n}\nphotoEditorPaintIconActive: icon {{ \"photo_editor/paint\", photoEditorButtonIconFgActive }};\n\nphotoEditorUndoButton: IconButton(photoEditorRotateButton) {\n\ticon: icon {{ \"photo_editor/undo\", photoEditorButtonIconFg }};\n\ticonOver: icon {{ \"photo_editor/undo\", photoEditorButtonIconFgOver }};\n}\n\nphotoEditorRedoButton: IconButton(photoEditorRotateButton) {\n\ticon: icon {{ \"photo_editor/undo-flip_horizontal\", photoEditorButtonIconFg }};\n\ticonOver: icon {{ \"photo_editor/undo-flip_horizontal\", photoEditorButtonIconFgOver }};\n}\n\nphotoEditorStickersButton: IconButton(photoEditorRotateButton) {\n\ticon: icon {{ \"settings/settings_stickers\", photoEditorButtonIconFg }};\n\ticonOver: icon {{ \"settings/settings_stickers\", photoEditorButtonIconFgOver }};\n}\nphotoEditorStickersIconActive: icon {{ \"settings/settings_stickers\", photoEditorButtonIconFgActive }};\n\nphotoEditorCropRatioButton: IconButton(photoEditorRotateButton) {\n\ticon: icon {{ \"photo_editor/ratio-23x23\", photoEditorButtonIconFg, point(0px, 1px) }};\n\ticonOver: icon {{ \"photo_editor/ratio-23x23\", photoEditorButtonIconFgOver, point(0px, 1px) }};\n}\nphotoEditorCropRatioIconActive: icon {{ \"photo_editor/ratio-23x23\", photoEditorButtonIconFgActive, point(0px, 1px) }};\n\nphotoEditorCropRatioMenu: PopupMenu(popupMenuWithIcons) {\n\tmenu: Menu(menuWithIcons) {\n\t\twidthMin: 108px;\n\t}\n}\n\nphotoEditorUndoButtonInactive: icon {{ \"photo_editor/undo\", photoEditorButtonIconFgInactive }};\nphotoEditorRedoButtonInactive: icon {{ \"photo_editor/undo-flip_horizontal\", photoEditorButtonIconFgInactive }};\n\nphotoEditorColorButtonSize: 24px;\nphotoEditorColorButtonBorder: 1px;\nphotoEditorColorButtonBorderFg: mediaviewCaptionFg;\nphotoEditorColorPaletteItemSize: 20px;\nphotoEditorColorPaletteGap: 6px;\nphotoEditorColorPaletteSelectionWidth: 2px;\nphotoEditorColorPaletteSelectionFg: mediaviewCaptionFg;\nphotoEditorColorPalettePlusLine: 2px;\nphotoEditorColorPalettePlusFg: mediaviewCaptionFg;\n\nphotoEditorToolButtonSize: 20px;\nphotoEditorToolButtonIconSize: 20px;\nphotoEditorToolButtonGap: 18px;\nphotoEditorToolButtonHoverDuration: 350;\nphotoEditorToolButtonSelectDuration: 120;\nphotoEditorToolButtonSelectedFg: mediaviewCaptionFg;\nphotoEditorToolButtonSelectedWidth: 2px;\nphotoEditorToolButtonSelectedPadding: 2px;\nphotoEditorToolButtonSelectedOpacity: 0.35;\nphotoEditorToolButtonSelectedExtra: 8px;\n\nphotoEditorMarkerOpacity: 0.35;\nphotoEditorMarkerSizeMultiplier: 2.5;\nphotoEditorArrowHeadLengthFactor: 2.5;\nphotoEditorArrowHeadMinDistanceFactor: 1.5;\nphotoEditorArrowHeadAngleDegrees: 26;\nphotoEditorEraserPreviewOpacity: 0.25;\nphotoEditorBlurSizeMultiplier: 3.0;\nphotoEditorBlurPreviewOpacity: 0.25;\nphotoEditorBlurRadius: 20;\n\nphotoEditorBrushSizeControlLeftSkip: 0px;\nphotoEditorBrushSizeControlHeight: 280px;\nphotoEditorBrushSizeControlCollapsedWidth: 2px;\nphotoEditorBrushSizeControlExpandedTopWidth: 25px;\nphotoEditorBrushSizeControlExpandedBottomWidth: 4px;\nphotoEditorBrushSizeControlExpandShift: 14px;\nphotoEditorBrushSizeControlHitPadding: 24px;\nphotoEditorBrushSizeControlTopInset: 1px;\n\nphotoEditorCropPointSize: 10px;\nphotoEditorCropMinSize: 20px;\n\nphotoEditorItemHandleSize: 10px;\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/editor_crop.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/editor_crop.h\"\n\n#include \"ui/userpic_view.h\"\n#include \"styles/style_editor.h\"\n#include \"styles/style_basic.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Editor {\nnamespace {\n\nconstexpr auto kETL = Qt::TopEdge | Qt::LeftEdge;\nconstexpr auto kETR = Qt::TopEdge | Qt::RightEdge;\nconstexpr auto kEBL = Qt::BottomEdge | Qt::LeftEdge;\nconstexpr auto kEBR = Qt::BottomEdge | Qt::RightEdge;\nconstexpr auto kEL = Qt::Edges(Qt::LeftEdge);\nconstexpr auto kER = Qt::Edges(Qt::RightEdge);\nconstexpr auto kET = Qt::Edges(Qt::TopEdge);\nconstexpr auto kEB = Qt::Edges(Qt::BottomEdge);\nconstexpr auto kEAll = Qt::TopEdge\n\t| Qt::LeftEdge\n\t| Qt::BottomEdge\n\t| Qt::RightEdge;\n\nstd::tuple<int, int, int, int> RectEdges(const QRectF &r) {\n\treturn { r.left(), r.top(), r.left() + r.width(), r.top() + r.height() };\n}\n\nQPoint PointOfEdge(Qt::Edges e, const QRectF &r) {\n\tswitch(e) {\n\tcase kETL: return QPoint(r.x(), r.y());\n\tcase kETR: return QPoint(r.x() + r.width(), r.y());\n\tcase kEBL: return QPoint(r.x(), r.y() + r.height());\n\tcase kEBR: return QPoint(r.x() + r.width(), r.y() + r.height());\n\tcase kEL: return QPoint(r.x(), r.y() + (r.height() / 2.));\n\tcase kER: return QPoint(r.x() + r.width(), r.y() + (r.height() / 2.));\n\tcase kET: return QPoint(r.x() + (r.width() / 2.), r.y());\n\tcase kEB: return QPoint(r.x() + (r.width() / 2.), r.y() + r.height());\n\tdefault: return QPoint();\n\t}\n}\n\nQSizeF FlipSizeByRotation(const QSizeF &size, int angle) {\n\treturn (((angle / 90) % 2) == 1) ? size.transposed() : size;\n}\n\n[[nodiscard]] QRectF OriginalCrop(QSize outer, QSize inner) {\n\tconst auto size = inner.scaled(outer, Qt::KeepAspectRatio);\n\treturn QRectF(\n\t\t(outer.width() - size.width()) / 2,\n\t\t(outer.height() - size.height()) / 2,\n\t\tsize.width(),\n\t\tsize.height());\n}\n\n} // namespace\n\nCrop::Crop(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst PhotoModifications &modifications,\n\tconst QSize &imageSize,\n\tEditorData data)\n: RpWidget(parent)\n, _pointSize(st::photoEditorCropPointSize)\n, _pointSizeH(_pointSize / 2.)\n, _innerMargins(QMarginsF(_pointSizeH, _pointSizeH, _pointSizeH, _pointSizeH)\n\t.toMargins())\n, _offset(_innerMargins.left(), _innerMargins.top())\n, _edgePointMargins(_pointSizeH, _pointSizeH, -_pointSizeH, -_pointSizeH)\n, _imageSize(imageSize)\n, _data(std::move(data))\n, _cropOriginal(modifications.crop.isValid()\n\t? modifications.crop\n\t: !_data.exactSize.isEmpty()\n\t? OriginalCrop(_imageSize, _data.exactSize)\n\t: QRectF(QPoint(), _imageSize))\n, _angle(modifications.angle)\n, _flipped(modifications.flipped)\n, _keepAspectRatio(_data.keepAspectRatio) {\n\n\tsetMouseTracking(true);\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(this);\n\n\t\tp.fillPath(_painterPath, st::photoCropFadeBg);\n\t\tpaintFrame(p);\n\t\tconst auto gridOpacity = _gridOpacityAnimation.value(\n\t\t\t_gridVisible ? 1. : 0.);\n\t\tif (gridOpacity > 0.) {\n\t\t\tpaintGrid(p, gridOpacity);\n\t\t}\n\t}, lifetime());\n\n}\n\nvoid Crop::applyTransform(\n\t\tconst QRect &geometry,\n\t\tint angle,\n\t\tbool flipped,\n\t\tconst QSizeF &scaledImageSize) {\n\tif (geometry.isEmpty()) {\n\t\treturn;\n\t}\n\tsetGeometry(geometry);\n\t_innerRect = QRectF(_offset, FlipSizeByRotation(scaledImageSize, angle));\n\t_ratio.w = scaledImageSize.width() / float64(_imageSize.width());\n\t_ratio.h = scaledImageSize.height() / float64(_imageSize.height());\n\t_flipped = flipped;\n\t_angle = angle;\n\n\tconst auto cropHolder = QRectF(QPointF(), scaledImageSize);\n\tconst auto cropHolderCenter = cropHolder.center();\n\n\tauto matrix = QTransform()\n\t\t.translate(cropHolderCenter.x(), cropHolderCenter.y())\n\t\t.scale(flipped ? -1 : 1, 1)\n\t\t.rotate(angle)\n\t\t.translate(-cropHolderCenter.x(), -cropHolderCenter.y());\n\n\tconst auto cropHolderRotated = matrix.mapRect(cropHolder);\n\n\tauto cropPaint = matrix\n\t\t.scale(_ratio.w, _ratio.h)\n\t\t.mapRect(_cropOriginal)\n\t\t.translated(\n\t\t\t-cropHolderRotated.x() + _offset.x(),\n\t\t\t-cropHolderRotated.y() + _offset.y());\n\n\t// Check boundaries.\n\tconst auto min = float64(st::photoEditorCropMinSize);\n\tif ((cropPaint.width() < min) || (cropPaint.height() < min)) {\n\t\tcropPaint.setWidth(std::max(min, cropPaint.width()));\n\t\tcropPaint.setHeight(std::max(min, cropPaint.height()));\n\n\t\tconst auto p = cropPaint.center().toPoint();\n\t\tsetCropPaint(std::move(cropPaint));\n\n\t\tcomputeDownState(p);\n\t\tperformMove(p);\n\t\tclearDownState();\n\n\t\tconvertCropPaintToOriginal();\n\t} else {\n\t\tsetCropPaint(std::move(cropPaint));\n\t}\n}\n\nQPainterPath Crop::cropPath() const {\n\tauto result = QPainterPath();\n\tif (_data.cropType == EditorData::CropType::Ellipse) {\n\t\tresult.addEllipse(_cropPaint);\n\t} else if (_data.cropType == EditorData::CropType::RoundedRect) {\n\t\tconst auto radius = std::min(_cropPaint.width(), _cropPaint.height())\n\t\t\t* Ui::ForumUserpicRadiusMultiplier();\n\t\tresult.addRoundedRect(_cropPaint, radius, radius);\n\t} else {\n\t\tresult.addRect(_cropPaint);\n\t}\n\treturn result;\n}\n\nvoid Crop::paintFrame(QPainter &p) {\n\tconst auto framePath = cropPath();\n\tauto frameStroker = QPainterPathStroker();\n\tframeStroker.setWidth(st::lineWidth * 2);\n\tframeStroker.setJoinStyle(Qt::MiterJoin);\n\tframeStroker.setCapStyle(Qt::SquareCap);\n\tauto frameShape = frameStroker.createStroke(framePath);\n\tp.save();\n\tp.setRenderHint(QPainter::Antialiasing, true);\n\tp.fillPath(frameShape, st::photoCropPointFg);\n\t{\n\t\tconst auto cornerLength = std::min(\n\t\t\tfloat64(st::photoEditorCropPointSize * 2),\n\t\t\tstd::min(_cropPaint.width(), _cropPaint.height()) / 2.);\n\t\tif (cornerLength > 0.) {\n\t\t\tauto cornerLines = QPainterPath();\n\t\t\tcornerLines.moveTo(_cropPaint.left(), _cropPaint.top());\n\t\t\tcornerLines.lineTo(\n\t\t\t\t_cropPaint.left() + cornerLength,\n\t\t\t\t_cropPaint.top());\n\t\t\tcornerLines.moveTo(_cropPaint.left(), _cropPaint.top());\n\t\t\tcornerLines.lineTo(\n\t\t\t\t_cropPaint.left(),\n\t\t\t\t_cropPaint.top() + cornerLength);\n\t\t\tcornerLines.moveTo(_cropPaint.right(), _cropPaint.top());\n\t\t\tcornerLines.lineTo(\n\t\t\t\t_cropPaint.right() - cornerLength,\n\t\t\t\t_cropPaint.top());\n\t\t\tcornerLines.moveTo(_cropPaint.right(), _cropPaint.top());\n\t\t\tcornerLines.lineTo(\n\t\t\t\t_cropPaint.right(),\n\t\t\t\t_cropPaint.top() + cornerLength);\n\t\t\tcornerLines.moveTo(_cropPaint.left(), _cropPaint.bottom());\n\t\t\tcornerLines.lineTo(\n\t\t\t\t_cropPaint.left() + cornerLength,\n\t\t\t\t_cropPaint.bottom());\n\t\t\tcornerLines.moveTo(_cropPaint.left(), _cropPaint.bottom());\n\t\t\tcornerLines.lineTo(\n\t\t\t\t_cropPaint.left(),\n\t\t\t\t_cropPaint.bottom() - cornerLength);\n\t\t\tcornerLines.moveTo(_cropPaint.right(), _cropPaint.bottom());\n\t\t\tcornerLines.lineTo(\n\t\t\t\t_cropPaint.right() - cornerLength,\n\t\t\t\t_cropPaint.bottom());\n\t\t\tcornerLines.moveTo(_cropPaint.right(), _cropPaint.bottom());\n\t\t\tcornerLines.lineTo(\n\t\t\t\t_cropPaint.right(),\n\t\t\t\t_cropPaint.bottom() - cornerLength);\n\n\t\t\tauto cornerStroker = QPainterPathStroker();\n\t\t\tcornerStroker.setWidth(st::lineWidth * 4);\n\t\t\tcornerStroker.setJoinStyle(Qt::MiterJoin);\n\t\t\tcornerStroker.setCapStyle(Qt::SquareCap);\n\t\t\tauto cornerColor = st::photoCropPointFg->c;\n\t\t\tcornerColor.setAlpha(255);\n\t\t\tp.fillPath(cornerStroker.createStroke(cornerLines), cornerColor);\n\t\t}\n\t}\n\tp.restore();\n}\n\nvoid Crop::paintGrid(QPainter &p, float64 opacity) {\n\tif ((_cropPaint.width() <= 0.) || (_cropPaint.height() <= 0.)) {\n\t\treturn;\n\t}\n\tp.save();\n\tp.setOpacity(opacity);\n\tp.setRenderHint(QPainter::Antialiasing, true);\n\tp.setClipPath(cropPath());\n\tauto pen = QPen(st::photoCropPointFg, st::lineWidth);\n\tpen.setCapStyle(Qt::FlatCap);\n\tp.setPen(pen);\n\tconst auto horizontalStep = _cropPaint.width() / 3.;\n\tconst auto verticalStep = _cropPaint.height() / 3.;\n\tfor (auto i = 1; i < 3; ++i) {\n\t\tconst auto x = _cropPaint.left() + (horizontalStep * i);\n\t\tconst auto y = _cropPaint.top() + (verticalStep * i);\n\t\tp.drawLine(\n\t\t\tQPointF(x, _cropPaint.top()),\n\t\t\tQPointF(x, _cropPaint.bottom()));\n\t\tp.drawLine(\n\t\t\tQPointF(_cropPaint.left(), y),\n\t\t\tQPointF(_cropPaint.right(), y));\n\t}\n\tp.restore();\n}\n\nvoid Crop::setCropPaint(QRectF &&rect) {\n\t_cropPaint = std::move(rect);\n\n\tupdateEdges();\n\n\t_painterPath.clear();\n\t_painterPath.addRect(_innerRect);\n\t_painterPath.addPath(cropPath());\n}\n\nvoid Crop::convertCropPaintToOriginal() {\n\tconst auto cropHolder = QTransform()\n\t\t.scale(_ratio.w, _ratio.h)\n\t\t.mapRect(QRectF(QPointF(), FlipSizeByRotation(_imageSize, _angle)));\n\tconst auto cropHolderCenter = cropHolder.center();\n\n\tconst auto matrix = QTransform()\n\t\t.translate(cropHolderCenter.x(), cropHolderCenter.y())\n\t\t.rotate(-_angle)\n\t\t.scale((_flipped ? -1 : 1) * 1. / _ratio.w, 1. / _ratio.h)\n\t\t.translate(-cropHolderCenter.x(), -cropHolderCenter.y());\n\n\tconst auto cropHolderRotated = matrix.mapRect(cropHolder);\n\n\t_cropOriginal = matrix\n\t\t.mapRect(QRectF(_cropPaint).translated(-_offset))\n\t\t.translated(\n\t\t\t-cropHolderRotated.x(),\n\t\t\t-cropHolderRotated.y());\n}\n\nvoid Crop::updateEdges() {\n\tconst auto &s = _pointSize;\n\tconst auto &m = _edgePointMargins;\n\tconst auto &r = _cropPaint;\n\tconst auto sideWidth = std::max(0., r.width() - s);\n\tconst auto sideHeight = std::max(0., r.height() - s);\n\tfor (const auto &e : { kETL, kETR, kEBL, kEBR }) {\n\t\t_edges[e] = QRectF(PointOfEdge(e, r), QSize(s, s)) + m;\n\t}\n\tif (!_keepAspectRatio) {\n\t\t_edges[kEL] = QRectF(\n\t\t\tr.left() - _pointSizeH,\n\t\t\tr.top() + _pointSizeH,\n\t\t\ts,\n\t\t\tsideHeight);\n\t\t_edges[kER] = QRectF(\n\t\t\tr.right() - _pointSizeH,\n\t\t\tr.top() + _pointSizeH,\n\t\t\ts,\n\t\t\tsideHeight);\n\t\t_edges[kET] = QRectF(\n\t\t\tr.left() + _pointSizeH,\n\t\t\tr.top() - _pointSizeH,\n\t\t\tsideWidth,\n\t\t\ts);\n\t\t_edges[kEB] = QRectF(\n\t\t\tr.left() + _pointSizeH,\n\t\t\tr.bottom() - _pointSizeH,\n\t\t\tsideWidth,\n\t\t\ts);\n\t} else {\n\t\t_edges.erase(kEL);\n\t\t_edges.erase(kER);\n\t\t_edges.erase(kET);\n\t\t_edges.erase(kEB);\n\t}\n}\n\nQt::Edges Crop::mouseState(const QPoint &p) {\n\tfor (const auto &e : { kETL, kETR, kEBL, kEBR, kEL, kER, kET, kEB }) {\n\t\tif (const auto i = _edges.find(e); i != end(_edges)) {\n\t\t\tif (i->second.contains(p)) {\n\t\t\t\treturn e;\n\t\t\t}\n\t\t}\n\t}\n\tif (_cropPaint.contains(p)) {\n\t\treturn kEAll;\n\t}\n\treturn Qt::Edges();\n}\n\nvoid Crop::mousePressEvent(QMouseEvent *e) {\n\tcomputeDownState(e->pos());\n\tif (_down.edge) {\n\t\tsetGridVisible(true, false);\n\t}\n}\n\nvoid Crop::mouseReleaseEvent(QMouseEvent *e) {\n\tconst auto hadEdge = bool(_down.edge);\n\tif (hadEdge) {\n\t\tsetGridVisible(false, true);\n\t}\n\tclearDownState();\n\tconvertCropPaintToOriginal();\n}\n\nvoid Crop::computeDownState(const QPoint &p) {\n\tconst auto edge = mouseState(p);\n\tconst auto &inner = _innerRect;\n\tconst auto &crop = _cropPaint;\n\tconst auto &[iLeft, iTop, iRight, iBottom] = RectEdges(inner);\n\tconst auto &[cLeft, cTop, cRight, cBottom] = RectEdges(crop);\n\t_down = InfoAtDown{\n\t\t.rect = crop,\n\t\t.edge = edge,\n\t\t.point = (p - PointOfEdge(edge, crop)),\n\t\t.cropRatio = (_cropOriginal.width() / _cropOriginal.height()),\n\t\t.borders = InfoAtDown::Borders{\n\t\t\t.left = iLeft - cLeft,\n\t\t\t.right = iRight - cRight,\n\t\t\t.top = iTop - cTop,\n\t\t\t.bottom = iBottom - cBottom,\n\t\t}\n\t};\n\tif (_keepAspectRatio && (edge != kEAll)) {\n\t\tconst auto hasLeft = (edge & Qt::LeftEdge);\n\t\tconst auto hasTop = (edge & Qt::TopEdge);\n\n\t\tconst auto xSign = hasLeft ? -1 : 1;\n\t\tconst auto ySign = hasTop ? -1 : 1;\n\n\t\tauto &xSide = (hasLeft ? _down.borders.left : _down.borders.right);\n\t\tauto &ySide = (hasTop ? _down.borders.top : _down.borders.bottom);\n\n\t\tconst auto min = std::abs(std::min(xSign * xSide, ySign * ySide));\n\t\tconst auto xIsMin = ((xSign * xSide) < (ySign * ySide));\n\t\txSide = xSign * min;\n\t\tySide = ySign * min;\n\t\tif (!xIsMin) {\n\t\t\txSide *= _down.cropRatio;\n\t\t} else {\n\t\t\tySide /= _down.cropRatio;\n\t\t}\n\t}\n}\n\nvoid Crop::clearDownState() {\n\t_down = InfoAtDown();\n}\n\nvoid Crop::setGridVisible(bool visible, bool animated) {\n\tif ((_gridVisible == visible) && !_gridOpacityAnimation.animating()) {\n\t\treturn;\n\t}\n\tconst auto from = _gridOpacityAnimation.value(_gridVisible ? 1. : 0.);\n\tconst auto to = visible ? 1. : 0.;\n\t_gridVisible = visible;\n\t_gridOpacityAnimation.stop();\n\tif (animated) {\n\t\t_gridOpacityAnimation.start(\n\t\t\t[=] { update(); },\n\t\t\tfrom,\n\t\t\tto,\n\t\t\tst::photoEditorBarAnimationDuration * std::abs(to - from));\n\t}\n\tupdate();\n}\n\nvoid Crop::performCrop(const QPoint &pos) {\n\tconst auto &crop = _down.rect;\n\tconst auto &pressedEdge = _down.edge;\n\tconst auto hasLeft = (pressedEdge & Qt::LeftEdge);\n\tconst auto hasTop = (pressedEdge & Qt::TopEdge);\n\tconst auto hasRight = (pressedEdge & Qt::RightEdge);\n\tconst auto hasBottom = (pressedEdge & Qt::BottomEdge);\n\tconst auto diff = [&] {\n\t\tauto diff = pos - PointOfEdge(pressedEdge, crop) - _down.point;\n\t\tconst auto xFactor = hasLeft ? 1 : -1;\n\t\tconst auto yFactor = hasTop ? 1 : -1;\n\t\tconst auto &borders = _down.borders;\n\t\tconst auto &cropRatio = _down.cropRatio;\n\t\tif (_keepAspectRatio) {\n\t\t\tconst auto diffSign = xFactor * yFactor;\n\t\t\tdiff = (cropRatio != 1.)\n\t\t\t\t? QPoint(diff.x(), (1. / cropRatio) * diff.x() * diffSign)\n\t\t\t\t// For square/circle.\n\t\t\t\t: ((diff.x() * xFactor) < (diff.y() * yFactor))\n\t\t\t\t? QPoint(diff.x(), diff.x() * diffSign)\n\t\t\t\t: QPoint(diff.y() * diffSign, diff.y());\n\t\t}\n\n\t\tconst auto &minSize = st::photoEditorCropMinSize;\n\t\tconst auto minW = (_keepAspectRatio && cropRatio > 1.)\n\t\t\t? (minSize * cropRatio)\n\t\t\t: float64(minSize);\n\t\tconst auto minH = (_keepAspectRatio && cropRatio < 1.)\n\t\t\t? (minSize / cropRatio)\n\t\t\t: float64(minSize);\n\t\tconst auto xMin = xFactor * int(crop.width() - minW);\n\t\tconst auto yMin = yFactor * int(crop.height() - minH);\n\n\t\tconst auto x = std::clamp(\n\t\t\tdiff.x(),\n\t\t\thasLeft ? borders.left : xMin,\n\t\t\thasLeft ? xMin : borders.right);\n\t\tconst auto y = std::clamp(\n\t\t\tdiff.y(),\n\t\t\thasTop ? borders.top : yMin,\n\t\t\thasTop ? yMin : borders.bottom);\n\t\treturn QPoint(x, y);\n\t}();\n\tsetCropPaint(crop - QMargins(\n\t\thasLeft ? diff.x() : 0,\n\t\thasTop ? diff.y() : 0,\n\t\thasRight ? -diff.x() : 0,\n\t\thasBottom ? -diff.y() : 0));\n}\n\nvoid Crop::performMove(const QPoint &pos) {\n\tconst auto &inner = _down.rect;\n\tconst auto &b = _down.borders;\n\tconst auto diffX = std::clamp(pos.x() - _down.point.x(), b.left, b.right);\n\tconst auto diffY = std::clamp(pos.y() - _down.point.y(), b.top, b.bottom);\n\tsetCropPaint(inner.translated(diffX, diffY));\n}\n\nvoid Crop::mouseMoveEvent(QMouseEvent *e) {\n\tconst auto pos = e->pos();\n\tconst auto pressedEdge = _down.edge;\n\n\tif (pressedEdge) {\n\t\tif (pressedEdge == kEAll) {\n\t\t\tperformMove(pos);\n\t\t} else if (pressedEdge) {\n\t\t\tperformCrop(pos);\n\t\t}\n\t\tupdate();\n\t}\n\n\tconst auto edge = pressedEdge ? pressedEdge : mouseState(pos);\n\n\tconst auto cursor = ((edge == kETL) || (edge == kEBR))\n\t\t? style::cur_sizefdiag\n\t\t: ((edge == kETR) || (edge == kEBL))\n\t\t? style::cur_sizebdiag\n\t\t: ((edge == kEL) || (edge == kER))\n\t\t? style::cur_sizehor\n\t\t: ((edge == kET) || (edge == kEB))\n\t\t? style::cur_sizever\n\t\t: (edge == kEAll)\n\t\t? style::cur_sizeall\n\t\t: style::cur_default;\n\tsetCursor(cursor);\n}\n\nstyle::margins Crop::cropMargins() const {\n\treturn _innerMargins;\n}\n\nvoid Crop::setAspectRatio(float64 ratio) {\n\tconst auto free = (ratio <= 0.);\n\t_keepAspectRatio = !free;\n\n\tif (!free) {\n\t\tconst auto maxW = _innerRect.width();\n\t\tconst auto maxH = _innerRect.height();\n\t\tauto newW = maxW;\n\t\tauto newH = maxW / ratio;\n\t\tif (newH > maxH) {\n\t\t\tnewH = maxH;\n\t\t\tnewW = maxH * ratio;\n\t\t}\n\n\t\tconst auto center = _cropPaint.center();\n\t\tauto adjusted = QRectF(\n\t\t\tcenter.x() - newW / 2.,\n\t\t\tcenter.y() - newH / 2.,\n\t\t\tnewW,\n\t\t\tnewH);\n\n\t\tif (adjusted.left() < _innerRect.left()) {\n\t\t\tadjusted.moveLeft(_innerRect.left());\n\t\t}\n\t\tif (adjusted.top() < _innerRect.top()) {\n\t\t\tadjusted.moveTop(_innerRect.top());\n\t\t}\n\t\tif (adjusted.right() > _innerRect.right()) {\n\t\t\tadjusted.moveRight(_innerRect.right());\n\t\t}\n\t\tif (adjusted.bottom() > _innerRect.bottom()) {\n\t\t\tadjusted.moveBottom(_innerRect.bottom());\n\t\t}\n\n\t\tsetCropPaint(std::move(adjusted));\n\t\tconvertCropPaintToOriginal();\n\t} else {\n\t\tupdateEdges();\n\t}\n\tupdate();\n}\n\nQRect Crop::saveCropRect() {\n\tconst auto savedCrop = _cropOriginal.toRect();\n\treturn (!savedCrop.topLeft().isNull() || (savedCrop.size() != _imageSize))\n\t\t? savedCrop\n\t\t: QRect();\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/editor_crop.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n\n#include \"base/flat_map.h\"\n#include \"editor/photo_editor_common.h\"\n\nnamespace Editor {\n\n// Crop control.\nclass Crop final : public Ui::RpWidget {\npublic:\n\tCrop(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst PhotoModifications &modifications,\n\t\tconst QSize &imageSize,\n\t\tEditorData type);\n\n\tvoid applyTransform(\n\t\tconst QRect &geometry,\n\t\tint angle,\n\t\tbool flipped,\n\t\tconst QSizeF &scaledImageSize);\n\t[[nodiscard]] QRect saveCropRect();\n\t[[nodiscard]] style::margins cropMargins() const;\n\tvoid setAspectRatio(float64 ratio);\n\nprotected:\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\nprivate:\n\tstruct InfoAtDown {\n\t\tQRectF rect;\n\t\tQt::Edges edge;\n\t\tQPoint point;\n\t\tfloat64 cropRatio = 0.;\n\n\t\tstruct Borders {\n\t\t\tint left = 0;\n\t\t\tint right = 0;\n\t\t\tint top = 0;\n\t\t\tint bottom = 0;\n\t\t} borders;\n\t};\n\n\tvoid paintFrame(QPainter &p);\n\tvoid paintGrid(QPainter &p, float64 opacity);\n\tvoid setGridVisible(bool visible, bool animated);\n\t[[nodiscard]] QPainterPath cropPath() const;\n\n\tvoid updateEdges();\n\t[[nodiscard]] QPoint pointOfEdge(Qt::Edges e) const;\n\tvoid setCropPaint(QRectF &&rect);\n\tvoid convertCropPaintToOriginal();\n\n\tvoid computeDownState(const QPoint &p);\n\tvoid clearDownState();\n\t[[nodiscard]] Qt::Edges mouseState(const QPoint &p);\n\tvoid performCrop(const QPoint &pos);\n\tvoid performMove(const QPoint &pos);\n\n\tconst int _pointSize;\n\tconst float _pointSizeH;\n\tconst style::margins _innerMargins;\n\tconst QPoint _offset;\n\tconst QMarginsF _edgePointMargins;\n\tconst QSize _imageSize;\n\tconst EditorData _data;\n\n\tbase::flat_map<Qt::Edges, QRectF> _edges;\n\n\tstruct {\n\t\tfloat64 w = 0.;\n\t\tfloat64 h = 0.;\n\t} _ratio;\n\n\tQRectF _cropPaint;\n\tQRectF _cropOriginal;\n\tQRectF _innerRect;\n\n\tQPainterPath _painterPath;\n\n\tInfoAtDown _down;\n\tUi::Animations::Simple _gridOpacityAnimation;\n\n\tint _angle = 0;\n\tbool _flipped = false;\n\tbool _gridVisible = false;\n\n\tbool _keepAspectRatio = false;\n\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/editor_layer_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/editor_layer_widget.h\"\n\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n\n#include <QtGui/QGuiApplication>\n\nnamespace Editor {\nnamespace {\n\nconstexpr auto kCacheBackgroundFastTimeout = crl::time(200);\nconstexpr auto kCacheBackgroundFullTimeout = crl::time(1000);\nconstexpr auto kFadeBackgroundDuration = crl::time(200);\n\n// Thread: Main.\n[[nodiscard]] bool IsNightMode() {\n\treturn (st::windowBg->c.lightnessF() < 0.5);\n}\n\n[[nodiscard]] QColor BlurOverlayColor(bool night) {\n\treturn QColor(16, 16, 16, night ? 128 : 192);\n}\n\n[[nodiscard]] QImage ProcessBackground(QImage image, bool night) {\n\tconst auto size = image.size();\n\tauto p = QPainter(&image);\n\tp.fillRect(\n\t\tQRect(QPoint(), image.size() / image.devicePixelRatio()),\n\t\tBlurOverlayColor(night));\n\tp.end();\n\treturn Images::DitherImage(\n\t\tImages::BlurLargeImage(\n\t\t\tstd::move(image).scaled(\n\t\t\t\tsize / style::ConvertScale(4),\n\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\tQt::SmoothTransformation),\n\t\t\t24).scaled(\n\t\t\t\tsize,\n\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\tQt::SmoothTransformation));\n}\n\n} // namespace\n\nLayerWidget::LayerWidget(\n\tnot_null<QWidget*> parent,\n\tbase::unique_qptr<Ui::RpWidget> content)\n: Ui::LayerWidget(parent)\n, _content(std::move(content))\n, _backgroundTimer([=] { checkCacheBackground(); }) {\n\t_content->setParent(this);\n\t_content->show();\n\n\tpaintRequest(\n\t) | rpl::on_next([=](const QRect &clip) {\n\t\tauto p = QPainter(this);\n\t\tconst auto faded = _backgroundFade.value(1.);\n\t\tif (faded < 1.) {\n\t\t\tp.drawImage(rect(), _backgroundBack);\n\t\t\tif (faded > 0.) {\n\t\t\t\tp.setOpacity(faded);\n\t\t\t\tp.drawImage(rect(), _background);\n\t\t\t}\n\t\t} else {\n\t\t\tp.drawImage(rect(), _background);\n\t\t}\n\t}, lifetime());\n}\n\nbool LayerWidget::eventHook(QEvent *e) {\n\treturn RpWidget::eventHook(e);\n}\n\nvoid LayerWidget::start() {\n\t_backgroundNight = IsNightMode();\n\t_background = ProcessBackground(renderBackground(), _backgroundNight);\n\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\tcheckBackgroundStale();\n\t\t_content->resize(size);\n\t}, lifetime());\n\n\tstyle::PaletteChanged() | rpl::on_next([=] {\n\t\tcheckBackgroundStale();\n\t}, lifetime());\n}\n\nvoid LayerWidget::checkBackgroundStale() {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto &ready = _backgroundNext.isNull()\n\t\t? _background\n\t\t: _backgroundNext;\n\tif (ready.size() == size() * ratio\n\t\t&& _backgroundNight == IsNightMode()) {\n\t\t_backgroundTimer.cancel();\n\t} else if (!_backgroundCaching && !_backgroundTimer.isActive()) {\n\t\t_lastAreaChangeTime = crl::now();\n\t\t_backgroundTimer.callOnce(kCacheBackgroundFastTimeout);\n\t}\n}\n\nQImage LayerWidget::renderBackground() {\n\tconst auto parent = parentWidget();\n\tconst auto target = parent->parentWidget();\n\tUi::SendPendingMoveResizeEvents(target);\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto image = QImage(size() * ratio, QImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(ratio);\n\n\tconst auto shown = !parent->isHidden();\n\tconst auto focused = shown && Ui::InFocusChain(parent);\n\tif (shown) {\n\t\tif (focused) {\n\t\t\ttarget->setFocus();\n\t\t}\n\t\tparent->hide();\n\t}\n\tauto p = QPainter(&image);\n\tUi::RenderWidget(p, target, QPoint(), geometry());\n\tp.end();\n\n\tif (shown) {\n\t\tparent->show();\n\t\tif (focused) {\n\t\t\tif (isHidden()) {\n\t\t\t\tparent->setFocus();\n\t\t\t} else {\n\t\t\t\tsetInnerFocus();\n\t\t\t}\n\t\t}\n\t}\n\n\treturn image;\n}\n\nvoid LayerWidget::checkCacheBackground() {\n\tif (_backgroundCaching || _backgroundTimer.isActive()) {\n\t\treturn;\n\t}\n\tconst auto now = crl::now();\n\tif (now - _lastAreaChangeTime < kCacheBackgroundFullTimeout\n\t\t&& QGuiApplication::mouseButtons() != 0) {\n\t\t_backgroundTimer.callOnce(kCacheBackgroundFastTimeout);\n\t\treturn;\n\t}\n\tcacheBackground();\n}\n\nvoid LayerWidget::cacheBackground() {\n\t_backgroundCaching = true;\n\tconst auto weak = base::make_weak(this);\n\tconst auto night = IsNightMode();\n\tcrl::async([weak, night, image = renderBackground()]() mutable {\n\t\tauto result = ProcessBackground(image, night);\n\t\tcrl::on_main([weak, night, result = std::move(result)]() mutable {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->backgroundReady(std::move(result), night);\n\t\t\t}\n\t\t});\n\t});\n}\n\nvoid LayerWidget::backgroundReady(QImage background, bool night) {\n\t_backgroundCaching = false;\n\n\tconst auto required = size() * style::DevicePixelRatio();\n\tif (background.size() == required && night == IsNightMode()) {\n\t\t_backgroundNext = std::move(background);\n\t\t_backgroundNight = night;\n\t\tif (!_backgroundFade.animating()) {\n\t\t\tstartBackgroundFade();\n\t\t}\n\t\tupdate();\n\t} else if (_background.size() != required) {\n\t\t_backgroundTimer.callOnce(kCacheBackgroundFastTimeout);\n\t}\n}\n\nvoid LayerWidget::startBackgroundFade() {\n\tif (_backgroundNext.isNull()) {\n\t\treturn;\n\t}\n\t_backgroundBack = std::move(_background);\n\t_background = base::take(_backgroundNext);\n\t_backgroundFade.start([=] {\n\t\tupdate();\n\t\tif (!_backgroundFade.animating()) {\n\t\t\t_backgroundBack = QImage();\n\t\t\tstartBackgroundFade();\n\t\t}\n\t}, 0., 1., kFadeBackgroundDuration);\n}\n\nvoid LayerWidget::parentResized() {\n\tresizeToWidth(parentWidget()->width());\n\tif (_background.isNull()) {\n\t\tstart();\n\t}\n}\n\nvoid LayerWidget::keyPressEvent(QKeyEvent *e) {\n\tQGuiApplication::sendEvent(_content.get(), e);\n}\n\nint LayerWidget::resizeGetHeight(int newWidth) {\n\treturn parentWidget()->height();\n}\n\nbool LayerWidget::closeByOutsideClick() const {\n\treturn false;\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/editor_layer_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/layer_widget.h\"\n#include \"ui/image/image.h\"\n#include \"editor/photo_editor_common.h\"\n#include \"base/unique_qptr.h\"\n#include \"base/timer.h\"\n\nenum class ImageRoundRadius;\n\nnamespace Window {\nclass Controller;\nclass SessionController;\n} // namespace Window\n\nnamespace Editor {\n\nclass LayerWidget final : public Ui::LayerWidget {\npublic:\n\tLayerWidget(\n\t\tnot_null<QWidget*> parent,\n\t\tbase::unique_qptr<Ui::RpWidget> content);\n\n\tvoid parentResized() override;\n\tbool closeByOutsideClick() const override;\n\nprivate:\n\tbool eventHook(QEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid start();\n\tvoid cacheBackground();\n\tvoid checkBackgroundStale();\n\tvoid checkCacheBackground();\n\t[[nodiscard]] QImage renderBackground();\n\tvoid backgroundReady(QImage background, bool night);\n\tvoid startBackgroundFade();\n\n\tconst base::unique_qptr<Ui::RpWidget> _content;\n\tQImage _backgroundBack;\n\tQImage _background;\n\tQImage _backgroundNext;\n\tUi::Animations::Simple _backgroundFade;\n\tbase::Timer _backgroundTimer;\n\tcrl::time _lastAreaChangeTime = 0;\n\tbool _backgroundCaching = false;\n\tbool _backgroundNight = false;\n\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/editor_paint.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/editor_paint.h\"\n\n#include \"core/mime_type.h\"\n#include \"editor/controllers/controllers.h\"\n#include \"editor/scene/scene_item_canvas.h\"\n#include \"editor/scene/scene_item_image.h\"\n#include \"editor/scene/scene_item_sticker.h\"\n#include \"editor/scene/scene.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n\n#include <QGraphicsView>\n#include <QScrollBar>\n#include <QWheelEvent>\n#include <QtCore/QMimeData>\n\nnamespace Editor {\nnamespace {\n\nconstexpr auto kMaxBrush = 25.;\nconstexpr auto kMinBrush = 1.;\nconstexpr auto kMinCanvasZoom = 1.;\nconstexpr auto kMaxCanvasZoom = 8.;\nconstexpr auto kCanvasZoomStep = 1.15;\nconstexpr auto kZoomEpsilon = 0.0001;\n\nstd::shared_ptr<Scene> EnsureScene(\n\t\tPhotoModifications &mods,\n\t\tconst QSize &size) {\n\tif (!mods.paint) {\n\t\tmods.paint = std::make_shared<Scene>(QRectF(QPointF(), size));\n\t}\n\treturn mods.paint;\n}\n\n} // namespace\n\nusing ItemPtr = std::shared_ptr<QGraphicsItem>;\n\nPaint::Paint(\n\tnot_null<Ui::RpWidget*> parent,\n\tPhotoModifications &modifications,\n\tconst QSize &imageSize,\n\tstd::shared_ptr<Controllers> controllers,\n\tFn<QImage(QRect)> blurSource)\n: RpWidget(parent)\n, _controllers(controllers)\n, _scene(EnsureScene(modifications, imageSize))\n, _view(base::make_unique_q<QGraphicsView>(_scene.get(), this))\n, _viewport(_view->viewport())\n, _imageSize(imageSize) {\n\tExpects(modifications.paint != nullptr);\n\n\t_scene->setBlurSource(std::move(blurSource));\n\n\tkeepResult();\n\n\t_view->show();\n\t_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);\n\t_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);\n\t_view->setFrameStyle(int(QFrame::NoFrame) | QFrame::Plain);\n\t_view->setBackgroundBrush(Qt::transparent);\n\t_view->setAttribute(Qt::WA_TranslucentBackground, true);\n\t_viewport->setAutoFillBackground(false);\n\t_viewport->setAttribute(Qt::WA_TranslucentBackground, true);\n\t_viewport->installEventFilter(this);\n\n\t// Undo / Redo.\n\tcontrollers->undoController->performRequestChanges(\n\t) | rpl::on_next([=](const Undo &command) {\n\t\tif (command == Undo::Undo) {\n\t\t\t_scene->performUndo();\n\t\t} else {\n\t\t\t_scene->performRedo();\n\t\t}\n\n\t\t_hasUndo = _scene->hasUndo();\n\t\t_hasRedo = _scene->hasRedo();\n\t}, lifetime());\n\n\tcontrollers->undoController->setCanPerformChanges(rpl::merge(\n\t\t_hasUndo.value() | rpl::map([](bool enable) {\n\t\t\treturn UndoController::EnableRequest{\n\t\t\t\t.command = Undo::Undo,\n\t\t\t\t.enable = enable,\n\t\t\t};\n\t\t}),\n\t\t_hasRedo.value() | rpl::map([](bool enable) {\n\t\t\treturn UndoController::EnableRequest{\n\t\t\t\t.command = Undo::Redo,\n\t\t\t\t.enable = enable,\n\t\t\t};\n\t\t})));\n\n\tif (controllers->stickersPanelController) {\n\t\tusing ShowRequest = StickersPanelController::ShowRequest;\n\n\t\tcontrollers->stickersPanelController->setShowRequestChanges(\n\t\t\tcontrollers->stickersPanelController->stickerChosen(\n\t\t\t) | rpl::map_to(ShowRequest::HideAnimated));\n\n\t\tcontrollers->stickersPanelController->stickerChosen(\n\t\t) | rpl::on_next([=](not_null<DocumentData*> document) {\n\t\t\tconst auto item = std::make_shared<ItemSticker>(\n\t\t\t\tdocument,\n\t\t\t\titemBaseData());\n\t\t\t_scene->addItem(item);\n\t\t\t_scene->clearSelection();\n\t\t}, lifetime());\n\t}\n\n\trpl::merge(\n\t\tcontrollers->stickersPanelController\n\t\t\t? controllers->stickersPanelController->stickerChosen(\n\t\t\t\t) | rpl::to_empty\n\t\t\t: rpl::never<>() | rpl::type_erased,\n\t\t_scene->addsItem()\n\t) | rpl::on_next([=] {\n\t\tclearRedoList();\n\t\tupdateUndoState();\n\t}, lifetime());\n\n\t_scene->removesItem(\n\t) | rpl::on_next([=] {\n\t\tupdateUndoState();\n\t}, lifetime());\n\n}\n\nPaint::~Paint() {\n\tif (_viewport) {\n\t\t_viewport->removeEventFilter(this);\n\t}\n}\n\nvoid Paint::updateViewGeometry() {\n\tif (_imageGeometry.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto target = (_transform.userZoom - kMinCanvasZoom) > kZoomEpsilon\n\t\t? _outerGeometry\n\t\t: _imageGeometry;\n\tif (geometry() != target) {\n\t\tsetGeometry(target);\n\t}\n\t_view->setGeometry(rect());\n}\n\nvoid Paint::applyTransform(QRect geometry, int angle, bool flipped) {\n\tif (geometry.isEmpty()) {\n\t\treturn;\n\t}\n\t_imageGeometry = geometry;\n\t_outerGeometry = parentWidget() ? parentWidget()->rect() : geometry;\n\n\tconst auto center = (_transform.fitZoom <= 0.)\n\t\t|| _view->viewport()->rect().isEmpty()\n\t\t? rect::center(_scene->sceneRect())\n\t\t: _view->mapToScene(_view->viewport()->rect().center());\n\tconst auto size = geometry.size();\n\n\tconst auto rotatedImageSize = QTransform()\n\t\t.rotate(angle)\n\t\t.mapRect(QRect(QPoint(), _imageSize));\n\n\tconst auto ratioW = size.width() / float64(rotatedImageSize.width())\n\t\t* (flipped ? -1 : 1);\n\tconst auto ratioH = size.height() / float64(rotatedImageSize.height());\n\n\t_view->setGeometry(rect());\n\n\t_transform = {\n\t\t.angle = angle,\n\t\t.flipped = flipped,\n\t\t.fitZoom = ((std::abs(ratioW) + std::abs(ratioH)) / 2.),\n\t\t.ratioW = ratioW,\n\t\t.ratioH = ratioH,\n\t\t.userZoom = _transform.userZoom,\n\t};\n\tupdateViewGeometry();\n\tapplyViewTransform();\n\t_view->centerOn(center);\n\tif (const auto parent = parentWidget()) {\n\t\tparent->update();\n\t}\n}\n\nstd::shared_ptr<Scene> Paint::saveScene() const {\n\t_scene->save(SaveState::Save);\n\treturn _scene->items().empty()\n\t\t? nullptr\n\t\t: _scene;\n}\n\nvoid Paint::restoreScene() {\n\t_scene->restore(SaveState::Save);\n}\n\nvoid Paint::cancel() {\n\t_scene->restore(SaveState::Keep);\n}\n\nvoid Paint::keepResult() {\n\t_scene->save(SaveState::Keep);\n}\n\nvoid Paint::clearRedoList() {\n\t_scene->clearRedoList();\n\n\t_hasRedo = false;\n}\n\nvoid Paint::updateUndoState() {\n\t_hasUndo = _scene->hasUndo();\n\t_hasRedo = _scene->hasRedo();\n}\n\nvoid Paint::applyBrush(const Brush &brush) {\n\t_scene->applyBrush(\n\t\tbrush.color,\n\t\t(kMinBrush + float64(kMaxBrush - kMinBrush) * brush.sizeRatio),\n\t\tbrush.tool);\n}\n\nvoid Paint::handleMimeData(const QMimeData *data) {\n\tconst auto add = [&](QImage image) {\n\t\tif (image.isNull()) {\n\t\t\treturn;\n\t\t}\n\t\tif (!Ui::ValidateThumbDimensions(image.width(), image.height())) {\n\t\t\t_controllers->show->showBox(\n\t\t\t\tUi::MakeInformBox(tr::lng_edit_media_invalid_file()));\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto item = std::make_shared<ItemImage>(\n\t\t\tUi::PixmapFromImage(std::move(image)),\n\t\t\titemBaseData());\n\t\t_scene->addItem(item);\n\t\t_scene->clearSelection();\n\t};\n\n\tusing Error = Ui::PreparedList::Error;\n\tconst auto premium = false; // Don't support > 2GB files here.\n\tconst auto list = Core::ReadMimeUrls(data);\n\tauto result = !list.isEmpty()\n\t\t? Storage::PrepareMediaList(\n\t\t\tlist.mid(0, 1),\n\t\t\t_imageSize.width() / 2,\n\t\t\tpremium)\n\t\t: Ui::PreparedList(Error::EmptyFile, QString());\n\tif (result.error == Error::None) {\n\t\tadd(base::take(result.files.front().preview));\n\t} else if (auto read = Core::ReadMimeImage(data)) {\n\t\tadd(std::move(read.image));\n\t}\n}\n\nvoid Paint::paintImage(QPainter &p, const QPixmap &image) const {\n\tif (_view->geometry().isEmpty()) {\n\t\treturn;\n\t}\n\tp.save();\n\tp.setClipRect(geometry(), Qt::IntersectClip);\n\tp.translate(pos());\n\tp.setTransform(_view->viewportTransform(), true);\n\tp.drawPixmap(Rect(_imageSize), image);\n\tp.restore();\n}\n\nvoid Paint::resetView() {\n\tif (_transform.userZoom == kMinCanvasZoom) {\n\t\treturn;\n\t}\n\t_transform.userZoom = kMinCanvasZoom;\n\tupdateViewGeometry();\n\tapplyViewTransform();\n\t_view->centerOn(rect::center(_scene->sceneRect()));\n\tif (const auto parent = parentWidget()) {\n\t\tparent->update(geometry());\n\t}\n}\n\nItemBase::Data Paint::itemBaseData() const {\n\tconst auto s = _scene->sceneRect().toRect().size();\n\tconst auto size = std::min(s.width(), s.height()) / 2;\n\tconst auto x = s.width() / 2;\n\tconst auto y = s.height() / 2;\n\treturn ItemBase::Data{\n\t\t.initialZoom = _transform.zoom,\n\t\t.zPtr = _scene->lastZ(),\n\t\t.size = size,\n\t\t.x = x,\n\t\t.y = y,\n\t\t.flipped = _transform.flipped,\n\t\t.rotation = -_transform.angle,\n\t\t.imageSize = _imageSize,\n\t};\n}\n\nvoid Paint::applyViewTransform() {\n\t_view->setTransform(QTransform()\n\t\t.scale(\n\t\t\t_transform.ratioW * _transform.userZoom,\n\t\t\t_transform.ratioH * _transform.userZoom)\n\t\t.rotate(_transform.angle));\n\t_transform.zoom = _transform.fitZoom * _transform.userZoom;\n\t_scene->updateZoom(_transform.zoom);\n}\n\nbool Paint::eventFilter(QObject *obj, QEvent *e) {\n\tif (obj != _viewport) {\n\t\treturn RpWidget::eventFilter(obj, e);\n\t}\n\tconst auto view = _view.get();\n\tif (!view || !_viewport) {\n\t\treturn true;\n\t}\n\tif (e->type() == QEvent::Wheel) {\n\t\tconst auto wheel = static_cast<QWheelEvent*>(e);\n\t\tconst auto delta = wheel->angleDelta().y();\n\t\tif (!delta) {\n\t\t\treturn true;\n\t\t}\n\n\t\tconst auto step = delta / float64(QWheelEvent::DefaultDeltasPerStep);\n\t\tconst auto factor = std::pow(kCanvasZoomStep, step);\n\t\tconst auto newZoom = std::clamp(\n\t\t\t_transform.userZoom * factor,\n\t\t\tkMinCanvasZoom,\n\t\t\tkMaxCanvasZoom);\n\t\tif (std::abs(newZoom - _transform.userZoom) < 0.0001) {\n\t\t\treturn true;\n\t\t}\n\n\t\tconst auto viewportPoint = wheel->position().toPoint();\n\t\tconst auto globalPoint = _viewport->mapToGlobal(viewportPoint);\n\t\tconst auto scenePoint = view->mapToScene(viewportPoint);\n\t\t_transform.userZoom = newZoom;\n\t\tupdateViewGeometry();\n\t\tapplyViewTransform();\n\t\tconst auto scenePointAfter = view->mapToScene(\n\t\t\t_viewport->mapFromGlobal(globalPoint));\n\t\tconst auto center = view->mapToScene(rect::center(_viewport->rect()));\n\t\tview->centerOn(center - (scenePointAfter - scenePoint));\n\t\tif (const auto parent = parentWidget()) {\n\t\t\tparent->update(geometry());\n\t\t}\n\t\treturn true;\n\t} else if (e->type() == QEvent::MouseButtonPress) {\n\t\tconst auto mouse = static_cast<QMouseEvent*>(e);\n\t\tif (mouse->button() == Qt::MiddleButton) {\n\t\t\t_pan = {\n\t\t\t\t.active = (_transform.userZoom > kMinCanvasZoom),\n\t\t\t\t.point = mouse->pos(),\n\t\t\t};\n\t\t\tif (_pan.active) {\n\t\t\t\t_viewport->setCursor(Qt::ClosedHandCursor);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t} else if (e->type() == QEvent::MouseMove) {\n\t\tconst auto mouse = static_cast<QMouseEvent*>(e);\n\t\tif (_pan.active) {\n\t\t\tconst auto point = mouse->pos();\n\t\t\tconst auto delta = point - _pan.point;\n\t\t\t_pan.point = point;\n\n\t\t\tif (_transform.userZoom > kMinCanvasZoom) {\n\t\t\t\tview->horizontalScrollBar()->setValue(\n\t\t\t\t\tview->horizontalScrollBar()->value() - delta.x());\n\t\t\t\tview->verticalScrollBar()->setValue(\n\t\t\t\t\tview->verticalScrollBar()->value() - delta.y());\n\t\t\t\tif (const auto parent = parentWidget()) {\n\t\t\t\t\tparent->update(geometry());\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t} else if (e->type() == QEvent::MouseButtonRelease) {\n\t\tconst auto mouse = static_cast<QMouseEvent*>(e);\n\t\tif (mouse->button() == Qt::MiddleButton) {\n\t\t\tif (_pan.active) {\n\t\t\t\t_viewport->unsetCursor();\n\t\t\t}\n\t\t\t_pan.active = false;\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn RpWidget::eventFilter(obj, e);\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/editor_paint.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\n#include \"editor/photo_editor_common.h\"\n#include \"editor/photo_editor_inner_common.h\"\n#include \"editor/scene/scene_item_base.h\"\n\nclass QGraphicsItem;\nclass QGraphicsView;\n\nnamespace Editor {\n\nstruct Controllers;\nclass Scene;\n\n// Paint control.\nclass Paint final : public Ui::RpWidget {\npublic:\n\tPaint(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tPhotoModifications &modifications,\n\t\tconst QSize &imageSize,\n\t\tstd::shared_ptr<Controllers> controllers,\n\t\tFn<QImage(QRect)> blurSource);\n\t~Paint() override;\n\n\t[[nodiscard]] std::shared_ptr<Scene> saveScene() const;\n\tvoid restoreScene();\n\n\tvoid applyTransform(QRect geometry, int angle, bool flipped);\n\tvoid applyBrush(const Brush &brush);\n\tvoid cancel();\n\tvoid keepResult();\n\tvoid updateUndoState();\n\n\tvoid handleMimeData(const QMimeData *data);\n\tvoid paintImage(QPainter &p, const QPixmap &image) const;\n\tvoid resetView();\n\nprivate:\n\tbool eventFilter(QObject *obj, QEvent *e) override;\n\tvoid updateViewGeometry();\n\n\tstruct SavedItem {\n\t\tstd::shared_ptr<QGraphicsItem> item;\n\t\tbool undid = false;\n\t};\n\n\tItemBase::Data itemBaseData() const;\n\tvoid applyViewTransform();\n\n\tvoid clearRedoList();\n\n\tconst std::shared_ptr<Controllers> _controllers;\n\tconst std::shared_ptr<Scene> _scene;\n\tconst base::unique_qptr<QGraphicsView> _view;\n\tQPointer<QWidget> _viewport;\n\tconst QSize _imageSize;\n\tQRect _imageGeometry;\n\tQRect _outerGeometry;\n\n\tstruct {\n\t\tint angle = 0;\n\t\tbool flipped = false;\n\t\tfloat64 zoom = 0.;\n\t\tfloat64 fitZoom = 0.;\n\t\tfloat64 ratioW = 0.;\n\t\tfloat64 ratioH = 0.;\n\t\tfloat64 userZoom = 1.;\n\t} _transform;\n\n\tstruct {\n\t\tbool active = false;\n\t\tQPoint point;\n\t} _pan;\n\n\trpl::variable<bool> _hasUndo = true;\n\trpl::variable<bool> _hasRedo = true;\n\n\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/photo_editor.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/photo_editor.h\"\n\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"editor/color_picker.h\"\n#include \"editor/controllers/controllers.h\"\n#include \"editor/photo_editor_content.h\"\n#include \"editor/photo_editor_controls.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"ui/layers/layer_widget.h\"\n#include \"styles/style_editor.h\"\n\nnamespace Editor {\nnamespace {\n\nconstexpr auto kPrecision = 100000;\nconstexpr auto kBrushesVersion = -2;\nconstexpr auto kDefaultBrushSizeRatio = 0.9;\n\n[[nodiscard]] int ToolIndex(Brush::Tool tool) {\n\tswitch (tool) {\n\tcase Brush::Tool::Pen: return 0;\n\tcase Brush::Tool::Arrow: return 1;\n\tcase Brush::Tool::Marker: return 2;\n\tcase Brush::Tool::Blur: return 3;\n\tcase Brush::Tool::Eraser: return 4;\n\t}\n\treturn 0;\n}\n\n[[nodiscard]] Brush::Tool ToolFromIndex(int index) {\n\tswitch (index) {\n\tcase 0: return Brush::Tool::Pen;\n\tcase 1: return Brush::Tool::Arrow;\n\tcase 2: return Brush::Tool::Marker;\n\tcase 3: return Brush::Tool::Blur;\n\tcase 4: return Brush::Tool::Eraser;\n\t}\n\treturn Brush::Tool::Pen;\n}\n\n[[nodiscard]] Brush::Tool ToolFromSerialized(qint32 value) {\n\tswitch (value) {\n\tcase int(Brush::Tool::Pen): return Brush::Tool::Pen;\n\tcase int(Brush::Tool::Arrow): return Brush::Tool::Arrow;\n\tcase int(Brush::Tool::Marker): return Brush::Tool::Marker;\n\tcase int(Brush::Tool::Eraser): return Brush::Tool::Eraser;\n\tcase int(Brush::Tool::Blur): return Brush::Tool::Blur;\n\t}\n\treturn Brush::Tool::Pen;\n}\n\n[[nodiscard]] QColor DefaultBrushColor(Brush::Tool tool) {\n\tswitch (tool) {\n\tcase Brush::Tool::Pen: return QColor(234, 39, 57);\n\tcase Brush::Tool::Arrow: return QColor(252, 150, 77);\n\tcase Brush::Tool::Marker: return QColor(252, 222, 101);\n\tcase Brush::Tool::Eraser: return QColor(0, 0, 0);\n\tcase Brush::Tool::Blur: return QColor(0, 0, 0);\n\t}\n\treturn QColor(234, 39, 57);\n}\n\n[[nodiscard]] Brush DefaultBrush(Brush::Tool tool) {\n\tauto result = Brush();\n\tresult.sizeRatio = kDefaultBrushSizeRatio;\n\tresult.color = DefaultBrushColor(tool);\n\tresult.tool = tool;\n\treturn result;\n}\n\n[[nodiscard]] std::array<Brush, 5> DefaultBrushes() {\n\tauto result = std::array<Brush, 5>();\n\tfor (auto i = 0; i != int(result.size()); ++i) {\n\t\tconst auto tool = ToolFromIndex(i);\n\t\tresult[i] = DefaultBrush(tool);\n\t}\n\treturn result;\n}\n\nstruct BrushState {\n\tstd::array<Brush, 5> brushes = DefaultBrushes();\n\tBrush::Tool tool = Brush::Tool::Pen;\n};\n\n[[nodiscard]] QByteArray Serialize(\n\t\tconst std::array<Brush, 5> &brushes,\n\t\tBrush::Tool tool) {\n\tauto result = QByteArray();\n\tauto stream = QDataStream(&result, QIODevice::WriteOnly);\n\tstream.setVersion(QDataStream::Qt_5_3);\n\tstream\n\t\t<< qint32(kBrushesVersion)\n\t\t<< qint32(int(tool))\n\t\t<< qint32(brushes.size());\n\tfor (auto i = 0; i != int(brushes.size()); ++i) {\n\t\tconst auto tool = ToolFromIndex(i);\n\t\tconst auto &brush = brushes[i];\n\t\tstream\n\t\t\t<< qint32(int(tool))\n\t\t\t<< qint32(brush.sizeRatio * kPrecision)\n\t\t\t<< brush.color;\n\t}\n\tstream.device()->close();\n\n\treturn result;\n}\n\n[[nodiscard]] BrushState Deserialize(const QByteArray &data) {\n\tauto result = BrushState();\n\tif (data.isEmpty()) {\n\t\treturn result;\n\t}\n\tauto stream = QDataStream(data);\n\tauto head = qint32(0);\n\tstream >> head;\n\tif (stream.status() != QDataStream::Ok) {\n\t\treturn result;\n\t}\n\tif (head < 0) {\n\t\tconst auto version = head;\n\t\tauto toolValue = qint32(int(Brush::Tool::Pen));\n\t\tauto count = qint32(0);\n\t\tstream >> toolValue >> count;\n\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\treturn result;\n\t\t}\n\t\tresult.tool = ToolFromSerialized(toolValue);\n\t\tauto limit = int(count);\n\t\tif (limit < 0) {\n\t\t\tlimit = 0;\n\t\t} else if (limit > int(result.brushes.size())) {\n\t\t\tlimit = int(result.brushes.size());\n\t\t}\n\t\tfor (auto i = 0; i != limit; ++i) {\n\t\t\tauto entryTool = qint32(int(Brush::Tool::Pen));\n\t\t\tauto size = qint32(0);\n\t\t\tauto color = QColor();\n\t\t\tstream >> entryTool >> size >> color;\n\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tconst auto tool = ToolFromSerialized(entryTool);\n\t\t\tconst auto index = ToolIndex(tool);\n\t\t\tif (version == kBrushesVersion && size > 0) {\n\t\t\t\tresult.brushes[index].sizeRatio = size / float(kPrecision);\n\t\t\t}\n\t\t\tif (color.isValid()) {\n\t\t\t\tresult.brushes[index].color = color;\n\t\t\t}\n\t\t\tresult.brushes[index].tool = tool;\n\t\t}\n\t\treturn result;\n\t}\n\tauto color = QColor();\n\tstream >> color;\n\tif (stream.status() != QDataStream::Ok) {\n\t\treturn result;\n\t}\n\tauto toolValue = qint32(int(Brush::Tool::Pen));\n\tif (!stream.atEnd()) {\n\t\tstream >> toolValue;\n\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\ttoolValue = qint32(int(Brush::Tool::Pen));\n\t\t}\n\t}\n\tconst auto tool = ToolFromSerialized(toolValue);\n\tconst auto index = ToolIndex(tool);\n\tif (color.isValid()) {\n\t\tresult.brushes[index].color = color;\n\t}\n\tresult.brushes[index].tool = tool;\n\tresult.tool = tool;\n\treturn result;\n}\n\n} // namespace\n\nPhotoEditor::PhotoEditor(\n\tnot_null<QWidget*> parent,\n\tnot_null<Window::Controller*> controller,\n\tstd::shared_ptr<Image> photo,\n\tPhotoModifications modifications,\n\tEditorData data)\n: PhotoEditor(\n\tparent,\n\tcontroller->uiShow(),\n\t(controller->sessionController()\n\t\t? controller->sessionController()->uiShow()\n\t\t: nullptr),\n\tstd::move(photo),\n\tstd::move(modifications),\n\tstd::move(data)) {\n}\n\nPhotoEditor::PhotoEditor(\n\tnot_null<QWidget*> parent,\n\tstd::shared_ptr<Ui::Show> show,\n\tstd::shared_ptr<ChatHelpers::Show> sessionShow,\n\tstd::shared_ptr<Image> photo,\n\tPhotoModifications modifications,\n\tEditorData data)\n: RpWidget(parent)\n, _modifications(std::move(modifications))\n, _controllers(std::make_shared<Controllers>(\n\tsessionShow\n\t\t? std::make_unique<StickersPanelController>(\n\t\t\tthis,\n\t\t\tstd::move(sessionShow))\n\t\t: nullptr,\n\tstd::make_unique<UndoController>(),\n\tshow))\n, _content(base::make_unique_q<PhotoEditorContent>(\n\tthis,\n\tphoto,\n\t_modifications,\n\t_controllers,\n\tdata))\n, _controls(base::make_unique_q<PhotoEditorControls>(\n\tthis,\n\t_controllers,\n\t_modifications,\n\tdata,\n\tphoto->size()))\n, _brushes(Deserialize(Core::App().settings().photoEditorBrush()).brushes)\n, _brushTool(Deserialize(Core::App().settings().photoEditorBrush()).tool)\n, _colorPicker(std::make_unique<ColorPicker>(\n\tthis,\n\tstd::move(show),\n\t_brushes,\n\t_brushTool)) {\n\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\tif (size.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\t_content->setGeometry(rect() - st::photoEditorContentMargins);\n\t}, lifetime());\n\n\t_content->innerRect(\n\t) | rpl::on_next([=](QRect inner) {\n\t\tif (inner.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\t_colorPicker->setCanvasRect(inner.translated(_content->pos()));\n\t\tconst auto innerTop = _content->y() + inner.top();\n\t\tconst auto skip = st::photoEditorCropPointSize;\n\t\tconst auto controlsRect = rect()\n\t\t\t- style::margins(0, innerTop + inner.height() + skip, 0, 0);\n\t\t_controls->setGeometry(controlsRect);\n\t}, lifetime());\n\n\t_controls->colorLinePositionValue(\n\t) | rpl::on_next([=](const QPoint &p) {\n\t\t_colorPicker->moveLine(p);\n\t}, _controls->lifetime());\n\n\t_controls->colorLineShownValue(\n\t) | rpl::on_next([=](bool shown) {\n\t\t_colorPicker->setVisible(shown);\n\t}, _controls->lifetime());\n\n\t_mode.value(\n\t) | rpl::on_next([=](const PhotoEditorMode &mode) {\n\t\t_content->applyMode(mode);\n\t\t_controls->applyMode(mode);\n\t}, lifetime());\n\n\t_controls->rotateRequests(\n\t) | rpl::on_next([=](int angle) {\n\t\t_modifications.angle += 90;\n\t\tif (_modifications.angle >= 360) {\n\t\t\t_modifications.angle -= 360;\n\t\t}\n\t\t_content->applyModifications(_modifications);\n\t}, lifetime());\n\n\t_controls->flipRequests(\n\t) | rpl::on_next([=] {\n\t\t_modifications.flipped = !_modifications.flipped;\n\t\t_content->applyModifications(_modifications);\n\t}, lifetime());\n\n\t_controls->aspectRatioChanges() | rpl::on_next([=](float64 ratio) {\n\t\t_content->applyAspectRatio(ratio);\n\t}, lifetime());\n\n\t_controls->paintModeRequests(\n\t) | rpl::on_next([=] {\n\t\t_mode = PhotoEditorMode{\n\t\t\t.mode = PhotoEditorMode::Mode::Paint,\n\t\t\t.action = PhotoEditorMode::Action::None,\n\t\t};\n\t}, lifetime());\n\n\t_controls->doneRequests(\n\t) | rpl::on_next([=] {\n\t\tconst auto mode = _mode.current().mode;\n\t\tif (mode == PhotoEditorMode::Mode::Paint) {\n\t\t\t_mode = PhotoEditorMode{\n\t\t\t\t.mode = PhotoEditorMode::Mode::Transform,\n\t\t\t\t.action = PhotoEditorMode::Action::Save,\n\t\t\t};\n\t\t} else if (mode == PhotoEditorMode::Mode::Transform) {\n\t\t\t_mode = PhotoEditorMode{\n\t\t\t\t.mode = PhotoEditorMode::Mode::Out,\n\t\t\t\t.action = PhotoEditorMode::Action::Save,\n\t\t\t};\n\t\t\tsave();\n\t\t}\n\t}, lifetime());\n\n\t_controls->cancelRequests(\n\t) | rpl::on_next([=] {\n\t\tconst auto mode = _mode.current().mode;\n\t\tif (mode == PhotoEditorMode::Mode::Paint) {\n\t\t\t_mode = PhotoEditorMode{\n\t\t\t\t.mode = PhotoEditorMode::Mode::Transform,\n\t\t\t\t.action = PhotoEditorMode::Action::Discard,\n\t\t\t};\n\t\t} else if (mode == PhotoEditorMode::Mode::Transform) {\n\t\t\t_mode = PhotoEditorMode{\n\t\t\t\t.mode = PhotoEditorMode::Mode::Out,\n\t\t\t\t.action = PhotoEditorMode::Action::Discard,\n\t\t\t};\n\t\t\t_cancel.fire({});\n\t\t}\n\t}, lifetime());\n\n\t_colorPicker->saveBrushRequests(\n\t) | rpl::on_next([=](const Brush &brush) {\n\t\t_content->applyBrush(brush);\n\n\t\t_brushTool = brush.tool;\n\t\t_brushes[ToolIndex(brush.tool)] = brush;\n\t\tconst auto serialized = Serialize(_brushes, _brushTool);\n\t\tif (Core::App().settings().photoEditorBrush() != serialized) {\n\t\t\tCore::App().settings().setPhotoEditorBrush(serialized);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}\n\t}, lifetime());\n}\n\nvoid PhotoEditor::keyPressEvent(QKeyEvent *e) {\n\tif (!_colorPicker->preventHandleKeyPress()) {\n\t\t_content->handleKeyPress(e) || _controls->handleKeyPress(e);\n\t}\n}\n\nvoid PhotoEditor::save() {\n\t_content->save(_modifications);\n\t_done.fire_copy(_modifications);\n}\n\nrpl::producer<PhotoModifications> PhotoEditor::doneRequests() const {\n\treturn _done.events();\n}\n\nrpl::producer<> PhotoEditor::cancelRequests() const {\n\treturn _cancel.events();\n}\n\nvoid InitEditorLayer(\n\t\tnot_null<Ui::LayerWidget*> layer,\n\t\tnot_null<PhotoEditor*> editor,\n\t\tFn<void(PhotoModifications)> doneCallback) {\n\teditor->cancelRequests(\n\t) | rpl::on_next([=] {\n\t\tlayer->closeLayer();\n\t}, editor->lifetime());\n\n\tconst auto weak = base::make_weak(layer.get());\n\teditor->doneRequests(\n\t) | rpl::on_next([=, done = std::move(doneCallback)](\n\t\t\tconst PhotoModifications &mods) {\n\t\tdone(mods);\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->closeLayer();\n\t\t}\n\t}, editor->lifetime());\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/photo_editor.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/image/image.h\"\n#include \"base/unique_qptr.h\"\n#include \"editor/photo_editor_common.h\"\n#include \"editor/photo_editor_inner_common.h\"\n\n#include <array>\n\nnamespace Ui {\nclass LayerWidget;\nclass Show;\n} // namespace Ui\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Window {\nclass Controller;\n} // namespace Window\n\nnamespace Editor {\n\nclass ColorPicker;\nclass PhotoEditorContent;\nclass PhotoEditorControls;\nstruct Controllers;\n\nclass PhotoEditor final : public Ui::RpWidget {\npublic:\n\tPhotoEditor(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::Controller*> controller,\n\t\tstd::shared_ptr<Image> photo,\n\t\tPhotoModifications modifications,\n\t\tEditorData data = EditorData());\n\tPhotoEditor(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tstd::shared_ptr<ChatHelpers::Show> sessionShow,\n\t\tstd::shared_ptr<Image> photo,\n\t\tPhotoModifications modifications,\n\t\tEditorData data = EditorData());\n\n\tvoid save();\n\t[[nodiscard]] rpl::producer<PhotoModifications> doneRequests() const;\n\t[[nodiscard]] rpl::producer<> cancelRequests() const;\n\nprivate:\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\n\tPhotoModifications _modifications;\n\n\tconst std::shared_ptr<Controllers> _controllers;\n\n\tbase::unique_qptr<PhotoEditorContent> _content;\n\tbase::unique_qptr<PhotoEditorControls> _controls;\n\tstd::array<Brush, 5> _brushes;\n\tBrush::Tool _brushTool = Brush::Tool::Pen;\n\tconst std::unique_ptr<ColorPicker> _colorPicker;\n\n\trpl::variable<PhotoEditorMode> _mode = PhotoEditorMode{\n\t\t.mode = PhotoEditorMode::Mode::Transform,\n\t\t.action = PhotoEditorMode::Action::None,\n\t};\n\trpl::event_stream<PhotoModifications> _done;\n\trpl::event_stream<> _cancel;\n\n};\n\nvoid InitEditorLayer(\n\tnot_null<Ui::LayerWidget*> layer,\n\tnot_null<PhotoEditor*> editor,\n\tFn<void(PhotoModifications)> doneCallback);\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/photo_editor_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/photo_editor_common.h\"\n\n#include \"editor/scene/scene.h\"\n#include \"ui/painter.h\"\n\nnamespace Editor {\n\nQImage ImageModified(QImage image, const PhotoModifications &mods) {\n\tExpects(!image.isNull());\n\n\tif (!mods) {\n\t\treturn image;\n\t}\n\tif (mods.paint) {\n\t\tif (image.format() != QImage::Format_ARGB32_Premultiplied) {\n\t\t\timage = image.convertToFormat(\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t}\n\n\t\tPainter p(&image);\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tmods.paint->render(&p, image.rect());\n\t}\n\tauto cropped = mods.crop.isValid()\n\t\t? image.copy(mods.crop)\n\t\t: image;\n\tQTransform transform;\n\tif (mods.flipped) {\n\t\ttransform.scale(-1, 1);\n\t}\n\tif (mods.angle) {\n\t\ttransform.rotate(mods.angle);\n\t}\n\treturn cropped.transformed(transform);\n}\n\nbool PhotoModifications::empty() const {\n\treturn !angle && !flipped && !crop.isValid() && !paint;\n}\n\nPhotoModifications::operator bool() const {\n\treturn !empty();\n}\n\nPhotoModifications::~PhotoModifications() {\n\tif (paint && (paint.use_count() == 1)) {\n\t\tpaint->deleteLater();\n\t}\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/photo_editor_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Editor {\n\nclass Scene;\n\nstruct PhotoModifications {\n\tint angle = 0;\n\tbool flipped = false;\n\tQRect crop;\n\tstd::shared_ptr<Scene> paint = nullptr;\n\n\t[[nodiscard]] bool empty() const;\n\t[[nodiscard]] explicit operator bool() const;\n\t~PhotoModifications();\n\n};\n\nstruct EditorData {\n\tenum class CropType {\n\t\tRect,\n\t\tEllipse,\n\t\tRoundedRect,\n\t};\n\n\tTextWithEntities about;\n\tQString confirm;\n\tQSize exactSize;\n\tCropType cropType = CropType::Rect;\n\tbool keepAspectRatio = false;\n};\n\n[[nodiscard]] QImage ImageModified(\n\tQImage image,\n\tconst PhotoModifications &mods);\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/photo_editor_content.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/photo_editor_content.h\"\n\n#include \"editor/editor_crop.h\"\n#include \"editor/editor_paint.h\"\n#include \"history/history_drag_area.h\"\n#include \"media/view/media_view_pip.h\"\n#include \"storage/storage_media_prepare.h\"\n\nnamespace Editor {\n\nusing Media::View::FlipSizeByRotation;\nusing Media::View::RotatedRect;\n\nPhotoEditorContent::PhotoEditorContent(\n\tnot_null<Ui::RpWidget*> parent,\n\tstd::shared_ptr<Image> photo,\n\tPhotoModifications modifications,\n\tstd::shared_ptr<Controllers> controllers,\n\tEditorData data)\n: RpWidget(parent)\n, _photoSize(photo->size())\n, _paint(base::make_unique_q<Paint>(\n\tthis,\n\tmodifications,\n\t_photoSize,\n\tstd::move(controllers),\n\t[photo](QRect rect) {\n\t\tconst auto &img = photo->original();\n\t\tconst auto dpr = img.devicePixelRatio();\n\t\tconst auto pixelRect = QRect(\n\t\t\tint(rect.x() * dpr),\n\t\t\tint(rect.y() * dpr),\n\t\t\tint(rect.width() * dpr),\n\t\t\tint(rect.height() * dpr));\n\t\tauto result = img.copy(pixelRect.intersected(img.rect()));\n\t\tresult.setDevicePixelRatio(dpr);\n\t\treturn result;\n\t}))\n, _crop(base::make_unique_q<Crop>(\n\tthis,\n\tmodifications,\n\t_photoSize,\n\tstd::move(data)))\n, _photo(std::move(photo))\n, _modifications(modifications) {\n\n\trpl::combine(\n\t\t_modifications.value(),\n\t\tsizeValue()\n\t) | rpl::on_next([=](\n\t\t\tconst PhotoModifications &mods, const QSize &size) {\n\t\tif (size.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto imageSizeF = [&] {\n\t\t\tconst auto rotatedSize\n\t\t\t\t= FlipSizeByRotation(size, mods.angle);\n\t\t\tconst auto m = _crop->cropMargins();\n\t\t\tconst auto sizeForCrop = rotatedSize\n\t\t\t\t- QSize(m.left() + m.right(), m.top() + m.bottom());\n\t\t\tconst auto originalSize = QSizeF(_photoSize);\n\t\t\tif ((originalSize.width() > sizeForCrop.width())\n\t\t\t\t|| (originalSize.height() > sizeForCrop.height())) {\n\t\t\t\treturn originalSize.scaled(\n\t\t\t\t\tsizeForCrop,\n\t\t\t\t\tQt::KeepAspectRatio);\n\t\t\t}\n\t\t\treturn originalSize;\n\t\t}();\n\t\tconst auto imageSize = QSize(imageSizeF.width(), imageSizeF.height());\n\t\t_imageRect = QRect(\n\t\t\tQPoint(-imageSize.width() / 2, -imageSize.height() / 2),\n\t\t\timageSize);\n\n\t\t_imageMatrix.reset();\n\t\t_imageMatrix.translate(size.width() / 2, size.height() / 2);\n\t\tif (mods.flipped) {\n\t\t\t_imageMatrix.scale(-1, 1);\n\t\t}\n\t\t_imageMatrix.rotate(mods.angle);\n\n\t\tconst auto geometry = _imageMatrix.mapRect(_imageRect);\n\t\t_crop->applyTransform(\n\t\t\tgeometry + _crop->cropMargins(),\n\t\t\tmods.angle,\n\t\t\tmods.flipped, imageSizeF);\n\t\t_paint->applyTransform(geometry, mods.angle, mods.flipped);\n\n\t\t_innerRect = geometry;\n\t}, lifetime());\n\n\tpaintRequest(\n\t) | rpl::on_next([=](const QRect &clip) {\n\t\tauto p = QPainter(this);\n\n\t\tp.fillRect(clip, Qt::transparent);\n\t\tif (_mode.mode == PhotoEditorMode::Mode::Paint) {\n\t\t\t_paint->paintImage(p, _photo->pix(_photoSize));\n\t\t} else {\n\t\t\tp.setTransform(_imageMatrix);\n\t\t\tp.drawPixmap(_imageRect, _photo->pix(_imageRect.size()));\n\t\t}\n\t}, lifetime());\n\n\tsetupDragArea();\n}\n\nvoid PhotoEditorContent::applyModifications(\n\t\tPhotoModifications modifications) {\n\t_modifications = std::move(modifications);\n\tupdate();\n}\n\nvoid PhotoEditorContent::save(PhotoModifications &modifications) {\n\tmodifications.crop = _crop->saveCropRect();\n\t_paint->keepResult();\n\n\tconst auto savedScene = _paint->saveScene();\n\tif (!modifications.paint) {\n\t\tmodifications.paint = savedScene;\n\t}\n}\n\nvoid PhotoEditorContent::applyMode(const PhotoEditorMode &mode) {\n\tif (mode.mode == PhotoEditorMode::Mode::Out) {\n\t\tif (mode.action == PhotoEditorMode::Action::Discard) {\n\t\t\t_paint->restoreScene();\n\t\t}\n\t\treturn;\n\t}\n\tconst auto isTransform = (mode.mode == PhotoEditorMode::Mode::Transform);\n\t_crop->setVisible(isTransform);\n\n\t_paint->setAttribute(Qt::WA_TransparentForMouseEvents, isTransform);\n\tif (!isTransform) {\n\t\t_paint->updateUndoState();\n\t} else {\n\t\t_paint->resetView();\n\t}\n\n\tif (mode.action == PhotoEditorMode::Action::Discard) {\n\t\t_paint->cancel();\n\t} else if (mode.action == PhotoEditorMode::Action::Save) {\n\t\t_paint->keepResult();\n\t}\n\t_mode = mode;\n\tupdate();\n}\n\nvoid PhotoEditorContent::applyAspectRatio(float64 ratio) {\n\t_crop->setAspectRatio(ratio);\n}\n\nvoid PhotoEditorContent::applyBrush(const Brush &brush) {\n\t_paint->applyBrush(brush);\n}\n\nbool PhotoEditorContent::handleKeyPress(not_null<QKeyEvent*> e) const {\n\treturn false;\n}\n\nvoid PhotoEditorContent::setupDragArea() {\n\tauto dragEnterFilter = [=](const QMimeData *data) {\n\t\treturn (_mode.mode == PhotoEditorMode::Mode::Paint)\n\t\t\t? Storage::ValidatePhotoEditorMediaDragData(data)\n\t\t\t: false;\n\t};\n\n\tconst auto areas = DragArea::SetupDragAreaToContainer(\n\t\tthis,\n\t\tstd::move(dragEnterFilter),\n\t\tnullptr,\n\t\tnullptr,\n\t\t[](const QMimeData *d) { return Storage::MimeDataState::Image; },\n\t\ttrue);\n\n\tareas.photo->setDroppedCallback([=](const QMimeData *data) {\n\t\t_paint->handleMimeData(data);\n\t});\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/photo_editor_content.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\n#include \"editor/photo_editor_common.h\"\n#include \"editor/photo_editor_inner_common.h\"\n#include \"ui/image/image.h\"\n\nnamespace Editor {\n\nclass Crop;\nclass Paint;\nstruct Controllers;\n\nclass PhotoEditorContent final : public Ui::RpWidget {\npublic:\n\tPhotoEditorContent(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<Image> photo,\n\t\tPhotoModifications modifications,\n\t\tstd::shared_ptr<Controllers> controllers,\n\t\tEditorData data);\n\n\tvoid applyModifications(PhotoModifications modifications);\n\tvoid applyMode(const PhotoEditorMode &mode);\n\tvoid applyBrush(const Brush &brush);\n\tvoid applyAspectRatio(float64 ratio);\n\tvoid save(PhotoModifications &modifications);\n\n\tbool handleKeyPress(not_null<QKeyEvent*> e) const;\n\n\tvoid setupDragArea();\n\n\t[[nodiscard]] rpl::producer<QRect> innerRect() const {\n\t\treturn _innerRect.value();\n\t}\n\nprivate:\n\n\tconst QSize _photoSize;\n\tconst base::unique_qptr<Paint> _paint;\n\tconst base::unique_qptr<Crop> _crop;\n\tconst std::shared_ptr<Image> _photo;\n\n\trpl::variable<QRect> _innerRect;\n\trpl::variable<PhotoModifications> _modifications;\n\trpl::event_stream<int> _keyPresses;\n\n\tQRect _imageRect;\n\tQTransform _imageMatrix;\n\tPhotoEditorMode _mode;\n\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/photo_editor_controls.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/photo_editor_controls.h\"\n\n#include \"editor/controllers/controllers.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"styles/style_editor.h\"\n#include \"styles/style_media_player.h\" // mediaPlayerMenuCheck\n\n#include <QRegion>\n\nnamespace Editor {\n\nclass EdgeButton final : public Ui::RippleButton {\npublic:\n\tEdgeButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst QString &text,\n\t\tint height,\n\t\tconst style::color &bg,\n\t\tconst style::color &fg,\n\t\tconst style::RippleAnimation &st);\n\nprotected:\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\nprivate:\n\tvoid init();\n\n\tconst style::color &_fg;\n\tUi::Text::String _text;\n\tconst int _width;\n\tconst QRect _rippleRect;\n\tconst QColor _bg;\n\n\tQImage rounded(std::optional<QColor> color) const;\n\n};\n\nEdgeButton::EdgeButton(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst QString &text,\n\tint height,\n\tconst style::color &bg,\n\tconst style::color &fg,\n\tconst style::RippleAnimation &st)\n: Ui::RippleButton(parent, st)\n, _fg(fg)\n, _text(st::photoEditorButtonStyle, text)\n, _width(_text.maxWidth()\n\t+ st::photoEditorTextButtonPadding.left()\n\t+ st::photoEditorTextButtonPadding.right())\n, _rippleRect(QRect(\n\trect::m::pos::tl(st::photoEditorEdgeButtonMargins),\n\tQSize(\n\t\t_width,\n\t\theight - rect::m::sum::v(st::photoEditorEdgeButtonMargins))))\n, _bg(bg->c) {\n\tresize(\n\t\t_width + rect::m::sum::h(st::photoEditorEdgeButtonMargins),\n\t\theight);\n\tinit();\n}\n\nvoid EdgeButton::init() {\n\tconst auto bg = rounded(_bg);\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(this);\n\n\t\tif (isOver()) {\n\t\t\tp.drawImage(_rippleRect.topLeft(), bg);\n\t\t}\n\n\t\tpaintRipple(p, _rippleRect.x(), _rippleRect.y());\n\n\t\tp.setPen(_fg);\n\t\tconst auto textTop = _rippleRect.y()\n\t\t\t+ (_rippleRect.height() - _text.minHeight()) / 2;\n\t\t_text.draw(\n\t\t\tp,\n\t\t\t_rippleRect.x(),\n\t\t\ttextTop,\n\t\t\t_rippleRect.width(),\n\t\t\tstyle::al_center);\n\t}, lifetime());\n}\n\nQImage EdgeButton::rounded(std::optional<QColor> color) const {\n\tauto result = QImage(\n\t\t_rippleRect.size() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult.fill(color.value_or(Qt::white));\n\n\tconst auto radius = std::min(_rippleRect.width(), _rippleRect.height())\n\t\t/ 2;\n\tconst auto mask = Images::CornersMask(radius);\n\treturn Images::Round(std::move(result), mask);\n}\n\nQImage EdgeButton::prepareRippleMask() const {\n\treturn rounded(std::nullopt);\n}\n\nQPoint EdgeButton::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos()) - _rippleRect.topLeft();\n}\n\nclass ButtonBar final : public Ui::RpWidget {\npublic:\n\tButtonBar(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::color &bg);\n\nprivate:\n\tQImage _roundedBg;\n\n};\n\nButtonBar::ButtonBar(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst style::color &bg)\n: RpWidget(parent) {\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\tconst auto children = RpWidget::children();\n\t\tconst auto widgets = ranges::views::all(\n\t\t\tchildren\n\t\t) | ranges::views::filter([](not_null<const QObject*> object) {\n\t\t\treturn object->isWidgetType();\n\t\t}) | ranges::views::transform([](not_null<QObject*> object) {\n\t\t\treturn static_cast<QWidget*>(object.get());\n\t\t}) | ranges::to_vector;\n\t\tif (widgets.size() < 2) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto layout = [&](bool symmetrical) {\n\t\t\tauto widths = widgets | ranges::views::transform(\n\t\t\t\t&QWidget::width\n\t\t\t) | ranges::to_vector;\n\t\t\tconst auto count = int(widths.size());\n\t\t\tconst auto middle = count / 2;\n\t\t\tif (symmetrical) {\n\t\t\t\tfor (auto i = 0; i != middle; ++i) {\n\t\t\t\t\tconst auto j = count - i - 1;\n\t\t\t\t\twidths[i] = widths[j] = std::max(widths[i], widths[j]);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto residualWidth = size.width()\n\t\t\t\t- ranges::accumulate(widths, 0);\n\t\t\tif (symmetrical && residualWidth < 0) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto step = residualWidth / float(count - 1);\n\n\t\t\tauto left = 0.;\n\t\t\tauto &&ints = ranges::views::ints(0, ranges::unreachable);\n\t\t\tauto &&list = ranges::views::zip(widgets, widths, ints);\n\t\t\tfor (const auto &[widget, width, index] : list) {\n\t\t\t\twidget->move(int((index >= middle)\n\t\t\t\t\t? (left + width - widget->width())\n\t\t\t\t\t: left), 0);\n\t\t\t\tleft += width + step;\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\t\tif (!layout(true)) {\n\t\t\tlayout(false);\n\t\t}\n\n\t\tauto result = QImage(\n\t\t\tsize * style::DevicePixelRatio(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\t\tresult.fill(bg->c);\n\n\t\tconst auto radius = std::min(size.width(), size.height()) / 2;\n\t\tconst auto mask = Images::CornersMask(radius);\n\t\t_roundedBg = Images::Round(std::move(result), mask);\n\t}, lifetime());\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(this);\n\t\tp.drawImage(QPoint(), _roundedBg);\n\t}, lifetime());\n}\n\nPhotoEditorControls::PhotoEditorControls(\n\tnot_null<Ui::RpWidget*> parent,\n\tstd::shared_ptr<Controllers> controllers,\n\tconst PhotoModifications modifications,\n\tconst EditorData &data,\n\tconst QSize &imageSize)\n: RpWidget(parent)\n, _imageSize(imageSize)\n, _bg(st::roundedBg)\n, _buttonHeight(st::photoEditorButtonBarHeight)\n, _transformButtons(base::make_unique_q<ButtonBar>(this, _bg))\n, _paintTopButtons(base::make_unique_q<ButtonBar>(this, _bg))\n, _paintBottomButtons(base::make_unique_q<ButtonBar>(this, _bg))\n, _about(data.about.empty()\n\t? nullptr\n\t: base::make_unique_q<Ui::FadeWrap<Ui::FlatLabel>>(\n\t\tthis,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\trpl::single(data.about),\n\t\t\tst::photoEditorAbout)))\n, _transformCancel(base::make_unique_q<EdgeButton>(\n\t_transformButtons,\n\ttr::lng_cancel(tr::now),\n\t_buttonHeight,\n\tst::photoEditorEdgeButtonBg,\n\tst::mediaviewCaptionFg,\n\tst::photoEditorRotateButton.ripple))\n, _flipButton(base::make_unique_q<Ui::IconButton>(\n\t_transformButtons,\n\tst::photoEditorFlipButton))\n, _rotateButton(base::make_unique_q<Ui::IconButton>(\n\t_transformButtons,\n\tst::photoEditorRotateButton))\n, _paintModeButton(base::make_unique_q<Ui::IconButton>(\n\t_transformButtons,\n\tst::photoEditorPaintModeButton))\n, _cropRatioButton(data.keepAspectRatio\n\t? nullptr\n\t: base::make_unique_q<Ui::IconButton>(\n\t\t_transformButtons,\n\t\tst::photoEditorCropRatioButton))\n, _transformDone(base::make_unique_q<EdgeButton>(\n\t_transformButtons,\n\t(data.confirm.isEmpty() ? tr::lng_box_done(tr::now) : data.confirm),\n\t_buttonHeight,\n\tst::photoEditorEdgeButtonBg,\n\tst::mediaviewTextLinkFg,\n\tst::photoEditorRotateButton.ripple))\n, _paintCancel(base::make_unique_q<EdgeButton>(\n\t_paintBottomButtons,\n\ttr::lng_cancel(tr::now),\n\t_buttonHeight,\n\tst::photoEditorEdgeButtonBg,\n\tst::mediaviewCaptionFg,\n\tst::photoEditorRotateButton.ripple))\n, _undoButton(base::make_unique_q<Ui::IconButton>(\n\t_paintTopButtons,\n\tst::photoEditorUndoButton))\n, _redoButton(base::make_unique_q<Ui::IconButton>(\n\t_paintTopButtons,\n\tst::photoEditorRedoButton))\n, _paintModeButtonActive(base::make_unique_q<Ui::IconButton>(\n\t_paintBottomButtons,\n\tst::photoEditorPaintModeButton))\n, _stickersButton(controllers->stickersPanelController\n\t\t? base::make_unique_q<Ui::IconButton>(\n\t\t\t_paintBottomButtons,\n\t\t\tst::photoEditorStickersButton)\n\t\t: nullptr)\n, _paintDone(base::make_unique_q<EdgeButton>(\n\t_paintBottomButtons,\n\ttr::lng_box_done(tr::now),\n\t_buttonHeight,\n\tst::photoEditorEdgeButtonBg,\n\tst::mediaviewTextLinkFg,\n\tst::photoEditorRotateButton.ripple)) {\n\n\t{\n\t\tconst auto icon = &st::photoEditorPaintIconActive;\n\t\t_paintModeButtonActive->setIconOverride(icon, icon);\n\t}\n\t_paintModeButtonActive->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\tif (size.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto &padding = st::photoEditorButtonBarPadding;\n\t\tconst auto w = std::min(st::photoEditorButtonBarWidth, size.width())\n\t\t\t- padding.left()\n\t\t\t- padding.right();\n\t\t_transformButtons->resize(w, _buttonHeight);\n\t\t_paintBottomButtons->resize(w, _buttonHeight);\n\t\t_paintTopButtons->resize(w, _buttonHeight);\n\n\t\tconst auto buttonsTop = bottomButtonsTop();\n\n\t\tconst auto &current = _transformButtons->isHidden()\n\t\t\t? _paintBottomButtons\n\t\t\t: _transformButtons;\n\n\t\tcurrent->moveToLeft(\n\t\t\t(size.width() - current->width()) / 2,\n\t\t\tbuttonsTop);\n\n\t\tif (_about) {\n\t\t\tconst auto &margin = st::photoEditorAboutMargin;\n\t\t\tconst auto skip = st::photoEditorCropPointSize;\n\t\t\t_about->resizeToWidth(\n\t\t\t\tsize.width() - margin.left() - margin.right());\n\t\t\t_about->moveToLeft(\n\t\t\t\t(size.width() - _about->width()) / 2,\n\t\t\t\tmargin.top() - skip);\n\t\t}\n\t}, lifetime());\n\n\t_mode.changes(\n\t) | rpl::on_next([=](const PhotoEditorMode &mode) {\n\t\tif (mode.mode == PhotoEditorMode::Mode::Out) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto animated = (_paintBottomButtons->isVisible()\n\t\t\t\t== _transformButtons->isVisible())\n\t\t\t? anim::type::instant\n\t\t\t: anim::type::normal;\n\t\tshowAnimated(mode.mode, animated);\n\t}, lifetime());\n\n\t_paintBottomButtons->positionValue(\n\t) | rpl::on_next([=](const QPoint &containerPos) {\n\t\t_paintTopButtons->moveToLeft(\n\t\t\tcontainerPos.x(),\n\t\t\tcontainerPos.y()\n\t\t\t\t- st::photoEditorControlsCenterSkip\n\t\t\t\t- _paintTopButtons->height());\n\t}, _paintBottomButtons->lifetime());\n\n\t_paintBottomButtons->shownValue(\n\t) | rpl::on_next([=](bool shown) {\n\t\t_paintTopButtons->setVisible(shown);\n\t}, _paintBottomButtons->lifetime());\n\n\tauto aboutChanges = _about\n\t\t? rpl::merge(\n\t\t\t_about->geometryValue() | rpl::to_empty,\n\t\t\t_about->shownValue() | rpl::to_empty)\n\t\t: rpl::never<>() | rpl::type_erased;\n\trpl::merge(\n\t\tgeometryValue() | rpl::to_empty,\n\t\t_transformButtons->geometryValue() | rpl::to_empty,\n\t\t_transformButtons->shownValue() | rpl::to_empty,\n\t\t_paintBottomButtons->geometryValue() | rpl::to_empty,\n\t\t_paintBottomButtons->shownValue() | rpl::to_empty,\n\t\t_paintTopButtons->geometryValue() | rpl::to_empty,\n\t\t_paintTopButtons->shownValue() | rpl::to_empty,\n\t\tstd::move(aboutChanges)\n\t) | rpl::on_next([=] {\n\t\tupdateInputMask();\n\t}, lifetime());\n\n\tcontrollers->undoController->setPerformRequestChanges(rpl::merge(\n\t\t_undoButton->clicks() | rpl::map_to(Undo::Undo),\n\t\t_redoButton->clicks() | rpl::map_to(Undo::Redo),\n\t\t_keyPresses.events(\n\t\t) | rpl::filter([=](not_null<QKeyEvent*> e) {\n\t\t\tusing Mode = PhotoEditorMode::Mode;\n\t\t\treturn (e->matches(QKeySequence::Undo)\n\t\t\t\t\t&& !_undoButton->isHidden()\n\t\t\t\t\t&& !_undoButton->testAttribute(\n\t\t\t\t\t\tQt::WA_TransparentForMouseEvents)\n\t\t\t\t\t&& (_mode.current().mode == Mode::Paint))\n\t\t\t\t|| (e->matches(QKeySequence::Redo)\n\t\t\t\t\t&& !_redoButton->isHidden()\n\t\t\t\t\t&& !_redoButton->testAttribute(\n\t\t\t\t\t\tQt::WA_TransparentForMouseEvents)\n\t\t\t\t\t&& (_mode.current().mode == Mode::Paint));\n\t\t}) | rpl::map([=](not_null<QKeyEvent*> e) {\n\t\t\treturn e->matches(QKeySequence::Undo) ? Undo::Undo : Undo::Redo;\n\t\t})));\n\n\tcontrollers->undoController->canPerformChanges(\n\t) | rpl::on_next([=](const UndoController::EnableRequest &r) {\n\t\tconst auto isUndo = (r.command == Undo::Undo);\n\t\tconst auto &button = isUndo ? _undoButton : _redoButton;\n\t\tbutton->setAttribute(Qt::WA_TransparentForMouseEvents, !r.enable);\n\t\tif (!r.enable) {\n\t\t\tbutton->clearState();\n\t\t}\n\n\t\tbutton->setIconOverride(r.enable\n\t\t\t? nullptr\n\t\t\t: isUndo\n\t\t\t? &st::photoEditorUndoButtonInactive\n\t\t\t: &st::photoEditorRedoButtonInactive);\n\t}, lifetime());\n\n\tif (_stickersButton) {\n\t\tusing ShowRequest = StickersPanelController::ShowRequest;\n\n\t\tcontrollers->stickersPanelController->setShowRequestChanges(\n\t\t\trpl::merge(\n\t\t\t\t_mode.value(\n\t\t\t\t) | rpl::map_to(ShowRequest::HideFast),\n\t\t\t\t_stickersButton->clicks(\n\t\t\t\t) | rpl::map_to(ShowRequest::ToggleAnimated)\n\t\t\t));\n\n\t\tcontrollers->stickersPanelController->setMoveRequestChanges(\n\t\t\t_paintBottomButtons->positionValue(\n\t\t\t) | rpl::map([=](const QPoint &containerPos) {\n\t\t\t\treturn QPoint(\n\t\t\t\t\t(x() + width()) / 2,\n\t\t\t\t\ty() + containerPos.y() + _stickersButton->y());\n\t\t\t}));\n\n\t\tcontrollers->stickersPanelController->panelShown(\n\t\t) | rpl::on_next([=](bool shown) {\n\t\t\tconst auto icon = shown\n\t\t\t\t? &st::photoEditorStickersIconActive\n\t\t\t\t: nullptr;\n\t\t\t_stickersButton->setIconOverride(icon, icon);\n\t\t}, _stickersButton->lifetime());\n\t}\n\n\trpl::single(rpl::empty) | rpl::skip(\n\t\tmodifications.flipped ? 0 : 1\n\t) | rpl::then(\n\t\t_flipButton->clicks() | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\t_flipped = !_flipped;\n\t\tconst auto icon = _flipped ? &st::photoEditorFlipIconActive : nullptr;\n\t\t_flipButton->setIconOverride(icon, icon);\n\t}, _flipButton->lifetime());\n\n\tif (_cropRatioButton) {\n\t\tconst auto imageRatio = float64(\n\t\t\t_imageSize.width()) / _imageSize.height();\n\t\tconst auto ratiosMatch = [](float64 a, float64 b) {\n\t\t\treturn std::abs(a - b) < 0.01;\n\t\t};\n\t\t_cropRatioButton->setClickedCallback([=] {\n\t\t\t_ratioMenu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\t_cropRatioButton.get(),\n\t\t\t\tst::photoEditorCropRatioMenu);\n\t\t\t_ratioMenu->setForcedOrigin(\n\t\t\t\tUi::PanelAnimation::Origin::BottomRight);\n\t\t\tconst auto check = &st::mediaPlayerMenuCheck;\n\t\t\tconst auto add = [&](const QString &text, float64 ratio) {\n\t\t\t\tconst auto selected = ratiosMatch(_currentRatio, ratio);\n\t\t\t\t_ratioMenu->addAction(\n\t\t\t\t\ttext,\n\t\t\t\t\t[=] {\n\t\t\t\t\t\tif (ratiosMatch(_currentRatio, ratio)) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t_currentRatio = ratio;\n\t\t\t\t\t\t_aspectRatioChanges.fire_copy(ratio);\n\t\t\t\t\t\tconst auto locked = (ratio > 0.);\n\t\t\t\t\t\tconst auto icon = locked\n\t\t\t\t\t\t\t? &st::photoEditorCropRatioIconActive\n\t\t\t\t\t\t\t: nullptr;\n\t\t\t\t\t\t_cropRatioButton->setIconOverride(icon, icon);\n\t\t\t\t\t},\n\t\t\t\t\tselected ? check : nullptr);\n\t\t\t};\n\t\t\tadd(tr::lng_photo_editor_crop_original(tr::now), imageRatio);\n\t\t\tadd(tr::lng_photo_editor_crop_square(tr::now), 1.);\n\t\t\tadd(u\"3:2\"_q, 3. / 2.);\n\t\t\tadd(u\"16:9\"_q, 16. / 9.);\n\t\t\tadd(u\"9:16\"_q, 9. / 16.);\n\t\t\tadd(tr::lng_photo_editor_crop_free(tr::now), 0.);\n\t\t\tconst auto button = _cropRatioButton.get();\n\t\t\tconst auto bottomRight = button->mapToGlobal(\n\t\t\t\tQPoint(button->width(), 0));\n\t\t\t_ratioMenu->popup(bottomRight);\n\t\t});\n\t}\n\n\tupdateInputMask();\n\n}\n\nrpl::producer<int> PhotoEditorControls::rotateRequests() const {\n\treturn _rotateButton->clicks() | rpl::map_to(90);\n}\n\nrpl::producer<> PhotoEditorControls::flipRequests() const {\n\treturn _flipButton->clicks() | rpl::to_empty;\n}\n\nrpl::producer<> PhotoEditorControls::paintModeRequests() const {\n\treturn _paintModeButton->clicks() | rpl::to_empty;\n}\n\nrpl::producer<> PhotoEditorControls::doneRequests() const {\n\treturn rpl::merge(\n\t\t_transformDone->clicks() | rpl::to_empty,\n\t\t_paintDone->clicks() | rpl::to_empty,\n\t\t_keyPresses.events(\n\t\t) | rpl::filter([=](not_null<QKeyEvent*> e) {\n\t\t\tconst auto key = e->key();\n\t\t\treturn ((key == Qt::Key_Enter) || (key == Qt::Key_Return))\n\t\t\t\t&& !_toggledBarAnimation.animating();\n\t\t}) | rpl::to_empty);\n}\n\nrpl::producer<> PhotoEditorControls::cancelRequests() const {\n\treturn rpl::merge(\n\t\t_transformCancel->clicks() | rpl::to_empty,\n\t\t_paintCancel->clicks() | rpl::to_empty,\n\t\t_keyPresses.events(\n\t) | rpl::filter([=](not_null<QKeyEvent*> e) {\n\t\tconst auto key = e->key();\n\t\treturn (key == Qt::Key_Escape)\n\t\t\t&& !_toggledBarAnimation.animating();\n\t}) | rpl::to_empty);\n}\n\nrpl::producer<float64> PhotoEditorControls::aspectRatioChanges() const {\n\treturn _aspectRatioChanges.events();\n}\n\nint PhotoEditorControls::bottomButtonsTop() const {\n\treturn height()\n\t\t- st::photoEditorControlsBottomSkip\n\t\t- _transformButtons->height();\n}\n\nvoid PhotoEditorControls::showAnimated(\n\t\tPhotoEditorMode::Mode mode,\n\t\tanim::type animated) {\n\tusing Mode = PhotoEditorMode::Mode;\n\n\tconst auto duration = st::photoEditorBarAnimationDuration;\n\n\tconst auto isTransform = (mode == Mode::Transform);\n\tif (_about) {\n\t\t_about->toggle(isTransform, animated);\n\t}\n\n\tconst auto buttonsLeft = (width() - _transformButtons->width()) / 2;\n\tconst auto buttonsTop = bottomButtonsTop();\n\n\tconst auto visibleBar = _transformButtons->isVisible()\n\t\t? _transformButtons.get()\n\t\t: _paintBottomButtons.get();\n\n\tconst auto shouldVisibleBar = isTransform\n\t\t? _transformButtons.get()\n\t\t: _paintBottomButtons.get(); // Mode::Paint\n\n\tconst auto computeTop = [=](float64 progress) {\n\t\treturn anim::interpolate(buttonsTop, height() * 2, progress);\n\t};\n\n\tconst auto showShouldVisibleBar = [=] {\n\t\t_toggledBarAnimation.stop();\n\t\tauto callback = [=](float64 value) {\n\t\t\tshouldVisibleBar->moveToLeft(buttonsLeft, computeTop(value));\n\t\t};\n\t\tif (animated == anim::type::instant) {\n\t\t\tcallback(1.);\n\t\t} else {\n\t\t\t_toggledBarAnimation.start(\n\t\t\t\tstd::move(callback),\n\t\t\t\t1.,\n\t\t\t\t0.,\n\t\t\t\tduration);\n\t\t}\n\t};\n\n\tauto animationCallback = [=](float64 value) {\n\t\tif (shouldVisibleBar == visibleBar) {\n\t\t\tshowShouldVisibleBar();\n\t\t\treturn;\n\t\t}\n\t\tvisibleBar->moveToLeft(buttonsLeft, computeTop(value));\n\n\t\tif (value == 1.) {\n\t\t\tshouldVisibleBar->show();\n\t\t\tshouldVisibleBar->moveToLeft(buttonsLeft, computeTop(1.));\n\t\t\tvisibleBar->hide();\n\n\t\t\tshowShouldVisibleBar();\n\t\t}\n\t};\n\n\tif (animated == anim::type::instant) {\n\t\tanimationCallback(1.);\n\t} else {\n\t\t_toggledBarAnimation.start(\n\t\t\tstd::move(animationCallback),\n\t\t\t0.,\n\t\t\t1.,\n\t\t\tduration);\n\t}\n}\n\nvoid PhotoEditorControls::updateInputMask() {\n\tauto region = QRegion();\n\tconst auto visibleRect = rect();\n\tconst auto add = [&](not_null<const QWidget*> widget) {\n\t\tif (!widget->isHidden()) {\n\t\t\tconst auto geometry = widget->geometry() & visibleRect;\n\t\t\tif (!geometry.isEmpty()) {\n\t\t\t\tregion += geometry;\n\t\t\t}\n\t\t}\n\t};\n\tadd(_transformButtons);\n\tadd(_paintBottomButtons);\n\tadd(_paintTopButtons);\n\tif (_about && !_about->isHidden()) {\n\t\tconst auto geometry = _about->geometry() & visibleRect;\n\t\tif (!geometry.isEmpty()) {\n\t\t\tregion += geometry;\n\t\t}\n\t}\n\tif (region.isEmpty()) {\n\t\tclearMask();\n\t} else {\n\t\tsetMask(region);\n\t}\n}\n\nvoid PhotoEditorControls::applyMode(const PhotoEditorMode &mode) {\n\t_mode = mode;\n}\n\nrpl::producer<QPoint> PhotoEditorControls::colorLinePositionValue() const {\n\treturn rpl::merge(\n\t\tgeometryValue() | rpl::to_empty,\n\t\t_paintTopButtons->geometryValue() | rpl::to_empty\n\t) | rpl::map([=] {\n\t\tconst auto r = _paintTopButtons->geometry();\n\t\treturn mapToParent(r.topLeft())\n\t\t\t+ QPoint(r.width() / 2, r.height() / 2);\n\t});\n}\n\nrpl::producer<bool> PhotoEditorControls::colorLineShownValue() const {\n\treturn _paintTopButtons->shownValue();\n}\n\nbool PhotoEditorControls::handleKeyPress(not_null<QKeyEvent*> e) const {\n\t_keyPresses.fire(std::move(e));\n\treturn true;\n}\n\nbool PhotoEditorControls::animating() const {\n\treturn _toggledBarAnimation.animating();\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/photo_editor_controls.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\n#include \"base/unique_qptr.h\"\n#include \"ui/effects/animations.h\"\n#include \"editor/photo_editor_common.h\"\n#include \"editor/photo_editor_inner_common.h\"\n\nnamespace Ui {\nclass IconButton;\nclass FlatLabel;\nclass PopupMenu;\ntemplate <typename Widget>\nclass FadeWrap;\n} // namespace Ui\n\nnamespace Editor {\n\nclass EdgeButton;\nclass ButtonBar;\nstruct Controllers;\nstruct EditorData;\n\nclass PhotoEditorControls final : public Ui::RpWidget {\npublic:\n\tPhotoEditorControls(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<Controllers> controllers,\n\t\tconst PhotoModifications modifications,\n\t\tconst EditorData &data,\n\t\tconst QSize &imageSize);\n\n\t[[nodiscard]] rpl::producer<int> rotateRequests() const;\n\t[[nodiscard]] rpl::producer<> flipRequests() const;\n\t[[nodiscard]] rpl::producer<> paintModeRequests() const;\n\t[[nodiscard]] rpl::producer<> doneRequests() const;\n\t[[nodiscard]] rpl::producer<> cancelRequests() const;\n\t[[nodiscard]] rpl::producer<QPoint> colorLinePositionValue() const;\n\t[[nodiscard]] rpl::producer<bool> colorLineShownValue() const;\n\t[[nodiscard]] rpl::producer<float64> aspectRatioChanges() const;\n\n\t[[nodiscard]] bool animating() const;\n\n\tbool handleKeyPress(not_null<QKeyEvent*> e) const;\n\n\tvoid applyMode(const PhotoEditorMode &mode);\n\nprivate:\n\tvoid showAnimated(\n\t\tPhotoEditorMode::Mode mode,\n\t\tanim::type animated = anim::type::normal);\n\tvoid updateInputMask();\n\n\tint bottomButtonsTop() const;\n\n\tconst QSize _imageSize;\n\tconst style::color &_bg;\n\tconst int _buttonHeight;\n\tconst base::unique_qptr<ButtonBar> _transformButtons;\n\tconst base::unique_qptr<ButtonBar> _paintTopButtons;\n\tconst base::unique_qptr<ButtonBar> _paintBottomButtons;\n\n\tconst base::unique_qptr<Ui::FadeWrap<Ui::FlatLabel>> _about;\n\n\tconst base::unique_qptr<EdgeButton> _transformCancel;\n\tconst base::unique_qptr<Ui::IconButton> _flipButton;\n\tconst base::unique_qptr<Ui::IconButton> _rotateButton;\n\tconst base::unique_qptr<Ui::IconButton> _paintModeButton;\n\tconst base::unique_qptr<Ui::IconButton> _cropRatioButton;\n\tconst base::unique_qptr<EdgeButton> _transformDone;\n\n\tconst base::unique_qptr<EdgeButton> _paintCancel;\n\tconst base::unique_qptr<Ui::IconButton> _undoButton;\n\tconst base::unique_qptr<Ui::IconButton> _redoButton;\n\tconst base::unique_qptr<Ui::IconButton> _paintModeButtonActive;\n\tconst base::unique_qptr<Ui::IconButton> _stickersButton;\n\tconst base::unique_qptr<EdgeButton> _paintDone;\n\n\tbase::unique_qptr<Ui::PopupMenu> _ratioMenu;\n\tfloat64 _currentRatio = 0.;\n\n\tbool _flipped = false;\n\n\tUi::Animations::Simple _toggledBarAnimation;\n\n\trpl::variable<PhotoEditorMode> _mode;\n\trpl::event_stream<not_null<QKeyEvent*>> _keyPresses;\n\trpl::event_stream<float64> _aspectRatioChanges;\n\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/photo_editor_inner_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Editor {\n\nclass Scene;\n\nstruct PhotoEditorMode {\n\tenum class Mode {\n\t\tTransform,\n\t\tPaint,\n\t\tOut,\n\t} mode = Mode::Transform;\n\n\tenum class Action {\n\t\tNone,\n\t\tSave,\n\t\tDiscard,\n\t} action = Action::None;\n};\n\nstruct Brush {\n\tenum class Tool : uchar {\n\t\tPen,\n\t\tArrow,\n\t\tMarker,\n\t\tEraser,\n\t\tBlur,\n\t};\n\tfloat64 sizeRatio = 0.;\n\tQColor color;\n\tTool tool = Tool::Pen;\n};\n\nenum class SaveState {\n\tSave,\n\tKeep,\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/photo_editor_layer_widget.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/boxes/confirm_box.h\" // InformBox\n#include \"editor/editor_layer_widget.h\"\n#include \"editor/photo_editor.h\"\n#include \"storage/localimageloader.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n\n#include <QtGui/QGuiApplication>\n\nnamespace Editor {\n\nvoid OpenWithPreparedFile(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<Ui::PreparedFile*> file,\n\t\tint previewWidth,\n\t\tFn<void(bool ok)> &&doneCallback,\n\t\tint sideLimit,\n\t\tQSize exactSize) {\n\tusing ImageInfo = Ui::PreparedFileInformation::Image;\n\tconst auto image = std::get_if<ImageInfo>(&file->information->media);\n\tif (!image) {\n\t\tdoneCallback(false);\n\t\treturn;\n\t}\n\tconst auto photoType = (file->type == Ui::PreparedFile::Type::Photo);\n\tconst auto modifiedFileType = (file->type == Ui::PreparedFile::Type::File)\n\t\t&& !image->modifications.empty();\n\tif (!photoType && !modifiedFileType) {\n\t\tdoneCallback(false);\n\t\treturn;\n\t}\n\n\tsideLimit = sideLimit ? sideLimit : PhotoSideLimit(true);\n\tconst auto accepted = std::make_shared<bool>();\n\tauto callback = [=](const PhotoModifications &mods) {\n\t\t*accepted = true;\n\t\timage->modifications = mods;\n\t\tStorage::UpdateImageDetails(*file, previewWidth, sideLimit);\n\t\t{\n\t\t\tusing namespace Ui;\n\t\t\tconst auto size = file->preview.size();\n\t\t\tfile->type = ValidateThumbDimensions(size.width(), size.height())\n\t\t\t\t? PreparedFile::Type::Photo\n\t\t\t\t: PreparedFile::Type::File;\n\t\t}\n\t\tdoneCallback(true);\n\t};\n\tauto copy = image->data;\n\tconst auto fileImage = std::make_shared<Image>(std::move(copy));\n\tconst auto keepRatio = !exactSize.isEmpty();\n\tauto editor = base::make_unique_q<PhotoEditor>(\n\t\tparent,\n\t\tshow,\n\t\tshow,\n\t\tfileImage,\n\t\timage->modifications,\n\t\tEditorData{ .exactSize = exactSize, .keepAspectRatio = keepRatio });\n\tconst auto raw = editor.get();\n\tauto layer = std::make_unique<LayerWidget>(parent, std::move(editor));\n\tInitEditorLayer(layer.get(), raw, std::move(callback));\n\tQObject::connect(layer.get(), &QObject::destroyed, [=] {\n\t\tif (!*accepted) {\n\t\t\tdoneCallback(false);\n\t\t}\n\t});\n\tshow->showLayer(std::move(layer), Ui::LayerOption::KeepOther);\n}\n\nvoid PrepareProfilePhoto(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::Controller*> controller,\n\t\tEditorData data,\n\t\tFn<void(QImage &&image)> &&doneCallback,\n\t\tQImage &&image) {\n\tconst auto resizeToMinSize = [=](\n\t\t\tQImage &&image,\n\t\t\tQt::AspectRatioMode mode) {\n\t\tconst auto &minSize = kProfilePhotoSize;\n\t\tif ((image.width() < minSize) || (image.height() < minSize)) {\n\t\t\treturn image.scaled(\n\t\t\t\tminSize,\n\t\t\t\tminSize,\n\t\t\t\tmode,\n\t\t\t\tQt::SmoothTransformation);\n\t\t}\n\t\treturn std::move(image);\n\t};\n\n\tif (image.isNull()\n\t\t|| (image.width() > (10 * image.height()))\n\t\t|| (image.height() > (10 * image.width()))) {\n\t\tcontroller->show(Ui::MakeInformBox(tr::lng_bad_photo()));\n\t\treturn;\n\t}\n\timage = resizeToMinSize(\n\t\tstd::move(image),\n\t\tQt::KeepAspectRatioByExpanding);\n\tconst auto fileImage = std::make_shared<Image>(std::move(image));\n\n\tauto applyModifications = [=, done = std::move(doneCallback)](\n\t\t\tconst PhotoModifications &mods) {\n\t\tdone(resizeToMinSize(\n\t\t\tImageModified(fileImage->original(), mods),\n\t\t\tQt::KeepAspectRatio));\n\t};\n\n\tauto crop = [&] {\n\t\tconst auto &i = fileImage;\n\t\tconst auto minSide = std::min(i->width(), i->height());\n\t\treturn QRect(\n\t\t\t(i->width() - minSide) / 2,\n\t\t\t(i->height() - minSide) / 2,\n\t\t\tminSide,\n\t\t\tminSide);\n\t}();\n\n\tauto editor = base::make_unique_q<PhotoEditor>(\n\t\tparent,\n\t\tcontroller,\n\t\tfileImage,\n\t\tPhotoModifications{ .crop = std::move(crop) },\n\t\tdata);\n\tconst auto raw = editor.get();\n\tauto layer = std::make_unique<LayerWidget>(parent, std::move(editor));\n\tInitEditorLayer(layer.get(), raw, std::move(applyModifications));\n\tcontroller->showLayer(std::move(layer), Ui::LayerOption::KeepOther);\n}\n\nvoid PrepareProfilePhotoFromFile(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::Controller*> controller,\n\t\tEditorData data,\n\t\tFn<void(QImage &&image)> &&doneCallback) {\n\tconst auto callback = [=, done = std::move(doneCallback)](\n\t\t\tconst FileDialog::OpenResult &result) mutable {\n\t\tif (result.paths.isEmpty() && result.remoteContent.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tauto image = Images::Read({\n\t\t\t.path = result.paths.isEmpty() ? QString() : result.paths.front(),\n\t\t\t.content = result.remoteContent,\n\t\t\t.forceOpaque = true,\n\t\t}).image;\n\t\tPrepareProfilePhoto(\n\t\t\tparent,\n\t\t\tcontroller,\n\t\t\tdata,\n\t\t\tstd::move(done),\n\t\t\tstd::move(image));\n\t};\n\tFileDialog::GetOpenPath(\n\t\tparent.get(),\n\t\ttr::lng_choose_image(tr::now),\n\t\tFileDialog::ImagesOrAllFilter(),\n\t\tcrl::guard(parent, callback));\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/photo_editor_layer_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nenum class ImageRoundRadius;\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Ui {\nclass RpWidget;\nstruct PreparedFile;\n} // namespace Ui\n\nnamespace Window {\nclass Controller;\nclass SessionController;\n} // namespace Window\n\nnamespace Editor {\n\nconstexpr auto kProfilePhotoSize = int(640);\n\nstruct EditorData;\n\nvoid OpenWithPreparedFile(\n\tnot_null<QWidget*> parent,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<Ui::PreparedFile*> file,\n\tint previewWidth,\n\tFn<void(bool ok)> &&doneCallback,\n\tint sideLimit = 0,\n\tQSize exactSize = {});\n\nvoid PrepareProfilePhoto(\n\tnot_null<QWidget*> parent,\n\tnot_null<Window::Controller*> controller,\n\tEditorData data,\n\tFn<void(QImage &&image)> &&doneCallback,\n\tQImage &&image);\n\nvoid PrepareProfilePhotoFromFile(\n\tnot_null<QWidget*> parent,\n\tnot_null<Window::Controller*> controller,\n\tEditorData data,\n\tFn<void(QImage &&image)> &&doneCallback);\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/scene/scene.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/scene/scene.h\"\n\n#include \"editor/scene/scene_item_canvas.h\"\n#include \"editor/scene/scene_item_line.h\"\n#include \"editor/scene/scene_item_sticker.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/rp_widget.h\"\n#include \"styles/style_editor.h\"\n\n#include <QGraphicsSceneMouseEvent>\n\nnamespace Editor {\nnamespace {\n\nusing ItemPtr = std::shared_ptr<NumberedItem>;\n\nclass ItemEraser final : public NumberedItem {\npublic:\n\tstruct Target {\n\t\tstd::shared_ptr<ItemLine> item;\n\t\tQPixmap before;\n\t};\n\n\tItemEraser(\n\t\tQPixmap mask,\n\t\tQPointF maskPos,\n\t\tstd::vector<Target> targets)\n\t: _mask(std::move(mask))\n\t, _maskPos(maskPos)\n\t, _targets(std::move(targets)) {\n\t}\n\n\tvoid apply() {\n\t\tfor (const auto &target : _targets) {\n\t\t\ttarget.item->applyEraser(_mask, _maskPos);\n\t\t}\n\t}\n\n\tvoid revert() {\n\t\tfor (const auto &target : _targets) {\n\t\t\ttarget.item->setPixmap(target.before);\n\t\t}\n\t}\n\n\tQRectF boundingRect() const override {\n\t\treturn QRectF();\n\t}\n\n\tvoid paint(\n\t\t\tQPainter *,\n\t\t\tconst QStyleOptionGraphicsItem *,\n\t\t\tQWidget *) override {\n\t}\n\n\tbool hasState(SaveState state) const override {\n\t\tconst auto &saved = (state == SaveState::Keep) ? _keeped : _saved;\n\t\treturn saved.saved;\n\t}\n\n\tvoid save(SaveState state) override {\n\t\tauto &saved = (state == SaveState::Keep) ? _keeped : _saved;\n\t\tsaved = {\n\t\t\t.saved = true,\n\t\t\t.status = status(),\n\t\t};\n\t}\n\n\tvoid restore(SaveState state) override {\n\t\tif (!hasState(state)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto &saved = (state == SaveState::Keep) ? _keeped : _saved;\n\t\tsetStatus(saved.status);\n\t\tif (saved.status == Status::Normal) {\n\t\t\tapply();\n\t\t} else if (saved.status == Status::Undid) {\n\t\t\trevert();\n\t\t}\n\t}\n\nprivate:\n\tQPixmap _mask;\n\tQPointF _maskPos;\n\tstd::vector<Target> _targets;\n\n\tstruct {\n\t\tbool saved = false;\n\t\tNumberedItem::Status status;\n\t} _saved, _keeped;\n};\n\nbool SkipMouseEvent(not_null<QGraphicsSceneMouseEvent*> event) {\n\treturn event->isAccepted() || (event->button() == Qt::RightButton);\n}\n\n} // namespace\n\nScene::Scene(const QRectF &rect)\n: QGraphicsScene(rect)\n, _canvas(std::make_shared<ItemCanvas>())\n, _lastZ(std::make_shared<float64>(9000.)) {\n\tQGraphicsScene::addItem(_canvas.get());\n\t_canvas->clearPixmap();\n\n\t_canvas->grabContentRequests(\n\t) | rpl::on_next([=](ItemCanvas::Content &&content) {\n\t\tif (content.clear) {\n\t\t\tauto mask = std::move(content.pixmap);\n\t\t\tif (mask.isNull()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto maskPos = content.position;\n\t\t\tconst auto maskSize = mask.size()\n\t\t\t\t/ float64(mask.devicePixelRatio());\n\t\t\tconst auto maskRect = QRectF(maskPos, maskSize);\n\t\t\tauto targets = std::vector<ItemEraser::Target>();\n\t\t\tconst auto hits = QGraphicsScene::items(\n\t\t\t\tmaskRect,\n\t\t\t\tQt::IntersectsItemBoundingRect,\n\t\t\t\tQt::DescendingOrder);\n\t\t\tfor (auto *raw : hits) {\n\t\t\t\tconst auto it = _itemsByPointer.find(raw);\n\t\t\t\tif (it == end(_itemsByPointer)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto &item = it->second;\n\t\t\t\tif (!item->isNormalStatus()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto line = std::dynamic_pointer_cast<ItemLine>(item);\n\t\t\t\tif (!line) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tauto before = line->pixmap();\n\t\t\t\tif (!line->applyEraser(mask, maskPos)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\ttargets.push_back({\n\t\t\t\t\t.item = line,\n\t\t\t\t\t.before = std::move(before),\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (!targets.empty()) {\n\t\t\t\tconst auto eraser = std::make_shared<ItemEraser>(\n\t\t\t\t\tstd::move(mask),\n\t\t\t\t\tmaskPos,\n\t\t\t\t\tstd::move(targets));\n\t\t\t\taddItem(eraser);\n\t\t\t\t_canvas->setZValue(++_lastLineZ);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (content.blur) {\n\t\t\tauto mask = std::move(content.pixmap);\n\t\t\tif (mask.isNull() || !_blurSource) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto maskPos = content.position;\n\t\t\tconst auto maskSize = mask.size()\n\t\t\t\t/ float64(mask.devicePixelRatio());\n\t\t\tconst auto sourceRect = QRectF(maskPos, maskSize);\n\t\t\tconst auto expandedRect = sourceRect.toAlignedRect().adjusted(\n\t\t\t\t-st::photoEditorBlurRadius,\n\t\t\t\t-st::photoEditorBlurRadius,\n\t\t\t\tst::photoEditorBlurRadius,\n\t\t\t\tst::photoEditorBlurRadius);\n\t\t\tconst auto captureRect = expandedRect.intersected(\n\t\t\t\tsceneRect().toAlignedRect());\n\t\t\tif (captureRect.isEmpty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto source = _blurSource(captureRect);\n\t\t\tif (source.isNull()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto sourceDpr = source.devicePixelRatio();\n\t\t\tif (source.format() != QImage::Format_ARGB32_Premultiplied) {\n\t\t\t\tsource = source.convertToFormat(\n\t\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t\tsource.setDevicePixelRatio(sourceDpr);\n\t\t\t}\n\t\t\tconst auto canvasVisible = _canvas->isVisible();\n\t\t\t_canvas->setVisible(false);\n\t\t\t{\n\t\t\t\tauto p = QPainter(&source);\n\t\t\t\trender(\n\t\t\t\t\t&p,\n\t\t\t\t\tQRectF(QPointF(), QSizeF(captureRect.size())),\n\t\t\t\t\tQRectF(captureRect),\n\t\t\t\t\tQt::IgnoreAspectRatio);\n\t\t\t}\n\t\t\t_canvas->setVisible(canvasVisible);\n\t\t\tauto blurred = Images::BlurLargeImage(\n\t\t\t\tstd::move(source),\n\t\t\t\tst::photoEditorBlurRadius);\n\t\t\tif (blurred.isNull()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tblurred.setDevicePixelRatio(sourceDpr);\n\t\t\tauto result = QImage(\n\t\t\t\tmask.size(),\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tresult.setDevicePixelRatio(mask.devicePixelRatio());\n\t\t\tresult.fill(Qt::transparent);\n\t\t\t{\n\t\t\t\tauto p = QPainter(&result);\n\t\t\t\tp.drawImage(\n\t\t\t\t\tQRectF(QPointF(), maskSize),\n\t\t\t\t\tblurred,\n\t\t\t\t\tQRectF(\n\t\t\t\t\t\tsourceRect.x() - captureRect.x(),\n\t\t\t\t\t\tsourceRect.y() - captureRect.y(),\n\t\t\t\t\t\tsourceRect.width(),\n\t\t\t\t\t\tsourceRect.height()));\n\t\t\t\tp.setCompositionMode(\n\t\t\t\t\tQPainter::CompositionMode_DestinationIn);\n\t\t\t\tp.drawPixmap(0, 0, mask);\n\t\t\t}\n\t\t\tauto blurPixmap = QPixmap::fromImage(std::move(result));\n\t\t\tconst auto item = std::make_shared<ItemLine>(\n\t\t\t\tstd::move(blurPixmap));\n\t\t\titem->setPos(maskPos);\n\t\t\taddItem(item);\n\t\t\t_canvas->setZValue(++_lastLineZ);\n\t\t\treturn;\n\t\t}\n\t\tconst auto item = std::make_shared<ItemLine>(\n\t\t\tstd::move(content.pixmap));\n\t\titem->setPos(content.position);\n\t\taddItem(item);\n\t\t_canvas->setZValue(++_lastLineZ);\n\t}, _lifetime);\n}\n\nvoid Scene::cancelDrawing() {\n\t_canvas->cancelDrawing();\n}\n\nvoid Scene::addItem(ItemPtr item) {\n\tif (!item) {\n\t\treturn;\n\t}\n\titem->setNumber(_itemNumber++);\n\tQGraphicsScene::addItem(item.get());\n\tconst auto raw = item.get();\n\t_items.push_back(std::move(item));\n\t_itemsByPointer.emplace(raw, _items.back());\n\t_addsItem.fire({});\n}\n\nvoid Scene::removeItem(not_null<QGraphicsItem*> item) {\n\tconst auto it = ranges::find_if(_items, [&](const ItemPtr &i) {\n\t\treturn i.get() == item;\n\t});\n\tif (it == end(_items)) {\n\t\treturn;\n\t}\n\tremoveItem(*it);\n}\n\nvoid Scene::removeItem(const ItemPtr &item) {\n\titem->setStatus(NumberedItem::Status::Removed);\n\t_removesItem.fire({});\n}\n\nvoid Scene::mousePressEvent(QGraphicsSceneMouseEvent *event) {\n\tQGraphicsScene::mousePressEvent(event);\n\tif (SkipMouseEvent(event) || !sceneRect().contains(event->scenePos())) {\n\t\treturn;\n\t}\n\t_canvas->handleMousePressEvent(event);\n}\n\nvoid Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {\n\tQGraphicsScene::mouseReleaseEvent(event);\n\tif (SkipMouseEvent(event)) {\n\t\treturn;\n\t}\n\t_canvas->handleMouseReleaseEvent(event);\n}\n\nvoid Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {\n\tQGraphicsScene::mouseMoveEvent(event);\n\tif (SkipMouseEvent(event)) {\n\t\treturn;\n\t}\n\t_canvas->handleMouseMoveEvent(event);\n}\n\nvoid Scene::applyBrush(const QColor &color, float size, Brush::Tool tool) {\n\t_canvas->applyBrush(color, size, tool);\n}\n\nvoid Scene::setBlurSource(Fn<QImage(QRect)> source) {\n\t_blurSource = std::move(source);\n}\n\nrpl::producer<> Scene::addsItem() const {\n\treturn _addsItem.events();\n}\n\nrpl::producer<> Scene::removesItem() const {\n\treturn _removesItem.events();\n}\n\nstd::vector<ItemPtr> Scene::items(\n\t\tQt::SortOrder order) const {\n\tauto copyItems = _items;\n\n\tranges::sort(copyItems, [&](ItemPtr a, ItemPtr b) {\n\t\tconst auto numA = a->number();\n\t\tconst auto numB = b->number();\n\t\treturn (order == Qt::AscendingOrder) ? (numA < numB) : (numA > numB);\n\t});\n\n\treturn copyItems;\n}\n\nstd::shared_ptr<float64> Scene::lastZ() const {\n\treturn _lastZ;\n}\n\nvoid Scene::updateZoom(float64 zoom) {\n\t_canvas->updateZoom(zoom);\n\tfor (const auto &item : items()) {\n\t\tif (item->type() >= ItemBase::Type) {\n\t\t\tstatic_cast<ItemBase*>(item.get())->updateZoom(zoom);\n\t\t}\n\t}\n}\n\nbool Scene::hasUndo() const {\n\treturn ranges::any_of(_items, &NumberedItem::isNormalStatus);\n}\n\nbool Scene::hasRedo() const {\n\treturn ranges::any_of(_items, &NumberedItem::isUndidStatus);\n}\n\nvoid Scene::performUndo() {\n\tconst auto filtered = items(Qt::DescendingOrder);\n\n\tconst auto it = ranges::find_if(filtered, &NumberedItem::isNormalStatus);\n\tif (it != filtered.end()) {\n\t\tif (const auto eraser = dynamic_cast<ItemEraser*>(it->get())) {\n\t\t\teraser->revert();\n\t\t}\n\t\t(*it)->setStatus(NumberedItem::Status::Undid);\n\t}\n}\n\nvoid Scene::performRedo() {\n\tconst auto filtered = items(Qt::AscendingOrder);\n\n\tconst auto it = ranges::find_if(filtered, &NumberedItem::isUndidStatus);\n\tif (it != filtered.end()) {\n\t\tif (const auto eraser = dynamic_cast<ItemEraser*>(it->get())) {\n\t\t\teraser->apply();\n\t\t}\n\t\t(*it)->setStatus(NumberedItem::Status::Normal);\n\t}\n}\n\nvoid Scene::removeIf(Fn<bool(const ItemPtr &)> proj) {\n\tauto copy = std::vector<ItemPtr>();\n\tfor (const auto &item : _items) {\n\t\tconst auto toRemove = proj(item);\n\t\tif (toRemove) {\n\t\t\t// Scene loses ownership of an item.\n\t\t\t// It seems for some reason this line causes a crash. =(\n\t\t\t// QGraphicsScene::removeItem(item.get());\n\t\t} else {\n\t\t\tcopy.push_back(item);\n\t\t}\n\t}\n\t_items = std::move(copy);\n\t_itemsByPointer.clear();\n\tfor (const auto &item : _items) {\n\t\t_itemsByPointer.emplace(item.get(), item);\n\t}\n}\n\nvoid Scene::clearRedoList() {\n\tfor (const auto &item : _items) {\n\t\tif (item->isUndidStatus()) {\n\t\t\titem->setStatus(NumberedItem::Status::Removed);\n\t\t}\n\t}\n}\n\nvoid Scene::save(SaveState state) {\n\tremoveIf([](const ItemPtr &item) {\n\t\treturn item->isRemovedStatus()\n\t\t\t&& !item->hasState(SaveState::Keep)\n\t\t\t&& !item->hasState(SaveState::Save);\n\t});\n\n\tfor (const auto &item : _items) {\n\t\titem->save(state);\n\t}\n\tclearSelection();\n\tcancelDrawing();\n}\n\nvoid Scene::restore(SaveState state) {\n\tremoveIf([=](const ItemPtr &item) {\n\t\treturn !item->hasState(state);\n\t});\n\n\tfor (const auto &item : _items) {\n\t\titem->restore(state);\n\t}\n\tclearSelection();\n\tcancelDrawing();\n}\n\nScene::~Scene() {\n\t// Prevent destroying by scene of all items.\n\tQGraphicsScene::removeItem(_canvas.get());\n\tfor (const auto &item : items()) {\n\t\t// Scene loses ownership of an item.\n\t\tQGraphicsScene::removeItem(item.get());\n\t}\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/scene/scene.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <base/unique_qptr.h>\n#include <editor/photo_editor_inner_common.h>\n\n#include <QGraphicsScene>\n\nclass QGraphicsSceneMouseEvent;\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Editor {\n\nclass ItemCanvas;\nclass NumberedItem;\n\nclass Scene final : public QGraphicsScene {\npublic:\n\tusing ItemPtr = std::shared_ptr<NumberedItem>;\n\n\tScene(const QRectF &rect);\n\t~Scene();\n\tvoid applyBrush(const QColor &color, float size, Brush::Tool tool);\n\tvoid setBlurSource(Fn<QImage(QRect)> source);\n\n\t[[nodiscard]] std::vector<ItemPtr> items(\n\t\tQt::SortOrder order = Qt::DescendingOrder) const;\n\tvoid addItem(ItemPtr item);\n\tvoid removeItem(not_null<QGraphicsItem*> item);\n\tvoid removeItem(const ItemPtr &item);\n\t[[nodiscard]] rpl::producer<> addsItem() const;\n\t[[nodiscard]] rpl::producer<> removesItem() const;\n\n\t[[nodiscard]] std::shared_ptr<float64> lastZ() const;\n\n\tvoid updateZoom(float64 zoom);\n\n\tvoid cancelDrawing();\n\n\t[[nodiscard]] bool hasUndo() const;\n\t[[nodiscard]] bool hasRedo() const;\n\n\tvoid performUndo();\n\tvoid performRedo();\n\n\tvoid save(SaveState state);\n\tvoid restore(SaveState state);\n\n\tvoid clearRedoList();\nprotected:\n\tvoid mousePressEvent(QGraphicsSceneMouseEvent *event) override;\n\tvoid mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;\n\tvoid mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;\nprivate:\n\tvoid removeIf(Fn<bool(const ItemPtr &)> proj);\n\tconst std::shared_ptr<ItemCanvas> _canvas;\n\tconst std::shared_ptr<float64> _lastZ;\n\tFn<QImage(QRect)> _blurSource;\n\n\tstd::vector<ItemPtr> _items;\n\tstd::unordered_map<QGraphicsItem*, ItemPtr> _itemsByPointer;\n\n\tfloat64 _lastLineZ = 0.;\n\tint _itemNumber = 0;\n\n\trpl::event_stream<> _addsItem, _removesItem;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/scene/scene_item_base.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/scene/scene_item_base.h\"\n\n#include \"editor/scene/scene.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_editor.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QGraphicsScene>\n#include <QGraphicsSceneHoverEvent>\n#include <QGraphicsSceneMouseEvent>\n#include <QStyleOptionGraphicsItem>\n#include <QtMath>\n\nnamespace Editor {\nnamespace {\n\nconstexpr auto kSnapAngle = 45.;\n\nconst auto kDuplicateSequence = QKeySequence(\"ctrl+d\");\nconst auto kFlipSequence = QKeySequence(\"ctrl+s\");\nconst auto kDeleteSequence = QKeySequence(\"delete\");\n\nconstexpr auto kMinSizeRatio = 0.05;\nconstexpr auto kMaxSizeRatio = 1.00;\n\nauto Normalized(float64 angle) {\n\treturn angle\n\t\t+ ((std::abs(angle) < 360) ? 0 : (-360 * (angle < 0 ? -1 : 1)));\n}\n\n} // namespace\n\nint NumberedItem::type() const {\n\treturn NumberedItem::Type;\n}\n\nint NumberedItem::number() const {\n\treturn _number;\n}\n\nvoid NumberedItem::setNumber(int number) {\n\t_number = number;\n}\n\nNumberedItem::Status NumberedItem::status() const {\n\treturn _status;\n}\n\nbool NumberedItem::isNormalStatus() const {\n\treturn _status == Status::Normal;\n}\n\nbool NumberedItem::isUndidStatus() const {\n\treturn _status == Status::Undid;\n}\n\nbool NumberedItem::isRemovedStatus() const {\n\treturn _status == Status::Removed;\n}\n\nvoid NumberedItem::save(SaveState state) {\n}\n\nvoid NumberedItem::restore(SaveState state) {\n}\n\nbool NumberedItem::hasState(SaveState state) const {\n\treturn false;\n}\n\nvoid NumberedItem::setStatus(Status status) {\n\tif (status != _status) {\n\t\t_status = status;\n\t\tsetVisible(status == Status::Normal);\n\t}\n}\n\nItemBase::ItemBase(Data data)\n: _lastZ(data.zPtr)\n, _imageSize(data.imageSize)\n, _horizontalSize(data.size) {\n\tsetFlags(QGraphicsItem::ItemIsMovable\n\t\t| QGraphicsItem::ItemIsSelectable\n\t\t| QGraphicsItem::ItemIsFocusable);\n\tsetAcceptHoverEvents(true);\n\tapplyData(data);\n}\n\nQRectF ItemBase::boundingRect() const {\n\treturn innerRect() + _scaledInnerMargins;\n}\n\nQRectF ItemBase::contentRect() const {\n\treturn innerRect() - _scaledInnerMargins;\n}\n\nQRectF ItemBase::innerRect() const {\n\tconst auto &hSize = _horizontalSize;\n\tconst auto &vSize = _verticalSize;\n\treturn QRectF(-hSize / 2, -vSize / 2, hSize, vSize);\n}\n\nvoid ItemBase::paint(\n\t\tQPainter *p,\n\t\tconst QStyleOptionGraphicsItem *option,\n\t\tQWidget *) {\n\tif (!(option->state & QStyle::State_Selected)) {\n\t\treturn;\n\t}\n\tPainterHighQualityEnabler hq(*p);\n\tconst auto hasFocus = (option->state & QStyle::State_HasFocus);\n\tp->setPen(hasFocus ? _pens.select : _pens.selectInactive);\n\tp->drawRect(innerRect());\n\n\tp->setPen(hasFocus ? _pens.handle : _pens.handleInactive);\n\tp->setBrush(st::photoEditorItemBaseHandleFg);\n\tp->drawEllipse(rightHandleRect());\n\tp->drawEllipse(leftHandleRect());\n}\n\nvoid ItemBase::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {\n\tif (isHandling()) {\n\t\tconst auto mousePos = event->pos();\n\t\tconst auto shift = event->modifiers().testFlag(Qt::ShiftModifier);\n\t\tconst auto isLeft = (_handle == HandleType::Left);\n\t\tif (!shift) {\n\t\t\t// Resize.\n\t\t\tconst auto p = isLeft ? (mousePos * -1) : mousePos;\n\t\t\tconst auto dx = int(2.0 * p.x());\n\t\t\tconst auto dy = int(2.0 * p.y());\n\t\t\tprepareGeometryChange();\n\t\t\t_horizontalSize = std::clamp(\n\t\t\t\t(dx > dy ? dx : dy),\n\t\t\t\t_sizeLimits.min,\n\t\t\t\t_sizeLimits.max);\n\t\t\tupdateVerticalSize();\n\t\t}\n\n\t\t// Rotate.\n\t\tconst auto origin = mapToScene(boundingRect().center());\n\t\tconst auto pos = mapToScene(mousePos);\n\n\t\tconst auto diff = pos - origin;\n\t\tconst auto angle = Normalized((isLeft ? 180 : 0)\n\t\t\t+ (std::atan2(diff.y(), diff.x()) * 180 / M_PI));\n\t\tsetRotation(shift\n\t\t\t? (base::SafeRound(angle / kSnapAngle) * kSnapAngle)\n\t\t\t: angle);\n\t} else {\n\t\tQGraphicsItem::mouseMoveEvent(event);\n\t}\n}\n\nvoid ItemBase::hoverMoveEvent(QGraphicsSceneHoverEvent *event) {\n\tsetCursor(isHandling()\n\t\t? Qt::ClosedHandCursor\n\t\t: (handleType(event->pos()) != HandleType::None) && isSelected()\n\t\t? Qt::OpenHandCursor\n\t\t: Qt::ArrowCursor);\n\tQGraphicsItem::hoverMoveEvent(event);\n}\n\nvoid ItemBase::mousePressEvent(QGraphicsSceneMouseEvent *event) {\n\tsetZValue((*_lastZ)++);\n\tif (event->button() == Qt::LeftButton) {\n\t\t_handle = handleType(event->pos());\n\t}\n\tif (isHandling()) {\n\t\tsetCursor(Qt::ClosedHandCursor);\n\t} else {\n\t\tQGraphicsItem::mousePressEvent(event);\n\t}\n}\n\nvoid ItemBase::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {\n\tif ((event->button() == Qt::LeftButton) && isHandling()) {\n\t\t_handle = HandleType::None;\n\t} else {\n\t\tQGraphicsItem::mouseReleaseEvent(event);\n\t}\n}\n\nvoid ItemBase::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) {\n\tif (scene()) {\n\t\tscene()->clearSelection();\n\t\tsetSelected(true);\n\t}\n\n\tconst auto add = [&](\n\t\t\tauto base,\n\t\t\tconst QKeySequence &sequence,\n\t\t\tFn<void()> callback,\n\t\t\tconst style::icon *icon) {\n\t\t// TODO: refactor.\n\t\tconst auto sequenceText = QChar('\\t')\n\t\t\t+ sequence.toString(QKeySequence::NativeText);\n\t\t_menu->addAction(\n\t\t\tbase(tr::now) + sequenceText,\n\t\t\tstd::move(callback),\n\t\t\ticon);\n\t};\n\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tnullptr,\n\t\tst::popupMenuWithIcons);\n\tadd(\n\t\ttr::lng_photo_editor_menu_delete,\n\t\tkDeleteSequence,\n\t\t[=] { actionDelete(); },\n\t\t&st::menuIconDelete);\n\tadd(\n\t\ttr::lng_photo_editor_menu_flip,\n\t\tkFlipSequence,\n\t\t[=] { actionFlip(); },\n\t\t&st::menuIconFlip);\n\tadd(\n\t\ttr::lng_photo_editor_menu_duplicate,\n\t\tkDuplicateSequence,\n\t\t[=] { actionDuplicate(); },\n\t\t&st::menuIconCopy);\n\n\t_menu->popup(event->screenPos());\n}\n\nvoid ItemBase::performForSelectedItems(Action action) {\n\tif (const auto s = scene()) {\n\t\tfor (const auto item : s->selectedItems()) {\n\t\t\tif (const auto base = static_cast<ItemBase*>(item)) {\n\t\t\t\t(base->*action)();\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ItemBase::actionFlip() {\n\tsetFlip(!flipped());\n}\n\nvoid ItemBase::actionDelete() {\n\tif (const auto s = static_cast<Scene*>(scene())) {\n\t\ts->removeItem(this);\n\t}\n}\n\nvoid ItemBase::actionDuplicate() {\n\tif (const auto s = static_cast<Scene*>(scene())) {\n\t\tauto data = generateData();\n\t\tdata.x += int(_horizontalSize / 3);\n\t\tdata.y += int(_verticalSize / 3);\n\t\tconst auto newItem = duplicate(std::move(data));\n\t\tif (hasFocus()) {\n\t\t\tnewItem->setFocus();\n\t\t}\n\t\tconst auto selected = isSelected();\n\t\tnewItem->setSelected(selected);\n\t\tsetSelected(false);\n\t\ts->addItem(newItem);\n\t}\n}\n\nvoid ItemBase::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Escape) {\n\t\tif (const auto s = scene()) {\n\t\t\ts->clearSelection();\n\t\t\ts->clearFocus();\n\t\t\treturn;\n\t\t}\n\t}\n\thandleActionKey(e);\n}\n\nvoid ItemBase::handleActionKey(not_null<QKeyEvent*> e) {\n\tconst auto matches = [&](const QKeySequence &sequence) {\n\t\tconst auto searchKey = (e->modifiers() | e->key())\n\t\t\t& ~(Qt::KeypadModifier | Qt::GroupSwitchModifier);\n\t\tconst auto events = QKeySequence(searchKey);\n\t\treturn sequence.matches(events) == QKeySequence::ExactMatch;\n\t};\n\tif (matches(kDuplicateSequence)) {\n\t\tperformForSelectedItems(&ItemBase::actionDuplicate);\n\t} else if (matches(kDeleteSequence)) {\n\t\tperformForSelectedItems(&ItemBase::actionDelete);\n\t} else if (matches(kFlipSequence)) {\n\t\tperformForSelectedItems(&ItemBase::actionFlip);\n\t}\n}\n\nQRectF ItemBase::rightHandleRect() const {\n\treturn QRectF(\n\t\t(_horizontalSize / 2) - (_scaledHandleSize / 2),\n\t\t0 - (_scaledHandleSize / 2),\n\t\t_scaledHandleSize,\n\t\t_scaledHandleSize);\n}\n\nQRectF ItemBase::leftHandleRect() const {\n\treturn QRectF(\n\t\t(-_horizontalSize / 2) - (_scaledHandleSize / 2),\n\t\t0 - (_scaledHandleSize / 2),\n\t\t_scaledHandleSize,\n\t\t_scaledHandleSize);\n}\n\nbool ItemBase::isHandling() const {\n\treturn _handle != HandleType::None;\n}\n\nfloat64 ItemBase::size() const {\n\treturn _horizontalSize;\n}\n\nvoid ItemBase::updateVerticalSize() {\n\tconst auto verticalSize = _horizontalSize * _aspectRatio;\n\t_verticalSize = std::max(\n\t\tverticalSize,\n\t\tfloat64(_sizeLimits.min));\n\tif (verticalSize < _sizeLimits.min) {\n\t\t_horizontalSize = _verticalSize / _aspectRatio;\n\t}\n}\n\nvoid ItemBase::setAspectRatio(float64 aspectRatio) {\n\t_aspectRatio = aspectRatio;\n\tupdateVerticalSize();\n}\n\nItemBase::HandleType ItemBase::handleType(const QPointF &pos) const {\n\treturn rightHandleRect().contains(pos)\n\t\t? HandleType::Right\n\t\t: leftHandleRect().contains(pos)\n\t\t? HandleType::Left\n\t\t: HandleType::None;\n}\n\nbool ItemBase::flipped() const {\n\treturn _flipped;\n}\n\nvoid ItemBase::setFlip(bool value) {\n\tif (_flipped != value) {\n\t\tperformFlip();\n\t\t_flipped = value;\n\t}\n}\n\nint ItemBase::type() const {\n\treturn ItemBase::Type;\n}\n\nvoid ItemBase::updateZoom(float64 zoom) {\n\t_scaledHandleSize = st::photoEditorItemHandleSize / zoom;\n\t_scaledInnerMargins = QMarginsF(\n\t\t_scaledHandleSize,\n\t\t_scaledHandleSize,\n\t\t_scaledHandleSize,\n\t\t_scaledHandleSize) * 0.5;\n\n\tconst auto maxSide = std::max(\n\t\t_imageSize.width(),\n\t\t_imageSize.height());\n\t_sizeLimits = {\n\t\t.min = int(maxSide * kMinSizeRatio),\n\t\t.max = int(maxSide * kMaxSizeRatio),\n\t};\n\t_horizontalSize = std::clamp(\n\t\t_horizontalSize,\n\t\tfloat64(_sizeLimits.min),\n\t\tfloat64(_sizeLimits.max));\n\tupdateVerticalSize();\n\n\tupdatePens(QPen(\n\t\tQBrush(),\n\t\t1 / zoom,\n\t\tQt::DashLine,\n\t\tQt::SquareCap,\n\t\tQt::RoundJoin));\n}\n\nvoid ItemBase::performFlip() {\n}\n\nvoid ItemBase::updatePens(QPen pen) {\n\t_pens = {\n\t\t.select = pen,\n\t\t.selectInactive = pen,\n\t\t.handle = pen,\n\t\t.handleInactive = pen,\n\t};\n\t_pens.select.setColor(Qt::white);\n\t_pens.selectInactive.setColor(Qt::gray);\n\t_pens.handle.setColor(Qt::white);\n\t_pens.handleInactive.setColor(Qt::gray);\n\t_pens.handle.setStyle(Qt::SolidLine);\n\t_pens.handleInactive.setStyle(Qt::SolidLine);\n}\n\nItemBase::Data ItemBase::generateData() const {\n\treturn {\n\t\t.initialZoom = (st::photoEditorItemHandleSize / _scaledHandleSize),\n\t\t.zPtr = _lastZ,\n\t\t.size = int(_horizontalSize),\n\t\t.x = int(scenePos().x()),\n\t\t.y = int(scenePos().y()),\n\t\t.flipped = flipped(),\n\t\t.rotation = int(rotation()),\n\t\t.imageSize = _imageSize,\n\t};\n}\n\nvoid ItemBase::applyData(const Data &data) {\n\t// _lastZ is const.\n\t// _imageSize is const.\n\t_horizontalSize = data.size;\n\tsetPos(data.x, data.y);\n\tsetZValue((*_lastZ)++);\n\tsetFlip(data.flipped);\n\tsetRotation(data.rotation);\n\tupdateZoom(data.initialZoom);\n\tupdate();\n}\n\nvoid ItemBase::save(SaveState state) {\n\tconst auto z = zValue();\n\tauto &saved = (state == SaveState::Keep) ? _keeped : _saved;\n\tsaved = {\n\t\t.data = generateData(),\n\t\t.zValue = z,\n\t\t.status = status(),\n\t};\n}\n\nvoid ItemBase::restore(SaveState state) {\n\tif (!hasState(state)) {\n\t\treturn;\n\t}\n\tconst auto &saved = (state == SaveState::Keep) ? _keeped : _saved;\n\tapplyData(saved.data);\n\tsetZValue(saved.zValue);\n\tsetStatus(saved.status);\n}\n\nbool ItemBase::hasState(SaveState state) const {\n\tconst auto &saved = (state == SaveState::Keep) ? _keeped : _saved;\n\treturn saved.zValue;\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/scene/scene_item_base.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n#include \"editor/photo_editor_inner_common.h\"\n\n#include <QGraphicsItem>\n\nclass QGraphicsSceneHoverEvent;\nclass QGraphicsSceneMouseEvent;\nclass QStyleOptionGraphicsItem;\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Editor {\n\nclass NumberedItem : public QGraphicsItem {\npublic:\n\tenum class Status {\n\t\tNormal,\n\t\tUndid,\n\t\tRemoved,\n\t};\n\n\tenum { Type = UserType + 1 };\n\tusing QGraphicsItem::QGraphicsItem;\n\n\tint type() const override;\n\tvoid setNumber(int number);\n\t[[nodiscard]] int number() const;\n\n\t[[nodiscard]] Status status() const;\n\tvoid setStatus(Status status);\n\t[[nodiscard]] bool isNormalStatus() const;\n\t[[nodiscard]] bool isUndidStatus() const;\n\t[[nodiscard]] bool isRemovedStatus() const;\n\n\tvirtual void save(SaveState state);\n\tvirtual void restore(SaveState state);\n\tvirtual bool hasState(SaveState state) const;\nprivate:\n\tint _number = 0;\n\tStatus _status = Status::Normal;\n};\n\nclass ItemBase : public NumberedItem {\npublic:\n\tenum { Type = UserType + 2 };\n\n\tstruct Data {\n\t\tfloat64 initialZoom = 0.;\n\t\tstd::shared_ptr<float64> zPtr;\n\t\tint size = 0;\n\t\tint x = 0;\n\t\tint y = 0;\n\t\tbool flipped = false;\n\t\tint rotation = 0;\n\t\tQSize imageSize;\n\t};\n\n\tItemBase(Data data);\n\tQRectF boundingRect() const override;\n\tvoid paint(\n\t\tQPainter *p,\n\t\tconst QStyleOptionGraphicsItem *option,\n\t\tQWidget *widget) override;\n\tint type() const override;\n\n\tbool flipped() const;\n\tvoid setFlip(bool value);\n\n\tvoid updateZoom(float64 zoom);\n\n\tbool hasState(SaveState state) const override;\n\tvoid save(SaveState state) override;\n\tvoid restore(SaveState state) override;\nprotected:\n\tenum HandleType {\n\t\tNone,\n\t\tLeft,\n\t\tRight,\n\t};\n\tvoid mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;\n\tvoid hoverMoveEvent(QGraphicsSceneHoverEvent *event) override;\n\tvoid mousePressEvent(QGraphicsSceneMouseEvent *event) override;\n\tvoid mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;\n\tvoid contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\n\tusing Action = void(ItemBase::*)();\n\tvoid performForSelectedItems(Action action);\n\tvoid actionFlip();\n\tvoid actionDelete();\n\tvoid actionDuplicate();\n\n\tQRectF contentRect() const;\n\tQRectF innerRect() const;\n\tfloat64 size() const;\n\tfloat64 horizontalSize() const;\n\tfloat64 verticalSize() const;\n\tvoid setAspectRatio(float64 aspectRatio);\n\n\tvirtual void performFlip();\n\tvirtual std::shared_ptr<ItemBase> duplicate(Data data) const = 0;\nprivate:\n\tHandleType handleType(const QPointF &pos) const;\n\tQRectF rightHandleRect() const;\n\tQRectF leftHandleRect() const;\n\tbool isHandling() const;\n\tvoid updateVerticalSize();\n\tvoid updatePens(QPen pen);\n\tvoid handleActionKey(not_null<QKeyEvent*> e);\n\n\tData generateData() const;\n\tvoid applyData(const Data &data);\n\n\tconst std::shared_ptr<float64> _lastZ;\n\tconst QSize _imageSize;\n\n\tstruct {\n\t\tQPen select;\n\t\tQPen selectInactive;\n\t\tQPen handle;\n\t\tQPen handleInactive;\n\t} _pens;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tstruct {\n\t\tData data;\n\t\tfloat64 zValue = 0.;\n\t\tNumberedItem::Status status;\n\t} _saved, _keeped;\n\n\tstruct {\n\t\tint min = 0;\n\t\tint max = 0;\n\t} _sizeLimits;\n\tfloat64 _scaledHandleSize = 1.0;\n\tQMarginsF _scaledInnerMargins;\n\n\tfloat64 _horizontalSize = 0;\n\tfloat64 _verticalSize = 0;\n\tfloat64 _aspectRatio = 1.0;\n\tHandleType _handle = HandleType::None;\n\n\tbool _flipped = false;\n\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/scene/scene_item_canvas.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/scene/scene_item_canvas.h\"\n#include \"styles/style_editor.h\"\n\n#include <QGraphicsScene>\n#include <QGraphicsSceneMouseEvent>\n#include <QtMath>\n\nnamespace Editor {\nnamespace {\n\nconstexpr auto kMinPointDistanceBase = 2.0;\nconstexpr auto kMaxPointDistance = 15.0;\nconstexpr auto kSmoothingStrength = 0.5;\nconstexpr auto kSegmentOverlap = 3;\nconstexpr auto kOriginStartJumpRatio = 0.25;\nconstexpr auto kHalfStrength = kSmoothingStrength / 2.0;\nconstexpr auto kInvStrength = 1.0 - kSmoothingStrength;\n\n[[nodiscard]] float64 PointDistance(const QPointF &a, const QPointF &b) {\n\tconst auto dx = a.x() - b.x();\n\tconst auto dy = a.y() - b.y();\n\treturn std::sqrt(dx * dx + dy * dy);\n}\n\n[[nodiscard]] bool IsValidPoint(const QPointF &point) {\n\treturn std::isfinite(point.x()) && std::isfinite(point.y());\n}\n\n} // namespace\n\nItemCanvas::ItemCanvas() {\n\tsetAcceptedMouseButtons({});\n}\n\nvoid ItemCanvas::clearPixmap() {\n\t_hq = nullptr;\n\t_p = nullptr;\n\n\t_pixmap = QPixmap(\n\t\t(scene()->sceneRect().size() * style::DevicePixelRatio()).toSize());\n\t_pixmap.setDevicePixelRatio(style::DevicePixelRatio());\n\t_pixmap.fill(Qt::transparent);\n\n\t_p = std::make_unique<Painter>(&_pixmap);\n\t_hq = std::make_unique<PainterHighQualityEnabler>(*_p);\n\t_p->setPen(Qt::NoPen);\n\t_p->setBrush(_brushData.color);\n}\n\nvoid ItemCanvas::applyBrush(const QColor &color, float size, Brush::Tool tool) {\n\t_brushData.color = color;\n\t_brushData.size = size;\n\t_brushData.tool = tool;\n\t_p->setBrush(color);\n\tconst auto width = strokeWidth(1.0);\n\tconst auto extra = (tool == Brush::Tool::Arrow)\n\t\t? arrowHeadLength()\n\t\t: 0.;\n\tconst auto margin = width + extra;\n\t_brushMargins = QMarginsF(margin, margin, margin, margin);\n}\n\nQRectF ItemCanvas::boundingRect() const {\n\treturn scene()->sceneRect();\n}\n\nvoid ItemCanvas::computeContentRect(const QPointF &p) {\n\tif (!scene() || !IsValidPoint(p)) {\n\t\treturn;\n\t}\n\tconst auto sceneSize = scene()->sceneRect().size();\n\tconst auto contentLeft = std::max(0., _contentRect.x());\n\tconst auto contentTop = std::max(0., _contentRect.y());\n\t_contentRect = QRectF(\n\t\tQPointF(\n\t\t\tstd::clamp(p.x() - _brushMargins.left(), 0., contentLeft),\n\t\t\tstd::clamp(p.y() - _brushMargins.top(), 0., contentTop)),\n\t\tQPointF(\n\t\t\tstd::clamp(\n\t\t\t\tp.x() + _brushMargins.right(),\n\t\t\t\tcontentLeft + _contentRect.width(),\n\t\t\t\tsceneSize.width()),\n\t\t\tstd::clamp(\n\t\t\t\tp.y() + _brushMargins.bottom(),\n\t\t\t\tcontentTop + _contentRect.height(),\n\t\t\t\tsceneSize.height())));\n}\n\nstd::vector<ItemCanvas::StrokePoint> ItemCanvas::smoothStroke(\n\t\tconst std::vector<StrokePoint> &points) const {\n\tif (points.size() < 4) {\n\t\treturn points;\n\t}\n\tauto result = std::vector<StrokePoint>();\n\tresult.reserve(points.size());\n\tresult.push_back(points[0]);\n\tresult.push_back(points[1]);\n\tfor (auto i = 2; i < int(points.size()) - 1; ++i) {\n\t\tconst auto &prev = points[i - 1].pos;\n\t\tconst auto &curr = points[i].pos;\n\t\tconst auto &next = points[i + 1].pos;\n\t\tconst auto smoothed = curr * kInvStrength\n\t\t\t+ (prev + next) * kHalfStrength;\n\t\tresult.push_back({\n\t\t\t.pos = smoothed,\n\t\t\t.pressure = points[i].pressure,\n\t\t\t.time = points[i].time,\n\t\t});\n\t}\n\tresult.push_back(points.back());\n\treturn result;\n}\n\nfloat64 ItemCanvas::strokeWidth(float64 pressure) const {\n\tauto width = _brushData.size * pressure;\n\tif (_brushData.tool == Brush::Tool::Marker) {\n\t\twidth *= st::photoEditorMarkerSizeMultiplier;\n\t} else if (_brushData.tool == Brush::Tool::Blur) {\n\t\twidth *= st::photoEditorBlurSizeMultiplier;\n\t}\n\treturn width;\n}\n\nQColor ItemCanvas::strokeColor() const {\n\tif (_brushData.tool == Brush::Tool::Eraser\n\t\t|| _brushData.tool == Brush::Tool::Blur) {\n\t\treturn QColor(0, 0, 0, 255);\n\t}\n\tauto color = _brushData.color;\n\tif (_brushData.tool == Brush::Tool::Marker) {\n\t\tcolor.setAlphaF(color.alphaF() * st::photoEditorMarkerOpacity);\n\t}\n\treturn color;\n}\n\nfloat64 ItemCanvas::arrowHeadLength() const {\n\treturn _brushData.size * st::photoEditorArrowHeadLengthFactor;\n}\n\nvoid ItemCanvas::renderSegment(\n\t\tconst std::vector<StrokePoint> &points,\n\t\tint startIdx) {\n\tif (points.size() < 2 || startIdx >= int(points.size()) - 1) {\n\t\treturn;\n\t}\n\tif (!IsValidPoint(points.back().pos)) {\n\t\treturn;\n\t}\n\tauto path = QPainterPath();\n\tconst auto effectiveStart = std::max(0, startIdx);\n\tif (!IsValidPoint(points[effectiveStart].pos)) {\n\t\treturn;\n\t}\n\tpath.moveTo(points[effectiveStart].pos);\n\tfor (auto i = effectiveStart; i < int(points.size()) - 1; ++i) {\n\t\tconst auto &p0 = points[i].pos;\n\t\tconst auto &p1 = points[i + 1].pos;\n\t\tif (!IsValidPoint(p0) || !IsValidPoint(p1)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto ctrl = (p0 + p1) / 2.0;\n\t\tif (!IsValidPoint(ctrl)) {\n\t\t\treturn;\n\t\t}\n\t\tif (i == effectiveStart) {\n\t\t\tpath.lineTo(ctrl);\n\t\t} else {\n\t\t\tpath.quadTo(p0, ctrl);\n\t\t}\n\t}\n\tpath.lineTo(points.back().pos);\n\tconst auto count = points.size() - std::max(0, startIdx);\n\tconst auto avgPressure = count > 0\n\t\t? std::accumulate(\n\t\t\tpoints.begin() + std::max(0, startIdx),\n\t\t\tpoints.end(),\n\t\t\t0.0,\n\t\t\t[](float64 sum, const StrokePoint &p) {\n\t\t\t\treturn sum + p.pressure;\n\t\t\t}) / count\n\t\t: 1.0;\n\tconst auto width = strokeWidth(avgPressure);\n\tauto stroker = QPainterPathStroker();\n\tstroker.setWidth(width);\n\tstroker.setCapStyle(Qt::RoundCap);\n\tstroker.setJoinStyle(Qt::RoundJoin);\n\tconst auto outline = stroker.createStroke(path);\n\tconst auto color = strokeColor();\n\tif (_brushData.tool == Brush::Tool::Marker) {\n\t\t_p->save();\n\t\t_p->setCompositionMode(QPainter::CompositionMode_Source);\n\t\t_p->fillPath(outline, color);\n\t\t_p->restore();\n\t} else {\n\t\t_p->fillPath(outline, color);\n\t}\n\t_rectToUpdate |= outline.boundingRect() + _brushMargins;\n}\n\nvoid ItemCanvas::drawIncrementalStroke() {\n\tif (_currentStroke.size() < 2) {\n\t\treturn;\n\t}\n\tconst auto startIdx = std::max(\n\t\t0,\n\t\t_lastRenderedIndex - kSegmentOverlap);\n\tauto segment = std::vector<StrokePoint>(\n\t\t_currentStroke.begin() + startIdx,\n\t\t_currentStroke.end());\n\tif (segment.size() < 2) {\n\t\treturn;\n\t}\n\tif (segment.size() >= 4) {\n\t\tfor (auto i = 0; i < 2; ++i) {\n\t\t\tsegment = smoothStroke(segment);\n\t\t}\n\t}\n\trenderSegment(\n\t\tsegment,\n\t\tstd::min(kSegmentOverlap, int(segment.size()) - 1));\n\t_lastRenderedIndex = std::max(\n\t\t0,\n\t\tint(_currentStroke.size()) - kSegmentOverlap);\n}\n\nvoid ItemCanvas::drawArrowHead() {\n\tif (_brushData.tool != Brush::Tool::Arrow) {\n\t\treturn;\n\t}\n\tif (_currentStroke.size() < 2) {\n\t\treturn;\n\t}\n\tconst auto tip = _currentStroke.back().pos;\n\tconst auto minDistance = _brushData.size\n\t\t* st::photoEditorArrowHeadMinDistanceFactor;\n\tauto base = _currentStroke.front().pos;\n\tfor (auto i = int(_currentStroke.size()) - 2; i >= 0; --i) {\n\t\tconst auto &p = _currentStroke[i].pos;\n\t\tif (PointDistance(tip, p) >= minDistance) {\n\t\t\tbase = p;\n\t\t\tbreak;\n\t\t}\n\t}\n\tauto direction = tip - base;\n\tconst auto length = std::sqrt(\n\t\tdirection.x() * direction.x()\n\t\t\t+ direction.y() * direction.y());\n\tif (length <= 0.) {\n\t\treturn;\n\t}\n\tdirection /= length;\n\tconst auto angle = qDegreesToRadians(\n\t\tdouble(st::photoEditorArrowHeadAngleDegrees));\n\tconst auto sinA = std::sin(angle);\n\tconst auto cosA = std::cos(angle);\n\tconst auto rotate = [&](const QPointF &v, float64 s, float64 c) {\n\t\treturn QPointF(v.x() * c - v.y() * s, v.x() * s + v.y() * c);\n\t};\n\tconst auto headLength = arrowHeadLength();\n\tconst auto left = tip - rotate(direction, sinA, cosA) * headLength;\n\tconst auto right = tip - rotate(direction, -sinA, cosA) * headLength;\n\n\tauto arrow = QPainterPath();\n\tarrow.moveTo(tip);\n\tarrow.lineTo(left);\n\tarrow.moveTo(tip);\n\tarrow.lineTo(right);\n\tauto stroker = QPainterPathStroker();\n\tstroker.setWidth(strokeWidth(_currentStroke.back().pressure));\n\tstroker.setCapStyle(Qt::RoundCap);\n\tstroker.setJoinStyle(Qt::RoundJoin);\n\tconst auto outline = stroker.createStroke(arrow);\n\t_p->fillPath(outline, strokeColor());\n\t_rectToUpdate |= outline.boundingRect() + _brushMargins;\n\tcomputeContentRect(left);\n\tcomputeContentRect(right);\n}\n\nvoid ItemCanvas::addStrokePoint(const QPointF &point, int64 time) {\n\tif (!IsValidPoint(point)) {\n\t\treturn;\n\t}\n\tif ((_currentStroke.size() == 1)\n\t\t&& (_currentStroke.front().pos == QPointF())\n\t\t&& (point != QPointF())) {\n\t\tif (const auto currentScene = scene()) {\n\t\t\tconst auto size = currentScene->sceneRect().size();\n\t\t\tconst auto maxStartJump = std::max(size.width(), size.height())\n\t\t\t\t* kOriginStartJumpRatio;\n\t\t\tif (PointDistance(_currentStroke.front().pos, point)\n\t\t\t\t> maxStartJump) {\n\t\t\t\t_currentStroke.front() = {\n\t\t\t\t\t.pos = point,\n\t\t\t\t\t.pressure = 1.0,\n\t\t\t\t\t.time = time,\n\t\t\t\t};\n\t\t\t\t_lastPointTime = time;\n\t\t\t\t_contentRect = QRectF(point, point) + _brushMargins;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\tif (!_currentStroke.empty()) {\n\t\tconst auto distance = PointDistance(\n\t\t\tpoint,\n\t\t\t_currentStroke.back().pos);\n\t\tconst auto minDistance = (_zoom > 1.0)\n\t\t\t? (kMinPointDistanceBase / _zoom)\n\t\t\t: (kMinPointDistanceBase * _zoom);\n\t\tif (distance < minDistance) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto maxDistance = (_zoom > 1.0)\n\t\t\t? (kMaxPointDistance / _zoom)\n\t\t\t: kMaxPointDistance;\n\t\tif (distance > maxDistance) {\n\t\t\tconst auto steps = int(std::ceil(distance / maxDistance));\n\t\t\tconst auto lastPos = _currentStroke.back().pos;\n\t\t\tconst auto lastPressure = _currentStroke.back().pressure;\n\t\t\tfor (auto i = 1; i < steps; ++i) {\n\t\t\t\tconst auto t = float64(i) / steps;\n\t\t\t\tconst auto interpolated = lastPos * (1.0 - t) + point * t;\n\t\t\t\tconst auto interpTime = _lastPointTime\n\t\t\t\t\t+ int64((time - _lastPointTime) * t);\n\t\t\t\t_currentStroke.push_back({\n\t\t\t\t\t.pos = interpolated,\n\t\t\t\t\t.pressure = lastPressure,\n\t\t\t\t\t.time = interpTime,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\t_currentStroke.push_back({\n\t\t.pos = point,\n\t\t.pressure = 1.0,\n\t\t.time = time,\n\t});\n\t_lastPointTime = time;\n\tcomputeContentRect(point);\n}\n\nvoid ItemCanvas::handleMousePressEvent(\n\t\tnot_null<QGraphicsSceneMouseEvent*> e) {\n\t_lastPoint = e->scenePos();\n\tif (!IsValidPoint(_lastPoint)) {\n\t\t_drawing = false;\n\t\treturn;\n\t}\n\t_rectToUpdate = QRectF();\n\t_contentRect = QRectF();\n\t_currentStroke.clear();\n\t_lastRenderedIndex = 0;\n\t_lastPointTime = 0;\n\tconst auto now = crl::now();\n\taddStrokePoint(_lastPoint, now);\n\t_contentRect = QRectF(_lastPoint, _lastPoint) + _brushMargins;\n\t_drawing = true;\n}\n\nvoid ItemCanvas::handleMouseMoveEvent(\n\t\tnot_null<QGraphicsSceneMouseEvent*> e) {\n\tif (!_drawing) {\n\t\treturn;\n\t}\n\tconst auto scenePos = e->scenePos();\n\tif (!IsValidPoint(scenePos)) {\n\t\treturn;\n\t}\n\tconst auto now = crl::now();\n\taddStrokePoint(scenePos, now);\n\t_lastPoint = scenePos;\n\tif (_currentStroke.size() - _lastRenderedIndex >= 3) {\n\t\tdrawIncrementalStroke();\n\t\tupdate(_rectToUpdate);\n\t}\n}\n\nvoid ItemCanvas::handleMouseReleaseEvent(\n\t\tnot_null<QGraphicsSceneMouseEvent*> e) {\n\tif (!_drawing) {\n\t\treturn;\n\t}\n\t_drawing = false;\n\tdrawIncrementalStroke();\n\tdrawArrowHead();\n\tupdate(_rectToUpdate);\n\tif (_contentRect.isValid()) {\n\t\tconst auto scaledContentRect = QRectF(\n\t\t\t_contentRect.x() * style::DevicePixelRatio(),\n\t\t\t_contentRect.y() * style::DevicePixelRatio(),\n\t\t\t_contentRect.width() * style::DevicePixelRatio(),\n\t\t\t_contentRect.height() * style::DevicePixelRatio());\n\t\t_grabContentRequests.fire({\n\t\t\t.pixmap = _pixmap.copy(scaledContentRect.toRect()),\n\t\t\t.position = _contentRect.topLeft(),\n\t\t\t.clear = (_brushData.tool == Brush::Tool::Eraser),\n\t\t\t.blur = (_brushData.tool == Brush::Tool::Blur),\n\t\t});\n\t}\n\t_currentStroke.clear();\n\t_lastRenderedIndex = 0;\n\t_lastPointTime = 0;\n\t_currentPath = QPainterPath();\n\tclearPixmap();\n\tupdate();\n}\n\nvoid ItemCanvas::paint(\n\t\tQPainter *p,\n\t\tconst QStyleOptionGraphicsItem *,\n\t\tQWidget *) {\n\tp->fillRect(_rectToUpdate, Qt::transparent);\n\tif (_brushData.tool == Brush::Tool::Eraser) {\n\t\tp->save();\n\t\tp->setOpacity(st::photoEditorEraserPreviewOpacity);\n\t\tp->drawPixmap(0, 0, _pixmap);\n\t\tp->restore();\n\t} else if (_brushData.tool == Brush::Tool::Blur) {\n\t\tp->save();\n\t\tp->setOpacity(st::photoEditorBlurPreviewOpacity);\n\t\tp->drawPixmap(0, 0, _pixmap);\n\t\tp->restore();\n\t} else {\n\t\tp->drawPixmap(0, 0, _pixmap);\n\t}\n\t_rectToUpdate = QRectF();\n}\n\nrpl::producer<ItemCanvas::Content> ItemCanvas::grabContentRequests() const {\n\treturn _grabContentRequests.events();\n}\n\nbool ItemCanvas::collidesWithItem(\n\t\tconst QGraphicsItem *,\n\t\tQt::ItemSelectionMode) const {\n\treturn false;\n}\n\nbool ItemCanvas::collidesWithPath(\n\t\tconst QPainterPath &,\n\t\tQt::ItemSelectionMode) const {\n\treturn false;\n}\n\nvoid ItemCanvas::cancelDrawing() {\n\t_drawing = false;\n\t_currentStroke.clear();\n\t_lastRenderedIndex = 0;\n\t_lastPointTime = 0;\n\t_currentPath = QPainterPath();\n\t_contentRect = QRectF();\n\tclearPixmap();\n\tupdate();\n}\n\nvoid ItemCanvas::updateZoom(float64 zoom) {\n\t_zoom = zoom;\n}\n\nItemCanvas::~ItemCanvas() {\n\t_hq = nullptr;\n\t_p = nullptr;\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/scene/scene_item_canvas.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"editor/photo_editor_inner_common.h\"\n#include \"ui/painter.h\"\n\n#include <QGraphicsItem>\n\nclass QGraphicsSceneMouseEvent;\n\nnamespace Editor {\n\nclass ItemCanvas : public QGraphicsItem {\npublic:\n\tstruct Content {\n\t\tQPixmap pixmap;\n\t\tQPointF position;\n\t\tbool clear = false;\n\t\tbool blur = false;\n\t};\n\n\tItemCanvas();\n\t~ItemCanvas();\n\n\tvoid applyBrush(const QColor &color, float size, Brush::Tool tool);\n\tvoid clearPixmap();\n\tvoid cancelDrawing();\n\tvoid updateZoom(float64 zoom);\n\n\tQRectF boundingRect() const override;\n\tvoid paint(\n\t\tQPainter *p,\n\t\tconst QStyleOptionGraphicsItem *option,\n\t\tQWidget *widget) override;\n\n\tvoid handleMousePressEvent(not_null<QGraphicsSceneMouseEvent*> event);\n\tvoid handleMouseReleaseEvent(not_null<QGraphicsSceneMouseEvent*> event);\n\tvoid handleMouseMoveEvent(not_null<QGraphicsSceneMouseEvent*> event);\n\n\t[[nodiscard]] rpl::producer<Content> grabContentRequests() const;\n\nprotected:\n\tbool collidesWithItem(\n\t\tconst QGraphicsItem *,\n\t\tQt::ItemSelectionMode) const override;\n\n\tbool collidesWithPath(\n\t\tconst QPainterPath &,\n\t\tQt::ItemSelectionMode) const override;\nprivate:\n\tstruct StrokePoint {\n\t\tQPointF pos;\n\t\tfloat64 pressure = 1.0;\n\t\tint64 time = 0;\n\t};\n\n\tvoid computeContentRect(const QPointF &p);\n\tvoid addStrokePoint(const QPointF &point, int64 time);\n\tvoid drawIncrementalStroke();\n\tvoid drawArrowHead();\n\tstd::vector<StrokePoint> smoothStroke(\n\t\tconst std::vector<StrokePoint> &points) const;\n\tvoid renderSegment(\n\t\tconst std::vector<StrokePoint> &points,\n\t\tint startIdx);\n\t[[nodiscard]] float64 strokeWidth(float64 pressure) const;\n\t[[nodiscard]] QColor strokeColor() const;\n\t[[nodiscard]] float64 arrowHeadLength() const;\n\n\tbool _drawing = false;\n\tstd::vector<StrokePoint> _currentStroke;\n\tint _lastRenderedIndex = 0;\n\tfloat64 _zoom = 1.0;\n\tint64 _lastPointTime = 0;\n\n\tstd::unique_ptr<PainterHighQualityEnabler> _hq;\n\tstd::unique_ptr<Painter> _p;\n\n\tQRectF _rectToUpdate;\n\tQRectF _contentRect;\n\tQMarginsF _brushMargins;\n\n\tQPointF _lastPoint;\n\n\tQPixmap _pixmap;\n\tQPainterPath _currentPath;\n\n\tstruct {\n\t\tfloat size = 1.;\n\t\tQColor color;\n\t\tBrush::Tool tool = Brush::Tool::Pen;\n\t} _brushData;\n\n\trpl::event_stream<Content> _grabContentRequests;\n\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/scene/scene_item_image.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/scene/scene_item_image.h\"\n\nnamespace Editor {\n\nItemImage::ItemImage(\n\tQPixmap &&pixmap,\n\tItemBase::Data data)\n: ItemBase(std::move(data))\n, _pixmap(std::move(pixmap)) {\n\tsetAspectRatio(_pixmap.isNull()\n\t\t? 1.0\n\t\t: (_pixmap.height() / float64(_pixmap.width())));\n}\n\nvoid ItemImage::paint(\n\t\tQPainter *p,\n\t\tconst QStyleOptionGraphicsItem *option,\n\t\tQWidget *w) {\n\tconst auto rect = contentRect();\n\tconst auto pixmapSize = QSizeF(_pixmap.size() / style::DevicePixelRatio())\n\t\t.scaled(rect.size(), Qt::KeepAspectRatio);\n\tconst auto resultRect = QRectF(rect.topLeft(), pixmapSize).translated(\n\t\t(rect.width() - pixmapSize.width()) / 2.,\n\t\t(rect.height() - pixmapSize.height()) / 2.);\n\tp->drawPixmap(resultRect.toRect(), _pixmap);\n\tItemBase::paint(p, option, w);\n}\n\nvoid ItemImage::performFlip() {\n\t_pixmap = _pixmap.transformed(QTransform().scale(-1, 1));\n\tupdate();\n}\n\nstd::shared_ptr<ItemBase> ItemImage::duplicate(ItemBase::Data data) const {\n\tauto pixmap = _pixmap;\n\treturn std::make_shared<ItemImage>(std::move(pixmap), std::move(data));\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/scene/scene_item_image.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"editor/scene/scene_item_base.h\"\n\nnamespace Editor {\n\nclass ItemImage : public ItemBase {\npublic:\n\tItemImage(\n\t\tQPixmap &&pixmap,\n\t\tItemBase::Data data);\n\tvoid paint(\n\t\tQPainter *p,\n\t\tconst QStyleOptionGraphicsItem *option,\n\t\tQWidget *widget) override;\nprotected:\n\tvoid performFlip() override;\n\tstd::shared_ptr<ItemBase> duplicate(ItemBase::Data data) const override;\nprivate:\n\tQPixmap _pixmap;\n\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/scene/scene_item_line.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/scene/scene_item_line.h\"\n\n#include <QGraphicsScene>\n#include <QtGui/QPainter>\n\nnamespace Editor {\n\nItemLine::ItemLine(QPixmap &&pixmap)\n: _pixmap(std::move(pixmap))\n, _rect(QPointF(), _pixmap.size() / float64(style::DevicePixelRatio())) {\n}\n\nQRectF ItemLine::boundingRect() const {\n\treturn _rect;\n}\n\nvoid ItemLine::paint(\n\t\tQPainter *p,\n\t\tconst QStyleOptionGraphicsItem *,\n\t\tQWidget *) {\n\tp->drawPixmap(0, 0, _pixmap);\n}\n\nconst QPixmap &ItemLine::pixmap() const {\n\treturn _pixmap;\n}\n\nvoid ItemLine::setPixmap(QPixmap pixmap) {\n\t_pixmap = std::move(pixmap);\n\tupdate();\n}\n\nbool ItemLine::applyEraser(const QPixmap &mask, const QPointF &maskPos) {\n\tif (mask.isNull()) {\n\t\treturn false;\n\t}\n\tconst auto maskSize = mask.size() / float64(mask.devicePixelRatio());\n\tconst auto localTopLeft = maskPos - pos();\n\tconst auto localRect = QRectF(localTopLeft, maskSize);\n\tif (!localRect.intersects(_rect)) {\n\t\treturn false;\n\t}\n\tauto image = _pixmap.toImage().convertToFormat(\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tauto p = QPainter(&image);\n\tp.setCompositionMode(QPainter::CompositionMode_DestinationOut);\n\tp.drawPixmap(localTopLeft, mask);\n\tp.end();\n\t_pixmap = QPixmap::fromImage(std::move(image));\n\tupdate();\n\treturn true;\n}\n\nbool ItemLine::collidesWithItem(\n\t\tconst QGraphicsItem *,\n\t\tQt::ItemSelectionMode) const {\n\treturn false;\n}\nbool ItemLine::collidesWithPath(\n\t\tconst QPainterPath &,\n\t\tQt::ItemSelectionMode) const {\n\treturn false;\n}\n\nvoid ItemLine::save(SaveState state) {\n\tauto &saved = (state == SaveState::Keep) ? _keeped : _saved;\n\tsaved = {\n\t\t.saved = true,\n\t\t.status = status(),\n\t};\n}\n\nvoid ItemLine::restore(SaveState state) {\n\tif (!hasState(state)) {\n\t\treturn;\n\t}\n\tconst auto &saved = (state == SaveState::Keep) ? _keeped : _saved;\n\tsetStatus(saved.status);\n}\n\nbool ItemLine::hasState(SaveState state) const {\n\tconst auto &saved = (state == SaveState::Keep) ? _keeped : _saved;\n\treturn saved.saved;\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/scene/scene_item_line.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"editor/scene/scene_item_base.h\"\n\nnamespace Editor {\n\nclass ItemLine : public NumberedItem {\npublic:\n\tItemLine(QPixmap &&pixmap);\n\tQRectF boundingRect() const override;\n\tvoid paint(\n\t\tQPainter *p,\n\t\tconst QStyleOptionGraphicsItem *option,\n\t\tQWidget *widget) override;\n\t[[nodiscard]] const QPixmap &pixmap() const;\n\tvoid setPixmap(QPixmap pixmap);\n\tbool applyEraser(const QPixmap &mask, const QPointF &maskPos);\n\n\tbool hasState(SaveState state) const override;\n\tvoid save(SaveState state) override;\n\tvoid restore(SaveState state) override;\nprotected:\n\tbool collidesWithItem(\n\t\tconst QGraphicsItem *,\n\t\tQt::ItemSelectionMode) const override;\n\tbool collidesWithPath(\n\t\tconst QPainterPath &,\n\t\tQt::ItemSelectionMode) const override;\nprivate:\n\tQPixmap _pixmap;\n\tconst QRectF _rect;\n\n\tstruct {\n\t\tbool saved = false;\n\t\tNumberedItem::Status status;\n\t} _saved, _keeped;\n\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/scene/scene_item_sticker.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"editor/scene/scene_item_sticker.h\"\n\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_session.h\"\n#include \"lottie/lottie_common.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"main/main_session.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_editor.h\"\n\nnamespace Editor {\nnamespace {\n\n} // namespace\n\nItemSticker::ItemSticker(\n\tnot_null<DocumentData*> document,\n\tItemBase::Data data)\n: ItemBase(std::move(data))\n, _document(document)\n, _mediaView(_document->createMediaView()) {\n\tconst auto stickerData = document->sticker();\n\tif (!stickerData) {\n\t\treturn;\n\t}\n\tconst auto updateThumbnail = [=] {\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\tif (_image.isNull()) {\n\t\t\t\tsetAspectRatio(1.);\n\t\t\t}\n\t\t});\n\t\tif (stickerData->isLottie()) {\n\t\t\t_lottie.player = ChatHelpers::LottiePlayerFromDocument(\n\t\t\t\t_mediaView.get(),\n\t\t\t\tChatHelpers::StickerLottieSize::MessageHistory,\n\t\t\t\tQSize(kStickerSideSize, kStickerSideSize)\n\t\t\t\t\t* style::DevicePixelRatio(),\n\t\t\t\tLottie::Quality::High);\n\t\t\t_lottie.player->updates(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tupdatePixmap(_lottie.player->frame());\n\t\t\t\t_lottie.player = nullptr;\n\t\t\t\t_lottie.lifetime.destroy();\n\t\t\t\tupdate();\n\t\t\t}, _lottie.lifetime);\n\t\t\treturn true;\n\t\t} else if (stickerData->isWebm()\n\t\t\t&& !_document->dimensions.isEmpty()) {\n\t\t\tconst auto callback = [=](::Media::Clip::Notification) {\n\t\t\t\tconst auto size = _document->dimensions;\n\t\t\t\tif (_webm && _webm->ready() && !_webm->started()) {\n\t\t\t\t\t_webm->start({ .frame = size, .keepAlpha = true });\n\t\t\t\t}\n\t\t\t\tif (_webm && _webm->started()) {\n\t\t\t\t\tupdatePixmap(_webm->current(\n\t\t\t\t\t\t{ .frame = size, .keepAlpha = true },\n\t\t\t\t\t\t0));\n\t\t\t\t\t_webm = nullptr;\n\t\t\t\t}\n\t\t\t};\n\t\t\t_webm = ::Media::Clip::MakeReader(\n\t\t\t\t_mediaView->owner()->location(),\n\t\t\t\t_mediaView->bytes(),\n\t\t\t\tcallback);\n\t\t\treturn true;\n\t\t}\n\t\tconst auto sticker = _mediaView->getStickerLarge();\n\t\tif (!sticker) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tauto pixmap = sticker->pixNoCache(sticker->size() * ratio);\n\t\tpixmap.setDevicePixelRatio(ratio);\n\t\tupdatePixmap(pixmap.toImage());\n\t\treturn true;\n\t};\n\tif (!updateThumbnail()) {\n\t\t_document->session().downloaderTaskFinished(\n\t\t) | rpl::on_next([=] {\n\t\t\tif (updateThumbnail()) {\n\t\t\t\t_loadingLifetime.destroy();\n\t\t\t\tupdate();\n\t\t\t}\n\t\t}, _loadingLifetime);\n\t}\n}\n\nvoid ItemSticker::updatePixmap(QImage &&image) {\n\t_image = std::move(image);\n\tif (flipped()) {\n\t\tperformFlip();\n\t} else {\n\t\tupdate();\n\t}\n\tif (!_image.isNull()) {\n\t\tsetAspectRatio(_image.height() / float64(_image.width()));\n\t}\n}\n\nvoid ItemSticker::paint(\n\t\tQPainter *p,\n\t\tconst QStyleOptionGraphicsItem *option,\n\t\tQWidget *w) {\n\tconst auto rect = contentRect();\n\tconst auto imageSize = QSizeF(_image.size() / style::DevicePixelRatio())\n\t\t.scaled(rect.size(), Qt::KeepAspectRatio);\n\tconst auto resultRect = QRectF(rect.topLeft(), imageSize).translated(\n\t\t(rect.width() - imageSize.width()) / 2.,\n\t\t(rect.height() - imageSize.height()) / 2.);\n\tp->drawImage(resultRect, _image);\n\tItemBase::paint(p, option, w);\n}\n\nnot_null<DocumentData*> ItemSticker::sticker() const {\n\treturn _document;\n}\n\nint ItemSticker::type() const {\n\treturn Type;\n}\n\nvoid ItemSticker::performFlip() {\n\t_image = _image.transformed(QTransform().scale(-1, 1));\n\tupdate();\n}\n\nstd::shared_ptr<ItemBase> ItemSticker::duplicate(ItemBase::Data data) const {\n\treturn std::make_shared<ItemSticker>(_document, std::move(data));\n}\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/editor/scene/scene_item_sticker.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"editor/scene/scene_item_base.h\"\n#include \"media/clip/media_clip_reader.h\"\n\nnamespace Data {\nclass DocumentMedia;\n} // namespace Data\nnamespace Lottie {\nclass SinglePlayer;\n} // namespace Lottie\nclass DocumentData;\n\nnamespace Editor {\n\nclass ItemSticker : public ItemBase {\npublic:\n\tenum { Type = ItemBase::Type + 1 };\n\n\tItemSticker(\n\t\tnot_null<DocumentData*> document,\n\t\tItemBase::Data data);\n\tvoid paint(\n\t\tQPainter *p,\n\t\tconst QStyleOptionGraphicsItem *option,\n\t\tQWidget *widget) override;\n\t[[nodiscard]] not_null<DocumentData*> sticker() const;\n\tint type() const override;\n\nprotected:\n\tvoid performFlip() override;\n\tstd::shared_ptr<ItemBase> duplicate(ItemBase::Data data) const override;\n\nprivate:\n\tconst not_null<DocumentData*> _document;\n\tconst std::shared_ptr<::Data::DocumentMedia> _mediaView;\n\n\tvoid updatePixmap(QImage &&image);\n\n\tstruct {\n\t\tstd::unique_ptr<Lottie::SinglePlayer> player;\n\t\trpl::lifetime lifetime;\n\t} _lottie;\n\t::Media::Clip::ReaderPointer _webm;\n\tQImage _image;\n\n\trpl::lifetime _loadingLifetime;\n\n};\n\n} // namespace Editor\n"
  },
  {
    "path": "Telegram/SourceFiles/export/data/export_data_types.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/data/export_data_types.h\"\n\n#include \"export/export_settings.h\"\n#include \"export/output/export_output_file.h\"\n#include \"base/base_file_utilities.h\"\n#include \"ui/text/format_values.h\"\n#include \"core/mime_type.h\"\n#include \"core/utils.h\"\n#include <QtCore/QDateTime>\n#include <QtCore/QTimeZone>\n#include <QtCore/QRegularExpression>\n#include <QtGui/QImageReader>\n#include <range/v3/algorithm/max_element.hpp>\n#include <range/v3/view/all.hpp>\n#include <range/v3/view/transform.hpp>\n#include <range/v3/range/conversion.hpp>\n\nnamespace Export {\nnamespace Data {\nnamespace {\n\nconstexpr auto kMaxImageSize = 10000;\nconstexpr auto kMigratedMessagesIdShift = -1'000'000'000;\n\nQString PrepareFileNameDatePart(TimeId date) {\n\treturn date\n\t\t? ('@' + QString::fromUtf8(FormatDateTime(date, 0, '-', '-', '_')))\n\t\t: QString();\n}\n\nQString PreparePhotoFileName(int index, TimeId date) {\n\treturn \"photo_\"\n\t\t+ QString::number(index)\n\t\t+ PrepareFileNameDatePart(date)\n\t\t+ \".jpg\";\n}\n\nQString PrepareStoryFileName(\n\t\tint index,\n\t\tTimeId date,\n\t\tconst Utf8String &extension) {\n\treturn \"story_\"\n\t\t+ QString::number(index)\n\t\t+ PrepareFileNameDatePart(date)\n\t\t+ extension;\n}\n\nstd::vector<std::vector<HistoryMessageMarkupButton>> ButtonRowsFromTL(\n\t\tconst MTPDreplyInlineMarkup &data) {\n\tconst auto &list = data.vrows().v;\n\n\tif (list.isEmpty()) {\n\t\treturn {};\n\t}\n\tusing Type = HistoryMessageMarkupButton::Type;\n\tauto rows = std::vector<std::vector<HistoryMessageMarkupButton>>();\n\trows.reserve(list.size());\n\n\tfor (const auto &tlRow : list) {\n\t\tauto row = std::vector<HistoryMessageMarkupButton>();\n\t\trow.reserve(tlRow.data().vbuttons().v.size());\n\t\tfor (const auto &button : tlRow.data().vbuttons().v) {\n\t\t\tbutton.match([&](const MTPDkeyboardButton &data) {\n\t\t\t\trow.push_back({ Type::Default, qs(data.vtext()) });\n\t\t\t}, [&](const MTPDkeyboardButtonCallback &data) {\n\t\t\t\trow.push_back({\n\t\t\t\t\t(data.is_requires_password()\n\t\t\t\t\t\t? Type::CallbackWithPassword\n\t\t\t\t\t\t: Type::Callback),\n\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\tqba(data.vdata())\n\t\t\t\t});\n\t\t\t}, [&](const MTPDkeyboardButtonRequestGeoLocation &data) {\n\t\t\t\trow.push_back({ Type::RequestLocation, qs(data.vtext()) });\n\t\t\t}, [&](const MTPDkeyboardButtonRequestPhone &data) {\n\t\t\t\trow.push_back({ Type::RequestPhone, qs(data.vtext()) });\n\t\t\t}, [&](const MTPDkeyboardButtonRequestPeer &data) {\n\t\t\t\trow.push_back({\n\t\t\t\t\tType::RequestPeer,\n\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\tQByteArray(\"unsupported\"),\n\t\t\t\t\tQString(),\n\t\t\t\t\tint64(data.vbutton_id().v),\n\t\t\t\t});\n\t\t\t}, [&](const MTPDkeyboardButtonUrl &data) {\n\t\t\t\trow.push_back({\n\t\t\t\t\tType::Url,\n\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\tqba(data.vurl())\n\t\t\t\t});\n\t\t\t}, [&](const MTPDkeyboardButtonSwitchInline &data) {\n\t\t\t\tconst auto type = data.is_same_peer()\n\t\t\t\t\t? Type::SwitchInlineSame\n\t\t\t\t\t: Type::SwitchInline;\n\t\t\t\trow.push_back({ type, qs(data.vtext()), qba(data.vquery()) });\n\t\t\t}, [&](const MTPDkeyboardButtonGame &data) {\n\t\t\t\trow.push_back({ Type::Game, qs(data.vtext()) });\n\t\t\t}, [&](const MTPDkeyboardButtonBuy &data) {\n\t\t\t\trow.push_back({ Type::Buy, qs(data.vtext()) });\n\t\t\t}, [&](const MTPDkeyboardButtonUrlAuth &data) {\n\t\t\t\trow.push_back({\n\t\t\t\t\tType::Auth,\n\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\tqba(data.vurl()),\n\t\t\t\t\tqs(data.vfwd_text().value_or_empty()),\n\t\t\t\t\tdata.vbutton_id().v,\n\t\t\t\t});\n\t\t\t}, [&](const MTPDkeyboardButtonRequestPoll &data) {\n\t\t\t\tconst auto quiz = [&] {\n\t\t\t\t\tif (!data.vquiz()) {\n\t\t\t\t\t\treturn QByteArray();\n\t\t\t\t\t}\n\t\t\t\t\treturn data.vquiz()->match([&](const MTPDboolTrue&) {\n\t\t\t\t\t\treturn QByteArray(1, 1);\n\t\t\t\t\t}, [&](const MTPDboolFalse&) {\n\t\t\t\t\t\treturn QByteArray(1, 0);\n\t\t\t\t\t});\n\t\t\t\t}();\n\t\t\t\trow.push_back({\n\t\t\t\t\tType::RequestPoll,\n\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\tquiz\n\t\t\t\t});\n\t\t\t}, [&](const MTPDkeyboardButtonUserProfile &data) {\n\t\t\t\trow.push_back({\n\t\t\t\t\tType::UserProfile,\n\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\tQByteArray::number(data.vuser_id().v)\n\t\t\t\t});\n\t\t\t}, [&](const MTPDinputKeyboardButtonUrlAuth &data) {\n\t\t\t}, [&](const MTPDinputKeyboardButtonUserProfile &data) {\n\t\t\t}, [&](const MTPDkeyboardButtonWebView &data) {\n\t\t\t\trow.push_back({\n\t\t\t\t\tType::WebView,\n\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\tdata.vurl().v\n\t\t\t\t});\n\t\t\t}, [&](const MTPDkeyboardButtonSimpleWebView &data) {\n\t\t\t\trow.push_back({\n\t\t\t\t\tType::SimpleWebView,\n\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\tdata.vurl().v\n\t\t\t\t});\n\t\t\t}, [&](const MTPDkeyboardButtonCopy &data) {\n\t\t\t\trow.push_back({\n\t\t\t\t\tType::CopyText,\n\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\tdata.vcopy_text().v,\n\t\t\t\t});\n\t\t\t}, [&](const MTPDinputKeyboardButtonRequestPeer &data) {\n\t\t\t});\n\t\t}\n\t\tif (!row.empty()) {\n\t\t\trows.push_back(std::move(row));\n\t\t}\n\t}\n\n\treturn rows;\n}\n\n} // namespace\n\nQByteArray HistoryMessageMarkupButton::TypeToString(\n\t\tconst HistoryMessageMarkupButton &button) {\n\tusing Type = HistoryMessageMarkupButton::Type;\n\tswitch (button.type) {\n\tcase Type::Default: return \"default\";\n\tcase Type::Url: return \"url\";\n\tcase Type::Callback: return \"callback\";\n\tcase Type::CallbackWithPassword: return \"callback_with_password\";\n\tcase Type::RequestPhone: return \"request_phone\";\n\tcase Type::RequestLocation: return \"request_location\";\n\tcase Type::RequestPoll: return \"request_poll\";\n\tcase Type::RequestPeer: return \"request_peer\";\n\tcase Type::SwitchInline: return \"switch_inline\";\n\tcase Type::SwitchInlineSame: return \"switch_inline_same\";\n\tcase Type::Game: return \"game\";\n\tcase Type::Buy: return \"buy\";\n\tcase Type::Auth: return \"auth\";\n\tcase Type::UserProfile: return \"user_profile\";\n\tcase Type::WebView: return \"web_view\";\n\tcase Type::SimpleWebView: return \"simple_web_view\";\n\tcase Type::CopyText: return \"copy_text\";\n\t}\n\tUnexpected(\"Type in HistoryMessageMarkupButton::Type.\");\n}\n\nuint8 PeerColorIndex(BareId bareId) {\n\tconst uint8 map[] = { 0, 7, 4, 1, 6, 3, 5 };\n\treturn map[bareId % base::array_size(map)];\n}\n\nBareId PeerToBareId(PeerId peerId) {\n\treturn (peerId.value & PeerId::kChatTypeMask);\n}\n\nuint8 PeerColorIndex(PeerId peerId) {\n\treturn PeerColorIndex(PeerToBareId(peerId));\n}\n\nBareId StringBarePeerId(const Utf8String &data) {\n\tauto result = BareId(0xFF);\n\tfor (const auto ch : data) {\n\t\tresult *= 239;\n\t\tresult += ch;\n\t\tresult &= 0xFF;\n\t}\n\treturn result;\n}\n\nuint8 ApplicationColorIndex(int applicationId) {\n\tstatic const auto official = std::map<int, int> {\n\t\t{ 1, 0 }, // iOS\n\t\t{ 7, 0 }, // iOS X\n\t\t{ 6, 1 }, // Android\n\t\t{ 21724, 1 }, // Android X\n\t\t{ 2834, 2 }, // macOS\n\t\t{ 2496, 3 }, // Webogram\n\t\t{ 2040, 4 }, // Desktop\n\t\t{ 1429, 5 }, // Windows Phone\n\t};\n\tif (const auto i = official.find(applicationId); i != end(official)) {\n\t\treturn i->second;\n\t}\n\treturn PeerColorIndex(applicationId);\n}\n\nint DomainApplicationId(const Utf8String &data) {\n\treturn 0x1000 + StringBarePeerId(data);\n}\n\nPeerId ParsePeerId(const MTPPeer &data) {\n\treturn peerFromMTP(data);\n}\n\nUtf8String ParseString(const MTPstring &data) {\n\treturn data.v;\n}\n\nstd::vector<TextPart> ParseText(\n\t\tconst MTPstring &data,\n\t\tconst QVector<MTPMessageEntity> &entities) {\n\tusing Type = TextPart::Type;\n\tconst auto text = QString::fromUtf8(data.v);\n\tconst auto size = data.v.size();\n\tconst auto mid = [&](int offset, int length) {\n\t\treturn text.mid(offset, length).toUtf8();\n\t};\n\tauto result = std::vector<TextPart>();\n\tauto offset = 0;\n\tauto addTextPart = [&](int till) {\n\t\tif (till > offset) {\n\t\t\tauto part = TextPart();\n\t\t\tpart.text = mid(offset, till - offset);\n\t\t\tresult.push_back(std::move(part));\n\t\t\toffset = till;\n\t\t}\n\t};\n\tfor (const auto &entity : entities) {\n\t\tconst auto start = entity.match([](const auto &data) {\n\t\t\treturn data.voffset().v;\n\t\t});\n\t\tconst auto length = entity.match([](const auto &data) {\n\t\t\treturn data.vlength().v;\n\t\t});\n\n\t\tif (start < offset || length <= 0 || start + length > size) {\n\t\t\tcontinue;\n\t\t}\n\n\t\taddTextPart(start);\n\n\t\tauto part = TextPart();\n\t\tpart.type = entity.match(\n\t\t\t[](const MTPDmessageEntityUnknown&) { return Type::Unknown; },\n\t\t\t[](const MTPDmessageEntityMention&) { return Type::Mention; },\n\t\t\t[](const MTPDmessageEntityHashtag&) { return Type::Hashtag; },\n\t\t\t[](const MTPDmessageEntityBotCommand&) {\n\t\t\t\treturn Type::BotCommand; },\n\t\t\t[](const MTPDmessageEntityUrl&) { return Type::Url; },\n\t\t\t[](const MTPDmessageEntityEmail&) { return Type::Email; },\n\t\t\t[](const MTPDmessageEntityBold&) { return Type::Bold; },\n\t\t\t[](const MTPDmessageEntityItalic&) { return Type::Italic; },\n\t\t\t[](const MTPDmessageEntityCode&) { return Type::Code; },\n\t\t\t[](const MTPDmessageEntityPre&) { return Type::Pre; },\n\t\t\t[](const MTPDmessageEntityTextUrl&) { return Type::TextUrl; },\n\t\t\t[](const MTPDmessageEntityMentionName&) {\n\t\t\t\treturn Type::MentionName; },\n\t\t\t[](const MTPDinputMessageEntityMentionName&) {\n\t\t\t\treturn Type::MentionName; },\n\t\t\t[](const MTPDmessageEntityPhone&) { return Type::Phone; },\n\t\t\t[](const MTPDmessageEntityCashtag&) { return Type::Cashtag; },\n\t\t\t[](const MTPDmessageEntityUnderline&) { return Type::Underline; },\n\t\t\t[](const MTPDmessageEntityStrike&) { return Type::Strike; },\n\t\t\t[](const MTPDmessageEntityBlockquote&) {\n\t\t\t\treturn Type::Blockquote; },\n\t\t\t[](const MTPDmessageEntityBankCard&) { return Type::BankCard; },\n\t\t\t[](const MTPDmessageEntitySpoiler&) { return Type::Spoiler; },\n\t\t\t[](const MTPDmessageEntityCustomEmoji&) { return Type::CustomEmoji; },\n\t\t\t[](const MTPDmessageEntityFormattedDate&) { return Type::Unknown; },\n\t\t\t[](const MTPDmessageEntityDiffInsert&) { return Type::Unknown; },\n\t\t\t[](const MTPDmessageEntityDiffReplace&) { return Type::Unknown; },\n\t\t\t[](const MTPDmessageEntityDiffDelete&) { return Type::Unknown; });\n\t\tpart.text = mid(start, length);\n\t\tpart.additional = entity.match(\n\t\t[](const MTPDmessageEntityPre &data) {\n\t\t\treturn ParseString(data.vlanguage());\n\t\t}, [](const MTPDmessageEntityTextUrl &data) {\n\t\t\treturn ParseString(data.vurl());\n\t\t}, [](const MTPDmessageEntityMentionName &data) {\n\t\t\treturn NumberToString(data.vuser_id().v);\n\t\t}, [](const MTPDmessageEntityCustomEmoji &data) {\n\t\t\treturn NumberToString(data.vdocument_id().v);\n\t\t}, [](const MTPDmessageEntityBlockquote &data) {\n\t\t\treturn data.is_collapsed() ? Utf8String(\"1\") : Utf8String();\n\t\t}, [](const auto &) { return Utf8String(); });\n\n\t\tresult.push_back(std::move(part));\n\t\toffset = start + length;\n\t}\n\taddTextPart(size);\n\treturn result;\n}\n\nUtf8String Reaction::TypeToString(const Reaction &reaction) {\n\tswitch (reaction.type) {\n\t\tcase Reaction::Type::Empty: return \"empty\";\n\t\tcase Reaction::Type::Emoji: return \"emoji\";\n\t\tcase Reaction::Type::CustomEmoji: return \"custom_emoji\";\n\t\tcase Reaction::Type::Paid: return \"paid\";\n\t}\n\tUnexpected(\"Type in Reaction::Type.\");\n}\n\nstd::vector<TextPart> ParseText(const MTPTextWithEntities &text) {\n\treturn ParseText(text.data().vtext(), text.data().ventities().v);\n}\n\nUtf8String Reaction::Id(const Reaction &reaction) {\n\tauto id = Utf8String();\n\tswitch (reaction.type) {\n\tcase Reaction::Type::Emoji:\n\t\tid = reaction.emoji.toUtf8();\n\t\tbreak;\n\tcase Reaction::Type::CustomEmoji:\n\t\tid = reaction.documentId;\n\t\tbreak;\n\t}\n\treturn Reaction::TypeToString(reaction) + id;\n}\n\nReaction ParseReaction(const MTPReaction& reaction) {\n\tauto result = Reaction();\n\treaction.match([&](const MTPDreactionEmoji &data) {\n\t\tresult.type = Reaction::Type::Emoji;\n\t\tresult.emoji = qs(data.vemoticon());\n\t}, [&](const MTPDreactionCustomEmoji &data) {\n\t\tresult.type = Reaction::Type::CustomEmoji;\n\t\tresult.documentId = NumberToString(data.vdocument_id().v);\n\t}, [&](const MTPDreactionPaid &data) {\n\t\tresult.type = Reaction::Type::Paid;\n\t}, [&](const MTPDreactionEmpty &data) {\n\t\tresult.type = Reaction::Type::Empty;\n\t});\n\treturn result;\n}\n\nstd::vector<Reaction> ParseReactions(const MTPMessageReactions &data) {\n\tauto reactionsMap = std::map<QString, Reaction>();\n\tauto reactionsOrder = std::vector<Utf8String>();\n\tfor (const auto &single : data.data().vresults().v) {\n\t\tauto reaction = ParseReaction(single.data().vreaction());\n\t\treaction.count = single.data().vcount().v;\n\t\tconst auto id = Reaction::Id(reaction);\n\t\tauto const &[_, inserted] = reactionsMap.try_emplace(\n\t\t\tid,\n\t\t\tstd::move(reaction));\n\t\tif (inserted) {\n\t\t\treactionsOrder.push_back(id);\n\t\t}\n\t}\n\tif (data.data().vrecent_reactions().has_value()) {\n\t\tif (const auto list = data.data().vrecent_reactions()) {\n\t\t\tfor (const auto &single : list->v) {\n\t\t\t\tauto reaction = ParseReaction(single.data().vreaction());\n\t\t\t\tconst auto id = Reaction::Id(reaction);\n\t\t\t\tauto const &[it, inserted] = reactionsMap.try_emplace(\n\t\t\t\t\tid,\n\t\t\t\t\tstd::move(reaction));\n\t\t\t\tif (inserted) {\n\t\t\t\t\treactionsOrder.push_back(id);\n\t\t\t\t}\n\t\t\t\tit->second.recent.push_back({\n\t\t\t\t\t.peerId = ParsePeerId(single.data().vpeer_id()),\n\t\t\t\t\t.date = single.data().vdate().v,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\treturn ranges::views::all(\n\t\treactionsOrder\n\t) | ranges::views::transform([&](const Utf8String &id) {\n\t\treturn reactionsMap.at(id);\n\t}) | ranges::to_vector;\n}\n\nUtf8String FillLeft(const Utf8String &data, int length, char filler) {\n\tif (length <= data.size()) {\n\t\treturn data;\n\t}\n\tauto result = Utf8String();\n\tresult.reserve(length);\n\tfor (auto i = 0, count = length - int(data.size()); i != count; ++i) {\n\t\tresult.append(filler);\n\t}\n\tresult.append(data);\n\treturn result;\n}\n\nbool RefreshFileReference(FileLocation &to, const FileLocation &from) {\n\tif (to.dcId != from.dcId || to.data.type() != from.data.type()) {\n\t\treturn false;\n\t}\n\tif (to.data.type() == mtpc_inputPhotoFileLocation) {\n\t\tconst auto &toData = to.data.c_inputPhotoFileLocation();\n\t\tconst auto &fromData = from.data.c_inputPhotoFileLocation();\n\t\tif (toData.vid().v != fromData.vid().v\n\t\t\t|| toData.vthumb_size().v != fromData.vthumb_size().v) {\n\t\t\treturn false;\n\t\t}\n\t\tto = from;\n\t\treturn true;\n\t} else if (to.data.type() == mtpc_inputDocumentFileLocation) {\n\t\tconst auto &toData = to.data.c_inputDocumentFileLocation();\n\t\tconst auto &fromData = from.data.c_inputDocumentFileLocation();\n\t\tif (toData.vid().v != fromData.vid().v\n\t\t\t|| toData.vthumb_size().v != fromData.vthumb_size().v) {\n\t\t\treturn false;\n\t\t}\n\t\tto = from;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nImage ParseMaxImage(\n\t\tconst MTPDphoto &photo,\n\t\tconst QString &suggestedPath) {\n\tauto result = Image();\n\tresult.file.suggestedPath = suggestedPath;\n\n\tauto maxArea = int64(0);\n\tfor (const auto &size : photo.vsizes().v) {\n\t\tsize.match([](const MTPDphotoSizeEmpty &) {\n\t\t}, [](const MTPDphotoStrippedSize &) {\n\t\t\t// Max image size should not be a stripped image.\n\t\t}, [](const MTPDphotoPathSize &) {\n\t\t\t// Max image size should not be a path image.\n\t\t}, [&](const auto &data) {\n\t\t\tconst auto area = data.vw().v * int64(data.vh().v);\n\t\t\tif (area > maxArea) {\n\t\t\t\tresult.width = data.vw().v;\n\t\t\t\tresult.height = data.vh().v;\n\t\t\t\tresult.file.location = FileLocation{\n\t\t\t\t\tphoto.vdc_id().v,\n\t\t\t\t\tMTP_inputPhotoFileLocation(\n\t\t\t\t\t\tphoto.vid(),\n\t\t\t\t\t\tphoto.vaccess_hash(),\n\t\t\t\t\t\tphoto.vfile_reference(),\n\t\t\t\t\t\tdata.vtype()) };\n\t\t\t\tif constexpr (MTPDphotoCachedSize::Is<decltype(data)>()) {\n\t\t\t\t\tresult.file.content = data.vbytes().v;\n\t\t\t\t\tresult.file.size = result.file.content.size();\n\t\t\t\t} else if constexpr (MTPDphotoSizeProgressive::Is<decltype(data)>()) {\n\t\t\t\t\tif (data.vsizes().v.isEmpty()) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tresult.file.content = QByteArray();\n\t\t\t\t\tresult.file.size = data.vsizes().v.back().v;\n\t\t\t\t} else {\n\t\t\t\t\tresult.file.content = QByteArray();\n\t\t\t\t\tresult.file.size = data.vsize().v;\n\t\t\t\t}\n\t\t\t\tmaxArea = area;\n\t\t\t}\n\t\t});\n\t}\n\treturn result;\n}\n\nPhoto ParsePhoto(const MTPPhoto &data, const QString &suggestedPath) {\n\tauto result = Photo();\n\tdata.match([&](const MTPDphoto &data) {\n\t\tresult.id = data.vid().v;\n\t\tresult.date = data.vdate().v;\n\t\tresult.image = ParseMaxImage(data, suggestedPath);\n\t}, [&](const MTPDphotoEmpty &data) {\n\t\tresult.id = data.vid().v;\n\t});\n\treturn result;\n}\n\nvoid ParseAttributes(\n\t\tDocument &result,\n\t\tconst MTPVector<MTPDocumentAttribute> &attributes) {\n\tfor (const auto &value : attributes.v) {\n\t\tvalue.match([&](const MTPDdocumentAttributeImageSize &data) {\n\t\t\tresult.width = data.vw().v;\n\t\t\tresult.height = data.vh().v;\n\t\t}, [&](const MTPDdocumentAttributeAnimated &data) {\n\t\t\tresult.isAnimated = true;\n\t\t}, [&](const MTPDdocumentAttributeSticker &data) {\n\t\t\tresult.isSticker = true;\n\t\t\tresult.stickerEmoji = ParseString(data.valt());\n\t\t}, [&](const MTPDdocumentAttributeCustomEmoji &data) {\n\t\t\tresult.isSticker = true;\n\t\t\tresult.stickerEmoji = ParseString(data.valt());\n\t\t}, [&](const MTPDdocumentAttributeVideo &data) {\n\t\t\tif (data.is_round_message()) {\n\t\t\t\tresult.isVideoMessage = true;\n\t\t\t} else {\n\t\t\t\tresult.isVideoFile = true;\n\t\t\t}\n\t\t\tresult.width = data.vw().v;\n\t\t\tresult.height = data.vh().v;\n\t\t\tresult.duration = int(data.vduration().v);\n\t\t}, [&](const MTPDdocumentAttributeAudio &data) {\n\t\t\tif (data.is_voice()) {\n\t\t\t\tresult.isVoiceMessage = true;\n\t\t\t} else {\n\t\t\t\tresult.isAudioFile = true;\n\t\t\t}\n\t\t\tif (const auto performer = data.vperformer()) {\n\t\t\t\tresult.songPerformer = ParseString(*performer);\n\t\t\t}\n\t\t\tif (const auto title = data.vtitle()) {\n\t\t\t\tresult.songTitle = ParseString(*title);\n\t\t\t}\n\t\t\tresult.duration = data.vduration().v;\n\t\t}, [&](const MTPDdocumentAttributeFilename &data) {\n\t\t\tresult.name = ParseString(data.vfile_name());\n\t\t}, [&](const MTPDdocumentAttributeHasStickers &data) {\n\t\t});\n\t}\n}\n\nQString ComputeDocumentName(\n\t\tParseMediaContext &context,\n\t\tconst Document &data,\n\t\tTimeId date) {\n\tif (!data.name.isEmpty()) {\n\t\treturn QString::fromUtf8(data.name);\n\t}\n\tconst auto mimeString = QString::fromUtf8(data.mime);\n\tconst auto mimeType = Core::MimeTypeForName(mimeString);\n\tconst auto hasMimeType = [&](const auto &mime) {\n\t\treturn !mimeString.compare(mime, Qt::CaseInsensitive);\n\t};\n\tconst auto patterns = mimeType.globPatterns();\n\tconst auto pattern = patterns.isEmpty() ? QString() : patterns.front();\n\tif (data.isVoiceMessage) {\n\t\tconst auto isMP3 = hasMimeType(u\"audio/mp3\"_q);\n\t\treturn u\"audio_\"_q\n\t\t\t+ QString::number(++context.audios)\n\t\t\t+ PrepareFileNameDatePart(date)\n\t\t\t+ (isMP3 ? u\".mp3\"_q : u\".ogg\"_q);\n\t} else if (data.isVideoFile) {\n\t\tconst auto extension = pattern.isEmpty()\n\t\t\t? u\".mov\"_q\n\t\t\t: QString(pattern).replace('*', QString());\n\t\treturn u\"video_\"_q\n\t\t\t+ QString::number(++context.videos)\n\t\t\t+ PrepareFileNameDatePart(date)\n\t\t\t+ extension;\n\t} else {\n\t\tconst auto extension = pattern.isEmpty()\n\t\t\t? u\".unknown\"_q\n\t\t\t: QString(pattern).replace('*', QString());\n\t\treturn u\"file_\"_q\n\t\t\t+ QString::number(++context.files)\n\t\t\t+ PrepareFileNameDatePart(date)\n\t\t\t+ extension;\n\t}\n}\n\nQString DocumentFolder(const Document &data) {\n\tif (data.isVideoFile) {\n\t\treturn \"video_files\";\n\t} else if (data.isAnimated) {\n\t\treturn \"animations\";\n\t} else if (data.isSticker) {\n\t\treturn \"stickers\";\n\t} else if (data.isVoiceMessage) {\n\t\treturn \"voice_messages\";\n\t} else if (data.isVideoMessage) {\n\t\treturn \"round_video_messages\";\n\t}\n\treturn \"files\";\n}\n\nImage ParseDocumentThumb(\n\t\tconst MTPDdocument &document,\n\t\tconst QString &documentPath) {\n\tconst auto thumbs = document.vthumbs();\n\tif (!thumbs) {\n\t\treturn Image();\n\t}\n\n\tconst auto area = [](const MTPPhotoSize &size) {\n\t\treturn size.match([](const MTPDphotoSizeEmpty &) {\n\t\t\treturn 0;\n\t\t}, [](const MTPDphotoStrippedSize &) {\n\t\t\treturn 0;\n\t\t}, [](const MTPDphotoPathSize &) {\n\t\t\treturn 0;\n\t\t}, [](const auto &data) {\n\t\t\treturn data.vw().v * data.vh().v;\n\t\t});\n\t};\n\tconst auto &list = thumbs->v;\n\tconst auto i = ranges::max_element(list, ranges::less(), area);\n\tif (i == list.end()) {\n\t\treturn Image();\n\t}\n\treturn i->match([](const MTPDphotoSizeEmpty &) {\n\t\treturn Image();\n\t}, [](const MTPDphotoStrippedSize &) {\n\t\treturn Image();\n\t}, [](const MTPDphotoPathSize &) {\n\t\treturn Image();\n\t}, [&](const auto &data) {\n\t\tauto result = Image();\n\t\tresult.width = data.vw().v;\n\t\tresult.height = data.vh().v;\n\t\tresult.file.location = FileLocation{\n\t\t\tdocument.vdc_id().v,\n\t\t\tMTP_inputDocumentFileLocation(\n\t\t\t\tdocument.vid(),\n\t\t\t\tdocument.vaccess_hash(),\n\t\t\t\tdocument.vfile_reference(),\n\t\t\t\tdata.vtype()) };\n\t\tif constexpr (MTPDphotoCachedSize::Is<decltype(data)>()) {\n\t\t\tresult.file.content = data.vbytes().v;\n\t\t\tresult.file.size = result.file.content.size();\n\t\t} else if constexpr (MTPDphotoSizeProgressive::Is<decltype(data)>()) {\n\t\t\tif (data.vsizes().v.isEmpty()) {\n\t\t\t\treturn Image();\n\t\t\t}\n\t\t\tresult.file.content = QByteArray();\n\t\t\tresult.file.size = data.vsizes().v.back().v;\n\t\t} else {\n\t\t\tresult.file.content = QByteArray();\n\t\t\tresult.file.size = data.vsize().v;\n\t\t}\n\t\tresult.file.suggestedPath = documentPath + \"_thumb.jpg\";\n\t\treturn result;\n\t});\n}\n\nDocument ParseDocument(\n\t\tParseMediaContext &context,\n\t\tconst MTPDocument &data,\n\t\tconst QString &suggestedFolder,\n\t\tTimeId date) {\n\tauto result = Document();\n\tdata.match([&](const MTPDdocument &data) {\n\t\tresult.id = data.vid().v;\n\t\tresult.date = data.vdate().v;\n\t\tresult.mime = ParseString(data.vmime_type());\n\t\tParseAttributes(result, data.vattributes());\n\n\t\tresult.file.size = data.vsize().v;\n\t\tresult.file.location.dcId = data.vdc_id().v;\n\t\tresult.file.location.data = MTP_inputDocumentFileLocation(\n\t\t\tdata.vid(),\n\t\t\tdata.vaccess_hash(),\n\t\t\tdata.vfile_reference(),\n\t\t\tMTP_string());\n\t\tresult.file.suggestedPath = suggestedFolder\n\t\t\t+ DocumentFolder(result) + '/'\n\t\t\t+ base::FileNameFromUserString(\n\t\t\t\tComputeDocumentName(context, result, date));\n\n\t\tresult.thumb = ParseDocumentThumb(\n\t\t\tdata,\n\t\t\tresult.file.suggestedPath);\n\t}, [&](const MTPDdocumentEmpty &data) {\n\t\tresult.id = data.vid().v;\n\t});\n\treturn result;\n}\n\nSharedContact ParseSharedContact(\n\t\tParseMediaContext &context,\n\t\tconst MTPDmessageMediaContact &data,\n\t\tconst QString &suggestedFolder) {\n\tauto result = SharedContact();\n\tresult.info.userId = data.vuser_id().v;\n\tresult.info.firstName = ParseString(data.vfirst_name());\n\tresult.info.lastName = ParseString(data.vlast_name());\n\tresult.info.phoneNumber = ParseString(data.vphone_number());\n\tif (!data.vvcard().v.isEmpty()) {\n\t\tresult.vcard.content = data.vvcard().v;\n\t\tresult.vcard.size = data.vvcard().v.size();\n\t\tresult.vcard.suggestedPath = suggestedFolder\n\t\t\t+ \"contacts/contact_\"\n\t\t\t+ QString::number(++context.contacts)\n\t\t\t+ \".vcard\";\n\t}\n\treturn result;\n}\n\nGeoPoint ParseGeoPoint(const MTPGeoPoint &data) {\n\tauto result = GeoPoint();\n\tdata.match([&](const MTPDgeoPoint &data) {\n\t\tresult.latitude = data.vlat().v;\n\t\tresult.longitude = data.vlong().v;\n\t\tresult.valid = true;\n\t}, [](const MTPDgeoPointEmpty &data) {});\n\treturn result;\n}\n\nVenue ParseVenue(const MTPDmessageMediaVenue &data) {\n\tauto result = Venue();\n\tresult.point = ParseGeoPoint(data.vgeo());\n\tresult.title = ParseString(data.vtitle());\n\tresult.address = ParseString(data.vaddress());\n\treturn result;\n}\n\nGame ParseGame(const MTPGame &data, UserId botId) {\n\treturn data.match([&](const MTPDgame &data) {\n\t\tauto result = Game();\n\t\tresult.id = data.vid().v;\n\t\tresult.title = ParseString(data.vtitle());\n\t\tresult.description = ParseString(data.vdescription());\n\t\tresult.shortName = ParseString(data.vshort_name());\n\t\tresult.botId = botId;\n\t\treturn result;\n\t});\n}\n\nInvoice ParseInvoice(const MTPDmessageMediaInvoice &data) {\n\tauto result = Invoice();\n\tresult.title = ParseString(data.vtitle());\n\tresult.description = ParseString(data.vdescription());\n\tresult.currency = ParseString(data.vcurrency());\n\tresult.amount = data.vtotal_amount().v;\n\tif (const auto receiptMsgId = data.vreceipt_msg_id()) {\n\t\tresult.receiptMsgId = receiptMsgId->v;\n\t}\n\treturn result;\n}\n\nPaidMedia ParsePaidMedia(\n\t\tParseMediaContext &context,\n\t\tconst MTPDmessageMediaPaidMedia &data,\n\t\tconst QString &folder,\n\t\tTimeId date) {\n\tauto result = PaidMedia();\n\tresult.stars = data.vstars_amount().v;\n\tresult.extended.reserve(data.vextended_media().v.size());\n\tfor (const auto &extended : data.vextended_media().v) {\n\t\tresult.extended.push_back(extended.match([](\n\t\t\tconst MTPDmessageExtendedMediaPreview &)\n\t\t-> std::unique_ptr<Media> {\n\t\t\treturn std::unique_ptr<Media>();\n\t\t}, [&](const MTPDmessageExtendedMedia &data)\n\t\t-> std::unique_ptr<Media> {\n\t\t\treturn std::make_unique<Media>(\n\t\t\t\tParseMedia(context, data.vmedia(), folder, date));\n\t\t}));\n\t}\n\treturn result;\n}\n\nPoll ParsePoll(const MTPDmessageMediaPoll &data) {\n\tauto result = Poll();\n\tdata.vpoll().match([&](const MTPDpoll &poll) {\n\t\tresult.id = poll.vid().v;\n\t\tresult.question = ParseText(poll.vquestion());\n\t\tresult.closed = poll.is_closed();\n\t\tresult.answers = ranges::views::all(\n\t\t\tpoll.vanswers().v\n\t\t) | ranges::views::transform([](const MTPPollAnswer &answer) {\n\t\t\tauto result = Poll::Answer();\n\t\t\tanswer.match([&](const MTPDpollAnswer &data) {\n\t\t\t\tresult.text = ParseText(data.vtext());\n\t\t\t\tresult.option = data.voption().v;\n\t\t\t}, [](const auto &) {\n\t\t\t});\n\t\t\treturn result;\n\t\t}) | ranges::to_vector;\n\t});\n\tdata.vresults().match([&](const MTPDpollResults &results) {\n\t\tif (const auto totalVotes = results.vtotal_voters()) {\n\t\t\tresult.totalVotes = totalVotes->v;\n\t\t}\n\t\tif (const auto resultsList = results.vresults()) {\n\t\t\tfor (const auto &single : resultsList->v) {\n\t\t\t\tconst auto &voters = single.data();\n\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\tresult.answers,\n\t\t\t\t\tvoters.voption().v,\n\t\t\t\t\t&Poll::Answer::option);\n\t\t\t\tif (i == end(result.answers)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (const auto votes = voters.vvoters()) {\n\t\t\t\t\ti->votes = votes->v;\n\t\t\t\t}\n\t\t\t\tif (voters.is_chosen()) {\n\t\t\t\t\ti->my = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\treturn result;\n}\n\nTodoListItem ParseTodoListItem(const MTPTodoItem &item) {\n\tconst auto &data = item.data();\n\tauto result = TodoListItem();\n\tresult.text = ParseText(data.vtitle());\n\tresult.id = data.vid().v;\n\treturn result;\n}\n\nTodoList ParseTodoList(const MTPDmessageMediaToDo &data) {\n\tauto result = TodoList();\n\tdata.vtodo().match([&](const MTPDtodoList &data) {\n\t\tresult.title = ParseText(data.vtitle());\n\t\tresult.othersCanAppend = data.is_others_can_append();\n\t\tresult.othersCanComplete = data.is_others_can_complete();\n\t\tresult.items = ranges::views::all(\n\t\t\tdata.vlist().v\n\t\t) | ranges::views::transform(\n\t\t\tParseTodoListItem\n\t\t) | ranges::to_vector;\n\t});\n\treturn result;\n}\n\nGiveawayStart ParseGiveaway(const MTPDmessageMediaGiveaway &data) {\n\tauto result = GiveawayStart{\n\t\t.untilDate = data.vuntil_date().v,\n\t\t.credits = data.vstars().value_or_empty(),\n\t\t.quantity = data.vquantity().v,\n\t\t.months = data.vmonths().value_or_empty(),\n\t\t.all = !data.is_only_new_subscribers(),\n\t};\n\tfor (const auto &id : data.vchannels().v) {\n\t\tresult.channels.push_back(ChannelId(id));\n\t}\n\tif (const auto countries = data.vcountries_iso2()) {\n\t\tresult.countries.reserve(countries->v.size());\n\t\tfor (const auto &country : countries->v) {\n\t\t\tresult.countries.push_back(qs(country));\n\t\t}\n\t}\n\tif (const auto additional = data.vprize_description()) {\n\t\tresult.additionalPrize = qs(*additional);\n\t}\n\treturn result;\n}\n\nGiveawayResults ParseGiveaway(const MTPDmessageMediaGiveawayResults &data) {\n\tconst auto additional = data.vadditional_peers_count();\n\tauto result = GiveawayResults{\n\t\t.channel = ChannelId(data.vchannel_id()),\n\t\t.untilDate = data.vuntil_date().v,\n\t\t.launchId = data.vlaunch_msg_id().v,\n\t\t.additionalPeersCount = additional.value_or_empty(),\n\t\t.winnersCount = data.vwinners_count().v,\n\t\t.unclaimedCount = data.vunclaimed_count().v,\n\t\t.months = data.vmonths().value_or_empty(),\n\t\t.credits = data.vstars().value_or_empty(),\n\t\t.refunded = data.is_refunded(),\n\t\t.all = !data.is_only_new_subscribers(),\n\t};\n\tresult.winners.reserve(data.vwinners().v.size());\n\tfor (const auto &id : data.vwinners().v) {\n\t\tresult.winners.push_back(UserId(id));\n\t}\n\tif (const auto additional = data.vprize_description()) {\n\t\tresult.additionalPrize = qs(*additional);\n\t}\n\treturn result;\n}\n\nUserpicsSlice ParseUserpicsSlice(\n\t\tconst MTPVector<MTPPhoto> &data,\n\t\tint baseIndex) {\n\tconst auto &list = data.v;\n\tauto result = UserpicsSlice();\n\tresult.list.reserve(list.size());\n\tfor (const auto &photo : list) {\n\t\tconst auto suggestedPath = \"profile_pictures/\"\n\t\t\t+ PreparePhotoFileName(\n\t\t\t\t++baseIndex,\n\t\t\t\t(photo.type() == mtpc_photo\n\t\t\t\t\t? photo.c_photo().vdate().v\n\t\t\t\t\t: TimeId(0)));\n\t\tresult.list.push_back(ParsePhoto(photo, suggestedPath));\n\t}\n\treturn result;\n}\n\n[[nodiscard]] ActionStarGift ParseStarGift(const MTPStarGift &gift) {\n\treturn gift.match([&](const MTPDstarGift &gift) {\n\t\treturn ActionStarGift{\n\t\t\t.giftId = uint64(gift.vid().v),\n\t\t\t.stars = int64(gift.vstars().v),\n\t\t\t.limited = gift.is_limited(),\n\t\t};\n\t}, [&](const MTPDstarGiftUnique &gift) {\n\t\treturn ActionStarGift{\n\t\t\t.giftId = uint64(gift.vid().v),\n\t\t};\n\t});\n}\n\nFile &Story::file() {\n\treturn media.file();\n}\n\nconst File &Story::file() const {\n\treturn media.file();\n}\n\nImage &Story::thumb() {\n\treturn media.thumb();\n}\n\nconst Image &Story::thumb() const {\n\treturn media.thumb();\n}\n\nStoriesSlice ParseStoriesSlice(\n\t\tconst MTPVector<MTPStoryItem> &data,\n\t\tint baseIndex) {\n\tconst auto &list = data.v;\n\tauto result = StoriesSlice();\n\tresult.list.reserve(list.size());\n\tfor (const auto &story : list) {\n\t\tresult.lastId = story.match([](const auto &data) {\n\t\t\treturn data.vid().v;\n\t\t});\n\t\t++result.skipped;\n\t\tstory.match([&](const MTPDstoryItem &data) {\n\t\t\tconst auto date = data.vdate().v;\n\t\t\tauto media = Media();\n\t\t\tdata.vmedia().match([&](const MTPDmessageMediaPhoto &data) {\n\t\t\t\tconst auto suggestedPath = \"stories/\"\n\t\t\t\t\t+ PrepareStoryFileName(\n\t\t\t\t\t\t++baseIndex,\n\t\t\t\t\t\tdate,\n\t\t\t\t\t\t\".jpg\"_q);\n\t\t\t\tconst auto photo = data.vphoto();\n\t\t\t\tauto content = photo\n\t\t\t\t\t? ParsePhoto(*photo, suggestedPath)\n\t\t\t\t\t: Photo();\n\t\t\t\tcontent.spoilered = data.is_spoiler();\n\t\t\t\tmedia.content = content;\n\t\t\t}, [&](const MTPDmessageMediaDocument &data) {\n\t\t\t\tconst auto document = data.vdocument();\n\t\t\t\tauto fake = ParseMediaContext();\n\t\t\t\tauto content = document\n\t\t\t\t\t? ParseDocument(fake, *document, \"stories\", date)\n\t\t\t\t\t: Document();\n\t\t\t\tconst auto extension = (content.mime == \"image/jpeg\")\n\t\t\t\t\t? \".jpg\"_q\n\t\t\t\t\t: (content.mime == \"image/png\")\n\t\t\t\t\t? \".png\"_q\n\t\t\t\t\t: [&] {\n\t\t\t\t\t\tconst auto mimeType = Core::MimeTypeForName(\n\t\t\t\t\t\t\tcontent.mime);\n\t\t\t\t\t\tQStringList patterns = mimeType.globPatterns();\n\t\t\t\t\t\tif (!patterns.isEmpty()) {\n\t\t\t\t\t\t\treturn patterns.front().replace(\n\t\t\t\t\t\t\t\t'*',\n\t\t\t\t\t\t\t\tQString()).toUtf8();\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn QByteArray();\n\t\t\t\t\t}();\n\t\t\t\tconst auto path = content.file.suggestedPath = \"stories/\"\n\t\t\t\t\t+ PrepareStoryFileName(\n\t\t\t\t\t\t++baseIndex,\n\t\t\t\t\t\tdate,\n\t\t\t\t\t\textension);\n\t\t\t\tcontent.thumb.file.suggestedPath = path + \"_thumb.jpg\";\n\t\t\t\tcontent.spoilered = data.is_spoiler();\n\t\t\t\tmedia.content = content;\n\t\t\t}, [&](const auto &data) {\n\t\t\t\tmedia.content = UnsupportedMedia();\n\t\t\t});\n\t\t\tif (!v::is<UnsupportedMedia>(media.content)) {\n\t\t\t\tresult.list.push_back(Story{\n\t\t\t\t\t.id = data.vid().v,\n\t\t\t\t\t.date = date,\n\t\t\t\t\t.expires = data.vexpire_date().v,\n\t\t\t\t\t.media = std::move(media),\n\t\t\t\t\t.pinned = data.is_pinned(),\n\t\t\t\t\t.caption = (data.vcaption()\n\t\t\t\t\t\t? ParseText(\n\t\t\t\t\t\t\t*data.vcaption(),\n\t\t\t\t\t\t\tdata.ventities().value_or_empty())\n\t\t\t\t\t\t: std::vector<TextPart>()),\n\t\t\t\t});\n\t\t\t\t--result.skipped;\n\t\t\t}\n\t\t}, [](const auto &) {});\n\t}\n\treturn result;\n}\n\nstd::pair<QString, QSize> WriteImageThumb(\n\t\tconst QString &basePath,\n\t\tconst QString &largePath,\n\t\tFn<QSize(QSize)> convertSize,\n\t\tstd::optional<QByteArray> format,\n\t\tstd::optional<int> quality,\n\t\tconst QString &postfix) {\n\tif (largePath.isEmpty()) {\n\t\treturn {};\n\t}\n\tconst auto path = basePath + largePath;\n\tQImageReader reader(path);\n\tif (!reader.canRead()) {\n\t\treturn {};\n\t}\n\tconst auto size = reader.size();\n\tif (size.isEmpty()\n\t\t|| size.width() >= kMaxImageSize\n\t\t|| size.height() >= kMaxImageSize) {\n\t\treturn {};\n\t}\n\tauto image = reader.read();\n\tif (image.isNull()) {\n\t\treturn {};\n\t}\n\tconst auto finalSize = convertSize(image.size());\n\tif (finalSize.isEmpty()) {\n\t\treturn {};\n\t}\n\timage = std::move(image).scaled(\n\t\tfinalSize,\n\t\tQt::IgnoreAspectRatio,\n\t\tQt::SmoothTransformation);\n\tconst auto finalFormat = format ? *format : reader.format();\n\tconst auto finalQuality = quality ? *quality : reader.quality();\n\tconst auto lastSlash = largePath.lastIndexOf('/');\n\tconst auto firstDot = largePath.indexOf('.', lastSlash + 1);\n\tconst auto thumb = (firstDot >= 0)\n\t\t? largePath.mid(0, firstDot) + postfix + largePath.mid(firstDot)\n\t\t: largePath + postfix;\n\tconst auto result = Output::File::PrepareRelativePath(basePath, thumb);\n\tif (!image.save(\n\t\t\tbasePath + result,\n\t\t\tfinalFormat.constData(),\n\t\t\tfinalQuality)) {\n\t\treturn {};\n\t}\n\treturn { result, finalSize };\n}\n\nQString WriteImageThumb(\n\t\tconst QString &basePath,\n\t\tconst QString &largePath,\n\t\tint width,\n\t\tint height,\n\t\tconst QString &postfix) {\n\treturn WriteImageThumb(\n\t\tbasePath,\n\t\tlargePath,\n\t\t[=](QSize size) { return QSize(width, height); },\n\t\tstd::nullopt,\n\t\tstd::nullopt,\n\t\tpostfix).first;\n}\n\nContactInfo ParseContactInfo(const MTPUser &data) {\n\tauto result = ContactInfo();\n\tdata.match([&](const MTPDuser &data) {\n\t\tresult.userId = data.vid().v;\n\t\tresult.colorIndex = PeerColorIndex(result.userId);\n\t\tif (const auto color = data.vcolor()) {\n\t\t\tcolor->match([&](const MTPDpeerColor &data) {\n\t\t\t\tif (const auto color = data.vcolor()) {\n\t\t\t\t\tresult.colorIndex = color->v;\n\t\t\t\t}\n\t\t\t}, [](const MTPDpeerColorCollectible &) {\n\t\t\t\t// themes\n\t\t\t}, [](const MTPDinputPeerColorCollectible &) {\n\t\t\t});\n\t\t}\n\t\tif (const auto firstName = data.vfirst_name()) {\n\t\t\tresult.firstName = ParseString(*firstName);\n\t\t}\n\t\tif (const auto lastName = data.vlast_name()) {\n\t\t\tresult.lastName = ParseString(*lastName);\n\t\t}\n\t\tif (const auto phone = data.vphone()) {\n\t\t\tresult.phoneNumber = ParseString(*phone);\n\t\t}\n\t}, [&](const MTPDuserEmpty &data) {\n\t\tresult.userId = data.vid().v;\n\t\tresult.colorIndex = PeerColorIndex(result.userId);\n\t});\n\treturn result;\n}\n\nuint8 ContactColorIndex(const ContactInfo &data) {\n\treturn data.colorIndex;\n}\n\nPeerId User::id() const {\n\treturn UserId(bareId);\n}\n\nUser ParseUser(const MTPUser &data) {\n\tauto result = User();\n\tresult.info = ParseContactInfo(data);\n\tdata.match([&](const MTPDuser &data) {\n\t\tresult.bareId = data.vid().v;\n\t\tresult.colorIndex = PeerColorIndex(result.bareId);\n\t\tif (const auto color = data.vcolor()) {\n\t\t\tcolor->match([&](const MTPDpeerColor &data) {\n\t\t\t\tif (const auto color = data.vcolor()) {\n\t\t\t\t\tresult.colorIndex = color->v;\n\t\t\t\t}\n\t\t\t}, [](const MTPDpeerColorCollectible &) {\n\t\t\t\t// themes\n\t\t\t}, [](const MTPDinputPeerColorCollectible &) {\n\t\t\t});\n\t\t}\n\t\tif (const auto username = data.vusername()) {\n\t\t\tresult.username = ParseString(*username);\n\t\t}\n\t\tif (data.vbot_info_version()) {\n\t\t\tresult.isBot = true;\n\t\t}\n\t\tif (data.is_self()) {\n\t\t\tresult.isSelf = true;\n\t\t} else if (data.vid().v == 1271266957) {\n\t\t\tresult.isReplies = true;\n\t\t} else if (data.vid().v == 489000) {\n\t\t\tresult.isVerifyCodes = true;\n\t\t}\n\t\tresult.input = MTP_inputUser(\n\t\t\tdata.vid(),\n\t\t\tMTP_long(data.vaccess_hash().value_or_empty()));\n\t}, [&](const MTPDuserEmpty &data) {\n\t\tresult.input = MTP_inputUser(data.vid(), MTP_long(0));\n\t});\n\treturn result;\n}\n\nstd::map<UserId, User> ParseUsersList(const MTPVector<MTPUser> &data) {\n\tauto result = std::map<UserId, User>();\n\tfor (const auto &user : data.v) {\n\t\tauto parsed = ParseUser(user);\n\t\tresult.emplace(parsed.info.userId, std::move(parsed));\n\t}\n\treturn result;\n}\n\nPeerId Chat::id() const {\n\treturn (isBroadcast || isSupergroup)\n\t\t? PeerId(ChannelId(bareId))\n\t\t: ChatId(bareId);\n}\n\nChat ParseChat(const MTPChat &data) {\n\tauto result = Chat();\n\tdata.match([&](const MTPDchat &data) {\n\t\tresult.bareId = data.vid().v;\n\t\tresult.title = ParseString(data.vtitle());\n\t\tresult.input = MTP_inputPeerChat(MTP_long(result.bareId));\n\t\tif (const auto migratedTo = data.vmigrated_to()) {\n\t\t\tresult.migratedToChannelId = migratedTo->match(\n\t\t\t[](const MTPDinputChannel &data) {\n\t\t\t\treturn data.vchannel_id().v;\n\t\t\t}, [](auto&&) { return BareId(); });\n\t\t}\n\t}, [&](const MTPDchatEmpty &data) {\n\t\tresult.bareId = data.vid().v;\n\t\tresult.input = MTP_inputPeerChat(MTP_long(result.bareId));\n\t}, [&](const MTPDchatForbidden &data) {\n\t\tresult.bareId = data.vid().v;\n\t\tresult.title = ParseString(data.vtitle());\n\t\tresult.input = MTP_inputPeerChat(MTP_long(result.bareId));\n\t}, [&](const MTPDchannel &data) {\n\t\tresult.bareId = data.vid().v;\n\t\tresult.colorIndex = PeerColorIndex(result.bareId);\n\t\tif (const auto color = data.vcolor()) {\n\t\t\tcolor->match([&](const MTPDpeerColor &data) {\n\t\t\t\tif (const auto color = data.vcolor()) {\n\t\t\t\t\tresult.colorIndex = color->v;\n\t\t\t\t}\n\t\t\t}, [](const MTPDpeerColorCollectible &) {\n\t\t\t}, [](const MTPDinputPeerColorCollectible &) {\n\t\t\t});\n\t\t}\n\t\tresult.isMonoforum = data.is_monoforum();\n\t\tresult.isBroadcast = data.is_broadcast();\n\t\tresult.isSupergroup = data.is_megagroup();\n\t\tresult.hasMonoforumAdminRights = data.is_broadcast()\n\t\t\t&& (data.is_creator()\n\t\t\t\t|| (data.vadmin_rights()\n\t\t\t\t\t&& data.vadmin_rights()->data().is_manage_direct_messages()));\n\t\tresult.monoforumLinkId\n\t\t\t= data.vlinked_monoforum_id().value_or_empty();\n\t\tresult.title = ParseString(data.vtitle());\n\t\tif (const auto username = data.vusername()) {\n\t\t\tresult.username = ParseString(*username);\n\t\t}\n\t\tresult.input = MTP_inputPeerChannel(\n\t\t\tMTP_long(result.bareId),\n\t\t\tMTP_long(data.vaccess_hash().value_or_empty()));\n\t}, [&](const MTPDchannelForbidden &data) {\n\t\tresult.bareId = data.vid().v;\n\t\tresult.isBroadcast = data.is_broadcast();\n\t\tresult.isSupergroup = data.is_megagroup();\n\t\tresult.isMonoforum = data.is_monoforum();\n\t\tresult.title = ParseString(data.vtitle());\n\t\tresult.input = MTP_inputPeerChannel(\n\t\t\tMTP_long(result.bareId),\n\t\t\tdata.vaccess_hash());\n\t});\n\treturn result;\n}\n\nUtf8String ContactInfo::name() const {\n\treturn firstName.isEmpty()\n\t\t? (lastName.isEmpty()\n\t\t\t? Utf8String()\n\t\t\t: lastName)\n\t\t: (lastName.isEmpty()\n\t\t\t? firstName\n\t\t\t: firstName + ' ' + lastName);\n\n}\n\nUtf8String User::name() const {\n\treturn info.name();\n}\n\nconst User *Peer::user() const {\n\treturn std::get_if<User>(&data);\n}\nconst Chat *Peer::chat() const {\n\treturn std::get_if<Chat>(&data);\n}\n\nPeerId Peer::id() const {\n\tif (const auto user = this->user()) {\n\t\treturn peerFromUser(user->info.userId);\n\t} else if (const auto chat = this->chat()) {\n\t\treturn chat->id();\n\t}\n\tUnexpected(\"Variant in Peer::id.\");\n}\n\nUtf8String Peer::name() const {\n\tif (const auto user = this->user()) {\n\t\treturn user->name();\n\t} else if (const auto chat = this->chat()) {\n\t\treturn chat->title;\n\t}\n\tUnexpected(\"Variant in Peer::id.\");\n}\n\nMTPInputPeer Peer::input() const {\n\tif (const auto user = this->user()) {\n\t\tif (user->input.type() == mtpc_inputUser) {\n\t\t\tconst auto &input = user->input.c_inputUser();\n\t\t\treturn MTP_inputPeerUser(input.vuser_id(), input.vaccess_hash());\n\t\t}\n\t\treturn MTP_inputPeerEmpty();\n\t} else if (const auto chat = this->chat()) {\n\t\treturn chat->input;\n\t}\n\tUnexpected(\"Variant in Peer::id.\");\n}\n\nuint8 Peer::colorIndex() const {\n\tif (const auto user = this->user()) {\n\t\treturn user->colorIndex;\n\t} else if (const auto chat = this->chat()) {\n\t\treturn chat->colorIndex;\n\t}\n\tUnexpected(\"Variant in Peer::colorIndex.\");\n}\n\nstd::map<PeerId, Peer> ParsePeersLists(\n\t\tconst MTPVector<MTPUser> &users,\n\t\tconst MTPVector<MTPChat> &chats) {\n\tauto result = std::map<PeerId, Peer>();\n\tfor (const auto &user : users.v) {\n\t\tauto parsed = ParseUser(user);\n\t\tresult.emplace(\n\t\t\tPeerId(parsed.info.userId),\n\t\t\tPeer{ std::move(parsed) });\n\t}\n\tfor (const auto &chat : chats.v) {\n\t\tauto parsed = ParseChat(chat);\n\t\tresult.emplace(parsed.id(), Peer{ std::move(parsed) });\n\t}\n\tfor (auto &[peerId, parsed] : result) {\n\t\tif (const auto chat = std::get_if<Chat>(&parsed.data)) {\n\t\t\tif (chat->isMonoforum) {\n\t\t\t\tconst auto i = result.find(\n\t\t\t\t\tPeerId(ChannelId(chat->monoforumLinkId)));\n\t\t\t\tif (i != end(result)) {\n\t\t\t\t\tchat->isMonoforumAdmin\n\t\t\t\t\t\t= i->second.chat()->hasMonoforumAdminRights;\n\t\t\t\t\tchat->isMonoforumOfPublicBroadcast\n\t\t\t\t\t\t= !i->second.chat()->username.isEmpty();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nUser EmptyUser(UserId userId) {\n\treturn ParseUser(MTP_userEmpty(MTP_long(userId.bare)));\n}\n\nChat EmptyChat(ChatId chatId) {\n\treturn ParseChat(MTP_chatEmpty(MTP_long(chatId.bare)));\n}\n\nPeer EmptyPeer(PeerId peerId) {\n\tif (peerIsUser(peerId)) {\n\t\treturn Peer{ EmptyUser(peerToUser(peerId)) };\n\t} else if (peerIsChat(peerId)) {\n\t\treturn Peer{ EmptyChat(peerToChat(peerId)) };\n\t} else if (peerIsChannel(peerId)) {\n\t\treturn Peer{ EmptyChat(peerToChat(peerId)) };\n\t}\n\tUnexpected(\"PeerId in EmptyPeer.\");\n}\n\nFile &Media::file() {\n\treturn v::match(content, [](Photo &data) -> File& {\n\t\treturn data.image.file;\n\t}, [](Document &data) -> File& {\n\t\treturn data.file;\n\t}, [](SharedContact &data) -> File& {\n\t\treturn data.vcard;\n\t}, [](auto&) -> File& {\n\t\tstatic File result;\n\t\treturn result;\n\t});\n}\n\nconst File &Media::file() const {\n\treturn v::match(content, [](const Photo &data) -> const File& {\n\t\treturn data.image.file;\n\t}, [](const Document &data) -> const File& {\n\t\treturn data.file;\n\t}, [](const SharedContact &data) -> const File& {\n\t\treturn data.vcard;\n\t}, [](const auto &) -> const File& {\n\t\tstatic const File result;\n\t\treturn result;\n\t});\n}\n\nImage &Media::thumb() {\n\treturn v::match(content, [](Document &data) -> Image& {\n\t\treturn data.thumb;\n\t}, [](auto&) -> Image& {\n\t\tstatic Image result;\n\t\treturn result;\n\t});\n}\n\nconst Image &Media::thumb() const {\n\treturn v::match(content, [](const Document &data) -> const Image& {\n\t\treturn data.thumb;\n\t}, [](const auto &) -> const Image& {\n\t\tstatic const Image result;\n\t\treturn result;\n\t});\n}\n\nMedia ParseMedia(\n\t\tParseMediaContext &context,\n\t\tconst MTPMessageMedia &data,\n\t\tconst QString &folder,\n\t\tTimeId date) {\n\tExpects(folder.isEmpty() || folder.endsWith(QChar('/')));\n\n\tauto result = Media();\n\tdata.match([&](const MTPDmessageMediaPhoto &data) {\n\t\tconst auto photo = data.vphoto();\n\t\tauto content = photo\n\t\t\t? ParsePhoto(\n\t\t\t\t*photo,\n\t\t\t\tfolder\n\t\t\t\t+ \"photos/\"\n\t\t\t\t+ PreparePhotoFileName(++context.photos, date))\n\t\t\t: Photo();\n\t\tcontent.spoilered = data.is_spoiler();\n\t\tif (const auto ttl = data.vttl_seconds()) {\n\t\t\tresult.ttl = ttl->v;\n\t\t\tcontent.image.file = File();\n\t\t}\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageMediaGeo &data) {\n\t\tresult.content = ParseGeoPoint(data.vgeo());\n\t}, [&](const MTPDmessageMediaContact &data) {\n\t\tresult.content = ParseSharedContact(context, data, folder);\n\t}, [&](const MTPDmessageMediaUnsupported &data) {\n\t\tresult.content = UnsupportedMedia();\n\t}, [&](const MTPDmessageMediaDocument &data) {\n\t\tconst auto document = data.vdocument();\n\t\tauto content = document\n\t\t\t? ParseDocument(context, *document, folder, date)\n\t\t\t: Document();\n\t\tif (const auto ttl = data.vttl_seconds()) {\n\t\t\tresult.ttl = ttl->v;\n\t\t\tcontent.file = File();\n\t\t}\n\t\tcontent.spoilered = data.is_spoiler();\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageMediaWebPage &data) {\n\t\t// Ignore web pages.\n\t}, [&](const MTPDmessageMediaVenue &data) {\n\t\tresult.content = ParseVenue(data);\n\t}, [&](const MTPDmessageMediaGame &data) {\n\t\tresult.content = ParseGame(data.vgame(), context.botId);\n\t}, [&](const MTPDmessageMediaInvoice &data) {\n\t\tresult.content = ParseInvoice(data);\n\t}, [&](const MTPDmessageMediaGeoLive &data) {\n\t\tresult.content = ParseGeoPoint(data.vgeo());\n\t\tresult.ttl = data.vperiod().v;\n\t}, [&](const MTPDmessageMediaPoll &data) {\n\t\tresult.content = ParsePoll(data);\n\t}, [&](const MTPDmessageMediaToDo &data) {\n\t\tresult.content = ParseTodoList(data);\n\t}, [](const MTPDmessageMediaDice &data) {\n\t\t// #TODO dice\n\t}, [](const MTPDmessageMediaStory &data) {\n\t\t// #TODO export stories\n\t}, [&](const MTPDmessageMediaGiveaway &data) {\n\t\tresult.content = ParseGiveaway(data);\n\t}, [&](const MTPDmessageMediaGiveawayResults &data) {\n\t\tresult.content = ParseGiveaway(data);\n\t}, [&](const MTPDmessageMediaPaidMedia &data) {\n\t\tresult.content = ParsePaidMedia(context, data, folder, date);\n\t}, [](const MTPDmessageMediaVideoStream &data) {\n\t\t// Live stories.\n\t}, [](const MTPDmessageMediaEmpty &data) {});\n\treturn result;\n}\n\nServiceAction ParseServiceAction(\n\t\tParseMediaContext &context,\n\t\tconst MTPMessageAction &data,\n\t\tconst QString &mediaFolder,\n\t\tTimeId date) {\n\tauto result = ServiceAction();\n\tdata.match([&](const MTPDmessageActionChatCreate &data) {\n\t\tauto content = ActionChatCreate();\n\t\tcontent.title = ParseString(data.vtitle());\n\t\tcontent.userIds.reserve(data.vusers().v.size());\n\t\tfor (const auto &userId : data.vusers().v) {\n\t\t\tcontent.userIds.push_back(userId.v);\n\t\t}\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionChatEditTitle &data) {\n\t\tauto content = ActionChatEditTitle();\n\t\tcontent.title = ParseString(data.vtitle());\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionChatEditPhoto &data) {\n\t\tauto content = ActionChatEditPhoto();\n\t\tcontent.photo = ParsePhoto(\n\t\t\tdata.vphoto(),\n\t\t\tmediaFolder\n\t\t\t+ \"photos/\"\n\t\t\t+ PreparePhotoFileName(++context.photos, date));\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionChatDeletePhoto &data) {\n\t\tresult.content = ActionChatDeletePhoto();\n\t}, [&](const MTPDmessageActionChatAddUser &data) {\n\t\tauto content = ActionChatAddUser();\n\t\tcontent.userIds.reserve(data.vusers().v.size());\n\t\tfor (const auto &user : data.vusers().v) {\n\t\t\tcontent.userIds.push_back(user.v);\n\t\t}\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionChatDeleteUser &data) {\n\t\tauto content = ActionChatDeleteUser();\n\t\tcontent.userId = data.vuser_id().v;\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionChatJoinedByLink &data) {\n\t\tauto content = ActionChatJoinedByLink();\n\t\tcontent.inviterId = data.vinviter_id().v;\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionChannelCreate &data) {\n\t\tauto content = ActionChannelCreate();\n\t\tcontent.title = ParseString(data.vtitle());\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionChatMigrateTo &data) {\n\t\tauto content = ActionChatMigrateTo();\n\t\tcontent.channelId = data.vchannel_id().v;\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionChannelMigrateFrom &data) {\n\t\tauto content = ActionChannelMigrateFrom();\n\t\tcontent.title = ParseString(data.vtitle());\n\t\tcontent.chatId = data.vchat_id().v;\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionPinMessage &data) {\n\t\tresult.content = ActionPinMessage();\n\t}, [&](const MTPDmessageActionHistoryClear &data) {\n\t\tresult.content = ActionHistoryClear();\n\t}, [&](const MTPDmessageActionGameScore &data) {\n\t\tauto content = ActionGameScore();\n\t\tcontent.gameId = data.vgame_id().v;\n\t\tcontent.score = data.vscore().v;\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionPaymentSentMe &data) {\n\t\t// Should not be in user inbox.\n\t}, [&](const MTPDmessageActionPaymentSent &data) {\n\t\tauto content = ActionPaymentSent();\n\t\tcontent.currency = ParseString(data.vcurrency());\n\t\tcontent.amount = data.vtotal_amount().v;\n\t\tcontent.recurringInit = data.is_recurring_init();\n\t\tcontent.recurringUsed = data.is_recurring_used();\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionPhoneCall &data) {\n\t\tauto content = ActionPhoneCall();\n\t\tif (const auto duration = data.vduration()) {\n\t\t\tcontent.duration = duration->v;\n\t\t}\n\t\tif (const auto reason = data.vreason()) {\n\t\t\tusing State = ActionPhoneCall::State;\n\t\t\tcontent.state = reason->match(\n\t\t\t[](const MTPDphoneCallDiscardReasonMissed &data) {\n\t\t\t\treturn State::Missed;\n\t\t\t}, [](const MTPDphoneCallDiscardReasonDisconnect &data) {\n\t\t\t\treturn State::Disconnect;\n\t\t\t}, [](const MTPDphoneCallDiscardReasonHangup &data) {\n\t\t\t\treturn State::Hangup;\n\t\t\t}, [](const MTPDphoneCallDiscardReasonBusy &data) {\n\t\t\t\treturn State::Busy;\n\t\t\t}, [](const MTPDphoneCallDiscardReasonMigrateConferenceCall &) {\n\t\t\t\treturn State::MigrateConferenceCall;\n\t\t\t});\n\t\t}\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionScreenshotTaken &data) {\n\t\tresult.content = ActionScreenshotTaken();\n\t}, [&](const MTPDmessageActionCustomAction &data) {\n\t\tauto content = ActionCustomAction();\n\t\tcontent.message = ParseString(data.vmessage());\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionBotAllowed &data) {\n\t\tauto content = ActionBotAllowed();\n\t\tif (const auto app = data.vapp()) {\n\t\t\tapp->match([&](const MTPDbotApp &data) {\n\t\t\t\tcontent.appId = data.vid().v;\n\t\t\t\tcontent.app = ParseString(data.vtitle());\n\t\t\t}, [](const MTPDbotAppNotModified &) {});\n\t\t}\n\t\tif (const auto domain = data.vdomain()) {\n\t\t\tcontent.domain = ParseString(*domain);\n\t\t}\n\t\tcontent.attachMenu = data.is_attach_menu();\n\t\tcontent.fromRequest = data.is_from_request();\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionSecureValuesSentMe &data) {\n\t\t// Should not be in user inbox.\n\t}, [&](const MTPDmessageActionSecureValuesSent &data) {\n\t\tauto content = ActionSecureValuesSent();\n\t\tcontent.types.reserve(data.vtypes().v.size());\n\t\tusing Type = ActionSecureValuesSent::Type;\n\t\tfor (const auto &type : data.vtypes().v) {\n\t\t\tcontent.types.push_back(type.match(\n\t\t\t[](const MTPDsecureValueTypePersonalDetails &data) {\n\t\t\t\treturn Type::PersonalDetails;\n\t\t\t}, [](const MTPDsecureValueTypePassport &data) {\n\t\t\t\treturn Type::Passport;\n\t\t\t}, [](const MTPDsecureValueTypeDriverLicense &data) {\n\t\t\t\treturn Type::DriverLicense;\n\t\t\t}, [](const MTPDsecureValueTypeIdentityCard &data) {\n\t\t\t\treturn Type::IdentityCard;\n\t\t\t}, [](const MTPDsecureValueTypeInternalPassport &data) {\n\t\t\t\treturn Type::InternalPassport;\n\t\t\t}, [](const MTPDsecureValueTypeAddress &data) {\n\t\t\t\treturn Type::Address;\n\t\t\t}, [](const MTPDsecureValueTypeUtilityBill &data) {\n\t\t\t\treturn Type::UtilityBill;\n\t\t\t}, [](const MTPDsecureValueTypeBankStatement &data) {\n\t\t\t\treturn Type::BankStatement;\n\t\t\t}, [](const MTPDsecureValueTypeRentalAgreement &data) {\n\t\t\t\treturn Type::RentalAgreement;\n\t\t\t}, [](const MTPDsecureValueTypePassportRegistration &data) {\n\t\t\t\treturn Type::PassportRegistration;\n\t\t\t}, [](const MTPDsecureValueTypeTemporaryRegistration &data) {\n\t\t\t\treturn Type::TemporaryRegistration;\n\t\t\t}, [](const MTPDsecureValueTypePhone &data) {\n\t\t\t\treturn Type::Phone;\n\t\t\t}, [](const MTPDsecureValueTypeEmail &data) {\n\t\t\t\treturn Type::Email;\n\t\t\t}));\n\t\t}\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionContactSignUp &data) {\n\t\tresult.content = ActionContactSignUp();\n\t}, [&](const MTPDmessageActionGeoProximityReached &data) {\n\t\tauto content = ActionGeoProximityReached();\n\t\tcontent.fromId = ParsePeerId(data.vfrom_id());\n\t\tif (content.fromId == context.selfPeerId) {\n\t\t\tcontent.fromSelf = true;\n\t\t}\n\t\tcontent.toId = ParsePeerId(data.vto_id());\n\t\tif (content.toId == context.selfPeerId) {\n\t\t\tcontent.toSelf = true;\n\t\t}\n\t\tcontent.distance = data.vdistance().v;\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionGroupCall &data) {\n\t\tauto content = ActionGroupCall();\n\t\tif (const auto duration = data.vduration()) {\n\t\t\tcontent.duration = duration->v;\n\t\t}\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionInviteToGroupCall &data) {\n\t\tauto content = ActionInviteToGroupCall();\n\t\tcontent.userIds.reserve(data.vusers().v.size());\n\t\tfor (const auto &user : data.vusers().v) {\n\t\t\tcontent.userIds.push_back(user.v);\n\t\t}\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionSetMessagesTTL &data) {\n\t\tresult.content = ActionSetMessagesTTL{\n\t\t\t.period = data.vperiod().v,\n\t\t};\n\t}, [&](const MTPDmessageActionGroupCallScheduled &data) {\n\t\tresult.content = ActionGroupCallScheduled{\n\t\t\t.date = data.vschedule_date().v,\n\t\t};\n\t}, [&](const MTPDmessageActionSetChatTheme &data) {\n\t\tdata.vtheme().match([&](const MTPDchatTheme &data) {\n\t\t\tresult.content = ActionSetChatTheme{\n\t\t\t\t.emoji = qs(data.vemoticon()),\n\t\t\t};\n\t\t}, [&](const MTPDchatThemeUniqueGift &data) {\n\t\t\tresult.content = ActionSetChatTheme{};\n\t\t});\n\t}, [&](const MTPDmessageActionChatJoinedByRequest &data) {\n\t\tresult.content = ActionChatJoinedByRequest();\n\t}, [&](const MTPDmessageActionWebViewDataSentMe &data) {\n\t\t// Should not be in user inbox.\n\t}, [&](const MTPDmessageActionWebViewDataSent &data) {\n\t\tauto content = ActionWebViewDataSent();\n\t\tcontent.text = ParseString(data.vtext());\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionGiftPremium &data) {\n\t\tauto content = ActionGiftPremium();\n\t\tcontent.cost = Ui::FillAmountAndCurrency(\n\t\t\tdata.vamount().v,\n\t\t\tqs(data.vcurrency())).toUtf8();\n\t\tcontent.days = data.vdays().v;\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionTopicCreate &data) {\n\t\tauto content = ActionTopicCreate();\n\t\tcontent.title = ParseString(data.vtitle());\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionTopicEdit &data) {\n\t\tauto content = ActionTopicEdit();\n\t\tif (const auto title = data.vtitle()) {\n\t\t\tcontent.title = ParseString(*title);\n\t\t}\n\t\tif (const auto icon = data.vicon_emoji_id()) {\n\t\t\tcontent.iconEmojiId = icon->v;\n\t\t}\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionSuggestProfilePhoto &data) {\n\t\tauto content = ActionSuggestProfilePhoto();\n\t\tcontent.photo = ParsePhoto(\n\t\t\tdata.vphoto(),\n\t\t\tmediaFolder\n\t\t\t+ \"photos/\"\n\t\t\t+ PreparePhotoFileName(++context.photos, date));\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionSetChatWallPaper &data) {\n\t\tauto content = ActionSetChatWallPaper();\n\t\t// #TODO wallpapers\n\t\tcontent.same = data.is_same();\n\t\tcontent.both = data.is_for_both();\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionRequestedPeer &data) {\n\t\tauto content = ActionRequestedPeer();\n\t\tfor (const auto &peer : data.vpeers().v) {\n\t\t\tcontent.peers.push_back(ParsePeerId(peer));\n\t\t}\n\t\tcontent.buttonId = data.vbutton_id().v;\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionGiftCode &data) {\n\t\tauto content = ActionGiftCode();\n\t\tcontent.boostPeerId = data.vboost_peer()\n\t\t\t? peerFromMTP(*data.vboost_peer())\n\t\t\t: PeerId();\n\t\tcontent.viaGiveaway = data.is_via_giveaway();\n\t\tcontent.unclaimed = data.is_unclaimed();\n\t\tcontent.days = data.vdays().v;\n\t\tcontent.code = data.vslug().v;\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionGiveawayLaunch &data) {\n\t\tauto content = ActionGiveawayLaunch();\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionGiveawayResults &data) {\n\t\tauto content = ActionGiveawayResults();\n\t\tcontent.winners = data.vwinners_count().v;\n\t\tcontent.unclaimed = data.vunclaimed_count().v;\n\t\tcontent.credits = data.is_stars();\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionBoostApply &data) {\n\t\tauto content = ActionBoostApply();\n\t\tcontent.boosts = data.vboosts().v;\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionRequestedPeerSentMe &data) {\n\t\t// Should not be in user inbox.\n\t}, [&](const MTPDmessageActionPaymentRefunded &data) {\n\t\tauto content = ActionPaymentRefunded();\n\t\tcontent.currency = ParseString(data.vcurrency());\n\t\tcontent.amount = data.vtotal_amount().v;\n\t\tcontent.peerId = ParsePeerId(data.vpeer());\n\t\tcontent.transactionId = data.vcharge().data().vid().v;\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionGiftStars &data) {\n\t\tauto content = ActionGiftCredits();\n\t\tcontent.cost = Ui::FillAmountAndCurrency(\n\t\t\tdata.vamount().v,\n\t\t\tqs(data.vcurrency())).toUtf8();\n\t\tcontent.amount = CreditsAmount(data.vstars().v, CreditsType::Stars);\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionGiftTon &data) {\n\t\tauto content = ActionGiftCredits();\n\t\tcontent.cost = Ui::FillAmountAndCurrency(\n\t\t\tdata.vamount().v,\n\t\t\tqs(data.vcurrency())).toUtf8();\n\t\tcontent.amount = CreditsAmount(\n\t\t\tdata.vamount().v / uint64(1'000'000'000),\n\t\t\tdata.vamount().v % uint64(1'000'000'000),\n\t\t\tCreditsType::Ton);\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionPrizeStars &data) {\n\t\tresult.content = ActionPrizeStars{\n\t\t\t.peerId = ParsePeerId(data.vboost_peer()),\n\t\t\t.amount = data.vstars().v,\n\t\t\t.transactionId = data.vtransaction_id().v,\n\t\t\t.giveawayMsgId = data.vgiveaway_msg_id().v,\n\t\t\t.isUnclaimed = data.is_unclaimed(),\n\t\t};\n\t}, [&](const MTPDmessageActionStarGift &data) {\n\t\tauto content = ParseStarGift(data.vgift());\n\t\tcontent.text = (data.vmessage()\n\t\t\t? ParseText(\n\t\t\t\tdata.vmessage()->data().vtext(),\n\t\t\t\tdata.vmessage()->data().ventities().v)\n\t\t\t: std::vector<TextPart>());\n\t\tcontent.anonymous = data.is_name_hidden();\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionStarGiftUnique &data) {\n\t\tresult.content = ParseStarGift(data.vgift());\n\t}, [&](const MTPDmessageActionPaidMessagesRefunded &data) {\n\t\tresult.content = ActionPaidMessagesRefunded{\n\t\t\t.messages = data.vcount().v,\n\t\t\t.stars = int64(data.vstars().v),\n\t\t};\n\t}, [&](const MTPDmessageActionPaidMessagesPrice &data) {\n\t\tresult.content = ActionPaidMessagesPrice{\n\t\t\t.stars = int(data.vstars().v),\n\t\t\t.broadcastAllowed = data.is_broadcast_messages_allowed(),\n\t\t};\n\t}, [&](const MTPDmessageActionTodoCompletions &data) {\n\t\tconst auto take = [](const MTPVector<MTPint> &list) {\n\t\t\treturn list.v\n\t\t\t\t| ranges::views::transform(&MTPint::v)\n\t\t\t\t| ranges::to_vector;\n\t\t};\n\t\tresult.content = ActionTodoCompletions{\n\t\t\t.completed = take(data.vcompleted()),\n\t\t\t.incompleted = take(data.vincompleted()),\n\t\t};\n\t}, [&](const MTPDmessageActionTodoAppendTasks &data) {\n\t\tresult.content = ActionTodoAppendTasks{\n\t\t\t.items = data.vlist().v\n\t\t\t\t| ranges::views::transform(ParseTodoListItem)\n\t\t\t\t| ranges::to_vector,\n\t\t};\n\t}, [&](const MTPDmessageActionPollAppendAnswer &data) {\n\t\tresult.content = ActionPollAppendAnswer{};\n\t}, [&](const MTPDmessageActionSuggestedPostApproval &data) {\n\t\tresult.content = ActionSuggestedPostApproval{\n\t\t\t.rejectComment = data.vreject_comment().value_or_empty(),\n\t\t\t.scheduleDate = data.vschedule_date().value_or_empty(),\n\t\t\t.price = CreditsAmountFromTL(data.vprice()),\n\t\t\t.rejected = data.is_rejected(),\n\t\t\t.balanceTooLow = data.is_balance_too_low(),\n\t\t};\n\t}, [&](const MTPDmessageActionSuggestedPostSuccess &data) {\n\t\tresult.content = ActionSuggestedPostSuccess{\n\t\t\t.price = CreditsAmountFromTL(data.vprice()),\n\t\t};\n\t}, [&](const MTPDmessageActionSuggestedPostRefund &data) {\n\t\tresult.content = ActionSuggestedPostRefund{\n\t\t\t.payerInitiated = data.is_payer_initiated(),\n\t\t};\n\t}, [&](const MTPDmessageActionConferenceCall &data) {\n\t\tauto content = ActionPhoneCall();\n\t\tusing State = ActionPhoneCall::State;\n\t\tcontent.conferenceId = data.vcall_id().v;\n\t\tif (const auto duration = data.vduration()) {\n\t\t\tcontent.duration = duration->v;\n\t\t}\n\t\tcontent.state = data.is_missed()\n\t\t\t? State::Missed\n\t\t\t: data.is_active()\n\t\t\t? State::Active\n\t\t\t: data.vduration().value_or_empty()\n\t\t\t? State::Hangup\n\t\t\t: State::Invitation;\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionSuggestBirthday &data) {\n\t\tauto content = ActionSuggestBirthday();\n\t\tconst auto &fields = data.vbirthday().data();\n\t\tcontent.birthday = Birthday(\n\t\t\tfields.vday().v,\n\t\t\tfields.vmonth().v,\n\t\t\tfields.vyear().value_or_empty());\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionStarGiftPurchaseOffer &data) {\n\t\tauto content = ParseStarGift(data.vgift());\n\t\tcontent.offer = true;\n\t\tcontent.offerPrice = CreditsAmountFromTL(data.vprice());\n\t\tcontent.offerExpireAt = data.vexpires_at().v;\n\t\tcontent.offerAccepted = data.is_accepted();\n\t\tcontent.offerDeclined = data.is_declined();\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionStarGiftPurchaseOfferDeclined &data) {\n\t\tauto content = ParseStarGift(data.vgift());\n\t\tcontent.offer = true;\n\t\tcontent.offerDeclined = true;\n\t\tcontent.offerExpired = data.is_expired();\n\t\tcontent.offerPrice = CreditsAmountFromTL(data.vprice());\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionNewCreatorPending &data) {\n\t\tauto content = ActionNewCreatorPending();\n\t\tcontent.newCreatorId = data.vnew_creator_id().v;\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionChangeCreator &data) {\n\t\tauto content = ActionChangeCreator();\n\t\tcontent.newCreatorId = data.vnew_creator_id().v;\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionNoForwardsToggle &data) {\n\t\tauto content = ActionNoForwardsToggle();\n\t\tcontent.newValue = (data.vnew_value().type() == mtpc_boolTrue);\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionNoForwardsRequest &data) {\n\t\tauto content = ActionNoForwardsRequest();\n\t\tcontent.expired = data.is_expired();\n\t\tcontent.newValue = (data.vnew_value().type() == mtpc_boolTrue);\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionManagedBotCreated &data) {\n\t\tauto content = ActionManagedBotCreated();\n\t\tcontent.botId = data.vbot_id().v;\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionPollAppendAnswer &data) {\n\t\tauto content = ActionPollAppendAnswer();\n\t\tdata.vanswer().match([&](const MTPDpollAnswer &answer) {\n\t\t\tcontent.option = ParseString(answer.vtext().match(\n\t\t\t\t[](const MTPDtextWithEntities &d) {\n\t\t\t\t\treturn d.vtext();\n\t\t\t\t}));\n\t\t}, [](const auto &) {});\n\t\tresult.content = content;\n\t}, [&](const MTPDmessageActionPollDeleteAnswer &data) {\n\t\tauto content = ActionPollDeleteAnswer();\n\t\tdata.vanswer().match([&](const MTPDpollAnswer &answer) {\n\t\t\tcontent.option = ParseString(answer.vtext().match(\n\t\t\t\t[](const MTPDtextWithEntities &d) {\n\t\t\t\t\treturn d.vtext();\n\t\t\t\t}));\n\t\t}, [](const auto &) {});\n\t\tresult.content = content;\n\t}, [](const MTPDmessageActionEmpty &data) {});\n\treturn result;\n}\n\nFile &Message::file() {\n\tconst auto content = &action.content;\n\tif (const auto photo = std::get_if<ActionChatEditPhoto>(content)) {\n\t\treturn photo->photo.image.file;\n\t} else if (const auto photo = std::get_if<ActionSuggestProfilePhoto>(\n\t\t\tcontent)) {\n\t\treturn photo->photo.image.file;\n\t// } else if (const auto wallpaper = std::get_if<ActionSetChatWallPaper>(\n\t// \t\tcontent)) {\n\t// #TODO wallpapers\n\t}\n\treturn media.file();\n}\n\nconst File &Message::file() const {\n\tconst auto content = &action.content;\n\tif (const auto photo = std::get_if<ActionChatEditPhoto>(content)) {\n\t\treturn photo->photo.image.file;\n\t} else if (const auto photo = std::get_if<ActionSuggestProfilePhoto>(\n\t\t\tcontent)) {\n\t\treturn photo->photo.image.file;\n\t// } else if (const auto wallpaper = std::get_if<ActionSetChatWallPaper>(\n\t// \t\tcontent)) {\n\t// #TODO wallpapers\n\t}\n\treturn media.file();\n}\n\nImage &Message::thumb() {\n\treturn media.thumb();\n}\n\nconst Image &Message::thumb() const {\n\treturn media.thumb();\n}\n\nMessage ParseMessage(\n\t\tParseMediaContext &context,\n\t\tconst MTPMessage &data,\n\t\tconst QString &mediaFolder) {\n\tauto result = Message();\n\tdata.match([&](const auto &data) {\n\t\tresult.id = data.vid().v;\n\t\tif constexpr (!MTPDmessageEmpty::Is<decltype(data)>()) {\n\t\t\tresult.date = data.vdate().v;\n\t\t\tresult.out = data.is_out();\n\t\t\tresult.selfId = context.selfPeerId;\n\t\t\tresult.peerId = ParsePeerId(data.vpeer_id());\n\t\t\tconst auto fromId = data.vfrom_id();\n\t\t\tif (fromId) {\n\t\t\t\tresult.fromId = ParsePeerId(*fromId);\n\t\t\t} else {\n\t\t\t\tresult.fromId = result.peerId;\n\t\t\t}\n\t\t\tif (const auto replyTo = data.vreply_to()) {\n\t\t\t\treplyTo->match([&](const MTPDmessageReplyHeader &data) {\n\t\t\t\t\tif (const auto replyToMsg = data.vreply_to_msg_id()) {\n\t\t\t\t\t\tresult.replyToMsgId = replyToMsg->v;\n\t\t\t\t\t\tresult.replyToPeerId = data.vreply_to_peer_id()\n\t\t\t\t\t\t\t? ParsePeerId(*data.vreply_to_peer_id())\n\t\t\t\t\t\t\t: 0;\n\t\t\t\t\t\tif (result.replyToPeerId == result.peerId) {\n\t\t\t\t\t\t\tresult.replyToPeerId = 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// #TODO export replies\n\t\t\t\t\t}\n\t\t\t\t}, [&](const MTPDmessageReplyStoryHeader &data) {\n\t\t\t\t\t// #TODO export stories\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t});\n\tdata.match([&](const MTPDmessage &data) {\n\t\tif (const auto editDate = data.vedit_date()) {\n\t\t\tresult.edited = editDate->v;\n\t\t}\n\t\tif (const auto fwdFrom = data.vfwd_from()) {\n\t\t\tresult.forwardedFromId = fwdFrom->match(\n\t\t\t[](const MTPDmessageFwdHeader &data) {\n\t\t\t\treturn data.vfrom_id()\n\t\t\t\t\t? ParsePeerId(*data.vfrom_id())\n\t\t\t\t\t: PeerId(0);\n\t\t\t});\n\t\t\tresult.forwardedFromName = fwdFrom->match(\n\t\t\t[](const MTPDmessageFwdHeader &data) {\n\t\t\t\tif (const auto fromName = data.vfrom_name()) {\n\t\t\t\t\treturn fromName->v;\n\t\t\t\t}\n\t\t\t\treturn QByteArray();\n\t\t\t});\n\t\t\tresult.forwardedDate = fwdFrom->match(\n\t\t\t[](const MTPDmessageFwdHeader &data) {\n\t\t\t\treturn data.vdate().v;\n\t\t\t});\n\t\t\tresult.savedFromChatId = fwdFrom->match(\n\t\t\t[](const MTPDmessageFwdHeader &data) {\n\t\t\t\tif (const auto savedFromPeer = data.vsaved_from_peer()) {\n\t\t\t\t\treturn ParsePeerId(*savedFromPeer);\n\t\t\t\t}\n\t\t\t\treturn PeerId(0);\n\t\t\t});\n\t\t\tresult.forwarded = result.forwardedFromId\n\t\t\t\t|| !result.forwardedFromName.isEmpty();\n\t\t\tresult.showForwardedAsOriginal = result.forwarded\n\t\t\t\t&& result.savedFromChatId;\n\t\t}\n\t\tif (const auto postAuthor = data.vpost_author()) {\n\t\t\tresult.signature = ParseString(*postAuthor);\n\t\t}\n\t\tif (const auto replyTo = data.vreply_to()) {\n\t\t\treplyTo->match([&](const MTPDmessageReplyHeader &data) {\n\t\t\t\tif (const auto replyToMsg = data.vreply_to_msg_id()) {\n\t\t\t\t\tresult.replyToMsgId = replyToMsg->v;\n\t\t\t\t\tresult.replyToPeerId = data.vreply_to_peer_id()\n\t\t\t\t\t\t? ParsePeerId(*data.vreply_to_peer_id())\n\t\t\t\t\t\t: PeerId(0);\n\t\t\t\t} else {\n\t\t\t\t\t// #TODO export replies\n\t\t\t\t}\n\t\t\t}, [&](const MTPDmessageReplyStoryHeader &data) {\n\t\t\t\t// #TODO export stories\n\t\t\t});\n\t\t}\n\t\tif (const auto viaBotId = data.vvia_bot_id()) {\n\t\t\tresult.viaBotId = viaBotId->v;\n\t\t}\n\t\tif (const auto media = data.vmedia()) {\n\t\t\tcontext.botId = result.viaBotId\n\t\t\t\t? result.viaBotId\n\t\t\t\t: peerIsUser(result.forwardedFromId)\n\t\t\t\t? peerToUser(result.forwardedFromId)\n\t\t\t\t: peerToUser(result.fromId);\n\t\t\tresult.media = ParseMedia(\n\t\t\t\tcontext,\n\t\t\t\t*media,\n\t\t\t\tmediaFolder,\n\t\t\t\tresult.date);\n\t\t\tif (result.media.ttl && !data.is_out()) {\n\t\t\t\tresult.media.file() = File();\n\t\t\t\tresult.media.thumb().file = File();\n\t\t\t}\n\t\t\tcontext.botId = 0;\n\t\t}\n\t\tif (const auto replyMarkup = data.vreply_markup()) {\n\t\t\treplyMarkup->match([](const MTPDreplyKeyboardMarkup &) {\n\t\t\t}, [&](const MTPDreplyInlineMarkup &data) {\n\t\t\t\tresult.inlineButtonRows = ButtonRowsFromTL(data);\n\t\t\t}, [](const MTPDreplyKeyboardHide &) {\n\t\t\t}, [](const MTPDreplyKeyboardForceReply &) {\n\t\t\t});\n\t\t}\n\t\tresult.text = ParseText(\n\t\t\tdata.vmessage(),\n\t\t\tdata.ventities().value_or_empty());\n\t\t\tif (data.vreactions().has_value()) {\n\t\t\t\tresult.reactions = ParseReactions(*data.vreactions());\n\t\t\t}\n\t}, [&](const MTPDmessageService &data) {\n\t\tresult.action = ParseServiceAction(\n\t\t\tcontext,\n\t\t\tdata.vaction(),\n\t\t\tmediaFolder,\n\t\t\tresult.date);\n\t}, [&](const MTPDmessageEmpty &data) {\n\t\tresult.id = data.vid().v;\n\t});\n\treturn result;\n}\n\nstd::map<MessageId, Message> ParseMessagesList(\n\t\tPeerId selfId,\n\t\tconst MTPVector<MTPMessage> &data,\n\t\tconst QString &mediaFolder) {\n\tauto context = ParseMediaContext{ .selfPeerId = selfId };\n\tauto result = std::map<MessageId, Message>();\n\tfor (const auto &message : data.v) {\n\t\tauto parsed = ParseMessage(context, message, mediaFolder);\n\t\tresult.emplace(\n\t\t\tMessageId{ peerToChannel(parsed.peerId), parsed.id },\n\t\t\tstd::move(parsed));\n\t}\n\treturn result;\n}\n\nPersonalInfo ParsePersonalInfo(const MTPDusers_userFull &data) {\n\tauto result = PersonalInfo();\n\tresult.user = ParseUser(data.vusers().v[0]);\n\tdata.vfull_user().match([&](const MTPDuserFull &data) {\n\t\tif (const auto about = data.vabout()) {\n\t\t\tresult.bio = ParseString(*about);\n\t\t}\n\t});\n\treturn result;\n}\n\nContactsList ParseContactsList(const MTPcontacts_Contacts &data) {\n\tExpects(data.type() == mtpc_contacts_contacts);\n\n\tauto result = ContactsList();\n\tconst auto &contacts = data.c_contacts_contacts();\n\tconst auto map = ParseUsersList(contacts.vusers());\n\tresult.list.reserve(contacts.vcontacts().v.size());\n\tfor (const auto &contact : contacts.vcontacts().v) {\n\t\tconst auto userId = contact.c_contact().vuser_id().v;\n\t\tif (const auto i = map.find(userId); i != end(map)) {\n\t\t\tresult.list.push_back(i->second.info);\n\t\t} else {\n\t\t\tresult.list.push_back(EmptyUser(userId).info);\n\t\t}\n\t}\n\treturn result;\n}\n\nContactsList ParseContactsList(const MTPVector<MTPSavedContact> &data) {\n\tauto result = ContactsList();\n\tresult.list.reserve(data.v.size());\n\tfor (const auto &contact : data.v) {\n\t\tauto info = contact.match([](const MTPDsavedPhoneContact &data) {\n\t\t\tauto info = ContactInfo();\n\t\t\tinfo.firstName = ParseString(data.vfirst_name());\n\t\t\tinfo.lastName = ParseString(data.vlast_name());\n\t\t\tinfo.phoneNumber = ParseString(data.vphone());\n\t\t\tinfo.date = data.vdate().v;\n\t\t\tinfo.colorIndex = PeerColorIndex(\n\t\t\t\tStringBarePeerId(info.phoneNumber));\n\t\t\treturn info;\n\t\t});\n\t\tresult.list.push_back(std::move(info));\n\t}\n\treturn result;\n}\n\nstd::vector<int> SortedContactsIndices(const ContactsList &data) {\n\tconst auto names = ranges::views::all(\n\t\tdata.list\n\t) | ranges::views::transform([](const Data::ContactInfo &info) {\n\t\treturn (QString::fromUtf8(info.firstName)\n\t\t\t+ ' '\n\t\t\t+ QString::fromUtf8(info.lastName)).toLower();\n\t}) | ranges::to_vector;\n\n\tauto indices = ranges::views::ints(0, int(data.list.size()))\n\t\t| ranges::to_vector;\n\tranges::sort(indices, [&](int i, int j) {\n\t\treturn names[i] < names[j];\n\t});\n\treturn indices;\n}\n\nbool AppendTopPeers(ContactsList &to, const MTPcontacts_TopPeers &data) {\n\treturn data.match([](const MTPDcontacts_topPeersNotModified &data) {\n\t\treturn false;\n\t}, [](const MTPDcontacts_topPeersDisabled &data) {\n\t\treturn true;\n\t}, [&](const MTPDcontacts_topPeers &data) {\n\t\tconst auto peers = ParsePeersLists(data.vusers(), data.vchats());\n\t\tconst auto append = [&](\n\t\t\t\tstd::vector<TopPeer> &to,\n\t\t\t\tconst MTPVector<MTPTopPeer> &list) {\n\t\t\tfor (const auto &topPeer : list.v) {\n\t\t\t\tto.push_back(topPeer.match([&](const MTPDtopPeer &data) {\n\t\t\t\t\tconst auto peerId = ParsePeerId(data.vpeer());\n\t\t\t\t\tauto peer = [&] {\n\t\t\t\t\t\tconst auto i = peers.find(peerId);\n\t\t\t\t\t\treturn (i != peers.end())\n\t\t\t\t\t\t\t? i->second\n\t\t\t\t\t\t\t: EmptyPeer(peerId);\n\t\t\t\t\t}();\n\t\t\t\t\treturn TopPeer{\n\t\t\t\t\t\tPeer{ std::move(peer) },\n\t\t\t\t\t\tdata.vrating().v\n\t\t\t\t\t};\n\t\t\t\t}));\n\t\t\t}\n\t\t};\n\t\tfor (const auto &list : data.vcategories().v) {\n\t\t\tconst auto appended = list.match(\n\t\t\t[&](const MTPDtopPeerCategoryPeers &data) {\n\t\t\t\tconst auto category = data.vcategory().type();\n\t\t\t\tif (category == mtpc_topPeerCategoryCorrespondents) {\n\t\t\t\t\tappend(to.correspondents, data.vpeers());\n\t\t\t\t\treturn true;\n\t\t\t\t} else if (category == mtpc_topPeerCategoryBotsInline) {\n\t\t\t\t\tappend(to.inlineBots, data.vpeers());\n\t\t\t\t\treturn true;\n\t\t\t\t} else if (category == mtpc_topPeerCategoryPhoneCalls) {\n\t\t\t\t\tappend(to.phoneCalls, data.vpeers());\n\t\t\t\t\treturn true;\n\t\t\t\t} else {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t});\n\t\t\tif (!appended) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t});\n}\n\nSession ParseSession(const MTPAuthorization &data) {\n\treturn data.match([&](const MTPDauthorization &data) {\n\t\tauto result = Session();\n\t\tresult.applicationId = data.vapi_id().v;\n\t\tresult.platform = ParseString(data.vplatform());\n\t\tresult.deviceModel = ParseString(data.vdevice_model());\n\t\tresult.systemVersion = ParseString(data.vsystem_version());\n\t\tresult.applicationName = ParseString(data.vapp_name());\n\t\tresult.applicationVersion = ParseString(data.vapp_version());\n\t\tresult.created = data.vdate_created().v;\n\t\tresult.lastActive = data.vdate_active().v;\n\t\tresult.ip = ParseString(data.vip());\n\t\tresult.country = ParseString(data.vcountry());\n\t\tresult.region = ParseString(data.vregion());\n\t\treturn result;\n\t});\n}\n\nSessionsList ParseSessionsList(const MTPaccount_Authorizations &data) {\n\treturn data.match([](const MTPDaccount_authorizations &data) {\n\t\tauto result = SessionsList();\n\t\tconst auto &list = data.vauthorizations().v;\n\t\tresult.list.reserve(list.size());\n\t\tfor (const auto &session : list) {\n\t\t\tresult.list.push_back(ParseSession(session));\n\t\t}\n\t\treturn result;\n\t});\n}\n\nWebSession ParseWebSession(\n\t\tconst MTPWebAuthorization &data,\n\t\tconst std::map<UserId, User> &users) {\n\treturn data.match([&](const MTPDwebAuthorization &data) {\n\t\tauto result = WebSession();\n\t\tconst auto i = users.find(data.vbot_id().v);\n\t\tif (i != users.end() && i->second.isBot) {\n\t\t\tresult.botUsername = i->second.username;\n\t\t}\n\t\tresult.domain = ParseString(data.vdomain());\n\t\tresult.platform = ParseString(data.vplatform());\n\t\tresult.browser = ParseString(data.vbrowser());\n\t\tresult.created = data.vdate_created().v;\n\t\tresult.lastActive = data.vdate_active().v;\n\t\tresult.ip = ParseString(data.vip());\n\t\tresult.region = ParseString(data.vregion());\n\t\treturn result;\n\t});\n}\n\nSessionsList ParseWebSessionsList(\n\t\tconst MTPaccount_WebAuthorizations &data) {\n\treturn data.match([&](const MTPDaccount_webAuthorizations &data) {\n\t\tauto result = SessionsList();\n\t\tconst auto users = ParseUsersList(data.vusers());\n\t\tconst auto &list = data.vauthorizations().v;\n\t\tresult.webList.reserve(list.size());\n\t\tfor (const auto &session : list) {\n\t\t\tresult.webList.push_back(ParseWebSession(session, users));\n\t\t}\n\t\treturn result;\n\t});\n}\n\nDialogInfo *DialogsInfo::item(int index) {\n\tconst auto chatsCount = chats.size();\n\treturn (index < 0)\n\t\t? nullptr\n\t\t: (index < chatsCount)\n\t\t? &chats[index]\n\t\t: (index - chatsCount < left.size())\n\t\t? &left[index - chatsCount]\n\t\t: nullptr;\n}\n\nconst DialogInfo *DialogsInfo::item(int index) const {\n\tconst auto chatsCount = chats.size();\n\treturn (index < 0)\n\t\t? nullptr\n\t\t: (index < chatsCount)\n\t\t? &chats[index]\n\t\t: (index - chatsCount < left.size())\n\t\t? &left[index - chatsCount]\n\t\t: nullptr;\n}\n\nDialogInfo::Type DialogTypeFromChat(const Chat &chat) {\n\tusing Type = DialogInfo::Type;\n\treturn (chat.isMonoforum && !chat.isMonoforumAdmin)\n\t\t? Type::Personal\n\t\t: (chat.isMonoforumAdmin && chat.isMonoforumOfPublicBroadcast)\n\t\t? Type::PublicSupergroup\n\t\t: chat.isMonoforumAdmin\n\t\t? Type::PrivateSupergroup\n\t\t: chat.username.isEmpty()\n\t\t? (chat.isBroadcast\n\t\t\t? Type::PrivateChannel\n\t\t\t: chat.isSupergroup\n\t\t\t? Type::PrivateSupergroup\n\t\t\t: Type::PrivateGroup)\n\t\t: (chat.isBroadcast\n\t\t\t? Type::PublicChannel\n\t\t\t: Type::PublicSupergroup);\n}\n\nDialogInfo::Type DialogTypeFromUser(const User &user) {\n\treturn user.isSelf\n\t\t? DialogInfo::Type::Self\n\t\t: user.isReplies\n\t\t? DialogInfo::Type::Replies\n\t\t: user.isVerifyCodes\n\t\t? DialogInfo::Type::VerifyCodes\n\t\t: user.isBot\n\t\t? DialogInfo::Type::Bot\n\t\t: DialogInfo::Type::Personal;\n}\n\nDialogsInfo ParseDialogsInfo(const MTPmessages_Dialogs &data) {\n\tauto result = DialogsInfo();\n\tconst auto folder = QString();\n\tdata.match([](const MTPDmessages_dialogsNotModified &data) {\n\t\tUnexpected(\"dialogsNotModified in ParseDialogsInfo.\");\n\t}, [&](const auto &data) { // MTPDmessages_dialogs &data) {\n\t\tconst auto peers = ParsePeersLists(data.vusers(), data.vchats());\n\t\tconst auto messages = ParseMessagesList(\n\t\t\tPeerId(0), // selfId, not used, we want only messages dates here.\n\t\t\tdata.vmessages(),\n\t\t\tfolder);\n\t\tresult.chats.reserve(result.chats.size() + data.vdialogs().v.size());\n\t\tfor (const auto &dialog : data.vdialogs().v) {\n\t\t\tif (dialog.type() != mtpc_dialog) {\n\t\t\t\tLOG((\"API Error: Unexpected dialog type in chats export.\"));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto &fields = dialog.c_dialog();\n\n\t\t\tauto info = DialogInfo();\n\t\t\tinfo.peerId = ParsePeerId(fields.vpeer());\n\t\t\tconst auto peerIt = peers.find(info.peerId);\n\t\t\tif (peerIt != end(peers)) {\n\t\t\t\tconst auto &peer = peerIt->second;\n\t\t\t\tinfo.type = peer.user()\n\t\t\t\t\t? DialogTypeFromUser(*peer.user())\n\t\t\t\t\t: DialogTypeFromChat(*peer.chat());\n\t\t\t\tinfo.name = peer.user()\n\t\t\t\t\t? peer.user()->info.firstName\n\t\t\t\t\t: peer.name();\n\t\t\t\tinfo.lastName = peer.user()\n\t\t\t\t\t? peer.user()->info.lastName\n\t\t\t\t\t: Utf8String();\n\t\t\t\tinfo.colorIndex = peer.colorIndex();\n\t\t\t\tinfo.input = peer.input();\n\t\t\t\tinfo.migratedToChannelId = peer.chat()\n\t\t\t\t\t? peer.chat()->migratedToChannelId\n\t\t\t\t\t: 0;\n\t\t\t\tinfo.isMonoforum = peer.chat()\n\t\t\t\t\t&& peer.chat()->isMonoforum;\n\t\t\t\tinfo.monoforumBroadcastInput = peer.chat()\n\t\t\t\t\t? peer.chat()->monoforumBroadcastInput\n\t\t\t\t\t: MTPInputPeer(MTP_inputPeerEmpty());\n\t\t\t}\n\t\t\tinfo.topMessageId = fields.vtop_message().v;\n\t\t\tconst auto messageIt = messages.find(MessageId{\n\t\t\t\tpeerToChannel(info.peerId),\n\t\t\t\tinfo.topMessageId,\n\t\t\t});\n\t\t\tif (messageIt != end(messages)) {\n\t\t\t\tconst auto &message = messageIt->second;\n\t\t\t\tinfo.topMessageDate = message.date;\n\t\t\t}\n\t\t\tresult.chats.push_back(std::move(info));\n\t\t}\n\t});\n\treturn result;\n}\n\nDialogInfo DialogInfoFromUser(const User &data) {\n\tauto result = DialogInfo();\n\tresult.input = (Peer{ data }).input();\n\tresult.name = data.info.firstName;\n\tresult.lastName = data.info.lastName;\n\tresult.peerId = data.id();\n\tresult.topMessageDate = 0;\n\tresult.topMessageId = 0;\n\tresult.type = DialogTypeFromUser(data);\n\tresult.isLeftChannel = false;\n\treturn result;\n}\n\nDialogInfo DialogInfoFromChat(const Chat &data) {\n\tauto result = DialogInfo();\n\tresult.input = data.input;\n\tresult.name = data.title;\n\tresult.peerId = data.id();\n\tresult.topMessageDate = 0;\n\tresult.topMessageId = 0;\n\tresult.type = DialogTypeFromChat(data);\n\tresult.migratedToChannelId = data.migratedToChannelId;\n\tresult.isMonoforum = data.isMonoforum;\n\tif (data.isMonoforumAdmin) {\n\t\tresult.monoforumBroadcastInput = data.monoforumBroadcastInput;\n\t}\n\treturn result;\n}\n\nDialogsInfo ParseLeftChannelsInfo(const MTPmessages_Chats &data) {\n\tauto result = DialogsInfo();\n\tdata.match([&](const auto &data) { //MTPDmessages_chats &data) {\n\t\tresult.left.reserve(data.vchats().v.size());\n\t\tfor (const auto &single : data.vchats().v) {\n\t\t\tauto info = DialogInfoFromChat(ParseChat(single));\n\t\t\tinfo.isLeftChannel = true;\n\t\t\tresult.left.push_back(std::move(info));\n\t\t}\n\t});\n\treturn result;\n}\n\nDialogsInfo ParseDialogsInfo(\n\t\tconst MTPInputPeer &singlePeer,\n\t\tconst MTPVector<MTPUser> &data) {\n\tconst auto singleId = singlePeer.match(\n\t[](const MTPDinputPeerUser &data) {\n\t\treturn UserId(data.vuser_id());\n\t}, [](const MTPDinputPeerSelf &data) {\n\t\treturn UserId();\n\t}, [](const auto &data) -> UserId {\n\t\tUnexpected(\"Single peer type in ParseDialogsInfo(users).\");\n\t});\n\tauto result = DialogsInfo();\n\tresult.chats.reserve(data.v.size());\n\tfor (const auto &single : data.v) {\n\t\tconst auto userId = single.match([&](const auto &data) {\n\t\t\treturn peerFromUser(data.vid());\n\t\t});\n\t\tif (userId != singleId\n\t\t\t&& (singleId != 0\n\t\t\t\t|| single.type() != mtpc_user\n\t\t\t\t|| !single.c_user().is_self())) {\n\t\t\tcontinue;\n\t\t}\n\t\tauto info = DialogInfoFromUser(ParseUser(single));\n\t\tresult.chats.push_back(std::move(info));\n\t}\n\treturn result;\n}\n\nDialogsInfo ParseDialogsInfo(\n\t\tconst MTPInputPeer &singlePeer,\n\t\tconst MTPmessages_Chats &data) {\n\tconst auto singleId = singlePeer.match(\n\t[](const MTPDinputPeerChat &data) {\n\t\treturn peerFromChat(data.vchat_id());\n\t}, [](const MTPDinputPeerChannel &data) {\n\t\treturn peerFromChannel(data.vchannel_id());\n\t}, [](const auto &data) -> PeerId {\n\t\tUnexpected(\"Single peer type in ParseDialogsInfo(chats).\");\n\t});\n\tauto result = DialogsInfo();\n\tdata.match([&](const auto &data) { //MTPDmessages_chats &data) {\n\t\tresult.chats.reserve(data.vchats().v.size());\n\t\tfor (const auto &single : data.vchats().v) {\n\t\t\tconst auto peerId = single.match([](const MTPDchannel &data) {\n\t\t\t\treturn peerFromChannel(data.vid());\n\t\t\t}, [](const MTPDchannelForbidden &data) {\n\t\t\t\treturn peerFromChannel(data.vid());\n\t\t\t}, [](const auto &data) {\n\t\t\t\treturn peerFromChat(data.vid());\n\t\t\t});\n\t\t\tif (peerId != singleId) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto chat = ParseChat(single);\n\t\t\tauto info = DialogInfoFromChat(ParseChat(single));\n\t\t\tinfo.isLeftChannel = false;\n\t\t\tresult.chats.push_back(std::move(info));\n\t\t}\n\t});\n\treturn result;\n}\n\nbool AddMigrateFromSlice(\n\t\tDialogInfo &to,\n\t\tconst DialogInfo &from,\n\t\tint splitIndex,\n\t\tint splitsCount) {\n\tExpects(splitIndex < splitsCount);\n\n\tconst auto good = to.migratedFromInput.match([](\n\t\t\tconst MTPDinputPeerEmpty &) {\n\t\treturn true;\n\t}, [&](const MTPDinputPeerChat &data) {\n\t\treturn (peerFromChat(data.vchat_id()) == from.peerId);\n\t}, [](auto&&) { return false; });\n\tif (!good) {\n\t\treturn false;\n\t}\n\tAssert(from.splits.size() == from.messagesCountPerSplit.size());\n\tfor (auto i = 0, count = int(from.splits.size()); i != count; ++i) {\n\t\tAssert(from.splits[i] < splitsCount);\n\t\tto.splits.push_back(from.splits[i] - splitsCount);\n\t\tto.messagesCountPerSplit.push_back(from.messagesCountPerSplit[i]);\n\t}\n\tto.migratedFromInput = from.input;\n\tto.splits.push_back(splitIndex - splitsCount);\n\tto.messagesCountPerSplit.push_back(0);\n\treturn true;\n}\n\nvoid FinalizeDialogsInfo(DialogsInfo &info, const Settings &settings) {\n\tauto &chats = info.chats;\n\tauto &left = info.left;\n\tconst auto fullCount = chats.size() + left.size();\n\tconst auto digits = Data::NumberToString(fullCount - 1).size();\n\tauto index = 0;\n\tfor (auto &dialog : chats) {\n\t\tconst auto number = Data::NumberToString(++index, digits, '0');\n\t\tdialog.relativePath = settings.onlySinglePeer()\n\t\t\t? QString()\n\t\t\t: \"chats/chat_\" + QString::fromUtf8(number) + '/';\n\n\t\tusing DialogType = DialogInfo::Type;\n\t\tusing Type = Settings::Type;\n\t\tconst auto setting = [&] {\n\t\t\tswitch (dialog.type) {\n\t\t\tcase DialogType::Self:\n\t\t\tcase DialogType::Personal: return Type::PersonalChats;\n\t\t\tcase DialogType::Bot: return Type::BotChats;\n\t\t\tcase DialogType::PrivateGroup:\n\t\t\tcase DialogType::PrivateSupergroup: return Type::PrivateGroups;\n\t\t\tcase DialogType::PrivateChannel: return Type::PrivateChannels;\n\t\t\tcase DialogType::PublicSupergroup: return Type::PublicGroups;\n\t\t\tcase DialogType::PublicChannel: return Type::PublicChannels;\n\t\t\t}\n\t\t\tUnexpected(\"Type in ApiWrap::onlyMyMessages.\");\n\t\t}();\n\t\tdialog.onlyMyMessages = (dialog.type != DialogType::Personal)\n\t\t\t&& ((settings.fullChats & setting) != setting);\n\n\t\tranges::sort(dialog.splits);\n\t}\n\tfor (auto &dialog : left) {\n\t\tAssert(!settings.onlySinglePeer());\n\n\t\tconst auto number = Data::NumberToString(++index, digits, '0');\n\t\tdialog.relativePath = \"chats/chat_\" + number + '/';\n\t\tdialog.onlyMyMessages = true;\n\t}\n}\n\nMessagesSlice ParseMessagesSlice(\n\t\tParseMediaContext &context,\n\t\tconst MTPVector<MTPMessage> &data,\n\t\tconst MTPVector<MTPUser> &users,\n\t\tconst MTPVector<MTPChat> &chats,\n\t\tconst QString &mediaFolder) {\n\tconst auto &list = data.v;\n\tauto result = MessagesSlice();\n\tresult.list.reserve(list.size());\n\tfor (auto i = list.size(); i != 0;) {\n\t\tconst auto &message = list[--i];\n\t\tresult.list.push_back(ParseMessage(context, message, mediaFolder));\n\t}\n\tresult.peers = ParsePeersLists(users, chats);\n\treturn result;\n}\n\nMessagesSlice AdjustMigrateMessageIds(MessagesSlice slice) {\n\tfor (auto &message : slice.list) {\n\t\tmessage.id += kMigratedMessagesIdShift;\n\t\tif (message.replyToMsgId && !message.replyToPeerId) {\n\t\t\tmessage.replyToMsgId += kMigratedMessagesIdShift;\n\t\t}\n\t}\n\treturn slice;\n}\n\nTimeId SingleMessageDate(const MTPmessages_Messages &data) {\n\treturn data.match([&](const MTPDmessages_messagesNotModified &data) {\n\t\treturn 0;\n\t}, [&](const auto &data) {\n\t\tconst auto &list = data.vmessages().v;\n\t\tif (list.isEmpty()) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn list[0].match([](const MTPDmessageEmpty &data) {\n\t\t\treturn 0;\n\t\t}, [](const auto &data) {\n\t\t\treturn data.vdate().v;\n\t\t});\n\t});\n}\n\nbool SingleMessageBefore(\n\t\tconst MTPmessages_Messages &data,\n\t\tTimeId date) {\n\tconst auto single = SingleMessageDate(data);\n\treturn (single > 0 && single < date);\n}\n\nbool SingleMessageAfter(\n\t\tconst MTPmessages_Messages &data,\n\t\tTimeId date) {\n\tconst auto single = SingleMessageDate(data);\n\treturn (single > 0 && single > date);\n}\n\nbool SkipMessageByDate(const Message &message, const Settings &settings) {\n\tconst auto goodFrom = (settings.singlePeerFrom <= 0)\n\t\t|| (settings.singlePeerFrom <= message.date);\n\tconst auto goodTill = (settings.singlePeerTill <= 0)\n\t\t|| (message.date < settings.singlePeerTill);\n\treturn !goodFrom || !goodTill;\n}\n\nUtf8String FormatPhoneNumber(const Utf8String &phoneNumber) {\n\treturn phoneNumber.isEmpty()\n\t\t? Utf8String()\n\t\t: Ui::FormatPhone(QString::fromUtf8(phoneNumber)).toUtf8();\n}\n\nUtf8String FormatDateTime(\n\t\tTimeId date,\n\t\tbool hasTimeZone,\n\t\tQChar dateSeparator,\n\t\tQChar timeSeparator,\n\t\tQChar separator) {\n\tif (!date) {\n\t\treturn Utf8String();\n\t}\n\tconst auto value = QDateTime::fromSecsSinceEpoch(date);\n\tconst auto timeZoneOffset = hasTimeZone\n\t\t? separator + value.timeZone().displayName(\n\t\t\tQTimeZone::StandardTime,\n\t\t\tQTimeZone::OffsetName)\n\t\t: QString();\n\treturn (QString(\"%1\") + dateSeparator + \"%2\" + dateSeparator + \"%3\"\n\t\t+ separator + \"%4\" + timeSeparator + \"%5\" + timeSeparator + \"%6\"\n\t\t+ \"%7\"\n\t).arg(value.date().day(), 2, 10, QChar('0')\n\t).arg(value.date().month(), 2, 10, QChar('0')\n\t).arg(value.date().year()\n\t).arg(value.time().hour(), 2, 10, QChar('0')\n\t).arg(value.time().minute(), 2, 10, QChar('0')\n\t).arg(value.time().second(), 2, 10, QChar('0')\n\t).arg(timeZoneOffset\n\t).toUtf8();\n}\n\nUtf8String FormatMoneyAmount(int64 amount, const Utf8String &currency) {\n\treturn Ui::FillAmountAndCurrency(\n\t\tamount,\n\t\tQString::fromUtf8(currency)).toUtf8();\n}\n\nUtf8String FormatFileSize(int64 size) {\n\treturn Ui::FormatSizeText(size).toUtf8();\n}\n\nUtf8String FormatDuration(int64 seconds) {\n\treturn Ui::FormatDurationText(seconds).toUtf8();\n}\n\n} // namespace Data\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/data/export_data_types.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"scheme.h\"\n#include \"base/optional.h\"\n#include \"base/variant.h\"\n#include \"core/credits_amount.h\"\n#include \"data/data_birthday.h\"\n#include \"data/data_peer_id.h\"\n\n#include <QtCore/QSize>\n#include <QtCore/QString>\n#include <QtCore/QByteArray>\n\n#include <vector>\n\nnamespace Export {\nstruct Settings;\nnamespace Data {\n\nusing Utf8String = QByteArray;\n\nuint8 PeerColorIndex(BareId bareId);\nBareId PeerToBareId(PeerId peerId);\nuint8 PeerColorIndex(PeerId peerId);\nuint8 ApplicationColorIndex(int applicationId);\nint DomainApplicationId(const Utf8String &data);\n\nUtf8String ParseString(const MTPstring &data);\n\nUtf8String FillLeft(const Utf8String &data, int length, char filler);\n\ntemplate <typename Type>\ninline auto NumberToString(Type value, int length = 0, char filler = '0')\n-> std::enable_if_t<std::is_arithmetic_v<Type>, Utf8String> {\n\tconst auto result = std::to_string(value);\n\treturn FillLeft(\n\t\tUtf8String(result.data(), int(result.size())),\n\t\tlength,\n\t\tfiller).replace(',', '.');\n}\n\nusing Birthday = ::Data::Birthday;\n\nstruct TextPart {\n\tenum class Type {\n\t\tText,\n\t\tUnknown,\n\t\tMention,\n\t\tHashtag,\n\t\tBotCommand,\n\t\tUrl,\n\t\tEmail,\n\t\tBold,\n\t\tItalic,\n\t\tCode,\n\t\tPre,\n\t\tTextUrl,\n\t\tMentionName,\n\t\tPhone,\n\t\tCashtag,\n\t\tUnderline,\n\t\tStrike,\n\t\tBlockquote,\n\t\tBankCard,\n\t\tSpoiler,\n\t\tCustomEmoji,\n\t};\n\tType type = Type::Text;\n\tUtf8String text;\n\tUtf8String additional;\n\n\t[[nodiscard]] static Utf8String UnavailableEmoji() {\n\t\treturn \"(unavailable)\";\n\t}\n};\n\nstruct UserpicsInfo {\n\tint count = 0;\n};\n\nstruct StoriesInfo {\n\tint count = 0;\n};\n\nstruct ProfileMusicInfo {\n\tint count = 0;\n};\n\nstruct FileLocation {\n\tint dcId = 0;\n\tMTPInputFileLocation data;\n\n\texplicit operator bool() const {\n\t\treturn dcId != 0;\n\t}\n};\n\nbool RefreshFileReference(FileLocation &to, const FileLocation &from);\n\nstruct File {\n\tenum class SkipReason {\n\t\tNone,\n\t\tUnavailable,\n\t\tFileType,\n\t\tFileSize,\n\t\tDateLimits,\n\t};\n\tFileLocation location;\n\tint64 size = 0;\n\tQByteArray content;\n\n\tQString suggestedPath;\n\n\tQString relativePath;\n\tSkipReason skipReason = SkipReason::None;\n};\n\nstruct Image {\n\tint width = 0;\n\tint height = 0;\n\tFile file;\n};\n\nstd::pair<QString, QSize> WriteImageThumb(\n\tconst QString &basePath,\n\tconst QString &largePath,\n\tFn<QSize(QSize)> convertSize,\n\tstd::optional<QByteArray> format = std::nullopt,\n\tstd::optional<int> quality = std::nullopt,\n\tconst QString &postfix = \"_thumb\");\n\nQString WriteImageThumb(\n\tconst QString &basePath,\n\tconst QString &largePath,\n\tint width,\n\tint height,\n\tconst QString &postfix = \"_thumb\");\n\nstruct ContactInfo {\n\tUserId userId = 0;\n\tUtf8String firstName;\n\tUtf8String lastName;\n\tUtf8String phoneNumber;\n\tTimeId date = 0;\n\tuint8 colorIndex = 0;\n\n\tUtf8String name() const;\n};\n\nContactInfo ParseContactInfo(const MTPUser &data);\nuint8 ContactColorIndex(const ContactInfo &data);\n\nstruct Photo {\n\tuint64 id = 0;\n\tTimeId date = 0;\n\tbool spoilered = false;\n\n\tImage image;\n};\n\nstruct Document {\n\tuint64 id = 0;\n\tTimeId date = 0;\n\n\tFile file;\n\tImage thumb;\n\n\tUtf8String name;\n\tUtf8String mime;\n\tint width = 0;\n\tint height = 0;\n\n\tUtf8String stickerEmoji;\n\tUtf8String songPerformer;\n\tUtf8String songTitle;\n\tint duration = 0;\n\n\tbool isSticker = false;\n\tbool isAnimated = false;\n\tbool isVideoMessage = false;\n\tbool isVoiceMessage = false;\n\tbool isVideoFile = false;\n\tbool isAudioFile = false;\n\tbool spoilered = false;\n};\n\nstruct SharedContact {\n\tContactInfo info;\n\tFile vcard;\n};\n\nstruct GeoPoint {\n\tfloat64 latitude = 0.;\n\tfloat64 longitude = 0.;\n\tbool valid = false;\n};\n\nstruct Venue {\n\tGeoPoint point;\n\tUtf8String title;\n\tUtf8String address;\n};\n\nstruct Game {\n\tuint64 id = 0;\n\tUtf8String shortName;\n\tUtf8String title;\n\tUtf8String description;\n\n\tUserId botId = 0;\n};\n\nstruct Invoice {\n\tUtf8String title;\n\tUtf8String description;\n\tUtf8String currency;\n\tuint64 amount = 0;\n\tint32 receiptMsgId = 0;\n};\n\nstruct Media;\nstruct PaidMedia {\n\tPaidMedia() = default;\n\tPaidMedia(PaidMedia &&) = default;\n\tPaidMedia &operator=(PaidMedia &&) = default;\n\tPaidMedia(const PaidMedia &) = delete;\n\tPaidMedia &operator=(const PaidMedia &) = delete;\n\n\tuint64 stars = 0;\n\tstd::vector<std::unique_ptr<Media>> extended;\n};\n\nstruct Poll {\n\tstruct Answer {\n\t\tstd::vector<TextPart> text;\n\t\tQByteArray option;\n\t\tint votes = 0;\n\t\tbool my = false;\n\t};\n\n\tuint64 id = 0;\n\tstd::vector<TextPart> question;\n\tstd::vector<Answer> answers;\n\tint totalVotes = 0;\n\tbool closed = false;\n};\n\nstruct TodoListItem {\n\tstd::vector<TextPart> text;\n\tint id = 0;\n};\n\nstruct TodoList {\n\tbool othersCanAppend = false;\n\tbool othersCanComplete = false;\n\tstd::vector<TextPart> title;\n\tstd::vector<TodoListItem> items;\n};\n\nstruct GiveawayStart {\n\tstd::vector<QString> countries;\n\tstd::vector<ChannelId> channels;\n\tQString additionalPrize;\n\tTimeId untilDate = 0;\n\tuint64 credits = 0;\n\tint quantity = 0;\n\tint months = 0;\n\tbool all = false;\n};\n\nstruct GiveawayResults {\n\tChannelId channel = 0;\n\tstd::vector<PeerId> winners;\n\tQString additionalPrize;\n\tTimeId untilDate = 0;\n\tint32 launchId = 0;\n\tint additionalPeersCount = 0;\n\tint winnersCount = 0;\n\tint unclaimedCount = 0;\n\tint months = 0;\n\tuint64 credits = 0;\n\tbool refunded = false;\n\tbool all = false;\n};\n\nstruct UserpicsSlice {\n\tstd::vector<Photo> list;\n};\n\nUserpicsSlice ParseUserpicsSlice(\n\tconst MTPVector<MTPPhoto> &data,\n\tint baseIndex);\n\nstruct User {\n\tPeerId id() const;\n\n\tBareId bareId = 0;\n\tContactInfo info;\n\tUtf8String username;\n\tuint8 colorIndex = 0;\n\tbool isBot = false;\n\tbool isSelf = false;\n\tbool isReplies = false;\n\tbool isVerifyCodes = false;\n\n\tMTPInputUser input = MTP_inputUserEmpty();\n\n\tUtf8String name() const;\n};\n\nUser ParseUser(const MTPUser &data);\nstd::map<UserId, User> ParseUsersList(const MTPVector<MTPUser> &data);\n\nstruct Chat {\n\tPeerId id() const;\n\n\tBareId bareId = 0;\n\tChannelId migratedToChannelId = 0;\n\tUtf8String title;\n\tUtf8String username;\n\tuint8 colorIndex = 0;\n\tbool isMonoforum = false;\n\tbool isBroadcast = false;\n\tbool isSupergroup = false;\n\tbool isMonoforumAdmin = false;\n\tbool hasMonoforumAdminRights = false;\n\tbool isMonoforumOfPublicBroadcast = false;\n\tBareId monoforumLinkId = 0;\n\n\tMTPInputPeer input = MTP_inputPeerEmpty();\n\tMTPInputPeer monoforumBroadcastInput = MTP_inputPeerEmpty();\n};\n\nChat ParseChat(const MTPChat &data);\n\nstruct Peer {\n\tPeerId id() const;\n\tUtf8String name() const;\n\tMTPInputPeer input() const;\n\tuint8 colorIndex() const;\n\n\tconst User *user() const;\n\tconst Chat *chat() const;\n\n\tstd::variant<User, Chat> data;\n\n};\n\nstd::map<PeerId, Peer> ParsePeersLists(\n\tconst MTPVector<MTPUser> &users,\n\tconst MTPVector<MTPChat> &chats);\n\nstruct PersonalInfo {\n\tUser user;\n\tUtf8String bio;\n};\n\nPersonalInfo ParsePersonalInfo(const MTPDusers_userFull &data);\n\nstruct TopPeer {\n\tPeer peer;\n\tfloat64 rating = 0.;\n};\n\nstruct ContactsList {\n\tstd::vector<ContactInfo> list;\n\tstd::vector<TopPeer> correspondents;\n\tstd::vector<TopPeer> inlineBots;\n\tstd::vector<TopPeer> phoneCalls;\n};\n\nContactsList ParseContactsList(const MTPcontacts_Contacts &data);\nContactsList ParseContactsList(const MTPVector<MTPSavedContact> &data);\nstd::vector<int> SortedContactsIndices(const ContactsList &data);\nbool AppendTopPeers(ContactsList &to, const MTPcontacts_TopPeers &data);\n\nstruct Session {\n\tint applicationId = 0;\n\tUtf8String platform;\n\tUtf8String deviceModel;\n\tUtf8String systemVersion;\n\tUtf8String applicationName;\n\tUtf8String applicationVersion;\n\tTimeId created = 0;\n\tTimeId lastActive = 0;\n\tUtf8String ip;\n\tUtf8String country;\n\tUtf8String region;\n};\n\nstruct WebSession {\n\tUtf8String botUsername;\n\tUtf8String domain;\n\tUtf8String browser;\n\tUtf8String platform;\n\tTimeId created = 0;\n\tTimeId lastActive = 0;\n\tUtf8String ip;\n\tUtf8String region;\n};\n\nstruct SessionsList {\n\tstd::vector<Session> list;\n\tstd::vector<WebSession> webList;\n};\n\nSessionsList ParseSessionsList(const MTPaccount_Authorizations &data);\nSessionsList ParseWebSessionsList(const MTPaccount_WebAuthorizations &data);\n\nstruct UnsupportedMedia {\n};\n\nstruct Media {\n\tstd::variant<\n\t\tv::null_t,\n\t\tPhoto,\n\t\tDocument,\n\t\tSharedContact,\n\t\tGeoPoint,\n\t\tVenue,\n\t\tGame,\n\t\tInvoice,\n\t\tPoll,\n\t\tTodoList,\n\t\tGiveawayStart,\n\t\tGiveawayResults,\n\t\tPaidMedia,\n\t\tUnsupportedMedia> content;\n\tTimeId ttl = 0;\n\n\tFile &file();\n\tconst File &file() const;\n\tImage &thumb();\n\tconst Image &thumb() const;\n};\n\nstruct ParseMediaContext {\n\tPeerId selfPeerId = 0;\n\tint photos = 0;\n\tint audios = 0;\n\tint videos = 0;\n\tint files = 0;\n\tint contacts = 0;\n\tUserId botId = 0;\n};\n\nDocument ParseDocument(\n\tParseMediaContext &context,\n\tconst MTPDocument &data,\n\tconst QString &suggestedFolder,\n\tTimeId date);\n\nMedia ParseMedia(\n\tParseMediaContext &context,\n\tconst MTPMessageMedia &data,\n\tconst QString &folder,\n\tTimeId date);\n\nstruct ActionChatCreate {\n\tUtf8String title;\n\tstd::vector<UserId> userIds;\n};\n\nstruct ActionChatEditTitle {\n\tUtf8String title;\n};\n\nstruct ActionChatEditPhoto {\n\tPhoto photo;\n};\n\nstruct ActionChatDeletePhoto {\n};\n\nstruct ActionChatAddUser {\n\tstd::vector<UserId> userIds;\n};\n\nstruct ActionChatDeleteUser {\n\tUserId userId = 0;\n};\n\nstruct ActionChatJoinedByLink {\n\tUserId inviterId = 0;\n};\n\nstruct ActionChannelCreate {\n\tUtf8String title;\n};\n\nstruct ActionChatMigrateTo {\n\tChannelId channelId = 0;\n};\n\nstruct ActionChannelMigrateFrom {\n\tUtf8String title;\n\tChatId chatId = 0;\n};\n\nstruct ActionPinMessage {\n};\n\nstruct ActionHistoryClear {\n};\n\nstruct ActionGameScore {\n\tuint64 gameId = 0;\n\tint score = 0;\n};\n\nstruct ActionPaymentSent {\n\tUtf8String currency;\n\tuint64 amount = 0;\n\tbool recurringInit = false;\n\tbool recurringUsed = false;\n};\n\nstruct ActionPhoneCall {\n\tenum class State {\n\t\tUnknown,\n\t\tMissed,\n\t\tDisconnect,\n\t\tHangup,\n\t\tBusy,\n\t\tMigrateConferenceCall,\n\t\tInvitation,\n\t\tActive,\n\t};\n\n\tuint64 conferenceId = 0;\n\tState state = State::Unknown;\n\tint duration = 0;\n};\n\nstruct ActionScreenshotTaken {\n};\n\nstruct ActionCustomAction {\n\tUtf8String message;\n};\n\nstruct ActionBotAllowed {\n\tuint64 appId = 0;\n\tUtf8String app;\n\tUtf8String domain;\n\tbool attachMenu = false;\n\tbool fromRequest = false;\n};\n\nstruct ActionSecureValuesSent {\n\tenum class Type {\n\t\tPersonalDetails,\n\t\tPassport,\n\t\tDriverLicense,\n\t\tIdentityCard,\n\t\tInternalPassport,\n\t\tAddress,\n\t\tUtilityBill,\n\t\tBankStatement,\n\t\tRentalAgreement,\n\t\tPassportRegistration,\n\t\tTemporaryRegistration,\n\t\tPhone,\n\t\tEmail,\n\t};\n\tstd::vector<Type> types;\n};\n\nstruct ActionContactSignUp {\n};\n\nstruct ActionPhoneNumberRequest {\n};\n\nstruct ActionGeoProximityReached {\n\tPeerId fromId = 0;\n\tPeerId toId = 0;\n\tint distance = 0;\n\tbool fromSelf = false;\n\tbool toSelf = false;\n};\n\nstruct ActionGroupCall {\n\tint duration = 0;\n};\n\nstruct ActionInviteToGroupCall {\n\tstd::vector<UserId> userIds;\n};\n\nstruct ActionSetMessagesTTL {\n\tTimeId period = 0;\n};\n\nstruct ActionGroupCallScheduled {\n\tTimeId date = 0;\n};\n\nstruct ActionSetChatTheme {\n\tQString emoji;\n};\n\nstruct ActionChatJoinedByRequest {\n};\n\nstruct ActionWebViewDataSent {\n\tUtf8String text;\n};\n\nstruct ActionGiftPremium {\n\tUtf8String cost;\n\tint days = 0;\n};\n\nstruct ActionTopicCreate {\n\tUtf8String title;\n};\n\nstruct ActionTopicEdit {\n\tUtf8String title;\n\tstd::optional<uint64> iconEmojiId = 0;\n};\n\nstruct ActionSuggestProfilePhoto {\n\tPhoto photo;\n};\n\nstruct ActionSetChatWallPaper {\n\tbool same = false;\n\tbool both = false;\n\t// #TODO wallpapers\n};\n\nstruct ActionGiftCode {\n\tQByteArray code;\n\tPeerId boostPeerId = 0;\n\tint days = 0;\n\tbool viaGiveaway = false;\n\tbool unclaimed = false;\n};\n\nstruct ActionRequestedPeer {\n\tstd::vector<PeerId> peers;\n\tint buttonId = 0;\n};\n\nstruct ActionGiveawayLaunch {\n};\n\nstruct ActionGiveawayResults {\n\tint winners = 0;\n\tint unclaimed = 0;\n\tbool credits = false;\n};\n\nstruct ActionBoostApply {\n\tint boosts = 0;\n};\n\nstruct ActionPaymentRefunded {\n\tPeerId peerId = 0;\n\tUtf8String currency;\n\tuint64 amount = 0;\n\tUtf8String transactionId;\n};\n\nstruct ActionGiftCredits {\n\tUtf8String cost;\n\tCreditsAmount amount;\n};\n\nstruct ActionPrizeStars {\n\tPeerId peerId = 0;\n\tuint64 amount = 0;\n\tUtf8String transactionId;\n\tint32 giveawayMsgId = 0;\n\tbool isUnclaimed = false;\n};\n\nstruct ActionStarGift {\n\tuint64 giftId = 0;\n\tint64 stars = 0;\n\tstd::vector<TextPart> text;\n\tbool anonymous = false;\n\tbool limited = false;\n\n\tCreditsAmount offerPrice;\n\tTimeId offerExpireAt = 0;\n\tbool offer = false;\n\tbool offerAccepted = false;\n\tbool offerDeclined = false;\n\tbool offerExpired = false;\n};\n\nstruct ActionPaidMessagesRefunded {\n\tint messages = 0;\n\tint64 stars = 0;\n};\n\nstruct ActionPaidMessagesPrice {\n\tint stars = 0;\n\tbool broadcastAllowed = false;\n};\n\nstruct ActionTodoCompletions {\n\tstd::vector<int> completed;\n\tstd::vector<int> incompleted;\n};\n\nstruct ActionTodoAppendTasks {\n\tstd::vector<TodoListItem> items;\n};\n\nstruct ActionPollAppendAnswer {\n\tUtf8String option;\n};\n\nstruct ActionPollDeleteAnswer {\n\tUtf8String option;\n};\n\nstruct ActionSuggestedPostApproval {\n\tUtf8String rejectComment;\n\tTimeId scheduleDate = 0;\n\tCreditsAmount price;\n\tbool rejected = false;\n\tbool balanceTooLow = false;\n};\n\nstruct ActionSuggestedPostSuccess {\n\tCreditsAmount price;\n};\n\nstruct ActionSuggestedPostRefund {\n\tbool payerInitiated = false;\n};\n\nstruct ActionSuggestBirthday {\n\tBirthday birthday;\n};\n\nstruct ActionNoForwardsToggle {\n\tbool newValue = false;\n};\n\nstruct ActionNoForwardsRequest {\n\tbool expired = false;\n\tbool newValue = false;\n};\n\nstruct ActionNewCreatorPending {\n\tUserId newCreatorId = 0;\n};\n\nstruct ActionChangeCreator {\n\tUserId newCreatorId = 0;\n};\n\nstruct ActionManagedBotCreated {\n\tUserId botId = 0;\n};\n\nstruct ServiceAction {\n\tstd::variant<\n\t\tv::null_t,\n\t\tActionChatCreate,\n\t\tActionChatEditTitle,\n\t\tActionChatEditPhoto,\n\t\tActionChatDeletePhoto,\n\t\tActionChatAddUser,\n\t\tActionChatDeleteUser,\n\t\tActionChatJoinedByLink,\n\t\tActionChannelCreate,\n\t\tActionChatMigrateTo,\n\t\tActionChannelMigrateFrom,\n\t\tActionPinMessage,\n\t\tActionHistoryClear,\n\t\tActionGameScore,\n\t\tActionPaymentSent,\n\t\tActionPhoneCall,\n\t\tActionScreenshotTaken,\n\t\tActionCustomAction,\n\t\tActionBotAllowed,\n\t\tActionSecureValuesSent,\n\t\tActionContactSignUp,\n\t\tActionPhoneNumberRequest,\n\t\tActionGeoProximityReached,\n\t\tActionGroupCall,\n\t\tActionInviteToGroupCall,\n\t\tActionSetMessagesTTL,\n\t\tActionGroupCallScheduled,\n\t\tActionSetChatTheme,\n\t\tActionChatJoinedByRequest,\n\t\tActionWebViewDataSent,\n\t\tActionGiftPremium,\n\t\tActionTopicCreate,\n\t\tActionTopicEdit,\n\t\tActionSuggestProfilePhoto,\n\t\tActionRequestedPeer,\n\t\tActionSetChatWallPaper,\n\t\tActionGiftCode,\n\t\tActionGiveawayLaunch,\n\t\tActionGiveawayResults,\n\t\tActionBoostApply,\n\t\tActionPaymentRefunded,\n\t\tActionGiftCredits,\n\t\tActionPrizeStars,\n\t\tActionStarGift,\n\t\tActionPaidMessagesRefunded,\n\t\tActionPaidMessagesPrice,\n\t\tActionTodoCompletions,\n\t\tActionTodoAppendTasks,\n\t\tActionPollAppendAnswer,\n\t\tActionPollDeleteAnswer,\n\t\tActionSuggestedPostApproval,\n\t\tActionSuggestedPostSuccess,\n\t\tActionSuggestedPostRefund,\n\t\tActionSuggestBirthday,\n\t\tActionNoForwardsToggle,\n\t\tActionNoForwardsRequest,\n\t\tActionNewCreatorPending,\n\t\tActionChangeCreator,\n\t\tActionManagedBotCreated> content;\n};\n\nServiceAction ParseServiceAction(\n\tParseMediaContext &context,\n\tconst MTPMessageAction &data,\n\tconst QString &mediaFolder);\n\nstruct Reaction {\n\tenum class Type {\n\t\tEmpty,\n\t\tEmoji,\n\t\tCustomEmoji,\n\t\tPaid,\n\t};\n\n\t[[nodiscard]] static Utf8String TypeToString(const Reaction &);\n\n\t[[nodiscard]] static Utf8String Id(const Reaction &);\n\n\tstruct Recent {\n\t\tPeerId peerId = 0;\n\t\tTimeId date = 0;\n\t};\n\n\tType type;\n\tQString emoji;\n\tUtf8String documentId;\n\tuint32 count = 0;\n\tstd::vector<Recent> recent;\n};\n\nstruct MessageId {\n\tChannelId channelId;\n\tint32 msgId = 0;\n};\n\ninline bool operator==(MessageId a, MessageId b) {\n\treturn (a.channelId == b.channelId) && (a.msgId == b.msgId);\n}\ninline bool operator!=(MessageId a, MessageId b) {\n\treturn !(a == b);\n}\ninline bool operator<(MessageId a, MessageId b) {\n\treturn (a.channelId < b.channelId)\n\t\t|| (a.channelId == b.channelId && a.msgId < b.msgId);\n}\ninline bool operator>(MessageId a, MessageId b) {\n\treturn (b < a);\n}\ninline bool operator<=(MessageId a, MessageId b) {\n\treturn !(b < a);\n}\ninline bool operator>=(MessageId a, MessageId b) {\n\treturn !(a < b);\n}\n\nstruct HistoryMessageMarkupButton {\n\tenum class Type {\n\t\tDefault,\n\t\tUrl,\n\t\tCallback,\n\t\tCallbackWithPassword,\n\t\tRequestPhone,\n\t\tRequestLocation,\n\t\tRequestPoll,\n\t\tRequestPeer,\n\t\tSwitchInline,\n\t\tSwitchInlineSame,\n\t\tGame,\n\t\tBuy,\n\t\tAuth,\n\t\tUserProfile,\n\t\tWebView,\n\t\tSimpleWebView,\n\t\tCopyText,\n\t};\n\n\tstatic QByteArray TypeToString(const HistoryMessageMarkupButton &);\n\n\tType type;\n\tQString text;\n\tQByteArray data;\n\tQString forwardText;\n\tint64 buttonId = 0;\n};\n\nstruct Message {\n\tint32 id = 0;\n\tTimeId date = 0;\n\tTimeId edited = 0;\n\tPeerId fromId = 0;\n\tPeerId peerId = 0;\n\tPeerId selfId = 0;\n\tPeerId forwardedFromId = 0;\n\tUtf8String forwardedFromName;\n\tTimeId forwardedDate = 0;\n\tbool forwarded = false;\n\tbool showForwardedAsOriginal = false;\n\tPeerId savedFromChatId = 0;\n\tUtf8String signature;\n\tUserId viaBotId = 0;\n\tint32 replyToMsgId = 0;\n\tPeerId replyToPeerId = 0;\n\tstd::vector<TextPart> text;\n\tstd::vector<Reaction> reactions;\n\tMedia media;\n\tServiceAction action;\n\tbool out = false;\n\tstd::vector<std::vector<HistoryMessageMarkupButton>> inlineButtonRows;\n\n\tFile &file();\n\tconst File &file() const;\n\tImage &thumb();\n\tconst Image &thumb() const;\n};\n\nstruct FileOrigin {\n\tint split = 0;\n\tMTPInputPeer peer;\n\tint32 messageId = 0;\n\tint32 storyId = 0;\n\tuint64 customEmojiId = 0;\n};\n\nstruct Story {\n\tint32 id = 0;\n\tTimeId date = 0;\n\tTimeId expires = 0;\n\tMedia media;\n\tbool pinned = false;\n\tstd::vector<TextPart> caption;\n\n\tFile &file();\n\tconst File &file() const;\n\tImage &thumb();\n\tconst Image &thumb() const;\n};\n\nstruct StoriesSlice {\n\tstd::vector<Story> list;\n\tint32 lastId = 0;\n\tint skipped = 0;\n};\n\nStoriesSlice ParseStoriesSlice(\n\tconst MTPVector<MTPStoryItem> &data,\n\tint baseIndex);\n\nstruct ProfileMusicSlice {\n\tstd::vector<Message> list;\n\tint skipped = 0;\n};\n\nMessage ParseMessage(\n\tParseMediaContext &context,\n\tconst MTPMessage &data,\n\tconst QString &mediaFolder);\nstd::map<MessageId, Message> ParseMessagesList(\n\tPeerId selfId,\n\tconst MTPVector<MTPMessage> &data,\n\tconst QString &mediaFolder);\n\nstruct DialogInfo {\n\tenum class Type {\n\t\tUnknown,\n\t\tSelf,\n\t\tReplies,\n\t\tVerifyCodes,\n\t\tPersonal,\n\t\tBot,\n\t\tPrivateGroup,\n\t\tPrivateSupergroup,\n\t\tPublicSupergroup,\n\t\tPrivateChannel,\n\t\tPublicChannel,\n\t};\n\tType type = Type::Unknown;\n\tUtf8String name;\n\tUtf8String lastName;\n\n\tMTPInputPeer input = MTP_inputPeerEmpty();\n\tint32 topMessageId = 0;\n\tTimeId topMessageDate = 0;\n\tPeerId peerId = 0;\n\tuint8 colorIndex = 0;\n\n\tMTPInputPeer migratedFromInput = MTP_inputPeerEmpty();\n\tChannelId migratedToChannelId = 0;\n\n\tMTPInputPeer monoforumBroadcastInput = MTP_inputPeerEmpty();\n\n\t// User messages splits which contained that dialog.\n\tstd::vector<int> splits;\n\n\t// Filled after the whole dialogs list is accumulated.\n\tbool onlyMyMessages = false;\n\tbool isLeftChannel = false;\n\tbool isMonoforum = false;\n\tQString relativePath;\n\n\t// Filled when requesting dialog messages.\n\tstd::vector<int> messagesCountPerSplit;\n};\n\nstruct DialogsInfo {\n\tDialogInfo *item(int index);\n\tconst DialogInfo *item(int index) const;\n\n\tstd::vector<DialogInfo> chats;\n\tstd::vector<DialogInfo> left;\n};\n\nDialogInfo::Type DialogTypeFromChat(const Chat &chat);\n\nDialogsInfo ParseDialogsInfo(const MTPmessages_Dialogs &data);\nDialogsInfo ParseDialogsInfo(\n\tconst MTPInputPeer &singlePeer,\n\tconst MTPVector<MTPUser> &data);\nDialogsInfo ParseDialogsInfo(\n\tconst MTPInputPeer &singlePeer,\n\tconst MTPmessages_Chats &data);\nDialogsInfo ParseLeftChannelsInfo(const MTPmessages_Chats &data);\nbool AddMigrateFromSlice(\n\tDialogInfo &to,\n\tconst DialogInfo &from,\n\tint splitIndex,\n\tint splitsCount);\nvoid FinalizeDialogsInfo(DialogsInfo &info, const Settings &settings);\n\nstruct MessagesSlice {\n\tstd::vector<Message> list;\n\tstd::map<PeerId, Peer> peers;\n};\n\nMessagesSlice ParseMessagesSlice(\n\tParseMediaContext &context,\n\tconst MTPVector<MTPMessage> &data,\n\tconst MTPVector<MTPUser> &users,\n\tconst MTPVector<MTPChat> &chats,\n\tconst QString &mediaFolder);\nMessagesSlice AdjustMigrateMessageIds(MessagesSlice slice);\n\nbool SingleMessageBefore(\n\tconst MTPmessages_Messages &data,\n\tTimeId date);\nbool SingleMessageAfter(\n\tconst MTPmessages_Messages &data,\n\tTimeId date);\nbool SkipMessageByDate(const Message &message, const Settings &settings);\n\nUtf8String FormatPhoneNumber(const Utf8String &phoneNumber);\nUtf8String FormatDateTime(\n\tTimeId date,\n\tbool hasTimeZone = false,\n\tQChar dateSeparator = QChar('.'),\n\tQChar timeSeparator = QChar(':'),\n\tQChar separator = QChar(' '));\nUtf8String FormatMoneyAmount(int64 amount, const Utf8String &currency);\nUtf8String FormatFileSize(int64 size);\nUtf8String FormatDuration(int64 seconds);\n\n} // namespace Data\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/export_api_wrap.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/export_api_wrap.h\"\n\n#include \"export/export_settings.h\"\n#include \"export/data/export_data_types.h\"\n#include \"export/output/export_output_result.h\"\n#include \"export/output/export_output_file.h\"\n#include \"mtproto/mtproto_response.h\"\n#include \"base/bytes.h\"\n#include \"base/options.h\"\n#include \"base/random.h\"\n#include <set>\n#include <deque>\n\nnamespace Export {\nnamespace {\n\nconstexpr auto kUserpicsSliceLimit = 100;\nconstexpr auto kFileChunkSize = 128 * 1024;\nconstexpr auto kFileRequestsCount = 2;\n//constexpr auto kFileNextRequestDelay = crl::time(20);\nconstexpr auto kChatsSliceLimit = 100;\nconstexpr auto kMessagesSliceLimit = 100;\nconstexpr auto kTopPeerSliceLimit = 100;\nconstexpr auto kFileMaxSize = 4000 * int64(1024 * 1024);\nconstexpr auto kLocationCacheSize = 100'000;\nconstexpr auto kMaxEmojiPerRequest = 100;\nconstexpr auto kStoriesSliceLimit = 100;\nconstexpr auto kProfileMusicSliceLimit = 100;\n\nstruct LocationKey {\n\tuint64 type;\n\tuint64 id;\n\n\tinline bool operator<(const LocationKey &other) const {\n\t\treturn std::tie(type, id) < std::tie(other.type, other.id);\n\t}\n};\n\nLocationKey ComputeLocationKey(const Data::FileLocation &value) {\n\tauto result = LocationKey();\n\tresult.type = value.dcId;\n\tvalue.data.match([&](const MTPDinputDocumentFileLocation &data) {\n\t\tconst auto letter = data.vthumb_size().v.isEmpty()\n\t\t\t? char(0)\n\t\t\t: data.vthumb_size().v[0];\n\t\tresult.type |= (2ULL << 24);\n\t\tresult.type |= (uint64(uint32(letter)) << 16);\n\t\tresult.id = data.vid().v;\n\t}, [&](const MTPDinputPhotoFileLocation &data) {\n\t\tconst auto letter = data.vthumb_size().v.isEmpty()\n\t\t\t? char(0)\n\t\t\t: data.vthumb_size().v[0];\n\t\tresult.type |= (6ULL << 24);\n\t\tresult.type |= (uint64(uint32(letter)) << 16);\n\t\tresult.id = data.vid().v;\n\t}, [&](const MTPDinputTakeoutFileLocation &data) {\n\t\tresult.type |= (5ULL << 24);\n\t}, [](const auto &data) {\n\t\tUnexpected(\"File location type in Export::ComputeLocationKey.\");\n\t});\n\treturn result;\n}\n\nSettings::Type SettingsFromDialogsType(Data::DialogInfo::Type type) {\n\tusing DialogType = Data::DialogInfo::Type;\n\tswitch (type) {\n\tcase DialogType::Self:\n\tcase DialogType::Personal:\n\t\treturn Settings::Type::PersonalChats;\n\tcase DialogType::Bot:\n\t\treturn Settings::Type::BotChats;\n\tcase DialogType::PrivateGroup:\n\tcase DialogType::PrivateSupergroup:\n\t\treturn Settings::Type::PrivateGroups;\n\tcase DialogType::PublicSupergroup:\n\t\treturn Settings::Type::PublicGroups;\n\tcase DialogType::PrivateChannel:\n\t\treturn Settings::Type::PrivateChannels;\n\tcase DialogType::PublicChannel:\n\t\treturn Settings::Type::PublicChannels;\n\t}\n\treturn Settings::Type(0);\n}\n\n} // namespace\n\nclass ApiWrap::LoadedFileCache {\npublic:\n\tusing Location = Data::FileLocation;\n\n\tLoadedFileCache(int limit);\n\n\tvoid save(const Location &location, const QString &relativePath);\n\tstd::optional<QString> find(const Location &location) const;\n\nprivate:\n\tint _limit = 0;\n\tstd::map<LocationKey, QString> _map;\n\tstd::deque<LocationKey> _list;\n\n};\n\nstruct ApiWrap::StartProcess {\n\tFnMut<void(StartInfo)> done;\n\n\tenum class Step {\n\t\tUserpicsCount,\n\t\tStoriesCount,\n\t\tProfileMusicCount,\n\t\tSplitRanges,\n\t\tDialogsCount,\n\t\tLeftChannelsCount,\n\t};\n\tstd::deque<Step> steps;\n\tint splitIndex = 0;\n\tStartInfo info;\n};\n\nstruct ApiWrap::ContactsProcess {\n\tFnMut<void(Data::ContactsList&&)> done;\n\n\tData::ContactsList result;\n\n\tint topPeersOffset = 0;\n};\n\nstruct ApiWrap::UserpicsProcess {\n\tFnMut<bool(Data::UserpicsInfo&&)> start;\n\tFn<bool(DownloadProgress)> fileProgress;\n\tFn<bool(Data::UserpicsSlice&&)> handleSlice;\n\tFnMut<void()> finish;\n\n\tint processed = 0;\n\tstd::optional<Data::UserpicsSlice> slice;\n\tuint64 maxId = 0;\n\tbool lastSlice = false;\n\tint fileIndex = 0;\n};\n\nstruct ApiWrap::StoriesProcess {\n\tFnMut<bool(Data::StoriesInfo&&)> start;\n\tFn<bool(DownloadProgress)> fileProgress;\n\tFn<bool(Data::StoriesSlice&&)> handleSlice;\n\tFnMut<void()> finish;\n\n\tint processed = 0;\n\tstd::optional<Data::StoriesSlice> slice;\n\tint offsetId = 0;\n\tbool lastSlice = false;\n\tint fileIndex = 0;\n};\n\nstruct ApiWrap::ProfileMusicProcess {\n\tFnMut<bool(Data::ProfileMusicInfo&&)> start;\n\tFn<bool(DownloadProgress)> fileProgress;\n\tFn<bool(Data::ProfileMusicSlice&&)> handleSlice;\n\tFnMut<void()> finish;\n\n\tint processed = 0;\n\tstd::optional<Data::ProfileMusicSlice> slice;\n\tint offsetId = 0;\n\tbool lastSlice = false;\n\tint fileIndex = 0;\n};\n\nstruct ApiWrap::OtherDataProcess {\n\tData::File file;\n\tFnMut<void(Data::File&&)> done;\n};\n\nstruct ApiWrap::FileProcess {\n\tFileProcess(const QString &path, Output::Stats *stats);\n\n\tOutput::File file;\n\tQString relativePath;\n\n\tFn<bool(FileProgress)> progress;\n\tFnMut<void(const QString &relativePath)> done;\n\n\tuint64 randomId = 0;\n\tData::FileLocation location;\n\tData::FileOrigin origin;\n\tint64 offset = 0;\n\tint64 size = 0;\n\n\tstruct Request {\n\t\tint64 offset = 0;\n\t\tQByteArray bytes;\n\t};\n\tstd::deque<Request> requests;\n\tmtpRequestId requestId = 0;\n};\n\nstruct ApiWrap::FileProgress {\n\tint64 ready = 0;\n\tint64 total = 0;\n};\n\nstruct ApiWrap::ChatsProcess {\n\tFn<bool(int count)> progress;\n\tFnMut<void(Data::DialogsInfo&&)> done;\n\n\tData::DialogsInfo info;\n\tint processedCount = 0;\n\tstd::map<PeerId, int> indexByPeer;\n};\n\nstruct ApiWrap::LeftChannelsProcess : ChatsProcess {\n\tint fullCount = 0;\n\tint offset = 0;\n\tbool finished = false;\n};\n\nstruct ApiWrap::DialogsProcess : ChatsProcess {\n\tint splitIndexPlusOne = 0;\n\tTimeId offsetDate = 0;\n\tint32 offsetId = 0;\n\tMTPInputPeer offsetPeer = MTP_inputPeerEmpty();\n};\n\nstruct ApiWrap::AbstractMessagesProcess {\n\tFn<bool(DownloadProgress)> fileProgress;\n\tFn<bool(Data::MessagesSlice&&)> handleSlice;\n\tFnMut<void()> done;\n\n\tFnMut<void(MTPmessages_Messages&&)> requestDone;\n\n\tData::ParseMediaContext context;\n\tstd::optional<Data::MessagesSlice> slice;\n\tbool lastSlice = false;\n\tint fileIndex = 0;\n};\n\nstruct ApiWrap::ChatProcess : AbstractMessagesProcess {\n\tData::DialogInfo info;\n\n\tFnMut<bool(const Data::DialogInfo &)> start;\n\n\tint localSplitIndex = 0;\n\tint32 largestIdPlusOne = 1;\n};\n\nstruct ApiWrap::TopicProcess : AbstractMessagesProcess {\n\tPeerId peerId = 0;\n\tMTPInputPeer inputPeer;\n\tint32 topicRootId = 0;\n\tQString relativePath;\n\n\tFnMut<bool(int count)> start;\n\n\tint32 offsetId = 0;\n\tint totalCount = 0;\n\tint processedCount = 0;\n};\n\n\ntemplate <typename Request>\nclass ApiWrap::RequestBuilder {\npublic:\n\tusing Original = MTP::ConcurrentSender::SpecificRequestBuilder<Request>;\n\tusing Response = typename Request::ResponseType;\n\n\tRequestBuilder(\n\t\tOriginal &&builder,\n\t\tFn<void(const MTP::Error&)> commonFailHandler);\n\n\t[[nodiscard]] RequestBuilder &done(FnMut<void()> &&handler);\n\t[[nodiscard]] RequestBuilder &done(\n\t\tFnMut<void(Response &&)> &&handler);\n\t[[nodiscard]] RequestBuilder &fail(\n\t\tFn<bool(const MTP::Error&)> &&handler);\n\n\tmtpRequestId send();\n\nprivate:\n\tOriginal _builder;\n\tFn<void(const MTP::Error&)> _commonFailHandler;\n\n};\n\ntemplate <typename Request>\nApiWrap::RequestBuilder<Request>::RequestBuilder(\n\tOriginal &&builder,\n\tFn<void(const MTP::Error&)> commonFailHandler)\n: _builder(std::move(builder))\n, _commonFailHandler(std::move(commonFailHandler)) {\n}\n\ntemplate <typename Request>\nauto ApiWrap::RequestBuilder<Request>::done(\n\tFnMut<void()> &&handler\n) -> RequestBuilder& {\n\tif (handler) {\n\t\t[[maybe_unused]] auto &silence_warning = _builder.done(std::move(handler));\n\t}\n\treturn *this;\n}\n\ntemplate <typename Request>\nauto ApiWrap::RequestBuilder<Request>::done(\n\tFnMut<void(Response &&)> &&handler\n) -> RequestBuilder& {\n\tif (handler) {\n\t\t[[maybe_unused]] auto &silence_warning = _builder.done(std::move(handler));\n\t}\n\treturn *this;\n}\n\ntemplate <typename Request>\nauto ApiWrap::RequestBuilder<Request>::fail(\n\tFn<bool(const MTP::Error &)> &&handler\n) -> RequestBuilder& {\n\tif (handler) {\n\t\t[[maybe_unused]] auto &silence_warning = _builder.fail([\n\t\t\tcommon = base::take(_commonFailHandler),\n\t\t\tspecific = std::move(handler)\n\t\t](const MTP::Error &error) {\n\t\t\tif (!specific(error)) {\n\t\t\t\tcommon(error);\n\t\t\t}\n\t\t});\n\t}\n\treturn *this;\n}\n\ntemplate <typename Request>\nmtpRequestId ApiWrap::RequestBuilder<Request>::send() {\n\treturn _commonFailHandler\n\t\t? _builder.fail(base::take(_commonFailHandler)).send()\n\t\t: _builder.send();\n}\n\nApiWrap::LoadedFileCache::LoadedFileCache(int limit) : _limit(limit) {\n\tExpects(limit >= 0);\n}\n\nvoid ApiWrap::LoadedFileCache::save(\n\t\tconst Location &location,\n\t\tconst QString &relativePath) {\n\tif (!location) {\n\t\treturn;\n\t}\n\tconst auto key = ComputeLocationKey(location);\n\t_map[key] = relativePath;\n\t_list.push_back(key);\n\tif (_list.size() > _limit) {\n\t\tconst auto key = _list.front();\n\t\t_list.pop_front();\n\t\t_map.erase(key);\n\t}\n}\n\nstd::optional<QString> ApiWrap::LoadedFileCache::find(\n\t\tconst Location &location) const {\n\tif (!location) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto key = ComputeLocationKey(location);\n\tif (const auto i = _map.find(key); i != end(_map)) {\n\t\treturn i->second;\n\t}\n\treturn std::nullopt;\n}\n\nApiWrap::FileProcess::FileProcess(const QString &path, Output::Stats *stats)\n: file(path, stats) {\n}\n\ntemplate <typename Request>\nauto ApiWrap::mainRequest(Request &&request) {\n\tExpects(_takeoutId.has_value());\n\n\tauto original = std::move(_mtp.request(MTPInvokeWithTakeout<Request>(\n\t\tMTP_long(*_takeoutId),\n\t\tstd::forward<Request>(request)\n\t)).toDC(MTP::ShiftDcId(0, MTP::kExportDcShift)));\n\n\treturn RequestBuilder<MTPInvokeWithTakeout<Request>>(\n\t\tstd::move(original),\n\t\t[=](const MTP::Error &result) { error(result); });\n}\n\ntemplate <typename Request>\nauto ApiWrap::splitRequest(int index, Request &&request) {\n\tExpects(index < _splits.size());\n\n\t//if (index == _splits.size() - 1) {\n\t//\treturn mainRequest(std::forward<Request>(request));\n\t//}\n\treturn mainRequest(MTPInvokeWithMessagesRange<Request>(\n\t\t_splits[index],\n\t\tstd::forward<Request>(request)));\n}\n\nauto ApiWrap::fileRequest(const Data::FileLocation &location, int64 offset) {\n\tExpects(location.dcId != 0\n\t\t|| location.data.type() == mtpc_inputTakeoutFileLocation);\n\tExpects(_takeoutId.has_value());\n\tExpects(_fileProcess->requestId == 0);\n\n\treturn std::move(_mtp.request(MTPInvokeWithTakeout<MTPupload_GetFile>(\n\t\tMTP_long(*_takeoutId),\n\t\tMTPupload_GetFile(\n\t\t\tMTP_flags(0),\n\t\t\tlocation.data,\n\t\t\tMTP_long(offset),\n\t\t\tMTP_int(kFileChunkSize))\n\t)).fail([=](const MTP::Error &result) {\n\t\t_fileProcess->requestId = 0;\n\t\tif (result.type() == u\"TAKEOUT_FILE_EMPTY\"_q\n\t\t\t&& _otherDataProcess != nullptr) {\n\t\t\tfilePartDone(\n\t\t\t\t0,\n\t\t\t\tMTP_upload_file(\n\t\t\t\t\tMTP_storage_filePartial(),\n\t\t\t\t\tMTP_int(0),\n\t\t\t\t\tMTP_bytes()));\n\t\t} else if (result.type() == u\"LOCATION_INVALID\"_q\n\t\t\t|| result.type() == u\"VERSION_INVALID\"_q\n\t\t\t|| result.type() == u\"LOCATION_NOT_AVAILABLE\"_q) {\n\t\t\tfilePartUnavailable();\n\t\t} else if (result.code() == 400\n\t\t\t&& result.type().startsWith(u\"FILE_REFERENCE_\"_q)) {\n\t\t\tfilePartRefreshReference(offset);\n\t\t} else {\n\t\t\terror(std::move(result));\n\t\t}\n\t}).toDC(MTP::ShiftDcId(location.dcId, MTP::kExportMediaDcShift)));\n}\n\nApiWrap::ApiWrap(\n\tbase::weak_qptr<MTP::Instance> weak,\n\tFn<void(FnMut<void()>)> runner)\n: _mtp(weak, std::move(runner))\n, _fileCache(std::make_unique<LoadedFileCache>(kLocationCacheSize)) {\n}\n\nrpl::producer<MTP::Error> ApiWrap::errors() const {\n\treturn _errors.events();\n}\n\nrpl::producer<Output::Result> ApiWrap::ioErrors() const {\n\treturn _ioErrors.events();\n}\n\nvoid ApiWrap::startExport(\n\t\tconst Settings &settings,\n\t\tOutput::Stats *stats,\n\t\tFnMut<void(StartInfo)> done) {\n\tExpects(_settings == nullptr);\n\tExpects(_startProcess == nullptr);\n\n\t_settings = std::make_unique<Settings>(settings);\n\t_stats = stats;\n\t_startProcess = std::make_unique<StartProcess>();\n\t_startProcess->done = std::move(done);\n\n\tusing Step = StartProcess::Step;\n\tif (_settings->types & Settings::Type::Userpics) {\n\t\t_startProcess->steps.push_back(Step::UserpicsCount);\n\t}\n\tif (_settings->types & Settings::Type::Stories) {\n\t\t_startProcess->steps.push_back(Step::StoriesCount);\n\t}\n\tif (_settings->types & Settings::Type::ProfileMusic) {\n\t\t_startProcess->steps.push_back(Step::ProfileMusicCount);\n\t}\n\tif (_settings->types & Settings::Type::AnyChatsMask) {\n\t\t_startProcess->steps.push_back(Step::SplitRanges);\n\t\t_startProcess->steps.push_back(Step::DialogsCount);\n\t}\n\tif (_settings->types & Settings::Type::GroupsChannelsMask) {\n\t\tif (!_settings->onlySinglePeer()) {\n\t\t\t_startProcess->steps.push_back(Step::LeftChannelsCount);\n\t\t}\n\t}\n\tstartMainSession([=] {\n\t\tsendNextStartRequest();\n\t});\n}\n\nvoid ApiWrap::sendNextStartRequest() {\n\tExpects(_startProcess != nullptr);\n\n\tauto &steps = _startProcess->steps;\n\tif (steps.empty()) {\n\t\tfinishStartProcess();\n\t\treturn;\n\t}\n\tusing Step = StartProcess::Step;\n\tconst auto step = steps.front();\n\tsteps.pop_front();\n\tswitch (step) {\n\tcase Step::UserpicsCount:\n\t\treturn requestUserpicsCount();\n\tcase Step::StoriesCount:\n\t\treturn requestStoriesCount();\n\tcase Step::ProfileMusicCount:\n\t\treturn requestProfileMusicCount();\n\tcase Step::SplitRanges:\n\t\treturn requestSplitRanges();\n\tcase Step::DialogsCount:\n\t\treturn requestDialogsCount();\n\tcase Step::LeftChannelsCount:\n\t\treturn requestLeftChannelsCount();\n\t}\n\tUnexpected(\"Step in ApiWrap::sendNextStartRequest.\");\n}\n\nvoid ApiWrap::requestUserpicsCount() {\n\tExpects(_startProcess != nullptr);\n\n\tmainRequest(MTPphotos_GetUserPhotos(\n\t\t_user,\n\t\tMTP_int(0),  // offset\n\t\tMTP_long(0), // max_id\n\t\tMTP_int(0)   // limit\n\t)).done([=](const MTPphotos_Photos &result) {\n\t\tExpects(_settings != nullptr);\n\t\tExpects(_startProcess != nullptr);\n\n\t\t_startProcess->info.userpicsCount = result.match(\n\t\t[](const MTPDphotos_photos &data) {\n\t\t\treturn int(data.vphotos().v.size());\n\t\t}, [](const MTPDphotos_photosSlice &data) {\n\t\t\treturn data.vcount().v;\n\t\t});\n\n\t\tsendNextStartRequest();\n\t}).send();\n}\n\nvoid ApiWrap::requestStoriesCount() {\n\tExpects(_startProcess != nullptr);\n\n\tmainRequest(MTPstories_GetStoriesArchive(\n\t\tMTP_inputPeerSelf(),\n\t\tMTP_int(0), // offset_id\n\t\tMTP_int(0) // limit\n\t)).done([=](const MTPstories_Stories &result) {\n\t\tExpects(_settings != nullptr);\n\t\tExpects(_startProcess != nullptr);\n\n\t\t_startProcess->info.storiesCount = result.data().vcount().v;\n\n\t\tsendNextStartRequest();\n\t}).send();\n}\n\nvoid ApiWrap::requestProfileMusicCount() {\n\tExpects(_startProcess != nullptr);\n\n\tmainRequest(MTPusers_GetSavedMusic(\n\t\t_user,\n\t\tMTP_int(0), // offset\n\t\tMTP_int(0), // limit\n\t\tMTP_long(0) // hash\n\t)).done([=](const MTPusers_SavedMusic &result) {\n\t\tExpects(_settings != nullptr);\n\t\tExpects(_startProcess != nullptr);\n\n\t\tconst auto count = result.match(\n\t\t[](const MTPDusers_savedMusic &data) {\n\t\t\treturn data.vcount().v;\n\t\t}, [](const MTPDusers_savedMusicNotModified &data) {\n\t\t\treturn -1;\n\t\t});\n\t\tif (count < 0) {\n\t\t\terror(\"Unexpected messagesNotModified received.\");\n\t\t\treturn;\n\t\t}\n\t\t_startProcess->info.profileMusicCount = count;\n\n\t\tsendNextStartRequest();\n\t}).send();\n}\n\nvoid ApiWrap::requestSplitRanges() {\n\tExpects(_startProcess != nullptr);\n\n\tmainRequest(MTPmessages_GetSplitRanges(\n\t)).done([=](const MTPVector<MTPMessageRange> &result) {\n\t\t_splits = result.v;\n\t\tif (_splits.empty()) {\n\t\t\t_splits.push_back(MTP_messageRange(\n\t\t\t\tMTP_int(1),\n\t\t\t\tMTP_int(std::numeric_limits<int>::max())));\n\t\t}\n\t\t_startProcess->splitIndex = useOnlyLastSplit()\n\t\t\t? (_splits.size() - 1)\n\t\t\t: 0;\n\n\t\tsendNextStartRequest();\n\t}).send();\n}\n\nvoid ApiWrap::requestDialogsCount() {\n\tExpects(_startProcess != nullptr);\n\n\tif (_settings->onlySinglePeer()) {\n\t\t_startProcess->info.dialogsCount\n\t\t\t= (_settings->singlePeer.type() == mtpc_inputPeerChannel\n\t\t\t\t? 1\n\t\t\t\t: _splits.size());\n\t\tsendNextStartRequest();\n\t\treturn;\n\t}\n\n\tconst auto offsetDate = 0;\n\tconst auto offsetId = 0;\n\tconst auto offsetPeer = MTP_inputPeerEmpty();\n\tconst auto limit = 1;\n\tconst auto hash = uint64(0);\n\tsplitRequest(_startProcess->splitIndex, MTPmessages_GetDialogs(\n\t\tMTP_flags(0),\n\t\tMTPint(), // folder_id\n\t\tMTP_int(offsetDate),\n\t\tMTP_int(offsetId),\n\t\toffsetPeer,\n\t\tMTP_int(limit),\n\t\tMTP_long(hash)\n\t)).done([=](const MTPmessages_Dialogs &result) {\n\t\tExpects(_settings != nullptr);\n\t\tExpects(_startProcess != nullptr);\n\n\t\tconst auto count = result.match(\n\t\t[](const MTPDmessages_dialogs &data) {\n\t\t\treturn int(data.vdialogs().v.size());\n\t\t}, [](const MTPDmessages_dialogsSlice &data) {\n\t\t\treturn data.vcount().v;\n\t\t}, [](const MTPDmessages_dialogsNotModified &data) {\n\t\t\treturn -1;\n\t\t});\n\t\tif (count < 0) {\n\t\t\terror(\"Unexpected dialogsNotModified received.\");\n\t\t\treturn;\n\t\t}\n\t\t_startProcess->info.dialogsCount += count;\n\n\t\tif (++_startProcess->splitIndex >= _splits.size()) {\n\t\t\tsendNextStartRequest();\n\t\t} else {\n\t\t\trequestDialogsCount();\n\t\t}\n\t}).send();\n}\n\nvoid ApiWrap::requestLeftChannelsCount() {\n\tExpects(_startProcess != nullptr);\n\tExpects(_leftChannelsProcess == nullptr);\n\n\t_leftChannelsProcess = std::make_unique<LeftChannelsProcess>();\n\trequestLeftChannelsSliceGeneric([=] {\n\t\tExpects(_startProcess != nullptr);\n\t\tExpects(_leftChannelsProcess != nullptr);\n\n\t\t_startProcess->info.dialogsCount\n\t\t\t+= _leftChannelsProcess->fullCount;\n\t\tsendNextStartRequest();\n\t});\n}\n\nvoid ApiWrap::finishStartProcess() {\n\tExpects(_startProcess != nullptr);\n\n\tconst auto process = base::take(_startProcess);\n\tprocess->done(process->info);\n}\n\nbool ApiWrap::useOnlyLastSplit() const {\n\treturn !(_settings->types & Settings::Type::NonChannelChatsMask);\n}\n\nvoid ApiWrap::requestLeftChannelsList(\n\t\tFn<bool(int count)> progress,\n\t\tFnMut<void(Data::DialogsInfo&&)> done) {\n\tExpects(_leftChannelsProcess != nullptr);\n\n\t_leftChannelsProcess->progress = std::move(progress);\n\t_leftChannelsProcess->done = std::move(done);\n\trequestLeftChannelsSlice();\n}\n\nvoid ApiWrap::requestLeftChannelsSlice() {\n\trequestLeftChannelsSliceGeneric([=] {\n\t\tExpects(_leftChannelsProcess != nullptr);\n\n\t\tif (_leftChannelsProcess->finished) {\n\t\t\tconst auto process = base::take(_leftChannelsProcess);\n\t\t\tprocess->done(std::move(process->info));\n\t\t} else {\n\t\t\trequestLeftChannelsSlice();\n\t\t}\n\t});\n}\n\nvoid ApiWrap::requestDialogsList(\n\t\tFn<bool(int count)> progress,\n\t\tFnMut<void(Data::DialogsInfo&&)> done) {\n\tExpects(_dialogsProcess == nullptr);\n\n\t_dialogsProcess = std::make_unique<DialogsProcess>();\n\t_dialogsProcess->splitIndexPlusOne = _splits.size();\n\t_dialogsProcess->progress = std::move(progress);\n\t_dialogsProcess->done = std::move(done);\n\n\trequestDialogsSlice();\n}\n\nvoid ApiWrap::startMainSession(FnMut<void()> done) {\n\tusing Type = Settings::Type;\n\tconst auto sizeLimit = _settings->media.sizeLimit;\n\tconst auto hasFiles = ((_settings->media.types != 0) && (sizeLimit > 0))\n\t\t|| (_settings->types & Type::Userpics)\n\t\t|| (_settings->types & Type::Stories);\n\n\tusing Flag = MTPaccount_InitTakeoutSession::Flag;\n\tconst auto flags = Flag(0)\n\t\t| (_settings->types & Type::Contacts ? Flag::f_contacts : Flag(0))\n\t\t| (hasFiles ? Flag::f_files : Flag(0))\n\t\t| ((hasFiles && sizeLimit < kFileMaxSize)\n\t\t\t? Flag::f_file_max_size\n\t\t\t: Flag(0))\n\t\t| (_settings->types & (Type::PersonalChats | Type::BotChats)\n\t\t\t? Flag::f_message_users\n\t\t\t: Flag(0))\n\t\t| (_settings->types & Type::PrivateGroups\n\t\t\t? (Flag::f_message_chats | Flag::f_message_megagroups)\n\t\t\t: Flag(0))\n\t\t| (_settings->types & Type::PublicGroups\n\t\t\t? Flag::f_message_megagroups\n\t\t\t: Flag(0))\n\t\t| (_settings->types & (Type::PrivateChannels | Type::PublicChannels)\n\t\t\t? Flag::f_message_channels\n\t\t\t: Flag(0));\n\n\t_mtp.request(MTPusers_GetUsers(\n\t\tMTP_vector<MTPInputUser>(1, MTP_inputUserSelf())\n\t)).done([=, done = std::move(done)](\n\t\t\tconst MTPVector<MTPUser> &result) mutable {\n\t\tfor (const auto &user : result.v) {\n\t\t\tuser.match([&](const MTPDuser &data) {\n\t\t\t\tif (data.is_self()) {\n\t\t\t\t\t_selfId.emplace(data.vid());\n\t\t\t\t}\n\t\t\t}, [&](const MTPDuserEmpty&) {\n\t\t\t});\n\t\t}\n\t\tif (!_selfId) {\n\t\t\terror(\"Could not retrieve selfId.\");\n\t\t\treturn;\n\t\t}\n\t\t_mtp.request(MTPaccount_InitTakeoutSession(\n\t\t\tMTP_flags(flags),\n\t\t\tMTP_long(sizeLimit)\n\t\t)).done([=, done = std::move(done)](\n\t\t\t\tconst MTPaccount_Takeout &result) mutable {\n\t\t\t_takeoutId = result.match([](const MTPDaccount_takeout &data) {\n\t\t\t\treturn data.vid().v;\n\t\t\t});\n\t\t\tdone();\n\t\t}).fail([=](const MTP::Error &result) {\n\t\t\terror(result);\n\t\t}).toDC(MTP::ShiftDcId(0, MTP::kExportDcShift)).send();\n\t}).fail([=](const MTP::Error &result) {\n\t\terror(result);\n\t}).send();\n}\n\nvoid ApiWrap::requestPersonalInfo(FnMut<void(Data::PersonalInfo&&)> done) {\n\tmainRequest(MTPusers_GetFullUser(\n\t\t_user\n\t)).done([=, done = std::move(done)](const MTPusers_UserFull &result) mutable {\n\t\tresult.match([&](const MTPDusers_userFull &data) {\n\t\t\tif (!data.vusers().v.empty()) {\n\t\t\t\tdone(Data::ParsePersonalInfo(data));\n\t\t\t} else {\n\t\t\t\terror(\"Bad user type.\");\n\t\t\t}\n\t\t});\n\t}).send();\n}\n\nvoid ApiWrap::requestOtherData(\n\t\tconst QString &suggestedPath,\n\t\tFnMut<void(Data::File&&)> done) {\n\tExpects(_otherDataProcess == nullptr);\n\n\t_otherDataProcess = std::make_unique<OtherDataProcess>();\n\t_otherDataProcess->done = std::move(done);\n\t_otherDataProcess->file.location.data = MTP_inputTakeoutFileLocation();\n\t_otherDataProcess->file.suggestedPath = suggestedPath;\n\tloadFile(\n\t\t_otherDataProcess->file,\n\t\tData::FileOrigin(),\n\t\t[](FileProgress progress) { return true; },\n\t\t[=](const QString &result) { otherDataDone(result); });\n}\n\nvoid ApiWrap::otherDataDone(const QString &relativePath) {\n\tExpects(_otherDataProcess != nullptr);\n\n\tconst auto process = base::take(_otherDataProcess);\n\tprocess->file.relativePath = relativePath;\n\tif (relativePath.isEmpty()) {\n\t\tprocess->file.skipReason = Data::File::SkipReason::Unavailable;\n\t}\n\tprocess->done(std::move(process->file));\n}\n\nvoid ApiWrap::requestUserpics(\n\t\tFnMut<bool(Data::UserpicsInfo&&)> start,\n\t\tFn<bool(DownloadProgress)> progress,\n\t\tFn<bool(Data::UserpicsSlice&&)> slice,\n\t\tFnMut<void()> finish) {\n\tExpects(_userpicsProcess == nullptr);\n\n\t_userpicsProcess = std::make_unique<UserpicsProcess>();\n\t_userpicsProcess->start = std::move(start);\n\t_userpicsProcess->fileProgress = std::move(progress);\n\t_userpicsProcess->handleSlice = std::move(slice);\n\t_userpicsProcess->finish = std::move(finish);\n\n\tmainRequest(MTPphotos_GetUserPhotos(\n\t\t_user,\n\t\tMTP_int(0), // offset\n\t\tMTP_long(_userpicsProcess->maxId),\n\t\tMTP_int(kUserpicsSliceLimit)\n\t)).done([=](const MTPphotos_Photos &result) mutable {\n\t\tExpects(_userpicsProcess != nullptr);\n\n\t\tauto startInfo = result.match(\n\t\t[](const MTPDphotos_photos &data) {\n\t\t\treturn Data::UserpicsInfo{ int(data.vphotos().v.size()) };\n\t\t}, [](const MTPDphotos_photosSlice &data) {\n\t\t\treturn Data::UserpicsInfo{ data.vcount().v };\n\t\t});\n\t\tif (!_userpicsProcess->start(std::move(startInfo))) {\n\t\t\treturn;\n\t\t}\n\n\t\thandleUserpicsSlice(result);\n\t}).send();\n}\n\nvoid ApiWrap::handleUserpicsSlice(const MTPphotos_Photos &result) {\n\tExpects(_userpicsProcess != nullptr);\n\n\tresult.match([&](const auto &data) {\n\t\tif constexpr (MTPDphotos_photos::Is<decltype(data)>()) {\n\t\t\t_userpicsProcess->lastSlice = true;\n\t\t}\n\t\tloadUserpicsFiles(Data::ParseUserpicsSlice(\n\t\t\tdata.vphotos(),\n\t\t\t_userpicsProcess->processed));\n\t});\n}\n\nvoid ApiWrap::loadUserpicsFiles(Data::UserpicsSlice &&slice) {\n\tExpects(_userpicsProcess != nullptr);\n\tExpects(!_userpicsProcess->slice.has_value());\n\n\tif (slice.list.empty()) {\n\t\t_userpicsProcess->lastSlice = true;\n\t}\n\t_userpicsProcess->slice = std::move(slice);\n\t_userpicsProcess->fileIndex = 0;\n\tloadNextUserpic();\n}\n\nvoid ApiWrap::loadNextUserpic() {\n\tExpects(_userpicsProcess != nullptr);\n\tExpects(_userpicsProcess->slice.has_value());\n\n\tfor (auto &list = _userpicsProcess->slice->list\n\t\t; _userpicsProcess->fileIndex < list.size()\n\t\t; ++_userpicsProcess->fileIndex) {\n\t\tconst auto ready = processFileLoad(\n\t\t\tlist[_userpicsProcess->fileIndex].image.file,\n\t\t\tData::FileOrigin(),\n\t\t\t[=](FileProgress value) { return loadUserpicProgress(value); },\n\t\t\t[=](const QString &path) { loadUserpicDone(path); });\n\t\tif (!ready) {\n\t\t\treturn;\n\t\t}\n\t}\n\tfinishUserpicsSlice();\n}\n\nvoid ApiWrap::finishUserpicsSlice() {\n\tExpects(_userpicsProcess != nullptr);\n\tExpects(_userpicsProcess->slice.has_value());\n\n\tauto slice = *base::take(_userpicsProcess->slice);\n\tif (!slice.list.empty()) {\n\t\t_userpicsProcess->processed += slice.list.size();\n\t\t_userpicsProcess->maxId = slice.list.back().id;\n\t\tif (!_userpicsProcess->handleSlice(std::move(slice))) {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (_userpicsProcess->lastSlice) {\n\t\tfinishUserpics();\n\t\treturn;\n\t}\n\n\tmainRequest(MTPphotos_GetUserPhotos(\n\t\t_user,\n\t\tMTP_int(0), // offset\n\t\tMTP_long(_userpicsProcess->maxId),\n\t\tMTP_int(kUserpicsSliceLimit)\n\t)).done([=](const MTPphotos_Photos &result) {\n\t\thandleUserpicsSlice(result);\n\t}).send();\n}\n\nbool ApiWrap::loadUserpicProgress(FileProgress progress) {\n\tExpects(_fileProcess != nullptr);\n\tExpects(_userpicsProcess != nullptr);\n\tExpects(_userpicsProcess->slice.has_value());\n\tExpects((_userpicsProcess->fileIndex >= 0)\n\t\t&& (_userpicsProcess->fileIndex\n\t\t\t< _userpicsProcess->slice->list.size()));\n\n\treturn _userpicsProcess->fileProgress(DownloadProgress{\n\t\t_fileProcess->randomId,\n\t\t_fileProcess->relativePath,\n\t\t_userpicsProcess->fileIndex,\n\t\tprogress.ready,\n\t\tprogress.total });\n}\n\nvoid ApiWrap::loadUserpicDone(const QString &relativePath) {\n\tExpects(_userpicsProcess != nullptr);\n\tExpects(_userpicsProcess->slice.has_value());\n\tExpects((_userpicsProcess->fileIndex >= 0)\n\t\t&& (_userpicsProcess->fileIndex\n\t\t\t< _userpicsProcess->slice->list.size()));\n\n\tconst auto index = _userpicsProcess->fileIndex;\n\tauto &file = _userpicsProcess->slice->list[index].image.file;\n\tfile.relativePath = relativePath;\n\tif (relativePath.isEmpty()) {\n\t\tfile.skipReason = Data::File::SkipReason::Unavailable;\n\t}\n\tloadNextUserpic();\n}\n\nvoid ApiWrap::finishUserpics() {\n\tExpects(_userpicsProcess != nullptr);\n\n\tbase::take(_userpicsProcess)->finish();\n}\n\nvoid ApiWrap::requestStories(\n\t\tFnMut<bool(Data::StoriesInfo&&)> start,\n\t\tFn<bool(DownloadProgress)> progress,\n\t\tFn<bool(Data::StoriesSlice&&)> slice,\n\t\tFnMut<void()> finish) {\n\tExpects(_storiesProcess == nullptr);\n\n\t_storiesProcess = std::make_unique<StoriesProcess>();\n\t_storiesProcess->start = std::move(start);\n\t_storiesProcess->fileProgress = std::move(progress);\n\t_storiesProcess->handleSlice = std::move(slice);\n\t_storiesProcess->finish = std::move(finish);\n\n\tmainRequest(MTPstories_GetStoriesArchive(\n\t\tMTP_inputPeerSelf(),\n\t\tMTP_int(_storiesProcess->offsetId),\n\t\tMTP_int(kStoriesSliceLimit)\n\t)).done([=](const MTPstories_Stories &result) mutable {\n\t\tExpects(_storiesProcess != nullptr);\n\n\t\tauto startInfo = Data::StoriesInfo{ result.data().vcount().v };\n\t\tif (!_storiesProcess->start(std::move(startInfo))) {\n\t\t\treturn;\n\t\t}\n\n\t\thandleStoriesSlice(result);\n\t}).send();\n}\n\nvoid ApiWrap::handleStoriesSlice(const MTPstories_Stories &result) {\n\tExpects(_storiesProcess != nullptr);\n\n\tloadStoriesFiles(Data::ParseStoriesSlice(\n\t\tresult.data().vstories(),\n\t\t_storiesProcess->processed));\n}\n\nvoid ApiWrap::loadStoriesFiles(Data::StoriesSlice &&slice) {\n\tExpects(_storiesProcess != nullptr);\n\tExpects(!_storiesProcess->slice.has_value());\n\n\tif (!slice.lastId) {\n\t\t_storiesProcess->lastSlice = true;\n\t}\n\t_storiesProcess->slice = std::move(slice);\n\t_storiesProcess->fileIndex = 0;\n\tloadNextStory();\n}\n\nvoid ApiWrap::loadNextStory() {\n\tExpects(_storiesProcess != nullptr);\n\tExpects(_storiesProcess->slice.has_value());\n\n\tfor (auto &list = _storiesProcess->slice->list\n\t\t; _storiesProcess->fileIndex < list.size()\n\t\t; ++_storiesProcess->fileIndex) {\n\t\tauto &story = list[_storiesProcess->fileIndex];\n\t\tconst auto origin = Data::FileOrigin{ .storyId = story.id };\n\t\tconst auto ready = processFileLoad(\n\t\t\tstory.file(),\n\t\t\torigin,\n\t\t\t[=](FileProgress value) { return loadStoryProgress(value); },\n\t\t\t[=](const QString &path) { loadStoryDone(path); });\n\t\tif (!ready) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto thumbProgress = [=](FileProgress value) {\n\t\t\treturn loadStoryThumbProgress(value);\n\t\t};\n\t\tconst auto thumbReady = processFileLoad(\n\t\t\tstory.thumb().file,\n\t\t\torigin,\n\t\t\tthumbProgress,\n\t\t\t[=](const QString &path) { loadStoryThumbDone(path); },\n\t\t\tnullptr,\n\t\t\t&story);\n\t\tif (!thumbReady) {\n\t\t\treturn;\n\t\t}\n\t}\n\tfinishStoriesSlice();\n}\n\nvoid ApiWrap::finishStoriesSlice() {\n\tExpects(_storiesProcess != nullptr);\n\tExpects(_storiesProcess->slice.has_value());\n\n\tauto slice = *base::take(_storiesProcess->slice);\n\tif (slice.lastId) {\n\t\t_storiesProcess->processed += slice.list.size();\n\t\t_storiesProcess->offsetId = slice.lastId;\n\t\tif (!_storiesProcess->handleSlice(std::move(slice))) {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (_storiesProcess->lastSlice) {\n\t\tfinishStories();\n\t\treturn;\n\t}\n\n\tmainRequest(MTPstories_GetStoriesArchive(\n\t\tMTP_inputPeerSelf(),\n\t\tMTP_int(_storiesProcess->offsetId),\n\t\tMTP_int(kStoriesSliceLimit)\n\t)).done([=](const MTPstories_Stories &result) {\n\t\thandleStoriesSlice(result);\n\t}).send();\n}\n\nbool ApiWrap::loadStoryProgress(FileProgress progress) {\n\tExpects(_fileProcess != nullptr);\n\tExpects(_storiesProcess != nullptr);\n\tExpects(_storiesProcess->slice.has_value());\n\tExpects((_storiesProcess->fileIndex >= 0)\n\t\t&& (_storiesProcess->fileIndex\n\t\t\t< _storiesProcess->slice->list.size()));\n\n\treturn _storiesProcess->fileProgress(DownloadProgress{\n\t\t_fileProcess->randomId,\n\t\t_fileProcess->relativePath,\n\t\t_storiesProcess->fileIndex,\n\t\tprogress.ready,\n\t\tprogress.total });\n}\n\nvoid ApiWrap::loadStoryDone(const QString &relativePath) {\n\tExpects(_storiesProcess != nullptr);\n\tExpects(_storiesProcess->slice.has_value());\n\tExpects((_storiesProcess->fileIndex >= 0)\n\t\t&& (_storiesProcess->fileIndex\n\t\t\t< _storiesProcess->slice->list.size()));\n\n\tconst auto index = _storiesProcess->fileIndex;\n\tauto &file = _storiesProcess->slice->list[index].file();\n\tfile.relativePath = relativePath;\n\tif (relativePath.isEmpty()) {\n\t\tfile.skipReason = Data::File::SkipReason::Unavailable;\n\t}\n\tloadNextStory();\n}\n\nbool ApiWrap::loadStoryThumbProgress(FileProgress progress) {\n\treturn loadStoryProgress(progress);\n}\n\nvoid ApiWrap::loadStoryThumbDone(const QString &relativePath) {\n\tExpects(_storiesProcess != nullptr);\n\tExpects(_storiesProcess->slice.has_value());\n\tExpects((_storiesProcess->fileIndex >= 0)\n\t\t&& (_storiesProcess->fileIndex\n\t\t\t< _storiesProcess->slice->list.size()));\n\n\tconst auto index = _storiesProcess->fileIndex;\n\tauto &file = _storiesProcess->slice->list[index].thumb().file;\n\tfile.relativePath = relativePath;\n\tif (relativePath.isEmpty()) {\n\t\tfile.skipReason = Data::File::SkipReason::Unavailable;\n\t}\n\tloadNextStory();\n}\n\nvoid ApiWrap::finishStories() {\n\tExpects(_storiesProcess != nullptr);\n\n\tbase::take(_storiesProcess)->finish();\n}\n\nvoid ApiWrap::requestProfileMusic(\n\t\tFnMut<bool(Data::ProfileMusicInfo&&)> start,\n\t\tFn<bool(DownloadProgress)> progress,\n\t\tFn<bool(Data::ProfileMusicSlice&&)> slice,\n\t\tFnMut<void()> finish) {\n\tExpects(_profileMusicProcess == nullptr);\n\n\t_profileMusicProcess = std::make_unique<ProfileMusicProcess>();\n\t_profileMusicProcess->start = std::move(start);\n\t_profileMusicProcess->fileProgress = std::move(progress);\n\t_profileMusicProcess->handleSlice = std::move(slice);\n\t_profileMusicProcess->finish = std::move(finish);\n\n\tmainRequest(MTPusers_GetSavedMusic(\n\t\t_user,\n\t\tMTP_int(0), // offset\n\t\tMTP_int(kProfileMusicSliceLimit), // limit\n\t\tMTP_long(0) // hash\n\t)).done([=](const MTPusers_SavedMusic &result) mutable {\n\t\tExpects(_profileMusicProcess != nullptr);\n\n\t\tauto startInfo = result.match(\n\t\t[](const MTPDusers_savedMusic &data) {\n\t\t\treturn Data::ProfileMusicInfo{ data.vcount().v };\n\t\t}, [](const MTPDusers_savedMusicNotModified &data) {\n\t\t\treturn Data::ProfileMusicInfo{ 0 };\n\t\t});\n\t\tif (!_profileMusicProcess->start(std::move(startInfo))) {\n\t\t\treturn;\n\t\t}\n\n\t\thandleProfileMusicSlice(result);\n\t}).send();\n}\n\nvoid ApiWrap::handleProfileMusicSlice(const MTPusers_SavedMusic &result) {\n\tExpects(_profileMusicProcess != nullptr);\n\tExpects(_selfId.has_value());\n\n\tauto context = Data::ParseMediaContext();\n\tcontext.selfPeerId = peerFromUser(*_selfId);\n\n\tauto slice = result.match([&](const MTPDusers_savedMusic &data) {\n\t\tif (data.vdocuments().v.size() < kProfileMusicSliceLimit) {\n\t\t\t_profileMusicProcess->lastSlice = true;\n\t\t}\n\t\tauto result = Data::MessagesSlice();\n\t\tfor (const auto &doc : data.vdocuments().v) {\n\t\t\tauto message = Data::Message();\n\t\t\tmessage.id = ++_profileMusicProcess->processed;\n\t\t\tmessage.date = 0;\n\t\t\tmessage.media.content = Data::ParseDocument(\n\t\t\t\tcontext,\n\t\t\t\tdoc,\n\t\t\t\t\"profile_music/\",\n\t\t\t\t0);\n\t\t\tresult.list.push_back(std::move(message));\n\t\t}\n\t\treturn result;\n\t}, [&](const MTPDusers_savedMusicNotModified &) {\n\t\t_profileMusicProcess->lastSlice = true;\n\t\treturn Data::MessagesSlice();\n\t});\n\n\tauto profileSlice = Data::ProfileMusicSlice();\n\tprofileSlice.list.reserve(slice.list.size());\n\tfor (auto &message : slice.list) {\n\t\tif (v::is<Data::Document>(message.media.content)) {\n\t\t\tconst auto &doc = v::get<Data::Document>(message.media.content);\n\t\t\tif (doc.isAudioFile) {\n\t\t\t\tprofileSlice.list.push_back(std::move(message));\n\t\t\t}\n\t\t}\n\t}\n\n\tloadProfileMusicFiles(std::move(profileSlice));\n}\n\nvoid ApiWrap::loadProfileMusicFiles(Data::ProfileMusicSlice &&slice) {\n\tExpects(_profileMusicProcess != nullptr);\n\tExpects(!_profileMusicProcess->slice.has_value());\n\n\tif (slice.list.empty()) {\n\t\t_profileMusicProcess->lastSlice = true;\n\t}\n\t_profileMusicProcess->slice = std::move(slice);\n\t_profileMusicProcess->fileIndex = 0;\n\tloadNextProfileMusic();\n}\n\nvoid ApiWrap::loadNextProfileMusic() {\n\tExpects(_profileMusicProcess != nullptr);\n\tExpects(_profileMusicProcess->slice.has_value());\n\n\tfor (auto &list = _profileMusicProcess->slice->list\n\t\t; _profileMusicProcess->fileIndex < list.size()\n\t\t; ++_profileMusicProcess->fileIndex) {\n\t\tauto &message = list[_profileMusicProcess->fileIndex];\n\t\tconst auto origin = Data::FileOrigin{ .messageId = message.id };\n\t\tconst auto ready = processFileLoad(\n\t\t\tmessage.file(),\n\t\t\torigin,\n\t\t\t[=](FileProgress value) { return loadProfileMusicProgress(value); },\n\t\t\t[=](const QString &path) { loadProfileMusicDone(path); },\n\t\t\t&message);\n\t\tif (!ready) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto thumbProgress = [=](FileProgress value) {\n\t\t\treturn loadProfileMusicThumbProgress(value);\n\t\t};\n\t\tconst auto thumbReady = processFileLoad(\n\t\t\tmessage.thumb().file,\n\t\t\torigin,\n\t\t\tthumbProgress,\n\t\t\t[=](const QString &path) { loadProfileMusicThumbDone(path); },\n\t\t\t&message);\n\t\tif (!thumbReady) {\n\t\t\treturn;\n\t\t}\n\t}\n\tfinishProfileMusicSlice();\n}\n\nvoid ApiWrap::finishProfileMusicSlice() {\n\tExpects(_profileMusicProcess != nullptr);\n\tExpects(_profileMusicProcess->slice.has_value());\n\n\tauto slice = *base::take(_profileMusicProcess->slice);\n\tif (!slice.list.empty()) {\n\t\t_profileMusicProcess->processed += slice.list.size();\n\t\t_profileMusicProcess->offsetId = slice.list.back().id;\n\t\tif (!_profileMusicProcess->handleSlice(std::move(slice))) {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (_profileMusicProcess->lastSlice) {\n\t\tfinishProfileMusic();\n\t\treturn;\n\t}\n\n\tmainRequest(MTPusers_GetSavedMusic(\n\t\t_user,\n\t\tMTP_int(_profileMusicProcess->offsetId),\n\t\tMTP_int(kProfileMusicSliceLimit),\n\t\tMTP_long(0)\n\t)).done([=](const MTPusers_SavedMusic &result) {\n\t\thandleProfileMusicSlice(result);\n\t}).send();\n}\n\nbool ApiWrap::loadProfileMusicProgress(FileProgress progress) {\n\tExpects(_fileProcess != nullptr);\n\tExpects(_profileMusicProcess != nullptr);\n\tExpects(_profileMusicProcess->slice.has_value());\n\tExpects((_profileMusicProcess->fileIndex >= 0)\n\t\t&& (_profileMusicProcess->fileIndex\n\t\t\t< _profileMusicProcess->slice->list.size()));\n\n\treturn _profileMusicProcess->fileProgress(DownloadProgress{\n\t\t_fileProcess->randomId,\n\t\t_fileProcess->relativePath,\n\t\t_profileMusicProcess->fileIndex,\n\t\tprogress.ready,\n\t\tprogress.total });\n}\n\nvoid ApiWrap::loadProfileMusicDone(const QString &relativePath) {\n\tExpects(_profileMusicProcess != nullptr);\n\tExpects(_profileMusicProcess->slice.has_value());\n\tExpects((_profileMusicProcess->fileIndex >= 0)\n\t\t&& (_profileMusicProcess->fileIndex\n\t\t\t< _profileMusicProcess->slice->list.size()));\n\n\tconst auto index = _profileMusicProcess->fileIndex;\n\tauto &file = _profileMusicProcess->slice->list[index].file();\n\tfile.relativePath = relativePath;\n\tif (relativePath.isEmpty()) {\n\t\tfile.skipReason = Data::File::SkipReason::Unavailable;\n\t}\n\tloadNextProfileMusic();\n}\n\nbool ApiWrap::loadProfileMusicThumbProgress(FileProgress progress) {\n\treturn loadProfileMusicProgress(progress);\n}\n\nvoid ApiWrap::loadProfileMusicThumbDone(const QString &relativePath) {\n\tExpects(_profileMusicProcess != nullptr);\n\tExpects(_profileMusicProcess->slice.has_value());\n\tExpects((_profileMusicProcess->fileIndex >= 0)\n\t\t&& (_profileMusicProcess->fileIndex\n\t\t\t< _profileMusicProcess->slice->list.size()));\n\n\tconst auto index = _profileMusicProcess->fileIndex;\n\tauto &file = _profileMusicProcess->slice->list[index].thumb().file;\n\tfile.relativePath = relativePath;\n\tif (relativePath.isEmpty()) {\n\t\tfile.skipReason = Data::File::SkipReason::Unavailable;\n\t}\n\tloadNextProfileMusic();\n}\n\nvoid ApiWrap::finishProfileMusic() {\n\tExpects(_profileMusicProcess != nullptr);\n\n\tbase::take(_profileMusicProcess)->finish();\n}\n\nvoid ApiWrap::requestContacts(FnMut<void(Data::ContactsList&&)> done) {\n\tExpects(_contactsProcess == nullptr);\n\n\t_contactsProcess = std::make_unique<ContactsProcess>();\n\t_contactsProcess->done = std::move(done);\n\tmainRequest(MTPcontacts_GetSaved(\n\t)).done([=](const MTPVector<MTPSavedContact> &result) {\n\t\t_contactsProcess->result = Data::ParseContactsList(result);\n\n\t\tconst auto resolve = [=](int index, const auto &resolveNext) -> void {\n\t\t\tif (index == _contactsProcess->result.list.size()) {\n\t\t\t\treturn requestTopPeersSlice();\n\t\t\t}\n\t\t\tconst auto &contact = _contactsProcess->result.list[index];\n\t\t\tmainRequest(MTPcontacts_ResolvePhone(\n\t\t\t\tMTP_string(qs(contact.phoneNumber))\n\t\t\t)).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\t\t\tauto &contact = _contactsProcess->result.list[index];\n\t\t\t\tcontact.userId = result.data().vpeer().match([&](\n\t\t\t\t\t\tconst MTPDpeerUser &user) {\n\t\t\t\t\treturn UserId(user.vuser_id());\n\t\t\t\t}, [](const auto &) {\n\t\t\t\t\treturn UserId();\n\t\t\t\t});\n\t\t\t\tresolveNext(index + 1, resolveNext);\n\t\t\t}).fail([=](const MTP::Error &) {\n\t\t\t\tresolveNext(index + 1, resolveNext);\n\t\t\t\treturn true;\n\t\t\t}).send();\n\t\t};\n\n\t\tif (base::options::lookup<bool>(\"show-peer-id-below-about\").value()) {\n\t\t\tresolve(0, resolve);\n\t\t} else {\n\t\t\trequestTopPeersSlice();\n\t\t}\n\n\t}).send();\n}\n\nvoid ApiWrap::requestTopPeersSlice() {\n\tExpects(_contactsProcess != nullptr);\n\n\tusing Flag = MTPcontacts_GetTopPeers::Flag;\n\tmainRequest(MTPcontacts_GetTopPeers(\n\t\tMTP_flags(Flag::f_correspondents\n\t\t\t| Flag::f_bots_inline\n\t\t\t| Flag::f_phone_calls),\n\t\tMTP_int(_contactsProcess->topPeersOffset),\n\t\tMTP_int(kTopPeerSliceLimit),\n\t\tMTP_long(0) // hash\n\t)).done([=](const MTPcontacts_TopPeers &result) {\n\t\tExpects(_contactsProcess != nullptr);\n\n\t\tif (!Data::AppendTopPeers(_contactsProcess->result, result)) {\n\t\t\terror(\"Unexpected data in ApiWrap::requestTopPeersSlice.\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto offset = _contactsProcess->topPeersOffset;\n\t\tconst auto loaded = result.match(\n\t\t[](const MTPDcontacts_topPeersNotModified &data) {\n\t\t\treturn true;\n\t\t}, [](const MTPDcontacts_topPeersDisabled &data) {\n\t\t\treturn true;\n\t\t}, [&](const MTPDcontacts_topPeers &data) {\n\t\t\tfor (const auto &category : data.vcategories().v) {\n\t\t\t\tconst auto loaded = category.match(\n\t\t\t\t[&](const MTPDtopPeerCategoryPeers &data) {\n\t\t\t\t\treturn offset + data.vpeers().v.size() >= data.vcount().v;\n\t\t\t\t});\n\t\t\t\tif (!loaded) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\n\t\tif (loaded) {\n\t\t\tauto process = base::take(_contactsProcess);\n\t\t\tprocess->done(std::move(process->result));\n\t\t} else {\n\t\t\t_contactsProcess->topPeersOffset = std::max(std::max(\n\t\t\t\t_contactsProcess->result.correspondents.size(),\n\t\t\t\t_contactsProcess->result.inlineBots.size()),\n\t\t\t\t_contactsProcess->result.phoneCalls.size());\n\t\t\trequestTopPeersSlice();\n\t\t}\n\t}).send();\n}\n\nvoid ApiWrap::requestSessions(FnMut<void(Data::SessionsList&&)> done) {\n\tmainRequest(MTPaccount_GetAuthorizations(\n\t)).done([=, done = std::move(done)](\n\t\t\tconst MTPaccount_Authorizations &result) mutable {\n\t\tauto list = Data::ParseSessionsList(result);\n\t\tmainRequest(MTPaccount_GetWebAuthorizations(\n\t\t)).done([=, done = std::move(done), list = std::move(list)](\n\t\t\t\tconst MTPaccount_WebAuthorizations &result) mutable {\n\t\t\tlist.webList = Data::ParseWebSessionsList(result).webList;\n\t\t\tdone(std::move(list));\n\t\t}).send();\n\t}).send();\n}\n\nvoid ApiWrap::requestMessages(\n\t\tconst Data::DialogInfo &info,\n\t\tFnMut<bool(const Data::DialogInfo &)> start,\n\t\tFn<bool(DownloadProgress)> progress,\n\t\tFn<bool(Data::MessagesSlice&&)> slice,\n\t\tFnMut<void()> done) {\n\tExpects(_chatProcess == nullptr);\n\tExpects(_selfId.has_value());\n\n\t_chatProcess = std::make_unique<ChatProcess>();\n\t_chatProcess->context.selfPeerId = peerFromUser(*_selfId);\n\t_chatProcess->info = info;\n\t_chatProcess->start = std::move(start);\n\t_chatProcess->fileProgress = std::move(progress);\n\t_chatProcess->handleSlice = std::move(slice);\n\t_chatProcess->done = std::move(done);\n\n\trequestMessagesCount(0);\n}\n\nvoid ApiWrap::requestMessagesCount(int localSplitIndex) {\n\tExpects(_chatProcess != nullptr);\n\tExpects(localSplitIndex < _chatProcess->info.splits.size());\n\n\trequestChatMessages(\n\t\t_chatProcess->info.splits[localSplitIndex],\n\t\t0, // offset_id\n\t\t0, // add_offset\n\t\t1, // limit\n\t\t[=](const MTPmessages_Messages &result) {\n\t\tExpects(_chatProcess != nullptr);\n\n\t\tconst auto count = result.match(\n\t\t\t[](const MTPDmessages_messages &data) {\n\t\t\treturn int(data.vmessages().v.size());\n\t\t}, [](const MTPDmessages_messagesSlice &data) {\n\t\t\treturn data.vcount().v;\n\t\t}, [](const MTPDmessages_channelMessages &data) {\n\t\t\treturn data.vcount().v;\n\t\t}, [](const MTPDmessages_messagesNotModified &data) {\n\t\t\treturn -1;\n\t\t});\n\t\tif (count < 0) {\n\t\t\terror(\"Unexpected messagesNotModified received.\");\n\t\t\treturn;\n\t\t}\n\t\tconst auto skipSplit = !Data::SingleMessageAfter(\n\t\t\tresult,\n\t\t\t_settings->singlePeerFrom);\n\t\tif (skipSplit) {\n\t\t\t// No messages from the requested range, skip this split.\n\t\t\tmessagesCountLoaded(localSplitIndex, 0);\n\t\t\treturn;\n\t\t}\n\t\tcheckFirstMessageDate(localSplitIndex, count);\n\t});\n}\n\nvoid ApiWrap::checkFirstMessageDate(int localSplitIndex, int count) {\n\tExpects(_chatProcess != nullptr);\n\tExpects(localSplitIndex < _chatProcess->info.splits.size());\n\n\tif (_settings->singlePeerTill <= 0) {\n\t\tmessagesCountLoaded(localSplitIndex, count);\n\t\treturn;\n\t}\n\n\t// Request first message in this split to check if its' date < till.\n\trequestChatMessages(\n\t\t_chatProcess->info.splits[localSplitIndex],\n\t\t1, // offset_id\n\t\t-1, // add_offset\n\t\t1, // limit\n\t\t[=](const MTPmessages_Messages &result) {\n\t\tExpects(_chatProcess != nullptr);\n\n\t\tconst auto skipSplit = !Data::SingleMessageBefore(\n\t\t\tresult,\n\t\t\t_settings->singlePeerTill);\n\t\tmessagesCountLoaded(localSplitIndex, skipSplit ? 0 : count);\n\t});\n}\n\nvoid ApiWrap::messagesCountLoaded(int localSplitIndex, int count) {\n\tExpects(_chatProcess != nullptr);\n\tExpects(localSplitIndex < _chatProcess->info.splits.size());\n\n\t_chatProcess->info.messagesCountPerSplit[localSplitIndex] = count;\n\tif (localSplitIndex + 1 < _chatProcess->info.splits.size()) {\n\t\trequestMessagesCount(localSplitIndex + 1);\n\t} else if (_chatProcess->start(_chatProcess->info)) {\n\t\trequestMessagesSlice();\n\t}\n}\n\nvoid ApiWrap::finishExport(FnMut<void()> done) {\n\tconst auto guard = gsl::finally([&] { _takeoutId = std::nullopt; });\n\n\tmainRequest(MTPaccount_FinishTakeoutSession(\n\t\tMTP_flags(MTPaccount_FinishTakeoutSession::Flag::f_success)\n\t)).done(std::move(done)).send();\n}\n\nvoid ApiWrap::skipFile(uint64 randomId) {\n\tif (!_fileProcess || _fileProcess->randomId != randomId) {\n\t\treturn;\n\t}\n\tLOG((\"Export Info: File skipped.\"));\n\tAssert(!_fileProcess->requests.empty());\n\tAssert(_fileProcess->requestId != 0);\n\t_mtp.request(base::take(_fileProcess->requestId)).cancel();\n\tbase::take(_fileProcess)->done(QString());\n}\n\nvoid ApiWrap::cancelExportFast() {\n\tif (_takeoutId.has_value()) {\n\t\tconst auto requestId = mainRequest(MTPaccount_FinishTakeoutSession(\n\t\t\tMTP_flags(0)\n\t\t)).send();\n\t\t_mtp.request(requestId).detach();\n\t}\n}\n\nvoid ApiWrap::requestSinglePeerDialog() {\n\tauto doneSinglePeer = [=](const auto &result) {\n\t\tappendSinglePeerDialogs(\n\t\t\tData::ParseDialogsInfo(_settings->singlePeer, result));\n\t};\n\tconst auto requestUser = [&](const MTPInputUser &data) {\n\t\tmainRequest(MTPusers_GetUsers(\n\t\t\tMTP_vector<MTPInputUser>(1, data)\n\t\t)).done(std::move(doneSinglePeer)).send();\n\t};\n\t_settings->singlePeer.match([&](const MTPDinputPeerUser &data) {\n\t\trequestUser(MTP_inputUser(data.vuser_id(), data.vaccess_hash()));\n\t}, [&](const MTPDinputPeerChat &data) {\n\t\tmainRequest(MTPmessages_GetChats(\n\t\t\tMTP_vector<MTPlong>(1, data.vchat_id())\n\t\t)).done(std::move(doneSinglePeer)).send();\n\t}, [&](const MTPDinputPeerChannel &data) {\n\t\tmainRequest(MTPchannels_GetChannels(\n\t\t\tMTP_vector<MTPInputChannel>(\n\t\t\t\t1,\n\t\t\t\tMTP_inputChannel(data.vchannel_id(), data.vaccess_hash()))\n\t\t)).done(std::move(doneSinglePeer)).send();\n\t}, [&](const MTPDinputPeerSelf &data) {\n\t\trequestUser(MTP_inputUserSelf());\n\t}, [&](const MTPDinputPeerUserFromMessage &data) {\n\t\tUnexpected(\"From message peer in ApiWrap::requestSinglePeerDialog.\");\n\t}, [&](const MTPDinputPeerChannelFromMessage &data) {\n\t\tUnexpected(\"From message peer in ApiWrap::requestSinglePeerDialog.\");\n\t}, [](const MTPDinputPeerEmpty &data) {\n\t\tUnexpected(\"Empty peer in ApiWrap::requestSinglePeerDialog.\");\n\t});\n}\n\nmtpRequestId ApiWrap::requestSinglePeerMigrated(\n\t\tconst Data::DialogInfo &info) {\n\tconst auto input = info.input.match([&](\n\t\tconst MTPDinputPeerChannel & data) {\n\t\treturn MTP_inputChannel(\n\t\t\tdata.vchannel_id(),\n\t\t\tdata.vaccess_hash());\n\t}, [](auto&&) -> MTPinputChannel {\n\t\tUnexpected(\"Peer type in a supergroup.\");\n\t});\n\treturn mainRequest(MTPchannels_GetFullChannel(\n\t\tinput\n\t)).done([=](const MTPmessages_ChatFull &result) {\n\t\tauto info = result.match([&](\n\t\t\t\tconst MTPDmessages_chatFull &data) {\n\t\t\tconst auto migratedChatId = data.vfull_chat().match([&](\n\t\t\t\t\tconst MTPDchannelFull &data) {\n\t\t\t\treturn data.vmigrated_from_chat_id().value_or_empty();\n\t\t\t}, [](auto &&other) -> BareId {\n\t\t\t\treturn 0;\n\t\t\t});\n\t\t\treturn migratedChatId\n\t\t\t\t? Data::ParseDialogsInfo(\n\t\t\t\t\tMTP_inputPeerChat(MTP_long(migratedChatId)),\n\t\t\t\t\tMTP_messages_chats(data.vchats()))\n\t\t\t\t: Data::DialogsInfo();\n\t\t});\n\t\tappendSinglePeerDialogs(std::move(info));\n\t}).send();\n}\n\nvoid ApiWrap::appendSinglePeerDialogs(Data::DialogsInfo &&info) {\n\tconst auto isSupergroupType = [](Data::DialogInfo::Type type) {\n\t\tusing Type = Data::DialogInfo::Type;\n\t\treturn (type == Type::PrivateSupergroup)\n\t\t\t|| (type == Type::PublicSupergroup);\n\t};\n\tconst auto isChannelType = [](Data::DialogInfo::Type type) {\n\t\tusing Type = Data::DialogInfo::Type;\n\t\treturn (type == Type::PrivateChannel)\n\t\t\t|| (type == Type::PublicChannel);\n\t};\n\n\tauto migratedRequestId = mtpRequestId(0);\n\tconst auto last = _dialogsProcess->splitIndexPlusOne - 1;\n\tfor (auto &info : info.chats) {\n\t\tif (isSupergroupType(info.type) && !migratedRequestId) {\n\t\t\tmigratedRequestId = requestSinglePeerMigrated(info);\n\t\t\tcontinue;\n\t\t} else if (isChannelType(info.type) || info.isMonoforum) {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (auto i = last; i != 0; --i) {\n\t\t\tinfo.splits.push_back(i - 1);\n\t\t\tinfo.messagesCountPerSplit.push_back(0);\n\t\t}\n\t}\n\n\tif (!migratedRequestId) {\n\t\t_dialogsProcess->processedCount += info.chats.size();\n\t}\n\tappendDialogsSlice(std::move(info));\n\n\tif (migratedRequestId\n\t\t|| !_dialogsProcess->progress(_dialogsProcess->processedCount)) {\n\t\treturn;\n\t}\n\tfinishDialogsList();\n}\n\nvoid ApiWrap::requestDialogsSlice() {\n\tExpects(_dialogsProcess != nullptr);\n\n\tif (_settings->onlySinglePeer()) {\n\t\trequestSinglePeerDialog();\n\t\treturn;\n\t}\n\n\tconst auto splitIndex = _dialogsProcess->splitIndexPlusOne - 1;\n\tconst auto hash = uint64(0);\n\tsplitRequest(splitIndex, MTPmessages_GetDialogs(\n\t\tMTP_flags(0),\n\t\tMTPint(), // folder_id\n\t\tMTP_int(_dialogsProcess->offsetDate),\n\t\tMTP_int(_dialogsProcess->offsetId),\n\t\t_dialogsProcess->offsetPeer,\n\t\tMTP_int(kChatsSliceLimit),\n\t\tMTP_long(hash)\n\t)).done([=](const MTPmessages_Dialogs &result) {\n\t\tif (result.type() == mtpc_messages_dialogsNotModified) {\n\t\t\terror(\"Unexpected dialogsNotModified received.\");\n\t\t\treturn;\n\t\t}\n\t\tauto finished = result.match(\n\t\t[](const MTPDmessages_dialogs &data) {\n\t\t\treturn true;\n\t\t}, [](const MTPDmessages_dialogsSlice &data) {\n\t\t\treturn data.vdialogs().v.isEmpty();\n\t\t}, [](const MTPDmessages_dialogsNotModified &data) {\n\t\t\treturn true;\n\t\t});\n\n\t\tauto info = Data::ParseDialogsInfo(result);\n\t\t_dialogsProcess->processedCount += info.chats.size();\n\t\tconst auto last = info.chats.empty()\n\t\t\t? Data::DialogInfo()\n\t\t\t: info.chats.back();\n\t\tappendDialogsSlice(std::move(info));\n\n\t\tif (!_dialogsProcess->progress(_dialogsProcess->processedCount)) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!finished && last.topMessageDate > 0) {\n\t\t\t_dialogsProcess->offsetId = last.topMessageId;\n\t\t\t_dialogsProcess->offsetDate = last.topMessageDate;\n\t\t\t_dialogsProcess->offsetPeer = last.input;\n\t\t} else if (!useOnlyLastSplit()\n\t\t\t&& --_dialogsProcess->splitIndexPlusOne > 0) {\n\t\t\t_dialogsProcess->offsetId = 0;\n\t\t\t_dialogsProcess->offsetDate = 0;\n\t\t\t_dialogsProcess->offsetPeer = MTP_inputPeerEmpty();\n\t\t} else {\n\t\t\trequestLeftChannelsIfNeeded();\n\t\t\treturn;\n\t\t}\n\t\trequestDialogsSlice();\n\t}).send();\n}\n\nvoid ApiWrap::appendDialogsSlice(Data::DialogsInfo &&info) {\n\tExpects(_dialogsProcess != nullptr);\n\tExpects(_dialogsProcess->splitIndexPlusOne <= _splits.size());\n\n\tappendChatsSlice(\n\t\t*_dialogsProcess,\n\t\t_dialogsProcess->info.chats,\n\t\tstd::move(info.chats),\n\t\t_dialogsProcess->splitIndexPlusOne - 1);\n}\n\nvoid ApiWrap::requestLeftChannelsIfNeeded() {\n\tif (_settings->types & Settings::Type::GroupsChannelsMask) {\n\t\trequestLeftChannelsList([=](int count) {\n\t\t\tExpects(_dialogsProcess != nullptr);\n\n\t\t\treturn _dialogsProcess->progress(\n\t\t\t\t_dialogsProcess->processedCount + count);\n\t\t}, [=](Data::DialogsInfo &&result) {\n\t\t\tExpects(_dialogsProcess != nullptr);\n\n\t\t\t_dialogsProcess->info.left = std::move(result.left);\n\t\t\tfinishDialogsList();\n\t\t});\n\t} else {\n\t\tfinishDialogsList();\n\t}\n}\n\nvoid ApiWrap::finishDialogsList() {\n\tExpects(_dialogsProcess != nullptr);\n\n\tconst auto process = base::take(_dialogsProcess);\n\tData::FinalizeDialogsInfo(process->info, *_settings);\n\tprocess->done(std::move(process->info));\n}\n\nvoid ApiWrap::requestLeftChannelsSliceGeneric(FnMut<void()> done) {\n\tExpects(_leftChannelsProcess != nullptr);\n\n\tmainRequest(MTPchannels_GetLeftChannels(\n\t\tMTP_int(_leftChannelsProcess->offset)\n\t)).done([=, done = std::move(done)](\n\t\t\tconst MTPmessages_Chats &result) mutable {\n\t\tExpects(_leftChannelsProcess != nullptr);\n\n\t\tappendLeftChannelsSlice(Data::ParseLeftChannelsInfo(result));\n\n\t\tconst auto process = _leftChannelsProcess.get();\n\t\tprocess->offset += result.match(\n\t\t[](const auto &data) {\n\t\t\treturn int(data.vchats().v.size());\n\t\t});\n\n\t\tprocess->fullCount = result.match(\n\t\t[](const MTPDmessages_chats &data) {\n\t\t\treturn int(data.vchats().v.size());\n\t\t}, [](const MTPDmessages_chatsSlice &data) {\n\t\t\treturn data.vcount().v;\n\t\t});\n\n\t\tprocess->finished = result.match(\n\t\t[](const MTPDmessages_chats &data) {\n\t\t\treturn true;\n\t\t}, [](const MTPDmessages_chatsSlice &data) {\n\t\t\treturn data.vchats().v.isEmpty();\n\t\t});\n\n\t\tif (process->progress) {\n\t\t\tif (!process->progress(process->info.left.size())) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tdone();\n\t}).send();\n}\n\nvoid ApiWrap::appendLeftChannelsSlice(Data::DialogsInfo &&info) {\n\tExpects(_leftChannelsProcess != nullptr);\n\tExpects(!_splits.empty());\n\n\tappendChatsSlice(\n\t\t*_leftChannelsProcess,\n\t\t_leftChannelsProcess->info.left,\n\t\tstd::move(info.left),\n\t\t_splits.size() - 1);\n}\n\nvoid ApiWrap::appendChatsSlice(\n\t\tChatsProcess &process,\n\t\tstd::vector<Data::DialogInfo> &to,\n\t\tstd::vector<Data::DialogInfo> &&from,\n\t\tint splitIndex) {\n\tExpects(_settings != nullptr);\n\n\tconst auto types = _settings->types;\n\tconst auto goodByTypes = [&](const Data::DialogInfo &info) {\n\t\treturn ((types & SettingsFromDialogsType(info.type)) != 0);\n\t};\n\tauto filtered = ranges::views::all(\n\t\tfrom\n\t) | ranges::views::filter([&](const Data::DialogInfo &info) {\n\t\tif (goodByTypes(info)) {\n\t\t\treturn true;\n\t\t} else if (info.migratedToChannelId\n\t\t\t&& (((types & Settings::Type::PublicGroups) != 0)\n\t\t\t\t|| ((types & Settings::Type::PrivateGroups) != 0))) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t});\n\tto.reserve(to.size() + from.size());\n\tfor (auto &info : filtered) {\n\t\tconst auto nextIndex = to.size();\n\t\tif (info.migratedToChannelId) {\n\t\t\tconst auto toPeerId = PeerId(info.migratedToChannelId);\n\t\t\tconst auto i = process.indexByPeer.find(toPeerId);\n\t\t\tif (i != process.indexByPeer.end()\n\t\t\t\t&& Data::AddMigrateFromSlice(\n\t\t\t\t\tto[i->second],\n\t\t\t\t\tinfo,\n\t\t\t\t\tsplitIndex,\n\t\t\t\t\tint(_splits.size()))) {\n\t\t\t\tcontinue;\n\t\t\t} else if (!goodByTypes(info)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tconst auto &[i, ok] = process.indexByPeer.emplace(\n\t\t\tinfo.peerId,\n\t\t\tnextIndex);\n\t\tif (ok) {\n\t\t\tto.push_back(std::move(info));\n\t\t}\n\t\tto[i->second].splits.push_back(splitIndex);\n\t\tto[i->second].messagesCountPerSplit.push_back(0);\n\t}\n}\n\nvoid ApiWrap::requestMessagesSlice() {\n\tExpects(_chatProcess != nullptr);\n\n\tconst auto count = _chatProcess->info.messagesCountPerSplit[\n\t\t_chatProcess->localSplitIndex];\n\tif (!count) {\n\t\tloadMessagesFiles({});\n\t\treturn;\n\t}\n\trequestChatMessages(\n\t\t_chatProcess->info.splits[_chatProcess->localSplitIndex],\n\t\t_chatProcess->largestIdPlusOne,\n\t\t-kMessagesSliceLimit,\n\t\tkMessagesSliceLimit,\n\t\t[=](const MTPmessages_Messages &result) {\n\t\tExpects(_chatProcess != nullptr);\n\n\t\tresult.match([&](const MTPDmessages_messagesNotModified &data) {\n\t\t\terror(\"Unexpected messagesNotModified received.\");\n\t\t}, [&](const auto &data) {\n\t\t\tif constexpr (MTPDmessages_messages::Is<decltype(data)>()) {\n\t\t\t\t_chatProcess->lastSlice = true;\n\t\t\t}\n\t\t\tloadMessagesFiles(Data::ParseMessagesSlice(\n\t\t\t\t_chatProcess->context,\n\t\t\t\tdata.vmessages(),\n\t\t\t\tdata.vusers(),\n\t\t\t\tdata.vchats(),\n\t\t\t\t_chatProcess->info.relativePath));\n\t\t});\n\t});\n}\n\nvoid ApiWrap::requestChatMessages(\n\t\tint splitIndex,\n\t\tint offsetId,\n\t\tint addOffset,\n\t\tint limit,\n\t\tFnMut<void(MTPmessages_Messages&&)> done) {\n\tExpects(_chatProcess != nullptr);\n\n\t_chatProcess->requestDone = std::move(done);\n\tconst auto doneHandler = [=](MTPmessages_Messages &&result) {\n\t\tExpects(_chatProcess != nullptr);\n\n\t\tbase::take(_chatProcess->requestDone)(std::move(result));\n\t};\n\tconst auto splitsCount = int(_splits.size());\n\tconst auto realPeerInput = (splitIndex >= 0)\n\t\t? _chatProcess->info.input\n\t\t: _chatProcess->info.migratedFromInput;\n\tconst auto outgoingInput = _chatProcess->info.isMonoforum\n\t\t? _chatProcess->info.monoforumBroadcastInput\n\t\t: MTP_inputPeerSelf();\n\tconst auto realSplitIndex = (splitIndex >= 0)\n\t\t? splitIndex\n\t\t: (splitsCount + splitIndex);\n\tif (_chatProcess->info.onlyMyMessages) {\n\t\tsplitRequest(realSplitIndex, MTPmessages_Search(\n\t\t\tMTP_flags(MTPmessages_Search::Flag::f_from_id),\n\t\t\trealPeerInput,\n\t\t\tMTP_string(), // query\n\t\t\toutgoingInput,\n\t\t\tMTPInputPeer(), // saved_peer_id\n\t\t\tMTPVector<MTPReaction>(), // saved_reaction\n\t\t\tMTPint(), // top_msg_id\n\t\t\tMTP_inputMessagesFilterEmpty(),\n\t\t\tMTP_int(0), // min_date\n\t\t\tMTP_int(0), // max_date\n\t\t\tMTP_int(offsetId),\n\t\t\tMTP_int(addOffset),\n\t\t\tMTP_int(limit),\n\t\t\tMTP_int(0), // max_id\n\t\t\tMTP_int(0), // min_id\n\t\t\tMTP_long(0) // hash\n\t\t)).done(doneHandler).send();\n\t} else {\n\t\tsplitRequest(realSplitIndex, MTPmessages_GetHistory(\n\t\t\trealPeerInput,\n\t\t\tMTP_int(offsetId),\n\t\t\tMTP_int(0), // offset_date\n\t\t\tMTP_int(addOffset),\n\t\t\tMTP_int(limit),\n\t\t\tMTP_int(0), // max_id\n\t\t\tMTP_int(0), // min_id\n\t\t\tMTP_long(0)  // hash\n\t\t)).fail([=](const MTP::Error &error) {\n\t\t\tExpects(_chatProcess != nullptr);\n\n\t\t\tif (error.type() == u\"CHANNEL_PRIVATE\"_q) {\n\t\t\t\tif (realPeerInput.type() == mtpc_inputPeerChannel\n\t\t\t\t\t&& !_chatProcess->info.onlyMyMessages) {\n\n\t\t\t\t\t// Perhaps we just left / were kicked from channel.\n\t\t\t\t\t// Just switch to only my messages.\n\t\t\t\t\t_chatProcess->info.onlyMyMessages = true;\n\t\t\t\t\trequestChatMessages(\n\t\t\t\t\t\tsplitIndex,\n\t\t\t\t\t\toffsetId,\n\t\t\t\t\t\taddOffset,\n\t\t\t\t\t\tlimit,\n\t\t\t\t\t\tbase::take(_chatProcess->requestDone));\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t}).done(doneHandler).send();\n\t}\n}\n\nvoid ApiWrap::loadMessagesFiles(Data::MessagesSlice &&slice) {\n\tExpects(_chatProcess != nullptr);\n\tExpects(!_chatProcess->slice.has_value());\n\n\tcollectMessagesCustomEmoji(slice);\n\n\tif (slice.list.empty()) {\n\t\t_chatProcess->lastSlice = true;\n\t}\n\t_chatProcess->slice = std::move(slice);\n\t_chatProcess->fileIndex = 0;\n\n\tresolveCustomEmoji();\n}\n\nvoid ApiWrap::collectMessagesCustomEmoji(const Data::MessagesSlice &slice) {\n\tfor (const auto &message : slice.list) {\n\t\tfor (const auto &part : message.text) {\n\t\t\tif (part.type == Data::TextPart::Type::CustomEmoji) {\n\t\t\t\tif (const auto id = part.additional.toULongLong()) {\n\t\t\t\t\tif (!_resolvedCustomEmoji.contains(id)) {\n\t\t\t\t\t\t_unresolvedCustomEmoji.emplace(id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor (const auto &reaction : message.reactions) {\n\t\t\tif (reaction.type == Data::Reaction::Type::CustomEmoji) {\n\t\t\t\tif (const auto id = reaction.documentId.toULongLong()) {\n\t\t\t\t\tif (!_resolvedCustomEmoji.contains(id)) {\n\t\t\t\t\t\t_unresolvedCustomEmoji.emplace(id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ApiWrap::resolveCustomEmoji() {\n\tif (_unresolvedCustomEmoji.empty()) {\n\t\tloadNextMessageFile();\n\t\treturn;\n\t}\n\tconst auto count = std::min(\n\t\tint(_unresolvedCustomEmoji.size()),\n\t\tkMaxEmojiPerRequest);\n\tauto v = QVector<MTPlong>();\n\tv.reserve(count);\n\tconst auto till = end(_unresolvedCustomEmoji);\n\tconst auto from = end(_unresolvedCustomEmoji) - count;\n\tfor (auto i = from; i != till; ++i) {\n\t\tv.push_back(MTP_long(*i));\n\t}\n\t_unresolvedCustomEmoji.erase(from, till);\n\tconst auto finalize = [=] {\n\t\tfor (const auto &id : v) {\n\t\t\tif (_resolvedCustomEmoji.contains(id.v)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t_resolvedCustomEmoji.emplace(\n\t\t\t\tid.v,\n\t\t\t\tData::Document{\n\t\t\t\t\t.file = {\n\t\t\t\t\t\t.skipReason = Data::File::SkipReason::Unavailable,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t}\n\t\tresolveCustomEmoji();\n\t};\n\tmainRequest(MTPmessages_GetCustomEmojiDocuments(\n\t\tMTP_vector<MTPlong>(v)\n\t)).fail([=](const MTP::Error &error) {\n\t\tLOG((\"Export Error: Failed to get documents for emoji.\"));\n\t\tfinalize();\n\t\treturn true;\n\t}).done([=](const MTPVector<MTPDocument> &result) {\n\t\tfor (const auto &entry : result.v) {\n\t\t\tauto document = Data::ParseDocument(\n\t\t\t\t_chatProcess->context,\n\t\t\t\tentry,\n\t\t\t\t_chatProcess->info.relativePath,\n\t\t\t\tTimeId());\n\t\t\t_resolvedCustomEmoji.emplace(document.id, std::move(document));\n\t\t}\n\t\tfinalize();\n\t}).send();\n}\n\nData::Message *ApiWrap::currentFileMessage() const {\n\tExpects(_chatProcess != nullptr);\n\tExpects(_chatProcess->slice.has_value());\n\n\treturn &_chatProcess->slice->list[_chatProcess->fileIndex];\n}\n\nData::FileOrigin ApiWrap::currentFileMessageOrigin() const {\n\tExpects(_chatProcess != nullptr);\n\tExpects(_chatProcess->slice.has_value());\n\n\tconst auto splitIndex = _chatProcess->info.splits[\n\t\t_chatProcess->localSplitIndex];\n\tauto result = Data::FileOrigin();\n\tresult.messageId = currentFileMessage()->id;\n\tresult.split = (splitIndex >= 0)\n\t\t? splitIndex\n\t\t: (int(_splits.size()) + splitIndex);\n\tresult.peer = (splitIndex >= 0)\n\t\t? _chatProcess->info.input\n\t\t: _chatProcess->info.migratedFromInput;\n\treturn result;\n}\n\nstd::optional<QByteArray> ApiWrap::getCustomEmoji(QByteArray &data) {\n\tif (const auto id = data.toULongLong()) {\n\t\tconst auto i = _resolvedCustomEmoji.find(id);\n\t\tif (i == end(_resolvedCustomEmoji)) {\n\t\t\treturn Data::TextPart::UnavailableEmoji();\n\t\t}\n\t\tauto &file = i->second.file;\n\t\tconst auto fileProgress = [=](FileProgress value) {\n\t\t\tif (_chatProcess) {\n\t\t\t\treturn loadMessageEmojiProgress(value);\n\t\t\t} else if (_topicProcess) {\n\t\t\t\treturn loadTopicEmojiProgress(value);\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\t\tconst auto ready = processFileLoad(\n\t\t\tfile,\n\t\t\t{ .customEmojiId = id },\n\t\t\tfileProgress,\n\t\t\t[=](const QString &path) { loadCustomEmojiDone(id, path); });\n\t\tif (!ready) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\tusing SkipReason = Data::File::SkipReason;\n\t\tif (file.skipReason == SkipReason::Unavailable) {\n\t\t\treturn Data::TextPart::UnavailableEmoji();\n\t\t} else if (file.skipReason == SkipReason::FileType\n\t\t\t|| file.skipReason == SkipReason::FileSize) {\n\t\t\treturn QByteArray();\n\t\t} else {\n\t\t\treturn file.relativePath.toUtf8();\n\t\t}\n\t}\n\treturn data;\n}\n\nbool ApiWrap::messageCustomEmojiReady(Data::Message &message) {\n\tfor (auto &part : message.text) {\n\t\tif (part.type == Data::TextPart::Type::CustomEmoji) {\n\t\t\tauto data = getCustomEmoji(part.additional);\n\t\t\tif (data.has_value()) {\n\t\t\t\tpart.additional = base::take(*data);\n\t\t\t} else {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\tfor (auto &reaction : message.reactions) {\n\t\tif (reaction.type == Data::Reaction::Type::CustomEmoji) {\n\t\t\tauto data = getCustomEmoji(reaction.documentId);\n\t\t\tif (data.has_value()) {\n\t\t\t\treaction.documentId = base::take(*data);\n\t\t\t} else {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid ApiWrap::loadNextMessageFile() {\n\tExpects(_chatProcess != nullptr);\n\tExpects(_chatProcess->slice.has_value());\n\n\tfor (auto &list = _chatProcess->slice->list\n\t\t; _chatProcess->fileIndex < list.size()\n\t\t; ++_chatProcess->fileIndex) {\n\t\tauto &message = list[_chatProcess->fileIndex];\n\t\tif (Data::SkipMessageByDate(message, *_settings)) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!messageCustomEmojiReady(message)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto fileProgress = [=](FileProgress value) {\n\t\t\treturn loadMessageFileProgress(value);\n\t\t};\n\t\tconst auto ready = processFileLoad(\n\t\t\tlist[_chatProcess->fileIndex].file(),\n\t\t\tcurrentFileMessageOrigin(),\n\t\t\tfileProgress,\n\t\t\t[=](const QString &path) { loadMessageFileDone(path); },\n\t\t\tcurrentFileMessage());\n\t\tif (!ready) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto thumbProgress = [=](FileProgress value) {\n\t\t\treturn loadMessageThumbProgress(value);\n\t\t};\n\t\tconst auto thumbReady = processFileLoad(\n\t\t\tlist[_chatProcess->fileIndex].thumb().file,\n\t\t\tcurrentFileMessageOrigin(),\n\t\t\tthumbProgress,\n\t\t\t[=](const QString &path) { loadMessageThumbDone(path); },\n\t\t\tcurrentFileMessage());\n\t\tif (!thumbReady) {\n\t\t\treturn;\n\t\t}\n\t}\n\tfinishMessagesSlice();\n}\n\nvoid ApiWrap::finishMessagesSlice() {\n\tExpects(_chatProcess != nullptr);\n\tExpects(_chatProcess->slice.has_value());\n\n\tauto slice = *base::take(_chatProcess->slice);\n\tif (!slice.list.empty()) {\n\t\t_chatProcess->largestIdPlusOne = slice.list.back().id + 1;\n\t\tconst auto splitIndex = _chatProcess->info.splits[\n\t\t\t_chatProcess->localSplitIndex];\n\t\tif (splitIndex < 0) {\n\t\t\tslice = AdjustMigrateMessageIds(std::move(slice));\n\t\t}\n\t\tif (!_chatProcess->handleSlice(std::move(slice))) {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (_chatProcess->lastSlice\n\t\t&& (++_chatProcess->localSplitIndex\n\t\t\t< _chatProcess->info.splits.size())) {\n\t\t_chatProcess->lastSlice = false;\n\t\t_chatProcess->largestIdPlusOne = 1;\n\t}\n\tif (!_chatProcess->lastSlice) {\n\t\trequestMessagesSlice();\n\t} else {\n\t\tfinishMessages();\n\t}\n}\n\nbool ApiWrap::loadMessageFileProgress(FileProgress progress) {\n\tExpects(_fileProcess != nullptr);\n\tExpects(_chatProcess != nullptr);\n\tExpects(_chatProcess->slice.has_value());\n\tExpects((_chatProcess->fileIndex >= 0)\n\t\t&& (_chatProcess->fileIndex < _chatProcess->slice->list.size()));\n\n\treturn _chatProcess->fileProgress(DownloadProgress{\n\t\t.randomId = _fileProcess->randomId,\n\t\t.path = _fileProcess->relativePath,\n\t\t.itemIndex = _chatProcess->fileIndex,\n\t\t.ready = progress.ready,\n\t\t.total = progress.total });\n}\n\nvoid ApiWrap::loadMessageFileDone(const QString &relativePath) {\n\tExpects(_chatProcess != nullptr);\n\tExpects(_chatProcess->slice.has_value());\n\tExpects((_chatProcess->fileIndex >= 0)\n\t\t&& (_chatProcess->fileIndex < _chatProcess->slice->list.size()));\n\n\tconst auto index = _chatProcess->fileIndex;\n\tauto &file = _chatProcess->slice->list[index].file();\n\tfile.relativePath = relativePath;\n\tif (relativePath.isEmpty()) {\n\t\tfile.skipReason = Data::File::SkipReason::Unavailable;\n\t}\n\tloadNextMessageFile();\n}\n\nbool ApiWrap::loadMessageThumbProgress(FileProgress progress) {\n\treturn loadMessageFileProgress(progress);\n}\n\nvoid ApiWrap::loadMessageThumbDone(const QString &relativePath) {\n\tExpects(_chatProcess != nullptr);\n\tExpects(_chatProcess->slice.has_value());\n\tExpects((_chatProcess->fileIndex >= 0)\n\t\t&& (_chatProcess->fileIndex < _chatProcess->slice->list.size()));\n\n\tconst auto index = _chatProcess->fileIndex;\n\tauto &file = _chatProcess->slice->list[index].thumb().file;\n\tfile.relativePath = relativePath;\n\tif (relativePath.isEmpty()) {\n\t\tfile.skipReason = Data::File::SkipReason::Unavailable;\n\t}\n\tloadNextMessageFile();\n}\n\nbool ApiWrap::loadMessageEmojiProgress(FileProgress progress) {\n\treturn loadMessageFileProgress(progress);\n}\n\nvoid ApiWrap::loadMessageEmojiDone(uint64 id, const QString &relativePath) {\n\tconst auto i = _resolvedCustomEmoji.find(id);\n\tif (i != end(_resolvedCustomEmoji)) {\n\t\ti->second.file.relativePath = relativePath;\n\t\tif (relativePath.isEmpty()) {\n\t\t\ti->second.file.skipReason = Data::File::SkipReason::Unavailable;\n\t\t}\n\t}\n\tloadNextMessageFile();\n}\n\nbool ApiWrap::loadTopicEmojiProgress(FileProgress progress) {\n\tExpects(_fileProcess != nullptr);\n\tExpects(_topicProcess != nullptr);\n\tExpects(_topicProcess->slice.has_value());\n\tExpects((_topicProcess->fileIndex >= 0)\n\t\t&& (_topicProcess->fileIndex < _topicProcess->slice->list.size()));\n\n\treturn _topicProcess->fileProgress(DownloadProgress{\n\t\t.randomId = _fileProcess->randomId,\n\t\t.path = _fileProcess->relativePath,\n\t\t.itemIndex = _topicProcess->fileIndex,\n\t\t.ready = progress.ready,\n\t\t.total = progress.total });\n}\n\nvoid ApiWrap::loadCustomEmojiDone(uint64 id, const QString &relativePath) {\n\tconst auto i = _resolvedCustomEmoji.find(id);\n\tif (i != end(_resolvedCustomEmoji)) {\n\t\ti->second.file.relativePath = relativePath;\n\t\tif (relativePath.isEmpty()) {\n\t\t\ti->second.file.skipReason = Data::File::SkipReason::Unavailable;\n\t\t}\n\t}\n\tif (_chatProcess) {\n\t\tloadNextMessageFile();\n\t} else if (_topicProcess) {\n\t\tloadNextTopicMessageFile();\n\t}\n}\n\nvoid ApiWrap::finishMessages() {\n\tExpects(_chatProcess != nullptr);\n\tExpects(!_chatProcess->slice.has_value());\n\n\tconst auto process = base::take(_chatProcess);\n\tprocess->done();\n}\n\nvoid ApiWrap::requestTopicMessages(\n\t\tPeerId peerId,\n\t\tMTPInputPeer inputPeer,\n\t\tint32 topicRootId,\n\t\tFnMut<bool(int count)> start,\n\t\tFn<bool(DownloadProgress)> progress,\n\t\tFn<bool(Data::MessagesSlice&&)> slice,\n\t\tFnMut<void()> done) {\n\tExpects(_topicProcess == nullptr);\n\tExpects(_selfId.has_value());\n\n\t_topicProcess = std::make_unique<TopicProcess>();\n\t_topicProcess->context.selfPeerId = peerFromUser(*_selfId);\n\t_topicProcess->peerId = peerId;\n\t_topicProcess->inputPeer = inputPeer;\n\t_topicProcess->topicRootId = topicRootId;\n\t_topicProcess->relativePath = \"chats/chat_\"\n\t\t+ QString::number(peerId.value)\n\t\t+ \"/topic_\"\n\t\t+ QString::number(topicRootId)\n\t\t+ \"/\";\n\t_topicProcess->start = std::move(start);\n\t_topicProcess->fileProgress = std::move(progress);\n\t_topicProcess->handleSlice = std::move(slice);\n\t_topicProcess->done = std::move(done);\n\n\tmainRequest(MTPchannels_GetMessages(\n\t\tMTP_inputChannel(\n\t\t\tinputPeer.c_inputPeerChannel().vchannel_id(),\n\t\t\tinputPeer.c_inputPeerChannel().vaccess_hash()),\n\t\tMTP_vector<MTPInputMessage>(\n\t\t\t1,\n\t\t\tMTP_inputMessageID(MTP_int(topicRootId)))\n\t)).done([=](const MTPmessages_Messages &rootResult) {\n\t\tExpects(_topicProcess != nullptr);\n\n\t\tauto rootSlice = rootResult.match([&](\n\t\t\t\tconst MTPDmessages_messagesNotModified &) {\n\t\t\treturn Data::MessagesSlice();\n\t\t}, [&](const auto &data) {\n\t\t\treturn Data::ParseMessagesSlice(\n\t\t\t\t_topicProcess->context,\n\t\t\t\tdata.vmessages(),\n\t\t\t\tdata.vusers(),\n\t\t\t\tdata.vchats(),\n\t\t\t\t_topicProcess->relativePath);\n\t\t});\n\n\t\tauto rootSlicePtr = std::make_shared<Data::MessagesSlice>(\n\t\t\tstd::move(rootSlice));\n\n\t\trequestTopicReplies(\n\t\t\t0,\n\t\t\t0,\n\t\t\tkMessagesSliceLimit,\n\t\t\t[=](const MTPmessages_Messages &result) {\n\t\t\t\tExpects(_topicProcess != nullptr);\n\n\t\t\t\tconst auto count = result.match(\n\t\t\t\t\t[](const MTPDmessages_messages &data) {\n\t\t\t\t\treturn int(data.vmessages().v.size());\n\t\t\t\t}, [](const MTPDmessages_messagesSlice &data) {\n\t\t\t\t\treturn data.vcount().v;\n\t\t\t\t}, [](const MTPDmessages_channelMessages &data) {\n\t\t\t\t\treturn data.vcount().v;\n\t\t\t\t}, [](const MTPDmessages_messagesNotModified &data) {\n\t\t\t\t\treturn -1;\n\t\t\t\t});\n\t\t\t\tif (count < 0) {\n\t\t\t\t\terror(\"Unexpected messagesNotModified received.\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t_topicProcess->totalCount = count;\n\t\t\t\tif (!_topicProcess->start(count)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (!rootSlicePtr->list.empty()) {\n\t\t\t\t\tcollectMessagesCustomEmoji(*rootSlicePtr);\n\t\t\t\t\t_topicProcess->slice = std::move(*rootSlicePtr);\n\t\t\t\t\t_topicProcess->fileIndex = 0;\n\t\t\t\t\tresolveTopicCustomEmoji();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\trequestTopicMessagesSlice();\n\t\t\t});\n\t}).send();\n}\n\nvoid ApiWrap::requestTopicMessagesSlice() {\n\tExpects(_topicProcess != nullptr);\n\n\tconst auto offsetId = (_topicProcess->offsetId == 0)\n\t\t? 1\n\t\t: (_topicProcess->offsetId + 1);\n\trequestTopicReplies(\n\t\toffsetId,\n\t\t-kMessagesSliceLimit,\n\t\tkMessagesSliceLimit,\n\t\t[=](const MTPmessages_Messages &result) {\n\t\t\tExpects(_topicProcess != nullptr);\n\n\t\t\tresult.match([&](const MTPDmessages_messagesNotModified &data) {\n\t\t\t\terror(\"Unexpected messagesNotModified received.\");\n\t\t\t}, [&](const auto &data) {\n\t\t\t\tif constexpr (MTPDmessages_messages::Is<decltype(data)>()) {\n\t\t\t\t\t_topicProcess->lastSlice = true;\n\t\t\t\t}\n\t\t\t\tauto slice = Data::ParseMessagesSlice(\n\t\t\t\t\t_topicProcess->context,\n\t\t\t\t\tdata.vmessages(),\n\t\t\t\t\tdata.vusers(),\n\t\t\t\t\tdata.vchats(),\n\t\t\t\t\t_topicProcess->relativePath);\n\t\t\t\tif (slice.list.empty()) {\n\t\t\t\t\t_topicProcess->lastSlice = true;\n\t\t\t\t}\n\t\t\t\tloadTopicMessagesFiles(std::move(slice));\n\t\t\t});\n\t\t});\n}\n\nvoid ApiWrap::requestTopicReplies(\n\t\tint offsetId,\n\t\tint addOffset,\n\t\tint limit,\n\t\tFnMut<void(MTPmessages_Messages&&)> done) {\n\tExpects(_topicProcess != nullptr);\n\n\t_topicProcess->requestDone = std::move(done);\n\tconst auto doneHandler = [=](MTPmessages_Messages &&result) {\n\t\tExpects(_topicProcess != nullptr);\n\t\tbase::take(_topicProcess->requestDone)(std::move(result));\n\t};\n\n\tmainRequest(MTPmessages_GetReplies(\n\t\t_topicProcess->inputPeer,\n\t\tMTP_int(_topicProcess->topicRootId),\n\t\tMTP_int(offsetId),\n\t\tMTP_int(0),\n\t\tMTP_int(addOffset),\n\t\tMTP_int(limit),\n\t\tMTP_int(0),\n\t\tMTP_int(0),\n\t\tMTP_long(0)\n\t)).done(doneHandler).send();\n}\n\nvoid ApiWrap::loadTopicMessagesFiles(Data::MessagesSlice &&slice) {\n\tExpects(_topicProcess != nullptr);\n\tExpects(!_topicProcess->slice.has_value());\n\n\tcollectMessagesCustomEmoji(slice);\n\n\tif (slice.list.empty()) {\n\t\t_topicProcess->lastSlice = true;\n\t}\n\t_topicProcess->slice = std::move(slice);\n\t_topicProcess->fileIndex = 0;\n\n\tresolveTopicCustomEmoji();\n}\n\nvoid ApiWrap::resolveTopicCustomEmoji() {\n\tif (_unresolvedCustomEmoji.empty()) {\n\t\tloadNextTopicMessageFile();\n\t\treturn;\n\t}\n\tconst auto count = std::min(\n\t\tint(_unresolvedCustomEmoji.size()),\n\t\tkMaxEmojiPerRequest);\n\tauto v = QVector<MTPlong>();\n\tv.reserve(count);\n\tconst auto till = end(_unresolvedCustomEmoji);\n\tconst auto from = end(_unresolvedCustomEmoji) - count;\n\tfor (auto i = from; i != till; ++i) {\n\t\tv.push_back(MTP_long(*i));\n\t}\n\t_unresolvedCustomEmoji.erase(from, till);\n\tconst auto finalize = [=] {\n\t\tfor (const auto &id : v) {\n\t\t\tif (_resolvedCustomEmoji.contains(id.v)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t_resolvedCustomEmoji.emplace(\n\t\t\t\tid.v,\n\t\t\t\tData::Document{\n\t\t\t\t\t.file = {\n\t\t\t\t\t\t.skipReason = Data::File::SkipReason::Unavailable,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t}\n\t\tresolveTopicCustomEmoji();\n\t};\n\tmainRequest(MTPmessages_GetCustomEmojiDocuments(\n\t\tMTP_vector<MTPlong>(v)\n\t)).fail([=](const MTP::Error &error) {\n\t\tLOG((\"Export Error: Failed to get documents for emoji.\"));\n\t\tfinalize();\n\t\treturn true;\n\t}).done([=](const MTPVector<MTPDocument> &result) {\n\t\tfor (const auto &entry : result.v) {\n\t\t\tauto document = Data::ParseDocument(\n\t\t\t\t_topicProcess->context,\n\t\t\t\tentry,\n\t\t\t\t_topicProcess->relativePath,\n\t\t\t\tTimeId());\n\t\t\t_resolvedCustomEmoji.emplace(document.id, std::move(document));\n\t\t}\n\t\tfinalize();\n\t}).send();\n}\n\nvoid ApiWrap::loadNextTopicMessageFile() {\n\tExpects(_topicProcess != nullptr);\n\tExpects(_topicProcess->slice.has_value());\n\n\tconst auto makeProgress = [=](FileProgress progress) {\n\t\treturn _topicProcess->fileProgress(DownloadProgress{\n\t\t\t.randomId = _fileProcess->randomId,\n\t\t\t.path = _fileProcess->relativePath,\n\t\t\t.itemIndex = _topicProcess->fileIndex,\n\t\t\t.ready = progress.ready,\n\t\t\t.total = progress.total,\n\t\t});\n\t};\n\tfor (auto &list = _topicProcess->slice->list\n\t\t; _topicProcess->fileIndex < list.size()\n\t\t; ++_topicProcess->fileIndex) {\n\t\tauto &message = list[_topicProcess->fileIndex];\n\t\tif (!messageCustomEmojiReady(message)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto origin = Data::FileOrigin{\n\t\t\t.peer = _topicProcess->inputPeer,\n\t\t\t.messageId = message.id\n\t\t};\n\t\tconst auto ready = processFileLoad(\n\t\t\tmessage.file(),\n\t\t\torigin,\n\t\t\tmakeProgress,\n\t\t\t[=, &message](const QString &path) {\n\t\t\t\tloadTopicMessageFileOrThumbDone(message.file(), path);\n\t\t\t},\n\t\t\t&message);\n\t\tif (!ready) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto thumbReady = processFileLoad(\n\t\t\tmessage.thumb().file,\n\t\t\torigin,\n\t\t\tmakeProgress,\n\t\t\t[=, &message](const QString &path) {\n\t\t\t\tloadTopicMessageFileOrThumbDone(message.thumb().file, path);\n\t\t\t},\n\t\t\t&message);\n\t\tif (!thumbReady) {\n\t\t\treturn;\n\t\t}\n\t}\n\tfinishTopicMessagesSlice();\n}\n\nvoid ApiWrap::finishTopicMessagesSlice() {\n\tExpects(_topicProcess != nullptr);\n\tExpects(_topicProcess->slice.has_value());\n\n\tauto slice = *base::take(_topicProcess->slice);\n\tif (!slice.list.empty()) {\n\t\t_topicProcess->offsetId = slice.list.back().id;\n\t\t_topicProcess->processedCount += slice.list.size();\n\t\tif (!_topicProcess->handleSlice(std::move(slice))) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tconst auto reachedTotal = _topicProcess->totalCount > 0\n\t\t&& _topicProcess->processedCount >= _topicProcess->totalCount;\n\n\tif (!_topicProcess->lastSlice && !reachedTotal) {\n\t\trequestTopicMessagesSlice();\n\t} else {\n\t\tfinishTopicMessages();\n\t}\n}\n\nvoid ApiWrap::loadTopicMessageFileOrThumbDone(\n\t\tData::File &file,\n\t\tconst QString &relativePath) {\n\tExpects(_topicProcess != nullptr);\n\tExpects(_topicProcess->slice.has_value());\n\tExpects((_topicProcess->fileIndex >= 0)\n\t\t&& (_topicProcess->fileIndex < _topicProcess->slice->list.size()));\n\n\tfile.relativePath = relativePath;\n\tif (relativePath.isEmpty()) {\n\t\tfile.skipReason = Data::File::SkipReason::Unavailable;\n\t}\n\tloadNextTopicMessageFile();\n}\n\nvoid ApiWrap::finishTopicMessages() {\n\tExpects(_topicProcess != nullptr);\n\tExpects(!_topicProcess->slice.has_value());\n\n\tconst auto process = base::take(_topicProcess);\n\tprocess->done();\n}\n\nbool ApiWrap::processFileLoad(\n\t\tData::File &file,\n\t\tconst Data::FileOrigin &origin,\n\t\tFn<bool(FileProgress)> progress,\n\t\tFnMut<void(QString)> done,\n\t\tData::Message *message,\n\t\tData::Story *story) {\n\tusing SkipReason = Data::File::SkipReason;\n\n\tif (!file.relativePath.isEmpty()\n\t\t|| file.skipReason != SkipReason::None) {\n\t\treturn true;\n\t} else if (!file.location && file.content.isEmpty()) {\n\t\tfile.skipReason = SkipReason::Unavailable;\n\t\treturn true;\n\t} else if (writePreloadedFile(file, origin)) {\n\t\treturn !file.relativePath.isEmpty();\n\t}\n\n\tusing Type = MediaSettings::Type;\n\tconst auto media = message\n\t\t? &message->media\n\t\t: story\n\t\t? &story->media\n\t\t: nullptr;\n\tconst auto type = media ? v::match(media->content, [&](\n\t\t\tconst Data::Document &data) {\n\t\tif (data.isSticker) {\n\t\t\treturn Type::Sticker;\n\t\t} else if (data.isVideoMessage) {\n\t\t\treturn Type::VideoMessage;\n\t\t} else if (data.isVoiceMessage) {\n\t\t\treturn Type::VoiceMessage;\n\t\t} else if (data.isAnimated) {\n\t\t\treturn Type::GIF;\n\t\t} else if (data.isVideoFile) {\n\t\t\treturn Type::Video;\n\t\t} else {\n\t\t\treturn Type::File;\n\t\t}\n\t}, [](const auto &data) {\n\t\treturn Type::Photo;\n\t}) : Type(0);\n\n\tconst auto fullSize = message\n\t\t? message->file().size\n\t\t: story\n\t\t? story->file().size\n\t\t: file.size;\n\tif (message && Data::SkipMessageByDate(*message, *_settings)) {\n\t\tfile.skipReason = SkipReason::DateLimits;\n\t\treturn true;\n\t} else if (!story && (_settings->media.types & type) != type) {\n\t\tfile.skipReason = SkipReason::FileType;\n\t\treturn true;\n\t} else if (!story && fullSize > _settings->media.sizeLimit) {\n\t\t// Don't load thumbs for large files that we skip.\n\t\tfile.skipReason = SkipReason::FileSize;\n\t\treturn true;\n\t}\n\tloadFile(file, origin, std::move(progress), std::move(done));\n\treturn false;\n}\n\nbool ApiWrap::writePreloadedFile(\n\t\tData::File &file,\n\t\tconst Data::FileOrigin &origin) {\n\tExpects(_settings != nullptr);\n\n\tusing namespace Output;\n\n\tif (const auto path = _fileCache->find(file.location)) {\n\t\tfile.relativePath = *path;\n\t\treturn true;\n\t} else if (!file.content.isEmpty()) {\n\t\tconst auto process = prepareFileProcess(file, origin);\n\t\tif (const auto result = process->file.writeBlock(file.content)) {\n\t\t\tfile.relativePath = process->relativePath;\n\t\t\t_fileCache->save(file.location, file.relativePath);\n\t\t} else {\n\t\t\tioError(result);\n\t\t}\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid ApiWrap::loadFile(\n\t\tconst Data::File &file,\n\t\tconst Data::FileOrigin &origin,\n\t\tFn<bool(FileProgress)> progress,\n\t\tFnMut<void(QString)> done) {\n\tExpects(_fileProcess == nullptr);\n\tExpects(file.location.dcId != 0\n\t\t|| file.location.data.type() == mtpc_inputTakeoutFileLocation);\n\n\t_fileProcess = prepareFileProcess(file, origin);\n\t_fileProcess->progress = std::move(progress);\n\t_fileProcess->done = std::move(done);\n\n\tif (_fileProcess->progress) {\n\t\tconst auto progress = FileProgress{\n\t\t\t_fileProcess->file.size(),\n\t\t\t_fileProcess->size\n\t\t};\n\t\tif (!_fileProcess->progress(progress)) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tloadFilePart();\n\n\tEnsures(_fileProcess->requestId != 0);\n}\n\nauto ApiWrap::prepareFileProcess(\n\tconst Data::File &file,\n\tconst Data::FileOrigin &origin) const\n-> std::unique_ptr<FileProcess> {\n\tExpects(_settings != nullptr);\n\n\tconst auto relativePath = Output::File::PrepareRelativePath(\n\t\t_settings->path,\n\t\tfile.suggestedPath);\n\tauto result = std::make_unique<FileProcess>(\n\t\t_settings->path + relativePath,\n\t\t_stats);\n\tresult->relativePath = relativePath;\n\tresult->location = file.location;\n\tresult->size = file.size;\n\tresult->origin = origin;\n\tresult->randomId = base::RandomValue<uint64>();\n\treturn result;\n}\n\nvoid ApiWrap::loadFilePart() {\n\tif (!_fileProcess\n\t\t|| _fileProcess->requestId\n\t\t|| _fileProcess->requests.size() >= kFileRequestsCount\n\t\t|| (_fileProcess->size > 0\n\t\t\t&& _fileProcess->offset >= _fileProcess->size)) {\n\t\treturn;\n\t}\n\n\tconst auto offset = _fileProcess->offset;\n\t_fileProcess->requests.push_back({ offset });\n\t_fileProcess->requestId = fileRequest(\n\t\t_fileProcess->location,\n\t\t_fileProcess->offset\n\t).done([=](const MTPupload_File &result) {\n\t\t_fileProcess->requestId = 0;\n\t\tfilePartDone(offset, result);\n\t}).send();\n\t_fileProcess->offset += kFileChunkSize;\n\n\tif (_fileProcess->size > 0\n\t\t&& _fileProcess->requests.size() < kFileRequestsCount) {\n\t\t// Only one request at a time supported right now.\n\t\t//const auto runner = _runner;\n\t\t//crl::on_main([=] {\n\t\t//\tQTimer::singleShot(kFileNextRequestDelay, [=] {\n\t\t//\t\trunner([=] {\n\t\t//\t\t\tloadFilePart();\n\t\t//\t\t});\n\t\t//\t});\n\t\t//});\n\t}\n}\n\nvoid ApiWrap::filePartDone(int64 offset, const MTPupload_File &result) {\n\tExpects(_fileProcess != nullptr);\n\tExpects(!_fileProcess->requests.empty());\n\n\tif (result.type() == mtpc_upload_fileCdnRedirect) {\n\t\terror(\"Cdn redirect is not supported.\");\n\t\treturn;\n\t}\n\tconst auto &data = result.c_upload_file();\n\tif (data.vbytes().v.isEmpty()) {\n\t\tif (_fileProcess->size > 0) {\n\t\t\terror(\"Empty bytes received in file part.\");\n\t\t\treturn;\n\t\t}\n\t\tconst auto result = _fileProcess->file.writeBlock({});\n\t\tif (!result) {\n\t\t\tioError(result);\n\t\t\treturn;\n\t\t}\n\t} else {\n\t\tusing Request = FileProcess::Request;\n\t\tauto &requests = _fileProcess->requests;\n\t\tconst auto i = ranges::find(\n\t\t\trequests,\n\t\t\toffset,\n\t\t\t[](const Request &request) { return request.offset; });\n\t\tAssert(i != end(requests));\n\n\t\ti->bytes = data.vbytes().v;\n\n\t\tauto &file = _fileProcess->file;\n\t\twhile (!requests.empty() && !requests.front().bytes.isEmpty()) {\n\t\t\tconst auto &bytes = requests.front().bytes;\n\t\t\tif (const auto result = file.writeBlock(bytes); !result) {\n\t\t\t\tioError(result);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\trequests.pop_front();\n\t\t}\n\n\t\tif (_fileProcess->progress) {\n\t\t\t_fileProcess->progress(FileProgress{\n\t\t\t\tfile.size(),\n\t\t\t\t_fileProcess->size });\n\t\t}\n\n\t\tif (!requests.empty()\n\t\t\t|| !_fileProcess->size\n\t\t\t|| _fileProcess->size > _fileProcess->offset) {\n\t\t\tloadFilePart();\n\t\t\treturn;\n\t\t}\n\t}\n\n\tauto process = base::take(_fileProcess);\n\tconst auto relativePath = process->relativePath;\n\t_fileCache->save(process->location, relativePath);\n\tprocess->done(process->relativePath);\n}\n\nvoid ApiWrap::filePartRefreshReference(int64 offset) {\n\tExpects(_fileProcess != nullptr);\n\tExpects(_fileProcess->requestId == 0);\n\n\tconst auto &origin = _fileProcess->origin;\n\tif (origin.storyId) {\n\t\t_fileProcess->requestId = mainRequest(MTPstories_GetStoriesByID(\n\t\t\tMTP_inputPeerSelf(),\n\t\t\tMTP_vector<MTPint>(1, MTP_int(origin.storyId))\n\t\t)).fail([=](const MTP::Error &error) {\n\t\t\t_fileProcess->requestId = 0;\n\t\t\tfilePartUnavailable();\n\t\t\treturn true;\n\t\t}).done([=](const MTPstories_Stories &result) {\n\t\t\t_fileProcess->requestId = 0;\n\t\t\tfilePartExtractReference(offset, result);\n\t\t}).send();\n\t\treturn;\n\t} else if (!origin.messageId) {\n\t\terror(\"FILE_REFERENCE error for non-message file.\");\n\t\treturn;\n\t}\n\tif (origin.peer.type() == mtpc_inputPeerChannel\n\t\t|| origin.peer.type() == mtpc_inputPeerChannelFromMessage) {\n\t\tconst auto channel = (origin.peer.type() == mtpc_inputPeerChannel)\n\t\t\t? MTP_inputChannel(\n\t\t\t\torigin.peer.c_inputPeerChannel().vchannel_id(),\n\t\t\t\torigin.peer.c_inputPeerChannel().vaccess_hash())\n\t\t\t: MTP_inputChannelFromMessage(\n\t\t\t\torigin.peer.c_inputPeerChannelFromMessage().vpeer(),\n\t\t\t\torigin.peer.c_inputPeerChannelFromMessage().vmsg_id(),\n\t\t\t\torigin.peer.c_inputPeerChannelFromMessage().vchannel_id());\n\t\t_fileProcess->requestId = mainRequest(MTPchannels_GetMessages(\n\t\t\tchannel,\n\t\t\tMTP_vector<MTPInputMessage>(\n\t\t\t\t1,\n\t\t\t\tMTP_inputMessageID(MTP_int(origin.messageId)))\n\t\t)).fail([=](const MTP::Error &error) {\n\t\t\t_fileProcess->requestId = 0;\n\t\t\tfilePartUnavailable();\n\t\t\treturn true;\n\t\t}).done([=](const MTPmessages_Messages &result) {\n\t\t\t_fileProcess->requestId = 0;\n\t\t\tfilePartExtractReference(offset, result);\n\t\t}).send();\n\t} else {\n\t\t_fileProcess->requestId = splitRequest(\n\t\t\torigin.split,\n\t\t\tMTPmessages_GetMessages(\n\t\t\t\tMTP_vector<MTPInputMessage>(\n\t\t\t\t\t1,\n\t\t\t\t\tMTP_inputMessageID(MTP_int(origin.messageId)))\n\t\t\t)\n\t\t).fail([=](const MTP::Error &error) {\n\t\t\t_fileProcess->requestId = 0;\n\t\t\tfilePartUnavailable();\n\t\t\treturn true;\n\t\t}).done([=](const MTPmessages_Messages &result) {\n\t\t\t_fileProcess->requestId = 0;\n\t\t\tfilePartExtractReference(offset, result);\n\t\t}).send();\n\t}\n}\n\nvoid ApiWrap::filePartExtractReference(\n\t\tint64 offset,\n\t\tconst MTPmessages_Messages &result) {\n\tExpects(_fileProcess != nullptr);\n\tExpects(_fileProcess->requestId == 0);\n\n\tresult.match([&](const MTPDmessages_messagesNotModified &data) {\n\t\terror(\"Unexpected messagesNotModified received.\");\n\t}, [&](const auto &data) {\n\t\tExpects(_selfId.has_value());\n\n\t\tauto context = Data::ParseMediaContext();\n\t\tcontext.selfPeerId = peerFromUser(*_selfId);\n\t\tconst auto messages = Data::ParseMessagesSlice(\n\t\t\tcontext,\n\t\t\tdata.vmessages(),\n\t\t\tdata.vusers(),\n\t\t\tdata.vchats(),\n\t\t\t_chatProcess->info.relativePath);\n\t\tfor (const auto &message : messages.list) {\n\t\t\tif (message.id == _fileProcess->origin.messageId) {\n\t\t\t\tconst auto refresh1 = Data::RefreshFileReference(\n\t\t\t\t\t_fileProcess->location,\n\t\t\t\t\tmessage.file().location);\n\t\t\t\tconst auto refresh2 = Data::RefreshFileReference(\n\t\t\t\t\t_fileProcess->location,\n\t\t\t\t\tmessage.thumb().file.location);\n\t\t\t\tif (refresh1 || refresh2) {\n\t\t\t\t\t_fileProcess->requestId = fileRequest(\n\t\t\t\t\t\t_fileProcess->location,\n\t\t\t\t\t\toffset\n\t\t\t\t\t).done([=](const MTPupload_File &result) {\n\t\t\t\t\t\t_fileProcess->requestId = 0;\n\t\t\t\t\t\tfilePartDone(offset, result);\n\t\t\t\t\t}).send();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfilePartUnavailable();\n\t});\n}\n\nvoid ApiWrap::filePartExtractReference(\n\t\tint64 offset,\n\t\tconst MTPstories_Stories &result) {\n\tExpects(_fileProcess != nullptr);\n\tExpects(_fileProcess->requestId == 0);\n\n\tconst auto stories = Data::ParseStoriesSlice(\n\t\tresult.data().vstories(),\n\t\t0);\n\tfor (const auto &story : stories.list) {\n\t\tif (story.id == _fileProcess->origin.storyId) {\n\t\t\tconst auto refresh1 = Data::RefreshFileReference(\n\t\t\t\t_fileProcess->location,\n\t\t\t\tstory.file().location);\n\t\t\tconst auto refresh2 = Data::RefreshFileReference(\n\t\t\t\t_fileProcess->location,\n\t\t\t\tstory.thumb().file.location);\n\t\t\tif (refresh1 || refresh2) {\n\t\t\t\t_fileProcess->requestId = fileRequest(\n\t\t\t\t\t_fileProcess->location,\n\t\t\t\t\toffset\n\t\t\t\t).done([=](const MTPupload_File &result) {\n\t\t\t\t\t_fileProcess->requestId = 0;\n\t\t\t\t\tfilePartDone(offset, result);\n\t\t\t\t}).send();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\tfilePartUnavailable();\n}\n\nvoid ApiWrap::filePartUnavailable() {\n\tExpects(_fileProcess != nullptr);\n\tExpects(!_fileProcess->requests.empty());\n\n\tLOG((\"Export Error: File unavailable.\"));\n\n\tbase::take(_fileProcess)->done(QString());\n}\n\nvoid ApiWrap::error(const MTP::Error &error) {\n\t_errors.fire_copy(error);\n}\n\nvoid ApiWrap::error(const QString &text) {\n\terror(MTP::Error(\n\t\tMTP_rpc_error(MTP_int(0), MTP_string(\"API_ERROR: \" + text))));\n}\n\nvoid ApiWrap::ioError(const Output::Result &result) {\n\t_ioErrors.fire_copy(result);\n}\n\nApiWrap::~ApiWrap() = default;\n\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/export_api_wrap.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/mtproto_concurrent_sender.h\"\n#include \"data/data_peer_id.h\"\n\nnamespace Export {\nnamespace Data {\nstruct File;\nstruct Chat;\nstruct Document;\nstruct FileLocation;\nstruct PersonalInfo;\nstruct UserpicsInfo;\nstruct UserpicsSlice;\nstruct StoriesInfo;\nstruct StoriesSlice;\nstruct ProfileMusicInfo;\nstruct ProfileMusicSlice;\nstruct ContactsList;\nstruct SessionsList;\nstruct DialogsInfo;\nstruct DialogInfo;\nstruct MessagesSlice;\nstruct Message;\nstruct Story;\nstruct FileOrigin;\n} // namespace Data\n\nnamespace Output {\nstruct Result;\nclass Stats;\n} // namespace Output\n\nstruct Settings;\n\nclass ApiWrap {\npublic:\n\tApiWrap(\n\t\tbase::weak_qptr<MTP::Instance> weak,\n\t\tFn<void(FnMut<void()>)> runner);\n\n\trpl::producer<MTP::Error> errors() const;\n\trpl::producer<Output::Result> ioErrors() const;\n\n\tstruct StartInfo {\n\t\tint userpicsCount = 0;\n\t\tint storiesCount = 0;\n\t\tint profileMusicCount = 0;\n\t\tint dialogsCount = 0;\n\t};\n\tvoid startExport(\n\t\tconst Settings &settings,\n\t\tOutput::Stats *stats,\n\t\tFnMut<void(StartInfo)> done);\n\n\tvoid requestDialogsList(\n\t\tFn<bool(int count)> progress,\n\t\tFnMut<void(Data::DialogsInfo&&)> done);\n\n\tvoid requestPersonalInfo(FnMut<void(Data::PersonalInfo&&)> done);\n\n\tvoid requestOtherData(\n\t\tconst QString &suggestedPath,\n\t\tFnMut<void(Data::File&&)> done);\n\n\tstruct DownloadProgress {\n\t\tuint64 randomId = 0;\n\t\tQString path;\n\t\tint itemIndex = 0;\n\t\tint64 ready = 0;\n\t\tint64 total = 0;\n\t};\n\tvoid requestUserpics(\n\t\tFnMut<bool(Data::UserpicsInfo&&)> start,\n\t\tFn<bool(DownloadProgress)> progress,\n\t\tFn<bool(Data::UserpicsSlice&&)> slice,\n\t\tFnMut<void()> finish);\n\n\tvoid requestStories(\n\t\tFnMut<bool(Data::StoriesInfo&&)> start,\n\t\tFn<bool(DownloadProgress)> progress,\n\t\tFn<bool(Data::StoriesSlice&&)> slice,\n\t\tFnMut<void()> finish);\n\n\tvoid requestProfileMusic(\n\t\tFnMut<bool(Data::ProfileMusicInfo&&)> start,\n\t\tFn<bool(DownloadProgress)> progress,\n\t\tFn<bool(Data::ProfileMusicSlice&&)> slice,\n\t\tFnMut<void()> finish);\n\n\tvoid requestContacts(FnMut<void(Data::ContactsList&&)> done);\n\n\tvoid requestSessions(FnMut<void(Data::SessionsList&&)> done);\n\n\tvoid requestMessages(\n\t\tconst Data::DialogInfo &info,\n\t\tFnMut<bool(const Data::DialogInfo &)> start,\n\t\tFn<bool(DownloadProgress)> progress,\n\t\tFn<bool(Data::MessagesSlice&&)> slice,\n\t\tFnMut<void()> done);\n\n\tvoid requestTopicMessages(\n\t\tPeerId peerId,\n\t\tMTPInputPeer inputPeer,\n\t\tint32 topicRootId,\n\t\tFnMut<bool(int count)> start,\n\t\tFn<bool(DownloadProgress)> progress,\n\t\tFn<bool(Data::MessagesSlice&&)> slice,\n\t\tFnMut<void()> done);\n\n\tvoid finishExport(FnMut<void()> done);\n\tvoid skipFile(uint64 randomId);\n\tvoid cancelExportFast();\n\n\t~ApiWrap();\n\nprivate:\n\tclass LoadedFileCache;\n\tstruct StartProcess;\n\tstruct ContactsProcess;\n\tstruct UserpicsProcess;\n\tstruct StoriesProcess;\n\tstruct ProfileMusicProcess;\n\tstruct OtherDataProcess;\n\tstruct FileProcess;\n\tstruct FileProgress;\n\tstruct ChatsProcess;\n\tstruct LeftChannelsProcess;\n\tstruct DialogsProcess;\n\tstruct AbstractMessagesProcess;\n\tstruct ChatProcess;\n\tstruct TopicProcess;\n\n\tvoid startMainSession(FnMut<void()> done);\n\tvoid sendNextStartRequest();\n\tvoid requestUserpicsCount();\n\tvoid requestStoriesCount();\n\tvoid requestProfileMusicCount();\n\tvoid requestSplitRanges();\n\tvoid requestDialogsCount();\n\tvoid requestLeftChannelsCount();\n\tvoid finishStartProcess();\n\n\tvoid requestTopPeersSlice();\n\n\tvoid handleUserpicsSlice(const MTPphotos_Photos &result);\n\tvoid loadUserpicsFiles(Data::UserpicsSlice &&slice);\n\tvoid loadNextUserpic();\n\tbool loadUserpicProgress(FileProgress value);\n\tvoid loadUserpicDone(const QString &relativePath);\n\tvoid finishUserpicsSlice();\n\tvoid finishUserpics();\n\n\tvoid handleStoriesSlice(const MTPstories_Stories &result);\n\tvoid loadStoriesFiles(Data::StoriesSlice &&slice);\n\tvoid loadNextStory();\n\tbool loadStoryProgress(FileProgress value);\n\tvoid loadStoryDone(const QString &relativePath);\n\tbool loadStoryThumbProgress(FileProgress value);\n\tvoid loadStoryThumbDone(const QString &relativePath);\n\tvoid finishStoriesSlice();\n\tvoid finishStories();\n\n\tvoid handleProfileMusicSlice(const MTPusers_SavedMusic &result);\n\tvoid loadProfileMusicFiles(Data::ProfileMusicSlice &&slice);\n\tvoid loadNextProfileMusic();\n\tbool loadProfileMusicProgress(FileProgress value);\n\tvoid loadProfileMusicDone(const QString &relativePath);\n\tbool loadProfileMusicThumbProgress(FileProgress value);\n\tvoid loadProfileMusicThumbDone(const QString &relativePath);\n\tvoid finishProfileMusicSlice();\n\tvoid finishProfileMusic();\n\n\tvoid otherDataDone(const QString &relativePath);\n\n\tbool useOnlyLastSplit() const;\n\n\tvoid requestDialogsSlice();\n\tvoid appendDialogsSlice(Data::DialogsInfo &&info);\n\tvoid finishDialogsList();\n\tvoid requestSinglePeerDialog();\n\tmtpRequestId requestSinglePeerMigrated(const Data::DialogInfo &info);\n\tvoid appendSinglePeerDialogs(Data::DialogsInfo &&info);\n\n\tvoid requestLeftChannelsIfNeeded();\n\tvoid requestLeftChannelsList(\n\t\tFn<bool(int count)> progress,\n\t\tFnMut<void(Data::DialogsInfo&&)> done);\n\tvoid requestLeftChannelsSliceGeneric(FnMut<void()> done);\n\tvoid requestLeftChannelsSlice();\n\tvoid appendLeftChannelsSlice(Data::DialogsInfo &&info);\n\n\tvoid appendChatsSlice(\n\t\tChatsProcess &process,\n\t\tstd::vector<Data::DialogInfo> &to,\n\t\tstd::vector<Data::DialogInfo> &&from,\n\t\tint splitIndex);\n\n\tvoid requestMessagesCount(int localSplitIndex);\n\tvoid checkFirstMessageDate(int localSplitIndex, int count);\n\tvoid messagesCountLoaded(int localSplitIndex, int count);\n\tvoid requestMessagesSlice();\n\tvoid requestChatMessages(\n\t\tint splitIndex,\n\t\tint offsetId,\n\t\tint addOffset,\n\t\tint limit,\n\t\tFnMut<void(MTPmessages_Messages&&)> done);\n\tvoid requestTopicMessagesSlice();\n\tvoid requestTopicReplies(\n\t\tint offsetId,\n\t\tint addOffset,\n\t\tint limit,\n\t\tFnMut<void(MTPmessages_Messages&&)> done);\n\tvoid collectMessagesCustomEmoji(const Data::MessagesSlice &slice);\n\tvoid resolveCustomEmoji();\n\tvoid loadMessagesFiles(Data::MessagesSlice &&slice);\n\tvoid loadNextMessageFile();\n\t[[nodiscard]] std::optional<QByteArray> getCustomEmoji(QByteArray &data);\n\tbool messageCustomEmojiReady(Data::Message &message);\n\tbool loadMessageFileProgress(FileProgress value);\n\tvoid loadMessageFileDone(const QString &relativePath);\n\tbool loadMessageThumbProgress(FileProgress value);\n\tvoid loadMessageThumbDone(const QString &relativePath);\n\tbool loadMessageEmojiProgress(FileProgress progress);\n\tvoid loadMessageEmojiDone(uint64 id, const QString &relativePath);\n\tvoid finishMessagesSlice();\n\tvoid finishMessages();\n\n\tvoid loadTopicMessagesFiles(Data::MessagesSlice &&slice);\n\tvoid resolveTopicCustomEmoji();\n\tvoid loadNextTopicMessageFile();\n\tbool loadTopicEmojiProgress(FileProgress progress);\n\tvoid loadCustomEmojiDone(uint64 id, const QString &relativePath);\n\tvoid loadTopicMessageFileOrThumbDone(\n\t\tData::File &file,\n\t\tconst QString &relativePath);\n\tvoid finishTopicMessagesSlice();\n\tvoid finishTopicMessages();\n\n\t[[nodiscard]] Data::Message *currentFileMessage() const;\n\t[[nodiscard]] Data::FileOrigin currentFileMessageOrigin() const;\n\n\tbool processFileLoad(\n\t\tData::File &file,\n\t\tconst Data::FileOrigin &origin,\n\t\tFn<bool(FileProgress)> progress,\n\t\tFnMut<void(QString)> done,\n\t\tData::Message *message = nullptr,\n\t\tData::Story *story = nullptr);\n\tstd::unique_ptr<FileProcess> prepareFileProcess(\n\t\tconst Data::File &file,\n\t\tconst Data::FileOrigin &origin) const;\n\tbool writePreloadedFile(\n\t\tData::File &file,\n\t\tconst Data::FileOrigin &origin);\n\tvoid loadFile(\n\t\tconst Data::File &file,\n\t\tconst Data::FileOrigin &origin,\n\t\tFn<bool(FileProgress)> progress,\n\t\tFnMut<void(QString)> done);\n\tvoid loadFilePart();\n\tvoid filePartDone(int64 offset, const MTPupload_File &result);\n\tvoid filePartUnavailable();\n\tvoid filePartRefreshReference(int64 offset);\n\tvoid filePartExtractReference(\n\t\tint64 offset,\n\t\tconst MTPmessages_Messages &result);\n\tvoid filePartExtractReference(\n\t\tint64 offset,\n\t\tconst MTPstories_Stories &result);\n\n\ttemplate <typename Request>\n\tclass RequestBuilder;\n\n\ttemplate <typename Request>\n\t[[nodiscard]] auto mainRequest(Request &&request);\n\n\ttemplate <typename Request>\n\t[[nodiscard]] auto splitRequest(int index, Request &&request);\n\n\t[[nodiscard]] auto fileRequest(\n\t\tconst Data::FileLocation &location,\n\t\tint64 offset);\n\n\tvoid error(const MTP::Error &error);\n\tvoid error(const QString &text);\n\tvoid ioError(const Output::Result &result);\n\n\tMTP::ConcurrentSender _mtp;\n\tstd::optional<uint64> _takeoutId;\n\tstd::optional<UserId> _selfId;\n\tOutput::Stats *_stats = nullptr;\n\n\tstd::unique_ptr<Settings> _settings;\n\tMTPInputUser _user = MTP_inputUserSelf();\n\n\tstd::unique_ptr<StartProcess> _startProcess;\n\tstd::unique_ptr<LoadedFileCache> _fileCache;\n\tstd::unique_ptr<ContactsProcess> _contactsProcess;\n\tstd::unique_ptr<UserpicsProcess> _userpicsProcess;\n\tstd::unique_ptr<StoriesProcess> _storiesProcess;\n\tstd::unique_ptr<ProfileMusicProcess> _profileMusicProcess;\n\tstd::unique_ptr<OtherDataProcess> _otherDataProcess;\n\tstd::unique_ptr<FileProcess> _fileProcess;\n\tstd::unique_ptr<LeftChannelsProcess> _leftChannelsProcess;\n\tstd::unique_ptr<DialogsProcess> _dialogsProcess;\n\tstd::unique_ptr<ChatProcess> _chatProcess;\n\tstd::unique_ptr<TopicProcess> _topicProcess;\n\tbase::flat_set<uint64> _unresolvedCustomEmoji;\n\tbase::flat_map<uint64, Data::Document> _resolvedCustomEmoji;\n\tQVector<MTPMessageRange> _splits;\n\n\trpl::event_stream<MTP::Error> _errors;\n\trpl::event_stream<Output::Result> _ioErrors;\n\n};\n\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/export_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/export_controller.h\"\n\n#include \"export/export_api_wrap.h\"\n#include \"export/export_settings.h\"\n#include \"export/data/export_data_types.h\"\n#include \"export/output/export_output_abstract.h\"\n#include \"export/output/export_output_result.h\"\n#include \"export/output/export_output_stats.h\"\n#include \"mtproto/mtp_instance.h\"\n\nnamespace Export {\nnamespace {\n\nconst auto kNullStateCallback = [](ProcessingState&) {};\n\nSettings NormalizeSettings(const Settings &settings) {\n\tif (!settings.onlySinglePeer()) {\n\t\treturn base::duplicate(settings);\n\t}\n\tauto result = base::duplicate(settings);\n\tresult.types = result.fullChats = Settings::Type::AnyChatsMask;\n\treturn result;\n}\n\n} // namespace\n\nclass ControllerObject {\npublic:\n\tControllerObject(\n\t\tcrl::weak_on_queue<ControllerObject> weak,\n\t\tQPointer<MTP::Instance> mtproto,\n\t\tconst MTPInputPeer &peer);\n\tControllerObject(\n\t\tcrl::weak_on_queue<ControllerObject> weak,\n\t\tQPointer<MTP::Instance> mtproto,\n\t\tconst MTPInputPeer &peer,\n\t\tint32 topicRootId,\n\t\tuint64 peerId,\n\t\tconst QString &topicTitle);\n\n\trpl::producer<State> state() const;\n\n\t// Password step.\n\t//void submitPassword(const QString &password);\n\t//void requestPasswordRecover();\n\t//rpl::producer<PasswordUpdate> passwordUpdate() const;\n\t//void reloadPasswordState();\n\t//void cancelUnconfirmedPassword();\n\n\t// Processing step.\n\tvoid startExport(\n\t\tconst Settings &settings,\n\t\tconst Environment &environment);\n\tvoid skipFile(uint64 randomId);\n\tvoid cancelExportFast();\n\nprivate:\n\tusing Step = ProcessingState::Step;\n\tusing DownloadProgress = ApiWrap::DownloadProgress;\n\n\t[[nodiscard]] bool stopped() const;\n\tvoid setState(State &&state);\n\tvoid ioError(const QString &path);\n\tbool ioCatchError(Output::Result result);\n\tvoid setFinishedState();\n\n\t//void requestPasswordState();\n\t//void passwordStateDone(const MTPaccount_Password &password);\n\n\tvoid fillExportSteps();\n\tvoid fillSubstepsInSteps(const ApiWrap::StartInfo &info);\n\tvoid exportNext();\n\tvoid initialize();\n\tvoid initialized(const ApiWrap::StartInfo &info);\n\tvoid collectDialogsList();\n\tvoid exportPersonalInfo();\n\tvoid exportUserpics();\n\tvoid exportStories();\n\tvoid exportProfileMusic();\n\tvoid exportContacts();\n\tvoid exportSessions();\n\tvoid exportOtherData();\n\tvoid exportDialogs();\n\tvoid exportNextDialog();\n\tvoid exportTopic();\n\n\ttemplate <typename Callback = const decltype(kNullStateCallback) &>\n\tProcessingState prepareState(\n\t\tStep step,\n\t\tCallback &&callback = kNullStateCallback) const;\n\tProcessingState stateInitializing() const;\n\tProcessingState stateDialogsList(int processed) const;\n\tProcessingState statePersonalInfo() const;\n\tProcessingState stateUserpics(const DownloadProgress &progress) const;\n\tProcessingState stateStories(const DownloadProgress &progress) const;\n\tProcessingState stateProfileMusic(const DownloadProgress &progress) const;\n\tProcessingState stateContacts() const;\n\tProcessingState stateSessions() const;\n\tProcessingState stateOtherData() const;\n\tProcessingState stateDialogs(const DownloadProgress &progress) const;\n\tProcessingState stateTopic(const DownloadProgress &progress) const;\n\tvoid fillMessagesState(\n\t\tProcessingState &result,\n\t\tconst Data::DialogsInfo &info,\n\t\tint index,\n\t\tconst DownloadProgress &progress) const;\n\n\tint substepsInStep(Step step) const;\n\n\tApiWrap _api;\n\tSettings _settings;\n\tEnvironment _environment;\n\n\tData::DialogsInfo _dialogsInfo;\n\tint _dialogIndex = -1;\n\n\tint _messagesWritten = 0;\n\tint _messagesCount = 0;\n\n\tint _userpicsWritten = 0;\n\tint _userpicsCount = 0;\n\n\tint _storiesWritten = 0;\n\tint _storiesCount = 0;\n\n\tint _profileMusicWritten = 0;\n\tint _profileMusicCount = 0;\n\n\t// rpl::variable<State> fails to compile in MSVC :(\n\tState _state;\n\trpl::event_stream<State> _stateChanges;\n\n\tOutput::Stats _stats;\n\n\tstd::vector<int> _substepsInStep;\n\tint _substepsTotal = 0;\n\tmutable int _substepsPassed = 0;\n\tmutable Step _lastProcessingStep = Step::Initializing;\n\n\tstd::unique_ptr<Output::AbstractWriter> _writer;\n\tstd::vector<Step> _steps;\n\tint _stepIndex = -1;\n\n\tint32 _topicRootId = 0;\n\tuint64 _topicPeerId = 0;\n\tQString _topicTitle;\n\n\trpl::lifetime _lifetime;\n\n};\n\nControllerObject::ControllerObject(\n\tcrl::weak_on_queue<ControllerObject> weak,\n\tQPointer<MTP::Instance> mtproto,\n\tconst MTPInputPeer &peer)\n: _api(mtproto, weak.runner())\n, _state(PasswordCheckState{}) {\n\t_api.errors(\n\t) | rpl::on_next([=](const MTP::Error &error) {\n\t\tsetState(ApiErrorState{ error });\n\t}, _lifetime);\n\n\t_api.ioErrors(\n\t) | rpl::on_next([=](const Output::Result &result) {\n\t\tioCatchError(result);\n\t}, _lifetime);\n\n\t//requestPasswordState();\n\tauto state = PasswordCheckState();\n\tstate.checked = false;\n\tstate.requesting = false;\n\tstate.singlePeer = peer;\n\tsetState(std::move(state));\n}\n\nControllerObject::ControllerObject(\n\tcrl::weak_on_queue<ControllerObject> weak,\n\tQPointer<MTP::Instance> mtproto,\n\tconst MTPInputPeer &peer,\n\tint32 topicRootId,\n\tuint64 peerId,\n\tconst QString &topicTitle)\n: _api(mtproto, weak.runner())\n, _state(PasswordCheckState{})\n, _topicRootId(topicRootId)\n, _topicPeerId(peerId)\n, _topicTitle(topicTitle) {\n\t_api.errors(\n\t) | rpl::on_next([=](const MTP::Error &error) {\n\t\tsetState(ApiErrorState{ error });\n\t}, _lifetime);\n\n\t_api.ioErrors(\n\t) | rpl::on_next([=](const Output::Result &result) {\n\t\tioCatchError(result);\n\t}, _lifetime);\n\n\t//requestPasswordState();\n\tauto state = PasswordCheckState();\n\tstate.checked = false;\n\tstate.requesting = false;\n\tstate.singlePeer = peer;\n\tsetState(std::move(state));\n}\n\nrpl::producer<State> ControllerObject::state() const {\n\treturn rpl::single(\n\t\t_state\n\t) | rpl::then(\n\t\t_stateChanges.events()\n\t) | rpl::filter([](const State &state) {\n\t\tconst auto password = std::get_if<PasswordCheckState>(&state);\n\t\treturn !password || !password->requesting;\n\t});\n}\n\nbool ControllerObject::stopped() const {\n\treturn v::is<CancelledState>(_state)\n\t\t|| v::is<ApiErrorState>(_state)\n\t\t|| v::is<OutputErrorState>(_state)\n\t\t|| v::is<FinishedState>(_state);\n}\n\nvoid ControllerObject::setState(State &&state) {\n\tif (stopped()) {\n\t\treturn;\n\t}\n\t_state = std::move(state);\n\t_stateChanges.fire_copy(_state);\n}\n\nvoid ControllerObject::ioError(const QString &path) {\n\tsetState(OutputErrorState{ path });\n}\n\nbool ControllerObject::ioCatchError(Output::Result result) {\n\tif (!result) {\n\t\tioError(result.path);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\n//void ControllerObject::submitPassword(const QString &password) {\n//\n//}\n//\n//void ControllerObject::requestPasswordRecover() {\n//\n//}\n//\n//rpl::producer<PasswordUpdate> ControllerObject::passwordUpdate() const {\n//\treturn nullptr;\n//}\n//\n//void ControllerObject::reloadPasswordState() {\n//\t//_mtp.request(base::take(_passwordRequestId)).cancel();\n//\trequestPasswordState();\n//}\n//\n//void ControllerObject::requestPasswordState() {\n//\tif (_passwordRequestId) {\n//\t\treturn;\n//\t}\n//\t//_passwordRequestId = _mtp.request(MTPaccount_GetPassword(\n//\t//)).done([=](const MTPaccount_Password &result) {\n//\t//\t_passwordRequestId = 0;\n//\t//\tpasswordStateDone(result);\n//\t//}).fail([=](const MTP::Error &error) {\n//\t//\tapiError(error);\n//\t//}).send();\n//}\n//\n//void ControllerObject::passwordStateDone(const MTPaccount_Password &result) {\n//\tauto state = PasswordCheckState();\n//\tstate.checked = false;\n//\tstate.requesting = false;\n//\tstate.hasPassword;\n//\tstate.hint;\n//\tstate.unconfirmedPattern;\n//\tsetState(std::move(state));\n//}\n//\n//void ControllerObject::cancelUnconfirmedPassword() {\n//\n//}\n\nvoid ControllerObject::startExport(\n\t\tconst Settings &settings,\n\t\tconst Environment &environment) {\n\tif (!_settings.path.isEmpty()) {\n\t\treturn;\n\t}\n\t_settings = NormalizeSettings(settings);\n\t_environment = environment;\n\t_settings.singleTopicRootId = _topicRootId;\n\t_settings.singleTopicPeerId = _topicPeerId;\n\n\t_settings.path = Output::NormalizePath(_settings);\n\t_writer = Output::CreateWriter(_settings.format);\n\tfillExportSteps();\n\texportNext();\n}\n\nvoid ControllerObject::skipFile(uint64 randomId) {\n\tif (stopped()) {\n\t\treturn;\n\t}\n\t_api.skipFile(randomId);\n}\n\nvoid ControllerObject::fillExportSteps() {\n\tusing Type = Settings::Type;\n\t_steps.push_back(Step::Initializing);\n\tif (_settings.onlySingleTopic()) {\n\t\t_steps.push_back(Step::Topic);\n\t\treturn;\n\t}\n\tif (_settings.types & Type::AnyChatsMask) {\n\t\t_steps.push_back(Step::DialogsList);\n\t}\n\tif (_settings.types & Type::PersonalInfo) {\n\t\t_steps.push_back(Step::PersonalInfo);\n\t}\n\tif (_settings.types & Type::Userpics) {\n\t\t_steps.push_back(Step::Userpics);\n\t}\n\tif (_settings.types & Type::Stories) {\n\t\t_steps.push_back(Step::Stories);\n\t}\n\tif (_settings.types & Type::ProfileMusic) {\n\t\t_steps.push_back(Step::ProfileMusic);\n\t}\n\tif (_settings.types & Type::Contacts) {\n\t\t_steps.push_back(Step::Contacts);\n\t}\n\tif (_settings.types & Type::Sessions) {\n\t\t_steps.push_back(Step::Sessions);\n\t}\n\tif (_settings.types & Type::OtherData) {\n\t\t_steps.push_back(Step::OtherData);\n\t}\n\tif (_settings.types & Type::AnyChatsMask) {\n\t\t_steps.push_back(Step::Dialogs);\n\t}\n}\n\nvoid ControllerObject::fillSubstepsInSteps(const ApiWrap::StartInfo &info) {\n\tauto result = std::vector<int>();\n\tconst auto push = [&](Step step, int count) {\n\t\tconst auto index = static_cast<int>(step);\n\t\tif (index >= result.size()) {\n\t\t\tresult.resize(index + 1, 0);\n\t\t}\n\t\tresult[index] = count;\n\t};\n\tpush(Step::Initializing, 1);\n\tif (_settings.types & Settings::Type::AnyChatsMask) {\n\t\tpush(Step::DialogsList, 1);\n\t}\n\tif (_settings.types & Settings::Type::PersonalInfo) {\n\t\tpush(Step::PersonalInfo, 1);\n\t}\n\tif (_settings.types & Settings::Type::Userpics) {\n\t\tpush(Step::Userpics, 1);\n\t}\n\tif (_settings.types & Settings::Type::Stories) {\n\t\tpush(Step::Stories, 1);\n\t}\n\tif (_settings.types & Settings::Type::ProfileMusic) {\n\t\tpush(Step::ProfileMusic, 1);\n\t}\n\tif (_settings.types & Settings::Type::Contacts) {\n\t\tpush(Step::Contacts, 1);\n\t}\n\tif (_settings.types & Settings::Type::Sessions) {\n\t\tpush(Step::Sessions, 1);\n\t}\n\tif (_settings.types & Settings::Type::OtherData) {\n\t\tpush(Step::OtherData, 1);\n\t}\n\tif (_settings.types & Settings::Type::AnyChatsMask) {\n\t\tpush(Step::Dialogs, info.dialogsCount);\n\t}\n\tif (_settings.onlySingleTopic()) {\n\t\tpush(Step::Topic, 1);\n\t}\n\t_substepsInStep = std::move(result);\n\t_substepsTotal = ranges::accumulate(_substepsInStep, 0);\n}\n\nvoid ControllerObject::cancelExportFast() {\n\t_api.cancelExportFast();\n\tsetState(CancelledState());\n}\n\nvoid ControllerObject::exportNext() {\n\tif (++_stepIndex >= _steps.size()) {\n\t\tif (ioCatchError(_writer->finish())) {\n\t\t\treturn;\n\t\t}\n\t\t_api.finishExport([=] {\n\t\t\tsetFinishedState();\n\t\t});\n\t\treturn;\n\t}\n\n\tconst auto step = _steps[_stepIndex];\n\tswitch (step) {\n\tcase Step::Initializing: return initialize();\n\tcase Step::DialogsList: return collectDialogsList();\n\tcase Step::PersonalInfo: return exportPersonalInfo();\n\tcase Step::Userpics: return exportUserpics();\n\tcase Step::Stories: return exportStories();\n\tcase Step::ProfileMusic: return exportProfileMusic();\n\tcase Step::Contacts: return exportContacts();\n\tcase Step::Sessions: return exportSessions();\n\tcase Step::OtherData: return exportOtherData();\n\tcase Step::Dialogs: return exportDialogs();\n\tcase Step::Topic: return exportTopic();\n\t}\n\tUnexpected(\"Step in ControllerObject::exportNext.\");\n}\n\nvoid ControllerObject::initialize() {\n\tsetState(stateInitializing());\n\t_api.startExport(_settings, &_stats, [=](ApiWrap::StartInfo info) {\n\t\tinitialized(info);\n\t});\n}\n\nvoid ControllerObject::initialized(const ApiWrap::StartInfo &info) {\n\tif (ioCatchError(_writer->start(_settings, _environment, &_stats))) {\n\t\treturn;\n\t}\n\tfillSubstepsInSteps(info);\n\texportNext();\n}\n\nvoid ControllerObject::collectDialogsList() {\n\tsetState(stateDialogsList(0));\n\t_api.requestDialogsList([=](int count) {\n\t\tif (count > 0) {\n\t\t\tsetState(stateDialogsList(count - 1));\n\t\t}\n\t\treturn true;\n\t}, [=](Data::DialogsInfo &&result) {\n\t\t_dialogsInfo = std::move(result);\n\t\texportNext();\n\t});\n}\n\nvoid ControllerObject::exportPersonalInfo() {\n\tsetState(statePersonalInfo());\n\t_api.requestPersonalInfo([=](Data::PersonalInfo &&result) {\n\t\tif (ioCatchError(_writer->writePersonal(result))) {\n\t\t\treturn;\n\t\t}\n\t\texportNext();\n\t});\n}\n\nvoid ControllerObject::exportUserpics() {\n\t_api.requestUserpics([=](Data::UserpicsInfo &&start) {\n\t\tif (ioCatchError(_writer->writeUserpicsStart(start))) {\n\t\t\treturn false;\n\t\t}\n\t\t_userpicsWritten = 0;\n\t\t_userpicsCount = start.count;\n\t\treturn true;\n\t}, [=](DownloadProgress progress) {\n\t\tsetState(stateUserpics(progress));\n\t\treturn true;\n\t}, [=](Data::UserpicsSlice &&slice) {\n\t\tif (ioCatchError(_writer->writeUserpicsSlice(slice))) {\n\t\t\treturn false;\n\t\t}\n\t\t_userpicsWritten += slice.list.size();\n\t\tsetState(stateUserpics(DownloadProgress()));\n\t\treturn true;\n\t}, [=] {\n\t\tif (ioCatchError(_writer->writeUserpicsEnd())) {\n\t\t\treturn;\n\t\t}\n\t\texportNext();\n\t});\n}\n\nvoid ControllerObject::exportStories() {\n\t_api.requestStories([=](Data::StoriesInfo &&start) {\n\t\tif (ioCatchError(_writer->writeStoriesStart(start))) {\n\t\t\treturn false;\n\t\t}\n\t\t_storiesWritten = 0;\n\t\t_storiesCount = start.count;\n\t\treturn true;\n\t}, [=](DownloadProgress progress) {\n\t\tsetState(stateStories(progress));\n\t\treturn true;\n\t}, [=](Data::StoriesSlice &&slice) {\n\t\tif (ioCatchError(_writer->writeStoriesSlice(slice))) {\n\t\t\treturn false;\n\t\t}\n\t\t_storiesWritten += slice.list.size();\n\t\tsetState(stateStories(DownloadProgress()));\n\t\treturn true;\n\t}, [=] {\n\t\tif (ioCatchError(_writer->writeStoriesEnd())) {\n\t\t\treturn;\n\t\t}\n\t\texportNext();\n\t});\n}\n\nvoid ControllerObject::exportProfileMusic() {\n\t_api.requestProfileMusic([=](Data::ProfileMusicInfo &&start) {\n\t\tif (ioCatchError(_writer->writeProfileMusicStart(start))) {\n\t\t\treturn false;\n\t\t}\n\t\t_profileMusicWritten = 0;\n\t\t_profileMusicCount = start.count;\n\t\treturn true;\n\t}, [=](DownloadProgress progress) {\n\t\tsetState(stateProfileMusic(progress));\n\t\treturn true;\n\t}, [=](Data::ProfileMusicSlice &&slice) {\n\t\tif (ioCatchError(_writer->writeProfileMusicSlice(slice))) {\n\t\t\treturn false;\n\t\t}\n\t\t_profileMusicWritten += slice.list.size();\n\t\tsetState(stateProfileMusic(DownloadProgress()));\n\t\treturn true;\n\t}, [=] {\n\t\tif (ioCatchError(_writer->writeProfileMusicEnd())) {\n\t\t\treturn;\n\t\t}\n\t\texportNext();\n\t});\n}\n\nvoid ControllerObject::exportContacts() {\n\tsetState(stateContacts());\n\t_api.requestContacts([=](Data::ContactsList &&result) {\n\t\tif (ioCatchError(_writer->writeContactsList(result))) {\n\t\t\treturn;\n\t\t}\n\t\texportNext();\n\t});\n}\n\nvoid ControllerObject::exportSessions() {\n\tsetState(stateSessions());\n\t_api.requestSessions([=](Data::SessionsList &&result) {\n\t\tif (ioCatchError(_writer->writeSessionsList(result))) {\n\t\t\treturn;\n\t\t}\n\t\texportNext();\n\t});\n}\n\nvoid ControllerObject::exportOtherData() {\n\tsetState(stateOtherData());\n\tconst auto relativePath = \"lists/other_data.json\";\n\t_api.requestOtherData(relativePath, [=](Data::File &&result) {\n\t\tif (ioCatchError(_writer->writeOtherData(result))) {\n\t\t\treturn;\n\t\t}\n\t\texportNext();\n\t});\n}\n\nvoid ControllerObject::exportDialogs() {\n\tif (ioCatchError(_writer->writeDialogsStart(_dialogsInfo))) {\n\t\treturn;\n\t}\n\n\texportNextDialog();\n}\n\nvoid ControllerObject::exportNextDialog() {\n\tconst auto index = ++_dialogIndex;\n\tconst auto info = _dialogsInfo.item(index);\n\tif (info) {\n\t\t_api.requestMessages(*info, [=](const Data::DialogInfo &info) {\n\t\t\tif (ioCatchError(_writer->writeDialogStart(info))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t_messagesWritten = 0;\n\t\t\t_messagesCount = ranges::accumulate(\n\t\t\t\tinfo.messagesCountPerSplit,\n\t\t\t\t0);\n\t\t\tsetState(stateDialogs(DownloadProgress()));\n\t\t\treturn true;\n\t\t}, [=](DownloadProgress progress) {\n\t\t\tsetState(stateDialogs(progress));\n\t\t\treturn true;\n\t\t}, [=](Data::MessagesSlice &&result) {\n\t\t\tif (ioCatchError(_writer->writeDialogSlice(result))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t_messagesWritten += result.list.size();\n\t\t\tsetState(stateDialogs(DownloadProgress()));\n\t\t\treturn true;\n\t\t}, [=] {\n\t\t\tif (ioCatchError(_writer->writeDialogEnd())) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\texportNextDialog();\n\t\t});\n\t\treturn;\n\t}\n\tif (ioCatchError(_writer->writeDialogsEnd())) {\n\t\treturn;\n\t}\n\texportNext();\n}\n\ntemplate <typename Callback>\nProcessingState ControllerObject::prepareState(\n\t\tStep step,\n\t\tCallback &&callback) const {\n\tif (step != _lastProcessingStep) {\n\t\t_substepsPassed += substepsInStep(_lastProcessingStep);\n\t\t_lastProcessingStep = step;\n\t}\n\n\tauto result = ProcessingState();\n\tcallback(result);\n\tresult.step = step;\n\tresult.substepsPassed = _substepsPassed;\n\tresult.substepsNow = substepsInStep(_lastProcessingStep);\n\tresult.substepsTotal = _substepsTotal;\n\treturn result;\n}\n\nProcessingState ControllerObject::stateInitializing() const {\n\treturn ProcessingState();\n}\n\nProcessingState ControllerObject::stateDialogsList(int processed) const {\n\tconst auto step = Step::DialogsList;\n\treturn prepareState(step, [&](ProcessingState &result) {\n\t\tresult.entityIndex = processed;\n\t\tresult.entityCount = std::max(\n\t\t\tprocessed,\n\t\t\tsubstepsInStep(Step::Dialogs));\n\t});\n}\nProcessingState ControllerObject::statePersonalInfo() const {\n\treturn prepareState(Step::PersonalInfo);\n}\n\nProcessingState ControllerObject::stateUserpics(\n\t\tconst DownloadProgress &progress) const {\n\treturn prepareState(Step::Userpics, [&](ProcessingState &result) {\n\t\tresult.entityIndex = _userpicsWritten + progress.itemIndex;\n\t\tresult.entityCount = std::max(_userpicsCount, result.entityIndex);\n\t\tresult.bytesRandomId = progress.randomId;\n\t\tif (!progress.path.isEmpty()) {\n\t\t\tconst auto last = progress.path.lastIndexOf('/');\n\t\t\tresult.bytesName = progress.path.mid(last + 1);\n\t\t}\n\t\tresult.bytesLoaded = progress.ready;\n\t\tresult.bytesCount = progress.total;\n\t});\n}\n\nProcessingState ControllerObject::stateStories(\n\t\tconst DownloadProgress &progress) const {\n\treturn prepareState(Step::Stories, [&](ProcessingState &result) {\n\t\tresult.entityIndex = _storiesWritten + progress.itemIndex;\n\t\tresult.entityCount = std::max(_storiesCount, result.entityIndex);\n\t\tresult.bytesRandomId = progress.randomId;\n\t\tif (!progress.path.isEmpty()) {\n\t\t\tconst auto last = progress.path.lastIndexOf('/');\n\t\t\tresult.bytesName = progress.path.mid(last + 1);\n\t\t}\n\t\tresult.bytesLoaded = progress.ready;\n\t\tresult.bytesCount = progress.total;\n\t});\n}\n\nProcessingState ControllerObject::stateProfileMusic(\n\t\tconst DownloadProgress &progress) const {\n\treturn prepareState(Step::ProfileMusic, [&](ProcessingState &result) {\n\t\tresult.entityIndex = _profileMusicWritten + progress.itemIndex;\n\t\tresult.entityCount = std::max(_profileMusicCount, result.entityIndex);\n\t\tresult.bytesRandomId = progress.randomId;\n\t\tif (!progress.path.isEmpty()) {\n\t\t\tconst auto last = progress.path.lastIndexOf('/');\n\t\t\tresult.bytesName = progress.path.mid(last + 1);\n\t\t}\n\t\tresult.bytesLoaded = progress.ready;\n\t\tresult.bytesCount = progress.total;\n\t});\n}\n\nProcessingState ControllerObject::stateContacts() const {\n\treturn prepareState(Step::Contacts);\n}\n\nProcessingState ControllerObject::stateSessions() const {\n\treturn prepareState(Step::Sessions);\n}\n\nProcessingState ControllerObject::stateOtherData() const {\n\treturn prepareState(Step::OtherData);\n}\n\nProcessingState ControllerObject::stateDialogs(\n\t\tconst DownloadProgress &progress) const {\n\tconst auto step = Step::Dialogs;\n\treturn prepareState(step, [&](ProcessingState &result) {\n\t\tfillMessagesState(\n\t\t\tresult,\n\t\t\t_dialogsInfo,\n\t\t\t_dialogIndex,\n\t\t\tprogress);\n\t});\n}\n\nvoid ControllerObject::fillMessagesState(\n\t\tProcessingState &result,\n\t\tconst Data::DialogsInfo &info,\n\t\tint index,\n\t\tconst DownloadProgress &progress) const {\n\tconst auto dialog = info.item(index);\n\tAssert(dialog != nullptr);\n\n\tresult.entityIndex = index;\n\tresult.entityCount = info.chats.size() + info.left.size();\n\tresult.entityName = dialog->name;\n\tresult.entityType = (dialog->type == Data::DialogInfo::Type::Self)\n\t\t? ProcessingState::EntityType::SavedMessages\n\t\t: (dialog->type == Data::DialogInfo::Type::Replies)\n\t\t? ProcessingState::EntityType::RepliesMessages\n\t\t: (dialog->type == Data::DialogInfo::Type::VerifyCodes)\n\t\t? ProcessingState::EntityType::VerifyCodes\n\t\t: ProcessingState::EntityType::Chat;\n\tresult.itemIndex = _messagesWritten + progress.itemIndex;\n\tresult.itemCount = std::max(_messagesCount, result.itemIndex);\n\tresult.bytesRandomId = progress.randomId;\n\tif (!progress.path.isEmpty()) {\n\t\tconst auto last = progress.path.lastIndexOf('/');\n\t\tresult.bytesName = progress.path.mid(last + 1);\n\t}\n\tresult.bytesLoaded = progress.ready;\n\tresult.bytesCount = progress.total;\n}\n\nint ControllerObject::substepsInStep(Step step) const {\n\tExpects(_substepsInStep.size() > static_cast<int>(step));\n\n\treturn _substepsInStep[static_cast<int>(step)];\n}\n\nvoid ControllerObject::exportTopic() {\n\tauto topicInfo = Data::DialogInfo();\n\ttopicInfo.type = Data::DialogInfo::Type::PublicSupergroup;\n\ttopicInfo.name = _topicTitle.toUtf8();\n\ttopicInfo.peerId = PeerId(_topicPeerId);\n\ttopicInfo.relativePath = QString();\n\n\tif (ioCatchError(_writer->writeDialogStart(topicInfo))) {\n\t\treturn;\n\t}\n\n\t_api.requestTopicMessages(\n\t\tPeerId(_topicPeerId),\n\t\t_settings.singlePeer,\n\t\t_topicRootId,\n\t\t[=](int count) {\n\t\t\t_messagesWritten = 0;\n\t\t\t_messagesCount = count;\n\t\t\tsetState(stateTopic(DownloadProgress()));\n\t\t\treturn true;\n\t\t},\n\t\t[=](DownloadProgress progress) {\n\t\t\tsetState(stateTopic(progress));\n\t\t\treturn true;\n\t\t},\n\t\t[=](Data::MessagesSlice &&slice) {\n\t\t\tif (ioCatchError(_writer->writeDialogSlice(slice))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t_messagesWritten += slice.list.size();\n\t\t\tsetState(stateTopic(DownloadProgress()));\n\t\t\treturn true;\n\t\t},\n\t\t[=] {\n\t\t\tif (ioCatchError(_writer->writeDialogEnd())) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (ioCatchError(_writer->finish())) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_api.finishExport([=] {\n\t\t\t\tsetFinishedState();\n\t\t\t});\n\t\t});\n}\n\nProcessingState ControllerObject::stateTopic(\n\t\tconst DownloadProgress &progress) const {\n\treturn prepareState(Step::Topic, [&](ProcessingState &result) {\n\t\tresult.entityType = ProcessingState::EntityType::Topic;\n\t\tresult.entityName = _topicTitle;\n\t\tresult.entityIndex = 0;\n\t\tresult.entityCount = 1;\n\t\tresult.itemIndex = _messagesWritten + progress.itemIndex;\n\t\tresult.itemCount = std::max(_messagesCount, result.itemIndex);\n\t\tresult.bytesRandomId = progress.randomId;\n\t\tif (!progress.path.isEmpty()) {\n\t\t\tconst auto last = progress.path.lastIndexOf('/');\n\t\t\tresult.bytesName = progress.path.mid(last + 1);\n\t\t}\n\t\tresult.bytesLoaded = progress.ready;\n\t\tresult.bytesCount = progress.total;\n\t});\n}\n\nvoid ControllerObject::setFinishedState() {\n\tsetState(FinishedState{\n\t\t_writer->mainFilePath(),\n\t\t_stats.filesCount(),\n\t\t_stats.bytesCount() });\n}\n\nController::Controller(\n\tQPointer<MTP::Instance> mtproto,\n\tconst MTPInputPeer &peer)\n: _wrapped(std::move(mtproto), peer) {\n}\n\nController::Controller(\n\tQPointer<MTP::Instance> mtproto,\n\tconst MTPInputPeer &peer,\n\tint32 topicRootId,\n\tuint64 peerId,\n\tconst QString &topicTitle)\n: _wrapped(\n\tstd::move(mtproto),\n\tpeer,\n\tstatic_cast<int32>(topicRootId),\n\tstatic_cast<uint64>(peerId),\n\ttopicTitle) {\n}\n\nrpl::producer<State> Controller::state() const {\n\treturn _wrapped.producer_on_main([=](const Implementation &unwrapped) {\n\t\treturn unwrapped.state();\n\t});\n}\n\n//void Controller::submitPassword(const QString &password) {\n//\t_wrapped.with([=](Implementation &unwrapped) {\n//\t\tunwrapped.submitPassword(password);\n//\t});\n//}\n//\n//void Controller::requestPasswordRecover() {\n//\t_wrapped.with([=](Implementation &unwrapped) {\n//\t\tunwrapped.requestPasswordRecover();\n//\t});\n//}\n//\n//rpl::producer<PasswordUpdate> Controller::passwordUpdate() const {\n//\treturn _wrapped.producer_on_main([=](const Implementation &unwrapped) {\n//\t\treturn unwrapped.passwordUpdate();\n//\t});\n//}\n//\n//void Controller::reloadPasswordState() {\n//\t_wrapped.with([=](Implementation &unwrapped) {\n//\t\tunwrapped.reloadPasswordState();\n//\t});\n//}\n//\n//void Controller::cancelUnconfirmedPassword() {\n//\t_wrapped.with([=](Implementation &unwrapped) {\n//\t\tunwrapped.cancelUnconfirmedPassword();\n//\t});\n//}\n\nvoid Controller::startExport(\n\t\tconst Settings &settings,\n\t\tconst Environment &environment) {\n\tLOG((\"Export Info: Started export to '%1'.\").arg(settings.path));\n\n\t_wrapped.with([=](Implementation &unwrapped) {\n\t\tunwrapped.startExport(settings, environment);\n\t});\n}\n\nvoid Controller::skipFile(uint64 randomId) {\n\t_wrapped.with([=](Implementation &unwrapped) {\n\t\tunwrapped.skipFile(randomId);\n\t});\n}\n\nvoid Controller::cancelExportFast() {\n\tLOG((\"Export Info: Cancelled export.\"));\n\n\t_wrapped.with([=](Implementation &unwrapped) {\n\t\tunwrapped.cancelExportFast();\n\t});\n}\n\nrpl::lifetime &Controller::lifetime() {\n\treturn _lifetime;\n}\n\nController::~Controller() {\n\tLOG((\"Export Info: Controller destroyed.\"));\n}\n\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/export_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/variant.h\"\n#include \"mtproto/mtproto_response.h\"\n\n#include <QtCore/QPointer>\n#include <crl/crl_object_on_queue.h>\n\nnamespace MTP {\nclass Instance;\n} // namespace MTP\n\nnamespace Export {\n\nclass ControllerObject;\nstruct Settings;\nstruct Environment;\n\nstruct PasswordCheckState {\n\tQString hint;\n\tQString unconfirmedPattern;\n\tbool requesting = true;\n\tbool hasPassword = false;\n\tbool checked = false;\n\tMTPInputPeer singlePeer = MTP_inputPeerEmpty();\n};\n\nstruct ProcessingState {\n\tenum class Step {\n\t\tInitializing,\n\t\tDialogsList,\n\t\tPersonalInfo,\n\t\tUserpics,\n\t\tStories,\n\t\tProfileMusic,\n\t\tContacts,\n\t\tSessions,\n\t\tOtherData,\n\t\tDialogs,\n\t\tTopic,\n\t};\n\tenum class EntityType {\n\t\tChat,\n\t\tSavedMessages,\n\t\tRepliesMessages,\n\t\tVerifyCodes,\n\t\tTopic,\n\t\tOther,\n\t};\n\n\tStep step = Step::Initializing;\n\n\tint substepsPassed = 0;\n\tint substepsNow = 0;\n\tint substepsTotal = 0;\n\n\tEntityType entityType = EntityType::Other;\n\tQString entityName;\n\tint entityIndex = 0;\n\tint entityCount = 0;\n\n\tint itemIndex = 0;\n\tint itemCount = 0;\n\n\tuint64 bytesRandomId = 0;\n\tQString bytesName;\n\tint64 bytesLoaded = 0;\n\tint64 bytesCount = 0;\n};\n\nstruct ApiErrorState {\n\tMTP::Error data;\n};\n\nstruct OutputErrorState {\n\tQString path;\n};\n\nstruct CancelledState {\n};\n\nstruct FinishedState {\n\tQString path;\n\tint filesCount = 0;\n\tint64 bytesCount = 0;\n};\n\nusing State = std::variant<\n\tv::null_t,\n\tPasswordCheckState,\n\tProcessingState,\n\tApiErrorState,\n\tOutputErrorState,\n\tCancelledState,\n\tFinishedState>;\n\n//struct PasswordUpdate {\n//\tenum class Type {\n//\t\tCheckSucceed,\n//\t\tWrongPassword,\n//\t\tFloodLimit,\n//\t\tRecoverUnavailable,\n//\t};\n//\tType type = Type::WrongPassword;\n//\n//};\n\nclass Controller {\npublic:\n\tController(\n\t\tQPointer<MTP::Instance> mtproto,\n\t\tconst MTPInputPeer &peer);\n\tController(\n\t\tQPointer<MTP::Instance> mtproto,\n\t\tconst MTPInputPeer &peer,\n\t\tint32 topicRootId,\n\t\tuint64 peerId,\n\t\tconst QString &topicTitle);\n\n\trpl::producer<State> state() const;\n\n\t// Password step.\n\t//void submitPassword(const QString &password);\n\t//void requestPasswordRecover();\n\t//rpl::producer<PasswordUpdate> passwordUpdate() const;\n\t//void reloadPasswordState();\n\t//void cancelUnconfirmedPassword();\n\n\t// Processing step.\n\tvoid startExport(\n\t\tconst Settings &settings,\n\t\tconst Environment &environment);\n\tvoid skipFile(uint64 randomId);\n\tvoid cancelExportFast();\n\n\trpl::lifetime &lifetime();\n\n\t~Controller();\n\nprivate:\n\tusing Implementation = ControllerObject;\n\tcrl::object_on_queue<Implementation> _wrapped;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/export_manager.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/export_manager.h\"\n\n#include \"export/export_controller.h\"\n#include \"export/view/export_view_panel_controller.h\"\n#include \"data/data_peer.h\"\n#include \"main/main_session.h\"\n#include \"main/main_account.h\"\n#include \"ui/layers/box_content.h\"\n#include \"base/unixtime.h\"\n\nnamespace Export {\n\nManager::Manager() = default;\n\nManager::~Manager() = default;\n\nvoid Manager::start(not_null<PeerData*> peer) {\n\tstart(&peer->session(), peer->input());\n}\n\nvoid Manager::startTopic(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tconst QString &topicTitle) {\n\tif (_panel) {\n\t\t_panel->activatePanel();\n\t\treturn;\n\t}\n\t_controller = std::make_unique<Controller>(\n\t\t&peer->session().mtp(),\n\t\tpeer->input(),\n\t\tint32(topicRootId.bare),\n\t\tuint64(peer->id.value),\n\t\ttopicTitle);\n\tsetupPanel(&peer->session());\n}\n\nvoid Manager::start(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPInputPeer &singlePeer) {\n\tif (_panel) {\n\t\t_panel->activatePanel();\n\t\treturn;\n\t}\n\t_controller = std::make_unique<Controller>(\n\t\t&session->mtp(),\n\t\tsinglePeer);\n\tsetupPanel(session);\n}\n\nvoid Manager::setupPanel(not_null<Main::Session*> session) {\n\t_panel = std::make_unique<View::PanelController>(\n\t\tsession,\n\t\t_controller.get());\n\tsession->account().sessionChanges(\n\t) | rpl::filter([=](Main::Session *value) {\n\t\treturn (value != session);\n\t}) | rpl::on_next([=] {\n\t\tstop();\n\t}, _panel->lifetime());\n\n\t_viewChanges.fire(_panel.get());\n\n\t_panel->stopRequests(\n\t) | rpl::on_next([=] {\n\t\tLOG((\"Export Info: Stop requested.\"));\n\t\tstop();\n\t}, _controller->lifetime());\n}\n\nrpl::producer<View::PanelController*> Manager::currentView(\n) const {\n\treturn _viewChanges.events_starting_with(_panel.get());\n}\n\nbool Manager::inProgress() const {\n\treturn _controller != nullptr;\n}\n\nbool Manager::inProgress(not_null<Main::Session*> session) const {\n\treturn _panel && (&_panel->session() == session);\n}\n\nvoid Manager::stopWithConfirmation(Fn<void()> callback) {\n\tif (!_panel) {\n\t\tcallback();\n\t\treturn;\n\t}\n\tauto closeAndCall = [=, callback = std::move(callback)]() mutable {\n\t\tauto saved = std::move(callback);\n\t\tLOG((\"Export Info: Stop With Confirmation.\"));\n\t\tstop();\n\t\tif (saved) {\n\t\t\tsaved();\n\t\t}\n\t};\n\t_panel->stopWithConfirmation(std::move(closeAndCall));\n}\n\nvoid Manager::stop() {\n\tif (_panel) {\n\t\tLOG((\"Export Info: Destroying.\"));\n\t\t_panel = nullptr;\n\t\t_viewChanges.fire(nullptr);\n\t}\n\t_controller = nullptr;\n}\n\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/export_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\n\nnamespace Ui {\nclass BoxContent;\n} // namespace Ui\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Export {\n\nclass Controller;\n\nnamespace View {\nclass PanelController;\n} // namespace View\n\nclass Manager final {\npublic:\n\tManager();\n\t~Manager();\n\n\tvoid start(not_null<PeerData*> peer);\n\tvoid start(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPInputPeer &singlePeer = MTP_inputPeerEmpty());\n\tvoid startTopic(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tconst QString &topicTitle);\n\n\t[[nodiscard]] rpl::producer<View::PanelController*> currentView() const;\n\t[[nodiscard]] bool inProgress() const;\n\t[[nodiscard]] bool inProgress(not_null<Main::Session*> session) const;\n\tvoid stopWithConfirmation(Fn<void()> callback);\n\tvoid stop();\n\nprivate:\n\tvoid setupPanel(not_null<Main::Session*> session);\n\n\tstd::unique_ptr<Controller> _controller;\n\tstd::unique_ptr<View::PanelController> _panel;\n\trpl::event_stream<View::PanelController*> _viewChanges;\n\n};\n\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/export_pch.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\n#include <QtCore/QFile>\n#include <QtCore/QFileInfo>\n#include <QtCore/QDir>\n#include <QtCore/QSize>\n#include <QtCore/QTextStream>\n#include <QtCore/QDateTime>\n#include <QtCore/QString>\n#include <QtCore/QByteArray>\n#include <QtCore/QReadWriteLock>\n#include <QtCore/QRegularExpression>\n\n#include <crl/crl.h>\n#include <rpl/rpl.h>\n\n#include <vector>\n#include <map>\n#include <set>\n#include <deque>\n#include <atomic>\n\n#include <range/v3/all.hpp>\n\n#include \"base/flat_map.h\"\n#include \"base/flat_set.h\"\n\n#include \"scheme.h\"\n#include \"logs.h\"\n"
  },
  {
    "path": "Telegram/SourceFiles/export/export_settings.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/export_settings.h\"\n\n#include \"export/output/export_output_abstract.h\"\n\nnamespace Export {\nnamespace {\n\nconstexpr auto kMaxFileSize = 4000 * int64(1024 * 1024);\n\n} // namespace\n\nbool MediaSettings::validate() const {\n\tif ((types | Type::AllMask) != Type::AllMask) {\n\t\treturn false;\n\t} else if (sizeLimit < 0 || sizeLimit > kMaxFileSize) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nbool Settings::validate() const {\n\tusing Format = Output::Format;\n\tconst auto MustBeFull = Type::PersonalChats | Type::BotChats;\n\tconst auto MustNotBeFull = Type::PublicGroups | Type::PublicChannels;\n\tif ((types | Type::AllMask) != Type::AllMask) {\n\t\treturn false;\n\t} else if ((fullChats | Type::AllMask) != Type::AllMask) {\n\t\treturn false;\n\t} else if ((fullChats & MustBeFull) != MustBeFull) {\n\t\treturn false;\n\t} else if ((fullChats & MustNotBeFull) != 0) {\n\t\treturn false;\n\t} else if (format != Format::Html && format != Format::Json) {\n\t\treturn false;\n\t} else if (!media.validate()) {\n\t\treturn false;\n\t} else if (singlePeerTill > 0 && singlePeerTill <= singlePeerFrom) {\n\t\treturn false;\n\t}\n\treturn true;\n};\n\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/export_settings.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n#include \"base/flat_map.h\"\n\nnamespace Export {\nnamespace Output {\nenum class Format;\n} // namespace Output\n\nstruct MediaSettings {\n\tbool validate() const;\n\n\tenum class Type {\n\t\tPhoto        = 0x01,\n\t\tVideo        = 0x02,\n\t\tVoiceMessage = 0x04,\n\t\tVideoMessage = 0x08,\n\t\tSticker      = 0x10,\n\t\tGIF          = 0x20,\n\t\tFile         = 0x40,\n\n\t\tMediaMask    = Photo | Video | VoiceMessage | VideoMessage,\n\t\tAllMask      = MediaMask | Sticker | GIF | File,\n\t};\n\tusing Types = base::flags<Type>;\n\tfriend inline constexpr auto is_flag_type(Type) { return true; };\n\n\tTypes types = DefaultTypes();\n\tint64 sizeLimit = 8 * 1024 * 1024;\n\n\tstatic inline Types DefaultTypes() {\n\t\treturn Type::Photo;\n\t}\n\n};\n\nstruct Settings {\n\tbool validate() const;\n\n\tenum class Type {\n\t\tPersonalInfo        = 0x001,\n\t\tUserpics            = 0x002,\n\t\tContacts            = 0x004,\n\t\tSessions            = 0x008,\n\t\tOtherData           = 0x010,\n\t\tPersonalChats       = 0x020,\n\t\tBotChats            = 0x040,\n\t\tPrivateGroups       = 0x080,\n\t\tPublicGroups        = 0x100,\n\t\tPrivateChannels     = 0x200,\n\t\tPublicChannels      = 0x400,\n\t\tStories             = 0x800,\n\t\tProfileMusic        = 0x1000,\n\n\t\tGroupsMask          = PrivateGroups | PublicGroups,\n\t\tChannelsMask        = PrivateChannels | PublicChannels,\n\t\tGroupsChannelsMask  = GroupsMask | ChannelsMask,\n\t\tNonChannelChatsMask = PersonalChats | BotChats | PrivateGroups,\n\t\tAnyChatsMask        = PersonalChats | BotChats | GroupsChannelsMask,\n\t\tNonChatsMask        = (PersonalInfo\n\t\t\t| Userpics\n\t\t\t| Contacts\n\t\t\t| Stories\n\t\t\t| ProfileMusic\n\t\t\t| Sessions),\n\t\tAllMask             = NonChatsMask | OtherData | AnyChatsMask,\n\t};\n\tusing Types = base::flags<Type>;\n\tfriend inline constexpr auto is_flag_type(Type) { return true; };\n\n\tQString path;\n\tbool forceSubPath = false;\n\tOutput::Format format = Output::Format();\n\n\tTypes types = DefaultTypes();\n\tTypes fullChats = DefaultFullChats();\n\tMediaSettings media;\n\n\tMTPInputPeer singlePeer = MTP_inputPeerEmpty();\n\tTimeId singlePeerFrom = 0;\n\tTimeId singlePeerTill = 0;\n\n\tint32 singleTopicRootId = 0;\n\tuint64 singleTopicPeerId = 0;\n\tQString singleTopicTitle;\n\n\tTimeId availableAt = 0;\n\n\tbool onlySinglePeer() const {\n\t\treturn singlePeer.type() != mtpc_inputPeerEmpty;\n\t}\n\n\tbool onlySingleTopic() const {\n\t\treturn onlySinglePeer() && singleTopicRootId != 0;\n\t}\n\n\tstatic inline Types DefaultTypes() {\n\t\treturn Type::PersonalInfo\n\t\t\t| Type::Userpics\n\t\t\t| Type::Contacts\n\t\t\t| Type::Stories\n\t\t\t| Type::ProfileMusic\n\t\t\t| Type::PersonalChats\n\t\t\t| Type::PrivateGroups;\n\t}\n\n\tstatic inline Types DefaultFullChats() {\n\t\treturn Type::PersonalChats\n\t\t\t| Type::BotChats;\n\t}\n\n};\n\nstruct Environment {\n\tQString internalLinksDomain;\n\tQByteArray aboutTelegram;\n\tQByteArray aboutContacts;\n\tQByteArray aboutFrequent;\n\tQByteArray aboutSessions;\n\tQByteArray aboutWebSessions;\n\tQByteArray aboutChats;\n\tQByteArray aboutLeftChats;\n};\n\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/output/export_output_abstract.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/output/export_output_abstract.h\"\n\n#include \"export/output/export_output_html_and_json.h\"\n#include \"export/output/export_output_html.h\"\n#include \"export/output/export_output_json.h\"\n#include \"export/output/export_output_stats.h\"\n#include \"export/output/export_output_result.h\"\n\n#include <QtCore/QDir>\n#include <QtCore/QDate>\n\nnamespace Export {\nnamespace Output {\n\nQString NormalizePath(const Settings &settings) {\n\tQDir folder(settings.path);\n\tconst auto path = folder.absolutePath();\n\tauto result = path.endsWith('/') ? path : (path + '/');\n\tif (!folder.exists() && !settings.forceSubPath) {\n\t\treturn result;\n\t}\n\tconst auto mode = QDir::AllEntries | QDir::NoDotAndDotDot;\n\tconst auto list = folder.entryInfoList(mode);\n\tif (list.isEmpty() && !settings.forceSubPath) {\n\t\treturn result;\n\t}\n\tconst auto date = QDate::currentDate();\n\tconst auto base = QString(settings.onlySinglePeer()\n\t\t? \"ChatExport_%1\"\n\t\t: \"DataExport_%1\"\n\t).arg(date.toString(Qt::ISODate));\n\tconst auto add = [&](int i) {\n\t\treturn base + (i ? \" (\" + QString::number(i) + ')' : QString());\n\t};\n\tauto index = 0;\n\twhile (QDir(result + add(index)).exists()) {\n\t\t++index;\n\t}\n\tresult += add(index) + '/';\n\treturn result;\n}\n\nstd::unique_ptr<AbstractWriter> CreateWriter(Format format) {\n\tswitch (format) {\n\tcase Format::Html: return std::make_unique<HtmlWriter>();\n\tcase Format::Json: return std::make_unique<JsonWriter>();\n\tcase Format::HtmlAndJson: return std::make_unique<HtmlAndJsonWriter>();\n\t}\n\tUnexpected(\"Format in Export::Output::CreateWriter.\");\n}\n\nStats AbstractWriter::produceTestExample(\n\t\tconst QString &path,\n\t\tconst Environment &environment) {\n\tauto result = Stats();\n\tconst auto folder = QDir(path).absolutePath();\n\tauto settings = Settings();\n\tsettings.format = format();\n\tsettings.path = (folder.endsWith('/') ? folder : (folder + '/'))\n\t\t+ \"ExportExample/\";\n\tsettings.types = Settings::Type::AllMask;\n\tsettings.fullChats = Settings::Type::AllMask\n\t\t& ~(Settings::Type::PublicChannels | Settings::Type::PublicGroups);\n\tsettings.media.types = MediaSettings::Type::AllMask;\n\tsettings.media.sizeLimit = 1024 * 1024;\n\n\tconst auto check = [](Result result) {\n\t\tAssert(result.isSuccess());\n\t};\n\n\tcheck(start(settings, environment, &result));\n\n\tconst auto counter = [&] {\n\t\tstatic auto GlobalCounter = 0;\n\t\treturn ++GlobalCounter;\n\t};\n\tconst auto date = [&] {\n\t\treturn time(nullptr) - 86400 + counter();\n\t};\n\tconst auto prevdate = [&] {\n\t\treturn date() - 86400;\n\t};\n\n\tauto personal = Data::PersonalInfo();\n\tpersonal.bio = \"Nice text about me.\";\n\tpersonal.user.info.firstName = \"John\";\n\tpersonal.user.info.lastName = \"Preston\";\n\tpersonal.user.info.phoneNumber = \"447400000000\";\n\tpersonal.user.info.date = date();\n\tpersonal.user.username = \"preston\";\n\tpersonal.user.info.userId = counter();\n\tpersonal.user.isBot = false;\n\tpersonal.user.isSelf = true;\n\tcheck(writePersonal(personal));\n\n\tconst auto generatePhoto = [&] {\n\t\tstatic auto index = 0;\n\t\tauto result = Data::Photo();\n\t\tresult.date = date();\n\t\tresult.id = counter();\n\t\tresult.image.width = 512;\n\t\tresult.image.height = 512;\n\t\tresult.image.file.relativePath = \"files/photo_\"\n\t\t\t+ QString::number(++index)\n\t\t\t+ \".jpg\";\n\t\treturn result;\n\t};\n\n\tauto userpics = Data::UserpicsInfo();\n\tuserpics.count = 3;\n\tauto userpicsSlice1 = Data::UserpicsSlice();\n\tuserpicsSlice1.list.push_back(generatePhoto());\n\tuserpicsSlice1.list.push_back(generatePhoto());\n\tauto userpicsSlice2 = Data::UserpicsSlice();\n\tuserpicsSlice2.list.push_back(generatePhoto());\n\tcheck(writeUserpicsStart(userpics));\n\tcheck(writeUserpicsSlice(userpicsSlice1));\n\tcheck(writeUserpicsSlice(userpicsSlice2));\n\tcheck(writeUserpicsEnd());\n\n\tauto contacts = Data::ContactsList();\n\tauto topUser = Data::TopPeer();\n\tauto user = personal.user;\n\tauto peerUser = Data::Peer{ user };\n\ttopUser.peer = peerUser;\n\ttopUser.rating = 0.5;\n\tauto topChat = Data::TopPeer();\n\tauto chat = Data::Chat();\n\tchat.bareId = counter();\n\tchat.title = \"Group chat\";\n\tauto peerChat = Data::Peer{ chat };\n\ttopChat.peer = peerChat;\n\ttopChat.rating = 0.25;\n\tauto topBot = Data::TopPeer();\n\tauto bot = Data::User();\n\tbot.info.date = date();\n\tbot.isBot = true;\n\tbot.info.firstName = \"Bot\";\n\tbot.info.lastName = \"Father\";\n\tbot.info.userId = counter();\n\tbot.username = \"botfather\";\n\tauto peerBot = Data::Peer{ bot };\n\ttopBot.peer = peerBot;\n\ttopBot.rating = 0.125;\n\n\tauto peers = std::map<PeerId, Data::Peer>();\n\tpeers.emplace(peerUser.id(), peerUser);\n\tpeers.emplace(peerBot.id(), peerBot);\n\tpeers.emplace(peerChat.id(), peerChat);\n\n\tcontacts.correspondents.push_back(topUser);\n\tcontacts.correspondents.push_back(topChat);\n\tcontacts.inlineBots.push_back(topBot);\n\tcontacts.inlineBots.push_back(topBot);\n\tcontacts.phoneCalls.push_back(topUser);\n\tcontacts.list.push_back(user.info);\n\tcontacts.list.push_back(bot.info);\n\n\tcheck(writeContactsList(contacts));\n\n\tauto sessions = Data::SessionsList();\n\tauto session = Data::Session();\n\tsession.applicationName = \"Telegram Desktop\";\n\tsession.applicationVersion = \"1.3.8\";\n\tsession.country = \"GB\";\n\tsession.created = date();\n\tsession.deviceModel = \"PC\";\n\tsession.ip = \"127.0.0.1\";\n\tsession.lastActive = date();\n\tsession.platform = \"Windows\";\n\tsession.region = \"London\";\n\tsession.systemVersion = \"10\";\n\tsessions.list.push_back(session);\n\tsessions.list.push_back(session);\n\tauto webSession = Data::WebSession();\n\twebSession.botUsername = \"botfather\";\n\twebSession.browser = \"Google Chrome\";\n\twebSession.created = date();\n\twebSession.domain = \"telegram.org\";\n\twebSession.ip = \"127.0.0.1\";\n\twebSession.lastActive = date();\n\twebSession.platform = \"Windows\";\n\twebSession.region = \"London, GB\";\n\tsessions.webList.push_back(webSession);\n\tsessions.webList.push_back(webSession);\n\tcheck(writeSessionsList(sessions));\n\n\tauto sampleMessage = [&] {\n\t\tauto message = Data::Message();\n\t\tmessage.id = counter();\n\t\tmessage.date = prevdate();\n\t\tmessage.edited = date();\n\t\tstatic auto count = 0;\n\t\tif (++count % 3 == 0) {\n\t\t\tmessage.forwardedFromId = peerFromUser(user.info.userId);\n\t\t\tmessage.forwardedDate = date();\n\t\t} else if (count % 3 == 2) {\n\t\t\tmessage.forwardedFromName = \"Test hidden forward\";\n\t\t\tmessage.forwardedDate = date();\n\t\t}\n\t\tmessage.fromId = user.info.userId;\n\t\tmessage.replyToMsgId = counter();\n\t\tmessage.viaBotId = bot.info.userId;\n\t\tmessage.text.push_back(Data::TextPart{\n\t\t\tData::TextPart::Type::Text,\n\t\t\t(\"Text message \" + QString::number(counter())).toUtf8()\n\t\t});\n\t\treturn message;\n\t};\n\tauto sliceBot1 = Data::MessagesSlice();\n\tsliceBot1.peers = peers;\n\tsliceBot1.list.push_back(sampleMessage());\n\tsliceBot1.list.push_back([&] {\n\t\tauto message = sampleMessage();\n\t\tmessage.media.content = generatePhoto();\n\t\tmessage.media.ttl = counter();\n\t\treturn message;\n\t}());\n\tsliceBot1.list.push_back([&] {\n\t\tauto message = sampleMessage();\n\t\tauto document = Data::Document();\n\t\tdocument.date = prevdate();\n\t\tdocument.duration = counter();\n\t\tauto photo = generatePhoto();\n\t\tdocument.file = photo.image.file;\n\t\tdocument.width = photo.image.width;\n\t\tdocument.height = photo.image.height;\n\t\tdocument.id = counter();\n\t\tmessage.media.content = document;\n\t\treturn message;\n\t}());\n\tsliceBot1.list.push_back([&] {\n\t\tauto message = sampleMessage();\n\t\tauto contact = Data::SharedContact();\n\t\tcontact.info = user.info;\n\t\tmessage.media.content = contact;\n\t\treturn message;\n\t}());\n\tauto sliceBot2 = Data::MessagesSlice();\n\tsliceBot2.peers = peers;\n\tsliceBot2.list.push_back([&] {\n\t\tauto message = sampleMessage();\n\t\tauto point = Data::GeoPoint();\n\t\tpoint.latitude = 1.5;\n\t\tpoint.longitude = 2.8;\n\t\tpoint.valid = true;\n\t\tmessage.media.content = point;\n\t\tmessage.media.ttl = counter();\n\t\treturn message;\n\t}());\n\tsliceBot2.list.push_back([&] {\n\t\tauto message = sampleMessage();\n\t\tmessage.replyToMsgId = sliceBot1.list.back().id;\n\t\tauto venue = Data::Venue();\n\t\tvenue.point.latitude = 1.5;\n\t\tvenue.point.longitude = 2.8;\n\t\tvenue.point.valid = true;\n\t\tvenue.address = \"Test address\";\n\t\tvenue.title = \"Test venue\";\n\t\tmessage.media.content = venue;\n\t\treturn message;\n\t}());\n\tsliceBot2.list.push_back([&] {\n\t\tauto message = sampleMessage();\n\t\tauto game = Data::Game();\n\t\tgame.botId = bot.info.userId;\n\t\tgame.title = \"Test game\";\n\t\tgame.description = \"Test game description\";\n\t\tgame.id = counter();\n\t\tgame.shortName = \"testgame\";\n\t\tmessage.media.content = game;\n\t\treturn message;\n\t}());\n\tsliceBot2.list.push_back([&] {\n\t\tauto message = sampleMessage();\n\t\tauto invoice = Data::Invoice();\n\t\tinvoice.amount = counter();\n\t\tinvoice.currency = \"GBP\";\n\t\tinvoice.title = \"Huge invoice.\";\n\t\tinvoice.description = \"So money.\";\n\t\tinvoice.receiptMsgId = sliceBot2.list.front().id;\n\t\tmessage.media.content = invoice;\n\t\treturn message;\n\t}());\n\tauto serviceMessage = [&] {\n\t\tauto message = Data::Message();\n\t\tmessage.id = counter();\n\t\tmessage.date = prevdate();\n\t\tmessage.fromId = user.info.userId;\n\t\treturn message;\n\t};\n\tauto sliceChat1 = Data::MessagesSlice();\n\tsliceChat1.peers = peers;\n\tsliceChat1.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionChatCreate();\n\t\taction.title = \"Test chat\";\n\t\taction.userIds.push_back(user.info.userId);\n\t\taction.userIds.push_back(bot.info.userId);\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat1.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionChatEditTitle();\n\t\taction.title = \"New title\";\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat1.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionChatEditPhoto();\n\t\taction.photo = generatePhoto();\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat1.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionChatDeletePhoto();\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat1.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionChatAddUser();\n\t\taction.userIds.push_back(user.info.userId);\n\t\taction.userIds.push_back(bot.info.userId);\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat1.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionChatDeleteUser();\n\t\taction.userId = bot.info.userId;\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat1.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionChatJoinedByLink();\n\t\taction.inviterId = bot.info.userId;\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat1.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionChannelCreate();\n\t\taction.title = \"Channel name\";\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat1.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionChatMigrateTo();\n\t\taction.channelId = ChannelId(chat.bareId);\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat1.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionChannelMigrateFrom();\n\t\taction.chatId = ChatId(chat.bareId);\n\t\taction.title = \"Supergroup now\";\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tauto sliceChat2 = Data::MessagesSlice();\n\tsliceChat2.peers = peers;\n\tsliceChat2.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionPinMessage();\n\t\tmessage.replyToMsgId = sliceChat1.list.back().id;\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat2.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionHistoryClear();\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat2.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionGameScore();\n\t\taction.score = counter();\n\t\taction.gameId = counter();\n\t\tmessage.replyToMsgId = sliceChat2.list.back().id;\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat2.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionPaymentSent();\n\t\taction.amount = counter();\n\t\taction.currency = \"GBP\";\n\t\tmessage.replyToMsgId = sliceChat2.list.front().id;\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat2.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionPhoneCall();\n\t\taction.duration = counter();\n\t\taction.state = Data::ActionPhoneCall::State::Busy;\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat2.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionScreenshotTaken();\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat2.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionCustomAction();\n\t\taction.message = \"Custom chat action.\";\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat2.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionBotAllowed();\n\t\taction.domain = \"telegram.org\";\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat2.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionSecureValuesSent();\n\t\tusing Type = Data::ActionSecureValuesSent::Type;\n\t\taction.types.push_back(Type::BankStatement);\n\t\taction.types.push_back(Type::Phone);\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tsliceChat2.list.push_back([&] {\n\t\tauto message = serviceMessage();\n\t\tauto action = Data::ActionContactSignUp();\n\t\tmessage.action.content = action;\n\t\treturn message;\n\t}());\n\tauto dialogs = Data::DialogsInfo();\n\tauto dialogBot = Data::DialogInfo();\n\tdialogBot.messagesCountPerSplit.push_back(sliceBot1.list.size());\n\tdialogBot.messagesCountPerSplit.push_back(sliceBot2.list.size());\n\tdialogBot.type = Data::DialogInfo::Type::Bot;\n\tdialogBot.name = peerBot.name();\n\tdialogBot.onlyMyMessages = false;\n\tdialogBot.peerId = peerBot.id();\n\tdialogBot.relativePath = \"chats/chat_\"\n\t\t+ QString::number(counter())\n\t\t+ '/';\n\tdialogBot.splits.push_back(0);\n\tdialogBot.splits.push_back(1);\n\tdialogBot.topMessageDate = sliceBot2.list.back().date;\n\tdialogBot.topMessageId = sliceBot2.list.back().id;\n\tauto dialogChat = Data::DialogInfo();\n\tdialogChat.messagesCountPerSplit.push_back(sliceChat1.list.size());\n\tdialogChat.messagesCountPerSplit.push_back(sliceChat2.list.size());\n\tdialogChat.type = Data::DialogInfo::Type::PrivateGroup;\n\tdialogChat.name = peerChat.name();\n\tdialogChat.onlyMyMessages = true;\n\tdialogChat.peerId = peerChat.id();\n\tdialogChat.relativePath = \"chats/chat_\"\n\t\t+ QString::number(counter())\n\t\t+ '/';\n\tdialogChat.splits.push_back(0);\n\tdialogChat.splits.push_back(1);\n\tdialogChat.topMessageDate = sliceChat2.list.back().date;\n\tdialogChat.topMessageId = sliceChat2.list.back().id;\n\tdialogs.chats.push_back(dialogBot);\n\tdialogs.chats.push_back(dialogChat);\n\n\tcheck(writeDialogsStart(dialogs));\n\tcheck(writeDialogStart(dialogBot));\n\tcheck(writeDialogSlice(sliceBot1));\n\tcheck(writeDialogSlice(sliceBot2));\n\tcheck(writeDialogEnd());\n\tcheck(writeDialogStart(dialogChat));\n\tcheck(writeDialogSlice(sliceChat1));\n\tcheck(writeDialogSlice(sliceChat2));\n\tcheck(writeDialogEnd());\n\tcheck(writeDialogsEnd());\n\n\tcheck(finish());\n\n\treturn result;\n}\n\n} // namespace Output\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/output/export_output_abstract.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QString>\n\nnamespace Export {\nnamespace Data {\nstruct PersonalInfo;\nstruct UserpicsInfo;\nstruct UserpicsSlice;\nstruct StoriesInfo;\nstruct StoriesSlice;\nstruct ProfileMusicInfo;\nstruct ProfileMusicSlice;\nstruct ContactsList;\nstruct SessionsList;\nstruct DialogsInfo;\nstruct DialogInfo;\nstruct MessagesSlice;\nstruct File;\n} // namespace Data\n\nstruct Settings;\nstruct Environment;\n\nnamespace Output {\n\nQString NormalizePath(const Settings &settings);\n\nstruct Result;\nclass Stats;\n\nenum class Format {\n\tHtml,\n\tJson,\n\tHtmlAndJson,\n};\n\nclass AbstractWriter {\npublic:\n\t[[nodiscard]] virtual Format format() = 0;\n\n\t[[nodiscard]] virtual Result start(\n\t\tconst Settings &settings,\n\t\tconst Environment &environment,\n\t\tStats *stats) = 0;\n\n\t[[nodiscard]] virtual Result writePersonal(\n\t\tconst Data::PersonalInfo &data) = 0;\n\n\t[[nodiscard]] virtual Result writeUserpicsStart(\n\t\tconst Data::UserpicsInfo &data) = 0;\n\t[[nodiscard]] virtual Result writeUserpicsSlice(\n\t\tconst Data::UserpicsSlice &data) = 0;\n\t[[nodiscard]] virtual Result writeUserpicsEnd() = 0;\n\n\t[[nodiscard]] virtual Result writeStoriesStart(\n\t\tconst Data::StoriesInfo &data) = 0;\n\t[[nodiscard]] virtual Result writeStoriesSlice(\n\t\tconst Data::StoriesSlice &data) = 0;\n\t[[nodiscard]] virtual Result writeStoriesEnd() = 0;\n\n\t[[nodiscard]] virtual Result writeProfileMusicStart(\n\t\tconst Data::ProfileMusicInfo &data) = 0;\n\t[[nodiscard]] virtual Result writeProfileMusicSlice(\n\t\tconst Data::ProfileMusicSlice &data) = 0;\n\t[[nodiscard]] virtual Result writeProfileMusicEnd() = 0;\n\n\t[[nodiscard]] virtual Result writeContactsList(\n\t\tconst Data::ContactsList &data) = 0;\n\n\t[[nodiscard]] virtual Result writeSessionsList(\n\t\tconst Data::SessionsList &data) = 0;\n\n\t[[nodiscard]] virtual Result writeOtherData(\n\t\tconst Data::File &data) = 0;\n\n\t[[nodiscard]] virtual Result writeDialogsStart(\n\t\tconst Data::DialogsInfo &data) = 0;\n\t[[nodiscard]] virtual Result writeDialogStart(\n\t\tconst Data::DialogInfo &data) = 0;\n\t[[nodiscard]] virtual Result writeDialogSlice(\n\t\tconst Data::MessagesSlice &data) = 0;\n\t[[nodiscard]] virtual Result writeDialogEnd() = 0;\n\t[[nodiscard]] virtual Result writeDialogsEnd() = 0;\n\n\t[[nodiscard]] virtual Result finish() = 0;\n\n\t[[nodiscard]] virtual QString mainFilePath() = 0;\n\n\tvirtual ~AbstractWriter() = default;\n\n\tStats produceTestExample(\n\t\tconst QString &path,\n\t\tconst Environment &environment);\n\n};\n\nstd::unique_ptr<AbstractWriter> CreateWriter(Format format);\n\n} // namespace Output\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/output/export_output_file.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/output/export_output_file.h\"\n\n#include \"export/output/export_output_result.h\"\n#include \"export/output/export_output_stats.h\"\n#include \"base/qt/qt_string_view.h\"\n\n#include <QtCore/QFileInfo>\n#include <QtCore/QDir>\n\n#include <gsl/util>\n\nnamespace Export {\nnamespace Output {\n\nFile::File(const QString &path, Stats *stats) : _path(path), _stats(stats) {\n}\n\nint64 File::size() const {\n\treturn _offset;\n}\n\nbool File::empty() const {\n\treturn !_offset;\n}\n\nResult File::writeBlock(const QByteArray &block) {\n\tconst auto result = writeBlockAttempt(block);\n\tif (!result) {\n\t\t_file.reset();\n\t}\n\treturn result;\n}\n\nResult File::writeBlockAttempt(const QByteArray &block) {\n\tif (_stats && !_inStats) {\n\t\t_inStats = true;\n\t\t_stats->incrementFiles();\n\t}\n\tif (const auto result = reopen(); !result) {\n\t\treturn result;\n\t}\n\tconst auto size = block.size();\n\tif (!size) {\n\t\treturn Result::Success();\n\t}\n\tif (_file->write(block) == size && _file->flush()) {\n\t\t_offset += size;\n\t\tif (_stats) {\n\t\t\t_stats->incrementBytes(size);\n\t\t}\n\t\treturn Result::Success();\n\t}\n\treturn error();\n}\n\nResult File::reopen() {\n\tif (_file && _file->isOpen()) {\n\t\treturn Result::Success();\n\t}\n\t_file.emplace(_path);\n\tif (_file->exists()) {\n\t\tif (_file->size() < _offset) {\n\t\t\treturn fatalError();\n\t\t} else if (!_file->resize(_offset)) {\n\t\t\treturn error();\n\t\t}\n\t} else if (_offset > 0) {\n\t\treturn fatalError();\n\t}\n\tif (_file->open(QIODevice::Append)) {\n\t\treturn Result::Success();\n\t}\n\tconst auto info = QFileInfo(_path);\n\tconst auto dir = info.absoluteDir();\n\treturn (!dir.exists()\n\t\t&& dir.mkpath(dir.absolutePath())\n\t\t&& _file->open(QIODevice::Append))\n\t\t? Result::Success()\n\t\t: error();\n}\n\nResult File::error() const {\n\treturn Result(Result::Type::Error, _path);\n}\n\nResult File::fatalError() const {\n\treturn Result(Result::Type::FatalError, _path);\n}\n\nQString File::PrepareRelativePath(\n\t\tconst QString &folder,\n\t\tconst QString &suggested) {\n\tif (!QFile::exists(folder + suggested)) {\n\t\treturn suggested;\n\t}\n\n\t// Not lastIndexOf('.') so that \"file.tar.xz\" won't be messed up.\n\tconst auto position = suggested.indexOf('.');\n\tconst auto base = suggested.mid(0, position);\n\tconst auto extension = (position >= 0)\n\t\t? base::StringViewMid(suggested, position)\n\t\t: QStringView();\n\tconst auto relativePart = [&](int attempt) {\n\t\tauto result = base + QString(\" (%1)\").arg(attempt);\n\t\tresult.append(extension);\n\t\treturn result;\n\t};\n\tauto attempt = 0;\n\twhile (true) {\n\t\tconst auto relativePath = relativePart(++attempt);\n\t\tif (!QFile::exists(folder + relativePath)) {\n\t\t\treturn relativePath;\n\t\t}\n\t}\n}\n\nResult File::Copy(\n\t\tconst QString &source,\n\t\tconst QString &path,\n\t\tStats *stats) {\n\tQFile f(source);\n\tif (!f.exists() || !f.open(QIODevice::ReadOnly)) {\n\t\treturn Result(Result::Type::FatalError, source);\n\t}\n\tconst auto bytes = f.readAll();\n\tif (bytes.size() != f.size()) {\n\t\treturn Result(Result::Type::FatalError, source);\n\t}\n\treturn File(path, stats).writeBlock(bytes);\n}\n\n} // namespace Output\n} // namespace File\n"
  },
  {
    "path": "Telegram/SourceFiles/export/output/export_output_file.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/optional.h\"\n\n#include <QtCore/QFile>\n#include <QtCore/QString>\n#include <QtCore/QByteArray>\n\nnamespace Export {\nnamespace Output {\n\nstruct Result;\nclass Stats;\n\nclass File {\npublic:\n\tFile(const QString &path, Stats *stats);\n\n\t[[nodiscard]] int64 size() const;\n\t[[nodiscard]] bool empty() const;\n\n\t[[nodiscard]] Result writeBlock(const QByteArray &block);\n\n\t[[nodiscard]] static QString PrepareRelativePath(\n\t\tconst QString &folder,\n\t\tconst QString &suggested);\n\n\t[[nodiscard]] static Result Copy(\n\t\tconst QString &source,\n\t\tconst QString &path,\n\t\tStats *stats);\n\nprivate:\n\t[[nodiscard]] Result reopen();\n\t[[nodiscard]] Result writeBlockAttempt(const QByteArray &block);\n\n\t[[nodiscard]] Result error() const;\n\t[[nodiscard]] Result fatalError() const;\n\n\tQString _path;\n\tint64 _offset = 0;\n\tstd::optional<QFile> _file;\n\n\tStats *_stats = nullptr;\n\tbool _inStats = false;\n\n};\n\n} // namespace Output\n} // namespace File\n"
  },
  {
    "path": "Telegram/SourceFiles/export/output/export_output_html.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/output/export_output_html.h\"\n\n#include \"countries/countries_instance.h\"\n#include \"export/output/export_output_result.h\"\n#include \"export/data/export_data_types.h\"\n#include \"core/utils.h\"\n#include \"ui/text/format_values.h\"\n\n#include <QtCore/QSize>\n#include <QtCore/QFile>\n#include <QtCore/QDateTime>\n\nnamespace Export {\nnamespace Output {\nnamespace {\n\nconstexpr auto kMessagesInFile = 1000;\nconstexpr auto kPersonalUserpicSize = 90;\nconstexpr auto kEntryUserpicSize = 48;\nconstexpr auto kServiceMessagePhotoSize = 60;\nconstexpr auto kHistoryUserpicSize = 42;\nconstexpr auto kSavedMessagesColorIndex = uint8(3);\nconstexpr auto kJoinWithinSeconds = 900;\nconstexpr auto kPhotoMaxWidth = 520;\nconstexpr auto kPhotoMaxHeight = 520;\nconstexpr auto kPhotoMinWidth = 80;\nconstexpr auto kPhotoMinHeight = 80;\nconstexpr auto kStickerMaxWidth = 384;\nconstexpr auto kStickerMaxHeight = 384;\nconstexpr auto kStickerMinWidth = 80;\nconstexpr auto kStickerMinHeight = 80;\nconstexpr auto kStoryThumbWidth = 45;\nconstexpr auto kStoryThumbHeight = 80;\n\nconstexpr auto kChatsPriority = 0;\nconstexpr auto kContactsPriority = 2;\nconstexpr auto kFrequentContactsPriority = 3;\nconstexpr auto kUserpicsPriority = 4;\nconstexpr auto kStoriesPriority = 5;\nconstexpr auto kProfileMusicPriority = 6;\nconstexpr auto kSessionsPriority = 7;\nconstexpr auto kWebSessionsPriority = 8;\nconstexpr auto kOtherPriority = 9;\n\nconst auto kLineBreak = QByteArrayLiteral(\"<br>\");\n\nusing Context = details::HtmlContext;\nusing UserpicData = details::UserpicData;\nusing StoryData = details::StoryData;\nusing PeersMap = details::PeersMap;\nusing MediaData = details::MediaData;\n\nbool IsGlobalLink(const QString &link) {\n\treturn link.startsWith(u\"http://\"_q, Qt::CaseInsensitive)\n\t\t|| link.startsWith(u\"https://\"_q, Qt::CaseInsensitive);\n}\n\nQByteArray NoFileDescription(Data::File::SkipReason reason) {\n\tusing SkipReason = Data::File::SkipReason;\n\tswitch (reason) {\n\tcase SkipReason::Unavailable:\n\t\treturn \"Unavailable, please try again later.\";\n\tcase SkipReason::FileSize:\n\t\treturn \"Exceeds maximum size, \"\n\t\t\t\"change data exporting settings to download.\";\n\tcase SkipReason::FileType:\n\t\treturn \"Not included, \"\n\t\t\t\"change data exporting settings to download.\";\n\tcase SkipReason::None:\n\t\treturn \"\";\n\t}\n\tUnexpected(\"Skip reason in NoFileDescription.\");\n}\n\nauto CalculateThumbSize(\n\t\tint maxWidth,\n\t\tint maxHeight,\n\t\tint minWidth,\n\t\tint minHeight,\n\t\tbool expandForRetina = false) {\n\treturn [=](QSize largeSize) {\n\t\tconst auto multiplier = (expandForRetina ? 2 : 1);\n\t\tconst auto checkWidth = largeSize.width() * multiplier;\n\t\tconst auto checkHeight = largeSize.height() * multiplier;\n\t\tconst auto smallSize = (checkWidth > maxWidth\n\t\t\t|| checkHeight > maxHeight)\n\t\t\t? largeSize.scaled(\n\t\t\t\tmaxWidth,\n\t\t\t\tmaxHeight,\n\t\t\t\tQt::KeepAspectRatio)\n\t\t\t: largeSize;\n\t\tconst auto retinaSize = QSize(\n\t\t\tsmallSize.width() & ~0x01,\n\t\t\tsmallSize.height() & ~0x01);\n\t\treturn (retinaSize.width() < kPhotoMinWidth\n\t\t\t|| retinaSize.height() < kPhotoMinHeight)\n\t\t\t? QSize()\n\t\t\t: retinaSize;\n\t};\n}\n\nQByteArray SerializeString(const QByteArray &value) {\n\tconst auto size = value.size();\n\tconst auto begin = value.data();\n\tconst auto end = begin + size;\n\n\tauto result = QByteArray();\n\tresult.reserve(size * 6);\n\tfor (auto p = begin; p != end; ++p) {\n\t\tconst auto ch = *p;\n\t\tif (ch == '\\n') {\n\t\t\tresult.append(\"<br>\", 4);\n\t\t} else if (ch == '\"') {\n\t\t\tresult.append(\"&quot;\", 6);\n\t\t} else if (ch == '&') {\n\t\t\tresult.append(\"&amp;\", 5);\n\t\t} else if (ch == '\\'') {\n\t\t\tresult.append(\"&apos;\", 6);\n\t\t} else if (ch == '<') {\n\t\t\tresult.append(\"&lt;\", 4);\n\t\t} else if (ch == '>') {\n\t\t\tresult.append(\"&gt;\", 4);\n\t\t} else if (ch >= 0 && ch < 32) {\n\t\t\tresult.append(\"&#x\", 3).append('0' + (ch >> 4));\n\t\t\tconst auto left = (ch & 0x0F);\n\t\t\tif (left >= 10) {\n\t\t\t\tresult.append('A' + (left - 10));\n\t\t\t} else {\n\t\t\t\tresult.append('0' + left);\n\t\t\t}\n\t\t\tresult.append(';');\n\t\t} else if (ch == char(0xE2)\n\t\t\t&& (p + 2 < end)\n\t\t\t&& *(p + 1) == char(0x80)) {\n\t\t\tif (*(p + 2) == char(0xA8)) { // Line separator.\n\t\t\t\tresult.append(\"<br>\", 4);\n\t\t\t} else if (*(p + 2) == char(0xA9)) { // Paragraph separator.\n\t\t\t\tresult.append(\"<br>\", 4);\n\t\t\t} else {\n\t\t\t\tresult.append(ch);\n\t\t\t}\n\t\t} else {\n\t\t\tresult.append(ch);\n\t\t}\n\t}\n\treturn result;\n}\n\nQByteArray SerializeList(const std::vector<QByteArray> &values) {\n\tconst auto count = values.size();\n\tif (count == 1) {\n\t\treturn values[0];\n\t} else if (count > 1) {\n\t\tauto result = values[0];\n\t\tfor (auto i = 1; i != count - 1; ++i) {\n\t\t\tresult += \", \" + values[i];\n\t\t}\n\t\treturn result + \" and \" + values[count - 1];\n\t}\n\treturn QByteArray();\n}\n\nQByteArray MakeLinks(const QByteArray &value) {\n\tconst auto domain = QByteArray(\"https://telegram.org/\");\n\tauto result = QByteArray();\n\tauto offset = 0;\n\twhile (true) {\n\t\tconst auto start = value.indexOf(domain, offset);\n\t\tif (start < 0) {\n\t\t\tbreak;\n\t\t}\n\t\tauto end = start + domain.size();\n\t\tfor (; end != value.size(); ++end) {\n\t\t\tconst auto ch = value[end];\n\t\t\tif ((ch < 'a' || ch > 'z')\n\t\t\t\t&& (ch < 'A' || ch > 'Z')\n\t\t\t\t&& (ch < '0' || ch > '9')\n\t\t\t\t&& (ch != '-')\n\t\t\t\t&& (ch != '_')\n\t\t\t\t&& (ch != '/')) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (start > offset) {\n\t\t\tconst auto link = value.mid(start, end - start);\n\t\t\tresult.append(value.mid(offset, start - offset));\n\t\t\tresult.append(\"<a href=\\\"\").append(link).append(\"\\\">\");\n\t\t\tresult.append(link);\n\t\t\tresult.append(\"</a>\");\n\t\t\toffset = end;\n\t\t}\n\t}\n\tif (result.isEmpty()) {\n\t\treturn value;\n\t}\n\tif (offset < value.size()) {\n\t\tresult.append(value.mid(offset));\n\t}\n\treturn result;\n}\n\nQByteArray JoinList(\n\t\tconst QByteArray &separator,\n\t\tconst std::vector<QByteArray> &list) {\n\tif (list.empty()) {\n\t\treturn QByteArray();\n\t} else if (list.size() == 1) {\n\t\treturn list[0];\n\t}\n\tauto size = (list.size() - 1) * separator.size();\n\tfor (const auto &value : list) {\n\t\tsize += value.size();\n\t}\n\tauto result = QByteArray();\n\tresult.reserve(size);\n\tauto counter = 0;\n\twhile (true) {\n\t\tresult.append(list[counter]);\n\t\tif (++counter == list.size()) {\n\t\t\tbreak;\n\t\t} else {\n\t\t\tresult.append(separator);\n\t\t}\n\t}\n\treturn result;\n}\n\nQByteArray FormatCustomEmoji(\n\t\tconst Data::Utf8String &custom_emoji,\n\t\tconst QByteArray &text,\n\t\tconst QString &relativeLinkBase) {\n\treturn (custom_emoji.isEmpty()\n\t\t? \"<a href=\\\"\\\" onclick=\\\"return ShowNotLoadedEmoji();\\\">\"\n\t\t: (custom_emoji == Data::TextPart::UnavailableEmoji())\n\t\t? \"<a href=\\\"\\\" onclick=\\\"return ShowNotAvailableEmoji();\\\">\"\n\t\t: (\"<a href = \\\"\"\n\t\t\t+ (relativeLinkBase + custom_emoji).toUtf8()\n\t\t\t+ \"\\\">\"))\n\t\t+ text\n\t\t+ \"</a>\";\n}\n\nQByteArray FormatText(\n\t\tconst std::vector<Data::TextPart> &data,\n\t\tconst QString &internalLinksDomain,\n\t\tconst QString &relativeLinkBase) {\n\treturn JoinList(QByteArray(), ranges::views::all(\n\t\tdata\n\t) | ranges::views::transform([&](const Data::TextPart &part) {\n\t\tconst auto text = SerializeString(part.text);\n\t\tusing Type = Data::TextPart::Type;\n\t\tswitch (part.type) {\n\t\tcase Type::Text: return text;\n\t\tcase Type::Unknown: return text;\n\t\tcase Type::Mention:\n\t\t\treturn \"<a href=\\\"\"\n\t\t\t\t+ internalLinksDomain.toUtf8()\n\t\t\t\t+ text.mid(1)\n\t\t\t\t+ \"\\\">\" + text + \"</a>\";\n\t\tcase Type::Hashtag: return \"<a href=\\\"\\\" \"\n\t\t\t\"onclick=\\\"return ShowHashtag(\"\n\t\t\t+ SerializeString('\"' + text.mid(1) + '\"')\n\t\t\t+ \")\\\">\" + text + \"</a>\";\n\t\tcase Type::BotCommand: return \"<a href=\\\"\\\" \"\n\t\t\t\"onclick=\\\"return ShowBotCommand(\"\n\t\t\t+ SerializeString('\"' + text.mid(1) + '\"')\n\t\t\t+ \")\\\">\" + text + \"</a>\";\n\t\tcase Type::Url: return \"<a href=\\\"\"\n\t\t\t+ text\n\t\t\t+ \"\\\">\" + text + \"</a>\";\n\t\tcase Type::Email: return \"<a href=\\\"mailto:\"\n\t\t\t+ text\n\t\t\t+ \"\\\">\" + text + \"</a>\";\n\t\tcase Type::Bold: return \"<strong>\" + text + \"</strong>\";\n\t\tcase Type::Italic: return \"<em>\" + text + \"</em>\";\n\t\tcase Type::Code: return \"<code>\" + text + \"</code>\";\n\t\tcase Type::Pre: return \"<pre>\" + text + \"</pre>\";\n\t\tcase Type::TextUrl: return \"<a href=\\\"\"\n\t\t\t+ SerializeString(part.additional)\n\t\t\t+ \"\\\">\" + text + \"</a>\";\n\t\tcase Type::MentionName: return \"<a href=\\\"\\\" \"\n\t\t\t\"onclick=\\\"return ShowMentionName()\\\">\" + text + \"</a>\";\n\t\tcase Type::Phone: return \"<a href=\\\"tel:\"\n\t\t\t+ text\n\t\t\t+ \"\\\">\" + text + \"</a>\";\n\t\tcase Type::Cashtag: return \"<a href=\\\"\\\" \"\n\t\t\t\"onclick=\\\"return ShowCashtag(\"\n\t\t\t+ SerializeString('\"' + text.mid(1) + '\"')\n\t\t\t+ \")\\\">\" + text + \"</a>\";\n\t\tcase Type::Underline: return \"<u>\" + text + \"</u>\";\n\t\tcase Type::Strike: return \"<s>\" + text + \"</s>\";\n\t\tcase Type::Blockquote:\n\t\t\treturn \"<blockquote>\" + text + \"</blockquote>\";\n\t\tcase Type::BankCard:\n\t\t\treturn text;\n\t\tcase Type::Spoiler: return \"<span class=\\\"spoiler hidden\\\" \"\n\t\t\t\"onclick=\\\"ShowSpoiler(this)\\\">\"\n\t\t\t\"<span aria-hidden=\\\"true\\\">\"\n\t\t\t+ text + \"</span></span>\";\n\t\tcase Type::CustomEmoji:\n\t\t\treturn FormatCustomEmoji(part.additional, text, relativeLinkBase);\n\t\t}\n\t\tUnexpected(\"Type in text entities serialization.\");\n\t}) | ranges::to_vector);\n}\n\nData::Utf8String FormatUsername(const Data::Utf8String &username) {\n\treturn username.isEmpty() ? username : ('@' + username);\n}\n\nbool DisplayDate(TimeId date, TimeId previousDate) {\n\tif (!previousDate) {\n\t\treturn true;\n\t}\n\treturn QDateTime::fromSecsSinceEpoch(date).date()\n\t\t!= QDateTime::fromSecsSinceEpoch(previousDate).date();\n}\n\nQByteArray FormatDateText(TimeId date) {\n\tconst auto parsed = QDateTime::fromSecsSinceEpoch(date).date();\n\tconst auto month = [](int index) {\n\t\tswitch (index) {\n\t\tcase 1: return \"January\";\n\t\tcase 2: return \"February\";\n\t\tcase 3: return \"March\";\n\t\tcase 4: return \"April\";\n\t\tcase 5: return \"May\";\n\t\tcase 6: return \"June\";\n\t\tcase 7: return \"July\";\n\t\tcase 8: return \"August\";\n\t\tcase 9: return \"September\";\n\t\tcase 10: return \"October\";\n\t\tcase 11: return \"November\";\n\t\tcase 12: return \"December\";\n\t\t}\n\t\treturn \"Unknown\";\n\t};\n\treturn Data::NumberToString(parsed.day())\n\t\t+ ' '\n\t\t+ month(parsed.month())\n\t\t+ ' '\n\t\t+ Data::NumberToString(parsed.year());\n}\n\nQByteArray FormatTimeText(TimeId date) {\n\tconst auto parsed = QDateTime::fromSecsSinceEpoch(date).time();\n\treturn Data::NumberToString(parsed.hour(), 2)\n\t\t+ ':'\n\t\t+ Data::NumberToString(parsed.minute(), 2);\n}\n\n} // namespace\n\nnamespace details {\n\nstruct UserpicData {\n\tuint8 colorIndex = 0;\n\tint pixelSize = 0;\n\tQString imageLink;\n\tQString largeLink;\n\tQByteArray firstName;\n\tQByteArray lastName;\n\tQByteArray tooltip;\n};\n\nstruct StoryData {\n\tQString imageLink;\n\tQString largeLink;\n};\n\nclass PeersMap {\npublic:\n\tusing Peer = Data::Peer;\n\tusing User = Data::User;\n\tusing Chat = Data::Chat;\n\n\tPeersMap(const std::map<PeerId, Peer> &data);\n\n\tconst Peer &peer(PeerId peerId) const;\n\tconst User &user(UserId userId) const;\n\n\tQByteArray wrapPeerName(PeerId peerId) const;\n\tQByteArray wrapUserName(UserId userId) const;\n\tQByteArray wrapUserNames(const std::vector<UserId> &data) const;\n\nprivate:\n\tconst std::map<PeerId, Data::Peer> &_data;\n\n};\n\nstruct MediaData {\n\tQByteArray title;\n\tQByteArray description;\n\tQByteArray status;\n\tQByteArray classes;\n\tQString thumb;\n\tQString link;\n};\n\nPeersMap::PeersMap(const std::map<PeerId, Peer> &data) : _data(data) {\n}\n\nauto PeersMap::peer(PeerId peerId) const -> const Peer & {\n\tif (const auto i = _data.find(peerId); i != end(_data)) {\n\t\treturn i->second;\n\t}\n\tstatic auto empty = Peer{ User() };\n\treturn empty;\n}\n\nauto PeersMap::user(UserId userId) const -> const User & {\n\tif (const auto result = peer(peerFromUser(userId)).user()) {\n\t\treturn *result;\n\t}\n\tstatic auto empty = User();\n\treturn empty;\n}\n\nQByteArray PeersMap::wrapPeerName(PeerId peerId) const {\n\tconst auto result = peer(peerId).name();\n\treturn result.isEmpty()\n\t\t? QByteArray(\"Deleted\")\n\t\t: SerializeString(result);\n}\n\nQByteArray PeersMap::wrapUserName(UserId userId) const {\n\tconst auto result = user(userId).name();\n\treturn result.isEmpty()\n\t\t? QByteArray(\"Deleted Account\")\n\t\t: SerializeString(result);\n}\n\nQByteArray PeersMap::wrapUserNames(const std::vector<UserId> &data) const {\n\tauto list = std::vector<QByteArray>();\n\tfor (const auto &userId : data) {\n\t\tlist.push_back(wrapUserName(userId));\n\t}\n\treturn SerializeList(list);\n}\n\nQByteArray HtmlContext::pushTag(\n\t\tconst QByteArray &tag,\n\t\tstd::map<QByteArray, QByteArray> &&attributes) {\n\tauto data = Tag();\n\tdata.name = tag;\n\tauto empty = false;\n\tauto inner = QByteArray();\n\tfor (const auto &[name, value] : attributes) {\n\t\tif (name == \"inline\") {\n\t\t\tdata.block = false;\n\t\t} else if (name == \"empty\") {\n\t\t\tempty = true;\n\t\t} else {\n\t\t\tinner.append(' ').append(name);\n\t\t\tinner.append(\"=\\\"\").append(SerializeString(value)).append(\"\\\"\");\n\t\t}\n\t}\n\tauto result = (data.block ? (\"\\n\" + indent()) : QByteArray())\n\t\t+ \"<\" + data.name + inner + (empty ? \"/\" : \"\") + \">\"\n\t\t+ (data.block ? \"\\n\" : \"\");\n\tif (!empty) {\n\t\t_tags.push_back(data);\n\t}\n\treturn result;\n}\n\nQByteArray HtmlContext::popTag() {\n\tExpects(!_tags.empty());\n\n\tconst auto data = _tags.back();\n\t_tags.pop_back();\n\treturn (data.block ? (\"\\n\" + indent()) : QByteArray())\n\t\t+ \"</\" + data.name + \">\"\n\t\t+ (data.block ? \"\\n\" : \"\");\n}\n\nQByteArray HtmlContext::indent() const {\n\treturn QByteArray(_tags.size(), ' ');\n}\n\nbool HtmlContext::empty() const {\n\treturn _tags.empty();\n}\n\n} // namespace details\n\nstruct HtmlWriter::MessageInfo {\n\tenum class Type {\n\t\tService,\n\t\tDefault,\n\t};\n\tint id = 0;\n\tType type = Type::Service;\n\tPeerId fromId = 0;\n\tUserId viaBotId = 0;\n\tTimeId date = 0;\n\tPeerId forwardedFromId = 0;\n\tQString forwardedFromName;\n\tbool forwarded = false;\n\tbool showForwardedAsOriginal = false;\n\tTimeId forwardedDate = 0;\n};\n\nclass HtmlWriter::Wrap {\npublic:\n\tWrap(const QString &path, const QString &base, Stats *stats);\n\n\t[[nodiscard]] bool empty() const;\n\n\t[[nodiscard]] QByteArray pushTag(\n\t\tconst QByteArray &tag,\n\t\tstd::map<QByteArray, QByteArray> &&attributes = {});\n\t[[nodiscard]] QByteArray popTag();\n\t[[nodiscard]] QByteArray indent() const;\n\n\t[[nodiscard]] QByteArray pushDiv(\n\t\tconst QByteArray &className,\n\t\tconst QByteArray &style = {});\n\n\t[[nodiscard]] QByteArray pushUserpic(const UserpicData &userpic);\n\t[[nodiscard]] QByteArray pushListEntry(\n\t\tconst UserpicData &userpic,\n\t\tconst QByteArray &name,\n\t\tconst QByteArray &details,\n\t\tconst QByteArray &info,\n\t\tconst QString &link = QString());\n\t[[nodiscard]] QByteArray pushStoriesListEntry(\n\t\tconst StoryData &story,\n\t\tconst QByteArray &name,\n\t\tconst QByteArrayList &details,\n\t\tconst QByteArray &info,\n\t\tconst std::vector<Data::TextPart> &caption,\n\t\tconst QString &internalLinksDomain,\n\t\tconst QString &link = QString());\n\t[[nodiscard]] QByteArray pushAudioEntry(\n\t\tconst QByteArray &name,\n\t\tconst QByteArray &info,\n\t\tconst QByteArrayList &details,\n\t\tconst QByteArray &duration,\n\t\tconst QString &link = QString());\n\t[[nodiscard]] QByteArray pushSessionListEntry(\n\t\tint apiId,\n\t\tconst QByteArray &name,\n\t\tconst QByteArray &subname,\n\t\tstd::initializer_list<QByteArray> details,\n\t\tconst QByteArray &info = QByteArray());\n\n\t[[nodiscard]] QByteArray pushHeader(\n\t\tconst QByteArray &header,\n\t\tconst QString &path = QString());\n\t[[nodiscard]] QByteArray pushSection(\n\t\tconst QByteArray &label,\n\t\tconst QByteArray &type,\n\t\tint count,\n\t\tconst QString &path);\n\t[[nodiscard]] QByteArray pushAbout(\n\t\tconst QByteArray &text,\n\t\tbool withDivider = false);\n\t[[nodiscard]] QByteArray pushServiceMessage(\n\t\tint messageId,\n\t\tconst Data::DialogInfo &dialog,\n\t\tconst QString &basePath,\n\t\tconst QByteArray &text,\n\t\tconst Data::Photo *photo = nullptr);\n\t[[nodiscard]] std::pair<MessageInfo, QByteArray> pushMessage(\n\t\tconst Data::Message &message,\n\t\tconst MessageInfo *previous,\n\t\tconst Data::DialogInfo &dialog,\n\t\tconst QString &basePath,\n\t\tconst PeersMap &peers,\n\t\tconst QString &internalLinksDomain,\n\t\tFn<QByteArray(int messageId, QByteArray text)> wrapMessageLink);\n\n\t[[nodiscard]] Result writeBlock(const QByteArray &block);\n\n\t[[nodiscard]] Result close();\n\n\t[[nodiscard]] QString relativePath(const QString &path) const;\n\t[[nodiscard]] QString relativePath(const Data::File &file) const;\n\n\t~Wrap();\n\nprivate:\n\t[[nodiscard]] QByteArray composeStart();\n\t[[nodiscard]] QByteArray pushGenericListEntry(\n\t\tconst QString &link,\n\t\tconst UserpicData &userpic,\n\t\tconst QByteArray &name,\n\t\tconst QByteArray &subname,\n\t\tstd::initializer_list<QByteArray> details,\n\t\tconst QByteArray &info);\n\n\t[[nodiscard]] bool messageNeedsWrap(\n\t\tconst Data::Message &message,\n\t\tconst MessageInfo *previous) const;\n\t[[nodiscard]] bool forwardedNeedsWrap(\n\t\tconst Data::Message &message,\n\t\tconst MessageInfo *previous) const;\n\n\t[[nodiscard]] MediaData prepareMediaData(\n\t\tconst Data::Message &message,\n\t\tconst QString &basePath,\n\t\tconst PeersMap &peers,\n\t\tconst QString &internalLinksDomain) const;\n\t[[nodiscard]] QByteArray pushMedia(\n\t\tconst Data::Message &message,\n\t\tconst QString &basePath,\n\t\tconst PeersMap &peers,\n\t\tconst QString &internalLinksDomain,\n\t\tFn<QByteArray(int messageId, QByteArray text)> wrapMessageLink);\n\t[[nodiscard]] QByteArray pushGenericMedia(const MediaData &data);\n\t[[nodiscard]] QByteArray pushStickerMedia(\n\t\tconst Data::Document &data,\n\t\tconst QString &basePath);\n\t[[nodiscard]] QByteArray pushAnimatedMedia(\n\t\tconst Data::Document &data,\n\t\tconst QString &basePath);\n\t[[nodiscard]] QByteArray pushVideoFileMedia(\n\t\tconst Data::Document &data,\n\t\tconst QString &basePath);\n\t[[nodiscard]] QByteArray pushPhotoMedia(\n\t\tconst Data::Photo &data,\n\t\tconst QString &basePath);\n\t[[nodiscard]] QByteArray pushPoll(\n\t\tconst Data::Poll &data,\n\t\tconst QString &internalLinksDomain,\n\t\tconst QString &relativeLinkBase);\n\t[[nodiscard]] QByteArray pushTodoList(\n\t\tconst Data::TodoList &data,\n\t\tconst QString &internalLinksDomain,\n\t\tconst QString &relativeLinkBase);\n\t[[nodiscard]] QByteArray pushGiveaway(\n\t\tconst PeersMap &peers,\n\t\tconst Data::GiveawayStart &data);\n\t[[nodiscard]] QByteArray pushGiveaway(\n\t\tconst PeersMap &peers,\n\t\tconst Data::GiveawayResults &data,\n\t\tFn<QByteArray(int messageId, QByteArray text)> wrapMessageLink);\n\n\tFile _file;\n\tQByteArray _composedStart;\n\tbool _closed = false;\n\tQByteArray _base;\n\tContext _context;\n\n};\n\nstruct HtmlWriter::SavedSection {\n\tint priority = 0;\n\tQByteArray label;\n\tQByteArray type;\n\tint count = 0;\n\tQString path;\n};\n\nvoid FillUserpicNames(UserpicData &data, const Data::Peer &peer) {\n\tif (peer.user()) {\n\t\tdata.firstName = peer.user()->info.firstName;\n\t\tdata.lastName = peer.user()->info.lastName;\n\t} else if (peer.chat()) {\n\t\tdata.firstName = peer.name();\n\t}\n}\n\nvoid FillUserpicNames(UserpicData &data, const QByteArray &full) {\n\tconst auto names = full.split(' ');\n\tdata.firstName = names[0];\n\tfor (auto i = 1; i != names.size(); ++i) {\n\t\tif (names[i].isEmpty()) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!data.lastName.isEmpty()) {\n\t\t\tdata.lastName.append(' ');\n\t\t}\n\t\tdata.lastName.append(names[i]);\n\t}\n}\n\nQByteArray ComposeName(const UserpicData &data, const QByteArray &empty) {\n\treturn ((data.firstName.isEmpty() && data.lastName.isEmpty())\n\t\t? empty\n\t\t: (data.firstName + ' ' + data.lastName));\n}\n\nQString WriteUserpicThumb(\n\t\tconst QString &basePath,\n\t\tconst QString &largePath,\n\t\tconst UserpicData &userpic,\n\t\tconst QString &postfix = \"_thumb\") {\n\treturn Data::WriteImageThumb(\n\t\tbasePath,\n\t\tlargePath,\n\t\tuserpic.pixelSize * 2,\n\t\tuserpic.pixelSize * 2,\n\t\tpostfix);\n}\n\nHtmlWriter::Wrap::Wrap(\n\tconst QString &path,\n\tconst QString &base,\n\tStats *stats)\n: _file(path, stats) {\n\tExpects(base.endsWith('/'));\n\tExpects(path.startsWith(base));\n\n\tconst auto left = path.mid(base.size());\n\tconst auto nesting = ranges::count(left, '/');\n\t_base = QString(\"../\").repeated(nesting).toUtf8();\n\n\t_composedStart = composeStart();\n}\n\nbool HtmlWriter::Wrap::empty() const {\n\treturn _file.empty();\n}\n\nQByteArray HtmlWriter::Wrap::pushTag(\n\t\tconst QByteArray &tag,\n\t\tstd::map<QByteArray, QByteArray> &&attributes) {\n\treturn _context.pushTag(tag, std::move(attributes));\n}\n\nQByteArray HtmlWriter::Wrap::popTag() {\n\treturn _context.popTag();\n}\n\nQByteArray HtmlWriter::Wrap::indent() const {\n\treturn _context.indent();\n}\n\nQByteArray HtmlWriter::Wrap::pushDiv(\n\t\tconst QByteArray &className,\n\t\tconst QByteArray &style) {\n\treturn style.isEmpty()\n\t\t? _context.pushTag(\"div\", { { \"class\", className } })\n\t\t: _context.pushTag(\"div\", {\n\t\t\t{ \"class\", className },\n\t\t\t{ \"style\", style }\n\t\t});\n}\n\nQByteArray HtmlWriter::Wrap::pushUserpic(const UserpicData &userpic) {\n\tconst auto size = Data::NumberToString(userpic.pixelSize) + \"px\";\n\tauto result = QByteArray();\n\tif (!userpic.largeLink.isEmpty()) {\n\t\tresult.append(pushTag(\"a\", {\n\t\t\t{ \"class\", \"userpic_link\" },\n\t\t\t{ \"href\", relativePath(userpic.largeLink).toUtf8() }\n\t\t}));\n\t}\n\tconst auto sizeStyle = \"width: \" + size + \"; height: \" + size;\n\tif (!userpic.imageLink.isEmpty()) {\n\t\tresult.append(pushTag(\"img\", {\n\t\t\t{ \"class\", \"userpic\" },\n\t\t\t{ \"style\", sizeStyle },\n\t\t\t{ \"src\", relativePath(userpic.imageLink).toUtf8() },\n\t\t\t{ \"empty\", \"\" }\n\t\t}));\n\t} else {\n\t\tresult.append(pushTag(\"div\", {\n\t\t\t{\n\t\t\t\t\"class\",\n\t\t\t\t\"userpic userpic\"\n\t\t\t\t+ Data::NumberToString(userpic.colorIndex + 1)\n\t\t\t},\n\t\t\t{ \"style\", sizeStyle }\n\t\t}));\n\t\tif (userpic.tooltip.isEmpty()) {\n\t\t\tresult.append(pushDiv(\n\t\t\t\t\"initials\",\n\t\t\t\t\"line-height: \" + size));\n\t\t} else {\n\t\t\tresult.append(pushTag(\"div\", {\n\t\t\t\t{ \"class\", \"initials\" },\n\t\t\t\t{ \"style\", \"line-height: \" + size },\n\t\t\t\t{ \"title\", userpic.tooltip },\n\t\t\t}));\n\t\t}\n\t\tauto character = [](const QByteArray &from) {\n\t\t\tconst auto utf = QString::fromUtf8(from).trimmed();\n\t\t\treturn utf.isEmpty()\n\t\t\t\t? QByteArray()\n\t\t\t\t: SerializeString(utf.mid(0, 1).toUtf8());\n\t\t};\n\t\tresult.append(character(userpic.firstName));\n\t\tresult.append(character(userpic.lastName));\n\t\tresult.append(popTag());\n\t\tresult.append(popTag());\n\t}\n\tif (!userpic.largeLink.isEmpty()) {\n\t\tresult.append(popTag());\n\t}\n\treturn result;\n}\n\nQByteArray HtmlWriter::Wrap::pushListEntry(\n\t\tconst UserpicData &userpic,\n\t\tconst QByteArray &name,\n\t\tconst QByteArray &details,\n\t\tconst QByteArray &info,\n\t\tconst QString &link) {\n\treturn pushGenericListEntry(\n\t\tlink,\n\t\tuserpic,\n\t\tname,\n\t\t{},\n\t\t{ details },\n\t\tinfo);\n}\n\nQByteArray HtmlWriter::Wrap::pushStoriesListEntry(\n\t\tconst StoryData &story,\n\t\tconst QByteArray &name,\n\t\tconst QByteArrayList &details,\n\t\tconst QByteArray &info,\n\t\tconst std::vector<Data::TextPart> &caption,\n\t\tconst QString &internalLinksDomain,\n\t\tconst QString &link) {\n\tauto result = pushDiv(\"entry clearfix\");\n\tif (!link.isEmpty()) {\n\t\tresult.append(pushTag(\"a\", {\n\t\t\t{ \"class\", \"pull_left userpic_wrap\" },\n\t\t\t{ \"href\", relativePath(link).toUtf8() + \"#allow_back\" },\n\t\t}));\n\t} else {\n\t\tresult.append(pushDiv(\"pull_left userpic_wrap\"));\n\t}\n\tif (!story.imageLink.isEmpty()) {\n\t\tconst auto sizeStyle = \"width: \"\n\t\t\t+ Data::NumberToString(kStoryThumbWidth)\n\t\t\t+ \"px; height: \"\n\t\t\t+ Data::NumberToString(kStoryThumbHeight)\n\t\t\t+ \"px\";\n\t\tresult.append(pushTag(\"img\", {\n\t\t\t{ \"class\", \"story\" },\n\t\t\t{ \"style\", sizeStyle },\n\t\t\t{ \"src\", relativePath(story.imageLink).toUtf8() },\n\t\t\t{ \"empty\", \"\" }\n\t\t}));\n\t}\n\tresult.append(popTag());\n\tresult.append(pushDiv(\"body\"));\n\tif (!info.isEmpty()) {\n\t\tresult.append(pushDiv(\"pull_right info details\"));\n\t\tresult.append(SerializeString(info));\n\t\tresult.append(popTag());\n\t}\n\tif (!name.isEmpty()) {\n\t\tif (!link.isEmpty()) {\n\t\t\tresult.append(pushTag(\"a\", {\n\t\t\t\t{ \"class\", \"block_link expanded\" },\n\t\t\t\t{ \"href\", relativePath(link).toUtf8() + \"#allow_back\" },\n\t\t\t}));\n\t\t}\n\t\tresult.append(pushDiv(\"name bold\"));\n\t\tresult.append(SerializeString(name));\n\t\tresult.append(popTag());\n\t\tif (!link.isEmpty()) {\n\t\t\tresult.append(popTag());\n\t\t}\n\t}\n\tconst auto text = caption.empty()\n\t\t? QByteArray()\n\t\t: FormatText(caption, internalLinksDomain, _base);\n\tif (!text.isEmpty()) {\n\t\tresult.append(pushDiv(\"text\"));\n\t\tresult.append(text);\n\t\tresult.append(popTag());\n\t}\n\tfor (const auto &detail : details) {\n\t\tresult.append(pushDiv(\"details_entry details\"));\n\t\tresult.append(SerializeString(detail));\n\t\tresult.append(popTag());\n\t}\n\tresult.append(popTag());\n\tresult.append(popTag());\n\treturn result;\n}\n\nQByteArray HtmlWriter::Wrap::pushAudioEntry(\n\t\tconst QByteArray &name,\n\t\tconst QByteArray &info,\n\t\tconst QByteArrayList &details,\n\t\tconst QByteArray &duration,\n\t\tconst QString &link) {\n\tauto result = pushDiv(\"entry clearfix\");\n\tif (!link.isEmpty()) {\n\t\tresult.append(pushTag(\"a\", {\n\t\t\t{ \"class\", \"pull_left userpic_wrap\" },\n\t\t\t{ \"href\", relativePath(link).toUtf8() + \"#allow_back\" },\n\t\t}));\n\t} else {\n\t\tresult.append(pushDiv(\"pull_left userpic_wrap\"));\n\t}\n\tresult.append(pushDiv(\"userpic audio_icon\"));\n\tresult.append(popTag());\n\tresult.append(popTag());\n\tresult.append(pushDiv(\"body\"));\n\tif (!duration.isEmpty()) {\n\t\tresult.append(pushDiv(\"pull_right info details\"));\n\t\tresult.append(SerializeString(duration));\n\t\tresult.append(popTag());\n\t}\n\tif (!info.isEmpty()) {\n\t\tif (!link.isEmpty()) {\n\t\t\tresult.append(pushTag(\"a\", {\n\t\t\t\t{ \"class\", \"block_link expanded\" },\n\t\t\t\t{ \"href\", relativePath(link).toUtf8() + \"#allow_back\" },\n\t\t\t}));\n\t\t}\n\t\tresult.append(pushDiv(\"name bold\"));\n\t\tresult.append(SerializeString(info));\n\t\tresult.append(popTag());\n\t\tif (!link.isEmpty()) {\n\t\t\tresult.append(popTag());\n\t\t}\n\t}\n\tfor (const auto &detail : details) {\n\t\tresult.append(pushDiv(\"details_entry details\"));\n\t\tresult.append(SerializeString(detail));\n\t\tresult.append(popTag());\n\t}\n\tresult.append(popTag());\n\tresult.append(popTag());\n\treturn result;\n}\n\nQByteArray HtmlWriter::Wrap::pushSessionListEntry(\n\t\tint apiId,\n\t\tconst QByteArray &name,\n\t\tconst QByteArray &subname,\n\t\tstd::initializer_list<QByteArray> details,\n\t\tconst QByteArray &info) {\n\tconst auto link = QString();\n\tauto userpic = UserpicData{\n\t\tData::ApplicationColorIndex(apiId),\n\t\tkEntryUserpicSize\n\t};\n\tuserpic.firstName = name;\n\treturn pushGenericListEntry(\n\t\tlink,\n\t\tuserpic,\n\t\tname,\n\t\tsubname,\n\t\tdetails,\n\t\tinfo);\n}\n\nQByteArray HtmlWriter::Wrap::pushGenericListEntry(\n\t\tconst QString &link,\n\t\tconst UserpicData &userpic,\n\t\tconst QByteArray &name,\n\t\tconst QByteArray &subname,\n\t\tstd::initializer_list<QByteArray> details,\n\t\tconst QByteArray &info) {\n\tauto result = link.isEmpty()\n\t\t? pushDiv(\"entry clearfix\")\n\t\t: pushTag(\"a\", {\n\t\t\t{ \"class\", \"entry block_link clearfix\" },\n\t\t\t{ \"href\", relativePath(link).toUtf8() + \"#allow_back\" },\n\t\t});\n\tresult.append(pushDiv(\"pull_left userpic_wrap\"));\n\tresult.append(pushUserpic(userpic));\n\tresult.append(popTag());\n\tresult.append(pushDiv(\"body\"));\n\tif (!info.isEmpty()) {\n\t\tresult.append(pushDiv(\"pull_right info details\"));\n\t\tresult.append(SerializeString(info));\n\t\tresult.append(popTag());\n\t}\n\tif (!name.isEmpty()) {\n\t\tresult.append(pushDiv(\"name bold\"));\n\t\tresult.append(SerializeString(name));\n\t\tresult.append(popTag());\n\t}\n\tif (!subname.isEmpty()) {\n\t\tresult.append(pushDiv(\"subname bold\"));\n\t\tresult.append(SerializeString(subname));\n\t\tresult.append(popTag());\n\t}\n\tfor (const auto &detail : details) {\n\t\tresult.append(pushDiv(\"details_entry details\"));\n\t\tresult.append(SerializeString(detail));\n\t\tresult.append(popTag());\n\t}\n\tresult.append(popTag());\n\tresult.append(popTag());\n\treturn result;\n}\n\nResult HtmlWriter::Wrap::writeBlock(const QByteArray &block) {\n\tExpects(!_closed);\n\n\tconst auto result = [&] {\n\t\tif (block.isEmpty()) {\n\t\t\treturn _file.writeBlock(block);\n\t\t} else if (_file.empty()) {\n\t\t\treturn _file.writeBlock(_composedStart + block);\n\t\t}\n\t\treturn _file.writeBlock(block);\n\t}();\n\tif (!result) {\n\t\t_closed = true;\n\t}\n\treturn result;\n}\n\nQByteArray HtmlWriter::Wrap::pushHeader(\n\t\tconst QByteArray &header,\n\t\tconst QString &path) {\n\tauto result = pushDiv(\"page_header\");\n\tresult.append(path.isEmpty()\n\t\t? pushDiv(\"content\")\n\t\t: pushTag(\"a\", {\n\t\t\t{ \"class\", \"content block_link\" },\n\t\t\t{ \"href\", relativePath(path).toUtf8() },\n\t\t\t{ \"onclick\", \"return GoBack(this)\"},\n\t\t}));\n\tresult.append(pushDiv(\"text bold\"));\n\tresult.append(SerializeString(header));\n\tresult.append(popTag());\n\tresult.append(popTag());\n\tresult.append(popTag());\n\treturn result;\n}\n\nQByteArray HtmlWriter::Wrap::pushSection(\n\t\tconst QByteArray &header,\n\t\tconst QByteArray &type,\n\t\tint count,\n\t\tconst QString &link) {\n\tauto result = pushTag(\"a\", {\n\t\t{ \"class\", \"section block_link \" + type },\n\t\t{ \"href\", link.toUtf8() + \"#allow_back\" },\n\t});\n\tresult.append(pushDiv(\"counter details\"));\n\tresult.append(Data::NumberToString(count));\n\tresult.append(popTag());\n\tresult.append(pushDiv(\"label bold\"));\n\tresult.append(SerializeString(header));\n\tresult.append(popTag());\n\tresult.append(popTag());\n\treturn result;\n}\n\nQByteArray HtmlWriter::Wrap::pushAbout(\n\t\tconst QByteArray &text,\n\t\tbool withDivider) {\n\tauto result = pushDiv(withDivider\n\t\t? \"page_about details with_divider\"\n\t\t: \"page_about details\");\n\tresult.append(MakeLinks(SerializeString(text)));\n\tresult.append(popTag());\n\treturn result;\n}\n\nQByteArray HtmlWriter::Wrap::pushServiceMessage(\n\t\tint messageId,\n\t\tconst Data::DialogInfo &dialog,\n\t\tconst QString &basePath,\n\t\tconst QByteArray &serialized,\n\t\tconst Data::Photo *photo) {\n\tauto result = pushTag(\"div\", {\n\t\t{ \"class\", \"message service\" },\n\t\t{ \"id\", \"message\" + Data::NumberToString(messageId) }\n\t});\n\tresult.append(pushDiv(\"body details\"));\n\tresult.append(serialized);\n\tresult.append(popTag());\n\tif (photo) {\n\t\tauto userpic = UserpicData();\n\t\tuserpic.colorIndex = dialog.colorIndex;\n\t\tuserpic.firstName = dialog.name;\n\t\tuserpic.lastName = dialog.lastName;\n\t\tuserpic.pixelSize = kServiceMessagePhotoSize;\n\t\tuserpic.largeLink = photo->image.file.relativePath;\n\t\tuserpic.imageLink = WriteUserpicThumb(\n\t\t\tbasePath,\n\t\t\tuserpic.largeLink,\n\t\t\tuserpic);\n\t\tresult.append(pushDiv(\"userpic_wrap\"));\n\t\tresult.append(pushUserpic(userpic));\n\t\tresult.append(popTag());\n\t}\n\tresult.append(popTag());\n\treturn result;\n}\n\nauto HtmlWriter::Wrap::pushMessage(\n\tconst Data::Message &message,\n\tconst MessageInfo *previous,\n\tconst Data::DialogInfo &dialog,\n\tconst QString &basePath,\n\tconst PeersMap &peers,\n\tconst QString &internalLinksDomain,\n\tFn<QByteArray(int messageId, QByteArray text)> wrapMessageLink\n) -> std::pair<MessageInfo, QByteArray> {\n\tusing namespace Data;\n\n\tauto info = MessageInfo();\n\tinfo.id = message.id;\n\tinfo.fromId = message.fromId;\n\tinfo.viaBotId = message.viaBotId;\n\tinfo.date = message.date;\n\tinfo.forwardedFromId = message.forwardedFromId;\n\tinfo.forwardedFromName = message.forwardedFromName;\n\tinfo.forwardedDate = message.forwardedDate;\n\tinfo.forwarded = message.forwarded;\n\tinfo.showForwardedAsOriginal = message.showForwardedAsOriginal;\n\tif (v::is<UnsupportedMedia>(message.media.content)) {\n\t\treturn { info, pushServiceMessage(\n\t\t\tmessage.id,\n\t\t\tdialog,\n\t\t\tbasePath,\n\t\t\t\"This message is not supported by this version \"\n\t\t\t\"of Telegram Desktop. Please update the application.\") };\n\t}\n\n\tconst auto wrapReplyToLink = [&](const QByteArray &text) {\n\t\treturn wrapMessageLink(message.replyToMsgId, text);\n\t};\n\n\tusing DialogType = Data::DialogInfo::Type;\n\tconst auto isChannel = (dialog.type == DialogType::PrivateChannel)\n\t\t|| (dialog.type == DialogType::PublicChannel);\n\tconst auto serviceFrom = peers.wrapPeerName(message.fromId);\n\tconst auto serviceText = v::match(message.action.content, [&](\n\t\t\tconst ActionChatCreate &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" created group &laquo;\"\n\t\t\t+ SerializeString(data.title)\n\t\t\t+ \"&raquo;\"\n\t\t\t+ (data.userIds.empty()\n\t\t\t\t? QByteArray()\n\t\t\t\t: \" with members \" + peers.wrapUserNames(data.userIds));\n\t}, [&](const ActionChatEditTitle &data) {\n\t\treturn isChannel\n\t\t\t? (\"Channel title changed to &laquo;\"\n\t\t\t\t+ SerializeString(data.title)\n\t\t\t\t+ \"&raquo;\")\n\t\t\t: (serviceFrom\n\t\t\t\t+ \" changed group title to &laquo;\"\n\t\t\t\t+ SerializeString(data.title)\n\t\t\t\t+ \"&raquo;\");\n\t}, [&](const ActionChatEditPhoto &data) {\n\t\treturn isChannel\n\t\t\t? QByteArray(\"Channel photo changed\")\n\t\t\t: (serviceFrom + \" changed group photo\");\n\t}, [&](const ActionChatDeletePhoto &data) {\n\t\treturn isChannel\n\t\t\t? QByteArray(\"Channel photo removed\")\n\t\t\t: (serviceFrom + \" removed group photo\");\n\t}, [&](const ActionChatAddUser &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" invited \"\n\t\t\t+ peers.wrapUserNames(data.userIds);\n\t}, [&](const ActionChatDeleteUser &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" removed \"\n\t\t\t+ peers.wrapUserName(data.userId);\n\t}, [&](const ActionChatJoinedByLink &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" joined group by link from \"\n\t\t\t+ peers.wrapUserName(data.inviterId);\n\t}, [&](const ActionChannelCreate &data) {\n\t\treturn \"Channel &laquo;\"\n\t\t\t+ SerializeString(data.title)\n\t\t\t+ \"&raquo; created\";\n\t}, [&](const ActionChatMigrateTo &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" converted this group to a supergroup\";\n\t}, [&](const ActionChannelMigrateFrom &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" converted a basic group to this supergroup \"\n\t\t\t+ \"&laquo;\" + SerializeString(data.title) + \"&raquo;\";\n\t}, [&](const ActionPinMessage &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" pinned \"\n\t\t\t+ wrapReplyToLink(\"this message\");\n\t}, [&](const ActionHistoryClear &data) {\n\t\treturn QByteArray(\"History cleared\");\n\t}, [&](const ActionGameScore &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" scored \"\n\t\t\t+ NumberToString(data.score)\n\t\t\t+ \" in \"\n\t\t\t+ wrapReplyToLink(\"this game\");\n\t}, [&](const ActionPaymentSent &data) {\n\t\tconst auto amount = FormatMoneyAmount(data.amount, data.currency);\n\t\tif (data.recurringUsed) {\n\t\t\treturn \"You were charged \" + amount + \" via recurring payment\";\n\t\t}\n\t\tauto result = \"You have successfully transferred \"\n\t\t\t+ amount\n\t\t\t+ \" for \"\n\t\t\t+ wrapReplyToLink(\"this invoice\");\n\t\tif (data.recurringInit) {\n\t\t\tresult += \" and allowed future recurring payments\";\n\t\t}\n\t\treturn result;\n\t}, [&](const ActionPhoneCall &data) {\n\t\treturn QByteArray();\n\t}, [&](const ActionScreenshotTaken &data) {\n\t\treturn serviceFrom + \" took a screenshot\";\n\t}, [&](const ActionCustomAction &data) {\n\t\treturn data.message;\n\t}, [&](const ActionBotAllowed &data) {\n\t\treturn data.attachMenu\n\t\t\t? \"You allowed this bot to message you \"\n\t\t\t\"when you added it in the attachment menu.\"_q\n\t\t\t: data.fromRequest\n\t\t\t? \"You allowed this bot to message you in his web-app.\"_q\n\t\t\t: data.app.isEmpty()\n\t\t\t? (\"You allowed this bot to message you when you opened \"\n\t\t\t\t+ SerializeString(data.app))\n\t\t\t: (\"You allowed this bot to message you when you logged in on \"\n\t\t\t\t+ SerializeString(data.domain));\n\t}, [&](const ActionSecureValuesSent &data) {\n\t\tauto list = std::vector<QByteArray>();\n\t\tfor (const auto type : data.types) {\n\t\t\tlist.push_back([&] {\n\t\t\t\tusing Type = ActionSecureValuesSent::Type;\n\t\t\t\tswitch (type) {\n\t\t\t\tcase Type::PersonalDetails: return \"Personal details\";\n\t\t\t\tcase Type::Passport: return \"Passport\";\n\t\t\t\tcase Type::DriverLicense: return \"Driver license\";\n\t\t\t\tcase Type::IdentityCard: return \"Identity card\";\n\t\t\t\tcase Type::InternalPassport: return \"Internal passport\";\n\t\t\t\tcase Type::Address: return \"Address information\";\n\t\t\t\tcase Type::UtilityBill: return \"Utility bill\";\n\t\t\t\tcase Type::BankStatement: return \"Bank statement\";\n\t\t\t\tcase Type::RentalAgreement: return \"Rental agreement\";\n\t\t\t\tcase Type::PassportRegistration:\n\t\t\t\t\treturn \"Passport registration\";\n\t\t\t\tcase Type::TemporaryRegistration:\n\t\t\t\t\treturn \"Temporary registration\";\n\t\t\t\tcase Type::Phone: return \"Phone number\";\n\t\t\t\tcase Type::Email: return \"Email\";\n\t\t\t\t}\n\t\t\t\treturn \"\";\n\t\t\t}());\n\t\t}\n\t\treturn \"You have sent the following documents: \"\n\t\t\t+ SerializeList(list);\n\t}, [&](const ActionContactSignUp &data) {\n\t\treturn serviceFrom + \" joined Telegram\";\n\t}, [&](const ActionGeoProximityReached &data) {\n\t\tconst auto fromName = peers.wrapPeerName(data.fromId);\n\t\tconst auto toName = peers.wrapPeerName(data.toId);\n\t\tconst auto distance = [&]() -> QString {\n\t\t\tif (data.distance >= 1000) {\n\t\t\t\tconst auto km = (10 * (data.distance / 10)) / 1000.;\n\t\t\t\treturn QString::number(km) + \" km\";\n\t\t\t} else if (data.distance == 1) {\n\t\t\t\treturn \"1 meter\";\n\t\t\t} else {\n\t\t\t\treturn QString::number(data.distance) + \" meters\";\n\t\t\t}\n\t\t}().toUtf8();\n\t\tif (data.fromSelf) {\n\t\t\treturn \"You are now within \" + distance + \" from \" + toName;\n\t\t} else if (data.toSelf) {\n\t\t\treturn fromName + \" is now within \" + distance + \" from you\";\n\t\t} else {\n\t\t\treturn fromName\n\t\t\t\t+ \" is now within \"\n\t\t\t\t+ distance\n\t\t\t\t+ \" from \"\n\t\t\t\t+ toName;\n\t\t}\n\t}, [&](const ActionPhoneNumberRequest &data) {\n\t\treturn serviceFrom + \" requested your phone number\";\n\t}, [&](const ActionGroupCall &data) {\n\t\tconst auto durationText = (data.duration\n\t\t\t? (\" (\" + QString::number(data.duration) + \" seconds)\")\n\t\t\t: QString()).toUtf8();\n\t\treturn isChannel\n\t\t\t? (\"Voice chat\" + durationText)\n\t\t\t: (serviceFrom + \" started voice chat\" + durationText);\n\t}, [&](const ActionInviteToGroupCall &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" invited \"\n\t\t\t+ peers.wrapUserNames(data.userIds)\n\t\t\t+ \" to the voice chat\";\n\t}, [&](const ActionSetMessagesTTL &data) {\n\t\tconst auto periodText = (data.period == 7 * 86400)\n\t\t\t? \"7 days\"\n\t\t\t: (data.period == 86400)\n\t\t\t? \"24 hours\"\n\t\t\t: QByteArray();\n\t\treturn isChannel\n\t\t\t? (data.period\n\t\t\t\t? \"New messages will auto-delete in \" + periodText\n\t\t\t\t: \"New messages will not auto-delete\")\n\t\t\t: (data.period\n\t\t\t\t? (serviceFrom\n\t\t\t\t\t+ \" has set messages to auto-delete in \" + periodText)\n\t\t\t\t: (serviceFrom\n\t\t\t\t\t+ \" has set messages not to auto-delete\"));\n\t}, [&](const ActionGroupCallScheduled &data) {\n\t\tconst auto dateText = FormatDateTime(data.date);\n\t\treturn isChannel\n\t\t\t? (\"Voice chat scheduled for \" + dateText)\n\t\t\t: (serviceFrom + \" scheduled a voice chat for \" + dateText);\n\t}, [&](const ActionSetChatTheme &data) {\n\t\tif (data.emoji.isEmpty()) {\n\t\t\treturn isChannel\n\t\t\t\t? \"Channel theme was disabled\"\n\t\t\t\t: (serviceFrom + \" disabled chat theme\");\n\t\t}\n\t\treturn isChannel\n\t\t\t? (\"Channel theme was changed to \" + data.emoji).toUtf8()\n\t\t\t: (serviceFrom + \" changed chat theme to \" + data.emoji).toUtf8();\n\t}, [&](const ActionChatJoinedByRequest &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" joined group by request\";\n\t}, [&](const ActionWebViewDataSent &data) {\n\t\treturn \"You have just successfully transferred data from the &laquo;\"\n\t\t\t+ SerializeString(data.text)\n\t\t\t+ \"&raquo; button to the bot\";\n\t}, [&](const ActionGiftPremium &data) {\n\t\tif (!data.days || data.cost.isEmpty()) {\n\t\t\treturn serviceFrom + \" sent you a gift.\";\n\t\t}\n\t\treturn serviceFrom\n\t\t\t+ \" sent you a gift for \"\n\t\t\t+ data.cost\n\t\t\t+ \": Telegram Premium for \"\n\t\t\t+ QString::number(data.days).toUtf8()\n\t\t\t+ \" days.\";\n\t}, [&](const ActionTopicCreate &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" created topic &laquo;\"\n\t\t\t+ SerializeString(data.title)\n\t\t\t+ \"&raquo;\";\n\t}, [&](const ActionTopicEdit &data) {\n\t\tauto parts = QList<QByteArray>();\n\t\tif (!data.title.isEmpty()) {\n\t\t\tparts.push_back(\"title to &laquo;\"\n\t\t\t\t+ SerializeString(data.title)\n\t\t\t\t+ \"&raquo;\");\n\t\t}\n\t\tif (data.iconEmojiId) {\n\t\t\tparts.push_back(\"icon to &laquo;\"\n\t\t\t\t+ QString::number(*data.iconEmojiId).toUtf8()\n\t\t\t\t+ \"&raquo;\");\n\t\t}\n\t\treturn serviceFrom + \" changed topic \" + parts.join(',');\n\t}, [&](const ActionSuggestProfilePhoto &data) {\n\t\treturn serviceFrom + \" suggests to use this photo\";\n\t}, [&](const ActionRequestedPeer &data) {\n\t\treturn \"requested: \"_q/* + data.peerId*/;\n\t}, [&](const ActionSetChatWallPaper &data) {\n\t\treturn serviceFrom\n\t\t\t+ (data.same\n\t\t\t\t? (\" set \"\n\t\t\t\t\t+ wrapReplyToLink(\"the same background\")\n\t\t\t\t\t+ \" for this chat\")\n\t\t\t\t: \" set a new background for this chat\");\n\t}, [&](const ActionGiftCode &data) {\n\t\treturn data.unclaimed\n\t\t\t? (\"This is an unclaimed Telegram Premium for \"\n\t\t\t\t+ NumberToString(data.days)\n\t\t\t\t+ (data.days > 1 ? \" days\" : \" day\")\n\t\t\t\t+ \" prize in a giveaway organized by a channel.\")\n\t\t\t: data.viaGiveaway\n\t\t\t? (\"You won a Telegram Premium for \"\n\t\t\t\t+ NumberToString(data.days)\n\t\t\t\t+ (data.days > 1 ? \" days\" : \" day\")\n\t\t\t\t+ \" prize in a giveaway organized by a channel.\")\n\t\t\t: (\"You've received a Telegram Premium for \"\n\t\t\t\t+ NumberToString(data.days)\n\t\t\t\t+ (data.days > 1 ? \" days\" : \" day\")\n\t\t\t\t+ \" gift from a channel.\");\n\t}, [&](const ActionGiveawayLaunch &data) {\n\t\treturn serviceFrom + \" just started a giveaway \"\n\t\t\t\"of Telegram Premium subscriptions to its followers.\";\n\t}, [&](const ActionGiveawayResults &data) {\n\t\treturn !data.winners\n\t\t\t? \"No winners of the giveaway could be selected.\"\n\t\t\t: (data.credits && data.unclaimed)\n\t\t\t? \"Some winners of the giveaway were randomly selected by \"\n\t\t\t\t\"Telegram and received their prize.\"\n\t\t\t: (!data.credits && data.unclaimed)\n\t\t\t? \"Some winners of the giveaway were randomly selected by \"\n\t\t\t\t\"Telegram and received private messages with giftcodes.\"\n\t\t\t: (data.credits && !data.unclaimed)\n\t\t\t? NumberToString(data.winners) + \" of the giveaway was randomly \"\n\t\t\t\t\"selected by Telegram and received their prize.\"\n\t\t\t: NumberToString(data.winners) + \" of the giveaway was randomly \"\n\t\t\t\t\"selected by Telegram and received private messages with \"\n\t\t\t\t\"giftcodes.\";\n\t}, [&](const ActionBoostApply &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" boosted the group \"\n\t\t\t+ QByteArray::number(data.boosts)\n\t\t\t+ (data.boosts > 1 ? \" times\" : \" time\");\n\t}, [&](const ActionPaymentRefunded &data) {\n\t\tconst auto amount = FormatMoneyAmount(data.amount, data.currency);\n\t\tauto result = peers.wrapPeerName(data.peerId)\n\t\t\t+ \" refunded back \"\n\t\t\t+ amount;\n\t\treturn result;\n\t}, [&](const ActionGiftCredits &data) {\n\t\tif (!data.amount || data.cost.isEmpty()) {\n\t\t\treturn serviceFrom + \" sent you a gift.\";\n\t\t}\n\t\treturn serviceFrom\n\t\t\t+ \" sent you a gift for \"\n\t\t\t+ data.cost\n\t\t\t+ \": \"\n\t\t\t+ QString::number(data.amount.value()).toUtf8()\n\t\t\t+ (data.amount.ton() ? \" TON.\" : \" Telegram Stars.\");\n\t}, [&](const ActionPrizeStars &data) {\n\t\treturn \"You won a prize in a giveaway organized by \"\n\t\t\t+ peers.wrapPeerName(data.peerId)\n\t\t\t+ \".\\n Your prize is \"\n\t\t\t+ QString::number(data.amount).toUtf8()\n\t\t\t+ \" Telegram Stars.\";\n\t}, [&](const ActionStarGift &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" sent you a gift of \"\n\t\t\t+ QByteArray::number(data.stars)\n\t\t\t+ \" Telegram Stars.\";\n\t}, [&](const ActionPaidMessagesRefunded &data) {\n\t\tauto result = message.out\n\t\t\t? (\"You refunded \"\n\t\t\t\t+ QString::number(data.stars).toUtf8()\n\t\t\t\t+ \" Stars for \"\n\t\t\t\t+ QString::number(data.messages).toUtf8()\n\t\t\t\t+ \" messages to \"\n\t\t\t\t+ peers.wrapPeerName(dialog.peerId))\n\t\t\t: (peers.wrapPeerName(dialog.peerId)\n\t\t\t\t+ \" refunded \"\n\t\t\t\t+ QString::number(data.stars).toUtf8()\n\t\t\t\t+ \" Stars for \"\n\t\t\t\t+ QString::number(data.messages).toUtf8()\n\t\t\t\t+ \" messages to you\");\n\t\treturn result;\n\t}, [&](const ActionPaidMessagesPrice &data) {\n\t\tif (isChannel) {\n\t\t\tauto result = !data.broadcastAllowed\n\t\t\t\t? \"Direct messages were disabled.\"\n\t\t\t\t: (\"Price per direct message changed to \"\n\t\t\t\t\t+ QString::number(data.stars).toUtf8()\n\t\t\t\t\t+ \" Telegram Stars.\");\n\t\t\treturn result;\n\t\t}\n\t\tauto result = \"Price per message changed to \"\n\t\t\t+ QString::number(data.stars).toUtf8()\n\t\t\t+ \" Telegram Stars.\";\n\t\treturn result;\n\t}, [&](const ActionTodoCompletions &data) {\n\t\tauto completed = QByteArrayList();\n\t\tfor (const auto index : data.completed) {\n\t\t\tcompleted.push_back(QByteArray::number(index));\n\t\t}\n\t\tauto incompleted = QByteArrayList();\n\t\tfor (const auto index : data.incompleted) {\n\t\t\tincompleted.push_back(QByteArray::number(index));\n\t\t}\n\t\tconst auto list = [](const QByteArrayList &v) {\n\t\t\treturn v.isEmpty()\n\t\t\t\t? QByteArray()\n\t\t\t\t: (v.size() > 1)\n\t\t\t\t? (v.mid(0, v.size() - 1).join(\", \") + \" and \" + v.back())\n\t\t\t\t: v.front();\n\t\t};\n\t\tif (completed.isEmpty() && !incompleted.isEmpty()) {\n\t\t\treturn serviceFrom\n\t\t\t\t+ \" marked \"\n\t\t\t\t+ list(incompleted)\n\t\t\t\t+ \" as not done yet in \"\n\t\t\t\t+ wrapReplyToLink(\"this todo list\") + \".\";\n\t\t} else if (!completed.isEmpty() && incompleted.isEmpty()) {\n\t\t\treturn serviceFrom\n\t\t\t\t+ \" marked \"\n\t\t\t\t+ list(completed)\n\t\t\t\t+ \" as done in \"\n\t\t\t\t+ wrapReplyToLink(\"this todo list\") + \".\";\n\t\t}\n\t\treturn serviceFrom\n\t\t\t+ \" marked \"\n\t\t\t+ list(completed)\n\t\t\t+ \" as done and \"\n\t\t\t+ list(incompleted)\n\t\t\t+ \" as not done yet in \"\n\t\t\t+ wrapReplyToLink(\"this todo list\") + \".\";\n\t}, [&](const ActionTodoAppendTasks &data) {\n\t\tauto tasks = QByteArrayList();\n\t\tfor (const auto &task : data.items) {\n\t\t\ttasks.push_back(\"&quot;\"\n\t\t\t\t+ FormatText(task.text, internalLinksDomain, _base)\n\t\t\t\t+ \"&quot;\");\n\t\t}\n\t\treturn serviceFrom + \" added tasks: \" + tasks.join(\", \");\n\t}, [&](const ActionPollAppendAnswer &data) {\n\t\treturn serviceFrom + \" added &quot;\"\n\t\t\t+ data.option\n\t\t\t+ \"&quot; to the poll.\";\n\t}, [&](const ActionPollDeleteAnswer &data) {\n\t\treturn serviceFrom + \" removed &quot;\"\n\t\t\t+ data.option\n\t\t\t+ \"&quot; from the poll.\";\n\t}, [&](const ActionSuggestedPostApproval &data) {\n\t\treturn serviceFrom\n\t\t\t+ (data.rejected ? \" rejected \" : \" approved \")\n\t\t\t+ \"your suggested post\"\n\t\t\t+ (data.price\n\t\t\t\t? (\", for \"\n\t\t\t\t\t+ QString::number(data.price.value()).toUtf8()\n\t\t\t\t\t+ (data.price.ton() ? \" TON\" : \" stars\"))\n\t\t\t\t: \"\")\n\t\t\t+ (data.scheduleDate\n\t\t\t\t? (\", \"\n\t\t\t\t\t+ FormatDateText(data.scheduleDate)\n\t\t\t\t\t+ \" at \"\n\t\t\t\t\t+ FormatTimeText(data.scheduleDate))\n\t\t\t\t: \"\")\n\t\t\t+ (data.rejectComment.isEmpty()\n\t\t\t\t? \".\"\n\t\t\t\t: (\", with comment: &quot;\"\n\t\t\t\t\t+ SerializeString(data.rejectComment)\n\t\t\t\t\t+ \"&quot;\"));\n\t}, [&](const ActionSuggestedPostSuccess &data) {\n\t\treturn \"The paid post was shown for 24 hours and \"\n\t\t\t+ QString::number(data.price.value()).toUtf8()\n\t\t\t+ (data.price.ton() ? \" TON\" : \" stars\")\n\t\t\t+ \" were transferred to the channel.\";\n\t}, [&](const ActionSuggestedPostRefund &data) {\n\t\treturn QByteArray() + (data.payerInitiated\n\t\t\t? \"The user refunded the payment, post was deleted.\"\n\t\t\t: \"The admin deleted the post early, the payment was refunded.\");\n\t}, [&](const ActionSuggestBirthday &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" suggests to add a date of birth: \"\n\t\t\t+ QByteArray::number(data.birthday.day())\n\t\t\t+ [&] {\n\t\t\t\tswitch (data.birthday.month()) {\n\t\t\t\tcase 1: return \" January\";\n\t\t\t\tcase 2: return \" February\";\n\t\t\t\tcase 3: return \" March\";\n\t\t\t\tcase 4: return \" April\";\n\t\t\t\tcase 5: return \" May\";\n\t\t\t\tcase 6: return \" June\";\n\t\t\t\tcase 7: return \" July\";\n\t\t\t\tcase 8: return \" August\";\n\t\t\t\tcase 9: return \" September\";\n\t\t\t\tcase 10: return \" October\";\n\t\t\t\tcase 11: return \" November\";\n\t\t\t\tcase 12: return \" December\";\n\t\t\t\t}\n\t\t\t\treturn \"\";\n\t\t\t}() + (data.birthday.year()\n\t\t\t\t? (' ' + QByteArray::number(data.birthday.year()))\n\t\t\t\t: QByteArray());\n\t}, [&](const ActionNoForwardsToggle &data) {\n\t\treturn serviceFrom\n\t\t\t+ (data.newValue\n\t\t\t\t? \" disabled sharing in this chat\"\n\t\t\t\t: \" enabled sharing in this chat\");\n\t}, [&](const ActionNoForwardsRequest &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" requested to enable sharing in this chat\";\n\t}, [&](const ActionNewCreatorPending &data) {\n\t\treturn peers.wrapUserName(data.newCreatorId)\n\t\t\t+ \" will become the new main admin in 7 days if \"\n\t\t\t+ serviceFrom\n\t\t\t+ \" does not return\";\n\t}, [&](const ActionChangeCreator &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" made \"\n\t\t\t+ peers.wrapUserName(data.newCreatorId)\n\t\t\t+ \" the new main admin of the group\";\n\t}, [&](const ActionManagedBotCreated &data) {\n\t\treturn serviceFrom\n\t\t\t+ \" created a bot \"\n\t\t\t+ peers.wrapUserName(data.botId);\n\t}, [](v::null_t) { return QByteArray(); });\n\n\tif (!serviceText.isEmpty()) {\n\t\tconst auto &content = message.action.content;\n\t\tconst auto photo = v::is<ActionChatEditPhoto>(content)\n\t\t\t? &v::get<ActionChatEditPhoto>(content).photo\n\t\t\t: v::is<ActionSuggestProfilePhoto>(content)\n\t\t\t? &v::get<ActionSuggestProfilePhoto>(content).photo\n\t\t\t: nullptr;\n\t\treturn { info, pushServiceMessage(\n\t\t\tmessage.id,\n\t\t\tdialog,\n\t\t\tbasePath,\n\t\t\tserviceText,\n\t\t\tphoto) };\n\t}\n\tinfo.type = MessageInfo::Type::Default;\n\n\tconst auto wrap = messageNeedsWrap(message, previous);\n\tconst auto fromPeerId = message.fromId;\n\tconst auto showForwardedInfo = message.forwarded\n\t\t&& !message.showForwardedAsOriginal;\n\tauto forwardedUserpic = UserpicData();\n\tif (message.forwarded) {\n\t\tforwardedUserpic.colorIndex = message.forwardedFromId\n\t\t\t? PeerColorIndex(message.forwardedFromId)\n\t\t\t: PeerColorIndex(message.id);\n\t\tforwardedUserpic.pixelSize = kHistoryUserpicSize;\n\t\tif (message.forwardedFromId) {\n\t\t\tFillUserpicNames(\n\t\t\t\tforwardedUserpic,\n\t\t\t\tpeers.peer(message.forwardedFromId));\n\t\t} else {\n\t\t\tFillUserpicNames(forwardedUserpic, message.forwardedFromName);\n\t\t}\n\t}\n\tauto userpic = UserpicData();\n\tif (message.showForwardedAsOriginal) {\n\t\tuserpic = forwardedUserpic;\n\t} else {\n\t\tuserpic.colorIndex = PeerColorIndex(fromPeerId);\n\t\tuserpic.pixelSize = kHistoryUserpicSize;\n\t\tFillUserpicNames(userpic, peers.peer(fromPeerId));\n\t}\n\n\tconst auto via = [&] {\n\t\tif (message.viaBotId) {\n\t\t\tconst auto &user = peers.user(message.viaBotId);\n\t\t\tif (!user.username.isEmpty()) {\n\t\t\t\treturn SerializeString(user.username);\n\t\t\t}\n\t\t}\n\t\treturn QByteArray();\n\t}();\n\n\tconst auto className = wrap\n\t\t? \"message default clearfix\"\n\t\t: \"message default clearfix joined\";\n\tauto block = pushTag(\"div\", {\n\t\t{ \"class\", className },\n\t\t{ \"id\", \"message\" + NumberToString(message.id) }\n\t});\n\tif (wrap) {\n\t\tblock.append(pushDiv(\"pull_left userpic_wrap\"));\n\t\tblock.append(pushUserpic(userpic));\n\t\tblock.append(popTag());\n\t}\n\tblock.append(pushDiv(\"body\"));\n\tblock.append(pushTag(\"div\", {\n\t\t{ \"class\", \"pull_right date details\" },\n\t\t{ \"title\", FormatDateTime(message.date, true) },\n\t}));\n\tblock.append(FormatTimeText(message.date));\n\tblock.append(popTag());\n\tif (wrap) {\n\t\tblock.append(pushDiv(\"from_name\"));\n\t\tblock.append(SerializeString(\n\t\t\tComposeName(userpic, \"Deleted Account\")));\n\t\tif (!via.isEmpty()\n\t\t\t&& (!message.forwarded || message.showForwardedAsOriginal)) {\n\t\t\tblock.append(\" via @\" + via);\n\t\t}\n\t\tblock.append(popTag());\n\t}\n\tif (showForwardedInfo) {\n\t\tconst auto forwardedWrap = forwardedNeedsWrap(message, previous);\n\t\tif (forwardedWrap) {\n\t\t\tblock.append(pushDiv(\"pull_left forwarded userpic_wrap\"));\n\t\t\tblock.append(pushUserpic(forwardedUserpic));\n\t\t\tblock.append(popTag());\n\t\t}\n\t\tblock.append(pushDiv(\"forwarded body\"));\n\t\tif (forwardedWrap) {\n\t\t\tblock.append(pushDiv(\"from_name\"));\n\t\t\tblock.append(SerializeString(\n\t\t\t\tComposeName(forwardedUserpic, \"Deleted Account\")));\n\t\t\tif (!via.isEmpty()) {\n\t\t\t\tblock.append(\" via @\" + via);\n\t\t\t}\n\t\t\tblock.append(pushTag(\"span\", {\n\t\t\t\t{ \"class\", \"date details\" },\n\t\t\t\t{ \"title\", FormatDateTime(message.forwardedDate, true) },\n\t\t\t\t{ \"inline\", \"\" }\n\t\t\t}));\n\t\t\tblock.append(' ' + FormatDateTime(message.forwardedDate));\n\t\t\tblock.append(popTag());\n\t\t\tblock.append(popTag());\n\t\t}\n\t}\n\tif (message.replyToMsgId) {\n\t\tblock.append(pushDiv(\"reply_to details\"));\n\t\tif (message.replyToPeerId) {\n\t\t\tblock.append(\"In reply to a message in another chat\");\n\t\t} else {\n\t\t\tblock.append(\"In reply to \");\n\t\t\tblock.append(wrapReplyToLink(\"this message\"));\n\t\t}\n\t\tblock.append(popTag());\n\t}\n\n\tblock.append(\n\t\tpushMedia(\n\t\t\tmessage,\n\t\t\tbasePath,\n\t\t\tpeers,\n\t\t\tinternalLinksDomain,\n\t\t\twrapMessageLink));\n\n\tconst auto text = FormatText(message.text, internalLinksDomain, _base);\n\tif (!text.isEmpty()) {\n\t\tblock.append(pushDiv(\"text\"));\n\t\tblock.append(text);\n\t\tblock.append(popTag());\n\t}\n\tif (!message.inlineButtonRows.empty()) {\n\t\tusing Type = HistoryMessageMarkupButton::Type;\n\t\tconst auto endline = u\" | \"_q;\n\t\tblock.append(pushTag(\"table\", { { \"class\", \"bot_buttons_table\" } }));\n\t\tblock.append(pushTag(\"tbody\"));\n\t\tfor (const auto &row : message.inlineButtonRows) {\n\t\t\tblock.append(pushTag(\"tr\"));\n\t\t\tblock.append(pushTag(\"td\", { { \"class\", \"bot_button_row\" } }));\n\t\t\tfor (const auto &button : row) {\n\t\t\t\tusing Attribute = std::pair<QByteArray, QByteArray>;\n\t\t\t\tconst auto content = (!button.data.isEmpty()\n\t\t\t\t\t\t? (u\"Data: \"_q + button.data + endline)\n\t\t\t\t\t\t: QString())\n\t\t\t\t\t+ (!button.forwardText.isEmpty()\n\t\t\t\t\t\t? (u\"Forward text: \"_q + button.forwardText + endline)\n\t\t\t\t\t\t: QString())\n\t\t\t\t\t+ (u\"Type: \"_q\n\t\t\t\t\t\t+ HistoryMessageMarkupButton::TypeToString(button));\n\t\t\t\tconst auto link = (button.type == Type::Url)\n\t\t\t\t\t? button.data\n\t\t\t\t\t: QByteArray();\n\t\t\t\tconst auto onclick = (button.type != Type::Url)\n\t\t\t\t\t? (\"return ShowTextCopied('\" + content + \"');\").toUtf8()\n\t\t\t\t\t: QByteArray();\n\t\t\t\tblock.append(pushTag(\"div\", { { \"class\", \"bot_button\" } }));\n\t\t\t\tblock.append(pushTag(\"a\", {\n\t\t\t\t\tlink.isEmpty() ? Attribute() : Attribute{ \"href\", link },\n\t\t\t\t\tonclick.isEmpty()\n\t\t\t\t\t\t? Attribute()\n\t\t\t\t\t\t: Attribute{ \"onclick\", onclick },\n\t\t\t\t}));\n\t\t\t\tblock.append(pushTag(\"div\"));\n\t\t\t\tblock.append(button.text.toUtf8());\n\t\t\t\tblock.append(popTag());\n\t\t\t\tblock.append(popTag());\n\t\t\t\tblock.append(popTag());\n\n\t\t\t\tif (&button != &row.back()) {\n\t\t\t\t\tblock.append(pushTag(\"div\", {\n\t\t\t\t\t\t{ \"class\", \"bot_button_column_separator\" }\n\t\t\t\t\t}));\n\t\t\t\t\tblock.append(popTag());\n\t\t\t\t}\n\t\t\t}\n\t\t\tblock.append(popTag());\n\t\t\tblock.append(popTag());\n\t\t}\n\t\tblock.append(popTag());\n\t\tblock.append(popTag());\n\t}\n\tif (!message.signature.isEmpty()) {\n\t\tblock.append(pushDiv(\"signature details\"));\n\t\tblock.append(SerializeString(message.signature));\n\t\tblock.append(popTag());\n\t}\n\tif (showForwardedInfo) {\n\t\tblock.append(popTag());\n\t}\n\tif (!message.reactions.empty()) {\n\t\tblock.append(pushTag(\"span\", {\n\t\t\t{ \"class\", \"reactions\" },\n\t\t}));\n\t\tfor (const auto &reaction : message.reactions) {\n\t\t\tauto reactionClass = QByteArray(\"reaction\");\n\t\t\tfor (const auto &recent : reaction.recent) {\n\t\t\t\tconst auto peer = peers.peer(recent.peerId);\n\t\t\t\tif (peer.user() && peer.user()->isSelf) {\n\t\t\t\t\treactionClass += \" active\";\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (reaction.type == Reaction::Type::Paid) {\n\t\t\t\treactionClass += \" paid\";\n\t\t\t}\n\n\t\t\tblock.append(pushTag(\"span\", {\n\t\t\t\t{ \"class\", reactionClass },\n\t\t\t}));\n\t\t\tblock.append(pushTag(\"span\", {\n\t\t\t\t{ \"class\", \"emoji\" },\n\t\t\t}));\n\t\t\tswitch (reaction.type) {\n\t\t\t\tcase Reaction::Type::Emoji:\n\t\t\t\t\tblock.append(SerializeString(reaction.emoji.toUtf8()));\n\t\t\t\t\tbreak;\n\t\t\t\tcase Reaction::Type::CustomEmoji:\n\t\t\t\t\tblock.append(FormatCustomEmoji(\n\t\t\t\t\t\treaction.documentId,\n\t\t\t\t\t\t\"\\U0001F44B\",\n\t\t\t\t\t\t_base));\n\t\t\t\t\tbreak;\n\t\t\t\tcase Reaction::Type::Paid:\n\t\t\t\t\tblock.append(SerializeString(\"\\u2B50\"));\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\tblock.append(popTag());\n\t\t\tif (!reaction.recent.empty()) {\n\t\t\t\tblock.append(pushTag(\"span\", {\n\t\t\t\t\t{ \"class\", \"userpics\" },\n\t\t\t\t}));\n\t\t\t\tfor (const auto &recent : reaction.recent) {\n\t\t\t\t\tconst auto peer = peers.peer(recent.peerId);\n\t\t\t\t\tblock.append(pushUserpic(UserpicData({\n\t\t\t\t\t\t.colorIndex = peer.colorIndex(),\n\t\t\t\t\t\t.pixelSize = 20,\n\t\t\t\t\t\t.firstName = peer.user()\n\t\t\t\t\t\t\t? peer.user()->info.firstName\n\t\t\t\t\t\t\t: peer.name(),\n\t\t\t\t\t\t.lastName = peer.user()\n\t\t\t\t\t\t\t? peer.user()->info.lastName\n\t\t\t\t\t\t\t: \"\",\n\t\t\t\t\t\t.tooltip = peer.name(),\n\t\t\t\t\t})));\n\t\t\t\t}\n\t\t\t\tblock.append(popTag());\n\t\t\t}\n\t\t\tif (reaction.recent.empty()\n\t\t\t\t|| (reaction.count > reaction.recent.size())) {\n\t\t\t\tblock.append(pushTag(\"span\", {\n\t\t\t\t\t{ \"class\", \"count\" },\n\t\t\t\t}));\n\t\t\t\tblock.append(NumberToString(reaction.count));\n\t\t\t\tblock.append(popTag());\n\t\t\t}\n\t\t\tblock.append(popTag());\n\t\t}\n\t\tblock.append(popTag());\n\t}\n\tblock.append(popTag());\n\tblock.append(popTag());\n\n\treturn { info, block };\n}\n\nbool HtmlWriter::Wrap::messageNeedsWrap(\n\t\tconst Data::Message &message,\n\t\tconst MessageInfo *previous) const {\n\tif (!previous) {\n\t\treturn true;\n\t} else if (previous->type != MessageInfo::Type::Default) {\n\t\treturn true;\n\t} else if (!message.fromId || previous->fromId != message.fromId) {\n\t\treturn true;\n\t} else if (message.viaBotId != previous->viaBotId) {\n\t\treturn true;\n\t} else if (QDateTime::fromSecsSinceEpoch(previous->date).date()\n\t\t!= QDateTime::fromSecsSinceEpoch(message.date).date()) {\n\t\treturn true;\n\t} else if (message.forwarded != previous->forwarded\n\t\t|| message.showForwardedAsOriginal != previous->showForwardedAsOriginal\n\t\t|| message.forwardedFromId != previous->forwardedFromId\n\t\t|| message.forwardedFromName != previous->forwardedFromName) {\n\t\treturn true;\n\t} else if (std::abs(message.date - previous->date)\n\t\t> ((message.forwardedFromId || !message.forwardedFromName.isEmpty())\n\t\t\t? 1\n\t\t\t: kJoinWithinSeconds)) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nQByteArray HtmlWriter::Wrap::pushMedia(\n\t\tconst Data::Message &message,\n\t\tconst QString &basePath,\n\t\tconst PeersMap &peers,\n\t\tconst QString &internalLinksDomain,\n\t\tFn<QByteArray(int messageId, QByteArray text)> wrapMessageLink) {\n\tconst auto data = prepareMediaData(\n\t\tmessage,\n\t\tbasePath,\n\t\tpeers,\n\t\tinternalLinksDomain);\n\tif (!data.classes.isEmpty()) {\n\t\treturn pushGenericMedia(data);\n\t}\n\tusing namespace Data;\n\tconst auto &content = message.media.content;\n\tif (const auto document = std::get_if<Document>(&content)) {\n\t\tAssert(!message.media.ttl);\n\t\tif (document->isSticker) {\n\t\t\treturn pushStickerMedia(*document, basePath);\n\t\t} else if (document->isAnimated) {\n\t\t\treturn pushAnimatedMedia(*document, basePath);\n\t\t} else if (document->isVideoFile) {\n\t\t\treturn pushVideoFileMedia(*document, basePath);\n\t\t}\n\t\tUnexpected(\"Non generic document in HtmlWriter::Wrap::pushMedia.\");\n\t} else if (const auto photo = std::get_if<Photo>(&content)) {\n\t\tAssert(!message.media.ttl);\n\t\treturn pushPhotoMedia(*photo, basePath);\n\t} else if (const auto poll = std::get_if<Poll>(&content)) {\n\t\treturn pushPoll(*poll, internalLinksDomain, _base);\n\t} else if (const auto todo = std::get_if<TodoList>(&content)) {\n\t\treturn pushTodoList(*todo, internalLinksDomain, _base);\n\t} else if (const auto giveaway = std::get_if<GiveawayStart>(&content)) {\n\t\treturn pushGiveaway(peers, *giveaway);\n\t} else if (const auto giveaway = std::get_if<GiveawayResults>(&content)) {\n\t\treturn pushGiveaway(peers, *giveaway, wrapMessageLink);\n\t}\n\tAssert(v::is_null(content));\n\treturn QByteArray();\n}\n\nQByteArray HtmlWriter::Wrap::pushGenericMedia(const MediaData &data) {\n\tauto result = pushDiv(\"media_wrap clearfix\");\n\tif (data.link.isEmpty()) {\n\t\tresult.append(pushDiv(\"media clearfix pull_left \" + data.classes));\n\t} else {\n\t\tresult.append(pushTag(\"a\", {\n\t\t\t{\n\t\t\t\t\"class\",\n\t\t\t\t\"media clearfix pull_left block_link \" + data.classes\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"href\",\n\t\t\t\t(IsGlobalLink(data.link)\n\t\t\t\t\t? data.link.toUtf8()\n\t\t\t\t\t: relativePath(data.link).toUtf8())\n\t\t\t}\n\t\t}));\n\t}\n\tif (data.thumb.isEmpty()) {\n\t\tresult.append(pushDiv(\"fill pull_left\"));\n\t\tresult.append(popTag());\n\t} else {\n\t\tresult.append(pushTag(\"img\", {\n\t\t\t{ \"class\", \"thumb pull_left\" },\n\t\t\t{ \"src\", relativePath(data.thumb).toUtf8() },\n\t\t\t{ \"empty\", \"\" }\n\t\t}));\n\t}\n\tresult.append(pushDiv(\"body\"));\n\tif (!data.title.isEmpty()) {\n\t\tresult.append(pushDiv(\"title bold\"));\n\t\tresult.append(SerializeString(data.title));\n\t\tresult.append(popTag());\n\t}\n\tif (!data.description.isEmpty()) {\n\t\tresult.append(pushDiv(\"description\"));\n\t\tresult.append(SerializeString(data.description));\n\t\tresult.append(popTag());\n\t}\n\tif (!data.status.isEmpty()) {\n\t\tresult.append(pushDiv(\"status details\"));\n\t\tresult.append(SerializeString(data.status));\n\t\tresult.append(popTag());\n\t}\n\tresult.append(popTag());\n\tresult.append(popTag());\n\tresult.append(popTag());\n\treturn result;\n}\n\nQByteArray HtmlWriter::Wrap::pushStickerMedia(\n\t\tconst Data::Document &data,\n\t\tconst QString &basePath) {\n\tusing namespace Data;\n\n\tconst auto &[thumb, size] = WriteImageThumb(\n\t\tbasePath,\n\t\tdata.file.relativePath,\n\t\tCalculateThumbSize(\n\t\t\tkStickerMaxWidth,\n\t\t\tkStickerMaxHeight,\n\t\t\tkStickerMinWidth,\n\t\t\tkStickerMinHeight),\n\t\t\"PNG\",\n\t\t-1);\n\tif (thumb.isEmpty()) {\n\t\tauto generic = MediaData();\n\t\tgeneric.title = \"Sticker\";\n\t\tgeneric.status = data.stickerEmoji;\n\t\tif (data.file.relativePath.isEmpty()) {\n\t\t\tif (!generic.status.isEmpty()) {\n\t\t\t\tgeneric.status += \", \";\n\t\t\t}\n\t\t\tgeneric.status += FormatFileSize(data.file.size);\n\t\t} else {\n\t\t\tgeneric.link = data.file.relativePath;\n\t\t}\n\t\tgeneric.description = NoFileDescription(data.file.skipReason);\n\t\tgeneric.classes = \"media_photo\";\n\t\treturn pushGenericMedia(generic);\n\t}\n\tauto result = pushDiv(\"media_wrap clearfix\");\n\tresult.append(pushTag(\"a\", {\n\t\t{ \"class\", \"sticker_wrap clearfix pull_left\" },\n\t\t{\n\t\t\t\"href\",\n\t\t\trelativePath(data.file.relativePath).toUtf8()\n\t\t}\n\t}));\n\tconst auto sizeStyle = \"width: \"\n\t\t+ NumberToString(size.width() / 2)\n\t\t+ \"px; height: \"\n\t\t+ NumberToString(size.height() / 2)\n\t\t+ \"px\";\n\tresult.append(pushTag(\"img\", {\n\t\t{ \"class\", \"sticker\" },\n\t\t{ \"style\", sizeStyle },\n\t\t{ \"src\", relativePath(thumb).toUtf8() },\n\t\t{ \"empty\", \"\" }\n\t}));\n\tresult.append(popTag());\n\tresult.append(popTag());\n\treturn result;\n}\n\nQByteArray HtmlWriter::Wrap::pushAnimatedMedia(\n\t\tconst Data::Document &data,\n\t\tconst QString &basePath) {\n\tusing namespace Data;\n\n\tauto size = QSize(data.width, data.height);\n\tauto thumbSize = CalculateThumbSize(\n\t\tkPhotoMaxWidth,\n\t\tkPhotoMaxHeight,\n\t\tkPhotoMinWidth,\n\t\tkPhotoMinHeight,\n\t\ttrue)(size);\n\tif (data.thumb.file.relativePath.isEmpty()\n\t\t|| data.file.relativePath.isEmpty()\n\t\t|| !thumbSize.width()\n\t\t|| !thumbSize.height()) {\n\t\tauto generic = MediaData();\n\t\tgeneric.title = \"Animation\";\n\t\tgeneric.status = FormatFileSize(data.file.size);\n\t\tgeneric.link = data.file.relativePath;\n\t\tgeneric.description = NoFileDescription(data.file.skipReason);\n\t\tgeneric.classes = \"media_video\";\n\t\treturn pushGenericMedia(generic);\n\t}\n\tauto result = pushDiv(\"media_wrap clearfix\");\n\tresult.append(pushTag(\"a\", {\n\t\t{ \"class\", \"animated_wrap clearfix pull_left\" },\n\t\t{\n\t\t\t\"href\",\n\t\t\trelativePath(data.file.relativePath).toUtf8()\n\t\t}\n\t}));\n\tresult.append(pushDiv(\"video_play_bg\"));\n\tresult.append(pushDiv(\"gif_play\"));\n\tresult.append(\"GIF\");\n\tresult.append(popTag());\n\tresult.append(popTag());\n\tconst auto sizeStyle = \"width: \"\n\t\t+ NumberToString(thumbSize.width() / 2)\n\t\t+ \"px; height: \"\n\t\t+ NumberToString(thumbSize.height() / 2)\n\t\t+ \"px\";\n\tresult.append(pushTag(\"img\", {\n\t\t{ \"class\", \"animated\" },\n\t\t{ \"style\", sizeStyle },\n\t\t{ \"src\", relativePath(data.thumb.file.relativePath).toUtf8() },\n\t\t{ \"empty\", \"\" }\n\t}));\n\tresult.append(popTag());\n\tresult.append(popTag());\n\treturn result;\n}\n\nQByteArray HtmlWriter::Wrap::pushVideoFileMedia(\n\t\tconst Data::Document &data,\n\t\tconst QString &basePath) {\n\tusing namespace Data;\n\n\tauto size = QSize(data.width, data.height);\n\tauto thumbSize = CalculateThumbSize(\n\t\tkPhotoMaxWidth,\n\t\tkPhotoMaxHeight,\n\t\tkPhotoMinWidth,\n\t\tkPhotoMinHeight,\n\t\ttrue)(size);\n\tif (data.thumb.file.relativePath.isEmpty()\n\t\t|| data.file.relativePath.isEmpty()\n\t\t|| !thumbSize.width()\n\t\t|| !thumbSize.height()) {\n\t\tauto generic = MediaData();\n\t\tgeneric.title = \"Video file\";\n\t\tgeneric.status = FormatDuration(data.duration);\n\t\tif (data.file.relativePath.isEmpty()) {\n\t\t\tgeneric.status += \", \" + FormatFileSize(data.file.size);\n\t\t} else {\n\t\t\tgeneric.link = data.file.relativePath;\n\t\t}\n\t\tgeneric.description = NoFileDescription(data.file.skipReason);\n\t\tgeneric.classes = \"media_video\";\n\t\treturn pushGenericMedia(generic);\n\t}\n\tauto result = pushDiv(\"media_wrap clearfix\");\n\tresult.append(pushTag(\"a\", {\n\t\t{ \"class\", \"video_file_wrap clearfix pull_left\" },\n\t\t{\n\t\t\t\"href\",\n\t\t\trelativePath(data.file.relativePath).toUtf8()\n\t\t}\n\t}));\n\tresult.append(pushDiv(\"video_play_bg\"));\n\tresult.append(pushDiv(\"video_play\"));\n\tresult.append(popTag());\n\tresult.append(popTag());\n\tresult.append(pushDiv(\"video_duration\"));\n\tresult.append(FormatDuration(data.duration));\n\tresult.append(popTag());\n\tconst auto sizeStyle = \"width: \"\n\t\t+ NumberToString(thumbSize.width() / 2)\n\t\t+ \"px; height: \"\n\t\t+ NumberToString(thumbSize.height() / 2)\n\t\t+ \"px\";\n\tresult.append(pushTag(\"img\", {\n\t\t{ \"class\", \"video_file\" },\n\t\t{ \"style\", sizeStyle },\n\t\t{ \"src\", relativePath(data.thumb.file.relativePath).toUtf8() },\n\t\t{ \"empty\", \"\" }\n\t}));\n\tresult.append(popTag());\n\tresult.append(popTag());\n\treturn result;\n}\n\nQByteArray HtmlWriter::Wrap::pushPhotoMedia(\n\t\tconst Data::Photo &data,\n\t\tconst QString &basePath) {\n\tusing namespace Data;\n\n\tconst auto &[thumb, size] = WriteImageThumb(\n\t\tbasePath,\n\t\tdata.image.file.relativePath,\n\t\tCalculateThumbSize(\n\t\t\tkPhotoMaxWidth,\n\t\t\tkPhotoMaxHeight,\n\t\t\tkPhotoMinWidth,\n\t\t\tkPhotoMinHeight));\n\tif (thumb.isEmpty()) {\n\t\tauto generic = MediaData();\n\t\tgeneric.title = \"Photo\";\n\t\tgeneric.status = Ui::FormatImageSizeText(\n\t\t\tQSize(data.image.width, data.image.height)).toUtf8();\n\t\tif (data.image.file.relativePath.isEmpty()) {\n\t\t\tgeneric.status += \", \" + FormatFileSize(data.image.file.size);\n\t\t} else {\n\t\t\tgeneric.link = data.image.file.relativePath;\n\t\t}\n\t\tgeneric.description = NoFileDescription(data.image.file.skipReason);\n\t\tgeneric.classes = \"media_photo\";\n\t\treturn pushGenericMedia(generic);\n\t}\n\tauto result = pushDiv(\"media_wrap clearfix\");\n\tresult.append(pushTag(\"a\", {\n\t\t{ \"class\", \"photo_wrap clearfix pull_left\" },\n\t\t{\n\t\t\t\"href\",\n\t\t\trelativePath(data.image.file.relativePath).toUtf8()\n\t\t}\n\t}));\n\tconst auto sizeStyle = \"width: \"\n\t\t+ NumberToString(size.width() / 2)\n\t\t+ \"px; height: \"\n\t\t+ NumberToString(size.height() / 2)\n\t\t+ \"px\";\n\tresult.append(pushTag(\"img\", {\n\t\t{ \"class\", \"photo\" },\n\t\t{ \"style\", sizeStyle },\n\t\t{ \"src\", relativePath(thumb).toUtf8() },\n\t\t{ \"empty\", \"\" }\n\t}));\n\tresult.append(popTag());\n\tresult.append(popTag());\n\treturn result;\n}\n\nQByteArray HtmlWriter::Wrap::pushPoll(\n\t\tconst Data::Poll &data,\n\t\tconst QString &internalLinksDomain,\n\t\tconst QString &relativeLinkBase) {\n\tusing namespace Data;\n\n\tauto result = pushDiv(\"media_wrap clearfix\");\n\tresult.append(pushDiv(\"media_poll\"));\n\tresult.append(pushDiv(\"question bold\"));\n\tresult.append(FormatText(\n\t\tdata.question,\n\t\tinternalLinksDomain,\n\t\trelativeLinkBase));\n\tresult.append(popTag());\n\tresult.append(pushDiv(\"details\"));\n\tif (data.closed) {\n\t\tresult.append(SerializeString(\"Final results\"));\n\t} else {\n\t\tresult.append(SerializeString(\"Anonymous poll\"));\n\t}\n\tresult.append(popTag());\n\tconst auto votes = [](int count) {\n\t\tif (count > 1) {\n\t\t\treturn NumberToString(count) + \" votes\";\n\t\t} else if (count > 0) {\n\t\t\treturn NumberToString(count) + \" vote\";\n\t\t}\n\t\treturn QByteArray(\"No votes\");\n\t};\n\tconst auto details = [&](const Poll::Answer &answer) {\n\t\tif (!answer.votes) {\n\t\t\treturn QByteArray(\"\");\n\t\t} else if (!answer.my) {\n\t\t\treturn \" <span class=\\\"details\\\">\"\n\t\t\t\t+ votes(answer.votes)\n\t\t\t\t+ \"</span>\";\n\t\t}\n\t\treturn \" <span class=\\\"details\\\">\"\n\t\t\t+ votes(answer.votes)\n\t\t\t+ \", chosen vote</span>\";\n\t};\n\tfor (const auto &answer : data.answers) {\n\t\tresult.append(pushDiv(\"answer\"));\n\t\tresult.append(\"- \"\n\t\t\t+ FormatText(answer.text, internalLinksDomain, relativeLinkBase)\n\t\t\t+ details(answer));\n\t\tresult.append(popTag());\n\t}\n\tresult.append(pushDiv(\"total details\t\"));\n\tresult.append(votes(data.totalVotes));\n\tresult.append(popTag());\n\tresult.append(popTag());\n\tresult.append(popTag());\n\treturn result;\n}\n\nQByteArray HtmlWriter::Wrap::pushTodoList(\n\t\tconst Data::TodoList &data,\n\t\tconst QString &internalLinksDomain,\n\t\tconst QString &relativeLinkBase) {\n\tusing namespace Data;\n\n\tauto result = pushDiv(\"media_wrap clearfix\");\n\tresult.append(pushDiv(\"media_poll\"));\n\tresult.append(pushDiv(\"question bold\"));\n\tresult.append(FormatText(\n\t\tdata.title,\n\t\tinternalLinksDomain,\n\t\trelativeLinkBase));\n\tresult.append(popTag());\n\tresult.append(pushDiv(\"details\"));\n\tresult.append(SerializeString(\"To-do List\"));\n\tresult.append(popTag());\n\tconst auto details = [&](const TodoListItem &item) {\n\t\treturn QByteArray(\"\"); // #TODO todo\n\t};\n\tfor (const auto &item : data.items) {\n\t\tresult.append(pushDiv(\"answer\"));\n\t\tresult.append(\"- \"\n\t\t\t+ FormatText(item.text, internalLinksDomain, relativeLinkBase)\n\t\t\t+ details(item));\n\t\tresult.append(popTag());\n\t}\n\tresult.append(popTag());\n\tresult.append(popTag());\n\treturn result;\n}\n\nQByteArray HtmlWriter::Wrap::pushGiveaway(\n\t\tconst PeersMap &peers,\n\t\tconst Data::GiveawayStart &data) {\n\tauto result = pushDiv(\"media_wrap clearfix\");\n\tresult.append(pushDiv(\"media_giveaway\"));\n\n\tresult.append(pushDiv(\"section_title bold\"));\n\tresult.append((data.quantity > 1)\n\t\t? SerializeString(\"Giveaway Prizes\")\n\t\t: SerializeString(\"Giveaway Prize\"));\n\tresult.append(popTag());\n\n\t{\n\t\tresult.append(pushDiv(\"section_body\"));\n\t\tresult.append(\"<b>\"\n\t\t\t+ Data::NumberToString(data.quantity)\n\t\t\t+ \"</b> \"\n\t\t\t+ SerializeString(data.additionalPrize.toUtf8()));\n\t\tresult.append(popTag());\n\t\tresult.append(pushDiv(\"section_title bold\"));\n\t\tresult.append(SerializeString(\"with\"));\n\t\tresult.append(popTag());\n\t};\n\tresult.append(pushDiv(\"section_body\"));\n\tif (data.credits > 0) {\n\t\tresult.append(\"<b>\"\n\t\t\t+ Data::NumberToString(data.credits)\n\t\t\t+ (SerializeString(data.credits == 1 ? (\" Star\") : (\" Stars\")))\n\t\t\t+ \"</b> \" + SerializeString(\"will be distributed \")\n\t\t\t+ ((data.quantity == 1)\n\t\t\t\t? SerializeString(\"to \")\n\t\t\t\t\t+ \"<b>\"\n\t\t\t\t\t+ Data::NumberToString(data.quantity)\n\t\t\t\t\t+ \"</b> \" + SerializeString(\"winner.\")\n\t\t\t\t: SerializeString(\"among \")\n\t\t\t\t\t+ \"<b>\"\n\t\t\t\t\t+ Data::NumberToString(data.quantity)\n\t\t\t\t\t+ \"</b> \" + SerializeString(\"winners.\")));\n\t} else {\n\t\tresult.append(\"<b>\"\n\t\t\t+ Data::NumberToString(data.quantity)\n\t\t\t+ \"</b> \"\n\t\t\t+ SerializeString((data.quantity > 1)\n\t\t\t\t? \"Telegram Premium Subscriptions\"\n\t\t\t\t: \"Telegram Premium Subscription\")\n\t\t\t+ \" for <b>\" + Data::NumberToString(data.months) + \"</b> \"\n\t\t\t+ (data.months > 1 ? \"months.\" : \"month.\"));\n\t}\n\tresult.append(popTag());\n\n\tresult.append(pushDiv(\"section_title bold\"));\n\tresult.append(SerializeString(\"Participants\"));\n\tresult.append(popTag());\n\tresult.append(pushDiv(\"section_body\"));\n\tauto channels = QByteArrayList();\n\tauto anyChannel = false;\n\tauto anyGroup = false;\n\tfor (const auto &channel : data.channels) {\n\t\tif (const auto chat = peers.peer(channel).chat()) {\n\t\t\tif (chat->isBroadcast) {\n\t\t\t\tanyChannel = true;\n\t\t\t} else if (chat->isSupergroup) {\n\t\t\t\tanyGroup = true;\n\t\t\t}\n\t\t}\n\t\tchannels.append(\"<b>\" + peers.wrapPeerName(channel) + \"</b>\");\n\t}\n\n\tconst auto participants = [&] {\n\t\tif (data.all && !anyGroup && anyChannel && channels.size() == 1) {\n\t\t\treturn \"All subscribers of the channel:\";\n\t\t}\n\t\tif (data.all && !anyGroup && anyChannel && channels.size() > 1) {\n\t\t\treturn \"All subscribers of the channels:\";\n\t\t}\n\t\tif (data.all && anyGroup && !anyChannel && channels.size() == 1) {\n\t\t\treturn \"All members of the group:\";\n\t\t}\n\t\tif (data.all && anyGroup && !anyChannel && channels.size() > 1) {\n\t\t\treturn \"All members of the groups:\";\n\t\t}\n\t\tif (data.all && anyGroup && anyChannel && channels.size() == 1) {\n\t\t\treturn \"All members of the group:\";\n\t\t}\n\t\tif (data.all && anyGroup && anyChannel && channels.size() > 1) {\n\t\t\treturn \"All members of the groups and channels:\";\n\t\t}\n\t\tif (!data.all && !anyGroup && anyChannel && channels.size() == 1) {\n\t\t\treturn \"All users who joined the channel below after this date:\";\n\t\t}\n\t\tif (!data.all && !anyGroup && anyChannel && channels.size() > 1) {\n\t\t\treturn \"All users who joined the channels below after this date:\";\n\t\t}\n\t\tif (!data.all && anyGroup && !anyChannel && channels.size() == 1) {\n\t\t\treturn \"All users who joined the group below after this date:\";\n\t\t}\n\t\tif (!data.all && anyGroup && !anyChannel && channels.size() > 1) {\n\t\t\treturn \"All users who joined the groups below after this date:\";\n\t\t}\n\t\tif (!data.all && anyGroup && anyChannel && channels.size() == 1) {\n\t\t\treturn \"All users who joined the group below after this date:\";\n\t\t}\n\t\tif (!data.all && anyGroup && anyChannel && channels.size() > 1) {\n\t\t\treturn \"All users who joined the groups and channels below \"\n\t\t\t\t\"after this date:\";\n\t\t}\n\t\treturn \"\";\n\t}();\n\n\tresult.append(SerializeString(participants)) + channels.join(\", \");\n\tresult.append(popTag());\n\n\t{\n\t\tconst auto &instance = Countries::Instance();\n\t\tauto countries = QStringList();\n\t\tfor (const auto &country : data.countries) {\n\t\t\tconst auto name = instance.countryNameByISO2(country);\n\t\t\tconst auto flag = instance.flagEmojiByISO2(country);\n\t\t\tcountries.push_back(flag + QChar(0xA0) + name);\n\t\t}\n\n\t\tif (const auto count = countries.size()) {\n\t\t\tauto united = countries.front();\n\t\t\tfor (auto i = 1; i != count; ++i) {\n\t\t\t\tunited = ((i + 1 == count)\n\t\t\t\t\t? u\"%1 and %2\"_q\n\t\t\t\t\t: u\"%1, %2\"_q).arg(united, countries[i]);\n\t\t\t}\n\t\t\tresult.append(pushDiv(\"section_body\"));\n\t\t\tresult.append(\n\t\t\t\tSerializeString((u\"from %1\"_q).arg(united).toUtf8()));\n\t\t\tresult.append(popTag());\n\t\t}\n\t}\n\tresult.append(pushDiv(\"section_title bold\"));\n\tresult.append(SerializeString(\"Winners Selection Date\"));\n\tresult.append(popTag());\n\tresult.append(pushDiv(\"section_body\"));\n\tresult.append(Data::FormatDateTime(data.untilDate));\n\tresult.append(popTag());\n\n\tresult.append(popTag());\n\tresult.append(popTag());\n\treturn result;\n}\n\nQByteArray HtmlWriter::Wrap::pushGiveaway(\n\t\tconst PeersMap &peers,\n\t\tconst Data::GiveawayResults &data,\n\t\tFn<QByteArray(int messageId, QByteArray text)> wrapMessageLink) {\n\tauto result = pushDiv(\"media_wrap clearfix\");\n\tresult.append(pushDiv(\"media_giveaway\"));\n\n\tresult.append(pushDiv(\"section_title bold\"));\n\tresult.append((data.winnersCount > 1)\n\t\t? SerializeString(\"Winners Selected!\")\n\t\t: SerializeString(\"Winner Selected!\"));\n\tresult.append(popTag());\n\n\tresult.append(pushDiv(\"section_body\"));\n\tresult.append(\n\t\t\"<b>\" + Data::NumberToString(data.winnersCount) + \"</b> \"\n\t\t+ SerializeString((data.winnersCount > 1) ? \"winners\" : \"winner\")\n\t\t+ \" of the \"\n\t\t+ wrapMessageLink(data.launchId, \"Giveaway\")\n\t\t+ \" was randomly selected by Telegram.\");\n\tresult.append(popTag());\n\n\tresult.append(pushDiv(\"section_title bold\"));\n\tresult.append((data.winnersCount > 1)\n\t\t? SerializeString(\"Winners\")\n\t\t: SerializeString(\"Winner\"));\n\tresult.append(popTag());\n\n\tresult.append(pushDiv(\"section_body\"));\n\tauto winners = QByteArrayList();\n\tfor (const auto &winner : data.winners) {\n\t\twinners.append(\"<b>\" + peers.wrapPeerName(winner) + \"</b>\");\n\t}\n\tconst auto andMore = [&, size = data.winners.size()] {\n\t\tif (data.winnersCount > size) {\n\t\t\treturn SerializeString(\" and \")\n\t\t\t\t+ Data::NumberToString(data.winnersCount - size)\n\t\t\t\t+ SerializeString(\" more!\");\n\t\t}\n\t\treturn QByteArray();\n\t}();\n\tresult.append(winners.join(\", \") + andMore);\n\tresult.append(popTag());\n\n\tresult.append(pushDiv(\"section_body\"));\n\tconst auto prize = [&, singleStar = (data.credits == 1)] {\n\t\tif (data.credits && data.winnersCount == 1) {\n\t\t\treturn SerializeString(\"The winner received \")\n\t\t\t\t+ \"<b>\"\n\t\t\t\t+ Data::NumberToString(data.credits)\n\t\t\t\t+ \"</b>\"\n\t\t\t\t+ SerializeString(singleStar ? \" Star.\" : \" Stars.\");\n\t\t} else if (data.credits && data.winnersCount > 1) {\n\t\t\treturn SerializeString(\"All winners received \")\n\t\t\t\t+ \"<b>\"\n\t\t\t\t+ Data::NumberToString(data.credits)\n\t\t\t\t+ \"</b>\"\n\t\t\t\t+ SerializeString(singleStar\n\t\t\t\t\t? \" Star in total.\"\n\t\t\t\t\t: \" Stars in total.\");\n\t\t} else if (data.unclaimedCount) {\n\t\t\treturn SerializeString(\"Some winners couldn't be selected.\");\n\t\t} else if (data.winnersCount == 1) {\n\t\t\treturn SerializeString(\n\t\t\t\t\"The winner received their gift link in a private message.\");\n\t\t} else if (data.winnersCount > 1) {\n\t\t\treturn SerializeString(\n\t\t\t\t\"All winners received gift links in private messages.\");\n\t\t}\n\t\treturn QByteArray();\n\t}();\n\tresult.append(prize);\n\tresult.append(popTag());\n\n\tresult.append(popTag());\n\tresult.append(popTag());\n\treturn result;\n}\n\nMediaData HtmlWriter::Wrap::prepareMediaData(\n\t\tconst Data::Message &message,\n\t\tconst QString &basePath,\n\t\tconst PeersMap &peers,\n\t\tconst QString &internalLinksDomain) const {\n\tusing namespace Data;\n\n\tauto result = MediaData();\n\tconst auto &action = message.action;\n\tif (const auto call = std::get_if<ActionPhoneCall>(&action.content)) {\n\t\tresult.classes = \"media_call\";\n\t\tresult.title = peers.peer(message.out\n\t\t\t? message.peerId\n\t\t\t: message.selfId).name();\n\t\tresult.status = [&] {\n\t\t\tusing State = ActionPhoneCall::State;\n\t\t\tconst auto state = call->state;\n\t\t\tif (state == State::Invitation) {\n\t\t\t\treturn \"Invitation\";\n\t\t\t} else if (state == State::Active) {\n\t\t\t\treturn \"Ongoing\";\n\t\t\t} else if (message.out) {\n\t\t\t\treturn (state == State::Missed) ? \"Cancelled\" : \"Outgoing\";\n\t\t\t} else if (state == State::Missed) {\n\t\t\t\treturn \"Missed\";\n\t\t\t} else if (state == State::Busy) {\n\t\t\t\treturn \"Declined\";\n\t\t\t}\n\t\t\treturn \"Incoming\";\n\t\t}();\n\t\tif (call->duration > 0) {\n\t\t\tresult.classes += \" success\";\n\t\t\tresult.status += \" (\"\n\t\t\t\t+ NumberToString(call->duration)\n\t\t\t\t+ \" seconds)\";\n\t\t}\n\t\treturn result;\n\t}\n\n\tv::match(message.media.content, [&](const Photo &data) {\n\t\tif (message.media.ttl) {\n\t\t\tresult.title = \"Self-destructing photo\";\n\t\t\tresult.status = data.id\n\t\t\t\t? \"Please view it on your mobile\"\n\t\t\t\t: \"Expired\";\n\t\t\tresult.classes = \"media_photo\";\n\t\t\treturn;\n\t\t}\n\t\t// At least try to pushPhotoMedia.\n\t}, [&](const Document &data) {\n\t\tif (message.media.ttl) {\n\t\t\tresult.title = \"Self-destructing video\";\n\t\t\tresult.status = data.id\n\t\t\t\t? \"Please view it on your mobile\"\n\t\t\t\t: \"Expired\";\n\t\t\tresult.classes = \"media_video\";\n\t\t\treturn;\n\t\t}\n\t\tconst auto hasFile = !data.file.relativePath.isEmpty();\n\t\tresult.link = data.file.relativePath;\n\t\tresult.description = NoFileDescription(data.file.skipReason);\n\t\tif (data.isSticker) {\n\t\t\t// At least try to pushStickerMedia.\n\t\t} else if (data.isVideoMessage) {\n\t\t\tresult.title = \"Video message\";\n\t\t\tresult.status = FormatDuration(data.duration);\n\t\t\tif (!hasFile) {\n\t\t\t\tresult.status += \", \" + FormatFileSize(data.file.size);\n\t\t\t}\n\t\t\tresult.thumb = data.thumb.file.relativePath;\n\t\t\tresult.classes = \"media_video\";\n\t\t} else if (data.isVoiceMessage) {\n\t\t\tresult.title = \"Voice message\";\n\t\t\tresult.status = FormatDuration(data.duration);\n\t\t\tif (!hasFile) {\n\t\t\t\tresult.status += \", \" + FormatFileSize(data.file.size);\n\t\t\t}\n\t\t\tresult.classes = \"media_voice_message\";\n\t\t} else if (data.isAnimated) {\n\t\t\t// At least try to pushAnimatedMedia.\n\t\t} else if (data.isVideoFile) {\n\t\t\t// At least try to pushVideoFileMedia.\n\t\t} else if (data.isAudioFile) {\n\t\t\tresult.title = (!data.songPerformer.isEmpty()\n\t\t\t\t&& !data.songTitle.isEmpty())\n\t\t\t\t? (data.songPerformer + \" \\xe2\\x80\\x93 \" + data.songTitle)\n\t\t\t\t: !data.name.isEmpty()\n\t\t\t\t? data.name\n\t\t\t\t: QByteArray(\"Audio file\");\n\t\t\tresult.status = FormatDuration(data.duration);\n\t\t\tif (!hasFile) {\n\t\t\t\tresult.status += \", \" + FormatFileSize(data.file.size);\n\t\t\t}\n\t\t\tresult.classes = \"media_audio_file\";\n\t\t} else {\n\t\t\tresult.title = data.name.isEmpty()\n\t\t\t\t? QByteArray(\"File\")\n\t\t\t\t: data.name;\n\t\t\tresult.status = FormatFileSize(data.file.size);\n\t\t\tresult.classes = \"media_file\";\n\t\t}\n\t}, [&](const SharedContact &data) {\n\t\tresult.title = data.info.firstName + ' ' + data.info.lastName;\n\t\tresult.classes = \"media_contact\";\n\t\tresult.status = FormatPhoneNumber(data.info.phoneNumber);\n\t\tif (!data.vcard.content.isEmpty()) {\n\t\t\tresult.status += \" - vCard\";\n\t\t\tresult.link = data.vcard.relativePath;\n\t\t}\n\t}, [&](const GeoPoint &data) {\n\t\tif (message.media.ttl) {\n\t\t\tresult.classes = \"media_live_location\";\n\t\t\tresult.title = \"Live location\";\n\t\t\tresult.status = \"\";\n\t\t} else {\n\t\t\tresult.classes = \"media_location\";\n\t\t\tresult.title = \"Location\";\n\t\t}\n\t\tif (data.valid) {\n\t\t\tconst auto latitude = NumberToString(data.latitude);\n\t\t\tconst auto longitude = NumberToString(data.longitude);\n\t\t\tconst auto coords = latitude + ',' + longitude;\n\t\t\tresult.status = latitude + \", \" + longitude;\n\t\t\tresult.link = \"https://maps.google.com/maps?q=\"\n\t\t\t\t+ coords\n\t\t\t\t+ \"&ll=\"\n\t\t\t\t+ coords\n\t\t\t\t+ \"&z=16\";\n\t\t}\n\t}, [&](const Venue &data) {\n\t\tresult.classes = \"media_venue\";\n\t\tresult.title = data.title;\n\t\tresult.description = data.address;\n\t\tif (data.point.valid) {\n\t\t\tconst auto latitude = NumberToString(data.point.latitude);\n\t\t\tconst auto longitude = NumberToString(data.point.longitude);\n\t\t\tconst auto coords = latitude + ',' + longitude;\n\t\t\tresult.link = \"https://maps.google.com/maps?q=\"\n\t\t\t\t+ coords\n\t\t\t\t+ \"&ll=\"\n\t\t\t\t+ coords\n\t\t\t\t+ \"&z=16\";\n\t\t}\n\t}, [&](const Game &data) {\n\t\tresult.classes = \"media_game\";\n\t\tresult.title = data.title;\n\t\tresult.description = data.description;\n\t\tif (data.botId != 0 && !data.shortName.isEmpty()) {\n\t\t\tconst auto bot = peers.user(data.botId);\n\t\t\tif (bot.isBot && !bot.username.isEmpty()) {\n\t\t\t\tconst auto link = internalLinksDomain.toUtf8()\n\t\t\t\t\t+ bot.username\n\t\t\t\t\t+ \"?game=\"\n\t\t\t\t\t+ data.shortName;\n\t\t\t\tresult.link = link;\n\t\t\t\tresult.status = link;\n\t\t\t}\n\t\t}\n\t}, [&](const Invoice &data) {\n\t\tresult.classes = \"media_invoice\";\n\t\tresult.title = data.title;\n\t\tresult.description = data.description;\n\t\tresult.status = Data::FormatMoneyAmount(data.amount, data.currency);\n\t}, [](const Poll &data) {\n\t}, [](const TodoList &data) {\n\t}, [](const GiveawayStart &data) {\n\t}, [](const GiveawayResults &data) {\n\t}, [&](const PaidMedia &data) {\n\t\tresult.classes = \"media_invoice\";\n\t\tresult.status = Data::FormatMoneyAmount(data.stars, \"XTR\");\n\t}, [](const UnsupportedMedia &data) {\n\t\tUnexpected(\"Unsupported message.\");\n\t}, [](v::null_t) {});\n\treturn result;\n}\n\nbool HtmlWriter::Wrap::forwardedNeedsWrap(\n\t\tconst Data::Message &message,\n\t\tconst MessageInfo *previous) const {\n\tExpects(message.forwarded);\n\n\tif (messageNeedsWrap(message, previous)) {\n\t\treturn true;\n\t} else if (!message.forwardedFromId\n\t\t|| message.forwardedFromId != previous->forwardedFromId) {\n\t\treturn true;\n\t} else if (!peerIsUser(message.forwardedFromId)) {\n\t\treturn true;\n\t} else if (abs(message.forwardedDate - previous->forwardedDate)\n\t\t> kJoinWithinSeconds) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nResult HtmlWriter::Wrap::close() {\n\tif (!std::exchange(_closed, true) && !_file.empty()) {\n\t\tauto block = QByteArray();\n\t\twhile (!_context.empty()) {\n\t\t\tblock.append(_context.popTag());\n\t\t}\n\t\treturn _file.writeBlock(block);\n\t}\n\treturn Result::Success();\n}\n\nQString HtmlWriter::Wrap::relativePath(const QString &path) const {\n\treturn _base + path;\n}\n\nQString HtmlWriter::Wrap::relativePath(const Data::File &file) const {\n\treturn relativePath(file.relativePath);\n}\n\nQByteArray HtmlWriter::Wrap::composeStart() {\n\tauto result = \"<!DOCTYPE html>\" + _context.pushTag(\"html\");\n\tresult.append(pushTag(\"head\"));\n\tresult.append(pushTag(\"meta\", {\n\t\t{ \"charset\", \"utf-8\" },\n\t\t{ \"empty\", \"\" }\n\t}));\n\tresult.append(pushTag(\"title\", { { \"inline\", \"\" } }));\n\tresult.append(\"Exported Data\");\n\tresult.append(popTag());\n\tresult.append(_context.pushTag(\"meta\", {\n\t\t{ \"name\", \"viewport\" },\n\t\t{ \"content\", \"width=device-width, initial-scale=1.0\" },\n\t\t{ \"empty\", \"\" }\n\t}));\n\tresult.append(_context.pushTag(\"link\", {\n\t\t{ \"href\", _base + \"css/style.css\" },\n\t\t{ \"rel\", \"stylesheet\" },\n\t\t{ \"empty\", \"\" }\n\t}));\n\tresult.append(_context.pushTag(\"script\", {\n\t\t{ \"src\", _base + \"js/script.js\" },\n\t\t{ \"type\", \"text/javascript\" },\n\t}));\n\tresult.append(_context.popTag());\n\tresult.append(popTag());\n\tresult.append(pushTag(\"body\", {\n\t\t{ \"onload\", \"CheckLocation();\" }\n\t}));\n\tresult.append(pushDiv(\"page_wrap\"));\n\treturn result;\n}\n\nHtmlWriter::Wrap::~Wrap() {\n\t(void)close();\n}\n\nHtmlWriter::HtmlWriter() = default;\n\nResult HtmlWriter::start(\n\t\tconst Settings &settings,\n\t\tconst Environment &environment,\n\t\tStats *stats) {\n\tExpects(settings.path.endsWith('/'));\n\n\t_settings = base::duplicate(settings);\n\t_environment = environment;\n\t_stats = stats;\n\n\t//const auto result = copyFile(\n\t//\t\":/export/css/bootstrap.min.css\",\n\t//\t\"css/bootstrap.min.css\");\n\t//if (!result) {\n\t//\treturn result;\n\t//}\n\tconst auto copy = [&](const QString &filename) {\n\t\treturn copyFile(\":/export/\" + filename, filename);\n\t};\n\tconst auto files = {\n\t\t\"css/style.css\",\n\t\t\"images/back.png\",\n\t\t\"images/media_call.png\",\n\t\t\"images/media_contact.png\",\n\t\t\"images/media_file.png\",\n\t\t\"images/media_game.png\",\n\t\t\"images/media_location.png\",\n\t\t\"images/media_music.png\",\n\t\t\"images/media_photo.png\",\n\t\t\"images/media_shop.png\",\n\t\t\"images/media_video.png\",\n\t\t\"images/media_voice.png\",\n\t\t\"images/section_calls.png\",\n\t\t\"images/section_chats.png\",\n\t\t\"images/section_contacts.png\",\n\t\t\"images/section_frequent.png\",\n\t\t\"images/section_music.png\",\n\t\t\"images/section_other.png\",\n\t\t\"images/section_photos.png\",\n\t\t\"images/section_sessions.png\",\n\t\t\"images/section_stories.png\",\n\t\t\"images/section_web.png\",\n\t\t\"js/script.js\",\n\t};\n\tfor (const auto path : files) {\n\t\tconst auto name = QString(path);\n\t\tif (const auto result = copy(name); !result) {\n\t\t\treturn result;\n\t\t} else if (const auto png = name.indexOf(\".png\"); png > 0) {\n\t\t\tconst auto x2 = name.mid(0, png) + \"@2x.png\";\n\t\t\tif (const auto result = copy(x2); !result) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (_settings.onlySinglePeer()) {\n\t\treturn Result::Success();\n\t}\n\t_summary = fileWithRelativePath(mainFileRelativePath());\n\tauto block = _summary->pushHeader(\"Exported Data\");\n\tblock.append(_summary->pushDiv(\"page_body\"));\n\treturn _summary->writeBlock(block);\n}\n\nResult HtmlWriter::writePersonal(const Data::PersonalInfo &data) {\n\tExpects(_summary != nullptr);\n\n\t_selfColorIndex = data.user.info.colorIndex;\n\tif (_settings.types & Settings::Type::Userpics) {\n\t\t_delayedPersonalInfo = std::make_unique<Data::PersonalInfo>(data);\n\t\treturn Result::Success();\n\t}\n\treturn writeDefaultPersonal(data);\n}\n\nResult HtmlWriter::writeDefaultPersonal(const Data::PersonalInfo &data) {\n\treturn writePreparedPersonal(data, QString());\n}\n\nResult HtmlWriter::writeDelayedPersonal(const QString &userpicPath) {\n\tif (!_delayedPersonalInfo) {\n\t\treturn Result::Success();\n\t}\n\tconst auto result = writePreparedPersonal(\n\t\t*base::take(_delayedPersonalInfo),\n\t\tuserpicPath);\n\tif (!result) {\n\t\treturn result;\n\t}\n\tif (_userpicsCount) {\n\t\tpushUserpicsSection();\n\t}\n\treturn Result::Success();\n}\n\nResult HtmlWriter::writePreparedPersonal(\n\t\tconst Data::PersonalInfo &data,\n\t\tconst QString &userpicPath) {\n\tExpects(_summary != nullptr);\n\n\tconst auto &info = data.user.info;\n\n\tauto userpic = UserpicData{ _selfColorIndex, kPersonalUserpicSize };\n\tuserpic.largeLink = userpicPath.isEmpty()\n\t\t? QString()\n\t\t: userpicsFilePath();\n\tuserpic.imageLink = WriteUserpicThumb(\n\t\t_settings.path,\n\t\tuserpicPath,\n\t\tuserpic,\n\t\t\"_info\");\n\tuserpic.firstName = info.firstName;\n\tuserpic.lastName = info.lastName;\n\n\tauto block = _summary->pushDiv(\"personal_info clearfix\");\n\tblock.append(_summary->pushDiv(\"pull_right userpic_wrap\"));\n\tblock.append(_summary->pushUserpic(userpic));\n\tblock.append(_summary->popTag());\n\tconst auto pushRows = [&](\n\t\t\tQByteArray name,\n\t\t\tstd::vector<std::pair<QByteArray, QByteArray>> &&values) {\n\t\tblock.append(_summary->pushDiv(\"rows \" + name));\n\t\tfor (const auto &[key, value] : values) {\n\t\t\tif (value.isEmpty()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tblock.append(_summary->pushDiv(\"row\"));\n\t\t\tblock.append(_summary->pushDiv(\"label details\"));\n\t\t\tblock.append(SerializeString(key));\n\t\t\tblock.append(_summary->popTag());\n\t\t\tblock.append(_summary->pushDiv(\"value bold\"));\n\t\t\tblock.append(SerializeString(value));\n\t\t\tblock.append(_summary->popTag());\n\t\t\tblock.append(_summary->popTag());\n\t\t}\n\t\tblock.append(_summary->popTag());\n\t};\n\tpushRows(\"names\", {\n\t\t{ \"First name\", info.firstName },\n\t\t{ \"Last name\", info.lastName },\n\t});\n\tpushRows(\"info\", {\n\t\t{ \"Phone number\", Data::FormatPhoneNumber(info.phoneNumber) },\n\t\t{ \"Username\", FormatUsername(data.user.username) },\n\t});\n\tpushRows(\"bio\", { { \"Bio\", data.bio } });\n\tblock.append(_summary->popTag());\n\n\t_summaryNeedDivider = true;\n\treturn _summary->writeBlock(block);\n}\n\nResult HtmlWriter::writeUserpicsStart(const Data::UserpicsInfo &data) {\n\tExpects(_summary != nullptr);\n\tExpects(_userpics == nullptr);\n\n\t_userpicsCount = data.count;\n\tif (!_userpicsCount) {\n\t\treturn Result::Success();\n\t}\n\t_userpics = fileWithRelativePath(userpicsFilePath());\n\n\tauto block = _userpics->pushHeader(\n\t\t\"Profile pictures\",\n\t\tmainFileRelativePath());\n\tblock.append(_userpics->pushDiv(\"page_body list_page\"));\n\tblock.append(_userpics->pushDiv(\"entry_list\"));\n\tif (const auto result = _userpics->writeBlock(block); !result) {\n\t\treturn result;\n\t}\n\tif (!_delayedPersonalInfo) {\n\t\tpushUserpicsSection();\n\t}\n\treturn Result::Success();\n}\n\nResult HtmlWriter::writeUserpicsSlice(const Data::UserpicsSlice &data) {\n\tExpects(_userpics != nullptr);\n\tExpects(!data.list.empty());\n\n\tconst auto firstPath = data.list.front().image.file.relativePath;\n\tif (const auto result = writeDelayedPersonal(firstPath); !result) {\n\t\treturn result;\n\t}\n\n\tauto block = QByteArray();\n\tfor (const auto &userpic : data.list) {\n\t\tauto data = UserpicData{ _selfColorIndex, kEntryUserpicSize };\n\t\tusing SkipReason = Data::File::SkipReason;\n\t\tconst auto &file = userpic.image.file;\n\t\tAssert(!file.relativePath.isEmpty()\n\t\t\t|| file.skipReason != SkipReason::None);\n\t\tconst auto status = [&]() -> Data::Utf8String {\n\t\t\tswitch (file.skipReason) {\n\t\t\tcase SkipReason::Unavailable:\n\t\t\t\treturn \"(Photo unavailable, please try again later)\";\n\t\t\tcase SkipReason::FileSize:\n\t\t\t\treturn \"(Photo exceeds maximum size. \"\n\t\t\t\t\t\"Change data exporting settings to download.)\";\n\t\t\tcase SkipReason::FileType:\n\t\t\t\treturn \"(Photo not included. \"\n\t\t\t\t\t\"Change data exporting settings to download.)\";\n\t\t\tcase SkipReason::None: return Data::FormatFileSize(file.size);\n\t\t\t}\n\t\t\tUnexpected(\"Skip reason while writing photo path.\");\n\t\t}();\n\t\tconst auto &path = userpic.image.file.relativePath;\n\t\tdata.imageLink = WriteUserpicThumb(_settings.path, path, data);\n\t\tdata.firstName = path.toUtf8();\n\t\tblock.append(_userpics->pushListEntry(\n\t\t\tdata,\n\t\t\t(path.isEmpty() ? QString(\"Photo unavailable\") : path).toUtf8(),\n\t\t\tstatus,\n\t\t\t(userpic.date > 0\n\t\t\t\t? Data::FormatDateTime(userpic.date)\n\t\t\t\t: QByteArray()),\n\t\t\tpath));\n\t}\n\treturn _userpics->writeBlock(block);\n}\n\nResult HtmlWriter::writeUserpicsEnd() {\n\tif (const auto result = writeDelayedPersonal(QString()); !result) {\n\t\treturn result;\n\t} else if (_userpics) {\n\t\treturn base::take(_userpics)->close();\n\t}\n\treturn Result::Success();\n}\n\nQString HtmlWriter::userpicsFilePath() const {\n\treturn \"lists/profile_pictures.html\";\n}\n\nvoid HtmlWriter::pushUserpicsSection() {\n\tpushSection(\n\t\tkUserpicsPriority,\n\t\t\"Profile pictures\",\n\t\t\"photos\",\n\t\t_userpicsCount,\n\t\tuserpicsFilePath());\n}\n\nResult HtmlWriter::writeStoriesStart(const Data::StoriesInfo &data) {\n\tExpects(_summary != nullptr);\n\tExpects(_stories == nullptr);\n\n\t_storiesCount = data.count;\n\tif (!_storiesCount) {\n\t\treturn Result::Success();\n\t}\n\t_stories = fileWithRelativePath(storiesFilePath());\n\n\tauto block = _stories->pushHeader(\n\t\t\"Stories archive\",\n\t\tmainFileRelativePath());\n\tblock.append(_stories->pushDiv(\"page_body list_page\"));\n\tblock.append(_stories->pushDiv(\"entry_list\"));\n\tif (const auto result = _stories->writeBlock(block); !result) {\n\t\treturn result;\n\t}\n\treturn Result::Success();\n}\n\nResult HtmlWriter::writeStoriesSlice(const Data::StoriesSlice &data) {\n\tExpects(_stories != nullptr);\n\n\t_storiesCount -= data.skipped;\n\tif (data.list.empty()) {\n\t\treturn Result::Success();\n\t}\n\tauto block = QByteArray();\n\tfor (const auto &story : data.list) {\n\t\tauto data = StoryData{};\n\t\tusing SkipReason = Data::File::SkipReason;\n\t\tconst auto &file = story.file();\n\t\tAssert(!file.relativePath.isEmpty()\n\t\t\t|| file.skipReason != SkipReason::None);\n\t\tauto status = QByteArrayList();\n\t\tif (story.pinned) {\n\t\t\tstatus.append(\"Saved to Profile\");\n\t\t}\n\t\tif (story.expires > 0) {\n\t\t\tstatus.append(\"Expiring: \" + Data::FormatDateTime(story.expires));\n\t\t}\n\t\tstatus.append([&]() -> Data::Utf8String {\n\t\t\tswitch (file.skipReason) {\n\t\t\tcase SkipReason::Unavailable:\n\t\t\t\treturn \"(Story unavailable, please try again later)\";\n\t\t\tcase SkipReason::FileSize:\n\t\t\t\treturn \"(Story exceeds maximum size. \"\n\t\t\t\t\t\"Change data exporting settings to download.)\";\n\t\t\tcase SkipReason::FileType:\n\t\t\t\treturn \"(Story not included. \"\n\t\t\t\t\t\"Change data exporting settings to download.)\";\n\t\t\tcase SkipReason::None: return Data::FormatFileSize(file.size);\n\t\t\t}\n\t\t\tUnexpected(\"Skip reason while writing story path.\");\n\t\t}());\n\t\tconst auto &path = story.file().relativePath;\n\t\tconst auto &image = story.thumb().file.relativePath.isEmpty()\n\t\t\t? story.file().relativePath\n\t\t\t: story.thumb().file.relativePath;\n\t\tdata.imageLink = Data::WriteImageThumb(\n\t\t\t_settings.path,\n\t\t\timage,\n\t\t\tkStoryThumbWidth * 2,\n\t\t\tkStoryThumbHeight * 2);\n\t\tconst auto info = (story.date > 0)\n\t\t\t? Data::FormatDateTime(story.date)\n\t\t\t: QByteArray();\n\t\tblock.append(_stories->pushStoriesListEntry(\n\t\t\tdata,\n\t\t\t(path.isEmpty() ? QString(\"Story unavailable\") : path).toUtf8(),\n\t\t\tstatus,\n\t\t\tinfo,\n\t\t\tstory.caption,\n\t\t\t_environment.internalLinksDomain,\n\t\t\tpath));\n\t}\n\treturn _stories->writeBlock(block);\n}\n\nResult HtmlWriter::writeStoriesEnd() {\n\tpushStoriesSection();\n\tif (_stories) {\n\t\treturn base::take(_stories)->close();\n\t}\n\treturn Result::Success();\n}\n\nResult HtmlWriter::writeProfileMusicStart(const Data::ProfileMusicInfo &data) {\n\tExpects(_summary != nullptr);\n\tExpects(_profileMusic == nullptr);\n\n\t_profileMusicCount = data.count;\n\tif (!_profileMusicCount) {\n\t\treturn Result::Success();\n\t}\n\t_profileMusic = fileWithRelativePath(profileMusicFilePath());\n\n\tauto block = _profileMusic->pushHeader(\n\t\t\"Profile Music\",\n\t\tmainFileRelativePath());\n\tblock.append(_profileMusic->pushDiv(\"page_body list_page\"));\n\tblock.append(_profileMusic->pushDiv(\"entry_list\"));\n\tif (const auto result = _profileMusic->writeBlock(block); !result) {\n\t\treturn result;\n\t}\n\treturn Result::Success();\n}\n\nResult HtmlWriter::writeProfileMusicSlice(const Data::ProfileMusicSlice &data) {\n\tExpects(_profileMusic != nullptr);\n\n\t_profileMusicCount -= data.skipped;\n\tif (data.list.empty()) {\n\t\treturn Result::Success();\n\t}\n\tauto block = QByteArray();\n\tfor (const auto &message : data.list) {\n\t\tif (!v::is<Data::Document>(message.media.content)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto &doc = v::get<Data::Document>(message.media.content);\n\t\tif (!doc.isAudioFile) {\n\t\t\tcontinue;\n\t\t}\n\t\tusing SkipReason = Data::File::SkipReason;\n\t\tconst auto &file = doc.file;\n\t\tAssert(!file.relativePath.isEmpty()\n\t\t\t|| file.skipReason != SkipReason::None);\n\t\tauto status = QByteArrayList();\n\t\tstatus.append([&]() -> Data::Utf8String {\n\t\t\tswitch (file.skipReason) {\n\t\t\tcase SkipReason::Unavailable:\n\t\t\t\treturn \"(File unavailable, please try again later)\";\n\t\t\tcase SkipReason::FileSize:\n\t\t\t\treturn \"(File exceeds maximum size. \"\n\t\t\t\t\t\"Change data exporting settings to download.)\";\n\t\t\tcase SkipReason::FileType:\n\t\t\t\treturn \"(File not included. \"\n\t\t\t\t\t\"Change data exporting settings to download.)\";\n\t\t\tcase SkipReason::None: return Data::FormatFileSize(file.size);\n\t\t\t}\n\t\t\tUnexpected(\"Skip reason while writing profile music path.\");\n\t\t}());\n\t\tconst auto &path = file.relativePath;\n\t\tconst auto title = !doc.songTitle.isEmpty()\n\t\t\t? doc.songTitle\n\t\t\t: !doc.name.isEmpty()\n\t\t\t? doc.name\n\t\t\t: Data::Utf8String(\"Unknown Track\");\n\t\tconst auto performer = !doc.songPerformer.isEmpty()\n\t\t\t? doc.songPerformer\n\t\t\t: Data::Utf8String(\"Unknown Artist\");\n\t\tconst auto info = performer + \" - \" + title;\n\t\tconst auto duration = doc.duration > 0\n\t\t\t? Data::FormatDuration(doc.duration)\n\t\t\t: QByteArray();\n\t\tblock.append(_profileMusic->pushAudioEntry(\n\t\t\t(path.isEmpty() ? QString(\"File unavailable\") : path).toUtf8(),\n\t\t\tinfo,\n\t\t\tstatus,\n\t\t\tduration,\n\t\t\tpath));\n\t}\n\treturn _profileMusic->writeBlock(block);\n}\n\nResult HtmlWriter::writeProfileMusicEnd() {\n\tpushProfileMusicSection();\n\tif (_profileMusic) {\n\t\treturn base::take(_profileMusic)->close();\n\t}\n\treturn Result::Success();\n}\n\nQString HtmlWriter::storiesFilePath() const {\n\treturn \"lists/stories.html\";\n}\n\nvoid HtmlWriter::pushStoriesSection() {\n\tpushSection(\n\t\tkStoriesPriority,\n\t\t\"Stories archive\",\n\t\t\"stories\",\n\t\t_storiesCount,\n\t\tstoriesFilePath());\n}\n\nQString HtmlWriter::profileMusicFilePath() const {\n\treturn \"lists/profile_music.html\";\n}\n\nvoid HtmlWriter::pushProfileMusicSection() {\n\tpushSection(\n\t\tkProfileMusicPriority,\n\t\t\"Profile Music\",\n\t\t\"music\",\n\t\t_profileMusicCount,\n\t\tprofileMusicFilePath());\n}\n\nResult HtmlWriter::writeContactsList(const Data::ContactsList &data) {\n\tExpects(_summary != nullptr);\n\n\tif (const auto result = writeSavedContacts(data); !result) {\n\t\treturn result;\n\t} else if (const auto result = writeFrequentContacts(data); !result) {\n\t\treturn result;\n\t}\n\treturn Result::Success();\n}\n\nResult HtmlWriter::writeSavedContacts(const Data::ContactsList &data) {\n\tif (data.list.empty()) {\n\t\treturn Result::Success();\n\t}\n\n\tconst auto filename = \"lists/contacts.html\";\n\tconst auto file = fileWithRelativePath(filename);\n\tauto block = file->pushHeader(\n\t\t\"Contacts\",\n\t\tmainFileRelativePath());\n\tblock.append(file->pushDiv(\"page_body list_page\"));\n\tblock.append(file->pushAbout(_environment.aboutContacts));\n\tblock.append(file->pushDiv(\"entry_list\"));\n\tfor (const auto index : Data::SortedContactsIndices(data)) {\n\t\tconst auto &contact = data.list[index];\n\t\tauto userpic = UserpicData{\n\t\t\tData::ContactColorIndex(contact),\n\t\t\tkEntryUserpicSize\n\t\t};\n\t\tuserpic.firstName = contact.firstName;\n\t\tuserpic.lastName = contact.lastName;\n\t\tif (contact.userId) {\n\t\t\tconst auto raw = contact.userId.bare & PeerId::kChatTypeMask;\n\t\t\tuserpic.tooltip = (u\"ID: \"_q + QString::number(raw)).toUtf8();\n\t\t}\n\t\tblock.append(file->pushListEntry(\n\t\t\tuserpic,\n\t\t\tComposeName(userpic, \"Deleted Account\"),\n\t\t\tData::FormatPhoneNumber(contact.phoneNumber),\n\t\t\tData::FormatDateTime(contact.date)));\n\t}\n\tif (const auto result = file->writeBlock(block); !result) {\n\t\treturn result;\n\t} else if (const auto closed = file->close(); !closed) {\n\t\treturn closed;\n\t}\n\n\tpushSection(\n\t\tkContactsPriority,\n\t\t\"Contacts\",\n\t\t\"contacts\",\n\t\tdata.list.size(),\n\t\tfilename);\n\treturn Result::Success();\n}\n\nResult HtmlWriter::writeFrequentContacts(const Data::ContactsList &data) {\n\tconst auto size = data.correspondents.size()\n\t\t+ data.inlineBots.size()\n\t\t+ data.phoneCalls.size();\n\tif (!size) {\n\t\treturn Result::Success();\n\t}\n\n\tconst auto filename = \"lists/frequent.html\";\n\tconst auto file = fileWithRelativePath(filename);\n\tauto block = file->pushHeader(\n\t\t\"Frequent contacts\",\n\t\tmainFileRelativePath());\n\tblock.append(file->pushDiv(\"page_body list_page\"));\n\tblock.append(file->pushAbout(_environment.aboutFrequent));\n\tblock.append(file->pushDiv(\"entry_list\"));\n\tconst auto writeList = [&](\n\t\t\tconst std::vector<Data::TopPeer> &peers,\n\t\t\tData::Utf8String category) {\n\t\tfor (const auto &top : peers) {\n\t\t\tconst auto name = [&]() -> Data::Utf8String {\n\t\t\t\tif (top.peer.chat()) {\n\t\t\t\t\treturn top.peer.name();\n\t\t\t\t} else if (top.peer.user()->isSelf) {\n\t\t\t\t\treturn \"Saved messages\";\n\t\t\t\t} else {\n\t\t\t\t\treturn top.peer.user()->info.firstName;\n\t\t\t\t}\n\t\t\t}();\n\t\t\tconst auto lastName = [&]() -> Data::Utf8String {\n\t\t\t\tif (top.peer.user() && !top.peer.user()->isSelf) {\n\t\t\t\t\treturn top.peer.user()->info.lastName;\n\t\t\t\t}\n\t\t\t\treturn {};\n\t\t\t}();\n\t\t\tauto userpic = UserpicData{\n\t\t\t\tData::PeerColorIndex(top.peer.id()),\n\t\t\t\tkEntryUserpicSize\n\t\t\t};\n\t\t\tuserpic.firstName = name;\n\t\t\tuserpic.lastName = lastName;\n\t\t\tblock.append(file->pushListEntry(\n\t\t\t\tuserpic,\n\t\t\t\tComposeName(userpic, \"Deleted Account\"),\n\t\t\t\t\"Rating: \" + Data::NumberToString(top.rating),\n\t\t\t\tcategory));\n\t\t}\n\t};\n\twriteList(data.correspondents, \"people\");\n\twriteList(data.inlineBots, \"inline bots\");\n\twriteList(data.phoneCalls, \"calls\");\n\tif (const auto result = file->writeBlock(block); !result) {\n\t\treturn result;\n\t} else if (const auto closed = file->close(); !closed) {\n\t\treturn closed;\n\t}\n\n\tpushSection(\n\t\tkFrequentContactsPriority,\n\t\t\"Frequent contacts\",\n\t\t\"frequent\",\n\t\tsize,\n\t\tfilename);\n\treturn Result::Success();\n}\n\nResult HtmlWriter::writeSessionsList(const Data::SessionsList &data) {\n\tExpects(_summary != nullptr);\n\n\tif (const auto result = writeSessions(data); !result) {\n\t\treturn result;\n\t} else if (const auto result = writeWebSessions(data); !result) {\n\t\treturn result;\n\t}\n\treturn Result::Success();\n}\n\nResult HtmlWriter::writeSessions(const Data::SessionsList &data) {\n\tExpects(_summary != nullptr);\n\n\tif (data.list.empty()) {\n\t\treturn Result::Success();\n\t}\n\n\tconst auto filename = \"lists/sessions.html\";\n\tconst auto file = fileWithRelativePath(filename);\n\tauto block = file->pushHeader(\n\t\t\"Sessions\",\n\t\tmainFileRelativePath());\n\tblock.append(file->pushDiv(\"page_body list_page\"));\n\tblock.append(file->pushAbout(_environment.aboutSessions));\n\tblock.append(file->pushDiv(\"entry_list\"));\n\tfor (const auto &session : data.list) {\n\t\tblock.append(file->pushSessionListEntry(\n\t\t\tsession.applicationId,\n\t\t\t((session.applicationName.isEmpty()\n\t\t\t\t? Data::Utf8String(\"Unknown\")\n\t\t\t\t: session.applicationName)\n\t\t\t\t+ ' '\n\t\t\t\t+ session.applicationVersion),\n\t\t\t(session.deviceModel\n\t\t\t\t+ \", \"\n\t\t\t\t+ session.platform\n\t\t\t\t+ ' '\n\t\t\t\t+ session.systemVersion),\n\t\t\t{\n\t\t\t\t(session.ip\n\t\t\t\t\t+ \" \\xE2\\x80\\x93 \"\n\t\t\t\t\t+ session.region\n\t\t\t\t\t+ ((session.region.isEmpty() || session.country.isEmpty())\n\t\t\t\t\t\t? QByteArray()\n\t\t\t\t\t\t: QByteArray(\", \"))\n\t\t\t\t\t+ session.country),\n\t\t\t\t\"Last active: \" + Data::FormatDateTime(session.lastActive),\n\t\t\t\t\"Created: \" + Data::FormatDateTime(session.created)\n\t\t\t}));\n\t}\n\tif (const auto result = file->writeBlock(block); !result) {\n\t\treturn result;\n\t} else if (const auto closed = file->close(); !closed) {\n\t\treturn closed;\n\t}\n\n\tpushSection(\n\t\tkSessionsPriority,\n\t\t\"Sessions\",\n\t\t\"sessions\",\n\t\tdata.list.size(),\n\t\tfilename);\n\treturn Result::Success();\n}\n\nResult HtmlWriter::writeWebSessions(const Data::SessionsList &data) {\n\tExpects(_summary != nullptr);\n\n\tif (data.webList.empty()) {\n\t\treturn Result::Success();\n\t}\n\n\tconst auto filename = \"lists/web_sessions.html\";\n\tconst auto file = fileWithRelativePath(filename);\n\tauto block = file->pushHeader(\n\t\t\"Web sessions\",\n\t\tmainFileRelativePath());\n\tblock.append(file->pushDiv(\"page_body list_page\"));\n\tblock.append(file->pushAbout(_environment.aboutWebSessions));\n\tblock.append(file->pushDiv(\"entry_list\"));\n\tfor (const auto &session : data.webList) {\n\t\tblock.append(file->pushSessionListEntry(\n\t\t\tData::DomainApplicationId(session.domain),\n\t\t\t(session.domain.isEmpty()\n\t\t\t\t? Data::Utf8String(\"Unknown\")\n\t\t\t\t: session.domain),\n\t\t\tsession.platform + \", \" + session.browser,\n\t\t\t{\n\t\t\t\tsession.ip + \" \\xE2\\x80\\x93 \" + session.region,\n\t\t\t\t\"Last active: \" + Data::FormatDateTime(session.lastActive),\n\t\t\t\t\"Created: \" + Data::FormatDateTime(session.created)\n\t\t\t},\n\t\t\t(session.botUsername.isEmpty()\n\t\t\t\t? QByteArray()\n\t\t\t\t: ('@' + session.botUsername))));\n\t}\n\tif (const auto result = file->writeBlock(block); !result) {\n\t\treturn result;\n\t} else if (const auto closed = file->close(); !closed) {\n\t\treturn closed;\n\t}\n\n\tpushSection(\n\t\tkWebSessionsPriority,\n\t\t\"Web sessions\",\n\t\t\"web\",\n\t\tdata.webList.size(),\n\t\tfilename);\n\treturn Result::Success();\n}\n\nResult HtmlWriter::writeOtherData(const Data::File &data) {\n\tExpects(_summary != nullptr);\n\n\tpushSection(\n\t\tkOtherPriority,\n\t\t\"Other data\",\n\t\t\"other\",\n\t\t1,\n\t\tdata.relativePath);\n\treturn Result::Success();\n}\n\nResult HtmlWriter::writeDialogsStart(const Data::DialogsInfo &data) {\n\tExpects(_chats == nullptr);\n\n\tif (data.chats.empty() && data.left.empty()) {\n\t\treturn Result::Success();\n\t} else if (_settings.onlySinglePeer()) {\n\t\treturn Result::Success();\n\t}\n\n\t_dialogsRelativePath = \"lists/chats.html\";\n\t_chats = fileWithRelativePath(_dialogsRelativePath);\n\n\tauto block = _chats->pushHeader(\n\t\t\"Chats\",\n\t\tmainFileRelativePath());\n\tblock.append(_chats->pushDiv(\"page_body list_page\"));\n\tif (const auto result = _chats->writeBlock(block); !result) {\n\t\treturn result;\n\t}\n\n\tpushSection(\n\t\tkChatsPriority,\n\t\t\"Chats\",\n\t\t\"chats\",\n\t\tdata.chats.size() + data.left.size(),\n\t\t\"lists/chats.html\");\n\treturn writeSections();\n}\n\nResult HtmlWriter::writeDialogStart(const Data::DialogInfo &data) {\n\tExpects(_chat == nullptr);\n\n\t_chat = fileWithRelativePath(data.relativePath + messagesFile(0));\n\t_chatFileEmpty = true;\n\t_messagesCount = 0;\n\t_dateMessageId = 0;\n\t_lastMessageInfo = nullptr;\n\t_lastMessageIdsPerFile.clear();\n\t_dialog = data;\n\treturn Result::Success();\n}\n\nResult HtmlWriter::writeDialogSlice(const Data::MessagesSlice &data) {\n\tExpects(_chat != nullptr);\n\tExpects(!data.list.empty());\n\n\tconst auto messageLinkWrapper = [&](int messageId, QByteArray text) {\n\t\treturn wrapMessageLink(messageId, text);\n\t};\n\tauto oldIndex = (_messagesCount > 0)\n\t\t? ((_messagesCount - 1) / kMessagesInFile)\n\t\t: 0;\n\tauto previous = _lastMessageInfo.get();\n\tauto saved = std::optional<MessageInfo>();\n\tauto block = QByteArray();\n\tfor (const auto &message : data.list) {\n\t\tif (Data::SkipMessageByDate(message, _settings)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto newIndex = (_messagesCount / kMessagesInFile);\n\t\tif (oldIndex != newIndex) {\n\t\t\tif (const auto result = _chat->writeBlock(block); !result) {\n\t\t\t\treturn result;\n\t\t\t} else if (const auto next = switchToNextChatFile(newIndex)) {\n\t\t\t\tAssert(saved.has_value() || _lastMessageInfo != nullptr);\n\t\t\t\t_lastMessageIdsPerFile.push_back(saved\n\t\t\t\t\t? saved->id\n\t\t\t\t\t: _lastMessageInfo->id);\n\t\t\t\tblock = QByteArray();\n\t\t\t\t_lastMessageInfo = nullptr;\n\t\t\t\tprevious = nullptr;\n\t\t\t\tsaved = std::nullopt;\n\t\t\t\toldIndex = newIndex;\n\t\t\t} else {\n\t\t\t\treturn next;\n\t\t\t}\n\t\t}\n\t\tif (_chatFileEmpty) {\n\t\t\tif (const auto result = writeDialogOpening(oldIndex); !result) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\t_chatFileEmpty = false;\n\t\t}\n\t\tconst auto date = message.date;\n\t\tif (DisplayDate(date, previous ? previous->date : 0)) {\n\t\t\tblock.append(_chat->pushServiceMessage(\n\t\t\t\t--_dateMessageId,\n\t\t\t\t_dialog,\n\t\t\t\t_settings.path,\n\t\t\t\tFormatDateText(date)));\n\t\t}\n\t\tconst auto &[info, content] = _chat->pushMessage(\n\t\t\tmessage,\n\t\t\tprevious,\n\t\t\t_dialog,\n\t\t\t_settings.path,\n\t\t\tdata.peers,\n\t\t\t_environment.internalLinksDomain,\n\t\t\tmessageLinkWrapper);\n\t\tblock.append(content);\n\n\t\t++_messagesCount;\n\t\tsaved = info;\n\t\tprevious = &*saved;\n\t}\n\tif (saved) {\n\t\t_lastMessageInfo = std::make_unique<MessageInfo>(*saved);\n\t}\n\treturn block.isEmpty() ? Result::Success() : _chat->writeBlock(block);\n}\n\nResult HtmlWriter::writeEmptySinglePeer() {\n\tExpects(_chat != nullptr);\n\n\tif (!_settings.onlySinglePeer() || _messagesCount != 0) {\n\t\treturn Result::Success();\n\t}\n\tAssert(_chatFileEmpty);\n\tif (const auto result = writeDialogOpening(0); !result) {\n\t\treturn result;\n\t}\n\treturn _chat->writeBlock(_chat->pushServiceMessage(\n\t\t--_dateMessageId,\n\t\t_dialog,\n\t\t_settings.path,\n\t\t\"No exported messages\"));\n}\n\nResult HtmlWriter::writeDialogEnd() {\n\tExpects(_settings.onlySinglePeer() || _chats != nullptr);\n\tExpects(_chat != nullptr);\n\n\tif (const auto result = writeEmptySinglePeer(); !result) {\n\t\treturn result;\n\t}\n\n\tif (const auto closed = base::take(_chat)->close(); !closed) {\n\t\treturn closed;\n\t} else if (_settings.onlySinglePeer()) {\n\t\treturn Result::Success();\n\t}\n\n\tusing Type = Data::DialogInfo::Type;\n\tconst auto TypeString = [](Type type) {\n\t\tswitch (type) {\n\t\tcase Type::Unknown: return \"unknown\";\n\t\tcase Type::Self:\n\t\tcase Type::Replies:\n\t\tcase Type::VerifyCodes:\n\t\tcase Type::Personal: return \"private\";\n\t\tcase Type::Bot: return \"bot\";\n\t\tcase Type::PrivateGroup:\n\t\tcase Type::PrivateSupergroup:\n\t\tcase Type::PublicSupergroup: return \"group\";\n\t\tcase Type::PrivateChannel:\n\t\tcase Type::PublicChannel: return \"channel\";\n\t\t}\n\t\tUnexpected(\"Dialog type in TypeString.\");\n\t};\n\tconst auto DeletedString = [](Type type) {\n\t\tswitch (type) {\n\t\tcase Type::Unknown:\n\t\tcase Type::Self:\n\t\tcase Type::Replies:\n\t\tcase Type::VerifyCodes:\n\t\tcase Type::Personal:\n\t\tcase Type::Bot: return \"Deleted Account\";\n\t\tcase Type::PrivateGroup:\n\t\tcase Type::PrivateSupergroup:\n\t\tcase Type::PublicSupergroup: return \"Deleted Group\";\n\t\tcase Type::PrivateChannel:\n\t\tcase Type::PublicChannel: return \"Deleted Channel\";\n\t\t}\n\t\tUnexpected(\"Dialog type in TypeString.\");\n\t};\n\tconst auto NameString = [](\n\t\t\tconst Data::DialogInfo &dialog) -> QByteArray {\n\t\tif (dialog.type == Type::Self) {\n\t\t\treturn \"Saved messages\";\n\t\t} else if (dialog.type == Type::Replies) {\n\t\t\treturn \"Replies\";\n\t\t} else if (dialog.type == Type::VerifyCodes) {\n\t\t\treturn \"Verification Codes\";\n\t\t}\n\t\treturn dialog.name;\n\t};\n\tconst auto LastNameString = [](\n\t\t\tconst Data::DialogInfo &dialog) -> QByteArray {\n\t\tif (dialog.type != Type::Personal && dialog.type != Type::Bot) {\n\t\t\treturn {};\n\t\t}\n\t\treturn dialog.lastName;\n\t};\n\tconst auto CountString = [](int count, bool outgoing) -> QByteArray {\n\t\tif (count == 1) {\n\t\t\treturn outgoing ? \"1 outgoing message\" : \"1 message\";\n\t\t} else if (!count) {\n\t\t\treturn outgoing ? \"No outgoing messages\" : \"No messages\";\n\t\t}\n\t\treturn Data::NumberToString(count)\n\t\t\t+ (outgoing ? \" outgoing messages\" : \" messages\");\n\t};\n\tauto userpic = UserpicData{\n\t\t((_dialog.type == Type::Self\n\t\t\t|| _dialog.type == Type::Replies\n\t\t\t|| _dialog.type == Type::VerifyCodes)\n\t\t\t? kSavedMessagesColorIndex\n\t\t\t: Data::PeerColorIndex(_dialog.peerId)),\n\t\tkEntryUserpicSize\n\t};\n\tuserpic.firstName = NameString(_dialog);\n\tuserpic.lastName = LastNameString(_dialog);\n\n\tconst auto result = validateDialogsMode(_dialog.isLeftChannel);\n\tif (!result) {\n\t\treturn result;\n\t}\n\n\treturn _chats->writeBlock(_chats->pushListEntry(\n\t\tuserpic,\n\t\tComposeName(userpic, DeletedString(_dialog.type)),\n\t\tCountString(_messagesCount, _dialog.onlyMyMessages),\n\t\tTypeString(_dialog.type),\n\t\t(_messagesCount > 0\n\t\t\t? (_dialog.relativePath + \"messages.html\")\n\t\t\t: QString())));\n}\n\nResult HtmlWriter::validateDialogsMode(bool isLeftChannel) {\n\tconst auto mode = isLeftChannel\n\t\t? DialogsMode::Left\n\t\t: DialogsMode::Chats;\n\tif (_dialogsMode == mode) {\n\t\treturn Result::Success();\n\t} else if (_dialogsMode != DialogsMode::None) {\n\t\tconst auto result = _chats->writeBlock(_chats->popTag());\n\t\tif (!result) {\n\t\t\treturn result;\n\t\t}\n\t}\n\t_dialogsMode = mode;\n\tauto block = _chats->pushAbout(isLeftChannel\n\t\t? _environment.aboutLeftChats\n\t\t: _environment.aboutChats);\n\tblock.append(_chats->pushDiv(\"entry_list\"));\n\treturn _chats->writeBlock(block);\n}\n\nResult HtmlWriter::writeDialogsEnd() {\n\tif (_chats) {\n\t\treturn base::take(_chats)->close();\n\t}\n\treturn Result::Success();\n}\n\nResult HtmlWriter::writeDialogOpening(int index) {\n\tconst auto name = (_dialog.name.isEmpty()\n\t\t&& _dialog.lastName.isEmpty())\n\t\t? QByteArray(\"Deleted Account\")\n\t\t: (_dialog.name + ' ' + _dialog.lastName);\n\tauto block = _chat->pushHeader(\n\t\tname,\n\t\t_settings.onlySinglePeer() ? QString() : _dialogsRelativePath);\n\tblock.append(_chat->pushDiv(\"page_body chat_page\"));\n\tblock.append(_chat->pushDiv(\"history\"));\n\tif (index > 0) {\n\t\tconst auto previousPath = messagesFile(index - 1);\n\t\tblock.append(_chat->pushTag(\"a\", {\n\t\t\t{ \"class\", \"pagination block_link\" },\n\t\t\t{ \"href\", previousPath.toUtf8() }\n\t\t\t}));\n\t\tblock.append(\"Previous messages\");\n\t\tblock.append(_chat->popTag());\n\t}\n\treturn _chat->writeBlock(block);\n}\n\nvoid HtmlWriter::pushSection(\n\t\tint priority,\n\t\tconst QByteArray &label,\n\t\tconst QByteArray &type,\n\t\tint count,\n\t\tconst QString &path) {\n\t_savedSections.push_back({\n\t\tpriority,\n\t\tlabel,\n\t\ttype,\n\t\tcount,\n\t\tpath\n\t});\n}\n\nResult HtmlWriter::writeSections() {\n\tExpects(_summary != nullptr);\n\n\tif (_savedSections.empty()) {\n\t\treturn Result::Success();\n\t} else if (!_haveSections) {\n\t\tauto block = _summary->pushDiv(\n\t\t\t_summaryNeedDivider ? \"sections with_divider\" : \"sections\");\n\t\tif (const auto result = _summary->writeBlock(block); !result) {\n\t\t\treturn result;\n\t\t}\n\t\t_haveSections = true;\n\t\t_summaryNeedDivider = false;\n\t}\n\n\tauto block = QByteArray();\n\tranges::sort(_savedSections, std::less<>(), [](const SavedSection &data) {\n\t\treturn data.priority;\n\t});\n\tfor (const auto &section : base::take(_savedSections)) {\n\t\tblock.append(_summary->pushSection(\n\t\t\tsection.label,\n\t\t\tsection.type,\n\t\t\tsection.count,\n\t\t\t_summary->relativePath(section.path)));\n\t}\n\treturn _summary->writeBlock(block);\n}\n\nQByteArray HtmlWriter::wrapMessageLink(int messageId, QByteArray text) {\n\tconst auto it = ranges::find_if(_lastMessageIdsPerFile, [&](int maxMessageId) {\n\t\treturn messageId <= maxMessageId;\n\t});\n\tif (it == end(_lastMessageIdsPerFile)) {\n\t\treturn \"<a href=\\\"#go_to_message\"\n\t\t\t+ Data::NumberToString(messageId)\n\t\t\t+ \"\\\" onclick=\\\"return GoToMessage(\"\n\t\t\t+ Data::NumberToString(messageId)\n\t\t\t+ \")\\\">\"\n\t\t\t+ text + \"</a>\";\n\t} else {\n\t\tconst auto index = it - begin(_lastMessageIdsPerFile);\n\t\treturn \"<a href=\\\"\" + messagesFile(index).toUtf8()\n\t\t\t+ \"#go_to_message\"\n\t\t\t+ Data::NumberToString(messageId)\n\t\t\t+ \"\\\">\"\n\t\t\t+ text + \"</a>\";\n\n\t}\n}\n\nResult HtmlWriter::switchToNextChatFile(int index) {\n\tExpects(_chat != nullptr);\n\n\tconst auto nextPath = messagesFile(index);\n\tauto next = _chat->pushTag(\"a\", {\n\t\t{ \"class\", \"pagination block_link\" },\n\t\t{ \"href\", nextPath.toUtf8() }\n\t});\n\tnext.append(\"Next messages\");\n\tnext.append(_chat->popTag());\n\tif (const auto result = _chat->writeBlock(next); !result) {\n\t\treturn result;\n\t} else if (const auto end = _chat->close(); !end) {\n\t\treturn end;\n\t}\n\t_chat = fileWithRelativePath(_dialog.relativePath + nextPath);\n\t_chatFileEmpty = true;\n\treturn Result::Success();\n}\n\nResult HtmlWriter::finish() {\n\tExpects(_settings.onlySinglePeer() || _summary != nullptr);\n\n\tif (_settings.onlySinglePeer()) {\n\t\treturn Result::Success();\n\t}\n\n\tif (const auto result = writeSections(); !result) {\n\t\treturn result;\n\t}\n\tauto block = QByteArray();\n\tif (_haveSections) {\n\t\tblock.append(_summary->popTag());\n\t\t_summaryNeedDivider = true;\n\t\t_haveSections = false;\n\t}\n\tblock.append(_summary->pushAbout(\n\t\t_environment.aboutTelegram,\n\t\t_summaryNeedDivider));\n\tif (const auto result = _summary->writeBlock(block); !result) {\n\t\treturn result;\n\t}\n\treturn _summary->close();\n}\n\nResult HtmlWriter::copyFile(\n\t\tconst QString &source,\n\t\tconst QString &relativePath) const {\n\treturn File::Copy(\n\t\tsource,\n\t\tpathWithRelativePath(relativePath),\n\t\t_stats);\n}\n\nQString HtmlWriter::mainFilePath() {\n\treturn pathWithRelativePath(_settings.onlySinglePeer()\n\t\t? messagesFile(0)\n\t\t: mainFileRelativePath());\n}\n\nQString HtmlWriter::mainFileRelativePath() const {\n\treturn \"export_results.html\";\n}\n\nQString HtmlWriter::pathWithRelativePath(const QString &path) const {\n\treturn _settings.path + path;\n}\n\nQString HtmlWriter::messagesFile(int index) const {\n\treturn \"messages\"\n\t\t+ (index > 0 ? QString::number(index + 1) : QString())\n\t\t+ \".html\";\n}\n\nstd::unique_ptr<HtmlWriter::Wrap> HtmlWriter::fileWithRelativePath(\n\t\tconst QString &path) const {\n\treturn std::make_unique<Wrap>(\n\t\tpathWithRelativePath(path),\n\t\t_settings.path,\n\t\t_stats);\n}\n\nHtmlWriter::~HtmlWriter() = default;\n\n} // namespace Output\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/output/export_output_html.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"export/output/export_output_abstract.h\"\n#include \"export/output/export_output_file.h\"\n#include \"export/export_settings.h\"\n#include \"export/data/export_data_types.h\"\n\nnamespace Export {\nnamespace Output {\nnamespace details {\n\nclass HtmlContext {\npublic:\n\t[[nodiscard]] QByteArray pushTag(\n\t\tconst QByteArray &tag,\n\t\tstd::map<QByteArray, QByteArray> &&attributes = {});\n\t[[nodiscard]] QByteArray popTag();\n\t[[nodiscard]] QByteArray indent() const;\n\t[[nodiscard]] bool empty() const;\n\nprivate:\n\tstruct Tag {\n\t\tQByteArray name;\n\t\tbool block = true;\n\t};\n\tstd::vector<Tag> _tags;\n\n};\n\nstruct UserpicData;\nstruct StoryData;\nclass PeersMap;\nstruct MediaData;\n\n} // namespace details\n\nclass HtmlWriter : public AbstractWriter {\npublic:\n\tHtmlWriter();\n\n\tFormat format() override {\n\t\treturn Format::Html;\n\t}\n\n\tResult start(\n\t\tconst Settings &settings,\n\t\tconst Environment &environment,\n\t\tStats *stats) override;\n\n\tResult writePersonal(const Data::PersonalInfo &data) override;\n\n\tResult writeUserpicsStart(const Data::UserpicsInfo &data) override;\n\tResult writeUserpicsSlice(const Data::UserpicsSlice &data) override;\n\tResult writeUserpicsEnd() override;\n\n\tResult writeStoriesStart(const Data::StoriesInfo &data) override;\n\tResult writeStoriesSlice(const Data::StoriesSlice &data) override;\n\tResult writeStoriesEnd() override;\n\n\tResult writeProfileMusicStart(const Data::ProfileMusicInfo &data) override;\n\tResult writeProfileMusicSlice(const Data::ProfileMusicSlice &data) override;\n\tResult writeProfileMusicEnd() override;\n\n\tResult writeContactsList(const Data::ContactsList &data) override;\n\n\tResult writeSessionsList(const Data::SessionsList &data) override;\n\n\tResult writeOtherData(const Data::File &data) override;\n\n\tResult writeDialogsStart(const Data::DialogsInfo &data) override;\n\tResult writeDialogStart(const Data::DialogInfo &data) override;\n\tResult writeDialogSlice(const Data::MessagesSlice &data) override;\n\tResult writeDialogEnd() override;\n\tResult writeDialogsEnd() override;\n\n\tResult finish() override;\n\n\tQString mainFilePath() override;\n\n\t~HtmlWriter();\n\nprivate:\n\tusing Context = details::HtmlContext;\n\tusing UserpicData = details::UserpicData;\n\tusing MediaData = details::MediaData;\n\tclass Wrap;\n\tstruct MessageInfo;\n\tenum class DialogsMode {\n\t\tNone,\n\t\tChats,\n\t\tLeft,\n\t};\n\n\t[[nodiscard]] Result copyFile(\n\t\tconst QString &source,\n\t\tconst QString &relativePath) const;\n\n\t[[nodiscard]] QString mainFileRelativePath() const;\n\t[[nodiscard]] QString pathWithRelativePath(const QString &path) const;\n\t[[nodiscard]] std::unique_ptr<Wrap> fileWithRelativePath(\n\t\tconst QString &path) const;\n\t[[nodiscard]] QString messagesFile(int index) const;\n\n\t[[nodiscard]] Result writeSavedContacts(const Data::ContactsList &data);\n\t[[nodiscard]] Result writeFrequentContacts(const Data::ContactsList &data);\n\n\t[[nodiscard]] Result writeSessions(const Data::SessionsList &data);\n\t[[nodiscard]] Result writeWebSessions(const Data::SessionsList &data);\n\n\t[[nodiscard]] Result validateDialogsMode(bool isLeftChannel);\n\t[[nodiscard]] Result writeDialogOpening(int index);\n\t[[nodiscard]] Result switchToNextChatFile(int index);\n\t[[nodiscard]] Result writeEmptySinglePeer();\n\n\tvoid pushSection(\n\t\tint priority,\n\t\tconst QByteArray &label,\n\t\tconst QByteArray &type,\n\t\tint count,\n\t\tconst QString &path);\n\t[[nodiscard]] Result writeSections();\n\n\t[[nodiscard]] Result writeDefaultPersonal(\n\t\tconst Data::PersonalInfo &data);\n\t[[nodiscard]] Result writeDelayedPersonal(const QString &userpicPath);\n\t[[nodiscard]] Result writePreparedPersonal(\n\t\tconst Data::PersonalInfo &data,\n\t\tconst QString &userpicPath);\n\tvoid pushUserpicsSection();\n\tvoid pushStoriesSection();\n\tvoid pushProfileMusicSection();\n\n\t[[nodiscard]] QString userpicsFilePath() const;\n\t[[nodiscard]] QString storiesFilePath() const;\n\t[[nodiscard]] QString profileMusicFilePath() const;\n\n\t[[nodiscard]] QByteArray wrapMessageLink(\n\t\tint messageId,\n\t\tQByteArray text);\n\n\tSettings _settings;\n\tEnvironment _environment;\n\tStats *_stats = nullptr;\n\n\tstruct SavedSection;\n\tstd::vector<SavedSection> _savedSections;\n\n\tstd::unique_ptr<Wrap> _summary;\n\tbool _summaryNeedDivider = false;\n\tbool _haveSections = false;\n\n\tuint8 _selfColorIndex = 0;\n\tstd::unique_ptr<Data::PersonalInfo> _delayedPersonalInfo;\n\n\tint _userpicsCount = 0;\n\tstd::unique_ptr<Wrap> _userpics;\n\n\tint _storiesCount = 0;\n\tstd::unique_ptr<Wrap> _stories;\n\n\tint _profileMusicCount = 0;\n\tstd::unique_ptr<Wrap> _profileMusic;\n\n\tQString _dialogsRelativePath;\n\tData::DialogInfo _dialog;\n\tDialogsMode _dialogsMode = DialogsMode::None;\n\n\tint _messagesCount = 0;\n\tstd::unique_ptr<MessageInfo> _lastMessageInfo;\n\tint _dateMessageId = 0;\n\tstd::unique_ptr<Wrap> _chats;\n\tstd::unique_ptr<Wrap> _chat;\n\tstd::vector<int> _lastMessageIdsPerFile;\n\tbool _chatFileEmpty = false;\n\n};\n\n} // namespace Output\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/output/export_output_html_and_json.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/output/export_output_html_and_json.h\"\n\n#include \"export/output/export_output_html.h\"\n#include \"export/output/export_output_json.h\"\n#include \"export/output/export_output_result.h\"\n\nnamespace Export::Output {\n\nHtmlAndJsonWriter::HtmlAndJsonWriter() {\n\t_writers.push_back(CreateWriter(Format::Html));\n\t_writers.push_back(CreateWriter(Format::Json));\n}\n\nFormat HtmlAndJsonWriter::format() {\n\treturn Format::HtmlAndJson;\n}\n\nResult HtmlAndJsonWriter::start(\n\t\tconst Settings &settings,\n\t\tconst Environment &environment,\n\t\tStats *stats) {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->start(settings, environment, stats);\n\t});\n}\n\nResult HtmlAndJsonWriter::writePersonal(const Data::PersonalInfo &data) {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writePersonal(data);\n\t});\n}\n\nResult HtmlAndJsonWriter::writeUserpicsStart(const Data::UserpicsInfo &data) {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeUserpicsStart(data);\n\t});\n}\n\nResult HtmlAndJsonWriter::writeUserpicsSlice(const Data::UserpicsSlice &d) {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeUserpicsSlice(d);\n\t});\n}\n\nResult HtmlAndJsonWriter::writeUserpicsEnd() {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeUserpicsEnd();\n\t});\n}\n\nResult HtmlAndJsonWriter::writeStoriesStart(const Data::StoriesInfo &data) {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeStoriesStart(data);\n\t});\n}\n\nResult HtmlAndJsonWriter::writeStoriesSlice(const Data::StoriesSlice &data) {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeStoriesSlice(data);\n\t});\n}\n\nResult HtmlAndJsonWriter::writeStoriesEnd() {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeStoriesEnd();\n\t});\n}\n\nResult HtmlAndJsonWriter::writeProfileMusicStart(const Data::ProfileMusicInfo &data) {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeProfileMusicStart(data);\n\t});\n}\n\nResult HtmlAndJsonWriter::writeProfileMusicSlice(const Data::ProfileMusicSlice &data) {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeProfileMusicSlice(data);\n\t});\n}\n\nResult HtmlAndJsonWriter::writeProfileMusicEnd() {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeProfileMusicEnd();\n\t});\n}\n\nResult HtmlAndJsonWriter::writeContactsList(const Data::ContactsList &data) {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeContactsList(data);\n\t});\n}\n\nResult HtmlAndJsonWriter::writeSessionsList(const Data::SessionsList &data) {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeSessionsList(data);\n\t});\n}\n\nResult HtmlAndJsonWriter::writeOtherData(const Data::File &data) {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeOtherData(data);\n\t});\n}\n\nResult HtmlAndJsonWriter::writeDialogsStart(const Data::DialogsInfo &data) {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeDialogsStart(data);\n\t});\n}\n\nResult HtmlAndJsonWriter::writeDialogStart(const Data::DialogInfo &data) {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeDialogStart(data);\n\t});\n}\n\nResult HtmlAndJsonWriter::writeDialogSlice(const Data::MessagesSlice &data) {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeDialogSlice(data);\n\t});\n}\n\nResult HtmlAndJsonWriter::writeDialogEnd() {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeDialogEnd();\n\t});\n}\n\nResult HtmlAndJsonWriter::writeDialogsEnd() {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->writeDialogsEnd();\n\t});\n}\n\nResult HtmlAndJsonWriter::finish() {\n\treturn invoke([&](WriterPtr w) {\n\t\treturn w->finish();\n\t});\n}\n\nQString HtmlAndJsonWriter::mainFilePath() {\n\treturn _writers.front()->mainFilePath();\n}\n\nHtmlAndJsonWriter::~HtmlAndJsonWriter() = default;\n\nResult HtmlAndJsonWriter::invoke(Fn<Result(WriterPtr)> method) const {\n\tauto result = Result(Result::Type::Success, QString());\n\tfor (const auto &writer : _writers) {\n\t\tconst auto current = method(writer);\n\t\tif (!current) {\n\t\t\tresult = current;\n\t\t}\n\t}\n\treturn result;\n}\n\n} // namespace Export::Output\n\n"
  },
  {
    "path": "Telegram/SourceFiles/export/output/export_output_html_and_json.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"export/output/export_output_abstract.h\"\n\nnamespace Export::Output {\n\nclass HtmlWriter;\nclass JsonWriter;\nstruct Result;\n\nclass HtmlAndJsonWriter final : public AbstractWriter {\npublic:\n\tHtmlAndJsonWriter();\n\n\tFormat format() override;\n\n\tResult start(\n\t\tconst Settings &settings,\n\t\tconst Environment &environment,\n\t\tStats *stats) override;\n\n\tResult writePersonal(const Data::PersonalInfo &data) override;\n\n\tResult writeUserpicsStart(const Data::UserpicsInfo &data) override;\n\tResult writeUserpicsSlice(const Data::UserpicsSlice &data) override;\n\tResult writeUserpicsEnd() override;\n\n\tResult writeStoriesStart(const Data::StoriesInfo &data) override;\n\tResult writeStoriesSlice(const Data::StoriesSlice &data) override;\n\tResult writeStoriesEnd() override;\n\n\tResult writeProfileMusicStart(const Data::ProfileMusicInfo &data) override;\n\tResult writeProfileMusicSlice(const Data::ProfileMusicSlice &data) override;\n\tResult writeProfileMusicEnd() override;\n\n\tResult writeContactsList(const Data::ContactsList &data) override;\n\n\tResult writeSessionsList(const Data::SessionsList &data) override;\n\n\tResult writeOtherData(const Data::File &data) override;\n\n\tResult writeDialogsStart(const Data::DialogsInfo &data) override;\n\tResult writeDialogStart(const Data::DialogInfo &data) override;\n\tResult writeDialogSlice(const Data::MessagesSlice &data) override;\n\tResult writeDialogEnd() override;\n\tResult writeDialogsEnd() override;\n\n\tResult finish() override;\n\n\tQString mainFilePath() override;\n\n\t~HtmlAndJsonWriter();\n\nprivate:\n\tusing WriterPtr = const std::unique_ptr<AbstractWriter> &;\n\tResult invoke(Fn<Result(WriterPtr)> method) const;\n\n\tstd::vector<std::unique_ptr<AbstractWriter>> _writers;\n\n};\n\n} // namespace Export::Output\n"
  },
  {
    "path": "Telegram/SourceFiles/export/output/export_output_json.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/output/export_output_json.h\"\n\n#include \"export/output/export_output_result.h\"\n#include \"export/data/export_data_types.h\"\n#include \"core/utils.h\"\n\n#include <QtCore/QDateTime>\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonObject>\n#include <QtCore/QJsonArray>\n#include <QtCore/QJsonValue>\n\nnamespace Export {\nnamespace Output {\nnamespace {\n\nusing Context = details::JsonContext;\n\nQByteArray SerializeString(const QByteArray &value) {\n\tconst auto size = value.size();\n\tconst auto begin = value.data();\n\tconst auto end = begin + size;\n\n\tauto result = QByteArray();\n\tresult.reserve(2 + size * 4);\n\tresult.append('\"');\n\tfor (auto p = begin; p != end; ++p) {\n\t\tconst auto ch = *p;\n\t\tif (ch == '\\n') {\n\t\t\tresult.append(\"\\\\n\", 2);\n\t\t} else if (ch == '\\r') {\n\t\t\tresult.append(\"\\\\r\", 2);\n\t\t} else if (ch == '\\t') {\n\t\t\tresult.append(\"\\\\t\", 2);\n\t\t} else if (ch == '\"') {\n\t\t\tresult.append(\"\\\\\\\"\", 2);\n\t\t} else if (ch == '\\\\') {\n\t\t\tresult.append(\"\\\\\\\\\", 2);\n\t\t} else if (ch >= 0 && ch < 32) {\n\t\t\tresult.append(\"\\\\x\", 2).append('0' + (ch >> 4));\n\t\t\tconst auto left = (ch & 0x0F);\n\t\t\tif (left >= 10) {\n\t\t\t\tresult.append('A' + (left - 10));\n\t\t\t} else {\n\t\t\t\tresult.append('0' + left);\n\t\t\t}\n\t\t} else if (ch == char(0xE2)\n\t\t\t&& (p + 2 < end)\n\t\t\t&& *(p + 1) == char(0x80)) {\n\t\t\tif (*(p + 2) == char(0xA8)) { // Line separator.\n\t\t\t\tresult.append(\"\\\\u2028\", 6);\n\t\t\t} else if (*(p + 2) == char(0xA9)) { // Paragraph separator.\n\t\t\t\tresult.append(\"\\\\u2029\", 6);\n\t\t\t} else {\n\t\t\t\tresult.append(ch);\n\t\t\t}\n\t\t} else {\n\t\t\tresult.append(ch);\n\t\t}\n\t}\n\tresult.append('\"');\n\treturn result;\n}\n\nQByteArray SerializeDate(TimeId date) {\n\treturn SerializeString(\n\t\tQDateTime::fromSecsSinceEpoch(date).toString(Qt::ISODate).toUtf8());\n}\n\nQByteArray SerializeDateRaw(TimeId date) {\n\treturn SerializeString(QString::number(date).toUtf8());\n}\n\nQByteArray StringAllowEmpty(const Data::Utf8String &data) {\n\treturn data.isEmpty() ? data : SerializeString(data);\n}\n\nQByteArray StringAllowNull(const Data::Utf8String &data) {\n\treturn data.isEmpty() ? QByteArray(\"null\") : SerializeString(data);\n}\n\nQByteArray Indentation(int size) {\n\treturn QByteArray(size, ' ');\n}\n\nQByteArray Indentation(const Context &context) {\n\treturn Indentation(context.nesting.size());\n}\n\nQByteArray SerializeObject(\n\t\tContext &context,\n\t\tconst std::vector<std::pair<QByteArray, QByteArray>> &values) {\n\tconst auto indent = Indentation(context);\n\n\tcontext.nesting.push_back(Context::kObject);\n\tconst auto guard = gsl::finally([&] { context.nesting.pop_back(); });\n\tconst auto next = '\\n' + Indentation(context);\n\n\tauto first = true;\n\tauto result = QByteArray();\n\tresult.append('{');\n\tfor (const auto &[key, value] : values) {\n\t\tif (value.isEmpty()) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (first) {\n\t\t\tfirst = false;\n\t\t} else {\n\t\t\tresult.append(',');\n\t\t}\n\t\tresult.append(next).append(SerializeString(key)).append(\": \", 2);\n\t\tresult.append(value);\n\t}\n\tresult.append('\\n').append(indent).append(\"}\");\n\treturn result;\n}\n\nQByteArray SerializeArray(\n\t\tContext &context,\n\t\tconst std::vector<QByteArray> &values) {\n\tconst auto indent = Indentation(context.nesting.size());\n\tconst auto next = '\\n' + Indentation(context.nesting.size() + 1);\n\n\tauto first = true;\n\tauto result = QByteArray();\n\tresult.append('[');\n\tfor (const auto &value : values) {\n\t\tif (first) {\n\t\t\tfirst = false;\n\t\t} else {\n\t\t\tresult.append(',');\n\t\t}\n\t\tresult.append(next).append(value);\n\t}\n\tresult.append('\\n').append(indent).append(\"]\");\n\treturn result;\n}\n\nQByteArray SerializeText(\n\t\tContext &context,\n\t\tconst std::vector<Data::TextPart> &data,\n\t\tbool serializeToObjects = false) {\n\tusing Type = Data::TextPart::Type;\n\n\tif (data.empty()) {\n\t\treturn serializeToObjects ? QByteArray(\"[]\") : SerializeString(\"\");\n\t}\n\n\tcontext.nesting.push_back(Context::kArray);\n\n\tconst auto text = ranges::views::all(\n\t\tdata\n\t) | ranges::views::transform([&](const Data::TextPart &part) {\n\t\tif ((part.type == Type::Text) && !serializeToObjects) {\n\t\t\treturn SerializeString(part.text);\n\t\t}\n\t\tconst auto typeString = [&] {\n\t\t\tswitch (part.type) {\n\t\t\tcase Type::Unknown: return \"unknown\";\n\t\t\tcase Type::Mention: return \"mention\";\n\t\t\tcase Type::Hashtag: return \"hashtag\";\n\t\t\tcase Type::BotCommand: return \"bot_command\";\n\t\t\tcase Type::Url: return \"link\";\n\t\t\tcase Type::Email: return \"email\";\n\t\t\tcase Type::Bold: return \"bold\";\n\t\t\tcase Type::Italic: return \"italic\";\n\t\t\tcase Type::Code: return \"code\";\n\t\t\tcase Type::Pre: return \"pre\";\n\t\t\tcase Type::Text: return \"plain\";\n\t\t\tcase Type::TextUrl: return \"text_link\";\n\t\t\tcase Type::MentionName: return \"mention_name\";\n\t\t\tcase Type::Phone: return \"phone\";\n\t\t\tcase Type::Cashtag: return \"cashtag\";\n\t\t\tcase Type::Underline: return \"underline\";\n\t\t\tcase Type::Strike: return \"strikethrough\";\n\t\t\tcase Type::Blockquote: return \"blockquote\";\n\t\t\tcase Type::BankCard: return \"bank_card\";\n\t\t\tcase Type::Spoiler: return \"spoiler\";\n\t\t\tcase Type::CustomEmoji: return \"custom_emoji\";\n\t\t\t}\n\t\t\tUnexpected(\"Type in SerializeText.\");\n\t\t}();\n\t\tconst auto additionalName = (part.type == Type::MentionName)\n\t\t\t? \"user_id\"\n\t\t\t: (part.type == Type::CustomEmoji)\n\t\t\t? \"document_id\"\n\t\t\t: (part.type == Type::Pre)\n\t\t\t? \"language\"\n\t\t\t: (part.type == Type::TextUrl)\n\t\t\t? \"href\"\n\t\t\t: (part.type == Type::Blockquote)\n\t\t\t? \"collapsed\"\n\t\t\t: \"none\";\n\t\tconst auto additionalValue = (part.type == Type::MentionName)\n\t\t\t? part.additional\n\t\t\t: (part.type == Type::Pre\n\t\t\t\t|| part.type == Type::TextUrl\n\t\t\t\t|| part.type == Type::CustomEmoji)\n\t\t\t? SerializeString(part.additional)\n\t\t\t: (part.type == Type::Blockquote)\n\t\t\t? (part.additional.isEmpty() ? \"false\" : \"true\")\n\t\t\t: QByteArray();\n\t\treturn SerializeObject(context, {\n\t\t\t{ \"type\", SerializeString(typeString) },\n\t\t\t{ \"text\", SerializeString(part.text) },\n\t\t\t{ additionalName, additionalValue },\n\t\t});\n\t}) | ranges::to_vector;\n\n\tcontext.nesting.pop_back();\n\n\tif (!serializeToObjects) {\n\t\tif (data.size() == 1 && data[0].type == Data::TextPart::Type::Text) {\n\t\t\treturn text[0];\n\t\t}\n\t}\n\treturn SerializeArray(context, text);\n}\n\nData::Utf8String FormatUsername(const Data::Utf8String &username) {\n\treturn username.isEmpty() ? username : ('@' + username);\n}\n\nQByteArray FormatFilePath(const Data::File &file) {\n\treturn file.relativePath.toUtf8();\n}\n\nQByteArray SerializeMessage(\n\t\tContext &context,\n\t\tconst Data::Message &message,\n\t\tconst std::map<PeerId, Data::Peer> &peers,\n\t\tconst QString &internalLinksDomain) {\n\tusing namespace Data;\n\n\tif (v::is<UnsupportedMedia>(message.media.content)) {\n\t\treturn SerializeObject(context, {\n\t\t\t{ \"id\", Data::NumberToString(message.id) },\n\t\t\t{ \"type\", SerializeString(\"unsupported\") }\n\t\t});\n\t}\n\n\tconst auto peer = [&](PeerId peerId) -> const Peer& {\n\t\tif (const auto i = peers.find(peerId); i != end(peers)) {\n\t\t\treturn i->second;\n\t\t}\n\t\tstatic auto empty = Peer{ User() };\n\t\treturn empty;\n\t};\n\tconst auto user = [&](UserId userId) -> const User& {\n\t\tif (const auto result = peer(userId).user()) {\n\t\t\treturn *result;\n\t\t}\n\t\tstatic auto empty = User();\n\t\treturn empty;\n\t};\n\n\tauto values = std::vector<std::pair<QByteArray, QByteArray>>{\n\t{ \"id\", NumberToString(message.id) },\n\t{\n\t\t\"type\",\n\t\tSerializeString(!v::is_null(message.action.content)\n\t\t\t? \"service\"\n\t\t\t: \"message\")\n\t},\n\t{ \"date\", SerializeDate(message.date) },\n\t{ \"date_unixtime\", SerializeDateRaw(message.date) },\n\t};\n\tcontext.nesting.push_back(Context::kObject);\n\tconst auto serialized = [&] {\n\t\tcontext.nesting.pop_back();\n\t\treturn SerializeObject(context, values);\n\t};\n\n\tconst auto pushBare = [&](\n\t\t\tconst QByteArray &key,\n\t\t\tconst QByteArray &value) {\n\t\tif (!value.isEmpty()) {\n\t\t\tvalues.emplace_back(key, value);\n\t\t}\n\t};\n\tif (message.edited) {\n\t\tpushBare(\"edited\", SerializeDate(message.edited));\n\t\tpushBare(\"edited_unixtime\", SerializeDateRaw(message.edited));\n\t}\n\n\tconst auto wrapPeerId = [&](PeerId peerId) {\n\t\tif (const auto chat = peerToChat(peerId)) {\n\t\t\treturn SerializeString(\"chat\"\n\t\t\t\t+ Data::NumberToString(chat.bare));\n\t\t} else if (const auto channel = peerToChannel(peerId)) {\n\t\t\treturn SerializeString(\"channel\"\n\t\t\t\t+ Data::NumberToString(channel.bare));\n\t\t}\n\t\treturn SerializeString(\"user\"\n\t\t\t+ Data::NumberToString(peerToUser(peerId).bare));\n\t};\n\tconst auto push = [&](const QByteArray &key, const auto &value) {\n\t\tusing V = std::decay_t<decltype(value)>;\n\t\tif constexpr (std::is_same_v<V, bool>) {\n\t\t\tpushBare(key, value ? \"true\" : \"false\");\n\t\t} else if constexpr (std::is_arithmetic_v<V>) {\n\t\t\tpushBare(key, Data::NumberToString(value));\n\t\t} else if constexpr (std::is_same_v<V, PeerId>) {\n\t\t\tpushBare(key, wrapPeerId(value));\n\t\t} else {\n\t\t\tconst auto wrapped = QByteArray(value);\n\t\t\tif (!wrapped.isEmpty()) {\n\t\t\t\tpushBare(key, SerializeString(wrapped));\n\t\t\t}\n\t\t}\n\t};\n\tconst auto wrapPeerName = [&](PeerId peerId) {\n\t\treturn StringAllowNull(peer(peerId).name());\n\t};\n\tconst auto wrapUserName = [&](UserId userId) {\n\t\treturn StringAllowNull(user(userId).name());\n\t};\n\tconst auto pushFrom = [&](const QByteArray &label = \"from\") {\n\t\tif (message.fromId) {\n\t\t\tpushBare(label, wrapPeerName(message.fromId));\n\t\t\tpush(label + \"_id\", message.fromId);\n\t\t}\n\t};\n\tconst auto pushReplyToMsgId = [&](\n\t\t\tconst QByteArray &label = \"reply_to_message_id\") {\n\t\tif (message.replyToMsgId) {\n\t\t\tpush(label, message.replyToMsgId);\n\t\t\tif (message.replyToPeerId) {\n\t\t\t\tpush(\"reply_to_peer_id\", message.replyToPeerId);\n\t\t\t}\n\t\t}\n\t};\n\tconst auto pushUserNames = [&](\n\t\t\tconst std::vector<UserId> &data,\n\t\t\tconst QByteArray &label = \"members\") {\n\t\tauto list = std::vector<QByteArray>();\n\t\tfor (const auto &userId : data) {\n\t\t\tlist.push_back(wrapUserName(userId));\n\t\t}\n\t\tpushBare(label, SerializeArray(context, list));\n\t};\n\tconst auto pushActor = [&] {\n\t\tpushFrom(\"actor\");\n\t};\n\tconst auto pushAction = [&](const QByteArray &action) {\n\t\tpush(\"action\", action);\n\t};\n\tconst auto pushTTL = [&](\n\t\t\tconst QByteArray &label = \"self_destruct_period_seconds\") {\n\t\tif (const auto ttl = message.media.ttl) {\n\t\t\tpush(label, ttl);\n\t\t}\n\t};\n\n\tusing SkipReason = Data::File::SkipReason;\n\tconst auto pushPath = [&](\n\t\t\tconst Data::File &file,\n\t\t\tconst QByteArray &label,\n\t\t\tconst QByteArray &name = QByteArray()) {\n\t\tExpects(!file.relativePath.isEmpty()\n\t\t\t|| file.skipReason != SkipReason::None);\n\n\t\tpush(label, [&]() -> QByteArray {\n\t\t\tconst auto pre = name.isEmpty() ? QByteArray() : name + ' ';\n\t\t\tswitch (file.skipReason) {\n\t\t\tcase SkipReason::Unavailable:\n\t\t\t\treturn pre + \"(File unavailable, please try again later)\";\n\t\t\tcase SkipReason::FileSize:\n\t\t\t\treturn pre + \"(File exceeds maximum size. \"\n\t\t\t\t\t\"Change data exporting settings to download.)\";\n\t\t\tcase SkipReason::FileType:\n\t\t\t\treturn pre + \"(File not included. \"\n\t\t\t\t\t\"Change data exporting settings to download.)\";\n\t\t\tcase SkipReason::None: return FormatFilePath(file);\n\t\t\t}\n\t\t\tUnexpected(\"Skip reason while writing file path.\");\n\t\t}());\n\t};\n\tconst auto pushPhoto = [&](const Image &image) {\n\t\tpushPath(image.file, \"photo\");\n\t\tpush(\"photo_file_size\", image.file.size);\n\t\tif (image.width && image.height) {\n\t\t\tpush(\"width\", image.width);\n\t\t\tpush(\"height\", image.height);\n\t\t}\n\t};\n\tconst auto pushSpoiler = [&](const auto &media) {\n\t\tif (media.spoilered) {\n\t\t\tpush(\"media_spoiler\", true);\n\t\t}\n\t};\n\n\tv::match(message.action.content, [&](const ActionChatCreate &data) {\n\t\tpushActor();\n\t\tpushAction(\"create_group\");\n\t\tpush(\"title\", data.title);\n\t\tpushUserNames(data.userIds);\n\t}, [&](const ActionChatEditTitle &data) {\n\t\tpushActor();\n\t\tpushAction(\"edit_group_title\");\n\t\tpush(\"title\", data.title);\n\t}, [&](const ActionChatEditPhoto &data) {\n\t\tpushActor();\n\t\tpushAction(\"edit_group_photo\");\n\t\tpushPhoto(data.photo.image);\n\t\tpushSpoiler(data.photo);\n\t}, [&](const ActionChatDeletePhoto &data) {\n\t\tpushActor();\n\t\tpushAction(\"delete_group_photo\");\n\t}, [&](const ActionChatAddUser &data) {\n\t\tpushActor();\n\t\tpushAction(\"invite_members\");\n\t\tpushUserNames(data.userIds);\n\t}, [&](const ActionChatDeleteUser &data) {\n\t\tpushActor();\n\t\tpushAction(\"remove_members\");\n\t\tpushUserNames({ data.userId });\n\t}, [&](const ActionChatJoinedByLink &data) {\n\t\tpushActor();\n\t\tpushAction(\"join_group_by_link\");\n\t\tpushBare(\"inviter\", wrapUserName(data.inviterId));\n\t}, [&](const ActionChannelCreate &data) {\n\t\tpushActor();\n\t\tpushAction(\"create_channel\");\n\t\tpush(\"title\", data.title);\n\t}, [&](const ActionChatMigrateTo &data) {\n\t\tpushActor();\n\t\tpushAction(\"migrate_to_supergroup\");\n\t}, [&](const ActionChannelMigrateFrom &data) {\n\t\tpushActor();\n\t\tpushAction(\"migrate_from_group\");\n\t\tpush(\"title\", data.title);\n\t}, [&](const ActionPinMessage &data) {\n\t\tpushActor();\n\t\tpushAction(\"pin_message\");\n\t\tpushReplyToMsgId(\"message_id\");\n\t}, [&](const ActionHistoryClear &data) {\n\t\tpushActor();\n\t\tpushAction(\"clear_history\");\n\t}, [&](const ActionGameScore &data) {\n\t\tpushActor();\n\t\tpushAction(\"score_in_game\");\n\t\tpushReplyToMsgId(\"game_message_id\");\n\t\tpush(\"score\", data.score);\n\t}, [&](const ActionPaymentSent &data) {\n\t\tpushAction(\"send_payment\");\n\t\tpush(\"amount\", data.amount);\n\t\tpush(\"currency\", data.currency);\n\t\tpushReplyToMsgId(\"invoice_message_id\");\n\t\tif (data.recurringUsed) {\n\t\t\tpush(\"recurring\", \"used\");\n\t\t} else if (data.recurringInit) {\n\t\t\tpush(\"recurring\", \"init\");\n\t\t}\n\t}, [&](const ActionPhoneCall &data) {\n\t\tpushActor();\n\t\tpushAction(data.conferenceId ? \"conference_call\" : \"phone_call\");\n\t\tif (data.duration) {\n\t\t\tpush(\"duration_seconds\", data.duration);\n\t\t}\n\t\tusing State = ActionPhoneCall::State;\n\t\tif (data.conferenceId) {\n\t\t\tpush(\"is_active\", data.state == State::Active);\n\t\t\tpush(\"is_missed\", data.state == State::Missed);\n\t\t} else {\n\t\t\tpush(\"discard_reason\", [&] {\n\t\t\t\tswitch (data.state) {\n\t\t\t\tcase State::Busy: return \"busy\";\n\t\t\t\tcase State::Disconnect: return \"disconnect\";\n\t\t\t\tcase State::Hangup: return \"hangup\";\n\t\t\t\tcase State::Missed: return \"missed\";\n\t\t\t\tcase State::MigrateConferenceCall:\n\t\t\t\t\treturn \"migrate_conference_all\";\n\t\t\t\t}\n\t\t\t\treturn \"\";\n\t\t\t}());\n\t\t}\n\t}, [&](const ActionScreenshotTaken &data) {\n\t\tpushActor();\n\t\tpushAction(\"take_screenshot\");\n\t}, [&](const ActionCustomAction &data) {\n\t\tpushActor();\n\t\tpush(\"information_text\", data.message);\n\t}, [&](const ActionBotAllowed &data) {\n\t\tif (data.attachMenu) {\n\t\t\tpushAction(\"attach_menu_bot_allowed\");\n\t\t} else if (data.fromRequest) {\n\t\t\tpushAction(\"web_app_bot_allowed\");\n\t\t} else if (data.appId) {\n\t\t\tpushAction(\"allow_sending_messages\");\n\t\t\tpush(\"reason_app_id\", data.appId);\n\t\t\tpush(\"reason_app_name\", data.app);\n\t\t} else {\n\t\t\tpushAction(\"allow_sending_messages\");\n\t\t\tpush(\"reason_domain\", data.domain);\n\t\t}\n\t}, [&](const ActionSecureValuesSent &data) {\n\t\tpushAction(\"send_passport_values\");\n\t\tauto list = std::vector<QByteArray>();\n\t\tfor (const auto type : data.types) {\n\t\t\tlist.push_back(SerializeString([&] {\n\t\t\t\tusing Type = ActionSecureValuesSent::Type;\n\t\t\t\tswitch (type) {\n\t\t\t\tcase Type::PersonalDetails: return \"personal_details\";\n\t\t\t\tcase Type::Passport: return \"passport\";\n\t\t\t\tcase Type::DriverLicense: return \"driver_license\";\n\t\t\t\tcase Type::IdentityCard: return \"identity_card\";\n\t\t\t\tcase Type::InternalPassport: return \"internal_passport\";\n\t\t\t\tcase Type::Address: return \"address_information\";\n\t\t\t\tcase Type::UtilityBill: return \"utility_bill\";\n\t\t\t\tcase Type::BankStatement: return \"bank_statement\";\n\t\t\t\tcase Type::RentalAgreement: return \"rental_agreement\";\n\t\t\t\tcase Type::PassportRegistration:\n\t\t\t\t\treturn \"passport_registration\";\n\t\t\t\tcase Type::TemporaryRegistration:\n\t\t\t\t\treturn \"temporary_registration\";\n\t\t\t\tcase Type::Phone: return \"phone_number\";\n\t\t\t\tcase Type::Email: return \"email\";\n\t\t\t\t}\n\t\t\t\treturn \"\";\n\t\t\t}()));\n\t\t}\n\t\tpushBare(\"values\", SerializeArray(context, list));\n\t}, [&](const ActionContactSignUp &data) {\n\t\tpushActor();\n\t\tpushAction(\"joined_telegram\");\n\t}, [&](const ActionGeoProximityReached &data) {\n\t\tpushAction(\"proximity_reached\");\n\t\tif (data.fromId) {\n\t\t\tpushBare(\"from\", wrapPeerName(data.fromId));\n\t\t\tpush(\"from_id\", data.fromId);\n\t\t}\n\t\tif (data.toId) {\n\t\t\tpushBare(\"to\", wrapPeerName(data.toId));\n\t\t\tpush(\"to_id\", data.toId);\n\t\t}\n\t\tpush(\"distance\", data.distance);\n\t}, [&](const ActionPhoneNumberRequest &data) {\n\t\tpushActor();\n\t\tpushAction(\"requested_phone_number\");\n\t}, [&](const ActionGroupCall &data) {\n\t\tpushActor();\n\t\tpushAction(\"group_call\");\n\t\tif (data.duration) {\n\t\t\tpush(\"duration\", data.duration);\n\t\t}\n\t}, [&](const ActionInviteToGroupCall &data) {\n\t\tpushActor();\n\t\tpushAction(\"invite_to_group_call\");\n\t\tpushUserNames(data.userIds);\n\t}, [&](const ActionSetMessagesTTL &data) {\n\t\tpushActor();\n\t\tpushAction(\"set_messages_ttl\");\n\t\tpush(\"period\", data.period);\n\t}, [&](const ActionGroupCallScheduled &data) {\n\t\tpushActor();\n\t\tpushAction(\"group_call_scheduled\");\n\t\tpush(\"schedule_date\", data.date);\n\t}, [&](const ActionSetChatTheme &data) {\n\t\tpushActor();\n\t\tpushAction(\"edit_chat_theme\");\n\t\tif (!data.emoji.isEmpty()) {\n\t\t\tpush(\"emoticon\", data.emoji.toUtf8());\n\t\t}\n\t}, [&](const ActionChatJoinedByRequest &data) {\n\t\tpushActor();\n\t\tpushAction(\"join_group_by_request\");\n\t}, [&](const ActionWebViewDataSent &data) {\n\t\tpushAction(\"send_webview_data\");\n\t\tpush(\"text\", data.text);\n\t}, [&](const ActionGiftPremium &data) {\n\t\tpushActor();\n\t\tpushAction(\"send_premium_gift\");\n\t\tif (!data.cost.isEmpty()) {\n\t\t\tpush(\"cost\", data.cost);\n\t\t}\n\t\tif (data.days) {\n\t\t\tpush(\"days\", data.days);\n\t\t}\n\t}, [&](const ActionTopicCreate &data) {\n\t\tpushActor();\n\t\tpushAction(\"topic_created\");\n\t\tpush(\"title\", data.title);\n\t}, [&](const ActionTopicEdit &data) {\n\t\tpushActor();\n\t\tpushAction(\"topic_edit\");\n\t\tif (!data.title.isEmpty()) {\n\t\t\tpush(\"new_title\", data.title);\n\t\t}\n\t\tif (data.iconEmojiId) {\n\t\t\tpush(\"new_icon_emoji_id\", *data.iconEmojiId);\n\t\t}\n\t}, [&](const ActionSuggestProfilePhoto &data) {\n\t\tpushActor();\n\t\tpushAction(\"suggest_profile_photo\");\n\t\tpushPhoto(data.photo.image);\n\t\tpushSpoiler(data.photo);\n\t}, [&](const ActionRequestedPeer &data) {\n\t\tpushActor();\n\t\tpushAction(\"requested_peer\");\n\t\tpush(\"button_id\", data.buttonId);\n\t\tauto values = std::vector<QByteArray>();\n\t\tfor (const auto &one : data.peers) {\n\t\t\tvalues.push_back(Data::NumberToString(one.value));\n\t\t}\n\t\tpush(\"peers\", SerializeArray(context, values));\n\t}, [&](const ActionGiftCode &data) {\n\t\tpushAction(\"gift_code_prize\");\n\t\tpush(\"gift_code\", data.code);\n\t\tif (data.boostPeerId) {\n\t\t\tpush(\"boost_peer_id\", data.boostPeerId);\n\t\t}\n\t\tpush(\"days\", data.days);\n\t\tpush(\"unclaimed\", data.unclaimed);\n\t\tpush(\"via_giveaway\", data.viaGiveaway);\n\t}, [&](const ActionGiveawayLaunch &data) {\n\t\tpushAction(\"giveaway_launch\");\n\t}, [&](const ActionGiveawayResults &data) {\n\t\tpushAction(\"giveaway_results\");\n\t\tpush(\"winners\", data.winners);\n\t\tpush(\"unclaimed\", data.unclaimed);\n\t\tpush(\"stars\", data.credits);\n\t}, [&](const ActionSetChatWallPaper &data) {\n\t\tpushActor();\n\t\tpushAction(data.same\n\t\t\t? \"set_same_chat_wallpaper\"\n\t\t\t: \"set_chat_wallpaper\");\n\t\tpushReplyToMsgId(\"message_id\");\n\t}, [&](const ActionBoostApply &data) {\n\t\tpushActor();\n\t\tpushAction(\"boost_apply\");\n\t\tpush(\"boosts\", data.boosts);\n\t}, [&](const ActionPaymentRefunded &data) {\n\t\tpushAction(\"refunded_payment\");\n\t\tpush(\"amount\", data.amount);\n\t\tpush(\"currency\", data.currency);\n\t\tpushBare(\"peer_name\", wrapPeerName(data.peerId));\n\t\tpush(\"peer_id\", data.peerId);\n\t\tpush(\"charge_id\", data.transactionId);\n\t}, [&](const ActionGiftCredits &data) {\n\t\tpushActor();\n\t\tpushAction(data.amount.ton()\n\t\t\t? \"send_ton_gift\"\n\t\t\t: \"send_stars_gift\");\n\t\tif (!data.cost.isEmpty()) {\n\t\t\tpush(\"cost\", data.cost);\n\t\t}\n\t\tif (data.amount) {\n\t\t\tpush(\"amount_whole\", data.amount.whole());\n\t\t\tpush(\"amount_nano\", data.amount.nano());\n\t\t}\n\t}, [&](const ActionPrizeStars &data) {\n\t\tpushActor();\n\t\tpushAction(\"stars_prize\");\n\t\tpush(\"boost_peer_id\", data.peerId);\n\t\tpushBare(\"boost_peer_name\", wrapPeerName(data.peerId));\n\t\tpush(\"stars\", data.amount);\n\t\tpush(\"is_unclaimed\", data.isUnclaimed);\n\t\tpush(\"giveaway_msg_id\", data.giveawayMsgId);\n\t\tpush(\"transaction_id\", data.transactionId);\n\t}, [&](const ActionStarGift &data) {\n\t\tpushActor();\n\t\tpushAction(\"send_star_gift\");\n\t\tpush(\"gift_id\", data.giftId);\n\t\tpush(\"stars\", data.stars);\n\t\tpush(\"is_limited\", data.limited);\n\t\tpush(\"is_anonymous\", data.anonymous);\n\t\tpushBare(\"gift_text\", SerializeText(context, data.text));\n\t}, [&](const ActionPaidMessagesRefunded &data) {\n\t\tpushActor();\n\t\tpushAction(\"paid_messages_refund\");\n\t\tpush(\"messages_count\", data.messages);\n\t\tpush(\"stars_count\", data.stars);\n\t}, [&](const ActionPaidMessagesPrice &data) {\n\t\tpushActor();\n\t\tpushAction(\"paid_messages_price_change\");\n\t\tpush(\"price_stars\", data.stars);\n\t\tpush(\"is_broadcast_messages_allowed\", data.broadcastAllowed);\n\t}, [&](const ActionTodoCompletions &data) {\n\t\tpushActor();\n\t\tpushAction(\"todo_completions\");\n\t\tauto completed = QByteArrayList();\n\t\tfor (const auto index : data.completed) {\n\t\t\tcompleted.push_back(QByteArray::number(index));\n\t\t}\n\t\tauto incompleted = QByteArrayList();\n\t\tfor (const auto index : data.incompleted) {\n\t\t\tincompleted.push_back(QByteArray::number(index));\n\t\t}\n\t\tpushBare(\"completed\", '[' + completed.join(',') + ']');\n\t\tpushBare(\"incompleted\", '[' + incompleted.join(',') + ']');\n\t}, [&](const ActionTodoAppendTasks &data) {\n\t\tpushActor();\n\t\tpushAction(\"todo_append_tasks\");\n\t\tconst auto items = ranges::views::all(\n\t\t\tdata.items\n\t\t) | ranges::views::transform([&](const TodoListItem &item) {\n\t\t\tcontext.nesting.push_back(Context::kArray);\n\t\t\tauto result = SerializeObject(context, {\n\t\t\t\t{ \"text\", SerializeText(context, item.text) },\n\t\t\t\t{ \"id\", NumberToString(item.id) },\n\t\t\t});\n\t\t\tcontext.nesting.pop_back();\n\t\t\treturn result;\n\t\t}) | ranges::to_vector;\n\t\tpushBare(\"items\", SerializeArray(context, items));\n\t}, [&](const ActionPollAppendAnswer &data) {\n\t\tpushActor();\n\t\tpushAction(\"poll_append_answer\");\n\t\tpush(\"option\", data.option);\n\t}, [&](const ActionPollDeleteAnswer &data) {\n\t\tpushActor();\n\t\tpushAction(\"poll_delete_answer\");\n\t\tpush(\"option\", data.option);\n\t}, [&](const ActionSuggestedPostApproval &data) {\n\t\tpushActor();\n\t\tpushAction(\"process_suggested_post\");\n\t\tif (data.rejected) {\n\t\t\tpushBare(\"rejected\", \"true\");\n\t\t\tif (!data.rejectComment.isEmpty()) {\n\t\t\t\tpush(\"comment\", data.rejectComment);\n\t\t\t}\n\t\t} else {\n\t\t\tpush(\"price_amount_whole\", NumberToString(data.price.whole()));\n\t\t\tpush(\"price_amount_nano\", NumberToString(data.price.nano()));\n\t\t\tpush(\"price_currency\", data.price.ton() ? \"TON\" : \"Stars\");\n\t\t\tpush(\"scheduled_date\", data.scheduleDate);\n\t\t}\n\t}, [&](const ActionSuggestedPostSuccess &data) {\n\t\tpushActor();\n\t\tpushAction(\"suggested_post_success\");\n\t\tpush(\"price_amount_whole\", NumberToString(data.price.whole()));\n\t\tpush(\"price_amount_nano\", NumberToString(data.price.nano()));\n\t\tpush(\"price_currency\", data.price.ton() ? \"TON\" : \"Stars\");\n\t}, [&](const ActionSuggestedPostRefund &data) {\n\t\tpushActor();\n\t\tpushAction(\"suggested_post_refund\");\n\t\tpush(\"user_initiated\", data.payerInitiated);\n\t}, [&](const ActionSuggestBirthday &data) {\n\t\tpushActor();\n\t\tpushAction(\"suggest_birthday\");\n\t\tpush(\"day\", data.birthday.day());\n\t\tpush(\"month\", data.birthday.month());\n\t\tif (const auto year = data.birthday.year()) {\n\t\t\tpush(\"year\", year);\n\t\t}\n\t}, [&](const ActionNoForwardsToggle &data) {\n\t\tpushActor();\n\t\tpushAction(\"no_forwards_toggle\");\n\t\tpush(\"new_value\", data.newValue);\n\t}, [&](const ActionNoForwardsRequest &data) {\n\t\tpushActor();\n\t\tpushAction(\"no_forwards_request\");\n\t\tpush(\"expired\", data.expired);\n\t\tpush(\"new_value\", data.newValue);\n\t}, [&](const ActionNewCreatorPending &data) {\n\t\tpushActor();\n\t\tpushAction(\"new_creator_pending\");\n\t\tpushBare(\"new_creator\", wrapUserName(data.newCreatorId));\n\t}, [&](const ActionChangeCreator &data) {\n\t\tpushActor();\n\t\tpushAction(\"change_creator\");\n\t\tpushBare(\"new_creator\", wrapUserName(data.newCreatorId));\n\t}, [&](const ActionManagedBotCreated &data) {\n\t\tpushActor();\n\t\tpushAction(\"managed_bot_created\");\n\t\tpushBare(\"bot\", wrapUserName(data.botId));\n\t}, [](v::null_t) {});\n\n\tif (v::is_null(message.action.content)) {\n\t\tpushFrom();\n\t\tpush(\"author\", message.signature);\n\t\tif (message.forwardedFromId) {\n\t\t\tpushBare(\n\t\t\t\t\"forwarded_from\",\n\t\t\t\twrapPeerName(message.forwardedFromId));\n\t\t\tpush(\"forwarded_from_id\", message.forwardedFromId);\n\t\t} else if (!message.forwardedFromName.isEmpty()) {\n\t\t\tpushBare(\n\t\t\t\t\"forwarded_from\",\n\t\t\t\tStringAllowNull(message.forwardedFromName));\n\t\t}\n\t\tif (message.savedFromChatId) {\n\t\t\tpushBare(\"saved_from\", wrapPeerName(message.savedFromChatId));\n\t\t}\n\t\tpushReplyToMsgId();\n\t\tif (message.viaBotId) {\n\t\t\tconst auto username = FormatUsername(\n\t\t\t\tuser(message.viaBotId).username);\n\t\t\tif (!username.isEmpty()) {\n\t\t\t\tpush(\"via_bot\", username);\n\t\t\t}\n\t\t}\n\t}\n\n\tv::match(message.media.content, [&](const Photo &photo) {\n\t\tpushPhoto(photo.image);\n\t\tpushSpoiler(photo);\n\t\tpushTTL();\n\t}, [&](const Document &data) {\n\t\tpushPath(data.file, \"file\");\n\t\tpush(\"file_name\", data.name);\n\t\tpush(\"file_size\", data.file.size);\n\t\tif (data.thumb.width > 0) {\n\t\t\tpushPath(data.thumb.file, \"thumbnail\");\n\t\t\tpush(\"thumbnail_file_size\", data.thumb.file.size);\n\t\t}\n\t\tconst auto pushType = [&](const QByteArray &value) {\n\t\t\tpush(\"media_type\", value);\n\t\t};\n\t\tif (data.isSticker) {\n\t\t\tpushType(\"sticker\");\n\t\t\tpush(\"sticker_emoji\", data.stickerEmoji);\n\t\t} else if (data.isVideoMessage) {\n\t\t\tpushType(\"video_message\");\n\t\t} else if (data.isVoiceMessage) {\n\t\t\tpushType(\"voice_message\");\n\t\t} else if (data.isAnimated) {\n\t\t\tpushType(\"animation\");\n\t\t} else if (data.isVideoFile) {\n\t\t\tpushType(\"video_file\");\n\t\t} else if (data.isAudioFile) {\n\t\t\tpushType(\"audio_file\");\n\t\t\tpush(\"performer\", data.songPerformer);\n\t\t\tpush(\"title\", data.songTitle);\n\t\t}\n\t\tpush(\"mime_type\", data.mime);\n\t\tif (data.duration) {\n\t\t\tpush(\"duration_seconds\", data.duration);\n\t\t}\n\t\tif (data.width && data.height) {\n\t\t\tpush(\"width\", data.width);\n\t\t\tpush(\"height\", data.height);\n\t\t}\n\t\tpushSpoiler(data);\n\t\tpushTTL();\n\t}, [&](const SharedContact &data) {\n\t\tpushBare(\"contact_information\", SerializeObject(context, {\n\t\t\t{ \"first_name\", SerializeString(data.info.firstName) },\n\t\t\t{ \"last_name\", SerializeString(data.info.lastName) },\n\t\t\t{\n\t\t\t\t\"phone_number\",\n\t\t\t\tSerializeString(FormatPhoneNumber(data.info.phoneNumber))\n\t\t\t}\n\t\t}));\n\t\tif (!data.vcard.content.isEmpty()) {\n\t\t\tpushPath(data.vcard, \"contact_vcard\");\n\t\t\tpush(\"contact_vcard_file_size\", data.vcard.size);\n\t\t}\n\t}, [&](const GeoPoint &data) {\n\t\tpushBare(\n\t\t\t\"location_information\",\n\t\t\tdata.valid ? SerializeObject(context, {\n\t\t\t{ \"latitude\", NumberToString(data.latitude) },\n\t\t\t{ \"longitude\", NumberToString(data.longitude) },\n\t\t\t}) : QByteArray(\"null\"));\n\t\tpushTTL(\"live_location_period_seconds\");\n\t}, [&](const Venue &data) {\n\t\tpush(\"place_name\", data.title);\n\t\tpush(\"address\", data.address);\n\t\tif (data.point.valid) {\n\t\t\tpushBare(\"location_information\", SerializeObject(context, {\n\t\t\t\t{ \"latitude\", NumberToString(data.point.latitude) },\n\t\t\t\t{ \"longitude\", NumberToString(data.point.longitude) },\n\t\t\t}));\n\t\t}\n\t}, [&](const Game &data) {\n\t\tpush(\"game_title\", data.title);\n\t\tpush(\"game_description\", data.description);\n\t\tif (data.botId != 0 && !data.shortName.isEmpty()) {\n\t\t\tconst auto bot = user(data.botId);\n\t\t\tif (bot.isBot && !bot.username.isEmpty()) {\n\t\t\t\tpush(\"game_link\", internalLinksDomain.toUtf8()\n\t\t\t\t\t+ bot.username\n\t\t\t\t\t+ \"?game=\"\n\t\t\t\t\t+ data.shortName);\n\t\t\t}\n\t\t}\n\t}, [&](const Invoice &data) {\n\t\tpush(\"invoice_information\", SerializeObject(context, {\n\t\t\t{ \"title\", SerializeString(data.title) },\n\t\t\t{ \"description\", SerializeString(data.description) },\n\t\t\t{ \"amount\", NumberToString(data.amount) },\n\t\t\t{ \"currency\", SerializeString(data.currency) },\n\t\t\t{ \"receipt_message_id\", (data.receiptMsgId\n\t\t\t\t? NumberToString(data.receiptMsgId)\n\t\t\t\t: QByteArray()) }\n\t\t}));\n\t}, [&](const Poll &data) {\n\t\tcontext.nesting.push_back(Context::kObject);\n\t\tconst auto answers = ranges::views::all(\n\t\t\tdata.answers\n\t\t) | ranges::views::transform([&](const Poll::Answer &answer) {\n\t\t\tcontext.nesting.push_back(Context::kArray);\n\t\t\tauto result = SerializeObject(context, {\n\t\t\t\t{ \"text\", SerializeText(context, answer.text) },\n\t\t\t\t{ \"voters\", NumberToString(answer.votes) },\n\t\t\t\t{ \"chosen\", answer.my ? \"true\" : \"false\" },\n\t\t\t});\n\t\t\tcontext.nesting.pop_back();\n\t\t\treturn result;\n\t\t}) | ranges::to_vector;\n\t\tconst auto serialized = SerializeArray(context, answers);\n\t\tcontext.nesting.pop_back();\n\n\t\tpushBare(\"poll\", SerializeObject(context, {\n\t\t\t{ \"question\", SerializeText(context, data.question) },\n\t\t\t{ \"closed\", data.closed ? \"true\" : \"false\" },\n\t\t\t{ \"total_voters\", NumberToString(data.totalVotes) },\n\t\t\t{ \"answers\", serialized }\n\t\t}));\n\t}, [&](const TodoList &data) {\n\t\tcontext.nesting.push_back(Context::kObject);\n\t\tconst auto items = ranges::views::all(\n\t\t\tdata.items\n\t\t) | ranges::views::transform([&](const TodoListItem &item) {\n\t\t\tcontext.nesting.push_back(Context::kArray);\n\t\t\tauto result = SerializeObject(context, {\n\t\t\t\t{ \"text\", SerializeText(context, item.text) },\n\t\t\t\t{ \"id\", NumberToString(item.id) },\n\t\t\t});\n\t\t\tcontext.nesting.pop_back();\n\t\t\treturn result;\n\t\t}) | ranges::to_vector;\n\t\tconst auto serialized = SerializeArray(context, items);\n\t\tcontext.nesting.pop_back();\n\n\t\tpushBare(\"todo_list\", SerializeObject(context, {\n\t\t\t{ \"title\", SerializeText(context, data.title) },\n\t\t\t{ \"others_can_append\", data.othersCanAppend ? \"true\" : \"false\" },\n\t\t\t{\n\t\t\t\t\"others_can_complete\",\n\t\t\t\tdata.othersCanComplete ? \"true\" : \"false\",\n\t\t\t},\n\t\t\t{ \"answers\", serialized }\n\t\t}));\n\t}, [&](const GiveawayStart &data) {\n\t\tcontext.nesting.push_back(Context::kArray);\n\t\tconst auto channels = ranges::views::all(\n\t\t\tdata.channels\n\t\t) | ranges::views::transform([&](ChannelId id) {\n\t\t\treturn NumberToString(id.bare);\n\t\t}) | ranges::to_vector;\n\t\tconst auto serialized = SerializeArray(context, channels);\n\t\tcontext.nesting.pop_back();\n\n\t\tcontext.nesting.push_back(Context::kArray);\n\t\tconst auto countries = ranges::views::all(\n\t\t\tdata.countries\n\t\t) | ranges::views::transform([&](const QString &code) {\n\t\t\treturn SerializeString(code.toUtf8());\n\t\t}) | ranges::to_vector;\n\t\tconst auto serializedCountries = SerializeArray(context, countries);\n\t\tcontext.nesting.pop_back();\n\n\t\tconst auto additionalPrize = data.additionalPrize.toUtf8();\n\n\t\tpushBare(\"giveaway_information\", SerializeObject(context, {\n\t\t\t{ \"quantity\", NumberToString(data.quantity) },\n\t\t\t{ \"months\", NumberToString(data.months) },\n\t\t\t{ \"until_date\", SerializeDate(data.untilDate) },\n\t\t\t{ \"channels\", serialized },\n\t\t\t{ \"countries\", serializedCountries },\n\t\t\t{ \"additional_prize\", SerializeString(additionalPrize) },\n\t\t\t{ \"stars\", NumberToString(data.credits) },\n\t\t\t{ \"is_only_new_subscribers\", (!data.all) ? \"true\" : \"false\" },\n\t\t}));\n\t}, [&](const GiveawayResults &data) {\n\t\tcontext.nesting.push_back(Context::kArray);\n\t\tconst auto winners = ranges::views::all(\n\t\t\tdata.winners\n\t\t) | ranges::views::transform([&](PeerId id) {\n\t\t\treturn NumberToString(id.value);\n\t\t}) | ranges::to_vector;\n\t\tconst auto serialized = SerializeArray(context, winners);\n\t\tcontext.nesting.pop_back();\n\n\t\tconst auto additionalPrize = data.additionalPrize.toUtf8();\n\t\tconst auto peersCount = data.additionalPeersCount;\n\n\t\tpushBare(\"giveaway_results\", SerializeObject(context, {\n\t\t\t{ \"channel\", NumberToString(data.channel.bare) },\n\t\t\t{ \"winners\", serialized },\n\t\t\t{ \"additional_prize\", SerializeString(additionalPrize) },\n\t\t\t{ \"until_date\", SerializeDate(data.untilDate) },\n\t\t\t{ \"launch_message_id\", NumberToString(data.launchId) },\n\t\t\t{ \"additional_peers_count\", NumberToString(peersCount) },\n\t\t\t{ \"winners_count\", NumberToString(data.winnersCount) },\n\t\t\t{ \"unclaimed_count\", NumberToString(data.unclaimedCount) },\n\t\t\t{ \"months\", NumberToString(data.months) },\n\t\t\t{ \"stars\", NumberToString(data.credits) },\n\t\t\t{ \"is_refunded\", data.refunded ? \"true\" : \"false\" },\n\t\t\t{ \"is_only_new_subscribers\", (!data.all) ? \"true\" : \"false\" },\n\t\t}));\n\t}, [&](const PaidMedia &data) {\n\t\tpush(\"paid_stars_amount\", data.stars);\n\t}, [](const UnsupportedMedia &data) {\n\t\tUnexpected(\"Unsupported message.\");\n\t}, [](v::null_t) {});\n\n\tpushBare(\"text\", SerializeText(context, message.text));\n\tpushBare(\"text_entities\", SerializeText(context, message.text, true));\n\n\tif (!message.inlineButtonRows.empty()) {\n\t\tconst auto serializeRow = [&](\n\t\t\t\tconst std::vector<HistoryMessageMarkupButton> &row) {\n\t\t\tcontext.nesting.push_back(Context::kArray);\n\t\t\tconst auto buttons = ranges::views::all(\n\t\t\t\trow\n\t\t\t) | ranges::views::transform([&](\n\t\t\t\t\tconst HistoryMessageMarkupButton &entry) {\n\t\t\t\tauto pairs = std::vector<std::pair<QByteArray, QByteArray>>();\n\t\t\t\tpairs.push_back({\n\t\t\t\t\t\"type\",\n\t\t\t\t\tSerializeString(\n\t\t\t\t\t\tHistoryMessageMarkupButton::TypeToString(entry)),\n\t\t\t\t});\n\t\t\t\tif (!entry.text.isEmpty()) {\n\t\t\t\t\tpairs.push_back({\n\t\t\t\t\t\t\"text\",\n\t\t\t\t\t\tSerializeString(entry.text.toUtf8()),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (!entry.data.isEmpty()) {\n\t\t\t\t\tusing Type = HistoryMessageMarkupButton::Type;\n\t\t\t\t\tconst auto isCallback = (entry.type == Type::Callback)\n\t\t\t\t\t\t|| (entry.type == Type::CallbackWithPassword);\n\t\t\t\t\tconst auto data = isCallback\n\t\t\t\t\t\t? entry.data.toBase64(QByteArray::Base64UrlEncoding\n\t\t\t\t\t\t\t| QByteArray::OmitTrailingEquals)\n\t\t\t\t\t\t: entry.data;\n\t\t\t\t\tif (isCallback) {\n\t\t\t\t\t\tpairs.push_back({\n\t\t\t\t\t\t\t\"dataBase64\",\n\t\t\t\t\t\t\tSerializeString(data),\n\t\t\t\t\t\t});\n\t\t\t\t\t\tpairs.push_back({ \"data\", SerializeString({}) });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tpairs.push_back({ \"data\", SerializeString(data) });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!entry.forwardText.isEmpty()) {\n\t\t\t\t\tpairs.push_back({\n\t\t\t\t\t\t\"forward_text\",\n\t\t\t\t\t\tSerializeString(entry.forwardText.toUtf8()),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (entry.buttonId) {\n\t\t\t\t\tpairs.push_back({\n\t\t\t\t\t\t\"button_id\",\n\t\t\t\t\t\tNumberToString(entry.buttonId),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn SerializeObject(context, pairs);\n\t\t\t}) | ranges::to_vector;\n\t\t\tcontext.nesting.pop_back();\n\t\t\treturn SerializeArray(context, buttons);\n\t\t};\n\t\tcontext.nesting.push_back(Context::kArray);\n\t\tconst auto rows = ranges::views::all(\n\t\t\tmessage.inlineButtonRows\n\t\t) | ranges::views::transform(serializeRow) | ranges::to_vector;\n\t\tcontext.nesting.pop_back();\n\t\tpushBare(\"inline_bot_buttons\", SerializeArray(context, rows));\n\t}\n\n\tif (!message.reactions.empty()) {\n\t\tconst auto serializeReaction = [&](const Reaction &reaction) {\n\t\t\tcontext.nesting.push_back(Context::kObject);\n\t\t\tconst auto guard = gsl::finally([&] {\n\t\t\t\tcontext.nesting.pop_back();\n\t\t\t});\n\n\t\t\tauto pairs = std::vector<std::pair<QByteArray, QByteArray>>();\n\t\t\tpairs.push_back({\n\t\t\t\t\"type\",\n\t\t\t\tSerializeString(Reaction::TypeToString(reaction)),\n\t\t\t});\n\t\t\tpairs.push_back({\n\t\t\t\t\"count\",\n\t\t\t\tNumberToString(reaction.count),\n\t\t\t});\n\t\t\tswitch (reaction.type) {\n\t\t\t\tcase Reaction::Type::Emoji:\n\t\t\t\t\tpairs.push_back({\n\t\t\t\t\t\t\"emoji\",\n\t\t\t\t\t\tSerializeString(reaction.emoji.toUtf8()),\n\t\t\t\t\t});\n\t\t\t\t\tbreak;\n\t\t\t\tcase Reaction::Type::CustomEmoji:\n\t\t\t\t\tpairs.push_back({\n\t\t\t\t\t\t\"document_id\",\n\t\t\t\t\t\tSerializeString(reaction.documentId),\n\t\t\t\t\t});\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (!reaction.recent.empty()) {\n\t\t\t\tcontext.nesting.push_back(Context::kArray);\n\t\t\t\tconst auto recents = ranges::views::all(\n\t\t\t\t\treaction.recent\n\t\t\t\t) | ranges::views::transform([&](\n\t\t\t\t\t\tconst Reaction::Recent &recent) {\n\t\t\t\t\tcontext.nesting.push_back(Context::kArray);\n\t\t\t\t\tconst auto guard = gsl::finally([&] {\n\t\t\t\t\t\tcontext.nesting.pop_back();\n\t\t\t\t\t});\n\t\t\t\t\treturn SerializeObject(context, {\n\t\t\t\t\t\t{ \"from\", wrapPeerName(recent.peerId) },\n\t\t\t\t\t\t{ \"from_id\", wrapPeerId(recent.peerId) },\n\t\t\t\t\t\t{ \"date\", SerializeDate(recent.date) },\n\t\t\t\t\t});\n\t\t\t\t}) | ranges::to_vector;\n\t\t\t\tpairs.push_back({\"recent\", SerializeArray(context, recents)});\n\t\t\t\tcontext.nesting.pop_back();\n\t\t\t}\n\n\t\t\treturn SerializeObject(context, pairs);\n\t\t};\n\n\t\tcontext.nesting.push_back(Context::kArray);\n\t\tconst auto reactions = ranges::views::all(\n\t\t\tmessage.reactions\n\t\t) | ranges::views::transform(serializeReaction) | ranges::to_vector;\n\t\tpushBare(\"reactions\", SerializeArray(context, reactions));\n\t\tcontext.nesting.pop_back();\n\t}\n\n\treturn serialized();\n}\n\n} // namespace\n\nResult JsonWriter::start(\n\t\tconst Settings &settings,\n\t\tconst Environment &environment,\n\t\tStats *stats) {\n\tExpects(_output == nullptr);\n\tExpects(settings.path.endsWith('/'));\n\n\t_settings = base::duplicate(settings);\n\t_environment = environment;\n\t_stats = stats;\n\t_output = fileWithRelativePath(mainFileRelativePath());\n\tif (_settings.onlySinglePeer()) {\n\t\treturn Result::Success();\n\t}\n\tauto block = pushNesting(Context::kObject);\n\tblock.append(prepareObjectItemStart(\"about\"));\n\tblock.append(SerializeString(_environment.aboutTelegram));\n\treturn _output->writeBlock(block);\n}\n\nQByteArray JsonWriter::pushNesting(Context::Type type) {\n\tExpects(_output != nullptr);\n\n\t_context.nesting.push_back(type);\n\t_currentNestingHadItem = false;\n\treturn (type == Context::kObject ? \"{\" : \"[\");\n}\n\nQByteArray JsonWriter::prepareObjectItemStart(const QByteArray &key) {\n\tconst auto guard = gsl::finally([&] { _currentNestingHadItem = true; });\n\treturn (_currentNestingHadItem ? \",\\n\" : \"\\n\")\n\t\t+ Indentation(_context)\n\t\t+ SerializeString(key)\n\t\t+ \": \";\n}\n\nQByteArray JsonWriter::prepareArrayItemStart() {\n\tconst auto guard = gsl::finally([&] { _currentNestingHadItem = true; });\n\treturn (_currentNestingHadItem ? \",\\n\" : \"\\n\") + Indentation(_context);\n}\n\nQByteArray JsonWriter::popNesting() {\n\tExpects(_output != nullptr);\n\tExpects(!_context.nesting.empty());\n\n\tconst auto type = Context::Type(_context.nesting.back());\n\t_context.nesting.pop_back();\n\n\t_currentNestingHadItem = true;\n\treturn '\\n'\n\t\t+ Indentation(_context)\n\t\t+ (type == Context::kObject ? '}' : ']');\n}\n\nResult JsonWriter::writePersonal(const Data::PersonalInfo &data) {\n\tExpects(_output != nullptr);\n\n\tconst auto &info = data.user.info;\n\treturn _output->writeBlock(\n\t\tprepareObjectItemStart(\"personal_information\")\n\t\t+ SerializeObject(_context, {\n\t\t{ \"user_id\", Data::NumberToString(data.user.bareId) },\n\t\t{ \"first_name\", SerializeString(info.firstName) },\n\t\t{ \"last_name\", SerializeString(info.lastName) },\n\t\t{\n\t\t\t\"phone_number\",\n\t\t\tSerializeString(Data::FormatPhoneNumber(info.phoneNumber))\n\t\t},\n\t\t{\n\t\t\t\"username\",\n\t\t\t(!data.user.username.isEmpty()\n\t\t\t\t? SerializeString(FormatUsername(data.user.username))\n\t\t\t\t: QByteArray())\n\t\t},\n\t\t{\n\t\t\t\"bio\",\n\t\t\t(!data.bio.isEmpty()\n\t\t\t\t? SerializeString(data.bio)\n\t\t\t\t: QByteArray())\n\t\t},\n\t}));\n}\n\nResult JsonWriter::writeUserpicsStart(const Data::UserpicsInfo &data) {\n\tExpects(_output != nullptr);\n\n\tauto block = prepareObjectItemStart(\"profile_pictures\");\n\treturn _output->writeBlock(block + pushNesting(Context::kArray));\n}\n\nResult JsonWriter::writeUserpicsSlice(const Data::UserpicsSlice &data) {\n\tExpects(_output != nullptr);\n\tExpects(!data.list.empty());\n\n\tauto block = QByteArray();\n\tfor (const auto &userpic : data.list) {\n\t\tusing SkipReason = Data::File::SkipReason;\n\t\tconst auto &file = userpic.image.file;\n\t\tAssert(!file.relativePath.isEmpty()\n\t\t\t|| file.skipReason != SkipReason::None);\n\t\tconst auto path = [&]() -> Data::Utf8String {\n\t\t\tswitch (file.skipReason) {\n\t\t\tcase SkipReason::Unavailable:\n\t\t\t\treturn \"(Photo unavailable, please try again later)\";\n\t\t\tcase SkipReason::FileSize:\n\t\t\t\treturn \"(Photo exceeds maximum size. \"\n\t\t\t\t\t\"Change data exporting settings to download.)\";\n\t\t\tcase SkipReason::FileType:\n\t\t\t\treturn \"(Photo not included. \"\n\t\t\t\t\t\"Change data exporting settings to download.)\";\n\t\t\tcase SkipReason::None: return FormatFilePath(file);\n\t\t\t}\n\t\t\tUnexpected(\"Skip reason while writing photo path.\");\n\t\t}();\n\t\tblock.append(prepareArrayItemStart());\n\t\tblock.append(SerializeObject(_context, {\n\t\t\t{\n\t\t\t\t\"date\",\n\t\t\t\tuserpic.date ? SerializeDate(userpic.date) : QByteArray()\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"date_unixtime\",\n\t\t\t\tuserpic.date ? SerializeDateRaw(userpic.date) : QByteArray()\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"photo\",\n\t\t\t\tSerializeString(path)\n\t\t\t},\n\t\t}));\n\t}\n\treturn _output->writeBlock(block);\n}\n\nResult JsonWriter::writeUserpicsEnd() {\n\tExpects(_output != nullptr);\n\n\treturn _output->writeBlock(popNesting());\n}\n\nResult JsonWriter::writeStoriesStart(const Data::StoriesInfo &data) {\n\tExpects(_output != nullptr);\n\n\tauto block = prepareObjectItemStart(\"stories\");\n\treturn _output->writeBlock(block + pushNesting(Context::kArray));\n}\n\nResult JsonWriter::writeStoriesSlice(const Data::StoriesSlice &data) {\n\tExpects(_output != nullptr);\n\n\tif (data.list.empty()) {\n\t\treturn Result::Success();\n\t}\n\n\tauto block = QByteArray();\n\tfor (const auto &story : data.list) {\n\t\tusing SkipReason = Data::File::SkipReason;\n\t\tconst auto &file = story.file();\n\t\tAssert(!file.relativePath.isEmpty()\n\t\t\t|| file.skipReason != SkipReason::None);\n\t\tconst auto path = [&]() -> Data::Utf8String {\n\t\t\tswitch (file.skipReason) {\n\t\t\tcase SkipReason::Unavailable:\n\t\t\t\treturn \"(Photo unavailable, please try again later)\";\n\t\t\tcase SkipReason::FileSize:\n\t\t\t\treturn \"(Photo exceeds maximum size. \"\n\t\t\t\t\t\"Change data exporting settings to download.)\";\n\t\t\tcase SkipReason::FileType:\n\t\t\t\treturn \"(Photo not included. \"\n\t\t\t\t\t\"Change data exporting settings to download.)\";\n\t\t\tcase SkipReason::None: return FormatFilePath(file);\n\t\t\t}\n\t\t\tUnexpected(\"Skip reason while writing story path.\");\n\t\t}();\n\t\tblock.append(prepareArrayItemStart());\n\t\tblock.append(SerializeObject(_context, {\n\t\t\t{\n\t\t\t\t\"date\",\n\t\t\t\tstory.date ? SerializeDate(story.date) : QByteArray()\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"date_unixtime\",\n\t\t\t\tstory.date ? SerializeDateRaw(story.date) : QByteArray()\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"expires\",\n\t\t\t\tstory.expires ? SerializeDate(story.expires) : QByteArray()\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"expires_unixtime\",\n\t\t\t\tstory.expires ? SerializeDateRaw(story.expires) : QByteArray()\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"pinned\",\n\t\t\t\tstory.pinned ? \"true\" : \"false\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"media\",\n\t\t\t\tSerializeString(path)\n\t\t\t},\n\t\t}));\n\t}\n\treturn _output->writeBlock(block);\n}\n\nResult JsonWriter::writeStoriesEnd() {\n\tExpects(_output != nullptr);\n\n\treturn _output->writeBlock(popNesting());\n}\n\nResult JsonWriter::writeProfileMusicStart(const Data::ProfileMusicInfo &data) {\n\tExpects(_output != nullptr);\n\n\tauto block = prepareObjectItemStart(\"profile_music\");\n\treturn _output->writeBlock(block + pushNesting(Context::kArray));\n}\n\nResult JsonWriter::writeProfileMusicSlice(const Data::ProfileMusicSlice &data) {\n\tExpects(_output != nullptr);\n\n\tif (data.list.empty()) {\n\t\treturn Result::Success();\n\t}\n\n\tauto block = QByteArray();\n\tfor (const auto &message : data.list) {\n\t\tblock.append(prepareArrayItemStart());\n\t\tblock.append(SerializeMessage(_context, message, {}, QString()));\n\t}\n\treturn _output->writeBlock(block);\n}\n\nResult JsonWriter::writeProfileMusicEnd() {\n\tExpects(_output != nullptr);\n\n\treturn _output->writeBlock(popNesting());\n}\n\nResult JsonWriter::writeContactsList(const Data::ContactsList &data) {\n\tExpects(_output != nullptr);\n\n\tif (const auto result = writeSavedContacts(data); !result) {\n\t\treturn result;\n\t} else if (const auto result = writeFrequentContacts(data); !result) {\n\t\treturn result;\n\t}\n\treturn Result::Success();\n}\n\nResult JsonWriter::writeSavedContacts(const Data::ContactsList &data) {\n\tExpects(_output != nullptr);\n\n\tauto block = prepareObjectItemStart(\"contacts\");\n\tblock.append(pushNesting(Context::kObject));\n\tblock.append(prepareObjectItemStart(\"about\"));\n\tblock.append(SerializeString(_environment.aboutContacts));\n\tblock.append(prepareObjectItemStart(\"list\"));\n\tblock.append(pushNesting(Context::kArray));\n\tfor (const auto index : Data::SortedContactsIndices(data)) {\n\t\tconst auto &contact = data.list[index];\n\t\tblock.append(prepareArrayItemStart());\n\n\t\tif (contact.firstName.isEmpty()\n\t\t\t&& contact.lastName.isEmpty()\n\t\t\t&& contact.phoneNumber.isEmpty()) {\n\t\t\tblock.append(SerializeObject(_context, {\n\t\t\t\t{ \"date\", SerializeDate(contact.date) },\n\t\t\t\t{ \"date_unixtime\", SerializeDateRaw(contact.date) },\n\t\t\t}));\n\t\t} else {\n\t\t\tblock.append(SerializeObject(_context, {\n\t\t\t\t{\n\t\t\t\t\t\"user_id\",\n\t\t\t\t\t(contact.userId\n\t\t\t\t\t\t? Data::NumberToString(contact.userId.bare)\n\t\t\t\t\t\t: QByteArray())\n\t\t\t\t},\n\t\t\t\t{ \"first_name\", SerializeString(contact.firstName) },\n\t\t\t\t{ \"last_name\", SerializeString(contact.lastName) },\n\t\t\t\t{\n\t\t\t\t\t\"phone_number\",\n\t\t\t\t\tSerializeString(\n\t\t\t\t\t\tData::FormatPhoneNumber(contact.phoneNumber))\n\t\t\t\t},\n\t\t\t\t{ \"date\", SerializeDate(contact.date) },\n\t\t\t\t{ \"date_unixtime\", SerializeDateRaw(contact.date) },\n\t\t\t}));\n\t\t}\n\t}\n\tblock.append(popNesting());\n\treturn _output->writeBlock(block + popNesting());\n}\n\nResult JsonWriter::writeFrequentContacts(const Data::ContactsList &data) {\n\tExpects(_output != nullptr);\n\n\tauto block = prepareObjectItemStart(\"frequent_contacts\");\n\tblock.append(pushNesting(Context::kObject));\n\tblock.append(prepareObjectItemStart(\"about\"));\n\tblock.append(SerializeString(_environment.aboutFrequent));\n\tblock.append(prepareObjectItemStart(\"list\"));\n\tblock.append(pushNesting(Context::kArray));\n\tconst auto writeList = [&](\n\t\t\tconst std::vector<Data::TopPeer> &peers,\n\t\t\tData::Utf8String category) {\n\t\tfor (const auto &top : peers) {\n\t\t\tconst auto type = [&] {\n\t\t\t\tif (const auto chat = top.peer.chat()) {\n\t\t\t\t\treturn chat->username.isEmpty()\n\t\t\t\t\t\t? (chat->isBroadcast\n\t\t\t\t\t\t\t? \"private_channel\"\n\t\t\t\t\t\t\t: (chat->isSupergroup\n\t\t\t\t\t\t\t\t? \"private_supergroup\"\n\t\t\t\t\t\t\t\t: \"private_group\"))\n\t\t\t\t\t\t: (chat->isBroadcast\n\t\t\t\t\t\t\t? \"public_channel\"\n\t\t\t\t\t\t\t: \"public_supergroup\");\n\t\t\t\t}\n\t\t\t\treturn \"user\";\n\t\t\t}();\n\t\t\tblock.append(prepareArrayItemStart());\n\t\t\tblock.append(SerializeObject(_context, {\n\t\t\t\t{ \"id\", Data::NumberToString(Data::PeerToBareId(top.peer.id())) },\n\t\t\t\t{ \"category\", SerializeString(category) },\n\t\t\t\t{ \"type\", SerializeString(type) },\n\t\t\t\t{ \"name\", StringAllowNull(top.peer.name()) },\n\t\t\t\t{ \"rating\", Data::NumberToString(top.rating) },\n\t\t\t}));\n\t\t}\n\t};\n\twriteList(data.correspondents, \"people\");\n\twriteList(data.inlineBots, \"inline_bots\");\n\twriteList(data.phoneCalls, \"calls\");\n\tblock.append(popNesting());\n\treturn _output->writeBlock(block + popNesting());\n}\n\nResult JsonWriter::writeSessionsList(const Data::SessionsList &data) {\n\tExpects(_output != nullptr);\n\n\tif (const auto result = writeSessions(data); !result) {\n\t\treturn result;\n\t} else if (const auto result = writeWebSessions(data); !result) {\n\t\treturn result;\n\t}\n\treturn Result::Success();\n}\n\nResult JsonWriter::writeOtherData(const Data::File &data) {\n\tExpects(_output != nullptr);\n\tExpects(data.skipReason == Data::File::SkipReason::None);\n\tExpects(!data.relativePath.isEmpty());\n\n\tQFile f(pathWithRelativePath(data.relativePath));\n\tif (!f.open(QIODevice::ReadOnly)) {\n\t\treturn Result(Result::Type::FatalError, f.fileName());\n\t}\n\tconst auto content = f.readAll();\n\tif (content.isEmpty()) {\n\t\treturn Result::Success();\n\t}\n\tauto error = QJsonParseError{ 0, QJsonParseError::NoError };\n\tconst auto document = QJsonDocument::fromJson(content, &error);\n\tif (error.error != QJsonParseError::NoError) {\n\t\treturn Result(Result::Type::FatalError, f.fileName());\n\t}\n\tauto block = prepareObjectItemStart(\"other_data\");\n\tFn<void(const QJsonObject &data)> pushObject;\n\tFn<void(const QJsonArray &data)> pushArray;\n\tFn<void(const QJsonValue &data)> pushValue;\n\tpushObject = [&](const QJsonObject &data) {\n\t\tblock.append(pushNesting(Context::kObject));\n\t\tfor (auto i = data.begin(); i != data.end(); ++i) {\n\t\t\tif ((*i).type() != QJsonValue::Undefined) {\n\t\t\t\tblock.append(prepareObjectItemStart(i.key().toUtf8()));\n\t\t\t\tpushValue(*i);\n\t\t\t}\n\t\t}\n\t\tblock.append(popNesting());\n\t};\n\tpushArray = [&](const QJsonArray &data) {\n\t\tblock.append(pushNesting(Context::kArray));\n\t\tfor (auto i = data.begin(); i != data.end(); ++i) {\n\t\t\tif ((*i).type() != QJsonValue::Undefined) {\n\t\t\t\tblock.append(prepareArrayItemStart());\n\t\t\t\tpushValue(*i);\n\t\t\t}\n\t\t}\n\t\tblock.append(popNesting());\n\t};\n\tpushValue = [&](const QJsonValue &data) {\n\t\tswitch (data.type()) {\n\t\tcase QJsonValue::Null:\n\t\t\tblock.append(\"null\");\n\t\t\treturn;\n\t\tcase QJsonValue::Bool:\n\t\t\tblock.append(data.toBool() ? \"true\" : \"false\");\n\t\t\treturn;\n\t\tcase QJsonValue::Double:\n\t\t\tblock.append(Data::NumberToString(data.toDouble()));\n\t\t\treturn;\n\t\tcase QJsonValue::String:\n\t\t\tblock.append(SerializeString(data.toString().toUtf8()));\n\t\t\treturn;\n\t\tcase QJsonValue::Array:\n\t\t\treturn pushArray(data.toArray());\n\t\tcase QJsonValue::Object:\n\t\t\treturn pushObject(data.toObject());\n\t\t}\n\t\tUnexpected(\"Type of json valuein JsonWriter::writeOtherData.\");\n\t};\n\tif (document.isObject()) {\n\t\tpushObject(document.object());\n\t} else {\n\t\tpushArray(document.array());\n\t}\n\treturn _output->writeBlock(block);\n}\n\nResult JsonWriter::writeSessions(const Data::SessionsList &data) {\n\tExpects(_output != nullptr);\n\n\tauto block = prepareObjectItemStart(\"sessions\");\n\tblock.append(pushNesting(Context::kObject));\n\tblock.append(prepareObjectItemStart(\"about\"));\n\tblock.append(SerializeString(_environment.aboutSessions));\n\tblock.append(prepareObjectItemStart(\"list\"));\n\tblock.append(pushNesting(Context::kArray));\n\tfor (const auto &session : data.list) {\n\t\tblock.append(prepareArrayItemStart());\n\t\tblock.append(SerializeObject(_context, {\n\t\t\t{ \"last_active\", SerializeDate(session.lastActive) },\n\t\t\t{ \"last_active_unixtime\", SerializeDateRaw(session.lastActive) },\n\t\t\t{ \"last_ip\", SerializeString(session.ip) },\n\t\t\t{ \"last_country\", SerializeString(session.country) },\n\t\t\t{ \"last_region\", SerializeString(session.region) },\n\t\t\t{\n\t\t\t\t\"application_name\",\n\t\t\t\tStringAllowNull(session.applicationName)\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"application_version\",\n\t\t\t\tStringAllowEmpty(session.applicationVersion)\n\t\t\t},\n\t\t\t{ \"device_model\", SerializeString(session.deviceModel) },\n\t\t\t{ \"platform\", SerializeString(session.platform) },\n\t\t\t{ \"system_version\", SerializeString(session.systemVersion) },\n\t\t\t{ \"created\", SerializeDate(session.created) },\n\t\t\t{ \"created_unixtime\", SerializeDateRaw(session.created) },\n\t\t}));\n\t}\n\tblock.append(popNesting());\n\treturn _output->writeBlock(block + popNesting());\n}\n\nResult JsonWriter::writeWebSessions(const Data::SessionsList &data) {\n\tExpects(_output != nullptr);\n\n\tauto block = prepareObjectItemStart(\"web_sessions\");\n\tblock.append(pushNesting(Context::kObject));\n\tblock.append(prepareObjectItemStart(\"about\"));\n\tblock.append(SerializeString(_environment.aboutWebSessions));\n\tblock.append(prepareObjectItemStart(\"list\"));\n\tblock.append(pushNesting(Context::kArray));\n\tfor (const auto &session : data.webList) {\n\t\tblock.append(prepareArrayItemStart());\n\t\tblock.append(SerializeObject(_context, {\n\t\t\t{ \"last_active\", SerializeDate(session.lastActive) },\n\t\t\t{ \"last_active_unixtime\", SerializeDateRaw(session.lastActive) },\n\t\t\t{ \"last_ip\", SerializeString(session.ip) },\n\t\t\t{ \"last_region\", SerializeString(session.region) },\n\t\t\t{ \"bot_username\", StringAllowNull(session.botUsername) },\n\t\t\t{ \"domain_name\", StringAllowNull(session.domain) },\n\t\t\t{ \"browser\", SerializeString(session.browser) },\n\t\t\t{ \"platform\", SerializeString(session.platform) },\n\t\t\t{ \"created\", SerializeDate(session.created) },\n\t\t\t{ \"created_unixtime\", SerializeDateRaw(session.created) },\n\t\t}));\n\t}\n\tblock.append(popNesting());\n\treturn _output->writeBlock(block + popNesting());\n}\n\nResult JsonWriter::writeDialogsStart(const Data::DialogsInfo &data) {\n\treturn Result::Success();\n}\n\nResult JsonWriter::writeDialogStart(const Data::DialogInfo &data) {\n\tExpects(_output != nullptr);\n\n\tif (!_settings.onlySinglePeer()) {\n\t\tconst auto result = validateDialogsMode(data.isLeftChannel);\n\t\tif (!result) {\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tusing Type = Data::DialogInfo::Type;\n\tconst auto TypeString = [](Type type) {\n\t\tswitch (type) {\n\t\tcase Type::Unknown: return \"\";\n\t\tcase Type::Self: return \"saved_messages\";\n\t\tcase Type::Replies: return \"replies\";\n\t\tcase Type::VerifyCodes: return \"verification_codes\";\n\t\tcase Type::Personal: return \"personal_chat\";\n\t\tcase Type::Bot: return \"bot_chat\";\n\t\tcase Type::PrivateGroup: return \"private_group\";\n\t\tcase Type::PrivateSupergroup: return \"private_supergroup\";\n\t\tcase Type::PublicSupergroup: return \"public_supergroup\";\n\t\tcase Type::PrivateChannel: return \"private_channel\";\n\t\tcase Type::PublicChannel: return \"public_channel\";\n\t\t}\n\t\tUnexpected(\"Dialog type in TypeString.\");\n\t};\n\n\tauto block = _settings.onlySinglePeer()\n\t\t? QByteArray()\n\t\t: prepareArrayItemStart();\n\tblock.append(pushNesting(Context::kObject));\n\tif (data.type != Type::Self\n\t\t&& data.type != Type::Replies\n\t\t&& data.type != Type::VerifyCodes) {\n\t\tblock.append(prepareObjectItemStart(\"name\")\n\t\t\t+ StringAllowNull(data.name));\n\t}\n\tblock.append(prepareObjectItemStart(\"type\")\n\t\t+ StringAllowNull(TypeString(data.type)));\n\tblock.append(prepareObjectItemStart(\"id\")\n\t\t+ Data::NumberToString(Data::PeerToBareId(data.peerId)));\n\tblock.append(prepareObjectItemStart(\"messages\"));\n\tblock.append(pushNesting(Context::kArray));\n\treturn _output->writeBlock(block);\n}\n\nResult JsonWriter::validateDialogsMode(bool isLeftChannel) {\n\tconst auto mode = isLeftChannel\n\t\t? DialogsMode::Left\n\t\t: DialogsMode::Chats;\n\tif (_dialogsMode == mode) {\n\t\treturn Result::Success();\n\t} else if (_dialogsMode != DialogsMode::None) {\n\t\tif (const auto result = writeChatsEnd(); !result) {\n\t\t\treturn result;\n\t\t}\n\t}\n\t_dialogsMode = mode;\n\treturn writeChatsStart(\n\t\tisLeftChannel ? \"left_chats\" : \"chats\",\n\t\t(isLeftChannel\n\t\t\t? _environment.aboutLeftChats\n\t\t\t: _environment.aboutChats));\n}\n\nResult JsonWriter::writeDialogSlice(const Data::MessagesSlice &data) {\n\tExpects(_output != nullptr);\n\n\tauto block = QByteArray();\n\tfor (const auto &message : data.list) {\n\t\tif (Data::SkipMessageByDate(message, _settings)) {\n\t\t\tcontinue;\n\t\t}\n\t\tblock.append(prepareArrayItemStart() + SerializeMessage(\n\t\t\t_context,\n\t\t\tmessage,\n\t\t\tdata.peers,\n\t\t\t_environment.internalLinksDomain));\n\t}\n\treturn block.isEmpty() ? Result::Success() : _output->writeBlock(block);\n}\n\nResult JsonWriter::writeDialogEnd() {\n\tExpects(_output != nullptr);\n\n\tauto block = popNesting();\n\treturn _output->writeBlock(block + popNesting());\n}\n\nResult JsonWriter::writeDialogsEnd() {\n\tif (_settings.onlySinglePeer()) {\n\t\treturn Result::Success();\n\t}\n\treturn writeChatsEnd();\n}\n\nResult JsonWriter::writeChatsStart(\n\t\tconst QByteArray &listName,\n\t\tconst QByteArray &about) {\n\tExpects(_output != nullptr);\n\n\tauto block = prepareObjectItemStart(listName);\n\tblock.append(pushNesting(Context::kObject));\n\tblock.append(prepareObjectItemStart(\"about\"));\n\tblock.append(SerializeString(about));\n\tblock.append(prepareObjectItemStart(\"list\"));\n\treturn _output->writeBlock(block + pushNesting(Context::kArray));\n}\n\nResult JsonWriter::writeChatsEnd() {\n\tExpects(_output != nullptr);\n\n\tauto block = popNesting();\n\treturn _output->writeBlock(block + popNesting());\n}\n\nResult JsonWriter::finish() {\n\tExpects(_output != nullptr);\n\n\tif (_settings.onlySinglePeer()) {\n\t\tAssert(_context.nesting.empty());\n\t\treturn Result::Success();\n\t}\n\tauto block = popNesting();\n\tAssert(_context.nesting.empty());\n\treturn _output->writeBlock(block);\n}\n\nQString JsonWriter::mainFilePath() {\n\treturn pathWithRelativePath(mainFileRelativePath());\n}\n\nQString JsonWriter::mainFileRelativePath() const {\n\treturn \"result.json\";\n}\n\nQString JsonWriter::pathWithRelativePath(const QString &path) const {\n\treturn _settings.path + path;\n}\n\nstd::unique_ptr<File> JsonWriter::fileWithRelativePath(\n\t\tconst QString &path) const {\n\treturn std::make_unique<File>(pathWithRelativePath(path), _stats);\n}\n\n} // namespace Output\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/output/export_output_json.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"export/output/export_output_abstract.h\"\n#include \"export/output/export_output_file.h\"\n#include \"export/export_settings.h\"\n#include \"export/data/export_data_types.h\"\n\nnamespace Export {\nnamespace Output {\nnamespace details {\n\nstruct JsonContext {\n\tusing Type = bool;\n\tstatic constexpr auto kObject = Type(true);\n\tstatic constexpr auto kArray = Type(false);\n\n\t// Always fun to use std::vector<bool>.\n\tstd::vector<Type> nesting;\n};\n\n} // namespace details\n\nclass JsonWriter : public AbstractWriter {\npublic:\n\tFormat format() override {\n\t\treturn Format::Json;\n\t}\n\n\tResult start(\n\t\tconst Settings &settings,\n\t\tconst Environment &environment,\n\t\tStats *stats) override;\n\n\tResult writePersonal(const Data::PersonalInfo &data) override;\n\n\tResult writeUserpicsStart(const Data::UserpicsInfo &data) override;\n\tResult writeUserpicsSlice(const Data::UserpicsSlice &data) override;\n\tResult writeUserpicsEnd() override;\n\n\tResult writeStoriesStart(const Data::StoriesInfo &data) override;\n\tResult writeStoriesSlice(const Data::StoriesSlice &data) override;\n\tResult writeStoriesEnd() override;\n\n\tResult writeProfileMusicStart(const Data::ProfileMusicInfo &data) override;\n\tResult writeProfileMusicSlice(const Data::ProfileMusicSlice &data) override;\n\tResult writeProfileMusicEnd() override;\n\n\tResult writeContactsList(const Data::ContactsList &data) override;\n\n\tResult writeSessionsList(const Data::SessionsList &data) override;\n\n\tResult writeOtherData(const Data::File &data) override;\n\n\tResult writeDialogsStart(const Data::DialogsInfo &data) override;\n\tResult writeDialogStart(const Data::DialogInfo &data) override;\n\tResult writeDialogSlice(const Data::MessagesSlice &data) override;\n\tResult writeDialogEnd() override;\n\tResult writeDialogsEnd() override;\n\n\tResult finish() override;\n\n\tQString mainFilePath() override;\n\nprivate:\n\tusing Context = details::JsonContext;\n\tenum class DialogsMode {\n\t\tNone,\n\t\tChats,\n\t\tLeft,\n\t};\n\n\t[[nodiscard]] QByteArray pushNesting(Context::Type type);\n\t[[nodiscard]] QByteArray prepareObjectItemStart(const QByteArray &key);\n\t[[nodiscard]] QByteArray prepareArrayItemStart();\n\t[[nodiscard]] QByteArray popNesting();\n\n\t[[nodiscard]] QString mainFileRelativePath() const;\n\t[[nodiscard]] QString pathWithRelativePath(const QString &path) const;\n\t[[nodiscard]] std::unique_ptr<File> fileWithRelativePath(\n\t\tconst QString &path) const;\n\n\t[[nodiscard]] Result writeSavedContacts(const Data::ContactsList &data);\n\t[[nodiscard]] Result writeFrequentContacts(const Data::ContactsList &data);\n\n\t[[nodiscard]] Result writeSessions(const Data::SessionsList &data);\n\t[[nodiscard]] Result writeWebSessions(const Data::SessionsList &data);\n\n\t[[nodiscard]] Result validateDialogsMode(bool isLeftChannel);\n\t[[nodiscard]] Result writeChatsStart(\n\t\tconst QByteArray &listName,\n\t\tconst QByteArray &about);\n\t[[nodiscard]] Result writeChatsEnd();\n\n\tSettings _settings;\n\tEnvironment _environment;\n\tStats *_stats = nullptr;\n\n\tContext _context;\n\tbool _currentNestingHadItem = false;\n\tDialogsMode _dialogsMode = DialogsMode::None;\n\n\tstd::unique_ptr<File> _output;\n\n};\n\n} // namespace Output\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/output/export_output_result.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QString>\n\nnamespace Export {\nnamespace Output {\n\nstruct Result {\n\tenum class Type : char {\n\t\tSuccess,\n\t\tError,\n\t\tFatalError\n\t};\n\n\tResult(Type type, QString path) : path(path), type(type) {\n\t}\n\n\tstatic Result Success() {\n\t\treturn Result(Type::Success, QString());\n\t}\n\n\tbool isSuccess() const {\n\t\treturn type == Type::Success;\n\t}\n\tbool isError() const {\n\t\treturn (type == Type::Error) || (type == Type::FatalError);\n\t}\n\tbool isFatalError() const {\n\t\treturn (type == Type::FatalError);\n\t}\n\texplicit operator bool() const {\n\t\treturn isSuccess();\n\t}\n\n\tQString path;\n\tType type;\n\n};\n\n} // namespace Output\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/output/export_output_stats.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/output/export_output_stats.h\"\n\nnamespace Export {\nnamespace Output {\n\nStats::Stats(const Stats &other)\n: _files(other._files.load())\n, _bytes(other._bytes.load()) {\n}\n\nvoid Stats::incrementFiles() {\n\t++_files;\n}\n\nvoid Stats::incrementBytes(int count) {\n\t_bytes += count;\n}\n\nint Stats::filesCount() const {\n\treturn _files;\n}\n\nint64 Stats::bytesCount() const {\n\treturn _bytes;\n}\n\n} // namespace Output\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/output/export_output_stats.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <atomic>\n\nnamespace Export {\nnamespace Output {\n\nclass Stats {\npublic:\n\tStats() = default;\n\tStats(const Stats &other);\n\n\tvoid incrementFiles();\n\tvoid incrementBytes(int count);\n\n\tint filesCount() const;\n\tint64 bytesCount() const;\n\nprivate:\n\tstd::atomic<int> _files;\n\tstd::atomic<int64> _bytes;\n\n};\n\n} // namespace Output\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/view/export.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\n\nusing \"ui/widgets/widgets.style\";\nusing \"boxes/boxes.style\";\n\nexportPanelSize: size(364px, 480px);\nexportSettingPadding: margins(22px, 8px, 22px, 8px);\nexportSubSettingPadding: margins(56px, 4px, 22px, 12px);\nexportHeaderLabel: FlatLabel(boxTitle) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(15px semibold);\n\t}\n}\nexportHeaderPadding: margins(22px, 20px, 22px, 9px);\nexportFileSizeSlider: MediaSlider(defaultContinuousSlider) {\n\tseekSize: size(15px, 15px);\n}\nexportFileSizeLabel: LabelSimple(defaultLabelSimple) {\n\tfont: boxTextFont;\n}\nexportFileSizePadding: margins(22px, 8px, 22px, 8px);\nexportFileSizeLabelBottom: 18px;\n\nexportLocationLabel: FlatLabel(boxLabel) {\n\tmaxHeight: 21px;\n}\nexportLocationPadding: margins(22px, 8px, 22px, 8px);\nexportLimitsPadding: margins(22px, 0px, 22px, 0px);\n\nexportAboutOptionLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n\tminWidth: 175px;\n}\nexportAboutOptionPadding: margins(22px, 0px, 22px, 16px);\n\nexportErrorLabel: FlatLabel(boxLabel) {\n\tminWidth: 175px;\n\talign: align(top);\n\ttextFg: boxTextFgError;\n}\n\nexportProgressDuration: 200;\nexportProgressRowHeight: 30px;\nexportProgressRowPadding: margins(22px, 10px, 22px, 10px);\nexportProgressRowSkip: 10px;\nexportProgressLabel: FlatLabel(boxLabel) {\n\ttextFg: windowBoldFg;\n\tmaxHeight: 20px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px semibold);\n\t}\n}\nexportProgressInfoLabel: FlatLabel(boxLabel) {\n\ttextFg: windowSubTextFg;\n\tmaxHeight: 20px;\n\tstyle: boxTextStyle;\n}\nexportProgressWidth: 3px;\nexportProgressFg: mediaPlayerActiveFg;\nexportProgressBg: mediaPlayerInactiveFg;\n\nexportCancelButton: RoundButton(attentionBoxButton) {\n\twidth: 200px;\n\theight: 44px;\n\ttextTop: 12px;\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(semibold 15px);\n\t}\n}\nexportCancelBottom: 30px;\nexportDoneButton: RoundButton(defaultActiveButton) {\n\twidth: 200px;\n\theight: 44px;\n\ttextTop: 12px;\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(semibold 15px);\n\t}\n}\n\nexportAboutLabel: FlatLabel(boxLabel) {\n\ttextFg: windowSubTextFg;\n}\nexportAboutPadding: margins(22px, 10px, 22px, 0px);\n\nexportTopBarLabel: FlatLabel(defaultFlatLabel) {\n\tmaxHeight: 20px;\n\tpalette: TextPalette(defaultTextPalette) {\n\t\tlinkFg: windowSubTextFg;\n\t}\n}\nexportCalendarSizes: CalendarSizes(defaultCalendarSizes) {\n\twidth: 320px;\n\tdaysHeight: 40px;\n\tcellSize: size(42px, 38px);\n\tcellInner: 32px;\n\tpadding: margins(14px, 0px, 14px, 0px);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/export/view/export_view_content.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/view/export_view_content.h\"\n\n#include \"export/export_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/text/format_values.h\"\n\nnamespace Export {\nnamespace View {\n\nconst QString Content::kDoneId = \"done\";\n\nContent ContentFromState(\n\t\tnot_null<Settings*> settings,\n\t\tconst ProcessingState &state) {\n\tusing Step = ProcessingState::Step;\n\n\tauto result = Content();\n\tconst auto push = [&](\n\t\t\tconst QString &id,\n\t\t\tconst QString &label,\n\t\t\tconst QString &info,\n\t\t\tfloat64 progress,\n\t\t\tuint64 randomId = 0) {\n\t\tresult.rows.push_back({ id, label, info, progress, randomId });\n\t};\n\tconst auto pushMain = [&](const QString &label) {\n\t\tconst auto info = (state.entityCount > 0)\n\t\t\t? (QString::number(state.entityIndex + 1)\n\t\t\t\t+ \" / \"\n\t\t\t\t+ QString::number(state.entityCount))\n\t\t\t: QString();\n\t\tif (!state.substepsTotal) {\n\t\t\tpush(\"main\", label, info, 0.);\n\t\t\treturn;\n\t\t}\n\t\tconst auto substepsTotal = state.substepsTotal;\n\t\tconst auto done = state.substepsPassed;\n\t\tconst auto add = state.substepsNow;\n\t\tconst auto doneProgress = done / float64(substepsTotal);\n\t\tconst auto addPart = [&](int index, int count) {\n\t\t\treturn (count > 0)\n\t\t\t\t? ((float64(add) * index)\n\t\t\t\t\t/ (float64(substepsTotal) * count))\n\t\t\t\t: 0.;\n\t\t};\n\t\tconst auto addProgress = (state.entityCount == 1\n\t\t\t&& !state.entityIndex)\n\t\t\t? addPart(state.itemIndex, state.itemCount)\n\t\t\t: addPart(state.entityIndex, state.entityCount);\n\t\tpush(\"main\", label, info, doneProgress + addProgress);\n\t};\n\tconst auto pushBytes = [&](\n\t\t\tconst QString &id,\n\t\t\tconst QString &label,\n\t\t\tuint64 randomId) {\n\t\tif (!state.bytesCount) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto progress = state.bytesLoaded / float64(state.bytesCount);\n\t\tconst auto info = Ui::FormatDownloadText(\n\t\t\tstate.bytesLoaded,\n\t\t\tstate.bytesCount);\n\t\tpush(id, label, info, progress, randomId);\n\t};\n\tswitch (state.step) {\n\tcase Step::Initializing:\n\t\tpushMain(tr::lng_export_state_initializing(tr::now));\n\t\tbreak;\n\tcase Step::DialogsList:\n\t\tpushMain(tr::lng_export_state_chats_list(tr::now));\n\t\tbreak;\n\tcase Step::PersonalInfo:\n\t\tpushMain(tr::lng_export_option_info(tr::now));\n\t\tbreak;\n\tcase Step::Userpics:\n\t\tpushMain(tr::lng_export_state_userpics(tr::now));\n\t\tpushBytes(\n\t\t\t\"userpic\" + QString::number(state.entityIndex),\n\t\t\tstate.bytesName,\n\t\t\tstate.bytesRandomId);\n\t\tbreak;\n\tcase Step::Contacts:\n\t\tpushMain(tr::lng_export_option_contacts(tr::now));\n\t\tbreak;\n\tcase Step::Stories:\n\t\tpushMain(tr::lng_export_option_stories(tr::now));\n\t\tpushBytes(\n\t\t\t\"story\" + QString::number(state.entityIndex),\n\t\t\tstate.bytesName,\n\t\t\tstate.bytesRandomId);\n\t\tbreak;\n\tcase Step::ProfileMusic:\n\t\tpushMain(tr::lng_export_option_profile_music(tr::now));\n\t\tpushBytes(\n\t\t\t\"music\" + QString::number(state.entityIndex),\n\t\t\tstate.bytesName,\n\t\t\tstate.bytesRandomId);\n\t\tbreak;\n\tcase Step::Sessions:\n\t\tpushMain(tr::lng_export_option_sessions(tr::now));\n\t\tbreak;\n\tcase Step::OtherData:\n\t\tpushMain(tr::lng_export_option_other(tr::now));\n\t\tbreak;\n\tcase Step::Dialogs:\n\t\tif (state.entityCount > 1) {\n\t\t\tpushMain(tr::lng_export_state_chats(tr::now));\n\t\t}\n\t\tpush(\n\t\t\t\"chat\" + QString::number(state.entityIndex),\n\t\t\t(state.entityName.isEmpty()\n\t\t\t\t? tr::lng_deleted(tr::now)\n\t\t\t\t: (state.entityType == ProcessingState::EntityType::Chat)\n\t\t\t\t? state.entityName\n\t\t\t\t: (state.entityType == ProcessingState::EntityType::SavedMessages)\n\t\t\t\t? tr::lng_saved_messages(tr::now)\n\t\t\t\t: tr::lng_replies_messages(tr::now)),\n\t\t\t(state.itemCount > 0\n\t\t\t\t? (QString::number(state.itemIndex)\n\t\t\t\t\t+ \" / \"\n\t\t\t\t\t+ QString::number(state.itemCount))\n\t\t\t\t: QString()),\n\t\t\t(state.itemCount > 0\n\t\t\t\t? (state.itemIndex / float64(state.itemCount))\n\t\t\t\t: 0.));\n\t\tpushBytes(\n\t\t\t(\"file\"\n\t\t\t\t+ QString::number(state.entityIndex)\n\t\t\t\t+ '_'\n\t\t\t\t+ QString::number(state.itemIndex)),\n\t\t\tstate.bytesName,\n\t\t\tstate.bytesRandomId);\n\t\tbreak;\n\tcase Step::Topic:\n\t\tpushMain(tr::lng_export_state_chats(tr::now));\n\t\tpush(\n\t\t\t\"topic\",\n\t\t\tstate.entityName.isEmpty()\n\t\t\t\t? tr::lng_deleted(tr::now)\n\t\t\t\t: state.entityName,\n\t\t\t(state.itemCount > 0\n\t\t\t\t? (QString::number(state.itemIndex)\n\t\t\t\t\t+ \" / \"\n\t\t\t\t\t+ QString::number(state.itemCount))\n\t\t\t\t: QString()),\n\t\t\t(state.itemCount > 0\n\t\t\t\t? (state.itemIndex / float64(state.itemCount))\n\t\t\t\t: 0.));\n\t\tpushBytes(\n\t\t\t\"file_topic_\" + QString::number(state.itemIndex),\n\t\t\tstate.bytesName,\n\t\t\tstate.bytesRandomId);\n\t\tbreak;\n\tdefault: Unexpected(\"Step in ContentFromState.\");\n\t}\n\tconst auto requiredRows = settings->onlySinglePeer() ? 2 : 3;\n\twhile (result.rows.size() < requiredRows) {\n\t\tresult.rows.emplace_back();\n\t}\n\treturn result;\n}\n\nContent ContentFromState(const FinishedState &state) {\n\tauto result = Content();\n\tresult.rows.push_back({\n\t\tContent::kDoneId,\n\t\ttr::lng_export_finished(tr::now),\n\t\tQString(),\n\t\t1. });\n\tresult.rows.push_back({\n\t\tContent::kDoneId,\n\t\ttr::lng_export_total_amount(\n\t\t\ttr::now,\n\t\t\tlt_amount,\n\t\t\tQString::number(state.filesCount)),\n\t\tQString(),\n\t\t1. });\n\tresult.rows.push_back({\n\t\tContent::kDoneId,\n\t\ttr::lng_export_total_size(\n\t\t\ttr::now,\n\t\t\tlt_size,\n\t\t\tUi::FormatSizeText(state.bytesCount)),\n\t\tQString(),\n\t\t1. });\n\treturn result;\n}\n\n} // namespace View\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/view/export_view_content.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"export/export_controller.h\"\n\nnamespace Export {\nstruct Settings;\n} // namespace Export\n\nnamespace Export {\nnamespace View {\n\nstruct Content {\n\tstruct Row {\n\t\tQString id;\n\t\tQString label;\n\t\tQString info;\n\t\tfloat64 progress = 0.;\n\t\tuint64 randomId = 0;\n\t};\n\n\tstd::vector<Row> rows;\n\n\tstatic const QString kDoneId;\n\n};\n\n[[nodiscard]] Content ContentFromState(\n\tnot_null<Settings*> settings,\n\tconst ProcessingState &state);\n[[nodiscard]] Content ContentFromState(const FinishedState &state);\n\n[[nodiscard]] inline auto ContentFromState(\n\t\tnot_null<Settings*> settings,\n\t\trpl::producer<State> state) {\n\treturn std::move(\n\t\tstate\n\t) | rpl::filter([](const State &state) {\n\t\treturn v::is<ProcessingState>(state) || v::is<FinishedState>(state);\n\t}) | rpl::map([=](const State &state) {\n\t\tif (const auto process = std::get_if<ProcessingState>(&state)) {\n\t\t\treturn ContentFromState(settings, *process);\n\t\t} else if (const auto done = std::get_if<FinishedState>(&state)) {\n\t\t\treturn ContentFromState(*done);\n\t\t}\n\t\tUnexpected(\"State type in ContentFromState.\");\n\t});\n}\n\n} // namespace View\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/view/export_view_panel_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/view/export_view_panel_controller.h\"\n\n#include \"export/view/export_view_settings.h\"\n#include \"export/view/export_view_progress.h\"\n#include \"export/export_manager.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/separate_panel.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"storage/storage_account.h\"\n#include \"core/application.h\"\n#include \"core/file_utilities.h\"\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/unixtime.h\"\n#include \"base/qt/qt_common_adapters.h\"\n#include \"boxes/abstract_box.h\" // Ui::show().\n#include \"styles/style_export.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Export {\nnamespace View {\nnamespace {\n\nconstexpr auto kSaveSettingsTimeout = crl::time(1000);\n\nclass SuggestBox : public Ui::BoxContent {\npublic:\n\tSuggestBox(QWidget*, not_null<Main::Session*> session);\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\n};\n\nSuggestBox::SuggestBox(QWidget*, not_null<Main::Session*> session)\n: _session(session) {\n}\n\nvoid SuggestBox::prepare() {\n\tsetTitle(tr::lng_export_suggest_title());\n\n\taddButton(tr::lng_box_ok(), [=] {\n\t\tconst auto session = _session;\n\t\tcloseBox();\n\t\tCore::App().exportManager().start(\n\t\t\tsession,\n\t\t\tsession->local().readExportSettings().singlePeer);\n\t});\n\taddButton(tr::lng_export_suggest_cancel(), [=] { closeBox(); });\n\tsetCloseByOutsideClick(false);\n\n\tconst auto content = Ui::CreateChild<Ui::FlatLabel>(\n\t\tthis,\n\t\ttr::lng_export_suggest_text(tr::now),\n\t\tst::boxLabel);\n\twidthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto contentWidth = width\n\t\t\t- st::boxPadding.left()\n\t\t\t- st::boxPadding.right();\n\t\tcontent->resizeToWidth(contentWidth);\n\t\tcontent->moveToLeft(st::boxPadding.left(), 0);\n\t}, content->lifetime());\n\tcontent->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tsetDimensions(st::boxWidth, height + st::boxPadding.bottom());\n\t}, content->lifetime());\n}\n\n} // namespace\n\nEnvironment PrepareEnvironment(not_null<Main::Session*> session) {\n\tauto result = Environment();\n\tresult.internalLinksDomain = session->serverConfig().internalLinksDomain;\n\tresult.aboutTelegram = tr::lng_export_about_telegram(tr::now).toUtf8();\n\tresult.aboutContacts = tr::lng_export_about_contacts(tr::now).toUtf8();\n\tresult.aboutFrequent = tr::lng_export_about_frequent(tr::now).toUtf8();\n\tresult.aboutSessions = tr::lng_export_about_sessions(tr::now).toUtf8();\n\tresult.aboutWebSessions = tr::lng_export_about_web_sessions(tr::now).toUtf8();\n\tresult.aboutChats = tr::lng_export_about_chats(tr::now).toUtf8();\n\tresult.aboutLeftChats = tr::lng_export_about_left_chats(tr::now).toUtf8();\n\treturn result;\n}\n\nbase::weak_qptr<Ui::BoxContent> SuggestStart(not_null<Main::Session*> session) {\n\tClearSuggestStart(session);\n\treturn Ui::show(\n\t\tBox<SuggestBox>(session),\n\t\tUi::LayerOption::KeepOther).get();\n}\n\nvoid ClearSuggestStart(not_null<Main::Session*> session) {\n\tsession->data().clearExportSuggestion();\n\n\tauto settings = session->local().readExportSettings();\n\tif (settings.availableAt) {\n\t\tsettings.availableAt = 0;\n\t\tsession->local().writeExportSettings(settings);\n\t}\n}\n\nbool IsDefaultPath(not_null<Main::Session*> session, const QString &path) {\n\tconst auto check = [](const QString &value) {\n\t\tconst auto result = value.endsWith('/')\n\t\t\t? value.mid(0, value.size() - 1)\n\t\t\t: value;\n\t\treturn Platform::IsWindows() ? result.toLower() : result;\n\t};\n\treturn (check(path) == check(File::DefaultDownloadPath(session)));\n}\n\nvoid ResolveSettings(not_null<Main::Session*> session, Settings &settings) {\n\tif (settings.path.isEmpty()) {\n\t\tsettings.path = File::DefaultDownloadPath(session);\n\t\tsettings.forceSubPath = true;\n\t} else {\n\t\tsettings.forceSubPath = IsDefaultPath(session, settings.path);\n\t}\n\tif (!settings.onlySinglePeer()) {\n\t\tsettings.singlePeerFrom = settings.singlePeerTill = 0;\n\t}\n}\n\nPanelController::PanelController(\n\tnot_null<Main::Session*> session,\n\tnot_null<Controller*> process)\n: _session(session)\n, _process(process)\n, _settings(\n\tstd::make_unique<Settings>(_session->local().readExportSettings()))\n, _saveSettingsTimer([=] { saveSettings(); }) {\n\tResolveSettings(session, *_settings);\n\n\t_process->state(\n\t) | rpl::on_next([=](State &&state) {\n\t\tupdateState(std::move(state));\n\t}, _lifetime);\n}\n\nPanelController::~PanelController() {\n\tif (_saveSettingsTimer.isActive()) {\n\t\tsaveSettings();\n\t}\n\tif (_panel) {\n\t\t_panel->hideLayer(anim::type::instant);\n\t}\n}\n\nvoid PanelController::activatePanel() {\n\tif (_panel) {\n\t\t_panel->showAndActivate();\n\t}\n}\n\nvoid PanelController::createPanel() {\n\tconst auto singlePeer = _settings->onlySinglePeer();\n\tconst auto singleTopic = _settings->onlySingleTopic();\n\t_panel = base::make_unique_q<Ui::SeparatePanel>(Ui::SeparatePanelArgs{\n\t\t.onAllSpaces = true,\n\t});\n\t_panel->setTitle((singleTopic\n\t\t? tr::lng_export_header_topic\n\t\t: singlePeer\n\t\t? tr::lng_export_header_chats\n\t\t: tr::lng_export_title)());\n\t_panel->setInnerSize(st::exportPanelSize);\n\t_panel->closeRequests(\n\t) | rpl::on_next([=] {\n\t\tLOG((\"Export Info: Panel Hide By Close.\"));\n\t\t_panel->hideGetDuration();\n\t}, _panel->lifetime());\n\t_panelCloseEvents.fire(_panel->closeEvents());\n\n\tshowSettings();\n}\n\nvoid PanelController::showSettings() {\n\tauto settings = base::make_unique_q<SettingsWidget>(\n\t\t_panel,\n\t\t_session,\n\t\t*_settings);\n\tsettings->setShowBoxCallback([=](object_ptr<Ui::BoxContent> box) {\n\t\t_panel->showBox(\n\t\t\tstd::move(box),\n\t\t\tUi::LayerOption::KeepOther,\n\t\t\tanim::type::normal);\n\t});\n\n\tsettings->startClicks(\n\t) | rpl::on_next([=]() {\n\t\tshowProgress();\n\t\t_process->startExport(*_settings, PrepareEnvironment(_session));\n\t}, settings->lifetime());\n\n\tsettings->cancelClicks(\n\t) | rpl::on_next([=] {\n\t\tLOG((\"Export Info: Panel Hide By Cancel.\"));\n\t\t_panel->hideGetDuration();\n\t}, settings->lifetime());\n\n\tsettings->changes(\n\t) | rpl::on_next([=](Settings &&settings) {\n\t\t*_settings = std::move(settings);\n\t}, settings->lifetime());\n\n\t_panel->showInner(std::move(settings));\n}\n\nvoid PanelController::showError(const ApiErrorState &error) {\n\tLOG((\"Export Info: API Error '%1'.\").arg(error.data.type()));\n\n\tif (error.data.type() == u\"TAKEOUT_INVALID\"_q) {\n\t\tshowError(tr::lng_export_invalid(tr::now));\n\t} else if (error.data.type().startsWith(u\"TAKEOUT_INIT_DELAY_\"_q)) {\n\t\tconst auto seconds = std::max(base::StringViewMid(\n\t\t\terror.data.type(),\n\t\t\tu\"TAKEOUT_INIT_DELAY_\"_q.size()).toInt(), 1);\n\t\tconst auto now = QDateTime::currentDateTime();\n\t\tconst auto when = now.addSecs(seconds);\n\t\tconst auto hours = seconds / 3600;\n\t\tconst auto hoursText = [&] {\n\t\t\tif (hours <= 0) {\n\t\t\t\treturn tr::lng_export_delay_less_than_hour(tr::now);\n\t\t\t}\n\t\t\treturn tr::lng_hours(tr::now, lt_count, hours);\n\t\t}();\n\t\tshowError(tr::lng_export_delay(\n\t\t\ttr::now,\n\t\t\tlt_hours,\n\t\t\thoursText,\n\t\t\tlt_date,\n\t\t\tlangDateTimeFull(when)));\n\n\t\t_settings->availableAt = base::unixtime::now() + seconds;\n\t\t_saveSettingsTimer.callOnce(kSaveSettingsTimeout);\n\n\t\t_session->data().suggestStartExport(_settings->availableAt);\n\t} else {\n\t\tshowCriticalError(\"API Error happened :(\\n\"\n\t\t\t+ QString::number(error.data.code()) + \": \" + error.data.type()\n\t\t\t+ \"\\n\" + error.data.description());\n\t}\n}\n\nvoid PanelController::showError(const OutputErrorState &error) {\n\tshowCriticalError(\"Disk Error happened :(\\n\"\n\t\t\"Could not write path:\\n\" + error.path);\n}\n\nvoid PanelController::showCriticalError(const QString &text) {\n\tauto container = base::make_unique_q<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\t_panel.get(),\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_panel.get(),\n\t\t\ttext,\n\t\t\tst::exportErrorLabel),\n\t\tstyle::margins(0, st::exportPanelSize.height() / 4, 0, 0));\n\tcontainer->widthValue(\n\t) | rpl::on_next([label = container->entity()](int width) {\n\t\tlabel->resize(width, label->height());\n\t}, container->lifetime());\n\n\t_panel->showInner(std::move(container));\n\t_panel->setHideOnDeactivate(false);\n}\n\nvoid PanelController::showError(const QString &text) {\n\tauto box = Ui::MakeInformBox(text);\n\tconst auto weak = base::make_weak(box.data());\n\tconst auto hidden = _panel->isHidden();\n\t_panel->showBox(\n\t\tstd::move(box),\n\t\tUi::LayerOption::CloseOther,\n\t\thidden ? anim::type::instant : anim::type::normal);\n\tweak->setCloseByEscape(false);\n\tweak->setCloseByOutsideClick(false);\n\tweak->boxClosing(\n\t) | rpl::on_next([=] {\n\t\tLOG((\"Export Info: Panel Hide By Error: %1.\").arg(text));\n\t\t_panel->hideGetDuration();\n\t}, weak->lifetime());\n\tif (hidden) {\n\t\t_panel->showAndActivate();\n\t}\n\t_panel->setHideOnDeactivate(false);\n}\n\nvoid PanelController::showProgress() {\n\t_settings->availableAt = 0;\n\tClearSuggestStart(_session);\n\n\t_panel->setTitle(tr::lng_export_progress_title());\n\n\tauto progress = base::make_unique_q<ProgressWidget>(\n\t\t_panel.get(),\n\t\trpl::single(\n\t\t\tContentFromState(_settings.get(), ProcessingState())\n\t\t) | rpl::then(progressState()));\n\n\tprogress->skipFileClicks(\n\t) | rpl::on_next([=](uint64 randomId) {\n\t\t_process->skipFile(randomId);\n\t}, progress->lifetime());\n\n\tprogress->cancelClicks(\n\t) | rpl::on_next([=] {\n\t\tstopWithConfirmation();\n\t}, progress->lifetime());\n\n\tprogress->doneClicks(\n\t) | rpl::on_next([=] {\n\t\tif (const auto finished = std::get_if<FinishedState>(&_state)) {\n\t\t\tFile::ShowInFolder(finished->path);\n\t\t\tLOG((\"Export Info: Panel Hide By Done: %1.\"\n\t\t\t\t).arg(finished->path));\n\t\t\t_panel->hideGetDuration();\n\t\t}\n\t}, progress->lifetime());\n\n\t_panel->showInner(std::move(progress));\n\t_panel->setHideOnDeactivate(true);\n}\n\nvoid PanelController::stopWithConfirmation(Fn<void()> callback) {\n\tif (!v::is<ProcessingState>(_state)) {\n\t\tLOG((\"Export Info: Stop Panel Without Confirmation.\"));\n\t\tstopExport();\n\t\tif (callback) {\n\t\t\tcallback();\n\t\t}\n\t\treturn;\n\t}\n\tauto stop = [=, callback = std::move(callback)]() mutable {\n\t\tif (auto saved = std::move(callback)) {\n\t\t\tLOG((\"Export Info: Stop Panel With Confirmation.\"));\n\t\t\tstopExport();\n\t\t\tsaved();\n\t\t} else {\n\t\t\t_process->cancelExportFast();\n\t\t}\n\t};\n\tconst auto hidden = _panel->isHidden();\n\tconst auto old = _confirmStopBox;\n\tauto box = Ui::MakeConfirmBox({\n\t\t.text = tr::lng_export_sure_stop(),\n\t\t.confirmed = std::move(stop),\n\t\t.confirmText = tr::lng_export_stop(),\n\t\t.confirmStyle = &st::attentionBoxButton,\n\t});\n\t_confirmStopBox = box.data();\n\t_panel->showBox(\n\t\tstd::move(box),\n\t\tUi::LayerOption::CloseOther,\n\t\thidden ? anim::type::instant : anim::type::normal);\n\tif (hidden) {\n\t\t_panel->showAndActivate();\n\t}\n\tif (old) {\n\t\told->closeBox();\n\t}\n}\n\nvoid PanelController::stopExport() {\n\t_stopRequested = true;\n\t_panel->showAndActivate();\n\tLOG((\"Export Info: Panel Hide By Stop\"));\n\t_panel->hideGetDuration();\n}\n\nrpl::producer<> PanelController::stopRequests() const {\n\treturn _panelCloseEvents.events(\n\t) | rpl::flatten_latest(\n\t) | rpl::filter([=] {\n\t\treturn !v::is<ProcessingState>(_state) || _stopRequested;\n\t});\n}\n\nvoid PanelController::fillParams(const PasswordCheckState &state) {\n\t_settings->singlePeer = state.singlePeer;\n}\n\nvoid PanelController::updateState(State &&state) {\n\tif (const auto start = std::get_if<PasswordCheckState>(&state)) {\n\t\tfillParams(*start);\n\t}\n\tif (!_panel) {\n\t\tcreatePanel();\n\t}\n\t_state = std::move(state);\n\tif (const auto apiError = std::get_if<ApiErrorState>(&_state)) {\n\t\tshowError(*apiError);\n\t} else if (const auto error = std::get_if<OutputErrorState>(&_state)) {\n\t\tshowError(*error);\n\t} else if (v::is<FinishedState>(_state)) {\n\t\t_panel->setTitle(tr::lng_export_title());\n\t\t_panel->setHideOnDeactivate(false);\n\t} else if (v::is<CancelledState>(_state)) {\n\t\tLOG((\"Export Info: Stop Panel After Cancel.\"));\n\t\tstopExport();\n\t}\n}\n\nvoid PanelController::saveSettings() const {\n\tconst auto check = [](const QString &value) {\n\t\tconst auto result = value.endsWith('/')\n\t\t\t? value.mid(0, value.size() - 1)\n\t\t\t: value;\n\t\treturn Platform::IsWindows() ? result.toLower() : result;\n\t};\n\tauto settings = *_settings;\n\tif (check(settings.path) == check(File::DefaultDownloadPath(_session))) {\n\t\tsettings.path = QString();\n\t}\n\t_session->local().writeExportSettings(settings);\n}\n\n} // namespace View\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/view/export_view_panel_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"export/export_controller.h\"\n#include \"export/view/export_view_content.h\"\n#include \"base/unique_qptr.h\"\n#include \"base/timer.h\"\n\nnamespace Ui {\nclass SeparatePanel;\nclass BoxContent;\n} // namespace Ui\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Export {\nnamespace View {\n\nbase::weak_qptr<Ui::BoxContent> SuggestStart(not_null<Main::Session*> session);\nvoid ClearSuggestStart(not_null<Main::Session*> session);\nbool IsDefaultPath(not_null<Main::Session*> session, const QString &path);\nvoid ResolveSettings(not_null<Main::Session*> session, Settings &settings);\n\nclass Panel;\n\nclass PanelController {\npublic:\n\tPanelController(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<Controller*> process);\n\t~PanelController();\n\n\t[[nodiscard]] Main::Session &session() const {\n\t\treturn *_session;\n\t}\n\n\tvoid activatePanel();\n\tvoid stopWithConfirmation(Fn<void()> callback = nullptr);\n\n\t[[nodiscard]] rpl::producer<> stopRequests() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\n\tauto progressState() const {\n\t\treturn ContentFromState(\n\t\t\t_settings.get(),\n\t\t\trpl::single(_state) | rpl::then(_process->state()));\n\t}\n\nprivate:\n\tvoid fillParams(const PasswordCheckState &state);\n\tvoid stopExport();\n\tvoid createPanel();\n\tvoid updateState(State &&state);\n\tvoid showSettings();\n\tvoid showProgress();\n\tvoid showError(const ApiErrorState &error);\n\tvoid showError(const OutputErrorState &error);\n\tvoid showError(const QString &text);\n\tvoid showCriticalError(const QString &text);\n\n\tvoid saveSettings() const;\n\n\tconst not_null<Main::Session*> _session;\n\tconst not_null<Controller*> _process;\n\tstd::unique_ptr<Settings> _settings;\n\tbase::Timer _saveSettingsTimer;\n\n\tbase::unique_qptr<Ui::SeparatePanel> _panel;\n\n\tState _state;\n\tbase::weak_qptr<Ui::BoxContent> _confirmStopBox;\n\trpl::event_stream<rpl::producer<>> _panelCloseEvents;\n\tbool _stopRequested = false;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace View\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/view/export_view_progress.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/view/export_view_progress.h\"\n\n#include \"ui/effects/animations.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_export.h\"\n\nnamespace Export {\nnamespace View {\nnamespace {\n\nconstexpr auto kShowSkipFileTimeout = 5 * crl::time(1000);\n\n} // namespace\n\nclass ProgressWidget::Row : public Ui::RpWidget {\npublic:\n\tRow(QWidget *parent, Content::Row &&data);\n\n\tvoid updateData(Content::Row &&data);\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tstruct Instance {\n\t\tbase::unique_qptr<Ui::FadeWrap<Ui::FlatLabel>> label;\n\t\tbase::unique_qptr<Ui::FadeWrap<Ui::FlatLabel>> info;\n\n\t\tfloat64 value = 0.;\n\t\tUi::Animations::Simple progress;\n\n\t\tbool hiding = true;\n\t\tUi::Animations::Simple opacity;\n\t};\n\n\tvoid fillCurrentInstance();\n\tvoid hideCurrentInstance();\n\tvoid setInstanceProgress(Instance &instance, float64 progress);\n\tvoid toggleInstance(Instance &data, bool shown);\n\tvoid instanceOpacityCallback(base::weak_qptr<Ui::FlatLabel> label);\n\tvoid removeOldInstance(base::weak_qptr<Ui::FlatLabel> label);\n\tvoid paintInstance(QPainter &p, const Instance &data);\n\n\tvoid updateControlsGeometry(int newWidth);\n\tvoid updateInstanceGeometry(const Instance &instance, int newWidth);\n\n\tContent::Row _data;\n\tInstance _current;\n\tstd::vector<Instance> _old;\n\n};\n\nProgressWidget::Row::Row(QWidget *parent, Content::Row &&data)\n: RpWidget(parent)\n, _data(std::move(data)) {\n\tfillCurrentInstance();\n}\n\nvoid ProgressWidget::Row::updateData(Content::Row &&data) {\n\tconst auto wasId = _data.id;\n\tconst auto nowId = data.id;\n\t_data = std::move(data);\n\tif (nowId.isEmpty()) {\n\t\thideCurrentInstance();\n\t} else if (wasId.isEmpty()) {\n\t\tfillCurrentInstance();\n\t} else {\n\t\t_current.label->entity()->setText(_data.label);\n\t\t_current.info->entity()->setText(_data.info);\n\t\tsetInstanceProgress(_current, _data.progress);\n\t\tif (nowId != wasId) {\n\t\t\t_current.progress.stop();\n\t\t}\n\t}\n\tupdateControlsGeometry(width());\n\tupdate();\n}\n\nvoid ProgressWidget::Row::fillCurrentInstance() {\n\t_current.label = base::make_unique_q<Ui::FadeWrap<Ui::FlatLabel>>(\n\t\tthis,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\t_data.label,\n\t\t\tst::exportProgressLabel));\n\t_current.info = base::make_unique_q<Ui::FadeWrap<Ui::FlatLabel>>(\n\t\tthis,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\t_data.info,\n\t\t\tst::exportProgressInfoLabel));\n\t_current.label->hide(anim::type::instant);\n\t_current.info->hide(anim::type::instant);\n\n\tsetInstanceProgress(_current, _data.progress);\n\ttoggleInstance(_current, true);\n\tif (_data.id == \"main\") {\n\t\t_current.opacity.stop();\n\t\t_current.label->finishAnimating();\n\t\t_current.info->finishAnimating();\n\t}\n}\n\nvoid ProgressWidget::Row::hideCurrentInstance() {\n\tif (!_current.label) {\n\t\treturn;\n\t}\n\tsetInstanceProgress(_current, 1.);\n\ttoggleInstance(_current, false);\n\t_old.push_back(std::move(_current));\n}\n\nvoid ProgressWidget::Row::setInstanceProgress(\n\t\tInstance &instance,\n\t\tfloat64 progress) {\n\tif (_current.value < progress) {\n\t\t_current.progress.start(\n\t\t\t[=] { update(); },\n\t\t\t_current.value,\n\t\t\tprogress,\n\t\t\tst::exportProgressDuration,\n\t\t\tanim::sineInOut);\n\t} else if (_current.value > progress) {\n\t\t_current.progress.stop();\n\t}\n\t_current.value = progress;\n}\n\nvoid ProgressWidget::Row::toggleInstance(Instance &instance, bool shown) {\n\tExpects(instance.label != nullptr);\n\n\tif (instance.hiding != shown) {\n\t\treturn;\n\t}\n\tconst auto label = base::make_weak(instance.label->entity());\n\tinstance.opacity.start(\n\t\t[=] { instanceOpacityCallback(label); },\n\t\tshown ? 0. : 1.,\n\t\tshown ? 1. : 0.,\n\t\tst::exportProgressDuration);\n\tinstance.hiding = !shown;\n\t_current.label->toggle(shown, anim::type::normal);\n\t_current.info->toggle(shown, anim::type::normal);\n}\n\nvoid ProgressWidget::Row::instanceOpacityCallback(\n\t\tbase::weak_qptr<Ui::FlatLabel> label) {\n\tupdate();\n\tconst auto i = ranges::find(_old, label, [](const Instance &instance) {\n\t\treturn base::make_weak(instance.label->entity());\n\t});\n\tif (i != end(_old) && i->hiding && !i->opacity.animating()) {\n\t\tcrl::on_main(this, [=] {\n\t\t\tremoveOldInstance(label);\n\t\t});\n\t}\n}\n\nvoid ProgressWidget::Row::removeOldInstance(\n\t\tbase::weak_qptr<Ui::FlatLabel> label) {\n\tconst auto i = ranges::find(_old, label, [](const Instance &instance) {\n\t\treturn base::make_weak(instance.label->entity());\n\t});\n\tif (i != end(_old)) {\n\t\t_old.erase(i);\n\t}\n}\n\nint ProgressWidget::Row::resizeGetHeight(int newWidth) {\n\tupdateControlsGeometry(newWidth);\n\treturn st::exportProgressRowHeight;\n}\n\nvoid ProgressWidget::Row::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto thickness = st::exportProgressWidth;\n\tconst auto top = height() - thickness;\n\tp.fillRect(0, top, width(), thickness, st::shadowFg);\n\n\tfor (const auto &instance : _old) {\n\t\tpaintInstance(p, instance);\n\t}\n\tpaintInstance(p, _current);\n}\n\nvoid ProgressWidget::Row::paintInstance(QPainter &p, const Instance &data) {\n\tconst auto opacity = data.opacity.value(data.hiding ? 0. : 1.);\n\n\tif (!opacity) {\n\t\treturn;\n\t}\n\tp.setOpacity(opacity);\n\n\tconst auto thickness = st::exportProgressWidth;\n\tconst auto top = height() - thickness;\n\tconst auto till = qRound(data.progress.value(data.value) * width());\n\tif (till > 0) {\n\t\tp.fillRect(0, top, till, thickness, st::exportProgressFg);\n\t}\n\tif (till < width()) {\n\t\tconst auto left = width() - till;\n\t\tp.fillRect(till, top, left, thickness, st::exportProgressBg);\n\t}\n}\n\nvoid ProgressWidget::Row::updateControlsGeometry(int newWidth) {\n\tupdateInstanceGeometry(_current, newWidth);\n\tfor (const auto &instance : _old) {\n\t\tupdateInstanceGeometry(instance, newWidth);\n\t}\n}\n\nvoid ProgressWidget::Row::updateInstanceGeometry(\n\t\tconst Instance &instance,\n\t\tint newWidth) {\n\tif (!instance.label) {\n\t\treturn;\n\t}\n\tinstance.info->resizeToNaturalWidth(newWidth);\n\tinstance.label->resizeToWidth(newWidth - instance.info->width());\n\tinstance.info->moveToRight(0, 0, newWidth);\n\tinstance.label->moveToLeft(0, 0, newWidth);\n}\n\nProgressWidget::ProgressWidget(\n\tQWidget *parent,\n\trpl::producer<Content> content)\n: RpWidget(parent)\n, _body(this)\n, _fileShowSkipTimer([=] { _skipFile->show(anim::type::normal); }) {\n\twidthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_body->resizeToWidth(width);\n\t\t_body->moveToLeft(0, 0);\n\t}, _body->lifetime());\n\n\tauto skipFileWrap = _body->add(object_ptr<Ui::FixedHeightWidget>(\n\t\t_body.data(),\n\t\tst::defaultLinkButton.font->height + st::exportProgressRowSkip));\n\t_skipFile = base::make_unique_q<Ui::FadeWrap<Ui::LinkButton>>(\n\t\tskipFileWrap,\n\t\tobject_ptr<Ui::LinkButton>(\n\t\t\tthis,\n\t\t\ttr::lng_export_skip_file(tr::now),\n\t\t\tst::defaultLinkButton));\n\t_skipFile->hide(anim::type::instant);\n\t_skipFile->moveToLeft(st::exportProgressRowPadding.left(), 0);\n\n\t_about = _body->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\ttr::lng_export_progress(tr::now),\n\t\t\tst::exportAboutLabel),\n\t\tst::exportAboutPadding);\n\n\tstd::move(\n\t\tcontent\n\t) | rpl::on_next([=](Content &&content) {\n\t\tupdateState(std::move(content));\n\t}, lifetime());\n\n\t_cancel = base::make_unique_q<Ui::RoundButton>(\n\t\tthis,\n\t\ttr::lng_export_stop(),\n\t\tst::exportCancelButton);\n\tsetupBottomButton(_cancel.get());\n}\n\nrpl::producer<uint64> ProgressWidget::skipFileClicks() const {\n\treturn _skipFile->entity()->clicks(\n\t) | rpl::map([=] { return _fileRandomId; });\n}\n\nrpl::producer<> ProgressWidget::cancelClicks() const {\n\treturn _cancel\n\t\t? (_cancel->clicks() | rpl::to_empty)\n\t\t: (rpl::never<>() | rpl::type_erased);\n}\n\nrpl::producer<> ProgressWidget::doneClicks() const {\n\treturn _doneClicks.events();\n}\n\nvoid ProgressWidget::setupBottomButton(not_null<Ui::RoundButton*> button) {\n\tbutton->show();\n\n\tsizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tbutton->move(\n\t\t\t(size.width() - button->width()) / 2,\n\t\t\t(size.height() - st::exportCancelBottom - button->height()));\n\t}, button->lifetime());\n}\n\nvoid ProgressWidget::updateState(Content &&content) {\n\tif (!content.rows.empty() && content.rows[0].id == Content::kDoneId) {\n\t\tshowDone();\n\t}\n\n\tconst auto wasCount = _rows.size();\n\tauto index = 0;\n\tfor (auto &row : content.rows) {\n\t\tif (index < _rows.size()) {\n\t\t\t_rows[index]->updateData(std::move(row));\n\t\t} else {\n\t\t\tif (index > 0) {\n\t\t\t\t_body->insert(\n\t\t\t\t\tindex * 2 - 1,\n\t\t\t\t\tobject_ptr<Ui::FixedHeightWidget>(\n\t\t\t\t\t\tthis,\n\t\t\t\t\t\tst::exportProgressRowSkip));\n\t\t\t}\n\t\t\t_rows.push_back(_body->insert(\n\t\t\t\tindex * 2,\n\t\t\t\tobject_ptr<Row>(this, std::move(row)),\n\t\t\t\tst::exportProgressRowPadding));\n\t\t\t_rows.back()->show();\n\t\t}\n\t\t++index;\n\t}\n\tconst auto fileRandomId = !content.rows.empty()\n\t\t? content.rows.back().randomId\n\t\t: uint64(0);\n\tif (_fileRandomId != fileRandomId) {\n\t\t_fileShowSkipTimer.cancel();\n\t\t_skipFile->hide(anim::type::normal);\n\t\t_fileRandomId = fileRandomId;\n\t\tif (_fileRandomId) {\n\t\t\t_fileShowSkipTimer.callOnce(kShowSkipFileTimeout);\n\t\t}\n\t}\n\tfor (const auto count = _rows.size(); index != count; ++index) {\n\t\t_rows[index]->updateData(Content::Row());\n\t}\n\tif (_rows.size() != wasCount) {\n\t\t_body->resizeToWidth(width());\n\t}\n}\n\nvoid ProgressWidget::showDone() {\n\t_cancel = nullptr;\n\t_skipFile->hide(anim::type::instant);\n\t_fileShowSkipTimer.cancel();\n\t_about->setText(tr::lng_export_about_done(tr::now));\n\t_done = base::make_unique_q<Ui::RoundButton>(\n\t\tthis,\n\t\ttr::lng_export_done(),\n\t\tst::exportDoneButton);\n\tconst auto desired = std::min(\n\t\tst::exportDoneButton.style.font->width(tr::lng_export_done(tr::now))\n\t\t+ st::exportDoneButton.height\n\t\t- st::exportDoneButton.style.font->height,\n\t\tst::exportPanelSize.width() - 2 * st::exportCancelBottom);\n\tif (_done->width() < desired) {\n\t\t_done->setFullWidth(desired);\n\t}\n\t_done->clicks(\n\t) | rpl::to_empty\n\t| rpl::start_to_stream(_doneClicks, _done->lifetime());\n\tsetupBottomButton(_done.get());\n}\n\nProgressWidget::~ProgressWidget() = default;\n\n} // namespace View\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/view/export_view_progress.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"export/view/export_view_content.h\"\n#include \"base/object_ptr.h\"\n#include \"base/timer.h\"\n\nnamespace Ui {\nclass VerticalLayout;\nclass RoundButton;\nclass FlatLabel;\nclass LinkButton;\ntemplate <typename Widget>\nclass FadeWrap;\n} // namespace Ui\n\nnamespace Export {\nnamespace View {\n\nclass ProgressWidget : public Ui::RpWidget {\npublic:\n\tProgressWidget(\n\t\tQWidget *parent,\n\t\trpl::producer<Content> content);\n\n\trpl::producer<uint64> skipFileClicks() const;\n\trpl::producer<> cancelClicks() const;\n\trpl::producer<> doneClicks() const;\n\n\t~ProgressWidget();\n\nprivate:\n\tvoid setupBottomButton(not_null<Ui::RoundButton*> button);\n\tvoid updateState(Content &&content);\n\tvoid showDone();\n\n\tContent _content;\n\n\tclass Row;\n\tobject_ptr<Ui::VerticalLayout> _body;\n\tstd::vector<not_null<Row*>> _rows;\n\n\tbase::unique_qptr<Ui::FadeWrap<Ui::LinkButton>> _skipFile;\n\tQPointer<Ui::FlatLabel> _about;\n\tbase::unique_qptr<Ui::RoundButton> _cancel;\n\tbase::unique_qptr<Ui::RoundButton> _done;\n\trpl::event_stream<> _doneClicks;\n\n\tuint64 _fileRandomId = 0;\n\tbase::Timer _fileShowSkipTimer;\n\n};\n\n} // namespace View\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/view/export_view_settings.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/view/export_view_settings.h\"\n\n#include \"export/output/export_output_abstract.h\"\n#include \"export/view/export_view_panel_controller.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/boxes/calendar_box.h\"\n#include \"ui/boxes/choose_time.h\"\n#include \"platform/platform_specific.h\"\n#include \"core/application.h\"\n#include \"core/file_utilities.h\"\n#include \"base/unixtime.h\"\n#include \"main/main_session.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_export.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Export {\nnamespace View {\nnamespace {\n\nconstexpr auto kMegabyte = int64(1024) * 1024;\n\n[[nodiscard]] PeerId ReadPeerId(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPInputPeer &data) {\n\treturn data.match([](const MTPDinputPeerUser &data) {\n\t\treturn peerFromUser(data.vuser_id().v);\n\t}, [](const MTPDinputPeerUserFromMessage &data) {\n\t\treturn peerFromUser(data.vuser_id().v);\n\t}, [](const MTPDinputPeerChat &data) {\n\t\treturn peerFromChat(data.vchat_id().v);\n\t}, [](const MTPDinputPeerChannel &data) {\n\t\treturn peerFromChannel(data.vchannel_id().v);\n\t}, [](const MTPDinputPeerChannelFromMessage &data) {\n\t\treturn peerFromChannel(data.vchannel_id().v);\n\t}, [&](const MTPDinputPeerSelf &data) {\n\t\treturn session->userPeerId();\n\t}, [](const MTPDinputPeerEmpty &data) {\n\t\treturn PeerId(0);\n\t});\n}\n\nvoid ChooseFormatBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tOutput::Format format,\n\t\tFn<void(Output::Format)> done) {\n\tusing Format = Output::Format;\n\tconst auto group = std::make_shared<Ui::RadioenumGroup<Format>>(format);\n\tconst auto addFormatOption = [&](QString label, Format format) {\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::Radioenum<Format>>(\n\t\t\t\tbox,\n\t\t\t\tgroup,\n\t\t\t\tformat,\n\t\t\t\tlabel,\n\t\t\t\tst::defaultBoxCheckbox),\n\t\t\tst::exportSettingPadding);\n\t};\n\tbox->setTitle(tr::lng_export_option_choose_format());\n\taddFormatOption(tr::lng_export_option_html(tr::now), Format::Html);\n\taddFormatOption(tr::lng_export_option_json(tr::now), Format::Json);\n\taddFormatOption(\n\t\ttr::lng_export_option_html_and_json(tr::now),\n\t\tFormat::HtmlAndJson);\n\tbox->addButton(tr::lng_settings_save(), [=] { done(group->current()); });\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\n} // namespace\n\nint64 SizeLimitByIndex(int index) {\n\tExpects(index >= 0 && index < kSizeValueCount);\n\n\tindex += 1;\n\tconst auto megabytes = [&] {\n\t\tif (index <= 10) {\n\t\t\treturn index;\n\t\t} else if (index <= 30) {\n\t\t\treturn 10 + (index - 10) * 2;\n\t\t} else if (index <= 40) {\n\t\t\treturn 50 + (index - 30) * 5;\n\t\t} else if (index <= 60) {\n\t\t\treturn 100 + (index - 40) * 10;\n\t\t} else if (index <= 70) {\n\t\t\treturn 300 + (index - 60) * 20;\n\t\t} else if (index <= 80) {\n\t\t\treturn 500 + (index - 70) * 50;\n\t\t} else if (index <= 90) {\n\t\t\treturn 1000 + (index - 80) * 100;\n\t\t} else {\n\t\t\treturn 2000 + (index - 90) * 200;\n\t\t}\n\t}();\n\treturn megabytes * kMegabyte;\n}\n\nSettingsWidget::SettingsWidget(\n\tQWidget *parent,\n\tnot_null<Main::Session*> session,\n\tSettings data)\n: RpWidget(parent)\n, _session(session)\n, _singlePeerId(ReadPeerId(session, data.singlePeer))\n, _internal_data(std::move(data)) {\n\tResolveSettings(session, _internal_data);\n\tsetupContent();\n}\n\nconst Settings &SettingsWidget::readData() const {\n\treturn _internal_data;\n}\n\ntemplate <typename Callback>\nvoid SettingsWidget::changeData(Callback &&callback) {\n\tcallback(_internal_data);\n\t_changes.fire_copy(_internal_data);\n}\n\nvoid SettingsWidget::setupContent() {\n\tconst auto scroll = Ui::CreateChild<Ui::ScrollArea>(\n\t\tthis,\n\t\tst::boxScroll);\n\tconst auto wrap = scroll->setOwnedWidget(\n\t\tobject_ptr<Ui::OverrideMargins>(\n\t\t\tscroll,\n\t\t\tobject_ptr<Ui::VerticalLayout>(scroll)));\n\tconst auto content = static_cast<Ui::VerticalLayout*>(wrap->entity());\n\n\tconst auto buttons = setupButtons(scroll, wrap);\n\tsetupOptions(content);\n\tsetupPathAndFormat(content);\n\n\tsizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tscroll->resize(size.width(), size.height() - buttons->height());\n\t\twrap->resizeToWidth(size.width());\n\t\tcontent->resizeToWidth(size.width());\n\t}, lifetime());\n}\n\nvoid SettingsWidget::setupOptions(not_null<Ui::VerticalLayout*> container) {\n\tif (!_singlePeerId) {\n\t\tsetupFullExportOptions(container);\n\t}\n\tsetupMediaOptions(container);\n\tif (!_singlePeerId) {\n\t\tsetupOtherOptions(container);\n\t}\n}\n\nvoid SettingsWidget::setupFullExportOptions(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\taddOptionWithAbout(\n\t\tcontainer,\n\t\ttr::lng_export_option_info(tr::now),\n\t\tType::PersonalInfo | Type::Userpics,\n\t\ttr::lng_export_option_info_about(tr::now));\n\taddOptionWithAbout(\n\t\tcontainer,\n\t\ttr::lng_export_option_contacts(tr::now),\n\t\tType::Contacts,\n\t\ttr::lng_export_option_contacts_about(tr::now));\n\taddOptionWithAbout(\n\t\tcontainer,\n\t\ttr::lng_export_option_stories(tr::now),\n\t\tType::Stories,\n\t\ttr::lng_export_option_stories_about(tr::now));\n\taddOptionWithAbout(\n\t\tcontainer,\n\t\ttr::lng_export_option_profile_music(tr::now),\n\t\tType::ProfileMusic,\n\t\ttr::lng_export_option_profile_music_about(tr::now));\n\taddHeader(container, tr::lng_export_header_chats(tr::now));\n\taddOption(\n\t\tcontainer,\n\t\ttr::lng_export_option_personal_chats(tr::now),\n\t\tType::PersonalChats);\n\taddOption(\n\t\tcontainer,\n\t\ttr::lng_export_option_bot_chats(tr::now),\n\t\tType::BotChats);\n\taddChatOption(\n\t\tcontainer,\n\t\ttr::lng_export_option_private_groups(tr::now),\n\t\tType::PrivateGroups);\n\taddChatOption(\n\t\tcontainer,\n\t\ttr::lng_export_option_private_channels(tr::now),\n\t\tType::PrivateChannels);\n\taddChatOption(\n\t\tcontainer,\n\t\ttr::lng_export_option_public_groups(tr::now),\n\t\tType::PublicGroups);\n\taddChatOption(\n\t\tcontainer,\n\t\ttr::lng_export_option_public_channels(tr::now),\n\t\tType::PublicChannels);\n}\n\nvoid SettingsWidget::setupMediaOptions(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tif (_singlePeerId != 0) {\n\t\taddMediaOptions(container);\n\t\treturn;\n\t}\n\tconst auto mediaWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto media = mediaWrap->entity();\n\taddHeader(media, tr::lng_export_header_media(tr::now));\n\taddMediaOptions(media);\n\n\tvalue() | rpl::map([](const Settings &data) {\n\t\treturn data.types;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](Settings::Types types) {\n\t\tmediaWrap->toggle((types & (Type::PersonalChats\n\t\t\t| Type::BotChats\n\t\t\t| Type::PrivateGroups\n\t\t\t| Type::PrivateChannels\n\t\t\t| Type::PublicGroups\n\t\t\t| Type::PublicChannels\n\t\t\t| Type::ProfileMusic)) != 0, anim::type::normal);\n\t}, mediaWrap->lifetime());\n\n\twidthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tmediaWrap->resizeToWidth(width);\n\t}, mediaWrap->lifetime());\n}\n\nvoid SettingsWidget::setupOtherOptions(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\taddHeader(container, tr::lng_export_header_other(tr::now));\n\taddOptionWithAbout(\n\t\tcontainer,\n\t\ttr::lng_export_option_sessions(tr::now),\n\t\tType::Sessions,\n\t\ttr::lng_export_option_sessions_about(tr::now));\n\taddOptionWithAbout(\n\t\tcontainer,\n\t\ttr::lng_export_option_other(tr::now),\n\t\tType::OtherData,\n\t\ttr::lng_export_option_other_about(tr::now));\n}\n\nvoid SettingsWidget::setupPathAndFormat(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tif (_singlePeerId != 0) {\n\t\taddFormatAndLocationLabel(container);\n\t\taddLimitsLabel(container);\n\t\treturn;\n\t}\n\tconst auto formatGroup = std::make_shared<Ui::RadioenumGroup<Format>>(\n\t\treadData().format);\n\tformatGroup->setChangedCallback([=](Format format) {\n\t\tchangeData([&](Settings &data) {\n\t\t\tdata.format = format;\n\t\t});\n\t});\n\tconst auto addFormatOption = [&](QString label, Format format) {\n\t\tcontainer->add(\n\t\t\tobject_ptr<Ui::Radioenum<Format>>(\n\t\t\t\tcontainer,\n\t\t\t\tformatGroup,\n\t\t\t\tformat,\n\t\t\t\tlabel,\n\t\t\t\tst::defaultBoxCheckbox),\n\t\t\tst::exportSettingPadding);\n\t};\n\taddHeader(container, tr::lng_export_header_format(tr::now));\n\taddLocationLabel(container);\n\taddFormatOption(tr::lng_export_option_html(tr::now), Format::Html);\n\taddFormatOption(tr::lng_export_option_json(tr::now), Format::Json);\n\taddFormatOption(tr::lng_export_option_html_and_json(tr::now), Format::HtmlAndJson);\n}\n\nvoid SettingsWidget::addLocationLabel(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n#ifndef OS_MAC_STORE\n\tauto pathLink = value() | rpl::map([](const Settings &data) {\n\t\treturn data.path;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::map([=](const QString &path) {\n\t\tconst auto text = IsDefaultPath(_session, path)\n\t\t\t? Core::App().canReadDefaultDownloadPath()\n\t\t\t? u\"Downloads/\"_q + File::DefaultDownloadPathFolder(_session)\n\t\t\t: tr::lng_download_path_temp(tr::now)\n\t\t\t: path;\n\t\treturn tr::link(\n\t\t\tQDir::toNativeSeparators(text),\n\t\t\tQString(\"internal:edit_export_path\"));\n\t});\n\tconst auto label = container->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_export_option_location(\n\t\t\t\tlt_path,\n\t\t\t\tstd::move(pathLink),\n\t\t\t\ttr::marked),\n\t\t\tst::exportLocationLabel),\n\t\tst::exportLocationPadding);\n\tlabel->overrideLinkClickHandler([=] {\n\t\tchooseFolder();\n\t});\n#endif // OS_MAC_STORE\n}\n\nvoid SettingsWidget::chooseFormat() {\n\tconst auto shared = std::make_shared<base::weak_qptr<Ui::GenericBox>>();\n\tconst auto callback = [=](Format format) {\n\t\tchangeData([&](Settings &data) {\n\t\t\tdata.format = format;\n\t\t});\n\t\tif (const auto strong = shared->get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t};\n\tauto box = Box(\n\t\tChooseFormatBox,\n\t\treadData().format,\n\t\tcallback);\n\t*shared = base::make_weak(box.data());\n\t_showBoxCallback(std::move(box));\n}\n\nvoid SettingsWidget::addFormatAndLocationLabel(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n#ifndef OS_MAC_STORE\n\tauto pathLink = value() | rpl::map([](const Settings &data) {\n\t\treturn data.path;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::map([=](const QString &path) {\n\t\tconst auto text = IsDefaultPath(_session, path)\n\t\t\t? Core::App().canReadDefaultDownloadPath()\n\t\t\t? u\"Downloads/\"_q + File::DefaultDownloadPathFolder(_session)\n\t\t\t: tr::lng_download_path_temp(tr::now)\n\t\t\t: path;\n\t\treturn tr::link(\n\t\t\tQDir::toNativeSeparators(text),\n\t\t\tu\"internal:edit_export_path\"_q);\n\t});\n\tauto formatLink = value() | rpl::map([](const Settings &data) {\n\t\treturn data.format;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::map([](Format format) {\n\t\tconst auto text = (format == Format::Html)\n\t\t\t? \"HTML\"\n\t\t\t: (format == Format::Json)\n\t\t\t? \"JSON\"\n\t\t\t: tr::lng_export_option_html_and_json(tr::now);\n\t\treturn tr::link(text, u\"internal:edit_format\"_q);\n\t});\n\tconst auto label = container->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_export_option_format_location(\n\t\t\t\tlt_format,\n\t\t\t\tstd::move(formatLink),\n\t\t\t\tlt_path,\n\t\t\t\tstd::move(pathLink),\n\t\t\t\ttr::marked),\n\t\t\tst::exportLocationLabel),\n\t\tst::exportLocationPadding);\n\tlabel->overrideLinkClickHandler([=](const QString &url) {\n\t\tif (url == u\"internal:edit_export_path\"_q) {\n\t\t\tchooseFolder();\n\t\t} else if (url == u\"internal:edit_format\"_q) {\n\t\t\tchooseFormat();\n\t\t} else {\n\t\t\tUnexpected(\"Click handler URL in export limits edit.\");\n\t\t}\n\t});\n#endif // OS_MAC_STORE\n}\n\nvoid SettingsWidget::addLimitsLabel(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tauto fromDateLink = value() | rpl::map([](const Settings &data) {\n\t\treturn data.singlePeerFrom;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::map([](TimeId from) {\n\t\treturn (from\n\t\t\t? rpl::single(langDayOfMonthFull(\n\t\t\t\tbase::unixtime::parse(from).date()))\n\t\t\t: tr::lng_export_beginning()\n\t\t) | rpl::map(tr::url(u\"internal:edit_from\"_q));\n\t}) | rpl::flatten_latest();\n\n\tconst auto mapToTime = [](TimeId id, const QString &link) {\n\t\treturn rpl::single(id\n\t\t\t? QLocale().toString(\n\t\t\t\tbase::unixtime::parse(id).time(),\n\t\t\t\tQLocale::ShortFormat)\n\t\t\t: QString()\n\t\t) | rpl::map(tr::url(link));\n\t};\n\n\tconst auto concat = [](TextWithEntities date, TextWithEntities link) {\n\t\treturn link.text.isEmpty()\n\t\t\t? date\n\t\t\t: date.append(u\", \"_q).append(std::move(link));\n\t};\n\n\tauto fromTimeLink = value() | rpl::map([](const Settings &data) {\n\t\treturn data.singlePeerFrom;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::map([=](TimeId from) {\n\t\treturn mapToTime(from, u\"internal:edit_from_time\"_q);\n\t}) | rpl::flatten_latest();\n\n\tauto fromLink = rpl::combine(\n\t\tstd::move(fromDateLink),\n\t\tstd::move(fromTimeLink)\n\t) | rpl::map(concat);\n\n\tauto tillDateLink = value() | rpl::map([](const Settings &data) {\n\t\treturn data.singlePeerTill;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::map([](TimeId till) {\n\t\treturn (till\n\t\t\t? rpl::single(langDayOfMonthFull(\n\t\t\t\tbase::unixtime::parse(till).date()))\n\t\t\t: tr::lng_export_end()\n\t\t) | rpl::map(tr::url(u\"internal:edit_till\"_q));\n\t}) | rpl::flatten_latest();\n\n\tauto tillTimeLink = value() | rpl::map([](const Settings &data) {\n\t\treturn data.singlePeerTill;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::map([=](TimeId till) {\n\t\treturn mapToTime(till, u\"internal:edit_till_time\"_q);\n\t}) | rpl::flatten_latest();\n\n\tauto tillLink = rpl::combine(\n\t\tstd::move(tillDateLink),\n\t\tstd::move(tillTimeLink)\n\t) | rpl::map(concat);\n\n\tauto datesText = tr::lng_export_limits(\n\t\tlt_from,\n\t\tstd::move(fromLink),\n\t\tlt_till,\n\t\tstd::move(tillLink),\n\t\ttr::marked\n\t) | rpl::after_next([=] {\n\t\tcontainer->resizeToWidth(container->width());\n\t});\n\n\tconst auto label = container->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\tstd::move(datesText),\n\t\t\tst::boxLabel),\n\t\tst::exportLimitsPadding);\n\n\tconst auto removeTime = [](TimeId dateTime) {\n\t\treturn base::unixtime::serialize(\n\t\t\tQDateTime(\n\t\t\t\tbase::unixtime::parse(dateTime).date(),\n\t\t\t\tQTime()));\n\t};\n\n\tconst auto editTimeLimit = [=](Fn<TimeId()> now, Fn<void(TimeId)> done) {\n\t\t_showBoxCallback(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tauto result = Ui::ChooseTimeWidget(\n\t\t\t\tbox->verticalLayout(),\n\t\t\t\t[&] {\n\t\t\t\t\tconst auto time = base::unixtime::parse(now()).time();\n\t\t\t\t\treturn time.hour() * 3600\n\t\t\t\t\t\t+ time.minute() * 60\n\t\t\t\t\t\t+ time.second();\n\t\t\t\t}(),\n\t\t\t\ttrue);\n\t\t\tconst auto widget = box->addRow(std::move(result.widget));\n\t\t\tconst auto toSave = widget->lifetime().make_state<TimeId>(0);\n\t\t\tstd::move(\n\t\t\t\tresult.secondsValue\n\t\t\t) | rpl::on_next([=](TimeId t) {\n\t\t\t\t*toSave = t;\n\t\t\t}, box->lifetime());\n\t\t\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\t\t\tdone(*toSave);\n\t\t\t\tbox->closeBox();\n\t\t\t});\n\t\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\t\tbox->closeBox();\n\t\t\t});\n\t\t\tbox->setTitle(tr::lng_settings_ttl_after_custom());\n\t\t}));\n\t};\n\n\tconstexpr auto kOffset = 600;\n\n\tlabel->overrideLinkClickHandler([=](const QString &url) {\n\t\tif (url == u\"internal:edit_from\"_q) {\n\t\t\tconst auto done = [=](TimeId limit) {\n\t\t\t\tchangeData([&](Settings &settings) {\n\t\t\t\t\tsettings.singlePeerFrom = limit;\n\t\t\t\t});\n\t\t\t};\n\t\t\teditDateLimit(\n\t\t\t\treadData().singlePeerFrom,\n\t\t\t\t0,\n\t\t\t\treadData().singlePeerTill,\n\t\t\t\ttr::lng_export_from_beginning(),\n\t\t\t\tdone);\n\t\t} else if (url == u\"internal:edit_from_time\"_q) {\n\t\t\tconst auto now = [=] {\n\t\t\t\tauto result = TimeId(0);\n\t\t\t\tchangeData([&](Settings &settings) {\n\t\t\t\t\tresult = settings.singlePeerFrom;\n\t\t\t\t});\n\t\t\t\treturn result;\n\t\t\t};\n\t\t\tconst auto done = [=](TimeId time) {\n\t\t\t\tchangeData([&](Settings &settings) {\n\t\t\t\t\tconst auto result = time\n\t\t\t\t\t\t+ removeTime(settings.singlePeerFrom);\n\t\t\t\t\tif (result >= settings.singlePeerTill\n\t\t\t\t\t\t\t&& settings.singlePeerTill) {\n\t\t\t\t\t\tsettings.singlePeerFrom = settings.singlePeerTill\n\t\t\t\t\t\t\t- kOffset;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsettings.singlePeerFrom = result;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t};\n\t\t\teditTimeLimit(now, done);\n\t\t} else if (url == u\"internal:edit_till\"_q) {\n\t\t\tconst auto done = [=](TimeId limit) {\n\t\t\t\tchangeData([&](Settings &settings) {\n\t\t\t\t\tif (limit <= settings.singlePeerFrom\n\t\t\t\t\t\t\t&& settings.singlePeerFrom) {\n\t\t\t\t\t\tsettings.singlePeerTill = settings.singlePeerFrom\n\t\t\t\t\t\t\t+ kOffset;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsettings.singlePeerTill = limit;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t};\n\t\t\teditDateLimit(\n\t\t\t\treadData().singlePeerTill,\n\t\t\t\treadData().singlePeerFrom,\n\t\t\t\t0,\n\t\t\t\ttr::lng_export_till_end(),\n\t\t\t\tdone);\n\t\t} else if (url == u\"internal:edit_till_time\"_q) {\n\t\t\tconst auto now = [=] {\n\t\t\t\tauto result = TimeId(0);\n\t\t\t\tchangeData([&](Settings &settings) {\n\t\t\t\t\tresult = settings.singlePeerTill;\n\t\t\t\t});\n\t\t\t\treturn result;\n\t\t\t};\n\t\t\tconst auto done = [=](TimeId time) {\n\t\t\t\tchangeData([&](Settings &settings) {\n\t\t\t\t\tconst auto result = time\n\t\t\t\t\t\t+ removeTime(settings.singlePeerTill);\n\t\t\t\t\tif (result <= settings.singlePeerFrom\n\t\t\t\t\t\t\t&& settings.singlePeerFrom) {\n\t\t\t\t\t\tsettings.singlePeerTill = settings.singlePeerFrom\n\t\t\t\t\t\t\t+ kOffset;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsettings.singlePeerTill = result;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t};\n\t\t\teditTimeLimit(now, done);\n\t\t} else {\n\t\t\tUnexpected(\"Click handler URL in export limits edit.\");\n\t\t}\n\t});\n}\n\nvoid SettingsWidget::editDateLimit(\n\t\tTimeId current,\n\t\tTimeId min,\n\t\tTimeId max,\n\t\trpl::producer<QString> resetLabel,\n\t\tFn<void(TimeId)> done) {\n\tExpects(_showBoxCallback != nullptr);\n\n\tconst auto highlighted = current\n\t\t? base::unixtime::parse(current).date()\n\t\t: max\n\t\t? base::unixtime::parse(max).date()\n\t\t: min\n\t\t? base::unixtime::parse(min).date()\n\t\t: QDate::currentDate();\n\tconst auto month = highlighted;\n\tconst auto shared = std::make_shared<base::weak_qptr<Ui::CalendarBox>>();\n\tconst auto finalize = [=](not_null<Ui::CalendarBox*> box) {\n\t\tbox->addLeftButton(std::move(resetLabel), crl::guard(this, [=] {\n\t\t\tdone(0);\n\t\t\tif (const auto weak = shared->get()) {\n\t\t\t\tweak->closeBox();\n\t\t\t}\n\t\t}));\n\t};\n\tconst auto callback = crl::guard(this, [=](\n\t\t\tconst QDate &date,\n\t\t\tFn<void()> close) {\n\t\tdone(base::unixtime::serialize(date.startOfDay()));\n\t\tclose();\n\t});\n\tauto box = Box<Ui::CalendarBox>(Ui::CalendarBoxArgs{\n\t\t.month = month,\n\t\t.highlighted = highlighted,\n\t\t.callback = callback,\n\t\t.finalize = finalize,\n\t\t.st = st::exportCalendarSizes,\n\t\t.minDate = (min\n\t\t\t? base::unixtime::parse(min).date()\n\t\t\t: QDate(2013, 8, 1)), // Telegram was launched in August 2013 :)\n\t\t.maxDate = (max\n\t\t\t? base::unixtime::parse(max).date()\n\t\t\t: QDate::currentDate()),\n\t});\n\t*shared = base::make_weak(box.data());\n\t_showBoxCallback(std::move(box));\n}\n\nnot_null<Ui::RpWidget*> SettingsWidget::setupButtons(\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\tnot_null<Ui::RpWidget*> wrap) {\n\tusing namespace rpl::mappers;\n\n\tconst auto buttonsPadding = st::defaultBox.buttonPadding;\n\tconst auto buttonsHeight = buttonsPadding.top()\n\t\t+ st::defaultBoxButton.height\n\t\t+ buttonsPadding.bottom();\n\tconst auto buttons = Ui::CreateChild<Ui::FixedHeightWidget>(\n\t\tthis,\n\t\tbuttonsHeight);\n\tconst auto topShadow = Ui::CreateChild<Ui::FadeShadow>(this);\n\tconst auto bottomShadow = Ui::CreateChild<Ui::FadeShadow>(this);\n\ttopShadow->toggleOn(scroll->scrollTopValue(\n\t) | rpl::map(_1 > 0));\n\tbottomShadow->toggleOn(rpl::combine(\n\t\tscroll->heightValue(),\n\t\tscroll->scrollTopValue(),\n\t\twrap->heightValue(),\n\t\t_2\n\t) | rpl::map([=](int top) {\n\t\treturn top < scroll->scrollTopMax();\n\t}));\n\n\tvalue() | rpl::map([](const Settings &data) {\n\t\treturn (data.types != Types(0)) || data.onlySinglePeer();\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](bool canStart) {\n\t\trefreshButtons(buttons, canStart);\n\t\ttopShadow->raise();\n\t\tbottomShadow->raise();\n\t}, buttons->lifetime());\n\n\tsizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tbuttons->resizeToWidth(size.width());\n\t\tbuttons->moveToLeft(0, size.height() - buttons->height());\n\t\ttopShadow->resizeToWidth(size.width());\n\t\ttopShadow->moveToLeft(0, 0);\n\t\tbottomShadow->resizeToWidth(size.width());\n\t\tbottomShadow->moveToLeft(0, buttons->y() - st::lineWidth);\n\t}, buttons->lifetime());\n\n\treturn buttons;\n}\n\nvoid SettingsWidget::addHeader(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst QString &text) {\n\tcontainer->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttext,\n\t\t\tst::exportHeaderLabel),\n\t\tst::exportHeaderPadding);\n}\n\nnot_null<Ui::Checkbox*> SettingsWidget::addOption(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst QString &text,\n\t\tTypes types) {\n\tconst auto checkbox = container->add(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tcontainer,\n\t\t\ttext,\n\t\t\t((readData().types & types) == types),\n\t\t\tst::defaultBoxCheckbox),\n\t\tst::exportSettingPadding);\n\tcheckbox->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tchangeData([&](Settings &data) {\n\t\t\tif (checked) {\n\t\t\t\tdata.types |= types;\n\t\t\t} else {\n\t\t\t\tdata.types &= ~types;\n\t\t\t}\n\t\t});\n\t}, checkbox->lifetime());\n\treturn checkbox;\n}\n\nnot_null<Ui::Checkbox*> SettingsWidget::addOptionWithAbout(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst QString &text,\n\t\tTypes types,\n\t\tconst QString &about) {\n\tconst auto result = addOption(container, text, types);\n\tcontainer->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\tabout,\n\t\t\tst::exportAboutOptionLabel),\n\t\tst::exportAboutOptionPadding);\n\treturn result;\n}\n\nvoid SettingsWidget::addChatOption(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst QString &text,\n\t\tTypes types) {\n\tconst auto checkbox = addOption(container, text, types);\n\tconst auto onlyMy = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::Checkbox>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_export_option_only_my(tr::now),\n\t\t\t\t((readData().fullChats & types) != types),\n\t\t\t\tst::defaultBoxCheckbox),\n\t\t\tst::exportSubSettingPadding));\n\n\tonlyMy->entity()->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tchangeData([&](Settings &data) {\n\t\t\tif (checked) {\n\t\t\t\tdata.fullChats &= ~types;\n\t\t\t} else {\n\t\t\t\tdata.fullChats |= types;\n\t\t\t}\n\t\t});\n\t}, onlyMy->lifetime());\n\n\tonlyMy->toggleOn(checkbox->checkedValue());\n\n\tif (types & (Type::PublicGroups | Type::PublicChannels)) {\n\t\tonlyMy->entity()->setChecked(true);\n\t\tonlyMy->entity()->setDisabled(true);\n\t}\n}\n\nvoid SettingsWidget::addMediaOptions(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\taddMediaOption(\n\t\tcontainer,\n\t\ttr::lng_export_option_photos(tr::now),\n\t\tMediaType::Photo);\n\taddMediaOption(\n\t\tcontainer,\n\t\ttr::lng_export_option_video_files(tr::now),\n\t\tMediaType::Video);\n\taddMediaOption(\n\t\tcontainer,\n\t\ttr::lng_export_option_voice_messages(tr::now),\n\t\tMediaType::VoiceMessage);\n\taddMediaOption(\n\t\tcontainer,\n\t\ttr::lng_export_option_video_messages(tr::now),\n\t\tMediaType::VideoMessage);\n\taddMediaOption(\n\t\tcontainer,\n\t\ttr::lng_export_option_stickers(tr::now),\n\t\tMediaType::Sticker);\n\taddMediaOption(\n\t\tcontainer,\n\t\ttr::lng_export_option_gifs(tr::now),\n\t\tMediaType::GIF);\n\taddMediaOption(\n\t\tcontainer,\n\t\ttr::lng_export_option_files(tr::now),\n\t\tMediaType::File);\n\taddSizeSlider(container);\n}\n\nvoid SettingsWidget::addMediaOption(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst QString &text,\n\t\tMediaType type) {\n\tconst auto checkbox = container->add(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tcontainer,\n\t\t\ttext,\n\t\t\t((readData().media.types & type) == type),\n\t\t\tst::defaultBoxCheckbox),\n\t\tst::exportSettingPadding);\n\tcheckbox->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tchangeData([&](Settings &data) {\n\t\t\tif (checked) {\n\t\t\t\tdata.media.types |= type;\n\t\t\t} else {\n\t\t\t\tdata.media.types &= ~type;\n\t\t\t}\n\t\t});\n\t}, checkbox->lifetime());\n}\n\nvoid SettingsWidget::addSizeSlider(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tusing namespace rpl::mappers;\n\n\tconst auto slider = container->add(\n\t\tobject_ptr<Ui::MediaSlider>(container, st::exportFileSizeSlider),\n\t\tst::exportFileSizePadding);\n\tslider->resize(st::exportFileSizeSlider.seekSize);\n\tslider->setPseudoDiscrete(\n\t\tkSizeValueCount,\n\t\tSizeLimitByIndex,\n\t\treadData().media.sizeLimit,\n\t\t[=](int64 limit) {\n\t\t\tchangeData([&](Settings &data) {\n\t\t\t\tdata.media.sizeLimit = limit;\n\t\t\t});\n\t\t});\n\n\tconst auto label = Ui::CreateChild<Ui::LabelSimple>(\n\t\tcontainer.get(),\n\t\tst::exportFileSizeLabel);\n\tvalue() | rpl::map([](const Settings &data) {\n\t\treturn data.media.sizeLimit;\n\t}) | rpl::on_next([=](int64 sizeLimit) {\n\t\tconst auto limit = sizeLimit / kMegabyte;\n\t\tconst auto size = QString::number(limit) + \" MB\";\n\t\tconst auto text = tr::lng_export_option_size_limit(\n\t\t\ttr::now,\n\t\t\tlt_size,\n\t\t\tsize);\n\t\tlabel->setText(text);\n\t}, slider->lifetime());\n\n\trpl::combine(\n\t\tlabel->widthValue(),\n\t\tslider->geometryValue(),\n\t\t_2\n\t) | rpl::on_next([=](QRect geometry) {\n\t\tlabel->moveToRight(\n\t\t\tst::exportFileSizePadding.right(),\n\t\t\tgeometry.y() - label->height() - st::exportFileSizeLabelBottom);\n\t}, label->lifetime());\n}\n\nvoid SettingsWidget::refreshButtons(\n\t\tnot_null<Ui::RpWidget*> container,\n\t\tbool canStart) {\n\tcontainer->hideChildren();\n\tconst auto children = container->children();\n\tfor (const auto child : children) {\n\t\tif (child->isWidgetType()) {\n\t\t\tchild->deleteLater();\n\t\t}\n\t}\n\tconst auto start = canStart\n\t\t? Ui::CreateChild<Ui::RoundButton>(\n\t\t\tcontainer.get(),\n\t\t\ttr::lng_export_start(),\n\t\t\tst::defaultBoxButton)\n\t\t: nullptr;\n\tif (start) {\n\t\tstart->show();\n\t\t_startClicks = start->clicks() | rpl::to_empty;\n\n\t\tcontainer->sizeValue(\n\t\t) | rpl::on_next([=](QSize size) {\n\t\t\tconst auto right = st::defaultBox.buttonPadding.right();\n\t\t\tconst auto top = st::defaultBox.buttonPadding.top();\n\t\t\tstart->moveToRight(right, top);\n\t\t}, start->lifetime());\n\t}\n\n\tconst auto cancel = Ui::CreateChild<Ui::RoundButton>(\n\t\tcontainer.get(),\n\t\ttr::lng_cancel(),\n\t\tst::defaultBoxButton);\n\tcancel->show();\n\t_cancelClicks = cancel->clicks() | rpl::to_empty;\n\n\trpl::combine(\n\t\tcontainer->sizeValue(),\n\t\tstart ? start->widthValue() : rpl::single(0)\n\t) | rpl::on_next([=](QSize size, int width) {\n\t\tconst auto right = st::defaultBox.buttonPadding.right()\n\t\t\t+ (width ? width + st::defaultBox.buttonPadding.left() : 0);\n\t\tconst auto top = st::defaultBox.buttonPadding.top();\n\t\tcancel->moveToRight(right, top);\n\t}, cancel->lifetime());\n}\n\nvoid SettingsWidget::chooseFolder() {\n\tconst auto callback = [=](QString &&result) {\n\t\tchangeData([&](Settings &data) {\n\t\t\tdata.path = std::move(result);\n\t\t\tdata.forceSubPath = IsDefaultPath(_session, data.path);\n\t\t});\n\t};\n\tFileDialog::GetFolder(\n\t\tthis,\n\t\ttr::lng_export_folder(tr::now),\n\t\treadData().path,\n\t\tcallback);\n}\n\nrpl::producer<Settings> SettingsWidget::changes() const {\n\treturn _changes.events();\n}\n\nrpl::producer<Settings> SettingsWidget::value() const {\n\treturn rpl::single(readData()) | rpl::then(changes());\n}\n\nrpl::producer<> SettingsWidget::startClicks() const {\n\treturn _startClicks.value(\n\t) | rpl::map([](Wrap &&wrap) {\n\t\treturn std::move(wrap.value);\n\t}) | rpl::flatten_latest();\n}\n\nrpl::producer<> SettingsWidget::cancelClicks() const {\n\treturn _cancelClicks.value(\n\t) | rpl::map([](Wrap &&wrap) {\n\t\treturn std::move(wrap.value);\n\t}) | rpl::flatten_latest();\n}\n\n} // namespace View\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/view/export_view_settings.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"export/export_settings.h\"\n#include \"ui/rp_widget.h\"\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\nclass VerticalLayout;\nclass Checkbox;\nclass ScrollArea;\nclass BoxContent;\n} // namespace Ui\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Export {\nnamespace View {\n\nconstexpr auto kSizeValueCount = 100;\nint64 SizeLimitByIndex(int index);\n\nclass SettingsWidget : public Ui::RpWidget {\npublic:\n\tSettingsWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Session*> session,\n\t\tSettings data);\n\n\trpl::producer<Settings> value() const;\n\trpl::producer<Settings> changes() const;\n\trpl::producer<> startClicks() const;\n\trpl::producer<> cancelClicks() const;\n\n\tvoid setShowBoxCallback(Fn<void(object_ptr<Ui::BoxContent>)> callback) {\n\t\t_showBoxCallback = std::move(callback);\n\t}\n\nprivate:\n\tusing Type = Settings::Type;\n\tusing Types = Settings::Types;\n\tusing MediaType = MediaSettings::Type;\n\tusing MediaTypes = MediaSettings::Types;\n\tusing Format = Output::Format;\n\n\tvoid setupContent();\n\tnot_null<Ui::RpWidget*> setupButtons(\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\tnot_null<Ui::RpWidget*> wrap);\n\tvoid setupOptions(not_null<Ui::VerticalLayout*> container);\n\tvoid setupFullExportOptions(not_null<Ui::VerticalLayout*> container);\n\tvoid setupMediaOptions(not_null<Ui::VerticalLayout*> container);\n\tvoid setupOtherOptions(not_null<Ui::VerticalLayout*> container);\n\tvoid setupPathAndFormat(not_null<Ui::VerticalLayout*> container);\n\tvoid addHeader(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst QString &text);\n\tnot_null<Ui::Checkbox*> addOption(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst QString &text,\n\t\tTypes types);\n\tnot_null<Ui::Checkbox*> addOptionWithAbout(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst QString &text,\n\t\tTypes types,\n\t\tconst QString &about);\n\tvoid addChatOption(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst QString &text,\n\t\tTypes types);\n\tvoid addMediaOptions(not_null<Ui::VerticalLayout*> container);\n\tvoid addMediaOption(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst QString &text,\n\t\tMediaType type);\n\tvoid addSizeSlider(not_null<Ui::VerticalLayout*> container);\n\tvoid addLocationLabel(\n\t\tnot_null<Ui::VerticalLayout*> container);\n\tvoid addFormatAndLocationLabel(\n\t\tnot_null<Ui::VerticalLayout*> container);\n\tvoid addLimitsLabel(\n\t\tnot_null<Ui::VerticalLayout*> container);\n\tvoid chooseFolder();\n\tvoid chooseFormat();\n\tvoid refreshButtons(\n\t\tnot_null<Ui::RpWidget*> container,\n\t\tbool canStart);\n\n\tvoid editDateLimit(\n\t\tTimeId current,\n\t\tTimeId min,\n\t\tTimeId max,\n\t\trpl::producer<QString> resetLabel,\n\t\tFn<void(TimeId)> done);\n\n\tconst Settings &readData() const;\n\ttemplate <typename Callback>\n\tvoid changeData(Callback &&callback);\n\n\tconst not_null<Main::Session*> _session;\n\tPeerId _singlePeerId = 0;\n\tFn<void(object_ptr<Ui::BoxContent>)> _showBoxCallback;\n\n\t// Use through readData / changeData wrappers.\n\tSettings _internal_data;\n\n\tstruct Wrap {\n\t\tWrap(rpl::producer<> value = nullptr)\n\t\t: value(std::move(value)) {\n\t\t}\n\n\t\trpl::producer<> value;\n\t};\n\trpl::event_stream<Settings> _changes;\n\trpl::variable<Wrap> _startClicks;\n\trpl::variable<Wrap> _cancelClicks;\n\n};\n\n} // namespace View\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/view/export_view_top_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"export/view/export_view_top_bar.h\"\n\n#include \"export/view/export_view_content.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_export.h\"\n#include \"styles/style_media_player.h\"\n\nnamespace Export {\nnamespace View {\n\nTopBar::TopBar(QWidget *parent, Content &&content)\n: RpWidget(parent)\n, _infoLeft(this, st::exportTopBarLabel)\n, _infoMiddle(this, st::exportTopBarLabel)\n, _infoRight(this, st::exportTopBarLabel)\n, _shadow(this)\n, _progress(this, st::mediaPlayerPlayback)\n, _button(this) {\n\tresize(width(), st::mediaPlayerHeight + st::lineWidth);\n\t_infoMiddle->setElisionMiddle(true);\n\t_progress->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tupdateData(std::move(content));\n}\n\nrpl::producer<Qt::MouseButton> TopBar::clicks() const {\n\treturn _button->clicks();\n}\n\nvoid TopBar::resizeToWidthInfo(int w) {\n\tif (w <= 0) {\n\t\treturn;\n\t}\n\tconst auto &infoFont = st::mediaPlayerName.style.font;\n\tconst auto infoTop = st::mediaPlayerNameTop - infoFont->ascent;\n\tconst auto padding = st::mediaPlayerPlayLeft + st::mediaPlayerPadding;\n\tconst auto spacing = infoFont->spacew;\n\t_infoLeft->moveToLeft(padding, infoTop);\n\tauto availableWidth = w;\n\tavailableWidth -= rect::right(_infoLeft);\n\tavailableWidth -= padding;\n\t_infoMiddle->resizeToWidth(_infoMiddle->naturalWidth());\n\t_infoRight->resizeToWidth(_infoRight->naturalWidth());\n\tconst auto requiredWidth = spacing\n\t\t+ _infoMiddle->naturalWidth()\n\t\t+ (_infoRight->naturalWidth()\n\t\t\t? (spacing + _infoRight->naturalWidth())\n\t\t\t: 0);\n\tif (requiredWidth > availableWidth) {\n\t\t_infoRight->moveToLeft(\n\t\t\tw - padding - _infoRight->width(),\n\t\t\tinfoTop);\n\t\t_infoMiddle->resizeToWidth(qMax(\n\t\t\t_infoRight->x()\n\t\t\t\t- rect::right(_infoLeft)\n\t\t\t\t- spacing * 2,\n\t\t\t0));\n\t\t_infoMiddle->moveToLeft(\n\t\t\trect::right(_infoLeft) + spacing,\n\t\t\tinfoTop);\n\t} else {\n\t\t_infoMiddle->moveToLeft(\n\t\t\trect::right(_infoLeft) + spacing,\n\t\t\tinfoTop);\n\t\t_infoRight->moveToLeft(\n\t\t\trect::right(_infoMiddle) + spacing,\n\t\t\tinfoTop);\n\t}\n}\n\nvoid TopBar::updateData(Content &&content) {\n\tif (content.rows.empty()) {\n\t\treturn;\n\t}\n\tconst auto &row = content.rows[0];\n\t_infoLeft->setMarkedText(\n\t\ttr::lng_export_progress_title(tr::now, tr::bold)\n\t\t\t.append(' ')\n\t\t\t.append(QChar(0x2013)));\n\t_infoMiddle->setText(row.label);\n\t_infoRight->setMarkedText(Ui::Text::Colorized(row.info));\n\tresizeToWidthInfo(width());\n\t_progress->setValue(row.progress);\n}\n\nvoid TopBar::resizeEvent(QResizeEvent *e) {\n\tresizeToWidthInfo(e->size().width());\n\t_button->setGeometry(0, 0, width(), height() - st::lineWidth);\n\t_progress->setGeometry(\n\t\t0,\n\t\theight() - st::mediaPlayerPlayback.fullWidth,\n\t\twidth(),\n\t\tst::mediaPlayerPlayback.fullWidth);\n}\n\nvoid TopBar::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tauto fill = e->rect().intersected(\n\t\tQRect(0, 0, width(), st::mediaPlayerHeight + st::lineWidth));\n\tif (!fill.isEmpty()) {\n\t\tp.fillRect(fill, st::mediaPlayerBg);\n\t}\n}\n\nvoid TopBar::setShadowGeometryToLeft(int x, int y, int w, int h) {\n\t_shadow->setGeometryToLeft(x, y, w, h);\n}\n\nvoid TopBar::showShadow() {\n\t_shadow->show();\n\t_progress->show();\n}\n\nvoid TopBar::hideShadow() {\n\t_shadow->hide();\n\t_progress->hide();\n}\n\nTopBar::~TopBar() = default;\n\n} // namespace View\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/export/view/export_view_top_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\nclass FlatLabel;\nclass FilledSlider;\nclass AbstractButton;\nclass PlainShadow;\n} // namespace Ui\n\nnamespace Export {\nnamespace View {\n\nstruct Content;\n\nclass TopBar : public Ui::RpWidget {\npublic:\n\tTopBar(QWidget *parent, Content &&content);\n\n\trpl::producer<Qt::MouseButton> clicks() const;\n\n\tvoid updateData(Content &&content);\n\n\tvoid setShadowGeometryToLeft(int x, int y, int w, int h);\n\tvoid showShadow();\n\tvoid hideShadow();\n\n\t~TopBar();\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid resizeToWidthInfo(int w);\n\n\tobject_ptr<Ui::FlatLabel> _infoLeft;\n\tobject_ptr<Ui::FlatLabel> _infoMiddle;\n\tobject_ptr<Ui::FlatLabel> _infoRight;\n\tobject_ptr<Ui::PlainShadow> _shadow = { nullptr };\n\tobject_ptr<Ui::FilledSlider> _progress;\n\tobject_ptr<Ui::AbstractButton> _button;\n\n};\n\n} // namespace View\n} // namespace Export\n"
  },
  {
    "path": "Telegram/SourceFiles/ffmpeg/ffmpeg_bytes_io_wrap.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ffmpeg/ffmpeg_utility.h\"\n\nnamespace FFmpeg {\n\nstruct ReadBytesWrap {\n\tint64 size = 0;\n\tint64 offset = 0;\n\tconst uchar *data = nullptr;\n\n\tstatic int Read(void *opaque, uint8_t *buf, int buf_size) {\n\t\tauto wrap = static_cast<ReadBytesWrap*>(opaque);\n\t\tconst auto toRead = std::min(\n\t\t\tint64(buf_size),\n\t\t\twrap->size - wrap->offset);\n\t\tif (!toRead) {\n\t\t\treturn AVERROR_EOF;\n\t\t} else if (toRead > 0) {\n\t\t\tmemcpy(buf, wrap->data + wrap->offset, toRead);\n\t\t\twrap->offset += toRead;\n\t\t}\n\t\treturn toRead;\n\t}\n\tstatic int64_t Seek(void *opaque, int64_t offset, int whence) {\n\t\tauto wrap = static_cast<ReadBytesWrap*>(opaque);\n\t\tauto updated = int64(-1);\n\t\tswitch (whence) {\n\t\tcase SEEK_SET: updated = offset; break;\n\t\tcase SEEK_CUR: updated = wrap->offset + offset; break;\n\t\tcase SEEK_END: updated = wrap->size + offset; break;\n\t\tcase AVSEEK_SIZE: return wrap->size; break;\n\t\t}\n\t\tif (updated < 0 || updated > wrap->size) {\n\t\t\treturn -1;\n\t\t}\n\t\twrap->offset = updated;\n\t\treturn updated;\n\t}\n};\n\nstruct WriteBytesWrap {\n\tQByteArray content;\n\tint64 offset = 0;\n\n#if DA_FFMPEG_CONST_WRITE_CALLBACK\n\tstatic int Write(void *opaque, const uint8_t *_buf, int buf_size) {\n\t\tuint8_t *buf = const_cast<uint8_t *>(_buf);\n#else\n\tstatic int Write(void *opaque, uint8_t *buf, int buf_size) {\n#endif\n\t\tauto wrap = static_cast<WriteBytesWrap*>(opaque);\n\t\tif (const auto total = wrap->offset + int64(buf_size)) {\n\t\t\tconst auto size = int64(wrap->content.size());\n\t\t\tconstexpr auto kReserve = 1024 * 1024;\n\t\t\twrap->content.reserve((total / kReserve) * kReserve);\n\t\t\tconst auto overwrite = std::min(\n\t\t\t\tsize - wrap->offset,\n\t\t\t\tint64(buf_size));\n\t\t\tif (overwrite) {\n\t\t\t\tmemcpy(wrap->content.data() + wrap->offset, buf, overwrite);\n\t\t\t}\n\t\t\tif (const auto append = buf_size - overwrite) {\n\t\t\t\twrap->content.append(\n\t\t\t\t\treinterpret_cast<const char*>(buf) + overwrite,\n\t\t\t\t\tappend);\n\t\t\t}\n\t\t\twrap->offset += buf_size;\n\t\t}\n\t\treturn buf_size;\n\t}\n\n\tstatic int64_t Seek(void *opaque, int64_t offset, int whence) {\n\t\tauto wrap = static_cast<WriteBytesWrap*>(opaque);\n\t\tconst auto &content = wrap->content;\n\t\tconst auto checkedSeek = [&](int64_t offset) {\n\t\t\tif (offset < 0 || offset > int64(content.size())) {\n\t\t\t\treturn int64_t(-1);\n\t\t\t}\n\t\t\treturn int64_t(wrap->offset = offset);\n\t\t};\n\t\tswitch (whence) {\n\t\tcase SEEK_SET: return checkedSeek(offset);\n\t\tcase SEEK_CUR: return checkedSeek(wrap->offset + offset);\n\t\tcase SEEK_END: return checkedSeek(int64(content.size()) + offset);\n\t\tcase AVSEEK_SIZE: return int64(content.size());\n\t\t}\n\t\treturn -1;\n\t}\n};\n\n} // namespace FFmpeg\n"
  },
  {
    "path": "Telegram/SourceFiles/ffmpeg/ffmpeg_frame_generator.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ffmpeg/ffmpeg_frame_generator.h\"\n\n#include \"ffmpeg/ffmpeg_utility.h\"\n#include \"base/debug_log.h\"\n\nnamespace FFmpeg {\nnamespace {\n\nconstexpr auto kMaxArea = 1920 * 1080 * 4;\n\n} // namespace\n\nclass FrameGenerator::Impl final {\npublic:\n\texplicit Impl(const QByteArray &bytes);\n\n\t[[nodiscard]] Frame renderNext(\n\t\tQImage storage,\n\t\tQSize size,\n\t\tQt::AspectRatioMode mode);\n\t[[nodiscard]] Frame renderCurrent(\n\t\tQImage storage,\n\t\tQSize size,\n\t\tQt::AspectRatioMode mode);\n\tvoid jumpToStart();\n\nprivate:\n\tstruct ReadFrame {\n\t\tFramePointer frame;\n\t\tcrl::time position = 0;\n\t\tcrl::time duration = 0;\n\t};\n\n\tvoid readNextFrame();\n\tvoid resolveNextFrameTiming();\n\n\t[[nodiscard]] QString wrapError(int result) const;\n\n\tbool rotationSwapWidthHeight() const {\n\t\treturn (_rotation == 90) || (_rotation == 270);\n\t}\n\n\t[[nodiscard]] static int Read(\n\t\tvoid *opaque,\n\t\tuint8_t *buf,\n\t\tint buf_size);\n\t[[nodiscard]] static int64_t Seek(\n\t\tvoid *opaque,\n\t\tint64_t offset,\n\t\tint whence);\n\t[[nodiscard]] int read(uint8_t *buf, int buf_size);\n\t[[nodiscard]] int64_t seek(int64_t offset, int whence);\n\n\tconst QByteArray _bytes;\n\tint _deviceOffset = 0;\n\n\tFormatPointer _format;\n\tReadFrame _current;\n\tReadFrame _next;\n\tCodecPointer _codec;\n\tSwscalePointer _scale;\n\n\tint _streamId = 0;\n\tint _rotation = 0;\n\t//AVRational _aspect = kNormalAspect;\n\n\tcrl::time _framePosition = 0;\n\tint _nextFrameDelay = 0;\n\tint _currentFrameDelay = 0;\n\n};\n\nFrameGenerator::Impl::Impl(const QByteArray &bytes)\n: _bytes(bytes) {\n\t_format = MakeFormatPointer(\n\t\tstatic_cast<void*>(this),\n\t\t&FrameGenerator::Impl::Read,\n\t\tnullptr,\n\t\t&FrameGenerator::Impl::Seek);\n\n\tauto error = 0;\n\tif ((error = avformat_find_stream_info(_format.get(), nullptr))) {\n\t\treturn;\n\t}\n\t_streamId = av_find_best_stream(\n\t\t_format.get(),\n\t\tAVMEDIA_TYPE_VIDEO,\n\t\t-1,\n\t\t-1,\n\t\tnullptr,\n\t\t0);\n\tif (_streamId < 0) {\n\t\treturn;\n\t}\n\n\tconst auto info = _format->streams[_streamId];\n\t_rotation = ReadRotationFromMetadata(info);\n\t//_aspect = ValidateAspectRatio(info->sample_aspect_ratio);\n\t_codec = MakeCodecPointer({ .stream = info });\n}\n\nint FrameGenerator::Impl::Read(void *opaque, uint8_t *buf, int buf_size) {\n\treturn static_cast<Impl*>(opaque)->read(buf, buf_size);\n}\n\nint FrameGenerator::Impl::read(uint8_t *buf, int buf_size) {\n\tconst auto available = _bytes.size() - _deviceOffset;\n\tif (available <= 0) {\n\t\treturn AVERROR_EOF;\n\t}\n\tconst auto fill = std::min(int(available), buf_size);\n\tmemcpy(buf, _bytes.data() + _deviceOffset, fill);\n\t_deviceOffset += fill;\n\treturn fill;\n}\n\nint64_t FrameGenerator::Impl::Seek(\n\t\tvoid *opaque,\n\t\tint64_t offset,\n\t\tint whence) {\n\treturn static_cast<Impl*>(opaque)->seek(offset, whence);\n}\n\nint64_t FrameGenerator::Impl::seek(int64_t offset, int whence) {\n\tif (whence == AVSEEK_SIZE) {\n\t\treturn _bytes.size();\n\t}\n\tconst auto now = [&] {\n\t\tswitch (whence) {\n\t\tcase SEEK_SET: return offset;\n\t\tcase SEEK_CUR: return _deviceOffset + offset;\n\t\tcase SEEK_END: return int64_t(_bytes.size()) + offset;\n\t\t}\n\t\treturn int64_t(-1);\n\t}();\n\tif (now < 0 || now > _bytes.size()) {\n\t\treturn -1;\n\t}\n\t_deviceOffset = now;\n\treturn now;\n}\n\nFrameGenerator::Frame FrameGenerator::Impl::renderCurrent(\n\t\tQImage storage,\n\t\tQSize size,\n\t\tQt::AspectRatioMode mode) {\n\tExpects(_current.frame != nullptr);\n\n\tconst auto frame = _current.frame.get();\n\tconst auto width = frame->width;\n\tconst auto height = frame->height;\n\tif (!width || !height) {\n\t\tLOG((\"Webm Error: Bad frame size: %1x%2 \").arg(width).arg(height));\n\t\treturn {};\n\t}\n\n\tauto scaled = QSize(width, height).scaled(size, mode);\n\tif (!scaled.isEmpty() && rotationSwapWidthHeight()) {\n\t\tscaled.transpose();\n\t}\n\tif (!GoodStorageForFrame(storage, size)) {\n\t\tstorage = CreateFrameStorage(size);\n\t}\n\tconst auto dx = (size.width() - scaled.width()) / 2;\n\tconst auto dy = (size.height() - scaled.height()) / 2;\n\tAssert(dx >= 0 && dy >= 0 && (!dx || !dy));\n\n\tconst auto srcFormat = (frame->format == AV_PIX_FMT_NONE)\n\t\t? _codec->pix_fmt\n\t\t: frame->format;\n\tconst auto srcSize = QSize(frame->width, frame->height);\n\tconst auto dstFormat = AV_PIX_FMT_BGRA;\n\tconst auto dstSize = scaled;\n\tconst auto bgra = (srcFormat == AV_PIX_FMT_BGRA);\n\tconst auto withAlpha = bgra || (srcFormat == AV_PIX_FMT_YUVA420P);\n\tconst auto dstPerLine = storage.bytesPerLine();\n\tauto dst = storage.bits() + dx * sizeof(int32) + dy * dstPerLine;\n\tif (bgra && srcSize == dstSize && frame->linesize[0] > 0) {\n\t\tconst auto srcPerLine = frame->linesize[0];\n\t\tconst auto perLine = std::min(srcPerLine, int(dstPerLine));\n\t\tauto src = frame->data[0];\n\t\tfor (auto y = 0, height = srcSize.height(); y != height; ++y) {\n\t\t\tmemcpy(dst, src, perLine);\n\t\t\tsrc += srcPerLine;\n\t\t\tdst += dstPerLine;\n\t\t}\n\t} else {\n\t\t_scale = MakeSwscalePointer(\n\t\t\tsrcSize,\n\t\t\tsrcFormat,\n\t\t\tdstSize,\n\t\t\tdstFormat,\n\t\t\t&_scale);\n\t\tAssert(_scale != nullptr);\n\n\t\t// AV_NUM_DATA_POINTERS defined in AVFrame struct\n\t\tuint8_t *dstData[AV_NUM_DATA_POINTERS] = { dst, nullptr };\n\t\tint dstLinesize[AV_NUM_DATA_POINTERS] = { int(dstPerLine), 0 };\n\t\tsws_scale(\n\t\t\t_scale.get(),\n\t\t\tframe->data,\n\t\t\tframe->linesize,\n\t\t\t0,\n\t\t\tframe->height,\n\t\t\tdstData,\n\t\t\tdstLinesize);\n\t}\n\tif (dx && size.height() > 0) {\n\t\tauto dst = storage.bits();\n\t\tconst auto line = scaled.width() * sizeof(int32);\n\t\tmemset(dst, 0, dx * sizeof(int32));\n\t\tdst += dx * sizeof(int32);\n\t\tfor (auto y = 0; y != size.height() - 1; ++y) {\n\t\t\tmemset(dst + line, 0, (dstPerLine - line));\n\t\t\tdst += dstPerLine;\n\t\t}\n\t\tdst += line;\n\t\tmemset(dst, 0, (size.width() - scaled.width() - dx) * sizeof(int32));\n\t} else if (dy && size.width() > 0) {\n\t\tconst auto dst = storage.bits();\n\t\tmemset(dst, 0, dstPerLine * dy);\n\t\tmemset(\n\t\t\tdst + dstPerLine * (dy + scaled.height()),\n\t\t\t0,\n\t\t\tdstPerLine * (size.height() - scaled.height() - dy));\n\t}\n\tif (withAlpha) {\n\t\tPremultiplyInplace(storage);\n\t}\n\tif (_rotation != 0) {\n\t\tauto transform = QTransform();\n\t\ttransform.rotate(_rotation);\n\t\tstorage = storage.transformed(transform);\n\t}\n\n\tconst auto duration = _next.frame\n\t\t? (_next.position - _current.position)\n\t\t: _current.duration;\n\treturn {\n\t\t.duration = duration,\n\t\t.image = std::move(storage),\n\t\t.last = !_next.frame,\n\t};\n}\n\nFrameGenerator::Frame FrameGenerator::Impl::renderNext(\n\t\tQImage storage,\n\t\tQSize size,\n\t\tQt::AspectRatioMode mode) {\n\tif (!_codec) {\n\t\treturn {};\n\t} else if (!_current.frame) {\n\t\treadNextFrame();\n\t}\n\tstd::swap(_current, _next);\n\tif (!_current.frame) {\n\t\treturn {};\n\t}\n\treadNextFrame();\n\treturn renderCurrent(std::move(storage), size, mode);\n}\n\nvoid FrameGenerator::Impl::jumpToStart() {\n\tif (!_codec) {\n\t\treturn;\n\t}\n\tauto result = 0;\n\tif ((result = avformat_seek_file(_format.get(), _streamId, std::numeric_limits<int64_t>::min(), 0, std::numeric_limits<int64_t>::max(), 0)) < 0) {\n\t\tif ((result = av_seek_frame(_format.get(), _streamId, 0, AVSEEK_FLAG_BYTE)) < 0) {\n\t\t\tif ((result = av_seek_frame(_format.get(), _streamId, 0, AVSEEK_FLAG_FRAME)) < 0) {\n\t\t\t\tif ((result = av_seek_frame(_format.get(), _streamId, 0, 0)) < 0) {\n\t\t\t\t\tLOG((\"Webm Error: Unable to av_seek_frame() to the start, \") + wrapError(result));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tavcodec_flush_buffers(_codec.get());\n\t_current = ReadFrame();\n\t_next = ReadFrame();\n\t_currentFrameDelay = _nextFrameDelay = 0;\n\t_framePosition = 0;\n}\n\nvoid FrameGenerator::Impl::resolveNextFrameTiming() {\n\tconst auto base = _format->streams[_streamId]->time_base;\n\tconst auto duration = _next.frame->duration;\n\tconst auto framePts = _next.frame->pts;\n\tauto framePosition = (framePts * 1000LL * base.num) / base.den;\n\t_currentFrameDelay = _nextFrameDelay;\n\tif (_framePosition + _currentFrameDelay < framePosition) {\n\t\t_currentFrameDelay = int32(framePosition - _framePosition);\n\t} else if (framePosition < _framePosition + _currentFrameDelay) {\n\t\tframePosition = _framePosition + _currentFrameDelay;\n\t}\n\n\tif (duration == AV_NOPTS_VALUE) {\n\t\t_nextFrameDelay = 0;\n\t} else {\n\t\t_nextFrameDelay = (duration * 1000LL * base.num) / base.den;\n\t}\n\t_framePosition = framePosition;\n\n\t_next.position = _framePosition;\n\t_next.duration = _nextFrameDelay;\n}\n\nvoid FrameGenerator::Impl::readNextFrame() {\n\tauto frame = _next.frame ? base::take(_next.frame) : MakeFramePointer();\n\twhile (true) {\n\t\tauto result = avcodec_receive_frame(_codec.get(), frame.get());\n\t\tif (result >= 0) {\n\t\t\tif (frame->width * frame->height > kMaxArea) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_next.frame = std::move(frame);\n\t\t\tresolveNextFrameTiming();\n\t\t\treturn;\n\t\t}\n\n\t\tif (result == AVERROR_EOF) {\n\t\t\treturn;\n\t\t} else if (result != AVERROR(EAGAIN)) {\n\t\t\tLOG((\"Webm Error: Unable to avcodec_receive_frame(), \")\n\t\t\t\t+ wrapError(result));\n\t\t\treturn;\n\t\t}\n\n\t\tauto packet = Packet();\n\t\tauto finished = false;\n\t\tdo {\n\t\t\tconst auto result = av_read_frame(\n\t\t\t\t_format.get(),\n\t\t\t\t&packet.fields());\n\t\t\tif (result == AVERROR_EOF) {\n\t\t\t\tfinished = true;\n\t\t\t\tbreak;\n\t\t\t} else if (result < 0) {\n\t\t\t\tLOG((\"Webm Error: Unable to av_read_frame(), \")\n\t\t\t\t\t+ wrapError(result));\n\t\t\t\treturn;\n\t\t\t}\n\t\t} while (packet.fields().stream_index != _streamId);\n\n\t\tif (finished) {\n\t\t\tresult = avcodec_send_packet(_codec.get(), nullptr); // Drain.\n\t\t} else {\n\t\t\tconst auto native = &packet.fields();\n\t\t\tconst auto guard = gsl::finally([\n\t\t\t\t&,\n\t\t\t\tsize = native->size,\n\t\t\t\tdata = native->data\n\t\t\t] {\n\t\t\t\tnative->size = size;\n\t\t\t\tnative->data = data;\n\t\t\t\tpacket = Packet();\n\t\t\t});\n\t\t\tresult = avcodec_send_packet(_codec.get(), native);\n\t\t}\n\t\tif (result < 0) {\n\t\t\tLOG((\"Webm Error: Unable to avcodec_send_packet(), \")\n\t\t\t\t+ wrapError(result));\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nQString FrameGenerator::Impl::wrapError(int result) const {\n\tauto error = std::array<char, AV_ERROR_MAX_STRING_SIZE>{};\n\treturn u\"error %1, %2\"_q\n\t\t.arg(result)\n\t\t.arg(av_make_error_string(error.data(), error.size(), result));\n}\n\nFrameGenerator::FrameGenerator(const QByteArray &bytes)\n: _impl(std::make_unique<Impl>(bytes)) {\n}\n\nFrameGenerator::~FrameGenerator() = default;\n\nint FrameGenerator::count() {\n\treturn 0;\n}\n\ndouble FrameGenerator::rate() {\n\treturn 0.;\n}\n\nFrameGenerator::Frame FrameGenerator::renderNext(\n\t\tQImage storage,\n\t\tQSize size,\n\t\tQt::AspectRatioMode mode) {\n\treturn _impl->renderNext(std::move(storage), size, mode);\n}\n\nFrameGenerator::Frame FrameGenerator::renderCurrent(\n\t\tQImage storage,\n\t\tQSize size,\n\t\tQt::AspectRatioMode mode) {\n\treturn _impl->renderCurrent(std::move(storage), size, mode);\n}\n\nvoid FrameGenerator::jumpToStart() {\n\t_impl->jumpToStart();\n}\n\n} // namespace FFmpeg\n"
  },
  {
    "path": "Telegram/SourceFiles/ffmpeg/ffmpeg_frame_generator.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/frame_generator.h\"\n\n#include <QtGui/QImage>\n#include <memory>\n\nnamespace FFmpeg {\n\nclass FrameGenerator final : public Ui::FrameGenerator {\npublic:\n\texplicit FrameGenerator(const QByteArray &bytes);\n\t~FrameGenerator();\n\n\tint count() override;\n\tdouble rate() override;\n\tFrame renderNext(\n\t\tQImage storage,\n\t\tQSize size,\n\t\tQt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override;\n\tFrame renderCurrent(\n\t\tQImage storage,\n\t\tQSize size,\n\t\tQt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override;\n\tvoid jumpToStart() override;\n\nprivate:\n\tclass Impl;\n\n\tstd::unique_ptr<Impl> _impl;\n\n};\n\n} // namespace FFmpeg\n"
  },
  {
    "path": "Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ffmpeg/ffmpeg_utility.h\"\n\n#include \"base/algorithm.h\"\n#include \"logs.h\"\n\n#if !defined Q_OS_WIN && !defined Q_OS_MAC\n#include \"base/platform/linux/base_linux_library.h\"\n#include <deque>\n#endif // !Q_OS_WIN && !Q_OS_MAC\n\n#include <QImage>\n\n#ifdef LIB_FFMPEG_USE_QT_PRIVATE_API\n#include <private/qdrawhelper_p.h>\n#endif // LIB_FFMPEG_USE_QT_PRIVATE_API\n\nextern \"C\" {\n#include <libavutil/opt.h>\n#include <libavutil/display.h>\n} // extern \"C\"\n\n#if !defined Q_OS_WIN && !defined Q_OS_MAC\nextern \"C\" {\nvoid _libvdpau_so_tramp_resolve_all(void) __attribute__((weak));\nvoid _libva_drm_so_tramp_resolve_all(void) __attribute__((weak));\nvoid _libva_x11_so_tramp_resolve_all(void) __attribute__((weak));\nvoid _libva_so_tramp_resolve_all(void) __attribute__((weak));\nvoid _libdrm_so_tramp_resolve_all(void) __attribute__((weak));\n} // extern \"C\"\n#endif // !Q_OS_WIN && !Q_OS_MAC\n\nnamespace FFmpeg {\nnamespace {\n\n// See https://github.com/telegramdesktop/tdesktop/issues/7225\nconstexpr auto kAlignImageBy = 64;\nconstexpr auto kImageFormat = QImage::Format_ARGB32_Premultiplied;\nconstexpr auto kMaxScaleByAspectRatio = 16;\nconstexpr auto kAvioBlockSize = 4096;\nconstexpr auto kTimeUnknown = std::numeric_limits<crl::time>::min();\nconstexpr auto kDurationMax = crl::time(std::numeric_limits<int>::max());\n\nusing GetFormatMethod = enum AVPixelFormat(*)(\n\tstruct AVCodecContext *s,\n\tconst enum AVPixelFormat *fmt);\n\nstruct HwAccelDescriptor {\n\tGetFormatMethod getFormat = nullptr;\n\tAVPixelFormat format = AV_PIX_FMT_NONE;\n};\n\nvoid AlignedImageBufferCleanupHandler(void* data) {\n\tconst auto buffer = static_cast<uchar*>(data);\n\tdelete[] buffer;\n}\n\n[[nodiscard]] bool IsValidAspectRatio(AVRational aspect) {\n\treturn (aspect.num > 0)\n\t\t&& (aspect.den > 0)\n\t\t&& (aspect.num <= aspect.den * kMaxScaleByAspectRatio)\n\t\t&& (aspect.den <= aspect.num * kMaxScaleByAspectRatio);\n}\n\n[[nodiscard]] bool IsAlignedImage(const QImage &image) {\n\treturn !(reinterpret_cast<uintptr_t>(image.bits()) % kAlignImageBy)\n\t\t&& !(image.bytesPerLine() % kAlignImageBy);\n}\n\nvoid UnPremultiplyLine(uchar *dst, const uchar *src, int intsCount) {\n\t[[maybe_unused]] const auto udst = reinterpret_cast<uint*>(dst);\n\tconst auto usrc = reinterpret_cast<const uint*>(src);\n\n#ifndef LIB_FFMPEG_USE_QT_PRIVATE_API\n\tfor (auto i = 0; i != intsCount; ++i) {\n\t\tudst[i] = qUnpremultiply(usrc[i]);\n\t}\n#else // !LIB_FFMPEG_USE_QT_PRIVATE_API\n\tstatic const auto layout = &qPixelLayouts[QImage::Format_ARGB32];\n\tlayout->storeFromARGB32PM(dst, usrc, 0, intsCount, nullptr, nullptr);\n#endif // LIB_FFMPEG_USE_QT_PRIVATE_API\n}\n\nvoid PremultiplyLine(uchar *dst, const uchar *src, int intsCount) {\n\tconst auto udst = reinterpret_cast<uint*>(dst);\n\t[[maybe_unused]] const auto usrc = reinterpret_cast<const uint*>(src);\n\n#ifndef LIB_FFMPEG_USE_QT_PRIVATE_API\n\tfor (auto i = 0; i != intsCount; ++i) {\n\t\tudst[i] = qPremultiply(usrc[i]);\n\t}\n#else // !LIB_FFMPEG_USE_QT_PRIVATE_API\n\tstatic const auto layout = &qPixelLayouts[QImage::Format_ARGB32];\n\tlayout->fetchToARGB32PM(udst, src, 0, intsCount, nullptr, nullptr);\n#endif // LIB_FFMPEG_USE_QT_PRIVATE_API\n}\n\n#if !defined Q_OS_WIN && !defined Q_OS_MAC\n[[nodiscard]] auto CheckHwLibs() {\n\tauto list = std::deque{\n\t\tAV_PIX_FMT_CUDA,\n\t};\n\tif (!_libvdpau_so_tramp_resolve_all\n\t\t\t|| base::Platform::LoadLibrary(\"libvdpau.so.1\")) {\n\t\tlist.push_front(AV_PIX_FMT_VDPAU);\n\t}\n\tif ([&] {\n\t\tconst auto list = std::array{\n\t\t\tstd::make_pair(_libva_drm_so_tramp_resolve_all, \"libva-drm.so.2\"),\n\t\t\tstd::make_pair(_libva_x11_so_tramp_resolve_all, \"libva-x11.so.2\"),\n\t\t\tstd::make_pair(_libva_so_tramp_resolve_all, \"libva.so.2\"),\n\t\t\tstd::make_pair(_libdrm_so_tramp_resolve_all, \"libdrm.so.2\"),\n\t\t};\n\t\tfor (const auto &lib : list) {\n\t\t\tif (lib.first && !base::Platform::LoadLibrary(lib.second)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}()) {\n\t\tlist.push_front(AV_PIX_FMT_VAAPI);\n\t}\n\treturn list;\n}\n#endif // !Q_OS_WIN && !Q_OS_MAC\n\n[[nodiscard]] bool InitHw(AVCodecContext *context, AVHWDeviceType type) {\n\tAVCodecContext *parent = static_cast<AVCodecContext*>(context->opaque);\n\n\tauto hwDeviceContext = (AVBufferRef*)nullptr;\n\tAvErrorWrap error = av_hwdevice_ctx_create(\n\t\t&hwDeviceContext,\n\t\ttype,\n\t\tnullptr,\n\t\tnullptr,\n\t\t0);\n\tif (error || !hwDeviceContext) {\n\t\tLogError(u\"av_hwdevice_ctx_create\"_q, error);\n\t\treturn false;\n\t}\n\tDEBUG_LOG((\"Video Info: \"\n\t\t\"Trying \\\"%1\\\" hardware acceleration for \\\"%2\\\" decoder.\"\n\t\t).arg(\n\t\t\tav_hwdevice_get_type_name(type),\n\t\t\tcontext->codec->name));\n\tif (parent->hw_device_ctx) {\n\t\tav_buffer_unref(&parent->hw_device_ctx);\n\t}\n\tparent->hw_device_ctx = av_buffer_ref(hwDeviceContext);\n\tav_buffer_unref(&hwDeviceContext);\n\n\tcontext->hw_device_ctx = parent->hw_device_ctx;\n\treturn true;\n}\n\n[[nodiscard]] enum AVPixelFormat GetHwFormat(\n\t\tAVCodecContext *context,\n\t\tconst enum AVPixelFormat *formats) {\n\tconst auto has = [&](enum AVPixelFormat format) {\n\t\tconst enum AVPixelFormat *p = nullptr;\n\t\tfor (p = formats; *p != AV_PIX_FMT_NONE; p++) {\n\t\t\tif (*p == format) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n#if defined Q_OS_WIN || defined Q_OS_MAC\n\tconst auto list = std::array{\n#ifdef Q_OS_WIN\n\t\tAV_PIX_FMT_D3D11,\n\t\tAV_PIX_FMT_DXVA2_VLD,\n\t\tAV_PIX_FMT_CUDA,\n#elif defined Q_OS_MAC // Q_OS_WIN\n\t\tAV_PIX_FMT_VIDEOTOOLBOX,\n#endif // Q_OS_WIN || Q_OS_MAC\n\t};\n#else // Q_OS_WIN || Q_OS_MAC\n\tstatic const auto list = CheckHwLibs();\n#endif // !Q_OS_WIN && !Q_OS_MAC\n\tfor (const auto format : list) {\n\t\tif (!has(format)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto type = [&] {\n\t\t\tswitch (format) {\n#ifdef Q_OS_WIN\n\t\t\tcase AV_PIX_FMT_D3D11: return AV_HWDEVICE_TYPE_D3D11VA;\n\t\t\tcase AV_PIX_FMT_DXVA2_VLD: return AV_HWDEVICE_TYPE_DXVA2;\n\t\t\tcase AV_PIX_FMT_CUDA: return AV_HWDEVICE_TYPE_CUDA;\n#elif defined Q_OS_MAC // Q_OS_WIN\n\t\t\tcase AV_PIX_FMT_VIDEOTOOLBOX:\n\t\t\t\treturn AV_HWDEVICE_TYPE_VIDEOTOOLBOX;\n#else // Q_OS_WIN || Q_OS_MAC\n\t\t\tcase AV_PIX_FMT_VAAPI: return AV_HWDEVICE_TYPE_VAAPI;\n\t\t\tcase AV_PIX_FMT_VDPAU: return AV_HWDEVICE_TYPE_VDPAU;\n\t\t\tcase AV_PIX_FMT_CUDA: return AV_HWDEVICE_TYPE_CUDA;\n#endif // Q_OS_WIN || Q_OS_MAC\n\t\t\t}\n\t\t\treturn AV_HWDEVICE_TYPE_NONE;\n\t\t}();\n\t\tif (type == AV_HWDEVICE_TYPE_NONE && context->hw_device_ctx) {\n\t\t\tav_buffer_unref(&context->hw_device_ctx);\n\t\t} else if (type != AV_HWDEVICE_TYPE_NONE && !InitHw(context, type)) {\n\t\t\tcontinue;\n\t\t}\n\t\treturn format;\n\t}\n\tenum AVPixelFormat result = AV_PIX_FMT_NONE;\n\tfor (const enum AVPixelFormat *p = formats; *p != AV_PIX_FMT_NONE; p++) {\n\t\tresult = *p;\n\t}\n\treturn result;\n}\n\ntemplate <AVPixelFormat Required>\nenum AVPixelFormat GetFormatImplementation(\n\t\tAVCodecContext *ctx,\n\t\tconst enum AVPixelFormat *pix_fmts) {\n\tconst enum AVPixelFormat *p = nullptr;\n\tfor (p = pix_fmts; *p != -1; p++) {\n\t\tif (*p == Required) {\n\t\t\treturn *p;\n\t\t}\n\t}\n\treturn AV_PIX_FMT_NONE;\n}\n\n} // namespace\n\nIOPointer MakeIOPointer(\n\t\tvoid *opaque,\n\t\tint(*read)(void *opaque, uint8_t *buffer, int bufferSize),\n#if DA_FFMPEG_CONST_WRITE_CALLBACK\n\t\tint(*write)(void *opaque, const uint8_t *buffer, int bufferSize),\n#else\n\t\tint(*write)(void *opaque, uint8_t *buffer, int bufferSize),\n#endif\n\t\tint64_t(*seek)(void *opaque, int64_t offset, int whence)) {\n\tauto buffer = reinterpret_cast<uchar*>(av_malloc(kAvioBlockSize));\n\tif (!buffer) {\n\t\tLogError(u\"av_malloc\"_q);\n\t\treturn {};\n\t}\n\tauto result = IOPointer(avio_alloc_context(\n\t\tbuffer,\n\t\tkAvioBlockSize,\n\t\twrite ? 1 : 0,\n\t\topaque,\n\t\tread,\n\t\twrite,\n\t\tseek));\n\tif (!result) {\n\t\tav_freep(&buffer);\n\t\tLogError(u\"avio_alloc_context\"_q);\n\t\treturn {};\n\t}\n\treturn result;\n}\n\nvoid IODeleter::operator()(AVIOContext *value) {\n\tif (value) {\n\t\tav_freep(&value->buffer);\n\t\tavio_context_free(&value);\n\t}\n}\n\nFormatPointer MakeFormatPointer(\n\t\tvoid *opaque,\n\t\tint(*read)(void *opaque, uint8_t *buffer, int bufferSize),\n#if DA_FFMPEG_CONST_WRITE_CALLBACK\n\t\tint(*write)(void *opaque, const uint8_t *buffer, int bufferSize),\n#else\n\t\tint(*write)(void *opaque, uint8_t *buffer, int bufferSize),\n#endif\n\t\tint64_t(*seek)(void *opaque, int64_t offset, int whence)) {\n\tauto io = MakeIOPointer(opaque, read, write, seek);\n\tif (!io) {\n\t\treturn {};\n\t}\n\tio->seekable = (seek != nullptr);\n\tauto result = avformat_alloc_context();\n\tif (!result) {\n\t\tLogError(u\"avformat_alloc_context\"_q);\n\t\treturn {};\n\t}\n\tresult->pb = io.get();\n\tresult->flags |= AVFMT_FLAG_CUSTOM_IO;\n\n\tauto options = (AVDictionary*)nullptr;\n\tconst auto guard = gsl::finally([&] { av_dict_free(&options); });\n\tav_dict_set(&options, \"usetoc\", \"1\", 0);\n\n\tconst auto error = AvErrorWrap(avformat_open_input(\n\t\t&result,\n\t\tnullptr,\n\t\tnullptr,\n\t\t&options));\n\tif (error) {\n\t\t// avformat_open_input freed 'result' in case an error happened.\n\t\tLogError(u\"avformat_open_input\"_q, error);\n\t\treturn {};\n\t}\n\tif (seek) {\n\t\tresult->flags |= AVFMT_FLAG_FAST_SEEK;\n\t}\n\n\t// Now FormatPointer will own and free the IO context.\n\tio.release();\n\treturn FormatPointer(result);\n}\n\nFormatPointer MakeWriteFormatPointer(\n\t\tvoid *opaque,\n\t\tint(*read)(void *opaque, uint8_t *buffer, int bufferSize),\n#if DA_FFMPEG_CONST_WRITE_CALLBACK\n\t\tint(*write)(void *opaque, const uint8_t *buffer, int bufferSize),\n#else\n\t\tint(*write)(void *opaque, uint8_t *buffer, int bufferSize),\n#endif\n\t\tint64_t(*seek)(void *opaque, int64_t offset, int whence),\n\t\tconst QByteArray &format) {\n\tconst AVOutputFormat *found = nullptr;\n\tvoid *i = nullptr;\n\twhile ((found = av_muxer_iterate(&i))) {\n\t\tif (found->name == format) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (!found) {\n\t\tLogError(\n\t\t\t\"av_muxer_iterate\",\n\t\t\tu\"Format %1 not found\"_q.arg(QString::fromUtf8(format)));\n\t\treturn {};\n\t}\n\n\tauto io = MakeIOPointer(opaque, read, write, seek);\n\tif (!io) {\n\t\treturn {};\n\t}\n\tio->seekable = (seek != nullptr);\n\n\tauto result = (AVFormatContext*)nullptr;\n\tauto error = AvErrorWrap(avformat_alloc_output_context2(\n\t\t&result,\n\t\t(AVOutputFormat*)found,\n\t\tnullptr,\n\t\tnullptr));\n\tif (!result || error) {\n\t\tLogError(\"avformat_alloc_output_context2\", error);\n\t\treturn {};\n\t}\n\tresult->pb = io.get();\n\tresult->flags |= AVFMT_FLAG_CUSTOM_IO;\n\n\t// Now FormatPointer will own and free the IO context.\n\tio.release();\n\treturn FormatPointer(result);\n}\n\nvoid FormatDeleter::operator()(AVFormatContext *value) {\n\tif (value) {\n\t\tconst auto deleter = IOPointer(value->pb);\n\t\tavformat_close_input(&value);\n\t}\n}\n\nconst AVCodec *FindDecoder(not_null<AVCodecContext*> context) {\n\t// Force libvpx-vp9, because we need alpha channel support.\n\treturn (context->codec_id == AV_CODEC_ID_VP9)\n\t\t? avcodec_find_decoder_by_name(\"libvpx-vp9\")\n\t\t: avcodec_find_decoder(context->codec_id);\n}\n\nCodecPointer MakeCodecPointer(CodecDescriptor descriptor) {\n\tauto error = AvErrorWrap();\n\n\tauto result = CodecPointer(avcodec_alloc_context3(nullptr));\n\tconst auto context = result.get();\n\tif (!context) {\n\t\tLogError(u\"avcodec_alloc_context3\"_q);\n\t\treturn {};\n\t}\n\tconst auto stream = descriptor.stream;\n\terror = avcodec_parameters_to_context(context, stream->codecpar);\n\tif (error) {\n\t\tLogError(u\"avcodec_parameters_to_context\"_q, error);\n\t\treturn {};\n\t}\n\tcontext->pkt_timebase = stream->time_base;\n\tav_opt_set(context, \"threads\", \"auto\", 0);\n\tav_opt_set_int(context, \"refcounted_frames\", 1, 0);\n\n\tconst auto codec = FindDecoder(context);\n\tif (!codec) {\n\t\tLogError(u\"avcodec_find_decoder\"_q, context->codec_id);\n\t\treturn {};\n\t}\n\n\tif (descriptor.hwAllowed) {\n\t\tcontext->get_format = GetHwFormat;\n\t\tcontext->opaque = context;\n\t} else {\n\t\tDEBUG_LOG((\"Video Info: Using software \\\"%2\\\" decoder.\"\n\t\t\t).arg(codec->name));\n\t}\n\n\tif ((error = avcodec_open2(context, codec, nullptr))) {\n\t\tLogError(u\"avcodec_open2\"_q, error);\n\t\treturn {};\n\t}\n\treturn result;\n}\n\nvoid CodecDeleter::operator()(AVCodecContext *value) {\n\tif (value) {\n\t\tavcodec_free_context(&value);\n\t}\n}\n\nFramePointer MakeFramePointer() {\n\treturn FramePointer(av_frame_alloc());\n}\n\nFramePointer DuplicateFramePointer(AVFrame *frame) {\n\treturn frame\n\t\t? FramePointer(av_frame_clone(frame))\n\t\t: FramePointer();\n}\n\nbool FrameHasData(AVFrame *frame) {\n\treturn (frame && frame->data[0] != nullptr);\n}\n\nvoid ClearFrameMemory(AVFrame *frame) {\n\tif (FrameHasData(frame)) {\n\t\tav_frame_unref(frame);\n\t}\n}\n\nvoid FrameDeleter::operator()(AVFrame *value) {\n\tav_frame_free(&value);\n}\n\nSwscalePointer MakeSwscalePointer(\n\t\tQSize srcSize,\n\t\tint srcFormat,\n\t\tQSize dstSize,\n\t\tint dstFormat,\n\t\tSwscalePointer *existing) {\n\t// We have to use custom caching for SwsContext, because\n\t// sws_getCachedContext checks passed flags with existing context flags,\n\t// and re-creates context if they're different, but in the process of\n\t// context creation the passed flags are modified before being written\n\t// to the resulting context, so the caching doesn't work.\n\tif (existing && (*existing) != nullptr) {\n\t\tconst auto &deleter = existing->get_deleter();\n\t\tif (deleter.srcSize == srcSize\n\t\t\t&& deleter.srcFormat == srcFormat\n\t\t\t&& deleter.dstSize == dstSize\n\t\t\t&& deleter.dstFormat == dstFormat) {\n\t\t\treturn std::move(*existing);\n\t\t}\n\t}\n\tif (srcFormat <= AV_PIX_FMT_NONE || srcFormat >= AV_PIX_FMT_NB) {\n\t\tLogError(u\"frame->format\"_q);\n\t\treturn SwscalePointer();\n\t}\n\n\tconst auto result = sws_getCachedContext(\n\t\texisting ? existing->release() : nullptr,\n\t\tsrcSize.width(),\n\t\tsrcSize.height(),\n\t\tAVPixelFormat(srcFormat),\n\t\tdstSize.width(),\n\t\tdstSize.height(),\n\t\tAVPixelFormat(dstFormat),\n\t\t0,\n\t\tnullptr,\n\t\tnullptr,\n\t\tnullptr);\n\tif (!result) {\n\t\tLogError(u\"sws_getCachedContext\"_q);\n\t}\n\treturn SwscalePointer(\n\t\tresult,\n\t\t{ srcSize, srcFormat, dstSize, dstFormat });\n}\n\nSwscalePointer MakeSwscalePointer(\n\t\tnot_null<AVFrame*> frame,\n\t\tQSize resize,\n\t\tSwscalePointer *existing) {\n\treturn MakeSwscalePointer(\n\t\tQSize(frame->width, frame->height),\n\t\tframe->format,\n\t\tresize,\n\t\tAV_PIX_FMT_BGRA,\n\t\texisting);\n}\n\nvoid SwresampleDeleter::operator()(SwrContext *value) {\n\tif (value) {\n\t\tswr_free(&value);\n\t}\n}\n\nSwresamplePointer MakeSwresamplePointer(\n\t\tAVChannelLayout *srcLayout,\n\t\tAVSampleFormat srcFormat,\n\t\tint srcRate,\n\t\tAVChannelLayout *dstLayout,\n\t\tAVSampleFormat dstFormat,\n\t\tint dstRate,\n\t\tSwresamplePointer *existing) {\n\t// We have to use custom caching for SwsContext, because\n\t// sws_getCachedContext checks passed flags with existing context flags,\n\t// and re-creates context if they're different, but in the process of\n\t// context creation the passed flags are modified before being written\n\t// to the resulting context, so the caching doesn't work.\n\tif (existing && (*existing) != nullptr) {\n\t\tconst auto &deleter = existing->get_deleter();\n\t\tif (srcLayout->nb_channels == deleter.srcChannels\n\t\t\t&& dstLayout->nb_channels == deleter.dstChannels\n\t\t\t&& srcFormat == deleter.srcFormat\n\t\t\t&& dstFormat == deleter.dstFormat\n\t\t\t&& srcRate == deleter.srcRate\n\t\t\t&& dstRate == deleter.dstRate) {\n\t\t\treturn std::move(*existing);\n\t\t}\n\t}\n\n\t// Initialize audio resampler\n\tauto result = (SwrContext*)nullptr;\n\tauto error = AvErrorWrap(swr_alloc_set_opts2(\n\t\t&result,\n\t\tdstLayout,\n\t\tdstFormat,\n\t\tdstRate,\n\t\tsrcLayout,\n\t\tsrcFormat,\n\t\tsrcRate,\n\t\t0,\n\t\tnullptr));\n\tif (error || !result) {\n\t\tLogError(u\"swr_alloc_set_opts2\"_q, error);\n\t\treturn SwresamplePointer();\n\t}\n\n\terror = AvErrorWrap(swr_init(result));\n\tif (error) {\n\t\tLogError(u\"swr_init\"_q, error);\n\t\tswr_free(&result);\n\t\treturn SwresamplePointer();\n\t}\n\n\treturn SwresamplePointer(\n\t\tresult,\n\t\t{\n\t\t\tsrcFormat,\n\t\t\tsrcRate,\n\t\t\tsrcLayout->nb_channels,\n\t\t\tdstFormat,\n\t\t\tdstRate,\n\t\t\tdstLayout->nb_channels,\n\t\t});\n}\n\nvoid SwscaleDeleter::operator()(SwsContext *value) {\n\tif (value) {\n\t\tsws_freeContext(value);\n\t}\n}\n\nvoid LogError(const QString &method, const QString &details) {\n\tLOG((\"Streaming Error: Error in %1%2.\"\n\t\t).arg(method\n\t\t).arg(details.isEmpty() ? QString() : \" - \" + details));\n}\n\nvoid LogError(\n\t\tconst QString &method,\n\t\tAvErrorWrap error,\n\t\tconst QString &details) {\n\tLOG((\"Streaming Error: Error in %1 (code: %2, text: %3)%4.\"\n\t\t).arg(method\n\t\t).arg(error.code()\n\t\t).arg(error.text()\n\t\t).arg(details.isEmpty() ? QString() : \" - \" + details));\n}\n\ncrl::time PtsToTime(int64_t pts, AVRational timeBase) {\n\treturn (pts == AV_NOPTS_VALUE || !timeBase.den)\n\t\t? kTimeUnknown\n\t\t: ((pts * 1000LL * timeBase.num) / timeBase.den);\n}\n\ncrl::time PtsToTimeCeil(int64_t pts, AVRational timeBase) {\n\treturn (pts == AV_NOPTS_VALUE || !timeBase.den)\n\t\t? kTimeUnknown\n\t\t: ((pts * 1000LL * timeBase.num + timeBase.den - 1) / timeBase.den);\n}\n\nint64_t TimeToPts(crl::time time, AVRational timeBase) {\n\treturn (time == kTimeUnknown || !timeBase.num)\n\t\t? AV_NOPTS_VALUE\n\t\t: (time * timeBase.den) / (1000LL * timeBase.num);\n}\n\ncrl::time PacketPosition(const Packet &packet, AVRational timeBase) {\n\tconst auto &native = packet.fields();\n\treturn PtsToTime(\n\t\t(native.pts == AV_NOPTS_VALUE) ? native.dts : native.pts,\n\t\ttimeBase);\n}\n\ncrl::time PacketDuration(const Packet &packet, AVRational timeBase) {\n\treturn PtsToTime(packet.fields().duration, timeBase);\n}\n\nint DurationByPacket(const Packet &packet, AVRational timeBase) {\n\tconst auto position = PacketPosition(packet, timeBase);\n\tconst auto duration = std::max(\n\t\tPacketDuration(packet, timeBase),\n\t\tcrl::time(1));\n\tconst auto bad = [](crl::time time) {\n\t\treturn (time < 0) || (time > kDurationMax);\n\t};\n\tif (bad(position) || bad(duration) || bad(position + duration + 1)) {\n\t\tLOG((\"Streaming Error: Wrong duration by packet: %1 + %2\"\n\t\t\t).arg(position\n\t\t\t).arg(duration));\n\t\treturn -1;\n\t}\n\treturn int(position + duration + 1);\n}\n\nint ReadRotationFromMetadata(not_null<AVStream*> stream) {\n\tconst auto displaymatrix = av_packet_side_data_get(\n\t\tstream->codecpar->coded_side_data,\n\t\tstream->codecpar->nb_coded_side_data,\n\t\tAV_PKT_DATA_DISPLAYMATRIX);\n\tauto theta = 0;\n\tif (displaymatrix) {\n\t\tconst auto matrix = (int32_t*)displaymatrix->data;\n\t\ttheta = -round(av_display_rotation_get(matrix));\n\t}\n\ttheta -= 360 * floor(theta / 360 + 0.9 / 360);\n\tconst auto result = int(base::SafeRound(theta));\n\treturn (result == 90 || result == 180 || result == 270) ? result : 0;\n}\n\nAVRational ValidateAspectRatio(AVRational aspect) {\n\treturn IsValidAspectRatio(aspect) ? aspect : kNormalAspect;\n}\n\nQSize CorrectByAspect(QSize size, AVRational aspect) {\n\tExpects(IsValidAspectRatio(aspect));\n\n\treturn QSize(size.width() * av_q2d(aspect), size.height());\n}\n\nbool RotationSwapWidthHeight(int rotation) {\n\treturn (rotation == 90 || rotation == 270);\n}\n\nQSize TransposeSizeByRotation(QSize size, int rotation) {\n\treturn RotationSwapWidthHeight(rotation) ? size.transposed() : size;\n}\n\nbool GoodStorageForFrame(const QImage &storage, QSize size) {\n\treturn !storage.isNull()\n\t\t&& (storage.format() == kImageFormat)\n\t\t&& (storage.size() == size)\n\t\t&& storage.isDetached()\n\t\t&& IsAlignedImage(storage);\n}\n\n// Create a QImage of desired size where all the data is properly aligned.\nQImage CreateFrameStorage(QSize size) {\n\tconst auto width = size.width();\n\tconst auto height = size.height();\n\tconst auto widthAlign = kAlignImageBy / kPixelBytesSize;\n\tconst auto neededWidth = width + ((width % widthAlign)\n\t\t? (widthAlign - (width % widthAlign))\n\t\t: 0);\n\tconst auto perLine = neededWidth * kPixelBytesSize;\n\tconst auto buffer = new uchar[size_t(perLine) * height + kAlignImageBy];\n\tconst auto cleanupData = static_cast<void *>(buffer);\n\tconst auto address = reinterpret_cast<uintptr_t>(buffer);\n\tconst auto alignedBuffer = buffer + ((address % kAlignImageBy)\n\t\t? (kAlignImageBy - (address % kAlignImageBy))\n\t\t: 0);\n\treturn QImage(\n\t\talignedBuffer,\n\t\twidth,\n\t\theight,\n\t\tperLine,\n\t\tkImageFormat,\n\t\tAlignedImageBufferCleanupHandler,\n\t\tcleanupData);\n}\n\nvoid UnPremultiply(QImage &dst, const QImage &src) {\n\t// This creates QImage::Format_ARGB32_Premultiplied, but we use it\n\t// as an image in QImage::Format_ARGB32 format.\n\tif (!GoodStorageForFrame(dst, src.size())) {\n\t\tdst = CreateFrameStorage(src.size());\n\t}\n\tconst auto srcPerLine = src.bytesPerLine();\n\tconst auto dstPerLine = dst.bytesPerLine();\n\tconst auto width = src.width();\n\tconst auto height = src.height();\n\tauto srcBytes = src.bits();\n\tauto dstBytes = dst.bits();\n\tif (srcPerLine != width * 4 || dstPerLine != width * 4) {\n\t\tfor (auto i = 0; i != height; ++i) {\n\t\t\tUnPremultiplyLine(dstBytes, srcBytes, width);\n\t\t\tsrcBytes += srcPerLine;\n\t\t\tdstBytes += dstPerLine;\n\t\t}\n\t} else {\n\t\tUnPremultiplyLine(dstBytes, srcBytes, width * height);\n\t}\n}\n\nvoid PremultiplyInplace(QImage &image) {\n\tconst auto perLine = image.bytesPerLine();\n\tconst auto width = image.width();\n\tconst auto height = image.height();\n\tauto bytes = image.bits();\n\tif (perLine != width * 4) {\n\t\tfor (auto i = 0; i != height; ++i) {\n\t\t\tPremultiplyLine(bytes, bytes, width);\n\t\t\tbytes += perLine;\n\t\t}\n\t} else {\n\t\tPremultiplyLine(bytes, bytes, width * height);\n\t}\n}\n\n} // namespace FFmpeg\n"
  },
  {
    "path": "Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/bytes.h\"\n#include \"base/algorithm.h\"\n\n#include <crl/crl_time.h>\n\n#include <QSize>\n#include <QString>\n\nextern \"C\" {\n#include <libavcodec/avcodec.h>\n#include <libavformat/avformat.h>\n#include <libswscale/swscale.h>\n#include <libswresample/swresample.h>\n#include <libavutil/opt.h>\n#include <libavutil/version.h>\n} // extern \"C\"\n\n#define DA_FFMPEG_CONST_WRITE_CALLBACK (LIBAVFORMAT_VERSION_INT >= \\\n\tAV_VERSION_INT(61, 01, 100))\n\nclass QImage;\n\nnamespace FFmpeg {\n\ninline constexpr auto kPixelBytesSize = 4;\ninline constexpr auto kAVBlockSize = 4096; // 4Kb for ffmpeg blocksize\n\nconstexpr auto kUniversalTimeBase = AVRational{ 1, AV_TIME_BASE };\nconstexpr auto kNormalAspect = AVRational{ 1, 1 };\n\nclass AvErrorWrap {\npublic:\n\tAvErrorWrap(int code = 0) : _code(code) {\n\t}\n\n\t[[nodiscard]] bool failed() const {\n\t\treturn (_code < 0);\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn failed();\n\t}\n\n\t[[nodiscard]] int code() const {\n\t\treturn _code;\n\t}\n\n\t[[nodiscard]] QString text() const {\n\t\tchar string[AV_ERROR_MAX_STRING_SIZE] = { 0 };\n\t\treturn QString::fromUtf8(av_make_error_string(\n\t\t\tstring,\n\t\t\tsizeof(string),\n\t\t\t_code));\n\t}\n\nprivate:\n\tint _code = 0;\n\n};\n\nclass Packet {\npublic:\n\tPacket() = default;\n\tPacket(Packet &&other) : _data(base::take(other._data)) {\n\t}\n\tPacket &operator=(Packet &&other) {\n\t\tif (this != &other) {\n\t\t\trelease();\n\t\t\t_data = base::take(other._data);\n\t\t}\n\t\treturn *this;\n\t}\n\t~Packet() {\n\t\trelease();\n\t}\n\n\t[[nodiscard]] AVPacket &fields() {\n\t\tif (!_data) {\n\t\t\t_data = av_packet_alloc();\n\t\t}\n\t\treturn *_data;\n\t}\n\t[[nodiscard]] const AVPacket &fields() const {\n\t\tif (!_data) {\n\t\t\t_data = av_packet_alloc();\n\t\t}\n\t\treturn *_data;\n\t}\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn !_data || !fields().data;\n\t}\n\tvoid release() {\n\t\tav_packet_free(&_data);\n\t}\n\nprivate:\n\tmutable AVPacket *_data = nullptr;\n\n};\n\nstruct IODeleter {\n\tvoid operator()(AVIOContext *value);\n};\nusing IOPointer = std::unique_ptr<AVIOContext, IODeleter>;\n[[nodiscard]] IOPointer MakeIOPointer(\n\tvoid *opaque,\n\tint(*read)(void *opaque, uint8_t *buffer, int bufferSize),\n#if DA_FFMPEG_CONST_WRITE_CALLBACK\n\tint(*write)(void *opaque, const uint8_t *buffer, int bufferSize),\n#else\n\tint(*write)(void *opaque, uint8_t *buffer, int bufferSize),\n#endif\n\tint64_t(*seek)(void *opaque, int64_t offset, int whence));\n\nstruct FormatDeleter {\n\tvoid operator()(AVFormatContext *value);\n};\nusing FormatPointer = std::unique_ptr<AVFormatContext, FormatDeleter>;\n[[nodiscard]] FormatPointer MakeFormatPointer(\n\tvoid *opaque,\n\tint(*read)(void *opaque, uint8_t *buffer, int bufferSize),\n#if DA_FFMPEG_CONST_WRITE_CALLBACK\n\tint(*write)(void *opaque, const uint8_t *buffer, int bufferSize),\n#else\n\tint(*write)(void *opaque, uint8_t *buffer, int bufferSize),\n#endif\n\tint64_t(*seek)(void *opaque, int64_t offset, int whence));\n[[nodiscard]] FormatPointer MakeWriteFormatPointer(\n\tvoid *opaque,\n\tint(*read)(void *opaque, uint8_t *buffer, int bufferSize),\n#if DA_FFMPEG_CONST_WRITE_CALLBACK\n\tint(*write)(void *opaque, const uint8_t *buffer, int bufferSize),\n#else\n\tint(*write)(void *opaque, uint8_t *buffer, int bufferSize),\n#endif\n\tint64_t(*seek)(void *opaque, int64_t offset, int whence),\n\tconst QByteArray &format);\n\nstruct CodecDeleter {\n\tvoid operator()(AVCodecContext *value);\n};\nusing CodecPointer = std::unique_ptr<AVCodecContext, CodecDeleter>;\n\nstruct CodecDescriptor {\n\tnot_null<AVStream*> stream;\n\tbool hwAllowed = false;\n};\n[[nodiscard]] CodecPointer MakeCodecPointer(CodecDescriptor descriptor);\n\nstruct FrameDeleter {\n\tvoid operator()(AVFrame *value);\n};\nusing FramePointer = std::unique_ptr<AVFrame, FrameDeleter>;\n[[nodiscard]] FramePointer MakeFramePointer();\n[[nodiscard]] FramePointer DuplicateFramePointer(AVFrame *frame);\n[[nodiscard]] bool FrameHasData(AVFrame *frame);\nvoid ClearFrameMemory(AVFrame *frame);\n\nstruct SwscaleDeleter {\n\tQSize srcSize;\n\tint srcFormat = int(AV_PIX_FMT_NONE);\n\tQSize dstSize;\n\tint dstFormat = int(AV_PIX_FMT_NONE);\n\n\tvoid operator()(SwsContext *value);\n};\nusing SwscalePointer = std::unique_ptr<SwsContext, SwscaleDeleter>;\n[[nodiscard]] SwscalePointer MakeSwscalePointer(\n\tQSize srcSize,\n\tint srcFormat,\n\tQSize dstSize,\n\tint dstFormat, // This field doesn't take part in caching!\n\tSwscalePointer *existing = nullptr);\n[[nodiscard]] SwscalePointer MakeSwscalePointer(\n\tnot_null<AVFrame*> frame,\n\tQSize resize,\n\tSwscalePointer *existing = nullptr);\n\nstruct SwresampleDeleter {\n\tAVSampleFormat srcFormat = AV_SAMPLE_FMT_NONE;\n\tint srcRate = 0;\n\tint srcChannels = 0;\n\tAVSampleFormat dstFormat = AV_SAMPLE_FMT_NONE;\n\tint dstRate = 0;\n\tint dstChannels = 0;\n\n\tvoid operator()(SwrContext *value);\n};\nusing SwresamplePointer = std::unique_ptr<SwrContext, SwresampleDeleter>;\n[[nodiscard]] SwresamplePointer MakeSwresamplePointer(\n\tAVChannelLayout *srcLayout,\n\tAVSampleFormat srcFormat,\n\tint srcRate,\n\tAVChannelLayout *dstLayout,\n\tAVSampleFormat dstFormat,\n\tint dstRate,\n\tSwresamplePointer *existing = nullptr);\n\nvoid LogError(const QString &method, const QString &details = {});\nvoid LogError(\n\tconst QString &method,\n\tFFmpeg::AvErrorWrap error,\n\tconst QString &details = {});\n\n[[nodiscard]] const AVCodec *FindDecoder(not_null<AVCodecContext*> context);\n[[nodiscard]] crl::time PtsToTime(int64_t pts, AVRational timeBase);\n// Used for full duration conversion.\n[[nodiscard]] crl::time PtsToTimeCeil(int64_t pts, AVRational timeBase);\n[[nodiscard]] int64_t TimeToPts(crl::time time, AVRational timeBase);\n[[nodiscard]] crl::time PacketPosition(\n\tconst FFmpeg::Packet &packet,\n\tAVRational timeBase);\n[[nodiscard]] crl::time PacketDuration(\n\tconst FFmpeg::Packet &packet,\n\tAVRational timeBase);\n[[nodiscard]] int DurationByPacket(\n\tconst FFmpeg::Packet &packet,\n\tAVRational timeBase);\n[[nodiscard]] int ReadRotationFromMetadata(not_null<AVStream*> stream);\n[[nodiscard]] AVRational ValidateAspectRatio(AVRational aspect);\n[[nodiscard]] bool RotationSwapWidthHeight(int rotation);\n[[nodiscard]] QSize TransposeSizeByRotation(QSize size, int rotation);\n[[nodiscard]] QSize CorrectByAspect(QSize size, AVRational aspect);\n\n[[nodiscard]] bool GoodStorageForFrame(const QImage &storage, QSize size);\n[[nodiscard]] QImage CreateFrameStorage(QSize size);\n\nvoid UnPremultiply(QImage &to, const QImage &from);\nvoid PremultiplyInplace(QImage &image);\n\n} // namespace FFmpeg\n"
  },
  {
    "path": "Telegram/SourceFiles/forkgram/gpu_demo_renderer.cpp",
    "content": "/*\nThis file is part of Forkgram Desktop,\nthe unofficial Telegram desktop client.\n\nFor license and copyright information please follow this link:\nhttps://github.com/nicegram/nicegram-desktop/blob/master/LEGAL\n*/\n#include \"forkgram/gpu_demo_renderer.h\"\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\n#include \"ui/rhi/rhi_shader.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_basic.h\"\n\n#include <rhi/qrhi.h>\n#include <QTimer>\n\nnamespace Forkgram {\nnamespace {\n\nstruct DemoUniforms {\n\tfloat viewport[2];\n\tfloat time;\n\tfloat _pad;\n};\nstatic_assert(sizeof(DemoUniforms) % 16 == 0);\n\n[[nodiscard]] QShader LoadShader(const QString &name) {\n\treturn Ui::Rhi::ShaderFromFile(\n\t\tu\":/shaders/\"_q + name + u\".qsb\"_q);\n}\n\n} // namespace\n\nGpuDemoRenderer::GpuDemoRenderer() {\n\t_elapsed.start();\n}\n\nGpuDemoRenderer::~GpuDemoRenderer() {\n\treleaseResources();\n}\n\nvoid GpuDemoRenderer::initialize(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\tif (_initialized && _rhi == rhi) {\n\t\treturn;\n\t}\n\treleaseResources();\n\n\t_rhi = rhi;\n\n\tconstexpr auto kVertexSize = 4 * sizeof(float);\n\tconstexpr auto kQuadVertices = 4;\n\n\t_vertexBuffer = rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::VertexBuffer,\n\t\tkQuadVertices * kVertexSize);\n\t_vertexBuffer->create();\n\n\t_uniformBuffer = rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::UniformBuffer,\n\t\tsizeof(DemoUniforms));\n\t_uniformBuffer->create();\n\n\t_srb = rhi->newShaderResourceBindings();\n\t_srb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer),\n\t});\n\t_srb->create();\n\n\tconst auto rpDesc = rt->renderPassDescriptor();\n\tconst auto vertShader = LoadShader(u\"demo.vert\"_q);\n\tconst auto fragShader = LoadShader(u\"demo.frag\"_q);\n\n\t_pipeline = rhi->newGraphicsPipeline();\n\t_pipeline->setShaderStages({\n\t\t{ QRhiShaderStage::Vertex, vertShader },\n\t\t{ QRhiShaderStage::Fragment, fragShader },\n\t});\n\n\tQRhiVertexInputLayout inputLayout;\n\tinputLayout.setBindings({\n\t\t{ quint32(kVertexSize) },\n\t});\n\tinputLayout.setAttributes({\n\t\t{ 0, 0, QRhiVertexInputAttribute::Float2, 0 },\n\t\t{ 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) },\n\t});\n\t_pipeline->setVertexInputLayout(inputLayout);\n\t_pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);\n\t_pipeline->setShaderResourceBindings(_srb);\n\t_pipeline->setRenderPassDescriptor(rpDesc);\n\t_pipeline->create();\n\n\t_initialized = true;\n\n\tLOG((\"[GPU_DEMO] backend=%1 device=%2\")\n\t\t.arg(rhi->backendName())\n\t\t.arg(rhi->driverInfo().deviceName));\n}\n\nvoid GpuDemoRenderer::render(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\t_rhi = rhi;\n\n\tconst auto size = rt->pixelSize();\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto w = float(size.width() / factor);\n\tconst auto h = float(size.height() / factor);\n\tconst auto t = float(_elapsed.elapsed()) / 1000.0f;\n\n\tconst float vertices[] = {\n\t\t0.f, 0.f,   0.f, 0.f,\n\t\tw,   0.f,   1.f, 0.f,\n\t\t0.f, h,     0.f, 1.f,\n\t\tw,   h,     1.f, 1.f,\n\t};\n\n\tDemoUniforms uniforms;\n\tuniforms.viewport[0] = w;\n\tuniforms.viewport[1] = h;\n\tuniforms.time = t;\n\tuniforms._pad = 0.f;\n\n\tauto *rub = rhi->nextResourceUpdateBatch();\n\trub->updateDynamicBuffer(_vertexBuffer, 0, sizeof(vertices), vertices);\n\trub->updateDynamicBuffer(\n\t\t_uniformBuffer, 0, sizeof(uniforms), &uniforms);\n\n\tconst auto bg = QColor(0, 0, 0, 255);\n\tcb->beginPass(rt, bg, { 1.0f, 0 }, rub);\n\n\tcb->setGraphicsPipeline(_pipeline);\n\tcb->setShaderResources(_srb);\n\tcb->setViewport({\n\t\t0, 0,\n\t\tfloat(size.width()),\n\t\tfloat(size.height()) });\n\n\tconst QRhiCommandBuffer::VertexInput vbufBinding(_vertexBuffer, 0);\n\tcb->setVertexInput(0, 1, &vbufBinding);\n\tcb->draw(4);\n\n\tcb->endPass();\n}\n\nvoid GpuDemoRenderer::releaseResources() {\n\tdelete _pipeline;\n\t_pipeline = nullptr;\n\tdelete _srb;\n\t_srb = nullptr;\n\tdelete _uniformBuffer;\n\t_uniformBuffer = nullptr;\n\tdelete _vertexBuffer;\n\t_vertexBuffer = nullptr;\n\t_initialized = false;\n}\n\nvoid GpuDemoRenderer::paintFallback(\n\t\tPainter &p,\n\t\tconst QRegion &clip,\n\t\tUi::GL::Backend backend) {\n\tp.fillRect(clip.boundingRect(), QColor(40, 40, 60));\n\tp.setPen(Qt::white);\n\tp.drawText(\n\t\tclip.boundingRect(),\n\t\tQt::AlignCenter,\n\t\tu\"GPU Demo (QRhi not available)\"_q);\n}\n\nUi::GL::ChosenRenderer ChooseDemoRenderer() {\n\treturn {\n\t\t.renderer = std::make_unique<GpuDemoRenderer>(),\n\t\t.backend = Ui::GL::Backend::QRhi,\n\t};\n}\n\nstd::unique_ptr<Ui::RpWidgetWrap> CreateGpuDemoWidget(\n\t\tQWidget *parent) {\n\tauto surface = Ui::GL::CreateSurface(parent, ChooseDemoRenderer());\n\tif (const auto widget = surface->rpWidget()) {\n\t\tconst auto raw = widget;\n\t\tauto *timer = new QTimer(raw);\n\t\ttimer->setInterval(16);\n\t\tQObject::connect(timer, &QTimer::timeout, raw, [raw] {\n\t\t\traw->update();\n\t\t});\n\t\ttimer->start();\n\t}\n\treturn surface;\n}\n\n} // namespace Forkgram\n\n#endif // Qt >= 6.7\n"
  },
  {
    "path": "Telegram/SourceFiles/forkgram/gpu_demo_renderer.h",
    "content": "/*\nThis file is part of Forkgram Desktop,\nthe unofficial Telegram desktop client.\n\nFor license and copyright information please follow this link:\nhttps://github.com/nicegram/nicegram-desktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rhi/rhi_renderer.h\"\n#include \"ui/gl/gl_surface.h\"\n\n#include <QElapsedTimer>\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\nclass QRhi;\nclass QRhiBuffer;\nclass QRhiSampler;\nclass QRhiGraphicsPipeline;\nclass QRhiShaderResourceBindings;\n\nnamespace Ui {\nclass RpWidgetWrap;\nclass RpWidget;\n} // namespace Ui\n\nnamespace Forkgram {\n\nclass GpuDemoRenderer final\n\t: public Ui::GL::Renderer\n\t, public Ui::Rhi::Renderer {\npublic:\n\tGpuDemoRenderer();\n\t~GpuDemoRenderer();\n\n\tvoid initialize(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) override;\n\tvoid render(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) override;\n\tvoid releaseResources() override;\n\n\tQColor rhiClearColor() override {\n\t\treturn QColor(0, 0, 0, 255);\n\t}\n\n\tstd::optional<QColor> clearColor() override {\n\t\treturn QColor(0, 0, 0, 255);\n\t}\n\n\tvoid paintFallback(\n\t\tPainter &p,\n\t\tconst QRegion &clip,\n\t\tUi::GL::Backend backend) override;\n\nprivate:\n\tQRhi *_rhi = nullptr;\n\tQRhiBuffer *_vertexBuffer = nullptr;\n\tQRhiBuffer *_uniformBuffer = nullptr;\n\tQRhiGraphicsPipeline *_pipeline = nullptr;\n\tQRhiShaderResourceBindings *_srb = nullptr;\n\n\tQElapsedTimer _elapsed;\n\tbool _initialized = false;\n\n};\n\n[[nodiscard]] Ui::GL::ChosenRenderer ChooseDemoRenderer();\n\n[[nodiscard]] std::unique_ptr<Ui::RpWidgetWrap> CreateGpuDemoWidget(\n\tQWidget *parent);\n\n} // namespace Forkgram\n\n#endif // Qt >= 6.7\n"
  },
  {
    "path": "Telegram/SourceFiles/forkgram/uri_menu.cpp",
    "content": "/*\nAuthor: 23rd.\n*/\n\n#include \"forkgram/uri_menu.h\"\n\n#include \"forkgram/uri_open.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/popup_menu.h\"\n\nnamespace Forkgram {\n\nvoid FillUrlWithCustomUri(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst QString &url) {\n\tif (!Core::App().settings().fork().askUriScheme()) {\n\t\treturn;\n\t}\n\tauto callback = [=] {\n\t\tOpenUrlWithCustomUri(Core::App().settings().fork().uriScheme() + url);\n\t};\n\tmenu->addAction(\n\t\ttr::lng_context_open_uri_link(tr::now),\n\t\tstd::move(callback));\n}\n\n} // namespace Forkgram\n"
  },
  {
    "path": "Telegram/SourceFiles/forkgram/uri_menu.h",
    "content": "/*\nAuthor: 23rd.\n*/\n#pragma once\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Forkgram {\n\nvoid FillUrlWithCustomUri(\n\tnot_null<Ui::PopupMenu*> menu,\n\tconst QString &url);\n\n} // namespace Forkgram\n"
  },
  {
    "path": "Telegram/SourceFiles/forkgram/uri_open.cpp",
    "content": "/*\nAuthor: 23rd.\n*/\n\n#include \"forkgram/uri_open.h\"\n\n#include <optional>\n\n#include \"base/platform/base_platform_info.h\"\n\n#include \"base/basic_types.h\"\n\n#ifdef Q_OS_WIN\n#include \"base/platform/win/base_windows_winrt.h\"\n\n#include <winrt/Windows.System.h>\n#include <winrt/Windows.Foundation.h>\n// For Windows 7.\n#include <windows.h>\n#include <shellapi.h>\n\n#else // !Q_OS_WIN\n#include <QDesktopServices>\n#include <QUrl>\n#endif\n\n// Since Qt 5.15 removes the second colon from \"customUri://https://gnu.org\"\n// for some reason, we have to ignore its implementation\n// and open links directly through the Win API.\n\nnamespace Forkgram {\n\nvoid OpenUrlWithCustomUri(const QString &url) {\n#ifdef Q_OS_WIN\n\tif (Platform::IsWindows10OrGreater()) {\n\t\twinrt::Windows::System::Launcher::LaunchUriAsync(\n\t\t\twinrt::Windows::Foundation::Uri(\n\t\t\t\twinrt::to_hstring(url.toStdString())),\n\t\t\twinrt::Windows::System::LauncherOptions());\n\t} else {\n\t\tShellExecute(0, 0, (LPCWSTR)url.utf16(), 0, 0 , SW_SHOW );\n\t}\n#else // !Q_OS_WIN\n\tQDesktopServices::openUrl(url);\n#endif // !Q_OS_WIN\n}\n\n} // namespace Forkgram\n"
  },
  {
    "path": "Telegram/SourceFiles/forkgram/uri_open.h",
    "content": "/*\nAuthor: 23rd.\n*/\n#pragma once\n\n#include <QString>\n\nnamespace Forkgram {\n\nvoid OpenUrlWithCustomUri(const QString &url);\n\n} // namespace Forkgram\n"
  },
  {
    "path": "Telegram/SourceFiles/history/admin_log/history_admin_log_filter.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/admin_log/history_admin_log_filter.h\"\n\n#include \"boxes/peers/edit_peer_permissions_box.h\"\n#include \"history/admin_log/history_admin_log_filter_value.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/wrap/vertical_layout.h\"\n\nnamespace AdminLog {\n\nEditFlagsDescriptor<FilterValue::Flags> FilterValueLabels(bool isChannel) {\n\tusing Label = EditFlagsLabel<FilterValue::Flags>;\n\tusing Flag = FilterValue::Flag;\n\n\tconst auto adminRights = Flag::Promote | Flag::Demote;\n\tconst auto restrictions = Flag::Ban\n\t\t| Flag::Unban\n\t\t| Flag::Kick\n\t\t| Flag::Unkick;\n\tconst auto membersNew = Flag::Join | Flag::Invite;\n\tconst auto membersRemoved = Flag::Leave;\n\tauto membersNewText = (isChannel\n\t\t? tr::lng_admin_log_filter_subscribers_new\n\t\t: tr::lng_admin_log_filter_members_new)(tr::now);\n\tauto membersRemovedText = (isChannel\n\t\t? tr::lng_admin_log_filter_subscribers_removed\n\t\t: tr::lng_admin_log_filter_members_removed)(tr::now);\n\n\tauto members = std::vector<Label>{\n\t\t{ adminRights, tr::lng_admin_log_filter_admins_new(tr::now) },\n\t\t{ Flag::EditRank, tr::lng_admin_log_filter_edit_rank(tr::now) },\n\t\t{ restrictions, tr::lng_admin_log_filter_restrictions(tr::now) },\n\t\t{ membersNew, std::move(membersNewText) },\n\t\t{ membersRemoved, std::move(membersRemovedText) },\n\t};\n\n\tconst auto info = Flag::Info | Flag::Settings;\n\tconst auto invites = Flag::Invites;\n\tconst auto calls = Flag::GroupCall;\n\tauto settings = std::vector<Label>{\n\t\t{\n\t\t\tinfo,\n\t\t\t((!isChannel)\n\t\t\t\t? tr::lng_admin_log_filter_info_group\n\t\t\t\t: tr::lng_admin_log_filter_info_channel)(tr::now),\n\t\t},\n\t\t{ invites, tr::lng_admin_log_filter_invite_links(tr::now) },\n\t\t{\n\t\t\tcalls,\n\t\t\t((!isChannel)\n\t\t\t\t? tr::lng_admin_log_filter_voice_chats\n\t\t\t\t: tr::lng_admin_log_filter_voice_chats_channel)(tr::now),\n\t\t},\n\t\t{\n\t\t\tFlag::SubExtend,\n\t\t\ttr::lng_admin_log_filter_sub_extend(tr::now),\n\t\t},\n\t};\n\tif (!isChannel) {\n\t\tsettings.push_back({\n\t\t\tFlag::Topics,\n\t\t\ttr::lng_admin_log_filter_topics(tr::now),\n\t\t});\n\t}\n\tconst auto deleted = Flag::Delete;\n\tconst auto edited = Flag::Edit;\n\tconst auto pinned = Flag::Pinned;\n\tauto messages = std::vector<Label>{\n\t\t{ deleted, tr::lng_admin_log_filter_messages_deleted(tr::now) },\n\t\t{ edited, tr::lng_admin_log_filter_messages_edited(tr::now) },\n\t};\n\tif (!isChannel) {\n\t\tmessages.push_back({\n\t\t\tpinned,\n\t\t\ttr::lng_admin_log_filter_messages_pinned(tr::now),\n\t\t});\n\t}\n\treturn { .labels = {\n\t\t{\n\t\t\t!isChannel\n\t\t\t\t? tr::lng_admin_log_filter_actions_member_section()\n\t\t\t\t: tr::lng_admin_log_filter_actions_subscriber_section(),\n\t\t\tstd::move(members),\n\t\t},\n\t\t{\n\t\t\t!isChannel\n\t\t\t\t? tr::lng_admin_log_filter_actions_settings_section()\n\t\t\t\t: tr::lng_admin_log_filter_actions_channel_settings_section(),\n\t\t\tstd::move(settings),\n\t\t},\n\t\t{\n\t\t\ttr::lng_admin_log_filter_actions_messages_section(),\n\t\t\tstd::move(messages),\n\t\t},\n\t}, .st = nullptr };\n}\n\nFn<FilterValue::Flags()> FillFilterValueList(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tbool isChannel,\n\t\tconst FilterValue &filter) {\n\tauto [checkboxes, getResult, changes, highlightWidget] = CreateEditAdminLogFilter(\n\t\tcontainer,\n\t\tfilter.flags ? (*filter.flags) : ~FilterValue::Flags(0),\n\t\tisChannel);\n\tcontainer->add(std::move(checkboxes));\n\treturn getResult;\n}\n\n} // namespace AdminLog\n"
  },
  {
    "path": "Telegram/SourceFiles/history/admin_log/history_admin_log_filter.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/admin_log/history_admin_log_filter_value.h\"\n#include \"ui/layers/box_content.h\"\n\ntemplate <typename Flags>\nstruct EditFlagsDescriptor;\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace AdminLog {\n\nstruct FilterValue;\n\n[[nodiscard]] Fn<FilterValue::Flags()> FillFilterValueList(\n\tnot_null<Ui::VerticalLayout*> container,\n\tbool isChannel,\n\tconst FilterValue &filter);\n\nEditFlagsDescriptor<FilterValue::Flags> FilterValueLabels(bool isChannel);\n\n} // namespace AdminLog\n"
  },
  {
    "path": "Telegram/SourceFiles/history/admin_log/history_admin_log_filter_value.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace AdminLog {\n\nstruct FilterValue final {\n\tenum class Flag : uint32 {\n\t\tJoin = (1U << 0),\n\t\tLeave = (1U << 1),\n\t\tInvite = (1U << 2),\n\t\tBan = (1U << 3),\n\t\tUnban = (1U << 4),\n\t\tKick = (1U << 5),\n\t\tUnkick = (1U << 6),\n\t\tPromote = (1U << 7),\n\t\tDemote = (1U << 8),\n\t\tInfo = (1U << 9),\n\t\tSettings = (1U << 10),\n\t\tPinned = (1U << 11),\n\t\tEdit = (1U << 12),\n\t\tDelete = (1U << 13),\n\t\tGroupCall = (1U << 14),\n\t\tInvites = (1U << 15),\n\t\tTopics = (1U << 16),\n\t\tSubExtend = (1U << 17),\n\t\tEditRank = (1U << 18),\n\n\t\tMAX_FIELD = (1U << 18),\n\t};\n\tusing Flags = base::flags<Flag>;\n\tfriend inline constexpr bool is_flag_type(Flag) { return true; };\n\n\t// Std::nullopt \"flags\" means all events.\n\tstd::optional<Flags> flags = std::nullopt;\n\t// Std::nullopt admins means all users.\n\tstd::optional<std::vector<not_null<UserData*>>> admins = std::nullopt;\n\n};\n\ninline bool operator==(const FilterValue &a, const FilterValue &b) {\n\treturn (a.flags == b.flags) && (a.admins == b.admins);\n}\n\ninline bool operator!=(const FilterValue &a, const FilterValue &b) {\n\treturn !(a == b);\n}\n\n} // namespace AdminLog\n"
  },
  {
    "path": "Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/admin_log/history_admin_log_inner.h\"\n\n#include \"history/view/history_view_context_menu_fork.h\"\n#include \"history/history.h\"\n#include \"history/view/media/history_view_media.h\"\n#include \"history/view/media/history_view_web_page.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_text.h\"\n#include \"history/admin_log/history_admin_log_section.h\"\n#include \"history/admin_log/history_admin_log_filter.h\"\n#include \"history/view/history_view_context_menu.h\"\n#include \"history/view/history_view_message.h\"\n#include \"history/view/history_view_service_message.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"boxes/translate_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/qt/qt_common_adapters.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"base/unixtime.h\"\n#include \"base/call_delayed.h\"\n#include \"mainwindow.h\"\n#include \"mainwidget.h\"\n#include \"core/application.h\"\n#include \"apiwrap.h\"\n#include \"api/api_chat_participants.h\"\n#include \"api/api_attached_stickers.h\"\n#include \"api/api_report.h\"\n#include \"window/window_session_controller.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/expandable_peer_list.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/image/image.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/inactive_press.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/ui_utility.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/file_utilities.h\"\n#include \"lang/lang_keys.h\"\n#include \"boxes/peers/edit_participant_box.h\"\n#include \"boxes/peers/edit_participants_box.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_session.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_document.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_cloud_file.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_user.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtWidgets/QApplication>\n#include <QtGui/QClipboard>\n\nnamespace AdminLog {\nnamespace {\n\n// If we require to support more admins we'll have to rewrite this anyway.\nconstexpr auto kMaxChannelAdmins = 200;\nconstexpr auto kScrollDateHideTimeout = 1000;\nconstexpr auto kEventsFirstPage = 20;\nconstexpr auto kEventsPerPage = 50;\nconstexpr auto kClearUserpicsAfter = 50;\n\n} // namespace\n\ntemplate <InnerWidget::EnumItemsDirection direction, typename Method>\nvoid InnerWidget::enumerateItems(Method method) {\n\tconstexpr auto TopToBottom = (direction == EnumItemsDirection::TopToBottom);\n\n\t// No displayed messages in this history.\n\tif (_items.empty()) {\n\t\treturn;\n\t}\n\tif (_visibleBottom <= _itemsTop || _itemsTop + _itemsHeight <= _visibleTop) {\n\t\treturn;\n\t}\n\n\tauto begin = std::rbegin(_items), end = std::rend(_items);\n\tauto from = TopToBottom ? std::lower_bound(begin, end, _visibleTop, [this](auto &elem, int top) {\n\t\treturn this->itemTop(elem) + elem->height() <= top;\n\t}) : std::upper_bound(begin, end, _visibleBottom, [this](int bottom, auto &elem) {\n\t\treturn this->itemTop(elem) + elem->height() >= bottom;\n\t});\n\tauto wasEnd = (from == end);\n\tif (wasEnd) {\n\t\t--from;\n\t}\n\tif (TopToBottom) {\n\t\tAssert(itemTop(from->get()) + from->get()->height() > _visibleTop);\n\t} else {\n\t\tAssert(itemTop(from->get()) < _visibleBottom);\n\t}\n\n\twhile (true) {\n\t\tauto item = from->get();\n\t\tauto itemtop = itemTop(item);\n\t\tauto itembottom = itemtop + item->height();\n\n\t\t// Binary search should've skipped all the items that are above / below the visible area.\n\t\tif (TopToBottom) {\n\t\t\tAssert(itembottom > _visibleTop);\n\t\t} else {\n\t\t\tAssert(itemtop < _visibleBottom);\n\t\t}\n\n\t\tif (!method(item, itemtop, itembottom)) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Skip all the items that are below / above the visible area.\n\t\tif (TopToBottom) {\n\t\t\tif (itembottom >= _visibleBottom) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else {\n\t\t\tif (itemtop <= _visibleTop) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tif (TopToBottom) {\n\t\t\tif (++from == end) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t} else {\n\t\t\tif (from == begin) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t--from;\n\t\t}\n\t}\n}\n\ntemplate <typename Method>\nvoid InnerWidget::enumerateUserpics(Method method) {\n\t// Find and remember the top of an attached messages pack\n\t// -1 means we didn't find an attached to next message yet.\n\tint lowestAttachedItemTop = -1;\n\n\tauto userpicCallback = [&](not_null<Element*> view, int itemtop, int itembottom) {\n\t\t// Skip all service messages.\n\t\tif (view->data()->isService()) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (lowestAttachedItemTop < 0 && view->isAttachedToNext()) {\n\t\t\tlowestAttachedItemTop = itemtop + view->marginTop();\n\t\t}\n\n\t\t// Call method on a userpic for all messages that have it and for those who are not showing it\n\t\t// because of their attachment to the next message if they are bottom-most visible.\n\t\tif (view->displayFromPhoto() || (view->hasFromPhoto() && itembottom >= _visibleBottom)) {\n\t\t\tif (lowestAttachedItemTop < 0) {\n\t\t\t\tlowestAttachedItemTop = itemtop + view->marginTop();\n\t\t\t}\n\t\t\t// Attach userpic to the bottom of the visible area with the same margin as the last message.\n\t\t\tauto userpicMinBottomSkip = st::historyPaddingBottom + st::msgMargin.bottom();\n\t\t\tauto userpicBottom = qMin(itembottom - view->marginBottom(), _visibleBottom - userpicMinBottomSkip);\n\n\t\t\t// Do not let the userpic go above the attached messages pack top line.\n\t\t\tuserpicBottom = qMax(userpicBottom, lowestAttachedItemTop + st::msgPhotoSize);\n\n\t\t\t// Call the template callback function that was passed\n\t\t\t// and return if it finished everything it needed.\n\t\t\tif (!method(view, userpicBottom - st::msgPhotoSize)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\t// Forget the found top of the pack, search for the next one from scratch.\n\t\tif (!view->isAttachedToNext()) {\n\t\t\tlowestAttachedItemTop = -1;\n\t\t}\n\n\t\treturn true;\n\t};\n\n\tenumerateItems<EnumItemsDirection::TopToBottom>(userpicCallback);\n}\n\ntemplate <typename Method>\nvoid InnerWidget::enumerateDates(Method method) {\n\t// Find and remember the bottom of an single-day messages pack\n\t// -1 means we didn't find a same-day with previous message yet.\n\tauto lowestInOneDayItemBottom = -1;\n\n\tauto dateCallback = [&](not_null<Element*> view, int itemtop, int itembottom) {\n\t\tconst auto item = view->data();\n\t\tif (lowestInOneDayItemBottom < 0 && view->isInOneDayWithPrevious()) {\n\t\t\tlowestInOneDayItemBottom = itembottom - view->marginBottom();\n\t\t}\n\n\t\t// Call method on a date for all messages that have it and for those who are not showing it\n\t\t// because they are in a one day together with the previous message if they are top-most visible.\n\t\tif (view->displayDate() || (!item->isEmpty() && itemtop <= _visibleTop)) {\n\t\t\tif (lowestInOneDayItemBottom < 0) {\n\t\t\t\tlowestInOneDayItemBottom = itembottom - view->marginBottom();\n\t\t\t}\n\t\t\t// Attach date to the top of the visible area with the same margin as it has in service message.\n\t\t\tauto dateTop = qMax(itemtop, _visibleTop) + st::msgServiceMargin.top();\n\n\t\t\t// Do not let the date go below the single-day messages pack bottom line.\n\t\t\tauto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();\n\t\t\tdateTop = qMin(dateTop, lowestInOneDayItemBottom - dateHeight);\n\n\t\t\t// Call the template callback function that was passed\n\t\t\t// and return if it finished everything it needed.\n\t\t\tif (!method(view, itemtop, dateTop)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\t// Forget the found bottom of the pack, search for the next one from scratch.\n\t\tif (!view->isInOneDayWithPrevious()) {\n\t\t\tlowestInOneDayItemBottom = -1;\n\t\t}\n\n\t\treturn true;\n\t};\n\n\tenumerateItems<EnumItemsDirection::BottomToTop>(dateCallback);\n}\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<ChannelData*> channel)\n: RpWidget(parent)\n, _controller(controller)\n, _channel(channel)\n, _history(channel->owner().history(channel))\n, _api(&_channel->session().mtp())\n, _pathGradient(\n\tHistoryView::MakePathShiftGradient(\n\t\tcontroller->chatStyle(),\n\t\t[=] { update(); }))\n, _scrollDateCheck([=] { scrollDateCheck(); })\n, _emptyText(\n\t\tst::historyAdminLogEmptyWidth\n\t\t- st::historyAdminLogEmptyPadding.left()\n\t\t- st::historyAdminLogEmptyPadding.left())\n, _antiSpamValidator(_controller, _channel)\n, _touchSelectTimer([=] { onTouchSelect(); })\n, _touchScrollTimer([=] { onTouchScrollTimer(); }) {\n\tWindow::ChatThemeValueFromPeer(\n\t\tcontroller,\n\t\tchannel\n\t) | rpl::on_next([=](std::shared_ptr<Ui::ChatTheme> &&theme) {\n\t\t_theme = std::move(theme);\n\t\tcontroller->setChatStyleTheme(_theme);\n\t}, lifetime());\n\n\tsetAttribute(Qt::WA_AcceptTouchEvents);\n\tsetMouseTracking(true);\n\t_scrollDateHideTimer.setCallback([=] { scrollDateHideByTimer(); });\n\tsession().data().viewRepaintRequest(\n\t) | rpl::on_next([=](Data::RequestViewRepaint data) {\n\t\tif (myView(data.view)) {\n\t\t\trepaintItem(data.view, data.rect);\n\t\t}\n\t}, lifetime());\n\tsession().data().viewResizeRequest(\n\t) | rpl::on_next([=](auto view) {\n\t\tif (myView(view)) {\n\t\t\tresizeItem(view);\n\t\t}\n\t}, lifetime());\n\tsession().data().itemViewRefreshRequest(\n\t) | rpl::on_next([=](auto item) {\n\t\tif (const auto view = viewForItem(item)) {\n\t\t\trefreshItem(view);\n\t\t}\n\t}, lifetime());\n\tsession().data().viewLayoutChanged(\n\t) | rpl::on_next([=](auto view) {\n\t\tif (myView(view)) {\n\t\t\tif (view->isUnderCursor()) {\n\t\t\t\tupdateSelected();\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\tsession().data().itemDataChanges(\n\t) | rpl::on_next([=](not_null<HistoryItem*> item) {\n\t\tif (const auto view = viewForItem(item)) {\n\t\t\tview->itemDataChanged();\n\t\t}\n\t}, lifetime());\n\tsession().data().itemVisibilityQueries(\n\t) | rpl::filter([=](\n\t\t\tconst Data::Session::ItemVisibilityQuery &query) {\n\t\treturn (_history == query.item->history())\n\t\t\t&& query.item->isAdminLogEntry()\n\t\t\t&& isVisible();\n\t}) | rpl::on_next([=](\n\t\t\tconst Data::Session::ItemVisibilityQuery &query) {\n\t\tif (const auto view = viewForItem(query.item)) {\n\t\t\tauto top = itemTop(view);\n\t\t\tif (top >= 0\n\t\t\t\t&& top + view->height() > _visibleTop\n\t\t\t\t&& top < _visibleBottom) {\n\t\t\t\t*query.isVisible = true;\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\tcontroller->adaptive().chatWideValue(\n\t) | rpl::on_next([=](bool wide) {\n\t\t_isChatWide = wide;\n\t}, lifetime());\n\n\tupdateEmptyText();\n\n\t_antiSpamValidator.resolveUser(crl::guard(\n\t\tthis,\n\t\t[=] { requestAdmins(); }));\n}\n\nbool InnerWidget::myView(not_null<const HistoryView::Element*> view) const {\n\treturn !_items.empty() && (view->delegate().get() == this);\n}\n\nMain::Session &InnerWidget::session() const {\n\treturn _controller->session();\n}\n\nrpl::producer<> InnerWidget::showSearchSignal() const {\n\treturn _showSearchSignal.events();\n}\n\nrpl::producer<int> InnerWidget::scrollToSignal() const {\n\treturn _scrollToSignal.events();\n}\n\nrpl::producer<> InnerWidget::cancelSignal() const {\n\treturn _cancelSignal.events();\n}\n\nvoid InnerWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tauto scrolledUp = (visibleTop < _visibleTop);\n\t_visibleTop = visibleTop;\n\t_visibleBottom = visibleBottom;\n\n\t// Unload userpics.\n\tif (_userpics.size() > kClearUserpicsAfter) {\n\t\t_userpicsCache = std::move(_userpics);\n\t}\n\n\tupdateVisibleTopItem();\n\tcheckPreloadMore();\n\tif (scrolledUp) {\n\t\t_scrollDateCheck.call();\n\t} else {\n\t\tscrollDateHideByTimer();\n\t}\n\t_controller->floatPlayerAreaUpdated();\n\tsession().data().itemVisibilitiesUpdated();\n}\n\nvoid InnerWidget::updateVisibleTopItem() {\n\tif (_visibleBottom == height()) {\n\t\t_visibleTopItem = nullptr;\n\t} else {\n\t\tauto begin = std::rbegin(_items), end = std::rend(_items);\n\t\tauto from = std::lower_bound(begin, end, _visibleTop, [this](auto &&elem, int top) {\n\t\t\treturn this->itemTop(elem) + elem->height() <= top;\n\t\t});\n\t\tif (from != end) {\n\t\t\t_visibleTopItem = *from;\n\t\t\t_visibleTopFromItem = _visibleTop - _visibleTopItem->y();\n\t\t} else {\n\t\t\t_visibleTopItem = nullptr;\n\t\t\t_visibleTopFromItem = _visibleTop;\n\t\t}\n\t}\n}\n\nbool InnerWidget::displayScrollDate() const {\n\treturn (_visibleTop <= height() - 2 * (_visibleBottom - _visibleTop));\n}\n\nvoid InnerWidget::scrollDateCheck() {\n\tif (!_visibleTopItem) {\n\t\t_scrollDateLastItem = nullptr;\n\t\t_scrollDateLastItemTop = 0;\n\t\tscrollDateHide();\n\t} else if (_visibleTopItem != _scrollDateLastItem || _visibleTopFromItem != _scrollDateLastItemTop) {\n\t\t// Show scroll date only if it is not the initial onScroll() event (with empty _scrollDateLastItem).\n\t\tif (_scrollDateLastItem && !_scrollDateShown) {\n\t\t\ttoggleScrollDateShown();\n\t\t}\n\t\t_scrollDateLastItem = _visibleTopItem;\n\t\t_scrollDateLastItemTop = _visibleTopFromItem;\n\t\t_scrollDateHideTimer.callOnce(kScrollDateHideTimeout);\n\t}\n}\n\nvoid InnerWidget::scrollDateHideByTimer() {\n\t_scrollDateHideTimer.cancel();\n\tscrollDateHide();\n}\n\nvoid InnerWidget::scrollDateHide() {\n\tif (_scrollDateShown) {\n\t\ttoggleScrollDateShown();\n\t}\n}\n\nvoid InnerWidget::toggleScrollDateShown() {\n\t_scrollDateShown = !_scrollDateShown;\n\tauto from = _scrollDateShown ? 0. : 1.;\n\tauto to = _scrollDateShown ? 1. : 0.;\n\t_scrollDateOpacity.start([this] { repaintScrollDateCallback(); }, from, to, st::historyDateFadeDuration);\n}\n\nvoid InnerWidget::repaintScrollDateCallback() {\n\tauto updateTop = _visibleTop;\n\tauto updateHeight = st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom();\n\tupdate(0, updateTop, width(), updateHeight);\n}\n\nvoid InnerWidget::checkPreloadMore() {\n\tif (_visibleTop + PreloadHeightsCount * (_visibleBottom - _visibleTop) > height()) {\n\t\tpreloadMore(Direction::Down);\n\t}\n\tif (_visibleTop < PreloadHeightsCount * (_visibleBottom - _visibleTop)) {\n\t\tpreloadMore(Direction::Up);\n\t}\n}\n\nvoid InnerWidget::applyFilter(FilterValue &&value) {\n\tif (_filter != value) {\n\t\t_filter = value;\n\t\tclearAndRequestLog();\n\t}\n}\n\nvoid InnerWidget::applySearch(const QString &query) {\n\tif (_searchQuery != query) {\n\t\t_searchQuery = query;\n\t\tclearAndRequestLog();\n\t}\n}\n\nvoid InnerWidget::requestAdmins() {\n\tconst auto offset = 0;\n\tconst auto participantsHash = uint64(0);\n\t_api.request(MTPchannels_GetParticipants(\n\t\t_channel->inputChannel(),\n\t\tMTP_channelParticipantsAdmins(),\n\t\tMTP_int(offset),\n\t\tMTP_int(kMaxChannelAdmins),\n\t\tMTP_long(participantsHash)\n\t)).done([=](const MTPchannels_ChannelParticipants &result) {\n\t\tresult.match([&](const MTPDchannels_channelParticipants &data) {\n\t\t\tconst auto &[availableCount, list] = Api::ChatParticipants::Parse(\n\t\t\t\t_channel,\n\t\t\t\tdata);\n\t\t\t_admins.clear();\n\t\t\t_adminsCanEdit.clear();\n\t\t\tif (const auto user = _antiSpamValidator.maybeAppendUser()) {\n\t\t\t\t_admins.emplace_back(user);\n\t\t\t}\n\t\t\tfor (const auto &parsed : list) {\n\t\t\t\tif (parsed.isUser()) {\n\t\t\t\t\tconst auto user = _channel->owner().userLoaded(\n\t\t\t\t\t\tparsed.userId());\n\t\t\t\t\tif (user) {\n\t\t\t\t\t\t_admins.emplace_back(user);\n\t\t\t\t\t\tif (parsed.canBeEdited() && !parsed.isCreator()) {\n\t\t\t\t\t\t\t_adminsCanEdit.emplace_back(user);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}, [&](const MTPDchannels_channelParticipantsNotModified &) {\n\t\t\tLOG((\"API Error: c\"\n\t\t\t\t\"hannels.channelParticipantsNotModified received!\"));\n\t\t});\n\t\tif (_admins.empty()) {\n\t\t\t_admins.push_back(session().user());\n\t\t}\n\t\tif (_showFilterCallback) {\n\t\t\tshowFilter(std::move(_showFilterCallback));\n\t\t}\n\t}).send();\n}\n\nvoid InnerWidget::showFilter(Fn<void(FilterValue &&filter)> callback) {\n\tif (_admins.empty()) {\n\t\t_showFilterCallback = std::move(callback);\n\t\treturn;\n\t}\n\tconst auto isChannel = !_channel->isMegagroup();\n\tconst auto filter = _filter;\n\tconst auto admins = _admins;\n\t_controller->uiShow()->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tbox->setTitle(tr::lng_manage_peer_recent_actions());\n\t\tUi::AddSubsectionTitle(\n\t\t\tbox->verticalLayout(),\n\t\t\ttr::lng_admin_log_filter_actions_type_subtitle());\n\t\tconst auto collectFlags = FillFilterValueList(\n\t\t\tbox->verticalLayout(),\n\t\t\tisChannel,\n\t\t\tfilter);\n\t\tUi::AddSkip(box->verticalLayout());\n\t\tUi::AddDivider(box->verticalLayout());\n\t\tUi::AddSkip(box->verticalLayout());\n\t\tUi::AddSubsectionTitle(\n\t\t\tbox->verticalLayout(),\n\t\t\ttr::lng_admin_log_filter_actions_admins_subtitle());\n\t\tUi::AddSkip(box->verticalLayout());\n\n\t\tauto checkedPeerId = std::vector<PeerId>();\n\t\t{\n\t\t\tcheckedPeerId.reserve(admins.size());\n\t\t\tfor (const auto &user : admins) {\n\t\t\t\tif (!filter.admins\n\t\t\t\t\t|| ranges::contains(\n\t\t\t\t\t\t(*filter.admins),\n\t\t\t\t\t\tuser->id,\n\t\t\t\t\t\t&UserData::id)) {\n\t\t\t\t\tcheckedPeerId.push_back(user->id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst auto checkbox = box->addRow(\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tbox->verticalLayout(),\n\t\t\t\ttr::lng_admin_log_filter_actions_admins_section(\n\t\t\t\t\ttr::now,\n\t\t\t\t\ttr::marked),\n\t\t\t\tcheckedPeerId.size() == admins.size(),\n\t\t\t\tst::defaultBoxCheckbox));\n\t\tusing Controller = Ui::ExpandablePeerListController;\n\t\tusing Data = Ui::ExpandablePeerListController::Data;\n\t\tconst auto controller = box->lifetime().make_state<Controller>(Data{\n\t\t\t.participants = ranges::views::all(\n\t\t\t\tadmins\n\t\t\t) | ranges::views::transform([](\n\t\t\t\t\tnot_null<UserData*> user) -> not_null<PeerData*> {\n\t\t\t\treturn not_null{ user };\n\t\t\t}) | ranges::to_vector,\n\t\t\t.checked = std::move(checkedPeerId),\n\t\t\t.skipSingle = true,\n\t\t\t.hideRightButton = true,\n\t\t\t.checkTopOnAllInner = true,\n\t\t});\n\t\tUi::AddExpandablePeerList(\n\t\t\tcheckbox,\n\t\t\tcontroller,\n\t\t\tbox->verticalLayout());\n\n\t\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\t\tconst auto peers = controller->collectRequests();\n\t\t\tconst auto users = ranges::views::all(\n\t\t\t\tpeers\n\t\t\t) | ranges::views::transform([](not_null<PeerData*> p) {\n\t\t\t\treturn not_null{ p->asUser() };\n\t\t\t}) | ranges::to_vector;\n\t\t\tcallback(FilterValue{\n\t\t\t\t.flags = collectFlags(),\n\t\t\t\t.admins = (admins.size() == users.size())\n\t\t\t\t\t? std::nullopt\n\t\t\t\t\t: std::optional(users),\n\t\t\t});\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [box] { box->closeBox(); });\n\t}));\n}\n\nvoid InnerWidget::clearAndRequestLog() {\n\t_api.request(base::take(_preloadUpRequestId)).cancel();\n\t_api.request(base::take(_preloadDownRequestId)).cancel();\n\t_filterChanged = true;\n\t_upLoaded = false;\n\t_downLoaded = true;\n\tupdateMinMaxIds();\n\tpreloadMore(Direction::Up);\n}\n\nvoid InnerWidget::updateEmptyText() {\n\tauto hasSearch = !_searchQuery.isEmpty();\n\tauto hasFilter = _filter.flags || _filter.admins;\n\tauto text = tr::semibold((hasSearch || hasFilter)\n\t\t? tr::lng_admin_log_no_results_title(tr::now)\n\t\t: tr::lng_admin_log_no_events_title(tr::now));\n\tauto description = hasSearch\n\t\t? tr::lng_admin_log_no_results_search_text(\n\t\t\ttr::now,\n\t\t\tlt_query,\n\t\t\t_searchQuery)\n\t\t: hasFilter\n\t\t? tr::lng_admin_log_no_results_text(tr::now)\n\t\t: _channel->isMegagroup()\n\t\t? tr::lng_admin_log_no_events_text(tr::now)\n\t\t: tr::lng_admin_log_no_events_text_channel(tr::now);\n\ttext.text.append(u\"\\n\\n\"_q + description);\n\t_emptyText.setMarkedText(st::defaultTextStyle, text);\n}\n\nQString InnerWidget::tooltipText() const {\n\tif (_mouseAction == MouseAction::None\n\t\t&& (_mouseCursorState == CursorState::Date\n\t\t\t|| _mouseCursorState == CursorState::LogAdminService)) {\n\t\tif (const auto view = Element::Hovered()) {\n\t\t\tauto dateText = HistoryView::DateTooltipText(view);\n\n\t\t\tconst auto sentIt = _itemDates.find(view->data());\n\t\t\tif (sentIt != end(_itemDates)) {\n\t\t\t\tdateText += '\\n' + tr::lng_sent_date(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_date,\n\t\t\t\t\tQLocale().toString(\n\t\t\t\t\t\tbase::unixtime::parse(sentIt->second),\n\t\t\t\t\t\tQLocale::LongFormat));\n\t\t\t}\n\t\t\treturn dateText;\n\t\t}\n\t} else if (_mouseCursorState == CursorState::Forwarded\n\t\t&& _mouseAction == MouseAction::None) {\n\t\tif (const auto view = Element::Hovered()) {\n\t\t\tif (const auto forwarded = view->data()->Get<HistoryMessageForwarded>()) {\n\t\t\t\treturn forwarded->text.toString();\n\t\t\t}\n\t\t}\n\t} else if (const auto lnk = ClickHandler::getActive()) {\n\t\treturn lnk->tooltip();\n\t}\n\treturn QString();\n}\n\nQPoint InnerWidget::tooltipPos() const {\n\treturn _mousePosition;\n}\n\nbool InnerWidget::tooltipWindowActive() const {\n\treturn Ui::AppInFocus() && Ui::InFocusChain(window());\n}\n\nHistoryView::Context InnerWidget::elementContext() {\n\treturn HistoryView::Context::AdminLog;\n}\n\nbool InnerWidget::elementUnderCursor(\n\t\tnot_null<const HistoryView::Element*> view) {\n\treturn (Element::Hovered() == view);\n}\n\nHistoryView::SelectionModeResult InnerWidget::elementInSelectionMode(\n\t\tconst HistoryView::Element *) {\n\treturn {};\n}\n\nbool InnerWidget::elementIntersectsRange(\n\t\tnot_null<const Element*> view,\n\t\tint from,\n\t\tint till) {\n\tExpects(view->delegate() == this);\n\n\tconst auto top = itemTop(view);\n\tconst auto bottom = top + view->height();\n\treturn (top < till && bottom > from);\n}\n\nvoid InnerWidget::elementStartStickerLoop(not_null<const Element*> view) {\n}\n\nvoid InnerWidget::elementShowPollResults(\n\tnot_null<PollData*> poll,\n\tFullMsgId context) {\n}\n\nvoid InnerWidget::elementShowAddPollOption(\n\tnot_null<HistoryView::Element*> view,\n\tnot_null<PollData*> poll,\n\tFullMsgId context,\n\tQRect optionRect) {\n}\n\nvoid InnerWidget::elementSubmitAddPollOption(FullMsgId context) {\n}\n\nvoid InnerWidget::elementOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) {\n\t_controller->openPhoto(photo, { context });\n}\n\nvoid InnerWidget::elementOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView) {\n\t_controller->openDocument(document, showInMediaView, { context });\n}\n\nvoid InnerWidget::elementCancelUpload(const FullMsgId &context) {\n\tif (const auto item = session().data().message(context)) {\n\t\t_controller->cancelUploadLayer(item);\n\t}\n}\n\nvoid InnerWidget::elementShowTooltip(\n\tconst TextWithEntities &text,\n\tFn<void()> hiddenCallback) {\n}\n\nbool InnerWidget::elementAnimationsPaused() {\n\treturn _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any);\n}\n\nbool InnerWidget::elementHideReply(not_null<const Element*> view) {\n\treturn false;\n}\n\nbool InnerWidget::elementShownUnread(not_null<const Element*> view) {\n\treturn false;\n}\n\nvoid InnerWidget::elementSendBotCommand(\n\tconst QString &command,\n\tconst FullMsgId &context) {\n}\n\nvoid InnerWidget::elementSearchInList(\n\tconst QString &query,\n\tconst FullMsgId &context) {\n\n}\n\nvoid InnerWidget::elementHandleViaClick(not_null<UserData*> bot) {\n}\n\nHistoryView::ElementChatMode InnerWidget::elementChatMode() {\n\tusing Mode = HistoryView::ElementChatMode;\n\treturn _isChatWide ? Mode::Wide : Mode::Default;\n}\n\nnot_null<Ui::PathShiftGradient*> InnerWidget::elementPathShiftGradient() {\n\treturn _pathGradient.get();\n}\n\nvoid InnerWidget::elementReplyTo(const FullReplyTo &to) {\n}\n\nvoid InnerWidget::elementStartInteraction(not_null<const Element*> view) {\n}\n\nvoid InnerWidget::elementStartPremium(\n\tnot_null<const Element*> view,\n\tElement *replacing) {\n}\n\nvoid InnerWidget::elementCancelPremium(not_null<const Element*> view) {\n}\n\nvoid InnerWidget::elementStartEffect(\n\tnot_null<const Element*> view,\n\tElement *replacing) {\n}\n\nQString InnerWidget::elementAuthorRank(not_null<const Element*> view) {\n\treturn {};\n}\n\nbool InnerWidget::elementHideTopicButton(not_null<const Element*> view) {\n\treturn false;\n}\n\nvoid InnerWidget::saveState(not_null<SectionMemento*> memento) {\n\tmemento->setFilter(std::move(_filter));\n\tmemento->setAdmins(std::move(_admins));\n\tmemento->setAdminsCanEdit(std::move(_adminsCanEdit));\n\tmemento->setSearchQuery(std::move(_searchQuery));\n\tif (!_filterChanged) {\n\t\tfor (auto &item : _items) {\n\t\t\titem.clearView();\n\t\t}\n\t\tmemento->setItems(\n\t\t\tbase::take(_items),\n\t\t\tbase::take(_eventIds),\n\t\t\t_upLoaded,\n\t\t\t_downLoaded);\n\t\tbase::take(_itemsByData);\n\t}\n\t_upLoaded = _downLoaded = true; // Don't load or handle anything anymore.\n}\n\nvoid InnerWidget::restoreState(not_null<SectionMemento*> memento) {\n\t// OwnedItem::refreshView may call requestItemResize.\n\t// So we postpone resizing until all views are created.\n\t_items.clear();\n\tauto items = memento->takeItems();\n\tfor (auto &item : items) {\n\t\titem.refreshView(this);\n\t\t_itemsByData.emplace(item->data(), item.get());\n\t}\n\t_items = std::move(items);\n\n\t_eventIds = memento->takeEventIds();\n\t_admins = memento->takeAdmins();\n\t_adminsCanEdit = memento->takeAdminsCanEdit();\n\t_filter = memento->takeFilter();\n\t_searchQuery = memento->takeSearchQuery();\n\t_upLoaded = memento->upLoaded();\n\t_downLoaded = memento->downLoaded();\n\t_filterChanged = false;\n\tupdateMinMaxIds();\n\tupdateSize();\n}\n\nvoid InnerWidget::preloadMore(Direction direction) {\n\tauto &requestId = (direction == Direction::Up) ? _preloadUpRequestId : _preloadDownRequestId;\n\tauto &loadedFlag = (direction == Direction::Up) ? _upLoaded : _downLoaded;\n\tif (requestId != 0 || loadedFlag) {\n\t\treturn;\n\t}\n\n\tauto flags = MTPchannels_GetAdminLog::Flags(0);\n\tconst auto filter = [&] {\n\t\tusing Flag = MTPDchannelAdminLogEventsFilter::Flag;\n\t\tusing LocalFlag = FilterValue::Flag;\n\t\tconst auto empty = MTPDchannelAdminLogEventsFilter::Flags(0);\n\t\tconst auto f = _filter.flags.value_or(LocalFlag());\n\t\treturn empty\n\t\t\t| ((f & LocalFlag::Join) ? Flag::f_join : empty)\n\t\t\t| ((f & LocalFlag::Leave) ? Flag::f_leave : empty)\n\t\t\t| ((f & LocalFlag::Invite) ? Flag::f_invite : empty)\n\t\t\t| ((f & LocalFlag::Ban) ? Flag::f_ban : empty)\n\t\t\t| ((f & LocalFlag::Unban) ? Flag::f_unban : empty)\n\t\t\t| ((f & LocalFlag::Kick) ? Flag::f_kick : empty)\n\t\t\t| ((f & LocalFlag::Unkick) ? Flag::f_unkick : empty)\n\t\t\t| ((f & LocalFlag::Promote) ? Flag::f_promote : empty)\n\t\t\t| ((f & LocalFlag::Demote) ? Flag::f_demote : empty)\n\t\t\t| ((f & LocalFlag::Info) ? Flag::f_info : empty)\n\t\t\t| ((f & LocalFlag::Settings) ? Flag::f_settings : empty)\n\t\t\t| ((f & LocalFlag::Pinned) ? Flag::f_pinned : empty)\n\t\t\t| ((f & LocalFlag::Edit) ? Flag::f_edit : empty)\n\t\t\t| ((f & LocalFlag::Delete) ? Flag::f_delete : empty)\n\t\t\t| ((f & LocalFlag::GroupCall) ? Flag::f_group_call : empty)\n\t\t\t| ((f & LocalFlag::Invites) ? Flag::f_invites : empty)\n\t\t\t| ((f & LocalFlag::Topics) ? Flag::f_forums : empty)\n\t\t\t| ((f & LocalFlag::SubExtend) ? Flag::f_sub_extend : empty)\n\t\t\t| ((f & LocalFlag::EditRank) ? Flag::f_edit_rank : empty);\n\t}();\n\tif (_filter.flags != 0) {\n\t\tflags |= MTPchannels_GetAdminLog::Flag::f_events_filter;\n\t}\n\tauto admins = QVector<MTPInputUser>(0);\n\tif (_filter.admins) {\n\t\tif (!_filter.admins->empty()) {\n\t\t\tadmins.reserve(_filter.admins->size());\n\t\t\tfor (const auto &admin : (*_filter.admins)) {\n\t\t\t\tadmins.push_back(admin->inputUser());\n\t\t\t}\n\t\t}\n\t\tflags |= MTPchannels_GetAdminLog::Flag::f_admins;\n\t}\n\tauto maxId = (direction == Direction::Up) ? _minId : 0;\n\tauto minId = (direction == Direction::Up) ? 0 : _maxId;\n\tauto perPage = _items.empty() ? kEventsFirstPage : kEventsPerPage;\n\trequestId = _api.request(MTPchannels_GetAdminLog(\n\t\tMTP_flags(flags),\n\t\t_channel->inputChannel(),\n\t\tMTP_string(_searchQuery),\n\t\tMTP_channelAdminLogEventsFilter(MTP_flags(filter)),\n\t\tMTP_vector<MTPInputUser>(admins),\n\t\tMTP_long(maxId),\n\t\tMTP_long(minId),\n\t\tMTP_int(perPage)\n\t)).done([=, &requestId, &loadedFlag](const MTPchannels_AdminLogResults &result) {\n\t\tExpects(result.type() == mtpc_channels_adminLogResults);\n\n\t\trequestId = 0;\n\n\t\tauto &results = result.c_channels_adminLogResults();\n\t\t_channel->owner().processUsers(results.vusers());\n\t\t_channel->owner().processChats(results.vchats());\n\t\tif (!loadedFlag) {\n\t\t\taddEvents(direction, results.vevents().v);\n\t\t}\n\t}).fail([this, &requestId, &loadedFlag] {\n\t\trequestId = 0;\n\t\tloadedFlag = true;\n\t\tupdate();\n\t}).send();\n}\n\nvoid InnerWidget::addEvents(Direction direction, const QVector<MTPChannelAdminLogEvent> &events) {\n\tif (_filterChanged) {\n\t\tclearAfterFilterChange();\n\t}\n\n\tauto up = (direction == Direction::Up);\n\tif (events.empty()) {\n\t\t(up ? _upLoaded : _downLoaded) = true;\n\t\tupdate();\n\t\treturn;\n\t}\n\n\t// When loading items up we just add them to the back of the _items vector.\n\t// When loading items down we add them to a new vector and copy _items after them.\n\tauto newItemsForDownDirection = std::vector<OwnedItem>();\n\tauto oldItemsCount = _items.size();\n\tauto &addToItems = (direction == Direction::Up)\n\t\t? _items\n\t\t: newItemsForDownDirection;\n\taddToItems.reserve(oldItemsCount + events.size() * 2);\n\n\tconst auto canRestrict = InnerWidget::canRestrict();\n\tconst auto antiSpamUserId = _antiSpamValidator.userId();\n\tfor (const auto &event : events) {\n\t\tconst auto &data = event.data();\n\t\tconst auto id = data.vid().v;\n\t\tif (_eventIds.find(id) != _eventIds.end()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto rememberRealMsgId = (antiSpamUserId\n\t\t\t== peerToUser(peerFromUser(data.vuser_id())));\n\n\t\tauto count = 0;\n\t\tconst auto addOne = [&](\n\t\t\t\tOwnedItem item,\n\t\t\t\tTimeId sentDate,\n\t\t\t\tMsgId realId) {\n\t\t\tif (sentDate) {\n\t\t\t\t_itemDates.emplace(item->data(), sentDate);\n\t\t\t}\n\t\t\t_eventIds.emplace(id);\n\t\t\t_itemsByData.emplace(item->data(), item.get());\n\t\t\tif (realId) {\n\t\t\t\tif (rememberRealMsgId) {\n\t\t\t\t\t_antiSpamValidator.addEventMsgId(\n\t\t\t\t\t\titem->data()->fullId(),\n\t\t\t\t\t\trealId);\n\t\t\t\t}\n\t\t\t\tif (canRestrict) {\n\t\t\t\t\t_realIdsForReport[item->data()->fullId()] = realId;\n\t\t\t\t}\n\t\t\t}\n\t\t\taddToItems.push_back(std::move(item));\n\t\t\t++count;\n\t\t};\n\t\tGenerateItems(\n\t\t\tthis,\n\t\t\t_history,\n\t\t\tdata,\n\t\t\taddOne);\n\t\tif (count > 1) {\n\t\t\t// Reverse the inner order of the added messages, because we load events\n\t\t\t// from bottom to top but inside one event they go from top to bottom.\n\t\t\tauto full = addToItems.size();\n\t\t\tauto from = full - count;\n\t\t\tfor (auto i = 0, toReverse = count / 2; i != toReverse; ++i) {\n\t\t\t\tstd::swap(addToItems[from + i], addToItems[full - i - 1]);\n\t\t\t}\n\t\t}\n\t}\n\tauto newItemsCount = _items.size() + ((direction == Direction::Up) ? 0 : newItemsForDownDirection.size());\n\tif (newItemsCount != oldItemsCount) {\n\t\tif (direction == Direction::Down) {\n\t\t\tfor (auto &item : _items) {\n\t\t\t\tnewItemsForDownDirection.push_back(std::move(item));\n\t\t\t}\n\t\t\t_items = std::move(newItemsForDownDirection);\n\t\t}\n\t\tupdateMinMaxIds();\n\t\titemsAdded(direction, newItemsCount - oldItemsCount);\n\t}\n\tupdate();\n}\n\nvoid InnerWidget::updateMinMaxIds() {\n\tif (_eventIds.empty() || _filterChanged) {\n\t\t_maxId = _minId = 0;\n\t} else {\n\t\t_maxId = *_eventIds.rbegin();\n\t\t_minId = *_eventIds.begin();\n\t\tif (_minId == 1) {\n\t\t\t_upLoaded = true;\n\t\t}\n\t}\n}\n\nvoid InnerWidget::itemsAdded(Direction direction, int addedCount) {\n\tExpects(addedCount >= 0);\n\tauto checkFrom = (direction == Direction::Up)\n\t\t? (_items.size() - addedCount)\n\t\t: 1; // Should be \": 0\", but zero is skipped anyway.\n\tauto checkTo = (direction == Direction::Up) ? (_items.size() + 1) : (addedCount + 1);\n\tfor (auto i = checkFrom; i != checkTo; ++i) {\n\t\tif (i > 0) {\n\t\t\tconst auto view = _items[i - 1].get();\n\t\t\tif (i < _items.size()) {\n\t\t\t\tconst auto previous = _items[i].get();\n\t\t\t\tview->setDisplayDate(view->dateTime().date() != previous->dateTime().date());\n\t\t\t\tconst auto attach = view->computeIsAttachToPrevious(previous);\n\t\t\t\tview->setAttachToPrevious(attach, previous);\n\t\t\t\tprevious->setAttachToNext(attach, view);\n\t\t\t} else {\n\t\t\t\tview->setDisplayDate(true);\n\t\t\t}\n\t\t}\n\t}\n\tupdateSize();\n}\n\nvoid InnerWidget::updateSize() {\n\tRpWidget::resizeToWidth(width());\n\trestoreScrollPosition();\n\tupdateVisibleTopItem();\n\tcheckPreloadMore();\n}\n\nint InnerWidget::resizeGetHeight(int newWidth) {\n\tupdate();\n\n\tconst auto resizeAllItems = (_itemsWidth != newWidth);\n\tauto newHeight = 0;\n\tfor (const auto &item : ranges::views::reverse(_items)) {\n\t\titem->setY(newHeight);\n\t\tif (item->pendingResize() || resizeAllItems) {\n\t\t\tnewHeight += item->resizeGetHeight(newWidth);\n\t\t} else {\n\t\t\tnewHeight += item->height();\n\t\t}\n\t}\n\t_itemsWidth = newWidth;\n\t_itemsHeight = newHeight;\n\t_itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom) ? (_minHeight - _itemsHeight - st::historyPaddingBottom) : 0;\n\treturn _itemsTop + _itemsHeight + st::historyPaddingBottom;\n}\n\nvoid InnerWidget::restoreScrollPosition() {\n\tconst auto newVisibleTop = _visibleTopItem\n\t\t? (itemTop(_visibleTopItem) + _visibleTopFromItem)\n\t\t: ScrollMax;\n\t_scrollToSignal.fire_copy(newVisibleTop);\n}\n\nUi::ChatPaintContext InnerWidget::preparePaintContext(QRect clip) const {\n\treturn _controller->preparePaintContext({\n\t\t.theme = _theme.get(),\n\t\t.clip = clip,\n\t\t.visibleAreaPositionGlobal = mapToGlobal(QPoint(0, _visibleTop)),\n\t\t.visibleAreaTop = _visibleTop,\n\t\t.visibleAreaWidth = width(),\n\t\t.visibleAreaHeight = _visibleBottom - _visibleTop,\n\t});\n}\n\nvoid InnerWidget::paintEvent(QPaintEvent *e) {\n\tif (_controller->contentOverlapped(this, e)) {\n\t\treturn;\n\t}\n\n\tconst auto guard = gsl::finally([&] {\n\t\t_userpicsCache.clear();\n\t});\n\n\tPainter p(this);\n\n\tauto clip = e->rect();\n\tauto context = preparePaintContext(clip);\n\tif (_items.empty() && _upLoaded && _downLoaded) {\n\t\tpaintEmpty(p, context.st);\n\t} else {\n\t\t_pathGradient->startFrame(\n\t\t\t0,\n\t\t\twidth(),\n\t\t\tstd::min(st::msgMaxWidth / 2, width() / 2));\n\n\t\tauto begin = std::rbegin(_items), end = std::rend(_items);\n\t\tauto from = std::lower_bound(begin, end, clip.top(), [this](auto &elem, int top) {\n\t\t\treturn this->itemTop(elem) + elem->height() <= top;\n\t\t});\n\t\tauto to = std::lower_bound(begin, end, clip.top() + clip.height(), [this](auto &elem, int bottom) {\n\t\t\treturn this->itemTop(elem) < bottom;\n\t\t});\n\t\tif (from != end) {\n\t\t\tauto top = itemTop(from->get());\n\t\t\tcontext.translate(0, -top);\n\t\t\tp.translate(0, top);\n\t\t\tfor (auto i = from; i != to; ++i) {\n\t\t\t\tconst auto view = i->get();\n\t\t\t\tcontext.outbg = view->hasOutLayout();\n\t\t\t\tcontext.selection = (view == _selectedItem)\n\t\t\t\t\t? _selectedText\n\t\t\t\t\t: TextSelection();\n\t\t\t\tview->draw(p, context);\n\n\t\t\t\tconst auto height = view->height();\n\t\t\t\ttop += height;\n\t\t\t\tcontext.translate(0, -height);\n\t\t\t\tp.translate(0, height);\n\t\t\t}\n\t\t\tcontext.translate(0, top);\n\t\t\tp.translate(0, -top);\n\n\t\t\tenumerateUserpics([&](not_null<Element*> view, int userpicTop) {\n\t\t\t\t// stop the enumeration if the userpic is below the painted rect\n\t\t\t\tif (userpicTop >= clip.top() + clip.height()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\t// paint the userpic if it intersects the painted rect\n\t\t\t\tif (userpicTop + st::msgPhotoSize > clip.top()) {\n\t\t\t\t\tconst auto from = view->data()->from();\n\t\t\t\t\tfrom->paintUserpicLeft(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\t_userpics[from],\n\t\t\t\t\t\tst::historyPhotoLeft,\n\t\t\t\t\t\tuserpicTop,\n\t\t\t\t\t\tview->width(),\n\t\t\t\t\t\tst::msgPhotoSize);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\n\t\t\tauto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();\n\t\t\tauto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.);\n\t\t\tenumerateDates([&](not_null<Element*> view, int itemtop, int dateTop) {\n\t\t\t\t// stop the enumeration if the date is above the painted rect\n\t\t\t\tif (dateTop + dateHeight <= clip.top()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tconst auto displayDate = view->displayDate();\n\t\t\t\tauto dateInPlace = displayDate;\n\t\t\t\tif (dateInPlace) {\n\t\t\t\t\tconst auto correctDateTop = itemtop + st::msgServiceMargin.top();\n\t\t\t\t\tdateInPlace = (dateTop < correctDateTop + dateHeight);\n\t\t\t\t}\n\t\t\t\t//bool noFloatingDate = (item->date.date() == lastDate && displayDate);\n\t\t\t\t//if (noFloatingDate) {\n\t\t\t\t//\tif (itemtop < showFloatingBefore) {\n\t\t\t\t//\t\tnoFloatingDate = false;\n\t\t\t\t//\t}\n\t\t\t\t//}\n\n\t\t\t\t// paint the date if it intersects the painted rect\n\t\t\t\tif (dateTop < clip.top() + clip.height()) {\n\t\t\t\t\tauto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity;\n\t\t\t\t\tif (opacity > 0.) {\n\t\t\t\t\t\tp.setOpacity(opacity);\n\t\t\t\t\t\tconst auto dateY = /*noFloatingDate ? itemtop :*/ (dateTop - st::msgServiceMargin.top());\n\t\t\t\t\t\tconst auto width = view->width();\n\t\t\t\t\t\tif (const auto date = view->Get<HistoryView::DateBadge>()) {\n\t\t\t\t\t\t\tdate->paint(p, context.st, dateY, width, _isChatWide);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tHistoryView::ServiceMessagePainter::PaintDate(\n\t\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\t\tcontext.st,\n\t\t\t\t\t\t\t\tview->dateTime(),\n\t\t\t\t\t\t\t\tdateY,\n\t\t\t\t\t\t\t\twidth,\n\t\t\t\t\t\t\t\t_isChatWide);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\t}\n}\n\nvoid InnerWidget::clearAfterFilterChange() {\n\t_visibleTopItem = nullptr;\n\t_visibleTopFromItem = 0;\n\t_scrollDateLastItem = nullptr;\n\t_scrollDateLastItemTop = 0;\n\t_mouseActionItem = nullptr;\n\t_selectedItem = nullptr;\n\t_selectedText = TextSelection();\n\t_filterChanged = false;\n\t_items.clear();\n\t_eventIds.clear();\n\t_itemsByData.clear();\n\tupdateEmptyText();\n\tupdateSize();\n}\n\nauto InnerWidget::viewForItem(const HistoryItem *item) -> Element* {\n\tif (item) {\n\t\tconst auto i = _itemsByData.find(item);\n\t\tif (i != _itemsByData.end()) {\n\t\t\treturn i->second;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid InnerWidget::paintEmpty(Painter &p, not_null<const Ui::ChatStyle*> st) {\n\tauto rectWidth = st::historyAdminLogEmptyWidth;\n\tauto innerWidth = rectWidth - st::historyAdminLogEmptyPadding.left() - st::historyAdminLogEmptyPadding.right();\n\tauto rectHeight = st::historyAdminLogEmptyPadding.top() + _emptyText.countHeight(innerWidth) + st::historyAdminLogEmptyPadding.bottom();\n\tauto rect = QRect((width() - rectWidth) / 2, (height() - rectHeight) / 3, rectWidth, rectHeight);\n\tHistoryView::ServiceMessagePainter::PaintBubble(p, st, rect);\n\n\tp.setPen(st->msgServiceFg());\n\t_emptyText.draw(p, rect.x() + st::historyAdminLogEmptyPadding.left(), rect.y() + st::historyAdminLogEmptyPadding.top(), innerWidth, style::al_top);\n}\n\nTextForMimeData InnerWidget::getSelectedText() const {\n\treturn _selectedItem\n\t\t? _selectedItem->selectedText(_selectedText)\n\t\t: TextForMimeData();\n}\n\nvoid InnerWidget::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Escape || e->key() == Qt::Key_Back) {\n\t\t_cancelSignal.fire({});\n\t} else if (e == QKeySequence::Copy && _selectedItem != nullptr) {\n\t\tcopySelectedText();\n#ifdef Q_OS_MAC\n\t} else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) {\n\t\tTextUtilities::SetClipboardText(getSelectedText(), QClipboard::FindBuffer);\n#endif // Q_OS_MAC\n\t} else {\n\t\te->ignore();\n\t}\n}\n\nvoid InnerWidget::mouseDoubleClickEvent(QMouseEvent *e) {\n\tmouseActionStart(e->globalPos(), e->button());\n\tif (((_mouseAction == MouseAction::Selecting && _selectedItem != nullptr) || (_mouseAction == MouseAction::None)) && _mouseSelectType == TextSelectType::Letters && _mouseActionItem) {\n\t\tStateRequest request;\n\t\trequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\tauto dragState = _mouseActionItem->textState(_dragStartPosition, request);\n\t\tif (dragState.cursor == CursorState::Text) {\n\t\t\t_mouseTextSymbol = dragState.symbol;\n\t\t\t_mouseSelectType = TextSelectType::Words;\n\t\t\tif (_mouseAction == MouseAction::None) {\n\t\t\t\t_mouseAction = MouseAction::Selecting;\n\t\t\t\tauto selection = TextSelection { dragState.symbol, dragState.symbol };\n\t\t\t\trepaintItem(std::exchange(_selectedItem, _mouseActionItem));\n\t\t\t\t_selectedText = selection;\n\t\t\t}\n\t\t\tmouseMoveEvent(e);\n\n\t\t\t_trippleClickPoint = e->globalPos();\n\t\t\t_trippleClickTimer.callOnce(QApplication::doubleClickInterval());\n\t\t}\n\t}\n}\n\nvoid InnerWidget::contextMenuEvent(QContextMenuEvent *e) {\n\tshowContextMenu(e);\n}\n\nvoid InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {\n\tif (e->reason() == QContextMenuEvent::Mouse) {\n\t\tmouseActionUpdate(e->globalPos());\n\t}\n\n\t// -1 - has selection, but no over, 0 - no selection, 1 - over text\n\tauto isUponSelected = 0;\n\tauto hasSelected = 0;\n\tif (_selectedItem) {\n\t\tisUponSelected = -1;\n\n\t\tauto selFrom = _selectedText.from;\n\t\tauto selTo = _selectedText.to;\n\t\thasSelected = (selTo > selFrom) ? 1 : 0;\n\t\tif (Element::Moused() && Element::Moused() == Element::Hovered()) {\n\t\t\tauto mousePos = mapPointToItem(\n\t\t\t\tmapFromGlobal(_mousePosition),\n\t\t\t\tElement::Moused());\n\t\t\tStateRequest request;\n\t\t\trequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t\tauto dragState = Element::Moused()->textState(mousePos, request);\n\t\t\tif (dragState.cursor == CursorState::Text\n\t\t\t\t&& base::in_range(dragState.symbol, selFrom, selTo)) {\n\t\t\t\tisUponSelected = 1;\n\t\t\t}\n\t\t}\n\t}\n\tif (showFromTouch && hasSelected && isUponSelected < hasSelected) {\n\t\tisUponSelected = hasSelected;\n\t}\n\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\tst::popupMenuExpandedSeparator);\n\n\tconst auto link = ClickHandler::getActive();\n\tauto view = Element::Hovered()\n\t\t? Element::Hovered()\n\t\t: Element::HoveredLink();\n\tconst auto lnkPhoto = link\n\t\t? reinterpret_cast<PhotoData*>(\n\t\t\tlink->property(kPhotoLinkMediaProperty).toULongLong())\n\t\t: nullptr;\n\tconst auto lnkDocument = link\n\t\t? reinterpret_cast<DocumentData*>(\n\t\t\tlink->property(kDocumentLinkMediaProperty).toULongLong())\n\t\t: nullptr;\n\tauto lnkIsVideo = lnkDocument ? lnkDocument->isVideoFile() : false;\n\tauto lnkIsVoice = lnkDocument ? lnkDocument->isVoiceMessage() : false;\n\tauto lnkIsAudio = lnkDocument ? lnkDocument->isAudioFile() : false;\n\tconst auto fromId = PeerId(link\n\t\t? link->property(kPeerLinkPeerIdProperty).toULongLong()\n\t\t: 0);\n\tif (lnkPhoto || lnkDocument) {\n\t\tif (isUponSelected > 0) {\n\t\t\t_menu->addAction(tr::lng_context_copy_selected(tr::now), [=] {\n\t\t\t\tcopySelectedText();\n\t\t\t}, &st::menuIconCopy);\n\t\t}\n\t\tif (view) {\n\t\t\tFork::AddReplaceMedia(_menu, view->data(), _controller);\n\t\t}\n\t\tif (lnkPhoto) {\n\t\t\tconst auto media = lnkPhoto->activeMediaView();\n\t\t\tif (!lnkPhoto->isNull() && media && media->loaded()) {\n\t\t\t\t_menu->addAction(tr::lng_context_save_image(tr::now), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [=] {\n\t\t\t\t\tsavePhotoToFile(lnkPhoto);\n\t\t\t\t}), &st::menuIconSaveImage);\n\t\t\t\t_menu->addAction(tr::lng_context_copy_image(tr::now), [=] {\n\t\t\t\t\tcopyContextImage(lnkPhoto);\n\t\t\t\t}, &st::menuIconCopy);\n\t\t\t}\n\t\t\tif (lnkPhoto->hasAttachedStickers()) {\n\t\t\t\tconst auto controller = _controller;\n\t\t\t\tauto callback = [=] {\n\t\t\t\t\tauto &attached = session().api().attachedStickers();\n\t\t\t\t\tattached.requestAttachedStickerSets(controller, lnkPhoto);\n\t\t\t\t};\n\t\t\t\t_menu->addAction(\n\t\t\t\t\ttr::lng_context_attached_stickers(tr::now),\n\t\t\t\t\tstd::move(callback),\n\t\t\t\t\t&st::menuIconStickers);\n\t\t\t}\n\t\t} else {\n\t\t\tif (lnkDocument->loading()) {\n\t\t\t\t_menu->addAction(tr::lng_context_cancel_download(tr::now), [=] {\n\t\t\t\t\tcancelContextDownload(lnkDocument);\n\t\t\t\t}, &st::menuIconCancel);\n\t\t\t} else {\n\t\t\t\tconst auto itemId = view\n\t\t\t\t\t? view->data()->fullId()\n\t\t\t\t\t: FullMsgId();\n\t\t\t\tif (const auto item = session().data().message(itemId)) {\n\t\t\t\t\tconst auto notAutoplayedGif = [&] {\n\t\t\t\t\t\treturn lnkDocument->isGifv()\n\t\t\t\t\t\t\t&& !Data::AutoDownload::ShouldAutoPlay(\n\t\t\t\t\t\t\t\tsession().settings().autoDownload(),\n\t\t\t\t\t\t\t\titem->history()->peer,\n\t\t\t\t\t\t\t\tlnkDocument);\n\t\t\t\t\t}();\n\t\t\t\t\tif (notAutoplayedGif) {\n\t\t\t\t\t\t_menu->addAction(tr::lng_context_open_gif(tr::now), [=] {\n\t\t\t\t\t\t\topenContextGif(itemId);\n\t\t\t\t\t\t}, &st::menuIconShowInChat);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!lnkDocument->filepath(true).isEmpty()) {\n\t\t\t\t\t_menu->addAction(Platform::IsMac() ? tr::lng_context_show_in_finder(tr::now) : tr::lng_context_show_in_folder(tr::now), [=] {\n\t\t\t\t\t\tshowContextInFolder(lnkDocument);\n\t\t\t\t\t}, &st::menuIconShowInFolder);\n\t\t\t\t}\n\t\t\t\t_menu->addAction(lnkIsVideo ? tr::lng_context_save_video(tr::now) : (lnkIsVoice ? tr::lng_context_save_audio(tr::now) : (lnkIsAudio ?  tr::lng_context_save_audio_file(tr::now) :  tr::lng_context_save_file(tr::now))), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, lnkDocument] {\n\t\t\t\t\tsaveDocumentToFile(lnkDocument);\n\t\t\t\t}), &st::menuIconDownload);\n\n\t\t\t\tHistoryView::AddCopyFilename(\n\t\t\t\t\t_menu,\n\t\t\t\t\tlnkDocument,\n\t\t\t\t\t[] { return false; });\n\n\t\t\t\tif (lnkDocument->hasAttachedStickers()) {\n\t\t\t\t\tconst auto controller = _controller;\n\t\t\t\t\tauto callback = [=] {\n\t\t\t\t\t\tauto &attached = session().api().attachedStickers();\n\t\t\t\t\t\tattached.requestAttachedStickerSets(controller, lnkDocument);\n\t\t\t\t\t};\n\t\t\t\t\t_menu->addAction(\n\t\t\t\t\t\ttr::lng_context_attached_stickers(tr::now),\n\t\t\t\t\t\tstd::move(callback),\n\t\t\t\t\t\t&st::menuIconStickers);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if (fromId) { // suggest to block\n\t\tif (const auto participant = session().data().peer(fromId)) {\n\t\t\tconst auto item = view ? view->data().get() : nullptr;\n\t\t\tauto realId = FullMsgId();\n\t\t\tif (const auto itemId = item ? item->fullId() : FullMsgId()) {\n\t\t\t\tconst auto it = _realIdsForReport.find(itemId);\n\t\t\t\tif (it != _realIdsForReport.end()) {\n\t\t\t\t\trealId = FullMsgId(_channel->id, it->second);\n\t\t\t\t}\n\t\t\t}\n\t\t\tsuggestRestrictParticipant(participant, realId);\n\t\t}\n\t} else { // maybe cursor on some text history item?\n\t\tconst auto item = view ? view->data().get() : nullptr;\n\t\tconst auto itemId = item ? item->fullId() : FullMsgId();\n\n\t\t_antiSpamValidator.addAction(_menu, itemId);\n\n\t\tif (isUponSelected > 0) {\n\t\t\t_menu->addAction(\n\t\t\t\ttr::lng_context_copy_selected(tr::now),\n\t\t\t\t[this] { copySelectedText(); },\n\t\t\t\t&st::menuIconCopy);\n\t\t\tif (item && !Ui::SkipTranslate(getSelectedText().rich)) {\n\t\t\t\tconst auto peer = item->history()->peer;\n\t\t\t\t_menu->addAction(tr::lng_context_translate_selected({}), [=] {\n\t\t\t\t\t_controller->show(Box(\n\t\t\t\t\t\tUi::TranslateBox,\n\t\t\t\t\t\tpeer,\n\t\t\t\t\t\tMsgId(),\n\t\t\t\t\t\tgetSelectedText().rich,\n\t\t\t\t\t\tfalse));\n\t\t\t\t}, &st::menuIconTranslate);\n\t\t\t}\n\t\t} else {\n\t\t\tif (item && !isUponSelected) {\n\t\t\t\tconst auto media = view->media();\n\t\t\t\tconst auto mediaHasTextForCopy = media && media->hasTextForCopy();\n\t\t\t\tif (const auto document = media ? media->getDocument() : nullptr) {\n\t\t\t\t\tif (document->sticker()) {\n\t\t\t\t\t\t_menu->addAction(tr::lng_context_save_image(tr::now), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, document] {\n\t\t\t\t\t\t\tsaveDocumentToFile(document);\n\t\t\t\t\t\t}), &st::menuIconDownload);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!item->isService()\n\t\t\t\t\t&& !link\n\t\t\t\t\t&& (view->hasVisibleText()\n\t\t\t\t\t\t|| mediaHasTextForCopy\n\t\t\t\t\t\t|| !item->factcheckText().empty()\n\t\t\t\t\t\t|| item->Has<HistoryMessageLogEntryOriginal>())) {\n\t\t\t\t\t_menu->addAction(tr::lng_context_copy_text(tr::now), [=] {\n\t\t\t\t\t\tcopyContextText(itemId);\n\t\t\t\t\t}, &st::menuIconCopy);\n\t\t\t\t}\n\t\t\t\tif (!item->isService() && !Ui::SkipTranslate(item->originalText())) {\n\t\t\t\t\tconst auto peer = item->history()->peer;\n\t\t\t\t\t_menu->addAction(tr::lng_context_translate({}), [=] {\n\t\t\t\t\t\t_controller->show(Box(\n\t\t\t\t\t\t\tUi::TranslateBox,\n\t\t\t\t\t\t\tpeer,\n\t\t\t\t\t\t\tMsgId(),\n\t\t\t\t\t\t\titem->originalText(),\n\t\t\t\t\t\t\tfalse));\n\t\t\t\t\t}, &st::menuIconTranslate);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst auto actionText = link\n\t\t\t? link->copyToClipboardContextItemText()\n\t\t\t: QString();\n\t\tif (!actionText.isEmpty()) {\n\t\t\t_menu->addAction(\n\t\t\t\tactionText,\n\t\t\t\t[text = link->copyToClipboardText()] {\n\t\t\t\t\tQGuiApplication::clipboard()->setText(text);\n\t\t\t\t},\n\t\t\t\t&st::menuIconCopy);\n\t\t}\n\t}\n\n\tif (_menu->empty()) {\n\t\t_menu = nullptr;\n\t} else {\n\t\t_menu->popup(e->globalPos());\n\t\te->accept();\n\t}\n}\n\nvoid InnerWidget::savePhotoToFile(not_null<PhotoData*> photo) {\n\tconst auto media = photo->activeMediaView();\n\tif (photo->isNull() || !media || !media->loaded()) {\n\t\treturn;\n\t}\n\n\tauto filter = u\"JPEG Image (*.jpg);;\"_q + FileDialog::AllFilesFilter();\n\tFileDialog::GetWritePath(\n\t\tthis,\n\t\ttr::lng_save_photo(tr::now),\n\t\tfilter,\n\t\tfiledialogDefaultName(u\"photo\"_q, u\".jpg\"_q),\n\t\tcrl::guard(this, [=](const QString &result) {\n\t\t\tif (!result.isEmpty()) {\n\t\t\t\tmedia->saveToFile(result);\n\t\t\t}\n\t\t}));\n}\n\nvoid InnerWidget::saveDocumentToFile(not_null<DocumentData*> document) {\n\tDocumentSaveClickHandler::Save(\n\t\tData::FileOrigin(),\n\t\tdocument,\n\t\tDocumentSaveClickHandler::Mode::ToNewFile);\n}\n\nvoid InnerWidget::copyContextImage(not_null<PhotoData*> photo) {\n\tconst auto media = photo->activeMediaView();\n\tif (photo->isNull() || !media || !media->loaded()) {\n\t\treturn;\n\t}\n\tmedia->setToClipboard();\n}\n\nvoid InnerWidget::copySelectedText() {\n\tTextUtilities::SetClipboardText(getSelectedText());\n}\n\nvoid InnerWidget::showStickerPackInfo(not_null<DocumentData*> document) {\n\tStickerSetBox::Show(_controller->uiShow(), document);\n}\n\nvoid InnerWidget::cancelContextDownload(not_null<DocumentData*> document) {\n\tdocument->cancel();\n}\n\nvoid InnerWidget::showContextInFolder(not_null<DocumentData*> document) {\n\tconst auto filepath = document->filepath(true);\n\tif (!filepath.isEmpty()) {\n\t\tFile::ShowInFolder(filepath);\n\t}\n}\n\nvoid InnerWidget::openContextGif(FullMsgId itemId) {\n\tif (const auto item = session().data().message(itemId)) {\n\t\tif (const auto media = item->media()) {\n\t\t\tif (const auto document = media->document()) {\n\t\t\t\t_controller->openDocument(document, true, { itemId });\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid InnerWidget::copyContextText(FullMsgId itemId) {\n\tif (const auto item = session().data().message(itemId)) {\n\t\tTextUtilities::SetClipboardText(HistoryItemText(item));\n\t}\n}\n\nvoid InnerWidget::suggestRestrictParticipant(\n\t\tnot_null<PeerData*> participant,\n\t\tFullMsgId realId) {\n\tExpects(_menu != nullptr);\n\n\tif (!canRestrict()) {\n\t\treturn;\n\t}\n\tif (ranges::contains(_admins, participant)) {\n\t\tif (!ranges::contains(_adminsCanEdit, participant)) {\n\t\t\treturn;\n\t\t}\n\t}\n\t_menu->addAction(tr::lng_context_restrict_user(tr::now), [=] {\n\t\tconst auto user = participant->asUser();\n\t\tauto editRestrictions = [=](\n\t\t\t\tbool hasAdminRights,\n\t\t\t\tChatRestrictionsInfo currentRights,\n\t\t\t\tUserData *by,\n\t\t\t\tTimeId since) {\n\t\t\tauto weak = QPointer<InnerWidget>(this);\n\t\t\tauto weakBox = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\t\t\tauto box = Box<EditRestrictedBox>(\n\t\t\t\t_channel,\n\t\t\t\tuser,\n\t\t\t\thasAdminRights,\n\t\t\t\tcurrentRights,\n\t\t\t\tQString(),\n\t\t\t\tby,\n\t\t\t\tsince);\n\t\t\tbox->setSaveCallback([=](\n\t\t\t\t\tChatRestrictionsInfo oldRights,\n\t\t\t\t\tChatRestrictionsInfo newRights) {\n\t\t\t\tif (weak) {\n\t\t\t\t\tweak->restrictParticipant(\n\t\t\t\t\t\tparticipant,\n\t\t\t\t\t\toldRights,\n\t\t\t\t\t\tnewRights);\n\t\t\t\t}\n\t\t\t\tif (*weakBox) {\n\t\t\t\t\t(*weakBox)->closeBox();\n\t\t\t\t}\n\t\t\t});\n\t\t\t*weakBox = _controller->show(std::move(box));\n\t\t};\n\t\tif (!user) {\n\t\t\tconst auto text = (_channel->isBroadcast()\n\t\t\t\t? tr::lng_profile_sure_kick_channel\n\t\t\t\t: tr::lng_profile_sure_kick)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tparticipant->name());\n\t\t\tauto weakBox = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\t\t\tconst auto sure = crl::guard(this, [=] {\n\t\t\t\trestrictParticipant(\n\t\t\t\t\tparticipant,\n\t\t\t\t\tChatRestrictionsInfo(),\n\t\t\t\t\tChannelData::KickedRestrictedRights(participant));\n\t\t\t\tif (*weakBox) {\n\t\t\t\t\t(*weakBox)->closeBox();\n\t\t\t\t}\n\t\t\t});\n\t\t\t*weakBox = _controller->show(Ui::MakeConfirmBox({ text, sure }));\n\t\t} else if (base::contains(_admins, user)) {\n\t\t\teditRestrictions(true, {}, nullptr, 0);\n\t\t} else {\n\t\t\t_api.request(MTPchannels_GetParticipant(\n\t\t\t\t_channel->inputChannel(),\n\t\t\t\tuser->input()\n\t\t\t)).done([=](const MTPchannels_ChannelParticipant &result) {\n\t\t\t\tuser->owner().processUsers(result.data().vusers());\n\n\t\t\t\tconst auto participant = Api::ChatParticipant(\n\t\t\t\t\tresult.data().vparticipant(),\n\t\t\t\t\tuser);\n\t\t\t\tusing Type = Api::ChatParticipant::Type;\n\t\t\t\tif (participant.type() == Type::Creator\n\t\t\t\t\t|| participant.type() == Type::Admin) {\n\t\t\t\t\teditRestrictions(true, {}, nullptr, 0);\n\t\t\t\t} else {\n\t\t\t\t\tconst auto since = participant.restrictedSince();\n\t\t\t\t\teditRestrictions(\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\tparticipant.restrictions(),\n\t\t\t\t\t\tuser->owner().user(participant.by()),\n\t\t\t\t\t\tsince);\n\t\t\t\t}\n\t\t\t}).fail([=] {\n\t\t\t\teditRestrictions(false, {}, nullptr, 0);\n\t\t\t}).send();\n\t\t}\n\t}, &st::menuIconPermissions);\n\n\t{\n\t\tconst auto lifetime = std::make_shared<rpl::lifetime>();\n\t\tauto handler = [=, this] {\n\t\t\tparticipant->session().changes().peerUpdates(\n\t\t\t\t_channel,\n\t\t\t\tData::PeerUpdate::Flag::Members\n\t\t\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\t\t\t_downLoaded = false;\n\t\t\t\tpreloadMore(Direction::Down);\n\t\t\t\tlifetime->destroy();\n\t\t\t}, *lifetime);\n\t\t\tparticipant->session().api().chatParticipants().kick(\n\t\t\t\t_channel,\n\t\t\t\tparticipant,\n\t\t\t\t{ _channel->restrictions(), 0 });\n\t\t};\n\t\tconst auto addAction = Ui::Menu::CreateAddActionCallback(_menu);\n\t\taddAction({\n\t\t\t.text = tr::lng_context_ban_user(tr::now),\n\t\t\t.handler = handler,\n\t\t\t.icon = &st::menuIconBlockAttention,\n\t\t\t.isAttention = true,\n\t\t});\n\n\t\tif (realId) {\n\t\t\taddAction({\n\t\t\t\t.text = tr::lng_report_and_ban(tr::now),\n\t\t\t\t.handler = [=, show = _controller->uiShow()] {\n\t\t\t\t\tApi::ReportSpam(participant, { realId });\n\t\t\t\t\thandler();\n\t\t\t\t\tshow->showToast(tr::lng_report_spam_done(tr::now));\n\t\t\t\t},\n\t\t\t\t.icon = &st::menuIconReportAttention,\n\t\t\t\t.isAttention = true,\n\t\t\t});\n\t\t}\n\t}\n}\n\nvoid InnerWidget::restrictParticipant(\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo oldRights,\n\t\tChatRestrictionsInfo newRights) {\n\tconst auto done = [=](ChatRestrictionsInfo newRights) {\n\t\trestrictParticipantDone(participant, newRights);\n\t};\n\tconst auto callback = SaveRestrictedCallback(\n\t\t_controller->uiShow(),\n\t\t_channel,\n\t\tparticipant,\n\t\tcrl::guard(this, done),\n\t\tnullptr);\n\tcallback(oldRights, newRights);\n}\n\nvoid InnerWidget::restrictParticipantDone(\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo rights) {\n\tif (rights.flags) {\n\t\t_admins.erase(\n\t\t\tstd::remove(_admins.begin(), _admins.end(), participant),\n\t\t\t_admins.end());\n\t\t_adminsCanEdit.erase(\n\t\t\tstd::remove(\n\t\t\t\t_adminsCanEdit.begin(),\n\t\t\t\t_adminsCanEdit.end(),\n\t\t\t\tparticipant),\n\t\t\t_adminsCanEdit.end());\n\t}\n\t_downLoaded = false;\n\tcheckPreloadMore();\n}\n\nbool InnerWidget::canRestrict() const {\n\treturn _channel->isMegagroup()\n\t\t&& _channel->canBanMembers()\n\t\t&& !_admins.empty();\n}\n\nvoid InnerWidget::mousePressEvent(QMouseEvent *e) {\n\tif (_menu) {\n\t\te->accept();\n\t\treturn; // ignore mouse press, that was hiding context menu\n\t}\n\tmouseActionStart(e->globalPos(), e->button());\n}\n\nvoid InnerWidget::mouseMoveEvent(QMouseEvent *e) {\n\tauto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton));\n\tif (!buttonsPressed && _mouseAction != MouseAction::None) {\n\t\tmouseReleaseEvent(e);\n\t}\n\tmouseActionUpdate(e->globalPos());\n}\n\nvoid InnerWidget::mouseReleaseEvent(QMouseEvent *e) {\n\tmouseActionFinish(e->globalPos(), e->button());\n\tif (!rect().contains(e->pos())) {\n\t\tleaveEvent(e);\n\t}\n}\n\nvoid InnerWidget::enterEventHook(QEnterEvent *e) {\n\tmouseActionUpdate(QCursor::pos());\n\treturn RpWidget::enterEventHook(e);\n}\n\nvoid InnerWidget::leaveEventHook(QEvent *e) {\n\tif (const auto view = Element::Hovered()) {\n\t\trepaintItem(view);\n\t\tElement::Hovered(nullptr);\n\t}\n\tClickHandler::clearActive();\n\tUi::Tooltip::Hide();\n\tif (!ClickHandler::getPressed() && _cursor != style::cur_default) {\n\t\t_cursor = style::cur_default;\n\t\tsetCursor(_cursor);\n\t}\n\treturn RpWidget::leaveEventHook(e);\n}\n\nvoid InnerWidget::mouseActionStart(const QPoint &screenPos, Qt::MouseButton button) {\n\tmouseActionUpdate(screenPos);\n\tif (button != Qt::LeftButton) return;\n\n\tClickHandler::pressed();\n\tif (Element::Pressed() != Element::Hovered()) {\n\t\trepaintItem(Element::Pressed());\n\t\tElement::Pressed(Element::Hovered());\n\t\trepaintItem(Element::Pressed());\n\t}\n\n\t_mouseAction = MouseAction::None;\n\t_mouseActionItem = Element::Moused();\n\t_dragStartPosition = mapPointToItem(\n\t\tmapFromGlobal(screenPos),\n\t\t_mouseActionItem);\n\t_pressWasInactive = Ui::WasInactivePress(_controller->widget());\n\tif (_pressWasInactive) {\n\t\tUi::MarkInactivePress(_controller->widget(), false);\n\t}\n\n\tif (ClickHandler::getPressed()) {\n\t\t_mouseAction = MouseAction::PrepareDrag;\n\t}\n\tif (_mouseAction == MouseAction::None && _mouseActionItem) {\n\t\tTextState dragState;\n\t\tif (_trippleClickTimer.isActive() && (screenPos - _trippleClickPoint).manhattanLength() < QApplication::startDragDistance()) {\n\t\t\tStateRequest request;\n\t\t\trequest.flags = Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t\tdragState = _mouseActionItem->textState(_dragStartPosition, request);\n\t\t\tif (dragState.cursor == CursorState::Text) {\n\t\t\t\tauto selection = TextSelection { dragState.symbol, dragState.symbol };\n\t\t\t\trepaintItem(std::exchange(_selectedItem, _mouseActionItem));\n\t\t\t\t_selectedText = selection;\n\t\t\t\t_mouseTextSymbol = dragState.symbol;\n\t\t\t\t_mouseAction = MouseAction::Selecting;\n\t\t\t\t_mouseSelectType = TextSelectType::Paragraphs;\n\t\t\t\tmouseActionUpdate(_mousePosition);\n\t\t\t\t_trippleClickTimer.callOnce(QApplication::doubleClickInterval());\n\t\t\t}\n\t\t} else if (Element::Pressed()) {\n\t\t\tStateRequest request;\n\t\t\trequest.flags = Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t\tdragState = _mouseActionItem->textState(_dragStartPosition, request);\n\t\t}\n\t\tif (_mouseSelectType != TextSelectType::Paragraphs) {\n\t\t\tif (Element::Pressed()) {\n\t\t\t\t_mouseTextSymbol = dragState.symbol;\n\t\t\t\tauto uponSelected = (dragState.cursor == CursorState::Text);\n\t\t\t\tif (uponSelected) {\n\t\t\t\t\tif (!_selectedItem || _selectedItem != _mouseActionItem) {\n\t\t\t\t\t\tuponSelected = false;\n\t\t\t\t\t} else if (_mouseTextSymbol < _selectedText.from || _mouseTextSymbol >= _selectedText.to) {\n\t\t\t\t\t\tuponSelected = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (uponSelected) {\n\t\t\t\t\t_mouseAction = MouseAction::PrepareDrag; // start text drag\n\t\t\t\t} else if (!_pressWasInactive) {\n\t\t\t\t\tif (dragState.afterSymbol) ++_mouseTextSymbol;\n\t\t\t\t\tauto selection = TextSelection { _mouseTextSymbol, _mouseTextSymbol };\n\t\t\t\t\trepaintItem(std::exchange(_selectedItem, _mouseActionItem));\n\t\t\t\t\t_selectedText = selection;\n\t\t\t\t\t_mouseAction = MouseAction::Selecting;\n\t\t\t\t\trepaintItem(_mouseActionItem);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!_mouseActionItem) {\n\t\t_mouseAction = MouseAction::None;\n\t} else if (_mouseAction == MouseAction::None) {\n\t\t_mouseActionItem = nullptr;\n\t}\n}\n\nvoid InnerWidget::mouseActionUpdate(const QPoint &screenPos) {\n\t_mousePosition = screenPos;\n\tupdateSelected();\n}\n\nvoid InnerWidget::mouseActionCancel() {\n\t_mouseActionItem = nullptr;\n\t_mouseAction = MouseAction::None;\n\t_dragStartPosition = QPoint(0, 0);\n\t_wasSelectedText = false;\n\t//_widget->noSelectingScroll(); // TODO\n}\n\nvoid InnerWidget::mouseActionFinish(const QPoint &screenPos, Qt::MouseButton button) {\n\tmouseActionUpdate(screenPos);\n\n\tauto activated = ClickHandler::unpressed();\n\tif (_mouseAction == MouseAction::Dragging) {\n\t\tactivated = nullptr;\n\t}\n\tif (const auto view = Element::Pressed()) {\n\t\trepaintItem(view);\n\t\tElement::Pressed(nullptr);\n\t}\n\n\t_wasSelectedText = false;\n\n\tif (activated) {\n\t\tmouseActionCancel();\n\t\tActivateClickHandler(window(), activated, {\n\t\t\tbutton,\n\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.elementDelegate = [weak = base::make_weak(this)] {\n\t\t\t\t\treturn (ElementDelegate*)weak.get();\n\t\t\t\t},\n\t\t\t\t.sessionWindow = base::make_weak(_controller),\n\t\t\t})\n\t\t});\n\t\treturn;\n\t}\n\tif (_mouseAction == MouseAction::PrepareDrag && !_pressWasInactive && button != Qt::RightButton) {\n\t\trepaintItem(base::take(_selectedItem));\n\t} else if (_mouseAction == MouseAction::Selecting) {\n\t\tif (_selectedItem && !_pressWasInactive) {\n\t\t\tif (_selectedText.from == _selectedText.to) {\n\t\t\t\t_selectedItem = nullptr;\n\t\t\t\t_controller->widget()->setInnerFocus();\n\t\t\t}\n\t\t}\n\t}\n\t_mouseAction = MouseAction::None;\n\t_mouseActionItem = nullptr;\n\t_mouseSelectType = TextSelectType::Letters;\n\t//_widget->noSelectingScroll(); // TODO\n\n\tif (QGuiApplication::clipboard()->supportsSelection()\n\t\t&& _selectedItem\n\t\t&& _selectedText.from != _selectedText.to) {\n\t\tTextUtilities::SetClipboardText(\n\t\t\t_selectedItem->selectedText(_selectedText),\n\t\t\tQClipboard::Selection);\n\t}\n}\n\nvoid InnerWidget::updateSelected() {\n\tauto mousePosition = mapFromGlobal(_mousePosition);\n\tauto point = QPoint(\n\t\tstd::clamp(mousePosition.x(), 0, width()),\n\t\tstd::clamp(mousePosition.y(), _visibleTop, _visibleBottom));\n\n\tauto itemPoint = QPoint();\n\tauto begin = std::rbegin(_items), end = std::rend(_items);\n\tauto from = (point.y() >= _itemsTop && point.y() < _itemsTop + _itemsHeight)\n\t\t? std::lower_bound(begin, end, point.y(), [this](auto &elem, int top) {\n\t\t\treturn this->itemTop(elem) + elem->height() <= top;\n\t\t})\n\t\t: end;\n\tconst auto view = (from != end) ? from->get() : nullptr;\n\tconst auto item = view ? view->data().get() : nullptr;\n\tif (item) {\n\t\tElement::Moused(view);\n\t\titemPoint = mapPointToItem(point, view);\n\t\tif (view->pointState(itemPoint) != PointState::Outside) {\n\t\t\tif (Element::Hovered() != view) {\n\t\t\t\trepaintItem(Element::Hovered());\n\t\t\t\tElement::Hovered(view);\n\t\t\t\trepaintItem(view);\n\t\t\t}\n\t\t} else if (const auto view = Element::Hovered()) {\n\t\t\trepaintItem(view);\n\t\t\tElement::Hovered(nullptr);\n\t\t}\n\t}\n\n\tTextState dragState;\n\tClickHandlerHost *lnkhost = nullptr;\n\tauto selectingText = _selectedItem\n\t\t&& (view == _mouseActionItem)\n\t\t&& (view == Element::Hovered());\n\tif (view) {\n\t\tif (view != _mouseActionItem || (itemPoint - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) {\n\t\t\tif (_mouseAction == MouseAction::PrepareDrag) {\n\t\t\t\t_mouseAction = MouseAction::Dragging;\n\t\t\t\tInvokeQueued(this, [this] { performDrag(); });\n\t\t\t}\n\t\t}\n\t\tStateRequest request;\n\t\tif (_mouseAction == MouseAction::Selecting) {\n\t\t\trequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t} else {\n\t\t\tselectingText = false;\n\t\t}\n\t\tif (base::IsAltPressed()) {\n\t\t\trequest.flags &= ~Ui::Text::StateRequest::Flag::LookupLink;\n\t\t}\n\t\tdragState = view->textState(itemPoint, request);\n\t\tlnkhost = view;\n\t\tif (item->isService()) {\n\t\t\tdragState.cursor = CursorState::LogAdminService;\n\t\t}\n\t\tif (!dragState.link && itemPoint.x() >= st::historyPhotoLeft && itemPoint.x() < st::historyPhotoLeft + st::msgPhotoSize) {\n\t\t\tif (!item->isService() && view->hasFromPhoto()) {\n\t\t\t\tenumerateUserpics([&](not_null<Element*> view, int userpicTop) {\n\t\t\t\t\t// stop enumeration if the userpic is below our point\n\t\t\t\t\tif (userpicTop > point.y()) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\n\t\t\t\t\t// stop enumeration if we've found a userpic under the cursor\n\t\t\t\t\tif (point.y() >= userpicTop && point.y() < userpicTop + st::msgPhotoSize) {\n\t\t\t\t\t\tdragState.link = view->data()->from()->openLink();\n\t\t\t\t\t\tlnkhost = view;\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\tauto lnkChanged = ClickHandler::setActive(dragState.link, lnkhost);\n\tif (lnkChanged || dragState.cursor != _mouseCursorState) {\n\t\tUi::Tooltip::Hide();\n\t}\n\tif (dragState.link\n\t\t|| dragState.cursor == CursorState::Date\n\t\t|| dragState.cursor == CursorState::Forwarded\n\t\t|| dragState.cursor == CursorState::LogAdminService) {\n\t\tUi::Tooltip::Show(1000, this);\n\t}\n\n\tauto cursor = style::cur_default;\n\tif (_mouseAction == MouseAction::None) {\n\t\t_mouseCursorState = dragState.cursor;\n\t\tif (dragState.link) {\n\t\t\tcursor = style::cur_pointer;\n\t\t} else if (_mouseCursorState == CursorState::Text) {\n\t\t\tcursor = style::cur_text;\n\t\t} else if (_mouseCursorState == CursorState::Date) {\n//\t\t\tcursor = style::cur_cross;\n\t\t}\n\t} else if (item) {\n\t\tif (_mouseAction == MouseAction::Selecting) {\n\t\t\tif (selectingText) {\n\t\t\t\tauto second = dragState.symbol;\n\t\t\t\tif (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) {\n\t\t\t\t\t++second;\n\t\t\t\t}\n\t\t\t\tauto selection = TextSelection { qMin(second, _mouseTextSymbol), qMax(second, _mouseTextSymbol) };\n\t\t\t\tif (_mouseSelectType != TextSelectType::Letters) {\n\t\t\t\t\tselection = _mouseActionItem->adjustSelection(\n\t\t\t\t\t\tselection,\n\t\t\t\t\t\t_mouseSelectType);\n\t\t\t\t}\n\t\t\t\tif (_selectedText != selection) {\n\t\t\t\t\t_selectedText = selection;\n\t\t\t\t\trepaintItem(_mouseActionItem);\n\t\t\t\t}\n\t\t\t\tif (!_wasSelectedText && (selection.from != selection.to)) {\n\t\t\t\t\t_wasSelectedText = true;\n\t\t\t\t\tsetFocus();\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (_mouseAction == MouseAction::Dragging) {\n\t\t}\n\n\t\tif (ClickHandler::getPressed()) {\n\t\t\tcursor = style::cur_pointer;\n\t\t} else if (_mouseAction == MouseAction::Selecting && _selectedItem) {\n\t\t\tcursor = style::cur_text;\n\t\t}\n\t}\n\n\t// Voice message seek support.\n\tif (const auto pressedView = Element::PressedLink()) {\n\t\tconst auto adjustedPoint = mapPointToItem(point, pressedView);\n\t\tpressedView->updatePressed(adjustedPoint);\n\t}\n\n\t//if (_mouseAction == MouseAction::Selecting) {\n\t//\t_widget->checkSelectingScroll(mousePos);\n\t//} else {\n\t//\t_widget->noSelectingScroll();\n\t//} // TODO\n\n\tif (_mouseAction == MouseAction::None && (lnkChanged || cursor != _cursor)) {\n\t\tsetCursor(_cursor = cursor);\n\t}\n}\n\nvoid InnerWidget::performDrag() {\n\tif (_mouseAction != MouseAction::Dragging) return;\n\n\t//auto uponSelected = false;\n\t//if (_mouseActionItem) {\n\t//\tif (!_selected.isEmpty() && _selected.cbegin().value() == FullSelection) {\n\t//\t\tuponSelected = _selected.contains(_mouseActionItem);\n\t//\t} else {\n\t//\t\tStateRequest request;\n\t//\t\trequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;\n\t//\t\tauto dragState = _mouseActionItem->textState(_dragStartPosition.x(), _dragStartPosition.y(), request);\n\t//\t\tuponSelected = (dragState.cursor == CursorState::Text);\n\t//\t\tif (uponSelected) {\n\t//\t\t\tif (_selected.isEmpty() ||\n\t//\t\t\t\t_selected.cbegin().value() == FullSelection ||\n\t//\t\t\t\t_selected.cbegin().key() != _mouseActionItem\n\t//\t\t\t\t) {\n\t//\t\t\t\tuponSelected = false;\n\t//\t\t\t} else {\n\t//\t\t\t\tuint16 selFrom = _selected.cbegin().value().from, selTo = _selected.cbegin().value().to;\n\t//\t\t\t\tif (dragState.symbol < selFrom || dragState.symbol >= selTo) {\n\t//\t\t\t\t\tuponSelected = false;\n\t//\t\t\t\t}\n\t//\t\t\t}\n\t//\t\t}\n\t//\t}\n\t//}\n\t//auto pressedHandler = ClickHandler::getPressed();\n\n\t//if (dynamic_cast<VoiceSeekClickHandler*>(pressedHandler.data())) {\n\t//\treturn;\n\t//}\n\n\t//TextWithEntities sel;\n\t//QList<QUrl> urls;\n\t//if (uponSelected) {\n\t//\tsel = getSelectedText();\n\t//} else if (pressedHandler) {\n\t//\tsel = { pressedHandler->dragText(), EntitiesInText() };\n\t//\t//if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') {\n\t//\t//\turls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o\n\t//\t//}\n\t//}\n\t//if (auto mimeData = mimeDataFromTextWithEntities(sel)) {\n\t//\tupdateDragSelection(0, 0, false);\n\t//\t_widget->noSelectingScroll();\n\n\t//\tif (!urls.isEmpty()) mimeData->setUrls(urls);\n\t//\tif (uponSelected && !Adaptive::OneColumn()) {\n\t//\t\tauto selectedState = getSelectionState();\n\t//\t\tif (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {\n\t//\t\t\tsession().data().setMimeForwardIds(getSelectedItems());\n\t//\t\t\tmimeData->setData(u\"application/x-td-forward\"_q, \"1\");\n\t//\t\t}\n\t//\t}\n\t//\t_controller->window()->launchDrag(std::move(mimeData));\n\t//\treturn;\n\t//} else {\n\t//\tauto forwardMimeType = QString();\n\t//\tauto pressedMedia = static_cast<HistoryView::Media*>(nullptr);\n\t//\tif (auto pressedItem = Element::Pressed()) {\n\t//\t\tpressedMedia = pressedItem->media();\n\t//\t\tif (_mouseCursorState == CursorState::Date) {\n\t//\t\t\tforwardMimeType = u\"application/x-td-forward\"_q;\n\t//\t\t\tsession().data().setMimeForwardIds(\n\t//\t\t\t\tsession().data().itemOrItsGroup(pressedItem->data()));\n\t//\t\t}\n\t//\t}\n\t//\tif (auto pressedLnkItem = Element::PressedLink()) {\n\t//\t\tif ((pressedMedia = pressedLnkItem->media())) {\n\t//\t\t\tif (forwardMimeType.isEmpty()\n\t//\t\t\t\t&& pressedMedia->dragItemByHandler(pressedHandler)) {\n\t//\t\t\t\tforwardMimeType = u\"application/x-td-forward\"_q;\n\t//\t\t\t\tsession().data().setMimeForwardIds(\n\t//\t\t\t\t\t{ 1, pressedLnkItem->fullId() });\n\t//\t\t\t}\n\t//\t\t}\n\t//\t}\n\t//\tif (!forwardMimeType.isEmpty()) {\n\t//\t\tauto mimeData = std::make_unique<QMimeData>();\n\t//\t\tmimeData->setData(forwardMimeType, \"1\");\n\t//\t\tif (auto document = (pressedMedia ? pressedMedia->getDocument() : nullptr)) {\n\t//\t\t\tauto filepath = document->filepath(true);\n\t//\t\t\tif (!filepath.isEmpty()) {\n\t//\t\t\t\tQList<QUrl> urls;\n\t//\t\t\t\turls.push_back(QUrl::fromLocalFile(filepath));\n\t//\t\t\t\tmimeData->setUrls(urls);\n\t//\t\t\t}\n\t//\t\t}\n\n\t//\t\t// This call enters event loop and can destroy any QObject.\n\t//\t\t_controller->window()->launchDrag(std::move(mimeData));\n\t//\t\treturn;\n\t//\t}\n\t//} // TODO\n}\n\nint InnerWidget::itemTop(not_null<const Element*> view) const {\n\treturn _itemsTop + view->y();\n}\n\nvoid InnerWidget::repaintItem(const Element *view) {\n\tif (!view) {\n\t\treturn;\n\t}\n\tconst auto top = itemTop(view);\n\tconst auto range = view->verticalRepaintRange();\n\tupdate(0, top + range.top, width(), range.height);\n}\n\nvoid InnerWidget::repaintItem(const Element *view, QRect rect) {\n\tif (rect.isNull()) {\n\t\treturn repaintItem(view);\n\t}\n\tif (!view) {\n\t\treturn;\n\t}\n\tconst auto top = itemTop(view);\n\tupdate(rect.translated(0, top));\n}\n\nvoid InnerWidget::resizeItem(not_null<Element*> view) {\n\tupdateSize();\n}\n\nvoid InnerWidget::refreshItem(not_null<const Element*> view) {\n\t// No need to refresh views in admin log.\n}\n\nQPoint InnerWidget::mapPointToItem(QPoint point, const Element *view) const {\n\tif (!view) {\n\t\treturn QPoint();\n\t}\n\treturn point - QPoint(0, itemTop(view));\n}\n\nbool InnerWidget::eventHook(QEvent *e) {\n\tif (e->type() == QEvent::TouchBegin\n\t\t|| e->type() == QEvent::TouchUpdate\n\t\t|| e->type() == QEvent::TouchEnd\n\t\t|| e->type() == QEvent::TouchCancel) {\n\t\tQTouchEvent *ev = static_cast<QTouchEvent*>(e);\n\t\tif (ev->device()->type() == base::TouchDevice::TouchScreen) {\n\t\t\ttouchEvent(ev);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn RpWidget::eventHook(e);\n}\n\nvoid InnerWidget::onTouchSelect() {\n\t_touchSelect = true;\n\tmouseActionStart(_touchPos, Qt::LeftButton);\n}\n\nvoid InnerWidget::onTouchScrollTimer() {\n\tauto nowTime = crl::now();\n\tif (_touchScrollState == Ui::TouchScrollState::Acceleration\n\t\t&& _touchWaitingAcceleration\n\t\t&& (nowTime - _touchAccelerationTime) > 40) {\n\t\t_touchScrollState = Ui::TouchScrollState::Manual;\n\t\ttouchResetSpeed();\n\t} else if (_touchScrollState == Ui::TouchScrollState::Auto\n\t\t|| _touchScrollState == Ui::TouchScrollState::Acceleration) {\n\t\tint32 elapsed = int32(nowTime - _touchTime);\n\t\tQPoint delta = _touchSpeed * elapsed / 1000;\n\t\tbool hasScrolled = !delta.isNull();\n\t\tif (hasScrolled) {\n\t\t\t_scrollToSignal.fire_copy(_visibleTop - delta.y());\n\t\t}\n\n\t\tif (_touchSpeed.isNull() || !hasScrolled) {\n\t\t\t_touchScrollState = Ui::TouchScrollState::Manual;\n\t\t\t_touchScroll = false;\n\t\t\t_touchScrollTimer.cancel();\n\t\t} else {\n\t\t\t_touchTime = nowTime;\n\t\t}\n\t\ttouchDeaccelerate(elapsed);\n\t}\n}\n\nvoid InnerWidget::touchUpdateSpeed() {\n\tconst auto nowTime = crl::now();\n\tif (_touchPrevPosValid) {\n\t\tconst int elapsed = nowTime - _touchSpeedTime;\n\t\tif (elapsed) {\n\t\t\tconst QPoint newPixelDiff = (_touchPos - _touchPrevPos);\n\t\t\tconst QPoint pixelsPerSecond = newPixelDiff * (1000 / elapsed);\n\n\t\t\tconst int newSpeedY = (qAbs(pixelsPerSecond.y())\n\t\t\t\t\t> Ui::kFingerAccuracyThreshold)\n\t\t\t\t? pixelsPerSecond.y()\n\t\t\t\t: 0;\n\t\t\tconst int newSpeedX = (qAbs(pixelsPerSecond.x())\n\t\t\t\t\t> Ui::kFingerAccuracyThreshold)\n\t\t\t\t? pixelsPerSecond.x()\n\t\t\t\t: 0;\n\t\t\tif (_touchScrollState == Ui::TouchScrollState::Auto) {\n\t\t\t\tconst int oldSpeedY = _touchSpeed.y();\n\t\t\t\tconst int oldSpeedX = _touchSpeed.x();\n\t\t\t\tif ((oldSpeedY <= 0 && newSpeedY <= 0) || ((oldSpeedY >= 0 && newSpeedY >= 0)\n\t\t\t\t\t&& (oldSpeedX <= 0 && newSpeedX <= 0)) || (oldSpeedX >= 0 && newSpeedX >= 0)) {\n\t\t\t\t\t_touchSpeed.setY(std::clamp(\n\t\t\t\t\t\t(oldSpeedY + (newSpeedY / 4)),\n\t\t\t\t\t\t-Ui::kMaxScrollAccelerated,\n\t\t\t\t\t\t+Ui::kMaxScrollAccelerated));\n\t\t\t\t\t_touchSpeed.setX(std::clamp(\n\t\t\t\t\t\t(oldSpeedX + (newSpeedX / 4)),\n\t\t\t\t\t\t-Ui::kMaxScrollAccelerated,\n\t\t\t\t\t\t+Ui::kMaxScrollAccelerated));\n\t\t\t\t} else {\n\t\t\t\t\t_touchSpeed = QPoint();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (!_touchSpeed.isNull()) {\n\t\t\t\t\t_touchSpeed.setX(std::clamp(\n\t\t\t\t\t\t(_touchSpeed.x() / 4) + (newSpeedX * 3 / 4),\n\t\t\t\t\t\t-Ui::kMaxScrollFlick,\n\t\t\t\t\t\t+Ui::kMaxScrollFlick));\n\t\t\t\t\t_touchSpeed.setY(std::clamp(\n\t\t\t\t\t\t(_touchSpeed.y() / 4) + (newSpeedY * 3 / 4),\n\t\t\t\t\t\t-Ui::kMaxScrollFlick,\n\t\t\t\t\t\t+Ui::kMaxScrollFlick));\n\t\t\t\t} else {\n\t\t\t\t\t_touchSpeed = QPoint(newSpeedX, newSpeedY);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\t_touchPrevPosValid = true;\n\t}\n\t_touchSpeedTime = nowTime;\n\t_touchPrevPos = _touchPos;\n}\n\nvoid InnerWidget::touchResetSpeed() {\n\t_touchSpeed = QPoint();\n\t_touchPrevPosValid = false;\n}\n\nvoid InnerWidget::touchDeaccelerate(int32 elapsed) {\n\tint32 x = _touchSpeed.x();\n\tint32 y = _touchSpeed.y();\n\t_touchSpeed.setX((x == 0)\n\t\t? x\n\t\t: (x > 0)\n\t\t? qMax(0, x - elapsed)\n\t\t: qMin(0, x + elapsed));\n\t_touchSpeed.setY((y == 0)\n\t\t? y\n\t\t: (y > 0)\n\t\t? qMax(0, y - elapsed)\n\t\t: qMin(0, y + elapsed));\n}\n\nvoid InnerWidget::touchEvent(QTouchEvent *e) {\n\tif (e->type() == QEvent::TouchCancel) {\n\t\tif (!_touchInProgress) {\n\t\t\treturn;\n\t\t}\n\t\t_touchInProgress = false;\n\t\t_touchSelectTimer.cancel();\n\t\t_touchScroll = _touchSelect = false;\n\t\t_touchScrollState = Ui::TouchScrollState::Manual;\n\t\tmouseActionCancel();\n\t\treturn;\n\t}\n\n\tif (!e->touchPoints().isEmpty()) {\n\t\t_touchPrevPos = _touchPos;\n\t\t_touchPos = e->touchPoints().cbegin()->screenPos().toPoint();\n\t}\n\n\tswitch (e->type()) {\n\tcase QEvent::TouchBegin: {\n\t\tif (_menu) {\n\t\t\te->accept();\n\t\t\treturn;\n\t\t}\n\t\tif (_touchInProgress || e->touchPoints().isEmpty()) {\n\t\t\treturn;\n\t\t}\n\n\t\t_touchInProgress = true;\n\t\tif (_touchScrollState == Ui::TouchScrollState::Auto) {\n\t\t\t_touchScrollState = Ui::TouchScrollState::Acceleration;\n\t\t\t_touchWaitingAcceleration = true;\n\t\t\t_touchAccelerationTime = crl::now();\n\t\t\ttouchUpdateSpeed();\n\t\t\t_touchStart = _touchPos;\n\t\t} else {\n\t\t\t_touchScroll = false;\n\t\t\t_touchSelectTimer.callOnce(QApplication::startDragTime());\n\t\t}\n\t\t_touchSelect = false;\n\t\t_touchStart = _touchPrevPos = _touchPos;\n\t} break;\n\n\tcase QEvent::TouchUpdate: {\n\t\tif (!_touchInProgress) {\n\t\t\treturn;\n\t\t} else if (_touchSelect) {\n\t\t\tmouseActionUpdate(_touchPos);\n\t\t} else if (!_touchScroll\n\t\t\t\t&& (_touchPos - _touchStart).manhattanLength()\n\t\t\t\t\t>= QApplication::startDragDistance()) {\n\t\t\t_touchSelectTimer.cancel();\n\t\t\t_touchScroll = true;\n\t\t\ttouchUpdateSpeed();\n\t\t}\n\t\tif (_touchScroll) {\n\t\t\tif (_touchScrollState == Ui::TouchScrollState::Manual) {\n\t\t\t\ttouchScrollUpdated(_touchPos);\n\t\t\t} else if (_touchScrollState == Ui::TouchScrollState::Acceleration) {\n\t\t\t\ttouchUpdateSpeed();\n\t\t\t\t_touchAccelerationTime = crl::now();\n\t\t\t\tif (_touchSpeed.isNull()) {\n\t\t\t\t\t_touchScrollState = Ui::TouchScrollState::Manual;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase QEvent::TouchEnd: {\n\t\tif (!_touchInProgress) {\n\t\t\treturn;\n\t\t}\n\t\t_touchInProgress = false;\n\t\tconst auto notMoved = (_touchPos - _touchStart).manhattanLength()\n\t\t\t< QApplication::startDragDistance();\n\t\tauto weak = base::make_weak(this);\n\t\tif (_touchSelect) {\n\t\t\tif (notMoved) {\n\t\t\t\tmouseActionFinish(_touchPos, Qt::RightButton);\n\t\t\t\tauto contextMenu = QContextMenuEvent(\n\t\t\t\t\tQContextMenuEvent::Mouse,\n\t\t\t\t\tmapFromGlobal(_touchPos),\n\t\t\t\t\t_touchPos);\n\t\t\t\tshowContextMenu(&contextMenu, true);\n\t\t\t}\n\t\t\t_touchScroll = false;\n\t\t} else if (_touchScroll) {\n\t\t\tif (_touchScrollState == Ui::TouchScrollState::Manual) {\n\t\t\t\t_touchScrollState = Ui::TouchScrollState::Auto;\n\t\t\t\t_touchPrevPosValid = false;\n\t\t\t\t_touchScrollTimer.callEach(15);\n\t\t\t\t_touchTime = crl::now();\n\t\t\t} else if (_touchScrollState == Ui::TouchScrollState::Auto) {\n\t\t\t\t_touchScrollState = Ui::TouchScrollState::Manual;\n\t\t\t\t_touchScroll = false;\n\t\t\t\ttouchResetSpeed();\n\t\t\t} else if (_touchScrollState == Ui::TouchScrollState::Acceleration) {\n\t\t\t\t_touchScrollState = Ui::TouchScrollState::Auto;\n\t\t\t\t_touchWaitingAcceleration = false;\n\t\t\t\t_touchPrevPosValid = false;\n\t\t\t}\n\t\t} else if (notMoved) {\n\t\t\tmouseActionStart(_touchPos, Qt::LeftButton);\n\t\t\tmouseActionFinish(_touchPos, Qt::LeftButton);\n\t\t}\n\t\tif (weak) {\n\t\t\t_touchSelectTimer.cancel();\n\t\t\t_touchSelect = false;\n\t\t}\n\t} break;\n\t}\n}\n\nvoid InnerWidget::touchScrollUpdated(const QPoint &screenPos) {\n\t_touchPos = screenPos;\n\t_scrollToSignal.fire_copy(_visibleTop - (_touchPos - _touchPrevPos).y());\n\ttouchUpdateSpeed();\n}\n\nInnerWidget::~InnerWidget() = default;\n\n} // namespace AdminLog\n"
  },
  {
    "path": "Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/history_view_element.h\"\n#include \"history/admin_log/history_admin_log_item.h\"\n#include \"history/admin_log/history_admin_log_filter_value.h\"\n#include \"menu/menu_antispam_validator.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"mtproto/sender.h\"\n#include \"base/timer.h\"\n\nstruct ChatRestrictionsInfo;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace HistoryView {\nclass Element;\nstruct TextState;\nstruct StateRequest;\nenum class CursorState : char;\nenum class PointState : char;\n} // namespace HistoryView\n\nnamespace Ui {\nclass PopupMenu;\nclass ChatStyle;\nclass ChatTheme;\nstruct PeerUserpicView;\nstruct ChatPaintContext;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace AdminLog {\n\nclass SectionMemento;\n\nclass InnerWidget final\n\t: public Ui::RpWidget\n\t, public Ui::AbstractTooltipShower\n\t, public HistoryView::ElementDelegate {\npublic:\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<ChannelData*> channel);\n\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] not_null<Ui::ChatTheme*> theme() const {\n\t\treturn _theme.get();\n\t}\n\n\t[[nodiscard]] rpl::producer<> showSearchSignal() const;\n\t[[nodiscard]] rpl::producer<int> scrollToSignal() const;\n\t[[nodiscard]] rpl::producer<> cancelSignal() const;\n\n\t[[nodiscard]] not_null<ChannelData*> channel() const {\n\t\treturn _channel;\n\t}\n\n\tUi::ChatPaintContext preparePaintContext(QRect clip) const;\n\n\t// Set the correct scroll position after being resized.\n\tvoid restoreScrollPosition();\n\n\tvoid resizeToWidth(int newWidth, int minHeight) {\n\t\t_minHeight = minHeight;\n\t\treturn RpWidget::resizeToWidth(newWidth);\n\t}\n\n\tvoid saveState(not_null<SectionMemento*> memento);\n\tvoid restoreState(not_null<SectionMemento*> memento);\n\n\t// Empty \"flags\" means all events.\n\tvoid applyFilter(FilterValue &&value);\n\tvoid applySearch(const QString &query);\n\tvoid showFilter(Fn<void(FilterValue &&filter)> callback);\n\n\t// Ui::AbstractTooltipShower interface.\n\tQString tooltipText() const override;\n\tQPoint tooltipPos() const override;\n\tbool tooltipWindowActive() const override;\n\n\t// HistoryView::ElementDelegate interface.\n\tHistoryView::Context elementContext() override;\n\tbool elementUnderCursor(\n\t\tnot_null<const HistoryView::Element*> view) override;\n\tHistoryView::SelectionModeResult elementInSelectionMode(\n\t\tconst HistoryView::Element *view) override;\n\tbool elementIntersectsRange(\n\t\tnot_null<const HistoryView::Element*> view,\n\t\tint from,\n\t\tint till) override;\n\tvoid elementStartStickerLoop(\n\t\tnot_null<const HistoryView::Element*> view) override;\n\tvoid elementShowPollResults(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context) override;\n\tvoid elementShowAddPollOption(\n\t\tnot_null<HistoryView::Element*> view,\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context,\n\t\tQRect optionRect) override;\n\tvoid elementSubmitAddPollOption(FullMsgId context) override;\n\tvoid elementOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) override;\n\tvoid elementOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView = false) override;\n\tvoid elementCancelUpload(const FullMsgId &context) override;\n\tvoid elementShowTooltip(\n\t\tconst TextWithEntities &text,\n\t\tFn<void()> hiddenCallback) override;\n\tbool elementAnimationsPaused() override;\n\tbool elementHideReply(\n\t\tnot_null<const HistoryView::Element*> view) override;\n\tbool elementShownUnread(\n\t\tnot_null<const HistoryView::Element*> view) override;\n\tvoid elementSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) override;\n\tvoid elementSearchInList(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) override;\n\tvoid elementHandleViaClick(not_null<UserData*> bot) override;\n\tHistoryView::ElementChatMode elementChatMode() override;\n\tnot_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;\n\tvoid elementReplyTo(const FullReplyTo &to) override;\n\tvoid elementStartInteraction(\n\t\tnot_null<const HistoryView::Element*> view) override;\n\tvoid elementStartPremium(\n\t\tnot_null<const HistoryView::Element*> view,\n\t\tHistoryView::Element *replacing) override;\n\tvoid elementCancelPremium(\n\t\tnot_null<const HistoryView::Element*> view) override;\n\tvoid elementStartEffect(\n\t\tnot_null<const HistoryView::Element*> view,\n\t\tHistoryView::Element *replacing) override;\n\tQString elementAuthorRank(\n\t\tnot_null<const HistoryView::Element*> view) override;\n\tbool elementHideTopicButton(\n\t\tnot_null<const HistoryView::Element*> view) override;\n\n\t~InnerWidget();\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid mouseDoubleClickEvent(QMouseEvent *e) override;\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\tbool eventHook(QEvent *e) override;\n\n\t// Resizes content and counts natural widget height for the desired width.\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tusing Element = HistoryView::Element;\n\tenum class Direction {\n\t\tUp,\n\t\tDown,\n\t};\n\tenum class MouseAction {\n\t\tNone,\n\t\tPrepareDrag,\n\t\tDragging,\n\t\tSelecting,\n\t};\n\tenum class EnumItemsDirection {\n\t\tTopToBottom,\n\t\tBottomToTop,\n\t};\n\tusing TextState = HistoryView::TextState;\n\tusing CursorState = HistoryView::CursorState;\n\tusing PointState = HistoryView::PointState;\n\tusing StateRequest = HistoryView::StateRequest;\n\n\tvoid mouseActionStart(const QPoint &screenPos, Qt::MouseButton button);\n\tvoid mouseActionUpdate(const QPoint &screenPos);\n\tvoid mouseActionFinish(const QPoint &screenPos, Qt::MouseButton button);\n\tvoid mouseActionCancel();\n\tvoid updateSelected();\n\tvoid performDrag();\n\tint itemTop(not_null<const Element*> view) const;\n\tvoid repaintItem(const Element *view);\n\tvoid repaintItem(const Element *view, QRect rect);\n\tvoid refreshItem(not_null<const Element*> view);\n\tvoid resizeItem(not_null<Element*> view);\n\tQPoint mapPointToItem(QPoint point, const Element *view) const;\n\n\tvoid showContextMenu(QContextMenuEvent *e, bool showFromTouch = false);\n\tvoid savePhotoToFile(not_null<PhotoData*> photo);\n\tvoid saveDocumentToFile(not_null<DocumentData*> document);\n\tvoid copyContextImage(not_null<PhotoData*> photo);\n\tvoid showStickerPackInfo(not_null<DocumentData*> document);\n\tvoid cancelContextDownload(not_null<DocumentData*> document);\n\tvoid showContextInFolder(not_null<DocumentData*> document);\n\tvoid openContextGif(FullMsgId itemId);\n\tvoid copyContextText(FullMsgId itemId);\n\tvoid copySelectedText();\n\tTextForMimeData getSelectedText() const;\n\tvoid suggestRestrictParticipant(\n\t\tnot_null<PeerData*> participant,\n\t\tFullMsgId realId);\n\tvoid restrictParticipant(\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo oldRights,\n\t\tChatRestrictionsInfo newRights);\n\tvoid restrictParticipantDone(\n\t\tnot_null<PeerData*> participant,\n\t\tChatRestrictionsInfo rights);\n\t[[nodiscard]] bool canRestrict() const;\n\n\tvoid requestAdmins();\n\tvoid checkPreloadMore();\n\tvoid updateVisibleTopItem();\n\tvoid preloadMore(Direction direction);\n\tvoid itemsAdded(Direction direction, int addedCount);\n\tvoid updateSize();\n\tvoid updateMinMaxIds();\n\tvoid updateEmptyText();\n\tvoid paintEmpty(Painter &p, not_null<const Ui::ChatStyle*> st);\n\tvoid clearAfterFilterChange();\n\tvoid clearAndRequestLog();\n\tvoid addEvents(\n\t\tDirection direction,\n\t\tconst QVector<MTPChannelAdminLogEvent> &events);\n\t[[nodiscard]] Element *viewForItem(const HistoryItem *item);\n\t[[nodiscard]] bool myView(\n\t\tnot_null<const HistoryView::Element*> view) const;\n\n\tvoid toggleScrollDateShown();\n\tvoid repaintScrollDateCallback();\n\tbool displayScrollDate() const;\n\tvoid scrollDateHide();\n\tvoid scrollDateCheck();\n\tvoid scrollDateHideByTimer();\n\n\t// This function finds all history items that are displayed and calls template method\n\t// for each found message (in given direction) in the passed history with passed top offset.\n\t//\n\t// Method has \"bool (*Method)(not_null<Element*> view, int itemtop, int itembottom)\" signature\n\t// if it returns false the enumeration stops immediately.\n\ttemplate <EnumItemsDirection direction, typename Method>\n\tvoid enumerateItems(Method method);\n\n\t// This function finds all userpics on the left that are displayed and calls template method\n\t// for each found userpic (from the top to the bottom) using enumerateItems() method.\n\t//\n\t// Method has \"bool (*Method)(not_null<Element*> view, int userpicTop)\" signature\n\t// if it returns false the enumeration stops immediately.\n\ttemplate <typename Method>\n\tvoid enumerateUserpics(Method method);\n\n\t// This function finds all date elements that are displayed and calls template method\n\t// for each found date element (from the bottom to the top) using enumerateItems() method.\n\t//\n\t// Method has \"bool (*Method)(not_null<HistoryItem*> item, int itemtop, int dateTop)\" signature\n\t// if it returns false the enumeration stops immediately.\n\ttemplate <typename Method>\n\tvoid enumerateDates(Method method);\n\n\tvoid touchEvent(QTouchEvent *e);\n\tvoid touchScrollUpdated(const QPoint &screenPos);\n\tvoid touchResetSpeed();\n\tvoid touchUpdateSpeed();\n\tvoid touchDeaccelerate(int32 elapsed);\n\tvoid onTouchSelect();\n\tvoid onTouchScrollTimer();\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<ChannelData*> _channel;\n\tconst not_null<History*> _history;\n\tMTP::Sender _api;\n\n\tconst std::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\tstd::shared_ptr<Ui::ChatTheme> _theme;\n\n\tstd::vector<OwnedItem> _items;\n\tstd::set<uint64> _eventIds;\n\tstd::map<not_null<const HistoryItem*>, not_null<Element*>> _itemsByData;\n\tbase::flat_map<not_null<const HistoryItem*>, TimeId> _itemDates;\n\tbase::flat_set<FullMsgId> _animatedStickersPlayed;\n\tbase::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> _userpics;\n\tbase::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> _userpicsCache;\n\tbase::flat_map<FullMsgId, MsgId> _realIdsForReport;\n\tint _itemsTop = 0;\n\tint _itemsWidth = 0;\n\tint _itemsHeight = 0;\n\n\tint _minHeight = 0;\n\tint _visibleTop = 0;\n\tint _visibleBottom = 0;\n\tElement *_visibleTopItem = nullptr;\n\tint _visibleTopFromItem = 0;\n\n\tbool _isChatWide = false;\n\tbool _scrollDateShown = false;\n\tUi::Animations::Simple _scrollDateOpacity;\n\tSingleQueuedInvokation _scrollDateCheck;\n\tbase::Timer _scrollDateHideTimer;\n\tElement *_scrollDateLastItem = nullptr;\n\tint _scrollDateLastItemTop = 0;\n\n\t// Up - max, Down - min.\n\tuint64 _maxId = 0;\n\tuint64 _minId = 0;\n\tmtpRequestId _preloadUpRequestId = 0;\n\tmtpRequestId _preloadDownRequestId = 0;\n\n\t// Don't load anything until the memento was read.\n\tbool _upLoaded = true;\n\tbool _downLoaded = true;\n\tbool _filterChanged = false;\n\tUi::Text::String _emptyText;\n\n\tMouseAction _mouseAction = MouseAction::None;\n\tTextSelectType _mouseSelectType = TextSelectType::Letters;\n\tQPoint _dragStartPosition;\n\tQPoint _mousePosition;\n\tElement *_mouseActionItem = nullptr;\n\tCursorState _mouseCursorState = CursorState();\n\tuint16 _mouseTextSymbol = 0;\n\tbool _pressWasInactive = false;\n\n\tElement *_selectedItem = nullptr;\n\tTextSelection _selectedText;\n\tbool _wasSelectedText = false; // was some text selected in current drag action\n\tQt::CursorShape _cursor = style::cur_default;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\tAntiSpamMenu::AntiSpamValidator _antiSpamValidator;\n\n\tQPoint _trippleClickPoint;\n\tbase::Timer _trippleClickTimer;\n\n\tbase::Timer _touchSelectTimer;\n\tbase::Timer _touchScrollTimer;\n\n\t// Touch scroll support.\n\tbool _touchScroll = false;\n\tbool _touchSelect = false;\n\tbool _touchInProgress = false;\n\tQPoint _touchStart, _touchPrevPos, _touchPos;\n\tUi::TouchScrollState _touchScrollState = Ui::TouchScrollState::Manual;\n\tbool _touchPrevPosValid = false;\n\tbool _touchWaitingAcceleration = false;\n\tQPoint _touchSpeed;\n\tcrl::time _touchSpeedTime = 0;\n\tcrl::time _touchAccelerationTime = 0;\n\tcrl::time _touchTime = 0;\n\n\tFilterValue _filter;\n\tQString _searchQuery;\n\tstd::vector<not_null<UserData*>> _admins;\n\tstd::vector<not_null<UserData*>> _adminsCanEdit;\n\tFn<void(FilterValue &&filter)> _showFilterCallback;\n\n\trpl::event_stream<> _showSearchSignal;\n\trpl::event_stream<int> _scrollToSignal;\n\trpl::event_stream<> _cancelSignal;\n\n};\n\n} // namespace AdminLog\n"
  },
  {
    "path": "Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/admin_log/history_admin_log_item.h\"\n\n#include \"history/admin_log/history_admin_log_inner.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history_location_manager.h\"\n#include \"api/api_chat_participants.h\"\n#include \"api/api_text_entities.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"data/data_message_reaction_id.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"main/main_session.h\"\n#include \"window/notifications_manager.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace AdminLog {\nnamespace {\n\nTextWithEntities PrepareText(\n\t\tconst QString &value,\n\t\tconst QString &emptyValue) {\n\tauto result = TextWithEntities{ value };\n\tif (result.text.isEmpty()) {\n\t\tresult.text = emptyValue;\n\t\tif (!emptyValue.isEmpty()) {\n\t\t\tresult.entities.push_back({\n\t\t\t\tEntityType::Italic,\n\t\t\t\t0,\n\t\t\t\tint(emptyValue.size()) });\n\t\t}\n\t} else {\n\t\tTextUtilities::ParseEntities(\n\t\t\tresult,\n\t\t\tTextParseLinks\n\t\t\t\t| TextParseMentions\n\t\t\t\t| TextParseHashtags\n\t\t\t\t| TextParseBotCommands);\n\t}\n\treturn result;\n}\n\n[[nodiscard]] TimeId ExtractSentDate(const MTPMessage &message) {\n\treturn message.match([](const MTPDmessageEmpty &) {\n\t\treturn 0;\n\t}, [](const MTPDmessageService &data) {\n\t\treturn data.vdate().v;\n\t}, [](const MTPDmessage &data) {\n\t\treturn data.vdate().v;\n\t});\n}\n\n[[nodiscard]] MsgId ExtractRealMsgId(const MTPMessage &message) {\n\treturn MsgId(message.match([](const MTPDmessageEmpty &) {\n\t\treturn 0;\n\t}, [](const MTPDmessageService &data) {\n\t\treturn data.vid().v;\n\t}, [](const MTPDmessage &data) {\n\t\treturn data.vid().v;\n\t}));\n}\n\nstd::optional<MTPMessageReplyHeader> PrepareLogReply(\n\t\tconst MTPMessageReplyHeader *header) {\n\tif (!header) {\n\t\treturn {};\n\t}\n\treturn header->match([&](const MTPDmessageReplyHeader &data)\n\t-> std::optional<MTPMessageReplyHeader> {\n\t\tif (data.vreply_to_peer_id() || data.vreply_to_msg_id()) {\n\t\t\treturn *header;\n\t\t} else if (data.is_forum_topic()) {\n\t\t\tconst auto topId = data.vreply_to_top_id().value_or(\n\t\t\t\tdata.vreply_to_msg_id().value_or_empty());\n\t\t\tif (topId) {\n\t\t\t\tusing Flag = MTPDmessageReplyHeader::Flag;\n\t\t\t\tconst auto removeFlags = Flag::f_reply_from\n\t\t\t\t\t| Flag::f_reply_media\n\t\t\t\t\t| Flag::f_reply_to_scheduled\n\t\t\t\t\t| Flag::f_quote\n\t\t\t\t\t| Flag::f_quote_entities\n\t\t\t\t\t| Flag::f_quote_offset\n\t\t\t\t\t| Flag::f_quote_text;\n\t\t\t\treturn MTP_messageReplyHeader(\n\t\t\t\t\tMTP_flags((data.vflags().v & ~removeFlags)\n\t\t\t\t\t\t| Flag::f_reply_to_msg_id),\n\t\t\t\t\tMTP_int(topId),\n\t\t\t\t\tMTPPeer(), // reply_to_peer_id\n\t\t\t\t\tMTPMessageFwdHeader(), // reply_from\n\t\t\t\t\tMTPMessageMedia(), // reply_media\n\t\t\t\t\tMTP_int(topId),\n\t\t\t\t\tMTPstring(), // quote_text\n\t\t\t\t\tMTPVector<MTPMessageEntity>(), // quote_entities\n\t\t\t\t\tMTPint(), // quote_offset\n\t\t\t\t\tMTPint(), // todo_item_id\n\t\t\t\t\tMTPbytes()); // poll_option\n\t\t\t}\n\t\t}\n\t\treturn {};\n\t}, [&](const MTPDmessageReplyStoryHeader &data) {\n\t\treturn std::optional<MTPMessageReplyHeader>();\n\t});\n}\n\nMTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) {\n\treturn message.match([&](const MTPDmessageEmpty &data) {\n\t\treturn MTP_messageEmpty(\n\t\t\tdata.vflags(),\n\t\t\tdata.vid(),\n\t\t\tdata.vpeer_id() ? *data.vpeer_id() : MTPPeer());\n\t}, [&](const MTPDmessageService &data) {\n\t\tusing Flag = MTPDmessageService::Flag;\n\t\tconst auto reply = PrepareLogReply(data.vreply_to());\n\t\tconst auto removeFlags = Flag::f_out\n\t\t\t| Flag::f_post\n\t\t\t| Flag::f_saved_peer_id\n\t\t\t| Flag::f_reactions_are_possible\n\t\t\t| Flag::f_reactions\n\t\t\t| Flag::f_ttl_period\n\t\t\t| (reply ? Flag() : Flag::f_reply_to);\n\t\treturn MTP_messageService(\n\t\t\tMTP_flags(data.vflags().v & ~removeFlags),\n\t\t\tdata.vid(),\n\t\t\tdata.vfrom_id() ? *data.vfrom_id() : MTPPeer(),\n\t\t\tdata.vpeer_id(),\n\t\t\tMTPPeer(), // saved_peer_id\n\t\t\treply.value_or(MTPMessageReplyHeader()),\n\t\t\tMTP_int(newDate),\n\t\t\tdata.vaction(),\n\t\t\tMTPMessageReactions(),\n\t\t\tMTPint()); // ttl_period\n\t}, [&](const MTPDmessage &data) {\n\t\tusing Flag = MTPDmessage::Flag;\n\t\tconst auto reply = PrepareLogReply(data.vreply_to());\n\t\tconst auto removeFlags = Flag::f_out\n\t\t\t| Flag::f_post\n\t\t\t| Flag::f_saved_peer_id\n\t\t\t| (reply ? Flag() : Flag::f_reply_to)\n\t\t\t| Flag::f_replies\n\t\t\t| Flag::f_edit_date\n\t\t\t| Flag::f_grouped_id\n\t\t\t| Flag::f_views\n\t\t\t| Flag::f_forwards\n\t\t\t//| Flag::f_reactions\n\t\t\t| Flag::f_restriction_reason\n\t\t\t| Flag::f_ttl_period\n\t\t\t| Flag::f_factcheck\n\t\t\t| Flag::f_report_delivery_until_date\n\t\t\t| Flag::f_suggested_post\n\t\t\t| Flag::f_summary_from_language;\n\t\treturn MTP_message(\n\t\t\tMTP_flags(data.vflags().v & ~removeFlags),\n\t\t\tdata.vid(),\n\t\t\tdata.vfrom_id() ? *data.vfrom_id() : MTPPeer(),\n\t\t\tMTPint(), // from_boosts_applied\n\t\t\tMTPstring(), // from_rank\n\t\t\tdata.vpeer_id(),\n\t\t\tMTPPeer(), // saved_peer_id\n\t\t\tdata.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(),\n\t\t\tMTP_long(data.vvia_bot_id().value_or_empty()),\n\t\t\tMTP_long(data.vvia_business_bot_id().value_or_empty()),\n\t\t\treply.value_or(MTPMessageReplyHeader()),\n\t\t\tMTP_int(newDate),\n\t\t\tdata.vmessage(),\n\t\t\tdata.vmedia() ? *data.vmedia() : MTPMessageMedia(),\n\t\t\tdata.vreply_markup() ? *data.vreply_markup() : MTPReplyMarkup(),\n\t\t\t(data.ventities()\n\t\t\t\t? *data.ventities()\n\t\t\t\t: MTPVector<MTPMessageEntity>()),\n\t\t\tMTP_int(data.vviews().value_or_empty()),\n\t\t\tMTP_int(data.vforwards().value_or_empty()),\n\t\t\tMTPMessageReplies(),\n\t\t\tMTPint(), // edit_date\n\t\t\tMTP_string(),\n\t\t\tMTP_long(0), // grouped_id\n\t\t\tMTPMessageReactions(),\n\t\t\tMTPVector<MTPRestrictionReason>(),\n\t\t\tMTPint(), // ttl_period\n\t\t\tMTPint(), // quick_reply_shortcut_id\n\t\t\tMTP_long(data.veffect().value_or_empty()),\n\t\t\tMTPFactCheck(),\n\t\t\tMTPint(), // report_delivery_until_date\n\t\t\tMTP_long(data.vpaid_message_stars().value_or_empty()),\n\t\t\tMTPSuggestedPost(),\n\t\t\tMTPint(), // schedule_repeat_period\n\t\t\tMTPstring()); // summary_from_language\n\t});\n}\n\nbool MediaCanHaveCaption(const MTPMessage &message) {\n\tif (message.type() != mtpc_message) {\n\t\treturn false;\n\t}\n\tconst auto &data = message.c_message();\n\tconst auto media = data.vmedia();\n\tconst auto mediaType = media ? media->type() : mtpc_messageMediaEmpty;\n\treturn (mediaType == mtpc_messageMediaDocument)\n\t\t|| (mediaType == mtpc_messageMediaPhoto);\n}\n\nuint64 MediaId(const MTPMessage &message) {\n\tif (!MediaCanHaveCaption(message)) {\n\t\treturn 0;\n\t}\n\tconst auto &media = message.c_message().vmedia();\n\treturn media\n\t\t? v::match(\n\t\t\tData::GetFileReferences(*media).data.begin()->first,\n\t\t\t[](const auto &d) { return d.id; })\n\t\t: 0;\n}\n\nTextWithEntities ExtractEditedText(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPMessage &message) {\n\tif (message.type() != mtpc_message) {\n\t\treturn TextWithEntities();\n\t}\n\tconst auto &data = message.c_message();\n\treturn {\n\t\tqs(data.vmessage()),\n\t\tApi::EntitiesFromMTP(session, data.ventities().value_or_empty())\n\t};\n}\n\nconst auto CollectChanges = [](\n\t\tauto &phraseMap,\n\t\tauto plusFlags,\n\t\tauto minusFlags) {\n\tauto withPrefix = [&phraseMap](auto flags, QChar prefix) {\n\t\tauto result = QString();\n\t\tfor (auto &phrase : phraseMap) {\n\t\t\tif (flags & phrase.first) {\n\t\t\t\tresult.append('\\n' + (prefix + phrase.second(tr::now)));\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\tconst auto kMinus = QChar(0x2212);\n\treturn withPrefix(plusFlags & ~minusFlags, '+')\n\t\t+ withPrefix(minusFlags & ~plusFlags, kMinus);\n};\n\nTextWithEntities GenerateAdminChangeText(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst TextWithEntities &user,\n\t\tChatAdminRightsInfo newRights,\n\t\tChatAdminRightsInfo prevRights) {\n\tusing Flag = ChatAdminRight;\n\tusing Flags = ChatAdminRights;\n\n\tauto result = tr::lng_admin_log_promoted(\n\t\ttr::now,\n\t\tlt_user,\n\t\tuser,\n\t\ttr::marked);\n\n\tconst auto useInviteLinkPhrase = channel->isMegagroup()\n\t\t&& channel->anyoneCanAddMembers();\n\tconst auto invitePhrase = useInviteLinkPhrase\n\t\t? tr::lng_admin_log_admin_invite_link\n\t\t: tr::lng_admin_log_admin_invite_users;\n\tconst auto callPhrase = channel->isBroadcast()\n\t\t? tr::lng_admin_log_admin_manage_calls_channel\n\t\t: tr::lng_admin_log_admin_manage_calls;\n\tstatic auto phraseMap = std::map<Flags, tr::phrase<>> {\n\t\t{ Flag::ChangeInfo, tr::lng_admin_log_admin_change_info },\n\t\t{ Flag::PostMessages, tr::lng_admin_log_admin_post_messages },\n\t\t{ Flag::EditMessages, tr::lng_admin_log_admin_edit_messages },\n\t\t{ Flag::DeleteMessages, tr::lng_admin_log_admin_delete_messages },\n\t\t{ Flag::BanUsers, tr::lng_admin_log_admin_ban_users },\n\t\t{ Flag::InviteByLinkOrAdd, invitePhrase },\n\t\t{ Flag::ManageTopics, tr::lng_admin_log_admin_manage_topics },\n\t\t{ Flag::PinMessages, tr::lng_admin_log_admin_pin_messages },\n\t\t{ Flag::ManageCall, tr::lng_admin_log_admin_manage_calls },\n\t\t{ Flag::ManageDirect, tr::lng_admin_log_admin_manage_direct },\n\t\t{ Flag::ManageRanks, tr::lng_admin_log_admin_manage_ranks },\n\t\t{ Flag::AddAdmins, tr::lng_admin_log_admin_add_admins },\n\t\t{ Flag::Anonymous, tr::lng_admin_log_admin_remain_anonymous },\n\t};\n\tphraseMap[Flag::InviteByLinkOrAdd] = invitePhrase;\n\tphraseMap[Flag::ManageCall] = callPhrase;\n\n\tconst auto changes = CollectChanges(\n\t\tphraseMap,\n\t\tnewRights.flags,\n\t\tprevRights.flags);\n\tif (!changes.isEmpty()) {\n\t\tresult.text.append('\\n' + changes);\n\t}\n\n\treturn result;\n};\n\nQString GeneratePermissionsChangeText(\n\t\tChatRestrictionsInfo newRights,\n\t\tChatRestrictionsInfo prevRights,\n\t\tbool isUserSpecific = false) {\n\tusing Flag = ChatRestriction;\n\tusing Flags = ChatRestrictions;\n\n\tauto phraseMap = std::map<Flags, tr::phrase<>>{\n\t\t{ Flag::ViewMessages, tr::lng_admin_log_banned_view_messages },\n\t\t{ Flag::SendOther, tr::lng_admin_log_banned_send_messages },\n\t\t{ Flag::SendPhotos, tr::lng_admin_log_banned_send_photos },\n\t\t{ Flag::SendVideos, tr::lng_admin_log_banned_send_videos },\n\t\t{ Flag::SendMusic, tr::lng_admin_log_banned_send_music },\n\t\t{ Flag::SendFiles, tr::lng_admin_log_banned_send_files },\n\t\t{\n\t\t\tFlag::SendVoiceMessages,\n\t\t\ttr::lng_admin_log_banned_send_voice_messages },\n\t\t{\n\t\t\tFlag::SendVideoMessages,\n\t\t\ttr::lng_admin_log_banned_send_video_messages },\n\t\t{ Flag::SendStickers\n\t\t\t| Flag::SendGifs\n\t\t\t| Flag::SendInline\n\t\t\t| Flag::SendGames, tr::lng_admin_log_banned_send_stickers },\n\t\t{ Flag::EmbedLinks, tr::lng_admin_log_banned_embed_links },\n\t\t{ Flag::SendPolls, tr::lng_admin_log_banned_send_polls },\n\t\t{ Flag::ChangeInfo, tr::lng_admin_log_admin_change_info },\n\t\t{ Flag::AddParticipants, tr::lng_admin_log_admin_invite_users },\n\t\t{ Flag::CreateTopics, tr::lng_admin_log_admin_create_topics },\n\t\t{ Flag::PinMessages, tr::lng_admin_log_admin_pin_messages },\n\t\t{ Flag::EditRank, isUserSpecific\n\t\t\t? tr::lng_admin_log_banned_edit_rank_single\n\t\t\t: tr::lng_admin_log_banned_edit_rank },\n\t};\n\treturn CollectChanges(phraseMap, prevRights.flags, newRights.flags);\n}\n\nTextWithEntities GeneratePermissionsChangeText(\n\t\tPeerId participantId,\n\t\tconst TextWithEntities &user,\n\t\tChatRestrictionsInfo newRights,\n\t\tChatRestrictionsInfo prevRights) {\n\tusing Flag = ChatRestriction;\n\n\tconst auto newFlags = newRights.flags;\n\tconst auto newUntil = newRights.until;\n\tconst auto prevFlags = prevRights.flags;\n\tconst auto indefinitely = ChannelData::IsRestrictedForever(newUntil);\n\tif (newFlags & Flag::ViewMessages) {\n\t\tif (indefinitely) {\n\t\t\treturn tr::lng_admin_log_banned(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tuser,\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn tr::lng_admin_log_banned_until(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\tuser,\n\t\t\tlt_until,\n\t\t\ttr::lng_admin_log_restricted_until(\n\t\t\t\ttr::now,\n\t\t\t\tlt_date,\n\t\t\t\t{ langDateTime(base::unixtime::parse(newUntil)) },\n\t\t\t\ttr::marked),\n\t\t\ttr::marked);\n\t} else if (newFlags == 0\n\t\t&& (prevFlags & Flag::ViewMessages)\n\t\t&& !peerIsUser(participantId)) {\n\t\treturn tr::lng_admin_log_unbanned(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\tuser,\n\t\t\ttr::marked);\n\t}\n\tconst auto untilText = indefinitely\n\t\t? tr::lng_admin_log_restricted_forever(tr::now)\n\t\t: tr::lng_admin_log_restricted_until(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\tlangDateTime(base::unixtime::parse(newUntil)));\n\tauto result = tr::lng_admin_log_restricted(\n\t\ttr::now,\n\t\tlt_user,\n\t\tuser,\n\t\tlt_until,\n\t\tTextWithEntities { untilText },\n\t\ttr::marked);\n\tconst auto changes = GeneratePermissionsChangeText(\n\t\tnewRights,\n\t\tprevRights,\n\t\ttrue);\n\tif (!changes.isEmpty()) {\n\t\tresult.text.append('\\n' + changes);\n\t}\n\treturn result;\n}\n\nQString PublicJoinLink() {\n\treturn u\"(public_join_link)\"_q;\n}\n\nQString ExtractInviteLink(const MTPExportedChatInvite &data) {\n\treturn data.match([&](const MTPDchatInviteExported &data) {\n\t\treturn qs(data.vlink());\n\t}, [&](const MTPDchatInvitePublicJoinRequests &data) {\n\t\treturn PublicJoinLink();\n\t});\n}\n\nQString ExtractInviteLinkLabel(const MTPExportedChatInvite &data) {\n\treturn data.match([&](const MTPDchatInviteExported &data) {\n\t\treturn qs(data.vtitle().value_or_empty());\n\t}, [&](const MTPDchatInvitePublicJoinRequests &data) {\n\t\treturn PublicJoinLink();\n\t});\n}\n\nQString InternalInviteLinkUrl(const MTPExportedChatInvite &data) {\n\tconst auto base64 = ExtractInviteLink(data).toUtf8().toBase64();\n\treturn \"internal:show_invite_link/?link=\" + QString::fromLatin1(base64);\n}\n\nQString GenerateInviteLinkText(const MTPExportedChatInvite &data) {\n\tconst auto label = ExtractInviteLinkLabel(data);\n\treturn label.isEmpty() ? ExtractInviteLink(data).replace(\n\t\tu\"https://\"_q,\n\t\tQString()\n\t).replace(\n\t\tu\"t.me/joinchat/\"_q,\n\t\tQString()\n\t) : label;\n}\n\nTextWithEntities GenerateInviteLinkLink(const MTPExportedChatInvite &data) {\n\tconst auto text = GenerateInviteLinkText(data);\n\treturn text.endsWith(Ui::kQEllipsis)\n\t\t? TextWithEntities{ .text = text }\n\t\t: tr::link(text, InternalInviteLinkUrl(data));\n}\n\nTextWithEntities GenerateInviteLinkChangeText(\n\t\tconst MTPExportedChatInvite &newLink,\n\t\tconst MTPExportedChatInvite &prevLink) {\n\tauto link = TextWithEntities{ GenerateInviteLinkText(newLink) };\n\tif (!link.text.endsWith(Ui::kQEllipsis)) {\n\t\tlink.entities.push_back({\n\t\t\tEntityType::CustomUrl,\n\t\t\t0,\n\t\t\tint(link.text.size()),\n\t\t\tInternalInviteLinkUrl(newLink) });\n\t}\n\tauto result = tr::lng_admin_log_edited_invite_link(\n\t\ttr::now,\n\t\tlt_link,\n\t\tlink,\n\t\ttr::marked);\n\tresult.text.append('\\n');\n\n\tconst auto label = [](const MTPExportedChatInvite &link) {\n\t\treturn link.match([](const MTPDchatInviteExported &data) {\n\t\t\treturn qs(data.vtitle().value_or_empty());\n\t\t}, [&](const MTPDchatInvitePublicJoinRequests &data) {\n\t\t\treturn PublicJoinLink();\n\t\t});\n\t};\n\tconst auto expireDate = [](const MTPExportedChatInvite &link) {\n\t\treturn link.match([](const MTPDchatInviteExported &data) {\n\t\t\treturn data.vexpire_date().value_or_empty();\n\t\t}, [&](const MTPDchatInvitePublicJoinRequests &data) {\n\t\t\treturn TimeId();\n\t\t});\n\t};\n\tconst auto usageLimit = [](const MTPExportedChatInvite &link) {\n\t\treturn link.match([](const MTPDchatInviteExported &data) {\n\t\t\treturn data.vusage_limit().value_or_empty();\n\t\t}, [&](const MTPDchatInvitePublicJoinRequests &data) {\n\t\t\treturn 0;\n\t\t});\n\t};\n\tconst auto requestApproval = [](const MTPExportedChatInvite &link) {\n\t\treturn link.match([](const MTPDchatInviteExported &data) {\n\t\t\treturn data.is_request_needed();\n\t\t}, [&](const MTPDchatInvitePublicJoinRequests &data) {\n\t\t\treturn true;\n\t\t});\n\t};\n\tconst auto wrapDate = [](TimeId date) {\n\t\treturn date\n\t\t\t? langDateTime(base::unixtime::parse(date))\n\t\t\t: tr::lng_group_invite_expire_never(tr::now);\n\t};\n\tconst auto wrapUsage = [](int count) {\n\t\treturn count\n\t\t\t? QString::number(count)\n\t\t\t: tr::lng_group_invite_usage_any(tr::now);\n\t};\n\tconst auto wasLabel = label(prevLink);\n\tconst auto nowLabel = label(newLink);\n\tconst auto wasExpireDate = expireDate(prevLink);\n\tconst auto nowExpireDate = expireDate(newLink);\n\tconst auto wasUsageLimit = usageLimit(prevLink);\n\tconst auto nowUsageLimit = usageLimit(newLink);\n\tconst auto wasRequestApproval = requestApproval(prevLink);\n\tconst auto nowRequestApproval = requestApproval(newLink);\n\tif (wasLabel != nowLabel) {\n\t\tresult.text.append('\\n').append(\n\t\t\ttr::lng_admin_log_invite_link_label(\n\t\t\t\ttr::now,\n\t\t\t\tlt_previous,\n\t\t\t\twasLabel,\n\t\t\t\tlt_limit,\n\t\t\t\tnowLabel));\n\t}\n\tif (wasExpireDate != nowExpireDate) {\n\t\tresult.text.append('\\n').append(\n\t\t\ttr::lng_admin_log_invite_link_expire_date(\n\t\t\t\ttr::now,\n\t\t\t\tlt_previous,\n\t\t\t\twrapDate(wasExpireDate),\n\t\t\t\tlt_limit,\n\t\t\t\twrapDate(nowExpireDate)));\n\t}\n\tif (wasUsageLimit != nowUsageLimit) {\n\t\tresult.text.append('\\n').append(\n\t\t\ttr::lng_admin_log_invite_link_usage_limit(\n\t\t\t\ttr::now,\n\t\t\t\tlt_previous,\n\t\t\t\twrapUsage(wasUsageLimit),\n\t\t\t\tlt_limit,\n\t\t\t\twrapUsage(nowUsageLimit)));\n\t}\n\tif (wasRequestApproval != nowRequestApproval) {\n\t\tresult.text.append('\\n').append(\n\t\t\tnowRequestApproval\n\t\t\t\t? tr::lng_admin_log_invite_link_request_needed(tr::now)\n\t\t\t\t: tr::lng_admin_log_invite_link_request_not_needed(tr::now));\n\t}\n\n\tresult.entities.push_front(\n\t\tEntityInText(EntityType::Italic, 0, result.text.size()));\n\treturn result;\n};\n\nauto GenerateParticipantString(\n\t\tnot_null<Main::Session*> session,\n\t\tPeerId participantId) {\n\t// User name in \"User name (@username)\" format with entities.\n\tconst auto peer = session->data().peer(participantId);\n\tauto name = TextWithEntities { peer->name()};\n\tif (const auto user = peer->asUser()) {\n\t\tconst auto data = TextUtilities::MentionNameDataFromFields({\n\t\t\t.selfId = session->userId().bare,\n\t\t\t.userId = peerToUser(user->id).bare,\n\t\t\t.accessHash = user->accessHash(),\n\t\t});\n\t\tname.entities.push_back({\n\t\t\tEntityType::MentionName,\n\t\t\t0,\n\t\t\tint(name.text.size()),\n\t\t\tdata,\n\t\t});\n\t}\n\tconst auto username = peer->username();\n\tif (username.isEmpty()) {\n\t\treturn name;\n\t}\n\tauto mention = TextWithEntities { '@' + username };\n\tmention.entities.push_back({\n\t\tEntityType::Mention,\n\t\t0,\n\t\tint(mention.text.size()) });\n\treturn tr::lng_admin_log_user_with_username(\n\t\ttr::now,\n\t\tlt_name,\n\t\tname,\n\t\tlt_mention,\n\t\tmention,\n\t\ttr::marked);\n}\n\nauto GenerateParticipantChangeText(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst Api::ChatParticipant &participant,\n\t\tstd::optional<Api::ChatParticipant> oldParticipant = std::nullopt) {\n\tusing Type = Api::ChatParticipant::Type;\n\tconst auto oldRights = oldParticipant\n\t\t? oldParticipant->rights()\n\t\t: ChatAdminRightsInfo();\n\tconst auto oldRestrictions = oldParticipant\n\t\t? oldParticipant->restrictions()\n\t\t: ChatRestrictionsInfo();\n\n\tconst auto generateOther = [&](PeerId participantId) {\n\t\tauto user = GenerateParticipantString(\n\t\t\t&channel->session(),\n\t\t\tparticipantId);\n\t\tif (oldParticipant && oldParticipant->type() == Type::Admin) {\n\t\t\treturn GenerateAdminChangeText(\n\t\t\t\tchannel,\n\t\t\t\tuser,\n\t\t\t\tChatAdminRightsInfo(),\n\t\t\t\toldRights);\n\t\t} else if (oldParticipant && oldParticipant->type() == Type::Banned) {\n\t\t\treturn GeneratePermissionsChangeText(\n\t\t\t\tparticipantId,\n\t\t\t\tuser,\n\t\t\t\tChatRestrictionsInfo(),\n\t\t\t\toldRestrictions);\n\t\t} else if (oldParticipant\n\t\t\t\t&& oldParticipant->type() == Type::Restricted\n\t\t\t\t&& (participant.type() == Type::Member\n\t\t\t\t\t\t|| participant.type() == Type::Left)) {\n\t\t\treturn GeneratePermissionsChangeText(\n\t\t\t\tparticipantId,\n\t\t\t\tuser,\n\t\t\t\tChatRestrictionsInfo(),\n\t\t\t\toldRestrictions);\n\t\t}\n\t\treturn tr::lng_admin_log_invited(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\tuser,\n\t\t\ttr::marked);\n\t};\n\n\tauto result = [&] {\n\t\tconst auto &peerId = participant.id();\n\t\tswitch (participant.type()) {\n\t\tcase Api::ChatParticipant::Type::Creator: {\n\t\t\t// No valid string here :(\n\t\t\tconst auto user = GenerateParticipantString(\n\t\t\t\t&channel->session(),\n\t\t\t\tpeerId);\n\t\t\tif (peerId == channel->session().userPeerId()) {\n\t\t\t\treturn GenerateAdminChangeText(\n\t\t\t\t\tchannel,\n\t\t\t\t\tuser,\n\t\t\t\t\tparticipant.rights(),\n\t\t\t\t\toldRights);\n\t\t\t}\n\t\t\treturn tr::lng_admin_log_transferred(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tuser,\n\t\t\t\ttr::marked);\n\t\t}\n\t\tcase Api::ChatParticipant::Type::Admin: {\n\t\t\tconst auto user = GenerateParticipantString(\n\t\t\t\t&channel->session(),\n\t\t\t\tpeerId);\n\t\t\treturn GenerateAdminChangeText(\n\t\t\t\tchannel,\n\t\t\t\tuser,\n\t\t\t\tparticipant.rights(),\n\t\t\t\toldRights);\n\t\t}\n\t\tcase Api::ChatParticipant::Type::Restricted:\n\t\tcase Api::ChatParticipant::Type::Banned: {\n\t\t\tconst auto user = GenerateParticipantString(\n\t\t\t\t&channel->session(),\n\t\t\t\tpeerId);\n\t\t\treturn GeneratePermissionsChangeText(\n\t\t\t\tpeerId,\n\t\t\t\tuser,\n\t\t\t\tparticipant.restrictions(),\n\t\t\t\toldRestrictions);\n\t\t}\n\t\tcase Api::ChatParticipant::Type::Left:\n\t\tcase Api::ChatParticipant::Type::Member:\n\t\t\treturn generateOther(peerId);\n\t\t};\n\t\tUnexpected(\"Participant type in GenerateParticipantChangeText.\");\n\t}();\n\n\tresult.entities.push_front(\n\t\tEntityInText(EntityType::Italic, 0, result.text.size()));\n\treturn result;\n}\n\nTextWithEntities GenerateParticipantChangeText(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst MTPChannelParticipant &participant,\n\t\tstd::optional<MTPChannelParticipant>oldParticipant = std::nullopt) {\n\treturn GenerateParticipantChangeText(\n\t\tchannel,\n\t\tApi::ChatParticipant(participant, channel),\n\t\toldParticipant\n\t\t\t? std::make_optional(Api::ChatParticipant(\n\t\t\t\t*oldParticipant,\n\t\t\t\tchannel))\n\t\t\t: std::nullopt);\n}\n\nTextWithEntities GenerateDefaultBannedRightsChangeText(\n\t\tnot_null<ChannelData*> channel,\n\t\tChatRestrictionsInfo rights,\n\t\tChatRestrictionsInfo oldRights) {\n\tauto result = TextWithEntities{\n\t\ttr::lng_admin_log_changed_default_permissions(tr::now)\n\t};\n\tconst auto changes = GeneratePermissionsChangeText(rights, oldRights);\n\tif (!changes.isEmpty()) {\n\t\tresult.text.append('\\n' + changes);\n\t}\n\tresult.entities.push_front(\n\t\tEntityInText(EntityType::Italic, 0, result.text.size()));\n\treturn result;\n}\n\n[[nodiscard]] bool IsTopicClosed(const MTPForumTopic &topic) {\n\treturn topic.match([](const MTPDforumTopic &data) {\n\t\treturn data.is_closed();\n\t}, [](const MTPDforumTopicDeleted &) {\n\t\treturn false;\n\t});\n}\n\n[[nodiscard]] bool IsTopicHidden(const MTPForumTopic &topic) {\n\treturn topic.match([](const MTPDforumTopic &data) {\n\t\treturn data.is_hidden();\n\t}, [](const MTPDforumTopicDeleted &) {\n\t\treturn false;\n\t});\n}\n\n[[nodiscard]] TextWithEntities GenerateTopicLink(\n\t\tnot_null<ChannelData*> channel,\n\t\tconst MTPForumTopic &topic) {\n\treturn topic.match([&](const MTPDforumTopic &data) {\n\t\treturn tr::link(\n\t\t\tData::ForumTopicIconWithTitle(\n\t\t\t\tdata.vid().v,\n\t\t\t\tdata.vicon_emoji_id().value_or_empty(),\n\t\t\t\tqs(data.vtitle())),\n\t\t\tu\"internal:url:https://t.me/c/%1/%2\"_q.arg(\n\t\t\t\tpeerToChannel(channel->id).bare).arg(\n\t\t\t\t\tdata.vid().v));\n\t}, [](const MTPDforumTopicDeleted &) {\n\t\treturn TextWithEntities{ u\"Deleted\"_q };\n\t});\n}\n\n} // namespace\n\nOwnedItem::OwnedItem(std::nullptr_t) {\n}\n\nOwnedItem::OwnedItem(\n\tnot_null<HistoryView::ElementDelegate*> delegate,\n\tnot_null<HistoryItem*> data)\n: _data(data)\n, _view(_data->createView(delegate)) {\n}\n\nOwnedItem::OwnedItem(OwnedItem &&other)\n: _data(base::take(other._data))\n, _view(base::take(other._view)) {\n}\n\nOwnedItem &OwnedItem::operator=(OwnedItem &&other) {\n\t_data = base::take(other._data);\n\t_view = base::take(other._view);\n\treturn *this;\n}\n\nOwnedItem::~OwnedItem() {\n\tclearView();\n\tif (_data) {\n\t\t_data->destroy();\n\t}\n}\n\nvoid OwnedItem::refreshView(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate) {\n\t_view = _data->createView(delegate);\n}\n\nvoid OwnedItem::clearView() {\n\t_view = nullptr;\n}\n\nvoid GenerateItems(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate,\n\t\tnot_null<History*> history,\n\t\tconst MTPDchannelAdminLogEvent &event,\n\t\tFn<void(OwnedItem item, TimeId sentDate, MsgId)> callback) {\n\tExpects(history->peer->isChannel());\n\n\tusing LogTitle = MTPDchannelAdminLogEventActionChangeTitle;\n\tusing LogAbout = MTPDchannelAdminLogEventActionChangeAbout;\n\tusing LogUsername = MTPDchannelAdminLogEventActionChangeUsername;\n\tusing LogPhoto = MTPDchannelAdminLogEventActionChangePhoto;\n\tusing LogInvites = MTPDchannelAdminLogEventActionToggleInvites;\n\tusing LogSign = MTPDchannelAdminLogEventActionToggleSignatures;\n\tusing LogPin = MTPDchannelAdminLogEventActionUpdatePinned;\n\tusing LogEdit = MTPDchannelAdminLogEventActionEditMessage;\n\tusing LogDelete = MTPDchannelAdminLogEventActionDeleteMessage;\n\tusing LogJoin = MTPDchannelAdminLogEventActionParticipantJoin;\n\tusing LogLeave = MTPDchannelAdminLogEventActionParticipantLeave;\n\tusing LogInvite = MTPDchannelAdminLogEventActionParticipantInvite;\n\tusing LogBan = MTPDchannelAdminLogEventActionParticipantToggleBan;\n\tusing LogPromote = MTPDchannelAdminLogEventActionParticipantToggleAdmin;\n\tusing LogSticker = MTPDchannelAdminLogEventActionChangeStickerSet;\n\tusing LogEmoji = MTPDchannelAdminLogEventActionChangeEmojiStickerSet;\n\tusing LogPreHistory\n\t\t= MTPDchannelAdminLogEventActionTogglePreHistoryHidden;\n\tusing LogPermissions = MTPDchannelAdminLogEventActionDefaultBannedRights;\n\tusing LogPoll = MTPDchannelAdminLogEventActionStopPoll;\n\tusing LogDiscussion = MTPDchannelAdminLogEventActionChangeLinkedChat;\n\tusing LogLocation = MTPDchannelAdminLogEventActionChangeLocation;\n\tusing LogSlowMode = MTPDchannelAdminLogEventActionToggleSlowMode;\n\tusing LogStartCall = MTPDchannelAdminLogEventActionStartGroupCall;\n\tusing LogDiscardCall = MTPDchannelAdminLogEventActionDiscardGroupCall;\n\tusing LogMute = MTPDchannelAdminLogEventActionParticipantMute;\n\tusing LogUnmute = MTPDchannelAdminLogEventActionParticipantUnmute;\n\tusing LogCallSetting\n\t\t= MTPDchannelAdminLogEventActionToggleGroupCallSetting;\n\tusing LogJoinByInvite\n\t\t= MTPDchannelAdminLogEventActionParticipantJoinByInvite;\n\tusing LogInviteDelete\n\t\t= MTPDchannelAdminLogEventActionExportedInviteDelete;\n\tusing LogInviteRevoke\n\t\t= MTPDchannelAdminLogEventActionExportedInviteRevoke;\n\tusing LogInviteEdit = MTPDchannelAdminLogEventActionExportedInviteEdit;\n\tusing LogVolume = MTPDchannelAdminLogEventActionParticipantVolume;\n\tusing LogTTL = MTPDchannelAdminLogEventActionChangeHistoryTTL;\n\tusing LogJoinByRequest\n\t\t= MTPDchannelAdminLogEventActionParticipantJoinByRequest;\n\tusing LogNoForwards = MTPDchannelAdminLogEventActionToggleNoForwards;\n\tusing LogSendMessage = MTPDchannelAdminLogEventActionSendMessage;\n\tusing LogChangeAvailableReactions = MTPDchannelAdminLogEventActionChangeAvailableReactions;\n\tusing LogChangeUsernames = MTPDchannelAdminLogEventActionChangeUsernames;\n\tusing LogToggleForum = MTPDchannelAdminLogEventActionToggleForum;\n\tusing LogCreateTopic = MTPDchannelAdminLogEventActionCreateTopic;\n\tusing LogEditTopic = MTPDchannelAdminLogEventActionEditTopic;\n\tusing LogDeleteTopic = MTPDchannelAdminLogEventActionDeleteTopic;\n\tusing LogPinTopic = MTPDchannelAdminLogEventActionPinTopic;\n\tusing LogToggleAntiSpam = MTPDchannelAdminLogEventActionToggleAntiSpam;\n\tusing LogChangePeerColor = MTPDchannelAdminLogEventActionChangePeerColor;\n\tusing LogChangeProfilePeerColor = MTPDchannelAdminLogEventActionChangeProfilePeerColor;\n\tusing LogChangeWallpaper = MTPDchannelAdminLogEventActionChangeWallpaper;\n\tusing LogChangeEmojiStatus = MTPDchannelAdminLogEventActionChangeEmojiStatus;\n\tusing LogToggleSignatureProfiles = MTPDchannelAdminLogEventActionToggleSignatureProfiles;\n\tusing LogParticipantSubExtend = MTPDchannelAdminLogEventActionParticipantSubExtend;\n\tusing LogToggleAutotranslation = MTPDchannelAdminLogEventActionToggleAutotranslation;\n\tusing LogParticipantEditRank = MTPDchannelAdminLogEventActionParticipantEditRank;\n\n\tconst auto session = &history->session();\n\tconst auto id = event.vid().v;\n\tconst auto from = history->owner().user(event.vuser_id().v);\n\tconst auto channel = history->peer->asChannel();\n\tconst auto broadcast = channel->isBroadcast();\n\tconst auto &action = event.vaction();\n\tconst auto date = event.vdate().v;\n\tconst auto addPart = [&](\n\t\t\tnot_null<HistoryItem*> item,\n\t\t\tTimeId sentDate = 0,\n\t\t\tMsgId realId = MsgId()) {\n\t\treturn callback(OwnedItem(delegate, item), sentDate, realId);\n\t};\n\n\tconst auto fromName = from->name();\n\tconst auto fromLink = from->createOpenLink();\n\tconst auto fromLinkText = tr::link(fromName, QString());\n\n\tconst auto addSimpleServiceMessage = [&](\n\t\t\tconst TextWithEntities &text,\n\t\t\tMsgId realId = MsgId(),\n\t\t\tPhotoData *photo = nullptr) {\n\t\tauto message = PreparedServiceText{ text };\n\t\tmessage.links.push_back(fromLink);\n\t\taddPart(\n\t\t\thistory->makeMessage({\n\t\t\t\t.id = history->nextNonHistoryEntryId(),\n\t\t\t\t.flags = MessageFlag::AdminLogEntry,\n\t\t\t\t.from = from->id,\n\t\t\t\t.date = date,\n\t\t\t}, std::move(message), photo),\n\t\t\t0,\n\t\t\trealId);\n\t};\n\n\tconst auto createChangeTitle = [&](const LogTitle &action) {\n\t\tauto text = (channel->isMegagroup()\n\t\t\t? tr::lng_action_changed_title\n\t\t\t: tr::lng_admin_log_changed_title_channel)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_title,\n\t\t\t\t{ .text = qs(action.vnew_value()) },\n\t\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(std::move(text));\n\t};\n\n\tconst auto makeSimpleTextMessage = [&](TextWithEntities &&text) {\n\t\treturn history->makeMessage({\n\t\t\t.id = history->nextNonHistoryEntryId(),\n\t\t\t.flags = MessageFlag::HasFromId | MessageFlag::AdminLogEntry,\n\t\t\t.from = from->id,\n\t\t\t.date = date,\n\t\t}, std::move(text), MTP_messageMediaEmpty());\n\t};\n\n\tconst auto addSimpleTextMessage = [&](TextWithEntities &&text) {\n\t\taddPart(makeSimpleTextMessage(std::move(text)));\n\t};\n\n\tconst auto createChangeAbout = [&](const LogAbout &action) {\n\t\tconst auto newValue = qs(action.vnew_value());\n\t\tconst auto oldValue = qs(action.vprev_value());\n\t\tconst auto text = (channel->isMegagroup()\n\t\t\t? (newValue.isEmpty()\n\t\t\t\t? tr::lng_admin_log_removed_description_group\n\t\t\t\t: tr::lng_admin_log_changed_description_group)\n\t\t\t: (newValue.isEmpty()\n\t\t\t\t? tr::lng_admin_log_removed_description_channel\n\t\t\t\t: tr::lng_admin_log_changed_description_channel)\n\t\t\t)(tr::now, lt_from, fromLinkText, tr::marked);\n\t\taddSimpleServiceMessage(text);\n\n\t\tconst auto body = makeSimpleTextMessage(\n\t\t\tPrepareText(newValue, QString()));\n\t\tif (!oldValue.isEmpty()) {\n\t\t\tconst auto oldDescription = PrepareText(oldValue, QString());\n\t\t\tbody->addLogEntryOriginal(\n\t\t\t\tid,\n\t\t\t\ttr::lng_admin_log_previous_description(tr::now),\n\t\t\t\toldDescription);\n\t\t}\n\t\taddPart(body);\n\t};\n\n\tconst auto createChangeUsername = [&](const LogUsername &action) {\n\t\tconst auto newValue = qs(action.vnew_value());\n\t\tconst auto oldValue = qs(action.vprev_value());\n\t\tconst auto text = (channel->isMegagroup()\n\t\t\t? (newValue.isEmpty()\n\t\t\t\t? tr::lng_admin_log_removed_link_group\n\t\t\t\t: tr::lng_admin_log_changed_link_group)\n\t\t\t: (newValue.isEmpty()\n\t\t\t\t? tr::lng_admin_log_removed_link_channel\n\t\t\t\t: tr::lng_admin_log_changed_link_channel)\n\t\t\t)(tr::now, lt_from, fromLinkText, tr::marked);\n\t\taddSimpleServiceMessage(text);\n\n\t\tconst auto body = makeSimpleTextMessage(newValue.isEmpty()\n\t\t\t? TextWithEntities()\n\t\t\t: PrepareText(\n\t\t\t\thistory->session().createInternalLinkFull(newValue),\n\t\t\t\tQString()));\n\t\tif (!oldValue.isEmpty()) {\n\t\t\tconst auto oldLink = PrepareText(\n\t\t\t\thistory->session().createInternalLinkFull(oldValue),\n\t\t\t\tQString());\n\t\t\tbody->addLogEntryOriginal(\n\t\t\t\tid,\n\t\t\t\ttr::lng_admin_log_previous_link(tr::now),\n\t\t\t\toldLink);\n\t\t}\n\t\taddPart(body);\n\t};\n\n\tconst auto createChangePhoto = [&](const LogPhoto &action) {\n\t\taction.vnew_photo().match([&](const MTPDphoto &data) {\n\t\t\tconst auto photo = history->owner().processPhoto(data);\n\t\t\tconst auto text = (channel->isMegagroup()\n\t\t\t\t? tr::lng_admin_log_changed_photo_group\n\t\t\t\t: tr::lng_admin_log_changed_photo_channel)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\ttr::marked);\n\t\t\taddSimpleServiceMessage(text, MsgId(), photo);\n\t\t}, [&](const MTPDphotoEmpty &data) {\n\t\t\tconst auto text = (channel->isMegagroup()\n\t\t\t\t? tr::lng_admin_log_removed_photo_group\n\t\t\t\t: tr::lng_admin_log_removed_photo_channel)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\ttr::marked);\n\t\t\taddSimpleServiceMessage(text);\n\t\t});\n\t};\n\n\tconst auto createToggleInvites = [&](const LogInvites &action) {\n\t\tconst auto enabled = (action.vnew_value().type() == mtpc_boolTrue);\n\t\tconst auto text = (enabled\n\t\t\t? tr::lng_admin_log_invites_enabled\n\t\t\t: tr::lng_admin_log_invites_disabled)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto createToggleSignatures = [&](const LogSign &action) {\n\t\tconst auto enabled = (action.vnew_value().type() == mtpc_boolTrue);\n\t\tconst auto text = (enabled\n\t\t\t? tr::lng_admin_log_signatures_enabled\n\t\t\t: tr::lng_admin_log_signatures_disabled)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto createUpdatePinned = [&](const LogPin &action) {\n\t\taction.vmessage().match([&](const MTPDmessage &data) {\n\t\t\tconst auto pinned = data.is_pinned();\n\t\t\tconst auto realId = ExtractRealMsgId(action.vmessage());\n\t\t\tconst auto text = (pinned\n\t\t\t\t? tr::lng_admin_log_pinned_message\n\t\t\t\t: tr::lng_admin_log_unpinned_message)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\ttr::marked);\n\t\t\taddSimpleServiceMessage(text, realId);\n\n\t\t\taddPart(\n\t\t\t\thistory->createItem(\n\t\t\t\t\thistory->nextNonHistoryEntryId(),\n\t\t\t\t\tPrepareLogMessage(action.vmessage(), date),\n\t\t\t\t\tMessageFlag::AdminLogEntry),\n\t\t\t\tExtractSentDate(action.vmessage()),\n\t\t\t\trealId);\n\t\t}, [&](const auto &) {\n\t\t\tconst auto text = tr::lng_admin_log_unpinned_message(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\t\taddSimpleServiceMessage(text);\n\t\t});\n\t};\n\n\tconst auto createEditMessage = [&](const LogEdit &action) {\n\t\tconst auto realId = ExtractRealMsgId(action.vnew_message());\n\t\tconst auto sentDate = ExtractSentDate(action.vnew_message());\n\t\tconst auto newValue = ExtractEditedText(\n\t\t\tsession,\n\t\t\taction.vnew_message());\n\t\tauto oldValue = ExtractEditedText(\n\t\t\tsession,\n\t\t\taction.vprev_message());\n\n\t\tconst auto canHaveCaption = MediaCanHaveCaption(\n\t\t\taction.vnew_message());\n\t\tconst auto changedCaption = (newValue != oldValue);\n\t\tconst auto changedMedia = MediaId(action.vnew_message())\n\t\t\t!= MediaId(action.vprev_message());\n\t\tconst auto removedCaption = !oldValue.text.isEmpty()\n\t\t\t&& newValue.text.isEmpty();\n\t\tconst auto text = (!canHaveCaption\n\t\t\t? tr::lng_admin_log_edited_message\n\t\t\t: (changedMedia && removedCaption)\n\t\t\t? tr::lng_admin_log_edited_media_and_removed_caption\n\t\t\t: (changedMedia && changedCaption)\n\t\t\t? tr::lng_admin_log_edited_media_and_caption\n\t\t\t: changedMedia\n\t\t\t? tr::lng_admin_log_edited_media\n\t\t\t: removedCaption\n\t\t\t? tr::lng_admin_log_removed_caption\n\t\t\t: changedCaption\n\t\t\t? tr::lng_admin_log_edited_caption\n\t\t\t: tr::lng_admin_log_edited_message)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text, realId);\n\n\t\tconst auto body = history->createItem(\n\t\t\thistory->nextNonHistoryEntryId(),\n\t\t\tPrepareLogMessage(action.vnew_message(), date),\n\t\t\tMessageFlag::AdminLogEntry);\n\t\t/*\n\t\tif (oldValue.text.isEmpty()) {\n\t\t\toldValue = PrepareText(\n\t\t\t\tQString(),\n\t\t\t\ttr::lng_admin_log_empty_text(tr::now));\n\t\t}\n\n\t\tbody->addLogEntryOriginal(\n\t\t\tid,\n\t\t\t(canHaveCaption\n\t\t\t\t? tr::lng_admin_log_previous_caption\n\t\t\t\t: tr::lng_admin_log_previous_message)(tr::now),\n\t\t\toldValue);\n\t\t*/\n\t\taddPart(body, sentDate, realId);\n\n\t\taddSimpleServiceMessage({ u\"was\"_q }, realId);\n\t\taddPart(\n\t\t\thistory->createItem(\n\t\t\t\thistory->nextNonHistoryEntryId(),\n\t\t\t\tPrepareLogMessage(action.vprev_message(), date),\n\t\t\t\tMessageFlag::AdminLogEntry),\n\t\t\tExtractSentDate(action.vprev_message()),\n\t\t\tExtractRealMsgId(action.vprev_message()));\n\t};\n\n\tconst auto createDeleteMessage = [&](const LogDelete &action) {\n\t\tconst auto realId = ExtractRealMsgId(action.vmessage());\n\t\tconst auto text = tr::lng_admin_log_deleted_message(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText,\n\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text, realId);\n\n\t\taddPart(\n\t\t\thistory->createItem(\n\t\t\t\thistory->nextNonHistoryEntryId(),\n\t\t\t\tPrepareLogMessage(action.vmessage(), date),\n\t\t\t\tMessageFlag::AdminLogEntry),\n\t\t\tExtractSentDate(action.vmessage()),\n\t\t\trealId);\n\t};\n\n\tconst auto createParticipantJoin = [&](const LogJoin&) {\n\t\tconst auto text = (channel->isMegagroup()\n\t\t\t? tr::lng_admin_log_participant_joined\n\t\t\t: tr::lng_admin_log_participant_joined_channel)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto createParticipantLeave = [&](const LogLeave&) {\n\t\tconst auto text = (channel->isMegagroup()\n\t\t\t? tr::lng_admin_log_participant_left\n\t\t\t: tr::lng_admin_log_participant_left_channel)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto createParticipantInvite = [&](const LogInvite &action) {\n\t\taddSimpleTextMessage(\n\t\t\tGenerateParticipantChangeText(channel, action.vparticipant()));\n\t};\n\n\tconst auto createParticipantToggleBan = [&](const LogBan &action) {\n\t\taddSimpleTextMessage(\n\t\t\tGenerateParticipantChangeText(\n\t\t\t\tchannel,\n\t\t\t\taction.vnew_participant(),\n\t\t\t\taction.vprev_participant()));\n\t};\n\n\tconst auto createParticipantToggleAdmin = [&](const LogPromote &action) {\n\t\tif ((action.vnew_participant().type() == mtpc_channelParticipantAdmin)\n\t\t\t&& (action.vprev_participant().type()\n\t\t\t\t== mtpc_channelParticipantCreator)) {\n\t\t\t// In case of ownership transfer we show that message in\n\t\t\t// the \"User > Creator\" part and skip the \"Creator > Admin\" part.\n\t\t\treturn;\n\t\t}\n\t\taddSimpleTextMessage(\n\t\t\tGenerateParticipantChangeText(\n\t\t\t\tchannel,\n\t\t\t\taction.vnew_participant(),\n\t\t\t\taction.vprev_participant()));\n\t};\n\n\tconst auto createChangeStickerSet = [&](const LogSticker &action) {\n\t\tconst auto set = action.vnew_stickerset();\n\t\tconst auto removed = (set.type() == mtpc_inputStickerSetEmpty);\n\t\tif (removed) {\n\t\t\tconst auto text = tr::lng_admin_log_removed_stickers_group(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\t\taddSimpleServiceMessage(text);\n\t\t} else {\n\t\t\tconst auto text = tr::lng_admin_log_changed_stickers_group(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_sticker_set,\n\t\t\t\ttr::link(\n\t\t\t\t\ttr::lng_admin_log_changed_stickers_set(tr::now),\n\t\t\t\t\tQString()),\n\t\t\t\ttr::marked);\n\t\t\tconst auto setLink = std::make_shared<LambdaClickHandler>([=](\n\t\t\t\t\tClickContext context) {\n\t\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\t\t\tcontroller->show(\n\t\t\t\t\t\tBox<StickerSetBox>(\n\t\t\t\t\t\t\tcontroller->uiShow(),\n\t\t\t\t\t\t\tData::FromInputSet(set),\n\t\t\t\t\t\t\tData::StickersType::Stickers),\n\t\t\t\t\t\tUi::LayerOption::CloseOther);\n\t\t\t\t}\n\t\t\t});\n\t\t\tauto message = PreparedServiceText{ text };\n\t\t\tmessage.links.push_back(fromLink);\n\t\t\tmessage.links.push_back(setLink);\n\t\t\taddPart(history->makeMessage({\n\t\t\t\t.id = history->nextNonHistoryEntryId(),\n\t\t\t\t.flags = MessageFlag::AdminLogEntry,\n\t\t\t\t.from = from->id,\n\t\t\t\t.date = date,\n\t\t\t}, std::move(message)));\n\t\t}\n\t};\n\n\tconst auto createChangeEmojiSet = [&](const LogEmoji &action) {\n\t\tconst auto set = action.vnew_stickerset();\n\t\tconst auto removed = (set.type() == mtpc_inputStickerSetEmpty);\n\t\tif (removed) {\n\t\t\tconst auto text = tr::lng_admin_log_removed_emoji_group(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\t\taddSimpleServiceMessage(text);\n\t\t} else {\n\t\t\tconst auto text = tr::lng_admin_log_changed_emoji_group(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_sticker_set,\n\t\t\t\ttr::link(\n\t\t\t\t\ttr::lng_admin_log_changed_emoji_set(tr::now),\n\t\t\t\t\tQString()),\n\t\t\t\ttr::marked);\n\t\t\tconst auto setLink = std::make_shared<LambdaClickHandler>([=](\n\t\t\t\t\tClickContext context) {\n\t\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\t\t\tcontroller->show(\n\t\t\t\t\t\tBox<StickerSetBox>(\n\t\t\t\t\t\t\tcontroller->uiShow(),\n\t\t\t\t\t\t\tData::FromInputSet(set),\n\t\t\t\t\t\t\tData::StickersType::Emoji),\n\t\t\t\t\t\tUi::LayerOption::CloseOther);\n\t\t\t\t}\n\t\t\t});\n\t\t\tauto message = PreparedServiceText{ text };\n\t\t\tmessage.links.push_back(fromLink);\n\t\t\tmessage.links.push_back(setLink);\n\t\t\taddPart(history->makeMessage({\n\t\t\t\t.id = history->nextNonHistoryEntryId(),\n\t\t\t\t.flags = MessageFlag::AdminLogEntry,\n\t\t\t\t.from = from->id,\n\t\t\t\t.date = date,\n\t\t\t}, std::move(message)));\n\t\t}\n\t};\n\n\tconst auto createTogglePreHistoryHidden = [&](\n\t\t\tconst LogPreHistory &action) {\n\t\tconst auto hidden = (action.vnew_value().type() == mtpc_boolTrue);\n\t\tconst auto text = (hidden\n\t\t\t? tr::lng_admin_log_history_made_hidden\n\t\t\t: tr::lng_admin_log_history_made_visible)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto createDefaultBannedRights = [&](\n\t\t\tconst LogPermissions &action) {\n\t\taddSimpleTextMessage(\n\t\t\tGenerateDefaultBannedRightsChangeText(\n\t\t\t\tchannel,\n\t\t\t\tChatRestrictionsInfo(action.vnew_banned_rights()),\n\t\t\t\tChatRestrictionsInfo(action.vprev_banned_rights())));\n\t};\n\n\tconst auto createStopPoll = [&](const LogPoll &action) {\n\t\tconst auto realId = ExtractRealMsgId(action.vmessage());\n\t\tconst auto text = tr::lng_admin_log_stopped_poll(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText,\n\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text, realId);\n\n\t\taddPart(\n\t\t\thistory->createItem(\n\t\t\t\thistory->nextNonHistoryEntryId(),\n\t\t\t\tPrepareLogMessage(action.vmessage(), date),\n\t\t\t\tMessageFlag::AdminLogEntry),\n\t\t\tExtractSentDate(action.vmessage()),\n\t\t\trealId);\n\t};\n\n\tconst auto createChangeLinkedChat = [&](const LogDiscussion &action) {\n\t\tconst auto now = history->owner().channelLoaded(\n\t\t\taction.vnew_value().v);\n\t\tif (!now) {\n\t\t\tconst auto text = (broadcast\n\t\t\t\t? tr::lng_admin_log_removed_linked_chat\n\t\t\t\t: tr::lng_admin_log_removed_linked_channel)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\ttr::marked);\n\t\t\taddSimpleServiceMessage(text);\n\t\t} else {\n\t\t\tconst auto text = (broadcast\n\t\t\t\t? tr::lng_admin_log_changed_linked_chat\n\t\t\t\t: tr::lng_admin_log_changed_linked_channel)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_chat,\n\t\t\t\t\ttr::link(now->name(), QString()),\n\t\t\t\t\ttr::marked);\n\t\t\tconst auto chatLink = std::make_shared<LambdaClickHandler>([=] {\n\t\t\t\tif (const auto window = now->session().tryResolveWindow()) {\n\t\t\t\t\twindow->showPeerHistory(now);\n\t\t\t\t}\n\t\t\t});\n\t\t\tauto message = PreparedServiceText{ text };\n\t\t\tmessage.links.push_back(fromLink);\n\t\t\tmessage.links.push_back(chatLink);\n\t\t\taddPart(history->makeMessage({\n\t\t\t\t.id = history->nextNonHistoryEntryId(),\n\t\t\t\t.flags = MessageFlag::AdminLogEntry,\n\t\t\t\t.from = from->id,\n\t\t\t\t.date = date,\n\t\t\t}, std::move(message)));\n\t\t}\n\t};\n\n\tconst auto createChangeLocation = [&](const LogLocation &action) {\n\t\taction.vnew_value().match([&](const MTPDchannelLocation &data) {\n\t\t\tconst auto address = qs(data.vaddress());\n\t\t\tconst auto link = data.vgeo_point().match([&](\n\t\t\t\t\tconst MTPDgeoPoint &data) {\n\t\t\t\treturn tr::link(\n\t\t\t\t\taddress,\n\t\t\t\t\tLocationClickHandler::Url(Data::LocationPoint(data)));\n\t\t\t}, [&](const MTPDgeoPointEmpty &) {\n\t\t\t\treturn TextWithEntities{ .text = address };\n\t\t\t});\n\t\t\tconst auto text = tr::lng_admin_log_changed_location_chat(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_address,\n\t\t\t\tlink,\n\t\t\t\ttr::marked);\n\t\t\taddSimpleServiceMessage(text);\n\t\t}, [&](const MTPDchannelLocationEmpty &) {\n\t\t\tconst auto text = tr::lng_admin_log_removed_location_chat(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\t\taddSimpleServiceMessage(text);\n\t\t});\n\t};\n\n\tconst auto createToggleSlowMode = [&](const LogSlowMode &action) {\n\t\tif (const auto seconds = action.vnew_value().v) {\n\t\t\tconst auto duration = (seconds >= 60)\n\t\t\t\t? tr::lng_minutes(tr::now, lt_count, seconds / 60)\n\t\t\t\t: tr::lng_seconds(tr::now, lt_count, seconds);\n\t\t\tconst auto text = tr::lng_admin_log_changed_slow_mode(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_duration,\n\t\t\t\t{ .text = duration },\n\t\t\t\ttr::marked);\n\t\t\taddSimpleServiceMessage(text);\n\t\t} else {\n\t\t\tconst auto text = tr::lng_admin_log_removed_slow_mode(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\t\taddSimpleServiceMessage(text);\n\t\t}\n\t};\n\n\tconst auto createStartGroupCall = [&](const LogStartCall &data) {\n\t\tconst auto text = (broadcast\n\t\t\t? tr::lng_admin_log_started_group_call_channel\n\t\t\t: tr::lng_admin_log_started_group_call)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto createDiscardGroupCall = [&](const LogDiscardCall &data) {\n\t\tconst auto text = (broadcast\n\t\t\t? tr::lng_admin_log_discarded_group_call_channel\n\t\t\t: tr::lng_admin_log_discarded_group_call)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto groupCallParticipantPeer = [&](\n\t\t\tconst MTPGroupCallParticipant &data) {\n\t\treturn data.match([&](const MTPDgroupCallParticipant &data) {\n\t\t\treturn history->owner().peer(peerFromMTP(data.vpeer()));\n\t\t});\n\t};\n\n\tconst auto addServiceMessageWithLink = [&](\n\t\t\tconst TextWithEntities &text,\n\t\t\tconst ClickHandlerPtr &link) {\n\t\tauto message = PreparedServiceText{ text };\n\t\tmessage.links.push_back(fromLink);\n\t\tmessage.links.push_back(link);\n\t\taddPart(history->makeMessage({\n\t\t\t.id = history->nextNonHistoryEntryId(),\n\t\t\t.flags = MessageFlag::AdminLogEntry,\n\t\t\t.from = from->id,\n\t\t\t.date = date,\n\t\t}, std::move(message)));\n\t};\n\n\tconst auto createParticipantMute = [&](const LogMute &data) {\n\t\tconst auto participantPeer = groupCallParticipantPeer(\n\t\t\tdata.vparticipant());\n\t\tconst auto participantPeerLink = participantPeer->createOpenLink();\n\t\tconst auto participantPeerLinkText = tr::link(\n\t\t\tparticipantPeer->name(),\n\t\t\tQString());\n\t\tconst auto text = (broadcast\n\t\t\t? tr::lng_admin_log_muted_participant_channel\n\t\t\t: tr::lng_admin_log_muted_participant)(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText,\n\t\t\tlt_user,\n\t\t\tparticipantPeerLinkText,\n\t\t\ttr::marked);\n\t\taddServiceMessageWithLink(text, participantPeerLink);\n\t};\n\n\tconst auto createParticipantUnmute = [&](const LogUnmute &data) {\n\t\tconst auto participantPeer = groupCallParticipantPeer(\n\t\t\tdata.vparticipant());\n\t\tconst auto participantPeerLink = participantPeer->createOpenLink();\n\t\tconst auto participantPeerLinkText = tr::link(\n\t\t\tparticipantPeer->name(),\n\t\t\tQString());\n\t\tconst auto text = (broadcast\n\t\t\t? tr::lng_admin_log_unmuted_participant_channel\n\t\t\t: tr::lng_admin_log_unmuted_participant)(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText,\n\t\t\tlt_user,\n\t\t\tparticipantPeerLinkText,\n\t\t\ttr::marked);\n\t\taddServiceMessageWithLink(text, participantPeerLink);\n\t};\n\n\tconst auto createToggleGroupCallSetting = [&](\n\t\t\tconst LogCallSetting &data) {\n\t\tconst auto text = (mtpIsTrue(data.vjoin_muted())\n\t\t\t? (broadcast\n\t\t\t\t? tr::lng_admin_log_disallowed_unmute_self_channel\n\t\t\t\t: tr::lng_admin_log_disallowed_unmute_self)\n\t\t\t: (broadcast\n\t\t\t\t? tr::lng_admin_log_allowed_unmute_self_channel\n\t\t\t\t: tr::lng_admin_log_allowed_unmute_self))(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText,\n\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto addInviteLinkServiceMessage = [&](\n\t\t\tconst TextWithEntities &text,\n\t\t\tconst MTPExportedChatInvite &data,\n\t\t\tClickHandlerPtr additional = nullptr) {\n\t\tauto message = PreparedServiceText{ text };\n\t\tmessage.links.push_back(fromLink);\n\t\tif (!ExtractInviteLink(data).endsWith(Ui::kQEllipsis)) {\n\t\t\tmessage.links.push_back(std::make_shared<UrlClickHandler>(\n\t\t\t\tInternalInviteLinkUrl(data)));\n\t\t}\n\t\tif (additional) {\n\t\t\tmessage.links.push_back(std::move(additional));\n\t\t}\n\t\taddPart(history->makeMessage({\n\t\t\t.id = history->nextNonHistoryEntryId(),\n\t\t\t.flags = MessageFlag::AdminLogEntry,\n\t\t\t.from = from->id,\n\t\t\t.date = date,\n\t\t}, std::move(message)));\n\t};\n\n\tconst auto createParticipantJoinByInvite = [&](\n\t\t\tconst LogJoinByInvite &data) {\n\t\tconst auto text = data.is_via_chatlist()\n\t\t\t? (channel->isMegagroup()\n\t\t\t\t? tr::lng_admin_log_participant_joined_by_filter_link\n\t\t\t\t: tr::lng_admin_log_participant_joined_by_filter_link_channel)\n\t\t\t: (channel->isMegagroup()\n\t\t\t\t? tr::lng_admin_log_participant_joined_by_link\n\t\t\t\t: tr::lng_admin_log_participant_joined_by_link_channel);\n\t\taddInviteLinkServiceMessage(\n\t\t\ttext(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_link,\n\t\t\t\tGenerateInviteLinkLink(data.vinvite()),\n\t\t\t\ttr::marked),\n\t\t\tdata.vinvite());\n\t};\n\n\tconst auto createExportedInviteDelete = [&](const LogInviteDelete &data) {\n\t\taddInviteLinkServiceMessage(\n\t\t\ttr::lng_admin_log_delete_invite_link(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_link,\n\t\t\t\tGenerateInviteLinkLink(data.vinvite()),\n\t\t\t\ttr::marked),\n\t\t\tdata.vinvite());\n\t};\n\n\tconst auto createExportedInviteRevoke = [&](const LogInviteRevoke &data) {\n\t\taddInviteLinkServiceMessage(\n\t\t\ttr::lng_admin_log_revoke_invite_link(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_link,\n\t\t\t\tGenerateInviteLinkLink(data.vinvite()),\n\t\t\t\ttr::marked),\n\t\t\tdata.vinvite());\n\t};\n\n\tconst auto createExportedInviteEdit = [&](const LogInviteEdit &data) {\n\t\taddSimpleTextMessage(\n\t\t\tGenerateInviteLinkChangeText(\n\t\t\t\tdata.vnew_invite(),\n\t\t\t\tdata.vprev_invite()));\n\t};\n\n\tconst auto createParticipantVolume = [&](const LogVolume &data) {\n\t\tconst auto participantPeer = groupCallParticipantPeer(\n\t\t\tdata.vparticipant());\n\t\tconst auto participantPeerLink = participantPeer->createOpenLink();\n\t\tconst auto participantPeerLinkText = tr::link(\n\t\t\tparticipantPeer->name(),\n\t\t\tQString());\n\t\tconst auto volume = data.vparticipant().match([&](\n\t\t\t\tconst MTPDgroupCallParticipant &data) {\n\t\t\treturn data.vvolume().value_or(10000);\n\t\t});\n\t\tconst auto volumeText = QString::number(volume / 100) + '%';\n\t\tauto text = (broadcast\n\t\t\t? tr::lng_admin_log_participant_volume_channel\n\t\t\t: tr::lng_admin_log_participant_volume)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_user,\n\t\t\t\tparticipantPeerLinkText,\n\t\t\t\tlt_percent,\n\t\t\t\t{ .text = volumeText },\n\t\t\t\ttr::marked);\n\t\taddServiceMessageWithLink(text, participantPeerLink);\n\t};\n\n\tconst auto createChangeHistoryTTL = [&](const LogTTL &data) {\n\t\tconst auto was = data.vprev_value().v;\n\t\tconst auto now = data.vnew_value().v;\n\t\tconst auto wrap = [](int duration) -> TextWithEntities {\n\t\t\tconst auto text = (duration == 5)\n\t\t\t\t? u\"5 seconds\"_q\n\t\t\t\t: Ui::FormatTTL(duration);\n\t\t\treturn { .text = text };\n\t\t};\n\t\tconst auto text = !was\n\t\t\t? tr::lng_admin_log_messages_ttl_set(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_duration,\n\t\t\t\twrap(now),\n\t\t\t\ttr::marked)\n\t\t\t: !now\n\t\t\t? tr::lng_admin_log_messages_ttl_removed(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_duration,\n\t\t\t\twrap(was),\n\t\t\t\ttr::marked)\n\t\t\t: tr::lng_admin_log_messages_ttl_changed(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_previous,\n\t\t\t\twrap(was),\n\t\t\t\tlt_duration,\n\t\t\t\twrap(now),\n\t\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto createParticipantJoinByRequest = [&](\n\t\t\tconst LogJoinByRequest &data) {\n\t\tconst auto user = channel->owner().user(UserId(data.vapproved_by()));\n\t\tconst auto linkText = GenerateInviteLinkLink(data.vinvite());\n\t\tconst auto text = (linkText.text == PublicJoinLink())\n\t\t\t? (channel->isMegagroup()\n\t\t\t\t? tr::lng_admin_log_participant_approved_by_request\n\t\t\t\t: tr::lng_admin_log_participant_approved_by_request_channel)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::link(user->name(), QString()),\n\t\t\t\t\ttr::marked)\n\t\t\t: (channel->isMegagroup()\n\t\t\t\t? tr::lng_admin_log_participant_approved_by_link\n\t\t\t\t: tr::lng_admin_log_participant_approved_by_link_channel)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_link,\n\t\t\t\t\tlinkText,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::link(user->name(), QString()),\n\t\t\t\t\ttr::marked);\n\t\taddInviteLinkServiceMessage(\n\t\t\ttext,\n\t\t\tdata.vinvite(),\n\t\t\tuser->createOpenLink());\n\t};\n\n\tconst auto createToggleNoForwards = [&](const LogNoForwards &data) {\n\t\tconst auto disabled = (data.vnew_value().type() == mtpc_boolTrue);\n\t\tconst auto text = (disabled\n\t\t\t? tr::lng_admin_log_forwards_disabled\n\t\t\t: tr::lng_admin_log_forwards_enabled)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto createSendMessage = [&](const LogSendMessage &data) {\n\t\tconst auto realId = ExtractRealMsgId(data.vmessage());\n\t\tconst auto text = tr::lng_admin_log_sent_message(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText,\n\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text, realId);\n\n\t\taddPart(\n\t\t\thistory->createItem(\n\t\t\t\thistory->nextNonHistoryEntryId(),\n\t\t\t\tPrepareLogMessage(data.vmessage(), date),\n\t\t\t\tMessageFlag::AdminLogEntry),\n\t\t\tExtractSentDate(data.vmessage()),\n\t\t\trealId);\n\t};\n\n\tconst auto createChangeAvailableReactions = [&](\n\t\t\tconst LogChangeAvailableReactions &data) {\n\t\tconst auto text = data.vnew_value().match([&](\n\t\t\t\tconst MTPDchatReactionsNone&) {\n\t\t\treturn tr::lng_admin_log_reactions_disabled(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\t}, [&](const MTPDchatReactionsSome &data) {\n\t\t\tusing namespace Window::Notifications;\n\t\t\tauto list = TextWithEntities();\n\t\t\tfor (const auto &one : data.vreactions().v) {\n\t\t\t\tif (!list.empty()) {\n\t\t\t\t\tlist.append(\", \");\n\t\t\t\t}\n\t\t\t\tlist.append(Manager::ComposeReactionEmoji(\n\t\t\t\t\tsession,\n\t\t\t\t\tData::ReactionFromMTP(one)));\n\t\t\t}\n\t\t\treturn tr::lng_admin_log_reactions_updated(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_emoji,\n\t\t\t\tlist,\n\t\t\t\ttr::marked);\n\t\t}, [&](const MTPDchatReactionsAll &data) {\n\t\t\treturn (data.is_allow_custom()\n\t\t\t\t? tr::lng_admin_log_reactions_allowed_all\n\t\t\t\t: tr::lng_admin_log_reactions_allowed_official)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\ttr::marked);\n\t\t});\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto createChangeUsernames = [&](const LogChangeUsernames &data) {\n\t\tconst auto newValue = data.vnew_value().v;\n\t\tconst auto oldValue = data.vprev_value().v;\n\n\t\tconst auto list = [&](const auto &tlList) {\n\t\t\tauto result = TextWithEntities();\n\t\t\tfor (const auto &tlValue : tlList) {\n\t\t\t\tresult.append(PrepareText(\n\t\t\t\t\thistory->session().createInternalLinkFull(qs(tlValue)),\n\t\t\t\t\tQString()));\n\t\t\t\tresult.append('\\n');\n\t\t\t}\n\t\t\treturn result;\n\t\t};\n\n\t\tif (newValue.size() == oldValue.size()) {\n\t\t\tif (newValue.size() == 1) {\n\t\t\t\tconst auto tl = MTP_channelAdminLogEventActionChangeUsername(\n\t\t\t\t\tnewValue.front(),\n\t\t\t\t\toldValue.front());\n\t\t\t\ttl.match([&](const LogUsername &data) {\n\t\t\t\t\tcreateChangeUsername(data);\n\t\t\t\t}, [](const auto &) {\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tconst auto wasReordered = [&] {\n\t\t\t\t\tfor (const auto &newLink : newValue) {\n\t\t\t\t\t\tif (!ranges::contains(oldValue, newLink)) {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t}();\n\t\t\t\tif (wasReordered) {\n\t\t\t\t\taddSimpleServiceMessage((channel->isMegagroup()\n\t\t\t\t\t\t? tr::lng_admin_log_reordered_link_group\n\t\t\t\t\t\t: tr::lng_admin_log_reordered_link_channel)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\t\tfromLinkText,\n\t\t\t\t\t\t\ttr::marked));\n\t\t\t\t\tconst auto body = makeSimpleTextMessage(list(newValue));\n\t\t\t\t\tbody->addLogEntryOriginal(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\ttr::lng_admin_log_previous_links_order(tr::now),\n\t\t\t\t\t\tlist(oldValue));\n\t\t\t\t\taddPart(body);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (std::abs(newValue.size() - oldValue.size()) == 1) {\n\t\t\tconst auto activated = newValue.size() > oldValue.size();\n\t\t\tconst auto changed = [&] {\n\t\t\t\tconst auto value = activated ? oldValue : newValue;\n\t\t\t\tfor (const auto &link : (activated ? newValue : oldValue)) {\n\t\t\t\t\tif (!ranges::contains(value, link)) {\n\t\t\t\t\t\treturn qs(link);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn QString();\n\t\t\t}();\n\t\t\taddSimpleServiceMessage((activated\n\t\t\t\t? tr::lng_admin_log_activated_link\n\t\t\t\t: tr::lng_admin_log_deactivated_link)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_link,\n\t\t\t\t\t{ changed },\n\t\t\t\t\ttr::marked));\n\t\t\treturn;\n\t\t}\n\t\t// Probably will never happen.\n\t\tauto resultText = fromLinkText;\n\t\taddSimpleServiceMessage(resultText.append({\n\t\t\t.text = channel->isMegagroup()\n\t\t\t\t? u\" changed list of group links:\"_q\n\t\t\t\t: u\" changed list of channel links:\"_q,\n\t\t}));\n\t\tconst auto body = makeSimpleTextMessage(list(newValue));\n\t\tbody->addLogEntryOriginal(\n\t\t\tid,\n\t\t\t\"Previous links\",\n\t\t\tlist(oldValue));\n\t\taddPart(body);\n\t};\n\n\tconst auto createToggleForum = [&](const LogToggleForum &data) {\n\t\tconst auto enabled = (data.vnew_value().type() == mtpc_boolTrue);\n\t\tconst auto text = (enabled\n\t\t\t? tr::lng_admin_log_topics_enabled\n\t\t\t: tr::lng_admin_log_topics_disabled)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto createCreateTopic = [&](const LogCreateTopic &data) {\n\t\tauto topicLink = GenerateTopicLink(channel, data.vtopic());\n\t\taddSimpleServiceMessage(tr::lng_admin_log_topics_created(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText,\n\t\t\tlt_topic,\n\t\t\ttopicLink,\n\t\t\ttr::marked));\n\t};\n\n\tconst auto createEditTopic = [&](const LogEditTopic &data) {\n\t\tconst auto prevLink = GenerateTopicLink(channel, data.vprev_topic());\n\t\tconst auto nowLink = GenerateTopicLink(channel, data.vnew_topic());\n\t\tif (prevLink != nowLink) {\n\t\t\taddSimpleServiceMessage(tr::lng_admin_log_topics_changed(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_topic,\n\t\t\t\tprevLink,\n\t\t\t\tlt_new_topic,\n\t\t\t\tnowLink,\n\t\t\t\ttr::marked));\n\t\t}\n\t\tconst auto wasClosed = IsTopicClosed(data.vprev_topic());\n\t\tconst auto nowClosed = IsTopicClosed(data.vnew_topic());\n\t\tif (nowClosed != wasClosed) {\n\t\t\taddSimpleServiceMessage((nowClosed\n\t\t\t\t? tr::lng_admin_log_topics_closed\n\t\t\t\t: tr::lng_admin_log_topics_reopened)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_topic,\n\t\t\t\t\tnowLink,\n\t\t\t\t\ttr::marked));\n\t\t}\n\t\tconst auto wasHidden = IsTopicHidden(data.vprev_topic());\n\t\tconst auto nowHidden = IsTopicHidden(data.vnew_topic());\n\t\tif (nowHidden != wasHidden) {\n\t\t\taddSimpleServiceMessage((nowHidden\n\t\t\t\t? tr::lng_admin_log_topics_hidden\n\t\t\t\t: tr::lng_admin_log_topics_unhidden)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_topic,\n\t\t\t\t\tnowLink,\n\t\t\t\t\ttr::marked));\n\t\t}\n\t};\n\n\tconst auto createDeleteTopic = [&](const LogDeleteTopic &data) {\n\t\tauto topicLink = GenerateTopicLink(channel, data.vtopic());\n\t\tif (!topicLink.entities.empty()) {\n\t\t\ttopicLink.entities.erase(topicLink.entities.begin());\n\t\t}\n\t\taddSimpleServiceMessage(tr::lng_admin_log_topics_deleted(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText,\n\t\t\tlt_topic,\n\t\t\ttopicLink,\n\t\t\ttr::marked));\n\t};\n\n\tconst auto createPinTopic = [&](const LogPinTopic &data) {\n\t\tif (const auto &topic = data.vnew_topic()) {\n\t\t\tauto topicLink = GenerateTopicLink(channel, *topic);\n\t\t\taddSimpleServiceMessage(tr::lng_admin_log_topics_pinned(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_topic,\n\t\t\t\ttopicLink,\n\t\t\t\ttr::marked));\n\t\t} else if (const auto &previous = data.vprev_topic()) {\n\t\t\tauto topicLink = GenerateTopicLink(channel, *previous);\n\t\t\taddSimpleServiceMessage(tr::lng_admin_log_topics_unpinned(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_topic,\n\t\t\t\ttopicLink,\n\t\t\t\ttr::marked));\n\t\t}\n\t};\n\n\tconst auto createToggleAntiSpam = [&](const LogToggleAntiSpam &data) {\n\t\tconst auto enabled = (data.vnew_value().type() == mtpc_boolTrue);\n\t\tconst auto text = (enabled\n\t\t\t? tr::lng_admin_log_antispam_enabled\n\t\t\t: tr::lng_admin_log_antispam_disabled)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto createColorChange = [&](\n\t\t\tconst MTPPeerColor &was,\n\t\t\tconst MTPPeerColor &now,\n\t\t\tconst auto &colorPhrase,\n\t\t\tconst auto &setEmoji,\n\t\t\tconst auto &removeEmoji,\n\t\t\tconst auto &changeEmoji) {\n\t\tconst auto prevColor = (was.type() == mtpc_peerColor)\n\t\t\t? was.c_peerColor().vcolor()\n\t\t\t: tl::conditional<MTPint>();\n\t\tconst auto nextColor = (now.type() == mtpc_peerColor)\n\t\t\t? now.c_peerColor().vcolor()\n\t\t\t: tl::conditional<MTPint>();\n\t\tif (prevColor != nextColor) {\n\t\t\tconst auto wrap = [&](tl::conditional<MTPint> value) {\n\t\t\t\treturn value\n\t\t\t\t\t? value->v\n\t\t\t\t\t: Data::DecideColorIndex(history->peer->id);\n\t\t\t};\n\t\t\tconst auto text = colorPhrase(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_previous,\n\t\t\t\t{ '#' + QString::number(wrap(prevColor) + 1) },\n\t\t\t\tlt_color,\n\t\t\t\t{ '#' + QString::number(wrap(nextColor) + 1) },\n\t\t\t\ttr::marked);\n\t\t\taddSimpleServiceMessage(text);\n\t\t}\n\t\tconst auto prevEmoji = (was.type() == mtpc_peerColor)\n\t\t\t? was.c_peerColor().vbackground_emoji_id().value_or_empty()\n\t\t\t: uint64();\n\t\tconst auto nextEmoji = (now.type() == mtpc_peerColor)\n\t\t\t? now.c_peerColor().vbackground_emoji_id().value_or_empty()\n\t\t\t: uint64();\n\t\tif (prevEmoji != nextEmoji) {\n\t\t\tconst auto text = !prevEmoji\n\t\t\t\t? setEmoji(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\tUi::Text::SingleCustomEmoji(\n\t\t\t\t\t\tData::SerializeCustomEmojiId(nextEmoji)),\n\t\t\t\t\ttr::marked)\n\t\t\t\t: !nextEmoji\n\t\t\t\t? removeEmoji(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\tUi::Text::SingleCustomEmoji(\n\t\t\t\t\t\tData::SerializeCustomEmojiId(prevEmoji)),\n\t\t\t\t\ttr::marked)\n\t\t\t\t: changeEmoji(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_previous,\n\t\t\t\t\tUi::Text::SingleCustomEmoji(\n\t\t\t\t\t\tData::SerializeCustomEmojiId(prevEmoji)),\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\tUi::Text::SingleCustomEmoji(\n\t\t\t\t\t\tData::SerializeCustomEmojiId(nextEmoji)),\n\t\t\t\t\ttr::marked);\n\t\t\taddSimpleServiceMessage(text);\n\t\t}\n\t};\n\n\tconst auto createChangePeerColor = [&](const LogChangePeerColor &data) {\n\t\tcreateColorChange(\n\t\t\tdata.vprev_value(),\n\t\t\tdata.vnew_value(),\n\t\t\ttr::lng_admin_log_change_color,\n\t\t\ttr::lng_admin_log_set_background_emoji,\n\t\t\ttr::lng_admin_log_removed_background_emoji,\n\t\t\ttr::lng_admin_log_change_background_emoji);\n\t};\n\n\tconst auto createChangeProfilePeerColor = [&](const LogChangeProfilePeerColor &data) {\n\t\tconst auto group = channel->isMegagroup();\n\t\tcreateColorChange(\n\t\t\tdata.vprev_value(),\n\t\t\tdata.vnew_value(),\n\t\t\t(group\n\t\t\t\t? tr::lng_admin_log_change_profile_color_group\n\t\t\t\t: tr::lng_admin_log_change_profile_color),\n\t\t\t(group\n\t\t\t\t? tr::lng_admin_log_set_profile_background_emoji_group\n\t\t\t\t: tr::lng_admin_log_set_profile_background_emoji),\n\t\t\t(group\n\t\t\t\t? tr::lng_admin_log_removed_profile_background_emoji_group\n\t\t\t\t: tr::lng_admin_log_removed_profile_background_emoji),\n\t\t\t(group\n\t\t\t\t? tr::lng_admin_log_change_profile_background_emoji_group\n\t\t\t\t: tr::lng_admin_log_change_profile_background_emoji));\n\t};\n\n\tconst auto createChangeWallpaper = [&](const LogChangeWallpaper &data) {\n\t\taddSimpleServiceMessage(tr::lng_admin_log_change_wallpaper(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText,\n\t\t\ttr::marked));\n\t};\n\n\tconst auto createChangeEmojiStatus = [&](const LogChangeEmojiStatus &data) {\n\t\tconst auto parse = [](const MTPEmojiStatus &status) {\n\t\t\treturn status.match([](\n\t\t\t\t\tconst MTPDemojiStatus &data) {\n\t\t\t\treturn data.vdocument_id().v;\n\t\t\t}, [](const MTPDemojiStatusCollectible &data) {\n\t\t\t\treturn data.vdocument_id().v;\n\t\t\t}, [](const MTPDemojiStatusEmpty &) {\n\t\t\t\treturn DocumentId();\n\t\t\t}, [](const MTPDinputEmojiStatusCollectible &) {\n\t\t\t\treturn DocumentId();\n\t\t\t});\n\t\t};\n\t\tconst auto prevEmoji = parse(data.vprev_value());\n\t\tconst auto nextEmoji = parse(data.vnew_value());\n\t\tconst auto nextUntil = data.vnew_value().match([](\n\t\t\t\tconst MTPDemojiStatus &data) {\n\t\t\treturn TimeId(data.vuntil().value_or_empty());\n\t\t}, [](const auto &) { return TimeId(); });\n\n\t\tconst auto text = !prevEmoji\n\t\t\t? (nextUntil\n\t\t\t\t? tr::lng_admin_log_set_status_until(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\tUi::Text::SingleCustomEmoji(\n\t\t\t\t\t\tData::SerializeCustomEmojiId(nextEmoji)),\n\t\t\t\t\tlt_date,\n\t\t\t\t\tTextWithEntities{\n\t\t\t\t\t\tlangDateTime(base::unixtime::parse(nextUntil)) },\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_admin_log_set_status(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\tUi::Text::SingleCustomEmoji(\n\t\t\t\t\t\tData::SerializeCustomEmojiId(nextEmoji)),\n\t\t\t\t\ttr::marked))\n\t\t\t: !nextEmoji\n\t\t\t? tr::lng_admin_log_removed_status(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_emoji,\n\t\t\t\tUi::Text::SingleCustomEmoji(\n\t\t\t\t\tData::SerializeCustomEmojiId(prevEmoji)),\n\t\t\t\ttr::marked)\n\t\t\t: (nextUntil\n\t\t\t\t? tr::lng_admin_log_change_status_until(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_previous,\n\t\t\t\t\tUi::Text::SingleCustomEmoji(\n\t\t\t\t\t\tData::SerializeCustomEmojiId(prevEmoji)),\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\tUi::Text::SingleCustomEmoji(\n\t\t\t\t\t\tData::SerializeCustomEmojiId(nextEmoji)),\n\t\t\t\t\tlt_date,\n\t\t\t\t\tTextWithEntities{\n\t\t\t\t\t\tlangDateTime(base::unixtime::parse(nextUntil)) },\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_admin_log_change_status(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_previous,\n\t\t\t\t\tUi::Text::SingleCustomEmoji(\n\t\t\t\t\t\tData::SerializeCustomEmojiId(prevEmoji)),\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\tUi::Text::SingleCustomEmoji(\n\t\t\t\t\t\tData::SerializeCustomEmojiId(nextEmoji)),\n\t\t\t\t\ttr::marked));\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto createToggleSignatureProfiles = [&](const LogToggleSignatureProfiles &action) {\n\t\tconst auto enabled = (action.vnew_value().type() == mtpc_boolTrue);\n\t\tconst auto text = (enabled\n\t\t\t? tr::lng_admin_log_signature_profiles_enabled\n\t\t\t: tr::lng_admin_log_signature_profiles_disabled)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto createParticipantSubExtend = [&](const LogParticipantSubExtend &action) {\n\t\tconst auto participant = Api::ChatParticipant(\n\t\t\taction.vnew_participant(),\n\t\t\tchannel);\n\t\tif (!participant.subscriptionDate()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto participantPeer = channel->owner().peer(participant.id());\n\t\tconst auto participantPeerLink = participantPeer->createOpenLink();\n\t\tconst auto participantPeerLinkText = tr::link(\n\t\t\tparticipantPeer->name(),\n\t\t\tQString());\n\t\tconst auto parsed = base::unixtime::parse(\n\t\t\tparticipant.subscriptionDate());\n\t\taddServiceMessageWithLink(\n\t\t\ttr::lng_admin_log_subscription_extend(\n\t\t\t\ttr::now,\n\t\t\t\tlt_name,\n\t\t\t\tparticipantPeerLinkText,\n\t\t\t\tlt_date,\n\t\t\t\t{ langDateTimeFull(parsed) },\n\t\t\t\ttr::marked),\n\t\t\tparticipantPeerLink);\n\t};\n\n\tconst auto createToggleAutotranslation = [&](const LogToggleAutotranslation &action) {\n\t\tconst auto enabled = mtpIsTrue(action.vnew_value());\n\t\tconst auto text = (enabled\n\t\t\t? tr::lng_admin_log_autotranslate_enabled\n\t\t\t: tr::lng_admin_log_autotranslate_disabled)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\ttr::marked);\n\t\taddSimpleServiceMessage(text);\n\t};\n\n\tconst auto createParticipantEditRank = [&](const LogParticipantEditRank &action) {\n\t\tconst auto user = history->owner().user(action.vuser_id().v);\n\t\tconst auto prevRank = qs(action.vprev_rank());\n\t\tconst auto newRank = qs(action.vnew_rank());\n\t\tconst auto isSelf = (user == from);\n\t\tconst auto text = [&] {\n\t\t\tif (isSelf) {\n\t\t\t\tif (newRank.isEmpty()) {\n\t\t\t\t\treturn tr::lng_admin_log_removed_own_rank(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\tfromLinkText,\n\t\t\t\t\t\tlt_previous,\n\t\t\t\t\t\t{ prevRank },\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t} else if (prevRank.isEmpty()) {\n\t\t\t\t\treturn tr::lng_admin_log_set_own_rank(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\tfromLinkText,\n\t\t\t\t\t\tlt_tag,\n\t\t\t\t\t\t{ newRank },\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t}\n\t\t\t\treturn tr::lng_admin_log_changed_own_rank_from(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_previous,\n\t\t\t\t\t{ prevRank },\n\t\t\t\t\tlt_tag,\n\t\t\t\t\t{ newRank },\n\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t\tconst auto userLinkText = tr::link(user->name(), QString());\n\t\t\tif (newRank.isEmpty()) {\n\t\t\t\treturn tr::lng_admin_log_removed_rank(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tuserLinkText,\n\t\t\t\t\tlt_previous,\n\t\t\t\t\t{ prevRank },\n\t\t\t\t\ttr::marked);\n\t\t\t} else if (prevRank.isEmpty()) {\n\t\t\t\treturn tr::lng_admin_log_set_rank(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tuserLinkText,\n\t\t\t\t\tlt_tag,\n\t\t\t\t\t{ newRank },\n\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t\treturn tr::lng_admin_log_changed_rank_from(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText,\n\t\t\t\tlt_user,\n\t\t\t\tuserLinkText,\n\t\t\t\tlt_previous,\n\t\t\t\t{ prevRank },\n\t\t\t\tlt_tag,\n\t\t\t\t{ newRank },\n\t\t\t\ttr::marked);\n\t\t}();\n\t\tif (isSelf) {\n\t\t\taddSimpleServiceMessage(text);\n\t\t} else {\n\t\t\taddServiceMessageWithLink(text, user->createOpenLink());\n\t\t}\n\t};\n\n\taction.match(\n\t\tcreateChangeTitle,\n\t\tcreateChangeAbout,\n\t\tcreateChangeUsername,\n\t\tcreateChangePhoto,\n\t\tcreateToggleInvites,\n\t\tcreateToggleSignatures,\n\t\tcreateUpdatePinned,\n\t\tcreateEditMessage,\n\t\tcreateDeleteMessage,\n\t\tcreateParticipantJoin,\n\t\tcreateParticipantLeave,\n\t\tcreateParticipantInvite,\n\t\tcreateParticipantToggleBan,\n\t\tcreateParticipantToggleAdmin,\n\t\tcreateChangeStickerSet,\n\t\tcreateChangeEmojiSet,\n\t\tcreateTogglePreHistoryHidden,\n\t\tcreateDefaultBannedRights,\n\t\tcreateStopPoll,\n\t\tcreateChangeLinkedChat,\n\t\tcreateChangeLocation,\n\t\tcreateToggleSlowMode,\n\t\tcreateStartGroupCall,\n\t\tcreateDiscardGroupCall,\n\t\tcreateParticipantMute,\n\t\tcreateParticipantUnmute,\n\t\tcreateToggleGroupCallSetting,\n\t\tcreateParticipantJoinByInvite,\n\t\tcreateExportedInviteDelete,\n\t\tcreateExportedInviteRevoke,\n\t\tcreateExportedInviteEdit,\n\t\tcreateParticipantVolume,\n\t\tcreateChangeHistoryTTL,\n\t\tcreateParticipantJoinByRequest,\n\t\tcreateToggleNoForwards,\n\t\tcreateSendMessage,\n\t\tcreateChangeAvailableReactions,\n\t\tcreateChangeUsernames,\n\t\tcreateToggleForum,\n\t\tcreateCreateTopic,\n\t\tcreateEditTopic,\n\t\tcreateDeleteTopic,\n\t\tcreatePinTopic,\n\t\tcreateToggleAntiSpam,\n\t\tcreateChangePeerColor,\n\t\tcreateChangeProfilePeerColor,\n\t\tcreateChangeWallpaper,\n\t\tcreateChangeEmojiStatus,\n\t\tcreateToggleSignatureProfiles,\n\t\tcreateParticipantSubExtend,\n\t\tcreateToggleAutotranslation,\n\t\tcreateParticipantEditRank);\n}\n\n} // namespace AdminLog\n"
  },
  {
    "path": "Telegram/SourceFiles/history/admin_log/history_admin_log_item.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass History;\n\nnamespace HistoryView {\nclass ElementDelegate;\nclass Element;\n} // namespace HistoryView\n\nnamespace AdminLog {\n\nclass OwnedItem;\n\nvoid GenerateItems(\n\tnot_null<HistoryView::ElementDelegate*> delegate,\n\tnot_null<History*> history,\n\tconst MTPDchannelAdminLogEvent &event,\n\tFn<void(OwnedItem item, TimeId sentDate, MsgId)> callback);\n\n// Smart pointer wrapper for HistoryItem* that destroys the owned item.\nclass OwnedItem {\npublic:\n\tOwnedItem(std::nullptr_t = nullptr);\n\tOwnedItem(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate,\n\t\tnot_null<HistoryItem*> data);\n\tOwnedItem(const OwnedItem &other) = delete;\n\tOwnedItem &operator=(const OwnedItem &other) = delete;\n\tOwnedItem(OwnedItem &&other);\n\tOwnedItem &operator=(OwnedItem &&other);\n\t~OwnedItem();\n\n\t[[nodiscard]] HistoryView::Element *get() const {\n\t\treturn _view.get();\n\t}\n\t[[nodiscard]] HistoryView::Element *operator->() const {\n\t\treturn get();\n\t}\n\t[[nodiscard]] operator HistoryView::Element*() const {\n\t\treturn get();\n\t}\n\n\tvoid refreshView(not_null<HistoryView::ElementDelegate*> delegate);\n\tvoid clearView();\n\nprivate:\n\tHistoryItem *_data = nullptr;\n\tstd::unique_ptr<HistoryView::Element> _view;\n\n};\n\n} // namespace AdminLog\n"
  },
  {
    "path": "Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/admin_log/history_admin_log_section.h\"\n\n#include \"history/admin_log/history_admin_log_inner.h\"\n#include \"history/admin_log/history_admin_log_filter.h\"\n#include \"profile/profile_back_button.h\"\n#include \"core/shortcuts.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/controls/swipe_handler.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/ui_utility.h\"\n#include \"apiwrap.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/window_session_controller.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"base/timer.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_info.h\"\n\nnamespace AdminLog {\n\nclass FixedBar final : public Ui::RpWidget {\npublic:\n\tFixedBar(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<ChannelData*> channel);\n\n\t[[nodiscard]] rpl::producer<> searchCancelRequests() const;\n\t[[nodiscard]] rpl::producer<QString> searchRequests() const;\n\n\t// When animating mode is enabled the content is hidden and the\n\t// whole fixed bar acts like a back button.\n\tvoid setAnimatingMode(bool enabled);\n\n\tvoid applyFilter(const FilterValue &value);\n\tvoid goBack();\n\tvoid showSearch();\n\tbool setSearchFocus() {\n\t\tif (_searchShown) {\n\t\t\t_field->setFocus();\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\nprotected:\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tvoid toggleSearch();\n\tvoid cancelSearch();\n\tvoid searchUpdated();\n\tvoid applySearch();\n\tvoid searchAnimationCallback();\n\n\tnot_null<Window::SessionController*> _controller;\n\tnot_null<ChannelData*> _channel;\n\tobject_ptr<Ui::InputField> _field;\n\tobject_ptr<Profile::BackButton> _backButton;\n\tobject_ptr<Ui::IconButton> _search;\n\tobject_ptr<Ui::CrossButton> _cancel;\n\n\tUi::Animations::Simple _searchShownAnimation;\n\tbool _searchShown = false;\n\tbool _animatingMode = false;\n\tbase::Timer _searchTimer;\n\n\trpl::event_stream<> _searchCancelRequests;\n\trpl::event_stream<QString> _searchRequests;\n\n};\n\nobject_ptr<Window::SectionWidget> SectionMemento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tWindow::Column column,\n\t\tconst QRect &geometry) {\n\tif (column == Window::Column::Third) {\n\t\treturn nullptr;\n\t}\n\tauto result = object_ptr<Widget>(parent, controller, _channel);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nFixedBar::FixedBar(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<ChannelData*> channel)\n: RpWidget(parent)\n, _controller(controller)\n, _channel(channel)\n, _field(this, st::defaultMultiSelectSearchField, tr::lng_dlg_filter())\n, _backButton(this)\n, _search(this, st::topBarSearch)\n, _cancel(this, st::historyAdminLogCancelSearch) {\n\t_backButton->moveToLeft(0, 0);\n\t_backButton->setClickedCallback([=] { goBack(); });\n\t_search->setClickedCallback([=] { showSearch(); });\n\t_cancel->setClickedCallback([=] { cancelSearch(); });\n\t_field->hide();\n\t_field->cancelled() | rpl::on_next([=] {\n\t\tcancelSearch();\n\t}, _field->lifetime());\n\t_field->changes() | rpl::on_next([=] {\n\t\tsearchUpdated();\n\t}, _field->lifetime());\n\t_field->submits(\n\t) | rpl::on_next([=] { applySearch(); }, _field->lifetime());\n\t_searchTimer.setCallback([=] { applySearch(); });\n\n\t_cancel->hide(anim::type::instant);\n\n\t_backButton->setSubtext(tr::lng_admin_log_title_all(tr::now));\n\tInfo::Profile::NameValue(channel) | rpl::on_next([=](QString name) {\n\t\t_backButton->setText(name);\n\t}, _backButton->lifetime());\n\t_backButton->setWidget(Ui::CreateChild<Ui::UserpicButton>(\n\t\t_backButton.get(),\n\t\tchannel,\n\t\tst::topBarInfoButton));\n}\n\nvoid FixedBar::applyFilter(const FilterValue &value) {\n\tauto hasFilter = value.flags || value.admins;\n\t_backButton->setText(hasFilter\n\t\t? tr::lng_admin_log_title_selected(tr::now)\n\t\t: tr::lng_admin_log_title_all(tr::now));\n}\n\nvoid FixedBar::goBack() {\n\t_controller->showBackFromStack();\n}\n\nvoid FixedBar::showSearch() {\n\tif (!_searchShown) {\n\t\ttoggleSearch();\n\t}\n}\n\nvoid FixedBar::toggleSearch() {\n\t_searchShown = !_searchShown;\n\t_cancel->toggle(_searchShown, anim::type::normal);\n\t_searchShownAnimation.start(\n\t\t[=] { searchAnimationCallback(); },\n\t\t_searchShown ? 0. : 1.,\n\t\t_searchShown ? 1. : 0.,\n\t\tst::historyAdminLogSearchSlideDuration);\n\t_search->setDisabled(_searchShown);\n\tif (_searchShown) {\n\t\t_field->show();\n\t\t_field->setFocus();\n\t} else {\n\t\t_searchCancelRequests.fire({});\n\t}\n}\n\nvoid FixedBar::searchAnimationCallback() {\n\tif (!_searchShownAnimation.animating()) {\n\t\t_field->setVisible(_searchShown);\n\t\t_search->setIconOverride(\n\t\t\t_searchShown ? &st::topBarSearch.icon : nullptr,\n\t\t\t_searchShown ? &st::topBarSearch.icon : nullptr);\n\t\t_search->setRippleColorOverride(\n\t\t\t_searchShown ? &st::topBarBg : nullptr);\n\t\t_search->setCursor(\n\t\t\t_searchShown ? style::cur_default : style::cur_pointer);\n\t\t_backButton->setOpacity(1.);\n\t}\n\tresizeToWidth(width());\n}\n\nvoid FixedBar::cancelSearch() {\n\tif (_searchShown) {\n\t\tif (!_field->getLastText().isEmpty()) {\n\t\t\t_field->clear();\n\t\t\t_field->setFocus();\n\t\t\tapplySearch();\n\t\t} else {\n\t\t\ttoggleSearch();\n\t\t}\n\t}\n}\n\nvoid FixedBar::searchUpdated() {\n\tif (_field->getLastText().isEmpty()) {\n\t\tapplySearch();\n\t} else {\n\t\t_searchTimer.callOnce(AutoSearchTimeout);\n\t}\n}\n\nvoid FixedBar::applySearch() {\n\t_searchRequests.fire_copy(_field->getLastText());\n}\n\nint FixedBar::resizeGetHeight(int newWidth) {\n\tconst auto offset = st::historySendRight + st::lineWidth;\n\tconst auto searchShownLeft = st::topBarArrowPadding.left();\n\tconst auto searchHiddenLeft = newWidth - _search->width() - offset;\n\tconst auto searchShown = _searchShownAnimation.value(_searchShown\n\t\t? 1.\n\t\t: 0.);\n\tconst auto searchCurrentLeft = anim::interpolate(\n\t\tsearchHiddenLeft,\n\t\tsearchShownLeft,\n\t\tsearchShown);\n\t_search->moveToLeft(searchCurrentLeft, 0);\n\t_backButton->setOpacity(1. - searchShown);\n\t_backButton->resizeToWidth(searchCurrentLeft);\n\t_backButton->moveToLeft(0, 0);\n\n\tconst auto cancelLeft = newWidth - _cancel->width() - offset;\n\t_cancel->moveToLeft(cancelLeft, 0);\n\n\tconst auto newHeight = _backButton->height();\n\tconst auto fieldLeft = searchShownLeft + _search->width();\n\t_field->setGeometryToLeft(\n\t\tfieldLeft,\n\t\tst::historyAdminLogSearchTop,\n\t\tcancelLeft - fieldLeft,\n\t\t_field->height());\n\n\treturn newHeight;\n}\n\nrpl::producer<> FixedBar::searchCancelRequests() const {\n\treturn _searchCancelRequests.events();\n}\n\nrpl::producer<QString> FixedBar::searchRequests() const {\n\treturn _searchRequests.events();\n}\n\nvoid FixedBar::setAnimatingMode(bool enabled) {\n\tif (_animatingMode != enabled) {\n\t\t_animatingMode = enabled;\n\t\tsetCursor(_animatingMode ? style::cur_pointer : style::cur_default);\n\t\tif (_animatingMode) {\n\t\t\tsetAttribute(Qt::WA_OpaquePaintEvent, false);\n\t\t\thideChildren();\n\t\t} else {\n\t\t\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\t\t\tshowChildren();\n\t\t\t_field->hide();\n\t\t\t_cancel->setVisible(false);\n\t\t}\n\t\tshow();\n\t}\n}\n\nvoid FixedBar::paintEvent(QPaintEvent *e) {\n\tif (!_animatingMode) {\n\t\tauto p = QPainter(this);\n\t\tp.fillRect(e->rect(), st::topBarBg);\n\t}\n}\n\nvoid FixedBar::mousePressEvent(QMouseEvent *e) {\n\tif (e->button() == Qt::LeftButton) {\n\t\tgoBack();\n\t} else {\n\t\tRpWidget::mousePressEvent(e);\n\t}\n}\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<ChannelData*> channel)\n: Window::SectionWidget(parent, controller, rpl::single<PeerData*>(channel))\n, _scroll(this, st::historyScroll, false)\n, _fixedBar(this, controller, channel)\n, _fixedBarShadow(this)\n, _settingsFilter(\n\tthis,\n\ttr::lng_menu_settings(tr::now),\n\tst::historyComposeButton)\n, _whatIsThis(this, st::historyAdminLogWhatIsThis) {\n\t_fixedBar->move(0, 0);\n\t_fixedBar->resizeToWidth(width());\n\t_fixedBar->searchCancelRequests(\n\t) | rpl::on_next([=] {\n\t\tsetInnerFocus();\n\t}, lifetime());\n\t_fixedBar->searchRequests(\n\t) | rpl::on_next([=](const QString &query) {\n\t\t_inner->applySearch(query);\n\t}, lifetime());\n\t_fixedBar->show();\n\n\t_whatIsThis->raise();\n\t_fixedBarShadow->raise();\n\n\tcontroller->adaptive().value(\n\t) | rpl::on_next([=] {\n\t\tupdateAdaptiveLayout();\n\t}, lifetime());\n\n\t_inner = _scroll->setOwnedWidget(\n\t\tobject_ptr<InnerWidget>(this, controller, channel));\n\t_inner->showSearchSignal(\n\t) | rpl::on_next([=] {\n\t\t_fixedBar->showSearch();\n\t}, lifetime());\n\t_inner->cancelSignal(\n\t) | rpl::on_next([=] {\n\t\t_fixedBar->goBack();\n\t}, lifetime());\n\t_inner->scrollToSignal(\n\t) | rpl::on_next([=](int top) {\n\t\t_scroll->scrollToY(top);\n\t}, lifetime());\n\n\t_scroll->move(0, _fixedBar->height());\n\t_scroll->show();\n\t_scroll->scrolls() | rpl::on_next([=] {\n\t\tonScroll();\n\t}, lifetime());\n\n\t_settingsFilter->setClickedCallback([=] {\n\t\tshowFilter();\n\t});\n\t_whatIsThis->setClickedCallback([=] {\n\t\tcontroller->show(Ui::MakeInformBox(channel->isMegagroup()\n\t\t\t? tr::lng_admin_log_about_text()\n\t\t\t: tr::lng_admin_log_about_text_channel()));\n\t});\n\n\tsetupShortcuts();\n\tsetupSwipeReply();\n}\n\nvoid Widget::showFilter() {\n\t_inner->showFilter([this](FilterValue &&filter) {\n\t\tapplyFilter(std::move(filter));\n\t\tcontroller()->hideLayer();\n\t});\n}\n\nvoid Widget::updateAdaptiveLayout() {\n\t_fixedBarShadow->moveToLeft(\n\t\tcontroller()->adaptive().isOneColumn()\n\t\t\t? 0\n\t\t\t: st::lineWidth,\n\t\t_fixedBar->height());\n}\n\nnot_null<ChannelData*> Widget::channel() const {\n\treturn _inner->channel();\n}\n\nDialogs::RowDescriptor Widget::activeChat() const {\n\treturn {\n\t\tchannel()->owner().history(channel()),\n\t\tFullMsgId(channel()->id, ShowAtUnreadMsgId)\n\t};\n}\n\nQPixmap Widget::grabForShowAnimation(\n\t\tconst Window::SectionSlideParams &params) {\n\tif (params.withTopBarShadow) {\n\t\t_fixedBarShadow->hide();\n\t}\n\tauto result = Ui::GrabWidget(this);\n\tif (params.withTopBarShadow) {\n\t\t_fixedBarShadow->show();\n\t}\n\treturn result;\n}\n\nvoid Widget::doSetInnerFocus() {\n\tif (!_fixedBar->setSearchFocus()) {\n\t\t_inner->setFocus();\n\t}\n}\n\nbool Widget::showInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) {\n\tif (auto logMemento = dynamic_cast<SectionMemento*>(memento.get())) {\n\t\tif (logMemento->getChannel() == channel()) {\n\t\t\trestoreState(logMemento);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<SectionMemento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nvoid Widget::setupShortcuts() {\n\tShortcuts::Requests(\n\t) | rpl::filter([=] {\n\t\treturn Ui::AppInFocus()\n\t\t\t&& Ui::InFocusChain(this)\n\t\t\t&& !controller()->isLayerShown()\n\t\t\t&& isActiveWindow();\n\t}) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {\n\t\tusing Command = Shortcuts::Command;\n\t\trequest->check(Command::Search, 2) && request->handle([=] {\n\t\t\t_fixedBar->showSearch();\n\t\t\treturn true;\n\t\t});\n\t}, lifetime());\n}\n\nvoid Widget::setupSwipeReply() {\n\tauto update = [=](Ui::Controls::SwipeContextData data) {\n\t\tif (data.translation > 0) {\n\t\t\tif (!_swipeBackData.callback) {\n\t\t\t\t_swipeBackData = Ui::Controls::SetupSwipeBack(\n\t\t\t\t\tthis,\n\t\t\t\t\t[=]() -> std::pair<QColor, QColor> {\n\t\t\t\t\t\tauto context = _inner->preparePaintContext({});\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontext.st->msgServiceBg()->c,\n\t\t\t\t\t\t\tcontext.st->msgServiceFg()->c,\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t}\n\t\t\t_swipeBackData.callback(data);\n\t\t\treturn;\n\t\t} else if (_swipeBackData.lifetime) {\n\t\t\t_swipeBackData = {};\n\t\t}\n\t};\n\n\tauto init = [=](int, Qt::LayoutDirection direction) {\n\t\tif (direction == Qt::RightToLeft) {\n\t\t\treturn Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {\n\t\t\t\tcontroller()->showBackFromStack();\n\t\t\t});\n\t\t}\n\t\treturn Ui::Controls::SwipeHandlerFinishData();\n\t};\n\n\tUi::Controls::SetupSwipeHandler({\n\t\t.widget = _inner.data(),\n\t\t.scroll = _scroll.data(),\n\t\t.update = std::move(update),\n\t\t.init = std::move(init),\n\t});\n}\n\nstd::shared_ptr<Window::SectionMemento> Widget::createMemento() {\n\tauto result = std::make_shared<SectionMemento>(channel());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<SectionMemento*> memento) {\n\tmemento->setScrollTop(_scroll->scrollTop());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<SectionMemento*> memento) {\n\t_inner->restoreState(memento);\n\tauto scrollTop = memento->getScrollTop();\n\t_scroll->scrollToY(scrollTop);\n\t_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());\n}\n\nvoid Widget::resizeEvent(QResizeEvent *e) {\n\tif (!width() || !height()) {\n\t\treturn;\n\t}\n\n\tconst auto contentWidth = width();\n\n\tconst auto newScrollTop = _scroll->scrollTop() + topDelta();\n\t_fixedBar->resizeToWidth(contentWidth);\n\t_fixedBarShadow->resize(contentWidth, st::lineWidth);\n\n\tconst auto bottom = height();\n\tconst auto scrollHeight = bottom\n\t\t- _fixedBar->height()\n\t\t- _settingsFilter->height();\n\tconst auto scrollSize = QSize(contentWidth, scrollHeight);\n\tif (_scroll->size() != scrollSize) {\n\t\t_scroll->resize(scrollSize);\n\t\t_inner->resizeToWidth(scrollSize.width(), _scroll->height());\n\t\t_inner->restoreScrollPosition();\n\t}\n\n\tif (!_scroll->isHidden()) {\n\t\tif (topDelta()) {\n\t\t\t_scroll->scrollToY(newScrollTop);\n\t\t}\n\t\tauto scrollTop = _scroll->scrollTop();\n\t\t_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());\n\t}\n\tconst auto fullWidthButtonRect = myrtlrect(\n\t\t0,\n\t\tbottom - _settingsFilter->height(),\n\t\tcontentWidth,\n\t\t_settingsFilter->height());\n\t_settingsFilter->setGeometry(fullWidthButtonRect);\n\t_whatIsThis->moveToRight(\n\t\tst::historySendRight,\n\t\tbottom - _whatIsThis->height());\n}\n\nvoid Widget::paintEvent(QPaintEvent *e) {\n\tif (animatingShow()) {\n\t\tSectionWidget::paintEvent(e);\n\t\treturn;\n\t} else if (controller()->contentOverlapped(this, e)) {\n\t\treturn;\n\t}\n\t//if (hasPendingResizedItems()) {\n\t//\tupdateListSize();\n\t//}\n\n\t//auto ms = crl::now();\n\t//_historyDownShown.step(ms);\n\n\tconst auto clip = e->rect();\n\tSectionWidget::PaintBackground(controller(), _inner->theme(), this, clip);\n}\n\nvoid Widget::onScroll() {\n\tint scrollTop = _scroll->scrollTop();\n\t_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());\n}\n\nvoid Widget::showAnimatedHook(\n\t\tconst Window::SectionSlideParams &params) {\n\t_fixedBar->setAnimatingMode(true);\n\tif (params.withTopBarShadow) {\n\t\t_fixedBarShadow->show();\n\t}\n}\n\nvoid Widget::showFinishedHook() {\n\t_fixedBar->setAnimatingMode(false);\n}\n\nbool Widget::floatPlayerHandleWheelEvent(QEvent *e) {\n\treturn _scroll->viewportEvent(e);\n}\n\nQRect Widget::floatPlayerAvailableRect() {\n\treturn mapToGlobal(_scroll->geometry());\n}\n\nvoid Widget::applyFilter(FilterValue &&value) {\n\t_fixedBar->applyFilter(value);\n\t_inner->applyFilter(std::move(value));\n}\n\n} // namespace AdminLog\n\n"
  },
  {
    "path": "Telegram/SourceFiles/history/admin_log/history_admin_log_section.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"window/section_widget.h\"\n#include \"window/section_memento.h\"\n#include \"history/admin_log/history_admin_log_item.h\"\n#include \"history/admin_log/history_admin_log_filter_value.h\"\n#include \"ui/controls/swipe_handler_data.h\"\n#include \"mtproto/sender.h\"\n\nnamespace Ui {\nclass ScrollArea;\nclass PlainShadow;\nclass FlatButton;\nclass IconButton;\n} // namespace Ui\n\nnamespace Profile {\nclass BackButton;\n} // namespace Profile\n\nnamespace AdminLog {\n\nclass FixedBar;\nclass InnerWidget;\nclass SectionMemento;\n\nclass Widget final : public Window::SectionWidget {\npublic:\n\tWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<ChannelData*> channel);\n\n\tnot_null<ChannelData*> channel() const;\n\tDialogs::RowDescriptor activeChat() const override;\n\n\tbool hasTopBarShadow() const override {\n\t\treturn true;\n\t}\n\n\tQPixmap grabForShowAnimation(const Window::SectionSlideParams &params) override;\n\n\tbool showInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) override;\n\tstd::shared_ptr<Window::SectionMemento> createMemento() override;\n\n\tvoid setInternalState(const QRect &geometry, not_null<SectionMemento*> memento);\n\n\t// Float player interface.\n\tbool floatPlayerHandleWheelEvent(QEvent *e) override;\n\tQRect floatPlayerAvailableRect() override;\n\n\tvoid applyFilter(FilterValue &&value);\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid showAnimatedHook(\n\t\tconst Window::SectionSlideParams &params) override;\n\tvoid showFinishedHook() override;\n\tvoid doSetInnerFocus() override;\n\nprivate:\n\tvoid showFilter();\n\tvoid onScroll();\n\tvoid updateAdaptiveLayout();\n\tvoid saveState(not_null<SectionMemento*> memento);\n\tvoid restoreState(not_null<SectionMemento*> memento);\n\tvoid setupShortcuts();\n\tvoid setupSwipeReply();\n\n\tobject_ptr<Ui::ScrollArea> _scroll;\n\tQPointer<InnerWidget> _inner;\n\tobject_ptr<FixedBar> _fixedBar;\n\tobject_ptr<Ui::PlainShadow> _fixedBarShadow;\n\tobject_ptr<Ui::FlatButton> _settingsFilter;\n\tobject_ptr<Ui::IconButton> _whatIsThis;\n\n\tUi::Controls::SwipeBackResult _swipeBackData;\n\n};\n\nclass SectionMemento : public Window::SectionMemento {\npublic:\n\tusing Element = HistoryView::Element;\n\n\tSectionMemento(not_null<ChannelData*> channel) : _channel(channel) {\n\t}\n\n\tobject_ptr<Window::SectionWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tWindow::Column column,\n\t\tconst QRect &geometry) override;\n\n\tnot_null<ChannelData*> getChannel() const {\n\t\treturn _channel;\n\t}\n\tvoid setScrollTop(int scrollTop) {\n\t\t_scrollTop = scrollTop;\n\t}\n\tint getScrollTop() const {\n\t\treturn _scrollTop;\n\t}\n\n\tvoid setAdmins(std::vector<not_null<UserData*>> admins) {\n\t\t_admins = std::move(admins);\n\t}\n\tvoid setAdminsCanEdit(std::vector<not_null<UserData*>> admins) {\n\t\t_adminsCanEdit = std::move(admins);\n\t}\n\tstd::vector<not_null<UserData*>> takeAdmins() {\n\t\treturn std::move(_admins);\n\t}\n\tstd::vector<not_null<UserData*>> takeAdminsCanEdit() {\n\t\treturn std::move(_adminsCanEdit);\n\t}\n\n\tvoid setItems(\n\t\t\tstd::vector<OwnedItem> &&items,\n\t\t\tstd::set<uint64> &&eventIds,\n\t\t\tbool upLoaded,\n\t\t\tbool downLoaded) {\n\t\t_items = std::move(items);\n\t\t_eventIds = std::move(eventIds);\n\t\t_upLoaded = upLoaded;\n\t\t_downLoaded = downLoaded;\n\t}\n\tvoid setFilter(FilterValue &&filter) {\n\t\t_filter = std::move(filter);\n\t}\n\tvoid setSearchQuery(QString &&query) {\n\t\t_searchQuery = std::move(query);\n\t}\n\tstd::vector<OwnedItem> takeItems() {\n\t\treturn std::move(_items);\n\t}\n\tstd::set<uint64> takeEventIds() {\n\t\treturn std::move(_eventIds);\n\t}\n\tbool upLoaded() const {\n\t\treturn _upLoaded;\n\t}\n\tbool downLoaded() const {\n\t\treturn _downLoaded;\n\t}\n\tFilterValue takeFilter() {\n\t\treturn std::move(_filter);\n\t}\n\tQString takeSearchQuery() {\n\t\treturn std::move(_searchQuery);\n\t}\n\nprivate:\n\tnot_null<ChannelData*> _channel;\n\tint _scrollTop = 0;\n\tstd::vector<not_null<UserData*>> _admins;\n\tstd::vector<not_null<UserData*>> _adminsCanEdit;\n\tstd::vector<OwnedItem> _items;\n\tstd::set<uint64> _eventIds;\n\tbool _upLoaded = false;\n\tbool _downLoaded = true;\n\tFilterValue _filter;\n\tQString _searchQuery;\n\n};\n\n} // namespace AdminLog\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history.h\"\n\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_item_preview.h\"\n#include \"history/view/history_view_translate_tracker.h\"\n#include \"dialogs/dialogs_indexed_list.h\"\n#include \"history/history_inner_widget.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history_streamed_drafts.h\"\n#include \"history/history_translation.h\"\n#include \"history/history_unread_things.h\"\n#include \"core/ui_integration.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/components/scheduled_messages.h\"\n#include \"data/components/sponsored_messages.h\"\n#include \"data/components/top_peers.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"data/data_drafts.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_channel_admins.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_replies_list.h\"\n#include \"data/data_send_action.h\"\n#include \"data/data_star_gift.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_document.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_history_messages.h\"\n#include \"api/api_text_entities.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_todo_list.h\"\n#include \"lang/lang_keys.h\"\n#include \"apiwrap.h\"\n#include \"api/api_chat_participants.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"main/main_session.h\"\n#include \"window/notifications_manager.h\"\n#include \"calls/calls_instance.h\"\n#include \"spellcheck/spellcheck_types.h\"\n#include \"storage/localstorage.h\"\n#include \"storage/storage_facade.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"storage/storage_account.h\"\n#include \"support/support_helper.h\"\n#include \"ui/image/image.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/painter.h\" // remove when History::paintUserpic accepts QPainter\n#include \"payments/payments_checkout_process.h\"\n#include \"core/crash_reports.h\"\n#include \"core/application.h\"\n#include \"base/unixtime.h\"\n#include \"base/qt/qt_common_adapters.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace {\n\nconstexpr auto kNewBlockEachMessage = 50;\nconstexpr auto kSkipCloudDraftsFor = TimeId(2);\n\nusing UpdateFlag = Data::HistoryUpdate::Flag;\n\n[[nodiscard]] HistoryItemCommonFields WithLocalFlag(\n\t\tHistoryItemCommonFields fields) {\n\tfields.flags |= MessageFlag::Local;\n\treturn fields;\n}\n\n[[nodiscard]] Dialogs::UnreadState AdjustedForumUnreadState(\n\t\tDialogs::UnreadState state) {\n\tconst auto allMuted = (state.chats == state.chatsMuted);\n\tstate.chatsMuted = (state.chats && allMuted) ? 1 : 0;\n\tstate.chats = state.chats ? 1 : 0;\n\treturn state;\n}\n\n} // namespace\n\nHistory::History(not_null<Data::Session*> owner, PeerId peerId)\n: Thread(owner, Type::History)\n, peer(owner->peer(peerId))\n, _delegateMixin(HistoryInner::DelegateMixin())\n, _chatListNameSortKey(TextUtilities::NameSortKey(peer->name()))\n, _sendActionPainter(this) {\n\tThread::setMuted(owner->notifySettings().isMuted(peer));\n\n\tif (const auto user = peer->asUser()) {\n\t\tif (user->isBot()) {\n\t\t\t_outboxReadBefore = std::numeric_limits<MsgId>::max();\n\t\t}\n\t}\n}\n\nHistory::~History() = default;\n\nvoid History::clearLastKeyboard() {\n\tif (lastKeyboardId) {\n\t\tif (lastKeyboardId == lastKeyboardHiddenId) {\n\t\t\tlastKeyboardHiddenId = 0;\n\t\t}\n\t\tlastKeyboardId = 0;\n\t\tsession().changes().historyUpdated(this, UpdateFlag::BotKeyboard);\n\t}\n\tlastKeyboardInited = true;\n\tlastKeyboardFrom = 0;\n}\n\nint History::height() const {\n\treturn _height;\n}\n\nbool History::hasPendingResizedItems() const {\n\treturn _flags & Flag::HasPendingResizedItems;\n}\n\nvoid History::setHasPendingResizedItems() {\n\t_flags |= Flag::HasPendingResizedItems;\n}\n\nvoid History::itemRemoved(not_null<HistoryItem*> item) {\n\tif (item == _joinedMessage) {\n\t\t_joinedMessage = nullptr;\n\t} else if (item == _newPeerNameChange) {\n\t\t_newPeerNameChange = nullptr;\n\t} else if (item == _newPeerPhotoChange) {\n\t\t_newPeerPhotoChange = nullptr;\n\t}\n\titem->removeMainView();\n\tif (_lastServerMessage == item) {\n\t\t_lastServerMessage = std::nullopt;\n\t}\n\tif (lastMessage() == item) {\n\t\t_lastMessage = std::nullopt;\n\t\tif (loadedAtBottom()) {\n\t\t\tif (const auto last = lastAvailableMessage()) {\n\t\t\t\tsetLastMessage(last);\n\t\t\t}\n\t\t}\n\t}\n\tcheckChatListMessageRemoved(item);\n\titemVanished(item);\n\tif (IsClientMsgId(item->id)) {\n\t\tunregisterClientSideMessage(item);\n\t}\n\tif (const auto topic = item->topic()) {\n\t\ttopic->applyItemRemoved(item->id);\n\t}\n\tif (const auto sublist = item->savedSublist()) {\n\t\tsublist->applyItemRemoved(item->id);\n\t}\n\tif (const auto streamed = _streamedDrafts.get()) {\n\t\tstreamed->applyItemRemoved(item);\n\t}\n\tif (const auto chat = peer->asChat()) {\n\t\tif (const auto to = chat->getMigrateToChannel()) {\n\t\t\tif (const auto history = owner().historyLoaded(to)) {\n\t\t\t\thistory->checkChatListMessageRemoved(item);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid History::checkChatListMessageRemoved(not_null<HistoryItem*> item) {\n\tif (chatListMessage() != item) {\n\t\treturn;\n\t}\n\tsetChatListMessageUnknown();\n\trefreshChatListMessage();\n}\n\nvoid History::itemVanished(not_null<HistoryItem*> item) {\n\tif (const auto thread = item->maybeNotificationThread()) {\n\t\tthread->removeNotification(item);\n\t}\n\tif (lastKeyboardId == item->id) {\n\t\tclearLastKeyboard();\n\t}\n\tif ((!item->out() || item->isPost())\n\t\t&& item->unread(this)\n\t\t&& unreadCount() > 0) {\n\t\tsetUnreadCount(unreadCount() - 1);\n\t}\n\tif (const auto media = item->media()) {\n\t\tif (media->gift()) {\n\t\t\tusing GiftAction = Data::GiftUpdate::Action;\n\t\t\towner().notifyGiftUpdate({\n\t\t\t\t.id = Data::SavedStarGiftId::User(item->id),\n\t\t\t\t.action = GiftAction::Delete,\n\t\t\t});\n\t\t}\n\t}\n}\n\nvoid History::takeLocalDraft(not_null<History*> from) {\n\tconst auto topicRootId = MsgId(0);\n\tconst auto monoforumPeerId = PeerId(0);\n\tconst auto i = from->_drafts.find(\n\t\tData::DraftKey::Local(topicRootId, monoforumPeerId));\n\tif (i == end(from->_drafts)) {\n\t\treturn;\n\t}\n\tauto &draft = i->second;\n\tif (!draft->textWithTags.text.isEmpty()\n\t\t&& !_drafts.contains(\n\t\t\tData::DraftKey::Local(topicRootId, monoforumPeerId))) {\n\t\t// Edit and reply to drafts can't migrate.\n\t\t// Cloud drafts do not migrate automatically.\n\t\tdraft->reply = FullReplyTo();\n\t\tsetLocalDraft(std::move(draft));\n\t}\n\tfrom->clearLocalDraft(topicRootId, monoforumPeerId);\n\tsession().api().saveDraftToCloudDelayed(from);\n}\n\nvoid History::createLocalDraftFromCloud(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId) {\n\tconst auto draft = cloudDraft(topicRootId, monoforumPeerId);\n\tif (!draft) {\n\t\tclearLocalDraft(topicRootId, monoforumPeerId);\n\t\treturn;\n\t} else if (Data::DraftIsNull(draft) || !draft->date) {\n\t\treturn;\n\t}\n\n\tdraft->reply.topicRootId = topicRootId;\n\tdraft->reply.monoforumPeerId = monoforumPeerId;\n\tif (!suggestDraftAllowed()) {\n\t\tdraft->suggest = SuggestOptions();\n\t}\n\tauto existing = localDraft(topicRootId, monoforumPeerId);\n\tif (Data::DraftIsNull(existing)\n\t\t|| !existing->date\n\t\t|| draft->date >= existing->date) {\n\t\tif (!existing) {\n\t\t\tsetLocalDraft(std::make_unique<Data::Draft>(\n\t\t\t\tdraft->textWithTags,\n\t\t\t\tdraft->reply,\n\t\t\t\tdraft->suggest,\n\t\t\t\tdraft->cursor,\n\t\t\t\tdraft->webpage));\n\t\t\texisting = localDraft(topicRootId, monoforumPeerId);\n\t\t} else if (existing != draft) {\n\t\t\texisting->textWithTags = draft->textWithTags;\n\t\t\texisting->reply = draft->reply;\n\t\t\texisting->suggest = draft->suggest;\n\t\t\texisting->cursor = draft->cursor;\n\t\t\texisting->webpage = draft->webpage;\n\t\t}\n\t\texisting->date = draft->date;\n\t}\n}\n\nData::Draft *History::draft(Data::DraftKey key) const {\n\tif (!key) {\n\t\treturn nullptr;\n\t}\n\tconst auto i = _drafts.find(key);\n\treturn (i != _drafts.end()) ? i->second.get() : nullptr;\n}\n\nvoid History::setDraft(\n\t\tData::DraftKey key,\n\t\tstd::unique_ptr<Data::Draft> &&draft) {\n\tif (!key) {\n\t\treturn;\n\t}\n\tconst auto cloudThread = key.isCloud()\n\t\t? threadFor(key.topicRootId(), key.monoforumPeerId())\n\t\t: nullptr;\n\tif (cloudThread) {\n\t\tcloudThread->cloudDraftTextCache().clear();\n\t}\n\tif (draft) {\n\t\t_drafts[key] = std::move(draft);\n\t} else if (_drafts.remove(key) && cloudThread) {\n\t\tcloudThread->updateChatListSortPosition();\n\t}\n}\n\nconst Data::HistoryDrafts &History::draftsMap() const {\n\treturn _drafts;\n}\n\nvoid History::setDraftsMap(Data::HistoryDrafts &&map) {\n\tfor (auto &[key, draft] : _drafts) {\n\t\tmap[key] = std::move(draft);\n\t}\n\t_drafts = std::move(map);\n}\n\nvoid History::clearDraft(Data::DraftKey key) {\n\tsetDraft(key, nullptr);\n}\n\nvoid History::clearDrafts() {\n\tfor (auto &[key, draft] : base::take(_drafts)) {\n\t\tconst auto cloudThread = key.isCloud()\n\t\t\t? threadFor(key.topicRootId(), key.monoforumPeerId())\n\t\t\t: nullptr;\n\t\tif (cloudThread) {\n\t\t\tcloudThread->cloudDraftTextCache().clear();\n\t\t\tcloudThread->updateChatListSortPosition();\n\t\t}\n\t}\n}\n\nData::Draft *History::createCloudDraft(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tconst Data::Draft *fromDraft) {\n\tif (Data::DraftIsNull(fromDraft)) {\n\t\tsetCloudDraft(std::make_unique<Data::Draft>(\n\t\t\tTextWithTags(),\n\t\t\tFullReplyTo{\n\t\t\t\t.topicRootId = topicRootId,\n\t\t\t\t.monoforumPeerId = monoforumPeerId,\n\t\t\t},\n\t\t\tSuggestOptions(),\n\t\t\tMessageCursor(),\n\t\t\tData::WebPageDraft()));\n\t\tcloudDraft(topicRootId, monoforumPeerId)->date = TimeId(0);\n\t} else {\n\t\tauto existing = cloudDraft(topicRootId, monoforumPeerId);\n\t\tif (!existing) {\n\t\t\tauto reply = fromDraft->reply;\n\t\t\treply.topicRootId = topicRootId;\n\t\t\treply.monoforumPeerId = monoforumPeerId;\n\t\t\tsetCloudDraft(std::make_unique<Data::Draft>(\n\t\t\t\tfromDraft->textWithTags,\n\t\t\t\treply,\n\t\t\t\tfromDraft->suggest,\n\t\t\t\tfromDraft->cursor,\n\t\t\t\tfromDraft->webpage));\n\t\t\texisting = cloudDraft(topicRootId, monoforumPeerId);\n\t\t} else if (existing != fromDraft) {\n\t\t\texisting->textWithTags = fromDraft->textWithTags;\n\t\t\texisting->reply = fromDraft->reply;\n\t\t\texisting->suggest = fromDraft->suggest;\n\t\t\texisting->cursor = fromDraft->cursor;\n\t\t\texisting->webpage = fromDraft->webpage;\n\t\t}\n\t\texisting->date = base::unixtime::now();\n\t\texisting->reply.topicRootId = topicRootId;\n\t\texisting->reply.monoforumPeerId = monoforumPeerId;\n\t\tif (!suggestDraftAllowed()) {\n\t\t\texisting->suggest = SuggestOptions();\n\t\t}\n\t}\n\n\tif (const auto thread = threadFor(topicRootId, monoforumPeerId)) {\n\t\tthread->cloudDraftTextCache().clear();\n\t\tthread->updateChatListSortPosition();\n\t}\n\n\treturn cloudDraft(topicRootId, monoforumPeerId);\n}\n\nbool History::skipCloudDraftUpdate(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tTimeId date) const {\n\tconst auto key = Data::DraftKey::Local(topicRootId, monoforumPeerId);\n\tconst auto i = _acceptCloudDraftsAfter.find(key);\n\treturn _savingCloudDraftRequests.contains(key)\n\t\t|| (i != _acceptCloudDraftsAfter.end() && date < i->second);\n}\n\nvoid History::startSavingCloudDraft(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId) {\n\tconst auto key = Data::DraftKey::Local(topicRootId, monoforumPeerId);\n\t++_savingCloudDraftRequests[key];\n}\n\nvoid History::finishSavingCloudDraft(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tTimeId savedAt) {\n\tconst auto key = Data::DraftKey::Local(topicRootId, monoforumPeerId);\n\tconst auto i = _savingCloudDraftRequests.find(key);\n\tif (i != _savingCloudDraftRequests.end()) {\n\t\tif (--i->second <= 0) {\n\t\t\t_savingCloudDraftRequests.erase(i);\n\t\t}\n\t}\n\tauto &after = _acceptCloudDraftsAfter[key];\n\tafter = std::max(after, savedAt + kSkipCloudDraftsFor);\n}\n\nvoid History::applyCloudDraft(MsgId topicRootId, PeerId monoforumPeerId) {\n\tif (!topicRootId && session().supportMode()) {\n\t\tupdateChatListEntry();\n\t\tsession().supportHelper().cloudDraftChanged(this);\n\t} else {\n\t\tcreateLocalDraftFromCloud(topicRootId, monoforumPeerId);\n\t\tif (const auto thread = threadFor(topicRootId, monoforumPeerId)) {\n\t\t\tthread->updateChatListSortPosition();\n\t\t\tif (topicRootId) {\n\t\t\t\tsession().changes().topicUpdated(\n\t\t\t\t\tthread->asTopic(),\n\t\t\t\t\tData::TopicUpdate::Flag::CloudDraft);\n\t\t\t} else if (monoforumPeerId) {\n\t\t\t\tsession().changes().sublistUpdated(\n\t\t\t\t\tthread->asSublist(),\n\t\t\t\t\tData::SublistUpdate::Flag::CloudDraft);\n\t\t\t} else {\n\t\t\t\tsession().changes().historyUpdated(\n\t\t\t\t\tthis,\n\t\t\t\t\tUpdateFlag::CloudDraft);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid History::draftSavedToCloud(MsgId topicRootId, PeerId monoforumPeerId) {\n\tif (const auto thread = threadFor(topicRootId, monoforumPeerId)) {\n\t\tthread->updateChatListEntry();\n\t}\n\tsession().local().writeDrafts(this);\n}\n\nconst Data::ForwardDraft &History::forwardDraft(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId) const {\n\tconst auto key = Data::DraftKey::Local(topicRootId, monoforumPeerId);\n\tstatic const auto kEmpty = Data::ForwardDraft();\n\tconst auto i = _forwardDrafts.find(key);\n\treturn (i != end(_forwardDrafts)) ? i->second : kEmpty;\n}\n\nData::ResolvedForwardDraft History::resolveForwardDraft(\n\t\tconst Data::ForwardDraft &draft) const {\n\treturn Data::ResolvedForwardDraft{\n\t\t.items = owner().idsToItems(draft.ids),\n\t\t.options = draft.options,\n\t};\n}\n\nData::ResolvedForwardDraft History::resolveForwardDraft(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId) {\n\tconst auto &draft = forwardDraft(topicRootId, monoforumPeerId);\n\tauto result = resolveForwardDraft(draft);\n\tif (result.items.size() != draft.ids.size()) {\n\t\tsetForwardDraft(topicRootId, monoforumPeerId, {\n\t\t\t.ids = owner().itemsToIds(result.items),\n\t\t\t.options = result.options,\n\t\t});\n\t}\n\treturn result;\n}\n\nvoid History::setForwardDraft(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tData::ForwardDraft &&draft) {\n\tauto changed = false;\n\tconst auto key = Data::DraftKey::Local(topicRootId, monoforumPeerId);\n\tif (draft.ids.empty()) {\n\t\tchanged = _forwardDrafts.remove(key);\n\t} else {\n\t\tauto &now = _forwardDrafts[key];\n\t\tif (now != draft) {\n\t\t\tnow = std::move(draft);\n\t\t\tchanged = true;\n\t\t}\n\t}\n\tif (changed) {\n\t\tif (const auto thread = threadFor(topicRootId, monoforumPeerId)) {\n\t\t\tsession().changes().entryUpdated(\n\t\t\t\tthread,\n\t\t\t\tData::EntryUpdate::Flag::ForwardDraft);\n\t\t}\n\t}\n}\n\nnot_null<HistoryItem*> History::createItem(\n\t\tMsgId id,\n\t\tconst MTPMessage &message,\n\t\tMessageFlags localFlags,\n\t\tbool detachExistingItem,\n\t\tbool newMessage) {\n\towner().fillMessagePeers(peer->id, message);\n\tif (const auto result = owner().message(peer, id)) {\n\t\tif (detachExistingItem) {\n\t\t\tresult->removeMainView();\n\t\t}\n\t\tif (result->needsUpdateForVideoQualities(message)) {\n\t\t\towner().updateEditedMessage(message);\n\t\t}\n\t\treturn result;\n\t}\n\tconst auto result = message.match([&](const auto &data) {\n\t\treturn makeMessage(id, data, localFlags);\n\t});\n\tif (newMessage && result->out() && result->isRegular()) {\n\t\tsession().topPeers().increment(peer, result->date());\n\t\tif (result->starsPaid()) {\n\t\t\tsession().credits().load(true);\n\t\t}\n\t}\n\treturn result;\n}\n\nstd::vector<not_null<HistoryItem*>> History::createItems(\n\t\tconst QVector<MTPMessage> &data) {\n\tauto result = std::vector<not_null<HistoryItem*>>();\n\tresult.reserve(data.size());\n\tconst auto localFlags = MessageFlags();\n\tconst auto detachExistingItem = true;\n\tfor (auto i = data.cend(), e = data.cbegin(); i != e;) {\n\t\tconst auto &data = *--i;\n\t\tconst auto id = IdFromMessage(data);\n\t\tif ((id.bare == 1) && (data.type() == mtpc_messageEmpty)) {\n\t\t\t// The first message of channels should be a service message\n\t\t\t// about its creation. But if channel auto-cleaning is enabled,\n\t\t\t// the first message comes empty and is displayed incorrectly.\n\t\t\tcontinue;\n\t\t}\n\t\tresult.emplace_back(createItem(\n\t\t\tid,\n\t\t\tdata,\n\t\t\tlocalFlags,\n\t\t\tdetachExistingItem));\n\t}\n\treturn result;\n}\n\nnot_null<HistoryItem*> History::addNewMessage(\n\t\tMsgId id,\n\t\tconst MTPMessage &message,\n\t\tMessageFlags localFlags,\n\t\tNewMessageType type) {\n\tconst auto newMessage = (type == NewMessageType::Unread);\n\tconst auto detachExisting = newMessage;\n\tconst auto item = createItem(\n\t\tid,\n\t\tmessage,\n\t\tlocalFlags,\n\t\tdetachExisting,\n\t\tnewMessage);\n\tif (type == NewMessageType::Existing || item->mainView()) {\n\t\treturn item;\n\t}\n\tif (newMessage && item->isHistoryEntry()) {\n\t\tapplyMessageChanges(item, message);\n\t}\n\treturn addNewItem(item, newMessage);\n}\n\nnot_null<HistoryItem*> History::insertItem(\n\t\tstd::unique_ptr<HistoryItem> item) {\n\tExpects(item != nullptr);\n\n\tconst auto &[i, ok] = _items.insert(std::move(item));\n\n\tconst auto result = i->get();\n\towner().registerMessage(result);\n\n\tEnsures(ok);\n\treturn result;\n}\n\nvoid History::destroyMessage(not_null<HistoryItem*> item) {\n\tExpects(item->isHistoryEntry() || !item->mainView());\n\n\tconst auto peerId = peer->id;\n\tif (item->isHistoryEntry()) {\n\t\t// All this must be done for all items manually in History::clear()!\n\t\titem->destroyHistoryEntry();\n\t\tif (item->isRegular()) {\n\t\t\tif (const auto messages = _messages.get()) {\n\t\t\t\tmessages->removeOne(item->id);\n\t\t\t}\n\t\t\tif (const auto types = item->sharedMediaTypes()) {\n\t\t\t\tsession().storage().remove(Storage::SharedMediaRemoveOne(\n\t\t\t\t\tpeerId,\n\t\t\t\t\ttypes,\n\t\t\t\t\titem->id));\n\t\t\t}\n\t\t}\n\t\titemRemoved(item);\n\t}\n\tif (item->isSending()) {\n\t\tsession().api().cancelLocalItem(item);\n\t}\n\n\tconst auto documentToCancel = [&] {\n\t\tconst auto media = item->isAdminLogEntry()\n\t\t\t? nullptr\n\t\t\t: item->media();\n\t\treturn media ? media->document() : nullptr;\n\t}();\n\n\towner().unregisterMessage(item);\n\tCore::App().notifications().clearFromItem(item);\n\n\tauto hack = std::unique_ptr<HistoryItem>(item.get());\n\tconst auto i = _items.find(hack);\n\thack.release();\n\n\tAssert(i != end(_items));\n\t_items.erase(i);\n\n\tif (documentToCancel) {\n\t\tsession().data().documentMessageRemoved(documentToCancel);\n\t}\n}\n\nvoid History::destroyMessagesByDates(TimeId minDate, TimeId maxDate) {\n\tauto toDestroy = std::vector<not_null<HistoryItem*>>();\n\ttoDestroy.reserve(_items.size());\n\tfor (const auto &message : _items) {\n\t\tif (message->isRegular()\n\t\t\t&& message->date() > minDate\n\t\t\t&& message->date() < maxDate) {\n\t\t\ttoDestroy.push_back(message.get());\n\t\t}\n\t}\n\tfor (const auto &item : toDestroy) {\n\t\titem->destroy();\n\t}\n}\n\nvoid History::destroyMessagesByTopic(MsgId topicRootId) {\n\tauto toDestroy = std::vector<not_null<HistoryItem*>>();\n\ttoDestroy.reserve(_items.size());\n\tfor (const auto &message : _items) {\n\t\tif (message->topicRootId() == topicRootId) {\n\t\t\ttoDestroy.push_back(message.get());\n\t\t}\n\t}\n\tfor (const auto &item : toDestroy) {\n\t\titem->destroy();\n\t}\n}\n\nvoid History::destroyMessagesBySublist(not_null<PeerData*> sublistPeer) {\n\tauto toDestroy = std::vector<not_null<HistoryItem*>>();\n\ttoDestroy.reserve(_items.size());\n\tconst auto peerId = sublistPeer->id;\n\tfor (const auto &message : _items) {\n\t\tif (message->sublistPeerId() == peerId) {\n\t\t\ttoDestroy.push_back(message.get());\n\t\t}\n\t}\n\tfor (const auto &item : toDestroy) {\n\t\titem->destroy();\n\t}\n}\n\nvoid History::unpinMessagesFor(MsgId topicRootId, PeerId monoforumPeerId) {\n\tif (topicRootId) {\n\t\tsession().storage().remove(\n\t\t\tStorage::SharedMediaRemoveAll(\n\t\t\t\tpeer->id,\n\t\t\t\ttopicRootId,\n\t\t\t\tStorage::SharedMediaType::Pinned));\n\t\tif (const auto topic = peer->forumTopicFor(topicRootId)) {\n\t\t\ttopic->setHasPinnedMessages(false);\n\t\t}\n\t\tfor (const auto &item : _items) {\n\t\t\tif (item->isPinned() && item->topicRootId() == topicRootId) {\n\t\t\t\titem->setIsPinned(false);\n\t\t\t}\n\t\t}\n\t} else if (monoforumPeerId) {\n\t\tsession().storage().remove(\n\t\t\tStorage::SharedMediaRemoveAll(\n\t\t\t\tpeer->id,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tStorage::SharedMediaType::Pinned));\n\t\tif (const auto sublist = peer->monoforumSublistFor(\n\t\t\t\tmonoforumPeerId)) {\n\t\t\tsublist->setHasPinnedMessages(false);\n\t\t}\n\t\tfor (const auto &item : _items) {\n\t\t\tif (item->isPinned()\n\t\t\t\t&& item->sublistPeerId() == monoforumPeerId) {\n\t\t\t\titem->setIsPinned(false);\n\t\t\t}\n\t\t}\n\t} else {\n\t\tsession().storage().remove(\n\t\t\tStorage::SharedMediaRemoveAll(\n\t\t\t\tpeer->id,\n\t\t\t\tStorage::SharedMediaType::Pinned));\n\t\tsetHasPinnedMessages(false);\n\t\tif (const auto forum = peer->forum()) {\n\t\t\tforum->enumerateTopics([](not_null<Data::ForumTopic*> topic) {\n\t\t\t\ttopic->setHasPinnedMessages(false);\n\t\t\t});\n\t\t}\n\t\tfor (const auto &item : _items) {\n\t\t\tif (item->isPinned()) {\n\t\t\t\titem->setIsPinned(false);\n\t\t\t}\n\t\t}\n\t}\n}\n\nnot_null<HistoryItem*> History::addNewItem(\n\t\tnot_null<HistoryItem*> item,\n\t\tbool unread) {\n\tif (item->isScheduled()) {\n\t\tsession().scheduledMessages().appendSending(item);\n\t\treturn item;\n\t} else if (item->isBusinessShortcut()) {\n\t\towner().shortcutMessages().appendSending(item);\n\t\treturn item;\n\t} else if (!item->isHistoryEntry()) {\n\t\treturn item;\n\t}\n\n\t// In case we've loaded a new 'last' message\n\t// and it is not in blocks and we think that\n\t// we have all the messages till the bottom\n\t// we should unload known history or mark\n\t// currently loaded slice as not reaching bottom.\n\tconst auto shouldMarkBottomNotLoaded = loadedAtBottom()\n\t\t&& !unread\n\t\t&& !isEmpty();\n\tif (shouldMarkBottomNotLoaded) {\n\t\tsetNotLoadedAtBottom();\n\t}\n\n\tif (!loadedAtBottom() || peer->migrateTo()) {\n\t\tsetLastMessage(item);\n\t\tif (unread) {\n\t\t\tnewItemAdded(item);\n\t\t}\n\t} else {\n\t\taddNewToBack(item, unread);\n\t\tcheckForLoadedAtTop(item);\n\t}\n\n\tif (const auto sublist = item->savedSublist()) {\n\t\tsublist->applyMaybeLast(item);\n\t}\n\n\treturn item;\n}\n\nvoid History::checkForLoadedAtTop(not_null<HistoryItem*> added) {\n\tif (peer->isChat()) {\n\t\tif (added->isGroupEssential() && !added->isGroupMigrate()) {\n\t\t\t// We added the first message about group creation.\n\t\t\t_loadedAtTop = true;\n\t\t\taddEdgesToSharedMedia();\n\t\t}\n\t} else if (peer->isChannel()) {\n\t\tif (added->id == 1) {\n\t\t\t_loadedAtTop = true;\n\t\t\tcheckLocalMessages();\n\t\t\taddEdgesToSharedMedia();\n\t\t}\n\t}\n}\n\nnot_null<HistoryItem*> History::addNewLocalMessage(\n\t\tHistoryItemCommonFields &&fields,\n\t\tconst TextWithEntities &text,\n\t\tconst MTPMessageMedia &media) {\n\treturn addNewItem(\n\t\tmakeMessage(WithLocalFlag(std::move(fields)), text, media),\n\t\ttrue);\n}\n\nnot_null<HistoryItem*> History::addNewLocalMessage(\n\t\tHistoryItemCommonFields &&fields,\n\t\tnot_null<HistoryItem*> forwardOriginal) {\n\treturn addNewItem(\n\t\tmakeMessage(WithLocalFlag(std::move(fields)), forwardOriginal),\n\t\ttrue);\n}\n\nnot_null<HistoryItem*> History::addNewLocalMessage(\n\t\tHistoryItemCommonFields &&fields,\n\t\tnot_null<DocumentData*> document,\n\t\tconst TextWithEntities &caption) {\n\treturn addNewItem(\n\t\tmakeMessage(WithLocalFlag(std::move(fields)), document, caption),\n\t\ttrue);\n}\n\nnot_null<HistoryItem*> History::addNewLocalMessage(\n\t\tHistoryItemCommonFields &&fields,\n\t\tnot_null<PhotoData*> photo,\n\t\tconst TextWithEntities &caption) {\n\treturn addNewItem(\n\t\tmakeMessage(WithLocalFlag(std::move(fields)), photo, caption),\n\t\ttrue);\n}\n\nnot_null<HistoryItem*> History::addNewLocalMessage(\n\t\tHistoryItemCommonFields &&fields,\n\t\tnot_null<GameData*> game) {\n\treturn addNewItem(\n\t\tmakeMessage(WithLocalFlag(std::move(fields)), game),\n\t\ttrue);\n}\n\nnot_null<HistoryItem*> History::addNewLocalMessage(\n\t\tnot_null<HistoryItem*> item) {\n\tExpects(item->history() == this);\n\tExpects(item->isLocal());\n\n\treturn addNewItem(item, true);\n}\n\nnot_null<HistoryItem*> History::addSponsoredMessage(\n\t\tMsgId id,\n\t\tData::SponsoredFrom from,\n\t\tconst TextWithEntities &textWithEntities) {\n\treturn addNewItem(\n\t\tmakeMessage(id, from, textWithEntities, nullptr),\n\t\ttrue);\n}\n\nvoid History::clearUnreadMentionsFor(MsgId topicRootId) {\n\tconst auto forum = peer->forum();\n\tif (!topicRootId) {\n\t\tif (forum) {\n\t\t\tforum->clearAllUnreadMentions();\n\t\t}\n\t\tunreadMentions().clear();\n\t\treturn;\n\t} else if (forum) {\n\t\tif (const auto topic = forum->topicFor(topicRootId)) {\n\t\t\ttopic->unreadMentions().clear();\n\t\t}\n\t}\n\tconst auto &ids = unreadMentionsIds();\n\tif (ids.empty()) {\n\t\treturn;\n\t}\n\tconst auto owner = &this->owner();\n\tconst auto peerId = peer->id;\n\tauto items = base::flat_set<MsgId>();\n\titems.reserve(ids.size());\n\tfor (const auto &id : ids) {\n\t\tif (const auto item = owner->message(peerId, id)) {\n\t\t\tif (item->topicRootId() == topicRootId) {\n\t\t\t\titems.emplace(id);\n\t\t\t}\n\t\t}\n\t}\n\tfor (const auto &id : items) {\n\t\tunreadMentions().erase(id);\n\t}\n}\n\nvoid History::clearUnreadReactionsFor(\n\t\tMsgId topicRootId,\n\t\tData::SavedSublist *sublist) {\n\tconst auto forum = peer->forum();\n\tconst auto monoforum = peer->monoforum();\n\tconst auto sublistPeerId = sublist ? sublist->sublistPeer()->id : 0;\n\tif ((!topicRootId && !sublist)\n\t\t|| (!topicRootId && forum)\n\t\t|| (!sublist && monoforum)) {\n\t\tif (forum) {\n\t\t\tforum->clearAllUnreadReactions();\n\t\t} else if (monoforum) {\n\t\t\tmonoforum->clearAllUnreadReactions();\n\t\t}\n\t\tunreadReactions().clear();\n\t\treturn;\n\t} else if (forum) {\n\t\tif (const auto topic = forum->topicFor(topicRootId)) {\n\t\t\ttopic->unreadReactions().clear();\n\t\t}\n\t} else if (monoforum) {\n\t\tsublist->unreadReactions().clear();\n\t}\n\tconst auto &ids = unreadReactionsIds();\n\tif (ids.empty()) {\n\t\treturn;\n\t}\n\tconst auto owner = &this->owner();\n\tconst auto peerId = peer->id;\n\tauto items = base::flat_set<MsgId>();\n\titems.reserve(ids.size());\n\tfor (const auto &id : ids) {\n\t\tif (const auto item = owner->message(peerId, id)) {\n\t\t\tif ((topicRootId && item->topicRootId() == topicRootId)\n\t\t\t\t|| (sublist && item->sublistPeerId() == sublistPeerId)) {\n\t\t\t\titems.emplace(id);\n\t\t\t}\n\t\t}\n\t}\n\tfor (const auto &id : items) {\n\t\tunreadReactions().erase(id);\n\t}\n}\n\nint History::unreadPollVotesCount() const {\n\treturn _unreadPollVotesCount;\n}\n\nvoid History::setUnreadPollVotesCount(int count) {\n\tif (_unreadPollVotesCount == count) {\n\t\treturn;\n\t}\n\tconst auto notifier = unreadStateChangeNotifier(\n\t\tuseMyUnreadInParent());\n\t_unreadPollVotesCount = count;\n\t_unreadPollVotesCountChanges.fire_copy(count);\n}\n\nrpl::producer<int> History::unreadPollVotesCountChanges() const {\n\treturn _unreadPollVotesCountChanges.events();\n}\n\nvoid History::clearUnreadPollVotesFor(MsgId topicRootId) {\n\tconst auto forum = peer->forum();\n\tif (!topicRootId) {\n\t\tif (forum) {\n\t\t\tforum->clearAllUnreadPollVotes();\n\t\t}\n\t\tunreadPollVotes().clear();\n\t\treturn;\n\t} else if (forum) {\n\t\tif (const auto topic = forum->topicFor(topicRootId)) {\n\t\t\ttopic->unreadPollVotes().clear();\n\t\t}\n\t}\n\tconst auto &ids = unreadPollVotesIds();\n\tif (ids.empty()) {\n\t\treturn;\n\t}\n\tconst auto owner = &this->owner();\n\tconst auto peerId = peer->id;\n\tauto items = base::flat_set<MsgId>();\n\titems.reserve(ids.size());\n\tfor (const auto &id : ids) {\n\t\tif (const auto item = owner->message(peerId, id)) {\n\t\t\tif (item->topicRootId() == topicRootId) {\n\t\t\t\titems.emplace(id);\n\t\t\t}\n\t\t}\n\t}\n\tfor (const auto &id : items) {\n\t\tunreadPollVotes().erase(id);\n\t}\n}\n\nnot_null<HistoryItem*> History::addNewToBack(\n\t\tnot_null<HistoryItem*> item,\n\t\tbool unread) {\n\tExpects(!isBuildingFrontBlock());\n\n\taddItemToBlock(item);\n\n\tif (!unread && item->isRegular()) {\n\t\tconst auto from = loadedAtTop() ? 0 : minMsgId();\n\t\tconst auto till = loadedAtBottom() ? ServerMaxMsgId : maxMsgId();\n\t\tif (_messages) {\n\t\t\t_messages->addExisting(item->id, { from, till });\n\t\t}\n\t\tif (const auto types = item->sharedMediaTypes()) {\n\t\t\tauto &storage = session().storage();\n\t\t\tstorage.add(Storage::SharedMediaAddExisting(\n\t\t\t\tpeer->id,\n\t\t\t\tMsgId(0), // topicRootId\n\t\t\t\tPeerId(0), // monoforumPeerId\n\t\t\t\ttypes,\n\t\t\t\titem->id,\n\t\t\t\t{ from, till }));\n\t\t\tconst auto pinned = types.test(Storage::SharedMediaType::Pinned);\n\t\t\tif (pinned) {\n\t\t\t\tsetHasPinnedMessages(true);\n\t\t\t}\n\t\t\tif (const auto topic = item->topic()) {\n\t\t\t\tstorage.add(Storage::SharedMediaAddExisting(\n\t\t\t\t\tpeer->id,\n\t\t\t\t\ttopic->rootId(),\n\t\t\t\t\tPeerId(), // monoforumPeerId\n\t\t\t\t\ttypes,\n\t\t\t\t\titem->id,\n\t\t\t\t\t{ item->id, item->id}));\n\t\t\t\tif (pinned) {\n\t\t\t\t\ttopic->setHasPinnedMessages(true);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (const auto sublist = item->savedSublist()) {\n\t\t\t\tstorage.add(Storage::SharedMediaAddExisting(\n\t\t\t\t\tpeer->id,\n\t\t\t\t\tMsgId(), // topicRootId\n\t\t\t\t\titem->sublistPeerId(),\n\t\t\t\t\ttypes,\n\t\t\t\t\titem->id,\n\t\t\t\t\t{ item->id, item->id }));\n\t\t\t\tif (pinned) {\n\t\t\t\t\tsublist->setHasPinnedMessages(true);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (item->from()->id) {\n\t\tif (auto user = item->from()->asUser()) {\n\t\t\tauto getLastAuthors = [this]() -> std::deque<not_null<UserData*>>* {\n\t\t\t\tif (auto chat = peer->asChat()) {\n\t\t\t\t\treturn &chat->lastAuthors;\n\t\t\t\t} else if (auto channel = peer->asMegagroup()) {\n\t\t\t\t\treturn channel->canViewMembers()\n\t\t\t\t\t\t? &channel->mgInfo->lastParticipants\n\t\t\t\t\t\t: nullptr;\n\t\t\t\t}\n\t\t\t\treturn nullptr;\n\t\t\t};\n\t\t\tif (auto megagroup = peer->asMegagroup()) {\n\t\t\t\tif (user->isBot()) {\n\t\t\t\t\tauto mgInfo = megagroup->mgInfo.get();\n\t\t\t\t\tAssert(mgInfo != nullptr);\n\t\t\t\t\tmgInfo->bots.insert(user);\n\t\t\t\t\tif (mgInfo->botStatus == Data::BotStatus::NoBots) {\n\t\t\t\t\t\tmgInfo->botStatus = Data::BotStatus::HasBots;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (auto lastAuthors = getLastAuthors()) {\n\t\t\t\tauto prev = ranges::find(\n\t\t\t\t\t*lastAuthors,\n\t\t\t\t\tuser,\n\t\t\t\t\t[](not_null<UserData*> user) { return user.get(); });\n\t\t\t\tauto index = (prev != lastAuthors->end())\n\t\t\t\t\t? (lastAuthors->end() - prev)\n\t\t\t\t\t: -1;\n\t\t\t\tif (index > 0) {\n\t\t\t\t\tlastAuthors->erase(prev);\n\t\t\t\t} else if (index < 0 && peer->isMegagroup()) { // nothing is outdated if just reordering\n\t\t\t\t\t// admins information outdated\n\t\t\t\t}\n\t\t\t\tif (index) {\n\t\t\t\t\tlastAuthors->push_front(user);\n\t\t\t\t}\n\t\t\t\tif (auto megagroup = peer->asMegagroup()) {\n\t\t\t\t\tsession().changes().peerUpdated(\n\t\t\t\t\t\tpeer,\n\t\t\t\t\t\tData::PeerUpdate::Flag::Members);\n\t\t\t\t\towner().addNewMegagroupParticipant(megagroup, user);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (item->definesReplyKeyboard()) {\n\t\t\tauto markupFlags = item->replyKeyboardFlags();\n\t\t\tif (!(markupFlags & ReplyMarkupFlag::Selective)\n\t\t\t\t|| item->mentionsMe()) {\n\t\t\t\tauto getMarkupSenders = [this]() -> base::flat_set<not_null<PeerData*>>* {\n\t\t\t\t\tif (auto chat = peer->asChat()) {\n\t\t\t\t\t\treturn &chat->markupSenders;\n\t\t\t\t\t} else if (auto channel = peer->asMegagroup()) {\n\t\t\t\t\t\treturn &channel->mgInfo->markupSenders;\n\t\t\t\t\t}\n\t\t\t\t\treturn nullptr;\n\t\t\t\t};\n\t\t\t\tif (auto markupSenders = getMarkupSenders()) {\n\t\t\t\t\tmarkupSenders->insert(item->from());\n\t\t\t\t}\n\t\t\t\tif (markupFlags & ReplyMarkupFlag::None) {\n\t\t\t\t\t// None markup means replyKeyboardHide.\n\t\t\t\t\tif (lastKeyboardFrom == item->from()->id\n\t\t\t\t\t\t|| (!lastKeyboardInited\n\t\t\t\t\t\t\t&& !peer->isChat()\n\t\t\t\t\t\t\t&& !peer->isMegagroup()\n\t\t\t\t\t\t\t&& !item->out())) {\n\t\t\t\t\t\tclearLastKeyboard();\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tbool botNotInChat = false;\n\t\t\t\t\tif (peer->isChat()) {\n\t\t\t\t\t\tbotNotInChat = item->from()->isUser()\n\t\t\t\t\t\t\t&& (!peer->asChat()->participants.empty()\n\t\t\t\t\t\t\t\t|| !Data::CanSendAnything(peer))\n\t\t\t\t\t\t\t&& !peer->asChat()->participants.contains(\n\t\t\t\t\t\t\t\titem->from()->asUser());\n\t\t\t\t\t} else if (peer->isMegagroup()) {\n\t\t\t\t\t\tbotNotInChat = item->from()->isUser()\n\t\t\t\t\t\t\t&& (peer->asChannel()->mgInfo->botStatus != Data::BotStatus::Unknown\n\t\t\t\t\t\t\t\t|| !Data::CanSendAnything(peer))\n\t\t\t\t\t\t\t&& !peer->asChannel()->mgInfo->bots.contains(\n\t\t\t\t\t\t\t\titem->from()->asUser());\n\t\t\t\t\t}\n\t\t\t\t\tif (botNotInChat) {\n\t\t\t\t\t\tclearLastKeyboard();\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlastKeyboardInited = true;\n\t\t\t\t\t\tlastKeyboardId = item->id;\n\t\t\t\t\t\tlastKeyboardFrom = item->from()->id;\n\t\t\t\t\t\tlastKeyboardUsed = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tsetLastMessage(item);\n\tif (unread) {\n\t\tnewItemAdded(item);\n\t}\n\n\towner().notifyHistoryChangeDelayed(this);\n\treturn item;\n}\n\nvoid History::applyMessageChanges(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPMessage &data) {\n\tif (data.type() == mtpc_messageService) {\n\t\tapplyServiceChanges(item, data.c_messageService());\n\t}\n\towner().stickers().checkSavedGif(item);\n\tsession().changes().messageUpdated(\n\t\titem,\n\t\tData::MessageUpdate::Flag::NewAdded);\n}\n\nvoid History::applyServiceChanges(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPDmessageService &data) {\n\tconst auto replyTo = data.vreply_to();\n\tconst auto processJoinedUser = [&](\n\t\t\tnot_null<ChannelData*> megagroup,\n\t\t\tnot_null<MegagroupInfo*> mgInfo,\n\t\t\tnot_null<UserData*> user) {\n\t\tif (!base::contains(mgInfo->lastParticipants, user)\n\t\t\t&& megagroup->canViewMembers()) {\n\t\t\tmgInfo->lastParticipants.push_front(user);\n\t\t\tsession().changes().peerUpdated(\n\t\t\t\tpeer,\n\t\t\t\tData::PeerUpdate::Flag::Members);\n\t\t\towner().addNewMegagroupParticipant(megagroup, user);\n\t\t}\n\t\tif (user->isBot()) {\n\t\t\tmgInfo->bots.insert(user);\n\t\t\tif (mgInfo->botStatus == Data::BotStatus::NoBots) {\n\t\t\t\tmgInfo->botStatus = Data::BotStatus::HasBots;\n\t\t\t}\n\t\t}\n\t};\n\tconst auto processJoinedPeer = [&](not_null<PeerData*> joined) {\n\t\tif (const auto megagroup = peer->asMegagroup()) {\n\t\t\tconst auto mgInfo = megagroup->mgInfo.get();\n\t\t\tAssert(mgInfo != nullptr);\n\t\t\tif (const auto user = joined->asUser()) {\n\t\t\t\tprocessJoinedUser(megagroup, mgInfo, user);\n\t\t\t}\n\t\t}\n\t};\n\tdata.vaction().match([&](const MTPDmessageActionChatAddUser &data) {\n\t\tif (const auto megagroup = peer->asMegagroup()) {\n\t\t\tconst auto mgInfo = megagroup->mgInfo.get();\n\t\t\tAssert(mgInfo != nullptr);\n\t\t\tfor (const auto &userId : data.vusers().v) {\n\t\t\t\tif (const auto user = owner().userLoaded(userId.v)) {\n\t\t\t\t\tprocessJoinedUser(megagroup, mgInfo, user);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDmessageActionChatJoinedByLink &data) {\n\t\tprocessJoinedPeer(item->from());\n\t}, [&](const MTPDmessageActionChatDeletePhoto &data) {\n\t\tif (const auto chat = peer->asChat()) {\n\t\t\tchat->setPhoto(MTP_chatPhotoEmpty());\n\t\t}\n\t}, [&](const MTPDmessageActionChatDeleteUser &data) {\n\t\tconst auto uid = data.vuser_id().v;\n\t\tif (lastKeyboardFrom == peerFromUser(uid)) {\n\t\t\tclearLastKeyboard();\n\t\t}\n\t\tif (const auto megagroup = peer->asMegagroup()) {\n\t\t\tif (const auto user = owner().userLoaded(uid)) {\n\t\t\t\tconst auto mgInfo = megagroup->mgInfo.get();\n\t\t\t\tAssert(mgInfo != nullptr);\n\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\tmgInfo->lastParticipants,\n\t\t\t\t\tuser,\n\t\t\t\t\t[](not_null<UserData*> user) { return user.get(); });\n\t\t\t\tif (i != mgInfo->lastParticipants.end()) {\n\t\t\t\t\tmgInfo->lastParticipants.erase(i);\n\t\t\t\t\tsession().changes().peerUpdated(\n\t\t\t\t\t\tpeer,\n\t\t\t\t\t\tData::PeerUpdate::Flag::Members);\n\t\t\t\t}\n\t\t\t\towner().removeMegagroupParticipant(megagroup, user);\n\t\t\t\tif (megagroup->membersCount() > 1) {\n\t\t\t\t\tmegagroup->setMembersCount(\n\t\t\t\t\t\tmegagroup->membersCount() - 1);\n\t\t\t\t} else {\n\t\t\t\t\tmgInfo->lastParticipantsStatus\n\t\t\t\t\t\t|= MegagroupInfo::LastParticipantsCountOutdated;\n\t\t\t\t\tmgInfo->lastParticipantsCount = 0;\n\t\t\t\t}\n\t\t\t\tif (mgInfo->lastAdmins.contains(user)) {\n\t\t\t\t\tmgInfo->lastAdmins.remove(user);\n\t\t\t\t\tif (megagroup->adminsCount() > 1) {\n\t\t\t\t\t\tmegagroup->setAdminsCount(\n\t\t\t\t\t\t\tmegagroup->adminsCount() - 1);\n\t\t\t\t\t}\n\t\t\t\t\tsession().changes().peerUpdated(\n\t\t\t\t\t\tpeer,\n\t\t\t\t\t\tData::PeerUpdate::Flag::Admins);\n\t\t\t\t}\n\t\t\t\tmgInfo->bots.remove(user);\n\t\t\t\tif (mgInfo->bots.empty() && mgInfo->botStatus == Data::BotStatus::HasBots) {\n\t\t\t\t\tmgInfo->botStatus = Data::BotStatus::NoBots;\n\t\t\t\t}\n\t\t\t}\n\t\t\tData::ChannelAdminChanges(megagroup).remove(uid);\n\t\t}\n\t}, [&](const MTPDmessageActionChatEditPhoto &data) {\n\t\tdata.vphoto().match([&](const MTPDphoto &data) {\n\t\t\tusing Flag = MTPDchatPhoto::Flag;\n\t\t\tconst auto photo = owner().processPhoto(data);\n\t\t\tphoto->peer = peer;\n\t\t\tconst auto chatPhoto = MTP_chatPhoto(\n\t\t\t\tMTP_flags((photo->hasVideo() ? Flag::f_has_video : Flag(0))\n\t\t\t\t\t| (photo->inlineThumbnailBytes().isEmpty()\n\t\t\t\t\t\t? Flag(0)\n\t\t\t\t\t\t: Flag::f_stripped_thumb)),\n\t\t\t\tMTP_long(photo->id),\n\t\t\t\tMTP_bytes(photo->inlineThumbnailBytes()),\n\t\t\t\tdata.vdc_id());\n\t\t\tif (const auto chat = peer->asChat()) {\n\t\t\t\tchat->setPhoto(chatPhoto);\n\t\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\t\tchannel->setPhoto(chatPhoto);\n\t\t\t}\n\t\t\tpeer->loadUserpic();\n\t\t}, [&](const MTPDphotoEmpty &data) {\n\t\t\tif (const auto chat = peer->asChat()) {\n\t\t\t\tchat->setPhoto(MTP_chatPhotoEmpty());\n\t\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\t\tchannel->setPhoto(MTP_chatPhotoEmpty());\n\t\t\t}\n\t\t});\n\t}, [&](const MTPDmessageActionChatEditTitle &data) {\n\t\tif (const auto chat = peer->asChat()) {\n\t\t\tchat->setName(qs(data.vtitle()));\n\t\t}\n\t}, [&](const MTPDmessageActionChatMigrateTo &data) {\n\t\tif (const auto chat = peer->asChat()) {\n\t\t\tchat->addFlags(ChatDataFlag::Deactivated);\n\t\t\tif (const auto channel = owner().channelLoaded(\n\t\t\t\t\tdata.vchannel_id().v)) {\n\t\t\t\tData::ApplyMigration(chat, channel);\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDmessageActionChannelMigrateFrom &data) {\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\tchannel->addFlags(ChannelDataFlag::Megagroup);\n\t\t\tif (const auto chat = owner().chatLoaded(data.vchat_id().v)) {\n\t\t\t\tData::ApplyMigration(chat, channel);\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDmessageActionPinMessage &data) {\n\t\tif (replyTo) {\n\t\t\treplyTo->match([&](const MTPDmessageReplyHeader &data) {\n\t\t\t\tconst auto id = data.vreply_to_msg_id().value_or_empty();\n\t\t\t\tif (id && item) {\n\t\t\t\t\tsession().storage().add(Storage::SharedMediaAddSlice(\n\t\t\t\t\t\tpeer->id,\n\t\t\t\t\t\tMsgId(0), // topicRootId\n\t\t\t\t\t\tPeerId(0), // monoforumPeerId\n\t\t\t\t\t\tStorage::SharedMediaType::Pinned,\n\t\t\t\t\t\t{ id },\n\t\t\t\t\t\t{ id, ServerMaxMsgId }));\n\t\t\t\t\tsetHasPinnedMessages(true);\n\t\t\t\t\tif (const auto topic = item->topic()) {\n\t\t\t\t\t\tsession().storage().add(Storage::SharedMediaAddSlice(\n\t\t\t\t\t\t\tpeer->id,\n\t\t\t\t\t\t\ttopic->rootId(),\n\t\t\t\t\t\t\tPeerId(), // monoforumPeerId\n\t\t\t\t\t\t\tStorage::SharedMediaType::Pinned,\n\t\t\t\t\t\t\t{ id },\n\t\t\t\t\t\t\t{ id, ServerMaxMsgId }));\n\t\t\t\t\t\ttopic->setHasPinnedMessages(true);\n\t\t\t\t\t}\n\t\t\t\t\tif (const auto sublist = item->savedSublist()) {\n\t\t\t\t\t\tsession().storage().add(Storage::SharedMediaAddSlice(\n\t\t\t\t\t\t\tpeer->id,\n\t\t\t\t\t\t\tMsgId(), // topicRootId\n\t\t\t\t\t\t\titem->sublistPeerId(),\n\t\t\t\t\t\t\tStorage::SharedMediaType::Pinned,\n\t\t\t\t\t\t\t{ id },\n\t\t\t\t\t\t\t{ id, ServerMaxMsgId }));\n\t\t\t\t\t\tsublist->setHasPinnedMessages(true);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, [&](const MTPDmessageReplyStoryHeader &data) {\n\t\t\t\tLOG((\"API Error: story reply in messageActionPinMessage.\"));\n\t\t\t});\n\t\t}\n\t}, [&](const MTPDmessageActionGroupCall &data) {\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\tchannel->setGroupCall(data.vcall());\n\t\t} else if (const auto chat = peer->asChat()) {\n\t\t\tchat->setGroupCall(data.vcall());\n\t\t}\n\t}, [&](const MTPDmessageActionGroupCallScheduled &data) {\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\tchannel->setGroupCall(data.vcall(), data.vschedule_date().v);\n\t\t} else if (const auto chat = peer->asChat()) {\n\t\t\tchat->setGroupCall(data.vcall(), data.vschedule_date().v);\n\t\t}\n\t}, [&](const MTPDmessageActionPaymentSent &data) {\n\t\tif (const auto payment = item->Get<HistoryServicePayment>()) {\n\t\t\tauto paid = std::optional<Payments::PaidInvoice>();\n\t\t\tif (const auto message = payment->msg) {\n\t\t\t\tif (const auto media = message->media()) {\n\t\t\t\t\tif (media->invoice()) {\n\t\t\t\t\t\tpaid = Payments::CheckoutProcess::InvoicePaid(\n\t\t\t\t\t\t\tmessage);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (!payment->slug.isEmpty()) {\n\t\t\t\tusing Payments::CheckoutProcess;\n\t\t\t\tpaid = Payments::CheckoutProcess::InvoicePaid(\n\t\t\t\t\t&session(),\n\t\t\t\t\tpayment->slug);\n\t\t\t}\n\t\t\tif (paid) {\n\t\t\t\t// Toast on a current active window.\n\t\t\t\tUi::Toast::Show({\n\t\t\t\t\t.text = tr::lng_payments_success(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_amount,\n\t\t\t\t\t\tUi::Text::Wrapped(payment->amount, EntityType::Bold),\n\t\t\t\t\t\tlt_title,\n\t\t\t\t\t\ttr::bold(paid->title),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\t.textContext = Core::TextContext({\n\t\t\t\t\t\t.session = &session(),\n\t\t\t\t\t}),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDmessageActionSetChatTheme &data) {\n\t\tdata.vtheme().match([&](const MTPDchatTheme &data) {\n\t\t\tpeer->setThemeToken(qs(data.vemoticon()));\n\t\t}, [&](const MTPDchatThemeUniqueGift &data) {\n\t\t\tpeer->setThemeToken(\n\t\t\t\towner().cloudThemes().processGiftThemeGetToken(data));\n\t\t});\n\t}, [&](const MTPDmessageActionSetChatWallPaper &data) {\n\t\tif (item->out() || data.is_for_both()) {\n\t\t\tpeer->setWallPaper(\n\t\t\t\tData::WallPaper::Create(&session(), data.vwallpaper()),\n\t\t\t\t!item->out() && data.is_for_both());\n\t\t}\n\t}, [&](const MTPDmessageActionChatJoinedByRequest &data) {\n\t\tprocessJoinedPeer(item->from());\n\t}, [&](const MTPDmessageActionTopicCreate &data) {\n\t\tif (const auto forum = peer->forum()) {\n\t\t\tforum->applyTopicAdded(\n\t\t\t\titem->id,\n\t\t\t\tqs(data.vtitle()),\n\t\t\t\tdata.vicon_color().v,\n\t\t\t\tdata.vicon_emoji_id().value_or(DocumentId()),\n\t\t\t\titem->from()->id,\n\t\t\t\titem->date(),\n\t\t\t\titem->out());\n\t\t}\n\t}, [&](const MTPDmessageActionTopicEdit &data) {\n\t\tif (const auto topic = item->topic()) {\n\t\t\tif (const auto &title = data.vtitle()) {\n\t\t\t\ttopic->applyTitle(qs(*title));\n\t\t\t}\n\t\t\tif (const auto icon = data.vicon_emoji_id()) {\n\t\t\t\ttopic->applyIconId(icon->v);\n\t\t\t}\n\t\t\tif (const auto closed = data.vclosed()) {\n\t\t\t\ttopic->setClosed(mtpIsTrue(*closed));\n\t\t\t}\n\t\t\tif (const auto hidden = data.vhidden()) {\n\t\t\t\ttopic->setHidden(mtpIsTrue(*hidden));\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDmessageActionConferenceCall &data) {\n\t\tif (!data.is_active() && !data.is_missed() && !item->out()) {\n\t\t\tif (const auto user = item->history()->peer->asUser()) {\n\t\t\t\tCore::App().calls().showConferenceInvite(user, item->id);\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDmessageActionTodoCompletions &data) {\n\t\tif (const auto done = item->Get<HistoryServiceTodoCompletions>()) {\n\t\t\tconst auto list = done->msg\n\t\t\t\t? done->msg\n\t\t\t\t: owner().message(peer, done->msgId);\n\t\t\tif (const auto media = list ? list->media() : nullptr) {\n\t\t\t\tif (const auto todolist = media->todolist()) {\n\t\t\t\t\ttodolist->apply(item, data);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDmessageActionTodoAppendTasks &data) {\n\t\tif (const auto done = item->Get<HistoryServiceTodoCompletions>()) {\n\t\t\tconst auto list = done->msg\n\t\t\t\t? done->msg\n\t\t\t\t: owner().message(peer, done->msgId);\n\t\t\tif (const auto media = list ? list->media() : nullptr) {\n\t\t\t\tif (const auto todolist = media->todolist()) {\n\t\t\t\t\ttodolist->apply(data);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDmessageActionPollAppendAnswer &data) {\n\t\tif (const auto append = item->Get<HistoryServicePollAppendAnswer>()) {\n\t\t\tconst auto list = append->msg\n\t\t\t\t? append->msg\n\t\t\t\t: owner().message(peer, append->msgId);\n\t\t\tif (const auto media = list ? list->media() : nullptr) {\n\t\t\t\tif (const auto poll = media->poll()) {\n\t\t\t\t\tdata.vanswer().match([&](const MTPDpollAnswer &answer) {\n\t\t\t\t\t\tauto parsed = PollAnswer();\n\t\t\t\t\t\tparsed.option = answer.voption().v;\n\t\t\t\t\t\tparsed.text = Api::ParseTextWithEntities(\n\t\t\t\t\t\t\t&session(),\n\t\t\t\t\t\t\tanswer.vtext());\n\t\t\t\t\t\tif (!poll->answerByOption(parsed.option)) {\n\t\t\t\t\t\t\tpoll->answers.push_back(std::move(parsed));\n\t\t\t\t\t\t\t++poll->version;\n\t\t\t\t\t\t\towner().notifyPollUpdateDelayed(poll);\n\t\t\t\t\t\t}\n\t\t\t\t\t}, [](const auto &) {});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDmessageActionPollDeleteAnswer &data) {\n\t\tif (const auto del = item->Get<HistoryServicePollDeleteAnswer>()) {\n\t\t\tconst auto list = del->msg\n\t\t\t\t? del->msg\n\t\t\t\t: owner().message(peer, del->msgId);\n\t\t\tif (const auto media = list ? list->media() : nullptr) {\n\t\t\t\tif (const auto poll = media->poll()) {\n\t\t\t\t\tconst auto option = del->answer.option;\n\t\t\t\t\tauto &answers = poll->answers;\n\t\t\t\t\tconst auto size = answers.size();\n\t\t\t\t\tanswers.erase(\n\t\t\t\t\t\tranges::remove(answers, option, &PollAnswer::option),\n\t\t\t\t\t\tend(answers));\n\t\t\t\t\tif (answers.size() != size) {\n\t\t\t\t\t\t++poll->version;\n\t\t\t\t\t\towner().notifyPollUpdateDelayed(poll);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDmessageActionStarGift &data) {\n\t\tif (data.is_auction_acquired() && data.vto_id()) {\n\t\t\tconst auto to = peer->owner().peer(peerFromMTP(*data.vto_id()));\n\t\t\tdata.vgift().match([&](const MTPDstarGift &data) {\n\t\t\t\tpeer->owner().notifyGiftAuctionGot({ data.vid().v, to });\n\t\t\t}, [](const auto &) {});\n\t\t}\n\t}, [&](const MTPDmessageActionNoForwardsToggle &data) {\n\t\tif (const auto user = peer->asUser()) {\n\t\t\tconst auto enabled = mtpIsTrue(data.vnew_value());\n\t\t\tuser->setNoForwardsFlags(\n\t\t\t\tenabled && item->out(),\n\t\t\t\tenabled && !item->out());\n\t\t}\n\t}, [](const auto &) {\n\t});\n}\n\nvoid History::viewHeightAdjusted(not_null<Element*> view, int delta) {\n\tif (view->data()->mainView() == view) {\n\t\tmainViewHeightAdjusted(view, delta);\n\t}\n\towner().notifyViewHeightAdjusted(view, delta);\n}\n\nvoid History::mainViewHeightAdjusted(not_null<Element*> view, int delta) {\n\tconst auto viewInBlock = view->indexInBlock();\n\tif (viewInBlock < 0 || _width <= 0) {\n\t\treturn;\n\t}\n\tconst auto block = view->block();\n\tfor (auto i = viewInBlock + 1, count = int(block->messages.size())\n\t\t; i != count\n\t\t; ++i) {\n\t\tconst auto view = block->messages[i].get();\n\t\tview->setY(view->y() + delta);\n\t}\n\tblock->resizeGetHeight(\n\t\t_width,\n\t\tHistoryBlock::ResizeRequest::ResizePending);\n\tconst auto blockInHistory = block->indexInHistory();\n\tfor (auto i = blockInHistory + 1, count = int(blocks.size())\n\t\t; i != count\n\t\t; ++i) {\n\t\tconst auto block = blocks[i].get();\n\t\tblock->setY(block->y() + delta);\n\t}\n\t_height += delta;\n}\n\nvoid History::mainViewRemoved(\n\t\tnot_null<HistoryBlock*> block,\n\t\tnot_null<HistoryView::Element*> view) {\n\tExpects(_joinedMessage != view->data());\n\tExpects(_newPeerNameChange != view->data());\n\tExpects(_newPeerPhotoChange != view->data());\n\n\tif (_firstUnreadView == view) {\n\t\tgetNextFirstUnreadMessage();\n\t}\n\tif (_unreadBarView == view) {\n\t\t_unreadBarView = nullptr;\n\t}\n\tif (scrollTopItem == view) {\n\t\tgetNextScrollTopItem(block, view->indexInBlock());\n\t}\n}\n\nvoid History::newItemAdded(not_null<HistoryItem*> item) {\n\titem->indexAsNewItem();\n\titem->addToMessagesIndex();\n\tif (const auto from = item->from() ? item->from()->asUser() : nullptr) {\n\t\tif (from == item->author()) {\n\t\t\t_sendActionPainter.clear(from);\n\t\t\towner().sendActionManager().repliesPaintersClear(this, from);\n\t\t}\n\t\tfrom->madeAction(item->date());\n\t}\n\titem->contributeToSlowmode();\n\tauto notification = Data::ItemNotification{\n\t\t.item = item,\n\t\t.type = Data::ItemNotificationType::Message,\n\t};\n\tif (item->showNotification()) {\n\t\titem->notificationThread()->pushNotification(notification);\n\t}\n\towner().notifyNewItemAdded(item);\n\tconst auto stillShow = item->showNotification(); // Could be read already.\n\tif (stillShow) {\n\t\tCore::App().notifications().schedule(notification);\n\t}\n\tif (item->out()) {\n\t\tif (item->isFromScheduled() && unreadCountRefreshNeeded(item->id)) {\n\t\t\tif (unreadCountKnown()) {\n\t\t\t\tsetUnreadCount(unreadCount() + 1);\n\t\t\t} else if (!isForum()) {\n\t\t\t\towner().histories().requestDialogEntry(this);\n\t\t\t}\n\t\t} else {\n\t\t\tdestroyUnreadBar();\n\t\t}\n\t\tif (!item->unread(this)) {\n\t\t\toutboxRead(item);\n\t\t}\n\t\tif (item->changesWallPaper()) {\n\t\t\tpeer->updateFullForced();\n\t\t}\n\t} else {\n\t\tif (item->unread(this)) {\n\t\t\tif (unreadCountKnown()) {\n\t\t\t\tsetUnreadCount(unreadCount() + 1);\n\t\t\t} else if (!isForum()) {\n\t\t\t\towner().histories().requestDialogEntry(this);\n\t\t\t}\n\t\t} else {\n\t\t\tinboxRead(item);\n\t\t}\n\t}\n\titem->incrementReplyToTopCounter();\n\tif (!folderKnown()) {\n\t\towner().histories().requestDialogEntry(this);\n\t}\n\tif (const auto topic = item->topic()) {\n\t\ttopic->applyItemAdded(item);\n\t}\n\tif (const auto sublist = item->savedSublist()) {\n\t\tsublist->applyItemAdded(item);\n\t}\n\tif (const auto media = item->media()) {\n\t\tif (const auto gift = media->gift()) {\n\t\t\tif (const auto unique = gift->unique.get()) {\n\t\t\t\tif (unique->ownerId == session().userPeerId()\n\t\t\t\t\t|| unique->hostId == session().userPeerId()) {\n\t\t\t\t\towner().emojiStatuses().refreshCollectibles();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid History::registerClientSideMessage(not_null<HistoryItem*> item) {\n\tExpects(item->isHistoryEntry());\n\tExpects(IsClientMsgId(item->id));\n\n\t_clientSideMessages.emplace(item);\n\tsession().changes().historyUpdated(this, UpdateFlag::ClientSideMessages);\n}\n\nvoid History::unregisterClientSideMessage(not_null<HistoryItem*> item) {\n\tconst auto removed = _clientSideMessages.remove(item);\n\tAssert(removed);\n\n\tsession().changes().historyUpdated(this, UpdateFlag::ClientSideMessages);\n}\n\nconst base::flat_set<not_null<HistoryItem*>> &History::clientSideMessages() {\n\treturn _clientSideMessages;\n}\n\nHistoryItem *History::latestSendingMessage() const {\n\tauto sending = ranges::views::all(\n\t\t_clientSideMessages\n\t) | ranges::views::filter([](not_null<HistoryItem*> item) {\n\t\treturn item->isSending();\n\t});\n\tconst auto i = ranges::max_element(sending, ranges::less(), [](\n\t\t\tnot_null<HistoryItem*> item) {\n\t\treturn std::pair(item->date(), item->id.bare);\n\t});\n\treturn (i == sending.end()) ? nullptr : i->get();\n}\n\nHistoryBlock *History::prepareBlockForAddingItem() {\n\tif (isBuildingFrontBlock()) {\n\t\tif (_buildingFrontBlock->block) {\n\t\t\treturn _buildingFrontBlock->block;\n\t\t}\n\n\t\tblocks.push_front(std::make_unique<HistoryBlock>(this));\n\t\tfor (auto i = 0, l = int(blocks.size()); i != l; ++i) {\n\t\t\tblocks[i]->setIndexInHistory(i);\n\t\t}\n\t\t_buildingFrontBlock->block = blocks.front().get();\n\t\tif (_buildingFrontBlock->expectedItemsCount > 0) {\n\t\t\t_buildingFrontBlock->block->messages.reserve(\n\t\t\t\t_buildingFrontBlock->expectedItemsCount + 1);\n\t\t}\n\t\treturn _buildingFrontBlock->block;\n\t}\n\n\tconst auto addNewBlock = blocks.empty()\n\t\t|| (blocks.back()->messages.size() >= kNewBlockEachMessage);\n\tif (addNewBlock) {\n\t\tblocks.push_back(std::make_unique<HistoryBlock>(this));\n\t\tblocks.back()->setIndexInHistory(blocks.size() - 1);\n\t\tblocks.back()->messages.reserve(kNewBlockEachMessage);\n\t}\n\treturn blocks.back().get();\n}\n\nvoid History::viewReplaced(not_null<const Element*> was, Element *now) {\n\tif (scrollTopItem == was) scrollTopItem = now;\n\tif (_firstUnreadView == was) _firstUnreadView = now;\n\tif (_unreadBarView == was) _unreadBarView = now;\n}\n\nvoid History::addItemToBlock(not_null<HistoryItem*> item) {\n\tExpects(!item->mainView());\n\n\tauto block = prepareBlockForAddingItem();\n\n\tblock->messages.push_back(item->createView(_delegateMixin->delegate()));\n\tconst auto view = block->messages.back().get();\n\tview->attachToBlock(block, block->messages.size() - 1);\n\n\tif (item->Has<HistoryServiceNoForwardsToggle>()) {\n\t\tif (const auto prev = view->previousInBlocks()) {\n\t\t\tif (const auto nfr = prev->data()->Get<HistoryServiceNoForwardsRequest>()) {\n\t\t\t\tif (!nfr->actionTaken) {\n\t\t\t\t\tnfr->actionTaken = true;\n\t\t\t\t\tif (nfr->expired) {\n\t\t\t\t\t\towner().requestItemViewRefresh(prev->data());\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (isBuildingFrontBlock() && _buildingFrontBlock->expectedItemsCount > 0) {\n\t\t--_buildingFrontBlock->expectedItemsCount;\n\t}\n}\n\nvoid History::addEdgesToSharedMedia() {\n\tauto from = loadedAtTop() ? 0 : minMsgId();\n\tauto till = loadedAtBottom() ? ServerMaxMsgId : maxMsgId();\n\tfor (auto i = 0; i != Storage::kSharedMediaTypeCount; ++i) {\n\t\tconst auto type = static_cast<Storage::SharedMediaType>(i);\n\t\tsession().storage().add(Storage::SharedMediaAddSlice(\n\t\t\tpeer->id,\n\t\t\tMsgId(0), // topicRootId\n\t\t\tPeerId(0), // monoforumPeerId\n\t\t\ttype,\n\t\t\t{},\n\t\t\t{ from, till }));\n\t}\n}\n\nvoid History::addOlderSlice(const QVector<MTPMessage> &slice) {\n\tif (slice.isEmpty()) {\n\t\t_loadedAtTop = true;\n\t\tcheckLocalMessages();\n\t\treturn;\n\t}\n\n\tif (const auto added = createItems(slice); !added.empty()) {\n\t\taddCreatedOlderSlice(added);\n\t} else {\n\t\t// If no items were added it means we've loaded everything old.\n\t\t_loadedAtTop = true;\n\t\taddEdgesToSharedMedia();\n\t}\n\tcheckLocalMessages();\n\tcheckLastMessage();\n}\n\nvoid History::addCreatedOlderSlice(\n\t\tconst std::vector<not_null<HistoryItem*>> &items) {\n\tstartBuildingFrontBlock(items.size());\n\tfor (const auto &item : items) {\n\t\taddItemToBlock(item);\n\t}\n\tfinishBuildingFrontBlock();\n\n\tif (loadedAtBottom()) {\n\t\t// Add photos to overview and authors to lastAuthors.\n\t\taddItemsToLists(items);\n\n\t\tfor (const auto &item : items) {\n\t\t\tif (const auto sublist = item->savedSublist()) {\n\t\t\t\tsublist->applyMaybeLast(item);\n\t\t\t}\n\t\t}\n\t}\n\taddToSharedMedia(items);\n}\n\nvoid History::addNewerSlice(const QVector<MTPMessage> &slice) {\n\tbool wasLoadedAtBottom = loadedAtBottom();\n\n\tif (slice.isEmpty()) {\n\t\t_loadedAtBottom = true;\n\t\tif (!lastMessage()) {\n\t\t\tsetLastMessage(lastAvailableMessage());\n\t\t}\n\t}\n\n\tif (const auto added = createItems(slice); !added.empty()) {\n\t\tAssert(!isBuildingFrontBlock());\n\n\t\tfor (const auto &item : added) {\n\t\t\taddItemToBlock(item);\n\t\t}\n\n\t\taddToSharedMedia(added);\n\t} else {\n\t\t_loadedAtBottom = true;\n\t\tsetLastMessage(lastAvailableMessage());\n\t\taddEdgesToSharedMedia();\n\t}\n\n\tif (!wasLoadedAtBottom) {\n\t\tcheckAddAllToUnreadMentions();\n\t}\n\n\tcheckLocalMessages();\n\tcheckLastMessage();\n}\n\nvoid History::checkLastMessage() {\n\tif (const auto last = lastMessage()) {\n\t\tif (!_loadedAtBottom && last->mainView()) {\n\t\t\t_loadedAtBottom = true;\n\t\t\tcheckAddAllToUnreadMentions();\n\t\t}\n\t} else if (_loadedAtBottom) {\n\t\tsetLastMessage(lastAvailableMessage());\n\t}\n}\n\nvoid History::addItemsToLists(\n\t\tconst std::vector<not_null<HistoryItem*>> &items) {\n\tstd::deque<not_null<UserData*>> *lastAuthors = nullptr;\n\tbase::flat_set<not_null<PeerData*>> *markupSenders = nullptr;\n\tif (peer->isChat()) {\n\t\tlastAuthors = &peer->asChat()->lastAuthors;\n\t\tmarkupSenders = &peer->asChat()->markupSenders;\n\t} else if (peer->isMegagroup()) {\n\t\t// We don't add users to mgInfo->lastParticipants here.\n\t\t// We're scrolling back and we see messages from users that\n\t\t// could be gone from the megagroup already. It is fine for\n\t\t// chat->lastAuthors, because they're used only for field\n\t\t// autocomplete, but this is bad for megagroups, because its\n\t\t// lastParticipants are displayed in Profile as members list.\n\t\tmarkupSenders = &peer->asChannel()->mgInfo->markupSenders;\n\t}\n\tfor (const auto &item : ranges::views::reverse(items)) {\n\t\titem->addToUnreadThings(HistoryUnreadThings::AddType::Existing);\n\t\tif (item->from()->id) {\n\t\t\tif (lastAuthors) { // chats\n\t\t\t\tif (auto user = item->from()->asUser()) {\n\t\t\t\t\tif (!base::contains(*lastAuthors, user)) {\n\t\t\t\t\t\tlastAuthors->push_back(user);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (item->author()->id) {\n\t\t\tif (markupSenders) { // chats with bots\n\t\t\t\tif (!lastKeyboardInited && item->definesReplyKeyboard() && !item->out()) {\n\t\t\t\t\tconst auto markupFlags = item->replyKeyboardFlags();\n\t\t\t\t\tif (!(markupFlags & ReplyMarkupFlag::Selective) || item->mentionsMe()) {\n\t\t\t\t\t\tbool wasKeyboardHide = markupSenders->contains(item->author());\n\t\t\t\t\t\tif (!wasKeyboardHide) {\n\t\t\t\t\t\t\tmarkupSenders->insert(item->author());\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!(markupFlags & ReplyMarkupFlag::None)) {\n\t\t\t\t\t\t\tif (!lastKeyboardInited) {\n\t\t\t\t\t\t\t\tbool botNotInChat = false;\n\t\t\t\t\t\t\t\tif (peer->isChat()) {\n\t\t\t\t\t\t\t\t\tbotNotInChat = (!Data::CanSendAnything(peer)\n\t\t\t\t\t\t\t\t\t\t|| !peer->asChat()->participants.empty())\n\t\t\t\t\t\t\t\t\t\t&& item->author()->isUser()\n\t\t\t\t\t\t\t\t\t\t&& !peer->asChat()->participants.contains(item->author()->asUser());\n\t\t\t\t\t\t\t\t} else if (peer->isMegagroup()) {\n\t\t\t\t\t\t\t\t\tbotNotInChat = (!Data::CanSendAnything(peer)\n\t\t\t\t\t\t\t\t\t\t|| peer->asChannel()->mgInfo->botStatus != Data::BotStatus::Unknown)\n\t\t\t\t\t\t\t\t\t\t&& item->author()->isUser()\n\t\t\t\t\t\t\t\t\t\t&& !peer->asChannel()->mgInfo->bots.contains(item->author()->asUser());\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif (wasKeyboardHide || botNotInChat) {\n\t\t\t\t\t\t\t\t\tclearLastKeyboard();\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tlastKeyboardInited = true;\n\t\t\t\t\t\t\t\t\tlastKeyboardId = item->id;\n\t\t\t\t\t\t\t\t\tlastKeyboardFrom = item->author()->id;\n\t\t\t\t\t\t\t\t\tlastKeyboardUsed = false;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (!lastKeyboardInited && item->definesReplyKeyboard() && !item->out()) { // conversations with bots\n\t\t\t\tconst auto markupFlags = item->replyKeyboardFlags();\n\t\t\t\tif (!(markupFlags & ReplyMarkupFlag::Selective) || item->mentionsMe()) {\n\t\t\t\t\tif (markupFlags & ReplyMarkupFlag::None) {\n\t\t\t\t\t\tclearLastKeyboard();\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlastKeyboardInited = true;\n\t\t\t\t\t\tlastKeyboardId = item->id;\n\t\t\t\t\t\tlastKeyboardFrom = item->author()->id;\n\t\t\t\t\t\tlastKeyboardUsed = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n}\n\nvoid History::checkAddAllToUnreadMentions() {\n\tif (!loadedAtBottom()) {\n\t\treturn;\n\t}\n\n\tfor (const auto &block : blocks) {\n\t\tfor (const auto &message : block->messages) {\n\t\t\tconst auto item = message->data();\n\t\t\titem->addToUnreadThings(HistoryUnreadThings::AddType::Existing);\n\t\t}\n\t}\n}\n\nvoid History::addToSharedMedia(\n\t\tconst std::vector<not_null<HistoryItem*>> &items) {\n\tstd::vector<MsgId> medias[Storage::kSharedMediaTypeCount];\n\tauto topicsWithPinned = base::flat_set<not_null<Data::ForumTopic*>>();\n\tfor (const auto &item : items) {\n\t\tif (const auto types = item->sharedMediaTypes()) {\n\t\t\tfor (auto i = 0; i != Storage::kSharedMediaTypeCount; ++i) {\n\t\t\t\tconst auto type = static_cast<Storage::SharedMediaType>(i);\n\t\t\t\tif (types.test(type)) {\n\t\t\t\t\tif (medias[i].empty()) {\n\t\t\t\t\t\tmedias[i].reserve(items.size());\n\t\t\t\t\t}\n\t\t\t\t\tmedias[i].push_back(item->id);\n\t\t\t\t\tif (type == Storage::SharedMediaType::Pinned) {\n\t\t\t\t\t\tif (const auto topic = item->topic()) {\n\t\t\t\t\t\t\tif (!topic->hasPinnedMessages()) {\n\t\t\t\t\t\t\t\ttopicsWithPinned.emplace(topic);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tconst auto from = loadedAtTop() ? 0 : minMsgId();\n\tconst auto till = loadedAtBottom() ? ServerMaxMsgId : maxMsgId();\n\tfor (auto i = 0; i != Storage::kSharedMediaTypeCount; ++i) {\n\t\tif (!medias[i].empty()) {\n\t\t\tconst auto type = static_cast<Storage::SharedMediaType>(i);\n\t\t\tsession().storage().add(Storage::SharedMediaAddSlice(\n\t\t\t\tpeer->id,\n\t\t\t\tMsgId(0), // topicRootId\n\t\t\t\tPeerId(0), // monoforumPeerId\n\t\t\t\ttype,\n\t\t\t\tstd::move(medias[i]),\n\t\t\t\t{ from, till }));\n\t\t\tif (type == Storage::SharedMediaType::Pinned) {\n\t\t\t\tsetHasPinnedMessages(true);\n\t\t\t}\n\t\t}\n\t}\n\tfor (const auto &topic : topicsWithPinned) {\n\t\ttopic->setHasPinnedMessages(true);\n\t}\n}\n\nvoid History::calculateFirstUnreadMessage() {\n\tif (!_inboxReadBefore) {\n\t\treturn;\n\t}\n\n\t_firstUnreadView = nullptr;\n\tif (!unreadCount() || !trackUnreadMessages()) {\n\t\treturn;\n\t}\n\tfor (const auto &block : ranges::views::reverse(blocks)) {\n\t\tfor (const auto &message : ranges::views::reverse(block->messages)) {\n\t\t\tconst auto item = message->data();\n\t\t\tif (!item->isRegular()) {\n\t\t\t\tcontinue;\n\t\t\t} else if (!item->out()) {\n\t\t\t\tif (item->id >= *_inboxReadBefore) {\n\t\t\t\t\t_firstUnreadView = message.get();\n\t\t\t\t} else {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nbool History::readInboxTillNeedsRequest(MsgId tillId) {\n\tExpects(!tillId || IsServerMsgId(tillId));\n\n\treadClientSideMessages();\n\tif (unreadMark()) {\n\t\towner().histories().changeDialogUnreadMark(this, false);\n\t}\n\tDEBUG_LOG((\"Reading: readInboxTillNeedsRequest is_server %1, before %2.\"\n\t\t).arg(Logs::b(IsServerMsgId(tillId))\n\t\t).arg(_inboxReadBefore.value_or(-666).bare));\n\treturn IsServerMsgId(tillId) && (_inboxReadBefore.value_or(1) <= tillId);\n}\n\nvoid History::readClientSideMessages() {\n\tauto &histories = owner().histories();\n\tfor (const auto &item : _clientSideMessages) {\n\t\thistories.readClientSideMessage(item);\n\t}\n}\n\nbool History::unreadCountRefreshNeeded(MsgId readTillId) const {\n\treturn !unreadCountKnown()\n\t\t|| ((readTillId + 1) > _inboxReadBefore.value_or(0));\n}\n\nstd::optional<int> History::countStillUnreadLocal(MsgId readTillId) const {\n\tif (isEmpty() || !folderKnown()) {\n\t\tDEBUG_LOG((\"Reading: countStillUnreadLocal unknown %1 and %2.\").arg(\n\t\t\tLogs::b(isEmpty()),\n\t\t\tLogs::b(folderKnown())));\n\t\treturn std::nullopt;\n\t}\n\tif (_inboxReadBefore) {\n\t\tconst auto before = *_inboxReadBefore;\n\t\tDEBUG_LOG((\"Reading: check before %1 with min %2 and max %3.\"\n\t\t\t).arg(before.bare\n\t\t\t).arg(minMsgId().bare\n\t\t\t).arg(maxMsgId().bare));\n\t\tif (minMsgId() <= before && maxMsgId() >= readTillId) {\n\t\t\tauto result = 0;\n\t\t\tfor (const auto &block : blocks) {\n\t\t\t\tfor (const auto &message : block->messages) {\n\t\t\t\t\tconst auto item = message->data();\n\t\t\t\t\tif (!item->isRegular()\n\t\t\t\t\t\t|| (item->out() && !item->isFromScheduled())) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t} else if (item->id > readTillId) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else if (item->id >= before) {\n\t\t\t\t\t\t++result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tDEBUG_LOG((\"Reading: check before result %1 with existing %2\"\n\t\t\t\t).arg(result\n\t\t\t\t).arg(_unreadCount.value_or(-666)));\n\t\t\tif (_unreadCount) {\n\t\t\t\treturn std::max(*_unreadCount - result, 0);\n\t\t\t}\n\t\t}\n\t}\n\tconst auto minimalServerId = minMsgId();\n\tDEBUG_LOG((\"Reading: check at end loaded from %1 loaded %2 - %3\").arg(\n\t\tQString::number(minimalServerId.bare),\n\t\tLogs::b(loadedAtBottom()),\n\t\tLogs::b(loadedAtTop())));\n\tif (!loadedAtBottom()\n\t\t|| (!loadedAtTop() && !minimalServerId)\n\t\t|| minimalServerId > readTillId) {\n\t\treturn std::nullopt;\n\t}\n\tauto result = 0;\n\tfor (const auto &block : ranges::views::reverse(blocks)) {\n\t\tfor (const auto &message : ranges::views::reverse(block->messages)) {\n\t\t\tconst auto item = message->data();\n\t\t\tif (item->isRegular()) {\n\t\t\t\tif (item->id <= readTillId) {\n\t\t\t\t\treturn result;\n\t\t\t\t} else if (!item->out()) {\n\t\t\t\t\t++result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tDEBUG_LOG((\"Reading: check at end counted %1\").arg(result));\n\treturn result;\n}\n\nvoid History::applyInboxReadUpdate(\n\t\tFolderId folderId,\n\t\tMsgId upTo,\n\t\tint stillUnread,\n\t\tint32 channelPts) {\n\tconst auto folder = folderId ? owner().folderLoaded(folderId) : nullptr;\n\tif (folder && this->folder() != folder) {\n\t\t// If history folder is unknown or not synced, request both.\n\t\towner().histories().requestDialogEntry(this);\n\t\towner().histories().requestDialogEntry(folder);\n\t}\n\tif (_inboxReadBefore.value_or(1) <= upTo) {\n\t\tif (!peer->isChannel() || peer->asChannel()->pts() == channelPts) {\n\t\t\tinboxRead(upTo, stillUnread);\n\t\t} else {\n\t\t\tinboxRead(upTo);\n\t\t}\n\t}\n}\n\nvoid History::inboxRead(MsgId upTo, std::optional<int> stillUnread) {\n\tif (stillUnread.has_value() && folderKnown()) {\n\t\tsetUnreadCount(*stillUnread);\n\t} else if (const auto still = countStillUnreadLocal(upTo)) {\n\t\tsetUnreadCount(*still);\n\t} else {\n\t\towner().histories().requestDialogEntry(this);\n\t}\n\tsetInboxReadTill(upTo);\n\tupdateChatListEntry();\n\tif (const auto to = peer->migrateTo()) {\n\t\tif (const auto migrated = peer->owner().historyLoaded(to->id)) {\n\t\t\tmigrated->updateChatListEntry();\n\t\t}\n\t}\n\n\t_firstUnreadView = nullptr;\n\tCore::App().notifications().clearIncomingFromHistory(this);\n}\n\nvoid History::inboxRead(not_null<const HistoryItem*> wasRead) {\n\tif (wasRead->isRegular()) {\n\t\tinboxRead(wasRead->id);\n\t}\n}\n\nvoid History::outboxRead(MsgId upTo) {\n\tsetOutboxReadTill(upTo);\n\tif (const auto last = chatListMessage()) {\n\t\tif (last->out() && last->isRegular() && last->id <= upTo) {\n\t\t\tsession().changes().messageUpdated(\n\t\t\t\tlast,\n\t\t\t\tData::MessageUpdate::Flag::DialogRowRepaint);\n\t\t}\n\t}\n\tupdateChatListEntry();\n\tsession().changes().historyUpdated(this, UpdateFlag::OutboxRead);\n}\n\nvoid History::outboxRead(not_null<const HistoryItem*> wasRead) {\n\tif (wasRead->isRegular()) {\n\t\toutboxRead(wasRead->id);\n\t}\n}\n\nMsgId History::loadAroundId() const {\n\tif (_unreadCount && *_unreadCount > 0 && _inboxReadBefore) {\n\t\treturn *_inboxReadBefore;\n\t}\n\treturn MsgId(0);\n}\n\nbool History::inboxReadTillKnown() const {\n\treturn _inboxReadBefore.has_value();\n}\n\nMsgId History::inboxReadTillId() const {\n\treturn _inboxReadBefore.value_or(1) - 1;\n}\n\nMsgId History::outboxReadTillId() const {\n\treturn _outboxReadBefore.value_or(1) - 1;\n}\n\nHistoryItem *History::lastAvailableMessage() const {\n\treturn isEmpty() ? nullptr : blocks.back()->messages.back()->data().get();\n}\n\nint History::unreadCount() const {\n\tif (isTopPromoted()) {\n\t\treturn 0;\n\t}\n\treturn _unreadCount ? *_unreadCount : 0;\n}\n\nbool History::unreadCountKnown() const {\n\treturn _unreadCount.has_value();\n}\n\nbool History::useMyUnreadInParent() const {\n\treturn !isForum() && !amMonoforumAdmin();\n}\n\nvoid History::setUnreadCount(int newUnreadCount) {\n\tExpects(folderKnown());\n\tif (isTopPromoted()) {\n\t\tnewUnreadCount = 0;\n\t}\n\n\tif (_unreadCount == newUnreadCount) {\n\t\treturn;\n\t}\n\tconst auto notifier = unreadStateChangeNotifier(useMyUnreadInParent());\n\t_unreadCount = newUnreadCount;\n\n\tconst auto lastOutgoing = [&] {\n\t\tconst auto last = lastMessage();\n\t\treturn last\n\t\t\t&& last->isRegular()\n\t\t\t&& loadedAtBottom()\n\t\t\t&& !isEmpty()\n\t\t\t&& blocks.back()->messages.back()->data() == last\n\t\t\t&& last->out();\n\t}();\n\tif (newUnreadCount == 1 && !lastOutgoing) {\n\t\tif (loadedAtBottom()) {\n\t\t\t_firstUnreadView = !isEmpty()\n\t\t\t\t? blocks.back()->messages.back().get()\n\t\t\t\t: nullptr;\n\t\t}\n\t\tif (const auto last = msgIdForRead()) {\n\t\t\tsetInboxReadTill(last - 1);\n\t\t}\n\t} else if (!newUnreadCount) {\n\t\t_firstUnreadView = nullptr;\n\t\tif (const auto last = msgIdForRead()) {\n\t\t\tsetInboxReadTill(last);\n\t\t}\n\t} else if (!_firstUnreadView && !_unreadBarView && loadedAtBottom()) {\n\t\tcalculateFirstUnreadMessage();\n\t}\n\tif (Core::ForkSettings::PrimaryUnmutedMessages()) {\n\t\tupdateChatListSortPosition();\n\t\tupdateChatListEntry();\n\t}\n}\n\nvoid History::setUnreadMark(bool unread) {\n\tif (clearUnreadOnClientSide()) {\n\t\tunread = false;\n\t}\n\tif (unreadMark() == unread) {\n\t\treturn;\n\t}\n\tconst auto notifier = unreadStateChangeNotifier(\n\t\tuseMyUnreadInParent() && !unreadCount());\n\tThread::setUnreadMarkFlag(unread);\n}\n\nvoid History::setFakeUnreadWhileOpened(bool enabled) {\n\tif (fakeUnreadWhileOpened() == enabled) {\n\t\treturn;\n\t} else if (enabled) {\n\t\tif (!inChatList()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto state = chatListBadgesState();\n\t\tif (!state.unread && !state.mention) {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (enabled) {\n\t\t_flags |= Flag::FakeUnreadWhileOpened;\n\t} else {\n\t\t_flags &= ~Flag::FakeUnreadWhileOpened;\n\t}\n\towner().chatsFilters().refreshHistory(this);\n}\n\n[[nodiscard]] bool History::fakeUnreadWhileOpened() const {\n\treturn (_flags & Flag::FakeUnreadWhileOpened);\n}\n\nvoid History::setMuted(bool muted) {\n\tif (this->muted() == muted) {\n\t\treturn;\n\t} else {\n\t\tconst auto state = useMyUnreadInParent()\n\t\t\t? computeBadgesState()\n\t\t\t: Dialogs::BadgesState();\n\t\tconst auto notify = (state.unread || state.reaction || state.poll);\n\t\tconst auto notifier = unreadStateChangeNotifier(notify);\n\t\tThread::setMuted(muted);\n\t}\n\tsession().changes().peerUpdated(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::Notifications);\n\towner().chatsFilters().refreshHistory(this);\n\tif (const auto forum = peer->forum()) {\n\t\towner().notifySettings().forumParentMuteUpdated(forum);\n\t}\n\tif (Core::ForkSettings::PrimaryUnmutedMessages()) {\n\t\tupdateChatListSortPosition();\n\t\tupdateChatListEntry();\n\t}\n}\n\nvoid History::getNextFirstUnreadMessage() {\n\tExpects(_firstUnreadView != nullptr);\n\n\tconst auto block = _firstUnreadView->block();\n\tconst auto index = _firstUnreadView->indexInBlock();\n\tconst auto setFromMessage = [&](const auto &view) {\n\t\tif (view->data()->isRegular()) {\n\t\t\t_firstUnreadView = view.get();\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\tif (index >= 0) {\n\t\tconst auto count = int(block->messages.size());\n\t\tfor (auto i = index + 1; i != count; ++i) {\n\t\t\tconst auto &message = block->messages[i];\n\t\t\tif (setFromMessage(message)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst auto count = int(blocks.size());\n\tfor (auto j = block->indexInHistory() + 1; j != count; ++j) {\n\t\tfor (const auto &message : blocks[j]->messages) {\n\t\t\tif (setFromMessage(message)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\t_firstUnreadView = nullptr;\n}\n\nMsgId History::nextNonHistoryEntryId() {\n\treturn owner().nextNonHistoryEntryId();\n}\n\nbool History::folderKnown() const {\n\treturn _folder.has_value();\n}\n\nData::Folder *History::folder() const {\n\treturn _folder.value_or(nullptr);\n}\n\nvoid History::setFolder(\n\t\tnot_null<Data::Folder*> folder,\n\t\tHistoryItem *folderDialogItem) {\n\tsetFolderPointer(folder);\n\tif (folderDialogItem) {\n\t\tsetLastServerMessage(folderDialogItem);\n\t}\n}\n\nvoid History::clearFolder() {\n\tsetFolderPointer(nullptr);\n}\n\nvoid History::setFolderPointer(Data::Folder *folder) {\n\tif (_folder == folder) {\n\t\treturn;\n\t}\n\tif (isPinnedDialog(FilterId())) {\n\t\towner().setChatPinned(this, FilterId(), false);\n\t}\n\tconst auto wasKnown = folderKnown();\n\tconst auto wasInList = inChatList();\n\tif (wasInList) {\n\t\tremoveFromChatList(0, owner().chatsList(this->folder()));\n\t}\n\tconst auto was = _folder.value_or(nullptr);\n\t_folder = folder;\n\tif (was) {\n\t\twas->unregisterOne(this);\n\t}\n\tif (wasInList) {\n\t\taddToChatList(0, owner().chatsList(folder));\n\n\t\towner().chatsFilters().refreshHistory(this);\n\t\tupdateChatListEntry();\n\n\t\towner().chatsListChanged(was);\n\t\towner().chatsListChanged(folder);\n\t} else if (!wasKnown) {\n\t\tupdateChatListSortPosition();\n\t}\n\tif (folder) {\n\t\tfolder->registerOne(this);\n\t}\n\tsession().changes().historyUpdated(this, UpdateFlag::Folder);\n}\n\nint History::chatListNameVersion() const {\n\treturn peer->nameVersion();\n}\n\nvoid History::hasUnreadMentionChanged(bool has) {\n\tif (isForum()) {\n\t\treturn;\n\t}\n\tauto was = chatListUnreadState();\n\tif (has) {\n\t\twas.mentions = 0;\n\t} else {\n\t\twas.mentions = 1;\n\t}\n\tnotifyUnreadStateChange(was);\n}\n\nvoid History::hasUnreadReactionChanged(bool has) {\n\tif (isForum()) {\n\t\treturn;\n\t}\n\tauto was = chatListUnreadState();\n\tif (has) {\n\t\twas.reactions = was.reactionsMuted = 0;\n\t} else {\n\t\twas.reactions = 1;\n\t\twas.reactionsMuted = muted() ? was.reactions : 0;\n\t}\n\tnotifyUnreadStateChange(was);\n}\n\nvoid History::hasUnreadPollVoteChanged(bool has) {\n\tif (isForum()) {\n\t\treturn;\n\t}\n\tauto was = chatListUnreadState();\n\tif (has) {\n\t\twas.polls = was.pollsMuted = 0;\n\t} else {\n\t\twas.polls = 1;\n\t\twas.pollsMuted = muted() ? was.polls : 0;\n\t}\n\tnotifyUnreadStateChange(was);\n}\n\nvoid History::applyPinnedUpdate(const MTPDupdateDialogPinned &data) {\n\tconst auto folderId = data.vfolder_id().value_or_empty();\n\tif (!folderKnown()) {\n\t\tif (folderId) {\n\t\t\tsetFolder(owner().folder(folderId));\n\t\t} else {\n\t\t\tclearFolder();\n\t\t}\n\t}\n\towner().setChatPinned(this, FilterId(), data.is_pinned());\n}\n\nTimeId History::adjustedChatListTimeId() const {\n\tconst auto result = chatListTimeId();\n\tif (const auto draft = cloudDraft(MsgId(), PeerId())) {\n\t\tif (!peer->forum()\n\t\t\t&& !Data::DraftIsNull(draft)\n\t\t\t&& !session().supportMode()) {\n\t\t\treturn std::max(result, draft->date);\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid History::countScrollState(int top) {\n\tstd::tie(scrollTopItem, scrollTopOffset) = findItemAndOffset(top);\n}\n\nauto History::findItemAndOffset(int top) const -> std::pair<Element*, int> {\n\tif (const auto element = findScrollTopItem(top)) {\n\t\treturn { element, (top - element->block()->y() - element->y()) };\n\t}\n\treturn {};\n}\n\nauto History::findScrollTopItem(int top) const -> Element* {\n\tif (isEmpty()) {\n\t\treturn nullptr;\n\t}\n\n\tauto itemIndex = 0;\n\tauto blockIndex = 0;\n\tauto itemTop = 0;\n\tif (scrollTopItem) {\n\t\titemIndex = scrollTopItem->indexInBlock();\n\t\tblockIndex = scrollTopItem->block()->indexInHistory();\n\t\titemTop = blocks[blockIndex]->y() + scrollTopItem->y();\n\t}\n\tif (itemTop > top) {\n\t\t// go backward through history while we don't find an item that starts above\n\t\tdo {\n\t\t\tconst auto &block = blocks[blockIndex];\n\t\t\tfor (--itemIndex; itemIndex >= 0; --itemIndex) {\n\t\t\t\tconst auto view = block->messages[itemIndex].get();\n\t\t\t\titemTop = block->y() + view->y();\n\t\t\t\tif (itemTop <= top) {\n\t\t\t\t\treturn view;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (--blockIndex >= 0) {\n\t\t\t\titemIndex = blocks[blockIndex]->messages.size();\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t} while (true);\n\n\t\treturn blocks.front()->messages.front().get();\n\t}\n\t// go forward through history while we don't find the last item that starts above\n\tfor (auto blocksCount = int(blocks.size()); blockIndex < blocksCount; ++blockIndex) {\n\t\tconst auto &block = blocks[blockIndex];\n\t\tfor (auto itemsCount = int(block->messages.size()); itemIndex < itemsCount; ++itemIndex) {\n\t\t\titemTop = block->y() + block->messages[itemIndex]->y();\n\t\t\tif (itemTop > top) {\n\t\t\t\tAssert(itemIndex > 0 || blockIndex > 0);\n\t\t\t\tif (itemIndex > 0) {\n\t\t\t\t\treturn block->messages[itemIndex - 1].get();\n\t\t\t\t}\n\t\t\t\treturn blocks[blockIndex - 1]->messages.back().get();\n\t\t\t}\n\t\t}\n\t\titemIndex = 0;\n\t}\n\treturn blocks.back()->messages.back().get();\n}\n\nvoid History::getNextScrollTopItem(HistoryBlock *block, int32 i) {\n\t++i;\n\tif (i > 0 && i < block->messages.size()) {\n\t\tscrollTopItem = block->messages[i].get();\n\t\treturn;\n\t}\n\tint j = block->indexInHistory() + 1;\n\tif (j > 0 && j < blocks.size()) {\n\t\tscrollTopItem = blocks[j]->messages.front().get();\n\t\treturn;\n\t}\n\tscrollTopItem = nullptr;\n}\n\nvoid History::addUnreadBar() {\n\tif (!_unreadBarView && _firstUnreadView && unreadCount()) {\n\t\t_unreadBarView = _firstUnreadView;\n\t\t_unreadBarView->createUnreadBar(tr::lng_unread_bar_some());\n\t}\n}\n\nvoid History::destroyUnreadBar() {\n\tif (const auto view = base::take(_unreadBarView)) {\n\t\tview->destroyUnreadBar();\n\t}\n}\n\nvoid History::unsetFirstUnreadMessage() {\n\t_firstUnreadView = nullptr;\n}\n\nHistoryView::Element *History::unreadBar() const {\n\treturn _unreadBarView;\n}\n\nHistoryView::Element *History::firstUnreadMessage() const {\n\treturn _firstUnreadView;\n}\n\nnot_null<HistoryItem*> History::addNewInTheMiddle(\n\t\tnot_null<HistoryItem*> item,\n\t\tint blockIndex,\n\t\tint itemIndex) {\n\tExpects(blockIndex >= 0);\n\tExpects(blockIndex < blocks.size());\n\tExpects(itemIndex >= 0);\n\tExpects(itemIndex <= blocks[blockIndex]->messages.size());\n\n\tconst auto &block = blocks[blockIndex];\n\n\tconst auto it = block->messages.insert(\n\t\tblock->messages.begin() + itemIndex,\n\t\titem->createView(_delegateMixin->delegate()));\n\t(*it)->attachToBlock(block.get(), itemIndex);\n\tif (itemIndex + 1 < block->messages.size()) {\n\t\tfor (auto i = itemIndex + 1, l = int(block->messages.size()); i != l; ++i) {\n\t\t\tblock->messages[i]->setIndexInBlock(i);\n\t\t}\n\t\tblock->messages[itemIndex + 1]->previousInBlocksChanged();\n\t} else if (blockIndex + 1 < blocks.size() && !blocks[blockIndex + 1]->messages.empty()) {\n\t\tblocks[blockIndex + 1]->messages.front()->previousInBlocksChanged();\n\t} else {\n\t\t(*it)->nextInBlocksRemoved();\n\t}\n\n\treturn item;\n}\n\nHistory *History::migrateSibling() const {\n\tconst auto addFromId = [&] {\n\t\tif (const auto from = peer->migrateFrom()) {\n\t\t\treturn from->id;\n\t\t} else if (const auto to = peer->migrateTo()) {\n\t\t\treturn to->id;\n\t\t}\n\t\treturn PeerId(0);\n\t}();\n\treturn owner().historyLoaded(addFromId);\n}\n\nDialogs::UnreadState History::chatListUnreadState() const {\n\tif (const auto forum = peer->forum()) {\n\t\treturn AdjustedForumUnreadState(forum->topicsList()->unreadState());\n\t} else if (const auto monoforum = peer->monoforum()) {\n\t\treturn AdjustedForumUnreadState(\n\t\t\twithMyMuted(monoforum->chatsList()->unreadState()));;\n\t}\n\treturn computeUnreadState();\n}\n\nDialogs::BadgesState History::chatListBadgesState() const {\n\tif (const auto forum = peer->forum()) {\n\t\treturn adjustBadgesStateByFolder(\n\t\t\tDialogs::BadgesForUnread(\n\t\t\t\tforum->topicsList()->unreadState(),\n\t\t\t\tDialogs::CountInBadge::Chats,\n\t\t\t\tDialogs::IncludeInBadge::UnmutedOrAll));\n\t} else if (const auto monoforum = peer->monoforum()) {\n\t\treturn adjustBadgesStateByFolder(\n\t\t\tDialogs::BadgesForUnread(\n\t\t\t\twithMyMuted(monoforum->chatsList()->unreadState()),\n\t\t\t\tDialogs::CountInBadge::Chats,\n\t\t\t\tDialogs::IncludeInBadge::All));\n\t}\n\treturn computeBadgesState();\n}\n\nDialogs::BadgesState History::computeBadgesState() const {\n\treturn adjustBadgesStateByFolder(\n\t\tDialogs::BadgesForUnread(\n\t\t\tcomputeUnreadState(),\n\t\t\tDialogs::CountInBadge::Messages,\n\t\t\tDialogs::IncludeInBadge::All));\n}\n\nDialogs::BadgesState History::adjustBadgesStateByFolder(\n\t\tDialogs::BadgesState state) const {\n\tif (folder()) {\n\t\tstate.mentionMuted\n\t\t\t= state.reactionMuted\n\t\t\t= state.pollMuted\n\t\t\t= state.unreadMuted\n\t\t\t= true;\n\t}\n\treturn state;\n}\n\nDialogs::UnreadState History::computeUnreadState() const {\n\tauto result = Dialogs::UnreadState();\n\tconst auto count = _unreadCount.value_or(0);\n\tconst auto mark = !count && unreadMark();\n\tconst auto muted = this->muted();\n\tresult.messages = count;\n\tresult.chats = count ? 1 : 0;\n\tresult.marks = mark ? 1 : 0;\n\tresult.mentions = unreadMentions().has() ? 1 : 0;\n\tresult.reactions = unreadReactions().has() ? 1 : 0;\n\tresult.polls = unreadPollVotes().has() ? 1 : 0;\n\tresult.messagesMuted = muted ? result.messages : 0;\n\tresult.chatsMuted = muted ? result.chats : 0;\n\tresult.marksMuted = muted ? result.marks : 0;\n\tresult.reactionsMuted = muted ? result.reactions : 0;\n\tresult.pollsMuted = muted ? result.polls : 0;\n\tresult.known = _unreadCount.has_value();\n\treturn result;\n}\n\nDialogs::UnreadState History::withMyMuted(Dialogs::UnreadState state) const {\n\tif (muted()) {\n\t\tstate.chatsMuted = state.chats;\n\t\tstate.marksMuted = state.marks;\n\t\tstate.messagesMuted = state.messages;\n\t\tstate.reactionsMuted = state.reactions;\n\t\tstate.pollsMuted = state.polls;\n\t}\n\treturn state;\n}\n\nvoid History::allowChatListMessageResolve() {\n\tif (_flags & Flag::ResolveChatListMessage) {\n\t\treturn;\n\t}\n\t_flags |= Flag::ResolveChatListMessage;\n\tif (!chatListMessageKnown()) {\n\t\trequestChatListMessage();\n\t} else {\n\t\tresolveChatListMessageGroup();\n\t}\n}\n\nvoid History::resolveChatListMessageGroup() {\n\tconst auto item = _chatListMessage.value_or(nullptr);\n\tif (!(_flags & Flag::ResolveChatListMessage)\n\t\t|| !item\n\t\t|| !hasOrphanMediaGroupPart()) {\n\t\treturn;\n\t}\n\t// If we set a single album part, request the full album.\n\tconst auto withImages = !item->toPreview({\n\t\t.hideSender = true,\n\t\t.hideCaption = true }).images.empty();\n\tif (withImages) {\n\t\towner().histories().requestGroupAround(item);\n\t}\n\tif (unreadCountKnown() && !unreadCount()) {\n\t\t// When we add just one last item, like we do while loading dialogs,\n\t\t// we want to remove a single added grouped media, otherwise it will\n\t\t// jump once we open the message history (first we show only that\n\t\t// media, then we load the rest of the group and show the group).\n\t\t//\n\t\t// That way when we open the message history we show nothing until a\n\t\t// whole history part is loaded, it certainly will contain the group.\n\t\tclear(ClearType::Unload);\n\t}\n}\n\nHistoryItem *History::chatListMessage() const {\n\treturn _chatListMessage.value_or(nullptr);\n}\n\nbool History::chatListMessageKnown() const {\n\treturn _chatListMessage.has_value();\n}\n\nconst QString &History::chatListName() const {\n\tif (const auto broadcast = peer->monoforumBroadcast()) {\n\t\treturn broadcast->name();\n\t}\n\treturn peer->name();\n}\n\nconst QString &History::chatListNameSortKey() const {\n\treturn _chatListNameSortKey;\n}\n\nvoid History::refreshChatListNameSortKey() {\n\t_chatListNameSortKey = TextUtilities::NameSortKey(peer->name());\n}\n\nconst base::flat_set<QString> &History::chatListNameWords() const {\n\treturn peer->nameWords();\n}\n\nconst base::flat_set<QChar> &History::chatListFirstLetters() const {\n\treturn peer->nameFirstLetters();\n}\n\nvoid History::chatListPreloadData() {\n\tpeer->loadUserpic();\n\tallowChatListMessageResolve();\n}\n\nvoid History::paintUserpic(\n\t\tPainter &p,\n\t\tUi::PeerUserpicView &view,\n\t\tconst Dialogs::Ui::PaintContext &context) const {\n\tpeer->paintUserpic(\n\t\tp,\n\t\tview,\n\t\tcontext.st->padding.left(),\n\t\tcontext.st->padding.top(),\n\t\tcontext.st->photoSize);\n}\n\nvoid History::startBuildingFrontBlock(int expectedItemsCount) {\n\tAssert(!isBuildingFrontBlock());\n\tAssert(expectedItemsCount > 0);\n\n\t_buildingFrontBlock = std::make_unique<BuildingBlock>();\n\t_buildingFrontBlock->expectedItemsCount = expectedItemsCount;\n}\n\nvoid History::finishBuildingFrontBlock() {\n\tExpects(isBuildingFrontBlock());\n\n\t// Some checks if there was some message history already\n\tif (const auto block = base::take(_buildingFrontBlock)->block) {\n\t\tif (blocks.size() > 1) {\n\t\t\t// ... item, item, item, last ], [ first, item, item ...\n\t\t\tconst auto first = blocks[1]->messages.front().get();\n\n\t\t\t// we've added a new front block, so previous item for\n\t\t\t// the old first item of a first block was changed\n\t\t\tfirst->previousInBlocksChanged();\n\n\t\t\tif (first->data()->Has<HistoryServiceNoForwardsToggle>()) {\n\t\t\t\tconst auto last = block->messages.back()->data();\n\t\t\t\tif (const auto nfr = last->Get<HistoryServiceNoForwardsRequest>()) {\n\t\t\t\t\tif (!nfr->actionTaken) {\n\t\t\t\t\t\tnfr->actionTaken = true;\n\t\t\t\t\t\tif (nfr->expired) {\n\t\t\t\t\t\t\towner().requestItemViewRefresh(last);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tblock->messages.back()->nextInBlocksRemoved();\n\t\t}\n\t}\n}\n\nbool History::loadedAtBottom() const {\n\treturn _loadedAtBottom;\n}\n\nbool History::loadedAtTop() const {\n\treturn _loadedAtTop;\n}\n\nbool History::isReadyFor(MsgId msgId) {\n\tif (msgId < 0 && -msgId < ServerMaxMsgId && peer->migrateFrom()) {\n\t\t// Old group history.\n\t\treturn owner().history(peer->migrateFrom()->id)->isReadyFor(-msgId);\n\t}\n\n\tif (msgId == ShowAtTheEndMsgId) {\n\t\treturn loadedAtBottom();\n\t}\n\tif (msgId == ShowAtUnreadMsgId) {\n\t\tif (const auto migratePeer = peer->migrateFrom()) {\n\t\t\tif (const auto migrated = owner().historyLoaded(migratePeer)) {\n\t\t\t\tif (migrated->unreadCount()) {\n\t\t\t\t\treturn migrated->isReadyFor(msgId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (unreadCount() && _inboxReadBefore) {\n\t\t\tif (!isEmpty()) {\n\t\t\t\treturn (loadedAtTop() || minMsgId() <= *_inboxReadBefore)\n\t\t\t\t\t&& (loadedAtBottom() || maxMsgId() >= *_inboxReadBefore);\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\treturn loadedAtBottom();\n\t}\n\tconst auto item = owner().message(peer, msgId);\n\treturn item && (item->history() == this) && item->mainView();\n}\n\nvoid History::getReadyFor(MsgId msgId) {\n\tif (msgId < 0 && -msgId < ServerMaxMsgId && peer->migrateFrom()) {\n\t\tconst auto migrated = owner().history(peer->migrateFrom()->id);\n\t\tmigrated->getReadyFor(-msgId);\n\t\tif (migrated->isEmpty()) {\n\t\t\tclear(ClearType::Unload);\n\t\t}\n\t\treturn;\n\t}\n\tif (msgId == ShowAtUnreadMsgId) {\n\t\tif (const auto migratePeer = peer->migrateFrom()) {\n\t\t\tif (const auto migrated = owner().historyLoaded(migratePeer)) {\n\t\t\t\tif (migrated->unreadCount()) {\n\t\t\t\t\tclear(ClearType::Unload);\n\t\t\t\t\tmigrated->getReadyFor(msgId);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (!isReadyFor(msgId)) {\n\t\tclear(ClearType::Unload);\n\t\tif (const auto migratePeer = peer->migrateFrom()) {\n\t\t\tif (const auto migrated = owner().historyLoaded(migratePeer)) {\n\t\t\t\tmigrated->clear(ClearType::Unload);\n\t\t\t}\n\t\t}\n\t\tif ((msgId == ShowAtTheEndMsgId)\n\t\t\t|| (msgId == ShowAtUnreadMsgId && !unreadCount())) {\n\t\t\t_loadedAtBottom = true;\n\t\t}\n\t}\n}\n\nvoid History::setNotLoadedAtBottom() {\n\t_loadedAtBottom = false;\n\n\tsession().storage().invalidate(\n\t\tStorage::SharedMediaInvalidateBottom(peer->id));\n\tif (const auto messages = _messages.get()) {\n\t\tmessages->invalidateBottom();\n\t}\n}\n\nvoid History::clearSharedMedia() {\n\tsession().storage().remove(\n\t\tStorage::SharedMediaRemoveAll(peer->id));\n}\n\nvoid History::setLastServerMessage(HistoryItem *item) {\n\t_lastServerMessage = item;\n\tif (_lastMessage\n\t\t&& *_lastMessage\n\t\t&& !(*_lastMessage)->isRegular()\n\t\t&& (!item || (*_lastMessage)->date() > item->date())) {\n\t\treturn;\n\t}\n\tsetLastMessage(item);\n}\n\nvoid History::setLastMessage(HistoryItem *item) {\n\tif (_lastMessage && *_lastMessage == item) {\n\t\treturn;\n\t}\n\t_lastMessage = item;\n\tif (!item || item->isRegular()) {\n\t\t_lastServerMessage = item;\n\t}\n\tif (peer->migrateTo()) {\n\t\t// We don't want to request last message for all deactivated chats.\n\t\t// This is a heavy request for them, because we need to get last\n\t\t// two items by messages.getHistory to skip the migration message.\n\t\tsetChatListMessageUnknown();\n\t} else {\n\t\tsetChatListMessageFromLast();\n\t\tif (!chatListMessageKnown()) {\n\t\t\tsetFakeChatListMessage();\n\t\t}\n\t}\n}\n\nvoid History::refreshChatListMessage() {\n\tconst auto known = chatListMessageKnown();\n\tsetChatListMessageFromLast();\n\tif (known && !_chatListMessage) {\n\t\trequestChatListMessage();\n\t}\n}\n\nvoid History::setChatListMessage(HistoryItem *item) {\n\tif (_chatListMessage && *_chatListMessage == item) {\n\t\treturn;\n\t}\n\tconst auto was = _chatListMessage.value_or(nullptr);\n\tif (item) {\n\t\tif (item->isSponsored()) {\n\t\t\treturn;\n\t\t}\n\t\tif (_chatListMessage\n\t\t\t&& *_chatListMessage\n\t\t\t&& !(*_chatListMessage)->isRegular()\n\t\t\t&& (*_chatListMessage)->date() > item->date()) {\n\t\t\treturn;\n\t\t}\n\t\t_chatListMessage = item;\n\t\tsetChatListTimeId(item->date());\n\t\tresolveChatListMessageGroup();\n\t} else if (!_chatListMessage || *_chatListMessage) {\n\t\t_chatListMessage = nullptr;\n\t\tupdateChatListEntry();\n\t}\n\tif (const auto folder = this->folder()) {\n\t\tfolder->oneListMessageChanged(was, item);\n\t}\n\tif (const auto to = peer->migrateTo()) {\n\t\tif (const auto history = owner().historyLoaded(to)) {\n\t\t\tif (!history->chatListMessageKnown()) {\n\t\t\t\thistory->requestChatListMessage();\n\t\t\t}\n\t\t}\n\t}\n}\n\nauto History::computeChatListMessageFromLast() const\n-> std::optional<HistoryItem*> {\n\tif (!_lastMessage) {\n\t\treturn _lastMessage;\n\t}\n\n\t// In migrated groups we want to skip essential message\n\t// about migration in the chats list and display the last\n\t// non-migration message from the original legacy group.\n\tconst auto last = lastMessage();\n\tif (!last || !last->isGroupMigrate()) {\n\t\treturn _lastMessage;\n\t}\n\tif (peer->isChat()) {\n\t\t// In chats we try to take the item before the 'last', which\n\t\t// is the empty-displayed migration message.\n\t\tif (!loadedAtBottom()) {\n\t\t\t// We don't know the tail of the history.\n\t\t\treturn std::nullopt;\n\t\t}\n\t\tconst auto before = [&]() -> HistoryItem* {\n\t\t\tfor (const auto &block : ranges::views::reverse(blocks)) {\n\t\t\t\tconst auto &messages = block->messages;\n\t\t\t\tfor (const auto &item : ranges::views::reverse(messages)) {\n\t\t\t\t\tif (item->data() != last) {\n\t\t\t\t\t\treturn item->data();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nullptr;\n\t\t}();\n\t\tif (before) {\n\t\t\t// We found a message that is not the migration one.\n\t\t\treturn before;\n\t\t} else if (loadedAtTop()) {\n\t\t\t// No other messages in this history.\n\t\t\treturn _lastMessage;\n\t\t}\n\t\treturn std::nullopt;\n\t} else if (const auto from = migrateFrom()) {\n\t\t// In megagroups we just try to use\n\t\t// the message from the original group.\n\t\treturn from->chatListMessageKnown()\n\t\t\t? std::make_optional(from->chatListMessage())\n\t\t\t: std::nullopt;\n\t}\n\treturn _lastMessage;\n}\n\nvoid History::setChatListMessageFromLast() {\n\tif (const auto good = computeChatListMessageFromLast()) {\n\t\tsetChatListMessage(*good);\n\t} else {\n\t\tsetChatListMessageUnknown();\n\t}\n}\n\nvoid History::setChatListMessageUnknown() {\n\tif (!_chatListMessage.has_value()) {\n\t\treturn;\n\t}\n\tconst auto was = *_chatListMessage;\n\t_chatListMessage = std::nullopt;\n\tif (const auto folder = this->folder()) {\n\t\tfolder->oneListMessageChanged(was, nullptr);\n\t}\n}\n\nvoid History::requestChatListMessage() {\n\tif (!lastMessageKnown()) {\n\t\towner().histories().requestDialogEntry(this, [=] {\n\t\t\trequestChatListMessage();\n\t\t});\n\t\treturn;\n\t} else if (chatListMessageKnown()) {\n\t\treturn;\n\t}\n\tsetChatListMessageFromLast();\n\tif (!chatListMessageKnown()) {\n\t\tsetFakeChatListMessage();\n\t}\n}\n\nvoid History::setFakeChatListMessage() {\n\tif (!(_flags & Flag::ResolveChatListMessage)) {\n\t\tif (!chatListTimeId()) {\n\t\t\tif (const auto last = lastMessage()) {\n\t\t\t\tsetChatListTimeId(last->date());\n\t\t\t}\n\t\t}\n\t\treturn;\n\t} else if (peer->isChat()) {\n\t\t// In chats we try to take the item before the 'last', which\n\t\t// is the empty-displayed migration message.\n\t\towner().histories().requestFakeChatListMessage(this);\n\t} else if (const auto from = migrateFrom()) {\n\t\t// In megagroups we just try to use\n\t\t// the message from the original group.\n\t\tfrom->allowChatListMessageResolve();\n\t\tfrom->requestChatListMessage();\n\t}\n}\n\nvoid History::setFakeChatListMessageFrom(const MTPmessages_Messages &data) {\n\tif (!lastMessageKnown()) {\n\t\trequestChatListMessage();\n\t\treturn;\n\t}\n\tconst auto finalize = gsl::finally([&] {\n\t\t// Make sure that we have chatListMessage when we get out of here.\n\t\tif (!chatListMessageKnown()) {\n\t\t\tsetChatListMessage(lastMessage());\n\t\t}\n\t});\n\tconst auto last = lastMessage();\n\tif (!last || !last->isGroupMigrate()) {\n\t\t// Last message is good enough.\n\t\treturn;\n\t}\n\tconst auto other = data.match([&](\n\t\t\tconst MTPDmessages_messagesNotModified &) {\n\t\treturn static_cast<const MTPMessage*>(nullptr);\n\t}, [&](const auto &data) {\n\t\tfor (const auto &message : data.vmessages().v) {\n\t\t\tconst auto id = message.match([](const auto &data) {\n\t\t\t\treturn data.vid().v;\n\t\t\t});\n\t\t\tif (id != last->id) {\n\t\t\t\treturn &message;\n\t\t\t}\n\t\t}\n\t\treturn static_cast<const MTPMessage*>(nullptr);\n\t});\n\tif (!other) {\n\t\t// Other (non equal to the last one) message not found.\n\t\treturn;\n\t}\n\tconst auto item = owner().addNewMessage(\n\t\t*other,\n\t\tMessageFlags(),\n\t\tNewMessageType::Existing);\n\tif (!item || item->isGroupMigrate()) {\n\t\t// Not better than the last one.\n\t\treturn;\n\t}\n\tsetChatListMessage(item);\n}\n\nvoid History::applyChatListGroup(\n\t\tPeerId dataPeerId,\n\t\tconst MTPmessages_Messages &data) {\n\tif (!isEmpty()\n\t\t|| !_chatListMessage\n\t\t|| !*_chatListMessage\n\t\t|| (*_chatListMessage)->history() != this\n\t\t|| !_lastMessage\n\t\t|| !*_lastMessage\n\t\t|| dataPeerId != peer->id) {\n\t\treturn;\n\t}\n\t// Apply loaded album as a last slice.\n\tconst auto processMessages = [&](const MTPVector<MTPMessage> &messages) {\n\t\tauto items = std::vector<not_null<HistoryItem*>>();\n\t\titems.reserve(messages.v.size());\n\t\tfor (const auto &message : messages.v) {\n\t\t\tconst auto id = IdFromMessage(message);\n\t\t\tif (const auto message = owner().message(dataPeerId, id)) {\n\t\t\t\titems.push_back(message);\n\t\t\t}\n\t\t}\n\t\tif (!ranges::contains(items, not_null(*_lastMessage))\n\t\t\t|| !ranges::contains(items, not_null(*_chatListMessage))) {\n\t\t\treturn;\n\t\t}\n\t\t_loadedAtBottom = true;\n\t\tranges::sort(items, ranges::less{}, &HistoryItem::id);\n\t\taddCreatedOlderSlice(items);\n\t\tcheckLocalMessages();\n\t\tcheckLastMessage();\n\t};\n\tdata.match([&](const MTPDmessages_messagesNotModified &) {\n\t}, [&](const auto &data) {\n\t\tprocessMessages(data.vmessages());\n\t});\n}\n\nHistoryItem *History::lastMessage() const {\n\treturn _lastMessage.value_or(nullptr);\n}\n\nbool History::lastMessageKnown() const {\n\treturn _lastMessage.has_value();\n}\n\nHistoryItem *History::lastServerMessage() const {\n\treturn _lastServerMessage.value_or(nullptr);\n}\n\nbool History::lastServerMessageKnown() const {\n\treturn _lastServerMessage.has_value();\n}\n\nvoid History::updateChatListExistence() {\n\tEntry::updateChatListExistence();\n}\n\nbool History::useTopPromotion() const {\n\tif (!isTopPromoted()) {\n\t\treturn false;\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn !isPinnedDialog(FilterId()) && !channel->amIn();\n\t} else if (const auto user = peer->asUser()) {\n\t\treturn !isPinnedDialog(FilterId()) && user->isBot() && isEmpty();\n\t}\n\treturn false;\n}\n\nint History::fixedOnTopIndex() const {\n\treturn useTopPromotion() ? kTopPromotionFixOnTopIndex : 0;\n}\n\nbool History::trackUnreadMessages() const {\n\tif (const auto channel = peer->asChannel()) {\n\t\treturn channel->amIn();\n\t}\n\treturn true;\n}\n\nbool History::shouldBeInChatList() const {\n\tif (peer->migrateTo() || !folderKnown()) {\n\t\treturn false;\n\t} else if (isPinnedDialog(FilterId())) {\n\t\treturn true;\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tif (!channel->amIn()) {\n\t\t\treturn isTopPromoted();\n\t\t}\n\t} else if (const auto chat = peer->asChat()) {\n\t\treturn chat->amIn()\n\t\t\t|| !lastMessageKnown()\n\t\t\t|| (lastMessage() != nullptr);\n\t} else if (const auto user = peer->asUser()) {\n\t\tif (user->isBot() && isTopPromoted()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn !lastMessageKnown()\n\t\t|| (lastMessage() != nullptr);\n}\n\nvoid History::unknownMessageDeleted(MsgId messageId) {\n\tLOG((\"History::unknownMessageDeleted. Peer ID: %1, Message ID: %2.\")\n\t\t.arg(peer->id.value & PeerId::kChatTypeMask)\n\t\t.arg(messageId.bare));\n\t_unknownDeletedMessages[messageId] = base::unixtime::now();\n\tif (_inboxReadBefore && messageId >= *_inboxReadBefore) {\n\t\towner().histories().requestDialogEntry(this);\n\t}\n}\n\nbool History::isUnknownMessageDeleted(MsgId messageId) const {\n\treturn _unknownDeletedMessages.contains(messageId);\n}\n\nbool History::isServerSideUnread(not_null<const HistoryItem*> item) const {\n\tExpects(item->isRegular());\n\n\treturn item->out()\n\t\t? (!_outboxReadBefore || (item->id >= *_outboxReadBefore))\n\t\t: (!_inboxReadBefore || (item->id >= *_inboxReadBefore));\n}\n\nvoid History::applyDialog(\n\t\tData::Folder *requestFolder,\n\t\tconst MTPDdialog &data) {\n\tconst auto folderId = data.vfolder_id();\n\tconst auto folder = !folderId\n\t\t? requestFolder\n\t\t: folderId->v\n\t\t? owner().folder(folderId->v).get()\n\t\t: nullptr;\n\tapplyDialogFields(\n\t\tfolder,\n\t\tdata.vunread_count().v,\n\t\tdata.vread_inbox_max_id().v,\n\t\tdata.vread_outbox_max_id().v);\n\tapplyDialogTopMessage(data.vtop_message().v);\n\tsetUnreadMark(data.is_unread_mark());\n\tunreadMentions().setCount(data.vunread_mentions_count().v);\n\tunreadReactions().setCount(data.vunread_reactions_count().v);\n\tconst auto pollVotesCount = data.vunread_poll_votes_count().v;\n\tsetUnreadPollVotesCount(pollVotesCount);\n\tunreadPollVotes().setCount(pollVotesCount);\n\tsession().changes().historyUpdated(\n\t\tthis,\n\t\tData::HistoryUpdate::Flag::UnreadPollVotes);\n\tif (const auto channel = peer->asChannel()) {\n\t\tif (const auto pts = data.vpts()) {\n\t\t\tchannel->ptsReceived(pts->v);\n\t\t}\n\t\tif (!channel->amCreator()) {\n\t\t\tconst auto topMessageId = FullMsgId(\n\t\t\t\tchannel->id,\n\t\t\t\tdata.vtop_message().v);\n\t\t\tif (const auto item = owner().message(topMessageId)) {\n\t\t\t\tif (item->date() <= channel->date) {\n\t\t\t\t\tsession().api().chatParticipants().requestSelf(channel);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tchannel->setViewAsMessagesFlag(data.is_view_forum_as_messages());\n\t}\n\towner().notifySettings().apply(\n\t\tMTP_notifyPeer(data.vpeer()),\n\t\tdata.vnotify_settings());\n\n\tconst auto draft = data.vdraft();\n\tif (draft && draft->type() == mtpc_draftMessage) {\n\t\tData::ApplyPeerCloudDraft(\n\t\t\t&session(),\n\t\t\tpeer->id,\n\t\t\tMsgId(), // topicRootId\n\t\t\tPeerId(), // monoforumPeerId\n\t\t\tdraft->c_draftMessage());\n\t}\n\tif (const auto ttl = data.vttl_period()) {\n\t\tpeer->setMessagesTTL(ttl->v);\n\t}\n\towner().histories().dialogEntryApplied(this);\n}\n\nvoid History::dialogEntryApplied() {\n\tif (!lastServerMessageKnown()) {\n\t\tsetLastServerMessage(nullptr);\n\t} else if (!lastMessageKnown()) {\n\t\tsetLastMessage(nullptr);\n\t}\n\tif (peer->migrateTo()) {\n\t\treturn;\n\t} else if (!chatListMessageKnown()) {\n\t\trequestChatListMessage();\n\t\treturn;\n\t}\n\tif (!chatListMessage()) {\n\t\tclear(ClearType::Unload, true);\n\t\taddNewerSlice(QVector<MTPMessage>());\n\t\taddOlderSlice(QVector<MTPMessage>());\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\tconst auto inviter = channel->inviter;\n\t\t\tif (inviter && channel->amIn() && owner().userLoaded(inviter)) {\n\t\t\t\tinsertJoinedMessage();\n\t\t\t}\n\t\t}\n\t\treturn;\n\t}\n\n\tif (chatListTimeId() != 0 && loadedAtBottom()) {\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\tconst auto inviter = channel->inviter;\n\t\t\tif (inviter\n\t\t\t\t&& chatListTimeId() <= channel->inviteDate\n\t\t\t\t&& channel->amIn()\n\t\t\t\t&& owner().userLoaded(inviter)) {\n\t\t\t\tinsertJoinedMessage();\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid History::cacheTopPromotion(\n\t\tbool promoted,\n\t\tconst QString &type,\n\t\tconst QString &message) {\n\tconst auto changed = (isTopPromoted() != promoted);\n\tcacheTopPromoted(promoted);\n\tif (topPromotionType() != type || _topPromotedMessage != message) {\n\t\t_topPromotedType = type;\n\t\t_topPromotedMessage = message;\n\t\tcloudDraftTextCache().clear();\n\t} else if (changed) {\n\t\tcloudDraftTextCache().clear();\n\t}\n}\n\nQStringView History::topPromotionType() const {\n\treturn topPromotionAboutShown()\n\t\t? base::StringViewMid(_topPromotedType, 5)\n\t\t: QStringView(_topPromotedType);\n}\n\nbool History::topPromotionAboutShown() const {\n\treturn _topPromotedType.startsWith(\"seen^\");\n}\n\nvoid History::markTopPromotionAboutShown() {\n\tif (!topPromotionAboutShown()) {\n\t\t_topPromotedType = \"seen^\" + _topPromotedType;\n\t}\n}\n\nQString History::topPromotionMessage() const {\n\treturn _topPromotedMessage;\n}\n\nbool History::clearUnreadOnClientSide() const {\n\tif (!session().supportMode()) {\n\t\treturn false;\n\t}\n\tif (const auto user = peer->asUser()) {\n\t\tif (user->isInaccessible()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool History::skipUnreadUpdate() const {\n\treturn clearUnreadOnClientSide();\n}\n\nvoid History::applyDialogFields(\n\t\tData::Folder *folder,\n\t\tint unreadCount,\n\t\tMsgId maxInboxRead,\n\t\tMsgId maxOutboxRead) {\n\tif (folder) {\n\t\tsetFolder(folder);\n\t} else {\n\t\tclearFolder();\n\t}\n\tif (!skipUnreadUpdate()\n\t\t&& maxInboxRead + 1 >= _inboxReadBefore.value_or(1)) {\n\t\tsetUnreadCount(unreadCount);\n\t\tsetInboxReadTill(maxInboxRead);\n\t}\n\tsetOutboxReadTill(maxOutboxRead);\n}\n\nvoid History::applyDialogTopMessage(MsgId topMessageId) {\n\tif (topMessageId) {\n\t\tconst auto itemId = FullMsgId(peer->id, topMessageId);\n\t\tif (const auto item = owner().message(itemId)) {\n\t\t\tsetLastServerMessage(item);\n\t\t} else {\n\t\t\tsetLastServerMessage(nullptr);\n\t\t}\n\t} else {\n\t\tsetLastServerMessage(nullptr);\n\t}\n\tif (clearUnreadOnClientSide()) {\n\t\tsetUnreadCount(0);\n\t\tif (const auto last = lastMessage()) {\n\t\t\tsetInboxReadTill(last->id);\n\t\t}\n\t}\n}\n\nvoid History::tryMarkForumIntervalRead(\n\t\tMsgId wasInboxReadBefore,\n\t\tMsgId nowInboxReadBefore) {\n\tif (!isForum()\n\t\t|| !peer->useSubsectionTabs()\n\t\t|| (nowInboxReadBefore <= wasInboxReadBefore)) {\n\t\treturn;\n\t} else if (loadedAtBottom() && nowInboxReadBefore >= minMsgId()) {\n\t\t// Count for each sublist how many messages are still not read.\n\t\tauto counts = base::flat_map<not_null<Data::ForumTopic*>, int>();\n\t\tfor (const auto &block : blocks) {\n\t\t\tfor (const auto &message : block->messages) {\n\t\t\t\tconst auto item = message->data();\n\t\t\t\tif (!item->isRegular() || item->id < nowInboxReadBefore) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (const auto topic = item->topic()) {\n\t\t\t\t\t++counts[topic];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (const auto forum = peer->forum()) {\n\t\t\tforum->updateUnreadCounts(nowInboxReadBefore - 1, counts);\n\t\t}\n\t} else if (minMsgId() <= wasInboxReadBefore\n\t\t&& maxMsgId() >= nowInboxReadBefore) {\n\t\t// Count for each sublist how many messages were read.\n\t\tfor (const auto &block : blocks) {\n\t\t\tfor (const auto &message : block->messages) {\n\t\t\t\tconst auto item = message->data();\n\t\t\t\tif (!item->isRegular() || item->id < wasInboxReadBefore) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (item->id >= nowInboxReadBefore) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (const auto topic = item->topic()) {\n\t\t\t\t\tconst auto replies = topic->replies();\n\t\t\t\t\tconst auto unread = replies->unreadCountCurrent();\n\t\t\t\t\tif (unread > 0) {\n\t\t\t\t\t\treplies->setInboxReadTill(item->id, unread - 1);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// We can't invalidate sublist unread counts here, because no read\n\t\t// request was yet sent to the server (so it can't return correct\n\t\t// values yet), we need to do that after we send read request.\n\t\t_flags |= Flag::MonoAndForumUnreadInvalidatePending;\n\t}\n}\n\nvoid History::tryMarkMonoforumIntervalRead(\n\t\tMsgId wasInboxReadBefore,\n\t\tMsgId nowInboxReadBefore) {\n\tif (!amMonoforumAdmin() || (nowInboxReadBefore <= wasInboxReadBefore)) {\n\t\treturn;\n\t} else if (loadedAtBottom() && nowInboxReadBefore >= minMsgId()) {\n\t\t// Count for each sublist how many messages are still not read.\n\t\tauto counts = base::flat_map<not_null<Data::SavedSublist*>, int>();\n\t\tfor (const auto &block : blocks) {\n\t\t\tfor (const auto &message : block->messages) {\n\t\t\t\tconst auto item = message->data();\n\t\t\t\tif (!item->isRegular() || item->id < nowInboxReadBefore) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (const auto sublist = item->savedSublist()) {\n\t\t\t\t\t++counts[sublist];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (const auto monoforum = peer->monoforum()) {\n\t\t\tmonoforum->updateUnreadCounts(nowInboxReadBefore - 1, counts);\n\t\t}\n\t} else if (minMsgId() <= wasInboxReadBefore\n\t\t&& maxMsgId() >= nowInboxReadBefore) {\n\t\t// Count for each sublist how many messages were read.\n\t\tfor (const auto &block : blocks) {\n\t\t\tfor (const auto &message : block->messages) {\n\t\t\t\tconst auto item = message->data();\n\t\t\t\tif (!item->isRegular() || item->id < wasInboxReadBefore) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (item->id >= nowInboxReadBefore) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (const auto sublist = item->savedSublist()) {\n\t\t\t\t\tconst auto unread = sublist->unreadCountCurrent();\n\t\t\t\t\tif (unread > 0) {\n\t\t\t\t\t\tsublist->setInboxReadTill(item->id, unread - 1);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// We can't invalidate sublist unread counts here, because no read\n\t\t// request was yet sent to the server (so it can't return correct\n\t\t// values yet), we need to do that after we send read request.\n\t\t_flags |= Flag::MonoAndForumUnreadInvalidatePending;\n\t}\n}\n\nvoid History::validateMonoAndForumUnread(MsgId readTillId) {\n\tif (!(_flags & Flag::MonoAndForumUnreadInvalidatePending)) {\n\t\treturn;\n\t}\n\t_flags &= ~Flag::MonoAndForumUnreadInvalidatePending;\n\tif (isForum()) {\n\t\tif (const auto forum = peer->forum()) {\n\t\t\tforum->markUnreadCountsUnknown(readTillId);\n\t\t}\n\t} else if (amMonoforumAdmin()) {\n\t\tif (const auto monoforum = peer->monoforum()) {\n\t\t\tmonoforum->markUnreadCountsUnknown(readTillId);\n\t\t}\n\t}\n}\n\nvoid History::setInboxReadTill(MsgId upTo) {\n\tif (_inboxReadBefore) {\n\t\ttryMarkForumIntervalRead(*_inboxReadBefore, upTo + 1);\n\t\ttryMarkMonoforumIntervalRead(*_inboxReadBefore, upTo + 1);\n\t\taccumulate_max(*_inboxReadBefore, upTo + 1);\n\t} else {\n\t\t_inboxReadBefore = upTo + 1;\n\t\tfor (const auto &item : _items) {\n\t\t\titem->applyEffectWatchedOnUnreadKnown();\n\t\t}\n\t}\n}\n\nvoid History::setOutboxReadTill(MsgId upTo) {\n\tif (_outboxReadBefore) {\n\t\taccumulate_max(*_outboxReadBefore, upTo + 1);\n\t} else {\n\t\t_outboxReadBefore = upTo + 1;\n\t}\n}\n\nMsgId History::minMsgId() const {\n\tfor (const auto &block : blocks) {\n\t\tfor (const auto &message : block->messages) {\n\t\t\tconst auto item = message->data();\n\t\t\tif (item->isRegular()) {\n\t\t\t\treturn item->id;\n\t\t\t}\n\t\t}\n\t}\n\treturn 0;\n}\n\nMsgId History::maxMsgId() const {\n\tfor (const auto &block : ranges::views::reverse(blocks)) {\n\t\tfor (const auto &message : ranges::views::reverse(block->messages)) {\n\t\t\tconst auto item = message->data();\n\t\t\tif (item->isRegular()) {\n\t\t\t\treturn item->id;\n\t\t\t}\n\t\t}\n\t}\n\treturn 0;\n}\n\nMsgId History::msgIdForRead() const {\n\tconst auto last = lastMessage();\n\tconst auto result = (last && last->isRegular())\n\t\t? last->id\n\t\t: MsgId(0);\n\treturn loadedAtBottom()\n\t\t? std::max(result, maxMsgId())\n\t\t: result;\n}\n\nHistoryItem *History::lastEditableMessage() const {\n\tif (!loadedAtBottom()) {\n\t\treturn nullptr;\n\t}\n\tconst auto now = base::unixtime::now();\n\tfor (const auto &block : ranges::views::reverse(blocks)) {\n\t\tfor (const auto &message : ranges::views::reverse(block->messages)) {\n\t\t\tconst auto item = message->data();\n\t\t\tif (item->allowsEdit(now)) {\n\t\t\t\treturn owner().groups().findItemToEdit(item);\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid History::resizeToWidth(int newWidth) {\n\tusing Request = HistoryBlock::ResizeRequest;\n\tconst auto request = (_flags & Flag::PendingAllItemsResize)\n\t\t? Request::ReinitAll\n\t\t: (_width != newWidth)\n\t\t? Request::ResizeAll\n\t\t: Request::ResizePending;\n\tif (request == Request::ResizePending && !hasPendingResizedItems()) {\n\t\treturn;\n\t}\n\t_flags &= ~(Flag::HasPendingResizedItems | Flag::PendingAllItemsResize);\n\n\t_width = newWidth;\n\tint y = 0;\n\tfor (const auto &block : blocks) {\n\t\tblock->setY(y);\n\t\ty += block->resizeGetHeight(newWidth, request);\n\t}\n\t_height = y;\n}\n\nvoid History::forceFullResize() {\n\t_width = 0;\n\t_flags |= Flag::HasPendingResizedItems;\n}\n\nData::Thread *History::threadFor(MsgId topicRootId, PeerId monoforumPeerId) {\n\treturn topicRootId\n\t\t? peer->forumTopicFor(topicRootId)\n\t\t: monoforumPeerId\n\t\t? peer->monoforumSublistFor(monoforumPeerId)\n\t\t: static_cast<Data::Thread*>(this);\n}\n\nconst Data::Thread *History::threadFor(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId) const {\n\treturn const_cast<History*>(this)->threadFor(\n\t\ttopicRootId,\n\t\tmonoforumPeerId);\n}\n\nvoid History::forumChanged(Data::Forum *old) {\n\tif (inChatList()) {\n\t\tnotifyUnreadStateChange(old\n\t\t\t? AdjustedForumUnreadState(old->topicsList()->unreadState())\n\t\t\t: computeUnreadState());\n\t}\n\n\tif (const auto forum = peer->forum()) {\n\t\t_flags |= Flag::IsForum;\n\n\t\tforum->topicsList()->unreadStateChanges(\n\t\t) | rpl::filter([=] {\n\t\t\treturn (_flags & Flag::IsForum) && inChatList();\n\t\t}) | rpl::map(\n\t\t\tAdjustedForumUnreadState\n\t\t) | rpl::on_next([=](const Dialogs::UnreadState &old) {\n\t\t\tnotifyUnreadStateChange(old);\n\t\t}, forum->lifetime());\n\n\t\tforum->chatsListChanges(\n\t\t) | rpl::on_next([=] {\n\t\t\tupdateChatListEntry();\n\t\t}, forum->lifetime());\n\t} else {\n\t\t_flags &= ~Flag::IsForum;\n\t}\n\tif (cloudDraft(MsgId(), PeerId())) {\n\t\tupdateChatListSortPosition();\n\t}\n\t_flags |= Flag::PendingAllItemsResize;\n}\n\nbool History::isForum() const {\n\treturn (_flags & Flag::IsForum);\n}\n\nvoid History::monoforumChanged(Data::SavedMessages *old) {\n\tif (inChatList()) {\n\t\tnotifyUnreadStateChange(old\n\t\t\t? AdjustedForumUnreadState(\n\t\t\t\twithMyMuted(old->chatsList()->unreadState()))\n\t\t\t: computeUnreadState());\n\t}\n\n\tif (const auto monoforum = peer->monoforum()) {\n\t\t_flags |= Flag::IsMonoforumAdmin;\n\n\t\tmonoforum->chatsList()->unreadStateChanges(\n\t\t) | rpl::filter([=] {\n\t\t\treturn (_flags & Flag::IsMonoforumAdmin) && inChatList();\n\t\t}) | rpl::map([=](const Dialogs::UnreadState &was) {\n\t\t\treturn AdjustedForumUnreadState(withMyMuted(was));\n\t\t}) | rpl::on_next([=](const Dialogs::UnreadState &old) {\n\t\t\tnotifyUnreadStateChange(old);\n\t\t}, monoforum->lifetime());\n\n\t\tmonoforum->chatsListChanges(\n\t\t) | rpl::on_next([=] {\n\t\t\tupdateChatListEntry();\n\t\t}, monoforum->lifetime());\n\t} else {\n\t\t_flags &= ~Flag::IsMonoforumAdmin;\n\t}\n\tif (cloudDraft(MsgId(), PeerId())) {\n\t\tupdateChatListSortPosition();\n\t}\n\t_flags |= Flag::PendingAllItemsResize;\n}\n\nbool History::amMonoforumAdmin() const {\n\treturn (_flags & Flag::IsMonoforumAdmin);\n}\n\nbool History::suggestDraftAllowed() const {\n\treturn peer->isMonoforum() && !peer->amMonoforumAdmin();\n}\n\nbool History::hasForumThreadBars() const {\n\tif (amMonoforumAdmin()) {\n\t\treturn true;\n\t}\n\treturn peer->useSubsectionTabs();\n}\n\nvoid History::forumTabsChanged(bool forumTabs) {\n\tfor (auto &block : blocks) {\n\t\tfor (auto &view : block->messages) {\n\t\t\tview->setPendingResize();\n\t\t\tif (forumTabs || view->Has<HistoryView::ForumThreadBar>()) {\n\t\t\t\tview->previousInBlocksChanged();\n\t\t\t}\n\t\t}\n\t}\n}\n\nnot_null<History*> History::migrateToOrMe() const {\n\tif (const auto to = peer->migrateTo()) {\n\t\treturn owner().history(to);\n\t}\n\t// We could get it by owner().history(peer), but we optimize.\n\treturn const_cast<History*>(this);\n}\n\nHistory *History::migrateFrom() const {\n\tif (const auto from = peer->migrateFrom()) {\n\t\treturn owner().history(from);\n\t}\n\treturn nullptr;\n}\n\nMsgRange History::rangeForDifferenceRequest() const {\n\tauto fromId = MsgId(0);\n\tauto toId = MsgId(0);\n\tfor (const auto &block : blocks) {\n\t\tfor (const auto &item : block->messages) {\n\t\t\tconst auto id = item->data()->id;\n\t\t\tif (id > 0) {\n\t\t\t\tfromId = id;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (fromId) break;\n\t}\n\tif (fromId) {\n\t\tfor (auto blockIndex = blocks.size(); blockIndex > 0;) {\n\t\t\tconst auto &block = blocks[--blockIndex];\n\t\t\tfor (auto itemIndex = block->messages.size(); itemIndex > 0;) {\n\t\t\t\tconst auto id = block->messages[--itemIndex]->data()->id;\n\t\t\t\tif (id > 0) {\n\t\t\t\t\ttoId = id;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (toId) break;\n\t\t}\n\t\treturn { fromId, toId + 1 };\n\t}\n\treturn MsgRange();\n}\n\nData::HistoryMessages &History::messages() {\n\tif (!_messages) {\n\t\t_messages = std::make_unique<Data::HistoryMessages>();\n\n\t\tconst auto max = maxMsgId();\n\t\tconst auto from = loadedAtTop() ? 0 : minMsgId();\n\t\tconst auto till = loadedAtBottom() ? ServerMaxMsgId : max;\n\t\tauto list = std::vector<MsgId>();\n\t\tlist.reserve(std::min(\n\t\t\tint(_items.size()),\n\t\t\tint(blocks.size()) * kNewBlockEachMessage));\n\t\tauto sort = false;\n\t\tfor (const auto &block : blocks) {\n\t\t\tfor (const auto &view : block->messages) {\n\t\t\t\tconst auto item = view->data();\n\t\t\t\tif (item->isRegular()) {\n\t\t\t\t\tconst auto id = item->id;\n\t\t\t\t\tif (!list.empty() && list.back() >= id) {\n\t\t\t\t\t\tsort = true;\n\t\t\t\t\t}\n\t\t\t\t\tlist.push_back(id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (sort) {\n\t\t\tranges::sort(list);\n\t\t}\n\t\tif (max || (loadedAtTop() && loadedAtBottom())) {\n\t\t\t_messages->addSlice(std::move(list), { from, till }, {});\n\t\t}\n\t}\n\treturn *_messages;\n}\n\nconst Data::HistoryMessages &History::messages() const {\n\treturn const_cast<History*>(this)->messages();\n}\n\nData::HistoryMessages *History::maybeMessages() {\n\treturn _messages.get();\n}\n\nHistoryItem *History::insertJoinedMessage() {\n\tconst auto channel = peer->asChannel();\n\tif (!channel\n\t\t|| channel->isMonoforum()\n\t\t|| _joinedMessage\n\t\t|| !channel->amIn()\n\t\t|| (channel->isMegagroup()\n\t\t\t&& channel->mgInfo->joinedMessageFound)) {\n\t\treturn _joinedMessage;\n\t}\n\n\tconst auto inviter = (channel->inviter.bare > 0)\n\t\t? owner().userLoaded(channel->inviter)\n\t\t: nullptr;\n\tif (!inviter) {\n\t\treturn nullptr;\n\t}\n\n\tif (peer->isMegagroup()\n\t\t&& peer->migrateFrom()\n\t\t&& !blocks.empty()\n\t\t&& blocks.front()->messages.front()->data()->id == 1) {\n\t\tchannel->mgInfo->joinedMessageFound = true;\n\t\treturn nullptr;\n\t}\n\n\t_joinedMessage = GenerateJoinedMessage(\n\t\tthis,\n\t\tchannel->inviteDate,\n\t\tinviter,\n\t\tchannel->inviteViaRequest);\n\tinsertMessageToBlocks(_joinedMessage);\n\treturn _joinedMessage;\n}\n\nvoid History::checkNewPeerMessages() {\n\tif (!loadedAtTop()) {\n\t\treturn;\n\t}\n\tconst auto user = peer->asUser();\n\tif (!user) {\n\t\treturn;\n\t}\n\tconst auto photo = user->photoChangeDate();\n\tconst auto name = user->nameChangeDate();\n\tif (!photo && _newPeerPhotoChange) {\n\t\t_newPeerPhotoChange->destroy();\n\t}\n\tif (!name && _newPeerNameChange) {\n\t\t_newPeerNameChange->destroy();\n\t}\n\tif ((!photo || _newPeerPhotoChange) && (!name || _newPeerNameChange)) {\n\t\treturn;\n\t}\n\n\tconst auto when = [](TimeId date) {\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto passed = now - date;\n\t\tif (passed < 3600) {\n\t\t\treturn tr::lng_new_contact_updated_now(tr::now);\n\t\t} else if (passed < 24 * 3600) {\n\t\t\treturn tr::lng_new_contact_updated_hours(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\t(passed / 3600));\n\t\t} else if (passed < 60 * 24 * 3600) {\n\t\t\treturn tr::lng_new_contact_updated_days(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\t(passed / (24 * 3600)));\n\t\t}\n\t\treturn tr::lng_new_contact_updated_months(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\t(passed / (30 * 24 * 3600)));\n\t};\n\n\tauto firstDate = TimeId();\n\tfor (const auto &block : blocks) {\n\t\tfor (const auto &message : block->messages) {\n\t\t\tconst auto item = message->data();\n\t\t\tif (item != _newPeerPhotoChange && item != _newPeerNameChange) {\n\t\t\t\tfirstDate = item->date();\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (firstDate) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (!firstDate) {\n\t\tfirstDate = base::unixtime::serialize(\n\t\t\tQDateTime(QDate(2013, 8, 1), QTime(0, 0)));\n\t}\n\tconst auto add = [&](tr::phrase<lngtag_when> phrase, TimeId date) {\n\t\tconst auto result = makeMessage({\n\t\t\t.id = owner().nextLocalMessageId(),\n\t\t\t.flags = MessageFlag::Local | MessageFlag::HideDisplayDate,\n\t\t\t.date = (--firstDate),\n\t\t}, PreparedServiceText{ TextWithEntities{\n\t\t\tphrase(tr::now, lt_when, when(date)),\n\t\t} });\n\t\tinsertMessageToBlocks(result);\n\t\treturn result;\n\t};\n\n\tif (photo && !_newPeerPhotoChange) {\n\t\t_newPeerPhotoChange = add(tr::lng_new_contact_updated_photo, photo);\n\t}\n\tif (name && !_newPeerNameChange) {\n\t\t_newPeerNameChange = add(tr::lng_new_contact_updated_name, name);\n\t}\n}\n\nvoid History::insertMessageToBlocks(not_null<HistoryItem*> item) {\n\tExpects(item->mainView() == nullptr);\n\n\tif (isEmpty()) {\n\t\taddNewToBack(item, false);\n\t\treturn;\n\t}\n\n\tconst auto itemDate = item->date();\n\tfor (auto blockIndex = blocks.size(); blockIndex > 0;) {\n\t\tconst auto &block = blocks[--blockIndex];\n\t\tfor (auto itemIndex = block->messages.size(); itemIndex > 0;) {\n\t\t\tif (block->messages[--itemIndex]->data()->date() <= itemDate) {\n\t\t\t\t++itemIndex;\n\t\t\t\taddNewInTheMiddle(item, blockIndex, itemIndex);\n\t\t\t\tconst auto lastDate = chatListTimeId();\n\t\t\t\tif (!lastDate || itemDate >= lastDate) {\n\t\t\t\t\tsetLastMessage(item);\n\t\t\t\t\towner().notifyHistoryChangeDelayed(this);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tstartBuildingFrontBlock();\n\taddItemToBlock(item);\n\tfinishBuildingFrontBlock();\n}\n\nvoid History::checkLocalMessages() {\n\tif (isEmpty() && (!loadedAtTop() || !loadedAtBottom())) {\n\t\treturn;\n\t}\n\tconst auto firstDate = loadedAtTop()\n\t\t? 0\n\t\t: blocks.front()->messages.front()->data()->date();\n\tconst auto lastDate = loadedAtBottom()\n\t\t? std::numeric_limits<TimeId>::max()\n\t\t: blocks.back()->messages.back()->data()->date();\n\tconst auto goodDate = [&](TimeId date) {\n\t\treturn (date >= firstDate && date < lastDate);\n\t};\n\tfor (const auto &item : _clientSideMessages) {\n\t\tif (!item->mainView() && goodDate(item->date())) {\n\t\t\tinsertMessageToBlocks(item);\n\t\t}\n\t}\n\tif (peer->isChannel()\n\t\t&& !_joinedMessage\n\t\t&& peer->asChannel()->inviter\n\t\t&& goodDate(peer->asChannel()->inviteDate)) {\n\t\tinsertJoinedMessage();\n\t} else {\n\t\tcheckNewPeerMessages();\n\t}\n}\n\nHistoryStreamedDrafts &History::streamedDrafts() {\n\tif (!_streamedDrafts) {\n\t\t_streamedDrafts = std::make_unique<HistoryStreamedDrafts>(this);\n\t}\n\treturn *_streamedDrafts;\n}\n\nHistoryStreamedDrafts *History::streamedDraftsIfExists() const {\n\treturn _streamedDrafts.get();\n}\n\nHistoryItem *History::joinedMessageInstance() const {\n\treturn _joinedMessage;\n}\n\nvoid History::removeJoinedMessage() {\n\tif (_joinedMessage) {\n\t\t_joinedMessage->destroy();\n\t}\n}\n\nvoid History::removeNewPeerMessages() {\n\tif (_newPeerNameChange) {\n\t\t_newPeerNameChange->destroy();\n\t}\n\tif (_newPeerPhotoChange) {\n\t\t_newPeerPhotoChange->destroy();\n\t}\n}\n\nvoid History::reactionsEnabledChanged(bool enabled) {\n\tif (!enabled) {\n\t\tfor (const auto &item : _items) {\n\t\t\titem->updateReactions(nullptr);\n\t\t}\n\t} else {\n\t\tfor (const auto &item : _items) {\n\t\t\titem->updateReactionsUnknown();\n\t\t}\n\t}\n}\n\nbool History::isEmpty() const {\n\treturn blocks.empty();\n}\n\nbool History::isDisplayedEmpty() const {\n\tif (!loadedAtTop() || !loadedAtBottom()) {\n\t\treturn false;\n\t}\n\tconst auto first = findFirstNonEmpty();\n\tif (!first) {\n\t\treturn true;\n\t}\n\tconst auto chat = peer->asChat();\n\tif (!chat || !chat->amCreator()) {\n\t\treturn false;\n\t}\n\n\t// For legacy chats we want to show the chat with only\n\t// messages about you creating the group and maybe about you\n\t// changing the group photo as an empty chat with\n\t// a nice information about the group features.\n\tif (nonEmptyCountMoreThan(2)) {\n\t\treturn false;\n\t}\n\tconst auto isChangePhoto = [](not_null<HistoryItem*> item) {\n\t\tif (const auto media = item->media()) {\n\t\t\treturn (media->photo() != nullptr) && item->isService();\n\t\t}\n\t\treturn false;\n\t};\n\tconst auto last = findLastNonEmpty();\n\tif (first == last) {\n\t\treturn first->data()->isGroupEssential()\n\t\t\t|| isChangePhoto(first->data());\n\t}\n\treturn first->data()->isGroupEssential() && isChangePhoto(last->data());\n}\n\nauto History::findFirstNonEmpty() const -> Element* {\n\tfor (const auto &block : blocks) {\n\t\tfor (const auto &element : block->messages) {\n\t\t\tif (!element->data()->isEmpty()) {\n\t\t\t\treturn element.get();\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nauto History::findFirstDisplayed() const -> Element* {\n\tfor (const auto &block : blocks) {\n\t\tfor (const auto &element : block->messages) {\n\t\t\tif (!element->data()->isEmpty() && !element->isHidden()) {\n\t\t\t\treturn element.get();\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nauto History::findLastNonEmpty() const -> Element* {\n\tfor (const auto &block : ranges::views::reverse(blocks)) {\n\t\tfor (const auto &element : ranges::views::reverse(block->messages)) {\n\t\t\tif (!element->data()->isEmpty()) {\n\t\t\t\treturn element.get();\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nauto History::findLastDisplayed() const -> Element* {\n\tfor (const auto &block : ranges::views::reverse(blocks)) {\n\t\tfor (const auto &element : ranges::views::reverse(block->messages)) {\n\t\t\tif (!element->data()->isEmpty() && !element->isHidden()) {\n\t\t\t\treturn element.get();\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nbool History::nonEmptyCountMoreThan(int count) const {\n\tExpects(count >= 0);\n\n\tfor (const auto &block : blocks) {\n\t\tfor (const auto &element : block->messages) {\n\t\t\tif (!element->data()->isEmpty()) {\n\t\t\t\tif (!count--) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nbool History::hasOrphanMediaGroupPart() const {\n\tif (loadedAtTop() || !loadedAtBottom()) {\n\t\treturn false;\n\t} else if (blocks.size() != 1) {\n\t\treturn false;\n\t} else if (blocks.front()->messages.size() != 1) {\n\t\treturn false;\n\t}\n\tconst auto last = blocks.front()->messages.front()->data();\n\treturn last->groupId() != MessageGroupId();\n}\n\nstd::vector<MsgId> History::collectMessagesFromParticipantToDelete(\n\t\tnot_null<PeerData*> participant) const {\n\tauto result = std::vector<MsgId>();\n\tfor (const auto &block : blocks) {\n\t\tfor (const auto &message : block->messages) {\n\t\t\tconst auto item = message->data();\n\t\t\tif (item->from() == participant && item->canDelete()) {\n\t\t\t\tresult.push_back(item->id);\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid History::clear(ClearType type, bool markEmpty) {\n\t_unreadBarView = nullptr;\n\t_firstUnreadView = nullptr;\n\tremoveJoinedMessage();\n\tbase::take(_streamedDrafts);\n\n\tforgetScrollState();\n\tblocks.clear();\n\towner().notifyHistoryUnloaded(this);\n\tlastKeyboardInited = false;\n\tif (type == ClearType::Unload) {\n\t\t_loadedAtTop = _loadedAtBottom = markEmpty;\n\t} else {\n\t\t// Leave the 'sending' messages in local messages.\n\t\tauto local = base::flat_set<not_null<HistoryItem*>>();\n\t\tfor (const auto &item : _clientSideMessages) {\n\t\t\tif (!item->isSending()) {\n\t\t\t\tlocal.emplace(item);\n\t\t\t}\n\t\t}\n\t\tfor (const auto &item : local) {\n\t\t\titem->destroy();\n\t\t}\n\t\tclearNotifications();\n\t\towner().notifyHistoryCleared(this);\n\t\tif (unreadCountKnown()) {\n\t\t\tsetUnreadCount(0);\n\t\t}\n\t\tif (type == ClearType::DeleteChat) {\n\t\t\tsetLastServerMessage(nullptr);\n\t\t} else if (_lastMessage && *_lastMessage) {\n\t\t\tif ((*_lastMessage)->isRegular()) {\n\t\t\t\t(*_lastMessage)->applyEditionToHistoryCleared();\n\t\t\t} else {\n\t\t\t\t_lastMessage = std::nullopt;\n\t\t\t}\n\t\t}\n\t\tconst auto tillId = (_lastMessage\n\t\t\t&& (*_lastMessage)\n\t\t\t&& (*_lastMessage)->isRegular())\n\t\t\t? (*_lastMessage)->id\n\t\t\t: MsgId(std::numeric_limits<int64>::max());\n\t\tclearUpTill(tillId);\n\t\tif (blocks.empty() && _lastMessage && *_lastMessage) {\n\t\t\taddItemToBlock(*_lastMessage);\n\t\t}\n\t\t_loadedAtTop = _loadedAtBottom = _lastMessage.has_value();\n\t\tclearSharedMedia();\n\t\tif (const auto messages = _messages.get()) {\n\t\t\tmessages->removeAll();\n\t\t}\n\t\tclearLastKeyboard();\n\t}\n\n\tif (const auto chat = peer->asChat()) {\n\t\tchat->lastAuthors.clear();\n\t\tchat->markupSenders.clear();\n\t} else if (const auto channel = peer->asMegagroup()) {\n\t\tchannel->mgInfo->markupSenders.clear();\n\t}\n\n\towner().notifyHistoryChangeDelayed(this);\n\towner().sendHistoryChangeNotifications();\n}\n\nvoid History::clearUpTill(MsgId availableMinId) {\n\tauto remove = std::vector<not_null<HistoryItem*>>();\n\tremove.reserve(_items.size());\n\tfor (const auto &item : _items) {\n\t\tconst auto itemId = item->id;\n\t\tif (!item->isRegular()) {\n\t\t\tcontinue;\n\t\t} else if (itemId == availableMinId) {\n\t\t\titem->applyEditionToHistoryCleared();\n\t\t} else if (itemId < availableMinId) {\n\t\t\tremove.push_back(item.get());\n\t\t}\n\t}\n\tfor (const auto &item : remove) {\n\t\titem->destroy();\n\t}\n\trequestChatListMessage();\n}\n\nvoid History::applyGroupAdminChanges(const base::flat_set<UserId> &changes) {\n\tfor (const auto &block : blocks) {\n\t\tfor (const auto &message : block->messages) {\n\t\t\tmessage->applyGroupAdminChanges(changes);\n\t\t}\n\t}\n}\n\nvoid History::changedChatListPinHook() {\n\tsession().changes().historyUpdated(this, UpdateFlag::IsPinned);\n}\n\nvoid History::removeBlock(not_null<HistoryBlock*> block) {\n\tExpects(block->messages.empty());\n\n\tif (_buildingFrontBlock && block == _buildingFrontBlock->block) {\n\t\t_buildingFrontBlock->block = nullptr;\n\t}\n\n\tint index = block->indexInHistory();\n\tblocks.erase(blocks.begin() + index);\n\tif (index < blocks.size()) {\n\t\tfor (int i = index, l = blocks.size(); i < l; ++i) {\n\t\t\tblocks[i]->setIndexInHistory(i);\n\t\t}\n\t\tblocks[index]->messages.front()->previousInBlocksChanged();\n\t} else if (!blocks.empty() && !blocks.back()->messages.empty()) {\n\t\tblocks.back()->messages.back()->nextInBlocksRemoved();\n\t}\n}\n\nvoid History::cacheTopPromoted(bool promoted) {\n\tif (isTopPromoted() == promoted) {\n\t\treturn;\n\t} else if (promoted) {\n\t\t_flags |= Flag::IsTopPromoted;\n\t} else {\n\t\t_flags &= ~Flag::IsTopPromoted;\n\t}\n\tupdateChatListSortPosition();\n\tupdateChatListEntry();\n\tif (!isTopPromoted()) {\n\t\tupdateChatListExistence();\n\t}\n}\n\nbool History::isTopPromoted() const {\n\treturn (_flags & Flag::IsTopPromoted);\n}\n\nvoid History::translateOfferFrom(LanguageId id) {\n\tif (!id) {\n\t\tif (translatedTo()) {\n\t\t\t_translation->offerFrom(id);\n\t\t} else if (_translation) {\n\t\t\t_translation = nullptr;\n\t\t\tsession().changes().historyUpdated(\n\t\t\t\tthis,\n\t\t\t\tUpdateFlag::TranslateFrom);\n\t\t}\n\t} else if (!_translation) {\n\t\t_translation = std::make_unique<HistoryTranslation>(this, id);\n\t\tusing Flag = PeerData::TranslationFlag;\n\t\tif (peer->autoTranslation()\n\t\t\t&& (peer->translationFlag() == Flag::Enabled)) {\n\t\t\ttranslateTo(Core::App().settings().translateTo());\n\t\t}\n\t} else {\n\t\t_translation->offerFrom(id);\n\t}\n}\n\nLanguageId History::translateOfferedFrom() const {\n\treturn _translation ? _translation->offeredFrom() : LanguageId();\n}\n\nvoid History::translateTo(LanguageId id) {\n\tif (!_translation) {\n\t\treturn;\n\t} else if (!id && !translateOfferedFrom()) {\n\t\t_translation = nullptr;\n\t\tsession().changes().historyUpdated(this, UpdateFlag::TranslatedTo);\n\t} else {\n\t\t_translation->translateTo(id);\n\t}\n}\n\nLanguageId History::translatedTo() const {\n\treturn _translation ? _translation->translatedTo() : LanguageId();\n}\n\nHistoryTranslation *History::translation() const {\n\treturn _translation.get();\n}\n\nvoid History::refreshHiddenLinksItems() {\n\tauto refresh = base::flat_set<FullMsgId>();\n\tfor (const auto &item : _items) {\n\t\tif (item->hasHiddenLinks()) {\n\t\t\trefresh.emplace(item->fullId());\n\t\t}\n\t}\n\tconst auto owner = &this->owner();\n\tfor (const auto &id : refresh) {\n\t\tif (const auto item = owner->message(id)) {\n\t\t\titem->setHasHiddenLinks(false);\n\t\t\towner->requestItemViewRefresh(item);\n\t\t}\n\t}\n}\n\nHistoryBlock::HistoryBlock(not_null<History*> history)\n: _history(history) {\n}\n\nint HistoryBlock::resizeGetHeight(int newWidth, ResizeRequest request) {\n\tauto y = 0;\n\tif (request == ResizeRequest::ReinitAll) {\n\t\tfor (const auto &message : messages) {\n\t\t\tmessage->setY(y);\n\t\t\tmessage->initDimensions();\n\t\t\ty += message->resizeGetHeight(newWidth);\n\t\t}\n\t} else if (request == ResizeRequest::ResizeAll) {\n\t\tfor (const auto &message : messages) {\n\t\t\tmessage->setY(y);\n\t\t\ty += message->resizeGetHeight(newWidth);\n\t\t}\n\t} else {\n\t\tfor (const auto &message : messages) {\n\t\t\tmessage->setY(y);\n\t\t\ty += message->pendingResize()\n\t\t\t\t? message->resizeGetHeight(newWidth)\n\t\t\t\t: message->height();\n\t\t}\n\t}\n\t_height = y;\n\treturn _height;\n}\n\nvoid HistoryBlock::remove(not_null<Element*> view) {\n\tExpects(view->block() == this);\n\n\t_history->owner().notifyViewAboutToBeRemoved(view);\n\t_history->mainViewRemoved(this, view);\n\n\tconst auto blockIndex = indexInHistory();\n\tconst auto itemIndex = view->indexInBlock();\n\tconst auto item = view->data();\n\titem->clearMainView();\n\tmessages.erase(messages.begin() + itemIndex);\n\tfor (auto i = itemIndex, l = int(messages.size()); i < l; ++i) {\n\t\tmessages[i]->setIndexInBlock(i);\n\t}\n\tif (messages.empty()) {\n\t\t// Deletes this.\n\t\t_history->removeBlock(this);\n\t} else if (itemIndex < messages.size()) {\n\t\tmessages[itemIndex]->previousInBlocksChanged();\n\t} else if (blockIndex + 1 < _history->blocks.size()) {\n\t\t_history->blocks[blockIndex + 1]->messages.front()->previousInBlocksChanged();\n\t} else if (!_history->blocks.empty() && !_history->blocks.back()->messages.empty()) {\n\t\t_history->blocks.back()->messages.back()->nextInBlocksRemoved();\n\t}\n}\n\nvoid HistoryBlock::refreshView(not_null<Element*> view) {\n\tExpects(view->block() == this);\n\n\tconst auto item = view->data();\n\tauto refreshed = item->createView(\n\t\t_history->delegateMixin()->delegate(),\n\t\tview);\n\n\tauto blockIndex = indexInHistory();\n\tauto itemIndex = view->indexInBlock();\n\t_history->viewReplaced(view, refreshed.get());\n\n\tmessages[itemIndex] = std::move(refreshed);\n\tmessages[itemIndex]->attachToBlock(this, itemIndex);\n\tif (itemIndex + 1 < messages.size()) {\n\t\tmessages[itemIndex + 1]->previousInBlocksChanged();\n\t} else if (blockIndex + 1 < _history->blocks.size()) {\n\t\t_history->blocks[blockIndex + 1]->messages.front()->previousInBlocksChanged();\n\t} else if (!_history->blocks.empty() && !_history->blocks.back()->messages.empty()) {\n\t\t_history->blocks.back()->messages.back()->nextInBlocksRemoved();\n\t}\n}\n\nHistoryBlock::~HistoryBlock() = default;\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_types.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_drafts.h\"\n#include \"data/data_thread.h\"\n#include \"history/view/history_view_send_action.h\"\n#include \"base/variant.h\"\n#include \"base/flat_set.h\"\n#include \"base/flags.h\"\n\nclass History;\nclass HistoryBlock;\nclass HistoryTranslation;\nclass HistoryItem;\nstruct HistoryItemCommonFields;\nstruct HistoryMessageMarkupData;\nclass HistoryMainElementDelegateMixin;\nclass HistoryStreamedDrafts;\nstruct LanguageId;\n\nnamespace Data {\nstruct Draft;\nclass Forum;\nclass Session;\nclass Folder;\nclass ChatFilter;\nstruct SponsoredFrom;\nclass SponsoredMessages;\nclass HistoryMessages;\nclass SavedMessages;\n} // namespace Data\n\nnamespace Dialogs {\nclass Row;\nclass IndexedList;\n} // namespace Dialogs\n\nnamespace HistoryView {\nclass Element;\n} // namespace HistoryView\n\nenum class NewMessageType {\n\tUnread,\n\tLast,\n\tExisting,\n};\n\nclass History final : public Data::Thread {\npublic:\n\tusing Element = HistoryView::Element;\n\n\tHistory(not_null<Data::Session*> owner, PeerId peerId);\n\t~History();\n\n\t[[nodiscard]] not_null<History*> owningHistory() override {\n\t\treturn this;\n\t}\n\t[[nodiscard]] Data::Thread *threadFor(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId);\n\t[[nodiscard]] const Data::Thread *threadFor(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId) const;\n\n\t[[nodiscard]] auto delegateMixin() const\n\t\t\t-> not_null<HistoryMainElementDelegateMixin*> {\n\t\treturn _delegateMixin.get();\n\t}\n\n\tvoid forumChanged(Data::Forum *old);\n\t[[nodiscard]] bool isForum() const;\n\n\tvoid monoforumChanged(Data::SavedMessages *old);\n\t[[nodiscard]] bool amMonoforumAdmin() const;\n\t[[nodiscard]] bool suggestDraftAllowed() const;\n\t[[nodiscard]] bool hasForumThreadBars() const;\n\tvoid forumTabsChanged(bool forumTabs);\n\n\t[[nodiscard]] not_null<History*> migrateToOrMe() const;\n\t[[nodiscard]] History *migrateFrom() const;\n\t[[nodiscard]] MsgRange rangeForDifferenceRequest() const;\n\n\t[[nodiscard]] Data::HistoryMessages &messages();\n\t[[nodiscard]] const Data::HistoryMessages &messages() const;\n\t[[nodiscard]] Data::HistoryMessages *maybeMessages();\n\n\t[[nodiscard]] HistoryStreamedDrafts &streamedDrafts();\n\t[[nodiscard]] HistoryStreamedDrafts *streamedDraftsIfExists() const;\n\n\t[[nodiscard]] HistoryItem *joinedMessageInstance() const;\n\tvoid checkLocalMessages();\n\tvoid removeJoinedMessage();\n\tvoid removeNewPeerMessages();\n\n\tvoid reactionsEnabledChanged(bool enabled);\n\n\t[[nodiscard]] bool isEmpty() const;\n\t[[nodiscard]] bool isDisplayedEmpty() const;\n\t[[nodiscard]] Element *findFirstNonEmpty() const;\n\t[[nodiscard]] Element *findFirstDisplayed() const;\n\t[[nodiscard]] Element *findLastNonEmpty() const;\n\t[[nodiscard]] Element *findLastDisplayed() const;\n\t[[nodiscard]] bool hasOrphanMediaGroupPart() const;\n\t[[nodiscard]] std::vector<MsgId> collectMessagesFromParticipantToDelete(\n\t\tnot_null<PeerData*> participant) const;\n\n\tenum class ClearType {\n\t\tUnload,\n\t\tDeleteChat,\n\t\tClearHistory,\n\t};\n\tvoid clear(ClearType type, bool markEmpty = false);\n\tvoid clearUpTill(MsgId availableMinId);\n\n\tvoid applyGroupAdminChanges(const base::flat_set<UserId> &changes);\n\n\ttemplate <typename ...Args>\n\tnot_null<HistoryItem*> makeMessage(MsgId id, Args &&...args) {\n\t\treturn static_cast<HistoryItem*>(\n\t\t\tinsertItem(\n\t\t\t\tstd::make_unique<HistoryItem>(\n\t\t\t\t\tthis,\n\t\t\t\t\tid,\n\t\t\t\t\tstd::forward<Args>(args)...)).get());\n\t}\n\ttemplate <typename ...Args>\n\tnot_null<HistoryItem*> makeMessage(\n\t\t\tHistoryItemCommonFields &&fields,\n\t\t\tArgs &&...args) {\n\t\treturn static_cast<HistoryItem*>(\n\t\t\tinsertItem(\n\t\t\t\tstd::make_unique<HistoryItem>(\n\t\t\t\t\tthis,\n\t\t\t\t\tstd::move(fields),\n\t\t\t\t\tstd::forward<Args>(args)...)).get());\n\t}\n\n\tvoid destroyMessage(not_null<HistoryItem*> item);\n\tvoid destroyMessagesByDates(TimeId minDate, TimeId maxDate);\n\tvoid destroyMessagesByTopic(MsgId topicRootId);\n\tvoid destroyMessagesBySublist(not_null<PeerData*> sublistPeer);\n\n\tvoid unpinMessagesFor(MsgId topicRootId, PeerId monoforumPeerId);\n\n\tnot_null<HistoryItem*> addNewMessage(\n\t\tMsgId id,\n\t\tconst MTPMessage &message,\n\t\tMessageFlags localFlags,\n\t\tNewMessageType type);\n\n\tnot_null<HistoryItem*> addNewLocalMessage(\n\t\tHistoryItemCommonFields &&fields,\n\t\tconst TextWithEntities &text,\n\t\tconst MTPMessageMedia &media);\n\tnot_null<HistoryItem*> addNewLocalMessage(\n\t\tHistoryItemCommonFields &&fields,\n\t\tnot_null<HistoryItem*> forwardOriginal);\n\tnot_null<HistoryItem*> addNewLocalMessage(\n\t\tHistoryItemCommonFields &&fields,\n\t\tnot_null<DocumentData*> document,\n\t\tconst TextWithEntities &caption);\n\tnot_null<HistoryItem*> addNewLocalMessage(\n\t\tHistoryItemCommonFields &&fields,\n\t\tnot_null<PhotoData*> photo,\n\t\tconst TextWithEntities &caption);\n\tnot_null<HistoryItem*> addNewLocalMessage(\n\t\tHistoryItemCommonFields &&fields,\n\t\tnot_null<GameData*> game);\n\tnot_null<HistoryItem*> addNewLocalMessage(not_null<HistoryItem*> item);\n\n\tnot_null<HistoryItem*> addSponsoredMessage(\n\t\tMsgId id,\n\t\tData::SponsoredFrom from,\n\t\tconst TextWithEntities &textWithEntities); // sponsored\n\n\t// Used only internally and for channel admin log.\n\tnot_null<HistoryItem*> createItem(\n\t\tMsgId id,\n\t\tconst MTPMessage &message,\n\t\tMessageFlags localFlags,\n\t\tbool detachExistingItem = false,\n\t\tbool newMessage = false);\n\tstd::vector<not_null<HistoryItem*>> createItems(\n\t\tconst QVector<MTPMessage> &data);\n\n\tvoid addOlderSlice(const QVector<MTPMessage> &slice);\n\tvoid addNewerSlice(const QVector<MTPMessage> &slice);\n\n\tvoid newItemAdded(not_null<HistoryItem*> item);\n\n\tvoid registerClientSideMessage(not_null<HistoryItem*> item);\n\tvoid unregisterClientSideMessage(not_null<HistoryItem*> item);\n\t[[nodiscard]] auto clientSideMessages()\n\t\t-> const base::flat_set<not_null<HistoryItem*>> &;\n\t[[nodiscard]] HistoryItem *latestSendingMessage() const;\n\n\t[[nodiscard]] bool readInboxTillNeedsRequest(MsgId tillId);\n\tvoid applyInboxReadUpdate(\n\t\tFolderId folderId,\n\t\tMsgId upTo,\n\t\tint stillUnread,\n\t\tint32 channelPts = 0);\n\tvoid inboxRead(MsgId upTo, std::optional<int> stillUnread = {});\n\tvoid inboxRead(not_null<const HistoryItem*> wasRead);\n\tvoid outboxRead(MsgId upTo);\n\tvoid outboxRead(not_null<const HistoryItem*> wasRead);\n\t[[nodiscard]] MsgId loadAroundId() const;\n\t[[nodiscard]] bool inboxReadTillKnown() const;\n\t[[nodiscard]] MsgId inboxReadTillId() const;\n\t[[nodiscard]] MsgId outboxReadTillId() const;\n\n\t[[nodiscard]] bool isServerSideUnread(\n\t\tnot_null<const HistoryItem*> item) const override;\n\n\t[[nodiscard]] bool trackUnreadMessages() const;\n\t[[nodiscard]] int unreadCount() const;\n\t[[nodiscard]] bool unreadCountKnown() const;\n\n\t// Some old unread count is known, but we read history till some place.\n\t[[nodiscard]] bool unreadCountRefreshNeeded(MsgId readTillId) const;\n\n\tvoid setUnreadCount(int newUnreadCount);\n\tvoid setUnreadMark(bool unread);\n\tvoid setFakeUnreadWhileOpened(bool enabled);\n\t[[nodiscard]] bool fakeUnreadWhileOpened() const;\n\tvoid setMuted(bool muted) override;\n\tvoid addUnreadBar();\n\tvoid destroyUnreadBar();\n\t[[nodiscard]] Element *unreadBar() const;\n\tvoid calculateFirstUnreadMessage();\n\tvoid unsetFirstUnreadMessage();\n\t[[nodiscard]] Element *firstUnreadMessage() const;\n\n\t[[nodiscard]] bool loadedAtBottom() const; // last message is in the list\n\tvoid setNotLoadedAtBottom();\n\t[[nodiscard]] bool loadedAtTop() const; // nothing was added after loading history back\n\t[[nodiscard]] bool isReadyFor(MsgId msgId); // has messages for showing history at msgId\n\tvoid getReadyFor(MsgId msgId);\n\n\t[[nodiscard]] HistoryItem *lastMessage() const;\n\t[[nodiscard]] HistoryItem *lastServerMessage() const;\n\t[[nodiscard]] bool lastMessageKnown() const;\n\t[[nodiscard]] bool lastServerMessageKnown() const;\n\tvoid unknownMessageDeleted(MsgId messageId);\n\t[[nodiscard]] bool isUnknownMessageDeleted(MsgId messageId) const;\n\tvoid applyDialogTopMessage(MsgId topMessageId);\n\tvoid applyDialog(Data::Folder *requestFolder, const MTPDdialog &data);\n\tvoid applyPinnedUpdate(const MTPDupdateDialogPinned &data);\n\tvoid applyDialogFields(\n\t\tData::Folder *folder,\n\t\tint unreadCount,\n\t\tMsgId maxInboxRead,\n\t\tMsgId maxOutboxRead);\n\tvoid dialogEntryApplied();\n\n\tvoid cacheTopPromotion(\n\t\tbool promoted,\n\t\tconst QString &type,\n\t\tconst QString &message);\n\t[[nodiscard]] QStringView topPromotionType() const;\n\t[[nodiscard]] QString topPromotionMessage() const;\n\t[[nodiscard]] bool topPromotionAboutShown() const;\n\tvoid markTopPromotionAboutShown();\n\n\tMsgId minMsgId() const;\n\tMsgId maxMsgId() const;\n\tMsgId msgIdForRead() const;\n\tHistoryItem *lastEditableMessage() const;\n\n\tvoid resizeToWidth(int newWidth);\n\tvoid forceFullResize();\n\tint height() const;\n\n\tvoid itemRemoved(not_null<HistoryItem*> item);\n\tvoid itemVanished(not_null<HistoryItem*> item);\n\n\tbool hasPendingResizedItems() const;\n\tvoid setHasPendingResizedItems();\n\n\t[[nodiscard]] auto sendActionPainter()\n\t-> HistoryView::SendActionPainter* override {\n\t\treturn &_sendActionPainter;\n\t}\n\n\tvoid clearLastKeyboard();\n\tvoid clearUnreadMentionsFor(MsgId topicRootId);\n\tvoid clearUnreadReactionsFor(\n\t\tMsgId topicRootId,\n\t\tData::SavedSublist *sublist);\n\tvoid clearUnreadPollVotesFor(MsgId topicRootId);\n\n\t[[nodiscard]] int unreadPollVotesCount() const;\n\tvoid setUnreadPollVotesCount(int count);\n\t[[nodiscard]] rpl::producer<int> unreadPollVotesCountChanges() const;\n\n\tData::Draft *draft(Data::DraftKey key) const;\n\tvoid setDraft(Data::DraftKey key, std::unique_ptr<Data::Draft> &&draft);\n\tvoid clearDraft(Data::DraftKey key);\n\n\t[[nodiscard]] const Data::HistoryDrafts &draftsMap() const;\n\tvoid setDraftsMap(Data::HistoryDrafts &&map);\n\n\tData::Draft *localDraft(\n\t\t\tMsgId topicRootId,\n\t\t\tPeerId monoforumPeerId) const {\n\t\treturn draft(Data::DraftKey::Local(topicRootId, monoforumPeerId));\n\t}\n\tData::Draft *localEditDraft(\n\t\t\tMsgId topicRootId,\n\t\t\tPeerId monoforumPeerId) const {\n\t\treturn draft(\n\t\t\tData::DraftKey::LocalEdit(topicRootId, monoforumPeerId));\n\t}\n\tData::Draft *cloudDraft(\n\t\t\tMsgId topicRootId,\n\t\t\tPeerId monoforumPeerId) const {\n\t\treturn draft(Data::DraftKey::Cloud(topicRootId, monoforumPeerId));\n\t}\n\tvoid setLocalDraft(std::unique_ptr<Data::Draft> &&draft) {\n\t\tsetDraft(\n\t\t\tData::DraftKey::Local(\n\t\t\t\tdraft->reply.topicRootId,\n\t\t\t\tdraft->reply.monoforumPeerId),\n\t\t\tstd::move(draft));\n\t}\n\tvoid setLocalEditDraft(std::unique_ptr<Data::Draft> &&draft) {\n\t\tsetDraft(\n\t\t\tData::DraftKey::LocalEdit(\n\t\t\t\tdraft->reply.topicRootId,\n\t\t\t\tdraft->reply.monoforumPeerId),\n\t\t\tstd::move(draft));\n\t}\n\tvoid setCloudDraft(std::unique_ptr<Data::Draft> &&draft) {\n\t\tsetDraft(\n\t\t\tData::DraftKey::Cloud(\n\t\t\t\tdraft->reply.topicRootId,\n\t\t\t\tdraft->reply.monoforumPeerId),\n\t\t\tstd::move(draft));\n\t}\n\tvoid clearLocalDraft(\n\t\t\tMsgId topicRootId,\n\t\t\tPeerId monoforumPeerId) {\n\t\tclearDraft(Data::DraftKey::Local(topicRootId, monoforumPeerId));\n\t}\n\tvoid clearCloudDraft(\n\t\t\tMsgId topicRootId,\n\t\t\tPeerId monoforumPeerId) {\n\t\tclearDraft(Data::DraftKey::Cloud(topicRootId, monoforumPeerId));\n\t}\n\tvoid clearLocalEditDraft(\n\t\t\tMsgId topicRootId,\n\t\t\tPeerId monoforumPeerId) {\n\t\tclearDraft(Data::DraftKey::LocalEdit(topicRootId, monoforumPeerId));\n\t}\n\tvoid clearDrafts();\n\tData::Draft *createCloudDraft(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tconst Data::Draft *fromDraft);\n\t[[nodiscard]] bool skipCloudDraftUpdate(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tTimeId date) const;\n\tvoid startSavingCloudDraft(MsgId topicRootId, PeerId monoforumPeerId);\n\tvoid finishSavingCloudDraft(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tTimeId savedAt);\n\tvoid takeLocalDraft(not_null<History*> from);\n\tvoid applyCloudDraft(MsgId topicRootId, PeerId monoforumPeerId);\n\tvoid draftSavedToCloud(MsgId topicRootId, PeerId monoforumPeerId);\n\tvoid requestChatListMessage();\n\n\t[[nodiscard]] const Data::ForwardDraft &forwardDraft(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId) const;\n\t[[nodiscard]] Data::ResolvedForwardDraft resolveForwardDraft(\n\t\tconst Data::ForwardDraft &draft) const;\n\t[[nodiscard]] Data::ResolvedForwardDraft resolveForwardDraft(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId);\n\tvoid setForwardDraft(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tData::ForwardDraft &&draft);\n\n\tHistory *migrateSibling() const;\n\t[[nodiscard]] bool useTopPromotion() const;\n\tint fixedOnTopIndex() const override;\n\tvoid updateChatListExistence() override;\n\tbool shouldBeInChatList() const override;\n\tDialogs::UnreadState chatListUnreadState() const override;\n\tDialogs::BadgesState chatListBadgesState() const override;\n\tHistoryItem *chatListMessage() const override;\n\tbool chatListMessageKnown() const override;\n\tconst QString &chatListName() const override;\n\tconst QString &chatListNameSortKey() const override;\n\tint chatListNameVersion() const override;\n\tconst base::flat_set<QString> &chatListNameWords() const override;\n\tconst base::flat_set<QChar> &chatListFirstLetters() const override;\n\tvoid chatListPreloadData() override;\n\tvoid paintUserpic(\n\t\tPainter &p,\n\t\tUi::PeerUserpicView &view,\n\t\tconst Dialogs::Ui::PaintContext &context) const override;\n\n\tvoid refreshChatListNameSortKey();\n\n\tvoid setFakeChatListMessageFrom(const MTPmessages_Messages &data);\n\tvoid checkChatListMessageRemoved(not_null<HistoryItem*> item);\n\n\tvoid applyChatListGroup(\n\t\tPeerId dataPeerId,\n\t\tconst MTPmessages_Messages &data);\n\n\tvoid viewHeightAdjusted(not_null<Element*> view, int delta);\n\tvoid forgetScrollState() {\n\t\tscrollTopItem = nullptr;\n\t}\n\n\t// find the correct scrollTopItem and scrollTopOffset using given top\n\t// of the displayed window relative to the history start coordinate\n\tvoid countScrollState(int top);\n\n\t[[nodiscard]] std::pair<Element*, int> findItemAndOffset(int top) const;\n\n\t[[nodiscard]] MsgId nextNonHistoryEntryId();\n\n\tbool folderKnown() const override;\n\tData::Folder *folder() const override;\n\tvoid setFolder(\n\t\tnot_null<Data::Folder*> folder,\n\t\tHistoryItem *folderDialogItem = nullptr);\n\tvoid clearFolder();\n\n\t// Interface for Data::Histories.\n\tvoid setInboxReadTill(MsgId upTo);\n\tstd::optional<int> countStillUnreadLocal(MsgId readTillId) const;\n\tvoid tryMarkMonoforumIntervalRead(\n\t\tMsgId wasInboxReadBefore,\n\t\tMsgId nowInboxReadBefore);\n\tvoid tryMarkForumIntervalRead(\n\t\tMsgId wasInboxReadBefore,\n\t\tMsgId nowInboxReadBefore);\n\tvoid validateMonoAndForumUnread(MsgId readTillId);\n\n\t[[nodiscard]] bool isTopPromoted() const;\n\n\tvoid translateOfferFrom(LanguageId id);\n\t[[nodiscard]] LanguageId translateOfferedFrom() const;\n\tvoid translateTo(LanguageId id);\n\t[[nodiscard]] LanguageId translatedTo() const;\n\n\t[[nodiscard]] HistoryTranslation *translation() const;\n\n\tvoid refreshHiddenLinksItems();\n\n\tconst not_null<PeerData*> peer;\n\n\t// Still public data.\n\tstd::deque<std::unique_ptr<HistoryBlock>> blocks;\n\n\t// we save the last showAtMsgId to restore the state when switching\n\t// between different conversation histories\n\tMsgId showAtMsgId = ShowAtUnreadMsgId;\n\n\t// we save a pointer of the history item at the top of the displayed window\n\t// together with an offset from the window top to the top of this message\n\t// resulting scrollTop = top(scrollTopItem) + scrollTopOffset\n\tElement *scrollTopItem = nullptr;\n\tint scrollTopOffset = 0;\n\n\tbool lastKeyboardInited = false;\n\tbool lastKeyboardUsed = false;\n\tMsgId lastKeyboardId = 0;\n\tMsgId lastKeyboardHiddenId = 0;\n\tPeerId lastKeyboardFrom = 0;\n\n\tmtpRequestId sendRequestId = 0;\n\nprivate:\n\tfriend class HistoryBlock;\n\n\tenum class Flag : ushort {\n\t\tHasPendingResizedItems = (1 << 0),\n\t\tPendingAllItemsResize = (1 << 1),\n\t\tIsTopPromoted = (1 << 2),\n\t\tIsForum = (1 << 3),\n\t\tIsMonoforumAdmin = (1 << 4),\n\t\tFakeUnreadWhileOpened = (1 << 5),\n\t\tHasPinnedMessages = (1 << 6),\n\t\tResolveChatListMessage = (1 << 7),\n\t\tMonoAndForumUnreadInvalidatePending = (1 << 8),\n\t};\n\tusing Flags = base::flags<Flag>;\n\tfriend inline constexpr auto is_flag_type(Flag) {\n\t\treturn true;\n\t};\n\n\tvoid cacheTopPromoted(bool promoted);\n\n\t// when this item is destroyed scrollTopItem just points to the next one\n\t// and scrollTopOffset remains the same\n\t// if we are at the bottom of the window scrollTopItem == nullptr and\n\t// scrollTopOffset is undefined\n\tvoid getNextScrollTopItem(HistoryBlock *block, int32 i);\n\n\t// helper method for countScrollState(int top)\n\t[[nodiscard]] Element *findScrollTopItem(int top) const;\n\n\t// this method just removes a block from the blocks list\n\t// when the last item from this block was detached and\n\t// calls the required previousItemChanged()\n\tvoid removeBlock(not_null<HistoryBlock*> block);\n\tvoid clearSharedMedia();\n\n\tnot_null<HistoryItem*> insertItem(std::unique_ptr<HistoryItem> item);\n\tnot_null<HistoryItem*> addNewItem(\n\t\tnot_null<HistoryItem*> item,\n\t\tbool unread);\n\tnot_null<HistoryItem*> addNewToBack(\n\t\tnot_null<HistoryItem*> item,\n\t\tbool unread);\n\n\tfriend class Data::SponsoredMessages;\n\tnot_null<HistoryItem*> addNewInTheMiddle(\n\t\tnot_null<HistoryItem*> item,\n\t\tint blockIndex,\n\t\tint itemIndex);\n\n\t// All this methods add a new item to the first or last block\n\t// depending on if we are in isBuildingFronBlock() state.\n\t// The last block is created on the go if it is needed.\n\n\t// Adds the item to the back or front block, depending on\n\t// isBuildingFrontBlock(), creating the block if necessary.\n\tvoid addItemToBlock(not_null<HistoryItem*> item);\n\n\t// Usually all new items are added to the last block.\n\t// Only when we scroll up and add a new slice to the\n\t// front we want to create a new front block.\n\tvoid startBuildingFrontBlock(int expectedItemsCount = 1);\n\tvoid finishBuildingFrontBlock();\n\tbool isBuildingFrontBlock() const {\n\t\treturn _buildingFrontBlock != nullptr;\n\t}\n\n\tvoid addCreatedOlderSlice(\n\t\tconst std::vector<not_null<HistoryItem*>> &items);\n\n\tvoid checkForLoadedAtTop(not_null<HistoryItem*> added);\n\tvoid mainViewRemoved(\n\t\tnot_null<HistoryBlock*> block,\n\t\tnot_null<Element*> view);\n\tvoid mainViewHeightAdjusted(not_null<Element*> view, int delta);\n\n\tTimeId adjustedChatListTimeId() const override;\n\tvoid changedChatListPinHook() override;\n\n\tvoid setOutboxReadTill(MsgId upTo);\n\tvoid readClientSideMessages();\n\n\tvoid applyMessageChanges(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPMessage &original);\n\tvoid applyServiceChanges(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPDmessageService &data);\n\n\t// After adding a new history slice check lastMessage / loadedAtBottom.\n\tvoid checkLastMessage();\n\tvoid setLastMessage(HistoryItem *item);\n\tvoid setLastServerMessage(HistoryItem *item);\n\n\tvoid refreshChatListMessage();\n\tvoid setChatListMessage(HistoryItem *item);\n\tstd::optional<HistoryItem*> computeChatListMessageFromLast() const;\n\tvoid setChatListMessageFromLast();\n\tvoid setChatListMessageUnknown();\n\tvoid setFakeChatListMessage();\n\tvoid allowChatListMessageResolve();\n\tvoid resolveChatListMessageGroup();\n\n\t// Add all items to the unread mentions if we were not loaded at bottom and now are.\n\tvoid checkAddAllToUnreadMentions();\n\n\tvoid addToSharedMedia(const std::vector<not_null<HistoryItem*>> &items);\n\tvoid addEdgesToSharedMedia();\n\n\tvoid addItemsToLists(const std::vector<not_null<HistoryItem*>> &items);\n\tbool clearUnreadOnClientSide() const;\n\tbool skipUnreadUpdate() const;\n\n\tHistoryItem *lastAvailableMessage() const;\n\tvoid getNextFirstUnreadMessage();\n\tbool nonEmptyCountMoreThan(int count) const;\n\n\t// Creates if necessary a new block for adding item.\n\t// Depending on isBuildingFrontBlock() gets front or back block.\n\tHistoryBlock *prepareBlockForAddingItem();\n\n\tvoid viewReplaced(not_null<const Element*> was, Element *now);\n\n\tvoid createLocalDraftFromCloud(\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId);\n\n\tHistoryItem *insertJoinedMessage();\n\tvoid insertMessageToBlocks(not_null<HistoryItem*> item);\n\tvoid checkNewPeerMessages();\n\n\t[[nodiscard]] Dialogs::BadgesState computeBadgesState() const;\n\t[[nodiscard]] Dialogs::BadgesState adjustBadgesStateByFolder(\n\t\tDialogs::BadgesState state) const;\n\t[[nodiscard]] Dialogs::UnreadState computeUnreadState() const;\n\t[[nodiscard]] Dialogs::UnreadState withMyMuted(\n\t\tDialogs::UnreadState state) const;\n\tvoid setFolderPointer(Data::Folder *folder);\n\n\tvoid hasUnreadMentionChanged(bool has) override;\n\tvoid hasUnreadReactionChanged(bool has) override;\n\tvoid hasUnreadPollVoteChanged(bool has) override;\n\t[[nodiscard]] bool useMyUnreadInParent() const;\n\n\tconst std::unique_ptr<HistoryMainElementDelegateMixin> _delegateMixin;\n\n\tFlags _flags = 0;\n\tint _width = 0;\n\tint _height = 0;\n\tElement *_unreadBarView = nullptr;\n\tElement *_firstUnreadView = nullptr;\n\tHistoryItem *_joinedMessage = nullptr;\n\tHistoryItem *_newPeerNameChange = nullptr;\n\tHistoryItem *_newPeerPhotoChange = nullptr;\n\tbool _loadedAtTop = false;\n\tbool _loadedAtBottom = true;\n\n\tstd::optional<Data::Folder*> _folder;\n\n\tstd::optional<MsgId> _inboxReadBefore;\n\tstd::optional<MsgId> _outboxReadBefore;\n\tstd::optional<int> _unreadCount;\n\tint _unreadPollVotesCount = 0;\n\trpl::event_stream<int> _unreadPollVotesCountChanges;\n\tstd::optional<HistoryItem*> _lastMessage;\n\tstd::optional<HistoryItem*> _lastServerMessage;\n\tbase::flat_set<not_null<HistoryItem*>> _clientSideMessages;\n\tstd::unordered_set<std::unique_ptr<HistoryItem>> _items;\n\n\tstd::unique_ptr<Data::HistoryMessages> _messages;\n\tstd::unique_ptr<HistoryStreamedDrafts> _streamedDrafts;\n\n\t// This almost always is equal to _lastMessage. The only difference is\n\t// for a group that migrated to a supergroup. Then _lastMessage can\n\t// be a migrate message, but _chatListMessage should be the one before.\n\tstd::optional<HistoryItem*> _chatListMessage;\n\n\tQString _chatListNameSortKey;\n\n\t// A pointer to the block that is currently being built.\n\t// We hold this pointer so we can destroy it while building\n\t// and then create a new one if it is necessary.\n\tstruct BuildingBlock {\n\t\tint expectedItemsCount = 0; // optimization for block->items.reserve() call\n\t\tHistoryBlock *block = nullptr;\n\t};\n\tstd::unique_ptr<BuildingBlock> _buildingFrontBlock;\n\tstd::unique_ptr<HistoryTranslation> _translation;\n\n\tData::HistoryDrafts _drafts;\n\tbase::flat_map<Data::DraftKey, TimeId> _acceptCloudDraftsAfter;\n\tbase::flat_map<Data::DraftKey, int> _savingCloudDraftRequests;\n\tbase::flat_map<Data::DraftKey, Data::ForwardDraft> _forwardDrafts;\n\n\tbase::flat_map<MsgId, TimeId> _unknownDeletedMessages;\n\n\tQString _topPromotedMessage;\n\tQString _topPromotedType;\n\n\tHistoryView::SendActionPainter _sendActionPainter;\n\n\n};\n\nclass HistoryBlock {\npublic:\n\tusing Element = HistoryView::Element;\n\n\tenum class ResizeRequest {\n\t\tReinitAll = 0,\n\t\tResizeAll = 1,\n\t\tResizePending = 2,\n\t};\n\n\tHistoryBlock(not_null<History*> history);\n\tHistoryBlock(const HistoryBlock &) = delete;\n\tHistoryBlock &operator=(const HistoryBlock &) = delete;\n\t~HistoryBlock();\n\n\tstd::vector<std::unique_ptr<Element>> messages;\n\n\tvoid remove(not_null<Element*> view);\n\tvoid refreshView(not_null<Element*> view);\n\n\tint resizeGetHeight(int newWidth, ResizeRequest request);\n\tint y() const {\n\t\treturn _y;\n\t}\n\tvoid setY(int y) {\n\t\t_y = y;\n\t}\n\tint height() const {\n\t\treturn _height;\n\t}\n\tnot_null<History*> history() const {\n\t\treturn _history;\n\t}\n\n\tHistoryBlock *previousBlock() const {\n\t\tExpects(_indexInHistory >= 0);\n\n\t\treturn (_indexInHistory > 0)\n\t\t\t? _history->blocks[_indexInHistory - 1].get()\n\t\t\t: nullptr;\n\t}\n\tHistoryBlock *nextBlock() const {\n\t\tExpects(_indexInHistory >= 0);\n\n\t\treturn (_indexInHistory + 1 < _history->blocks.size())\n\t\t\t? _history->blocks[_indexInHistory + 1].get()\n\t\t\t: nullptr;\n\t}\n\tvoid setIndexInHistory(int index) {\n\t\t_indexInHistory = index;\n\t}\n\tint indexInHistory() const {\n\t\tExpects(_indexInHistory >= 0);\n\t\tExpects(_indexInHistory < _history->blocks.size());\n\t\tExpects(_history->blocks[_indexInHistory].get() == this);\n\n\t\treturn _indexInHistory;\n\t}\n\nprotected:\n\tconst not_null<History*> _history;\n\n\tint _y = 0;\n\tint _height = 0;\n\tint _indexInHistory = -1;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_drag_area.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_drag_area.h\"\n\n#include \"base/event_filter.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"inline_bots/inline_bot_layout_item.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"history/history_widget.h\"\n#include \"storage/localstorage.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"mainwindow.h\"\n#include \"apiwrap.h\"\n#include \"mainwidget.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n\nnamespace {\n\nconstexpr auto kDragAreaEvents = {\n\tQEvent::DragEnter,\n\tQEvent::DragLeave,\n\tQEvent::Drop,\n\tQEvent::MouseButtonRelease,\n\tQEvent::Leave,\n};\n\n} // namespace\n\nDragArea::Areas DragArea::SetupDragAreaToContainer(\n\t\tnot_null<Ui::RpWidget*> container,\n\t\tFn<bool(not_null<const QMimeData*>)> &&dragEnterFilter,\n\t\tFn<void(bool)> &&setAcceptDropsField,\n\t\tFn<void()> &&updateControlsGeometry,\n\t\tDragArea::CallbackComputeState &&computeState,\n\t\tbool hideSubtext) {\n\n\tusing DragState = Storage::MimeDataState;\n\n\tauto &lifetime = container->lifetime();\n\tcontainer->setAcceptDrops(true);\n\n\tconst auto attachDragDocument\n\t\t= Ui::CreateChild<DragArea>(container.get());\n\tconst auto attachDragPhoto = Ui::CreateChild<DragArea>(container.get());\n\n\tattachDragDocument->hide();\n\tattachDragPhoto->hide();\n\n\tattachDragDocument->raise();\n\tattachDragPhoto->raise();\n\n\tconst auto attachDragState\n\t\t= lifetime.make_state<DragState>(DragState::None);\n\n\tconst auto width = [=] {\n\t\treturn container->width();\n\t};\n\tconst auto height = [=] {\n\t\treturn container->height();\n\t};\n\n\tconst auto horizontalMargins = st::dragMargin.left()\n\t\t+ st::dragMargin.right();\n\tconst auto verticalMargins = st::dragMargin.top()\n\t\t+ st::dragMargin.bottom();\n\tconst auto resizeToFull = [=](not_null<DragArea*> w) {\n\t\tw->resize(width() - horizontalMargins, height() - verticalMargins);\n\t};\n\tconst auto moveToTop = [=](not_null<DragArea*> w) {\n\t\tw->move(st::dragMargin.left(), st::dragMargin.top());\n\t};\n\tconst auto updateAttachGeometry = crl::guard(container, [=] {\n\t\tif (updateControlsGeometry) {\n\t\t\tupdateControlsGeometry();\n\t\t}\n\n\t\tswitch (*attachDragState) {\n\t\tcase DragState::Files:\n\t\t\tresizeToFull(attachDragDocument);\n\t\t\tmoveToTop(attachDragDocument);\n\t\tbreak;\n\t\tcase DragState::PhotoFiles:\n\t\tcase DragState::MediaFiles:\n\t\t\tattachDragDocument->resize(\n\t\t\t\twidth() - horizontalMargins,\n\t\t\t\t(height() - verticalMargins) / 2);\n\t\t\tmoveToTop(attachDragDocument);\n\t\t\tattachDragPhoto->resize(\n\t\t\t\tattachDragDocument->width(),\n\t\t\t\tattachDragDocument->height());\n\t\t\tattachDragPhoto->move(\n\t\t\t\tst::dragMargin.left(),\n\t\t\t\theight()\n\t\t\t\t\t- attachDragPhoto->height()\n\t\t\t\t\t- st::dragMargin.bottom());\n\t\tbreak;\n\t\tcase DragState::Image:\n\t\t\tresizeToFull(attachDragPhoto);\n\t\t\tmoveToTop(attachDragPhoto);\n\t\tbreak;\n\t\t}\n\t});\n\n\tconst auto updateDragAreas = [=] {\n\t\tif (setAcceptDropsField) {\n\t\t\tsetAcceptDropsField(*attachDragState == DragState::None);\n\t\t}\n\t\tupdateAttachGeometry();\n\n\t\tswitch (*attachDragState) {\n\t\tcase DragState::None:\n\t\t\tattachDragDocument->otherLeave();\n\t\t\tattachDragPhoto->otherLeave();\n\t\tbreak;\n\t\tcase DragState::Files:\n\t\t\tattachDragDocument->setText(\n\t\t\t\ttr::lng_drag_files_here(tr::now),\n\t\t\t\thideSubtext\n\t\t\t\t\t? QString()\n\t\t\t\t\t: tr::lng_drag_to_send_files(tr::now));\n\t\t\tattachDragDocument->otherEnter();\n\t\t\tattachDragPhoto->hideFast();\n\t\tbreak;\n\t\tcase DragState::PhotoFiles:\n\t\t\tattachDragDocument->setText(\n\t\t\t\ttr::lng_drag_images_here(tr::now),\n\t\t\t\thideSubtext\n\t\t\t\t\t? QString()\n\t\t\t\t\t: tr::lng_drag_to_send_no_compression(tr::now));\n\t\t\tattachDragPhoto->setText(\n\t\t\t\ttr::lng_drag_photos_here(tr::now),\n\t\t\t\thideSubtext\n\t\t\t\t\t? QString()\n\t\t\t\t\t: tr::lng_drag_to_send_quick(tr::now));\n\t\t\tattachDragDocument->otherEnter();\n\t\t\tattachDragPhoto->otherEnter();\n\t\tbreak;\n\t\tcase DragState::MediaFiles:\n\t\t\tattachDragDocument->setText(\n\t\t\t\ttr::lng_drag_files_here(tr::now),\n\t\t\t\thideSubtext\n\t\t\t\t\t? QString()\n\t\t\t\t\t: tr::lng_drag_to_send_files(tr::now));\n\t\t\tattachDragPhoto->setText(\n\t\t\t\ttr::lng_drag_media_here(tr::now),\n\t\t\t\thideSubtext\n\t\t\t\t\t? QString()\n\t\t\t\t\t: tr::lng_drag_to_send_media(tr::now));\n\t\t\tattachDragDocument->otherEnter();\n\t\t\tattachDragPhoto->otherEnter();\n\t\tbreak;\n\t\tcase DragState::Image:\n\t\t\tattachDragPhoto->setText(\n\t\t\t\ttr::lng_drag_images_here(tr::now),\n\t\t\t\thideSubtext\n\t\t\t\t\t? QString()\n\t\t\t\t\t: tr::lng_drag_to_send_quick(tr::now));\n\t\t\tattachDragDocument->hideFast();\n\t\t\tattachDragPhoto->otherEnter();\n\t\tbreak;\n\t\t};\n\t};\n\n\tcontainer->sizeValue(\n\t) | rpl::on_next(updateAttachGeometry, lifetime);\n\n\tconst auto resetDragStateIfNeeded = [=] {\n\t\tif (*attachDragState != DragState::None\n\t\t\t|| !attachDragPhoto->isHidden()\n\t\t\t|| !attachDragDocument->isHidden()) {\n\t\t\t*attachDragState = DragState::None;\n\t\t\tupdateDragAreas();\n\t\t}\n\t};\n\n\tconst auto dragEnterEvent = [=](QDragEnterEvent *e) {\n\t\tif (dragEnterFilter && !dragEnterFilter(e->mimeData())) {\n\t\t\treturn;\n\t\t}\n\n\t\t*attachDragState = computeState\n\t\t\t? computeState(e->mimeData())\n\t\t\t: Storage::ComputeMimeDataState(e->mimeData());\n\t\tupdateDragAreas();\n\n\t\tif (*attachDragState != DragState::None) {\n\t\t\te->setDropAction(Qt::IgnoreAction);\n\t\t\te->accept();\n\t\t}\n\t};\n\n\tconst auto dragLeaveEvent = [=](QDragLeaveEvent *e) {\n\t\tresetDragStateIfNeeded();\n\t};\n\n\tconst auto dropEvent = [=](QDropEvent *e) {\n\t\t// Hide fast to avoid visual bugs in resizable boxes.\n\t\tattachDragDocument->hideFast();\n\t\tattachDragPhoto->hideFast();\n\n\t\t*attachDragState = DragState::None;\n\t\tupdateDragAreas();\n\t\te->setDropAction(Qt::CopyAction);\n\t\te->accept();\n\t};\n\n\tconst auto processDragEvents = [=](not_null<QEvent*> event) {\n\t\tswitch (event->type()) {\n\t\tcase QEvent::DragEnter:\n\t\t\tdragEnterEvent(static_cast<QDragEnterEvent*>(event.get()));\n\t\t\treturn true;\n\t\tcase QEvent::DragLeave:\n\t\t\tdragLeaveEvent(static_cast<QDragLeaveEvent*>(event.get()));\n\t\t\treturn true;\n\t\tcase QEvent::Drop:\n\t\t\tdropEvent(static_cast<QDropEvent*>(event.get()));\n\t\t\treturn true;\n\t\t};\n\t\treturn false;\n\t};\n\n\tcontainer->events(\n\t) | rpl::filter([=](not_null<QEvent*> event) {\n\t\treturn ranges::contains(kDragAreaEvents, event->type());\n\t}) | rpl::on_next([=](not_null<QEvent*> event) {\n\t\tconst auto type = event->type();\n\n\t\tif (processDragEvents(event)) {\n\t\t\treturn;\n\t\t} else if (type == QEvent::Leave\n\t\t\t|| type == QEvent::MouseButtonRelease) {\n\t\t\tresetDragStateIfNeeded();\n\t\t}\n\t}, lifetime);\n\n\tconst auto eventFilter = [=](not_null<QEvent*> event) {\n\t\tprocessDragEvents(event);\n\t\treturn base::EventFilterResult::Continue;\n\t};\n\tbase::install_event_filter(attachDragDocument, eventFilter);\n\tbase::install_event_filter(attachDragPhoto, eventFilter);\n\n\tupdateDragAreas();\n\n\treturn {\n\t\t.document = attachDragDocument,\n\t\t.photo = attachDragPhoto,\n\t};\n}\n\nDragArea::DragArea(QWidget *parent) : Ui::RpWidget(parent) {\n\tsetMouseTracking(true);\n\tsetAcceptDrops(true);\n}\n\nbool DragArea::overlaps(const QRect &globalRect) {\n\tif (isHidden() || _a_opacity.animating()) {\n\t\treturn false;\n\t}\n\n\tconst auto inner = rect() - st::dragPadding;\n\tconst auto testRect = QRect(\n\t\tmapFromGlobal(globalRect.topLeft()),\n\t\tglobalRect.size());\n\tconst auto h = QMargins(st::boxRadius, 0, st::boxRadius, 0);\n\tconst auto v = QMargins(0, st::boxRadius, 0, st::boxRadius);\n\treturn inner.marginsRemoved(h).contains(testRect)\n\t\t|| inner.marginsRemoved(v).contains(testRect);\n}\n\n\nvoid DragArea::mouseMoveEvent(QMouseEvent *e) {\n\tif (_hiding) {\n\t\treturn;\n\t}\n\n\tsetIn((rect() - st::dragPadding).contains(e->pos()));\n}\n\nvoid DragArea::dragMoveEvent(QDragMoveEvent *e) {\n\tsetIn((rect() - st::dragPadding).contains(e->pos()));\n\te->setDropAction(_in ? Qt::CopyAction : Qt::IgnoreAction);\n\te->accept();\n}\n\nvoid DragArea::setIn(bool in) {\n\tif (_in != in) {\n\t\t_in = in;\n\t\t_a_in.start(\n\t\t\t[=] { update(); },\n\t\t\t_in ? 0. : 1.,\n\t\t\t_in ? 1. : 0.,\n\t\t\tst::boxDuration);\n\t}\n}\n\nvoid DragArea::setText(const QString &text, const QString &subtext) {\n\t_text = text;\n\t_subtext = subtext;\n\tupdate();\n}\n\nvoid DragArea::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tconst auto opacity = _a_opacity.value(_hiding ? 0. : 1.);\n\tif (!_a_opacity.animating() && _hiding) {\n\t\treturn;\n\t}\n\tp.setOpacity(opacity);\n\tconst auto inner = rect() - st::dragPadding;\n\n\tif (!_cache.isNull()) {\n\t\tp.drawPixmapLeft(\n\t\t\tinner.x() - st::boxRoundShadow.extend.left(),\n\t\t\tinner.y() - st::boxRoundShadow.extend.top(),\n\t\t\twidth(),\n\t\t\t_cache);\n\t\treturn;\n\t}\n\n\tUi::Shadow::paint(p, inner, width(), st::boxRoundShadow);\n\tUi::FillRoundRect(p, inner, st::boxBg, Ui::BoxCorners);\n\n\tp.setPen(anim::pen(\n\t\tst::dragColor,\n\t\tst::dragDropColor,\n\t\t_a_in.value(_in ? 1. : 0.)));\n\n\tp.setFont(st::dragFont);\n\tconst auto rText = QRect(\n\t\t0,\n\t\t(height() - st::dragHeight) / 2,\n\t\twidth(),\n\t\tst::dragFont->height);\n\tp.drawText(rText, _text, QTextOption(style::al_top));\n\n\tp.setFont(st::dragSubfont);\n\tconst auto rSubtext = QRect(\n\t\t0,\n\t\t(height() + st::dragHeight) / 2 - st::dragSubfont->height,\n\t\twidth(),\n\t\tst::dragSubfont->height * 2);\n\tp.drawText(rSubtext, _subtext, QTextOption(style::al_top));\n}\n\nvoid DragArea::dragEnterEvent(QDragEnterEvent *e) {\n\te->setDropAction(Qt::IgnoreAction);\n\te->accept();\n}\n\nvoid DragArea::dragLeaveEvent(QDragLeaveEvent *e) {\n\tsetIn(false);\n}\n\nvoid DragArea::dropEvent(QDropEvent *e) {\n\tif (e->isAccepted() && _droppedCallback) {\n\t\t_droppedCallback(e->mimeData());\n\t}\n}\n\nvoid DragArea::otherEnter() {\n\tshowStart();\n}\n\nvoid DragArea::otherLeave() {\n\thideStart();\n}\n\nvoid DragArea::hideFast() {\n\t_a_opacity.stop();\n\thide();\n}\n\nvoid DragArea::hideStart() {\n\tif (_hiding || isHidden()) {\n\t\treturn;\n\t}\n\tif (_cache.isNull()) {\n\t\t_cache = Ui::GrabWidget(\n\t\t\tthis,\n\t\t\trect() - st::dragPadding + st::boxRoundShadow.extend);\n\t}\n\t_hiding = true;\n\tsetIn(false);\n\t_a_opacity.start(\n\t\t[=] { opacityAnimationCallback(); },\n\t\t1.,\n\t\t0.,\n\t\tst::boxDuration);\n}\n\nvoid DragArea::hideFinish() {\n\thide();\n\t_in = false;\n\t_a_in.stop();\n}\n\nvoid DragArea::showStart() {\n\tif (!_hiding && !isHidden()) {\n\t\treturn;\n\t}\n\t_hiding = false;\n\tif (_cache.isNull()) {\n\t\t_cache = Ui::GrabWidget(\n\t\t\tthis,\n\t\t\trect() - st::dragPadding + st::boxRoundShadow.extend);\n\t}\n\tshow();\n\t_a_opacity.start(\n\t\t[=] { opacityAnimationCallback(); },\n\t\t0.,\n\t\t1.,\n\t\tst::boxDuration);\n}\n\nvoid DragArea::opacityAnimationCallback() {\n\tupdate();\n\tif (!_a_opacity.animating()) {\n\t\t_cache = QPixmap();\n\t\tif (_hiding) {\n\t\t\thideFinish();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_drag_area.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace Storage {\nenum class MimeDataState;\n} // namespace Storage\n\nclass DragArea : public Ui::RpWidget {\npublic:\n\tDragArea(QWidget *parent);\n\n\tstruct Areas {\n\t\tDragArea *document;\n\t\tDragArea *photo;\n\t};\n\n\tusing CallbackComputeState\n\t\t= Fn<Storage::MimeDataState(const QMimeData *data)>;\n\n\tstatic Areas SetupDragAreaToContainer(\n\t\tnot_null<Ui::RpWidget*> container,\n\t\tFn<bool(not_null<const QMimeData*>)> &&dragEnterFilter = nullptr,\n\t\tFn<void(bool)> &&setAcceptDropsField = nullptr,\n\t\tFn<void()> &&updateControlsGeometry = nullptr,\n\t\tCallbackComputeState &&computeState = nullptr,\n\t\tbool hideSubtext = false);\n\n\tvoid setText(const QString &text, const QString &subtext);\n\n\tvoid otherEnter();\n\tvoid otherLeave();\n\n\tbool overlaps(const QRect &globalRect);\n\n\tvoid hideFast();\n\n\tvoid setDroppedCallback(Fn<void(const QMimeData *data)> callback) {\n\t\t_droppedCallback = std::move(callback);\n\t}\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid dragMoveEvent(QDragMoveEvent *e) override;\n\t// These events should be filtered by parent!\n\tvoid dragEnterEvent(QDragEnterEvent *e) override;\n\tvoid dragLeaveEvent(QDragLeaveEvent *e) override;\n\tvoid dropEvent(QDropEvent *e) override;\n\nprivate:\n\tvoid hideStart();\n\tvoid hideFinish();\n\n\tvoid showStart();\n\n\tvoid setIn(bool in);\n\tvoid opacityAnimationCallback();\n\n\tbool _hiding = false;\n\tbool _in = false;\n\tQPixmap _cache;\n\tFn<void(const QMimeData *data)> _droppedCallback;\n\n\tUi::Animations::Simple _a_opacity;\n\tUi::Animations::Simple _a_in;\n\n\tQString _text, _subtext;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_inner_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_inner_widget.h\"\n\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"api/api_sending.h\"\n#include \"forkgram/uri_menu.h\"\n#include \"ui/widgets/menu/menu.h\"\n\n#include \"api/api_polls.h\"\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"core/application.h\"\n#include \"core/file_utilities.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/phone_click_handler.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/view/controls/history_view_forward_panel.h\"\n#include \"history/view/controls/history_view_draft_options.h\"\n#include \"history/view/controls/history_view_suggest_options.h\"\n#include \"history/view/media/history_view_save_document_action.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"history/view/media/history_view_web_page.h\"\n#include \"history/view/reactions/history_view_reactions.h\"\n#include \"history/view/reactions/history_view_reactions_button.h\"\n#include \"history/view/reactions/history_view_reactions_selector.h\"\n#include \"history/view/history_view_about_view.h\"\n#include \"history/view/history_view_message.h\"\n#include \"history/view/history_view_service_message.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_reply_button.h\"\n#include \"history/view/history_view_context_menu.h\"\n#include \"history/view/history_view_reaction_preview.h\"\n#include \"history/view/history_view_quick_action.h\"\n#include \"history/view/history_view_add_poll_option.h\"\n#include \"history/view/history_view_element_overlay.h\"\n#include \"history/view/history_view_emoji_interactions.h\"\n#include \"history/view/history_view_top_peers_selector.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_text.h\"\n#include \"payments/payments_reaction_process.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/menu/menu_multiline_action.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/effects/message_sending_animation_controller.h\"\n#include \"ui/effects/reaction_fly_animation.h\"\n#include \"ui/text/text_isolated_emoji.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/boxes/edit_factcheck_box.h\"\n#include \"ui/boxes/report_box_graphics.h\"\n#include \"ui/controls/delete_message_context_action.h\"\n#include \"ui/controls/who_reacted_context_action.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/controls/swipe_handler.h\"\n#include \"ui/inactive_press.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/notifications_manager.h\"\n#include \"info/info_memento.h\"\n#include \"info/statistics/info_statistics_widget.h\"\n#include \"boxes/about_sponsored_box.h\"\n#include \"boxes/delete_messages_box.h\"\n#include \"boxes/moderate_messages_box.h\"\n#include \"boxes/report_messages_box.h\"\n#include \"boxes/send_gif_with_caption_box.h\"\n#include \"boxes/star_gift_box.h\" // ShowStarGiftBox\n#include \"boxes/sticker_set_box.h\"\n#include \"boxes/share_box.h\"\n#include \"boxes/translate_box.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"chat_helpers/emoji_interactions.h\"\n#include \"history/history_widget.h\"\n#include \"history/view/history_view_translate_tracker.h\"\n#include \"history/view/history_view_read_metrics_tracker.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/qt/qt_common_adapters.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"base/unixtime.h\"\n#include \"base/call_delayed.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"mainwidget.h\"\n#include \"menu/menu_item_download_files.h\"\n#include \"menu/menu_item_rate_transcribe.h\"\n#include \"menu/menu_item_rate_transcribe_session.h\"\n#include \"menu/menu_timecode_action.h\"\n#include \"menu/menu_sponsored.h\"\n#include \"core/application.h\"\n#include \"apiwrap.h\"\n#include \"api/api_attached_stickers.h\"\n#include \"api/api_suggest_post.h\"\n#include \"api/api_toggling_media.h\"\n#include \"api/api_who_reacted.h\"\n#include \"api/api_views.h\"\n#include \"lang/lang_keys.h\"\n#include \"data/components/factchecks.h\"\n#include \"data/components/sponsored_messages.h\"\n#include \"data/data_saved_music.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_todo_list.h\"\n#include \"dialogs/ui/dialogs_video_userpic.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtGui/QClipboard>\n#include <QtGui/QDesktopServices>\n#include <QtWidgets/QApplication>\n#include <QtCore/QCoreApplication>\n#include <QtCore/QMimeData>\n\n#include \"api/api_as_copy.h\"\n#include \"history/view/history_view_context_menu_fork.h\"\n\nnamespace {\n\nconstexpr auto kScrollDateHideTimeout = 1000;\nconstexpr auto kUnloadHeavyPartsPages = 2;\nconstexpr auto kClearUserpicsAfter = 50;\n\n// Helper binary search for an item in a list that is not completely\n// above the given top of the visible area or below the given bottom of the visible area\n// is applied once for blocks list in a history and once for items list in the found block.\ntemplate <bool TopToBottom, typename T>\nint BinarySearchBlocksOrItems(const T &list, int edge) {\n\t// static_cast to work around GCC bug #78693\n\tauto start = 0, end = static_cast<int>(list.size());\n\twhile (end - start > 1) {\n\t\tauto middle = (start + end) / 2;\n\t\tauto top = list[middle]->y();\n\t\tauto chooseLeft = (TopToBottom ? (top <= edge) : (top < edge));\n\t\tif (chooseLeft) {\n\t\t\tstart = middle;\n\t\t} else {\n\t\t\tend = middle;\n\t\t}\n\t}\n\treturn start;\n}\n\n} // namespace\n\n// flick scroll taken from http://qt-project.org/doc/qt-4.8/demos-embedded-anomaly-src-flickcharm-cpp.html\n\nHistoryMainElementDelegateMixin::HistoryMainElementDelegateMixin() = default;\n\nHistoryMainElementDelegateMixin::~HistoryMainElementDelegateMixin()\n\t= default;\n\nclass HistoryMainElementDelegate final\n\t: public HistoryView::ElementDelegate\n\t, public HistoryMainElementDelegateMixin {\npublic:\n\tusing Element = HistoryView::Element;\n\n\tHistoryView::Context elementContext() override {\n\t\treturn HistoryView::Context::History;\n\t}\n\tbool elementUnderCursor(\n\t\t\tnot_null<const Element*> view) override {\n\t\treturn (Element::Moused() == view);\n\t}\n\tHistoryView::SelectionModeResult elementInSelectionMode(\n\t\t\tconst Element *view) override {\n\t\tif (view && view->data()->isSponsored()) {\n\t\t\treturn HistoryView::SelectionModeResult();\n\t\t}\n\t\treturn _widget\n\t\t\t? _widget->inSelectionMode()\n\t\t\t: HistoryView::SelectionModeResult();\n\t}\n\tbool elementIntersectsRange(\n\t\t\tnot_null<const Element*> view,\n\t\t\tint from,\n\t\t\tint till) override {\n\t\treturn _widget\n\t\t\t? _widget->elementIntersectsRange(view, from, till)\n\t\t\t: false;\n\t}\n\tvoid elementStartStickerLoop(\n\t\t\tnot_null<const Element*> view) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementStartStickerLoop(view);\n\t\t}\n\t}\n\tvoid elementShowPollResults(\n\t\t\tnot_null<PollData*> poll,\n\t\t\tFullMsgId context) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementShowPollResults(poll, context);\n\t\t}\n\t}\n\tvoid elementShowAddPollOption(\n\t\t\tnot_null<Element*> view,\n\t\t\tnot_null<PollData*> poll,\n\t\t\tFullMsgId context,\n\t\t\tQRect optionRect) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementShowAddPollOption(view, poll, context, optionRect);\n\t\t}\n\t}\n\tvoid elementSubmitAddPollOption(FullMsgId context) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementSubmitAddPollOption(context);\n\t\t}\n\t}\n\tvoid elementOpenPhoto(\n\t\t\tnot_null<PhotoData*> photo,\n\t\t\tFullMsgId context) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementOpenPhoto(photo, context);\n\t\t}\n\t}\n\tvoid elementOpenDocument(\n\t\t\tnot_null<DocumentData*> document,\n\t\t\tFullMsgId context,\n\t\t\tbool showInMediaView = false) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementOpenDocument(\n\t\t\t\tdocument,\n\t\t\t\tcontext,\n\t\t\t\tshowInMediaView);\n\t\t}\n\t}\n\tvoid elementCancelUpload(const FullMsgId &context) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementCancelUpload(context);\n\t\t}\n\t}\n\tvoid elementShowTooltip(\n\t\t\tconst TextWithEntities &text,\n\t\t\tFn<void()> hiddenCallback) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementShowTooltip(text, hiddenCallback);\n\t\t}\n\t}\n\tbool elementAnimationsPaused() override {\n\t\treturn _widget ? _widget->elementAnimationsPaused() : false;\n\t}\n\tbool elementHideReply(not_null<const Element*> view) override {\n\t\tif (!view->isTopicRootReply()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto reply = view->data()->Get<HistoryMessageReply>();\n\t\treturn reply && !reply->fields().manualQuote;\n\t}\n\tbool elementShownUnread(not_null<const Element*> view) override {\n\t\treturn view->data()->unread(view->data()->history());\n\t}\n\tvoid elementSendBotCommand(\n\t\t\tconst QString &command,\n\t\t\tconst FullMsgId &context) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementSendBotCommand(command, context);\n\t\t}\n\t}\n\tvoid elementSearchInList(\n\t\t\tconst QString &query,\n\t\t\tconst FullMsgId &context) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementSearchInList(query, context);\n\t\t}\n\t}\n\tvoid elementHandleViaClick(not_null<UserData*> bot) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementHandleViaClick(bot);\n\t\t}\n\t}\n\tHistoryView::ElementChatMode elementChatMode() override {\n\t\tusing Mode = HistoryView::ElementChatMode;\n\t\treturn _widget ? _widget->elementChatMode() : Mode::Default;\n\t}\n\tnot_null<Ui::PathShiftGradient*> elementPathShiftGradient() override {\n\t\tExpects(_widget != nullptr);\n\n\t\treturn _widget->elementPathShiftGradient();\n\t}\n\tvoid elementReplyTo(const FullReplyTo &to) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementReplyTo(to);\n\t\t}\n\t}\n\tvoid elementStartInteraction(not_null<const Element*> view) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementStartInteraction(view);\n\t\t}\n\t}\n\tvoid elementStartPremium(\n\t\t\tnot_null<const Element*> view,\n\t\t\tElement *replacing) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementStartPremium(view, replacing);\n\t\t}\n\t}\n\tvoid elementCancelPremium(not_null<const Element*> view) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementCancelPremium(view);\n\t\t}\n\t}\n\tvoid elementStartEffect(\n\t\t\tnot_null<const Element*> view,\n\t\t\tElement *replacing) override {\n\t\tif (_widget) {\n\t\t\t_widget->elementStartEffect(view, replacing);\n\t\t}\n\t}\n\n\tQString elementAuthorRank(not_null<const Element*> view) override {\n\t\treturn {};\n\t}\n\n\tbool elementHideTopicButton(not_null<const Element*> view) override {\n\t\treturn false;\n\t}\n\n\tnot_null<HistoryView::ElementDelegate*> delegate() override {\n\t\treturn this;\n\t}\n\n};\n\nHistoryInner::HistoryInner(\n\tnot_null<HistoryWidget*> historyWidget,\n\tnot_null<Ui::ScrollArea*> scroll,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<History*> history)\n: RpWidget(nullptr)\n, _widget(historyWidget)\n, _scroll(scroll)\n, _controller(controller)\n, _peer(history->peer)\n, _history(history)\n, _elementDelegate(_history->delegateMixin()->delegate())\n, _emojiInteractions(std::make_unique<HistoryView::EmojiInteractions>(\n\tthis,\n\tcontroller->content(),\n\t&controller->session(),\n\t[=](not_null<const Element*> view) { return itemTop(view); }))\n, _migrated(history->migrateFrom())\n, _translateTracker(std::make_unique<HistoryView::TranslateTracker>(history))\n, _readMetricsTracker(std::make_unique<HistoryView::ReadMetricsTracker>(\n\t_peer))\n, _pathGradient(\n\tHistoryView::MakePathShiftGradient(\n\t\tcontroller->chatStyle(),\n\t\t[=] { update(); }))\n, _reactionsManager(\n\tstd::make_unique<HistoryView::Reactions::Manager>(\n\t\tthis,\n\t\t[=](QRect updated) { update(updated); }))\n, _replyButtonManager(\n\tstd::make_unique<HistoryView::ReplyButton::Manager>(\n\t\t[=](QRect updated) { update(updated); }))\n, _touchSelectTimer([=] { onTouchSelect(); })\n, _touchScrollTimer([=] { onTouchScrollTimer(); })\n, _middleClickAutoscroll(\n\t[=](int d) { _scroll->scrollToY(_scroll->scrollTop() + d); },\n\t[=](const QCursor &cursor) { setCursor(cursor); },\n\t[=] { mouseActionUpdate(QCursor::pos()); setCursor(_cursor); },\n\t[=] { return window()->isActiveWindow(); })\n, _scrollDateCheck([this] { scrollDateCheck(); })\n, _scrollDateHideTimer([this] { scrollDateHideByTimer(); }) {\n\t_history->delegateMixin()->setCurrent(this);\n\tif (_migrated) {\n\t\t_migrated->delegateMixin()->setCurrent(this);\n\t\t_migrated->translateTo(_history->translatedTo());\n\t}\n\n\tWindow::ChatThemeValueFromPeer(\n\t\tcontroller,\n\t\t_peer\n\t) | rpl::on_next([=](std::shared_ptr<Ui::ChatTheme> &&theme) {\n\t\t_theme = std::move(theme);\n\t\tcontroller->setChatStyleTheme(_theme);\n\t}, lifetime());\n\tAssert(_theme != nullptr);\n\n\tsetAttribute(Qt::WA_AcceptTouchEvents);\n\n\trefreshAboutView();\n\n\tsetMouseTracking(true);\n\tCore::App().inAppKeyPressed(\n\t) | rpl::on_next([=] {\n\t\tregisterReadMetricsActivity();\n\t}, lifetime());\n\t_controller->gifPauseLevelChanged(\n\t) | rpl::on_next([=] {\n\t\tif (!elementAnimationsPaused()) {\n\t\t\tupdate();\n\t\t}\n\t}, lifetime());\n\n\tusing PlayRequest = ChatHelpers::EmojiInteractionPlayRequest;\n\t_controller->emojiInteractions().playRequests(\n\t) | rpl::filter([=](const PlayRequest &request) {\n\t\treturn (request.item->history() == _history)\n\t\t\t&& _controller->widget()->isActive();\n\t}) | rpl::on_next([=](PlayRequest &&request) {\n\t\tif (const auto view = viewByItem(request.item)) {\n\t\t\t_emojiInteractions->play(std::move(request), view);\n\t\t}\n\t}, lifetime());\n\t_emojiInteractions->playStarted(\n\t) | rpl::on_next([=](QString &&emoji) {\n\t\t_controller->emojiInteractions().playStarted(_peer, std::move(emoji));\n\t}, lifetime());\n\n\t_reactionsManager->chosen(\n\t) | rpl::on_next([=](ChosenReaction reaction) {\n\t\t_reactionsManager->updateButton({});\n\t\treactionChosen(reaction);\n\t}, lifetime());\n\n\tsession().data().peerDecorationsUpdated(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\tsession().data().itemRemoved(\n\t) | rpl::on_next(\n\t\t[this](auto item) { itemRemoved(item); },\n\t\tlifetime());\n\tsetupThanosEffect();\n\tsession().data().viewRemoved(\n\t) | rpl::on_next(\n\t\t[this](auto view) { viewRemoved(view); },\n\t\tlifetime());\n\trpl::merge(\n\t\tsession().data().historyUnloaded(),\n\t\tsession().data().historyCleared()\n\t) | rpl::filter([this](not_null<const History*> history) {\n\t\treturn (_history == history);\n\t}) | rpl::on_next([this] {\n\t\tmouseActionCancel();\n\t}, lifetime());\n\tsession().data().viewRepaintRequest(\n\t) | rpl::on_next([this](Data::RequestViewRepaint data) {\n\t\trepaintItem(data.view, data.rect);\n\t}, lifetime());\n\tsession().data().viewLayoutChanged(\n\t) | rpl::filter([=](not_null<const Element*> view) {\n\t\treturn (view == viewByItem(view->data()));\n\t}) | rpl::on_next([=](not_null<const Element*> view) {\n\t\tmarkReadMetricsStale();\n\t\tif (view->isUnderCursor()) {\n\t\t\tmouseActionUpdate();\n\t\t}\n\t}, lifetime());\n\n\tsession().data().itemDataChanges(\n\t) | rpl::on_next([=](not_null<HistoryItem*> item) {\n\t\tif (const auto view = viewByItem(item)) {\n\t\t\tview->itemDataChanged();\n\t\t}\n\t}, lifetime());\n\n\tsession().changes().historyUpdates(\n\t\t_history,\n\t\t(Data::HistoryUpdate::Flag::OutboxRead\n\t\t\t| Data::HistoryUpdate::Flag::TranslatedTo)\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\tHistoryView::Reactions::SetupManagerList(\n\t\t_reactionsManager.get(),\n\t\t_reactionsItem.value());\n\n\tCore::App().settings().cornerReactionValue(\n\t) | rpl::on_next([=](bool value) {\n\t\t_useCornerReaction = value;\n\t\tif (!value) {\n\t\t\t_reactionsManager->updateButton({});\n\t\t}\n\t}, lifetime());\n\n\tCore::App().settings().cornerReplyValue(\n\t) | rpl::on_next([=](bool value) {\n\t\t_useCornerReply = value;\n\t\tif (!value) {\n\t\t\t_replyButtonManager->updateButton({});\n\t\t}\n\t}, lifetime());\n\n\tcontroller->adaptive().chatWideValue(\n\t) | rpl::on_next([=](bool wide) {\n\t\t_isChatWide = wide;\n\t\tif (_overlayHost) {\n\t\t\t_overlayHost->hide();\n\t\t}\n\t}, lifetime());\n\n\t_selectScroll.scrolls(\n\t) | rpl::on_next([=](int d) {\n\t\t_scroll->scrollToY(_scroll->scrollTop() + d);\n\t}, _scroll->lifetime());\n\n\tsetupSharingDisallowed();\n\tsetupSwipeReplyAndBack();\n}\n\nvoid HistoryInner::reactionChosen(const ChosenReaction &reaction) {\n\tconst auto item = session().data().message(reaction.context);\n\tif (!item) {\n\t\treturn;\n\t} else if (reaction.id.paid()) {\n\t\tPayments::ShowPaidReactionDetails(\n\t\t\t_controller,\n\t\t\titem,\n\t\t\tviewByItem(item),\n\t\t\tHistoryReactionSource::Selector);\n\t\treturn;\n\t} else if (Window::ShowReactPremiumError(\n\t\t\t_controller,\n\t\t\titem,\n\t\t\treaction.id)) {\n\t\tif (_menu) {\n\t\t\t_menu->hideMenu();\n\t\t}\n\t\treturn;\n\t}\n\titem->toggleReaction(reaction.id, HistoryReactionSource::Selector);\n\tif (!ranges::contains(item->chosenReactions(), reaction.id)) {\n\t\treturn;\n\t} else if (const auto view = viewByItem(item)) {\n\t\tif (const auto top = itemTop(view); top >= 0) {\n\t\t\tconst auto geometry = reaction.localGeometry.isEmpty()\n\t\t\t\t? mapFromGlobal(reaction.globalGeometry)\n\t\t\t\t: reaction.localGeometry;\n\t\t\tview->animateReaction({\n\t\t\t\t.id = reaction.id,\n\t\t\t\t.flyIcon = reaction.icon,\n\t\t\t\t.flyFrom = geometry.translated(0, -top),\n\t\t\t});\n\t\t}\n\t}\n}\n\nMain::Session &HistoryInner::session() const {\n\treturn _controller->session();\n}\n\nvoid HistoryInner::setupSharingDisallowed() {\n\tExpects(_peer != nullptr);\n\n\tif (const auto user = _peer->asUser()) {\n\t\t_sharingDisallowed = rpl::combine(\n\t\t\tData::PeerFlagValue(user, UserDataFlag::NoForwardsMyEnabled),\n\t\t\tData::PeerFlagValue(user, UserDataFlag::NoForwardsPeerEnabled)\n\t\t) | rpl::map([](bool my, bool peer) {\n\t\t\treturn my || peer;\n\t\t});\n\t} else {\n\t\tconst auto chat = _peer->asChat();\n\t\tconst auto channel = _peer->asChannel();\n\t\t_sharingDisallowed = chat\n\t\t\t? Data::PeerFlagValue(chat, ChatDataFlag::NoForwards)\n\t\t\t: Data::PeerFlagValue(\n\t\t\t\tchannel,\n\t\t\t\tChannelDataFlag::NoForwards\n\t\t\t) | rpl::type_erased;\n\t}\n\n\tconst auto clearIfRestricted = [=] {\n\t\tif (hasSelectRestriction() && !getSelectedItems().empty()) {\n\t\t\t_widget->clearSelected();\n\t\t\tif (_mouseAction == MouseAction::PrepareSelect) {\n\t\t\t\tmouseActionCancel();\n\t\t\t}\n\t\t}\n\t};\n\n\tif (const auto chat = _peer->asChat()) {\n\t\tauto rights = chat->adminRightsValue();\n\t\tauto canDelete = std::move(\n\t\t\trights\n\t\t) | rpl::map([=] {\n\t\t\treturn chat->canDeleteMessages();\n\t\t});\n\t\trpl::combine(\n\t\t\t_sharingDisallowed.value(),\n\t\t\tstd::move(canDelete)\n\t\t) | rpl::on_next([=] {\n\t\t\tclearIfRestricted();\n\t\t}, lifetime());\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\tauto rights = channel->adminRightsValue();\n\t\tauto canDelete = std::move(\n\t\t\trights\n\t\t) | rpl::map([=] {\n\t\t\treturn channel->canDeleteMessages();\n\t\t});\n\t\trpl::combine(\n\t\t\t_sharingDisallowed.value(),\n\t\t\tstd::move(canDelete)\n\t\t) | rpl::on_next([=] {\n\t\t\tclearIfRestricted();\n\t\t}, lifetime());\n\t} else {\n\t\t_sharingDisallowed.value(\n\t\t) | rpl::on_next([=] {\n\t\t\tclearIfRestricted();\n\t\t}, lifetime());\n\t}\n}\n\nvoid HistoryInner::setupSwipeReplyAndBack() {\n\tif (!_peer) {\n\t\treturn;\n\t}\n\tconst auto peer = _peer;\n\n\tauto update = [=, history = _history](\n\t\t\tUi::Controls::SwipeContextData data) {\n\t\tif (data.translation > 0) {\n\t\t\tif (!_swipeBackData.callback) {\n\t\t\t\t_swipeBackData = Ui::Controls::SetupSwipeBack(\n\t\t\t\t\t_widget,\n\t\t\t\t\t[=]() -> std::pair<QColor, QColor> {\n\t\t\t\t\t\tauto context = preparePaintContext({});\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontext.st->msgServiceBg()->c,\n\t\t\t\t\t\t\tcontext.st->msgServiceFg()->c,\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t}\n\t\t\t_swipeBackData.callback(data);\n\t\t\treturn;\n\t\t} else if (_swipeBackData.lifetime) {\n\t\t\t_swipeBackData = {};\n\t\t}\n\t\tconst auto changed = (_gestureHorizontal.msgBareId != data.msgBareId)\n\t\t\t|| (_gestureHorizontal.translation != data.translation)\n\t\t\t|| (_gestureHorizontal.reachRatio != data.reachRatio);\n\t\tif (changed) {\n\t\t\t_gestureHorizontal = data;\n\t\t\tconst auto item = history->peer->owner().message(\n\t\t\t\thistory->peer->id,\n\t\t\t\tMsgId{ data.msgBareId });\n\t\t\tif (item) {\n\t\t\t\trepaintItem(item);\n\t\t\t}\n\t\t}\n\t};\n\n\tauto init = [=, show = _controller->uiShow()](\n\t\t\tint cursorTop,\n\t\t\tQt::LayoutDirection direction) {\n\t\tif (direction == Qt::RightToLeft) {\n\t\t\tauto good = true;\n\t\t\tenumerateItems<EnumItemsDirection::BottomToTop>([&](\n\t\t\t\t\tnot_null<Element*> view,\n\t\t\t\t\tint itemtop,\n\t\t\t\t\tint itembottom) {\n\t\t\t\tif (view->data()->showSimilarChannels()) {\n\t\t\t\t\tgood = false;\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t});\n\t\t\tif (good) {\n\t\t\t\treturn Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {\n\t\t\t\t\t_controller->showBackFromStack();\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\treturn Ui::Controls::SwipeHandlerFinishData();\n\t\t\t}\n\t\t}\n\t\tauto result = Ui::Controls::SwipeHandlerFinishData();\n\t\tif (inSelectionMode().inSelectionMode\n\t\t\t|| (peer->isChannel() && !peer->isMegagroup())) {\n\t\t\treturn result;\n\t\t}\n\t\tenumerateItems<EnumItemsDirection::BottomToTop>([&](\n\t\t\t\tnot_null<Element*> view,\n\t\t\t\tint itemtop,\n\t\t\t\tint itembottom) {\n\t\t\tif ((cursorTop < itemtop)\n\t\t\t\t|| (cursorTop > itembottom)\n\t\t\t\t|| !view->data()->isRegular()\n\t\t\t\t|| view->data()->showSimilarChannels()\n\t\t\t\t|| view->data()->isService()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst auto item = view->data();\n\t\t\tconst auto canSendReply = CanSendReply(item);\n\t\t\tconst auto canReply = (canSendReply || item->allowsForward());\n\t\t\tif (!canReply) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (_overlayHost) {\n\t\t\t\t_overlayHost->hide();\n\t\t\t}\n\t\t\tresult.msgBareId = item->fullId().msg.bare;\n\t\t\tresult.callback = [=, itemId = item->fullId()] {\n\t\t\t\tconst auto still = show->session().data().message(itemId);\n\t\t\t\tconst auto selected = selectedQuote(still);\n\t\t\t\tconst auto replyToItemId = (selected.item\n\t\t\t\t\t? selected.item\n\t\t\t\t\t: still)->fullId();\n\t\t\t\t_widget->replyToMessage({\n\t\t\t\t\t.messageId = replyToItemId,\n\t\t\t\t\t.quote = selected.highlight.quote,\n\t\t\t\t\t.quoteOffset = selected.highlight.quoteOffset,\n\t\t\t\t});\n\t\t\t\tif (!selected.highlight.quote.empty()) {\n\t\t\t\t\t_widget->clearSelected();\n\t\t\t\t}\n\t\t\t};\n\t\t\treturn false;\n\t\t});\n\t\treturn result;\n\t};\n\n\tUi::Controls::SetupSwipeHandler({\n\t\t.widget = this,\n\t\t.scroll = _scroll,\n\t\t.update = std::move(update),\n\t\t.init = std::move(init),\n\t\t.dontStart = _touchMaybeSelecting.value(),\n\t});\n}\n\nbool HistoryInner::hasSelectRestriction() const {\n\tif (session().frozen()) {\n\t\treturn true;\n\t} else if (!_sharingDisallowed.current()) {\n\t\treturn false;\n\t} else if (const auto chat = _peer->asChat()) {\n\t\treturn !chat->canDeleteMessages();\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\treturn !channel->canDeleteMessages();\n\t}\n\treturn true;\n}\n\nvoid HistoryInner::messagesReceived(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QVector<MTPMessage> &messages) {\n\tif (_history->peer == peer) {\n\t\t_history->addOlderSlice(messages);\n\t\tif (!messages.isEmpty()) {\n\t\t\t_translateTracker->addBunchFromBlocks();\n\t\t}\n\t} else if (_migrated && _migrated->peer == peer) {\n\t\tconst auto newLoaded = _migrated\n\t\t\t&& _migrated->isEmpty()\n\t\t\t&& !_history->isEmpty();\n\t\t_migrated->addOlderSlice(messages);\n\t\tif (newLoaded) {\n\t\t\t_migrated->addNewerSlice(QVector<MTPMessage>());\n\t\t}\n\t}\n}\n\nvoid HistoryInner::messagesReceivedDown(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QVector<MTPMessage> &messages) {\n\tif (_history->peer == peer) {\n\t\tconst auto oldLoaded = _migrated\n\t\t\t&& _history->isEmpty()\n\t\t\t&& !_migrated->isEmpty();\n\t\t_history->addNewerSlice(messages);\n\t\tif (oldLoaded) {\n\t\t\t_history->addOlderSlice(QVector<MTPMessage>());\n\t\t}\n\t} else if (_migrated && _migrated->peer == peer) {\n\t\t_migrated->addNewerSlice(messages);\n\t}\n}\n\nvoid HistoryInner::repaintItem(const HistoryItem *item) {\n\tif (const auto view = viewByItem(item)) {\n\t\trepaintItem(view);\n\t}\n}\n\nvoid HistoryInner::repaintItem(const Element *view) {\n\tif (_widget->skipItemRepaint()) {\n\t\treturn;\n\t}\n\tconst auto top = itemTop(view);\n\tif (top >= 0) {\n\t\tconst auto range = view->verticalRepaintRange();\n\t\tupdate(0, top + range.top, width(), range.height);\n\t\tconst auto id = view->data()->fullId();\n\t\tif (const auto area = _reactionsManager->lookupEffectArea(id)) {\n\t\t\tupdate(*area);\n\t\t}\n\t}\n}\n\nvoid HistoryInner::repaintItem(const Element *view, QRect rect) {\n\tif (rect.isNull()) {\n\t\treturn repaintItem(view);\n\t}\n\tif (_widget->skipItemRepaint()) {\n\t\treturn;\n\t}\n\tconst auto top = itemTop(view);\n\tif (top >= 0) {\n\t\tupdate(rect.translated(0, top));\n\t}\n}\n\ntemplate <bool TopToBottom, typename Method>\nvoid HistoryInner::enumerateItemsInHistory(History *history, int historytop, Method method) {\n\t// No displayed messages in this history.\n\tif (historytop < 0 || history->isEmpty()) {\n\t\treturn;\n\t}\n\tif (_visibleAreaBottom <= historytop || historytop + history->height() <= _visibleAreaTop) {\n\t\treturn;\n\t}\n\n\tauto collapseGapsTotal = 0;\n\tfor (const auto &gap : _collapseGaps) {\n\t\tcollapseGapsTotal += gap.height;\n\t}\n\n\tauto searchEdge = TopToBottom\n\t\t? (_visibleAreaTop - collapseGapsTotal)\n\t\t: _visibleAreaBottom;\n\n\t// Binary search for blockIndex of the first block that is not completely below the visible area.\n\tauto blockIndex = BinarySearchBlocksOrItems<TopToBottom>(history->blocks, searchEdge - historytop);\n\n\t// Binary search for itemIndex of the first item that is not completely below the visible area.\n\tauto block = history->blocks[blockIndex].get();\n\tauto blocktop = historytop + block->y();\n\tauto blockbottom = blocktop + block->height();\n\tauto itemIndex = BinarySearchBlocksOrItems<TopToBottom>(block->messages, searchEdge - blocktop);\n\n\tconst auto gapCount = int(_collapseGaps.size());\n\tauto nextGapIndex = TopToBottom ? 0 : gapCount;\n\tauto collapseShift = TopToBottom ? 0 : collapseGapsTotal;\n\n\twhile (true) {\n\t\twhile (true) {\n\t\t\tauto view = block->messages[itemIndex].get();\n\t\t\tauto logicalTop = blocktop + view->y();\n\n\t\t\tif (TopToBottom) {\n\t\t\t\twhile (nextGapIndex < gapCount) {\n\t\t\t\t\tconst auto &gap = _collapseGaps[nextGapIndex];\n\t\t\t\t\tif (logicalTop < gap.absY) break;\n\t\t\t\t\tcollapseShift += gap.height;\n\t\t\t\t\t++nextGapIndex;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twhile (nextGapIndex > 0) {\n\t\t\t\t\tconst auto &gap = _collapseGaps[nextGapIndex - 1];\n\t\t\t\t\tif (logicalTop >= gap.absY) break;\n\t\t\t\t\tcollapseShift -= gap.height;\n\t\t\t\t\t--nextGapIndex;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tauto itemtop = logicalTop + collapseShift;\n\t\t\tauto itembottom = itemtop + view->height();\n\n\t\t\tif (TopToBottom) {\n\t\t\t\tif (itembottom <= _visibleAreaTop) {\n\t\t\t\t\tif (++itemIndex >= block->messages.size()) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (itemtop >= _visibleAreaBottom) {\n\t\t\t\t\tif (--itemIndex < 0) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!method(view, itemtop, itembottom)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Skip all the items that are below / above the visible area.\n\t\t\tif (TopToBottom) {\n\t\t\t\tif (itembottom >= _visibleAreaBottom) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (itemtop <= _visibleAreaTop) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (TopToBottom) {\n\t\t\t\tif (++itemIndex >= block->messages.size()) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (--itemIndex < 0) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Skip all the rest blocks that are below / above the visible area.\n\t\tif (TopToBottom) {\n\t\t\tif (blockbottom >= _visibleAreaBottom) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else {\n\t\t\tif (blocktop <= _visibleAreaTop) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tif (TopToBottom) {\n\t\t\tif (++blockIndex >= history->blocks.size()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else {\n\t\t\tif (--blockIndex < 0) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tblock = history->blocks[blockIndex].get();\n\t\tblocktop = historytop + block->y();\n\t\tblockbottom = blocktop + block->height();\n\t\tif (TopToBottom) {\n\t\t\titemIndex = 0;\n\t\t} else {\n\t\t\titemIndex = block->messages.size() - 1;\n\t\t}\n\t}\n}\n\nbool HistoryInner::canHaveFromUserpics() const {\n\tif (_peer->isUser()\n\t\t&& !_peer->isSelf()\n\t\t&& !_peer->isRepliesChat()\n\t\t&& !_peer->isVerifyCodes()\n\t\t&& !_isChatWide) {\n\t\treturn false;\n\t} else if (const auto channel = _peer->asBroadcast()) {\n\t\treturn channel->signatureProfiles();\n\t}\n\treturn _isChatWide || !_removeFromUserpics;\n}\n\nvoid HistoryInner::toggleRemoveFromUserpics(bool remove) {\n\t_removeFromUserpics = remove;\n}\n\ntemplate <typename Method>\nvoid HistoryInner::enumerateUserpics(Method method) {\n\tif (!canHaveFromUserpics()) {\n\t\treturn;\n\t}\n\n\t// Find and remember the top of an attached messages pack\n\t// -1 means we didn't find an attached to next message yet.\n\tint lowestAttachedItemTop = -1;\n\n\tauto userpicCallback = [&](not_null<Element*> view, int itemtop, int itembottom) {\n\t\t// Skip all service messages.\n\t\tconst auto item = view->data();\n\t\tif (view->isHidden() || item->isService()) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (lowestAttachedItemTop < 0 && view->isAttachedToNext()) {\n\t\t\tlowestAttachedItemTop = itemtop + view->marginTop();\n\t\t}\n\n\t\t// Call method on a userpic for all messages that have it and for those who are not showing it\n\t\t// because of their attachment to the next message if they are bottom-most visible.\n\t\tif (view->displayFromPhoto() || (view->hasFromPhoto() && itembottom >= _visibleAreaBottom)) {\n\t\t\tif (lowestAttachedItemTop < 0) {\n\t\t\t\tlowestAttachedItemTop = itemtop + view->marginTop();\n\t\t\t}\n\t\t\t// Attach userpic to the bottom of the visible area with the same margin as the last message.\n\t\t\tauto userpicMinBottomSkip = _historyMarginBottom + st::msgMargin.bottom();\n\t\t\tauto userpicBottom = qMin(itembottom - view->marginBottom(), _visibleAreaBottom - userpicMinBottomSkip);\n\n\t\t\t// Do not let the userpic go above the attached messages pack top line.\n\t\t\tuserpicBottom = qMax(userpicBottom, lowestAttachedItemTop + st::msgPhotoSize);\n\n\t\t\t// Call the template callback function that was passed\n\t\t\t// and return if it finished everything it needed.\n\t\t\tif (!method(view, userpicBottom - st::msgPhotoSize)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\t// Forget the found top of the pack, search for the next one from scratch.\n\t\tif (!view->isAttachedToNext()) {\n\t\t\tlowestAttachedItemTop = -1;\n\t\t}\n\n\t\treturn true;\n\t};\n\n\tenumerateItems<EnumItemsDirection::TopToBottom>(userpicCallback);\n}\n\ntemplate <typename Method>\nvoid HistoryInner::enumerateDates(Method method) {\n\tauto drawtop = historyDrawTop();\n\n\t// Find and remember the bottom of an single-day messages pack\n\t// -1 means we didn't find a same-day with previous message yet.\n\tauto lowestInOneDayItemBottom = -1;\n\n\tauto dateCallback = [&](not_null<Element*> view, int itemtop, int itembottom) {\n\t\tconst auto item = view->data();\n\t\tif (lowestInOneDayItemBottom < 0 && view->isInOneDayWithPrevious()) {\n\t\t\tlowestInOneDayItemBottom = itembottom - view->marginBottom();\n\t\t}\n\n\t\t// Call method on a date for all messages that have it and for those who are not showing it\n\t\t// because they are in a one day together with the previous message if they are top-most visible.\n\t\tif (view->displayDate() || (!item->isEmpty() && itemtop <= _visibleAreaTop)) {\n\t\t\t// skip the date of history migrate item if it will be in migrated\n\t\t\tif (itemtop < drawtop && item->history() == _history) {\n\t\t\t\tif (itemtop > _visibleAreaTop) {\n\t\t\t\t\t// Previous item (from the _migrated history) is drawing date now.\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (lowestInOneDayItemBottom < 0) {\n\t\t\t\tlowestInOneDayItemBottom = itembottom - view->marginBottom();\n\t\t\t}\n\t\t\t// Attach date to the top of the visible area with the same margin as it has in service message.\n\t\t\tint dateTop = qMax(itemtop, _visibleAreaTop) + st::msgServiceMargin.top();\n\n\t\t\t// Do not let the date go below the single-day messages pack bottom line.\n\t\t\tint dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();\n\t\t\tdateTop = qMin(dateTop, lowestInOneDayItemBottom - dateHeight);\n\n\t\t\t// Call the template callback function that was passed\n\t\t\t// and return if it finished everything it needed.\n\t\t\tif (!method(view, itemtop, dateTop)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\t// Forget the found bottom of the pack, search for the next one from scratch.\n\t\tif (!view->isInOneDayWithPrevious()) {\n\t\t\tlowestInOneDayItemBottom = -1;\n\t\t}\n\n\t\treturn true;\n\t};\n\n\tenumerateItems<EnumItemsDirection::BottomToTop>(dateCallback);\n}\n\ntemplate <typename Method>\nvoid HistoryInner::enumerateForumThreadBars(Method method) {\n\tif (!_history->hasForumThreadBars()) {\n\t\treturn;\n\t}\n\n\tconst auto skip = (_scrollDateOpacity.animating() || _scrollDateShown)\n\t\t? int(base::SafeRound(\n\t\t\t(_scrollDateOpacity.value(_scrollDateShown ? 1. : 0.)\n\t\t\t\t* (st::msgServicePadding.bottom()\n\t\t\t\t\t+ st::msgServiceFont->height\n\t\t\t\t\t+ st::msgServicePadding.top()\n\t\t\t\t\t+ st::msgServiceMargin.top()))))\n\t\t: 0;\n\n\t// Find and remember the bottom of an single-day messages pack\n\t// -1 means we didn't find a same-day with previous message yet.\n\tauto lowestInOneBunchItemBottom = -1;\n\n\tauto barCallback = [&](not_null<Element*> view, int itemtop, int itembottom) {\n\t\tconst auto item = view->data();\n\t\tif (lowestInOneBunchItemBottom < 0 && view->isInOneBunchWithPrevious()) {\n\t\t\tlowestInOneBunchItemBottom = itembottom - view->marginBottom();\n\t\t}\n\n\t\t// Call method on a bar for all messages that have it and for those who are not showing it\n\t\t// because they are in a one day together with the previous message if they are top-most visible.\n\t\tif (view->displayForumThreadBar() || (!item->isEmpty() && itemtop <= _visibleAreaTop)) {\n\t\t\tif (lowestInOneBunchItemBottom < 0) {\n\t\t\t\tlowestInOneBunchItemBottom = itembottom - view->marginBottom();\n\t\t\t}\n\t\t\t// Attach bar to the top of the visible area with the same margin as it has in service message.\n\t\t\tint barTop = qMax(itemtop + view->displayedDateHeight(), _visibleAreaTop + skip) + st::msgServiceMargin.top();\n\n\t\t\t// Do not let the bar go below the single-bar messages pack bottom line.\n\t\t\tint barHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();\n\t\t\tbarTop = qMin(barTop, lowestInOneBunchItemBottom - barHeight);\n\n\t\t\t// Call the template callback function that was passed\n\t\t\t// and return if it finished everything it needed.\n\t\t\tif (!method(view, itemtop, barTop)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\t// Forget the found bottom of the pack, search for the next one from scratch.\n\t\tif (!view->isInOneBunchWithPrevious()) {\n\t\t\tlowestInOneBunchItemBottom = -1;\n\t\t}\n\n\t\treturn true;\n\t};\n\n\tenumerateItems<EnumItemsDirection::BottomToTop>(barCallback);\n}\n\nTextSelection HistoryInner::computeRenderSelection(\n\t\tnot_null<const SelectedItems*> selected,\n\t\tnot_null<Element*> view) const {\n\tif (view->isHiddenByGroup()) {\n\t\treturn TextSelection();\n\t}\n\tconst auto item = view->data();\n\tconst auto itemSelection = [&](not_null<HistoryItem*> item) {\n\t\tauto i = selected->find(item);\n\t\tif (i != selected->end()) {\n\t\t\treturn i->second;\n\t\t}\n\t\treturn TextSelection();\n\t};\n\tconst auto result = itemSelection(item);\n\tif (result != TextSelection() && result != FullSelection) {\n\t\treturn result;\n\t}\n\tif (const auto group = session().data().groups().find(item)) {\n\t\tauto parts = TextSelection();\n\t\tauto allFullSelected = true;\n\t\tconst auto count = int(group->items.size());\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tconst auto part = group->items[i];\n\t\t\tconst auto selection = itemSelection(part);\n\t\t\tif (part == item\n\t\t\t\t&& selection != FullSelection\n\t\t\t\t&& selection != TextSelection()) {\n\t\t\t\treturn selection;\n\t\t\t} else if (selection == FullSelection) {\n\t\t\t\tparts = AddGroupItemSelection(parts, i);\n\t\t\t} else {\n\t\t\t\tallFullSelected = false;\n\t\t\t}\n\t\t}\n\t\treturn allFullSelected ? FullSelection : parts;\n\t}\n\treturn itemSelection(item);\n}\n\nTextSelection HistoryInner::itemRenderSelection(\n\t\tnot_null<Element*> view,\n\t\tint selfromy,\n\t\tint seltoy) const {\n\tconst auto item = view->data();\n\tconst auto y = view->block()->y() + view->y();\n\tif (y >= selfromy && y < seltoy) {\n\t\tif (_dragSelecting && !item->isService() && item->isRegular()) {\n\t\t\treturn FullSelection;\n\t\t}\n\t} else if (!_selected.empty()) {\n\t\treturn computeRenderSelection(&_selected, view);\n\t}\n\treturn TextSelection();\n}\n\nvoid HistoryInner::paintEmpty(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tint width,\n\t\tint height) {\n\tif (!_emptyPainter) {\n\t\t_emptyPainter = std::make_unique<HistoryView::EmptyPainter>(\n\t\t\t_history);\n\t}\n\t_emptyPainter->paint(p, st, width, height);\n}\n\nUi::ChatPaintContext HistoryInner::preparePaintContext(\n\t\tconst QRect &clip) const {\n\tconst auto visibleAreaPositionGlobal = mapToGlobal(\n\t\tQPoint(0, _visibleAreaTop));\n\treturn _controller->preparePaintContext({\n\t\t.theme = _theme.get(),\n\t\t.clip = clip,\n\t\t.visibleAreaPositionGlobal = visibleAreaPositionGlobal,\n\t\t.visibleAreaTop = _visibleAreaTop,\n\t\t.visibleAreaWidth = width(),\n\t\t.visibleAreaHeight = _visibleAreaBottom - _visibleAreaTop,\n\t});\n}\n\nvoid HistoryInner::startEffectOnRead(not_null<HistoryItem*> item) {\n\tif (item->history() == _history) {\n\t\tif (const auto view = item->mainView()) {\n\t\t\t_emojiInteractions->playEffectOnRead(view);\n\t\t}\n\t}\n}\n\nvoid HistoryInner::paintEvent(QPaintEvent *e) {\n\tconst auto overlapped = _controller->contentOverlapped(this, e);\n\tconst auto pendingResized = hasPendingResizedItems();\n\t_readMetricsTracker->setScreenActive(!overlapped\n\t\t&& !pendingResized\n\t\t&& _widget->markingContentsRead());\n\tif (overlapped || pendingResized) {\n\t\treturn;\n\t} else if (_recountedAfterPendingResizedItems) {\n\t\t_recountedAfterPendingResizedItems = false;\n\t\tmouseActionUpdate();\n\t}\n\n\tPainter p(this);\n\tauto clip = e->rect();\n\n\tauto context = preparePaintContext(clip);\n\tcontext.gestureHorizontal = _gestureHorizontal;\n\tcontext.highlightPathCache = &_highlightPathCache;\n\t_pathGradient->startFrame(\n\t\t0,\n\t\twidth(),\n\t\tstd::min(st::msgMaxWidth / 2, width() / 2));\n\n\tconst auto historyDisplayedEmpty = _history->isDisplayedEmpty()\n\t\t&& (!_migrated || _migrated->isDisplayedEmpty());\n\tif (const auto view = _aboutView ? _aboutView->view() : nullptr) {\n\t\tif (clip.y() < _aboutView->top + _aboutView->height\n\t\t\t&& clip.y() + clip.height() > _aboutView->top) {\n\t\t\tconst auto top = _aboutView->top;\n\t\t\tcontext.translate(0, -top);\n\t\t\tcontext.selection = computeRenderSelection(&_selected, view);\n\t\t\tp.translate(0, top);\n\t\t\tview->draw(p, context);\n\t\t\tcontext.translate(0, top);\n\t\t\tp.translate(0, -top);\n\t\t}\n\t} else if (historyDisplayedEmpty) {\n\t\tpaintEmpty(p, context.st, width(), height());\n\t} else {\n\t\t_emptyPainter = nullptr;\n\t}\n\n\tconst auto mtop = migratedTop();\n\tconst auto htop = historyTop();\n\tif (historyDisplayedEmpty || (mtop < 0 && htop < 0)) {\n\t\treturn;\n\t}\n\n\t_translateTracker->startBunch();\n\tconst auto metricsStale = base::take(_readMetricsStale);\n\tif (metricsStale) {\n\t\t_readMetricsTracker->startBatch(_visibleAreaTop, _visibleAreaBottom);\n\t}\n\tauto readTill = (HistoryItem*)nullptr;\n\tauto readContents = base::flat_set<not_null<HistoryItem*>>();\n\tauto startEffects = base::flat_set<not_null<const Element*>>();\n\tconst auto markingAsViewed = _widget->markingContentsRead();\n\tconst auto guard = gsl::finally([&] {\n\t\tif (_pinnedItem) {\n\t\t\t_translateTracker->add(_pinnedItem);\n\t\t}\n\t\tif (metricsStale) {\n\t\t\t_readMetricsTracker->endBatch();\n\t\t}\n\t\t_translateTracker->finishBunch();\n\t\tif (!startEffects.empty()) {\n\t\t\tfor (const auto &view : startEffects) {\n\t\t\t\t_emojiInteractions->playEffectOnRead(view);\n\t\t\t}\n\t\t}\n\t\tif (readTill && _widget->markingMessagesRead()) {\n\t\t\tsession().data().histories().readInboxTill(readTill);\n\t\t}\n\t\tif (markingAsViewed && !readContents.empty()) {\n\t\t\tsession().api().markContentsRead(readContents);\n\t\t}\n\t\t_userpicsCache.clear();\n\t});\n\n\tconst auto processPainted = [&](\n\t\t\tnot_null<Element*> view,\n\t\t\tint top,\n\t\t\tint height) {\n\t\t_translateTracker->add(view);\n\t\tconst auto item = view->data();\n\t\tif (metricsStale && height > 0) {\n\t\t\t_readMetricsTracker->push(item, top, height);\n\t\t}\n\t\tconst auto isSponsored = item->isSponsored();\n\t\tconst auto isUnread = !item->out()\n\t\t\t&& item->unread(_history)\n\t\t\t&& (item->history() == _history);\n\t\tconst auto withReaction = item->hasUnreadReaction();\n\t\tconst auto yShown = [&](int y) {\n\t\t\treturn (_visibleAreaBottom >= y && _visibleAreaTop <= y);\n\t\t};\n\t\tconst auto markShown = isSponsored\n\t\t\t? view->markSponsoredViewed(_visibleAreaBottom - top)\n\t\t\t: withReaction\n\t\t\t? yShown(top + context.reactionInfo->position.y())\n\t\t\t: isUnread\n\t\t\t? yShown(top + height)\n\t\t\t: yShown(top + height / 2);\n\t\tif (markShown) {\n\t\t\tif (isSponsored) {\n\t\t\t\tsession().sponsoredMessages().view(item->fullId());\n\t\t\t} else if (isUnread) {\n\t\t\t\treadTill = item;\n\t\t\t}\n\t\t\tif (markingAsViewed && item->hasUnwatchedEffect()) {\n\t\t\t\tstartEffects.emplace(view);\n\t\t\t}\n\t\t\tif (markingAsViewed && item->hasViews()) {\n\t\t\t\tsession().api().views().scheduleIncrement(item);\n\t\t\t}\n\t\t\tif (withReaction) {\n\t\t\t\treadContents.insert(item);\n\t\t\t} else if (item->isUnreadMention()\n\t\t\t\t&& !item->isUnreadMedia()) {\n\t\t\t\treadContents.insert(item);\n\t\t\t\t_widget->enqueueMessageHighlight({ item });\n\t\t\t}\n\t\t\tif (item->hasUnreadPollVote()) {\n\t\t\t\treadContents.insert(item);\n\t\t\t}\n\t\t}\n\t\tsession().data().reactions().poll(item, context.now);\n\t\tif (item->hasUnpaidContent()) {\n\t\t\tsession().api().views().pollExtendedMedia(item);\n\t\t}\n\t\t_reactionsManager->recordCurrentReactionEffect(\n\t\t\titem->fullId(),\n\t\t\tQPoint(0, top));\n\t};\n\n\tadjustCurrent(clip.top());\n\n\tconst auto drawToY = clip.y() + clip.height();\n\n\tauto selfromy = itemTop(_dragSelFrom);\n\tauto seltoy = itemTop(_dragSelTo);\n\tif (selfromy < 0 || seltoy < 0) {\n\t\tselfromy = seltoy = -1;\n\t} else {\n\t\tseltoy += _dragSelTo->height();\n\t}\n\n\tconst auto hdrawtop = historyDrawTop();\n\tif (mtop >= 0) {\n\t\tauto iBlock = (_curHistory == _migrated ? _curBlock : (_migrated->blocks.size() - 1));\n\t\tauto block = _migrated->blocks[iBlock].get();\n\t\tauto iItem = (_curHistory == _migrated ? _curItem : (block->messages.size() - 1));\n\t\tauto view = block->messages[iItem].get();\n\t\tauto top = mtop + block->y() + view->y();\n\t\tcontext.translate(0, -top);\n\t\tp.translate(0, top);\n\t\tif (context.clip.y() < view->height()) while (top < drawToY) {\n\t\t\tconst auto height = view->height();\n\t\t\tcontext.reactionInfo\n\t\t\t\t= _reactionsManager->currentReactionPaintInfo();\n\t\t\tcontext.outbg = view->hasOutLayout();\n\t\t\tcontext.selection = itemRenderSelection(\n\t\t\t\tview,\n\t\t\t\tselfromy - mtop,\n\t\t\t\tseltoy - mtop);\n\t\t\tcontext.highlight = _widget->itemHighlight(view->data());\n\t\t\tview->draw(p, context);\n\t\t\tprocessPainted(view, top, height);\n\n\t\t\ttop += height;\n\t\t\tcontext.translate(0, -height);\n\t\t\tp.translate(0, height);\n\n\t\t\t++iItem;\n\t\t\tif (iItem == block->messages.size()) {\n\t\t\t\tiItem = 0;\n\t\t\t\t++iBlock;\n\t\t\t\tif (iBlock == _migrated->blocks.size()) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tblock = _migrated->blocks[iBlock].get();\n\t\t\t}\n\t\t\tview = block->messages[iItem].get();\n\t\t}\n\t\tcontext.translate(0, top);\n\t\tp.translate(0, -top);\n\t}\n\tif (htop >= 0) {\n\t\tauto iBlock = (_curHistory == _history ? _curBlock : 0);\n\t\tauto block = _history->blocks[iBlock].get();\n\t\tauto iItem = (_curHistory == _history ? _curItem : 0);\n\t\tauto view = block->messages[iItem].get();\n\t\tauto top = htop + block->y() + view->y();\n\n\t\tauto nextGapIndex = 0;\n\t\tauto collapseShift = 0;\n\t\tfor (; nextGapIndex < int(_collapseGaps.size()); ++nextGapIndex) {\n\t\t\tconst auto &gap = _collapseGaps[nextGapIndex];\n\t\t\tif (top < gap.absY) break;\n\t\t\tcollapseShift += gap.height;\n\t\t}\n\t\ttop += collapseShift;\n\n\t\tcontext.clip = clip.intersected(\n\t\t\tQRect(0, hdrawtop, width(), clip.top() + clip.height()));\n\t\tcontext.translate(0, -top);\n\t\tp.translate(0, top);\n\t\tconst auto &sendingAnimation = _controller->sendingAnimation();\n\t\twhile (top < drawToY) {\n\t\t\twhile (nextGapIndex < int(_collapseGaps.size())) {\n\t\t\t\tconst auto &gap = _collapseGaps[nextGapIndex];\n\t\t\t\tif (top - collapseShift < gap.absY) break;\n\t\t\t\ttop += gap.height;\n\t\t\t\tcollapseShift += gap.height;\n\t\t\t\tcontext.translate(0, -gap.height);\n\t\t\t\tp.translate(0, gap.height);\n\t\t\t\t++nextGapIndex;\n\t\t\t}\n\n\t\t\tconst auto height = view->height();\n\t\t\tconst auto item = view->data();\n\t\t\tif ((context.clip.y() < height)\n\t\t\t\t&& (hdrawtop < top + height)\n\t\t\t\t&& !sendingAnimation.hasAnimatedMessage(item)) {\n\t\t\t\tcontext.reactionInfo\n\t\t\t\t\t= _reactionsManager->currentReactionPaintInfo();\n\t\t\t\tcontext.outbg = view->hasOutLayout();\n\t\t\t\tcontext.selection = itemRenderSelection(\n\t\t\t\t\tview,\n\t\t\t\t\tselfromy - htop,\n\t\t\t\t\tseltoy - htop);\n\t\t\t\tcontext.highlight = _widget->itemHighlight(item);\n\t\t\t\tview->draw(p, context);\n\t\t\t\tprocessPainted(view, top, height);\n\t\t\t}\n\t\t\ttop += height;\n\t\t\tcontext.translate(0, -height);\n\t\t\tp.translate(0, height);\n\n\t\t\t++iItem;\n\t\t\tif (iItem == block->messages.size()) {\n\t\t\t\tiItem = 0;\n\t\t\t\t++iBlock;\n\t\t\t\tif (iBlock == _history->blocks.size()) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tblock = _history->blocks[iBlock].get();\n\t\t\t}\n\t\t\tview = block->messages[iItem].get();\n\t\t}\n\t\tcontext.translate(0, top);\n\t\tp.translate(0, -top);\n\t}\n\n\tenumerateUserpics([&](not_null<Element*> view, int userpicTop) {\n\t\t// stop the enumeration if the userpic is below the painted rect\n\t\tif (userpicTop >= clip.top() + clip.height()) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// paint the userpic if it intersects the painted rect\n\t\tif (userpicTop + st::msgPhotoSize > clip.top()) {\n\t\t\tconst auto item = view->data();\n\t\t\tconst auto hasTranslation = context.gestureHorizontal.translation\n\t\t\t\t&& (context.gestureHorizontal.msgBareId\n\t\t\t\t\t== item->fullId().msg.bare);\n\t\t\tif (hasTranslation) {\n\t\t\t\tp.translate(context.gestureHorizontal.translation, 0);\n\t\t\t\tupdate(\n\t\t\t\t\tQRect(\n\t\t\t\t\t\tst::historyPhotoLeft\n\t\t\t\t\t\t\t+ context.gestureHorizontal.translation,\n\t\t\t\t\t\tuserpicTop,\n\t\t\t\t\t\tst::msgPhotoSize\n\t\t\t\t\t\t\t- context.gestureHorizontal.translation,\n\t\t\t\t\t\tst::msgPhotoSize));\n\t\t\t}\n\t\t\tif (const auto from = item->displayFrom()) {\n\t\t\t\tDialogs::Ui::PaintUserpic(\n\t\t\t\t\tp,\n\t\t\t\t\tfrom,\n\t\t\t\t\tvalidateVideoUserpic(from),\n\t\t\t\t\t_userpics[from],\n\t\t\t\t\tst::historyPhotoLeft,\n\t\t\t\t\tuserpicTop,\n\t\t\t\t\twidth(),\n\t\t\t\t\tst::msgPhotoSize,\n\t\t\t\t\tcontext.paused);\n\t\t\t} else if (const auto info = item->displayHiddenSenderInfo()) {\n\t\t\t\tif (info->customUserpic.empty()) {\n\t\t\t\t\tinfo->emptyUserpic.paintCircle(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tst::historyPhotoLeft,\n\t\t\t\t\t\tuserpicTop,\n\t\t\t\t\t\twidth(),\n\t\t\t\t\t\tst::msgPhotoSize);\n\t\t\t\t} else {\n\t\t\t\t\tauto &userpic = _hiddenSenderUserpics[item->id];\n\t\t\t\t\tconst auto valid = info->paintCustomUserpic(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tuserpic,\n\t\t\t\t\t\tst::historyPhotoLeft,\n\t\t\t\t\t\tuserpicTop,\n\t\t\t\t\t\twidth(),\n\t\t\t\t\t\tst::msgPhotoSize);\n\t\t\t\t\tif (!valid) {\n\t\t\t\t\t\tinfo->customUserpic.load(&session(), item->fullId());\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tUnexpected(\"Corrupt forwarded information in message.\");\n\t\t\t}\n\t\t\tif (hasTranslation) {\n\t\t\t\tp.translate(-_gestureHorizontal.translation, 0);\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t});\n\n\tconst auto dateHeight = st::msgServicePadding.bottom()\n\t\t+ st::msgServiceFont->height\n\t\t+ st::msgServicePadding.top();\n\tauto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.);\n\tenumerateDates([&](not_null<Element*> view, int itemtop, int dateTop) {\n\t\t// stop the enumeration if the date is above the painted rect\n\t\tif (dateTop + dateHeight <= clip.top()) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst auto displayDate = view->displayDate();\n\t\tauto dateInPlace = displayDate;\n\t\tif (dateInPlace) {\n\t\t\tconst auto correctDateTop = itemtop + st::msgServiceMargin.top();\n\t\t\tdateInPlace = (dateTop < correctDateTop + dateHeight);\n\t\t}\n\n\t\t// paint the date if it intersects the painted rect\n\t\tif (dateTop < clip.top() + clip.height()) {\n\t\t\tauto opacity = dateInPlace ? 1. : scrollDateOpacity;\n\t\t\tif (opacity > 0.) {\n\t\t\t\tp.setOpacity(opacity);\n\t\t\t\tconst auto dateY = dateTop - st::msgServiceMargin.top();\n\t\t\t\tif (const auto date = view->Get<HistoryView::DateBadge>()) {\n\t\t\t\t\tdate->paint(p, context.st, dateY, _contentWidth, _isChatWide);\n\t\t\t\t} else {\n\t\t\t\t\tHistoryView::ServiceMessagePainter::PaintDate(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tcontext.st,\n\t\t\t\t\t\tview->dateTime(),\n\t\t\t\t\t\tdateY,\n\t\t\t\t\t\t_contentWidth,\n\t\t\t\t\t\t_isChatWide);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t});\n\tp.setOpacity(1.);\n\n\tenumerateForumThreadBars([&](not_null<Element*> view, int itemtop, int barTop) {\n\t\t// stop the enumeration if the bar is above the painted rect\n\t\tif (barTop + dateHeight <= clip.top()) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst auto displayBar = view->displayForumThreadBar();\n\t\tauto barInPlace = displayBar;\n\t\tif (barInPlace) {\n\t\t\tconst auto correctBarTop = itemtop + view->displayedDateHeight() + st::msgServiceMargin.top();\n\t\t\tbarInPlace = (barTop < correctBarTop + st::msgServiceMargin.top());\n\t\t}\n\n\t\t// paint the bar if it intersects the painted rect\n\t\tif (barTop < clip.top() + clip.height()) {\n\t\t\tconst auto barY = barTop - st::msgServiceMargin.top();\n\t\t\tif (const auto bar = view->Get<HistoryView::ForumThreadBar>()) {\n\t\t\t\tbar->paint(p, context.st, barY, _contentWidth, _isChatWide, !barInPlace);\n\t\t\t} else {\n\t\t\t\t_forumThreadBarWidth = HistoryView::ForumThreadBar::PaintForGetWidth(\n\t\t\t\t\tp,\n\t\t\t\t\tcontext.st,\n\t\t\t\t\tview,\n\t\t\t\t\t_forumThreadBarUserpicView,\n\t\t\t\t\tbarY,\n\t\t\t\t\t_contentWidth,\n\t\t\t\t\t_isChatWide);\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t});\n\n\t_replyButtonManager->paint(p, context);\n\t_reactionsManager->paint(p, context);\n}\n\nbool HistoryInner::eventHook(QEvent *e) {\n\tif (e->type() == QEvent::TouchBegin\n\t\t|| e->type() == QEvent::TouchUpdate\n\t\t|| e->type() == QEvent::TouchEnd\n\t\t|| e->type() == QEvent::TouchCancel) {\n\t\tQTouchEvent *ev = static_cast<QTouchEvent*>(e);\n\t\tif (ev->device()->type() == base::TouchDevice::TouchScreen) {\n\t\t\ttouchEvent(ev);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn RpWidget::eventHook(e);\n}\n\nint HistoryInner::SelectionViewOffset(\n\t\tnot_null<const HistoryInner*> inner,\n\t\tnot_null<const Element*> view) {\n\tif (inner->_lastInSelectionMode) {\n\t\tconst auto translation\n\t\t\t= Element::AdditionalSpaceForSelectionCheckbox(view);\n\t\tconst auto progress = inner->_inSelectionModeAnimation.value(1.);\n\t\treturn translation * progress;\n\t}\n\treturn 0;\n}\n\nHistoryInner::VideoUserpic *HistoryInner::validateVideoUserpic(\n\t\tnot_null<PeerData*> peer) {\n\tif (!peer->isPremium()\n\t\t|| peer->userpicPhotoUnknown()\n\t\t|| !peer->userpicHasVideo()) {\n\t\t_videoUserpics.remove(peer);\n\t\treturn nullptr;\n\t}\n\tconst auto i = _videoUserpics.find(peer);\n\tif (i != end(_videoUserpics)) {\n\t\treturn i->second.get();\n\t}\n\tconst auto repaint = [=] {\n\t\tif (hasPendingResizedItems()) {\n\t\t\treturn;\n\t\t}\n\t\tenumerateUserpics([&](not_null<Element*> view, int userpicTop) {\n\t\t\t// stop the enumeration if the userpic is below the painted rect\n\t\t\tif (userpicTop >= _visibleAreaBottom) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// repaint the userpic if it intersects the painted rect\n\t\t\tif (userpicTop + st::msgPhotoSize > _visibleAreaTop) {\n\t\t\t\tif (const auto from = view->data()->displayFrom()) {\n\t\t\t\t\tif (from == peer) {\n\t\t\t\t\t\trtlupdate(\n\t\t\t\t\t\t\tst::historyPhotoLeft,\n\t\t\t\t\t\t\tuserpicTop,\n\t\t\t\t\t\t\tst::msgPhotoSize,\n\t\t\t\t\t\t\tst::msgPhotoSize);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\t};\n\treturn _videoUserpics.emplace(peer, std::make_unique<VideoUserpic>(\n\t\tpeer,\n\t\trepaint\n\t)).first->second.get();\n}\n\nvoid HistoryInner::onTouchScrollTimer() {\n\tauto nowTime = crl::now();\n\tif (_touchScrollState == Ui::TouchScrollState::Acceleration\n\t\t&& _touchWaitingAcceleration\n\t\t&& (nowTime - _touchAccelerationTime) > 40) {\n\t\t_touchScrollState = Ui::TouchScrollState::Manual;\n\t\ttouchResetSpeed();\n\t} else if (_touchScrollState == Ui::TouchScrollState::Auto\n\t\t|| _touchScrollState == Ui::TouchScrollState::Acceleration) {\n\t\tint32 elapsed = int32(nowTime - _touchTime);\n\t\tQPoint delta = _touchSpeed * elapsed / 1000;\n\t\tconst auto consumedHorizontal = consumeScrollAction(delta);\n\t\tif (consumedHorizontal) {\n\t\t\t_horizontalScrollLocked = true;\n\t\t}\n\t\tconst auto hasScrolled = consumedHorizontal\n\t\t\t|| (!_horizontalScrollLocked && _widget->touchScroll(delta));\n\n\t\tif (_touchSpeed.isNull() || !hasScrolled) {\n\t\t\t_touchScrollState = Ui::TouchScrollState::Manual;\n\t\t\t_touchScroll = false;\n\t\t\t_horizontalScrollLocked = false;\n\t\t\t_touchScrollTimer.cancel();\n\t\t} else {\n\t\t\t_touchTime = nowTime;\n\t\t}\n\t\ttouchDeaccelerate(elapsed);\n\t}\n}\n\nvoid HistoryInner::touchUpdateSpeed() {\n\tconst auto nowTime = crl::now();\n\tif (_touchPrevPosValid) {\n\t\tconst int elapsed = nowTime - _touchSpeedTime;\n\t\tif (elapsed) {\n\t\t\tconst QPoint newPixelDiff = (_touchPos - _touchPrevPos);\n\t\t\tconst QPoint pixelsPerSecond = newPixelDiff * (1000 / elapsed);\n\n\t\t\t// fingers are inacurates, we ignore small changes to avoid stopping the autoscroll because\n\t\t\t// of a small horizontal offset when scrolling vertically\n\t\t\tconst int newSpeedY = (qAbs(pixelsPerSecond.y()) > Ui::kFingerAccuracyThreshold) ? pixelsPerSecond.y() : 0;\n\t\t\tconst int newSpeedX = (qAbs(pixelsPerSecond.x()) > Ui::kFingerAccuracyThreshold) ? pixelsPerSecond.x() : 0;\n\t\t\tif (_touchScrollState == Ui::TouchScrollState::Auto) {\n\t\t\t\tconst int oldSpeedY = _touchSpeed.y();\n\t\t\t\tconst int oldSpeedX = _touchSpeed.x();\n\t\t\t\tif ((oldSpeedY <= 0 && newSpeedY <= 0) || ((oldSpeedY >= 0 && newSpeedY >= 0)\n\t\t\t\t\t&& (oldSpeedX <= 0 && newSpeedX <= 0)) || (oldSpeedX >= 0 && newSpeedX >= 0)) {\n\t\t\t\t\t_touchSpeed.setY(std::clamp(\n\t\t\t\t\t\t(oldSpeedY + (newSpeedY / 4)),\n\t\t\t\t\t\t-Ui::kMaxScrollAccelerated,\n\t\t\t\t\t\t+Ui::kMaxScrollAccelerated));\n\t\t\t\t\t_touchSpeed.setX(std::clamp(\n\t\t\t\t\t\t(oldSpeedX + (newSpeedX / 4)),\n\t\t\t\t\t\t-Ui::kMaxScrollAccelerated,\n\t\t\t\t\t\t+Ui::kMaxScrollAccelerated));\n\t\t\t\t} else {\n\t\t\t\t\t_touchSpeed = QPoint();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// we average the speed to avoid strange effects with the last delta\n\t\t\t\tif (!_touchSpeed.isNull()) {\n\t\t\t\t\t_touchSpeed.setX(std::clamp(\n\t\t\t\t\t\t(_touchSpeed.x() / 4) + (newSpeedX * 3 / 4),\n\t\t\t\t\t\t-Ui::kMaxScrollFlick,\n\t\t\t\t\t\t+Ui::kMaxScrollFlick));\n\t\t\t\t\t_touchSpeed.setY(std::clamp(\n\t\t\t\t\t\t(_touchSpeed.y() / 4) + (newSpeedY * 3 / 4),\n\t\t\t\t\t\t-Ui::kMaxScrollFlick,\n\t\t\t\t\t\t+Ui::kMaxScrollFlick));\n\t\t\t\t} else {\n\t\t\t\t\t_touchSpeed = QPoint(newSpeedX, newSpeedY);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\t_touchPrevPosValid = true;\n\t}\n\t_touchSpeedTime = nowTime;\n\t_touchPrevPos = _touchPos;\n}\n\nvoid HistoryInner::touchResetSpeed() {\n\t_touchSpeed = QPoint();\n\t_touchPrevPosValid = false;\n}\n\nvoid HistoryInner::touchDeaccelerate(int32 elapsed) {\n\tint32 x = _touchSpeed.x();\n\tint32 y = _touchSpeed.y();\n\t_touchSpeed.setX((x == 0) ? x : (x > 0) ? qMax(0, x - elapsed) : qMin(0, x + elapsed));\n\t_touchSpeed.setY((y == 0) ? y : (y > 0) ? qMax(0, y - elapsed) : qMin(0, y + elapsed));\n}\n\nvoid HistoryInner::touchEvent(QTouchEvent *e) {\n\tif (e->type() == QEvent::TouchCancel) { // cancel\n\t\tif (!_touchInProgress) {\n\t\t\treturn;\n\t\t}\n\t\t_touchInProgress = false;\n\t\t_touchSelectTimer.cancel();\n\t\t_touchScroll = _touchSelect = false;\n\t\t_horizontalScrollLocked = false;\n\t\t_touchScrollState = Ui::TouchScrollState::Manual;\n\t\t_touchMaybeSelecting = false;\n\t\tmouseActionCancel();\n\t\treturn;\n\t}\n\n\tif (!e->touchPoints().isEmpty()) {\n\t\t_touchPrevPos = _touchPos;\n\t\t_touchPos = e->touchPoints().cbegin()->screenPos().toPoint();\n\t}\n\tregisterReadMetricsActivity();\n\n\tswitch (e->type()) {\n\tcase QEvent::TouchBegin: {\n\t\tif (_menu) {\n\t\t\te->accept();\n\t\t\treturn; // ignore mouse press, that was hiding context menu\n\t\t}\n\t\tif (_touchInProgress || e->touchPoints().isEmpty()) {\n\t\t\treturn;\n\t\t}\n\n\t\t_touchInProgress = true;\n\t\t_horizontalScrollLocked = false;\n\t\tif (_touchScrollState == Ui::TouchScrollState::Auto) {\n\t\t\t_touchMaybeSelecting = false;\n\t\t\t_touchScrollState = Ui::TouchScrollState::Acceleration;\n\t\t\t_touchWaitingAcceleration = true;\n\t\t\t_touchAccelerationTime = crl::now();\n\t\t\ttouchUpdateSpeed();\n\t\t\t_touchStart = _touchPos;\n\t\t} else {\n\t\t\t_touchScroll = false;\n\t\t\t_touchMaybeSelecting = true;\n\t\t\t_touchSelectTimer.callOnce(QApplication::startDragTime());\n\t\t}\n\t\t_touchSelect = false;\n\t\t_touchStart = _touchPrevPos = _touchPos;\n\t} break;\n\n\tcase QEvent::TouchUpdate: {\n\t\tif (!_touchInProgress) {\n\t\t\treturn;\n\t\t} else if (_touchSelect) {\n\t\t\tmouseActionUpdate(_touchPos);\n\t\t} else if (!_touchScroll && (_touchPos - _touchStart).manhattanLength() >= QApplication::startDragDistance()) {\n\t\t\t_touchSelectTimer.cancel();\n\t\t\t_touchMaybeSelecting = false;\n\t\t\t_touchScroll = true;\n\t\t\ttouchUpdateSpeed();\n\t\t}\n\t\tif (_touchScroll) {\n\t\t\tif (_touchScrollState == Ui::TouchScrollState::Manual) {\n\t\t\t\ttouchScrollUpdated(_touchPos);\n\t\t\t} else if (_touchScrollState == Ui::TouchScrollState::Acceleration) {\n\t\t\t\ttouchUpdateSpeed();\n\t\t\t\t_touchAccelerationTime = crl::now();\n\t\t\t\tif (_touchSpeed.isNull()) {\n\t\t\t\t\t_touchScrollState = Ui::TouchScrollState::Manual;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase QEvent::TouchEnd: {\n\t\tif (!_touchInProgress) {\n\t\t\treturn;\n\t\t}\n\t\t_touchInProgress = false;\n\t\tconst auto notMoved = (_touchPos - _touchStart).manhattanLength()\n\t\t\t< QApplication::startDragDistance();\n\t\tauto weak = base::make_weak(this);\n\t\tif (_touchSelect) {\n\t\t\tif (notMoved || _touchMaybeSelecting.current()) {\n\t\t\t\tmouseActionFinish(_touchPos, Qt::RightButton);\n\t\t\t\tauto contextMenu = QContextMenuEvent(\n\t\t\t\t\tQContextMenuEvent::Mouse,\n\t\t\t\t\tmapFromGlobal(_touchPos),\n\t\t\t\t\t_touchPos);\n\t\t\t\tshowContextMenu(&contextMenu, true);\n\t\t\t}\n\t\t\t_touchScroll = false;\n\t\t} else if (_touchScroll) {\n\t\t\tif (_touchScrollState == Ui::TouchScrollState::Manual) {\n\t\t\t\t_touchScrollState = Ui::TouchScrollState::Auto;\n\t\t\t\t_touchPrevPosValid = false;\n\t\t\t\t_touchScrollTimer.callEach(15);\n\t\t\t\t_touchTime = crl::now();\n\t\t\t} else if (_touchScrollState == Ui::TouchScrollState::Auto) {\n\t\t\t\t_touchScrollState = Ui::TouchScrollState::Manual;\n\t\t\t\t_horizontalScrollLocked = false;\n\t\t\t\t_touchScroll = false;\n\t\t\t\ttouchResetSpeed();\n\t\t\t} else if (_touchScrollState == Ui::TouchScrollState::Acceleration) {\n\t\t\t\t_touchScrollState = Ui::TouchScrollState::Auto;\n\t\t\t\t_touchWaitingAcceleration = false;\n\t\t\t\t_touchPrevPosValid = false;\n\t\t\t}\n\t\t} else if (notMoved) { // One short tap is like left mouse click.\n\t\t\tmouseActionStart(_touchPos, Qt::LeftButton);\n\t\t\tmouseActionFinish(_touchPos, Qt::LeftButton);\n\t\t}\n\t\tif (weak) {\n\t\t\t_touchSelectTimer.cancel();\n\t\t\t_touchMaybeSelecting = false;\n\t\t\t_touchSelect = false;\n\t\t}\n\t} break;\n\t}\n}\n\nvoid HistoryInner::mouseMoveEvent(QMouseEvent *e) {\n\tstatic auto lastGlobalPosition = e->globalPos();\n\tauto reallyMoved = (lastGlobalPosition != e->globalPos());\n\tif (_middleClickAutoscroll.active()) {\n\t\tif (reallyMoved) {\n\t\t\t_mouseActive = true;\n\t\t\tlastGlobalPosition = e->globalPos();\n\t\t}\n\t\te->accept();\n\t\treturn;\n\t}\n\tauto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton));\n\tif (!buttonsPressed && _mouseAction != MouseAction::None) {\n\t\tmouseReleaseEvent(e);\n\t}\n\tif (reallyMoved) {\n\t\t_mouseActive = true;\n\t\tregisterReadMetricsActivity();\n\t\tlastGlobalPosition = e->globalPos();\n\t\tif (!buttonsPressed || (_scrollDateLink && ClickHandler::getPressed() == _scrollDateLink)) {\n\t\t\tkeepScrollDateForNow();\n\t\t}\n\t}\n\tmouseActionUpdate(e->globalPos());\n}\n\nvoid HistoryInner::mouseActionUpdate(const QPoint &screenPos) {\n\t_mousePosition = screenPos;\n\tmouseActionUpdate();\n}\n\nvoid HistoryInner::touchScrollUpdated(const QPoint &screenPos) {\n\t_touchPos = screenPos;\n\tif (consumeScrollAction(_touchPos - _touchPrevPos)) {\n\t\t_horizontalScrollLocked = true;\n\t} else if (!_horizontalScrollLocked) {\n\t\t_widget->touchScroll(_touchPos - _touchPrevPos);\n\t}\n\ttouchUpdateSpeed();\n}\n\nQPoint HistoryInner::mapPointToItem(QPoint p, const Element *view) const {\n\tif (view) {\n\t\tconst auto top = itemTop(view);\n\t\tp.setY(p.y() - top);\n\t\treturn p;\n\t}\n\treturn QPoint();\n}\n\nQPoint HistoryInner::mapPointToItem(\n\t\tQPoint p,\n\t\tconst HistoryItem *item) const {\n\tif (const auto view = viewByItem(item)) {\n\t\treturn mapPointToItem(p, view);\n\t}\n\treturn QPoint();\n}\n\nvoid HistoryInner::mousePressEvent(QMouseEvent *e) {\n\tif (_menu) {\n\t\te->accept();\n\t\treturn; // ignore mouse press, that was hiding context menu\n\t}\n\tif (_overlayHost) {\n\t\t_overlayHost->handleClickOutside(e->pos());\n\t}\n\tif (_middleClickAutoscroll.active()) {\n\t\t_middleClickAutoscroll.stop();\n\t\te->accept();\n\t\treturn;\n\t}\n\tif (e->button() == Qt::MiddleButton) {\n\t\tmouseActionCancel();\n\t\tClickHandler::unpressed();\n\t\t_middleClickAutoscroll.toggleOrBeginHold(e->globalPos());\n\t\te->accept();\n\t\treturn;\n\t}\n\t_mouseActive = true;\n\tregisterReadMetricsActivity();\n\tmouseActionStart(e->globalPos(), e->button());\n}\n\nvoid HistoryInner::mouseActionStart(const QPoint &screenPos, Qt::MouseButton button) {\n\tmouseActionUpdate(screenPos);\n\tif (button != Qt::LeftButton) return;\n\n\tClickHandler::pressed();\n\tif (Element::Pressed() != Element::Hovered()) {\n\t\trepaintItem(Element::Pressed());\n\t\tElement::Pressed(Element::Hovered());\n\t\trepaintItem(Element::Pressed());\n\t}\n\n\tconst auto mouseActionView = Element::Moused();\n\t_mouseAction = MouseAction::None;\n\t_mouseActionItem = mouseActionView\n\t\t? mouseActionView->data().get()\n\t\t: nullptr;\n\t_dragStartPosition = mapPointToItem(mapFromGlobal(screenPos), mouseActionView);\n\t_pressWasInactive = Ui::WasInactivePress(_controller->widget());\n\tif (_pressWasInactive) {\n\t\tUi::MarkInactivePress(_controller->widget(), false);\n\t}\n\n\tconst auto pressed = ClickHandler::getPressed();\n\tif (pressed\n\t\t&& (!Element::Hovered()\n\t\t\t|| !Element::Hovered()->allowTextSelectionByHandler(pressed))) {\n\t\t_mouseAction = MouseAction::PrepareDrag;\n\t} else if (inSelectionMode().inSelectionMode) {\n\t\tif (_dragStateItem\n\t\t\t&& _selected.find(_dragStateItem) != _selected.cend()\n\t\t\t&& Element::Hovered()) {\n\t\t\t_mouseAction = MouseAction::PrepareDrag; // start items drag\n\t\t} else if (!_pressWasInactive) {\n\t\t\t_mouseAction = MouseAction::PrepareSelect; // start items select\n\t\t}\n\t}\n\tif (_mouseAction == MouseAction::None && mouseActionView) {\n\t\tTextState dragState;\n\t\tif (_trippleClickTimer.isActive() && (screenPos - _trippleClickPoint).manhattanLength() < QApplication::startDragDistance()) {\n\t\t\tStateRequest request;\n\t\t\trequest.flags = Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t\tdragState = mouseActionView->textState(_dragStartPosition, request);\n\t\t\tif (dragState.cursor == CursorState::Text) {\n\t\t\t\tTextSelection selStatus = { dragState.symbol, dragState.symbol };\n\t\t\t\tif (selStatus != FullSelection && (_selected.empty() || _selected.cbegin()->second != FullSelection)) {\n\t\t\t\t\tif (!_selected.empty()) {\n\t\t\t\t\t\trepaintItem(_selected.cbegin()->first);\n\t\t\t\t\t\t_selected.clear();\n\t\t\t\t\t}\n\t\t\t\t\t_selected.emplace(_mouseActionItem, selStatus);\n\t\t\t\t\t_mouseTextSymbol = dragState.symbol;\n\t\t\t\t\t_mouseAction = MouseAction::Selecting;\n\t\t\t\t\t_mouseSelectType = TextSelectType::Paragraphs;\n\t\t\t\t\tmouseActionUpdate(_mousePosition);\n\t\t\t\t\t_trippleClickTimer.callOnce(\n\t\t\t\t\t\tQApplication::doubleClickInterval());\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (Element::Pressed()) {\n\t\t\tStateRequest request;\n\t\t\trequest.flags = Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t\tdragState = mouseActionView->textState(_dragStartPosition, request);\n\t\t}\n\t\tif (_mouseSelectType != TextSelectType::Paragraphs) {\n\t\t\tif (Element::Pressed()) {\n\t\t\t\t_mouseTextSymbol = dragState.symbol;\n\t\t\t\tbool uponSelected = (dragState.cursor == CursorState::Text);\n\t\t\t\tif (uponSelected) {\n\t\t\t\t\tif (_selected.empty()\n\t\t\t\t\t\t|| _selected.cbegin()->second == FullSelection\n\t\t\t\t\t\t|| _selected.cbegin()->first != _mouseActionItem) {\n\t\t\t\t\t\tuponSelected = false;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tuint16 selFrom = _selected.cbegin()->second.from, selTo = _selected.cbegin()->second.to;\n\t\t\t\t\t\tif (_mouseTextSymbol < selFrom || _mouseTextSymbol >= selTo) {\n\t\t\t\t\t\t\tuponSelected = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (uponSelected) {\n\t\t\t\t\t_mouseAction = MouseAction::PrepareDrag; // start text drag\n\t\t\t\t} else if (!_pressWasInactive) {\n\t\t\t\t\tif (_mouseCursorState == CursorState::Date) {\n\t\t\t\t\t\t_mouseAction = MouseAction::PrepareDrag; // start sticker drag or by-date drag\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (dragState.afterSymbol) ++_mouseTextSymbol;\n\t\t\t\t\t\tTextSelection selStatus = { _mouseTextSymbol, _mouseTextSymbol };\n\t\t\t\t\t\tif (selStatus != FullSelection && (_selected.empty() || _selected.cbegin()->second != FullSelection)) {\n\t\t\t\t\t\t\tif (!_selected.empty()) {\n\t\t\t\t\t\t\t\trepaintItem(_selected.cbegin()->first);\n\t\t\t\t\t\t\t\t_selected.clear();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t_selected.emplace(_mouseActionItem, selStatus);\n\t\t\t\t\t\t\t_mouseAction = MouseAction::Selecting;\n\t\t\t\t\t\t\trepaintItem(_mouseActionItem);\n\t\t\t\t\t\t} else if (!hasSelectRestriction()) {\n\t\t\t\t\t\t\t_mouseAction = MouseAction::PrepareSelect;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (!_pressWasInactive && !hasSelectRestriction()) {\n\t\t\t\t_mouseAction = MouseAction::PrepareSelect; // start items select\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!_mouseActionItem) {\n\t\t_mouseAction = MouseAction::None;\n\t} else if (_mouseAction == MouseAction::None) {\n\t\t_mouseActionItem = nullptr;\n\t}\n}\n\nvoid HistoryInner::mouseActionCancel() {\n\t_mouseActionItem = nullptr;\n\t_dragStateItem = nullptr;\n\t_mouseAction = MouseAction::None;\n\t_dragStartPosition = QPoint(0, 0);\n\t_dragSelFrom = _dragSelTo = nullptr;\n\t_wasSelectedText = false;\n\t_selectScroll.cancel();\n}\n\nstd::unique_ptr<QMimeData> HistoryInner::prepareDrag() {\n\tif (_mouseAction != MouseAction::Dragging) {\n\t\treturn nullptr;\n\t}\n\n\tconst auto pressedHandler = ClickHandler::getPressed();\n\tif (dynamic_cast<VoiceSeekClickHandler*>(pressedHandler.get())\n\t\t|| hasCopyRestriction()) {\n\t\treturn nullptr;\n\t}\n\n\tconst auto pressedView = viewByItem(_mouseActionItem);\n\tbool uponSelected = false;\n\tif (pressedView) {\n\t\tif (!_selected.empty() && _selected.cbegin()->second == FullSelection) {\n\t\t\tuponSelected = _mouseActionItem\n\t\t\t\t&& (_selected.find(_mouseActionItem) != _selected.cend());\n\t\t} else {\n\t\t\tStateRequest request;\n\t\t\trequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t\tauto dragState = pressedView->textState(_dragStartPosition, request);\n\t\t\tuponSelected = (dragState.cursor == CursorState::Text);\n\t\t\tif (uponSelected) {\n\t\t\t\tif (_selected.empty()\n\t\t\t\t\t|| _selected.cbegin()->second == FullSelection\n\t\t\t\t\t|| _selected.cbegin()->first != _mouseActionItem) {\n\t\t\t\t\tuponSelected = false;\n\t\t\t\t} else {\n\t\t\t\t\tuint16 selFrom = _selected.cbegin()->second.from, selTo = _selected.cbegin()->second.to;\n\t\t\t\t\tif (dragState.symbol < selFrom || dragState.symbol >= selTo) {\n\t\t\t\t\t\tuponSelected = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tauto urls = QList<QUrl>();\n\tconst auto selectedText = [&] {\n\t\tif (uponSelected) {\n\t\t\treturn getSelectedText();\n\t\t} else if (pressedHandler) {\n\t\t\t//if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') {\n\t\t\t//\turls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o\n\t\t\t//}\n\t\t\treturn TextForMimeData::Simple(pressedHandler->dragText());\n\t\t}\n\t\treturn TextForMimeData();\n\t}();\n\tif (auto mimeData = TextUtilities::MimeDataFromText(selectedText)) {\n\t\tupdateDragSelection(nullptr, nullptr, false);\n\t\t_selectScroll.cancel();\n\n\t\tif (!urls.isEmpty()) {\n\t\t\tmimeData->setUrls(urls);\n\t\t}\n\t\tif (uponSelected && !_controller->adaptive().isOneColumn()) {\n\t\t\tauto selectedState = getSelectionState();\n\t\t\tif (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {\n\t\t\t\tsession().data().setMimeForwardIds(getSelectedItems());\n\t\t\t\tmimeData->setData(u\"application/x-td-forward\"_q, \"1\");\n\t\t\t}\n\t\t}\n\t\treturn mimeData;\n\t} else if (pressedView) {\n\t\tauto forwardIds = MessageIdsList();\n\t\tconst auto tryForwardSelection = uponSelected\n\t\t\t&& !_controller->adaptive().isOneColumn();\n\t\tconst auto forwardSelectionState = tryForwardSelection\n\t\t\t? getSelectionState()\n\t\t\t: HistoryView::TopBarWidget::SelectedState();\n\t\tif (forwardSelectionState.count > 0\n\t\t\t&& (forwardSelectionState.count\n\t\t\t\t== forwardSelectionState.canForwardCount)) {\n\t\t\tforwardIds = getSelectedItems();\n\t\t} else if (_mouseCursorState == CursorState::Date) {\n\t\t\tconst auto item = _mouseActionItem;\n\t\t\tif (item && item->allowsForward()) {\n\t\t\t\tforwardIds = session().data().itemOrItsGroup(item);\n\t\t\t}\n\t\t} else if ((pressedView->isHiddenByGroup() && pressedHandler)\n\t\t\t|| (pressedView->media()\n\t\t\t\t&& pressedView->media()->dragItemByHandler(pressedHandler))) {\n\t\t\tconst auto item = _dragStateItem\n\t\t\t\t? _dragStateItem\n\t\t\t\t: _mouseActionItem;\n\t\t\tif (item && item->allowsForward()) {\n\t\t\t\tforwardIds = MessageIdsList(1, item->fullId());\n\t\t\t}\n\t\t}\n\n\t\tif (pressedHandler) {\n\t\t\tconst auto lnkDocument = reinterpret_cast<DocumentData*>(\n\t\t\t\tpressedHandler->property(\n\t\t\t\t\tkDocumentLinkMediaProperty).toULongLong());\n\t\t\tif (lnkDocument) {\n\t\t\t\tconst auto filepath = lnkDocument->filepath(true);\n\t\t\t\tif (!filepath.isEmpty()) {\n\t\t\t\t\turls.push_back(QUrl::fromLocalFile(filepath));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (forwardIds.empty() && urls.isEmpty()) {\n\t\t\treturn nullptr;\n\t\t}\n\n\t\tauto result = std::make_unique<QMimeData>();\n\t\tif (!forwardIds.empty()) {\n\t\t\tsession().data().setMimeForwardIds(std::move(forwardIds));\n\t\t\tresult->setData(u\"application/x-td-forward\"_q, \"1\");\n\t\t}\n\t\tif (!urls.isEmpty()) {\n\t\t\tresult->setUrls(urls);\n\t\t}\n\t\treturn result;\n\t}\n\treturn nullptr;\n}\n\nvoid HistoryInner::performDrag() {\n\tif (auto mimeData = prepareDrag()) {\n\t\t// This call enters event loop and can destroy any QObject.\n\t\t_reactionsManager->updateButton({});\n\t\t_replyButtonManager->updateButton({});\n\t\t_controller->widget()->launchDrag(\n\t\t\tstd::move(mimeData),\n\t\t\tcrl::guard(this, [=] { mouseActionUpdate(QCursor::pos()); }));\n\t}\n}\n\nvoid HistoryInner::itemRemoved(not_null<const HistoryItem*> item) {\n\tif (_pinnedItem == item) {\n\t\t_pinnedItem = nullptr;\n\t}\n\tif (_history != item->history() && _migrated != item->history()) {\n\t\treturn;\n\t}\n\tif (_reactionsItem.current() == item) {\n\t\t_reactionsItem = nullptr;\n\t}\n\t_animatedStickersPlayed.remove(item);\n\t_reactionsManager->remove(item->fullId());\n\t_replyButtonManager->remove(item->fullId());\n\n\tauto i = _selected.find(item);\n\tif (i != _selected.cend()) {\n\t\t_selected.erase(i);\n\t\t_widget->updateTopBarSelection();\n\t}\n\n\tif (_mouseActionItem == item) {\n\t\tmouseActionCancel();\n\t}\n\tif (_dragStateItem == item) {\n\t\t_dragStateItem = nullptr;\n\t}\n\n\tif ((_dragSelFrom && _dragSelFrom->data() == item)\n\t\t|| (_dragSelTo && _dragSelTo->data() == item)) {\n\t\t_dragSelFrom = nullptr;\n\t\t_dragSelTo = nullptr;\n\t\tupdate();\n\t}\n\tif (_scrollDateLastItem && _scrollDateLastItem->data() == item) {\n\t\t_scrollDateLastItem = nullptr;\n\t}\n\tmouseActionUpdate();\n}\n\nvoid HistoryInner::viewRemoved(not_null<const Element*> view) {\n\tif (_overlayHost) {\n\t\t_overlayHost->viewGone(view);\n\t}\n\tconst auto refresh = [&](auto &saved) {\n\t\tif (saved == view) {\n\t\t\tconst auto now = viewByItem(view->data());\n\t\t\tsaved = (now && now != view) ? now : nullptr;\n\t\t}\n\t};\n\trefresh(_dragSelFrom);\n\trefresh(_dragSelTo);\n\trefresh(_scrollDateLastItem);\n}\n\nvoid HistoryInner::mouseActionFinish(\n\t\tconst QPoint &screenPos,\n\t\tQt::MouseButton button) {\n\tmouseActionUpdate(screenPos);\n\n\tauto activated = ClickHandler::unpressed();\n\tif (_mouseAction == MouseAction::Dragging) {\n\t\tactivated = nullptr;\n\t} else if (_mouseActionItem) {\n\t\t// if we are in selecting items mode perhaps we want to\n\t\t// toggle selection instead of activating the pressed link\n\t\tif (_mouseAction == MouseAction::PrepareDrag\n\t\t\t&& !_pressWasInactive\n\t\t\t&& inSelectionMode().inSelectionMode\n\t\t\t&& button != Qt::RightButton) {\n\t\t\tif (const auto view = viewByItem(_mouseActionItem)) {\n\t\t\t\tif (view->toggleSelectionByHandlerClick(activated)) {\n\t\t\t\t\tactivated = nullptr;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tconst auto pressedItemView = Element::Pressed();\n\tif (pressedItemView) {\n\t\trepaintItem(pressedItemView);\n\t\tElement::Pressed(nullptr);\n\t}\n\n\t_wasSelectedText = false;\n\n\tif (activated) {\n\t\tconst auto pressedItemId = pressedItemView\n\t\t\t? pressedItemView->data()->fullId()\n\t\t\t: _mouseActionItem\n\t\t\t? _mouseActionItem->fullId()\n\t\t\t: FullMsgId();\n\t\tconst auto weak = base::make_weak(_controller);\n\t\tmouseActionCancel();\n\t\tActivateClickHandler(\n\t\t\twindow(),\n\t\t\tactivated,\n\t\t\tprepareClickContext(button, pressedItemId));\n\t\treturn;\n\t}\n\tif ((_mouseAction == MouseAction::PrepareSelect)\n\t\t&& !_pressWasInactive\n\t\t&& inSelectionMode().inSelectionMode) {\n\t\tchangeSelectionAsGroup(\n\t\t\t&_selected,\n\t\t\t_mouseActionItem,\n\t\t\tSelectAction::Invert);\n\t\trepaintItem(_mouseActionItem);\n\t} else if ((_mouseAction == MouseAction::PrepareDrag)\n\t\t&& !_pressWasInactive\n\t\t&& _dragStateItem\n\t\t&& (button != Qt::RightButton)) {\n\t\tauto i = _selected.find(_dragStateItem);\n\t\tif (i != _selected.cend() && i->second == FullSelection) {\n\t\t\t_selected.erase(i);\n\t\t\trepaintItem(_mouseActionItem);\n\t\t} else if ((i == _selected.cend())\n\t\t\t&& !_dragStateItem->isService()\n\t\t\t&& _dragStateItem->isRegular()\n\t\t\t&& inSelectionMode().inSelectionMode) {\n\t\t\tif (_selected.size() < MaxSelectedItems) {\n\t\t\t\t_selected.emplace(_dragStateItem, FullSelection);\n\t\t\t\trepaintItem(_mouseActionItem);\n\t\t\t}\n\t\t} else if (_mouseCursorState == CursorState::Date\n\t\t\t&& !hasSelectRestriction()\n\t\t\t&& _dragStateItem->isRegular()\n\t\t\t&& !_dragStateItem->isService()) {\n\t\t\tchangeSelectionAsGroup(\n\t\t\t\t&_selected,\n\t\t\t\t_dragStateItem,\n\t\t\t\tSelectAction::Select);\n\t\t\trepaintItem(_mouseActionItem);\n\t\t} else {\n\t\t\t_selected.clear();\n\t\t\tupdate();\n\t\t}\n\t} else if (_mouseAction == MouseAction::Selecting) {\n\t\tif (_dragSelFrom && _dragSelTo) {\n\t\t\tapplyDragSelection();\n\t\t\t_dragSelFrom = _dragSelTo = nullptr;\n\t\t} else if (!_selected.empty() && !_pressWasInactive) {\n\t\t\tauto sel = _selected.cbegin()->second;\n\t\t\tif (sel != FullSelection && sel.from == sel.to) {\n\t\t\t\t_selected.clear();\n\t\t\t\t_controller->widget()->setInnerFocus();\n\t\t\t}\n\t\t}\n\t}\n\t_mouseAction = MouseAction::None;\n\t_mouseActionItem = nullptr;\n\t_mouseSelectType = TextSelectType::Letters;\n\t_selectScroll.cancel();\n\t_widget->updateTopBarSelection();\n\n\tif (QGuiApplication::clipboard()->supportsSelection()\n\t\t&& !_selected.empty()\n\t\t&& _selected.cbegin()->second != FullSelection\n\t\t&& !hasCopyRestriction(_selected.cbegin()->first)) {\n\t\tconst auto &[item, selection] = *_selected.cbegin();\n\t\tif (const auto view = viewByItem(item)) {\n\t\t\tTextUtilities::SetClipboardText(\n\t\t\t\tview->selectedText(selection),\n\t\t\t\tQClipboard::Selection);\n\t\t}\n\t}\n}\n\nvoid HistoryInner::mouseReleaseEvent(QMouseEvent *e) {\n\tif (_middleClickAutoscroll.finishHold(e->button())) {\n\t\te->accept();\n\t\treturn;\n\t}\n\tif (_middleClickAutoscroll.active()) {\n\t\te->accept();\n\t\treturn;\n\t}\n\tregisterReadMetricsActivity();\n\tmouseActionFinish(e->globalPos(), e->button());\n\tif (!rect().contains(e->pos())) {\n\t\tleaveEvent(e);\n\t}\n}\n\nvoid HistoryInner::mouseDoubleClickEvent(QMouseEvent *e) {\n\tregisterReadMetricsActivity();\n\tmouseActionStart(e->globalPos(), e->button());\n\n\tconst auto mouseActionView = viewByItem(_mouseActionItem);\n\tif (_mouseSelectType == TextSelectType::Letters\n\t\t&& mouseActionView\n\t\t&& ((_mouseAction == MouseAction::Selecting\n\t\t\t&& !_selected.empty()\n\t\t\t&& _selected.cbegin()->second != FullSelection)\n\t\t\t|| (_mouseAction == MouseAction::None\n\t\t\t\t&& (_selected.empty()\n\t\t\t\t\t|| _selected.cbegin()->second != FullSelection)))) {\n\t\tStateRequest request;\n\t\trequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\tauto dragState = mouseActionView->textState(_dragStartPosition, request);\n\t\tif (dragState.cursor == CursorState::Text) {\n\t\t\t_mouseTextSymbol = dragState.symbol;\n\t\t\t_mouseSelectType = TextSelectType::Words;\n\t\t\tif (_mouseAction == MouseAction::None) {\n\t\t\t\t_mouseAction = MouseAction::Selecting;\n\t\t\t\tTextSelection selStatus = { dragState.symbol, dragState.symbol };\n\t\t\t\tif (!_selected.empty()) {\n\t\t\t\t\trepaintItem(_selected.cbegin()->first);\n\t\t\t\t\t_selected.clear();\n\t\t\t\t}\n\t\t\t\t_selected.emplace(_mouseActionItem, selStatus);\n\t\t\t}\n\t\t\tmouseMoveEvent(e);\n\n\t\t\t_trippleClickPoint = e->globalPos();\n\t\t\t_trippleClickTimer.callOnce(QApplication::doubleClickInterval());\n\t\t}\n\t}\n\tif (!ClickHandler::getActive()\n\t\t&& !ClickHandler::getPressed()\n\t\t&& (_mouseCursorState == CursorState::None\n\t\t\t|| _mouseCursorState == CursorState::Date)\n\t\t&& !inSelectionMode().inSelectionMode\n\t\t&& !_emptyPainter\n\t\t&& e->button() == Qt::LeftButton) {\n\t\tif (const auto view = Element::Moused()) {\n\t\t\tmouseActionCancel();\n\t\t\tswitch (HistoryView::CurrentQuickAction()) {\n\t\t\tcase HistoryView::DoubleClickQuickAction::Reply: {\n\t\t\t\t_widget->replyToMessage(view->data());\n\t\t\t} break;\n\t\t\tcase HistoryView::DoubleClickQuickAction::React: {\n\t\t\t\ttoggleFavoriteReaction(view);\n\t\t\t} break;\n\t\t\tdefault: break;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid HistoryInner::toggleFavoriteReaction(not_null<Element*> view) const {\n\tconst auto item = view->data();\n\tconst auto favorite = session().data().reactions().favoriteId();\n\tif (!ranges::contains(\n\t\t\tData::LookupPossibleReactions(item).recent,\n\t\t\tfavorite,\n\t\t\t&Data::Reaction::id)\n\t\t|| Window::ShowReactPremiumError(_controller, item, favorite)) {\n\t\treturn;\n\t} else if (!ranges::contains(item->chosenReactions(), favorite)) {\n\t\tif (const auto top = itemTop(view); top >= 0) {\n\t\t\tview->animateReaction({ .id = favorite });\n\t\t}\n\t}\n\titem->toggleReaction(favorite, HistoryReactionSource::Quick);\n}\n\nHistoryView::SelectedQuote HistoryInner::selectedQuote(\n\t\tnot_null<HistoryItem*> item) const {\n\tif (_selected.size() != 1\n\t\t|| _selected.begin()->first != item\n\t\t|| _selected.begin()->second == FullSelection) {\n\t\treturn {};\n\t}\n\tconst auto view = item->mainView();\n\tif (!view) {\n\t\treturn {};\n\t}\n\treturn view->selectedQuote(_selected.begin()->second);\n}\n\nvoid HistoryInner::contextMenuEvent(QContextMenuEvent *e) {\n\tshowContextMenu(e);\n}\n\nvoid HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {\n\tif (e->reason() == QContextMenuEvent::Mouse) {\n\t\tmouseActionUpdate(e->globalPos());\n\t}\n\n\tconst auto link = ClickHandler::getActive();\n\tif (_controller->showFrozenError()) {\n\t\treturn;\n\t} else if (link\n\t\t&& !link->property(\n\t\t\tkSendReactionEmojiProperty).value<Data::ReactionId>().empty()\n\t\t&& _reactionsManager->showContextMenu(\n\t\t\tthis,\n\t\t\te,\n\t\t\tsession().data().reactions().favoriteId())) {\n\t\treturn;\n\t} else if (link && link->property(kFastShareProperty).value<bool>()) {\n\t\tif (const auto item = _dragStateItem) {\n\t\t\tconst auto view = viewByItem(item);\n\t\t\tconst auto rightSize = view->rightActionSize().value_or(QSize());\n\t\t\tconst auto reactionsSkip = view->embedReactionsInBubble()\n\t\t\t\t? 0\n\t\t\t\t: view->reactionButtonParameters({}, {}).reactionsHeight;\n\t\t\tconst auto top = itemTop(view)\n\t\t\t\t+ view->height()\n\t\t\t\t- reactionsSkip\n\t\t\t\t- _visibleAreaTop\n\t\t\t\t- rightSize.height();\n\t\t\tconst auto right = rect::right(view->innerGeometry())\n\t\t\t\t- st::historyFastShareLeft\n\t\t\t\t- rightSize.width();\n\t\t\tHistoryView::ShowTopPeersSelector(\n\t\t\t\tthis,\n\t\t\t\t_controller->uiShow(),\n\t\t\t\titem->fullId(),\n\t\t\t\tparentWidget()->mapToGlobal(QPoint(right, top)));\n\t\t\treturn;\n\t\t}\n\t}\n\tauto selectedState = getSelectionState();\n\n\t// -2 - has full selected items, but not over, -1 - has selection, but no over, 0 - no selection, 1 - over text, 2 - over full selected items\n\tauto isUponSelected = 0;\n\tauto hasSelected = 0;\n\tif (!_selected.empty()) {\n\t\tisUponSelected = -1;\n\t\tif (_selected.cbegin()->second == FullSelection) {\n\t\t\thasSelected = 2;\n\t\t\tif (_dragStateItem && _selected.find(_dragStateItem) != _selected.cend()) {\n\t\t\t\tisUponSelected = 2;\n\t\t\t} else {\n\t\t\t\tisUponSelected = -2;\n\t\t\t}\n\t\t} else if (Element::Moused()\n\t\t\t&& Element::Moused() == Element::Hovered()\n\t\t\t&& _selected.cbegin()->first == Element::Moused()->data()) {\n\t\t\tuint16 selFrom = _selected.cbegin()->second.from, selTo = _selected.cbegin()->second.to;\n\t\t\thasSelected = (selTo > selFrom) ? 1 : 0;\n\t\t\tauto mousePos = mapPointToItem(mapFromGlobal(_mousePosition), Element::Moused());\n\t\t\tStateRequest request;\n\t\t\trequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t\tauto dragState = Element::Moused()->textState(mousePos, request);\n\t\t\tif (dragState.cursor == CursorState::Text\n\t\t\t\t&& dragState.symbol >= selFrom\n\t\t\t\t&& dragState.symbol < selTo) {\n\t\t\t\tisUponSelected = 1;\n\t\t\t}\n\t\t}\n\t}\n\tif (showFromTouch && hasSelected && isUponSelected < hasSelected) {\n\t\tisUponSelected = hasSelected;\n\t}\n\n\tconst auto groupLeaderOrSelf = [](HistoryItem *item) -> HistoryItem* {\n\t\tif (!item) {\n\t\t\treturn nullptr;\n\t\t} else if (const auto group = item->history()->owner().groups().find(item)) {\n\t\t\treturn group->items.front();\n\t\t}\n\t\treturn item;\n\t};\n\tconst auto leaderOrSelf = groupLeaderOrSelf(_dragStateItem);\n\tconst auto hasWhoReactedItem = leaderOrSelf\n\t\t&& Api::WhoReactedExists(leaderOrSelf, Api::WhoReactedList::All);\n\tusing namespace HistoryView::Reactions;\n\tconst auto clickedReaction = ReactionIdOfLink(link);\n\tconst auto linkPhoneNumber = link\n\t\t? link->property(kPhoneNumberLinkProperty).toString()\n\t\t: QString();\n\tconst auto linkUserpicPeerId = (link && _dragStateUserpic)\n\t\t? link->property(kPeerLinkPeerIdProperty).toULongLong()\n\t\t: 0;\n\tconst auto todoListTaskId = link\n\t\t? link->property(kTodoListItemIdProperty).toInt()\n\t\t: 0;\n\tconst auto pollOptionLink = link\n\t\t? link->property(kPollOptionProperty).toByteArray()\n\t\t: QByteArray();\n\tconst auto session = &this->session();\n\t_whoReactedMenuLifetime.destroy();\n\tif (!clickedReaction.empty() && leaderOrSelf) {\n\t\tif (clickedReaction.paid()) {\n\t\t\tPayments::ShowPaidReactionDetails(\n\t\t\t\t_controller,\n\t\t\t\tleaderOrSelf,\n\t\t\t\tviewByItem(leaderOrSelf),\n\t\t\t\tHistoryReactionSource::Selector);\n\t\t\te->accept();\n\t\t\treturn;\n\t\t} else if (Api::WhoReactedExists(\n\t\t\t\tleaderOrSelf,\n\t\t\t\tApi::WhoReactedList::One)) {\n\t\t\tHistoryView::ShowWhoReactedMenu(\n\t\t\t\t&_menu,\n\t\t\t\te->globalPos(),\n\t\t\t\tthis,\n\t\t\t\tleaderOrSelf,\n\t\t\t\tclickedReaction,\n\t\t\t\t_controller,\n\t\t\t\t_whoReactedMenuLifetime);\n\t\t\te->accept();\n\t\t\treturn;\n\t\t} else if (HistoryView::ShowReactionPreview(\n\t\t\t\t_controller,\n\t\t\t\tleaderOrSelf->fullId(),\n\t\t\t\tclickedReaction)) {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (!linkPhoneNumber.isEmpty()) {\n\t\tPhoneClickHandler(session, linkPhoneNumber).onClick(\n\t\t\tprepareClickContext(\n\t\t\t\tQt::LeftButton,\n\t\t\t\t_dragStateItem ? _dragStateItem->fullId() : FullMsgId()));\n\t\treturn;\n\t}\n\t_menu = base::make_unique_q<Ui::PopupMenu>(this, st::popupMenuWithIcons);\n\tif (linkUserpicPeerId) {\n\t\t_widget->fillSenderUserpicMenu(\n\t\t\t_menu.get(),\n\t\t\tsession->data().peer(PeerId(linkUserpicPeerId)));\n\t\tWindow::AddSenderUserpicModerateAction(\n\t\t\t_controller,\n\t\t\t[&] {\n\t\t\t\tauto moderateItem = _dragStateItem;\n\t\t\t\tconst auto contextY = mapFromGlobal(e->globalPos()).y();\n\t\t\t\tenumerateItems<EnumItemsDirection::TopToBottom>(\n\t\t\t\t\t[&](not_null<Element*> view, int top, int bottom) {\n\t\t\t\t\t\tif (bottom <= contextY) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t} else if (top > contextY) {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst auto item = view->data();\n\t\t\t\t\t\tif (item->from()->id == PeerId(linkUserpicPeerId)) {\n\t\t\t\t\t\t\tmoderateItem = item;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t});\n\t\t\t\treturn moderateItem;\n\t\t\t}(),\n\t\t\tUi::Menu::CreateAddActionCallback(_menu.get()));\n\t\tif (_menu->empty()) {\n\t\t\t_menu = nullptr;\n\t\t} else {\n\t\t\t_menu->setForcedOrigin(Ui::PanelAnimation::Origin::BottomLeft);\n\t\t\t_menu->popup(e->globalPos());\n\t\t\te->accept();\n\t\t}\n\t\treturn;\n\t}\n\tconst auto controller = _controller;\n\tconst auto addItemActions = [&](\n\t\t\tHistoryItem *item,\n\t\t\tHistoryItem *albumPartItem) {\n\t\tif (!item\n\t\t\t|| !item->isRegular()\n\t\t\t|| isUponSelected == 2\n\t\t\t|| isUponSelected == -2) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto itemId = item->fullId();\n\t\tconst auto repliesCount = item->repliesCount();\n\t\tconst auto withReplies = (repliesCount > 0);\n\t\tconst auto topicRootId = item->history()->isForum()\n\t\t\t? item->topicRootId()\n\t\t\t: 0;\n\t\tif (topicRootId\n\t\t\t|| (withReplies && item->history()->peer->isMegagroup())) {\n\t\t\tconst auto highlightId = topicRootId ? item->id : 0;\n\t\t\tconst auto rootId = topicRootId\n\t\t\t\t? topicRootId\n\t\t\t\t: repliesCount\n\t\t\t\t? item->id\n\t\t\t\t: item->replyToTop();\n\t\t\tconst auto phrase = topicRootId\n\t\t\t\t? tr::lng_replies_view_topic(tr::now)\n\t\t\t\t: (repliesCount > 0)\n\t\t\t\t? tr::lng_replies_view(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\trepliesCount)\n\t\t\t\t: tr::lng_replies_view_thread(tr::now);\n\t\t\t_menu->addAction(phrase, [=] {\n\t\t\t\tcontroller->showRepliesForMessage(\n\t\t\t\t\t_history,\n\t\t\t\t\trootId,\n\t\t\t\t\thighlightId);\n\t\t\t}, &st::menuIconViewReplies);\n\t\t}\n\t\tconst auto t = base::unixtime::now();\n\t\tconst auto editItem = (albumPartItem && albumPartItem->allowsEdit(t))\n\t\t\t? albumPartItem\n\t\t\t: item->allowsEdit(t)\n\t\t\t? item\n\t\t\t: nullptr;\n\t\tif (editItem) {\n\t\t\tconst auto editItemId = editItem->fullId();\n\t\t\t_menu->addAction(tr::lng_context_edit_msg(tr::now), [=] {\n\t\t\t\tif (const auto item = session->data().message(editItemId)) {\n\t\t\t\t\tauto it = _selected.find(item);\n\t\t\t\t\tconst auto selection = ((it != _selected.end())\n\t\t\t\t\t\t\t&& (it->second != FullSelection))\n\t\t\t\t\t\t? it->second\n\t\t\t\t\t\t: TextSelection();\n\t\t\t\t\tif (!selection.empty()) {\n\t\t\t\t\t\tclearSelected(true);\n\t\t\t\t\t}\n\t\t\t\t\t_widget->editMessage(item, selection);\n\t\t\t\t}\n\t\t\t}, &st::menuIconEdit);\n\t\t}\n\t\tif (session->factchecks().canEdit(item)) {\n\t\t\tconst auto text = item->factcheckText();\n\t\t\tconst auto phrase = text.empty()\n\t\t\t\t? tr::lng_context_add_factcheck(tr::now)\n\t\t\t\t: tr::lng_context_edit_factcheck(tr::now);\n\t\t\t_menu->addAction(phrase, [=] {\n\t\t\t\tconst auto limit = session->factchecks().lengthLimit();\n\t\t\t\tcontroller->show(Box(EditFactcheckBox, text, limit, [=](\n\t\t\t\t\t\tTextWithEntities result) {\n\t\t\t\t\tconst auto show = controller->uiShow();\n\t\t\t\t\tsession->factchecks().save(itemId, text, result, show);\n\t\t\t\t}, FactcheckFieldIniter(controller->uiShow())));\n\t\t\t}, &st::menuIconFactcheck);\n\t\t}\n\t\tconst auto pinItem = (item->canPin() && item->isPinned())\n\t\t\t? item\n\t\t\t: groupLeaderOrSelf(item);\n\t\tif (pinItem->canPin()) {\n\t\t\tconst auto isPinned = pinItem->isPinned();\n\t\t\tconst auto pinItemId = pinItem->fullId();\n\t\t\t_menu->addAction(isPinned ? tr::lng_context_unpin_msg(tr::now) : tr::lng_context_pin_msg(tr::now), crl::guard(controller, [=] {\n\t\t\t\tWindow::ToggleMessagePinned(controller, pinItemId, !isPinned);\n\t\t\t}), isPinned ? &st::menuIconUnpin : &st::menuIconPin);\n\t\t}\n\t\tif (!item->isService()\n\t\t\t&& peerIsChannel(itemId.peer)\n\t\t\t&& !_peer->isMegagroup()) {\n\t\t\tconstexpr auto kMinViewsCount = 10;\n\t\t\tif (const auto channel = _peer->asChannel()) {\n\t\t\t\tif ((channel->flags() & ChannelDataFlag::CanGetStatistics)\n\t\t\t\t\t|| (channel->canPostMessages()\n\t\t\t\t\t\t&& item->viewsCount() >= kMinViewsCount)) {\n\t\t\t\t\tauto callback = crl::guard(controller, [=] {\n\t\t\t\t\t\tcontroller->showSection(\n\t\t\t\t\t\t\tInfo::Statistics::Make(channel, itemId, {}));\n\t\t\t\t\t});\n\t\t\t\t\t_menu->addAction(\n\t\t\t\t\t\ttr::lng_stats_title(tr::now),\n\t\t\t\t\t\tstd::move(callback),\n\t\t\t\t\t\t&st::menuIconStats);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t_menu->addAction(tr::lng_background_share(tr::now), [=] {\n\t\t\tFastShareMessage(controller, item);\n\t\t}, &st::menuIconShare);\n\t\tFork::AddReplaceMedia(_menu, item, controller);\n\t};\n\n\tconst auto msg = [=] {\n\t\tconst auto self = session->data().history(session->userPeerId());\n\t\tauto message = Api::MessageToSend(Api::SendAction(self));\n\t\tmessage.textWithTags = PrepareEditText(_dragStateItem);\n\t\treturn message;\n\t};\n\n\tconst auto groupToSaved = [=](bool deleteSelected) {\n\t\tconst auto ids = getSelectedItems();\n\t\tif (ids.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tauto toSend = Api::AsCopy::ToSend{\n\t\t\t.peers = { _peer }\n\t\t};\n\t\tconst auto items = session->data().idsToItems(ids);\n\t\tauto filtered = ranges::view::all(\n\t\t\titems\n\t\t) | ranges::view::filter([=](HistoryItem *item) -> bool {\n\t\t\treturn item->media();\n\t\t}) | ranges::to_vector;\n\t\tApi::AsCopy::UpdateFileRef(\n\t\t\titems,\n\t\t\t[=] {\n\t\t\t\tApi::AsCopy::SendAlbumFromItems(\n\t\t\t\t\tstd::move(filtered),\n\t\t\t\t\tbase::duplicate(toSend),\n\t\t\t\t\tdeleteSelected);\n\t\t\t},\n\t\t\t[=](QString a) { _controller->showToast(a); });\n\t\t_widget->clearSelected();\n\t};\n\tconst auto addSwapMediaAction = [=] {\n\t\tconst auto ids = getSelectedItems();\n\t\tconst auto items = session->data().idsToItems(ids);\n\t\tif (items.size() != 2) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto controller = _controller;\n\t\tFork::AddSwapMedia(_menu, items[0], items[1], controller, [=] {\n\t\t\t_widget->clearSelected();\n\t\t});\n\t};\n\n\n\tconst auto addPhotoActions = [&](not_null<PhotoData*> photo, HistoryItem *item) {\n\t\tconst auto media = photo->activeMediaView();\n\t\tconst auto itemId = item ? item->fullId() : FullMsgId();\n\t\tif (!photo->isNull() && media && media->loaded() && !hasCopyMediaRestriction(item)) {\n\t\t\t_menu->addAction(tr::lng_context_save_image(tr::now), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [=] {\n\t\t\t\tsavePhotoToFile(photo);\n\t\t\t}), &st::menuIconSaveImage);\n\t\t\t_menu->addAction(tr::lng_context_copy_image(tr::now), [=] {\n\t\t\t\tcopyContextImage(photo, itemId);\n\t\t\t}, &st::menuIconCopy);\n\t\t}\n\t\tif (photo->hasAttachedStickers()) {\n\t\t\t_menu->addAction(tr::lng_context_attached_stickers(tr::now), [=] {\n\t\t\t\tsession->api().attachedStickers().requestAttachedStickerSets(\n\t\t\t\t\tcontroller,\n\t\t\t\t\tphoto);\n\t\t\t}, &st::menuIconStickers);\n\t\t}\n\n\t\t_menu->addAction(tr::lng_context_to_save_messages(tr::now), [=] {\n\t\t\tApi::SendExistingPhoto(msg(), photo);\n\t\t}, &st::menuIconImportTheme);\n\t};\n\tauto rateTranscriptionItem = (HistoryItem*)(nullptr);\n\tconst auto addDocumentActions = [&](not_null<DocumentData*> document, HistoryItem *item) {\n\t\tif (document->loading()) {\n\t\t\t_menu->addAction(tr::lng_context_cancel_download(tr::now), [=] {\n\t\t\t\tcancelContextDownload(document);\n\t\t\t}, &st::menuIconCancel);\n\t\t\treturn;\n\t\t}\n\t\tconst auto itemId = item ? item->fullId() : FullMsgId();\n\t\tif (document->isGifv()) {\n\t\t\tconst auto notAutoplayedGif = [&] {\n\t\t\t\treturn item\n\t\t\t\t\t&& !Data::AutoDownload::ShouldAutoPlay(\n\t\t\t\t\t\tsession->settings().autoDownload(),\n\t\t\t\t\t\titem->history()->peer,\n\t\t\t\t\t\tdocument);\n\t\t\t}();\n\t\t\tif (notAutoplayedGif) {\n\t\t\t\t_menu->addAction(tr::lng_context_open_gif(tr::now), [=] {\n\t\t\t\t\topenContextGif(itemId);\n\t\t\t\t}, &st::menuIconShowInChat);\n\t\t\t}\n\t\t\tif (!hasCopyMediaRestriction(item)) {\n\t\t\t\t_menu->addAction(tr::lng_context_save_gif(tr::now), [=] {\n\t\t\t\t\tsaveContextGif(itemId);\n\t\t\t\t}, &st::menuIconGif);\n\t\t\t}\n\t\t}\n\t\tif (!document->filepath(true).isEmpty()) {\n\t\t\t_menu->addAction(Platform::IsMac() ? tr::lng_context_show_in_finder(tr::now) : tr::lng_context_show_in_folder(tr::now), [=] {\n\t\t\t\tshowContextInFolder(document);\n\t\t\t}, &st::menuIconShowInFolder);\n\t\t}\n\t\tif (item\n\t\t\t&& !hasCopyMediaRestriction(item)\n\t\t\t&& !HistoryView::ItemHasTtl(item)) {\n\t\t\tHistoryView::AddSaveSoundForNotifications(\n\t\t\t\t_menu,\n\t\t\t\titem,\n\t\t\t\tdocument,\n\t\t\t\tcontroller);\n\t\t\tHistoryView::AddSaveDocumentAction(\n\t\t\t\tUi::Menu::CreateAddActionCallback(_menu),\n\t\t\t\titem,\n\t\t\t\tdocument,\n\t\t\t\tcontroller);\n\t\t\tHistoryView::AddCopyFilename(\n\t\t\t\t_menu,\n\t\t\t\tdocument,\n\t\t\t\t[=] { return showCopyRestrictionForSelected(); });\n\t\t}\n\t\tif (document->hasAttachedStickers()) {\n\t\t\t_menu->addAction(tr::lng_context_attached_stickers(tr::now), [=] {\n\t\t\t\tsession->api().attachedStickers().requestAttachedStickerSets(\n\t\t\t\t\tcontroller,\n\t\t\t\t\tdocument);\n\t\t\t}, &st::menuIconStickers);\n\t\t}\n\t\tif (item\n\t\t\t&& (document->isVoiceMessage() || document->isVideoMessage())\n\t\t\t&& Menu::HasRateTranscribeItem(item)) {\n\t\t\trateTranscriptionItem = item;\n\t\t}\n\n\t\t_menu->addAction(tr::lng_context_to_save_messages(tr::now), [=] {\n\t\t\tApi::SendExistingDocument(msg(), document);\n\t\t}, &st::menuIconImportTheme);\n\t};\n\n#ifdef _DEBUG // Sometimes we need to save emoji to files.\n\tif (const auto item = _dragStateItem) {\n\t\tconst auto emojiStickers = &session->emojiStickersPack();\n\t\tif (const auto view = item->media() ? nullptr : item->mainView()) {\n\t\t\tif (const auto isolated = view->isolatedEmoji()) {\n\t\t\t\tif (const auto sticker = emojiStickers->stickerForEmoji(\n\t\t\t\t\t\tisolated)) {\n\t\t\t\t\taddDocumentActions(sticker.document, item);\n\t\t\t\t} else if (v::is<QString>(isolated.items.front())\n\t\t\t\t\t&& v::is_null(isolated.items[1])) {\n\t\t\t\t\tconst auto id = v::get<QString>(isolated.items.front());\n\t\t\t\t\tconst auto docId = id.toULongLong();\n\t\t\t\t\tconst auto document = session->data().document(docId);\n\t\t\t\t\tif (document->sticker()) {\n\t\t\t\t\t\taddDocumentActions(document, item);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n#endif\n\n\tconst auto asGroup = !Element::Moused()\n\t\t|| (Element::Moused() != Element::Hovered())\n\t\t|| (Element::Moused()->pointState(\n\t\t\tmapPointToItem(\n\t\t\t\tmapFromGlobal(_mousePosition),\n\t\t\t\tElement::Moused())\n\t\t) != HistoryView::PointState::GroupPart);\n\tconst auto addSelectMessageAction = [&](not_null<HistoryItem*> item) {\n\t\tif (item->isRegular()\n\t\t\t&& !item->isService()\n\t\t\t&& !hasSelectRestriction()) {\n\t\t\tconst auto itemId = item->fullId();\n\t\t\t_menu->addAction(tr::lng_context_select_msg(tr::now), [=] {\n\t\t\t\tif (const auto item = session->data().message(itemId)) {\n\t\t\t\t\tif ([[maybe_unused]] const auto view = viewByItem(item)) {\n\t\t\t\t\t\tif (asGroup) {\n\t\t\t\t\t\t\tchangeSelectionAsGroup(\n\t\t\t\t\t\t\t\t&_selected,\n\t\t\t\t\t\t\t\titem,\n\t\t\t\t\t\t\t\tSelectAction::Select);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tchangeSelection(&_selected, item, SelectAction::Select);\n\t\t\t\t\t\t}\n\t\t\t\t\t\trepaintItem(item);\n\t\t\t\t\t\t_widget->updateTopBarSelection();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, &st::menuIconSelect);\n\t\t\tconst auto collectBetween = [=](\n\t\t\t\t\tnot_null<HistoryItem*> from,\n\t\t\t\t\tnot_null<HistoryItem*> to,\n\t\t\t\t\tint max) -> HistoryItemsList {\n\t\t\t\tauto current = from;\n\t\t\t\tauto collected = HistoryItemsList();\n\t\t\t\tcollected.reserve(max);\n\t\t\t\tcollected.push_back(from);\n\t\t\t\tcollected.push_back(to);\n\t\t\t\tconst auto toId = to->fullId();\n\t\t\t\twhile (true) {\n\t\t\t\t\tif (collected.size() > max) {\n\t\t\t\t\t\treturn {};\n\t\t\t\t\t}\n\t\t\t\t\tconst auto view = viewByItem(current);\n\t\t\t\t\tconst auto nextView = nextItem(view);\n\t\t\t\t\tif (!nextView) {\n\t\t\t\t\t\treturn {};\n\t\t\t\t\t}\n\t\t\t\t\tconst auto nextItem = nextView->data();\n\t\t\t\t\tif (nextItem->fullId() == toId) {\n\t\t\t\t\t\treturn collected;\n\t\t\t\t\t}\n\t\t\t\t\tif (nextItem->isRegular() && !nextItem->isService()) {\n\t\t\t\t\t\tcollected.push_back(nextItem);\n\t\t\t\t\t}\n\t\t\t\t\tcurrent = nextItem;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t[&] { // Select up to this message.\n\t\t\t\tif (selectedState.count <= 0) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto toItem = groupLeaderOrSelf(item);\n\t\t\t\tauto topToBottom = false;\n\t\t\t\tauto nearestItem = (HistoryItem*)(nullptr);\n\t\t\t\t{\n\t\t\t\t\tauto minDiff = std::numeric_limits<int>::max();\n\t\t\t\t\tfor (const auto &[item, _] : _selected) {\n\t\t\t\t\t\tconst auto diff = item->fullId().msg.bare\n\t\t\t\t\t\t\t- toItem->fullId().msg.bare;\n\t\t\t\t\t\tif (std::abs(diff) < minDiff) {\n\t\t\t\t\t\t\tnearestItem = item;\n\t\t\t\t\t\t\tminDiff = std::abs(diff);\n\t\t\t\t\t\t\ttopToBottom = (diff < 0);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!nearestItem) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto start = (topToBottom ? nearestItem : toItem);\n\t\t\t\tconst auto end = (topToBottom ? toItem : nearestItem);\n\t\t\t\tconst auto left = MaxSelectedItems\n\t\t\t\t\t- selectedState.count\n\t\t\t\t\t+ (topToBottom ? 0 : 1);\n\t\t\t\tif (collectBetween(start, end, left).empty()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto startId = start->fullId();\n\t\t\t\tconst auto endId = end->fullId();\n\t\t\t\tconst auto callback = [=] {\n\t\t\t\t\tconst auto from = session->data().message(startId);\n\t\t\t\t\tconst auto to = session->data().message(endId);\n\t\t\t\t\tif (from && to) {\n\t\t\t\t\t\tfor (const auto &i : collectBetween(from, to, left)) {\n\t\t\t\t\t\t\tchangeSelectionAsGroup(\n\t\t\t\t\t\t\t\t&_selected,\n\t\t\t\t\t\t\t\ti,\n\t\t\t\t\t\t\t\tSelectAction::Select);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tupdate();\n\t\t\t\t\t\t_widget->updateTopBarSelection();\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\t_menu->addAction(\n\t\t\t\t\ttr::lng_context_select_msg_bulk(tr::now),\n\t\t\t\t\tcallback,\n\t\t\t\t\t&st::menuIconSelect);\n\t\t\t}();\n\t\t}\n\t};\n\n\tconst auto addReplyAction = [&](HistoryItem *item) {\n\t\tif (!item || !item->isRegular()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto canSendReply = CanSendReply(item);\n\t\tconst auto canReply = canSendReply || item->allowsForward();\n\t\tif (canReply) {\n\t\t\tconst auto selected = selectedQuote(item);\n\t\t\tauto text = (selected\n\t\t\t\t? tr::lng_context_quote_and_reply\n\t\t\t\t: todoListTaskId\n\t\t\t\t? tr::lng_context_reply_to_task\n\t\t\t\t: tr::lng_context_reply_msg)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tUi::Text::FixAmpersandInAction);\n\t\t\tconst auto replyToItem = selected.item ? selected.item : item;\n\t\t\tconst auto itemId = replyToItem->fullId();\n\t\t\t_menu->addAction(std::move(text), [=] {\n\t\t\t\t_widget->replyToMessage({\n\t\t\t\t\t.messageId = itemId,\n\t\t\t\t\t.quote = selected.highlight.quote,\n\t\t\t\t\t.quoteOffset = selected.highlight.quoteOffset,\n\t\t\t\t\t.todoItemId = todoListTaskId,\n\t\t\t\t});\n\t\t\t\tif (!selected.highlight.quote.empty()) {\n\t\t\t\t\t_widget->clearSelected();\n\t\t\t\t}\n\t\t\t}, &st::menuIconReply);\n\t\t\tconst auto media = item->media();\n\t\t\tconst auto document = media\n\t\t\t\t? media->document()\n\t\t\t\t: nullptr;\n\t\t\tif (canSendReply && document && document->isVoiceMessage()) {\n\t\t\t\tconst auto msgId = item->fullId();\n\t\t\t\tif (const auto t = HistoryView::CurrentVoiceTimecode(msgId)) {\n\t\t\t\t\tMenu::AddTimecodeAction(\n\t\t\t\t\t\t_menu.get(),\n\t\t\t\t\t\t*t,\n\t\t\t\t\t\tHistoryView::VoiceTimecodeUpdates(msgId),\n\t\t\t\t\t\t[=] {\n\t\t\t\t\t\t\tconst auto cur\n\t\t\t\t\t\t\t\t= HistoryView::CurrentVoiceTimecode(msgId);\n\t\t\t\t\t\t\t_widget->replyToMessage({ .messageId = msgId });\n\t\t\t\t\t\t\t_widget->insertTextAtCursor(cur.value_or(*t));\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\tconst auto addTodoListAction = [&](HistoryItem *item) {\n\t\tif (!item || !Window::PeerMenuShowAddTodoListTasks(item)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto itemId = item->fullId();\n\t\t_menu->addAction(\n\t\t\ttr::lng_todo_add_title(tr::now),\n\t\t\tcrl::guard(this, [=] {\n\t\t\t\tif (const auto item = session->data().message(itemId)) {\n\t\t\t\t\tWindow::PeerMenuAddTodoListTasks(_controller, item);\n\t\t\t\t}\n\t\t\t}),\n\t\t\t&st::menuIconAdd);\n\t};\n\tconst auto lnkPhoto = link\n\t\t? reinterpret_cast<PhotoData*>(\n\t\t\tlink->property(kPhotoLinkMediaProperty).toULongLong())\n\t\t: nullptr;\n\tconst auto lnkDocument = link\n\t\t? reinterpret_cast<DocumentData*>(\n\t\t\tlink->property(kDocumentLinkMediaProperty).toULongLong())\n\t\t: nullptr;\n\tif (lnkPhoto || lnkDocument) {\n\t\tconst auto item = _dragStateItem;\n\t\tconst auto itemId = item ? item->fullId() : FullMsgId();\n\t\taddReplyAction(item);\n\n\t\tif (isUponSelected > 0) {\n\t\t\tconst auto selectedText = getSelectedText();\n\t\t\tif (!hasCopyRestrictionForSelected()\n\t\t\t\t&& !selectedText.empty()) {\n\t\t\t\tauto owned = base::make_unique_q<Ui::Menu::Action>(\n\t\t\t\t\t_menu->menu(),\n\t\t\t\t\t_menu->st().menu,\n\t\t\t\t\tUi::Menu::CreateAction(\n\t\t\t\t\t\t_menu,\n\t\t\t\t\t\t(isUponSelected > 1\n\t\t\t\t\t\t\t? tr::lng_context_copy_selected_items(tr::now)\n\t\t\t\t\t\t\t: tr::lng_context_copy_selected(tr::now)),\n\t\t\t\t\t\t[=] { copySelectedText(); }),\n\t\t\t\t\t&st::menuIconCopy,\n\t\t\t\t\t&st::menuIconCopy);\n\t\t\t\taddDepersonalized(owned.get());\n\t\t\t\t_menu->addAction(std::move(owned));\n\t\t\t}\n\t\t\tif (item && !Ui::SkipTranslate(selectedText.rich)) {\n\t\t\t\tconst auto peer = item->history()->peer;\n\t\t\t\t_menu->addAction(tr::lng_context_translate_selected({}), [=] {\n\t\t\t\t\t_controller->show(Box(\n\t\t\t\t\t\tUi::TranslateBox,\n\t\t\t\t\t\tpeer,\n\t\t\t\t\t\tMsgId(),\n\t\t\t\t\t\tgetSelectedText().rich,\n\t\t\t\t\t\thasCopyRestrictionForSelected()));\n\t\t\t\t}, &st::menuIconTranslate);\n\t\t\t}\n\t\t\taddSearchAction();\n\t\t}\n\t\taddItemActions(item, item);\n\t\tif (!selectedState.count) {\n\t\t\tif (lnkPhoto) {\n\t\t\t\taddPhotoActions(lnkPhoto, item);\n\t\t\t} else {\n\t\t\t\taddDocumentActions(lnkDocument, item);\n\t\t\t}\n\t\t}\n\t\tif (item && item->hasDirectLink() && isUponSelected != 2 && isUponSelected != -2) {\n\t\t\t_menu->addAction(item->history()->peer->isMegagroup() ? tr::lng_context_copy_message_link(tr::now) : tr::lng_context_copy_post_link(tr::now), [=] {\n\t\t\t\tHistoryView::CopyPostLink(controller, itemId, HistoryView::Context::History);\n\t\t\t}, &st::menuIconLink);\n\t\t}\n\t\tif (isUponSelected > 1) {\n\t\t\tif (selectedState.count > 0 && selectedState.canForwardCount == selectedState.count) {\n\t\t\t\t_menu->addAction(tr::lng_context_forward_selected(tr::now), [=] {\n\t\t\t\t\t_widget->forwardSelected();\n\t\t\t\t}, &st::menuIconForward);\n\t\t\t}\n\t\t\tif (selectedState.count > 0 && selectedState.canDeleteCount == selectedState.count) {\n\t\t\t\t_menu->addAction(tr::lng_context_delete_selected(tr::now), [=] {\n\t\t\t\t\t_widget->confirmDeleteSelected();\n\t\t\t\t}, &st::menuIconDelete);\n\t\t\t}\n\t\t\tif (selectedState.count > 1 && selectedState.count <= 10) {\n\t\t\t\tFork::AddGroupSelected(_menu, groupToSaved);\n\t\t\t}\n\t\t\tif (selectedState.count == 2) {\n\t\t\t\taddSwapMediaAction();\n\t\t\t}\n\t\t\tif (selectedState.count > 0 && !hasCopyRestrictionForSelected()) {\n\t\t\t\tMenu::AddDownloadFilesAction(_menu, controller, _selected, this);\n\t\t\t}\n\t\t\t_menu->addAction(tr::lng_context_clear_selection(tr::now), [=] {\n\t\t\t\t_widget->clearSelected();\n\t\t\t}, &st::menuIconSelect);\n\t\t\tFork::AddShowSumDurations(_menu, _selected, controller);\n\t\t} else if (item) {\n\t\t\tconst auto itemId = item->fullId();\n\t\t\tconst auto blockSender = item->history()->peer->isRepliesChat();\n\t\t\tif (isUponSelected != -2) {\n\t\t\t\tif (item->allowsForward()) {\n\t\t\t\t\t_menu->addAction(tr::lng_context_forward_msg(tr::now), [=] {\n\t\t\t\t\t\tforwardItem(itemId);\n\t\t\t\t\t}, &st::menuIconForward);\n\t\t\t\t}\n\t\t\t\tif (HistoryView::CanAddOfferToMessage(item)) {\n\t\t\t\t\t_menu->addAction(tr::lng_context_add_offer(tr::now), [=] {\n\t\t\t\t\t\tApi::AddOfferToMessage(_controller->uiShow(), itemId);\n\t\t\t\t\t}, &st::menuIconTagSell);\n\t\t\t\t}\n\t\t\t\tif (item->canDelete()) {\n\t\t\t\t\tconst auto callback = [=] { deleteItem(itemId); };\n\t\t\t\t\tif (item->isUploading()) {\n\t\t\t\t\t\tif (item->media()\n\t\t\t\t\t\t\t&& item->media()->allowsEditCaption()) {\n\t\t\t\t\t\t\t_menu->addAction(\n\t\t\t\t\t\t\t\ttr::lng_context_upload_edit_caption(tr::now),\n\t\t\t\t\t\t\t\t[=] { editCaptionUploadLayer(item); },\n\t\t\t\t\t\t\t\t&st::menuIconEdit);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t_menu->addAction(tr::lng_context_cancel_upload(tr::now), callback, &st::menuIconCancel);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t_menu->addAction(Ui::DeleteMessageContextAction(\n\t\t\t\t\t\t\t_menu->menu(),\n\t\t\t\t\t\t\tcallback,\n\t\t\t\t\t\t\titem->ttlDestroyAt(),\n\t\t\t\t\t\t\t[=] { _menu = nullptr; }));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!blockSender && item->suggestReport()) {\n\t\t\t\t\t_menu->addAction(tr::lng_context_report_msg(tr::now), [=] {\n\t\t\t\t\t\treportItem(itemId);\n\t\t\t\t\t}, &st::menuIconReport);\n\t\t\t\t}\n\t\t\t}\n\t\t\taddSelectMessageAction(item);\n\t\t\tif (isUponSelected != -2 && blockSender) {\n\t\t\t\t_menu->addAction(tr::lng_profile_block_user(tr::now), [=] {\n\t\t\t\t\tblockSenderItem(itemId);\n\t\t\t\t}, &st::menuIconBlock);\n\t\t\t}\n\t\t}\n\t} else { // maybe cursor on some text history item?\n\t\tconst auto albumPartItem = _dragStateItem;\n\t\tconst auto item = [&] {\n\t\t\tconst auto result = Element::Hovered()\n\t\t\t\t? Element::Hovered()->data().get()\n\t\t\t\t: Element::HoveredLink()\n\t\t\t\t? Element::HoveredLink()->data().get()\n\t\t\t\t: nullptr;\n\t\t\treturn result ? groupLeaderOrSelf(result) : nullptr;\n\t\t}();\n\t\tconst auto partItemOrLeader = (asGroup || !albumPartItem)\n\t\t\t? item\n\t\t\t: albumPartItem;\n\t\tconst auto itemId = item ? item->fullId() : FullMsgId();\n\t\tconst auto canDelete = item\n\t\t\t&& item->canDelete()\n\t\t\t&& (item->isRegular() || !item->isService());\n\t\tconst auto canForward = item && item->allowsForward();\n\t\tconst auto canReport = item && item->suggestReport();\n\t\tconst auto canBlockSender = item && item->history()->peer->isRepliesChat();\n\t\tconst auto view = viewByItem(item);\n\t\tconst auto actionText = link\n\t\t\t? link->copyToClipboardContextItemText()\n\t\t\t: QString();\n\n\t\tconst auto sponsored = (item && item->isSponsored())\n\t\t\t? item\n\t\t\t: (Element::Moused() && Element::Moused()->data()->isSponsored())\n\t\t\t? Element::Moused()->data().get()\n\t\t\t: nullptr;\n\t\tif (sponsored) {\n\t\t\tMenu::FillSponsored(\n\t\t\t\tUi::Menu::CreateAddActionCallback(_menu),\n\t\t\t\tcontroller->uiShow(),\n\t\t\t\tsponsored->fullId());\n\t\t}\n\t\tif (isUponSelected > 0) {\n\t\t\taddReplyAction(item);\n\t\t\t\tconst auto selectedText = getSelectedText();\n\t\t\tif (!hasCopyRestrictionForSelected() && !selectedText.empty()) {\n\t\t\t\tauto owned = base::make_unique_q<Ui::Menu::Action>(\n\t\t\t\t\t_menu->menu(),\n\t\t\t\t\t_menu->st().menu,\n\t\t\t\t\tUi::Menu::CreateAction(\n\t\t\t\t\t\t_menu,\n\t\t\t\t\t\t((isUponSelected > 1)\n\t\t\t\t\t\t\t? tr::lng_context_copy_selected_items(tr::now)\n\t\t\t\t\t\t\t: tr::lng_context_copy_selected(tr::now)),\n\t\t\t\t\t\t[=] { copySelectedText(); }),\n\t\t\t\t\t&st::menuIconCopy,\n\t\t\t\t\t&st::menuIconCopy);\n\t\t\t\taddDepersonalized(owned.get());\n\t\t\t\t_menu->addAction(std::move(owned));\n\t\t\t}\n\t\t\tif (item && !Ui::SkipTranslate(selectedText.rich)) {\n\t\t\t\tconst auto peer = item->history()->peer;\n\t\t\t\t_menu->addAction(tr::lng_context_translate_selected({}), [=] {\n\t\t\t\t\t_controller->show(Box(\n\t\t\t\t\t\tUi::TranslateBox,\n\t\t\t\t\t\tpeer,\n\t\t\t\t\t\tMsgId(),\n\t\t\t\t\t\tselectedText.rich,\n\t\t\t\t\t\thasCopyRestrictionForSelected()));\n\t\t\t\t}, &st::menuIconTranslate);\n\t\t\t}\n\t\t\taddItemActions(item, item);\n\t\t\taddSearchAction();\n\t\t} else {\n\t\t\taddReplyAction(partItemOrLeader);\n\t\t\taddTodoListAction(partItemOrLeader);\n\t\t\taddItemActions(item, albumPartItem);\n\t\t\tif (item && !isUponSelected) {\n\t\t\t\tconst auto media = (view ? view->media() : nullptr);\n\t\t\t\tconst auto mediaHasTextForCopy = media && media->hasTextForCopy();\n\t\t\t\tif (const auto document = media ? media->getDocument() : nullptr) {\n\t\t\t\t\tif (!view->isIsolatedEmoji() && document->sticker()) {\n\t\t\t\t\t\tif (document->sticker()->set) {\n\t\t\t\t\t\t\t_menu->addAction(document->isStickerSetInstalled() ? tr::lng_context_pack_info(tr::now) : tr::lng_context_pack_add(tr::now), [=] {\n\t\t\t\t\t\t\t\tshowStickerPackInfo(document);\n\t\t\t\t\t\t\t}, &st::menuIconStickers);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tconst auto isFaved = session->data().stickers().isFaved(document);\n\t\t\t\t\t\t\t_menu->addAction(isFaved ? tr::lng_faved_stickers_remove(tr::now) : tr::lng_faved_stickers_add(tr::now), [=] {\n\t\t\t\t\t\t\t\tApi::ToggleFavedSticker(controller->uiShow(), document, itemId);\n\t\t\t\t\t\t\t}, isFaved ? &st::menuIconUnfave : &st::menuIconFave);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!hasCopyMediaRestriction(item)) {\n\t\t\t\t\t\t\t_menu->addAction(tr::lng_context_save_image(tr::now), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [=] {\n\t\t\t\t\t\t\t\tsaveDocumentToFile(itemId, document);\n\t\t\t\t\t\t\t}), &st::menuIconDownload);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (const auto media = item->media()) {\n\t\t\t\t\tif (const auto poll = media->poll()) {\n\t\t\t\t\t\tHistoryView::AddPollActions(\n\t\t\t\t\t\t\t_menu,\n\t\t\t\t\t\t\tpoll,\n\t\t\t\t\t\t\titem,\n\t\t\t\t\t\t\tHistoryView::Context::History,\n\t\t\t\t\t\t\t_controller,\n\t\t\t\t\t\t\t!pollOptionLink.isEmpty());\n\t\t\t\t\t} else if (const auto contact = media->sharedContact()) {\n\t\t\t\t\t\tconst auto phone = contact->phoneNumber;\n\t\t\t\t\t\t_menu->addAction(tr::lng_profile_copy_phone(tr::now), [=] {\n\t\t\t\t\t\t\tQGuiApplication::clipboard()->setText(phone);\n\t\t\t\t\t\t}, &st::menuIconCopy);\n\t\t\t\t\t} else if (const auto gift = media->gift()) {\n\t\t\t\t\t\tconst auto peer = item->history()->peer;\n\t\t\t\t\t\tconst auto user = peer->asUser();\n\t\t\t\t\t\tif (!user\n\t\t\t\t\t\t\t|| (!user->isInaccessible()\n\t\t\t\t\t\t\t\t&& !user->isNotificationsUser())) {\n\t\t\t\t\t\t\tconst auto controller = _controller;\n\t\t\t\t\t\t\tconst auto starGiftUpgrade = gift->upgrade\n\t\t\t\t\t\t\t\t&& (gift->type == Data::GiftType::StarGift);\n\t\t\t\t\t\t\tconst auto isGift = gift->slug.isEmpty()\n\t\t\t\t\t\t\t\t|| !gift->channel;\n\t\t\t\t\t\t\tconst auto out = item->out();\n\t\t\t\t\t\t\tconst auto outgoingGift = isGift\n\t\t\t\t\t\t\t\t&& (starGiftUpgrade ? !out : out);\n\t\t\t\t\t\t\tif (outgoingGift\n\t\t\t\t\t\t\t\t&& gift->type\n\t\t\t\t\t\t\t\t\t!= Data::GiftType::BirthdaySuggest) {\n\t\t\t\t\t\t\t\t_menu->addAction(\n\t\t\t\t\t\t\t\t\ttr::lng_context_gift_send(tr::now),\n\t\t\t\t\t\t\t\t\t[=] {\n\t\t\t\t\t\t\t\t\t\tUi::ShowStarGiftBox(controller, peer);\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t&st::menuIconGiftPremium);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (!rateTranscriptionItem && media->document()) {\n\t\t\t\t\t\tif ((media->document()->isVoiceMessage()\n\t\t\t\t\t\t\t\t|| media->document()->isVideoMessage())\n\t\t\t\t\t\t\t&& Menu::HasRateTranscribeItem(item)) {\n\t\t\t\t\t\t\trateTranscriptionItem = item;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!item->isService() && view && actionText.isEmpty()) {\n\t\t\t\t\tconst auto hasRestriction = hasCopyRestriction(item);\n\t\t\t\t\tif (!hasRestriction\n\t\t\t\t\t\t&& (view->hasVisibleText() || mediaHasTextForCopy)) {\n\t\t\t\t\t\t_menu->addAction(\n\t\t\t\t\t\t\ttr::lng_context_copy_text(tr::now),\n\t\t\t\t\t\t\t[=] { copyContextText(itemId); },\n\t\t\t\t\t\t\t&st::menuIconCopy);\n\t\t\t\t\t}\n\t\t\t\t\tif ((!item->translation() || !_history->translatedTo())\n\t\t\t\t\t\t&& (view->hasVisibleText() || mediaHasTextForCopy)) {\n\t\t\t\t\t\tconst auto peer = item->history()->peer;\n\t\t\t\t\t\tconst auto itemId = item->id;\n\t\t\t\t\t\tconst auto translate = mediaHasTextForCopy\n\t\t\t\t\t\t\t? (HistoryView::TransribedText(item)\n\t\t\t\t\t\t\t\t.append('\\n')\n\t\t\t\t\t\t\t\t.append(item->originalText()))\n\t\t\t\t\t\t\t: item->originalText();\n\t\t\t\t\t\tif (!translate.text.isEmpty()\n\t\t\t\t\t\t\t&& !Ui::SkipTranslate(translate)) {\n\t\t\t\t\t\t\t_menu->addAction(tr::lng_context_translate(tr::now), [=] {\n\t\t\t\t\t\t\t\t_controller->show(Box(\n\t\t\t\t\t\t\t\t\tUi::TranslateBox,\n\t\t\t\t\t\t\t\t\tpeer,\n\t\t\t\t\t\t\t\t\tmediaHasTextForCopy ? MsgId() : itemId,\n\t\t\t\t\t\t\t\t\ttranslate,\n\t\t\t\t\t\t\t\t\thasRestriction));\n\t\t\t\t\t\t\t}, &st::menuIconTranslate);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!actionText.isEmpty()) {\n\t\t\t_menu->addAction(\n\t\t\t\tactionText,\n\t\t\t\t[text = link->copyToClipboardText()] {\n\t\t\t\t\tQGuiApplication::clipboard()->setText(text);\n\t\t\t\t},\n\t\t\t\t&st::menuIconCopy);\n\t\t\tForkgram::FillUrlWithCustomUri(\n\t\t\t\t_menu.get(),\n\t\t\t\tlink->copyToClipboardText());\n\t\t} else if (item && item->hasDirectLink() && isUponSelected != 2 && isUponSelected != -2) {\n\t\t\t_menu->addAction(item->history()->peer->isMegagroup() ? tr::lng_context_copy_message_link(tr::now) : tr::lng_context_copy_post_link(tr::now), [=] {\n\t\t\t\tHistoryView::CopyPostLink(controller, itemId, HistoryView::Context::History);\n\t\t\t}, &st::menuIconLink);\n\t\t}\n\t\tif (sponsored) {\n\t\t\tconst auto hasAbout = ranges::any_of(\n\t\t\t\t_menu->actions(),\n\t\t\t\t[about = tr::lng_sponsored_menu_revenued_about(tr::now)](\n\t\t\t\t\t\tconst QAction *action) {\n\t\t\t\t\treturn action->text() == about;\n\t\t\t\t});\n\t\t\tif (!hasAbout) {\n\t\t\t\tif (!_menu->empty()) {\n\t\t\t\t\t_menu->addSeparator(&st::expandedMenuSeparator);\n\t\t\t\t}\n\t\t\t\tauto item = base::make_unique_q<Ui::Menu::MultilineAction>(\n\t\t\t\t\t_menu->menu(),\n\t\t\t\t\tst::menuWithIcons,\n\t\t\t\t\tst::historyHasCustomEmoji,\n\t\t\t\t\tst::historySponsoredAboutMenuLabelPosition,\n\t\t\t\t\tTextWithEntities{ tr::lng_sponsored_title(tr::now) },\n\t\t\t\t\t&st::menuIconInfo);\n\t\t\t\titem->clicks(\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\tcontroller->show(Box(Ui::AboutSponsoredBox));\n\t\t\t\t}, item->lifetime());\n\t\t\t\t_menu->addAction(std::move(item));\n\t\t\t}\n\t\t}\n\t\tif (isUponSelected > 1) {\n\t\t\tif (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {\n\t\t\t\t_menu->addAction(tr::lng_context_forward_selected(tr::now), [=] {\n\t\t\t\t\t_widget->forwardSelected();\n\t\t\t\t}, &st::menuIconForward);\n\t\t\t}\n\t\t\tif (selectedState.count > 0 && selectedState.count == selectedState.canDeleteCount) {\n\t\t\t\t_menu->addAction(tr::lng_context_delete_selected(tr::now), [=] {\n\t\t\t\t\t_widget->confirmDeleteSelected();\n\t\t\t\t}, &st::menuIconDelete);\n\t\t\t}\n\t\t\tif (selectedState.count > 1 && selectedState.count <= 10) {\n\t\t\t\tFork::AddGroupSelected(_menu, groupToSaved);\n\t\t\t}\n\t\t\tif (selectedState.count == 2) {\n\t\t\t\taddSwapMediaAction();\n\t\t\t}\n\t\t\tif (selectedState.count > 0 && !hasCopyRestrictionForSelected()) {\n\t\t\t\tMenu::AddDownloadFilesAction(_menu, controller, _selected, this);\n\t\t\t}\n\t\t\t_menu->addAction(tr::lng_context_clear_selection(tr::now), [=] {\n\t\t\t\t_widget->clearSelected();\n\t\t\t}, &st::menuIconSelect);\n\t\t} else if (item && ((isUponSelected != -2 && (canForward || canDelete)) || item->isRegular())) {\n\t\t\tif (isUponSelected != -2) {\n\t\t\t\tif (canForward) {\n\t\t\t\t\t_menu->addAction(tr::lng_context_forward_msg(tr::now), [=] {\n\t\t\t\t\t\tforwardAsGroup(itemId);\n\t\t\t\t\t}, &st::menuIconForward);\n\t\t\t\t}\n\t\t\t\tif (HistoryView::CanAddOfferToMessage(item)) {\n\t\t\t\t\t_menu->addAction(tr::lng_context_add_offer(tr::now), [=] {\n\t\t\t\t\t\tApi::AddOfferToMessage(_controller->uiShow(), itemId);\n\t\t\t\t\t}, &st::menuIconTagSell);\n\t\t\t\t}\n\t\t\t\tif (canDelete) {\n\t\t\t\t\tconst auto callback = [=] {\n\t\t\t\t\t\tdeleteAsGroup(itemId);\n\t\t\t\t\t};\n\t\t\t\t\tif (item->isUploading()) {\n\t\t\t\t\t\tif (item->media()\n\t\t\t\t\t\t\t&& item->media()->allowsEditCaption()) {\n\t\t\t\t\t\t\t_menu->addAction(\n\t\t\t\t\t\t\t\ttr::lng_context_upload_edit_caption(tr::now),\n\t\t\t\t\t\t\t\t[=] { editCaptionUploadLayer(item); },\n\t\t\t\t\t\t\t\t&st::menuIconEdit);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t_menu->addAction(tr::lng_context_cancel_upload(tr::now), callback, &st::menuIconCancel);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t_menu->addAction(Ui::DeleteMessageContextAction(\n\t\t\t\t\t\t\t_menu->menu(),\n\t\t\t\t\t\t\tcallback,\n\t\t\t\t\t\t\titem->ttlDestroyAt(),\n\t\t\t\t\t\t\t[=] { _menu = nullptr; }));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!canBlockSender && canReport) {\n\t\t\t\t\t_menu->addAction(tr::lng_context_report_msg(tr::now), [=] {\n\t\t\t\t\t\treportAsGroup(itemId);\n\t\t\t\t\t}, &st::menuIconReport);\n\t\t\t\t}\n\t\t\t}\n\t\t\taddSelectMessageAction(partItemOrLeader);\n\t\t\tif (isUponSelected != -2 && canBlockSender) {\n\t\t\t\t_menu->addAction(tr::lng_profile_block_user(tr::now), [=] {\n\t\t\t\t\tblockSenderAsGroup(itemId);\n\t\t\t\t}, &st::menuIconBlock);\n\t\t\t}\n\t\t} else if (Element::Moused()) {\n\t\t\taddSelectMessageAction(Element::Moused()->data());\n\t\t}\n\t}\n\n\tif (_dragStateItem) {\n\t\tconst auto view = viewByItem(_dragStateItem);\n\t\tconst auto textItem = view ? view->textItem() : _dragStateItem;\n\t\tconst auto wasAmount = _menu->actions().size();\n\t\tHistoryView::AddEmojiPacksAction(\n\t\t\t_menu,\n\t\t\ttextItem ? textItem : _dragStateItem,\n\t\t\tHistoryView::EmojiPacksSource::Message,\n\t\t\t_controller);\n\t\tconst auto added = (_menu->actions().size() > wasAmount);\n\t\tHistoryView::AddSelectRestrictionAction(\n\t\t\t_menu,\n\t\t\ttextItem ? textItem : _dragStateItem,\n\t\t\t!added);\n\t}\n\tif (hasWhoReactedItem) {\n\t\tHistoryView::AddWhoReactedAction(\n\t\t\t_menu,\n\t\t\tthis,\n\t\t\tleaderOrSelf,\n\t\t\t_controller);\n\t} else if (leaderOrSelf) {\n\t\tHistoryView::MaybeAddWhenEditedForwardedAction(\n\t\t\t_menu,\n\t\t\tleaderOrSelf,\n\t\t\t_controller);\n\t}\n\n\tif (!_menu->empty() && rateTranscriptionItem) {\n\t\t_menu->insertAction(0, base::make_unique_q<Menu::RateTranscribe>(\n\t\t\t_menu,\n\t\t\t_menu->st().menu,\n\t\t\tMenu::RateTranscribeCallbackFactory(rateTranscriptionItem)));\n\t}\n\n\tif (!pollOptionLink.isEmpty() && leaderOrSelf && !_menu->empty()) {\n\t\tconst auto pollItemId = leaderOrSelf->fullId();\n\t\t_menu->stashContent([=](not_null<Ui::PopupMenu*> menu) {\n\t\t\tHistoryView::FillPollOptionPage(\n\t\t\t\tmenu,\n\t\t\t\t&session->data(),\n\t\t\t\tpollItemId,\n\t\t\t\tpollOptionLink,\n\t\t\t\t_controller,\n\t\t\t\t[=] {\n\t\t\t\t\t_widget->replyToMessage({\n\t\t\t\t\t\t.messageId = pollItemId,\n\t\t\t\t\t\t.pollOption = pollOptionLink,\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t});\n\t}\n\n\tif (leaderOrSelf && !_menu->empty()) {\n\t\tconst auto media = leaderOrSelf->media();\n\t\tconst auto poll = media ? media->poll() : nullptr;\n\t\tif (poll && !poll->closed() && poll->hideResultsUntilClose()) {\n\t\t\tHistoryView::InsertPollHiddenResultsLabel(_menu.get());\n\t\t}\n\t}\n\n\tif (_menu->empty()) {\n\t\t_menu = nullptr;\n\t\treturn;\n\t}\n\tusing namespace HistoryView::Reactions;\n\tconst auto desiredPosition = e->globalPos();\n\tconst auto reactItem = Element::Hovered()\n\t\t? Element::Hovered()->data().get()\n\t\t: nullptr;\n\tconst auto attached = reactItem\n\t\t? AttachSelectorToMenu(\n\t\t\t_menu.get(),\n\t\t\tcontroller,\n\t\t\tdesiredPosition,\n\t\t\treactItem,\n\t\t\t[=](ChosenReaction reaction) { reactionChosen(reaction); },\n\t\t\tItemReactionsAbout(reactItem))\n\t\t: AttachSelectorResult::Skipped;\n\tif (attached == AttachSelectorResult::Failed) {\n\t\t_menu = nullptr;\n\t\treturn;\n\t}\n\tHistoryView::AttachPollOptionTabs(_menu.get(), desiredPosition);\n\tif (attached == AttachSelectorResult::Attached) {\n\t\t_menu->popupPrepared();\n\t} else {\n\t\t_menu->popup(desiredPosition);\n\t}\n\te->accept();\n}\n\nbool HistoryInner::hasCopyRestriction(HistoryItem *item) const {\n\treturn !_peer->allowsForwarding() || (item && item->forbidsForward());\n}\n\nbool HistoryInner::hasCopyMediaRestriction(\n\t\tnot_null<HistoryItem*> item) const {\n\treturn hasCopyRestriction(item) || item->forbidsSaving();\n}\n\nbool HistoryInner::showCopyRestriction(HistoryItem *item) {\n\tif (!hasCopyRestriction(item)) {\n\t\treturn false;\n\t}\n\t_controller->showToast(_peer->isBroadcast()\n\t\t? tr::lng_error_nocopy_channel(tr::now)\n\t\t: _peer->isUser()\n\t\t? tr::lng_error_nocopy_user(tr::now)\n\t\t: tr::lng_error_nocopy_group(tr::now));\n\treturn true;\n}\n\nbool HistoryInner::showCopyMediaRestriction(not_null<HistoryItem*> item) {\n\tif (!hasCopyMediaRestriction(item)) {\n\t\treturn false;\n\t}\n\t_controller->showToast(_peer->isBroadcast()\n\t\t? tr::lng_error_nocopy_channel(tr::now)\n\t\t: _peer->isUser()\n\t\t? tr::lng_error_nocopy_user(tr::now)\n\t\t: tr::lng_error_nocopy_group(tr::now));\n\treturn true;\n}\n\nbool HistoryInner::hasCopyRestrictionForSelected() const {\n\tif (hasCopyRestriction()) {\n\t\treturn true;\n\t}\n\tfor (const auto &[item, selection] : _selected) {\n\t\tif (item && item->forbidsForward()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool HistoryInner::showCopyRestrictionForSelected() {\n\tfor (const auto &[item, selection] : _selected) {\n\t\tif (showCopyRestriction(item)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid HistoryInner::copySelectedText() {\n\tif (!showCopyRestrictionForSelected()) {\n\t\tTextUtilities::SetClipboardText(getSelectedText());\n\t}\n}\n\nvoid HistoryInner::addSearchAction() {\n\tif (Core::App().settings().fork().searchEngine()) {\n\t\t_menu->addAction(tr::lng_context_search_selected(tr::now), [=] {\n\t\t\tauto url =\n\t\t\t\tCore::App().settings().fork().searchEngineUrl();\n\t\t\tQDesktopServices::openUrl(url.replace(qsl(\"%q\"), this->getSelectedText().rich.text));\n\t\t}, &st::menuIconIpAddress);\n\t}\n}\n\nvoid HistoryInner::editCaptionUploadLayer(not_null<HistoryItem*> item) {\n\tif (const auto view = viewByItem(item)) {\n\t\tif (item->isUploading()) {\n\t\t\t_controller->uiShow()->show(Box(Ui::EditCaptionBox, view));\n\t\t}\n\t}\n}\n\nvoid HistoryInner::savePhotoToFile(not_null<PhotoData*> photo) {\n\tconst auto media = photo->activeMediaView();\n\tif (photo->isNull() || !media || !media->loaded()) {\n\t\treturn;\n\t}\n\n\tauto filter = u\"JPEG Image (*.jpg);;\"_q + FileDialog::AllFilesFilter();\n\tFileDialog::GetWritePath(\n\t\tthis,\n\t\ttr::lng_save_photo(tr::now),\n\t\tfilter,\n\t\tfiledialogDefaultName(u\"photo\"_q, u\".jpg\"_q),\n\t\tcrl::guard(this, [=](const QString &result) {\n\t\t\tif (!result.isEmpty()) {\n\t\t\t\tmedia->saveToFile(result);\n\t\t\t}\n\t\t}));\n}\n\nvoid HistoryInner::copyContextImage(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId itemId) {\n\tconst auto item = session().data().message(itemId);\n\tconst auto media = photo->activeMediaView();\n\tconst auto restricted = item\n\t\t? showCopyMediaRestriction(item)\n\t\t: IsServerMsgId(itemId.msg);\n\tif (photo->isNull() || !media || !media->loaded()) {\n\t\treturn;\n\t} else if (!restricted) {\n\t\tmedia->setToClipboard();\n\t}\n}\n\nvoid HistoryInner::showStickerPackInfo(not_null<DocumentData*> document) {\n\tStickerSetBox::Show(_controller->uiShow(), document);\n}\n\nvoid HistoryInner::cancelContextDownload(not_null<DocumentData*> document) {\n\tdocument->cancel();\n}\n\nvoid HistoryInner::showContextInFolder(not_null<DocumentData*> document) {\n\tconst auto filepath = document->filepath(true);\n\tif (!filepath.isEmpty()) {\n\t\tFile::ShowInFolder(filepath);\n\t}\n}\n\nvoid HistoryInner::saveDocumentToFile(\n\t\tFullMsgId contextId,\n\t\tnot_null<DocumentData*> document) {\n\tDocumentSaveClickHandler::SaveAndTrack(\n\t\tcontextId,\n\t\tdocument,\n\t\tDocumentSaveClickHandler::Mode::ToNewFile);\n}\n\nvoid HistoryInner::openContextGif(FullMsgId itemId) {\n\tif (const auto item = session().data().message(itemId)) {\n\t\tif (const auto media = item->media()) {\n\t\t\tif (const auto document = media->document()) {\n\t\t\t\t_controller->openDocument(document, true, { itemId });\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid HistoryInner::saveContextGif(FullMsgId itemId) {\n\tif (const auto item = session().data().message(itemId)) {\n\t\tif (!hasCopyMediaRestriction(item)) {\n\t\t\tif (const auto media = item->media()) {\n\t\t\t\tif (const auto document = media->document()) {\n\t\t\t\t\tApi::ToggleSavedGif(\n\t\t\t\t\t\t_controller->uiShow(),\n\t\t\t\t\t\tdocument,\n\t\t\t\t\t\titem->fullId(),\n\t\t\t\t\t\ttrue);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid HistoryInner::copyContextText(FullMsgId itemId) {\n\tif (const auto item = session().data().message(itemId)) {\n\t\tif (!showCopyRestriction(item)) {\n\t\t\tif (const auto group = session().data().groups().find(item)) {\n\t\t\t\tTextUtilities::SetClipboardText(HistoryGroupText(group));\n\t\t\t} else {\n\t\t\t\tTextUtilities::SetClipboardText(HistoryItemText(item));\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid HistoryInner::resizeEvent(QResizeEvent *e) {\n\tif (e->oldSize().width() != e->size().width() && _overlayHost) {\n\t\t_overlayHost->hide();\n\t}\n\tmouseActionUpdate();\n}\n\nTextForMimeData HistoryInner::getSelectedText(bool depersonalized) const {\n\tauto selected = _selected;\n\n\tif (_mouseAction == MouseAction::Selecting && _dragSelFrom && _dragSelTo) {\n\t\tapplyDragSelection(&selected);\n\t}\n\n\tif (selected.empty()) {\n\t\treturn TextForMimeData();\n\t}\n\tif (selected.cbegin()->second != FullSelection) {\n\t\tconst auto &[item, selection] = *selected.cbegin();\n\t\tif (const auto view = viewByItem(item)) {\n\t\t\treturn view->selectedText(selection);\n\t\t}\n\t\treturn TextForMimeData();\n\t}\n\n\tstruct Part {\n\t\tQString name;\n\t\tQString time;\n\t\tTextForMimeData unwrapped;\n\t};\n\n\tauto groups = base::flat_set<not_null<const Data::Group*>>();\n\tauto fullSize = 0;\n\tauto texts = base::flat_map<Data::MessagePosition, Part>();\n\tauto personMap = base::flat_map<not_null<PeerData*>, int>();\n\tauto personCounter = 1;\n\n\tconst auto wrapItem = [&](\n\t\t\tnot_null<HistoryItem*> item,\n\t\t\tTextForMimeData &&unwrapped) {\n\t\tauto name = QString();\n\t\tif (depersonalized) {\n\t\t\tconst auto author = item->author();\n\t\t\tconst auto it = personMap.find(author);\n\t\t\tif (it != personMap.end()) {\n\t\t\t\tname = QString(\"Person %1\").arg(it->second);\n\t\t\t} else {\n\t\t\t\tname = QString(\"Person %1\").arg(personCounter);\n\t\t\t\tpersonMap.emplace(author, personCounter++);\n\t\t\t}\n\t\t} else {\n\t\t\tname = item->author()->name();\n\t\t}\n\t\tconst auto i = texts.emplace(item->position(), Part{\n\t\t\t.name = name,\n\t\t\t.time = QString(\"[%1] \").arg(\n\t\t\t\tQLocale().toString(ItemDateTime(item), QLocale::ShortFormat)),\n\t\t\t.unwrapped = std::move(unwrapped),\n\t\t}).first;\n\t\tfullSize += i->second.time.size()\n\t\t\t+ i->second.name.size()\n\t\t\t+ 2\n\t\t\t+ i->second.unwrapped.expanded.size();\n\t};\n\tconst auto addItem = [&](not_null<HistoryItem*> item) {\n\t\twrapItem(item, HistoryItemText(item));\n\t};\n\tconst auto addGroup = [&](not_null<const Data::Group*> group) {\n\t\tExpects(!group->items.empty());\n\n\t\twrapItem(group->items.back(), HistoryGroupText(group));\n\t};\n\n\tfor (const auto &[item, selection] : selected) {\n\t\tif (const auto group = session().data().groups().find(item)) {\n\t\t\tif (groups.contains(group)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (isSelectedGroup(&selected, group)) {\n\t\t\t\tgroups.emplace(group);\n\t\t\t\taddGroup(group);\n\t\t\t} else {\n\t\t\t\taddItem(item);\n\t\t\t}\n\t\t} else {\n\t\t\taddItem(item);\n\t\t}\n\t}\n\tif (texts.size() == 1) {\n\t\treturn texts.front().second.unwrapped;\n\t}\n\tauto result = TextForMimeData();\n\tconst auto sep = u\"\\n\"_q;\n\tresult.reserve(fullSize + (texts.size() - 1) * sep.size());\n\tfor (auto i = texts.begin(), e = texts.end(); i != e;) {\n\t\tresult.append(i->second.time).append(i->second.name).append(u\": \"_q);\n\t\tresult.append(std::move(i->second.unwrapped));\n\t\tif (++i != e) {\n\t\t\tresult.append(sep);\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid HistoryInner::keyPressEvent(QKeyEvent *e) {\n\tif (_middleClickAutoscroll.active() && e->key() == Qt::Key_Escape) {\n\t\t_middleClickAutoscroll.stop();\n\t\treturn;\n\t}\n\tif (e->key() == Qt::Key_Escape) {\n\t\t_widget->escape();\n\t} else if (e == QKeySequence::Copy && !_selected.empty()) {\n\t\tcopySelectedText();\n#ifdef Q_OS_MAC\n\t} else if (e->key() == Qt::Key_E\n\t\t&& e->modifiers().testFlag(Qt::ControlModifier)\n\t\t&& !showCopyRestrictionForSelected()) {\n\t\tTextUtilities::SetClipboardText(getSelectedText(), QClipboard::FindBuffer);\n#endif // Q_OS_MAC\n\t} else if (e == QKeySequence::Delete || e->key() == Qt::Key_Backspace) {\n\t\tauto selectedState = getSelectionState();\n\t\tif (selectedState.count > 0\n\t\t\t&& selectedState.canDeleteCount == selectedState.count) {\n\t\t\t_widget->confirmDeleteSelected();\n\t\t}\n\t} else if (!(e->modifiers() & ~Qt::ShiftModifier)\n\t\t&& e->key() != Qt::Key_Shift) {\n\t\t_widget->tryProcessKeyInput(e);\n\t} else {\n\t\te->ignore();\n\t}\n}\n\nvoid HistoryInner::markReadMetricsStale() {\n\t_readMetricsStale = true;\n\tupdate();\n}\n\nvoid HistoryInner::registerReadMetricsActivity() {\n\tif (_widget->markingContentsRead()) {\n\t\t_readMetricsTracker->registerActivity();\n\t}\n}\n\nvoid HistoryInner::checkActivation() {\n\tif (!_widget->markingMessagesRead()) {\n\t\treturn;\n\t}\n\tadjustCurrent(_visibleAreaBottom);\n\tif (_history->loadedAtBottom() && _visibleAreaBottom >= height()) {\n\t\t// Clear possible message notifications.\n\t\t// Side-effect: Also clears all notifications from forum topics.\n\t\tCore::App().notifications().clearFromHistory(_history);\n\t}\n\tif (_curHistory != _history || _history->isEmpty()) {\n\t\treturn;\n\t}\n\tauto block = _history->blocks[_curBlock].get();\n\tauto view = block->messages[_curItem].get();\n\twhile (_curBlock > 0 || _curItem > 0) {\n\t\tconst auto bottom = itemTop(view) + view->height();\n\t\tif (_visibleAreaBottom >= bottom) {\n\t\t\tbreak;\n\t\t}\n\t\tif (_curItem > 0) {\n\t\t\tview = block->messages[--_curItem].get();\n\t\t} else {\n\t\t\twhile (_curBlock > 0) {\n\t\t\t\tblock = _history->blocks[--_curBlock].get();\n\t\t\t\t_curItem = block->messages.size();\n\t\t\t\tif (_curItem > 0) {\n\t\t\t\t\tview = block->messages[--_curItem].get();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tsession().data().histories().readInboxTill(view->data());\n}\n\nvoid HistoryInner::recountHistoryGeometry(bool initial) {\n\t_contentWidth = _scroll->width();\n\n\tif (_history->hasPendingResizedItems()\n\t\t|| (_migrated && _migrated->hasPendingResizedItems())) {\n\t\t_recountedAfterPendingResizedItems = true;\n\t}\n\tconst auto aboutAboveHistory = _aboutView && _aboutView->aboveHistory();\n\tconst auto visibleHeight = _scroll->height();\n\tauto oldHistoryMarginTop = qMax(\n\t\tvisibleHeight - historyHeight() - _historyMarginBottom,\n\t\t0);\n\tif (aboutAboveHistory) {\n\t\taccumulate_max(oldHistoryMarginTop, _aboutView->height);\n\t}\n\n\tupdateBotInfo(false);\n\n\t_history->resizeToWidth(_contentWidth);\n\tif (_migrated) {\n\t\t_migrated->resizeToWidth(_contentWidth);\n\t}\n\n\t// With migrated history we perhaps do not need to display\n\t// the first _history message date (just skip it by height).\n\t_historySkipHeight = 0;\n\tif (_migrated\n\t\t&& _migrated->loadedAtBottom()\n\t\t&& _history->loadedAtTop()) {\n\t\tif (const auto first = _history->findFirstNonEmpty()) {\n\t\t\tif (const auto last = _migrated->findLastNonEmpty()) {\n\t\t\t\tif (first->dateTime().date() == last->dateTime().date()) {\n\t\t\t\t\tconst auto dateHeight = first->displayedDateHeight();\n\t\t\t\t\tif (_migrated->height() > dateHeight) {\n\t\t\t\t\t\t_historySkipHeight += dateHeight;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (const auto view = _aboutView ? _aboutView->view() : nullptr) {\n\t\t_aboutView->height = view->resizeGetHeight(_contentWidth);\n\t\tif (aboutAboveHistory) {\n\t\t\t_aboutView->top = qMin(\n\t\t\t\t_historyMarginTop - _aboutView->height,\n\t\t\t\tqMax(0, (_scroll->height() - _aboutView->height) / 2));\n\t\t} else {\n\t\t\t_aboutView->top = qMax(\n\t\t\t\tqMax(0, (_scroll->height() - _aboutView->height) / 2),\n\t\t\t\t_historyMarginTop + historyHeight() - _historyMarginBottom);\n\t\t}\n\t} else if (_aboutView) {\n\t\t_aboutView->top = _aboutView->height = 0;\n\t}\n\n\tauto newHistoryMarginTop = qMax(\n\t\tvisibleHeight - historyHeight() - _historyMarginBottom,\n\t\t0);\n\tif (aboutAboveHistory) {\n\t\taccumulate_max(newHistoryMarginTop, _aboutView->height);\n\t}\n\n\tconst auto marginDelta = newHistoryMarginTop - oldHistoryMarginTop;\n\tif (!initial && marginDelta) {\n\t\tif (_history->scrollTopItem) {\n\t\t\t_history->scrollTopOffset += marginDelta;\n\t\t} else if (_migrated && _migrated->scrollTopItem) {\n\t\t\t_migrated->scrollTopOffset += marginDelta;\n\t\t}\n\t}\n}\n\nvoid HistoryInner::updateBotInfo(bool recount) {\n\tif (!_aboutView) {\n\t\treturn;\n\t} else if (_aboutView->refresh() && recount && _contentWidth > 0) {\n\t\tconst auto view = _aboutView->view();\n\t\tconst auto now = view ? view->resizeGetHeight(_contentWidth) : 0;\n\t\tif (_aboutView->height != now) {\n\t\t\t_aboutView->height = now;\n\t\t\tupdateSize();\n\t\t}\n\t}\n}\n\nbool HistoryInner::wasSelectedText() const {\n\treturn _wasSelectedText;\n}\n\nvoid HistoryInner::visibleAreaUpdated(int top, int bottom) {\n\tauto scrolledUp = (top < _visibleAreaTop);\n\t_visibleAreaTop = top;\n\t_visibleAreaBottom = bottom;\n\tmarkReadMetricsStale();\n\tregisterReadMetricsActivity();\n\tconst auto visibleAreaHeight = bottom - top;\n\n\t// if history has pending resize events we should not update scrollTopItem\n\tif (hasPendingResizedItems()) {\n\t\treturn;\n\t}\n\n\tif (bottom >= _historyMarginTop + historyHeight() + _historyMarginBottom) {\n\t\t_history->forgetScrollState();\n\t\tif (_migrated) {\n\t\t\t_migrated->forgetScrollState();\n\t\t}\n\t} else {\n\t\tint htop = historyTop(), mtop = migratedTop();\n\t\tif ((htop >= 0 && top >= htop) || mtop < 0) {\n\t\t\t_history->countScrollState(top - htop);\n\t\t\tif (_migrated) {\n\t\t\t\t_migrated->forgetScrollState();\n\t\t\t}\n\t\t} else if (mtop >= 0 && top >= mtop) {\n\t\t\t_history->forgetScrollState();\n\t\t\t_migrated->countScrollState(top - mtop);\n\t\t} else {\n\t\t\t_history->countScrollState(top - htop);\n\t\t\tif (_migrated) {\n\t\t\t\t_migrated->forgetScrollState();\n\t\t\t}\n\t\t}\n\t}\n\tif (scrolledUp) {\n\t\t_scrollDateCheck.call();\n\t} else {\n\t\tscrollDateHideByTimer();\n\t}\n\n\t// Unload userpics.\n\tif (_userpics.size() > kClearUserpicsAfter) {\n\t\t_userpicsCache = std::move(_userpics);\n\t}\n\n\t// Unload lottie animations.\n\tconst auto pages = kUnloadHeavyPartsPages;\n\tconst auto from = _visibleAreaTop - pages * visibleAreaHeight;\n\tconst auto till = _visibleAreaBottom + pages * visibleAreaHeight;\n\tsession().data().unloadHeavyViewParts(_elementDelegate, from, till);\n\tif (_migratedElementDelegate) {\n\t\tsession().data().unloadHeavyViewParts(\n\t\t\t_migratedElementDelegate,\n\t\t\tfrom,\n\t\t\ttill);\n\t}\n\tcheckActivation();\n\n\t_emojiInteractions->visibleAreaUpdated(\n\t\t_visibleAreaTop,\n\t\t_visibleAreaBottom);\n\n\tif (_overlayHost) {\n\t\t_overlayHost->updatePosition();\n\t}\n}\n\nbool HistoryInner::displayScrollDate() const {\n\treturn (_visibleAreaTop <= height() - 2 * (_visibleAreaBottom - _visibleAreaTop));\n}\n\nvoid HistoryInner::scrollDateCheck() {\n\tauto newScrollDateItem = _history->scrollTopItem ? _history->scrollTopItem : (_migrated ? _migrated->scrollTopItem : nullptr);\n\tauto newScrollDateItemTop = _history->scrollTopItem ? _history->scrollTopOffset : (_migrated ? _migrated->scrollTopOffset : 0);\n\t//if (newScrollDateItem && !displayScrollDate()) {\n\t//\tif (!_history->isEmpty() && newScrollDateItem->date.date() == _history->blocks.back()->messages.back()->data()->date.date()) {\n\t//\t\tnewScrollDateItem = nullptr;\n\t//\t}\n\t//}\n\tif (!newScrollDateItem) {\n\t\t_scrollDateLastItem = nullptr;\n\t\t_scrollDateLastItemTop = 0;\n\t\tscrollDateHide();\n\t} else if (newScrollDateItem != _scrollDateLastItem || newScrollDateItemTop != _scrollDateLastItemTop) {\n\t\t// Show scroll date only if it is not the initial onScroll() event (with empty _scrollDateLastItem).\n\t\tif (_scrollDateLastItem && !_scrollDateShown) {\n\t\t\ttoggleScrollDateShown();\n\t\t}\n\t\t_scrollDateLastItem = newScrollDateItem;\n\t\t_scrollDateLastItemTop = newScrollDateItemTop;\n\t\t_scrollDateHideTimer.callOnce(kScrollDateHideTimeout);\n\t}\n}\n\nvoid HistoryInner::scrollDateHideByTimer() {\n\t_scrollDateHideTimer.cancel();\n\tif (!_scrollDateLink || ClickHandler::getPressed() != _scrollDateLink) {\n\t\tscrollDateHide();\n\t}\n}\n\nvoid HistoryInner::scrollDateHide() {\n\tif (_scrollDateShown) {\n\t\ttoggleScrollDateShown();\n\t}\n}\n\nvoid HistoryInner::keepScrollDateForNow() {\n\tif (!_scrollDateShown && _scrollDateLastItem && _scrollDateOpacity.animating()) {\n\t\ttoggleScrollDateShown();\n\t}\n\t_scrollDateHideTimer.callOnce(kScrollDateHideTimeout);\n}\n\nvoid HistoryInner::toggleScrollDateShown() {\n\t_scrollDateShown = !_scrollDateShown;\n\tauto from = _scrollDateShown ? 0. : 1.;\n\tauto to = _scrollDateShown ? 1. : 0.;\n\t_scrollDateOpacity.start([this] { repaintScrollDateCallback(); }, from, to, st::historyDateFadeDuration);\n}\n\nvoid HistoryInner::repaintScrollDateCallback() {\n\tint updateTop = _visibleAreaTop;\n\tint updateHeight = st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom();\n\tif (_history->hasForumThreadBars()) {\n\t\tupdateHeight *= 2;\n\t}\n\tupdate(0, updateTop, width(), updateHeight);\n}\n\nvoid HistoryInner::setItemsRevealHeight(int revealHeight) {\n\t_revealHeight = revealHeight;\n}\n\nvoid HistoryInner::setCollapseGaps(std::vector<CollapseGap> gaps) {\n\tif (_collapseGaps != gaps) {\n\t\t_collapseGaps = std::move(gaps);\n\t\tupdateSize();\n\t}\n}\n\nvoid HistoryInner::changeItemsRevealHeight(int revealHeight) {\n\tif (_revealHeight == revealHeight) {\n\t\treturn;\n\t}\n\t_revealHeight = revealHeight;\n\tupdateSize();\n}\n\nvoid HistoryInner::updateSize() {\n\tconst auto visibleHeight = _scroll->height();\n\tauto collapseGapTotal = 0;\n\tfor (const auto &gap : _collapseGaps) {\n\t\tcollapseGapTotal += gap.height;\n\t}\n\tconst auto itemsHeight = historyHeight() - _revealHeight + collapseGapTotal;\n\tconst auto aboutAboveHistory = _aboutView && _aboutView->aboveHistory();\n\tconst auto aboutBelowHistory = _aboutView && !aboutAboveHistory;\n\tauto newHistoryMarginBottom = st::historyPaddingBottom;\n\tif (aboutBelowHistory) {\n\t\taccumulate_max(newHistoryMarginBottom, _aboutView->height);\n\t}\n\tauto newHistoryMarginTop = qMax(\n\t\tvisibleHeight - itemsHeight - newHistoryMarginBottom,\n\t\t0);\n\tif (aboutAboveHistory) {\n\t\taccumulate_max(newHistoryMarginTop, _aboutView->height);\n\t}\n\n\tif (_aboutView && _aboutView->height > 0) {\n\t\tif (aboutAboveHistory) {\n\t\t\t_aboutView->top = qMin(\n\t\t\t\tnewHistoryMarginTop - _aboutView->height,\n\t\t\t\tqMax(0, (_scroll->height() - _aboutView->height) / 2));\n\t\t} else {\n\t\t\t_aboutView->top = qMax(\n\t\t\t\tqMax(0, (_scroll->height() - _aboutView->height) / 2),\n\t\t\t\t(newHistoryMarginTop\n\t\t\t\t\t+ itemsHeight\n\t\t\t\t\t+ newHistoryMarginBottom\n\t\t\t\t\t- _aboutView->height));\n\t\t}\n\t}\n\n\tif (_historyMarginTop != newHistoryMarginTop) {\n\t\t_historyMarginTop = newHistoryMarginTop;\n\t}\n\tif (_historyMarginBottom != newHistoryMarginBottom) {\n\t\t_historyMarginBottom = newHistoryMarginBottom;\n\t}\n\n\tconst auto newHeight = _historyMarginTop\n\t\t+ itemsHeight\n\t\t+ _historyMarginBottom;\n\tif (width() != _scroll->width() || height() != newHeight) {\n\t\tresize(_scroll->width(), newHeight);\n\n\t\tif (!_revealHeight) {\n\t\t\tmouseActionUpdate(QCursor::pos());\n\t\t}\n\t} else {\n\t\tupdate();\n\t}\n}\n\nvoid HistoryInner::setShownPinned(HistoryItem *item) {\n\t_pinnedItem = item;\n}\n\nvoid HistoryInner::enterEventHook(QEnterEvent *e) {\n\t_mouseActive = true;\n\tmouseActionUpdate(QCursor::pos());\n\treturn RpWidget::enterEventHook(e);\n}\n\nvoid HistoryInner::leaveEventHook(QEvent *e) {\n\t_reactionsManager->updateButton({ .cursorLeft = true });\n\t_replyButtonManager->updateButton({});\n\tif (auto item = Element::Hovered()) {\n\t\trepaintItem(item);\n\t\tElement::Hovered(nullptr);\n\t}\n\tClickHandler::clearActive();\n\tUi::Tooltip::Hide();\n\tif (!ClickHandler::getPressed() && _cursor != style::cur_default) {\n\t\t_cursor = style::cur_default;\n\t\tsetCursor(_cursor);\n\t}\n\t_mouseActive = false;\n\treturn RpWidget::leaveEventHook(e);\n}\n\nvoid HistoryInner::setupThanosEffect() {\n\tconst auto history = _history;\n\tconst auto migrated = [=] { return _migrated; };\n\t_thanosController = std::make_unique<Ui::ThanosEffectController>(\n\t\t&session(),\n\t\tUi::ThanosEffectController::Delegate{\n\t\t\t.viewForItem = [=](not_null<const HistoryItem*> item)\n\t\t\t\t\t-> HistoryView::Element* {\n\t\t\t\tif (item->history() != history\n\t\t\t\t\t&& item->history() != migrated()) {\n\t\t\t\t\treturn nullptr;\n\t\t\t\t}\n\t\t\t\treturn item->mainView();\n\t\t\t},\n\t\t\t.itemTop = [=](not_null<const HistoryView::Element*> view) {\n\t\t\t\treturn itemTop(view);\n\t\t\t},\n\t\t\t.visibleAreaTop = [=] { return _visibleAreaTop; },\n\t\t\t.visibleAreaBottom = [=] { return _visibleAreaBottom; },\n\t\t\t.contentWidth = [=] { return width(); },\n\t\t\t.preparePaintContext = [=](QRect clip) {\n\t\t\t\treturn preparePaintContext(clip);\n\t\t\t},\n\t\t\t.window = [=]() -> QWidget* { return window(); },\n\t\t\t.scrollArea = [=]() -> not_null<Ui::ScrollArea*> {\n\t\t\t\treturn _scroll;\n\t\t\t},\n\t\t\t.scrollToY = [=](int y) {\n\t\t\t\t_widget->synteticScrollToY(y);\n\t\t\t},\n\t\t\t.setCollapseGaps = [=](std::vector<CollapseGap> gaps) {\n\t\t\t\tsetCollapseGaps(std::move(gaps));\n\t\t\t},\n\t\t},\n\t\tlifetime());\n\n\tsession().data().viewAboutToBeRemoved(\n\t) | rpl::on_next([=](not_null<const HistoryView::Element*> view) {\n\t\t_thanosController->captureOnRemoval(view->data());\n\t}, lifetime());\n}\n\nHistoryInner::~HistoryInner() {\n\tif (_overlayHost) {\n\t\t_overlayHost->hide();\n\t}\n\t_aboutView = nullptr;\n\tfor (const auto &item : _animatedStickersPlayed) {\n\t\tif (const auto view = item->mainView()) {\n\t\t\tif (const auto media = view->media()) {\n\t\t\t\tmedia->stickerClearLoopPlayed();\n\t\t\t}\n\t\t}\n\t}\n\t_history->delegateMixin()->setCurrent(nullptr);\n\tif (_migrated) {\n\t\t_migrated->delegateMixin()->setCurrent(nullptr);\n\t}\n\tdelete _menu;\n\t_mouseAction = MouseAction::None;\n}\n\nbool HistoryInner::focusNextPrevChild(bool next) {\n\tif (_selected.empty()) {\n\t\treturn RpWidget::focusNextPrevChild(next);\n\t} else {\n\t\tclearSelected();\n\t\treturn true;\n\t}\n}\n\nvoid HistoryInner::adjustCurrent(int32 y) const {\n\tint32 htop = historyTop(), hdrawtop = historyDrawTop(), mtop = migratedTop();\n\t_curHistory = nullptr;\n\tif (mtop >= 0) {\n\t\tadjustCurrent(y - mtop, _migrated);\n\t}\n\tif (htop >= 0 && hdrawtop >= 0 && (mtop < 0 || y >= hdrawtop)) {\n\t\tadjustCurrent(y - htop, _history);\n\t}\n}\n\nvoid HistoryInner::adjustCurrent(int32 y, History *history) const {\n\tExpects(!history->isEmpty());\n\n\t_curHistory = history;\n\tif (_curBlock >= history->blocks.size()) {\n\t\t_curBlock = history->blocks.size() - 1;\n\t\t_curItem = 0;\n\t}\n\twhile (history->blocks[_curBlock]->y() > y && _curBlock > 0) {\n\t\t--_curBlock;\n\t\t_curItem = 0;\n\t}\n\twhile (history->blocks[_curBlock]->y() + history->blocks[_curBlock]->height() <= y && _curBlock + 1 < history->blocks.size()) {\n\t\t++_curBlock;\n\t\t_curItem = 0;\n\t}\n\tauto block = history->blocks[_curBlock].get();\n\tif (_curItem >= block->messages.size()) {\n\t\t_curItem = block->messages.size() - 1;\n\t}\n\tauto by = block->y();\n\twhile (block->messages[_curItem]->y() + by > y && _curItem > 0) {\n\t\t--_curItem;\n\t}\n\twhile (block->messages[_curItem]->y() + block->messages[_curItem]->height() + by <= y && _curItem + 1 < block->messages.size()) {\n\t\t++_curItem;\n\t}\n}\n\nauto HistoryInner::prevItem(Element *view) -> Element* {\n\tif (!view) {\n\t\treturn nullptr;\n\t} else if (const auto result = view->previousDisplayedInBlocks()) {\n\t\treturn result;\n\t} else if (view->history() == _history\n\t\t&& _migrated\n\t\t&& _history->loadedAtTop()\n\t\t&& !_migrated->isEmpty()\n\t\t&& _migrated->loadedAtBottom()) {\n\t\treturn _migrated->findLastDisplayed();\n\t}\n\treturn nullptr;\n}\n\nauto HistoryInner::nextItem(Element *view) -> Element* {\n\tif (!view) {\n\t\treturn nullptr;\n\t} else if (const auto result = view->nextDisplayedInBlocks()) {\n\t\treturn result;\n\t} else if (view->history() == _migrated\n\t\t&& _migrated->loadedAtBottom()\n\t\t&& _history->loadedAtTop()\n\t\t&& !_history->isEmpty()) {\n\t\treturn _history->findFirstDisplayed();\n\t}\n\treturn nullptr;\n}\n\nbool HistoryInner::canCopySelected() const {\n\treturn !_selected.empty();\n}\n\nbool HistoryInner::canDeleteSelected() const {\n\tconst auto selectedState = getSelectionState();\n\treturn (selectedState.count > 0)\n\t\t&& (selectedState.count == selectedState.canDeleteCount);\n}\n\nHistoryView::SelectionModeResult HistoryInner::inSelectionMode() const {\n\tconst auto inSelectionMode = [&] {\n\t\tif (hasSelectedItems()) {\n\t\t\treturn true;\n\t\t}\n\t\tconst auto isSelecting = _mouseAction == MouseAction::Selecting;\n\t\tif (isSelecting && _dragSelFrom && _dragSelTo) {\n\t\t\treturn true;\n\t\t} else if (_chooseForReportReason.has_value()) {\n\t\t\treturn true;\n\t\t} else if (_lastInSelectionMode && isSelecting) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}();\n\tconst auto now = inSelectionMode;\n\tif (_lastInSelectionMode != now) {\n\t\t_lastInSelectionMode = now;\n\t\tif (now && _overlayHost) {\n\t\t\t_overlayHost->hide();\n\t\t}\n\t\tif (_inSelectionModeAnimation.animating()) {\n\t\t\tconst auto progress = !now\n\t\t\t\t? _inSelectionModeAnimation.value(0.)\n\t\t\t\t: 1. - _inSelectionModeAnimation.value(0.);\n\t\t\t_inSelectionModeAnimation.change(\n\t\t\t\tnow ? 1. : 0.,\n\t\t\t\tst::universalDuration * (1. - progress));\n\t\t} else {\n\t\t\t_inSelectionModeAnimation.stop();\n\t\t\t_inSelectionModeAnimation.start(\n\t\t\t\t[this] {\n\t\t\t\t\tconst_cast<HistoryInner*>(this)->update(\n\t\t\t\t\t\tQRect(\n\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t_visibleAreaTop,\n\t\t\t\t\t\t\twidth(),\n\t\t\t\t\t\t\t_visibleAreaBottom - _visibleAreaTop));\n\t\t\t\t},\n\t\t\t\tnow ? 0. : 1.,\n\t\t\t\tnow ? 1. : 0.,\n\t\t\t\tst::universalDuration);\n\t\t}\n\t}\n\treturn { now, _inSelectionModeAnimation.value(now ? 1. : 0.) };\n}\n\nbool HistoryInner::elementIntersectsRange(\n\t\tnot_null<const Element*> view,\n\t\tint from,\n\t\tint till) const {\n\tconst auto top = itemTop(view);\n\tif (top < 0) {\n\t\treturn false;\n\t}\n\tconst auto bottom = top + view->height();\n\treturn (top < till && bottom > from);\n}\n\nvoid HistoryInner::elementStartStickerLoop(\n\t\tnot_null<const Element*> view) {\n\t_animatedStickersPlayed.emplace(view->data());\n}\n\nvoid HistoryInner::elementShowPollResults(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context) {\n\t_controller->showPollResults(poll, context);\n}\n\nHistoryView::ElementOverlayHost &HistoryInner::ensureOverlayHost() {\n\tif (!_overlayHost) {\n\t\t_overlayHost = std::make_unique<HistoryView::ElementOverlayHost>(\n\t\t\tthis,\n\t\t\t[=](not_null<const Element*> view) {\n\t\t\t\treturn itemTop(view);\n\t\t\t});\n\t}\n\treturn *_overlayHost;\n}\n\nvoid HistoryInner::elementShowAddPollOption(\n\t\tnot_null<HistoryView::Element*> view,\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context,\n\t\tQRect optionRect) {\n\tHistoryView::ShowAddPollOptionOverlay(\n\t\tensureOverlayHost(),\n\t\tthis,\n\t\tview,\n\t\tpoll,\n\t\tcontext,\n\t\t_controller,\n\t\t_controller->chatStyle());\n}\n\nvoid HistoryInner::elementSubmitAddPollOption(FullMsgId context) {\n\tif (_overlayHost) {\n\t\t_overlayHost->triggerSubmit(context);\n\t}\n}\n\nvoid HistoryInner::hideElementOverlay() {\n\tif (_overlayHost) {\n\t\t_overlayHost->hide();\n\t}\n}\n\nvoid HistoryInner::elementOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) {\n\tconst auto draw = Data::CanSendAnyOf(\n\t\t_history->peer,\n\t\tData::FilesSendRestrictions());\n\t_controller->openPhoto(photo, { .id = context, .showDrawButton = draw });\n}\n\nvoid HistoryInner::elementOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView) {\n\tconst auto showDrawButton = Data::CanSendAnyOf(\n\t\t_history->peer,\n\t\tData::FilesSendRestrictions());\n\t_controller->openDocument(\n\t\tdocument,\n\t\tshowInMediaView,\n\t\t{ .id = context, .showDrawButton = showDrawButton });\n}\n\nvoid HistoryInner::elementCancelUpload(const FullMsgId &context) {\n\tif (const auto item = session().data().message(context)) {\n\t\t_controller->cancelUploadLayer(item);\n\t}\n}\n\nvoid HistoryInner::elementShowTooltip(\n\t\tconst TextWithEntities &text,\n\t\tFn<void()> hiddenCallback) {\n\t_widget->showInfoTooltip(text, std::move(hiddenCallback));\n}\n\nbool HistoryInner::elementAnimationsPaused() {\n\treturn _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any);\n}\n\nvoid HistoryInner::elementSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) {\n\t_widget->sendBotCommand({ _history->peer, command, context });\n}\n\nvoid HistoryInner::elementSearchInList(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) {\n\tconst auto inChat = Dialogs::Key(_history);\n\t_controller->searchMessages(query, inChat);\n}\n\nvoid HistoryInner::elementHandleViaClick(not_null<UserData*> bot) {\n\t_widget->insertBotCommand('@' + bot->username());\n}\n\nHistoryView::ElementChatMode HistoryInner::elementChatMode() {\n\tusing Mode = HistoryView::ElementChatMode;\n\treturn _isChatWide\n\t\t? Mode::Wide\n\t\t: _removeFromUserpics\n\t\t? Mode::Narrow\n\t\t: Mode::Default;\n}\n\nnot_null<Ui::PathShiftGradient*> HistoryInner::elementPathShiftGradient() {\n\treturn _pathGradient.get();\n}\n\nvoid HistoryInner::elementReplyTo(const FullReplyTo &to) {\n\treturn _widget->replyToMessage(to);\n}\n\nvoid HistoryInner::elementStartInteraction(not_null<const Element*> view) {\n\t_controller->emojiInteractions().startOutgoing(view);\n}\n\nvoid HistoryInner::elementStartPremium(\n\t\tnot_null<const Element*> view,\n\t\tElement *replacing) {\n\tconst auto already = !_emojiInteractions->playPremiumEffect(\n\t\tview,\n\t\treplacing);\n\t_animatedStickersPlayed.emplace(view->data());\n\tif (already) {\n\t\t_widget->showPremiumStickerTooltip(view);\n\t}\n}\n\nvoid HistoryInner::elementCancelPremium(not_null<const Element*> view) {\n\t_emojiInteractions->cancelPremiumEffect(view);\n}\n\nvoid HistoryInner::elementStartEffect(\n\t\tnot_null<const Element*> view,\n\t\tElement *replacing) {\n\t_emojiInteractions->playEffect(view);\n}\n\nauto HistoryInner::getSelectionState() const\n-> HistoryView::TopBarWidget::SelectedState {\n\tauto result = HistoryView::TopBarWidget::SelectedState {};\n\tfor (auto &selected : _selected) {\n\t\tif (selected.second == FullSelection) {\n\t\t\t++result.count;\n\t\t\tif (selected.first->canDelete()) {\n\t\t\t\t++result.canDeleteCount;\n\t\t\t}\n\t\t\tif (selected.first->allowsForward()) {\n\t\t\t\t++result.canForwardCount;\n\t\t\t}\n\t\t} else if (selected.second.from != selected.second.to) {\n\t\t\tresult.textSelected = true;\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid HistoryInner::clearSelected(bool onlyTextSelection) {\n\tif (!_selected.empty() && (!onlyTextSelection || _selected.cbegin()->second != FullSelection)) {\n\t\t_selected.clear();\n\t\t_widget->updateTopBarSelection();\n\t\t_widget->update();\n\t}\n}\n\nbool HistoryInner::hasSelectedItems() const {\n\treturn !_selected.empty() && _selected.cbegin()->second == FullSelection;\n}\n\nMessageIdsList HistoryInner::getSelectedItems() const {\n\tusing namespace ranges;\n\n\tif (!hasSelectedItems()) {\n\t\treturn {};\n\t}\n\n\tauto result = ranges::make_subrange(\n\t\t_selected.begin(),\n\t\t_selected.end()\n\t) | views::filter([](const auto &selected) {\n\t\tconst auto item = selected.first;\n\t\treturn item && !item->isService() && item->isRegular();\n\t}) | views::transform([](const auto &selected) {\n\t\treturn selected.first->fullId();\n\t}) | to_vector;\n\n\tresult |= actions::sort(less{}, [](const FullMsgId &msgId) {\n\t\treturn peerIsChannel(msgId.peer)\n\t\t\t? msgId.msg\n\t\t\t: (msgId.msg - ServerMaxMsgId);\n\t});\n\treturn result;\n}\n\nvoid HistoryInner::onTouchSelect() {\n\t_touchSelect = true;\n\t_touchMaybeSelecting = true;\n\tmouseActionStart(_touchPos, Qt::LeftButton);\n}\n\nauto HistoryInner::reactionButtonParameters(\n\tnot_null<const Element*> view,\n\tQPoint position,\n\tconst HistoryView::TextState &reactionState) const\n-> HistoryView::Reactions::ButtonParameters {\n\tif (!_useCornerReaction) {\n\t\treturn {};\n\t}\n\tconst auto top = itemTop(view);\n\tif (top < 0\n\t\t|| !view->data()->canReact()\n\t\t|| _mouseAction == MouseAction::Dragging\n\t\t|| _mouseAction == MouseAction::Selecting\n\t\t|| inSelectionMode().inSelectionMode) {\n\t\treturn {};\n\t}\n\tauto result = view->reactionButtonParameters(\n\t\tposition,\n\t\treactionState\n\t).translated({ 0, top });\n\tresult.visibleTop = _visibleAreaTop;\n\tresult.visibleBottom = _visibleAreaBottom;\n\tresult.globalPointer = _mousePosition;\n\treturn result;\n}\n\nauto HistoryInner::replyButtonParameters(\n\tnot_null<const Element*> view,\n\tQPoint position,\n\tconst HistoryView::TextState &replyState) const\n-> HistoryView::ReplyButton::ButtonParameters {\n\tif (!_useCornerReply) {\n\t\treturn {};\n\t}\n\tconst auto top = itemTop(view);\n\tif (top < 0\n\t\t|| _mouseAction == MouseAction::Dragging\n\t\t|| inSelectionMode().inSelectionMode) {\n\t\treturn {};\n\t}\n\tauto result = view->replyButtonParameters(\n\t\tposition,\n\t\treplyState\n\t).translated({ 0, top });\n\tresult.visibleTop = _visibleAreaTop;\n\tresult.visibleBottom = _visibleAreaBottom;\n\tresult.globalPointer = _mousePosition;\n\treturn result;\n}\n\nvoid HistoryInner::mouseActionUpdate() {\n\tif (hasPendingResizedItems()\n\t\t|| (!_mouseActive && !window()->isActiveWindow())) {\n\t\treturn;\n\t}\n\n\tauto mousePos = mapFromGlobal(_mousePosition);\n\tauto point = _widget->clampMousePosition(mousePos);\n\n\tauto m = QPoint();\n\n\tadjustCurrent(point.y());\n\tconst auto reactionState = _reactionsManager->buttonTextState(point);\n\tconst auto reactionItem = session().data().message(reactionState.itemId);\n\tconst auto reactionView = viewByItem(reactionItem);\n\tconst auto replyBtnState = reactionView\n\t\t? HistoryView::TextState()\n\t\t: _replyButtonManager->buttonTextState(point);\n\tconst auto replyBtnItem = session().data().message(replyBtnState.itemId);\n\tconst auto replyBtnView = viewByItem(replyBtnItem);\n\tconst auto view = reactionView\n\t\t? reactionView\n\t\t: replyBtnView\n\t\t? replyBtnView\n\t\t: (_aboutView\n\t\t\t&& _aboutView->view()\n\t\t\t&& point.y() >= _aboutView->top\n\t\t\t&& point.y() < _aboutView->top + _aboutView->view()->height())\n\t\t? _aboutView->view()\n\t\t: (_curHistory && !_curHistory->isEmpty())\n\t\t? _curHistory->blocks[_curBlock]->messages[_curItem].get()\n\t\t: nullptr;\n\tconst auto item = view ? view->data().get() : nullptr;\n\tconst auto selectionViewOffset = view\n\t\t? QPoint(SelectionViewOffset(this, view), 0)\n\t\t: QPoint(0, 0);\n\tpoint -= selectionViewOffset;\n\tif (view) {\n\t\tconst auto changed = (Element::Moused() != view);\n\t\tif (changed) {\n\t\t\trepaintItem(Element::Moused());\n\t\t\tElement::Moused(view);\n\t\t\trepaintItem(Element::Moused());\n\t\t}\n\t\tm = mapPointToItem(point, view);\n\t\t_reactionsManager->updateButton(reactionButtonParameters(\n\t\t\tview,\n\t\t\tm,\n\t\t\treactionState));\n\t\t_replyButtonManager->updateButton(replyButtonParameters(\n\t\t\tview,\n\t\t\tm,\n\t\t\treplyBtnState));\n\t\tif (changed) {\n\t\t\t_reactionsItem = item;\n\t\t}\n\t\tif (view->pointState(m) != PointState::Outside) {\n\t\t\tif (Element::Hovered() != view) {\n\t\t\t\trepaintItem(Element::Hovered());\n\t\t\t\tElement::Hovered(view);\n\t\t\t\trepaintItem(Element::Hovered());\n\t\t\t}\n\t\t} else if (Element::Hovered()) {\n\t\t\trepaintItem(Element::Hovered());\n\t\t\tElement::Hovered(nullptr);\n\t\t}\n\t} else {\n\t\tif (Element::Moused()) {\n\t\t\trepaintItem(Element::Moused());\n\t\t\tElement::Moused(nullptr);\n\t\t}\n\t\t_reactionsManager->updateButton({});\n\t\t_replyButtonManager->updateButton({});\n\t}\n\tif (_mouseActionItem && !viewByItem(_mouseActionItem)) {\n\t\tmouseActionCancel();\n\t}\n\n\tTextState dragState;\n\tClickHandlerHost *lnkhost = nullptr;\n\tauto dragStateUserpic = false;\n\tauto selectingText = (item == _mouseActionItem)\n\t\t&& (view == Element::Hovered())\n\t\t&& !_selected.empty()\n\t\t&& (_selected.cbegin()->second != FullSelection);\n\tconst auto overReaction = reactionView && reactionState.link;\n\tconst auto overReplyBtn = replyBtnView && replyBtnState.link;\n\tif (overReaction) {\n\t\tdragState = reactionState;\n\t\tlnkhost = reactionView;\n\t} else if (overReplyBtn) {\n\t\tdragState = replyBtnState;\n\t\tlnkhost = _replyButtonManager.get();\n\t} else if (item) {\n\t\tif (item != _mouseActionItem || ((m + selectionViewOffset) - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) {\n\t\t\tif (_mouseAction == MouseAction::PrepareDrag) {\n\t\t\t\t_mouseAction = MouseAction::Dragging;\n\t\t\t\tInvokeQueued(this, [=] { performDrag(); });\n\t\t\t} else if (_mouseAction == MouseAction::PrepareSelect) {\n\t\t\t\t_mouseAction = MouseAction::Selecting;\n\t\t\t}\n\t\t}\n\n\t\tauto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();\n\t\tauto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.);\n\t\tenumerateDates([&](not_null<Element*> view, int itemtop, int dateTop) {\n\t\t\t// stop enumeration if the date is above our point\n\t\t\tif (dateTop + dateHeight <= point.y()) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst auto displayDate = view->displayDate();\n\t\t\tauto dateInPlace = displayDate;\n\t\t\tif (dateInPlace) {\n\t\t\t\tconst auto correctDateTop = itemtop + st::msgServiceMargin.top();\n\t\t\t\tdateInPlace = (dateTop < correctDateTop + dateHeight);\n\t\t\t}\n\n\t\t\t// stop enumeration if we've found a date under the cursor\n\t\t\tif (dateTop <= point.y()) {\n\t\t\t\tauto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity;\n\t\t\t\tif (opacity > 0.) {\n\t\t\t\t\tconst auto item = view->data();\n\t\t\t\t\tauto dateWidth = 0;\n\t\t\t\t\tif (const auto date = view->Get<HistoryView::DateBadge>()) {\n\t\t\t\t\t\tdateWidth = date->width;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdateWidth = st::msgServiceFont->width(langDayOfMonthFull(view->dateTime().date()));\n\t\t\t\t\t}\n\t\t\t\t\tdateWidth += st::msgServicePadding.left() + st::msgServicePadding.right();\n\t\t\t\t\tauto dateLeft = st::msgServiceMargin.left();\n\t\t\t\t\tauto maxwidth = _contentWidth;\n\t\t\t\t\tif (_isChatWide) {\n\t\t\t\t\t\tmaxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));\n\t\t\t\t\t}\n\t\t\t\t\tauto widthForDate = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left();\n\n\t\t\t\t\tdateLeft += (widthForDate - dateWidth) / 2;\n\n\t\t\t\t\tif (point.x() >= dateLeft && point.x() < dateLeft + dateWidth) {\n\t\t\t\t\t\tif (!_scrollDateLink) {\n\t\t\t\t\t\t\t_scrollDateLink = std::make_shared<Window::DateClickHandler>(item->history(), view->dateTime().date());\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tstatic_cast<Window::DateClickHandler*>(_scrollDateLink.get())->setDate(view->dateTime().date());\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdragState = TextState(\n\t\t\t\t\t\t\tnullptr,\n\t\t\t\t\t\t\t_scrollDateLink);\n\t\t\t\t\t\t_dragStateItem = session().data().message(dragState.itemId);\n\t\t\t\t\t\tlnkhost = view;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\t\tif (!dragState.link) {\n\t\t\tenumerateForumThreadBars([&](not_null<Element*> view, int itemtop, int barTop) {\n\t\t\t\t// stop the enumeration if the bar is above our point\n\t\t\t\tif (barTop + dateHeight <= point.y()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tconst auto displayBar = view->displayForumThreadBar();\n\t\t\t\tauto barInPlace = displayBar;\n\t\t\t\tif (barInPlace) {\n\t\t\t\t\tconst auto correctBarTop = itemtop + view->displayedDateHeight() + st::msgServiceMargin.top();\n\t\t\t\t\tbarInPlace = (barTop < correctBarTop + st::msgServiceMargin.top());\n\t\t\t\t}\n\n\t\t\t\t// stop enumeration if we've found a bar under the cursor\n\t\t\t\tif (barTop <= point.y()) {\n\t\t\t\t\tconst auto item = view->data();\n\t\t\t\t\tauto barWidth = 0;\n\t\t\t\t\tif (const auto bar = view->Get<HistoryView::ForumThreadBar>()) {\n\t\t\t\t\t\tbarWidth = bar->width;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbarWidth = _forumThreadBarWidth;\n\t\t\t\t\t}\n\t\t\t\t\tauto barLeft = st::msgServiceMargin.left();\n\t\t\t\t\tauto maxwidth = _contentWidth;\n\t\t\t\t\tif (_isChatWide) {\n\t\t\t\t\t\tmaxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));\n\t\t\t\t\t}\n\t\t\t\t\tauto widthForBar = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left();\n\n\t\t\t\t\tbarLeft += (widthForBar - barWidth) / 2;\n\n\t\t\t\t\tif (point.x() >= barLeft && point.x() < barLeft + barWidth) {\n\t\t\t\t\t\tif (!_forumThreadBarLink) {\n\t\t\t\t\t\t\t_forumThreadBarLink = std::make_shared<Window::ForumThreadClickHandler>(item);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tstatic_cast<Window::ForumThreadClickHandler*>(_forumThreadBarLink.get())->update(item);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdragState = TextState(\n\t\t\t\t\t\t\tnullptr,\n\t\t\t\t\t\t\t_forumThreadBarLink);\n\t\t\t\t\t\t_dragStateItem = session().data().message(dragState.itemId);\n\t\t\t\t\t\tlnkhost = view;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\t\tif (!dragState.link) {\n\t\t\tStateRequest request;\n\t\t\tif (_mouseAction == MouseAction::Selecting) {\n\t\t\t\trequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t\t} else {\n\t\t\t\tselectingText = false;\n\t\t\t}\n\t\t\tif (base::IsAltPressed()) {\n\t\t\t\trequest.flags &= ~Ui::Text::StateRequest::Flag::LookupLink;\n\t\t\t}\n\t\t\tdragState = view->textState(m, request);\n\t\t\t_dragStateItem = session().data().message(dragState.itemId);\n\t\t\tlnkhost = view;\n\t\t\tif (!dragState.link && m.x() >= st::historyPhotoLeft && m.x() < st::historyPhotoLeft + st::msgPhotoSize) {\n\t\t\t\tif (!item->isService() && view->hasFromPhoto()) {\n\t\t\t\t\tenumerateUserpics([&](not_null<Element*> view, int userpicTop) -> bool {\n\t\t\t\t\t\t// stop enumeration if the userpic is below our point\n\t\t\t\t\t\tif (userpicTop > point.y()) {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// stop enumeration if we've found a userpic under the cursor\n\t\t\t\t\t\tif (point.y() >= userpicTop && point.y() < userpicTop + st::msgPhotoSize) {\n\t\t\t\t\t\t\tdragState = TextState(nullptr, view->fromPhotoLink());\n\t\t\t\t\t\t\tdragState.cursor = CursorState::FromPhoto;\n\t\t\t\t\t\t\tdragStateUserpic = true;\n\t\t\t\t\t\t\t_dragStateItem = nullptr;\n\t\t\t\t\t\t\tlnkhost = view;\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tauto lnkChanged = ClickHandler::setActive(dragState.link, lnkhost);\n\t_dragStateUserpic = dragStateUserpic;\n\tif (lnkChanged || dragState.cursor != _mouseCursorState) {\n\t\tUi::Tooltip::Hide();\n\t}\n\tif (dragState.link\n\t\t|| dragState.cursor == CursorState::Date\n\t\t|| dragState.cursor == CursorState::Forwarded\n\t\t|| dragState.cursor == CursorState::FromPhoto\n\t\t|| dragState.customTooltip) {\n\t\tUi::Tooltip::Show(1000, this);\n\t}\n\n\tQt::CursorShape cur = style::cur_default;\n\t_acceptsHorizontalScroll = dragState.horizontalScroll;\n\tif (_mouseAction == MouseAction::None) {\n\t\t_mouseCursorState = dragState.cursor;\n\t\tif (dragState.link) {\n\t\t\tcur = style::cur_pointer;\n\t\t} else if (_mouseCursorState == CursorState::Text && (_selected.empty() || _selected.cbegin()->second != FullSelection)) {\n\t\t\tcur = style::cur_text;\n\t\t} else if (_mouseCursorState == CursorState::Date) {\n\t\t\t//cur = style::cur_cross;\n\t\t}\n\t} else if (item) {\n\t\tif (_mouseAction == MouseAction::Selecting) {\n\t\t\tif (selectingText) {\n\t\t\t\tuint16 second = dragState.symbol;\n\t\t\t\tif (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) {\n\t\t\t\t\t++second;\n\t\t\t\t}\n\t\t\t\tauto selState = TextSelection { qMin(second, _mouseTextSymbol), qMax(second, _mouseTextSymbol) };\n\t\t\t\tif (_mouseSelectType != TextSelectType::Letters) {\n\t\t\t\t\tif (const auto view = viewByItem(_mouseActionItem)) {\n\t\t\t\t\t\tselState = view->adjustSelection(selState, _mouseSelectType);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!selState.empty()) {\n\t\t\t\t\t// We started selecting text in web page preview.\n\t\t\t\t\tClickHandler::unpressed();\n\t\t\t\t}\n\t\t\t\tif (_selected[_mouseActionItem] != selState) {\n\t\t\t\t\t_selected[_mouseActionItem] = selState;\n\t\t\t\t\trepaintItem(_mouseActionItem);\n\t\t\t\t}\n\t\t\t\tif (!_wasSelectedText && (selState == FullSelection || selState.from != selState.to)) {\n\t\t\t\t\t_wasSelectedText = true;\n\t\t\t\t\tsetFocus();\n\t\t\t\t}\n\t\t\t\tupdateDragSelection(nullptr, nullptr, false);\n\t\t\t} else {\n\t\t\t\tauto selectingDown = (itemTop(_mouseActionItem) < itemTop(item)) || (_mouseActionItem == item && _dragStartPosition.y() < m.y());\n\t\t\t\tauto dragSelFrom = viewByItem(_mouseActionItem);\n\t\t\t\tauto dragSelTo = view;\n\t\t\t\t// Maybe exclude dragSelFrom.\n\t\t\t\tif (dragSelFrom->pointState(_dragStartPosition) == PointState::Outside) {\n\t\t\t\t\tif (selectingDown) {\n\t\t\t\t\t\tif (_dragStartPosition.y() >= dragSelFrom->height() - dragSelFrom->marginBottom() || ((view == dragSelFrom) && (m.y() < _dragStartPosition.y() + QApplication::startDragDistance() || m.y() < dragSelFrom->marginTop()))) {\n\t\t\t\t\t\t\tdragSelFrom = (dragSelFrom != dragSelTo)\n\t\t\t\t\t\t\t\t? nextItem(dragSelFrom)\n\t\t\t\t\t\t\t\t: nullptr;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (_dragStartPosition.y() < dragSelFrom->marginTop() || ((view == dragSelFrom) && (m.y() >= _dragStartPosition.y() - QApplication::startDragDistance() || m.y() >= dragSelFrom->height() - dragSelFrom->marginBottom()))) {\n\t\t\t\t\t\t\tdragSelFrom = (dragSelFrom != dragSelTo)\n\t\t\t\t\t\t\t\t? prevItem(dragSelFrom)\n\t\t\t\t\t\t\t\t: nullptr;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (_mouseActionItem != item) { // maybe exclude dragSelTo\n\t\t\t\t\tif (selectingDown) {\n\t\t\t\t\t\tif (m.y() < dragSelTo->marginTop()) {\n\t\t\t\t\t\t\tdragSelTo = (dragSelFrom != dragSelTo)\n\t\t\t\t\t\t\t\t? prevItem(dragSelTo)\n\t\t\t\t\t\t\t\t: nullptr;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (m.y() >= dragSelTo->height() - dragSelTo->marginBottom()) {\n\t\t\t\t\t\t\tdragSelTo = (dragSelFrom != dragSelTo)\n\t\t\t\t\t\t\t\t? nextItem(dragSelTo)\n\t\t\t\t\t\t\t\t: nullptr;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tauto dragSelecting = false;\n\t\t\t\tauto dragFirstAffected = dragSelFrom;\n\t\t\t\twhile (dragFirstAffected\n\t\t\t\t\t&& (!dragFirstAffected->data()->isRegular()\n\t\t\t\t\t\t|| dragFirstAffected->data()->isService())) {\n\t\t\t\t\tdragFirstAffected = (dragFirstAffected != dragSelTo)\n\t\t\t\t\t\t? (selectingDown\n\t\t\t\t\t\t\t? nextItem(dragFirstAffected)\n\t\t\t\t\t\t\t: prevItem(dragFirstAffected))\n\t\t\t\t\t\t: nullptr;\n\t\t\t\t}\n\t\t\t\tif (dragFirstAffected) {\n\t\t\t\t\tauto i = _selected.find(dragFirstAffected->data());\n\t\t\t\t\tdragSelecting = (i == _selected.cend() || i->second != FullSelection);\n\t\t\t\t}\n\t\t\t\tupdateDragSelection(dragSelFrom, dragSelTo, dragSelecting);\n\t\t\t}\n\t\t} else if (_mouseAction == MouseAction::Dragging) {\n\t\t}\n\n\t\tif (ClickHandler::getPressed()) {\n\t\t\tcur = style::cur_pointer;\n\t\t} else if ((_mouseAction == MouseAction::Selecting)\n\t\t\t&& !_selected.empty()\n\t\t\t&& (_selected.cbegin()->second != FullSelection)) {\n\t\t\tif (!_dragSelFrom || !_dragSelTo) {\n\t\t\t\tcur = style::cur_text;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Voice message seek support.\n\tif (const auto pressedItem = _dragStateItem) {\n\t\tif (const auto pressedView = viewByItem(pressedItem)) {\n\t\t\tif (pressedItem->history() == _history || pressedItem->history() == _migrated) {\n\t\t\t\tauto adjustedPoint = mapPointToItem(point, pressedView);\n\t\t\t\tpressedView->updatePressed(adjustedPoint);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (_mouseAction == MouseAction::Selecting) {\n\t\t_selectScroll.checkDeltaScroll(\n\t\t\tmousePos,\n\t\t\t_scroll->scrollTop(),\n\t\t\t_scroll->scrollTop() + _scroll->height());\n\t} else {\n\t\tupdateDragSelection(nullptr, nullptr, false);\n\t\t_selectScroll.cancel();\n\t}\n\n\tif (_mouseAction == MouseAction::None && (lnkChanged || cur != _cursor)) {\n\t\tsetCursor(_cursor = cur);\n\t}\n}\n\nvoid HistoryInner::updateDragSelection(Element *dragSelFrom, Element *dragSelTo, bool dragSelecting) {\n\tif (_dragSelFrom == dragSelFrom && _dragSelTo == dragSelTo && _dragSelecting == dragSelecting) {\n\t\treturn;\n\t} else if (dragSelFrom && hasSelectRestriction()) {\n\t\tupdateDragSelection(nullptr, nullptr, false);\n\t\treturn;\n\t}\n\t_dragSelFrom = dragSelFrom;\n\t_dragSelTo = dragSelTo;\n\tint32 fromy = itemTop(_dragSelFrom), toy = itemTop(_dragSelTo);\n\tif (fromy >= 0 && toy >= 0 && fromy > toy) {\n\t\tstd::swap(_dragSelFrom, _dragSelTo);\n\t}\n\t_dragSelecting = dragSelecting;\n\tif (!_wasSelectedText && _dragSelFrom && _dragSelTo && _dragSelecting) {\n\t\t_wasSelectedText = true;\n\t\tsetFocus();\n\t}\n\tupdate();\n}\n\nint HistoryInner::historyHeight() const {\n\tint result = 0;\n\tif (_history->isEmpty()) {\n\t\tresult += _migrated ? _migrated->height() : 0;\n\t} else {\n\t\tresult += _history->height() - _historySkipHeight + (_migrated ? _migrated->height() : 0);\n\t}\n\treturn result;\n}\n\nint HistoryInner::historyScrollTop() const {\n\tauto htop = historyTop();\n\tauto mtop = migratedTop();\n\tif (htop >= 0 && _history->scrollTopItem) {\n\t\treturn htop + _history->scrollTopItem->block()->y() + _history->scrollTopItem->y() + _history->scrollTopOffset;\n\t}\n\tif (mtop >= 0 && _migrated->scrollTopItem) {\n\t\treturn mtop + _migrated->scrollTopItem->block()->y() + _migrated->scrollTopItem->y() + _migrated->scrollTopOffset;\n\t}\n\treturn ScrollMax;\n}\n\nint HistoryInner::migratedTop() const {\n\treturn (_migrated && !_migrated->isEmpty()) ? _historyMarginTop : -1;\n}\n\nint HistoryInner::historyTop() const {\n\tint mig = migratedTop();\n\treturn !_history->isEmpty()\n\t\t? (mig >= 0\n\t\t\t? (mig + _migrated->height() - _historySkipHeight)\n\t\t\t: _historyMarginTop)\n\t\t: -1;\n}\n\nint HistoryInner::historyDrawTop() const {\n\tauto top = historyTop();\n\treturn (top >= 0) ? (top + _historySkipHeight) : -1;\n}\n\nvoid HistoryInner::setChooseReportReason(Data::ReportInput reportInput) {\n\t_chooseForReportReason = reportInput;\n}\n\nvoid HistoryInner::clearChooseReportReason() {\n\t_chooseForReportReason = std::nullopt;\n}\n\nauto HistoryInner::viewByItem(const HistoryItem *item) const -> Element* {\n\treturn !item\n\t\t? nullptr\n\t\t: (_aboutView && _aboutView->item() == item)\n\t\t? _aboutView->view()\n\t\t: item->mainView();\n}\n\n// -1 if should not be visible, -2 if bad history()\nint HistoryInner::itemTop(const HistoryItem *item) const {\n\treturn item ? itemTop(viewByItem(item)) : -2;\n}\n\nint HistoryInner::itemTop(const Element *view) const {\n\tif (!view) {\n\t\treturn -1;\n\t} else if (_aboutView && view == _aboutView->view()) {\n\t\treturn _aboutView->top;\n\t} else if (view->data()->mainView() != view) {\n\t\treturn -1;\n\t}\n\n\tconst auto top = (view->history() == _history)\n\t\t? historyTop()\n\t\t: (view->history() == _migrated\n\t\t\t? migratedTop()\n\t\t\t: -2);\n\treturn (top < 0) ? top : (top + view->y() + view->block()->y());\n}\n\nauto HistoryInner::findViewForPinnedTracking(int top) const\n-> std::pair<Element*, int> {\n\tconst auto normalTop = historyTop();\n\tconst auto oldTop = migratedTop();\n\tconst auto fromHistory = [&](not_null<History*> history, int historyTop)\n\t-> std::pair<Element*, int> {\n\t\tauto [view, offset] = history->findItemAndOffset(top - historyTop);\n\t\twhile (view && !view->data()->isRegular()) {\n\t\t\toffset -= view->height();\n\t\t\tview = view->nextInBlocks();\n\t\t}\n\t\treturn { view, offset };\n\t};\n\tif (normalTop >= 0 && (oldTop < 0 || top >= normalTop)) {\n\t\treturn fromHistory(_history, normalTop);\n\t} else if (oldTop >= 0) {\n\t\tauto [view, offset] = fromHistory(_migrated, oldTop);\n\t\tif (!view && normalTop >= 0) {\n\t\t\treturn fromHistory(_history, normalTop);\n\t\t}\n\t\treturn { view, offset };\n\t}\n\treturn { nullptr, 0 };\n}\n\nvoid HistoryInner::refreshAboutView(bool force) {\n\tconst auto refresh = [&] {\n\t\tif (force) {\n\t\t\t_aboutView = nullptr;\n\t\t}\n\t\tif (!_aboutView) {\n\t\t\t_aboutView = std::make_unique<HistoryView::AboutView>(\n\t\t\t\t_history,\n\t\t\t\t_history->delegateMixin()->delegate());\n\t\t\t_aboutView->refreshRequests() | rpl::on_next([=] {\n\t\t\t\tupdateBotInfo();\n\t\t\t}, _aboutView->lifetime());\n\t\t\t_aboutView->destroyRequests() | rpl::on_next([=] {\n\t\t\t\tcrl::on_main(this, [=] {\n\t\t\t\t\trefreshAboutView(true);\n\t\t\t\t\tupdate();\n\t\t\t\t});\n\t\t\t}, _aboutView->lifetime());\n\t\t\t_aboutView->sendIntroSticker() | rpl::start_to_stream(\n\t\t\t\t_sendIntroSticker,\n\t\t\t\t_aboutView->lifetime());\n\t\t}\n\t};\n\tif (const auto user = _peer->asUser()) {\n\t\tif (const auto info = user->botInfo.get()) {\n\t\t\trefresh();\n\t\t\tif (!info->inited) {\n\t\t\t\tsession().api().requestFullPeer(user);\n\t\t\t}\n\t\t} else if (!user->isContact()\n\t\t\t&& !user->phoneCountryCode().isEmpty()) {\n\t\t\trefresh();\n\t\t} else if (!historyHeight()) {\n\t\t\tif (user->starsPerMessage() > 0\n\t\t\t\t|| (user->requiresPremiumToWrite()\n\t\t\t\t\t&& !user->session().premium())\n\t\t\t\t|| user->isFullLoaded()) {\n\t\t\t\trefresh();\n\t\t\t} else {\n\t\t\t\tsession().api().requestFullPeer(user);\n\t\t\t}\n\t\t}\n\t} else if (const auto monoforum = _peer->asChannel()) {\n\t\tif (monoforum->isMonoforum() && !monoforum->amMonoforumAdmin()) {\n\t\t\trefresh();\n\t\t}\n\t}\n}\n\nvoid HistoryInner::notifyMigrateUpdated() {\n\tconst auto migrated = _history->migrateFrom();\n\tif (_migrated != migrated) {\n\t\tif (_migrated) {\n\t\t\t_migrated->delegateMixin()->setCurrent(nullptr);\n\t\t}\n\t\t_migrated = migrated;\n\t\tif (_migrated) {\n\t\t\t_migrated->delegateMixin()->setCurrent(this);\n\t\t\t_migrated->translateTo(_history->translatedTo());\n\t\t}\n\t}\n}\n\nvoid HistoryInner::applyDragSelection() {\n\tif (!hasSelectRestriction()) {\n\t\tapplyDragSelection(&_selected);\n\t}\n}\n\nbool HistoryInner::isSelected(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<HistoryItem*> item) const {\n\tconst auto i = toItems->find(item);\n\treturn (i != toItems->cend()) && (i->second == FullSelection);\n}\n\nbool HistoryInner::isSelectedGroup(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<const Data::Group*> group) const {\n\tfor (const auto &other : group->items) {\n\t\tif (!isSelected(toItems, other)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nbool HistoryInner::isSelectedAsGroup(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<HistoryItem*> item) const {\n\tif (const auto group = session().data().groups().find(item)) {\n\t\treturn isSelectedGroup(toItems, group);\n\t}\n\treturn isSelected(toItems, item);\n}\n\nbool HistoryInner::goodForSelection(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<HistoryItem*> item,\n\t\tint &totalCount) const {\n\tif (!item->isRegular() || item->isService()) {\n\t\treturn false;\n\t} else if (toItems->find(item) == toItems->end()) {\n\t\t++totalCount;\n\t}\n\treturn true;\n}\n\nvoid HistoryInner::addToSelection(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<HistoryItem*> item) const {\n\tconst auto i = toItems->find(item);\n\tif (i == toItems->cend()) {\n\t\tif (toItems->size() == 1\n\t\t\t&& toItems->begin()->second != FullSelection) {\n\t\t\ttoItems->clear();\n\t\t}\n\t\ttoItems->emplace(item, FullSelection);\n\t} else if (i->second != FullSelection) {\n\t\ti->second = FullSelection;\n\t}\n}\n\nvoid HistoryInner::removeFromSelection(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<HistoryItem*> item) const {\n\tconst auto i = toItems->find(item);\n\tif (i != toItems->cend()) {\n\t\ttoItems->erase(i);\n\t}\n}\n\nvoid HistoryInner::changeSelection(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<HistoryItem*> item,\n\t\tSelectAction action) const {\n\tif (action == SelectAction::Invert) {\n\t\taction = isSelected(toItems, item)\n\t\t\t? SelectAction::Deselect\n\t\t\t: SelectAction::Select;\n\t}\n\tauto total = int(toItems->size());\n\tconst auto add = (action == SelectAction::Select);\n\tif (add\n\t\t&& goodForSelection(toItems, item, total)\n\t\t&& total <= MaxSelectedItems) {\n\t\taddToSelection(toItems, item);\n\t} else {\n\t\tremoveFromSelection(toItems, item);\n\t}\n}\n\nvoid HistoryInner::changeSelectionAsGroup(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<HistoryItem*> item,\n\t\tSelectAction action) const {\n\tconst auto group = session().data().groups().find(item);\n\tif (!group) {\n\t\treturn changeSelection(toItems, item, action);\n\t}\n\tif (action == SelectAction::Invert) {\n\t\taction = isSelectedAsGroup(toItems, item)\n\t\t\t? SelectAction::Deselect\n\t\t\t: SelectAction::Select;\n\t}\n\tauto total = int(toItems->size());\n\tconst auto canSelect = [&] {\n\t\tfor (const auto &other : group->items) {\n\t\t\tif (!goodForSelection(toItems, other, total)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn (total <= MaxSelectedItems);\n\t}();\n\tif (action == SelectAction::Select && canSelect) {\n\t\tfor (const auto &other : group->items) {\n\t\t\taddToSelection(toItems, other);\n\t\t}\n\t} else {\n\t\tfor (const auto &other : group->items) {\n\t\t\tremoveFromSelection(toItems, other);\n\t\t}\n\t}\n}\n\nvoid HistoryInner::forwardItem(FullMsgId itemId) {\n\tWindow::ShowForwardMessagesBox(_controller, { 1, itemId });\n}\n\nvoid HistoryInner::forwardAsGroup(FullMsgId itemId) {\n\tif (const auto item = session().data().message(itemId)) {\n\t\tWindow::ShowForwardMessagesBox(\n\t\t\t_controller,\n\t\t\tsession().data().itemOrItsGroup(item));\n\t}\n}\n\nvoid HistoryInner::deleteItem(FullMsgId itemId) {\n\tif (const auto item = session().data().message(itemId)) {\n\t\tdeleteItem(item);\n\t}\n}\n\nvoid HistoryInner::deleteItem(not_null<HistoryItem*> item) {\n\tif (item->isUploading()) {\n\t\t_controller->cancelUploadLayer(item);\n\t\treturn;\n\t}\n\tconst auto list = HistoryItemsList{ item };\n\tif (CanCreateModerateMessagesBox(list)) {\n\t\tconst auto opt = DefaultModerateMessagesBoxOptions();\n\t\t_controller->show(Box(CreateModerateMessagesBox, list, nullptr, opt));\n\t} else {\n\t\tconst auto suggestModerate = false;\n\t\t_controller->show(Box<DeleteMessagesBox>(item, suggestModerate));\n\t}\n}\n\nbool HistoryInner::hasPendingResizedItems() const {\n\treturn _history->hasPendingResizedItems()\n\t\t|| (_migrated && _migrated->hasPendingResizedItems());\n}\n\nvoid HistoryInner::deleteAsGroup(FullMsgId itemId) {\n\tif (const auto item = session().data().message(itemId)) {\n\t\tconst auto group = session().data().groups().find(item);\n\t\tif (!group) {\n\t\t\treturn deleteItem(item);\n\t\t} else if (CanCreateModerateMessagesBox(group->items)) {\n\t\t\t_controller->show(Box(\n\t\t\t\tCreateModerateMessagesBox,\n\t\t\t\tgroup->items,\n\t\t\t\tnullptr,\n\t\t\t\tModerateMessagesBoxOptions{}));\n\t\t} else {\n\t\t\t_controller->show(Box<DeleteMessagesBox>(\n\t\t\t\t&session(),\n\t\t\t\tsession().data().itemsToIds(group->items)));\n\t\t}\n\t}\n}\n\nvoid HistoryInner::reportItem(FullMsgId itemId) {\n\tShowReportMessageBox(_controller->uiShow(), _peer, { itemId.msg }, {});\n}\n\nvoid HistoryInner::reportAsGroup(FullMsgId itemId) {\n\tif (const auto item = session().data().message(itemId)) {\n\t\tconst auto group = session().data().groups().find(item);\n\t\tconst auto ids = group\n\t\t\t? (ranges::views::all(\n\t\t\t\tgroup->items\n\t\t\t) | ranges::views::transform([](const auto &i) {\n\t\t\t\treturn i->fullId().msg;\n\t\t\t}) | ranges::to_vector)\n\t\t\t: std::vector<MsgId>{ 1, itemId.msg };\n\t\tShowReportMessageBox(_controller->uiShow(), _peer, ids, {});\n\t}\n}\n\nvoid HistoryInner::blockSenderItem(FullMsgId itemId) {\n\tif ([[maybe_unused]] const auto item = session().data().message(itemId)) {\n\t\t_controller->show(Box(\n\t\t\tWindow::BlockSenderFromRepliesBox,\n\t\t\t_controller,\n\t\t\titemId));\n\t}\n}\n\nvoid HistoryInner::blockSenderAsGroup(FullMsgId itemId) {\n\tblockSenderItem(itemId);\n}\n\nvoid HistoryInner::addSelectionRange(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<History*> history,\n\t\tint fromblock,\n\t\tint fromitem,\n\t\tint toblock,\n\t\tint toitem) const {\n\tif (fromblock >= 0 && fromitem >= 0 && toblock >= 0 && toitem >= 0) {\n\t\tfor (; fromblock <= toblock; ++fromblock) {\n\t\t\tauto block = history->blocks[fromblock].get();\n\t\t\tfor (int cnt = (fromblock < toblock) ? block->messages.size() : (toitem + 1); fromitem < cnt; ++fromitem) {\n\t\t\t\tauto item = block->messages[fromitem]->data();\n\t\t\t\tchangeSelectionAsGroup(toItems, item, SelectAction::Select);\n\t\t\t}\n\t\t\tif (toItems->size() >= MaxSelectedItems) break;\n\t\t\tfromitem = 0;\n\t\t}\n\t}\n}\n\nvoid HistoryInner::applyDragSelection(\n\t\tnot_null<SelectedItems*> toItems) const {\n\tconst auto selfromy = itemTop(_dragSelFrom);\n\tconst auto seltoy = [&] {\n\t\tauto result = itemTop(_dragSelTo);\n\t\treturn (result < 0) ? result : (result + _dragSelTo->height());\n\t}();\n\tif (selfromy < 0 || seltoy < 0) {\n\t\treturn;\n\t}\n\n\tif (!toItems->empty() && toItems->cbegin()->second != FullSelection) {\n\t\ttoItems->clear();\n\t}\n\tconst auto botAboutView = _aboutView ? _aboutView->view() : nullptr;\n\tif (_dragSelecting) {\n\t\tauto fromblock = (_dragSelFrom != botAboutView)\n\t\t\t? _dragSelFrom->block()->indexInHistory()\n\t\t\t: _history->blocks.empty()\n\t\t\t? -1\n\t\t\t: 0;\n\t\tauto fromitem = (_dragSelFrom != botAboutView)\n\t\t\t? _dragSelFrom->indexInBlock()\n\t\t\t: (_history->blocks.empty()\n\t\t\t\t|| _history->blocks[0]->messages.empty())\n\t\t\t? -1\n\t\t\t: 0;\n\t\tauto toblock = (_dragSelTo != botAboutView)\n\t\t\t? _dragSelTo->block()->indexInHistory()\n\t\t\t: _history->blocks.empty()\n\t\t\t? -1\n\t\t\t: 0;\n\t\tauto toitem = (_dragSelTo != botAboutView)\n\t\t\t? _dragSelTo->indexInBlock()\n\t\t\t: (_history->blocks.empty()\n\t\t\t\t|| _history->blocks[0]->messages.empty())\n\t\t\t? -1\n\t\t\t: 0;\n\t\tif (_migrated) {\n\t\t\tif (_dragSelFrom->history() == _migrated) {\n\t\t\t\tif (_dragSelTo->history() == _migrated) {\n\t\t\t\t\taddSelectionRange(toItems, _migrated, fromblock, fromitem, toblock, toitem);\n\t\t\t\t\ttoblock = -1;\n\t\t\t\t\ttoitem = -1;\n\t\t\t\t} else {\n\t\t\t\t\taddSelectionRange(toItems, _migrated, fromblock, fromitem, _migrated->blocks.size() - 1, _migrated->blocks.back()->messages.size() - 1);\n\t\t\t\t}\n\t\t\t\tfromblock = 0;\n\t\t\t\tfromitem = 0;\n\t\t\t} else if (_dragSelTo->history() == _migrated) { // wtf\n\t\t\t\ttoblock = -1;\n\t\t\t\ttoitem = -1;\n\t\t\t}\n\t\t}\n\t\taddSelectionRange(toItems, _history, fromblock, fromitem, toblock, toitem);\n\t} else {\n\t\tauto toRemove = std::vector<not_null<HistoryItem*>>();\n\t\tfor (const auto &item : *toItems) {\n\t\t\tauto iy = itemTop(item.first);\n\t\t\tif (iy < -1) {\n\t\t\t\ttoRemove.emplace_back(item.first);\n\t\t\t} else if (iy >= 0 && iy >= selfromy && iy < seltoy) {\n\t\t\t\ttoRemove.emplace_back(item.first);\n\t\t\t}\n\t\t}\n\t\tfor (const auto &item : toRemove) {\n\t\t\tchangeSelectionAsGroup(toItems, item, SelectAction::Deselect);\n\t\t}\n\t}\n}\n\nQString HistoryInner::tooltipText() const {\n\tif (_mouseCursorState == CursorState::Date\n\t\t&& _mouseAction == MouseAction::None) {\n\t\tif (const auto view = Element::Hovered()) {\n\t\t\treturn HistoryView::DateTooltipText(view);\n\t\t}\n\t}\n\tif (_mouseCursorState == CursorState::Forwarded\n\t\t&& _mouseAction == MouseAction::None) {\n\t\tif (const auto view = Element::Moused()) {\n\t\t\tif (const auto forwarded = view->data()->Get<HistoryMessageForwarded>()) {\n\t\t\t\treturn forwarded->text.toString();\n\t\t\t}\n\t\t}\n\t}\n\tif (const auto lnk = ClickHandler::getActive()) {\n\t\tusing namespace HistoryView::Reactions;\n\t\tconst auto count = ReactionCountOfLink(_dragStateItem, lnk);\n\t\tif (count.count && count.shortened) {\n\t\t\treturn Lang::FormatCountDecimal(count.count);\n\t\t}\n\t\tif (const auto text = lnk->tooltip(); !text.isEmpty()) {\n\t\t\treturn text;\n\t\t}\n\t}\n\tif (const auto view = Element::Moused()) {\n\t\tif (_mouseCursorState == CursorState::FromPhoto) {\n\t\t\tif (const auto from = view->data()->displayFrom()) {\n\t\t\t\treturn from->name();\n\t\t\t}\n\t\t}\n\t\tStateRequest request;\n\t\tconst auto local = mapFromGlobal(_mousePosition);\n\t\tconst auto point = _widget->clampMousePosition(local);\n\t\trequest.flags |= Ui::Text::StateRequest::Flag::LookupCustomTooltip;\n\t\tconst auto state = view->textState(\n\t\t\tmapPointToItem(point, view),\n\t\t\trequest);\n\t\treturn state.customTooltipText;\n\t}\n\treturn QString();\n}\n\nQPoint HistoryInner::tooltipPos() const {\n\treturn _mousePosition;\n}\n\nbool HistoryInner::tooltipWindowActive() const {\n\treturn Ui::AppInFocus() && Ui::InFocusChain(window());\n}\n\nvoid HistoryInner::onParentGeometryChanged() {\n\tauto mousePos = QCursor::pos();\n\tauto mouseOver = _widget->rect().contains(_widget->mapFromGlobal(mousePos));\n\tauto needToUpdate = (_mouseAction != MouseAction::None || _touchScroll || mouseOver);\n\tif (needToUpdate) {\n\t\tmouseActionUpdate(mousePos);\n\t}\n}\n\nbool HistoryInner::consumeScrollAction(QPoint delta) {\n\tconst auto horizontal = (std::abs(delta.x()) > std::abs(delta.y()));\n\tif (!horizontal || !_acceptsHorizontalScroll || !Element::Moused()) {\n\t\treturn false;\n\t}\n\tconst auto position = mapPointToItem(\n\t\tmapFromGlobal(_mousePosition),\n\t\tElement::Moused());\n\treturn Element::Moused()->consumeHorizontalScroll(position, delta.x());\n}\n\nFn<HistoryView::ElementDelegate*()> HistoryInner::elementDelegateFactory(\n\t\tFullMsgId itemId) const {\n\tconst auto weak = base::make_weak(_controller);\n\treturn [=]() -> HistoryView::ElementDelegate* {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tauto &data = strong->session().data();\n\t\t\tif (const auto item = data.message(itemId)) {\n\t\t\t\tconst auto history = item->history();\n\t\t\t\treturn history->delegateMixin()->delegate();\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t};\n}\n\nClickHandlerContext HistoryInner::prepareClickHandlerContext(\n\t\tFullMsgId itemId) const {\n\treturn ClickHandlerContext{\n\t\t.itemId = itemId,\n\t\t.elementDelegate = elementDelegateFactory(itemId),\n\t\t.sessionWindow = base::make_weak(_controller),\n\t};\n}\n\nClickContext HistoryInner::prepareClickContext(\n\t\tQt::MouseButton button,\n\t\tFullMsgId itemId) const {\n\treturn {\n\t\tbutton,\n\t\tQVariant::fromValue(prepareClickHandlerContext(itemId)),\n\t};\n}\n\nauto HistoryInner::sendIntroSticker() const\n-> rpl::producer<not_null<DocumentData*>> {\n\treturn _sendIntroSticker.events();\n}\n\nauto HistoryInner::DelegateMixin()\n-> std::unique_ptr<HistoryMainElementDelegateMixin> {\n\treturn std::make_unique<HistoryMainElementDelegate>();\n}\n\nvoid HistoryInner::addDepersonalized(not_null<Ui::RpWidget*> parent) {\n\tconst auto button = Ui::CreateChild<Ui::IconButton>(\n\t\tparent,\n\t\tst::botDownloadCancel);\n\tbutton->setIconOverride(&st::menuIconStealth, &st::menuIconStealth);\n\tparent->sizeValue() | rpl::on_next([=](QSize s) {\n\t\tbutton->move(\n\t\t\ts.width() - button->width() * 1.5,\n\t\t\t(s.height() - button->height()) / 2);\n\t}, button->lifetime());\n\tbutton->show();\n\n\tbutton->setClickedCallback([=] {\n\t\tif (!showCopyRestrictionForSelected()) {\n\t\t\tTextUtilities::SetClipboardText(getSelectedText(true));\n\t\t\tif (_menu) {\n\t\t\t\t_menu->hide();\n\t\t\t}\n\t\t}\n\t});\n}\n\nbool CanSendReply(not_null<const HistoryItem*> item) {\n\tconst auto peer = item->history()->peer;\n\tif (const auto topic = item->topic()) {\n\t\treturn Data::CanSendAnything(topic);\n\t} else if (!Data::CanSendAnything(peer)) {\n\t\treturn false;\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tif (const auto sublist = item->savedSublist()) {\n\t\t\treturn (sublist->sublistPeer() != peer);\n\t\t}\n\t\treturn channel->amIn();\n\t}\n\treturn true;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_inner_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"data/data_report.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/controls/swipe_handler_data.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/thanos_effect_controller.h\"\n#include \"ui/dragging_scroll_manager.h\"\n#include \"ui/widgets/middle_click_autoscroll.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/userpic_view.h\"\n#include \"history/view/history_view_top_bar_widget.h\"\n\n#include <QtGui/QPainterPath>\n\nstruct ClickContext;\nstruct ClickHandlerContext;\n\nnamespace Data {\nstruct Group;\n} // namespace Data\n\nnamespace HistoryView {\nclass ElementDelegate;\nclass EmojiInteractions;\nstruct TextState;\nstruct SelectionModeResult;\nstruct StateRequest;\nenum class CursorState : char;\nenum class PointState : char;\nenum class ElementChatMode : char;\nclass ElementOverlayHost;\nclass EmptyPainter;\nclass Element;\nclass TranslateTracker;\nclass ReadMetricsTracker;\nstruct PinnedId;\nstruct SelectedQuote;\nclass AboutView;\n} // namespace HistoryView\n\nnamespace HistoryView::Reactions {\nclass Manager;\nstruct ChosenReaction;\nstruct ButtonParameters;\n} // namespace HistoryView::Reactions\n\nnamespace HistoryView::ReplyButton {\nclass Manager;\nstruct ButtonParameters;\n} // namespace HistoryView::ReplyButton\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass ChatTheme;\nclass ChatStyle;\nclass PopupMenu;\nstruct ChatPaintContext;\nclass PathShiftGradient;\nstruct PeerUserpicView;\n} // namespace Ui\n\nnamespace Dialogs::Ui {\nusing namespace ::Ui;\nclass VideoUserpic;\n} // namespace Dialogs::Ui\n\nclass HistoryInner;\nclass HistoryMainElementDelegate;\nclass HistoryMainElementDelegateMixin {\npublic:\n\tvoid setCurrent(HistoryInner *widget) {\n\t\t_widget = widget;\n\t}\n\n\tvirtual not_null<HistoryView::ElementDelegate*> delegate() = 0;\n\tvirtual ~HistoryMainElementDelegateMixin();\n\nprivate:\n\tfriend class HistoryMainElementDelegate;\n\n\tHistoryMainElementDelegateMixin();\n\n\tHistoryInner *_widget = nullptr;\n\n};\n\nclass HistoryWidget;\nclass HistoryInner\n\t: public Ui::RpWidget\n\t, public Ui::AbstractTooltipShower {\npublic:\n\tusing Element = HistoryView::Element;\n\n\tHistoryInner(\n\t\tnot_null<HistoryWidget*> historyWidget,\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<History*> history);\n\t~HistoryInner();\n\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] not_null<Ui::ChatTheme*> theme() const {\n\t\treturn _theme.get();\n\t}\n\n\tUi::ChatPaintContext preparePaintContext(const QRect &clip) const;\n\n\tusing CollapseGap = Ui::CollapseGap;\n\tvoid setCollapseGaps(std::vector<CollapseGap> gaps);\n\n\tvoid messagesReceived(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QVector<MTPMessage> &messages);\n\tvoid messagesReceivedDown(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QVector<MTPMessage> &messages);\n\n\t[[nodiscard]] TextForMimeData getSelectedText(bool depersonalized = false) const;\n\n\tvoid touchScrollUpdated(const QPoint &screenPos);\n\n\tvoid setItemsRevealHeight(int revealHeight);\n\tvoid changeItemsRevealHeight(int revealHeight);\n\tvoid checkActivation();\n\tvoid recountHistoryGeometry(bool initial = false);\n\tvoid updateSize();\n\tvoid setShownPinned(HistoryItem *item);\n\n\tvoid repaintItem(const HistoryItem *item);\n\tvoid repaintItem(const Element *view);\n\tvoid repaintItem(const Element *view, QRect rect);\n\n\t[[nodiscard]] bool canCopySelected() const;\n\t[[nodiscard]] bool canDeleteSelected() const;\n\n\t[[nodiscard]] auto getSelectionState() const\n\t\t-> HistoryView::TopBarWidget::SelectedState;\n\tvoid clearSelected(bool onlyTextSelection = false);\n\t[[nodiscard]] MessageIdsList getSelectedItems() const;\n\t[[nodiscard]] bool hasSelectedItems() const;\n\t[[nodiscard]] HistoryView::SelectionModeResult inSelectionMode() const;\n\t[[nodiscard]] bool elementIntersectsRange(\n\t\tnot_null<const Element*> view,\n\t\tint from,\n\t\tint till) const;\n\tvoid elementStartStickerLoop(not_null<const Element*> view);\n\tvoid elementShowPollResults(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context);\n\tvoid elementShowAddPollOption(\n\t\tnot_null<HistoryView::Element*> view,\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context,\n\t\tQRect optionRect);\n\tvoid elementSubmitAddPollOption(FullMsgId context);\n\tvoid hideElementOverlay();\n\tvoid elementOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context);\n\tvoid elementOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView = false);\n\tvoid elementCancelUpload(const FullMsgId &context);\n\tvoid elementShowTooltip(\n\t\tconst TextWithEntities &text,\n\t\tFn<void()> hiddenCallback);\n\tbool elementAnimationsPaused();\n\tvoid elementSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context);\n\tvoid elementSearchInList(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context);\n\tvoid elementHandleViaClick(not_null<UserData*> bot);\n\tHistoryView::ElementChatMode elementChatMode();\n\tnot_null<Ui::PathShiftGradient*> elementPathShiftGradient();\n\tvoid elementReplyTo(const FullReplyTo &to);\n\tvoid elementStartInteraction(not_null<const Element*> view);\n\tvoid elementStartPremium(\n\t\tnot_null<const Element*> view,\n\t\tElement *replacing);\n\tvoid elementCancelPremium(not_null<const Element*> view);\n\tvoid elementStartEffect(\n\t\tnot_null<const Element*> view,\n\t\tElement *replacing);\n\n\tvoid startEffectOnRead(not_null<HistoryItem*> item);\n\tvoid updateBotInfo(bool recount = true);\n\n\tbool wasSelectedText() const;\n\n\t// updates history->scrollTopItem/scrollTopOffset\n\tvoid visibleAreaUpdated(int top, int bottom);\n\n\tint historyHeight() const;\n\tint historyScrollTop() const;\n\tint migratedTop() const;\n\tint historyTop() const;\n\tint historyDrawTop() const;\n\n\tvoid setChooseReportReason(Data::ReportInput reportInput);\n\tvoid clearChooseReportReason();\n\tvoid toggleRemoveFromUserpics(bool remove);\n\n\t// -1 if should not be visible, -2 if bad history()\n\t[[nodiscard]] int itemTop(const HistoryItem *item) const;\n\t[[nodiscard]] int itemTop(const Element *view) const;\n\t[[nodiscard]] Element *viewByItem(const HistoryItem *item) const;\n\n\t// Returns (view, offset-from-top).\n\t[[nodiscard]] std::pair<Element*, int> findViewForPinnedTracking(\n\t\tint top) const;\n\n\tvoid refreshAboutView(bool force = false);\n\tvoid notifyMigrateUpdated();\n\n\t// Ui::AbstractTooltipShower interface.\n\tQString tooltipText() const override;\n\tQPoint tooltipPos() const override;\n\tbool tooltipWindowActive() const override;\n\n\tvoid onParentGeometryChanged();\n\tbool consumeScrollAction(QPoint delta);\n\n\t[[nodiscard]] Fn<HistoryView::ElementDelegate*()> elementDelegateFactory(\n\t\tFullMsgId itemId) const;\n\t[[nodiscard]] ClickHandlerContext prepareClickHandlerContext(\n\t\tFullMsgId itemId) const;\n\t[[nodiscard]] ClickContext prepareClickContext(\n\t\tQt::MouseButton button,\n\t\tFullMsgId itemId) const;\n\n\t[[nodiscard]] auto sendIntroSticker() const\n\t\t-> rpl::producer<not_null<DocumentData*>>;\n\n\t[[nodiscard]] static auto DelegateMixin()\n\t-> std::unique_ptr<HistoryMainElementDelegateMixin>;\n\nprotected:\n\tbool focusNextPrevChild(bool next) override;\n\n\tbool eventHook(QEvent *e) override; // calls touchEvent when necessary\n\tvoid touchEvent(QTouchEvent *e);\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid mouseDoubleClickEvent(QMouseEvent *e) override;\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\nprivate:\n\tvoid onTouchSelect();\n\tvoid onTouchScrollTimer();\n\tvoid markReadMetricsStale();\n\tvoid registerReadMetricsActivity();\n\n\t[[nodiscard]] static int SelectionViewOffset(\n\t\tnot_null<const HistoryInner*> inner,\n\t\tnot_null<const Element*> view);\n\n\tusing ChosenReaction = HistoryView::Reactions::ChosenReaction;\n\tusing VideoUserpic = Dialogs::Ui::VideoUserpic;\n\tusing SelectedItems\n\t\t= base::flat_map<HistoryItem*, TextSelection, std::less<>>;\n\tenum class MouseAction {\n\t\tNone,\n\t\tPrepareDrag,\n\t\tDragging,\n\t\tPrepareSelect,\n\t\tSelecting,\n\t};\n\tenum class SelectAction {\n\t\tSelect,\n\t\tDeselect,\n\t\tInvert,\n\t};\n\tenum class EnumItemsDirection {\n\t\tTopToBottom,\n\t\tBottomToTop,\n\t};\n\tusing CursorState = HistoryView::CursorState;\n\tusing PointState = HistoryView::PointState;\n\tusing TextState = HistoryView::TextState;\n\tusing StateRequest = HistoryView::StateRequest;\n\n\t// This function finds all history items that are displayed and calls template method\n\t// for each found message (in given direction) in the passed history with passed top offset.\n\t//\n\t// Method has \"bool (*Method)(not_null<Element*> view, int itemtop, int itembottom)\" signature\n\t// if it returns false the enumeration stops immediately.\n\ttemplate <bool TopToBottom, typename Method>\n\tvoid enumerateItemsInHistory(History *history, int historytop, Method method);\n\n\ttemplate <EnumItemsDirection direction, typename Method>\n\tvoid enumerateItems(Method method) {\n\t\tconstexpr auto TopToBottom = (direction == EnumItemsDirection::TopToBottom);\n\t\tif (TopToBottom && _migrated) {\n\t\t\tenumerateItemsInHistory<TopToBottom>(_migrated, migratedTop(), method);\n\t\t}\n\t\tenumerateItemsInHistory<TopToBottom>(_history, historyTop(), method);\n\t\tif (!TopToBottom && _migrated) {\n\t\t\tenumerateItemsInHistory<TopToBottom>(_migrated, migratedTop(), method);\n\t\t}\n\t}\n\n\t// This function finds all userpics on the left that are displayed and calls template method\n\t// for each found userpic (from the top to the bottom) using enumerateItems() method.\n\t//\n\t// Method has \"bool (*Method)(not_null<Element*> view, int userpicTop)\" signature\n\t// if it returns false the enumeration stops immediately.\n\ttemplate <typename Method>\n\tvoid enumerateUserpics(Method method);\n\n\t// This function finds all date elements that are displayed and calls template method\n\t// for each found date element (from the bottom to the top) using enumerateItems() method.\n\t//\n\t// Method has \"bool (*Method)(not_null<Element*> view, int itemtop, int dateTop)\" signature\n\t// if it returns false the enumeration stops immediately.\n\ttemplate <typename Method>\n\tvoid enumerateDates(Method method);\n\n\t// This function finds all forum thread bar elements that are displayed and calls template method\n\t// for each found date element (from the bottom to the top) using enumerateItems() method.\n\t//\n\t// Method has \"bool (*Method)(not_null<Element*> view, int itemtop, int dateTop)\" signature\n\t// if it returns false the enumeration stops immediately.\n\ttemplate <typename Method>\n\tvoid enumerateForumThreadBars(Method method);\n\n\tvoid scrollDateCheck();\n\tvoid scrollDateHideByTimer();\n\tbool canHaveFromUserpics() const;\n\tvoid mouseActionStart(const QPoint &screenPos, Qt::MouseButton button);\n\tvoid mouseActionUpdate();\n\tvoid mouseActionUpdate(const QPoint &screenPos);\n\tvoid mouseActionFinish(const QPoint &screenPos, Qt::MouseButton button);\n\tvoid mouseActionCancel();\n\tstd::unique_ptr<QMimeData> prepareDrag();\n\tvoid performDrag();\n\n\tvoid paintEmpty(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tint width,\n\t\tint height);\n\n\tQPoint mapPointToItem(QPoint p, const Element *view) const;\n\tQPoint mapPointToItem(QPoint p, const HistoryItem *item) const;\n\t[[nodiscard]] HistoryView::SelectedQuote selectedQuote(\n\t\tnot_null<HistoryItem*> item) const;\n\n\tvoid showContextMenu(QContextMenuEvent *e, bool showFromTouch = false);\n\tvoid cancelContextDownload(not_null<DocumentData*> document);\n\tvoid openContextGif(FullMsgId itemId);\n\tvoid saveContextGif(FullMsgId itemId);\n\tvoid copyContextText(FullMsgId itemId);\n\tvoid showContextInFolder(not_null<DocumentData*> document);\n\tvoid savePhotoToFile(not_null<PhotoData*> photo);\n\tvoid saveDocumentToFile(\n\t\tFullMsgId contextId,\n\t\tnot_null<DocumentData*> document);\n\tvoid copyContextImage(not_null<PhotoData*> photo, FullMsgId itemId);\n\tvoid showStickerPackInfo(not_null<DocumentData*> document);\n\n\tvoid itemRemoved(not_null<const HistoryItem*> item);\n\tvoid viewRemoved(not_null<const Element*> view);\n\n\tvoid touchResetSpeed();\n\tvoid touchUpdateSpeed();\n\tvoid touchDeaccelerate(int32 elapsed);\n\n\tvoid adjustCurrent(int32 y) const;\n\tvoid adjustCurrent(int32 y, History *history) const;\n\tElement *prevItem(Element *item);\n\tElement *nextItem(Element *item);\n\tvoid updateDragSelection(Element *dragSelFrom, Element *dragSelTo, bool dragSelecting);\n\tTextSelection itemRenderSelection(\n\t\tnot_null<Element*> view,\n\t\tint selfromy,\n\t\tint seltoy) const;\n\tTextSelection computeRenderSelection(\n\t\tnot_null<const SelectedItems*> selected,\n\t\tnot_null<Element*> view) const;\n\n\tvoid toggleScrollDateShown();\n\tvoid repaintScrollDateCallback();\n\tbool displayScrollDate() const;\n\tvoid scrollDateHide();\n\tvoid keepScrollDateForNow();\n\n\tvoid applyDragSelection();\n\tvoid applyDragSelection(not_null<SelectedItems*> toItems) const;\n\tvoid addSelectionRange(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<History*> history,\n\t\tint fromblock,\n\t\tint fromitem,\n\t\tint toblock,\n\t\tint toitem) const;\n\tbool isSelected(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<HistoryItem*> item) const;\n\tbool isSelectedGroup(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<const Data::Group*> group) const;\n\tbool isSelectedAsGroup(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<HistoryItem*> item) const;\n\tbool goodForSelection(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<HistoryItem*> item,\n\t\tint &totalCount) const;\n\tvoid addToSelection(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<HistoryItem*> item) const;\n\tvoid removeFromSelection(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<HistoryItem*> item) const;\n\tvoid changeSelection(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<HistoryItem*> item,\n\t\tSelectAction action) const;\n\tvoid changeSelectionAsGroup(\n\t\tnot_null<SelectedItems*> toItems,\n\t\tnot_null<HistoryItem*> item,\n\t\tSelectAction action) const;\n\tvoid forwardItem(FullMsgId itemId);\n\tvoid forwardAsGroup(FullMsgId itemId);\n\tvoid deleteItem(not_null<HistoryItem*> item);\n\tvoid deleteItem(FullMsgId itemId);\n\tvoid deleteAsGroup(FullMsgId itemId);\n\tvoid reportItem(FullMsgId itemId);\n\tvoid reportAsGroup(FullMsgId itemId);\n\tvoid blockSenderItem(FullMsgId itemId);\n\tvoid blockSenderAsGroup(FullMsgId itemId);\n\tvoid copySelectedText();\n\tvoid editCaptionUploadLayer(not_null<HistoryItem*> item);\n\tvoid addSearchAction();\n\n\t[[nodiscard]] auto reactionButtonParameters(\n\t\tnot_null<const Element*> view,\n\t\tQPoint position,\n\t\tconst HistoryView::TextState &reactionState) const\n\t-> HistoryView::Reactions::ButtonParameters;\n\t[[nodiscard]] auto replyButtonParameters(\n\t\tnot_null<const Element*> view,\n\t\tQPoint position,\n\t\tconst HistoryView::TextState &replyState) const\n\t-> HistoryView::ReplyButton::ButtonParameters;\n\tvoid toggleFavoriteReaction(not_null<Element*> view) const;\n\tvoid reactionChosen(const ChosenReaction &reaction);\n\n\tvoid setupSharingDisallowed();\n\tvoid setupSwipeReplyAndBack();\n\t[[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const;\n\t[[nodiscard]] bool hasCopyMediaRestriction(\n\t\tnot_null<HistoryItem*> item) const;\n\tbool showCopyRestriction(HistoryItem *item = nullptr);\n\tbool showCopyMediaRestriction(not_null<HistoryItem*> item);\n\t[[nodiscard]] bool hasCopyRestrictionForSelected() const;\n\tbool showCopyRestrictionForSelected();\n\t[[nodiscard]] bool hasSelectRestriction() const;\n\n\tVideoUserpic *validateVideoUserpic(not_null<PeerData*> peer);\n\n\t// Does any of the shown histories has this flag set.\n\tbool hasPendingResizedItems() const;\n\n\tvoid addDepersonalized(not_null<Ui::RpWidget*> parent);\n\n\tconst not_null<HistoryWidget*> _widget;\n\tconst not_null<Ui::ScrollArea*> _scroll;\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<PeerData*> _peer;\n\tconst not_null<History*> _history;\n\tconst not_null<HistoryView::ElementDelegate*> _elementDelegate;\n\tconst std::unique_ptr<HistoryView::EmojiInteractions> _emojiInteractions;\n\tstd::shared_ptr<Ui::ChatTheme> _theme;\n\n\tHistory *_migrated = nullptr;\n\tHistoryView::ElementDelegate *_migratedElementDelegate = nullptr;\n\tint _contentWidth = 0;\n\tint _historyMarginTop = 0;\n\tint _historyMarginBottom = 0;\n\tint _revealHeight = 0;\n\tint _forumThreadBarWidth = 0;\n\tUi::PeerUserpicView _forumThreadBarUserpicView;\n\n\t// Save visible area coords for painting / pressing userpics.\n\tint _visibleAreaTop = 0;\n\tint _visibleAreaBottom = 0;\n\n\t// With migrated history we perhaps do not need to display\n\t// the first _history message date (just skip it by height).\n\tint _historySkipHeight = 0;\n\n\tstd::unique_ptr<HistoryView::AboutView> _aboutView;\n\tstd::unique_ptr<HistoryView::EmptyPainter> _emptyPainter;\n\tstd::unique_ptr<HistoryView::TranslateTracker> _translateTracker;\n\tstd::unique_ptr<HistoryView::ReadMetricsTracker> _readMetricsTracker;\n\tbool _readMetricsStale = false;\n\trpl::event_stream<not_null<DocumentData*>> _sendIntroSticker;\n\n\tmutable History *_curHistory = nullptr;\n\tmutable int _curBlock = 0;\n\tmutable int _curItem = 0;\n\n\tstyle::cursor _cursor = style::cur_default;\n\tSelectedItems _selected;\n\tstd::optional<Data::ReportInput> _chooseForReportReason;\n\n\tconst std::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\tQPainterPath _highlightPathCache;\n\tbool _isChatWide = false;\n\tbool _removeFromUserpics = false;\n\n\tbase::flat_set<not_null<const HistoryItem*>> _animatedStickersPlayed;\n\tbase::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> _userpics;\n\tbase::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> _userpicsCache;\n\tbase::flat_map<MsgId, Ui::PeerUserpicView> _hiddenSenderUserpics;\n\tbase::flat_map<\n\t\tnot_null<PeerData*>,\n\t\tstd::unique_ptr<VideoUserpic>> _videoUserpics;\n\n\tstd::unique_ptr<HistoryView::Reactions::Manager> _reactionsManager;\n\trpl::variable<HistoryItem*> _reactionsItem;\n\tstd::unique_ptr<HistoryView::ReplyButton::Manager> _replyButtonManager;\n\tHistoryItem *_pinnedItem = nullptr;\n\n\tMouseAction _mouseAction = MouseAction::None;\n\tTextSelectType _mouseSelectType = TextSelectType::Letters;\n\tQPoint _dragStartPosition;\n\tQPoint _mousePosition;\n\tHistoryItem *_mouseActionItem = nullptr;\n\tHistoryItem *_dragStateItem = nullptr;\n\tCursorState _mouseCursorState = CursorState();\n\tuint16 _mouseTextSymbol = 0;\n\tbool _mouseActive = false;\n\tbool _dragStateUserpic = false;\n\tbool _pressWasInactive = false;\n\tbool _recountedAfterPendingResizedItems = false;\n\tbool _useCornerReply = false;\n\tbool _useCornerReaction = false;\n\tbool _acceptsHorizontalScroll = false;\n\tbool _horizontalScrollLocked = false;\n\n\tQPoint _trippleClickPoint;\n\tbase::Timer _trippleClickTimer;\n\n\tElement *_dragSelFrom = nullptr;\n\tElement *_dragSelTo = nullptr;\n\tbool _dragSelecting = false;\n\tbool _wasSelectedText = false; // was some text selected in current drag action\n\n\tmutable bool _lastInSelectionMode = false;\n\tmutable Ui::Animations::Simple _inSelectionModeAnimation;\n\n\t// scroll by touch support (at least Windows Surface tablets)\n\tbool _touchScroll = false;\n\tbool _touchSelect = false;\n\tbool _touchInProgress = false;\n\tQPoint _touchStart, _touchPrevPos, _touchPos;\n\trpl::variable<bool> _touchMaybeSelecting;\n\tbase::Timer _touchSelectTimer;\n\n\tUi::DraggingScrollManager _selectScroll;\n\n\trpl::variable<bool> _sharingDisallowed = false;\n\n\tUi::TouchScrollState _touchScrollState = Ui::TouchScrollState::Manual;\n\tbool _touchPrevPosValid = false;\n\tbool _touchWaitingAcceleration = false;\n\tQPoint _touchSpeed;\n\tcrl::time _touchSpeedTime = 0;\n\tcrl::time _touchAccelerationTime = 0;\n\tcrl::time _touchTime = 0;\n\tbase::Timer _touchScrollTimer;\n\tUi::MiddleClickAutoscroll _middleClickAutoscroll;\n\n\tUi::Controls::SwipeContextData _gestureHorizontal;\n\tUi::Controls::SwipeBackResult _swipeBackData;\n\n\t// _menu must be destroyed before _whoReactedMenuLifetime.\n\trpl::lifetime _whoReactedMenuLifetime;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tbool _scrollDateShown = false;\n\tUi::Animations::Simple _scrollDateOpacity;\n\tSingleQueuedInvokation _scrollDateCheck;\n\tbase::Timer _scrollDateHideTimer;\n\tElement *_scrollDateLastItem = nullptr;\n\tint _scrollDateLastItemTop = 0;\n\tClickHandlerPtr _scrollDateLink;\n\tClickHandlerPtr _forumThreadBarLink;\n\n\t[[nodiscard]] HistoryView::ElementOverlayHost &ensureOverlayHost();\n\tstd::unique_ptr<HistoryView::ElementOverlayHost> _overlayHost;\n\n\tvoid setupThanosEffect();\n\n\tstd::unique_ptr<Ui::ThanosEffectController> _thanosController;\n\tstd::vector<CollapseGap> _collapseGaps;\n\n};\n\n[[nodiscard]] bool CanSendReply(not_null<const HistoryItem*> item);\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_item.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_item.h\"\n\n#include \"api/api_premium.h\"\n#include \"api/api_sensitive_content.h\"\n#include \"api/api_transcribes.h\"\n#include \"lang/lang_keys.h\"\n#include \"calls/calls_instance.h\" // Core::App().calls().joinGroupCall.\n#include \"history/view/history_view_item_preview.h\"\n#include \"history/view/history_view_message.h\"\n#include \"history/view/history_view_service_message.h\"\n#include \"history/view/media/history_view_media_grouped.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history_unread_things.h\"\n#include \"history/history.h\"\n#include \"iv/iv_data.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_isolated_emoji.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"settings/settings_credits_graphics.h\" // ShowRefundInfoBox.\n#include \"storage/file_upload.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"main/main_session.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session_settings.h\"\n#include \"menu/menu_ttl_validator.h\"\n#include \"apiwrap.h\"\n#include \"media/audio/media_audio.h\"\n#include \"core/application.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"core/click_handler_types.h\"\n#include \"base/unixtime.h\"\n#include \"base/timer_rpl.h\"\n#include \"boxes/send_credits_box.h\"\n#include \"api/api_text_entities.h\"\n#include \"api/api_updates.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/components/scheduled_messages.h\"\n#include \"data/components/sponsored_messages.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/data_bot_app.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_session.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_game.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_history_messages.h\"\n#include \"data/data_user.h\"\n#include \"data/data_group_call.h\" // Data::GroupCall::id().\n#include \"data/data_poll.h\" // PollData::publicVotes.\n#include \"data/data_todo_list.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_web_page.h\"\n#include \"chat_helpers/stickers_gift_box_pack.h\"\n#include \"payments/payments_checkout_process.h\" // CheckoutProcess::Start.\n#include \"payments/payments_non_panel_process.h\" // ProcessNonPanelPaymentFormFactory.\n#include \"platform/platform_notifications_manager.h\"\n#include \"spellcheck/spellcheck_highlight_syntax.h\"\n\nnamespace {\n\nconstexpr auto kNotificationTextLimit = 255;\nconstexpr auto kPinnedMessageTextLimit = 16;\n\nusing ItemPreview = HistoryView::ItemPreview;\n\ntemplate <typename T>\n[[nodiscard]] PreparedServiceText PrepareEmptyText(const T &) {\n\treturn PreparedServiceText();\n};\n\ntemplate <typename T>\n[[nodiscard]] PreparedServiceText PrepareErrorText(const T &data) {\n\tif constexpr (!std::is_same_v<T, MTPDmessageActionEmpty>) {\n\t\tconst auto name = QString::fromUtf8(typeid(data).name());\n\t\tLOG((\"API Error: %1 received.\").arg(name));\n\t}\n\treturn PreparedServiceText{ { tr::lng_message_empty(tr::now) } };\n}\n\n[[nodiscard]] TextWithEntities SpoilerLoginCode(TextWithEntities text) {\n\tconst auto r = QRegularExpression(\n\t\tu\"(?<![\\\\w\\\\-#])(\\\\d[\\\\d\\\\-]{2,6}\\\\d)(?!\\\\w\\\\-)\"_q);\n\tconst auto m = r.match(text.text);\n\tif (!m.hasMatch()) {\n\t\treturn text;\n\t}\n\tconst auto codeStart = int(m.capturedStart(1));\n\tconst auto codeLength = int(m.capturedLength(1));\n\tauto i = text.entities.begin();\n\tconst auto e = text.entities.end();\n\twhile (i != e && i->offset() < codeStart) {\n\t\tif (i->offset() + i->length() > codeStart) {\n\t\t\treturn text; // Entities should not intersect code.\n\t\t}\n\t\t++i;\n\t}\n\ttext.entities.insert(i, { EntityType::Spoiler, codeStart, codeLength });\n\treturn text;\n}\n\n[[nodiscard]] bool HasNotEmojiAndSpaces(const QString &text) {\n\tif (text.isEmpty()) {\n\t\treturn false;\n\t}\n\tauto emoji = 0;\n\tauto start = text.data();\n\tconst auto end = start + text.size();\n\twhile (start < end) {\n\t\tif (start->isSpace()) {\n\t\t\t++start;\n\t\t} else if (Ui::Emoji::Find(start, end, &emoji)) {\n\t\t\tstart += emoji;\n\t\t} else {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n[[nodiscard]] HistoryItemCommonFields ForwardedFields(\n\t\tHistoryItemCommonFields fields,\n\t\tnot_null<History*> history,\n\t\tnot_null<HistoryItem*> original) {\n\tif (fields.flags & MessageFlag::FakeHistoryItem) {\n\t\treturn fields;\n\t}\n\tfields.flags |= NewForwardedFlags(history->peer, fields.from, original);\n\treturn fields;\n}\n\n[[nodiscard]] TextWithEntities AmountAndStarCurrency(\n\t\tint64 amount,\n\t\tconst QString &currency) {\n\tif (currency == Ui::kCreditsCurrency) {\n\t\treturn Ui::CreditsEmojiSmall().append(\n\t\t\tLang::FormatCountDecimal(std::abs(amount)));\n\t}\n\treturn { Ui::FillAmountAndCurrency(amount, currency) };\n}\n\n} // namespace\n\nvoid HistoryItem::HistoryItem::Destroyer::operator()(HistoryItem *value) {\n\tif (value) {\n\t\tvalue->destroy();\n\t}\n}\n\nstruct HistoryItem::CreateConfig {\n\tReplyFields reply;\n\n\tUserId viaBotId = 0;\n\tUserId viaBusinessBotId = 0;\n\tint viewsCount = -1;\n\tint forwardsCount = -1;\n\tint boostsApplied = 0;\n\tQString postAuthor;\n\n\tMsgId originalId = 0;\n\tTimeId originalDate = 0;\n\tPeerId originalSenderId = 0;\n\tQString originalSenderName;\n\tQString originalPostAuthor;\n\n\tPeerId savedSublistPeer = 0;\n\n\tQString forwardPsaType;\n\tPeerId savedFromPeer = 0;\n\tMsgId savedFromMsgId = 0;\n\n\tPeerId savedFromSenderId = 0;\n\tQString savedFromSenderName;\n\tTimeId savedFromDate = 0;\n\tbool savedFromOutgoing = false;\n\n\tTimeId editDate = 0;\n\tTimeId scheduleRepeatPeriod = 0;\n\tHistoryMessageMarkupData markup;\n\tHistoryMessageRepliesData replies;\n\tHistoryMessageSuggestInfo suggest;\n\tbool imported = false;\n\n\t// For messages created from existing messages (forwarded).\n\tconst HistoryMessageReplyMarkup *inlineMarkup = nullptr;\n\n\tstd::vector<Data::UnavailableReason> restrictions;\n};\n\nvoid HistoryItem::FillForwardedInfo(\n\t\tCreateConfig &config,\n\t\tconst MTPDmessageFwdHeader &data) {\n\tconfig.originalId = data.vchannel_post().value_or_empty();\n\tconfig.originalDate = data.vdate().v;\n\tif (const auto fromId = data.vfrom_id()) {\n\t\tconfig.originalSenderId = peerFromMTP(*fromId);\n\t}\n\tconfig.originalSenderName = qs(data.vfrom_name().value_or_empty());\n\tconfig.originalPostAuthor = qs(data.vpost_author().value_or_empty());\n\tconfig.forwardPsaType = qs(data.vpsa_type().value_or_empty());\n\tconst auto savedFromPeer = data.vsaved_from_peer();\n\tconst auto savedFromMsgId = data.vsaved_from_msg_id();\n\tif (savedFromPeer && savedFromMsgId) {\n\t\tconfig.savedFromPeer = peerFromMTP(*savedFromPeer);\n\t\tconfig.savedFromMsgId = savedFromMsgId->v;\n\t}\n\tconfig.savedFromSenderId = data.vsaved_from_id()\n\t\t? peerFromMTP(*data.vsaved_from_id())\n\t\t: PeerId();\n\tconfig.savedFromSenderName = qs(\n\t\tdata.vsaved_from_name().value_or_empty());\n\tconfig.savedFromOutgoing = data.is_saved_out();\n\tconfig.savedFromDate = data.vsaved_date().value_or_empty();\n\n\tconfig.imported = data.is_imported();\n}\n\nstd::unique_ptr<Data::Media> HistoryItem::CreateMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPMessageMedia &media) {\n\tusing Result = std::unique_ptr<Data::Media>;\n\treturn media.match([&](const MTPDmessageMediaContact &media) -> Result {\n\t\treturn std::make_unique<Data::MediaContact>(\n\t\t\titem,\n\t\t\tmedia.vuser_id().v,\n\t\t\tqs(media.vfirst_name()),\n\t\t\tqs(media.vlast_name()),\n\t\t\tqs(media.vphone_number()),\n\t\t\tData::SharedContact::ParseVcard(qs(media.vvcard())));\n\t}, [&](const MTPDmessageMediaGeo &media) -> Result {\n\t\treturn media.vgeo().match([&](const MTPDgeoPoint &point) -> Result {\n\t\t\treturn std::make_unique<Data::MediaLocation>(\n\t\t\t\titem,\n\t\t\t\tData::LocationPoint(point));\n\t\t}, [](const MTPDgeoPointEmpty &) -> Result {\n\t\t\treturn nullptr;\n\t\t});\n\t}, [&](const MTPDmessageMediaGeoLive &media) -> Result {\n\t\treturn media.vgeo().match([&](const MTPDgeoPoint &point) -> Result {\n\t\t\treturn std::make_unique<Data::MediaLocation>(\n\t\t\t\titem,\n\t\t\t\tData::LocationPoint(point),\n\t\t\t\tmedia.vperiod().v);\n\t\t}, [](const MTPDgeoPointEmpty &) -> Result {\n\t\t\treturn nullptr;\n\t\t});\n\t}, [&](const MTPDmessageMediaVenue &media) -> Result {\n\t\treturn media.vgeo().match([&](const MTPDgeoPoint &point) -> Result {\n\t\t\treturn std::make_unique<Data::MediaLocation>(\n\t\t\t\titem,\n\t\t\t\tData::LocationPoint(point),\n\t\t\t\tqs(media.vtitle()),\n\t\t\t\tqs(media.vaddress()));\n\t\t}, [](const MTPDgeoPointEmpty &data) -> Result {\n\t\t\treturn nullptr;\n\t\t});\n\t}, [&](const MTPDmessageMediaPhoto &media) -> Result {\n\t\tconst auto photo = media.vphoto();\n\t\tif (media.vttl_seconds()) {\n\t\t\tLOG((\"App Error: \"\n\t\t\t\t\"Unexpected MTPMessageMediaPhoto \"\n\t\t\t\t\"with ttl_seconds in CreateMedia.\"));\n\t\t\treturn nullptr;\n\t\t} else if (!photo) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"Got MTPMessageMediaPhoto \"\n\t\t\t\t\"without photo and without ttl_seconds.\"));\n\t\t\treturn nullptr;\n\t\t}\n\t\treturn photo->match([&](const MTPDphoto &photo) -> Result {\n\t\t\treturn std::make_unique<Data::MediaPhoto>(\n\t\t\t\titem,\n\t\t\t\titem->history()->owner().processPhoto(photo),\n\t\t\t\tmedia.is_spoiler());\n\t\t}, [](const MTPDphotoEmpty &) -> Result {\n\t\t\treturn nullptr;\n\t\t});\n\t}, [&](const MTPDmessageMediaDocument &media) -> Result {\n\t\tconst auto document = media.vdocument();\n\t\tif (media.vttl_seconds() && media.is_video()) {\n\t\t\tLOG((\"App Error: \"\n\t\t\t\t\"Unexpected MTPMessageMediaDocument \"\n\t\t\t\t\"with ttl_seconds in CreateMedia.\"));\n\t\t\treturn nullptr;\n\t\t} else if (!document) {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"Got MTPMessageMediaDocument \"\n\t\t\t\t\"without document and without ttl_seconds.\"));\n\t\t\treturn nullptr;\n\t\t}\n\t\treturn document->match([&](const MTPDdocument &document) -> Result {\n\t\t\tconst auto list = media.valt_documents();\n\t\t\tconst auto owner = &item->history()->owner();\n\t\t\tconst auto data = owner->processDocument(document, list);\n\t\t\tusing Args = Data::MediaFile::Args;\n\t\t\treturn std::make_unique<Data::MediaFile>(item, data, Args{\n\t\t\t\t.ttlSeconds = media.vttl_seconds().value_or_empty(),\n\t\t\t\t.videoCover = (media.vvideo_cover()\n\t\t\t\t\t? owner->processPhoto(*media.vvideo_cover()).get()\n\t\t\t\t\t: nullptr),\n\t\t\t\t.videoTimestamp = media.vvideo_timestamp().value_or_empty(),\n\t\t\t\t.hasQualitiesList = list && !list->v.isEmpty(),\n\t\t\t\t.skipPremiumEffect = media.is_nopremium(),\n\t\t\t\t.spoiler = media.is_spoiler(),\n\t\t\t});\n\t\t}, [](const MTPDdocumentEmpty &) -> Result {\n\t\t\treturn nullptr;\n\t\t});\n\t}, [&](const MTPDmessageMediaWebPage &media) {\n\t\tusing Flag = MediaWebPageFlag;\n\t\tconst auto flags = Flag()\n\t\t\t| (media.is_force_large_media()\n\t\t\t\t? Flag::ForceLargeMedia\n\t\t\t\t: Flag())\n\t\t\t| (media.is_force_small_media()\n\t\t\t\t? Flag::ForceSmallMedia\n\t\t\t\t: Flag())\n\t\t\t| (media.is_manual() ? Flag::Manual : Flag())\n\t\t\t| (media.is_safe() ? Flag::Safe : Flag());\n\t\treturn media.vwebpage().match([](const MTPDwebPageEmpty &) -> Result {\n\t\t\treturn nullptr;\n\t\t}, [&](const MTPDwebPagePending &webpage) -> Result {\n\t\t\treturn std::make_unique<Data::MediaWebPage>(\n\t\t\t\titem,\n\t\t\t\titem->history()->owner().processWebpage(webpage),\n\t\t\t\tflags);\n\t\t}, [&](const MTPDwebPage &webpage) -> Result {\n\t\t\treturn std::make_unique<Data::MediaWebPage>(\n\t\t\t\titem,\n\t\t\t\titem->history()->owner().processWebpage(webpage),\n\t\t\t\tflags);\n\t\t}, [](const MTPDwebPageNotModified &) -> Result {\n\t\t\tLOG((\"API Error: \"\n\t\t\t\t\"webPageNotModified is unexpected in message media.\"));\n\t\t\treturn nullptr;\n\t\t});\n\t}, [&](const MTPDmessageMediaGame &media) -> Result {\n\t\treturn media.vgame().match([&](const MTPDgame &game) {\n\t\t\treturn std::make_unique<Data::MediaGame>(\n\t\t\t\titem,\n\t\t\t\titem->history()->owner().processGame(game));\n\t\t});\n\t}, [&](const MTPDmessageMediaInvoice &media) -> Result {\n\t\treturn std::make_unique<Data::MediaInvoice>(\n\t\t\titem,\n\t\t\tData::ComputeInvoiceData(item, media));\n\t}, [&](const MTPDmessageMediaPoll &media) -> Result {\n\t\treturn std::make_unique<Data::MediaPoll>(\n\t\t\titem,\n\t\t\titem->history()->owner().processPoll(media));\n\t}, [&](const MTPDmessageMediaToDo &media) -> Result {\n\t\treturn std::make_unique<Data::MediaTodoList>(\n\t\t\titem,\n\t\t\titem->history()->owner().processTodoList(item->fullId(), media));\n\t}, [&](const MTPDmessageMediaDice &media) -> Result {\n\t\tauto outcome = Data::DiceGameOutcome();\n\t\tif (const auto game = media.vgame_outcome()) {\n\t\t\tconst auto &data = game->data();\n\t\t\toutcome.seed = data.vseed().v;\n\t\t\toutcome.nanoTon = data.vton_amount().v;\n\t\t\toutcome.stakeNanoTon = data.vstake_ton_amount().v;\n\t\t}\n\t\treturn std::make_unique<Data::MediaDice>(\n\t\t\titem,\n\t\t\toutcome,\n\t\t\tqs(media.vemoticon()),\n\t\t\tmedia.vvalue().v);\n\t}, [&](const MTPDmessageMediaStory &media) -> Result {\n\t\treturn std::make_unique<Data::MediaStory>(item, FullStoryId{\n\t\t\tpeerFromMTP(media.vpeer()),\n\t\t\tmedia.vid().v,\n\t\t}, media.is_via_mention());\n\t}, [&](const MTPDmessageMediaGiveaway &media) -> Result {\n\t\treturn std::make_unique<Data::MediaGiveawayStart>(\n\t\t\titem,\n\t\t\tData::ComputeGiveawayStartData(item, media));\n\t}, [&](const MTPDmessageMediaGiveawayResults &media) -> Result {\n\t\treturn std::make_unique<Data::MediaGiveawayResults>(\n\t\t\titem,\n\t\t\tData::ComputeGiveawayResultsData(item, media));\n\t}, [&](const MTPDmessageMediaPaidMedia &media) -> Result {\n\t\treturn std::make_unique<Data::MediaInvoice>(\n\t\t\titem,\n\t\t\tData::ComputeInvoiceData(item, media));\n\t}, [](const MTPDmessageMediaVideoStream &) -> Result {\n\t\t// Live stories.\n\t\treturn nullptr;\n\t}, [](const MTPDmessageMediaEmpty &) -> Result {\n\t\treturn nullptr;\n\t}, [](const MTPDmessageMediaUnsupported &) -> Result {\n\t\treturn nullptr;\n\t});\n}\n\nHistoryItem::HistoryItem(\n\tnot_null<History*> history,\n\tMsgId id,\n\tconst MTPDmessage &data,\n\tMessageFlags localFlags)\n: HistoryItem(history, {\n\t.id = id,\n\t.flags = FlagsFromMTP(id, data.vflags().v, localFlags),\n\t.from = data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0),\n\t.date = data.vdate().v,\n\t.scheduleRepeatPeriod = data.vschedule_repeat_period().value_or_empty(),\n\t.shortcutId = data.vquick_reply_shortcut_id().value_or_empty(),\n\t.starsPaid = int(data.vpaid_message_stars().value_or_empty()),\n\t.effectId = data.veffect().value_or_empty(),\n}) {\n\t_boostsApplied = data.vfrom_boosts_applied().value_or_empty();\n\n\t// Called only for server-received messages, not locally created ones.\n\tapplyInitialEffectWatched();\n\n\tconst auto media = data.vmedia();\n\tconst auto checked = media\n\t\t? CheckMessageMedia(*media)\n\t\t: MediaCheckResult::Good;\n\tif (checked == MediaCheckResult::Unsupported) {\n\t\t_flags &= ~MessageFlag::HasPostAuthor;\n\t\t_flags |= MessageFlag::Legacy;\n\t\tcreateComponents(data);\n\t\tsetText(UnsupportedMessageText());\n\t\tif (!Has<HistoryMessageReplyMarkup>()) {\n\t\t\tAddComponents(HistoryMessageReplyMarkup::Bit());\n\t\t}\n\t\t_flags |= MessageFlag::HasReplyMarkup;\n\t\tGet<HistoryMessageReplyMarkup>()->updateData(\n\t\t\tUnsupportedMessageMarkup());\n\t} else if (checked == MediaCheckResult::Empty) {\n\t\tAddComponents(HistoryServiceData::Bit());\n\t\tsetServiceText({\n\t\t\ttr::lng_message_empty(tr::now, tr::marked)\n\t\t});\n\t} else if ((checked == MediaCheckResult::HasUnsupportedTimeToLive)\n\t\t\t|| (checked == MediaCheckResult::HasExpiredMediaTimeToLive)) {\n\t\tcreateServiceFromMtp(data);\n\t\tsetReactions(data.vreactions());\n\t\tapplyTTL(data);\n\t} else if (checked == MediaCheckResult::HasStoryMention) {\n\t\tsetMedia(*data.vmedia());\n\t\tcreateServiceFromMtp(data);\n\t\tsetReactions(data.vreactions());\n\t\tapplyTTL(data);\n\t} else {\n\t\tcreateComponents(data);\n\t\tif (const auto media = data.vmedia()) {\n\t\t\tsetMedia(*media);\n\t\t}\n\t\tauto textWithEntities = TextWithEntities{\n\t\t\tqs(data.vmessage()),\n\t\t\tApi::EntitiesFromMTP(\n\t\t\t\t&history->session(),\n\t\t\t\tdata.ventities().value_or_empty())\n\t\t};\n\t\tsetText(_media ? textWithEntities : EnsureNonEmpty(textWithEntities));\n\t\tif (const auto groupedId = data.vgrouped_id()) {\n\t\t\tsetGroupId(\n\t\t\t\tMessageGroupId::FromRaw(\n\t\t\t\t\thistory->peer->id,\n\t\t\t\t\tgroupedId->v,\n\t\t\t\t\t_flags & MessageFlag::IsOrWasScheduled));\n\t\t}\n\t\tsetReactions(data.vreactions());\n\t\tapplyTTL(data);\n\n\t\tif (const auto check = FromMTP(this, data.vfactcheck())) {\n\t\t\tAddComponents(HistoryMessageFactcheck::Bit());\n\t\t\tGet<HistoryMessageFactcheck>()->data = check;\n\t\t}\n\t}\n\n\tif (const auto rank = data.vfrom_rank()) {\n\t\tif (!rank->v.isEmpty()) {\n\t\t\tAddComponents(HistoryMessageFromRank::Bit());\n\t\t\tGet<HistoryMessageFromRank>()->rank = qs(*rank);\n\t\t}\n\t}\n\n\tif (const auto until = data.vreport_delivery_until_date()) {\n\t\tif (base::unixtime::now() < TimeId(until->v)) {\n\t\t\thistory->owner().histories().reportDelivery(this);\n\t\t}\n\t}\n\tif (!out()\n\t\t&& (history->peer->isNotificationsUser()\n\t\t\t|| history->peer->isVerifyCodes())\n\t\t&& (base::unixtime::now() - date() < 60 * 1)\n\t\t&& (Core::App().settings().fork().copyLoginCode())) {\n\t\tconst auto text = SpoilerLoginCode(_text);\n\t\tfor (const auto &entity : text.entities) {\n\t\t\tif (entity.type() == EntityType::Spoiler) {\n\t\t\t\tTextUtilities::SetClipboardText({\n\t\t\t\t\ttext.text.mid(\n\t\t\t\t\t\tentity.offset(),\n\t\t\t\t\t\tentity.length()),\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\nHistoryItem::HistoryItem(\n\tnot_null<History*> history,\n\tMsgId id,\n\tconst MTPDmessageService &data,\n\tMessageFlags localFlags)\n: HistoryItem(history, {\n\t.id = id,\n\t.flags = FlagsFromMTP(id, data.vflags().v, localFlags),\n\t.from = data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0),\n\t.date = data.vdate().v,\n}) {\n\tdata.vaction().match([&](const MTPDmessageActionPhoneCall &data) {\n\t\tcreateComponents(CreateConfig());\n\t\t_media = std::make_unique<Data::MediaCall>(\n\t\t\tthis,\n\t\t\tData::ComputeCallData(&history->owner(), data));\n\t\tsetTextValue({});\n\t}, [&](const MTPDmessageActionConferenceCall &data) {\n\t\tcreateComponents(CreateConfig());\n\t\t_media = std::make_unique<Data::MediaCall>(\n\t\t\tthis,\n\t\t\tData::ComputeCallData(&history->owner(), data));\n\t\tsetTextValue({});\n\t}, [&](const auto &) {\n\t\tcreateServiceFromMtp(data);\n\t});\n\tsetReactions(data.vreactions());\n\tapplyTTL(data);\n}\n\nHistoryItem::HistoryItem(\n\tnot_null<History*> history,\n\tMsgId id,\n\tconst MTPDmessageEmpty &data,\n\tMessageFlags localFlags)\n: HistoryItem(\n\thistory,\n\t{ .id = id, .flags = localFlags },\n\tPreparedServiceText{ tr::lng_message_empty(\n\t\ttr::now,\n\t\ttr::marked) }) {\n}\n\nHistoryItem::HistoryItem(\n\tnot_null<History*> history,\n\tHistoryItemCommonFields &&fields,\n\tPreparedServiceText &&message,\n\tPhotoData *photo)\n: HistoryItem(history, fields) {\n\tsetServiceText(std::move(message));\n\tif (photo) {\n\t\t_media = std::make_unique<Data::MediaPhoto>(\n\t\t\tthis,\n\t\t\thistory->peer,\n\t\t\tphoto);\n\t}\n}\n\nHistoryItem::HistoryItem(\n\tnot_null<History*> history,\n\tHistoryItemCommonFields &&fields,\n\tnot_null<HistoryItem*> original)\n: HistoryItem(history, ForwardedFields(fields, history, original)) {\n\tconst auto peer = history->peer;\n\n\tauto config = CreateConfig();\n\n\tconst auto originalMedia = original->media();\n\tconst auto dropForwardInfo = fields.ignoreForwardFrom\n\t\t|| original->computeDropForwardedInfo();\n\tconst auto topicRootId = fields.replyTo.topicRootId;\n\tconfig.reply.messageId = config.reply.topMessageId = topicRootId;\n\tconfig.reply.topicPost = (topicRootId != 0) ? 1 : 0;\n\tconfig.reply.monoforumPeerId = fields.replyTo.monoforumPeerId;\n\tif (const auto originalReply = original->Get<HistoryMessageReply>()) {\n\t\tif (originalReply->external()) {\n\t\t\tconfig.reply = originalReply->fields().clone(this);\n\t\t\tif (!config.reply.externalPeerId) {\n\t\t\t\tconfig.reply.messageId = 0;\n\t\t\t}\n\t\t}\n\t}\n\tconfig.suggest = fields.suggest;\n\tif (!dropForwardInfo) {\n\t\tconfig.originalDate = original->originalDate();\n\t\tif (const auto info = original->originalHiddenSenderInfo()) {\n\t\t\tconfig.originalSenderName = info->name;\n\t\t} else if (const auto originalSender = original->originalSender()) {\n\t\t\tconfig.originalSenderId = originalSender->id;\n\t\t\tif (originalSender->isChannel()) {\n\t\t\t\tconfig.originalId = original->originalId();\n\t\t\t}\n\t\t} else {\n\t\t\tUnexpected(\"Corrupt forwarded information in message.\");\n\t\t}\n\t\tconfig.originalPostAuthor = original->originalPostAuthor();\n\t}\n\tif (peer->isSelf()) {\n\t\t//\n\t\t// iOS app sends you to the original post if we forward a forward from channel.\n\t\t// But server returns not the original post but the forward in saved_from_...\n\t\t//\n\t\t//if (config.originalId) {\n\t\t//\tconfig.savedFromPeer = config.senderOriginal;\n\t\t//\tconfig.savedFromMsgId = config.originalId;\n\t\t//} else {\n\t\t\tconfig.savedFromPeer = original->history()->peer->id;\n\t\t\tconfig.savedFromMsgId = original->id;\n\t\t//}\n\n\t\tconfig.savedFromOutgoing = original->out();\n\t\tconfig.savedFromSenderId = original->Get<HistoryMessageForwarded>()\n\t\t\t? original->author()->id\n\t\t\t: PeerId();\n\t}\n\tif (_flags & MessageFlag::HasPostAuthor) {\n\t\tconfig.postAuthor = fields.postAuthor;\n\t}\n\tif (const auto fwdViaBot = original->viaBot()) {\n\t\tconfig.viaBotId = peerToUser(fwdViaBot->id);\n\t} else if (originalMedia && originalMedia->game()) {\n\t\tif (const auto sender = original->originalSender()) {\n\t\t\tif (const auto user = sender->asUser()) {\n\t\t\t\tif (user->isBot()) {\n\t\t\t\t\tconfig.viaBotId = peerToUser(user->id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tconst auto fwdViewsCount = original->viewsCount();\n\tif (fwdViewsCount > 0) {\n\t\tconfig.viewsCount = fwdViewsCount;\n\t} else if ((isPost() && !isScheduled())\n\t\t|| (original->originalSender()\n\t\t\t&& original->originalSender()->isChannel())) {\n\t\tconfig.viewsCount = 1;\n\t}\n\n\tconst auto mediaOriginal = original->media();\n\tif (CopyMarkupToForward(original)) {\n\t\tconfig.inlineMarkup = original->inlineReplyMarkup();\n\t}\n\tcreateComponents(std::move(config));\n\n\tconst auto ignoreMedia = [&] {\n\t\tif (mediaOriginal && mediaOriginal->webpage()) {\n\t\t\tif (peer->amRestricted(ChatRestriction::EmbedLinks)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\tif (mediaOriginal && !ignoreMedia()) {\n\t\t_media = mediaOriginal->clone(this);\n\t\tif (original->invertMedia()) {\n\t\t\t_flags |= MessageFlag::InvertMedia;\n\t\t}\n\t}\n\n\tconst auto dropText = fields.ignoreForwardCaptions\n\t\t&& _media\n\t\t&& (_media->allowsEditCaption() || _media->poll());\n\tconst auto forwardText = [&] {\n\t\tif (dropText) {\n\t\t\treturn TextWithEntities();\n\t\t}\n\t\tconst auto &text = original->originalText();\n\t\tif (!text.text.isEmpty()) {\n\t\t\treturn dropForwardInfo\n\t\t\t\t? DropDisallowedCustomEmoji(history->peer, text)\n\t\t\t\t: TextWithEntities(text);\n\t\t}\n\t\tif (const auto media = original->media()) {\n\t\t\treturn media->consumedMessageText();\n\t\t}\n\t\treturn TextWithEntities();\n\t}();\n\tsetText(forwardText);\n\n\tif (fields.groupedId) {\n\t\tsetGroupId(MessageGroupId::FromRaw(\n\t\t\thistory->peer->id,\n\t\t\tfields.groupedId,\n\t\t\t_flags & MessageFlag::IsOrWasScheduled));\n\t}\n}\n\nHistoryItem::HistoryItem(\n\tnot_null<History*> history,\n\tHistoryItemCommonFields &&fields,\n\tconst TextWithEntities &textWithEntities,\n\tconst MTPMessageMedia &media)\n: HistoryItem(history, fields) {\n\tcreateComponentsHelper(std::move(fields));\n\tsetMedia(media);\n\tsetText(textWithEntities);\n\tif (fields.groupedId) {\n\t\tsetGroupId(MessageGroupId::FromRaw(\n\t\t\thistory->peer->id,\n\t\t\tfields.groupedId,\n\t\t\t_flags & MessageFlag::IsOrWasScheduled));\n\t}\n}\n\nHistoryItem::HistoryItem(\n\tnot_null<History*> history,\n\tHistoryItemCommonFields &&fields,\n\tnot_null<DocumentData*> document,\n\tconst TextWithEntities &caption)\n: HistoryItem(history, fields) {\n\tcreateComponentsHelper(std::move(fields));\n\n\tconst auto video = document->video();\n\tusing Args = Data::MediaFile::Args;\n\t_media = std::make_unique<Data::MediaFile>(this, document, Args{\n\t\t.hasQualitiesList = video && !video->qualities.empty(),\n\t\t.skipPremiumEffect = !history->session().premium(),\n\t\t.spoiler = fields.mediaSpoiler,\n\t});\n\tsetText(caption);\n}\n\nHistoryItem::HistoryItem(\n\tnot_null<History*> history,\n\tHistoryItemCommonFields &&fields,\n\tnot_null<PhotoData*> photo,\n\tconst TextWithEntities &caption)\n: HistoryItem(history, fields) {\n\tcreateComponentsHelper(std::move(fields));\n\n\tconst auto spoiler = fields.mediaSpoiler;\n\t_media = std::make_unique<Data::MediaPhoto>(this, photo, spoiler);\n\tsetText(caption);\n}\n\nHistoryItem::HistoryItem(\n\tnot_null<History*> history,\n\tHistoryItemCommonFields &&fields,\n\tnot_null<GameData*> game)\n: HistoryItem(history, fields) {\n\tcreateComponentsHelper(std::move(fields));\n\n\t_media = std::make_unique<Data::MediaGame>(this, game);\n\tsetTextValue({});\n}\n\nHistoryItem::HistoryItem(\n\tnot_null<History*> history,\n\tMsgId id,\n\tData::SponsoredFrom from,\n\tconst TextWithEntities &textWithEntities,\n\tHistoryItem *injectedAfter)\n: HistoryItem(history, {\n\t.id = id,\n\t.flags = (MessageFlag::Local\n\t\t| MessageFlag::Sponsored\n\t\t| (history->peer->isChannel() ? MessageFlag::Post : MessageFlag(0))),\n\t.date = NewMessageDate(injectedAfter ? injectedAfter->date() : 0),\n}) {\n\tconst auto webpage = history->peer->owner().webpage(\n\t\thistory->peer->owner().nextLocalMessageId().bare,\n\t\tWebPageType::None,\n\t\tfrom.link,\n\t\tfrom.link,\n\t\tfrom.isRecommended\n\t\t\t? tr::lng_recommended_message_title(tr::now)\n\t\t\t: tr::lng_sponsored_message_title(tr::now),\n\t\tfrom.title,\n\t\ttextWithEntities,\n\t\t(from.photoId\n\t\t\t? history->owner().photo(from.photoId).get()\n\t\t\t: nullptr),\n\t\tnullptr,\n\t\tWebPageCollage(),\n\t\tnullptr,\n\t\tnullptr,\n\t\tnullptr,\n\t\t0,\n\t\tQString(),\n\t\tfalse,\n\t\tfalse,\n\t\t0);\n\tauto webpageMedia = std::make_unique<Data::MediaWebPage>(\n\t\tthis,\n\t\twebpage,\n\t\tMediaWebPageFlag::Sponsored);\n\t_media = std::move(webpageMedia);\n}\n\nHistoryItem::HistoryItem(\n\tnot_null<History*> history,\n\tconst HistoryItemCommonFields &fields)\n: id(fields.id)\n, _history(history)\n, _from((fields.flags & MessageFlag::HasFromId && fields.from)\n\t? history->owner().peer(fields.from)\n\t: history->peer)\n, _flags(FinalizeMessageFlags(history, fields.flags))\n, _date(fields.date)\n, _starsPaid(fields.starsPaid)\n, _shortcutId(fields.shortcutId)\n, _effectId(fields.effectId) {\n\tExpects(!_shortcutId\n\t\t|| isSending()\n\t\t|| _history->owner().shortcutMessages().lookupId(this));\n\n\tif (isHistoryEntry() && IsClientMsgId(id)) {\n\t\t_history->registerClientSideMessage(this);\n\t}\n\tif (_effectId) {\n\t\t_history->owner().reactions().preloadEffectImageFor(_effectId);\n\t}\n}\n\nHistoryItem::HistoryItem(\n\tnot_null<History*> history,\n\tMsgId id,\n\tnot_null<Data::Story*> story)\n: HistoryItem(history, {\n\t.id = id,\n\t.flags = (MessageFlag::Local\n\t\t| MessageFlag::Outgoing\n\t\t| MessageFlag::HasFromId\n\t\t| MessageFlag::FakeHistoryItem\n\t\t| MessageFlag::StoryItem),\n\t.from = history->peer->id,\n\t.date = story->date(),\n}) {\n\tsetStoryFields(story);\n}\n\nHistoryItem::~HistoryItem() {\n\t_media = nullptr;\n\tclearSavedMedia();\n\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\treply->clearData(this);\n\t}\n\tclearDependencyMessage();\n\tapplyTTL(0);\n}\n\nTimeId HistoryItem::date() const {\n\treturn _date;\n}\n\nint HistoryItem::starsPaid() const {\n\treturn _starsPaid;\n}\n\nbool HistoryItem::awaitingVideoProcessing() const {\n\treturn (_flags & MessageFlag::EstimatedDate);\n}\n\nHistoryServiceDependentData *HistoryItem::GetServiceDependentData() {\n\tif (const auto pinned = Get<HistoryServicePinned>()) {\n\t\treturn pinned;\n\t} else if (const auto clear = Get<HistoryServiceClearHistory>()) {\n\t\treturn clear;\n\t} else if (const auto gamescore = Get<HistoryServiceGameScore>()) {\n\t\treturn gamescore;\n\t} else if (const auto payment = Get<HistoryServicePayment>()) {\n\t\treturn payment;\n\t} else if (const auto info = Get<HistoryServiceTopicInfo>()) {\n\t\treturn info;\n\t} else if (const auto same = Get<HistoryServiceSameBackground>()) {\n\t\treturn same;\n\t} else if (const auto results = Get<HistoryServiceGiveawayResults>()) {\n\t\treturn results;\n\t} else if (const auto done = Get<HistoryServiceTodoCompletions>()) {\n\t\treturn done;\n\t} else if (const auto append = Get<HistoryServiceTodoAppendTasks>()) {\n\t\treturn append;\n\t} else if (const auto poll = Get<HistoryServicePollAppendAnswer>()) {\n\t\treturn poll;\n\t} else if (const auto decision = Get<HistoryServiceSuggestDecision>()) {\n\t\treturn decision;\n\t} else if (const auto finish = Get<HistoryServiceSuggestFinish>()) {\n\t\treturn finish;\n\t}\n\treturn nullptr;\n}\n\nauto HistoryItem::GetServiceDependentData() const\n-> const HistoryServiceDependentData * {\n\treturn const_cast<HistoryItem*>(this)->GetServiceDependentData();\n}\n\nvoid HistoryItem::dependencyItemRemoved(not_null<HistoryItem*> dependency) {\n\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\tconst auto documentId = reply->replyToDocumentId;\n\t\treply->itemRemoved(this, dependency);\n\t\tif (documentId != reply->replyToDocumentId\n\t\t\t&& generateLocalEntitiesByReply()) {\n\t\t\t_history->owner().requestItemTextRefresh(this);\n\t\t}\n\t} else {\n\t\tclearDependencyMessage();\n\t\tupdateDependentServiceText();\n\t}\n}\n\nvoid HistoryItem::dependencyStoryRemoved(\n\t\tnot_null<Data::Story*> dependency) {\n\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\tconst auto documentId = reply->replyToDocumentId;\n\t\treply->storyRemoved(this, dependency);\n\t\tif (documentId != reply->replyToDocumentId\n\t\t\t&& generateLocalEntitiesByReply()) {\n\t\t\t_history->owner().requestItemTextRefresh(this);\n\t\t}\n\t}\n}\n\nvoid HistoryItem::updateDependencyItem() {\n\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\tconst auto documentId = reply->replyToDocumentId;\n\t\tconst auto webpageId = reply->replyToWebPageId;\n\t\treply->updateData(this, true);\n\t\tconst auto mediaIdChanged = (documentId != reply->replyToDocumentId)\n\t\t\t|| (webpageId != reply->replyToWebPageId);\n\t\tif (mediaIdChanged && generateLocalEntitiesByReply()) {\n\t\t\t_history->owner().requestItemTextRefresh(this);\n\t\t}\n\t} else if (GetServiceDependentData()) {\n\t\tupdateServiceDependent(true);\n\t}\n}\n\nvoid HistoryItem::updateDependentServiceText() {\n\tif (Has<HistoryServicePinned>()) {\n\t\tupdateServiceText(preparePinnedText());\n\t} else if (Has<HistoryServiceGameScore>()) {\n\t\tupdateServiceText(prepareGameScoreText());\n\t} else if (Has<HistoryServicePayment>()) {\n\t\tupdateServiceText(preparePaymentSentText());\n\t} else if (Has<HistoryServiceTodoCompletions>()) {\n\t\tupdateServiceText(prepareTodoCompletionsText());\n\t} else if (Has<HistoryServiceTodoAppendTasks>()) {\n\t\tupdateServiceText(prepareTodoAppendTasksText());\n\t} else if (Has<HistoryServicePollAppendAnswer>()) {\n\t\tupdateServiceText(preparePollAppendAnswerText());\n\t}\n}\n\nvoid HistoryItem::updateServiceDependent(bool force) {\n\tauto dependent = GetServiceDependentData();\n\tAssert(dependent != nullptr);\n\n\tif (!force) {\n\t\tif (!dependent->msgId || dependent->msg) {\n\t\t\tdependent->pendingResolve = false;\n\t\t\treturn;\n\t\t}\n\t}\n\n\tif (!dependent->lnk) {\n\t\tauto todoItemId = 0;\n\t\tauto pollOption = QByteArray();\n\t\tif (const auto done = Get<HistoryServiceTodoCompletions>()) {\n\t\t\tconst auto &items = !done->completed.empty()\n\t\t\t\t? done->completed\n\t\t\t\t: done->incompleted;\n\t\t\tif (items.size() == 1) {\n\t\t\t\ttodoItemId = items.front();\n\t\t\t}\n\t\t} else if (const auto append = Get<HistoryServiceTodoAppendTasks>()) {\n\t\t\tif (append->list.size() == 1) {\n\t\t\t\ttodoItemId = append->list.front().id;\n\t\t\t}\n\t\t} else if (const auto pa = Get<HistoryServicePollAppendAnswer>()) {\n\t\t\tpollOption = pa->answer.option;\n\t\t} else if (const auto pd = Get<HistoryServicePollDeleteAnswer>()) {\n\t\t\tpollOption = pd->answer.option;\n\t\t}\n\t\tdependent->lnk = JumpToMessageClickHandler(\n\t\t\t(dependent->peerId\n\t\t\t\t? _history->owner().peer(dependent->peerId)\n\t\t\t\t: _history->peer),\n\t\t\tdependent->msgId,\n\t\t\tfullId(),\n\t\t\t{ .todoItemId = todoItemId, .pollOption = pollOption });\n\t}\n\tauto gotDependencyItem = false;\n\tif (!dependent->msg) {\n\t\tdependent->msg = _history->owner().message(\n\t\t\t(dependent->peerId\n\t\t\t\t? dependent->peerId\n\t\t\t\t: _history->peer->id),\n\t\t\tdependent->msgId);\n\t\tif (dependent->msg) {\n\t\t\tif (dependent->msg->isEmpty()) {\n\t\t\t\t// Really it is deleted.\n\t\t\t\tdependent->msg = nullptr;\n\t\t\t\tforce = true;\n\t\t\t} else {\n\t\t\t\t_history->owner().registerDependentMessage(\n\t\t\t\t\tthis,\n\t\t\t\t\tdependent->msg);\n\t\t\t\tgotDependencyItem = true;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Record resolve state for upcoming on-demand resolving.\n\tif (dependent->msg || !dependent->msgId || force) {\n\t\tdependent->pendingResolve = false;\n\t} else {\n\t\tdependent->pendingResolve = true;\n\t\tdependent->requestedResolve = false;\n\t}\n\n\t// updateDependentServiceText may call UpdateComponents!\n\t// So the `dependent` pointer becomes invalid.\n\tif (dependent->msg) {\n\t\tupdateDependentServiceText();\n\t} else if (force) {\n\t\tif (dependent->msgId > 0) {\n\t\t\tdependent->msgId = 0;\n\t\t\tgotDependencyItem = true;\n\t\t}\n\t\tupdateDependentServiceText();\n\t}\n\tif (force && gotDependencyItem) {\n\t\tCore::App().notifications().checkDelayed();\n\t}\n}\n\nMsgId HistoryItem::dependencyMsgId() const {\n\tif (auto dependent = GetServiceDependentData()) {\n\t\treturn dependent->msgId;\n\t}\n\treturn replyToId();\n}\n\nvoid HistoryItem::checkBuyButton() {\n\tif (const auto invoice = _media ? _media->invoice() : nullptr) {\n\t\tif (invoice->receiptMsgId) {\n\t\t\treplaceBuyWithReceiptInMarkup();\n\t\t}\n\t}\n}\n\nvoid HistoryItem::resolveDependent(\n\t\tnot_null<HistoryServiceDependentData*> dependent) {\n\tif (!dependent->pendingResolve || dependent->requestedResolve) {\n\t\treturn;\n\t}\n\tdependent->requestedResolve = true;\n\tRequestDependentMessageItem(\n\t\tthis,\n\t\t(dependent->peerId ? dependent->peerId : _history->peer->id),\n\t\tdependent->msgId);\n}\n\nvoid HistoryItem::resolveDependent(not_null<HistoryMessageReply*> reply) {\n\tif (!reply->acquireResolve()) {\n\t\treturn;\n\t} else if (const auto messageId = reply->messageId()) {\n\t\tif (Data::IsScheduledMsgId(messageId)) {\n\t\t\treply->updateData(this);\n\t\t\tif (!reply->acquireResolve()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tRequestDependentMessageItem(\n\t\t\tthis,\n\t\t\treply->externalPeerId(),\n\t\t\tmessageId);\n\t} else if (reply->storyId()) {\n\t\tRequestDependentMessageStory(\n\t\t\tthis,\n\t\t\treply->externalPeerId(),\n\t\t\treply->storyId());\n\t}\n}\n\nvoid HistoryItem::resolveDependent() {\n\tif (const auto dependent = GetServiceDependentData()) {\n\t\tresolveDependent(dependent);\n\t} else if (const auto reply = Get<HistoryMessageReply>()) {\n\t\tresolveDependent(reply);\n\t}\n}\n\nbool HistoryItem::notificationReady() const {\n\tif (const auto dependent = GetServiceDependentData()) {\n\t\tif (dependent->msg || !dependent->msgId) {\n\t\t\treturn true;\n\t\t}\n\t\tconst_cast<HistoryItem*>(this)->resolveDependent(\n\t\t\tconst_cast<HistoryServiceDependentData*>(dependent));\n\t}\n\treturn true;\n}\n\nvoid HistoryItem::finishEdition(int oldKeyboardTop) {\n\tif (const auto group = _history->owner().groups().find(this)) {\n\t\tfor (const auto &item : group->items) {\n\t\t\t_history->owner().requestItemViewRefresh(item);\n\t\t\titem->invalidateChatListEntry();\n\t\t}\n\t} else {\n\t\t_history->owner().requestItemViewRefresh(this);\n\t\tinvalidateChatListEntry();\n\t}\n\n\t// Should be completely redesigned as the oldTop no longer exists.\n\t//if (oldKeyboardTop >= 0) { // #TODO edit bot message\n\t//\tif (auto keyboard = Get<HistoryMessageReplyMarkup>()) {\n\t//\t\tkeyboard->oldTop = oldKeyboardTop;\n\t//\t}\n\t//}\n\n\t_history->owner().updateDependentMessages(this);\n}\n\nvoid HistoryItem::setGroupId(MessageGroupId groupId) {\n\tExpects(!_groupId);\n\n\t_groupId = groupId;\n\t_history->owner().groups().registerMessage(this);\n}\n\nbool HistoryItem::checkDiscussionLink(ChannelId id) const {\n\tif (!id) {\n\t\treturn true;\n\t} else if (const auto channel = _history->peer->asChannel()) {\n\t\tif (channel->discussionLinkKnown()\n\t\t\t|| !(channel->flags() & ChannelDataFlag::HasLink)) {\n\t\t\tconst auto link = channel->discussionLink();\n\t\t\tif (!link || peerToChannel(link->id) != id) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nSuggestionActions HistoryItem::computeSuggestionActions() const {\n\treturn computeSuggestionActions(Get<HistoryMessageSuggestion>());\n}\n\nSuggestionActions HistoryItem::computeSuggestionActions(\n\t\tconst HistoryMessageSuggestion *suggest) const {\n\treturn suggest\n\t\t? computeSuggestionActions(\n\t\t\tsuggest->accepted,\n\t\t\tsuggest->rejected,\n\t\t\tsuggest->gift ? suggest->date: 0)\n\t\t: SuggestionActions::None;\n}\n\nSuggestionActions HistoryItem::computeSuggestionActions(\n\t\tbool accepted,\n\t\tbool rejected,\n\t\tTimeId giftOfferExpiresAt) const {\n\tif (giftOfferExpiresAt) {\n\t\tconst auto can = isRegular()\n\t\t\t&& !(accepted || rejected)\n\t\t\t&& !out()\n\t\t\t&& (giftOfferExpiresAt > base::unixtime::now());\n\t\treturn can\n\t\t\t? SuggestionActions::GiftOfferActions\n\t\t\t: SuggestionActions::None;\n\t}\n\tconst auto channelIsAuthor = from()->isChannel();\n\tconst auto amMonoforumAdmin = history()->peer->amMonoforumAdmin();\n\tconst auto broadcast = history()->peer->monoforumBroadcast();\n\tconst auto canDecline = isRegular()\n\t\t&& !(accepted || rejected)\n\t\t&& (channelIsAuthor ? !amMonoforumAdmin : amMonoforumAdmin);\n\tconst auto canAccept = canDecline\n\t\t&& (channelIsAuthor\n\t\t\t? !amMonoforumAdmin\n\t\t\t: (amMonoforumAdmin\n\t\t\t\t&& broadcast\n\t\t\t\t&& broadcast->canPostMessages()));\n\treturn canAccept\n\t\t? SuggestionActions::AcceptAndDecline\n\t\t: canDecline\n\t\t? SuggestionActions::Decline\n\t\t: SuggestionActions::None;\n}\n\nvoid HistoryItem::updateSuggestControls(\n\t\tconst HistoryMessageSuggestion *suggest) {\n\tif (const auto markup = Get<HistoryMessageReplyMarkup>()) {\n\t\tmarkup->updateSuggestControls(computeSuggestionActions(suggest));\n\t}\n}\n\nvoid HistoryItem::setReplyMarkup(\n\t\tHistoryMessageMarkupData &&markup,\n\t\tbool ignoreSuggestButtons) {\n\tconst auto requestUpdate = [&] {\n\t\tconst auto actions = computeSuggestionActions();\n\t\tif (actions != SuggestionActions::None\n\t\t\t&& !Has<HistoryMessageReplyMarkup>()) {\n\t\t\tAddComponents(HistoryMessageReplyMarkup::Bit());\n\t\t}\n\t\tif (const auto markup = Get<HistoryMessageReplyMarkup>()) {\n\t\t\tmarkup->updateSuggestControls(actions);\n\t\t}\n\t\thistory()->owner().requestItemResize(this);\n\t\thistory()->session().changes().messageUpdated(\n\t\t\tthis,\n\t\t\tData::MessageUpdate::Flag::ReplyMarkup);\n\t};\n\tif (markup.isNull()) {\n\t\tif (_flags & MessageFlag::HasReplyMarkup) {\n\t\t\t_flags &= ~MessageFlag::HasReplyMarkup;\n\t\t\tif (Has<HistoryMessageReplyMarkup>()) {\n\t\t\t\tRemoveComponents(HistoryMessageReplyMarkup::Bit());\n\t\t\t}\n\t\t\trequestUpdate();\n\t\t}\n\t\treturn;\n\t}\n\n\t// optimization: don't create markup component for the case\n\t// MTPDreplyKeyboardHide with flags = 0, assume it has f_zero flag\n\tif (markup.isTrivial()) {\n\t\tbool changed = false;\n\t\tif (Has<HistoryMessageReplyMarkup>()) {\n\t\t\tRemoveComponents(HistoryMessageReplyMarkup::Bit());\n\t\t\tchanged = true;\n\t\t}\n\t\tif (!(_flags & MessageFlag::HasReplyMarkup)) {\n\t\t\t_flags |= MessageFlag::HasReplyMarkup;\n\t\t\tchanged = true;\n\t\t}\n\t\tif (changed) {\n\t\t\trequestUpdate();\n\t\t}\n\t} else {\n\t\tif (!(_flags & MessageFlag::HasReplyMarkup)) {\n\t\t\t_flags |= MessageFlag::HasReplyMarkup;\n\t\t}\n\t\tif (!Has<HistoryMessageReplyMarkup>()) {\n\t\t\tAddComponents(HistoryMessageReplyMarkup::Bit());\n\t\t}\n\t\tGet<HistoryMessageReplyMarkup>()->updateData(std::move(markup));\n\t\trequestUpdate();\n\t}\n}\n\nvoid HistoryItem::setCommentsInboxReadTill(MsgId readTillId) {\n\tconst auto views = Get<HistoryMessageViews>();\n\tif (!views) {\n\t\treturn;\n\t}\n\tconst auto newReadTillId = std::max(readTillId.bare, int64(1));\n\tconst auto ignore = (newReadTillId < views->commentsInboxReadTillId);\n\tif (ignore) {\n\t\treturn;\n\t}\n\tconst auto changed = (newReadTillId > views->commentsInboxReadTillId);\n\tif (!changed) {\n\t\treturn;\n\t}\n\tconst auto wasUnread = areCommentsUnread();\n\tviews->commentsInboxReadTillId = newReadTillId;\n\tif (wasUnread && !areCommentsUnread()) {\n\t\t_history->owner().requestItemRepaint(this);\n\t}\n}\n\nvoid HistoryItem::setCommentsMaxId(MsgId maxId) {\n\tif (const auto views = Get<HistoryMessageViews>()) {\n\t\tif (views->commentsMaxId != maxId) {\n\t\t\tconst auto wasUnread = areCommentsUnread();\n\t\t\tviews->commentsMaxId = maxId;\n\t\t\tif (wasUnread != areCommentsUnread()) {\n\t\t\t\t_history->owner().requestItemRepaint(this);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid HistoryItem::setCommentsPossibleMaxId(MsgId possibleMaxId) {\n\tif (const auto views = Get<HistoryMessageViews>()) {\n\t\tif (views->commentsMaxId < possibleMaxId) {\n\t\t\tconst auto wasUnread = areCommentsUnread();\n\t\t\tviews->commentsMaxId = possibleMaxId;\n\t\t\tif (!wasUnread && areCommentsUnread()) {\n\t\t\t\t_history->owner().requestItemRepaint(this);\n\t\t\t}\n\t\t}\n\t}\n}\n\nbool HistoryItem::areCommentsUnread() const {\n\tconst auto views = Get<HistoryMessageViews>();\n\tif (!views\n\t\t|| !views->commentsMegagroupId\n\t\t|| !checkDiscussionLink(views->commentsMegagroupId)) {\n\t\treturn false;\n\t}\n\tconst auto till = views->commentsInboxReadTillId;\n\tif (views->commentsInboxReadTillId < 2 || views->commentsMaxId <= till) {\n\t\treturn false;\n\t}\n\tconst auto group = views->commentsMegagroupId\n\t\t? _history->owner().historyLoaded(\n\t\t\tpeerFromChannel(views->commentsMegagroupId))\n\t\t: _history.get();\n\treturn !group || (views->commentsMaxId > group->inboxReadTillId());\n}\n\nFullMsgId HistoryItem::commentsItemId() const {\n\tif (const auto views = Get<HistoryMessageViews>()) {\n\t\treturn FullMsgId(\n\t\t\tPeerId(views->commentsMegagroupId),\n\t\t\tviews->commentsRootId);\n\t}\n\treturn FullMsgId();\n}\n\nvoid HistoryItem::setCommentsItemId(FullMsgId id) {\n\tif (id.peer == _history->peer->id) {\n\t\tif (id.msg != this->id) {\n\t\t\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\t\t\treply->setTopMessageId(id.msg);\n\t\t\t}\n\t\t}\n\t} else if (const auto views = Get<HistoryMessageViews>()) {\n\t\tif (const auto channelId = peerToChannel(id.peer)) {\n\t\t\tif (views->commentsMegagroupId != channelId) {\n\t\t\t\tviews->commentsMegagroupId = channelId;\n\t\t\t\t_history->owner().requestItemResize(this);\n\t\t\t}\n\t\t\tviews->commentsRootId = id.msg;\n\t\t}\n\t}\n}\n\nvoid HistoryItem::setServiceText(PreparedServiceText &&prepared) {\n\tAddComponents(HistoryServiceData::Bit());\n\t_flags &= ~MessageFlag::HasTextLinks;\n\tconst auto data = Get<HistoryServiceData>();\n\tconst auto had = !_text.empty();\n\t_text = std::move(prepared.text);\n\tdata->textLinks = std::move(prepared.links);\n\tif (had) {\n\t\t_history->owner().requestItemTextRefresh(this);\n\t}\n}\n\nvoid HistoryItem::updateServiceText(PreparedServiceText &&text) {\n\tsetServiceText(std::move(text));\n\t_history->owner().requestItemResize(this);\n\tinvalidateChatListEntry();\n\t_history->owner().updateDependentMessages(this);\n}\n\nvoid HistoryItem::updateStoryMentionText() {\n\tsetServiceText(prepareStoryMentionText());\n}\n\nHistoryMessageReplyMarkup *HistoryItem::inlineReplyMarkup() {\n\tif (const auto markup = Get<HistoryMessageReplyMarkup>()) {\n\t\tif (markup->data.flags & ReplyMarkupFlag::Inline) {\n\t\t\treturn markup;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nReplyKeyboard *HistoryItem::inlineReplyKeyboard() {\n\tif (const auto markup = inlineReplyMarkup()) {\n\t\treturn markup->inlineKeyboard.get();\n\t}\n\treturn nullptr;\n}\n\nChannelData *HistoryItem::discussionPostOriginalSender() const {\n\tif (!_history->peer->isMegagroup()) {\n\t\treturn nullptr;\n\t}\n\tif (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\tconst auto from = forwarded->savedFromPeer;\n\t\tif (const auto result = from ? from->asChannel() : nullptr) {\n\t\t\treturn result;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nbool HistoryItem::isDiscussionPost() const {\n\treturn (discussionPostOriginalSender() != nullptr);\n}\n\nHistoryItem *HistoryItem::lookupDiscussionPostOriginal() const {\n\tif (!_history->peer->isMegagroup()) {\n\t\treturn nullptr;\n\t}\n\tconst auto forwarded = Get<HistoryMessageForwarded>();\n\tif (!forwarded\n\t\t|| !forwarded->savedFromPeer\n\t\t|| !forwarded->savedFromMsgId) {\n\t\treturn nullptr;\n\t}\n\treturn _history->owner().message(\n\t\tforwarded->savedFromPeer->id,\n\t\tforwarded->savedFromMsgId);\n}\n\nPeerData *HistoryItem::computeDisplayFrom() const {\n\tif (const auto sender = discussionPostOriginalSender()) {\n\t\treturn sender;\n\t} else if (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\tif (showForwardsFromSender(forwarded)) {\n\t\t\treturn forwarded->forwardOfForward()\n\t\t\t\t? forwarded->savedFromSender\n\t\t\t\t: forwarded->originalSender;\n\t\t}\n\t}\n\treturn author().get();\n}\n\nPeerData *HistoryItem::displayFrom() const {\n\tif (_flags & MessageFlag::DisplayFromChecked) {\n\t\tconst auto showing = isPostShowingAuthor();\n\t\tconst auto flag = (_flags & MessageFlag::DisplayFromProfiles);\n\t\tif (showing && !flag) {\n\t\t\t_flags |= MessageFlag::DisplayFromProfiles;\n\t\t} else if (!showing && flag) {\n\t\t\t_flags &= ~MessageFlag::DisplayFromProfiles;\n\t\t} else {\n\t\t\treturn _displayFrom;\n\t\t}\n\t}\n\t_flags |= MessageFlag::DisplayFromChecked;\n\t_displayFrom = computeDisplayFrom();\n\treturn _displayFrom;\n}\n\nuint8 HistoryItem::colorIndex() const {\n\tif (const auto from = displayFrom()) {\n\t\treturn from->colorIndex();\n\t} else if (const auto info = displayHiddenSenderInfo()) {\n\t\treturn info->colorIndex;\n\t}\n\tUnexpected(\"No displayFrom and no displayHiddenSenderInfo.\");\n}\n\nDocumentId HistoryItem::backgroundEmojiId() const {\n\tif (const auto from = displayFrom()) {\n\t\treturn from->backgroundEmojiId();\n\t}\n\treturn DocumentId();\n}\n\nauto HistoryItem::colorCollectible() const\n-> const std::shared_ptr<Ui::ColorCollectible> & {\n\tif (const auto from = displayFrom()) {\n\t\treturn from->colorCollectible();\n\t}\n\tstatic const auto dummy = std::shared_ptr<Ui::ColorCollectible>();\n\treturn dummy;\n}\n\nPeerData *HistoryItem::contentColorsFrom() const {\n\tif (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\treturn forwarded->originalSender;\n\t}\n\treturn displayFrom();\n}\n\nuint8 HistoryItem::contentColorIndex() const {\n\tif (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\treturn forwarded->originalSender\n\t\t\t? forwarded->originalSender->colorIndex()\n\t\t\t: forwarded->originalHiddenSenderInfo->colorIndex;\n\t}\n\treturn colorIndex();\n}\n\nDocumentId HistoryItem::contentBackgroundEmojiId() const {\n\tif (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\treturn forwarded->originalSender\n\t\t\t? forwarded->originalSender->backgroundEmojiId()\n\t\t\t: DocumentId();\n\t}\n\treturn backgroundEmojiId();\n}\n\nauto HistoryItem::contentColorCollectible() const\n-> const std::shared_ptr<Ui::ColorCollectible> & {\n\tif (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\tif (forwarded->originalSender) {\n\t\t\treturn forwarded->originalSender->colorCollectible();\n\t\t}\n\t\tstatic const auto dummy = std::shared_ptr<Ui::ColorCollectible>();\n\t\treturn dummy;\n\t}\n\treturn colorCollectible();\n}\n\nstd::unique_ptr<HistoryView::Element> HistoryItem::createView(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate,\n\t\tHistoryView::Element *replacing) {\n\tif (isService()) {\n\t\treturn std::make_unique<HistoryView::Service>(\n\t\t\tdelegate,\n\t\t\tthis,\n\t\t\treplacing);\n\t}\n\treturn std::make_unique<HistoryView::Message>(delegate, this, replacing);\n}\n\nvoid HistoryItem::invalidateChatListEntry() {\n\t_history->session().changes().messageUpdated(\n\t\tthis,\n\t\tData::MessageUpdate::Flag::DialogRowRefresh);\n\t_history->lastItemDialogsView().itemInvalidated(this);\n\tif (const auto topic = this->topic()) {\n\t\ttopic->lastItemDialogsView().itemInvalidated(this);\n\t}\n\tif (const auto sublist = savedSublist()) {\n\t\tsublist->lastItemDialogsView().itemInvalidated(this);\n\t}\n}\n\nvoid HistoryItem::customEmojiRepaint() {\n\tif (!(_flags & MessageFlag::CustomEmojiRepainting)) {\n\t\t_flags |= MessageFlag::CustomEmojiRepainting;\n\t\t_history->owner().requestItemRepaint(this);\n\t}\n}\n\nbool HistoryItem::needsUpdateForVideoQualities(const MTPMessage &data) {\n\t// When video gets the converted alt-videos lists, we need to update\n\t// the message data even without edit-message update.\n\treturn data.match([&](const MTPDmessage &data) {\n\t\tconst auto media = data.vmedia();\n\t\tif (!media) {\n\t\t\treturn false;\n\t\t}\n\t\treturn media->match([&](const MTPDmessageMediaDocument &data) {\n\t\t\tconst auto document = data.vdocument();\n\t\t\tconst auto alts = data.valt_documents();\n\t\t\tif (!document || !alts || alts->v.isEmpty()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto id = document->match([](const auto &data) {\n\t\t\t\treturn DocumentId(data.vid().v);\n\t\t\t});\n\t\t\tconst auto existingMedia = this->media();\n\t\t\tconst auto existingDocument = existingMedia\n\t\t\t\t? existingMedia->document()\n\t\t\t\t: nullptr;\n\t\t\treturn !existingDocument\n\t\t\t\t|| (existingDocument->id != id)\n\t\t\t\t|| existingDocument->resolveQualities(this).empty();\n\t\t}, [](const auto &) {\n\t\t\treturn false;\n\t\t});\n\t}, [](const auto &) {\n\t\treturn false;\n\t});\n}\n\nvoid HistoryItem::finishEditionToEmpty() {\n\tfinishEdition(-1);\n\t_history->itemVanished(this);\n}\n\nbool HistoryItem::hasUnreadMediaFlag() const {\n\tif (_history->peer->isChannel()) {\n\t\tconst auto passed = base::unixtime::now() - date();\n\t\tconst auto &config = _history->session().serverConfig();\n\t\tif (passed >= config.channelsReadMediaPeriod) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn _flags & MessageFlag::MediaIsUnread;\n}\n\nbool HistoryItem::isUnreadMention() const {\n\treturn !out() && mentionsMe() && (_flags & MessageFlag::MediaIsUnread);\n}\n\nbool HistoryItem::hasUnreadReaction() const {\n\treturn (_flags & MessageFlag::HasUnreadReaction);\n}\n\nbool HistoryItem::hasUnreadPollVote() const {\n\treturn (_flags & MessageFlag::HasUnreadPollVote);\n}\n\nvoid HistoryItem::setHasUnreadPollVote() {\n\t_flags |= MessageFlag::HasUnreadPollVote;\n}\n\nbool HistoryItem::hasUnwatchedEffect() const {\n\treturn effectId() && !(_flags & MessageFlag::EffectWatched);\n}\n\nbool HistoryItem::markEffectWatched() {\n\tif (!hasUnwatchedEffect()) {\n\t\treturn false;\n\t}\n\t_flags |= MessageFlag::EffectWatched;\n\treturn true;\n}\n\nbool HistoryItem::mentionsMe() const {\n\tif (Has<HistoryServicePinned>()\n\t\t&& !Core::App().settings().notifyAboutPinned()) {\n\t\treturn false;\n\t}\n\treturn _flags & MessageFlag::MentionsMe;\n}\n\nbool HistoryItem::isUnreadMedia() const {\n\tif (!hasUnreadMediaFlag()) {\n\t\treturn false;\n\t} else if (const auto media = this->media()) {\n\t\tif (const auto document = media->document()) {\n\t\t\tif (document->isVoiceMessage() || document->isVideoMessage()) {\n\t\t\t\treturn (media->webpage() == nullptr);\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nbool HistoryItem::isIncomingUnreadMedia() const {\n\treturn !out() && isUnreadMedia();\n}\n\nvoid HistoryItem::markMediaAndMentionRead() {\n\tconst auto wasUnreadMedia = isUnreadMedia();\n\t_flags &= ~MessageFlag::MediaIsUnread;\n\n\tif (wasUnreadMedia) {\n\t\tinvalidateChatListEntry();\n\t}\n\n\tif (mentionsMe()) {\n\t\t_history->updateChatListEntry();\n\t\t_history->unreadMentions().erase(id);\n\t\tif (const auto topic = this->topic()) {\n\t\t\ttopic->updateChatListEntry();\n\t\t\ttopic->unreadMentions().erase(id);\n\t\t}\n\t}\n\n\tif (const auto selfdestruct = Get<HistoryServiceSelfDestruct>()) {\n\t\tif (selfdestruct->destructAt == crl::time()) {\n\t\t\tconst auto ttl = selfdestruct->timeToLive;\n\t\t\tif (const auto maybeTime = std::get_if<crl::time>(&ttl)) {\n\t\t\t\tconst auto time = *maybeTime;\n\t\t\t\tselfdestruct->destructAt = crl::now() + time;\n\t\t\t\t_history->owner().selfDestructIn(this, time);\n\t\t\t} else {\n\t\t\t\tselfdestruct->destructAt = TimeToLiveSingleView();\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid HistoryItem::markReactionsRead() {\n\tif (_reactions) {\n\t\t_reactions->markRead();\n\t}\n\t_flags &= ~MessageFlag::HasUnreadReaction;\n\t_history->updateChatListEntry();\n\t_history->unreadReactions().erase(id);\n\tif (const auto topic = this->topic()) {\n\t\ttopic->updateChatListEntry();\n\t\ttopic->unreadReactions().erase(id);\n\t} else if (const auto sublist = this->savedSublist()) {\n\t\tsublist->updateChatListEntry();\n\t\tsublist->unreadReactions().erase(id);\n\t}\n}\n\nvoid HistoryItem::markPollVotesRead() {\n\t_flags &= ~MessageFlag::HasUnreadPollVote;\n\t_history->updateChatListEntry();\n\t_history->unreadPollVotes().erase(id);\n\tif (const auto topic = this->topic()) {\n\t\ttopic->updateChatListEntry();\n\t\ttopic->unreadPollVotes().erase(id);\n\t}\n}\n\nbool HistoryItem::markContentsRead(bool fromThisClient) {\n\tauto result = false;\n\tif (hasUnreadReaction()) {\n\t\tif (fromThisClient) {\n\t\t\t_history->owner().requestUnreadReactionsAnimation(this);\n\t\t}\n\t\tmarkReactionsRead();\n\t\tresult = true;\n\t}\n\tif (isUnreadMention() || isIncomingUnreadMedia()) {\n\t\tmarkMediaAndMentionRead();\n\t\tresult = true;\n\t}\n\tif (hasUnreadPollVote()) {\n\t\tmarkPollVotesRead();\n\t\tresult = true;\n\t}\n\treturn result;\n}\n\nvoid HistoryItem::setIsPinned(bool pinned) {\n\tconst auto changed = (isPinned() != pinned);\n\tconst auto guard = gsl::finally([&] {\n\t\tif (changed) {\n\t\t\t_history->owner().notifyItemDataChange(this);\n\t\t}\n\t});\n\tif (pinned) {\n\t\t_flags |= MessageFlag::Pinned;\n\t\tif (_flags & MessageFlag::StoryItem) {\n\t\t\treturn;\n\t\t}\n\n\t\tauto &storage = _history->session().storage();\n\t\tstorage.add(Storage::SharedMediaAddExisting(\n\t\t\t_history->peer->id,\n\t\t\tMsgId(0), // topicRootId\n\t\t\tPeerId(0), // monoforumPeerId\n\t\t\tStorage::SharedMediaType::Pinned,\n\t\t\tid,\n\t\t\t{ id, id }));\n\t\t_history->setHasPinnedMessages(true);\n\t\tif (const auto topic = this->topic()) {\n\t\t\tstorage.add(Storage::SharedMediaAddExisting(\n\t\t\t\t_history->peer->id,\n\t\t\t\ttopic->rootId(),\n\t\t\t\tPeerId(), // monoforumPeerId\n\t\t\t\tStorage::SharedMediaType::Pinned,\n\t\t\t\tid,\n\t\t\t\t{ id, id }));\n\t\t\ttopic->setHasPinnedMessages(true);\n\t\t}\n\t\tif (const auto sublist = this->savedSublist()) {\n\t\t\tstorage.add(Storage::SharedMediaAddExisting(\n\t\t\t\t_history->peer->id,\n\t\t\t\tMsgId(0), // topicRootId\n\t\t\t\tsublistPeerId(),\n\t\t\t\tStorage::SharedMediaType::Pinned,\n\t\t\t\tid,\n\t\t\t\t{ id, id }));\n\t\t\tsublist->setHasPinnedMessages(true);\n\t\t}\n\t} else {\n\t\t_flags &= ~MessageFlag::Pinned;\n\t\tif (_flags & MessageFlag::StoryItem) {\n\t\t\treturn;\n\t\t}\n\n\t\t_history->session().storage().remove(Storage::SharedMediaRemoveOne(\n\t\t\t_history->peer->id,\n\t\t\tStorage::SharedMediaType::Pinned,\n\t\t\tid));\n\t}\n}\n\nvoid HistoryItem::setStoryInProfile(bool inProfile) {\n\tif (storyInProfile() == inProfile) {\n\t\treturn;\n\t} else if (inProfile) {\n\t\t_flags |= MessageFlag::StoryInProfile;\n\t} else {\n\t\t_flags &= ~MessageFlag::StoryInProfile;\n\t}\n\t_history->owner().notifyItemDataChange(this);\n}\n\nvoid HistoryItem::returnSavedMedia() {\n\tif (!isEditingMedia()) {\n\t\treturn;\n\t}\n\tconst auto wasGrouped = history()->owner().groups().isGrouped(this);\n\tconst auto data = Get<HistoryMessageSavedMediaData>();\n\t_media = std::move(data->media);\n\tsetText(data->text);\n\tclearSavedMedia();\n\tif (wasGrouped) {\n\t\thistory()->owner().groups().refreshMessage(this, true);\n\t} else {\n\t\thistory()->owner().requestItemViewRefresh(this);\n\t\thistory()->owner().updateDependentMessages(this);\n\t}\n}\n\nvoid HistoryItem::savePreviousMedia() {\n\tAddComponents(HistoryMessageSavedMediaData::Bit());\n\tconst auto data = Get<HistoryMessageSavedMediaData>();\n\tdata->text = originalText();\n\tdata->media = _media ? _media->clone(this) : nullptr;\n}\n\nbool HistoryItem::isEditingMedia() const {\n\treturn Has<HistoryMessageSavedMediaData>();\n}\n\nPaidPostType HistoryItem::paidType() const {\n\treturn (_flags & MessageFlag::StarsPaidSuggested)\n\t\t? PaidPostType::Stars\n\t\t: (_flags & MessageFlag::TonPaidSuggested)\n\t\t? PaidPostType::Ton\n\t\t: PaidPostType::None;\n}\n\nvoid HistoryItem::clearSavedMedia() {\n\tRemoveComponents(HistoryMessageSavedMediaData::Bit());\n}\n\nbool HistoryItem::definesReplyKeyboard() const {\n\tif (const auto markup = Get<HistoryMessageReplyMarkup>()) {\n\t\tif (markup->data.flags & ReplyMarkupFlag::Inline) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\t// optimization: don't create markup component for the case\n\t// MTPDreplyKeyboardHide with flags = 0, assume it has f_zero flag\n\treturn (_flags & MessageFlag::HasReplyMarkup);\n}\n\nReplyMarkupFlags HistoryItem::replyKeyboardFlags() const {\n\tExpects(definesReplyKeyboard());\n\n\tif (const auto markup = Get<HistoryMessageReplyMarkup>()) {\n\t\treturn markup->data.flags;\n\t}\n\n\t// optimization: don't create markup component for the case\n\t// MTPDreplyKeyboardHide with flags = 0, assume it has f_zero flag\n\treturn ReplyMarkupFlag::None;\n}\n\nvoid HistoryItem::addLogEntryOriginal(\n\t\tWebPageId localId,\n\t\tconst QString &label,\n\t\tconst TextWithEntities &content) {\n\tExpects(isAdminLogEntry());\n\n\tAddComponents(HistoryMessageLogEntryOriginal::Bit());\n\tGet<HistoryMessageLogEntryOriginal>()->page = _history->owner().webpage(\n\t\tlocalId,\n\t\tlabel,\n\t\tcontent);\n}\n\nvoid HistoryItem::setFactcheck(MessageFactcheck info) {\n\tif (!info) {\n\t\tif (Has<HistoryMessageFactcheck>()) {\n\t\t\tRemoveComponents(HistoryMessageFactcheck::Bit());\n\t\t\thistory()->owner().requestItemResize(this);\n\t\t}\n\t} else {\n\t\tAddComponents(HistoryMessageFactcheck::Bit());\n\t\tconst auto factcheck = Get<HistoryMessageFactcheck>();\n\t\tconst auto textChanged = (factcheck->data.text != info.text);\n\t\tif (factcheck->data.hash == info.hash\n\t\t\t&& (info.needCheck || !factcheck->data.needCheck)) {\n\t\t\treturn;\n\t\t} else if (textChanged\n\t\t\t|| factcheck->data.country != info.country\n\t\t\t|| factcheck->data.hash != info.hash) {\n\t\t\tfactcheck->data = std::move(info);\n\t\t\tfactcheck->requested = false;\n\t\t\tif (textChanged) {\n\t\t\t\tfactcheck->page = nullptr;\n\t\t\t}\n\t\t\thistory()->owner().requestItemResize(this);\n\t\t}\n\t}\n}\n\nbool HistoryItem::hasUnrequestedFactcheck() const {\n\tconst auto factcheck = Get<HistoryMessageFactcheck>();\n\treturn factcheck && factcheck->data.needCheck && !factcheck->requested;\n}\n\nTextWithEntities HistoryItem::factcheckText() const {\n\tif (const auto factcheck = Get<HistoryMessageFactcheck>()) {\n\t\treturn factcheck->data.text;\n\t}\n\treturn {};\n}\n\nconst Api::SummaryEntry &HistoryItem::summaryEntry() const {\n\tif (!(_flags & MessageFlag::HasSummaryEntry)) {\n\t\tstatic const auto empty = Api::SummaryEntry();\n\t\treturn empty;\n\t}\n\treturn history()->session().api().transcribes().summary(this);\n}\n\nvoid HistoryItem::setHasSummaryEntry() {\n\t_flags |= MessageFlag::HasSummaryEntry;\n}\n\nPeerData *HistoryItem::specialNotificationPeer() const {\n\treturn (mentionsMe() && !_history->peer->isUser())\n\t\t? from().get()\n\t\t: nullptr;\n}\n\nUserData *HistoryItem::viaBot() const {\n\tif (const auto via = Get<HistoryMessageVia>()) {\n\t\treturn via->bot;\n\t}\n\treturn nullptr;\n}\n\nUserData *HistoryItem::getMessageBot() const {\n\tif (const auto bot = viaBot()) {\n\t\treturn bot;\n\t}\n\tauto bot = from()->asUser();\n\tif (!bot) {\n\t\tbot = _history->peer->asUser();\n\t}\n\treturn (bot && bot->isBot()) ? bot : nullptr;\n}\n\nbool HistoryItem::hideLinks() const {\n\treturn !out() && history()->peer->hideLinks();\n}\n\nbool HistoryItem::isHistoryEntry() const {\n\treturn (_flags & MessageFlag::HistoryEntry);\n}\n\nbool HistoryItem::isAdminLogEntry() const {\n\treturn (_flags & MessageFlag::AdminLogEntry);\n}\n\nbool HistoryItem::isFromScheduled() const {\n\treturn isHistoryEntry()\n\t\t&& (_flags & MessageFlag::IsOrWasScheduled);\n}\n\nbool HistoryItem::isScheduled() const {\n\treturn !isHistoryEntry()\n\t\t&& !isAdminLogEntry()\n\t\t&& (_flags & MessageFlag::IsOrWasScheduled);\n}\n\nTimeId HistoryItem::scheduleRepeatPeriod() const {\n\tconst auto period = Get<HistoryMessageSchedulePeriod>();\n\treturn period ? period->schedulePeriod : TimeId();\n}\n\nbool HistoryItem::isSponsored() const {\n\treturn _flags & MessageFlag::Sponsored;\n}\n\nbool HistoryItem::canLookupMessageAuthor() const {\n\treturn isRegular()\n\t\t&& !isService()\n\t\t&& _history->amMonoforumAdmin()\n\t\t&& _from->isChannel();\n}\n\nbool HistoryItem::skipNotification() const {\n\tif (isSilent() && (_flags & MessageFlag::IsContactSignUp)) {\n\t\treturn true;\n\t} else if (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\tif (forwarded->imported) {\n\t\t\treturn true;\n\t\t}\n\t} else if (canLookupMessageAuthor()) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool HistoryItem::isUserpicSuggestion() const {\n\treturn (_flags & MessageFlag::IsUserpicSuggestion);\n}\n\nbool HistoryItem::isSavedMusicItem() const {\n\treturn (_flags & MessageFlag::SavedMusicItem);\n}\n\nBusinessShortcutId HistoryItem::shortcutId() const {\n\treturn _shortcutId;\n}\n\nbool HistoryItem::isBusinessShortcut() const {\n\treturn _shortcutId != 0;\n}\n\nvoid HistoryItem::setRealShortcutId(BusinessShortcutId id) {\n\t_shortcutId = id;\n}\n\nvoid HistoryItem::setCustomServiceLink(ClickHandlerPtr link) {\n\tAddComponents(HistoryServiceCustomLink::Bit());\n\tGet<HistoryServiceCustomLink>()->link = std::move(link);\n}\n\nvoid HistoryItem::destroy() {\n\t_history->destroyMessage(this);\n}\n\nnot_null<Data::Thread*> HistoryItem::notificationThread() const {\n\tif (const auto rootId = topicRootId()) {\n\t\tif (const auto forum = _history->asForum()) {\n\t\t\tif (!_history->peer->isBot()) {\n\t\t\t\treturn forum->enforceTopicFor(rootId);\n\t\t\t}\n\t\t}\n\t}\n\treturn _history;\n}\n\nData::Thread *HistoryItem::maybeNotificationThread() const {\n\tif (const auto rootId = topicRootId()) {\n\t\tif (const auto forum = _history->asForum()) {\n\t\t\treturn forum->topicFor(rootId);\n\t\t}\n\t}\n\treturn _history;\n}\n\nData::ForumTopic *HistoryItem::topic() const {\n\tif (const auto rootId = topicRootId()) {\n\t\tif (const auto forum = _history->asForum()) {\n\t\t\treturn forum->topicFor(rootId);\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid HistoryItem::refreshMainView() {\n\tif (const auto view = mainView()) {\n\t\t_history->owner().notifyHistoryChangeDelayed(_history);\n\t\tview->refreshInBlock();\n\t}\n}\n\nvoid HistoryItem::removeMainView() {\n\tif (const auto view = mainView()) {\n\t\t_history->owner().notifyHistoryChangeDelayed(_history);\n\t\tview->removeFromBlock();\n\t}\n}\n\nvoid HistoryItem::clearMainView() {\n\t_mainView = nullptr;\n}\n\nvoid HistoryItem::applyEdition(HistoryMessageEdition &&edition) {\n\tint keyboardTop = -1;\n\t//if (!pendingResize()) {// #TODO edit bot message\n\t//\tif (auto keyboard = inlineReplyKeyboard()) {\n\t//\t\tint h = st::msgBotKbButton.margin + keyboard->naturalHeight();\n\t//\t\tkeyboardTop = _height - h + st::msgBotKbButton.margin - marginBottom();\n\t//\t}\n\t//}\n\n\tconst auto editingMedia = isEditingMedia();\n\tconst auto updatingSavedLocalEdit = !edition.savePreviousMedia\n\t\t&& editingMedia;\n\tif (!editingMedia && edition.savePreviousMedia) {\n\t\tsavePreviousMedia();\n\t}\n\tAssert(!updatingSavedLocalEdit || !isLocalUpdateMedia());\n\n\tif (edition.isEditHide) {\n\t\t_flags |= MessageFlag::HideEdited;\n\t} else {\n\t\t_flags &= ~MessageFlag::HideEdited;\n\t}\n\tif (edition.invertMedia) {\n\t\t_flags |= MessageFlag::InvertMedia;\n\t} else {\n\t\t_flags &= ~MessageFlag::InvertMedia;\n\t}\n\n\tif (edition.editDate != -1) {\n\t\t//_flags |= MTPDmessage::Flag::f_edit_date;\n\t\tif (!Has<HistoryMessageEdited>()) {\n\t\t\tAddComponents(HistoryMessageEdited::Bit());\n\t\t}\n\t\tauto edited = Get<HistoryMessageEdited>();\n\t\tedited->date = edition.editDate;\n\t}\n\n\tif (!edition.useSameMarkup) {\n\t\tsetReplyMarkup(base::take(edition.replyMarkup));\n\t}\n\tif (!edition.useSameReactions) {\n\t\tupdateReactions(edition.mtpReactions);\n\t}\n\tif (!edition.useSameViews) {\n\t\tchangeViewsCount(edition.views);\n\t}\n\tif (!edition.useSameForwards) {\n\t\tsetForwardsCount(edition.forwards);\n\t}\n\tconst auto mediaCheck = edition.mtpMedia\n\t\t? CheckMessageMedia(*edition.mtpMedia)\n\t\t: MediaCheckResult::Good;\n\tif (updatingSavedLocalEdit) {\n\t\tGet<HistoryMessageSavedMediaData>()->media\n\t\t\t= (mediaCheck != MediaCheckResult::Unsupported\n\t\t\t\t&& edition.mtpMedia)\n\t\t\t? CreateMedia(this, *edition.mtpMedia)\n\t\t\t: nullptr;\n\t} else {\n\t\tremoveFromSharedMediaIndex();\n\t\tif (mediaCheck == MediaCheckResult::Unsupported) {\n\t\t\t_media = nullptr;\n\t\t\t_flags &= ~MessageFlag::HasPostAuthor;\n\t\t\t_flags |= MessageFlag::Legacy;\n\t\t} else {\n\t\t\trefreshMedia(edition.mtpMedia);\n\t\t\tif (_flags & MessageFlag::Legacy) {\n\t\t\t\t_flags &= ~MessageFlag::Legacy;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto &checkedMedia = updatingSavedLocalEdit\n\t\t? Get<HistoryMessageSavedMediaData>()->media\n\t\t: _media;\n\tauto updatedText = (mediaCheck == MediaCheckResult::Unsupported)\n\t\t? UnsupportedMessageText()\n\t\t: checkedMedia\n\t\t? edition.textWithEntities\n\t\t: EnsureNonEmpty(edition.textWithEntities);\n\tauto serviceText = (!checkedMedia\n\t\t&& edition.textWithEntities.empty()\n\t\t&& edition.mtpMedia)\n\t\t? prepareServiceTextForMessage(\n\t\t\t*edition.mtpMedia,\n\t\t\tedition.isMediaUnread)\n\t\t: PreparedServiceText();\n\tif (updatingSavedLocalEdit) {\n\t\tGet<HistoryMessageSavedMediaData>()->text = std::move(updatedText);\n\t} else if (!serviceText.text.empty()) {\n\t\tsetServiceText(std::move(serviceText));\n\t\taddToSharedMediaIndex();\n\t} else {\n\t\tsetText(std::move(updatedText));\n\t\taddToSharedMediaIndex();\n\t}\n\tif (mediaCheck == MediaCheckResult::Unsupported) {\n\t\tsetReplyMarkup(UnsupportedMessageMarkup());\n\t}\n\tif (!edition.useSameReplies) {\n\t\tif (!edition.replies.isNull) {\n\t\t\tif (checkRepliesPts(edition.replies)) {\n\t\t\t\tsetReplies(base::take(edition.replies));\n\t\t\t}\n\t\t} else {\n\t\t\tclearReplies();\n\t\t}\n\t}\n\n\tif (!edition.useSameSuggest) {\n\t\tif (edition.suggest.exists) {\n\t\t\tif (!Has<HistoryMessageSuggestion>()) {\n\t\t\t\tAddComponents(HistoryMessageSuggestion::Bit());\n\t\t\t}\n\t\t\tauto suggest = Get<HistoryMessageSuggestion>();\n\t\t\tsuggest->price = edition.suggest.price;\n\t\t\tsuggest->date = edition.suggest.date;\n\t\t\tsuggest->accepted = edition.suggest.accepted;\n\t\t\tsuggest->rejected = edition.suggest.rejected;\n\t\t\tupdateSuggestControls(suggest);\n\t\t} else {\n\t\t\tRemoveComponents(HistoryMessageSuggestion::Bit());\n\t\t\tupdateSuggestControls(nullptr);\n\t\t}\n\t}\n\n\tif (edition.repeatPeriod) {\n\t\tif (!Has<HistoryMessageSchedulePeriod>()) {\n\t\t\tAddComponents(HistoryMessageSchedulePeriod::Bit());\n\t\t}\n\t\tconst auto period = Get<HistoryMessageSchedulePeriod>();\n\t\tperiod->schedulePeriod = edition.repeatPeriod;\n\t} else {\n\t\tRemoveComponents(HistoryMessageSchedulePeriod::Bit());\n\t}\n\n\tapplyTTL(edition.ttl);\n\tsetFactcheck(FromMTP(this, edition.mtpFactcheck));\n\n\tif (!edition.fromRank.isEmpty()) {\n\t\tif (!Has<HistoryMessageFromRank>()) {\n\t\t\tAddComponents(HistoryMessageFromRank::Bit());\n\t\t}\n\t\tGet<HistoryMessageFromRank>()->rank = edition.fromRank;\n\t} else {\n\t\tRemoveComponents(HistoryMessageFromRank::Bit());\n\t}\n\n\tfinishEdition(keyboardTop);\n}\n\nvoid HistoryItem::applyChanges(not_null<Data::Story*> story) {\n\tExpects(_flags & MessageFlag::StoryItem);\n\tExpects(StoryIdFromMsgId(id) == story->id());\n\n\t_media = nullptr;\n\tsetStoryFields(story);\n\n\tfinishEdition(-1);\n}\n\nvoid HistoryItem::setStoryFields(not_null<Data::Story*> story) {\n\tif (const auto photo = story->photo()) {\n\t\tconst auto spoiler = false;\n\t\t_media = std::make_unique<Data::MediaPhoto>(this, photo, spoiler);\n\t} else if (const auto document = story->document()) {\n\t\tusing Args = Data::MediaFile::Args;\n\t\t_media = std::make_unique<Data::MediaFile>(this, document, Args{});\n\t}\n\tsetText(story->caption());\n\tif (story->pinnedToTop()) {\n\t\t_flags |= MessageFlag::Pinned;\n\t} else {\n\t\t_flags &= ~MessageFlag::Pinned;\n\t}\n\tif (story->inProfile()) {\n\t\t_flags |= MessageFlag::StoryInProfile;\n\t} else {\n\t\t_flags &= ~MessageFlag::StoryInProfile;\n\t}\n}\n\nvoid HistoryItem::applyEdition(const MTPDmessageService &message) {\n\tconst auto wasNfr = Get<HistoryServiceNoForwardsRequest>();\n\tconst auto wasActionTaken = wasNfr && wasNfr->actionTaken;\n\tconst auto wasSublist = savedSublist();\n\tif (message.vaction().type() == mtpc_messageActionHistoryClear) {\n\t\tconst auto wasGrouped = history()->owner().groups().isGrouped(this);\n\t\tsetReplyMarkup({}, true);\n\t\tremoveFromSharedMediaIndex();\n\t\trefreshMedia(nullptr);\n\t\tsetTextValue({});\n\t\tchangeViewsCount(-1);\n\t\tsetForwardsCount(-1);\n\t\tif (wasGrouped) {\n\t\t\thistory()->owner().groups().unregisterMessage(this);\n\t\t}\n\t\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\t\treply->clearData(this);\n\t\t}\n\t\tclearDependencyMessage();\n\t\tUpdateComponents(0);\n\t\tcreateServiceFromMtp(message);\n\t\tapplyServiceDateEdition(message);\n\t\tfinishEditionToEmpty();\n\t\t_flags &= ~MessageFlag::DisplayFromChecked;\n\t} else if (message.vaction().type() == mtpc_messageActionConferenceCall) {\n\t\tremoveFromSharedMediaIndex();\n\t\tconst auto owner = &history()->owner();\n\t\tconst auto &data = message.vaction().c_messageActionConferenceCall();\n\t\tconst auto info = Data::ComputeCallData(owner, data);\n\t\tif (const auto user = history()->peer->asUser()) {\n\t\t\tif (const auto conferenceId = out() ? info.conferenceId : 0) {\n\t\t\t\tCore::App().calls().unregisterConferenceInvite(\n\t\t\t\t\tconferenceId,\n\t\t\t\t\tuser,\n\t\t\t\t\tid,\n\t\t\t\t\t!out(),\n\t\t\t\t\ttrue);\n\t\t\t}\n\t\t}\n\t\t_media = nullptr;\n\t\t_media = std::make_unique<Data::MediaCall>(this, info);\n\t\taddToSharedMediaIndex();\n\t\tfinishEdition(-1);\n\t\t_flags &= ~MessageFlag::DisplayFromChecked;\n\n\t\tupdateReactions(message.vreactions());\n\t} else if (isService()) {\n\t\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\t\treply->clearData(this);\n\t\t}\n\t\tclearDependencyMessage();\n\t\tUpdateComponents(0);\n\t\tcreateServiceFromMtp(message);\n\t\tapplyServiceDateEdition(message);\n\t\tfinishEdition(-1);\n\t\t_flags &= ~MessageFlag::DisplayFromChecked;\n\n\t\tupdateReactions(message.vreactions());\n\t}\n\tif (const auto nowNfr = Get<HistoryServiceNoForwardsRequest>()) {\n\t\tnowNfr->actionTaken = wasActionTaken;\n\t}\n\tconst auto nowSublist = savedSublist();\n\tif (wasSublist && nowSublist != wasSublist) {\n\t\twasSublist->removeOne(this);\n\t\tnowSublist->applyMaybeLast(this);\n\t}\n}\n\nvoid HistoryItem::applyEdition(\n\t\tconst QVector<MTPMessageExtendedMedia> &media) {\n\tif (const auto existing = this->media()) {\n\t\tif (existing->updateExtendedMedia(this, media)) {\n\t\t\tcheckBuyButton();\n\t\t\tfinishEdition(-1);\n\t\t}\n\t}\n}\n\nvoid HistoryItem::applySentMessage(const MTPDmessage &data) {\n\tif (data.is_invert_media()) {\n\t\t_flags |= MessageFlag::InvertMedia;\n\t} else {\n\t\t_flags &= ~MessageFlag::InvertMedia;\n\t}\n\n\tupdateSentContent({\n\t\tqs(data.vmessage()),\n\t\tApi::EntitiesFromMTP(\n\t\t\t&_history->session(),\n\t\t\tdata.ventities().value_or_empty())\n\t}, data.vmedia());\n\tupdateReplyMarkup(HistoryMessageMarkupData(data.vreply_markup()));\n\tupdateForwardedInfo(data.vfwd_from());\n\tchangeViewsCount(data.vviews().value_or(-1));\n\tif (const auto replies = data.vreplies()) {\n\t\tsetReplies(HistoryMessageRepliesData(replies));\n\t} else {\n\t\tclearReplies();\n\t}\n\tsetForwardsCount(data.vforwards().value_or(-1));\n\tif (const auto reply = data.vreply_to()) {\n\t\treply->match([&](const MTPDmessageReplyHeader &data) {\n\t\t\tconst auto replyToPeer = data.vreply_to_peer_id()\n\t\t\t\t? peerFromMTP(*data.vreply_to_peer_id())\n\t\t\t\t: PeerId();\n\t\t\tif (!replyToPeer || replyToPeer == history()->peer->id) {\n\t\t\t\tif (const auto replyToId = data.vreply_to_msg_id()) {\n\t\t\t\t\tif (!isService() && !Has<HistoryMessageReply>()) {\n\t\t\t\t\t\tAddComponents(HistoryMessageReply::Bit());\n\t\t\t\t\t}\n\t\t\t\t\tsetReplyFields(\n\t\t\t\t\t\treplyToId->v,\n\t\t\t\t\t\tdata.vreply_to_top_id().value_or(replyToId->v),\n\t\t\t\t\t\tdata.is_forum_topic());\n\t\t\t\t}\n\t\t\t}\n\t\t}, [](const MTPDmessageReplyStoryHeader &data) {\n\t\t});\n\t}\n\tsetPostAuthor(data.vpost_author().value_or_empty());\n\tsetIsPinned(data.is_pinned());\n\tcontributeToSlowmode(data.vdate().v);\n\taddToSharedMediaIndex();\n\taddToMessagesIndex();\n\tinvalidateChatListEntry();\n\tif (const auto period = data.vttl_period(); period && period->v > 0) {\n\t\tapplyTTL(data.vdate().v + period->v);\n\t} else {\n\t\tapplyTTL(0);\n\t}\n\t_history->owner().notifyItemDataChange(this);\n\t_history->owner().requestItemTextRefresh(this);\n\t_history->owner().updateDependentMessages(this);\n}\n\nvoid HistoryItem::applySentMessage(\n\t\tconst QString &text,\n\t\tconst MTPDupdateShortSentMessage &data,\n\t\tbool wasAlready) {\n\tupdateSentContent({\n\t\ttext,\n\t\tApi::EntitiesFromMTP(\n\t\t\t&_history->session(),\n\t\t\tdata.ventities().value_or_empty())\n\t\t}, data.vmedia());\n\tcontributeToSlowmode(data.vdate().v);\n\tif (!wasAlready) {\n\t\taddToSharedMediaIndex();\n\t\taddToMessagesIndex();\n\t}\n\tinvalidateChatListEntry();\n\tif (const auto period = data.vttl_period(); period && period->v > 0) {\n\t\tapplyTTL(data.vdate().v + period->v);\n\t} else {\n\t\tapplyTTL(0);\n\t}\n}\n\nvoid HistoryItem::updateSentContent(\n\t\tconst TextWithEntities &textWithEntities,\n\t\tconst MTPMessageMedia *media) {\n\tif (isEditingMedia()) {\n\t\treturn;\n\t}\n\tconst auto mediaCheck = media\n\t\t? CheckMessageMedia(*media)\n\t\t: MediaCheckResult::Good;\n\tif (mediaCheck == MediaCheckResult::Unsupported) {\n\t\t_flags &= ~MessageFlag::HasPostAuthor;\n\t\t_flags |= MessageFlag::Legacy;\n\t\tsetText(UnsupportedMessageText());\n\t\tsetReplyMarkup(UnsupportedMessageMarkup());\n\t} else {\n\t\tif (_flags & MessageFlag::Legacy) {\n\t\t\t_flags &= ~MessageFlag::Legacy;\n\t\t}\n\t\tsetText(textWithEntities);\n\t}\n\tif (mediaCheck == MediaCheckResult::Unsupported) {\n\t\t_media = nullptr;\n\t} else if (_flags & MessageFlag::FromInlineBot) {\n\t\tif (!media || !_media || !_media->updateInlineResultMedia(*media)) {\n\t\t\trefreshSentMedia(media);\n\t\t}\n\t\t_flags &= ~MessageFlag::FromInlineBot;\n\t} else if (media || _media) {\n\t\tif (!media || !_media || !_media->updateSentMedia(*media)) {\n\t\t\trefreshSentMedia(media);\n\t\t}\n\t}\n\thistory()->owner().requestItemResize(this);\n}\n\nvoid HistoryItem::updateForwardedInfo(const MTPMessageFwdHeader *fwd) {\n\tconst auto forwarded = Get<HistoryMessageForwarded>();\n\tif (!fwd) {\n\t\tif (forwarded) {\n\t\t\tLOG((\"API Error: Server removed forwarded information.\"));\n\t\t}\n\t\treturn;\n\t} else if (!forwarded) {\n\t\tLOG((\"API Error: Server added forwarded information.\"));\n\t\treturn;\n\t}\n\tfwd->match([&](const MTPDmessageFwdHeader &data) {\n\t\tauto config = CreateConfig();\n\t\tFillForwardedInfo(config, data);\n\t\tsetupForwardedComponent(config);\n\t\thistory()->owner().requestItemResize(this);\n\t});\n}\n\nvoid HistoryItem::applyEditionToHistoryCleared() {\n\tconst auto rootId = topicRootId();\n\tconst auto topicPost = (rootId != Data::ForumTopic::kGeneralId);\n\tauto action = Api::SendAction(history());\n\taction.replyTo = FullReplyTo{\n\t\t.messageId = FullMsgId(_history->peer->id, topicPost ? rootId : 0),\n\t\t.topicRootId = rootId,\n\t};\n\tusing Flag = MTPDmessageService::Flag;\n\tapplyEdition(\n\t\tMTP_messageService(\n\t\t\tMTP_flags(Flag()\n\t\t\t\t| (topicPost ? Flag::f_reply_to : Flag())),\n\t\t\tMTP_int(id),\n\t\t\tpeerToMTP(PeerId(0)), // from_id\n\t\t\tpeerToMTP(_history->peer->id),\n\t\t\tMTPPeer(), // saved_peer_id\n\t\t\tNewMessageReplyHeader(action),\n\t\t\tMTP_int(date()),\n\t\t\tMTP_messageActionHistoryClear(),\n\t\t\tMTPMessageReactions(),\n\t\t\tMTPint() // ttl_period\n\t\t).c_messageService());\n}\n\nvoid HistoryItem::updateReplyMarkup(\n\t\tHistoryMessageMarkupData &&markup,\n\t\tbool ignoreSuggestButtons) {\n\tsetReplyMarkup(std::move(markup), ignoreSuggestButtons);\n}\n\nvoid HistoryItem::contributeToSlowmode(TimeId realDate) {\n\tif (const auto channel = history()->peer->asChannel()) {\n\t\tif (out() && isRegular() && !isService()) {\n\t\t\tchannel->growSlowmodeLastMessage(realDate ? realDate : date());\n\t\t}\n\t}\n}\n\nvoid HistoryItem::clearMediaAsExpired() {\n\tconst auto media = this->media();\n\tif (!media || !media->ttlSeconds()) {\n\t\treturn;\n\t}\n\tif (const auto document = media->document()) {\n\t\tapplyEditionToHistoryCleared();\n\t\tauto text = (document->isVideoFile()\n\t\t\t? tr::lng_ttl_video_expired\n\t\t\t: document->isVoiceMessage()\n\t\t\t? tr::lng_ttl_voice_expired\n\t\t\t: document->isVideoMessage()\n\t\t\t? tr::lng_ttl_round_expired\n\t\t\t: tr::lng_message_empty)(tr::now, tr::marked);\n\t\tupdateServiceText({ std::move(text) });\n\t\t_flags |= MessageFlag::ReactionsAllowed;\n\t} else if (media->photo()) {\n\t\tapplyEditionToHistoryCleared();\n\t\tupdateServiceText({\n\t\t\ttr::lng_ttl_photo_expired(tr::now, tr::marked)\n\t\t});\n\t\t_flags |= MessageFlag::ReactionsAllowed;\n\t}\n}\n\nvoid HistoryItem::addToUnreadThings(HistoryUnreadThings::AddType type) {\n\tif (!isRegular()) {\n\t\treturn;\n\t}\n\tconst auto mention = isUnreadMention();\n\tconst auto reaction = hasUnreadReaction();\n\tconst auto pollVote = hasUnreadPollVote();\n\tif (!mention && !reaction && !pollVote) {\n\t\treturn;\n\t}\n\tconst auto topic = this->topic();\n\tconst auto history = this->history();\n\tconst auto changes = &history->session().changes();\n\tif (mention) {\n\t\tif (history->unreadMentions().add(id, type)) {\n\t\t\tchanges->historyUpdated(\n\t\t\t\thistory,\n\t\t\t\tData::HistoryUpdate::Flag::UnreadMentions);\n\t\t}\n\t\tif (topic && topic->unreadMentions().add(id, type)) {\n\t\t\tchanges->topicUpdated(\n\t\t\t\ttopic,\n\t\t\t\tData::TopicUpdate::Flag::UnreadMentions);\n\t\t}\n\t}\n\tif (reaction) {\n\t\tconst auto sublist = this->savedSublist();\n\t\tconst auto toHistory = history->unreadReactions().add(id, type);\n\t\tconst auto toTopic = topic && topic->unreadReactions().add(id, type);\n\t\tconst auto toSublist = sublist\n\t\t\t&& sublist->parentChat()\n\t\t\t&& sublist->unreadReactions().add(id, type);\n\t\tif (toHistory || toTopic || toSublist) {\n\t\t\tif (type == HistoryUnreadThings::AddType::New) {\n\t\t\t\tchanges->messageUpdated(\n\t\t\t\t\tthis,\n\t\t\t\t\tData::MessageUpdate::Flag::NewUnreadReaction);\n\t\t\t}\n\t\t\tif (hasUnreadReaction()) {\n\t\t\t\tif (toHistory) {\n\t\t\t\t\tchanges->historyUpdated(\n\t\t\t\t\t\thistory,\n\t\t\t\t\t\tData::HistoryUpdate::Flag::UnreadReactions);\n\t\t\t\t}\n\t\t\t\tif (toTopic) {\n\t\t\t\t\tchanges->topicUpdated(\n\t\t\t\t\t\ttopic,\n\t\t\t\t\t\tData::TopicUpdate::Flag::UnreadReactions);\n\t\t\t\t}\n\t\t\t\tif (toSublist) {\n\t\t\t\t\tchanges->sublistUpdated(\n\t\t\t\t\t\tsublist,\n\t\t\t\t\t\tData::SublistUpdate::Flag::UnreadReactions);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (pollVote) {\n\t\tconst auto toHistory = history->unreadPollVotes().add(id, type);\n\t\tconst auto toTopic = topic\n\t\t\t&& topic->unreadPollVotes().add(id, type);\n\t\tif (toHistory || toTopic) {\n\t\t\tif (toHistory) {\n\t\t\t\tchanges->historyUpdated(\n\t\t\t\t\thistory,\n\t\t\t\t\tData::HistoryUpdate::Flag::UnreadPollVotes);\n\t\t\t}\n\t\t\tif (toTopic) {\n\t\t\t\tchanges->topicUpdated(\n\t\t\t\t\ttopic,\n\t\t\t\t\tData::TopicUpdate::Flag::UnreadPollVotes);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid HistoryItem::destroyHistoryEntry() {\n\tif (isUnreadMention()) {\n\t\thistory()->unreadMentions().erase(id);\n\t\tif (const auto topic = this->topic()) {\n\t\t\ttopic->unreadMentions().erase(id);\n\t\t}\n\t}\n\tif (hasUnreadReaction()) {\n\t\thistory()->unreadReactions().erase(id);\n\t\tif (const auto topic = this->topic()) {\n\t\t\ttopic->unreadReactions().erase(id);\n\t\t} else if (const auto sublist = this->savedSublist()) {\n\t\t\tsublist->unreadReactions().erase(id);\n\t\t}\n\t}\n\tif (hasUnreadPollVote()) {\n\t\thistory()->unreadPollVotes().erase(id);\n\t\tif (const auto topic = this->topic()) {\n\t\t\ttopic->unreadPollVotes().erase(id);\n\t\t}\n\t}\n\tif (isRegular() && _history->peer->isMegagroup()) {\n\t\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\t\tchangeReplyToTopCounter(reply, -1);\n\t\t}\n\t}\n}\n\nStorage::SharedMediaTypesMask HistoryItem::sharedMediaTypes() const {\n\tauto result = Storage::SharedMediaTypesMask{};\n\tconst auto saved = Get<HistoryMessageSavedMediaData>();\n\tconst auto media = saved ? saved->media.get() : _media.get();\n\tif (media) {\n\t\tresult.set(media->sharedMediaTypes());\n\t}\n\tif (hasTextLinks()) {\n\t\tresult.set(Storage::SharedMediaType::Link);\n\t}\n\tif (isPinned()) {\n\t\tresult.set(Storage::SharedMediaType::Pinned);\n\t}\n\treturn result;\n}\n\nvoid HistoryItem::indexAsNewItem() {\n\tif (isRegular()) {\n\t\taddToUnreadThings(HistoryUnreadThings::AddType::New);\n\t}\n\taddToSharedMediaIndex();\n}\n\nvoid HistoryItem::addToSharedMediaIndex() {\n\tif (isRegular()) {\n\t\tif (const auto types = sharedMediaTypes()) {\n\t\t\t_history->session().storage().add(Storage::SharedMediaAddNew(\n\t\t\t\t_history->peer->id,\n\t\t\t\ttopicRootId(),\n\t\t\t\tsublistPeerId(),\n\t\t\t\ttypes,\n\t\t\t\tid));\n\t\t\tif (types.test(Storage::SharedMediaType::Pinned)) {\n\t\t\t\t_history->setHasPinnedMessages(true);\n\t\t\t\tif (const auto topic = this->topic()) {\n\t\t\t\t\ttopic->setHasPinnedMessages(true);\n\t\t\t\t}\n\t\t\t\tif (const auto sublist = this->savedSublist()) {\n\t\t\t\t\tsublist->setHasPinnedMessages(true);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid HistoryItem::removeFromSharedMediaIndex() {\n\tif (isRegular()) {\n\t\tif (const auto types = sharedMediaTypes()) {\n\t\t\t_history->session().storage().remove(\n\t\t\t\tStorage::SharedMediaRemoveOne(\n\t\t\t\t\t_history->peer->id,\n\t\t\t\t\ttypes,\n\t\t\t\t\tid));\n\t\t}\n\t}\n}\n\nvoid HistoryItem::addToMessagesIndex() {\n\tif (isRegular()) {\n\t\tif (const auto messages = _history->maybeMessages()) {\n\t\t\tmessages->addNew(id);\n\t\t}\n\t}\n}\n\nvoid HistoryItem::incrementReplyToTopCounter() {\n\tif (isRegular()\n\t\t&& (_history->peer->isMegagroup() || _history->peer->forum())) {\n\t\t_history->session().changes().messageUpdated(\n\t\t\tthis,\n\t\t\tData::MessageUpdate::Flag::ReplyToTopAdded);\n\t\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\t\tchangeReplyToTopCounter(reply, 1);\n\t\t}\n\t}\n}\n\nvoid HistoryItem::changeReplyToTopCounter(\n\t\tnot_null<HistoryMessageReply*> reply,\n\t\tint delta) {\n\tconst auto topId = reply->topMessageId();\n\tif (!topId) {\n\t\treturn;\n\t}\n\tconst auto top = _history->owner().message(_history->peer->id, topId);\n\tif (!top) {\n\t\treturn;\n\t}\n\tconst auto from = displayFrom();\n\tconst auto replier = from ? from->id : PeerId();\n\ttop->changeRepliesCount(delta, replier);\n\tif (const auto original = top->lookupDiscussionPostOriginal()) {\n\t\toriginal->changeRepliesCount(delta, replier);\n\t}\n}\n\nQString HistoryItem::notificationHeader() const {\n\tif (isService()) {\n\t\treturn QString();\n\t} else if (out() && isFromScheduled() && !_history->peer->isSelf()) {\n\t\treturn tr::lng_from_you(tr::now);\n\t} else if (!_history->peer->isUser() && !isPostHidingAuthor()) {\n\t\treturn from()->name();\n\t}\n\treturn QString();\n}\n\nvoid HistoryItem::markTextAppearingStarted() {\n\t_flags |= MessageFlag::TextAppearingStarted;\n}\n\nvoid HistoryItem::setRealId(MsgId newId) {\n\tExpects(isSending() || textAppearing());\n\tExpects(IsClientMsgId(id));\n\n\tconst auto oldId = std::exchange(id, newId);\n\t_flags &= ~(MessageFlag::BeingSent | MessageFlag::Local);\n\tif (textAppearing()) {\n\t\tmarkTextAppearingStarted();\n\t}\n\tif (isBusinessShortcut()) {\n\t\t_date = 0;\n\t}\n\tif (isRegular()) {\n\t\t_history->unregisterClientSideMessage(this);\n\t}\n\t_history->owner().notifyItemIdChange({ fullId(), oldId });\n\n\t// We don't fire MessageUpdate::Flag::ReplyMarkup and update keyboard\n\t// in history widget, because it can't exist for an outgoing message.\n\t// Only inline keyboards can be in outgoing messages.\n\tif (const auto markup = inlineReplyMarkup()) {\n\t\tif (markup->inlineKeyboard) {\n\t\t\tmarkup->inlineKeyboard->updateMessageId();\n\t\t}\n\t}\n\n\t_history->owner().notifyItemDataChange(this);\n\t_history->owner().groups().refreshMessage(this);\n\t_history->owner().requestItemResize(this);\n\t_history->owner().requestItemRepaint(this);\n\n\tincrementReplyToTopCounter();\n\t_history->session().changes().messageUpdated(\n\t\tthis,\n\t\tData::MessageUpdate::Flag::NewMaybeAdded);\n\n\tif (out() && starsPaid()) {\n\t\t_history->session().credits().load(true);\n\t}\n}\n\nbool HistoryItem::canPin() const {\n\tif (!isRegular() || isService()) {\n\t\treturn false;\n\t} else if (const auto m = media(); m && m->call()) {\n\t\treturn false;\n\t}\n\treturn _history->peer->canPinMessages();\n}\n\nbool HistoryItem::allowsSendNow() const {\n\treturn !isService()\n\t\t&& isScheduled()\n\t\t&& !isSending()\n\t\t&& !hasFailed()\n\t\t&& !isEditingMedia()\n\t\t&& (paidType() == PaidPostType::None);\n}\n\nbool HistoryItem::allowsReschedule() const {\n\treturn allowsSendNow() && !awaitingVideoProcessing();\n}\n\nbool HistoryItem::allowsForward() const {\n\treturn !isService()\n\t\t&& isRegular()\n\t\t&& !forbidsForward()\n\t\t&& history()->peer->allowsForwarding()\n\t\t&& (!_media || _media->allowsForward());\n}\n\nbool HistoryItem::isTooOldForEdit(TimeId now) const {\n\treturn !_history->peer->canEditMessagesIndefinitely()\n\t\t&& !isScheduled()\n\t\t&& (now - date() >= _history->session().serverConfig().editTimeLimit);\n}\n\nbool HistoryItem::allowsEdit(TimeId now) const {\n\treturn !isService()\n\t\t&& canBeEdited()\n\t\t&& !isTooOldForEdit(now)\n\t\t&& (!_media || _media->allowsEdit())\n\t\t&& !isLegacyMessage()\n\t\t&& !isEditingMedia()\n\t\t&& (paidType() == PaidPostType::None);\n}\n\nbool HistoryItem::allowsEditMedia() const {\n\treturn !awaitingVideoProcessing()\n\t\t&& (!_media || _media->allowsEditMedia() || _media->webpage());\n}\n\nbool HistoryItem::canBeEdited() const {\n\tif ((!isRegular() && !isScheduled() && !isBusinessShortcut())\n\t\t|| Has<HistoryMessageVia>()\n\t\t|| Has<HistoryMessageForwarded>()) {\n\t\treturn false;\n\t}\n\n\tconst auto peer = _history->peer;\n\tif (peer->isSelf()) {\n\t\treturn true;\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tif (isPost() && channel->canEditMessages()) {\n\t\t\treturn true;\n\t\t} else if (out()) {\n\t\t\tif (isPost()) {\n\t\t\t\treturn channel->canPostMessages();\n\t\t\t} else if (const auto topic = this->topic()) {\n\t\t\t\treturn Data::CanSendAnything(topic);\n\t\t\t} else {\n\t\t\t\treturn Data::CanSendAnything(channel);\n\t\t\t}\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn out();\n}\n\nbool HistoryItem::canStopPoll() const {\n\treturn canBeEdited() && isRegular();\n}\n\nbool HistoryItem::forbidsForward() const {\n\treturn (_flags & MessageFlag::NoForwards);\n}\n\nbool HistoryItem::forbidsSaving() const {\n\tif (forbidsForward()) {\n\t\treturn true;\n\t} else if (const auto invoice = _media ? _media->invoice() : nullptr) {\n\t\treturn HasExtendedMedia(*invoice);\n\t}\n\treturn false;\n}\n\nbool HistoryItem::canDelete() const {\n\tif (isSponsored()) {\n\t\treturn false;\n\t} else if (IsStoryMsgId(id)) {\n\t\treturn false;\n\t} else if (isService() && !isRegular()) {\n\t\treturn false;\n\t} else if (topicRootId() == id) {\n\t\treturn false;\n\t} else if (!isHistoryEntry()\n\t\t&& !isScheduled()\n\t\t&& !isBusinessShortcut()) {\n\t\treturn false;\n\t}\n\tauto channel = _history->peer->asChannel();\n\tif (!channel) {\n\t\treturn !isGroupMigrate();\n\t}\n\n\tif (id == 1) {\n\t\treturn false;\n\t}\n\tif (channel->canDeleteMessages()) {\n\t\treturn true;\n\t} else if (out() && !isService()) {\n\t\treturn isPost() ? channel->canPostMessages() : true;\n\t}\n\treturn false;\n}\n\nbool HistoryItem::canDeleteForEveryone(TimeId now) const {\n\tconst auto peer = _history->peer;\n\tconst auto &config = _history->session().serverConfig();\n\tconst auto messageToMyself = peer->isSelf();\n\tconst auto messageTooOld = messageToMyself\n\t\t? false\n\t\t: peer->isUser()\n\t\t? (now - date() >= config.revokePrivateTimeLimit)\n\t\t: (now - date() >= config.revokeTimeLimit);\n\tif (!isRegular() || messageToMyself || messageTooOld || isPost()) {\n\t\treturn false;\n\t}\n\tif (peer->isChannel()) {\n\t\treturn false;\n\t} else if (const auto user = peer->asUser()) {\n\t\t// Bots receive all messages and there is no sense in revoking them.\n\t\t// See https://github.com/telegramdesktop/tdesktop/issues/3818\n\t\tif ((user->isBot() && !user->isSupport())\n\t\t\t|| user->isInaccessible()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\tif (const auto media = this->media()) {\n\t\tif (!media->allowsRevoke(now)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\tif (!out()) {\n\t\tif (const auto chat = peer->asChat()) {\n\t\t\tif (!chat->canDeleteMessages()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else if (peer->isUser()) {\n\t\t\treturn config.revokePrivateInbox;\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nbool HistoryItem::suggestReport() const {\n\tif (out() || isService() || !isRegular()) {\n\t\treturn false;\n\t} else if (_history->peer->isChannel()) {\n\t\treturn true;\n\t} else if (const auto user = _history->peer->asUser()) {\n\t\treturn user->isBot();\n\t}\n\treturn false;\n}\n\nbool HistoryItem::suggestBanReport() const {\n\tconst auto channel = _history->peer->asChannel();\n\tif (!channel || !channel->canRestrictParticipant(from())) {\n\t\treturn false;\n\t}\n\treturn !isPost() && !out();\n}\n\nbool HistoryItem::suggestDeleteAllReport() const {\n\tauto channel = _history->peer->asChannel();\n\tif (!channel || !channel->canDeleteMessages()) {\n\t\treturn false;\n\t}\n\treturn !isPost() && !out();\n}\n\nChatRestriction HistoryItem::requiredSendRight() const {\n\tconst auto media = this->media();\n\tif (media && media->game()) {\n\t\treturn ChatRestriction::SendGames;\n\t}\n\tconst auto photo = (media && !media->webpage())\n\t\t? media->photo()\n\t\t: nullptr;\n\tconst auto document = (media && !media->webpage())\n\t\t? media->document()\n\t\t: nullptr;\n\tif (photo) {\n\t\treturn ChatRestriction::SendPhotos;\n\t} else if (document) {\n\t\treturn document->requiredSendRight();\n\t} else if (media && media->poll()) {\n\t\treturn ChatRestriction::SendPolls;\n\t}\n\treturn ChatRestriction::SendOther;\n}\n\nbool HistoryItem::requiresSendInlineRight() const {\n\treturn Has<HistoryMessageVia>();\n}\n\nData::SendError HistoryItem::errorTextForForward(\n\t\tnot_null<Data::Thread*> to) const {\n\tconst auto requiredRight = requiredSendRight();\n\tconst auto requiresInline = requiresSendInlineRight();\n\tconst auto peer = to->peer();\n\tconstexpr auto kInline = ChatRestriction::SendInline;\n\tif (const auto error = Data::RestrictionError(peer, requiredRight)) {\n\t\treturn error;\n\t} else if (requiresInline && !Data::CanSend(to, kInline)) {\n\t\tconst auto forInline = Data::RestrictionError(peer, kInline);\n\t\treturn forInline ? forInline : tr::lng_forward_cant(tr::now);\n\t} else if (const auto specific = errorTextForForwardIgnoreRights(to)) {\n\t\treturn specific;\n\t} else if (!Data::CanSend(to, requiredRight, false)) {\n\t\treturn tr::lng_forward_cant(tr::now);\n\t}\n\treturn {};\n}\n\nData::SendError HistoryItem::errorTextForForwardIgnoreRights(\n\t\tnot_null<Data::Thread*> to) const {\n\tconst auto peer = to->peer();\n\tif (_media\n\t\t&& _media->poll()\n\t\t&& _media->poll()->publicVotes()\n\t\t&& peer->isBroadcast()) {\n\t\treturn tr::lng_restricted_send_public_polls(tr::now);\n\t} else if (_media\n\t\t&& _media->todolist()\n\t\t&& (peer->isBroadcast() || peer->isMonoforum())) {\n\t\treturn tr::lng_restricted_send_todo_lists(tr::now);\n\t} else if (_media\n\t\t&& _media->invoice()\n\t\t&& _media->invoice()->isPaidMedia\n\t\t&& peer->isBroadcast()\n\t\t&& peer->isFullLoaded()\n\t\t&& !peer->asBroadcast()->canPostPaidMedia()) {\n\t\treturn tr::lng_restricted_send_paid_media(tr::now);\n\t}\n\treturn {};\n}\n\nconst HistoryMessageTranslation *HistoryItem::translation() const {\n\treturn Get<HistoryMessageTranslation>();\n}\n\nbool HistoryItem::translationShowRequiresCheck(LanguageId to) const {\n\t// Check if a call to translationShowRequiresRequest(to) is not a no-op.\n\tif (!to) {\n\t\tif (const auto translation = Get<HistoryMessageTranslation>()) {\n\t\t\treturn (!translation->failed && translation->text.empty())\n\t\t\t\t|| translation->used;\n\t\t}\n\t\treturn false;\n\t} else if (const auto translation = Get<HistoryMessageTranslation>()) {\n\t\tif (translation->to == to) {\n\t\t\treturn !translation->used && !translation->text.empty();\n\t\t}\n\t\treturn true;\n\t} else {\n\t\treturn true;\n\t}\n}\n\nbool HistoryItem::translationShowRequiresRequest(LanguageId to) {\n\t// When changing be sure to reflect in translationShowRequiresCheck(to).\n\tif (!to) {\n\t\tif (const auto translation = Get<HistoryMessageTranslation>()) {\n\t\t\tif (!translation->failed && translation->text.empty()) {\n\t\t\t\tAssert(!translation->used);\n\t\t\t\tRemoveComponents(HistoryMessageTranslation::Bit());\n\t\t\t} else {\n\t\t\t\ttranslationToggle(translation, false);\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t} else if (const auto translation = Get<HistoryMessageTranslation>()) {\n\t\tif (translation->to == to) {\n\t\t\ttranslationToggle(translation, true);\n\t\t\treturn false;\n\t\t}\n\t\ttranslationToggle(translation, false);\n\t\ttranslation->to = to;\n\t\ttranslation->requested = true;\n\t\ttranslation->failed = false;\n\t\ttranslation->text = {};\n\t\treturn true;\n\t} else {\n\t\tAddComponents(HistoryMessageTranslation::Bit());\n\t\tconst auto added = Get<HistoryMessageTranslation>();\n\t\tadded->to = to;\n\t\tadded->requested = true;\n\t\treturn true;\n\t}\n}\n\nvoid HistoryItem::translationToggle(\n\t\tnot_null<HistoryMessageTranslation*> translation,\n\t\tbool used) {\n\tif (translation->used != used && !translation->text.empty()) {\n\t\ttranslation->used = used;\n\t\t_history->owner().requestItemTextRefresh(this);\n\t\t_history->owner().updateDependentMessages(this);\n\t}\n}\n\nvoid HistoryItem::translationDone(LanguageId to, TextWithEntities result) {\n\tconst auto set = [&](not_null<HistoryMessageTranslation*> translation) {\n\t\tif (result.empty()) {\n\t\t\ttranslation->failed = true;\n\t\t} else {\n\t\t\ttranslation->text = std::move(result);\n\t\t\tif (_history->translatedTo() == to) {\n\t\t\t\ttranslationToggle(translation, true);\n\t\t\t}\n\t\t}\n\t};\n\tif (const auto translation = Get<HistoryMessageTranslation>()) {\n\t\tif (translation->to == to && translation->text.empty()) {\n\t\t\ttranslation->requested = false;\n\t\t\tset(translation);\n\t\t}\n\t} else {\n\t\tAddComponents(HistoryMessageTranslation::Bit());\n\t\tconst auto added = Get<HistoryMessageTranslation>();\n\t\tadded->to = to;\n\t\tset(added);\n\t}\n}\n\nbool HistoryItem::canReact() const {\n\tif (!isRegular()) {\n\t\treturn false;\n\t} else if (isService()) {\n\t\treturn (_flags & MessageFlag::ReactionsAllowed);\n\t} else if (const auto media = this->media()) {\n\t\tif (media->call()) {\n\t\t\treturn (_flags & MessageFlag::ReactionsAllowed);\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid HistoryItem::addPaidReaction(\n\t\tint count,\n\t\tstd::optional<PeerId> shownPeer) {\n\tExpects(count >= 0);\n\tExpects(_history->peer->isBroadcast() || isDiscussionPost());\n\n\tif (!_reactions) {\n\t\t_reactions = std::make_unique<Data::MessageReactions>(this);\n\t}\n\t_reactions->scheduleSendPaid(count, shownPeer);\n\tif (count > 0) {\n\t\t_history->owner().notifyItemDataChange(this);\n\t}\n}\n\nvoid HistoryItem::cancelScheduledPaidReaction() {\n\tif (_reactions) {\n\t\t_reactions->cancelScheduledPaid();\n\t\t_history->owner().notifyItemDataChange(this);\n\t}\n}\n\nData::PaidReactionSend HistoryItem::startPaidReactionSending() {\n\treturn _reactions\n\t\t? _reactions->startPaidSending()\n\t\t: Data::PaidReactionSend();\n}\n\nvoid HistoryItem::finishPaidReactionSending(\n\t\tData::PaidReactionSend send,\n\t\tbool success) {\n\tExpects(_reactions != nullptr);\n\n\t_reactions->finishPaidSending(send, success);\n\t_history->owner().notifyItemDataChange(this);\n}\n\nvoid HistoryItem::toggleReaction(\n\t\tconst Data::ReactionId &reaction,\n\t\tHistoryReactionSource source) {\n\tExpects(!reaction.paid());\n\n\tconst auto addToRecent = (source == HistoryReactionSource::Selector);\n\tif (!_reactions) {\n\t\t_reactions = std::make_unique<Data::MessageReactions>(this);\n\t\tconst auto canViewReactions = !isDiscussionPost()\n\t\t\t&& (_history->peer->isChat() || _history->peer->isMegagroup());\n\t\tif (canViewReactions) {\n\t\t\t_flags |= MessageFlag::CanViewReactions;\n\t\t}\n\t\t_reactions->add(reaction, addToRecent);\n\t} else if (ranges::contains(_reactions->chosen(), reaction)) {\n\t\t_reactions->remove(reaction);\n\t\tif (_reactions->empty() && !_reactions->localPaidData()) {\n\t\t\t_reactions = nullptr;\n\t\t\t_flags &= ~MessageFlag::CanViewReactions;\n\t\t}\n\t} else {\n\t\t_reactions->add(reaction, addToRecent);\n\t}\n\t_history->owner().notifyItemDataChange(this);\n}\n\nvoid HistoryItem::updateReactionsUnknown() {\n\t_reactionsLastRefreshed = 1;\n}\n\nconst std::vector<Data::MessageReaction> &HistoryItem::reactions() const {\n\tstatic const auto kEmpty = std::vector<Data::MessageReaction>();\n\treturn _reactions ? _reactions->list() : kEmpty;\n}\n\nstd::vector<Data::MessageReaction> HistoryItem::reactionsWithLocal() const {\n\tif (!_reactions) {\n\t\treturn {};\n\t}\n\tauto result = _reactions->list();\n\tconst auto i = ranges::find(\n\t\tresult,\n\t\tData::ReactionId::Paid(),\n\t\t&Data::MessageReaction::id);\n\tif (const auto local = _reactions->localPaidCount()) {\n\t\tif (i != end(result)) {\n\t\t\ti->my = true;\n\t\t\ti->count += local;\n\t\t\tif (i != begin(result)) {\n\t\t\t\tstd::rotate(begin(result), i, i + 1);\n\t\t\t}\n\t\t} else {\n\t\t\tresult.insert(begin(result), Data::MessageReaction{\n\t\t\t\t.id = Data::ReactionId::Paid(),\n\t\t\t\t.count = local,\n\t\t\t\t.my = true,\n\t\t\t});\n\t\t}\n\t} else if (i != end(result) && i != begin(result)) {\n\t\tstd::rotate(begin(result), i, i + 1);\n\t}\n\treturn result;\n}\n\nint HistoryItem::reactionsPaidScheduled() const {\n\treturn _reactions ? _reactions->scheduledPaid() : 0;\n}\n\nPeerId HistoryItem::reactionsLocalShownPeer() const {\n\treturn _reactions\n\t\t? _reactions->localPaidShownPeer()\n\t\t: _history->session().userPeerId();\n}\n\nbool HistoryItem::reactionsAreTags() const {\n\treturn _flags & MessageFlag::ReactionsAreTags;\n}\n\nauto HistoryItem::recentReactions() const\n-> const base::flat_map<\n\t\tData::ReactionId,\n\t\tstd::vector<Data::RecentReaction>> & {\n\tstatic const auto kEmpty = base::flat_map<\n\t\tData::ReactionId,\n\t\tstd::vector<Data::RecentReaction>>();\n\treturn _reactions ? _reactions->recent() : kEmpty;\n}\n\nauto HistoryItem::topPaidReactionsWithLocal() const\n-> std::vector<Data::MessageReactionsTopPaid> {\n\tif (!_reactions) {\n\t\treturn {};\n\t}\n\tusing TopPaid = Data::MessageReactionsTopPaid;\n\tauto result = _reactions->topPaid();\n\tconst auto i = ranges::find_if(\n\t\tresult,\n\t\t[](const TopPaid &entry) { return entry.my != 0; });\n\tconst auto peerForMine = [&] {\n\t\tconst auto peerId = _reactions->localPaidShownPeer();\n\t\treturn peerId ? history()->owner().peer(peerId).get() : nullptr;\n\t};\n\tif (const auto local = _reactions->localPaidCount()) {\n\t\tconst auto top = [&](int mine) {\n\t\t\treturn ranges::count_if(result, [&](const TopPaid &entry) {\n\t\t\t\treturn !entry.my && entry.count >= mine;\n\t\t\t}) < 3;\n\t\t};\n\t\tif (i != end(result)) {\n\t\t\ti->count += local;\n\t\t\ti->peer = peerForMine();\n\t\t\ti->top = top(i->count) ? 1 : 0;\n\t\t} else {\n\t\t\tresult.push_back({\n\t\t\t\t.peer = peerForMine(),\n\t\t\t\t.count = uint32(local),\n\t\t\t\t.top = uint32(top(local) ? 1 : 0),\n\t\t\t\t.my = uint32(1),\n\t\t\t});\n\t\t}\n\t} else if (i != end(result)) {\n\t\ti->peer = peerForMine();\n\t}\n\treturn result;\n}\n\nbool HistoryItem::canViewReactions() const {\n\treturn (_flags & MessageFlag::CanViewReactions)\n\t\t&& _reactions\n\t\t&& !_reactions->list().empty();\n}\n\nstd::vector<Data::ReactionId> HistoryItem::chosenReactions() const {\n\treturn _reactions\n\t\t? _reactions->chosen()\n\t\t: std::vector<Data::ReactionId>();\n}\n\nData::ReactionId HistoryItem::lookupUnreadReaction(\n\t\tnot_null<UserData*> from) const {\n\tif (!_reactions) {\n\t\treturn {};\n\t}\n\tconst auto recent = _reactions->recent();\n\tfor (const auto &[id, list] : _reactions->recent()) {\n\t\tconst auto i = ranges::find(\n\t\t\tlist,\n\t\t\tfrom,\n\t\t\t&Data::RecentReaction::peer);\n\t\tif (i != end(list) && i->unread) {\n\t\t\treturn id;\n\t\t}\n\t}\n\treturn {};\n}\n\nQByteArray HistoryItem::lookupUnreadPollVote(\n\t\tnot_null<PeerData*> from) const {\n\tconst auto m = media();\n\tconst auto poll = m ? m->poll() : nullptr;\n\tif (!poll) {\n\t\treturn {};\n\t}\n\tfor (const auto &answer : poll->answers) {\n\t\tif (ranges::contains(answer.recentVoters, from)) {\n\t\t\treturn answer.option;\n\t\t}\n\t}\n\treturn {};\n}\n\ncrl::time HistoryItem::lastReactionsRefreshTime() const {\n\treturn _reactionsLastRefreshed;\n}\n\nbool HistoryItem::hasDirectLink() const {\n\treturn isRegular() && _history->peer->isChannel();\n}\n\nbool HistoryItem::changesWallPaper() const {\n\tif (const auto media = _media.get()) {\n\t\treturn media->paper() != nullptr;\n\t}\n\treturn Has<HistoryServiceSameBackground>();\n}\n\nFullMsgId HistoryItem::fullId() const {\n\treturn FullMsgId(_history->peer->id, id);\n}\n\nGlobalMsgId HistoryItem::globalId() const {\n\treturn { fullId(), _history->session().uniqueId() };\n}\n\nData::MessagePosition HistoryItem::position() const {\n\treturn { .fullId = fullId(), .date = date() };\n}\n\nbool HistoryItem::computeDropForwardedInfo() const {\n\tconst auto media = this->media();\n\treturn (media && media->dropForwardedInfo())\n\t\t|| (_history->peer->isSelf()\n\t\t\t&& !Has<HistoryMessageForwarded>()\n\t\t\t&& (!media || !media->forceForwardedInfo()));\n}\n\nbool HistoryItem::inThread(MsgId rootId) const {\n\treturn (replyToTop() == rootId)\n\t\t|| (topicRootId() == rootId);\n}\n\nnot_null<PeerData*> HistoryItem::author() const {\n\treturn (isPostHidingAuthor() && !isSponsored())\n\t\t? _history->peer\n\t\t: from();\n}\n\nTimeId HistoryItem::originalDate() const {\n\tif (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\treturn forwarded->originalDate;\n\t}\n\treturn date();\n}\n\nPeerData *HistoryItem::originalSender() const {\n\tif (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\treturn forwarded->originalSender;\n\t}\n\tconst auto peer = _history->peer;\n\treturn peer->isBroadcast() ? peer : from();\n}\n\nconst HiddenSenderInfo *HistoryItem::originalHiddenSenderInfo() const {\n\tif (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\treturn forwarded->originalHiddenSenderInfo.get();\n\t}\n\treturn nullptr;\n}\n\nconst HiddenSenderInfo *HistoryItem::displayHiddenSenderInfo() const {\n\tif (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\treturn forwarded->savedFromHiddenSenderInfo\n\t\t\t? forwarded->savedFromHiddenSenderInfo.get()\n\t\t\t: forwarded->originalHiddenSenderInfo.get();\n\t}\n\treturn nullptr;\n}\n\nbool HistoryItem::showForwardsFromSender(\n\t\tnot_null<const HistoryMessageForwarded*> forwarded) const {\n\tconst auto peer = history()->peer;\n\treturn !forwarded->story\n\t\t&& (peer->isSelf()\n\t\t\t|| peer->isRepliesChat()\n\t\t\t|| peer->isVerifyCodes()\n\t\t\t|| forwarded->imported);\n}\n\nnot_null<PeerData*> HistoryItem::fromOriginal() const {\n\tif (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\tif (forwarded->originalSender) {\n\t\t\tif (const auto user = forwarded->originalSender->asUser()) {\n\t\t\t\treturn user;\n\t\t\t}\n\t\t}\n\t}\n\treturn from();\n}\n\nQString HistoryItem::originalPostAuthor() const {\n\tif (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\treturn forwarded->originalPostAuthor;\n\t} else if (const auto msgsigned = Get<HistoryMessageSigned>()) {\n\t\tif (!msgsigned->isAnonymousRank && !msgsigned->viaBusinessBot) {\n\t\t\treturn msgsigned->author;\n\t\t}\n\t}\n\treturn QString();\n}\n\nQString HistoryItem::fromRank() const {\n\tif (const auto component = Get<HistoryMessageFromRank>()) {\n\t\treturn component->rank;\n\t}\n\treturn QString();\n}\n\nMsgId HistoryItem::originalId() const {\n\tif (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\treturn forwarded->originalId;\n\t}\n\treturn id;\n}\n\nconst TextWithEntities &HistoryItem::originalText() const {\n\tstatic const auto kEmpty = TextWithEntities();\n\treturn isService() ? kEmpty : _text;\n}\n\nconst TextWithEntities &HistoryItem::translatedText() const {\n\tif (isService()) {\n\t\tstatic const auto kEmpty = TextWithEntities();\n\t\treturn kEmpty;\n\t} else if (const auto translation = this->translation()\n\t\t; translation\n\t\t&& translation->used\n\t\t&& (translation->to == history()->translatedTo())) {\n\t\treturn translation->text;\n\t} else {\n\t\treturn originalText();\n\t}\n}\n\nTextWithEntities HistoryItem::translatedTextWithLocalEntities() const {\n\tif (isService()) {\n\t\treturn {};\n\t}\n\tauto result = withLocalEntities(translatedText());\n\n\tif (hideLinks()) {\n\t\tconst auto isUrl = [](const EntityInText &entity) {\n\t\t\tconst auto type = entity.type();\n\t\t\treturn (type == EntityType::Mention)\n\t\t\t\t|| (type == EntityType::Hashtag)\n\t\t\t\t|| (type == EntityType::Cashtag)\n\t\t\t\t|| (type == EntityType::Url)\n\t\t\t\t|| (type == EntityType::CustomUrl);\n\t\t};\n\t\tconst auto from = ranges::remove_if(result.entities, isUrl);\n\t\tif (from != result.entities.end()) {\n\t\t\tresult.entities.erase(from, result.entities.end());\n\t\t\tsetHasHiddenLinks(true);\n\t\t}\n\t}\n\n\treturn result;\n}\n\nvoid HistoryItem::setHasHiddenLinks(bool has) const {\n\tif (has) {\n\t\t_flags |= MessageFlag::HasHiddenLinks;\n\t} else {\n\t\t_flags &= ~MessageFlag::HasHiddenLinks;\n\t}\n}\n\nbool HistoryItem::hasHiddenLinks() const {\n\treturn _flags & MessageFlag::HasHiddenLinks;\n}\n\nTextForMimeData HistoryItem::clipboardText() const {\n\treturn isService()\n\t\t? TextForMimeData()\n\t\t: TextForMimeData::WithExpandedLinks(translatedText());\n}\n\nbool HistoryItem::changeViewsCount(int count) {\n\tconst auto views = Get<HistoryMessageViews>();\n\tif (!views\n\t\t|| views->views.count == count\n\t\t|| (count >= 0 && views->views.count > count)) {\n\t\treturn false;\n\t}\n\n\tviews->views.count = count;\n\treturn true;\n}\n\nvoid HistoryItem::setForwardsCount(int count) {\n\tconst auto views = Get<HistoryMessageViews>();\n\tif (!views\n\t\t|| views->forwardsCount == count\n\t\t|| (count >= 0 && views->forwardsCount > count)) {\n\t\treturn;\n\t}\n\n\tviews->forwardsCount = count;\n\thistory()->owner().notifyItemDataChange(this);\n}\n\nvoid HistoryItem::setPostAuthor(const QString &postAuthor) {\n\tauto msgsigned = Get<HistoryMessageSigned>();\n\tif (msgsigned && msgsigned->viaBusinessBot) {\n\t\treturn;\n\t} else if (postAuthor.isEmpty()) {\n\t\tif (!msgsigned) {\n\t\t\treturn;\n\t\t}\n\t\tRemoveComponents(HistoryMessageSigned::Bit());\n\t\thistory()->owner().requestItemResize(this);\n\t\treturn;\n\t}\n\tif (!msgsigned) {\n\t\tAddComponents(HistoryMessageSigned::Bit());\n\t\tmsgsigned = Get<HistoryMessageSigned>();\n\t} else if (msgsigned->author == postAuthor) {\n\t\treturn;\n\t}\n\tmsgsigned->author = postAuthor;\n\tmsgsigned->isAnonymousRank = !isDiscussionPost()\n\t\t&& this->author()->isMegagroup();\n\thistory()->owner().requestItemResize(this);\n}\n\nvoid HistoryItem::setReplies(HistoryMessageRepliesData &&data) {\n\tif (data.isNull) {\n\t\treturn;\n\t}\n\tauto views = Get<HistoryMessageViews>();\n\tif (!views) {\n\t\tAddComponents(HistoryMessageViews::Bit());\n\t\tviews = Get<HistoryMessageViews>();\n\t}\n\tconst auto &repliers = data.recentRepliers;\n\tconst auto count = data.repliesCount;\n\tconst auto channelId = data.channelId;\n\tconst auto readTillId = data.readMaxId\n\t\t? std::max({\n\t\t\tviews->commentsInboxReadTillId.bare,\n\t\t\tdata.readMaxId.bare,\n\t\t\tint64(1),\n\t\t})\n\t\t: views->commentsInboxReadTillId;\n\tconst auto maxId = data.maxId ? data.maxId : views->commentsMaxId;\n\tconst auto countsChanged = (views->replies.count != count)\n\t\t|| (views->commentsInboxReadTillId != readTillId)\n\t\t|| (views->commentsMaxId != maxId);\n\tconst auto megagroupChanged = (views->commentsMegagroupId != channelId);\n\tconst auto recentChanged = (views->recentRepliers != repliers);\n\tif (!countsChanged && !megagroupChanged && !recentChanged) {\n\t\treturn;\n\t}\n\tviews->replies.count = count;\n\tif (recentChanged) {\n\t\tviews->recentRepliers = repliers;\n\t}\n\tconst auto wasUnread = areCommentsUnread();\n\tviews->commentsMegagroupId = channelId;\n\tviews->commentsInboxReadTillId = readTillId;\n\tviews->commentsMaxId = maxId;\n\tif (wasUnread != areCommentsUnread()) {\n\t\thistory()->owner().requestItemRepaint(this);\n\t}\n\trefreshRepliesText(views, megagroupChanged);\n}\n\nvoid HistoryItem::clearReplies() {\n\tauto views = Get<HistoryMessageViews>();\n\tif (!views) {\n\t\treturn;\n\t}\n\tconst auto viewsPart = views->views;\n\tif (viewsPart.count < 0) {\n\t\tRemoveComponents(HistoryMessageViews::Bit());\n\t} else {\n\t\t*views = HistoryMessageViews();\n\t\tviews->views = viewsPart;\n\t}\n\thistory()->owner().requestItemResize(this);\n}\n\nvoid HistoryItem::refreshRepliesText(\n\t\tnot_null<HistoryMessageViews*> views,\n\t\tbool forceResize) {\n\tif (views->commentsMegagroupId) {\n\t\tviews->replies.text = (views->replies.count > 0)\n\t\t\t? tr::lng_comments_open_count(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_short,\n\t\t\t\tviews->replies.count)\n\t\t\t: tr::lng_comments_open_none(tr::now);\n\t\tviews->replies.textWidth = st::semiboldFont->width(\n\t\t\tviews->replies.text);\n\t\tviews->repliesSmall.text = (views->replies.count > 0)\n\t\t\t? Lang::FormatCountToShort(views->replies.count).string\n\t\t\t: QString();\n\t\tconst auto hadText = (views->repliesSmall.textWidth > 0);\n\t\tviews->repliesSmall.textWidth = (views->replies.count > 0)\n\t\t\t? st::semiboldFont->width(views->repliesSmall.text)\n\t\t\t: 0;\n\t\tconst auto hasText = (views->repliesSmall.textWidth > 0);\n\t\tif (hasText != hadText) {\n\t\t\tforceResize = true;\n\t\t}\n\t}\n\tif (forceResize) {\n\t\thistory()->owner().requestItemResize(this);\n\t} else {\n\t\thistory()->owner().requestItemRepaint(this);\n\t}\n}\n\nvoid HistoryItem::changeRepliesCount(int delta, PeerId replier) {\n\tconst auto views = Get<HistoryMessageViews>();\n\tconst auto limit = HistoryMessageViews::kMaxRecentRepliers;\n\tif (!views) {\n\t\treturn;\n\t}\n\n\t// Update full count.\n\tif (views->replies.count < 0) {\n\t\treturn;\n\t}\n\tviews->replies.count = std::max(views->replies.count + delta, 0);\n\tif (replier && views->commentsMegagroupId) {\n\t\tif (delta < 0) {\n\t\t\tviews->recentRepliers.erase(\n\t\t\t\tranges::remove(views->recentRepliers, replier),\n\t\t\t\tend(views->recentRepliers));\n\t\t} else if (!ranges::contains(views->recentRepliers, replier)) {\n\t\t\tviews->recentRepliers.insert(views->recentRepliers.begin(), replier);\n\t\t\twhile (views->recentRepliers.size() > limit) {\n\t\t\t\tviews->recentRepliers.pop_back();\n\t\t\t}\n\t\t}\n\t}\n\trefreshRepliesText(views);\n\thistory()->owner().notifyItemDataChange(this);\n}\n\nvoid HistoryItem::setReplyFields(\n\t\tMsgId replyTo,\n\t\tMsgId replyToTop,\n\t\tbool isForumPost) {\n\tif (isScheduled()) {\n\t\treturn;\n\t} else if (const auto data = GetServiceDependentData()) {\n\t\tif ((data->topId != replyToTop) && !IsServerMsgId(data->topId)) {\n\t\t\tdata->topId = replyToTop;\n\t\t\tif (isForumPost) {\n\t\t\t\tdata->topicPost = true;\n\t\t\t}\n\t\t}\n\t} else if (const auto reply = Get<HistoryMessageReply>()) {\n\t\tconst auto increment = (reply->topMessageId() != replyToTop)\n\t\t\t&& !IsServerMsgId(reply->topMessageId());\n\t\treply->updateFields(this, replyTo, replyToTop, isForumPost);\n\t\tif (increment) {\n\t\t\tincrementReplyToTopCounter();\n\t\t}\n\t}\n\tif (const auto topic = this->topic()) {\n\t\ttopic->maybeSetLastMessage(this);\n\t}\n}\n\nvoid HistoryItem::updateDate(TimeId newDate) {\n\tif (canUpdateDate() && _date != newDate) {\n\t\t_date = newDate;\n\t\t_history->owner().requestItemViewRefresh(this);\n\t}\n}\n\nbool HistoryItem::canUpdateDate() const {\n\treturn isScheduled();\n}\n\nvoid HistoryItem::applyTTL(TimeId destroyAt) {\n\tconst auto previousDestroyAt = std::exchange(_ttlDestroyAt, destroyAt);\n\tif (previousDestroyAt) {\n\t\t_history->owner().unregisterMessageTTL(previousDestroyAt, this);\n\t}\n\tif (!_ttlDestroyAt) {\n\t\treturn;\n\t} else if (base::unixtime::now() >= _ttlDestroyAt) {\n\t\tconst auto session = &_history->session();\n\t\tcrl::on_main(session, [session, id = fullId()]{\n\t\t\tif (const auto item = session->data().message(id)) {\n\t\t\t\titem->destroy();\n\t\t\t}\n\t\t});\n\t} else {\n\t\t_history->owner().registerMessageTTL(_ttlDestroyAt, this);\n\t}\n}\n\nvoid HistoryItem::replaceBuyWithReceiptInMarkup() {\n\tif (const auto markup = inlineReplyMarkup()) {\n\t\tfor (auto &row : markup->data.rows) {\n\t\t\tfor (auto &button : row) {\n\t\t\t\tif (button.type == HistoryMessageMarkupButton::Type::Buy) {\n\t\t\t\t\tconst auto receipt = tr::lng_payments_receipt_button(tr::now);\n\t\t\t\t\tif (button.text != receipt) {\n\t\t\t\t\t\tbutton.text = receipt;\n\t\t\t\t\t\tif (markup->inlineKeyboard) {\n\t\t\t\t\t\t\tmarkup->inlineKeyboard = nullptr;\n\t\t\t\t\t\t\t_history->owner().requestItemResize(this);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nbool HistoryItem::isUploading() const {\n\treturn _media && _media->uploading();\n}\n\nbool HistoryItem::hasRealFromId() const {\n\treturn !isPost() || (_flags & MessageFlag::HasFromId);\n}\n\nbool HistoryItem::isPostHidingAuthor() const {\n\tif (!isPost()) {\n\t\treturn false;\n\t} else if (const auto channel = _history->peer->asBroadcast()) {\n\t\treturn !channel->signatureProfiles();\n\t}\n\treturn false; // Should not happen, I guess.\n}\n\nbool HistoryItem::isPostShowingAuthor() const {\n\treturn isPost() && !isPostHidingAuthor();\n}\n\nbool HistoryItem::isRegular() const {\n\treturn isHistoryEntry() && !isLocal();\n}\n\nint HistoryItem::viewsCount() const {\n\tif (const auto views = Get<HistoryMessageViews>()) {\n\t\treturn std::max(views->views.count, 0);\n\t}\n\treturn hasViews() ? 1 : -1;\n}\n\nint HistoryItem::repliesCount() const {\n\tif (const auto views = Get<HistoryMessageViews>()) {\n\t\tif (!checkDiscussionLink(views->commentsMegagroupId)) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn std::max(views->replies.count, 0);\n\t}\n\treturn 0;\n}\n\nbool HistoryItem::repliesAreComments() const {\n\tif (const auto views = Get<HistoryMessageViews>()) {\n\t\treturn (views->commentsMegagroupId != 0)\n\t\t\t&& checkDiscussionLink(views->commentsMegagroupId);\n\t}\n\treturn false;\n}\n\nbool HistoryItem::externalReply() const {\n\tif (!_history->peer->isRepliesChat()) {\n\t\treturn false;\n\t} else if (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\treturn forwarded->savedFromPeer && forwarded->savedFromMsgId;\n\t}\n\treturn false;\n}\n\nbool HistoryItem::hasUnpaidContent() const {\n\tif (const auto media = _media.get()) {\n\t\tif (const auto invoice = media->invoice()) {\n\t\t\treturn HasUnpaidMedia(*invoice);\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid HistoryItem::sendFailed() {\n\tExpects(_flags & MessageFlag::BeingSent);\n\tExpects(!(_flags & MessageFlag::SendingFailed));\n\n\t_flags = (_flags | MessageFlag::SendingFailed) & ~MessageFlag::BeingSent;\n\t_history->owner().notifyItemDataChange(this);\n\t_history->session().changes().historyUpdated(\n\t\t_history,\n\t\tData::HistoryUpdate::Flag::ClientSideMessages);\n}\n\nbool HistoryItem::needCheck() const {\n\treturn (out() && !isEmpty())\n\t\t|| (!isRegular() && _history->peer->isSelf());\n}\n\nbool HistoryItem::isService() const {\n\treturn Has<HistoryServiceData>();\n}\n\nbool HistoryItem::unread(not_null<Data::Thread*> thread) const {\n\t// Messages from myself are always read, unless scheduled.\n\tif (_history->peer->isSelf() && !isFromScheduled()) {\n\t\treturn false;\n\t}\n\n\t// All messages in converted chats are always read.\n\tif (_history->peer->migrateTo()) {\n\t\treturn false;\n\t}\n\n\tif (isRegular()) {\n\t\tif (!thread->isServerSideUnread(this)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (out()) {\n\t\t\tif (const auto user = _history->peer->asUser()) {\n\t\t\t\tif (user->isBot() && !user->isSupport()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} else if (const auto channel = _history->peer->asChannel()) {\n\t\t\t\tif (!channel->isMegagroup()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\treturn out() || (_flags & MessageFlag::ClientSideUnread);\n}\n\nMsgId HistoryItem::replyToId() const {\n\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\treturn reply->messageId();\n\t}\n\treturn 0;\n}\n\nFullMsgId HistoryItem::replyToFullId() const {\n\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\tconst auto peer = reply->externalPeerId();\n\t\treturn { peer ? peer : history()->peer->id, reply->messageId() };\n\t}\n\treturn {};\n}\n\nMsgId HistoryItem::replyToTop() const {\n\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\treturn reply->topMessageId();\n\t} else if (const auto data = GetServiceDependentData()) {\n\t\treturn data->topId;\n\t}\n\treturn 0;\n}\n\nMsgId HistoryItem::topicRootId() const {\n\tif (const auto reply = Get<HistoryMessageReply>()\n\t\t; reply && reply->topicPost()) {\n\t\treturn reply->topMessageId();\n\t} else if (const auto data = GetServiceDependentData()\n\t\t; data && data->topicPost && data->topId) {\n\t\treturn data->topId;\n\t} else if (const auto info = Get<HistoryServiceTopicInfo>()) {\n\t\tif (info->created()) {\n\t\t\treturn id;\n\t\t}\n\t}\n\treturn Data::ForumTopic::kGeneralId;\n}\n\nFullStoryId HistoryItem::replyToStory() const {\n\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\tif (reply->storyId()) {\n\t\t\tconst auto peerId = reply->externalPeerId()\n\t\t\t\t? reply->externalPeerId()\n\t\t\t\t: _history->peer->id;\n\t\t\treturn { .peer = peerId, .story = reply->storyId() };\n\t\t}\n\t}\n\treturn {};\n}\n\nFullReplyTo HistoryItem::replyTo() const {\n\tconst auto monoforumPeerId = _history->peer->amMonoforumAdmin()\n\t\t? sublistPeerId()\n\t\t: PeerId();\n\tauto result = FullReplyTo{\n\t\t.topicRootId = topicRootId(),\n\t\t.monoforumPeerId = monoforumPeerId,\n\t};\n\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\tconst auto &fields = reply->fields();\n\t\tconst auto peer = fields.externalPeerId;\n\t\tconst auto replyToPeer = peer ? peer : _history->peer->id;\n\t\tif (const auto id = fields.messageId) {\n\t\t\tresult.messageId = { replyToPeer, id };\n\t\t\tresult.quote = fields.quote;\n\t\t\tresult.quoteOffset = fields.quoteOffset;\n\t\t}\n\t\tif (const auto id = fields.storyId) {\n\t\t\tresult.storyId = { replyToPeer, id };\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid HistoryItem::detectTextLinks(\n\t\tconst TextWithEntities &textWithEntities) {\n\tfor (const auto &entity : textWithEntities.entities) {\n\t\tauto type = entity.type();\n\t\tif (type == EntityType::Url\n\t\t\t|| type == EntityType::CustomUrl\n\t\t\t|| type == EntityType::Phone\n\t\t\t|| type == EntityType::BankCard\n\t\t\t|| type == EntityType::Email) {\n\t\t\t_flags |= MessageFlag::HasTextLinks;\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nvoid HistoryItem::setText(TextWithEntities textWithEntities) {\n\tdetectTextLinks(textWithEntities);\n\tsetTextValue((_media && _media->consumeMessageText(textWithEntities))\n\t\t? TextWithEntities()\n\t\t: std::move(textWithEntities));\n}\n\nvoid HistoryItem::setTextValue(TextWithEntities text, bool force) {\n\tif (const auto processId = Spellchecker::TryHighlightSyntax(text)) {\n\t\t_flags |= MessageFlag::InHighlightProcess;\n\t\thistory()->owner().registerHighlightProcess(processId, this);\n\t}\n\tconst auto had = !_text.empty();\n\t_text = std::move(text);\n\tRemoveComponents(HistoryMessageTranslation::Bit());\n\tif (had || force) {\n\t\thistory()->owner().requestItemTextRefresh(this);\n\t}\n}\n\nbool HistoryItem::inHighlightProcess() const {\n\treturn _flags & MessageFlag::InHighlightProcess;\n}\n\nvoid HistoryItem::highlightProcessDone() {\n\tExpects(inHighlightProcess());\n\n\t_flags &= ~MessageFlag::InHighlightProcess;\n\tif (!_text.empty()) {\n\t\tsetTextValue(base::take(_text), true);\n\t}\n}\n\nbool HistoryItem::showNotification() const {\n\tconst auto channel = _history->peer->asChannel();\n\tif (channel && !channel->amIn()) {\n\t\treturn false;\n\t}\n\treturn (out() || _history->peer->isSelf())\n\t\t? isFromScheduled()\n\t\t: unread(notificationThread());\n}\n\nvoid HistoryItem::markClientSideAsRead() {\n\t_flags &= ~MessageFlag::ClientSideUnread;\n}\n\nMessageGroupId HistoryItem::groupId() const {\n\treturn _groupId;\n}\n\nEffectId HistoryItem::effectId() const {\n\treturn _effectId;\n}\n\nQString HistoryItem::computeUnavailableReason() const {\n\tif (const auto restrictions = Get<HistoryMessageRestrictions>()) {\n\t\t_flags |= MessageFlag::HasRestrictions;\n\t\t_history->owner().registerRestricted(this, restrictions->reasons);\n\t\treturn Data::UnavailableReason::Compute(\n\t\t\t&history()->session(),\n\t\t\trestrictions->reasons);\n\t}\n\treturn QString();\n}\n\nbool HistoryItem::isMediaSensitive() const {\n\tif (!(_flags & MessageFlag::SensitiveContent)\n\t\t&& !_history->peer->hasSensitiveContent()) {\n\t\treturn false;\n\t}\n\t_flags |= MessageFlag::HasRestrictions;\n\t_history->owner().registerRestricted(this, u\"sensitive\"_q);\n\treturn !Data::UnavailableReason::IgnoreSensitiveMark(\n\t\t&_history->session());\n}\n\nbool HistoryItem::hasPossibleRestrictions() const {\n\treturn _flags & MessageFlag::HasRestrictions;\n}\n\nbool HistoryItem::isEmpty() const {\n\treturn _text.empty()\n\t\t&& !_media\n\t\t&& (!Has<HistoryMessageFactcheck>()\n\t\t\t|| Get<HistoryMessageFactcheck>()->data.text.empty())\n\t\t&& !Has<HistoryMessageLogEntryOriginal>();\n}\n\nData::SavedSublist *HistoryItem::savedSublist() const {\n\tif (isBusinessShortcut() || isScheduled()) {\n\t\treturn nullptr;\n\t} else if (const auto saved = Get<HistoryMessageSaved>()) {\n\t\tif (saved->savedMessagesSublist) {\n\t\t\treturn saved->savedMessagesSublist;\n\t\t} else if (const auto monoforum = _history->peer->monoforum()) {\n\t\t\tconst auto peer = _history->owner().peer(saved->sublistPeerId);\n\t\t\treturn monoforum->sublist(peer).get();\n\t\t}\n\t} else if (_history->peer->isSelf()) {\n\t\tconst auto sublist = _history->owner().savedMessages().sublist(\n\t\t\t_history->peer);\n\t\tconst auto that = const_cast<HistoryItem*>(this);\n\t\tthat->AddComponents(HistoryMessageSaved::Bit());\n\t\tconst auto saved = that->Get<HistoryMessageSaved>();\n\t\tsaved->sublistPeerId = _history->peer->id;\n\t\tsaved->savedMessagesSublist = sublist;\n\t\treturn sublist;\n\t} else if (const auto monoforum = _history->peer->monoforum()) {\n\t\tconst auto sublist = monoforum->sublist(_from);\n\t\tconst auto that = const_cast<HistoryItem*>(this);\n\t\tthat->AddComponents(HistoryMessageSaved::Bit());\n\t\tthat->Get<HistoryMessageSaved>()->sublistPeerId = _from->id;\n\t\treturn sublist;\n\t}\n\treturn nullptr;\n}\n\nPeerId HistoryItem::sublistPeerId() const {\n\tif (const auto saved = Get<HistoryMessageSaved>()) {\n\t\treturn saved->sublistPeerId;\n\t} else if (_history->peer->isSelf()) {\n\t\treturn _history->peer->id;\n\t} else if (_history->peer->monoforum()) {\n\t\treturn _from->id;\n\t}\n\treturn PeerId();\n}\n\nPeerData *HistoryItem::savedFromSender() const {\n\tif (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\treturn forwarded->savedFromSender;\n\t}\n\treturn nullptr;\n}\n\nconst HiddenSenderInfo *HistoryItem::savedFromHiddenSenderInfo() const {\n\tif (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\treturn forwarded->savedFromHiddenSenderInfo.get();\n\t}\n\treturn nullptr;\n}\n\nTextWithEntities HistoryItem::notificationText(\n\t\tNotificationTextOptions options) const {\n\tauto result = [&] {\n\t\tif (_media && !isService()) {\n\t\t\treturn _media->notificationText();\n\t\t} else if (!emptyText()) {\n\t\t\treturn _text;\n\t\t}\n\t\treturn TextWithEntities();\n\t}();\n\tif (options.spoilerLoginCode\n\t\t&& !out()\n\t\t&& (history()->peer->isNotificationsUser()\n\t\t\t|| history()->peer->isVerifyCodes())) {\n\t\tresult = SpoilerLoginCode(std::move(result));\n\t}\n\tif (result.text.size() <= kNotificationTextLimit) {\n\t\treturn result;\n\t}\n\treturn Ui::Text::Mid(result, 0, kNotificationTextLimit).append(\n\t\tUi::kQEllipsis);\n}\n\nItemPreview HistoryItem::toPreview(ToPreviewOptions options) const {\n\tif (isService()) {\n\t\tconst_cast<HistoryItem*>(this)->resolveDependent();\n\n\t\t// Don't show small media for service messages (chat photo changed).\n\t\t// Because larger version is shown exactly to the left of the small.\n\t\t//auto media = _media ? _media->toPreview(options) : ItemPreview();\n\t\treturn {\n\t\t\t.text = Ui::Text::Colorized(notificationText()),\n\t\t\t//.images = std::move(media.images),\n\t\t\t//.loadingContext = std::move(media.loadingContext),\n\t\t};\n\t}\n\n\tauto result = [&]() -> ItemPreview {\n\t\tif (_media) {\n\t\t\treturn _media->toPreview(options);\n\t\t} else if (!emptyText()) {\n\t\t\treturn {\n\t\t\t\t// wrap_rtl \"adds\" a newline in case text starts with quote.\n\t\t\t\t// So we remove those by DialogsPreviewText call.\n\t\t\t\t.text = st::wrap_rtl(Dialogs::Ui::DialogsPreviewText(\n\t\t\t\t\toptions.translated ? translatedText() : _text))\n\t\t\t};\n\t\t}\n\t\treturn {};\n\t}();\n\tif (options.spoilerLoginCode\n\t\t&& !out()\n\t\t&& (history()->peer->isNotificationsUser()\n\t\t\t|| history()->peer->isVerifyCodes())) {\n\t\tresult.text = SpoilerLoginCode(std::move(result.text));\n\t}\n\tconst auto fromSender = [](not_null<PeerData*> sender) {\n\t\treturn sender->isSelf()\n\t\t\t? tr::lng_from_you(tr::now)\n\t\t\t: sender->shortName();\n\t};\n\tconst auto forwarded = Get<HistoryMessageForwarded>();\n\tconst auto forwardFromSender = forwarded\n\t\t&& showForwardsFromSender(forwarded);\n\tresult.icon = (forwarded\n\t\t&& (!forwardFromSender || forwarded->forwardOfForward()))\n\t\t? ItemPreview::Icon::ForwardedMessage\n\t\t: replyToStory().valid()\n\t\t? ItemPreview::Icon::ReplyToStory\n\t\t: ItemPreview::Icon::None;\n\tconst auto fromForwarded = [&]() -> std::optional<QString> {\n\t\tif (forwarded) {\n\t\t\tconst auto sender = forwarded->forwardOfForward()\n\t\t\t\t? forwarded->savedFromSender\n\t\t\t\t: forwarded->originalSender;\n\t\t\treturn sender\n\t\t\t\t? fromSender(sender)\n\t\t\t\t: forwarded->savedFromHiddenSenderInfo\n\t\t\t\t? forwarded->savedFromHiddenSenderInfo->name\n\t\t\t\t: forwarded->originalHiddenSenderInfo->name;\n\t\t}\n\t\treturn {};\n\t};\n\tconst auto sender = [&]() -> std::optional<QString> {\n\t\tif (options.hideSender || isPostHidingAuthor() || isEmpty()) {\n\t\t\treturn {};\n\t\t} else if (!_history->peer->isUser()) {\n\t\t\tif (const auto from = displayFrom()) {\n\t\t\t\treturn fromSender(from);\n\t\t\t}\n\t\t\treturn fromForwarded();\n\t\t} else if (_history->peer->isSelf()\n\t\t\t|| _history->peer->isVerifyCodes()) {\n\t\t\treturn fromForwarded();\n\t\t}\n\t\treturn {};\n\t}();\n\tif (!sender) {\n\t\treturn result;\n\t}\n\tconst auto topic = options.ignoreTopic ? nullptr : this->topic();\n\treturn Dialogs::Ui::PreviewWithSender(\n\t\tstd::move(result),\n\t\t*sender,\n\t\ttopic ? topic->titleWithIcon() : TextWithEntities());\n}\n\nTextWithEntities HistoryItem::inReplyText() const {\n\tif (!isService()) {\n\t\treturn toPreview({\n\t\t\t.hideSender = true,\n\t\t\t.generateImages = false,\n\t\t\t.ignoreGroup = true,\n\t\t\t.translated = true,\n\t\t}).text;\n\t}\n\tauto result = notificationText();\n\tconst auto &name = author()->name();\n\tTextUtilities::Trim(result);\n\tif (result.text.startsWith(name)) {\n\t\tresult = Ui::Text::Mid(result, name.size());\n\t\tTextUtilities::Trim(result);\n\t}\n\treturn Ui::Text::Colorized(result);\n}\n\nconst std::vector<ClickHandlerPtr> &HistoryItem::customTextLinks() const {\n\tstatic const auto kEmpty = std::vector<ClickHandlerPtr>();\n\tconst auto service = Get<HistoryServiceData>();\n\treturn service ? service->textLinks : kEmpty;\n}\n\nvoid HistoryItem::createComponents(CreateConfig &&config) {\n\tuint64 mask = 0;\n\tif (config.reply.messageId\n\t\t|| config.reply.externalSenderId\n\t\t|| !config.reply.externalSenderName.isEmpty()\n\t\t|| config.reply.storyId) {\n\t\tmask |= HistoryMessageReply::Bit();\n\t}\n\tif (config.viaBotId) {\n\t\tmask |= HistoryMessageVia::Bit();\n\t}\n\tif (config.viewsCount >= 0 || !config.replies.isNull) {\n\t\tmask |= HistoryMessageViews::Bit();\n\t}\n\tif (!config.postAuthor.isEmpty() || config.viaBusinessBotId) {\n\t\tmask |= HistoryMessageSigned::Bit();\n\t} else if (_history->peer->isMegagroup() // Discussion posts signatures.\n\t\t&& config.savedFromPeer\n\t\t&& !config.originalPostAuthor.isEmpty()) {\n\t\tconst auto savedFrom = _history->owner().peerLoaded(\n\t\t\tconfig.savedFromPeer);\n\t\tif (savedFrom && savedFrom->isChannel()) {\n\t\t\tmask |= HistoryMessageSigned::Bit();\n\t\t}\n\t} else if (!config.originalPostAuthor.isEmpty()\n\t\t&& (_history->peer->isSelf()\n\t\t\t|| _history->peer->isRepliesChat()\n\t\t\t|| _history->peer->isVerifyCodes())) {\n\t\tmask |= HistoryMessageSigned::Bit();\n\t}\n\tif (config.editDate != TimeId(0)) {\n\t\tmask |= HistoryMessageEdited::Bit();\n\t}\n\tif (config.originalDate != 0) {\n\t\tmask |= HistoryMessageForwarded::Bit();\n\t}\n\tif (!config.markup.isTrivial()) {\n\t\tmask |= HistoryMessageReplyMarkup::Bit();\n\t} else if (config.inlineMarkup) {\n\t\tmask |= HistoryMessageReplyMarkup::Bit();\n\t}\n\tif (config.scheduleRepeatPeriod) {\n\t\tmask |= HistoryMessageSchedulePeriod::Bit();\n\t}\n\tconst auto requiresMonoforumPeer = _history->peer->amMonoforumAdmin();\n\tif (!isBusinessShortcut()\n\t\t&& !isScheduled()\n\t\t&& (_history->peer->isSelf()\n\t\t\t|| config.savedSublistPeer\n\t\t\t|| requiresMonoforumPeer)) {\n\t\tmask |= HistoryMessageSaved::Bit();\n\t}\n\tif (!config.restrictions.empty()) {\n\t\tif (config.restrictions.size() > 1\n\t\t\t|| !config.restrictions.front().sensitive()) {\n\t\t\tmask |= HistoryMessageRestrictions::Bit();\n\t\t}\n\t}\n\tif (config.suggest.exists) {\n\t\tmask |= HistoryMessageSuggestion::Bit();\n\t\tif (computeSuggestionActions(\n\t\t\tconfig.suggest.accepted,\n\t\t\tconfig.suggest.rejected,\n\t\t\tTimeId()\n\t\t) != SuggestionActions::None) {\n\t\t\tmask |= HistoryMessageReplyMarkup::Bit();\n\t\t}\n\t}\n\n\tUpdateComponents(mask);\n\n\tif (const auto saved = Get<HistoryMessageSaved>()) {\n\t\tif (!config.savedSublistPeer) {\n\t\t\tif (config.reply.monoforumPeerId) {\n\t\t\t\tconfig.savedSublistPeer = config.reply.monoforumPeerId;\n\t\t\t} else if (!_history->peer->isSelf()) {\n\t\t\t\tconfig.savedSublistPeer = _from->id;\n\t\t\t} else if (config.savedFromPeer) {\n\t\t\t\tconfig.savedSublistPeer = config.savedFromPeer;\n\t\t\t} else if (config.originalSenderId) {\n\t\t\t\tconfig.savedSublistPeer = config.originalSenderId;\n\t\t\t} else if (!config.originalSenderName.isEmpty()) {\n\t\t\t\tconfig.savedSublistPeer = PeerData::kSavedHiddenAuthorId;\n\t\t\t} else {\n\t\t\t\tconfig.savedSublistPeer = _history->session().userPeerId();\n\t\t\t}\n\t\t}\n\t\tsaved->sublistPeerId = config.savedSublistPeer;\n\t\tif (_history->peer->isSelf()) {\n\t\t\tsaved->savedMessagesSublist\n\t\t\t\t= _history->owner().savedMessages().sublist(\n\t\t\t\t\t_history->owner().peer(saved->sublistPeerId));\n\t\t}\n\t}\n\n\tif (const auto period = Get<HistoryMessageSchedulePeriod>()) {\n\t\tperiod->schedulePeriod = config.scheduleRepeatPeriod;\n\t}\n\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\treply->set(std::move(config.reply));\n\t\treply->updateData(this);\n\t}\n\tif (const auto via = Get<HistoryMessageVia>()) {\n\t\tvia->create(&_history->owner(), config.viaBotId);\n\t}\n\tif (Has<HistoryMessageViews>()) {\n\t\tchangeViewsCount(config.viewsCount);\n\t\tif (config.replies.isNull\n\t\t\t&& isSending()\n\t\t\t&& config.markup.isNull()) {\n\t\t\tif (const auto broadcast = _history->peer->asBroadcast()) {\n\t\t\t\tif (const auto link = broadcast->discussionLink()) {\n\t\t\t\t\tconfig.replies.isNull = false;\n\t\t\t\t\tconfig.replies.channelId = peerToChannel(link->id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tsetForwardsCount(config.forwardsCount);\n\t\tsetReplies(std::move(config.replies));\n\t}\n\tif (const auto edited = Get<HistoryMessageEdited>()) {\n\t\tedited->date = config.editDate;\n\t}\n\tif (const auto msgsigned = Get<HistoryMessageSigned>()) {\n\t\tif (config.viaBusinessBotId) {\n\t\t\tmsgsigned->viaBusinessBot = _history->owner().user(\n\t\t\t\tconfig.viaBusinessBotId);\n\t\t\tmsgsigned->author = msgsigned->viaBusinessBot->name();\n\t\t} else {\n\t\t\tmsgsigned->author = config.postAuthor.isEmpty()\n\t\t\t\t? config.originalPostAuthor\n\t\t\t\t: config.postAuthor;\n\t\t\tmsgsigned->isAnonymousRank = !isDiscussionPost()\n\t\t\t\t&& author()->isMegagroup();\n\t\t}\n\t}\n\tsetupForwardedComponent(config);\n\tif (const auto markup = Get<HistoryMessageReplyMarkup>()) {\n\t\tif (!config.markup.isTrivial()) {\n\t\t\tmarkup->updateData(std::move(config.markup));\n\t\t} else if (config.inlineMarkup) {\n\t\t\tmarkup->createForwarded(*config.inlineMarkup);\n\t\t}\n\t\tif (markup->data.flags & ReplyMarkupFlag::HasSwitchInlineButton) {\n\t\t\t_flags |= MessageFlag::HasSwitchInlineButton;\n\t\t}\n\t} else if (!config.markup.isNull()) {\n\t\t_flags |= MessageFlag::HasReplyMarkup;\n\t} else {\n\t\t_flags &= ~MessageFlag::HasReplyMarkup;\n\t}\n\tif (const auto restrictions = Get<HistoryMessageRestrictions>()) {\n\t\trestrictions->reasons = std::move(config.restrictions);\n\t\tconst auto i = ranges::find(\n\t\t\trestrictions->reasons,\n\t\t\ttrue,\n\t\t\t&Data::UnavailableReason::sensitive);\n\t\tif (i != end(restrictions->reasons)) {\n\t\t\trestrictions->reasons.erase(i);\n\t\t\tflagSensitiveContent();\n\t\t}\n\t} else if (!config.restrictions.empty()) {\n\t\tflagSensitiveContent();\n\t}\n\n\tif (const auto suggest = Get<HistoryMessageSuggestion>()) {\n\t\tsuggest->price = config.suggest.price;\n\t\tsuggest->date = config.suggest.date;\n\t\tsuggest->accepted = config.suggest.accepted;\n\t\tsuggest->rejected = config.suggest.rejected;\n\t\tupdateSuggestControls(suggest);\n\t}\n\n\tif (out() && isSending()) {\n\t\tif (const auto channel = _history->peer->asMegagroup()) {\n\t\t\t_boostsApplied = channel->mgInfo->boostsApplied;\n\t\t}\n\t}\n}\n\nvoid HistoryItem::flagSensitiveContent() {\n\t_flags |= MessageFlag::SensitiveContent;\n\t_history->session().api().sensitiveContent().preload();\n}\n\nbool HistoryItem::checkRepliesPts(\n\t\tconst HistoryMessageRepliesData &data) const {\n\tconst auto channel = _history->peer->asChannel();\n\tconst auto pts = channel\n\t\t? channel->pts()\n\t\t: _history->session().updates().pts();\n\treturn (data.pts >= pts);\n}\n\nvoid HistoryItem::setupForwardedComponent(const CreateConfig &config) {\n\tconst auto forwarded = Get<HistoryMessageForwarded>();\n\tif (!forwarded) {\n\t\treturn;\n\t}\n\tforwarded->originalDate = config.originalDate;\n\tconst auto originalSender = config.originalSenderId\n\t\t? config.originalSenderId\n\t\t: !config.originalSenderName.isEmpty()\n\t\t? PeerId()\n\t\t: from()->id;\n\tforwarded->originalSender = originalSender\n\t\t? _history->owner().peer(originalSender).get()\n\t\t: nullptr;\n\tif (!forwarded->originalSender) {\n\t\tforwarded->originalHiddenSenderInfo\n\t\t\t= std::make_unique<HiddenSenderInfo>(\n\t\t\t\tconfig.originalSenderName,\n\t\t\t\tconfig.imported);\n\t}\n\tforwarded->originalId = config.originalId;\n\tforwarded->originalPostAuthor = config.originalPostAuthor;\n\tforwarded->psaType = config.forwardPsaType;\n\tforwarded->savedFromPeer = _history->owner().peerLoaded(\n\t\tconfig.savedFromPeer);\n\tforwarded->savedFromMsgId = config.savedFromMsgId;\n\tforwarded->savedFromSender = _history->owner().peerLoaded(\n\t\tconfig.savedFromSenderId);\n\tforwarded->savedFromOutgoing = config.savedFromOutgoing;\n\tforwarded->savedFromDate = config.savedFromDate;\n\tif (!forwarded->savedFromSender\n\t\t&& !config.savedFromSenderName.isEmpty()) {\n\t\tforwarded->savedFromHiddenSenderInfo\n\t\t\t= std::make_unique<HiddenSenderInfo>(config.savedFromSenderName, false);\n\t}\n\tforwarded->imported = config.imported;\n}\n\nvoid HistoryItem::applyInitialEffectWatched() {\n\tif (!effectId()) {\n\t\treturn;\n\t} else if (out()) {\n\t\t// If this message came from the server, not generated on send.\n\t\t_flags |= MessageFlag::EffectWatched;\n\t} else if (_history->inboxReadTillId() && !unread(_history)) {\n\t\t_flags |= MessageFlag::EffectWatched;\n\t}\n}\n\nvoid HistoryItem::applyEffectWatchedOnUnreadKnown() {\n\tif (effectId() && !out() && !unread(_history)) {\n\t\t_flags |= MessageFlag::EffectWatched;\n\t}\n}\n\nbool HistoryItem::generateLocalEntitiesByReply() const {\n\tusing namespace HistoryView;\n\tif (!_media) {\n\t\treturn true;\n\t} else if (const auto document = _media->document()) {\n\t\treturn !DurationForTimestampLinks(document);\n\t} else if (const auto webpage = _media->webpage()) {\n\t\treturn (webpage->type != WebPageType::Video)\n\t\t\t&& !DurationForTimestampLinks(webpage);\n\t}\n\treturn true;\n}\n\nTextWithEntities HistoryItem::withLocalEntities(\n\t\tconst TextWithEntities &textWithEntities) const {\n\tusing namespace HistoryView;\n\tif (!generateLocalEntitiesByReply()) {\n\t\tif (!_media) {\n\t\t} else if (const auto document = _media->document()) {\n\t\t\tif (const auto duration = DurationForTimestampLinks(document)) {\n\t\t\t\treturn AddTimestampLinks(\n\t\t\t\t\ttextWithEntities,\n\t\t\t\t\tduration,\n\t\t\t\t\tTimestampLinkBase(document, fullId()));\n\t\t\t}\n\t\t} else if (const auto webpage = _media->webpage()) {\n\t\t\tif (const auto duration = DurationForTimestampLinks(webpage)) {\n\t\t\t\treturn AddTimestampLinks(\n\t\t\t\t\ttextWithEntities,\n\t\t\t\t\tduration,\n\t\t\t\t\tTimestampLinkBase(webpage, fullId()));\n\t\t\t}\n\t\t}\n\t\treturn textWithEntities;\n\t}\n\tif (const auto reply = Get<HistoryMessageReply>()) {\n\t\tconst auto document = reply->replyToDocumentId\n\t\t\t? _history->owner().document(reply->replyToDocumentId).get()\n\t\t\t: nullptr;\n\t\tconst auto webpage = reply->replyToWebPageId\n\t\t\t? _history->owner().webpage(reply->replyToWebPageId).get()\n\t\t\t: nullptr;\n\t\tif (document) {\n\t\t\tif (const auto duration = DurationForTimestampLinks(document)) {\n\t\t\t\tconst auto context = reply->resolvedMessage->fullId();\n\t\t\t\treturn AddTimestampLinks(\n\t\t\t\t\ttextWithEntities,\n\t\t\t\t\tduration,\n\t\t\t\t\tTimestampLinkBase(document, context));\n\t\t\t}\n\t\t} else if (webpage) {\n\t\t\tif (const auto duration = DurationForTimestampLinks(webpage)) {\n\t\t\t\tconst auto context = reply->resolvedMessage->fullId();\n\t\t\t\treturn AddTimestampLinks(\n\t\t\t\t\ttextWithEntities,\n\t\t\t\t\tduration,\n\t\t\t\t\tTimestampLinkBase(webpage, context));\n\t\t\t}\n\t\t}\n\t}\n\treturn textWithEntities;\n}\n\nvoid HistoryItem::createComponentsHelper(HistoryItemCommonFields &&fields) {\n\tconst auto &replyTo = fields.replyTo;\n\tauto config = CreateConfig();\n\tconfig.viaBotId = fields.viaBotId;\n\tconfig.scheduleRepeatPeriod = fields.scheduleRepeatPeriod;\n\tif (fields.flags & MessageFlag::HasReplyInfo) {\n\t\tconfig.reply.messageId = replyTo.messageId.msg;\n\t\tconfig.reply.storyId = replyTo.storyId.story;\n\t\tconfig.reply.externalPeerId = replyTo.storyId\n\t\t\t? replyTo.storyId.peer\n\t\t\t: (replyTo.messageId && replyTo.messageId.peer\n\t\t\t\t!= history()->peer->id)\n\t\t\t? replyTo.messageId.peer\n\t\t\t: PeerId();\n\t\tconst auto to = LookupReplyTo(_history, replyTo.messageId);\n\t\tconfig.reply.monoforumPeerId = (to && to->sublistPeerId())\n\t\t\t? to->sublistPeerId()\n\t\t\t: replyTo.monoforumPeerId\n\t\t\t? replyTo.monoforumPeerId\n\t\t\t: PeerId();\n\t\tconfig.reply.todoItemId = replyTo.todoItemId;\n\t\tconfig.reply.pollOption = replyTo.pollOption;\n\t\tconst auto replyToTop = replyTo.topicRootId\n\t\t\t? replyTo.topicRootId\n\t\t\t: LookupReplyToTop(_history, to);\n\t\tconfig.reply.topMessageId = replyToTop\n\t\t\t? replyToTop\n\t\t\t: (replyTo.messageId.peer == history()->peer->id)\n\t\t\t? replyTo.messageId.msg\n\t\t\t: MsgId();\n\t\tconst auto forum = _history->asForum();\n\t\tconst auto topic = forum\n\t\t\t? forum->topicFor(replyTo.topicRootId)\n\t\t\t: nullptr;\n\t\tif (!config.reply.externalPeerId\n\t\t\t&& topic\n\t\t\t&& to\n\t\t\t&& topic->rootId() != to->topicRootId()) {\n\t\t\tconfig.reply.externalPeerId = replyTo.messageId.peer;\n\t\t}\n\t\tconst auto topicPost = config.reply.externalPeerId\n\t\t\t? (replyTo.topicRootId\n\t\t\t\t&& (replyTo.topicRootId != Data::ForumTopic::kGeneralId))\n\t\t\t: (topic\n\t\t\t\t|| LookupReplyIsTopicPost(to)\n\t\t\t\t|| (to && to->Has<HistoryServiceTopicInfo>())\n\t\t\t\t|| (forum && forum->creating(config.reply.topMessageId)));\n\t\tconfig.reply.topicPost = topicPost ? 1 : 0;\n\t\tconfig.reply.manualQuote = replyTo.quote.empty() ? 0 : 1;\n\t\tconfig.reply.quoteOffset = replyTo.quoteOffset;\n\t\tconfig.reply.quote = std::move(replyTo.quote);\n\t}\n\tconfig.markup = std::move(fields.markup);\n\tif (fields.flags & MessageFlag::HasPostAuthor) {\n\t\tconfig.postAuthor = fields.postAuthor;\n\t}\n\tif (fields.flags & MessageFlag::HasViews) {\n\t\tconfig.viewsCount = 1;\n\t}\n\tif (fields.suggest.exists) {\n\t\tconfig.suggest = fields.suggest;\n\t}\n\n\tcreateComponents(std::move(config));\n}\n\nvoid HistoryItem::setReactions(const MTPMessageReactions *reactions) {\n\tExpects(!_reactions);\n\n\tif (changeReactions(reactions) && _reactions->hasUnread()) {\n\t\t_flags |= MessageFlag::HasUnreadReaction;\n\t}\n}\n\nvoid HistoryItem::updateReactions(const MTPMessageReactions *reactions) {\n\tconst auto wasRecentUsers = LookupRecentUnreadReactedUsers(this);\n\tconst auto hadUnread = hasUnreadReaction();\n\tconst auto changed = changeReactions(reactions);\n\tif (!changed) {\n\t\treturn;\n\t}\n\tconst auto hasUnread = _reactions && _reactions->hasUnread();\n\tif (hasUnread && !hadUnread) {\n\t\t_flags |= MessageFlag::HasUnreadReaction;\n\n\t\taddToUnreadThings(HistoryUnreadThings::AddType::New);\n\t} else if (!hasUnread && hadUnread) {\n\t\tmarkReactionsRead();\n\t}\n\tCheckReactionNotificationSchedule(this, wasRecentUsers);\n\t_history->owner().notifyItemDataChange(this);\n}\n\nbool HistoryItem::changeReactions(const MTPMessageReactions *reactions) {\n\tif (reactions || _reactionsLastRefreshed) {\n\t\t_reactionsLastRefreshed = crl::now();\n\t}\n\tconst auto changeToEmpty = [&] {\n\t\tif (!_reactions) {\n\t\t\treturn false;\n\t\t} else if (!_reactions->localPaidData()) {\n\t\t\t_reactions = nullptr;\n\t\t\treturn true;\n\t\t}\n\t\treturn _reactions->clearCloudData();\n\t};\n\tif (!reactions) {\n\t\t_flags &= ~MessageFlag::CanViewReactions;\n\t\tif (_history->peer->isSelf()) {\n\t\t\t_flags |= MessageFlag::ReactionsAreTags;\n\t\t}\n\t\treturn changeToEmpty();\n\t}\n\tconst auto &data = reactions->data();\n\tconst auto empty = data.vresults().v.isEmpty();\n\tif (data.is_reactions_as_tags()\n\t\t|| (empty && _history->peer->isSelf())) {\n\t\t_flags |= MessageFlag::ReactionsAreTags;\n\t} else {\n\t\t_flags &= ~MessageFlag::ReactionsAreTags;\n\t}\n\tif (data.is_can_see_list()) {\n\t\t_flags |= MessageFlag::CanViewReactions;\n\t} else {\n\t\t_flags &= ~MessageFlag::CanViewReactions;\n\t}\n\tif (empty) {\n\t\treturn changeToEmpty();\n\t} else if (!_reactions) {\n\t\t_reactions = std::make_unique<Data::MessageReactions>(this);\n\t}\n\tconst auto min = data.is_min();\n\tconst auto &list = data.vresults().v;\n\tconst auto &recent = data.vrecent_reactions().value_or_empty();\n\tconst auto &top = data.vtop_reactors().value_or_empty();\n\tif (min && hasUnreadReaction()) {\n\t\t// We can't update reactions from min if we have unread.\n\t\tif (_reactions->checkIfChanged(list, recent, min)) {\n\t\t\tupdateReactionsUnknown();\n\t\t}\n\t\treturn false;\n\t}\n\treturn _reactions->change(list, recent, top, min);\n}\n\nvoid HistoryItem::applyTTL(const MTPDmessage &data) {\n\tif (const auto period = data.vttl_period()) {\n\t\tif (period->v > 0) {\n\t\t\tapplyTTL(data.vdate().v + period->v);\n\t\t}\n\t}\n}\n\nvoid HistoryItem::applyTTL(const MTPDmessageService &data) {\n\tif (const auto period = data.vttl_period()) {\n\t\tif (period->v > 0) {\n\t\t\tapplyTTL(data.vdate().v + period->v);\n\t\t}\n\t}\n}\n\nvoid HistoryItem::createComponents(const MTPDmessage &data) {\n\tauto config = CreateConfig();\n\tconfig.savedSublistPeer = data.vsaved_peer_id()\n\t\t? peerFromMTP(*data.vsaved_peer_id())\n\t\t: PeerId();\n\tif (const auto forwarded = data.vfwd_from()) {\n\t\tforwarded->match([&](const MTPDmessageFwdHeader &data) {\n\t\t\tFillForwardedInfo(config, data);\n\t\t});\n\t}\n\tif (const auto reply = data.vreply_to()) {\n\t\tconfig.reply = ReplyFieldsFromMTP(this, *reply);\n\t}\n\tconfig.viaBotId = data.vvia_bot_id().value_or_empty();\n\tconfig.viaBusinessBotId = data.vvia_business_bot_id().value_or_empty();\n\tconfig.viewsCount = data.vviews().value_or(-1);\n\tconfig.forwardsCount = data.vforwards().value_or(-1);\n\tconfig.replies = isScheduled()\n\t\t? HistoryMessageRepliesData()\n\t\t: HistoryMessageRepliesData(data.vreplies());\n\tconfig.markup = HistoryMessageMarkupData(data.vreply_markup());\n\tconfig.editDate = data.vedit_date().value_or_empty();\n\tconfig.scheduleRepeatPeriod\n\t\t= data.vschedule_repeat_period().value_or_empty();\n\tconfig.postAuthor = qs(data.vpost_author().value_or_empty());\n\tconfig.restrictions = Data::UnavailableReason::Extract(\n\t\tdata.vrestriction_reason());\n\tconfig.suggest = HistoryMessageSuggestInfo(data.vsuggested_post());\n\tcreateComponents(std::move(config));\n}\n\nvoid HistoryItem::refreshMedia(const MTPMessageMedia *media) {\n\tconst auto was = (_media != nullptr);\n\tif (const auto invoice = was ? _media->invoice() : nullptr) {\n\t\tif (HasExtendedMedia(*invoice)) {\n\t\t\treturn;\n\t\t}\n\t}\n\t_media = nullptr;\n\tif (media) {\n\t\tsetMedia(*media);\n\t}\n\tif (was || _media) {\n\t\tif (const auto views = Get<HistoryMessageViews>()) {\n\t\t\trefreshRepliesText(views);\n\t\t}\n\t}\n}\n\nvoid HistoryItem::refreshSentMedia(const MTPMessageMedia *media) {\n\tconst auto wasGrouped = history()->owner().groups().isGrouped(this);\n\trefreshMedia(media);\n\tif (wasGrouped) {\n\t\thistory()->owner().groups().refreshMessage(this);\n\t} else {\n\t\thistory()->owner().requestItemViewRefresh(this);\n\t}\n}\n\nPreparedServiceText HistoryItem::prepareServiceTextForMessage(\n\t\tconst MTPMessageMedia &media,\n\t\tbool unread) {\n\treturn media.match([&](const MTPDmessageMediaStory &data) {\n\t\treturn prepareStoryMentionText();\n\t}, [&](const MTPDmessageMediaPhoto &data) -> PreparedServiceText {\n\t\tif (unread) {\n\t\t\tconst auto ttl = data.vttl_seconds();\n\t\t\tAssert(ttl != nullptr);\n\n\t\t\tif (out()) {\n\t\t\t\treturn {\n\t\t\t\t\ttr::lng_ttl_photo_sent(tr::now, tr::marked)\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\tauto result = PreparedServiceText();\n\t\t\t\tresult.links.push_back(fromLink());\n\t\t\t\tresult.text = tr::lng_ttl_photo_received(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\t\ttr::marked);\n\t\t\t\treturn result;\n\t\t\t}\n\t\t} else {\n\t\t\treturn {\n\t\t\t\ttr::lng_ttl_photo_expired(tr::now, tr::marked)\n\t\t\t};\n\t\t}\n\t}, [&](const MTPDmessageMediaDocument &data) -> PreparedServiceText {\n\t\tif (unread) {\n\t\t\tconst auto ttl = data.vttl_seconds();\n\t\t\tAssert(ttl != nullptr);\n\n\t\t\tif (data.is_video()) {\n\t\t\t\tif (out()) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\ttr::lng_ttl_video_sent(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t};\n\t\t\t\t} else {\n\t\t\t\t\tauto result = PreparedServiceText();\n\t\t\t\t\tresult.links.push_back(fromLink());\n\t\t\t\t\tresult.text = tr::lng_ttl_video_received(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t\treturn result;\n\t\t\t\t}\n\t\t\t} else if (out()) {\n\t\t\t\tauto text = (data.is_voice()\n\t\t\t\t\t? tr::lng_ttl_voice_sent\n\t\t\t\t\t: data.is_round()\n\t\t\t\t\t? tr::lng_ttl_round_sent\n\t\t\t\t\t: tr::lng_message_empty)(tr::now, tr::marked);\n\t\t\t\treturn { std::move(text) };\n\t\t\t}\n\t\t\treturn {};\n\t\t} else {\n\t\t\tauto text = (data.is_video()\n\t\t\t\t? tr::lng_ttl_video_expired\n\t\t\t\t: data.is_voice()\n\t\t\t\t? tr::lng_ttl_voice_expired\n\t\t\t\t: data.is_round()\n\t\t\t\t? tr::lng_ttl_round_expired\n\t\t\t\t: tr::lng_message_empty)(tr::now, tr::marked);\n\t\t\treturn { std::move(text) };\n\t\t}\n\t}, [](const auto &) {\n\t\treturn PreparedServiceText();\n\t});\n}\n\nvoid HistoryItem::createServiceFromMtp(const MTPDmessage &message) {\n\tAddComponents(HistoryServiceData::Bit());\n\n\t_flags |= MessageFlag::ReactionsAllowed;\n\n\tconst auto unread = message.is_media_unread();\n\tconst auto media = message.vmedia();\n\tAssert(media != nullptr);\n\n\tmedia->match([&](const MTPDmessageMediaPhoto &data) {\n\t\tif (unread) {\n\t\t\tconst auto ttl = data.vttl_seconds();\n\t\t\tAssert(ttl != nullptr);\n\n\t\t\tsetSelfDestruct(HistoryServiceSelfDestruct::Type::Photo, *ttl);\n\t\t}\n\t}, [&](const MTPDmessageMediaDocument &data) {\n\t\tif (unread) {\n\t\t\tconst auto ttl = data.vttl_seconds();\n\t\t\tAssert(ttl != nullptr);\n\n\t\t\tif (data.is_video()) {\n\t\t\t\tsetSelfDestruct(\n\t\t\t\t\tHistoryServiceSelfDestruct::Type::Video,\n\t\t\t\t\t*ttl);\n\t\t\t}\n\t\t}\n\t}, [](const auto &) {});\n\n\tsetServiceText(prepareServiceTextForMessage(*media, unread));\n}\n\nvoid HistoryItem::createServiceFromMtp(const MTPDmessageService &message) {\n\tAddComponents(HistoryServiceData::Bit());\n\n\tconst auto &action = message.vaction();\n\tconst auto type = action.type();\n\tif (type == mtpc_messageActionPinMessage) {\n\t\tUpdateComponents(HistoryServicePinned::Bit());\n\t} else if (type == mtpc_messageActionHistoryClear) {\n\t\tUpdateComponents(HistoryServiceClearHistory::Bit());\n\t} else if (type == mtpc_messageActionTopicCreate\n\t\t|| type == mtpc_messageActionTopicEdit) {\n\t\tUpdateComponents(HistoryServiceTopicInfo::Bit());\n\t\tconst auto info = Get<HistoryServiceTopicInfo>();\n\t\tinfo->topicPost = true;\n\t\tif (type == mtpc_messageActionTopicEdit) {\n\t\t\tconst auto &data = action.c_messageActionTopicEdit();\n\t\t\tif (const auto title = data.vtitle()) {\n\t\t\t\tinfo->title = qs(*title);\n\t\t\t\tinfo->renamed = true;\n\t\t\t}\n\t\t\tif (const auto icon = data.vicon_emoji_id()) {\n\t\t\t\tinfo->iconId = icon->v;\n\t\t\t\tinfo->reiconed = true;\n\t\t\t}\n\t\t\tif (const auto closed = data.vclosed()) {\n\t\t\t\tinfo->closed = mtpIsTrue(*closed);\n\t\t\t\tinfo->reopened = !info->closed;\n\t\t\t}\n\t\t\tif (const auto hidden = data.vhidden()) {\n\t\t\t\tinfo->hidden = mtpIsTrue(*hidden);\n\t\t\t\tinfo->unhidden = !info->hidden;\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto &data = action.c_messageActionTopicCreate();\n\t\t\tinfo->title = qs(data.vtitle());\n\t\t\tinfo->iconId = data.vicon_emoji_id().value_or_empty();\n\t\t}\n\t} else if (type == mtpc_messageActionSetChatTheme) {\n\t\tsetupChatThemeChange();\n\t} else if (type == mtpc_messageActionSetMessagesTTL) {\n\t\tsetupTTLChange();\n\t} else if (type == mtpc_messageActionGameScore) {\n\t\tconst auto &data = action.c_messageActionGameScore();\n\t\tUpdateComponents(HistoryServiceGameScore::Bit());\n\t\tGet<HistoryServiceGameScore>()->score = data.vscore().v;\n\t} else if (type == mtpc_messageActionPaymentSent) {\n\t\tconst auto &data = action.c_messageActionPaymentSent();\n\t\tUpdateComponents(HistoryServicePayment::Bit());\n\t\tconst auto amount = data.vtotal_amount().v;\n\t\tconst auto currency = qs(data.vcurrency());\n\t\tconst auto payment = Get<HistoryServicePayment>();\n\t\tconst auto id = fullId();\n\t\tconst auto owner = &_history->owner();\n\t\tpayment->slug = data.vinvoice_slug().value_or_empty();\n\t\tpayment->recurringInit = data.is_recurring_init();\n\t\tpayment->recurringUsed = data.is_recurring_used();\n\t\tpayment->isCreditsCurrency = (currency == Ui::kCreditsCurrency);\n\t\tpayment->amount = AmountAndStarCurrency(amount, currency);\n\t\tpayment->invoiceLink = std::make_shared<LambdaClickHandler>([=](\n\t\t\t\tClickContext context) {\n\t\t\tusing namespace Payments;\n\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\tconst auto weak = my.sessionWindow;\n\t\t\tif (const auto item = owner->message(id)) {\n\t\t\t\tCheckoutProcess::Start(\n\t\t\t\t\titem,\n\t\t\t\t\tMode::Receipt,\n\t\t\t\t\tcrl::guard(weak, [=](auto) { weak->window().activate(); }),\n\t\t\t\t\tPayments::ProcessNonPanelPaymentFormFactory(\n\t\t\t\t\t\tweak.get(),\n\t\t\t\t\t\titem));\n\t\t\t}\n\t\t});\n\t} else if (type == mtpc_messageActionGroupCall\n\t\t|| type == mtpc_messageActionGroupCallScheduled) {\n\t\tconst auto started = (type == mtpc_messageActionGroupCall);\n\t\tconst auto &callData = started\n\t\t\t? action.c_messageActionGroupCall().vcall()\n\t\t\t: action.c_messageActionGroupCallScheduled().vcall();\n\t\tconst auto duration = started\n\t\t\t? action.c_messageActionGroupCall().vduration()\n\t\t\t: tl::conditional<MTPint>();\n\t\tif (duration) {\n\t\t\tRemoveComponents(HistoryServiceOngoingCall::Bit());\n\t\t} else {\n\t\t\tUpdateComponents(HistoryServiceOngoingCall::Bit());\n\t\t\tconst auto call = Get<HistoryServiceOngoingCall>();\n\t\t\tcall->id = CallIdFromInput(callData);\n\t\t\tcall->link = GroupCallClickHandler(_history->peer, call->id);\n\t\t}\n\t} else if (type == mtpc_messageActionInviteToGroupCall) {\n\t\tconst auto &data = action.c_messageActionInviteToGroupCall();\n\t\tconst auto id = CallIdFromInput(data.vcall());\n\t\tconst auto peer = _history->peer;\n\t\tconst auto has = PeerHasThisCall(peer, id);\n\t\tauto hasLink = !has.has_value()\n\t\t\t? PeerHasThisCallValue(peer, id)\n\t\t\t: (*has)\n\t\t\t? PeerHasThisCallValue(\n\t\t\t\tpeer,\n\t\t\t\tid) | rpl::skip(1) | rpl::type_erased\n\t\t\t: rpl::producer<bool>();\n\t\tif (!hasLink) {\n\t\t\tRemoveComponents(HistoryServiceOngoingCall::Bit());\n\t\t} else {\n\t\t\tUpdateComponents(HistoryServiceOngoingCall::Bit());\n\t\t\tconst auto call = Get<HistoryServiceOngoingCall>();\n\t\t\tcall->id = id;\n\t\t\tcall->lifetime.destroy();\n\n\t\t\tconst auto users = data.vusers().v;\n\t\t\tstd::move(hasLink) | rpl::on_next([=](bool has) {\n\t\t\t\tupdateServiceText(\n\t\t\t\t\tprepareInvitedToCallText(\n\t\t\t\t\t\tParseInvitedToCallUsers(this, users),\n\t\t\t\t\t\thas ? id : 0));\n\t\t\t\tif (!has) {\n\t\t\t\t\tRemoveComponents(HistoryServiceOngoingCall::Bit());\n\t\t\t\t}\n\t\t\t}, call->lifetime);\n\t\t}\n\t} else if (type == mtpc_messageActionSetChatWallPaper) {\n\t\tif (action.c_messageActionSetChatWallPaper().is_same()) {\n\t\t\tUpdateComponents(HistoryServiceSameBackground::Bit());\n\t\t} else {\n\t\t\tRemoveComponents(HistoryServiceSameBackground::Bit());\n\t\t}\n\t} else if (type == mtpc_messageActionGiveawayResults) {\n\t\tUpdateComponents(HistoryServiceGiveawayResults::Bit());\n\t} else if (type == mtpc_messageActionPaymentRefunded) {\n\t\tconst auto &data = action.c_messageActionPaymentRefunded();\n\t\tUpdateComponents(HistoryServicePaymentRefund::Bit());\n\t\tconst auto refund = Get<HistoryServicePaymentRefund>();\n\t\trefund->peer = _history->owner().peer(peerFromMTP(data.vpeer()));\n\t\trefund->amount = data.vtotal_amount().v;\n\t\trefund->currency = qs(data.vcurrency());\n\t\trefund->transactionId = qs(data.vcharge().data().vid());\n\t\tconst auto id = fullId();\n\t\trefund->link = std::make_shared<LambdaClickHandler>([=](\n\t\t\tClickContext context) {\n\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\tif (const auto window = my.sessionWindow.get()) {\n\t\t\t\tSettings::ShowRefundInfoBox(window, id);\n\t\t\t}\n\t\t});\n\t} else if (type == mtpc_messageActionTodoCompletions) {\n\t\tconst auto &data = action.c_messageActionTodoCompletions();\n\t\tUpdateComponents(HistoryServiceTodoCompletions::Bit());\n\t\tconst auto done = Get<HistoryServiceTodoCompletions>();\n\t\tdone->completed = data.vcompleted().v\n\t\t\t| ranges::views::transform(&MTPint::v)\n\t\t\t| ranges::to_vector;\n\t\tdone->incompleted = data.vincompleted().v\n\t\t\t| ranges::views::transform(&MTPint::v)\n\t\t\t| ranges::to_vector;\n\t} else if (type == mtpc_messageActionTodoAppendTasks) {\n\t\tconst auto session = &_history->session();\n\t\tconst auto &data = action.c_messageActionTodoAppendTasks();\n\t\tUpdateComponents(HistoryServiceTodoAppendTasks::Bit());\n\t\tconst auto append = Get<HistoryServiceTodoAppendTasks>();\n\t\tappend->list = ranges::views::all(\n\t\t\tdata.vlist().v\n\t\t) | ranges::views::transform([&](const MTPTodoItem &item) {\n\t\t\treturn TodoListItemFromMTP(session, item);\n\t\t}) | ranges::to_vector;\n\t} else if (type == mtpc_messageActionPollAppendAnswer) {\n\t\tconst auto &data = action.c_messageActionPollAppendAnswer();\n\t\tUpdateComponents(HistoryServicePollAppendAnswer::Bit());\n\t\tconst auto append = Get<HistoryServicePollAppendAnswer>();\n\t\tdata.vanswer().match([&](const MTPDpollAnswer &answer) {\n\t\t\tappend->answer.option = answer.voption().v;\n\t\t\tappend->answer.text = Api::ParseTextWithEntities(\n\t\t\t\t&_history->session(),\n\t\t\t\tanswer.vtext());\n\t\t}, [](const auto &) {\n\t\t});\n\t} else if (type == mtpc_messageActionPollDeleteAnswer) {\n\t\tconst auto &data = action.c_messageActionPollDeleteAnswer();\n\t\tUpdateComponents(HistoryServicePollDeleteAnswer::Bit());\n\t\tconst auto del = Get<HistoryServicePollDeleteAnswer>();\n\t\tdata.vanswer().match([&](const MTPDpollAnswer &answer) {\n\t\t\tdel->answer.option = answer.voption().v;\n\t\t\tdel->answer.text = Api::ParseTextWithEntities(\n\t\t\t\t&_history->session(),\n\t\t\t\tanswer.vtext());\n\t\t}, [](const auto &) {\n\t\t});\n\t} else if (type == mtpc_messageActionSuggestedPostApproval) {\n\t\tconst auto &data = action.c_messageActionSuggestedPostApproval();\n\t\tUpdateComponents(HistoryServiceSuggestDecision::Bit());\n\t\tconst auto decision = Get<HistoryServiceSuggestDecision>();\n\t\tdecision->price = CreditsAmountFromTL(data.vprice());\n\t\tdecision->balanceTooLow = data.is_balance_too_low();\n\t\tdecision->rejected = data.is_rejected();\n\t\tdecision->rejectComment = qs(data.vreject_comment().value_or_empty());\n\t\tdecision->date = data.vschedule_date().value_or_empty();\n\t} else if (type == mtpc_messageActionSuggestedPostSuccess) {\n\t\tconst auto &data = action.c_messageActionSuggestedPostSuccess();\n\t\tUpdateComponents(HistoryServiceSuggestFinish::Bit());\n\t\tconst auto finish = Get<HistoryServiceSuggestFinish>();\n\t\tfinish->price = CreditsAmountFromTL(data.vprice());\n\t} else if (type == mtpc_messageActionSuggestedPostRefund) {\n\t\tconst auto &data = action.c_messageActionSuggestedPostRefund();\n\t\tUpdateComponents(HistoryServiceSuggestFinish::Bit());\n\t\tconst auto finish = Get<HistoryServiceSuggestFinish>();\n\t\tfinish->refundType = data.is_payer_initiated()\n\t\t\t? SuggestRefundType::User\n\t\t\t: SuggestRefundType::Admin;\n\t} else if (type == mtpc_messageActionStarGiftPurchaseOffer) {\n\t\tconst auto &data = action.c_messageActionStarGiftPurchaseOffer();\n\t\tconst auto accepted = data.is_accepted();\n\t\tconst auto rejected = data.is_declined();\n\t\tconst auto expiresAt = data.vexpires_at().v;\n\t\tconst auto gift = Api::FromTL(&history()->session(), data.vgift());\n\t\tif (const auto unique = gift ? gift->unique : nullptr) {\n\t\t\tauto mask = HistoryMessageSuggestion::Bit();\n\t\t\tconst auto actions = computeSuggestionActions(\n\t\t\t\taccepted,\n\t\t\t\trejected,\n\t\t\t\texpiresAt);\n\t\t\tif (actions != SuggestionActions::None) {\n\t\t\t\tmask |= HistoryMessageReplyMarkup::Bit();\n\t\t\t}\n\t\t\tUpdateComponents(mask);\n\n\t\t\tconst auto suggestion = Get<HistoryMessageSuggestion>();\n\t\t\tsuggestion->price = CreditsAmountFromTL(data.vprice());\n\t\t\tsuggestion->date = expiresAt;\n\t\t\tsuggestion->accepted = accepted;\n\t\t\tsuggestion->rejected = rejected;\n\t\t\tsuggestion->gift = unique;\n\n\t\t\tif (actions != SuggestionActions::None) {\n\t\t\t\tconst auto markup = Get<HistoryMessageReplyMarkup>();\n\t\t\t\tmarkup->updateSuggestControls(actions);\n\t\t\t}\n\t\t}\n\t} else if (type == mtpc_messageActionStarGiftPurchaseOfferDeclined) {\n\t\tconst auto &data = action.c_messageActionStarGiftPurchaseOfferDeclined();\n\t\tUpdateComponents(HistoryServiceSuggestFinish::Bit());\n\t\tconst auto finish = Get<HistoryServiceSuggestFinish>();\n\t\tfinish->refundType = data.is_expired()\n\t\t\t? SuggestRefundType::Expired\n\t\t\t: SuggestRefundType::User;\n\t\tfinish->price = CreditsAmountFromTL(data.vprice());\n\t} else if (type == mtpc_messageActionNoForwardsToggle) {\n\t\tAddComponents(HistoryServiceNoForwardsToggle::Bit());\n\t} else if (type == mtpc_messageActionNoForwardsRequest) {\n\t\tconst auto &data = action.c_messageActionNoForwardsRequest();\n\t\tconst auto expired = data.is_expired();\n\t\tconst auto period = _history->session().appConfig()\n\t\t\t.noForwardsRequestExpirePeriod();\n\t\tconst auto expiresAt = TimeId(_date + period);\n\t\tconst auto isExpired = expired\n\t\t\t|| (expiresAt <= base::unixtime::now());\n\t\tauto mask = HistoryServiceNoForwardsRequest::Bit();\n\t\tconst auto actions = (!isExpired && !out())\n\t\t\t? SuggestionActions::NoForwardsRequest\n\t\t\t: SuggestionActions::None;\n\t\tif (actions != SuggestionActions::None) {\n\t\t\tmask |= HistoryMessageReplyMarkup::Bit();\n\t\t}\n\t\tUpdateComponents(mask);\n\t\tconst auto request = Get<HistoryServiceNoForwardsRequest>();\n\t\trequest->expired = isExpired;\n\t\trequest->expiresAt = expiresAt;\n\t\tif (actions != SuggestionActions::None) {\n\t\t\tconst auto markup = Get<HistoryMessageReplyMarkup>();\n\t\t\tmarkup->updateSuggestControls(actions);\n\t\t}\n\t}\n\tif (const auto replyTo = message.vreply_to()) {\n\t\treplyTo->match([&](const MTPDmessageReplyHeader &data) {\n\t\t\tconst auto peerId = data.vreply_to_peer_id()\n\t\t\t\t? peerFromMTP(*data.vreply_to_peer_id())\n\t\t\t\t: _history->peer->id;\n\t\t\tif (const auto dependent = GetServiceDependentData()) {\n\t\t\t\tconst auto id = data.vreply_to_msg_id().value_or_empty();\n\t\t\t\tif (id) {\n\t\t\t\t\tdependent->peerId = (peerId != _history->peer->id)\n\t\t\t\t\t\t? peerId\n\t\t\t\t\t\t: 0;\n\t\t\t\t\tdependent->msgId = id;\n\t\t\t\t\tdependent->topId = data.vreply_to_top_id().value_or(id);\n\t\t\t\t\tdependent->topicPost = data.is_forum_topic()\n\t\t\t\t\t\t|| Has<HistoryServiceTopicInfo>();\n\t\t\t\t\tupdateServiceDependent();\n\t\t\t\t}\n\t\t\t}\n\t\t}, [](const MTPDmessageReplyStoryHeader &data) {\n\t\t});\n\t}\n\n\tconst auto savedSublistPeer = message.vsaved_peer_id()\n\t\t? peerFromMTP(*message.vsaved_peer_id())\n\t\t: PeerId();\n\tconst auto requiresMonoforumPeer = _history->peer->amMonoforumAdmin();\n\tif (!isBusinessShortcut()\n\t\t&& (savedSublistPeer || requiresMonoforumPeer)) {\n\t\tAddComponents(HistoryMessageSaved::Bit());\n\t\tconst auto saved = Get<HistoryMessageSaved>();\n\t\tsaved->sublistPeerId = savedSublistPeer\n\t\t\t? savedSublistPeer\n\t\t\t: _from->id;\n\t\tif (_history->peer->isSelf()) {\n\t\t\tsaved->savedMessagesSublist\n\t\t\t\t= _history->owner().savedMessages().sublist(\n\t\t\t\t\t_history->owner().peer(saved->sublistPeerId));\n\t\t}\n\t}\n\n\tsetServiceMessageByAction(action);\n}\n\nvoid HistoryItem::setMedia(const MTPMessageMedia &media) {\n\t_media = CreateMedia(this, media);\n\tcheckStoryForwardInfo();\n\tcheckBuyButton();\n}\n\nvoid HistoryItem::checkStoryForwardInfo() {\n\tif (const auto storyId = _media ? _media->storyId() : FullStoryId()) {\n\t\tconst auto adding = !Has<HistoryMessageForwarded>();\n\t\tif (adding) {\n\t\t\tAddComponents(HistoryMessageForwarded::Bit());\n\t\t}\n\t\tconst auto forwarded = Get<HistoryMessageForwarded>();\n\t\tif (forwarded->story || adding) {\n\t\t\tconst auto peer = history()->owner().peer(storyId.peer);\n\t\t\tforwarded->story = true;\n\t\t\tforwarded->originalSender = peer;\n\t\t}\n\t} else if (const auto forwarded = Get<HistoryMessageForwarded>()) {\n\t\tif (forwarded->story) {\n\t\t\tRemoveComponents(HistoryMessageForwarded::Bit());\n\t\t}\n\t}\n}\n\nvoid HistoryItem::applyServiceDateEdition(const MTPDmessageService &data) {\n\tconst auto date = data.vdate().v;\n\tif (_date == date) {\n\t\treturn;\n\t}\n\t_date = date;\n}\n\nvoid HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {\n\tauto prepareChatAddUserText = [this](const MTPDmessageActionChatAddUser &action) {\n\t\tauto result = PreparedServiceText();\n\t\tauto &users = action.vusers().v;\n\t\tif (users.size() == 1) {\n\t\t\tauto u = _history->owner().user(users[0].v);\n\t\t\tif (u == _from) {\n\t\t\t\tresult.links.push_back(fromLink());\n\t\t\t\tresult.text = tr::lng_action_user_joined(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\t\ttr::marked);\n\t\t\t} else {\n\t\t\t\tresult.links.push_back(fromLink());\n\t\t\t\tresult.links.push_back(u->createOpenLink());\n\t\t\t\tresult.text = tr::lng_action_add_user(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::link(u->name(), 2), // Link 2.\n\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t} else if (users.isEmpty()) {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tresult.text = tr::lng_action_add_user(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\tlt_user,\n\t\t\t\t{ .text = u\"somebody\"_q },\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tfor (auto i = 0, l = int(users.size()); i != l; ++i) {\n\t\t\t\tauto user = _history->owner().user(users[i].v);\n\t\t\t\tresult.links.push_back(user->createOpenLink());\n\n\t\t\t\tauto linkText = tr::link(user->name(), 2 + i);\n\t\t\t\tif (i == 0) {\n\t\t\t\t\tresult.text = linkText;\n\t\t\t\t} else if (i + 1 == l) {\n\t\t\t\t\tresult.text = tr::lng_action_add_users_and_last(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_accumulated,\n\t\t\t\t\t\tresult.text,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\tlinkText,\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t} else {\n\t\t\t\t\tresult.text = tr::lng_action_add_users_and_one(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_accumulated,\n\t\t\t\t\t\tresult.text,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\tlinkText,\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t}\n\t\t\t}\n\t\t\tresult.text = tr::lng_action_add_users_many(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\tlt_users,\n\t\t\t\tresult.text,\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareChatJoinedByLink = [this](const MTPDmessageActionChatJoinedByLink &action) {\n\t\tauto result = PreparedServiceText();\n\t\tresult.links.push_back(fromLink());\n\t\tresult.text = tr::lng_action_user_joined_by_link(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText(), // Link 1.\n\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareChatCreate = [this](const MTPDmessageActionChatCreate &action) {\n\t\tauto result = PreparedServiceText();\n\t\tif (_history->peer->isMonoforum()) {\n\t\t\tresult.text = tr::lng_action_created_monoforum(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tresult.text = tr::lng_action_created_chat(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\tlt_title,\n\t\t\t\t{ .text = qs(action.vtitle()) },\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareChannelCreate = [this](const MTPDmessageActionChannelCreate &action) {\n\t\tauto result = PreparedServiceText();\n\t\tif (_history->peer->isMonoforum()) {\n\t\t\tresult.text = tr::lng_action_created_monoforum(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked);\n\t\t} else if (isPost()) {\n\t\t\tresult.text = tr::lng_action_created_channel(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tresult.text = tr::lng_action_created_chat(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\tlt_title,\n\t\t\t\t{ .text = qs(action.vtitle()) },\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareChatDeletePhoto = [&](const MTPDmessageActionChatDeletePhoto &action) {\n\t\tauto result = PreparedServiceText();\n\t\tif (isPost()) {\n\t\t\tresult.text = tr::lng_action_removed_photo_channel(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tresult.text = tr::lng_action_removed_photo(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareChatDeleteUser = [this](const MTPDmessageActionChatDeleteUser &action) {\n\t\tauto result = PreparedServiceText();\n\t\tif (peerFromUser(action.vuser_id()) == _from->id) {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tresult.text = tr::lng_action_user_left(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tauto user = _history->owner().user(action.vuser_id().v);\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tresult.links.push_back(user->createOpenLink());\n\t\t\tresult.text = tr::lng_action_kick_user(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\tlt_user,\n\t\t\t\ttr::link(user->name(), 2), // Link 2.\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareChatEditPhoto = [this](const MTPDmessageActionChatEditPhoto &action) {\n\t\tauto result = PreparedServiceText();\n\t\tif (isPost()) {\n\t\t\tresult.text = tr::lng_action_changed_photo_channel(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tresult.text = tr::lng_action_changed_photo(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareChatEditTitle = [this](const MTPDmessageActionChatEditTitle &action) {\n\t\tauto result = PreparedServiceText();\n\t\tif (isPost()) {\n\t\t\tresult.text = tr::lng_action_changed_title_channel(\n\t\t\t\ttr::now,\n\t\t\t\tlt_title,\n\t\t\t\t{ .text = (qs(action.vtitle())) },\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tresult.text = tr::lng_action_changed_title(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\tlt_title,\n\t\t\t\t{ .text = qs(action.vtitle()) },\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto preparePinMessage = [&](const MTPDmessageActionPinMessage &) {\n\t\treturn preparePinnedText();\n\t};\n\n\tauto prepareGameScore = [&](const MTPDmessageActionGameScore &) {\n\t\treturn prepareGameScoreText();\n\t};\n\n\tauto preparePhoneCall = [&](const MTPDmessageActionPhoneCall &) -> PreparedServiceText {\n\t\tUnexpected(\"PhoneCall type in setServiceMessageFromMtp.\");\n\t};\n\n\tauto preparePaymentSent = [&](const MTPDmessageActionPaymentSent &) {\n\t\treturn preparePaymentSentText();\n\t};\n\n\tauto preparePaymentSentMe = [&](const MTPDmessageActionPaymentSentMe &data) {\n\t\tauto result = PreparedServiceText();\n\t\tresult.text = (data.is_recurring_used()\n\t\t\t? tr::lng_action_payment_bot_recurring\n\t\t\t: tr::lng_action_payment_bot_done)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_amount,\n\t\t\t\tAmountAndStarCurrency(data.vtotal_amount().v, qs(data.vcurrency())),\n\t\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareScreenshotTaken = [this](const MTPDmessageActionScreenshotTaken &) {\n\t\tauto result = PreparedServiceText();\n\t\tif (out()) {\n\t\t\tresult.text = tr::lng_action_you_took_screenshot(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tresult.text = tr::lng_action_took_screenshot(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareCustomAction = [&](const MTPDmessageActionCustomAction &action) {\n\t\tauto result = PreparedServiceText();\n\t\tresult.text = { .text = qs(action.vmessage()) };\n\t\treturn result;\n\t};\n\n\tauto prepareBotAllowed = [&](const MTPDmessageActionBotAllowed &action) {\n\t\tauto result = PreparedServiceText();\n\t\tif (action.is_attach_menu()) {\n\t\t\tresult.text = {\n\t\t\t\ttr::lng_action_attach_menu_bot_allowed(tr::now)\n\t\t\t};\n\t\t} else if (action.is_from_request()) {\n\t\t\tresult.text = {\n\t\t\t\ttr::lng_action_webapp_bot_allowed(tr::now)\n\t\t\t};\n\t\t} else if (const auto app = action.vapp()) {\n\t\t\tconst auto bot = history()->peer->asUser();\n\t\t\tconst auto botId = bot ? bot->id : PeerId();\n\t\t\tconst auto info = history()->owner().processBotApp(botId, *app);\n\t\t\tconst auto url = (bot && info)\n\t\t\t\t? history()->session().createInternalLinkFull(\n\t\t\t\t\tbot->username() + '/' + info->shortName)\n\t\t\t\t: QString();\n\t\t\tresult.text = tr::lng_action_bot_allowed_from_app(\n\t\t\t\ttr::now,\n\t\t\t\tlt_app,\n\t\t\t\t(url.isEmpty()\n\t\t\t\t\t? TextWithEntities{ u\"App\"_q }\n\t\t\t\t\t: tr::link(info->title, url)),\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tconst auto domain = qs(action.vdomain().value_or_empty());\n\t\t\tresult.text = tr::lng_action_bot_allowed_from_domain(\n\t\t\t\ttr::now,\n\t\t\t\tlt_domain,\n\t\t\t\ttr::link(domain, u\"http://\"_q + domain),\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareSecureValuesSent = [&](const MTPDmessageActionSecureValuesSent &action) {\n\t\tauto result = PreparedServiceText();\n\t\tauto documents = QStringList();\n\t\tfor (const auto &type : action.vtypes().v) {\n\t\t\tdocuments.push_back([&] {\n\t\t\t\tswitch (type.type()) {\n\t\t\t\tcase mtpc_secureValueTypePersonalDetails:\n\t\t\t\t\treturn tr::lng_action_secure_personal_details(tr::now);\n\t\t\t\tcase mtpc_secureValueTypePassport:\n\t\t\t\tcase mtpc_secureValueTypeDriverLicense:\n\t\t\t\tcase mtpc_secureValueTypeIdentityCard:\n\t\t\t\tcase mtpc_secureValueTypeInternalPassport:\n\t\t\t\t\treturn tr::lng_action_secure_proof_of_identity(tr::now);\n\t\t\t\tcase mtpc_secureValueTypeAddress:\n\t\t\t\t\treturn tr::lng_action_secure_address(tr::now);\n\t\t\t\tcase mtpc_secureValueTypeUtilityBill:\n\t\t\t\tcase mtpc_secureValueTypeBankStatement:\n\t\t\t\tcase mtpc_secureValueTypeRentalAgreement:\n\t\t\t\tcase mtpc_secureValueTypePassportRegistration:\n\t\t\t\tcase mtpc_secureValueTypeTemporaryRegistration:\n\t\t\t\t\treturn tr::lng_action_secure_proof_of_address(tr::now);\n\t\t\t\tcase mtpc_secureValueTypePhone:\n\t\t\t\t\treturn tr::lng_action_secure_phone(tr::now);\n\t\t\t\tcase mtpc_secureValueTypeEmail:\n\t\t\t\t\treturn tr::lng_action_secure_email(tr::now);\n\t\t\t\t}\n\t\t\t\tUnexpected(\"Type in prepareSecureValuesSent.\");\n\t\t\t}());\n\t\t};\n\t\tresult.links.push_back(_history->peer->createOpenLink());\n\t\tresult.text = tr::lng_action_secure_values_sent(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\ttr::link(_history->peer->name(), QString()), // Link 1.\n\t\t\tlt_documents,\n\t\t\t{ .text = documents.join(\", \") },\n\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareContactSignUp = [this](const MTPDmessageActionContactSignUp &data) {\n\t\tauto result = PreparedServiceText();\n\t\tresult.links.push_back(fromLink());\n\t\tresult.text = tr::lng_action_user_registered(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText(), // Link 1.\n\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareProximityReached = [this](const MTPDmessageActionGeoProximityReached &action) {\n\t\tauto result = PreparedServiceText();\n\t\tconst auto fromId = peerFromMTP(action.vfrom_id());\n\t\tconst auto fromPeer = _history->owner().peer(fromId);\n\t\tconst auto toId = peerFromMTP(action.vto_id());\n\t\tconst auto toPeer = _history->owner().peer(toId);\n\t\tconst auto selfId = _from->session().userPeerId();\n\t\tconst auto distanceMeters = action.vdistance().v;\n\t\tconst auto distance = [&] {\n\t\t\tif (distanceMeters >= 1000) {\n\t\t\t\tconst auto km = (10 * (distanceMeters / 10)) / 1000.;\n\t\t\t\treturn tr::lng_action_proximity_distance_km(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tkm);\n\t\t\t} else {\n\t\t\t\treturn tr::lng_action_proximity_distance_m(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tdistanceMeters);\n\t\t\t}\n\t\t}();\n\t\tresult.text = [&] {\n\t\t\tif (fromId == selfId) {\n\t\t\t\tresult.links.push_back(toPeer->createOpenLink());\n\t\t\t\treturn tr::lng_action_you_proximity_reached(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_distance,\n\t\t\t\t\t{ .text = distance },\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::link(toPeer->name(), QString()), // Link 1.\n\t\t\t\t\ttr::marked);\n\t\t\t} else if (toId == selfId) {\n\t\t\t\tresult.links.push_back(fromPeer->createOpenLink());\n\t\t\t\treturn tr::lng_action_proximity_reached_you(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\ttr::link(fromPeer->name(), QString()), // Link 1.\n\t\t\t\t\tlt_distance,\n\t\t\t\t\t{ .text = distance },\n\t\t\t\t\ttr::marked);\n\t\t\t} else {\n\t\t\t\tresult.links.push_back(fromPeer->createOpenLink());\n\t\t\t\tresult.links.push_back(toPeer->createOpenLink());\n\t\t\t\treturn tr::lng_action_proximity_reached(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\ttr::link(fromPeer->name(), 1), // Link 1.\n\t\t\t\t\tlt_distance,\n\t\t\t\t\t{ .text = distance },\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::link(toPeer->name(), 2), // Link 2.\n\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t}();\n\t\treturn result;\n\t};\n\n\tauto prepareGroupCall = [this](const MTPDmessageActionGroupCall &action) {\n\t\tauto result = PreparedServiceText();\n\t\tif (const auto duration = action.vduration()) {\n\t\t\tconst auto seconds = duration->v;\n\t\t\tconst auto days = seconds / 86400;\n\t\t\tconst auto hours = seconds / 3600;\n\t\t\tconst auto minutes = seconds / 60;\n\t\t\tauto text = (days > 1)\n\t\t\t\t? tr::lng_days(tr::now, lt_count, days)\n\t\t\t\t: (hours > 1)\n\t\t\t\t? tr::lng_hours(tr::now, lt_count, hours)\n\t\t\t\t: (minutes > 1)\n\t\t\t\t? tr::lng_minutes(tr::now, lt_count, minutes)\n\t\t\t\t: tr::lng_seconds(tr::now, lt_count, seconds);\n\t\t\tif (_history->peer->isBroadcast()) {\n\t\t\t\tresult.text = tr::lng_action_group_call_finished(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_duration,\n\t\t\t\t\t{ .text = text },\n\t\t\t\t\ttr::marked);\n\t\t\t} else {\n\t\t\t\tresult.links.push_back(fromLink());\n\t\t\t\tresult.text = tr::lng_action_group_call_finished_group(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\t\tlt_duration,\n\t\t\t\t\t{ .text = text },\n\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\t\tif (_history->peer->isBroadcast()) {\n\t\t\tresult.text = tr::lng_action_group_call_started_channel(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tresult.text = tr::lng_action_group_call_started_group(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareInviteToGroupCall = [this](const MTPDmessageActionInviteToGroupCall &action) {\n\t\tconst auto callId = CallIdFromInput(action.vcall());\n\t\tconst auto owner = &_history->owner();\n\t\tconst auto peer = _history->peer;\n\t\tfor (const auto &id : action.vusers().v) {\n\t\t\tconst auto user = owner->user(id.v);\n\t\t\tif (callId) {\n\t\t\t\towner->registerInvitedToCallUser(callId, peer, user, false);\n\t\t\t}\n\t\t};\n\t\tconst auto linkCallId = PeerHasThisCall(peer, callId).value_or(false)\n\t\t\t? callId\n\t\t\t: 0;\n\t\treturn prepareInvitedToCallText(\n\t\t\tParseInvitedToCallUsers(this, action.vusers().v),\n\t\t\tlinkCallId);\n\t};\n\n\tauto prepareSetMessagesTTL = [this](const MTPDmessageActionSetMessagesTTL &action) {\n\t\tauto result = PreparedServiceText();\n\t\tconst auto period = action.vperiod().v;\n\t\tconst auto duration = (period == 5)\n\t\t\t? u\"5 seconds\"_q\n\t\t\t: Ui::FormatTTL(period);\n\t\tif (const auto from = action.vauto_setting_from(); from && period) {\n\t\t\tif (const auto peer = _from->owner().peer(peerFromUser(*from))) {\n\t\t\t\tresult.text = (peer->id == peer->session().userPeerId())\n\t\t\t\t\t? tr::lng_action_ttl_global_me(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_duration,\n\t\t\t\t\t\t{ .text = duration },\n\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t: tr::lng_action_ttl_global(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\ttr::link(peer->name(), 1), // Link 1.\n\t\t\t\t\t\tlt_duration,\n\t\t\t\t\t\t{ .text = duration },\n\t\t\t\t\t\ttr::marked);\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t\tif (isPost()) {\n\t\t\tif (!period) {\n\t\t\t\tresult.text = tr::lng_action_ttl_removed_channel(\n\t\t\t\t\ttr::now,\n\t\t\t\t\ttr::marked);\n\t\t\t} else {\n\t\t\t\tresult.text = tr::lng_action_ttl_changed_channel(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_duration,\n\t\t\t\t\t{ .text = duration },\n\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t} else if (_from->isSelf()) {\n\t\t\tif (!period) {\n\t\t\t\tresult.text = tr::lng_action_ttl_removed_you(\n\t\t\t\t\ttr::now,\n\t\t\t\t\ttr::marked);\n\t\t\t} else {\n\t\t\t\tresult.text = tr::lng_action_ttl_changed_you(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_duration,\n\t\t\t\t\t{ .text = duration },\n\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t} else {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tif (!period) {\n\t\t\t\tresult.text = tr::lng_action_ttl_removed(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\t\ttr::marked);\n\t\t\t} else {\n\t\t\t\tresult.text = tr::lng_action_ttl_changed(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\t\tlt_duration,\n\t\t\t\t\t{ .text = duration },\n\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareGroupCallScheduled = [&](const MTPDmessageActionGroupCallScheduled &data) {\n\t\treturn prepareCallScheduledText(data.vschedule_date().v);\n\t};\n\n\tauto prepareSetChatTheme = [this](const MTPDmessageActionSetChatTheme &action) {\n\t\tauto result = PreparedServiceText();\n\t\taction.vtheme().match([&](const MTPDchatTheme &data) {\n\t\t\tconst auto text = qs(data.vemoticon());\n\t\t\tif (!text.isEmpty()) {\n\t\t\t\tif (_from->isSelf()) {\n\t\t\t\t\tresult.text = tr::lng_action_you_theme_changed(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\t{ .text = text },\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t} else {\n\t\t\t\t\tresult.links.push_back(fromLink());\n\t\t\t\t\tresult.text = tr::lng_action_theme_changed(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\t{ .text = text },\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (_from->isSelf()) {\n\t\t\t\t\tresult.text = tr::lng_action_you_theme_disabled(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t} else {\n\t\t\t\t\tresult.links.push_back(fromLink());\n\t\t\t\t\tresult.text = tr::lng_action_theme_disabled(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t}\n\t\t\t}\n\t\t}, [&](const MTPDchatThemeUniqueGift &data) {\n\t\t\tdata.vgift().match([&](const MTPDstarGiftUnique &data) {\n\t\t\t\tconst auto giftName = tr::bold(qs(data.vtitle())\n\t\t\t\t\t+ u\" #\"_q\n\t\t\t\t\t+ QString::number(data.vnum().v));\n\t\t\t\tif (_from->isSelf()) {\n\t\t\t\t\tresult.text = tr::lng_action_you_gift_theme_changed(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\tgiftName,\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t} else {\n\t\t\t\t\tresult.links.push_back(fromLink());\n\t\t\t\t\tresult.text = tr::lng_action_gift_theme_changed(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\tfromLinkText(),\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\tgiftName,\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t}\n\t\t\t}, [](const MTPDstarGift &) {\n\t\t\t});\n\t\t});\n\t\treturn result;\n\t};\n\n\tauto prepareChatJoinedByRequest = [this](const MTPDmessageActionChatJoinedByRequest &action) {\n\t\tauto result = PreparedServiceText();\n\t\tresult.links.push_back(fromLink());\n\t\tresult.text = tr::lng_action_user_joined_by_request(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText(), // Link 1.\n\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareWebViewDataSent = [](const MTPDmessageActionWebViewDataSent &action) {\n\t\tauto result = PreparedServiceText();\n\t\tresult.text = tr::lng_action_webview_data_done(\n\t\t\ttr::now,\n\t\t\tlt_text,\n\t\t\t{ .text = qs(action.vtext()) },\n\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareGiftPremium = [&](\n\t\t\tconst MTPDmessageActionGiftPremium &action) {\n\t\tauto result = PreparedServiceText();\n\t\tconst auto session = &_history->session();\n\t\tconst auto isSelf = _from->isSelf();\n\t\tconst auto peer = isSelf ? _history->peer : _from;\n\t\tsession->giftBoxStickersPacks().load();\n\t\tconst auto amount = action.vamount().v;\n\t\tconst auto currency = qs(action.vcurrency());\n\t\tconst auto cost = AmountAndStarCurrency(amount, currency);\n\t\tconst auto anonymous = _from->isServiceUser();\n\t\tif (anonymous) {\n\t\t\tresult.text = tr::lng_action_gift_received_anonymous(\n\t\t\t\ttr::now,\n\t\t\t\tlt_cost,\n\t\t\t\tcost,\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.links.push_back(peer->createOpenLink());\n\t\t\tresult.text = isSelf\n\t\t\t\t? tr::lng_action_gift_sent(tr::now,\n\t\t\t\t\tlt_cost,\n\t\t\t\t\tcost,\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_action_gift_received(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::link(peer->shortName(), 1), // Link 1.\n\t\t\t\t\tlt_cost,\n\t\t\t\t\tcost,\n\t\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareTopicCreate = [&](const MTPDmessageActionTopicCreate &action) {\n\t\tauto result = PreparedServiceText();\n\t\tconst auto topicUrl = u\"internal:url:https://t.me/c/%1/%2\"_q\n\t\t\t.arg(peerToChannel(_history->peer->id).bare)\n\t\t\t.arg(id.bare);\n\t\tresult.text = tr::lng_action_topic_created(\n\t\t\ttr::now,\n\t\t\tlt_topic,\n\t\t\ttr::link(\n\t\t\t\tData::ForumTopicIconWithTitle(\n\t\t\t\t\tid,\n\t\t\t\t\taction.vicon_emoji_id().value_or_empty(),\n\t\t\t\t\tqs(action.vtitle())),\n\t\t\t\ttopicUrl),\n\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareTopicEdit = [&](const MTPDmessageActionTopicEdit &action) {\n\t\tauto result = PreparedServiceText();\n\t\tconst auto linkText = history()->peer->isBot()\n\t\t\t? tr::lng_action_topic_bot_thread(tr::now)\n\t\t\t: tr::lng_action_topic_placeholder(tr::now);\n\t\tif (const auto closed = action.vclosed()) {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tresult.text = (mtpIsTrue(*closed)\n\t\t\t\t? tr::lng_action_topic_closed_inside_by(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_action_topic_reopened_inside_by(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\t\ttr::marked));\n\t\t} else if (const auto hidden = action.vhidden()) {\n\t\t\tresult.text = { mtpIsTrue(*hidden)\n\t\t\t\t? tr::lng_action_topic_hidden_inside(tr::now)\n\t\t\t\t: tr::lng_action_topic_unhidden_inside(tr::now) };\n\t\t} else if (!action.vtitle()) {\n\t\t\tif (const auto icon = action.vicon_emoji_id()) {\n\t\t\t\tif (const auto iconId = icon->v) {\n\t\t\t\t\tresult.links.push_back(fromLink());\n\t\t\t\t\tresult.text = tr::lng_action_topic_icon_changed(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\t{ linkText },\n\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\tData::SingleCustomEmoji(iconId),\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t} else {\n\t\t\t\t\tresult.links.push_back(fromLink());\n\t\t\t\t\tresult.text = tr::lng_action_topic_icon_removed(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\t{ linkText },\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tresult.text = tr::lng_action_topic_renamed(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\tlt_link,\n\t\t\t\t{ linkText },\n\t\t\t\tlt_title,\n\t\t\t\tData::ForumTopicIconWithTitle(\n\t\t\t\t\ttopicRootId(),\n\t\t\t\t\taction.vicon_emoji_id().value_or_empty(),\n\t\t\t\t\tqs(*action.vtitle())),\n\t\t\t\ttr::marked);\n\t\t}\n\t\tif (result.text.empty()) {\n\t\t\tresult.text = { tr::lng_message_empty(tr::now) };\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareSuggestProfilePhoto = [this](const MTPDmessageActionSuggestProfilePhoto &action) {\n\t\tauto result = PreparedServiceText{};\n\t\tconst auto isSelf = (_from->id == _from->session().userPeerId());\n\t\tconst auto isVideo = action.vphoto().match([&](const MTPDphoto &data) {\n\t\t\treturn data.vvideo_sizes().has_value()\n\t\t\t\t&& !data.vvideo_sizes()->v.isEmpty();\n\t\t}, [](const MTPDphotoEmpty &) {\n\t\t\treturn false;\n\t\t});\n\t\tconst auto peer = isSelf ? history()->peer : _from;\n\t\tconst auto user = peer->asUser();\n\t\tconst auto name = (user && !user->firstName.isEmpty())\n\t\t\t? user->firstName\n\t\t\t: peer->name();\n\t\tresult.links.push_back(peer->createOpenLink());\n\t\tresult.text = (isSelf\n\t\t\t? (isVideo\n\t\t\t\t? tr::lng_action_suggested_video_me\n\t\t\t\t: tr::lng_action_suggested_photo_me)\n\t\t\t: (isVideo\n\t\t\t\t? tr::lng_action_suggested_video\n\t\t\t\t: tr::lng_action_suggested_photo))(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\ttr::link(name, 1), // Link 1.\n\t\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareRequestedPeer = [&](\n\t\t\tconst MTPDmessageActionRequestedPeer &action) {\n\t\tauto result = PreparedServiceText{};\n\t\tresult.links.push_back(history()->peer->createOpenLink());\n\n\t\tconst auto &list = action.vpeers().v;\n\t\tfor (auto i = 0, count = int(list.size()); i != count; ++i) {\n\t\t\tconst auto id = peerFromMTP(list[i]);\n\n\t\t\tauto user = _history->owner().peer(id);\n\t\t\tresult.links.push_back(user->createOpenLink());\n\n\t\t\tauto linkText = tr::link(user->name(), 2 + i);\n\t\t\tif (i == 0) {\n\t\t\t\tresult.text = linkText;\n\t\t\t} else if (i + 1 == count) {\n\t\t\t\tresult.text = tr::lng_action_add_users_and_last(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_accumulated,\n\t\t\t\t\tresult.text,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tlinkText,\n\t\t\t\t\ttr::marked);\n\t\t\t} else {\n\t\t\t\tresult.text = tr::lng_action_add_users_and_one(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_accumulated,\n\t\t\t\t\tresult.text,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tlinkText,\n\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t}\n\n\t\tresult.text = tr::lng_action_shared_chat_with_bot(\n\t\t\ttr::now,\n\t\t\tlt_chat,\n\t\t\tresult.text,\n\t\t\tlt_bot,\n\t\t\ttr::link(history()->peer->name(), 1),\n\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareSetChatWallPaper = [&](\n\t\t\tconst MTPDmessageActionSetChatWallPaper &action) {\n\t\tconst auto isSelf = (_from->id == _from->session().userPeerId());\n\t\tconst auto same = action.is_same();\n\t\tconst auto both = action.is_for_both();\n\t\tconst auto peer = isSelf ? history()->peer : _from;\n\t\tconst auto user = peer->asUser();\n\t\tconst auto name = (user && !user->firstName.isEmpty())\n\t\t\t? user->firstName\n\t\t\t: peer->name();\n\t\tauto result = PreparedServiceText{};\n\t\tif (!isSelf) {\n\t\t\tresult.links.push_back(peer->createOpenLink());\n\t\t}\n\t\tresult.text = isSelf\n\t\t\t? ((!same && both)\n\t\t\t\t? tr::lng_action_set_wallpaper_both_me(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::link(tr::bold(name), 1),\n\t\t\t\t\ttr::marked)\n\t\t\t\t: (same\n\t\t\t\t\t? tr::lng_action_set_same_wallpaper_me\n\t\t\t\t\t: tr::lng_action_set_wallpaper_me)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::marked))\n\t\t\t: (same\n\t\t\t\t? tr::lng_action_set_same_wallpaper\n\t\t\t\t: tr::lng_action_set_wallpaper)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::link(tr::bold(name), 1),\n\t\t\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareGiftCode = [&](const MTPDmessageActionGiftCode &action) {\n\t\tauto result = PreparedServiceText();\n\t\t_history->session().giftBoxStickersPacks().load();\n\t\tif (const auto boosted = action.vboost_peer()) {\n\t\t\tresult.text = {\n\t\t\t\t(action.is_unclaimed()\n\t\t\t\t\t? tr::lng_prize_unclaimed_about\n\t\t\t\t\t: action.is_via_giveaway()\n\t\t\t\t\t? tr::lng_prize_about\n\t\t\t\t\t: tr::lng_prize_gift_about)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_channel,\n\t\t\t\t\t\t_from->owner().peer(\n\t\t\t\t\t\t\tpeerFromMTP(*action.vboost_peer()))->name()),\n\t\t\t};\n\t\t} else {\n\t\t\tconst auto isSelf = (_from->id == _from->session().userPeerId());\n\t\t\tconst auto peer = isSelf ? _history->peer : _from;\n\t\t\tconst auto cost = AmountAndStarCurrency(\n\t\t\t\taction.vamount().value_or_empty(),\n\t\t\t\tqs(action.vcurrency().value_or_empty()));\n\t\t\tresult.links.push_back(peer->createOpenLink());\n\t\t\tresult.text = isSelf\n\t\t\t\t? tr::lng_action_gift_sent(tr::now,\n\t\t\t\t\tlt_cost,\n\t\t\t\t\tcost,\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_action_gift_received(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::link(peer->shortName(), 1), // Link 1.\n\t\t\t\t\tlt_cost,\n\t\t\t\t\tcost,\n\t\t\t\t\ttr::marked);\n\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareGiveawayLaunch = [&](const MTPDmessageActionGiveawayLaunch &action) {\n\t\tconst auto credits = action.vstars().value_or_empty();\n\t\tauto result = PreparedServiceText();\n\t\tresult.links.push_back(fromLink());\n\t\tresult.text = credits\n\t\t\t? (_history->peer->isMegagroup()\n\t\t\t\t? tr::lng_action_giveaway_credits_started_group\n\t\t\t\t: tr::lng_action_giveaway_credits_started)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\t\tlt_amount,\n\t\t\t\t\ttr::lng_action_giveaway_credits_started_amount(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\tfloat64(credits),\n\t\t\t\t\t\ttr::bold),\n\t\t\t\t\ttr::marked)\n\t\t\t: (_history->peer->isMegagroup()\n\t\t\t\t? tr::lng_action_giveaway_started_group\n\t\t\t\t: tr::lng_action_giveaway_started)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareGiveawayResults = [&](const MTPDmessageActionGiveawayResults &action) {\n\t\tauto result = PreparedServiceText();\n\t\tconst auto winners = action.vwinners_count().v;\n\t\tconst auto unclaimed = action.vunclaimed_count().v;\n\t\tconst auto credits = action.is_stars();\n\t\tresult.text = {\n\t\t\t(!winners\n\t\t\t\t? tr::lng_action_giveaway_results_none(tr::now)\n\t\t\t\t: (credits && unclaimed)\n\t\t\t\t? tr::lng_action_giveaway_results_credits_some(tr::now)\n\t\t\t\t: (!credits && unclaimed)\n\t\t\t\t? tr::lng_action_giveaway_results_some(tr::now)\n\t\t\t\t: (credits && !unclaimed)\n\t\t\t\t? tr::lng_action_giveaway_results_credits(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\twinners)\n\t\t\t\t: tr::lng_action_giveaway_results(tr::now, lt_count, winners))\n\t\t};\n\t\treturn result;\n\t};\n\n\tauto prepareBoostApply = [&](const MTPDmessageActionBoostApply &action) {\n\t\tauto result = PreparedServiceText();\n\t\tconst auto boosts = action.vboosts().v;\n\t\tconst auto isSelf = (_from->id == _from->session().userPeerId());\n\t\tresult.links.push_back(fromLink());\n\t\tresult.text = isSelf\n\t\t\t? tr::lng_action_boost_apply_me(tr::now, tr::marked)\n\t\t\t: tr::lng_action_boost_apply(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tboosts,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\ttr::marked);\n\t\tconst auto channel = _history->peer->asChannel();\n\t\tsetCustomServiceLink(std::make_shared<LambdaClickHandler>([=](\n\t\t\t\tClickContext context) {\n\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\tconst auto weak = my.sessionWindow;\n\t\t\tif (const auto strong = channel ? weak.get() : nullptr) {\n\t\t\t\tstrong->resolveBoostState(channel);\n\t\t\t}\n\t\t}));\n\t\treturn result;\n\t};\n\tauto preparePaymentRefunded = [&](const MTPDmessageActionPaymentRefunded &action) {\n\t\tauto result = PreparedServiceText();\n\t\tconst auto refund = Get<HistoryServicePaymentRefund>();\n\t\tAssert(refund != nullptr);\n\t\tAssert(refund->peer != nullptr);\n\n\t\tconst auto amount = refund->amount;\n\t\tconst auto currency = refund->currency;\n\t\tresult.links.push_back(refund->peer->createOpenLink());\n\t\tresult.text = tr::lng_action_payment_refunded(\n\t\t\ttr::now,\n\t\t\tlt_peer,\n\t\t\ttr::link(refund->peer->name(), 1), // Link 1.\n\t\t\tlt_amount,\n\t\t\tAmountAndStarCurrency(amount, currency),\n\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareGiftStars = [&](\n\t\t\tconst MTPDmessageActionGiftStars &action) {\n\t\tauto result = PreparedServiceText();\n\t\tconst auto isSelf = (_from->id == _from->session().userPeerId());\n\t\tconst auto peer = isSelf ? _history->peer : _from;\n\t\t_history->session().giftBoxStickersPacks().load();\n\t\tconst auto amount = action.vamount().v;\n\t\tconst auto currency = qs(action.vcurrency());\n\t\tconst auto cost = AmountAndStarCurrency(amount, currency);\n\t\tconst auto anonymous = _from->isServiceUser();\n\t\tif (anonymous) {\n\t\t\tresult.text = tr::lng_action_gift_received_anonymous(\n\t\t\t\ttr::now,\n\t\t\t\tlt_cost,\n\t\t\t\tcost,\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.links.push_back(peer->createOpenLink());\n\t\t\tresult.text = isSelf\n\t\t\t\t? tr::lng_action_gift_sent(tr::now,\n\t\t\t\t\tlt_cost,\n\t\t\t\t\tcost,\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_action_gift_received(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::link(peer->shortName(), 1), // Link 1.\n\t\t\t\t\tlt_cost,\n\t\t\t\t\tcost,\n\t\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareGiftTon = [&](\n\t\t\tconst MTPDmessageActionGiftTon &action) {\n\t\tauto result = PreparedServiceText();\n\t\tconst auto isSelf = (_from->id == _from->session().userPeerId());\n\t\tconst auto peer = isSelf ? _history->peer : _from;\n\t\t_history->session().giftBoxStickersPacks().tonLoad();\n\t\tconst auto amount = action.vamount().v;\n\t\tconst auto currency = qs(action.vcurrency());\n\t\tconst auto cost = AmountAndStarCurrency(amount, currency);\n\t\tconst auto anonymous = _from->isServiceUser();\n\t\tif (anonymous) {\n\t\t\tresult.text = tr::lng_action_gift_received_anonymous(\n\t\t\t\ttr::now,\n\t\t\t\tlt_cost,\n\t\t\t\tcost,\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.links.push_back(peer->createOpenLink());\n\t\t\tresult.text = isSelf\n\t\t\t\t? tr::lng_action_gift_sent(tr::now,\n\t\t\t\t\tlt_cost,\n\t\t\t\t\tcost,\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_action_gift_received(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::link(peer->shortName(), 1), // Link 1.\n\t\t\t\t\tlt_cost,\n\t\t\t\t\tcost,\n\t\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareGiftPrize = [&](\n\t\t\tconst MTPDmessageActionPrizeStars &action) {\n\t\tauto result = PreparedServiceText();\n\t\t_history->session().giftBoxStickersPacks().load();\n\t\tresult.text = {\n\t\t\t(action.is_unclaimed()\n\t\t\t\t? tr::lng_prize_unclaimed_about\n\t\t\t\t: tr::lng_prize_about)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_channel,\n\t\t\t\t\t_from->owner().peer(\n\t\t\t\t\t\tpeerFromMTP(action.vboost_peer()))->name()),\n\t\t};\n\t\treturn result;\n\t};\n\n\tauto prepareStarGift = [&](\n\t\t\tconst MTPDmessageActionStarGift &action) {\n\t\tauto result = PreparedServiceText();\n\t\tconst auto upgradeGifted = action.is_prepaid_upgrade();\n\t\tconst auto upgradeSeparate = action.is_upgrade_separate();\n\t\tconst auto isSelf = _from->isSelf();\n\t\tconst auto peer = isSelf ? _history->peer : _from;\n\t\tconst auto stars = action.vgift().match([&](\n\t\t\t\tconst MTPDstarGift &data) {\n\t\t\treturn upgradeGifted\n\t\t\t\t? uint64(action.vupgrade_stars().value_or_empty())\n\t\t\t\t: upgradeSeparate\n\t\t\t\t? uint64(data.vstars().v)\n\t\t\t\t: (uint64(data.vstars().v)\n\t\t\t\t\t+ uint64(action.vupgrade_stars().value_or_empty()));\n\t\t}, [](const MTPDstarGiftUnique &) {\n\t\t\treturn uint64();\n\t\t});\n\t\tif (!stars) {\n\t\t\tif (!isSelf) {\n\t\t\t\tresult.links.push_back(peer->createOpenLink());\n\t\t\t}\n\t\t\tresult.text = isSelf\n\t\t\t\t? tr::lng_action_gift_unique_sent(\n\t\t\t\t\ttr::now,\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_action_gift_unique_received(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::link(peer->shortName(), 1), // Link 1.\n\t\t\t\t\ttr::marked);\n\t\t\treturn result;\n\t\t}\n\t\tconst auto cost = TextWithEntities{\n\t\t\ttr::lng_action_gift_for_stars(tr::now, lt_count, stars),\n\t\t};\n\t\tconst auto giftPeer = action.vpeer()\n\t\t\t? peerFromMTP(*action.vpeer())\n\t\t\t: PeerId();\n\t\tconst auto service = _from->isServiceUser();\n\t\tconst auto toChannel = service && peerIsChannel(giftPeer);\n\t\tconst auto anonymous = service && !toChannel;\n\t\tif (toChannel) {\n\t\t\tconst auto fromId = action.vfrom_id()\n\t\t\t\t? peerFromMTP(*action.vfrom_id())\n\t\t\t\t: PeerId();\n\t\t\tconst auto from = fromId\n\t\t\t\t? peer->owner().peer(fromId).get()\n\t\t\t\t: nullptr;\n\t\t\tconst auto channel = peer->owner().channel(\n\t\t\t\tpeerToChannel(giftPeer));\n\t\t\tif (!from || from->isSelf()) {\n\t\t\t\tresult.links.push_back(channel->createOpenLink());\n\t\t\t\tif (upgradeGifted) {\n\t\t\t\t\tresult.text = tr::lng_action_gift_sent_upgrade_self_channel(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\tcost,\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\ttr::link(channel->name(), 1),\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t} else {\n\t\t\t\t\tresult.text = tr::lng_action_gift_sent_self_channel(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\t\ttr::link(channel->name(), 1),\n\t\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\t\tcost,\n\t\t\t\t\t\t\ttr::marked);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tresult.links.push_back(from->createOpenLink());\n\t\t\t\tresult.links.push_back(channel->createOpenLink());\n\t\t\t\tif (upgradeGifted) {\n\t\t\t\t\tresult.text = tr::lng_action_gift_sent_upgrade_self_other(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\tcost,\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\ttr::link(channel->name(), 2),\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\ttr::link(from->shortName(), 1),\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t} else {\n\t\t\t\t\tresult.text = tr::lng_action_gift_sent_channel(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\ttr::link(from->shortName(), 1),\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\ttr::link(channel->name(), 2),\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\tcost,\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (anonymous || _history->peer->isSelf()) {\n\t\t\tresult.text = (action.is_auction_acquired()\n\t\t\t\t? tr::lng_action_gift_auction_won\n\t\t\t\t: anonymous\n\t\t\t\t? tr::lng_action_gift_received_anonymous\n\t\t\t\t: tr::lng_action_gift_self_bought)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_cost,\n\t\t\t\t\tcost,\n\t\t\t\t\ttr::marked);\n\t\t} else if (upgradeGifted) {\n\t\t\t// Who sent the gift.\n\t\t\tconst auto fromId = action.vfrom_id()\n\t\t\t\t? peerFromMTP(*action.vfrom_id())\n\t\t\t\t: PeerId();\n\t\t\tconst auto from = fromId\n\t\t\t\t? peer->owner().peer(fromId).get()\n\t\t\t\t: nullptr;\n\t\t\tif (isSelf) {\n\t\t\t\tresult.links.push_back(peer->createOpenLink());\n\t\t\t\tif (!from || from->isSelf()) {\n\t\t\t\t\tresult.text = tr::lng_action_gift_sent_upgrade_self(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\tcost,\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t} else {\n\t\t\t\t\tresult.links.push_back(from->createOpenLink());\n\t\t\t\t\tresult.text = tr::lng_action_gift_sent_upgrade_self_other(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\tcost,\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\ttr::link(peer->shortName(), 1),\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\ttr::link(from->shortName(), 2),\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tresult.links.push_back(peer->createOpenLink());\n\t\t\t\tif (from && from != peer && !from->isSelf()) {\n\t\t\t\t\tresult.links.push_back(from->createOpenLink());\n\t\t\t\t\tresult.text = tr::lng_action_gift_sent_upgrade_other(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\ttr::link(peer->shortName(), 1),\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\tcost,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\ttr::link(from->shortName(), 2),\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t} else {\n\t\t\t\t\tresult.text = tr::lng_action_gift_sent_upgrade(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\ttr::link(peer->shortName(), 1),\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\tcost,\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif (!isSelf) {\n\t\t\t\tresult.links.push_back(peer->createOpenLink());\n\t\t\t}\n\t\t\tresult.text = isSelf\n\t\t\t\t? tr::lng_action_gift_sent(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_cost,\n\t\t\t\t\tcost,\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_action_gift_received(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::link(peer->shortName(), 1), // Link 1.\n\t\t\t\t\tlt_cost,\n\t\t\t\t\tcost,\n\t\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareStarGiftUnique = [&](\n\t\t\tconst MTPDmessageActionStarGiftUnique &action) {\n\t\tauto result = PreparedServiceText();\n\t\tconst auto isSelf = _from->isSelf();\n\t\tconst auto resale = CreditsAmountFromTL(action.vresale_amount());\n\t\tconst auto fromOffer = action.is_from_offer();\n\t\tconst auto resaleCost = !resale\n\t\t\t? tr::marked()\n\t\t\t: tr::marked(PrepareCreditsAmountText(resale));\n\t\tconst auto giftPeer = action.vpeer()\n\t\t\t? peerFromMTP(*action.vpeer())\n\t\t\t: PeerId();\n\t\tconst auto service = _from->isServiceUser();\n\t\tconst auto toChannel = service && peerIsChannel(giftPeer);\n\t\tconst auto upgradeHelped = action.is_prepaid_upgrade();\n\t\tconst auto peer = isSelf ? _history->peer : _from;\n\t\tconst auto fromId = action.vfrom_id()\n\t\t\t? peerFromMTP(*action.vfrom_id())\n\t\t\t: PeerId();\n\t\tconst auto from = fromId ? peer->owner().peer(fromId) : peer;\n\t\tif (toChannel) {\n\t\t\tconst auto channel = peer->owner().channel(\n\t\t\t\tpeerToChannel(giftPeer));\n\t\t\tif (!from->isServiceUser() && !from->isSelf()) {\n\t\t\t\tresult.links.push_back(from->createOpenLink());\n\t\t\t\tif (resale) {\n\t\t\t\t\tresult.links.push_back(channel->createOpenLink());\n\t\t\t\t}\n\t\t\t\tresult.text = resale\n\t\t\t\t\t? tr::lng_action_gift_sent_channel(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\ttr::link(from->shortName(), 1),\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\ttr::link(channel->name(), 2),\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\tresaleCost,\n\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t: (action.is_upgrade()\n\t\t\t\t\t\t? tr::lng_action_gift_upgraded_channel\n\t\t\t\t\t\t: tr::lng_action_gift_transferred_channel)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\t\ttr::link(from->shortName(), 1),\n\t\t\t\t\t\t\tlt_channel,\n\t\t\t\t\t\t\ttr::link(channel->name(), 2),\n\t\t\t\t\t\t\ttr::marked);\n\t\t\t} else {\n\t\t\t\tresult.text = resale\n\t\t\t\t\t? tr::lng_action_gift_sent_self_channel(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\ttr::link(channel->name(), 1),\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\tresaleCost,\n\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t: (from->isServiceUser()\n\t\t\t\t\t\t? tr::lng_action_gift_transferred_unknown_channel\n\t\t\t\t\t\t: action.is_upgrade()\n\t\t\t\t\t\t? tr::lng_action_gift_upgraded_self_channel\n\t\t\t\t\t\t: tr::lng_action_gift_transferred_self_channel)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_channel,\n\t\t\t\t\t\t\ttr::link(channel->name(), 1),\n\t\t\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t\tresult.links.push_back(channel->createOpenLink());\n\t\t} else {\n\t\t\tif (upgradeHelped) {\n\t\t\t\tresult.links.push_back(peer->createOpenLink());\n\t\t\t\tif (isSelf) {\n\t\t\t\t\tresult.text = tr::lng_action_gift_upgraded_helped_self(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\ttr::link(peer->shortName(), 1),\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t} else {\n\t\t\t\t\tresult.text = tr::lng_action_gift_upgraded_helped(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\ttr::link(peer->shortName(), 1),\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t}\n\t\t\t} else if (!from->isServiceUser() && !_history->peer->isSelf()) {\n\t\t\t\tif (!resale || !isSelf) {\n\t\t\t\t\tresult.links.push_back(from->createOpenLink());\n\t\t\t\t}\n\t\t\t\tresult.text = fromOffer\n\t\t\t\t\t? (isSelf\n\t\t\t\t\t\t? tr::lng_action_gift_sent_sold(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\t\tresaleCost,\n\t\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t\t: tr::lng_action_gift_received_sold(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\t\ttr::link(peer->shortName(), 1), // Link 1.\n\t\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\t\tresaleCost,\n\t\t\t\t\t\t\ttr::marked))\n\t\t\t\t\t: resale\n\t\t\t\t\t? (isSelf\n\t\t\t\t\t\t? tr::lng_action_gift_sent(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\t\tresaleCost,\n\t\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t\t: tr::lng_action_gift_received(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\t\ttr::link(peer->shortName(), 1), // Link 1.\n\t\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\t\tresaleCost,\n\t\t\t\t\t\t\ttr::marked))\n\t\t\t\t\t: (action.is_upgrade()\n\t\t\t\t\t\t? (isSelf\n\t\t\t\t\t\t\t? tr::lng_action_gift_upgraded_mine\n\t\t\t\t\t\t\t: tr::lng_action_gift_upgraded)\n\t\t\t\t\t\t: (isSelf\n\t\t\t\t\t\t\t? tr::lng_action_gift_transferred_mine\n\t\t\t\t\t\t\t: tr::lng_action_gift_transferred))(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\t\t\ttr::link(from->shortName(), 1),\n\t\t\t\t\t\t\t\ttr::marked);\n\t\t\t} else if (action.is_assigned()) {\n\t\t\t\tconst auto gift = Api::FromTL(\n\t\t\t\t\t&history()->session(),\n\t\t\t\t\taction.vgift());\n\t\t\t\tresult.text = tr::lng_action_gift_displayed_self(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_name,\n\t\t\t\t\tTextWithEntities{ (gift && gift->unique)\n\t\t\t\t\t\t? Data::UniqueGiftName(*gift->unique)\n\t\t\t\t\t\t: QString(),\n\t\t\t\t\t},\n\t\t\t\t\ttr::marked);\n\t\t\t} else if (action.is_craft()) {\n\t\t\t\tresult.text = tr::lng_action_gift_crafted(\n\t\t\t\t\ttr::now,\n\t\t\t\t\ttr::marked);\n\t\t\t} else {\n\t\t\t\tresult.text = resale\n\t\t\t\t\t? tr::lng_action_gift_self_bought(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\tresaleCost,\n\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t: (from->isServiceUser()\n\t\t\t\t\t\t? tr::lng_action_gift_transferred_unknown\n\t\t\t\t\t\t: action.is_upgrade()\n\t\t\t\t\t\t? tr::lng_action_gift_upgraded_self\n\t\t\t\t\t\t: tr::lng_action_gift_transferred_self)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto preparePaidMessagesRefunded = [&](const MTPDmessageActionPaidMessagesRefunded &action) {\n\t\tauto result = PreparedServiceText();\n\t\tif (_from->isSelf()) {\n\t\t\tresult.links.push_back(_history->peer->createOpenLink());\n\t\t\tresult.text = tr::lng_action_paid_message_refund_self(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\taction.vstars().v,\n\t\t\t\tlt_name,\n\t\t\t\ttr::link(_history->peer->shortName(), 1),\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.links.push_back(_from->createOpenLink());\n\t\t\tresult.text = tr::lng_action_paid_message_refund(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\taction.vstars().v,\n\t\t\t\tlt_from,\n\t\t\t\ttr::link(_from->shortName(), 1),\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto preparePaidMessagesPrice = [&](const MTPDmessageActionPaidMessagesPrice &action) {\n\t\tconst auto stars = action.vstars().v;\n\t\tconst auto broadcastAllowed = action.is_broadcast_messages_allowed();\n\t\tauto result = PreparedServiceText();\n\t\tresult.text = _history->peer->isBroadcast()\n\t\t\t? (stars > 0\n\t\t\t\t? tr::lng_action_direct_messages_paid(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tstars,\n\t\t\t\t\ttr::marked)\n\t\t\t\t: broadcastAllowed\n\t\t\t\t? tr::lng_action_direct_messages_enabled(\n\t\t\t\t\ttr::now,\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_action_direct_messages_disabled(\n\t\t\t\t\ttr::now,\n\t\t\t\t\ttr::marked))\n\t\t\t: stars\n\t\t\t? tr::lng_action_message_price_paid(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tstars,\n\t\t\t\ttr::marked)\n\t\t\t: tr::lng_action_message_price_free(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareTodoCompletions = [&](const MTPDmessageActionTodoCompletions &) {\n\t\treturn prepareTodoCompletionsText();\n\t};\n\n\tauto prepareTodoAppendTasks = [&](const MTPDmessageActionTodoAppendTasks &) {\n\t\treturn prepareTodoAppendTasksText();\n\t};\n\n\tauto preparePollAppendAnswer = [&](const MTPDmessageActionPollAppendAnswer &) {\n\t\treturn preparePollAppendAnswerText();\n\t};\n\n\tauto preparePollDeleteAnswer = [&](const MTPDmessageActionPollDeleteAnswer &) {\n\t\treturn preparePollDeleteAnswerText();\n\t};\n\n\tauto prepareSuggestedPostApproval = [&](const MTPDmessageActionSuggestedPostApproval &data) {\n\t\treturn PreparedServiceText{ { data.is_rejected()\n\t\t\t? tr::lng_action_post_rejected(tr::now)\n\t\t\t: data.is_balance_too_low()\n\t\t\t? tr::lng_action_not_enough_funds(tr::now)\n\t\t\t: tr::lng_suggest_action_agreement(tr::now) } };\n\t};\n\n\tauto prepareSuggestedPostSuccess = [&](const MTPDmessageActionSuggestedPostSuccess &data) {\n\t\tconst auto price = CreditsAmountFromTL(&data.vprice());\n\t\tauto result = PreparedServiceText();\n\t\tresult.links.push_back(_from->createOpenLink());\n\t\tresult.text = (price.stars()\n\t\t\t? tr::lng_action_suggest_success_stars\n\t\t\t: tr::lng_action_suggest_success_ton)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\tprice.value(),\n\t\t\t\tlt_from,\n\t\t\t\ttr::link(_from->shortName(), 1),\n\t\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareSuggestedPostRefund = [&](const MTPDmessageActionSuggestedPostRefund &data) {\n\t\treturn PreparedServiceText{ { data.is_payer_initiated()\n\t\t\t? tr::lng_action_suggest_refund_user(tr::now)\n\t\t\t: tr::lng_action_suggest_refund_admin(tr::now)\n\t\t} };\n\t};\n\n\tauto prepareConferenceCall = [&](const MTPDmessageActionConferenceCall &) -> PreparedServiceText {\n\t\tUnexpected(\"PhoneCall type in setServiceMessageFromMtp.\");\n\t};\n\n\tauto prepareSuggestBirthday = [&](const MTPDmessageActionSuggestBirthday &action) {\n\t\tauto result = PreparedServiceText{};\n\t\tconst auto isSelf = (_from->id == _from->session().userPeerId());\n\t\tconst auto peer = isSelf ? history()->peer : _from;\n\t\tconst auto user = peer->asUser();\n\t\tconst auto name = (user && !user->firstName.isEmpty())\n\t\t\t? user->firstName\n\t\t\t: peer->name();\n\t\tresult.links.push_back(peer->createOpenLink());\n\t\tresult.text = (isSelf\n\t\t\t? tr::lng_action_suggested_birthday_me\n\t\t\t: tr::lng_action_suggested_birthday)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\ttr::link(name, 1), // Link 1.\n\t\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareStarGiftPurchaseOffer = [&](const MTPDmessageActionStarGiftPurchaseOffer &action) {\n\t\tauto result = PreparedServiceText{};\n\t\taction.vgift().match([&](const MTPDstarGiftUnique &data) {\n\t\t\tconst auto amount = CreditsAmountFromTL(action.vprice());\n\t\t\tconst auto cost = tr::marked(PrepareCreditsAmountText(amount));\n\t\t\tconst auto giftName = tr::bold(qs(data.vtitle())\n\t\t\t\t+ u\" #\"_q\n\t\t\t\t+ QString::number(data.vnum().v));\n\t\t\tif (_from->isSelf()) {\n\t\t\t\tresult.text = tr::lng_action_gift_offer_you(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_cost,\n\t\t\t\t\tcost,\n\t\t\t\t\tlt_name,\n\t\t\t\t\tgiftName,\n\t\t\t\t\ttr::marked);\n\t\t\t} else {\n\t\t\t\tresult.text = tr::lng_action_gift_offer_incoming(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_amount,\n\t\t\t\t\tcost,\n\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t}, [](const MTPDstarGift &) {\n\t\t});\n\t\treturn result;\n\t};\n\n\tauto prepareStarGiftPurchaseOfferDeclined = [&](const MTPDmessageActionStarGiftPurchaseOfferDeclined &action) {\n\t\tauto result = PreparedServiceText{};\n\t\taction.vgift().match([&](const MTPDstarGiftUnique &data) {\n\t\t\tconst auto amount = CreditsAmountFromTL(action.vprice());\n\t\t\tconst auto cost = tr::marked(PrepareCreditsAmountText(amount));\n\t\t\tconst auto giftName = tr::bold(qs(data.vtitle())\n\t\t\t\t+ u\" #\"_q\n\t\t\t\t+ QString::number(data.vnum().v));\n\t\t\tconst auto expired = action.is_expired();\n\t\t\tif (_from->isSelf()) {\n\t\t\t\tresult.links.push_back(_history->peer->createOpenLink());\n\t\t\t\tresult.text = expired\n\t\t\t\t\t? tr::lng_action_gift_offer_expired(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\ttr::link(st::wrap_rtl(_history->peer->name()), 1),\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\tgiftName,\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\tcost,\n\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t: tr::lng_action_gift_offer_declined_you(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\ttr::link(st::wrap_rtl(_history->peer->name()), 1),\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\tgiftName,\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\tcost,\n\t\t\t\t\t\ttr::marked);\n\t\t\t} else {\n\t\t\t\tif (!expired) {\n\t\t\t\t\tresult.links.push_back(fromLink());\n\t\t\t\t}\n\t\t\t\tresult.text = expired\n\t\t\t\t\t? tr::lng_action_gift_offer_expired_your(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\tgiftName,\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\tcost,\n\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t: tr::lng_action_gift_offer_declined(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\tfromLinkText(),\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\tgiftName,\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\tcost,\n\t\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t}, [](const MTPDstarGift &) {\n\t\t});\n\t\treturn result;\n\t};\n\n\tauto prepareNewCreatorPending = [this](const MTPDmessageActionNewCreatorPending &action) {\n\t\tauto result = PreparedServiceText();\n\t\tauto user = _history->owner().user(action.vnew_creator_id().v);\n\t\tresult.links.push_back(fromLink());\n\t\tresult.links.push_back(user->createOpenLink());\n\t\tresult.text = tr::lng_action_new_creator_pending(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\ttr::link(user->name(), 2),\n\t\t\tlt_from,\n\t\t\tfromLinkText(),\n\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareChangeCreator = [this](const MTPDmessageActionChangeCreator &action) {\n\t\tauto result = PreparedServiceText();\n\t\tauto user = _history->owner().user(action.vnew_creator_id().v);\n\t\tresult.links.push_back(fromLink());\n\t\tresult.links.push_back(user->createOpenLink());\n\t\tresult.text = tr::lng_action_change_creator(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText(),\n\t\t\tlt_user,\n\t\t\ttr::link(user->name(), 2),\n\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tauto prepareNoForwardsToggle = [this](const MTPDmessageActionNoForwardsToggle &action) {\n\t\tauto result = PreparedServiceText();\n\t\tconst auto enabled = mtpIsTrue(action.vnew_value());\n\t\tconst auto prevEnabled = mtpIsTrue(action.vprev_value());\n\t\tif (enabled == prevEnabled) {\n\t\t\tresult.text = tr::lng_action_no_forwards_still_disabled(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked);\n\t\t} else if (out()) {\n\t\t\tresult.text = (enabled\n\t\t\t\t? tr::lng_action_no_forwards_you_disabled\n\t\t\t\t: tr::lng_action_no_forwards_you_enabled)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tresult.text = (enabled\n\t\t\t\t? tr::lng_action_no_forwards_disabled\n\t\t\t\t: tr::lng_action_no_forwards_enabled)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLinkText(),\n\t\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareNoForwardsRequest = [this](const MTPDmessageActionNoForwardsRequest &action) {\n\t\tauto result = PreparedServiceText();\n\t\tconst auto nfr = Get<HistoryServiceNoForwardsRequest>();\n\t\tif (nfr && nfr->expired && !nfr->actionTaken) {\n\t\t\tresult.text = tr::lng_action_no_forwards_request_expired(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked);\n\t\t} else if (_from->isSelf()) {\n\t\t\tresult.text = tr::lng_action_no_forwards_request_you(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tresult.text = tr::lng_action_no_forwards_request(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(),\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn result;\n\t};\n\n\tauto prepareManagedBotCreated = [this](const MTPDmessageActionManagedBotCreated &action) {\n\t\tauto result = PreparedServiceText();\n\t\tauto bot = _history->owner().user(action.vbot_id().v);\n\t\tresult.links.push_back(fromLink());\n\t\tresult.links.push_back(bot->createOpenLink());\n\t\tresult.text = tr::lng_action_managed_bot_created(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText(),\n\t\t\tlt_bot,\n\t\t\ttr::link(bot->name(), 2),\n\t\t\ttr::marked);\n\t\treturn result;\n\t};\n\n\tsetServiceText(action.match(\n\t\tprepareChatAddUserText,\n\t\tprepareChatJoinedByLink,\n\t\tprepareChatCreate,\n\t\tPrepareEmptyText<MTPDmessageActionChatMigrateTo>,\n\t\tPrepareEmptyText<MTPDmessageActionChannelMigrateFrom>,\n\t\tPrepareEmptyText<MTPDmessageActionHistoryClear>,\n\t\tprepareChannelCreate,\n\t\tprepareChatDeletePhoto,\n\t\tprepareChatDeleteUser,\n\t\tprepareChatEditPhoto,\n\t\tprepareChatEditTitle,\n\t\tpreparePinMessage,\n\t\tprepareGameScore,\n\t\tpreparePhoneCall,\n\t\tpreparePaymentSent,\n\t\tprepareScreenshotTaken,\n\t\tprepareCustomAction,\n\t\tprepareBotAllowed,\n\t\tprepareSecureValuesSent,\n\t\tprepareContactSignUp,\n\t\tprepareProximityReached,\n\t\tpreparePaymentSentMe,\n\t\tPrepareErrorText<MTPDmessageActionSecureValuesSentMe>,\n\t\tprepareGroupCall,\n\t\tprepareInviteToGroupCall,\n\t\tprepareSetMessagesTTL,\n\t\tprepareGroupCallScheduled,\n\t\tprepareSetChatTheme,\n\t\tprepareChatJoinedByRequest,\n\t\tprepareWebViewDataSent,\n\t\tprepareGiftPremium,\n\t\tprepareTopicCreate,\n\t\tprepareTopicEdit,\n\t\tPrepareErrorText<MTPDmessageActionWebViewDataSentMe>,\n\t\tprepareSuggestProfilePhoto,\n\t\tprepareRequestedPeer,\n\t\tprepareSetChatWallPaper,\n\t\tprepareGiftCode,\n\t\tprepareGiveawayLaunch,\n\t\tprepareGiveawayResults,\n\t\tprepareBoostApply,\n\t\tpreparePaymentRefunded,\n\t\tprepareGiftStars,\n\t\tprepareGiftTon,\n\t\tprepareGiftPrize,\n\t\tprepareStarGift,\n\t\tprepareStarGiftUnique,\n\t\tpreparePaidMessagesRefunded,\n\t\tpreparePaidMessagesPrice,\n\t\tprepareConferenceCall,\n\t\tprepareTodoCompletions,\n\t\tprepareTodoAppendTasks,\n\t\tpreparePollAppendAnswer,\n\t\tprepareSuggestedPostApproval,\n\t\tprepareSuggestedPostSuccess,\n\t\tprepareSuggestedPostRefund,\n\t\tprepareSuggestBirthday,\n\t\tprepareStarGiftPurchaseOffer,\n\t\tprepareStarGiftPurchaseOfferDeclined,\n\t\tprepareNewCreatorPending,\n\t\tprepareChangeCreator,\n\t\tprepareNoForwardsToggle,\n\t\tprepareNoForwardsRequest,\n\t\tprepareManagedBotCreated,\n\t\tPrepareEmptyText<MTPDmessageActionPollAppendAnswer>,\n\t\tpreparePollDeleteAnswer,\n\t\tPrepareEmptyText<MTPDmessageActionRequestedPeerSentMe>,\n\t\tPrepareErrorText<MTPDmessageActionEmpty>));\n\n\tprocessAction(action);\n}\n\nvoid HistoryItem::processAction(const MTPMessageAction &action) {\n\taction.match([&](const MTPDmessageActionChatAddUser &data) {\n\t\tif (const auto channel = _history->peer->asMegagroup()) {\n\t\t\tconst auto selfUserId = _history->session().userId();\n\t\t\tfor (const auto &item : data.vusers().v) {\n\t\t\t\tif (peerFromUser(item) == selfUserId) {\n\t\t\t\t\tchannel->mgInfo->joinedMessageFound = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDmessageActionChatJoinedByLink &data) {\n\t\tif (_from->isSelf()) {\n\t\t\tif (const auto channel = _history->peer->asMegagroup()) {\n\t\t\t\tchannel->mgInfo->joinedMessageFound = true;\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDmessageActionChatEditPhoto &data) {\n\t\tdata.vphoto().match([&](const MTPDphoto &photo) {\n\t\t\t_media = std::make_unique<Data::MediaPhoto>(\n\t\t\t\tthis,\n\t\t\t\t_history->peer,\n\t\t\t\t_history->owner().processPhoto(photo));\n\t\t}, [](const MTPDphotoEmpty &) {\n\t\t});\n\t}, [&](const MTPDmessageActionChatCreate &) {\n\t\t_flags |= MessageFlag::IsGroupEssential;\n\t}, [&](const MTPDmessageActionChannelCreate &) {\n\t\t_flags |= MessageFlag::IsGroupEssential;\n\t}, [&](const MTPDmessageActionChatMigrateTo &) {\n\t\t_flags |= MessageFlag::IsGroupEssential;\n\t}, [&](const MTPDmessageActionChannelMigrateFrom &) {\n\t\t_flags |= MessageFlag::IsGroupEssential;\n\t}, [&](const MTPDmessageActionContactSignUp &) {\n\t\t_flags |= MessageFlag::IsContactSignUp;\n\t}, [&](const MTPDmessageActionChatJoinedByRequest &data) {\n\t\tif (_from->isSelf()) {\n\t\t\tif (const auto channel = _history->peer->asMegagroup()) {\n\t\t\t\tchannel->mgInfo->joinedMessageFound = true;\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDmessageActionGiftPremium &data) {\n\t\tconst auto session = &history()->session();\n\t\t_media = std::make_unique<Data::MediaGiftBox>(\n\t\t\tthis,\n\t\t\t_from,\n\t\t\tData::GiftCode{\n\t\t\t\t.message = (data.vmessage()\n\t\t\t\t\t? Api::ParseTextWithEntities(session, *data.vmessage())\n\t\t\t\t\t: TextWithEntities()),\n\t\t\t\t.count = data.vdays().v,\n\t\t\t\t.type = Data::GiftType::Premium,\n\t\t\t});\n\t}, [&](const MTPDmessageActionSuggestProfilePhoto &data) {\n\t\tdata.vphoto().match([&](const MTPDphoto &photo) {\n\t\t\t_flags |= MessageFlag::IsUserpicSuggestion;\n\t\t\t_media = std::make_unique<Data::MediaPhoto>(\n\t\t\t\tthis,\n\t\t\t\thistory()->peer,\n\t\t\t\thistory()->owner().processPhoto(photo));\n\t\t}, [](const MTPDphotoEmpty &) {\n\t\t});\n\t}, [&](const MTPDmessageActionSetChatWallPaper &data) {\n\t\tif (!data.is_same()) {\n\t\t\tusing namespace Data;\n\t\t\tconst auto session = &history()->session();\n\t\t\tconst auto &attached = data.vwallpaper();\n\t\t\tif (const auto paper = WallPaper::Create(session, attached)) {\n\t\t\t\t_media = std::make_unique<MediaWallPaper>(\n\t\t\t\t\tthis,\n\t\t\t\t\t*paper,\n\t\t\t\t\tdata.is_for_both());\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDmessageActionSetChatTheme &data) {\n\t\tdata.vtheme().match([](const MTPDchatTheme &) {\n\t\t}, [&](const MTPDchatThemeUniqueGift &data) {\n\t\t\tconst auto session = &history()->session();\n\t\t\tif (const auto gift = Api::FromTL(session, data.vgift())) {\n\t\t\t\t_media = std::make_unique<Data::MediaGiftBox>(\n\t\t\t\t\tthis,\n\t\t\t\t\t_from,\n\t\t\t\t\tData::GiftCode{\n\t\t\t\t\t\t.unique = gift->unique,\n\t\t\t\t\t\t.type = Data::GiftType::ChatTheme,\n\t\t\t\t\t});\n\t\t\t}\n\t\t}, [](const MTPDchatTheme &) {});\n\t}, [&](const MTPDmessageActionGiftCode &data) {\n\t\tconst auto boostedId = data.vboost_peer()\n\t\t\t? peerToChannel(peerFromMTP(*data.vboost_peer()))\n\t\t\t: ChannelId();\n\t\t_media = std::make_unique<Data::MediaGiftBox>(\n\t\t\tthis,\n\t\t\t_from,\n\t\t\tData::GiftCode{\n\t\t\t\t.slug = qs(data.vslug()),\n\t\t\t\t.message = (data.vmessage()\n\t\t\t\t\t? Api::ParseTextWithEntities(\n\t\t\t\t\t\t&history()->session(),\n\t\t\t\t\t\t*data.vmessage())\n\t\t\t\t\t: TextWithEntities()),\n\t\t\t\t.channel = (boostedId\n\t\t\t\t\t? history()->owner().channel(boostedId).get()\n\t\t\t\t\t: nullptr),\n\t\t\t\t.count = data.vdays().v,\n\t\t\t\t.type = Data::GiftType::Premium,\n\t\t\t\t.viaGiveaway = data.is_via_giveaway(),\n\t\t\t\t.unclaimed = data.is_unclaimed(),\n\t\t\t});\n\t}, [&](const MTPDmessageActionGiftStars &data) {\n\t\t_media = std::make_unique<Data::MediaGiftBox>(\n\t\t\tthis,\n\t\t\t_from,\n\t\t\tData::GiftType::Credits,\n\t\t\tdata.vstars().v);\n\t}, [&](const MTPDmessageActionGiftTon &data) {\n\t\t_media = std::make_unique<Data::MediaGiftBox>(\n\t\t\tthis,\n\t\t\t_from,\n\t\t\tData::GiftType::Ton,\n\t\t\tdata.vcrypto_amount().v);\n\t}, [&](const MTPDmessageActionPrizeStars &data) {\n\t\t_media = std::make_unique<Data::MediaGiftBox>(\n\t\t\tthis,\n\t\t\t_from,\n\t\t\tData::GiftCode{\n\t\t\t\t.slug = qs(data.vtransaction_id()),\n\t\t\t\t.channel = history()->owner().channel(\n\t\t\t\t\tpeerToChannel(peerFromMTP(data.vboost_peer()))),\n\t\t\t\t.giveawayMsgId = data.vgiveaway_msg_id().v,\n\t\t\t\t.count = int(data.vstars().v),\n\t\t\t\t.type = Data::GiftType::Credits,\n\t\t\t\t.viaGiveaway = true,\n\t\t\t\t.unclaimed = data.is_unclaimed(),\n\t\t\t});\n\t}, [&](const MTPDmessageActionStarGift &data) {\n\t\tconst auto service = _from->isServiceUser();\n\t\tconst auto from = data.vfrom_id()\n\t\t\t? peerFromMTP(*data.vfrom_id())\n\t\t\t: PeerId();\n\t\tconst auto to = data.vpeer()\n\t\t\t? peerFromMTP(*data.vpeer())\n\t\t\t: PeerId();\n\t\tconst auto upgradeMsgId = data.vupgrade_msg_id().value_or_empty();\n\t\tconst auto giftMsgId = data.vgift_msg_id().value_or_empty();\n\t\tconst auto title = data.vgift().match([&](const MTPDstarGift &gift) {\n\t\t\treturn qs(gift.vtitle().value_or_empty());\n\t\t}, [](const MTPDstarGiftUnique &) {\n\t\t\treturn QString();\n\t\t});\n\t\tconst auto bid = data.vgift().match([&](const MTPDstarGift &gift) {\n\t\t\treturn data.is_auction_acquired()\n\t\t\t\t? (int(gift.vstars().v)\n\t\t\t\t\t+ int(gift.vupgrade_stars().value_or_empty()))\n\t\t\t\t: 0;\n\t\t}, [](const MTPDstarGiftUnique &) {\n\t\t\treturn 0;\n\t\t});\n\t\tconst auto channel = (service && peerIsChannel(to))\n\t\t\t? history()->owner().channel(peerToChannel(to)).get()\n\t\t\t: nullptr;\n\t\tconst auto channelSavedId = channel\n\t\t\t? data.vsaved_id().value_or_empty()\n\t\t\t: uint64();\n\t\tconst auto realGiftMsgId = (peerIsUser(to) && data.vsaved_id())\n\t\t\t? MsgId(data.vsaved_id().value_or_empty())\n\t\t\t: upgradeMsgId\n\t\t\t? upgradeMsgId\n\t\t\t: giftMsgId;\n\n\t\tusing Fields = Data::GiftCode;\n\t\tauto fields = Fields{\n\t\t\t.message = (data.vmessage()\n\t\t\t\t? Api::ParseTextWithEntities(\n\t\t\t\t\t&history()->session(),\n\t\t\t\t\t*data.vmessage())\n\t\t\t\t: TextWithEntities()),\n\t\t\t.auctionTo = (service\n\t\t\t\t&& data.is_auction_acquired()\n\t\t\t\t&& data.vto_id())\n\t\t\t\t? history()->owner().peer(\n\t\t\t\t\tpeerFromMTP(*data.vto_id())).get()\n\t\t\t\t: nullptr,\n\t\t\t.channel = channel,\n\t\t\t.channelFrom = ((service && from)\n\t\t\t\t? history()->owner().peer(from).get()\n\t\t\t\t: nullptr),\n\t\t\t.channelSavedId = channelSavedId,\n\t\t\t.giftPrepayUpgradeHash = qs(\n\t\t\t\tdata.vprepaid_upgrade_hash().value_or_empty()),\n\t\t\t.giftTitle = title,\n\t\t\t.realGiftMsgId = realGiftMsgId,\n\t\t\t.starsConverted = int(data.vconvert_stars().value_or_empty()),\n\t\t\t.starsUpgradedBySender = int(\n\t\t\t\tdata.vupgrade_stars().value_or_empty()),\n\t\t\t.starsBid = bid,\n\t\t\t.giftNum = data.vgift_num().value_or_empty(),\n\t\t\t.type = Data::GiftType::StarGift,\n\t\t\t.upgradeSeparate = data.is_upgrade_separate(),\n\t\t\t.upgradeGifted = data.is_prepaid_upgrade(),\n\t\t\t.upgradable = data.is_can_upgrade(),\n\t\t\t.anonymous = data.is_name_hidden(),\n\t\t\t.converted = data.is_converted(),\n\t\t\t.upgraded = data.is_upgraded(),\n\t\t\t.saved = data.is_saved(),\n\t\t};\n\t\tif (auto gift = Api::FromTL(&history()->session(), data.vgift())) {\n\t\t\tfields.stargiftId = gift->id;\n\t\t\tfields.starsToUpgrade = gift->starsToUpgrade;\n\t\t\tfields.document = gift->document;\n\t\t\tfields.stargiftReleasedBy = gift->releasedBy;\n\t\t\tfields.limitedCount = gift->limitedCount;\n\t\t\tfields.limitedLeft = gift->limitedLeft;\n\t\t\tfields.count = gift->stars;\n\t\t\tfields.unique = gift->unique;\n\t\t}\n\t\t_media = std::make_unique<Data::MediaGiftBox>(\n\t\t\tthis,\n\t\t\t_from,\n\t\t\tstd::move(fields));\n\t}, [&](const MTPDmessageActionStarGiftUnique &data) {\n\t\tconst auto service = _from->isServiceUser();\n\t\tconst auto from = data.vfrom_id()\n\t\t\t? peerFromMTP(*data.vfrom_id())\n\t\t\t: PeerId();\n\t\tconst auto to = data.vpeer()\n\t\t\t? peerFromMTP(*data.vpeer())\n\t\t\t: PeerId();\n\t\tconst auto channel = (service && peerIsChannel(to))\n\t\t\t? history()->owner().channel(peerToChannel(to)).get()\n\t\t\t: nullptr;\n\t\tconst auto channelSavedId = channel\n\t\t\t? data.vsaved_id().value_or_empty()\n\t\t\t: uint64();\n\t\tconst auto realGiftMsgId = (peerIsUser(to) && data.vsaved_id())\n\t\t\t? MsgId(data.vsaved_id().value_or_empty())\n\t\t\t: id;\n\n\t\tusing Fields = Data::GiftCode;\n\t\tauto fields = Fields{\n\t\t\t.channel = channel,\n\t\t\t.channelFrom = ((service && from)\n\t\t\t\t? history()->owner().peer(from).get()\n\t\t\t\t: nullptr),\n\t\t\t.channelSavedId = channelSavedId,\n\t\t\t.realGiftMsgId = realGiftMsgId,\n\t\t\t.starsForDetailsRemove = int(\n\t\t\t\tdata.vdrop_original_details_stars().value_or_empty()),\n\t\t\t.type = Data::GiftType::StarGift,\n\t\t\t.transferred = data.is_transferred(),\n\t\t\t.refunded = data.is_refunded(),\n\t\t\t.upgrade = data.is_upgrade(),\n\t\t\t.saved = data.is_saved(),\n\t\t\t.craft = data.is_craft(),\n\t\t};\n\t\tif (auto gift = Api::FromTL(&history()->session(), data.vgift())) {\n\t\t\tfields.stargiftId = gift->id;\n\t\t\tfields.document = gift->document;\n\t\t\tfields.stargiftReleasedBy = gift->releasedBy;\n\t\t\tfields.limitedCount = gift->limitedCount;\n\t\t\tfields.limitedLeft = gift->limitedLeft;\n\t\t\tfields.count = gift->stars;\n\t\t\tfields.unique = std::move(gift->unique);\n\t\t\tif (const auto unique = fields.unique.get()) {\n\t\t\t\tunique->starsForTransfer\n\t\t\t\t\t= data.vtransfer_stars().value_or(-1);\n\t\t\t\tunique->exportAt = data.vcan_export_at().value_or_empty();\n\t\t\t\tunique->canTransferAt = data.vcan_transfer_at().value_or_empty();\n\t\t\t\tunique->canResellAt = data.vcan_resell_at().value_or_empty();\n\t\t\t\tunique->canCraftAt = data.vcan_craft_at().value_or_empty();\n\t\t\t}\n\t\t}\n\t\t_media = std::make_unique<Data::MediaGiftBox>(\n\t\t\tthis,\n\t\t\t_from,\n\t\t\tstd::move(fields));\n\t}, [&](const MTPDmessageActionSuggestBirthday &data) {\n\t\tconst auto &fields = data.vbirthday().data();\n\t\t_media = std::make_unique<Data::MediaGiftBox>(\n\t\t\tthis,\n\t\t\t_from,\n\t\t\tData::GiftCode{\n\t\t\t\t.count = Data::Birthday(\n\t\t\t\t\tfields.vday().v,\n\t\t\t\t\tfields.vmonth().v,\n\t\t\t\t\tfields.vyear().value_or_empty()).serialize(),\n\t\t\t\t.type = Data::GiftType::BirthdaySuggest,\n\t\t\t});\n\t}, [&](const MTPDmessageActionStarGiftPurchaseOffer &data) {\n\t\tif (const auto suggestion = Get<HistoryMessageSuggestion>()) {\n\t\t\tAssert(suggestion->gift != nullptr);\n\n\t\t\t_media = std::make_unique<Data::MediaGiftBox>(\n\t\t\t\tthis,\n\t\t\t\t_from,\n\t\t\t\tData::GiftCode{\n\t\t\t\t\t.unique = suggestion->gift,\n\t\t\t\t\t.type = Data::GiftType::GiftOffer,\n\t\t\t\t});\n\t\t}\n\t}, [](const auto &) {\n\t});\n}\n\nvoid HistoryItem::setSelfDestruct(\n\t\tHistorySelfDestructType type,\n\t\tMTPint mtpTTLvalue) {\n\tUpdateComponents(HistoryServiceSelfDestruct::Bit());\n\tconst auto selfdestruct = Get<HistoryServiceSelfDestruct>();\n\tif (mtpTTLvalue.v == TimeId(0x7FFFFFFF)) {\n\t\tselfdestruct->timeToLive = TimeToLiveSingleView();\n\t} else {\n\t\tselfdestruct->timeToLive = mtpTTLvalue.v * crl::time(1000);\n\t}\n\tselfdestruct->type = type;\n}\n\nPreparedServiceText HistoryItem::prepareInvitedToCallText(\n\t\tconst std::vector<not_null<UserData*>> &users,\n\t\tCallId linkCallId) {\n\tauto chatText = tr::lng_action_invite_user_chat(\n\t\ttr::now,\n\t\ttr::marked);\n\tauto result = PreparedServiceText();\n\tresult.links.push_back(fromLink());\n\tauto linkIndex = 1;\n\tif (linkCallId) {\n\t\tconst auto peer = _history->peer;\n\t\tresult.links.push_back(GroupCallClickHandler(peer, linkCallId));\n\t\tchatText = tr::link(chatText.text, ++linkIndex);\n\t}\n\tif (users.size() == 1) {\n\t\tauto user = users[0];\n\t\tresult.links.push_back(user->createOpenLink());\n\t\tresult.text = tr::lng_action_invite_user(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText(), // Link 1.\n\t\t\tlt_user,\n\t\t\ttr::link(user->name(), ++linkIndex), // Link N.\n\t\t\tlt_chat,\n\t\t\tchatText,\n\t\t\ttr::marked);\n\t} else if (users.empty()) {\n\t\tresult.text = tr::lng_action_invite_user(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText(), // Link 1.\n\t\t\tlt_user,\n\t\t\t{ .text = u\"somebody\"_q },\n\t\t\tlt_chat,\n\t\t\tchatText,\n\t\t\ttr::marked);\n\t} else {\n\t\tfor (auto i = 0, l = int(users.size()); i != l; ++i) {\n\t\t\tconst auto user = users[i];\n\t\t\tresult.links.push_back(user->createOpenLink());\n\n\t\t\tauto linkText = tr::link(user->name(), ++linkIndex);\n\t\t\tif (i == 0) {\n\t\t\t\tresult.text = linkText;\n\t\t\t} else if (i + 1 == l) {\n\t\t\t\tresult.text = tr::lng_action_invite_users_and_last(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_accumulated,\n\t\t\t\t\tresult.text,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tlinkText,\n\t\t\t\t\ttr::marked);\n\t\t\t} else {\n\t\t\t\tresult.text = tr::lng_action_invite_users_and_one(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_accumulated,\n\t\t\t\t\tresult.text,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tlinkText,\n\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t}\n\t\tresult.text = tr::lng_action_invite_users_many(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText(), // Link 1.\n\t\t\tlt_users,\n\t\t\tresult.text,\n\t\t\tlt_chat,\n\t\t\tchatText,\n\t\t\ttr::marked);\n\t}\n\treturn result;\n}\n\nPreparedServiceText HistoryItem::preparePinnedText() {\n\tauto result = PreparedServiceText();\n\tauto pinned = Get<HistoryServicePinned>();\n\tif (pinned && pinned->msg) {\n\t\tconst auto mediaText = [&] {\n\t\t\tusing TTL = HistoryServiceSelfDestruct;\n\t\t\tif (const auto media = pinned->msg->media()) {\n\t\t\t\treturn media->pinnedTextSubstring();\n\t\t\t} else if (const auto selfdestruct = pinned->msg->Get<TTL>()) {\n\t\t\t\tif (selfdestruct->type == TTL::Type::Photo) {\n\t\t\t\t\treturn tr::lng_action_pinned_media_photo(tr::now);\n\t\t\t\t} else if (selfdestruct->type == TTL::Type::Video) {\n\t\t\t\t\treturn tr::lng_action_pinned_media_video(tr::now);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn QString();\n\t\t}();\n\t\tresult.links.push_back(fromLink());\n\t\tresult.links.push_back(pinned->lnk);\n\t\tif (mediaText.isEmpty()) {\n\t\t\tauto original = pinned->msg->translatedText();\n\t\t\tauto cutAt = 0;\n\t\t\tauto limit = kPinnedMessageTextLimit;\n\t\t\tauto size = original.text.size();\n\t\t\tfor (; limit != 0;) {\n\t\t\t\t--limit;\n\t\t\t\tif (cutAt >= size) break;\n\t\t\t\tif (original.text.at(cutAt).isLowSurrogate()\n\t\t\t\t\t&& (cutAt + 1 < size)\n\t\t\t\t\t&& original.text.at(cutAt + 1).isHighSurrogate()) {\n\t\t\t\t\tcutAt += 2;\n\t\t\t\t} else {\n\t\t\t\t\t++cutAt;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!limit && cutAt + 5 < size) {\n\t\t\t\toriginal = Ui::Text::Mid(original, 0, cutAt).append(\n\t\t\t\t\tUi::kQEllipsis);\n\t\t\t}\n\t\t\toriginal = tr::link(\n\t\t\t\tUi::Text::Filtered(\n\t\t\t\t\tstd::move(original),\n\t\t\t\t\t{\n\t\t\t\t\t\tEntityType::Spoiler,\n\t\t\t\t\t\tEntityType::StrikeOut,\n\t\t\t\t\t\tEntityType::Italic,\n\t\t\t\t\t\tEntityType::CustomEmoji,\n\t\t\t\t\t}),\n\t\t\t\t2);\n\t\t\tresult.text = tr::lng_action_pinned_message(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\tlt_text,\n\t\t\t\tst::wrap_rtl(original), // Link 2.\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.text = tr::lng_action_pinned_media(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\tlt_media,\n\t\t\t\ttr::link(mediaText, 2), // Link 2.\n\t\t\t\ttr::marked);\n\t\t}\n\t} else if (pinned && pinned->msgId) {\n\t\tresult.links.push_back(fromLink());\n\t\tresult.links.push_back(pinned->lnk);\n\t\tresult.text = tr::lng_action_pinned_media(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText(), // Link 1.\n\t\t\tlt_media,\n\t\t\ttr::link(tr::lng_contacts_loading(tr::now), 2), // Link 2.\n\t\t\ttr::marked);\n\t} else {\n\t\tresult.links.push_back(fromLink());\n\t\tresult.text = tr::lng_action_pinned_media(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText(), // Link 1.\n\t\t\tlt_media,\n\t\t\t{ .text = tr::lng_deleted_message(tr::now) },\n\t\t\ttr::marked);\n\t}\n\treturn result;\n}\n\nPreparedServiceText HistoryItem::prepareGameScoreText() {\n\tauto result = PreparedServiceText();\n\tauto gamescore = Get<HistoryServiceGameScore>();\n\n\tauto computeGameTitle = [&]() -> TextWithEntities {\n\t\tif (gamescore && gamescore->msg) {\n\t\t\tif (const auto media = gamescore->msg->media()) {\n\t\t\t\tif (const auto game = media->game()) {\n\t\t\t\t\tconst auto row = 0;\n\t\t\t\t\tconst auto column = 0;\n\t\t\t\t\tresult.links.push_back(\n\t\t\t\t\t\tstd::make_shared<ReplyMarkupClickHandler>(\n\t\t\t\t\t\t\t&_history->owner(),\n\t\t\t\t\t\t\trow,\n\t\t\t\t\t\t\tcolumn,\n\t\t\t\t\t\t\tgamescore->msg->fullId()));\n\t\t\t\t\tauto titleText = game->title;\n\t\t\t\t\treturn tr::link(titleText, QString());\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn tr::lng_deleted_message(tr::now, tr::marked);\n\t\t} else if (gamescore && gamescore->msgId) {\n\t\t\treturn tr::lng_contacts_loading(tr::now, tr::marked);\n\t\t}\n\t\treturn {};\n\t};\n\n\tconst auto scoreNumber = gamescore ? gamescore->score : 0;\n\tif (_from->isSelf()) {\n\t\tauto gameTitle = computeGameTitle();\n\t\tif (gameTitle.text.isEmpty()) {\n\t\t\tresult.text = tr::lng_action_game_you_scored_no_game(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tscoreNumber,\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.text = tr::lng_action_game_you_scored(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tscoreNumber,\n\t\t\t\tlt_game,\n\t\t\t\tgameTitle,\n\t\t\t\ttr::marked);\n\t\t}\n\t} else {\n\t\tresult.links.push_back(fromLink());\n\t\tauto gameTitle = computeGameTitle();\n\t\tif (gameTitle.text.isEmpty()) {\n\t\t\tresult.text = tr::lng_action_game_score_no_game(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tscoreNumber,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.text = tr::lng_action_game_score(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tscoreNumber,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\tlt_game,\n\t\t\t\tgameTitle,\n\t\t\t\ttr::marked);\n\t\t}\n\t}\n\treturn result;\n}\n\nPreparedServiceText HistoryItem::preparePaymentSentText() {\n\tauto result = PreparedServiceText();\n\tconst auto payment = Get<HistoryServicePayment>();\n\tAssert(payment != nullptr);\n\n\tauto invoiceTitle = [&] {\n\t\tif (payment->msg) {\n\t\t\tif (const auto media = payment->msg->media()) {\n\t\t\t\tif (const auto invoice = media->invoice()) {\n\t\t\t\t\treturn tr::link(invoice->title, QString());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn TextWithEntities();\n\t}();\n\n\tif (invoiceTitle.text.isEmpty()) {\n\t\tif (payment->recurringUsed) {\n\t\t\tresult.text = tr::lng_action_payment_used_recurring(\n\t\t\t\ttr::now,\n\t\t\t\tlt_amount,\n\t\t\t\tpayment->amount,\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.text = (payment->recurringInit\n\t\t\t\t? tr::lng_action_payment_init_recurring\n\t\t\t\t: tr::lng_action_payment_done)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_amount,\n\t\t\t\t\tpayment->amount,\n\t\t\t\t\tlt_user,\n\t\t\t\t\t{ .text = _history->peer->name() },\n\t\t\t\t\ttr::marked);\n\t\t}\n\t} else {\n\t\tresult.text = (payment->recurringInit\n\t\t\t? tr::lng_action_payment_init_recurring_for\n\t\t\t: tr::lng_action_payment_done_for)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_amount,\n\t\t\t\tpayment->amount,\n\t\t\t\tlt_user,\n\t\t\t\t{ .text = _history->peer->name() },\n\t\t\t\tlt_invoice,\n\t\t\t\tinvoiceTitle,\n\t\t\t\ttr::marked);\n\t\tif (payment->msg) {\n\t\t\tresult.links.push_back(payment->lnk);\n\t\t}\n\t}\n\treturn result;\n}\n\nPreparedServiceText HistoryItem::prepareStoryMentionText() {\n\tauto result = PreparedServiceText();\n\tconst auto peer = history()->peer;\n\tresult.links.push_back(peer->createOpenLink());\n\tconst auto phrase = (this->media() && this->media()->storyExpired(true))\n\t\t? (out()\n\t\t\t? tr::lng_action_story_mention_me_unavailable\n\t\t\t: tr::lng_action_story_mention_unavailable)\n\t\t: (out()\n\t\t\t? tr::lng_action_story_mention_me\n\t\t\t: tr::lng_action_story_mention);\n\tresult.text = phrase(\n\t\ttr::now,\n\t\tlt_user,\n\t\tUi::Text::Wrapped(\n\t\t\ttr::bold(peer->shortName()),\n\t\t\tEntityType::CustomUrl,\n\t\t\tu\"internal:index\"_q + QChar(1)),\n\t\ttr::marked);\n\treturn result;\n}\n\nPreparedServiceText HistoryItem::prepareCallScheduledText(\n\t\tTimeId scheduleDate) {\n\tconst auto call = Get<HistoryServiceOngoingCall>();\n\tAssert(call != nullptr);\n\n\tconst auto scheduled = base::unixtime::parse(scheduleDate);\n\tconst auto date = scheduled.date();\n\tconst auto now = QDateTime::currentDateTime();\n\tconst auto secsToDateAddDays = [&](int days) {\n\t\treturn now.secsTo(QDateTime(date.addDays(days), QTime(0, 0)));\n\t};\n\tauto result = PreparedServiceText();\n\tconst auto prepareWithDate = [&](const QString &date) {\n\t\tif (_history->peer->isBroadcast()) {\n\t\t\tresult.text = tr::lng_action_group_call_scheduled_channel(\n\t\t\t\ttr::now,\n\t\t\t\tlt_date,\n\t\t\t\t{ .text = date },\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tresult.links.push_back(fromLink());\n\t\t\tresult.text = tr::lng_action_group_call_scheduled_group(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLinkText(), // Link 1.\n\t\t\t\tlt_date,\n\t\t\t\t{ .text = date },\n\t\t\t\ttr::marked);\n\t\t}\n\t};\n\tconst auto time = QLocale().toString(\n\t\tscheduled.time(),\n\t\tQLocale::ShortFormat);\n\tconst auto prepareGeneric = [&] {\n\t\tprepareWithDate(tr::lng_group_call_starts_date(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\tlangDayOfMonthFull(date),\n\t\t\tlt_time,\n\t\t\ttime));\n\t};\n\tauto nextIn = TimeId(0);\n\tif (now.date().addDays(1) < scheduled.date()) {\n\t\tnextIn = secsToDateAddDays(-1);\n\t\tprepareGeneric();\n\t} else if (now.date().addDays(1) == scheduled.date()) {\n\t\tnextIn = secsToDateAddDays(0);\n\t\tprepareWithDate(\n\t\t\ttr::lng_group_call_starts_tomorrow(tr::now, lt_time, time));\n\t} else if (now.date() == scheduled.date()) {\n\t\tnextIn = secsToDateAddDays(1);\n\t\tprepareWithDate(\n\t\t\ttr::lng_group_call_starts_today(tr::now, lt_time, time));\n\t} else {\n\t\tprepareGeneric();\n\t}\n\tif (nextIn) {\n\t\tcall->lifetime = base::timer_once(\n\t\t\t(nextIn + 2) * crl::time(1000)\n\t\t) | rpl::on_next([=] {\n\t\t\tupdateServiceText(prepareCallScheduledText(scheduleDate));\n\t\t});\n\t}\n\treturn result;\n}\n\nPreparedServiceText HistoryItem::composeTodoIncompleted(\n\t\tnot_null<HistoryServiceTodoCompletions*> done) {\n\tconst auto tasks = ComposeTodoTasksList(done->msg, done->incompleted);\n\tif (out()) {\n\t\treturn {\n\t\t\ttr::lng_action_todo_marked_not_done_self(\n\t\t\t\ttr::now,\n\t\t\t\tlt_tasks,\n\t\t\t\ttasks,\n\t\t\t\ttr::marked),\n\t\t};\n\t}\n\treturn {\n\t\t.text = tr::lng_action_todo_marked_not_done(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText(),\n\t\t\tlt_tasks,\n\t\t\ttasks,\n\t\t\ttr::marked),\n\t\t.links = { fromLink() },\n\t};\n}\n\nPreparedServiceText HistoryItem::composeTodoCompleted(\n\t\tnot_null<HistoryServiceTodoCompletions*> done) {\n\tconst auto tasks = ComposeTodoTasksList(done->msg, done->completed);\n\tif (out()) {\n\t\treturn {\n\t\t\ttr::lng_action_todo_marked_done_self(\n\t\t\t\ttr::now,\n\t\t\t\tlt_tasks,\n\t\t\t\ttasks,\n\t\t\t\ttr::marked),\n\t\t};\n\t}\n\treturn {\n\t\t.text = tr::lng_action_todo_marked_done(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText(),\n\t\t\tlt_tasks,\n\t\t\ttasks,\n\t\t\ttr::marked),\n\t\t.links = { fromLink() },\n\t};\n}\n\nPreparedServiceText HistoryItem::prepareTodoCompletionsText() {\n\tauto result = PreparedServiceText();\n\tconst auto done = Get<HistoryServiceTodoCompletions>();\n\tAssert(done != nullptr);\n\n\treturn done->completed.empty()\n\t\t? composeTodoIncompleted(done)\n\t\t: composeTodoCompleted(done);\n}\n\nPreparedServiceText HistoryItem::prepareTodoAppendTasksText() {\n\tauto result = PreparedServiceText();\n\tauto append = Get<HistoryServiceTodoAppendTasks>();\n\tAssert(append != nullptr);\n\n\tconst auto tasks = ComposeTodoTasksList(append);\n\tif (out()) {\n\t\treturn {\n\t\t\ttr::lng_action_todo_added_self(\n\t\t\t\ttr::now,\n\t\t\t\tlt_tasks,\n\t\t\t\ttasks,\n\t\t\t\ttr::marked),\n\t\t};\n\t}\n\treturn {\n\t\t.text = tr::lng_action_todo_added(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText(),\n\t\t\tlt_tasks,\n\t\t\ttasks,\n\t\t\ttr::marked),\n\t\t.links = { fromLink() },\n\t};\n\treturn result;\n}\n\nPreparedServiceText HistoryItem::preparePollAppendAnswerText() {\n\tconst auto append = Get<HistoryServicePollAppendAnswer>();\n\tconst auto option = append\n\t\t? append->answer.text.text\n\t\t: QString();\n\tif (out()) {\n\t\treturn {\n\t\t\ttr::lng_action_poll_added_answer_self(\n\t\t\t\ttr::now,\n\t\t\t\tlt_option,\n\t\t\t\t{ option },\n\t\t\t\ttr::marked),\n\t\t};\n\t}\n\treturn {\n\t\t.text = tr::lng_action_poll_added_answer(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText(),\n\t\t\tlt_option,\n\t\t\t{ option },\n\t\t\ttr::marked),\n\t\t.links = { fromLink() },\n\t};\n}\n\nPreparedServiceText HistoryItem::preparePollDeleteAnswerText() {\n\tconst auto del = Get<HistoryServicePollDeleteAnswer>();\n\tconst auto option = del\n\t\t? del->answer.text.text\n\t\t: QString();\n\tif (out()) {\n\t\treturn {\n\t\t\ttr::lng_action_poll_deleted_answer_self(\n\t\t\t\ttr::now,\n\t\t\t\tlt_option,\n\t\t\t\t{ option },\n\t\t\t\ttr::marked),\n\t\t};\n\t}\n\treturn {\n\t\t.text = tr::lng_action_poll_deleted_answer(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\tfromLinkText(),\n\t\t\tlt_option,\n\t\t\t{ option },\n\t\t\ttr::marked),\n\t\t.links = { fromLink() },\n\t};\n}\n\nTextWithEntities HistoryItem::fromLinkText() const {\n\treturn tr::link(st::wrap_rtl(_from->name()), 1);\n}\n\nClickHandlerPtr HistoryItem::fromLink() const {\n\treturn _from->createOpenLink();\n}\n\ncrl::time HistoryItem::getSelfDestructIn(crl::time now) {\n\tif (const auto selfdestruct = Get<HistoryServiceSelfDestruct>()) {\n\t\tconst auto at = std::get_if<crl::time>(&selfdestruct->destructAt);\n\t\tif (at && (*at) > 0) {\n\t\t\tconst auto destruct = *at;\n\t\t\tif (destruct <= now) {\n\t\t\t\tauto text = [&] {\n\t\t\t\t\tswitch (selfdestruct->type) {\n\t\t\t\t\tcase HistoryServiceSelfDestruct::Type::Photo:\n\t\t\t\t\t\treturn tr::lng_ttl_photo_expired(tr::now);\n\t\t\t\t\tcase HistoryServiceSelfDestruct::Type::Video:\n\t\t\t\t\t\treturn tr::lng_ttl_video_expired(tr::now);\n\t\t\t\t\t}\n\t\t\t\t\tUnexpected(\"Type in HistoryServiceSelfDestruct::Type\");\n\t\t\t\t};\n\t\t\t\tsetServiceText({ TextWithEntities{ .text = text() } });\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\treturn destruct - now;\n\t\t}\n\t}\n\treturn 0;\n}\n\nvoid HistoryItem::cacheOnlyEmojiAndSpaces(bool only) {\n\t_flags |= MessageFlag::OnlyEmojiAndSpacesSet;\n\tif (only) {\n\t\t_flags |= MessageFlag::OnlyEmojiAndSpaces;\n\t} else {\n\t\t_flags &= ~MessageFlag::OnlyEmojiAndSpaces;\n\t}\n}\n\nbool HistoryItem::isOnlyEmojiAndSpaces() const {\n\tif (!(_flags & MessageFlag::OnlyEmojiAndSpacesSet)) {\n\t\tconst_cast<HistoryItem*>(this)->cacheOnlyEmojiAndSpaces(\n\t\t\t!HasNotEmojiAndSpaces(_text.text));\n\t}\n\treturn (_flags & MessageFlag::OnlyEmojiAndSpaces);\n}\n\nvoid HistoryItem::setupChatThemeChange() {\n\tif (const auto user = history()->peer->asUser()) {\n\t\tauto link = std::make_shared<LambdaClickHandler>([=](\n\t\t\t\tClickContext context) {\n\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\t\tcontroller->toggleChooseChatTheme(user);\n\t\t\t}\n\t\t});\n\n\t\tUpdateComponents(HistoryServiceChatThemeChange::Bit());\n\t\tGet<HistoryServiceChatThemeChange>()->link = std::move(link);\n\t} else {\n\t\tRemoveComponents(HistoryServiceChatThemeChange::Bit());\n\t}\n}\n\nvoid HistoryItem::setupTTLChange() {\n\tconst auto peer = history()->peer;\n\tauto link = std::make_shared<LambdaClickHandler>([=](\n\t\t\tClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tconst auto validator = TTLMenu::TTLValidator(\n\t\t\t\tcontroller->uiShow(),\n\t\t\t\tpeer);\n\t\t\tif (validator.can()) {\n\t\t\t\tvalidator.showBox();\n\t\t\t}\n\t\t}\n\t});\n\n\tUpdateComponents(HistoryServiceTTLChange::Bit());\n\tGet<HistoryServiceTTLChange>()->link = std::move(link);\n}\n\nvoid HistoryItem::clearDependencyMessage() {\n\tif (const auto dependent = GetServiceDependentData()) {\n\t\tif (dependent->msg) {\n\t\t\t_history->owner().unregisterDependentMessage(\n\t\t\t\tthis,\n\t\t\t\tdependent->msg);\n\t\t\tdependent->msg = nullptr;\n\t\t\tdependent->msgId = 0;\n\t\t}\n\t}\n}\n\nvoid HistoryItem::overrideMedia(std::unique_ptr<Data::Media> media) {\n\tExpects(!media || media->parent() == this);\n\n\t_media = std::move(media);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_item.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/runtime_composer.h\"\n#include \"data/data_media_types.h\"\n#include \"history/history_item_edition.h\"\n\n#include <any>\n\nclass HiddenSenderInfo;\nclass History;\n\nstruct HistoryMessageReply;\nstruct HistoryMessageViews;\nstruct HistoryMessageMarkupData;\nstruct HistoryMessageReplyMarkup;\nstruct HistoryMessageTranslation;\nstruct HistoryMessageForwarded;\nstruct HistoryMessageSuggestion;\nstruct HistoryServiceDependentData;\nstruct HistoryServiceTodoCompletions;\nenum class HistorySelfDestructType;\nstruct PreparedServiceText;\nstruct MessageFactcheck;\nclass ReplyKeyboard;\nstruct LanguageId;\nenum class SuggestionActions : uchar;\n\nnamespace Api {\nstruct SummaryEntry;\n} // namespace Api\n\nnamespace base {\ntemplate <typename Enum>\nclass enum_mask;\n} // namespace base\n\nnamespace Storage {\nenum class SharedMediaType : signed char;\nusing SharedMediaTypesMask = base::enum_mask<SharedMediaType>;\n} // namespace Storage\n\nnamespace Data {\nstruct MessagePosition;\nstruct RecentReaction;\nstruct MessageReactionsTopPaid;\nstruct ReactionId;\nclass Media;\nstruct MessageReaction;\nclass MessageReactions;\nclass ForumTopic;\nclass Thread;\nstruct SponsoredFrom;\nclass Story;\nclass SavedSublist;\nstruct PaidReactionSend;\nstruct SendError;\n} // namespace Data\n\nnamespace HistoryUnreadThings {\nenum class AddType;\n} // namespace HistoryUnreadThings\n\nnamespace HistoryView {\nclass ElementDelegate;\nclass Element;\nclass Message;\nclass Service;\nclass ServiceMessagePainter;\n} // namespace HistoryView\n\nnamespace Ui {\nstruct ColorCollectible;\n} // namespace Ui\n\nstruct HistoryItemCommonFields {\n\tMsgId id = 0;\n\tMessageFlags flags = 0;\n\tPeerId from = 0;\n\tFullReplyTo replyTo;\n\tTimeId date = 0;\n\tTimeId scheduleRepeatPeriod = 0;\n\tBusinessShortcutId shortcutId = 0;\n\tint starsPaid = 0;\n\tUserId viaBotId = 0;\n\tQString postAuthor;\n\tuint64 groupedId = 0;\n\tEffectId effectId = 0;\n\tHistoryMessageMarkupData markup;\n\tHistoryMessageSuggestInfo suggest;\n\tbool ignoreForwardFrom = false;\n\tbool ignoreForwardCaptions = false;\n\tbool mediaSpoiler = false;\n};\n\nenum class HistoryReactionSource : char {\n\tSelector,\n\tQuick,\n\tExisting,\n};\n\nenum class PaidPostType : uchar {\n\tNone,\n\tStars,\n\tTon,\n};\n\nclass HistoryItem final : public RuntimeComposer<HistoryItem> {\npublic:\n\t[[nodiscard]] static std::unique_ptr<Data::Media> CreateMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPMessageMedia &media);\n\n\tHistoryItem(\n\t\tnot_null<History*> history,\n\t\tMsgId id,\n\t\tconst MTPDmessage &data,\n\t\tMessageFlags localFlags);\n\tHistoryItem(\n\t\tnot_null<History*> history,\n\t\tMsgId id,\n\t\tconst MTPDmessageService &data,\n\t\tMessageFlags localFlags);\n\tHistoryItem(\n\t\tnot_null<History*> history,\n\t\tMsgId id,\n\t\tconst MTPDmessageEmpty &data,\n\t\tMessageFlags localFlags);\n\n\tHistoryItem( // Sponsored message.\n\t\tnot_null<History*> history,\n\t\tMsgId id,\n\t\tData::SponsoredFrom from,\n\t\tconst TextWithEntities &textWithEntities,\n\t\tHistoryItem *injectedAfter);\n\tHistoryItem( // Story wrap.\n\t\tnot_null<History*> history,\n\t\tMsgId id,\n\t\tnot_null<Data::Story*> story);\n\n\tHistoryItem( // Local message.\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields,\n\t\tconst TextWithEntities &textWithEntities,\n\t\tconst MTPMessageMedia &media);\n\tHistoryItem( // Local service message.\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields,\n\t\tPreparedServiceText &&message,\n\t\tPhotoData *photo = nullptr);\n\tHistoryItem( // Local forwarded.\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields,\n\t\tnot_null<HistoryItem*> original);\n\tHistoryItem( // Local photo.\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields,\n\t\tnot_null<PhotoData*> photo,\n\t\tconst TextWithEntities &caption);\n\tHistoryItem( // Local document.\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields,\n\t\tnot_null<DocumentData*> document,\n\t\tconst TextWithEntities &caption);\n\tHistoryItem( // Local game.\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields,\n\t\tnot_null<GameData*> game);\n\t~HistoryItem();\n\n\tstruct Destroyer {\n\t\tvoid operator()(HistoryItem *value);\n\t};\n\n\tvoid dependencyItemRemoved(not_null<HistoryItem*> dependency);\n\tvoid dependencyStoryRemoved(not_null<Data::Story*> dependency);\n\tvoid updateDependencyItem();\n\t[[nodiscard]] MsgId dependencyMsgId() const;\n\t[[nodiscard]] bool notificationReady() const;\n\t[[nodiscard]] PeerData *specialNotificationPeer() const;\n\tvoid checkStoryForwardInfo();\n\tvoid checkBuyButton();\n\n\tvoid resolveDependent();\n\n\tvoid updateServiceText(PreparedServiceText &&text);\n\tvoid updateStoryMentionText();\n\n\t[[nodiscard]] UserData *viaBot() const;\n\t[[nodiscard]] UserData *getMessageBot() const;\n\t[[nodiscard]] bool hideLinks() const;\n\t[[nodiscard]] bool isHistoryEntry() const;\n\t[[nodiscard]] bool isAdminLogEntry() const;\n\t[[nodiscard]] bool isFromScheduled() const;\n\t[[nodiscard]] bool isScheduled() const;\n\t[[nodiscard]] TimeId scheduleRepeatPeriod() const;\n\t[[nodiscard]] bool isSponsored() const;\n\t[[nodiscard]] bool canLookupMessageAuthor() const;\n\t[[nodiscard]] bool skipNotification() const;\n\t[[nodiscard]] bool isUserpicSuggestion() const;\n\t[[nodiscard]] bool isSavedMusicItem() const;\n\t[[nodiscard]] BusinessShortcutId shortcutId() const;\n\t[[nodiscard]] bool isBusinessShortcut() const;\n\tvoid setRealShortcutId(BusinessShortcutId id);\n\tvoid setCustomServiceLink(ClickHandlerPtr link);\n\n\tvoid addLogEntryOriginal(\n\t\tWebPageId localId,\n\t\tconst QString &label,\n\t\tconst TextWithEntities &content);\n\tvoid setFactcheck(MessageFactcheck info);\n\t[[nodiscard]] bool hasUnrequestedFactcheck() const;\n\t[[nodiscard]] TextWithEntities factcheckText() const;\n\t[[nodiscard]] const Api::SummaryEntry &summaryEntry() const;\n\tvoid setHasSummaryEntry();\n\n\t[[nodiscard]] not_null<Data::Thread*> notificationThread() const;\n\t[[nodiscard]] Data::Thread *maybeNotificationThread() const;\n\t[[nodiscard]] not_null<History*> history() const {\n\t\treturn _history;\n\t}\n\t[[nodiscard]] Data::ForumTopic *topic() const;\n\t[[nodiscard]] not_null<PeerData*> from() const {\n\t\treturn _from;\n\t}\n\t[[nodiscard]] HistoryView::Element *mainView() const {\n\t\treturn _mainView;\n\t}\n\tvoid setMainView(not_null<HistoryView::Element*> view) {\n\t\t_mainView = view;\n\t}\n\tvoid refreshMainView();\n\tvoid clearMainView();\n\tvoid removeMainView();\n\n\tvoid invalidateChatListEntry();\n\n\tvoid destroy();\n\t[[nodiscard]] bool out() const {\n\t\treturn _flags & MessageFlag::Outgoing;\n\t}\n\t[[nodiscard]] bool isPinned() const {\n\t\treturn _flags & MessageFlag::Pinned;\n\t}\n\t[[nodiscard]] bool invertMedia() const {\n\t\treturn _flags & MessageFlag::InvertMedia;\n\t}\n\t[[nodiscard]] bool storyInProfile() const {\n\t\treturn _flags & MessageFlag::StoryInProfile;\n\t}\n\t[[nodiscard]] bool unread(not_null<Data::Thread*> thread) const;\n\t[[nodiscard]] bool showNotification() const;\n\tvoid markClientSideAsRead();\n\t[[nodiscard]] bool mentionsMe() const;\n\t[[nodiscard]] bool isUnreadMention() const;\n\t[[nodiscard]] bool hasUnreadReaction() const;\n\t[[nodiscard]] bool hasUnreadPollVote() const;\n\tvoid setHasUnreadPollVote();\n\t[[nodiscard]] bool hasUnwatchedEffect() const;\n\tbool markEffectWatched();\n\t[[nodiscard]] bool isUnreadMedia() const;\n\t[[nodiscard]] bool isIncomingUnreadMedia() const;\n\t[[nodiscard]] bool hasUnreadMediaFlag() const;\n\tvoid markReactionsRead();\n\tvoid markPollVotesRead();\n\tvoid markMediaAndMentionRead();\n\tbool markContentsRead(bool fromThisClient = false);\n\tvoid setIsPinned(bool isPinned);\n\tvoid setStoryInProfile(bool inProfile);\n\n\t// For edit media in history_message.\n\tvoid returnSavedMedia();\n\tvoid savePreviousMedia();\n\t[[nodiscard]] bool isEditingMedia() const;\n\tvoid clearSavedMedia();\n\n\t// Zero result means this message is not self-destructing right now.\n\t[[nodiscard]] crl::time getSelfDestructIn(crl::time now);\n\n\t[[nodiscard]] bool definesReplyKeyboard() const;\n\t[[nodiscard]] ReplyMarkupFlags replyKeyboardFlags() const;\n\n\tvoid cacheOnlyEmojiAndSpaces(bool only);\n\t[[nodiscard]] bool isOnlyEmojiAndSpaces() const;\n\t[[nodiscard]] bool hasSwitchInlineButton() const {\n\t\treturn _flags & MessageFlag::HasSwitchInlineButton;\n\t}\n\t[[nodiscard]] bool hasTextLinks() const {\n\t\treturn _flags & MessageFlag::HasTextLinks;\n\t}\n\t[[nodiscard]] bool isGroupEssential() const {\n\t\treturn _flags & MessageFlag::IsGroupEssential;\n\t}\n\t[[nodiscard]] bool isLocalUpdateMedia() const {\n\t\treturn _flags & MessageFlag::IsLocalUpdateMedia;\n\t}\n\tvoid setIsLocalUpdateMedia(bool flag) {\n\t\tif (flag) {\n\t\t\t_flags |= MessageFlag::IsLocalUpdateMedia;\n\t\t} else {\n\t\t\t_flags &= ~MessageFlag::IsLocalUpdateMedia;\n\t\t}\n\t}\n\t[[nodiscard]] bool isGroupMigrate() const {\n\t\treturn isGroupEssential() && isEmpty();\n\t}\n\t[[nodiscard]] bool hasViews() const {\n\t\treturn _flags & MessageFlag::HasViews;\n\t}\n\t[[nodiscard]] bool isPost() const {\n\t\treturn _flags & MessageFlag::Post;\n\t}\n\t[[nodiscard]] bool isSilent() const {\n\t\treturn _flags & MessageFlag::Silent;\n\t}\n\t[[nodiscard]] bool isSending() const {\n\t\treturn _flags & MessageFlag::BeingSent;\n\t}\n\t[[nodiscard]] bool hasFailed() const {\n\t\treturn _flags & MessageFlag::SendingFailed;\n\t}\n\t[[nodiscard]] bool hideEditedBadge() const {\n\t\treturn (_flags & MessageFlag::HideEdited);\n\t}\n\t[[nodiscard]] bool hideDisplayDate() const {\n\t\treturn isEmpty() || (_flags & MessageFlag::HideDisplayDate);\n\t}\n\t[[nodiscard]] bool isLocal() const {\n\t\treturn _flags & MessageFlag::Local;\n\t}\n\t[[nodiscard]] bool isFakeAboutView() const {\n\t\treturn _flags & MessageFlag::FakeAboutView;\n\t}\n\t[[nodiscard]] bool showSimilarChannels() const {\n\t\treturn _flags & MessageFlag::ShowSimilarChannels;\n\t}\n\t[[nodiscard]] bool canBeSummarized() const {\n\t\treturn _flags & MessageFlag::CanBeSummarized;\n\t}\n\t[[nodiscard]] bool textAppearing() const {\n\t\treturn _flags & MessageFlag::TextAppearing;\n\t}\n\t[[nodiscard]] bool textAppearingStarted() const {\n\t\treturn _flags & MessageFlag::TextAppearingStarted;\n\t}\n\t[[nodiscard]] bool hasRealFromId() const;\n\t[[nodiscard]] bool isPostHidingAuthor() const;\n\t[[nodiscard]] bool isPostShowingAuthor() const;\n\t[[nodiscard]] bool isRegular() const;\n\t[[nodiscard]] bool isUploading() const;\n\tvoid sendFailed();\n\t[[nodiscard]] int viewsCount() const;\n\t[[nodiscard]] int repliesCount() const;\n\t[[nodiscard]] bool repliesAreComments() const;\n\t[[nodiscard]] bool externalReply() const;\n\t[[nodiscard]] bool hasUnpaidContent() const;\n\t[[nodiscard]] bool inHighlightProcess() const;\n\tvoid highlightProcessDone();\n\t[[nodiscard]] PaidPostType paidType() const;\n\n\tvoid setCommentsInboxReadTill(MsgId readTillId);\n\tvoid setCommentsMaxId(MsgId maxId);\n\tvoid setCommentsPossibleMaxId(MsgId possibleMaxId);\n\t[[nodiscard]] bool areCommentsUnread() const;\n\n\t[[nodiscard]] FullMsgId commentsItemId() const;\n\tvoid setCommentsItemId(FullMsgId id);\n\n\t[[nodiscard]] bool needCheck() const;\n\n\t[[nodiscard]] bool isService() const;\n\tvoid applyEdition(HistoryMessageEdition &&edition);\n\tvoid applyChanges(not_null<Data::Story*> story);\n\n\tvoid applyEdition(const MTPDmessageService &message);\n\tvoid applyEdition(const QVector<MTPMessageExtendedMedia> &media);\n\tvoid updateForwardedInfo(const MTPMessageFwdHeader *fwd);\n\tvoid updateSentContent(\n\t\tconst TextWithEntities &textWithEntities,\n\t\tconst MTPMessageMedia *media);\n\tvoid applySentMessage(const MTPDmessage &data);\n\tvoid applySentMessage(\n\t\tconst QString &text,\n\t\tconst MTPDupdateShortSentMessage &data,\n\t\tbool wasAlready);\n\tvoid updateReactions(const MTPMessageReactions *reactions);\n\tvoid overrideMedia(std::unique_ptr<Data::Media> media);\n\n\tvoid applyEditionToHistoryCleared();\n\tvoid updateReplyMarkup(\n\t\tHistoryMessageMarkupData &&markup,\n\t\tbool ignoreSuggestButtons = false);\n\tvoid contributeToSlowmode(TimeId realDate = 0);\n\n\tvoid clearMediaAsExpired();\n\n\tvoid addToUnreadThings(HistoryUnreadThings::AddType type);\n\tvoid destroyHistoryEntry();\n\t[[nodiscard]] Storage::SharedMediaTypesMask sharedMediaTypes() const;\n\n\tvoid indexAsNewItem();\n\tvoid addToSharedMediaIndex();\n\tvoid addToMessagesIndex();\n\tvoid removeFromSharedMediaIndex();\n\n\tstruct NotificationTextOptions {\n\t\tbool spoilerLoginCode = false;\n\t};\n\t[[nodiscard]] QString notificationHeader() const;\n\t[[nodiscard]] TextWithEntities notificationText(\n\t\tNotificationTextOptions options) const;\n\t[[nodiscard]] TextWithEntities notificationText() const {\n\t\treturn notificationText({});\n\t}\n\n\tusing ToPreviewOptions = HistoryView::ToPreviewOptions;\n\tusing ItemPreview = HistoryView::ItemPreview;\n\n\t// Returns text with link-start and link-end commands for service-color highlighting.\n\t// Example: \"[link1-start]You:[link1-end] [link1-start]Photo,[link1-end] caption text\"\n\t[[nodiscard]] ItemPreview toPreview(ToPreviewOptions options) const;\n\t[[nodiscard]] TextWithEntities inReplyText() const;\n\t[[nodiscard]] const TextWithEntities &originalText() const;\n\t[[nodiscard]] const TextWithEntities &translatedText() const;\n\t[[nodiscard]] TextWithEntities translatedTextWithLocalEntities() const;\n\t[[nodiscard]] const std::vector<ClickHandlerPtr> &customTextLinks() const;\n\t[[nodiscard]] TextForMimeData clipboardText() const;\n\n\tbool changeViewsCount(int count);\n\tvoid setForwardsCount(int count);\n\tvoid setReplies(HistoryMessageRepliesData &&data);\n\tvoid clearReplies();\n\tvoid changeRepliesCount(int delta, PeerId replier);\n\tvoid setReplyFields(\n\t\tMsgId replyTo,\n\t\tMsgId replyToTop,\n\t\tbool isForumPost);\n\tvoid setPostAuthor(const QString &author);\n\tvoid setRealId(MsgId newId);\n\tvoid markTextAppearingStarted();\n\tvoid incrementReplyToTopCounter();\n\tvoid applyEffectWatchedOnUnreadKnown();\n\n\tvoid setHasHiddenLinks(bool has) const;\n\t[[nodiscard]] bool hasHiddenLinks() const;\n\n\t[[nodiscard]] bool emptyText() const {\n\t\treturn _text.empty();\n\t}\n\n\t[[nodiscard]] bool canPin() const;\n\t[[nodiscard]] bool canBeEdited() const;\n\t[[nodiscard]] bool canStopPoll() const;\n\t[[nodiscard]] bool forbidsForward() const;\n\t[[nodiscard]] bool forbidsSaving() const;\n\t[[nodiscard]] bool allowsSendNow() const;\n\t[[nodiscard]] bool allowsReschedule() const;\n\t[[nodiscard]] bool allowsForward() const;\n\t[[nodiscard]] bool allowsEdit(TimeId now) const;\n\t[[nodiscard]] bool allowsEditMedia() const;\n\t[[nodiscard]] bool canDelete() const;\n\t[[nodiscard]] bool canDeleteForEveryone(TimeId now) const;\n\t[[nodiscard]] bool suggestReport() const;\n\t[[nodiscard]] bool suggestBanReport() const;\n\t[[nodiscard]] bool suggestDeleteAllReport() const;\n\t[[nodiscard]] ChatRestriction requiredSendRight() const;\n\t[[nodiscard]] bool requiresSendInlineRight() const;\n\t[[nodiscard]] Data::SendError errorTextForForward(\n\t\tnot_null<Data::Thread*> to) const;\n\t[[nodiscard]] Data::SendError errorTextForForwardIgnoreRights(\n\t\tnot_null<Data::Thread*> to) const;\n\t[[nodiscard]] const HistoryMessageTranslation *translation() const;\n\t[[nodiscard]] bool translationShowRequiresCheck(LanguageId to) const;\n\tbool translationShowRequiresRequest(LanguageId to);\n\tvoid translationDone(LanguageId to, TextWithEntities result);\n\n\t[[nodiscard]] bool canReact() const;\n\tvoid toggleReaction(\n\t\tconst Data::ReactionId &reaction,\n\t\tHistoryReactionSource source);\n\tvoid addPaidReaction(int count, std::optional<PeerId> shownPeer = {});\n\tvoid cancelScheduledPaidReaction();\n\t[[nodiscard]] Data::PaidReactionSend startPaidReactionSending();\n\tvoid finishPaidReactionSending(\n\t\tData::PaidReactionSend send,\n\t\tbool success);\n\tvoid updateReactionsUnknown();\n\t[[nodiscard]] auto reactions() const\n\t\t-> const std::vector<Data::MessageReaction> &;\n\t[[nodiscard]] auto reactionsWithLocal() const\n\t\t-> std::vector<Data::MessageReaction>;\n\t[[nodiscard]] auto recentReactions() const\n\t\t-> const base::flat_map<\n\t\t\tData::ReactionId,\n\t\t\tstd::vector<Data::RecentReaction>> &;\n\t[[nodiscard]] auto topPaidReactionsWithLocal() const\n\t\t-> std::vector<Data::MessageReactionsTopPaid>;\n\t[[nodiscard]] int reactionsPaidScheduled() const;\n\t[[nodiscard]] PeerId reactionsLocalShownPeer() const;\n\t[[nodiscard]] bool canViewReactions() const;\n\t[[nodiscard]] std::vector<Data::ReactionId> chosenReactions() const;\n\t[[nodiscard]] Data::ReactionId lookupUnreadReaction(\n\t\tnot_null<UserData*> from) const;\n\t[[nodiscard]] QByteArray lookupUnreadPollVote(\n\t\tnot_null<PeerData*> from) const;\n\t[[nodiscard]] crl::time lastReactionsRefreshTime() const;\n\n\t[[nodiscard]] bool reactionsAreTags() const;\n\t[[nodiscard]] bool hasDirectLink() const;\n\t[[nodiscard]] bool changesWallPaper() const;\n\n\t[[nodiscard]] FullMsgId fullId() const;\n\t[[nodiscard]] GlobalMsgId globalId() const;\n\t[[nodiscard]] Data::MessagePosition position() const;\n\t[[nodiscard]] TimeId date() const;\n\t[[nodiscard]] bool awaitingVideoProcessing() const;\n\n\t[[nodiscard]] Data::Media *media() const {\n\t\treturn _media.get();\n\t}\n\t[[nodiscard]] bool computeDropForwardedInfo() const;\n\tvoid setText(TextWithEntities textWithEntities);\n\n\t[[nodiscard]] MsgId replyToId() const;\n\t[[nodiscard]] FullMsgId replyToFullId() const;\n\t[[nodiscard]] MsgId replyToTop() const;\n\t[[nodiscard]] MsgId topicRootId() const;\n\t[[nodiscard]] FullStoryId replyToStory() const;\n\t[[nodiscard]] FullReplyTo replyTo() const;\n\t[[nodiscard]] bool inThread(MsgId rootId) const;\n\n\t[[nodiscard]] not_null<PeerData*> author() const;\n\n\t[[nodiscard]] TimeId originalDate() const;\n\t[[nodiscard]] PeerData *originalSender() const;\n\t[[nodiscard]] const HiddenSenderInfo *originalHiddenSenderInfo() const;\n\t[[nodiscard]] not_null<PeerData*> fromOriginal() const;\n\t[[nodiscard]] QString originalPostAuthor() const;\n\t[[nodiscard]] MsgId originalId() const;\n\n\t[[nodiscard]] Data::SavedSublist *savedSublist() const;\n\t[[nodiscard]] PeerId sublistPeerId() const;\n\t[[nodiscard]] PeerData *savedFromSender() const;\n\t[[nodiscard]] const HiddenSenderInfo *savedFromHiddenSenderInfo() const;\n\n\t[[nodiscard]] const HiddenSenderInfo *displayHiddenSenderInfo() const;\n\n\t[[nodiscard]] bool showForwardsFromSender(\n\t\tnot_null<const HistoryMessageForwarded*> forwarded) const;\n\n\t[[nodiscard]] bool isEmpty() const;\n\t[[nodiscard]] MessageGroupId groupId() const;\n\t[[nodiscard]] EffectId effectId() const;\n\t[[nodiscard]] bool hasPossibleRestrictions() const;\n\t[[nodiscard]] QString computeUnavailableReason() const;\n\t[[nodiscard]] bool isMediaSensitive() const;\n\n\t[[nodiscard]] const HistoryMessageReplyMarkup *inlineReplyMarkup() const {\n\t\treturn const_cast<HistoryItem*>(this)->inlineReplyMarkup();\n\t}\n\t[[nodiscard]] const ReplyKeyboard *inlineReplyKeyboard() const {\n\t\treturn const_cast<HistoryItem*>(this)->inlineReplyKeyboard();\n\t}\n\t[[nodiscard]] HistoryMessageReplyMarkup *inlineReplyMarkup();\n\t[[nodiscard]] ReplyKeyboard *inlineReplyKeyboard();\n\n\t[[nodiscard]] ChannelData *discussionPostOriginalSender() const;\n\t[[nodiscard]] bool isDiscussionPost() const;\n\t[[nodiscard]] HistoryItem *lookupDiscussionPostOriginal() const;\n\t[[nodiscard]] PeerData *displayFrom() const;\n\n\t[[nodiscard]] uint8 colorIndex() const;\n\t[[nodiscard]] DocumentId backgroundEmojiId() const;\n\t[[nodiscard]] auto colorCollectible() const\n\t\t-> const std::shared_ptr<Ui::ColorCollectible> &;\n\n\t// In forwards we show name in sender's color, but the message\n\t// content uses the color of the original sender.\n\t[[nodiscard]] PeerData *contentColorsFrom() const;\n\t[[nodiscard]] uint8 contentColorIndex() const;\n\t[[nodiscard]] DocumentId contentBackgroundEmojiId() const;\n\t[[nodiscard]] auto contentColorCollectible() const\n\t\t-> const std::shared_ptr<Ui::ColorCollectible> &;\n\n\t[[nodiscard]] int starsPaid() const;\n\n\t[[nodiscard]] std::unique_ptr<HistoryView::Element> createView(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate,\n\t\tHistoryView::Element *replacing = nullptr);\n\n\tvoid updateDate(TimeId newDate);\n\t[[nodiscard]] bool canUpdateDate() const;\n\tvoid customEmojiRepaint();\n\n\t[[nodiscard]] SuggestionActions computeSuggestionActions() const;\n\t[[nodiscard]] SuggestionActions computeSuggestionActions(\n\t\tconst HistoryMessageSuggestion *suggest) const;\n\t[[nodiscard]] SuggestionActions computeSuggestionActions(\n\t\tbool accepted,\n\t\tbool rejected,\n\t\tTimeId giftOfferExpiresAt) const;\n\n\t[[nodiscard]] bool needsUpdateForVideoQualities(const MTPMessage &data);\n\n\t[[nodiscard]] TimeId ttlDestroyAt() const {\n\t\treturn _ttlDestroyAt;\n\t}\n\n\t[[nodiscard]] int boostsApplied() const {\n\t\treturn _boostsApplied;\n\t}\n\n\t[[nodiscard]] QString fromRank() const;\n\n\tMsgId id;\n\nprivate:\n\tstruct CreateConfig;\n\n\tHistoryItem(\n\t\tnot_null<History*> history,\n\t\tconst HistoryItemCommonFields &fields);\n\n\tvoid createComponentsHelper(HistoryItemCommonFields &&fields);\n\tvoid createComponents(CreateConfig &&config);\n\tvoid setupForwardedComponent(const CreateConfig &config);\n\tvoid applyInitialEffectWatched();\n\n\t[[nodiscard]] bool generateLocalEntitiesByReply() const;\n\t[[nodiscard]] TextWithEntities withLocalEntities(\n\t\tconst TextWithEntities &textWithEntities) const;\n\tvoid detectTextLinks(const TextWithEntities &textWithEntities);\n\tvoid setTextValue(TextWithEntities text, bool force = false);\n\t[[nodiscard]] bool isTooOldForEdit(TimeId now) const;\n\t[[nodiscard]] bool isLegacyMessage() const {\n\t\treturn _flags & MessageFlag::Legacy;\n\t}\n\n\t[[nodiscard]] bool checkDiscussionLink(ChannelId id) const;\n\n\tvoid setReplyMarkup(\n\t\tHistoryMessageMarkupData &&markup,\n\t\tbool ignoreSuggestButtons = false);\n\tvoid updateSuggestControls(const HistoryMessageSuggestion *suggest);\n\n\tvoid changeReplyToTopCounter(\n\t\tnot_null<HistoryMessageReply*> reply,\n\t\tint delta);\n\tvoid refreshRepliesText(\n\t\tnot_null<HistoryMessageViews*> views,\n\t\tbool forceResize = false);\n\n\t[[nodiscard]] bool checkRepliesPts(\n\t\tconst HistoryMessageRepliesData &data) const;\n\n\t[[nodiscard]] HistoryServiceDependentData *GetServiceDependentData();\n\t[[nodiscard]] auto GetServiceDependentData() const\n\t\t-> const HistoryServiceDependentData *;\n\tvoid updateDependentServiceText();\n\tvoid updateServiceDependent(bool force = false);\n\tvoid setServiceText(PreparedServiceText &&prepared);\n\n\tvoid setStoryFields(not_null<Data::Story*> story);\n\tvoid finishEdition(int oldKeyboardTop);\n\tvoid finishEditionToEmpty();\n\n\tvoid clearDependencyMessage();\n\tvoid setupChatThemeChange();\n\tvoid setupTTLChange();\n\n\tvoid translationToggle(\n\t\tnot_null<HistoryMessageTranslation*> translation,\n\t\tbool used);\n\tvoid setSelfDestruct(HistorySelfDestructType type, MTPint mtpTTLvalue);\n\n\tvoid resolveDependent(not_null<HistoryServiceDependentData*> dependent);\n\tvoid resolveDependent(not_null<HistoryMessageReply*> reply);\n\n\t[[nodiscard]] TextWithEntities fromLinkText() const;\n\t[[nodiscard]] ClickHandlerPtr fromLink() const;\n\n\tvoid setGroupId(MessageGroupId groupId);\n\n\tstatic void FillForwardedInfo(\n\t\tCreateConfig &config,\n\t\tconst MTPDmessageFwdHeader &data);\n\tvoid createComponents(const MTPDmessage &data);\n\tvoid setMedia(const MTPMessageMedia &media);\n\tvoid applyServiceDateEdition(const MTPDmessageService &data);\n\tvoid setReactions(const MTPMessageReactions *reactions);\n\t[[nodiscard]] bool changeReactions(const MTPMessageReactions *reactions);\n\tvoid setServiceMessageByAction(const MTPmessageAction &action);\n\tvoid processAction(const MTPMessageAction &action);\n\tvoid refreshMedia(const MTPMessageMedia *media);\n\tvoid refreshSentMedia(const MTPMessageMedia *media);\n\tvoid createServiceFromMtp(const MTPDmessage &message);\n\tvoid createServiceFromMtp(const MTPDmessageService &message);\n\tvoid applyTTL(const MTPDmessage &data);\n\tvoid applyTTL(const MTPDmessageService &data);\n\n\tvoid applyTTL(TimeId destroyAt);\n\n\t// For an invoice button we replace the button text with a \"Receipt\" key.\n\t// It should show the receipt for the payed invoice. Still let mobile apps do that.\n\tvoid replaceBuyWithReceiptInMarkup();\n\n\t[[nodiscard]] PreparedServiceText preparePinnedText();\n\t[[nodiscard]] PreparedServiceText prepareGameScoreText();\n\t[[nodiscard]] PreparedServiceText preparePaymentSentText();\n\t[[nodiscard]] PreparedServiceText prepareStoryMentionText();\n\t[[nodiscard]] PreparedServiceText prepareInvitedToCallText(\n\t\tconst std::vector<not_null<UserData*>> &users,\n\t\tCallId linkCallId);\n\t[[nodiscard]] PreparedServiceText prepareCallScheduledText(\n\t\tTimeId scheduleDate);\n\t[[nodiscard]] PreparedServiceText prepareTodoCompletionsText();\n\t[[nodiscard]] PreparedServiceText prepareTodoAppendTasksText();\n\t[[nodiscard]] PreparedServiceText preparePollAppendAnswerText();\n\t[[nodiscard]] PreparedServiceText preparePollDeleteAnswerText();\n\n\t[[nodiscard]] PreparedServiceText composeTodoIncompleted(\n\t\tnot_null<HistoryServiceTodoCompletions*> done);\n\t[[nodiscard]] PreparedServiceText composeTodoCompleted(\n\t\tnot_null<HistoryServiceTodoCompletions*> done);\n\n\t[[nodiscard]] PreparedServiceText prepareServiceTextForMessage(\n\t\tconst MTPMessageMedia &media,\n\t\tbool unread);\n\n\tvoid flagSensitiveContent();\n\t[[nodiscard]] PeerData *computeDisplayFrom() const;\n\n\tconst not_null<History*> _history;\n\tconst not_null<PeerData*> _from;\n\tmutable PeerData *_displayFrom = nullptr;\n\tmutable MessageFlags _flags = 0;\n\n\tTextWithEntities _text;\n\n\tstd::unique_ptr<Data::Media> _media;\n\tstd::unique_ptr<Data::MessageReactions> _reactions;\n\tcrl::time _reactionsLastRefreshed = 0;\n\n\tTimeId _date = 0;\n\tTimeId _ttlDestroyAt = 0;\n\tint _boostsApplied = 0;\n\tint _starsPaid = 0;\n\tBusinessShortcutId _shortcutId = 0;\n\n\tMessageGroupId _groupId = MessageGroupId();\n\tEffectId _effectId = 0;\n\tHistoryView::Element *_mainView = nullptr;\n\n\tfriend class HistoryView::Element;\n\tfriend class HistoryView::Message;\n\tfriend class HistoryView::Service;\n\tfriend class HistoryView::ServiceMessagePainter;\n\n};\n\nconstexpr auto kSize = int(sizeof(HistoryItem));\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_item_components.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_item_components.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"base/options.h\"\n#include \"base/unixtime.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/image/image.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/power_saving.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/view/history_view_message.h\" // FromNameFg.\n#include \"history/view/history_view_service_message.h\"\n#include \"history/view/media/history_view_document.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\"\n#include \"layout/layout_position.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/components/scheduled_messages.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_document.h\"\n#include \"data/data_web_page.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_todo_list.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n#include \"api/api_bot.h\"\n#include \"support/support_helper.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_dialogs.h\" // dialogsMiniReplyStory.\n#include \"styles/style_settings.h\"\n#include \"styles/style_widgets.h\"\n\n#include <QtGui/QGuiApplication>\n\nnamespace {\n\nconst auto kPsaForwardedPrefix = \"cloud_lng_forwarded_psa_\";\n\nbase::options::toggle FastButtonsModeOption({\n\t.id = kOptionFastButtonsMode,\n\t.name = \"Fast buttons mode\",\n\t.description = \"Trigger inline keyboard buttons by 1-9 keyboard keys.\",\n});\n\n[[nodiscard]] TextWithEntities ComposeTodoTasksList(\n\t\tint fullCount,\n\t\tconst std::vector<TextWithEntities> &names) {\n\tconst auto count = int(names.size());\n\tif (!count) {\n\t\treturn tr::lng_action_todo_tasks_fallback(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tfullCount,\n\t\t\ttr::marked);\n\t} else if (count == 1) {\n\t\treturn names.front();\n\t}\n\tauto full = names.front();\n\tfor (auto i = 1; i != count - 1; ++i) {\n\t\tfull = tr::lng_action_todo_tasks_and_one(\n\t\t\ttr::now,\n\t\t\tlt_tasks,\n\t\t\tfull,\n\t\t\tlt_task,\n\t\t\tnames[i],\n\t\t\ttr::marked);\n\t}\n\treturn tr::lng_action_todo_tasks_and_last(\n\t\ttr::now,\n\t\tlt_tasks,\n\t\tfull,\n\t\tlt_task,\n\t\tnames.back(),\n\t\ttr::marked);\n}\n\n} // namespace\n\nconst char kOptionFastButtonsMode[] = \"fast-buttons-mode\";\n\nbool FastButtonsMode() {\n\treturn FastButtonsModeOption.value();\n}\n\nvoid HistoryMessageVia::create(\n\t\tnot_null<Data::Session*> owner,\n\t\tUserId userId) {\n\tbot = owner->user(userId);\n\tmaxWidth = st::msgServiceNameFont->width(\n\t\ttr::lng_inline_bot_via(\n\t\t\ttr::now,\n\t\t\tlt_inline_bot,\n\t\t\t'@' + bot->username()));\n\tlink = std::make_shared<LambdaClickHandler>([bot = this->bot](\n\t\t\tClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tif (base::IsCtrlPressed()) {\n\t\t\t\tcontroller->showPeerInfo(bot);\n\t\t\t\treturn;\n\t\t\t} else if (!bot->isBot()\n\t\t\t\t|| bot->botInfo->inlinePlaceholder.isEmpty()) {\n\t\t\t\tcontroller->showPeerHistory(\n\t\t\t\t\tbot->id,\n\t\t\t\t\tWindow::SectionShow::Way::Forward);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tconst auto delegate = my.elementDelegate\n\t\t\t? my.elementDelegate()\n\t\t\t: nullptr;\n\t\tif (delegate) {\n\t\t\tdelegate->elementHandleViaClick(bot);\n\t\t}\n\t});\n}\n\nvoid HistoryMessageVia::resize(int32 availw) const {\n\tif (availw < 0) {\n\t\ttext = QString();\n\t\twidth = 0;\n\t} else {\n\t\ttext = tr::lng_inline_bot_via(\n\t\t\ttr::now,\n\t\t\tlt_inline_bot,\n\t\t\t'@' + bot->username());\n\t\tif (availw < maxWidth) {\n\t\t\ttext = st::msgServiceNameFont->elided(text, availw);\n\t\t\twidth = st::msgServiceNameFont->width(text);\n\t\t} else if (width < maxWidth) {\n\t\t\twidth = maxWidth;\n\t\t}\n\t}\n}\n\nHiddenSenderInfo::HiddenSenderInfo(\n\tconst QString &name,\n\tbool external,\n\tstd::optional<uint8> colorIndex)\n: name(name)\n, colorIndex(colorIndex.value_or(\n\tData::DecideColorIndex(Data::FakePeerIdForJustName(name))))\n, emptyUserpic(\n\tUi::EmptyUserpic::UserpicColor(this->colorIndex),\n\t(external\n\t\t? Ui::EmptyUserpic::ExternalName()\n\t\t: name)) {\n\tExpects(!name.isEmpty());\n\n\tconst auto parts = name.trimmed().split(' ', Qt::SkipEmptyParts);\n\tfirstName = parts[0];\n\tfor (const auto &part : parts.mid(1)) {\n\t\tif (!lastName.isEmpty()) {\n\t\t\tlastName.append(' ');\n\t\t}\n\t\tlastName.append(part);\n\t}\n}\n\nconst Ui::Text::String &HiddenSenderInfo::nameText() const {\n\tif (_nameText.isEmpty()) {\n\t\t_nameText.setText(st::msgNameStyle, name, Ui::NameTextOptions());\n\t}\n\treturn _nameText;\n}\n\nClickHandlerPtr HiddenSenderInfo::ForwardClickHandler() {\n\tstatic const auto hidden = std::make_shared<LambdaClickHandler>([](\n\t\t\tClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tconst auto weak = my.sessionWindow;\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->showToast(tr::lng_forwarded_hidden(tr::now));\n\t\t}\n\t});\n\treturn hidden;\n}\n\nbool HiddenSenderInfo::paintCustomUserpic(\n\t\tPainter &p,\n\t\tUi::PeerUserpicView &view,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size) const {\n\tExpects(!customUserpic.empty());\n\n\tauto valid = true;\n\tif (!customUserpic.isCurrentView(view.cloud)) {\n\t\tview.cloud = customUserpic.createView();\n\t\tvalid = false;\n\t}\n\tconst auto image = *view.cloud;\n\tif (image.isNull()) {\n\t\temptyUserpic.paintCircle(p, x, y, outerWidth, size);\n\t\treturn valid;\n\t}\n\tUi::ValidateUserpicCache(\n\t\tview,\n\t\timage.isNull() ? nullptr : &image,\n\t\timage.isNull() ? &emptyUserpic : nullptr,\n\t\tsize * style::DevicePixelRatio(),\n\t\tUi::PeerUserpicShape::Circle);\n\tp.drawImage(QRect(x, y, size, size), view.cached);\n\treturn valid;\n}\n\nvoid HistoryMessageForwarded::create(\n\t\tconst HistoryMessageVia *via,\n\t\tnot_null<const HistoryItem*> item) const {\n\tauto phrase = TextWithEntities();\n\tauto context = Core::TextContext({\n\t\t.session = &item->history()->session(),\n\t});\n\tconst auto fromChannel = originalSender\n\t\t&& originalSender->isChannel()\n\t\t&& !originalSender->isMegagroup();\n\tconst auto name = TextWithEntities{\n\t\t.text = (originalSender\n\t\t\t? originalSender->name()\n\t\t\t: originalHiddenSenderInfo->name)\n\t};\n\tif (const auto copy = originalSender) {\n\t\tcontext.repaint = [=] {\n\t\t\t// It is important to capture here originalSender by value,\n\t\t\t// not capture the HistoryMessageForwarded* and read the\n\t\t\t// originalSender field, because the components themselves\n\t\t\t// get moved from place to place and the captured `this`\n\t\t\t// pointer may become invalid, resulting in a crash.\n\t\t\tcopy->owner().requestItemRepaint(item);\n\t\t};\n\t\tphrase = Ui::Text::SingleCustomEmoji(\n\t\t\tcopy->owner().customEmojiManager().peerUserpicEmojiData(\n\t\t\t\tcopy,\n\t\t\t\tst::fwdTextUserpicPadding));\n\t}\n\tif (!originalPostAuthor.isEmpty()) {\n\t\tphrase.append(\n\t\t\ttr::lng_forwarded_signed(\n\t\t\t\ttr::now,\n\t\t\t\tlt_channel,\n\t\t\t\tname,\n\t\t\t\tlt_user,\n\t\t\t\t{ .text = originalPostAuthor },\n\t\t\t\ttr::marked));\n\t} else {\n\t\tphrase.append(name);\n\t}\n\tif (story) {\n\t\tphrase = tr::lng_forwarded_story(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\tUi::Text::Wrapped(phrase, EntityType::CustomUrl, QString()), // Link 1.\n\t\t\ttr::marked);\n\t} else if (via && psaType.isEmpty()) {\n\t\tconst auto linkData = tr::link(\n\t\t\tQString(),\n\t\t\t1).entities.front().data(); // Link 1.\n\t\tif (fromChannel) {\n\t\t\tphrase = tr::lng_forwarded_channel_via(\n\t\t\t\ttr::now,\n\t\t\t\tlt_channel,\n\t\t\t\tUi::Text::Wrapped(phrase, EntityType::CustomUrl, linkData), // Link 1.\n\t\t\t\tlt_inline_bot,\n\t\t\t\ttr::link('@' + via->bot->username(), 2), // Link 2.\n\t\t\t\ttr::marked);\n\t\t} else {\n\t\t\tphrase = tr::lng_forwarded_via(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tUi::Text::Wrapped(phrase, EntityType::CustomUrl, linkData), // Link 1.\n\t\t\t\tlt_inline_bot,\n\t\t\t\ttr::link('@' + via->bot->username(), 2), // Link 2.\n\t\t\t\ttr::marked);\n\t\t}\n\t} else {\n\t\tif (fromChannel || !psaType.isEmpty()) {\n\t\t\tauto custom = psaType.isEmpty()\n\t\t\t\t? QString()\n\t\t\t\t: Lang::GetNonDefaultValue(\n\t\t\t\t\tkPsaForwardedPrefix + psaType.toUtf8());\n\t\t\tif (!custom.isEmpty()) {\n\t\t\t\tcustom = custom.replace(\"{channel}\", phrase.text);\n\t\t\t\tconst auto index = int(custom.indexOf(phrase.text));\n\t\t\t\tconst auto size = int(phrase.text.size());\n\t\t\t\tphrase = TextWithEntities{\n\t\t\t\t\t.text = custom,\n\t\t\t\t\t.entities = {{ EntityType::CustomUrl, index, size, {} }},\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\tphrase = (psaType.isEmpty()\n\t\t\t\t\t? tr::lng_forwarded_channel\n\t\t\t\t\t: tr::lng_forwarded_psa_default)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_channel,\n\t\t\t\t\t\tUi::Text::Wrapped(\n\t\t\t\t\t\t\tphrase,\n\t\t\t\t\t\t\tEntityType::CustomUrl,\n\t\t\t\t\t\t\tQString()), // Link 1.\n\t\t\t\t\t\ttr::marked);\n\t\t\t}\n\t\t} else {\n\t\t\tphrase = tr::lng_forwarded(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tUi::Text::Wrapped(phrase, EntityType::CustomUrl, QString()), // Link 1.\n\t\t\t\ttr::marked);\n\t\t}\n\t}\n\tif (originalDate != TimeId(0)) {\n\t\tphrase.append(QString(\". Date: \")\n\t\t\t+ base::unixtime::parse(originalDate).toString(\n\t\t\t\tQLocale::system().dateTimeFormat(QLocale::ShortFormat)));\n\t}\n\ttext.setMarkedText(st::fwdTextStyle, phrase, kMarkupTextOptions, context);\n\n\ttext.setLink(1, fromChannel\n\t\t? JumpToMessageClickHandler(originalSender, originalId)\n\t\t: originalSender\n\t\t? originalSender->openLink()\n\t\t: HiddenSenderInfo::ForwardClickHandler());\n\tif (via) {\n\t\ttext.setLink(2, via->link);\n\t}\n}\n\nReplyFields ReplyFields::clone(not_null<HistoryItem*> parent) const {\n\treturn {\n\t\t.quote = quote,\n\t\t.externalMedia = (externalMedia\n\t\t\t? externalMedia->clone(parent)\n\t\t\t: nullptr),\n\t\t.externalSenderId = externalSenderId,\n\t\t.externalSenderName = externalSenderName,\n\t\t.externalPostAuthor = externalPostAuthor,\n\t\t.externalPeerId = externalPeerId,\n\t\t.messageId = messageId,\n\t\t.topMessageId = topMessageId,\n\t\t.storyId = storyId,\n\t\t.todoItemId = todoItemId,\n\t\t.pollOption = pollOption,\n\t\t.quoteOffset = quoteOffset,\n\t\t.manualQuote = manualQuote,\n\t\t.topicPost = topicPost,\n\t};\n}\n\nReplyFields ReplyFieldsFromMTP(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPMessageReplyHeader &reply) {\n\treturn reply.match([&](const MTPDmessageReplyHeader &data) {\n\t\tauto result = ReplyFields();\n\t\tif (const auto peer = data.vreply_to_peer_id()) {\n\t\t\tresult.externalPeerId = peerFromMTP(*peer);\n\t\t} else if (item->isAdminLogEntry()) {\n\t\t\tresult.externalPeerId = item->history()->peer->id;\n\t\t}\n\t\tconst auto owner = &item->history()->owner();\n\t\tif (const auto id = data.vreply_to_msg_id().value_or_empty()) {\n\t\t\tresult.messageId = data.is_reply_to_scheduled()\n\t\t\t\t? owner->session().scheduledMessages().localMessageId(id)\n\t\t\t\t: item->shortcutId()\n\t\t\t\t? owner->shortcutMessages().localMessageId(id)\n\t\t\t\t: id;\n\t\t\tresult.topMessageId\n\t\t\t\t= data.vreply_to_top_id().value_or(result.messageId.bare);\n\t\t\tresult.topicPost = data.is_forum_topic() ? 1 : 0;\n\t\t}\n\t\tresult.todoItemId = data.vtodo_item_id().value_or_empty();\n\t\tresult.pollOption = data.vpoll_option().value_or_empty();\n\t\tif (const auto header = data.vreply_from()) {\n\t\t\tconst auto &data = header->data();\n\t\t\tresult.externalPostAuthor\n\t\t\t\t= qs(data.vpost_author().value_or_empty());\n\t\t\tresult.externalSenderId = data.vfrom_id()\n\t\t\t\t? peerFromMTP(*data.vfrom_id())\n\t\t\t\t: PeerId();\n\t\t\tresult.externalSenderName\n\t\t\t\t= qs(data.vfrom_name().value_or_empty());\n\t\t}\n\t\tif (const auto media = data.vreply_media()) {\n\t\t\tresult.externalMedia = HistoryItem::CreateMedia(item, *media);\n\t\t}\n\t\tresult.quote = TextWithEntities{\n\t\t\tqs(data.vquote_text().value_or_empty()),\n\t\t\tApi::EntitiesFromMTP(\n\t\t\t\t&owner->session(),\n\t\t\t\tdata.vquote_entities().value_or_empty()),\n\t\t};\n\t\tresult.quoteOffset = data.vquote_offset().value_or_empty();\n\t\tresult.manualQuote = data.is_quote() ? 1 : 0;\n\t\treturn result;\n\t}, [&](const MTPDmessageReplyStoryHeader &data) {\n\t\treturn ReplyFields{\n\t\t\t.externalPeerId = peerFromMTP(data.vpeer()),\n\t\t\t.storyId = data.vstory_id().v,\n\t\t};\n\t});\n}\n\nFullReplyTo ReplyToFromMTP(\n\t\tnot_null<History*> history,\n\t\tconst MTPInputReplyTo &reply) {\n\treturn reply.match([&](const MTPDinputReplyToMessage &data) {\n\t\tauto result = FullReplyTo{\n\t\t\t.messageId = { history->peer->id, data.vreply_to_msg_id().v },\n\t\t};\n\t\tif (const auto peer = data.vreply_to_peer_id()) {\n\t\t\tconst auto parsed = Data::PeerFromInputMTP(\n\t\t\t\t&history->owner(),\n\t\t\t\t*peer);\n\t\t\tif (!parsed) {\n\t\t\t\treturn FullReplyTo();\n\t\t\t}\n\t\t\tresult.messageId.peer = parsed->id;\n\t\t}\n\t\tresult.topicRootId = data.vtop_msg_id().value_or_empty();\n\t\tresult.quote = TextWithEntities{\n\t\t\tqs(data.vquote_text().value_or_empty()),\n\t\t\tApi::EntitiesFromMTP(\n\t\t\t\t&history->session(),\n\t\t\t\tdata.vquote_entities().value_or_empty()),\n\t\t};\n\t\tresult.quoteOffset = data.vquote_offset().value_or_empty();\n\t\tresult.todoItemId = data.vtodo_item_id().value_or_empty();\n\t\tresult.pollOption = data.vpoll_option().value_or_empty();\n\t\treturn result;\n\t}, [&](const MTPDinputReplyToStory &data) {\n\t\tif (const auto parsed = Data::PeerFromInputMTP(\n\t\t\t\t&history->owner(),\n\t\t\t\tdata.vpeer())) {\n\t\t\treturn FullReplyTo{\n\t\t\t\t.storyId = { parsed->id, data.vstory_id().v },\n\t\t\t};\n\t\t}\n\t\treturn FullReplyTo();\n\t}, [&](const MTPDinputReplyToMonoForum &data) {\n\t\tconst auto parsed = Data::PeerFromInputMTP(\n\t\t\t&history->owner(),\n\t\t\tdata.vmonoforum_peer_id());\n\t\treturn FullReplyTo{\n\t\t\t.monoforumPeerId = parsed ? parsed->id : PeerId(),\n\t\t};\n\t});\n}\n\nHistoryMessageReply::HistoryMessageReply() = default;\n\nHistoryMessageReply &HistoryMessageReply::operator=(\n\tHistoryMessageReply &&other) = default;\n\nHistoryMessageReply::~HistoryMessageReply() {\n\t// clearData() should be called by holder.\n\tExpects(resolvedMessage.empty());\n\t_fields.externalMedia = nullptr;\n}\n\nvoid HistoryMessageReply::updateData(\n\t\tnot_null<HistoryItem*> holder,\n\t\tbool force) {\n\tconst auto guard = gsl::finally([&] { refreshReplyToMedia(); });\n\tif (!force) {\n\t\tif (resolvedMessage || resolvedStory || _unavailable) {\n\t\t\t_pendingResolve = 0;\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto peerId = _fields.externalPeerId\n\t\t? _fields.externalPeerId\n\t\t: holder->history()->peer->id;\n\tif (!resolvedMessage && _fields.messageId) {\n\t\tresolvedMessage = holder->history()->owner().message(\n\t\t\tpeerId,\n\t\t\t_fields.messageId);\n\t\tif (resolvedMessage) {\n\t\t\tif (resolvedMessage->isEmpty()) {\n\t\t\t\t// Really it is deleted.\n\t\t\t\tresolvedMessage = nullptr;\n\t\t\t\tforce = true;\n\t\t\t} else {\n\t\t\t\tholder->history()->owner().registerDependentMessage(\n\t\t\t\t\tholder,\n\t\t\t\t\tresolvedMessage.get());\n\t\t\t}\n\t\t}\n\t}\n\tif (!resolvedStory && _fields.storyId) {\n\t\tconst auto maybe = holder->history()->owner().stories().lookup({\n\t\t\tpeerId,\n\t\t\t_fields.storyId,\n\t\t});\n\t\tif (maybe) {\n\t\t\tresolvedStory = *maybe;\n\t\t\tholder->history()->owner().stories().registerDependentMessage(\n\t\t\t\tholder,\n\t\t\t\tresolvedStory.get());\n\t\t} else if (maybe.error() == Data::NoStory::Deleted) {\n\t\t\tforce = true;\n\t\t}\n\t}\n\n\tconst auto asExternal = displayAsExternal(holder);\n\tconst auto nonEmptyQuote = !_fields.quote.empty()\n\t\t&& (asExternal || _fields.manualQuote);\n\t_multiline = !_fields.storyId && (asExternal || nonEmptyQuote);\n\n\tconst auto displaying = resolvedMessage\n\t\t|| resolvedStory\n\t\t|| ((nonEmptyQuote || _fields.externalMedia)\n\t\t\t&& (!_fields.messageId || force));\n\t_displaying = displaying ? 1 : 0;\n\n\tconst auto unavailable = !resolvedMessage\n\t\t&& !resolvedStory\n\t\t&& ((!_fields.storyId && !_fields.messageId) || force);\n\t_unavailable = unavailable ? 1 : 0;\n\n\tif (force) {\n\t\tif (!_displaying && (_fields.messageId || _fields.storyId)) {\n\t\t\t_unavailable = 1;\n\t\t}\n\t\tholder->history()->owner().requestItemResize(holder);\n\t}\n\tif (resolvedMessage\n\t\t|| resolvedStory\n\t\t|| (!_fields.messageId && !_fields.storyId && external())\n\t\t|| _unavailable) {\n\t\t_pendingResolve = 0;\n\t} else if (!force) {\n\t\t_pendingResolve = 1;\n\t\t_requestedResolve = 0;\n\t}\n}\n\nvoid HistoryMessageReply::set(ReplyFields fields) {\n\t_fields = std::move(fields);\n}\n\nvoid HistoryMessageReply::updateFields(\n\t\tnot_null<HistoryItem*> holder,\n\t\tMsgId messageId,\n\t\tMsgId topMessageId,\n\t\tbool topicPost) {\n\t_fields.topicPost = topicPost ? 1 : 0;\n\tif ((_fields.messageId != messageId)\n\t\t&& !IsServerMsgId(_fields.messageId)) {\n\t\t_fields.messageId = messageId;\n\t\tupdateData(holder);\n\t}\n\tif ((_fields.topMessageId != topMessageId)\n\t\t&& !IsServerMsgId(_fields.topMessageId)) {\n\t\t_fields.topMessageId = topMessageId;\n\t}\n}\n\nbool HistoryMessageReply::acquireResolve() {\n\tif (!_pendingResolve || _requestedResolve) {\n\t\treturn false;\n\t}\n\t_requestedResolve = 1;\n\treturn true;\n}\n\nvoid HistoryMessageReply::setTopMessageId(MsgId topMessageId) {\n\t_fields.topMessageId = topMessageId;\n}\n\nvoid HistoryMessageReply::clearData(not_null<HistoryItem*> holder) {\n\tif (resolvedMessage) {\n\t\tholder->history()->owner().unregisterDependentMessage(\n\t\t\tholder,\n\t\t\tresolvedMessage.get());\n\t\tresolvedMessage = nullptr;\n\t}\n\tif (resolvedStory) {\n\t\tholder->history()->owner().stories().unregisterDependentMessage(\n\t\t\tholder,\n\t\t\tresolvedStory.get());\n\t\tresolvedStory = nullptr;\n\t}\n\t_unavailable = 1;\n\t_displaying = 0;\n\tif (_multiline) {\n\t\tholder->history()->owner().requestItemResize(holder);\n\t\t_multiline = 0;\n\t}\n\trefreshReplyToMedia();\n}\n\nbool HistoryMessageReply::external() const {\n\treturn _fields.externalPeerId\n\t\t|| _fields.externalSenderId\n\t\t|| !_fields.externalSenderName.isEmpty();\n}\n\nbool HistoryMessageReply::displayAsExternal(\n\t\tnot_null<HistoryItem*> holder) const {\n\t// Don't display replies that could be local as external.\n\treturn external()\n\t\t&& (!resolvedMessage\n\t\t\t|| (holder->history() != resolvedMessage->history())\n\t\t\t|| (holder->topicRootId() != resolvedMessage->topicRootId()));\n}\n\nvoid HistoryMessageReply::itemRemoved(\n\t\tnot_null<HistoryItem*> holder,\n\t\tnot_null<HistoryItem*> removed) {\n\tif (resolvedMessage.get() == removed) {\n\t\tclearData(holder);\n\t\tholder->history()->owner().requestItemResize(holder);\n\t}\n}\n\nvoid HistoryMessageReply::storyRemoved(\n\t\tnot_null<HistoryItem*> holder,\n\t\tnot_null<Data::Story*> removed) {\n\tif (resolvedStory.get() == removed) {\n\t\tclearData(holder);\n\t\tholder->history()->owner().requestItemResize(holder);\n\t}\n}\n\nvoid HistoryMessageReply::refreshReplyToMedia() {\n\treplyToDocumentId = 0;\n\treplyToWebPageId = 0;\n\tif (const auto media = resolvedMessage\n\t\t\t? resolvedMessage->media()\n\t\t\t: nullptr) {\n\t\tif (const auto document = media->document()) {\n\t\t\treplyToDocumentId = document->id;\n\t\t} else if (const auto webpage = media->webpage()) {\n\t\t\treplyToWebPageId = webpage->id;\n\t\t}\n\t}\n}\n\nReplyMarkupClickHandler::ReplyMarkupClickHandler(\n\tnot_null<Data::Session*> owner,\n\tint row,\n\tint column,\n\tFullMsgId context)\n: _owner(owner)\n, _itemId(context)\n, _row(row)\n, _column(column) {\n}\n\nQString ReplyMarkupClickHandler::dragText() const {\n\tconst auto button = getUrlButton();\n\treturn button ? QString::fromUtf8(button->data) : QString();\n}\n\n// Copy to clipboard support.\nQString ReplyMarkupClickHandler::copyToClipboardText() const {\n\tconst auto button = getUrlButton();\n\treturn button ? QString::fromUtf8(button->data) : QString();\n}\n\nQString ReplyMarkupClickHandler::copyToClipboardContextItemText() const {\n\tconst auto button = getUrlButton();\n\treturn button ? tr::lng_context_copy_link(tr::now) : QString();\n}\n\n// Finds the corresponding button in the items markup struct.\n// If the button is not found it returns nullptr.\n// Note: it is possible that we will point to the different button\n// than the one was used when constructing the handler, but not a big deal.\nconst HistoryMessageMarkupButton *ReplyMarkupClickHandler::getButton() const {\n\treturn HistoryMessageMarkupButton::Get(_owner, _itemId, _row, _column);\n}\n\nauto ReplyMarkupClickHandler::getUrlButton() const\n-> const HistoryMessageMarkupButton* {\n\tif (const auto button = getButton()) {\n\t\tusing Type = HistoryMessageMarkupButton::Type;\n\t\tif (button->type == Type::Url || button->type == Type::Auth) {\n\t\t\treturn button;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid ReplyMarkupClickHandler::onClick(ClickContext context) const {\n\tif (context.button != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tauto my = context.other.value<ClickHandlerContext>();\n\tmy.itemId = _itemId;\n\tApi::ActivateBotCommand(my, _row, _column);\n}\n\n// Returns the full text of the corresponding button.\nQString ReplyMarkupClickHandler::buttonText() const {\n\tif (const auto button = getButton()) {\n\t\treturn button->text;\n\t}\n\treturn QString();\n}\n\nQString ReplyMarkupClickHandler::tooltip() const {\n\tif (const auto button = getButton()) {\n\t\tif (button->type == HistoryMessageMarkupButton::Type::CopyText) {\n\t\t\treturn tr::lng_bot_copy_text_tooltip(\n\t\t\t\ttr::now,\n\t\t\t\tlt_text,\n\t\t\t\tst::wrap_rtl(QString::fromUtf8(button->data)));\n\t\t}\n\t}\n\tconst auto button = getUrlButton();\n\tconst auto url = button ? QString::fromUtf8(button->data) : QString();\n\tconst auto text = _fullDisplayed ? QString() : buttonText();\n\tif (!url.isEmpty() && !text.isEmpty()) {\n\t\treturn QString(\"%1\\n\\n%2\").arg(text, url);\n\t} else if (url.isEmpty() != text.isEmpty()) {\n\t\treturn text + url;\n\t} else {\n\t\treturn QString();\n\t}\n}\n\nReplyKeyboard::Button::Button() = default;\nReplyKeyboard::Button::Button(Button &&other) = default;\nReplyKeyboard::Button &ReplyKeyboard::Button::operator=(\n\tButton &&other) = default;\nReplyKeyboard::Button::~Button() = default;\n\nReplyKeyboard::ReplyKeyboard(\n\tnot_null<const HistoryItem*> item,\n\tstd::unique_ptr<Style> &&s)\n: _item(item)\n, _selectedAnimation([=](crl::time now) {\n\treturn selectedAnimationCallback(now);\n})\n, _st(std::move(s)) {\n\tif (const auto markup = _item->Get<HistoryMessageReplyMarkup>()) {\n\t\tconst auto owner = &_item->history()->owner();\n\t\tconst auto session = &owner->session();\n\t\tconst auto context = _item->fullId();\n\t\tconst auto rowCount = int(markup->data.rows.size());\n\t\t_rows.reserve(rowCount);\n\t\tfor (auto i = 0; i != rowCount; ++i) {\n\t\t\tconst auto &row = markup->data.rows[i];\n\t\t\tconst auto rowSize = int(row.size());\n\t\t\tauto newRow = std::vector<Button>();\n\t\t\tnewRow.reserve(rowSize);\n\t\t\tfor (auto j = 0; j != rowSize; ++j) {\n\t\t\t\tauto button = Button();\n\t\t\t\tusing Type = HistoryMessageMarkupButton::Type;\n\t\t\t\tstatic const auto RegExp = QRegularExpression(\"\\\\b\"\n\t\t\t\t\t+ Ui::kCreditsCurrency\n\t\t\t\t\t+ \"\\\\b\");\n\t\t\t\tconst auto type = row[j].type;\n\t\t\t\tconst auto text = (type == Type::Buy)\n\t\t\t\t\t? base::duplicate(row[j].text).replace(\n\t\t\t\t\t\tRegExp,\n\t\t\t\t\t\tQChar(0x2B50))\n\t\t\t\t\t: row[j].text;\n\t\t\t\tconst auto withEmoji = [&](const style::IconEmoji &icon) {\n\t\t\t\t\treturn Ui::Text::IconEmoji(&icon).append(text);\n\t\t\t\t};\n\t\t\t\tconst auto textWithEntities = [&] {\n\t\t\t\t\tif (type == Type::SuggestAccept) {\n\t\t\t\t\t\treturn withEmoji(st::chatSuggestAcceptIcon);\n\t\t\t\t\t} else if (type == Type::SuggestDecline) {\n\t\t\t\t\t\treturn withEmoji(st::chatSuggestDeclineIcon);\n\t\t\t\t\t} else if (type == Type::SuggestChange) {\n\t\t\t\t\t\treturn withEmoji(st::chatSuggestChangeIcon);\n\t\t\t\t\t}\n\t\t\t\t\tauto result = TextWithEntities();\n\t\t\t\t\tif (const auto iconId = row[j].visual.iconId) {\n\t\t\t\t\t\tusing namespace Data;\n\t\t\t\t\t\tresult.append(SingleCustomEmoji(iconId));\n\t\t\t\t\t\tif (!text.isEmpty()) {\n\t\t\t\t\t\t\tresult.append(' ');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (type == Type::Buy) {\n\t\t\t\t\t\tauto firstPart = true;\n\t\t\t\t\t\tfor (const auto &part : text.split(QChar(0x2B50))) {\n\t\t\t\t\t\t\tif (!firstPart) {\n\t\t\t\t\t\t\t\tresult.append(Ui::Text::IconEmoji(\n\t\t\t\t\t\t\t\t\t&st::starIconEmojiLarge));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tresult.append(part);\n\t\t\t\t\t\t\tfirstPart = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (!result.entities.empty()) {\n\t\t\t\t\t\tresult.append(text);\n\t\t\t\t\t}\n\t\t\t\t\treturn result.entities.empty()\n\t\t\t\t\t\t? TextWithEntities()\n\t\t\t\t\t\t: result;\n\t\t\t\t}();\n\t\t\t\tbutton.type = type;\n\t\t\t\tbutton.link = std::make_shared<ReplyMarkupClickHandler>(\n\t\t\t\t\towner,\n\t\t\t\t\ti,\n\t\t\t\t\tj,\n\t\t\t\t\tcontext);\n\t\t\t\tif (!textWithEntities.text.isEmpty()) {\n\t\t\t\t\tbutton.text.setMarkedText(\n\t\t\t\t\t\t_st->textStyle(),\n\t\t\t\t\t\tTextUtilities::SingleLine(textWithEntities),\n\t\t\t\t\t\tkMarkupTextOptions,\n\t\t\t\t\t\tCore::TextContext({\n\t\t\t\t\t\t\t.session = session,\n\t\t\t\t\t\t\t.repaint = [=] { _st->repaint(_item); },\n\t\t\t\t\t\t}));\n\t\t\t\t} else {\n\t\t\t\t\tbutton.text.setText(\n\t\t\t\t\t\t_st->textStyle(),\n\t\t\t\t\t\tTextUtilities::SingleLine(text),\n\t\t\t\t\t\tkPlainTextOptions);\n\t\t\t\t}\n\t\t\t\tbutton.characters = text.isEmpty() ? 1 : text.size();\n\t\t\t\tbutton.color = row[j].visual.color;\n\t\t\t\tnewRow.push_back(std::move(button));\n\t\t\t}\n\t\t\t_rows.push_back(std::move(newRow));\n\t\t}\n\t}\n}\n\nvoid ReplyKeyboard::updateMessageId() {\n\tconst auto msgId = _item->fullId();\n\tfor (const auto &row : _rows) {\n\t\tfor (const auto &button : row) {\n\t\t\tbutton.link->setMessageId(msgId);\n\t\t}\n\t}\n}\n\nvoid ReplyKeyboard::resize(int width, int height) {\n\t_width = width;\n\n\tauto y = 0.;\n\tauto buttonHeight = _rows.empty()\n\t\t? float64(_st->buttonHeight())\n\t\t: (float64(height + _st->buttonSkip()) / _rows.size());\n\tfor (auto &row : _rows) {\n\t\tauto s = int(row.size());\n\n\t\tauto widthForButtons = _width - ((s - 1) * _st->buttonSkip());\n\t\tauto widthForText = widthForButtons;\n\t\tauto widthOfText = 0;\n\t\tauto maxMinButtonWidth = 0;\n\t\tfor (const auto &button : row) {\n\t\t\twidthOfText += qMax(button.text.maxWidth(), 1);\n\t\t\tint minButtonWidth = _st->minButtonWidth(button.type);\n\t\t\twidthForText -= minButtonWidth;\n\t\t\taccumulate_max(maxMinButtonWidth, minButtonWidth);\n\t\t}\n\t\tconst auto exact = (widthForText == widthOfText);\n\t\tconst auto enough\n\t\t\t= (widthForButtons - s * maxMinButtonWidth) >= widthOfText;\n\n\t\tauto x = 0.;\n\t\tfor (auto &button : row) {\n\t\t\tint buttonw = qMax(button.text.maxWidth(), 1);\n\t\t\tfloat64 textw = buttonw, minw = _st->minButtonWidth(button.type);\n\t\t\tfloat64 w = textw;\n\t\t\tif (exact) {\n\t\t\t\tw += minw;\n\t\t\t} else if (enough) {\n\t\t\t\tw = (widthForButtons / float64(s));\n\t\t\t\ttextw = w - minw;\n\t\t\t} else {\n\t\t\t\ttextw = (widthForText / float64(s));\n\t\t\t\tw = minw + textw;\n\t\t\t\taccumulate_max(w, 2 * float64(_st->buttonPadding()));\n\t\t\t}\n\n\t\t\tconst auto rectx = static_cast<int>(std::floor(x));\n\t\t\tconst auto rectw = static_cast<int>(std::floor(x + w)) - rectx;\n\t\t\tbutton.rect = QRect(\n\t\t\t\trectx,\n\t\t\t\tqRound(y),\n\t\t\t\trectw,\n\t\t\t\tqRound(buttonHeight - _st->buttonSkip()));\n\t\t\tif (rtl()) {\n\t\t\t\tbutton.rect.setX(\n\t\t\t\t\t_width - button.rect.x() - button.rect.width());\n\t\t\t}\n\t\t\tx += w + _st->buttonSkip();\n\n\t\t\tbutton.link->setFullDisplayed(textw >= buttonw);\n\t\t}\n\t\ty += buttonHeight;\n\t}\n}\n\nbool ReplyKeyboard::isEnoughSpace(\n\t\tint width,\n\t\tconst style::BotKeyboardButton &st) const {\n\tfor (const auto &row : _rows) {\n\t\tauto s = int(row.size());\n\t\tauto widthLeft = width - ((s - 1) * st.margin + s * 2 * st.padding);\n\t\tfor (const auto &button : row) {\n\t\t\twidthLeft -= qMax(button.text.maxWidth(), 1);\n\t\t\tif (widthLeft < 0) {\n\t\t\t\tif (row.size() > 3) {\n\t\t\t\t\treturn false;\n\t\t\t\t} else {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid ReplyKeyboard::setStyle(std::unique_ptr<Style> &&st) {\n\t_st = std::move(st);\n}\n\nint ReplyKeyboard::naturalWidth() const {\n\tauto result = 0;\n\tfor (const auto &row : _rows) {\n\t\tauto maxMinButtonWidth = 0;\n\t\tfor (const auto &button : row) {\n\t\t\taccumulate_max(\n\t\t\t\tmaxMinButtonWidth,\n\t\t\t\t_st->minButtonWidth(button.type));\n\t\t}\n\t\tauto rowMaxButtonWidth = 0;\n\t\tfor (const auto &button : row) {\n\t\t\taccumulate_max(\n\t\t\t\trowMaxButtonWidth,\n\t\t\t\tqMax(button.text.maxWidth(), 1) + maxMinButtonWidth);\n\t\t}\n\n\t\tconst auto rowSize = int(row.size());\n\t\taccumulate_max(\n\t\t\tresult,\n\t\t\trowSize * rowMaxButtonWidth + (rowSize - 1) * _st->buttonSkip());\n\t}\n\treturn result;\n}\n\nint ReplyKeyboard::naturalHeight() const {\n\treturn (_rows.size() - 1) * _st->buttonSkip()\n\t\t+ _rows.size() * _st->buttonHeight();\n}\n\nvoid ReplyKeyboard::paint(\n\t\tPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tUi::BubbleRounding rounding,\n\t\tint outerWidth,\n\t\tconst QRect &clip,\n\t\tbool paused) const {\n\tAssert(_st != nullptr);\n\tAssert(_width > 0);\n\n\tauto number = hasFastButtonMode() ? 1 : 0;\n\tfor (auto y = 0, rowsCount = int(_rows.size()); y != rowsCount; ++y) {\n\t\tfor (auto x = 0, count = int(_rows[y].size()); x != count; ++x) {\n\t\t\tconst auto guard = gsl::finally([&] { if (number) ++number; });\n\t\t\tconst auto &button = _rows[y][x];\n\t\t\tconst auto rect = button.rect;\n\t\t\tif (rect.y() >= clip.y() + clip.height()) {\n\t\t\t\treturn;\n\t\t\t} else if (rect.y() + rect.height() < clip.y()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// just ignore the buttons that didn't layout well\n\t\t\tif (rect.x() + rect.width() > _width) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tauto buttonRounding = Ui::BubbleRounding();\n\t\t\tusing Corner = Ui::BubbleCornerRounding;\n\t\t\tbuttonRounding.topLeft = ((!y)\n\t\t\t\t&& !x\n\t\t\t\t&& !st\n\t\t\t\t&& (rounding.topLeft == Corner::Large))\n\t\t\t\t? Corner::Large\n\t\t\t\t: Corner::Small;\n\t\t\tbuttonRounding.topRight = ((!y)\n\t\t\t\t&& (x + 1 == count)\n\t\t\t\t&& !st\n\t\t\t\t&& (rounding.topRight == Corner::Large))\n\t\t\t\t? Corner::Large\n\t\t\t\t: Corner::Small;\n\t\t\tbuttonRounding.bottomLeft = ((y + 1 == rowsCount)\n\t\t\t\t&& !x\n\t\t\t\t&& (rounding.bottomLeft == Corner::Large))\n\t\t\t\t? Corner::Large\n\t\t\t\t: Corner::Small;\n\t\t\tbuttonRounding.bottomRight = ((y + 1 == rowsCount)\n\t\t\t\t&& (x + 1 == count)\n\t\t\t\t&& (rounding.bottomRight == Corner::Large))\n\t\t\t\t? Corner::Large\n\t\t\t\t: Corner::Small;\n\t\t\t_st->paintButton(\n\t\t\t\tp,\n\t\t\t\tst,\n\t\t\t\touterWidth,\n\t\t\t\tbutton,\n\t\t\t\tbuttonRounding,\n\t\t\t\tpaused);\n\n\t\t\tif (number) {\n\t\t\t\tp.setFont(st::dialogsUnreadFont);\n\t\t\t\tp.setPen(st->msgServiceFg());\n\t\t\t\tp.drawText(\n\t\t\t\t\trect.x() + st::msgBotKbIconPadding,\n\t\t\t\t\trect.y() + st::dialogsUnreadFont->ascent,\n\t\t\t\t\tQString::number(number));\n\t\t\t}\n\t\t}\n\t}\n}\n\nbool ReplyKeyboard::hasFastButtonMode() const {\n\treturn FastButtonsMode()\n\t\t&& _item->inlineReplyKeyboard()\n\t\t&& (_item == _item->history()->lastMessage())\n\t\t&& _item->history()->session().fastButtonsBots().enabled(\n\t\t\t_item->history()->peer);\n}\n\nClickHandlerPtr ReplyKeyboard::getLink(QPoint point) const {\n\tAssert(_width > 0);\n\n\tfor (const auto &row : _rows) {\n\t\tfor (const auto &button : row) {\n\t\t\tQRect rect(button.rect);\n\n\t\t\t// just ignore the buttons that didn't layout well\n\t\t\tif (rect.x() + rect.width() > _width) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (rect.contains(point)) {\n\t\t\t\tif (_item->isAdminLogEntry()\n\t\t\t\t\t&& button.type != HistoryMessageMarkupButton::Type::Url) {\n\t\t\t\t\treturn ClickHandlerPtr();\n\t\t\t\t}\n\t\t\t\t_savedCoords = point;\n\t\t\t\treturn button.link;\n\t\t\t}\n\t\t}\n\t}\n\treturn ClickHandlerPtr();\n}\n\nClickHandlerPtr ReplyKeyboard::getLinkByIndex(int index) const {\n\tauto number = 1;\n\tfor (const auto &row : _rows) {\n\t\tfor (const auto &button : row) {\n\t\t\tif (number == index + 1) {\n\t\t\t\treturn button.link;\n\t\t\t}\n\t\t\t++number;\n\t\t}\n\t}\n\treturn ClickHandlerPtr();\n}\n\nvoid ReplyKeyboard::clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool active) {\n\tif (!p) {\n\t\treturn;\n\t}\n\n\t_savedActive = active ? p : ClickHandlerPtr();\n\tauto coords = findButtonCoordsByClickHandler(p);\n\tif (coords.i >= 0 && _savedPressed != p) {\n\t\tstartAnimation(coords.i, coords.j, active ? 1 : -1);\n\t}\n}\n\nReplyKeyboard::ButtonCoords ReplyKeyboard::findButtonCoordsByClickHandler(\n\t\tconst ClickHandlerPtr &p) {\n\tfor (int i = 0, rows = _rows.size(); i != rows; ++i) {\n\t\tauto &row = _rows[i];\n\t\tfor (int j = 0, cols = row.size(); j != cols; ++j) {\n\t\t\tif (row[j].link == p) {\n\t\t\t\treturn { i, j };\n\t\t\t}\n\t\t}\n\t}\n\treturn { -1, -1 };\n}\n\nvoid ReplyKeyboard::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed,\n\t\tUi::BubbleRounding rounding) {\n\tif (!handler) {\n\t\treturn;\n\t}\n\n\t_savedPressed = pressed ? handler : ClickHandlerPtr();\n\tauto coords = findButtonCoordsByClickHandler(handler);\n\tif (coords.i >= 0) {\n\t\tauto &button = _rows[coords.i][coords.j];\n\t\tif (pressed) {\n\t\t\tif (!button.ripple) {\n\t\t\t\tconst auto sides = RectPart()\n\t\t\t\t\t| (!coords.i ? RectPart::Top : RectPart())\n\t\t\t\t\t| (!coords.j ? RectPart::Left : RectPart())\n\t\t\t\t\t| ((coords.i + 1 == _rows.size())\n\t\t\t\t\t\t? RectPart::Bottom\n\t\t\t\t\t\t: RectPart())\n\t\t\t\t\t| ((coords.j + 1 == _rows[coords.i].size())\n\t\t\t\t\t\t? RectPart::Right\n\t\t\t\t\t\t: RectPart());\n\t\t\t\tauto mask = Ui::RippleAnimation::RoundRectMask(\n\t\t\t\t\tbutton.rect.size(),\n\t\t\t\t\t_st->buttonRounding(rounding, sides));\n\t\t\t\tbutton.ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\t\t_st->_st->ripple,\n\t\t\t\t\tstd::move(mask),\n\t\t\t\t\t[=] { _st->repaint(_item); });\n\t\t\t}\n\t\t\tbutton.ripple->add(_savedCoords - button.rect.topLeft());\n\t\t} else {\n\t\t\tif (button.ripple) {\n\t\t\t\tbutton.ripple->lastStop();\n\t\t\t}\n\t\t\tif (_savedActive != handler) {\n\t\t\t\tstartAnimation(coords.i, coords.j, -1);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ReplyKeyboard::startAnimation(int i, int j, int direction) {\n\tauto notStarted = _animations.empty();\n\n\tint indexForAnimation = Layout::PositionToIndex(i, j + 1) * direction;\n\n\t_animations.remove(-indexForAnimation);\n\tif (!_animations.contains(indexForAnimation)) {\n\t\t_animations.emplace(indexForAnimation, crl::now());\n\t}\n\n\tif (notStarted && !_selectedAnimation.animating()) {\n\t\t_selectedAnimation.start();\n\t}\n}\n\nbool ReplyKeyboard::selectedAnimationCallback(crl::time now) {\n\tif (anim::Disabled()) {\n\t\tnow += st::botKbDuration;\n\t}\n\tfor (auto i = _animations.begin(); i != _animations.end();) {\n\t\tconst auto index = std::abs(i->first) - 1;\n\t\tconst auto &[row, col] = Layout::IndexToPosition(index);\n\t\tconst auto dt = float64(now - i->second) / st::botKbDuration;\n\t\tif (dt >= 1) {\n\t\t\t_rows[row][col].howMuchOver = (i->first > 0) ? 1 : 0;\n\t\t\ti = _animations.erase(i);\n\t\t} else {\n\t\t\t_rows[row][col].howMuchOver = (i->first > 0) ? dt : (1 - dt);\n\t\t\t++i;\n\t\t}\n\t}\n\t_st->repaint(_item);\n\treturn !_animations.empty();\n}\n\nvoid ReplyKeyboard::clearSelection() {\n\tfor (const auto &[relativeIndex, time] : _animations) {\n\t\tconst auto index = std::abs(relativeIndex) - 1;\n\t\tconst auto &[row, col] = Layout::IndexToPosition(index);\n\t\t_rows[row][col].howMuchOver = 0;\n\t}\n\t_animations.clear();\n\t_selectedAnimation.stop();\n}\n\nint ReplyKeyboard::Style::buttonSkip() const {\n\treturn _st->margin;\n}\n\nint ReplyKeyboard::Style::buttonPadding() const {\n\treturn _st->padding;\n}\n\nint ReplyKeyboard::Style::buttonHeight() const {\n\treturn _st->height;\n}\n\nvoid ReplyKeyboard::Style::paintButton(\n\t\tPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tint outerWidth,\n\t\tconst ReplyKeyboard::Button &button,\n\t\tUi::BubbleRounding rounding,\n\t\tbool paused) const {\n\tconst auto &rect = button.rect;\n\tpaintButtonBg(p, st, rect, button.color, rounding, button.howMuchOver);\n\tif (button.ripple) {\n\t\tconst auto color = st\n\t\t\t? &st->msgBotKbRippleBg()->c\n\t\t\t: (button.color != HistoryMessageMarkupButton::Color::Normal)\n\t\t\t? &st::shadowFg->c\n\t\t\t: nullptr;\n\t\tbutton.ripple->paint(p, rect.x(), rect.y(), outerWidth, color);\n\t\tif (button.ripple->empty()) {\n\t\t\tbutton.ripple.reset();\n\t\t}\n\t}\n\tpaintButtonIcon(p, st, rect, outerWidth, button.type);\n\tif (button.type == HistoryMessageMarkupButton::Type::CallbackWithPassword\n\t\t|| button.type == HistoryMessageMarkupButton::Type::Callback\n\t\t|| button.type == HistoryMessageMarkupButton::Type::Game) {\n\t\tif (const auto data = button.link->getButton()) {\n\t\t\tif (data->requestId) {\n\t\t\t\tpaintButtonLoading(\n\t\t\t\t\tp,\n\t\t\t\t\tst,\n\t\t\t\t\trect,\n\t\t\t\t\tbutton.color,\n\t\t\t\t\touterWidth,\n\t\t\t\t\trounding);\n\t\t\t}\n\t\t}\n\t}\n\n\tint tx = rect.x(), tw = rect.width();\n\tif (tw >= st::botKbStyle.font->elidew + _st->padding * 2) {\n\t\ttx += _st->padding;\n\t\ttw -= _st->padding * 2;\n\t} else if (tw > st::botKbStyle.font->elidew) {\n\t\ttx += (tw - st::botKbStyle.font->elidew) / 2;\n\t\ttw = st::botKbStyle.font->elidew;\n\t}\n\tpaintButtonStart(p, st, button.color);\n\tbutton.text.draw(p, {\n\t\t.position = {\n\t\t\ttx,\n\t\t\trect.y() + _st->textTop + ((rect.height() - _st->height) / 2),\n\t\t},\n\t\t.availableWidth = tw,\n\t\t.align = style::al_top,\n\t\t.paused = paused || On(PowerSaving::kEmojiChat),\n\t\t.elisionLines = 1,\n\t});\n\tif (button.type == HistoryMessageMarkupButton::Type::SimpleWebView) {\n\t\tconst auto &icon = st::markupWebview;\n\t\tst::markupWebview.paint(\n\t\t\tp,\n\t\t\trect::right(rect) - icon.width() - _st->padding / 2,\n\t\t\trect.y() + _st->padding / 2,\n\t\t\trect.width(),\n\t\t\tp.pen().color());\n\t}\n}\n\nvoid HistoryMessageReplyMarkup::createForwarded(\n\t\tconst HistoryMessageReplyMarkup &original) {\n\tExpects(!inlineKeyboard);\n\n\tdata.fillForwardedData(original.data);\n}\n\nvoid HistoryMessageReplyMarkup::updateData(\n\t\tHistoryMessageMarkupData &&markup) {\n\tdata = std::move(markup);\n\tinlineKeyboard = nullptr;\n}\n\nbool HistoryMessageReplyMarkup::hiddenBy(Data::Media *media) const {\n\tif (media && (data.flags & ReplyMarkupFlag::OnlyBuyButton)) {\n\t\tif (const auto invoice = media->invoice()) {\n\t\t\tif (HasUnpaidMedia(*invoice)\n\t\t\t\t|| (HasExtendedMedia(*invoice) && !invoice->receiptMsgId)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid HistoryMessageReplyMarkup::updateSuggestControls(\n\t\tSuggestionActions actions) {\n\tif (actions == SuggestionActions::AcceptAndDecline\n\t\t|| actions == SuggestionActions::GiftOfferActions\n\t\t|| actions == SuggestionActions::NoForwardsRequest) {\n\t\tdata.flags |= ReplyMarkupFlag::SuggestionAccept;\n\t} else {\n\t\tdata.flags &= ~ReplyMarkupFlag::SuggestionAccept;\n\t}\n\tif (actions == SuggestionActions::None) {\n\t\tdata.flags &= ~ReplyMarkupFlag::SuggestionDecline;\n\t} else {\n\t\tdata.flags |= ReplyMarkupFlag::Inline\n\t\t\t| ReplyMarkupFlag::SuggestionDecline;\n\t}\n\tusing Type = HistoryMessageMarkupButton::Type;\n\tusing Visual = HistoryMessageMarkupButton::Visual;\n\tconst auto has = [&](Type type) {\n\t\treturn !data.rows.empty()\n\t\t\t&& ranges::contains(\n\t\t\t\tdata.rows.back(),\n\t\t\t\ttype,\n\t\t\t\t&HistoryMessageMarkupButton::type);\n\t};\n\tif (actions == SuggestionActions::GiftOfferActions) {\n\t\tif (has(Type::SuggestAccept)) {\n\t\t\t// Nothing changed.\n\t\t}\n\t\tdata.rows.push_back({\n\t\t\t{\n\t\t\t\tType::SuggestDecline,\n\t\t\t\ttr::lng_action_gift_offer_decline(tr::now),\n\t\t\t\tVisual(),\n\t\t\t},\n\t\t\t{\n\t\t\t\tType::SuggestAccept,\n\t\t\t\ttr::lng_action_gift_offer_accept(tr::now),\n\t\t\t\tVisual(),\n\t\t\t},\n\t\t});\n\t} else if (actions == SuggestionActions::NoForwardsRequest) {\n\t\tdata.rows.push_back({\n\t\t\t{\n\t\t\t\tType::SuggestDecline,\n\t\t\t\ttr::lng_action_no_forwards_reject(tr::now),\n\t\t\t\tVisual(),\n\t\t\t},\n\t\t\t{\n\t\t\t\tType::SuggestAccept,\n\t\t\t\ttr::lng_action_no_forwards_accept(tr::now),\n\t\t\t\tVisual(),\n\t\t\t},\n\t\t});\n\t} else if (actions == SuggestionActions::AcceptAndDecline) {\n\t\t//     ... rows ...\n\t\t// [decline] | [accept]\n\t\t//   [suggestchanges]\n\t\tif (has(Type::SuggestChange)) {\n\t\t\t// Nothing changed.\n\t\t} else {\n\t\t\tif (has(Type::SuggestDecline)) {\n\t\t\t\tdata.rows.pop_back();\n\t\t\t}\n\t\t\tdata.rows.push_back({\n\t\t\t\t{\n\t\t\t\t\tType::SuggestDecline,\n\t\t\t\t\ttr::lng_suggest_action_decline(tr::now),\n\t\t\t\t\tVisual(),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tType::SuggestAccept,\n\t\t\t\t\ttr::lng_suggest_action_accept(tr::now),\n\t\t\t\t\tVisual(),\n\t\t\t\t},\n\t\t\t});\n\t\t\tdata.rows.push_back({ {\n\t\t\t\tType::SuggestChange,\n\t\t\t\ttr::lng_suggest_action_change(tr::now),\n\t\t\t\tVisual(),\n\t\t\t} });\n\t\t\tdata.flags |= ReplyMarkupFlag::SuggestionAccept\n\t\t\t\t| ReplyMarkupFlag::SuggestionDecline;\n\t\t}\n\t\tif (data.rows.size() > 2) {\n\t\t\tdata.flags |= ReplyMarkupFlag::SuggestionSeparator;\n\t\t} else {\n\t\t\tdata.flags &= ~ReplyMarkupFlag::SuggestionSeparator;\n\t\t}\n\t} else {\n\t\twhile (!data.rows.empty()) {\n\t\t\tif (has(Type::SuggestChange) || has(Type::SuggestAccept)) {\n\t\t\t\tdata.rows.pop_back();\n\t\t\t} else if (has(Type::SuggestDecline)\n\t\t\t\t&& actions == SuggestionActions::None) {\n\t\t\t\tdata.rows.pop_back();\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tdata.flags &= ~ReplyMarkupFlag::SuggestionAccept;\n\t\tif (actions == SuggestionActions::None) {\n\t\t\tdata.flags &= ~ReplyMarkupFlag::SuggestionDecline;\n\t\t\tdata.flags &= ~ReplyMarkupFlag::SuggestionSeparator;\n\t\t} else {\n\t\t\tif (!has(Type::SuggestDecline)) {\n\t\t\t\t// ... rows ...\n\t\t\t\t//  [decline]\n\t\t\t\tdata.rows.push_back({ {\n\t\t\t\t\tType::SuggestDecline,\n\t\t\t\t\ttr::lng_suggest_action_decline(tr::now),\n\t\t\t\t\tVisual(),\n\t\t\t\t} });\n\t\t\t\tdata.flags |= ReplyMarkupFlag::SuggestionDecline;\n\t\t\t}\n\t\t\tif (data.rows.size() > 1) {\n\t\t\t\tdata.flags |= ReplyMarkupFlag::SuggestionSeparator;\n\t\t\t} else {\n\t\t\t\tdata.flags &= ~ReplyMarkupFlag::SuggestionSeparator;\n\t\t\t}\n\t\t}\n\t}\n\n\tinlineKeyboard = nullptr;\n}\n\nHistoryMessageLogEntryOriginal::HistoryMessageLogEntryOriginal() = default;\n\nHistoryMessageLogEntryOriginal::HistoryMessageLogEntryOriginal(\n\tHistoryMessageLogEntryOriginal &&other)\n: page(std::move(other.page)) {\n}\n\nHistoryMessageLogEntryOriginal &HistoryMessageLogEntryOriginal::operator=(\n\t\tHistoryMessageLogEntryOriginal &&other) {\n\tpage = std::move(other.page);\n\treturn *this;\n}\n\nHistoryMessageLogEntryOriginal::~HistoryMessageLogEntryOriginal() = default;\n\nMessageFactcheck FromMTP(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst tl::conditional<MTPFactCheck> &factcheck) {\n\treturn FromMTP(&item->history()->session(), factcheck);\n}\n\nMessageFactcheck FromMTP(\n\t\tnot_null<Main::Session*> session,\n\t\tconst tl::conditional<MTPFactCheck> &factcheck) {\n\tauto result = MessageFactcheck();\n\tif (!factcheck) {\n\t\treturn result;\n\t}\n\tconst auto &data = factcheck->data();\n\tif (const auto text = data.vtext()) {\n\t\tresult.text = Api::ParseTextWithEntities(session, *text);\n\t}\n\tif (const auto country = data.vcountry()) {\n\t\tresult.country = qs(country->v);\n\t}\n\tresult.hash = data.vhash().v;\n\tresult.needCheck = data.is_need_check();\n\treturn result;\n}\n\nTextWithEntities ComposeTodoTasksList(\n\t\tHistoryItem *itemWithList,\n\t\tconst std::vector<int> &ids) {\n\tconst auto media = itemWithList ? itemWithList->media() : nullptr;\n\tconst auto list = media ? media->todolist() : nullptr;\n\tauto names = std::vector<TextWithEntities>();\n\tif (list) {\n\t\tnames.reserve(ids.size());\n\t\tfor (const auto &id : ids) {\n\t\t\tconst auto i = ranges::find(list->items, id, &TodoListItem::id);\n\t\t\tif (i == end(list->items)) {\n\t\t\t\tnames.clear();\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tnames.push_back(\n\t\t\t\tTextWithEntities().append('\"').append(i->text).append('\"'));\n\t\t}\n\t}\n\treturn ComposeTodoTasksList(ids.size(), names);\n}\n\nTextWithEntities ComposeTodoTasksList(\n\t\tnot_null<HistoryServiceTodoAppendTasks*> append) {\n\tauto names = std::vector<TextWithEntities>();\n\tnames.reserve(append->list.size());\n\tfor (const auto &task : append->list) {\n\t\tnames.push_back(\n\t\t\tTextWithEntities().append('\"').append(task.text).append('\"'));\n\t}\n\treturn ComposeTodoTasksList(names.size(), names);\n}\n\nHistoryDocumentCaptioned::HistoryDocumentCaptioned()\n: caption(st::msgFileMinWidth - rect::m::sum::h(st::msgPadding)) {\n}\n\nHistoryDocumentVoicePlayback::HistoryDocumentVoicePlayback(\n\tconst HistoryView::Document *that)\n: progress(0., 0.)\n, progressAnimation([=](crl::time now) {\n\tconst auto nonconst = const_cast<HistoryView::Document*>(that);\n\treturn nonconst->voiceProgressAnimationCallback(now);\n}) {\n}\n\nvoid HistoryDocumentVoice::ensurePlayback(\n\t\tconst HistoryView::Document *that) const {\n\tif (!playback) {\n\t\tplayback = std::make_unique<HistoryDocumentVoicePlayback>(that);\n\t}\n}\n\nvoid HistoryDocumentVoice::checkPlaybackFinished() const {\n\tif (playback && !playback->progressAnimation.animating()) {\n\t\tplayback.reset();\n\t}\n}\n\nvoid HistoryDocumentVoice::startSeeking() {\n\t_seeking = true;\n\t_seekingCurrent = _seekingStart;\n\tMedia::Player::instance()->startSeeking(AudioMsgId::Type::Voice);\n}\n\nvoid HistoryDocumentVoice::stopSeeking() {\n\t_seeking = false;\n\tMedia::Player::instance()->cancelSeeking(AudioMsgId::Type::Voice);\n}\n\nbool HistoryDocumentVoice::seeking() const {\n\treturn _seeking;\n}\n\nfloat64 HistoryDocumentVoice::seekingStart() const {\n\treturn _seekingStart / kFloatToIntMultiplier;\n}\n\nvoid HistoryDocumentVoice::setSeekingStart(float64 seekingStart) const {\n\t_seekingStart = qRound(seekingStart * kFloatToIntMultiplier);\n}\n\nfloat64 HistoryDocumentVoice::seekingCurrent() const {\n\treturn _seekingCurrent / kFloatToIntMultiplier;\n}\n\nvoid HistoryDocumentVoice::setSeekingCurrent(float64 seekingCurrent) {\n\t_seekingCurrent = qRound(seekingCurrent * kFloatToIntMultiplier);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_item_components.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_cloud_file.h\"\n#include \"data/data_poll.h\"\n#include \"history/history_item.h\"\n#include \"spellcheck/spellcheck_types.h\" // LanguageId.\n#include \"ui/empty_userpic.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/chat/message_bubble.h\"\n\nstruct WebPageData;\nstruct TodoListItem;\nclass VoiceSeekClickHandler;\nclass ReplyKeyboard;\n\nnamespace Ui {\nstruct ChatPaintContext;\nclass ChatStyle;\nstruct PeerUserpicView;\n} // namespace Ui\n\nnamespace Ui::Text {\nstruct GeometryDescriptor;\n} // namespace Ui::Text\n\nnamespace Data {\nclass Session;\nclass Story;\nclass SavedSublist;\nstruct UnavailableReason;\n} // namespace Data\n\nnamespace Media::Player {\nclass RoundPainter;\n} // namespace Media::Player\n\nnamespace Images {\nstruct CornersMaskRef;\n} // namespace Images\n\nnamespace HistoryView {\nclass Element;\nclass Document;\nclass TranscribeButton;\n} // namespace HistoryView\n\nnamespace style {\nstruct BotKeyboardButton;\n} // namespace style\n\nextern const char kOptionFastButtonsMode[];\n[[nodiscard]] bool FastButtonsMode();\n\nenum class SuggestionActions : uchar {\n\tNone,\n\tDecline,\n\tAcceptAndDecline,\n\tGiftOfferActions,\n\tNoForwardsRequest,\n};\n\nstruct HistoryMessageVia : RuntimeComponent<HistoryMessageVia, HistoryItem> {\n\tvoid create(not_null<Data::Session*> owner, UserId userId);\n\tvoid resize(int32 availw) const;\n\n\tUserData *bot = nullptr;\n\tmutable QString text;\n\tmutable int width = 0;\n\tmutable int maxWidth = 0;\n\tClickHandlerPtr link;\n};\n\nstruct HistoryMessageViews\n: RuntimeComponent<HistoryMessageViews, HistoryItem> {\n\tstatic constexpr auto kMaxRecentRepliers = 3;\n\n\tstruct Part {\n\t\tQString text;\n\t\tint textWidth = 0;\n\t\tint count = -1;\n\t};\n\tstd::vector<PeerId> recentRepliers;\n\tPart views;\n\tPart replies;\n\tPart repliesSmall;\n\tChannelId commentsMegagroupId = 0;\n\tMsgId commentsRootId = 0;\n\tMsgId commentsInboxReadTillId = 0;\n\tMsgId commentsMaxId = 0;\n\tint forwardsCount = 0;\n};\n\nstruct HistoryMessageSigned\n: RuntimeComponent<HistoryMessageSigned, HistoryItem> {\n\tQString author;\n\tUserData *viaBusinessBot = nullptr;\n\tbool isAnonymousRank = false;\n};\n\nstruct HistoryMessageFromRank\n: RuntimeComponent<HistoryMessageFromRank, HistoryItem> {\n\tQString rank;\n};\n\nstruct HistoryMessageEdited\n: RuntimeComponent<HistoryMessageEdited, HistoryItem> {\n\tTimeId date = 0;\n};\n\nclass HiddenSenderInfo {\npublic:\n\tHiddenSenderInfo(\n\t\tconst QString &name,\n\t\tbool external,\n\t\tstd::optional<uint8> colorIndex = {});\n\n\tQString name;\n\tQString firstName;\n\tQString lastName;\n\tuint8 colorIndex = 0;\n\tUi::EmptyUserpic emptyUserpic;\n\tmutable Data::CloudImage customUserpic;\n\n\t[[nodiscard]] static ClickHandlerPtr ForwardClickHandler();\n\n\t[[nodiscard]] const Ui::Text::String &nameText() const;\n\t[[nodiscard]] bool paintCustomUserpic(\n\t\tPainter &p,\n\t\tUi::PeerUserpicView &view,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size) const;\n\n\tinline bool operator==(const HiddenSenderInfo &other) const {\n\t\treturn name == other.name;\n\t}\n\tinline bool operator!=(const HiddenSenderInfo &other) const {\n\t\treturn !(*this == other);\n\t}\n\nprivate:\n\tmutable Ui::Text::String _nameText;\n\n};\n\nstruct HistoryMessageForwarded\n: RuntimeComponent<HistoryMessageForwarded, HistoryItem> {\n\tvoid create(\n\t\tconst HistoryMessageVia *via,\n\t\tnot_null<const HistoryItem*> item) const;\n\n\t[[nodiscard]] bool forwardOfForward() const {\n\t\treturn savedFromSender || savedFromHiddenSenderInfo;\n\t}\n\n\tTimeId originalDate = 0;\n\tPeerData *originalSender = nullptr;\n\tstd::unique_ptr<HiddenSenderInfo> originalHiddenSenderInfo;\n\tQString originalPostAuthor;\n\tQString psaType;\n\tMsgId originalId = 0;\n\tmutable Ui::Text::String text = { 1 };\n\n\tPeerData *savedFromPeer = nullptr;\n\tMsgId savedFromMsgId = 0;\n\tTimeId savedFromDate = 0;\n\n\tPeerData *savedFromSender = nullptr;\n\tstd::unique_ptr<HiddenSenderInfo> savedFromHiddenSenderInfo;\n\n\tbool savedFromOutgoing = false;\n\tbool imported = false;\n\tbool story = false;\n};\n\nstruct HistoryMessageSavedMediaData\n: RuntimeComponent<HistoryMessageSavedMediaData, HistoryItem> {\n\tTextWithEntities text;\n\tstd::unique_ptr<Data::Media> media;\n};\n\nstruct HistoryMessageSaved\n: RuntimeComponent<HistoryMessageSaved, HistoryItem> {\n\tPeerId sublistPeerId = 0;\n\n\t// This can't change after the message is created, but is required\n\t// frequently in reactions, so we cache the value here.\n\tData::SavedSublist *savedMessagesSublist = nullptr;\n};\n\nclass ReplyToMessagePointer final {\npublic:\n\tReplyToMessagePointer(HistoryItem *item = nullptr) : _data(item) {\n\t}\n\tReplyToMessagePointer(ReplyToMessagePointer &&other)\n\t: _data(base::take(other._data)) {\n\t}\n\tReplyToMessagePointer &operator=(ReplyToMessagePointer &&other) {\n\t\t_data = base::take(other._data);\n\t\treturn *this;\n\t}\n\tReplyToMessagePointer &operator=(HistoryItem *item) {\n\t\t_data = item;\n\t\treturn *this;\n\t}\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn !_data;\n\t}\n\t[[nodiscard]] HistoryItem *get() const {\n\t\treturn _data;\n\t}\n\texplicit operator bool() const {\n\t\treturn !empty();\n\t}\n\n\t[[nodiscard]] HistoryItem *operator->() const {\n\t\treturn _data;\n\t}\n\t[[nodiscard]] HistoryItem &operator*() const {\n\t\treturn *_data;\n\t}\n\nprivate:\n\tHistoryItem *_data = nullptr;\n\n};\n\nclass ReplyToStoryPointer final {\npublic:\n\tReplyToStoryPointer(Data::Story *story = nullptr) : _data(story) {\n\t}\n\tReplyToStoryPointer(ReplyToStoryPointer &&other)\n\t: _data(base::take(other._data)) {\n\t}\n\tReplyToStoryPointer &operator=(ReplyToStoryPointer &&other) {\n\t\t_data = base::take(other._data);\n\t\treturn *this;\n\t}\n\tReplyToStoryPointer &operator=(Data::Story *item) {\n\t\t_data = item;\n\t\treturn *this;\n\t}\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn !_data;\n\t}\n\t[[nodiscard]] Data::Story *get() const {\n\t\treturn _data;\n\t}\n\texplicit operator bool() const {\n\t\treturn !empty();\n\t}\n\n\t[[nodiscard]] Data::Story *operator->() const {\n\t\treturn _data;\n\t}\n\t[[nodiscard]] Data::Story &operator*() const {\n\t\treturn *_data;\n\t}\n\nprivate:\n\tData::Story *_data = nullptr;\n\n};\n\nstruct ReplyFields {\n\t[[nodiscard]] ReplyFields clone(not_null<HistoryItem*> parent) const;\n\n\tTextWithEntities quote;\n\tstd::unique_ptr<Data::Media> externalMedia;\n\tPeerId externalSenderId = 0;\n\tQString externalSenderName;\n\tQString externalPostAuthor;\n\tPeerId externalPeerId = 0;\n\tPeerId monoforumPeerId = 0;\n\tMsgId messageId = 0;\n\tMsgId topMessageId = 0;\n\tStoryId storyId = 0;\n\tint todoItemId = 0;\n\tQByteArray pollOption;\n\tuint32 quoteOffset : 30 = 0;\n\tuint32 manualQuote : 1 = 0;\n\tuint32 topicPost : 1 = 0;\n};\n\n[[nodiscard]] ReplyFields ReplyFieldsFromMTP(\n\tnot_null<HistoryItem*> item,\n\tconst MTPMessageReplyHeader &reply);\n\n[[nodiscard]] FullReplyTo ReplyToFromMTP(\n\tnot_null<History*> history,\n\tconst MTPInputReplyTo &reply);\n\nstruct HistoryMessageReply\n: RuntimeComponent<HistoryMessageReply, HistoryItem> {\n\tHistoryMessageReply();\n\tHistoryMessageReply(const HistoryMessageReply &other) = delete;\n\tHistoryMessageReply(HistoryMessageReply &&other) = delete;\n\tHistoryMessageReply &operator=(\n\t\tconst HistoryMessageReply &other) = delete;\n\tHistoryMessageReply &operator=(HistoryMessageReply &&other);\n\t~HistoryMessageReply();\n\n\tvoid set(ReplyFields fields);\n\n\tvoid updateFields(\n\t\tnot_null<HistoryItem*> holder,\n\t\tMsgId messageId,\n\t\tMsgId topMessageId,\n\t\tbool topicPost);\n\tvoid updateData(not_null<HistoryItem*> holder, bool force = false);\n\n\t// Must be called before destructor.\n\tvoid clearData(not_null<HistoryItem*> holder);\n\n\t[[nodiscard]] bool external() const;\n\t[[nodiscard]] bool displayAsExternal(\n\t\tnot_null<HistoryItem*> holder) const;\n\tvoid itemRemoved(\n\t\tnot_null<HistoryItem*> holder,\n\t\tnot_null<HistoryItem*> removed);\n\tvoid storyRemoved(\n\t\tnot_null<HistoryItem*> holder,\n\t\tnot_null<Data::Story*> removed);\n\n\t[[nodiscard]] const ReplyFields &fields() const {\n\t\treturn _fields;\n\t}\n\t[[nodiscard]] PeerId externalPeerId() const {\n\t\treturn _fields.externalPeerId;\n\t}\n\t[[nodiscard]] MsgId messageId() const {\n\t\treturn _fields.messageId;\n\t}\n\t[[nodiscard]] StoryId storyId() const {\n\t\treturn _fields.storyId;\n\t}\n\t[[nodiscard]] MsgId topMessageId() const {\n\t\treturn _fields.topMessageId;\n\t}\n\t[[nodiscard]] bool topicPost() const {\n\t\treturn _fields.topicPost;\n\t}\n\t[[nodiscard]] bool manualQuote() const {\n\t\treturn _fields.manualQuote;\n\t}\n\t[[nodiscard]] bool unavailable() const {\n\t\treturn _unavailable;\n\t}\n\t[[nodiscard]] bool displaying() const {\n\t\treturn _displaying;\n\t}\n\t[[nodiscard]] bool multiline() const {\n\t\treturn _multiline;\n\t}\n\n\t[[nodiscard]] bool acquireResolve();\n\n\tvoid setTopMessageId(MsgId topMessageId);\n\n\tvoid refreshReplyToMedia();\n\n\tDocumentId replyToDocumentId = 0;\n\tWebPageId replyToWebPageId = 0;\n\tReplyToMessagePointer resolvedMessage;\n\tReplyToStoryPointer resolvedStory;\n\nprivate:\n\tReplyFields _fields;\n\tuint8 _unavailable : 1 = 0;\n\tuint8 _displaying : 1 = 0;\n\tuint8 _multiline : 1 = 0;\n\tuint8 _pendingResolve : 1 = 0;\n\tuint8 _requestedResolve : 1 = 0;\n\n};\n\nstruct HistoryMessageTranslation\n: RuntimeComponent<HistoryMessageTranslation, HistoryItem> {\n\tTextWithEntities text;\n\tLanguageId to;\n\tbool requested = false;\n\tbool failed = false;\n\tbool used = false;\n};\n\nstruct HistoryMessageReplyMarkup\n: RuntimeComponent<HistoryMessageReplyMarkup, HistoryItem> {\n\tusing Button = HistoryMessageMarkupButton;\n\n\tvoid createForwarded(const HistoryMessageReplyMarkup &original);\n\tvoid updateData(HistoryMessageMarkupData &&markup);\n\tvoid updateSuggestControls(SuggestionActions actions);\n\n\t[[nodiscard]] bool hiddenBy(Data::Media *media) const;\n\n\tHistoryMessageMarkupData data;\n\tstd::unique_ptr<ReplyKeyboard> inlineKeyboard;\n\n};\n\nclass ReplyMarkupClickHandler : public ClickHandler {\npublic:\n\tReplyMarkupClickHandler(\n\t\tnot_null<Data::Session*> owner,\n\t\tint row,\n\t\tint column,\n\t\tFullMsgId context);\n\n\tQString tooltip() const override;\n\n\tvoid setFullDisplayed(bool full) {\n\t\t_fullDisplayed = full;\n\t}\n\n\tQString dragText() const override;\n\n\t// Copy to clipboard support.\n\tQString copyToClipboardText() const override;\n\tQString copyToClipboardContextItemText() const override;\n\n\t// Finds the corresponding button in the items markup struct.\n\t// If the button is not found it returns nullptr.\n\t// Note: it is possible that we will point to the different button\n\t// than the one was used when constructing the handler, but not a big deal.\n\tconst HistoryMessageMarkupButton *getButton() const;\n\n\tconst HistoryMessageMarkupButton *getUrlButton() const;\n\n\t// We hold only FullMsgId, not HistoryItem*, because all click handlers\n\t// are activated async and the item may be already destroyed.\n\tvoid setMessageId(const FullMsgId &msgId) {\n\t\t_itemId = msgId;\n\t}\n\n\tvoid onClick(ClickContext context) const override;\n\nprivate:\n\tconst not_null<Data::Session*> _owner;\n\tFullMsgId _itemId;\n\tint _row = 0;\n\tint _column = 0;\n\tbool _fullDisplayed = true;\n\n\t// Returns the full text of the corresponding button.\n\tQString buttonText() const;\n\n};\n\nclass ReplyKeyboard {\nprivate:\n\tstruct Button;\n\npublic:\n\tclass Style {\n\tpublic:\n\t\tStyle(const style::BotKeyboardButton &st) : _st(&st) {\n\t\t}\n\n\t\tvirtual const style::TextStyle &textStyle() const = 0;\n\n\t\tint buttonSkip() const;\n\t\tint buttonPadding() const;\n\t\tint buttonHeight() const;\n\t\t[[nodiscard]] virtual Images::CornersMaskRef buttonRounding(\n\t\t\tUi::BubbleRounding outer,\n\t\t\tRectParts sides) const = 0;\n\n\t\tvirtual void repaint(not_null<const HistoryItem*> item) const = 0;\n\t\tvirtual ~Style() {\n\t\t}\n\n\tprotected:\n\t\tvirtual void paintButtonBg(\n\t\t\tQPainter &p,\n\t\t\tconst Ui::ChatStyle *st,\n\t\t\tconst QRect &rect,\n\t\t\tHistoryMessageMarkupButton::Color color,\n\t\t\tUi::BubbleRounding rounding,\n\t\t\tfloat64 howMuchOver) const = 0;\n\t\tvirtual void paintButtonStart(\n\t\t\tQPainter &p,\n\t\t\tconst Ui::ChatStyle *st,\n\t\t\tHistoryMessageMarkupButton::Color color) const = 0;\n\t\tvirtual void paintButtonIcon(\n\t\t\tQPainter &p,\n\t\t\tconst Ui::ChatStyle *st,\n\t\t\tconst QRect &rect,\n\t\t\tint outerWidth,\n\t\t\tHistoryMessageMarkupButton::Type type) const = 0;\n\t\tvirtual void paintButtonLoading(\n\t\t\tQPainter &p,\n\t\t\tconst Ui::ChatStyle *st,\n\t\t\tconst QRect &rect,\n\t\t\tHistoryMessageMarkupButton::Color color,\n\t\t\tint outerWidth,\n\t\t\tUi::BubbleRounding rounding) const = 0;\n\t\tvirtual int minButtonWidth(\n\t\t\tHistoryMessageMarkupButton::Type type) const = 0;\n\n\tprivate:\n\t\tconst style::BotKeyboardButton *_st;\n\n\t\tvoid paintButton(\n\t\t\tPainter &p,\n\t\t\tconst Ui::ChatStyle *st,\n\t\t\tint outerWidth,\n\t\t\tconst ReplyKeyboard::Button &button,\n\t\t\tUi::BubbleRounding rounding,\n\t\t\tbool paused) const;\n\t\tfriend class ReplyKeyboard;\n\n\t};\n\n\tReplyKeyboard(\n\t\tnot_null<const HistoryItem*> item,\n\t\tstd::unique_ptr<Style> &&s);\n\tReplyKeyboard(const ReplyKeyboard &other) = delete;\n\tReplyKeyboard &operator=(const ReplyKeyboard &other) = delete;\n\n\tbool isEnoughSpace(int width, const style::BotKeyboardButton &st) const;\n\tvoid setStyle(std::unique_ptr<Style> &&s);\n\tvoid resize(int width, int height);\n\n\t// what width and height will best fit this keyboard\n\tint naturalWidth() const;\n\tint naturalHeight() const;\n\n\tvoid paint(\n\t\tPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tUi::BubbleRounding rounding,\n\t\tint outerWidth,\n\t\tconst QRect &clip,\n\t\tbool paused) const;\n\tClickHandlerPtr getLink(QPoint point) const;\n\tClickHandlerPtr getLinkByIndex(int index) const;\n\n\tvoid clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool active);\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed,\n\t\tUi::BubbleRounding rounding);\n\n\tvoid clearSelection();\n\tvoid updateMessageId();\n\nprivate:\n\tfriend class Style;\n\tstruct Button {\n\t\tButton();\n\t\tButton(Button &&other);\n\t\tButton &operator=(Button &&other);\n\t\t~Button();\n\n\t\tUi::Text::String text = { 1 };\n\t\tQRect rect;\n\t\tint characters = 0;\n\t\tfloat64 howMuchOver = 0.;\n\t\tHistoryMessageMarkupButton::Type type = {};\n\t\tHistoryMessageMarkupButton::Color color = {};\n\t\tstd::shared_ptr<ReplyMarkupClickHandler> link;\n\t\tmutable std::unique_ptr<Ui::RippleAnimation> ripple;\n\t};\n\tstruct ButtonCoords {\n\t\tint i = 0;\n\t\tint j = 0;\n\t};\n\n\tvoid startAnimation(int i, int j, int direction);\n\t[[nodiscard]] bool hasFastButtonMode() const;\n\n\tButtonCoords findButtonCoordsByClickHandler(const ClickHandlerPtr &p);\n\n\tbool selectedAnimationCallback(crl::time now);\n\n\tconst not_null<const HistoryItem*> _item;\n\tint _width = 0;\n\n\tstd::vector<std::vector<Button>> _rows;\n\n\tbase::flat_map<int, crl::time> _animations;\n\tUi::Animations::Basic _selectedAnimation;\n\tstd::unique_ptr<Style> _st;\n\n\tClickHandlerPtr _savedPressed;\n\tClickHandlerPtr _savedActive;\n\tmutable QPoint _savedCoords;\n\n};\n\n// Special type of Component for the channel actions log.\nstruct HistoryMessageLogEntryOriginal\n: RuntimeComponent<HistoryMessageLogEntryOriginal, HistoryItem> {\n\tHistoryMessageLogEntryOriginal();\n\tHistoryMessageLogEntryOriginal(HistoryMessageLogEntryOriginal &&other);\n\tHistoryMessageLogEntryOriginal &operator=(HistoryMessageLogEntryOriginal &&other);\n\t~HistoryMessageLogEntryOriginal();\n\n\tWebPageData *page = nullptr;\n\n};\n\nstruct MessageFactcheck {\n\tTextWithEntities text;\n\tQString country;\n\tuint64 hash = 0;\n\tbool needCheck = false;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn text.empty() && country.isEmpty() && !hash;\n\t}\n\texplicit operator bool() const {\n\t\treturn !empty();\n\t}\n};\n\n[[nodiscard]] MessageFactcheck FromMTP(\n\tnot_null<HistoryItem*> item,\n\tconst tl::conditional<MTPFactCheck> &factcheck);\n[[nodiscard]] MessageFactcheck FromMTP(\n\tnot_null<Main::Session*> session,\n\tconst tl::conditional<MTPFactCheck> &factcheck);\n\nstruct HistoryMessageFactcheck\n: RuntimeComponent<HistoryMessageFactcheck, HistoryItem> {\n\tMessageFactcheck data;\n\tWebPageData *page = nullptr;\n\tbool requested = false;\n};\n\nstruct HistoryMessageSuggestion\n: RuntimeComponent<HistoryMessageSuggestion, HistoryItem> {\n\tstd::shared_ptr<Data::UniqueGift> gift;\n\tCreditsAmount price;\n\tTimeId date = 0;\n\tmtpRequestId requestId = 0;\n\tbool accepted = false;\n\tbool rejected = false;\n};\n\nstruct HistoryMessageRestrictions\n: RuntimeComponent<HistoryMessageRestrictions, HistoryItem> {\n\tstd::vector<Data::UnavailableReason> reasons;\n};\n\nstruct HistoryServiceData\n: RuntimeComponent<HistoryServiceData, HistoryItem> {\n\tstd::vector<ClickHandlerPtr> textLinks;\n};\n\nstruct HistoryServiceDependentData {\n\tPeerId peerId = 0;\n\tHistoryItem *msg = nullptr;\n\tClickHandlerPtr lnk;\n\tMsgId msgId = 0;\n\tMsgId topId = 0;\n\tbool topicPost = false;\n\tbool pendingResolve = false;\n\tbool requestedResolve = false;\n};\n\nstruct HistoryServicePinned\n: RuntimeComponent<HistoryServicePinned, HistoryItem>\n, HistoryServiceDependentData {\n};\n\nstruct HistoryServiceClearHistory\n: RuntimeComponent<HistoryServiceClearHistory, HistoryItem>\n, HistoryServiceDependentData {\n};\n\nstruct HistoryServiceTopicInfo\n: RuntimeComponent<HistoryServiceTopicInfo, HistoryItem>\n, HistoryServiceDependentData {\n\tQString title;\n\tDocumentId iconId = 0;\n\tbool closed = false;\n\tbool reopened = false;\n\tbool reiconed = false;\n\tbool renamed = false;\n\tbool hidden = false;\n\tbool unhidden = false;\n\n\t[[nodiscard]] bool created() const {\n\t\treturn !closed\n\t\t\t&& !reopened\n\t\t\t&& !reiconed\n\t\t\t&& !renamed\n\t\t\t&& !hidden\n\t\t\t&& !unhidden;\n\t}\n};\n\nstruct HistoryServiceTodoCompletions\n: RuntimeComponent<HistoryServiceTodoCompletions, HistoryItem>\n, HistoryServiceDependentData {\n\tstd::vector<int> completed;\n\tstd::vector<int> incompleted;\n};\n\n[[nodiscard]] TextWithEntities ComposeTodoTasksList(\n\tHistoryItem *itemWithList,\n\tconst std::vector<int> &ids);\n\nstruct HistoryServiceTodoAppendTasks\n: RuntimeComponent<HistoryServiceTodoAppendTasks, HistoryItem>\n, HistoryServiceDependentData {\n\tstd::vector<TodoListItem> list;\n};\n\n[[nodiscard]] TextWithEntities ComposeTodoTasksList(\n\tnot_null<HistoryServiceTodoAppendTasks*> append);\n\nstruct HistoryServicePollAppendAnswer\n: RuntimeComponent<HistoryServicePollAppendAnswer, HistoryItem>\n, HistoryServiceDependentData {\n\tPollAnswer answer;\n};\n\nstruct HistoryServicePollDeleteAnswer\n: RuntimeComponent<HistoryServicePollDeleteAnswer, HistoryItem>\n, HistoryServiceDependentData {\n\tPollAnswer answer;\n};\n\nstruct HistoryServiceSuggestDecision\n: RuntimeComponent<HistoryServiceSuggestDecision, HistoryItem>\n, HistoryServiceDependentData {\n\tCreditsAmount price;\n\tTimeId date = 0;\n\tQString rejectComment;\n\tbool rejected = false;\n\tbool balanceTooLow = false;\n};\n\nenum class SuggestRefundType {\n\tNone,\n\tUser,\n\tAdmin,\n\tExpired,\n};\n\nstruct HistoryServiceSuggestFinish\n: RuntimeComponent<HistoryServiceSuggestFinish, HistoryItem>\n, HistoryServiceDependentData {\n\tCreditsAmount price;\n\tSuggestRefundType refundType = SuggestRefundType::None;\n};\n\nstruct HistoryServiceNoForwardsRequest\n: RuntimeComponent<HistoryServiceNoForwardsRequest, HistoryItem> {\n\tTimeId expiresAt = 0;\n\tmtpRequestId requestId = 0;\n\tbool expired = false;\n\tbool actionTaken = false;\n};\n\nstruct HistoryServiceNoForwardsToggle\n: RuntimeComponent<HistoryServiceNoForwardsToggle, HistoryItem> {\n};\n\nstruct HistoryServiceGameScore\n: RuntimeComponent<HistoryServiceGameScore, HistoryItem>\n, HistoryServiceDependentData {\n\tint score = 0;\n};\n\nstruct HistoryServicePayment\n: RuntimeComponent<HistoryServicePayment, HistoryItem>\n, HistoryServiceDependentData {\n\tQString slug;\n\tTextWithEntities amount;\n\tClickHandlerPtr invoiceLink;\n\tbool recurringInit = false;\n\tbool recurringUsed = false;\n\tbool isCreditsCurrency = false;\n};\n\nstruct HistoryServiceSameBackground\n: RuntimeComponent<HistoryServiceSameBackground, HistoryItem>\n, HistoryServiceDependentData {\n};\n\nstruct HistoryServiceGiveawayResults\n: RuntimeComponent<HistoryServiceGiveawayResults, HistoryItem>\n, HistoryServiceDependentData {\n};\n\nstruct HistoryServiceCustomLink\n: RuntimeComponent<HistoryServiceCustomLink, HistoryItem> {\n\tClickHandlerPtr link;\n};\n\nstruct HistoryServicePaymentRefund\n: RuntimeComponent<HistoryServicePaymentRefund, HistoryItem> {\n\tClickHandlerPtr link;\n\tPeerData *peer = nullptr;\n\tQString transactionId;\n\tQString currency;\n\tuint64 amount = 0;\n};\n\nenum class HistorySelfDestructType {\n\tPhoto,\n\tVideo,\n};\n\nstruct TimeToLiveSingleView {\n\tfriend inline auto operator<=>(\n\t\tTimeToLiveSingleView,\n\t\tTimeToLiveSingleView) = default;\n\tfriend inline bool operator==(\n\t\tTimeToLiveSingleView,\n\t\tTimeToLiveSingleView) = default;\n};\n\nstruct HistoryServiceSelfDestruct\n: RuntimeComponent<HistoryServiceSelfDestruct, HistoryItem> {\n\tusing Type = HistorySelfDestructType;\n\n\tType type = Type::Photo;\n\tstd::variant<crl::time, TimeToLiveSingleView> timeToLive = crl::time();\n\tstd::variant<crl::time, TimeToLiveSingleView> destructAt = crl::time();\n};\n\nstruct HistoryServiceOngoingCall\n: RuntimeComponent<HistoryServiceOngoingCall, HistoryItem> {\n\tCallId id = 0;\n\tClickHandlerPtr link;\n\trpl::lifetime lifetime;\n};\n\nstruct HistoryServiceChatThemeChange\n: RuntimeComponent<HistoryServiceChatThemeChange, HistoryItem> {\n\tClickHandlerPtr link;\n};\n\nstruct HistoryServiceTTLChange\n: RuntimeComponent<HistoryServiceTTLChange, HistoryItem> {\n\tClickHandlerPtr link;\n};\n\nclass FileClickHandler;\nstruct HistoryDocumentThumbed\n: RuntimeComponent<HistoryDocumentThumbed, HistoryView::Document> {\n\tstd::shared_ptr<FileClickHandler> linksavel;\n\tstd::shared_ptr<FileClickHandler> linkopenwithl;\n\tstd::shared_ptr<FileClickHandler> linkcancell;\n\tmutable QImage thumbnail;\n\tmutable QString link;\n\tint thumbw = 0;\n\tmutable int linkw = 0;\n\tmutable Ui::BubbleRounding rounding;\n\tmutable bool blurred : 1 = false;\n};\n\nstruct HistoryDocumentCaptioned\n: RuntimeComponent<HistoryDocumentCaptioned, HistoryView::Document> {\n\tHistoryDocumentCaptioned();\n\n\tUi::Text::String caption;\n};\n\nstruct HistoryDocumentNamed\n: RuntimeComponent<HistoryDocumentNamed, HistoryView::Document> {\n\tUi::Text::String name;\n};\n\nstruct HistoryDocumentVoicePlayback {\n\tHistoryDocumentVoicePlayback(const HistoryView::Document *that);\n\n\tint32 position = 0;\n\tanim::value progress;\n\tUi::Animations::Basic progressAnimation;\n};\n\nclass HistoryDocumentVoice\n: public RuntimeComponent<HistoryDocumentVoice, HistoryView::Document> {\n\t// We don't use float64 because components should align to pointer even on 32bit systems.\n\tstatic constexpr float64 kFloatToIntMultiplier = 65536.;\n\npublic:\n\tvoid ensurePlayback(const HistoryView::Document *interfaces) const;\n\tvoid checkPlaybackFinished() const;\n\n\tmutable std::unique_ptr<HistoryDocumentVoicePlayback> playback;\n\tstd::shared_ptr<VoiceSeekClickHandler> seekl;\n\tmutable int lastDurationMs = 0;\n\n\t[[nodiscard]] bool seeking() const;\n\tvoid startSeeking();\n\tvoid stopSeeking();\n\t[[nodiscard]] float64 seekingStart() const;\n\tvoid setSeekingStart(float64 seekingStart) const;\n\t[[nodiscard]] float64 seekingCurrent() const;\n\tvoid setSeekingCurrent(float64 seekingCurrent);\n\n\tstd::unique_ptr<HistoryView::TranscribeButton> transcribe;\n\tUi::Text::String transcribeText;\n\tstd::unique_ptr<Media::Player::RoundPainter> round;\n\nprivate:\n\tbool _seeking = false;\n\n\tmutable int _seekingStart = 0;\n\tmutable int _seekingCurrent = 0;\n\n};\n\nstruct HistoryMessageSchedulePeriod\n: RuntimeComponent<HistoryMessageSchedulePeriod, HistoryItem> {\n\tTimeId schedulePeriod = 0;\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_item_edition.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_item_edition.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"main/main_session.h\"\n\nHistoryMessageEdition::HistoryMessageEdition(\n\tnot_null<Main::Session*> session,\n\tconst MTPDmessage &message)\n: suggest(HistoryMessageSuggestInfo(message.vsuggested_post())) {\n\tisEditHide = message.is_edit_hide();\n\tisMediaUnread = message.is_media_unread();\n\trepeatPeriod = message.vschedule_repeat_period().value_or_empty();\n\teditDate = message.vedit_date().value_or(-1);\n\ttextWithEntities = TextWithEntities{\n\t\tqs(message.vmessage()),\n\t\tApi::EntitiesFromMTP(\n\t\t\tsession,\n\t\t\tmessage.ventities().value_or_empty())\n\t};\n\treplyMarkup = HistoryMessageMarkupData(message.vreply_markup());\n\tmtpMedia = message.vmedia();\n\tmtpReactions = message.vreactions();\n\tmtpFactcheck = message.vfactcheck();\n\tviews = message.vviews().value_or(-1);\n\tforwards = message.vforwards().value_or(-1);\n\tif (const auto mtpReplies = message.vreplies()) {\n\t\treplies = HistoryMessageRepliesData(mtpReplies);\n\t}\n\tinvertMedia = message.is_invert_media();\n\tif (const auto rank = message.vfrom_rank()) {\n\t\tfromRank = qs(*rank);\n\t}\n\n\tconst auto period = message.vttl_period();\n\tttl = (period && period->v > 0) ? (message.vdate().v + period->v) : 0;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_item_edition.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/history_item_reply_markup.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nstruct HistoryMessageEdition {\n\texplicit HistoryMessageEdition() = default;\n\tHistoryMessageEdition(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDmessage &message);\n\n\tbool isEditHide = false;\n\tbool isMediaUnread = false;\n\tTimeId repeatPeriod = 0;\n\tTimeId editDate = 0;\n\tint views = -1;\n\tint forwards = -1;\n\tint ttl = 0;\n\tbool useSameViews = false;\n\tbool useSameForwards = false;\n\tbool useSameReplies = false;\n\tbool useSameMarkup = false;\n\tbool useSameReactions = false;\n\tbool useSameSuggest = false;\n\tbool savePreviousMedia = false;\n\tbool invertMedia = false;\n\tTextWithEntities textWithEntities;\n\tHistoryMessageMarkupData replyMarkup;\n\tHistoryMessageRepliesData replies;\n\tHistoryMessageSuggestInfo suggest;\n\tconst MTPMessageMedia *mtpMedia = nullptr;\n\tconst MTPMessageReactions *mtpReactions = nullptr;\n\tconst MTPFactCheck *mtpFactcheck = nullptr;\n\tQString fromRank;\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_item_helpers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_item_helpers.h\"\n\n#include \"api/api_reactions_notify_settings.h\"\n#include \"api/api_text_entities.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"calls/calls_instance.h\"\n#include \"data/components/sponsored_messages.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_document.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_user.h\"\n#include \"history/view/controls/history_view_suggest_options.h\"\n#include \"history/history.h\"\n#include \"history/history_item_components.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"menu/menu_sponsored.h\"\n#include \"platform/platform_notifications_manager.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\" // ClickHandlerContext.\n#include \"settings/settings_credits_graphics.h\"\n#include \"storage/storage_account.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/emoji_stake_box.h\" // InsufficientTonBox\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/item_text_options.h\"\n#include \"lang/lang_keys.h\"\n\nnamespace {\n\nbool PeerCallKnown(not_null<PeerData*> peer) {\n\tif (peer->groupCall() != nullptr) {\n\t\treturn true;\n\t} else if (const auto chat = peer->asChat()) {\n\t\treturn !(chat->flags() & ChatDataFlag::CallActive);\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn !(channel->flags() & ChannelDataFlag::CallActive);\n\t}\n\treturn true;\n}\n\n} // namespace\n\nint ComputeSendingMessagesCount(\n\t\tnot_null<History*> history,\n\t\tconst SendingErrorRequest &request) {\n\tauto result = 0;\n\tif (request.text && !request.text->empty()) {\n\t\tauto sending = TextWithEntities();\n\t\tauto left = TextWithEntities{\n\t\t\trequest.text->text,\n\t\t\tTextUtilities::ConvertTextTagsToEntities(request.text->tags)\n\t\t};\n\t\tauto prepareFlags = Ui::ItemTextOptions(\n\t\t\thistory,\n\t\t\thistory->session().user()).flags;\n\t\tTextUtilities::PrepareForSending(left, prepareFlags);\n\n\t\twhile (TextUtilities::CutPart(sending, left, MaxMessageSize)) {\n\t\t\t++result;\n\t\t}\n\t\tif (!result) {\n\t\t\t++result;\n\t\t}\n\t}\n\treturn result\n\t\t+ (request.story ? 1 : 0)\n\t\t+ (request.forward ? int(request.forward->size()) : 0);\n}\n\nData::SendError GetErrorForSending(\n\t\tnot_null<PeerData*> peer,\n\t\tSendingErrorRequest request) {\n\tconst auto forum = request.topicRootId ? peer->forum() : nullptr;\n\tconst auto topic = forum\n\t\t? forum->topicFor(request.topicRootId)\n\t\t: nullptr;\n\tconst auto thread = topic\n\t\t? not_null<Data::Thread*>(topic)\n\t\t: peer->owner().history(peer);\n\tif (request.story) {\n\t\tif (const auto error = request.story->errorTextForForward(thread)) {\n\t\t\treturn error;\n\t\t}\n\t}\n\tif (request.forward) {\n\t\tfor (const auto &item : *request.forward) {\n\t\t\tif (const auto error = item->errorTextForForward(thread)) {\n\t\t\t\treturn error;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto hasText = (request.text && !request.text->empty());\n\tif (hasText) {\n\t\tconst auto error = Data::RestrictionError(\n\t\t\tpeer,\n\t\t\tChatRestriction::SendOther);\n\t\tif (error) {\n\t\t\treturn error;\n\t\t} else if (!Data::CanSendTexts(thread)) {\n\t\t\treturn tr::lng_forward_cant(tr::now);\n\t\t}\n\t}\n\tif (peer->slowmodeApplied()) {\n\t\tconst auto count = request.messagesCount\n\t\t\t? request.messagesCount\n\t\t\t: ComputeSendingMessagesCount(thread->owningHistory(), request);\n\t\tif (const auto history = peer->owner().historyLoaded(peer)) {\n\t\t\tif (!request.ignoreSlowmodeCountdown\n\t\t\t\t&& (history->latestSendingMessage() != nullptr)\n\t\t\t\t&& (count > 0)) {\n\t\t\t\treturn tr::lng_slowmode_no_many(tr::now);\n\t\t\t}\n\t\t}\n\t\tif (request.text && request.text->text.size() > MaxMessageSize) {\n\t\t\treturn tr::lng_slowmode_too_long(tr::now);\n\t\t} else if ((hasText || request.story) && count > 1) {\n\t\t\treturn tr::lng_slowmode_no_many(tr::now);\n\t\t} else if (count > 1) {\n\t\t\tconst auto albumForward = [&] {\n\t\t\t\tconst auto first = request.forward->front();\n\t\t\t\tif (const auto groupId = first->groupId()) {\n\t\t\t\t\tfor (const auto &item : *request.forward) {\n\t\t\t\t\t\tif (item->groupId() != groupId) {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}();\n\t\t\tif (!albumForward) {\n\t\t\t\treturn tr::lng_slowmode_no_many(tr::now);\n\t\t\t}\n\t\t}\n\t}\n\tif (const auto left = peer->slowmodeSecondsLeft()) {\n\t\tif (!request.ignoreSlowmodeCountdown) {\n\t\t\treturn tr::lng_slowmode_enabled(\n\t\t\t\ttr::now,\n\t\t\t\tlt_left,\n\t\t\t\tUi::FormatDurationWordsSlowmode(left));\n\t\t}\n\t}\n\treturn {};\n}\n\nData::SendError GetErrorForSending(\n\t\tnot_null<Data::Thread*> thread,\n\t\tSendingErrorRequest request) {\n\trequest.topicRootId = thread->topicRootId();\n\treturn GetErrorForSending(thread->peer(), std::move(request));\n}\n\nData::SendErrorWithThread GetErrorForSending(\n\t\tconst std::vector<not_null<Data::Thread*>> &threads,\n\t\tSendingErrorRequest request) {\n\tfor (const auto &thread : threads) {\n\t\tconst auto error = GetErrorForSending(thread, request);\n\t\tif (error) {\n\t\t\treturn Data::SendErrorWithThread{ error, thread };\n\t\t}\n\t}\n\treturn {};\n}\n\nstd::optional<SendPaymentDetails> ComputePaymentDetails(\n\t\tnot_null<PeerData*> peer,\n\t\tint messagesCount) {\n\tconst auto user = peer->asUser();\n\tconst auto channel = user ? nullptr : peer->asChannel();\n\tconst auto has = (user && user->hasStarsPerMessage())\n\t\t|| (channel && channel->hasStarsPerMessage());\n\tif (!has) {\n\t\treturn SendPaymentDetails();\n\t}\n\n\tconst auto known1 = peer->session().credits().loaded();\n\tif (!known1) {\n\t\tpeer->session().credits().load();\n\t}\n\n\tconst auto known2 = user\n\t\t? user->messageMoneyRestrictionsKnown()\n\t\t: channel->starsPerMessageKnown();\n\tif (!known2) {\n\t\tpeer->updateFull();\n\t}\n\n\tif (!known1 || !known2) {\n\t\treturn {};\n\t} else if (const auto perMessage = peer->starsPerMessageChecked()) {\n\t\treturn SendPaymentDetails{\n\t\t\t.messages = messagesCount,\n\t\t\t.stars = messagesCount * perMessage,\n\t\t};\n\t}\n\treturn SendPaymentDetails();\n}\n\nbool SuggestPaymentDataReady(\n\t\tnot_null<PeerData*> peer,\n\t\tSuggestOptions suggest) {\n\tif (!suggest.exists || !suggest.price() || peer->amMonoforumAdmin()) {\n\t\treturn true;\n\t} else if (suggest.ton && !peer->session().credits().tonLoaded()) {\n\t\tpeer->session().credits().tonLoad();\n\t\treturn false;\n\t} else if (!suggest.ton && !peer->session().credits().loaded()) {\n\t\tpeer->session().credits().load();\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nobject_ptr<Ui::BoxContent> MakeSendErrorBox(\n\t\tconst Data::SendErrorWithThread &error,\n\t\tbool withTitle) {\n\tExpects(error.error.has_value() && error.thread != nullptr);\n\n\tauto text = TextWithEntities();\n\tif (withTitle) {\n\t\ttext.append(\n\t\t\ttr::bold(error.thread->chatListName())\n\t\t).append(\"\\n\\n\");\n\t}\n\tif (error.error.boostsToLift) {\n\t\ttext.append(tr::link(error.error.text));\n\t} else {\n\t\ttext.append(error.error.text);\n\t}\n\tconst auto peer = error.thread->peer();\n\tconst auto lifting = error.error.boostsToLift;\n\tconst auto filter = [=](const auto &...) {\n\t\tExpects(peer->isChannel());\n\n\t\tconst auto window = ChatHelpers::ResolveWindowDefault()(\n\t\t\t&peer->session());\n\t\twindow->resolveBoostState(peer->asChannel(), lifting);\n\t\treturn false;\n\t};\n\treturn Ui::MakeInformBox({\n\t\t.text = text,\n\t\t.labelFilter = filter,\n\t});\n}\n\nvoid ShowSendPaidConfirm(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tSendPaymentDetails details,\n\t\tFn<void()> confirmed,\n\t\tPaidConfirmStyles styles,\n\t\tint suggestStarsPrice) {\n\treturn ShowSendPaidConfirm(\n\t\tnavigation->uiShow(),\n\t\tpeer,\n\t\tdetails,\n\t\tconfirmed,\n\t\tstyles,\n\t\tsuggestStarsPrice);\n}\n\nvoid ShowSendPaidConfirm(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<PeerData*> peer,\n\t\tSendPaymentDetails details,\n\t\tFn<void()> confirmed,\n\t\tPaidConfirmStyles styles,\n\t\tint suggestStarsPrice) {\n\tShowSendPaidConfirm(\n\t\tstd::move(show),\n\t\tstd::vector<not_null<PeerData*>>{ peer },\n\t\tdetails,\n\t\tconfirmed,\n\t\tstyles,\n\t\tsuggestStarsPrice);\n}\n\nvoid ShowSendPaidConfirm(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tconst std::vector<not_null<PeerData*>> &peers,\n\t\tSendPaymentDetails details,\n\t\tFn<void()> confirmed,\n\t\tPaidConfirmStyles styles,\n\t\tint suggestStarsPrice) {\n\tExpects(!peers.empty());\n\n\tconst auto singlePeer = (peers.size() > 1)\n\t\t? (PeerData*)nullptr\n\t\t: peers.front().get();\n\tconst auto singlePeerId = singlePeer ? singlePeer->id : PeerId();\n\tconst auto check = [=] {\n\t\tconst auto required = details.stars + suggestStarsPrice;\n\t\tif (!required) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto done = [=](Settings::SmallBalanceResult result) {\n\t\t\tif (result == Settings::SmallBalanceResult::Success\n\t\t\t\t|| result == Settings::SmallBalanceResult::Already) {\n\t\t\t\tconfirmed();\n\t\t\t}\n\t\t};\n\t\tusing namespace Settings;\n\t\tMaybeRequestBalanceIncrease(\n\t\t\tshow,\n\t\t\trequired,\n\t\t\t(suggestStarsPrice\n\t\t\t\t? SmallBalanceSource(SmallBalanceForSuggest{ singlePeerId })\n\t\t\t\t: SmallBalanceForMessage{ singlePeerId }),\n\t\t\tdone);\n\t};\n\tauto usersOnly = true;\n\tfor (const auto &peer : peers) {\n\t\tif (!peer->isUser()) {\n\t\t\tusersOnly = false;\n\t\t\tbreak;\n\t\t}\n\t}\n\tconst auto singlePeerStars = singlePeer\n\t\t? singlePeer->starsPerMessageChecked()\n\t\t: 0;\n\tif (singlePeer) {\n\t\tconst auto session = &singlePeer->session();\n\t\tconst auto trusted = session->local().isPeerTrustedPayForMessage(\n\t\t\tsinglePeerId,\n\t\t\tsinglePeerStars);\n\t\tif (trusted) {\n\t\t\tcheck();\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto messages = details.messages;\n\tconst auto stars = details.stars;\n\tshow->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto trust = std::make_shared<base::weak_qptr<Ui::Checkbox>>();\n\t\tconst auto proceed = [=](Fn<void()> close) {\n\t\t\tif (singlePeer && (*trust)->checked()) {\n\t\t\t\tconst auto session = &singlePeer->session();\n\t\t\t\tsession->local().markPeerTrustedPayForMessage(\n\t\t\t\t\tsinglePeerId,\n\t\t\t\t\tsinglePeerStars);\n\t\t\t}\n\t\t\tcheck();\n\t\t\tclose();\n\t\t};\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = (singlePeer\n\t\t\t\t? tr::lng_payment_confirm_text(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tstars / messages,\n\t\t\t\t\tlt_name,\n\t\t\t\t\ttr::bold(singlePeer->shortName()),\n\t\t\t\t\ttr::rich)\n\t\t\t\t: (usersOnly\n\t\t\t\t\t? tr::lng_payment_confirm_users\n\t\t\t\t\t: tr::lng_payment_confirm_chats)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tint(peers.size()),\n\t\t\t\t\t\ttr::rich)).append(' ').append(\n\t\t\t\t\t\t\ttr::lng_payment_confirm_sure(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\t\tmessages,\n\t\t\t\t\t\t\t\tlt_amount,\n\t\t\t\t\t\t\t\ttr::lng_payment_confirm_amount(\n\t\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\t\t\tstars,\n\t\t\t\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t\t\t\ttr::rich)),\n\t\t\t.confirmed = proceed,\n\t\t\t.confirmText = tr::lng_payment_confirm_button(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(messages * 1.)),\n\t\t\t.labelStyle = styles.label,\n\t\t\t.title = tr::lng_payment_confirm_title(),\n\t\t});\n\t\tif (singlePeer) {\n\t\t\tconst auto skip = st::defaultCheckbox.margin.top();\n\t\t\t*trust = box->addRow(\n\t\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\t\tbox,\n\t\t\t\t\ttr::lng_payment_confirm_dont_ask(tr::now),\n\t\t\t\t\tfalse,\n\t\t\t\t\t(styles.checkbox\n\t\t\t\t\t\t? *styles.checkbox\n\t\t\t\t\t\t: st::defaultCheckbox)),\n\t\t\t\tst::boxRowPadding + QMargins(0, skip, 0, skip));\n\t\t}\n\t}));\n}\n\nbool SendPaymentHelper::check(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tApi::SendOptions options,\n\t\tint messagesCount,\n\t\tFn<void(int)> resend,\n\t\tPaidConfirmStyles styles) {\n\treturn check(\n\t\tnavigation->uiShow(),\n\t\tpeer,\n\t\toptions,\n\t\tmessagesCount,\n\t\tstd::move(resend),\n\t\tstyles);\n}\n\nbool SendPaymentHelper::check(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<PeerData*> peer,\n\t\tApi::SendOptions options,\n\t\tint messagesCount,\n\t\tFn<void(int)> resend,\n\t\tPaidConfirmStyles styles) {\n\tclear();\n\n\tconst auto admin = peer->amMonoforumAdmin();\n\tconst auto suggest = options.suggest;\n\tconst auto starsApproved = options.starsApproved;\n\tconst auto checkSuggestPriceStars = (admin || suggest.ton)\n\t\t? 0\n\t\t: int(base::SafeRound(suggest.price().value()));\n\tconst auto checkSuggestPriceTon = (!admin && suggest.ton)\n\t\t? suggest.price()\n\t\t: CreditsAmount();\n\tconst auto details = ComputePaymentDetails(peer, messagesCount);\n\tconst auto suggestDetails = SuggestPaymentDataReady(peer, suggest);\n\tif (!details || !suggestDetails) {\n\t\t_resend = [=] { resend(starsApproved); };\n\n\t\tif ((!details || !suggest.ton)\n\t\t\t&& !peer->session().credits().loaded()) {\n\t\t\tpeer->session().credits().loadedValue(\n\t\t\t) | rpl::filter(\n\t\t\t\trpl::mappers::_1\n\t\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t\tif (const auto callback = base::take(_resend)) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}, _lifetime);\n\t\t}\n\n\t\tif ((!suggestDetails && suggest.ton)\n\t\t\t&& !peer->session().credits().tonLoaded()) {\n\t\t\tpeer->session().credits().tonLoadedValue(\n\t\t\t) | rpl::filter(\n\t\t\t\trpl::mappers::_1\n\t\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t\tif (const auto callback = base::take(_resend)) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}, _lifetime);\n\t\t}\n\n\t\tpeer->session().changes().peerUpdates(\n\t\t\tpeer,\n\t\t\tData::PeerUpdate::Flag::FullInfo\n\t\t) | rpl::on_next([=] {\n\t\t\tif (const auto callback = base::take(_resend)) {\n\t\t\t\tcallback();\n\t\t\t}\n\t\t}, _lifetime);\n\n\t\treturn false;\n\t} else if (const auto stars = details->stars; stars > starsApproved) {\n\t\tShowSendPaidConfirm(show, peer, *details, [=] {\n\t\t\tresend(stars);\n\t\t}, styles, checkSuggestPriceStars);\n\t\treturn false;\n\t} else if (checkSuggestPriceStars\n\t\t&& (CreditsAmount(details->stars + checkSuggestPriceStars)\n\t\t\t> peer->session().credits().balance())) {\n\t\tusing namespace Settings;\n\t\tconst auto broadcast = peer->monoforumBroadcast();\n\t\tconst auto broadcastId = (broadcast ? broadcast : peer)->id;\n\t\tconst auto forMessages = details->stars;\n\t\tconst auto required = forMessages + checkSuggestPriceStars;\n\t\tconst auto done = [=](SmallBalanceResult result) {\n\t\t\tif (result == SmallBalanceResult::Success\n\t\t\t\t|| result == SmallBalanceResult::Already) {\n\t\t\t\tresend(forMessages);\n\t\t\t}\n\t\t};\n\t\tMaybeRequestBalanceIncrease(\n\t\t\tshow,\n\t\t\trequired,\n\t\t\tSmallBalanceForSuggest{ broadcastId },\n\t\t\tdone);\n\t\treturn false;\n\t}\n\tconst auto session = &peer->session();\n\tif (checkSuggestPriceTon\n\t\t&& checkSuggestPriceTon > session->credits().tonBalance()) {\n\t\tusing namespace HistoryView;\n\t\tshow->show(\n\t\t\tBox(Ui::InsufficientTonBox, session, checkSuggestPriceTon));\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nvoid SendPaymentHelper::clear() {\n\t_lifetime.destroy();\n\t_resend = nullptr;\n}\n\nvoid RequestDependentMessageItem(\n\t\tnot_null<HistoryItem*> item,\n\t\tPeerId peerId,\n\t\tMsgId msgId) {\n\tif (!IsServerMsgId(msgId)) {\n\t\treturn;\n\t}\n\tconst auto fullId = item->fullId();\n\tconst auto history = item->history();\n\tconst auto session = &history->session();\n\tconst auto done = [=] {\n\t\tif (const auto item = session->data().message(fullId)) {\n\t\t\titem->updateDependencyItem();\n\t\t}\n\t};\n\thistory->session().api().requestMessageData(\n\t\t(peerId ? history->owner().peer(peerId) : history->peer),\n\t\tmsgId,\n\t\tdone);\n}\n\nvoid RequestDependentMessageStory(\n\t\tnot_null<HistoryItem*> item,\n\t\tPeerId peerId,\n\t\tStoryId storyId) {\n\tconst auto fullId = item->fullId();\n\tconst auto history = item->history();\n\tconst auto session = &history->session();\n\tconst auto done = [=] {\n\t\tif (const auto item = session->data().message(fullId)) {\n\t\t\titem->updateDependencyItem();\n\t\t}\n\t};\n\thistory->owner().stories().resolve(\n\t\t{ peerId ? peerId : history->peer->id, storyId },\n\t\tdone);\n}\n\nMessageFlags NewMessageFlags(not_null<PeerData*> peer) {\n\treturn MessageFlag::BeingSent\n\t\t| (peer->isSelf() ? MessageFlag() : MessageFlag::Outgoing);\n}\n\nTimeId NewMessageDate(TimeId scheduled) {\n\treturn scheduled ? scheduled : base::unixtime::now();\n}\n\nTimeId NewMessageDate(const Api::SendOptions &options) {\n\treturn options.shortcutId ? 1 : NewMessageDate(options.scheduled);\n}\n\nPeerId NewMessageFromId(const Api::SendAction &action) {\n\treturn action.options.sendAs\n\t\t? action.options.sendAs->id\n\t\t: action.history->peer->amMonoforumAdmin()\n\t\t? action.history->peer->monoforumBroadcast()->id\n\t\t: action.history->peer->amAnonymous()\n\t\t? PeerId()\n\t\t: action.history->session().userPeerId();\n}\n\nQString NewMessagePostAuthor(const Api::SendAction &action) {\n\treturn !action.history->peer->isBroadcast()\n\t\t? QString()\n\t\t: (action.options.sendAs == action.history->peer)\n\t\t? QString()\n\t\t: action.options.sendAs\n\t\t? action.options.sendAs->name()\n\t\t: action.history->session().user()->name();\n}\n\nbool ShouldSendSilent(\n\t\tnot_null<PeerData*> peer,\n\t\tconst Api::SendOptions &options) {\n\treturn options.silent\n\t\t|| (peer->isBroadcast()\n\t\t\t&& peer->owner().notifySettings().silentPosts(peer))\n\t\t|| (peer->session().supportMode()\n\t\t\t&& peer->session().settings().supportAllSilent());\n}\n\nHistoryItem *LookupReplyTo(not_null<History*> history, FullMsgId replyTo) {\n\treturn history->owner().message(replyTo);\n}\n\nMsgId LookupReplyToTop(not_null<History*> history, HistoryItem *replyTo) {\n\treturn (replyTo && replyTo->history() == history)\n\t\t? replyTo->replyToTop()\n\t\t: 0;\n}\n\nMsgId LookupReplyToTop(not_null<History*> history, FullReplyTo replyTo) {\n\treturn replyTo.topicRootId\n\t\t? replyTo.topicRootId\n\t\t: LookupReplyToTop(\n\t\t\thistory,\n\t\t\tLookupReplyTo(history, replyTo.messageId));\n}\n\nbool LookupReplyIsTopicPost(HistoryItem *replyTo) {\n\treturn replyTo\n\t\t&& (replyTo->topicRootId() != Data::ForumTopic::kGeneralId);\n}\n\nTextWithEntities DropDisallowedCustomEmoji(\n\t\tnot_null<PeerData*> to,\n\t\tTextWithEntities text) {\n\tif (to->session().premium() || to->isSelf()) {\n\t\treturn text;\n\t}\n\tconst auto channel = to->asMegagroup();\n\tconst auto allowSetId = channel ? channel->mgInfo->emojiSet.id : 0;\n\tif (!allowSetId) {\n\t\ttext.entities.erase(\n\t\t\tranges::remove(\n\t\t\t\ttext.entities,\n\t\t\t\tEntityType::CustomEmoji,\n\t\t\t\t&EntityInText::type),\n\t\t\ttext.entities.end());\n\t} else {\n\t\tconst auto predicate = [&](const EntityInText &entity) {\n\t\t\tif (entity.type() != EntityType::CustomEmoji) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (const auto id = Data::ParseCustomEmojiData(entity.data())) {\n\t\t\t\tconst auto document = to->owner().document(id);\n\t\t\t\tif (const auto sticker = document->sticker()) {\n\t\t\t\t\tif (sticker->set.id == allowSetId) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\t\ttext.entities.erase(\n\t\t\tranges::remove_if(text.entities, predicate),\n\t\t\ttext.entities.end());\n\t}\n\treturn text;\n}\n\nMain::Session *SessionByUniqueId(uint64 sessionUniqueId) {\n\tif (!sessionUniqueId) {\n\t\treturn nullptr;\n\t}\n\tfor (const auto &[index, account] : Core::App().domain().accounts()) {\n\t\tif (const auto session = account->maybeSession()) {\n\t\t\tif (session->uniqueId() == sessionUniqueId) {\n\t\t\t\treturn session;\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nHistoryItem *MessageByGlobalId(GlobalMsgId globalId) {\n\tconst auto sessionId = globalId.itemId ? globalId.sessionUniqueId : 0;\n\tif (const auto session = SessionByUniqueId(sessionId)) {\n\t\treturn session->data().message(globalId.itemId);\n\t}\n\treturn nullptr;\n}\n\nQDateTime ItemDateTime(not_null<const HistoryItem*> item) {\n\treturn base::unixtime::parse(item->date());\n}\n\nQString ItemDateText(not_null<const HistoryItem*> item, bool isUntilOnline) {\n\tconst auto dateText = langDayOfMonthFull(ItemDateTime(item).date());\n\treturn !item->isScheduled()\n\t\t? dateText\n\t\t: isUntilOnline\n\t\t\t? tr::lng_scheduled_date_until_online(tr::now)\n\t\t\t: tr::lng_scheduled_date(tr::now, lt_date, dateText);\n}\n\nbool IsItemScheduledUntilOnline(not_null<const HistoryItem*> item) {\n\treturn item->isScheduled()\n\t\t&& (item->date() == Api::kScheduledUntilOnlineTimestamp);\n}\n\nClickHandlerPtr JumpToMessageClickHandler(\n\t\tnot_null<HistoryItem*> item,\n\t\tFullMsgId returnToId,\n\t\tMessageHighlightId highlight) {\n\treturn JumpToMessageClickHandler(\n\t\titem->history()->peer,\n\t\titem->id,\n\t\treturnToId,\n\t\tstd::move(highlight));\n}\n\nClickHandlerPtr JumpToMessageClickHandler(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId msgId,\n\t\tFullMsgId returnToId,\n\t\tMessageHighlightId highlight) {\n\treturn std::make_shared<LambdaClickHandler>([=] {\n\t\tconst auto separate = Core::App().separateWindowFor(peer);\n\t\tconst auto controller = separate\n\t\t\t? separate->sessionController()\n\t\t\t: peer->session().tryResolveWindow(peer);\n\t\tif (controller) {\n\t\t\tauto params = Window::SectionShow{\n\t\t\t\tWindow::SectionShow::Way::Forward,\n\t\t\t};\n\t\t\tparams.highlight = highlight;\n\t\t\tparams.allowDuplicateInStack = true;\n\t\t\tparams.origin = Window::SectionShow::OriginMessage{\n\t\t\t\treturnToId\n\t\t\t};\n\t\t\tif (const auto item = peer->owner().message(peer, msgId)) {\n\t\t\t\tcontroller->showMessage(item, params);\n\t\t\t} else {\n\t\t\t\tcontroller->showPeerHistory(peer, params, msgId);\n\t\t\t}\n\t\t}\n\t});\n}\n\nClickHandlerPtr JumpToStoryClickHandler(not_null<Data::Story*> story) {\n\treturn JumpToStoryClickHandler(story->peer(), story->id());\n}\n\nClickHandlerPtr JumpToStoryClickHandler(\n\t\tnot_null<PeerData*> peer,\n\t\tStoryId storyId) {\n\treturn std::make_shared<LambdaClickHandler>([=] {\n\t\tconst auto separate = Core::App().separateWindowFor(peer);\n\t\tconst auto controller = separate\n\t\t\t? separate->sessionController()\n\t\t\t: peer->session().tryResolveWindow();\n\t\tif (controller) {\n\t\t\tcontroller->openPeerStory(\n\t\t\t\tpeer,\n\t\t\t\tstoryId,\n\t\t\t\t{ Data::StoriesContextSingle() });\n\t\t}\n\t});\n}\n\nClickHandlerPtr HideSponsoredClickHandler() {\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tconst auto &session = controller->session();\n\t\t\tif (session.premium()) {\n\t\t\t\tusing Result = Data::SponsoredReportResult;\n\t\t\t\tsession.sponsoredMessages().createReportCallback(\n\t\t\t\t\tmy.itemId\n\t\t\t\t).callback(Result::Id(\"-1\"), [](const auto &) {});\n\t\t\t} else {\n\t\t\t\tShowPremiumPreviewBox(controller, PremiumFeature::NoAds);\n\t\t\t}\n\t\t}\n\t});\n}\n\nClickHandlerPtr ReportSponsoredClickHandler(not_null<HistoryItem*> item) {\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tMenu::ShowSponsored(\n\t\t\t\tcontroller->widget(),\n\t\t\t\tcontroller->uiShow(),\n\t\t\t\titem->fullId());\n\t\t}\n\t});\n}\n\nClickHandlerPtr AboutSponsoredClickHandler() {\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tMenu::ShowSponsoredAbout(controller->uiShow(), my.itemId);\n\t\t}\n\t});\n}\n\n\nMessageFlags FlagsFromMTP(\n\t\tMsgId id,\n\t\tMTPDmessage::Flags flags,\n\t\tMessageFlags localFlags) {\n\tusing Flag = MessageFlag;\n\tusing MTP = MTPDmessage::Flag;\n\treturn localFlags\n\t\t| (IsServerMsgId(id) ? Flag::HistoryEntry : Flag())\n\t\t| ((flags & MTP::f_out) ? Flag::Outgoing : Flag())\n\t\t| ((flags & MTP::f_mentioned) ? Flag::MentionsMe : Flag())\n\t\t| ((flags & MTP::f_media_unread) ? Flag::MediaIsUnread : Flag())\n\t\t| ((flags & MTP::f_silent) ? Flag::Silent : Flag())\n\t\t| ((flags & MTP::f_post) ? Flag::Post : Flag())\n\t\t| ((flags & MTP::f_legacy) ? Flag::Legacy : Flag())\n\t\t| ((flags & MTP::f_edit_hide) ? Flag::HideEdited : Flag())\n\t\t| ((flags & MTP::f_pinned) ? Flag::Pinned : Flag())\n\t\t| ((flags & MTP::f_from_id) ? Flag::HasFromId : Flag())\n\t\t| ((flags & MTP::f_reply_to) ? Flag::HasReplyInfo : Flag())\n\t\t| ((flags & MTP::f_reply_markup) ? Flag::HasReplyMarkup : Flag())\n\t\t| ((flags & MTP::f_quick_reply_shortcut_id)\n\t\t\t? Flag::ShortcutMessage\n\t\t\t: Flag())\n\t\t| ((flags & MTP::f_from_scheduled)\n\t\t\t? Flag::IsOrWasScheduled\n\t\t\t: Flag())\n\t\t| ((flags & MTP::f_views) ? Flag::HasViews : Flag())\n\t\t| ((flags & MTP::f_noforwards) ? Flag::NoForwards : Flag())\n\t\t| ((flags & MTP::f_invert_media) ? Flag::InvertMedia : Flag())\n\t\t| ((flags & MTP::f_video_processing_pending)\n\t\t\t? Flag::EstimatedDate\n\t\t\t: Flag())\n\t\t| ((flags & MTP::f_paid_suggested_post_ton)\n\t\t\t? Flag::TonPaidSuggested\n\t\t\t: (flags & MTP::f_paid_suggested_post_stars)\n\t\t\t? Flag::StarsPaidSuggested\n\t\t\t: Flag())\n\t\t| ((flags & MTP::f_summary_from_language)\n\t\t\t? Flag::CanBeSummarized\n\t\t\t: Flag());\n}\n\nMessageFlags FlagsFromMTP(\n\t\tMsgId id,\n\t\tMTPDmessageService::Flags flags,\n\t\tMessageFlags localFlags) {\n\tusing Flag = MessageFlag;\n\tusing MTP = MTPDmessageService::Flag;\n\treturn localFlags\n\t\t| (IsServerMsgId(id) ? Flag::HistoryEntry : Flag())\n\t\t| ((flags & MTP::f_out) ? Flag::Outgoing : Flag())\n\t\t| ((flags & MTP::f_mentioned) ? Flag::MentionsMe : Flag())\n\t\t| ((flags & MTP::f_media_unread) ? Flag::MediaIsUnread : Flag())\n\t\t| ((flags & MTP::f_silent) ? Flag::Silent : Flag())\n\t\t| ((flags & MTP::f_post) ? Flag::Post : Flag())\n\t\t| ((flags & MTP::f_legacy) ? Flag::Legacy : Flag())\n\t\t| ((flags & MTP::f_from_id) ? Flag::HasFromId : Flag())\n\t\t| ((flags & MTP::f_reply_to) ? Flag::HasReplyInfo : Flag())\n\t\t| ((flags & MTP::f_reactions_are_possible)\n\t\t\t? Flag::ReactionsAllowed\n\t\t\t: Flag());\n}\n\nMTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) {\n\tif (const auto replyTo = action.replyTo) {\n\t\tif (replyTo.storyId) {\n\t\t\treturn MTP_messageReplyStoryHeader(\n\t\t\t\tpeerToMTP(replyTo.storyId.peer),\n\t\t\t\tMTP_int(replyTo.storyId.story));\n\t\t}\n\t\tusing Flag = MTPDmessageReplyHeader::Flag;\n\t\tconst auto historyPeer = action.history->peer->id;\n\t\tconst auto externalPeerId = (replyTo.messageId.peer == historyPeer)\n\t\t\t? PeerId()\n\t\t\t: replyTo.messageId.peer;\n\t\tconst auto replyToTop = LookupReplyToTop(action.history, replyTo);\n\t\tconst auto topicPost = replyTo.topicRootId\n\t\t\t&& (replyTo.topicRootId != Data::ForumTopic::kGeneralId);\n\t\tauto quoteEntities = Api::EntitiesToMTP(\n\t\t\t&action.history->session(),\n\t\t\treplyTo.quote.entities,\n\t\t\tApi::ConvertOption::SkipLocal);\n\t\treturn MTP_messageReplyHeader(\n\t\t\tMTP_flags(Flag::f_reply_to_msg_id\n\t\t\t\t| (replyToTop ? Flag::f_reply_to_top_id : Flag())\n\t\t\t\t| (externalPeerId ? Flag::f_reply_to_peer_id : Flag())\n\t\t\t\t| (replyTo.quote.empty()\n\t\t\t\t\t? Flag()\n\t\t\t\t\t: (Flag::f_quote\n\t\t\t\t\t\t| Flag::f_quote_text\n\t\t\t\t\t\t| Flag::f_quote_offset))\n\t\t\t\t| (quoteEntities.v.empty()\n\t\t\t\t\t? Flag()\n\t\t\t\t\t: Flag::f_quote_entities)\n\t\t\t\t| (replyTo.todoItemId ? Flag::f_todo_item_id : Flag())\n\t\t\t\t| (replyTo.pollOption.isEmpty()\n\t\t\t\t\t? Flag()\n\t\t\t\t\t: Flag::f_poll_option)\n\t\t\t\t| (topicPost ? Flag::f_forum_topic : Flag())),\n\t\t\tMTP_int(replyTo.messageId.msg),\n\t\t\tpeerToMTP(externalPeerId),\n\t\t\tMTPMessageFwdHeader(), // reply_from\n\t\t\tMTPMessageMedia(), // reply_media\n\t\t\tMTP_int(replyToTop),\n\t\t\tMTP_string(replyTo.quote.text),\n\t\t\tquoteEntities,\n\t\t\tMTP_int(replyTo.quoteOffset),\n\t\t\tMTP_int(replyTo.todoItemId),\n\t\t\tMTP_bytes(replyTo.pollOption));\n\t}\n\treturn MTPMessageReplyHeader();\n}\n\nMediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) {\n\tusing Result = MediaCheckResult;\n\treturn media.match([](const MTPDmessageMediaEmpty &) {\n\t\treturn Result::Good;\n\t}, [](const MTPDmessageMediaContact &) {\n\t\treturn Result::Good;\n\t}, [](const MTPDmessageMediaGeo &data) {\n\t\treturn data.vgeo().match([](const MTPDgeoPoint &) {\n\t\t\treturn Result::Good;\n\t\t}, [](const MTPDgeoPointEmpty &) {\n\t\t\treturn Result::Empty;\n\t\t});\n\t}, [](const MTPDmessageMediaVenue &data) {\n\t\treturn data.vgeo().match([](const MTPDgeoPoint &) {\n\t\t\treturn Result::Good;\n\t\t}, [](const MTPDgeoPointEmpty &) {\n\t\t\treturn Result::Empty;\n\t\t});\n\t}, [](const MTPDmessageMediaGeoLive &data) {\n\t\treturn data.vgeo().match([](const MTPDgeoPoint &) {\n\t\t\treturn Result::Good;\n\t\t}, [](const MTPDgeoPointEmpty &) {\n\t\t\treturn Result::Empty;\n\t\t});\n\t}, [](const MTPDmessageMediaPhoto &data) {\n\t\tconst auto photo = data.vphoto();\n\t\tif (data.vttl_seconds()) {\n\t\t\treturn Result::HasUnsupportedTimeToLive;\n\t\t} else if (!photo) {\n\t\t\treturn Result::Empty;\n\t\t}\n\t\treturn photo->match([](const MTPDphoto &) {\n\t\t\treturn Result::Good;\n\t\t}, [](const MTPDphotoEmpty &) {\n\t\t\treturn Result::Empty;\n\t\t});\n\t}, [](const MTPDmessageMediaDocument &data) {\n\t\tconst auto document = data.vdocument();\n\t\tif (data.vttl_seconds()) {\n\t\t\tif (data.is_video()) {\n\t\t\t\treturn Result::HasUnsupportedTimeToLive;\n\t\t\t} else if (!document) {\n\t\t\t\treturn Result::HasExpiredMediaTimeToLive;\n\t\t\t}\n\t\t} else if (!document) {\n\t\t\treturn Result::Empty;\n\t\t}\n\t\treturn document->match([](const MTPDdocument &) {\n\t\t\treturn Result::Good;\n\t\t}, [](const MTPDdocumentEmpty &) {\n\t\t\treturn Result::Empty;\n\t\t});\n\t}, [](const MTPDmessageMediaWebPage &data) {\n\t\treturn data.vwebpage().match([](const MTPDwebPage &) {\n\t\t\treturn Result::Good;\n\t\t}, [](const MTPDwebPageEmpty &) {\n\t\t\treturn Result::Good;\n\t\t}, [](const MTPDwebPagePending &) {\n\t\t\treturn Result::Good;\n\t\t}, [](const MTPDwebPageNotModified &) {\n\t\t\treturn Result::Unsupported;\n\t\t});\n\t}, [](const MTPDmessageMediaGame &data) {\n\t\treturn data.vgame().match([](const MTPDgame &) {\n\t\t\treturn Result::Good;\n\t\t});\n\t}, [](const MTPDmessageMediaInvoice &) {\n\t\treturn Result::Good;\n\t}, [](const MTPDmessageMediaPoll &) {\n\t\treturn Result::Good;\n\t}, [](const MTPDmessageMediaToDo &) {\n\t\treturn Result::Good;\n\t}, [](const MTPDmessageMediaDice &) {\n\t\treturn Result::Good;\n\t}, [](const MTPDmessageMediaStory &data) {\n\t\treturn data.is_via_mention()\n\t\t\t? Result::HasStoryMention\n\t\t\t: Result::Good;\n\t}, [](const MTPDmessageMediaGiveaway &) {\n\t\treturn Result::Good;\n\t}, [](const MTPDmessageMediaGiveawayResults &) {\n\t\treturn Result::Good;\n\t}, [](const MTPDmessageMediaPaidMedia &) {\n\t\treturn Result::Good;\n\t}, [](const MTPDmessageMediaVideoStream &) {\n\t\treturn Result::Good;\n\t}, [](const MTPDmessageMediaUnsupported &) {\n\t\treturn Result::Unsupported;\n\t});\n}\n\n[[nodiscard]] CallId CallIdFromInput(const MTPInputGroupCall &data) {\n\treturn data.match([&](const MTPDinputGroupCall &data) {\n\t\treturn data.vid().v;\n\t}, [](const auto &) -> CallId {\n\t\tUnexpected(\"slug/msg in CallIdFromInput.\");\n\t});\n}\n\nstd::vector<not_null<UserData*>> ParseInvitedToCallUsers(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst QVector<MTPlong> &users) {\n\tauto &owner = item->history()->owner();\n\treturn ranges::views::all(\n\t\tusers\n\t) | ranges::views::transform([&](const MTPlong &id) {\n\t\treturn owner.user(id.v);\n\t}) | ranges::to_vector;\n}\n\nPreparedServiceText GenerateJoinedText(\n\t\tnot_null<History*> history,\n\t\tnot_null<UserData*> inviter,\n\t\tbool viaRequest) {\n\tif (inviter->id != history->session().userPeerId()) {\n\t\tauto result = PreparedServiceText();\n\t\tresult.links.push_back(inviter->createOpenLink());\n\t\tresult.text = (history->peer->isMegagroup()\n\t\t\t? tr::lng_action_add_you_group\n\t\t\t: tr::lng_action_add_you)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\ttr::link(inviter->name(), QString()),\n\t\t\t\ttr::marked);\n\t\treturn result;\n\t} else if (history->peer->isMegagroup()) {\n\t\tif (viaRequest) {\n\t\t\treturn { tr::lng_action_you_joined_by_request(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked) };\n\t\t}\n\t\tauto self = history->session().user();\n\t\tauto result = PreparedServiceText();\n\t\tresult.links.push_back(self->createOpenLink());\n\t\tresult.text = tr::lng_action_user_joined(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\ttr::link(self->name(), QString()),\n\t\t\ttr::marked);\n\t\treturn result;\n\t}\n\treturn { viaRequest\n\t\t? tr::lng_action_you_joined_by_request_channel(\n\t\t\ttr::now,\n\t\t\ttr::marked)\n\t\t: tr::lng_action_you_joined(tr::now, tr::marked) };\n}\n\nnot_null<HistoryItem*> GenerateJoinedMessage(\n\t\tnot_null<History*> history,\n\t\tTimeId inviteDate,\n\t\tnot_null<UserData*> inviter,\n\t\tbool viaRequest) {\n\treturn history->makeMessage({\n\t\t.id = history->owner().nextLocalMessageId(),\n\t\t.flags = MessageFlag::Local | MessageFlag::ShowSimilarChannels,\n\t\t.date = inviteDate,\n\t}, GenerateJoinedText(history, inviter, viaRequest));\n}\n\nstd::optional<bool> PeerHasThisCall(\n\t\tnot_null<PeerData*> peer,\n\t\tCallId id) {\n\tconst auto call = peer->groupCall();\n\treturn call\n\t\t? std::make_optional(call->id() == id)\n\t\t: PeerCallKnown(peer)\n\t\t? std::make_optional(false)\n\t\t: std::nullopt;\n}\n[[nodiscard]] rpl::producer<bool> PeerHasThisCallValue(\n\t\tnot_null<PeerData*> peer,\n\t\tCallId id) {\n\treturn peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::GroupCall\n\t) | rpl::filter([=] {\n\t\treturn PeerCallKnown(peer);\n\t}) | rpl::map([=] {\n\t\tconst auto call = peer->groupCall();\n\t\treturn (call && call->id() == id);\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::take_while([=](bool hasThisCall) {\n\t\treturn hasThisCall;\n\t}) | rpl::then(\n\t\trpl::single(false)\n\t);\n}\n\n[[nodiscard]] ClickHandlerPtr GroupCallClickHandler(\n\t\tnot_null<PeerData*> peer,\n\t\tCallId callId) {\n\treturn std::make_shared<LambdaClickHandler>([=] {\n\t\tconst auto call = peer->groupCall();\n\t\tif (call && call->id() == callId) {\n\t\t\tconst auto &windows = peer->session().windows();\n\t\t\tif (windows.empty()) {\n\t\t\t\tCore::App().domain().activate(&peer->session().account());\n\t\t\t\tif (windows.empty()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\twindows.front()->startOrJoinGroupCall(peer, {});\n\t\t}\n\t});\n}\n\n[[nodiscard]] MessageFlags FinalizeMessageFlags(\n\t\tnot_null<History*> history,\n\t\tMessageFlags flags) {\n\tif (!(flags & MessageFlag::FakeHistoryItem)\n\t\t&& !(flags & MessageFlag::IsOrWasScheduled)\n\t\t&& !(flags & MessageFlag::ShortcutMessage)\n\t\t&& !(flags & MessageFlag::AdminLogEntry)) {\n\t\tflags |= MessageFlag::HistoryEntry;\n\t\tif (history->peer->isSelf()) {\n\t\t\tflags |= MessageFlag::ReactionsAreTags;\n\t\t}\n\t}\n\treturn flags;\n}\n\nusing OnStackUsers = std::array<UserData*, kMaxUnreadReactions>;\n[[nodiscard]] OnStackUsers LookupRecentUnreadReactedUsers(\n\t\tnot_null<HistoryItem*> item) {\n\tauto result = OnStackUsers();\n\tauto index = 0;\n\tfor (const auto &[emoji, reactions] : item->recentReactions()) {\n\t\tfor (const auto &reaction : reactions) {\n\t\t\tif (!reaction.unread) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (const auto user = reaction.peer->asUser()) {\n\t\t\t\tresult[index++] = user;\n\t\t\t\tif (index == result.size()) {\n\t\t\t\t\treturn result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid CheckReactionNotificationSchedule(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst OnStackUsers &wasUsers) {\n\t// Call to addToUnreadThings may have read the reaction already.\n\tif (!item->hasUnreadReaction()) {\n\t\treturn;\n\t}\n\tconst auto from = item->history()->session().api()\n\t\t.reactionsNotifySettings().messagesFromCurrent();\n\tif (from == Api::ReactionsNotifyFrom::None) {\n\t\treturn;\n\t}\n\tfor (const auto &[emoji, reactions] : item->recentReactions()) {\n\t\tfor (const auto &reaction : reactions) {\n\t\t\tif (!reaction.unread) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto user = reaction.peer->asUser();\n\t\t\tif (!user || ranges::contains(wasUsers, user)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (from == Api::ReactionsNotifyFrom::Contacts\n\t\t\t\t&& !user->isContact()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tusing Status = PeerData::BlockStatus;\n\t\t\tif (user->blockStatus() == Status::Unknown) {\n\t\t\t\tuser->updateFull();\n\t\t\t}\n\t\t\tconst auto notification = Data::ItemNotification{\n\t\t\t\t.item = item,\n\t\t\t\t.reactionOrVoteSender = user,\n\t\t\t\t.type = Data::ItemNotificationType::Reaction,\n\t\t\t};\n\t\t\titem->notificationThread()->pushNotification(notification);\n\t\t\tCore::App().notifications().schedule(notification);\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nvoid CheckPollVoteNotificationSchedule(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst std::vector<not_null<PeerData*>> &wasRecentVoters) {\n\tconst auto media = item->media();\n\tconst auto poll = media ? media->poll() : nullptr;\n\tif (!poll || !poll->creator()) {\n\t\treturn;\n\t}\n\tconst auto from = item->history()->session().api()\n\t\t.reactionsNotifySettings().pollVotesFromCurrent();\n\tif (from == Api::ReactionsNotifyFrom::None) {\n\t\treturn;\n\t}\n\tfor (const auto &answer : poll->answers) {\n\t\tfor (const auto &voter : answer.recentVoters) {\n\t\t\tconst auto user = voter->asUser();\n\t\t\tif (!user || ranges::contains(wasRecentVoters, voter)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (from == Api::ReactionsNotifyFrom::Contacts\n\t\t\t\t&& !user->isContact()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tusing Status = PeerData::BlockStatus;\n\t\t\tif (user->blockStatus() == Status::Unknown) {\n\t\t\t\tuser->updateFull();\n\t\t\t}\n\t\t\tconst auto notification = Data::ItemNotification{\n\t\t\t\t.item = item,\n\t\t\t\t.reactionOrVoteSender = user,\n\t\t\t\t.type = Data::ItemNotificationType::PollVote,\n\t\t\t};\n\t\t\titem->notificationThread()->pushNotification(notification);\n\t\t\tCore::App().notifications().schedule(notification);\n\t\t\treturn;\n\t\t}\n\t}\n}\n\n[[nodiscard]] MessageFlags NewForwardedFlags(\n\t\tnot_null<PeerData*> peer,\n\t\tPeerId from,\n\t\tnot_null<HistoryItem*> fwd) {\n\tauto result = NewMessageFlags(peer);\n\tif (from) {\n\t\tresult |= MessageFlag::HasFromId;\n\t}\n\tif (const auto media = fwd->media()) {\n\t\tif ((!peer->isChannel() || peer->isMegagroup())\n\t\t\t&& media->forwardedBecomesUnread()) {\n\t\t\tresult |= MessageFlag::MediaIsUnread;\n\t\t}\n\t}\n\tif (fwd->hasViews()) {\n\t\tresult |= MessageFlag::HasViews;\n\t}\n\treturn result;\n}\n\n[[nodiscard]] bool CopyMarkupToForward(not_null<const HistoryItem*> item) {\n\tauto mediaOriginal = item->media();\n\tif (mediaOriginal && mediaOriginal->game()) {\n\t\t// Copy inline keyboard when forwarding messages with a game.\n\t\treturn true;\n\t}\n\tconst auto markup = item->inlineReplyMarkup();\n\tif (!markup) {\n\t\treturn false;\n\t}\n\tusing Type = HistoryMessageMarkupButton::Type;\n\tfor (const auto &row : markup->data.rows) {\n\t\tfor (const auto &button : row) {\n\t\t\tconst auto switchInline = (button.type == Type::SwitchInline)\n\t\t\t\t|| (button.type == Type::SwitchInlineSame);\n\t\t\tconst auto url = (button.type == Type::Url)\n\t\t\t\t|| (button.type == Type::Auth);\n\t\t\tif ((!switchInline || !item->viaBot()) && !url) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\treturn true;\n}\n\n[[nodiscard]] TextWithEntities EnsureNonEmpty(\n\t\tconst TextWithEntities &text) {\n\treturn !text.text.isEmpty() ? text : TextWithEntities{ u\":-(\"_q };\n}\n\n[[nodiscard]] TextWithEntities UnsupportedMessageText() {\n\tconst auto siteLink = u\"https://desktop.telegram.org\"_q;\n\tauto result = TextWithEntities{\n\t\ttr::lng_message_unsupported(tr::now, lt_link, siteLink)\n\t};\n\tTextUtilities::ParseEntities(result, Ui::ItemTextNoMonoOptions().flags);\n\tresult.entities.push_front(\n\t\tEntityInText(EntityType::Italic, 0, result.text.size()));\n\treturn result;\n}\n\nHistoryMessageMarkupData UnsupportedMessageMarkup() {\n\tusing Button = HistoryMessageMarkupButton;\n\tauto markup = HistoryMessageMarkupData();\n\tmarkup.flags = ReplyMarkupFlag::Inline;\n\tauto row = std::vector<Button>();\n\trow.emplace_back(\n\t\tButton::Type::Url,\n\t\ttr::lng_update_telegram(tr::now),\n\t\tButton::Visual(),\n\t\tQByteArray(\"https://desktop.telegram.org\"));\n\tmarkup.rows.push_back(std::move(row));\n\treturn markup;\n}\n\nvoid ShowTrialTranscribesToast(int left, TimeId until) {\n\tconst auto window = Core::App().activeWindow();\n\tif (!window) {\n\t\treturn;\n\t}\n\tconst auto filter = [=](const auto &...) {\n\t\tif (const auto controller = window->sessionController()) {\n\t\t\tShowPremiumPreviewBox(controller, PremiumFeature::VoiceToText);\n\t\t\twindow->activate();\n\t\t}\n\t\treturn false;\n\t};\n\tconst auto date = langDateTime(base::unixtime::parse(until));\n\tconstexpr auto kToastDuration = crl::time(4000);\n\tconst auto text = left\n\t\t? tr::lng_audio_transcribe_trials_left(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tleft,\n\t\t\tlt_date,\n\t\t\t{ date },\n\t\t\ttr::marked)\n\t\t: tr::lng_audio_transcribe_trials_over(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\ttr::bold(date),\n\t\t\tlt_link,\n\t\t\ttr::link(tr::lng_settings_privacy_premium_link(tr::now)),\n\t\t\ttr::marked);\n\twindow->uiShow()->showToast(Ui::Toast::Config{\n\t\t.text = text,\n\t\t.filter = filter,\n\t\t.duration = kToastDuration,\n\t});\n}\n\nint ItemsForwardSendersCount(const HistoryItemsList &list) {\n\tauto peers = base::flat_set<not_null<PeerData*>>();\n\tauto names = base::flat_set<QString>();\n\tfor (const auto &item : list) {\n\t\tif (const auto peer = item->originalSender()) {\n\t\t\tpeers.emplace(peer);\n\t\t} else {\n\t\t\tnames.emplace(item->originalHiddenSenderInfo()->name);\n\t\t}\n\t}\n\treturn int(peers.size()) + int(names.size());\n}\n\nint ItemsForwardCaptionsCount(const HistoryItemsList &list) {\n\tauto result = 0;\n\tfor (const auto &item : list) {\n\t\tif (const auto media = item->media()) {\n\t\t\tconst auto hasCaption = !item->originalText().text.isEmpty()\n\t\t\t\t|| !media->consumedMessageText().text.isEmpty();\n\t\t\tif (hasCaption\n\t\t\t\t&& (media->allowsEditCaption() || media->poll())) {\n\t\t\t\t++result;\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_item_helpers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nclass History;\n\nnamespace style {\nstruct FlatLabel;\nstruct Checkbox;\n} // namespace style\n\nnamespace Api {\nstruct SendOptions;\nstruct SendAction;\n} // namespace Api\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nclass Story;\nclass Thread;\nstruct SendError;\nstruct SendErrorWithThread;\n} // namespace Data\n\nnamespace Main {\nclass Session;\nclass SessionShow;\n} // namespace Main\n\nnamespace Ui {\nclass BoxContent;\n} // namespace Ui\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nstruct HistoryMessageMarkupData;\n\nstruct PreparedServiceText {\n\tTextWithEntities text;\n\tstd::vector<ClickHandlerPtr> links;\n};\n\n[[nodiscard]] MessageFlags FlagsFromMTP(\n\tMsgId id,\n\tMTPDmessage::Flags flags,\n\tMessageFlags localFlags);\n[[nodiscard]] MessageFlags FlagsFromMTP(\n\tMsgId id,\n\tMTPDmessageService::Flags flags,\n\tMessageFlags localFlags);\n[[nodiscard]] MTPMessageReplyHeader NewMessageReplyHeader(\n\tconst Api::SendAction &action);\n\nenum class MediaCheckResult {\n\tGood,\n\tUnsupported,\n\tEmpty,\n\tHasExpiredMediaTimeToLive,\n\tHasUnsupportedTimeToLive,\n\tHasStoryMention,\n};\n[[nodiscard]] MediaCheckResult CheckMessageMedia(\n\tconst MTPMessageMedia &media);\n[[nodiscard]] CallId CallIdFromInput(const MTPInputGroupCall &data);\n\n[[nodiscard]] std::vector<not_null<UserData*>> ParseInvitedToCallUsers(\n\tnot_null<HistoryItem*> item,\n\tconst QVector<MTPlong> &users);\n\ninline constexpr auto kMaxUnreadReactions = 5; // Now 3, but just in case.\nusing OnStackUsers = std::array<UserData*, kMaxUnreadReactions>;\n[[nodiscard]] OnStackUsers LookupRecentUnreadReactedUsers(\n\tnot_null<HistoryItem*> item);\nvoid CheckReactionNotificationSchedule(\n\tnot_null<HistoryItem*> item,\n\tconst OnStackUsers &wasUsers);\nvoid CheckPollVoteNotificationSchedule(\n\tnot_null<HistoryItem*> item,\n\tconst std::vector<not_null<PeerData*>> &wasRecentVoters);\n[[nodiscard]] MessageFlags NewForwardedFlags(\n\tnot_null<PeerData*> peer,\n\tPeerId from,\n\tnot_null<HistoryItem*> fwd);\n[[nodiscard]] MessageFlags FinalizeMessageFlags(\n\tnot_null<History*> history,\n\tMessageFlags flags);\n[[nodiscard]] bool CopyMarkupToForward(not_null<const HistoryItem*> item);\n[[nodiscard]] TextWithEntities EnsureNonEmpty(\n\tconst TextWithEntities &text = TextWithEntities());\n[[nodiscard]] TextWithEntities UnsupportedMessageText();\n[[nodiscard]] HistoryMessageMarkupData UnsupportedMessageMarkup();\n\nvoid RequestDependentMessageItem(\n\tnot_null<HistoryItem*> item,\n\tPeerId peerId,\n\tMsgId msgId);\nvoid RequestDependentMessageStory(\n\tnot_null<HistoryItem*> item,\n\tPeerId peerId,\n\tStoryId storyId);\n[[nodiscard]] MessageFlags NewMessageFlags(not_null<PeerData*> peer);\n[[nodiscard]] TimeId NewMessageDate(TimeId scheduled);\n[[nodiscard]] TimeId NewMessageDate(const Api::SendOptions &options);\n[[nodiscard]] PeerId NewMessageFromId(const Api::SendAction &action);\n[[nodiscard]] QString NewMessagePostAuthor(const Api::SendAction &action);\n[[nodiscard]] bool ShouldSendSilent(\n\tnot_null<PeerData*> peer,\n\tconst Api::SendOptions &options);\n[[nodiscard]] HistoryItem *LookupReplyTo(\n\tnot_null<History*> history,\n\tFullMsgId replyToId);\n[[nodiscard]] MsgId LookupReplyToTop(\n\tnot_null<History*> history,\n\tHistoryItem *replyTo);\n[[nodiscard]] MsgId LookupReplyToTop(\n\tnot_null<History*> history,\n\tFullReplyTo replyTo);\n[[nodiscard]] bool LookupReplyIsTopicPost(HistoryItem *replyTo);\n\nstruct SendingErrorRequest {\n\tMsgId topicRootId = 0;\n\tconst HistoryItemsList *forward = nullptr;\n\tconst Data::Story *story = nullptr;\n\tconst TextWithTags *text = nullptr;\n\tint messagesCount = 0;\n\tbool ignoreSlowmodeCountdown = false;\n};\n[[nodiscard]] int ComputeSendingMessagesCount(\n\tnot_null<History*> history,\n\tconst SendingErrorRequest &request);\n[[nodiscard]] Data::SendError GetErrorForSending(\n\tnot_null<PeerData*> peer,\n\tSendingErrorRequest request);\n[[nodiscard]] Data::SendError GetErrorForSending(\n\tnot_null<Data::Thread*> thread,\n\tSendingErrorRequest request);\n\nstruct SendPaymentDetails {\n\tint messages = 0;\n\tint stars = 0;\n};\n[[nodiscard]] std::optional<SendPaymentDetails> ComputePaymentDetails(\n\tnot_null<PeerData*> peer,\n\tint messagesCount);\n\n[[nodiscard]] bool SuggestPaymentDataReady(\n\tnot_null<PeerData*> peer,\n\tSuggestOptions suggest);\n\nstruct PaidConfirmStyles {\n\tconst style::FlatLabel *label = nullptr;\n\tconst style::Checkbox *checkbox = nullptr;\n};\nvoid ShowSendPaidConfirm(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tSendPaymentDetails details,\n\tFn<void()> confirmed,\n\tPaidConfirmStyles styles = {},\n\tint suggestStarsPrice = 0);\nvoid ShowSendPaidConfirm(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tnot_null<PeerData*> peer,\n\tSendPaymentDetails details,\n\tFn<void()> confirmed,\n\tPaidConfirmStyles styles = {},\n\tint suggestStarsPrice = 0);\nvoid ShowSendPaidConfirm(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tconst std::vector<not_null<PeerData*>> &peers,\n\tSendPaymentDetails details,\n\tFn<void()> confirmed,\n\tPaidConfirmStyles styles = {},\n\tint suggestStarsPrice = 0);\n\nclass SendPaymentHelper final {\npublic:\n\t[[nodiscard]] bool check(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tApi::SendOptions options,\n\t\tint messagesCount,\n\t\tFn<void(int)> resend,\n\t\tPaidConfirmStyles styles = {});\n\t[[nodiscard]] bool check(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<PeerData*> peer,\n\t\tApi::SendOptions options,\n\t\tint messagesCount,\n\t\tFn<void(int)> resend,\n\t\tPaidConfirmStyles styles = {});\n\n\tvoid clear();\n\nprivate:\n\tFn<void()> _resend;\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] Data::SendErrorWithThread GetErrorForSending(\n\tconst std::vector<not_null<Data::Thread*>> &threads,\n\tSendingErrorRequest request);\n[[nodiscard]] object_ptr<Ui::BoxContent> MakeSendErrorBox(\n\tconst Data::SendErrorWithThread &error,\n\tbool withTitle);\n\n[[nodiscard]] TextWithEntities DropDisallowedCustomEmoji(\n\tnot_null<PeerData*> to,\n\tTextWithEntities text);\n\n[[nodiscard]] Main::Session *SessionByUniqueId(uint64 sessionUniqueId);\n[[nodiscard]] HistoryItem *MessageByGlobalId(GlobalMsgId globalId);\n\n[[nodiscard]] QDateTime ItemDateTime(not_null<const HistoryItem*> item);\n[[nodiscard]] QString ItemDateText(\n\tnot_null<const HistoryItem*> item,\n\tbool isUntilOnline);\n[[nodiscard]] bool IsItemScheduledUntilOnline(\n\tnot_null<const HistoryItem*> item);\n\n[[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler(\n\tnot_null<PeerData*> peer,\n\tMsgId msgId,\n\tFullMsgId returnToId = FullMsgId(),\n\tMessageHighlightId highlight = {});\n[[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler(\n\tnot_null<HistoryItem*> item,\n\tFullMsgId returnToId = FullMsgId(),\n\tMessageHighlightId highlight = {});\n[[nodiscard]] ClickHandlerPtr JumpToStoryClickHandler(\n\tnot_null<Data::Story*> story);\nClickHandlerPtr JumpToStoryClickHandler(\n\tnot_null<PeerData*> peer,\n\tStoryId storyId);\n[[nodiscard]] ClickHandlerPtr HideSponsoredClickHandler();\n[[nodiscard]] ClickHandlerPtr ReportSponsoredClickHandler(\n\tnot_null<HistoryItem*> item);\n[[nodiscard]] ClickHandlerPtr AboutSponsoredClickHandler();\n\n[[nodiscard]] not_null<HistoryItem*> GenerateJoinedMessage(\n\tnot_null<History*> history,\n\tTimeId inviteDate,\n\tnot_null<UserData*> inviter,\n\tbool viaRequest);\n\n[[nodiscard]] std::optional<bool> PeerHasThisCall(\n\tnot_null<PeerData*> peer,\n\tCallId id);\n[[nodiscard]] rpl::producer<bool> PeerHasThisCallValue(\n\tnot_null<PeerData*> peer,\n\tCallId id);\n[[nodiscard]] ClickHandlerPtr GroupCallClickHandler(\n\tnot_null<PeerData*> peer,\n\tCallId callId);\n\nvoid ShowTrialTranscribesToast(int left, TimeId until);\n\n[[nodiscard]] int ItemsForwardSendersCount(const HistoryItemsList &list);\n[[nodiscard]] int ItemsForwardCaptionsCount(const HistoryItemsList &list);\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_item_reply_markup.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_item_reply_markup.h\"\n\n#include \"data/data_session.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n\n#include <QtCore/QDataStream>\n\nnamespace {\n\n[[nodiscard]] HistoryMessageMarkupButton::Visual ParseVisual(\n\t\tconst tl::conditional<MTPKeyboardButtonStyle> &style) {\n\tif (!style) {\n\t\treturn {};\n\t}\n\tusing Color = HistoryMessageMarkupButton::Color;\n\tconst auto &data = style->data();\n\tif (data.vicon()) {\n\t\t[[maybe_unused]] int a = 0;\n\t}\n\treturn {\n\t\t.iconId = data.vicon().value_or_empty(),\n\t\t.color = (data.is_bg_danger()\n\t\t\t? Color::Danger\n\t\t\t: data.is_bg_primary()\n\t\t\t? Color::Primary\n\t\t\t: data.is_bg_success()\n\t\t\t? Color::Success\n\t\t\t: Color::Normal),\n\t};\n}\n\n} // namespace\n\nRequestPeerQuery RequestPeerQueryFromTL(\n\t\tconst MTPDkeyboardButtonRequestPeer &query) {\n\tusing Type = RequestPeerQuery::Type;\n\tusing Restriction = RequestPeerQuery::Restriction;\n\tauto result = RequestPeerQuery();\n\tresult.maxQuantity = query.vmax_quantity().v;\n\tconst auto restriction = [](const MTPBool *value) {\n\t\treturn !value\n\t\t\t? Restriction::Any\n\t\t\t: mtpIsTrue(*value)\n\t\t\t? Restriction::Yes\n\t\t\t: Restriction::No;\n\t};\n\tconst auto rights = [](const MTPChatAdminRights *value) {\n\t\treturn value ? ChatAdminRightsInfo(*value).flags : ChatAdminRights();\n\t};\n\tquery.vpeer_type().match([&](const MTPDrequestPeerTypeUser &data) {\n\t\tresult.type = Type::User;\n\t\tresult.userIsBot = restriction(data.vbot());\n\t\tresult.userIsPremium = restriction(data.vpremium());\n\t}, [&](const MTPDrequestPeerTypeChat &data) {\n\t\tresult.type = Type::Group;\n\t\tresult.amCreator = data.is_creator();\n\t\tresult.isBotParticipant = data.is_bot_participant();\n\t\tresult.groupIsForum = restriction(data.vforum());\n\t\tresult.hasUsername = restriction(data.vhas_username());\n\t\tresult.myRights = rights(data.vuser_admin_rights());\n\t\tresult.botRights = rights(data.vbot_admin_rights());\n\t}, [&](const MTPDrequestPeerTypeBroadcast &data) {\n\t\tresult.type = Type::Broadcast;\n\t\tresult.amCreator = data.is_creator();\n\t\tresult.hasUsername = restriction(data.vhas_username());\n\t\tresult.myRights = rights(data.vuser_admin_rights());\n\t\tresult.botRights = rights(data.vbot_admin_rights());\n\t}, [](const MTPDrequestPeerTypeCreateBot &) {\n\t});\n\treturn result;\n}\n\nInlineBots::PeerTypes PeerTypesFromMTP(\n\t\tconst MTPvector<MTPInlineQueryPeerType> &types) {\n\tusing namespace InlineBots;\n\tauto result = PeerTypes(0);\n\tfor (const auto &type : types.v) {\n\t\tresult |= type.match([&](const MTPDinlineQueryPeerTypePM &data) {\n\t\t\treturn PeerType::User;\n\t\t}, [&](const MTPDinlineQueryPeerTypeChat &data) {\n\t\t\treturn PeerType::Group;\n\t\t}, [&](const MTPDinlineQueryPeerTypeMegagroup &data) {\n\t\t\treturn PeerType::Group;\n\t\t}, [&](const MTPDinlineQueryPeerTypeBroadcast &data) {\n\t\t\treturn PeerType::Broadcast;\n\t\t}, [&](const MTPDinlineQueryPeerTypeBotPM &data) {\n\t\t\treturn PeerType::Bot;\n\t\t}, [&](const MTPDinlineQueryPeerTypeSameBotPM &data) {\n\t\t\treturn PeerType();\n\t\t});\n\t}\n\treturn result;\n}\n\nHistoryMessageMarkupButton::HistoryMessageMarkupButton(\n\tType type,\n\tconst QString &text,\n\tVisual visual,\n\tconst QByteArray &data,\n\tconst QString &forwardText,\n\tint64 buttonId)\n: type(type)\n, visual(visual)\n, text(text)\n, forwardText(forwardText)\n, data(data)\n, buttonId(buttonId) {\n}\n\nHistoryMessageMarkupButton *HistoryMessageMarkupButton::Get(\n\t\tnot_null<Data::Session*> owner,\n\t\tFullMsgId itemId,\n\t\tint row,\n\t\tint column) {\n\tif (const auto item = owner->message(itemId)) {\n\t\tif (const auto markup = item->Get<HistoryMessageReplyMarkup>()) {\n\t\t\tif (row < markup->data.rows.size()) {\n\t\t\t\tauto &buttons = markup->data.rows[row];\n\t\t\t\tif (column < buttons.size()) {\n\t\t\t\t\treturn &buttons[column];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid HistoryMessageMarkupData::fillRows(\n\t\tconst QVector<MTPKeyboardButtonRow> &list) {\n\trows.clear();\n\tif (list.isEmpty()) {\n\t\treturn;\n\t}\n\n\tusing Type = Button::Type;\n\trows.reserve(list.size());\n\tfor (const auto &row : list) {\n\t\trow.match([&](const MTPDkeyboardButtonRow &data) {\n\t\t\tauto row = std::vector<Button>();\n\t\t\trow.reserve(data.vbuttons().v.size());\n\t\t\tfor (const auto &button : data.vbuttons().v) {\n\t\t\t\tbutton.match([&](const MTPDkeyboardButton &data) {\n\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\tType::Default,\n\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\tParseVisual(data.vstyle()));\n\t\t\t\t}, [&](const MTPDkeyboardButtonCallback &data) {\n\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\t(data.is_requires_password()\n\t\t\t\t\t\t\t? Type::CallbackWithPassword\n\t\t\t\t\t\t\t: Type::Callback),\n\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\tParseVisual(data.vstyle()),\n\t\t\t\t\t\tqba(data.vdata()));\n\t\t\t\t}, [&](const MTPDkeyboardButtonRequestGeoLocation &data) {\n\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\tType::RequestLocation,\n\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\tParseVisual(data.vstyle()));\n\t\t\t\t}, [&](const MTPDkeyboardButtonRequestPhone &data) {\n\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\tType::RequestPhone,\n\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\tParseVisual(data.vstyle()));\n\t\t\t\t}, [&](const MTPDkeyboardButtonRequestPeer &data) {\n\t\t\t\t\tdata.vpeer_type().match([&](\n\t\t\t\t\t\t\tconst MTPDrequestPeerTypeCreateBot &createData) {\n\t\t\t\t\t\tauto serialized = QByteArray();\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tauto stream = QDataStream(\n\t\t\t\t\t\t\t\t&serialized,\n\t\t\t\t\t\t\t\tQIODevice::WriteOnly);\n\t\t\t\t\t\t\tstream\n\t\t\t\t\t\t\t\t<< qs(createData.vsuggested_name()\n\t\t\t\t\t\t\t\t\t.value_or_empty())\n\t\t\t\t\t\t\t\t<< qs(createData.vsuggested_username()\n\t\t\t\t\t\t\t\t\t.value_or_empty());\n\t\t\t\t\t\t}\n\t\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\t\tType::CreateBot,\n\t\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\t\tParseVisual(data.vstyle()),\n\t\t\t\t\t\t\tserialized,\n\t\t\t\t\t\t\tQString(),\n\t\t\t\t\t\t\tint64(data.vbutton_id().v));\n\t\t\t\t\t}, [&](const auto &) {\n\t\t\t\t\t\tconst auto query = RequestPeerQueryFromTL(data);\n\t\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\t\tType::RequestPeer,\n\t\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\t\tParseVisual(data.vstyle()),\n\t\t\t\t\t\t\tQByteArray(\n\t\t\t\t\t\t\t\treinterpret_cast<const char*>(&query),\n\t\t\t\t\t\t\t\tsizeof(query)),\n\t\t\t\t\t\t\tQString(),\n\t\t\t\t\t\t\tint64(data.vbutton_id().v));\n\t\t\t\t\t});\n\t\t\t\t}, [&](const MTPDkeyboardButtonUrl &data) {\n\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\tType::Url,\n\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\tParseVisual(data.vstyle()),\n\t\t\t\t\t\tqba(data.vurl()));\n\t\t\t\t}, [&](const MTPDkeyboardButtonSwitchInline &data) {\n\t\t\t\t\tconst auto type = data.is_same_peer()\n\t\t\t\t\t\t? Type::SwitchInlineSame\n\t\t\t\t\t\t: Type::SwitchInline;\n\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\ttype,\n\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\tParseVisual(data.vstyle()),\n\t\t\t\t\t\tqba(data.vquery()));\n\t\t\t\t\tif (type == Type::SwitchInline) {\n\t\t\t\t\t\t// Optimization flag.\n\t\t\t\t\t\t// Fast check on all new messages if there is a switch button to auto-click it.\n\t\t\t\t\t\tflags |= ReplyMarkupFlag::HasSwitchInlineButton;\n\t\t\t\t\t\tif (const auto types = data.vpeer_types()) {\n\t\t\t\t\t\t\trow.back().peerTypes = PeerTypesFromMTP(*types);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}, [&](const MTPDkeyboardButtonGame &data) {\n\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\tType::Game,\n\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\tParseVisual(data.vstyle()));\n\t\t\t\t}, [&](const MTPDkeyboardButtonBuy &data) {\n\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\tType::Buy,\n\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\tParseVisual(data.vstyle()));\n\t\t\t\t}, [&](const MTPDkeyboardButtonUrlAuth &data) {\n\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\tType::Auth,\n\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\tParseVisual(data.vstyle()),\n\t\t\t\t\t\tqba(data.vurl()),\n\t\t\t\t\t\tqs(data.vfwd_text().value_or_empty()),\n\t\t\t\t\t\tdata.vbutton_id().v);\n\t\t\t\t}, [&](const MTPDkeyboardButtonRequestPoll &data) {\n\t\t\t\t\tconst auto quiz = [&] {\n\t\t\t\t\t\tif (!data.vquiz()) {\n\t\t\t\t\t\t\treturn QByteArray();\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn data.vquiz()->match([&](const MTPDboolTrue&) {\n\t\t\t\t\t\t\treturn QByteArray(1, 1);\n\t\t\t\t\t\t}, [&](const MTPDboolFalse&) {\n\t\t\t\t\t\t\treturn QByteArray(1, 0);\n\t\t\t\t\t\t});\n\t\t\t\t\t}();\n\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\tType::RequestPoll,\n\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\tParseVisual(data.vstyle()),\n\t\t\t\t\t\tquiz);\n\t\t\t\t}, [&](const MTPDkeyboardButtonUserProfile &data) {\n\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\tType::UserProfile,\n\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\tParseVisual(data.vstyle()),\n\t\t\t\t\t\tQByteArray::number(data.vuser_id().v));\n\t\t\t\t}, [&](const MTPDinputKeyboardButtonUrlAuth &data) {\n\t\t\t\t\tLOG((\"API Error: inputKeyboardButtonUrlAuth.\"));\n\t\t\t\t\t// Should not get those for the users.\n\t\t\t\t}, [&](const MTPDinputKeyboardButtonUserProfile &data) {\n\t\t\t\t\tLOG((\"API Error: inputKeyboardButtonUserProfile.\"));\n\t\t\t\t\t// Should not get those for the users.\n\t\t\t\t}, [&](const MTPDkeyboardButtonWebView &data) {\n\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\tType::WebView,\n\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\tParseVisual(data.vstyle()),\n\t\t\t\t\t\tdata.vurl().v);\n\t\t\t\t}, [&](const MTPDkeyboardButtonSimpleWebView &data) {\n\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\tType::SimpleWebView,\n\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\tParseVisual(data.vstyle()),\n\t\t\t\t\t\tdata.vurl().v);\n\t\t\t\t}, [&](const MTPDkeyboardButtonCopy &data) {\n\t\t\t\t\trow.emplace_back(\n\t\t\t\t\t\tType::CopyText,\n\t\t\t\t\t\tqs(data.vtext()),\n\t\t\t\t\t\tParseVisual(data.vstyle()),\n\t\t\t\t\t\tdata.vcopy_text().v);\n\t\t\t\t}, [&](const MTPDinputKeyboardButtonRequestPeer &data) {\n\t\t\t\t\tLOG((\"API Error: inputKeyboardButtonRequestPeer.\"));\n\t\t\t\t\t// Should not get those for the users.\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (!row.empty()) {\n\t\t\t\trows.push_back(std::move(row));\n\t\t\t}\n\t\t});\n\t}\n\tif (rows.size() == 1\n\t\t&& rows.front().size() == 1\n\t\t&& rows.front().front().type == Type::Buy) {\n\t\tflags |= ReplyMarkupFlag::OnlyBuyButton;\n\t}\n}\n\nHistoryMessageMarkupData::HistoryMessageMarkupData(\n\t\tconst MTPReplyMarkup *data) {\n\tif (!data) {\n\t\treturn;\n\t}\n\tusing Flag = ReplyMarkupFlag;\n\tdata->match([&](const MTPDreplyKeyboardMarkup &data) {\n\t\tflags = (data.is_resize() ? Flag::Resize : Flag())\n\t\t\t| (data.is_selective() ? Flag::Selective : Flag())\n\t\t\t| (data.is_single_use() ? Flag::SingleUse : Flag())\n\t\t\t| (data.is_persistent() ? Flag::Persistent : Flag());\n\t\tplaceholder = qs(data.vplaceholder().value_or_empty());\n\t\tfillRows(data.vrows().v);\n\t}, [&](const MTPDreplyInlineMarkup &data) {\n\t\tflags = Flag::Inline;\n\t\tplaceholder = QString();\n\t\tfillRows(data.vrows().v);\n\t}, [&](const MTPDreplyKeyboardHide &data) {\n\t\tflags = Flag::None\n\t\t\t| (data.is_selective() ? Flag::Selective : Flag());\n\t\tplaceholder = QString();\n\t}, [&](const MTPDreplyKeyboardForceReply &data) {\n\t\tflags = Flag::ForceReply\n\t\t\t| (data.is_selective() ? Flag::Selective : Flag())\n\t\t\t| (data.is_single_use() ? Flag::SingleUse : Flag());\n\t\tplaceholder = qs(data.vplaceholder().value_or_empty());\n\t});\n}\n\nvoid HistoryMessageMarkupData::fillForwardedData(\n\t\tconst HistoryMessageMarkupData &original) {\n\tExpects(isNull());\n\tExpects(!original.isNull());\n\n\tflags = original.flags;\n\tplaceholder = original.placeholder;\n\n\trows.reserve(original.rows.size());\n\tusing Type = HistoryMessageMarkupButton::Type;\n\tfor (const auto &existing : original.rows) {\n\t\tauto row = std::vector<Button>();\n\t\trow.reserve(existing.size());\n\t\tfor (const auto &button : existing) {\n\t\t\tconst auto newType = (button.type != Type::SwitchInlineSame)\n\t\t\t\t? button.type\n\t\t\t\t: Type::SwitchInline;\n\t\t\tconst auto text = button.forwardText.isEmpty()\n\t\t\t\t? button.text\n\t\t\t\t: button.forwardText;\n\t\t\trow.emplace_back(\n\t\t\t\tnewType,\n\t\t\t\ttext,\n\t\t\t\tbutton.visual,\n\t\t\t\tbutton.data,\n\t\t\t\tQString(),\n\t\t\t\tbutton.buttonId);\n\t\t}\n\t\tif (!row.empty()) {\n\t\t\trows.push_back(std::move(row));\n\t\t}\n\t}\n}\n\nbool HistoryMessageMarkupData::isNull() const {\n\tif (flags & ReplyMarkupFlag::IsNull) {\n\t\tAssert(isTrivial());\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool HistoryMessageMarkupData::isTrivial() const {\n\treturn rows.empty()\n\t\t&& placeholder.isEmpty()\n\t\t&& !(flags & ~ReplyMarkupFlag::IsNull);\n}\n\nHistoryMessageRepliesData::HistoryMessageRepliesData(\n\t\tconst MTPMessageReplies *data) {\n\tif (!data) {\n\t\treturn;\n\t}\n\tconst auto &fields = data->data();\n\tif (const auto list = fields.vrecent_repliers()) {\n\t\trecentRepliers.reserve(list->v.size());\n\t\tfor (const auto &id : list->v) {\n\t\t\trecentRepliers.push_back(peerFromMTP(id));\n\t\t}\n\t}\n\trepliesCount = fields.vreplies().v;\n\tchannelId = ChannelId(fields.vchannel_id().value_or_empty());\n\treadMaxId = fields.vread_max_id().value_or_empty();\n\tmaxId = fields.vmax_id().value_or_empty();\n\tisNull = false;\n\tpts = fields.vreplies_pts().v;\n}\n\nHistoryMessageSuggestInfo::HistoryMessageSuggestInfo(\n\t\tconst MTPSuggestedPost *data) {\n\tif (!data) {\n\t\treturn;\n\t}\n\tconst auto &fields = data->data();\n\tprice = CreditsAmountFromTL(fields.vprice());\n\tdate = fields.vschedule_date().value_or_empty();\n\taccepted = fields.is_accepted();\n\trejected = fields.is_rejected();\n\texists = true;\n}\n\nHistoryMessageSuggestInfo::HistoryMessageSuggestInfo(\n\tconst Api::SendOptions &options)\n: HistoryMessageSuggestInfo(options.suggest) {\n}\n\nHistoryMessageSuggestInfo::HistoryMessageSuggestInfo(\n\t\tSuggestOptions options) {\n\tif (!options.exists) {\n\t\treturn;\n\t}\n\tprice = options.price();\n\tdate = options.date;\n\texists = true;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_item_reply_markup.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n#include \"data/data_chat_participant_status.h\"\n\nnamespace Api {\nstruct SendOptions;\n} // namespace Api\n\nnamespace Data {\nclass Session;\n} // namespace Data\n\nnamespace InlineBots {\nenum class PeerType : uint8;\nusing PeerTypes = base::flags<PeerType>;\n} // namespace InlineBots\n\n[[nodiscard]] InlineBots::PeerTypes PeerTypesFromMTP(\n\tconst MTPvector<MTPInlineQueryPeerType> &types);\n\nenum class ReplyMarkupFlag : uint32 {\n\tNone                  = (1U << 0),\n\tForceReply            = (1U << 1),\n\tHasSwitchInlineButton = (1U << 2),\n\tInline                = (1U << 3),\n\tResize                = (1U << 4),\n\tSingleUse             = (1U << 5),\n\tSelective             = (1U << 6),\n\tIsNull                = (1U << 7),\n\tOnlyBuyButton         = (1U << 8),\n\tPersistent            = (1U << 9),\n\tSuggestionDecline     = (1U << 10),\n\tSuggestionAccept      = (1U << 11),\n\tSuggestionSeparator   = (1U << 12),\n};\ninline constexpr bool is_flag_type(ReplyMarkupFlag) { return true; }\nusing ReplyMarkupFlags = base::flags<ReplyMarkupFlag>;\n\nstruct RequestPeerQuery {\n\tenum class Type : uchar {\n\t\tUser,\n\t\tGroup,\n\t\tBroadcast,\n\t};\n\tenum class Restriction : uchar {\n\t\tAny,\n\t\tYes,\n\t\tNo,\n\t};\n\n\tint maxQuantity = 0;\n\tType type = Type::User;\n\tRestriction userIsBot = Restriction::Any;\n\tRestriction userIsPremium = Restriction::Any;\n\tRestriction groupIsForum = Restriction::Any;\n\tRestriction hasUsername = Restriction::Any;\n\tbool amCreator = false;\n\tbool isBotParticipant = false;\n\tChatAdminRights myRights = {};\n\tChatAdminRights botRights = {};\n};\nstatic_assert(std::is_trivially_copy_assignable_v<RequestPeerQuery>);\n\nclass MTPDkeyboardButtonRequestPeer;\n\n[[nodiscard]] RequestPeerQuery RequestPeerQueryFromTL(\n\tconst MTPDkeyboardButtonRequestPeer &query);\n\nstruct HistoryMessageMarkupButton {\n\tenum class Type : uchar {\n\t\tDefault,\n\t\tUrl,\n\t\tCallback,\n\t\tCallbackWithPassword,\n\t\tRequestPhone,\n\t\tRequestLocation,\n\t\tRequestPoll,\n\t\tRequestPeer,\n\t\tSwitchInline,\n\t\tSwitchInlineSame,\n\t\tGame,\n\t\tBuy,\n\t\tAuth,\n\t\tUserProfile,\n\t\tWebView,\n\t\tSimpleWebView,\n\t\tCopyText,\n\n\t\tSuggestDecline,\n\t\tSuggestAccept,\n\t\tSuggestChange,\n\t\tCreateBot,\n\t};\n\n\tenum class Color : uchar {\n\t\tNormal,\n\t\tPrimary,\n\t\tDanger,\n\t\tSuccess,\n\t};\n\n\tstruct Visual {\n\t\tDocumentId iconId = 0;\n\t\tColor color = Color::Normal;\n\t};\n\n\tHistoryMessageMarkupButton(\n\t\tType type,\n\t\tconst QString &text,\n\t\tVisual visual,\n\t\tconst QByteArray &data = QByteArray(),\n\t\tconst QString &forwardText = QString(),\n\t\tint64 buttonId = 0);\n\n\tstatic HistoryMessageMarkupButton *Get(\n\t\tnot_null<Data::Session*> owner,\n\t\tFullMsgId itemId,\n\t\tint row,\n\t\tint column);\n\n\tType type;\n\tVisual visual;\n\tQString text, forwardText;\n\tQByteArray data;\n\tint64 buttonId = 0;\n\tInlineBots::PeerTypes peerTypes = 0;\n\tmutable mtpRequestId requestId = 0;\n\n};\n\nstruct HistoryMessageMarkupData {\n\tHistoryMessageMarkupData() = default;\n\texplicit HistoryMessageMarkupData(const MTPReplyMarkup *data);\n\n\tvoid fillForwardedData(const HistoryMessageMarkupData &original);\n\n\t[[nodiscard]] bool isNull() const;\n\t[[nodiscard]] bool isTrivial() const;\n\n\tusing Button = HistoryMessageMarkupButton;\n\tstd::vector<std::vector<Button>> rows;\n\tReplyMarkupFlags flags = ReplyMarkupFlag::IsNull;\n\tQString placeholder;\n\nprivate:\n\tvoid fillRows(const QVector<MTPKeyboardButtonRow> &v);\n\n};\n\nstruct HistoryMessageRepliesData {\n\tHistoryMessageRepliesData() = default;\n\texplicit HistoryMessageRepliesData(const MTPMessageReplies *data);\n\n\tstd::vector<PeerId> recentRepliers;\n\tChannelId channelId = 0;\n\tMsgId readMaxId = 0;\n\tMsgId maxId = 0;\n\tint repliesCount = 0;\n\tbool isNull = true;\n\tint pts = 0;\n};\n\nstruct HistoryMessageSuggestInfo {\n\tHistoryMessageSuggestInfo() = default;\n\texplicit HistoryMessageSuggestInfo(const MTPSuggestedPost *data);\n\texplicit HistoryMessageSuggestInfo(const Api::SendOptions &options);\n\texplicit HistoryMessageSuggestInfo(SuggestOptions options);\n\n\tCreditsAmount price;\n\tTimeId date = 0;\n\tbool accepted = false;\n\tbool rejected = false;\n\tbool exists = false;\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_item_text.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_item_text.h\"\n\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_web_page.h\"\n#include \"data/data_groups.h\"\n#include \"data/data_peer.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/text/text_options.h\"\n\nTextForMimeData HistoryItemText(not_null<HistoryItem*> item) {\n\tconst auto media = item->media();\n\n\tauto mediaResult = media ? media->clipboardText() : TextForMimeData();\n\tauto textResult = mediaResult.empty()\n\t\t? item->clipboardText()\n\t\t: TextForMimeData();\n\tauto logEntryOriginalResult = [&] {\n\t\tconst auto entry = item->Get<HistoryMessageLogEntryOriginal>();\n\t\tif (!entry) {\n\t\t\treturn TextForMimeData();\n\t\t}\n\t\tconst auto title = TextUtilities::SingleLine(\n\t\t\tentry->page->title.isEmpty()\n\t\t\t\t? entry->page->author\n\t\t\t\t: entry->page->title);\n\t\tauto titleResult = TextForMimeData::Rich(\n\t\t\tTextUtilities::ParseEntities(\n\t\t\t\ttitle,\n\t\t\t\tUi::WebpageTextTitleOptions().flags));\n\t\tauto descriptionResult = TextForMimeData::Rich(\n\t\t\tbase::duplicate(entry->page->description));\n\t\tif (titleResult.empty()) {\n\t\t\treturn descriptionResult;\n\t\t} else if (descriptionResult.empty()) {\n\t\t\treturn titleResult;\n\t\t}\n\t\ttitleResult.append('\\n').append(std::move(descriptionResult));\n\t\treturn titleResult;\n\t}();\n\tauto factcheckResult = [&] {\n\t\tconst auto factcheck = item->Get<HistoryMessageFactcheck>();\n\t\treturn factcheck\n\t\t\t? TextForMimeData::Rich(base::duplicate(factcheck->data.text))\n\t\t\t: TextForMimeData();\n\t}();\n\tauto result = textResult;\n\tif (result.empty()) {\n\t\tresult = std::move(mediaResult);\n\t} else if (!mediaResult.empty()) {\n\t\tresult.append(qstr(\"\\n\\n\")).append(std::move(mediaResult));\n\t}\n\tif (result.empty()) {\n\t\tresult = std::move(logEntryOriginalResult);\n\t} else if (!logEntryOriginalResult.empty()) {\n\t\tresult.append(u\"\\n\\n\"_q).append(std::move(logEntryOriginalResult));\n\t}\n\tif (result.empty()) {\n\t\tresult = std::move(factcheckResult);\n\t} else if (!factcheckResult.empty()) {\n\t\tresult.append(u\"\\n\\n\"_q).append(std::move(factcheckResult));\n\t}\n\treturn result;\n}\n\nTextForMimeData HistoryGroupText(not_null<const Data::Group*> group) {\n\tExpects(!group->items.empty());\n\n\tconst auto columnAlbum = [&] {\n\t\tconst auto item = group->items.front();\n\t\tif (const auto media = item->media()) {\n\t\t\tif (const auto document = media->document()) {\n\t\t\t\treturn !document->isVideoFile();\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}();\n\tconst auto hasCaption = [](not_null<HistoryItem*> item) {\n\t\treturn !item->clipboardText().empty();\n\t};\n\tif (columnAlbum) {\n\t\tconst auto simple = !ranges::any_of(group->items, hasCaption);\n\t\tif (!simple) {\n\t\t\tauto result = TextForMimeData();\n\t\t\tfor (const auto &item : group->items) {\n\t\t\t\tif (result.empty()) {\n\t\t\t\t\tresult = HistoryItemText(item);\n\t\t\t\t} else {\n\t\t\t\t\tresult.append(u\"\\n\\n\"_q).append(HistoryItemText(item));\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\t}\n\treturn [&] {\n\t\tauto &&nonempty = ranges::views::all(\n\t\t\tgroup->items\n\t\t) | ranges::views::filter(\n\t\t\thasCaption\n\t\t) | ranges::views::take(2);\n\t\tauto first = nonempty.begin();\n\t\tauto end = nonempty.end();\n\t\tif (first == end) {\n\t\t\treturn TextForMimeData();\n\t\t}\n\t\tauto result = (*first)->clipboardText();\n\t\treturn (++first == end) ? result : TextForMimeData();\n\t}();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_item_text.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass HistoryItem;\n\nnamespace Data {\nstruct Group;\n} // namespace Data\n\nTextForMimeData HistoryItemText(not_null<HistoryItem*> item);\nTextForMimeData HistoryGroupText(not_null<const Data::Group*> group);\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_location_manager.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_location_manager.h\"\n\n#include \"mainwidget.h\"\n#include \"core/file_utilities.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/image/image.h\"\n#include \"data/data_file_origin.h\"\n#include \"platform/platform_specific.h\"\n\nQString LocationClickHandler::copyToClipboardText() const {\n\treturn _text;\n}\n\nQString LocationClickHandler::copyToClipboardContextItemText() const {\n\treturn tr::lng_context_copy_link(tr::now);\n}\n\nvoid LocationClickHandler::onClick(ClickContext context) const {\n\tPlatform::LaunchMaps(_point, [text = _text] {\n\t\tFile::OpenUrl(text);\n\t});\n}\n\nvoid LocationClickHandler::setup() {\n\t_text = Url(_point);\n}\n\nQString LocationClickHandler::Url(const Data::LocationPoint &point) {\n\tconst auto latlon = point.latAsString() + ',' + point.lonAsString();\n\treturn u\"https://maps.google.com/maps?q=\"_q\n\t\t+ latlon\n\t\t+ u\"&ll=\"_q\n\t\t+ latlon\n\t\t+ u\"&z=16\"_q;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_location_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_location.h\"\n\nclass LocationClickHandler : public ClickHandler {\npublic:\n\tLocationClickHandler(const Data::LocationPoint &point)\n\t: _point(point) {\n\t\tsetup();\n\t}\n\n\tstatic QString Url(const Data::LocationPoint &coords);\n\n\tvoid onClick(ClickContext context) const override;\n\n\tQString tooltip() const override {\n\t\treturn QString();\n\t}\n\n\tQString url() const override {\n\t\treturn _text;\n\t}\n\n\tQString dragText() const override {\n\t\treturn _text;\n\t}\n\n\tQString copyToClipboardText() const override;\n\tQString copyToClipboardContextItemText() const override;\n\nprivate:\n\n\tvoid setup();\n\tData::LocationPoint _point;\n\tQString _text;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_message.cpp",
    "content": ""
  },
  {
    "path": "Telegram/SourceFiles/history/history_streamed_drafts.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_streamed_drafts.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_peer_id.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"main/main_session.h\"\n\nnamespace {\n\nconstexpr auto kClearTimeout = 30 * crl::time(1000);\n\n[[nodiscard]] int CommonPrefixLength(const QString &a, const QString &b) {\n\tconst auto count = std::min(a.size(), b.size());\n\tauto i = 0;\n\twhile (i < count && a[i] == b[i]) {\n\t\t++i;\n\t}\n\treturn i;\n}\n\n} // namespace\n\nHistoryStreamedDrafts::HistoryStreamedDrafts(not_null<History*> history)\n: _history(history)\n, _checkTimer([=] { check(); }) {\n}\n\nHistoryStreamedDrafts::~HistoryStreamedDrafts() {\n\tfor (const auto &[randomId, draft] : base::take(_drafts)) {\n\t\tdraft.message->destroy();\n\t}\n}\n\nTextWithEntities HistoryStreamedDrafts::loadingEmoji() {\n\tif (_loadingEmoji.empty()) {\n\t\t_loadingEmoji = Data::SingleCustomEmoji(\n\t\t\tChatHelpers::GenerateLocalTgsSticker(\n\t\t\t\t&_history->session(),\n\t\t\t\tu\"transcribe_loading\"_q,\n\t\t\t\ttrue));\n\t}\n\treturn _loadingEmoji;\n}\n\nvoid HistoryStreamedDrafts::apply(\n\t\tMsgId rootId,\n\t\tPeerId fromId,\n\t\tTimeId when,\n\t\tconst MTPDsendMessageTextDraftAction &data) {\n\tconst auto replyToId = rootId\n\t\t? FullMsgId(_history->peer->id, rootId)\n\t\t: FullMsgId();\n\tif (!rootId) {\n\t\trootId = Data::ForumTopic::kGeneralId;\n\t}\n\tconst auto randomId = data.vrandom_id().v;\n\tif (!when) {\n\t\tclearByRandomId(randomId);\n\t\treturn;\n\t}\n\tconst auto text = Api::ParseTextWithEntities(\n\t\t&_history->session(),\n\t\tdata.vtext()\n\t).append(loadingEmoji());\n\tif (update(randomId, text)) {\n\t\treturn;\n\t}\n\t_drafts.emplace(randomId, Draft{\n\t\t.message = _history->addNewLocalMessage({\n\t\t\t.id = _history->owner().nextLocalMessageId(),\n\t\t\t.flags = (MessageFlag::Local\n\t\t\t\t| MessageFlag::HasReplyInfo\n\t\t\t\t| MessageFlag::TextAppearing),\n\t\t\t.from = fromId,\n\t\t\t.replyTo = {\n\t\t\t\t.messageId = replyToId,\n\t\t\t\t.topicRootId = rootId,\n\t\t\t},\n\t\t\t.date = when,\n\t\t}, text, MTP_messageMediaEmpty()),\n\t\t.rootId = rootId,\n\t\t.fromId = fromId,\n\t\t.updated = crl::now(),\n\t});\n\tif (!_checkTimer.isActive()) {\n\t\t_checkTimer.callOnce(kClearTimeout);\n\t}\n\tcrl::on_main(this, [=] {\n\t\tcrl::on_main(this, [=] {\n\t\t\t// Thread topics create views for messages in double on_main:\n\t\t\t// - First we postpone HistoryUpdate::Flag::ClientSideMessages.\n\t\t\t// - Then we postpone RepliesList push of new messages list.\n\t\t\tconst auto i = _drafts.find(randomId);\n\t\t\tif (i != end(_drafts)) {\n\t\t\t\ti->second.message->markTextAppearingStarted();\n\t\t\t}\n\t\t});\n\t});\n}\n\nbool HistoryStreamedDrafts::update(\n\t\tuint64 randomId,\n\t\tconst TextWithEntities &text) {\n\tconst auto i = _drafts.find(randomId);\n\tif (i == end(_drafts)) {\n\t\treturn false;\n\t}\n\tconst auto item = i->second.message;\n\titem->setText(text);\n\titem->invalidateChatListEntry();\n\ti->second.updated = crl::now();\n\treturn true;\n}\n\nvoid HistoryStreamedDrafts::clearByRandomId(uint64 randomId) {\n\tif (const auto draft = _drafts.take(randomId)) {\n\t\tdraft->message->destroy();\n\t}\n\tif (_drafts.empty()) {\n\t\tscheduleDestroy();\n\t}\n}\n\nbool HistoryStreamedDrafts::hasFor(not_null<HistoryItem*> item) const {\n\tconst auto rootId = item->topicRootId();\n\tconst auto from = item->from();\n\tfor (const auto &[randomId, draft] : _drafts) {\n\t\tif (draft.rootId == rootId && draft.message->from() == from) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid HistoryStreamedDrafts::applyItemRemoved(not_null<HistoryItem*> item) {\n\tfor (auto i = begin(_drafts); i != end(_drafts); ++i) {\n\t\tif (i->second.message == item) {\n\t\t\t_drafts.erase(i);\n\t\t\tif (_drafts.empty()) {\n\t\t\t\tscheduleDestroy();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nHistoryItem *HistoryStreamedDrafts::adoptIncoming(\n\t\tconst MTPDmessage &data) {\n\tif (_drafts.empty()) {\n\t\treturn nullptr;\n\t}\n\tconst auto fromId = data.vfrom_id()\n\t\t? peerFromMTP(*data.vfrom_id())\n\t\t: _history->peer->id;\n\tauto rootId = MsgId(0);\n\tif (const auto reply = data.vreply_to()) {\n\t\treply->match([&](const MTPDmessageReplyHeader &d) {\n\t\t\tif (d.is_forum_topic()) {\n\t\t\t\trootId = d.vreply_to_top_id().value_or_empty();\n\t\t\t\tif (!rootId) {\n\t\t\t\t\trootId = d.vreply_to_msg_id().value_or_empty();\n\t\t\t\t}\n\t\t\t}\n\t\t}, [](const MTPDmessageReplyStoryHeader &) {});\n\t}\n\tif (!rootId) {\n\t\trootId = Data::ForumTopic::kGeneralId;\n\t}\n\tconst auto incomingText = qs(data.vmessage());\n\tauto best = end(_drafts);\n\tauto bestPrefix = 0;\n\tfor (auto i = begin(_drafts); i != end(_drafts); ++i) {\n\t\tconst auto &draft = i->second;\n\t\tif (draft.rootId != rootId) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (draft.message->from()->id != fromId) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto prefix = CommonPrefixLength(\n\t\t\tdraft.message->originalText().text,\n\t\t\tincomingText);\n\t\tif (prefix > bestPrefix) {\n\t\t\tbestPrefix = prefix;\n\t\t\tbest = i;\n\t\t}\n\t}\n\tif (best == end(_drafts) || bestPrefix <= 0) {\n\t\treturn nullptr;\n\t}\n\tconst auto item = best->second.message.get();\n\t_drafts.erase(best);\n\n\titem->setRealId(data.vid().v);\n\tif (const auto topic = item->topic()) {\n\t\ttopic->applyMaybeLast(item);\n\t}\n\tif (const auto sublist = item->savedSublist()) {\n\t\tsublist->applyMaybeLast(item);\n\t}\n\t_history->owner().updateExistingMessage(data);\n\n\tif (_drafts.empty()) {\n\t\tscheduleDestroy();\n\t}\n\treturn item;\n}\n\nvoid HistoryStreamedDrafts::check() {\n\tauto closest = crl::time();\n\tconst auto now = crl::now();\n\tfor (auto i = begin(_drafts); i != end(_drafts);) {\n\t\tif (now - i->second.updated >= kClearTimeout) {\n\t\t\tconst auto message = i->second.message;\n\t\t\ti = _drafts.erase(i);\n\t\t\tmessage->destroy();\n\t\t} else {\n\t\t\tif (!closest || closest > i->second.updated) {\n\t\t\t\tclosest = i->second.updated;\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t}\n\tif (closest) {\n\t\t_checkTimer.callOnce(kClearTimeout - (now - closest));\n\t} else {\n\t\tscheduleDestroy();\n\t}\n}\n\nvoid HistoryStreamedDrafts::scheduleDestroy() {\n\tExpects(_drafts.empty());\n\n\tcrl::on_main(this, [=] {\n\t\tif (_drafts.empty()) {\n\t\t\t_destroyRequests.fire({});\n\t\t}\n\t});\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_streamed_drafts.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n\nclass History;\nclass MTPDmessage;\n\nclass HistoryStreamedDrafts final : public base::has_weak_ptr {\npublic:\n\texplicit HistoryStreamedDrafts(not_null<History*> history);\n\t~HistoryStreamedDrafts();\n\n\t[[nodiscard]] rpl::producer<> destroyRequests() const;\n\n\tvoid apply(\n\t\tMsgId rootId,\n\t\tPeerId fromId,\n\t\tTimeId when,\n\t\tconst MTPDsendMessageTextDraftAction &data);\n\n\t[[nodiscard]] bool hasFor(not_null<HistoryItem*> item) const;\n\tvoid applyItemRemoved(not_null<HistoryItem*> item);\n\tHistoryItem *adoptIncoming(const MTPDmessage &data);\n\nprivate:\n\tstruct Draft {\n\t\tnot_null<HistoryItem*> message;\n\t\tMsgId rootId = 0;\n\t\tPeerId fromId = 0;\n\t\tcrl::time updated = 0;\n\t};\n\n\tbool update(uint64 randomId, const TextWithEntities &text);\n\tvoid clearByRandomId(uint64 randomId);\n\n\tvoid check();\n\tvoid scheduleDestroy();\n\n\t[[nodiscard]] TextWithEntities loadingEmoji();\n\n\tconst not_null<History*> _history;\n\tbase::flat_map<uint64, Draft> _drafts;\n\n\tbase::Timer _checkTimer;\n\n\trpl::event_stream<> _destroyRequests;\n\n\tTextWithEntities _loadingEmoji;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_translation.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_translation.h\"\n\n#include \"data/data_changes.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n\nnamespace {\n\nusing UpdateFlag = Data::HistoryUpdate::Flag;\n\n} // namespace\n\nHistoryTranslation::HistoryTranslation(\n\tnot_null<History*> history,\n\tconst LanguageId &offerFrom)\n: _history(history) {\n\tthis->offerFrom(offerFrom);\n}\n\nvoid HistoryTranslation::offerFrom(LanguageId id) {\n\tif (_offerFrom == id) {\n\t\treturn;\n\t}\n\t_offerFrom = id;\n\tauto &changes = _history->session().changes();\n\tchanges.historyUpdated(_history, UpdateFlag::TranslateFrom);\n}\n\nLanguageId HistoryTranslation::offeredFrom() const {\n\treturn _offerFrom;\n}\n\nvoid HistoryTranslation::translateTo(LanguageId id) {\n\tif (_translatedTo == id) {\n\t\treturn;\n\t}\n\t_translatedTo = id;\n\tauto &changes = _history->session().changes();\n\tchanges.historyUpdated(_history, UpdateFlag::TranslatedTo);\n}\n\nLanguageId HistoryTranslation::translatedTo() const {\n\treturn _translatedTo;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_translation.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"spellcheck/spellcheck_types.h\"\n\nclass History;\n\nclass HistoryTranslation final {\npublic:\n\tHistoryTranslation(\n\t\tnot_null<History*> history,\n\t\tconst LanguageId &offerFrom);\n\n\tvoid offerFrom(LanguageId id);\n\t[[nodiscard]] LanguageId offeredFrom() const;\n\n\tvoid translateTo(LanguageId id);\n\t[[nodiscard]] LanguageId translatedTo() const;\n\nprivate:\n\tconst not_null<History*> _history;\n\n\tLanguageId _offerFrom;\n\tLanguageId _translatedTo;\n\n};"
  },
  {
    "path": "Telegram/SourceFiles/history/history_unread_things.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_unread_things.h\"\n\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_chat_filters.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n\nnamespace HistoryUnreadThings {\nnamespace {\n\ntemplate <typename Update>\n[[nodiscard]] typename Update::Flag UpdateFlag(Type type) {\n\tusing Flag = typename Update::Flag;\n\tswitch (type) {\n\tcase Type::Mentions: return Flag::UnreadMentions;\n\tcase Type::Reactions: return Flag::UnreadReactions;\n\tcase Type::PollVotes: return Flag::UnreadPollVotes;\n\t}\n\tUnexpected(\"Type in HistoryUnreadThings::UpdateFlag.\");\n}\n\n[[nodiscard]] Data::HistoryUpdate::Flag HistoryUpdateFlag(Type type) {\n\treturn UpdateFlag<Data::HistoryUpdate>(type);\n}\n\n[[nodiscard]] Data::TopicUpdate::Flag TopicUpdateFlag(Type type) {\n\treturn UpdateFlag<Data::TopicUpdate>(type);\n}\n\n[[nodiscard]] Data::SublistUpdate::Flag SublistUpdateFlag(Type type) {\n\tExpects(type == Type::Reactions || type == Type::PollVotes);\n\n\tif (type == Type::PollVotes) {\n\t\treturn Data::SublistUpdate::Flag::UnreadPollVotes;\n\t}\n\treturn Data::SublistUpdate::Flag::UnreadReactions;\n}\n\n} // namespace\n\nvoid Proxy::setCount(int count) {\n\tif (!_known) {\n\t\t_thread->setUnreadThingsKnown();\n\t}\n\tif (!_data) {\n\t\tif (!count) {\n\t\t\treturn;\n\t\t}\n\t\tcreateData();\n\t}\n\tauto &list = resolveList();\n\tif (!count) {\n\t\tlist.clear();\n\t}\n\tconst auto loaded = list.loadedCount();\n\tif (loaded > count) {\n\t\tLOG((\"API Warning: \"\n\t\t\t\"real count is greater than received unread count\"));\n\t\tcount = loaded;\n\t}\n\tconst auto had = (list.count() > 0);\n\tconst auto othersEmpty = [&] {\n\t\tfor (auto t : { Type::Mentions, Type::Reactions, Type::PollVotes }) {\n\t\t\tif (t == _type) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto &other = (t == Type::Mentions)\n\t\t\t\t? _data->mentions\n\t\t\t\t: (t == Type::Reactions)\n\t\t\t\t? _data->reactions\n\t\t\t\t: _data->pollVotes;\n\t\t\tif (other.count(-1) != 0) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}();\n\tif (!count && othersEmpty) {\n\t\t_data = nullptr;\n\t} else {\n\t\tlist.setCount(count);\n\t}\n\tconst auto has = (count > 0);\n\tif (has != had && _thread->inChatList()) {\n\t\tif (_type == Type::Mentions) {\n\t\t\t_thread->hasUnreadMentionChanged(has);\n\t\t} else if (_type == Type::Reactions) {\n\t\t\t_thread->hasUnreadReactionChanged(has);\n\t\t} else if (_type == Type::PollVotes) {\n\t\t\t_thread->hasUnreadPollVoteChanged(has);\n\t\t}\n\t}\n}\n\nbool Proxy::add(MsgId msgId, AddType type) {\n\tif (const auto history = _thread->asHistory()) {\n\t\tif (history->peer->isBroadcast()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tif (!_data) {\n\t\tcreateData();\n\t}\n\tauto &list = resolveList();\n\tconst auto count = list.count();\n\tconst auto loaded = list.loadedCount();\n\tconst auto allLoaded = (count >= 0) && (loaded >= count);\n\tif (allLoaded) {\n\t\tif (type == AddType::New || !list.contains(msgId)) {\n\t\t\tlist.insert(msgId);\n\t\t\tsetCount(count + 1);\n\t\t\treturn true;\n\t\t}\n\t} else if (loaded > 0 && type != AddType::New) {\n\t\tlist.insert(msgId);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Proxy::erase(MsgId msgId) {\n\tif (!_data) {\n\t\treturn;\n\t}\n\tauto &list = resolveList();\n\tlist.erase(msgId);\n\tif (const auto count = list.count(); count > 0) {\n\t\tsetCount(count - 1);\n\t}\n\tnotifyUpdated();\n}\n\nvoid Proxy::clear() {\n\tif (!_data || !count()) {\n\t\treturn;\n\t}\n\tauto &list = resolveList();\n\tlist.clear();\n\tsetCount(0);\n\tnotifyUpdated();\n}\n\nvoid Proxy::addSlice(const MTPmessages_Messages &slice, int alreadyLoaded) {\n\tif (!alreadyLoaded && _data) {\n\t\tresolveList().clear();\n\t}\n\tconst auto history = _thread->owningHistory();\n\tauto fullCount = slice.match([&](\n\t\t\tconst MTPDmessages_messagesNotModified &) {\n\t\tLOG((\"API Error: received messages.messagesNotModified! \"\n\t\t\t\"(Proxy::addSlice)\"));\n\t\treturn 0;\n\t}, [&](const MTPDmessages_messages &data) {\n\t\thistory->peer->processTopics(data.vtopics());\n\t\treturn int(data.vmessages().v.size());\n\t}, [&](const MTPDmessages_messagesSlice &data) {\n\t\thistory->peer->processTopics(data.vtopics());\n\t\treturn data.vcount().v;\n\t}, [&](const MTPDmessages_channelMessages &data) {\n\t\tif (const auto channel = history->peer->asChannel()) {\n\t\t\tchannel->ptsReceived(data.vpts().v);\n\t\t} else {\n\t\t\tLOG((\"API Error: received messages.channelMessages when \"\n\t\t\t\t\"no channel was passed! (Proxy::addSlice)\"));\n\t\t}\n\t\thistory->peer->processTopics(data.vtopics());\n\t\treturn data.vcount().v;\n\t});\n\n\tauto &owner = _thread->owner();\n\tconst auto messages = slice.match([&](\n\t\t\tconst MTPDmessages_messagesNotModified &) {\n\t\tLOG((\"API Error: received messages.messagesNotModified! \"\n\t\t\t\"(Proxy::addSlice)\"));\n\t\treturn QVector<MTPMessage>();\n\t}, [&](const auto &data) {\n\t\towner.processUsers(data.vusers());\n\t\towner.processChats(data.vchats());\n\t\treturn data.vmessages().v;\n\t});\n\tif (!messages.isEmpty() && !_data) {\n\t\tcreateData();\n\t}\n\tauto added = false;\n\tconst auto list = _data ? &resolveList() : nullptr;\n\tconst auto localFlags = MessageFlags();\n\tconst auto type = NewMessageType::Existing;\n\tfor (const auto &message : messages) {\n\t\tconst auto item = history->addNewMessage(\n\t\t\tIdFromMessage(message),\n\t\t\tmessage,\n\t\t\tlocalFlags,\n\t\t\ttype);\n\t\tif (_type == Type::PollVotes) {\n\t\t\titem->setHasUnreadPollVote();\n\t\t}\n\t\tconst auto is = [&] {\n\t\t\tswitch (_type) {\n\t\t\tcase Type::Mentions: return item->isUnreadMention();\n\t\t\tcase Type::Reactions: return item->hasUnreadReaction();\n\t\t\tcase Type::PollVotes: return item->hasUnreadPollVote();\n\t\t\t}\n\t\t\tUnexpected(\"Type in Proxy::addSlice.\");\n\t\t}();\n\t\tif (is) {\n\t\t\tlist->insert(item->id);\n\t\t\tadded = true;\n\t\t}\n\t}\n\tif (!added) {\n\t\tfullCount = loadedCount();\n\t}\n\tsetCount(fullCount);\n\tnotifyUpdated();\n}\n\nvoid Proxy::checkAdd(MsgId msgId, bool resolved) {\n\tExpects(_type == Type::Reactions || _type == Type::PollVotes);\n\n\tif (!_data) {\n\t\treturn;\n\t}\n\tauto &list = resolveList();\n\tif (!list.loadedCount() || list.maxLoaded() <= msgId) {\n\t\treturn;\n\t}\n\tconst auto history = _thread->owningHistory();\n\tconst auto peer = history->peer;\n\tconst auto item = peer->owner().message(peer, msgId);\n\tconst auto matches = item\n\t\t&& ((_type == Type::Reactions) ? item->hasUnreadReaction()\n\t\t\t: item->hasUnreadPollVote());\n\tif (matches) {\n\t\titem->addToUnreadThings(AddType::Existing);\n\t} else if (!item && !resolved) {\n\t\tconst auto type = _type;\n\t\tpeer->session().api().requestMessageData(peer, msgId, [=] {\n\t\t\tif (type == Type::Reactions) {\n\t\t\t\thistory->unreadReactions().checkAdd(msgId, true);\n\t\t\t} else {\n\t\t\t\thistory->unreadPollVotes().checkAdd(msgId, true);\n\t\t\t}\n\t\t});\n\t}\n}\n\nvoid Proxy::notifyUpdated() {\n\tif (const auto history = _thread->asHistory()) {\n\t\thistory->session().changes().historyUpdated(\n\t\t\thistory,\n\t\t\tHistoryUpdateFlag(_type));\n\t} else if (const auto topic = _thread->asTopic()) {\n\t\ttopic->session().changes().topicUpdated(\n\t\t\ttopic,\n\t\t\tTopicUpdateFlag(_type));\n\t} else if (const auto sublist = _thread->asSublist()) {\n\t\tsublist->session().changes().sublistUpdated(\n\t\t\tsublist,\n\t\t\tSublistUpdateFlag(_type));\n\t}\n}\n\nvoid Proxy::createData() {\n\t_data = std::make_unique<All>();\n\tif (_known) {\n\t\t_data->mentions.setCount(0);\n\t\t_data->reactions.setCount(0);\n\t\t_data->pollVotes.setCount(0);\n\t}\n}\n\nList &Proxy::resolveList() {\n\tExpects(_data != nullptr);\n\n\tswitch (_type) {\n\tcase Type::Mentions: return _data->mentions;\n\tcase Type::Reactions: return _data->reactions;\n\tcase Type::PollVotes: return _data->pollVotes;\n\t}\n\tUnexpected(\"Unread things type in Proxy::resolveList.\");\n}\n\n} // namespace HistoryUnreadThings\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_unread_things.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass History;\n\nnamespace Data {\nclass Thread;\n} // namespace Data\n\nnamespace HistoryUnreadThings {\n\nenum class AddType {\n\tNew,\n\tExisting,\n};\n\nenum class Type {\n\tMentions,\n\tReactions,\n\tPollVotes,\n};\n\nclass List final {\npublic:\n\t[[nodiscard]] int loadedCount() const {\n\t\treturn _messages.size();\n\t}\n\t[[nodiscard]] MsgId minLoaded() const {\n\t\treturn _messages.empty() ? 0 : _messages.front();\n\t}\n\t[[nodiscard]] MsgId maxLoaded() const {\n\t\treturn _messages.empty() ? 0 : _messages.back();\n\t}\n\t[[nodiscard]] int count(int notKnownValue = -1) const {\n\t\treturn _count.value_or(notKnownValue);\n\t}\n\t[[nodiscard]] bool has() const {\n\t\treturn (count() > 0);\n\t}\n\t[[nodiscard]] bool contains(MsgId msgId) const {\n\t\treturn _messages.contains(msgId);\n\t}\n\t[[nodiscard]] const base::flat_set<MsgId> &ids() const {\n\t\treturn _messages;\n\t}\n\tvoid setCount(int count) {\n\t\t_count = count;\n\t}\n\tvoid insert(MsgId msgId) {\n\t\t_messages.insert(msgId);\n\t}\n\tvoid erase(MsgId msgId) {\n\t\t_messages.remove(msgId);\n\t}\n\tvoid clear() {\n\t\t_messages.clear();\n\t}\n\nprivate:\n\tstd::optional<int> _count;\n\tbase::flat_set<MsgId> _messages;\n\n};\n\nstruct All {\n\tList mentions;\n\tList reactions;\n\tList pollVotes;\n};\n\nclass ConstProxy {\npublic:\n\tConstProxy(const List *list, bool known) : _list(list), _known(known) {\n\t}\n\tConstProxy(const ConstProxy &) = delete;\n\tConstProxy &operator=(const ConstProxy &) = delete;\n\n\t[[nodiscard]] int loadedCount() const {\n\t\treturn _list ? _list->loadedCount() : 0;\n\t}\n\t[[nodiscard]] MsgId minLoaded() const {\n\t\treturn _list ? _list->minLoaded() : 0;\n\t}\n\t[[nodiscard]] MsgId maxLoaded() const {\n\t\treturn _list ? _list->maxLoaded() : 0;\n\t}\n\t[[nodiscard]] int count(int notKnownValue = -1) const {\n\t\treturn _list\n\t\t\t? _list->count(notKnownValue)\n\t\t\t: _known\n\t\t\t? 0\n\t\t\t: notKnownValue;\n\t}\n\t[[nodiscard]] bool has() const {\n\t\treturn _list && _list->has();\n\t}\n\nprivate:\n\tconst List *_list = nullptr;\n\tconst bool _known = false;\n\n};\n\nclass Proxy final : public ConstProxy {\npublic:\n\tProxy(\n\t\tnot_null<Data::Thread*> thread,\n\t\tstd::unique_ptr<All> &data,\n\t\tType type,\n\t\tbool known)\n\t: ConstProxy(\n\t\t(!data\n\t\t\t? nullptr\n\t\t\t: (type == Type::Mentions)\n\t\t\t? &data->mentions\n\t\t\t: (type == Type::Reactions)\n\t\t\t? &data->reactions\n\t\t\t: &data->pollVotes),\n\t\tknown)\n\t, _thread(thread)\n\t, _data(data)\n\t, _type(type)\n\t, _known(known) {\n\t}\n\n\tvoid setCount(int count);\n\tbool add(MsgId msgId, AddType type);\n\tvoid erase(MsgId msgId);\n\tvoid clear();\n\n\tvoid addSlice(const MTPmessages_Messages &slice, int alreadyLoaded);\n\n\tvoid checkAdd(MsgId msgId, bool resolved = false);\n\nprivate:\n\tvoid createData();\n\tvoid notifyUpdated();\n\t[[nodiscard]] List &resolveList();\n\n\tconst not_null<Data::Thread*> _thread;\n\tstd::unique_ptr<All> &_data;\n\tType _type = Type::Mentions;\n\tbool _known = false;\n\n};\n\n} // namespace HistoryUnreadThings\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_view_highlight_manager.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_view_highlight_manager.h\"\n\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/history_view_element.h\"\n#include \"ui/chat/chat_style.h\"\n\nnamespace HistoryView {\n\nElementHighlighter::ElementHighlighter(\n\tnot_null<Data::Session*> data,\n\tViewForItem viewForItem,\n\tRepaintView repaintView)\n: _data(data)\n, _viewForItem(std::move(viewForItem))\n, _repaintView(std::move(repaintView))\n, _animation(*this) {\n}\n\nvoid ElementHighlighter::enqueue(const SelectedQuote &quote) {\n\tconst auto data = computeHighlight(quote);\n\tif (_queue.empty() && !_animation.animating()) {\n\t\thighlight(data);\n\t} else if (_highlighted != data && !base::contains(_queue, data)) {\n\t\t_queue.push_back(data);\n\t\tcheckNextHighlight();\n\t}\n}\n\nvoid ElementHighlighter::highlight(const SelectedQuote &quote) {\n\thighlight(computeHighlight(quote));\n}\n\nvoid ElementHighlighter::checkNextHighlight() {\n\tif (_animation.animating()) {\n\t\treturn;\n\t}\n\tconst auto next = [&] {\n\t\twhile (!_queue.empty()) {\n\t\t\tconst auto highlight = _queue.front();\n\t\t\t_queue.pop_front();\n\t\t\tif (const auto item = _data->message(highlight.itemId)) {\n\t\t\t\tif (_viewForItem(item)) {\n\t\t\t\t\treturn highlight;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn Highlight();\n\t}();\n\tif (next) {\n\t\thighlight(next);\n\t}\n}\n\nUi::ChatPaintHighlight ElementHighlighter::state(\n\t\tnot_null<const HistoryItem*> item) const {\n\tif (item->fullId() == _highlighted.itemId) {\n\t\tauto result = _animation.state();\n\t\tresult.range = _highlighted.part;\n\t\tresult.todoItemId = _highlighted.todoListId;\n\t\tresult.pollOption = _highlighted.pollOption;\n\t\treturn result;\n\t}\n\treturn {};\n}\n\nElementHighlighter::Highlight ElementHighlighter::computeHighlight(\n\t\tconst SelectedQuote &quote) {\n\tAssert(quote.item != nullptr);\n\n\tconst auto item = not_null(quote.item);\n\tconst auto owner = &item->history()->owner();\n\tif (const auto group = owner->groups().find(item)) {\n\t\tconst auto leader = group->items.front();\n\t\tconst auto leaderId = leader->fullId();\n\t\tconst auto i = ranges::find(group->items, item);\n\t\tif (i != end(group->items)) {\n\t\t\tconst auto index = int(i - begin(group->items));\n\t\t\tif (quote.highlight.empty()) {\n\t\t\t\treturn { leaderId, AddGroupItemSelection({}, index) };\n\t\t\t} else if (const auto leaderView = _viewForItem(leader)) {\n\t\t\t\treturn {\n\t\t\t\t\tleaderId,\n\t\t\t\t\tleaderView->selectionFromQuote(quote),\n\t\t\t\t\tquote.highlight.todoItemId,\n\t\t\t\t\tquote.highlight.pollOption,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tleaderId,\n\t\t\t{},\n\t\t\tquote.highlight.todoItemId,\n\t\t\tquote.highlight.pollOption,\n\t\t};\n\t} else if (quote.highlight.quote.empty()) {\n\t\treturn {\n\t\t\titem->fullId(),\n\t\t\t{},\n\t\t\tquote.highlight.todoItemId,\n\t\t\tquote.highlight.pollOption,\n\t\t};\n\t} else if (const auto view = _viewForItem(item)) {\n\t\treturn {\n\t\t\titem->fullId(),\n\t\t\tview->selectionFromQuote(quote),\n\t\t\tquote.highlight.todoItemId,\n\t\t\tquote.highlight.pollOption,\n\t\t};\n\t}\n\treturn {\n\t\titem->fullId(),\n\t\t{},\n\t\tquote.highlight.todoItemId,\n\t\tquote.highlight.pollOption,\n\t};\n}\n\nvoid ElementHighlighter::highlight(Highlight data) {\n\tif (const auto item = _data->message(data.itemId)) {\n\t\tif (const auto view = _viewForItem(item)) {\n\t\t\tif (_highlighted && _highlighted.itemId != data.itemId) {\n\t\t\t\tif (const auto was = _data->message(_highlighted.itemId)) {\n\t\t\t\t\tif (const auto view = _viewForItem(was)) {\n\t\t\t\t\t\trepaintHighlightedItem(view);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t_highlighted = data;\n\t\t\t_animation.start((!data.part.empty()\n\t\t\t\t\t|| data.todoListId\n\t\t\t\t\t|| !data.pollOption.isEmpty())\n\t\t\t\t&& !IsSubGroupSelection(data.part));\n\n\t\t\trepaintHighlightedItem(view);\n\t\t}\n\t}\n}\n\nvoid ElementHighlighter::repaintHighlightedItem(\n\t\tnot_null<const Element*> view) {\n\tif (view->isHiddenByGroup()) {\n\t\tif (const auto group = _data->groups().find(view->data())) {\n\t\t\tif (const auto leader = _viewForItem(group->items.front())) {\n\t\t\t\tif (!leader->isHiddenByGroup()) {\n\t\t\t\t\t_repaintView(leader);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t_repaintView(view);\n}\n\nvoid ElementHighlighter::updateMessage() {\n\tif (const auto item = _data->message(_highlighted.itemId)) {\n\t\tif (const auto view = _viewForItem(item)) {\n\t\t\trepaintHighlightedItem(view);\n\t\t}\n\t}\n}\n\nvoid ElementHighlighter::clear() {\n\t_animation.cancel();\n\t_highlighted = {};\n\t_lastHighlightedMessageId = FullMsgId();\n\t_queue.clear();\n}\n\nElementHighlighter::AnimationManager::AnimationManager(\n\tElementHighlighter &parent)\n: _parent(parent) {\n}\n\nbool ElementHighlighter::AnimationManager::animating() const {\n\tif (_timer && _timer->isActive()) {\n\t\treturn true;\n\t} else if (!anim::Disabled()) {\n\t\treturn _simple.animating();\n\t}\n\treturn false;\n}\n\nUi::ChatPaintHighlight ElementHighlighter::AnimationManager::state() const {\n\tif (anim::Disabled()) {\n\t\treturn {\n\t\t\t.opacity = !_timer ? 0. : 1.,\n\t\t\t.collapsion = !_timer ? 0. : _fadingOut ? 1. : 0.,\n\t\t};\n\t}\n\treturn {\n\t\t.opacity = ((!_fadingOut && _collapsing)\n\t\t\t? 1.\n\t\t\t: _simple.value(_fadingOut ? 0. : 1.)),\n\t\t.collapsion = ((!_withTextPart || !_collapsing)\n\t\t\t? 0.\n\t\t\t: _fadingOut\n\t\t\t? 1.\n\t\t\t: _simple.value(1.)),\n\t};\n}\n\nMsgId ElementHighlighter::latestSingleHighlightedMsgId() const {\n\treturn _highlighted.itemId\n\t\t? _highlighted.itemId.msg\n\t\t: _lastHighlightedMessageId.msg;\n}\n\nvoid ElementHighlighter::AnimationManager::start(bool withTextPart) {\n\t_withTextPart = withTextPart;\n\tconst auto finish = [=] {\n\t\tcancel();\n\t\t_parent._lastHighlightedMessageId = base::take(\n\t\t\t_parent._highlighted.itemId);\n\t\t_parent.checkNextHighlight();\n\t};\n\tcancel();\n\tif (anim::Disabled()) {\n\t\t_timer.emplace([=] {\n\t\t\t_parent.updateMessage();\n\t\t\tif (_withTextPart && !_fadingOut) {\n\t\t\t\t_fadingOut = true;\n\t\t\t\t_timer->callOnce(st::activeFadeOutDuration);\n\t\t\t} else {\n\t\t\t\tfinish();\n\t\t\t}\n\t\t});\n\t\t_timer->callOnce(_withTextPart\n\t\t\t? st::activeFadeInDuration\n\t\t\t: st::activeFadeOutDuration);\n\t\t_parent.updateMessage();\n\t} else {\n\t\t_simple.start(\n\t\t\t[=](float64 value) {\n\t\t\t\t_parent.updateMessage();\n\t\t\t\tif (value == 1.) {\n\t\t\t\t\tif (_withTextPart) {\n\t\t\t\t\t\t_timer.emplace([=] {\n\t\t\t\t\t\t\t_parent.updateMessage();\n\t\t\t\t\t\t\tif (_collapsing) {\n\t\t\t\t\t\t\t\t_fadingOut = true;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t_collapsing = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t_simple.start([=](float64 value) {\n\t\t\t\t\t\t\t\t_parent.updateMessage();\n\t\t\t\t\t\t\t\tif (_fadingOut && value == 0.) {\n\t\t\t\t\t\t\t\t\tfinish();\n\t\t\t\t\t\t\t\t} else if (!_fadingOut && value == 1.) {\n\t\t\t\t\t\t\t\t\t_timer->callOnce(\n\t\t\t\t\t\t\t\t\t\tst::activeFadeOutDuration);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t_fadingOut ? 1. : 0.,\n\t\t\t\t\t\t\t_fadingOut ? 0. : 1.,\n\t\t\t\t\t\t\t(_fadingOut\n\t\t\t\t\t\t\t\t? st::activeFadeInDuration\n\t\t\t\t\t\t\t\t: st::fadeWrapDuration));\n\t\t\t\t\t\t});\n\t\t\t\t\t\t_timer->callOnce(st::activeFadeInDuration);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t_fadingOut = true;\n\t\t\t\t\t\t_simple.start([=](float64 value) {\n\t\t\t\t\t\t\t_parent.updateMessage();\n\t\t\t\t\t\t\tif (value == 0.) {\n\t\t\t\t\t\t\t\tfinish();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t1.,\n\t\t\t\t\t\t0.,\n\t\t\t\t\t\tst::activeFadeOutDuration);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t0.,\n\t\t\t1.,\n\t\t\tst::activeFadeInDuration);\n\t}\n}\n\nvoid ElementHighlighter::AnimationManager::cancel() {\n\t_simple.stop();\n\t_timer.reset();\n\t_fadingOut = false;\n\t_collapsed = false;\n\t_collapsing = false;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_view_highlight_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"ui/effects/animations.h\"\n\nclass HistoryItem;\n\nnamespace Data {\nclass Session;\n} // namespace Data\n\nnamespace Ui {\nstruct ChatPaintHighlight;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass Element;\nstruct SelectedQuote;\n\nclass ElementHighlighter final {\npublic:\n\tusing ViewForItem = Fn<Element*(const HistoryItem*)>;\n\tusing RepaintView = Fn<void(const Element*)>;\n\tElementHighlighter(\n\t\tnot_null<Data::Session*> data,\n\t\tViewForItem viewForItem,\n\t\tRepaintView repaintView);\n\n\tvoid enqueue(const SelectedQuote &quote);\n\tvoid highlight(const SelectedQuote &quote);\n\tvoid clear();\n\n\t[[nodiscard]] Ui::ChatPaintHighlight state(\n\t\tnot_null<const HistoryItem*> item) const;\n\t[[nodiscard]] MsgId latestSingleHighlightedMsgId() const;\n\nprivate:\n\tclass AnimationManager final {\n\tpublic:\n\t\tAnimationManager(ElementHighlighter &parent);\n\t\t[[nodiscard]] bool animating() const;\n\t\t[[nodiscard]] Ui::ChatPaintHighlight state() const;\n\t\tvoid start(bool withTextPart);\n\t\tvoid cancel();\n\n\tprivate:\n\t\tElementHighlighter &_parent;\n\t\tUi::Animations::Simple _simple;\n\t\tstd::optional<base::Timer> _timer;\n\t\tbool _withTextPart = false;\n\t\tbool _collapsing = false;\n\t\tbool _collapsed = false;\n\t\tbool _fadingOut = false;\n\n\t};\n\n\tstruct Highlight {\n\t\tFullMsgId itemId;\n\t\tTextSelection part;\n\t\tint todoListId = 0;\n\t\tQByteArray pollOption;\n\n\t\texplicit operator bool() const {\n\t\t\treturn itemId.operator bool();\n\t\t}\n\t\tfriend inline bool operator==(Highlight, Highlight) = default;\n\t};\n\n\t[[nodiscard]] Highlight computeHighlight(const SelectedQuote &quote);\n\tvoid highlight(Highlight data);\n\tvoid checkNextHighlight();\n\tvoid repaintHighlightedItem(not_null<const Element*> view);\n\tvoid updateMessage();\n\n\tconst not_null<Data::Session*> _data;\n\tconst ViewForItem _viewForItem;\n\tconst RepaintView _repaintView;\n\n\tHighlight _highlighted;\n\tFullMsgId _lastHighlightedMessageId;\n\tstd::deque<Highlight> _queue;\n\n\tAnimationManager _animation;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_view_swipe_back_session.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_view_swipe_back_session.h\"\n\n#include \"history/view/history_view_list_widget.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/controls/swipe_handler.h\"\n#include \"ui/controls/swipe_handler_data.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace Window {\n\nvoid SetupSwipeBackSection(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\tnot_null<HistoryView::ListWidget*> list) {\n\tconst auto swipeBackData\n\t\t= list->lifetime().make_state<Ui::Controls::SwipeBackResult>();\n\tauto update = [=](Ui::Controls::SwipeContextData data) {\n\t\tif (data.translation > 0) {\n\t\t\tif (!swipeBackData->callback) {\n\t\t\t\tconst auto color = [=]() -> std::pair<QColor, QColor> {\n\t\t\t\t\tconst auto c = list->delegate()->listPreparePaintContext({\n\t\t\t\t\t\t.theme = list->delegate()->listChatTheme(),\n\t\t\t\t\t});\n\t\t\t\t\treturn {\n\t\t\t\t\t\tc.st->msgServiceBg()->c,\n\t\t\t\t\t\tc.st->msgServiceFg()->c,\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t\t(*swipeBackData) = Ui::Controls::SetupSwipeBack(\n\t\t\t\t\tparent,\n\t\t\t\t\tcolor);\n\t\t\t}\n\t\t\tswipeBackData->callback(data);\n\t\t\treturn;\n\t\t} else if (swipeBackData->lifetime) {\n\t\t\t(*swipeBackData) = {};\n\t\t}\n\t};\n\tauto init = [=](int, Qt::LayoutDirection direction) {\n\t\tif (direction != Qt::RightToLeft) {\n\t\t\treturn Ui::Controls::SwipeHandlerFinishData();\n\t\t}\n\t\treturn Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {\n\t\t\tlist->controller()->showBackFromStack();\n\t\t});\n\t};\n\tUi::Controls::SetupSwipeHandler({\n\t\t.widget = list,\n\t\t.scroll = scroll,\n\t\t.update = std::move(update),\n\t\t.init = std::move(init),\n\t\t.dontStart = list->touchMaybeSelectingValue(),\n\t});\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_view_swipe_back_session.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass WindowListDelegate;\n\nnamespace HistoryView {\nclass ListWidget;\n} // namespace HistoryView\n\nnamespace Ui {\nclass RpWidget;\nclass ScrollArea;\n} // namespace Ui\n\nnamespace Window {\n\nclass SectionWidget;\n\nvoid SetupSwipeBackSection(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Ui::ScrollArea*> scroll,\n\tnot_null<HistoryView::ListWidget*> list);\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_view_top_toast.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_view_top_toast.h\"\n\n#include \"ui/toast/toast.h\"\n#include \"core/ui_integration.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace HistoryView {\n\nnamespace {\n\n[[nodiscard]] crl::time CountToastDuration(const TextWithEntities &text) {\n\treturn std::clamp(\n\t\tcrl::time(1000) * int(text.text.size()) / 14,\n\t\tcrl::time(1000) * 5,\n\t\tcrl::time(1000) * 8);\n}\n\n} // namespace\n\nInfoTooltip::InfoTooltip() = default;\n\nvoid InfoTooltip::show(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Main::Session*> session,\n\t\tconst TextWithEntities &text,\n\t\tFn<void()> hiddenCallback) {\n\thide(anim::type::normal);\n\t_topToast = Ui::Toast::Show(parent, Ui::Toast::Config{\n\t\t.text = text,\n\t\t.textContext = Core::TextContext({ .session = session }),\n\t\t.icon = &st::historyInfoToastIcon,\n\t\t.st = &st::historyInfoToast,\n\t\t.attach = RectPart::Top,\n\t\t.duration = CountToastDuration(text),\n\t});\n\tif (const auto strong = _topToast.get()) {\n\t\tif (hiddenCallback) {\n\t\t\tQObject::connect(\n\t\t\t\tstrong->widget(),\n\t\t\t\t&QObject::destroyed,\n\t\t\t\thiddenCallback);\n\t\t}\n\t} else if (hiddenCallback) {\n\t\thiddenCallback();\n\t}\n}\n\nvoid InfoTooltip::hide(anim::type animated) {\n\tif (const auto strong = _topToast.get()) {\n\t\tif (animated == anim::type::normal) {\n\t\t\tstrong->hideAnimated();\n\t\t} else {\n\t\t\tstrong->hide();\n\t\t}\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_view_top_toast.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui::Toast {\nclass Instance;\n} // namespace Ui::Toast\n\nnamespace HistoryView {\n\nclass InfoTooltip final {\npublic:\n\tInfoTooltip();\n\n\tvoid show(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Main::Session*> session,\n\t\tconst TextWithEntities &text,\n\t\tFn<void()> hiddenCallback);\n\tvoid hide(anim::type animated);\n\nprivate:\n\tbase::weak_ptr<Ui::Toast::Instance> _topToast;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/history_widget.h\"\n\n#include \"base/random.h\"\n\n#include \"api/api_editing.h\"\n#include \"api/api_bot.h\"\n#include \"api/api_chat_participants.h\"\n#include \"api/api_global_privacy.h\"\n#include \"api/api_report.h\"\n#include \"api/api_sending.h\"\n#include \"api/api_send_progress.h\"\n#include \"api/api_unread_things.h\"\n#include \"base/random.h\"\n#include \"boxes/compose_ai_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/delete_messages_box.h\"\n#include \"boxes/send_credits_box.h\"\n#include \"boxes/send_gif_with_caption_box.h\"\n#include \"boxes/send_files_box.h\"\n#include \"boxes/share_box.h\"\n#include \"boxes/edit_caption_box.h\"\n#include \"boxes/moderate_messages_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"boxes/peers/edit_peer_permissions_box.h\" // ShowAboutGigagroup.\n#include \"boxes/peers/edit_peer_requests_box.h\"\n#include \"core/core_settings.h\"\n#include \"core/file_utilities.h\"\n#include \"core/mime_type.h\"\n#include \"history/view/history_view_draw_to_reply.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/chat/choose_theme_controller.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/inner_dropdown.h\"\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/message_sending_animation_controller.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/chat/message_bar.h\"\n#include \"ui/chat/attach/attach_send_files_way.h\"\n#include \"ui/chat/choose_send_as.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/image/image.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/controls/compose_ai_button_factory.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/controls/send_button.h\"\n#include \"ui/controls/send_as_button.h\"\n#include \"ui/controls/silent_toggle.h\"\n#include \"ui/ui_utility.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"base/event_filter.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"base/unixtime.h\"\n#include \"base/call_delayed.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/components/credits.h\"\n#include \"data/components/scheduled_messages.h\"\n#include \"data/components/sponsored_messages.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_drafts.h\"\n#include \"data/data_session.h\"\n#include \"data/data_todo_list.h\"\n#include \"data/data_web_page.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_peer_values.h\" // Data::AmPremiumValue.\n#include \"data/data_premium_limits.h\" // Data::PremiumLimits.\n#include \"data/stickers/data_stickers.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\" // GetErrorForSending.\n#include \"history/history_drag_area.h\"\n#include \"history/history_inner_widget.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_streamed_drafts.h\"\n#include \"history/history_unread_things.h\"\n#include \"history/admin_log/history_admin_log_section.h\"\n#include \"history/view/controls/history_view_characters_limit.h\"\n#include \"history/view/controls/history_view_compose_ai_button.h\"\n#include \"history/view/controls/history_view_compose_ai_tooltip.h\"\n#include \"history/view/controls/history_view_compose_search.h\"\n#include \"history/view/controls/history_view_forward_panel.h\"\n#include \"history/view/controls/history_view_draft_options.h\"\n#include \"history/view/controls/history_view_suggest_options.h\"\n#include \"history/view/controls/history_view_ttl_button.h\"\n#include \"history/view/controls/history_view_voice_record_bar.h\"\n#include \"history/view/controls/history_view_webpage_processor.h\"\n#include \"history/view/reactions/history_view_reactions_button.h\"\n#include \"history/view/history_view_chat_section.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_service_message.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_scheduled_section.h\"\n#include \"history/view/history_view_schedule_box.h\"\n#include \"history/view/history_view_top_bar_widget.h\"\n#include \"history/view/history_view_contact_status.h\"\n#include \"history/view/history_view_paid_reaction_toast.h\"\n#include \"history/view/history_view_pinned_tracker.h\"\n#include \"history/view/history_view_pinned_section.h\"\n#include \"history/view/history_view_pinned_bar.h\"\n#include \"history/view/history_view_group_call_bar.h\"\n#include \"history/view/history_view_group_members_widget.h\"\n#include \"history/view/history_view_item_preview.h\"\n#include \"history/view/history_view_reply.h\"\n#include \"history/view/history_view_requests_bar.h\"\n#include \"history/view/history_view_self_forwards_tagger.h\"\n#include \"history/view/history_view_sticker_toast.h\"\n#include \"history/view/history_view_subsection_tabs.h\"\n#include \"history/view/history_view_translate_bar.h\"\n#include \"history/view/media/history_view_media.h\"\n#include \"core/click_handler_types.h\"\n#include \"chat_helpers/field_autocomplete.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"chat_helpers/tabbed_section.h\"\n#include \"chat_helpers/bot_keyboard.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"menu/menu_send.h\"\n#include \"menu/menu_timecode_action.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"lang/lang_keys.h\"\n#include \"settings/business/settings_quick_replies.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"storage/localimageloader.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/file_upload.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/audio/media_audio_capture.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"core/application.h\"\n#include \"apiwrap.h\"\n#include \"base/qthelp_regex.h\"\n#include \"ui/boxes/report_box_graphics.h\"\n#include \"ui/chat/pinned_bar.h\"\n#include \"ui/chat/group_call_bar.h\"\n#include \"ui/chat/requests_bar.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/continuous_scroll.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/elastic_scroll.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/item_text_options.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"main/session/send_as_peers.h\"\n#include \"webrtc/webrtc_environment.h\"\n#include \"window/notifications_manager.h\"\n#include \"window/window_adaptive.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_slide_animation.h\"\n#include \"window/window_peer_menu.h\"\n#include \"inline_bots/inline_results_widget.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n#include \"info/profile/info_profile_values.h\" // SharedMediaCountValue.\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"core/shortcuts.h\"\n#include \"core/ui_integration.h\"\n#include \"support/support_common.h\"\n#include \"support/support_autocomplete.h\"\n#include \"support/support_helper.h\"\n#include \"support/support_preload.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"calls/calls_instance.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_info.h\"\n\n#include <QtGui/QWindow>\n#include <QtCore/QMimeData>\n\n#include <QClipboard>\n#include <QApplication>\n\nnamespace {\n\nconstexpr auto kMessagesPerPageFirst = 30;\nconstexpr auto kMessagesPerPage = 50;\nconstexpr auto kPreloadHeightsCount = 3; // when 3 screens to scroll left make a preload request\nconstexpr auto kScrollToVoiceAfterScrolledMs = 1000;\nconstexpr auto kSkipRepaintWhileScrollMs = 100;\nconstexpr auto kShowMembersDropdownTimeoutMs = 300;\nconstexpr auto kDisplayEditTimeWarningMs = 300 * 1000;\nconstexpr auto kFullDayInMs = 86400 * 1000;\nconstexpr auto kSaveDraftTimeout = crl::time(1000);\nconstexpr auto kSaveDraftAnywayTimeout = 5 * crl::time(1000);\nconstexpr auto kSaveCloudDraftIdleTimeout = 14 * crl::time(1000);\nconstexpr auto kRefreshSlowmodeLabelTimeout = crl::time(200);\nconstexpr auto kCommonModifiers = 0\n\t| Qt::ShiftModifier\n\t| Qt::MetaModifier\n\t| Qt::ControlModifier;\nconst auto kPsaAboutPrefix = \"cloud_lng_about_psa_\";\n\n[[nodiscard]] rpl::producer<PeerData*> ActivePeerValue(\n\t\tnot_null<Window::SessionController*> controller) {\n\treturn controller->activeChatValue(\n\t) | rpl::map([](Dialogs::Key key) {\n\t\tconst auto history = key.history();\n\t\treturn history ? history->peer.get() : nullptr;\n\t});\n}\n\n[[nodiscard]] QString FirstEmoji(const QString &s) {\n\tconst auto begin = s.data();\n\tconst auto end = begin + s.size();\n\tfor (auto ch = begin; ch != end; ch++) {\n\t\tauto length = 0;\n\t\tif (const auto e = Ui::Emoji::Find(ch, end, &length)) {\n\t\t\treturn e->text();\n\t\t}\n\t}\n\treturn QString();\n}\n\n} // namespace\n\nHistoryWidget::HistoryWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Window::AbstractSectionWidget(\n\tparent,\n\tcontroller,\n\tActivePeerValue(controller))\n, _api(&controller->session().mtp())\n, _updateEditTimeLeftDisplay([=] { updateField(); })\n, _fieldBarCancel(this, st::historyReplyCancel)\n, _topBars(std::make_unique<Ui::RpWidget>(this))\n, _topBar(this, controller)\n, _scroll(\n\tthis,\n\tcontroller->chatStyle()->value(lifetime(), st::historyScroll),\n\tfalse)\n, _updateHistoryItems([=] { updateHistoryItemsByTimer(); })\n, _cornerButtons(\n\t_scroll.data(),\n\tcontroller->chatStyle(),\n\tstatic_cast<HistoryView::CornerButtonsDelegate*>(this))\n, _supportAutocomplete(session().supportMode()\n\t? object_ptr<Support::Autocomplete>(this, &session())\n\t: nullptr)\n, _send(std::make_shared<Ui::SendButton>(this, st::historySend))\n, _aiButton(Ui::CreateChild<HistoryView::Controls::ComposeAiButton>(\n\tthis,\n\tst::historyAiComposeButton))\n, _sendAsFile(Ui::CreateChild<Ui::IconButton>(\n\tthis,\n\tst::historySendAsFileButton))\n, _unblock(\n\tthis,\n\ttr::lng_unblock_button(tr::now).toUpper(),\n\tst::historyUnblock)\n, _botStart(\n\tthis,\n\ttr::lng_bot_start(tr::now).toUpper(),\n\tst::historyComposeButton)\n, _joinChannel(\n\tthis,\n\ttr::lng_profile_join_channel(tr::now).toUpper(),\n\tst::historyComposeButton)\n, _muteUnmute(\n\tthis,\n\ttr::lng_channel_mute(tr::now).toUpper(),\n\tst::historyComposeButton)\n, _reportMessages(this, QString(), st::historyComposeButton)\n, _attachToggle(this, st::historyAttach)\n, _tabbedSelectorToggle(this, st::historyAttachEmoji)\n, _botKeyboardShow(this, st::historyBotKeyboardShow)\n, _botKeyboardHide(this, st::historyBotKeyboardHide)\n, _botCommandStart(this, st::historyBotCommandStart)\n, _voiceRecordBar(std::make_unique<VoiceRecordBar>(\n\tthis,\n\tcontroller->uiShow(),\n\t_send,\n\tst::historySendSize.height()))\n, _forwardPanel(std::make_unique<ForwardPanel>([=] { updateField(); }))\n, _field(\n\tthis,\n\tst::historyComposeField,\n\tUi::InputField::Mode::MultiLine,\n\ttr::lng_message_ph())\n, _kbScroll(this, st::botKbScroll)\n, _keyboard(_kbScroll->setOwnedWidget(object_ptr<BotKeyboard>(\n\tcontroller,\n\tthis)))\n, _membersDropdownShowTimer([=] { showMembersDropdown(); })\n, _highlighter(\n\t&session().data(),\n\t[=](const HistoryItem *item) { return item->mainView(); },\n\t[=](const HistoryView::Element *view) {\n\t\tsession().data().requestViewRepaint(view);\n\t})\n, _saveDraftTimer([=] { saveDraft(); })\n, _saveCloudDraftTimer([=] { saveCloudDraft(); })\n, _paidReactionToast(std::make_unique<HistoryView::PaidReactionToast>(\n\tthis,\n\t&session().data(),\n\trpl::single(0),\n\t[=](not_null<const HistoryView::Element*> view) {\n\t\treturn _list && _list->itemTop(view) >= 0;\n\t}))\n, _topShadow(this) {\n\tsetAcceptDrops(true);\n\n\tsession().downloaderTaskFinished() | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\tbase::install_event_filter(_scroll.data(), [=](not_null<QEvent*> e) {\n\t\tconst auto consumed = (e->type() == QEvent::Wheel)\n\t\t\t&& _list\n\t\t\t&& _list->consumeScrollAction(\n\t\t\t\tUi::ScrollDelta(static_cast<QWheelEvent*>(e.get())));\n\t\treturn consumed\n\t\t\t? base::EventFilterResult::Cancel\n\t\t\t: base::EventFilterResult::Continue;\n\t});\n\t_scroll->scrolls() | rpl::on_next([=] {\n\t\thandleScroll();\n\t}, lifetime());\n\t_scroll->geometryChanged(\n\t) | rpl::on_next(crl::guard(_list, [=] {\n\t\t_list->onParentGeometryChanged();\n\t}), lifetime());\n\n\t_scroll->addContentRequests(\n\t) | rpl::on_next([=] {\n\t\tif (_history && _history->loadedAtBottom()) {\n\t\t\tusing Result = Data::SponsoredMessages::AppendResult;\n\t\t\tconst auto tryToAppend = [=] {\n\t\t\t\tconst auto sponsored = &session().sponsoredMessages();\n\t\t\t\tconst auto result = sponsored->append(_history);\n\t\t\t\tif (result == Result::Appended) {\n\t\t\t\t\t_scroll->contentAdded();\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t};\n\t\t\tif (tryToAppend() == Result::MediaLoading\n\t\t\t\t&& !_historySponsoredPreloading) {\n\t\t\t\tsession().downloaderTaskFinished(\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\tif (tryToAppend() != Result::MediaLoading) {\n\t\t\t\t\t\t_historySponsoredPreloading.destroy();\n\t\t\t\t\t}\n\t\t\t\t}, _historySponsoredPreloading);\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\t_fieldBarCancel->addClickHandler([=] { cancelFieldAreaState(); });\n\t_send->addClickHandler([=] { sendButtonClicked(); });\n\n\t_mediaEditManager.updateRequests() | rpl::on_next([this] {\n\t\tupdateOverStates(mapFromGlobal(QCursor::pos()));\n\t}, lifetime());\n\n\tsetupSendMenu(_send.get(), [=](SendMenu::Action action, SendMenu::Details) {\n\t\tif (action.type == SendMenu::ActionType::Send) {\n\t\t\tsend(action.options);\n\t\t} else {\n\t\t\tsendScheduled(action.options);\n\t\t}\n\t});\n\t_unblock->addClickHandler([=] { unblockUser(); });\n\t_botStart->addClickHandler([=] { sendBotStartCommand(); });\n\t_joinChannel->addClickHandler([=] { joinChannel(); });\n\t_muteUnmute->addClickHandler([=] { toggleMuteUnmute(); });\n\tsetupGiftToChannelButton();\n\tsetupDirectMessageButton();\n\t_reportMessages->addClickHandler([=] { reportSelectedMessages(); });\n\t_field->submits(\n\t) | rpl::on_next([=](Qt::KeyboardModifiers modifiers) {\n\t\tsendWithModifiers(modifiers);\n\t}, _field->lifetime());\n\t_field->cancelled(\n\t) | rpl::on_next([=] {\n\t\tif (_peer->amMonoforumAdmin()) {\n\t\t\tQWidget::setEnabled(false);\n\t\t\tcrl::on_main([=] {\n\t\t\t\tQWidget::setEnabled(true);\n\t\t\t\tQWidget::setFocus();\n\t\t\t});\n\t\t}\n\t\tescape();\n\t}, _field->lifetime());\n\t_field->tabbed(\n\t) | rpl::on_next([=](not_null<bool*> handled) {\n\t\tif (_supportAutocomplete) {\n\t\t\t_supportAutocomplete->activate(_field.data());\n\t\t\t*handled = true;\n\t\t}\n\t}, _field->lifetime());\n\t_field->heightChanges(\n\t) | rpl::on_next([=] {\n\t\tfieldResized();\n\t}, _field->lifetime());\n\t_field->focusedChanges(\n\t) | rpl::filter(rpl::mappers::_1) | rpl::on_next([=] {\n\t\tfieldFocused();\n\t}, _field->lifetime());\n\t_field->changes(\n\t) | rpl::on_next([=] {\n\t\tfieldChanged();\n\t}, _field->lifetime());\n#ifdef Q_OS_MAC\n\t// Removed an ability to insert text from the menu bar\n\t// when the field is hidden.\n\t_field->shownValue(\n\t) | rpl::on_next([=](bool shown) {\n\t\t_field->setEnabled(shown);\n\t}, _field->lifetime());\n#endif // Q_OS_MAC\n\tcontroller->widget()->shownValue(\n\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\twindowIsVisibleChanged();\n\t}, lifetime());\n\n\tinitTabbedSelector();\n\n\t_attachToggle->setClickedCallback([=] {\n\t\tconst auto toggle = _attachBotsMenu && _attachBotsMenu->isHidden();\n\t\tbase::call_delayed(st::historyAttach.ripple.hideDuration, this, [=] {\n\t\t\tif (_attachBotsMenu && toggle) {\n\t\t\t\t_attachBotsMenu->showAnimated();\n\t\t\t} else {\n\t\t\t\tchooseAttach();\n\t\t\t\tif (_attachBotsMenu) {\n\t\t\t\t\t_attachBotsMenu->hideAnimated();\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n\n\tconst auto rawTextEdit = _field->rawTextEdit().get();\n\trpl::merge(\n\t\t_field->scrollTop().changes() | rpl::to_empty,\n\t\tbase::qt_signal_producer(\n\t\t\trawTextEdit,\n\t\t\t&QTextEdit::cursorPositionChanged)\n\t) | rpl::on_next([=] {\n\t\tsaveDraftDelayed();\n\t}, _field->lifetime());\n\n\tbase::install_event_filter(_field, [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::MouseButtonPress) {\n\t\t\tconst auto m = static_cast<QMouseEvent*>(e.get());\n\t\t\tif (m->button() == Qt::MiddleButton) {\n\t\t\t\tconst auto mimeData = QApplication::clipboard()->mimeData();\n\t\t\t\tif (canSendFiles(mimeData)) {\n\t\t\t\t\tconfirmSendingFiles(mimeData);\n\t\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\t_fieldBarCancel->hide();\n\n\t_topBar->hide();\n\t_scroll->hide();\n\t_kbScroll->hide();\n\n\tcontroller->chatStyle()->paletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_scroll->updateBars();\n\t}, lifetime());\n\n\t_forwardPanel->itemsUpdated(\n\t) | rpl::on_next([=] {\n\t\tupdateControlsVisibility();\n\t\tupdateControlsGeometry();\n\t}, lifetime());\n\n\t_fieldChatStyle = InitMessageField(controller, _field, [=](\n\t\t\tnot_null<DocumentData*> document) {\n\t\tif (_peer && Data::AllowEmojiWithoutPremium(_peer, document)) {\n\t\t\treturn true;\n\t\t}\n\t\tshowPremiumToast(document);\n\t\treturn false;\n\t});\n\tInitMessageFieldFade(_field, st::historyComposeField.textBg);\n\n\tsetupFastButtonMode();\n\tinitAiButton();\n\tinitSendAsFileButton();\n\n\t_fieldCharsCountManager.limitExceeds(\n\t) | rpl::on_next([=] {\n\t\tconst auto hide = _fieldCharsCountManager.isLimitExceeded();\n\t\tif (_silent) {\n\t\t\t_silent->setVisible(!hide);\n\t\t}\n\t\tif (_ttlInfo) {\n\t\t\t_ttlInfo->setVisible(!hide);\n\t\t}\n\t\tif (_giftToUser) {\n\t\t\t_giftToUser->setVisible(!hide);\n\t\t}\n\t\tif (_scheduled) {\n\t\t\t_scheduled->setVisible(!hide);\n\t\t}\n\t\tupdateFieldSize();\n\t\tmoveFieldControls();\n\t}, lifetime());\n\n\t_send->widthValue() | rpl::skip(1) | rpl::on_next([=] {\n\t\tupdateFieldSize();\n\t\tmoveFieldControls();\n\t}, _send->lifetime());\n\n\t_keyboard->sendCommandRequests(\n\t) | rpl::on_next([=](Bot::SendCommandRequest r) {\n\t\tsendBotCommand(r);\n\t}, lifetime());\n\n\tif (_supportAutocomplete) {\n\t\tsupportInitAutocomplete();\n\t}\n\t_field->rawTextEdit()->installEventFilter(this);\n\t_field->setMimeDataHook(WrappedMessageFieldMimeHook([=](\n\t\t\tnot_null<const QMimeData*> data,\n\t\t\tUi::InputField::MimeAction action) {\n\t\tconst auto pasteResult = Ui::CheckLargeTextPaste(_field, data);\n\t\tif (pasteResult.exceeds) {\n\t\t\tif (action == Ui::InputField::MimeAction::Check) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst auto text = _field->getTextWithTags();\n\t\t\tconst auto cursor = _field->textCursor();\n\t\t\tsendTextAsFile(\n\t\t\t\tpasteResult.resultingText,\n\t\t\t\ttext,\n\t\t\t\tcursor.position(),\n\t\t\t\tcursor.anchor());\n\t\t\treturn true;\n\t\t}\n\t\tif (action == Ui::InputField::MimeAction::Check) {\n\t\t\treturn canSendFiles(data);\n\t\t} else if (action == Ui::InputField::MimeAction::Insert) {\n\t\t\treturn confirmSendingFiles(\n\t\t\t\tdata,\n\t\t\t\tstd::nullopt,\n\t\t\t\tCore::ReadMimeText(data));\n\t\t}\n\t\tUnexpected(\"action in MimeData hook.\");\n\t}, _field));\n\n\tupdateFieldSubmitSettings();\n\n\t_field->hide();\n\t_send->hide();\n\t_unblock->hide();\n\t_botStart->hide();\n\t_joinChannel->hide();\n\t_muteUnmute->hide();\n\t_reportMessages->hide();\n\n\tinitVoiceRecordBar();\n\n\t_attachToggle->hide();\n\t_tabbedSelectorToggle->hide();\n\t_botKeyboardShow->hide();\n\t_botKeyboardHide->hide();\n\t_botCommandStart->hide();\n\n\tsession().attachWebView().requestBots();\n\trpl::merge(\n\t\tsession().attachWebView().attachBotsUpdates(),\n\t\tsession().changes().peerUpdates(\n\t\t\tData::PeerUpdate::Flag::Rights\n\t\t\t| Data::PeerUpdate::Flag::StarsPerMessage\n\t\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\t\treturn update.peer == _peer;\n\t\t}) | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\trefreshAttachBotsMenu();\n\t}, lifetime());\n\n\t_botKeyboardShow->addClickHandler([=] { toggleKeyboard(); });\n\t_botKeyboardHide->addClickHandler([=] { toggleKeyboard(); });\n\t_botCommandStart->addClickHandler([=] { startBotCommand(); });\n\n\t_topShadow->hide();\n\n\t_attachDragAreas = DragArea::SetupDragAreaToContainer(\n\t\tthis,\n\t\tcrl::guard(this, [=](not_null<const QMimeData*> d) {\n\t\t\tif (!_peer || isRecording()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto topic = resolveReplyToTopic();\n\t\t\treturn topic\n\t\t\t\t? Data::CanSendAnyOf(topic, Data::FilesSendRestrictions())\n\t\t\t\t: Data::CanSendAnyOf(_peer, Data::FilesSendRestrictions());\n\t\t}),\n\t\tcrl::guard(this, [=](bool f) { _field->setAcceptDrops(f); }),\n\t\tcrl::guard(this, [=] { updateControlsGeometry(); }));\n\t_attachDragAreas.document->setDroppedCallback([=](const QMimeData *data) {\n\t\tconfirmSendingFiles(data, false);\n\t\tWindow::ActivateWindow(controller);\n\t});\n\t_attachDragAreas.photo->setDroppedCallback([=](const QMimeData *data) {\n\t\tconfirmSendingFiles(data, true);\n\t\tWindow::ActivateWindow(controller);\n\t});\n\n\tCore::App().mediaDevices().recordAvailabilityValue(\n\t) | rpl::on_next([=](Webrtc::RecordAvailability value) {\n\t\t_recordAvailability = value;\n\t\tif (_list) {\n\t\t\tupdateSendButtonType();\n\t\t}\n\t}, lifetime());\n\n\tsession().data().newItemAdded(\n\t) | rpl::on_next([=](not_null<HistoryItem*> item) {\n\t\tnewItemAdded(item);\n\t}, lifetime());\n\n\tsession().data().historyChanged(\n\t) | rpl::on_next([=](not_null<History*> history) {\n\t\thandleHistoryChange(history);\n\t}, lifetime());\n\n\tsession().data().viewResizeRequest(\n\t) | rpl::on_next([=](not_null<HistoryView::Element*> view) {\n\t\tconst auto item = view->data();\n\t\tconst auto history = item->history();\n\t\tif (item->mainView() == view\n\t\t\t&& (history == _history || history == _migrated)) {\n\t\t\tupdateHistoryGeometry();\n\t\t}\n\t}, lifetime());\n\tsession().data().viewHeightAdjusted(\n\t) | rpl::on_next([=](Data::Session::ViewHeightAdjusted data) {\n\t\tconst auto item = data.view->data();\n\t\tconst auto history = item->history();\n\t\tif (item->mainView() == data.view\n\t\t\t&& (history == _history || history == _migrated)) {\n\t\t\tupdateHistoryGeometry();\n\t\t}\n\t}, lifetime());\n\n\tsession().data().itemShowHighlightRequest(\n\t) | rpl::on_next([=](not_null<HistoryItem*> item) {\n\t\tconst auto history = item->history();\n\t\tif (history == _history || history == _migrated) {\n\t\t\tif (item->mainView()) {\n\t\t\t\tenqueueMessageHighlight({ item });\n\t\t\t\tanimatedScrollToItem(item->id);\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\tsession().data().itemDataChanges(\n\t) | rpl::filter([=](not_null<HistoryItem*> item) {\n\t\treturn !_list && (item->mainView() != nullptr);\n\t}) | rpl::on_next([=](not_null<HistoryItem*> item) {\n\t\titem->mainView()->itemDataChanged();\n\t}, lifetime());\n\n\tCore::App().settings().largeEmojiChanges(\n\t) | rpl::on_next([=] {\n\t\tcrl::on_main(this, [=] {\n\t\t\tupdateHistoryGeometry();\n\t\t});\n\t}, lifetime());\n\tCore::App().settings().sendSubmitWayValue(\n\t) | rpl::on_next([=] {\n\t\tcrl::on_main(this, [=] {\n\t\t\tupdateFieldSubmitSettings();\n\t\t});\n\t}, lifetime());\n\n\tsession().data().channelDifferenceTooLong(\n\t) | rpl::filter([=](not_null<ChannelData*> channel) {\n\t\treturn _peer == channel.get();\n\t}) | rpl::on_next([=] {\n\t\t_cornerButtons.updateJumpDownVisibility();\n\t\tpreloadHistoryIfNeeded();\n\t}, lifetime());\n\n\tsession().data().userIsBotChanges(\n\t) | rpl::filter([=](not_null<UserData*> user) {\n\t\treturn (_peer == user.get());\n\t}) | rpl::on_next([=](not_null<UserData*> user) {\n\t\t_list->refreshAboutView();\n\t\t_list->updateBotInfo();\n\t\tupdateControlsVisibility();\n\t\tupdateControlsGeometry();\n\t}, lifetime());\n\n\tsession().data().botCommandsChanges(\n\t) | rpl::filter([=](not_null<PeerData*> peer) {\n\t\treturn _peer && (_peer == peer);\n\t}) | rpl::on_next([=] {\n\t\tif (updateCmdStartShown()) {\n\t\t\tupdateControlsVisibility();\n\t\t\tupdateControlsGeometry();\n\t\t}\n\t}, lifetime());\n\n\tusing EntryUpdateFlag = Data::EntryUpdate::Flag;\n\tsession().changes().entryUpdates(\n\t\tEntryUpdateFlag::HasPinnedMessages\n\t\t| EntryUpdateFlag::ForwardDraft\n\t) | rpl::on_next([=](const Data::EntryUpdate &update) {\n\t\tif (_pinnedTracker\n\t\t\t&& (update.flags & EntryUpdateFlag::HasPinnedMessages)\n\t\t\t&& ((update.entry.get() == _history)\n\t\t\t\t|| (update.entry.get() == _migrated))) {\n\t\t\tcheckPinnedBarState();\n\t\t}\n\t\tif (update.flags & EntryUpdateFlag::ForwardDraft) {\n\t\t\tupdateForwarding();\n\t\t}\n\t}, lifetime());\n\n\tusing HistoryUpdateFlag = Data::HistoryUpdate::Flag;\n\tsession().changes().historyUpdates(\n\t\tHistoryUpdateFlag::MessageSent\n\t\t| HistoryUpdateFlag::BotKeyboard\n\t\t| HistoryUpdateFlag::CloudDraft\n\t\t| HistoryUpdateFlag::UnreadMentions\n\t\t| HistoryUpdateFlag::UnreadReactions\n\t\t| HistoryUpdateFlag::UnreadPollVotes\n\t\t| HistoryUpdateFlag::UnreadView\n\t\t| HistoryUpdateFlag::TopPromoted\n\t\t| HistoryUpdateFlag::ClientSideMessages\n\t) | rpl::filter([=](const Data::HistoryUpdate &update) {\n\t\treturn (_history == update.history.get());\n\t}) | rpl::on_next([=](const Data::HistoryUpdate &update) {\n\t\tconst auto flags = update.flags;\n\t\tif (flags & HistoryUpdateFlag::MessageSent) {\n\t\t\tsynteticScrollToY(_scroll->scrollTopMax());\n\t\t}\n\t\tif (flags & HistoryUpdateFlag::BotKeyboard) {\n\t\t\tupdateBotKeyboard(update.history);\n\t\t}\n\t\tif (flags & HistoryUpdateFlag::CloudDraft) {\n\t\t\tapplyCloudDraft(update.history);\n\t\t}\n\t\tif (flags & HistoryUpdateFlag::ClientSideMessages) {\n\t\t\tupdateSendButtonType();\n\t\t}\n\t\tif ((flags & HistoryUpdateFlag::UnreadMentions)\n\t\t\t|| (flags & HistoryUpdateFlag::UnreadReactions)\n\t\t\t|| (flags & HistoryUpdateFlag::UnreadPollVotes)) {\n\t\t\t_cornerButtons.updateUnreadThingsVisibility();\n\t\t}\n\t\tif (flags & HistoryUpdateFlag::UnreadView) {\n\t\t\tunreadCountUpdated();\n\t\t}\n\t\tif (flags & HistoryUpdateFlag::TopPromoted) {\n\t\t\tupdateHistoryGeometry();\n\t\t\tupdateControlsVisibility();\n\t\t\tupdateControlsGeometry();\n\t\t\tthis->update();\n\t\t}\n\t}, lifetime());\n\n\tusing MessageUpdateFlag = Data::MessageUpdate::Flag;\n\tsession().changes().messageUpdates(\n\t\tMessageUpdateFlag::Destroyed\n\t\t| MessageUpdateFlag::Edited\n\t\t| MessageUpdateFlag::ReplyMarkup\n\t\t| MessageUpdateFlag::BotCallbackSent\n\t) | rpl::on_next([=](const Data::MessageUpdate &update) {\n\t\tconst auto flags = update.flags;\n\t\tif (flags & MessageUpdateFlag::Destroyed) {\n\t\t\titemRemoved(update.item);\n\t\t\treturn;\n\t\t}\n\t\tif (flags & MessageUpdateFlag::Edited) {\n\t\t\titemEdited(update.item);\n\t\t}\n\t\tif (flags & MessageUpdateFlag::ReplyMarkup) {\n\t\t\tif (_keyboard->forMsgId() == update.item->fullId()) {\n\t\t\t\tupdateBotKeyboard(update.item->history(), true);\n\t\t\t}\n\t\t}\n\t\tif (flags & MessageUpdateFlag::BotCallbackSent) {\n\t\t\tbotCallbackSent(update.item);\n\t\t}\n\t}, lifetime());\n\n\tsession().changes().realtimeMessageUpdates(\n\t\tMessageUpdateFlag::NewUnreadReaction\n\t) | rpl::on_next([=](const Data::MessageUpdate &update) {\n\t\tmaybeMarkReactionsRead(update.item);\n\t}, lifetime());\n\n\tsession().data().sentToScheduled(\n\t) | rpl::on_next([=](const Data::SentToScheduled &value) {\n\t\tconst auto history = value.history;\n\t\tif (history == _history) {\n\t\t\tconst auto id = value.scheduledId;\n\t\t\tcrl::on_main(this, [=] {\n\t\t\t\tif (history == _history) {\n\t\t\t\t\tcontroller->showSection(\n\t\t\t\t\t\tstd::make_shared<HistoryView::ScheduledMemento>(\n\t\t\t\t\t\t\thistory,\n\t\t\t\t\t\t\tid));\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t}, lifetime());\n\n\tsession().data().sentFromScheduled(\n\t) | rpl::on_next([=](const Data::SentFromScheduled &value) {\n\t\tif (value.item->awaitingVideoProcessing()\n\t\t\t&& !_sentFromScheduledTip\n\t\t\t&& HistoryView::ShowScheduledVideoPublished(\n\t\t\t\tcontroller,\n\t\t\t\tvalue,\n\t\t\t\tcrl::guard(this, [=] { _sentFromScheduledTip = false; }))) {\n\t\t\t_sentFromScheduledTip = true;\n\t\t}\n\t}, lifetime());\n\n\tusing MediaSwitch = Media::Player::Instance::Switch;\n\tMedia::Player::instance()->switchToNextEvents(\n\t) | rpl::filter([=](const MediaSwitch &pair) {\n\t\treturn (pair.from.type() == AudioMsgId::Type::Voice);\n\t}) | rpl::on_next([=](const MediaSwitch &pair) {\n\t\tscrollToCurrentVoiceMessage(pair.from.contextId(), pair.to);\n\t}, lifetime());\n\n\tsession().user()->flagsValue(\n\t) | rpl::on_next([=](UserData::Flags::Change change) {\n\t\tif (change.diff & UserData::Flag::Premium) {\n\t\t\tif (const auto user = _peer ? _peer->asUser() : nullptr) {\n\t\t\t\tif (user->requiresPremiumToWrite()) {\n\t\t\t\t\thandlePeerUpdate();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\tusing PeerUpdateFlag = Data::PeerUpdate::Flag;\n\tsession().changes().peerUpdates(\n\t\tPeerUpdateFlag::Rights\n\t\t| PeerUpdateFlag::Migration\n\t\t| PeerUpdateFlag::UnavailableReason\n\t\t| PeerUpdateFlag::IsBlocked\n\t\t| PeerUpdateFlag::Admins\n\t\t| PeerUpdateFlag::Members\n\t\t| PeerUpdateFlag::OnlineStatus\n\t\t| PeerUpdateFlag::Notifications\n\t\t| PeerUpdateFlag::ChannelAmIn\n\t\t| PeerUpdateFlag::DiscussionLink\n\t\t| PeerUpdateFlag::Slowmode\n\t\t| PeerUpdateFlag::BotStartToken\n\t\t| PeerUpdateFlag::MessagesTTL\n\t\t| PeerUpdateFlag::ChatThemeToken\n\t\t| PeerUpdateFlag::FullInfo\n\t\t| PeerUpdateFlag::ManagedBot\n\t\t| PeerUpdateFlag::StarsPerMessage\n\t\t| PeerUpdateFlag::GiftSettings\n\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\tif (update.peer.get() == _peer) {\n\t\t\treturn true;\n\t\t} else if (update.peer->isSelf()\n\t\t\t&& (update.flags & PeerUpdateFlag::GiftSettings)) {\n\t\t\trefreshSendGiftToggle();\n\t\t\tupdateControlsVisibility();\n\t\t\tupdateControlsGeometry();\n\t\t}\n\t\treturn false;\n\t}) | rpl::map([](const Data::PeerUpdate &update) {\n\t\treturn update.flags;\n\t}) | rpl::on_next([=](Data::PeerUpdate::Flags flags) {\n\t\tif (flags & PeerUpdateFlag::Rights) {\n\t\t\tupdateFieldPlaceholder();\n\t\t\tupdateSendButtonType();\n\t\t\t_preview->checkNow(false);\n\n\t\t\tconst auto was = (_sendAs != nullptr);\n\t\t\trefreshSendAsToggle();\n\t\t\tif (was != (_sendAs != nullptr)) {\n\t\t\t\tupdateControlsVisibility();\n\t\t\t\tupdateControlsGeometry();\n\t\t\t\torderWidgets();\n\t\t\t}\n\t\t}\n\t\tif (flags & PeerUpdateFlag::Migration) {\n\t\t\thandlePeerMigration();\n\t\t}\n\t\tif (flags & PeerUpdateFlag::Notifications) {\n\t\t\tupdateNotifyControls();\n\t\t}\n\t\tif (flags & PeerUpdateFlag::UnavailableReason) {\n\t\t\tconst auto unavailable = _peer->computeUnavailableReason();\n\t\t\tif (!unavailable.isEmpty()) {\n\t\t\t\tconst auto account = not_null(&_peer->account());\n\t\t\t\tcloseCurrent();\n\t\t\t\tif (const auto primary = Core::App().windowFor(account)) {\n\t\t\t\t\tprimary->showToast(unavailable);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (flags & PeerUpdateFlag::StarsPerMessage) {\n\t\t\tupdateFieldPlaceholder();\n\t\t\tupdateSendButtonType();\n\t\t}\n\t\tif (flags & PeerUpdateFlag::GiftSettings) {\n\t\t\trefreshSendGiftToggle();\n\t\t}\n\t\tif (flags & (PeerUpdateFlag::BotStartToken\n\t\t\t| PeerUpdateFlag::GiftSettings)) {\n\t\t\tupdateControlsVisibility();\n\t\t\tupdateControlsGeometry();\n\t\t}\n\t\tif (flags & PeerUpdateFlag::Slowmode) {\n\t\t\tupdateSendButtonType();\n\t\t}\n\t\tif ((flags & PeerUpdateFlag::ManagedBot) && _list) {\n\t\t\t_list->refreshAboutView();\n\t\t\t_list->updateBotInfo();\n\t\t}\n\t\tif (flags & (PeerUpdateFlag::IsBlocked\n\t\t\t| PeerUpdateFlag::Admins\n\t\t\t| PeerUpdateFlag::Members\n\t\t\t| PeerUpdateFlag::OnlineStatus\n\t\t\t| PeerUpdateFlag::Rights\n\t\t\t| PeerUpdateFlag::ChannelAmIn\n\t\t\t| PeerUpdateFlag::DiscussionLink)) {\n\t\t\thandlePeerUpdate();\n\t\t}\n\t\tif (flags & PeerUpdateFlag::MessagesTTL) {\n\t\t\tcheckMessagesTTL();\n\t\t}\n\t\tif ((flags & PeerUpdateFlag::ChatThemeToken) && _list) {\n\t\t\tconst auto emoji = _peer->themeToken();\n\t\t\tif (Data::CloudThemes::TestingColors() && !emoji.isEmpty()) {\n\t\t\t\t_peer->owner().cloudThemes().themeForTokenValue(\n\t\t\t\t\temoji\n\t\t\t\t) | rpl::filter_optional(\n\t\t\t\t) | rpl::take(\n\t\t\t\t\t1\n\t\t\t\t) | rpl::on_next([=](const Data::CloudTheme &theme) {\n\t\t\t\t\tconst auto &themes = _peer->owner().cloudThemes();\n\t\t\t\t\tconst auto text = themes.prepareTestingLink(theme);\n\t\t\t\t\tif (!text.isEmpty()) {\n\t\t\t\t\t\t_field->setText(text);\n\t\t\t\t\t}\n\t\t\t\t}, _list->lifetime());\n\t\t\t}\n\t\t}\n\t\tif (flags & PeerUpdateFlag::FullInfo) {\n\t\t\tfullInfoUpdated();\n\t\t\tupdateSendButtonType();\n\t\t\tif (_peer->starsPerMessageChecked()) {\n\t\t\t\tsession().credits().load();\n\t\t\t} else if (const auto channel = _peer->asChannel()) {\n\t\t\t\tif (channel->allowedReactions().paidEnabled) {\n\t\t\t\t\tsession().credits().load();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\tusing Type = Data::DefaultNotify;\n\trpl::merge(\n\t\tsession().data().notifySettings().defaultUpdates(Type::User),\n\t\tsession().data().notifySettings().defaultUpdates(Type::Group),\n\t\tsession().data().notifySettings().defaultUpdates(Type::Broadcast)\n\t) | rpl::on_next([=] {\n\t\tupdateNotifyControls();\n\t}, lifetime());\n\n\tsession().data().itemVisibilityQueries(\n\t) | rpl::filter([=](\n\t\t\tconst Data::Session::ItemVisibilityQuery &query) {\n\t\treturn !_showAnimation\n\t\t\t&& (_history == query.item->history())\n\t\t\t&& (query.item->mainView() != nullptr)\n\t\t\t&& isVisible();\n\t}) | rpl::on_next([=](\n\t\t\tconst Data::Session::ItemVisibilityQuery &query) {\n\t\tif (const auto view = query.item->mainView()) {\n\t\t\tauto top = _list->itemTop(view);\n\t\t\tif (top >= 0) {\n\t\t\t\tauto scrollTop = _scroll->scrollTop();\n\t\t\t\tif (top + view->height() > scrollTop\n\t\t\t\t\t&& top < scrollTop + _scroll->height()) {\n\t\t\t\t\t*query.isVisible = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\t_topBar->membersShowAreaActive(\n\t) | rpl::on_next([=](bool active) {\n\t\tsetMembersShowAreaActive(active);\n\t}, _topBar->lifetime());\n\t_topBar->forwardSelectionRequest(\n\t) | rpl::on_next([=] {\n\t\tforwardSelected();\n\t}, _topBar->lifetime());\n\t_topBar->forwardAndDeleteSelectionRequest(\n\t) | rpl::on_next([=] {\n\t\tsession().api().rememberToDeleteAfterForward(getSelectedItems());\n\t\tforwardSelected();\n\t}, _topBar->lifetime());\n\t_topBar->deleteSelectionRequest(\n\t) | rpl::on_next([=] {\n\t\tconfirmDeleteSelected();\n\t}, _topBar->lifetime());\n\t_topBar->clearSelectionRequest(\n\t) | rpl::on_next([=] {\n\t\tclearSelected();\n\t}, _topBar->lifetime());\n\t_topBar->cancelChooseForReportRequest(\n\t) | rpl::on_next([=] {\n\t\tsetChooseReportMessagesDetails({}, nullptr);\n\t}, _topBar->lifetime());\n\t_topBar->searchRequest(\n\t) | rpl::on_next([=] {\n\t\tif (_history) {\n\t\t\tcontroller->searchInChat(_history);\n\t\t}\n\t}, _topBar->lifetime());\n\n\tsession().api().sendActions(\n\t) | rpl::filter([=](const Api::SendAction &action) {\n\t\tif (_creatingBotTopic\n\t\t\t&& action.history == _creatingBotTopic->owningHistory()\n\t\t\t&& action.replyTo.topicRootId == _creatingBotTopic->rootId()) {\n\t\t\tUi::PostponeCall(_creatingBotTopic, [=] {\n\t\t\t\tusing namespace HistoryView;\n\t\t\t\tconst auto topic = base::take(_creatingBotTopic);\n\t\t\t\tcontroller->showSection(\n\t\t\t\t\tstd::make_shared<ChatMemento>(ChatViewId{\n\t\t\t\t\t\t.history = topic->owningHistory(),\n\t\t\t\t\t\t.repliesRootId = topic->rootId(),\n\t\t\t\t\t}),\n\t\t\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\t\t});\n\t\t\treturn false;\n\t\t}\n\t\treturn (action.history == _history);\n\t}) | rpl::on_next([=](const Api::SendAction &action) {\n\t\tconst auto lastKeyboardUsed = lastForceReplyReplied(\n\t\t\taction.replyTo.messageId);\n\t\tif (action.replaceMediaOf) {\n\t\t} else if (action.options.scheduled) {\n\t\t\tcancelReplyOrSuggest(lastKeyboardUsed);\n\t\t\tcrl::on_main(this, [=, history = action.history] {\n\t\t\t\tcontroller->showSection(\n\t\t\t\t\tstd::make_shared<HistoryView::ScheduledMemento>(history));\n\t\t\t});\n\t\t} else {\n\t\t\tfastShowAtEnd(action.history);\n\t\t\tif (!_justMarkingAsRead\n\t\t\t\t&& cancelReplyOrSuggest(lastKeyboardUsed)\n\t\t\t\t&& !action.clearDraft) {\n\t\t\t\tsaveCloudDraft();\n\t\t\t}\n\t\t}\n\t\tif (action.options.handleSupportSwitch) {\n\t\t\thandleSupportSwitch(action.history);\n\t\t}\n\t}, lifetime());\n\n\t_selfForwardsTagger = std::make_unique<HistoryView::SelfForwardsTagger>(\n\t\tcontroller,\n\t\tthis,\n\t\t[=] { return _list; },\n\t\t_scroll.data(),\n\t\t[=] { return _history; });\n\n\tif (session().supportMode()) {\n\t\tsession().data().chatListEntryRefreshes(\n\t\t) | rpl::on_next([=] {\n\t\t\tcrl::on_main(this, [=] { checkSupportPreload(true); });\n\t\t}, lifetime());\n\t}\n\n\tCore::App().materializeLocalDraftsRequests(\n\t) | rpl::on_next([=] {\n\t\tsaveFieldToHistoryLocalDraft();\n\t}, lifetime());\n\n\tsetupScheduledToggle();\n\tsetupSendAsToggle();\n\torderWidgets();\n\tsetupShortcuts();\n\n\t_attachToggle->setAccessibleName(tr::lng_attach(tr::now));\n\t_tabbedSelectorToggle->setAccessibleName(tr::lng_emoji_sticker_gif(tr::now));\n\t_botKeyboardShow->setAccessibleName(tr::lng_bot_keyboard_show(tr::now));\n\t_botKeyboardHide->setAccessibleName(tr::lng_bot_keyboard_hide(tr::now));\n\t_botCommandStart->setAccessibleName(tr::lng_bot_commands_start(tr::now));\n\t_fieldBarCancel->setAccessibleName(tr::lng_cancel(tr::now));\n\n}\n\nvoid HistoryWidget::setGeometryWithTopMoved(\n\t\tconst QRect &newGeometry,\n\t\tint topDelta) {\n\t_topDelta = topDelta;\n\tbool willBeResized = (size() != newGeometry.size());\n\tif (geometry() != newGeometry) {\n\t\tauto weak = base::make_weak(this);\n\t\tsetGeometry(newGeometry);\n\t\tif (!weak) {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (!willBeResized) {\n\t\tresizeEvent(nullptr);\n\t}\n\t_topDelta = 0;\n}\n\nDialogs::EntryState HistoryWidget::computeDialogsEntryState() const {\n\treturn Dialogs::EntryState{\n\t\t.key = _history,\n\t\t.section = Dialogs::EntryState::Section::History,\n\t\t.currentReplyTo = replyTo(),\n\t\t.currentSuggest = suggestOptions(),\n\t};\n}\n\nvoid HistoryWidget::refreshJoinChannelText() {\n\tif (const auto channel = _peer ? _peer->asChannel() : nullptr) {\n\t\t_joinChannel->setText((channel->isBroadcast()\n\t\t\t? tr::lng_profile_join_channel(tr::now)\n\t\t\t: (channel->requestToJoin() && !channel->amCreator())\n\t\t\t? tr::lng_profile_apply_to_join_group(tr::now)\n\t\t\t: tr::lng_profile_join_group(tr::now)).toUpper());\n\t}\n}\n\nvoid HistoryWidget::refreshGiftToChannelShown() {\n\tif (!_giftToChannel || !_peer) {\n\t\treturn;\n\t}\n\tconst auto channel = _peer->asChannel();\n\t_giftToChannel->setVisible(channel\n\t\t&& channel->isBroadcast()\n\t\t&& channel->stargiftsAvailable());\n}\n\nvoid HistoryWidget::refreshDirectMessageShown() {\n\tif (!_directMessage || !_peer) {\n\t\treturn;\n\t}\n\tconst auto channel = _peer->asChannel();\n\tconst auto monoforum = channel ? channel->broadcastMonoforum() : nullptr;\n\tconst auto visible = monoforum && !monoforum->monoforumDisabled();\n\t_directMessage->setVisible(visible);\n\tif (visible) {\n\t\tusing Flags = Data::Flags<ChannelDataFlags>;\n\t\t_directMessageLifetime = monoforum->flagsValue(\n\t\t) | rpl::skip(\n\t\t\t1\n\t\t) | rpl::on_next([=](Flags::Change change) {\n\t\t\tif (change.diff & ChannelDataFlag::MonoforumDisabled) {\n\t\t\t\trefreshDirectMessageShown();\n\t\t\t}\n\t\t});\n\t}\n}\n\nvoid HistoryWidget::refreshTopBarActiveChat() {\n\tconst auto state = computeDialogsEntryState();\n\t_topBar->setActiveChat(state, _history->sendActionPainter());\n\tif (state.key) {\n\t\tcontroller()->setDialogsEntryState(state);\n\t}\n}\n\nvoid HistoryWidget::refreshTabbedPanel() {\n\tif (_peer && controller()->hasTabbedSelectorOwnership()) {\n\t\tcreateTabbedPanel();\n\t} else {\n\t\tsetTabbedPanel(nullptr);\n\t}\n}\n\nvoid HistoryWidget::initVoiceRecordBar() {\n\t_voiceRecordBar->setStartRecordingFilter([=] {\n\t\tconst auto error = [&]() -> Data::SendError {\n\t\t\tif (_peer) {\n\t\t\t\tif (const auto error = Data::RestrictionError(\n\t\t\t\t\t\t_peer,\n\t\t\t\t\t\tChatRestriction::SendVoiceMessages)) {\n\t\t\t\t\treturn error;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {};\n\t\t}();\n\t\tif (error) {\n\t\t\tData::ShowSendErrorToast(controller(), _peer, error);\n\t\t\treturn true;\n\t\t} else if (showSlowmodeError()) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t});\n\t_voiceRecordBar->setTTLFilter([=] {\n\t\tif (const auto peer = _history ? _history->peer.get() : nullptr) {\n\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\tif (!user->isSelf() && !user->isBot()) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t});\n\n\tconst auto applyLocalDraft = [=] {\n\t\tif (_history && _history->localDraft(MsgId(), PeerId())) {\n\t\t\tapplyDraft();\n\t\t}\n\t};\n\n\t_voiceRecordBar->sendActionUpdates(\n\t) | rpl::on_next([=](const auto &data) {\n\t\tif (!_history) {\n\t\t\treturn;\n\t\t}\n\t\tsession().sendProgressManager().update(\n\t\t\t_history,\n\t\t\tdata.type,\n\t\t\tdata.progress);\n\t}, lifetime());\n\n\t_voiceRecordBar->sendVoiceRequests(\n\t) | rpl::on_next([=](const VoiceToSend &data) {\n\t\tsendVoice(data);\n\t}, lifetime());\n\n\t_voiceRecordBar->cancelRequests(\n\t) | rpl::on_next(applyLocalDraft, lifetime());\n\n\t_voiceRecordBar->lockShowStarts(\n\t) | rpl::on_next([=] {\n\t\t_cornerButtons.updateJumpDownVisibility();\n\t\t_cornerButtons.updateUnreadThingsVisibility();\n\t}, lifetime());\n\n\t_voiceRecordBar->errors(\n\t) | rpl::on_next([=](::Media::Capture::Error error) {\n\t\tusing Error = ::Media::Capture::Error;\n\t\tswitch (error) {\n\t\tcase Error::AudioInit:\n\t\tcase Error::AudioTimeout:\n\t\t\tcontroller()->showToast(tr::lng_record_audio_problem(tr::now));\n\t\t\tbreak;\n\t\tcase Error::VideoInit:\n\t\tcase Error::VideoTimeout:\n\t\t\tcontroller()->showToast(tr::lng_record_video_problem(tr::now));\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tcontroller()->showToast(u\"Unknown error.\"_q);\n\t\t\tbreak;\n\t\t}\n\t}, lifetime());\n\n\t_voiceRecordBar->updateSendButtonTypeRequests(\n\t) | rpl::on_next([=] {\n\t\tupdateSendButtonType();\n\t}, lifetime());\n\n\t_voiceRecordBar->lockViewportEvents(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\t_scroll->viewportEvent(e);\n\t}, lifetime());\n\n\t_voiceRecordBar->recordingTipRequests(\n\t) | rpl::on_next([=] {\n\t\tCore::App().settings().setRecordVideoMessages(\n\t\t\t!Core::App().settings().recordVideoMessages());\n\t\tupdateSendButtonType();\n\t\tswitch (_send->type()) {\n\t\tcase Ui::SendButton::Type::Record: {\n\t\t\tconst auto can = Webrtc::RecordAvailability::VideoAndAudio;\n\t\t\tcontroller()->showToast((_recordAvailability == can)\n\t\t\t\t? tr::lng_record_voice_tip(tr::now)\n\t\t\t\t: tr::lng_record_hold_tip(tr::now));\n\t\t} break;\n\t\tcase Ui::SendButton::Type::Round:\n\t\t\tcontroller()->showToast(tr::lng_record_video_tip(tr::now));\n\t\t\tbreak;\n\t\t}\n\t}, lifetime());\n\n\t_voiceRecordBar->recordingStateChanges(\n\t) | rpl::on_next([=](bool active) {\n\t\t_field->setDisabled(active);\n\t\tcontroller()->widget()->setInnerFocus();\n\t\tupdateAiButtonVisibility();\n\t\tupdateSendAsFileVisibility();\n\t}, lifetime());\n\n\t_voiceRecordBar->hideFast();\n}\n\nvoid HistoryWidget::initAiButton() {\n\t_aiButton->hide();\n\t_aiButton->setAccessibleName(tr::lng_ai_compose_title(tr::now));\n\t_aiButton->setClickedCallback([=] {\n\t\tif (_aiTooltipManager) {\n\t\t\t_aiTooltipManager->hideAndRemember();\n\t\t}\n\t\tupdateAiButtonVisibility();\n\t\tshowAiComposeBox();\n\t});\n\n\t_aiTooltipManager = std::make_unique<HistoryView::Controls::AiTooltipManager>(\n\t\tthis,\n\t\t_aiButton,\n\t\ttr::lng_ai_compose_tooltip(tr::rich),\n\t\t\"ai_compose_tooltip_hidden\"_cs,\n\t\t[=] { return width(); });\n}\n\nvoid HistoryWidget::initSendAsFileButton() {\n\t_sendAsFile->hide();\n\t_sendAsFile->setClickedCallback([=] {\n\t\tif (_sendAsFileTooltipManager) {\n\t\t\t_sendAsFileTooltipManager->hideAndRemember();\n\t\t}\n\t\tconst auto cursor = _field->textCursor();\n\t\tconst auto text = _field->getTextWithTags();\n\t\tsendTextAsFile(text.text, text, cursor.position(), cursor.anchor());\n\t});\n\n\t_sendAsFileTooltipManager = std::make_unique<HistoryView::Controls::AiTooltipManager>(\n\t\tthis,\n\t\t_sendAsFile,\n\t\ttr::lng_send_as_file_tooltip(tr::rich),\n\t\t\"send_as_file_tooltip_hidden\"_cs,\n\t\t[=] { return width(); });\n}\n\nvoid HistoryWidget::sendTextAsFile(\n\t\tconst QString &fileText,\n\t\tTextWithTags restoreText,\n\t\tint restorePosition,\n\t\tint restoreAnchor) {\n\tauto result = Ui::PrepareTextAsFile(fileText);\n\n\t_field->setTextWithTags({});\n\n\tauto box = Box<SendFilesBox>(\n\t\tcontroller(),\n\t\tstd::move(result),\n\t\tTextWithTags{},\n\t\t_peer,\n\t\tApi::SendType::Normal,\n\t\tsendMenuDetails());\n\tbox->setConfirmedCallback(crl::guard(this, [=](\n\t\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\t\tApi::SendOptions options) {\n\t\tsendingFilesConfirmed(std::move(bundle), options);\n\t}));\n\tbox->setCancelledCallback(crl::guard(this, [=] {\n\t\t_field->setTextWithTags(restoreText);\n\t\tauto cursor = _field->textCursor();\n\t\tcursor.setPosition(restoreAnchor);\n\t\tif (restorePosition != restoreAnchor) {\n\t\t\tcursor.setPosition(restorePosition, QTextCursor::KeepAnchor);\n\t\t}\n\t\t_field->setTextCursor(cursor);\n\t}));\n\n\tWindow::ActivateWindow(controller());\n\tcontroller()->show(std::move(box));\n}\n\nvoid HistoryWidget::initTabbedSelector() {\n\trefreshTabbedPanel();\n\n\tif (!Core::App().settings().fork().emojiPopupOnClick()) {\n\t_tabbedSelectorToggle->addClickHandler([=] {\n\t\tif (_tabbedPanel && _tabbedPanel->isHidden()) {\n\t\t\t_tabbedPanel->showAnimated();\n\t\t} else {\n\t\t\ttoggleTabbedSelectorMode();\n\t\t}\n\t});\n\t} else {\n\t\t_tabbedSelectorToggle->clicks(\n\t\t) | rpl::on_next([=](Qt::MouseButton button) {\n\t\t\tif (button == Qt::LeftButton) {\n\t\t\t\t_tabbedPanel->toggleAnimated();\n\t\t\t} else if (button == Qt::RightButton) {\n\t\t\t\ttoggleTabbedSelectorMode();\n\t\t\t}\n\t\t}, lifetime());\n\t\t_tabbedSelectorToggle->setAcceptBoth(true);\n\t}\n\n\tconst auto selector = controller()->tabbedSelector();\n\n\tbase::install_event_filter(this, selector, [=](not_null<QEvent*> e) {\n\t\tif (_tabbedPanel && e->type() == QEvent::ParentChange) {\n\t\t\tsetTabbedPanel(nullptr);\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\tauto filter = rpl::filter([=] {\n\t\treturn !isHidden();\n\t});\n\tusing Selector = TabbedSelector;\n\n\tselector->emojiChosen(\n\t) | rpl::filter([=] {\n\t\treturn !isHidden() && !_field->isHidden();\n\t}) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\tUi::InsertEmojiAtCursor(_field->textCursor(), data.emoji);\n\t}, lifetime());\n\n\trpl::merge(\n\t\tselector->fileChosen() | filter,\n\t\tselector->customEmojiChosen() | filter,\n\t\tcontroller()->stickerOrEmojiChosen() | filter\n\t) | rpl::on_next([=](ChatHelpers::FileChosen &&data) {\n\t\tfileChosen(std::move(data));\n\t}, lifetime());\n\n\tselector->photoChosen(\n\t) | filter | rpl::on_next([=](ChatHelpers::PhotoChosen data) {\n\t\tsendExistingPhoto(data.photo, data.options);\n\t}, lifetime());\n\n\tselector->inlineResultChosen(\n\t) | filter | rpl::filter([=](const ChatHelpers::InlineChosen &data) {\n\t\tif (!data.recipientOverride) {\n\t\t\treturn true;\n\t\t} else if (data.recipientOverride != _peer) {\n\t\t\tshowHistory(data.recipientOverride->id, ShowAtTheEndMsgId);\n\t\t}\n\t\treturn (data.recipientOverride == _peer);\n\t}) | rpl::on_next([=](ChatHelpers::InlineChosen data) {\n\t\tsendInlineResult(data);\n\t}, lifetime());\n\n\tselector->contextMenuRequested(\n\t) | filter | rpl::on_next([=] {\n\t\tselector->showMenuWithDetails(sendMenuDetails());\n\t}, lifetime());\n\n\tselector->choosingStickerUpdated(\n\t) | rpl::on_next([=](const Selector::Action &data) {\n\t\tif (!_history) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto type = Api::SendProgressType::ChooseSticker;\n\t\tif (data != Selector::Action::Cancel) {\n\t\t\tsession().sendProgressManager().update(_history, type);\n\t\t} else {\n\t\t\tsession().sendProgressManager().cancel(_history, type);\n\t\t}\n\t}, lifetime());\n}\n\nvoid HistoryWidget::supportInitAutocomplete() {\n\t_supportAutocomplete->hide();\n\n\t_supportAutocomplete->insertRequests(\n\t) | rpl::on_next([=](const QString &text) {\n\t\tsupportInsertText(text);\n\t}, _supportAutocomplete->lifetime());\n\n\t_supportAutocomplete->shareContactRequests(\n\t) | rpl::on_next([=](const Support::Contact &contact) {\n\t\tsupportShareContact(contact);\n\t}, _supportAutocomplete->lifetime());\n}\n\nvoid HistoryWidget::supportInsertText(const QString &text) {\n\t_field->setFocus();\n\t_field->textCursor().insertText(text);\n\t_field->ensureCursorVisible();\n}\n\nvoid HistoryWidget::supportShareContact(Support::Contact contact) {\n\tif (!_history) {\n\t\treturn;\n\t}\n\tsupportInsertText(contact.comment);\n\tcontact.comment = _field->getLastText();\n\n\tconst auto submit = [=](Qt::KeyboardModifiers modifiers) {\n\t\tconst auto history = _history;\n\t\tif (!history) {\n\t\t\treturn;\n\t\t}\n\t\tauto options = Api::SendOptions{\n\t\t\t.sendAs = prepareSendAction({}).options.sendAs,\n\t\t};\n\t\tauto action = Api::SendAction(history);\n\t\tsend(options);\n\t\toptions.handleSupportSwitch = Support::HandleSwitch(modifiers);\n\t\taction.options = options;\n\t\tsession().api().shareContact(\n\t\t\tcontact.phone,\n\t\t\tcontact.firstName,\n\t\t\tcontact.lastName,\n\t\t\taction);\n\t};\n\tconst auto box = controller()->show(Box<Support::ConfirmContactBox>(\n\t\tcontroller(),\n\t\t_history,\n\t\tcontact,\n\t\tcrl::guard(this, submit)));\n\tbox->boxClosing(\n\t) | rpl::on_next([=] {\n\t\t_field->document()->undo();\n\t}, lifetime());\n}\n\nvoid HistoryWidget::scrollToCurrentVoiceMessage(\n\t\tFullMsgId fromId,\n\t\tFullMsgId toId) {\n\tif (crl::now() <= _lastUserScrolled + kScrollToVoiceAfterScrolledMs) {\n\t\treturn;\n\t}\n\tif (!_list) {\n\t\treturn;\n\t}\n\n\tauto from = session().data().message(fromId);\n\tauto to = session().data().message(toId);\n\tif (!from || !to) {\n\t\treturn;\n\t}\n\n\t// If history has pending resize items, the scrollTopItem won't be updated.\n\t// And the scrollTop will be reset back to scrollTopItem + scrollTopOffset.\n\thandlePendingHistoryUpdate();\n\n\tif (const auto toView = to->mainView()) {\n\t\tauto toTop = _list->itemTop(toView);\n\t\tif (toTop >= 0 && !isItemCompletelyHidden(from)) {\n\t\t\tauto scrollTop = _scroll->scrollTop();\n\t\t\tauto scrollBottom = scrollTop + _scroll->height();\n\t\t\tauto toBottom = toTop + toView->height();\n\t\t\tif ((toTop < scrollTop && toBottom < scrollBottom)\n\t\t\t\t|| (toTop > scrollTop && toBottom > scrollBottom)) {\n\t\t\t\tanimatedScrollToItem(to->id);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid HistoryWidget::animatedScrollToItem(MsgId msgId) {\n\tExpects(_history != nullptr);\n\n\tif (hasPendingResizedItems()) {\n\t\tupdateListSize();\n\t}\n\n\tauto to = session().data().message(_history->peer, msgId);\n\tif (_list->itemTop(to) < 0) {\n\t\treturn;\n\t}\n\n\tauto scrollTo = std::clamp(\n\t\titemTopForHighlight(to->mainView()),\n\t\t0,\n\t\t_scroll->scrollTopMax());\n\tanimatedScrollToY(scrollTo, to);\n}\n\nvoid HistoryWidget::animatedScrollToY(int scrollTo, HistoryItem *attachTo) {\n\tExpects(_history != nullptr);\n\n\tif (hasPendingResizedItems()) {\n\t\tupdateListSize();\n\t}\n\n\t// Attach our scroll animation to some item.\n\tauto itemTop = _list->itemTop(attachTo);\n\tauto scrollTop = _scroll->scrollTop();\n\tif (itemTop < 0 && !_history->isEmpty()) {\n\t\tattachTo = _history->blocks.back()->messages.back()->data();\n\t\titemTop = _list->itemTop(attachTo);\n\t}\n\tif (itemTop < 0 || (scrollTop == scrollTo)) {\n\t\tsynteticScrollToY(scrollTo);\n\t\treturn;\n\t}\n\n\t_scrollToAnimation.stop();\n\tauto maxAnimatedDelta = _scroll->height();\n\tauto transition = anim::sineInOut;\n\tif (scrollTo > scrollTop + maxAnimatedDelta) {\n\t\tscrollTop = scrollTo - maxAnimatedDelta;\n\t\tsynteticScrollToY(scrollTop);\n\t\ttransition = anim::easeOutCubic;\n\t} else if (scrollTo + maxAnimatedDelta < scrollTop) {\n\t\tscrollTop = scrollTo + maxAnimatedDelta;\n\t\tsynteticScrollToY(scrollTop);\n\t\ttransition = anim::easeOutCubic;\n\t} else {\n\t\t// In local showHistory() we forget current scroll state,\n\t\t// so we need to restore it synchronously, otherwise we may\n\t\t// jump to the bottom of history in some updateHistoryGeometry() call.\n\t\tsynteticScrollToY(scrollTop);\n\t}\n\tconst auto itemId = attachTo->fullId();\n\tconst auto relativeFrom = scrollTop - itemTop;\n\tconst auto relativeTo = scrollTo - itemTop;\n\t_scrollToAnimation.start(\n\t\t[=] { scrollToAnimationCallback(itemId, relativeTo); },\n\t\trelativeFrom,\n\t\trelativeTo,\n\t\tst::slideDuration,\n\t\tanim::sineInOut);\n}\n\nvoid HistoryWidget::scrollToAnimationCallback(\n\t\tFullMsgId attachToId,\n\t\tint relativeTo) {\n\tauto itemTop = _list->itemTop(session().data().message(attachToId));\n\tif (itemTop < 0) {\n\t\t_scrollToAnimation.stop();\n\t} else {\n\t\tsynteticScrollToY(qRound(_scrollToAnimation.value(relativeTo))\n\t\t\t+ itemTop);\n\t}\n\tif (!_scrollToAnimation.animating()) {\n\t\tpreloadHistoryByScroll();\n\t\tcheckReplyReturns();\n\t}\n}\n\nvoid HistoryWidget::enqueueMessageHighlight(\n\t\tconst HistoryView::SelectedQuote &quote) {\n\t_highlighter.enqueue(quote);\n}\n\nUi::ChatPaintHighlight HistoryWidget::itemHighlight(\n\t\tnot_null<const HistoryItem*> item) const {\n\treturn _highlighter.state(item);\n}\n\nint HistoryWidget::itemTopForHighlight(\n\t\tnot_null<HistoryView::Element*> view) const {\n\tif (const auto group = session().data().groups().find(view->data())) {\n\t\tif (const auto leader = group->items.front()->mainView()) {\n\t\t\tview = leader;\n\t\t}\n\t}\n\tconst auto itemTop = _list->itemTop(view);\n\tAssert(itemTop >= 0);\n\n\tconst auto item = view->data();\n\tconst auto unwatchedEffect = item->hasUnwatchedEffect();\n\tconst auto showReactions = item->hasUnreadReaction() || unwatchedEffect;\n\tconst auto reactionCenter = showReactions\n\t\t? view->reactionButtonParameters({}, {}).center.y()\n\t\t: -1;\n\n\tconst auto visibleAreaHeight = _scroll->height();\n\tconst auto viewHeight = view->height();\n\tconst auto heightLeft = (visibleAreaHeight - viewHeight);\n\tif (heightLeft >= 0) {\n\t\treturn std::max(itemTop - (heightLeft / 2), 0);\n\t}\n\tconst auto highlight = itemHighlight(item);\n\tif (const auto range = HistoryView::FindHighlightYRange(\n\t\t\tview,\n\t\t\thighlight)) {\n\t\treturn HistoryView::AdjustScrollForRange(\n\t\t\titemTop,\n\t\t\tvisibleAreaHeight,\n\t\t\trange);\n\t} else if (reactionCenter >= 0) {\n\t\tconst auto maxSize = st::reactionInlineImage;\n\n\t\t// Show message right till the bottom.\n\t\tconst auto forBottom = itemTop + viewHeight - visibleAreaHeight;\n\n\t\t// Show message bottom and some space below for the effect.\n\t\tconst auto bottomResult = forBottom + maxSize;\n\n\t\t// Show the reaction button center in the middle.\n\t\tconst auto byReactionResult = itemTop\n\t\t\t+ reactionCenter\n\t\t\t- visibleAreaHeight / 2;\n\n\t\t// Show the reaction center and some space above it for the effect.\n\t\tconst auto maxAllowed = itemTop + reactionCenter - 2 * maxSize;\n\t\treturn std::max(\n\t\t\tstd::min(maxAllowed, std::max(bottomResult, byReactionResult)),\n\t\t\t0);\n\t}\n\treturn itemTop;\n}\n\nvoid HistoryWidget::initFieldAutocomplete() {\n\t_emojiSuggestions = nullptr;\n\t_autocomplete = nullptr;\n\tif (!_peer) {\n\t\treturn;\n\t}\n\tconst auto processShortcut = [=](QString shortcut) {\n\t\tif (!_peer) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto messages = &_peer->owner().shortcutMessages();\n\t\tconst auto shortcutId = messages->lookupShortcutId(shortcut);\n\t\tif (shortcut.isEmpty()) {\n\t\t\tcontroller()->showSettings(Settings::QuickRepliesId());\n\t\t} else if (!_peer->session().premium()) {\n\t\t\tShowPremiumPreviewToBuy(\n\t\t\t\tcontroller(),\n\t\t\t\tPremiumFeature::QuickReplies);\n\t\t} else if (shortcutId) {\n\t\t\tsession().api().sendShortcutMessages(_peer, shortcutId);\n\t\t\tsession().api().finishForwarding(prepareSendAction({}));\n\t\t\tsetFieldText(_field->getTextWithTagsPart(\n\t\t\t\t_field->textCursor().position()));\n\t\t}\n\t};\n\tChatHelpers::InitFieldAutocomplete(_autocomplete, {\n\t\t.parent = this,\n\t\t.show = controller()->uiShow(),\n\t\t.field = _field.data(),\n\t\t.peer = _peer,\n\t\t.features = [=] {\n\t\t\tauto result = ChatHelpers::ComposeFeatures();\n\t\t\tif (_showAnimation\n\t\t\t\t|| isChoosingTheme()\n\t\t\t\t|| (_inlineBot && !_inlineLookingUpBot)) {\n\t\t\t\tresult.autocompleteMentions = false;\n\t\t\t\tresult.autocompleteHashtags = false;\n\t\t\t\tresult.autocompleteCommands = false;\n\t\t\t}\n\t\t\tif (_editMsgId) {\n\t\t\t\tresult.autocompleteCommands = false;\n\t\t\t\tresult.suggestStickersByEmoji = false;\n\t\t\t}\n\t\t\treturn result;\n\t\t},\n\t\t.sendMenuDetails = [=] { return sendMenuDetails(); },\n\t\t.stickerChoosing = [=] {\n\t\t\tif (_history) {\n\t\t\t\tsession().sendProgressManager().update(\n\t\t\t\t\t_history,\n\t\t\t\t\tApi::SendProgressType::ChooseSticker);\n\t\t\t}\n\t\t},\n\t\t.stickerChosen = [=](ChatHelpers::FileChosen &&data) {\n\t\t\tfileChosen(std::move(data));\n\t\t},\n\t\t.setText = [=](TextWithTags text) { if (_peer) setFieldText(text); },\n\t\t.sendBotCommand = [=](QString command) {\n\t\t\tif (_peer) {\n\t\t\t\tsendBotCommand({ _peer, command, FullMsgId(), replyTo() });\n\t\t\t\tsession().api().finishForwarding(prepareSendAction({}));\n\t\t\t}\n\t\t},\n\t\t.processShortcut = processShortcut,\n\t\t.moderateKeyActivateCallback = [=](int key) {\n\t\t\tconst auto context = [=](FullMsgId itemId) {\n\t\t\t\treturn _list->prepareClickContext(Qt::LeftButton, itemId);\n\t\t\t};\n\t\t\treturn !_keyboard->isHidden() && _keyboard->moderateKeyActivate(\n\t\t\t\tkey,\n\t\t\t\tcontext);\n\t\t},\n\t});\n\tconst auto allow = [=](const auto&) {\n\t\treturn _peer->isSelf();\n\t};\n\t_emojiSuggestions.reset(Ui::Emoji::SuggestionsController::Init(\n\t\tthis,\n\t\t_field,\n\t\t&controller()->session(),\n\t\t{ .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow }));\n}\n\nInlineBotQuery HistoryWidget::parseInlineBotQuery() const {\n\treturn (isChoosingTheme() || _editMsgId)\n\t\t? InlineBotQuery()\n\t\t: ParseInlineBotQuery(&session(), _field);\n}\n\nvoid HistoryWidget::updateInlineBotQuery() {\n\tif (!_history) {\n\t\treturn;\n\t}\n\tconst auto query = parseInlineBotQuery();\n\tif (_inlineBotUsername != query.username) {\n\t\t_inlineBotUsername = query.username;\n\t\tif (_inlineBotResolveRequestId) {\n\t\t\t_api.request(_inlineBotResolveRequestId).cancel();\n\t\t\t_inlineBotResolveRequestId = 0;\n\t\t}\n\t\tif (query.lookingUpBot) {\n\t\t\t_inlineBot = nullptr;\n\t\t\t_inlineLookingUpBot = true;\n\t\t\tconst auto username = _inlineBotUsername;\n\t\t\t_inlineBotResolveRequestId = _api.request(\n\t\t\t\tMTPcontacts_ResolveUsername(\n\t\t\t\t\tMTP_flags(0),\n\t\t\t\t\tMTP_string(username),\n\t\t\t\t\tMTP_string())\n\t\t\t).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\t\t\tconst auto &data = result.data();\n\t\t\t\tconst auto resolvedBot = [&]() -> UserData* {\n\t\t\t\t\tif (const auto user = session().data().processUsers(\n\t\t\t\t\t\t\tdata.vusers())) {\n\t\t\t\t\t\tif (user->isBot()\n\t\t\t\t\t\t\t&& !user->botInfo->inlinePlaceholder.isEmpty()) {\n\t\t\t\t\t\t\treturn user;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn nullptr;\n\t\t\t\t}();\n\t\t\t\tsession().data().processChats(data.vchats());\n\n\t\t\t\t_inlineBotResolveRequestId = 0;\n\t\t\t\tconst auto query = parseInlineBotQuery();\n\t\t\t\tif (_inlineBotUsername == query.username) {\n\t\t\t\t\tapplyInlineBotQuery(\n\t\t\t\t\t\tquery.lookingUpBot ? resolvedBot : query.bot,\n\t\t\t\t\t\tquery.query);\n\t\t\t\t} else {\n\t\t\t\t\tclearInlineBot();\n\t\t\t\t}\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\t_inlineBotResolveRequestId = 0;\n\t\t\t\tif (username == _inlineBotUsername) {\n\t\t\t\t\tclearInlineBot();\n\t\t\t\t}\n\t\t\t}).send();\n\t\t} else {\n\t\t\tapplyInlineBotQuery(query.bot, query.query);\n\t\t}\n\t} else if (query.lookingUpBot) {\n\t\tif (!_inlineLookingUpBot) {\n\t\t\tapplyInlineBotQuery(_inlineBot, query.query);\n\t\t}\n\t} else {\n\t\tapplyInlineBotQuery(query.bot, query.query);\n\t}\n}\n\nvoid HistoryWidget::applyInlineBotQuery(UserData *bot, const QString &query) {\n\tif (bot) {\n\t\tif (_inlineBot != bot) {\n\t\t\t_inlineBot = bot;\n\t\t\t_inlineLookingUpBot = false;\n\t\t\tinlineBotChanged();\n\t\t}\n\t\tif (!_inlineResults) {\n\t\t\t_inlineResults.create(this, controller());\n\t\t\t_inlineResults->setResultSelectedCallback([=](\n\t\t\t\t\tInlineBots::ResultSelected result) {\n\t\t\t\tif (result.open) {\n\t\t\t\t\tconst auto request = result.result->openRequest();\n\t\t\t\t\tconst auto showDrawButton = canWriteMessage();\n\t\t\t\t\tif (const auto photo = request.photo()) {\n\t\t\t\t\t\tcontroller()->openPhoto(\n\t\t\t\t\t\t\tphoto,\n\t\t\t\t\t\t\t{ .showDrawButton = showDrawButton });\n\t\t\t\t\t} else if (const auto document = request.document()) {\n\t\t\t\t\t\tcontroller()->openDocument(\n\t\t\t\t\t\t\tdocument,\n\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\t{ .showDrawButton = showDrawButton });\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tsendInlineResult(result);\n\t\t\t\t}\n\t\t\t});\n\t\t\t_inlineResults->setSendMenuDetails([=] {\n\t\t\t\treturn sendMenuDetails();\n\t\t\t});\n\t\t\t_inlineResults->requesting(\n\t\t\t) | rpl::on_next([=](bool requesting) {\n\t\t\t\t_tabbedSelectorToggle->setLoading(requesting);\n\t\t\t}, _inlineResults->lifetime());\n\t\t\tupdateControlsGeometry();\n\t\t\torderWidgets();\n\t\t}\n\t\t_inlineResults->queryInlineBot(_inlineBot, _peer, query);\n\t\tif (_autocomplete) {\n\t\t\t_autocomplete->hideAnimated();\n\t\t}\n\t} else {\n\t\tclearInlineBot();\n\t}\n}\n\nvoid HistoryWidget::orderWidgets() {\n\t_voiceRecordBar->raise();\n\t_send->raise();\n\t_aiButton->raise();\n\t_sendAsFile->raise();\n\t_topBars->raise();\n\tif (_businessBotStatus) {\n\t\t_businessBotStatus->bar().raise();\n\t}\n\tif (_contactStatus) {\n\t\t_contactStatus->bar().raise();\n\t}\n\tif (_paysStatus) {\n\t\t_paysStatus->bar().raise();\n\t}\n\tif (_translateBar) {\n\t\t_translateBar->raise();\n\t}\n\tif (_sponsoredMessageBar) {\n\t\t_sponsoredMessageBar->raise();\n\t}\n\tif (_pinnedBar) {\n\t\t_pinnedBar->raise();\n\t}\n\tif (_requestsBar) {\n\t\t_requestsBar->raise();\n\t}\n\tif (_groupCallBar) {\n\t\t_groupCallBar->raise();\n\t}\n\tif (_chooseTheme) {\n\t\t_chooseTheme->raise();\n\t}\n\tif (_subsectionTabs) {\n\t\t_subsectionTabs->raise();\n\t}\n\t_topShadow->raise();\n\tif (_autocomplete) {\n\t\t_autocomplete->raise();\n\t}\n\tif (_membersDropdown) {\n\t\t_membersDropdown->raise();\n\t}\n\tif (_inlineResults) {\n\t\t_inlineResults->raise();\n\t}\n\tif (_tabbedPanel) {\n\t\t_tabbedPanel->raise();\n\t}\n\tif (_emojiSuggestions) {\n\t\t_emojiSuggestions->raise();\n\t}\n\tif (_attachBotsMenu) {\n\t\t_attachBotsMenu->raise();\n\t}\n\tif (_aiTooltipManager) {\n\t\t_aiTooltipManager->raise();\n\t}\n\tif (_sendAsFileTooltipManager) {\n\t\t_sendAsFileTooltipManager->raise();\n\t}\n\t_attachDragAreas.document->raise();\n\t_attachDragAreas.photo->raise();\n}\n\nvoid HistoryWidget::toggleChooseChatTheme(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::optional<bool> show) {\n\tconst auto update = [=] {\n\t\tupdateInlineBotQuery();\n\t\tupdateControlsGeometry();\n\t\tupdateControlsVisibility();\n\t};\n\tif (peer.get() != _peer) {\n\t\treturn;\n\t} else if (_chooseTheme) {\n\t\tif (isChoosingTheme() && !show.value_or(false)) {\n\t\t\tconst auto was = base::take(_chooseTheme);\n\t\t\tif (Ui::InFocusChain(this)) {\n\t\t\t\tsetInnerFocus();\n\t\t\t}\n\t\t\tupdate();\n\t\t}\n\t\treturn;\n\t} else if (!show.value_or(true)) {\n\t\treturn;\n\t} else if (_voiceRecordBar->isActive()) {\n\t\tcontroller()->showToast(tr::lng_chat_theme_cant_voice(tr::now));\n\t\treturn;\n\t}\n\t_chooseTheme = std::make_unique<Ui::ChooseThemeController>(\n\t\tthis,\n\t\tcontroller(),\n\t\tpeer);\n\t_chooseTheme->shouldBeShownValue(\n\t) | rpl::on_next(update, _chooseTheme->lifetime());\n}\n\nUi::ChatTheme *HistoryWidget::customChatTheme() const {\n\treturn _list ? _list->theme().get() : nullptr;\n}\n\nvoid HistoryWidget::fieldChanged() {\n\tconst auto updateTyping = (_textUpdateEvents\n\t\t& TextUpdateEvent::SendTyping);\n\n\tInvokeQueued(this, [=] {\n\t\tupdateInlineBotQuery();\n\t\tif (_history\n\t\t\t&& !_inlineBot\n\t\t\t&& !_editMsgId\n\t\t\t&& (!_autocomplete || !_autocomplete->stickersEmoji())\n\t\t\t&& updateTyping) {\n\t\t\tsession().sendProgressManager().update(\n\t\t\t\t_history,\n\t\t\t\tApi::SendProgressType::Typing);\n\t\t}\n\t});\n\n\tcheckCharsCount();\n\n\tupdateSendButtonType();\n\tif (!HasSendText(_field)) {\n\t\t_fieldIsEmpty = true;\n\t} else if (_fieldIsEmpty) {\n\t\t_fieldIsEmpty = false;\n\t\tif (_kbShown) {\n\t\t\ttoggleKeyboard();\n\t\t}\n\t}\n\tif (updateCmdStartShown()) {\n\t\tupdateControlsVisibility();\n\t\tupdateControlsGeometry();\n\t}\n\tupdateAiButtonVisibility();\n\tupdateSendAsFileVisibility();\n\n\t_saveCloudDraftTimer.cancel();\n\tif (!_peer || !(_textUpdateEvents & TextUpdateEvent::SaveDraft)) {\n\t\treturn;\n\t}\n\n\t_saveDraftText = true;\n\tsaveDraft(true);\n}\n\nvoid HistoryWidget::saveDraftDelayed() {\n\tif (!_peer || !(_textUpdateEvents & TextUpdateEvent::SaveDraft)) {\n\t\treturn;\n\t}\n\tif (!_field->textCursor().position()\n\t\t&& !_field->textCursor().anchor()\n\t\t&& !_field->scrollTop().current()) {\n\t\tif (!session().local().hasDraftCursors(_peer->id)) {\n\t\t\treturn;\n\t\t}\n\t}\n\tsaveDraft(true);\n}\n\nvoid HistoryWidget::saveDraft(bool delayed) {\n\tif (!_peer) {\n\t\treturn;\n\t} else if (delayed) {\n\t\tauto ms = crl::now();\n\t\tif (!_saveDraftStart) {\n\t\t\t_saveDraftStart = ms;\n\t\t\treturn _saveDraftTimer.callOnce(kSaveDraftTimeout);\n\t\t} else if (ms - _saveDraftStart < kSaveDraftAnywayTimeout) {\n\t\t\treturn _saveDraftTimer.callOnce(kSaveDraftTimeout);\n\t\t}\n\t}\n\twriteDrafts();\n}\n\nvoid HistoryWidget::saveFieldToHistoryLocalDraft() {\n\tif (!_history) {\n\t\treturn;\n\t}\n\n\tconst auto topicRootId = MsgId();\n\tconst auto monoforumPeerId = PeerId();\n\tif (_editMsgId) {\n\t\t_history->setLocalEditDraft(std::make_unique<Data::Draft>(\n\t\t\t_field,\n\t\t\tFullReplyTo{\n\t\t\t\t.messageId = FullMsgId(_history->peer->id, _editMsgId),\n\t\t\t\t.topicRootId = topicRootId,\n\t\t\t\t.monoforumPeerId = monoforumPeerId,\n\t\t\t},\n\t\t\tsuggestOptions(true),\n\t\t\t_preview->draft(),\n\t\t\t_saveEditMsgRequestId));\n\t} else {\n\t\tconst auto suggest = suggestOptions();\n\t\tif (_replyTo || suggest.exists || !_field->empty()) {\n\t\t\t_history->setLocalDraft(std::make_unique<Data::Draft>(\n\t\t\t\t_field,\n\t\t\t\t_replyTo,\n\t\t\t\tsuggest,\n\t\t\t\t_preview->draft()));\n\t\t} else {\n\t\t\t_history->clearLocalDraft(topicRootId, monoforumPeerId);\n\t\t}\n\t\t_history->clearLocalEditDraft(topicRootId, monoforumPeerId);\n\t}\n}\n\nvoid HistoryWidget::fileChosen(ChatHelpers::FileChosen &&data) {\n\tcontroller()->hideLayer(anim::type::normal);\n\tif (const auto info = data.document->sticker()\n\t\t; info && info->setType == Data::StickersType::Emoji) {\n\t\tif (data.document->isPremiumEmoji()\n\t\t\t&& !session().premium()\n\t\t\t&& (!_peer\n\t\t\t\t|| !Data::AllowEmojiWithoutPremium(\n\t\t\t\t\t_peer,\n\t\t\t\t\tdata.document))) {\n\t\t\tshowPremiumToast(data.document);\n\t\t} else if (!_field->isHidden()) {\n\t\t\tData::InsertCustomEmoji(_field.data(), data.document);\n\t\t}\n\t} else if (_history) {\n\t\tcontroller()->sendingAnimation().appendSending(\n\t\t\tdata.messageSendingFrom);\n\t\tconst auto localId = data.messageSendingFrom.localId;\n\t\tauto messageToSend = Api::MessageToSend(\n\t\t\tprepareSendAction(data.options));\n\t\tmessageToSend.textWithTags = base::take(data.caption);\n\t\tsendExistingDocument(\n\t\t\tdata.document,\n\t\t\tstd::move(messageToSend),\n\t\t\tlocalId);\n\t}\n}\n\nvoid HistoryWidget::saveCloudDraft() {\n\tcontroller()->session().api().saveCurrentDraftToCloud();\n}\n\nvoid HistoryWidget::writeDraftTexts() {\n\tExpects(_history != nullptr);\n\n\tsession().local().writeDrafts(_history);\n\tif (_migrated) {\n\t\t_migrated->clearDrafts();\n\t\tsession().local().writeDrafts(_migrated);\n\t}\n}\n\nvoid HistoryWidget::writeDraftCursors() {\n\tExpects(_history != nullptr);\n\n\tsession().local().writeDraftCursors(_history);\n\tif (_migrated) {\n\t\t_migrated->clearDrafts();\n\t\tsession().local().writeDraftCursors(_migrated);\n\t}\n}\n\nvoid HistoryWidget::writeDrafts() {\n\tconst auto save = (_history != nullptr) && (_saveDraftStart > 0);\n\t_saveDraftStart = 0;\n\t_saveDraftTimer.cancel();\n\tif (save) {\n\t\tif (_saveDraftText) {\n\t\t\twriteDraftTexts();\n\t\t}\n\t\twriteDraftCursors();\n\t}\n\t_saveDraftText = false;\n\n\tif (!_editMsgId && !_inlineBot) {\n\t\t_saveCloudDraftTimer.callOnce(kSaveCloudDraftIdleTimeout);\n\t}\n}\n\nbool HistoryWidget::isRecording() const {\n\treturn _voiceRecordBar->isRecording();\n}\n\nvoid HistoryWidget::activate() {\n\tif (_history) {\n\t\tif (!_historyInited) {\n\t\t\tupdateHistoryGeometry(true);\n\t\t} else if (hasPendingResizedItems()) {\n\t\t\tupdateHistoryGeometry();\n\t\t}\n\t}\n\tcontroller()->widget()->setInnerFocus();\n}\n\nvoid HistoryWidget::setInnerFocus() {\n\tif (_list) {\n\t\tif (isSearching() && !_nonEmptySelection) {\n\t\t\t_composeSearch->setInnerFocus();\n\t\t} else if (isChoosingTheme()) {\n\t\t\t_chooseTheme->setFocus();\n\t\t} else if (_showAnimation\n\t\t\t|| _nonEmptySelection\n\t\t\t|| (_list && _list->wasSelectedText())\n\t\t\t|| isRecording()\n\t\t\t|| isJoinChannel()\n\t\t\t|| isBotStart()\n\t\t\t|| isBlocked()\n\t\t\t|| (!_canSendTexts && !_editMsgId)) {\n\t\t\tif (_scroll->isHidden()) {\n\t\t\t\tsetFocus();\n\t\t\t} else {\n\t\t\t\t_list->setFocus();\n\t\t\t}\n\t\t} else {\n\t\t\t_field->setFocus();\n\t\t}\n\t} else if (_scroll->isHidden()) {\n\t\tsetFocus();\n\t}\n}\n\nbool HistoryWidget::notify_switchInlineBotButtonReceived(\n\t\tconst QString &query,\n\t\tUserData *samePeerBot,\n\t\tMsgId samePeerReplyTo) {\n\tif (samePeerBot) {\n\t\tconst auto to = controller()->dialogsEntryStateCurrent();\n\t\tif (!to.key.owningHistory()) {\n\t\t\treturn false;\n\t\t}\n\t\tcontroller()->switchInlineQuery(to, samePeerBot, query);\n\t\treturn true;\n\t} else if (const auto bot = _peer ? _peer->asUser() : nullptr) {\n\t\tconst auto to = bot->isBot()\n\t\t\t? bot->botInfo->inlineReturnTo\n\t\t\t: Dialogs::EntryState();\n\t\tif (!to.key.owningHistory()) {\n\t\t\treturn false;\n\t\t}\n\t\tbot->botInfo->inlineReturnTo = Dialogs::EntryState();\n\t\tcontroller()->switchInlineQuery(to, bot, query);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid HistoryWidget::tryProcessKeyInput(not_null<QKeyEvent*> e) {\n\te->accept();\n\tkeyPressEvent(e);\n\tif (!e->isAccepted()\n\t\t&& _canSendTexts\n\t\t&& _field->isVisible()\n\t\t&& !e->text().isEmpty()) {\n\t\t_field->setFocusFast();\n\t\tQCoreApplication::sendEvent(_field->rawTextEdit(), e);\n\t}\n}\n\nvoid HistoryWidget::setupShortcuts() {\n\tShortcuts::Requests(\n\t) | rpl::filter([=] {\n\t\treturn _history\n\t\t\t&& Ui::AppInFocus()\n\t\t\t&& Ui::InFocusChain(this)\n\t\t\t&& !controller()->isLayerShown()\n\t\t\t&& window()->isActiveWindow();\n\t}) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {\n\t\tusing Command = Shortcuts::Command;\n\t\trequest->check(Command::Search, 1) && request->handle([=] {\n\t\t\tcontroller()->searchInChat(_history);\n\t\t\treturn true;\n\t\t});\n\t\trequest->check(Command::ShowChatMenu, 1) && request->handle([=] {\n\t\t\tWindow::ActivateWindow(controller());\n\t\t\t_topBar->showPeerMenu();\n\t\t\treturn true;\n\t\t});\n\t\t_canSendMessages\n\t\t\t&& request->check(Command::ShowScheduled, 1)\n\t\t\t&& request->handle([=] {\n\t\t\t\tusing Scheduled = HistoryView::ScheduledMemento;\n\t\t\t\tcontroller()->showSection(\n\t\t\t\t\tstd::make_shared<Scheduled>(_history));\n\t\t\t\treturn true;\n\t\t\t});\n\t\tif (showRecordButton()\n\t\t\t&& _canSendMessages\n\t\t\t&& _joinChannel->isHidden()\n\t\t\t&& !_composeSearch) {\n\t\t\tconst auto isVoice = request->check(Command::RecordVoice, 1);\n\t\t\tconst auto isRound = !isVoice\n\t\t\t\t&& request->check(Command::RecordRound, 1);\n\t\t\t(isVoice || isRound) && request->handle([=] {\n\t\t\t\tif (_voiceRecordBar) {\n\t\t\t\t\t_voiceRecordBar->startRecordingAndLock(isRound);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t});\n\t\t}\n\t\tconst auto channel = _peer ? _peer->asChannel() : nullptr;\n\t\tconst auto hasRecentActions = channel\n\t\t\t&& (channel->hasAdminRights() || channel->amCreator());\n\t\tif (hasRecentActions) {\n\t\t\trequest->check(Command::ShowAdminLog, 1) && request->handle([=] {\n\t\t\t\tcontroller()->showSection(\n\t\t\t\t\tstd::make_shared<AdminLog::SectionMemento>(channel));\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\t\tif (session().supportMode()) {\n\t\t\trequest->check(\n\t\t\t\tCommand::SupportToggleMuted\n\t\t\t) && request->handle([=] {\n\t\t\t\ttoggleMuteUnmute();\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\t}, lifetime());\n}\n\nvoid HistoryWidget::setupGiftToChannelButton() {\n\t_giftToChannel = Ui::CreateChild<Ui::IconButton>(\n\t\t_muteUnmute.data(),\n\t\tst::historyGiftToChannel);\n\t_giftToChannel->setAccessibleName(tr::lng_gift_channel_title(tr::now));\n\twidthValue() | rpl::on_next([=](int width) {\n\t\t_giftToChannel->moveToRight(0, 0, width);\n\t}, _giftToChannel->lifetime());\n\t_giftToChannel->setClickedCallback([=] {\n\t\tUi::ShowStarGiftBox(controller(), _peer);\n\t});\n\trpl::combine(\n\t\t_muteUnmute->shownValue(),\n\t\t_joinChannel->shownValue()\n\t) | rpl::on_next([=](bool muteUnmute, bool joinChannel) {\n\t\tconst auto newParent = (muteUnmute && !joinChannel)\n\t\t\t? _muteUnmute.data()\n\t\t\t: (joinChannel && !muteUnmute)\n\t\t\t? _joinChannel.data()\n\t\t\t: nullptr;\n\t\tif (newParent) {\n\t\t\t_giftToChannel->setParent(newParent);\n\t\t\t_giftToChannel->moveToRight(0, 0);\n\t\t\trefreshGiftToChannelShown();\n\t\t}\n\t}, _giftToChannel->lifetime());\n}\n\nvoid HistoryWidget::setupDirectMessageButton() {\n\t_directMessage = Ui::CreateChild<Ui::IconButton>(\n\t\t_muteUnmute.data(),\n\t\tst::historyDirectMessage);\n\t\t_directMessage->setAccessibleName(tr::lng_profile_direct_messages(tr::now));\n\twidthValue() | rpl::on_next([=](int width) {\n\t\t_directMessage->moveToLeft(0, 0, width);\n\t}, _directMessage->lifetime());\n\t_directMessage->setClickedCallback([=] {\n\t\tif (const auto channel = _peer ? _peer->asChannel() : nullptr) {\n\t\t\tif (channel->invitePeekExpires()) {\n\t\t\t\tcontroller()->showToast(\n\t\t\t\t\ttr::lng_channel_invite_private(tr::now));\n\t\t\t} else if (const auto monoforum = channel->monoforumLink()) {\n\t\t\t\tcontroller()->showPeerHistory(\n\t\t\t\t\tmonoforum,\n\t\t\t\t\tWindow::SectionShow::Way::Forward);\n\t\t\t}\n\t\t}\n\t});\n\trpl::combine(\n\t\t_muteUnmute->shownValue(),\n\t\t_joinChannel->shownValue()\n\t) | rpl::on_next([=](bool muteUnmute, bool joinChannel) {\n\t\tconst auto newParent = (muteUnmute && !joinChannel)\n\t\t\t? _muteUnmute.data()\n\t\t\t: (joinChannel && !muteUnmute)\n\t\t\t? _joinChannel.data()\n\t\t\t: nullptr;\n\t\tif (newParent) {\n\t\t\t_directMessage->setParent(newParent);\n\t\t\t_directMessage->moveToLeft(0, 0);\n\t\t\trefreshDirectMessageShown();\n\t\t}\n\t}, _directMessage->lifetime());\n}\n\nvoid HistoryWidget::pushReplyReturn(not_null<HistoryItem*> item) {\n\tif (item->history() != _history && item->history() != _migrated) {\n\t\treturn;\n\t}\n\t_cornerButtons.pushReplyReturn(item);\n\tupdateControlsVisibility();\n}\n\nQVector<FullMsgId> HistoryWidget::replyReturns() const {\n\treturn _cornerButtons.replyReturns();\n}\n\nvoid HistoryWidget::setReplyReturns(\n\t\tPeerId peer,\n\t\tQVector<FullMsgId> replyReturns) {\n\tif (!_peer || _peer->id != peer) {\n\t\treturn;\n\t}\n\t_cornerButtons.setReplyReturns(std::move(replyReturns));\n}\n\nvoid HistoryWidget::fastShowAtEnd(not_null<History*> history) {\n\tif (_history != history) {\n\t\treturn;\n\t}\n\n\tclearAllLoadRequests();\n\tsetMsgId(ShowAtUnreadMsgId);\n\t_pinnedClickedId = FullMsgId();\n\t_minPinnedId = std::nullopt;\n\tif (_history->isReadyFor(_showAtMsgId)) {\n\t\t_history->forgetScrollState();\n\t\tif (_migrated) {\n\t\t\t_migrated->forgetScrollState();\n\t\t}\n\t\thistoryLoaded();\n\t} else {\n\t\tfirstLoadMessages();\n\t\tdoneShow();\n\t}\n}\n\nbool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) {\n\tInvokeQueued(this, [=] {\n\t\tif (_autocomplete) {\n\t\t\t_autocomplete->requestStickersUpdate();\n\t\t}\n\t});\n\n\tconst auto editDraft = _history\n\t\t? _history->localEditDraft(MsgId(), PeerId())\n\t\t: nullptr;\n\tconst auto draft = editDraft\n\t\t? editDraft\n\t\t: _history\n\t\t? _history->localDraft(MsgId(), PeerId())\n\t\t: nullptr;\n\tauto fieldAvailable = canWriteMessage();\n\tconst auto editMsgId = editDraft ? editDraft->reply.messageId.msg : 0;\n\tif (_voiceRecordBar->isActive() || (!_canSendTexts && !editMsgId)) {\n\t\tif (!_canSendTexts) {\n\t\t\tclearFieldText(0, fieldHistoryAction);\n\t\t}\n\t\treturn false;\n\t}\n\n\tif (!draft || (!editDraft && !fieldAvailable)) {\n\t\tconst auto fieldWillBeHiddenAfterEdit = (!fieldAvailable\n\t\t\t&& _editMsgId != 0);\n\t\tclearFieldText(0, fieldHistoryAction);\n\t\tsetInnerFocus();\n\t\t_processingReplyItem = _replyEditMsg = nullptr;\n\t\t_processingReplyTo = _replyTo = FullReplyTo();\n\t\tsetEditMsgId(0);\n\t\tif (_preview) {\n\t\t\t_preview->apply({ .removed = true });\n\t\t}\n\t\tif (fieldWillBeHiddenAfterEdit) {\n\t\t\tupdateControlsVisibility();\n\t\t\tupdateControlsGeometry();\n\t\t}\n\t\trefreshTopBarActiveChat();\n\t\treturn true;\n\t}\n\n\t_textUpdateEvents = 0;\n\tsetFieldText(draft->textWithTags, 0, fieldHistoryAction);\n\tsetInnerFocus();\n\tdraft->cursor.applyTo(_field);\n\t_textUpdateEvents = TextUpdateEvent::SaveDraft\n\t\t| TextUpdateEvent::SendTyping;\n\n\t_processingReplyItem = _replyEditMsg = nullptr;\n\t_processingReplyTo = _replyTo = FullReplyTo();\n\tsetEditMsgId(editMsgId);\n\tupdateCmdStartShown();\n\tupdateControlsVisibility();\n\tupdateControlsGeometry();\n\trefreshTopBarActiveChat();\n\tif (_editMsgId) {\n\t\tupdateReplyEditTexts();\n\t\tif (!_replyEditMsg) {\n\t\t\trequestMessageData(_editMsgId);\n\t\t}\n\t\tif (editDraft && editDraft->suggest) {\n\t\t\tusing namespace HistoryView;\n\t\t\tapplySuggestOptions(editDraft->suggest, SuggestMode::Change);\n\t\t} else {\n\t\t\tcancelSuggestPost();\n\t\t}\n\t} else {\n\t\tconst auto draft = _history->localDraft(MsgId(), PeerId());\n\t\t_processingReplyTo = draft ? draft->reply : FullReplyTo();\n\t\tif (_processingReplyTo) {\n\t\t\t_processingReplyItem = session().data().message(\n\t\t\t\t_processingReplyTo.messageId);\n\t\t} else if (draft && draft->suggest) {\n\t\t\tusing namespace HistoryView;\n\t\t\tapplySuggestOptions(draft->suggest, SuggestMode::New);\n\t\t}\n\t\tprocessReply();\n\t}\n\n\tif (_preview) {\n\t\t_preview->setDisabled(_editMsgId\n\t\t\t&& _replyEditMsg\n\t\t\t&& _replyEditMsg->media()\n\t\t\t&& !_replyEditMsg->media()->webpage());\n\t\tif (!_editMsgId) {\n\t\t\t_preview->apply(draft->webpage, true);\n\t\t} else if (!_replyEditMsg\n\t\t\t|| !_replyEditMsg->media()\n\t\t\t|| _replyEditMsg->media()->webpage()) {\n\t\t\t_preview->apply(draft->webpage, false);\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid HistoryWidget::applyCloudDraft(History *history) {\n\tExpects(!session().supportMode());\n\n\tif (_history == history && !_editMsgId) {\n\t\tapplyDraft(Ui::InputField::HistoryAction::NewEntry);\n\n\t\tupdateControlsVisibility();\n\t\tupdateControlsGeometry();\n\t}\n}\n\nbool HistoryWidget::insideJumpToEndInsteadOfToUnread() const {\n\tExpects(_history != nullptr);\n\n\tif (session().supportMode() || !_history->trackUnreadMessages()) {\n\t\treturn true;\n\t} else if (!_historyInited) {\n\t\treturn false;\n\t}\n\t_history->calculateFirstUnreadMessage();\n\tconst auto unread = _history->firstUnreadMessage();\n\tconst auto visibleBottom = _scroll->scrollTop() + _scroll->height();\n\tDEBUG_LOG((\"JumpToEnd(%1, %2, %3): \"\n\t\t\"unread: %4, top: %5, visibleBottom: %6.\"\n\t\t).arg(_history->peer->name()\n\t\t).arg(_history->inboxReadTillId().bare\n\t\t).arg(Logs::b(_history->loadedAtBottom())\n\t\t).arg(unread ? unread->data()->id.bare : 0\n\t\t).arg(unread ? _list->itemTop(unread) : -1\n\t\t).arg(visibleBottom));\n\treturn unread && _list->itemTop(unread) <= visibleBottom;\n}\n\nvoid HistoryWidget::showHistory(\n\t\tPeerId peerId,\n\t\tMsgId showAtMsgId,\n\t\tconst Window::SectionShow &params) {\n\t_pinnedClickedId = FullMsgId();\n\t_minPinnedId = std::nullopt;\n\t_showAtMsgParams = {};\n\n\tconst auto wasState = controller()->dialogsEntryStateCurrent();\n\tconst auto startBot = (showAtMsgId == ShowAndStartBotMsgId);\n\t_showAndMaybeSendStart = (showAtMsgId == ShowAndMaybeStartBotMsgId);\n\tif (startBot || _showAndMaybeSendStart) {\n\t\tshowAtMsgId = ShowAtTheEndMsgId;\n\t}\n\n\t_highlighter.clear();\n\tcontroller()->sendingAnimation().clear();\n\t_topToast.hide(anim::type::instant);\n\tif (_history) {\n\t\tif (_peer->id == peerId) {\n\t\t\tupdateForwarding();\n\n\t\t\tif (params.reapplyLocalDraft) {\n\t\t\t\treturn;\n\t\t\t} else if (showAtMsgId == ShowAtUnreadMsgId\n\t\t\t\t&& insideJumpToEndInsteadOfToUnread()) {\n\t\t\t\tDEBUG_LOG((\"JumpToEnd(%1, %2, %3): \"\n\t\t\t\t\t\"Jumping to end instead of unread.\"\n\t\t\t\t\t).arg(_history->peer->name()\n\t\t\t\t\t).arg(_history->inboxReadTillId().bare\n\t\t\t\t\t).arg(Logs::b(_history->loadedAtBottom())));\n\t\t\t\tshowAtMsgId = ShowAtTheEndMsgId;\n\t\t\t} else if (showAtMsgId == ShowForChooseMessagesMsgId) {\n\t\t\t\tif (_chooseForReport) {\n\t\t\t\t\tclearSelected();\n\t\t\t\t\t_chooseForReport->active = true;\n\t\t\t\t\t_list->setChooseReportReason(\n\t\t\t\t\t\t_chooseForReport->reportInput);\n\t\t\t\t\tupdateControlsVisibility();\n\t\t\t\t\tupdateControlsGeometry();\n\t\t\t\t\tupdateTopBarChooseForReport();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!IsServerMsgId(showAtMsgId)\n\t\t\t\t&& !IsClientMsgId(showAtMsgId)\n\t\t\t\t&& !IsServerMsgId(-showAtMsgId)) {\n\t\t\t\t// To end or to unread.\n\t\t\t\tdestroyUnreadBar();\n\t\t\t}\n\t\t\tconst auto canShowNow = _history->isReadyFor(showAtMsgId);\n\t\t\tif (!canShowNow) {\n\t\t\t\tif (!_firstLoadRequest) {\n\t\t\t\t\tDEBUG_LOG((\"JumpToEnd(%1, %2, %3): Showing delayed at %4.\"\n\t\t\t\t\t\t).arg(_history->peer->name()\n\t\t\t\t\t\t).arg(_history->inboxReadTillId().bare\n\t\t\t\t\t\t).arg(Logs::b(_history->loadedAtBottom())\n\t\t\t\t\t\t).arg(showAtMsgId.bare));\n\t\t\t\t\tdelayedShowAt(showAtMsgId, params);\n\t\t\t\t} else if (_showAtMsgId != showAtMsgId) {\n\t\t\t\t\tclearAllLoadRequests();\n\t\t\t\t\tsetMsgId(showAtMsgId, params);\n\t\t\t\t\tfirstLoadMessages();\n\t\t\t\t\tdoneShow();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t_history->forgetScrollState();\n\t\t\t\tif (_migrated) {\n\t\t\t\t\t_migrated->forgetScrollState();\n\t\t\t\t}\n\n\t\t\t\tclearDelayedShowAt();\n\t\t\t\tconst auto skipId = (_migrated && showAtMsgId < 0)\n\t\t\t\t\t? FullMsgId(_migrated->peer->id, -showAtMsgId)\n\t\t\t\t\t: (showAtMsgId > 0)\n\t\t\t\t\t? FullMsgId(_history->peer->id, showAtMsgId)\n\t\t\t\t\t: FullMsgId();\n\t\t\t\tif (skipId) {\n\t\t\t\t\t_cornerButtons.skipReplyReturn(skipId);\n\t\t\t\t}\n\n\t\t\t\tsetMsgId(showAtMsgId, params);\n\t\t\t\tif (_historyInited) {\n\t\t\t\t\tDEBUG_LOG((\"JumpToEnd(%1, %2, %3): \"\n\t\t\t\t\t\t\"Showing instant at %4.\"\n\t\t\t\t\t\t).arg(_history->peer->name()\n\t\t\t\t\t\t).arg(_history->inboxReadTillId().bare\n\t\t\t\t\t\t).arg(Logs::b(_history->loadedAtBottom())\n\t\t\t\t\t\t).arg(showAtMsgId.bare));\n\n\t\t\t\t\tconst auto to = countInitialScrollTop();\n\t\t\t\t\tconst auto item = getItemFromHistoryOrMigrated(\n\t\t\t\t\t\t_showAtMsgId);\n\t\t\t\t\tanimatedScrollToY(\n\t\t\t\t\t\tstd::clamp(to, 0, _scroll->scrollTopMax()),\n\t\t\t\t\t\titem);\n\t\t\t\t} else {\n\t\t\t\t\thistoryLoaded();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t_topBar->update();\n\t\t\tupdate();\n\n\t\t\tif (const auto user = _peer->asUser()) {\n\t\t\t\tif (const auto &info = user->botInfo) {\n\t\t\t\t\tif (startBot || clearMaybeSendStart()) {\n\t\t\t\t\t\tif (startBot && wasState.key) {\n\t\t\t\t\t\t\tinfo->inlineReturnTo = wasState;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsendBotStartCommand();\n\t\t\t\t\t\t_history->clearLocalDraft(MsgId(), PeerId());\n\t\t\t\t\t\tapplyDraft();\n\t\t\t\t\t\t_send->finishAnimating();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t} else {\n\t\t\t_sponsoredMessagesStateKnown = false;\n\t\t\tsession().sponsoredMessages().clearItems(_history);\n\t\t\tsession().data().hideShownSpoilers();\n\t\t\t_composeSearch = nullptr;\n\t\t}\n\t\tsession().sendProgressManager().update(\n\t\t\t_history,\n\t\t\tApi::SendProgressType::Typing,\n\t\t\t-1);\n\t\tsession().data().histories().sendPendingReadInbox(_history);\n\t\tsession().sendProgressManager().cancelTyping(_history);\n\t}\n\n\t_cornerButtons.clearReplyReturns();\n\tif (_history) {\n\t\tif (Ui::InFocusChain(this)) {\n\t\t\t// Removing focus from list clears selected and updates top bar.\n\t\t\tsetFocus();\n\t\t}\n\t\tcontroller()->session().api().saveCurrentDraftToCloud();\n\t\tif (_migrated) {\n\t\t\t_migrated->clearDrafts(); // use migrated draft only once\n\t\t}\n\n\t\t_history->showAtMsgId = _showAtMsgId;\n\n\t\tdestroyUnreadBarOnClose();\n\t\t_sponsoredMessageBar = nullptr;\n\t\t_pinnedBar = nullptr;\n\t\t_translateBar = nullptr;\n\t\t_pinnedTracker = nullptr;\n\t\t_groupCallBar = nullptr;\n\t\t_requestsBar = nullptr;\n\t\t_chooseTheme = nullptr;\n\t\t_membersDropdown.destroy();\n\t\t_scrollToAnimation.stop();\n\n\t\tsetHistory(nullptr);\n\t\t_list = nullptr;\n\t\t_peer = nullptr;\n\t\t_suggestOptions = nullptr;\n\t\t_sendPayment.clear();\n\t\t_topicsRequested.clear();\n\t\t_canSendMessages = false;\n\t\t_canSendTexts = false;\n\t\t_fieldDisabled = nullptr;\n\t\t_silent.destroy();\n\t\tupdateBotKeyboard();\n\n\t\t_subsectionCheckLifetime.destroy();\n\t\tif (_subsectionTabs) {\n\t\t\t_subsectionTabsLifetime.destroy();\n\t\t\tcontroller()->saveSubsectionTabs(base::take(_subsectionTabs));\n\t\t}\n\t} else {\n\t\tAssert(_list == nullptr);\n\t}\n\n\tHistoryView::Element::ClearGlobal();\n\n\t_saveEditMsgRequestId = 0;\n\t_processingReplyItem = _replyEditMsg = nullptr;\n\t_processingReplyTo = _replyTo = FullReplyTo();\n\t_editMsgId = MsgId();\n\t_canReplaceMedia = _canAddMedia = false;\n\t_photoEditMedia = nullptr;\n\tupdateReplaceMediaButton();\n\t_fieldBarCancel->hide();\n\n\t_mediaEditManager.cancel();\n\t_membersDropdownShowTimer.cancel();\n\t_scroll->takeWidget<HistoryInner>().destroy();\n\n\tclearInlineBot();\n\n\t_showAtMsgId = showAtMsgId;\n\t_showAtMsgParams = params;\n\t_historyInited = false;\n\t_paysStatus = nullptr;\n\t_contactStatus = nullptr;\n\t_businessBotStatus = nullptr;\n\n\tCore::App().mediaDevices().refreshRecordAvailability();\n\n\tif (peerId) {\n\t\tusing namespace HistoryView;\n\t\t_peer = session().data().peer(peerId);\n\t\t_contactStatus = std::make_unique<ContactStatus>(\n\t\t\tcontroller(),\n\t\t\t_topBars.get(),\n\t\t\t_peer,\n\t\t\tfalse);\n\t\t_contactStatus->bar().heightValue(\n\t\t) | rpl::on_next([=] {\n\t\t\tupdateControlsGeometry();\n\t\t}, _contactStatus->bar().lifetime());\n\n\t\trefreshGiftToChannelShown();\n\t\trefreshDirectMessageShown();\n\t\tif (const auto user = _peer->asUser()) {\n\t\t\t_paysStatus = std::make_unique<PaysStatus>(\n\t\t\t\tcontroller(),\n\t\t\t\t_topBars.get(),\n\t\t\t\tuser);\n\t\t\t_paysStatus->bar().heightValue(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tupdateControlsGeometry();\n\t\t\t}, _paysStatus->bar().lifetime());\n\t\t\t_businessBotStatus = std::make_unique<BusinessBotStatus>(\n\t\t\t\tcontroller(),\n\t\t\t\t_topBars.get(),\n\t\t\t\tuser);\n\t\t\t_businessBotStatus->bar().heightValue(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tupdateControlsGeometry();\n\t\t\t}, _businessBotStatus->bar().lifetime());\n\t\t}\n\t\torderWidgets();\n\t\tcontroller()->tabbedSelector()->setCurrentPeer(_peer);\n\t}\n\trefreshTabbedPanel();\n\tinitFieldAutocomplete();\n\n\tif (_peer) {\n\t\t_unblock->setText(((_peer->isUser()\n\t\t\t&& _peer->asUser()->isBot()\n\t\t\t&& !_peer->asUser()->isSupport())\n\t\t\t\t? tr::lng_restart_button(tr::now)\n\t\t\t\t: tr::lng_unblock_button(tr::now)).toUpper());\n\t}\n\n\t_nonEmptySelection = false;\n\t_itemRevealPending.clear();\n\t_itemRevealAnimations.clear();\n\t_itemsRevealHeight = 0;\n\n\tif (_peer) {\n\t\tsetHistory(_peer->owner().history(_peer));\n\t\tif (_migrated\n\t\t\t&& !_migrated->isEmpty()\n\t\t\t&& (!_history->loadedAtTop() || !_migrated->loadedAtBottom())) {\n\t\t\t_migrated->clear(History::ClearType::Unload);\n\t\t}\n\t\t_history->setFakeUnreadWhileOpened(true);\n\n\t\tif (_showAtMsgId == ShowForChooseMessagesMsgId) {\n\t\t\t_showAtMsgId = ShowAtUnreadMsgId;\n\t\t\tif (_chooseForReport) {\n\t\t\t\t_chooseForReport->active = true;\n\t\t\t}\n\t\t} else {\n\t\t\t_chooseForReport = nullptr;\n\t\t}\n\t\tif (_showAtMsgId == ShowAtUnreadMsgId\n\t\t\t&& !_history->trackUnreadMessages()\n\t\t\t&& !hasSavedScroll()) {\n\t\t\t_showAtMsgId = ShowAtTheEndMsgId;\n\t\t}\n\t\trefreshTopBarActiveChat();\n\t\tupdateTopBarSelection();\n\n\t\tif (_peer->isChannel()) {\n\t\t\tupdateNotifyControls();\n\t\t\tsession().data().notifySettings().request(_peer);\n\t\t\trefreshSilentToggle();\n\t\t} else if (_peer->isRepliesChat() || _peer->isVerifyCodes()) {\n\t\t\tupdateNotifyControls();\n\t\t}\n\t\trefreshSuggestPostToggle();\n\t\trefreshScheduledToggle();\n\t\trefreshSendGiftToggle();\n\t\trefreshSendAsToggle();\n\n\t\tif (_showAtMsgId == ShowAtUnreadMsgId) {\n\t\t\tif (_history->scrollTopItem) {\n\t\t\t\t_showAtMsgId = _history->showAtMsgId;\n\t\t\t}\n\t\t} else {\n\t\t\t_history->forgetScrollState();\n\t\t\tif (_migrated) {\n\t\t\t\t_migrated->forgetScrollState();\n\t\t\t}\n\t\t}\n\n\t\t_scroll->hide();\n\t\t_list = _scroll->setOwnedWidget(\n\t\t\tobject_ptr<HistoryInner>(this, _scroll, controller(), _history));\n\t\t_list->sendIntroSticker(\n\t\t) | rpl::on_next([=](not_null<DocumentData*> sticker) {\n\t\t\tsendExistingDocument(\n\t\t\t\tsticker,\n\t\t\t\tApi::MessageToSend(prepareSendAction({})));\n\t\t}, _list->lifetime());\n\t\t_list->show();\n\n\t\tif (const auto channel = _peer->asChannel()) {\n\t\t\tchannel->updateFull();\n\t\t\tif (!channel->isBroadcast()) {\n\t\t\t\tusing Flags = Data::Flags<ChannelDataFlags>;\n\t\t\t\tchannel->flagsValue() | rpl::skip(\n\t\t\t\t\t1\n\t\t\t\t) | rpl::on_next([=](Flags::Change change) {\n\t\t\t\t\trefreshJoinChannelText();\n\t\t\t\t\tif (change.diff & ChannelDataFlag::MonoforumDisabled) {\n\t\t\t\t\t\tupdateCanSendMessage();\n\t\t\t\t\t\tupdateSendRestriction();\n\t\t\t\t\t\tupdateHistoryGeometry();\n\t\t\t\t\t}\n\t\t\t\t}, _list->lifetime());\n\t\t\t}\n\t\t\trefreshJoinChannelText();\n\t\t}\n\n\t\tcontroller()->adaptive().changes(\n\t\t) | rpl::on_next([=] {\n\t\t\t_history->forceFullResize();\n\t\t\tif (_migrated) {\n\t\t\t\t_migrated->forceFullResize();\n\t\t\t}\n\t\t\tupdateHistoryGeometry();\n\t\t\tupdate();\n\t\t}, _list->lifetime());\n\n\t\tif (_chooseForReport && _chooseForReport->active) {\n\t\t\t_list->setChooseReportReason(_chooseForReport->reportInput);\n\t\t}\n\t\tupdateTopBarChooseForReport();\n\n\t\t_updateHistoryItems.cancel();\n\n\t\tsetupTranslateBar();\n\t\tsetupPinnedTracker();\n\t\tsetupGroupCallBar();\n\t\tsetupRequestsBar();\n\t\tcheckMessagesTTL();\n\t\tif (_history->scrollTopItem\n\t\t\t|| (_migrated && _migrated->scrollTopItem)\n\t\t\t|| _history->isReadyFor(_showAtMsgId)) {\n\t\t\thistoryLoaded();\n\t\t} else {\n\t\t\tfirstLoadMessages();\n\t\t\tdoneShow();\n\t\t}\n\n\t\thandlePeerUpdate();\n\n\t\tsession().local().readDraftsWithCursors(_history);\n\t\tif (!applyDraft()) {\n\t\t\tclearFieldText();\n\t\t}\n\t\tcheckCharsCount();\n\t\t_send->finishAnimating();\n\n\t\tupdateControlsGeometry();\n\n\t\tif (const auto user = _peer->asUser()) {\n\t\t\tif (const auto &info = user->botInfo) {\n\t\t\t\tif (startBot\n\t\t\t\t\t|| (!_history->isEmpty() && clearMaybeSendStart())) {\n\t\t\t\t\tif (startBot && wasState.key) {\n\t\t\t\t\t\tinfo->inlineReturnTo = wasState;\n\t\t\t\t\t}\n\t\t\t\t\tsendBotStartCommand();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tInfo::Profile::BirthdayValue(\n\t\t\t\t\tuser\n\t\t\t\t) | rpl::map(\n\t\t\t\t\tData::IsBirthdayTodayValue\n\t\t\t\t) | rpl::flatten_latest(\n\t\t\t\t) | rpl::distinct_until_changed(\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\trefreshSendGiftToggle();\n\t\t\t\t\tupdateControlsVisibility();\n\t\t\t\t\tupdateControlsGeometry();\n\t\t\t\t}, _list->lifetime());\n\t\t\t}\n\t\t}\n\t\tif (!_history->folderKnown()) {\n\t\t\tsession().data().histories().requestDialogEntry(_history);\n\t\t}\n\n\t\t// Must be done before unreadCountUpdated(), or we auto-close.\n\t\tif (_history->unreadMark()) {\n\t\t\tsession().data().histories().changeDialogUnreadMark(\n\t\t\t\t_history,\n\t\t\t\tfalse);\n\t\t}\n\t\tif (_migrated && _migrated->unreadMark()) {\n\t\t\tsession().data().histories().changeDialogUnreadMark(\n\t\t\t\t_migrated,\n\t\t\t\tfalse);\n\t\t}\n\t\tunreadCountUpdated(); // set _historyDown badge.\n\t\tshowAboutTopPromotion();\n\n\t\tif (!session().sponsoredMessages().isTopBarFor(_history)) {\n\t\t\t_scroll->setTrackingContent(false);\n\t\t\tconst auto checkState = [=] {\n\t\t\t\tusing State = Data::SponsoredMessages::State;\n\t\t\t\tconst auto state = session().sponsoredMessages().state(\n\t\t\t\t\t_history);\n\t\t\t\t_sponsoredMessagesStateKnown = (state != State::None);\n\t\t\t\tif (state == State::AppendToEnd) {\n\t\t\t\t\t_scroll->setTrackingContent(\n\t\t\t\t\t\tsession().sponsoredMessages().canHaveFor(_history));\n\t\t\t\t} else if (state == State::InjectToMiddle) {\n\t\t\t\t\tinjectSponsoredMessages();\n\t\t\t\t} else if (state == State::AppendToTopBar) {\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto history = _history;\n\t\t\tsession().sponsoredMessages().request(\n\t\t\t\t_history,\n\t\t\t\tcrl::guard(this, [=, this] {\n\t\t\t\t\tif (history == _history) {\n\t\t\t\t\t\tcheckState();\n\t\t\t\t\t}\n\t\t\t\t}));\n\t\t\tcheckState();\n\t\t} else {\n\t\t\trequestSponsoredMessageBar();\n\t\t}\n\t} else {\n\t\t_chooseForReport = nullptr;\n\t\trefreshTopBarActiveChat();\n\t\tupdateTopBarSelection();\n\t\tcheckMessagesTTL();\n\t\tclearFieldText();\n\t\tdoneShow();\n\t}\n\tupdateForwarding();\n\tupdateOverStates(mapFromGlobal(QCursor::pos()));\n\n\tif (_history) {\n\t\tconst auto msgId = (_showAtMsgId == ShowAtTheEndMsgId)\n\t\t\t? ShowAtUnreadMsgId\n\t\t\t: _showAtMsgId;\n\t\tcontroller()->setActiveChatEntry({\n\t\t\t_history,\n\t\t\tFullMsgId(_history->peer->id, msgId) });\n\t}\n\tupdate();\n\tcontroller()->floatPlayerAreaUpdated();\n\tsession().data().itemVisibilitiesUpdated();\n\n\tcrl::on_main(this, [=] { controller()->widget()->setInnerFocus(); });\n}\n\nvoid HistoryWidget::setHistory(History *history) {\n\tif (_history == history) {\n\t\treturn;\n\t}\n\n\tconst auto was = _attachBotsMenu && _history && _history->peer->isUser();\n\tconst auto now = _attachBotsMenu && history && history->peer->isUser();\n\tif (was && !now) {\n\t\t_attachToggle->removeEventFilter(_attachBotsMenu.get());\n\t\t_attachBotsMenu->hideFast();\n\t} else if (now && !was && !ChatHelpers::ShowPanelOnClick()) {\n\t\t_attachToggle->installEventFilter(_attachBotsMenu.get());\n\t}\n\n\tconst auto unloadHeavyViewParts = [](History *history) {\n\t\tif (history) {\n\t\t\thistory->owner().unloadHeavyViewParts(\n\t\t\t\thistory->delegateMixin()->delegate());\n\t\t\thistory->forceFullResize();\n\t\t}\n\t};\n\n\tif (_history) {\n\t\tunregisterDraftSources();\n\t\tclearAllLoadRequests();\n\t\tclearSupportPreloadRequest();\n\t\t_historySponsoredPreloading.destroy();\n\t\tconst auto wasHistory = base::take(_history);\n\t\tconst auto wasMigrated = base::take(_migrated);\n\t\tunloadHeavyViewParts(wasHistory);\n\t\tunloadHeavyViewParts(wasMigrated);\n\t\tif (const auto wasCreatingBotTopic = base::take(_creatingBotTopic)) {\n\t\t\twasCreatingBotTopic->forum()->discardCreatingId(\n\t\t\t\twasCreatingBotTopic->rootId());\n\t\t}\n\t}\n\tif (history) {\n\t\t_history = history;\n\t\t_migrated = _history ? _history->migrateFrom() : nullptr;\n\t\tregisterDraftSource();\n\t\tif (_history) {\n\t\t\tsetupPreview();\n\t\t} else {\n\t\t\t_previewDrawPreview = nullptr;\n\t\t\t_preview = nullptr;\n\t\t}\n\t}\n\trefreshAttachBotsMenu();\n}\n\nvoid HistoryWidget::setupPreview() {\n\tExpects(_history != nullptr);\n\n\tusing namespace HistoryView::Controls;\n\t_preview = std::make_unique<WebpageProcessor>(_history, _field);\n\t_preview->repaintRequests() | rpl::on_next([=] {\n\t\tupdateField();\n\t}, _preview->lifetime());\n\n\t_preview->parsedValue(\n\t) | rpl::on_next([=](WebpageParsed value) {\n\t\t_previewTitle.setText(\n\t\t\tst::msgNameStyle,\n\t\t\tvalue.title,\n\t\t\tUi::NameTextOptions());\n\t\t_previewDescription.setText(\n\t\t\tst::defaultTextStyle,\n\t\t\tvalue.description,\n\t\t\tUi::DialogTextOptions());\n\t\tconst auto changed = (!_previewDrawPreview != !value.drawPreview);\n\t\t_previewDrawPreview = value.drawPreview;\n\t\tif (changed) {\n\t\t\tupdateControlsGeometry();\n\t\t\tupdateControlsVisibility();\n\t\t}\n\t\tupdateField();\n\t}, _preview->lifetime());\n}\n\nvoid HistoryWidget::injectSponsoredMessages() const {\n\tsession().sponsoredMessages().inject(\n\t\t_history,\n\t\t_showAtMsgId,\n\t\t_scroll->height() * 2,\n\t\t_scroll->width());\n}\n\nvoid HistoryWidget::refreshAttachBotsMenu() {\n\t_attachBotsMenu = nullptr;\n\tif (!_history) {\n\t\treturn;\n\t}\n\t_attachBotsMenu = InlineBots::MakeAttachBotsMenu(\n\t\tthis,\n\t\tcontroller(),\n\t\t_history->peer,\n\t\t[=] { return prepareSendAction({}); },\n\t\t[=](bool compress) { chooseAttach(compress); });\n\tif (!_attachBotsMenu) {\n\t\treturn;\n\t}\n\t_attachBotsMenu->setOrigin(\n\t\tUi::PanelAnimation::Origin::BottomLeft);\n\tif (!ChatHelpers::ShowPanelOnClick()) {\n\t\t_attachToggle->installEventFilter(_attachBotsMenu.get());\n\t}\n\t_attachBotsMenu->heightValue(\n\t) | rpl::on_next([=] {\n\t\tmoveFieldControls();\n\t}, _attachBotsMenu->lifetime());\n}\n\nvoid HistoryWidget::unregisterDraftSources() {\n\tif (!_history) {\n\t\treturn;\n\t}\n\tsession().local().unregisterDraftSource(\n\t\t_history,\n\t\tData::DraftKey::Local(MsgId(), PeerId()));\n\tsession().local().unregisterDraftSource(\n\t\t_history,\n\t\tData::DraftKey::LocalEdit(MsgId(), PeerId()));\n}\n\nvoid HistoryWidget::registerDraftSource() {\n\tif (!_history) {\n\t\treturn;\n\t}\n\tconst auto peerId = _history->peer->id;\n\tconst auto editMsgId = _editMsgId;\n\tconst auto draft = [=] {\n\t\treturn Storage::MessageDraft{\n\t\t\t(editMsgId\n\t\t\t\t? FullReplyTo{ FullMsgId(peerId, editMsgId) }\n\t\t\t\t: _replyTo),\n\t\t\tsuggestOptions(editMsgId != 0),\n\t\t\t_field->getTextWithTags(),\n\t\t\t_preview->draft(),\n\t\t};\n\t};\n\tauto draftSource = Storage::MessageDraftSource{\n\t\t.draft = draft,\n\t\t.cursor = [=] { return MessageCursor(_field); },\n\t};\n\tsession().local().registerDraftSource(\n\t\t_history,\n\t\t(editMsgId\n\t\t\t? Data::DraftKey::LocalEdit(MsgId(), PeerId())\n\t\t\t: Data::DraftKey::Local(MsgId(), PeerId())),\n\t\tstd::move(draftSource));\n}\n\nvoid HistoryWidget::setEditMsgId(MsgId msgId) {\n\tunregisterDraftSources();\n\t_editMsgId = msgId;\n\tif (!msgId) {\n\t\t_mediaEditManager.cancel();\n\t\t_canReplaceMedia = _canAddMedia = false;\n\t\tif (_preview) {\n\t\t\t_preview->setDisabled(false);\n\t\t}\n\t}\n\tif (_history) {\n\t\trefreshSendAsToggle();\n\t\torderWidgets();\n\t}\n\tregisterDraftSource();\n}\n\nvoid HistoryWidget::clearDelayedShowAt() {\n\t_delayedShowAtMsgId = -1;\n\tclearDelayedShowAtRequest();\n}\n\nvoid HistoryWidget::clearDelayedShowAtRequest() {\n\tExpects(_history != nullptr);\n\n\tif (_delayedShowAtRequest) {\n\t\t_history->owner().histories().cancelRequest(_delayedShowAtRequest);\n\t\t_delayedShowAtRequest = 0;\n\t}\n}\n\nvoid HistoryWidget::clearSupportPreloadRequest() {\n\tExpects(_history != nullptr);\n\n\tif (_supportPreloadRequest) {\n\t\tauto &histories = _history->owner().histories();\n\t\thistories.cancelRequest(_supportPreloadRequest);\n\t\t_supportPreloadRequest = 0;\n\t}\n}\n\nvoid HistoryWidget::clearAllLoadRequests() {\n\tExpects(_history != nullptr);\n\n\tauto &histories = _history->owner().histories();\n\tclearDelayedShowAtRequest();\n\tif (_firstLoadRequest) {\n\t\thistories.cancelRequest(_firstLoadRequest);\n\t\t_firstLoadRequest = 0;\n\t}\n\tif (_preloadRequest) {\n\t\thistories.cancelRequest(_preloadRequest);\n\t\t_preloadRequest = 0;\n\t}\n\tif (_preloadDownRequest) {\n\t\thistories.cancelRequest(_preloadDownRequest);\n\t\t_preloadDownRequest = 0;\n\t}\n}\n\nbool HistoryWidget::updateReplaceMediaButton() {\n\tif (!_canReplaceMedia && !_canAddMedia) {\n\t\tconst auto result = (_replaceMedia != nullptr);\n\t\t_replaceMedia.destroy();\n\t\treturn result;\n\t} else if (_replaceMedia) {\n\t\treturn false;\n\t}\n\t_replaceMedia.create(\n\t\tthis,\n\t\t_canReplaceMedia ? st::historyReplaceMedia : st::historyAddMedia);\n\t_replaceMedia->setAccessibleName(_canReplaceMedia\n\t\t? tr::lng_attach_replace(tr::now)\n\t\t: tr::lng_attach(tr::now));\n\tconst auto hideDuration = st::historyReplaceMedia.ripple.hideDuration;\n\t_replaceMedia->setClickedCallback([=] {\n\t\tbase::call_delayed(hideDuration, this, [=] {\n\t\t\tEditCaptionBox::StartMediaReplace(\n\t\t\t\tcontroller(),\n\t\t\t\t{ _history->peer->id, _editMsgId },\n\t\t\t\t_field->getTextWithTags(),\n\t\t\t\tsuggestOptions(),\n\t\t\t\t_mediaEditManager.spoilered(),\n\t\t\t\t_mediaEditManager.invertCaption(),\n\t\t\t\tcrl::guard(_list, [=] { cancelEdit(); }));\n\t\t});\n\t});\n\treturn true;\n}\n\nvoid HistoryWidget::updateFieldSubmitSettings() {\n\tconst auto settings = _isInlineBot\n\t\t? Ui::InputField::SubmitSettings::None\n\t\t: Core::App().settings().sendSubmitWay();\n\t_field->setSubmitSettings(settings);\n}\n\nvoid HistoryWidget::updateNotifyControls() {\n\tif (!_peer || (!_peer->isChannel() && !_peer->isRepliesChat() && !_peer->isVerifyCodes())) {\n\t\treturn;\n\t}\n\n\t_muteUnmute->setText((_history->muted()\n\t\t? tr::lng_channel_unmute(tr::now)\n\t\t: tr::lng_channel_mute(tr::now)).toUpper());\n\tif (!session().data().notifySettings().silentPostsUnknown(_peer)) {\n\t\tif (_silent) {\n\t\t\t_silent->setChecked(\n\t\t\t\tsession().data().notifySettings().silentPosts(_peer));\n\t\t\tupdateFieldPlaceholder();\n\t\t} else if (hasSilentToggle()) {\n\t\t\trefreshSilentToggle();\n\t\t\tupdateControlsVisibility();\n\t\t\tupdateControlsGeometry();\n\t\t}\n\t}\n}\n\nvoid HistoryWidget::refreshSilentToggle() {\n\tif (!_silent && hasSilentToggle()) {\n\t\t_silent.create(this, _peer->asChannel());\n\t\torderWidgets();\n\t} else if (_silent && !hasSilentToggle()) {\n\t\t_silent.destroy();\n\t}\n}\n\nvoid HistoryWidget::setupFastButtonMode() {\n\tconst auto field = _field->rawTextEdit();\n\tbase::install_event_filter(field, [=](not_null<QEvent*> e) {\n\t\tif (e->type() != QEvent::KeyPress\n\t\t\t|| !_history\n\t\t\t|| !FastButtonsMode()\n\t\t\t|| !session().fastButtonsBots().enabled(_history->peer)\n\t\t\t|| !_field->getLastText().isEmpty()) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\t\tconst auto k = static_cast<QKeyEvent*>(e.get());\n\t\tconst auto key = k->key();\n\t\tif (key < Qt::Key_1 || key > Qt::Key_9 || k->modifiers()) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\t\tconst auto item = _history ? _history->lastMessage() : nullptr;\n\t\tconst auto markup = item ? item->inlineReplyKeyboard() : nullptr;\n\t\tconst auto link = markup\n\t\t\t? markup->getLinkByIndex(key - Qt::Key_1)\n\t\t\t: nullptr;\n\t\tif (!link) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\t\tActivateClickHandler(window(), link, {\n\t\t\tQt::LeftButton,\n\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.itemId = item->fullId(),\n\t\t\t\t.sessionWindow = base::make_weak(controller()),\n\t\t\t}),\n\t\t});\n\t\treturn base::EventFilterResult::Cancel;\n\t});\n}\n\nvoid HistoryWidget::setupScheduledToggle() {\n\tcontroller()->activeChatValue(\n\t) | rpl::map([=](Dialogs::Key key) -> rpl::producer<> {\n\t\tif (const auto history = key.history()) {\n\t\t\treturn session().scheduledMessages().updates(history);\n\t\t} else if (const auto topic = key.topic()) {\n\t\t\treturn session().scheduledMessages().updates(\n\t\t\t\ttopic->owningHistory());\n\t\t}\n\t\treturn rpl::never<rpl::empty_value>();\n\t}) | rpl::flatten_latest(\n\t) | rpl::on_next([=] {\n\t\trefreshScheduledToggle();\n\t\tupdateControlsVisibility();\n\t\tupdateControlsGeometry();\n\t}, lifetime());\n}\n\nvoid HistoryWidget::refreshScheduledToggle() {\n\tconst auto has = _history\n\t\t&& _canSendMessages\n\t\t&& (session().scheduledMessages().count(_history) > 0);\n\tif (!_scheduled && has) {\n\t\t_scheduled.create(this, st::historyScheduledToggle);\n\t\t_scheduled->setAccessibleName(tr::lng_scheduled_messages(tr::now));\n\t\t_scheduled->show();\n\t\t_scheduled->addClickHandler([=] {\n\t\t\tcontroller()->showSection(\n\t\t\t\tstd::make_shared<HistoryView::ScheduledMemento>(_history));\n\t\t});\n\t\torderWidgets(); // Raise drag areas to the top.\n\t} else if (_scheduled && !has) {\n\t\t_scheduled.destroy();\n\t}\n}\n\nvoid HistoryWidget::refreshSendGiftToggle() {\n\tusing Type = Api::DisallowedGiftType;\n\tconst auto user = _peer ? _peer->asUser() : nullptr;\n\tconst auto disallowed = user ? user->disallowedGiftTypes() : Type();\n\tconst auto all = Type::Premium\n\t\t| Type::Unlimited\n\t\t| Type::Limited\n\t\t| Type::Unique;\n\tconst auto has = user\n\t\t&& _canSendMessages\n\t\t&& !user->isServiceUser()\n\t\t&& !user->isSelf()\n\t\t&& !user->isBot()\n\t\t&& ((disallowed & Type::SendHide)\n\t\t\t|| (session().user()->disallowedGiftTypes() & Type::SendHide)\n\t\t\t|| Data::IsBirthdayToday(user->birthday()))\n\t\t&& ((disallowed & all) != all);\n\tif (!_giftToUser && has) {\n\t\t_giftToUser.create(this, st::historyGiftToUser);\n\t\t_giftToUser->setAccessibleName(tr::lng_gift_send_title(tr::now));\n\t\t_giftToUser->show();\n\t\t_giftToUser->addClickHandler([=] {\n\t\t\tUi::ShowStarGiftBox(controller(), _peer);\n\t\t});\n\t\torderWidgets(); // Raise drag areas to the top.\n\t} else if (_giftToUser && !has) {\n\t\t_giftToUser.destroy();\n\t}\n}\n\nvoid HistoryWidget::applySuggestOptions(\n\t\tSuggestOptions suggest,\n\t\tHistoryView::SuggestMode mode) {\n\tExpects(suggest.exists);\n\n\tusing namespace HistoryView;\n\t_suggestOptions = std::make_unique<SuggestOptionsBar>(\n\t\tcontroller()->uiShow(),\n\t\t_peer,\n\t\tsuggest,\n\t\tmode);\n\t_suggestOptions->updates() | rpl::on_next([=] {\n\t\tupdateField();\n\t\tsaveDraftWithTextNow();\n\t}, _suggestOptions->lifetime());\n\tsaveDraftWithTextNow();\n}\n\nvoid HistoryWidget::saveDraftWithTextNow() {\n\t_saveDraftText = true;\n\t_saveDraftStart = crl::now();\n\tsaveDraft();\n}\n\nvoid HistoryWidget::refreshSuggestPostToggle() {\n\tconst auto has = _peer\n\t\t&& _peer->isMonoforum()\n\t\t&& !_peer->amMonoforumAdmin();\n\tif (!_toggleSuggestPost && has) {\n\t\t_toggleSuggestPost.create(this, st::historySuggestPostToggle);\n\t\t_toggleSuggestPost->setVisible(!_suggestOptions);\n\t\t_toggleSuggestPost->addClickHandler([=] {\n\t\t\tusing namespace HistoryView;\n\t\t\tapplySuggestOptions({ .exists = 1 }, SuggestMode::New);\n\t\t\tcancelReply();\n\t\t\t_processingReplyTo = FullReplyTo();\n\t\t\t_processingReplyItem = nullptr;\n\t\t\tupdateControlsVisibility();\n\t\t\tupdateControlsGeometry();\n\t\t});\n\t\torderWidgets();\n\t} else if (_toggleSuggestPost && !has) {\n\t\t_toggleSuggestPost.destroy();\n\t\tcancelSuggestPost();\n\t}\n}\n\nvoid HistoryWidget::setupSendAsToggle() {\n\tsession().sendAsPeers().updated(\n\t) | rpl::filter([=](Main::SendAsKey key) {\n\t\treturn (key.peer == _peer)\n\t\t\t&& (key.type == Main::SendAsType::Message);\n\t}) | rpl::on_next([=] {\n\t\trefreshSendAsToggle();\n\t\tupdateControlsVisibility();\n\t\tupdateControlsGeometry();\n\t\torderWidgets();\n\t}, lifetime());\n}\n\nvoid HistoryWidget::refreshSendAsToggle() {\n\tExpects(_peer != nullptr);\n\n\tconst auto key = Main::SendAsKey{ _peer, Main::SendAsType::Message };\n\tif (_editMsgId || !session().sendAsPeers().shouldChoose(key)) {\n\t\t_sendAs.destroy();\n\t\treturn;\n\t} else if (_sendAs) {\n\t\treturn;\n\t}\n\tconst auto &st = st::defaultComposeControls.chooseSendAs;\n\t_sendAs.create(this, st.button);\n\t_sendAs->setAccessibleName(tr::lng_send_as_title(tr::now));\n\tUi::SetupSendAsButton(_sendAs.data(), st, controller());\n}\n\nbool HistoryWidget::contentOverlapped(const QRect &globalRect) {\n\treturn _attachDragAreas.document->overlaps(globalRect)\n\t\t|| _attachDragAreas.photo->overlaps(globalRect)\n\t\t|| (_autocomplete && _autocomplete->overlaps(globalRect))\n\t\t|| (_tabbedPanel && _tabbedPanel->overlaps(globalRect))\n\t\t|| (_inlineResults && _inlineResults->overlaps(globalRect));\n}\n\nbool HistoryWidget::canWriteMessage() const {\n\treturn _history\n\t\t&& _canSendMessages\n\t\t&& !isBlocked()\n\t\t&& !isJoinChannel()\n\t\t&& !isMuteUnmute()\n\t\t&& !isBotStart()\n\t\t&& !isSearching();\n}\n\nvoid HistoryWidget::updateControlsVisibility() {\n\tauto fieldDisabledRemoved = (_fieldDisabled != nullptr);\n\tconst auto hideExtraButtons = _fieldCharsCountManager.isLimitExceeded();\n\tconst auto guard = gsl::finally([&] {\n\t\tif (fieldDisabledRemoved) {\n\t\t\t_fieldDisabled = nullptr;\n\t\t}\n\t});\n\n\tif (!_showAnimation) {\n\t\t_topShadow->setVisible(_peer != nullptr);\n\t\t_topBar->setVisible(_peer != nullptr);\n\t}\n\t_cornerButtons.updateJumpDownVisibility();\n\t_cornerButtons.updateUnreadThingsVisibility();\n\tif (!_history || _showAnimation) {\n\t\thideChildWidgets();\n\t\treturn;\n\t}\n\n\tif (_firstLoadRequest && !_scroll->isHidden()) {\n\t\tif (Ui::InFocusChain(_scroll.data())) {\n\t\t\t// Don't loose focus back to chats list.\n\t\t\tsetFocus();\n\t\t}\n\t\t_scroll->hide();\n\t} else if (!_firstLoadRequest && _scroll->isHidden()) {\n\t\t_scroll->show();\n\t}\n\t_topBars->show();\n\tif (_sponsoredMessageBar && checkSponsoredMessageBarVisibility()) {\n\t\t_sponsoredMessageBar->toggle(true, anim::type::normal);\n\t}\n\tif (_paysStatus) {\n\t\t_paysStatus->show();\n\t}\n\tif (_contactStatus) {\n\t\t_contactStatus->show();\n\t}\n\tif (_businessBotStatus) {\n\t\t_businessBotStatus->show();\n\t}\n\tif (_subsectionTabs) {\n\t\t_subsectionTabs->show();\n\t}\n\tif (isChoosingTheme()\n\t\t|| (!editingMessage()\n\t\t\t&& (isSearching()\n\t\t\t\t|| isBlocked()\n\t\t\t\t|| isJoinChannel()\n\t\t\t\t|| isMuteUnmute()\n\t\t\t\t|| isBotStart()\n\t\t\t\t|| isReportMessages()))) {\n\t\tconst auto toggle = [&](Ui::FlatButton *shown) {\n\t\t\tconst auto toggleOne = [&](not_null<Ui::FlatButton*> button) {\n\t\t\t\tif (button.get() != shown) {\n\t\t\t\t\tbutton->hide();\n\t\t\t\t} else if (button->isHidden()) {\n\t\t\t\t\tbutton->clearState();\n\t\t\t\t\tbutton->show();\n\t\t\t\t}\n\t\t\t};\n\t\t\ttoggleOne(_reportMessages);\n\t\t\ttoggleOne(_joinChannel);\n\t\t\ttoggleOne(_muteUnmute);\n\t\t\ttoggleOne(_botStart);\n\t\t\ttoggleOne(_unblock);\n\t\t};\n\t\tif (isChoosingTheme()) {\n\t\t\t_chooseTheme->show();\n\t\t\tsetInnerFocus();\n\t\t\ttoggle(nullptr);\n\t\t} else if (isReportMessages()) {\n\t\t\ttoggle(_reportMessages);\n\t\t} else if (isBlocked()) {\n\t\t\ttoggle(_unblock);\n\t\t} else if (isJoinChannel()) {\n\t\t\ttoggle(_joinChannel);\n\t\t} else if (isMuteUnmute()) {\n\t\t\ttoggle(_muteUnmute);\n\t\t} else if (isBotStart()) {\n\t\t\ttoggle(_botStart);\n\t\t}\n\t\t_kbShown = false;\n\t\tif (_autocomplete) {\n\t\t\t_autocomplete->hide();\n\t\t}\n\t\tif (_supportAutocomplete) {\n\t\t\t_supportAutocomplete->hide();\n\t\t}\n\t\t_send->hide();\n\t\tif (_silent) {\n\t\t\t_silent->hide();\n\t\t}\n\t\tif (_scheduled) {\n\t\t\t_scheduled->hide();\n\t\t}\n\t\tif (_toggleSuggestPost) {\n\t\t\t_toggleSuggestPost->hide();\n\t\t}\n\t\tif (_giftToUser) {\n\t\t\t_giftToUser->hide();\n\t\t}\n\t\tif (_ttlInfo) {\n\t\t\t_ttlInfo->hide();\n\t\t}\n\t\tif (_sendAs) {\n\t\t\t_sendAs->hide();\n\t\t}\n\t\t_kbScroll->hide();\n\t\t_fieldBarCancel->hide();\n\t\t_attachToggle->hide();\n\t\tif (_replaceMedia) {\n\t\t\t_replaceMedia->hide();\n\t\t}\n\t\t_tabbedSelectorToggle->hide();\n\t\t_botKeyboardShow->hide();\n\t\t_botKeyboardHide->hide();\n\t\t_botCommandStart->hide();\n\t\tif (_botMenu.button) {\n\t\t\t_botMenu.button->hide();\n\t\t}\n\t\tif (_tabbedPanel) {\n\t\t\t_tabbedPanel->hide();\n\t\t}\n\t\tif (_voiceRecordBar) {\n\t\t\t_voiceRecordBar->hideFast();\n\t\t}\n\t\tif (_inlineResults) {\n\t\t\t_inlineResults->hide();\n\t\t}\n\t\tif (_sendRestriction) {\n\t\t\t_sendRestriction->hide();\n\t\t}\n\t\thideFieldIfVisible();\n\t} else if (editingMessage() || _canSendMessages) {\n\t\tif (_autocomplete) {\n\t\t\t_autocomplete->requestRefresh();\n\t\t}\n\t\t_unblock->hide();\n\t\t_botStart->hide();\n\t\t_joinChannel->hide();\n\t\t_muteUnmute->hide();\n\t\t_reportMessages->hide();\n\t\t_send->show();\n\t\tupdateSendButtonType();\n\n\t\tif (_canSendTexts || _editMsgId) {\n\t\t\t_field->show();\n\t\t} else {\n\t\t\tfieldDisabledRemoved = false;\n\t\t\tif (!_fieldDisabled) {\n\t\t\t\t_fieldDisabled = CreateDisabledFieldView(this, _peer);\n\t\t\t\torderWidgets();\n\t\t\t\tupdateControlsGeometry();\n\t\t\t\tupdate();\n\t\t\t}\n\t\t\t_fieldDisabled->show();\n\t\t\thideFieldIfVisible();\n\t\t}\n\t\tif (_kbShown) {\n\t\t\t_kbScroll->show();\n\t\t\t_tabbedSelectorToggle->hide();\n\t\t\tshowKeyboardHideButton();\n\t\t\t_botKeyboardShow->hide();\n\t\t\t_botCommandStart->hide();\n\t\t} else if (_kbReplyTo) {\n\t\t\t_kbScroll->hide();\n\t\t\t_tabbedSelectorToggle->show();\n\t\t\t_botKeyboardHide->hide();\n\t\t\t_botKeyboardShow->hide();\n\t\t\t_botCommandStart->hide();\n\t\t} else {\n\t\t\t_kbScroll->hide();\n\t\t\t_tabbedSelectorToggle->show();\n\t\t\t_botKeyboardHide->hide();\n\t\t\tif (_keyboard->hasMarkup()) {\n\t\t\t\t_botKeyboardShow->show();\n\t\t\t\t_botCommandStart->hide();\n\t\t\t} else {\n\t\t\t\t_botKeyboardShow->hide();\n\t\t\t\t_botCommandStart->setVisible(_cmdStartShown);\n\t\t\t}\n\t\t}\n\t\tif (_replaceMedia) {\n\t\t\t_replaceMedia->show();\n\t\t\t_attachToggle->hide();\n\t\t} else {\n\t\t\t_attachToggle->show();\n\t\t}\n\t\tif (_botMenu.button) {\n\t\t\t_botMenu.button->show();\n\t\t}\n\t\tif (_sendRestriction) {\n\t\t\t_sendRestriction->hide();\n\t\t}\n\t\t{\n\t\t\tauto rightButtonsChanged = false;\n\t\t\tif (_silent) {\n\t\t\t\tconst auto was = _silent->isVisible();\n\t\t\t\tconst auto now = (!_editMsgId) && (!hideExtraButtons);\n\t\t\t\tif (was != now) {\n\t\t\t\t\t_silent->setVisible(now);\n\t\t\t\t\trightButtonsChanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_scheduled) {\n\t\t\t\tconst auto was = _scheduled->isVisible();\n\t\t\t\tconst auto now = (!_editMsgId) && (!hideExtraButtons);\n\t\t\t\tif (was != now) {\n\t\t\t\t\t_scheduled->setVisible(now);\n\t\t\t\t\trightButtonsChanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_toggleSuggestPost) {\n\t\t\t\tconst auto was = _toggleSuggestPost->isVisible();\n\t\t\t\tconst auto now = !_suggestOptions;\n\t\t\t\tif (was != now) {\n\t\t\t\t\t_toggleSuggestPost->setVisible(now);\n\t\t\t\t\trightButtonsChanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_giftToUser) {\n\t\t\t\tconst auto was = _giftToUser->isVisible();\n\t\t\t\tconst auto now = (!_editMsgId) && (!hideExtraButtons);\n\t\t\t\tif (was != now) {\n\t\t\t\t\t_giftToUser->setVisible(now);\n\t\t\t\t\trightButtonsChanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_ttlInfo) {\n\t\t\t\tconst auto was = _ttlInfo->isVisible();\n\t\t\t\tconst auto now = (!_editMsgId) && (!hideExtraButtons);\n\t\t\t\tif (was != now) {\n\t\t\t\t\t_ttlInfo->setVisible(now);\n\t\t\t\t\trightButtonsChanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (rightButtonsChanged) {\n\t\t\t\tupdateFieldSize();\n\t\t\t}\n\t\t}\n\t\tif (_sendAs) {\n\t\t\t_sendAs->show();\n\t\t}\n\t\tupdateFieldPlaceholder();\n\n\t\tif (_editMsgId\n\t\t\t|| _replyTo\n\t\t\t|| readyToForward()\n\t\t\t|| _previewDrawPreview\n\t\t\t|| _kbReplyTo\n\t\t\t|| _suggestOptions) {\n\t\t\tif (_fieldBarCancel->isHidden()) {\n\t\t\t\t_fieldBarCancel->show();\n\t\t\t\tupdateControlsGeometry();\n\t\t\t\tupdate();\n\t\t\t}\n\t\t} else {\n\t\t\t_fieldBarCancel->hide();\n\t\t}\n\t} else {\n\t\tif (_autocomplete) {\n\t\t\t_autocomplete->hide();\n\t\t}\n\t\tif (_supportAutocomplete) {\n\t\t\t_supportAutocomplete->hide();\n\t\t}\n\t\t_send->hide();\n\t\t_unblock->hide();\n\t\t_botStart->hide();\n\t\t_joinChannel->hide();\n\t\t_muteUnmute->hide();\n\t\t_reportMessages->hide();\n\t\t_attachToggle->hide();\n\t\tif (_silent) {\n\t\t\t_silent->hide();\n\t\t}\n\t\tif (_scheduled) {\n\t\t\t_scheduled->hide();\n\t\t}\n\t\tif (_toggleSuggestPost) {\n\t\t\t_toggleSuggestPost->hide();\n\t\t}\n\t\tif (_giftToUser) {\n\t\t\t_giftToUser->hide();\n\t\t}\n\t\tif (_ttlInfo) {\n\t\t\t_ttlInfo->hide();\n\t\t}\n\t\tif (_sendAs) {\n\t\t\t_sendAs->hide();\n\t\t}\n\t\tif (_botMenu.button) {\n\t\t\t_botMenu.button->hide();\n\t\t}\n\t\t_kbScroll->hide();\n\t\tif (_replyTo || readyToForward() || _kbReplyTo) {\n\t\t\tif (_fieldBarCancel->isHidden()) {\n\t\t\t\t_fieldBarCancel->show();\n\t\t\t\tupdateControlsGeometry();\n\t\t\t\tupdate();\n\t\t\t}\n\t\t} else {\n\t\t\t_fieldBarCancel->hide();\n\t\t}\n\t\t_tabbedSelectorToggle->hide();\n\t\t_botKeyboardShow->hide();\n\t\t_botKeyboardHide->hide();\n\t\t_botCommandStart->hide();\n\t\tif (_tabbedPanel) {\n\t\t\t_tabbedPanel->hide();\n\t\t}\n\t\tif (_voiceRecordBar) {\n\t\t\t_voiceRecordBar->hideFast();\n\t\t}\n\t\tif (_composeSearch) {\n\t\t\t_composeSearch->hideAnimated();\n\t\t}\n\t\tif (_inlineResults) {\n\t\t\t_inlineResults->hide();\n\t\t}\n\t\tif (_sendRestriction) {\n\t\t\t_sendRestriction->show();\n\t\t}\n\t\t_kbScroll->hide();\n\t\thideFieldIfVisible();\n\t}\n\t//checkTabbedSelectorToggleTooltip();\n\tupdateAiButtonVisibility();\n\tupdateSendAsFileVisibility();\n\tupdateMouseTracking();\n}\n\nvoid HistoryWidget::hideFieldIfVisible() {\n\tif (_field->isHidden()) {\n\t\treturn;\n\t} else if (InFocusChain(_field)) {\n\t\tsetFocus();\n\t}\n\t_field->hide();\n\tupdateControlsGeometry();\n\tupdate();\n}\n\nvoid HistoryWidget::showAboutTopPromotion() {\n\tExpects(_history != nullptr);\n\tExpects(_list != nullptr);\n\n\tif (!_history->useTopPromotion() || _history->topPromotionAboutShown()) {\n\t\treturn;\n\t}\n\t_history->markTopPromotionAboutShown();\n\tconst auto type = _history->topPromotionType();\n\tconst auto custom = type.isEmpty()\n\t\t? QString()\n\t\t: Lang::GetNonDefaultValue(kPsaAboutPrefix + type.toUtf8());\n\tconst auto text = type.isEmpty()\n\t\t? tr::lng_proxy_sponsor_about(tr::now, tr::rich)\n\t\t: custom.isEmpty()\n\t\t? tr::lng_about_psa_default(tr::now, tr::rich)\n\t\t: tr::rich(custom);\n\tshowInfoTooltip(text, nullptr);\n}\n\nvoid HistoryWidget::updateMouseTracking() {\n\tconst auto trackMouse = !_fieldBarCancel->isHidden();\n\tsetMouseTracking(trackMouse);\n}\n\nvoid HistoryWidget::destroyUnreadBar() {\n\tif (_history) _history->destroyUnreadBar();\n\tif (_migrated) _migrated->destroyUnreadBar();\n}\n\nvoid HistoryWidget::destroyUnreadBarOnClose() {\n\tif (!_history || !_historyInited) {\n\t\treturn;\n\t} else if (_scroll->scrollTop() == _scroll->scrollTopMax()) {\n\t\tdestroyUnreadBar();\n\t\treturn;\n\t}\n\tconst auto top = unreadBarTop();\n\tif (top && *top < _scroll->scrollTop()) {\n\t\tdestroyUnreadBar();\n\t\treturn;\n\t}\n}\n\nvoid HistoryWidget::newItemAdded(not_null<HistoryItem*> item) {\n\tif (_history != item->history()\n\t\t|| !_historyInited\n\t\t|| item->isScheduled()) {\n\t\treturn;\n\t}\n\tif (item->isSponsored()) {\n\t\tif (const auto view = item->mainView()) {\n\t\t\tview->resizeGetHeight(width());\n\t\t\tupdateHistoryGeometry(\n\t\t\t\tfalse,\n\t\t\t\ttrue,\n\t\t\t\t{ ScrollChangeNoJumpToBottom, 0 });\n\t\t}\n\t\treturn;\n\t}\n\n\t// If we get here in non-resized state we can't rely on results of\n\t// markingMessagesRead() and mark chat as read.\n\t// If we receive N messages being not at bottom:\n\t// - on first message we set unreadcount += 1, firstUnreadMessage.\n\t// - on second we get wrong markingMessagesRead() and read both.\n\tsession().data().sendHistoryChangeNotifications();\n\n\tif (item->isSending()) {\n\t\tsynteticScrollToY(_scroll->scrollTopMax());\n\t} else if (_scroll->scrollTop() < _scroll->scrollTopMax()) {\n\t\treturn;\n\t}\n\tif (item->showNotification()) {\n\t\tdestroyUnreadBar();\n\t\tif (markingMessagesRead()) {\n\t\t\tif (_list && item->hasUnwatchedEffect()) {\n\t\t\t\t_list->startEffectOnRead(item);\n\t\t\t}\n\t\t\tif (item->isUnreadMention() && !item->isUnreadMedia()) {\n\t\t\t\tsession().api().markContentsRead(item);\n\t\t\t}\n\t\t\tsession().data().histories().readInboxOnNewMessage(item);\n\n\t\t\t// Also clear possible scheduled messages notifications.\n\t\t\t// Side-effect: Also clears all notifications from forum topics.\n\t\t\tCore::App().notifications().clearFromHistory(_history);\n\t\t}\n\t}\n\tconst auto view = item->mainView();\n\tif (!view) {\n\t\treturn;\n\t} else if (anim::Disabled()) {\n\t\tif (!On(PowerSaving::kChatBackground)) {\n\t\t\t// Strange case of disabled animations, but enabled bg rotation.\n\t\t\tif (item->out() || _history->peer->isSelf()) {\n\t\t\t\t_list->theme()->rotateComplexGradientBackground();\n\t\t\t}\n\t\t}\n\t\treturn;\n\t}\n\tif (!item->history()->streamedDrafts().hasFor(item)) {\n\t\t_itemRevealPending.emplace(item);\n\t}\n}\n\nvoid HistoryWidget::maybeMarkReactionsRead(not_null<HistoryItem*> item) {\n\tif (!_historyInited || !_list) {\n\t\treturn;\n\t}\n\tconst auto view = item->mainView();\n\tconst auto itemTop = _list->itemTop(view);\n\tif (itemTop <= 0 || !markingContentsRead()) {\n\t\treturn;\n\t}\n\tconst auto reactionCenter\n\t\t= view->reactionButtonParameters({}, {}).center.y();\n\tconst auto visibleTop = _scroll->scrollTop();\n\tconst auto visibleBottom = visibleTop + _scroll->height();\n\tif (itemTop + reactionCenter < visibleTop\n\t\t|| itemTop + view->height() > visibleBottom) {\n\t\treturn;\n\t}\n\tsession().api().markContentsRead(item);\n}\n\nbool HistoryWidget::handleDrawToReplyRequest(\n\t\tData::DrawToReplyRequest request) {\n\tif (!_peer || request.messageId.peer != _peer->id) {\n\t\treturn false;\n\t}\n\tauto image = HistoryView::ResolveDrawToReplyImage(\n\t\t&session().data(),\n\t\trequest);\n\tif (image.isNull()) {\n\t\treturn false;\n\t}\n\tconst auto replyTo = request.messageId;\n\tHistoryView::OpenDrawToReplyEditor(\n\t\tcontroller(),\n\t\tstd::move(image),\n\t\tcrl::guard(this, [=](QImage &&result) {\n\t\t\tif (result.isNull()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (replyTo) {\n\t\t\t\treplyToMessage({ .messageId = replyTo });\n\t\t\t}\n\t\t\tauto list = Storage::PrepareMediaFromImage(\n\t\t\t\tstd::move(result),\n\t\t\t\tQByteArray(),\n\t\t\t\tst::sendMediaPreviewSize);\n\t\t\tconfirmSendingFiles(std::move(list));\n\t\t}));\n\treturn true;\n}\n\nvoid HistoryWidget::unreadCountUpdated() {\n\tif (_history->unreadMark() || (_migrated && _migrated->unreadMark())) {\n\t\tcrl::on_main(this, [=, history = _history] {\n\t\t\tif (history == _history) {\n\t\t\t\tcloseCurrent();\n\t\t\t}\n\t\t});\n\t} else {\n\t\tconst auto hideCounter = _history->isForum()\n\t\t\t|| !_history->trackUnreadMessages();\n\t\t_cornerButtons.updateJumpDownVisibility(hideCounter\n\t\t\t? 0\n\t\t\t: _history->amMonoforumAdmin()\n\t\t\t? _history->chatListUnreadState().messages\n\t\t\t: _history->chatListBadgesState().unreadCounter);\n\t}\n}\n\nvoid HistoryWidget::closeCurrent() {\n\tif (controller()->isPrimary()) {\n\t\tcontroller()->showBackFromStack();\n\t} else {\n\t\tcontroller()->window().close();\n\t}\n}\n\nvoid HistoryWidget::messagesFailed(const MTP::Error &error, int requestId) {\n\tif (error.type() == u\"CHANNEL_PRIVATE\"_q\n\t\t&& _peer->isChannel()\n\t\t&& _peer->asChannel()->invitePeekExpires()) {\n\t\t_peer->asChannel()->privateErrorReceived();\n\t} else if (error.type() == u\"CHANNEL_PRIVATE\"_q\n\t\t|| error.type() == u\"CHANNEL_PUBLIC_GROUP_NA\"_q\n\t\t|| error.type() == u\"USER_BANNED_IN_CHANNEL\"_q) {\n\t\tauto was = _peer;\n\t\tcloseCurrent();\n\t\tconst auto wasAccount = not_null(&was->account());\n\t\tif (const auto primary = Core::App().windowFor(wasAccount)) {\n\t\t\tprimary->showToast(was->isMegagroup()\n\t\t\t\t? tr::lng_group_not_accessible(tr::now)\n\t\t\t\t: tr::lng_channel_not_accessible(tr::now));\n\t\t}\n\t\treturn;\n\t}\n\n\tLOG((\"RPC Error: %1 %2: %3\").arg(\n\t\tQString::number(error.code()),\n\t\terror.type(),\n\t\terror.description()));\n\n\tif (_preloadRequest == requestId) {\n\t\t_preloadRequest = 0;\n\t} else if (_preloadDownRequest == requestId) {\n\t\t_preloadDownRequest = 0;\n\t} else if (_firstLoadRequest == requestId) {\n\t\t_firstLoadRequest = 0;\n\t\tcloseCurrent();\n\t} else if (_delayedShowAtRequest == requestId) {\n\t\t_delayedShowAtRequest = 0;\n\t}\n}\n\nvoid HistoryWidget::messagesReceived(\n\t\tnot_null<PeerData*> peer,\n\t\tconst MTPmessages_Messages &messages,\n\t\tint requestId) {\n\tExpects(_history != nullptr);\n\n\tconst auto toMigrated = (peer == _peer->migrateFrom());\n\tif (peer != _peer && !toMigrated) {\n\t\tif (_preloadRequest == requestId) {\n\t\t\t_preloadRequest = 0;\n\t\t} else if (_preloadDownRequest == requestId) {\n\t\t\t_preloadDownRequest = 0;\n\t\t} else if (_firstLoadRequest == requestId) {\n\t\t\t_firstLoadRequest = 0;\n\t\t} else if (_delayedShowAtRequest == requestId) {\n\t\t\t_delayedShowAtRequest = 0;\n\t\t}\n\t\treturn;\n\t}\n\n\tauto count = 0;\n\tconst QVector<MTPMessage> emptyList, *histList = &emptyList;\n\tswitch (messages.type()) {\n\tcase mtpc_messages_messages: {\n\t\tauto &d(messages.c_messages_messages());\n\t\t_history->owner().processUsers(d.vusers());\n\t\t_history->owner().processChats(d.vchats());\n\t\tpeer->processTopics(d.vtopics());\n\t\thistList = &d.vmessages().v;\n\t\tcount = histList->size();\n\t} break;\n\tcase mtpc_messages_messagesSlice: {\n\t\tauto &d(messages.c_messages_messagesSlice());\n\t\t_history->owner().processUsers(d.vusers());\n\t\t_history->owner().processChats(d.vchats());\n\t\tpeer->processTopics(d.vtopics());\n\t\thistList = &d.vmessages().v;\n\t\tcount = d.vcount().v;\n\t} break;\n\tcase mtpc_messages_channelMessages: {\n\t\tauto &d(messages.c_messages_channelMessages());\n\t\t_history->owner().processUsers(d.vusers());\n\t\t_history->owner().processChats(d.vchats());\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\tchannel->ptsReceived(d.vpts().v);\n\t\t} else {\n\t\t\tLOG((\"API Error: received messages.channelMessages when \"\n\t\t\t\t\"no channel was passed! (HistoryWidget::messagesReceived)\"));\n\t\t}\n\t\tpeer->processTopics(d.vtopics());\n\t\thistList = &d.vmessages().v;\n\t\tcount = d.vcount().v;\n\t} break;\n\tcase mtpc_messages_messagesNotModified: {\n\t\tLOG((\"API Error: received messages.messagesNotModified! \"\n\t\t\t\"(HistoryWidget::messagesReceived)\"));\n\t} break;\n\t}\n\n\tif (_preloadRequest == requestId) {\n\t\taddMessagesToFront(peer, *histList);\n\t\t_preloadRequest = 0;\n\t\tpreloadHistoryIfNeeded();\n\t} else if (_preloadDownRequest == requestId) {\n\t\taddMessagesToBack(peer, *histList);\n\t\t_preloadDownRequest = 0;\n\t\tpreloadHistoryIfNeeded();\n\t\tif (_history->loadedAtBottom()) {\n\t\t\tcheckActivation();\n\t\t}\n\t} else if (_firstLoadRequest == requestId) {\n\t\tif (toMigrated) {\n\t\t\t_history->clear(History::ClearType::Unload);\n\t\t} else if (_migrated) {\n\t\t\t_migrated->clear(History::ClearType::Unload);\n\t\t}\n\t\taddMessagesToFront(peer, *histList);\n\t\t_firstLoadRequest = 0;\n\t\tif (_history->loadedAtTop() && _history->isEmpty() && count > 0) {\n\t\t\tfirstLoadMessages();\n\t\t\treturn;\n\t\t}\n\n\t\thistoryLoaded();\n\t\tinjectSponsoredMessages();\n\t} else if (_delayedShowAtRequest == requestId) {\n\t\tif (toMigrated) {\n\t\t\t_history->clear(History::ClearType::Unload);\n\t\t} else if (_migrated) {\n\t\t\t_migrated->clear(History::ClearType::Unload);\n\t\t}\n\n\t\tclearAllLoadRequests();\n\t\t_firstLoadRequest = -1; // hack - don't updateListSize yet\n\t\t_history->getReadyFor(_delayedShowAtMsgId);\n\t\tif (_history->isEmpty()) {\n\t\t\taddMessagesToFront(peer, *histList);\n\t\t}\n\t\t_firstLoadRequest = 0;\n\n\t\tif (_history->loadedAtTop()\n\t\t\t&& _history->isEmpty()\n\t\t\t&& count > 0) {\n\t\t\tfirstLoadMessages();\n\t\t\treturn;\n\t\t}\n\t\tconst auto skipId = (_migrated && _delayedShowAtMsgId < 0)\n\t\t\t? FullMsgId(_migrated->peer->id, -_delayedShowAtMsgId)\n\t\t\t: (_delayedShowAtMsgId > 0)\n\t\t\t? FullMsgId(_history->peer->id, _delayedShowAtMsgId)\n\t\t\t: FullMsgId();\n\t\tif (skipId) {\n\t\t\t_cornerButtons.skipReplyReturn(skipId);\n\t\t}\n\n\t\t_delayedShowAtRequest = 0;\n\t\tsetMsgId(_delayedShowAtMsgId, _delayedShowAtMsgParams);\n\t\thistoryLoaded();\n\t}\n\tif (session().supportMode()) {\n\t\tcrl::on_main(this, [=] { checkSupportPreload(); });\n\t}\n}\n\nvoid HistoryWidget::historyLoaded() {\n\t_historyInited = false;\n\tdoneShow();\n}\n\nbool HistoryWidget::clearMaybeSendStart() {\n\tif (!_showAndMaybeSendStart || !_history) {\n\t\treturn false;\n\t} else if (!_history->peer->isFullLoaded()) {\n\t\t_history->peer->updateFull();\n\t\treturn false;\n\t}\n\t_showAndMaybeSendStart = false;\n\tif (const auto user = _history ? _history->peer->asUser() : nullptr) {\n\t\tif (user->blockStatus() == PeerData::BlockStatus::NotBlocked) {\n\t\t\tif (const auto info = user->botInfo.get()) {\n\t\t\t\tif (!info->startToken.isEmpty()) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid HistoryWidget::windowShown() {\n\tupdateControlsGeometry();\n}\n\nbool HistoryWidget::markingMessagesRead() const {\n\treturn markingContentsRead() && !session().supportMode();\n}\n\nbool HistoryWidget::markingContentsRead() const {\n\treturn _history\n\t\t&& _list\n\t\t&& _historyInited\n\t\t&& !_firstLoadRequest\n\t\t&& !_delayedShowAtRequest\n\t\t&& !_showAnimation\n\t\t&& controller()->widget()->markingAsRead();\n}\n\nvoid HistoryWidget::checkActivation() {\n\tif (_list) {\n\t\t_list->checkActivation();\n\t}\n}\n\nvoid HistoryWidget::firstLoadMessages() {\n\tif (!_history || _firstLoadRequest) {\n\t\treturn;\n\t}\n\n\tauto from = _history;\n\tauto offsetId = MsgId();\n\tauto offset = 0;\n\tauto loadCount = kMessagesPerPage;\n\tif (_showAtMsgId == ShowAtUnreadMsgId) {\n\t\tif (const auto around = _migrated ? _migrated->loadAroundId() : 0) {\n\t\t\t_history->getReadyFor(_showAtMsgId);\n\t\t\tfrom = _migrated;\n\t\t\toffset = -loadCount / 2;\n\t\t\toffsetId = around;\n\t\t} else if (const auto around = _history->loadAroundId()) {\n\t\t\t_history->getReadyFor(_showAtMsgId);\n\t\t\toffset = -loadCount / 2;\n\t\t\toffsetId = around;\n\t\t} else {\n\t\t\t_history->getReadyFor(ShowAtTheEndMsgId);\n\t\t}\n\t} else if (_showAtMsgId == ShowAtTheEndMsgId) {\n\t\t_history->getReadyFor(_showAtMsgId);\n\t\tloadCount = kMessagesPerPageFirst;\n\t} else if (_showAtMsgId > 0) {\n\t\t_history->getReadyFor(_showAtMsgId);\n\t\toffset = -loadCount / 2;\n\t\toffsetId = _showAtMsgId;\n\t} else if (_showAtMsgId < 0 && _history->peer->isChannel()) {\n\t\tif (_showAtMsgId < 0 && -_showAtMsgId < ServerMaxMsgId && _migrated) {\n\t\t\t_history->getReadyFor(_showAtMsgId);\n\t\t\tfrom = _migrated;\n\t\t\toffset = -loadCount / 2;\n\t\t\toffsetId = -_showAtMsgId;\n\t\t} else if (_showAtMsgId == SwitchAtTopMsgId) {\n\t\t\t_history->getReadyFor(_showAtMsgId);\n\t\t}\n\t}\n\n\tconst auto offsetDate = 0;\n\tconst auto maxId = 0;\n\tconst auto minId = 0;\n\tconst auto historyHash = uint64(0);\n\n\tconst auto history = from;\n\tconst auto type = Data::Histories::RequestType::History;\n\tauto &histories = history->owner().histories();\n\t_firstLoadRequest = histories.sendRequest(history, type, [=](\n\t\t\tFn<void()> finish) {\n\t\treturn history->session().api().request(MTPmessages_GetHistory(\n\t\t\thistory->peer->input(),\n\t\t\tMTP_int(offsetId),\n\t\t\tMTP_int(offsetDate),\n\t\t\tMTP_int(offset),\n\t\t\tMTP_int(loadCount),\n\t\t\tMTP_int(maxId),\n\t\t\tMTP_int(minId),\n\t\t\tMTP_long(historyHash)\n\t\t)).done([=](const MTPmessages_Messages &result) {\n\t\t\tmessagesReceived(history->peer, result, _firstLoadRequest);\n\t\t\tfinish();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tmessagesFailed(error, _firstLoadRequest);\n\t\t\tfinish();\n\t\t}).send();\n\t});\n}\n\nvoid HistoryWidget::loadMessages() {\n\tif (!_history || _preloadRequest) {\n\t\treturn;\n\t}\n\n\tif (_history->isEmpty() && _migrated && _migrated->isEmpty()) {\n\t\treturn firstLoadMessages();\n\t}\n\n\tauto loadMigrated = _migrated\n\t\t&& (_history->isEmpty()\n\t\t\t|| _history->loadedAtTop()\n\t\t\t|| (!_migrated->isEmpty() && !_migrated->loadedAtBottom()));\n\tconst auto from = loadMigrated ? _migrated : _history;\n\tif (from->loadedAtTop()) {\n\t\treturn;\n\t}\n\n\tconst auto offsetId = from->minMsgId();\n\tconst auto addOffset = 0;\n\tconst auto loadCount = offsetId\n\t\t? kMessagesPerPage\n\t\t: kMessagesPerPageFirst;\n\tconst auto offsetDate = 0;\n\tconst auto maxId = 0;\n\tconst auto minId = 0;\n\tconst auto historyHash = uint64(0);\n\n\tDEBUG_LOG((\"JumpToEnd(%1, %2, %3): Loading up before %4.\"\n\t\t).arg(_history->peer->name()\n\t\t).arg(_history->inboxReadTillId().bare\n\t\t).arg(Logs::b(_history->loadedAtBottom())\n\t\t).arg(offsetId.bare));\n\n\tconst auto history = from;\n\tconst auto type = Data::Histories::RequestType::History;\n\tauto &histories = history->owner().histories();\n\t_preloadRequest = histories.sendRequest(history, type, [=](\n\t\t\tFn<void()> finish) {\n\t\treturn history->session().api().request(MTPmessages_GetHistory(\n\t\t\thistory->peer->input(),\n\t\t\tMTP_int(offsetId),\n\t\t\tMTP_int(offsetDate),\n\t\t\tMTP_int(addOffset),\n\t\t\tMTP_int(loadCount),\n\t\t\tMTP_int(maxId),\n\t\t\tMTP_int(minId),\n\t\t\tMTP_long(historyHash)\n\t\t)).done([=](const MTPmessages_Messages &result) {\n\t\t\tmessagesReceived(history->peer, result, _preloadRequest);\n\t\t\tfinish();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tmessagesFailed(error, _preloadRequest);\n\t\t\tfinish();\n\t\t}).send();\n\t});\n}\n\nvoid HistoryWidget::loadMessagesDown() {\n\tif (!_history || _preloadDownRequest) {\n\t\treturn;\n\t}\n\n\tif (_history->isEmpty() && _migrated && _migrated->isEmpty()) {\n\t\treturn firstLoadMessages();\n\t}\n\n\tconst auto loadMigrated = _migrated\n\t\t&& !(_migrated->isEmpty()\n\t\t\t|| _migrated->loadedAtBottom()\n\t\t\t|| (!_history->isEmpty() && !_history->loadedAtTop()));\n\tconst auto from = loadMigrated ? _migrated : _history;\n\tif (from->loadedAtBottom()) {\n\t\tif (_sponsoredMessagesStateKnown) {\n\t\t\tsession().sponsoredMessages().request(_history, nullptr);\n\t\t}\n\t\treturn;\n\t}\n\n\tconst auto loadCount = kMessagesPerPage;\n\tauto addOffset = -loadCount;\n\tauto offsetId = from->maxMsgId();\n\tif (!offsetId) {\n\t\tif (loadMigrated || !_migrated) {\n\t\t\treturn;\n\t\t}\n\t\t++offsetId;\n\t\t++addOffset;\n\t}\n\tconst auto offsetDate = 0;\n\tconst auto maxId = 0;\n\tconst auto minId = 0;\n\tconst auto historyHash = uint64(0);\n\n\tDEBUG_LOG((\"JumpToEnd(%1, %2, %3): Loading down after %4.\"\n\t\t).arg(_history->peer->name()\n\t\t).arg(_history->inboxReadTillId().bare\n\t\t).arg(Logs::b(_history->loadedAtBottom())\n\t\t).arg(offsetId.bare));\n\n\tconst auto history = from;\n\tconst auto type = Data::Histories::RequestType::History;\n\tauto &histories = history->owner().histories();\n\t_preloadDownRequest = histories.sendRequest(history, type, [=](\n\t\t\tFn<void()> finish) {\n\t\treturn history->session().api().request(MTPmessages_GetHistory(\n\t\t\thistory->peer->input(),\n\t\t\tMTP_int(offsetId + 1),\n\t\t\tMTP_int(offsetDate),\n\t\t\tMTP_int(addOffset),\n\t\t\tMTP_int(loadCount),\n\t\t\tMTP_int(maxId),\n\t\t\tMTP_int(minId),\n\t\t\tMTP_long(historyHash)\n\t\t)).done([=](const MTPmessages_Messages &result) {\n\t\t\tmessagesReceived(history->peer, result, _preloadDownRequest);\n\t\t\tfinish();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tmessagesFailed(error, _preloadDownRequest);\n\t\t\tfinish();\n\t\t}).send();\n\t});\n}\n\nvoid HistoryWidget::delayedShowAt(\n\t\tMsgId showAtMsgId,\n\t\tconst Window::SectionShow &params) {\n\tif (!_history) {\n\t\treturn;\n\t}\n\t_delayedShowAtMsgParams = params;\n\tif (_delayedShowAtRequest && _delayedShowAtMsgId == showAtMsgId) {\n\t\treturn;\n\t}\n\n\tclearAllLoadRequests();\n\t_delayedShowAtMsgId = showAtMsgId;\n\n\tDEBUG_LOG((\"JumpToEnd(%1, %2, %3): Loading delayed around %4.\"\n\t\t).arg(_history->peer->name()\n\t\t).arg(_history->inboxReadTillId().bare\n\t\t).arg(Logs::b(_history->loadedAtBottom())\n\t\t).arg(showAtMsgId.bare));\n\n\tauto from = _history;\n\tauto offsetId = MsgId();\n\tauto offset = 0;\n\tauto loadCount = kMessagesPerPage;\n\tif (_delayedShowAtMsgId == ShowAtUnreadMsgId) {\n\t\tif (const auto around = _migrated ? _migrated->loadAroundId() : 0) {\n\t\t\tfrom = _migrated;\n\t\t\toffset = -loadCount / 2;\n\t\t\toffsetId = around;\n\t\t} else if (const auto around = _history->loadAroundId()) {\n\t\t\toffset = -loadCount / 2;\n\t\t\toffsetId = around;\n\t\t} else {\n\t\t\tloadCount = kMessagesPerPageFirst;\n\t\t}\n\t} else if (_delayedShowAtMsgId == ShowAtTheEndMsgId) {\n\t\tloadCount = kMessagesPerPageFirst;\n\t} else if (_delayedShowAtMsgId > 0) {\n\t\toffset = -loadCount / 2;\n\t\toffsetId = _delayedShowAtMsgId;\n\t} else if (_delayedShowAtMsgId < 0 && _history->peer->isChannel()) {\n\t\tif ((_delayedShowAtMsgId < 0)\n\t\t\t&& (-_delayedShowAtMsgId < ServerMaxMsgId)\n\t\t\t&& _migrated) {\n\t\t\tfrom = _migrated;\n\t\t\toffset = -loadCount / 2;\n\t\t\toffsetId = -_delayedShowAtMsgId;\n\t\t}\n\t}\n\tconst auto offsetDate = 0;\n\tconst auto maxId = 0;\n\tconst auto minId = 0;\n\tconst auto historyHash = uint64(0);\n\n\tconst auto history = from;\n\tconst auto type = Data::Histories::RequestType::History;\n\tauto &histories = history->owner().histories();\n\t_delayedShowAtRequest = histories.sendRequest(history, type, [=](\n\t\t\tFn<void()> finish) {\n\t\treturn history->session().api().request(MTPmessages_GetHistory(\n\t\t\thistory->peer->input(),\n\t\t\tMTP_int(offsetId),\n\t\t\tMTP_int(offsetDate),\n\t\t\tMTP_int(offset),\n\t\t\tMTP_int(loadCount),\n\t\t\tMTP_int(maxId),\n\t\t\tMTP_int(minId),\n\t\t\tMTP_long(historyHash)\n\t\t)).done([=](const MTPmessages_Messages &result) {\n\t\t\tmessagesReceived(history->peer, result, _delayedShowAtRequest);\n\t\t\tfinish();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tmessagesFailed(error, _delayedShowAtRequest);\n\t\t\tfinish();\n\t\t}).send();\n\t});\n}\n\nvoid HistoryWidget::handleScroll() {\n\tif (!_itemsRevealHeight) {\n\t\tpreloadHistoryIfNeeded();\n\t}\n\tvisibleAreaUpdated();\n\tif (!_itemsRevealHeight) {\n\t\tupdatePinnedViewer();\n\t}\n\tconst auto now = crl::now();\n\tif (!_synteticScrollEvent) {\n\t\t_lastUserScrolled = now;\n\t}\n\tconst auto scrollTop = _scroll->scrollTop();\n\tif (scrollTop != _lastScrollTop) {\n\t\tif (!_synteticScrollEvent) {\n\t\t\tcheckLastPinnedClickedIdReset(_lastScrollTop, scrollTop);\n\t\t}\n\t\t_lastScrolled = now;\n\t\t_lastScrollTop = scrollTop;\n\t}\n}\n\nbool HistoryWidget::isItemCompletelyHidden(HistoryItem *item) const {\n\tconst auto view = item ? item->mainView() : nullptr;\n\tif (!view) {\n\t\treturn true;\n\t}\n\tconst auto top = _list ? _list->itemTop(item) : -2;\n\tif (top < 0) {\n\t\treturn true;\n\t}\n\n\tconst auto bottom = top + view->height();\n\tconst auto scrollTop = _scroll->scrollTop();\n\tconst auto scrollBottom = scrollTop + _scroll->height();\n\treturn (top >= scrollBottom || bottom <= scrollTop);\n}\n\nvoid HistoryWidget::visibleAreaUpdated() {\n\tif (_list && !_scroll->isHidden()) {\n\t\tconst auto scrollTop = _scroll->scrollTop();\n\t\tconst auto scrollBottom = scrollTop + _scroll->height();\n\t\t_list->visibleAreaUpdated(scrollTop, scrollBottom);\n\t\tcontroller()->floatPlayerAreaUpdated();\n\t\tsession().data().itemVisibilitiesUpdated();\n\t}\n}\n\nvoid HistoryWidget::preloadHistoryIfNeeded() {\n\tif (_firstLoadRequest\n\t\t|| _delayedShowAtRequest\n\t\t|| _scroll->isHidden()\n\t\t|| !_peer\n\t\t|| !_historyInited) {\n\t\treturn;\n\t}\n\n\t_cornerButtons.updateJumpDownVisibility();\n\t_cornerButtons.updateUnreadThingsVisibility();\n\tif (!_scrollToAnimation.animating()) {\n\t\tpreloadHistoryByScroll();\n\t\tcheckReplyReturns();\n\t}\n\tconst auto hasNonEmpty = _history->findFirstNonEmpty();\n\tconst auto readyForBotStart = hasNonEmpty\n\t\t|| (_history->loadedAtTop() && _history->loadedAtBottom());\n\tif (readyForBotStart && clearMaybeSendStart() && hasNonEmpty) {\n\t\tsendBotStartCommand();\n\t}\n}\n\nvoid HistoryWidget::preloadHistoryByScroll() {\n\tif (_firstLoadRequest\n\t\t|| _delayedShowAtRequest\n\t\t|| _scroll->isHidden()\n\t\t|| !_peer\n\t\t|| !_historyInited) {\n\t\treturn;\n\t}\n\n\tauto scrollTop = _scroll->scrollTop();\n\tauto scrollTopMax = _scroll->scrollTopMax();\n\tauto scrollHeight = _scroll->height();\n\tif (scrollTop + kPreloadHeightsCount * scrollHeight >= scrollTopMax) {\n\t\tloadMessagesDown();\n\t}\n\tif (scrollTop <= kPreloadHeightsCount * scrollHeight) {\n\t\tloadMessages();\n\t}\n\tif (session().supportMode()) {\n\t\tcrl::on_main(this, [=] { checkSupportPreload(); });\n\t}\n}\n\nvoid HistoryWidget::checkSupportPreload(bool force) {\n\tif (!_history\n\t\t|| _firstLoadRequest\n\t\t|| _preloadRequest\n\t\t|| _preloadDownRequest\n\t\t|| (_supportPreloadRequest && !force)\n\t\t|| controller()->activeChatEntryCurrent().key.history() != _history) {\n\t\treturn;\n\t}\n\n\tconst auto setting = session().settings().supportSwitch();\n\tconst auto command = Support::GetSwitchCommand(setting);\n\tconst auto descriptor = !command\n\t\t? Dialogs::RowDescriptor()\n\t\t: (*command == Shortcuts::Command::ChatNext)\n\t\t? controller()->resolveChatNext()\n\t\t: controller()->resolveChatPrevious();\n\tauto history = descriptor.key.history();\n\tif (!history || _supportPreloadHistory == history) {\n\t\treturn;\n\t}\n\tclearSupportPreloadRequest();\n\t_supportPreloadHistory = history;\n\t_supportPreloadRequest = Support::SendPreloadRequest(history, [=] {\n\t\t_supportPreloadRequest = 0;\n\t\t_supportPreloadHistory = nullptr;\n\t\tcrl::on_main(this, [=] { checkSupportPreload(); });\n\t});\n}\n\nvoid HistoryWidget::checkReplyReturns() {\n\tif (_firstLoadRequest\n\t\t|| _scroll->isHidden()\n\t\t|| !_peer\n\t\t|| !_historyInited) {\n\t\treturn;\n\t}\n\tauto scrollTop = _scroll->scrollTop();\n\tauto scrollTopMax = _scroll->scrollTopMax();\n\tauto scrollHeight = _scroll->height();\n\twhile (const auto replyReturn = _cornerButtons.replyReturn()) {\n\t\tauto below = !replyReturn->mainView()\n\t\t\t&& (replyReturn->history() == _history)\n\t\t\t&& !_history->isEmpty()\n\t\t\t&& (replyReturn->id\n\t\t\t\t< _history->blocks.back()->messages.back()->data()->id);\n\t\tif (!below) {\n\t\t\tbelow = !replyReturn->mainView()\n\t\t\t\t&& (replyReturn->history() == _migrated)\n\t\t\t\t&& !_history->isEmpty();\n\t\t}\n\t\tif (!below) {\n\t\t\tbelow = !replyReturn->mainView()\n\t\t\t\t&& _migrated\n\t\t\t\t&& (replyReturn->history() == _migrated)\n\t\t\t\t&& !_migrated->isEmpty()\n\t\t\t\t&& (replyReturn->id\n\t\t\t\t\t< _migrated->blocks.back()->messages.back()->data()->id);\n\t\t}\n\t\tif (!below && replyReturn->mainView()) {\n\t\t\tbelow = (scrollTop >= scrollTopMax)\n\t\t\t\t|| (_list->itemTop(replyReturn)\n\t\t\t\t\t< scrollTop + scrollHeight / 2);\n\t\t}\n\t\tif (below) {\n\t\t\t_cornerButtons.calculateNextReplyReturn();\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nvoid HistoryWidget::cancelInlineBot() {\n\tconst auto &textWithTags = _field->getTextWithTags();\n\tif (textWithTags.text.size() > _inlineBotUsername.size() + 2) {\n\t\tsetFieldText(\n\t\t\t{ '@' + _inlineBotUsername + ' ', TextWithTags::Tags() },\n\t\t\tTextUpdateEvent::SaveDraft,\n\t\t\tUi::InputField::HistoryAction::NewEntry);\n\t} else {\n\t\tclearFieldText(\n\t\t\tTextUpdateEvent::SaveDraft,\n\t\t\tUi::InputField::HistoryAction::NewEntry);\n\t}\n}\n\nvoid HistoryWidget::windowIsVisibleChanged() {\n\tInvokeQueued(this, [=] {\n\t\tpreloadHistoryIfNeeded();\n\t});\n}\n\nTextWithEntities HistoryWidget::prepareTextForEditMsg() const {\n\tconst auto textWithTags = _field->getTextWithAppliedMarkdown();\n\tconst auto prepareFlags = Ui::ItemTextOptions(\n\t\t_history,\n\t\tsession().user()).flags;\n\tauto left = TextWithEntities {\n\t\ttextWithTags.text,\n\t\tTextUtilities::ConvertTextTagsToEntities(textWithTags.tags) };\n\tTextUtilities::PrepareForSending(left, prepareFlags);\n\treturn left;\n}\n\nvoid HistoryWidget::setupSendMenu(\n\t\tnot_null<Ui::RpWidget*> button,\n\t\tFn<void(SendMenu::Action, SendMenu::Details)> action) {\n\tusing namespace SendMenu;\n\tSetupMenuAndShortcuts(\n\t\tbutton,\n\t\tcontroller()->uiShow(),\n\t\t[=] { return sendButtonMenuDetails(); },\n\t\t[=](Action value, Details details) {\n\t\t\tif (value.type == ActionType::CaptionUp\n\t\t\t\t|| value.type == ActionType::CaptionDown\n\t\t\t\t|| value.type == ActionType::SpoilerOn\n\t\t\t\t|| value.type == ActionType::SpoilerOff) {\n\t\t\t\t_mediaEditManager.apply(value);\n\t\t\t} else {\n\t\t\t\taction(value, details);\n\t\t\t}\n\t\t});\n}\n\nvoid HistoryWidget::showAiComposeBox() {\n\tconst auto text = prepareTextForEditMsg();\n\tif (text.text.isEmpty()) {\n\t\treturn;\n\t}\n\tauto send = Fn<void(TextWithEntities &&, Api::SendOptions, Fn<void()>)>(\n\t\tnullptr);\n\tauto setupMenu = Fn<void(\n\t\tnot_null<Ui::RpWidget*>,\n\t\tFn<void(Api::SendOptions)>)>(nullptr);\n\tif (_list && canSendAiComposeDirect()) {\n\t\tsend = crl::guard(_list, [=](\n\t\t\t\tTextWithEntities result,\n\t\t\t\tApi::SendOptions options,\n\t\t\t\tFn<void()> done) {\n\t\t\tsendWithTextOverride(std::move(result), options, std::move(done));\n\t\t});\n\t\tsetupMenu = crl::guard(_list, [=](\n\t\t\t\tnot_null<Ui::RpWidget*> button,\n\t\t\t\tFn<void(Api::SendOptions)> sendCallback) {\n\t\t\tsetupSendMenu(\n\t\t\t\tbutton,\n\t\t\t\tSendMenu::DefaultCallback(\n\t\t\t\t\tcontroller()->uiShow(),\n\t\t\t\t\tsendCallback));\n\t\t});\n\t}\n\tHistoryView::Controls::ShowComposeAiBox(controller()->uiShow(), {\n\t\t.session = &session(),\n\t\t.text = text,\n\t\t.chatStyle = _fieldChatStyle,\n\t\t.apply = crl::guard(this, [=](const TextWithEntities &result) {\n\t\t\tconst auto action = Ui::InputField::HistoryAction::NewEntry;\n\t\t\tsetFieldText({\n\t\t\t\tresult.text,\n\t\t\t\tTextUtilities::ConvertEntitiesToTextTags(result.entities),\n\t\t\t}, TextUpdateEvent::SaveDraft, action);\n\t\t}),\n\t\t.send = std::move(send),\n\t\t.setupMenu = std::move(setupMenu),\n\t});\n}\n\nvoid HistoryWidget::saveEditMessage(Api::SendOptions options) {\n\tExpects(_history != nullptr);\n\n\tif (_saveEditMsgRequestId) {\n\t\treturn;\n\t}\n\n\tconst auto item = session().data().message(_history->peer, _editMsgId);\n\tif (!item) {\n\t\tcancelEdit();\n\t\treturn;\n\t}\n\tconst auto webPageDraft = _preview->draft();\n\tconst auto sending = prepareTextForEditMsg();\n\n\tconst auto hasMediaWithCaption = item\n\t\t&& item->media()\n\t\t&& item->media()->allowsEditCaption();\n\tif (sending.text.isEmpty()\n\t\t&& (webPageDraft.removed\n\t\t\t|| webPageDraft.url.isEmpty()\n\t\t\t|| !webPageDraft.manual)\n\t\t&& !hasMediaWithCaption) {\n\t\tif (item->computeSuggestionActions() == SuggestionActions::None) {\n\t\t\tconst auto suggestModerateActions = false;\n\t\t\tcontroller()->show(\n\t\t\t\tBox<DeleteMessagesBox>(item, suggestModerateActions));\n\t\t}\n\t\treturn;\n\t} else {\n\t\tconst auto maxCaptionSize = !hasMediaWithCaption\n\t\t\t? MaxMessageSize\n\t\t\t: Data::PremiumLimits(&session()).captionLengthCurrent();\n\t\tconst auto remove = _fieldCharsCountManager.count() - maxCaptionSize;\n\t\tif (remove > 0) {\n\t\t\tcontroller()->showToast(\n\t\t\t\ttr::lng_edit_limit_reached(tr::now, lt_count, remove));\n#ifndef _DEBUG\n\t\t\treturn;\n#else\n\t\t\tif (!base::IsCtrlPressed()) {\n\t\t\t\treturn;\n\t\t\t}\n#endif\n\t\t}\n\t}\n\n\tconst auto weak = base::make_weak(this);\n\tconst auto history = _history;\n\n\tconst auto done = [=](mtpRequestId requestId) {\n\t\tcrl::guard(weak, [=] {\n\t\t\tif (requestId == _saveEditMsgRequestId) {\n\t\t\t\t_saveEditMsgRequestId = 0;\n\t\t\t\tcancelEdit();\n\t\t\t}\n\t\t})();\n\t\tif (const auto editDraft = history->localEditDraft({}, {})) {\n\t\t\tif (editDraft->saveRequestId == requestId) {\n\t\t\t\thistory->clearLocalEditDraft(MsgId(), PeerId());\n\t\t\t\thistory->session().local().writeDrafts(history);\n\t\t\t}\n\t\t}\n\t};\n\n\tconst auto fail = [=](const QString &error, mtpRequestId requestId) {\n\t\tif (const auto editDraft = history->localEditDraft({}, {})) {\n\t\t\tif (editDraft->saveRequestId == requestId) {\n\t\t\t\teditDraft->saveRequestId = 0;\n\t\t\t}\n\t\t}\n\t\tcrl::guard(weak, [=] {\n\t\t\tif (requestId == _saveEditMsgRequestId) {\n\t\t\t\t_saveEditMsgRequestId = 0;\n\t\t\t}\n\t\t\tif (ranges::contains(Api::kDefaultEditMessagesErrors, error)) {\n\t\t\t\tcontroller()->showToast(tr::lng_edit_error(tr::now));\n\t\t\t} else if (error == u\"MESSAGE_NOT_MODIFIED\"_q) {\n\t\t\t\tcancelEdit();\n\t\t\t} else if (error == u\"MESSAGE_EMPTY\"_q) {\n\t\t\t\t_field->selectAll();\n\t\t\t\tsetInnerFocus();\n\t\t\t} else {\n\t\t\t\tcontroller()->showToast(tr::lng_edit_error(tr::now));\n\t\t\t}\n\t\t\tupdate();\n\t\t})();\n\t};\n\n\toptions.invertCaption = _mediaEditManager.invertCaption();\n\toptions.suggest = suggestOptions(true);\n\n\tif (item->computeSuggestionActions()\n\t\t== SuggestionActions::AcceptAndDecline) {\n\t\tconst auto withPaymentApproved = [=](int approved) {\n\t\t\tauto copy = options;\n\t\t\tcopy.starsApproved = approved;\n\t\t\tsaveEditMessage(copy);\n\t\t};\n\t\tconst auto checked = checkSendPayment(\n\t\t\t1 + int(_forwardPanel->items().size()),\n\t\t\toptions,\n\t\t\twithPaymentApproved);\n\t\tif (!checked) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\t_saveEditMsgRequestId = Api::EditTextMessage(\n\t\titem,\n\t\tsending,\n\t\twebPageDraft,\n\t\toptions,\n\t\tdone,\n\t\tfail,\n\t\t_mediaEditManager.spoilered());\n}\n\nvoid HistoryWidget::hideChildWidgets() {\n\tif (Ui::InFocusChain(this)) {\n\t\t// Removing focus from list clears selected and updates top bar.\n\t\tsetFocus();\n\t}\n\tif (_tabbedPanel) {\n\t\t_tabbedPanel->hideFast();\n\t}\n\tif (_sponsoredMessageBar) {\n\t\t_sponsoredMessageBar->toggle(false, anim::type::instant);\n\t}\n\t_topBars->hide();\n\tif (_subsectionTabs) {\n\t\t_subsectionTabs->hide();\n\t}\n\tif (_voiceRecordBar) {\n\t\t_voiceRecordBar->hideFast();\n\t}\n\tif (_composeSearch) {\n\t\t_composeSearch->hideAnimated();\n\t}\n\tif (_chooseTheme) {\n\t\t_chooseTheme->hide();\n\t}\n\tif (_paysStatus) {\n\t\t_paysStatus->hide();\n\t}\n\tif (_contactStatus) {\n\t\t_contactStatus->hide();\n\t}\n\tif (_businessBotStatus) {\n\t\t_businessBotStatus->hide();\n\t}\n\thideChildren();\n}\n\nvoid HistoryWidget::hideSelectorControlsAnimated() {\n\tif (_autocomplete) {\n\t\t_autocomplete->hideAnimated();\n\t}\n\tif (_supportAutocomplete) {\n\t\t_supportAutocomplete->hide();\n\t}\n\tif (_tabbedPanel) {\n\t\t_tabbedPanel->hideAnimated();\n\t}\n\tif (_inlineResults) {\n\t\t_inlineResults->hideAnimated();\n\t}\n}\n\nApi::SendAction HistoryWidget::prepareSendAction(\n\t\tApi::SendOptions options) {\n\tauto result = Api::SendAction(_history, options);\n\tresult.replyTo = replyTo();\n\n\tif (const auto forum = _history->asForum()) {\n\t\tif (forum->bot() && Data::IsBotUserCreatesTopics(_history->peer)) {\n\t\t\tconst auto readyRootId = [&]() -> MsgId {\n\t\t\t\tif (const auto id = result.replyTo.messageId) {\n\t\t\t\t\tif (const auto item = session().data().message(id)) {\n\t\t\t\t\t\treturn item->topicRootId();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn {};\n\t\t\t}();\n\t\t\tif (readyRootId) {\n\t\t\t\tresult.replyTo.topicRootId = readyRootId;\n\t\t\t} else {\n\t\t\t\tif (!_creatingBotTopic) {\n\t\t\t\t\t_creatingBotTopic = forum->reserveNewBotTopic();\n\t\t\t\t\tauto draft = _history->forwardDraft(MsgId(0), PeerId());\n\t\t\t\t\tif (!draft.ids.empty()) {\n\t\t\t\t\t\t_history->setForwardDraft(MsgId(0), PeerId(), {});\n\t\t\t\t\t\t_history->setForwardDraft(\n\t\t\t\t\t\t\t_creatingBotTopic->rootId(),\n\t\t\t\t\t\t\tPeerId(),\n\t\t\t\t\t\t\tstd::move(draft));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tresult = Api::SendAction(_creatingBotTopic, options);\n\t\t\t\tresult.replyTo.topicRootId = _creatingBotTopic->rootId();\n\t\t\t}\n\t\t}\n\t}\n\n\tresult.options.suggest = suggestOptions();\n\tresult.options.sendAs = _sendAs\n\t\t? _history->session().sendAsPeers().resolveChosen(\n\t\t\t_history->peer).get()\n\t\t: nullptr;\n\treturn result;\n}\n\nvoid HistoryWidget::sendVoice(const VoiceToSend &data) {\n\tif (!canWriteMessage() || data.bytes.isEmpty() || !_history) {\n\t\treturn;\n\t}\n\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = data;\n\t\tcopy.options.starsApproved = approved;\n\t\tsendVoice(copy);\n\t};\n\tauto action = prepareSendAction(data.options);\n\tconst auto checked = checkSendPayment(\n\t\t1 + int(_forwardPanel->items().size()),\n\t\taction.options,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn;\n\t}\n\n\tsession().api().sendVoiceMessage(\n\t\tdata.bytes,\n\t\tdata.waveform,\n\t\tdata.duration,\n\t\tdata.video,\n\t\taction);\n\t_voiceRecordBar->clearListenState();\n}\n\nvoid HistoryWidget::send(Api::SendOptions options) {\n\tif (!_history) {\n\t\treturn;\n\t} else if (_editMsgId) {\n\t\tsaveEditMessage({});\n\t\treturn;\n\t} else if (!options.scheduled && showSlowmodeError()) {\n\t\treturn;\n\t} else if (_voiceRecordBar->isListenState()) {\n\t\t_voiceRecordBar->requestToSendWithOptions(options);\n\t\treturn;\n\t}\n\n\tsendTextWithTags(\n\t\t_field->getTextWithAppliedMarkdown(),\n\t\ttrue,\n\t\toptions,\n\t\tnullptr);\n}\n\nvoid HistoryWidget::sendTextWithTags(\n\t\tTextWithTags textWithTags,\n\t\tbool useWebPageDraft,\n\t\tApi::SendOptions options,\n\t\tFn<void()> done) {\n\tif (!options.scheduled) {\n\t\t_cornerButtons.clearReplyReturns();\n\t}\n\n\tauto message = Api::MessageToSend(prepareSendAction(options));\n\tmessage.textWithTags = textWithTags;\n\tif (useWebPageDraft && _preview) {\n\t\tmessage.webPage = _preview->draft();\n\t}\n\n\tconst auto ignoreSlowmodeCountdown = (options.scheduled != 0);\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = options;\n\t\tcopy.starsApproved = approved;\n\t\tsendTextWithTags(textWithTags, useWebPageDraft, copy, done);\n\t};\n\tif (showSendMessageError(\n\t\t\tmessage.textWithTags,\n\t\t\tignoreSlowmodeCountdown,\n\t\t\twithPaymentApproved,\n\t\t\tmessage.action.options)) {\n\t\treturn;\n\t}\n\n\tconst auto nextLocalMessageId = session().data().nextLocalMessageId();\n\tconst auto hasText = !message.textWithTags.text.trimmed().isEmpty();\n\n\tif (hasText\n\t\t&& message.webPage.url.isEmpty()\n\t\t&& (_field->document()->size().height() <= _field->height())) {\n\t\tcontroller()->sendingAnimation().appendSending({\n\t\t\t.type = Ui::MessageSendingAnimationFrom::Type::Text,\n\t\t\t.localId = nextLocalMessageId,\n\t\t\t.globalStartGeometry = _field->mapToGlobal(Rect(_field->size())),\n\t\t});\n\t}\n\n\t// Just a flag not to drop reply info if we're not sending anything.\n\t_justMarkingAsRead = !hasText\n\t\t&& message.webPage.url.isEmpty();\n\tsession().api().sendMessage(std::move(message), nextLocalMessageId);\n\t_justMarkingAsRead = false;\n\n\tclearFieldText();\n\tif (_preview) {\n\t\t_preview->apply({ .removed = true });\n\t}\n\tsaveDraftWithTextNow();\n\n\thideSelectorControlsAnimated();\n\n\tsetInnerFocus();\n\n\tif (!_keyboard->hasMarkup() && _keyboard->forceReply() && !_kbReplyTo) {\n\t\ttoggleKeyboard();\n\t}\n\tsession().changes().historyUpdated(\n\t\t_history,\n\t\t(options.scheduled\n\t\t\t? Data::HistoryUpdate::Flag::ScheduledSent\n\t\t\t: Data::HistoryUpdate::Flag::MessageSent));\n\tif (done) {\n\t\tdone();\n\t}\n}\n\nvoid HistoryWidget::sendWithTextOverride(\n\t\tTextWithEntities text,\n\t\tApi::SendOptions options,\n\t\tFn<void()> done) {\n\tif (!canSendAiComposeDirect()) {\n\t\treturn;\n\t}\n\tconst auto useWebPageDraft = (text.text == prepareTextForEditMsg().text);\n\tsendTextWithTags({\n\t\ttext.text,\n\t\tTextUtilities::ConvertEntitiesToTextTags(text.entities),\n\t}, useWebPageDraft, options, std::move(done));\n}\n\nvoid HistoryWidget::sendWithModifiers(Qt::KeyboardModifiers modifiers) {\n\tsend({ .handleSupportSwitch = Support::HandleSwitch(modifiers) });\n}\n\nvoid HistoryWidget::sendScheduled(Api::SendOptions initialOptions) {\n\tif (!_list) {\n\t\treturn;\n\t}\n\tconst auto ignoreSlowmodeCountdown = true;\n\tif (showSendMessageError(\n\t\t\t_field->getTextWithAppliedMarkdown(),\n\t\t\tignoreSlowmodeCountdown)) {\n\t\treturn;\n\t}\n\tcontroller()->show(\n\t\tHistoryView::PrepareScheduleBox(\n\t\t\t_list,\n\t\t\tcontroller()->uiShow(),\n\t\t\tsendButtonDefaultDetails(),\n\t\t\t[=](Api::SendOptions options) { send(options); },\n\t\t\tinitialOptions));\n}\n\nSendMenu::Details HistoryWidget::sendMenuDetails() const {\n\tconst auto type = !_peer\n\t\t? SendMenu::Type::Disabled\n\t\t: _peer->starsPerMessageChecked()\n\t\t? SendMenu::Type::SilentOnly\n\t\t: _peer->isSelf()\n\t\t? SendMenu::Type::Reminder\n\t\t: HistoryView::CanScheduleUntilOnline(_peer)\n\t\t? SendMenu::Type::ScheduledToUser\n\t\t: SendMenu::Type::Scheduled;\n\tconst auto effectAllowed = _peer && _peer->isUser();\n\treturn { .type = type, .effectAllowed = effectAllowed };\n}\n\nSendMenu::Details HistoryWidget::saveMenuDetails() const {\n\treturn (_editMsgId && _replyEditMsg)\n\t\t? _mediaEditManager.sendMenuDetails(HasSendText(_field))\n\t\t: SendMenu::Details();\n}\n\nauto HistoryWidget::computeSendButtonType() const {\n\tusing Type = Ui::SendButton::Type;\n\n\tif (_editMsgId) {\n\t\treturn Type::Save;\n\t} else if (_isInlineBot) {\n\t\treturn Type::Cancel;\n\t} else if (showRecordButton()) {\n\t\tconst auto both = Webrtc::RecordAvailability::VideoAndAudio;\n\t\tconst auto video = Core::App().settings().recordVideoMessages();\n\t\treturn (video && _recordAvailability == both)\n\t\t\t? Type::Round\n\t\t\t: Type::Record;\n\t}\n\treturn Type::Send;\n}\n\nbool HistoryWidget::canSendAiComposeDirect() const {\n\tusing Type = Ui::SendButton::Type;\n\treturn _history\n\t\t&& _peer\n\t\t&& (computeSendButtonType() == Type::Send)\n\t\t&& !_peer->slowmodeSecondsLeft()\n\t\t&& !(_peer->slowmodeApplied() && _history->latestSendingMessage())\n\t\t&& !_peer->starsPerMessageChecked();\n}\n\nSendMenu::Details HistoryWidget::sendButtonMenuDetails() const {\n\tusing Type = Ui::SendButton::Type;\n\tconst auto type = computeSendButtonType();\n\tif (type == Type::Save) {\n\t\treturn saveMenuDetails();\n\t} else if (type != Type::Send) {\n\t\treturn {};\n\t}\n\treturn sendButtonDefaultDetails();\n}\n\nSendMenu::Details HistoryWidget::sendButtonDefaultDetails() const {\n\tauto result = sendMenuDetails();\n\tif (!HasSendText(_field) && !_previewDrawPreview) {\n\t\tresult.effectAllowed = false;\n\t}\n\treturn result;\n}\n\nvoid HistoryWidget::unblockUser() {\n\tif (const auto user = _peer ? _peer->asUser() : nullptr) {\n\t\tconst auto show = controller()->uiShow();\n\t\tWindow::PeerMenuUnblockUserWithBotRestart(show, user);\n\t} else {\n\t\tupdateControlsVisibility();\n\t}\n}\n\nvoid HistoryWidget::sendBotStartCommand() {\n\tif (!_peer\n\t\t|| !_peer->isUser()\n\t\t|| !_peer->asUser()->isBot()\n\t\t|| !_canSendMessages) {\n\t\tupdateControlsVisibility();\n\t\treturn;\n\t}\n\tsession().api().sendBotStart(controller()->uiShow(), _peer->asUser());\n\tupdateControlsVisibility();\n\tupdateControlsGeometry();\n}\n\nvoid HistoryWidget::joinChannel() {\n\tif (!_peer || !_peer->isChannel() || !isJoinChannel()) {\n\t\tupdateControlsVisibility();\n\t\treturn;\n\t}\n\tsession().api().joinChannel(_peer->asChannel());\n}\n\nvoid HistoryWidget::toggleMuteUnmute() {\n\tconst auto wasMuted = _history->muted();\n\tconst auto muteForSeconds = Data::MuteValue{\n\t\t.unmute = wasMuted,\n\t\t.forever = !wasMuted,\n\t};\n\tsession().data().notifySettings().update(_peer, muteForSeconds);\n}\n\nvoid HistoryWidget::reportSelectedMessages() {\n\tif (!_list || !_chooseForReport || !_list->getSelectionState().count) {\n\t\treturn;\n\t}\n\tconst auto ids = _list->getSelectedItems();\n\tconst auto done = _chooseForReport->callback;\n\tclearSelected();\n\tcontroller()->clearChooseReportMessages();\n\tif (done) {\n\t\tdone(ranges::views::all(\n\t\t\tids\n\t\t) | ranges::views::transform(&FullMsgId::msg) | ranges::to_vector);\n\t}\n}\n\nHistory *HistoryWidget::history() const {\n\treturn _history;\n}\n\nPeerData *HistoryWidget::peer() const {\n\treturn _peer;\n}\n\n// Sometimes _showAtMsgId is set directly.\nvoid HistoryWidget::setMsgId(\n\t\tMsgId showAtMsgId,\n\t\tconst Window::SectionShow &params) {\n\t_showAtMsgParams = params;\n\tif (_showAtMsgId != showAtMsgId) {\n\t\t_showAtMsgId = showAtMsgId;\n\t\tif (_history) {\n\t\t\tcontroller()->setActiveChatEntry({\n\t\t\t\t_history,\n\t\t\t\tFullMsgId(_history->peer->id, _showAtMsgId) });\n\t\t}\n\t}\n}\n\nMsgId HistoryWidget::msgId() const {\n\treturn _showAtMsgId;\n}\n\nvoid HistoryWidget::showAnimated(\n\t\tWindow::SlideDirection direction,\n\t\tconst Window::SectionSlideParams &params) {\n\tvalidateSubsectionTabs();\n\n\t_showAnimation = nullptr;\n\n\t// If we show pinned bar here, we don't want it to change the\n\t// calculated and prepared scrollTop of the messages history.\n\t_preserveScrollTop = true;\n\tshow();\n\t_topBar->finishAnimating();\n\t_cornerButtons.finishAnimations();\n\tif (_pinnedBar) {\n\t\t_pinnedBar->finishAnimating();\n\t}\n\tif (_translateBar) {\n\t\t_translateBar->finishAnimating();\n\t}\n\tif (_groupCallBar) {\n\t\t_groupCallBar->finishAnimating();\n\t}\n\tif (_requestsBar) {\n\t\t_requestsBar->finishAnimating();\n\t}\n\t_topShadow->setVisible(params.withTopBarShadow ? false : true);\n\t_preserveScrollTop = false;\n\t_stickerToast = nullptr;\n\n\tauto newContentCache = Ui::GrabWidget(this);\n\n\thideChildWidgets();\n\tif (params.withTopBarShadow) _topShadow->show();\n\n\tif (_history) {\n\t\t_topBar->show();\n\t\t_topBar->setAnimatingMode(true);\n\t}\n\n\t_showAnimation = std::make_unique<Window::SlideAnimation>();\n\t_showAnimation->setDirection(direction);\n\t_showAnimation->setRepaintCallback([=] { update(); });\n\t_showAnimation->setFinishedCallback([=] { showFinished(); });\n\t_showAnimation->setPixmaps(params.oldContentCache, newContentCache);\n\t_showAnimation->start();\n\n\tactivate();\n}\n\nvoid HistoryWidget::showFast() {\n\tvalidateSubsectionTabs();\n\tshow();\n}\n\nvoid HistoryWidget::showFinished() {\n\t_cornerButtons.finishAnimations();\n\tif (_pinnedBar) {\n\t\t_pinnedBar->finishAnimating();\n\t}\n\tif (_translateBar) {\n\t\t_translateBar->finishAnimating();\n\t}\n\tif (_groupCallBar) {\n\t\t_groupCallBar->finishAnimating();\n\t}\n\tif (_requestsBar) {\n\t\t_requestsBar->finishAnimating();\n\t}\n\t_showAnimation = nullptr;\n\tdoneShow();\n\tsynteticScrollToY(_scroll->scrollTop());\n}\n\nvoid HistoryWidget::doneShow() {\n\t_topBar->setAnimatingMode(false);\n\tupdateCanSendMessage();\n\tupdateBotKeyboard();\n\tupdateControlsVisibility();\n\tif (!_historyInited) {\n\t\tupdateHistoryGeometry(true);\n\t} else {\n\t\thandlePendingHistoryUpdate();\n\t}\n\t// If we show pinned bar here, we don't want it to change the\n\t// calculated and prepared scrollTop of the messages history.\n\t_preserveScrollTop = true;\n\tpreloadHistoryIfNeeded();\n\tupdatePinnedViewer();\n\tif (_pinnedBar) {\n\t\t_pinnedBar->finishAnimating();\n\t}\n\tcheckSponsoredMessageBar();\n\tif (_sponsoredMessageBar) {\n\t\t_sponsoredMessageBar->finishAnimating();\n\t}\n\tif (_translateBar) {\n\t\t_translateBar->finishAnimating();\n\t}\n\tif (_groupCallBar) {\n\t\t_groupCallBar->finishAnimating();\n\t}\n\tif (_requestsBar) {\n\t\t_requestsBar->finishAnimating();\n\t}\n\tcheckActivation();\n\tcontroller()->widget()->setInnerFocus();\n\t_preserveScrollTop = false;\n\tcheckSuggestToGigagroup();\n\n\tif (_history) {\n\t\t_history->saveMeAsActiveSubsectionThread();\n\t}\n}\n\nvoid HistoryWidget::cornerButtonsShowAtPosition(\n\t\tData::MessagePosition position) {\n\tif (!_peer) {\n\t\treturn;\n\t} else if (position == Data::UnreadMessagePosition) {\n\t\tDEBUG_LOG((\"JumpToEnd(%1, %2, %3): Show at unread requested.\"\n\t\t\t).arg(_history->peer->name()\n\t\t\t).arg(_history->inboxReadTillId().bare\n\t\t\t).arg(Logs::b(_history->loadedAtBottom())));\n\t\tshowHistory(_peer->id, ShowAtUnreadMsgId);\n\t} else if (_peer && position.fullId.peer == _peer->id) {\n\t\tshowHistory(_peer->id, position.fullId.msg);\n\t} else if (_migrated && position.fullId.peer == _migrated->peer->id) {\n\t\tshowHistory(_peer->id, -position.fullId.msg);\n\t}\n}\n\nData::Thread *HistoryWidget::cornerButtonsThread() {\n\treturn _history;\n}\n\nFullMsgId HistoryWidget::cornerButtonsCurrentId() {\n\treturn (_migrated && _showAtMsgId < 0)\n\t\t? FullMsgId(_migrated->peer->id, -_showAtMsgId)\n\t\t: (_history && _showAtMsgId > 0)\n\t\t? FullMsgId(_history->peer->id, _showAtMsgId)\n\t\t: FullMsgId();\n}\n\nbool HistoryWidget::checkSendPayment(\n\t\tint messagesCount,\n\t\tApi::SendOptions options,\n\t\tFn<void(int)> withPaymentApproved) {\n\treturn _peer\n\t\t&& _sendPayment.check(\n\t\t\tcontroller(),\n\t\t\t_peer,\n\t\t\toptions,\n\t\t\tmessagesCount,\n\t\t\tstd::move(withPaymentApproved));\n}\n\nvoid HistoryWidget::checkSuggestToGigagroup() {\n\tconst auto group = _peer ? _peer->asMegagroup() : nullptr;\n\tif (!group || !group->owner().suggestToGigagroup(group)) {\n\t\treturn;\n\t}\n\tInvokeQueued(_list, [=] {\n\t\tif (!controller()->isLayerShown()) {\n\t\t\tgroup->owner().setSuggestToGigagroup(group, false);\n\t\t\tgroup->session().api().request(MTPhelp_DismissSuggestion(\n\t\t\t\tgroup->input(),\n\t\t\t\tMTP_string(\"convert_to_gigagroup\")\n\t\t\t)).send();\n\t\t\tcontroller()->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\tbox->setTitle(tr::lng_gigagroup_suggest_title());\n\t\t\t\tbox->addRow(\n\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\tbox,\n\t\t\t\t\t\ttr::lng_gigagroup_suggest_text(\n\t\t\t\t\t\t) | rpl::map(tr::rich),\n\t\t\t\t\t\tst::infoAboutGigagroup));\n\t\t\t\tbox->addButton(\n\t\t\t\t\ttr::lng_gigagroup_suggest_more(),\n\t\t\t\t\tAboutGigagroupCallback(group, controller()));\n\t\t\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t\t\t}));\n\t\t}\n\t});\n}\n\nvoid HistoryWidget::finishAnimating() {\n\tif (!_showAnimation) {\n\t\treturn;\n\t}\n\t_showAnimation = nullptr;\n\t_topShadow->setVisible(_peer != nullptr);\n\t_topBar->setVisible(_peer != nullptr);\n\t_cornerButtons.finishAnimations();\n}\n\nvoid HistoryWidget::chooseAttach(\n\t\tstd::optional<bool> overrideSendImagesAsPhotos) {\n\tif (_editMsgId) {\n\t\tcontroller()->showToast(tr::lng_edit_caption_attach(tr::now));\n\t\treturn;\n\t}\n\n\tif (!_peer || !_canSendMessages) {\n\t\treturn;\n\t} else if (const auto error = Data::AnyFileRestrictionError(_peer)) {\n\t\tData::ShowSendErrorToast(controller(), _peer, error);\n\t\treturn;\n\t} else if (showSlowmodeError()) {\n\t\treturn;\n\t}\n\n\tconst auto filter = (overrideSendImagesAsPhotos == true)\n\t\t? FileDialog::PhotoVideoFilesFilter()\n\t\t: FileDialog::AllOrImagesFilter();\n\n\tconst auto callbackOnResult = crl::guard(this, [=](\n\t\t\tFileDialog::OpenResult &&result) {\n\t\tif (result.paths.isEmpty() && result.remoteContent.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!result.remoteContent.isEmpty()) {\n\t\t\tauto read = Images::Read({\n\t\t\t\t.content = result.remoteContent,\n\t\t\t});\n\t\t\tif (!read.image.isNull() && !read.animated) {\n\t\t\t\tconfirmSendingFiles(\n\t\t\t\t\tstd::move(read.image),\n\t\t\t\t\tstd::move(result.remoteContent),\n\t\t\t\t\toverrideSendImagesAsPhotos);\n\t\t\t} else {\n\t\t\t\tuploadFile(result.remoteContent, SendMediaType::File);\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto premium = controller()->session().user()->isPremium();\n\t\t\tauto list = Storage::PrepareMediaList(\n\t\t\t\tresult.paths,\n\t\t\t\tst::sendMediaPreviewSize,\n\t\t\t\tpremium);\n\t\t\tlist.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;\n\t\t\tconfirmSendingFiles(std::move(list));\n\t\t}\n\t});\n\tFileDialog::GetOpenPaths(\n\t\tthis,\n\t\ttr::lng_choose_files(tr::now),\n\t\tfilter,\n\t\tcallbackOnResult,\n\t\tnullptr);\n}\n\nvoid HistoryWidget::sendButtonClicked() {\n\tconst auto type = _send->type();\n\tif (type == Ui::SendButton::Type::Cancel) {\n\t\tcancelInlineBot();\n\t} else if (type != Ui::SendButton::Type::Record\n\t\t&& type != Ui::SendButton::Type::Round) {\n\t\tsend({});\n\t}\n}\n\nvoid HistoryWidget::leaveEventHook(QEvent *e) {\n\tif (hasMouseTracking()) {\n\t\tmouseMoveEvent(nullptr);\n\t}\n}\n\nvoid HistoryWidget::mouseMoveEvent(QMouseEvent *e) {\n\tconst auto pos = e ? e->pos() : mapFromGlobal(QCursor::pos());\n\tupdateOverStates(pos);\n}\n\nvoid HistoryWidget::updateOverStates(QPoint pos) {\n\tconst auto isReadyToForward = readyToForward();\n\tconst auto detailsRect = QRect(\n\t\t0,\n\t\t_field->y() - st::historySendPadding - st::historyReplyHeight,\n\t\twidth() - _fieldBarCancel->width(),\n\t\tst::historyReplyHeight);\n\tconst auto hasWebPage = !!_previewDrawPreview;\n\tconst auto inDetails = detailsRect.contains(pos)\n\t\t&& (_editMsgId\n\t\t\t|| replyTo()\n\t\t\t|| isReadyToForward\n\t\t\t|| hasWebPage\n\t\t\t|| _suggestOptions);\n\tconst auto inPhotoEdit = inDetails\n\t\t&& _photoEditMedia\n\t\t&& QRect(\n\t\t\tdetailsRect.x() + st::historyReplySkip,\n\t\t\t(detailsRect.y()\n\t\t\t\t+ (detailsRect.height() - st::historyReplyPreview) / 2),\n\t\t\tst::historyReplyPreview,\n\t\t\tst::historyReplyPreview).contains(pos);\n\tconst auto inClickable = inDetails;\n\tif (_inPhotoEdit != inPhotoEdit) {\n\t\t_inPhotoEdit = inPhotoEdit;\n\t\tif (_photoEditMedia) {\n\t\t\t_inPhotoEditOver.start(\n\t\t\t\t[=] { updateField(); },\n\t\t\t\t_inPhotoEdit ? 0. : 1.,\n\t\t\t\t_inPhotoEdit ? 1. : 0.,\n\t\t\t\tst::defaultMessageBar.duration);\n\t\t} else {\n\t\t\t_inPhotoEditOver.stop();\n\t\t}\n\t}\n\t_inDetails = inDetails && !inPhotoEdit;\n\tif (inClickable != _inClickable) {\n\t\t_inClickable = inClickable;\n\t\tsetCursor(_inClickable ? style::cur_pointer : style::cur_default);\n\t}\n}\n\nvoid HistoryWidget::leaveToChildEvent(QEvent *e, QWidget *child) {\n// e -- from enterEvent() of child RpWidget\n\tif (hasMouseTracking()) {\n\t\tupdateOverStates(mapFromGlobal(QCursor::pos()));\n\t}\n}\n\nvoid HistoryWidget::mouseReleaseEvent(QMouseEvent *e) {\n}\n\nvoid HistoryWidget::sendBotCommand(const Bot::SendCommandRequest &request) {\n\tsendBotCommand(request, {});\n}\n\nvoid HistoryWidget::sendBotCommand(\n\t\tconst Bot::SendCommandRequest &request,\n\t\tApi::SendOptions options) {\n\t// replyTo != 0 from ReplyKeyboardMarkup, == 0 from command links\n\tif (_peer != request.peer.get()) {\n\t\treturn;\n\t} else if (showSlowmodeError()) {\n\t\treturn;\n\t}\n\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = options;\n\t\tcopy.starsApproved = approved;\n\t\tsendBotCommand(request, copy);\n\t};\n\n\tconst auto action = prepareSendAction(options);\n\tconst auto checked = checkSendPayment(\n\t\t1,\n\t\taction.options,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn;\n\t}\n\n\tconst auto forMsgId = _keyboard->forMsgId();\n\tconst auto lastKeyboardUsed = (forMsgId == request.replyTo.messageId)\n\t\t&& (forMsgId == FullMsgId(_peer->id, _history->lastKeyboardId));\n\n\t// 'bot' may be nullptr in case of sending from FieldAutocomplete.\n\tconst auto toSend = (request.replyTo/* || !bot*/)\n\t\t? request.command\n\t\t: Bot::WrapCommandInChat(_peer, request.command, request.context);\n\n\tauto message = Api::MessageToSend(action);\n\tmessage.textWithTags = { toSend, TextWithTags::Tags() };\n\tmessage.action.replyTo = request.replyTo\n\t\t? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/)\n\t\t\t? request.replyTo\n\t\t\t: replyTo())\n\t\t: FullReplyTo();\n\tsession().api().sendMessage(std::move(message));\n\tif (request.replyTo) {\n\t\tif (_replyTo == request.replyTo) {\n\t\t\tcancelReply();\n\t\t\tsaveCloudDraft();\n\t\t}\n\t\tif (_keyboard->singleUse()\n\t\t\t&& _keyboard->hasMarkup()\n\t\t\t&& lastKeyboardUsed) {\n\t\t\tif (_kbShown) {\n\t\t\t\ttoggleKeyboard(false);\n\t\t\t}\n\t\t\t_history->lastKeyboardUsed = true;\n\t\t}\n\t}\n\n\tsetInnerFocus();\n}\n\nvoid HistoryWidget::hideSingleUseKeyboard(FullMsgId replyToId) {\n\tif (!_peer || _peer->id != replyToId.peer) {\n\t\treturn;\n\t}\n\n\tconst auto lastKeyboardUsed = (_keyboard->forMsgId() == replyToId)\n\t\t&& (_keyboard->forMsgId()\n\t\t\t== FullMsgId(_peer->id, _history->lastKeyboardId));\n\tif (replyToId) {\n\t\tif (_replyTo.messageId == replyToId) {\n\t\t\tcancelReply();\n\t\t\tsaveCloudDraft();\n\t\t}\n\t\tif (_keyboard->singleUse()\n\t\t\t&& _keyboard->hasMarkup()\n\t\t\t&& lastKeyboardUsed) {\n\t\t\tif (_kbShown) {\n\t\t\t\ttoggleKeyboard(false);\n\t\t\t}\n\t\t\t_history->lastKeyboardUsed = true;\n\t\t}\n\t}\n}\n\nbool HistoryWidget::insertBotCommand(const QString &cmd) {\n\tif (!_canSendTexts) {\n\t\treturn false;\n\t}\n\n\tconst auto insertingInlineBot = !cmd.isEmpty() && (cmd.at(0) == '@');\n\tauto toInsert = cmd;\n\tif (!toInsert.isEmpty() && !insertingInlineBot) {\n\t\tauto bot = (PeerData*)(_peer->asUser());\n\t\tif (!bot) {\n\t\t\tif (const auto link = HistoryView::Element::HoveredLink()) {\n\t\t\t\tbot = link->data()->fromOriginal().get();\n\t\t\t}\n\t\t}\n\t\tif (bot && (!bot->isUser() || !bot->asUser()->isBot())) {\n\t\t\tbot = nullptr;\n\t\t}\n\t\tconst auto username = bot ? bot->asUser()->username() : QString();\n\t\tconst auto botStatus = _peer->isChat()\n\t\t\t? _peer->asChat()->botStatus\n\t\t\t: _peer->isMegagroup()\n\t\t\t? _peer->asChannel()->mgInfo->botStatus\n\t\t\t: Data::BotStatus::NoBots;\n\t\tif ((toInsert.indexOf('@') < 0)\n\t\t\t&& !username.isEmpty()\n\t\t\t&& (botStatus != Data::BotStatus::NoBots)) {\n\t\t\ttoInsert += '@' + username;\n\t\t}\n\t}\n\ttoInsert += ' ';\n\n\tif (!insertingInlineBot) {\n\t\tauto &textWithTags = _field->getTextWithTags();\n\t\tauto textWithTagsToSet = TextWithTags();\n\t\tconst auto m = QRegularExpression(\n\t\t\tu\"^/[A-Za-z_0-9]{0,64}(@[A-Za-z_0-9]{0,32})?(\\\\s|$)\"_q).match(\n\t\t\t\ttextWithTags.text);\n\t\ttextWithTagsToSet = m.hasMatch()\n\t\t\t? _field->getTextWithTagsPart(m.capturedLength())\n\t\t\t: textWithTags;\n\t\ttextWithTagsToSet.text = toInsert + textWithTagsToSet.text;\n\t\tfor (auto &tag : textWithTagsToSet.tags) {\n\t\t\ttag.offset += toInsert.size();\n\t\t}\n\t\t_field->setTextWithTags(textWithTagsToSet);\n\n\t\tauto cur = QTextCursor(_field->textCursor());\n\t\tcur.movePosition(QTextCursor::End);\n\t\t_field->setTextCursor(cur);\n\t} else {\n\t\tsetFieldText(\n\t\t\t{ toInsert, TextWithTags::Tags() },\n\t\t\tTextUpdateEvent::SaveDraft,\n\t\t\tUi::InputField::HistoryAction::NewEntry);\n\t\tsetInnerFocus();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid HistoryWidget::insertTextAtCursor(const QString &text) {\n\tif (!_canSendTexts) {\n\t\treturn;\n\t}\n\tMenu::InsertTextAtCursor(_field, text);\n}\n\nbool HistoryWidget::eventFilter(QObject *obj, QEvent *e) {\n\tif (e->type() == QEvent::KeyPress) {\n\t\tconst auto k = static_cast<QKeyEvent*>(e);\n\t\tif ((k->modifiers() & kCommonModifiers) == Qt::ControlModifier) {\n\t\t\tif (k->key() == Qt::Key_Up) {\n#ifdef Q_OS_MAC\n\t\t\t\t// Cmd + Up is used instead of Home.\n\t\t\t\tif (HasSendText(_field)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n#endif\n\t\t\t\treturn replyToPreviousMessage();\n\t\t\t} else if (k->key() == Qt::Key_Down) {\n#ifdef Q_OS_MAC\n\t\t\t\t// Cmd + Down is used instead of End.\n\t\t\t\tif (HasSendText(_field)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n#endif\n\t\t\t\treturn replyToNextMessage();\n\t\t\t}\n\t\t}\n\t}\n\treturn RpWidget::eventFilter(obj, e);\n}\n\nbool HistoryWidget::floatPlayerHandleWheelEvent(QEvent *e) {\n\treturn _peer ? _scroll->viewportEvent(e) : false;\n}\n\nQRect HistoryWidget::floatPlayerAvailableRect() {\n\treturn _peer ? mapToGlobal(_scroll->geometry()) : mapToGlobal(rect());\n}\n\nbool HistoryWidget::readyToForward() const {\n\treturn _canSendMessages && !_forwardPanel->empty();\n}\n\nbool HistoryWidget::hasSilentToggle() const {\n\treturn _peer\n\t\t&& _peer->isBroadcast()\n\t\t&& Data::CanSendAnything(_peer)\n\t\t&& !session().data().notifySettings().silentPostsUnknown(_peer);\n}\n\nvoid HistoryWidget::handleSupportSwitch(not_null<History*> updated) {\n\tif (_history != updated || !session().supportMode()) {\n\t\treturn;\n\t}\n\n\tconst auto setting = session().settings().supportSwitch();\n\tif (auto method = Support::GetSwitchMethod(setting)) {\n\t\tcrl::on_main(this, std::move(method));\n\t}\n}\n\nbool HistoryWidget::isBotStart() const {\n\tconst auto user = _peer ? _peer->asUser() : nullptr;\n\tif (!user\n\t\t|| !user->isBot()\n\t\t|| !_canSendMessages) {\n\t\treturn false;\n\t} else if (!user->botInfo->startToken.isEmpty()) {\n\t\treturn true;\n\t} else if (_history->isEmpty() && !_history->lastMessage()) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool HistoryWidget::isReportMessages() const {\n\treturn _peer && _chooseForReport && _chooseForReport->active;\n}\n\nbool HistoryWidget::isBlocked() const {\n\treturn _peer && _peer->isUser() && _peer->asUser()->isBlocked();\n}\n\nbool HistoryWidget::isJoinChannel() const {\n\tif (const auto channel = _peer ? _peer->asChannel() : nullptr) {\n\t\treturn !channel->amIn() && !channel->isMonoforum();\n\t}\n\treturn false;\n}\n\nbool HistoryWidget::isChoosingTheme() const {\n\treturn _chooseTheme && _chooseTheme->shouldBeShown();\n}\n\nbool HistoryWidget::isMuteUnmute() const {\n\treturn _peer\n\t\t&& ((_peer->isBroadcast() && !_peer->asChannel()->canPostMessages())\n\t\t\t|| (_peer->isGigagroup() && !Data::CanSendAnything(_peer))\n\t\t\t|| _peer->isRepliesChat()\n\t\t\t|| _peer->isVerifyCodes());\n}\n\nbool HistoryWidget::isSearching() const {\n\treturn _composeSearch != nullptr;\n}\n\nbool HistoryWidget::showRecordButton() const {\n\treturn (_recordAvailability != Webrtc::RecordAvailability::None)\n\t\t&& !_voiceRecordBar->isListenState()\n\t\t&& !_voiceRecordBar->isRecordingByAnotherBar()\n\t\t&& !HasSendText(_field)\n\t\t&& !_previewDrawPreview\n\t\t&& (_replyTo || !readyToForward())\n\t\t&& !_editMsgId;\n}\n\nbool HistoryWidget::showInlineBotCancel() const {\n\treturn _inlineBot && !_inlineLookingUpBot;\n}\n\nvoid HistoryWidget::updateSendButtonType() {\n\tusing Type = Ui::SendButton::Type;\n\n\tconst auto type = computeSendButtonType();\n\tconst auto forbidden = [&] {\n\t\tif (type != Type::Record && type != Type::Round) {\n\t\t\treturn false;\n\t\t}\n\t\tif (!_peer) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto restriction = (type == Type::Record)\n\t\t\t? ChatRestriction::SendVoiceMessages\n\t\t\t: ChatRestriction::SendVideoMessages;\n\t\treturn !!Data::RestrictionError(_peer, restriction);\n\t}();\n\t// This logic is duplicated in ChatWidget.\n\tconst auto disabledBySlowmode = _peer\n\t\t&& _peer->slowmodeApplied()\n\t\t&& (_history->latestSendingMessage() != nullptr);\n\tconst auto delay = [&] {\n\t\treturn (type != Type::Cancel && type != Type::Save && _peer)\n\t\t\t? _peer->slowmodeSecondsLeft()\n\t\t\t: 0;\n\t}();\n\tconst auto perMessage = _peer ? _peer->starsPerMessageChecked() : 0;\n\tconst auto messages = !_peer\n\t\t? 0\n\t\t: _voiceRecordBar->isListenState()\n\t\t? 1\n\t\t: ComputeSendingMessagesCount(_history, {\n\t\t\t.forward = &_forwardPanel->items(),\n\t\t\t.text = &_field->getTextWithTags(),\n\t\t});\n\tconst auto stars = perMessage ? (perMessage * messages) : 0;\n\t_send->setState({\n\t\t.type = (delay > 0) ? Type::Slowmode : type,\n\t\t.slowmodeDelay = delay,\n\t\t.starsToSend = stars,\n\t\t.forbidden = forbidden,\n\t});\n\t_send->setDisabled(disabledBySlowmode\n\t\t&& (type == Type::Send\n\t\t\t|| type == Type::Record\n\t\t\t|| type == Type::Round));\n\n\tif (delay != 0) {\n\t\tbase::call_delayed(\n\t\t\tkRefreshSlowmodeLabelTimeout,\n\t\t\tthis,\n\t\t\t[=] { updateSendButtonType(); });\n\t}\n}\n\nbool HistoryWidget::updateCmdStartShown() {\n\tconst auto bot = (_peer && _peer->isUser() && _peer->asUser()->isBot())\n\t\t? _peer->asUser()\n\t\t: nullptr;\n\tauto cmdStartShown = false;\n\tif (_history\n\t\t&& _peer\n\t\t&& (false\n\t\t\t|| (_peer->isChat() && !_peer->asChat()->botCommands().empty())\n\t\t\t|| (_peer->isMegagroup()\n\t\t\t\t&& !_peer->asChannel()->mgInfo->botCommands().empty()))) {\n\t\tif (!isBotStart()\n\t\t\t&& !isBlocked()\n\t\t\t&& !_keyboard->hasMarkup()\n\t\t\t&& !_keyboard->forceReply()\n\t\t\t&& !_editMsgId) {\n\t\t\tif (!HasSendText(_field)) {\n\t\t\t\tcmdStartShown = true;\n\t\t\t}\n\t\t}\n\t}\n\tconstexpr auto kSmallMenuAfter = 10;\n\tconst auto commandsChanged = (_cmdStartShown != cmdStartShown);\n\tauto buttonChanged = false;\n\tif (!bot\n\t\t|| (bot->botInfo->botMenuButtonUrl.isEmpty()\n\t\t\t&& bot->botInfo->commands.empty())) {\n\t\tbuttonChanged = (_botMenu.button != nullptr);\n\t\t_botMenu.button.destroy();\n\t} else if (!_botMenu.button) {\n\t\tbuttonChanged = true;\n\t\t_botMenu.text = bot->botInfo->botMenuButtonText;\n\t\t_botMenu.small = (_fieldCharsCountManager.count() > kSmallMenuAfter);\n\t\tif (_botMenu.small) {\n\t\t\tif (const auto e = FirstEmoji(_botMenu.text); !e.isEmpty()) {\n\t\t\t\t_botMenu.text = e;\n\t\t\t}\n\t\t}\n\t\t_botMenu.button.create(\n\t\t\tthis,\n\t\t\t(_botMenu.text.isEmpty()\n\t\t\t\t? tr::lng_bot_menu_button()\n\t\t\t\t: rpl::single(_botMenu.text)),\n\t\t\tst::historyBotMenuButton);\n\t\torderWidgets();\n\n\t\t_botMenu.button->setFullRadius(true);\n\t\t_botMenu.button->setClickedCallback([=] {\n\t\t\tconst auto user = _peer ? _peer->asUser() : nullptr;\n\t\t\tconst auto bot = (user && user->isBot()) ? user : nullptr;\n\t\t\tif (bot && !bot->botInfo->botMenuButtonUrl.isEmpty()) {\n\t\t\t\tsession().attachWebView().open({\n\t\t\t\t\t.bot = bot,\n\t\t\t\t\t.context = { .controller = controller() },\n\t\t\t\t\t.button = {\n\t\t\t\t\t\t.url = bot->botInfo->botMenuButtonUrl.toUtf8(),\n\t\t\t\t\t},\n\t\t\t\t\t.source = InlineBots::WebViewSourceBotMenu(),\n\t\t\t\t});\n\t\t\t} else if (_autocomplete && !_autocomplete->isHidden()) {\n\t\t\t\t_autocomplete->hideAnimated();\n\t\t\t} else if (_autocomplete) {\n\t\t\t\t_autocomplete->showFiltered(_peer, \"/\", true);\n\t\t\t}\n\t\t});\n\t\t_botMenu.button->widthValue(\n\t\t) | rpl::on_next([=](int width) {\n\t\t\tif (width > st::historyBotMenuMaxWidth) {\n\t\t\t\t_botMenu.button->setFullWidth(st::historyBotMenuMaxWidth);\n\t\t\t} else {\n\t\t\t\tupdateFieldSize();\n\t\t\t}\n\t\t}, _botMenu.button->lifetime());\n\t}\n\tconst auto textSmall = _fieldCharsCountManager.count() > kSmallMenuAfter;\n\tconst auto textChanged = _botMenu.button\n\t\t&& ((_botMenu.text != bot->botInfo->botMenuButtonText)\n\t\t\t|| (_botMenu.small != textSmall));\n\tif (textChanged) {\n\t\t_botMenu.text = bot->botInfo->botMenuButtonText;\n\t\tif ((_botMenu.small = textSmall)) {\n\t\t\tif (const auto e = FirstEmoji(_botMenu.text); !e.isEmpty()) {\n\t\t\t\t_botMenu.text = e;\n\t\t\t}\n\t\t}\n\t\t_botMenu.button->setText(_botMenu.text.isEmpty()\n\t\t\t? tr::lng_bot_menu_button()\n\t\t\t: rpl::single(_botMenu.text));\n\t}\n\t_cmdStartShown = cmdStartShown;\n\treturn commandsChanged || buttonChanged || textChanged;\n}\n\nbool HistoryWidget::searchInChatEmbedded(\n\t\tQString query,\n\t\tDialogs::Key chat,\n\t\tPeerData *searchFrom) {\n\tconst auto peer = chat.peer(); // windows todo\n\tif (!peer\n\t\t|| ((Window::SeparateId(peer) != controller()->windowId())\n\t\t\t&& !controller()->isPrimary())) {\n\t\treturn false;\n\t} else if (_peer != peer) {\n\t\tconst auto weak = base::make_weak(this);\n\t\tcontroller()->showPeerHistory(peer);\n\t\tif (!weak) {\n\t\t\treturn false;\n\t\t}\n\t}\n\tif (_peer != peer) {\n\t\treturn false;\n\t} else if (_composeSearch) {\n\t\t_composeSearch->setQuery(query);\n\t\t_composeSearch->setInnerFocus();\n\t\treturn true;\n\t}\n\tswitchToSearch(query);\n\treturn true;\n}\n\nvoid HistoryWidget::switchToSearch(QString query) {\n\tconst auto search = crl::guard(_list, [=] {\n\t\tif (!_peer) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto update = [=] {\n\t\t\tupdateControlsVisibility();\n\t\t\tupdateBotKeyboard();\n\t\t\tupdateFieldPlaceholder();\n\n\t\t\tupdateControlsGeometry();\n\t\t};\n\t\tconst auto from = (PeerData*)nullptr;\n\t\t_composeSearch = std::make_unique<HistoryView::ComposeSearch>(\n\t\t\tthis,\n\t\t\tcontroller(),\n\t\t\t_history,\n\t\t\tfrom,\n\t\t\tquery);\n\n\t\tupdate();\n\t\tsetInnerFocus();\n\n\t\tusing Activation = HistoryView::ComposeSearch::Activation;\n\t\t_composeSearch->activations(\n\t\t) | rpl::on_next([=](Activation activation) {\n\t\t\tconst auto item = activation.item;\n\t\t\tauto params = ::Window::SectionShow(\n\t\t\t\t::Window::SectionShow::Way::ClearStack);\n\t\t\tparams.highlight = Window::SearchHighlightId(activation.query);\n\t\t\tcontroller()->showPeerHistory(\n\t\t\t\titem->history()->peer->id,\n\t\t\t\tparams,\n\t\t\t\titem->fullId().msg);\n\t\t}, _composeSearch->lifetime());\n\n\t\t_composeSearch->destroyRequests(\n\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t_composeSearch = nullptr;\n\n\t\t\tupdate();\n\t\t\tsetInnerFocus();\n\t\t}, _composeSearch->lifetime());\n\t});\n\tif (!preventsClose(search)) {\n\t\tsearch();\n\t}\n}\n\nbool HistoryWidget::kbWasHidden() const {\n\treturn _history\n\t\t&& (_keyboard->forMsgId()\n\t\t\t== FullMsgId(\n\t\t\t\t_history->peer->id,\n\t\t\t\t_history->lastKeyboardHiddenId));\n}\n\nvoid HistoryWidget::showKeyboardHideButton() {\n\t_botKeyboardHide->setVisible(!_peer->isUser()\n\t\t|| !_keyboard->persistent());\n}\n\nvoid HistoryWidget::toggleKeyboard(bool manual) {\n\tconst auto fieldEnabled = canWriteMessage() && !_showAnimation;\n\tif (_kbShown || _kbReplyTo) {\n\t\t_botKeyboardHide->hide();\n\t\tif (_kbShown) {\n\t\t\tif (fieldEnabled) {\n\t\t\t\t_botKeyboardShow->show();\n\t\t\t}\n\t\t\tif (manual && _history) {\n\t\t\t\t_history->lastKeyboardHiddenId = _keyboard->forMsgId().msg;\n\t\t\t}\n\n\t\t\t_kbScroll->hide();\n\t\t\t_kbShown = false;\n\n\t\t\t_field->setMaxHeight(computeMaxFieldHeight());\n\n\t\t\t_kbReplyTo = nullptr;\n\t\t\tif (!readyToForward()\n\t\t\t\t&& !_previewDrawPreview\n\t\t\t\t&& !_editMsgId\n\t\t\t\t&& !_replyTo\n\t\t\t\t&& !_suggestOptions) {\n\t\t\t\t_fieldBarCancel->hide();\n\t\t\t\tupdateMouseTracking();\n\t\t\t}\n\t\t} else {\n\t\t\tif (_history) {\n\t\t\t\t_history->clearLastKeyboard();\n\t\t\t} else {\n\t\t\t\tupdateBotKeyboard();\n\t\t\t}\n\t\t}\n\t} else if (!_keyboard->hasMarkup() && _keyboard->forceReply()) {\n\t\t_botKeyboardHide->hide();\n\t\t_botKeyboardShow->hide();\n\t\tif (fieldEnabled) {\n\t\t\t_botCommandStart->show();\n\t\t}\n\t\t_kbScroll->hide();\n\t\t_kbShown = false;\n\n\t\t_field->setMaxHeight(computeMaxFieldHeight());\n\n\t\t_kbReplyTo = (false\n\t\t\t|| _peer->isChat()\n\t\t\t|| _peer->isChannel()\n\t\t\t|| _keyboard->forceReply())\n\t\t\t? session().data().message(_keyboard->forMsgId())\n\t\t\t: nullptr;\n\t\tif (_kbReplyTo && !_editMsgId && !_replyTo && fieldEnabled) {\n\t\t\tupdateReplyToName();\n\t\t\tupdateReplyEditText(_kbReplyTo);\n\t\t}\n\t\tif (manual && _history) {\n\t\t\t_history->lastKeyboardHiddenId = 0;\n\t\t}\n\t} else if (fieldEnabled) {\n\t\tshowKeyboardHideButton();\n\t\t_botKeyboardShow->hide();\n\t\t_kbScroll->show();\n\t\t_kbShown = true;\n\n\t\tconst auto maxheight = computeMaxFieldHeight();\n\t\tconst auto kbheight = qMin(\n\t\t\t_keyboard->height(),\n\t\t\tmaxheight - (maxheight / 2));\n\t\t_field->setMaxHeight(maxheight - kbheight);\n\n\t\t_kbReplyTo = (false\n\t\t\t|| _peer->isChat()\n\t\t\t|| _peer->isChannel()\n\t\t\t|| _keyboard->forceReply())\n\t\t\t? session().data().message(_keyboard->forMsgId())\n\t\t\t: nullptr;\n\t\tif (_kbReplyTo && !_editMsgId && !_replyTo) {\n\t\t\tupdateReplyToName();\n\t\t\tupdateReplyEditText(_kbReplyTo);\n\t\t}\n\t\tif (manual && _history) {\n\t\t\t_history->lastKeyboardHiddenId = 0;\n\t\t}\n\t}\n\tupdateControlsGeometry();\n\tupdateAiButtonVisibility();\n\tupdateSendAsFileVisibility();\n\tupdateFieldPlaceholder();\n\tif (_botKeyboardHide->isHidden()\n\t\t&& canWriteMessage()\n\t\t&& !_showAnimation) {\n\t\t_tabbedSelectorToggle->show();\n\t} else {\n\t\t_tabbedSelectorToggle->hide();\n\t}\n\tupdateField();\n}\n\nvoid HistoryWidget::startBotCommand() {\n\tsetFieldText(\n\t\t{ u\"/\"_q, TextWithTags::Tags() },\n\t\t0,\n\t\tUi::InputField::HistoryAction::NewEntry);\n}\n\nvoid HistoryWidget::setMembersShowAreaActive(bool active) {\n\tif (!active) {\n\t\t_membersDropdownShowTimer.cancel();\n\t}\n\tif (active && _peer && (_peer->isChat() || _peer->isMegagroup())) {\n\t\tif (_membersDropdown) {\n\t\t\t_membersDropdown->otherEnter();\n\t\t} else if (!_membersDropdownShowTimer.isActive()) {\n\t\t\t_membersDropdownShowTimer.callOnce(kShowMembersDropdownTimeoutMs);\n\t\t}\n\t} else if (_membersDropdown) {\n\t\t_membersDropdown->otherLeave();\n\t}\n}\n\nvoid HistoryWidget::showMembersDropdown() {\n\tif (!_peer) {\n\t\treturn;\n\t}\n\tif (!_membersDropdown) {\n\t\t_membersDropdown.create(this, st::membersInnerDropdown);\n\t\t_membersDropdown->setOwnedWidget(\n\t\t\tobject_ptr<HistoryView::GroupMembersWidget>(this, controller(), _peer));\n\t\t_membersDropdown->resizeToWidth(st::membersInnerWidth);\n\n\t\t_membersDropdown->setMaxHeight(countMembersDropdownHeightMax());\n\t\t_membersDropdown->moveToLeft(0, _topBar->height());\n\t\t_membersDropdown->setHiddenCallback([this] {\n\t\t\t_membersDropdown.destroyDelayed();\n\t\t});\n\t}\n\t_membersDropdown->otherEnter();\n}\n\nbool HistoryWidget::pushTabbedSelectorToThirdSection(\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst Window::SectionShow &params) {\n\tif (!_tabbedPanel) {\n\t\treturn true;\n\t} else if (!Data::CanSendAnyOf(\n\t\t\tthread,\n\t\t\tData::TabbedPanelSendRestrictions())) {\n\t\tCore::App().settings().setTabbedReplacedWithInfo(true);\n\t\tcontroller()->showPeerInfo(thread, params.withThirdColumn());\n\t\treturn false;\n\t}\n\tCore::App().settings().setTabbedReplacedWithInfo(false);\n\tcontroller()->resizeForThirdSection();\n\tcontroller()->showSection(\n\t\tstd::make_shared<ChatHelpers::TabbedMemento>(),\n\t\tparams.withThirdColumn());\n\treturn true;\n}\n\nbool HistoryWidget::returnTabbedSelector() {\n\tcreateTabbedPanel();\n\tmoveFieldControls();\n\treturn true;\n}\n\nvoid HistoryWidget::createTabbedPanel() {\n\tsetTabbedPanel(std::make_unique<TabbedPanel>(\n\t\tthis,\n\t\tcontroller(),\n\t\tcontroller()->tabbedSelector()));\n}\n\nvoid HistoryWidget::setTabbedPanel(std::unique_ptr<TabbedPanel> panel) {\n\t_tabbedPanel = std::move(panel);\n\tif (const auto raw = _tabbedPanel.get()) {\n\t\tif (!Core::App().settings().fork().emojiPopupOnClick()) {\n\t\t\t_tabbedSelectorToggle->installEventFilter(raw);\n\t\t}\n\t\t_tabbedSelectorToggle->setColorOverrides(nullptr, nullptr, nullptr);\n\t} else {\n\t\t_tabbedSelectorToggle->setColorOverrides(\n\t\t\t&st::historyAttachEmojiActive,\n\t\t\t&st::historyRecordVoiceFgActive,\n\t\t\t&st::historyRecordVoiceRippleBgActive);\n\t}\n}\n\nbool HistoryWidget::preventsClose(Fn<void()> &&continueCallback) const {\n\tif (_voiceRecordBar->isActive()) {\n\t\t_voiceRecordBar->showDiscardBox(std::move(continueCallback));\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid HistoryWidget::toggleTabbedSelectorMode() {\n\tif (!_history) {\n\t\treturn;\n\t}\n\tif (_tabbedPanel) {\n\t\tif (controller()->canShowThirdSection()\n\t\t\t&& !controller()->adaptive().isOneColumn()) {\n\t\t\tCore::App().settings().setTabbedSelectorSectionEnabled(true);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\tpushTabbedSelectorToThirdSection(\n\t\t\t\t_history,\n\t\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\t} else {\n\t\t\t_tabbedPanel->toggleAnimated();\n\t\t}\n\t} else {\n\t\tcontroller()->closeThirdSection();\n\t}\n}\n\nvoid HistoryWidget::recountChatWidth() {\n\tconst auto layout = (width() < st::adaptiveChatWideWidth)\n\t\t? Window::Adaptive::ChatLayout::Normal\n\t\t: Window::Adaptive::ChatLayout::Wide;\n\tcontroller()->adaptive().setChatLayout(layout);\n}\n\nint HistoryWidget::fieldHeight() const {\n\treturn (_canSendTexts || _editMsgId)\n\t\t? _field->height()\n\t\t: (st::historySendSize.height() - 2 * st::historySendPadding);\n}\n\nbool HistoryWidget::fieldOrDisabledShown() const {\n\treturn !_field->isHidden() || _fieldDisabled;\n}\n\nbool HistoryWidget::hasEnoughLinesForAi() const {\n\treturn _history\n\t\t&& !_voiceRecordBar->isActive()\n\t\t&& Ui::HasEnoughLinesForAi(&session(), _field);\n}\n\nbool HistoryWidget::textExceedsMaxSize() const {\n\treturn _history\n\t\t&& !_voiceRecordBar->isActive()\n\t\t&& _field->getLastText().size() > MaxMessageSize;\n}\n\nvoid HistoryWidget::updateAiButtonVisibility() {\n\tconst auto hidden = !hasEnoughLinesForAi()\n\t\t|| !_send->isVisible()\n\t\t|| !_field->isVisible();\n\tif (_aiButton->isHidden() == hidden) {\n\t\treturn;\n\t}\n\tconst auto shown = !hidden;\n\t_aiButton->setVisible(shown);\n\tif (shown) {\n\t\tupdateAiButtonGeometry();\n\t}\n\tif (_aiTooltipManager) {\n\t\t_aiTooltipManager->updateVisibility(shown);\n\t}\n}\n\nvoid HistoryWidget::updateAiButtonGeometry() {\n\tif (_aiButton->isHidden()) {\n\t\treturn;\n\t}\n\tconst auto x = _send->x() + _send->width() - _aiButton->width();\n\t_aiButton->move(QPoint(x, _field->y()) + st::historyAiComposeButtonPosition);\n\tif (_aiTooltipManager) {\n\t\t_aiTooltipManager->updateGeometry();\n\t}\n}\n\nvoid HistoryWidget::updateSendAsFileVisibility() {\n\tconst auto hidden = !textExceedsMaxSize()\n\t\t|| _send->isHidden()\n\t\t|| _field->isHidden()\n\t\t|| editingMessage();\n\tif (_sendAsFile->isHidden() == hidden) {\n\t\treturn;\n\t}\n\t_sendAsFile->setVisible(!hidden);\n\tif (!hidden) {\n\t\tupdateSendAsFileGeometry();\n\t}\n\tif (_sendAsFileTooltipManager) {\n\t\t_sendAsFileTooltipManager->updateVisibility(!hidden);\n\t}\n}\n\nvoid HistoryWidget::updateSendAsFileGeometry() {\n\tif (_sendAsFile->isHidden()) {\n\t\treturn;\n\t}\n\tconst auto x = _send->x() + _send->width() - _sendAsFile->width();\n\t_sendAsFile->move(QPoint(x, _field->y()) + st::historyAiComposeButtonPosition);\n\tif (_sendAsFileTooltipManager) {\n\t\t_sendAsFileTooltipManager->updateGeometry();\n\t}\n}\n\nvoid HistoryWidget::moveFieldControls() {\n\tauto keyboardHeight = 0;\n\tauto bottom = height();\n\tauto maxKeyboardHeight = computeMaxFieldHeight() - fieldHeight();\n\t_keyboard->resizeToWidth(width(), maxKeyboardHeight);\n\tif (_kbShown) {\n\t\tkeyboardHeight = qMin(_keyboard->height(), maxKeyboardHeight);\n\t\tbottom -= keyboardHeight;\n\t\t_kbScroll->setGeometryToLeft(0, bottom, width(), keyboardHeight);\n\t}\n\n// (_botMenu.button) (_attachToggle|_replaceMedia) (_sendAs) ---- _inlineResults ------------------------------ _tabbedPanel ------ _fieldBarCancel\n// (_attachDocument|_attachPhoto) _field (_ttlInfo) (_scheduled) (_giftToUser) (_silent|_cmdStart|_kbShow) (_toggleSuggestPost) (_kbHide|_tabbedSelectorToggle) _send\n// (_botStart|_unblock|_joinChannel|_muteUnmute|_reportMessages)\n\n\tauto buttonsBottom = bottom - _attachToggle->height();\n\tauto left = st::historySendRight;\n\tif (_botMenu.button) {\n\t\tconst auto skip = st::historyBotMenuSkip;\n\t\t_botMenu.button->moveToLeft(left + skip, buttonsBottom + skip);\n\t\tleft += skip + _botMenu.button->width();\n\t}\n\tif (_replaceMedia) {\n\t\t_replaceMedia->moveToLeft(left, buttonsBottom);\n\t}\n\t_attachToggle->moveToLeft(left, buttonsBottom);\n\tleft += _attachToggle->width();\n\tif (_sendAs) {\n\t\t_sendAs->moveToLeft(left, buttonsBottom);\n\t\tleft += _sendAs->width();\n\t}\n\t_field->moveToLeft(\n\t\tleft,\n\t\tbottom - _field->height() - st::historySendPadding);\n\tif (_fieldDisabled) {\n\t\t_fieldDisabled->moveToLeft(\n\t\t\tleft,\n\t\t\tbottom - fieldHeight() - st::historySendPadding);\n\t}\n\tauto right = st::historySendRight;\n\t_send->moveToRight(right, buttonsBottom); right += _send->width();\n\t_voiceRecordBar->moveToLeft(0, bottom - _voiceRecordBar->height());\n\t_tabbedSelectorToggle->moveToRight(right, buttonsBottom);\n\t_botKeyboardHide->moveToRight(right, buttonsBottom);\n\tright += _botKeyboardHide->width();\n\t_botKeyboardShow->moveToRight(right, buttonsBottom);\n\t_botCommandStart->moveToRight(right, buttonsBottom);\n\tif (_silent) {\n\t\t_silent->moveToRight(right, buttonsBottom);\n\t}\n\tconst auto kbShowShown = _history && !_kbShown && _keyboard->hasMarkup();\n\tif (kbShowShown || _cmdStartShown || _silent) {\n\t\tright += _botCommandStart->width();\n\t}\n\tif (_toggleSuggestPost) {\n\t\t_toggleSuggestPost->moveToRight(right, buttonsBottom);\n\t\tright += _toggleSuggestPost->width();\n\t}\n\tif (_giftToUser) {\n\t\t_giftToUser->moveToRight(right, buttonsBottom);\n\t\tright += _giftToUser->width();\n\t}\n\tif (_scheduled) {\n\t\t_scheduled->moveToRight(right, buttonsBottom);\n\t\tright += _scheduled->width();\n\t}\n\tif (_ttlInfo) {\n\t\t_ttlInfo->move(width() - right - _ttlInfo->width(), buttonsBottom);\n\t}\n\tupdateAiButtonGeometry();\n\tupdateSendAsFileGeometry();\n\n\t_fieldBarCancel->moveToRight(\n\t\t0,\n\t\t_field->y() - st::historySendPadding - _fieldBarCancel->height());\n\tif (_inlineResults) {\n\t\t_inlineResults->moveBottom(_field->y() - st::historySendPadding);\n\t}\n\tif (_tabbedPanel) {\n\t\t_tabbedPanel->moveBottomRight(buttonsBottom, width());\n\t}\n\tif (_attachBotsMenu) {\n\t\t_attachBotsMenu->moveToLeft(\n\t\t\t0,\n\t\t\tbuttonsBottom - _attachBotsMenu->height());\n\t}\n\n\tconst auto fullWidthButtonRect = myrtlrect(\n\t\t0,\n\t\tbottom - _botStart->height(),\n\t\twidth(),\n\t\t_botStart->height());\n\t_botStart->setGeometry(fullWidthButtonRect);\n\t_unblock->setGeometry(fullWidthButtonRect);\n\t_joinChannel->setGeometry(fullWidthButtonRect);\n\t_muteUnmute->setGeometry(fullWidthButtonRect);\n\t_reportMessages->setGeometry(fullWidthButtonRect);\n\tif (_sendRestriction) {\n\t\t_sendRestriction->setGeometry(fullWidthButtonRect);\n\t}\n}\n\nvoid HistoryWidget::updateFieldSize() {\n\tconst auto kbShowShown = _history && !_kbShown && _keyboard->hasMarkup();\n\tauto fieldWidth = width()\n\t\t- _attachToggle->width()\n\t\t- st::historySendRight\n\t\t- _send->width()\n\t\t- _tabbedSelectorToggle->width();\n\tif (_botMenu.button) {\n\t\tfieldWidth -= st::historyBotMenuSkip + _botMenu.button->width();\n\t}\n\tif (_sendAs) {\n\t\tfieldWidth -= _sendAs->width();\n\t}\n\tif (kbShowShown) {\n\t\tfieldWidth -= _botKeyboardShow->width();\n\t}\n\tif (_cmdStartShown) {\n\t\tfieldWidth -= _botCommandStart->width();\n\t}\n\tif (_silent && !_silent->isHidden()) {\n\t\tfieldWidth -= _silent->width();\n\t}\n\tif (_toggleSuggestPost && !_toggleSuggestPost->isHidden()) {\n\t\tfieldWidth -= _toggleSuggestPost->width();\n\t}\n\tif (_giftToUser && !_giftToUser->isHidden()) {\n\t\tfieldWidth -= _giftToUser->width();\n\t}\n\tif (_scheduled && !_scheduled->isHidden()) {\n\t\tfieldWidth -= _scheduled->width();\n\t}\n\tif (_ttlInfo && _ttlInfo->isVisible()) {\n\t\tfieldWidth -= _ttlInfo->width();\n\t}\n\n\tif (_fieldDisabled) {\n\t\t_fieldDisabled->resize(width(), st::historySendSize.height());\n\t}\n\tif (_field->width() != fieldWidth) {\n\t\t_field->resize(fieldWidth, _field->height());\n\t} else {\n\t\tmoveFieldControls();\n\t}\n}\n\nvoid HistoryWidget::clearInlineBot() {\n\tif (_inlineBot || _inlineLookingUpBot) {\n\t\t_inlineBot = nullptr;\n\t\t_inlineLookingUpBot = false;\n\t\tinlineBotChanged();\n\t\t_field->finishAnimating();\n\t}\n\tif (_inlineResults) {\n\t\t_inlineResults->clearInlineBot();\n\t}\n\tif (_autocomplete) {\n\t\t_autocomplete->requestRefresh();\n\t}\n}\n\nvoid HistoryWidget::inlineBotChanged() {\n\tbool isInlineBot = showInlineBotCancel();\n\tif (_isInlineBot != isInlineBot) {\n\t\t_isInlineBot = isInlineBot;\n\t\tupdateFieldPlaceholder();\n\t\tupdateFieldSubmitSettings();\n\t\tupdateControlsVisibility();\n\t}\n}\n\nvoid HistoryWidget::fieldResized() {\n\tmoveFieldControls();\n\tupdateAiButtonVisibility();\n\tupdateSendAsFileVisibility();\n\tupdateHistoryGeometry();\n\tupdateField();\n}\n\nvoid HistoryWidget::fieldFocused() {\n\tif (_list) {\n\t\t_list->clearSelected(true);\n\t\t_list->hideElementOverlay();\n\t}\n}\n\nvoid HistoryWidget::updateFieldPlaceholder() {\n\t_voiceRecordBar->setPauseInsteadSend(_history\n\t\t&& _history->peer->starsPerMessageChecked() > 0);\n\n\tif (!_editMsgId && _inlineBot && !_inlineLookingUpBot) {\n\t\t_field->setPlaceholder(\n\t\t\trpl::single(_inlineBot->botInfo->inlinePlaceholder.mid(1)),\n\t\t\t_inlineBotUsername.size() + 2);\n\t\treturn;\n\t}\n\n\t_field->setPlaceholder([&]() -> rpl::producer<QString> {\n\t\tconst auto peer = _history ? _history->peer.get() : nullptr;\n\t\tif (_editMsgId) {\n\t\t\treturn tr::lng_edit_message_text();\n\t\t} else if (!peer) {\n\t\t\treturn tr::lng_message_ph();\n\t\t} else if ((_kbShown || _keyboard->forceReply())\n\t\t\t&& !_keyboard->placeholder().isEmpty()) {\n\t\t\treturn rpl::single(_keyboard->placeholder());\n\t\t} else if (const auto stars = peer->starsPerMessageChecked()) {\n\t\t\treturn tr::lng_message_stars_ph(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(stars * 1.));\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\tconst auto topic = resolveReplyToTopic();\n\t\t\tconst auto topicRootId = topic\n\t\t\t\t? topic->rootId()\n\t\t\t\t: channel->forum()\n\t\t\t\t? resolveReplyToTopicRootId()\n\t\t\t\t: MsgId();\n\t\t\tif (topicRootId) {\n\t\t\t\tauto title = rpl::single(topic\n\t\t\t\t\t? topic->title()\n\t\t\t\t\t: (topicRootId == Data::ForumTopic::kGeneralId)\n\t\t\t\t\t? u\"General\"_q\n\t\t\t\t\t: u\"Topic\"_q\n\t\t\t\t) | rpl::then(session().changes().topicUpdates(\n\t\t\t\t\tData::TopicUpdate::Flag::Title\n\t\t\t\t) | rpl::filter([=](const Data::TopicUpdate &update) {\n\t\t\t\t\treturn (update.topic->peer() == channel)\n\t\t\t\t\t\t&& (update.topic->rootId() == topicRootId);\n\t\t\t\t}) | rpl::map([=](const Data::TopicUpdate &update) {\n\t\t\t\t\treturn update.topic->title();\n\t\t\t\t}));\n\t\t\t\tconst auto phrase = replyTo().messageId\n\t\t\t\t\t? tr::lng_forum_reply_in\n\t\t\t\t\t: tr::lng_forum_message_in;\n\t\t\t\treturn phrase(lt_topic, std::move(title));\n\t\t\t} else if (channel->isBroadcast()) {\n\t\t\t\treturn session().data().notifySettings().silentPosts(channel)\n\t\t\t\t\t? tr::lng_broadcast_silent_ph()\n\t\t\t\t\t: tr::lng_broadcast_ph();\n\t\t\t} else if (channel->adminRights() & ChatAdminRight::Anonymous) {\n\t\t\t\treturn tr::lng_send_anonymous_ph();\n\t\t\t} else {\n\t\t\t\treturn tr::lng_message_ph();\n\t\t\t}\n\t\t} else if (const auto user = peer->asUser()) {\n\t\t\tif (const auto &info = user->botInfo) {\n\t\t\t\tif (info->forum() && !info->userCreatesTopics) {\n\t\t\t\t\treturn tr::lng_bot_off_thread_ph();\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn tr::lng_message_ph();\n\t\t} else {\n\t\t\treturn tr::lng_message_ph();\n\t\t}\n\t}());\n\tupdateSendButtonType();\n}\n\nbool HistoryWidget::showSendingFilesError(\n\t\tconst Ui::PreparedList &list) const {\n\tconst auto show = controller()->uiShow();\n\treturn Data::ShowSendError(show, _peer, list, std::nullopt);\n}\n\nbool HistoryWidget::showSendingFilesError(\n\t\tconst Ui::PreparedBundle &bundle) const {\n\treturn Data::ShowSendError(controller()->uiShow(), _peer, bundle);\n}\n\nMsgId HistoryWidget::resolveReplyToTopicRootId() {\n\tExpects(_peer != nullptr);\n\n\tconst auto replyToInfo = replyTo();\n\tconst auto replyToMessage = (replyToInfo.messageId.peer == _peer->id)\n\t\t? session().data().message(replyToInfo.messageId)\n\t\t: nullptr;\n\tconst auto result = replyToMessage\n\t\t? replyToMessage->topicRootId()\n\t\t: replyToInfo.topicRootId;\n\tif (result\n\t\t&& _peer->isForum()\n\t\t&& !_peer->forumTopicFor(result)\n\t\t&& _topicsRequested.emplace(result).second) {\n\t\t_peer->forum()->requestTopic(result, crl::guard(_list, [=] {\n\t\t\tupdateCanSendMessage();\n\t\t\tupdateFieldPlaceholder();\n\t\t\t_topicsRequested.remove(result);\n\t\t}));\n\t}\n\treturn result;\n}\n\nData::ForumTopic *HistoryWidget::resolveReplyToTopic() {\n\treturn _peer\n\t\t? _peer->forumTopicFor(resolveReplyToTopicRootId())\n\t\t: nullptr;\n}\n\nbool HistoryWidget::showSendMessageError(\n\t\tconst TextWithTags &textWithTags,\n\t\tbool ignoreSlowmodeCountdown,\n\t\tFn<void(int starsApproved)> withPaymentApproved,\n\t\tApi::SendOptions options) {\n\tif (!_canSendMessages) {\n\t\treturn false;\n\t}\n\tconst auto topicRootId = resolveReplyToTopicRootId();\n\tauto request = SendingErrorRequest{\n\t\t.topicRootId = topicRootId,\n\t\t.forward = &_forwardPanel->items(),\n\t\t.text = &textWithTags,\n\t\t.ignoreSlowmodeCountdown = ignoreSlowmodeCountdown,\n\t};\n\trequest.messagesCount = ComputeSendingMessagesCount(_history, request);\n\tconst auto error = GetErrorForSending(_peer, request);\n\tif (error) {\n\t\tData::ShowSendErrorToast(controller(), _peer, error);\n\t\treturn true;\n\t}\n\n\treturn withPaymentApproved\n\t\t&& !checkSendPayment(\n\t\t\trequest.messagesCount,\n\t\t\toptions,\n\t\t\twithPaymentApproved);\n}\n\nbool HistoryWidget::confirmSendingFiles(const QStringList &files) {\n\treturn confirmSendingFiles(files, QString());\n}\n\nbool HistoryWidget::confirmSendingFiles(not_null<const QMimeData*> data) {\n\treturn confirmSendingFiles(data, std::nullopt);\n}\n\nbool HistoryWidget::confirmSendingFiles(\n\t\tconst QStringList &files,\n\t\tconst QString &insertTextOnCancel) {\n\tconst auto premium = controller()->session().user()->isPremium();\n\treturn confirmSendingFiles(\n\t\tStorage::PrepareMediaList(files, st::sendMediaPreviewSize, premium),\n\t\tinsertTextOnCancel);\n}\n\nbool HistoryWidget::confirmSendingFiles(\n\t\tUi::PreparedList &&list,\n\t\tconst QString &insertTextOnCancel) {\n\tif (_editMsgId) {\n\t\tif (_canReplaceMedia || _canAddMedia) {\n\t\t\tEditCaptionBox::StartMediaReplace(\n\t\t\t\tcontroller(),\n\t\t\t\t{ _history->peer->id, _editMsgId },\n\t\t\t\tstd::move(list),\n\t\t\t\t_field->getTextWithTags(),\n\t\t\t\tsuggestOptions(),\n\t\t\t\t_mediaEditManager.spoilered(),\n\t\t\t\t_mediaEditManager.invertCaption(),\n\t\t\t\tcrl::guard(_list, [=] { cancelEdit(); }));\n\t\t\treturn true;\n\t\t}\n\t\tcontroller()->showToast(tr::lng_edit_caption_attach(tr::now));\n\t\treturn false;\n\t} else if (!_peer || showSendingFilesError(list)) {\n\t\treturn false;\n\t}\n\n\tconst auto cursor = _field->textCursor();\n\tconst auto position = cursor.position();\n\tconst auto anchor = cursor.anchor();\n\tconst auto text = _field->getTextWithTags();\n\tauto box = Box<SendFilesBox>(\n\t\tcontroller(),\n\t\tstd::move(list),\n\t\ttext,\n\t\t_peer,\n\t\tApi::SendType::Normal,\n\t\tsendMenuDetails());\n\t_field->setTextWithTags({});\n\tbox->setConfirmedCallback(crl::guard(this, [=](\n\t\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\t\tApi::SendOptions options) {\n\t\tif (bundle->way.asVoice && !bundle->groups.empty()\n\t\t\t&& !bundle->groups.front().list.files.empty()) {\n\t\t\tconst auto &front = bundle->groups.front().list.files.front();\n\t\t\tauto file = QFile(front.path);\n\t\t\tif (file.open(QIODevice::ReadOnly)) {\n\t\t\t\tusing Song = Ui::PreparedFileInformation::Song;\n\t\t\t\tif (auto song = std::get_if<Song>(&front.information->media)) {\n\n\t\t\t\t\tauto waveform = VoiceWaveform();\n\t\t\t\t\twaveform.resize(100);\n\t\t\t\t\tfor (auto i = 0; i < 100; i++) {\n\t\t\t\t\t\twaveform[i] = char(uint32(base::RandomIndex(31)));\n\t\t\t\t\t}\n\t\t\t\t\tsession().api().sendVoiceMessage(\n\t\t\t\t\t\tfile.readAll(),\n\t\t\t\t\t\twaveform,\n\t\t\t\t\t\tsong->duration,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\tprepareSendAction(options));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfile.close();\n\t\t\t}\n\t\t}\n\t\tsendingFilesConfirmed(std::move(bundle), options);\n\t}));\n\tbox->setCancelledCallback(crl::guard(this, [=] {\n\t\t_field->setTextWithTags(text);\n\t\tauto cursor = _field->textCursor();\n\t\tcursor.setPosition(anchor);\n\t\tif (position != anchor) {\n\t\t\tcursor.setPosition(position, QTextCursor::KeepAnchor);\n\t\t}\n\t\t_field->setTextCursor(cursor);\n\t\tif (Ui::InsertTextOnImageCancel(insertTextOnCancel)) {\n\t\t\t_field->textCursor().insertText(insertTextOnCancel);\n\t\t}\n\t}));\n\tbox->takeTextWithTagsRequests() | rpl::on_next([=](TextWithTags &&text) {\n\t\t_field->setTextWithTags(std::move(text));\n\t}, box->lifetime());\n\n\tWindow::ActivateWindow(controller());\n\tcontroller()->show(std::move(box));\n\n\treturn true;\n}\n\nvoid HistoryWidget::sendingFilesConfirmed(\n\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\tApi::SendOptions options) {\n\tif (!_peer || showSendingFilesError(*bundle)) {\n\t\treturn;\n\t}\n\n\tconst auto compress = bundle->way.sendImagesAsPhotos();\n\tconst auto type = compress ? SendMediaType::Photo : SendMediaType::File;\n\tauto action = prepareSendAction(options);\n\taction.clearDraft = false;\n\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = options;\n\t\tcopy.starsApproved = approved;\n\t\tsendingFilesConfirmed(bundle, copy);\n\t};\n\tconst auto checked = checkSendPayment(\n\t\tbundle->totalCount,\n\t\taction.options,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn;\n\t}\n\n\tauto &api = session().api();\n\tfor (auto &group : bundle->groups) {\n\t\tconst auto album = (group.type != Ui::AlbumType::None)\n\t\t\t? std::make_shared<SendingAlbum>()\n\t\t\t: nullptr;\n\t\tapi.sendFiles(std::move(group.list), type, album, action);\n\t}\n}\n\nbool HistoryWidget::confirmSendingFiles(\n\t\tQImage &&image,\n\t\tQByteArray &&content,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos,\n\t\tconst QString &insertTextOnCancel) {\n\tif (image.isNull()) {\n\t\treturn false;\n\t}\n\n\tauto list = Storage::PrepareMediaFromImage(\n\t\tstd::move(image),\n\t\tstd::move(content),\n\t\tst::sendMediaPreviewSize);\n\tlist.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;\n\treturn confirmSendingFiles(std::move(list), insertTextOnCancel);\n}\n\nbool HistoryWidget::canSendFiles(not_null<const QMimeData*> data) const {\n\tif (!canWriteMessage()) {\n\t\treturn false;\n\t} else if (data->hasImage()) {\n\t\treturn true;\n\t} else if (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) {\n\t\tif (ranges::all_of(urls, &QUrl::isLocalFile)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool HistoryWidget::confirmSendingFiles(\n\t\tnot_null<const QMimeData*> data,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos,\n\t\tconst QString &insertTextOnCancel) {\n\tif (!canWriteMessage()) {\n\t\tif (_composeSearch) {\n\t\t\t_composeSearch->hideAnimated();\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tconst auto hasImage = data->hasImage();\n\tconst auto premium = controller()->session().user()->isPremium();\n\n\tif (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) {\n\t\tauto list = Storage::PrepareMediaList(\n\t\t\turls,\n\t\t\tst::sendMediaPreviewSize,\n\t\t\tpremium);\n\t\tif (list.error != Ui::PreparedList::Error::NonLocalUrl) {\n\t\t\tif (list.error == Ui::PreparedList::Error::None\n\t\t\t\t|| !hasImage) {\n\t\t\t\tconst auto emptyTextOnCancel = QString();\n\t\t\t\tlist.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;\n\t\t\t\tconfirmSendingFiles(std::move(list), emptyTextOnCancel);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (auto read = Core::ReadMimeImage(data)) {\n\t\tconfirmSendingFiles(\n\t\t\tstd::move(read.image),\n\t\t\tstd::move(read.content),\n\t\t\toverrideSendImagesAsPhotos,\n\t\t\tinsertTextOnCancel);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid HistoryWidget::uploadFile(\n\t\tconst QByteArray &fileContent,\n\t\tSendMediaType type) {\n\tif (!canWriteMessage()) return;\n\n\tsession().api().sendFile(fileContent, type, prepareSendAction({}));\n}\n\nvoid HistoryWidget::handleHistoryChange(not_null<const History*> history) {\n\tif (_list && (_history == history || _migrated == history)) {\n\t\thandlePendingHistoryUpdate();\n\t\tupdateBotKeyboard();\n\t\tif (!_scroll->isHidden()) {\n\t\t\tconst auto unblock = isBlocked();\n\t\t\tconst auto botStart = isBotStart();\n\t\t\tconst auto joinChannel = isJoinChannel();\n\t\t\tconst auto muteUnmute = isMuteUnmute();\n\t\t\tconst auto reportMessages = isReportMessages();\n\t\t\tconst auto update = false\n\t\t\t\t|| (_reportMessages->isHidden() == reportMessages)\n\t\t\t\t|| (!reportMessages && _unblock->isHidden() == unblock)\n\t\t\t\t|| (!reportMessages\n\t\t\t\t\t&& !unblock\n\t\t\t\t\t&& _botStart->isHidden() == botStart)\n\t\t\t\t|| (!reportMessages\n\t\t\t\t\t&& !unblock\n\t\t\t\t\t&& !botStart\n\t\t\t\t\t&& _joinChannel->isHidden() == joinChannel)\n\t\t\t\t|| (!reportMessages\n\t\t\t\t\t&& !unblock\n\t\t\t\t\t&& !botStart\n\t\t\t\t\t&& !joinChannel\n\t\t\t\t\t&& _muteUnmute->isHidden() == muteUnmute);\n\t\t\tif (update) {\n\t\t\t\tupdateControlsVisibility();\n\t\t\t\tupdateControlsGeometry();\n\t\t\t}\n\t\t}\n\t}\n}\n\nQPixmap HistoryWidget::grabForShowAnimation(\n\t\tconst Window::SectionSlideParams &params) {\n\tif (params.withTopBarShadow) {\n\t\t_topShadow->hide();\n\t}\n\t_inGrab = true;\n\tupdateControlsGeometry();\n\tauto result = Ui::GrabWidget(this);\n\t_inGrab = false;\n\tupdateControlsGeometry();\n\tif (params.withTopBarShadow) {\n\t\t_topShadow->show();\n\t}\n\treturn result;\n}\n\nbool HistoryWidget::skipItemRepaint() {\n\tauto ms = crl::now();\n\tif (_lastScrolled + kSkipRepaintWhileScrollMs <= ms) {\n\t\treturn false;\n\t}\n\t_updateHistoryItems.callOnce(\n\t\t_lastScrolled + kSkipRepaintWhileScrollMs - ms);\n\treturn true;\n}\n\nvoid HistoryWidget::updateHistoryItemsByTimer() {\n\tif (!_list) {\n\t\treturn;\n\t}\n\n\tauto ms = crl::now();\n\tif (_lastScrolled + kSkipRepaintWhileScrollMs <= ms) {\n\t\t_list->update();\n\t} else {\n\t\t_updateHistoryItems.callOnce(\n\t\t\t_lastScrolled + kSkipRepaintWhileScrollMs - ms);\n\t}\n}\n\nvoid HistoryWidget::handlePendingHistoryUpdate() {\n\tif (hasPendingResizedItems() || _updateHistoryGeometryRequired) {\n\t\tupdateHistoryGeometry();\n\t\t_list->update();\n\t}\n}\n\nvoid HistoryWidget::resizeEvent(QResizeEvent *e) {\n\t//updateTabbedSelectorSectionShown();\n\trecountChatWidth();\n\tupdateControlsGeometry();\n}\n\nvoid HistoryWidget::updateControlsGeometry() {\n\tconst auto width = this->width();\n\n\t_topBar->resizeToWidth(width);\n\t_topBar->moveToLeft(0, 0);\n\n\tconst auto tabsLeftSkip = _subsectionTabs\n\t\t? _subsectionTabs->leftSkip()\n\t\t: 0;\n\tconst auto innerWidth = width - tabsLeftSkip;\n\n\t_voiceRecordBar->resizeToWidth(width);\n\n\tmoveFieldControls();\n\n\t_topBars->move(tabsLeftSkip, _topBar->bottomNoMargins()\n\t\t+ (_subsectionTabs ? _subsectionTabs->topSkip() : 0));\n\tconst auto groupCallTop = 0;\n\tif (_groupCallBar) {\n\t\t_groupCallBar->move(0, groupCallTop);\n\t\t_groupCallBar->resizeToWidth(innerWidth);\n\t}\n\tconst auto requestsTop = groupCallTop\n\t\t+ (_groupCallBar ? _groupCallBar->height() : 0);\n\tif (_requestsBar) {\n\t\t_requestsBar->move(0, requestsTop);\n\t\t_requestsBar->resizeToWidth(innerWidth);\n\t}\n\tconst auto pinnedBarTop = requestsTop\n\t\t+ (_requestsBar ? _requestsBar->height() : 0);\n\tif (_pinnedBar) {\n\t\t_pinnedBar->move(0, pinnedBarTop);\n\t\t_pinnedBar->resizeToWidth(innerWidth);\n\t}\n\tconst auto sponsoredMessageBarTop = pinnedBarTop\n\t\t+ (_pinnedBar ? _pinnedBar->height() : 0);\n\tif (_sponsoredMessageBar) {\n\t\t_sponsoredMessageBar->move(0, sponsoredMessageBarTop);\n\t\t_sponsoredMessageBar->resizeToWidth(innerWidth);\n\t}\n\tconst auto translateTop = sponsoredMessageBarTop\n\t\t+ (_sponsoredMessageBar ? _sponsoredMessageBar->height() : 0);\n\tif (_translateBar) {\n\t\t_translateBar->move(0, translateTop);\n\t\t_translateBar->resizeToWidth(innerWidth);\n\t}\n\tconst auto paysStatusTop = translateTop\n\t\t+ (_translateBar ? _translateBar->height() : 0);\n\tif (_paysStatus) {\n\t\t_paysStatus->bar().move(0, paysStatusTop);\n\t}\n\tconst auto contactStatusTop = paysStatusTop\n\t\t+ (_paysStatus ? _paysStatus->bar().height() : 0);\n\tif (_contactStatus) {\n\t\t_contactStatus->bar().move(tabsLeftSkip, contactStatusTop);\n\t}\n\tconst auto businessBotTop = contactStatusTop\n\t\t+ (_contactStatus ? _contactStatus->bar().height() : 0);\n\tif (_businessBotStatus) {\n\t\t_businessBotStatus->bar().move(tabsLeftSkip, businessBotTop);\n\t}\n\tconst auto scrollAreaTop = _topBars->y()\n\t\t+ businessBotTop\n\t\t+ (_businessBotStatus ? _businessBotStatus->bar().height() : 0);\n\t_topBars->resize(\n\t\tinnerWidth,\n\t\tscrollAreaTop - _topBars->y() + st::lineWidth);\n\tif (_scroll->y() != scrollAreaTop || _scroll->x() != tabsLeftSkip) {\n\t\t_scroll->moveToLeft(tabsLeftSkip, scrollAreaTop);\n\t\tif (_autocomplete) {\n\t\t\t_autocomplete->setBoundings(_scroll->geometry());\n\t\t}\n\t\tif (_supportAutocomplete) {\n\t\t\t_supportAutocomplete->setBoundings(_scroll->geometry());\n\t\t}\n\t}\n\n\tupdateHistoryGeometry(false, false, { ScrollChangeAdd, _topDelta });\n\n\tupdateFieldSize();\n\n\t_cornerButtons.updatePositions();\n\n\tif (_membersDropdown) {\n\t\t_membersDropdown->setMaxHeight(countMembersDropdownHeightMax());\n\t}\n\n\tconst auto isOneColumn = controller()->adaptive().isOneColumn();\n\tconst auto isThreeColumn = controller()->adaptive().isThreeColumn();\n\tconst auto topShadowLeft = (isOneColumn || _inGrab)\n\t\t? 0\n\t\t: st::lineWidth;\n\tconst auto topShadowRight = (isThreeColumn && !_inGrab && _peer)\n\t\t? st::lineWidth\n\t\t: 0;\n\t_topShadow->setGeometryToLeft(\n\t\ttopShadowLeft,\n\t\t_topBar->bottomNoMargins(),\n\t\twidth - topShadowLeft - topShadowRight,\n\t\tst::lineWidth);\n}\n\nvoid HistoryWidget::itemRemoved(not_null<const HistoryItem*> item) {\n\tif (item == _replyEditMsg && _editMsgId) {\n\t\tcancelEdit();\n\t}\n\tif (item == _replyEditMsg && _replyTo) {\n\t\tcancelReply();\n\t}\n\tif (item == _processingReplyItem) {\n\t\t_processingReplyTo = {};\n\t\t_processingReplyItem = nullptr;\n\t}\n\tif (_kbReplyTo && item == _kbReplyTo) {\n\t\ttoggleKeyboard();\n\t\t_kbReplyTo = nullptr;\n\t}\n\tconst auto i = _itemRevealAnimations.find(item);\n\tif (i != end(_itemRevealAnimations)) {\n\t\t_itemRevealAnimations.erase(i);\n\t\trevealItemsCallback();\n\t}\n\tconst auto j = _itemRevealPending.find(item);\n\tif (j != _itemRevealPending.end()) {\n\t\t_itemRevealPending.erase(j);\n\t}\n}\n\nvoid HistoryWidget::itemEdited(not_null<HistoryItem*> item) {\n\tif (item.get() == _replyEditMsg) {\n\t\tupdateReplyEditTexts(true);\n\t}\n}\n\nFullReplyTo HistoryWidget::replyTo() const {\n\treturn _replyTo\n\t\t? _replyTo\n\t\t: _kbReplyTo\n\t\t? FullReplyTo{ _kbReplyTo->fullId() }\n\t\t: (_peer && _peer->forum() && !_peer->isBot())\n\t\t? FullReplyTo{ .topicRootId = Data::ForumTopic::kGeneralId }\n\t\t: FullReplyTo();\n}\n\nSuggestOptions HistoryWidget::suggestOptions(\n\t\tbool skipNoAdminCheck) const {\n\tconst auto checked = skipNoAdminCheck\n\t\t|| (_history && _history->suggestDraftAllowed());\n\treturn (checked && _suggestOptions)\n\t\t? _suggestOptions->values()\n\t\t: SuggestOptions();\n}\n\nbool HistoryWidget::hasSavedScroll() const {\n\tExpects(_history != nullptr);\n\n\treturn _history->scrollTopItem\n\t\t|| (_migrated && _migrated->scrollTopItem);\n}\n\nint HistoryWidget::countInitialScrollTop() {\n\tif (hasSavedScroll()) {\n\t\treturn _list->historyScrollTop();\n\t} else if (_showAtMsgId\n\t\t&& (IsServerMsgId(_showAtMsgId)\n\t\t\t|| IsClientMsgId(_showAtMsgId)\n\t\t\t|| IsServerMsgId(-_showAtMsgId))) {\n\t\tconst auto item = getItemFromHistoryOrMigrated(_showAtMsgId);\n\t\tconst auto itemTop = _list->itemTop(item);\n\t\tif (itemTop < 0) {\n\t\t\tsetMsgId(ShowAtUnreadMsgId);\n\t\t\tcontroller()->showToast(tr::lng_message_not_found(tr::now));\n\t\t\treturn countInitialScrollTop();\n\t\t} else {\n\t\t\tconst auto view = item->mainView();\n\t\t\tAssert(view != nullptr);\n\n\t\t\tenqueueMessageHighlight({\n\t\t\t\titem,\n\t\t\t\tbase::take(_showAtMsgParams.highlight),\n\t\t\t});\n\t\t\tconst auto result = itemTopForHighlight(view);\n\t\t\tcreateUnreadBarIfBelowVisibleArea(result);\n\t\t\treturn result;\n\t\t}\n\t} else if (_showAtMsgId == ShowAtTheEndMsgId) {\n\t\treturn ScrollMax;\n\t} else if (const auto top = unreadBarTop()) {\n\t\treturn *top;\n\t} else {\n\t\t_history->calculateFirstUnreadMessage();\n\t\treturn countAutomaticScrollTop();\n\t}\n}\n\nvoid HistoryWidget::createUnreadBarIfBelowVisibleArea(int withScrollTop) {\n\tExpects(_history != nullptr);\n\n\tif (_history->unreadBar()) {\n\t\treturn;\n\t}\n\t_history->calculateFirstUnreadMessage();\n\tif (const auto unread = _history->firstUnreadMessage()) {\n\t\tif (_list->itemTop(unread) > withScrollTop) {\n\t\t\tcreateUnreadBarAndResize();\n\t\t}\n\t}\n}\n\nvoid HistoryWidget::createUnreadBarAndResize() {\n\tif (!_history->firstUnreadMessage()) {\n\t\treturn;\n\t}\n\tconst auto was = base::take(_historyInited);\n\t_history->addUnreadBar();\n\tif (hasPendingResizedItems()) {\n\t\tupdateListSize();\n\t}\n\t_historyInited = was;\n}\n\nint HistoryWidget::countAutomaticScrollTop() {\n\tExpects(_history != nullptr);\n\tExpects(_list != nullptr);\n\n\tif (const auto unread = _history->firstUnreadMessage()) {\n\t\tconst auto firstUnreadTop = _list->itemTop(unread);\n\t\tconst auto possibleUnreadBarTop = _scroll->scrollTopMax()\n\t\t\t+ HistoryView::UnreadBar::height()\n\t\t\t- HistoryView::UnreadBar::marginTop();\n\t\tif (firstUnreadTop < possibleUnreadBarTop) {\n\t\t\tcreateUnreadBarAndResize();\n\t\t\tif (_history->unreadBar() != nullptr) {\n\t\t\t\tsetMsgId(ShowAtUnreadMsgId);\n\t\t\t\treturn countInitialScrollTop();\n\t\t\t}\n\t\t}\n\t}\n\treturn ScrollMax;\n}\n\nData::SendError HistoryWidget::computeSendRestriction() const {\n\tif (!_canSendMessages\n\t\t&& _peer->amMonoforumAdmin()\n\t\t&& !_peer->asChannel()->monoforumDisabled()) {\n\t\treturn Data::SendError({\n\t\t\t.text = tr::lng_monoforum_choose_to_reply(tr::now),\n\t\t\t.monoforumAdmin = true,\n\t\t});\n\t}\n\tconst auto allWithoutPolls = Data::AllSendRestrictions()\n\t\t& ~ChatRestriction::SendPolls;\n\treturn (_peer && !Data::CanSendAnyOf(_peer, allWithoutPolls))\n\t\t? Data::RestrictionError(_peer, ChatRestriction::SendOther)\n\t\t: Data::SendError();\n}\n\nvoid HistoryWidget::updateSendRestriction() {\n\tconst auto restriction = computeSendRestriction();\n\tif (_sendRestrictionKey == restriction.text) {\n\t\treturn;\n\t}\n\t_sendRestrictionKey = restriction.text;\n\tif (!restriction) {\n\t\t_sendRestriction = nullptr;\n\t} else if (restriction.frozen) {\n\t\tconst auto show = controller()->uiShow();\n\t\t_sendRestriction = FrozenWriteRestriction(\n\t\t\tthis,\n\t\t\tshow,\n\t\t\tFrozenWriteRestrictionType::MessageField);\n\t} else if (restriction.premiumToLift) {\n\t\t_sendRestriction = PremiumRequiredSendRestriction(\n\t\t\tthis,\n\t\t\t_peer->asUser(),\n\t\t\tcontroller());\n\t} else if (const auto lifting = restriction.boostsToLift) {\n\t\tconst auto show = controller()->uiShow();\n\t\t_sendRestriction = BoostsToLiftWriteRestriction(\n\t\t\tthis,\n\t\t\tshow,\n\t\t\t_peer,\n\t\t\tlifting);\n\t} else {\n\t\t_sendRestriction = TextErrorSendRestriction(this, restriction.text);\n\t}\n\tif (_sendRestriction) {\n\t\t_sendRestriction->show();\n\t\tmoveFieldControls();\n\t}\n}\n\nvoid HistoryWidget::updateHistoryGeometry(\n\t\tbool initial,\n\t\tbool loadedDown,\n\t\tconst ScrollChange &change) {\n\tconst auto guard = gsl::finally([&] {\n\t\t_itemRevealPending.clear();\n\t});\n\tif (!_history\n\t\t|| (initial && _historyInited)\n\t\t|| (!initial && !_historyInited)) {\n\t\treturn;\n\t}\n\tif (_firstLoadRequest || _showAnimation) {\n\t\t_updateHistoryGeometryRequired = true;\n\t\t// scrollTopMax etc are not working after recountHistoryGeometry()\n\t\treturn;\n\t}\n\n\tconst auto newScrollWidth = width()\n\t\t- (_subsectionTabs ? _subsectionTabs->leftSkip() : 0);\n\tconst auto subsectionTabsTop = _topBar->bottomNoMargins();\n\tauto newScrollHeight = height()\n\t\t- subsectionTabsTop\n\t\t- (_subsectionTabs ? _subsectionTabs->topSkip() : 0)\n\t\t- (_subsectionTabs ? _subsectionTabs->bottomSkip() : 0);\n\tif (_translateBar) {\n\t\tnewScrollHeight -= _translateBar->height();\n\t}\n\tif (_sponsoredMessageBar) {\n\t\tnewScrollHeight -= _sponsoredMessageBar->height();\n\t}\n\tif (_pinnedBar) {\n\t\tnewScrollHeight -= _pinnedBar->height();\n\t}\n\tif (_groupCallBar) {\n\t\tnewScrollHeight -= _groupCallBar->height();\n\t}\n\tif (_requestsBar) {\n\t\tnewScrollHeight -= _requestsBar->height();\n\t}\n\tif (_paysStatus) {\n\t\tnewScrollHeight -= _paysStatus->bar().height();\n\t}\n\tif (_contactStatus) {\n\t\tnewScrollHeight -= _contactStatus->bar().height();\n\t}\n\tif (_businessBotStatus) {\n\t\tnewScrollHeight -= _businessBotStatus->bar().height();\n\t}\n\tif (isChoosingTheme()) {\n\t\tnewScrollHeight -= _chooseTheme->height();\n\t} else if (!editingMessage()\n\t\t&& (isSearching()\n\t\t\t|| isBlocked()\n\t\t\t|| isBotStart()\n\t\t\t|| isJoinChannel()\n\t\t\t|| isMuteUnmute()\n\t\t\t|| isReportMessages())) {\n\t\tnewScrollHeight -= _unblock->height();\n\t} else {\n\t\tif (editingMessage() || _canSendMessages) {\n\t\t\tnewScrollHeight -= (fieldHeight() + 2 * st::historySendPadding);\n\t\t} else if (_sendRestriction) {\n\t\t\tnewScrollHeight -= _sendRestriction->height();\n\t\t}\n\t\tif (_editMsgId\n\t\t\t|| replyTo()\n\t\t\t|| readyToForward()\n\t\t\t|| _previewDrawPreview\n\t\t\t|| _suggestOptions) {\n\t\t\tnewScrollHeight -= st::historyReplyHeight;\n\t\t}\n\t\tif (_kbShown) {\n\t\t\tnewScrollHeight -= _kbScroll->height();\n\t\t}\n\t}\n\tif (newScrollHeight <= 0) {\n\t\treturn;\n\t}\n\tconst auto wasScrollTop = _scroll->scrollTop();\n\tconst auto wasAtBottom = (wasScrollTop == _scroll->scrollTopMax());\n\tconst auto needResize = (_scroll->width() != newScrollWidth)\n\t\t|| (_scroll->height() != newScrollHeight);\n\tif (needResize) {\n\t\t_scroll->resize(newScrollWidth, newScrollHeight);\n\t\t// on initial updateListSize we didn't put the _scroll->scrollTop\n\t\t// correctly yet so visibleAreaUpdated() call will erase it\n\t\t// with the new (undefined) value\n\t\tif (!initial) {\n\t\t\tvisibleAreaUpdated();\n\t\t}\n\t}\n\tif (needResize || initial) {\n\t\tif (_autocomplete) {\n\t\t\t_autocomplete->setBoundings(_scroll->geometry());\n\t\t}\n\t\tif (_supportAutocomplete) {\n\t\t\t_supportAutocomplete->setBoundings(_scroll->geometry());\n\t\t}\n\t\t_cornerButtons.updatePositions();\n\t\tcontroller()->floatPlayerAreaUpdated();\n\t}\n\tif (_subsectionTabs) {\n\t\tconst auto tabsBottomSkip = _subsectionTabs->bottomSkip();\n\t\tconst auto scrollBottom = _scroll->y() + newScrollHeight;\n\t\tconst auto areaHeight = scrollBottom\n\t\t\t+ tabsBottomSkip\n\t\t\t- subsectionTabsTop;\n\t\t_subsectionTabs->setBoundingRect(\n\t\t\t{ 0, subsectionTabsTop, width(), areaHeight });\n\t}\n\n\tupdateListSize();\n\t_updateHistoryGeometryRequired = false;\n\n\tauto newScrollTop = 0;\n\tif (initial) {\n\t\tnewScrollTop = countInitialScrollTop();\n\t\t_historyInited = true;\n\t\t_scrollToAnimation.stop();\n\t} else if (wasAtBottom && !loadedDown && !_history->unreadBar()) {\n\t\tnewScrollTop = countAutomaticScrollTop();\n\t} else {\n\t\tnewScrollTop = std::min(\n\t\t\t_list->historyScrollTop(),\n\t\t\t_scroll->scrollTopMax());\n\t\tif (change.type == ScrollChangeAdd) {\n\t\t\tnewScrollTop += change.value;\n\t\t} else if (change.type == ScrollChangeNoJumpToBottom) {\n\t\t\tnewScrollTop = wasScrollTop;\n\t\t}\n\t}\n\tconst auto toY = std::clamp(newScrollTop, 0, _scroll->scrollTopMax());\n\tsynteticScrollToY(toY);\n\tif (initial && _showAtMsgId) {\n\t\tconst auto timestamp = base::take(_showAtMsgParams.videoTimestamp);\n\t\tif (timestamp.has_value()) {\n\t\t\tconst auto item = session().data().message(_peer, _showAtMsgId);\n\t\t\tconst auto media = item ? item->media() : nullptr;\n\t\t\tconst auto document = media ? media->document() : nullptr;\n\t\t\tif (document && document->isVideoFile()) {\n\t\t\t\tconst auto draw = canWriteMessage();\n\t\t\t\tcontroller()->openDocument(\n\t\t\t\t\tdocument,\n\t\t\t\t\ttrue,\n\t\t\t\t\t{ .id = item->fullId(), .showDrawButton = draw },\n\t\t\t\t\tnullptr,\n\t\t\t\t\ttimestamp);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid HistoryWidget::revealItemsCallback() {\n\tauto height = 0;\n\tif (!_historyInited) {\n\t\t_itemRevealAnimations.clear();\n\t}\n\tfor (auto i = begin(_itemRevealAnimations)\n\t\t; i != end(_itemRevealAnimations);) {\n\t\tif (!i->second.animation.animating()) {\n\t\t\ti = _itemRevealAnimations.erase(i);\n\t\t} else {\n\t\t\theight += anim::interpolate(\n\t\t\t\ti->second.startHeight,\n\t\t\t\t0,\n\t\t\t\ti->second.animation.value(1.));\n\t\t\t++i;\n\t\t}\n\t}\n\tif (_itemsRevealHeight != height) {\n\t\tconst auto wasScrollTop = _scroll->scrollTop();\n\t\tconst auto wasAtBottom = (wasScrollTop == _scroll->scrollTopMax());\n\t\tif (!wasAtBottom) {\n\t\t\theight = 0;\n\t\t\t_itemRevealAnimations.clear();\n\t\t}\n\n\t\t_itemsRevealHeight = height;\n\t\t_list->changeItemsRevealHeight(_itemsRevealHeight);\n\n\t\tconst auto newScrollTop = (wasAtBottom && !_history->unreadBar())\n\t\t\t? countAutomaticScrollTop()\n\t\t\t: _list->historyScrollTop();\n\t\tconst auto toY = std::clamp(newScrollTop, 0, _scroll->scrollTopMax());\n\t\tsynteticScrollToY(toY);\n\t}\n}\n\nvoid HistoryWidget::startItemRevealAnimations() {\n\tfor (const auto &item : base::take(_itemRevealPending)) {\n\t\tif (const auto view = item->mainView()) {\n\t\t\tif (const auto top = _list->itemTop(view); top >= 0) {\n\t\t\t\tif (const auto height = view->height()) {\n\t\t\t\t\tstartMessageSendingAnimation(item);\n\t\t\t\t\tif (!_itemRevealAnimations.contains(item)) {\n\t\t\t\t\t\tauto &animation = _itemRevealAnimations[item];\n\t\t\t\t\t\tanimation.startHeight = height;\n\t\t\t\t\t\t_itemsRevealHeight += height;\n\t\t\t\t\t\tanimation.animation.start(\n\t\t\t\t\t\t\t[=] { revealItemsCallback(); },\n\t\t\t\t\t\t\t0.,\n\t\t\t\t\t\t\t1.,\n\t\t\t\t\t\t\tst::itemRevealDuration,\n\t\t\t\t\t\t\tanim::easeOutCirc);\n\t\t\t\t\t\tif (item->out() || _history->peer->isSelf()) {\n\t\t\t\t\t\t\t_list->theme()->rotateComplexGradientBackground();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid HistoryWidget::startMessageSendingAnimation(\n\t\tnot_null<HistoryItem*> item) {\n\tif (_list->elementChatMode() == HistoryView::ElementChatMode::Default\n\t\t&& width() > st::columnMaximalWidthLeft\n\t\t&& !item->media()) {\n\t\treturn;\n\t}\n\tauto &sendingAnimation = controller()->sendingAnimation();\n\tif (!sendingAnimation.checkExpectedType(item)) {\n\t\treturn;\n\t}\n\tAssert(item->mainView() != nullptr);\n\n\tauto globalEndTopLeft = rpl::merge(\n\t\t_scroll->innerResizes() | rpl::to_empty,\n\t\tsession().data().newItemAdded() | rpl::to_empty,\n\t\tgeometryValue() | rpl::to_empty,\n\t\t_scroll->geometryValue() | rpl::to_empty,\n\t\t_list->geometryValue() | rpl::to_empty\n\t) | rpl::map([=]() -> std::optional<QPoint> {\n\t\tconst auto view = item->mainView();\n\t\tconst auto top = view ? _list->itemTop(view) : -1;\n\t\tif (top < 0) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\tconst auto additional = (_list->height() == _scroll->height())\n\t\t\t? view->height()\n\t\t\t: 0;\n\t\treturn _list->mapToGlobal(QPoint(0, top - additional));\n\t});\n\n\tsendingAnimation.startAnimation({\n\t\t.globalEndTopLeft = std::move(globalEndTopLeft),\n\t\t.view = [=] { return item->mainView(); },\n\t\t.paintContext = [=] { return _list->preparePaintContext({}); },\n\t});\n}\n\nvoid HistoryWidget::updateListSize() {\n\tExpects(_list != nullptr);\n\n\t_list->recountHistoryGeometry(!_historyInited);\n\tauto washidden = _scroll->isHidden();\n\tif (washidden) {\n\t\t_scroll->show();\n\t}\n\tstartItemRevealAnimations();\n\t_list->setItemsRevealHeight(_itemsRevealHeight);\n\t_list->updateSize();\n\tif (washidden) {\n\t\t_scroll->hide();\n\t}\n\t_updateHistoryGeometryRequired = true;\n}\n\nbool HistoryWidget::hasPendingResizedItems() const {\n\tif (!_list) {\n\t\t// Based on the crash reports there is a codepath (at least on macOS)\n\t\t// that leads from _list = _scroll->setOwnedWidget(...) right into\n\t\t// the HistoryWidget::paintEvent (by sending fake mouse move events\n\t\t// inside scroll area -> hiding tooltip window -> exposing the main\n\t\t// window -> syncing it backing store synchronously).\n\t\t//\n\t\t// So really we could get here with !_list && (_history != nullptr).\n\t\treturn false;\n\t}\n\treturn (_history && _history->hasPendingResizedItems())\n\t\t|| (_migrated && _migrated->hasPendingResizedItems());\n}\n\nstd::optional<int> HistoryWidget::unreadBarTop() const {\n\tconst auto bar = [&]() -> HistoryView::Element* {\n\t\tif (const auto bar = _migrated ? _migrated->unreadBar() : nullptr) {\n\t\t\treturn bar;\n\t\t}\n\t\treturn _history->unreadBar();\n\t}();\n\tif (bar) {\n\t\tconst auto result = _list->itemTop(bar)\n\t\t\t+ HistoryView::UnreadBar::marginTop();\n\t\tif (bar->Has<HistoryView::DateBadge>()) {\n\t\t\treturn result + bar->Get<HistoryView::DateBadge>()->height();\n\t\t}\n\t\treturn result;\n\t}\n\treturn std::nullopt;\n}\n\nvoid HistoryWidget::addMessagesToFront(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QVector<MTPMessage> &messages) {\n\t_list->messagesReceived(peer, messages);\n\tif (!_firstLoadRequest) {\n\t\tupdateHistoryGeometry();\n\t\tupdateBotKeyboard();\n\t}\n}\n\nvoid HistoryWidget::addMessagesToBack(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QVector<MTPMessage> &messages) {\n\tconst auto checkForUnreadStart = [&] {\n\t\tif (_history->unreadBar() || !_history->trackUnreadMessages()) {\n\t\t\treturn false;\n\t\t}\n\t\t_history->calculateFirstUnreadMessage();\n\t\treturn !_history->firstUnreadMessage();\n\t}();\n\t_list->messagesReceivedDown(peer, messages);\n\tif (checkForUnreadStart) {\n\t\t_history->calculateFirstUnreadMessage();\n\t\tcreateUnreadBarAndResize();\n\t}\n\tif (!_firstLoadRequest) {\n\t\tupdateHistoryGeometry(false, true, { ScrollChangeNoJumpToBottom, 0 });\n\t}\n\tinjectSponsoredMessages();\n}\n\nvoid HistoryWidget::updateBotKeyboard(History *h, bool force) {\n\tif (h && h != _history && h != _migrated) {\n\t\treturn;\n\t}\n\n\tconst auto wasVisible = _kbShown || _kbReplyTo;\n\tconst auto wasMsgId = _keyboard->forMsgId();\n\tauto changed = false;\n\tif ((_replyTo && !_replyEditMsg) || _editMsgId || !_history) {\n\t\tchanged = _keyboard->updateMarkup(nullptr, force);\n\t} else if (_replyTo && _replyEditMsg) {\n\t\tchanged = _keyboard->updateMarkup(_replyEditMsg, force);\n\t} else {\n\t\tconst auto keyboardItem = _history->lastKeyboardId\n\t\t\t? session().data().message(\n\t\t\t\t_history->peer,\n\t\t\t\t_history->lastKeyboardId)\n\t\t\t: nullptr;\n\t\tchanged = _keyboard->updateMarkup(keyboardItem, force);\n\t}\n\tconst auto controlsChanged = updateCmdStartShown();\n\tif (!changed) {\n\t\tif (controlsChanged) {\n\t\t\tupdateControlsGeometry();\n\t\t}\n\t\treturn;\n\t} else if (_keyboard->forMsgId() != wasMsgId) {\n\t\t_kbScroll->scrollTo({ 0, 0 });\n\t}\n\n\tconst auto hasMarkup = _keyboard->hasMarkup();\n\tconst auto forceReply = _keyboard->forceReply()\n\t\t&& (!_replyTo || !_replyEditMsg);\n\tif (hasMarkup || forceReply) {\n\t\tif (_keyboard->singleUse()\n\t\t\t&& _keyboard->hasMarkup()\n\t\t\t&& (_keyboard->forMsgId()\n\t\t\t\t== FullMsgId(_history->peer->id, _history->lastKeyboardId))\n\t\t\t&& _history->lastKeyboardUsed) {\n\t\t\t_history->lastKeyboardHiddenId = _history->lastKeyboardId;\n\t\t}\n\t\tif (!isSearching()\n\t\t\t&& !isBotStart()\n\t\t\t&& !isBlocked()\n\t\t\t&& _canSendMessages\n\t\t\t&& (wasVisible\n\t\t\t\t|| (_replyTo && _replyEditMsg)\n\t\t\t\t|| (!HasSendText(_field) && !kbWasHidden()))) {\n\t\t\tif (!_showAnimation) {\n\t\t\t\tif (hasMarkup) {\n\t\t\t\t\t_kbScroll->show();\n\t\t\t\t\t_tabbedSelectorToggle->hide();\n\t\t\t\t\tshowKeyboardHideButton();\n\t\t\t\t} else {\n\t\t\t\t\t_kbScroll->hide();\n\t\t\t\t\t_tabbedSelectorToggle->show();\n\t\t\t\t\t_botKeyboardHide->hide();\n\t\t\t\t}\n\t\t\t\t_botKeyboardShow->hide();\n\t\t\t\t_botCommandStart->hide();\n\t\t\t}\n\t\t\tconst auto maxheight = computeMaxFieldHeight();\n\t\t\tconst auto kbheight = hasMarkup\n\t\t\t\t? qMin(_keyboard->height(), maxheight - (maxheight / 2))\n\t\t\t\t: 0;\n\t\t\t_field->setMaxHeight(maxheight - kbheight);\n\t\t\t_kbShown = hasMarkup;\n\t\t\t_kbReplyTo = (_peer->isChat()\n\t\t\t\t\t|| _peer->isChannel()\n\t\t\t\t\t|| _keyboard->forceReply())\n\t\t\t\t? session().data().message(_keyboard->forMsgId())\n\t\t\t\t: nullptr;\n\t\t\tif (_kbReplyTo && !_replyTo) {\n\t\t\t\tupdateReplyToName();\n\t\t\t\tupdateReplyEditText(_kbReplyTo);\n\t\t\t}\n\t\t} else {\n\t\t\tif (!_showAnimation) {\n\t\t\t\t_kbScroll->hide();\n\t\t\t\t_tabbedSelectorToggle->show();\n\t\t\t\t_botKeyboardHide->hide();\n\t\t\t\t_botKeyboardShow->show();\n\t\t\t\t_botCommandStart->hide();\n\t\t\t}\n\t\t\t_field->setMaxHeight(computeMaxFieldHeight());\n\t\t\t_kbShown = false;\n\t\t\t_kbReplyTo = nullptr;\n\t\t\tif (!readyToForward()\n\t\t\t\t&& !_previewDrawPreview\n\t\t\t\t&& !_replyTo\n\t\t\t\t&& !_suggestOptions) {\n\t\t\t\t_fieldBarCancel->hide();\n\t\t\t\tupdateMouseTracking();\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (!_scroll->isHidden()) {\n\t\t\t_kbScroll->hide();\n\t\t\t_tabbedSelectorToggle->show();\n\t\t\t_botKeyboardHide->hide();\n\t\t\t_botKeyboardShow->hide();\n\t\t\t_botCommandStart->setVisible(!_editMsgId);\n\t\t}\n\t\t_field->setMaxHeight(computeMaxFieldHeight());\n\t\t_kbShown = false;\n\t\t_kbReplyTo = nullptr;\n\t\tif (!readyToForward()\n\t\t\t&& !_previewDrawPreview\n\t\t\t&& !_replyTo\n\t\t\t&& !_editMsgId\n\t\t\t&& !_suggestOptions) {\n\t\t\t_fieldBarCancel->hide();\n\t\t\tupdateMouseTracking();\n\t\t}\n\t}\n\trefreshTopBarActiveChat();\n\tupdateFieldPlaceholder();\n\tupdateControlsGeometry();\n\tupdate();\n}\n\nvoid HistoryWidget::botCallbackSent(not_null<HistoryItem*> item) {\n\tif (!item->isRegular() || _peer != item->history()->peer) {\n\t\treturn;\n\t}\n\n\tconst auto keyId = _keyboard->forMsgId();\n\tconst auto lastKeyboardUsed = (keyId == FullMsgId(_peer->id, item->id))\n\t\t&& (keyId == FullMsgId(_peer->id, _history->lastKeyboardId));\n\n\tsession().data().requestItemRepaint(item);\n\n\tif (_replyTo.messageId == item->fullId()) {\n\t\tcancelReply();\n\t}\n\tif (_keyboard->singleUse()\n\t\t&& _keyboard->hasMarkup()\n\t\t&& lastKeyboardUsed) {\n\t\tif (_kbShown) {\n\t\t\ttoggleKeyboard(false);\n\t\t}\n\t\t_history->lastKeyboardUsed = true;\n\t}\n}\n\nint HistoryWidget::computeMaxFieldHeight() const {\n\tconst auto available = height()\n\t\t- _topBar->height()\n\t\t- (_paysStatus ? _paysStatus->bar().height() : 0)\n\t\t- (_contactStatus ? _contactStatus->bar().height() : 0)\n\t\t- (_businessBotStatus ? _businessBotStatus->bar().height() : 0)\n\t\t- (_sponsoredMessageBar ? _sponsoredMessageBar->height() : 0)\n\t\t- (_pinnedBar ? _pinnedBar->height() : 0)\n\t\t- (_groupCallBar ? _groupCallBar->height() : 0)\n\t\t- (_requestsBar ? _requestsBar->height() : 0)\n\t\t- ((_editMsgId\n\t\t\t|| replyTo()\n\t\t\t|| readyToForward()\n\t\t\t|| _previewDrawPreview)\n\t\t\t? st::historyReplyHeight\n\t\t\t: 0)\n\t\t- (2 * st::historySendPadding)\n\t\t- st::historyReplyHeight; // at least this height for history.\n\treturn std::min(st::historyComposeFieldMaxHeight, available);\n}\n\nbool HistoryWidget::cornerButtonsIgnoreVisibility() {\n\treturn _showAnimation != nullptr;\n}\n\nstd::optional<bool> HistoryWidget::cornerButtonsDownShown() {\n\tif (!_list || _firstLoadRequest) {\n\t\treturn false;\n\t}\n\tif (_voiceRecordBar->isLockPresent()\n\t\t|| _voiceRecordBar->isTTLButtonShown()) {\n\t\treturn false;\n\t}\n\tif (!_history->loadedAtBottom() || _cornerButtons.replyReturn()) {\n\t\treturn true;\n\t}\n\tconst auto top = _scroll->scrollTop() + st::historyToDownShownAfter;\n\tif (top < _scroll->scrollTopMax()) {\n\t\treturn true;\n\t}\n\n\tconst auto haveUnreadBelowBottom = [&](History *history) {\n\t\tif (!_list\n\t\t\t|| !history\n\t\t\t|| history->unreadCount() <= 0\n\t\t\t|| !history->trackUnreadMessages()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto unread = history->firstUnreadMessage();\n\t\tif (!unread) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto top = _list->itemTop(unread);\n\t\treturn (top >= _scroll->scrollTop() + _scroll->height());\n\t};\n\tif (haveUnreadBelowBottom(_history)\n\t\t|| haveUnreadBelowBottom(_migrated)) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool HistoryWidget::cornerButtonsUnreadMayBeShown() {\n\treturn !_firstLoadRequest && !_voiceRecordBar->isLockPresent();\n}\n\nbool HistoryWidget::cornerButtonsHas(HistoryView::CornerButtonType type) {\n\treturn true;\n}\n\nvoid HistoryWidget::mousePressEvent(QMouseEvent *e) {\n\tif (!_list) {\n\t\t// Remove focus from the chats list search.\n\t\tsetFocus();\n\n\t\t// Set it back to the chats list so that typing filter chats.\n\t\tcontroller()->widget()->setInnerFocus();\n\t\treturn;\n\t}\n\tconst auto isReadyToForward = readyToForward();\n\tif (_editMsgId\n\t\t&& (_inDetails || _inPhotoEdit)\n\t\t&& (e->button() == Qt::RightButton)) {\n\t\t_mediaEditManager.showMenu(\n\t\t\t_list,\n\t\t\t[=] { mouseMoveEvent(nullptr); },\n\t\t\tHasSendText(_field));\n\t} else if (_inPhotoEdit && _photoEditMedia) {\n\t\tEditCaptionBox::StartPhotoEdit(\n\t\t\tcontroller(),\n\t\t\t_photoEditMedia,\n\t\t\t{ _history->peer->id, _editMsgId },\n\t\t\t_field->getTextWithTags(),\n\t\t\tsuggestOptions(),\n\t\t\t_mediaEditManager.spoilered(),\n\t\t\t_mediaEditManager.invertCaption(),\n\t\t\tcrl::guard(_list, [=] { cancelEdit(); }));\n\t} else if (!_inDetails) {\n\t\treturn;\n\t} else if (_previewDrawPreview) {\n\t\teditDraftOptions();\n\t} else if (_editMsgId) {\n\t\tif (_suggestOptions) {\n\t\t\t_suggestOptions->edit();\n\t\t} else {\n\t\t\tcontroller()->showPeerHistory(\n\t\t\t\t_peer,\n\t\t\t\tWindow::SectionShow::Way::Forward,\n\t\t\t\t_editMsgId);\n\t\t}\n\t} else if (_replyTo\n\t\t&& ((e->modifiers() & Qt::ControlModifier)\n\t\t\t|| (e->button() != Qt::LeftButton))) {\n\t\tjumpToReply(_replyTo);\n\t} else if (_replyTo\n\t\t|| (isReadyToForward && e->button() == Qt::LeftButton)) {\n\t\teditDraftOptions();\n\t} else if (isReadyToForward) {\n\t\t_forwardPanel->editToNextOption();\n\t} else if (_kbReplyTo) {\n\t\tcontroller()->showPeerHistory(\n\t\t\t_kbReplyTo->history()->peer->id,\n\t\t\tWindow::SectionShow::Way::Forward,\n\t\t\t_kbReplyTo->id);\n\t} else if (_suggestOptions) {\n\t\t_suggestOptions->edit();\n\t}\n}\n\nvoid HistoryWidget::editDraftOptions() {\n\tExpects(_history != nullptr);\n\n\tconst auto history = _history;\n\tconst auto reply = _replyTo;\n\tconst auto suggest = suggestOptions();\n\tconst auto webpage = _preview->draft();\n\tconst auto forward = _forwardPanel->draft();\n\n\tconst auto done = [=](\n\t\t\tFullReplyTo replyTo,\n\t\t\tData::WebPageDraft webpage,\n\t\t\tData::ForwardDraft forward) {\n\t\tif (replyTo) {\n\t\t\treplyToMessage(replyTo);\n\t\t} else {\n\t\t\tcancelReply();\n\t\t}\n\t\thistory->setForwardDraft(MsgId(), PeerId(), std::move(forward));\n\t\t_preview->apply(webpage);\n\t};\n\tconst auto replyToId = reply.messageId;\n\tconst auto highlight = crl::guard(this, [=](FullReplyTo to) {\n\t\tjumpToReply(to);\n\t});\n\n\tusing namespace HistoryView::Controls;\n\tEditDraftOptions({\n\t\t.show = controller()->uiShow(),\n\t\t.history = history,\n\t\t.draft = Data::Draft(_field, reply, suggest, _preview->draft()),\n\t\t.usedLink = _preview->link(),\n\t\t.forward = _forwardPanel->draft(),\n\t\t.links = _preview->links(),\n\t\t.resolver = _preview->resolver(),\n\t\t.done = done,\n\t\t.highlight = highlight,\n\t\t.clearOldDraft = [=] {\n\t\t\tClearDraftReplyTo(history, MsgId(), PeerId(), replyToId);\n\t\t},\n\t});\n}\n\nvoid HistoryWidget::jumpToReply(FullReplyTo to) {\n\tif (const auto item = session().data().message(to.messageId)) {\n\t\tJumpToMessageClickHandler(item, {}, to.highlight())->onClick({});\n\t}\n}\n\nvoid HistoryWidget::keyPressEvent(QKeyEvent *e) {\n\tif (!_history) return;\n\n\tconst auto commonModifiers = e->modifiers() & kCommonModifiers;\n\tif (e->key() == Qt::Key_Escape) {\n\t\tif (hasFocus()) {\n\t\t\tescape();\n\t\t} else {\n\t\t\te->ignore();\n\t\t}\n\t} else if (e->key() == Qt::Key_Back) {\n\t\t_cancelRequests.fire({});\n\t} else if (e->key() == Qt::Key_PageDown) {\n\t\t_scroll->keyPressEvent(e);\n\t} else if (e->key() == Qt::Key_PageUp) {\n\t\t_scroll->keyPressEvent(e);\n\t} else if (e->key() == Qt::Key_Down && !commonModifiers) {\n\t\t_scroll->keyPressEvent(e);\n\t} else if (e->key() == Qt::Key_Up && !commonModifiers) {\n\t\tif (!_field->empty()\n\t\t\t|| !canWriteMessage()\n\t\t\t|| _editMsgId\n\t\t\t|| _replyTo) {\n\t\t\t_scroll->keyPressEvent(e);\n\t\t} else {\n\t\t\tconst auto last = _history->lastMessage();\n\t\t\tif (last && last->isLocal()) {\n\t\t\t\tif (last->media() && last->media()->allowsEdit()) {\n\t\t\t\t\tif (const auto view = last->mainView()) {\n\t\t\t\t\t\tcontroller()->show(Box(Ui::EditCaptionBox, view));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t_scroll->keyPressEvent(e);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto item = _history\n\t\t\t\t? _history->lastEditableMessage()\n\t\t\t\t: nullptr;\n\t\t\tif (item) {\n\t\t\t\teditMessage(item, {});\n\t\t\t} else {\n\t\t\t\t_scroll->keyPressEvent(e);\n\t\t\t}\n\t\t}\n\t} else if (e->key() == Qt::Key_Up\n\t\t&& commonModifiers == Qt::ControlModifier) {\n\t\tif (!replyToPreviousMessage()) {\n\t\t\te->ignore();\n\t\t}\n\t} else if (e->key() == Qt::Key_Down\n\t\t&& commonModifiers == Qt::ControlModifier) {\n\t\tif (!replyToNextMessage()) {\n\t\t\te->ignore();\n\t\t}\n\t} else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {\n\t\tif (!_botStart->isHidden()) {\n\t\t\tsendBotStartCommand();\n\t\t}\n\t\tif (!_canSendMessages) {\n\t\t\tconst auto submitting = Ui::InputField::ShouldSubmit(\n\t\t\t\tCore::App().settings().sendSubmitWay(),\n\t\t\t\te->modifiers());\n\t\t\tif (submitting) {\n\t\t\t\tsendWithModifiers(e->modifiers());\n\t\t\t}\n\t\t}\n\t} else if ((e->key() == Qt::Key_O)\n\t\t&& (e->modifiers() == Qt::ControlModifier)) {\n\t\tchooseAttach();\n\t} else {\n\t\te->ignore();\n\t}\n}\n\nvoid HistoryWidget::handlePeerMigration() {\n\tconst auto current = _peer->migrateToOrMe();\n\tconst auto chat = current->migrateFrom();\n\tif (!chat) {\n\t\treturn;\n\t}\n\tconst auto channel = current->asChannel();\n\tAssert(channel != nullptr);\n\n\tif (_peer != channel) {\n\t\tshowHistory(\n\t\t\tchannel->id,\n\t\t\t(_showAtMsgId > 0) ? (-_showAtMsgId) : _showAtMsgId);\n\t\tchannel->session().api().chatParticipants().requestCountDelayed(\n\t\t\tchannel);\n\t} else {\n\t\t_migrated = _history->migrateFrom();\n\t\t_list->notifyMigrateUpdated();\n\t\tsetupPinnedTracker();\n\t\tsetupGroupCallBar();\n\t\tsetupRequestsBar();\n\t\tupdateHistoryGeometry();\n\t}\n\tconst auto from = chat->owner().historyLoaded(chat);\n\tconst auto to = channel->owner().historyLoaded(channel);\n\tif (from\n\t\t&& to\n\t\t&& !from->isEmpty()\n\t\t&& (!from->loadedAtBottom() || !to->loadedAtTop())) {\n\t\tfrom->clear(History::ClearType::Unload);\n\t}\n}\n\nbool HistoryWidget::replyToPreviousMessage() {\n\tif (!_history\n\t\t|| _editMsgId\n\t\t|| _history->isForum()\n\t\t|| (_replyTo && _replyTo.messageId.peer != _history->peer->id)) {\n\t\treturn false;\n\t}\n\tconst auto fullId = FullMsgId(\n\t\t_history->peer->id,\n\t\t(_field->isVisible()\n\t\t\t? _replyTo.messageId.msg\n\t\t\t: _highlighter.latestSingleHighlightedMsgId()));\n\tconst auto skipLocal = [](HistoryView::Element *from) {\n\t\tauto view = from;\n\t\twhile (view && view->data()->isLocal()) {\n\t\t\tview = view->previousDisplayedInBlocks();\n\t\t}\n\t\treturn view;\n\t};\n\tif (const auto item = session().data().message(fullId)) {\n\t\tif (const auto view = item->mainView()) {\n\t\t\tif (const auto target = skipLocal(\n\t\t\t\t\tview->previousDisplayedInBlocks())) {\n\t\t\t\tconst auto previous = target->data();\n\t\t\t\tcontroller()->showMessage(previous);\n\t\t\t\tif (_field->isVisible()) {\n\t\t\t\t\treplyToMessage(previous);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t} else if (const auto target = skipLocal(\n\t\t\t_history->findLastDisplayed())) {\n\t\tconst auto previous = target->data();\n\t\tcontroller()->showMessage(previous);\n\t\tif (_field->isVisible()) {\n\t\t\treplyToMessage(previous);\n\t\t}\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool HistoryWidget::replyToNextMessage() {\n\tif (!_history\n\t\t|| _editMsgId\n\t\t|| _history->isForum()\n\t\t|| (_replyTo && _replyTo.messageId.peer != _history->peer->id)) {\n\t\treturn false;\n\t}\n\tconst auto fullId = FullMsgId(\n\t\t_history->peer->id,\n\t\t(_field->isVisible()\n\t\t\t? _replyTo.messageId.msg\n\t\t\t: _highlighter.latestSingleHighlightedMsgId()));\n\tif (const auto item = session().data().message(fullId)) {\n\t\tif (const auto view = item->mainView()) {\n\t\t\tauto next = view->nextDisplayedInBlocks();\n\t\t\twhile (next && next->data()->isLocal()) {\n\t\t\t\tnext = next->nextDisplayedInBlocks();\n\t\t\t}\n\t\t\tif (next) {\n\t\t\t\tconst auto item = next->data();\n\t\t\t\tcontroller()->showMessage(item);\n\t\t\t\tif (_field->isVisible()) {\n\t\t\t\t\treplyToMessage(item);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t_highlighter.clear();\n\t\t\t\tcancelReply(false);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool HistoryWidget::showSlowmodeError() {\n\tconst auto text = [&] {\n\t\tif (const auto left = _peer->slowmodeSecondsLeft()) {\n\t\t\treturn tr::lng_slowmode_enabled(\n\t\t\t\ttr::now,\n\t\t\t\tlt_left,\n\t\t\t\tUi::FormatDurationWordsSlowmode(left));\n\t\t} else if (_peer->slowmodeApplied()) {\n\t\t\tif (const auto item = _history->latestSendingMessage()) {\n\t\t\t\tif (item->mainView()) {\n\t\t\t\t\tanimatedScrollToItem(item->id);\n\t\t\t\t\tenqueueMessageHighlight({ item });\n\t\t\t\t}\n\t\t\t\treturn tr::lng_slowmode_no_many(tr::now);\n\t\t\t}\n\t\t}\n\t\treturn QString();\n\t}();\n\tif (text.isEmpty()) {\n\t\treturn false;\n\t}\n\tcontroller()->showToast(text);\n\treturn true;\n}\n\nvoid HistoryWidget::sendInlineResult(InlineBots::ResultSelected result) {\n\tif (!_peer || !_canSendMessages) {\n\t\treturn;\n\t} else if (showSlowmodeError()) {\n\t\treturn;\n\t} else if (const auto error = result.result->getErrorOnSend(_history)) {\n\t\tData::ShowSendErrorToast(controller(), _peer, error);\n\t\treturn;\n\t}\n\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = result;\n\t\tcopy.options.starsApproved = approved;\n\t\tsendInlineResult(copy);\n\t};\n\n\tauto action = prepareSendAction(result.options);\n\taction.generateLocal = true;\n\n\tconst auto checked = checkSendPayment(\n\t\t1,\n\t\taction.options,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn;\n\t}\n\n\tcontroller()->sendingAnimation().appendSending(\n\t\tresult.messageSendingFrom);\n\tsession().api().sendInlineResult(\n\t\tresult.bot,\n\t\tresult.result.get(),\n\t\taction,\n\t\tresult.messageSendingFrom.localId);\n\n\tclearFieldText();\n\tsaveDraftWithTextNow();\n\n\tauto &bots = cRefRecentInlineBots();\n\tconst auto index = bots.indexOf(result.bot);\n\tif (index) {\n\t\tif (index > 0) {\n\t\t\tbots.removeAt(index);\n\t\t} else if (bots.size() >= RecentInlineBotsLimit) {\n\t\t\tbots.resize(RecentInlineBotsLimit - 1);\n\t\t}\n\t\tbots.push_front(result.bot);\n\t\tsession().local().writeRecentHashtagsAndBots();\n\t}\n\n\thideSelectorControlsAnimated();\n\n\tsetInnerFocus();\n}\n\nvoid HistoryWidget::updatePinnedViewer() {\n\tif (_firstLoadRequest\n\t\t|| _delayedShowAtRequest\n\t\t|| _scroll->isHidden()\n\t\t|| !_history\n\t\t|| !_historyInited\n\t\t|| !_pinnedTracker) {\n\t\treturn;\n\t}\n\tconst auto visibleBottom = _scroll->scrollTop() + _scroll->height();\n\tauto [view, offset] = _list->findViewForPinnedTracking(visibleBottom);\n\tconst auto lessThanId = !view\n\t\t? (ServerMaxMsgId - 1)\n\t\t: (view->history() != _history)\n\t\t? (view->data()->id + (offset > 0 ? 1 : 0) - ServerMaxMsgId)\n\t\t: (view->data()->id + (offset > 0 ? 1 : 0));\n\tconst auto lastClickedId = !_pinnedClickedId\n\t\t? (ServerMaxMsgId - 1)\n\t\t: (!_migrated || peerIsChannel(_pinnedClickedId.peer))\n\t\t? _pinnedClickedId.msg\n\t\t: (_pinnedClickedId.msg - ServerMaxMsgId);\n\tif (_pinnedClickedId\n\t\t&& lessThanId <= lastClickedId\n\t\t&& !_scrollToAnimation.animating()) {\n\t\t_pinnedClickedId = FullMsgId();\n\t}\n\tif (_pinnedClickedId && !_minPinnedId) {\n\t\t_minPinnedId = Data::ResolveMinPinnedId(\n\t\t\t_peer,\n\t\t\tMsgId(0), // topicRootId\n\t\t\tPeerId(0), // monoforumPeerId\n\t\t\t_migrated ? _migrated->peer.get() : nullptr);\n\t}\n\tif (_pinnedClickedId\n\t\t&& _minPinnedId\n\t\t&& (_minPinnedId >= _pinnedClickedId)) {\n\t\t// After click on the last pinned message we should the top one.\n\t\t_pinnedTracker->trackAround(ServerMaxMsgId - 1);\n\t} else {\n\t\t_pinnedTracker->trackAround(std::min(lessThanId, lastClickedId));\n\t}\n}\n\nvoid HistoryWidget::checkLastPinnedClickedIdReset(\n\t\tint wasScrollTop,\n\t\tint nowScrollTop) {\n\tif (_firstLoadRequest\n\t\t|| _delayedShowAtRequest\n\t\t|| _scroll->isHidden()\n\t\t|| !_history\n\t\t|| !_historyInited) {\n\t\treturn;\n\t}\n\tif (wasScrollTop < nowScrollTop && _pinnedClickedId) {\n\t\t// User scrolled down.\n\t\t_pinnedClickedId = FullMsgId();\n\t\t_minPinnedId = std::nullopt;\n\t\tupdatePinnedViewer();\n\t}\n}\n\nvoid HistoryWidget::setupTranslateBar() {\n\tExpects(_history != nullptr);\n\n\t_translateBar = std::make_unique<HistoryView::TranslateBar>(\n\t\t_topBars.get(),\n\t\tcontroller(),\n\t\t_history);\n\n\tcontroller()->adaptive().oneColumnValue(\n\t) | rpl::on_next([=, raw = _translateBar.get()](bool one) {\n\t\traw->setShadowGeometryPostprocess([=](QRect geometry) {\n\t\t\tif (!one) {\n\t\t\t\tgeometry.setLeft(geometry.left() + st::lineWidth);\n\t\t\t}\n\t\t\treturn geometry;\n\t\t});\n\t}, _translateBar->lifetime());\n\n\t_translateBarHeight = 0;\n\t_translateBar->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\t_topDelta = _preserveScrollTop ? 0 : (height - _translateBarHeight);\n\t\t_translateBarHeight = height;\n\t\tupdateHistoryGeometry();\n\t\tupdateControlsGeometry();\n\t\t_topDelta = 0;\n\t}, _translateBar->lifetime());\n\n\torderWidgets();\n}\n\nvoid HistoryWidget::setupPinnedTracker() {\n\tExpects(_history != nullptr);\n\n\t_pinnedTracker = std::make_unique<HistoryView::PinnedTracker>(_history);\n\t_pinnedBar = nullptr;\n\tcheckPinnedBarState();\n}\n\nvoid HistoryWidget::checkPinnedBarState() {\n\tExpects(_pinnedTracker != nullptr);\n\tExpects(_list != nullptr);\n\n\tconst auto hiddenId = _peer->canPinMessages()\n\t\t? MsgId(0)\n\t\t: session().settings().hiddenPinnedMessageId(_peer->id);\n\tconst auto currentPinnedId = Data::ResolveTopPinnedId(\n\t\t_peer,\n\t\tMsgId(0), // topicRootId\n\t\tPeerId(0), // monoforumPeerId\n\t\t_migrated ? _migrated->peer.get() : nullptr);\n\tconst auto universalPinnedId = !currentPinnedId\n\t\t? int32(0)\n\t\t: (_migrated && !peerIsChannel(currentPinnedId.peer))\n\t\t? (currentPinnedId.msg - ServerMaxMsgId)\n\t\t: currentPinnedId.msg;\n\tif (universalPinnedId == hiddenId) {\n\t\tif (_pinnedBar) {\n\t\t\t_pinnedBar->setContent(rpl::single(Ui::MessageBarContent()));\n\t\t\t_pinnedTracker->reset();\n\t\t\t_list->setShownPinned(nullptr);\n\t\t\t_hidingPinnedBar = base::take(_pinnedBar);\n\t\t\tconst auto raw = _hidingPinnedBar.get();\n\t\t\tbase::call_delayed(st::defaultMessageBar.duration, this, [=] {\n\t\t\t\tif (_hidingPinnedBar.get() == raw) {\n\t\t\t\t\tclearHidingPinnedBar();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\treturn;\n\t}\n\tif (_pinnedBar || !universalPinnedId) {\n\t\treturn;\n\t}\n\n\tclearHidingPinnedBar();\n\t_pinnedBar = std::make_unique<Ui::PinnedBar>(_topBars.get(), [=] {\n\t\treturn controller()->isGifPausedAtLeastFor(\n\t\t\tWindow::GifPauseReason::Any);\n\t}, controller()->gifPauseLevelChanged());\n\tauto pinnedRefreshed = Info::Profile::SharedMediaCountValue(\n\t\t_peer,\n\t\tMsgId(0), // topicRootId\n\t\tPeerId(0), // monoforumPeerId\n\t\tnullptr,\n\t\tStorage::SharedMediaType::Pinned\n\t) | rpl::distinct_until_changed(\n\t) | rpl::map([=](int count) {\n\t\tif (_pinnedClickedId) {\n\t\t\t_pinnedClickedId = FullMsgId();\n\t\t\t_minPinnedId = std::nullopt;\n\t\t\tupdatePinnedViewer();\n\t\t}\n\t\treturn (count > 1);\n\t}) | rpl::distinct_until_changed();\n\tauto customButtonItem = HistoryView::PinnedBarItemWithCustomButton(\n\t\t&session(),\n\t\t_pinnedTracker->shownMessageId());\n\trpl::combine(\n\t\trpl::duplicate(pinnedRefreshed),\n\t\trpl::duplicate(customButtonItem)\n\t) | rpl::on_next([=](bool many, HistoryItem *item) {\n\t\trefreshPinnedBarButton(many, item);\n\t}, _pinnedBar->lifetime());\n\n\t_pinnedBar->setContent(rpl::combine(\n\t\tHistoryView::PinnedBarContent(\n\t\t\t&session(),\n\t\t\t_pinnedTracker->shownMessageId(),\n\t\t\t[bar = _pinnedBar.get()] { bar->customEmojiRepaint(); }),\n\t\tstd::move(pinnedRefreshed),\n\t\tstd::move(customButtonItem)\n\t) | rpl::map([=](Ui::MessageBarContent &&content, bool, HistoryItem*) {\n\t\tconst auto id = (!content.title.isEmpty() || !content.text.empty())\n\t\t\t? _pinnedTracker->currentMessageId().message\n\t\t\t: FullMsgId();\n\t\tif (const auto list = _list.data()) {\n\t\t\t// Sometimes we get here with non-empty content and id of\n\t\t\t// message that is being deleted right now. We get here in\n\t\t\t// the moment when _itemRemoved was already fired (so in\n\t\t\t// the _list the _pinnedItem is already cleared) and the\n\t\t\t// MessageUpdate::Flag::Destroyed being fired right now,\n\t\t\t// so the message is still in Data::Session. So we need to\n\t\t\t// call data().message() async, otherwise we get a nearly-\n\t\t\t// destroyed message from it and save the pointer in _list.\n\t\t\tcrl::on_main(list, [=] {\n\t\t\t\tlist->setShownPinned(session().data().message(id));\n\t\t\t});\n\t\t}\n\t\treturn std::move(content);\n\t}));\n\n\tcontroller()->adaptive().oneColumnValue(\n\t) | rpl::on_next([=, raw = _pinnedBar.get()](bool one) {\n\t\traw->setShadowGeometryPostprocess([=](QRect geometry) {\n\t\t\tif (!one) {\n\t\t\t\tgeometry.setLeft(geometry.left() + st::lineWidth);\n\t\t\t}\n\t\t\treturn geometry;\n\t\t});\n\t}, _pinnedBar->lifetime());\n\n\t_pinnedBar->barClicks(\n\t) | rpl::on_next([=] {\n\t\tconst auto id = _pinnedTracker->currentMessageId();\n\t\tif (const auto item = session().data().message(id.message)) {\n\t\t\tcontroller()->showPeerHistory(\n\t\t\t\titem->history()->peer,\n\t\t\t\tWindow::SectionShow::Way::Forward,\n\t\t\t\titem->id);\n\t\t\tif (const auto group = session().data().groups().find(item)) {\n\t\t\t\t// Hack for the case when a non-first item of an album\n\t\t\t\t// is pinned and we still want the 'show last after first'.\n\t\t\t\t_pinnedClickedId = group->items.front()->fullId();\n\t\t\t} else {\n\t\t\t\t_pinnedClickedId = id.message;\n\t\t\t}\n\t\t\t_minPinnedId = std::nullopt;\n\t\t\tupdatePinnedViewer();\n\t\t}\n\t}, _pinnedBar->lifetime());\n\n\t_pinnedBarHeight = 0;\n\t_pinnedBar->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\t_topDelta = _preserveScrollTop ? 0 : (height - _pinnedBarHeight);\n\t\t_pinnedBarHeight = height;\n\t\tupdateHistoryGeometry();\n\t\tupdateControlsGeometry();\n\t\t_topDelta = 0;\n\t}, _pinnedBar->lifetime());\n\n\torderWidgets();\n}\n\nvoid HistoryWidget::clearHidingPinnedBar() {\n\tif (!_hidingPinnedBar) {\n\t\treturn;\n\t}\n\tif (const auto delta = -_pinnedBarHeight) {\n\t\t_pinnedBarHeight = 0;\n\t\tsetGeometryWithTopMoved(geometry(), delta);\n\t}\n\t_hidingPinnedBar = nullptr;\n}\n\nvoid HistoryWidget::checkMessagesTTL() {\n\tif (!_peer || !_peer->messagesTTL()) {\n\t\tif (_ttlInfo) {\n\t\t\t_ttlInfo = nullptr;\n\t\t\tupdateControlsGeometry();\n\t\t\tupdateControlsVisibility();\n\t\t}\n\t} else if (!_ttlInfo || _ttlInfo->peer() != _peer) {\n\t\t_ttlInfo = std::make_unique<HistoryView::Controls::TTLButton>(\n\t\t\tthis,\n\t\t\tcontroller()->uiShow(),\n\t\t\t_peer);\n\t\torderWidgets();\n\t\tupdateControlsGeometry();\n\t\tupdateControlsVisibility();\n\t}\n}\n\nvoid HistoryWidget::setChooseReportMessagesDetails(\n\t\tData::ReportInput reportInput,\n\t\tFn<void(std::vector<MsgId>)> callback) {\n\tif (!callback) {\n\t\tconst auto refresh = _chooseForReport && _chooseForReport->active;\n\t\t_chooseForReport = nullptr;\n\t\tif (_list) {\n\t\t\t_list->clearChooseReportReason();\n\t\t}\n\t\tif (refresh) {\n\t\t\tclearSelected();\n\t\t\tupdateControlsVisibility();\n\t\t\tupdateControlsGeometry();\n\t\t\tupdateTopBarChooseForReport();\n\t\t}\n\t} else {\n\t\t_chooseForReport = std::make_unique<ChooseMessagesForReport>(\n\t\t\tChooseMessagesForReport{\n\t\t\t\t.reportInput = reportInput,\n\t\t\t\t.callback = std::move(callback) });\n\t}\n}\n\nvoid HistoryWidget::refreshPinnedBarButton(bool many, HistoryItem *item) {\n\tif (!_pinnedBar) {\n\t\treturn; // It can be in process of hiding.\n\t}\n\tconst auto openSection = [=] {\n\t\tconst auto id = _pinnedTracker\n\t\t\t? _pinnedTracker->currentMessageId()\n\t\t\t: HistoryView::PinnedId();\n\t\tif (!id.message) {\n\t\t\treturn;\n\t\t}\n\t\tcontroller()->showSection(\n\t\t\tstd::make_shared<HistoryView::PinnedMemento>(\n\t\t\t\t_history,\n\t\t\t\t((!_migrated || peerIsChannel(id.message.peer))\n\t\t\t\t\t? id.message.msg\n\t\t\t\t\t: (id.message.msg - ServerMaxMsgId))));\n\t};\n\tconst auto context = [copy = _list](FullMsgId itemId) {\n\t\tif (const auto raw = copy.data()) {\n\t\t\treturn raw->prepareClickHandlerContext(itemId);\n\t\t}\n\t\treturn ClickHandlerContext();\n\t};\n\tauto customButton = CreatePinnedBarCustomButton(this, item, context);\n\tif (customButton) {\n\t\tstruct State {\n\t\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t\t};\n\t\tconst auto buttonRaw = customButton.data();\n\t\tconst auto state = buttonRaw->lifetime().make_state<State>();\n\t\t_pinnedBar->contextMenuRequested(\n\t\t) | rpl::on_next([=] {\n\t\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(buttonRaw);\n\t\t\tstate->menu->addAction(\n\t\t\t\ttr::lng_settings_events_pinned(tr::now),\n\t\t\t\topenSection);\n\t\t\tstate->menu->popup(QCursor::pos());\n\t\t}, buttonRaw->lifetime());\n\t\t_pinnedBar->setRightButton(std::move(customButton));\n\t\treturn;\n\t}\n\n\tconst auto close = !many;\n\tauto button = object_ptr<Ui::IconButton>(\n\t\tthis,\n\t\tclose ? st::historyReplyCancel : st::historyPinnedShowAll);\n\tbutton->setAccessibleName(close\n\t\t? tr::lng_cancel(tr::now)\n\t\t: tr::lng_settings_events_pinned(tr::now));\n\tbutton->clicks(\n\t) | rpl::on_next([=] {\n\t\tif (close) {\n\t\t\thidePinnedMessage();\n\t\t} else {\n\t\t\topenSection();\n\t\t}\n\t}, button->lifetime());\n\t_pinnedBar->setRightButton(std::move(button));\n}\n\nvoid HistoryWidget::setupGroupCallBar() {\n\tExpects(_history != nullptr);\n\n\tconst auto peer = _history->peer;\n\tif (!peer->isChannel() && !peer->isChat()) {\n\t\t_groupCallBar = nullptr;\n\t\treturn;\n\t}\n\t_groupCallBar = std::make_unique<Ui::GroupCallBar>(\n\t\t_topBars.get(),\n\t\tHistoryView::GroupCallBarContentByPeer(\n\t\t\tpeer,\n\t\t\tst::historyGroupCallUserpics.size,\n\t\t\tfalse),\n\t\tCore::App().appDeactivatedValue());\n\n\tcontroller()->adaptive().oneColumnValue(\n\t) | rpl::on_next([=](bool one) {\n\t\t_groupCallBar->setShadowGeometryPostprocess([=](QRect geometry) {\n\t\t\tif (!one) {\n\t\t\t\tgeometry.setLeft(geometry.left() + st::lineWidth);\n\t\t\t}\n\t\t\treturn geometry;\n\t\t});\n\t}, _groupCallBar->lifetime());\n\n\trpl::merge(\n\t\t_groupCallBar->barClicks(),\n\t\t_groupCallBar->joinClicks()\n\t) | rpl::on_next([=] {\n\t\tconst auto peer = _history->peer;\n\t\tif (peer->groupCall()) {\n\t\t\tcontroller()->startOrJoinGroupCall(peer, {});\n\t\t}\n\t}, _groupCallBar->lifetime());\n\n\t_groupCallBarHeight = 0;\n\t_groupCallBar->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\t_topDelta = _preserveScrollTop ? 0 : (height - _groupCallBarHeight);\n\t\t_groupCallBarHeight = height;\n\t\tupdateHistoryGeometry();\n\t\tupdateControlsGeometry();\n\t\t_topDelta = 0;\n\t}, _groupCallBar->lifetime());\n\n\torderWidgets();\n}\n\nvoid HistoryWidget::setupRequestsBar() {\n\tExpects(_history != nullptr);\n\n\tconst auto peer = _history->peer;\n\tif (!peer->isChannel() && !peer->isChat()) {\n\t\t_requestsBar = nullptr;\n\t\treturn;\n\t}\n\t_requestsBar = std::make_unique<Ui::RequestsBar>(\n\t\t_topBars.get(),\n\t\tHistoryView::RequestsBarContentByPeer(\n\t\t\tpeer,\n\t\t\tst::historyRequestsUserpics.size,\n\t\t\tfalse));\n\n\tcontroller()->adaptive().oneColumnValue(\n\t) | rpl::on_next([=](bool one) {\n\t\t_requestsBar->setShadowGeometryPostprocess([=](QRect geometry) {\n\t\t\tif (!one) {\n\t\t\t\tgeometry.setLeft(geometry.left() + st::lineWidth);\n\t\t\t}\n\t\t\treturn geometry;\n\t\t});\n\t}, _requestsBar->lifetime());\n\n\t_requestsBar->barClicks(\n\t) | rpl::on_next([=] {\n\t\tRequestsBoxController::Start(controller(), _peer);\n\t}, _requestsBar->lifetime());\n\n\t_requestsBarHeight = 0;\n\t_requestsBar->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\t_topDelta = _preserveScrollTop ? 0 : (height - _requestsBarHeight);\n\t\t_requestsBarHeight = height;\n\t\tupdateHistoryGeometry();\n\t\tupdateControlsGeometry();\n\t\t_topDelta = 0;\n\t}, _requestsBar->lifetime());\n\n\torderWidgets();\n}\n\nvoid HistoryWidget::requestMessageData(MsgId msgId) {\n\tif (!_peer) {\n\t\treturn;\n\t}\n\tconst auto peer = _peer;\n\tconst auto callback = crl::guard(this, [=] {\n\t\tmessageDataReceived(peer, msgId);\n\t});\n\tsession().api().requestMessageData(_peer, msgId, callback);\n}\n\nbool HistoryWidget::checkSponsoredMessageBarVisibility() const {\n\tconst auto h = _list->height()\n\t\t- (_kbScroll->isHidden() ? 0 : _kbScroll->height());\n\treturn (h > _scroll->height());\n}\n\nvoid HistoryWidget::requestSponsoredMessageBar() {\n\tif (!_history || !session().sponsoredMessages().isTopBarFor(_history)) {\n\t\treturn;\n\t}\n\tconst auto checkState = [=, this] {\n\t\tusing State = Data::SponsoredMessages::State;\n\t\tconst auto state = session().sponsoredMessages().state(\n\t\t\t_history);\n\t\t_sponsoredMessagesStateKnown = (state != State::None);\n\t\tif (state == State::AppendToTopBar) {\n\t\t\tcreateSponsoredMessageBar();\n\t\t\tif (checkSponsoredMessageBarVisibility()) {\n\t\t\t\t_sponsoredMessageBar->toggle(true, anim::type::normal);\n\t\t\t} else {\n\t\t\t\tauto &lifetime = _sponsoredMessageBar->lifetime();\n\t\t\t\tconst auto heightLifetime\n\t\t\t\t\t= lifetime.make_state<rpl::lifetime>();\n\t\t\t\t_list->heightValue(\n\t\t\t\t) | rpl::on_next([=, this] {\n\t\t\t\t\tif (_sponsoredMessageBar->toggled()) {\n\t\t\t\t\t\theightLifetime->destroy();\n\t\t\t\t\t} else if (checkSponsoredMessageBarVisibility()) {\n\t\t\t\t\t\t_sponsoredMessageBar->toggle(\n\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\tanim::type::normal);\n\t\t\t\t\t\theightLifetime->destroy();\n\t\t\t\t\t}\n\t\t\t\t}, *heightLifetime);\n\t\t\t}\n\t\t}\n\t};\n\tconst auto history = _history;\n\tsession().sponsoredMessages().request(\n\t\t_history,\n\t\tcrl::guard(this, [=, this] {\n\t\t\tif (history == _history) {\n\t\t\t\tcheckState();\n\t\t\t}\n\t\t}));\n}\n\nvoid HistoryWidget::checkSponsoredMessageBar() {\n\tif (!_history || !session().sponsoredMessages().isTopBarFor(_history)) {\n\t\treturn;\n\t}\n\tconst auto state = session().sponsoredMessages().state(_history);\n\tif (state == Data::SponsoredMessages::State::AppendToTopBar) {\n\t\tif (checkSponsoredMessageBarVisibility()) {\n\t\t\tif (!_sponsoredMessageBar) {\n\t\t\t\tcreateSponsoredMessageBar();\n\t\t\t}\n\t\t\t_sponsoredMessageBar->toggle(true, anim::type::instant);\n\t\t}\n\t}\n}\n\nvoid HistoryWidget::createSponsoredMessageBar() {\n\t_sponsoredMessageBar = base::make_unique_q<Ui::SlideWrap<>>(\n\t\t_topBars.get(),\n\t\tobject_ptr<Ui::RpWidget>(this));\n\n\t_sponsoredMessageBar->entity()->resizeToWidth(_scroll->width());\n\tconst auto maybeFullId = session().sponsoredMessages().fillTopBar(\n\t\t_history,\n\t\t_sponsoredMessageBar->entity());\n\tsession().sponsoredMessages().itemRemoved(\n\t\tmaybeFullId\n\t) | rpl::on_next([this] {\n\t\t_sponsoredMessageBar->toggle(false, anim::type::normal);\n\t\t_sponsoredMessageBar->shownValue() | rpl::filter(\n\t\t\t!rpl::mappers::_1\n\t\t) | rpl::on_next([this] {\n\t\t\t_sponsoredMessageBar = nullptr;\n\t\t}, _sponsoredMessageBar->lifetime());\n\t}, _sponsoredMessageBar->lifetime());\n\n\tif (maybeFullId) {\n\t\tconst auto viewLifetime\n\t\t\t= _sponsoredMessageBar->lifetime().make_state<rpl::lifetime>();\n\t\trpl::combine(\n\t\t\t_sponsoredMessageBar->entity()->heightValue(),\n\t\t\t_sponsoredMessageBar->heightValue()\n\t\t) | rpl::filter(\n\t\t\trpl::mappers::_1 == rpl::mappers::_2\n\t\t) | rpl::on_next([=] {\n\t\t\tsession().sponsoredMessages().view(maybeFullId);\n\t\t\tviewLifetime->destroy();\n\t\t}, *viewLifetime);\n\t}\n\n\t_sponsoredMessageBarHeight = 0;\n\t_sponsoredMessageBar->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\t_topDelta = _preserveScrollTop\n\t\t\t? 0\n\t\t\t: (height - _sponsoredMessageBarHeight);\n\t\t_sponsoredMessageBarHeight = height;\n\t\tupdateHistoryGeometry();\n\t\tupdateControlsGeometry();\n\t\t_topDelta = 0;\n\t}, _sponsoredMessageBar->lifetime());\n\t_sponsoredMessageBar->toggle(false, anim::type::instant);\n}\n\nbool HistoryWidget::sendExistingDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tApi::MessageToSend messageToSend,\n\t\tstd::optional<MsgId> localId) {\n\tconst auto error = _peer\n\t\t? Data::RestrictionError(_peer, ChatRestriction::SendStickers)\n\t\t: Data::SendError();\n\tif (error) {\n\t\tData::ShowSendErrorToast(controller(), _peer, error);\n\t\treturn false;\n\t} else if (!_peer\n\t\t|| !_canSendMessages\n\t\t|| showSlowmodeError()\n\t\t|| ShowSendPremiumError(controller(), document)) {\n\t\treturn false;\n\t}\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = messageToSend;\n\t\tcopy.action.options.starsApproved = approved;\n\t\tsendExistingDocument(document, std::move(copy), localId);\n\t};\n\tconst auto checked = checkSendPayment(\n\t\t1,\n\t\tmessageToSend.action.options,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn false;\n\t}\n\n\tApi::SendExistingDocument(\n\t\tstd::move(messageToSend),\n\t\tdocument,\n\t\tlocalId);\n\n\tif (_autocomplete && _autocomplete->stickersShown()) {\n\t\tclearFieldText();\n\t\t//saveDraftWithTextNow();\n\n\t\t// won't be needed if SendInlineBotResult will clear the cloud draft\n\t\tsaveCloudDraft();\n\t}\n\n\thideSelectorControlsAnimated();\n\n\tsetInnerFocus();\n\treturn true;\n}\n\nbool HistoryWidget::sendExistingPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tApi::SendOptions options) {\n\tconst auto error = _peer\n\t\t? Data::RestrictionError(_peer, ChatRestriction::SendPhotos)\n\t\t: Data::SendError();\n\tif (error) {\n\t\tData::ShowSendErrorToast(controller(), _peer, error);\n\t\treturn false;\n\t} else if (!_peer || !_canSendMessages) {\n\t\treturn false;\n\t} else if (showSlowmodeError()) {\n\t\treturn false;\n\t}\n\tconst auto action = prepareSendAction(options);\n\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = options;\n\t\tcopy.starsApproved = approved;\n\t\tsendExistingPhoto(photo, copy);\n\t};\n\tconst auto checked = checkSendPayment(\n\t\t1,\n\t\taction.options,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn false;\n\t}\n\n\tApi::SendExistingPhoto(Api::MessageToSend(action), photo);\n\n\thideSelectorControlsAnimated();\n\n\tsetInnerFocus();\n\treturn true;\n}\n\nvoid HistoryWidget::showInfoTooltip(\n\t\tconst TextWithEntities &text,\n\t\tFn<void()> hiddenCallback) {\n\t_topToast.show(\n\t\t_scroll.data(),\n\t\t&session(),\n\t\ttext,\n\t\tstd::move(hiddenCallback));\n}\n\nvoid HistoryWidget::showPremiumStickerTooltip(\n\t\tnot_null<const HistoryView::Element*> view) {\n\tif (const auto media = view->data()->media()) {\n\t\tif (const auto document = media->document()) {\n\t\t\tshowPremiumToast(document);\n\t\t}\n\t}\n}\n\nvoid HistoryWidget::showPremiumToast(not_null<DocumentData*> document) {\n\tif (!_stickerToast) {\n\t\t_stickerToast = std::make_unique<HistoryView::StickerToast>(\n\t\t\tcontroller(),\n\t\t\tthis,\n\t\t\t[=] { _stickerToast = nullptr; });\n\t}\n\t_stickerToast->showFor(document);\n}\n\nvoid HistoryWidget::validateSubsectionTabs() {\n\tif (!_subsectionCheckLifetime\n\t\t&& _history\n\t\t&& _history->peer->isMegagroup()) {\n\t\t_subsectionCheckLifetime = _history->peer->asChannel()->flagsValue(\n\t\t) | rpl::skip(\n\t\t\t1\n\t\t) | rpl::filter([=](Data::Flags<ChannelDataFlags>::Change change) {\n\t\t\tconst auto mask = ChannelDataFlag::Forum\n\t\t\t\t| ChannelDataFlag::ForumTabs\n\t\t\t\t| ChannelDataFlag::MonoforumAdmin;\n\t\t\treturn change.diff & mask;\n\t\t}) | rpl::on_next([=] {\n\t\t\tvalidateSubsectionTabs();\n\t\t});\n\t}\n\tif (!_history || !HistoryView::SubsectionTabs::UsedFor(_history)) {\n\t\tif (_subsectionTabs) {\n\t\t\t_subsectionTabsLifetime.destroy();\n\t\t\t_subsectionTabs = nullptr;\n\t\t\tupdateControlsGeometry();\n\t\t\tif (const auto forum = _history->asForum()) {\n\t\t\t\tcontroller()->showForum(forum, {\n\t\t\t\t\tWindow::SectionShow::Way::Backward,\n\t\t\t\t\tanim::type::normal,\n\t\t\t\t\tanim::activation::background,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\treturn;\n\t} else if (_subsectionTabs) {\n\t\treturn;\n\t}\n\t_subsectionTabs = controller()->restoreSubsectionTabsFor(this, _history);\n\tif (!_subsectionTabs) {\n\t\t_subsectionTabs = std::make_unique<HistoryView::SubsectionTabs>(\n\t\t\tcontroller(),\n\t\t\tthis,\n\t\t\t_history);\n\t}\n\t_subsectionTabs->removeRequests() | rpl::on_next([=] {\n\t\t_subsectionTabsLifetime.destroy();\n\t\t_subsectionTabs = nullptr;\n\t\tupdateControlsGeometry();\n\t}, _subsectionTabsLifetime);\n\t_subsectionTabs->layoutRequests() | rpl::on_next([=] {\n\t\t_list->toggleRemoveFromUserpics(_subsectionTabs->leftSkip() > 0);\n\t\tupdateControlsGeometry();\n\t\torderWidgets();\n\t}, _subsectionTabsLifetime);\n\t_list->toggleRemoveFromUserpics(_subsectionTabs->leftSkip() > 0);\n\tupdateControlsGeometry();\n\torderWidgets();\n}\n\nvoid HistoryWidget::checkCharsCount() {\n\t_fieldCharsCountManager.setCount(Ui::ComputeFieldCharacterCount(_field));\n\tcheckCharsLimitation();\n}\n\nvoid HistoryWidget::checkCharsLimitation() {\n\tif (!_history || !_editMsgId) {\n\t\t_charsLimitation = nullptr;\n\t\treturn;\n\t}\n\tconst auto item = session().data().message(_history->peer, _editMsgId);\n\tif (!item) {\n\t\t_charsLimitation = nullptr;\n\t\treturn;\n\t}\n\tconst auto hasMediaWithCaption = item->media()\n\t\t&& item->media()->allowsEditCaption();\n\tconst auto maxCaptionSize = !hasMediaWithCaption\n\t\t? MaxMessageSize\n\t\t: Data::PremiumLimits(&session()).captionLengthCurrent();\n\tconst auto remove = _fieldCharsCountManager.count() - maxCaptionSize;\n\tif (remove > 0) {\n\t\tif (!_charsLimitation) {\n\t\t\t_charsLimitation = base::make_unique_q<CharactersLimitLabel>(\n\t\t\t\tthis,\n\t\t\t\t_send.get(),\n\t\t\t\tstyle::al_bottom);\n\t\t\t_charsLimitation->show();\n\t\t\tData::AmPremiumValue(\n\t\t\t\t&session()\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tcheckCharsLimitation();\n\t\t\t}, _charsLimitation->lifetime());\n\t\t}\n\t\t_charsLimitation->setLeft(remove);\n\t} else {\n\t\t_charsLimitation = nullptr;\n\t}\n}\n\nvoid HistoryWidget::setFieldText(\n\t\tconst TextWithTags &textWithTags,\n\t\tTextUpdateEvents events,\n\t\tFieldHistoryAction fieldHistoryAction) {\n\t_textUpdateEvents = events;\n\t_field->setTextWithTags(textWithTags, fieldHistoryAction);\n\tauto cursor = _field->textCursor();\n\tcursor.movePosition(QTextCursor::End);\n\t_field->setTextCursor(cursor);\n\t_textUpdateEvents = TextUpdateEvent::SaveDraft\n\t\t| TextUpdateEvent::SendTyping;\n\n\tcheckCharsCount();\n\n\tif (_preview) {\n\t\t_preview->checkNow(false);\n\t}\n}\n\nvoid HistoryWidget::clearFieldText(\n\t\tTextUpdateEvents events,\n\t\tFieldHistoryAction fieldHistoryAction) {\n\tsetFieldText(TextWithTags(), events, fieldHistoryAction);\n}\n\nvoid HistoryWidget::replyToMessage(FullReplyTo id) {\n\tif (const auto item = session().data().message(id.messageId)) {\n\t\tif (CanSendReply(item) && !base::IsCtrlPressed()) {\n\t\t\treplyToMessage(item, id);\n\t\t} else if (item->allowsForward()) {\n\t\t\tconst auto show = controller()->uiShow();\n\t\t\tHistoryView::Controls::ShowReplyToChatBox(show, id);\n\t\t} else {\n\t\t\tcontroller()->showToast(\n\t\t\t\ttr::lng_error_cant_reply_other(tr::now));\n\t\t}\n\t}\n}\n\nvoid HistoryWidget::replyToMessage(\n\t\tnot_null<HistoryItem*> item,\n\t\tFullReplyTo fields) {\n\tif (isJoinChannel()) {\n\t\treturn;\n\t}\n\tfields.messageId = item->fullId();\n\t_processingReplyTo = fields;\n\t_processingReplyItem = item;\n\tprocessReply();\n}\n\nvoid HistoryWidget::processReply() {\n\tconst auto processContinue = [=] {\n\t\treturn crl::guard(_list, [=] {\n\t\t\tif (!_peer || !_processingReplyTo) {\n\t\t\t\treturn;\n\t\t\t} else if (!_processingReplyItem) {\n\t\t\t\t_processingReplyItem = _peer->owner().message(\n\t\t\t\t\t_processingReplyTo.messageId);\n\t\t\t\tif (!_processingReplyItem) {\n\t\t\t\t\t_processingReplyTo = {};\n\t\t\t\t} else {\n\t\t\t\t\tprocessReply();\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t};\n\tconst auto processCancel = [=] {\n\t\t_processingReplyTo = {};\n\t\t_processingReplyItem = nullptr;\n\t};\n\n\tif (!_peer || !_processingReplyTo) {\n\t\treturn processCancel();\n\t}\n\tcancelSuggestPost();\n\tif (!_processingReplyItem) {\n\t\tsession().api().requestMessageData(\n\t\t\tsession().data().peer(_processingReplyTo.messageId.peer),\n\t\t\t_processingReplyTo.messageId.msg,\n\t\t\tprocessContinue());\n\t\treturn;\n#if 0 // Now we can \"reply\" to old legacy group messages.\n\t} else if (_processingReplyItem->history() == _migrated) {\n\t\tif (_processingReplyItem->isService()) {\n\t\t\tcontroller()->showToast(tr::lng_reply_cant(tr::now));\n\t\t} else {\n\t\t\tconst auto itemId = _processingReplyItem->fullId();\n\t\t\tcontroller()->show(\n\t\t\t\tUi::MakeConfirmBox({\n\t\t\t\t\t.text = tr::lng_reply_cant_forward(),\n\t\t\t\t\t.confirmed = crl::guard(this, [=] {\n\t\t\t\t\t\tcontroller()->content()->setForwardDraft(\n\t\t\t\t\t\t\t_history,\n\t\t\t\t\t\t\t{ .ids = { 1, itemId } });\n\t\t\t\t\t}),\n\t\t\t\t\t.confirmText = tr::lng_selected_forward(),\n\t\t\t\t}));\n\t\t}\n\t\treturn processCancel();\n#endif\n\t} else if (!_processingReplyItem->isRegular()) {\n\t\treturn processCancel();\n\t} else if (const auto forum = _peer->forum()\n\t\t; forum && _processingReplyItem->history() == _history) {\n\t\tconst auto topicRootId = _processingReplyItem->topicRootId();\n\t\tusing namespace Data;\n\t\tif (forum->topicDeleted(topicRootId)\n\t\t\t&& !(topicRootId == ForumTopic::kGeneralId && _peer->isBot())) {\n\t\t\treturn processCancel();\n\t\t} else if (const auto topic = forum->topicFor(topicRootId)) {\n\t\t\tif (!Data::CanSendAnything(topic)) {\n\t\t\t\treturn processCancel();\n\t\t\t}\n\t\t} else {\n\t\t\tforum->requestTopic(topicRootId, processContinue());\n\t\t}\n\t} else if (!Data::CanSendAnything(_peer)) {\n\t\treturn processCancel();\n\t}\n\tsetReplyFieldsFromProcessing();\n}\n\nvoid HistoryWidget::setReplyFieldsFromProcessing() {\n\tif (!_processingReplyTo || !_processingReplyItem) {\n\t\treturn;\n\t}\n\n\tif (_composeSearch) {\n\t\t_composeSearch->hideAnimated();\n\t}\n\n\tconst auto id = base::take(_processingReplyTo);\n\tconst auto item = base::take(_processingReplyItem);\n\tif (_editMsgId) {\n\t\tif (const auto localDraft = _history->localDraft({}, {})) {\n\t\t\tlocalDraft->reply = id;\n\t\t\tlocalDraft->suggest = SuggestOptions();\n\t\t} else {\n\t\t\t_history->setLocalDraft(std::make_unique<Data::Draft>(\n\t\t\t\tTextWithTags(),\n\t\t\t\tid,\n\t\t\t\tSuggestOptions(),\n\t\t\t\tMessageCursor(),\n\t\t\t\tData::WebPageDraft()));\n\t\t}\n\t} else {\n\t\t_replyEditMsg = item;\n\t\t_replyTo = id;\n\t\tif (_replyTo) {\n\t\t\tif (const auto i = session().data().message(_replyTo.messageId)) {\n\t\t\t\tif (const auto media = i->media()) {\n\t\t\t\t\tusing namespace SendMenu;\n\t\t\t\t\tconst auto type = media->hasSpoiler()\n\t\t\t\t\t\t? Action{ .type = Action::Type::SpoilerOn }\n\t\t\t\t\t\t: Action{ .type = Action::Type::SpoilerOff };\n\t\t\t\t\t_mediaEditManager.apply(type);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t_mediaEditManager.cancel();\n\t\t}\n\t\tcancelSuggestPost();\n\t\tupdateReplyEditText(_replyEditMsg);\n\t\tupdateCanSendMessage();\n\t\tupdateBotKeyboard();\n\t\tupdateReplyToName();\n\t\tupdateControlsVisibility();\n\t\tupdateControlsGeometry();\n\t\tupdateField();\n\t\trefreshTopBarActiveChat();\n\t}\n\n\tsaveDraftWithTextNow();\n\tsetInnerFocus();\n}\n\nvoid HistoryWidget::editMessage(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst TextSelection &selection) {\n\tif (_chooseTheme) {\n\t\ttoggleChooseChatTheme(_peer);\n\t} else if (_voiceRecordBar->isActive()) {\n\t\tcontroller()->showToast(tr::lng_edit_caption_voice(tr::now));\n\t\treturn;\n\t} else if (const auto media = item->media()) {\n\t\tif (media->todolist()) {\n\t\t\tWindow::PeerMenuEditTodoList(controller(), item);\n\t\t\treturn;\n\t\t}\n\t}\n\tif (_composeSearch) {\n\t\t_composeSearch->hideAnimated();\n\t}\n\n\tif (isRecording()) {\n\t\t// Just fix some strange inconsistency.\n\t\t_send->clearState();\n\t}\n\tif (!_editMsgId) {\n\t\tconst auto suggest = suggestOptions();\n\t\tif (_replyTo || suggest.exists || !_field->empty()) {\n\t\t\t_history->setLocalDraft(std::make_unique<Data::Draft>(\n\t\t\t\t_field,\n\t\t\t\t_replyTo,\n\t\t\t\tsuggest,\n\t\t\t\t_preview->draft()));\n\t\t} else {\n\t\t\t_history->clearLocalDraft(MsgId(), PeerId());\n\t\t}\n\t}\n\n\tconst auto editData = PrepareEditText(item);\n\tconst auto cursor = MessageCursor {\n\t\tint(editData.text.size()),\n\t\tint(editData.text.size()),\n\t\tUi::kQFixedMax\n\t};\n\tconst auto previewDraft = Data::WebPageDraft::FromItem(item);\n\t_history->setLocalEditDraft(std::make_unique<Data::Draft>(\n\t\teditData,\n\t\tFullReplyTo{ item->fullId() },\n\t\tSuggestOptions(),\n\t\tcursor,\n\t\tpreviewDraft));\n\tapplyDraft();\n\n\tupdateBotKeyboard();\n\n\tif (fieldOrDisabledShown()) {\n\t\t_fieldBarCancel->show();\n\t}\n\tupdateFieldPlaceholder();\n\tupdateMouseTracking();\n\tupdateReplyToName();\n\tupdateControlsGeometry();\n\tupdateField();\n\tSelectTextInFieldWithMargins(_field, selection);\n\n\tsaveDraftWithTextNow();\n\tsetInnerFocus();\n}\n\nvoid HistoryWidget::fillSenderUserpicMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<PeerData*> peer) {\n\tif (!_peer || _peer->isUser()) {\n\t\t// No need to offer View Profile / Send Message in private chat.\n\t\treturn;\n\t}\n\tconst auto inGroup = _peer && (_peer->isChat() || _peer->isMegagroup());\n\tWindow::FillSenderUserpicMenu(\n\t\tcontroller(),\n\t\tpeer,\n\t\tinGroup ? _peer : nullptr,\n\t\t(inGroup && _canSendTexts) ? _field.data() : nullptr,\n\t\tinGroup ? _peer->owner().history(_peer) : Dialogs::Key(),\n\t\tUi::Menu::CreateAddActionCallback(menu));\n}\n\nvoid HistoryWidget::hidePinnedMessage() {\n\tExpects(_pinnedBar != nullptr);\n\n\tconst auto id = _pinnedTracker->currentMessageId();\n\tif (!id.message) {\n\t\treturn;\n\t}\n\tif (_peer->canPinMessages()) {\n\t\tWindow::ToggleMessagePinned(controller(), id.message, false);\n\t} else {\n\t\tconst auto callback = [=] {\n\t\t\tif (_pinnedTracker) {\n\t\t\t\tcheckPinnedBarState();\n\t\t\t}\n\t\t};\n\t\tWindow::HidePinnedBar(\n\t\t\tcontroller(),\n\t\t\t_peer,\n\t\t\tMsgId(0), // topicRootId\n\t\t\tPeerId(0), // monoforumPeerId\n\t\t\tcrl::guard(this, callback));\n\t}\n}\n\nbool HistoryWidget::lastForceReplyReplied(const FullMsgId &replyTo) const {\n\treturn _peer\n\t\t&& (replyTo.peer == _peer->id)\n\t\t&& _keyboard->forceReply()\n\t\t&& (_keyboard->forMsgId()\n\t\t\t== FullMsgId(_peer->id, _history->lastKeyboardId))\n\t\t&& _keyboard->forMsgId().msg == replyTo.msg;\n}\n\nbool HistoryWidget::lastForceReplyReplied() const {\n\treturn _peer\n\t\t&& _keyboard->forceReply()\n\t\t&& _keyboard->forMsgId() == replyTo().messageId\n\t\t&& (_keyboard->forMsgId()\n\t\t\t== FullMsgId(_peer->id, _history->lastKeyboardId));\n}\n\nbool HistoryWidget::cancelReplyOrSuggest(bool lastKeyboardUsed) {\n\tconst auto ok1 = cancelReply(lastKeyboardUsed);\n\tconst auto ok2 = cancelSuggestPost();\n\treturn ok1 || ok2;\n}\n\nbool HistoryWidget::cancelReply(bool lastKeyboardUsed) {\n\tbool wasReply = false;\n\tif (_replyTo) {\n\t\twasReply = true;\n\n\t\t_processingReplyItem = _replyEditMsg = nullptr;\n\t\t_processingReplyTo = _replyTo = FullReplyTo();\n\t\tmouseMoveEvent(0);\n\t\tif (!readyToForward()\n\t\t\t&& !_previewDrawPreview\n\t\t\t&& !_kbReplyTo\n\t\t\t&& !_suggestOptions) {\n\t\t\t_fieldBarCancel->hide();\n\t\t\tupdateMouseTracking();\n\t\t}\n\t\tupdateBotKeyboard();\n\t\trefreshTopBarActiveChat();\n\t\tupdateCanSendMessage();\n\t\tupdateControlsVisibility();\n\t\tupdateControlsGeometry();\n\t\tupdate();\n\t} else if (const auto localDraft\n\t\t\t= (_history ? _history->localDraft({}, {}) : nullptr)) {\n\t\tif (localDraft->reply) {\n\t\t\tif (localDraft->textWithTags.text.isEmpty()) {\n\t\t\t\t_history->clearLocalDraft(MsgId(), PeerId());\n\t\t\t} else {\n\t\t\t\tlocalDraft->reply = {};\n\t\t\t}\n\t\t}\n\t}\n\tif (wasReply) {\n\t\tsaveDraftWithTextNow();\n\t}\n\tif (!_editMsgId\n\t\t&& _keyboard->singleUse()\n\t\t&& _keyboard->forceReply()\n\t\t&& lastKeyboardUsed) {\n\t\tif (_kbReplyTo) {\n\t\t\ttoggleKeyboard(false);\n\t\t}\n\t}\n\treturn wasReply;\n}\n\nvoid HistoryWidget::cancelReplyAfterMediaSend(bool lastKeyboardUsed) {\n\tif (cancelReplyOrSuggest(lastKeyboardUsed)) {\n\t\tsaveCloudDraft();\n\t}\n}\n\nint HistoryWidget::countMembersDropdownHeightMax() const {\n\tauto result = height()\n\t\t- rect::m::sum::v(st::membersInnerDropdown.padding);\n\tresult -= _tabbedSelectorToggle->height();\n\taccumulate_min(result, st::membersInnerHeightMax);\n\treturn result;\n}\n\nvoid HistoryWidget::cancelEdit() {\n\tif (!_editMsgId) {\n\t\treturn;\n\t}\n\n\t_canReplaceMedia = _canAddMedia = false;\n\t_photoEditMedia = nullptr;\n\tupdateReplaceMediaButton();\n\t_replyEditMsg = nullptr;\n\tsetEditMsgId(0);\n\t_history->clearLocalEditDraft(MsgId(), PeerId());\n\tcancelSuggestPost();\n\tapplyDraft();\n\n\tif (_saveEditMsgRequestId) {\n\t\t_history->session().api().request(_saveEditMsgRequestId).cancel();\n\t\t_saveEditMsgRequestId = 0;\n\t}\n\n\tsaveDraftWithTextNow();\n\n\tmouseMoveEvent(nullptr);\n\tif (!readyToForward()\n\t\t&& !_previewDrawPreview\n\t\t&& !replyTo()\n\t\t&& !_suggestOptions) {\n\t\t_fieldBarCancel->hide();\n\t\tupdateMouseTracking();\n\t}\n\n\tauto old = _textUpdateEvents;\n\t_textUpdateEvents = 0;\n\tfieldChanged();\n\t_textUpdateEvents = old;\n\n\tupdateControlsVisibility();\n\tupdateBotKeyboard();\n\tupdateFieldPlaceholder();\n\n\tupdateControlsGeometry();\n\tupdate();\n}\n\nvoid HistoryWidget::cancelFieldAreaState() {\n\tcontroller()->hideLayer();\n\tif (_previewDrawPreview) {\n\t\t_preview->apply({ .removed = true });\n\t} else if (_editMsgId) {\n\t\tcancelEdit();\n\t} else if (_replyTo) {\n\t\tcancelReply();\n\t} else if (readyToForward()) {\n\t\t_history->setForwardDraft(MsgId(), PeerId(), {});\n\t} else if (_kbReplyTo) {\n\t\ttoggleKeyboard();\n\t} else if (_suggestOptions) {\n\t\tcancelSuggestPost();\n\t}\n}\n\nbool HistoryWidget::cancelSuggestPost() {\n\tif (!_suggestOptions) {\n\t\treturn false;\n\t}\n\t_suggestOptions = nullptr;\n\tupdateControlsVisibility();\n\tupdateControlsGeometry();\n\tsaveDraftWithTextNow();\n\treturn true;\n}\n\nvoid HistoryWidget::fullInfoUpdated() {\n\tauto refresh = false;\n\tif (_list) {\n\t\tif (updateCanSendMessage()) {\n\t\t\trefresh = true;\n\t\t}\n\t\tif (_autocomplete) {\n\t\t\t_autocomplete->requestRefresh();\n\t\t}\n\t\t_list->refreshAboutView();\n\t\t_list->updateBotInfo();\n\n\t\thandlePeerUpdate();\n\t\tcheckSuggestToGigagroup();\n\n\t\tconst auto hasNonEmpty = _history->findFirstNonEmpty();\n\t\tconst auto readyForBotStart = hasNonEmpty\n\t\t\t|| (_history->loadedAtTop() && _history->loadedAtBottom());\n\t\tif (readyForBotStart && clearMaybeSendStart() && hasNonEmpty) {\n\t\t\tsendBotStartCommand();\n\t\t}\n\t\trefreshGiftToChannelShown();\n\t\trefreshDirectMessageShown();\n\t}\n\tif (updateCmdStartShown()) {\n\t\trefresh = true;\n\t} else if (!_scroll->isHidden() && _unblock->isHidden() == isBlocked()) {\n\t\trefresh = true;\n\t}\n\tif (_history\n\t\t\t&& HistoryView::SubsectionTabs::UsedFor(_history)\n\t\t\t&& !_subsectionTabs) {\n\t\tvalidateSubsectionTabs();\n\t}\n\tif (refresh) {\n\t\tupdateControlsVisibility();\n\t\tupdateControlsGeometry();\n\t}\n}\n\nvoid HistoryWidget::handlePeerUpdate() {\n\tbool resize = false;\n\tupdateSendRestriction();\n\tupdateHistoryGeometry();\n\tif (_peer->isChat() && _peer->asChat()->noParticipantInfo()) {\n\t\tsession().api().requestFullPeer(_peer);\n\t} else if (_peer->isUser()\n\t\t&& ((_peer->asUser()->blockStatus() == UserData::BlockStatus::Unknown)\n\t\t\t|| (_peer->asUser()->callsStatus()\n\t\t\t\t== UserData::CallsStatus::Unknown))) {\n\t\tsession().api().requestFullPeer(_peer);\n\t} else if (auto channel = _peer->asMegagroup()) {\n\t\tif (channel->mgInfo->botStatus == Data::BotStatus::Unknown) {\n\t\t\tsession().api().chatParticipants().requestBots(channel);\n\t\t}\n\t\tif (!channel->mgInfo->adminsLoaded) {\n\t\t\tsession().api().chatParticipants().requestAdmins(channel);\n\t\t}\n\t}\n\tif (!_showAnimation) {\n\t\tconst auto blockChanged = (_unblock->isHidden() == isBlocked());\n\t\tif (blockChanged\n\t\t\t|| (!isBlocked()\n\t\t\t\t&& (_joinChannel->isHidden() == isJoinChannel()))) {\n\t\t\tresize = true;\n\t\t}\n\t\tif (updateCanSendMessage()) {\n\t\t\tresize = true;\n\t\t}\n\t\tif (blockChanged) {\n\t\t\t_list->refreshAboutView(true);\n\t\t\t_list->updateBotInfo();\n\t\t}\n\t\tupdateControlsVisibility();\n\t\tif (resize) {\n\t\t\tupdateControlsGeometry();\n\t\t}\n\t}\n}\n\nbool HistoryWidget::updateCanSendMessage() {\n\tif (!_peer) {\n\t\treturn false;\n\t}\n\tconst auto topic = resolveReplyToTopic();\n\tconst auto allWithoutPolls = Data::AllSendRestrictions()\n\t\t& ~ChatRestriction::SendPolls;\n\tconst auto onlyReplies = _peer->amMonoforumAdmin();\n\tconst auto restrictedOnlyReplies = onlyReplies\n\t\t&& (!_replyTo.messageId || _replyTo.messageId.peer != _peer->id);\n\tconst auto newCanSendMessages = restrictedOnlyReplies\n\t\t? false\n\t\t: topic\n\t\t? Data::CanSendAnyOf(topic, allWithoutPolls)\n\t\t: Data::CanSendAnyOf(_peer, allWithoutPolls);\n\tconst auto newCanSendTexts = restrictedOnlyReplies\n\t\t? false\n\t\t: topic\n\t\t? Data::CanSend(topic, ChatRestriction::SendOther)\n\t\t: Data::CanSend(_peer, ChatRestriction::SendOther);\n\tif (_canSendMessages == newCanSendMessages\n\t\t&& _canSendTexts == newCanSendTexts) {\n\t\treturn false;\n\t}\n\t_canSendMessages = newCanSendMessages;\n\t_canSendTexts = newCanSendTexts;\n\tif (!_canSendMessages) {\n\t\tcancelReplyOrSuggest();\n\t}\n\trefreshSuggestPostToggle();\n\trefreshScheduledToggle();\n\trefreshSendGiftToggle();\n\trefreshSilentToggle();\n\treturn true;\n}\n\nvoid HistoryWidget::forwardSelected() {\n\tif (!_list) {\n\t\treturn;\n\t}\n\tconst auto weak = base::make_weak(this);\n\tWindow::ShowForwardMessagesBox(controller(), getSelectedItems(), [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->clearSelected();\n\t\t}\n\t});\n}\n\nvoid HistoryWidget::confirmDeleteSelected() {\n\tif (!_list) return;\n\n\tauto ids = _list->getSelectedItems();\n\tif (ids.empty()) {\n\t\treturn;\n\t}\n\tconst auto items = session().data().idsToItems(ids);\n\tif (CanCreateModerateMessagesBox(items)) {\n\t\tconst auto opt = DefaultModerateMessagesBoxOptions();\n\t\tcontroller()->show(Box(\n\t\t\tCreateModerateMessagesBox,\n\t\t\titems,\n\t\t\tcrl::guard(this, [=] { clearSelected(); }),\n\t\t\topt));\n\t} else {\n\t\tauto box = Box<DeleteMessagesBox>(&session(), std::move(ids));\n\t\tbox->setDeleteConfirmedCallback(crl::guard(this, [=] {\n\t\t\tclearSelected();\n\t\t}));\n\t\tcontroller()->show(std::move(box));\n\t}\n}\n\nvoid HistoryWidget::escape() {\n\tif (_composeSearch) {\n\t\tif (_nonEmptySelection) {\n\t\t\tclearSelected();\n\t\t} else {\n\t\t\t_composeSearch->hideAnimated();\n\t\t}\n\t} else if (_chooseForReport) {\n\t\tcontroller()->clearChooseReportMessages();\n\t} else if (_nonEmptySelection && _list) {\n\t\tclearSelected();\n\t} else if (_isInlineBot) {\n\t\tcancelInlineBot();\n\t} else if (_editMsgId) {\n\t\tif (_replyEditMsg\n\t\t\t&& EditTextChanged(_replyEditMsg, _field->getTextWithTags())) {\n\t\t\tcontroller()->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_cancel_edit_post_sure(),\n\t\t\t\t.confirmed = crl::guard(this, [this](Fn<void()> &&close) {\n\t\t\t\t\tif (_editMsgId) {\n\t\t\t\t\t\tcancelEdit();\n\t\t\t\t\t\tclose();\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t\t.confirmText = tr::lng_cancel_edit_post_yes(),\n\t\t\t\t.cancelText = tr::lng_cancel_edit_post_no(),\n\t\t\t}));\n\t\t} else {\n\t\t\tcancelEdit();\n\t\t}\n\t} else if (readyToForward() && _history) {\n\t\t_history->setForwardDraft(MsgId(), PeerId(), {});\n\t} else if (_autocomplete && !_autocomplete->isHidden()) {\n\t\t_autocomplete->hideAnimated();\n\t} else if ((_replyTo || _suggestOptions)\n\t\t&& _field->getTextWithTags().empty()) {\n\t\tcancelReplyOrSuggest();\n\t} else if (auto &voice = _voiceRecordBar; voice->isActive()) {\n\t\tvoice->showDiscardBox(nullptr, anim::type::normal);\n\t} else {\n\t\t_cancelRequests.fire({});\n\t}\n}\n\nvoid HistoryWidget::clearSelected() {\n\tif (_list) {\n\t\t_list->clearSelected();\n\t}\n}\n\nHistoryItem *HistoryWidget::getItemFromHistoryOrMigrated(\n\t\tMsgId genericMsgId) const {\n\treturn (genericMsgId < 0 && -genericMsgId < ServerMaxMsgId && _migrated)\n\t\t? session().data().message(_migrated->peer, -genericMsgId)\n\t\t: _peer\n\t\t? session().data().message(_peer, genericMsgId)\n\t\t: nullptr;\n}\n\nMessageIdsList HistoryWidget::getSelectedItems() const {\n\treturn _list ? _list->getSelectedItems() : MessageIdsList();\n}\n\nvoid HistoryWidget::updateTopBarChooseForReport() {\n\tif (_chooseForReport && _chooseForReport->active) {\n\t\t_topBar->showChooseMessagesForReport(\n\t\t\t_chooseForReport->reportInput);\n\t} else {\n\t\t_topBar->clearChooseMessagesForReport();\n\t}\n\tupdateTopBarSelection();\n\tupdateControlsVisibility();\n\tupdateControlsGeometry();\n}\n\nvoid HistoryWidget::updateTopBarSelection() {\n\tif (!_list) {\n\t\t_topBar->showSelected(HistoryView::TopBarWidget::SelectedState {});\n\t\treturn;\n\t}\n\n\tauto selectedState = _list->getSelectionState();\n\t_nonEmptySelection = (selectedState.count > 0)\n\t\t|| selectedState.textSelected;\n\t_topBar->showSelected(selectedState);\n\n\tif ((selectedState.count > 0) && _composeSearch) {\n\t\t_composeSearch->hideAnimated();\n\t}\n\n\tconst auto transparent = Qt::WA_TransparentForMouseEvents;\n\tif (selectedState.count == 0) {\n\t\t_reportMessages->clearState();\n\t\t_reportMessages->setAttribute(transparent);\n\t\t_reportMessages->setColorOverride(st::windowSubTextFg->c);\n\t} else if (_reportMessages->testAttribute(transparent)) {\n\t\t_reportMessages->setAttribute(transparent, false);\n\t\t_reportMessages->setColorOverride(std::nullopt);\n\t}\n\t_reportMessages->setText(selectedState.count\n\t\t? tr::lng_report_messages_count(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tselectedState.count,\n\t\t\ttr::upper)\n\t\t: tr::lng_report_messages_none(tr::now, tr::upper));\n\tupdateControlsVisibility();\n\tupdateHistoryGeometry();\n\tif (!controller()->isLayerShown()\n\t\t&& !Core::App().passcodeLocked()) {\n\t\tif (isSearching() && !_nonEmptySelection) {\n\t\t\t_composeSearch->setInnerFocus();\n\t\t} else if (_nonEmptySelection\n\t\t\t|| (_list && _list->wasSelectedText())\n\t\t\t|| isRecording()\n\t\t\t|| isBotStart()\n\t\t\t|| isBlocked()\n\t\t\t|| (!_canSendTexts && !_editMsgId)) {\n\t\t\t_list->setFocus();\n\t\t} else {\n\t\t\t_field->setFocus();\n\t\t}\n\t}\n\t_topBar->update();\n\tupdate();\n}\n\nvoid HistoryWidget::messageDataReceived(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId msgId) {\n\tif (!_peer || _peer != peer || !msgId) {\n\t\treturn;\n\t} else if (_editMsgId == msgId\n\t\t|| (_replyTo.messageId == FullMsgId(peer->id, msgId))) {\n\t\tupdateReplyEditTexts(true);\n\t\tif (_editMsgId == msgId) {\n\t\t\t_preview->setDisabled(_editMsgId\n\t\t\t\t&& _replyEditMsg\n\t\t\t\t&& _replyEditMsg->media()\n\t\t\t\t&& !_replyEditMsg->media()->webpage());\n\t\t}\n\t}\n}\n\nvoid HistoryWidget::updateReplyEditText(not_null<HistoryItem*> item) {\n\tconst auto context = Core::TextContext({\n\t\t.session = &session(),\n\t\t.repaint = [=] { updateField(); },\n\t});\n\tconst auto text = [&] {\n\t\tconst auto media = (_replyTo.todoItemId\n\t\t\t\t|| !_replyTo.pollOption.isEmpty())\n\t\t\t? item->media()\n\t\t\t: nullptr;\n\t\tif (const auto todolist = media ? media->todolist() : nullptr) {\n\t\t\tconst auto i = ranges::find(\n\t\t\t\ttodolist->items,\n\t\t\t\t_replyTo.todoItemId,\n\t\t\t\t&TodoListItem::id);\n\t\t\tif (i != end(todolist->items)) {\n\t\t\t\treturn i->text;\n\t\t\t}\n\t\t}\n\t\tif (const auto poll = media ? media->poll() : nullptr) {\n\t\t\tif (const auto answer = poll->answerByOption(\n\t\t\t\t\t_replyTo.pollOption)) {\n\t\t\t\treturn answer->text;\n\t\t\t}\n\t\t}\n\t\treturn (_editMsgId || _replyTo.quote.empty())\n\t\t\t? item->inReplyText()\n\t\t\t: _replyTo.quote;\n\t}();\n\t_replyEditMsgText.setMarkedText(\n\t\tst::defaultTextStyle,\n\t\ttext,\n\t\tUi::DialogTextOptions(),\n\t\tcontext);\n\tif (fieldOrDisabledShown() || isRecording()) {\n\t\t_fieldBarCancel->show();\n\t\tupdateMouseTracking();\n\t}\n}\n\nvoid HistoryWidget::updateReplyEditTexts(bool force) {\n\tif (!force) {\n\t\tif (_replyEditMsg || (!_editMsgId && !_replyTo)) {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (!_replyEditMsg && _peer) {\n\t\t_replyEditMsg = session().data().message(\n\t\t\t_editMsgId ? _peer->id : _replyTo.messageId.peer,\n\t\t\t_editMsgId ? _editMsgId : _replyTo.messageId.msg);\n\t\tif (!_editMsgId) {\n\t\t\tupdateFieldPlaceholder();\n\t\t}\n\t}\n\tif (_replyEditMsg) {\n\t\tconst auto editMedia = _editMsgId\n\t\t\t? _replyEditMsg->media()\n\t\t\t: nullptr;\n\t\tif (_editMsgId && _replyEditMsg) {\n\t\t\t_mediaEditManager.start(_replyEditMsg);\n\t\t}\n\t\t_canReplaceMedia = _editMsgId && _replyEditMsg->allowsEditMedia();\n\t\tif (editMedia && editMedia->allowsEditMedia()) {\n\t\t\t_canAddMedia = false;\n\t\t} else {\n\t\t\t_canAddMedia = base::take(_canReplaceMedia);\n\t\t}\n\t\tif (_canReplaceMedia || _canAddMedia) {\n\t\t\t// Invalidate the button, maybe icon has changed.\n\t\t\t_replaceMedia.destroy();\n\t\t}\n\t\t_photoEditMedia = (_canReplaceMedia\n\t\t\t&& editMedia->photo()\n\t\t\t&& !editMedia->photo()->isNull())\n\t\t\t? editMedia->photo()->createMediaView()\n\t\t\t: nullptr;\n\t\tif (_photoEditMedia) {\n\t\t\t_photoEditMedia->wanted(\n\t\t\t\tData::PhotoSize::Large,\n\t\t\t\t_replyEditMsg->fullId());\n\t\t}\n\t\tif (updateReplaceMediaButton()) {\n\t\t\tupdateControlsVisibility();\n\t\t\tupdateControlsGeometry();\n\t\t}\n\t\tupdateReplyEditText(_replyEditMsg);\n\t\tupdateBotKeyboard();\n\t\tupdateReplyToName();\n\t\tupdateField();\n\t} else if (force) {\n\t\tif (_editMsgId) {\n\t\t\tcancelEdit();\n\t\t} else {\n\t\t\tcancelReply();\n\t\t}\n\t}\n}\n\nvoid HistoryWidget::updateForwarding() {\n\t_forwardPanel->update(_history, _history\n\t\t? _history->resolveForwardDraft(MsgId(), PeerId())\n\t\t: Data::ResolvedForwardDraft());\n\tupdateControlsVisibility();\n\tupdateControlsGeometry();\n}\n\nvoid HistoryWidget::updateReplyToName() {\n\tif (!_history || _editMsgId) {\n\t\treturn;\n\t} else if (!_replyEditMsg && (_replyTo || !_kbReplyTo)) {\n\t\treturn;\n\t}\n\tconst auto context = Core::TextContext({\n\t\t.session = &_history->session(),\n\t\t.customEmojiLoopLimit = 1,\n\t});\n\tconst auto to = _replyEditMsg ? _replyEditMsg : _kbReplyTo;\n\t_replyToName.setMarkedText(\n\t\tst::fwdTextStyle,\n\t\tHistoryView::Reply::ComposePreviewName(_history, to, _replyTo),\n\t\tUi::NameTextOptions(),\n\t\tcontext);\n}\n\nvoid HistoryWidget::updateField() {\n\tif (_repaintFieldScheduled) {\n\t\treturn;\n\t}\n\t_repaintFieldScheduled = true;\n\tconst auto fieldAreaTop = _scroll->y() + _scroll->height();\n\trtlupdate(0, fieldAreaTop, width(), height() - fieldAreaTop);\n}\n\nvoid HistoryWidget::drawField(Painter &p, const QRect &rect) {\n\t_repaintFieldScheduled = false;\n\n\tauto backy = _field->y() - st::historySendPadding;\n\tauto backh = fieldHeight() + 2 * st::historySendPadding;\n\tauto hasForward = readyToForward();\n\tauto drawMsgText = (_editMsgId || _replyTo) ? _replyEditMsg : _kbReplyTo;\n\tif (_editMsgId\n\t\t|| _replyTo\n\t\t|| hasForward\n\t\t|| _kbReplyTo\n\t\t|| _previewDrawPreview\n\t\t|| _suggestOptions) {\n\t\tbacky -= st::historyReplyHeight;\n\t\tbackh += st::historyReplyHeight;\n\t}\n\tp.setInactive(\n\t\tcontroller()->isGifPausedAtLeastFor(Window::GifPauseReason::Any));\n\tp.fillRect(myrtlrect(0, backy, width(), backh), st::historyReplyBg);\n\n\tconst auto media = (!_previewDrawPreview && drawMsgText)\n\t\t? drawMsgText->media()\n\t\t: nullptr;\n\tconst auto poll = media ? media->poll() : nullptr;\n\tconst auto pollAnswer = poll\n\t\t? poll->answerByOption(_replyTo.pollOption)\n\t\t: nullptr;\n\tconst auto pollMediaPtr = pollAnswer\n\t\t? &pollAnswer->media\n\t\t: (poll && _replyTo.pollOption.isEmpty())\n\t\t? &poll->attachedMedia\n\t\t: nullptr;\n\tconst auto pollMediaHasPreview = pollMediaPtr\n\t\t&& (pollMediaPtr->photo || pollMediaPtr->document);\n\tconst auto hasPreview = pollMediaHasPreview\n\t\t|| (media && media->hasReplyPreview());\n\tconst auto preview = _mediaEditManager\n\t\t? _mediaEditManager.mediaPreview()\n\t\t: pollMediaHasPreview\n\t\t? (pollMediaPtr->photo\n\t\t\t? pollMediaPtr->photo->getReplyPreview(drawMsgText)\n\t\t\t: pollMediaPtr->document->getReplyPreview(drawMsgText))\n\t\t: (media && media->hasReplyPreview())\n\t\t? media->replyPreview()\n\t\t: nullptr;\n\tconst auto spoilered = _mediaEditManager.spoilered();\n\tif (!spoilered) {\n\t\t_replySpoiler = nullptr;\n\t} else if (!_replySpoiler) {\n\t\t_replySpoiler = std::make_unique<Ui::SpoilerAnimation>([=] {\n\t\t\tupdateField();\n\t\t});\n\t}\n\n\tif (_previewDrawPreview) {\n\t\tst::historyLinkIcon.paint(\n\t\t\tp,\n\t\t\tst::historyReplyIconPosition + QPoint(0, backy),\n\t\t\twidth());\n\t\tconst auto textTop = backy + st::msgReplyPadding.top();\n\t\tauto previewLeft = st::historyReplySkip;\n\n\t\tconst auto to = QRect(\n\t\t\tpreviewLeft,\n\t\t\tbacky + (st::historyReplyHeight - st::historyReplyPreview) / 2,\n\t\t\tst::historyReplyPreview,\n\t\t\tst::historyReplyPreview);\n\t\tif (_previewDrawPreview(p, to)) {\n\t\t\tpreviewLeft += st::historyReplyPreview + st::msgReplyBarSkip;\n\t\t}\n\t\tp.setPen(st::historyReplyNameFg);\n\t\tconst auto elidedWidth = width()\n\t\t\t- previewLeft\n\t\t\t- _fieldBarCancel->width()\n\t\t\t- st::msgReplyPadding.right();\n\n\t\t_previewTitle.drawElided(\n\t\t\tp,\n\t\t\tpreviewLeft,\n\t\t\ttextTop,\n\t\t\telidedWidth);\n\t\tp.setPen(st::historyComposeAreaFg);\n\t\t_previewDescription.drawElided(\n\t\t\tp,\n\t\t\tpreviewLeft,\n\t\t\ttextTop + st::msgServiceNameFont->height,\n\t\t\telidedWidth);\n\t} else if (_editMsgId || _replyTo || (!hasForward && _kbReplyTo)) {\n\t\tconst auto now = crl::now();\n\t\tconst auto paused = p.inactive();\n\t\tconst auto pausedSpoiler = paused || On(PowerSaving::kChatSpoiler);\n\t\tauto replyLeft = st::historyReplySkip;\n\t\tif (_suggestOptions) {\n\t\t\t_suggestOptions->paintIcon(p, 0, backy, width());\n\t\t} else {\n\t\t\t(_editMsgId\n\t\t\t\t? st::historyEditIcon\n\t\t\t\t: (_replyTo && !_replyTo.quote.empty())\n\t\t\t\t? st::historyQuoteIcon\n\t\t\t\t: st::historyReplyIcon).paint(\n\t\t\t\t\tp,\n\t\t\t\t\tst::historyReplyIconPosition + QPoint(0, backy),\n\t\t\t\t\twidth());\n\t\t}\n\t\tif (drawMsgText) {\n\t\t\tif (hasPreview) {\n\t\t\t\tif (preview) {\n\t\t\t\t\tconst auto overEdit = _photoEditMedia\n\t\t\t\t\t\t? _inPhotoEditOver.value(_inPhotoEdit ? 1. : 0.)\n\t\t\t\t\t\t: 0.;\n\t\t\t\t\tauto to = QRect(\n\t\t\t\t\t\treplyLeft,\n\t\t\t\t\t\t(st::historyReplyHeight - st::historyReplyPreview) / 2\n\t\t\t\t\t\t\t+ backy,\n\t\t\t\t\t\tst::historyReplyPreview,\n\t\t\t\t\t\tst::historyReplyPreview);\n\t\t\t\t\tp.drawPixmap(to.x(), to.y(), preview->pixSingle(\n\t\t\t\t\t\tpreview->size() / style::DevicePixelRatio(),\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t.options = Images::Option::RoundSmall,\n\t\t\t\t\t\t\t.outer = to.size(),\n\t\t\t\t\t\t}));\n\t\t\t\t\tif (_replySpoiler) {\n\t\t\t\t\t\tif (overEdit > 0.) {\n\t\t\t\t\t\t\tp.setOpacity(1. - overEdit);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tUi::FillSpoilerRect(\n\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\tto,\n\t\t\t\t\t\t\tUi::DefaultImageSpoiler().frame(\n\t\t\t\t\t\t\t\t_replySpoiler->index(now, pausedSpoiler)));\n\t\t\t\t\t}\n\t\t\t\t\tif (overEdit > 0.) {\n\t\t\t\t\t\tp.setOpacity(overEdit);\n\t\t\t\t\t\tp.fillRect(to, st::historyEditMediaBg);\n\t\t\t\t\t\tst::historyEditMedia.paintInCenter(p, to);\n\t\t\t\t\t\tp.setOpacity(1.);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treplyLeft += st::historyReplyPreview + st::msgReplyBarSkip;\n\t\t\t}\n\t\t\tif (_suggestOptions) {\n\t\t\t\t_suggestOptions->paintLines(p, replyLeft, backy, width());\n\t\t\t} else {\n\t\t\t\tp.setPen(st::historyReplyNameFg);\n\t\t\t\tif (_editMsgId) {\n\t\t\t\t\tpaintEditHeader(p, rect, replyLeft, backy);\n\t\t\t\t} else {\n\t\t\t\t\t_replyToName.drawElided(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\treplyLeft,\n\t\t\t\t\t\tbacky + st::msgReplyPadding.top(),\n\t\t\t\t\t\twidth()\n\t\t\t\t\t\t\t- replyLeft\n\t\t\t\t\t\t\t- _fieldBarCancel->width()\n\t\t\t\t\t\t\t- st::msgReplyPadding.right());\n\t\t\t\t}\n\t\t\t\tp.setPen(st::historyComposeAreaFg);\n\t\t\t\t_replyEditMsgText.draw(p, {\n\t\t\t\t\t.position = QPoint(\n\t\t\t\t\t\treplyLeft,\n\t\t\t\t\t\tst::msgReplyPadding.top()\n\t\t\t\t\t\t\t+ st::msgServiceNameFont->height\n\t\t\t\t\t\t\t+ backy),\n\t\t\t\t\t.availableWidth = width()\n\t\t\t\t\t\t- replyLeft\n\t\t\t\t\t\t- _fieldBarCancel->width()\n\t\t\t\t\t\t- st::msgReplyPadding.right(),\n\t\t\t\t\t.palette = &st::historyComposeAreaPalette,\n\t\t\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t\t\t.now = now,\n\t\t\t\t\t.pausedEmoji = paused || On(PowerSaving::kEmojiChat),\n\t\t\t\t\t.pausedSpoiler = pausedSpoiler,\n\t\t\t\t\t.elisionLines = 1,\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\tp.setFont(st::msgDateFont);\n\t\t\tp.setPen(st::historyComposeAreaFgService);\n\t\t\tp.drawText(\n\t\t\t\treplyLeft,\n\t\t\t\tbacky\n\t\t\t\t\t+ (st::historyReplyHeight - st::msgDateFont->height) / 2\n\t\t\t\t\t+ st::msgDateFont->ascent,\n\t\t\t\tst::msgDateFont->elided(\n\t\t\t\t\ttr::lng_profile_loading(tr::now),\n\t\t\t\t\twidth()\n\t\t\t\t\t\t- replyLeft\n\t\t\t\t\t\t- _fieldBarCancel->width()\n\t\t\t\t\t\t- st::msgReplyPadding.right()));\n\t\t}\n\t} else if (hasForward) {\n\t\tst::historyForwardIcon.paint(\n\t\t\tp,\n\t\t\tst::historyReplyIconPosition + QPoint(0, backy), width());\n\t\tconst auto x = st::historyReplySkip;\n\t\tconst auto available = width()\n\t\t\t- x\n\t\t\t- _fieldBarCancel->width()\n\t\t\t- st::msgReplyPadding.right();\n\t\t_forwardPanel->paint(p, x, backy, available, width());\n\t} else if (_suggestOptions) {\n\t\t_suggestOptions->paintBar(p, 0, backy, width());\n\t}\n}\n\nvoid HistoryWidget::paintEditHeader(\n\t\tPainter &p,\n\t\tconst QRect &rect,\n\t\tint left,\n\t\tint top) const {\n\tif (!rect.intersects(\n\t\t\tmyrtlrect(left, top, width() - left, st::normalFont->height))) {\n\t\treturn;\n\t}\n\n\tp.setFont(st::msgServiceNameFont);\n\tp.drawTextLeft(\n\t\tleft,\n\t\ttop + st::msgReplyPadding.top(),\n\t\twidth(),\n\t\ttr::lng_edit_message(tr::now));\n\n\tif (!_replyEditMsg\n\t\t|| _replyEditMsg->history()->peer->canEditMessagesIndefinitely()) {\n\t\treturn;\n\t}\n\n\tauto editTimeLeftText = QString();\n\tauto updateIn = int(-1);\n\tauto timeSinceMessage = ItemDateTime(_replyEditMsg).msecsTo(\n\t\tQDateTime::currentDateTime());\n\tauto editTimeLeft = (session().serverConfig().editTimeLimit * 1000LL)\n\t\t- timeSinceMessage;\n\tif (editTimeLeft < 2) {\n\t\teditTimeLeftText = u\"0:00\"_q;\n\t} else if (editTimeLeft > kDisplayEditTimeWarningMs) {\n\t\tupdateIn = static_cast<int>(qMin(\n\t\t\teditTimeLeft - kDisplayEditTimeWarningMs,\n\t\t\tqint64(kFullDayInMs)));\n\t} else {\n\t\tupdateIn = static_cast<int>(editTimeLeft % 1000);\n\t\tif (!updateIn) {\n\t\t\tupdateIn = 1000;\n\t\t}\n\t\t++updateIn;\n\n\t\teditTimeLeft = (editTimeLeft - 1) / 1000; // seconds\n\t\teditTimeLeftText = u\"%1:%2\"_q\n\t\t\t.arg(editTimeLeft / 60)\n\t\t\t.arg(editTimeLeft % 60, 2, 10, QChar('0'));\n\t}\n\n\t// Restart timer only if we are sure that we've painted the whole timer.\n\tif (rect.contains(\n\t\t\tmyrtlrect(left, top, width() - left, st::normalFont->height))\n\t\t&& (updateIn > 0)) {\n\t\t_updateEditTimeLeftDisplay.callOnce(updateIn);\n\t}\n\n\tif (!editTimeLeftText.isEmpty()) {\n\t\tp.setFont(st::normalFont);\n\t\tp.setPen(st::historyComposeAreaFgService);\n\t\tp.drawText(\n\t\t\tleft\n\t\t\t\t+ st::msgServiceNameFont->width(tr::lng_edit_message(tr::now))\n\t\t\t\t+ st::normalFont->spacew,\n\t\t\ttop + st::msgReplyPadding.top() + st::msgServiceNameFont->ascent,\n\t\t\teditTimeLeftText);\n\t}\n}\n\nbool HistoryWidget::paintShowAnimationFrame() {\n\tif (_showAnimation) {\n\t\tauto p = QPainter(this);\n\t\t_showAnimation->paintContents(p);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid HistoryWidget::paintEvent(QPaintEvent *e) {\n\tif (paintShowAnimationFrame()\n\t\t|| controller()->contentOverlapped(this, e)) {\n\t\treturn;\n\t}\n\tif (hasPendingResizedItems()) {\n\t\tupdateListSize();\n\t}\n\n\tWindow::SectionWidget::PaintBackground(\n\t\tcontroller(),\n\t\tcontroller()->currentChatTheme(),\n\t\tthis,\n\t\te->rect());\n\n\tPainter p(this);\n\tconst auto clip = e->rect();\n\tif (_list) {\n\t\tconst auto restrictionHidden = fieldOrDisabledShown()\n\t\t\t|| isRecording();\n\t\tif (restrictionHidden\n\t\t\t|| replyTo()\n\t\t\t|| readyToForward()\n\t\t\t|| _kbShown\n\t\t\t|| _suggestOptions) {\n\t\t\tif (!isSearching()) {\n\t\t\t\tdrawField(p, clip);\n\t\t\t}\n\t\t}\n\t} else {\n\t\tconst auto w = 0\n\t\t\t+ st::msgServiceFont->width(tr::lng_willbe_history(tr::now))\n\t\t\t+ st::msgPadding.left()\n\t\t\t+ st::msgPadding.right();\n\t\tconst auto h = st::msgServiceFont->height\n\t\t\t+ st::msgServicePadding.top()\n\t\t\t+ st::msgServicePadding.bottom();\n\t\tconst auto tr = QRect(\n\t\t\t(width() - w) / 2,\n\t\t\tst::msgServiceMargin.top() + (height()\n\t\t\t\t- fieldHeight()\n\t\t\t\t- 2 * st::historySendPadding\n\t\t\t\t- h\n\t\t\t\t- st::msgServiceMargin.top()\n\t\t\t\t- st::msgServiceMargin.bottom()) / 2,\n\t\t\tw,\n\t\t\th);\n\t\tconst auto st = controller()->chatStyle();\n\t\tHistoryView::ServiceMessagePainter::PaintBubble(p, st, tr);\n\n\t\tp.setPen(st->msgServiceFg());\n\t\tp.setFont(st::msgServiceFont->f);\n\t\tp.drawTextLeft(\n\t\t\ttr.left() + st::msgPadding.left(),\n\t\t\ttr.top() + st::msgServicePadding.top(),\n\t\t\twidth(),\n\t\t\ttr::lng_willbe_history(tr::now));\n\t}\n}\n\nQPoint HistoryWidget::clampMousePosition(QPoint point) {\n\tif (point.x() < 0) {\n\t\tpoint.setX(0);\n\t} else if (point.x() >= _scroll->width()) {\n\t\tpoint.setX(_scroll->width() - 1);\n\t}\n\tif (point.y() < _scroll->scrollTop()) {\n\t\tpoint.setY(_scroll->scrollTop());\n\t} else if (point.y() >= _scroll->scrollTop() + _scroll->height()) {\n\t\tpoint.setY(_scroll->scrollTop() + _scroll->height() - 1);\n\t}\n\treturn point;\n}\n\nbool HistoryWidget::touchScroll(const QPoint &delta) {\n\tconst auto scTop = _scroll->scrollTop();\n\tconst auto scMax = _scroll->scrollTopMax();\n\tconst auto scNew = std::clamp(scTop - delta.y(), 0, scMax);\n\tif (scNew == scTop) {\n\t\treturn false;\n\t}\n\t_scroll->scrollToY(scNew);\n\treturn true;\n}\n\nvoid HistoryWidget::synteticScrollToY(int y) {\n\t_synteticScrollEvent = true;\n\tif (_scroll->scrollTop() == y) {\n\t\tvisibleAreaUpdated();\n\t} else {\n\t\t_scroll->scrollToY(y);\n\t}\n\t_synteticScrollEvent = false;\n}\n\nHistoryWidget::~HistoryWidget() {\n\tif (_history) {\n\t\t// Saving a draft on account switching.\n\t\tsaveFieldToHistoryLocalDraft();\n\t\tsession().api().saveDraftToCloudDelayed(_history);\n\t\tsetHistory(nullptr);\n\n\t\tsession().data().itemVisibilitiesUpdated();\n\t}\n\t_subsectionTabsLifetime.destroy();\n\t_subsectionTabs = nullptr;\n\tsetTabbedPanel(nullptr);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/history/history_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/controls/history_view_compose_media_edit_manager.h\"\n#include \"history/view/history_view_corner_buttons.h\"\n#include \"history/history_drag_area.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history_view_highlight_manager.h\"\n#include \"history/history_view_top_toast.h\"\n#include \"history/history.h\"\n#include \"chat_helpers/field_characters_count_manager.h\"\n#include \"data/data_report.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_session_controller.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"mtproto/sender.h\"\n\nenum class SendMediaType;\nclass MessageLinksParser;\nstruct InlineBotQuery;\n\nnamespace MTP {\nclass Error;\n} // namespace MTP\n\nnamespace Data {\nclass ForumTopic;\nclass PhotoMedia;\nstruct DrawToReplyRequest;\nstruct SendError;\n} // namespace Data\n\nnamespace SendMenu {\nstruct Action;\nstruct Details;\n} // namespace SendMenu\n\nnamespace Api {\nstruct MessageToSend;\nstruct SendOptions;\nstruct SendAction;\n} // namespace Api\n\nnamespace InlineBots {\nnamespace Layout {\nclass Widget;\n} // namespace Layout\nstruct ResultSelected;\n} // namespace InlineBots\n\nnamespace Support {\nclass Autocomplete;\nstruct Contact;\n} // namespace Support\n\nnamespace Ui {\nclass InnerDropdown;\nclass DropdownMenu;\nclass PlainShadow;\nclass IconButton;\nclass EmojiButton;\nclass RpWidget;\nclass SendButton;\nclass SilentToggle;\nclass FlatButton;\nclass RoundButton;\nclass PinnedBar;\nclass GroupCallBar;\nclass RequestsBar;\nstruct PreparedList;\nstruct PreparedBundle;\nclass SendFilesWay;\nclass SendAsButton;\nclass SpoilerAnimation;\nclass ChooseThemeController;\nclass ContinuousScroll;\nstruct ChatPaintHighlight;\nclass ChatStyle;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Ui::Emoji {\nclass SuggestionsController;\n} // namespace Ui::Emoji\n\nnamespace Webrtc {\nenum class RecordAvailability : uchar;\n} // namespace Webrtc\n\nnamespace ChatHelpers {\nclass TabbedPanel;\nclass TabbedSelector;\nclass FieldAutocomplete;\nstruct FileChosen;\n} // namespace ChatHelpers\n\nnamespace HistoryView {\nclass StickerToast;\nclass PaidReactionToast;\nclass SelfForwardsTagger;\nclass TopBarWidget;\nclass PaysStatus;\nclass ContactStatus;\nclass BusinessBotStatus;\nclass Element;\nclass PinnedTracker;\nclass TranslateBar;\nclass ComposeSearch;\nclass SubsectionTabs;\nstruct SelectedQuote;\nclass SuggestOptionsBar;\nenum class SuggestMode;\n} // namespace HistoryView\n\nnamespace HistoryView::Controls {\nclass RecordLock;\nclass VoiceRecordBar;\nclass ForwardPanel;\nclass TTLButton;\nclass WebpageProcessor;\nclass CharactersLimitLabel;\nclass PhotoEditSpoilerManager;\nclass ComposeAiButton;\nclass ComposeTooltipManager;\nusing AiTooltipManager = ComposeTooltipManager;\nstruct VoiceToSend;\n} // namespace HistoryView::Controls\n\nclass BotKeyboard;\nclass HistoryInner;\n\nclass HistoryWidget final\n\t: public Window::AbstractSectionWidget\n\t, private HistoryView::CornerButtonsDelegate {\npublic:\n\tusing FieldHistoryAction = Ui::InputField::HistoryAction;\n\tusing RecordLock = HistoryView::Controls::RecordLock;\n\tusing VoiceRecordBar = HistoryView::Controls::VoiceRecordBar;\n\tusing ForwardPanel = HistoryView::Controls::ForwardPanel;\n\n\tHistoryWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\tvoid historyLoaded();\n\n\t[[nodiscard]] bool preventsClose(Fn<void()> &&continueCallback) const;\n\n\t// When resizing the widget with top edge moved up or down and we\n\t// want to add this top movement to the scroll position, so inner\n\t// content will not move.\n\tvoid setGeometryWithTopMoved(const QRect &newGeometry, int topDelta);\n\n\tvoid windowShown();\n\t[[nodiscard]] bool markingMessagesRead() const;\n\t[[nodiscard]] bool markingContentsRead() const;\n\tbool skipItemRepaint();\n\tvoid checkActivation();\n\n\tvoid leaveToChildEvent(QEvent *e, QWidget *child) override;\n\n\tbool isItemCompletelyHidden(HistoryItem *item) const;\n\tvoid updateTopBarSelection();\n\tvoid updateTopBarChooseForReport();\n\tbool handleDrawToReplyRequest(Data::DrawToReplyRequest request);\n\n\tvoid loadMessages();\n\tvoid loadMessagesDown();\n\tvoid firstLoadMessages();\n\tvoid delayedShowAt(MsgId showAtMsgId, const Window::SectionShow &params);\n\n\tbool updateReplaceMediaButton();\n\tvoid updateFieldPlaceholder();\n\n\tbool confirmSendingFiles(const QStringList &files);\n\tbool confirmSendingFiles(not_null<const QMimeData*> data);\n\n\tvoid updateControlsVisibility();\n\tvoid updateControlsGeometry();\n\n\tHistory *history() const;\n\tPeerData *peer() const;\n\tvoid setMsgId(MsgId showAtMsgId, const Window::SectionShow &params = {});\n\tMsgId msgId() const;\n\n\tbool hasTopBarShadow() const {\n\t\treturn peer() != nullptr;\n\t}\n\tvoid showAnimated(\n\t\tWindow::SlideDirection direction,\n\t\tconst Window::SectionSlideParams &params);\n\tvoid showFast();\n\tvoid finishAnimating();\n\n\tvoid doneShow();\n\n\tQPoint clampMousePosition(QPoint point);\n\n\tbool touchScroll(const QPoint &delta);\n\n\tvoid enqueueMessageHighlight(const HistoryView::SelectedQuote &quote);\n\t[[nodiscard]] Ui::ChatPaintHighlight itemHighlight(\n\t\tnot_null<const HistoryItem*> item) const;\n\n\tMessageIdsList getSelectedItems() const;\n\tvoid itemEdited(not_null<HistoryItem*> item);\n\n\tvoid replyToMessage(FullReplyTo id);\n\tvoid replyToMessage(\n\t\tnot_null<HistoryItem*> item,\n\t\tFullReplyTo fields = {});\n\tvoid editMessage(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst TextSelection &selection);\n\n\tvoid fillSenderUserpicMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<PeerData*> peer);\n\n\t[[nodiscard]] FullReplyTo replyTo() const;\n\t[[nodiscard]] SuggestOptions suggestOptions(\n\t\tbool skipNoAdminCheck = false) const;\n\tbool lastForceReplyReplied(const FullMsgId &replyTo) const;\n\tbool lastForceReplyReplied() const;\n\tbool cancelReplyOrSuggest(bool lastKeyboardUsed = false);\n\tbool cancelReply(bool lastKeyboardUsed = false);\n\tbool cancelSuggestPost();\n\tvoid cancelEdit();\n\tvoid updateForwarding();\n\n\tvoid pushReplyReturn(not_null<HistoryItem*> item);\n\t[[nodiscard]] QVector<FullMsgId> replyReturns() const;\n\tvoid setReplyReturns(PeerId peer, QVector<FullMsgId> replyReturns);\n\n\tvoid escape();\n\n\tvoid sendBotCommand(const Bot::SendCommandRequest &request);\n\tvoid hideSingleUseKeyboard(FullMsgId replyToId);\n\tbool insertBotCommand(const QString &cmd);\n\tvoid insertTextAtCursor(const QString &text);\n\n\tbool eventFilter(QObject *obj, QEvent *e) override;\n\n\t// With force=true the markup is updated even if it is\n\t// already shown for the passed history item.\n\tvoid updateBotKeyboard(History *h = nullptr, bool force = false);\n\tvoid botCallbackSent(not_null<HistoryItem*> item);\n\n\tvoid fastShowAtEnd(not_null<History*> history);\n\tbool applyDraft(\n\t\tFieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);\n\tvoid showHistory(\n\t\tPeerId peerId,\n\t\tMsgId showAtMsgId,\n\t\tconst Window::SectionShow &params = {});\n\tvoid setChooseReportMessagesDetails(\n\t\tData::ReportInput reportInput,\n\t\tFn<void(std::vector<MsgId>)> callback);\n\tvoid clearAllLoadRequests();\n\tvoid clearSupportPreloadRequest();\n\tvoid clearDelayedShowAtRequest();\n\tvoid clearDelayedShowAt();\n\n\tvoid toggleChooseChatTheme(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::optional<bool> show = std::nullopt);\n\t[[nodiscard]] Ui::ChatTheme *customChatTheme() const;\n\n\tvoid applyCloudDraft(History *history);\n\n\tvoid activate();\n\tvoid setInnerFocus();\n\t[[nodiscard]] rpl::producer<> cancelRequests() const {\n\t\treturn _cancelRequests.events();\n\t}\n\tbool searchInChatEmbedded(\n\t\tQString query,\n\t\tDialogs::Key chat,\n\t\tPeerData *searchFrom = nullptr);\n\n\tvoid updateNotifyControls();\n\n\tbool contentOverlapped(const QRect &globalRect);\n\n\tQPixmap grabForShowAnimation(const Window::SectionSlideParams &params);\n\n\tvoid forwardSelected();\n\tvoid confirmDeleteSelected();\n\tvoid clearSelected();\n\n\t[[nodiscard]] SendMenu::Details sendMenuDetails() const;\n\t[[nodiscard]] SendMenu::Details saveMenuDetails() const;\n\tbool sendExistingDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tApi::MessageToSend messageToSend,\n\t\tstd::optional<MsgId> localId = std::nullopt);\n\tbool sendExistingPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tApi::SendOptions options);\n\n\tvoid showInfoTooltip(\n\t\tconst TextWithEntities &text,\n\t\tFn<void()> hiddenCallback);\n\tvoid showPremiumStickerTooltip(\n\t\tnot_null<const HistoryView::Element*> view);\n\tvoid showPremiumToast(not_null<DocumentData*> document);\n\n\t// Tabbed selector management.\n\tbool pushTabbedSelectorToThirdSection(\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst Window::SectionShow &params) override;\n\tbool returnTabbedSelector() override;\n\n\t// Float player interface.\n\tbool floatPlayerHandleWheelEvent(QEvent *e) override;\n\tQRect floatPlayerAvailableRect() override;\n\n\tbool notify_switchInlineBotButtonReceived(const QString &query, UserData *samePeerBot, MsgId samePeerReplyTo);\n\n\tvoid tryProcessKeyInput(not_null<QKeyEvent*> e);\n\n\t~HistoryWidget();\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\npublic:\n\tvoid synteticScrollToY(int y);\n\nprivate:\n\tusing TabbedPanel = ChatHelpers::TabbedPanel;\n\tusing TabbedSelector = ChatHelpers::TabbedSelector;\n\tusing VoiceToSend = HistoryView::Controls::VoiceToSend;\n\tenum ScrollChangeType {\n\t\tScrollChangeNone,\n\n\t\t// When we toggle a pinned message.\n\t\tScrollChangeAdd,\n\n\t\t// When loading a history part while scrolling down.\n\t\tScrollChangeNoJumpToBottom,\n\t};\n\tstruct ScrollChange {\n\t\tScrollChangeType type;\n\t\tint value;\n\t};\n\tstruct ChooseMessagesForReport {\n\t\tData::ReportInput reportInput;\n\t\tFn<void(std::vector<MsgId>)> callback;\n\t\tbool active = false;\n\t};\n\tstruct ItemRevealAnimation {\n\t\tUi::Animations::Simple animation;\n\t\tint startHeight = 0;\n\t};\n\tenum class TextUpdateEvent {\n\t\tSaveDraft = (1 << 0),\n\t\tSendTyping = (1 << 1),\n\t};\n\tusing TextUpdateEvents = base::flags<TextUpdateEvent>;\n\tfriend inline constexpr bool is_flag_type(TextUpdateEvent) { return true; };\n\n\tvoid cornerButtonsShowAtPosition(\n\t\tData::MessagePosition position) override;\n\tData::Thread *cornerButtonsThread() override;\n\tFullMsgId cornerButtonsCurrentId() override;\n\tbool cornerButtonsIgnoreVisibility() override;\n\tstd::optional<bool> cornerButtonsDownShown() override;\n\tbool cornerButtonsUnreadMayBeShown() override;\n\tbool cornerButtonsHas(HistoryView::CornerButtonType type) override;\n\n\t[[nodiscard]] bool checkSendPayment(\n\t\tint messagesCount,\n\t\tApi::SendOptions options,\n\t\tFn<void(int)> withPaymentApproved);\n\n\tvoid checkSuggestToGigagroup();\n\tvoid processReply();\n\tvoid setReplyFieldsFromProcessing();\n\n\tvoid initTabbedSelector();\n\tvoid initVoiceRecordBar();\n\tvoid refreshTabbedPanel();\n\tvoid createTabbedPanel();\n\tvoid setTabbedPanel(std::unique_ptr<TabbedPanel> panel);\n\tvoid updateField();\n\tvoid fieldChanged();\n\tvoid fieldFocused();\n\tvoid fieldResized();\n\n\tvoid initFieldAutocomplete();\n\tvoid cancelInlineBot();\n\tvoid saveDraft(bool delayed = false);\n\tvoid saveCloudDraft();\n\tvoid saveDraftDelayed();\n\tvoid saveDraftWithTextNow();\n\tvoid showMembersDropdown();\n\tvoid windowIsVisibleChanged();\n\tvoid saveFieldToHistoryLocalDraft();\n\tvoid fileChosen(ChatHelpers::FileChosen &&data);\n\tvoid setupSendMenu(\n\t\tnot_null<Ui::RpWidget*> button,\n\t\tFn<void(SendMenu::Action, SendMenu::Details)> action);\n\n\tvoid updateFieldSubmitSettings();\n\tbool clearMaybeSendStart();\n\n\t// Checks if we are too close to the top or to the bottom\n\t// in the scroll area and preloads history if needed.\n\tvoid preloadHistoryIfNeeded();\n\n\tvoid handleScroll();\n\tvoid updateHistoryItemsByTimer();\n\n\t[[nodiscard]] Dialogs::EntryState computeDialogsEntryState() const;\n\tvoid refreshTopBarActiveChat();\n\n\tvoid refreshJoinChannelText();\n\tvoid refreshGiftToChannelShown();\n\tvoid refreshDirectMessageShown();\n\tvoid requestMessageData(MsgId msgId);\n\tvoid messageDataReceived(not_null<PeerData*> peer, MsgId msgId);\n\n\t[[nodiscard]] Api::SendAction prepareSendAction(\n\t\tApi::SendOptions options);\n\tvoid sendTextWithTags(\n\t\tTextWithTags textWithTags,\n\t\tbool useWebPageDraft,\n\t\tApi::SendOptions options,\n\t\tFn<void()> done);\n\tvoid sendVoice(const VoiceToSend &data);\n\tvoid sendWithTextOverride(\n\t\tTextWithEntities text,\n\t\tApi::SendOptions options,\n\t\tFn<void()> done);\n\tvoid send(Api::SendOptions options);\n\tvoid sendWithModifiers(Qt::KeyboardModifiers modifiers);\n\tvoid sendScheduled(Api::SendOptions initialOptions);\n\t[[nodiscard]] SendMenu::Details sendButtonMenuDetails() const;\n\t[[nodiscard]] SendMenu::Details sendButtonDefaultDetails() const;\n\tvoid handlePendingHistoryUpdate();\n\tvoid fullInfoUpdated();\n\tvoid toggleTabbedSelectorMode();\n\tvoid recountChatWidth();\n\tvoid handlePeerUpdate();\n\tbool updateCanSendMessage();\n\tvoid setMembersShowAreaActive(bool active);\n\tvoid handleHistoryChange(not_null<const History*> history);\n\tvoid showAboutTopPromotion();\n\tvoid hideFieldIfVisible();\n\tvoid unreadCountUpdated();\n\tvoid closeCurrent();\n\n\t[[nodiscard]] int computeMaxFieldHeight() const;\n\tvoid toggleMuteUnmute();\n\tvoid reportSelectedMessages();\n\tvoid showKeyboardHideButton();\n\tvoid toggleKeyboard(bool manual = true);\n\tvoid startBotCommand();\n\tvoid hidePinnedMessage();\n\tvoid cancelFieldAreaState();\n\tvoid unblockUser();\n\tvoid sendBotStartCommand();\n\tvoid joinChannel();\n\n\tvoid supportInitAutocomplete();\n\tvoid supportInsertText(const QString &text);\n\tvoid supportShareContact(Support::Contact contact);\n\n\t[[nodiscard]] auto computeSendButtonType() const;\n\n\tvoid showFinished();\n\tvoid updateOverStates(QPoint pos);\n\tvoid chooseAttach(std::optional<bool> overrideSendImagesAsPhotos = {});\n\tvoid sendButtonClicked();\n\tvoid newItemAdded(not_null<HistoryItem*> item);\n\tvoid maybeMarkReactionsRead(not_null<HistoryItem*> item);\n\n\tbool canSendFiles(not_null<const QMimeData*> data) const;\n\tbool confirmSendingFiles(\n\t\tconst QStringList &files,\n\t\tconst QString &insertTextOnCancel);\n\tbool confirmSendingFiles(\n\t\tQImage &&image,\n\t\tQByteArray &&content,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos = std::nullopt,\n\t\tconst QString &insertTextOnCancel = QString());\n\tbool confirmSendingFiles(\n\t\tnot_null<const QMimeData*> data,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos,\n\t\tconst QString &insertTextOnCancel = QString());\n\tbool confirmSendingFiles(\n\t\tUi::PreparedList &&list,\n\t\tconst QString &insertTextOnCancel = QString());\n\tbool showSendingFilesError(const Ui::PreparedList &list) const;\n\tbool showSendingFilesError(const Ui::PreparedBundle &bundle) const;\n\n\tbool showSendMessageError(\n\t\tconst TextWithTags &textWithTags,\n\t\tbool ignoreSlowmodeCountdown,\n\t\tFn<void(int starsApproved)> withPaymentApproved = nullptr,\n\t\tApi::SendOptions options = {});\n\n\tvoid sendingFilesConfirmed(\n\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\tApi::SendOptions options);\n\n\tvoid sendBotCommand(\n\t\tconst Bot::SendCommandRequest &request,\n\t\tApi::SendOptions options);\n\n\tvoid uploadFile(const QByteArray &fileContent, SendMediaType type);\n\tvoid itemRemoved(not_null<const HistoryItem*> item);\n\n\t// Updates position of controls around the message field,\n\t// like send button, emoji button and others.\n\tvoid moveFieldControls();\n\tvoid updateFieldSize();\n\tvoid initAiButton();\n\tvoid updateAiButtonVisibility();\n\tvoid updateAiButtonGeometry();\n\tvoid showAiComposeBox();\n\tvoid initSendAsFileButton();\n\tvoid sendTextAsFile(\n\t\tconst QString &fileText,\n\t\tTextWithTags restoreText,\n\t\tint restorePosition,\n\t\tint restoreAnchor);\n\tvoid updateSendAsFileVisibility();\n\tvoid updateSendAsFileGeometry();\n\t[[nodiscard]] bool canSendAiComposeDirect() const;\n\n\t[[nodiscard]] MsgId resolveReplyToTopicRootId();\n\t[[nodiscard]] Data::ForumTopic *resolveReplyToTopic();\n\t[[nodiscard]] bool canWriteMessage() const;\n\t[[nodiscard]] bool hasEnoughLinesForAi() const;\n\t[[nodiscard]] bool textExceedsMaxSize() const;\n\tvoid orderWidgets();\n\n\t[[nodiscard]] InlineBotQuery parseInlineBotQuery() const;\n\n\tvoid clearInlineBot();\n\tvoid inlineBotChanged();\n\n\t// Look in the _field for the inline bot and query string.\n\tvoid updateInlineBotQuery();\n\n\t// Request to show results in the emoji panel.\n\tvoid applyInlineBotQuery(UserData *bot, const QString &query);\n\n\tvoid cancelReplyAfterMediaSend(bool lastKeyboardUsed);\n\tbool replyToPreviousMessage();\n\tbool replyToNextMessage();\n\t[[nodiscard]] bool showSlowmodeError();\n\n\tvoid hideChildWidgets();\n\tvoid hideSelectorControlsAnimated();\n\tint countMembersDropdownHeightMax() const;\n\n\tvoid updateReplyToName();\n\t[[nodiscard]] bool editingMessage() const {\n\t\treturn _editMsgId != 0;\n\t}\n\n\tvoid setupShortcuts();\n\tvoid setupGiftToChannelButton();\n\tvoid setupDirectMessageButton();\n\n\tvoid handlePeerMigration();\n\n\tvoid updateReplyEditTexts(bool force = false);\n\tvoid updateReplyEditText(not_null<HistoryItem*> item);\n\n\tvoid updatePinnedViewer();\n\tvoid setupTranslateBar();\n\tvoid setupPinnedTracker();\n\tvoid checkPinnedBarState();\n\tvoid clearHidingPinnedBar();\n\tvoid refreshPinnedBarButton(bool many, HistoryItem *item);\n\tvoid checkLastPinnedClickedIdReset(\n\t\tint wasScrollTop,\n\t\tint nowScrollTop);\n\n\tvoid checkMessagesTTL();\n\tvoid setupGroupCallBar();\n\tvoid setupRequestsBar();\n\n\tvoid checkSponsoredMessageBar();\n\t[[nodiscard]] bool checkSponsoredMessageBarVisibility() const;\n\tvoid requestSponsoredMessageBar();\n\tvoid createSponsoredMessageBar();\n\n\tvoid sendInlineResult(InlineBots::ResultSelected result);\n\n\tvoid drawField(Painter &p, const QRect &rect);\n\tvoid paintEditHeader(\n\t\tPainter &p,\n\t\tconst QRect &rect,\n\t\tint left,\n\t\tint top) const;\n\tbool paintShowAnimationFrame();\n\n\tvoid updateMouseTracking();\n\n\t// destroys _history and _migrated unread bars\n\tvoid destroyUnreadBar();\n\tvoid destroyUnreadBarOnClose();\n\tvoid createUnreadBarIfBelowVisibleArea(int withScrollTop);\n\t[[nodiscard]] bool insideJumpToEndInsteadOfToUnread() const;\n\tvoid createUnreadBarAndResize();\n\n\t[[nodiscard]] TextWithEntities prepareTextForEditMsg() const;\n\tvoid saveEditMessage(Api::SendOptions options = {});\n\n\tvoid setupPreview();\n\tvoid editDraftOptions();\n\tvoid jumpToReply(FullReplyTo to);\n\n\tvoid messagesReceived(not_null<PeerData*> peer, const MTPmessages_Messages &messages, int requestId);\n\tvoid messagesFailed(const MTP::Error &error, int requestId);\n\tvoid addMessagesToFront(not_null<PeerData*> peer, const QVector<MTPMessage> &messages);\n\tvoid addMessagesToBack(not_null<PeerData*> peer, const QVector<MTPMessage> &messages);\n\n\tvoid updateSendRestriction();\n\t[[nodiscard]] Data::SendError computeSendRestriction() const;\n\tvoid updateHistoryGeometry(bool initial = false, bool loadedDown = false, const ScrollChange &change = { ScrollChangeNone, 0 });\n\tvoid updateListSize();\n\tvoid startItemRevealAnimations();\n\tvoid revealItemsCallback();\n\n\tvoid startMessageSendingAnimation(not_null<HistoryItem*> item);\n\n\t// Does any of the shown histories has this flag set.\n\tbool hasPendingResizedItems() const;\n\n\t// Counts scrollTop for placing the scroll right at the unread\n\t// messages bar, choosing from _history and _migrated unreadBar.\n\tstd::optional<int> unreadBarTop() const;\n\tint itemTopForHighlight(not_null<HistoryView::Element*> view) const;\n\tvoid scrollToCurrentVoiceMessage(FullMsgId fromId, FullMsgId toId);\n\n\tvoid writeDrafts();\n\tvoid writeDraftTexts();\n\tvoid writeDraftCursors();\n\tvoid setFieldText(\n\t\tconst TextWithTags &textWithTags,\n\t\tTextUpdateEvents events = 0,\n\t\tFieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);\n\tvoid clearFieldText(\n\t\tTextUpdateEvents events = 0,\n\t\tFieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);\n\t[[nodiscard]] int fieldHeight() const;\n\t[[nodiscard]] bool fieldOrDisabledShown() const;\n\n\tvoid unregisterDraftSources();\n\tvoid registerDraftSource();\n\tvoid setHistory(History *history);\n\tvoid setEditMsgId(MsgId msgId);\n\n\tHistoryItem *getItemFromHistoryOrMigrated(MsgId genericMsgId) const;\n\tvoid animatedScrollToItem(MsgId msgId);\n\tvoid animatedScrollToY(int scrollTo, HistoryItem *attachTo = nullptr);\n\n\t// when scroll position or scroll area size changed this method\n\t// updates the boundings of the visible area in HistoryInner\n\t[[nodiscard]] bool hasSavedScroll() const;\n\tvoid visibleAreaUpdated();\n\tint countInitialScrollTop();\n\tint countAutomaticScrollTop();\n\tvoid preloadHistoryByScroll();\n\tvoid checkReplyReturns();\n\tvoid scrollToAnimationCallback(FullMsgId attachToId, int relativeTo);\n\n\t[[nodiscard]] bool readyToForward() const;\n\t[[nodiscard]] bool hasSilentToggle() const;\n\n\tvoid checkSupportPreload(bool force = false);\n\tvoid handleSupportSwitch(not_null<History*> updated);\n\n\t[[nodiscard]] bool isRecording() const;\n\t[[nodiscard]] bool isSearching() const;\n\n\t[[nodiscard]] bool isBotStart() const;\n\t[[nodiscard]] bool isBlocked() const;\n\t[[nodiscard]] bool isJoinChannel() const;\n\t[[nodiscard]] bool isMuteUnmute() const;\n\t[[nodiscard]] bool isReportMessages() const;\n\tbool updateCmdStartShown();\n\tvoid updateSendButtonType();\n\t[[nodiscard]] bool showRecordButton() const;\n\t[[nodiscard]] bool showInlineBotCancel() const;\n\tvoid refreshSilentToggle();\n\tvoid setupFastButtonMode();\n\n\t[[nodiscard]] bool isChoosingTheme() const;\n\n\tvoid setupScheduledToggle();\n\tvoid refreshScheduledToggle();\n\tvoid refreshSendGiftToggle();\n\tvoid refreshSuggestPostToggle();\n\tvoid applySuggestOptions(\n\t\tSuggestOptions suggest,\n\t\tHistoryView::SuggestMode mode);\n\tvoid setupSendAsToggle();\n\tvoid refreshSendAsToggle();\n\tvoid refreshAttachBotsMenu();\n\n\tvoid injectSponsoredMessages() const;\n\n\tbool kbWasHidden() const;\n\n\tvoid switchToSearch(QString query);\n\n\tvoid validateSubsectionTabs();\n\n\tvoid checkCharsCount();\n\tvoid checkCharsLimitation();\n\n\tMTP::Sender _api;\n\tFullReplyTo _replyTo;\n\tUi::Text::String _replyToName;\n\n\tFullReplyTo _processingReplyTo;\n\tHistoryItem *_processingReplyItem = nullptr;\n\n\tMsgId _editMsgId = 0;\n\tstd::shared_ptr<Data::PhotoMedia> _photoEditMedia;\n\tbool _canReplaceMedia = false;\n\tbool _canAddMedia = false;\n\tHistoryView::MediaEditManager _mediaEditManager;\n\n\tHistoryItem *_replyEditMsg = nullptr;\n\tUi::Text::String _replyEditMsgText;\n\tstd::unique_ptr<Ui::SpoilerAnimation> _replySpoiler;\n\tmutable base::Timer _updateEditTimeLeftDisplay;\n\n\tstd::unique_ptr<HistoryView::SuggestOptionsBar> _suggestOptions;\n\n\tobject_ptr<Ui::IconButton> _fieldBarCancel;\n\n\tstd::unique_ptr<Ui::RpWidget> _topBars;\n\n\tstd::unique_ptr<HistoryView::TranslateBar> _translateBar;\n\tint _translateBarHeight = 0;\n\n\tstd::unique_ptr<HistoryView::PinnedTracker> _pinnedTracker;\n\tstd::unique_ptr<Ui::PinnedBar> _pinnedBar;\n\tstd::unique_ptr<Ui::PinnedBar> _hidingPinnedBar;\n\tint _pinnedBarHeight = 0;\n\tFullMsgId _pinnedClickedId;\n\tstd::optional<FullMsgId> _minPinnedId;\n\n\tstd::unique_ptr<Ui::GroupCallBar> _groupCallBar;\n\tint _groupCallBarHeight = 0;\n\tstd::unique_ptr<Ui::RequestsBar> _requestsBar;\n\tint _requestsBarHeight = 0;\n\n\tbase::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _sponsoredMessageBar;\n\tint _sponsoredMessageBarHeight = 0;\n\n\tbool _preserveScrollTop = false;\n\tbool _repaintFieldScheduled = false;\n\tbool _sentFromScheduledTip = false;\n\n\tmtpRequestId _saveEditMsgRequestId = 0;\n\n\tstd::unique_ptr<HistoryView::Controls::WebpageProcessor> _preview;\n\tFn<bool(QPainter &p, QRect to)> _previewDrawPreview;\n\tUi::Text::String _previewTitle;\n\tUi::Text::String _previewDescription;\n\n\tPeerData *_peer = nullptr;\n\n\tbool _canSendMessages = false;\n\tbool _canSendTexts = false;\n\tMsgId _showAtMsgId = ShowAtUnreadMsgId;\n\tbase::flat_set<MsgId> _topicsRequested;\n\tWindow::SectionShow _showAtMsgParams;\n\tbool _showAndMaybeSendStart = false;\n\n\tint _firstLoadRequest = 0; // Not real mtpRequestId.\n\tint _preloadRequest = 0; // Not real mtpRequestId.\n\tint _preloadDownRequest = 0; // Not real mtpRequestId.\n\n\tMsgId _delayedShowAtMsgId = -1;\n\tWindow::SectionShow _delayedShowAtMsgParams;\n\tint _delayedShowAtRequest = 0; // Not real mtpRequestId.\n\n\tHistory *_supportPreloadHistory = nullptr;\n\tint _supportPreloadRequest = 0; // Not real mtpRequestId.\n\n\tobject_ptr<HistoryView::TopBarWidget> _topBar;\n\tobject_ptr<Ui::ContinuousScroll> _scroll;\n\tQPointer<HistoryInner> _list;\n\tHistory *_migrated = nullptr;\n\tHistory *_history = nullptr;\n\tmutable Data::ForumTopic *_creatingBotTopic = nullptr;\n\trpl::lifetime _historySponsoredPreloading;\n\n\t// Initial updateHistoryGeometry() was called.\n\tbool _historyInited = false;\n\t// If updateListSize() was called without updateHistoryGeometry().\n\tbool _updateHistoryGeometryRequired = false;\n\n\tint _lastScrollTop = 0; // gifs optimization\n\tcrl::time _lastScrolled = 0;\n\tbase::Timer _updateHistoryItems;\n\n\tcrl::time _lastUserScrolled = 0;\n\tbool _synteticScrollEvent = false;\n\tUi::Animations::Simple _scrollToAnimation;\n\n\tHistoryView::CornerButtons _cornerButtons;\n\n\tstd::unique_ptr<ChatHelpers::FieldAutocomplete> _autocomplete;\n\tstd::unique_ptr<Ui::Emoji::SuggestionsController> _emojiSuggestions;\n\tobject_ptr<Support::Autocomplete> _supportAutocomplete;\n\n\tUserData *_inlineBot = nullptr;\n\tQString _inlineBotUsername;\n\tbool _inlineLookingUpBot = false;\n\tmtpRequestId _inlineBotResolveRequestId = 0;\n\tbool _isInlineBot = false;\n\n\tWebrtc::RecordAvailability _recordAvailability = {};\n\n\tstd::unique_ptr<HistoryView::PaysStatus> _paysStatus;\n\tstd::unique_ptr<HistoryView::ContactStatus> _contactStatus;\n\tstd::unique_ptr<HistoryView::BusinessBotStatus> _businessBotStatus;\n\n\tconst std::shared_ptr<Ui::SendButton> _send;\n\tHistoryView::Controls::ComposeAiButton * const _aiButton = nullptr;\n\tUi::IconButton * const _sendAsFile = nullptr;\n\tobject_ptr<Ui::FlatButton> _unblock;\n\tobject_ptr<Ui::FlatButton> _botStart;\n\tobject_ptr<Ui::FlatButton> _joinChannel;\n\tobject_ptr<Ui::FlatButton> _muteUnmute;\n\tQPointer<Ui::IconButton> _giftToChannel;\n\tQPointer<Ui::IconButton> _directMessage;\n\trpl::lifetime _directMessageLifetime;\n\tobject_ptr<Ui::FlatButton> _reportMessages;\n\tstruct {\n\t\tobject_ptr<Ui::RoundButton> button = { nullptr };\n\t\tQString text;\n\t\tbool small = false;\n\t} _botMenu;\n\tobject_ptr<Ui::IconButton> _attachToggle;\n\tobject_ptr<Ui::IconButton> _replaceMedia = { nullptr };\n\tobject_ptr<Ui::SendAsButton> _sendAs = { nullptr };\n\tobject_ptr<Ui::EmojiButton> _tabbedSelectorToggle;\n\tobject_ptr<Ui::IconButton> _botKeyboardShow;\n\tobject_ptr<Ui::IconButton> _botKeyboardHide;\n\tobject_ptr<Ui::IconButton> _botCommandStart;\n\tobject_ptr<Ui::IconButton> _toggleSuggestPost = { nullptr };\n\tobject_ptr<Ui::IconButton> _giftToUser = { nullptr };\n\tobject_ptr<Ui::SilentToggle> _silent = { nullptr };\n\tobject_ptr<Ui::IconButton> _scheduled = { nullptr };\n\tstd::unique_ptr<HistoryView::Controls::TTLButton> _ttlInfo;\n\tconst std::unique_ptr<VoiceRecordBar> _voiceRecordBar;\n\tconst std::unique_ptr<ForwardPanel> _forwardPanel;\n\tstd::unique_ptr<HistoryView::ComposeSearch> _composeSearch;\n\tstd::unique_ptr<HistoryView::SubsectionTabs> _subsectionTabs;\n\trpl::lifetime _subsectionTabsLifetime;\n\trpl::lifetime _subsectionCheckLifetime;\n\tstd::unique_ptr<HistoryView::Controls::AiTooltipManager> _aiTooltipManager;\n\tstd::unique_ptr<HistoryView::Controls::AiTooltipManager> _sendAsFileTooltipManager;\n\tstd::shared_ptr<Ui::ChatStyle> _fieldChatStyle;\n\tbool _cmdStartShown = false;\n\tobject_ptr<Ui::InputField> _field;\n\tbase::unique_qptr<Ui::RpWidget> _fieldDisabled;\n\tstd::unique_ptr<Ui::RpWidget> _sendRestriction;\n\tusing CharactersLimitLabel = HistoryView::Controls::CharactersLimitLabel;\n\tbase::unique_qptr<CharactersLimitLabel> _charsLimitation;\n\tQString _sendRestrictionKey;\n\tUi::Animations::Simple _inPhotoEditOver;\n\tbool _inDetails = false;\n\tbool _inPhotoEdit = false;\n\tbool _inClickable = false;\n\n\tbool _kbShown = false;\n\tbool _fieldIsEmpty = true;\n\tHistoryItem *_kbReplyTo = nullptr;\n\tobject_ptr<Ui::ScrollArea> _kbScroll;\n\tconst not_null<BotKeyboard*> _keyboard;\n\n\tFieldCharsCountManager _fieldCharsCountManager;\n\n\tstd::unique_ptr<Ui::ChooseThemeController> _chooseTheme;\n\n\tobject_ptr<Ui::InnerDropdown> _membersDropdown = { nullptr };\n\tbase::Timer _membersDropdownShowTimer;\n\n\tobject_ptr<InlineBots::Layout::Widget> _inlineResults = { nullptr };\n\tstd::unique_ptr<TabbedPanel> _tabbedPanel;\n\tstd::unique_ptr<Ui::DropdownMenu> _attachBotsMenu;\n\n\tDragArea::Areas _attachDragAreas;\n\n\tbool _nonEmptySelection = false;\n\n\tTextUpdateEvents _textUpdateEvents = (TextUpdateEvents() | TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping);\n\n\tQString _confirmSource;\n\n\tstd::unique_ptr<Window::SlideAnimation> _showAnimation;\n\n\tHistoryView::ElementHighlighter _highlighter;\n\n\tcrl::time _saveDraftStart = 0;\n\tbool _saveDraftText = false;\n\tbase::Timer _saveDraftTimer;\n\tbase::Timer _saveCloudDraftTimer;\n\n\tHistoryView::InfoTooltip _topToast;\n\tstd::unique_ptr<HistoryView::StickerToast> _stickerToast;\n\tstd::unique_ptr<HistoryView::SelfForwardsTagger> _selfForwardsTagger;\n\tstd::unique_ptr<ChooseMessagesForReport> _chooseForReport;\n\n\tstd::unique_ptr<HistoryView::PaidReactionToast> _paidReactionToast;\n\n\tbase::flat_set<not_null<HistoryItem*>> _itemRevealPending;\n\tbase::flat_map<\n\t\tnot_null<HistoryItem*>,\n\t\tItemRevealAnimation> _itemRevealAnimations;\n\tint _itemsRevealHeight = 0;\n\n\n\tbool _sponsoredMessagesStateKnown = false;\n\tbool _justMarkingAsRead = false;\n\n\tobject_ptr<Ui::PlainShadow> _topShadow;\n\tbool _inGrab = false;\n\n\tint _topDelta = 0;\n\n\tSendPaymentHelper _sendPayment;\n\n\trpl::event_stream<> _cancelRequests;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/compose_controls_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_common.h\"\n#include \"ui/text/text_entity.h\"\n\nnamespace Api {\nenum class SendProgressType;\nstruct SendAction;\n} // namespace Api\n\nnamespace Data {\nclass GroupCall;\n} // namespace Data\n\nclass History;\n\nnamespace HistoryView::Controls {\n\nstruct MessageToEdit {\n\tFullMsgId fullId;\n\tApi::SendOptions options;\n\tTextWithTags textWithTags;\n\tbool spoilered = false;\n};\nstruct VoiceToSend {\n\tQByteArray bytes;\n\tVoiceWaveform waveform;\n\tcrl::time duration = 0;\n\tApi::SendOptions options;\n\tbool video = false;\n};\nstruct SendActionUpdate {\n\tApi::SendProgressType type = Api::SendProgressType();\n\tint progress = 0;\n\tbool cancel = false;\n};\n\nenum class WriteRestrictionType {\n\tNone,\n\tRights,\n\tPremiumRequired,\n\tFrozen,\n\tHidden,\n};\n\nstruct WriteRestriction {\n\tusing Type = WriteRestrictionType;\n\n\tQString text;\n\tQString button;\n\tType type = Type::None;\n\tint boostsToLift = false;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn (type == Type::None);\n\t}\n\texplicit operator bool() const {\n\t\treturn !empty();\n\t}\n\n\tfriend inline bool operator==(\n\t\tconst WriteRestriction &a,\n\t\tconst WriteRestriction &b) = default;\n};\n\nstruct SetHistoryArgs {\n\trequired<History*> history;\n\tstd::shared_ptr<Data::GroupCall> videoStream;\n\tMsgId topicRootId = 0;\n\tPeerId monoforumPeerId = 0;\n\tFn<bool()> showSlowmodeError;\n\tFn<Api::SendAction()> sendActionFactory;\n\tFn<void(TextWithEntities, Api::SendOptions, Fn<void()>)> sendWithText;\n\trpl::producer<int> slowmodeSecondsLeft;\n\trpl::producer<bool> sendDisabledBySlowmode;\n\trpl::producer<bool> liked;\n\trpl::producer<int> minStarsCount;\n\trpl::producer<WriteRestriction> writeRestriction;\n};\n\nstruct ReplyNextRequest {\n\tenum class Direction {\n\t\tNext,\n\t\tPrevious,\n\t};\n\tconst FullMsgId replyId;\n\tconst Direction direction;\n};\n\nenum class ToggleCommentsState {\n\tEmpty,\n\tShown,\n\tHidden,\n\tWithNew,\n};\n\nstruct SendStarButtonEffect {\n\tnot_null<PeerData*> from;\n\tint stars = 0;\n};\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_characters_limit.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/controls/history_view_characters_limit.h\"\n\n#include \"ui/rect.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace {\n\nconstexpr auto kMinus = QChar(0x2212);\nconstexpr auto kLimit = int(999);\n\n[[nodiscard]] int CountDigits(int n) {\n\treturn n == 0 ? 1 : static_cast<int>(std::log10(std::abs(n))) + 1;\n}\n\n} // namespace\n\nnamespace HistoryView::Controls {\n\nCharactersLimitLabel::CharactersLimitLabel(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Ui::RpWidget*> widgetToAlign,\n\tstyle::align align,\n\tQMargins margins)\n: Ui::FlatLabel(parent, st::historyCharsLimitationLabel)\n, _widgetToAlign(widgetToAlign)\n, _position((align == style::al_top)\n\t? Fn<void(int, const QRect &)>([=](int height, const QRect &g) {\n\t\tconst auto w = textMaxWidth();\n\t\tmove(\n\t\t\tg.x() + (g.width() - w) / 2 + margins.left(),\n\t\t\trect::bottom(g) + margins.top());\n\t})\n\t: Fn<void(int, const QRect &)>([=](int height, const QRect &g) {\n\t\tconst auto w = textMaxWidth();\n\t\tmove(\n\t\t\tg.x() + (g.width() - w) / 2 + margins.left(),\n\t\t\tg.y() - height - margins.bottom());\n\t})) {\n\tExpects((align == style::al_top) || align == style::al_bottom);\n\trpl::combine(\n\t\tUi::RpWidget::heightValue(),\n\t\twidgetToAlign->geometryValue()\n\t) | rpl::on_next(_position, lifetime());\n}\n\nvoid CharactersLimitLabel::setLeft(int value) {\n\tconst auto orderChanged = (CountDigits(value) != CountDigits(_lastValue));\n\t_lastValue = value;\n\n\tif (value > 0) {\n\t\tsetTextColorOverride(st::historyCharsLimitationLabel.textFg->c);\n\t\tUi::FlatLabel::setText(kMinus\n\t\t\t+ QString::number(std::min(value, kLimit)));\n\t} else {\n\t\tsetTextColorOverride(st::windowSubTextFg->c);\n\t\tUi::FlatLabel::setText(QString::number(-value));\n\t}\n\n\tif (orderChanged) {\n\t\t_position(height(), _widgetToAlign->geometry());\n\t}\n}\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_characters_limit.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/labels.h\"\n\nnamespace HistoryView::Controls {\n\nclass CharactersLimitLabel final : public Ui::FlatLabel {\npublic:\n\tCharactersLimitLabel(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Ui::RpWidget*> widgetToAlign,\n\t\tstyle::align align,\n\t\tQMargins margins = {});\n\n\tvoid setLeft(int value);\n\nprivate:\n\tint _lastValue = 0;\n\tnot_null<Ui::RpWidget*> _widgetToAlign;\n\tFn<void(int, const QRect &)> _position;\n\n};\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_compose_ai_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/controls/history_view_compose_ai_button.h\"\n\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace HistoryView::Controls {\nnamespace {\n\nconstexpr auto kAnimationDuration = crl::time(640);\n\n} // namespace\n\nComposeAiButton::ComposeAiButton(\n\tQWidget *parent,\n\tconst style::IconButton &st)\n: RippleButton(parent, st.ripple)\n, _st(st) {\n\tresize(_st.width, _st.height);\n\tsetCursor(style::cur_pointer);\n\n\tshownValue(\n\t) | rpl::on_next([=](bool shown) {\n\t\tif (shown) {\n\t\t\t_animation.start([=] { update(); }, 0., 1., kAnimationDuration);\n\t\t} else {\n\t\t\t_animation.stop();\n\t\t}\n\t}, lifetime());\n}\n\nvoid ComposeAiButton::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\tPainterHighQualityEnabler hq(p);\n\n\tconst auto over = isDown() || isOver() || forceRippled();\n\tpaintRipple(p, _st.rippleAreaPosition);\n\n\tconst auto progress = _animation.value(1.);\n\tauto star1Opacity = 1.;\n\tauto star2Opacity = 1.;\n\tif (progress < 0.25) {\n\t\tstar1Opacity = 1. - (progress / 0.25);\n\t} else if (progress < 0.5) {\n\t\tstar1Opacity = 0.;\n\t\tstar2Opacity = 1. - ((progress - 0.25) / 0.25);\n\t} else if (progress < 0.75) {\n\t\tstar1Opacity = (progress - 0.5) / 0.25;\n\t\tstar2Opacity = 0.;\n\t} else {\n\t\tstar2Opacity = (progress - 0.75) / 0.25;\n\t}\n\n\tconst auto part = [&](const style::icon &icon) {\n\t\tif (over) {\n\t\t\ticon.paintInCenter(p, rect(), st::historyComposeIconFgOver->c);\n\t\t} else {\n\t\t\ticon.paintInCenter(p, rect());\n\t\t}\n\t};\n\tpart(st::historyAiComposeButtonLetters);\n\tif (star1Opacity > 0.) {\n\t\tp.setOpacity(star1Opacity);\n\t\tpart(st::historyAiComposeButtonStar1);\n\t}\n\tif (star2Opacity > 0.) {\n\t\tp.setOpacity(star2Opacity);\n\t\tpart(st::historyAiComposeButtonStar2);\n\t}\n}\n\nvoid ComposeAiButton::onStateChanged(State was, StateChangeSource source) {\n\tRippleButton::onStateChanged(was, source);\n\tupdate();\n}\n\nQImage ComposeAiButton::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::EllipseMask(\n\t\tQSize(_st.rippleAreaSize, _st.rippleAreaSize));\n}\n\nQPoint ComposeAiButton::prepareRippleStartPosition() const {\n\tconst auto result = mapFromGlobal(QCursor::pos()) - _st.rippleAreaPosition;\n\tconst auto rect = QRect(0, 0, _st.rippleAreaSize, _st.rippleAreaSize);\n\treturn rect.contains(result)\n\t\t? result\n\t\t: DisabledRippleStartPosition();\n}\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_compose_ai_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n#include \"ui/widgets/buttons.h\"\n\nnamespace HistoryView::Controls {\n\nclass ComposeAiButton final : public Ui::RippleButton {\npublic:\n\tComposeAiButton(QWidget *parent, const style::IconButton &st);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid onStateChanged(State was, StateChangeSource source) override;\n\n\t[[nodiscard]] QImage prepareRippleMask() const override;\n\t[[nodiscard]] QPoint prepareRippleStartPosition() const override;\n\nprivate:\n\tconst style::IconButton &_st;\n\tUi::Animations::Simple _animation;\n\n};\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_compose_ai_tooltip.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/controls/history_view_compose_ai_tooltip.h\"\n\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace HistoryView::Controls {\n\nComposeTooltipManager::ComposeTooltipManager(\n\tnot_null<QWidget*> parent,\n\tnot_null<Ui::RpWidget*> button,\n\trpl::producer<TextWithEntities> text,\n\tbase::const_string prefKey,\n\tFn<int()> widthProvider)\n: _button(button)\n, _prefKey(prefKey)\n, _widthProvider(std::move(widthProvider)) {\n\t_tooltip.reset(Ui::CreateChild<Ui::ImportantTooltip>(\n\t\tparent,\n\t\tUi::MakeTooltipWithClose(\n\t\t\tparent,\n\t\t\tstd::move(text),\n\t\t\tst::historyMessagesTTLLabel.minWidth,\n\t\t\tst::ttlMediaImportantTooltipLabel,\n\t\t\tst::importantTooltipHide,\n\t\t\tst::defaultImportantTooltip.padding,\n\t\t\t[=] { hideAndRemember(); }),\n\t\tst::historyRecordTooltip));\n\t_tooltip->toggleFast(false);\n\t_button->geometryValue(\n\t) | rpl::on_next([=](const QRect &geometry) {\n\t\tif (!geometry.isEmpty()) {\n\t\t\tupdateGeometry();\n\t\t}\n\t}, _tooltip->lifetime());\n}\n\nvoid ComposeTooltipManager::hideAndRemember() {\n\tif (!Core::App().settings().readPref<bool>(_prefKey)) {\n\t\tCore::App().settings().writePref<bool>(_prefKey, true);\n\t}\n\t_shown = false;\n\t_tooltip->toggleAnimated(false);\n}\n\nvoid ComposeTooltipManager::updateVisibility(bool buttonShown) {\n\tconst auto showTooltip = buttonShown\n\t\t&& !Core::App().settings().readPref<bool>(_prefKey);\n\tif (showTooltip) {\n\t\tupdateGeometry();\n\t}\n\tif ((_shown != showTooltip)\n\t\t|| (showTooltip && _tooltip->isHidden())) {\n\t\t_shown = showTooltip;\n\t\t_tooltip->toggleAnimated(showTooltip);\n\t}\n}\n\nvoid ComposeTooltipManager::updateGeometry() {\n\tif (_button->isHidden()) {\n\t\treturn;\n\t}\n\tconst auto geometry = _button->geometry();\n\tconst auto maxWidth = _widthProvider();\n\tconst auto countPosition = [=](QSize size) {\n\t\tconst auto left = geometry.x()\n\t\t\t+ geometry.width()\n\t\t\t- size.width();\n\t\treturn QPoint(\n\t\t\tstd::max(std::min(left, maxWidth - size.width()), 0),\n\t\t\tgeometry.y() - size.height() - st::historyAiComposeTooltipSkip);\n\t};\n\t_tooltip->pointAt(geometry, RectPart::Top, countPosition);\n}\n\nvoid ComposeTooltipManager::raise() {\n\t_tooltip->raise();\n}\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_compose_ai_tooltip.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/const_string.h\"\n#include \"base/unique_qptr.h\"\n\nnamespace Ui {\nclass ImportantTooltip;\nclass RpWidget;\n} // namespace Ui\n\nnamespace HistoryView::Controls {\n\nclass ComposeTooltipManager final {\npublic:\n\tComposeTooltipManager(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Ui::RpWidget*> button,\n\t\trpl::producer<TextWithEntities> text,\n\t\tbase::const_string prefKey,\n\t\tFn<int()> widthProvider);\n\n\tvoid hideAndRemember();\n\tvoid updateVisibility(bool buttonShown);\n\tvoid updateGeometry();\n\tvoid raise();\n\nprivate:\n\tconst not_null<Ui::RpWidget*> _button;\n\tconst base::const_string _prefKey;\n\tconst Fn<int()> _widthProvider;\n\tbase::unique_qptr<Ui::ImportantTooltip> _tooltip;\n\tbool _shown = false;\n\n};\n\nusing AiTooltipManager = ComposeTooltipManager;\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/controls/history_view_compose_controls.h\"\n\n#include \"base/call_delayed.h\"\n#include \"base/event_filter.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"base/random.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/compose_ai_box.h\"\n#include \"boxes/edit_caption_box.h\"\n#include \"boxes/send_files_box.h\"\n#include \"calls/group/ui/calls_group_stars_coloring.h\"\n#include \"calls/group/calls_group_stars_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_section.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"chat_helpers/field_autocomplete.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/shortcuts.h\"\n#include \"core/ui_integration.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_drafts.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_messages.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_document.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_premium_limits.h\" // Data::PremiumLimits.\n#include \"data/stickers/data_stickers.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_web_page.h\"\n#include \"storage/storage_account.h\"\n#include \"apiwrap.h\"\n#include \"api/api_chat_participants.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/color_int_conversion.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/controls/history_view_characters_limit.h\"\n#include \"history/view/controls/history_view_compose_ai_button.h\"\n#include \"history/view/controls/history_view_compose_ai_tooltip.h\"\n#include \"history/view/controls/history_view_compose_media_edit_manager.h\"\n#include \"history/view/controls/history_view_forward_panel.h\"\n#include \"history/view/controls/history_view_draft_options.h\"\n#include \"history/view/controls/history_view_suggest_options.h\"\n#include \"history/view/controls/history_view_ttl_button.h\"\n#include \"history/view/controls/history_view_voice_record_bar.h\"\n#include \"history/view/controls/history_view_webpage_processor.h\"\n#include \"history/view/history_view_reply.h\"\n#include \"history/view/history_view_schedule_box.h\"\n#include \"history/view/history_view_webpage_preview.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n#include \"inline_bots/inline_results_widget.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"main/session/send_as_peers.h\"\n#include \"media/audio/media_audio_capture.h\"\n#include \"media/audio/media_audio.h\"\n#include \"menu/menu_checked_action.h\"\n#include \"menu/menu_send.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/item_text_options.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/controls/compose_ai_button_factory.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/controls/send_button.h\"\n#include \"ui/controls/send_as_button.h\"\n#include \"ui/controls/silent_toggle.h\"\n#include \"ui/chat/choose_send_as.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/effects/reaction_fly_animation.h\"\n#include \"webrtc/webrtc_environment.h\"\n#include \"window/window_adaptive.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"mainwindow.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kSaveDraftTimeout = crl::time(1000);\nconstexpr auto kSaveDraftAnywayTimeout = 5 * crl::time(1000);\nconstexpr auto kSaveCloudDraftIdleTimeout = 14 * crl::time(1000);\nconstexpr auto kMouseEvents = {\n\tQEvent::MouseMove,\n\tQEvent::MouseButtonPress,\n\tQEvent::MouseButtonRelease\n};\nconstexpr auto kRefreshSlowmodeLabelTimeout = crl::time(200);\nconstexpr auto kMaxStarSendEffects = 4;\nconstexpr auto kMaxStarEffects = 4;\nconstexpr auto kStarEffectDuration = 2 * crl::time(1000);\nconstexpr auto kStarEffectRotationMax = 12;\nconstexpr auto kStarEffectScaleMin = 0.3;\nconstexpr auto kStarEffectScaleMax = 0.7;\n\nconstexpr auto kCommonModifiers = 0\n\t| Qt::ShiftModifier\n\t| Qt::MetaModifier\n\t| Qt::ControlModifier;\n\nusing FileChosen = ComposeControls::FileChosen;\nusing PhotoChosen = ComposeControls::PhotoChosen;\nusing MessageToEdit = ComposeControls::MessageToEdit;\nusing VoiceToSend = ComposeControls::VoiceToSend;\nusing SendActionUpdate = ComposeControls::SendActionUpdate;\nusing SetHistoryArgs = ComposeControls::SetHistoryArgs;\nusing VoiceRecordBar = Controls::VoiceRecordBar;\nusing ForwardPanel = Controls::ForwardPanel;\n\n} // namespace\n\nconst ChatHelpers::PauseReason kDefaultPanelsLevel\n\t= ChatHelpers::PauseReason::TabbedPanel;\n\nclass FieldHeader final : public Ui::RpWidget {\npublic:\n\tFieldHeader(\n\t\tQWidget *parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tFn<bool()> hasSendText);\n\n\tvoid setHistory(const SetHistoryArgs &args);\n\tvoid updateTopicRootId(MsgId topicRootId);\n\tvoid init();\n\n\tvoid editMessage(\n\t\tFullMsgId id,\n\t\tSuggestOptions suggest,\n\t\tbool photoEditAllowed = false);\n\tvoid replyToMessage(FullReplyTo id);\n\tvoid updateForwarding(\n\t\tData::Thread *thread,\n\t\tData::ResolvedForwardDraft items);\n\tvoid previewReady(rpl::producer<Controls::WebpageParsed> parsed);\n\tvoid previewUnregister();\n\n\tvoid mediaEditManagerApply(SendMenu::Action action);\n\n\t[[nodiscard]] bool isDisplayed() const;\n\t[[nodiscard]] bool isEditingMessage() const;\n\t[[nodiscard]] bool readyToForward() const;\n\t[[nodiscard]] const HistoryItemsList &forwardItems() const;\n\t[[nodiscard]] const Data::ResolvedForwardDraft &forwardDraft() const;\n\t[[nodiscard]] FullReplyTo replyingToMessage() const;\n\t[[nodiscard]] FullMsgId editMsgId() const;\n\t[[nodiscard]] rpl::producer<FullMsgId> editMsgIdValue() const;\n\t[[nodiscard]] rpl::producer<FullReplyTo> jumpToItemRequests() const;\n\t[[nodiscard]] rpl::producer<> editPhotoRequests() const;\n\t[[nodiscard]] rpl::producer<> editOptionsRequests() const;\n\t[[nodiscard]] MessageToEdit queryToEdit();\n\t[[nodiscard]] SendMenu::Details saveMenuDetails(bool hasSendText) const;\n\n\t[[nodiscard]] FullReplyTo getDraftReply() const;\n\t[[nodiscard]] SuggestOptions suggestOptions() const;\n\t[[nodiscard]] rpl::producer<> editCancelled() const {\n\t\treturn _editCancelled.events();\n\t}\n\t[[nodiscard]] rpl::producer<> replyCancelled() const {\n\t\treturn _replyCancelled.events();\n\t}\n\t[[nodiscard]] rpl::producer<> forwardCancelled() const {\n\t\treturn _forwardCancelled.events();\n\t}\n\t[[nodiscard]] rpl::producer<> previewCancelled() const {\n\t\treturn _previewCancelled.events();\n\t}\n\t[[nodiscard]] rpl::producer<> saveDraftRequests() const {\n\t\treturn _saveDraftRequests.events();\n\t}\n\n\t[[nodiscard]] rpl::producer<bool> visibleChanged();\n\nprivate:\n\tvoid updateControlsGeometry(QSize size);\n\tvoid updateVisible();\n\tvoid setShownMessage(HistoryItem *message);\n\tvoid resolveMessageData();\n\tvoid updateShownMessageText();\n\tvoid customEmojiRepaint();\n\n\tvoid paintWebPage(Painter &p, not_null<PeerData*> peer);\n\tvoid paintEditOrReplyToMessage(Painter &p);\n\tvoid paintForwardInfo(Painter &p);\n\n\tbool hasPreview() const;\n\n\tvoid applySuggestOptions(SuggestOptions suggest, SuggestMode mode);\n\tvoid cancelSuggestPost();\n\n\tstruct Preview {\n\t\tControls::WebpageParsed parsed;\n\t\tUi::Text::String title;\n\t\tUi::Text::String description;\n\t};\n\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst Fn<bool()> _hasSendText;\n\n\tHistory *_history = nullptr;\n\tMsgId _topicRootId = 0;\n\tPeerId _monoforumPeerId = 0;\n\n\tPreview _preview;\n\trpl::event_stream<> _editCancelled;\n\trpl::event_stream<> _replyCancelled;\n\trpl::event_stream<> _forwardCancelled;\n\trpl::event_stream<> _previewCancelled;\n\trpl::event_stream<> _saveDraftRequests;\n\trpl::lifetime _previewLifetime;\n\n\trpl::variable<FullMsgId> _editMsgId;\n\trpl::variable<FullReplyTo> _replyTo;\n\tstd::unique_ptr<ForwardPanel> _forwardPanel;\n\tstd::unique_ptr<SuggestOptionsBar> _suggestOptions;\n\trpl::producer<> _toForwardUpdated;\n\n\tHistoryItem *_shownMessage = nullptr;\n\tUi::Text::String _shownMessageName;\n\tUi::Text::String _shownMessageText;\n\tstd::unique_ptr<Ui::SpoilerAnimation> _shownPreviewSpoiler;\n\tUi::Animations::Simple _inPhotoEditOver;\n\tbool _shownMessageHasPreview : 1 = false;\n\tbool _inPhotoEdit : 1 = false;\n\tbool _photoEditAllowed : 1 = false;\n\tbool _repaintScheduled : 1 = false;\n\tbool _inClickable : 1 = false;\n\n\tHistoryView::MediaEditManager _mediaEditManager;\n\n\tconst not_null<Data::Session*> _data;\n\tconst not_null<Ui::IconButton*> _cancel;\n\n\tQRect _clickableRect;\n\tQRect _shownMessagePreviewRect;\n\n\trpl::event_stream<bool> _visibleChanged;\n\trpl::event_stream<FullReplyTo> _jumpToItemRequests;\n\trpl::event_stream<> _editOptionsRequests;\n\trpl::event_stream<> _editPhotoRequests;\n\n};\n\nFieldHeader::FieldHeader(\n\tQWidget *parent,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tFn<bool()> hasSendText)\n: RpWidget(parent)\n, _show(std::move(show))\n, _hasSendText(std::move(hasSendText))\n, _forwardPanel(\n\tstd::make_unique<ForwardPanel>([=] { customEmojiRepaint(); }))\n, _data(&_show->session().data())\n, _cancel(Ui::CreateChild<Ui::IconButton>(this, st::historyReplyCancel)) {\n\t_cancel->setAccessibleName(tr::lng_cancel(tr::now));\n\tresize(QSize(parent->width(), st::historyReplyHeight));\n\tinit();\n}\n\nvoid FieldHeader::setHistory(const SetHistoryArgs &args) {\n\t_history = *args.history;\n\t_topicRootId = args.topicRootId;\n\t_monoforumPeerId = args.monoforumPeerId;\n}\n\nvoid FieldHeader::updateTopicRootId(MsgId topicRootId) {\n\t_topicRootId = topicRootId;\n}\n\nvoid FieldHeader::init() {\n\tsizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tupdateControlsGeometry(size);\n\t}, lifetime());\n\n\t_forwardPanel->itemsUpdated(\n\t) | rpl::on_next([=] {\n\t\tupdateVisible();\n\t}, lifetime());\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(this);\n\t\tp.setInactive(_show->paused(Window::GifPauseReason::Any));\n\t\tp.fillRect(rect(), st::historyComposeAreaBg);\n\n\t\tconst auto position = st::historyReplyIconPosition;\n\t\tif (_suggestOptions) {\n\t\t\t_suggestOptions->paintIcon(p, 0, 0, width());\n\t\t} else if (_preview.parsed) {\n\t\t\tst::historyLinkIcon.paint(p, position, width());\n\t\t} else if (isEditingMessage()) {\n\t\t\tst::historyEditIcon.paint(p, position, width());\n\t\t} else if (const auto reply = replyingToMessage(); reply.replying()) {\n\t\t\tif (!reply.quote.empty()) {\n\t\t\t\tst::historyQuoteIcon.paint(p, position, width());\n\t\t\t} else {\n\t\t\t\tst::historyReplyIcon.paint(p, position, width());\n\t\t\t}\n\t\t} else if (readyToForward()) {\n\t\t\tst::historyForwardIcon.paint(p, position, width());\n\t\t}\n\n\t\tif (_preview.parsed) {\n\t\t\tpaintWebPage(\n\t\t\t\tp,\n\t\t\t\t_history ? _history->peer : _data->session().user());\n\t\t} else if (isEditingMessage() || replyingToMessage()) {\n\t\t\tpaintEditOrReplyToMessage(p);\n\t\t} else if (readyToForward()) {\n\t\t\tpaintForwardInfo(p);\n\t\t}\n\t}, lifetime());\n\n\t_editMsgId.value(\n\t) | rpl::on_next([=](FullMsgId value) {\n\t\tconst auto shown = value ? value : _replyTo.current().messageId;\n\t\tsetShownMessage(_data->message(shown));\n\t}, lifetime());\n\n\t_replyTo.value(\n\t) | rpl::on_next([=](const FullReplyTo &value) {\n\t\tif (!_editMsgId.current()) {\n\t\t\tsetShownMessage(_data->message(value.messageId));\n\t\t}\n\t}, lifetime());\n\n\t_data->session().changes().messageUpdates(\n\t\tData::MessageUpdate::Flag::Edited\n\t\t| Data::MessageUpdate::Flag::Destroyed\n\t) | rpl::filter([=](const Data::MessageUpdate &update) {\n\t\treturn (update.item == _shownMessage);\n\t}) | rpl::on_next([=](const Data::MessageUpdate &update) {\n\t\tif (update.flags & Data::MessageUpdate::Flag::Destroyed) {\n\t\t\tif (_editMsgId.current() == update.item->fullId()) {\n\t\t\t\t_editCancelled.fire({});\n\t\t\t}\n\t\t\tif (_replyTo.current().messageId == update.item->fullId()) {\n\t\t\t\t_replyCancelled.fire({});\n\t\t\t}\n\t\t} else {\n\t\t\tupdateShownMessageText();\n\t\t}\n\t}, lifetime());\n\n\t_cancel->addClickHandler([=] {\n\t\tif (hasPreview()) {\n\t\t\t_preview = {};\n\t\t\t_previewCancelled.fire({});\n\t\t} else if (_editMsgId.current()) {\n\t\t\t_editCancelled.fire({});\n\t\t} else if (_replyTo.current()) {\n\t\t\t_replyCancelled.fire({});\n\t\t} else if (readyToForward()) {\n\t\t\t_forwardCancelled.fire({});\n\t\t}\n\t\tupdateVisible();\n\t\tupdate();\n\t});\n\n\tsetMouseTracking(true);\n\tevents(\n\t) | rpl::filter([=](not_null<QEvent*> event) {\n\t\tconst auto type = event->type();\n\t\tconst auto leaving = (type == QEvent::Leave);\n\t\treturn (ranges::contains(kMouseEvents, type) || leaving)\n\t\t\t&& (isEditingMessage()\n\t\t\t\t|| readyToForward()\n\t\t\t\t|| replyingToMessage()\n\t\t\t\t|| _preview.parsed);\n\t}) | rpl::on_next([=](not_null<QEvent*> event) {\n\t\tconst auto updateOver = [&](bool inClickable, bool inPhotoEdit) {\n\t\t\tif (_inClickable != inClickable) {\n\t\t\t\t_inClickable = inClickable;\n\t\t\t\tsetCursor(_inClickable\n\t\t\t\t\t? style::cur_pointer\n\t\t\t\t\t: style::cur_default);\n\t\t\t}\n\t\t\tif (_inPhotoEdit != inPhotoEdit) {\n\t\t\t\t_inPhotoEdit = inPhotoEdit;\n\t\t\t\t_inPhotoEditOver.start(\n\t\t\t\t\t[=] { update(); },\n\t\t\t\t\t_inPhotoEdit ? 0. : 1.,\n\t\t\t\t\t_inPhotoEdit ? 1. : 0.,\n\t\t\t\t\tst::defaultMessageBar.duration);\n\t\t\t}\n\t\t};\n\t\tconst auto type = event->type();\n\t\tif (type == QEvent::Leave) {\n\t\t\tupdateOver(false, false);\n\t\t\treturn;\n\t\t}\n\t\tconst auto e = static_cast<QMouseEvent*>(event.get());\n\t\tconst auto pos = e->pos();\n\t\tconst auto inPreviewRect = _clickableRect.contains(pos);\n\t\tconst auto inPhotoEdit = _shownMessageHasPreview\n\t\t\t&& _photoEditAllowed\n\t\t\t&& _shownMessagePreviewRect.contains(pos);\n\n\t\tif (type == QEvent::MouseMove) {\n\t\t\tupdateOver(inPreviewRect, inPhotoEdit);\n\t\t\treturn;\n\t\t}\n\t\tconst auto isLeftButton = (e->button() == Qt::LeftButton);\n\t\tif (type == QEvent::MouseButtonPress) {\n\t\t\tif (isLeftButton && inPhotoEdit) {\n\t\t\t\t_editPhotoRequests.fire({});\n\t\t\t} else if (isLeftButton && inPreviewRect) {\n\t\t\t\tconst auto reply = replyingToMessage();\n\t\t\t\tif (_preview.parsed) {\n\t\t\t\t\t_editOptionsRequests.fire({});\n\t\t\t\t} else if (isEditingMessage()) {\n\t\t\t\t\tif (_suggestOptions) {\n\t\t\t\t\t\t_suggestOptions->edit();\n\t\t\t\t\t} else {\n\t\t\t\t\t\t_jumpToItemRequests.fire(FullReplyTo{\n\t\t\t\t\t\t\t.messageId = _editMsgId.current()\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} else if (reply && (e->modifiers() & Qt::ControlModifier)) {\n\t\t\t\t\t_jumpToItemRequests.fire_copy(reply);\n\t\t\t\t} else if (reply || readyToForward()) {\n\t\t\t\t\t_editOptionsRequests.fire({});\n\t\t\t\t}\n\t\t\t} else if (!isLeftButton) {\n\t\t\t\tif (inPreviewRect && isEditingMessage()) {\n\t\t\t\t\t_mediaEditManager.showMenu(\n\t\t\t\t\t\tthis,\n\t\t\t\t\t\t[=] { update(); },\n\t\t\t\t\t\t_hasSendText());\n\t\t\t\t} else if (const auto reply = replyingToMessage()) {\n\t\t\t\t\t_jumpToItemRequests.fire_copy(reply);\n\t\t\t\t} else if (readyToForward()) {\n\t\t\t\t\t_forwardPanel->editToNextOption();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, lifetime());\n}\n\nvoid FieldHeader::updateShownMessageText() {\n\tExpects(_shownMessage != nullptr);\n\n\tconst auto context = Core::TextContext({\n\t\t.session = &_data->session(),\n\t\t.repaint = [=] { customEmojiRepaint(); },\n\t});\n\tconst auto reply = replyingToMessage();\n\t_shownMessageText.setMarkedText(\n\t\tst::messageTextStyle,\n\t\t((isEditingMessage() || reply.quote.empty())\n\t\t\t? _shownMessage->inReplyText()\n\t\t\t: reply.quote),\n\t\tUi::DialogTextOptions(),\n\t\tcontext);\n}\n\nvoid FieldHeader::customEmojiRepaint() {\n\tif (_repaintScheduled) {\n\t\treturn;\n\t}\n\t_repaintScheduled = true;\n\tupdate();\n}\n\nvoid FieldHeader::setShownMessage(HistoryItem *item) {\n\t_shownMessage = item;\n\tif (item) {\n\t\tupdateShownMessageText();\n\t} else {\n\t\t_shownMessageText.clear();\n\t\tresolveMessageData();\n\t}\n\tif (isEditingMessage()) {\n\t\t_shownMessageName.setText(\n\t\t\tst::msgNameStyle,\n\t\t\ttr::lng_edit_message(tr::now),\n\t\t\tUi::NameTextOptions());\n\t} else if (item) {\n\t\tconst auto context = Core::TextContext({\n\t\t\t.session = &_history->session(),\n\t\t\t.customEmojiLoopLimit = 1,\n\t\t});\n\t\tconst auto replyTo = _replyTo.current();\n\t\t_shownMessageName.setMarkedText(\n\t\t\tst::fwdTextStyle,\n\t\t\tHistoryView::Reply::ComposePreviewName(_history, item, replyTo),\n\t\t\tUi::NameTextOptions(),\n\t\t\tcontext);\n\t} else {\n\t\t_shownMessageName.clear();\n\t}\n\tupdateVisible();\n\tupdate();\n}\n\nvoid FieldHeader::resolveMessageData() {\n\tconst auto id = isEditingMessage()\n\t\t? _editMsgId.current()\n\t\t: _replyTo.current().messageId;\n\tif (!id) {\n\t\treturn;\n\t}\n\tconst auto peer = _data->peer(id.peer);\n\tconst auto itemId = id.msg;\n\tconst auto callback = crl::guard(this, [=] {\n\t\tconst auto now = isEditingMessage()\n\t\t\t? _editMsgId.current()\n\t\t\t: _replyTo.current().messageId;\n\t\tif (now == id && !_shownMessage) {\n\t\t\tif (const auto message = _data->message(peer, itemId)) {\n\t\t\t\tsetShownMessage(message);\n\t\t\t} else if (isEditingMessage()) {\n\t\t\t\t_editCancelled.fire({});\n\t\t\t} else {\n\t\t\t\t_replyCancelled.fire({});\n\t\t\t}\n\t\t}\n\t});\n\t_data->session().api().requestMessageData(peer, itemId, callback);\n}\n\nvoid FieldHeader::previewReady(\n\t\trpl::producer<Controls::WebpageParsed> parsed) {\n\t_previewLifetime.destroy();\n\n\tstd::move(\n\t\tparsed\n\t) | rpl::on_next([=](Controls::WebpageParsed parsed) {\n\t\t_preview.parsed = std::move(parsed);\n\t\t_preview.title.setText(\n\t\t\tst::msgNameStyle,\n\t\t\t_preview.parsed.title,\n\t\t\tUi::NameTextOptions());\n\t\t_preview.description.setText(\n\t\t\tst::messageTextStyle,\n\t\t\t_preview.parsed.description,\n\t\t\tUi::DialogTextOptions());\n\t\tupdateVisible();\n\t}, _previewLifetime);\n}\n\nvoid FieldHeader::previewUnregister() {\n\t_previewLifetime.destroy();\n}\n\nvoid FieldHeader::mediaEditManagerApply(SendMenu::Action action) {\n\t_mediaEditManager.apply(action);\n}\n\nvoid FieldHeader::paintWebPage(Painter &p, not_null<PeerData*> context) {\n\tExpects(!!_preview.parsed);\n\n\tconst auto textTop = st::msgReplyPadding.top();\n\tauto previewLeft = st::historyReplySkip;\n\n\tconst QRect to(\n\t\tpreviewLeft,\n\t\t(st::historyReplyHeight - st::historyReplyPreview) / 2,\n\t\tst::historyReplyPreview,\n\t\tst::historyReplyPreview);\n\tif (_preview.parsed.drawPreview(p, to)) {\n\t\tpreviewLeft += st::historyReplyPreview + st::msgReplyBarSkip;\n\t}\n\tconst auto elidedWidth = width()\n\t\t- previewLeft\n\t\t- _cancel->width()\n\t\t- st::msgReplyPadding.right();\n\n\tp.setPen(st::historyReplyNameFg);\n\t_preview.title.drawElided(\n\t\tp,\n\t\tpreviewLeft,\n\t\ttextTop,\n\t\telidedWidth);\n\n\tp.setPen(st::historyComposeAreaFg);\n\t_preview.description.drawElided(\n\t\tp,\n\t\tpreviewLeft,\n\t\ttextTop + st::msgServiceNameFont->height,\n\t\telidedWidth);\n}\n\nvoid FieldHeader::paintEditOrReplyToMessage(Painter &p) {\n\t_repaintScheduled = false;\n\n\tconst auto replySkip = st::historyReplySkip;\n\tconst auto availableWidth = width()\n\t\t- replySkip\n\t\t- _cancel->width()\n\t\t- st::msgReplyPadding.right();\n\n\tif (!_shownMessage) {\n\t\tp.setFont(st::msgDateFont);\n\t\tp.setPen(st::historyComposeAreaFgService);\n\t\tconst auto top = (st::historyReplyHeight - st::msgDateFont->height) / 2;\n\t\tp.drawText(\n\t\t\treplySkip,\n\t\t\ttop + st::msgDateFont->ascent,\n\t\t\tst::msgDateFont->elided(\n\t\t\t\ttr::lng_profile_loading(tr::now),\n\t\t\t\tavailableWidth));\n\t\treturn;\n\t}\n\n\tconst auto media = _shownMessage->media();\n\tconst auto poll = media ? media->poll() : nullptr;\n\tconst auto reply = replyingToMessage();\n\tconst auto pollAnswer = poll\n\t\t? poll->answerByOption(reply.pollOption)\n\t\t: nullptr;\n\tconst auto pollMediaPtr = pollAnswer\n\t\t? &pollAnswer->media\n\t\t: (poll && reply.pollOption.isEmpty())\n\t\t? &poll->attachedMedia\n\t\t: nullptr;\n\tconst auto pollMediaHasPreview = pollMediaPtr\n\t\t&& (pollMediaPtr->photo || pollMediaPtr->document);\n\t_shownMessageHasPreview = pollMediaHasPreview\n\t\t|| (media && media->hasReplyPreview());\n\tconst auto preview = _mediaEditManager\n\t\t? _mediaEditManager.mediaPreview()\n\t\t: pollMediaHasPreview\n\t\t? (pollMediaPtr->photo\n\t\t\t? pollMediaPtr->photo->getReplyPreview(_shownMessage)\n\t\t\t: pollMediaPtr->document->getReplyPreview(_shownMessage))\n\t\t: (media && media->hasReplyPreview())\n\t\t? media->replyPreview()\n\t\t: nullptr;\n\tconst auto spoilered = _mediaEditManager.spoilered();\n\tif (!spoilered) {\n\t\t_shownPreviewSpoiler = nullptr;\n\t} else if (!_shownPreviewSpoiler) {\n\t\t_shownPreviewSpoiler = std::make_unique<Ui::SpoilerAnimation>([=] {\n\t\t\tupdate();\n\t\t});\n\t}\n\tconst auto previewSkipValue = st::historyReplyPreview\n\t\t+ st::msgReplyBarSkip;\n\tconst auto previewSkip = _shownMessageHasPreview ? previewSkipValue : 0;\n\tconst auto textLeft = replySkip + previewSkip;\n\tconst auto textAvailableWidth = availableWidth - previewSkip;\n\tif (preview) {\n\t\tconst auto overEdit = _photoEditAllowed\n\t\t\t? _inPhotoEditOver.value(_inPhotoEdit ? 1. : 0.)\n\t\t\t: 0.;\n\t\tconst auto to = QRect(\n\t\t\treplySkip,\n\t\t\t(st::historyReplyHeight - st::historyReplyPreview) / 2,\n\t\t\tst::historyReplyPreview,\n\t\t\tst::historyReplyPreview);\n\t\tp.drawPixmap(to.x(), to.y(), preview->pixSingle(\n\t\t\tpreview->size() / style::DevicePixelRatio(),\n\t\t\t{\n\t\t\t\t.options = Images::Option::RoundSmall,\n\t\t\t\t.outer = to.size(),\n\t\t\t}));\n\t\tif (_shownPreviewSpoiler) {\n\t\t\tif (overEdit > 0.) {\n\t\t\t\tp.setOpacity(1. - overEdit);\n\t\t\t}\n\t\t\tUi::FillSpoilerRect(\n\t\t\t\tp,\n\t\t\t\tto,\n\t\t\t\tUi::DefaultImageSpoiler().frame(\n\t\t\t\t\t_shownPreviewSpoiler->index(crl::now(), p.inactive())));\n\t\t}\n\t\tif (overEdit > 0.) {\n\t\t\tp.setOpacity(overEdit);\n\t\t\tp.fillRect(to, st::historyEditMediaBg);\n\t\t\tst::historyEditMedia.paintInCenter(p, to);\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t}\n\n\tif (_suggestOptions) {\n\t\t_suggestOptions->paintLines(p, textLeft, 0, width());\n\t\treturn;\n\t}\n\n\tp.setPen(st::historyReplyNameFg);\n\tp.setFont(st::msgServiceNameFont);\n\t_shownMessageName.drawElided(\n\t\tp,\n\t\ttextLeft,\n\t\tst::msgReplyPadding.top(),\n\t\ttextAvailableWidth);\n\n\tp.setPen(st::historyComposeAreaFg);\n\t_shownMessageText.draw(p, {\n\t\t.position = QPoint(\n\t\t\ttextLeft,\n\t\t\tst::msgReplyPadding.top() + st::msgServiceNameFont->height),\n\t\t.availableWidth = textAvailableWidth,\n\t\t.palette = &st::historyComposeAreaPalette,\n\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t.now = crl::now(),\n\t\t.pausedEmoji = p.inactive() || On(PowerSaving::kEmojiChat),\n\t\t.pausedSpoiler = p.inactive() || On(PowerSaving::kChatSpoiler),\n\t\t.elisionLines = 1,\n\t});\n}\n\nvoid FieldHeader::paintForwardInfo(Painter &p) {\n\t_repaintScheduled = false;\n\n\tconst auto replySkip = st::historyReplySkip;\n\tconst auto availableWidth = width()\n\t\t- replySkip\n\t\t- _cancel->width()\n\t\t- st::msgReplyPadding.right();\n\t_forwardPanel->paint(p, replySkip, 0, availableWidth, width());\n}\n\nvoid FieldHeader::updateVisible() {\n\tisDisplayed() ? show() : hide();\n\t_visibleChanged.fire(isVisible());\n}\n\nrpl::producer<bool> FieldHeader::visibleChanged() {\n\treturn _visibleChanged.events();\n}\n\nbool FieldHeader::isDisplayed() const {\n\treturn isEditingMessage()\n\t\t|| readyToForward()\n\t\t|| replyingToMessage()\n\t\t|| hasPreview();\n}\n\nbool FieldHeader::isEditingMessage() const {\n\treturn !!_editMsgId.current();\n}\n\nFullMsgId FieldHeader::editMsgId() const {\n\treturn _editMsgId.current();\n}\n\nbool FieldHeader::readyToForward() const {\n\treturn !_forwardPanel->empty();\n}\n\nconst HistoryItemsList &FieldHeader::forwardItems() const {\n\treturn _forwardPanel->items();\n}\n\nconst Data::ResolvedForwardDraft &FieldHeader::forwardDraft() const {\n\treturn _forwardPanel->draft();\n}\n\nFullReplyTo FieldHeader::replyingToMessage() const {\n\treturn _replyTo.current();\n}\n\nbool FieldHeader::hasPreview() const {\n\treturn !!_preview.parsed;\n}\n\nFullReplyTo FieldHeader::getDraftReply() const {\n\treturn isEditingMessage()\n\t\t? FullReplyTo{ _editMsgId.current() }\n\t\t: _replyTo.current();\n}\n\nSuggestOptions FieldHeader::suggestOptions() const {\n\treturn _suggestOptions\n\t\t? _suggestOptions->values()\n\t\t: SuggestOptions();\n}\n\nvoid FieldHeader::updateControlsGeometry(QSize size) {\n\t_cancel->moveToRight(0, 0);\n\t_clickableRect = QRect(\n\t\t0,\n\t\t0,\n\t\twidth() - _cancel->width(),\n\t\theight());\n\t_shownMessagePreviewRect = QRect(\n\t\tst::historyReplySkip,\n\t\t(st::historyReplyHeight - st::historyReplyPreview) / 2,\n\t\tst::historyReplyPreview,\n\t\tst::historyReplyPreview);\n}\n\nvoid FieldHeader::editMessage(\n\t\tFullMsgId id,\n\t\tSuggestOptions suggest,\n\t\tbool photoEditAllowed) {\n\t_photoEditAllowed = photoEditAllowed;\n\t_editMsgId = id;\n\tif (!id) {\n\t\t_mediaEditManager.cancel();\n\t} else if (const auto item = _show->session().data().message(id)) {\n\t\t_mediaEditManager.start(item);\n\t}\n\tif (!photoEditAllowed) {\n\t\t_inPhotoEdit = false;\n\t\t_inPhotoEditOver.stop();\n\t}\n\tif (id && suggest) {\n\t\tapplySuggestOptions(suggest, SuggestMode::Change);\n\t} else {\n\t\tcancelSuggestPost();\n\t}\n\tupdate();\n}\n\nvoid FieldHeader::applySuggestOptions(\n\t\tSuggestOptions suggest,\n\t\tSuggestMode mode) {\n\tExpects(suggest.exists);\n\n\tusing namespace HistoryView;\n\t_suggestOptions = std::make_unique<SuggestOptionsBar>(\n\t\t_show,\n\t\t_history->peer,\n\t\tsuggest,\n\t\tmode);\n\t_suggestOptions->updates() | rpl::on_next([=] {\n\t\tupdate();\n\t\t_saveDraftRequests.fire({});\n\t}, _suggestOptions->lifetime());\n}\n\nvoid FieldHeader::cancelSuggestPost() {\n\tif (!_suggestOptions) {\n\t\treturn;\n\t}\n\t_suggestOptions = nullptr;\n}\n\nvoid FieldHeader::replyToMessage(FullReplyTo id) {\n\tid.monoforumPeerId = 0;\n\t_replyTo = id;\n}\n\nvoid FieldHeader::updateForwarding(\n\t\tData::Thread *thread,\n\t\tData::ResolvedForwardDraft items) {\n\t_forwardPanel->update(thread, std::move(items));\n\tupdateControlsGeometry(size());\n}\n\nrpl::producer<FullMsgId> FieldHeader::editMsgIdValue() const {\n\treturn _editMsgId.value();\n}\n\nrpl::producer<FullReplyTo> FieldHeader::jumpToItemRequests() const {\n\treturn _jumpToItemRequests.events();\n}\n\nrpl::producer<> FieldHeader::editPhotoRequests() const {\n\treturn _editPhotoRequests.events();\n}\n\nrpl::producer<> FieldHeader::editOptionsRequests() const {\n\treturn _editOptionsRequests.events();\n}\n\nMessageToEdit FieldHeader::queryToEdit() {\n\tconst auto item = _data->message(_editMsgId.current());\n\tif (!isEditingMessage() || !item) {\n\t\treturn {};\n\t}\n\treturn {\n\t\t.fullId = item->fullId(),\n\t\t.options = {\n\t\t\t.scheduled = item->isScheduled() ? item->date() : 0,\n\t\t\t.shortcutId = item->shortcutId(),\n\t\t\t.invertCaption = _mediaEditManager.invertCaption(),\n\t\t\t.suggest = suggestOptions(),\n\t\t},\n\t\t.spoilered = _mediaEditManager.spoilered(),\n\t};\n}\n\nSendMenu::Details FieldHeader::saveMenuDetails(bool hasSendText) const {\n\treturn isEditingMessage()\n\t\t? _mediaEditManager.sendMenuDetails(hasSendText)\n\t\t: SendMenu::Details();\n}\n\nstruct ComposeControls::StarEffect {\n\tStarEffect(\n\t\tnot_null<Ui::RpWidget*> canvas,\n\t\tSendStarButtonEffect effect);\n\n\tUi::ReactionFlyAnimation around;\n\tUi::PeerUserpicView userpic;\n\tQImage badge;\n\tnot_null<PeerData*> from;\n\tcrl::time start = 0;\n\tfloat64 shift = 0.;\n\tfloat64 progress = 0.;\n\tint stars = 0;\n};\n\nComposeControls::StarEffect::StarEffect(\n\tnot_null<Ui::RpWidget*> canvas,\n\tSendStarButtonEffect effect)\n: around(\n\t&effect.from->owner().reactions(),\n\tUi::ReactionFlyAnimationArgs{\n\t\t.id = Data::ReactionId::Paid(),\n\t\t.effectOnly = true,\n\t},\n\t[canvas] { canvas->update(); },\n\tst::reactionInlineImage)\n, from(effect.from)\n, start(crl::now())\n, stars(effect.stars) {\n\tauto price = Ui::Text::String(\n\t\tst::whoReadDateStyle,\n\t\tUi::Text::IconEmoji(\n\t\t\t&st::starIconEmojiSmall\n\t\t).append(Lang::FormatCountDecimal(stars)),\n\t\tkMarkupTextOptions);\n\tconst auto padding = st::groupCallEffectPadding;\n\tconst auto priceHeight = st::whoReadDateStyle.font->height;\n\tconst auto priceTop = padding.top();\n\tconst auto height = priceTop + priceHeight + padding.bottom();\n\n\tconst auto userpicPadding = st::groupCallEffectUserpicPadding;\n\tconst auto userpicSize = height\n\t\t- userpicPadding.top()\n\t\t- userpicPadding.bottom();\n\n\tconst auto leftSkip = userpicPadding.left()\n\t\t+ userpicSize\n\t\t+ userpicPadding.right();\n\tconst auto widthSkip = leftSkip + padding.right();\n\n\tconst auto width = widthSkip + price.maxWidth();\n\tconst auto priceLeft = leftSkip;\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tbadge = QImage(\n\t\tQSize(width, height) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tbadge.fill(Qt::transparent);\n\tbadge.setDevicePixelRatio(ratio);\n\n\tauto p = QPainter(&badge);\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto bg = Ui::ColorFromSerialized(StarsColoringForCount(\n\t\tfrom->session().appConfig().groupCallColorings(),\n\t\tstars).bgLight);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(bg);\n\tp.drawRoundedRect(0, 0, width, height, height / 2., height / 2.);\n\tfrom->paintUserpic(p, userpic, PaintUserpicContext{\n\t\t.position = QPoint(userpicPadding.left(), userpicPadding.top()),\n\t\t.size = userpicSize,\n\t\t.shape = Ui::PeerUserpicShape::Circle,\n\t});\n\tp.setPen(st::white);\n\tprice.draw(p, {\n\t\t.position = QPoint(priceLeft, priceTop),\n\t\t.availableWidth = price.maxWidth(),\n\t});\n\tshift = base::RandomIndex(360) / 360.;\n}\n\nComposeControls::ComposeControls(\n\tnot_null<Ui::RpWidget*> parent,\n\tComposeControlsDescriptor descriptor)\n: _st(descriptor.stOverride\n\t? *descriptor.stOverride\n\t: st::defaultComposeControls)\n, _features(descriptor.features)\n, _parent(parent)\n, _panelsParent(descriptor.panelsParent\n\t? descriptor.panelsParent\n\t: _parent.get())\n, _show(std::move(descriptor.show))\n, _session(&_show->session())\n, _regularWindow(descriptor.regularWindow)\n, _ownedSelector((_regularWindow && _features.commonTabbedPanel)\n\t? nullptr\n\t: std::make_unique<ChatHelpers::TabbedSelector>(\n\t\t_panelsParent,\n\t\tChatHelpers::TabbedSelectorDescriptor{\n\t\t\t.show = _show,\n\t\t\t.st = _st.tabbed,\n\t\t\t.level = descriptor.panelsLevel,\n\t\t\t.mode = (_features.emojiOnlyPanel\n\t\t\t\t? ChatHelpers::TabbedSelector::Mode::EmojiOnly\n\t\t\t\t: ChatHelpers::TabbedSelector::Mode::Full),\n\t\t\t.features = _features,\n\t\t}))\n, _selector((_regularWindow && _features.commonTabbedPanel)\n\t? _regularWindow->tabbedSelector()\n\t: not_null(_ownedSelector.get()))\n, _mode(descriptor.mode)\n, _wrap(std::make_unique<Ui::RpWidget>(_parent))\n, _send(std::make_shared<Ui::SendButton>(_wrap.get(), _st.send))\n, _aiButton(Ui::CreateChild<Controls::ComposeAiButton>(\n\t_wrap.get(),\n\tst::historyAiComposeButton))\n, _sendAsFile(_features.attachments\n\t? Ui::CreateChild<Ui::IconButton>(\n\t\t_wrap.get(),\n\t\tst::historySendAsFileButton)\n\t: nullptr)\n, _like(_features.likes\n\t? Ui::CreateChild<Ui::IconButton>(_wrap.get(), _st.like)\n\t: nullptr)\n, _chosenStarsCount(_features.editMessageStars ? 0 : std::optional<int>())\n, _attachToggle(_features.attachments\n\t? Ui::CreateChild<Ui::IconButton>(_wrap.get(), _st.attach)\n\t: nullptr)\n, _tabbedSelectorToggle(Ui::CreateChild<Ui::EmojiButton>(\n\t_wrap.get(),\n\t_st.emoji))\n, _fieldCustomPlaceholder(std::move(descriptor.customPlaceholder))\n, _field(\n\tUi::CreateChild<Ui::InputField>(\n\t\t_wrap.get(),\n\t\t_st.field,\n\t\tUi::InputField::Mode::MultiLine,\n\t\t(_fieldCustomPlaceholder\n\t\t\t? rpl::duplicate(_fieldCustomPlaceholder)\n\t\t\t: tr::lng_message_ph())))\n, _botCommandStart(_features.botCommandSend\n\t? Ui::CreateChild<Ui::IconButton>(\n\t\t_wrap.get(),\n\t\tst::historyBotCommandStart)\n\t: nullptr)\n, _header(std::make_unique<FieldHeader>(\n\t_wrap.get(),\n\t_show,\n\t[=] { return HasSendText(_field); }))\n, _voiceRecordBar(std::make_unique<VoiceRecordBar>(\n\t_wrap.get(),\n\tControls::VoiceRecordBarDescriptor{\n\t\t.outerContainer = _parent,\n\t\t.show = _show,\n\t\t.send = _send,\n\t\t.customCancelText = descriptor.voiceCustomCancelText,\n\t\t.stOverride = &_st.record,\n\t\t.recorderHeight = st::historySendSize.height(),\n\t\t.lockFromBottom = descriptor.voiceLockFromBottom,\n\t}))\n, _sendMenuDetails(descriptor.sendMenuDetails)\n, _unavailableEmojiPasted(std::move(descriptor.unavailableEmojiPasted))\n, _saveDraftTimer([=] { saveDraft(); })\n, _saveCloudDraftTimer([=] { saveCloudDraft(); }) {\n\tif (_st.radius > 0) {\n\t\t_backgroundRect.emplace(_st.radius, _st.bg);\n\t}\n\tif (descriptor.stickerOrEmojiChosen) {\n\t\tstd::move(\n\t\t\tdescriptor.stickerOrEmojiChosen\n\t\t) | rpl::start_to_stream(_stickerOrEmojiChosen, _wrap->lifetime());\n\t}\n\tif (descriptor.scheduledToggleValue) {\n\t\tstd::move(\n\t\t\tdescriptor.scheduledToggleValue\n\t\t) | rpl::on_next([=](bool hasScheduled) {\n\t\t\tif (!_scheduled && hasScheduled) {\n\t\t\t\t_scheduled = base::make_unique_q<Ui::IconButton>(\n\t\t\t\t\t_wrap.get(),\n\t\t\t\t\tst::historyScheduledToggle);\n\t\t\t\t_scheduled->show();\n\t\t\t\t_scheduled->clicks(\n\t\t\t\t) | rpl::filter(\n\t\t\t\t\trpl::mappers::_1 == Qt::LeftButton\n\t\t\t\t) | rpl::to_empty | rpl::start_to_stream(\n\t\t\t\t\t_showScheduledRequests,\n\t\t\t\t\t_scheduled->lifetime());\n\t\t\t\torderControls(); // Raise drag areas to the top.\n\t\t\t\tupdateControlsVisibility();\n\t\t\t\tupdateControlsGeometry(_wrap->size());\n\t\t\t} else if (_scheduled && !hasScheduled) {\n\t\t\t\t_scheduled = nullptr;\n\t\t\t}\n\t\t}, _wrap->lifetime());\n\t}\n\tinit();\n}\n\nrpl::producer<> ComposeControls::showScheduledRequests() const {\n\treturn _showScheduledRequests.events();\n}\n\nComposeControls::~ComposeControls() {\n\tsaveFieldToHistoryLocalDraft();\n\tunregisterDraftSources();\n\tsetTabbedPanel(nullptr);\n\tsession().api().request(_inlineBotResolveRequestId).cancel();\n}\n\nMain::Session &ComposeControls::session() const {\n\treturn _show->session();\n}\n\nvoid ComposeControls::updateTopicRootId(MsgId topicRootId) {\n\t_topicRootId = topicRootId;\n\t_header->updateTopicRootId(_topicRootId);\n}\n\nvoid ComposeControls::updateShortcutId(BusinessShortcutId shortcutId) {\n\tunregisterDraftSources();\n\t_shortcutId = shortcutId;\n\tregisterDraftSource();\n}\n\nvoid ComposeControls::setHistory(SetHistoryArgs &&args) {\n\t_showSlowmodeError = std::move(args.showSlowmodeError);\n\t_sendActionFactory = std::move(args.sendActionFactory);\n\t_sendWithText = std::move(args.sendWithText);\n\t_slowmodeSecondsLeft = rpl::single(0)\n\t\t| rpl::then(std::move(args.slowmodeSecondsLeft));\n\t_sendDisabledBySlowmode = rpl::single(false)\n\t\t| rpl::then(std::move(args.sendDisabledBySlowmode));\n\t_liked = args.liked ? std::move(args.liked) : rpl::single(false);\n\t_writeRestriction = rpl::single(Controls::WriteRestriction())\n\t\t| rpl::then(std::move(args.writeRestriction));\n\t_minStarsCount = args.minStarsCount\n\t\t? std::move(args.minStarsCount)\n\t\t: rpl::single(0);\n\tconst auto history = *args.history;\n\tif (_history == history) {\n\t\treturn;\n\t}\n\tunregisterDraftSources();\n\t_history = history;\n\t_topicRootId = args.topicRootId;\n\t_monoforumPeerId = args.monoforumPeerId;\n\t_historyLifetime.destroy();\n\t_header->setHistory(args);\n\tregisterDraftSource();\n\t_selector->setCurrentPeer(history ? history->peer.get() : nullptr);\n\tinitFieldAutocomplete();\n\tinitWebpageProcess();\n\tinitWriteRestriction();\n\tinitForwardProcess();\n\tupdateBotCommandShown();\n\tupdateLikeShown();\n\tupdateMessagesTTLShown();\n\tupdateControlsGeometry(_wrap->size());\n\tupdateControlsVisibility();\n\tupdateFieldPlaceholder();\n\tupdateAttachBotsMenu();\n\n\t_sendAs = nullptr;\n\t_silent = nullptr;\n\tif (!_history) {\n\t\treturn;\n\t}\n\tconst auto peer = _history->peer;\n\tinitSendAsButton(peer, args.videoStream);\n\tif (peer->isChat() && peer->asChat()->noParticipantInfo()) {\n\t\tsession().api().requestFullPeer(peer);\n\t} else if (const auto channel = peer->asMegagroup()) {\n\t\tif (channel->mgInfo->botStatus == Data::BotStatus::Unknown) {\n\t\t\tsession().api().chatParticipants().requestBots(channel);\n\t\t}\n\t} else if (hasSilentBroadcastToggle()) {\n\t\t_silent = std::make_unique<Ui::SilentToggle>(\n\t\t\t_wrap.get(),\n\t\t\tpeer->asChannel());\n\t}\n\tsession().local().readDraftsWithCursors(_history);\n\tapplyDraft();\n\torderControls();\n\t_field->setMode(args.videoStream\n\t\t? Ui::InputField::Mode::NoNewlines\n\t\t: Ui::InputField::Mode::MultiLine);\n}\n\nvoid ComposeControls::initLikeButton() {\n\tif (_like) {\n\t\t_like->setClickedCallback([=] { _likeToggled.fire({}); });\n\t\t_liked.value(\n\t\t) | rpl::on_next([=](bool liked) {\n\t\t\tconst auto icon = liked ? &_st.liked : nullptr;\n\t\t\t_like->setIconOverride(icon, icon);\n\t\t}, _like->lifetime());\n\t}\n}\n\nvoid ComposeControls::initEditStarsButton() {\n\tif (!editStarsButtonShown()) {\n\t\tdelete base::take(_editStars);\n\t\tif (_chosenStarsCount) {\n\t\t\t_chosenStarsCount = std::nullopt;\n\t\t\tupdateSendButtonType();\n\t\t}\n\t\treturn;\n\t}\n\tif (_chosenStarsCount.value_or(0) < _minStarsCount.current()) {\n\t\t_chosenStarsCount = _minStarsCount.current();\n\t\tupdateSendButtonType();\n\t}\n\tif (_editStars) {\n\t\treturn;\n\t}\n\t_editStars = Ui::CreateChild<Ui::IconButton>(\n\t\t_wrap.get(),\n\t\t_st.editStars);\n\t_editStars->show();\n\t_editStars->setClickedCallback([=] {\n\t\teditStarsFrom();\n\t});\n}\n\nvoid ComposeControls::editStarsFrom(int selected) {\n\tconst auto min = _minStarsCount.current();\n\tif (!selected) {\n\t\tselected = _chosenStarsCount.value_or(0);\n\t}\n\t_show->show(Calls::Group::MakeVideoStreamStarsBox({\n\t\t.show = _show,\n\t\t.min = min,\n\t\t.current = std::max(selected, min),\n\t\t.save = crl::guard(_editStars, [=](int count) {\n\t\t\t_chosenStarsCount = count;\n\t\t\tupdateSendButtonType();\n\t\t}),\n\t\t.name = _history ? _history->peer->shortName() : QString(),\n\t}));\n}\n\nvoid ComposeControls::updateControlsParents() {\n\tconst auto toggle = [&](auto &&control, bool inRestriction) {\n\t\tif (!control) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto hidden = control->isHidden();\n\t\tif (_writeRestricted && inRestriction) {\n\t\t\tcontrol->setParent(_writeRestricted.get());\n\t\t} else {\n\t\t\tcontrol->setParent(_wrap.get());\n\t\t}\n\t\tif (!hidden) {\n\t\t\tcontrol->show();\n\t\t\tupdateControlsGeometry(_wrap->size());\n\t\t}\n\t};\n\tusing Type = Controls::WriteRestrictionType;\n\tconst auto &restriction = _writeRestriction.current();\n\ttoggle(_like, restriction.type == Type::PremiumRequired);\n\ttoggle(_commentsShown, restriction.type != Type::None);\n\ttoggle(_starsReaction, restriction.type != Type::None);\n}\n\nvoid ComposeControls::updateFeatures(ChatHelpers::ComposeFeatures features) {\n\tauto changed = false;\n\tconst auto was = std::exchange(_features, features);\n\tif (was.likes != features.likes) {\n\t\tif (!features.likes) {\n\t\t\tdelete base::take(_like);\n\t\t\t_likeShown = false;\n\t\t} else {\n\t\t\t_like = Ui::CreateChild<Ui::IconButton>(_wrap.get(), _st.like);\n\t\t\tinitLikeButton();\n\t\t\tupdateControlsParents();\n\t\t\tif (updateLikeShown()) {\n\t\t\t\tupdateControlsVisibility();\n\t\t\t}\n\t\t}\n\t\tchanged = true;\n\t}\n\tif (was.editMessageStars != features.editMessageStars) {\n\t\tinitEditStarsButton();\n\t\tchanged = true;\n\t}\n\tif (was.recordMediaMessage != features.recordMediaMessage) {\n\t\tclearChosenStarsForMessage();\n\t}\n\tif (was.attachments != features.attachments) {\n\t\tif (!features.attachments) {\n\t\t\tdelete base::take(_attachToggle);\n\t\t} else {\n\t\t\t_attachToggle = Ui::CreateChild<Ui::IconButton>(\n\t\t\t\t_wrap.get(),\n\t\t\t\t_st.attach);\n\t\t\tupdateControlsVisibility();\n\t\t}\n\t\tupdateAttachBotsMenu();\n\t\tchanged = true;\n\t}\n\tif (was.emojiOnlyPanel != features.emojiOnlyPanel) {\n\t\tinitFieldAutocomplete();\n\t}\n\tif (changed) {\n\t\tupdateControlsGeometry(_wrap->size());\n\t}\n}\n\nvoid ComposeControls::setCurrentDialogsEntryState(\n\t\tDialogs::EntryState state) {\n\tunregisterDraftSources();\n\tstate.currentReplyTo.topicRootId = _topicRootId;\n\tstate.currentReplyTo.monoforumPeerId = _monoforumPeerId;\n\tstate.currentSuggest = SuggestOptions();\n\t_currentDialogsEntryState = state;\n\tupdateForwarding();\n\tregisterDraftSource();\n}\n\nPeerData *ComposeControls::sendAsPeer() const {\n\treturn (_sendAs && _history)\n\t\t? session().sendAsPeers().resolveChosen(_history->peer).get()\n\t\t: nullptr;\n}\n\nvoid ComposeControls::move(int x, int y) {\n\t_wrap->move(x, y);\n\tif (_writeRestricted) {\n\t\t_writeRestricted->move(x, y);\n\t}\n}\n\nvoid ComposeControls::resizeToWidth(int width) {\n\t_wrap->resizeToWidth(width);\n\tif (_writeRestricted) {\n\t\t_writeRestricted->resizeToWidth(width);\n\t}\n\tupdateHeight();\n}\n\nvoid ComposeControls::setAutocompleteBoundingRect(QRect rect) {\n\tif (_autocomplete) {\n\t\t_autocomplete->setBoundings(rect);\n\t}\n}\n\nrpl::producer<int> ComposeControls::height() const {\n\tusing namespace rpl::mappers;\n\treturn rpl::conditional(\n\t\trpl::combine(\n\t\t\t_writeRestriction.value(),\n\t\t\t_hidden.value()) | rpl::map(!_1 && !_2),\n\t\t_wrap->heightValue(),\n\t\trpl::single(_st.attach.height));\n}\n\nint ComposeControls::heightCurrent() const {\n\treturn (_writeRestriction.current() || _hidden.current())\n\t\t? _st.attach.height\n\t\t: _wrap->height();\n}\n\nconst HistoryItemsList &ComposeControls::forwardItems() const {\n\treturn _header->forwardItems();\n}\n\nvoid ComposeControls::setupCommentsShownNewDot() {\n\tif (_commentsShownNewDot) {\n\t\treturn;\n\t}\n\t_commentsShownNewDot = Ui::CreateChild<Ui::RpWidget>(\n\t\t_commentsShown);\n\t_commentsShownNewDot->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto size = _st.commentsUnreadSize;\n\tconst auto add = _st.commentsUnreadMargin;\n\tconst auto full = size + 2 * add;\n\t_commentsShownNewDot->setGeometry(\n\t\tQRect(_st.commentsUnreadPosition, QSize(full, full)));\n\t_commentsShownNewDot->show();\n\t_commentsShownNewDot->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(_commentsShownNewDot);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::groupCallMembersBg);\n\t\tp.drawEllipse(_commentsShownNewDot->rect());\n\t\tp.setBrush(st::attentionButtonFg);\n\t\tp.drawEllipse(add, add, size, size);\n\t}, _commentsShownNewDot->lifetime());\n}\n\nvoid ComposeControls::setToggleCommentsButton(\n\t\trpl::producer<ToggleCommentsState> state) {\n\tif (!state) {\n\t\tdelete base::take(_commentsShown);\n\t} else {\n\t\t_commentsShown = Ui::CreateChild<Ui::IconButton>(\n\t\t\t_wrap.get(),\n\t\t\t_st.commentsShow);\n\t\t_commentsShown->setClickedCallback([=] {\n\t\t\t_commentsShownToggles.fire({});\n\t\t});\n\t\tupdateControlsParents();\n\t\t_commentsShownHidden.value(\n\t\t) | rpl::on_next([=](bool hidden) {\n\t\t\tif (_commentsShown->isHidden() != hidden) {\n\t\t\t\tif (hidden) {\n\t\t\t\t\t_commentsShown->hide();\n\t\t\t\t} else {\n\t\t\t\t\t_commentsShown->show();\n\t\t\t\t\tupdateControlsGeometry(_wrap->size());\n\t\t\t\t}\n\t\t\t}\n\t\t}, _commentsShown->lifetime());\n\t\tstd::move(\n\t\t\tstate\n\t\t) | rpl::on_next([=](ToggleCommentsState value) {\n\t\t\tif (value == ToggleCommentsState::Empty) {\n\t\t\t\t_commentsShownHidden = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_commentsShownHidden = false;\n\t\t\tconst auto icon = (value == ToggleCommentsState::Shown)\n\t\t\t\t? &_st.commentsShown\n\t\t\t\t: nullptr;\n\t\t\t_commentsShown->setIconOverride(icon, icon);\n\t\t\tif (value == ToggleCommentsState::WithNew) {\n\t\t\t\tsetupCommentsShownNewDot();\n\t\t\t} else {\n\t\t\t\tdelete base::take(_commentsShownNewDot);\n\t\t\t}\n\t\t}, _commentsShown->lifetime());\n\t}\n\tupdateControlsVisibility();\n\tupdateControlsGeometry(_wrap->size());\n}\n\nrpl::producer<> ComposeControls::commentsShownToggles() const {\n\treturn _commentsShownToggles.events();\n}\n\nvoid ComposeControls::setStarsReactionCounter(\n\t\trpl::producer<Ui::SendStarButtonState> count,\n\t\trpl::producer<SendStarButtonEffect> effects) {\n\tif (!count) {\n\t\tdelete base::take(_starsReaction);\n\t\tupdateControlsGeometry(_wrap->size());\n\t} else {\n\t\t_starsReaction = Ui::CreateChild<Ui::SendStarButton>(\n\t\t\t_wrap.get(),\n\t\t\t_st.attach,\n\t\t\t_st.starsReactionCounter,\n\t\t\tstd::move(count));\n\t\tupdateControlsParents();\n\t\tupdateControlsVisibility();\n\n\t\t_starsReaction->widthValue(\n\t\t) | rpl::on_next([=](int width) {\n\t\t\tupdateControlsGeometry(_wrap->size());\n\t\t}, _starsReaction->lifetime());\n\n\t\t_starsReaction->setAcceptBoth();\n\t\t_starsReaction->clicks(\n\t\t) | rpl::on_next([=](Qt::MouseButton button) {\n\t\t\tif (_chosenStarsCount && button == Qt::LeftButton) {\n\t\t\t\t_starsReactionIncrements.fire({ .count = 1 });\n\t\t\t\tstartStarsSendEffect();\n\t\t\t} else {\n\t\t\t\t_show->show(Calls::Group::MakeVideoStreamStarsBox({\n\t\t\t\t\t.show = _show,\n\t\t\t\t\t.top = _starsReactionTop.current(),\n\t\t\t\t\t.current = 0,\n\t\t\t\t\t.sending = true,\n\t\t\t\t\t.admin = !_chosenStarsCount,\n\t\t\t\t\t.save = crl::guard(_starsReaction, [=](int count) {\n\t\t\t\t\t\t_starsReactionIncrements.fire({\n\t\t\t\t\t\t\t.count = count,\n\t\t\t\t\t\t\t.fromBox = true,\n\t\t\t\t\t\t});\n\t\t\t\t\t}),\n\t\t\t\t\t.name = _history ? _history->peer->shortName() : QString(),\n\t\t\t\t}));\n\t\t\t}\n\t\t}, _starsReaction->lifetime());\n\n\t\tstd::move(\n\t\t\teffects\n\t\t) | rpl::on_next([=](const SendStarButtonEffect &event) {\n\t\t\tstartStarsEffect(event);\n\t\t}, _starsReaction->lifetime());\n\t}\n}\n\nvoid ComposeControls::startStarsSendEffect() {\n\tif (!_starSendEffectsCanvas) {\n\t\tsetupStarsSendEffectsCanvas();\n\t}\n\twhile (_starSendEffects.size() >= kMaxStarSendEffects) {\n\t\t_starSendEffects.erase(begin(_starSendEffects));\n\t}\n\t_starSendEffects.push_back(std::make_unique<Ui::ReactionFlyAnimation>(\n\t\t&_show->session().data().reactions(),\n\t\tUi::ReactionFlyAnimationArgs{\n\t\t\t.id = Data::ReactionId::Paid(),\n\t\t\t.effectOnly = true,\n\t\t},\n\t\t[raw = _starSendEffectsCanvas.get()] { raw->update(); },\n\t\tst::reactionInlineImage));\n}\n\nvoid ComposeControls::setupStarsSendEffectsCanvas() {\n\t_starSendEffectsCanvas = std::make_unique<Ui::RpWidget>(_parent);\n\n\tconst auto raw = _starSendEffectsCanvas.get();\n\traw->show();\n\traw->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto effectSize = st::reactionInlineImage * 2;\n\n\trpl::combine(\n\t\t_wrap->geometryValue(),\n\t\t_writeRestricted->geometryValue(),\n\t\t_starsReaction->geometryValue()\n\t) | rpl::on_next([=](QRect wrap, QRect restriction, QRect star) {\n\t\tconst auto parent = (_starsReaction->parentWidget() == _wrap.get())\n\t\t\t? wrap\n\t\t\t: restriction;\n\t\tconst auto adjusted = star.translated(parent.topLeft());\n\n\t\traw->setGeometry(\n\t\t\tadjusted.x() + (adjusted.width() - effectSize) / 2,\n\t\t\tadjusted.y() + (adjusted.height() - effectSize) / 2,\n\t\t\teffectSize,\n\t\t\teffectSize);\n\t}, raw->lifetime());\n\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tfor (auto i = begin(_starSendEffects); i != end(_starSendEffects);) {\n\t\t\tif ((*i)->finished()) {\n\t\t\t\ti = _starSendEffects.erase(i);\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t\tif (_starSendEffects.empty()) {\n\t\t\tcrl::on_main(raw, [=] {\n\t\t\t\tif (_starSendEffectsCanvas.get() == raw\n\t\t\t\t\t&& _starSendEffects.empty()) {\n\t\t\t\t\t_starSendEffectsCanvas = nullptr;\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tauto p = QPainter(raw);\n\t\tconst auto now = crl::now();\n\t\tconst auto size = raw->width();\n\t\tconst auto color = st::radialFg->c;\n\t\tconst auto skip = (size - st::reactionInlineImage) / 2;\n\t\tconst auto target = QRect(\n\t\t\tQPoint(skip, skip),\n\t\t\tQSize(st::reactionInlineImage, st::reactionInlineImage));\n\t\tfor (const auto &animation : _starSendEffects) {\n\t\t\tanimation->paintGetArea(p, {}, target, color, {}, now);\n\t\t}\n\t}, raw->lifetime());\n}\n\nvoid ComposeControls::startStarsEffect(SendStarButtonEffect event) {\n\tif (!_starEffectsCanvas) {\n\t\tsetupStarsEffectsCanvas();\n\t}\n\twhile (_starEffects.size() >= kMaxStarEffects) {\n\t\t_starEffects.erase(begin(_starEffects));\n\t}\n\t_starEffects.push_back(std::make_unique<StarEffect>(\n\t\t_starEffectsCanvas.get(),\n\t\tevent));\n}\n\nvoid ComposeControls::setupStarsEffectsCanvas() {\n\t_starEffectsCanvas = std::make_unique<Ui::RpWidget>(_parent);\n\n\tconst auto raw = _starEffectsCanvas.get();\n\traw->show();\n\traw->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\traw->lifetime().make_state<Ui::Animations::Basic>([=] {\n\t\traw->update();\n\t})->start();\n\n\tconst auto effectSize = st::reactionInlineImage * 2;\n\tconst auto width = effectSize * 2;\n\tconst auto height = effectSize * 4;\n\n\trpl::combine(\n\t\t_wrap->geometryValue(),\n\t\t_writeRestricted->geometryValue(),\n\t\t_starsReaction->geometryValue()\n\t) | rpl::on_next([=](QRect wrap, QRect restriction, QRect star) {\n\t\tconst auto parent = (_starsReaction->parentWidget() == _wrap.get())\n\t\t\t? wrap\n\t\t\t: restriction;\n\t\tconst auto adjusted = star.translated(parent.topLeft());\n\n\t\traw->setGeometry(\n\t\t\tadjusted.x() + (adjusted.width() - width) / 2,\n\t\t\tadjusted.y() + adjusted.height() - height,\n\t\t\twidth,\n\t\t\theight);\n\t}, raw->lifetime());\n\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tconst auto now = crl::now();\n\t\tfor (auto i = begin(_starEffects); i != end(_starEffects);) {\n\t\t\tconst auto progress = float64(now - (*i)->start)\n\t\t\t\t/ kStarEffectDuration;\n\t\t\tif (progress >= 1.) {\n\t\t\t\ti = _starEffects.erase(i);\n\t\t\t} else {\n\t\t\t\t(*i)->progress = progress;\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t\tif (_starEffects.empty()) {\n\t\t\tcrl::on_main(raw, [=] {\n\t\t\t\tif (_starEffectsCanvas.get() == raw\n\t\t\t\t\t&& _starEffects.empty()) {\n\t\t\t\t\t_starEffectsCanvas = nullptr;\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tauto p = QPainter(raw);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto color = st::radialFg->c;\n\t\tconst auto skip = (effectSize - st::reactionInlineImage) / 2;\n\t\tfor (const auto &animation : _starEffects) {\n\t\t\tconst auto progress = animation->progress;\n\t\t\tconst auto left = anim::interpolate(\n\t\t\t\t0,\n\t\t\t\twidth - effectSize,\n\t\t\t\tanimation->shift);\n\t\t\tconst auto top = anim::interpolate(\n\t\t\t\theight - effectSize,\n\t\t\t\t0,\n\t\t\t\tprogress);\n\t\t\tconst auto opacity = (progress < 0.125) ?\n\t\t\t\t(progress / 0.125) :\n\t\t\t\t(progress > 0.875) ?\n\t\t\t\t(1. - progress) / 0.125\n\t\t\t\t: 1.;\n\t\t\tconst auto scale = kStarEffectScaleMin\n\t\t\t\t+ (kStarEffectScaleMax - kStarEffectScaleMin) * opacity;\n\n\t\t\tconst auto rotation = qSin(-M_PI_2\n\t\t\t\t+ M_PI * (animation->shift + animation->progress)\n\t\t\t) * kStarEffectRotationMax;\n\t\t\tconst auto target = QRect(\n\t\t\t\tQPoint(left + skip, top + skip),\n\t\t\t\tQSize(st::reactionInlineImage, st::reactionInlineImage));\n\t\t\tconst auto size = animation->badge.size()\n\t\t\t\t/ animation->badge.devicePixelRatio();\n\t\t\tconst auto dx = (target.width() - size.width()) / 2;\n\t\t\tconst auto dy = (target.height() - size.height()) / 2;\n\n\t\t\tp.save();\n\t\t\tp.translate(target.center());\n\t\t\tp.rotate(rotation);\n\t\t\tp.scale(scale, scale);\n\t\t\tp.translate(-target.center());\n\t\t\tp.setOpacity(opacity);\n\t\t\tp.drawImage(target.topLeft() + QPoint(dx, dy), animation->badge);\n\t\t\tp.restore();\n\n\t\t\tanimation->around.paintGetArea(p, {}, target, color, {}, now);\n\t\t}\n\t}, raw->lifetime());\n}\n\nvoid ComposeControls::setStarsReactionTop(\n\t\trpl::producer<std::vector<StarReactionTop>> top) {\n\t_starsReactionTop = std::move(top);\n}\n\nauto ComposeControls::starsReactionIncrements() const\n-> rpl::producer<StarReactionIncrement> {\n\treturn _starsReactionIncrements.events();\n}\n\nbool ComposeControls::focus() {\n\tif (_wrap->isHidden() || _field->isHidden()) {\n\t\treturn false;\n\t}\n\tif (isRecording()) {\n\t\t_wrap->setFocus();\n\t\treturn true;\n\t}\n\t_field->setFocus();\n\treturn true;\n}\n\nbool ComposeControls::focused() const {\n\treturn Ui::InFocusChain(_wrap.get());\n}\n\nrpl::producer<bool> ComposeControls::focusedValue() const {\n\treturn rpl::single(focused()) | rpl::then(_field->focusedChanges());\n}\n\nrpl::producer<bool> ComposeControls::tabbedPanelShownValue() const {\n\treturn _tabbedPanel ? _tabbedPanel->shownValue() : rpl::single(false);\n}\n\nrpl::producer<> ComposeControls::cancelRequests() const {\n\treturn _cancelRequests.events();\n}\n\nauto ComposeControls::scrollKeyEvents() const\n-> rpl::producer<not_null<QKeyEvent*>> {\n\treturn _scrollKeyEvents.events();\n}\n\nauto ComposeControls::editLastMessageRequests() const\n-> rpl::producer<not_null<QKeyEvent*>> {\n\treturn _editLastMessageRequests.events();\n}\n\nauto ComposeControls::replyNextRequests() const\n-> rpl::producer<ReplyNextRequest> {\n\treturn _replyNextRequests.events();\n}\n\nrpl::producer<> ComposeControls::focusRequests() const {\n\treturn _focusRequests.events();\n}\n\nauto ComposeControls::sendContentRequests(SendRequestType requestType) const {\n\tauto filter = rpl::filter([=] {\n\t\tconst auto type = (_mode == Mode::Normal)\n\t\t\t? Ui::SendButton::Type::Send\n\t\t\t: Ui::SendButton::Type::Schedule;\n\t\tconst auto sendRequestType = _voiceRecordBar->isListenState()\n\t\t\t? SendRequestType::Voice\n\t\t\t: SendRequestType::Text;\n\t\treturn (_send->type() == type) && (sendRequestType == requestType);\n\t});\n\tauto map = rpl::map_to(Api::SendOptions());\n\treturn rpl::merge(\n\t\t_send->clicks() | filter | map,\n\t\t_field->submits() | filter | map,\n\t\t_sendCustomRequests.events());\n}\n\nrpl::producer<> ComposeControls::scrollToMaxRequests() const {\n\treturn _field->submits() | rpl::filter([=]{\n\t\tif (_mode == Mode::Normal\n\t\t\t&& !_voiceRecordBar->isListenState()\n\t\t\t&& getTextWithAppliedMarkdown().text.isEmpty()) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}) | rpl::to_empty;\n}\n\nrpl::producer<Api::SendOptions> ComposeControls::sendRequests() const {\n\treturn sendContentRequests(\n\t\tSendRequestType::Text\n\t) | rpl::filter([=] {\n\t\tif (!_chosenStarsCount) {\n\t\t\treturn true;\n\t\t}\n\t\tusing namespace Calls::Group::Ui;\n\t\tconst auto count = *_chosenStarsCount;\n\t\tconst auto &appConfig = _show->session().appConfig();\n\t\tconst auto &colorings = appConfig.groupCallColorings();\n\t\tconst auto required = StarsRequiredForMessage(\n\t\t\tcolorings,\n\t\t\tgetTextWithAppliedMarkdown());\n\t\tif (required <= count) {\n\t\t\treturn true;\n\t\t}\n\t\tconst_cast<ComposeControls*>(this)->editStarsFrom(required);\n\t\treturn false;\n\t});\n}\n\nrpl::producer<VoiceToSend> ComposeControls::sendVoiceRequests() const {\n\treturn _voiceRecordBar->sendVoiceRequests();\n}\n\nrpl::producer<QString> ComposeControls::sendCommandRequests() const {\n\treturn _sendCommandRequests.events();\n}\n\nrpl::producer<MessageToEdit> ComposeControls::editRequests() const {\n\tauto toValue = rpl::map([=] { return _header->queryToEdit(); });\n\tauto filter = rpl::filter([=] {\n\t\treturn _send->type() == Ui::SendButton::Type::Save;\n\t});\n\treturn rpl::merge(\n\t\t_send->clicks() | filter | toValue,\n\t\t_field->submits() | filter | toValue);\n}\n\nrpl::producer<std::optional<bool>> ComposeControls::attachRequests() const {\n\treturn rpl::merge(\n\t\t_attachToggle->clicks() | rpl::map_to(std::optional<bool>()),\n\t\t_attachRequests.events()\n\t) | rpl::filter([=] {\n\t\tif (isEditingMessage()) {\n\t\t\t_show->showBox(\n\t\t\t\tUi::MakeInformBox(tr::lng_edit_caption_attach()));\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n}\n\nvoid ComposeControls::setMimeDataHook(MimeDataHook hook) {\n\tif (_sendAsFile) {\n\t\t_field->setMimeDataHook(\n\t\t\tWrappedMessageFieldMimeHook(\n\t\t\t\t[=, originalHook = std::move(hook)](\n\t\t\t\t\t\tnot_null<const QMimeData*> data,\n\t\t\t\t\t\tUi::InputField::MimeAction action) {\n\t\t\t\t\tif (checkLargeTextPaste(data, action)) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\treturn originalHook\n\t\t\t\t\t\t? originalHook(data, action)\n\t\t\t\t\t\t: false;\n\t\t\t\t}, _field));\n\t} else {\n\t\t_field->setMimeDataHook(\n\t\t\tWrappedMessageFieldMimeHook(std::move(hook), _field));\n\t}\n}\n\nbool ComposeControls::confirmMediaEdit(Ui::PreparedList &list) {\n\tif (!isEditingMessage() || !_regularWindow) {\n\t\treturn false;\n\t} else if (_canReplaceMedia || _canAddMedia) {\n\t\tconst auto queryToEdit = _header->queryToEdit();\n\t\tEditCaptionBox::StartMediaReplace(\n\t\t\t_regularWindow,\n\t\t\t_editingId,\n\t\t\tstd::move(list),\n\t\t\t_field->getTextWithTags(),\n\t\t\t_header->suggestOptions(),\n\t\t\tqueryToEdit.spoilered,\n\t\t\tqueryToEdit.options.invertCaption,\n\t\t\tcrl::guard(_wrap.get(), [=] { cancelEditMessage(); }));\n\t} else {\n\t\t_show->showToast(tr::lng_edit_caption_attach(tr::now));\n\t}\n\treturn true;\n}\n\nrpl::producer<FileChosen> ComposeControls::fileChosen() const {\n\treturn _fileChosen.events();\n}\n\nrpl::producer<PhotoChosen> ComposeControls::photoChosen() const {\n\treturn _photoChosen.events();\n}\n\nauto ComposeControls::inlineResultChosen() const\n-> rpl::producer<InlineChosen> {\n\treturn _inlineResultChosen.events();\n}\n\nvoid ComposeControls::showStarted() {\n\tif (_inlineResults) {\n\t\t_inlineResults->hideFast();\n\t}\n\tif (_tabbedPanel) {\n\t\t_tabbedPanel->hideFast();\n\t}\n\tif (_attachBotsMenu) {\n\t\t_attachBotsMenu->hideFast();\n\t}\n\t_voiceRecordBar->hideFast();\n\tif (_autocomplete) {\n\t\t_autocomplete->hideFast();\n\t}\n\t_wrap->hide();\n\tif (_writeRestricted) {\n\t\t_writeRestricted->hide();\n\t}\n}\n\nvoid ComposeControls::showFinished() {\n\tif (_inlineResults) {\n\t\t_inlineResults->hideFast();\n\t}\n\tif (_tabbedPanel) {\n\t\t_tabbedPanel->hideFast();\n\t}\n\tif (_attachBotsMenu) {\n\t\t_attachBotsMenu->hideFast();\n\t}\n\t_voiceRecordBar->hideFast();\n\tif (_autocomplete) {\n\t\t_autocomplete->hideFast();\n\t}\n\tupdateWrappingVisibility();\n\t_aiButton->raise();\n\tif (_sendAsFile) {\n\t\t_sendAsFile->raise();\n\t}\n\tif (_aiTooltipManager) {\n\t\t_aiTooltipManager->raise();\n\t}\n\tif (_sendAsFileTooltipManager) {\n\t\t_sendAsFileTooltipManager->raise();\n\t}\n\t_voiceRecordBar->orderControls();\n}\n\nvoid ComposeControls::raisePanels() {\n\tif (_autocomplete) {\n\t\t_autocomplete->raise();\n\t}\n\tif (_inlineResults) {\n\t\t_inlineResults->raise();\n\t}\n\tif (_tabbedPanel) {\n\t\t_tabbedPanel->raise();\n\t}\n\tif (_attachBotsMenu) {\n\t\t_attachBotsMenu->raise();\n\t}\n\tif (_emojiSuggestions) {\n\t\t_emojiSuggestions->raise();\n\t}\n}\n\nvoid ComposeControls::showForGrab() {\n\tshowFinished();\n}\n\nTextWithTags ComposeControls::getTextWithAppliedMarkdown() const {\n\treturn _field->getTextWithAppliedMarkdown();\n}\n\nvoid ComposeControls::clear() {\n\t// Otherwise cancelReplyMessage() will save the draft.\n\tconst auto saveTextDraft = !replyingToMessage();\n\tsetFieldText(\n\t\t{},\n\t\tsaveTextDraft ? TextUpdateEvent::SaveDraft : TextUpdateEvent());\n\tcancelReplyMessage();\n\tclearChosenStarsForMessage();\n\tif (_preview) {\n\t\t_preview->apply({ .removed = true });\n\t}\n}\n\nvoid ComposeControls::setText(const TextWithTags &textWithTags) {\n\tsetFieldText(textWithTags);\n}\n\nvoid ComposeControls::setFieldText(\n\t\tconst TextWithTags &textWithTags,\n\t\tTextUpdateEvents events,\n\t\tFieldHistoryAction fieldHistoryAction) {\n\t_textUpdateEvents = events;\n\t_field->setTextWithTags(textWithTags, fieldHistoryAction);\n\tauto cursor = _field->textCursor();\n\tcursor.movePosition(QTextCursor::End);\n\t_field->setTextCursor(cursor);\n\t_textUpdateEvents = TextUpdateEvent::SaveDraft\n\t\t| TextUpdateEvent::SendTyping;\n\n\tcheckCharsLimitation();\n\n\tif (_preview) {\n\t\t_preview->checkNow(false);\n\t}\n}\n\nvoid ComposeControls::saveFieldToHistoryLocalDraft() {\n\tconst auto key = draftKeyCurrent();\n\tif (!_history || !key) {\n\t\treturn;\n\t}\n\tconst auto id = _header->getDraftReply();\n\tif (_preview && (id || !_field->empty())) {\n\t\tconst auto key = draftKeyCurrent();\n\t\t_history->setDraft(\n\t\t\tkey,\n\t\t\tstd::make_unique<Data::Draft>(\n\t\t\t\t_field,\n\t\t\t\tid,\n\t\t\t\tSuggestOptions(),\n\t\t\t\t_preview->draft()));\n\t} else {\n\t\t_history->clearDraft(draftKeyCurrent());\n\t}\n}\n\nvoid ComposeControls::clearFieldText(\n\t\tTextUpdateEvents events,\n\t\tFieldHistoryAction fieldHistoryAction) {\n\tsetFieldText({}, events, fieldHistoryAction);\n}\n\nvoid ComposeControls::hidePanelsAnimated() {\n\tif (_autocomplete) {\n\t\t_autocomplete->hideAnimated();\n\t}\n\tif (_tabbedPanel) {\n\t\t_tabbedPanel->hideAnimated();\n\t}\n\tif (_attachBotsMenu) {\n\t\t_attachBotsMenu->hideAnimated();\n\t}\n\tif (_inlineResults) {\n\t\t_inlineResults->hideAnimated();\n\t}\n}\n\nvoid ComposeControls::hide() {\n\tshowStarted();\n\t_hidden = true;\n}\n\nvoid ComposeControls::show() {\n\tif (_hidden.current()) {\n\t\t_hidden = false;\n\t\tshowFinished();\n\t\tif (_autocomplete) {\n\t\t\t_autocomplete->requestRefresh();\n\t\t}\n\t}\n}\n\nvoid ComposeControls::init() {\n\tif (_attachToggle) {\n\t\t_attachToggle->setAccessibleName(tr::lng_attach(tr::now));\n\t}\n\t_tabbedSelectorToggle->setAccessibleName(tr::lng_emoji_sticker_gif(tr::now));\n\n\tinitField();\n\tinitTabbedSelector();\n\tinitSendButton();\n\tinitAiButton();\n\tinitSendAsFileButton();\n\tinitWriteRestriction();\n\tinitVoiceRecordBar();\n\tinitKeyHandler();\n\tinitEditStarsButton();\n\t_minStarsCount.changes() | rpl::on_next([=] {\n\t\tinitEditStarsButton();\n\t\tupdateControlsGeometry(_wrap->size());\n\t}, _wrap->lifetime());\n\n\t_hidden.changes(\n\t) | rpl::on_next([=] {\n\t\tupdateWrappingVisibility();\n\t}, _wrap->lifetime());\n\n\tif (_botCommandStart) {\n\t\t_botCommandStart->setAccessibleName(tr::lng_bot_commands_start(tr::now));\n\t\t_botCommandStart->setClickedCallback([=] { setText({ \"/\" }); });\n\t}\n\n\tinitLikeButton();\n\n\t_wrap->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tupdateControlsGeometry(size);\n\t}, _wrap->lifetime());\n\n\t_wrap->geometryValue(\n\t) | rpl::on_next([=](QRect rect) {\n\t\tupdateOuterGeometry(rect);\n\t}, _wrap->lifetime());\n\n\t_wrap->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(_wrap.get());\n\t\tpaintBackground(p, _wrap->rect(), clip);\n\t}, _wrap->lifetime());\n\n\t_header->editMsgIdValue(\n\t) | rpl::on_next([=](const auto &id) {\n\t\tunregisterDraftSources();\n\t\tupdateSendButtonType();\n\t\tif (_history && updateSendAsButton(nullptr)) {\n\t\t\tupdateControlsVisibility();\n\t\t\tupdateControlsGeometry(_wrap->size());\n\t\t\torderControls();\n\t\t}\n\t\tregisterDraftSource();\n\t}, _wrap->lifetime());\n\n\t_header->editPhotoRequests(\n\t) | rpl::on_next([=] {\n\t\tconst auto queryToEdit = _header->queryToEdit();\n\t\tEditCaptionBox::StartPhotoEdit(\n\t\t\t_regularWindow,\n\t\t\t_photoEditMedia,\n\t\t\t_editingId,\n\t\t\t_field->getTextWithTags(),\n\t\t\t_header->suggestOptions(),\n\t\t\tqueryToEdit.spoilered,\n\t\t\tqueryToEdit.options.invertCaption,\n\t\t\tcrl::guard(_wrap.get(), [=] { cancelEditMessage(); }));\n\t}, _wrap->lifetime());\n\n\t_header->editOptionsRequests(\n\t) | rpl::on_next([=] {\n\t\tconst auto history = _history;\n\t\tconst auto topicRootId = _topicRootId;\n\t\tconst auto monoforumPeerId = _monoforumPeerId;\n\t\tconst auto reply = _header->replyingToMessage();\n\t\tconst auto suggest = SuggestOptions();\n\t\tconst auto webpage = _preview->draft();\n\n\t\tconst auto done = [=](\n\t\t\t\tFullReplyTo replyTo,\n\t\t\t\tData::WebPageDraft webpage,\n\t\t\t\tData::ForwardDraft forward) {\n\t\t\tif (replyTo) {\n\t\t\t\treplyToMessage(replyTo);\n\t\t\t} else {\n\t\t\t\tcancelReplyMessage();\n\t\t\t}\n\t\t\thistory->setForwardDraft(\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tstd::move(forward));\n\t\t\t_preview->apply(webpage);\n\t\t\t_field->setFocus();\n\t\t};\n\t\tconst auto replyToId = reply.messageId;\n\t\tconst auto highlight = crl::guard(_wrap.get(), [=](FullReplyTo to) {\n\t\t\t_jumpToItemRequests.fire_copy(to);\n\t\t});\n\n\t\tusing namespace HistoryView::Controls;\n\t\tEditDraftOptions({\n\t\t\t.show = _show,\n\t\t\t.history = history,\n\t\t\t.draft = Data::Draft(_field, reply, suggest, _preview->draft()),\n\t\t\t.usedLink = _preview->link(),\n\t\t\t.forward = _header->forwardDraft(),\n\t\t\t.links = _preview->links(),\n\t\t\t.resolver = _preview->resolver(),\n\t\t\t.done = done,\n\t\t\t.highlight = highlight,\n\t\t\t.clearOldDraft = [=] { ClearDraftReplyTo(\n\t\t\t\thistory,\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\treplyToId); },\n\t\t});\n\t}, _wrap->lifetime());\n\n\t_header->previewCancelled(\n\t) | rpl::on_next([=] {\n\t\tif (_preview) {\n\t\t\t_preview->apply({ .removed = true });\n\t\t}\n\t\tsaveDraftWithTextNow();\n\t}, _wrap->lifetime());\n\n\t_header->saveDraftRequests(\n\t) | rpl::on_next([=] {\n\t\tsaveDraftWithTextNow();\n\t}, _wrap->lifetime());\n\n\t_header->editCancelled(\n\t) | rpl::on_next([=] {\n\t\tcancelEditMessage();\n\t}, _wrap->lifetime());\n\n\t_header->replyCancelled(\n\t) | rpl::on_next([=] {\n\t\tcancelReplyMessage();\n\t}, _wrap->lifetime());\n\n\t_header->forwardCancelled(\n\t) | rpl::on_next([=] {\n\t\tcancelForward();\n\t}, _wrap->lifetime());\n\n\t_header->visibleChanged(\n\t) | rpl::on_next([=](bool shown) {\n\t\tupdateHeight();\n\t\tif (shown) {\n\t\t\traisePanels();\n\t\t}\n\t}, _wrap->lifetime());\n\n\tsendContentRequests(\n\t\tSendRequestType::Voice\n\t) | rpl::on_next([=](Api::SendOptions options) {\n\t\t_voiceRecordBar->requestToSendWithOptions(options);\n\t}, _wrap->lifetime());\n\n\t_header->editMsgIdValue(\n\t) | rpl::on_next([=](const auto &id) {\n\t\t_editingId = id;\n\t}, _wrap->lifetime());\n\n\tsession().data().itemRemoved(\n\t) | rpl::filter([=](not_null<const HistoryItem*> item) {\n\t\treturn (_editingId == item->fullId());\n\t}) | rpl::on_next([=] {\n\t\tcancelEditMessage();\n\t}, _wrap->lifetime());\n\n\tCore::App().materializeLocalDraftsRequests(\n\t) | rpl::on_next([=] {\n\t\tsaveFieldToHistoryLocalDraft();\n\t}, _wrap->lifetime());\n\n\tCore::App().settings().sendSubmitWayValue(\n\t) | rpl::on_next([=] {\n\t\tupdateSubmitSettings();\n\t}, _wrap->lifetime());\n\n\tsession().attachWebView().attachBotsUpdates(\n\t) | rpl::on_next([=] {\n\t\tupdateAttachBotsMenu();\n\t}, _wrap->lifetime());\n\n\torderControls();\n}\n\nvoid ComposeControls::orderControls() {\n\t_voiceRecordBar->raise();\n\t_send->raise();\n}\n\nbool ComposeControls::showRecordButton() const {\n\treturn _features.recordMediaMessage\n\t\t&& (_recordAvailability != Webrtc::RecordAvailability::None)\n\t\t&& !_voiceRecordBar->isListenState()\n\t\t&& !_voiceRecordBar->isRecordingByAnotherBar()\n\t\t&& !HasSendText(_field)\n\t\t&& !readyToForward()\n\t\t&& !isEditingMessage();\n}\n\nbool ComposeControls::showEditStarsButton() const {\n\treturn editStarsButtonShown()\n\t\t&& !HasSendText(_field)\n\t\t&& !readyToForward()\n\t\t&& !isEditingMessage()\n\t\t&& !shownStarsPerMessage();\n}\n\nint ComposeControls::shownStarsPerMessage() const {\n\treturn _chosenStarsCount.value_or(\n\t\t_history ? _history->peer->starsPerMessageChecked() : 0);\n}\n\nvoid ComposeControls::clearListenState() {\n\t_voiceRecordBar->clearListenState();\n}\n\nvoid ComposeControls::clearChosenStarsForMessage() {\n\tconst auto empty = editStarsButtonShown()\n\t\t? _minStarsCount.current()\n\t\t: std::optional<int>();\n\tif (_chosenStarsCount != empty) {\n\t\t_chosenStarsCount = empty;\n\t\tupdateSendButtonType();\n\t}\n}\n\nbool ComposeControls::editStarsButtonShown() const {\n\treturn _features.editMessageStars && !_videoStreamAdmin.current();\n}\n\nint ComposeControls::chosenStarsForMessage() const {\n\treturn _chosenStarsCount.value_or(0);\n}\n\nvoid ComposeControls::initKeyHandler() {\n\t_wrap->events(\n\t) | rpl::filter([=](not_null<QEvent*> event) {\n\t\treturn (event->type() == QEvent::KeyPress);\n\t}) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tauto keyEvent = static_cast<QKeyEvent*>(e.get());\n\t\tconst auto key = keyEvent->key();\n\t\tconst auto isCtrl = keyEvent->modifiers() == Qt::ControlModifier;\n\t\tconst auto hasModifiers = (Qt::NoModifier !=\n\t\t\t(keyEvent->modifiers()\n\t\t\t\t& ~(Qt::KeypadModifier | Qt::GroupSwitchModifier)));\n\t\tif (key == Qt::Key_O && isCtrl) {\n\t\t\t_attachRequests.fire({});\n\t\t\treturn;\n\t\t}\n\t\tif (key == Qt::Key_Up && !hasModifiers) {\n\t\t\tif (!isEditingMessage() && _field->empty()) {\n\t\t\t\t_editLastMessageRequests.fire(std::move(keyEvent));\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (!hasModifiers\n\t\t\t&& ((key == Qt::Key_Up)\n\t\t\t\t|| (key == Qt::Key_Down)\n\t\t\t\t|| (key == Qt::Key_PageUp)\n\t\t\t\t|| (key == Qt::Key_PageDown))) {\n\t\t\t_scrollKeyEvents.fire(std::move(keyEvent));\n\t\t}\n\t}, _wrap->lifetime());\n\n\tbase::install_event_filter(_wrap.get(), _field, [=](not_null<QEvent*> e) {\n\t\tusing Result = base::EventFilterResult;\n\t\tif (e->type() != QEvent::KeyPress) {\n\t\t\treturn Result::Continue;\n\t\t}\n\t\tconst auto k = static_cast<QKeyEvent*>(e.get());\n\n\t\tif ((k->modifiers() & kCommonModifiers) == Qt::ControlModifier) {\n\t\t\tconst auto isUp = (k->key() == Qt::Key_Up);\n\t\t\tconst auto isDown = (k->key() == Qt::Key_Down);\n\t\t\tif (isUp || isDown) {\n\t\t\t\tif (Platform::IsMac()) {\n\t\t\t\t\t// Cmd + Up is used instead of Home.\n\t\t\t\t\tif ((isUp && (!_field->textCursor().atStart()))\n\t\t\t\t\t\t// Cmd + Down is used instead of End.\n\t\t\t\t\t\t|| (isDown && (!_field->textCursor().atEnd()))) {\n\t\t\t\t\t\treturn Result::Continue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t_replyNextRequests.fire({\n\t\t\t\t\t.replyId = replyingToMessage().messageId,\n\t\t\t\t\t.direction = (isDown\n\t\t\t\t\t\t? ReplyNextRequest::Direction::Next\n\t\t\t\t\t\t: ReplyNextRequest::Direction::Previous)\n\t\t\t\t});\n\t\t\t\treturn Result::Cancel;\n\t\t\t}\n\t\t} else if (k->key() == Qt::Key_Escape) {\n\t\t\treturn Result::Cancel;\n\t\t}\n\t\treturn Result::Continue;\n\t});\n}\n\nvoid ComposeControls::initField() {\n\t_field->setMaxHeight(st::historyComposeFieldMaxHeight);\n\tupdateSubmitSettings();\n\t_field->cancelled(\n\t) | rpl::on_next([=] {\n\t\tescape();\n\t}, _field->lifetime());\n\t_field->heightChanges(\n\t) | rpl::on_next([=] {\n\t\tupdateHeight();\n\t\tupdateAiButtonVisibility();\n\t\tupdateSendAsFileVisibility();\n\t}, _field->lifetime());\n\t_field->changes(\n\t) | rpl::on_next([=] {\n\t\tfieldChanged();\n\t\tupdateAiButtonVisibility();\n\t\tupdateSendAsFileVisibility();\n\t}, _field->lifetime());\n#ifdef Q_OS_MAC\n\t// Removed an ability to insert text from the menu bar\n\t// when the field is hidden.\n\t_field->shownValue(\n\t) | rpl::on_next([=](bool shown) {\n\t\t_field->setEnabled(shown);\n\t}, _field->lifetime());\n#endif // Q_OS_MAC\n\t_chatStyle = InitMessageField(_show, _field, [=](not_null<DocumentData*> emoji) {\n\t\tif (_history\n\t\t\t&& Data::AllowEmojiWithoutPremium(_history->peer, emoji)) {\n\t\t\treturn true;\n\t\t}\n\t\tif (_unavailableEmojiPasted) {\n\t\t\t_unavailableEmojiPasted(emoji);\n\t\t}\n\t\treturn false;\n\t});\n\tInitMessageFieldFade(_field, _st.field.textBg);\n\t_field->setEditLinkCallback(\n\t\tDefaultEditLinkCallback(_show, _field, &_st.boxField));\n\t_field->setEditLanguageCallback(DefaultEditLanguageCallback(_show));\n\n\tconst auto rawTextEdit = _field->rawTextEdit().get();\n\trpl::merge(\n\t\t_field->scrollTop().changes() | rpl::to_empty,\n\t\tbase::qt_signal_producer(\n\t\t\trawTextEdit,\n\t\t\t&QTextEdit::cursorPositionChanged)\n\t) | rpl::on_next([=] {\n\t\tsaveDraftDelayed();\n\t}, _field->lifetime());\n}\n\nvoid ComposeControls::updateSubmitSettings() {\n\tconst auto settings = _isInlineBot\n\t\t? Ui::InputField::SubmitSettings::None\n\t\t: Core::App().settings().sendSubmitWay();\n\t_field->setSubmitSettings(settings);\n}\n\nvoid ComposeControls::initFieldAutocomplete() {\n\t_emojiSuggestions = nullptr;\n\t_autocomplete = nullptr;\n\tif (!_history || _features.emojiOnlyPanel) {\n\t\treturn;\n\t}\n\tChatHelpers::InitFieldAutocomplete(_autocomplete, {\n\t\t.parent = _parent,\n\t\t.show = _show,\n\t\t.field = _field.get(),\n\t\t.stOverride = &_st.tabbed,\n\t\t.peer = _history->peer,\n\t\t.features = [=] {\n\t\t\tauto result = _features;\n\t\t\tif (_inlineBot && !_inlineLookingUpBot) {\n\t\t\t\tresult.autocompleteMentions = false;\n\t\t\t\tresult.autocompleteHashtags = false;\n\t\t\t\tresult.autocompleteCommands = false;\n\t\t\t}\n\t\t\tif (isEditingMessage()) {\n\t\t\t\tresult.autocompleteCommands = false;\n\t\t\t\tresult.suggestStickersByEmoji = false;\n\t\t\t}\n\t\t\treturn result;\n\t\t},\n\t\t.sendMenuDetails = [=] { return sendMenuDetails(); },\n\t\t.stickerChoosing = [=] {\n\t\t\t_sendActionUpdates.fire({\n\t\t\t\t.type = Api::SendProgressType::ChooseSticker,\n\t\t\t});\n\t\t},\n\t\t.stickerChosen = [=](ChatHelpers::FileChosen &&data) {\n\t\t\tif (!_showSlowmodeError || !_showSlowmodeError()) {\n\t\t\t\tsetText({});\n\t\t\t}\n\t\t\t//saveDraftWithTextNow();\n\t\t\t// Won't be needed if SendInlineBotResult clears the cloud draft.\n\t\t\t//saveCloudDraft();\n\t\t\t_fileChosen.fire(std::move(data));\n\t\t},\n\t\t.setText = [=](TextWithTags text) { setText(text); },\n\t\t.sendBotCommand = [=](QString command) {\n\t\t\t_sendCommandRequests.fire_copy(command);\n\t\t},\n\t});\n\tconst auto allow = [=](not_null<DocumentData*> emoji) {\n\t\treturn Data::AllowEmojiWithoutPremium(_history->peer, emoji);\n\t};\n\t_emojiSuggestions.reset(Ui::Emoji::SuggestionsController::Init(\n\t\t_panelsParent,\n\t\t_field,\n\t\t_session,\n\t\t{\n\t\t\t.suggestCustomEmoji = true,\n\t\t\t.allowCustomWithoutPremium = allow,\n\t\t\t.st = &_st.suggestions,\n\t\t}));\n}\n\nvoid ComposeControls::updateFieldPlaceholder() {\n\t_voiceRecordBar->setPauseInsteadSend(_history\n\t\t&& _history->peer->starsPerMessageChecked() > 0);\n\n\tif (!isEditingMessage() && _isInlineBot) {\n\t\t_field->setPlaceholder(\n\t\t\trpl::single(_inlineBot->botInfo->inlinePlaceholder.mid(1)),\n\t\t\t_inlineBot->username().size() + 2);\n\t\treturn;\n\t}\n\n\t_field->setPlaceholder([&] {\n\t\tconst auto peer = _history ? _history->peer.get() : nullptr;\n\t\tif (_fieldCustomPlaceholder) {\n\t\t\treturn rpl::duplicate(_fieldCustomPlaceholder);\n\t\t} else if (isEditingMessage()) {\n\t\t\treturn tr::lng_edit_message_text();\n\t\t} else if (!peer) {\n\t\t\treturn tr::lng_message_ph();\n\t\t} else if (const auto stars = peer->starsPerMessageChecked()) {\n\t\t\treturn tr::lng_message_stars_ph(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(stars * 1.));\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\tif (channel->isBroadcast()) {\n\t\t\t\treturn session().data().notifySettings().silentPosts(channel)\n\t\t\t\t\t? tr::lng_broadcast_silent_ph()\n\t\t\t\t\t: tr::lng_broadcast_ph();\n\t\t\t} else if (channel->adminRights() & ChatAdminRight::Anonymous) {\n\t\t\t\treturn tr::lng_send_anonymous_ph();\n\t\t\t} else {\n\t\t\t\treturn tr::lng_message_ph();\n\t\t\t}\n\t\t} else {\n\t\t\treturn tr::lng_message_ph();\n\t\t}\n\t}());\n\tupdateSendButtonType();\n}\n\nvoid ComposeControls::updateSilentBroadcast() {\n\tif (!_silent || !_history) {\n\t\treturn;\n\t}\n\tconst auto &peer = _history->peer;\n\tif (!session().data().notifySettings().silentPostsUnknown(peer)) {\n\t\t_silent->setChecked(\n\t\t\tsession().data().notifySettings().silentPosts(peer));\n\t\tupdateFieldPlaceholder();\n\t}\n}\n\nvoid ComposeControls::fieldChanged() {\n\tconst auto typing = (!_inlineBot\n\t\t&& !_header->isEditingMessage()\n\t\t&& (_textUpdateEvents & TextUpdateEvent::SendTyping));\n\tupdateSendButtonType();\n\t_hasSendText = HasSendText(_field);\n\tif (updateBotCommandShown() || updateLikeShown()) {\n\t\tupdateControlsVisibility();\n\t\tupdateControlsGeometry(_wrap->size());\n\t}\n\tInvokeQueued(_field.get(), [=] {\n\t\tupdateInlineBotQuery();\n\t\tif ((!_autocomplete || !_autocomplete->stickersEmoji()) && typing) {\n\t\t\t_sendActionUpdates.fire({ Api::SendProgressType::Typing });\n\t\t}\n\t});\n\n\tcheckCharsLimitation();\n\n\t_saveCloudDraftTimer.cancel();\n\tif (!(_textUpdateEvents & TextUpdateEvent::SaveDraft)) {\n\t\treturn;\n\t}\n\t_saveDraftText = true;\n\tsaveDraft(true);\n}\n\nvoid ComposeControls::saveDraftDelayed() {\n\tif (!(_textUpdateEvents & TextUpdateEvent::SaveDraft)) {\n\t\treturn;\n\t}\n\tsaveDraft(true);\n}\n\nData::DraftKey ComposeControls::draftKey(DraftType type) const {\n\tusing Section = Dialogs::EntryState::Section;\n\tusing Key = Data::DraftKey;\n\n\tswitch (_currentDialogsEntryState.section) {\n\tcase Section::History:\n\tcase Section::Replies:\n\tcase Section::SavedSublist:\n\t\treturn (type == DraftType::Edit)\n\t\t\t? Key::LocalEdit(_topicRootId, _monoforumPeerId)\n\t\t\t: Key::Local(_topicRootId, _monoforumPeerId);\n\tcase Section::Scheduled:\n\t\treturn (type == DraftType::Edit)\n\t\t\t? Key::ScheduledEdit()\n\t\t\t: Key::Scheduled();\n\tcase Section::ShortcutMessages:\n\t\treturn (type == DraftType::Edit)\n\t\t\t? Key::ShortcutEdit(_shortcutId)\n\t\t\t: Key::Shortcut(_shortcutId);\n\t}\n\treturn Key::None();\n}\n\nData::DraftKey ComposeControls::draftKeyCurrent() const {\n\treturn draftKey(isEditingMessage() ? DraftType::Edit : DraftType::Normal);\n}\n\nvoid ComposeControls::saveDraftWithTextNow() {\n\t_saveDraftText = true;\n\t_saveDraftStart = crl::now();\n\tsaveDraft();\n}\n\nvoid ComposeControls::saveDraft(bool delayed) {\n\tif (delayed) {\n\t\tconst auto now = crl::now();\n\t\tif (!_saveDraftStart) {\n\t\t\t_saveDraftStart = now;\n\t\t\treturn _saveDraftTimer.callOnce(kSaveDraftTimeout);\n\t\t} else if (now - _saveDraftStart < kSaveDraftAnywayTimeout) {\n\t\t\treturn _saveDraftTimer.callOnce(kSaveDraftTimeout);\n\t\t}\n\t}\n\twriteDrafts();\n}\n\nvoid ComposeControls::saveCloudDraft() {\n\tsession().api().saveCurrentDraftToCloud();\n}\n\nvoid ComposeControls::writeDraftTexts() {\n\tExpects(_history != nullptr);\n\n\tsession().local().writeDrafts(_history);\n}\n\nvoid ComposeControls::writeDraftCursors() {\n\tExpects(_history != nullptr);\n\n\tsession().local().writeDraftCursors(_history);\n}\n\nvoid ComposeControls::unregisterDraftSources() {\n\tif (!_history) {\n\t\treturn;\n\t}\n\tconst auto normal = draftKey(DraftType::Normal);\n\tconst auto edit = draftKey(DraftType::Edit);\n\tif (normal != Data::DraftKey::None()) {\n\t\tsession().local().unregisterDraftSource(_history, normal);\n\t}\n\tif (edit != Data::DraftKey::None()) {\n\t\tsession().local().unregisterDraftSource(_history, edit);\n\t}\n}\n\nvoid ComposeControls::registerDraftSource() {\n\tif (!_history || !_preview) {\n\t\treturn;\n\t}\n\tconst auto key = draftKeyCurrent();\n\tif (key != Data::DraftKey::None()) {\n\t\tconst auto draft = [=] {\n\t\t\treturn Storage::MessageDraft{\n\t\t\t\t_header->getDraftReply(),\n\t\t\t\t_header->suggestOptions(),\n\t\t\t\t_field->getTextWithTags(),\n\t\t\t\t_preview->draft(),\n\t\t\t};\n\t\t};\n\t\tauto draftSource = Storage::MessageDraftSource{\n\t\t\t.draft = draft,\n\t\t\t.cursor = [=] { return MessageCursor(_field); },\n\t\t};\n\t\tsession().local().registerDraftSource(\n\t\t\t_history,\n\t\t\tkey,\n\t\t\tstd::move(draftSource));\n\t}\n}\n\nvoid ComposeControls::writeDrafts() {\n\tconst auto save = (_history != nullptr)\n\t\t&& (_saveDraftStart > 0)\n\t\t&& (draftKeyCurrent() != Data::DraftKey::None());\n\t_saveDraftStart = 0;\n\t_saveDraftTimer.cancel();\n\tif (save) {\n\t\tif (_saveDraftText) {\n\t\t\twriteDraftTexts();\n\t\t}\n\t\twriteDraftCursors();\n\t}\n\t_saveDraftText = false;\n\n\tif (!isEditingMessage() && !_inlineBot) {\n\t\t_saveCloudDraftTimer.callOnce(kSaveCloudDraftIdleTimeout);\n\t}\n}\n\nvoid ComposeControls::applyCloudDraft() {\n\tif (!isEditingMessage()) {\n\t\tapplyDraft(Ui::InputField::HistoryAction::NewEntry);\n\t}\n}\n\nvoid ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {\n\tExpects(_history != nullptr);\n\n\tconst auto editDraft = _history->draft(draftKey(DraftType::Edit));\n\tconst auto draft = editDraft\n\t\t? editDraft\n\t\t: _history->draft(draftKey(DraftType::Normal));\n\tconst auto editingId = (draft && draft == editDraft)\n\t\t? draft->reply.messageId\n\t\t: FullMsgId();\n\tconst auto editingSuggest = (draft && draft == editDraft)\n\t\t? draft->suggest\n\t\t: SuggestOptions();\n\n\tInvokeQueued(_autocomplete.get(), [=] {\n\t\tif (_autocomplete) {\n\t\t\t_autocomplete->requestStickersUpdate();\n\t\t}\n\t});\n\tconst auto guard = gsl::finally([&] {\n\t\tupdateSendButtonType();\n\t\tupdateReplaceMediaButton();\n\t\tupdateFieldPlaceholder();\n\t\tupdateControlsVisibility();\n\t\tupdateControlsGeometry(_wrap->size());\n\t});\n\n\tconst auto hadFocus = Ui::InFocusChain(_field);\n\tif (!draft) {\n\t\tclearFieldText(0, fieldHistoryAction);\n\t\tif (hadFocus) {\n\t\t\t_field->setFocus();\n\t\t}\n\t\t_header->editMessage({}, {});\n\t\t_header->replyToMessage({});\n\t\tif (_preview) {\n\t\t\t_preview->apply({ .removed = true });\n\t\t\t_preview->setDisabled(false);\n\t\t}\n\t\t_canReplaceMedia = _canAddMedia = false;\n\t\t_photoEditMedia = nullptr;\n\t\treturn;\n\t}\n\n\t_textUpdateEvents = 0;\n\tsetFieldText(draft->textWithTags, 0, fieldHistoryAction);\n\tif (hadFocus) {\n\t\t_field->setFocus();\n\t}\n\tdraft->cursor.applyTo(_field);\n\t_textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping;\n\tif (_preview) {\n\t\t_preview->apply(draft->webpage, draft != editDraft);\n\t}\n\n\tif (draft == editDraft) {\n\t\tconst auto resolve = [=] {\n\t\t\tif (const auto item = _history->owner().message(editingId)) {\n\t\t\t\tconst auto media = item->media();\n\t\t\t\t_canReplaceMedia = item->allowsEditMedia();\n\t\t\t\tif (media && media->allowsEditMedia()) {\n\t\t\t\t\t_canAddMedia = false;\n\t\t\t\t} else {\n\t\t\t\t\t_canAddMedia = base::take(_canReplaceMedia);\n\t\t\t\t}\n\t\t\t\tif (_canReplaceMedia || _canAddMedia) {\n\t\t\t\t\t// Invalidate the button, maybe icon has changed.\n\t\t\t\t\t_replaceMedia = nullptr;\n\t\t\t\t}\n\t\t\t\t_photoEditMedia = (_canReplaceMedia\n\t\t\t\t\t&& _regularWindow\n\t\t\t\t\t&& media->photo()\n\t\t\t\t\t&& !media->photo()->isNull())\n\t\t\t\t\t? media->photo()->createMediaView()\n\t\t\t\t\t: nullptr;\n\t\t\t\tif (_photoEditMedia) {\n\t\t\t\t\t_photoEditMedia->wanted(\n\t\t\t\t\t\tData::PhotoSize::Large,\n\t\t\t\t\t\titem->fullId());\n\t\t\t\t}\n\t\t\t\t_header->editMessage(\n\t\t\t\t\teditingId,\n\t\t\t\t\teditingSuggest,\n\t\t\t\t\t_photoEditMedia != nullptr);\n\t\t\t\tif (_preview) {\n\t\t\t\t\t_preview->apply(\n\t\t\t\t\t\tData::WebPageDraft::FromItem(item),\n\t\t\t\t\t\tfalse);\n\t\t\t\t\t_preview->setDisabled(media && !media->webpage());\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\t_canReplaceMedia = _canAddMedia = false;\n\t\t\t_photoEditMedia = nullptr;\n\t\t\t_header->editMessage(editingId, SuggestOptions(), false);\n\t\t\treturn false;\n\t\t};\n\t\tif (!resolve()) {\n\t\t\tconst auto callback = crl::guard(_header.get(), [=] {\n\t\t\t\tif (_header->editMsgId() == editingId\n\t\t\t\t\t&& resolve()\n\t\t\t\t\t&& updateReplaceMediaButton()) {\n\t\t\t\t\tupdateControlsVisibility();\n\t\t\t\t\tupdateControlsGeometry(_wrap->size());\n\t\t\t\t}\n\t\t\t});\n\t\t\t_history->session().api().requestMessageData(\n\t\t\t\t_history->peer,\n\t\t\t\teditingId.msg,\n\t\t\t\tcallback);\n\t\t}\n\t\t_header->replyToMessage({});\n\t} else {\n\t\t_canReplaceMedia = _canAddMedia = false;\n\t\t_photoEditMedia = nullptr;\n\t\t_header->replyToMessage(draft->reply);\n\t\t_header->editMessage({}, {});\n\t\tif (_preview) {\n\t\t\t_preview->setDisabled(false);\n\t\t}\n\t}\n\tcheckCharsLimitation();\n}\n\nvoid ComposeControls::cancelForward() {\n\t_history->setForwardDraft(_topicRootId, _monoforumPeerId, {});\n\tupdateForwarding();\n}\n\nrpl::producer<SendActionUpdate> ComposeControls::sendActionUpdates() const {\n\treturn rpl::merge(\n\t\t_sendActionUpdates.events(),\n\t\t_voiceRecordBar->sendActionUpdates());\n}\n\nvoid ComposeControls::initTabbedSelector() {\n\tif (!_regularWindow\n\t\t|| !_features.commonTabbedPanel\n\t\t|| _regularWindow->hasTabbedSelectorOwnership()) {\n\t\tcreateTabbedPanel();\n\t} else {\n\t\tsetTabbedPanel(nullptr);\n\t}\n\n\tif (!Core::App().settings().fork().emojiPopupOnClick()) {\n\t_tabbedSelectorToggle->addClickHandler([=] {\n\t\tif (_tabbedPanel && _tabbedPanel->isHidden()) {\n\t\t\t_tabbedPanel->showAnimated();\n\t\t} else {\n\t\t\ttoggleTabbedSelectorMode();\n\t\t}\n\t});\n\t} else {\n\t\t_tabbedSelectorToggle->clicks(\n\t\t) | rpl::on_next([=](Qt::MouseButton button) {\n\t\t\tif (button == Qt::LeftButton) {\n\t\t\t\t_tabbedPanel->toggleAnimated();\n\t\t\t} else if (button == Qt::RightButton) {\n\t\t\t\ttoggleTabbedSelectorMode();\n\t\t\t}\n\t\t}, _wrap->lifetime());\n\t\t_tabbedSelectorToggle->setAcceptBoth(true);\n\t}\n\n\tconst auto wrap = _wrap.get();\n\n\tbase::install_event_filter(wrap, _selector, [=](not_null<QEvent*> e) {\n\t\tif (_tabbedPanel && e->type() == QEvent::ParentChange) {\n\t\t\tsetTabbedPanel(nullptr);\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\t_selector->emojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\tUi::InsertEmojiAtCursor(_field->textCursor(), data.emoji);\n\t}, wrap->lifetime());\n\n\trpl::merge(\n\t\t_selector->fileChosen(),\n\t\t_selector->customEmojiChosen(),\n\t\t_stickerOrEmojiChosen.events()\n\t) | rpl::on_next([=](ChatHelpers::FileChosen &&data) {\n\t\tif (const auto info = data.document->sticker()\n\t\t\t; info && info->setType == Data::StickersType::Emoji) {\n\t\t\tif (data.document->isPremiumEmoji()\n\t\t\t\t&& !session().premium()\n\t\t\t\t&& (!_history\n\t\t\t\t\t|| !Data::AllowEmojiWithoutPremium(\n\t\t\t\t\t\t_history->peer,\n\t\t\t\t\t\tdata.document))) {\n\t\t\t\tif (_unavailableEmojiPasted) {\n\t\t\t\t\t_unavailableEmojiPasted(data.document);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tData::InsertCustomEmoji(_field, data.document);\n\t\t\t}\n\t\t} else {\n\t\t\t_fileChosen.fire(std::move(data));\n\t\t}\n\t}, wrap->lifetime());\n\n\t_selector->photoChosen(\n\t) | rpl::start_to_stream(_photoChosen, wrap->lifetime());\n\n\t_selector->inlineResultChosen(\n\t) | rpl::start_to_stream(_inlineResultChosen, wrap->lifetime());\n\n\t_selector->contextMenuRequested(\n\t) | rpl::on_next([=] {\n\t\t_selector->showMenuWithDetails(sendMenuDetails());\n\t}, wrap->lifetime());\n\n\t_selector->choosingStickerUpdated(\n\t) | rpl::on_next([=](ChatHelpers::TabbedSelector::Action action) {\n\t\t_sendActionUpdates.fire({\n\t\t\t.type = Api::SendProgressType::ChooseSticker,\n\t\t\t.cancel = (action == ChatHelpers::TabbedSelector::Action::Cancel),\n\t\t});\n\t}, wrap->lifetime());\n}\n\nvoid ComposeControls::initSendButton() {\n\trpl::combine(\n\t\t_slowmodeSecondsLeft.value(),\n\t\t_sendDisabledBySlowmode.value()\n\t) | rpl::on_next([=] {\n\t\tupdateSendButtonType();\n\t}, _send->lifetime());\n\n\tCore::App().mediaDevices().recordAvailabilityValue(\n\t) | rpl::on_next([=](Webrtc::RecordAvailability value) {\n\t\t_recordAvailability = value;\n\t\tupdateSendButtonType();\n\t}, _send->lifetime());\n\n\t_send->finishAnimating();\n\n\t_send->clicks(\n\t) | rpl::on_next([=] {\n\t\tif (_send->type() == Ui::SendButton::Type::Cancel) {\n\t\t\tcancelInlineBot();\n\t\t}\n\t}, _send->lifetime());\n\n\tconst auto send = crl::guard(_send.get(), [=](Api::SendOptions options) {\n\t\t_sendCustomRequests.fire(std::move(options));\n\t});\n\tsetupSendMenu(_send.get(), send);\n\n\t_send->widthValue() | rpl::skip(1) | rpl::on_next([=] {\n\t\tupdateControlsGeometry(_wrap->size());\n\t}, _send->lifetime());\n}\n\nvoid ComposeControls::setupSendMenu(\n\t\tnot_null<Ui::RpWidget*> button,\n\t\tFn<void(Api::SendOptions)> send) {\n\tusing namespace SendMenu;\n\tconst auto sendAction = [=](Action action, Details details) {\n\t\tif (action.type == ActionType::ChangePrice) {\n\t\t\t_chosenStarsCount = details.price.value_or(0);\n\t\t\tupdateSendButtonType();\n\t\t} else if (action.type == ActionType::CaptionUp\n\t\t\t|| action.type == ActionType::CaptionDown\n\t\t\t|| action.type == ActionType::SpoilerOn\n\t\t\t|| action.type == ActionType::SpoilerOff) {\n\t\t\t_header->mediaEditManagerApply(action);\n\t\t} else {\n\t\t\tSendMenu::DefaultCallback(_show, send)(action, details);\n\t\t}\n\t};\n\tSendMenu::SetupMenuAndShortcuts(\n\t\tbutton,\n\t\t_show,\n\t\t[=] { return sendButtonMenuDetails(); },\n\t\tsendAction,\n\t\t&_st.tabbed.menu,\n\t\t&_st.tabbed.icons);\n}\n\nvoid ComposeControls::initSendAsButton(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<Data::GroupCall> videoStream) {\n\tusing namespace rpl::mappers;\n\tusing namespace Main;\n\n\t// SendAsPeers::shouldChoose checks Data::CanSendAnything(PeerData*).\n\tconst auto key = SendAsKey{\n\t\tpeer,\n\t\tvideoStream ? SendAsType::VideoStream : SendAsType::Message,\n\t};\n\trpl::combine(\n\t\trpl::single(key) | rpl::then(\n\t\t\tsession().sendAsPeers().updated() | rpl::filter(_1 == key)\n\t\t),\n\t\t(videoStream\n\t\t\t? rpl::single(true)\n\t\t\t: Data::CanSendAnythingValue(key.peer, false))\n\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\tif (updateSendAsButton(videoStream)) {\n\t\t\tupdateControlsVisibility();\n\t\t\tupdateControlsGeometry(_wrap->size());\n\t\t\torderControls();\n\t\t}\n\t}, _historyLifetime);\n\n\tupdateSendAsButton(videoStream);\n}\n\nvoid ComposeControls::cancelInlineBot() {\n\tconst auto &textWithTags = _field->getTextWithTags();\n\tif (textWithTags.text.size() > _inlineBotUsername.size() + 2) {\n\t\tsetFieldText(\n\t\t\t{ '@' + _inlineBotUsername + ' ', TextWithTags::Tags() },\n\t\t\tTextUpdateEvent::SaveDraft,\n\t\t\tUi::InputField::HistoryAction::NewEntry);\n\t} else {\n\t\tclearFieldText(\n\t\t\tTextUpdateEvent::SaveDraft,\n\t\t\tUi::InputField::HistoryAction::NewEntry);\n\t}\n}\n\nvoid ComposeControls::clearInlineBot() {\n\tif (_inlineBot || _inlineLookingUpBot) {\n\t\t_inlineBot = nullptr;\n\t\t_inlineLookingUpBot = false;\n\t\tinlineBotChanged();\n\t\t_field->finishAnimating();\n\t}\n\tif (_inlineResults) {\n\t\t_inlineResults->clearInlineBot();\n\t}\n\tif (_autocomplete) {\n\t\t_autocomplete->requestRefresh();\n\t}\n}\n\nvoid ComposeControls::inlineBotChanged() {\n\tconst auto isInlineBot = (_inlineBot && !_inlineLookingUpBot);\n\tif (_isInlineBot != isInlineBot) {\n\t\t_isInlineBot = isInlineBot;\n\t\tupdateFieldPlaceholder();\n\t\tupdateSubmitSettings();\n\t\tif (_autocomplete) {\n\t\t\t_autocomplete->requestRefresh();\n\t\t}\n\t}\n}\n\nvoid SetupRestrictionView(\n\t\tnot_null<Ui::RpWidget*> widget,\n\t\tnot_null<const style::ComposeControls*> st,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<Controls::WriteRestriction> restriction,\n\t\tFn<void(QPainter &p, QRect clip)> paintBackground) {\n\tstruct State {\n\t\tstd::unique_ptr<Ui::FlatLabel> label;\n\t\tstd::unique_ptr<Ui::AbstractButton> button;\n\t\tstd::unique_ptr<Ui::RoundButton> unlock;\n\t\tstd::unique_ptr<Ui::RpWidget> icon;\n\t\tFn<void()> updateGeometries;\n\t};\n\tconst auto state = widget->lifetime().make_state<State>();\n\tstate->updateGeometries = [=] {\n\t\tif (!state->label && state->button) {\n\t\t\tstate->button->setGeometry(widget->rect());\n\t\t} else if (!state->label) {\n\t\t\treturn;\n\t\t} else if (state->button) {\n\t\t\tconst auto available = widget->width()\n\t\t\t\t- st->like.width\n\t\t\t\t- st::historySendRight\n\t\t\t\t- state->unlock->width()\n\t\t\t\t- st->premiumRequired.buttonSkip\n\t\t\t\t- st->premiumRequired.position.x();\n\t\t\tstate->label->resizeToWidth(available);\n\t\t\tstate->label->moveToLeft(\n\t\t\t\tst->premiumRequired.position.x(),\n\t\t\t\tst->premiumRequired.position.y(),\n\t\t\t\twidget->width());\n\t\t\tconst auto left = st->premiumRequired.position.x()\n\t\t\t\t+ std::min(available, state->label->textMaxWidth())\n\t\t\t\t+ st->premiumRequired.buttonSkip;\n\t\t\tstate->unlock->moveToLeft(\n\t\t\t\tleft,\n\t\t\t\tst->premiumRequired.buttonTop,\n\t\t\t\twidget->width());\n\t\t\tstate->button->setGeometry(QRect(\n\t\t\t\tQPoint(),\n\t\t\t\tQSize(left + state->unlock->width(), widget->height())));\n\t\t\tstate->icon->moveToLeft(0, 0, widget->width());\n\t\t} else {\n\t\t\tconst auto left = st::historySendRight;\n\t\t\tstate->label->resizeToWidth(widget->width() - 2 * left);\n\t\t\tstate->label->moveToLeft(\n\t\t\t\tleft,\n\t\t\t\t(widget->height() - state->label->height()) / 2,\n\t\t\t\twidget->width());\n\t\t}\n\t};\n\tconst auto makeLabel = [=](\n\t\t\tconst QString &text,\n\t\t\tconst style::FlatLabel &st) {\n\t\tauto label = std::make_unique<Ui::FlatLabel>(widget, text, st);\n\t\tlabel->show();\n\t\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tlabel->heightValue(\n\t\t) | rpl::on_next(\n\t\t\tstate->updateGeometries,\n\t\t\tlabel->lifetime());\n\t\treturn label;\n\t};\n\tconst auto makeUnlock = [=](const QString &text, const QString &name) {\n\t\tusing namespace Ui;\n\t\tauto unlock = std::make_unique<RoundButton>(\n\t\t\twidget,\n\t\t\trpl::single(text),\n\t\t\tst->premiumRequired.button);\n\t\tunlock->show();\n\t\tunlock->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tunlock->setFullRadius(true);\n\t\treturn unlock;\n\t};\n\tconst auto makeIcon = [=] {\n\t\tauto icon = std::make_unique<Ui::RpWidget>(widget);\n\t\ticon->resize(st->premiumRequired.icon.size());\n\t\ticon->show();\n\t\ticon->paintRequest() | rpl::on_next([st, raw = icon.get()] {\n\t\t\tauto p = QPainter(raw);\n\t\t\tst->premiumRequired.icon.paint(p, {}, raw->width());\n\t\t}, icon->lifetime());\n\t\treturn icon;\n\t};\n\tstd::move(\n\t\trestriction\n\t) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](Controls::WriteRestriction value) {\n\t\tusing Type = Controls::WriteRestriction::Type;\n\t\tif (value.type == Type::Frozen) {\n\t\t\tstate->icon = nullptr;\n\t\t\tstate->unlock = nullptr;\n\t\t\tstate->label = nullptr;\n\t\t\tstate->button = FrozenWriteRestriction(\n\t\t\t\twidget,\n\t\t\t\tshow,\n\t\t\t\tFrozenWriteRestrictionType::MessageField);\n\t\t} else if (const auto lifting = value.boostsToLift) {\n\t\t\tstate->icon = nullptr;\n\t\t\tstate->unlock = nullptr;\n\t\t\tstate->label = nullptr;\n\t\t\tstate->button = BoostsToLiftWriteRestriction(\n\t\t\t\twidget,\n\t\t\t\tshow,\n\t\t\t\tpeer,\n\t\t\t\tlifting);\n\t\t} else if (value.type == Type::Rights) {\n\t\t\tstate->icon = nullptr;\n\t\t\tstate->unlock = nullptr;\n\t\t\tstate->button = nullptr;\n\t\t\tstate->label = makeLabel(value.text, st->restrictionLabel);\n\t\t} else if (value.type == Type::PremiumRequired) {\n\t\t\tstate->icon = makeIcon();\n\t\t\tstate->unlock = makeUnlock(value.button, peer->shortName());\n\t\t\tstate->button = std::make_unique<Ui::AbstractButton>(widget);\n\t\t\tstate->button->setClickedCallback([=] {\n\t\t\t\t::Settings::ShowPremiumPromoToast(\n\t\t\t\t\tshow,\n\t\t\t\t\ttr::lng_send_non_premium_message_toast(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\tTextWithEntities{ peer->shortName() },\n\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\ttr::link(\n\t\t\t\t\t\t\ttr::bold(\n\t\t\t\t\t\t\t\ttr::lng_send_non_premium_message_toast_link(\n\t\t\t\t\t\t\t\t\ttr::now))),\n\t\t\t\t\t\ttr::rich),\n\t\t\t\t\tu\"require_premium\"_q);\n\t\t\t});\n\t\t\tstate->label = makeLabel(value.text, st->premiumRequired.label);\n\t\t}\n\t\tstate->updateGeometries();\n\t}, widget->lifetime());\n\n\twidget->sizeValue(\n\t) | rpl::on_next(state->updateGeometries, widget->lifetime());\n\n\twidget->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(widget);\n\t\tpaintBackground(p, clip);\n\t}, widget->lifetime());\n}\n\nvoid ComposeControls::initWriteRestriction() {\n\tif (!_history) {\n\t\tconst auto was = base::take(_writeRestricted);\n\t\tupdateWrappingVisibility();\n\t\treturn;\n\t}\n\tif (_like && _like->parentWidget() == _writeRestricted.get()) {\n\t\t// Fix a crash because of _like destruction with its parent.\n\t\t_like->setParent(_wrap.get());\n\t}\n\t_writeRestricted = std::make_unique<Ui::RpWidget>(_parent);\n\t_writeRestricted->move(_wrap->pos());\n\t_writeRestricted->resizeToWidth(_wrap->widthNoMargins());\n\t_writeRestricted->sizeValue() | rpl::on_next([=] {\n\t\tif (_like && _like->parentWidget() == _writeRestricted.get()) {\n\t\t\tupdateControlsGeometry(_wrap->size());\n\t\t}\n\t}, _writeRestricted->lifetime());\n\t_writeRestricted->resize(\n\t\t_writeRestricted->width(),\n\t\t_st.send.inner.height);\n\tconst auto background = [=](QPainter &p, QRect clip) {\n\t\tpaintBackground(p, _writeRestricted->rect(), clip);\n\t};\n\tSetupRestrictionView(\n\t\t_writeRestricted.get(),\n\t\t&_st,\n\t\t_show,\n\t\t_history->peer,\n\t\t_writeRestriction.value(),\n\t\tbackground);\n\n\t_writeRestriction.value(\n\t) | rpl::on_next([=] {\n\t\tupdateWrappingVisibility();\n\t}, _writeRestricted->lifetime());\n}\n\nvoid ComposeControls::changeFocusedControl() {\n\t_focusRequests.fire({});\n\tif (_regularWindow) {\n\t\t_regularWindow->widget()->setInnerFocus();\n\t}\n}\n\nvoid ComposeControls::initVoiceRecordBar() {\n\t_voiceRecordBar->recordingStateChanges(\n\t) | rpl::on_next([=](bool active) {\n\t\tif (active) {\n\t\t\t_recording = true;\n\t\t\tchangeFocusedControl();\n\t\t}\n\t\t_field->setDisabled(active);\n\t\t_field->setVisible(!active);\n\t\tif (!active) {\n\t\t\tchangeFocusedControl();\n\t\t\t_recording = false;\n\t\t}\n\t\tupdateAiButtonVisibility();\n\t\tupdateSendAsFileVisibility();\n\t}, _wrap->lifetime());\n\n\t_voiceRecordBar->setStartRecordingFilter([=] {\n\t\tconst auto error = [&]() -> Data::SendError {\n\t\t\tconst auto peer = _history ? _history->peer.get() : nullptr;\n\t\t\tif (peer) {\n\t\t\t\tif (const auto error = Data::RestrictionError(\n\t\t\t\t\t\tpeer,\n\t\t\t\t\t\tChatRestriction::SendVoiceMessages)) {\n\t\t\t\t\treturn error;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {};\n\t\t}();\n\t\tif (error) {\n\t\t\tData::ShowSendErrorToast(_show, _history->peer, error);\n\t\t\treturn true;\n\t\t} else if (_showSlowmodeError && _showSlowmodeError()) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t});\n\n\t_voiceRecordBar->recordingTipRequests(\n\t) | rpl::on_next([=] {\n\t\tCore::App().settings().setRecordVideoMessages(\n\t\t\t!Core::App().settings().recordVideoMessages());\n\t\tupdateSendButtonType();\n\t\tswitch (_send->type()) {\n\t\tcase Ui::SendButton::Type::Record: {\n\t\t\tconst auto both = Webrtc::RecordAvailability::VideoAndAudio;\n\t\t\t_show->showToast((_recordAvailability == both)\n\t\t\t\t? tr::lng_record_voice_tip(tr::now)\n\t\t\t\t: tr::lng_record_hold_tip(tr::now));\n\t\t} break;\n\t\tcase Ui::SendButton::Type::Round:\n\t\t\t_show->showToast(tr::lng_record_video_tip(tr::now));\n\t\t\tbreak;\n\t\t}\n\t}, _wrap->lifetime());\n\n\t_voiceRecordBar->errors(\n\t) | rpl::on_next([=](::Media::Capture::Error error) {\n\t\tusing Error = ::Media::Capture::Error;\n\t\tswitch (error) {\n\t\tcase Error::AudioInit:\n\t\tcase Error::AudioTimeout:\n\t\t\t_show->showToast(tr::lng_record_audio_problem(tr::now));\n\t\t\tbreak;\n\t\tcase Error::VideoInit:\n\t\tcase Error::VideoTimeout:\n\t\t\t_show->showToast(tr::lng_record_video_problem(tr::now));\n\t\t\tbreak;\n\t\tdefault:\n\t\t\t_show->showToast(u\"Unknown error.\"_q);\n\t\t\tbreak;\n\t\t}\n\t}, _wrap->lifetime());\n\n\t_voiceRecordBar->updateSendButtonTypeRequests(\n\t) | rpl::on_next([=] {\n\t\tupdateSendButtonType();\n\t}, _wrap->lifetime());\n\n\tShortcuts::Requests(\n\t) | rpl::filter([=] {\n\t\treturn Ui::AppInFocus();\n\t}) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {\n\t\tusing Command = Shortcuts::Command;\n\t\tif (Data::CanSendAnything(_history->peer, !_topicRootId)) {\n\t\t\tconst auto isVoice = request->check(Command::RecordVoice, 1);\n\t\t\tconst auto isRound = !isVoice\n\t\t\t\t&& request->check(Command::RecordRound, 1);\n\t\t\t(isVoice || isRound) && request->handle([=] {\n\t\t\t\tif (_voiceRecordBar) {\n\t\t\t\t\t_voiceRecordBar->startRecordingAndLock(isRound);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t});\n\t\t}\n\t}, _voiceRecordBar->lifetime());\n}\n\nvoid ComposeControls::initAiButton() {\n\t_aiButton->hide();\n\t_aiButton->setAccessibleName(tr::lng_ai_compose_title(tr::now));\n\t_aiButton->setClickedCallback([=] {\n\t\tif (_aiTooltipManager) {\n\t\t\t_aiTooltipManager->hideAndRemember();\n\t\t}\n\t\tupdateAiButtonVisibility();\n\t\tshowAiComposeBox();\n\t});\n\n\t_aiTooltipManager = std::make_unique<Controls::AiTooltipManager>(\n\t\t_wrap.get(),\n\t\t_aiButton,\n\t\ttr::lng_ai_compose_tooltip(tr::rich),\n\t\t\"ai_compose_tooltip_hidden\"_cs,\n\t\t[=] { return _wrap->width(); });\n}\n\nvoid ComposeControls::initSendAsFileButton() {\n\tif (!_sendAsFile) {\n\t\treturn;\n\t}\n\t_sendAsFile->hide();\n\t_sendAsFile->setClickedCallback([=] {\n\t\tif (_sendAsFileTooltipManager) {\n\t\t\t_sendAsFileTooltipManager->hideAndRemember();\n\t\t}\n\t\tconst auto text = _field->getTextWithTags();\n\t\tauto restore = restoreTextCallback(QString());\n\t\tfireSendTextAsFile(text.text, std::move(restore));\n\t});\n\n\t_sendAsFileTooltipManager = std::make_unique<Controls::AiTooltipManager>(\n\t\t_wrap.get(),\n\t\t_sendAsFile,\n\t\ttr::lng_send_as_file_tooltip(tr::rich),\n\t\t\"send_as_file_tooltip_hidden\"_cs,\n\t\t[=] { return _wrap->width(); });\n}\n\nvoid ComposeControls::setSendAsFileConfirmed(\n\t\tFn<void(std::shared_ptr<Ui::PreparedBundle>, Api::SendOptions)> confirmed) {\n\t_sendAsFileConfirmed = std::move(confirmed);\n}\n\nvoid ComposeControls::fireSendTextAsFile(\n\t\tconst QString &fileText,\n\t\tFn<void()> restoreText) {\n\tif (!_sendAsFileConfirmed || !_history) {\n\t\treturn;\n\t}\n\tconst auto peer = _history->peer;\n\tconst auto sendType = (_mode == Mode::Scheduled)\n\t\t? (CanScheduleUntilOnline(peer)\n\t\t\t? Api::SendType::ScheduledToUser\n\t\t\t: Api::SendType::Scheduled)\n\t\t: Api::SendType::Normal;\n\t_show->show(Box<SendFilesBox>(SendFilesBoxDescriptor{\n\t\t.show = _show,\n\t\t.list = Ui::PrepareTextAsFile(fileText),\n\t\t.caption = TextWithTags{},\n\t\t.toPeer = peer,\n\t\t.limits = DefaultLimitsForPeer(peer),\n\t\t.check = DefaultCheckForPeer(_show, peer),\n\t\t.sendType = sendType,\n\t\t.sendMenuDetails = _sendMenuDetails,\n\t\t.stOverride = &_st,\n\t\t.confirmed = _sendAsFileConfirmed,\n\t\t.cancelled = std::move(restoreText),\n\t}));\n}\n\nbool ComposeControls::checkLargeTextPaste(\n\t\tnot_null<const QMimeData*> data,\n\t\tUi::InputField::MimeAction action) {\n\tconst auto result = Ui::CheckLargeTextPaste(_field, data);\n\tif (!result.exceeds) {\n\t\treturn false;\n\t}\n\tif (action == Ui::InputField::MimeAction::Check) {\n\t\treturn true;\n\t}\n\tauto restore = restoreTextCallback(QString());\n\tfireSendTextAsFile(result.resultingText, std::move(restore));\n\treturn true;\n}\n\nvoid ComposeControls::updateWrappingVisibility() {\n\tconst auto &restriction = _writeRestriction.current();\n\tconst auto restricted = !restriction.empty() && _writeRestricted;\n\tusing Type = HistoryView::Controls::WriteRestrictionType;\n\tconst auto hidden = _hidden.current()\n\t\t|| (restriction.type == Type::Hidden);\n\tif (_writeRestricted) {\n\t\t_writeRestricted->setVisible(!hidden && restricted);\n\t}\n\t_wrap->setVisible(!hidden && !restricted);\n\tupdateControlsParents();\n\tupdateAiButtonVisibility();\n\tupdateSendAsFileVisibility();\n\tif (!hidden && !restricted) {\n\t\tupdateControlsGeometry(_wrap->size());\n\t\t_wrap->raise();\n\t}\n}\n\nauto ComposeControls::computeSendButtonType() const {\n\tusing Type = Ui::SendButton::Type;\n\n\tif (_header->isEditingMessage()) {\n\t\treturn Type::Save;\n\t} else if (_isInlineBot) {\n\t\treturn Type::Cancel;\n\t} else if (showRecordButton()) {\n\t\tconst auto both = Webrtc::RecordAvailability::VideoAndAudio;\n\t\tconst auto video = Core::App().settings().recordVideoMessages();\n\t\treturn (video && _recordAvailability == both)\n\t\t\t? Type::Round\n\t\t\t: Type::Record;\n\t} else if (showEditStarsButton()) {\n\t\treturn Type::EditPrice;\n\t}\n\treturn (_mode == Mode::Normal) ? Type::Send : Type::Schedule;\n}\n\nSendMenu::Details ComposeControls::sendMenuDetails() const {\n\treturn !_history ? SendMenu::Details() : _sendMenuDetails();\n}\n\nSendMenu::Details ComposeControls::saveMenuDetails() const {\n\treturn _header->saveMenuDetails(HasSendText(_field));\n}\n\nSendMenu::Details ComposeControls::sendButtonMenuDetails() const {\n\treturn (computeSendButtonType() == Ui::SendButton::Type::Save)\n\t\t? saveMenuDetails()\n\t\t: (computeSendButtonType() == Ui::SendButton::Type::Send)\n\t\t? sendMenuDetails()\n\t\t: SendMenu::Details();\n}\n\nvoid ComposeControls::updateSendButtonType() {\n\tusing Type = Ui::SendButton::Type;\n\tconst auto type = computeSendButtonType();\n\tconst auto forbidden = [&] {\n\t\tif (type != Type::Record && type != Type::Round) {\n\t\t\treturn false;\n\t\t}\n\t\tif (!_history) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto restriction = (type == Type::Record)\n\t\t\t? ChatRestriction::SendVoiceMessages\n\t\t\t: ChatRestriction::SendVideoMessages;\n\t\treturn !!Data::RestrictionError(_history->peer, restriction);\n\t}();\n\tconst auto delay = [&] {\n\t\treturn (type != Type::Cancel && type != Type::Save)\n\t\t\t? _slowmodeSecondsLeft.current()\n\t\t\t: 0;\n\t}();\n\tusing namespace Calls::Group::Ui;\n\tconst auto &appConfig = _show->session().appConfig();\n\t_send->setState({\n\t\t.type = type,\n\t\t.fillBgOverride = (_chosenStarsCount.value_or(0)\n\t\t\t? Ui::ColorFromSerialized(StarsColoringForCount(\n\t\t\t\tappConfig.groupCallColorings(),\n\t\t\t\t*_chosenStarsCount).bgLight)\n\t\t\t: QColor()),\n\t\t.slowmodeDelay = delay,\n\t\t.starsToSend = shownStarsPerMessage(),\n\t\t.forbidden = forbidden,\n\t});\n\t_send->setDisabled(_sendDisabledBySlowmode.current()\n\t\t&& (type == Type::Send\n\t\t\t|| type == Type::Record\n\t\t\t|| type == Type::Round));\n}\n\nvoid ComposeControls::finishAnimating() {\n\t_send->finishAnimating();\n\t_voiceRecordBar->finishAnimating();\n}\n\nvoid ComposeControls::updateControlsGeometry(QSize size) {\n\t// (_commentsShown) (_attachToggle|_replaceMedia) (_sendAs) -- _inlineResults ------ _tabbedPanel -- _fieldBarCancel (_starsReaction)\n\t// (_attachDocument|_attachPhoto) _field (_ttlInfo) (_scheduled) (_silent|_botCommandStart) _tabbedSelectorToggle _send\n\n\tconst auto commentsShown = _commentsShown\n\t\t&& !_commentsShown->isHidden();\n\tconst auto fieldWidth = size.width()\n\t\t- (commentsShown\n\t\t\t? (_commentsShown->width() + _st.commentsSkip)\n\t\t\t: 0)\n\t\t- ((_attachToggle || _sendAs) ? _st.padding.left() : _st.fieldLeft)\n\t\t- (_attachToggle ? _attachToggle->width() : 0)\n\t\t- (_sendAs ? _sendAs->width() : 0)\n\t\t- _st.padding.right()\n\t\t- _send->width()\n\t\t- (_editStars ? _editStars->width() : 0)\n\t\t- _tabbedSelectorToggle->width()\n\t\t- (_likeShown ? _like->width() : 0)\n\t\t- (_botCommandShown ? _botCommandStart->width() : 0)\n\t\t- (_silent ? _silent->width() : 0)\n\t\t- (_scheduled ? _scheduled->width() : 0)\n\t\t- (_ttlInfo ? _ttlInfo->width() : 0)\n\t\t- (_starsReaction\n\t\t\t? (_st.starsSkip + _starsReaction->width())\n\t\t\t: 0);\n\t{\n\t\tconst auto oldFieldHeight = _field->height();\n\t\t_field->resizeToWidth(fieldWidth);\n\t\t// If a height of the field is changed\n\t\t// then this method will be called with the updated size.\n\t\tif (oldFieldHeight != _field->height()) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tconst auto buttonsTop = size.height() - _st.attach.height;\n\n\tauto left = 0;\n\tif (commentsShown) {\n\t\t_commentsShown->moveToLeft(left, buttonsTop);\n\t\tleft += _commentsShown->width() + _st.commentsSkip;\n\t}\n\tleft += (_attachToggle || _sendAs) ? _st.padding.left() : _st.fieldLeft;\n\tif (_replaceMedia) {\n\t\t_replaceMedia->moveToLeft(left, buttonsTop);\n\t}\n\tif (_attachToggle) {\n\t\t_attachToggle->moveToLeft(left, buttonsTop);\n\t\tleft += _attachToggle->width();\n\t}\n\tif (_sendAs) {\n\t\t_sendAs->moveToLeft(left, buttonsTop);\n\t\tleft += _sendAs->width();\n\t}\n\t_field->moveToLeft(\n\t\tleft,\n\t\tsize.height() - _st.padding.bottom() - _field->height());\n\n\t_header->resizeToWidth(size.width());\n\t_header->moveToLeft(\n\t\t0,\n\t\t_field->y() - _st.padding.top() - _header->height());\n\n\tauto right = 0;\n\tif (_starsReaction) {\n\t\t_starsReaction->moveToRight(right, buttonsTop);\n\t\tright += _starsReaction->width() + _st.starsSkip;\n\t}\n\tright += _st.padding.right();\n\t_send->moveToRight(right, buttonsTop);\n\tright += _send->width();\n\tif (_editStars) {\n\t\t_editStars->moveToRight(right, buttonsTop);\n\t\tright += _editStars->width();\n\t}\n\t_tabbedSelectorToggle->moveToRight(right, buttonsTop);\n\tright += _tabbedSelectorToggle->width();\n\tif (_like) {\n\t\tusing Type = Controls::WriteRestrictionType;\n\t\tif (_writeRestriction.current().type == Type::PremiumRequired) {\n\t\t\t_like->moveToRight(st::historySendRight, 0);\n\t\t} else {\n\t\t\t_like->moveToRight(right, buttonsTop);\n\t\t\tif (_likeShown) {\n\t\t\t\tright += _like->width();\n\t\t\t}\n\t\t}\n\t}\n\tif (_botCommandStart) {\n\t\t_botCommandStart->moveToRight(right, buttonsTop);\n\t\tif (_botCommandShown) {\n\t\t\tright += _botCommandStart->width();\n\t\t}\n\t}\n\tif (_silent) {\n\t\t_silent->moveToRight(right, buttonsTop);\n\t\tright += _silent->width();\n\t}\n\tif (_scheduled) {\n\t\t_scheduled->moveToRight(right, buttonsTop);\n\t\tright += _scheduled->width();\n\t}\n\tif (_ttlInfo) {\n\t\t_ttlInfo->move(size.width() - right - _ttlInfo->width(), buttonsTop);\n\t}\n\tupdateAiButtonGeometry();\n\tupdateSendAsFileGeometry();\n\n\t_voiceRecordBar->resizeToWidth(size.width());\n\t_voiceRecordBar->moveToLeft(\n\t\t0,\n\t\tsize.height() - _voiceRecordBar->height());\n}\n\nvoid ComposeControls::updateControlsVisibility() {\n\tif (_botCommandStart) {\n\t\t_botCommandStart->setVisible(_botCommandShown);\n\t}\n\tif (_like) {\n\t\t_like->setVisible(_likeShown);\n\t}\n\tif (_editStars) {\n\t\t_editStars->show();\n\t}\n\tif (_ttlInfo) {\n\t\t_ttlInfo->show();\n\t}\n\tif (_sendAs) {\n\t\t_sendAs->show();\n\t}\n\tif (_replaceMedia) {\n\t\t_replaceMedia->show();\n\t}\n\tif (_attachToggle) {\n\t\t_attachToggle->setVisible(!_replaceMedia);\n\t}\n\tif (_scheduled) {\n\t\t_scheduled->setVisible(!isEditingMessage());\n\t}\n\tif (_commentsShown) {\n\t\t_commentsShown->setVisible(!_commentsShownHidden.current());\n\t}\n\tif (_starsReaction) {\n\t\t_starsReaction->show();\n\t}\n\tupdateAiButtonVisibility();\n\tupdateSendAsFileVisibility();\n}\n\nvoid ComposeControls::updateAiButtonVisibility() {\n\tconst auto hidden = !hasEnoughLinesForAi()\n\t\t|| !_wrap->isVisible()\n\t\t|| _recording.current()\n\t\t|| !_field->isVisible();\n\tif (_aiButton->isHidden() == hidden) {\n\t\treturn;\n\t}\n\tconst auto shown = !hidden;\n\t_aiButton->setVisible(shown);\n\tif (shown) {\n\t\tupdateAiButtonGeometry();\n\t}\n\tif (_aiTooltipManager) {\n\t\t_aiTooltipManager->updateVisibility(shown);\n\t}\n}\n\nvoid ComposeControls::updateAiButtonGeometry() {\n\tif (_aiButton->isHidden()) {\n\t\treturn;\n\t}\n\tconst auto x = _send->x() + _send->width() - _aiButton->width();\n\t_aiButton->move(QPoint(x, _field->y()) + st::historyAiComposeButtonPosition);\n\tif (_aiTooltipManager) {\n\t\t_aiTooltipManager->updateGeometry();\n\t}\n}\n\nvoid ComposeControls::updateSendAsFileVisibility() {\n\tif (!_sendAsFile) {\n\t\treturn;\n\t}\n\tconst auto hidden = !textExceedsMaxSize()\n\t\t|| _wrap->isHidden()\n\t\t|| _recording.current()\n\t\t|| _field->isHidden();\n\tif (_sendAsFile->isHidden() == hidden) {\n\t\treturn;\n\t}\n\t_sendAsFile->setVisible(!hidden);\n\tif (!hidden) {\n\t\tupdateSendAsFileGeometry();\n\t}\n\tif (_sendAsFileTooltipManager) {\n\t\t_sendAsFileTooltipManager->updateVisibility(!hidden);\n\t}\n}\n\nvoid ComposeControls::updateSendAsFileGeometry() {\n\tif (!_sendAsFile || _sendAsFile->isHidden()) {\n\t\treturn;\n\t}\n\tconst auto x = _send->x() + _send->width() - _sendAsFile->width();\n\t_sendAsFile->move(QPoint(x, _field->y()) + st::historyAiComposeButtonPosition);\n\tif (_sendAsFileTooltipManager) {\n\t\t_sendAsFileTooltipManager->updateGeometry();\n\t}\n}\n\nbool ComposeControls::updateLikeShown() {\n\tauto shown = _like && !HasSendText(_field);\n\tif (_likeShown != shown) {\n\t\t_likeShown = shown;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid ComposeControls::showAiComposeBox() {\n\tconst auto text = prepareTextForEditMsg();\n\tif (text.text.isEmpty()) {\n\t\treturn;\n\t}\n\tauto send = Fn<void(TextWithEntities, Api::SendOptions, Fn<void()>)>();\n\tauto setupMenu = Fn<void(\n\t\tnot_null<Ui::RpWidget*>,\n\t\tFn<void(Api::SendOptions)>)>();\n\tif (canSendAiComposeDirect() && _sendWithText) {\n\t\tsend = crl::guard(_wrap.get(), [=](\n\t\t\t\tTextWithEntities result,\n\t\t\t\tApi::SendOptions options,\n\t\t\t\tFn<void()> done) {\n\t\t\t_sendWithText(std::move(result), options, std::move(done));\n\t\t});\n\t\tsetupMenu = crl::guard(_wrap.get(), [=](\n\t\t\t\tnot_null<Ui::RpWidget*> button,\n\t\t\t\tFn<void(Api::SendOptions)> sendCallback) {\n\t\t\tsetupSendMenu(button, sendCallback);\n\t\t});\n\t}\n\tControls::ShowComposeAiBox(_show, {\n\t\t.session = _session,\n\t\t.text = text,\n\t\t.chatStyle = _chatStyle,\n\t\t.apply = crl::guard(_wrap.get(), [=](TextWithEntities result) {\n\t\t\tconst auto action = Ui::InputField::HistoryAction::NewEntry;\n\t\t\tsetFieldText({\n\t\t\t\tresult.text,\n\t\t\t\tTextUtilities::ConvertEntitiesToTextTags(result.entities),\n\t\t\t}, TextUpdateEvent::SaveDraft, action);\n\t\t}),\n\t\t.send = std::move(send),\n\t\t.setupMenu = std::move(setupMenu),\n\t});\n}\n\nbool ComposeControls::canSendAiComposeDirect() const {\n\tusing Type = Ui::SendButton::Type;\n\treturn _history\n\t\t&& (computeSendButtonType() == Type::Send)\n\t\t&& (_slowmodeSecondsLeft.current() == 0)\n\t\t&& !_sendDisabledBySlowmode.current()\n\t\t&& !shownStarsPerMessage();\n}\n\nbool ComposeControls::hasEnoughLinesForAi() const {\n\treturn _history\n\t\t&& !_recording.current()\n\t\t&& Ui::HasEnoughLinesForAi(&session(), _field);\n}\n\nbool ComposeControls::textExceedsMaxSize() const {\n\treturn _history\n\t\t&& !_recording.current()\n\t\t&& _field->getLastText().size() > MaxMessageSize;\n}\n\nbool ComposeControls::updateBotCommandShown() {\n\tauto shown = false;\n\tconst auto peer = _history ? _history->peer.get() : nullptr;\n\tif (_botCommandStart && peer) {\n\t\tconst auto hasBotCommands = [&] {\n\t\t\tif (peer->isChat()) {\n\t\t\t\treturn !peer->asChat()->botCommands().empty();\n\t\t\t} else if (peer->isMegagroup()) {\n\t\t\t\treturn !peer->asChannel()->mgInfo->botCommands().empty();\n\t\t\t} else if (peer->isUser()) {\n\t\t\t\treturn peer->asUser()->isBot();\n\t\t\t}\n\t\t\treturn false;\n\t\t}();\n\t\tif (hasBotCommands && !HasSendText(_field)) {\n\t\t\tshown = true;\n\t\t}\n\t}\n\tif (_botCommandShown != shown) {\n\t\t_botCommandShown = shown;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid ComposeControls::updateOuterGeometry(QRect rect) {\n\tif (_inlineResults) {\n\t\t_inlineResults->moveBottom(rect.y());\n\t}\n\tconst auto bottom = rect.y() + rect.height() - _st.attach.height;\n\tif (_tabbedPanel) {\n\t\t_tabbedPanel->moveBottomRight(bottom, rect.x() + rect.width());\n\t}\n\tif (_attachBotsMenu) {\n\t\t_attachBotsMenu->moveToLeft(0, bottom - _attachBotsMenu->height());\n\t}\n}\n\nvoid ComposeControls::updateMessagesTTLShown() {\n\tconst auto peer = _history ? _history->peer.get() : nullptr;\n\tconst auto shown = _features.ttlInfo\n\t\t&& peer\n\t\t&& (peer->messagesTTL() > 0);\n\tif (!shown && _ttlInfo) {\n\t\t_ttlInfo = nullptr;\n\t\tupdateControlsVisibility();\n\t\tupdateControlsGeometry(_wrap->size());\n\t} else if (shown && !_ttlInfo) {\n\t\t_ttlInfo = std::make_unique<Controls::TTLButton>(\n\t\t\t_wrap.get(),\n\t\t\t_show,\n\t\t\tpeer);\n\t\torderControls();\n\t\tupdateControlsVisibility();\n\t\tupdateControlsGeometry(_wrap->size());\n\t}\n}\n\nbool ComposeControls::updateSendAsButton(\n\t\tstd::shared_ptr<Data::GroupCall> videoStream) {\n\tconst auto peer = _history ? _history->peer.get() : nullptr;\n\tconst auto type = videoStream\n\t\t? Main::SendAsType::VideoStream\n\t\t: Main::SendAsType::Message;\n\tif (!_features.sendAs\n\t\t|| !peer\n\t\t|| isEditingMessage()\n\t\t|| !session().sendAsPeers().shouldChoose({ peer, type })) {\n\t\tif (!_sendAs) {\n\t\t\treturn false;\n\t\t}\n\t\t_videoStreamAdmin = false;\n\t\t_sendAs = nullptr;\n\t\treturn true;\n\t} else if (_sendAs) {\n\t\treturn false;\n\t}\n\tconst auto &st = _st.chooseSendAs;\n\t_sendAs = std::make_unique<Ui::SendAsButton>(_wrap.get(), st.button);\n\tif (videoStream) {\n\t\tUi::SetupSendAsButton(_sendAs.get(), st, videoStream, _show);\n\t\t_videoStreamAdmin = videoStream->creator();\n\t\tinitEditStarsButton();\n\t\tupdateControlsGeometry(_wrap->size());\n\t} else {\n\t\tUi::SetupSendAsButton(_sendAs.get(), st, rpl::single(peer), _show);\n\t\t_videoStreamAdmin = false;\n\t}\n\treturn true;\n}\n\nvoid ComposeControls::updateAttachBotsMenu() {\n\t_attachBotsMenu = nullptr;\n\tif (!_features.attachBotsMenu\n\t\t|| !_features.attachments\n\t\t|| !_history\n\t\t|| !_sendActionFactory\n\t\t|| !_regularWindow) {\n\t\treturn;\n\t}\n\t_attachBotsMenu = InlineBots::MakeAttachBotsMenu(\n\t\t_panelsParent,\n\t\t_regularWindow,\n\t\t_history->peer,\n\t\t_sendActionFactory,\n\t\t[=](bool compress) { _attachRequests.fire_copy(compress); });\n\tif (!_attachBotsMenu) {\n\t\treturn;\n\t}\n\t_attachBotsMenu->setOrigin(\n\t\tUi::PanelAnimation::Origin::BottomLeft);\n\t_attachToggle->installEventFilter(_attachBotsMenu.get());\n\t_attachBotsMenu->heightValue(\n\t) | rpl::on_next([=] {\n\t\tupdateOuterGeometry(_wrap->geometry());\n\t}, _attachBotsMenu->lifetime());\n}\n\nvoid ComposeControls::paintBackground(QPainter &p, QRect full, QRect clip) {\n\tif (_backgroundRect) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setBrush(_st.bg);\n\t\tp.setPen(Qt::NoPen);\n\t\tconst auto r = _st.radius;\n\t\tif (_commentsShown && !_commentsShown->isHidden()) {\n\t\t\tp.drawRoundedRect(_commentsShown->geometry(), r, r);\n\t\t\tfull.setLeft(full.left()\n\t\t\t\t+ _commentsShown->width()\n\t\t\t\t+ _st.commentsSkip);\n\t\t}\n\t\tif (_starsReaction) {\n\t\t\tfull.setWidth(full.width()\n\t\t\t\t- _starsReaction->width()\n\t\t\t\t- _st.starsSkip);\n\t\t}\n\t\tp.drawRoundedRect(full, _st.radius, _st.radius);\n\t} else {\n\t\tp.fillRect(clip, _st.bg);\n\t}\n}\n\nvoid ComposeControls::escape() {\n\tif (const auto voice = _voiceRecordBar.get(); voice->isActive()) {\n\t\tvoice->showDiscardBox(nullptr, anim::type::normal);\n\t} else {\n\t\t_cancelRequests.fire({});\n\t}\n}\n\nbool ComposeControls::pushTabbedSelectorToThirdSection(\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst Window::SectionShow &params) {\n\tif (!_tabbedPanel || !_regularWindow || !_features.commonTabbedPanel) {\n\t\treturn true;\n\t//} else if (!_canSendMessages) {\n\t//\tCore::App().settings().setTabbedReplacedWithInfo(true);\n\t//\t_window->showPeerInfo(_peer, params.withThirdColumn());\n\t//\treturn;\n\t}\n\tCore::App().settings().setTabbedReplacedWithInfo(false);\n\t_tabbedSelectorToggle->setColorOverrides(\n\t\t&st::historyAttachEmojiActive,\n\t\t&st::historyRecordVoiceFgActive,\n\t\t&st::historyRecordVoiceRippleBgActive);\n\t_regularWindow->resizeForThirdSection();\n\t_regularWindow->showSection(\n\t\tstd::make_shared<ChatHelpers::TabbedMemento>(),\n\t\tparams.withThirdColumn());\n\treturn true;\n}\n\nbool ComposeControls::returnTabbedSelector() {\n\tcreateTabbedPanel();\n\tupdateOuterGeometry(_wrap->geometry());\n\treturn true;\n}\n\nvoid ComposeControls::createTabbedPanel() {\n\tusing namespace ChatHelpers;\n\tauto descriptor = TabbedPanelDescriptor{\n\t\t.regularWindow = _regularWindow,\n\t\t.ownedSelector = (_ownedSelector\n\t\t\t? object_ptr<TabbedSelector>::fromRaw(_ownedSelector.release())\n\t\t\t: object_ptr<TabbedSelector>(nullptr)),\n\t\t.nonOwnedSelector = _ownedSelector ? nullptr : _selector.get(),\n\t};\n\tsetTabbedPanel(std::make_unique<TabbedPanel>(\n\t\t_panelsParent,\n\t\tstd::move(descriptor)));\n\t_tabbedPanel->setDesiredHeightValues(\n\t\tst::emojiPanHeightRatio,\n\t\t_st.tabbedHeightMin,\n\t\t_st.tabbedHeightMax);\n}\n\nvoid ComposeControls::setTabbedPanel(\n\t\tstd::unique_ptr<ChatHelpers::TabbedPanel> panel) {\n\t_tabbedPanel = std::move(panel);\n\tif (const auto raw = _tabbedPanel.get()) {\n\t\tif (!Core::App().settings().fork().emojiPopupOnClick()) {\n\t\t_tabbedSelectorToggle->installEventFilter(raw);\n\t\t}\n\t\t_tabbedSelectorToggle->setColorOverrides(nullptr, nullptr, nullptr);\n\t} else {\n\t\t_tabbedSelectorToggle->setColorOverrides(\n\t\t\t&st::historyAttachEmojiActive,\n\t\t\t&st::historyRecordVoiceFgActive,\n\t\t\t&st::historyRecordVoiceRippleBgActive);\n\t}\n}\n\nvoid ComposeControls::toggleTabbedSelectorMode() {\n\tif (!_history || !_regularWindow || !_features.commonTabbedPanel) {\n\t\treturn;\n\t}\n\tif (_tabbedPanel) {\n\t\tif (_regularWindow->canShowThirdSection()\n\t\t\t\t&& !_regularWindow->adaptive().isOneColumn()) {\n\t\t\tCore::App().settings().setTabbedSelectorSectionEnabled(true);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\tconst auto thread = _history->threadFor(\n\t\t\t\t_topicRootId,\n\t\t\t\t_monoforumPeerId);\n\t\t\tpushTabbedSelectorToThirdSection(\n\t\t\t\tthread ? thread : _history,\n\t\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\t} else {\n\t\t\t_tabbedPanel->toggleAnimated();\n\t\t}\n\t} else {\n\t\t_regularWindow->closeThirdSection();\n\t}\n}\n\nvoid ComposeControls::updateHeight() {\n\tconst auto height = (_header->isDisplayed() ? _header->height() : 0)\n\t\t+ _st.padding.top()\n\t\t+ _field->height()\n\t\t+ _st.padding.bottom();\n\tif (height != _wrap->height()) {\n\t\t_wrap->resize(_wrap->width(), height);\n\t}\n}\n\nvoid ComposeControls::editMessage(\n\t\tFullMsgId id,\n\t\tconst TextSelection &selection) {\n\tif (const auto item = session().data().message(id)) {\n\t\teditMessage(item);\n\t\tSelectTextInFieldWithMargins(_field, selection);\n\t}\n}\n\nvoid ComposeControls::editMessage(not_null<HistoryItem*> item) {\n\tExpects(_history != nullptr);\n\tExpects(draftKeyCurrent() != Data::DraftKey::None());\n\n\tif (_voiceRecordBar->isActive()) {\n\t\t_show->showBox(Ui::MakeInformBox(tr::lng_edit_caption_voice()));\n\t\treturn;\n\t} else if (const auto media = item->media()) {\n\t\tif (media->todolist()) {\n\t\t\tAssert(_regularWindow != nullptr);\n\t\t\tWindow::PeerMenuEditTodoList(_regularWindow, item);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tif (!isEditingMessage()) {\n\t\tsaveFieldToHistoryLocalDraft();\n\t}\n\tconst auto editData = PrepareEditText(item);\n\tconst auto cursor = MessageCursor{\n\t\tint(editData.text.size()),\n\t\tint(editData.text.size()),\n\t\tUi::kQFixedMax\n\t};\n\tconst auto key = draftKey(DraftType::Edit);\n\t_history->setDraft(\n\t\tkey,\n\t\tstd::make_unique<Data::Draft>(\n\t\t\teditData,\n\t\t\tFullReplyTo{\n\t\t\t\t.messageId = item->fullId(),\n\t\t\t\t.topicRootId = key.topicRootId(),\n\t\t\t\t.monoforumPeerId = key.monoforumPeerId(),\n\t\t\t},\n\t\t\tSuggestOptions(),\n\t\t\tcursor,\n\t\t\tData::WebPageDraft::FromItem(item)));\n\tapplyDraft();\n\tif (updateReplaceMediaButton()) {\n\t\tupdateControlsVisibility();\n\t\tupdateControlsGeometry(_wrap->size());\n\t}\n\n\tif (_autocomplete) {\n\t\tInvokeQueued(_autocomplete.get(), [=] {\n\t\t\t_autocomplete->requestRefresh();\n\t\t});\n\t}\n}\n\nbool ComposeControls::updateReplaceMediaButton() {\n\tif ((!_canReplaceMedia && !_canAddMedia) || !_regularWindow) {\n\t\tconst auto result = (_replaceMedia != nullptr);\n\t\t_replaceMedia = nullptr;\n\t\treturn result;\n\t} else if (_replaceMedia) {\n\t\treturn false;\n\t}\n\t_replaceMedia = std::make_unique<Ui::IconButton>(\n\t\t_wrap.get(),\n\t\t_canReplaceMedia ? st::historyReplaceMedia : st::historyAddMedia);\n\t_replaceMedia->setAccessibleName(_canReplaceMedia\n\t\t? tr::lng_attach_replace(tr::now)\n\t\t: tr::lng_attach(tr::now));\n\tconst auto hideDuration = st::historyReplaceMedia.ripple.hideDuration;\n\t_replaceMedia->setClickedCallback([=] {\n\t\tbase::call_delayed(hideDuration, _wrap.get(), [=] {\n\t\t\tconst auto queryToEdit = _header->queryToEdit();\n\t\t\tEditCaptionBox::StartMediaReplace(\n\t\t\t\t_regularWindow,\n\t\t\t\t_editingId,\n\t\t\t\t_field->getTextWithTags(),\n\t\t\t\t_header->suggestOptions(),\n\t\t\t\tqueryToEdit.spoilered,\n\t\t\t\tqueryToEdit.options.invertCaption,\n\t\t\t\tcrl::guard(_wrap.get(), [=] { cancelEditMessage(); }));\n\t\t});\n\t});\n\treturn true;\n}\n\nvoid ComposeControls::cancelEditMessage() {\n\tExpects(_history != nullptr);\n\tExpects(draftKeyCurrent() != Data::DraftKey::None());\n\n\t_history->clearDraft(draftKey(DraftType::Edit));\n\tapplyDraft();\n\tsaveDraftWithTextNow();\n}\n\nvoid ComposeControls::maybeCancelEditMessage() {\n\tExpects(_history != nullptr);\n\n\tconst auto item = _history->owner().message(_header->editMsgId());\n\tif (item && EditTextChanged(item, _field->getTextWithTags())) {\n\t\tconst auto guard = _field.get();\n\t\t_show->show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_cancel_edit_post_sure(),\n\t\t\t.confirmed = crl::guard(guard, [this](Fn<void()> &&close) {\n\t\t\t\tcancelEditMessage();\n\t\t\t\tclose();\n\t\t\t}),\n\t\t\t.confirmText = tr::lng_cancel_edit_post_yes(),\n\t\t\t.cancelText = tr::lng_cancel_edit_post_no(),\n\t\t}));\n\t} else {\n\t\tcancelEditMessage();\n\t}\n}\n\nvoid ComposeControls::replyToMessage(FullReplyTo id) {\n\tExpects(_history != nullptr);\n\tExpects(draftKeyCurrent() != Data::DraftKey::None());\n\n\tid.topicRootId = _topicRootId;\n\tid.monoforumPeerId = _monoforumPeerId;\n\tif (!id) {\n\t\tcancelReplyMessage();\n\t\treturn;\n\t}\n\tif (isEditingMessage()) {\n\t\tconst auto key = draftKey(DraftType::Normal);\n\t\tAssert(key.topicRootId() == id.topicRootId);\n\t\tAssert(key.monoforumPeerId() == id.monoforumPeerId);\n\t\tif (const auto localDraft = _history->draft(key)) {\n\t\t\tlocalDraft->reply = id;\n\t\t} else {\n\t\t\t_history->setDraft(\n\t\t\t\tkey,\n\t\t\t\tstd::make_unique<Data::Draft>(\n\t\t\t\t\tTextWithTags(),\n\t\t\t\t\tid,\n\t\t\t\t\tSuggestOptions(),\n\t\t\t\t\tMessageCursor(),\n\t\t\t\t\tData::WebPageDraft()));\n\t\t}\n\t} else {\n\t\t_header->replyToMessage(id);\n\t}\n\tsaveDraftWithTextNow();\n}\n\nvoid ComposeControls::cancelReplyMessage() {\n\tconst auto wasReply = replyingToMessage();\n\t_header->replyToMessage({});\n\tif (_history) {\n\t\tconst auto key = draftKey(DraftType::Normal);\n\t\tif (const auto localDraft = _history->draft(key)) {\n\t\t\tif (localDraft->reply.messageId) {\n\t\t\t\tif (localDraft->textWithTags.text.isEmpty()) {\n\t\t\t\t\t_history->clearDraft(key);\n\t\t\t\t} else {\n\t\t\t\t\tlocalDraft->reply = {};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (wasReply) {\n\t\t\tsaveDraftWithTextNow();\n\t\t}\n\t}\n}\n\nvoid ComposeControls::updateForwarding() {\n\tconst auto thread = (_history && (_topicRootId || _monoforumPeerId))\n\t\t? _history->threadFor(_topicRootId, _monoforumPeerId)\n\t\t: (Data::Thread*)_history;\n\t_header->updateForwarding(thread, thread\n\t\t? _history->resolveForwardDraft(_topicRootId, _monoforumPeerId)\n\t\t: Data::ResolvedForwardDraft());\n\tupdateSendButtonType();\n}\n\nbool ComposeControls::handleCancelRequest() {\n\tif (_isInlineBot) {\n\t\tcancelInlineBot();\n\t\treturn true;\n\t} else if (_autocomplete && !_autocomplete->isHidden()) {\n\t\t_autocomplete->hideAnimated();\n\t\treturn true;\n\t} else if (isEditingMessage()) {\n\t\tmaybeCancelEditMessage();\n\t\treturn true;\n\t} else if (replyingToMessage().replying()) {\n\t\tcancelReplyMessage();\n\t\treturn true;\n\t} else if (readyToForward()) {\n\t\tcancelForward();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid ComposeControls::tryProcessKeyInput(not_null<QKeyEvent*> e) {\n\tif (_field->isVisible() && !e->text().isEmpty()) {\n\t\t_field->setFocusFast();\n\t\tQCoreApplication::sendEvent(_field->rawTextEdit(), e);\n\t}\n}\n\nvoid ComposeControls::initWebpageProcess() {\n\tif (!_history) {\n\t\t_preview = nullptr;\n\t\t_header->previewUnregister();\n\t\treturn;\n\t}\n\n\t_preview = std::make_unique<Controls::WebpageProcessor>(\n\t\t_history,\n\t\t_field);\n\n\t_preview->repaintRequests(\n\t) | rpl::on_next(crl::guard(_header.get(), [=] {\n\t\t_header->update();\n\t}), _historyLifetime);\n\n\tsession().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::Rights\n\t\t| Data::PeerUpdate::Flag::Notifications\n\t\t| Data::PeerUpdate::Flag::MessagesTTL\n\t\t| Data::PeerUpdate::Flag::FullInfo\n\t\t| Data::PeerUpdate::Flag::StarsPerMessage\n\t) | rpl::filter([peer = _history->peer](const Data::PeerUpdate &update) {\n\t\treturn (update.peer.get() == peer);\n\t}) | rpl::map([](const Data::PeerUpdate &update) {\n\t\treturn update.flags;\n\t}) | rpl::on_next([=](Data::PeerUpdate::Flags flags) {\n\t\tif (flags & Data::PeerUpdate::Flag::Rights) {\n\t\t\t_preview->checkNow(false);\n\t\t\tupdateFieldPlaceholder();\n\t\t\tupdateSendButtonType();\n\t\t}\n\t\tif (flags & Data::PeerUpdate::Flag::Notifications) {\n\t\t\tupdateSilentBroadcast();\n\t\t}\n\t\tif (flags & Data::PeerUpdate::Flag::MessagesTTL) {\n\t\t\tupdateMessagesTTLShown();\n\t\t}\n\t\tif (flags & Data::PeerUpdate::Flag::StarsPerMessage) {\n\t\t\tupdateFieldPlaceholder();\n\t\t}\n\t\tif (flags & Data::PeerUpdate::Flag::FullInfo) {\n\t\t\tupdateSendButtonType();\n\t\t\tif (updateBotCommandShown()) {\n\t\t\t\tupdateControlsVisibility();\n\t\t\t\tupdateControlsGeometry(_wrap->size());\n\t\t\t}\n\t\t}\n\t}, _historyLifetime);\n\n\tsession().data().botCommandsChanges(\n\t) | rpl::filter([peer = _history->peer](not_null<PeerData*> p) {\n\t\treturn (p == peer);\n\t}) | rpl::on_next([=] {\n\t\tif (updateBotCommandShown()) {\n\t\t\tupdateControlsVisibility();\n\t\t\tupdateControlsGeometry(_wrap->size());\n\t\t}\n\t}, _historyLifetime);\n\n\t_header->previewReady(_preview->parsedValue());\n}\n\nvoid ComposeControls::initForwardProcess() {\n\tusing EntryUpdateFlag = Data::EntryUpdate::Flag;\n\tsession().changes().entryUpdates(\n\t\tEntryUpdateFlag::ForwardDraft\n\t) | rpl::on_next([=](const Data::EntryUpdate &update) {\n\t\tif (const auto topic = update.entry->asTopic()) {\n\t\t\tif (topic->history() == _history\n\t\t\t\t&& topic->rootId() == _topicRootId) {\n\t\t\t\tupdateForwarding();\n\t\t\t}\n\t\t} else if (const auto sublist = update.entry->asSublist()) {\n\t\t\tif (sublist->owningHistory() == _history\n\t\t\t\t&& sublist->sublistPeer()->id == _monoforumPeerId) {\n\t\t\t\tupdateForwarding();\n\t\t\t}\n\t\t}\n\t}, _wrap->lifetime());\n\n\tupdateForwarding();\n}\n\nData::WebPageDraft ComposeControls::webPageDraft() const {\n\treturn _preview ? _preview->draft() : Data::WebPageDraft();\n}\n\nrpl::producer<FullReplyTo> ComposeControls::jumpToItemRequests() const {\n\treturn rpl::merge(\n\t\t_header->jumpToItemRequests(),\n\t\t_jumpToItemRequests.events());\n}\n\nbool ComposeControls::isEditingMessage() const {\n\treturn _header->isEditingMessage();\n}\n\nFullReplyTo ComposeControls::replyingToMessage() const {\n\tauto result = _header->replyingToMessage();\n\tresult.topicRootId = _topicRootId;\n\tresult.monoforumPeerId = _monoforumPeerId;\n\treturn result;\n}\n\nbool ComposeControls::readyToForward() const {\n\treturn _header->readyToForward();\n}\n\nbool ComposeControls::isLockPresent() const {\n\treturn _voiceRecordBar->isLockPresent();\n}\n\nbool ComposeControls::isTTLButtonShown() const {\n\treturn _voiceRecordBar->isTTLButtonShown();\n}\n\nrpl::producer<bool> ComposeControls::lockShowStarts() const {\n\treturn _voiceRecordBar->lockShowStarts();\n}\n\nrpl::producer<not_null<QEvent*>> ComposeControls::viewportEvents() const {\n\treturn _voiceRecordBar->lockViewportEvents();\n}\n\nrpl::producer<> ComposeControls::likeToggled() const {\n\treturn _likeToggled.events();\n}\n\nbool ComposeControls::isRecording() const {\n\treturn _voiceRecordBar->isRecording();\n}\n\nbool ComposeControls::isRecordingPressed() const {\n\treturn !_voiceRecordBar->isRecordingLocked()\n\t\t&& (!_voiceRecordBar->isHidden()\n\t\t\t|| (_send->isDown()\n\t\t\t\t&& (_send->type() == Ui::SendButton::Type::Record\n\t\t\t\t\t|| _send->type() == Ui::SendButton::Type::Round)));\n}\n\nrpl::producer<bool> ComposeControls::recordingActiveValue() const {\n\treturn _voiceRecordBar->shownValue();\n}\n\nrpl::producer<bool> ComposeControls::hasSendTextValue() const {\n\treturn _hasSendText.value();\n}\n\nrpl::producer<bool> ComposeControls::fieldMenuShownValue() const {\n\treturn _field->menuShownValue();\n}\n\nUi::RpWidget *ComposeControls::likeAnimationTarget() const {\n\treturn _like;\n}\n\nint ComposeControls::fieldCharacterCount() const {\n\treturn Ui::ComputeFieldCharacterCount(_field);\n}\n\nbool ComposeControls::preventsClose(Fn<void()> &&continueCallback) const {\n\tif (_voiceRecordBar->isActive()) {\n\t\t_voiceRecordBar->showDiscardBox(std::move(continueCallback));\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool ComposeControls::hasSilentBroadcastToggle() const {\n\tif (!_features.silentBroadcastToggle || !_history) {\n\t\treturn false;\n\t}\n\tconst auto &peer = _history->peer;\n\treturn peer\n\t\t&& peer->isBroadcast()\n\t\t&& Data::CanSendAnything(peer)\n\t\t&& !session().data().notifySettings().silentPostsUnknown(peer);\n}\n\nvoid ComposeControls::updateInlineBotQuery() {\n\tif (!_history || !_regularWindow) {\n\t\treturn;\n\t}\n\tconst auto query = ParseInlineBotQuery(&session(), _field);\n\tif (_inlineBotUsername != query.username) {\n\t\t_inlineBotUsername = query.username;\n\t\tauto &api = session().api();\n\t\tif (_inlineBotResolveRequestId) {\n\t\t\tapi.request(_inlineBotResolveRequestId).cancel();\n\t\t\t_inlineBotResolveRequestId = 0;\n\t\t}\n\t\tif (query.lookingUpBot) {\n\t\t\t_inlineBot = nullptr;\n\t\t\t_inlineLookingUpBot = true;\n\t\t\tconst auto username = _inlineBotUsername;\n\t\t\t_inlineBotResolveRequestId = api.request(\n\t\t\t\tMTPcontacts_ResolveUsername(\n\t\t\t\t\tMTP_flags(0),\n\t\t\t\t\tMTP_string(username),\n\t\t\t\t\tMTP_string())\n\t\t\t).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\t\t\tExpects(result.type() == mtpc_contacts_resolvedPeer);\n\n\t\t\t\tconst auto &data = result.c_contacts_resolvedPeer();\n\t\t\t\tconst auto resolvedBot = [&]() -> UserData* {\n\t\t\t\t\tif (const auto user = session().data().processUsers(\n\t\t\t\t\t\t\tdata.vusers())) {\n\t\t\t\t\t\tif (user->isBot()\n\t\t\t\t\t\t\t&& !user->botInfo->inlinePlaceholder.isEmpty()) {\n\t\t\t\t\t\t\treturn user;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn nullptr;\n\t\t\t\t}();\n\t\t\t\tsession().data().processChats(data.vchats());\n\n\t\t\t\t_inlineBotResolveRequestId = 0;\n\t\t\t\tconst auto query = ParseInlineBotQuery(&session(), _field);\n\t\t\t\tif (_inlineBotUsername == query.username) {\n\t\t\t\t\tapplyInlineBotQuery(\n\t\t\t\t\t\tquery.lookingUpBot ? resolvedBot : query.bot,\n\t\t\t\t\t\tquery.query);\n\t\t\t\t} else {\n\t\t\t\t\tclearInlineBot();\n\t\t\t\t}\n\n\t\t\t}).fail([=] {\n\t\t\t\t_inlineBotResolveRequestId = 0;\n\t\t\t\tif (username == _inlineBotUsername) {\n\t\t\t\t\tclearInlineBot();\n\t\t\t\t}\n\t\t\t}).send();\n\t\t} else {\n\t\t\tapplyInlineBotQuery(query.bot, query.query);\n\t\t}\n\t} else if (query.lookingUpBot) {\n\t\tif (!_inlineLookingUpBot) {\n\t\t\tapplyInlineBotQuery(_inlineBot, query.query);\n\t\t}\n\t} else {\n\t\tapplyInlineBotQuery(query.bot, query.query);\n\t}\n}\n\nvoid ComposeControls::applyInlineBotQuery(\n\t\tUserData *bot,\n\t\tconst QString &query) {\n\tExpects(_regularWindow != nullptr);\n\n\tif (_history && bot) {\n\t\tif (_inlineBot != bot) {\n\t\t\t_inlineBot = bot;\n\t\t\t_inlineLookingUpBot = false;\n\t\t\tinlineBotChanged();\n\t\t}\n\t\tif (!_inlineResults) {\n\t\t\t_inlineResults = std::make_unique<InlineBots::Layout::Widget>(\n\t\t\t\t_panelsParent,\n\t\t\t\t_regularWindow);\n\t\t\t_inlineResults->setResultSelectedCallback([=](\n\t\t\t\t\tInlineBots::ResultSelected result) {\n\t\t\t\tif (result.open) {\n\t\t\t\t\tconst auto request = result.result->openRequest();\n\t\t\t\t\tif (const auto photo = request.photo()) {\n\t\t\t\t\t\t_regularWindow->openPhoto(photo, {});\n\t\t\t\t\t} else if (const auto document = request.document()) {\n\t\t\t\t\t\t_regularWindow->openDocument(document, false, {});\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t_inlineResultChosen.fire_copy(result);\n\t\t\t\t}\n\t\t\t});\n\t\t\t_inlineResults->setSendMenuDetails([=] {\n\t\t\t\treturn sendMenuDetails();\n\t\t\t});\n\t\t\t_inlineResults->requesting(\n\t\t\t) | rpl::on_next([=](bool requesting) {\n\t\t\t\t_tabbedSelectorToggle->setLoading(requesting);\n\t\t\t}, _inlineResults->lifetime());\n\t\t\tupdateOuterGeometry(_wrap->geometry());\n\t\t}\n\t\t_inlineResults->queryInlineBot(_inlineBot, _history->peer, query);\n\t\tif (_autocomplete) {\n\t\t\t_autocomplete->hideAnimated();\n\t\t}\n\t} else {\n\t\tclearInlineBot();\n\t}\n}\n\nFn<void()> ComposeControls::restoreTextCallback(\n\t\tconst QString &insertTextOnCancel) const {\n\tconst auto cursor = _field->textCursor();\n\tconst auto position = cursor.position();\n\tconst auto anchor = cursor.anchor();\n\tconst auto text = getTextWithAppliedMarkdown();\n\n\t_field->setTextWithTags({});\n\n\treturn crl::guard(_field, [=] {\n\t\t_field->setTextWithTags(text);\n\t\tauto cursor = _field->textCursor();\n\t\tcursor.setPosition(anchor);\n\t\tif (position != anchor) {\n\t\t\tcursor.setPosition(position, QTextCursor::KeepAnchor);\n\t\t}\n\t\t_field->setTextCursor(cursor);\n\t\tif (Ui::InsertTextOnImageCancel(insertTextOnCancel)) {\n\t\t\t_field->textCursor().insertText(insertTextOnCancel);\n\t\t}\n\t});\n}\n\nUi::InputField *ComposeControls::fieldForMention() const {\n\treturn _writeRestriction.current() ? nullptr : _field.get();\n}\n\nTextWithEntities ComposeControls::prepareTextForEditMsg() const {\n\tif (!_history) {\n\t\treturn {};\n\t}\n\tconst auto textWithTags = getTextWithAppliedMarkdown();\n\tconst auto prepareFlags = Ui::ItemTextOptions(\n\t\t_history,\n\t\tsession().user()).flags;\n\tauto left = TextWithEntities {\n\t\ttextWithTags.text,\n\t\tTextUtilities::ConvertTextTagsToEntities(textWithTags.tags) };\n\tTextUtilities::PrepareForSending(left, prepareFlags);\n\treturn left;\n}\n\nvoid ComposeControls::checkCharsLimitation() {\n\tif (!_history || !isEditingMessage()) {\n\t\t_charsLimitation = nullptr;\n\t\treturn;\n\t}\n\tconst auto item = _history->owner().message(_header->editMsgId());\n\tif (!item) {\n\t\t_charsLimitation = nullptr;\n\t\treturn;\n\t}\n\tconst auto hasMediaWithCaption = item->media()\n\t\t&& item->media()->allowsEditCaption();\n\tconst auto maxCaptionSize = !hasMediaWithCaption\n\t\t? MaxMessageSize\n\t\t: Data::PremiumLimits(&session()).captionLengthCurrent();\n\tconst auto remove = Ui::ComputeFieldCharacterCount(_field)\n\t\t- maxCaptionSize;\n\tif (remove > 0) {\n\t\tif (!_charsLimitation) {\n\t\t\tusing namespace Controls;\n\t\t\t_charsLimitation = base::make_unique_q<CharactersLimitLabel>(\n\t\t\t\t_wrap.get(),\n\t\t\t\t_send.get(),\n\t\t\t\tstyle::al_bottom);\n\t\t\t_charsLimitation->show();\n\t\t\tData::AmPremiumValue(\n\t\t\t\t&session()\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tcheckCharsLimitation();\n\t\t\t}, _charsLimitation->lifetime());\n\t\t}\n\t\t_charsLimitation->setLeft(remove);\n\t} else {\n\t\t_charsLimitation = nullptr;\n\t}\n}\n\nrpl::producer<int> SlowmodeSecondsLeft(not_null<PeerData*> peer) {\n\treturn peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::Slowmode\n\t) | rpl::map([=] {\n\t\treturn peer->slowmodeSecondsLeft();\n\t}) | rpl::map([=](int delay) -> rpl::producer<int> {\n\t\tauto start = rpl::single(delay);\n\t\tif (!delay) {\n\t\t\treturn start;\n\t\t}\n\t\treturn std::move(\n\t\t\tstart\n\t\t) | rpl::then(base::timer_each(\n\t\t\tkRefreshSlowmodeLabelTimeout\n\t\t) | rpl::map([=] {\n\t\t\treturn peer->slowmodeSecondsLeft();\n\t\t}) | rpl::take_while([=](int delay) {\n\t\t\treturn delay > 0;\n\t\t})) | rpl::then(rpl::single(0));\n\t}) | rpl::flatten_latest();\n}\n\nrpl::producer<bool> SendDisabledBySlowmode(not_null<PeerData*> peer) {\n\tconst auto history = peer->owner().history(peer);\n\tauto hasSendingMessage = peer->session().changes().historyFlagsValue(\n\t\thistory,\n\t\tData::HistoryUpdate::Flag::ClientSideMessages\n\t) | rpl::map([=] {\n\t\treturn history->latestSendingMessage() != nullptr;\n\t}) | rpl::distinct_until_changed();\n\n\tusing namespace rpl::mappers;\n\tconst auto channel = peer->asChannel();\n\treturn (!channel || channel->amCreator())\n\t\t? (rpl::single(false) | rpl::type_erased)\n\t\t: rpl::combine(\n\t\t\tchannel->slowmodeAppliedValue(),\n\t\t\tstd::move(hasSendingMessage),\n\t\t\t_1 && _2);\n}\n\nvoid ShowPhotoEditSpoilerMenu(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<HistoryItem*> item,\n\t\tconst std::optional<bool> &override,\n\t\tFn<void(bool)> callback) {\n\tconst auto media = item->media();\n\tconst auto hasPreview = media && media->hasReplyPreview();\n\tconst auto preview = hasPreview ? media->replyPreview() : nullptr;\n\tif (!preview) {\n\t\treturn;\n\t}\n\tconst auto spoilered = override\n\t\t? (*override)\n\t\t: (preview && media->hasSpoiler());\n\tconst auto menu = Ui::CreateChild<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\tMenu::AddCheckedAction(\n\t\tmenu,\n\t\ttr::lng_context_spoiler_effect(tr::now),\n\t\t[=] { callback(!spoilered); },\n\t\t&st::menuIconSpoiler,\n\t\tspoilered);\n\tmenu->popup(QCursor::pos());\n}\n\nImage *MediaPreviewWithOverriddenSpoiler(\n\t\tnot_null<HistoryItem*> item,\n\t\tbool spoiler) {\n\tif (const auto media = item->media()) {\n\t\tif (const auto photo = media->photo()) {\n\t\t\treturn photo->getReplyPreview(\n\t\t\t\titem->fullId(),\n\t\t\t\titem->history()->peer,\n\t\t\t\tspoiler);\n\t\t} else if (const auto document = media->document()) {\n\t\t\treturn document->getReplyPreview(\n\t\t\t\titem->fullId(),\n\t\t\t\titem->history()->peer,\n\t\t\t\tspoiler);\n\t\t}\n\t}\n\treturn nullptr;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_common.h\"\n#include \"base/required.h\"\n#include \"base/unique_qptr.h\"\n#include \"base/timer.h\"\n#include \"chat_helpers/compose/compose_features.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"history/view/controls/compose_controls_common.h\"\n#include \"ui/round_rect.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/widgets/fields/input_field.h\"\n\nclass History;\nclass DocumentData;\nclass Image;\n\nnamespace style {\nstruct ComposeControls;\n} // namespace style\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nnamespace ChatHelpers {\nclass TabbedPanel;\nclass TabbedSelector;\nstruct FileChosen;\nstruct PhotoChosen;\nclass Show;\nenum class PauseReason;\nclass FieldAutocomplete;\n} // namespace ChatHelpers\n\nnamespace Data {\nstruct MessagePosition;\nstruct Draft;\nclass DraftKey;\nclass PhotoMedia;\nclass GroupCall;\nstruct WebPageDraft;\nstruct MessageReactionsTopPaid;\n} // namespace Data\n\nnamespace InlineBots {\nnamespace Layout {\nclass ItemBase;\nclass Widget;\n} // namespace Layout\nclass Result;\nstruct ResultSelected;\n} // namespace InlineBots\n\nnamespace Ui {\nclass AbstractButton;\nclass SendButton;\nclass IconButton;\nclass EmojiButton;\nclass SendAsButton;\nclass SilentToggle;\nclass DropdownMenu;\nstruct PreparedBundle;\nstruct PreparedList;\nstruct SendStarButtonState;\nclass ReactionFlyAnimation;\nclass ChatStyle;\n} // namespace Ui\n\nnamespace Ui::Emoji {\nclass SuggestionsController;\n} // namespace Ui::Emoji\n\nnamespace Main {\nclass Session;\nstruct SendAsKey;\n} // namespace Main\n\nnamespace Webrtc {\nenum class RecordAvailability : uchar;\n} // namespace Webrtc\n\nnamespace Window {\nstruct SectionShow;\nclass SessionController;\n} // namespace Window\n\nnamespace Api {\nenum class SendProgressType;\n} // namespace Api\n\nnamespace HistoryView::Controls {\nclass VoiceRecordBar;\nclass TTLButton;\nclass WebpageProcessor;\nclass CharactersLimitLabel;\nclass ComposeAiButton;\nclass ComposeTooltipManager;\nusing AiTooltipManager = ComposeTooltipManager;\n} // namespace HistoryView::Controls\n\nnamespace HistoryView {\n\nclass FieldHeader;\n\nenum class ComposeControlsMode {\n\tNormal,\n\tScheduled,\n};\n\nextern const ChatHelpers::PauseReason kDefaultPanelsLevel;\n\nstruct ComposeControlsDescriptor {\n\tconst style::ComposeControls *stOverride = nullptr;\n\tstd::shared_ptr<ChatHelpers::Show> show;\n\tFn<void(not_null<DocumentData*>)> unavailableEmojiPasted;\n\tComposeControlsMode mode = ComposeControlsMode::Normal;\n\tFn<SendMenu::Details()> sendMenuDetails = nullptr;\n\tWindow::SessionController *regularWindow = nullptr;\n\trpl::producer<ChatHelpers::FileChosen> stickerOrEmojiChosen;\n\trpl::producer<QString> customPlaceholder;\n\tQWidget *panelsParent = nullptr;\n\tChatHelpers::PauseReason panelsLevel = kDefaultPanelsLevel;\n\tQString voiceCustomCancelText;\n\tbool voiceLockFromBottom = false;\n\tChatHelpers::ComposeFeatures features;\n\trpl::producer<bool> scheduledToggleValue;\n};\n\nclass ComposeControls final {\npublic:\n\tusing FileChosen = ChatHelpers::FileChosen;\n\tusing PhotoChosen = ChatHelpers::PhotoChosen;\n\tusing InlineChosen = InlineBots::ResultSelected;\n\n\tusing MessageToEdit = Controls::MessageToEdit;\n\tusing VoiceToSend = Controls::VoiceToSend;\n\tusing SendActionUpdate = Controls::SendActionUpdate;\n\tusing SetHistoryArgs = Controls::SetHistoryArgs;\n\tusing ReplyNextRequest = Controls::ReplyNextRequest;\n\tusing FieldHistoryAction = Ui::InputField::HistoryAction;\n\tusing Mode = ComposeControlsMode;\n\tusing ToggleCommentsState = Controls::ToggleCommentsState;\n\tusing SendStarButtonEffect = Controls::SendStarButtonEffect;\n\n\tComposeControls(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tComposeControlsDescriptor descriptor);\n\t~ComposeControls();\n\n\t[[nodiscard]] Main::Session &session() const;\n\tvoid setHistory(SetHistoryArgs &&args);\n\tvoid updateFeatures(ChatHelpers::ComposeFeatures features);\n\tvoid updateTopicRootId(MsgId topicRootId);\n\tvoid updateShortcutId(BusinessShortcutId shortcutId);\n\tvoid setCurrentDialogsEntryState(Dialogs::EntryState state);\n\t[[nodiscard]] PeerData *sendAsPeer() const;\n\n\tvoid finishAnimating();\n\n\tvoid move(int x, int y);\n\tvoid resizeToWidth(int width);\n\tvoid setAutocompleteBoundingRect(QRect rect);\n\t[[nodiscard]] rpl::producer<int> height() const;\n\t[[nodiscard]] int heightCurrent() const;\n\n\tvoid setupCommentsShownNewDot();\n\tvoid setToggleCommentsButton(rpl::producer<ToggleCommentsState> state);\n\t[[nodiscard]] rpl::producer<> commentsShownToggles() const;\n\tvoid setStarsReactionCounter(\n\t\trpl::producer<Ui::SendStarButtonState> count,\n\t\trpl::producer<SendStarButtonEffect> effects);\n\tusing StarReactionTop = Data::MessageReactionsTopPaid;\n\tvoid setStarsReactionTop(\n\t\trpl::producer<std::vector<StarReactionTop>> top);\n\tstruct StarReactionIncrement {\n\t\tint count = 0;\n\t\tbool fromBox = false;\n\t};\n\t[[nodiscard]] auto starsReactionIncrements() const\n\t\t-> rpl::producer<StarReactionIncrement>;\n\n\tbool focus();\n\t[[nodiscard]] bool focused() const;\n\t[[nodiscard]] rpl::producer<bool> focusedValue() const;\n\t[[nodiscard]] rpl::producer<bool> tabbedPanelShownValue() const;\n\t[[nodiscard]] rpl::producer<> cancelRequests() const;\n\t[[nodiscard]] rpl::producer<Api::SendOptions> sendRequests() const;\n\t[[nodiscard]] rpl::producer<VoiceToSend> sendVoiceRequests() const;\n\t[[nodiscard]] rpl::producer<QString> sendCommandRequests() const;\n\t[[nodiscard]] rpl::producer<MessageToEdit> editRequests() const;\n\t[[nodiscard]] rpl::producer<std::optional<bool>> attachRequests() const;\n\tvoid setSendAsFileConfirmed(\n\t\tFn<void(\n\t\t\tstd::shared_ptr<Ui::PreparedBundle>,\n\t\t\tApi::SendOptions)> confirmed);\n\t[[nodiscard]] rpl::producer<FileChosen> fileChosen() const;\n\t[[nodiscard]] rpl::producer<PhotoChosen> photoChosen() const;\n\t[[nodiscard]] rpl::producer<FullReplyTo> jumpToItemRequests() const;\n\t[[nodiscard]] rpl::producer<InlineChosen> inlineResultChosen() const;\n\t[[nodiscard]] rpl::producer<SendActionUpdate> sendActionUpdates() const;\n\t[[nodiscard]] rpl::producer<not_null<QEvent*>> viewportEvents() const;\n\t[[nodiscard]] rpl::producer<> likeToggled() const;\n\t[[nodiscard]] auto scrollKeyEvents() const\n\t-> rpl::producer<not_null<QKeyEvent*>>;\n\t[[nodiscard]] auto editLastMessageRequests() const\n\t-> rpl::producer<not_null<QKeyEvent*>>;\n\t[[nodiscard]] auto replyNextRequests() const\n\t-> rpl::producer<ReplyNextRequest>;\n\t[[nodiscard]] rpl::producer<> focusRequests() const;\n\t[[nodiscard]] rpl::producer<> showScheduledRequests() const;\n\t[[nodiscard]] rpl::producer<> scrollToMaxRequests() const;\n\n\tusing MimeDataHook = Fn<bool(\n\t\tnot_null<const QMimeData*> data,\n\t\tUi::InputField::MimeAction action)>;\n\tvoid setMimeDataHook(MimeDataHook hook);\n\tbool confirmMediaEdit(Ui::PreparedList &list);\n\n\tbool pushTabbedSelectorToThirdSection(\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst Window::SectionShow &params);\n\tbool returnTabbedSelector();\n\n\t[[nodiscard]] bool isEditingMessage() const;\n\t[[nodiscard]] bool readyToForward() const;\n\t[[nodiscard]] const HistoryItemsList &forwardItems() const;\n\t[[nodiscard]] FullReplyTo replyingToMessage() const;\n\n\t[[nodiscard]] bool preventsClose(Fn<void()> &&continueCallback) const;\n\n\tvoid showForGrab();\n\tvoid showStarted();\n\tvoid showFinished();\n\tvoid raisePanels();\n\n\tvoid editMessage(FullMsgId id, const TextSelection &selection);\n\tvoid cancelEditMessage();\n\tvoid maybeCancelEditMessage(); // Confirm if changed and cancel.\n\n\tvoid replyToMessage(FullReplyTo id);\n\tvoid cancelReplyMessage();\n\n\tvoid updateForwarding();\n\tvoid cancelForward();\n\n\tbool handleCancelRequest();\n\tvoid tryProcessKeyInput(not_null<QKeyEvent*> e);\n\n\t[[nodiscard]] TextWithTags getTextWithAppliedMarkdown() const;\n\t[[nodiscard]] Data::WebPageDraft webPageDraft() const;\n\tvoid setText(const TextWithTags &text);\n\tvoid clear();\n\tvoid hidePanelsAnimated();\n\tvoid clearListenState();\n\n\tvoid clearChosenStarsForMessage();\n\t[[nodiscard]] int chosenStarsForMessage() const;\n\n\tvoid hide();\n\tvoid show();\n\n\t[[nodiscard]] rpl::producer<bool> lockShowStarts() const;\n\t[[nodiscard]] bool isLockPresent() const;\n\t[[nodiscard]] bool isTTLButtonShown() const;\n\t[[nodiscard]] bool isRecording() const;\n\t[[nodiscard]] bool isRecordingPressed() const;\n\t[[nodiscard]] rpl::producer<bool> recordingActiveValue() const;\n\t[[nodiscard]] rpl::producer<bool> hasSendTextValue() const;\n\t[[nodiscard]] rpl::producer<bool> fieldMenuShownValue() const;\n\t[[nodiscard]] Ui::RpWidget *likeAnimationTarget() const;\n\t[[nodiscard]] int fieldCharacterCount() const;\n\n\t[[nodiscard]] TextWithEntities prepareTextForEditMsg() const;\n\n\tvoid applyCloudDraft();\n\tvoid applyDraft(\n\t\tFieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);\n\n\tFn<void()> restoreTextCallback(const QString &insertTextOnCancel) const;\n\n\t[[nodiscard]] Ui::InputField *fieldForMention() const;\n\nprivate:\n\tstruct StarEffect;\n\tenum class TextUpdateEvent {\n\t\tSaveDraft = (1 << 0),\n\t\tSendTyping = (1 << 1),\n\t};\n\tenum class DraftType {\n\t\tNormal,\n\t\tEdit,\n\t};\n\tenum class SendRequestType {\n\t\tText,\n\t\tVoice,\n\t};\n\tusing TextUpdateEvents = base::flags<TextUpdateEvent>;\n\tfriend inline constexpr bool is_flag_type(TextUpdateEvent) { return true; };\n\n\tvoid init();\n\tvoid initField();\n\tvoid initFieldAutocomplete();\n\tvoid initTabbedSelector();\n\tvoid initSendButton();\n\tvoid initSendAsButton(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<Data::GroupCall> videoStream);\n\tvoid initWebpageProcess();\n\tvoid initForwardProcess();\n\tvoid initWriteRestriction();\n\tvoid initVoiceRecordBar();\n\tvoid initKeyHandler();\n\tvoid initLikeButton();\n\tvoid initEditStarsButton();\n\tvoid initAiButton();\n\tvoid updateControlsParents();\n\tvoid updateSubmitSettings();\n\tvoid updateSendButtonType();\n\tvoid updateMessagesTTLShown();\n\tbool updateSendAsButton(std::shared_ptr<Data::GroupCall> videoStream);\n\tvoid updateAttachBotsMenu();\n\tvoid updateHeight();\n\tvoid updateWrappingVisibility();\n\tvoid updateControlsVisibility();\n\tvoid updateControlsGeometry(QSize size);\n\tvoid updateAiButtonVisibility();\n\tvoid updateAiButtonGeometry();\n\tvoid initSendAsFileButton();\n\tvoid fireSendTextAsFile(\n\t\tconst QString &fileText,\n\t\tFn<void()> restoreText);\n\t[[nodiscard]] bool checkLargeTextPaste(\n\t\tnot_null<const QMimeData*> data,\n\t\tUi::InputField::MimeAction action);\n\tvoid updateSendAsFileVisibility();\n\tvoid updateSendAsFileGeometry();\n\tvoid setupSendMenu(\n\t\tnot_null<Ui::RpWidget*> button,\n\t\tFn<void(Api::SendOptions)> send);\n\tbool updateReplaceMediaButton();\n\tvoid updateOuterGeometry(QRect rect);\n\tvoid paintBackground(QPainter &p, QRect full, QRect clip);\n\n\t[[nodiscard]] auto computeSendButtonType() const;\n\t[[nodiscard]] SendMenu::Details sendMenuDetails() const;\n\t[[nodiscard]] SendMenu::Details saveMenuDetails() const;\n\t[[nodiscard]] SendMenu::Details sendButtonMenuDetails() const;\n\n\t[[nodiscard]] auto sendContentRequests(\n\t\tSendRequestType requestType = SendRequestType::Text) const;\n\tvoid editStarsFrom(int selected = 0);\n\n\tvoid orderControls();\n\tvoid updateFieldPlaceholder();\n\tvoid updateSilentBroadcast();\n\tvoid editMessage(not_null<HistoryItem*> item);\n\n\tvoid escape();\n\tvoid fieldChanged();\n\tvoid toggleTabbedSelectorMode();\n\tvoid createTabbedPanel();\n\tvoid setTabbedPanel(std::unique_ptr<ChatHelpers::TabbedPanel> panel);\n\tvoid showAiComposeBox();\n\t[[nodiscard]] bool canSendAiComposeDirect() const;\n\n\t[[nodiscard]] bool showRecordButton() const;\n\t[[nodiscard]] bool showEditStarsButton() const;\n\t[[nodiscard]] int shownStarsPerMessage() const;\n\tbool updateBotCommandShown();\n\tbool updateLikeShown();\n\t[[nodiscard]] bool hasEnoughLinesForAi() const;\n\t[[nodiscard]] bool textExceedsMaxSize() const;\n\n\tvoid cancelInlineBot();\n\tvoid clearInlineBot();\n\tvoid inlineBotChanged();\n\n\t[[nodiscard]] bool hasSilentBroadcastToggle() const;\n\t[[nodiscard]] bool editStarsButtonShown() const;\n\tvoid startStarsSendEffect();\n\tvoid setupStarsSendEffectsCanvas();\n\tvoid startStarsEffect(SendStarButtonEffect event);\n\tvoid setupStarsEffectsCanvas();\n\n\t// Look in the _field for the inline bot and query string.\n\tvoid updateInlineBotQuery();\n\n\t// Request to show results in the emoji panel.\n\tvoid applyInlineBotQuery(UserData *bot, const QString &query);\n\n\t[[nodiscard]] Data::DraftKey draftKey(\n\t\tDraftType type = DraftType::Normal) const;\n\t[[nodiscard]] Data::DraftKey draftKeyCurrent() const;\n\tvoid saveDraft(bool delayed = false);\n\tvoid saveDraftDelayed();\n\tvoid saveDraftWithTextNow();\n\tvoid saveCloudDraft();\n\n\tvoid writeDrafts();\n\tvoid writeDraftTexts();\n\tvoid writeDraftCursors();\n\tvoid setFieldText(\n\t\tconst TextWithTags &textWithTags,\n\t\tTextUpdateEvents events = 0,\n\t\tFieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);\n\tvoid clearFieldText(\n\t\tTextUpdateEvents events = 0,\n\t\tFieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);\n\tvoid saveFieldToHistoryLocalDraft();\n\n\tvoid unregisterDraftSources();\n\tvoid registerDraftSource();\n\tvoid changeFocusedControl();\n\n\tvoid checkCharsLimitation();\n\n\tconst style::ComposeControls &_st;\n\tChatHelpers::ComposeFeatures _features;\n\tconst not_null<Ui::RpWidget*> _parent;\n\tconst not_null<QWidget*> _panelsParent;\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst not_null<Main::Session*> _session;\n\n\tWindow::SessionController * const _regularWindow = nullptr;\n\tstd::unique_ptr<ChatHelpers::TabbedSelector> _ownedSelector;\n\tconst not_null<ChatHelpers::TabbedSelector*> _selector;\n\trpl::event_stream<ChatHelpers::FileChosen> _stickerOrEmojiChosen;\n\n\tHistory *_history = nullptr;\n\tMsgId _topicRootId = 0;\n\tPeerId _monoforumPeerId = 0;\n\tBusinessShortcutId _shortcutId = 0;\n\tFn<bool()> _showSlowmodeError;\n\tFn<Api::SendAction()> _sendActionFactory;\n\tFn<void(TextWithEntities, Api::SendOptions, Fn<void()>)> _sendWithText;\n\trpl::variable<int> _slowmodeSecondsLeft;\n\trpl::variable<bool> _sendDisabledBySlowmode;\n\trpl::variable<bool> _liked;\n\trpl::variable<Controls::WriteRestriction> _writeRestriction;\n\trpl::variable<bool> _hidden;\n\tMode _mode = Mode::Normal;\n\n\tconst std::unique_ptr<Ui::RpWidget> _wrap;\n\tstd::unique_ptr<Ui::RpWidget> _writeRestricted;\n\trpl::event_stream<FullReplyTo> _jumpToItemRequests;\n\n\tstd::optional<Ui::RoundRect> _backgroundRect;\n\n\tconst std::shared_ptr<Ui::SendButton> _send;\n\tControls::ComposeAiButton * const _aiButton = nullptr;\n\tUi::IconButton * const _sendAsFile = nullptr;\n\tUi::IconButton *_editStars = nullptr;\n\tUi::IconButton *_like = nullptr;\n\trpl::variable<int> _minStarsCount;\n\tstd::optional<int> _chosenStarsCount;\n\tUi::IconButton *_commentsShown = nullptr;\n\trpl::variable<bool> _commentsShownHidden;\n\tUi::RpWidget *_commentsShownNewDot = nullptr;\n\tUi::IconButton *_attachToggle = nullptr;\n\tUi::AbstractButton *_starsReaction = nullptr;\n\tstd::vector<std::unique_ptr<Ui::ReactionFlyAnimation>> _starSendEffects;\n\tstd::unique_ptr<Ui::RpWidget> _starSendEffectsCanvas;\n\tstd::vector<std::unique_ptr<StarEffect>> _starEffects;\n\tstd::unique_ptr<Ui::RpWidget> _starEffectsCanvas;\n\tstd::unique_ptr<Ui::IconButton> _replaceMedia;\n\tconst not_null<Ui::EmojiButton*> _tabbedSelectorToggle;\n\trpl::producer<QString> _fieldCustomPlaceholder;\n\tconst not_null<Ui::InputField*> _field;\n\tUi::IconButton * const _botCommandStart = nullptr;\n\tstd::unique_ptr<Ui::SendAsButton> _sendAs;\n\trpl::variable<bool> _videoStreamAdmin;\n\tstd::unique_ptr<Ui::SilentToggle> _silent;\n\tstd::unique_ptr<Controls::TTLButton> _ttlInfo;\n\tbase::unique_qptr<Controls::CharactersLimitLabel> _charsLimitation;\n\tbase::unique_qptr<Ui::IconButton> _scheduled;\n\n\tstd::unique_ptr<InlineBots::Layout::Widget> _inlineResults;\n\tstd::unique_ptr<ChatHelpers::TabbedPanel> _tabbedPanel;\n\tstd::unique_ptr<Ui::DropdownMenu> _attachBotsMenu;\n\tstd::unique_ptr<ChatHelpers::FieldAutocomplete> _autocomplete;\n\tstd::unique_ptr<Ui::Emoji::SuggestionsController> _emojiSuggestions;\n\n\tfriend class FieldHeader;\n\tconst std::unique_ptr<FieldHeader> _header;\n\tconst std::unique_ptr<Controls::VoiceRecordBar> _voiceRecordBar;\n\tstd::unique_ptr<Controls::AiTooltipManager> _aiTooltipManager;\n\tstd::unique_ptr<Controls::AiTooltipManager> _sendAsFileTooltipManager;\n\tstd::shared_ptr<Ui::ChatStyle> _chatStyle;\n\n\tconst Fn<SendMenu::Details()> _sendMenuDetails;\n\tconst Fn<void(not_null<DocumentData*>)> _unavailableEmojiPasted;\n\n\trpl::event_stream<Api::SendOptions> _sendCustomRequests;\n\trpl::event_stream<> _cancelRequests;\n\trpl::event_stream<FileChosen> _fileChosen;\n\trpl::event_stream<PhotoChosen> _photoChosen;\n\trpl::event_stream<InlineChosen> _inlineResultChosen;\n\trpl::event_stream<SendActionUpdate> _sendActionUpdates;\n\trpl::event_stream<QString> _sendCommandRequests;\n\trpl::event_stream<not_null<QKeyEvent*>> _scrollKeyEvents;\n\trpl::event_stream<not_null<QKeyEvent*>> _editLastMessageRequests;\n\trpl::event_stream<std::optional<bool>> _attachRequests;\n\tFn<void(std::shared_ptr<Ui::PreparedBundle>, Api::SendOptions)> _sendAsFileConfirmed;\n\trpl::event_stream<> _likeToggled;\n\trpl::event_stream<ReplyNextRequest> _replyNextRequests;\n\trpl::event_stream<> _focusRequests;\n\trpl::event_stream<> _showScheduledRequests;\n\trpl::event_stream<> _commentsShownToggles;\n\trpl::event_stream<StarReactionIncrement> _starsReactionIncrements;\n\trpl::variable<std::vector<StarReactionTop>> _starsReactionTop;\n\trpl::variable<bool> _recording;\n\trpl::variable<bool> _hasSendText;\n\n\tTextUpdateEvents _textUpdateEvents = TextUpdateEvents()\n\t\t| TextUpdateEvent::SaveDraft\n\t\t| TextUpdateEvent::SendTyping;\n\tDialogs::EntryState _currentDialogsEntryState;\n\n\tcrl::time _saveDraftStart = 0;\n\tbool _saveDraftText = false;\n\tbase::Timer _saveDraftTimer;\n\tbase::Timer _saveCloudDraftTimer;\n\n\tUserData *_inlineBot = nullptr;\n\tQString _inlineBotUsername;\n\tbool _inlineLookingUpBot = false;\n\tmtpRequestId _inlineBotResolveRequestId = 0;\n\tbool _isInlineBot = false;\n\tbool _botCommandShown = false;\n\tbool _likeShown = false;\n\tWebrtc::RecordAvailability _recordAvailability = {};\n\n\tFullMsgId _editingId;\n\tstd::shared_ptr<Data::PhotoMedia> _photoEditMedia;\n\tbool _canReplaceMedia = false;\n\tbool _canAddMedia = false;\n\n\tstd::unique_ptr<Controls::WebpageProcessor> _preview;\n\n\trpl::lifetime _historyLifetime;\n\trpl::lifetime _uploaderSubscriptions;\n\n};\n\n[[nodiscard]] rpl::producer<int> SlowmodeSecondsLeft(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<bool> SendDisabledBySlowmode(\n\tnot_null<PeerData*> peer);\n\nvoid ShowPhotoEditSpoilerMenu(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<HistoryItem*> item,\n\tconst std::optional<bool> &override,\n\tFn<void(bool)> callback);\n\n[[nodiscard]] Image *MediaPreviewWithOverriddenSpoiler(\n\tnot_null<HistoryItem*> item,\n\tbool spoiler);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/controls/history_view_compose_media_edit_manager.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"menu/menu_send.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace HistoryView {\n\nMediaEditManager::MediaEditManager() = default;\n\nvoid MediaEditManager::start(\n\t\tnot_null<HistoryItem*> item,\n\t\tstd::optional<bool> spoilered,\n\t\tstd::optional<bool> invertCaption) {\n\tconst auto media = item->media();\n\tif (!media) {\n\t\tcancel();\n\t\treturn;\n\t}\n\t_item = item;\n\t_spoilered = spoilered.value_or(media->hasSpoiler());\n\t_invertCaption = invertCaption.value_or(item->invertMedia());\n\t_lifetime = item->history()->owner().itemRemoved(\n\t) | rpl::on_next([=](not_null<const HistoryItem*> removed) {\n\t\tif (removed == _item) {\n\t\t\tcancel();\n\t\t}\n\t});\n}\n\nvoid MediaEditManager::apply(SendMenu::Action action) {\n\tusing Type = SendMenu::Action::Type;\n\tif (action.type == Type::CaptionUp) {\n\t\t_invertCaption = true;\n\t} else if (action.type == Type::CaptionDown) {\n\t\t_invertCaption = false;\n\t} else if (action.type == Type::SpoilerOn) {\n\t\t_spoilered = true;\n\t} else if (action.type == Type::SpoilerOff) {\n\t\t_spoilered = false;\n\t}\n\t_updateRequests.fire({});\n}\n\nvoid MediaEditManager::cancel() {\n\t_menu = nullptr;\n\t_item = nullptr;\n\t_spoilered = false;\n\t_invertCaption = false;\n\t_lifetime.destroy();\n}\n\nvoid MediaEditManager::showMenu(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tFn<void()> finished,\n\t\tbool hasCaptionText) {\n\tif (!_item) {\n\t\treturn;\n\t}\n\tconst auto media = _item->media();\n\tconst auto hasPreview = media && media->hasReplyPreview();\n\tconst auto preview = hasPreview ? media->replyPreview() : nullptr;\n\tif (!preview || (media && media->webpage())) {\n\t\treturn;\n\t}\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\tconst auto callback = [=](SendMenu::Action action, const auto &) {\n\t\tapply(action);\n\t};\n\tconst auto position = QCursor::pos();\n\tSendMenu::FillSendMenu(\n\t\t_menu.get(),\n\t\tnullptr,\n\t\tsendMenuDetails(hasCaptionText),\n\t\tcallback,\n\t\t&st::defaultComposeIcons,\n\t\tposition);\n\t_menu->popup(position);\n}\n\nImage *MediaEditManager::mediaPreview() {\n\tif (const auto media = _item ? _item->media() : nullptr) {\n\t\tif (const auto photo = media->photo()) {\n\t\t\treturn photo->getReplyPreview(\n\t\t\t\t_item->fullId(),\n\t\t\t\t_item->history()->peer,\n\t\t\t\t_spoilered);\n\t\t} else if (const auto document = media->document()) {\n\t\t\treturn document->getReplyPreview(\n\t\t\t\t_item->fullId(),\n\t\t\t\t_item->history()->peer,\n\t\t\t\t_spoilered);\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nbool MediaEditManager::spoilered() const {\n\treturn _spoilered;\n}\n\nbool MediaEditManager::invertCaption() const {\n\treturn _invertCaption;\n}\n\nSendMenu::Details MediaEditManager::sendMenuDetails(\n\t\tbool hasCaptionText) const {\n\tconst auto media = _item ? _item->media() : nullptr;\n\tif (!media) {\n\t\treturn {};\n\t}\n\tconst auto editingMedia = media->allowsEditMedia();\n\tconst auto editPhoto = editingMedia ? media->photo() : nullptr;\n\tconst auto editDocument = editingMedia ? media->document() : nullptr;\n\tconst auto canSaveSpoiler = CanBeSpoilered(_item);\n\tconst auto canMoveCaption = media->allowsEditCaption()\n\t\t&& hasCaptionText\n\t\t&& (editPhoto\n\t\t\t|| (editDocument\n\t\t\t\t&& (editDocument->isVideoFile() || editDocument->isGifv())));\n\treturn {\n\t\t.spoiler = (!canSaveSpoiler\n\t\t\t? SendMenu::SpoilerState::None\n\t\t\t: _spoilered\n\t\t\t? SendMenu::SpoilerState::Enabled\n\t\t\t: SendMenu::SpoilerState::Possible),\n\t\t.caption = (!canMoveCaption\n\t\t\t? SendMenu::CaptionState::None\n\t\t\t: _invertCaption\n\t\t\t? SendMenu::CaptionState::Above\n\t\t\t: SendMenu::CaptionState::Below),\n\t};\n}\n\nrpl::producer<> MediaEditManager::updateRequests() const {\n\treturn _updateRequests.events();\n}\n\nbool MediaEditManager::CanBeSpoilered(not_null<HistoryItem*> item) {\n\tconst auto media = item ? item->media() : nullptr;\n\tconst auto editingMedia = media && media->allowsEditMedia();\n\tconst auto editPhoto = editingMedia ? media->photo() : nullptr;\n\tconst auto editDocument = editingMedia ? media->document() : nullptr;\n\treturn (editPhoto && !editPhoto->isNull())\n\t\t|| (editDocument\n\t\t\t&& (editDocument->isVideoFile() || editDocument->isGifv()));\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n\nnamespace SendMenu {\nstruct Details;\nstruct Action;\n} // namespace SendMenu\n\nnamespace Ui {\nclass RpWidget;\nclass PopupMenu;\n} // namespace Ui\n\nclass Image;\nclass HistoryItem;\n\nnamespace HistoryView {\n\nclass MediaEditManager final {\npublic:\n\tMediaEditManager();\n\n\tvoid start(\n\t\tnot_null<HistoryItem*> item,\n\t\tstd::optional<bool> spoilered = {},\n\t\tstd::optional<bool> invertCaption = {});\n\tvoid apply(SendMenu::Action action);\n\tvoid cancel();\n\n\tvoid showMenu(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tFn<void()> finished,\n\t\tbool hasCaptionText);\n\n\t[[nodiscard]] Image *mediaPreview();\n\n\t[[nodiscard]] bool spoilered() const;\n\t[[nodiscard]] bool invertCaption() const;\n\n\t[[nodiscard]] SendMenu::Details sendMenuDetails(\n\t\tbool hasCaptionText) const;\n\n\t[[nodiscard]] rpl::producer<> updateRequests() const;\n\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn _item != nullptr;\n\t}\n\n\t[[nodiscard]] static bool CanBeSpoilered(not_null<HistoryItem*> item);\n\nprivate:\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\tHistoryItem *_item = nullptr;\n\tbool _spoilered = false;\n\tbool _invertCaption = false;\n\n\trpl::event_stream<> _updateRequests;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/controls/history_view_compose_search.h\"\n\n#include \"api/api_messages_search_merged.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"dialogs/dialogs_search_from_controllers.h\" // SearchFromBox\n#include \"dialogs/dialogs_search_tags.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"history/view/history_view_context_menu.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/effects/show_animation.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/multi_select.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_info.h\"\n\nnamespace HistoryView {\nnamespace {\n\nusing Activation = ComposeSearch::Activation;\nusing SearchRequest = Api::MessagesSearchMerged::Request;\n\n[[nodiscard]] inline bool HasChooseFrom(not_null<History*> history) {\n\tif (const auto peer = history->peer) {\n\t\treturn (peer->isChat() || peer->isMegagroup());\n\t}\n\treturn false;\n}\n\nclass Row final : public PeerListRow {\npublic:\n\texplicit Row(\n\t\tstd::unique_ptr<Dialogs::FakeRow> fakeRow,\n\t\tnot_null<QString*> query);\n\n\t[[nodiscard]] FullMsgId fullId() const;\n\n\tQRect elementGeometry(int element, int outerWidth) const override;\n\tvoid elementAddRipple(\n\t\tint element,\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) override;\n\tvoid elementsStopLastRipple() override;\n\tvoid elementsPaint(\n\t\tPainter &p,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tint selectedElement) override;\n\nprivate:\n\tconst std::unique_ptr<Dialogs::FakeRow> _fakeRow;\n\n\tnot_null<QString*> _query;\n\tint _outerWidth = 0;\n\n};\n\nRow::Row(std::unique_ptr<Dialogs::FakeRow> fakeRow, not_null<QString*> query)\n: PeerListRow(\n\tfakeRow->searchInChat().history()->peer,\n\tfakeRow->item()->fullId().msg.bare)\n, _fakeRow(std::move(fakeRow))\n, _query(query) {\n}\n\nFullMsgId Row::fullId() const {\n\treturn _fakeRow->item()->fullId();\n}\n\nQRect Row::elementGeometry(int element, int outerWidth) const {\n\treturn QRect(0, 0, outerWidth, st::dialogsRowHeight);\n}\n\nvoid Row::elementAddRipple(\n\t\tint element,\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) {\n\t_fakeRow->addRipple(\n\t\tpoint,\n\t\t{ _outerWidth, st::dialogsRowHeight },\n\t\tstd::move(updateCallback));\n}\n\nvoid Row::elementsStopLastRipple() {\n\t_fakeRow->stopLastRipple();\n}\n\nvoid Row::elementsPaint(\n\t\tPainter &p,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tint selectedElement) {\n\t_outerWidth = outerWidth;\n\tDialogs::Ui::RowPainter::Paint(p, _fakeRow.get(), {\n\t\t.st = &st::defaultDialogRow,\n\t\t.currentBg = st::dialogsBg,\n\t\t.now = crl::now(),\n\t\t.searchLowerText = QStringView(*_query),\n\t\t.width = outerWidth,\n\t\t.selected = selected,\n\t\t.paused = p.inactive(),\n\t\t.search = true,\n\t});\n}\n\nclass ListController final : public PeerListController {\npublic:\n\texplicit ListController(not_null<History*> history);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowElementClicked(not_null<PeerListRow*> row, int element) override;\n\n\tvoid loadMoreRows() override;\n\n\tvoid addItems(const MessageIdsList &ids, bool clear);\n\tvoid setQuery(const QString &query);\n\n\t[[nodiscard]] rpl::producer<FullMsgId> showItemRequests() const;\n\t[[nodiscard]] rpl::producer<> searchMoreRequests() const;\n\t[[nodiscard]] rpl::producer<> resetScrollRequests() const;\n\nprivate:\n\tconst not_null<History*> _history;\n\trpl::event_stream<FullMsgId> _showItemRequests;\n\trpl::event_stream<> _searchMoreRequests;\n\trpl::event_stream<> _resetScrollRequests;\n\n\tQString _query;\n\n};\n\nListController::ListController(not_null<History*> history)\n: _history(history) {\n}\n\nMain::Session &ListController::session() const {\n\treturn _history->session();\n}\n\nvoid ListController::prepare() {\n}\n\nvoid ListController::rowClicked(not_null<PeerListRow*> row) {\n\t_showItemRequests.fire_copy(static_cast<Row*>(row.get())->fullId());\n}\n\nvoid ListController::rowElementClicked(\n\t\tnot_null<PeerListRow*> row,\n\t\tint element) {\n\tListController::rowClicked(row);\n}\n\nvoid ListController::loadMoreRows() {\n\t_searchMoreRequests.fire({});\n}\n\nrpl::producer<FullMsgId> ListController::showItemRequests() const {\n\treturn _showItemRequests.events();\n}\n\nrpl::producer<> ListController::searchMoreRequests() const {\n\treturn _searchMoreRequests.events();\n}\n\nrpl::producer<> ListController::resetScrollRequests() const {\n\treturn _resetScrollRequests.events();\n}\n\nvoid ListController::addItems(const MessageIdsList &ids, bool clear) {\n\tif (clear) {\n\t\t_resetScrollRequests.fire({});\n\t\tfor (auto i = 0; i != delegate()->peerListFullRowsCount();) {\n\t\t\tdelegate()->peerListRemoveRow(delegate()->peerListRowAt(i));\n\t\t}\n\t}\n\n\tconst auto &owner = _history->owner();\n\tconst auto key = Dialogs::Key{ _history };\n\tfor (const auto &id : ids) {\n\t\tif (const auto item = owner.message(id)) {\n\t\t\tconst auto shared = std::make_shared<Row*>(nullptr);\n\t\t\tauto row = std::make_unique<Row>(\n\t\t\t\tstd::make_unique<Dialogs::FakeRow>(\n\t\t\t\t\tkey,\n\t\t\t\t\titem,\n\t\t\t\t\t[=] { delegate()->peerListUpdateRow(*shared); }),\n\t\t\t\t&_query);\n\t\t\t*shared = row.get();\n\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t}\n\t}\n\n\tdelegate()->peerListRefreshRows();\n\n\tif (!delegate()->peerListFullRowsCount()) {\n\t\t_showItemRequests.fire({});\n\t}\n}\n\nvoid ListController::setQuery(const QString &query) {\n\t_query = query;\n}\n\nstruct List {\n\tbase::unique_qptr<Ui::RpWidget> container;\n\tstd::unique_ptr<ListController> controller;\n};\n\nList CreateList(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<History*> history,\n\t\trpl::producer<not_null<QKeyEvent*>> scrollKeys) {\n\tauto list = List{\n\t\tbase::make_unique_q<Ui::RpWidget>(parent),\n\t\tstd::make_unique<ListController>(history),\n\t};\n\tconst auto scroll = Ui::CreateChild<Ui::ScrollArea>(list.container.get());\n\n\tusing Delegate = PeerListContentDelegateSimple;\n\tconst auto delegate = scroll->lifetime().make_state<Delegate>();\n\tlist.controller->setStyleOverrides(&st::searchInChatPeerList);\n\n\tconst auto content = scroll->setOwnedWidget(\n\t\tobject_ptr<PeerListContent>(scroll, list.controller.get()));\n\n\tlist.controller->resetScrollRequests(\n\t) | rpl::on_next([=] {\n\t\tscroll->scrollToY(0);\n\t}, scroll->lifetime());\n\n\tscroll->scrolls(\n\t) | rpl::on_next([=] {\n\t\tconst auto top = scroll->scrollTop();\n\t\tcontent->setVisibleTopBottom(top, top + scroll->height());\n\t}, scroll->lifetime());\n\n\tdelegate->setContent(content);\n\tlist.controller->setDelegate(delegate);\n\n\tlist.container->sizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\tcontent->resize(size.width(), content->height());\n\t\tscroll->resize(size);\n\t}, list.container->lifetime());\n\n\tlist.container->paintRequest(\n\t) | rpl::on_next([weak = base::make_weak(list.container.get())](\n\t\t\tconst QRect &r) {\n\t\tauto p = QPainter(weak.get());\n\t\tp.fillRect(r, st::dialogsBg);\n\t}, list.container->lifetime());\n\n\tstd::move(\n\t\tscrollKeys\n\t) | rpl::on_next([=](not_null<QKeyEvent*> e) {\n\t\tconst auto delta = scroll->height();\n\t\tconst auto now = scroll->scrollTop();\n\t\tscroll->scrollToY((e->key() == Qt::Key_PageUp)\n\t\t\t? (now - delta)\n\t\t\t: (now + delta));\n\t}, scroll->lifetime());\n\n\treturn list;\n}\n\nclass TopBar final : public Ui::RpWidget {\npublic:\n\tTopBar(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<History*> history,\n\t\tPeerData *from,\n\t\tconst QString &query);\n\n\tvoid setInnerFocus();\n\tvoid setQuery(const QString &query);\n\n\t[[nodiscard]] rpl::producer<SearchRequest> searchRequests() const;\n\t[[nodiscard]] rpl::producer<PeerData*> fromValue() const;\n\t[[nodiscard]] rpl::producer<> queryChanges() const;\n\t[[nodiscard]] rpl::producer<> closeRequests() const;\n\t[[nodiscard]] rpl::producer<> cancelRequests() const;\n\t[[nodiscard]] rpl::producer<not_null<QKeyEvent*>> keyEvents() const;\n\n\tvoid setFrom(PeerData *peer);\n\tbool handleKeyPress(not_null<QKeyEvent*> e);\n\nprotected:\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tvoid clearItems();\n\tvoid refreshTags();\n\tvoid updateSize();\n\tvoid requestSearch(bool cache = true);\n\tvoid requestSearchDelayed();\n\n\tbase::unique_qptr<Ui::IconButton> _cancel;\n\tstd::vector<Data::ReactionId> _searchTagsSelected;\n\tbase::unique_qptr<Ui::MultiSelect> _select;\n\tstd::unique_ptr<Dialogs::SearchTags> _searchTags;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\tstd::optional<QPoint> _mouseGlobalPosition;\n\n\tconst not_null<Window::SessionController*> _window;\n\tconst not_null<History*> _history;\n\trpl::variable<PeerData*> _from = nullptr;\n\n\tbase::Timer _searchTimer;\n\n\tApi::MessagesSearchMerged::CachedRequests _typedRequests;\n\n\trpl::event_stream<SearchRequest> _searchRequests;\n\trpl::event_stream<> _queryChanges;\n\trpl::event_stream<> _cancelRequests;\n\trpl::event_stream<not_null<QKeyEvent*>> _keyEvents;\n};\n\nTopBar::TopBar(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Window::SessionController*> window,\n\tnot_null<History*> history,\n\tPeerData *from,\n\tconst QString &query)\n: Ui::RpWidget(parent)\n, _cancel(base::make_unique_q<Ui::IconButton>(this, st::historyTopBarBack))\n, _searchTagsSelected(Data::SearchTagsFromQuery(query))\n, _select(base::make_unique_q<Ui::MultiSelect>(\n\tthis,\n\tst::searchInChatMultiSelect,\n\ttr::lng_dlg_filter(),\n\t_searchTagsSelected.empty() ? query : QString()))\n, _window(window)\n, _history(history)\n, _searchTimer([=] { requestSearch(); }) {\n\tif (from) {\n\t\tsetFrom(from);\n\t}\n\trefreshTags();\n\n\tmoveToLeft(0, 0);\n\n\tparent->geometryValue(\n\t) | rpl::on_next([=] {\n\t\tupdateSize();\n\t}, lifetime());\n\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tconst auto height = st::topBarHeight;\n\t\t_cancel->moveToLeft(0, (height - _cancel->height()) / 2);\n\n\t\tconst auto selectLeft = _cancel->x() + _cancel->width();\n\t\t_select->resizeToWidth(s.width() - selectLeft);\n\t\t_select->moveToLeft(selectLeft, (height - _select->height()) / 2);\n\n\t}, lifetime());\n\n\tpaintRequest(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tauto p = QPainter(this);\n\t\tp.fillRect(r, st::dialogsBg);\n\t}, lifetime());\n\n\t_select->setQueryChangedCallback([=](const QString &) {\n\t\trequestSearchDelayed();\n\t\t_queryChanges.fire({});\n\t});\n\n\t_select->setSubmittedCallback([=](Qt::KeyboardModifiers) {\n\t\trequestSearch();\n\t});\n\n\t_select->setCancelledCallback([=] {\n\t\t_cancelRequests.fire({});\n\t});\n}\n\nvoid TopBar::keyPressEvent(QKeyEvent *e) {\n\t_keyEvents.fire_copy(e);\n}\n\nbool TopBar::handleKeyPress(not_null<QKeyEvent*> e) {\n\treturn false;\n}\n\nrpl::producer<not_null<QKeyEvent*>> TopBar::keyEvents() const {\n\treturn _keyEvents.events();\n}\n\nvoid TopBar::setInnerFocus() {\n\tif (Ui::AppInFocus() && Ui::InFocusChain(_select->window())) {\n\t\t_select->setInnerFocus();\n\t}\n}\n\nvoid TopBar::updateSize() {\n\tconst auto height = st::topBarHeight\n\t\t+ (_searchTags ? _searchTags->height() : 0);\n\tresize(parentWidget()->width(), height);\n}\n\nvoid TopBar::setQuery(const QString &query) {\n\tif (auto tags = Data::SearchTagsFromQuery(query); !tags.empty()) {\n\t\tif (_searchTagsSelected != tags) {\n\t\t\t_searchTagsSelected = std::move(tags);\n\t\t\trefreshTags();\n\t\t}\n\t\t_select->setQuery(QString());\n\t} else {\n\t\t_select->setQuery(query);\n\t}\n}\n\nvoid TopBar::clearItems() {\n\t_select->setItemRemovedCallback(nullptr);\n\n\tfor (const auto &id : _select->getItems()) {\n\t\t_select->removeItem(id);\n\t}\n\n\t_select->setItemRemovedCallback([=](uint64) {\n\t\t_from = nullptr;\n\t\trequestSearchDelayed();\n\t});\n}\n\nvoid TopBar::refreshTags() {\n\tif (!_history->peer->isSelf()) {\n\t\t_searchTags = nullptr;\n\t\treturn;\n\t}\n\tauto fullTagsList = _from.value() | rpl::map([=](PeerData *from) {\n\t\tconst auto sublist = from\n\t\t\t? _history->owner().savedMessages().sublist(from).get()\n\t\t\t: nullptr;\n\t\treturn _history->owner().reactions().myTagsValue(sublist);\n\t}) | rpl::flatten_latest();\n\t_searchTags = std::make_unique<Dialogs::SearchTags>(\n\t\t&_history->owner(),\n\t\tstd::move(fullTagsList),\n\t\t_searchTagsSelected);\n\n\tconst auto parent = _searchTags->lifetime().make_state<Ui::RpWidget>(\n\t\tthis);\n\tconst auto shadow = _searchTags->lifetime().make_state<Ui::PlainShadow>(\n\t\tparentWidget());\n\tparent->show();\n\n\t_searchTags->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tupdateSize();\n\t\tshadow->setVisible(height > 0);\n\t}, _searchTags->lifetime());\n\n\tgeometryValue() | rpl::on_next([=](QRect geometry) {\n\t\tshadow->setGeometry(\n\t\t\tgeometry.x(),\n\t\t\tgeometry.y() + geometry.height(),\n\t\t\tgeometry.width(),\n\t\t\tst::lineWidth);\n\t}, shadow->lifetime());\n\n\t_searchTags->selectedChanges(\n\t) | rpl::on_next([=](std::vector<Data::ReactionId> &&list) {\n\t\t_searchTagsSelected = std::move(list);\n\t\trequestSearch(false);\n\t}, _searchTags->lifetime());\n\n\t_searchTags->menuRequests(\n\t) | rpl::on_next([=](Data::ReactionId id) {\n\t\tShowTagInListMenu(\n\t\t\t&_menu,\n\t\t\t_mouseGlobalPosition.value_or(QCursor::pos()),\n\t\t\tthis,\n\t\t\tid,\n\t\t\t_window);\n\t}, _searchTags->lifetime());\n\n\tif (!_searchTagsSelected.empty()) {\n\t\tcrl::on_main(this, [=] {\n\t\t\trequestSearch(false);\n\t\t});\n\t}\n\n\tconst auto padding = st::searchInChatTagsPadding;\n\tconst auto position = QPoint(padding.left(), padding.top());\n\n\t_searchTags->repaintRequests() | rpl::on_next([=] {\n\t\tparent->update();\n\t}, _searchTags->lifetime());\n\n\twidthValue() | rpl::on_next([=](int width) {\n\t\twidth -= padding.left() + padding.right();\n\t\t_searchTags->resizeToWidth(width);\n\t}, _searchTags->lifetime());\n\n\trpl::combine(\n\t\twidthValue(),\n\t\t_searchTags->heightValue()\n\t) | rpl::on_next([=](int width, int height) {\n\t\theight += padding.top() + padding.bottom();\n\t\tparent->setGeometry(0, st::topBarHeight, width, height);\n\t}, _searchTags->lifetime());\n\n\tparent->paintRequest() | rpl::on_next([=](const QRect &r) {\n\t\tauto p = Painter(parent);\n\t\tp.fillRect(r, st::dialogsBg);\n\t\t_searchTags->paint(p, position, crl::now(), false);\n\t}, parent->lifetime());\n\n\tparent->setMouseTracking(true);\n\tparent->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::MouseMove) {\n\t\t\tconst auto mouse = static_cast<QMouseEvent*>(e.get());\n\t\t\t_mouseGlobalPosition = mouse->globalPos();\n\t\t\tconst auto point = mouse->pos() - position;\n\t\t\tconst auto handler = _searchTags->lookupHandler(point);\n\t\t\tClickHandler::setActive(handler);\n\t\t\tparent->setCursor(handler\n\t\t\t\t? style::cur_pointer\n\t\t\t\t: style::cur_default);\n\t\t} else if (e->type() == QEvent::MouseButtonPress) {\n\t\t\tconst auto mouse = static_cast<QMouseEvent*>(e.get());\n\t\t\tif (mouse->button() == Qt::LeftButton) {\n\t\t\t\tClickHandler::pressed();\n\t\t\t}\n\t\t} else if (e->type() == QEvent::MouseButtonRelease) {\n\t\t\tconst auto mouse = static_cast<QMouseEvent*>(e.get());\n\t\t\tif (mouse->button() == Qt::LeftButton) {\n\t\t\t\tconst auto handler = ClickHandler::unpressed();\n\t\t\t\tActivateClickHandler(parent, handler, ClickContext{\n\t\t\t\t\t.button = mouse->button(),\n\t\t\t\t\t.other = QVariant::fromValue(ClickHandlerContext{\n\t\t\t\t\t\t.sessionWindow = _window,\n\t\t\t\t\t}),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}, parent->lifetime());\n}\n\nvoid TopBar::requestSearch(bool cache) {\n\tconst auto search = SearchRequest{\n\t\t_select->getQuery(),\n\t\t_from.current(),\n\t\t_searchTagsSelected\n\t};\n\tif (cache) {\n\t\t_typedRequests.insert(search);\n\t}\n\t_searchRequests.fire_copy(search);\n}\n\nvoid TopBar::requestSearchDelayed() {\n\t// Check cached queries.\n\tconst auto search = SearchRequest{\n\t\t_select->getQuery(),\n\t\t_from.current(),\n\t\t_searchTagsSelected\n\t};\n\tif (_typedRequests.contains(search)) {\n\t\trequestSearch(false);\n\t\treturn;\n\t}\n\n\t_searchTimer.callOnce(AutoSearchTimeout);\n}\n\nrpl::producer<SearchRequest> TopBar::searchRequests() const {\n\treturn _searchRequests.events();\n}\n\nrpl::producer<> TopBar::queryChanges() const {\n\treturn _queryChanges.events();\n}\n\nrpl::producer<> TopBar::closeRequests() const {\n\treturn _cancel->clicks() | rpl::to_empty;\n}\n\nrpl::producer<> TopBar::cancelRequests() const {\n\treturn _cancelRequests.events();\n}\n\nrpl::producer<PeerData*> TopBar::fromValue() const {\n\treturn _from.value();\n}\n\nvoid TopBar::setFrom(PeerData *peer) {\n\tclearItems();\n\n\tconst auto guard = gsl::finally([&] {\n\t\t_from = peer;\n\t\trequestSearchDelayed();\n\t});\n\tif (!peer || _history->peer->isSelf()) {\n\t\treturn;\n\t}\n\n\t_select->addItem(\n\t\tpeer->id.value,\n\t\ttr::lng_dlg_search_from(tr::now, lt_user, peer->shortName()),\n\t\tst::activeButtonBg,\n\t\tPaintUserpicCallback(peer, false),\n\t\tUi::MultiSelect::AddItemWay::Default);\n}\n\nclass BottomBar final : public Ui::RpWidget {\npublic:\n\tusing Index = int;\n\tBottomBar(not_null<Ui::RpWidget*> parent, bool fastShowChooseFrom);\n\n\tvoid setTotal(int total);\n\tvoid setCurrent(int current);\n\n\t[[nodiscard]] rpl::producer<Index> showItemRequests() const;\n\t[[nodiscard]] rpl::producer<> showCalendarRequests() const;\n\t[[nodiscard]] rpl::producer<> showBoxFromRequests() const;\n\t[[nodiscard]] rpl::producer<> showListRequests() const;\n\n\tvoid buttonFromToggleOn(rpl::producer<bool> &&visible);\n\tvoid buttonCalendarToggleOn(rpl::producer<bool> &&visible);\n\n\tbool handleKeyPress(not_null<QKeyEvent*> e);\n\nprivate:\n\tvoid updateText(int current);\n\n\tbase::unique_qptr<Ui::FlatButton> _showList;\n\n\tstruct Navigation {\n\t\tbase::unique_qptr<Ui::IconButton> button;\n\t\tbool enabled = false;\n\n\t\tUi::IconButton *operator->() const {\n\t\t\treturn button.get();\n\t\t}\n\t};\n\n\tNavigation _previous;\n\tNavigation _next;\n\n\tbase::unique_qptr<Ui::IconButton> _jumpToDate;\n\tbase::unique_qptr<Ui::IconButton> _chooseFromUser;\n\tbase::unique_qptr<Ui::FlatLabel> _counter;\n\n\tint _total = -1;\n\trpl::variable<int> _current = 0;\n};\n\nBottomBar::BottomBar(not_null<Ui::RpWidget*> parent, bool fastShowChooseFrom)\n: Ui::RpWidget(parent)\n, _showList(base::make_unique_q<Ui::FlatButton>(\n\tthis,\n\tQString(),\n\tst::historyComposeButton))\n// Icons are swaped.\n, _previous({ base::make_unique_q<Ui::IconButton>(this, st::calendarNext) })\n, _next({ base::make_unique_q<Ui::IconButton>(this, st::calendarPrevious) })\n, _jumpToDate(base::make_unique_q<Ui::IconButton>(this, st::dialogCalendar))\n, _chooseFromUser(\n\tbase::make_unique_q<Ui::IconButton>(this, st::dialogSearchFrom))\n, _counter(base::make_unique_q<Ui::FlatLabel>(\n\tthis,\n\tst::defaultSettingsRightLabel)) {\n\n\t_counter->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_chooseFromUser->setVisible(fastShowChooseFrom);\n\n\tparent->geometryValue(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tconst auto height = st::historyComposeButton.height;\n\t\tresize(r.width(), height);\n\t\tmoveToLeft(0, r.height() - height);\n\t}, lifetime());\n\n\tauto mapSize = rpl::map([=] { return size(); });\n\trpl::merge(\n\t\t_jumpToDate->shownValue() | mapSize,\n\t\t_chooseFromUser->shownValue() | mapSize,\n\t\t_counter->sizeValue() | mapSize,\n\t\tsizeValue()\n\t) | rpl::on_next([=](const QSize &s) {\n\t\t_showList->setGeometry(QRect(QPoint(), s));\n\t\t_previous->moveToRight(0, (s.height() - _previous->height()) / 2);\n\t\t_next->moveToRight(\n\t\t\t_previous->width(),\n\t\t\t(s.height() - _next->height()) / 2);\n\n\t\tauto left = st::topBarActionSkip;\n\t\tconst auto list = std::vector<not_null<Ui::RpWidget*>>{\n\t\t\t_jumpToDate.get(),\n\t\t\t_chooseFromUser.get(),\n\t\t\t_counter.get() };\n\t\tfor (const auto &w : list) {\n\t\t\tif (w->isHidden()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tw->moveToLeft(left, (s.height() - w->height()) / 2);\n\t\t\tleft += w->width();\n\t\t}\n\t}, lifetime());\n\n\tpaintRequest(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tauto p = QPainter(this);\n\t\tp.fillRect(r, st::dialogsBg);\n\t}, lifetime());\n\n\t_current.value(\n\t) | rpl::on_next([=](int current) {\n\t\tconst auto nextDisabled = (current <= 0) || (current >= _total);\n\t\tconst auto prevDisabled = (current <= 1);\n\t\t_next.enabled = !nextDisabled;\n\t\t_previous.enabled = !prevDisabled;\n\t\t_next->setAttribute(Qt::WA_TransparentForMouseEvents, nextDisabled);\n\t\t_previous->setAttribute(\n\t\t\tQt::WA_TransparentForMouseEvents,\n\t\t\tprevDisabled);\n\t\t_next->setIconOverride(nextDisabled\n\t\t\t? &st::calendarPreviousDisabled\n\t\t\t: nullptr);\n\t\t_previous->setIconOverride(prevDisabled\n\t\t\t? &st::calendarNextDisabled\n\t\t\t: nullptr);\n\n\t\t_showList->setAttribute(\n\t\t\tQt::WA_TransparentForMouseEvents,\n\t\t\tnextDisabled && prevDisabled);\n\t\tupdateText(current);\n\t}, lifetime());\n\n\trpl::merge(\n\t\t_next->clicks() | rpl::map_to(1),\n\t\t_previous->clicks() | rpl::map_to(-1)\n\t) | rpl::on_next([=](int way) {\n\t\t_current = _current.current() + way;\n\t}, lifetime());\n}\n\nbool BottomBar::handleKeyPress(not_null<QKeyEvent*> e) {\n\tif (e->key() == Qt::Key_F3) {\n\t\tconst auto modifiers = e->modifiers();\n\t\tif (modifiers == Qt::NoModifier && _next.enabled) {\n\t\t\t_next->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);\n\t\t\treturn true;\n\t\t} else if (modifiers == Qt::ShiftModifier && _previous.enabled) {\n\t\t\t_previous->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);\n\t\t\treturn true;\n\t\t}\n\t}\n#ifdef Q_OS_MAC\n\tif (e->key() == Qt::Key_G) {\n\t\tconst auto modifiers = e->modifiers();\n\t\tif (modifiers.testFlag(Qt::ControlModifier)) {\n\t\t\tconst auto &navigation = (modifiers.testFlag(Qt::ShiftModifier)\n\t\t\t\t? _previous\n\t\t\t\t: _next);\n\t\t\tif (navigation.enabled) {\n\t\t\t\tnavigation->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n#endif\n\treturn false;\n}\n\nvoid BottomBar::setTotal(int total) {\n\t_total = total;\n\tsetCurrent(1);\n}\n\nvoid BottomBar::setCurrent(int current) {\n\t_current.force_assign(current);\n}\n\nvoid BottomBar::updateText(int current) {\n\tif (_total < 0) {\n\t\t_counter->setText(QString());\n\t} else if (_total) {\n\t\t_counter->setText(tr::lng_search_messages_n_of_amount(\n\t\t\ttr::now,\n\t\t\tlt_n,\n\t\t\tQString::number(current),\n\t\t\tlt_amount,\n\t\t\tQString::number(_total)));\n\t} else {\n\t\t_counter->setText(tr::lng_search_messages_none(tr::now));\n\t}\n}\n\nrpl::producer<BottomBar::Index> BottomBar::showItemRequests() const {\n\treturn _current.changes() | rpl::map(rpl::mappers::_1 - 1);\n}\n\nrpl::producer<> BottomBar::showCalendarRequests() const {\n\treturn _jumpToDate->clicks() | rpl::to_empty;\n}\n\nrpl::producer<> BottomBar::showBoxFromRequests() const {\n\treturn _chooseFromUser->clicks() | rpl::to_empty;\n}\n\nrpl::producer<> BottomBar::showListRequests() const {\n\treturn _showList->clicks() | rpl::to_empty;\n}\n\nvoid BottomBar::buttonFromToggleOn(rpl::producer<bool> &&visible) {\n\tstd::move(\n\t\tvisible\n\t) | rpl::on_next([=](bool value) {\n\t\t_chooseFromUser->setVisible(value);\n\t}, _chooseFromUser->lifetime());\n}\n\nvoid BottomBar::buttonCalendarToggleOn(rpl::producer<bool> &&visible) {\n\tstd::move(\n\t\tvisible\n\t) | rpl::on_next([=](bool value) {\n\t\t_jumpToDate->setVisible(value);\n\t}, _jumpToDate->lifetime());\n}\n\n} // namespace\n\nclass ComposeSearch::Inner final {\npublic:\n\tInner(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<History*> history,\n\t\tPeerData *from,\n\t\tconst QString &query);\n\t~Inner();\n\n\tvoid hideAnimated();\n\tvoid setInnerFocus();\n\tvoid setQuery(const QString &query);\n\tvoid setTopMsgId(MsgId topMsgId);\n\tvoid setSearchFilter(Api::SearchFilter filter);\n\tvoid setCalendarChat(const Dialogs::Key &chat);\n\tvoid setCalendarJumpHandler(Fn<void(FullMsgId, Fn<void()>)> jump);\n\n\t[[nodiscard]] rpl::producer<Activation> activations() const;\n\t[[nodiscard]] rpl::producer<> destroyRequests() const;\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tvoid showAnimated();\n\tvoid hideList();\n\n\tconst not_null<Window::SessionController*> _window;\n\tconst not_null<History*> _history;\n\tconst base::unique_qptr<TopBar> _topBar;\n\tconst base::unique_qptr<BottomBar> _bottomBar;\n\tconst List _list;\n\n\tApi::MessagesSearchMerged _apiSearch;\n\n\tstruct {\n\t\tstruct {\n\t\t\tQString token;\n\t\t\tBottomBar::Index index = -1;\n\t\t} data;\n\t\trpl::event_stream<BottomBar::Index> jumps;\n\t} _pendingJump;\n\n\tMsgId _topMsgId;\n\tApi::SearchFilter _searchFilter = Api::SearchFilter::NoFilter;\n\trpl::variable<bool> _filterAllowsFrom = true;\n\tDialogs::Key _calendarChat;\n\tFn<void(FullMsgId, Fn<void()>)> _calendarJump;\n\n\trpl::event_stream<Activation> _activations;\n\trpl::event_stream<> _destroyRequests;\n\n};\n\nComposeSearch::Inner::Inner(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Window::SessionController*> window,\n\tnot_null<History*> history,\n\tPeerData *from,\n\tconst QString &query)\n: _window(window)\n, _history(history)\n, _topBar(base::make_unique_q<TopBar>(parent, window, history, from, query))\n, _bottomBar(base::make_unique_q<BottomBar>(parent, HasChooseFrom(history)))\n, _list(\n\tCreateList(\n\t\tparent,\n\t\thistory,\n\t\t_topBar->keyEvents() | rpl::filter([](not_null<QKeyEvent*> e) {\n\t\t\treturn e->key() == Qt::Key_PageDown || e->key() == Qt::Key_PageUp;\n\t\t})))\n, _apiSearch(history)\n, _calendarChat(history) {\n\tshowAnimated();\n\n\trpl::combine(\n\t\t_topBar->geometryValue(),\n\t\t_bottomBar->geometryValue()\n\t) | rpl::on_next([=](const QRect &top, const QRect &bottom) {\n\t\t_list.container->setGeometry(QRect(\n\t\t\ttop.topLeft() + QPoint(0, top.height()),\n\t\t\tbottom.topLeft() + QPoint(bottom.width(), 0)));\n\t}, _list.container->lifetime());\n\n\t_topBar->keyEvents(\n\t) | rpl::on_next([=](not_null<QKeyEvent*> e) {\n\t\tif (!_bottomBar->handleKeyPress(e)) {\n\t\t\t_topBar->handleKeyPress(e);\n\t\t}\n\t}, _topBar->lifetime());\n\n\t_topBar->searchRequests(\n\t) | rpl::on_next([=](SearchRequest search) {\n\t\tif (search.query.isEmpty() && search.tags.empty()) {\n\t\t\tif (!search.from || _history->peer->isSelf()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tsearch.topMsgId = _topMsgId;\n\t\tsearch.filter = _searchFilter;\n\t\t_apiSearch.clear();\n\n\t\t_list.controller->addItems({}, true);\n\t\t_list.controller->setQuery(search.query);\n\t\t_apiSearch.search(search);\n\t}, _topBar->lifetime());\n\n\t_topBar->queryChanges(\n\t) | rpl::on_next([=] {\n\t\thideList();\n\t}, _topBar->lifetime());\n\n\t_topBar->closeRequests(\n\t) | rpl::on_next([=] {\n\t\thideAnimated();\n\t}, _topBar->lifetime());\n\n\t_topBar->cancelRequests(\n\t) | rpl::on_next([=] {\n\t\tif (!_list.container->isHidden()) {\n\t\t\tUi::Animations::HideWidgets({ _list.container.get() });\n\t\t} else {\n\t\t\thideAnimated();\n\t\t}\n\t}, _topBar->lifetime());\n\n\t_apiSearch.newFounds(\n\t) | rpl::on_next([=] {\n\t\tconst auto &apiData = _apiSearch.messages();\n\t\tconst auto weak = base::make_weak(_bottomBar.get());\n\t\t_bottomBar->setTotal(apiData.total);\n\t\tif (weak) {\n\t\t\t// Activating the first search result may switch the chat.\n\t\t\t_list.controller->addItems(apiData.messages, true);\n\t\t}\n\t}, _topBar->lifetime());\n\n\t_apiSearch.nextFounds(\n\t) | rpl::on_next([=] {\n\t\tif (_pendingJump.data.token == _apiSearch.messages().nextToken) {\n\t\t\t_pendingJump.jumps.fire_copy(_pendingJump.data.index);\n\t\t}\n\t\t_list.controller->addItems(_apiSearch.messages().messages, false);\n\t}, _topBar->lifetime());\n\n\trpl::merge(\n\t\t_pendingJump.jumps.events() | rpl::filter(rpl::mappers::_1 >= 0),\n\t\t_bottomBar->showItemRequests()\n\t) | rpl::on_next([=](BottomBar::Index index) {\n\t\tconst auto &apiData = _apiSearch.messages();\n\t\tconst auto &messages = apiData.messages;\n\t\tconst auto size = int(messages.size());\n\t\tif (index >= (size - 1) && size != apiData.total) {\n\t\t\t_apiSearch.searchMore();\n\t\t}\n\t\tif (index >= size || index < 0) {\n\t\t\t_pendingJump.data = { apiData.nextToken, index };\n\t\t\treturn;\n\t\t}\n\t\t_pendingJump.data = {};\n\t\tconst auto item = _history->owner().message(messages[index]);\n\t\tif (item) {\n\t\t\tconst auto weak = base::make_weak(_topBar.get());\n\t\t\t_activations.fire_copy({ item, _apiSearch.request().query });\n\t\t\tif (weak) {\n\t\t\t\thideList();\n\t\t\t}\n\t\t}\n\t}, _bottomBar->lifetime());\n\n\t_list.controller->showItemRequests(\n\t) | rpl::on_next([=](const FullMsgId &id) {\n\t\tconst auto &messages = _apiSearch.messages().messages;\n\t\tconst auto it = ranges::find(messages, id);\n\t\tif (it != end(messages)) {\n\t\t\t_bottomBar->setCurrent(std::distance(begin(messages), it) + 1);\n\t\t}\n\t}, _list.container->lifetime());\n\n\t_list.controller->searchMoreRequests(\n\t) | rpl::on_next([=] {\n\t\tconst auto &apiData = _apiSearch.messages();\n\t\tif (int(apiData.messages.size()) != apiData.total) {\n\t\t\t_apiSearch.searchMore();\n\t\t}\n\t}, _list.container->lifetime());\n\n\t_bottomBar->showCalendarRequests(\n\t) | rpl::on_next([=] {\n\t\thideList();\n\t\tauto descriptor = Window::SessionController::ShowCalendarDescriptor();\n\t\tdescriptor.chat = _calendarChat;\n\t\tdescriptor.customJump = _calendarJump;\n\t\t_window->showCalendar(std::move(descriptor));\n\t}, _bottomBar->lifetime());\n\n\t_bottomBar->showBoxFromRequests(\n\t) | rpl::on_next([=] {\n\t\tconst auto peer = _history->peer;\n\t\tauto box = Dialogs::SearchFromBox(\n\t\t\tpeer,\n\t\t\tcrl::guard(_bottomBar.get(), [=](not_null<PeerData*> from) {\n\t\t\t\t_window->hideLayer();\n\t\t\t\t_topBar->setFrom(from);\n\t\t\t}),\n\t\t\tcrl::guard(_bottomBar.get(), [=] { setInnerFocus(); }));\n\n\t\t_window->show(std::move(box));\n\t}, _bottomBar->lifetime());\n\n\t_bottomBar->showListRequests(\n\t) | rpl::on_next([=] {\n\t\tif (_list.container->isHidden()) {\n\t\t\tUi::Animations::ShowWidgets({ _list.container.get() });\n\t\t} else {\n\t\t\thideList();\n\t\t}\n\t}, _bottomBar->lifetime());\n\n\t_bottomBar->buttonCalendarToggleOn(_topBar->fromValue(\n\t) | rpl::map([=](PeerData *from) {\n\t\treturn !from;\n\t}));\n\n\t_bottomBar->buttonFromToggleOn(rpl::combine(\n\t\t_topBar->fromValue(),\n\t\t_filterAllowsFrom.value()\n\t) | rpl::map([=](PeerData *from, bool allowed) {\n\t\treturn allowed && HasChooseFrom(_history) && !from;\n\t}));\n\n\tif (!query.isEmpty()) {\n\t\t_apiSearch.search({ query });\n\t}\n}\n\nvoid ComposeSearch::Inner::setInnerFocus() {\n\tif (Ui::AppInFocus() && Ui::InFocusChain(_topBar->window())) {\n\t\t_topBar->setInnerFocus();\n\t}\n}\n\nvoid ComposeSearch::Inner::setQuery(const QString &query) {\n\t_topBar->setQuery(query);\n}\n\nvoid ComposeSearch::Inner::setTopMsgId(MsgId topMsgId) {\n\tif (topMsgId) {\n\t\t_apiSearch.disableMigrated();\n\t}\n\t_topMsgId = topMsgId;\n}\n\nvoid ComposeSearch::Inner::setSearchFilter(Api::SearchFilter filter) {\n\t_searchFilter = filter;\n\t_filterAllowsFrom = (filter != Api::SearchFilter::Pinned);\n}\n\nvoid ComposeSearch::Inner::setCalendarChat(const Dialogs::Key &chat) {\n\t_calendarChat = chat;\n}\n\nvoid ComposeSearch::Inner::setCalendarJumpHandler(\n\t\tFn<void(FullMsgId, Fn<void()>)> jump) {\n\t_calendarJump = std::move(jump);\n}\n\nvoid ComposeSearch::Inner::showAnimated() {\n\t// Don't animate bottom bar.\n\t_bottomBar->show();\n\tUi::Animations::ShowWidgets({ _topBar.get() });\n}\n\nvoid ComposeSearch::Inner::hideAnimated() {\n\thideList();\n\tUi::Animations::HideWidgets({ _topBar.get(), _bottomBar.get() });\n\n\t_destroyRequests.fire({});\n}\n\nvoid ComposeSearch::Inner::hideList() {\n\tif (!_list.container->isHidden()) {\n\t\tUi::Animations::HideWidgets({ _list.container.get() });\n\t}\n}\n\nrpl::producer<Activation> ComposeSearch::Inner::activations() const {\n\treturn _activations.events();\n}\n\nrpl::producer<> ComposeSearch::Inner::destroyRequests() const {\n\treturn _destroyRequests.events();\n}\n\nrpl::lifetime &ComposeSearch::Inner::lifetime() {\n\treturn _topBar->lifetime();\n}\n\nComposeSearch::Inner::~Inner() {\n}\n\nComposeSearch::ComposeSearch(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Window::SessionController*> window,\n\tnot_null<History*> history,\n\tPeerData *from,\n\tconst QString &query)\n: _inner(std::make_unique<Inner>(parent, window, history, from, query)) {\n}\n\nComposeSearch::~ComposeSearch() {\n}\n\nvoid ComposeSearch::hideAnimated() {\n\t_inner->hideAnimated();\n}\n\nvoid ComposeSearch::setInnerFocus() {\n\t_inner->setInnerFocus();\n}\n\nvoid ComposeSearch::setQuery(const QString &query) {\n\t_inner->setQuery(query);\n}\n\nvoid ComposeSearch::setTopMsgId(MsgId topMsgId) {\n\t_inner->setTopMsgId(topMsgId);\n}\n\nvoid ComposeSearch::setSearchFilter(Api::SearchFilter filter) {\n\t_inner->setSearchFilter(filter);\n}\n\nvoid ComposeSearch::setCalendarChat(const Dialogs::Key &chat) {\n\t_inner->setCalendarChat(chat);\n}\n\nvoid ComposeSearch::setCalendarJumpHandler(\n\t\tFn<void(FullMsgId, Fn<void()>)> jump) {\n\t_inner->setCalendarJumpHandler(std::move(jump));\n}\n\nrpl::producer<ComposeSearch::Activation> ComposeSearch::activations() const {\n\treturn _inner->activations();\n}\n\nrpl::producer<> ComposeSearch::destroyRequests() const {\n\treturn _inner->destroyRequests();\n}\n\nrpl::lifetime &ComposeSearch::lifetime() {\n\treturn _inner->lifetime();\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_compose_search.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Dialogs {\nclass Key;\n} // namespace Dialogs\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Api {\nenum class SearchFilter;\n} // namespace Api\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nclass History;\n\nnamespace HistoryView {\n\nclass ComposeSearch final {\npublic:\n\tComposeSearch(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<History*> history,\n\t\tPeerData *from = nullptr,\n\t\tconst QString &query = QString());\n\t~ComposeSearch();\n\n\tvoid hideAnimated();\n\tvoid setInnerFocus();\n\tvoid setQuery(const QString &query);\n\n\tvoid setTopMsgId(MsgId topMsgId);\n\tvoid setSearchFilter(Api::SearchFilter filter);\n\tvoid setCalendarChat(const Dialogs::Key &chat);\n\tvoid setCalendarJumpHandler(Fn<void(FullMsgId, Fn<void()>)> jump);\n\n\tstruct Activation {\n\t\tnot_null<HistoryItem*> item;\n\t\tQString query;\n\t};\n\t[[nodiscard]] rpl::producer<Activation> activations() const;\n\t[[nodiscard]] rpl::producer<> destroyRequests() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tclass Inner;\n\tconst std::unique_ptr<Inner> _inner;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/controls/history_view_draft_options.h\"\n\n#include \"base/random.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/filters/edit_filter_chats_list.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_drafts.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_session.h\"\n#include \"data/data_thread.h\"\n#include \"data/data_user.h\"\n#include \"data/data_web_page.h\"\n#include \"history/view/controls/history_view_forward_panel.h\"\n#include \"history/view/controls/history_view_webpage_processor.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/discrete_sliders.h\"\n#include \"ui/painter.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\n#include <QtWidgets/QApplication>\n#include <QtWidgets/QWidget>\n\nnamespace HistoryView::Controls {\nnamespace {\n\nenum class Section {\n\tReply,\n\tForward,\n\tLink,\n};\n\nclass PreviewDelegate final : public DefaultElementDelegate {\npublic:\n\tPreviewDelegate(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Ui::ChatStyle*> st,\n\t\tFn<void()> update);\n\n\tbool elementAnimationsPaused() override;\n\tnot_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;\n\tContext elementContext() override;\n\nprivate:\n\tconst not_null<QWidget*> _parent;\n\tconst std::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\n};\n\n[[nodiscard]] TextWithEntities HighlightParsedLinks(\n\t\tTextWithEntities text,\n\t\tconst std::vector<MessageLinkRange> &links) {\n\tauto i = text.entities.begin();\n\tfor (const auto &range : links) {\n\t\tif (range.custom.isEmpty()) {\n\t\t\twhile (i != text.entities.end()) {\n\t\t\t\tif (i->offset() > range.start) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t++i;\n\t\t\t}\n\t\t\ti = text.entities.insert(\n\t\t\t\ti,\n\t\t\t\tEntityInText(EntityType::Url, range.start, range.length));\n\t\t\t++i;\n\t\t}\n\t}\n\treturn text;\n}\n\nclass PreviewWrap final : public Ui::RpWidget {\npublic:\n\tPreviewWrap(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<History*> history);\n\t~PreviewWrap();\n\n\t[[nodiscard]] bool hasViewForItem(\n\t\tnot_null<const HistoryItem*> item) const;\n\n\tvoid showForwardSelector(Data::ResolvedForwardDraft draft);\n\t[[nodiscard]] rpl::producer<SelectedQuote> showQuoteSelector(\n\t\tconst SelectedQuote &quote);\n\t[[nodiscard]] rpl::producer<QString> showLinkSelector(\n\t\tconst TextWithTags &message,\n\t\tData::WebPageDraft webpage,\n\t\tconst std::vector<MessageLinkRange> &links,\n\t\tconst QString &usedLink);\n\n\t[[nodiscard]] rpl::producer<int> draggingScrollDelta() const {\n\t\treturn _draggingScrollDelta.events();\n\t}\n\nprivate:\n\tstruct Entry {\n\t\tHistoryItem *item = nullptr;\n\t\tstd::unique_ptr<Element> view;\n\t};\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid mouseDoubleClickEvent(QMouseEvent *e) override;\n\n\tvoid visibleTopBottomUpdated(int top, int bottom) override {\n\t\t_visibleTop = top;\n\t\t_visibleBottom = bottom;\n\t}\n\n\tvoid clear(std::vector<Entry> entries);\n\tvoid initElements();\n\tvoid highlightUsedLink(\n\t\tconst TextWithTags &message,\n\t\tconst QString &usedLink,\n\t\tconst std::vector<MessageLinkRange> &links);\n\tvoid startSelection(TextSelectType type);\n\t[[nodiscard]] TextSelection resolveNewSelection() const;\n\n\tconst not_null<Ui::GenericBox*> _box;\n\tconst not_null<History*> _history;\n\tconst std::unique_ptr<Ui::ChatTheme> _theme;\n\tconst std::unique_ptr<Ui::ChatStyle> _style;\n\tconst std::unique_ptr<PreviewDelegate> _delegate;\n\n\tSection _section = Section::Reply;\n\tstd::vector<Entry> _entries;\n\tbase::flat_set<not_null<const Element*>> _views;\n\trpl::variable<TextSelection> _selection;\n\trpl::event_stream<QString> _chosenUrl;\n\tUi::PeerUserpicView _userpic;\n\trpl::lifetime _elementLifetime;\n\n\tQPoint _position;\n\trpl::event_stream<int> _draggingScrollDelta;\n\tint _visibleTop = 0;\n\tint _visibleBottom = 0;\n\n\tbase::Timer _trippleClickTimer;\n\tClickHandlerPtr _link;\n\tClickHandlerPtr _pressedLink;\n\tTextSelectType _selectType = TextSelectType::Letters;\n\tuint16 _symbol = 0;\n\tuint16 _selectionStartSymbol = 0;\n\tbool _onlyMessageText = false;\n\tbool _afterSymbol = false;\n\tbool _selectionStartAfterSymbol = false;\n\tbool _over = false;\n\tbool _textCursor = false;\n\tbool _linkCursor = false;\n\tbool _selecting = false;\n\n};\n\nPreviewWrap::PreviewWrap(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<History*> history)\n: RpWidget(box)\n, _box(box)\n, _history(history)\n, _theme(Window::Theme::DefaultChatThemeOn(lifetime()))\n, _style(std::make_unique<Ui::ChatStyle>(\n\thistory->session().colorIndicesValue()))\n, _delegate(std::make_unique<PreviewDelegate>(\n\tbox,\n\t_style.get(),\n\t[=] { update(); }))\n, _position(0, st::msgMargin.bottom()) {\n\t_style->apply(_theme.get());\n\n\tconst auto session = &_history->session();\n\tsession->data().viewRepaintRequest(\n\t) | rpl::on_next([=](Data::RequestViewRepaint data) {\n\t\tif (_views.contains(data.view)) {\n\t\t\tupdate();\n\t\t}\n\t}, lifetime());\n\n\t_selection.changes() | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\t_box->setAttribute(Qt::WA_OpaquePaintEvent, false);\n\n\t_box->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tconst auto geometry = Ui::MapFrom(_box, this, rect());\n\t\tconst auto fill = geometry.intersected(clip);\n\t\tif (!fill.isEmpty()) {\n\t\t\tauto p = QPainter(_box);\n\t\t\tp.setClipRect(fill);\n\t\t\tWindow::SectionWidget::PaintBackground(\n\t\t\t\tp,\n\t\t\t\t_theme.get(),\n\t\t\t\tQSize(_box->width(), _box->window()->height()),\n\t\t\t\tfill);\n\t\t}\n\t}, lifetime());\n\n\tsetMouseTracking(true);\n}\n\nPreviewWrap::~PreviewWrap() {\n\t_selection.reset(TextSelection());\n\tbase::take(_views);\n\tclear(base::take(_entries));\n}\n\nvoid PreviewWrap::clear(std::vector<Entry> entries) {\n\t_elementLifetime.destroy();\n\tfor (auto &entry : entries) {\n\t\tentry.view = nullptr;\n\t\tif (const auto item = entry.item) {\n\t\t\titem->destroy();\n\t\t}\n\t}\n}\n\nbool PreviewWrap::hasViewForItem(not_null<const HistoryItem*> item) const {\n\treturn (item->history() == _history)\n\t\t&& ranges::contains(_views, item, &Element::data);\n}\n\nvoid PreviewWrap::showForwardSelector(Data::ResolvedForwardDraft draft) {\n\tExpects(!draft.items.empty());\n\n\t_selection.reset(TextSelection());\n\n\tauto was = base::take(_entries);\n\tauto groups = base::flat_map<MessageGroupId, uint64>();\n\tconst auto groupByItem = [&](not_null<HistoryItem*> item) {\n\t\tconst auto groupId = item->groupId();\n\t\tif (!groupId) {\n\t\t\treturn uint64();\n\t\t}\n\t\tauto i = groups.find(groupId);\n\t\tif (i == end(groups)) {\n\t\t\ti = groups.emplace(groupId, base::RandomValue<uint64>()).first;\n\t\t}\n\t\treturn i->second;\n\t};\n\tconst auto wasViews = base::take(_views);\n\tusing Options = Data::ForwardOptions;\n\tconst auto dropNames = (draft.options != Options::PreserveInfo);\n\tconst auto dropCaptions = (draft.options == Options::NoNamesAndCaptions);\n\tfor (const auto &source : draft.items) {\n\t\tconst auto groupedId = groupByItem(source);\n\t\tconst auto item = _history->addNewLocalMessage({\n\t\t\t.id = _history->nextNonHistoryEntryId(),\n\t\t\t.flags = (MessageFlag::FakeHistoryItem\n\t\t\t\t| MessageFlag::Outgoing\n\t\t\t\t| MessageFlag::HasFromId\n\t\t\t\t| (source->invertMedia()\n\t\t\t\t\t? MessageFlag::InvertMedia\n\t\t\t\t\t: MessageFlag())),\n\t\t\t.from = _history->session().userPeerId(),\n\t\t\t.date = base::unixtime::now(),\n\t\t\t.groupedId = groupedId,\n\t\t\t.ignoreForwardFrom = dropNames,\n\t\t\t.ignoreForwardCaptions = dropCaptions,\n\t\t}, source);\n\t\t_entries.push_back({ item });\n\t}\n\tfor (auto &entry : _entries) {\n\t\tentry.view = entry.item->createView(_delegate.get());\n\t\t_views.emplace(entry.view.get());\n\t}\n\t_link = _pressedLink = nullptr;\n\tclear(std::move(was));\n\n\t_section = Section::Forward;\n\n\tinitElements();\n}\n\nrpl::producer<SelectedQuote> PreviewWrap::showQuoteSelector(\n\t\tconst SelectedQuote &quote) {\n\t_selection.reset(TextSelection());\n\n\tauto was = base::take(_entries);\n\tconst auto wasViews = base::take(_views);\n\tconst auto item = quote.item;\n\tconst auto group = item->history()->owner().groups().find(item);\n\tconst auto leader = group ? group->items.front().get() : item;\n\t_entries.push_back({\n\t\t.view = leader->createView(_delegate.get()),\n\t});\n\t_views.emplace(_entries.back().view.get());\n\t_link = _pressedLink = nullptr;\n\tclear(std::move(was));\n\n\tconst auto media = item->media();\n\t_onlyMessageText = media\n\t\t&& (media->webpage()\n\t\t\t|| media->game()\n\t\t\t|| (!media->photo() && !media->document()));\n\t_section = Section::Reply;\n\n\tinitElements();\n\n\tconst auto view = _entries.back().view.get();\n\t_selection = view->selectionFromQuote(quote);\n\treturn _selection.value(\n\t) | rpl::map([=](TextSelection selection) {\n\t\tif (const auto result = view->selectedQuote(selection)) {\n\t\t\treturn result;\n\t\t}\n\t\treturn SelectedQuote{ item };\n\t});\n}\n\nrpl::producer<QString> PreviewWrap::showLinkSelector(\n\t\tconst TextWithTags &message,\n\t\tData::WebPageDraft webpage,\n\t\tconst std::vector<MessageLinkRange> &links,\n\t\tconst QString &usedLink) {\n\t_selection.reset(TextSelection());\n\tbase::take(_views);\n\tclear(base::take(_entries));\n\n\tusing Flag = MTPDmessageMediaWebPage::Flag;\n\tconst auto item = _history->addNewLocalMessage({\n\t\t.id = _history->nextNonHistoryEntryId(),\n\t\t.flags = (MessageFlag::FakeHistoryItem\n\t\t\t| MessageFlag::Outgoing\n\t\t\t| MessageFlag::HasFromId\n\t\t\t| (webpage.invert ? MessageFlag::InvertMedia : MessageFlag())),\n\t\t.from = _history->session().userPeerId(),\n\t\t.date = base::unixtime::now(),\n\t}, HighlightParsedLinks({\n\t\tmessage.text,\n\t\tTextUtilities::ConvertTextTagsToEntities(message.tags),\n\t}, links), MTP_messageMediaWebPage(\n\t\tMTP_flags(Flag()\n\t\t\t| (webpage.forceLargeMedia\n\t\t\t\t? Flag::f_force_large_media\n\t\t\t\t: Flag())\n\t\t\t| (webpage.forceSmallMedia\n\t\t\t\t? Flag::f_force_small_media\n\t\t\t\t: Flag())),\n\t\tMTP_webPagePending(\n\t\t\tMTP_flags(webpage.url.isEmpty()\n\t\t\t\t? MTPDwebPagePending::Flag()\n\t\t\t\t: MTPDwebPagePending::Flag::f_url),\n\t\t\tMTP_long(webpage.id),\n\t\t\tMTP_string(webpage.url),\n\t\t\tMTP_int(0))));\n\t_entries.push_back({ item, item->createView(_delegate.get()) });\n\t_views.emplace(_entries.back().view.get());\n\n\t_selectType = TextSelectType::Letters;\n\t_symbol = _selectionStartSymbol = 0;\n\t_afterSymbol = _selectionStartAfterSymbol = false;\n\t_section = Section::Link;\n\n\tinitElements();\n\thighlightUsedLink(message, usedLink, links);\n\n\treturn _chosenUrl.events();\n}\n\nvoid PreviewWrap::highlightUsedLink(\n\t\tconst TextWithTags &message,\n\t\tconst QString &usedLink,\n\t\tconst std::vector<MessageLinkRange> &links) {\n\tauto selection = TextSelection();\n\tconst auto view = QStringView(message.text);\n\tfor (const auto &range : links) {\n\t\tauto text = view.mid(range.start, range.length);\n\t\tif (range.custom == usedLink\n\t\t\t|| (range.custom.isEmpty()\n\t\t\t\t&& range.length == usedLink.size()\n\t\t\t\t&& text == usedLink)) {\n\t\t\tselection = {\n\t\t\t\tuint16(range.start),\n\t\t\t\tuint16(range.start + range.length),\n\t\t\t};\n\t\t\tconst auto skip = [](QChar ch) {\n\t\t\t\treturn ch.isSpace() || Ui::Text::IsNewline(ch);\n\t\t\t};\n\t\t\twhile (!text.isEmpty() && skip(text.front())) {\n\t\t\t\ttext = text.mid(1);\n\t\t\t\t++selection.from;\n\t\t\t}\n\t\t\twhile (!text.isEmpty() && skip(text.back())) {\n\t\t\t\ttext = text.mid(0, text.size() - 1);\n\t\t\t\t--selection.to;\n\t\t\t}\n\t\t\tconst auto view = _entries.back().view.get();\n\t\t\tconst auto basic = view->textState(QPoint(0, 0), {\n\t\t\t\t.flags = Ui::Text::StateRequest::Flag::LookupSymbol,\n\t\t\t\t.onlyMessageText = true,\n\t\t\t});\n\t\t\tif (basic.symbol > 0) {\n\t\t\t\tselection.from += basic.symbol;\n\t\t\t\tselection.to += basic.symbol;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\t_selection = selection;\n}\n\nvoid PreviewWrap::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\n\tp.translate(_position);\n\n\tauto context = _theme->preparePaintContext(\n\t\t_style.get(),\n\t\trect(),\n\t\trect(),\n\t\te->rect(),\n\t\t!window()->isActiveWindow());\n\tfor (const auto &entry : _entries) {\n\t\tcontext.outbg = entry.view->hasOutLayout();\n\t\tcontext.selection = _selecting\n\t\t\t? resolveNewSelection()\n\t\t\t: _selection.current();\n\n\t\tentry.view->draw(p, context);\n\n\t\tconst auto height = entry.view->height();\n\t\tp.translate(0, height);\n\t\tcontext.translate(0, -height);\n\t}\n\tconst auto top = _entries.empty() ? nullptr : _entries.back().view.get();\n\tif (top && top->displayFromPhoto()) {\n\t\tauto userpicBottom = height()\n\t\t\t- top->marginBottom()\n\t\t\t- top->marginTop();\n\t\tconst auto item = top->data();\n\t\tconst auto userpicTop = userpicBottom - st::msgPhotoSize;\n\t\tif (const auto from = item->displayFrom()) {\n\t\t\tfrom->paintUserpicLeft(\n\t\t\t\tp,\n\t\t\t\t_userpic,\n\t\t\t\tst::historyPhotoLeft,\n\t\t\t\tuserpicTop,\n\t\t\t\twidth(),\n\t\t\t\tst::msgPhotoSize);\n\t\t} else if (const auto info = item->displayHiddenSenderInfo()) {\n\t\t\tif (info->customUserpic.empty()) {\n\t\t\t\tinfo->emptyUserpic.paintCircle(\n\t\t\t\t\tp,\n\t\t\t\t\tst::historyPhotoLeft,\n\t\t\t\t\tuserpicTop,\n\t\t\t\t\twidth(),\n\t\t\t\t\tst::msgPhotoSize);\n\t\t\t} else {\n\t\t\t\tconst auto valid = info->paintCustomUserpic(\n\t\t\t\t\tp,\n\t\t\t\t\t_userpic,\n\t\t\t\t\tst::historyPhotoLeft,\n\t\t\t\t\tuserpicTop,\n\t\t\t\t\twidth(),\n\t\t\t\t\tst::msgPhotoSize);\n\t\t\t\tif (!valid) {\n\t\t\t\t\tinfo->customUserpic.load(\n\t\t\t\t\t\t&item->history()->session(),\n\t\t\t\t\t\titem->fullId());\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tUnexpected(\"Corrupt forwarded information in message.\");\n\t\t}\n\t}\n}\n\nvoid PreviewWrap::leaveEventHook(QEvent *e) {\n\tif (!_over) {\n\t\treturn;\n\t}\n\t_over = false;\n\t_textCursor = false;\n\t_linkCursor = false;\n\tif (!_selecting) {\n\t\tsetCursor(style::cur_default);\n\t}\n}\n\nvoid PreviewWrap::mouseMoveEvent(QMouseEvent *e) {\n\tif (_entries.empty()) {\n\t\treturn;\n\t}\n\tusing Flag = Ui::Text::StateRequest::Flag;\n\tauto request = StateRequest{\n\t\t.flags = (_section == Section::Reply\n\t\t\t? Flag::LookupSymbol\n\t\t\t: Flag::LookupLink),\n\t\t.onlyMessageText = (_section == Section::Link || _onlyMessageText),\n\t};\n\tconst auto position = e->pos();\n\tauto local = position - _position;\n\tauto resolved = TextState();\n\tfor (auto &entry : _entries) {\n\t\tconst auto height = entry.view->height();\n\t\tif (local.y() < height) {\n\t\t\tresolved = entry.view->textState(local, request);\n\t\t\tbreak;\n\t\t}\n\t\tlocal.setY(local.y() - height);\n\t}\n\t_over = true;\n\tconst auto text = (_section == Section::Reply)\n\t\t&& (resolved.cursor == CursorState::Text);\n\t_link = (_section == Section::Link && resolved.overMessageText)\n\t\t? resolved.link\n\t\t: nullptr;\n\tconst auto link = (_link != nullptr) || (_pressedLink != nullptr);\n\tif (_textCursor != text || _linkCursor != link) {\n\t\t_textCursor = text;\n\t\t_linkCursor = link;\n\t\tsetCursor((text || _selecting)\n\t\t\t? style::cur_text\n\t\t\t: link\n\t\t\t? style::cur_pointer\n\t\t\t: style::cur_default);\n\t}\n\tif (_symbol != resolved.symbol\n\t\t|| _afterSymbol != resolved.afterSymbol) {\n\t\t_symbol = resolved.symbol;\n\t\t_afterSymbol = resolved.afterSymbol;\n\t\tif (_selecting) {\n\t\t\tupdate();\n\t\t}\n\t}\n\n\t_draggingScrollDelta.fire([&] {\n\t\tif (!_selecting || _visibleTop >= _visibleBottom) {\n\t\t\treturn 0;\n\t\t} else if (position.y() < _visibleTop) {\n\t\t\treturn position.y() - _visibleTop;\n\t\t} else if (position.y() >= _visibleBottom) {\n\t\t\treturn position.y() + 1 - _visibleBottom;\n\t\t}\n\t\treturn 0;\n\t}());\n}\n\nvoid PreviewWrap::mousePressEvent(QMouseEvent *e) {\n\tif (!_over) {\n\t\treturn;\n\t} else if (_section == Section::Reply) {\n\t\tstartSelection(_trippleClickTimer.isActive()\n\t\t\t? TextSelectType::Paragraphs\n\t\t\t: TextSelectType::Letters);\n\t} else {\n\t\t_pressedLink = _link;\n\t}\n}\n\nvoid PreviewWrap::mouseReleaseEvent(QMouseEvent *e) {\n\tif (_section == Section::Reply) {\n\t\tif (!_selecting) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto result = resolveNewSelection();\n\t\t_selecting = false;\n\t\t_selectType = TextSelectType::Letters;\n\t\tif (!_textCursor) {\n\t\t\tsetCursor(style::cur_default);\n\t\t}\n\t\t_selection = result;\n\t} else if (base::take(_pressedLink) == _link && _link) {\n\t\tif (const auto url = _link->url(); !url.isEmpty()) {\n\t\t\t_chosenUrl.fire_copy(url);\n\t\t}\n\t} else if (!_link) {\n\t\tsetCursor(style::cur_default);\n\t}\n}\n\nvoid PreviewWrap::mouseDoubleClickEvent(QMouseEvent *e) {\n\tif (!_over) {\n\t\treturn;\n\t} else if (_section == Section::Reply) {\n\t\tstartSelection(TextSelectType::Words);\n\t\t_trippleClickTimer.callOnce(QApplication::doubleClickInterval());\n\t}\n}\n\nvoid PreviewWrap::initElements() {\n\tfor (auto &entry : _entries) {\n\t\tentry.view->initDimensions();\n\t}\n\twidthValue(\n\t) | rpl::filter([=](int width) {\n\t\treturn width > st::msgMinWidth;\n\t}) | rpl::on_next([=](int width) {\n\t\tauto height = _position.y();\n\t\tfor (const auto &entry : _entries) {\n\t\t\theight += entry.view->resizeGetHeight(width);\n\t\t}\n\t\theight += st::msgMargin.top();\n\t\tresize(width, height);\n\t}, _elementLifetime);\n}\n\nTextSelection PreviewWrap::resolveNewSelection() const {\n\tif (_section != Section::Reply || _entries.empty()) {\n\t\treturn TextSelection();\n\t}\n\tconst auto make = [](uint16 symbol, bool afterSymbol) {\n\t\treturn uint16(symbol + (afterSymbol ? 1 : 0));\n\t};\n\tconst auto first = make(_symbol, _afterSymbol);\n\tconst auto second = make(\n\t\t_selectionStartSymbol,\n\t\t_selectionStartAfterSymbol);\n\tconst auto result = (first <= second)\n\t\t? TextSelection{ first, second }\n\t\t: TextSelection{ second, first };\n\treturn _entries.back().view->adjustSelection(result, _selectType);\n}\n\nvoid PreviewWrap::startSelection(TextSelectType type) {\n\tif (_selecting && _selectType >= type) {\n\t\treturn;\n\t}\n\t_selecting = true;\n\t_selectType = type;\n\t_selectionStartSymbol = _symbol;\n\t_selectionStartAfterSymbol = _afterSymbol;\n\tif (!_textCursor) {\n\t\tsetCursor(style::cur_text);\n\t}\n\tupdate();\n}\n\nPreviewDelegate::PreviewDelegate(\n\tnot_null<QWidget*> parent,\n\tnot_null<Ui::ChatStyle*> st,\n\tFn<void()> update)\n: _parent(parent)\n, _pathGradient(MakePathShiftGradient(st, update)) {\n}\n\nbool PreviewDelegate::elementAnimationsPaused() {\n\treturn _parent->window()->isActiveWindow();\n}\n\nauto PreviewDelegate::elementPathShiftGradient()\n-> not_null<Ui::PathShiftGradient*> {\n\treturn _pathGradient.get();\n}\n\nContext PreviewDelegate::elementContext() {\n\treturn Context::Replies;\n}\n\nvoid AddFilledSkip(not_null<Ui::VerticalLayout*> container) {\n\tconst auto skip = container->add(object_ptr<Ui::FixedHeightWidget>(\n\t\tcontainer,\n\t\tst::settingsPrivacySkipTop));\n\tskip->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tQPainter(skip).fillRect(clip, st::boxBg);\n\t}, skip->lifetime());\n};\n\nvoid DraftOptionsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tEditDraftOptionsArgs &&args,\n\t\tHistoryItem *replyItem,\n\t\tWebPageData *previewData) {\n\tbox->setWidth(st::boxWideWidth);\n\n\tconst auto &draft = args.draft;\n\tstruct State {\n\t\trpl::variable<Section> shown = Section::Link;\n\t\trpl::lifetime shownLifetime;\n\t\trpl::variable<SelectedQuote> quote;\n\t\tData::ResolvedForwardDraft forward;\n\t\tData::WebPageDraft webpage;\n\t\tWebPageData *preview = nullptr;\n\t\tQString link;\n\t\tUi::SettingsSlider *tabs = nullptr;\n\t\tPreviewWrap *wrap = nullptr;\n\n\t\tFn<void(const QString &link, WebPageData *page)> performSwitch;\n\t\tFn<void(const QString &link, bool force)> requestAndSwitch;\n\t\trpl::lifetime resolveLifetime;\n\n\t\tFn<void()> rebuild;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tstate->link = args.usedLink;\n\tstate->quote = SelectedQuote{\n\t\treplyItem,\n\t\t{ draft.reply.quote, draft.reply.quoteOffset },\n\t};\n\tstate->forward = std::move(args.forward);\n\tstate->webpage = draft.webpage;\n\tstate->preview = previewData;\n\n\tstate->rebuild = [=] {\n\t\tconst auto hasLink = (state->preview != nullptr);\n\t\tconst auto hasReply = (state->quote.current().item != nullptr);\n\t\tconst auto hasForward = !state->forward.items.empty();\n\t\tif (!hasLink && !hasReply && !hasForward) {\n\t\t\tbox->closeBox();\n\t\t\treturn;\n\t\t}\n\t\tconst auto section = state->shown.current();\n\t\tconst auto changeSection = (section == Section::Link)\n\t\t\t? !hasLink\n\t\t\t: (section == Section::Reply)\n\t\t\t? !hasReply\n\t\t\t: !hasForward;\n\t\tconst auto now = !changeSection\n\t\t\t? section\n\t\t\t: hasLink\n\t\t\t? Section::Link\n\t\t\t: hasReply\n\t\t\t? Section::Reply\n\t\t\t: Section::Forward;\n\t\tauto labels = std::vector<QString>();\n\t\tauto indices = base::flat_map<Section, int>();\n\t\tauto sections = std::vector<Section>();\n\t\tconst auto push = [&](Section section, tr::phrase<> phrase) {\n\t\t\tindices[section] = labels.size();\n\t\t\tlabels.push_back(phrase(tr::now));\n\t\t\tsections.push_back(section);\n\t\t};\n\t\tif (hasLink) {\n\t\t\tpush(Section::Link, tr::lng_link_header_short);\n\t\t}\n\t\tif (hasReply) {\n\t\t\tpush(Section::Reply, tr::lng_reply_header_short);\n\t\t}\n\t\tif (hasForward) {\n\t\t\tpush(Section::Forward, tr::lng_forward_header_short);\n\t\t}\n\t\tif (labels.size() > 1) {\n\t\t\tbox->setNoContentMargin(true);\n\t\t\tstate->tabs = box->setPinnedToTopContent(\n\t\t\t\tobject_ptr<Ui::SettingsSlider>(\n\t\t\t\t\tbox.get(),\n\t\t\t\t\tst::defaultTabsSlider));\n\t\t\tstate->tabs->resizeToWidth(st::boxWideWidth);\n\t\t\tstate->tabs->move(0, 0);\n\t\t\tstate->tabs->setRippleTopRoundRadius(st::boxRadius);\n\t\t\tstate->tabs->setSections(labels);\n\t\t\tstate->tabs->setActiveSectionFast(indices[now]);\n\t\t\tstate->tabs->sectionActivated(\n\t\t\t) | rpl::on_next([=](int index) {\n\t\t\t\tstate->shown = sections[index];\n\t\t\t}, box->lifetime());\n\t\t} else {\n\t\t\tconst auto forwardCount = state->forward.items.size();\n\t\t\tbox->setTitle(hasLink\n\t\t\t\t? tr::lng_link_options_header()\n\t\t\t\t: hasReply\n\t\t\t\t? (state->quote.current().highlight.quote.empty()\n\t\t\t\t\t? tr::lng_reply_options_header()\n\t\t\t\t\t: tr::lng_reply_options_quote())\n\t\t\t\t: (forwardCount == 1)\n\t\t\t\t? tr::lng_forward_title()\n\t\t\t\t: tr::lng_forward_many_title(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(forwardCount * 1.0)));\n\t\t}\n\t\tstate->shown.force_assign(now);\n\t};\n\tstate->rebuild();\n\n\tconst auto bottom = box->setPinnedToBottomContent(\n\t\tobject_ptr<Ui::VerticalLayout>(box));\n\n\tconst auto &done = args.done;\n\tconst auto &show = args.show;\n\tconst auto &highlight = args.highlight;\n\tconst auto &clearOldDraft = args.clearOldDraft;\n\tconst auto resolveReply = [=] {\n\t\tauto result = draft.reply;\n\t\tif (const auto current = state->quote.current()) {\n\t\t\tresult.messageId = current.item->fullId();\n\t\t\tresult.quote = current.highlight.quote;\n\t\t\tresult.quoteOffset = current.highlight.quoteOffset;\n//\t\t\tresult.todoItemId = current.highlight.todoItemId;\n\t\t} else {\n\t\t\tresult.quote = {};\n//\t\t\tresult.todoItemId = 0;\n\t\t}\n\t\treturn result;\n\t};\n\tconst auto finish = [=](\n\t\t\tFullReplyTo result,\n\t\t\tData::WebPageDraft webpage,\n\t\t\tstd::optional<Data::ForwardOptions> options) {\n\t\tconst auto weak = base::make_weak(box);\n\t\tauto forward = Data::ForwardDraft();\n\t\tif (options) {\n\t\t\tforward.options = *options;\n\t\t\tfor (const auto &item : state->forward.items) {\n\t\t\t\tforward.ids.push_back(item->fullId());\n\t\t\t}\n\t\t}\n\t\tdone(std::move(result), std::move(webpage), std::move(forward));\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t};\n\tconst auto setupReplyActions = [=] {\n\t\tAddFilledSkip(bottom);\n\n\t\tconst auto item = state->quote.current().item;\n\t\tif (item->allowsForward()) {\n\t\t\tSettings::AddButtonWithIcon(\n\t\t\t\tbottom,\n\t\t\t\ttr::lng_reply_in_another_chat(),\n\t\t\t\tst::settingsButton,\n\t\t\t\t{ &st::menuIconReplace }\n\t\t\t)->setClickedCallback([=] {\n\t\t\t\tShowReplyToChatBox(show, resolveReply(), clearOldDraft);\n\t\t\t});\n\t\t}\n\n\t\tconst auto weak = base::make_weak(box);\n\t\tSettings::AddButtonWithIcon(\n\t\t\tbottom,\n\t\t\ttr::lng_reply_show_in_chat(),\n\t\t\tst::settingsButton,\n\t\t\t{ &st::menuIconShowInChat }\n\t\t)->setClickedCallback([=] {\n\t\t\thighlight(resolveReply());\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t});\n\n\t\tSettings::AddButtonWithIcon(\n\t\t\tbottom,\n\t\t\ttr::lng_reply_remove(),\n\t\t\tst::settingsAttentionButtonWithIcon,\n\t\t\t{ &st::menuIconDeleteAttention }\n\t\t)->setClickedCallback([=] {\n\t\t\tfinish({}, state->webpage, state->forward.options);\n\t\t});\n\n\t\tif (!item->originalText().empty()) {\n\t\t\tAddFilledSkip(bottom);\n\t\t\tUi::AddDividerText(bottom, tr::lng_reply_about_quote());\n\t\t}\n\t};\n\tconst auto setupLinkActions = [=] {\n\t\tAddFilledSkip(bottom);\n\n\t\tif (!draft.textWithTags.empty()) {\n\t\t\tSettings::AddButtonWithIcon(\n\t\t\t\tbottom,\n\t\t\t\t(state->webpage.invert\n\t\t\t\t\t? tr::lng_link_move_down()\n\t\t\t\t\t: tr::lng_link_move_up()),\n\t\t\t\tst::settingsButton,\n\t\t\t\t{ state->webpage.invert\n\t\t\t\t\t? &st::menuIconBelow\n\t\t\t\t\t: &st::menuIconAbove }\n\t\t\t)->setClickedCallback([=] {\n\t\t\t\tstate->webpage.invert = !state->webpage.invert;\n\t\t\t\tstate->webpage.manual = true;\n\t\t\t\tstate->shown.force_assign(Section::Link);\n\t\t\t});\n\t\t}\n\n\t\tif (state->preview->hasLargeMedia) {\n\t\t\tconst auto small = state->webpage.forceSmallMedia\n\t\t\t\t|| (!state->webpage.forceLargeMedia\n\t\t\t\t\t&& state->preview->computeDefaultSmallMedia());\n\t\t\tconst auto hasVideo = state->preview->document\n\t\t\t\t&& state->preview->document->isVideoFile();\n\t\t\tSettings::AddButtonWithIcon(\n\t\t\t\tbottom,\n\t\t\t\t(small\n\t\t\t\t\t? (hasVideo\n\t\t\t\t\t\t? tr::lng_link_enlarge_video()\n\t\t\t\t\t\t: tr::lng_link_enlarge_photo())\n\t\t\t\t\t: (hasVideo\n\t\t\t\t\t\t? tr::lng_link_shrink_video()\n\t\t\t\t\t\t: tr::lng_link_shrink_photo())),\n\t\t\t\tst::settingsButton,\n\t\t\t\t{ small ? &st::menuIconEnlarge : &st::menuIconShrink }\n\t\t\t)->setClickedCallback([=] {\n\t\t\t\tif (small) {\n\t\t\t\t\tstate->webpage.forceSmallMedia = false;\n\t\t\t\t\tstate->webpage.forceLargeMedia = true;\n\t\t\t\t} else {\n\t\t\t\t\tstate->webpage.forceLargeMedia = false;\n\t\t\t\t\tstate->webpage.forceSmallMedia = true;\n\t\t\t\t}\n\t\t\t\tstate->webpage.manual = true;\n\t\t\t\tstate->shown.force_assign(Section::Link);\n\t\t\t});\n\t\t}\n\n\t\tSettings::AddButtonWithIcon(\n\t\t\tbottom,\n\t\t\ttr::lng_link_remove(),\n\t\t\tst::settingsAttentionButtonWithIcon,\n\t\t\t{ &st::menuIconDeleteAttention }\n\t\t)->setClickedCallback([=] {\n\t\t\tconst auto options = state->forward.options;\n\t\t\tfinish(resolveReply(), { .removed = true }, options);\n\t\t});\n\n\t\tif (args.links.size() > 1) {\n\t\t\tAddFilledSkip(bottom);\n\t\t\tUi::AddDividerText(bottom, tr::lng_link_about_choose());\n\t\t}\n\t};\n\n\tconst auto setupForwardActions = [=] {\n\t\tusing Options = Data::ForwardOptions;\n\t\tconst auto now = state->forward.options;\n\t\tconst auto &items = state->forward.items;\n\t\tconst auto count = items.size();\n\t\tconst auto dropNames = (now != Options::PreserveInfo);\n\t\tconst auto sendersCount = ItemsForwardSendersCount(items);\n\t\tconst auto captionsCount = ItemsForwardCaptionsCount(items);\n\t\tconst auto hasOnlyForcedForwardedInfo = !captionsCount\n\t\t\t&& HasOnlyForcedForwardedInfo(items);\n\t\tconst auto dropCaptions = (now == Options::NoNamesAndCaptions);\n\n\t\tAddFilledSkip(bottom);\n\n\t\tif (!hasOnlyForcedForwardedInfo\n\t\t\t&& HasDropForwardedInfoSetting(items)) {\n\t\t\tSettings::AddButtonWithIcon(\n\t\t\t\tbottom,\n\t\t\t\t(dropNames\n\t\t\t\t\t? (sendersCount == 1\n\t\t\t\t\t\t? tr::lng_forward_action_show_sender\n\t\t\t\t\t\t: tr::lng_forward_action_show_senders)\n\t\t\t\t\t: (sendersCount == 1\n\t\t\t\t\t\t? tr::lng_forward_action_hide_sender\n\t\t\t\t\t\t: tr::lng_forward_action_hide_senders))(),\n\t\t\t\tst::settingsButton,\n\t\t\t\t{ dropNames\n\t\t\t\t\t? &st::menuIconUserShow\n\t\t\t\t\t: &st::menuIconUserHide }\n\t\t\t)->setClickedCallback([=] {\n\t\t\t\tstate->forward.options = dropNames\n\t\t\t\t\t? Options::PreserveInfo\n\t\t\t\t\t: Options::NoSenderNames;\n\t\t\t\tstate->shown.force_assign(Section::Forward);\n\t\t\t});\n\t\t}\n\t\tif (captionsCount) {\n\t\t\tSettings::AddButtonWithIcon(\n\t\t\t\tbottom,\n\t\t\t\t(dropCaptions\n\t\t\t\t\t? (captionsCount == 1\n\t\t\t\t\t\t? tr::lng_forward_action_show_caption\n\t\t\t\t\t\t: tr::lng_forward_action_show_captions)\n\t\t\t\t\t: (captionsCount == 1\n\t\t\t\t\t\t? tr::lng_forward_action_hide_caption\n\t\t\t\t\t\t: tr::lng_forward_action_hide_captions))(),\n\t\t\t\tst::settingsButton,\n\t\t\t\t{ dropCaptions\n\t\t\t\t\t? &st::menuIconCaptionShow\n\t\t\t\t\t: &st::menuIconCaptionHide }\n\t\t\t)->setClickedCallback([=] {\n\t\t\t\tstate->forward.options = dropCaptions\n\t\t\t\t\t? Options::NoSenderNames\n\t\t\t\t\t: Options::NoNamesAndCaptions;\n\t\t\t\tstate->shown.force_assign(Section::Forward);\n\t\t\t});\n\t\t}\n\n\t\tSettings::AddButtonWithIcon(\n\t\t\tbottom,\n\t\t\ttr::lng_forward_action_change_recipient(),\n\t\t\tst::settingsButton,\n\t\t\t{ &st::menuIconReplace }\n\t\t)->setClickedCallback([=] {\n\t\t\tauto draft = base::take(state->forward);\n\t\t\tfinish(resolveReply(), state->webpage, std::nullopt);\n\t\t\tWindow::ShowForwardMessagesBox(show, {\n\t\t\t\t.ids = show->session().data().itemsToIds(draft.items),\n\t\t\t\t.options = draft.options,\n\t\t\t});\n\t\t});\n\n\t\tSettings::AddButtonWithIcon(\n\t\t\tbottom,\n\t\t\ttr::lng_forward_action_remove(),\n\t\t\tst::settingsAttentionButtonWithIcon,\n\t\t\t{ &st::menuIconDeleteAttention }\n\t\t)->setClickedCallback([=] {\n\t\t\tfinish(resolveReply(), state->webpage, std::nullopt);\n\t\t});\n\n\t\tAddFilledSkip(bottom);\n\t\tUi::AddDividerText(bottom, (count == 1\n\t\t\t? tr::lng_forward_about()\n\t\t\t: tr::lng_forward_many_about()));\n\t};\n\n\tconst auto &resolver = args.resolver;\n\tstate->performSwitch = [=](const QString &link, WebPageData *page) {\n\t\tconst auto now = base::unixtime::now();\n\t\tif (!page || (page->pendingTill > 0 && page->pendingTill < now)) {\n\t\t\tshow->showToast(tr::lng_preview_cant(tr::now));\n\t\t} else if (page->pendingTill > 0) {\n\t\t\tconst auto delay = std::max(page->pendingTill - now, TimeId());\n\t\t\tbase::timer_once(\n\t\t\t\t(delay + 1) * crl::time(1000)\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tstate->requestAndSwitch(link, true);\n\t\t\t}, state->resolveLifetime);\n\n\t\t\tpage->owner().webPageUpdates(\n\t\t\t) | rpl::on_next([=](not_null<WebPageData*> updated) {\n\t\t\t\tif (updated == page && !updated->pendingTill) {\n\t\t\t\t\tstate->resolveLifetime.destroy();\n\t\t\t\t\tstate->performSwitch(link, page);\n\t\t\t\t}\n\t\t\t}, state->resolveLifetime);\n\t\t} else {\n\t\t\tstate->preview = page;\n\t\t\tstate->webpage.id = page->id;\n\t\t\tstate->webpage.url = page->url;\n\t\t\tstate->webpage.manual = true;\n\t\t\tstate->link = link;\n\t\t\tstate->shown.force_assign(Section::Link);\n\t\t}\n\t};\n\tstate->requestAndSwitch = [=](const QString &link, bool force) {\n\t\tresolver->request(link, force);\n\n\t\tstate->resolveLifetime = resolver->resolved(\n\t\t) | rpl::on_next([=](const QString &resolved) {\n\t\t\tif (resolved == link) {\n\t\t\t\tstate->resolveLifetime.destroy();\n\t\t\t\tstate->performSwitch(\n\t\t\t\t\tlink,\n\t\t\t\t\tresolver->lookup(link).value_or(nullptr));\n\t\t\t}\n\t\t});\n\t};\n\tconst auto switchTo = [=](const QString &link) {\n\t\tif (link == state->link) {\n\t\t\treturn;\n\t\t} else if (const auto value = resolver->lookup(link)) {\n\t\t\tstate->performSwitch(link, *value);\n\t\t} else {\n\t\t\tstate->requestAndSwitch(link, false);\n\t\t}\n\t};\n\n\tstate->wrap = box->addRow(\n\t\tobject_ptr<PreviewWrap>(box, args.history),\n\t\tstyle::margins());\n\tstate->wrap->draggingScrollDelta(\n\t) | rpl::on_next([=](int delta) {\n\t\tbox->scrollByDraggingDelta(delta);\n\t}, state->wrap->lifetime());\n\n\tconst auto &linkRanges = args.links;\n\tstate->shown.value() | rpl::on_next([=](Section shown) {\n\t\tbottom->clear();\n\t\tstate->shownLifetime.destroy();\n\t\tswitch (shown) {\n\t\t\tcase Section::Reply: {\n\t\t\t\tstate->quote = state->wrap->showQuoteSelector(\n\t\t\t\t\tstate->quote.current());\n\t\t\t\tsetupReplyActions();\n\t\t\t} break;\n\t\t\tcase Section::Link: {\n\t\t\t\tstate->wrap->showLinkSelector(\n\t\t\t\t\tdraft.textWithTags,\n\t\t\t\t\tstate->webpage,\n\t\t\t\t\tlinkRanges,\n\t\t\t\t\tstate->link\n\t\t\t\t) | rpl::on_next([=](QString link) {\n\t\t\t\t\tswitchTo(link);\n\t\t\t\t}, state->shownLifetime);\n\t\t\t\tsetupLinkActions();\n\t\t\t} break;\n\t\t\tcase Section::Forward: {\n\t\t\t\tstate->wrap->showForwardSelector(state->forward);\n\t\t\t\tsetupForwardActions();\n\t\t\t} break;\n\t\t}\n\t}, box->lifetime());\n\n\tauto save = rpl::combine(\n\t\tstate->quote.value(),\n\t\tstate->shown.value()\n\t) | rpl::map([=](const SelectedQuote &quote, Section shown) {\n\t\treturn (quote.highlight.quote.empty() || shown != Section::Reply)\n\t\t\t? tr::lng_settings_save()\n\t\t\t: tr::lng_reply_quote_selected();\n\t}) | rpl::flatten_latest();\n\tconst auto submit = [=] {\n\t\tif (state->quote.current().overflown) {\n\t\t\tshow->showToast({\n\t\t\t\t.title = tr::lng_reply_quote_long_title(tr::now),\n\t\t\t\t.text = { tr::lng_reply_quote_long_text(tr::now) },\n\t\t\t});\n\t\t} else {\n\t\t\tconst auto options = state->forward.options;\n\t\t\tfinish(resolveReply(), state->webpage, options);\n\t\t}\n\t};\n\tbox->addButton(std::move(save), submit);\n\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n\n\tbox->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::KeyPress) {\n\t\t\tconst auto key = static_cast<QKeyEvent*>(e.get())->key();\n\t\t\tif (key == Qt::Key_Enter || key == Qt::Key_Return) {\n\t\t\t\tsubmit();\n\t\t\t}\n\t\t}\n\t}, box->lifetime());\n\n\targs.show->session().data().itemRemoved(\n\t) | rpl::on_next([=](not_null<const HistoryItem*> removed) {\n\t\tconst auto inReply = (state->quote.current().item == removed);\n\t\tif (inReply) {\n\t\t\tstate->quote = SelectedQuote();\n\t\t}\n\t\tconst auto i = ranges::find(state->forward.items, removed);\n\t\tconst auto inForward = (i != end(state->forward.items));\n\t\tif (inForward) {\n\t\t\tstate->forward.items.erase(i);\n\t\t}\n\t\tif (inReply || inForward) {\n\t\t\tstate->rebuild();\n\t\t}\n\t}, box->lifetime());\n\n\targs.show->session().data().itemViewRefreshRequest(\n\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\tif (state->wrap->hasViewForItem(item)) {\n\t\t\tstate->rebuild();\n\t\t}\n\t}, box->lifetime());\n\n}\n\nstruct AuthorSelector {\n\tobject_ptr<Ui::RpWidget> content = { nullptr };\n\tFn<bool(int, int, int)> overrideKey;\n\tFn<void()> activate;\n};\n[[nodiscard]] AuthorSelector AuthorRowSelector(\n\t\tnot_null<Main::Session*> session,\n\t\tFullReplyTo reply,\n\t\tFn<void(not_null<Data::Thread*>)> chosen) {\n\tconst auto item = session->data().message(reply.messageId);\n\tif (!item) {\n\t\treturn {};\n\t}\n\tconst auto displayFrom = item->displayFrom();\n\tconst auto from = displayFrom ? displayFrom : item->from().get();\n\tif (!from->isUser() || from == item->history()->peer || from->isSelf()) {\n\t\treturn {};\n\t}\n\n\tclass AuthorController final : public PeerListController {\n\tpublic:\n\t\tAuthorController(not_null<PeerData*> peer, Fn<void()> click)\n\t\t: _peer(peer)\n\t\t, _click(std::move(click)) {\n\t\t}\n\n\t\tvoid prepare() override {\n\t\t\tdelegate()->peerListAppendRow(\n\t\t\t\tstd::make_unique<ChatsListBoxController::Row>(\n\t\t\t\t\t_peer->owner().history(_peer),\n\t\t\t\t\t&computeListSt().item));\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t\tTrackMessageMoneyRestrictionsChanges(this, _lifetime);\n\t\t}\n\t\tvoid loadMoreRows() override {\n\t\t}\n\t\tvoid rowClicked(not_null<PeerListRow*> row) override {\n\t\t\tif (RecipientRow::ShowLockedError(\n\t\t\t\t\tthis,\n\t\t\t\t\trow,\n\t\t\t\t\tWriteMoneyRestrictionError)) {\n\t\t\t\treturn;\n\t\t\t} else if (const auto onstack = _click) {\n\t\t\t\tonstack();\n\t\t\t}\n\t\t}\n\t\tMain::Session &session() const override {\n\t\t\treturn _peer->session();\n\t\t}\n\n\tprivate:\n\t\tconst not_null<PeerData*> _peer;\n\t\tFn<void()> _click;\n\t\trpl::lifetime _lifetime;\n\n\t};\n\n\tauto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);\n\tconst auto container = result.data();\n\n\tcontainer->add(CreatePeerListSectionSubtitle(\n\t\tcontainer,\n\t\ttr::lng_reply_in_author()));\n\tUi::AddSkip(container);\n\n\tconst auto activate = [=] {\n\t\tchosen(from->owner().history(from));\n\t};\n\tconst auto delegate = container->lifetime().make_state<\n\t\tPeerListContentDelegateSimple\n\t>();\n\tconst auto controller = container->lifetime().make_state<\n\t\tAuthorController\n\t>(from, activate);\n\tcontroller->setStyleOverrides(&st::peerListSingleRow);\n\tconst auto content = container->add(object_ptr<PeerListContent>(\n\t\tcontainer,\n\t\tcontroller));\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\n\tUi::AddSkip(container);\n\tcontainer->add(CreatePeerListSectionSubtitle(\n\t\tcontainer,\n\t\ttr::lng_reply_in_chats_list()));\n\n\tconst auto overrideKey = [=](int direction, int from, int to) {\n\t\tif (!content->isVisible()) {\n\t\t\treturn false;\n\t\t} else if (direction > 0 && from < 0 && to >= 0) {\n\t\t\tif (content->hasSelection()) {\n\t\t\t\tconst auto was = content->selectedIndex();\n\t\t\t\tconst auto now = content->selectSkip(1).reallyMovedTo;\n\t\t\t\tif (was != now) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tcontent->clearSelection();\n\t\t\t} else {\n\t\t\t\tcontent->selectSkip(1);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (direction < 0 && to < 0) {\n\t\t\tif (!content->hasSelection()) {\n\t\t\t\tcontent->selectLast();\n\t\t\t} else if (from >= 0 || content->hasSelection()) {\n\t\t\t\tcontent->selectSkip(-1);\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\n\treturn {\n\t\t.content = std::move(result),\n\t\t.overrideKey = overrideKey,\n\t\t.activate = activate,\n\t};\n}\n\n} // namespace\n\nvoid ShowReplyToChatBox(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tFullReplyTo reply,\n\t\tFn<void()> clearOldDraft) {\n\tclass Controller final : public ChooseRecipientBoxController {\n\tpublic:\n\t\tusing Chosen = not_null<Data::Thread*>;\n\n\t\tController(not_null<Main::Session*> session, FullReplyTo reply)\n\t\t: ChooseRecipientBoxController({\n\t\t\t.session = session,\n\t\t\t.callback = [=](Chosen thread) {\n\t\t\t\t_singleChosen.fire_copy(thread);\n\t\t\t},\n\t\t\t.moneyRestrictionError = WriteMoneyRestrictionError,\n\t\t}) {\n\t\t\t_authorRow = AuthorRowSelector(\n\t\t\t\tsession,\n\t\t\t\treply,\n\t\t\t\t[=](Chosen thread) { _singleChosen.fire_copy(thread); });\n\t\t\tif (_authorRow.content) {\n\t\t\t\tsetStyleOverrides(&st::peerListSmallSkips);\n\t\t\t}\n\t\t}\n\n\t\tvoid noSearchSubmit() {\n\t\t\tif (const auto onstack = _authorRow.activate) {\n\t\t\t\tonstack();\n\t\t\t}\n\t\t}\n\n\t\t[[nodiscard]] rpl::producer<Chosen> singleChosen() const {\n\t\t\treturn _singleChosen.events();\n\t\t}\n\n\t\tQString savedMessagesChatStatus() const override {\n\t\t\treturn tr::lng_saved_quote_here(tr::now);\n\t\t}\n\n\t\tbool overrideKeyboardNavigation(\n\t\t\t\tint direction,\n\t\t\t\tint fromIndex,\n\t\t\t\tint toIndex) override {\n\t\t\treturn _authorRow.overrideKey\n\t\t\t\t&& _authorRow.overrideKey(direction, fromIndex, toIndex);\n\t\t}\n\n\tprivate:\n\t\tvoid prepareViewHook() override {\n\t\t\tif (_authorRow.content) {\n\t\t\t\tdelegate()->peerListSetAboveWidget(\n\t\t\t\t\tstd::move(_authorRow.content));\n\t\t\t}\n\t\t\tChooseRecipientBoxController::prepareViewHook();\n\t\t\tdelegate()->peerListSetTitle(tr::lng_reply_in_another_title());\n\t\t}\n\n\t\trpl::event_stream<Chosen> _singleChosen;\n\t\tAuthorSelector _authorRow;\n\n\t};\n\n\tstruct State {\n\t\tnot_null<PeerListBox*> box;\n\t\tnot_null<Controller*> controller;\n\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t};\n\tconst auto session = &show->session();\n\tconst auto state = [&] {\n\t\tauto controller = std::make_unique<Controller>(session, reply);\n\t\tconst auto controllerRaw = controller.get();\n\t\tauto box = Box<PeerListBox>(std::move(controller), [=](\n\t\t\t\tnot_null<PeerListBox*> box) {\n\t\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\n\t\t\tbox->noSearchSubmits() | rpl::on_next([=] {\n\t\t\t\tcontrollerRaw->noSearchSubmit();\n\t\t\t}, box->lifetime());\n\t\t});\n\t\tconst auto boxRaw = box.data();\n\t\tshow->show(std::move(box));\n\t\tauto state = State{ boxRaw, controllerRaw };\n\t\treturn boxRaw->lifetime().make_state<State>(std::move(state));\n\t}();\n\n\tauto chosen = [=](not_null<Data::Thread*> thread) mutable {\n\t\tconst auto history = thread->owningHistory();\n\t\tconst auto topicRootId = thread->topicRootId();\n\t\tconst auto monoforumPeerId = thread->monoforumPeerId();\n\t\tconst auto draft = history->localDraft(topicRootId, monoforumPeerId);\n\t\tconst auto textWithTags = draft\n\t\t\t? draft->textWithTags\n\t\t\t: TextWithTags();\n\t\tconst auto cursor = draft ? draft->cursor : MessageCursor();\n\t\treply.topicRootId = topicRootId;\n\t\treply.monoforumPeerId = monoforumPeerId;\n\t\thistory->setLocalDraft(std::make_unique<Data::Draft>(\n\t\t\ttextWithTags,\n\t\t\treply,\n\t\t\tSuggestOptions(),\n\t\t\tcursor,\n\t\t\tData::WebPageDraft()));\n\t\thistory->clearLocalEditDraft(topicRootId, monoforumPeerId);\n\t\thistory->session().changes().entryUpdated(\n\t\t\tthread,\n\t\t\tData::EntryUpdate::Flag::LocalDraftSet);\n\n\t\tif (clearOldDraft) {\n\t\t\tcrl::on_main(&history->session(), clearOldDraft);\n\t\t}\n\t\treturn true;\n\t};\n\tauto callback = [=, chosen = std::move(chosen)](\n\t\t\tController::Chosen thread) mutable {\n\t\tconst auto weak = base::make_weak(state->box);\n\t\tif (!chosen(thread)) {\n\t\t\treturn;\n\t\t} else if (const auto strong = weak.get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t};\n\tstate->controller->singleChosen(\n\t) | rpl::on_next(std::move(callback), state->box->lifetime());\n}\n\nvoid EditDraftOptions(EditDraftOptionsArgs &&args) {\n\tconst auto &draft = args.draft;\n\tconst auto session = &args.show->session();\n\tconst auto replyItem = session->data().message(draft.reply.messageId);\n\tconst auto previewDataRaw = draft.webpage.id\n\t\t? session->data().webpage(draft.webpage.id).get()\n\t\t: nullptr;\n\tconst auto previewData = (previewDataRaw\n\t\t&& !previewDataRaw->pendingTill\n\t\t&& !previewDataRaw->failed)\n\t\t? previewDataRaw\n\t\t: nullptr;\n\tif (!replyItem && !previewData && args.forward.items.empty()) {\n\t\treturn;\n\t}\n\targs.show->show(\n\t\tBox(DraftOptionsBox, std::move(args), replyItem, previewData));\n}\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_draft_options.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_drafts.h\"\n\nclass History;\nstruct MessageLinkRange;\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace HistoryView::Controls {\n\nclass WebpageResolver;\n\nstruct EditDraftOptionsArgs {\n\tstd::shared_ptr<ChatHelpers::Show> show;\n\tnot_null<History*> history;\n\tData::Draft draft;\n\tQString usedLink;\n\tData::ResolvedForwardDraft forward;\n\tstd::vector<MessageLinkRange> links;\n\tstd::shared_ptr<WebpageResolver> resolver;\n\tFn<void(FullReplyTo, Data::WebPageDraft, Data::ForwardDraft)> done;\n\tFn<void(FullReplyTo)> highlight;\n\tFn<void()> clearOldDraft;\n};\n\nvoid EditDraftOptions(EditDraftOptionsArgs &&args);\n\nvoid ShowReplyToChatBox(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tFullReplyTo reply,\n\tFn<void()> clearOldDraft = nullptr);\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/controls/history_view_forward_panel.h\"\n\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history_item_components.h\"\n#include \"history/view/history_view_item_preview.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_forum_topic.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/forward_options_box.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"core/ui_integration.h\"\n#include \"lang/lang_keys.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n\n#include \"apiwrap.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"data/data_changes.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\nnamespace HistoryView::Controls {\nnamespace {\n\nconstexpr auto kUnknownVersion = -1;\nconstexpr auto kNameWithCaptionsVersion = -2;\nconstexpr auto kNameNoCaptionsVersion = -3;\n\n} // namespace\n\nForwardPanel::ForwardPanel(Fn<void()> repaint)\n: _repaint(std::move(repaint)) {\n}\n\nvoid ForwardPanel::update(\n\t\tData::Thread *to,\n\t\tData::ResolvedForwardDraft draft) {\n\tif (_to == to\n\t\t&& _data.items == draft.items\n\t\t&& _data.options == draft.options) {\n\t\treturn;\n\t}\n\t_dataLifetime.destroy();\n\t_data = std::move(draft);\n\t_to = to;\n\tif (!empty()) {\n\t\tAssert(to != nullptr);\n\n\t\t_data.items.front()->history()->owner().itemRemoved(\n\t\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\t\titemRemoved(item);\n\t\t}, _dataLifetime);\n\n\t\tif (const auto topic = _to->asTopic()) {\n\t\t\ttopic->destroyed(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tupdate(nullptr, {});\n\t\t\t}, _dataLifetime);\n\t\t} else if (const auto sublist = _to->asSublist()) {\n\t\t\tsublist->destroyed(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tupdate(nullptr, {});\n\t\t\t}, _dataLifetime);\n\t\t}\n\n\t\tupdateTexts();\n\t}\n\t_itemsUpdated.fire({});\n}\n\nrpl::producer<> ForwardPanel::itemsUpdated() const {\n\treturn _itemsUpdated.events();\n}\n\nvoid ForwardPanel::checkTexts() {\n\tif (empty()) {\n\t\treturn;\n\t}\n\tconst auto keepNames = (_data.options\n\t\t== Data::ForwardOptions::PreserveInfo);\n\tconst auto keepCaptions = (_data.options\n\t\t!= Data::ForwardOptions::NoNamesAndCaptions);\n\tauto version = keepNames\n\t\t? 0\n\t\t: keepCaptions\n\t\t? kNameWithCaptionsVersion\n\t\t: kNameNoCaptionsVersion;\n\tif (keepNames) {\n\t\tfor (const auto &item : _data.items) {\n\t\t\tif (const auto from = item->originalSender()) {\n\t\t\t\tversion += from->nameVersion();\n\t\t\t} else if (item->originalHiddenSenderInfo()) {\n\t\t\t\t++version;\n\t\t\t} else {\n\t\t\t\tUnexpected(\"Corrupt forwarded information in message.\");\n\t\t\t}\n\t\t}\n\t}\n\tif (_nameVersion != version) {\n\t\t_nameVersion = version;\n\t\tupdateTexts();\n\t}\n}\n\nvoid ForwardPanel::updateTexts() {\n\tconst auto repainter = gsl::finally([&] {\n\t\t_repaint();\n\t});\n\tif (empty()) {\n\t\t_from.clear();\n\t\t_text.clear();\n\t\treturn;\n\t}\n\tQString from;\n\tTextWithEntities text;\n\tconst auto keepNames = (_data.options\n\t\t== Data::ForwardOptions::PreserveInfo);\n\tconst auto keepCaptions = (_data.options\n\t\t!= Data::ForwardOptions::NoNamesAndCaptions);\n\tif (const auto count = int(_data.items.size())) {\n\t\tauto insertedPeers = base::flat_set<not_null<PeerData*>>();\n\t\tauto insertedNames = base::flat_set<QString>();\n\t\tauto fullname = QString();\n\t\tauto names = std::vector<QString>();\n\t\tnames.reserve(_data.items.size());\n\t\tfor (const auto &item : _data.items) {\n\t\t\tif (const auto from = item->originalSender()) {\n\t\t\t\tif (!insertedPeers.contains(from)) {\n\t\t\t\t\tinsertedPeers.emplace(from);\n\t\t\t\t\tnames.push_back(from->shortName());\n\t\t\t\t\tfullname = from->name();\n\t\t\t\t}\n\t\t\t} else if (const auto info = item->originalHiddenSenderInfo()) {\n\t\t\t\tif (!insertedNames.contains(info->name)) {\n\t\t\t\t\tinsertedNames.emplace(info->name);\n\t\t\t\t\tnames.push_back(info->firstName);\n\t\t\t\t\tfullname = info->name;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tUnexpected(\"Corrupt forwarded information in message.\");\n\t\t\t}\n\t\t}\n\t\tif (!keepNames || HasOnlyDroppedForwardedInfo(_data.items)) {\n\t\t\tfrom = tr::lng_forward_sender_names_removed(tr::now);\n\t\t} else if (names.size() > 2) {\n\t\t\tfrom = tr::lng_forwarding_from(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tnames.size() - 1,\n\t\t\t\tlt_user,\n\t\t\t\tnames[0]);\n\t\t} else if (names.size() < 2) {\n\t\t\tfrom = fullname;\n\t\t} else {\n\t\t\tfrom = tr::lng_forwarding_from_two(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tnames[0],\n\t\t\t\tlt_second_user,\n\t\t\t\tnames[1]);\n\t\t}\n\n\t\tif (count < 2) {\n\t\t\tconst auto item = _data.items.front();\n\t\t\ttext = item->toPreview({\n\t\t\t\t.hideSender = true,\n\t\t\t\t.hideCaption = !keepCaptions,\n\t\t\t\t.generateImages = false,\n\t\t\t\t.ignoreGroup = true,\n\t\t\t}).text;\n\t\t\tif (item->computeDropForwardedInfo() || !keepNames) {\n\t\t\t\ttext = DropDisallowedCustomEmoji(_to->peer(), std::move(text));\n\t\t\t}\n\t\t} else {\n\t\t\ttext = Ui::Text::Colorized(\n\t\t\t\ttr::lng_forward_messages(tr::now, lt_count, count));\n\t\t}\n\t}\n\t_from.setText(st::msgNameStyle, from, Ui::NameTextOptions());\n\tconst auto context = Core::TextContext({\n\t\t.session = &_to->session(),\n\t\t.repaint = _repaint,\n\t});\n\t_text.setMarkedText(\n\t\tst::defaultTextStyle,\n\t\ttext,\n\t\tUi::DialogTextOptions(),\n\t\tcontext);\n}\n\nvoid ForwardPanel::refreshTexts() {\n\t_nameVersion = kUnknownVersion;\n\tcheckTexts();\n}\n\nvoid ForwardPanel::itemRemoved(not_null<const HistoryItem*> item) {\n\tconst auto i = ranges::find(_data.items, item);\n\tif (i != end(_data.items)) {\n\t\t_data.items.erase(i);\n\t\trefreshTexts();\n\t\t_itemsUpdated.fire({});\n\t}\n}\n\nconst Data::ResolvedForwardDraft &ForwardPanel::draft() const {\n\treturn _data;\n}\n\nconst HistoryItemsList &ForwardPanel::items() const {\n\treturn _data.items;\n}\n\nbool ForwardPanel::empty() const {\n\treturn _data.items.empty();\n}\n\nvoid ForwardPanel::applyOptions(Data::ForwardOptions options) {\n\tif (_data.items.empty()) {\n\t\treturn;\n\t} else if (_data.options != options) {\n\t\tconst auto topicRootId = _to->topicRootId();\n\t\tconst auto monoforumPeerId = _to->monoforumPeerId();\n\t\t_data.options = options;\n\t\t_to->owningHistory()->setForwardDraft(topicRootId, monoforumPeerId, {\n\t\t\t.ids = _to->owner().itemsToIds(_data.items),\n\t\t\t.options = options,\n\t\t});\n\t\t_repaint();\n\t}\n}\n\nvoid ForwardPanel::editToNextOption() {\n\tusing Options = Data::ForwardOptions;\n\tconst auto captionsCount = ItemsForwardCaptionsCount(_data.items);\n\tconst auto hasOnlyForcedForwardedInfo = !captionsCount\n\t\t&& HasOnlyForcedForwardedInfo(_data.items);\n\tif (hasOnlyForcedForwardedInfo) {\n\t\treturn;\n\t}\n\n\tconst auto now = _data.options;\n\tconst auto next = (now == Options::PreserveInfo)\n\t\t? Options::NoSenderNames\n\t\t: ((now == Options::NoSenderNames) && captionsCount)\n\t\t? Options::NoNamesAndCaptions\n\t\t: Options::PreserveInfo;\n\n\tconst auto topicRootId = _to->topicRootId();\n\tconst auto monoforumPeerId = _to->monoforumPeerId();\n\t_to->owningHistory()->setForwardDraft(topicRootId, monoforumPeerId, {\n\t\t.ids = _to->owner().itemsToIds(_data.items),\n\t\t.options = next,\n\t});\n\t_repaint();\n}\n\nvoid ForwardPanel::paint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint available,\n\t\tint outerWidth) const {\n\tif (empty()) {\n\t\treturn;\n\t}\n\tconst_cast<ForwardPanel*>(this)->checkTexts();\n\tconst auto now = crl::now();\n\tconst auto paused = p.inactive();\n\tconst auto pausedSpoiler = paused || On(PowerSaving::kChatSpoiler);\n\tconst auto firstItem = _data.items.front();\n\tconst auto firstMedia = firstItem->media();\n\tconst auto hasPreview = (_data.items.size() < 2)\n\t\t&& firstMedia\n\t\t&& firstMedia->hasReplyPreview();\n\tconst auto preview = hasPreview ? firstMedia->replyPreview() : nullptr;\n\tconst auto spoiler = preview && firstMedia->hasSpoiler();\n\tif (!spoiler) {\n\t\t_spoiler = nullptr;\n\t} else if (!_spoiler) {\n\t\t_spoiler = std::make_unique<Ui::SpoilerAnimation>(_repaint);\n\t}\n\tif (preview) {\n\t\tauto to = QRect(\n\t\t\tx,\n\t\t\ty + (st::historyReplyHeight - st::historyReplyPreview) / 2,\n\t\t\tst::historyReplyPreview,\n\t\t\tst::historyReplyPreview);\n\t\tp.drawPixmap(to.x(), to.y(), preview->pixSingle(\n\t\t\tpreview->size() / style::DevicePixelRatio(),\n\t\t\t{\n\t\t\t\t.options = Images::Option::RoundSmall,\n\t\t\t\t.outer = to.size(),\n\t\t\t}));\n\t\tif (_spoiler) {\n\t\t\tUi::FillSpoilerRect(p, to, Ui::DefaultImageSpoiler().frame(\n\t\t\t\t_spoiler->index(now, pausedSpoiler)));\n\t\t}\n\t\tconst auto skip = st::historyReplyPreview + st::msgReplyBarSkip;\n\t\tx += skip;\n\t\tavailable -= skip;\n\t}\n\tp.setPen(st::historyReplyNameFg);\n\t_from.drawElided(\n\t\tp,\n\t\tx,\n\t\ty + st::msgReplyPadding.top(),\n\t\tavailable);\n\tp.setPen(st::historyComposeAreaFg);\n\t_text.draw(p, {\n\t\t.position = QPoint(\n\t\t\tx,\n\t\t\ty + st::msgReplyPadding.top() + st::msgServiceNameFont->height),\n\t\t.availableWidth = available,\n\t\t.palette = &st::historyComposeAreaPalette,\n\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t.now = now,\n\t\t.pausedEmoji = paused || On(PowerSaving::kEmojiChat),\n\t\t.pausedSpoiler = pausedSpoiler,\n\t\t.elisionLines = 1,\n\t});\n}\n\nvoid ClearDraftReplyTo(\n\t\tnot_null<History*> history,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tFullMsgId equalTo) {\n\tconst auto local = history->localDraft(topicRootId, monoforumPeerId);\n\tif (!local || (equalTo && local->reply.messageId != equalTo)) {\n\t\treturn;\n\t}\n\tauto draft = *local;\n\tdraft.reply = {\n\t\t.topicRootId = topicRootId,\n\t\t.monoforumPeerId = monoforumPeerId,\n\t};\n\tdraft.suggest = SuggestOptions();\n\tif (Data::DraftIsNull(&draft)) {\n\t\thistory->clearLocalDraft(topicRootId, monoforumPeerId);\n\t} else {\n\t\thistory->setLocalDraft(\n\t\t\tstd::make_unique<Data::Draft>(std::move(draft)));\n\t}\n\tconst auto thread = history->threadFor(topicRootId, monoforumPeerId);\n\tif (thread) {\n\t\thistory->session().api().saveDraftToCloudDelayed(thread);\n\t}\n}\n\nvoid EditWebPageOptions(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<WebPageData*> webpage,\n\t\tData::WebPageDraft draft,\n\t\tFn<void(Data::WebPageDraft)> done) {\n\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tbox->setTitle(u\"Link Preview\"_q);\n\n\t\tstruct State {\n\t\t\trpl::variable<Data::WebPageDraft> result;\n\t\t\tUi::SettingsButton *large = nullptr;\n\t\t\tUi::SettingsButton *small = nullptr;\n\t\t};\n\t\tconst auto state = box->lifetime().make_state<State>(State{\n\t\t\t.result = draft,\n\t\t\t});\n\n\t\tstate->large = Settings::AddButtonWithIcon(\n\t\t\tbox->verticalLayout(),\n\t\t\trpl::single(u\"Force large media\"_q),\n\t\t\tst::settingsButton,\n\t\t\t{ &st::menuIconMakeBig });\n\t\tstate->large->setClickedCallback([=] {\n\t\t\tauto copy = state->result.current();\n\t\t\tcopy.forceLargeMedia = true;\n\t\t\tcopy.forceSmallMedia = false;\n\t\t\tstate->result = copy;\n\t\t});\n\n\t\tstate->small = Settings::AddButtonWithIcon(\n\t\t\tbox->verticalLayout(),\n\t\t\trpl::single(u\"Force small media\"_q),\n\t\t\tst::settingsButton,\n\t\t\t{ &st::menuIconMakeSmall });\n\t\tstate->small->setClickedCallback([=] {\n\t\t\tauto copy = state->result.current();\n\t\t\tcopy.forceSmallMedia = true;\n\t\t\tcopy.forceLargeMedia = false;\n\t\t\tstate->result = copy;\n\t\t});\n\n\t\tstate->result.value(\n\t\t) | rpl::on_next([=](const Data::WebPageDraft &draft) {\n\t\t\tstate->large->setColorOverride(draft.forceLargeMedia\n\t\t\t\t? st::windowActiveTextFg->c\n\t\t\t\t: std::optional<QColor>());\n\t\t\tstate->small->setColorOverride(draft.forceSmallMedia\n\t\t\t\t? st::windowActiveTextFg->c\n\t\t\t\t: std::optional<QColor>());\n\t\t}, box->lifetime());\n\n\t\tSettings::AddButtonWithIcon(\n\t\t\tbox->verticalLayout(),\n\t\t\tstate->result.value(\n\t\t\t) | rpl::map([=](const Data::WebPageDraft &draft) {\n\t\t\t\treturn draft.invert\n\t\t\t\t\t? u\"Above message\"_q\n\t\t\t\t\t: u\"Below message\"_q;\n\t\t\t}),\n\t\t\tst::settingsButton,\n\t\t\t{ &st::menuIconChangeOrder }\n\t\t)->setClickedCallback([=] {\n\t\t\tauto copy = state->result.current();\n\t\t\tcopy.invert = !copy.invert;\n\t\t\tstate->result = copy;\n\t\t});\n\n\t\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\t\tconst auto weak = base::make_weak(box.get());\n\t\t\tauto result = state->result.current();\n\t\t\tresult.manual = true;\n\t\t\tdone(result);\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t}));\n}\n\nbool HasOnlyForcedForwardedInfo(const HistoryItemsList &list) {\n\tfor (const auto &item : list) {\n\t\tif (const auto media = item->media()) {\n\t\t\tif (!media->forceForwardedInfo()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nbool HasOnlyDroppedForwardedInfo(const HistoryItemsList &list) {\n\tfor (const auto &item : list) {\n\t\tif (item->isSavedMusicItem() || !item->computeDropForwardedInfo()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nbool HasDropForwardedInfoSetting(const HistoryItemsList &list) {\n\tfor (const auto &item : list) {\n\t\tif (!item->computeDropForwardedInfo()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/history.h\"\n#include \"ui/text/text.h\"\n#include \"base/weak_ptr.h\"\n\nclass Painter;\nclass HistoryItem;\n\nnamespace Ui {\nclass SpoilerAnimation;\n} // namespace Ui\n\nnamespace Data {\nclass Thread;\nstruct WebPageDraft;\n} // namespace Data\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace HistoryView::Controls {\n\nclass ForwardPanel final : public base::has_weak_ptr {\npublic:\n\texplicit ForwardPanel(Fn<void()> repaint);\n\n\tvoid update(Data::Thread *to, Data::ResolvedForwardDraft draft);\n\tvoid paint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint available,\n\t\tint outerWidth) const;\n\n\t[[nodiscard]] rpl::producer<> itemsUpdated() const;\n\n\tvoid applyOptions(Data::ForwardOptions options);\n\tvoid editToNextOption();\n\n\t[[nodiscard]] const Data::ResolvedForwardDraft &draft() const;\n\t[[nodiscard]] const HistoryItemsList &items() const;\n\t[[nodiscard]] bool empty() const;\n\nprivate:\n\tvoid checkTexts();\n\tvoid updateTexts();\n\tvoid refreshTexts();\n\tvoid itemRemoved(not_null<const HistoryItem*> item);\n\n\tFn<void()> _repaint;\n\n\tData::Thread *_to = nullptr;\n\tData::ResolvedForwardDraft _data;\n\trpl::lifetime _dataLifetime;\n\n\trpl::event_stream<> _itemsUpdated;\n\tUi::Text::String _from, _text;\n\tmutable std::unique_ptr<Ui::SpoilerAnimation> _spoiler;\n\tint _nameVersion = 0;\n\n};\n\nvoid ClearDraftReplyTo(\n\tnot_null<History*> history,\n\tMsgId topicRootId,\n\tPeerId monoforumPeerId,\n\tFullMsgId equalTo);\n\nvoid EditWebPageOptions(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<WebPageData*> webpage,\n\tData::WebPageDraft draft,\n\tFn<void(Data::WebPageDraft)> done);\n\n[[nodiscard]] bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list);\n[[nodiscard]] bool HasOnlyDroppedForwardedInfo(const HistoryItemsList &list);\n[[nodiscard]] bool HasDropForwardedInfoSetting(const HistoryItemsList &list);\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/controls/history_view_suggest_options.h\"\n\n#include \"base/unixtime.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"core/ui_integration.h\"\n#include \"data/components/credits.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/boxes/choose_date_time.h\"\n#include \"ui/boxes/emoji_stake_box.h\"\n#include \"ui/boxes/single_choice_box.h\"\n#include \"ui/controls/ton_common.h\"\n#include \"ui/widgets/fields/number_input.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/vertical_list.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_channel_earn.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\nnamespace HistoryView {\nnamespace {\n\n[[nodiscard]] rpl::producer<CreditsAmount> StarsPriceValue(\n\t\trpl::producer<CreditsAmount> full) {\n\treturn rpl::single(\n\t\tCreditsAmount()\n\t) | rpl::then(std::move(\n\t\tfull\n\t) | rpl::filter([=](CreditsAmount amount) {\n\t\treturn amount.stars();\n\t}));\n}\n\n[[nodiscard]] rpl::producer<CreditsAmount> TonPriceValue(\n\t\trpl::producer<CreditsAmount> full) {\n\treturn rpl::single(\n\t\tCreditsAmount()\n\t) | rpl::then(std::move(\n\t\tfull\n\t) | rpl::filter([=](CreditsAmount amount) {\n\t\treturn amount.ton();\n\t}));\n}\n\n} // namespace\n\nvoid ChooseSuggestTimeBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tSuggestTimeBoxArgs &&args) {\n\tconst auto now = base::unixtime::now();\n\tconst auto min = args.session->appConfig().suggestedPostDelayMin() + 60;\n\tconst auto max = args.session->appConfig().suggestedPostDelayMax();\n\tconst auto value = args.value\n\t\t? std::clamp(args.value, now + min, now + max)\n\t\t: (now + 86400);\n\tconst auto done = args.done;\n\tUi::ChooseDateTimeBox(box, {\n\t\t.title = ((args.mode == SuggestMode::New\n\t\t\t|| args.mode == SuggestMode::Publish)\n\t\t\t? tr::lng_suggest_options_date()\n\t\t\t: tr::lng_suggest_menu_edit_time()),\n\t\t.submit = ((args.mode == SuggestMode::Publish)\n\t\t\t? tr::lng_suggest_options_date_publish()\n\t\t\t: (args.mode == SuggestMode::New)\n\t\t\t? tr::lng_settings_save()\n\t\t\t: tr::lng_suggest_options_update_date()),\n\t\t.done = done,\n\t\t.min = [=] { return now + min; },\n\t\t.time = value,\n\t\t.max = [=] { return now + max; },\n\t});\n\n\tbox->addLeftButton((args.mode == SuggestMode::Publish)\n\t\t? tr::lng_suggest_options_date_now()\n\t\t: tr::lng_suggest_options_date_any(), [=] {\n\t\tdone(TimeId());\n\t});\n}\n\nStarsTonPriceInput AddStarsTonPriceInput(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tStarsTonPriceArgs &&args) {\n\tstruct State {\n\t\trpl::variable<bool> ton;\n\t\trpl::variable<CreditsAmount> price;\n\t\trpl::event_stream<> updates;\n\t\trpl::event_stream<> submits;\n\t};\n\tconst auto state = container->lifetime().make_state<State>();\n\tstate->ton = std::move(args.showTon);\n\tstate->price = args.price;\n\n\tconst auto session = args.session;\n\tconst auto added = st::boxRowPadding - st::defaultSubsectionTitlePadding;\n\n\tconst auto starsWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto starsInner = starsWrap->entity();\n\n\tUi::AddSubsectionTitle(\n\t\tstarsInner,\n\t\ttr::lng_suggest_options_stars_price(),\n\t\tQMargins(\n\t\t\tadded.left(),\n\t\t\t0,\n\t\t\tadded.right(),\n\t\t\t-st::defaultSubsectionTitlePadding.bottom()));\n\n\tconst auto starsField = AddStarsInputField(starsInner, {\n\t\t.value = ((args.price && args.price.stars())\n\t\t\t? args.price.whole()\n\t\t\t: std::optional<int64>()),\n\t});\n\n\tAddApproximateUsd(\n\t\tstarsField,\n\t\tsession,\n\t\tStarsPriceValue(state->price.value()));\n\n\tUi::AddSkip(starsInner);\n\tUi::AddSkip(starsInner);\n\tif (args.starsAbout) {\n\t\tUi::AddDividerText(starsInner, std::move(args.starsAbout));\n\t}\n\n\tconst auto tonWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto tonInner = tonWrap->entity();\n\n\tUi::AddSubsectionTitle(\n\t\ttonInner,\n\t\ttr::lng_suggest_options_ton_price(),\n\t\tQMargins(\n\t\t\tadded.left(),\n\t\t\t0,\n\t\t\tadded.right(),\n\t\t\t-st::defaultSubsectionTitlePadding.bottom()));\n\n\tconst auto tonField = AddTonInputField(tonInner, {\n\t\t.value = (args.price && args.price.ton())\n\t\t\t? (args.price.whole() * Ui::kNanosInOne + args.price.nano())\n\t\t\t: 0,\n\t});\n\n\tAddApproximateUsd(\n\t\ttonField,\n\t\tsession,\n\t\tTonPriceValue(state->price.value()));\n\n\tUi::AddSkip(tonInner);\n\tUi::AddSkip(tonInner);\n\tif (args.tonAbout) {\n\t\tUi::AddDividerText(tonInner, std::move(args.tonAbout));\n\t}\n\n\ttonWrap->toggleOn(state->ton.value(), anim::type::instant);\n\tstarsWrap->toggleOn(\n\t\tstate->ton.value() | rpl::map(!rpl::mappers::_1),\n\t\tanim::type::instant);\n\n\tauto computeResult = [=]() -> std::optional<CreditsAmount> {\n\t\tauto amount = CreditsAmount();\n\t\tconst auto ton = state->ton.current();\n\t\tif (ton) {\n\t\t\tconst auto text = tonField->getLastText();\n\t\t\tconst auto now = Ui::ParseTonAmountString(text);\n\t\t\tamount = CreditsAmount(\n\t\t\t\tnow.value_or(0) / Ui::kNanosInOne,\n\t\t\t\tnow.value_or(0) % Ui::kNanosInOne,\n\t\t\t\tCreditsType::Ton);\n\t\t\tconst auto bad = (!now || !*now)\n\t\t\t\t? (!args.allowEmpty)\n\t\t\t\t: ((*now < args.nanoTonMin) || (*now > args.nanoTonMax));\n\t\t\tif (!bad) {\n\t\t\t\treturn amount;\n\t\t\t}\n\t\t\ttonField->showError();\n\t\t} else {\n\t\t\tconst auto now = starsField->getLastText().toLongLong();\n\t\t\tamount = CreditsAmount(now);\n\t\t\tconst auto bad = !now\n\t\t\t\t? (!args.allowEmpty)\n\t\t\t\t: ((now < args.starsMin) || (now > args.starsMax));\n\t\t\tif (!bad) {\n\t\t\t\treturn amount;\n\t\t\t}\n\t\t\tstarsField->showError();\n\t\t}\n\t\tif (const auto hook = args.errorHook) {\n\t\t\thook(amount);\n\t\t}\n\t\treturn {};\n\t};\n\n\tconst auto updatePrice = [=] {\n\t\tif (auto result = computeResult()) {\n\t\t\tstate->price = *result;\n\t\t}\n\t\tstate->updates.fire({});\n\t};\n\tconst auto updateTonFromStars = [=] {\n\t\tif (auto result = computeResult(); result && result->stars()) {\n\t\t\tconst auto v = Ui::TonFromStars(session, *result);\n\t\t\tconst auto amount = v.whole() * Ui::kNanosInOne + v.nano();\n\t\t\ttonField->setText(\n\t\t\t\tUi::FormatTonAmount(amount, Ui::TonFormatFlag::Simple).full);\n\t\t}\n\t};\n\tconst auto updateStarsFromTon = [=] {\n\t\tif (auto result = computeResult(); result && result->ton()) {\n\t\t\tconst auto v = Ui::StarsFromTon(session, *result);\n\t\t\tstarsField->setText(QString::number(v.whole()));\n\t\t}\n\t};\n\tQObject::connect(starsField, &Ui::NumberInput::changed, starsField, [=] {\n\t\tif (!state->ton.current()) {\n\t\t\tupdatePrice();\n\t\t\tupdateTonFromStars();\n\t\t}\n\t});\n\ttonField->changes(\n\t) | rpl::on_next([=] {\n\t\tif (state->ton.current()) {\n\t\t\tupdatePrice();\n\t\t\tupdateStarsFromTon();\n\t\t}\n\t}, tonField->lifetime());\n\n\tstate->ton.changes(\n\t) | rpl::on_next(updatePrice, container->lifetime());\n\tif (state->ton.current()) {\n\t\tupdateStarsFromTon();\n\t} else {\n\t\tupdateTonFromStars();\n\t}\n\n\tQObject::connect(\n\t\tstarsField,\n\t\t&Ui::NumberInput::submitted,\n\t\tcontainer,\n\t\t[=] { state->submits.fire({}); });\n\ttonField->submits(\n\t) | rpl::to_empty | rpl::start_to_stream(\n\t\tstate->submits,\n\t\ttonField->lifetime());\n\n\tauto focusCallback = [=] {\n\t\tif (state->ton.current()) {\n\t\t\ttonField->selectAll();\n\t\t\ttonField->setFocusFast();\n\t\t} else {\n\t\t\tstarsField->selectAll();\n\t\t\tstarsField->setFocusFast();\n\t\t}\n\t};\n\n\treturn {\n\t\t.focusCallback = std::move(focusCallback),\n\t\t.computeResult = std::move(computeResult),\n\t\t.submits = state->submits.events(),\n\t\t.updates = state->updates.events(),\n\t\t.result = state->price.value(),\n\t};\n}\n\nvoid ChooseSuggestPriceBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tSuggestPriceBoxArgs &&args) {\n\tstruct Button {\n\t\tQRect geometry;\n\t\tUi::Text::String text;\n\t\tbool active = false;\n\t};\n\tstruct State {\n\t\tstd::vector<Button> buttons;\n\t\trpl::event_stream<> fieldsChanges;\n\t\trpl::variable<CreditsAmount> price;\n\t\trpl::variable<TimeId> date;\n\t\trpl::variable<TimeId> offerDuration;\n\t\trpl::variable<bool> ton;\n\t\tFn<std::optional<CreditsAmount>()> computePrice;\n\t\tFn<void()> save;\n\t\tstd::optional<CreditsAmount> lastSmallPrice;\n\t\tbool savePending = false;\n\t\tbool inButton = false;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tstate->date = args.value.date;\n\tstate->ton = (args.value.ton != 0);\n\tstate->price = args.value.price();\n\n\tconst auto peer = args.peer;\n\t[[maybe_unused]] const auto details = ComputePaymentDetails(peer, 1);\n\n\tconst auto mode = args.mode;\n\tconst auto gift = (mode == SuggestMode::Gift);\n\tconst auto admin = peer->amMonoforumAdmin();\n\tconst auto broadcast = peer->monoforumBroadcast();\n\tconst auto usePeer = broadcast ? broadcast : peer;\n\tconst auto session = &peer->session();\n\tconst auto &appConfig = session->appConfig();\n\tif (!admin) {\n\t\tsession->credits().load();\n\t\tsession->credits().tonLoad();\n\t}\n\tconst auto container = box->verticalLayout();\n\n\tbox->setStyle(st::suggestPriceBox);\n\n\tauto title = gift\n\t\t? tr::lng_gift_offer_title()\n\t\t: (mode == SuggestMode::New)\n\t\t? tr::lng_suggest_options_title()\n\t\t: tr::lng_suggest_options_change();\n\tif (admin) {\n\t\tbox->setTitle(std::move(title));\n\t} else {\n\t\tbox->setNoContentMargin(true);\n\n\t\tUi::AddSkip(container, st::boxTitleHeight * 1.1);\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\tstd::move(title),\n\t\t\t\tst::settingsPremiumUserTitle),\n\t\t\tstyle::al_top);\n\t}\n\n\tstate->buttons.push_back({\n\t\t.text = Ui::Text::String(\n\t\t\tst::semiboldTextStyle,\n\t\t\t(admin\n\t\t\t\t? tr::lng_suggest_options_stars_request(tr::now)\n\t\t\t\t: tr::lng_suggest_options_stars_offer(tr::now))),\n\t\t.active = !state->ton.current(),\n\t});\n\tstate->buttons.push_back({\n\t\t.text = Ui::Text::String(\n\t\t\tst::semiboldTextStyle,\n\t\t\t(admin\n\t\t\t\t? tr::lng_suggest_options_ton_request(tr::now)\n\t\t\t\t: tr::lng_suggest_options_ton_offer(tr::now))),\n\t\t.active = state->ton.current(),\n\t});\n\n\tauto x = 0;\n\tauto y = st::giftBoxTabsMargin.top();\n\tconst auto padding = st::giftBoxTabPadding;\n\tfor (auto &button : state->buttons) {\n\t\tconst auto width = button.text.maxWidth();\n\t\tconst auto height = st::semiboldTextStyle.font->height;\n\t\tconst auto r = QRect(0, 0, width, height).marginsAdded(padding);\n\t\tbutton.geometry = QRect(QPoint(x, y), r.size());\n\t\tx += r.width() + st::giftBoxTabSkip;\n\t}\n\tconst auto buttonsSkip = admin ? 0 : st::normalFont->height;\n\tconst auto buttons = box->addRow(\n\t\tobject_ptr<Ui::RpWidget>(box),\n\t\t(st::boxRowPadding\n\t\t\t- QMargins(\n\t\t\t\tpadding.left() / 2,\n\t\t\t\t-buttonsSkip,\n\t\t\t\tpadding.right() / 2,\n\t\t\t\t0)));\n\tconst auto height = y\n\t\t+ state->buttons.back().geometry.height()\n\t\t+ st::giftBoxTabsMargin.bottom();\n\tbuttons->resize(buttons->width(), height);\n\n\tbuttons->setMouseTracking(true);\n\tbuttons->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tswitch (type) {\n\t\tcase QEvent::MouseMove: {\n\t\t\tconst auto in = [&] {\n\t\t\t\tconst auto me = static_cast<QMouseEvent*>(e.get());\n\t\t\t\tconst auto position = me->pos();\n\t\t\t\tfor (const auto &button : state->buttons) {\n\t\t\t\t\tif (button.geometry.contains(position)) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}();\n\t\t\tif (state->inButton != in) {\n\t\t\t\tstate->inButton = in;\n\t\t\t\tbuttons->setCursor(in\n\t\t\t\t\t? style::cur_pointer\n\t\t\t\t\t: style::cur_default);\n\t\t\t}\n\t\t} break;\n\t\tcase QEvent::MouseButtonPress: {\n\t\t\tconst auto me = static_cast<QMouseEvent*>(e.get());\n\t\t\tif (me->button() != Qt::LeftButton) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst auto position = me->pos();\n\t\t\tfor (auto i = 0, c = int(state->buttons.size()); i != c; ++i) {\n\t\t\t\tif (state->buttons[i].geometry.contains(position)) {\n\t\t\t\t\tstate->ton = (i != 0);\n\t\t\t\t\tstate->buttons[i].active = true;\n\t\t\t\t\tstate->buttons[1 - i].active = false;\n\t\t\t\t\tbuttons->update();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} break;\n\t\t}\n\t}, buttons->lifetime());\n\n\tbuttons->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(buttons);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto padding = st::giftBoxTabPadding;\n\t\tfor (const auto &button : state->buttons) {\n\t\t\tconst auto geometry = button.geometry;\n\t\t\tif (button.active) {\n\t\t\t\tp.setBrush(st::giftBoxTabBgActive);\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tconst auto radius = geometry.height() / 2.;\n\t\t\t\tp.drawRoundedRect(geometry, radius, radius);\n\t\t\t\tp.setPen(st::giftBoxTabFgActive);\n\t\t\t} else {\n\t\t\t\tp.setPen(st::giftBoxTabFg);\n\t\t\t}\n\t\t\tbutton.text.draw(p, {\n\t\t\t\t.position = geometry.marginsRemoved(padding).topLeft(),\n\t\t\t\t.availableWidth = button.text.maxWidth(),\n\t\t\t});\n\t\t}\n\t}, buttons->lifetime());\n\n\tUi::AddSkip(container);\n\n\tconst auto computePrice = [session](CreditsAmount amount) {\n\t\treturn PriceAfterCommission(session, amount).value();\n\t};\n\tconst auto formatCommission = [session](CreditsAmount amount) {\n\t\treturn FormatAfterCommissionPercent(session, amount);\n\t};\n\tconst auto youGet = [=](rpl::producer<CreditsAmount> price, bool stars) {\n\t\treturn (stars\n\t\t\t? tr::lng_suggest_options_you_get_stars\n\t\t\t: tr::lng_suggest_options_you_get_ton)(\n\t\t\t\tlt_count_decimal,\n\t\t\t\trpl::duplicate(price) | rpl::map(computePrice),\n\t\t\t\tlt_percent,\n\t\t\t\trpl::duplicate(price) | rpl::map(formatCommission));\n\t};\n\tauto starsAbout = admin\n\t\t? rpl::combine(\n\t\t\tyouGet(StarsPriceValue(state->price.value()), true),\n\t\t\ttr::lng_suggest_options_stars_warning(tr::rich)\n\t\t) | rpl::map([=](const QString &t1, const TextWithEntities &t2) {\n\t\t\treturn TextWithEntities{ t1 }.append(\"\\n\\n\").append(t2);\n\t\t})\n\t\t: gift\n\t\t? tr::lng_gift_offer_stars_about(\n\t\t\tlt_name,\n\t\t\trpl::single(tr::marked(args.giftName)),\n\t\t\ttr::rich)\n\t\t: tr::lng_suggest_options_stars_price_about(tr::rich);\n\tauto tonAbout = admin\n\t\t? youGet(\n\t\t\tTonPriceValue(state->price.value()),\n\t\t\tfalse\n\t\t) | rpl::map(tr::rich)\n\t\t: gift\n\t\t? tr::lng_gift_offer_ton_about(\n\t\t\tlt_name,\n\t\t\trpl::single(tr::marked(args.giftName)),\n\t\t\ttr::rich)\n\t\t: tr::lng_suggest_options_ton_price_about(tr::rich);\n\tconst auto nanoTonMin = gift\n\t\t? appConfig.giftResaleNanoTonMin()\n\t\t: appConfig.suggestedPostNanoTonMin();\n\tconst auto nanoTonMax = gift\n\t\t? appConfig.giftResaleNanoTonMax()\n\t\t: appConfig.suggestedPostNanoTonMax();\n\tconst auto starsMin = gift\n\t\t? appConfig.giftResaleStarsMin()\n\t\t: appConfig.suggestedPostStarsMin();\n\tconst auto starsMax = gift\n\t\t? appConfig.giftResaleStarsMax()\n\t\t: appConfig.suggestedPostStarsMax();\n\tconst auto recordBadAmount = [=](CreditsAmount amount) {\n\t\tif (false\n\t\t\t|| (amount.ton()\n\t\t\t\t&& (amount.value()\n\t\t\t\t\t> (nanoTonMin + nanoTonMax) / (2. * Ui::kNanosInOne)))\n\t\t\t|| (!amount.ton()\n\t\t\t\t&& (amount.whole() >= starsMax))) {\n\t\t\tstate->lastSmallPrice = {};\n\t\t\treturn;\n\t\t}\n\t\tstate->lastSmallPrice = amount;\n\t};\n\tauto priceInput = AddStarsTonPriceInput(container, {\n\t\t.session = session,\n\t\t.showTon = state->ton.value(),\n\t\t.price = args.value.price(),\n\t\t.starsMin = starsMin,\n\t\t.starsMax = starsMax,\n\t\t.nanoTonMin = nanoTonMin,\n\t\t.nanoTonMax = nanoTonMax,\n\t\t.allowEmpty = !gift,\n\t\t.errorHook = recordBadAmount,\n\t\t.starsAbout = std::move(starsAbout),\n\t\t.tonAbout = std::move(tonAbout),\n\t});\n\tstate->price = std::move(priceInput.result);\n\tstate->computePrice = std::move(priceInput.computeResult);\n\tbox->setFocusCallback(std::move(priceInput.focusCallback));\n\n\tUi::AddSkip(container);\n\n\tif (gift) {\n\t\tconst auto day = 86400;\n\t\tauto durations = std::vector{\n\t\t\tday / 4,\n\t\t\tday / 2,\n\t\t\tday,\n\t\t\tday + day / 2,\n\t\t\tday * 2,\n\t\t\tday * 3,\n\t\t};\n\t\tif (peer->session().isTestMode()) {\n\t\t\tdurations.insert(begin(durations), 120);\n\t\t}\n\t\tconst auto durationToText = [](TimeId date) {\n\t\t\treturn (date >= 3600)\n\t\t\t\t? tr::lng_hours(tr::now, lt_count, date / 3600)\n\t\t\t\t: tr::lng_minutes(tr::now, lt_count, date / 60);\n\t\t};\n\t\tstate->offerDuration = day;\n\t\tconst auto duration = Settings::AddButtonWithLabel(\n\t\t\tcontainer,\n\t\t\ttr::lng_gift_offer_duration(),\n\t\t\tstate->offerDuration.value() | rpl::map(durationToText),\n\t\t\tst::settingsButtonNoIcon);\n\n\t\tduration->setClickedCallback([=] {\n\t\t\tbox->uiShow()->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\tconst auto save = [=](int index) {\n\t\t\t\t\tstate->offerDuration = durations[index];\n\t\t\t\t};\n\t\t\t\tauto options = durations\n\t\t\t\t\t| ranges::views::transform(durationToText)\n\t\t\t\t\t| ranges::to_vector;\n\t\t\t\tconst auto selected = ranges::find(\n\t\t\t\t\tdurations,\n\t\t\t\t\tstate->offerDuration.current());\n\t\t\t\tSingleChoiceBox(box, {\n\t\t\t\t\t.title = tr::lng_gift_offer_duration(),\n\t\t\t\t\t.options = options,\n\t\t\t\t\t.initialSelection = int(selected - begin(durations)),\n\t\t\t\t\t.callback = save,\n\t\t\t\t});\n\t\t\t}));\n\t\t});\n\n\t\tUi::AddSkip(container);\n\t\tUi::AddDividerText(\n\t\t\tcontainer,\n\t\t\ttr::lng_gift_offer_duration_about(\n\t\t\t\tlt_user,\n\t\t\t\trpl::single(peer->shortName())));\n\t} else {\n\t\tconst auto time = Settings::AddButtonWithLabel(\n\t\t\tcontainer,\n\t\t\ttr::lng_suggest_options_date(),\n\t\t\tstate->date.value() | rpl::map([](TimeId date) {\n\t\t\t\treturn date\n\t\t\t\t\t? langDateTime(base::unixtime::parse(date))\n\t\t\t\t\t: tr::lng_suggest_options_date_any(tr::now);\n\t\t\t}),\n\t\t\tst::settingsButtonNoIcon);\n\n\t\ttime->setClickedCallback([=] {\n\t\t\tconst auto weak = std::make_shared<\n\t\t\t\tbase::weak_qptr<Ui::BoxContent>\n\t\t\t>();\n\t\t\tconst auto parentWeak = base::make_weak(box);\n\t\t\tconst auto done = [=](TimeId result) {\n\t\t\t\tif (parentWeak) {\n\t\t\t\t\tstate->date = result;\n\t\t\t\t}\n\t\t\t\tif (const auto strong = weak->get()) {\n\t\t\t\t\tstrong->closeBox();\n\t\t\t\t}\n\t\t\t};\n\t\t\tauto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{\n\t\t\t\t.session = session,\n\t\t\t\t.done = done,\n\t\t\t\t.value = state->date.current(),\n\t\t\t\t.mode = args.mode,\n\t\t\t});\n\t\t\t*weak = dateBox.data();\n\t\t\tbox->uiShow()->show(std::move(dateBox));\n\t\t});\n\n\t\tUi::AddSkip(container);\n\t\tUi::AddDividerText(container, tr::lng_suggest_options_date_about());\n\t}\n\n\tstate->save = [=] {\n\t\tconst auto ton = uint32(state->ton.current() ? 1 : 0);\n\t\tconst auto price = state->computePrice();\n\t\tif (!price) {\n\t\t\tif (const auto amount = state->lastSmallPrice) {\n\t\t\t\tbox->uiShow()->showToast(amount->ton()\n\t\t\t\t\t? tr::lng_gift_sell_min_price_ton(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tnanoTonMin / float64(Ui::kNanosInOne),\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: tr::lng_gift_sell_min_price(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tstarsMin,\n\t\t\t\t\t\ttr::rich));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst auto value = *price;\n\t\tconst auto credits = &session->credits();\n\t\tif (!admin && ton) {\n\t\t\tif (!credits->tonLoaded()) {\n\t\t\t\tstate->savePending = true;\n\t\t\t\treturn;\n\t\t\t} else if (credits->tonBalance() < value) {\n\t\t\t\tbox->uiShow()->show(\n\t\t\t\t\tBox(Ui::InsufficientTonBox, session, value));\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tconst auto requiredStars = peer->starsPerMessageChecked()\n\t\t\t+ (ton ? 0 : int(base::SafeRound(value.value())));\n\t\tif (!admin && requiredStars) {\n\t\t\tif (!credits->loaded()) {\n\t\t\t\tstate->savePending = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (credits->balance() < CreditsAmount(requiredStars)) {\n\t\t\t\tusing namespace Settings;\n\t\t\t\tconst auto done = [=](SmallBalanceResult result) {\n\t\t\t\t\tif (result == SmallBalanceResult::Success\n\t\t\t\t\t\t|| result == SmallBalanceResult::Already) {\n\t\t\t\t\t\tstate->save();\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tconst auto source = gift\n\t\t\t\t\t? Settings::SmallBalanceSource(SmallBalanceForOffer())\n\t\t\t\t\t: SmallBalanceForSuggest{ usePeer->id };\n\t\t\t\tMaybeRequestBalanceIncrease(\n\t\t\t\t\tMain::MakeSessionShow(box->uiShow(), session),\n\t\t\t\t\trequiredStars,\n\t\t\t\t\tsource,\n\t\t\t\t\tdone);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\targs.done({\n\t\t\t.exists = true,\n\t\t\t.priceWhole = uint32(value.whole()),\n\t\t\t.priceNano = uint32(value.nano()),\n\t\t\t.ton = ton,\n\t\t\t.date = state->date.current(),\n\t\t\t.offerDuration = state->offerDuration.current(),\n\t\t});\n\t};\n\n\tconst auto credits = &session->credits();\n\trpl::combine(\n\t\tcredits->tonBalanceValue(),\n\t\tcredits->balanceValue()\n\t) | rpl::filter([=] {\n\t\treturn state->savePending;\n\t}) | rpl::on_next([=] {\n\t\tstate->savePending = false;\n\t\tif (const auto onstack = state->save) {\n\t\t\tonstack();\n\t\t}\n\t}, box->lifetime());\n\n\tstd::move(\n\t\tpriceInput.submits\n\t) | rpl::on_next(state->save, box->lifetime());\n\n\tauto helper = Ui::Text::CustomEmojiHelper();\n\tconst auto button = box->addButton(rpl::single(QString()), state->save);\n\tconst auto coloredTonIcon = helper.paletteDependent(\n\t\tUi::Earn::IconCurrencyEmoji());\n\tbutton->setContext(helper.context());\n\tbutton->setText(state->price.value(\n\t) | rpl::map([=](CreditsAmount price) {\n\t\tif (args.mode == SuggestMode::Change) {\n\t\t\treturn tr::lng_suggest_options_update(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked);\n\t\t} else if (price.empty()) {\n\t\t\treturn tr::lng_suggest_options_offer_free(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked);\n\t\t} else if (price.ton()) {\n\t\t\treturn tr::lng_suggest_options_offer(\n\t\t\t\ttr::now,\n\t\t\t\tlt_amount,\n\t\t\t\tUi::Text::IconEmoji(&st::tonIconEmoji).append(\n\t\t\t\t\tLang::FormatCreditsAmountDecimal(price)),\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn tr::lng_suggest_options_offer(\n\t\t\ttr::now,\n\t\t\tlt_amount,\n\t\t\tUi::Text::IconEmoji(&st::starIconEmoji).append(\n\t\t\t\tLang::FormatCreditsAmountDecimal(price)),\n\t\t\ttr::marked);\n\t}));\n\tconst auto buttonWidth = st::boxWidth\n\t\t- rect::m::sum::h(st::suggestPriceBox.buttonPadding);\n\tbutton->widthValue() | rpl::filter([=] {\n\t\treturn (button->widthNoMargins() != buttonWidth);\n\t}) | rpl::on_next([=] {\n\t\tbutton->resizeToWidth(buttonWidth);\n\t}, button->lifetime());\n\n\tif (admin) {\n\t\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t} else {\n\t\tconst auto close = Ui::CreateChild<Ui::IconButton>(\n\t\t\tcontainer,\n\t\t\tst::boxTitleClose);\n\t\tclose->setClickedCallback([=] { box->closeBox(); });\n\t\tcontainer->widthValue() | rpl::on_next([=](int) {\n\t\t\tclose->moveToRight(0, 0);\n\t\t}, close->lifetime());\n\n\t\tsession->credits().load(true);\n\t\tsession->credits().tonLoad(true);\n\t\tconst auto balance = Settings::AddBalanceWidget(\n\t\t\tcontainer,\n\t\t\tsession,\n\t\t\trpl::conditional(\n\t\t\t\tstate->ton.value(),\n\t\t\t\tsession->credits().tonBalanceValue(),\n\t\t\t\tsession->credits().balanceValue()),\n\t\t\tfalse);\n\t\trpl::combine(\n\t\t\tbalance->sizeValue(),\n\t\t\tcontainer->sizeValue()\n\t\t) | rpl::on_next([=](const QSize &, const QSize &) {\n\t\t\tbalance->moveToLeft(\n\t\t\t\tst::creditsHistoryRightSkip * 2,\n\t\t\t\tst::creditsHistoryRightSkip);\n\t\t\tbalance->update();\n\t\t}, balance->lifetime());\n\t}\n}\n\nbool CanEditSuggestedMessage(not_null<HistoryItem*> item) {\n\tconst auto media = item->media();\n\treturn !media || media->allowsEditCaption();\n}\n\nbool CanAddOfferToMessage(not_null<HistoryItem*> item) {\n\tconst auto history = item->history();\n\tconst auto broadcast = history->peer->monoforumBroadcast();\n\treturn broadcast\n\t\t&& !history->amMonoforumAdmin()\n\t\t&& !item->Get<HistoryMessageSuggestion>()\n\t\t&& !item->groupId()\n\t\t&& item->isRegular()\n\t\t&& !item->isService()\n\t\t&& !item->errorTextForForwardIgnoreRights(\n\t\t\thistory->owner().history(broadcast)).has_value();\n}\n\nCreditsAmount PriceAfterCommission(\n\t\tnot_null<Main::Session*> session,\n\t\tCreditsAmount price) {\n\tconst auto appConfig = &session->appConfig();\n\tconst auto mul = price.stars()\n\t\t? appConfig->suggestedPostCommissionStars()\n\t\t: appConfig->suggestedPostCommissionTon();\n\tconst auto exact = price.multiplied(mul / 1000.);\n\treturn price.stars()\n\t\t? CreditsAmount(exact.whole(), 0, CreditsType::Stars)\n\t\t: exact;\n}\n\nQString FormatAfterCommissionPercent(\n\t\tnot_null<Main::Session*> session,\n\t\tCreditsAmount price) {\n\tconst auto appConfig = &session->appConfig();\n\tconst auto mul = price.stars()\n\t\t? appConfig->suggestedPostCommissionStars()\n\t\t: appConfig->suggestedPostCommissionTon();\n\treturn QString::number(mul / 10.) + '%';\n}\n\nSuggestOptionsBar::SuggestOptionsBar(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<PeerData*> peer,\n\tSuggestOptions values,\n\tSuggestMode mode)\n: _show(std::move(show))\n, _peer(peer)\n, _mode(mode)\n, _values(values) {\n\tupdateTexts();\n}\n\nSuggestOptionsBar::~SuggestOptionsBar() = default;\n\nvoid SuggestOptionsBar::paintIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth) {\n\tst::historySuggestIconActive.paint(\n\t\tp,\n\t\tQPoint(x, y) + st::historySuggestIconPosition,\n\t\touterWidth);\n}\n\nvoid SuggestOptionsBar::paintBar(QPainter &p, int x, int y, int outerWidth) {\n\tpaintIcon(p, x, y, outerWidth);\n\tpaintLines(p, x + st::historyReplySkip, y, outerWidth);\n}\n\nvoid SuggestOptionsBar::paintLines(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth) {\n\tauto available = outerWidth\n\t\t- x\n\t\t- st::historyReplyCancel.width\n\t\t- st::msgReplyPadding.right();\n\tp.setPen(st::windowActiveTextFg);\n\t_title.draw(p, {\n\t\t.position = QPoint(x, y + st::msgReplyPadding.top()),\n\t\t.availableWidth = available,\n\t});\n\tp.setPen(st::windowSubTextFg);\n\t_text.draw(p, {\n\t\t.position = QPoint(\n\t\t\tx,\n\t\t\ty + st::msgReplyPadding.top() + st::msgServiceNameFont->height),\n\t\t.availableWidth = available,\n\t});\n}\n\nvoid SuggestOptionsBar::edit() {\n\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tconst auto apply = [=](SuggestOptions values) {\n\t\t_values = values;\n\t\tupdateTexts();\n\t\t_updates.fire({});\n\t\tif (const auto strong = weak->get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t};\n\t*weak = _show->show(Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{\n\t\t.peer = _peer,\n\t\t.done = apply,\n\t\t.value = _values,\n\t\t.mode = _mode,\n\t}));\n}\n\nvoid SuggestOptionsBar::updateTexts() {\n\t_title.setText(\n\t\tst::semiboldTextStyle,\n\t\t((_mode == SuggestMode::New)\n\t\t\t? tr::lng_suggest_bar_title(tr::now)\n\t\t\t: tr::lng_suggest_options_change(tr::now)));\n\n\tauto helper = Ui::Text::CustomEmojiHelper();\n\tconst auto text = composeText(helper);\n\t_text.setMarkedText(\n\t\tst::defaultTextStyle,\n\t\ttext,\n\t\tkMarkupTextOptions,\n\t\thelper.context());\n}\n\nTextWithEntities SuggestOptionsBar::composeText(\n\t\tUi::Text::CustomEmojiHelper &helper) const {\n\tconst auto amount = _values.price().ton()\n\t\t? helper.paletteDependent(Ui::Earn::IconCurrencyEmoji({\n\t\t\t.size = st::suggestBarTonIconSize,\n\t\t\t.margin = st::suggestBarTonIconMargins,\n\t\t})).append(Lang::FormatCreditsAmountDecimal(_values.price()))\n\t\t: helper.paletteDependent(\n\t\t\tUi::Earn::IconCreditsEmojiSmall()\n\t\t).append(Lang::FormatCreditsAmountDecimal(_values.price()));\n\tconst auto date = langDateTime(base::unixtime::parse(_values.date));\n\tif (!_values.price() && !_values.date) {\n\t\treturn tr::lng_suggest_bar_text(tr::now, tr::marked);\n\t} else if (!_values.date) {\n\t\treturn tr::lng_suggest_bar_priced(\n\t\t\ttr::now,\n\t\t\tlt_amount,\n\t\t\tamount,\n\t\t\ttr::marked);\n\t} else if (!_values.price()) {\n\t\treturn tr::lng_suggest_bar_dated(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\ttr::marked(date),\n\t\t\ttr::marked);\n\t}\n\treturn tr::marked().append(\n\t\tamount\n\t).append(\"   \").append(\n\t\tQString::fromUtf8(\"\\xf0\\x9f\\x93\\x86 \")\n\t).append(date);\n}\n\nSuggestOptions SuggestOptionsBar::values() const {\n\tauto result = _values;\n\tresult.exists = 1;\n\treturn result;\n}\n\nrpl::producer<> SuggestOptionsBar::updates() const {\n\treturn _updates.events();\n}\n\nrpl::lifetime &SuggestOptionsBar::lifetime() {\n\treturn _lifetime;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_common.h\"\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Ui {\nclass GenericBox;\nclass VerticalLayout;\nclass NumberInput;\nclass InputField;\n} // namespace Ui\n\nnamespace Ui::Text {\nclass CustomEmojiHelper;\n} // namespace Ui::Text\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace HistoryView {\n\nenum class SuggestMode {\n\tNew,\n\tChange,\n\tPublish,\n\tGift,\n};\n\nstruct SuggestTimeBoxArgs {\n\tnot_null<Main::Session*> session;\n\tFn<void(TimeId)> done;\n\tTimeId value = 0;\n\tSuggestMode mode = SuggestMode::New;\n};\nvoid ChooseSuggestTimeBox(\n\tnot_null<Ui::GenericBox*> box,\n\tSuggestTimeBoxArgs &&args);\n\nstruct StarsTonPriceInput {\n\tFn<void()> focusCallback;\n\tFn<std::optional<CreditsAmount>()> computeResult;\n\trpl::producer<> submits;\n\trpl::producer<> updates;\n\trpl::producer<CreditsAmount> result;\n};\n\nstruct StarsTonPriceArgs {\n\tnot_null<Main::Session*> session;\n\trpl::producer<bool> showTon;\n\tCreditsAmount price;\n\tint starsMin = 0;\n\tint starsMax = 0;\n\tint64 nanoTonMin = 0;\n\tint64 nanoTonMax = 0;\n\tbool allowEmpty = false;\n\tFn<void(CreditsAmount)> errorHook;\n\trpl::producer<TextWithEntities> starsAbout;\n\trpl::producer<TextWithEntities> tonAbout;\n};\n\n[[nodiscard]] StarsTonPriceInput AddStarsTonPriceInput(\n\tnot_null<Ui::VerticalLayout*> container,\n\tStarsTonPriceArgs &&args);\n\nstruct SuggestPriceBoxArgs {\n\tnot_null<PeerData*> peer;\n\tbool updating = false;\n\tFn<void(SuggestOptions)> done;\n\tSuggestOptions value;\n\tSuggestMode mode = SuggestMode::New;\n\tQString giftName;\n};\nvoid ChooseSuggestPriceBox(\n\tnot_null<Ui::GenericBox*> box,\n\tSuggestPriceBoxArgs &&args);\n\n[[nodiscard]] bool CanEditSuggestedMessage(not_null<HistoryItem*> item);\n\n[[nodiscard]] bool CanAddOfferToMessage(not_null<HistoryItem*> item);\n\n[[nodiscard]] CreditsAmount PriceAfterCommission(\n\tnot_null<Main::Session*> session,\n\tCreditsAmount price);\n[[nodiscard]] QString FormatAfterCommissionPercent(\n\tnot_null<Main::Session*> session,\n\tCreditsAmount price);\n\nclass SuggestOptionsBar final {\npublic:\n\tSuggestOptionsBar(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tSuggestOptions values,\n\t\tSuggestMode mode);\n\t~SuggestOptionsBar();\n\n\tvoid paintBar(QPainter &p, int x, int y, int outerWidth);\n\tvoid edit();\n\n\tvoid paintIcon(QPainter &p, int x, int y, int outerWidth);\n\tvoid paintLines(QPainter &p, int x, int y, int outerWidth);\n\n\t[[nodiscard]] SuggestOptions values() const;\n\n\t[[nodiscard]] rpl::producer<> updates() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tvoid updateTexts();\n\n\t[[nodiscard]] TextWithEntities composeText(\n\t\tUi::Text::CustomEmojiHelper &helper) const;\n\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst not_null<PeerData*> _peer;\n\tconst SuggestMode _mode = SuggestMode::New;\n\n\tUi::Text::String _title;\n\tUi::Text::String _text;\n\n\tSuggestOptions _values;\n\trpl::event_stream<> _updates;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_ttl_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/controls/history_view_ttl_button.h\"\n\n#include \"data/data_changes.h\"\n#include \"data/data_peer.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_ttl_validator.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView::Controls {\n\nTTLButton::TTLButton(\n\tnot_null<Ui::RpWidget*> parent,\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<PeerData*> peer)\n: _peer(peer)\n, _button(parent, st::historyMessagesTTL) {\n\n\tconst auto validator = TTLMenu::TTLValidator(std::move(show), peer);\n\t_button.setClickedCallback([=] {\n\t\tif (!validator.can()) {\n\t\t\tvalidator.showToast();\n\t\t\treturn;\n\t\t}\n\t\tvalidator.showBox();\n\t});\n\n\tpeer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::MessagesTTL\n\t) | rpl::on_next([=] {\n\t\t_button.setText(Ui::FormatTTLTiny(peer->messagesTTL()));\n\t}, _button.lifetime());\n}\n\nvoid TTLButton::show() {\n\t_button.show();\n}\n\nvoid TTLButton::hide() {\n\t_button.hide();\n}\n\nvoid TTLButton::setVisible(bool visible) {\n\t_button.setVisible(visible);\n}\n\nbool TTLButton::isVisible() const {\n\treturn _button.isVisible();\n}\n\nvoid TTLButton::move(int x, int y) {\n\t_button.move(x, y);\n}\n\nint TTLButton::width() const {\n\treturn _button.width();\n}\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_ttl_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/icon_button_with_text.h\"\n\nnamespace Ui {\nclass Show;\n} // namespace Ui\n\nnamespace HistoryView::Controls {\n\nclass TTLButton final {\npublic:\n\tTTLButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer);\n\n\t[[nodiscard]] not_null<PeerData*> peer() const {\n\t\treturn _peer;\n\t}\n\n\tvoid show();\n\tvoid hide();\n\tvoid setVisible(bool visible);\n\t[[nodiscard]] bool isVisible() const;\n\tvoid move(int x, int y);\n\n\t[[nodiscard]] int width() const;\n\nprivate:\n\tconst not_null<PeerData*> _peer;\n\tUi::IconButtonWithText _button;\n\n};\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/controls/history_view_voice_record_bar.h\"\n\n#include \"api/api_send_progress.h\"\n#include \"base/event_filter.h\"\n#include \"base/random.h\"\n#include \"base/unixtime.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"calls/calls_instance.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"core/application.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_session.h\"\n#include \"history/history_item_components.h\"\n#include \"history/view/controls/history_view_voice_record_button.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\" // MainWidget::stopAndClosePlayer\n#include \"mainwindow.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/audio/media_audio_edit.h\"\n#include \"media/audio/media_audio_capture.h\"\n#include \"media/player/media_player_button.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"media/streaming/media_streaming_instance.h\"\n#include \"media/streaming/media_streaming_round_preview.h\"\n#include \"storage/storage_account.h\"\n#include \"ui/controls/round_video_recorder.h\"\n#include \"ui/controls/send_button.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/effects/animation_value_f.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/painter.h\"\n#include \"ui/widgets/fields/input_field.h\" // ShouldSubmit.\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_media_player.h\"\n\n#include <tgcalls/VideoCaptureInterface.h>\n\nnamespace HistoryView::Controls {\nnamespace {\n\nconstexpr auto kAudioVoiceUpdateView = crl::time(200);\nconstexpr auto kAudioVoiceMaxLength = 100 * 60; // 100 minutes\nconstexpr auto kMaxSamples\n\t= ::Media::Player::kDefaultFrequency * kAudioVoiceMaxLength;\nconstexpr auto kMinSamples\n\t= ::Media::Player::kDefaultFrequency / 5; // 0.2 seconds\n\nconstexpr auto kPrecision = 10;\n\nconstexpr auto kLockArcAngle = 15.;\n\nconstexpr auto kHideWaveformBgOffset = 50;\nconstexpr auto kTrimPlaybackEpsilon = 0.0001;\n\nenum class FilterType {\n\tContinue,\n\tShowBox,\n\tCancel,\n};\n\nclass SoundedPreview final : public Ui::DynamicImage {\npublic:\n\tSoundedPreview(\n\t\tnot_null<DocumentData*> document,\n\t\trpl::producer<> repaints);\n\tstd::shared_ptr<DynamicImage> clone() override;\n\tQImage image(int size) override;\n\tvoid subscribeToUpdates(Fn<void()> callback) override;\n\nprivate:\n\tconst not_null<DocumentData*> _document;\n\tQImage _roundingMask;\n\tFn<void()> _repaint;\n\trpl::lifetime _lifetime;\n\n};\n\nSoundedPreview::SoundedPreview(\n\tnot_null<DocumentData*> document,\n\trpl::producer<> repaints)\n: _document(document) {\n\tstd::move(repaints) | rpl::on_next([=] {\n\t\tif (const auto onstack = _repaint) {\n\t\t\tonstack();\n\t\t}\n\t}, _lifetime);\n}\n\nstd::shared_ptr<Ui::DynamicImage> SoundedPreview::clone() {\n\tUnexpected(\"ListenWrap::videoPreview::clone.\");\n}\n\nQImage SoundedPreview::image(int size) {\n\tconst auto player = ::Media::Player::instance();\n\tconst auto streamed = player->roundVideoPreview(_document);\n\tif (!streamed) {\n\t\treturn {};\n\t}\n\n\tconst auto full = QSize(size, size) * style::DevicePixelRatio();\n\tif (_roundingMask.size() != full) {\n\t\t_roundingMask = Images::EllipseMask(full);\n\t}\n\tconst auto frame = streamed->frameWithInfo({\n\t\t.resize = full,\n\t\t.outer = full,\n\t\t.mask = _roundingMask,\n\t});\n\treturn frame.image;\n}\n\nvoid SoundedPreview::subscribeToUpdates(Fn<void()> callback) {\n\t_repaint = std::move(callback);\n}\n\n[[nodiscard]] auto Progress(int low, int high) {\n\treturn std::clamp(float64(low) / high, 0., 1.);\n}\n\n[[nodiscard]] auto FormatVoiceDuration(int samples) {\n\tconst int duration = kPrecision\n\t\t* (float64(samples) / ::Media::Player::kDefaultFrequency);\n\tconst auto durationString = Ui::FormatDurationText(duration / kPrecision);\n\tconst auto decimalPart = QString::number(duration % kPrecision);\n\treturn durationString + QLocale().decimalPoint() + decimalPart;\n}\n\n[[nodiscard]] QString FormatTrimDuration(crl::time duration) {\n\tauto result = Ui::FormatDurationText(duration / 1000);\n\tif ((result.size() == 5)\n\t\t&& (result[0] == QChar('0'))\n\t\t&& (result[2] == QChar(':'))) {\n\t\tresult.remove(0, 1);\n\t}\n\treturn result;\n}\n\n[[nodiscard]] std::unique_ptr<VoiceData> ProcessCaptureResult(\n\t\tconst VoiceWaveform &waveform) {\n\tauto voiceData = std::make_unique<VoiceData>();\n\tvoiceData->waveform = waveform;\n\tvoiceData->wavemax = voiceData->waveform.empty()\n\t\t? uchar(0)\n\t\t: *ranges::max_element(voiceData->waveform);\n\treturn voiceData;\n}\n\n[[nodiscard]] not_null<DocumentData*> DummyDocument(\n\t\tnot_null<Data::Session*> owner) {\n\treturn owner->document(\n\t\tbase::RandomValue<DocumentId>(),\n\t\tuint64(0),\n\t\tQByteArray(),\n\t\tbase::unixtime::now(),\n\t\tQVector<MTPDocumentAttribute>(),\n\t\tQString(),\n\t\tInlineImageLocation(),\n\t\tImageWithLocation(),\n\t\tImageWithLocation(),\n\t\tfalse, // isPremiumSticker\n\t\towner->session().mainDcId(),\n\t\tint32(0));\n}\n\n[[nodiscard]] VoiceWaveform ResampleWaveformToRange(\n\t\tconst VoiceWaveform &source,\n\t\tfloat64 left,\n\t\tfloat64 right) {\n\tif (source.isEmpty() || (source[0] < 0)) {\n\t\treturn {};\n\t}\n\tconst auto size = int(source.size());\n\tif (size <= 0) {\n\t\treturn {};\n\t}\n\tleft = std::clamp(left, 0., 1.);\n\tright = std::clamp(right, left, 1.);\n\tif ((right - left) <= 0.) {\n\t\treturn {};\n\t}\n\tconst auto begin = left * size;\n\tconst auto end = right * size;\n\tconst auto range = end - begin;\n\tif (range <= 0.) {\n\t\treturn {};\n\t}\n\tauto result = VoiceWaveform(size, 0);\n\tfor (auto i = 0; i != size; ++i) {\n\t\tconst auto segmentStart = begin + (range * i) / size;\n\t\tconst auto segmentEnd = begin + (range * (i + 1)) / size;\n\t\tconst auto from = std::clamp(\n\t\t\tint(std::floor(segmentStart)),\n\t\t\t0,\n\t\t\tsize - 1);\n\t\tconst auto till = std::clamp(\n\t\t\tint(std::ceil(segmentEnd)) - 1,\n\t\t\tfrom,\n\t\t\tsize - 1);\n\t\tauto peak = uchar(0);\n\t\tfor (auto j = from; j <= till; ++j) {\n\t\t\tpeak = std::max(peak, uchar(source[j]));\n\t\t}\n\t\tresult[i] = char(peak);\n\t}\n\treturn result;\n}\n\n[[nodiscard]] VoiceWaveform ResampleWaveformToSize(\n\t\tconst VoiceWaveform &source,\n\t\tint targetSize) {\n\tif (source.isEmpty() || (source[0] < 0) || (targetSize <= 0)) {\n\t\treturn {};\n\t}\n\tconst auto sourceSize = int(source.size());\n\tif (sourceSize <= 0) {\n\t\treturn {};\n\t}\n\tif (sourceSize == targetSize) {\n\t\treturn source;\n\t}\n\tauto result = VoiceWaveform(targetSize, 0);\n\tfor (auto i = 0; i != targetSize; ++i) {\n\t\tconst auto segmentStart = (float64(sourceSize) * i) / targetSize;\n\t\tconst auto segmentEnd = (float64(sourceSize) * (i + 1)) / targetSize;\n\t\tconst auto from = std::clamp(\n\t\t\tint(std::floor(segmentStart)),\n\t\t\t0,\n\t\t\tsourceSize - 1);\n\t\tconst auto till = std::clamp(\n\t\t\tint(std::ceil(segmentEnd)) - 1,\n\t\t\tfrom,\n\t\t\tsourceSize - 1);\n\t\tauto peak = uchar(0);\n\t\tfor (auto j = from; j <= till; ++j) {\n\t\t\tpeak = std::max(peak, uchar(source[j]));\n\t\t}\n\t\tresult[i] = char(peak);\n\t}\n\treturn result;\n}\n\n[[nodiscard]] VoiceWaveform MergeWaveformsByDuration(\n\t\tconst VoiceWaveform &first,\n\t\tcrl::time firstDuration,\n\t\tconst VoiceWaveform &second,\n\t\tcrl::time secondDuration) {\n\tconst auto totalDuration = firstDuration + secondDuration;\n\tif (totalDuration <= 0) {\n\t\treturn {};\n\t}\n\tconst auto targetSize = int(::Media::Player::kWaveformSamplesCount);\n\tif (targetSize <= 0) {\n\t\treturn {};\n\t}\n\tauto firstSize = int(\n\t\t((firstDuration * targetSize) + (totalDuration / 2))\n\t\t/ totalDuration);\n\tif ((firstDuration > 0) && (secondDuration > 0)) {\n\t\tfirstSize = std::clamp(firstSize, 1, targetSize - 1);\n\t} else {\n\t\tfirstSize = std::clamp(firstSize, 0, targetSize);\n\t}\n\tconst auto secondSize = targetSize - firstSize;\n\tauto result = VoiceWaveform();\n\tresult.reserve(targetSize);\n\tif (firstSize > 0) {\n\t\tconst auto part = ResampleWaveformToSize(first, firstSize);\n\t\tif (part.isEmpty()) {\n\t\t\treturn {};\n\t\t}\n\t\tfor (const auto value : part) {\n\t\t\tresult.push_back(value);\n\t\t}\n\t}\n\tif (secondSize > 0) {\n\t\tconst auto part = ResampleWaveformToSize(second, secondSize);\n\t\tif (part.isEmpty()) {\n\t\t\treturn {};\n\t\t}\n\t\tfor (const auto value : part) {\n\t\t\tresult.push_back(value);\n\t\t}\n\t}\n\treturn (int(result.size()) == targetSize) ? result : VoiceWaveform();\n}\n\n[[nodiscard]] Ui::RoundVideoResult ToRoundVideoResult(\n\t\t::Media::Capture::Result &&data) {\n\treturn Ui::RoundVideoResult{\n\t\t.content = std::move(data.bytes),\n\t\t.waveform = std::move(data.waveform),\n\t\t.duration = data.duration,\n\t};\n}\n\nvoid PaintWaveform(\n\t\tQPainter &p,\n\t\tnot_null<const VoiceData*> voiceData,\n\t\tint availableWidth,\n\t\tconst QColor &active,\n\t\tconst QColor &inactive,\n\t\tfloat64 progress) {\n\tconst auto wf = [&]() -> const VoiceWaveform* {\n\t\tif (voiceData->waveform.isEmpty()) {\n\t\t\treturn nullptr;\n\t\t} else if (voiceData->waveform.at(0) < 0) {\n\t\t\treturn nullptr;\n\t\t}\n\t\treturn &voiceData->waveform;\n\t}();\n\n\tconst auto samplesCount = wf\n\t\t? wf->size()\n\t\t: ::Media::Player::kWaveformSamplesCount;\n\tconst auto activeWidth = base::SafeRound(availableWidth * progress);\n\n\tconst auto &barWidth = st::historyRecordWaveformBar;\n\tconst auto barFullWidth = barWidth + st::msgWaveformSkip;\n\tconst auto totalBarsCountF = (float)availableWidth / barFullWidth;\n\tconst auto totalBarsCount = int(totalBarsCountF);\n\tconst auto samplesPerBar = samplesCount / totalBarsCountF;\n\tconst auto barNormValue = (wf ? voiceData->wavemax : 0) + 1;\n\tconst auto maxDelta = st::msgWaveformMax - st::msgWaveformMin;\n\tconst auto &bottom = st::msgWaveformMax;\n\n\tp.setPen(Qt::NoPen);\n\tint barNum = 0;\n\tconst auto paintBar = [&](const auto &barValue) {\n\t\tconst auto barHeight = st::msgWaveformMin + barValue;\n\t\tconst auto barTop = (bottom - barHeight) / 2.;\n\t\tconst auto barLeft = barNum * barFullWidth;\n\t\tconst auto rect = [&](const auto &l, const auto &w) {\n\t\t\treturn QRectF(l, barTop, w, barHeight);\n\t\t};\n\n\t\tif ((barLeft < activeWidth) && (barLeft + barWidth > activeWidth)) {\n\t\t\tconst auto leftWidth = activeWidth - barLeft;\n\t\t\tconst auto rightWidth = barWidth - leftWidth;\n\t\t\tp.fillRect(rect(barLeft, leftWidth), active);\n\t\t\tp.fillRect(rect(activeWidth, rightWidth), inactive);\n\t\t} else {\n\t\t\tconst auto &color = (barLeft >= activeWidth) ? inactive : active;\n\t\t\tp.fillRect(rect(barLeft, barWidth), color);\n\t\t}\n\t\tbarNum++;\n\t};\n\n\tauto barCounter = 0.;\n\tauto nextBarNum = 0;\n\n\tauto sum = 0;\n\tauto maxValue = 0;\n\n\tfor (auto i = 0; i < samplesCount; i++) {\n\t\tconst auto value = wf ? wf->at(i) : 0;\n\t\tif (i != nextBarNum) {\n\t\t\tmaxValue = std::max(maxValue, value);\n\t\t\tsum += totalBarsCount;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Compute height.\n\t\tsum += totalBarsCount - samplesCount;\n\t\tconst auto isSumSmaller = (sum < (totalBarsCount + 1) / 2);\n\t\tif (isSumSmaller) {\n\t\t\tmaxValue = std::max(maxValue, value);\n\t\t}\n\t\tconst auto barValue = ((maxValue * maxDelta) + (barNormValue / 2))\n\t\t\t/ barNormValue;\n\t\tmaxValue = isSumSmaller ? 0 : value;\n\n\t\tconst auto lastBarNum = nextBarNum;\n\t\twhile (lastBarNum == nextBarNum) {\n\t\t\tbarCounter += samplesPerBar;\n\t\t\tnextBarNum = (int)barCounter;\n\t\t\tpaintBar(barValue);\n\t\t}\n\t}\n}\n\nvoid FillWithMinithumbs(\n\t\tQPainter &p,\n\t\tnot_null<const Ui::RoundVideoResult*> data,\n\t\tQRect rect,\n\t\tfloat64 progress) {\n\tif (!data->minithumbsCount || !data->minithumbSize || rect.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto size = rect.height();\n\tconst auto single = data->minithumbSize;\n\tconst auto perrow = data->minithumbs.width() / single;\n\tconst auto thumbs = (rect.width() + size - 1) / size;\n\tif (!thumbs || !perrow) {\n\t\treturn;\n\t}\n\tfor (auto i = 0; i != thumbs - 1; ++i) {\n\t\tconst auto index = (i * data->minithumbsCount) / thumbs;\n\t\tp.drawImage(\n\t\t\tQRect(rect.x() + i * size, rect.y(), size, size),\n\t\t\tdata->minithumbs,\n\t\t\tQRect(\n\t\t\t\t(index % perrow) * single,\n\t\t\t\t(index / perrow) * single,\n\t\t\t\tsingle,\n\t\t\t\tsingle));\n\t}\n\tconst auto last = rect.width() - (thumbs - 1) * size;\n\tconst auto index = ((thumbs - 1) * data->minithumbsCount) / thumbs;\n\tp.drawImage(\n\t\tQRect(rect.x() + (thumbs - 1) * size, rect.y(), last, size),\n\t\tdata->minithumbs,\n\t\tQRect(\n\t\t\t(index % perrow) * single,\n\t\t\t(index / perrow) * single,\n\t\t\t(last * single) / size,\n\t\t\tsingle));\n}\n\n[[nodiscard]] QRect DrawLockCircle(\n\t\tQPainter &p,\n\t\tconst QRect &widgetRect,\n\t\tconst style::RecordBarLock &st,\n\t\tfloat64 progress) {\n\tconst auto &originTop = st.originTop;\n\tconst auto &originBottom = st.originBottom;\n\tconst auto &originBody = st.originBody;\n\tconst auto &shadowTop = st.shadowTop;\n\tconst auto &shadowBottom = st.shadowBottom;\n\tconst auto &shadowBody = st.shadowBody;\n\tconst auto &shadowMargins = st::historyRecordLockMargin;\n\n\tconst auto bottomMargin = anim::interpolate(\n\t\t0,\n\t\twidgetRect.height() - shadowTop.height() - shadowBottom.height(),\n\t\tprogress);\n\n\tconst auto topMargin = anim::interpolate(\n\t\twidgetRect.height() / 4,\n\t\t0,\n\t\tprogress);\n\n\tconst auto full = widgetRect - QMargins(0, topMargin, 0, bottomMargin);\n\tconst auto inner = full - shadowMargins;\n\tconst auto content = inner\n\t\t- style::margins(0, originTop.height(), 0, originBottom.height());\n\tconst auto contentShadow = full\n\t\t- style::margins(0, shadowTop.height(), 0, shadowBottom.height());\n\n\tconst auto w = full.width();\n\t{\n\t\tshadowTop.paint(p, full.topLeft(), w);\n\t\toriginTop.paint(p, inner.topLeft(), w);\n\t}\n\t{\n\t\tconst auto shadowPos = QPoint(\n\t\t\tfull.x(),\n\t\t\tcontentShadow.y() + contentShadow.height());\n\t\tconst auto originPos = QPoint(\n\t\t\tinner.x(),\n\t\t\tcontent.y() + content.height());\n\t\tshadowBottom.paint(p, shadowPos, w);\n\t\toriginBottom.paint(p, originPos, w);\n\t}\n\t{\n\t\tshadowBody.fill(p, contentShadow);\n\t\toriginBody.fill(p, content);\n\t}\n\tif (progress < 1.) {\n\t\tconst auto &arrow = st.arrow;\n\t\tconst auto arrowRect = QRect(\n\t\t\tinner.x(),\n\t\t\tcontent.y() + content.height() - arrow.height() / 2,\n\t\t\tinner.width(),\n\t\t\tarrow.height());\n\t\tp.setOpacity(1. - progress);\n\t\tarrow.paintInCenter(p, arrowRect);\n\t\tp.setOpacity(1.);\n\t}\n\n\treturn inner;\n}\n\nclass TTLButton final : public Ui::RippleButton {\npublic:\n\tTTLButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::RecordBar &st,\n\t\tbool recordingVideo);\n\n\tvoid clearState() override;\n\nprotected:\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\nprivate:\n\tconst style::RecordBar &_st;\n\tconst QRect _rippleRect;\n\n\tUi::Animations::Simple _activeAnimation;\n\tbase::unique_qptr<Ui::ImportantTooltip> _tooltip;\n\n};\n\nTTLButton::TTLButton(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst style::RecordBar &st,\n\tbool recordingVideo)\n: RippleButton(parent, st.lock.ripple)\n, _st(st)\n, _rippleRect(Rect(Size(st::historyRecordLockTopShadow.width()))\n\t- (st::historyRecordLockRippleMargin)) {\n\tQWidget::resize(Size(st::historyRecordLockTopShadow.width()));\n\tUi::AbstractButton::setDisabled(true);\n\n\tUi::AbstractButton::setClickedCallback([=] {\n\t\tUi::AbstractButton::setDisabled(!Ui::AbstractButton::isDisabled());\n\t\tconst auto isActive = !Ui::AbstractButton::isDisabled();\n\t\t_activeAnimation.start(\n\t\t\t[=] { update(); },\n\t\t\tisActive ? 0. : 1.,\n\t\t\tisActive ? 1. : 0.,\n\t\t\tst::universalDuration);\n\t});\n\n\tUi::RpWidget::shownValue() | rpl::on_next([=](bool shown) {\n\t\tif (!shown) {\n\t\t\t_tooltip = nullptr;\n\t\t\treturn;\n\t\t} else if (_tooltip) {\n\t\t\treturn;\n\t\t}\n\t\tauto text = rpl::conditional(\n\t\t\tCore::App().settings().ttlVoiceClickTooltipHiddenValue(),\n\t\t\t(recordingVideo\n\t\t\t\t? tr::lng_record_once_active_video\n\t\t\t\t: tr::lng_record_once_active_tooltip)(\n\t\t\t\t\ttr::rich),\n\t\t\ttr::lng_record_once_first_tooltip(\n\t\t\t\ttr::rich));\n\t\t_tooltip.reset(Ui::CreateChild<Ui::ImportantTooltip>(\n\t\t\tparent.get(),\n\t\t\tobject_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\t\t\tparent.get(),\n\t\t\t\tUi::MakeNiceTooltipLabel(\n\t\t\t\t\tparent,\n\t\t\t\t\tstd::move(text),\n\t\t\t\t\tst::historyMessagesTTLLabel.minWidth,\n\t\t\t\t\tst::ttlMediaImportantTooltipLabel),\n\t\t\t\tst::defaultImportantTooltip.padding),\n\t\t\tst::historyRecordTooltip));\n\t\tUi::RpWidget::geometryValue(\n\t\t) | rpl::on_next([=](const QRect &r) {\n\t\t\tif (r.isEmpty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_tooltip->pointAt(r, RectPart::Right, [=](QSize size) {\n\t\t\t\treturn QPoint(\n\t\t\t\t\tr.left()\n\t\t\t\t\t\t- size.width()\n\t\t\t\t\t\t- st::defaultImportantTooltip.padding.left(),\n\t\t\t\t\tr.top()\n\t\t\t\t\t\t+ r.height()\n\t\t\t\t\t\t- size.height()\n\t\t\t\t\t\t- st::historyRecordTooltipSkip\n\t\t\t\t\t\t+ st::historyRecordTooltip.padding.top());\n\t\t\t});\n\t\t}, _tooltip->lifetime());\n\t\t_tooltip->show();\n\t\tif (!Core::App().settings().ttlVoiceClickTooltipHidden()) {\n\t\t\tclicks(\n\t\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t\tCore::App().settings().setTtlVoiceClickTooltipHidden(true);\n\t\t\t}, _tooltip->lifetime());\n\t\t\t_tooltip->toggleAnimated(true);\n\t\t} else {\n\t\t\t_tooltip->toggleFast(false);\n\t\t}\n\n\t\tclicks(\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto toggled = !Ui::AbstractButton::isDisabled();\n\t\t\t_tooltip->toggleAnimated(toggled);\n\n\t\t\tif (toggled) {\n\t\t\t\tconstexpr auto kTimeout = crl::time(3000);\n\t\t\t\t_tooltip->hideAfter(kTimeout);\n\t\t\t}\n\t\t}, _tooltip->lifetime());\n\n\t\tUi::RpWidget::geometryValue(\n\t\t) | rpl::map([=](const QRect &r) {\n\t\t\treturn (r.left() + r.width() > parentWidget()->width());\n\t\t}) | rpl::distinct_until_changed(\n\t\t) | rpl::on_next([=](bool toHide) {\n\t\t\tconst auto isFirstTooltip\n\t\t\t\t= !Core::App().settings().ttlVoiceClickTooltipHidden();\n\t\t\tif (isFirstTooltip || toHide) {\n\t\t\t\t_tooltip->toggleAnimated(!toHide);\n\t\t\t}\n\t\t}, _tooltip->lifetime());\n\t}, lifetime());\n\n\tpaintRequest(\n\t) | rpl::on_next([=](const QRect &clip) {\n\t\tauto p = QPainter(this);\n\n\t\tconst auto inner = DrawLockCircle(p, rect(), _st.lock, 1.);\n\n\t\tUi::RippleButton::paintRipple(p, _rippleRect.x(), _rippleRect.y());\n\n\t\tconst auto activeProgress = _activeAnimation.value(\n\t\t\t!Ui::AbstractButton::isDisabled() ? 1 : 0);\n\n\t\tp.setOpacity(1. - activeProgress);\n\t\tst::historyRecordVoiceOnceInactive.paintInCenter(p, inner);\n\n\t\tif (activeProgress) {\n\t\t\tp.setOpacity(activeProgress);\n\t\t\tst::historyRecordVoiceOnceBg.paintInCenter(p, inner);\n\t\t\tst::historyRecordVoiceOnceFg.paintInCenter(p, inner);\n\t\t}\n\n\t}, lifetime());\n\tsetAccessibleName((recordingVideo\n\t\t? tr::lng_in_dlg_video_message_ttl\n\t\t: tr::lng_in_dlg_voice_message_ttl)(tr::now));\n}\n\nvoid TTLButton::clearState() {\n\tUi::AbstractButton::setDisabled(true);\n\tQWidget::update();\n\tUi::RpWidget::hide();\n}\n\nQImage TTLButton::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::EllipseMask(_rippleRect.size());\n}\n\nQPoint TTLButton::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos()) - _rippleRect.topLeft();\n}\n\n} // namespace\n\nclass ListenWrap final {\npublic:\n\tListenWrap(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::RecordBar &st,\n\t\tstd::shared_ptr<Ui::SendButton> send,\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<Ui::RoundVideoResult*> data,\n\t\tbool allowTrim,\n\t\tconst style::font &font);\n\n\tvoid requestPaintProgress(float64 progress);\n\tvoid prepareForSendAnimation();\n\t[[nodiscard]] rpl::producer<> stopRequests() const;\n\tvoid applyTrimBeforeSend();\n\n\tvoid playPause();\n\t[[nodiscard]] std::shared_ptr<Ui::DynamicImage> videoPreview();\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tstruct TrimGeometry {\n\t\tQRect frame;\n\t\tQRect leftHandle;\n\t\tQRect rightHandle;\n\t};\n\n\tstruct TrimRange {\n\t\tcrl::time from = 0;\n\t\tcrl::time till = 0;\n\t};\n\n\tstruct TrimBoundaries {\n\t\tfloat64 left = 0.;\n\t\tfloat64 right = 1.;\n\t};\n\n\tvoid init();\n\tvoid initPlayButton();\n\tvoid initPlayProgress();\n\tvoid applyTrimSelection(bool resetSelection);\n\tvoid updateControlGeometry();\n\tvoid updateTrimGeometry();\n\t[[nodiscard]] TrimGeometry computeTrimGeometry(\n\t\tconst QRect &trimRect) const;\n\tvoid updateDurationText();\n\n\t[[nodiscard]] bool isInPlayer(\n\t\tconst ::Media::Player::TrackState &state) const;\n\t[[nodiscard]] bool isInPlayer() const;\n\t[[nodiscard]] bool canTrim() const;\n\t[[nodiscard]] float64 trimProgressFromPosition(int x) const;\n\t[[nodiscard]] float64 minimumTrimProgress() const;\n\t[[nodiscard]] float64 minimumControlTrimProgress() const;\n\t[[nodiscard]] crl::time selectedDuration() const;\n\t[[nodiscard]] std::optional<TrimRange> selectedTrimRange() const;\n\t[[nodiscard]] TrimBoundaries selectedTrimBoundaries() const;\n\n\t[[nodiscard]] int computeTopMargin(int height) const;\n\t[[nodiscard]] QRect computeWaveformRect(const QRect &centerRect) const;\n\n\tconst not_null<Ui::RpWidget*> _parent;\n\n\tconst style::RecordBar &_st;\n\tconst std::shared_ptr<Ui::SendButton> _send;\n\tconst not_null<Main::Session*> _session;\n\tconst not_null<DocumentData*> _document;\n\tconst std::unique_ptr<VoiceData> _voiceData;\n\tconst std::shared_ptr<Data::DocumentMedia> _mediaView;\n\tconst not_null<Ui::RoundVideoResult*> _data;\n\tconst bool _allowTrim = false;\n\tconst base::unique_qptr<Ui::IconButton> _delete;\n\tconst style::font &_durationFont;\n\tQString _duration;\n\tint _durationWidth = 0;\n\tconst style::MediaPlayerButton &_playPauseSt;\n\tconst base::unique_qptr<Ui::AbstractButton> _playPauseButton;\n\tconst QColor _activeWaveformBar;\n\tconst QColor _inactiveWaveformBar;\n\n\tbool _isShowAnimation = true;\n\n\tQRect _waveformBgRect;\n\tQRect _waveformBgFinalCenterRect;\n\tQRect _waveformFgRect;\n\tQRect _controlRect;\n\tbool _controlHasDuration = true;\n\tQRect _trimFrameRect;\n\tQRect _trimLeftHandleRect;\n\tQRect _trimRightHandleRect;\n\tfloat64 _trimLeftProgress = 0.;\n\tfloat64 _trimRightProgress = 1.;\n\n\t::Media::Player::PlayButtonLayout _playPause;\n\n\tanim::value _playProgress;\n\n\trpl::variable<float64> _showProgress = 0.;\n\trpl::event_stream<> _videoRepaints;\n\tQImage _sendAnimationCache;\n\tbool _useSendAnimationCache = false;\n\tbool _playPauseHiddenForSendAnimation = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\nListenWrap::ListenWrap(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst style::RecordBar &st,\n\tstd::shared_ptr<Ui::SendButton> send,\n\tnot_null<Main::Session*> session,\n\tnot_null<Ui::RoundVideoResult*> data,\n\tbool allowTrim,\n\tconst style::font &font)\n: _parent(parent)\n, _st(st)\n, _send(send)\n, _session(session)\n, _document(DummyDocument(&session->data()))\n, _voiceData(ProcessCaptureResult(data->waveform))\n, _mediaView(_document->createMediaView())\n, _data(data)\n, _allowTrim(allowTrim)\n, _delete(base::make_unique_q<Ui::IconButton>(parent, _st.remove))\n, _durationFont(font)\n, _duration(FormatTrimDuration(_data->duration))\n, _durationWidth(_durationFont->width(_duration))\n, _playPauseSt(st::mediaPlayerButton)\n, _playPauseButton(base::make_unique_q<Ui::AbstractButton>(parent))\n, _activeWaveformBar(st::historyRecordVoiceFgActiveIcon->c)\n, _inactiveWaveformBar(\n\tanim::with_alpha(\n\t\t_activeWaveformBar,\n\t\tst::historyRecordWaveformInactiveAlpha))\n, _playPause(_playPauseSt, [=] { _playPauseButton->update(); }) {\n\t_delete->setAccessibleName(tr::lng_record_lock_delete(tr::now));\n\tinit();\n}\n\nvoid ListenWrap::init() {\n\tauto deleteShow = _showProgress.value(\n\t) | rpl::map([](auto value) {\n\t\treturn value == 1.;\n\t}) | rpl::distinct_until_changed();\n\t_delete->showOn(std::move(deleteShow));\n\n\trpl::combine(\n\t\t_parent->sizeValue(),\n\t\t_send->widthValue()\n\t) | rpl::on_next([=](QSize size, int send) {\n\t\t_waveformBgRect = QRect({ 0, 0 }, size)\n\t\t\t.marginsRemoved(st::historyRecordWaveformBgMargins);\n\t\t{\n\t\t\tconst auto left = _st.remove.width;\n\t\t\tconst auto right = send;\n\t\t\t_waveformBgFinalCenterRect = _waveformBgRect.marginsRemoved(\n\t\t\t\tstyle::margins(left, 0, right, 0));\n\t\t}\n\t\t_waveformFgRect = computeWaveformRect(_waveformBgFinalCenterRect);\n\t\tupdateTrimGeometry();\n\t\tupdateControlGeometry();\n\t}, _lifetime);\n\n\t_parent->paintRequest(\n\t) | rpl::on_next([=](const QRect &clip) {\n\t\tauto p = QPainter(_parent);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto progress = _showProgress.current();\n\t\tconst auto useSendAnimationCache = _useSendAnimationCache\n\t\t\t&& !_sendAnimationCache.isNull();\n\t\tp.setOpacity(progress);\n\t\tconst auto &remove = _st.remove;\n\t\tif (progress > 0. && progress < 1. && !useSendAnimationCache) {\n\t\t\tremove.icon.paint(p, remove.iconPosition, _parent->width());\n\t\t}\n\n\t\t{\n\t\t\tconst auto hideOffset = _isShowAnimation\n\t\t\t\t? 0\n\t\t\t\t: anim::interpolate(kHideWaveformBgOffset, 0, progress);\n\t\t\tconst auto deleteIconLeft = remove.iconPosition.x();\n\t\t\tconst auto bgRectRight = anim::interpolate(\n\t\t\t\tdeleteIconLeft,\n\t\t\t\t_send->width(),\n\t\t\t\t_isShowAnimation ? progress : 1.);\n\t\t\tconst auto bgRectLeft = anim::interpolate(\n\t\t\t\t_parent->width() - deleteIconLeft - _waveformBgRect.height(),\n\t\t\t\tremove.width,\n\t\t\t\t_isShowAnimation ? progress : 1.);\n\t\t\tconst auto bgRectMargins = style::margins(\n\t\t\t\tbgRectLeft - hideOffset,\n\t\t\t\t0,\n\t\t\t\tbgRectRight + hideOffset,\n\t\t\t\t0);\n\t\t\tconst auto bgRect = _waveformBgRect.marginsRemoved(bgRectMargins);\n\t\t\tconst auto radius = st::historyRecordWaveformBgRadius;\n\t\t\tconst auto bgCenterRect = bgRect.marginsRemoved(\n\t\t\t\tstyle::margins(radius, 0, radius, 0));\n\t\t\tif (useSendAnimationCache) {\n\t\t\t\tp.save();\n\t\t\t\tp.setClipRect(bgRect);\n\t\t\t\tp.drawImage(bgRect.topLeft(), _sendAnimationCache);\n\t\t\t\tp.restore();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto trimGeometry = (progress == 1.)\n\t\t\t\t? TrimGeometry{\n\t\t\t\t\t.frame = _trimFrameRect,\n\t\t\t\t\t.leftHandle = _trimLeftHandleRect,\n\t\t\t\t\t.rightHandle = _trimRightHandleRect,\n\t\t\t\t}\n\t\t\t\t: computeTrimGeometry(bgCenterRect);\n\t\t\tconst auto &trimFrameRect = trimGeometry.frame;\n\t\t\tconst auto &trimLeftHandleRect = trimGeometry.leftHandle;\n\t\t\tconst auto &trimRightHandleRect = trimGeometry.rightHandle;\n\n\t\t\tif (!_isShowAnimation) {\n\t\t\t\tp.setOpacity(progress);\n\t\t\t} else {\n\t\t\t\tp.fillRect(bgRect, _st.bg);\n\t\t\t}\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(anim::with_alpha(\n\t\t\t\t_st.cancelActive->c,\n\t\t\t\tst::historyRecordWaveformOutsideAlpha));\n\t\t\tp.drawRoundedRect(bgRect, radius, radius);\n\t\t\tif (canTrim() && !trimFrameRect.isEmpty()) {\n\t\t\t\tconst auto activeBgRect = trimFrameRect.intersected(bgRect);\n\t\t\t\tauto clipPath = QPainterPath();\n\t\t\t\tclipPath.addRoundedRect(bgRect, radius, radius);\n\t\t\t\tp.save();\n\t\t\t\tp.setClipPath(clipPath);\n\t\t\t\tif (activeBgRect.isEmpty()) {\n\t\t\t\t\tp.fillRect(bgRect, _st.cancelActive);\n\t\t\t\t} else {\n\t\t\t\t\tif (activeBgRect.x() > bgRect.x()) {\n\t\t\t\t\t\tp.fillRect(\n\t\t\t\t\t\t\tQRect(\n\t\t\t\t\t\t\t\tbgRect.x(),\n\t\t\t\t\t\t\t\tbgRect.y(),\n\t\t\t\t\t\t\t\tactiveBgRect.x() - bgRect.x(),\n\t\t\t\t\t\t\t\tbgRect.height()),\n\t\t\t\t\t\t\t_st.cancelActive);\n\t\t\t\t\t}\n\t\t\t\t\tif (rect::right(activeBgRect) < rect::right(bgRect)) {\n\t\t\t\t\t\tp.fillRect(\n\t\t\t\t\t\t\tQRect(\n\t\t\t\t\t\t\t\trect::right(activeBgRect) + 1,\n\t\t\t\t\t\t\t\tbgRect.y(),\n\t\t\t\t\t\t\t\trect::right(bgRect)\n\t\t\t\t\t\t\t\t\t- rect::right(activeBgRect),\n\t\t\t\t\t\t\t\tbgRect.height()),\n\t\t\t\t\t\t\t_st.cancelActive);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tp.restore();\n\t\t\t}\n\n\t\t\t// Waveform paint.\n\t\t\tconst auto waveformRect = (progress == 1.)\n\t\t\t\t? _waveformFgRect\n\t\t\t\t: computeWaveformRect(bgCenterRect);\n\t\t\tif (!waveformRect.isEmpty()) {\n\t\t\t\tconst auto playProgress = _playProgress.current();\n\t\t\t\tconst auto paintWaveform = [&](\n\t\t\t\t\t\tQRect rect,\n\t\t\t\t\t\tfloat64 opacity,\n\t\t\t\t\t\tconst QColor &activeBar,\n\t\t\t\t\t\tconst QColor &inactiveBar) {\n\t\t\t\t\tif (rect.isEmpty() || (opacity <= 0.)) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tp.save();\n\t\t\t\t\tp.setClipRect(rect);\n\t\t\t\t\tp.setOpacity(p.opacity() * opacity);\n\t\t\t\t\tif (_data->minithumbs.isNull()) {\n\t\t\t\t\t\tp.translate(waveformRect.topLeft());\n\t\t\t\t\t\tPaintWaveform(\n\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\t_voiceData.get(),\n\t\t\t\t\t\t\twaveformRect.width(),\n\t\t\t\t\t\t\tactiveBar,\n\t\t\t\t\t\t\tinactiveBar,\n\t\t\t\t\t\t\tplayProgress);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tFillWithMinithumbs(\n\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\t_data,\n\t\t\t\t\t\t\twaveformRect,\n\t\t\t\t\t\t\tplayProgress);\n\t\t\t\t\t}\n\t\t\t\t\tp.restore();\n\t\t\t\t};\n\t\t\t\tconst auto activeWaveformRect = trimFrameRect.intersected(\n\t\t\t\t\twaveformRect);\n\t\t\t\tif (canTrim() && !activeWaveformRect.isEmpty()) {\n\t\t\t\t\tconst auto outsideOpacity = std::clamp(\n\t\t\t\t\t\tst::historyRecordWaveformOutsideAlpha,\n\t\t\t\t\t\t0.,\n\t\t\t\t\t\t1.);\n\t\t\t\t\tpaintWaveform(\n\t\t\t\t\t\tactiveWaveformRect,\n\t\t\t\t\t\t1.,\n\t\t\t\t\t\t_activeWaveformBar,\n\t\t\t\t\t\t_inactiveWaveformBar);\n\t\t\t\t\tpaintWaveform(\n\t\t\t\t\t\tQRect(\n\t\t\t\t\t\t\twaveformRect.x(),\n\t\t\t\t\t\t\twaveformRect.y(),\n\t\t\t\t\t\t\tstd::max(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tactiveWaveformRect.x() - waveformRect.x()),\n\t\t\t\t\t\t\twaveformRect.height()),\n\t\t\t\t\t\toutsideOpacity,\n\t\t\t\t\t\t_inactiveWaveformBar,\n\t\t\t\t\t\t_inactiveWaveformBar);\n\t\t\t\t\tpaintWaveform(\n\t\t\t\t\t\tQRect(\n\t\t\t\t\t\t\trect::right(activeWaveformRect) + 1,\n\t\t\t\t\t\t\twaveformRect.y(),\n\t\t\t\t\t\t\tstd::max(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\trect::right(waveformRect)\n\t\t\t\t\t\t\t\t\t- rect::right(activeWaveformRect)),\n\t\t\t\t\t\t\twaveformRect.height()),\n\t\t\t\t\t\toutsideOpacity,\n\t\t\t\t\t\t_inactiveWaveformBar,\n\t\t\t\t\t\t_inactiveWaveformBar);\n\t\t\t\t} else {\n\t\t\t\t\tpaintWaveform(\n\t\t\t\t\t\twaveformRect,\n\t\t\t\t\t\t1.,\n\t\t\t\t\t\t_activeWaveformBar,\n\t\t\t\t\t\t_inactiveWaveformBar);\n\t\t\t\t}\n\t\t\t\tif (canTrim() && !trimFrameRect.isEmpty()) {\n\t\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\t\tconst auto inner = st::historyRecordTrimHandleInnerSize;\n\t\t\t\t\tconst auto drawInner = [&](const QRect &handle) {\n\t\t\t\t\t\tconst auto width = std::min(\n\t\t\t\t\t\t\tinner.width(),\n\t\t\t\t\t\t\thandle.width());\n\t\t\t\t\t\tconst auto height = std::min(\n\t\t\t\t\t\t\tinner.height(),\n\t\t\t\t\t\t\thandle.height());\n\t\t\t\t\t\tconst auto x = handle.x()\n\t\t\t\t\t\t\t+ (handle.width() - width) / 2;\n\t\t\t\t\t\tconst auto y = handle.y()\n\t\t\t\t\t\t\t+ (handle.height() - height) / 2;\n\t\t\t\t\t\tp.drawRoundedRect(\n\t\t\t\t\t\t\tQRect(x, y, width, height),\n\t\t\t\t\t\t\twidth / 2.,\n\t\t\t\t\t\t\twidth / 2.);\n\t\t\t\t\t};\n\t\t\t\t\tp.setBrush(_activeWaveformBar);\n\t\t\t\t\tdrawInner(trimLeftHandleRect);\n\t\t\t\t\tdrawInner(trimRightHandleRect);\n\t\t\t\t\tp.setBrush(_st.bg);\n\t\t\t\t\tconst auto lineTop = trimLeftHandleRect.y();\n\t\t\t\t\tconst auto lineHeight = trimLeftHandleRect.height();\n\t\t\t\t\tconst auto leftLineX = trimFrameRect.x();\n\t\t\t\t\tconst auto rightLineX = rect::right(trimFrameRect);\n\t\t\t\t\tif (lineHeight > 0) {\n\t\t\t\t\t\tp.fillRect(leftLineX, lineTop, 1, lineHeight, _st.bg);\n\t\t\t\t\t\tif (rightLineX != leftLineX) {\n\t\t\t\t\t\t\tp.fillRect(\n\t\t\t\t\t\t\t\trightLineX,\n\t\t\t\t\t\t\t\tlineTop,\n\t\t\t\t\t\t\t\t1,\n\t\t\t\t\t\t\t\tlineHeight,\n\t\t\t\t\t\t\t\t_st.bg);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (!_controlRect.isEmpty()) {\n\t\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\t\tp.setBrush(_st.cancelActive);\n\t\t\t\t\tp.drawRoundedRect(\n\t\t\t\t\t\t_controlRect,\n\t\t\t\t\t\t_controlRect.height() / 2.,\n\t\t\t\t\t\t_controlRect.height() / 2.);\n\n\t\t\t\t\tif (_controlHasDuration) {\n\t\t\t\t\t\tp.setFont(_durationFont);\n\t\t\t\t\t\tp.setPen(st::historyRecordVoiceFgActiveIcon);\n\t\t\t\t\t\tconst auto ascent = _durationFont->ascent;\n\t\t\t\t\t\tconst auto left = rect::right(_playPauseButton)\n\t\t\t\t\t\t\t/*+ st::historyRecordCenterControlTextSkip*/;\n\t\t\t\t\t\tconst auto top = _controlRect.y()\n\t\t\t\t\t\t\t+ (_controlRect.height() - ascent) / 2;\n\t\t\t\t\t\tp.drawText(\n\t\t\t\t\t\t\tQRect(left, top, _durationWidth, ascent),\n\t\t\t\t\t\t\tstyle::al_left,\n\t\t\t\t\t\t\t_duration);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n\n\tinitPlayButton();\n\tinitPlayProgress();\n}\n\nvoid ListenWrap::initPlayButton() {\n\tusing namespace ::Media::Player;\n\tusing State = TrackState;\n\n\t_mediaView->setBytes(_data->content);\n\t_document->size = _data->content.size();\n\t_document->type = _data->minithumbs.isNull()\n\t\t? VoiceDocument\n\t\t: RoundVideoDocument;\n\n\tconst auto &play = _playPauseSt.playOuter;\n\tupdateControlGeometry();\n\t_playPauseButton->show();\n\t_playPauseButton->setAccessibleName(tr::lng_record_lock_play(tr::now));\n\n\t_playPauseButton->paintRequest(\n\t) | rpl::on_next([=](const QRect &clip) {\n\t\tauto p = QPainter(_playPauseButton);\n\t\tconst auto size = _playPauseButton->size();\n\n\t\tconst auto progress = _showProgress.current()\n\t\t\t* st::historyRecordCenterControlIconScale;\n\t\tp.translate(size.width() / 2, size.height() / 2);\n\t\tp.scale(progress, progress);\n\t\tp.translate(-play.width() / 2, -play.height() / 2);\n\t\t_playPause.paint(p, st::historyRecordVoiceFgActiveIcon);\n\t}, _playPauseButton->lifetime());\n\n\t_playPauseButton->setClickedCallback([=] {\n\t\tplayPause();\n\t});\n\n\tconst auto showPause = _lifetime.make_state<rpl::variable<bool>>(false);\n\tshowPause->changes(\n\t) | rpl::on_next([=](bool pause) {\n\t\t_playPauseButton->setAccessibleName(pause\n\t\t\t? tr::lng_record_lock_pause(tr::now)\n\t\t\t: tr::lng_record_lock_play(tr::now));\n\t\t_playPause.setState(pause\n\t\t\t? PlayButtonLayout::State::Pause\n\t\t\t: PlayButtonLayout::State::Play);\n\t}, _lifetime);\n\n\tinstance()->updatedNotifier(\n\t) | rpl::on_next([=](const State &state) {\n\t\tif (isInPlayer(state)) {\n\t\t\t*showPause = ShowPauseIcon(state.state);\n\t\t\tif (!_data->minithumbs.isNull()) {\n\t\t\t\t_videoRepaints.fire({});\n\t\t\t}\n\t\t} else if (showPause->current()) {\n\t\t\t*showPause = false;\n\t\t}\n\t}, _lifetime);\n\n\tinstance()->stops(\n\t\tAudioMsgId::Type::Voice\n\t) | rpl::on_next([=] {\n\t\t*showPause = false;\n\t}, _lifetime);\n\n\t_lifetime.add([=] {\n\t\tconst auto current = instance()->current(AudioMsgId::Type::Voice);\n\t\tif (current.audio() == _document) {\n\t\t\tinstance()->stop(AudioMsgId::Type::Voice, true);\n\t\t}\n\t});\n}\n\nvoid ListenWrap::initPlayProgress() {\n\tusing namespace ::Media::Player;\n\tusing State = TrackState;\n\tenum class DragMode {\n\t\tNone,\n\t\tSeek,\n\t\tTrimLeft,\n\t\tTrimRight,\n\t};\n\n\tconst auto animation = _lifetime.make_state<Ui::Animations::Basic>();\n\tconst auto dragMode = _lifetime.make_state<DragMode>(DragMode::None);\n\tconst auto trimPlaybackSeekInProgress = _lifetime.make_state<bool>(false);\n\tconst auto &voice = AudioMsgId::Type::Voice;\n\tconst auto stopPlayingPreviewOnTrim = [=] {\n\t\tconst auto state = instance()->getState(voice);\n\t\tif (isInPlayer(state) && ShowPauseIcon(state.state)) {\n\t\t\tinstance()->stop(voice, true);\n\t\t}\n\t};\n\tconst auto canSeekAt = [=](const QPoint &p) {\n\t\treturn isInPlayer()\n\t\t\t&& _waveformFgRect.contains(p)\n\t\t\t&& (_controlRect.isEmpty() || !_controlRect.contains(p));\n\t};\n\n\tconst auto updateCursor = [=](const QPoint &p) {\n\t\tif (canTrim()\n\t\t\t&& (_trimLeftHandleRect.contains(p)\n\t\t\t\t|| _trimRightHandleRect.contains(p))) {\n\t\t\t_parent->setCursor(style::cur_sizehor);\n\t\t} else if (canSeekAt(p)) {\n\t\t\t_parent->setCursor(style::cur_pointer);\n\t\t} else {\n\t\t\t_parent->setCursor(style::cur_default);\n\t\t}\n\t};\n\t_parent->setMouseTracking(canTrim());\n\n\trpl::merge(\n\t\tinstance()->startsPlay(voice) | rpl::map_to(true),\n\t\tinstance()->stops(voice) | rpl::map_to(false)\n\t) | rpl::on_next([=](bool play) {\n\t\t_parent->setMouseTracking(canTrim() || (isInPlayer() && play));\n\t\tupdateCursor(_parent->mapFromGlobal(QCursor::pos()));\n\t}, _lifetime);\n\n\tinstance()->updatedNotifier(\n\t) | rpl::on_next([=](const State &state) {\n\t\tif (*trimPlaybackSeekInProgress) {\n\t\t\treturn;\n\t\t}\n\t\tif (!isInPlayer(state)) {\n\t\t\treturn;\n\t\t} else if (!_isShowAnimation && (_showProgress.current() < 1.)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto [leftBoundary, rightBoundary] = selectedTrimBoundaries();\n\t\tconst auto playbackTrimmed = canTrim()\n\t\t\t&& ((leftBoundary > kTrimPlaybackEpsilon)\n\t\t\t\t|| (rightBoundary < (1. - kTrimPlaybackEpsilon)));\n\t\tconst auto length = int(state.length);\n\t\tconst auto position = std::min(state.position, int64(length));\n\t\tauto progress = length\n\t\t\t? Progress(position, length)\n\t\t\t: 0.;\n\t\tif (playbackTrimmed && length > 0) {\n\t\t\tif (ShowPauseIcon(state.state)\n\t\t\t\t&& (progress < (leftBoundary - kTrimPlaybackEpsilon))) {\n\t\t\t\t*trimPlaybackSeekInProgress = true;\n\t\t\t\tinstance()->startSeeking(voice);\n\t\t\t\tinstance()->finishSeeking(voice, leftBoundary);\n\t\t\t\t*trimPlaybackSeekInProgress = false;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (ShowPauseIcon(state.state)\n\t\t\t\t&& (progress >= (rightBoundary - kTrimPlaybackEpsilon))) {\n\t\t\t\tinstance()->stop(voice, true);\n\t\t\t\t_playProgress = anim::value(leftBoundary, leftBoundary);\n\t\t\t\t_parent->update(_waveformFgRect);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tprogress = std::clamp(progress, leftBoundary, rightBoundary);\n\t\t}\n\t\tif (IsStopped(state.state)) {\n\t\t\t_playProgress = playbackTrimmed\n\t\t\t\t? anim::value(leftBoundary, leftBoundary)\n\t\t\t\t: anim::value();\n\t\t} else {\n\t\t\t_playProgress.start(progress);\n\t\t}\n\t\tanimation->start();\n\t}, _lifetime);\n\n\tauto animationCallback = [=](crl::time now) {\n\t\tif (anim::Disabled()) {\n\t\t\tnow += kAudioVoiceUpdateView;\n\t\t}\n\n\t\tconst auto dt = (now - animation->started())\n\t\t\t/ float64(kAudioVoiceUpdateView);\n\t\tif (dt >= 1.) {\n\t\t\tanimation->stop();\n\t\t\t_playProgress.finish();\n\t\t} else {\n\t\t\t_playProgress.update(std::min(dt, 1.), anim::linear);\n\t\t}\n\t\t_parent->update(_waveformFgRect);\n\t\treturn (dt < 1.);\n\t};\n\tanimation->init(std::move(animationCallback));\n\n\t_parent->events(\n\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\treturn (e->type() == QEvent::MouseMove\n\t\t\t|| e->type() == QEvent::MouseButtonPress\n\t\t\t|| e->type() == QEvent::MouseButtonRelease);\n\t}) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (!isInPlayer() && !canTrim()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto type = e->type();\n\t\tconst auto &pos = static_cast<QMouseEvent*>(e.get())->pos();\n\t\tif ((type == QEvent::MouseMove) && (*dragMode == DragMode::None)) {\n\t\t\tupdateCursor(pos);\n\t\t}\n\n\t\tif (type == QEvent::MouseButtonPress) {\n\t\t\tif (canTrim() && _trimLeftHandleRect.contains(pos)) {\n\t\t\t\tstopPlayingPreviewOnTrim();\n\t\t\t\t*dragMode = DragMode::TrimLeft;\n\t\t\t\t_parent->setCursor(style::cur_sizehor);\n\t\t\t\treturn;\n\t\t\t} else if (canTrim() && _trimRightHandleRect.contains(pos)) {\n\t\t\t\tstopPlayingPreviewOnTrim();\n\t\t\t\t*dragMode = DragMode::TrimRight;\n\t\t\t\t_parent->setCursor(style::cur_sizehor);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (canSeekAt(pos)) {\n\t\t\t\tinstance()->startSeeking(voice);\n\t\t\t\t*dragMode = DragMode::Seek;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto isRelease = (type == QEvent::MouseButtonRelease);\n\t\tif (*dragMode == DragMode::Seek) {\n\t\t\tif (isRelease || (type == QEvent::MouseMove)) {\n\t\t\t\tauto progress = trimProgressFromPosition(pos.x());\n\t\t\t\tif (canTrim()) {\n\t\t\t\t\tconst auto [left, right] = selectedTrimBoundaries();\n\t\t\t\t\tprogress = std::clamp(progress, left, right);\n\t\t\t\t}\n\t\t\t\t_playProgress = anim::value(progress, progress);\n\t\t\t\t_parent->update(_waveformFgRect);\n\t\t\t\tif (isRelease) {\n\t\t\t\t\tinstance()->finishSeeking(voice, progress);\n\t\t\t\t\t*dragMode = DragMode::None;\n\t\t\t\t\tupdateCursor(pos);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif ((*dragMode == DragMode::TrimLeft)\n\t\t\t|| (*dragMode == DragMode::TrimRight)) {\n\t\t\tif (isRelease || (type == QEvent::MouseMove)) {\n\t\t\t\tconst auto progress = trimProgressFromPosition(pos.x());\n\t\t\t\tconst auto minDelta = minimumTrimProgress();\n\t\t\t\tif (*dragMode == DragMode::TrimLeft) {\n\t\t\t\t\t_trimLeftProgress = std::clamp(\n\t\t\t\t\t\tprogress,\n\t\t\t\t\t\t0.,\n\t\t\t\t\t\tstd::max(0., _trimRightProgress - minDelta));\n\t\t\t\t} else {\n\t\t\t\t\t_trimRightProgress = std::clamp(\n\t\t\t\t\t\tprogress,\n\t\t\t\t\t\tstd::min(1., _trimLeftProgress + minDelta),\n\t\t\t\t\t\t1.);\n\t\t\t\t}\n\t\t\t\tupdateDurationText();\n\t\t\t\tupdateTrimGeometry();\n\t\t\t\tupdateControlGeometry();\n\t\t\t\t_parent->update();\n\t\t\t\tif (isRelease) {\n\t\t\t\t\t*dragMode = DragMode::None;\n\t\t\t\t\tupdateCursor(pos);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (isRelease) {\n\t\t\tupdateCursor(pos);\n\t\t}\n\t}, _lifetime);\n}\n\n\nbool ListenWrap::isInPlayer(const ::Media::Player::TrackState &state) const {\n\treturn (state.id && (state.id.audio() == _document));\n}\n\nbool ListenWrap::isInPlayer() const {\n\tusing Type = AudioMsgId::Type;\n\treturn isInPlayer(::Media::Player::instance()->getState(Type::Voice));\n}\n\nbool ListenWrap::canTrim() const {\n\treturn _allowTrim;\n}\n\nfloat64 ListenWrap::trimProgressFromPosition(int x) const {\n\tconst auto width = _waveformBgFinalCenterRect.width() - 1;\n\tif (width <= 0) {\n\t\treturn 0.;\n\t}\n\treturn std::clamp(\n\t\tfloat64(x - _waveformBgFinalCenterRect.x()) / width,\n\t\t0.,\n\t\t1.);\n}\n\nfloat64 ListenWrap::minimumTrimProgress() const {\n\tconst auto samplesProgress = [&] {\n\t\tconst auto samples = int((_data->duration\n\t\t\t* ::Media::Player::kDefaultFrequency) / crl::time(1000));\n\t\tif (samples <= 0) {\n\t\t\treturn 0.;\n\t\t}\n\t\treturn std::clamp(\n\t\t\tfloat64(kMinSamples) / samples,\n\t\t\t0.,\n\t\t\t1.);\n\t}();\n\treturn std::max(samplesProgress, minimumControlTrimProgress());\n}\n\nfloat64 ListenWrap::minimumControlTrimProgress() const {\n\tif (!canTrim() || _waveformBgFinalCenterRect.isEmpty()) {\n\t\treturn 0.;\n\t}\n\tconst auto trimRect = _waveformBgFinalCenterRect;\n\tconst auto handleWidth = std::max(\n\t\t1,\n\t\tstd::min(\n\t\t\tst::historyRecordTrimHandleWidth,\n\t\t\ttrimRect.width() / 2));\n\tconst auto previewRange = std::max(\n\t\t1,\n\t\ttrimRect.width() - (handleWidth * 2));\n\tconst auto controlHeight = std::min(\n\t\tst::historyRecordCenterControlHeight,\n\t\ttrimRect.height());\n\tconst auto iconWidth = controlHeight;\n\tconst auto minControlWidth = (st::historyRecordCenterControlPadding * 2)\n\t\t+ iconWidth\n\t\t+ st::historyRecordCenterControlMinimumProgressPadding * 2;\n\treturn std::clamp(float64(minControlWidth) / previewRange, 0., 1.);\n}\n\ncrl::time ListenWrap::selectedDuration() const {\n\tif (!canTrim()) {\n\t\treturn _data->duration;\n\t}\n\tif (const auto range = selectedTrimRange()) {\n\t\treturn std::max(crl::time(0), range->till - range->from);\n\t}\n\treturn _data->duration;\n}\n\nstd::optional<ListenWrap::TrimRange> ListenWrap::selectedTrimRange() const {\n\tif (!canTrim()) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto left = std::clamp(_trimLeftProgress, 0., 1.);\n\tconst auto right = std::clamp(_trimRightProgress, left, 1.);\n\tif ((left <= 0.) && (right >= 1.)) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto currentSamples = int((_data->duration\n\t\t* ::Media::Player::kDefaultFrequency) / crl::time(1000));\n\tif (currentSamples <= 0) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto fromSamples = base::SafeRound(currentSamples * left);\n\tconst auto tillSamples = base::SafeRound(currentSamples * right);\n\tif (tillSamples <= fromSamples) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto from = (fromSamples * crl::time(1000))\n\t\t/ ::Media::Player::kDefaultFrequency;\n\tconst auto till = (tillSamples * crl::time(1000))\n\t\t/ ::Media::Player::kDefaultFrequency;\n\tif (till <= from) {\n\t\treturn std::nullopt;\n\t}\n\treturn TrimRange{ .from = crl::time(from), .till = crl::time(till) };\n}\n\nListenWrap::TrimBoundaries ListenWrap::selectedTrimBoundaries() const {\n\tconst auto dur = _data->duration;\n\tif (const auto range = selectedTrimRange(); range && (dur > 0)) {\n\t\tconst auto left = std::clamp(float64(range->from) / dur, 0., 1.);\n\t\tconst auto right = std::clamp(float64(range->till) / dur, left, 1.);\n\t\treturn { left, right };\n\t}\n\treturn { 0., 1. };\n}\n\nListenWrap::TrimGeometry ListenWrap::computeTrimGeometry(\n\t\tconst QRect &trimRect) const {\n\tauto result = TrimGeometry();\n\tif (!canTrim() || trimRect.isEmpty()) {\n\t\treturn result;\n\t}\n\tconst auto width = trimRect.width();\n\tif (width <= 0) {\n\t\treturn result;\n\t}\n\tconst auto handleWidth = std::max(\n\t\t1,\n\t\tstd::min(\n\t\t\tst::historyRecordTrimHandleWidth,\n\t\t\twidth / 2));\n\tconst auto previewRange = std::max(1, width - (handleWidth * 2));\n\tconst auto minBoundary = trimRect.x() + handleWidth;\n\tconst auto maxBoundary = trimRect.right() - handleWidth + 1;\n\tif (maxBoundary < minBoundary) {\n\t\treturn result;\n\t}\n\tconst auto leftProgress = std::clamp(_trimLeftProgress, 0., 1.);\n\tconst auto rightProgress = std::clamp(\n\t\t_trimRightProgress,\n\t\tleftProgress,\n\t\t1.);\n\tconst auto leftBoundary = std::clamp(\n\t\tminBoundary + int(base::SafeRound(previewRange * leftProgress)),\n\t\tminBoundary,\n\t\tmaxBoundary);\n\tconst auto rightBoundary = std::clamp(\n\t\tminBoundary + int(base::SafeRound(previewRange * rightProgress)),\n\t\tleftBoundary,\n\t\tmaxBoundary);\n\tresult.leftHandle = QRect(\n\t\tleftBoundary - handleWidth,\n\t\ttrimRect.y(),\n\t\thandleWidth,\n\t\ttrimRect.height());\n\tresult.rightHandle = QRect(\n\t\trightBoundary,\n\t\ttrimRect.y(),\n\t\thandleWidth,\n\t\ttrimRect.height());\n\tconst auto previewLeft = leftBoundary;\n\tconst auto previewRight = std::max(previewLeft, rightBoundary - 1);\n\tresult.frame = QRect(\n\t\tQPoint(previewLeft, trimRect.y()),\n\t\tQPoint(previewRight, rect::bottom(trimRect)));\n\treturn result;\n}\n\nvoid ListenWrap::updateControlGeometry() {\n\tconst auto availableRect = (canTrim() && !_trimFrameRect.isEmpty())\n\t\t? _trimFrameRect\n\t\t: _waveformBgFinalCenterRect;\n\tif (availableRect.isEmpty()) {\n\t\t_controlRect = QRect();\n\t\t_controlHasDuration = false;\n\t\treturn;\n\t}\n\tconst auto controlHeight = std::min(\n\t\tst::historyRecordCenterControlHeight,\n\t\tavailableRect.height());\n\tconst auto iconWidth = controlHeight;\n\tconst auto iconOnlyWidth = (st::historyRecordCenterControlPadding * 2)\n\t\t+ iconWidth;\n\tconst auto fullWidth = iconOnlyWidth\n\t\t+ st::historyRecordCenterControlTextSkip\n\t\t+ _durationWidth;\n\tconst auto skip = st::historyRecordCenterControlMinimumProgressPadding;\n\t_controlHasDuration = (availableRect.width() - skip * 2 >= fullWidth);\n\tauto controlWidth = _controlHasDuration ? fullWidth : iconOnlyWidth;\n\tcontrolWidth = std::min(controlWidth, availableRect.width());\n\tif (controlWidth <= 0 || controlHeight <= 0) {\n\t\t_controlRect = QRect();\n\t\t_controlHasDuration = false;\n\t\treturn;\n\t}\n\t_controlRect = QRect(\n\t\tavailableRect.x() + (availableRect.width() - controlWidth) / 2,\n\t\tavailableRect.y() + (availableRect.height() - controlHeight) / 2,\n\t\tcontrolWidth,\n\t\tcontrolHeight);\n\t_playPauseButton->resize(iconWidth, controlHeight);\n\tconst auto iconLeft = _controlHasDuration\n\t\t? (_controlRect.x() + st::historyRecordCenterControlPadding)\n\t\t: (_controlRect.x() + (_controlRect.width() - iconWidth) / 2);\n\t_playPauseButton->moveToLeft(\n\t\ticonLeft,\n\t\t_controlRect.y());\n}\n\nvoid ListenWrap::updateTrimGeometry() {\n\tif (!canTrim() || _waveformBgFinalCenterRect.isEmpty()) {\n\t\t_trimFrameRect = QRect();\n\t\t_trimLeftHandleRect = QRect();\n\t\t_trimRightHandleRect = QRect();\n\t\treturn;\n\t}\n\tconst auto minDelta = minimumTrimProgress();\n\tif ((_trimRightProgress - _trimLeftProgress) < minDelta) {\n\t\tconst auto center = (_trimLeftProgress + _trimRightProgress) / 2.;\n\t\tconst auto half = minDelta / 2.;\n\t\tauto left = center - half;\n\t\tauto right = center + half;\n\t\tif (left < 0.) {\n\t\t\tright = std::min(1., right - left);\n\t\t\tleft = 0.;\n\t\t}\n\t\tif (right > 1.) {\n\t\t\tleft = std::max(0., left - (right - 1.));\n\t\t\tright = 1.;\n\t\t}\n\t\t_trimLeftProgress = left;\n\t\t_trimRightProgress = right;\n\t}\n\tconst auto geometry = computeTrimGeometry(_waveformBgFinalCenterRect);\n\t_trimFrameRect = geometry.frame;\n\t_trimLeftHandleRect = geometry.leftHandle;\n\t_trimRightHandleRect = geometry.rightHandle;\n}\n\nvoid ListenWrap::applyTrimSelection(bool resetSelection) {\n\tif (!canTrim()) {\n\t\treturn;\n\t}\n\tconst auto range = selectedTrimRange();\n\tif (!range) {\n\t\treturn;\n\t}\n\tconst auto [waveLeft, waveRight] = selectedTrimBoundaries();\n\tauto waveform = ResampleWaveformToRange(\n\t\t_data->waveform,\n\t\twaveLeft,\n\t\twaveRight);\n\tconst auto from = range->from;\n\tconst auto till = range->till;\n\tconst auto selected = till - from;\n\tconst auto selectedSamples = int((selected\n\t\t* ::Media::Player::kDefaultFrequency) / crl::time(1000));\n\tif (selectedSamples < kMinSamples) {\n\t\treturn;\n\t}\n\tconst auto trimmed = ::Media::TrimAudioToRange(_data->content, from, till);\n\tif (trimmed.content.isEmpty()) {\n\t\treturn;\n\t}\n\tif (isInPlayer()) {\n\t\t::Media::Player::instance()->stop(AudioMsgId::Type::Voice, true);\n\t}\n\t_data->content = std::move(trimmed.content);\n\tif (waveform.isEmpty()) {\n\t\twaveform = std::move(trimmed.waveform);\n\t}\n\t_data->waveform = std::move(waveform);\n\t_data->duration = trimmed.duration;\n\t_mediaView->setBytes(_data->content);\n\t_document->size = _data->content.size();\n\t_voiceData->waveform = _data->waveform;\n\t_voiceData->wavemax = _voiceData->waveform.empty()\n\t\t? uchar(0)\n\t\t: *ranges::max_element(_voiceData->waveform);\n\tif (resetSelection) {\n\t\t_trimLeftProgress = 0.;\n\t\t_trimRightProgress = 1.;\n\t}\n\tupdateDurationText();\n\t_waveformFgRect = computeWaveformRect(_waveformBgFinalCenterRect);\n\tupdateTrimGeometry();\n\tupdateControlGeometry();\n\t_playProgress = anim::value();\n\t_parent->update();\n}\n\nvoid ListenWrap::updateDurationText() {\n\t_duration = FormatTrimDuration(selectedDuration());\n\t_durationWidth = _durationFont->width(_duration);\n}\n\nvoid ListenWrap::applyTrimBeforeSend() {\n\tapplyTrimSelection(true);\n}\n\nvoid ListenWrap::prepareForSendAnimation() {\n\tif (_waveformBgRect.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto cacheRect = _waveformBgRect\n\t\t- style::margins(_st.remove.width, 0, _send->width(), 0);\n\tif (cacheRect.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto deleteVisible = _delete->isVisible();\n\tif (deleteVisible) {\n\t\t_delete->hide();\n\t}\n\t_sendAnimationCache = Ui::GrabWidgetToImage(_parent, cacheRect);\n\tif (deleteVisible) {\n\t\t_delete->show();\n\t}\n\t_useSendAnimationCache = !_sendAnimationCache.isNull();\n\tif (_useSendAnimationCache && _playPauseButton->isVisible()) {\n\t\t_playPauseButton->hide();\n\t\t_playPauseHiddenForSendAnimation = true;\n\t}\n\tif (_useSendAnimationCache) {\n\t\t_parent->update(cacheRect);\n\t}\n}\n\nvoid ListenWrap::playPause() {\n\t::Media::Player::instance()->playPause({ _document, FullMsgId() });\n}\n\nQRect ListenWrap::computeWaveformRect(const QRect &centerRect) const {\n\tconst auto top = computeTopMargin(st::msgWaveformMax);\n\tconst auto left = st::historyRecordTrimHandleWidth;\n\treturn centerRect - style::margins(left, top, left, top);\n}\n\nint ListenWrap::computeTopMargin(int height) const {\n\treturn (_waveformBgRect.height() - height) / 2;\n}\n\nvoid ListenWrap::requestPaintProgress(float64 progress) {\n\t_isShowAnimation = (_showProgress.current() < progress);\n\tif (_isShowAnimation && _useSendAnimationCache) {\n\t\t_useSendAnimationCache = false;\n\t\t_sendAnimationCache = QImage();\n\t\tif (_playPauseHiddenForSendAnimation) {\n\t\t\t_playPauseButton->show();\n\t\t\t_playPauseHiddenForSendAnimation = false;\n\t\t}\n\t}\n\tif (!_isShowAnimation && (progress < 1.)) {\n\t\tconst auto value = _playProgress.current();\n\t\t_playProgress = anim::value(value, value);\n\t}\n\t_showProgress = progress;\n}\n\nrpl::producer<> ListenWrap::stopRequests() const {\n\treturn _delete->clicks() | rpl::to_empty;\n}\n\nstd::shared_ptr<Ui::DynamicImage> ListenWrap::videoPreview() {\n\treturn std::make_shared<SoundedPreview>(\n\t\t_document,\n\t\t_videoRepaints.events());\n}\n\nrpl::lifetime &ListenWrap::lifetime() {\n\treturn _lifetime;\n}\n\nclass RecordLock final : public Ui::RippleButton {\npublic:\n\tRecordLock(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::RecordBarLock &st);\n\n\tvoid requestPaintProgress(float64 progress);\n\tvoid requestPaintLockToStopProgress(float64 progress);\n\tvoid requestPaintPauseToInputProgress(float64 progress);\n\tvoid setVisibleTopPart(int part);\n\tvoid setRecordingVideo(bool value);\n\n\t[[nodiscard]] rpl::producer<> locks() const;\n\t[[nodiscard]] bool isLocked() const;\n\t[[nodiscard]] bool isStopState() const;\n\n\t[[nodiscard]] float64 lockToStopProgress() const;\n\nprotected:\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\nprivate:\n\tvoid init();\n\n\tvoid drawProgress(QPainter &p);\n\tvoid setProgress(float64 progress);\n\tvoid startLockingAnimation(float64 to);\n\n\tconst style::RecordBarLock &_st;\n\tconst QRect _rippleRect;\n\tconst QPen _arcPen;\n\n\tUi::Animations::Simple _lockEnderAnimation;\n\n\tfloat64 _lockToStopProgress = 0.;\n\tfloat64 _pauseToInputProgress = 0.;\n\trpl::variable<float64> _progress = 0.;\n\tint _visibleTopPart = -1;\n\tbool _recordingVideo = false;\n\n};\n\nRecordLock::RecordLock(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst style::RecordBarLock &st)\n: RippleButton(parent, st.ripple)\n, _st(st)\n, _rippleRect(Rect(Size(st::historyRecordLockTopShadow.width()))\n\t- (st::historyRecordLockRippleMargin))\n, _arcPen(\n\tQColor(Qt::white),\n\tst::historyRecordLockIconLineWidth,\n\tQt::SolidLine,\n\tQt::SquareCap,\n\tQt::RoundJoin) {\n\tinit();\n}\n\nvoid RecordLock::setVisibleTopPart(int part) {\n\t_visibleTopPart = part;\n}\n\nvoid RecordLock::setRecordingVideo(bool value) {\n\t_recordingVideo = value;\n}\n\nvoid RecordLock::init() {\n\tshownValue(\n\t) | rpl::on_next([=](bool shown) {\n\t\tresize(\n\t\t\tst::historyRecordLockTopShadow.width(),\n\t\t\tst::historyRecordLockSize.height());\n\t\tif (!shown) {\n\t\t\tsetCursor(style::cur_default);\n\t\t\tsetAttribute(Qt::WA_TransparentForMouseEvents, true);\n\t\t\t_lockEnderAnimation.stop();\n\t\t\t_lockToStopProgress = 0.;\n\t\t\t_pauseToInputProgress = 0.;\n\t\t\t_progress = 0.;\n\t\t}\n\t}, lifetime());\n\n\tpaintRequest(\n\t) | rpl::on_next([=](const QRect &clip) {\n\t\tif (!_visibleTopPart) {\n\t\t\treturn;\n\t\t}\n\t\tauto p = QPainter(this);\n\t\tif (_visibleTopPart > 0 && _visibleTopPart < height()) {\n\t\t\tp.setClipRect(0, 0, width(), _visibleTopPart);\n\t\t}\n\t\tif (isLocked()) {\n\t\t\tconst auto top = anim::interpolate(\n\t\t\t\t0,\n\t\t\t\theight() - st::historyRecordLockTopShadow.height() * 2,\n\t\t\t\t_lockToStopProgress);\n\t\t\tp.translate(0, top);\n\t\t\tdrawProgress(p);\n\t\t\treturn;\n\t\t}\n\t\tdrawProgress(p);\n\t}, lifetime());\n\tsetAccessibleName(tr::lng_record_lock(tr::now));\n}\n\nvoid RecordLock::drawProgress(QPainter &p) {\n\tconst auto progress = _progress.current();\n\n\tconst auto inner = DrawLockCircle(p, rect(), _st, progress);\n\n\tif (isLocked()) {\n\t\tUi::RippleButton::paintRipple(p, _rippleRect.x(), _rippleRect.y());\n\t}\n\t{\n\t\tconst auto &arcOffset = st::historyRecordLockIconLineSkip;\n\t\tconst auto &size = st::historyRecordLockIconSize;\n\n\t\tconst auto arcWidth = size.width() - arcOffset * 2;\n\t\tconst auto &arcHeight = st::historyRecordLockIconArcHeight;\n\n\t\tconst auto &blockHeight = st::historyRecordLockIconBottomHeight;\n\n\t\tconst auto blockRectWidth = anim::interpolateToF(\n\t\t\tsize.width(),\n\t\t\tst::historyRecordStopIconWidth,\n\t\t\t_lockToStopProgress);\n\t\tconst auto blockRectHeight = anim::interpolateToF(\n\t\t\tblockHeight,\n\t\t\tst::historyRecordStopIconWidth,\n\t\t\t_lockToStopProgress);\n\t\tconst auto blockRectTop = anim::interpolateToF(\n\t\t\tsize.height() - blockHeight,\n\t\t\tbase::SafeRound((size.height() - blockRectHeight) / 2.),\n\t\t\t_lockToStopProgress);\n\n\t\tconst auto blockRect = QRectF(\n\t\t\t(size.width() - blockRectWidth) / 2,\n\t\t\tblockRectTop,\n\t\t\tblockRectWidth,\n\t\t\tblockRectHeight);\n\t\tconst auto &lineHeight = st::historyRecordLockIconLineHeight;\n\n\t\tconst auto lockTranslation = QPoint(\n\t\t\t(inner.width() - size.width()) / 2,\n\t\t\t(_st.originTop.height() * 2 - size.height()) / 2);\n\t\tconst auto xRadius = anim::interpolateF(2, 3, _lockToStopProgress);\n\n\t\tconst auto pauseLineOffset = blockRectWidth / 2\n\t\t\t+ st::historyRecordLockIconLineWidth;\n\t\tif (_lockToStopProgress == 1.) {\n\t\t\t// Paint the block.\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.translate(inner.topLeft() + lockTranslation);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(_st.fg);\n\t\t\tif (_pauseToInputProgress > 0.) {\n\t\t\t\tp.setOpacity(_pauseToInputProgress);\n\t\t\t\tconst auto &icon = _recordingVideo\n\t\t\t\t\t? st::historyRecordLockRound\n\t\t\t\t\t: st::historyRecordLockInput;\n\t\t\t\ticon.paintInCenter(p, blockRect.toRect());\n\t\t\t\tp.setOpacity(1. - _pauseToInputProgress);\n\t\t\t}\n\t\t\tp.drawRoundedRect(\n\t\t\t\tblockRect - QMargins(0, 0, pauseLineOffset, 0),\n\t\t\t\txRadius,\n\t\t\t\t3);\n\t\t\tp.drawRoundedRect(\n\t\t\t\tblockRect - QMargins(pauseLineOffset, 0, 0, 0),\n\t\t\t\txRadius,\n\t\t\t\t3);\n\t\t} else {\n\t\t\t// Paint an animation frame.\n\t\t\tauto frame = QImage(\n\t\t\t\tinner.size() * style::DevicePixelRatio(),\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tframe.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\tframe.fill(Qt::transparent);\n\n\t\t\tauto q = QPainter(&frame);\n\t\t\tauto hq = PainterHighQualityEnabler(q);\n\n\t\t\tq.setPen(Qt::NoPen);\n\t\t\tq.setBrush(_arcPen.brush());\n\n\t\t\tq.translate(lockTranslation);\n\t\t\t{\n\t\t\t\tconst auto offset = anim::interpolateF(\n\t\t\t\t\t0,\n\t\t\t\t\tpauseLineOffset,\n\t\t\t\t\t_lockToStopProgress);\n\t\t\t\tq.drawRoundedRect(\n\t\t\t\t\tblockRect - QMarginsF(0, 0, offset, 0),\n\t\t\t\t\txRadius,\n\t\t\t\t\t3);\n\t\t\t\tq.drawRoundedRect(\n\t\t\t\t\tblockRect - QMarginsF(offset, 0, 0, 0),\n\t\t\t\t\txRadius,\n\t\t\t\t\t3);\n\t\t\t}\n\n\t\t\tconst auto offsetTranslate = _lockToStopProgress *\n\t\t\t\t(lineHeight + arcHeight + _arcPen.width() * 2);\n\t\t\tq.translate(\n\t\t\t\tsize.width() - arcOffset,\n\t\t\t\tblockRect.y() + offsetTranslate);\n\n\t\t\tif (progress < 1. && progress > 0.) {\n\t\t\t\tq.rotate(kLockArcAngle * progress);\n\t\t\t}\n\n\t\t\tconst auto lockProgress = 1. - _lockToStopProgress;\n\t\t\t{\n\t\t\t\tauto arcPen = _arcPen;\n\t\t\t\tarcPen.setWidthF(_arcPen.widthF() * lockProgress);\n\t\t\t\tq.setPen(arcPen);\n\t\t\t}\n\t\t\tconst auto rLine = QLineF(0, 0, 0, -lineHeight);\n\t\t\tq.drawLine(rLine);\n\n\t\t\tq.drawArc(\n\t\t\t\t-arcWidth,\n\t\t\t\trLine.dy() - arcHeight - _arcPen.width() + rLine.y1(),\n\t\t\t\tarcWidth,\n\t\t\t\tarcHeight * 2,\n\t\t\t\t0,\n\t\t\t\tarc::kHalfLength);\n\n\t\t\tif (progress == 1. && lockProgress < 1.) {\n\t\t\t\tq.drawLine(\n\t\t\t\t\t-arcWidth,\n\t\t\t\t\trLine.y2(),\n\t\t\t\t\t-arcWidth,\n\t\t\t\t\trLine.dy() * lockProgress);\n\t\t\t}\n\t\t\tq.end();\n\n\t\t\tp.drawImage(\n\t\t\t\tinner.topLeft(),\n\t\t\t\tstyle::colorizeImage(frame, _st.fg));\n\t\t}\n\t}\n}\n\nvoid RecordLock::startLockingAnimation(float64 to) {\n\t_lockEnderAnimation.start(\n\t\t[=](float64 value) { setProgress(value); },\n\t\t0.,\n\t\tto,\n\t\tst::universalDuration);\n}\n\nvoid RecordLock::requestPaintProgress(float64 progress) {\n\tif (isHidden()\n\t\t|| isLocked()\n\t\t|| _lockEnderAnimation.animating()\n\t\t|| (_progress.current() == progress)) {\n\t\treturn;\n\t}\n\tif (!_progress.current() && (progress > .3)) {\n\t\tstartLockingAnimation(progress);\n\t\treturn;\n\t}\n\tsetProgress(progress);\n}\n\nvoid RecordLock::requestPaintLockToStopProgress(float64 progress) {\n\t_lockToStopProgress = progress;\n\tif (isStopState()) {\n\t\tsetCursor(style::cur_pointer);\n\t\tsetAttribute(Qt::WA_TransparentForMouseEvents, false);\n\n\t\tresize(\n\t\t\tst::historyRecordLockTopShadow.width(),\n\t\t\tst::historyRecordLockTopShadow.width());\n\t}\n\tupdate();\n}\n\nvoid RecordLock::requestPaintPauseToInputProgress(float64 progress) {\n\t_pauseToInputProgress = progress;\n\tupdate();\n}\n\nfloat64 RecordLock::lockToStopProgress() const {\n\treturn _lockToStopProgress;\n}\n\nvoid RecordLock::setProgress(float64 progress) {\n\t_progress = progress;\n\tupdate();\n}\n\nbool RecordLock::isLocked() const {\n\treturn _progress.current() == 1.;\n}\n\nbool RecordLock::isStopState() const {\n\treturn isLocked() && (_lockToStopProgress == 1.);\n}\n\nrpl::producer<> RecordLock::locks() const {\n\treturn _progress.changes(\n\t) | rpl::filter([=] { return isLocked(); }) | rpl::to_empty;\n}\n\nQImage RecordLock::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::EllipseMask(_rippleRect.size());\n}\n\nQPoint RecordLock::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos()) - _rippleRect.topLeft();\n}\n\nclass CancelButton final : public Ui::RippleButton {\npublic:\n\tCancelButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::RecordBar &st,\n\t\tint height);\n\n\tvoid requestPaintProgress(float64 progress);\n\nprotected:\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\nprivate:\n\tvoid init();\n\n\tconst style::RecordBar &_st;\n\tconst int _width;\n\tconst QRect _rippleRect;\n\n\trpl::variable<float64> _showProgress = 0.;\n\n\tUi::Text::String _text;\n\n};\n\nCancelButton::CancelButton(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst style::RecordBar &st,\n\tint height)\n: Ui::RippleButton(parent, st.cancelRipple)\n, _st(st)\n, _width(st::historyRecordCancelButtonWidth)\n, _rippleRect(QRect(0, (height - _width) / 2, _width, _width))\n, _text(st::semiboldTextStyle, tr::lng_selected_clear(tr::now)) {\n\tsetAccessibleName(tr::lng_record_cancel_recording(tr::now));\n\tresize(_width, height);\n\tinit();\n}\n\nvoid CancelButton::init() {\n\t_showProgress.value(\n\t) | rpl::map(rpl::mappers::_1 > 0.) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](bool hasProgress) {\n\t\tsetVisible(hasProgress);\n\t}, lifetime());\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(this);\n\n\t\tp.setOpacity(_showProgress.current());\n\n\t\tUi::RippleButton::paintRipple(p, _rippleRect.x(), _rippleRect.y());\n\n\t\tp.setPen(_st.cancelActive);\n\t\t_text.draw(p, {\n\t\t\t.position = QPoint(0, (height() - _text.minHeight()) / 2),\n\t\t\t.outerWidth = width(),\n\t\t\t.availableWidth = width(),\n\t\t\t.align = style::al_center,\n\t\t});\n\t}, lifetime());\n}\n\nQImage CancelButton::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::EllipseMask(_rippleRect.size());\n}\n\nQPoint CancelButton::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos()) - _rippleRect.topLeft();\n}\n\nvoid CancelButton::requestPaintProgress(float64 progress) {\n\t_showProgress = progress;\n\tupdate();\n}\n\nVoiceRecordBar::VoiceRecordBar(\n\tnot_null<Ui::RpWidget*> parent,\n\tVoiceRecordBarDescriptor &&descriptor)\n: RpWidget(parent)\n, _st(descriptor.stOverride ? *descriptor.stOverride : st::defaultRecordBar)\n, _outerContainer(descriptor.outerContainer)\n, _show(std::move(descriptor.show))\n, _send(std::move(descriptor.send))\n, _lock(std::make_unique<RecordLock>(_outerContainer, _st.lock))\n, _level(std::make_unique<VoiceRecordButton>(_outerContainer, _st))\n, _cancel(std::make_unique<CancelButton>(this, _st, descriptor.recorderHeight))\n, _startTimer([=] { startRecording(); })\n, _message(\n\tst::historyRecordTextStyle,\n\t(!descriptor.customCancelText.isEmpty()\n\t\t? descriptor.customCancelText\n\t\t: tr::lng_record_cancel(tr::now)),\n\tTextParseOptions{ TextParseMultiline, 0, 0, Qt::LayoutDirectionAuto })\n, _lockFromBottom(descriptor.lockFromBottom)\n, _cancelFont(st::historyRecordFont) {\n\tresize(QSize(parent->width(), descriptor.recorderHeight));\n\tinit();\n\thideFast();\n}\n\nVoiceRecordBar::VoiceRecordBar(\n\tnot_null<Ui::RpWidget*> parent,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tstd::shared_ptr<Ui::SendButton> send,\n\tint recorderHeight)\n: VoiceRecordBar(parent, {\n\t.outerContainer = parent,\n\t.show = std::move(show),\n\t.send = std::move(send),\n\t.recorderHeight = recorderHeight,\n}) {\n}\n\nVoiceRecordBar::~VoiceRecordBar() {\n\tif (isActive()) {\n\t\tstopRecording(StopType::Cancel);\n\t}\n}\n\nvoid VoiceRecordBar::updateMessageGeometry() {\n\tconst auto left = rect::right(_durationRect) + st::historyRecordTextLeft;\n\tconst auto right = width()\n\t\t- _send->width()\n\t\t- st::historyRecordTextRight;\n\tconst auto textWidth = _message.maxWidth();\n\tconst auto width = ((right - left) < textWidth)\n\t\t? st::historyRecordTextWidthForWrap\n\t\t: textWidth;\n\tconst auto countLines = std::ceil((float)textWidth / width);\n\tconst auto textHeight = _message.minHeight() * countLines;\n\t_messageRect = QRect(\n\t\tleft + (right - left - width) / 2,\n\t\t(height() - textHeight) / 2,\n\t\twidth,\n\t\ttextHeight);\n}\n\nvoid VoiceRecordBar::updateLockGeometry() {\n\tconst auto parent = parentWidget();\n\tconst auto me = Ui::MapFrom(_outerContainer, parent, geometry());\n\tconst auto finalTop = me.y()\n\t\t- st::historyRecordLockPosition.y()\n\t\t- _lock->height();\n\tconst auto finalRight = _outerContainer->width()\n\t\t- rect::right(me)\n\t\t+ st::historyRecordLockPosition.x();\n\tconst auto progress = _showLockAnimation.value(\n\t\t_lockShowing.current() ? 1. : 0.);\n\tif (_lockFromBottom) {\n\t\tconst auto top = anim::interpolate(me.y(), finalTop, progress);\n\t\t_lock->moveToRight(finalRight, top);\n\t\t_lock->setVisibleTopPart(me.y() - top);\n\t} else {\n\t\tconst auto from = -_lock->width();\n\t\tconst auto right = anim::interpolate(from, finalRight, progress);\n\t\t_lock->moveToRight(right, finalTop);\n\t}\n}\n\nvoid VoiceRecordBar::updateTTLGeometry(\n\t\tTTLAnimationType type,\n\t\tfloat64 progress) {\n\tif (!_ttlButton) {\n\t\treturn;\n\t}\n\tconst auto parent = parentWidget();\n\tconst auto me = Ui::MapFrom(_outerContainer, parent, geometry());\n\tconst auto anyTop = me.y() - st::historyRecordLockPosition.y();\n\tconst auto lockHiddenProgress = (_lockShowing.current() || !_fullRecord)\n\t\t? 0.\n\t\t: (1. - _showLockAnimation.value(0.));\n\tconst auto ttlFrom = anyTop\n\t\t- _ttlButton->height()\n\t\t- (_ttlButton->height() * (1. - lockHiddenProgress));\n\tif (type == TTLAnimationType::RightLeft) {\n\t\tconst auto finalRight = _outerContainer->width()\n\t\t\t- rect::right(me)\n\t\t\t+ st::historyRecordLockPosition.x();\n\n\t\tconst auto from = -_ttlButton->width();\n\t\tconst auto right = anim::interpolate(from, finalRight, progress);\n\t\t_ttlButton->moveToRight(right, ttlFrom);\n#if 0\n\t} else if (type == TTLAnimationType::TopBottom) {\n\t\tconst auto ttlFrom = anyTop - _ttlButton->height() * 2;\n\t\tconst auto ttlTo = anyTop - _lock->height();\n\t\t_ttlButton->moveToLeft(\n\t\t\t_ttlButton->x(),\n\t\t\tanim::interpolate(ttlFrom, ttlTo, 1. - progress));\n#endif\n\t} else if (type == TTLAnimationType::RightTopStatic) {\n\t\t_ttlButton->moveToRight(-_ttlButton->width(), ttlFrom);\n\t}\n}\n\nvoid VoiceRecordBar::init() {\n\tif (_st.radius > 0) {\n\t\t_backgroundRect.emplace(_st.radius, _st.bg);\n\t}\n\n\t// Keep VoiceRecordBar behind SendButton.\n\trpl::single(\n\t) | rpl::then(\n\t\t_send->events(\n\t\t) | rpl::filter([](not_null<QEvent*> e) {\n\t\t\treturn e->type() == QEvent::ZOrderChange;\n\t\t}) | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\torderControls();\n\t}, lifetime());\n\n\tshownValue(\n\t) | rpl::on_next([=](bool show) {\n\t\tif (!show) {\n\t\t\tfinish();\n\t\t}\n\t}, lifetime());\n\n\tsizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\t_centerY = size.height() / 2;\n\t\t{\n\t\t\tconst auto maxD = st::historyRecordSignalRadius * 2;\n\t\t\tconst auto point = _centerY - st::historyRecordSignalRadius;\n\t\t\t_redCircleRect = { point, point, maxD, maxD };\n\t\t}\n\t\t{\n\t\t\tconst auto durationLeft = _redCircleRect.x()\n\t\t\t\t+ _redCircleRect.width()\n\t\t\t\t+ st::historyRecordDurationSkip;\n\t\t\tconst auto &ascent = _cancelFont->ascent;\n\t\t\t_durationRect = QRect(\n\t\t\t\tdurationLeft,\n\t\t\t\t_redCircleRect.y() - (ascent - _redCircleRect.height()) / 2,\n\t\t\t\t_cancelFont->width(FormatVoiceDuration(kMaxSamples)),\n\t\t\t\tascent);\n\t\t}\n\t\t_cancel->moveToLeft((size.width() - _cancel->width()) / 2, 0);\n\t\tupdateMessageGeometry();\n\t}, lifetime());\n\n\tpaintRequest(\n\t) | rpl::on_next([=](const QRect &clip) {\n\t\tauto p = QPainter(this);\n\t\tif (_showAnimation.animating()) {\n\t\t\tp.setOpacity(showAnimationRatio());\n\t\t}\n\t\tif (_backgroundRect) {\n\t\t\t_backgroundRect->paint(p, rect());\n\t\t} else {\n\t\t\tp.fillRect(clip, _st.bg);\n\t\t}\n\n\t\tp.setOpacity(std::min(p.opacity(), 1. - showListenAnimationRatio()));\n\t\tconst auto opacity = p.opacity();\n\t\t_cancel->requestPaintProgress(_lock->isStopState()\n\t\t\t? (opacity * _lock->lockToStopProgress())\n\t\t\t: 0.);\n\n\t\tif (!opacity) {\n\t\t\treturn;\n\t\t}\n\t\tif (clip.intersects(_messageRect)) {\n\t\t\t// The message should be painted first to avoid flickering.\n\t\t\tdrawMessage(p, activeAnimationRatio());\n\t\t}\n\t\tif (clip.intersects(_durationRect)) {\n\t\t\tdrawDuration(p);\n\t\t}\n\t\tif (clip.intersects(_redCircleRect)) {\n\t\t\t// Should be the last to be drawn.\n\t\t\tdrawRedCircle(p);\n\t\t}\n\t}, lifetime());\n\n\t_inField.changes(\n\t) | rpl::on_next([=](bool value) {\n\t\tactiveAnimate(value);\n\t}, lifetime());\n\n\t_lockShowing.changes(\n\t) | rpl::on_next([=](bool show) {\n\t\tconst auto to = show ? 1. : 0.;\n\t\tconst auto from = show ? 0. : 1.;\n\t\tconst auto &duration = st::historyRecordLockShowDuration;\n\t\t_lock->show();\n\t\tauto callback = [=](float64 value) {\n\t\t\tupdateLockGeometry();\n\t\t\tif (value == 0. && !show) {\n\t\t\t\t_lock->hide();\n\t\t\t} else if (value == 1. && show) {\n\t\t\t\t_lock->requestPaintProgress(calcLockProgress(QCursor::pos()));\n\t\t\t}\n\t\t\tif (_fullRecord && !show) {\n\t\t\t\tupdateTTLGeometry(TTLAnimationType::RightLeft, 1.);\n\t\t\t}\n\t\t};\n\t\t_showLockAnimation.start(std::move(callback), from, to, duration);\n\t}, lifetime());\n\n\tconst auto setLevelAsSend = [=] {\n\t\t_level->setType(VoiceRecordButton::Type::Send);\n\n\t\t_level->clicks(\n\t\t) | rpl::on_next([=] {\n\t\t\tstop(true);\n\t\t}, _recordingLifetime);\n\n\t\trpl::single(\n\t\t\tfalse\n\t\t) | rpl::then(\n\t\t\t_level->actives()\n\t\t) | rpl::on_next([=](bool enter) {\n\t\t\t_inField = enter;\n\t\t}, _recordingLifetime);\n\t};\n\n\tconst auto paintShowListenCallback = [=](float64 value) {\n\t\tif (_listen) {\n\t\t\t_listen->requestPaintProgress(value);\n\t\t}\n\t\t_level->requestPaintProgress(1. - value);\n\t\t_lock->requestPaintPauseToInputProgress(value);\n\t\tupdate();\n\t};\n\n\t_lock->setClickedCallback([=] {\n\t\tif (isListenState()) {\n\t\t\tapplyListenTrimForResume();\n\t\t\tstartRecording();\n\t\t\t_showListenAnimation.stop();\n\t\t\t_showListenAnimation.start([=](float64 value) {\n\t\t\t\t_listen->requestPaintProgress(1.);\n\t\t\t\tpaintShowListenCallback(value);\n\t\t\t\tif (!value) {\n\t\t\t\t\t_listen = nullptr;\n\t\t\t\t}\n\t\t\t}, 1., 0., st::universalDuration * 2);\n\t\t\tsetLevelAsSend();\n\n\t\t\treturn;\n\t\t}\n\t\tif (!_lock->isStopState()) {\n\t\t\treturn;\n\t\t}\n\n\t\tstopRecording(StopType::Listen);\n\t});\n\n\t_paused.value() | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](bool paused) {\n\t\t_lock->setAccessibleName(paused\n\t\t\t? tr::lng_record_lock_resume(tr::now)\n\t\t\t: tr::lng_record_lock(tr::now));\n\t\tif (!paused) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto to = 1.;\n\t\tauto callback = [=](float64 value) {\n\t\t\tpaintShowListenCallback(value);\n\t\t\tif (to == value) {\n\t\t\t\t_recordingLifetime.destroy();\n\t\t\t}\n\t\t};\n\t\t_showListenAnimation.stop();\n\t\t_showListenAnimation.start(\n\t\t\tstd::move(callback),\n\t\t\t0.,\n\t\t\tto,\n\t\t\tst::universalDuration);\n\t}, lifetime());\n\n\t_lock->locks(\n\t) | rpl::on_next([=] {\n\t\tif (_hasTTLFilter && _hasTTLFilter()) {\n\t\t\tif (!_ttlButton) {\n\t\t\t\t_ttlButton = std::make_unique<TTLButton>(\n\t\t\t\t\t_outerContainer,\n\t\t\t\t\t_st,\n\t\t\t\t\t_recordingVideo);\n\t\t\t}\n\t\t\t_ttlButton->show();\n\t\t}\n\t\tupdateTTLGeometry(TTLAnimationType::RightTopStatic, 0);\n\n\t\tsetLevelAsSend();\n\n\t\tauto callback = [=](float64 value) {\n\t\t\t_lock->requestPaintLockToStopProgress(value);\n\t\t\t_level->requestPaintColor(activeAnimationRatio());\n\t\t\tupdate();\n\t\t\tupdateTTLGeometry(TTLAnimationType::RightLeft, value);\n\t\t};\n\t\t_lockToStopAnimation.start(\n\t\t\tstd::move(callback),\n\t\t\t0.,\n\t\t\t1.,\n\t\t\tst::universalDuration);\n\t}, lifetime());\n\n\t_send->events(\n\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\treturn isTypeRecord()\n\t\t\t&& !isRecording()\n\t\t\t&& !_showAnimation.animating()\n\t\t\t&& !_lock->isLocked()\n\t\t\t&& (e->type() == QEvent::MouseButtonPress\n\t\t\t\t|| e->type() == QEvent::MouseButtonRelease);\n\t}) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::MouseButtonPress) {\n\t\t\tif (_startRecordingFilter && _startRecordingFilter()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tprepareOnSendPress();\n\t\t\t_startTimer.callOnce(st::universalDuration);\n\t\t} else if (e->type() == QEvent::MouseButtonRelease) {\n\t\t\tcheckTipRequired();\n\t\t\t_startTimer.cancel();\n\t\t}\n\t}, lifetime());\n\n\t_listenChanges.events(\n\t) | rpl::filter([=] {\n\t\treturn _listen != nullptr;\n\t}) | rpl::on_next([=] {\n\t\t_listen->stopRequests(\n\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\thideAnimated();\n\t\t}, _listen->lifetime());\n\n\t\t_listen->lifetime().add([=] { _listenChanges.fire({}); });\n\n\t\tinstallListenStateFilter();\n\t}, lifetime());\n\n\t_cancel->setClickedCallback([=] {\n\t\thideAnimated();\n\t});\n\n\tinitLockGeometry();\n\tinitLevelGeometry();\n}\n\nvoid VoiceRecordBar::prepareOnSendPress() {\n\t_recordingTipRequire = crl::now();\n\t_recordingVideo = (_send->type() == Ui::SendButton::Type::Round);\n\t_fullRecord = false;\n\t_ttlButton = nullptr;\n\tclearResumeState();\n\t_lock->setRecordingVideo(_recordingVideo);\n}\n\nvoid VoiceRecordBar::applyListenTrimForResume() {\n\tconst auto beforeDuration = _data.duration;\n\tconst auto beforeSize = _data.content.size();\n\t_listen->applyTrimBeforeSend();\n\t_resumeFromTrimmedListen = (_data.duration != beforeDuration)\n\t\t|| (_data.content.size() != beforeSize);\n}\n\nvoid VoiceRecordBar::activeAnimate(bool active) {\n\tconst auto to = active ? 1. : 0.;\n\tif (_activeAnimation.animating()) {\n\t\t_activeAnimation.change(to, st::universalDuration);\n\t} else {\n\t\tauto callback = [=] {\n\t\t\tupdate(_messageRect);\n\t\t\t_level->requestPaintColor(activeAnimationRatio());\n\t\t};\n\t\t_activeAnimation.start(\n\t\t\tstd::move(callback),\n\t\t\tactive ? 0. : 1.,\n\t\t\tto,\n\t\t\tst::universalDuration);\n\t}\n}\n\nvoid VoiceRecordBar::visibilityAnimate(bool show, Fn<void()> &&callback) {\n\tif (_send->type() == Ui::SendButton::Type::Round) {\n\t\t_level->setType(VoiceRecordButton::Type::Round);\n\t} else {\n\t\t_level->setType(VoiceRecordButton::Type::Record);\n\t}\n\tconst auto to = show ? 1. : 0.;\n\tconst auto from = show ? 0. : 1.;\n\tauto animationCallback = [=, callback = std::move(callback)](auto value) {\n\t\tif (!_listen) {\n\t\t\t_level->requestPaintProgress(value);\n\t\t} else {\n\t\t\t_listen->requestPaintProgress(value);\n\t\t}\n\t\tupdate();\n\t\tif (!show) {\n\t\t\tupdateTTLGeometry(TTLAnimationType::RightLeft, value);\n\t\t}\n\t\tif ((show && value == 1.) || (!show && value == 0.)) {\n\t\t\tif (callback) {\n\t\t\t\tcallback();\n\t\t\t}\n\t\t}\n\t};\n\t_showAnimation.start(\n\t\tstd::move(animationCallback),\n\t\tfrom,\n\t\tto,\n\t\tst::universalDuration);\n}\n\nvoid VoiceRecordBar::setStartRecordingFilter(FilterCallback &&callback) {\n\t_startRecordingFilter = std::move(callback);\n}\n\nvoid VoiceRecordBar::setTTLFilter(FilterCallback &&callback) {\n\t_hasTTLFilter = std::move(callback);\n}\n\nvoid VoiceRecordBar::setPauseInsteadSend(bool pauseInsteadSend) {\n\t_pauseInsteadSend = pauseInsteadSend;\n}\n\nvoid VoiceRecordBar::initLockGeometry() {\n\tconst auto parent = static_cast<Ui::RpWidget*>(parentWidget());\n\trpl::merge(\n\t\t_lock->heightValue() | rpl::to_empty,\n\t\tgeometryValue() | rpl::to_empty,\n\t\tparent->geometryValue() | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\tupdateLockGeometry();\n\t}, lifetime());\n\tparent->geometryValue(\n\t) | rpl::on_next([=] {\n\t\tupdateTTLGeometry(TTLAnimationType::RightLeft, 1.);\n\t}, lifetime());\n}\n\nvoid VoiceRecordBar::initLevelGeometry() {\n\trpl::combine(\n\t\t_send->geometryValue(),\n\t\tgeometryValue(),\n\t\tstatic_cast<Ui::RpWidget*>(parentWidget())->geometryValue()\n\t) | rpl::on_next([=](QRect send, auto, auto) {\n\t\tconst auto mapped = Ui::MapFrom(\n\t\t\t_outerContainer,\n\t\t\t_send->parentWidget(),\n\t\t\tsend);\n\t\tconst auto center = (send.width() - _level->width()) / 2;\n\t\t_level->moveToLeft(mapped.x() + center, mapped.y() + center);\n\t}, lifetime());\n}\n\nvoid VoiceRecordBar::startRecordingAndLock(bool round) {\n\t{\n\t\tauto sendState = _send->state();\n\t\tsendState.type = round\n\t\t\t? Ui::SendButton::Type::Round\n\t\t\t: Ui::SendButton::Type::Record;\n\t\t_send->setState(std::move(sendState));\n\t}\n\tif (_startRecordingFilter && _startRecordingFilter()) {\n\t\treturn;\n\t}\n\tprepareOnSendPress();\n\n\t_lock->show();\n\t_lock->requestPaintProgress(1.);\n\tstartRecording();\n}\n\nvoid VoiceRecordBar::startRecording() {\n\tif (isRecording()) {\n\t\treturn;\n\t}\n\tauto appearanceCallback = [=] {\n\t\tif (_showAnimation.animating()) {\n\t\t\treturn;\n\t\t}\n\n\t\tusing namespace ::Media::Capture;\n\t\tif (_recordingVideo && !createVideoRecorder()) {\n\t\t\tstop(false);\n\t\t\treturn;\n\t\t}\n\t\tif (!instance()->available()) {\n\t\t\tstop(false);\n\t\t\treturn;\n\t\t}\n\n\t\t_lockShowing = true;\n\t\tstartRedCircleAnimation();\n\n\t\t_recording = true;\n\t\tif (_paused.current()) {\n\t\t\t_paused = false;\n\t\t\tif (_videoRecorder) {\n\t\t\t\tinstance()->pause(false, nullptr);\n\t\t\t\t_videoRecorder->resume({\n\t\t\t\t\t.video = std::move(_data),\n\t\t\t\t});\n\t\t\t\tclearResumePrefix();\n\t\t\t} else {\n\t\t\t\tinstance()->pause(false, nullptr);\n\t\t\t\tif (_resumeFromTrimmedListen && (_pausedRawDuration > 0)) {\n\t\t\t\t\tsetupResumePrefixFromCurrentData();\n\t\t\t\t\t_recordingSamples = _resumePrefixSamples;\n\t\t\t\t} else {\n\t\t\t\t\tclearResumePrefix();\n\t\t\t\t}\n\t\t\t\t_resumeFromTrimmedListen = false;\n\t\t\t\tupdate(_durationRect);\n\t\t\t}\n\t\t} else {\n\t\t\tclearResumePrefix();\n\t\t\tinstance()->start(_videoRecorder\n\t\t\t\t? _videoRecorder->audioChunkProcessor()\n\t\t\t\t: nullptr);\n\t\t}\n\t\tinstance()->updated(\n\t\t) | rpl::on_next_error([=](const Update &update) {\n\t\t\trecordUpdated(update.level, update.samples);\n\t\t}, [=] {\n\t\t\tstop(false);\n\t\t}, _recordingLifetime);\n\t\tif (_videoRecorder) {\n\t\t\t_videoRecorder->updated(\n\t\t\t) | rpl::on_next_error([=](const Update &update) {\n\t\t\t\trecordUpdated(update.level, update.samples);\n\t\t\t\tif (update.finished) {\n\t\t\t\t\t_fullRecord = true;\n\t\t\t\t\tstopRecording(StopType::Listen);\n\t\t\t\t\t_lockShowing = false;\n\t\t\t\t}\n\t\t\t}, [=](Error error) {\n\t\t\t\tstop(false);\n\t\t\t\t_errors.fire_copy(error);\n\t\t\t}, _recordingLifetime);\n\t\t}\n\t\t_recordingLifetime.add([=] {\n\t\t\t_recording = false;\n\t\t});\n\t};\n\tvisibilityAnimate(true, std::move(appearanceCallback));\n\tshow();\n\n\t_inField = true;\n\n\tstruct FloatingState {\n\t\tUi::Animations::Basic animation;\n\t\tfloat64 animationProgress = 0;\n\t\tfloat64 cursorProgress = 0;\n\t\tbool lockCapturedByInput = false;\n\t\tfloat64 frameCounter = 0;\n\t\trpl::lifetime lifetime;\n\t};\n\tconst auto stateOwned\n\t\t= _recordingLifetime.make_state<std::unique_ptr<FloatingState>>(\n\t\t\tstd::make_unique<FloatingState>());\n\tconst auto state = stateOwned->get();\n\n\t_lock->locks() | rpl::on_next([=] {\n\t\tstateOwned->reset();\n\t}, state->lifetime);\n\n\tconstexpr auto kAnimationThreshold = 0.35;\n\tconst auto calcStateRatio = [=](float64 counter) {\n\t\treturn (1 - std::cos(std::fmod(counter, 2 * M_PI))) * 0.5;\n\t};\n\tstate->animation.init([=](crl::time now) {\n\t\tif (state->cursorProgress > kAnimationThreshold) {\n\t\t\tstate->lockCapturedByInput = true;\n\t\t}\n\t\tif (state->lockCapturedByInput) {\n\t\t\tif (state->cursorProgress < 0.01) {\n\t\t\t\tstate->lockCapturedByInput = false;\n\t\t\t\tstate->frameCounter = 0;\n\t\t\t} else {\n\t\t\t\t_lock->requestPaintProgress(state->cursorProgress);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tconst auto progress = anim::interpolateF(\n\t\t\tstate->cursorProgress,\n\t\t\tkAnimationThreshold,\n\t\t\tcalcStateRatio(state->frameCounter));\n\t\tstate->frameCounter += 0.01;\n\t\t_lock->requestPaintProgress(progress);\n\t});\n\tstate->animation.start();\n\tif (hasDuration()) {\n\t\tstateOwned->reset();\n\t}\n\n\t_send->events(\n\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\treturn (e->type() == QEvent::MouseMove\n\t\t\t|| e->type() == QEvent::MouseButtonRelease)\n\t\t\t&& isTypeRecord()\n\t\t\t&& !_lock->isLocked();\n\t}) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::MouseMove) {\n\t\t\tconst auto mouse = static_cast<QMouseEvent*>(e.get());\n\t\t\tconst auto globalPos = mouse->globalPos();\n\t\t\tconst auto localPos = mapFromGlobal(globalPos);\n\t\t\tconst auto inField = rect().contains(localPos);\n\t\t\t_inField = inField\n\t\t\t\t? inField\n\t\t\t\t: _level->inCircle(_level->mapFromGlobal(globalPos));\n\n\t\t\tif (_showLockAnimation.animating() || !hasDuration()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto inputProgress = calcLockProgress(mouse->globalPos());\n\t\t\tif (inputProgress > state->animationProgress) {\n\t\t\t\tstate->cursorProgress = inputProgress;\n\t\t\t}\n\t\t} else if (type == QEvent::MouseButtonRelease) {\n\t\t\tcheckTipRequired();\n\t\t\tstop(_inField.current());\n\t\t}\n\t}, _recordingLifetime);\n\n\t_listenChanges.events_starting_with(\n\t\trpl::empty_value()\n\t) | rpl::filter([=] {\n\t\treturn _listen == nullptr;\n\t}) | rpl::on_next([=] {\n\t\tauto keyFilterCallback = [=](not_null<QEvent*> e) {\n\t\t\tusing Result = base::EventFilterResult;\n\t\t\tif (_send->type() != Ui::SendButton::Type::Record\n\t\t\t\t&& _send->type() != Ui::SendButton::Type::Round) {\n\t\t\t\treturn Result::Continue;\n\t\t\t}\n\t\t\tswitch(e->type()) {\n\t\t\tcase QEvent::KeyPress: {\n\t\t\t\tif (!_warningShown\n\t\t\t\t\t&& isRecordingLocked()\n\t\t\t\t\t&& Ui::ShouldSubmit(\n\t\t\t\t\t\tstatic_cast<QKeyEvent*>(e.get()),\n\t\t\t\t\t\tCore::App().settings().sendSubmitWay())) {\n\t\t\t\t\tstop(true);\n\t\t\t\t\treturn Result::Cancel;\n\t\t\t\t}\n\t\t\t\treturn Result::Continue;\n\t\t\t}\n\t\t\tdefault: return Result::Continue;\n\t\t\t}\n\t\t};\n\n\t\t_keyFilterInRecordingState = base::unique_qptr{\n\t\t\tbase::install_event_filter(\n\t\t\t\tQCoreApplication::instance(),\n\t\t\t\tstd::move(keyFilterCallback)).get()\n\t\t};\n\t}, lifetime());\n}\n\nvoid VoiceRecordBar::checkTipRequired() {\n\tconst auto require = base::take(_recordingTipRequire);\n\tconst auto duration = st::universalDuration\n\t\t+ (kMinSamples * crl::time(1000)\n\t\t\t/ ::Media::Player::kDefaultFrequency);\n\tif (require && (require + duration > crl::now())) {\n\t\t_recordingTipRequests.fire({});\n\t}\n}\n\nvoid VoiceRecordBar::recordUpdated(quint16 level, int samples) {\n\t_level->requestPaintLevel(level);\n\tconst auto resumedSamples = std::max(0, samples - _resumeRawSamples);\n\tconst auto totalSamples = _resumePrefixSamples + resumedSamples;\n\t_recordingSamples = totalSamples;\n\tif (totalSamples < 0 || totalSamples >= kMaxSamples) {\n\t\tstop(totalSamples > 0 && _inField.current());\n\t}\n\tCore::App().updateNonIdle();\n\tupdate(_durationRect);\n\tconst auto type = _recordingVideo\n\t\t? Api::SendProgressType::RecordRound\n\t\t: Api::SendProgressType::RecordVoice;\n\t_sendActionUpdates.fire({ type });\n}\n\nvoid VoiceRecordBar::stop(bool send) {\n\tif (isHidden() && !send) {\n\t\treturn;\n\t} else if (send && _pauseInsteadSend) {\n\t\t_fullRecord = true;\n\t\tstopRecording(StopType::Listen);\n\t\t_lockShowing = false;\n\t\treturn;\n\t}\n\tconst auto ttlBeforeHide = peekTTLState();\n\tauto disappearanceCallback = [=] {\n\t\thide();\n\n\t\tconst auto type = send ? StopType::Send : StopType::Cancel;\n\t\tstopRecording(type, ttlBeforeHide);\n\t};\n\tvisibilityAnimate(false, std::move(disappearanceCallback));\n}\n\nvoid VoiceRecordBar::finish() {\n\t_recordingLifetime.destroy();\n\t_lockShowing = false;\n\t_inField = false;\n\t_redCircleProgress = 0.;\n\t_recordingSamples = 0;\n\t_paused = false;\n\n\t_showAnimation.stop();\n\t_lockToStopAnimation.stop();\n\n\t_listen = nullptr;\n\n\t[[maybe_unused]] const auto s = takeTTLState();\n\n\tconst auto type = _recordingVideo\n\t\t? Api::SendProgressType::RecordRound\n\t\t: Api::SendProgressType::RecordVoice;\n\t_sendActionUpdates.fire({ type, -1 });\n\n\t_data = {};\n}\n\nvoid VoiceRecordBar::hideFast() {\n\thide();\n\t_lock->hide();\n\t_level->hide();\n\t[[maybe_unused]] const auto s = takeTTLState();\n\t_keyFilterInRecordingState = nullptr;\n}\n\nvoid VoiceRecordBar::clearResumePrefix() {\n\t_resumePrefixData = {};\n\t_resumePrefixSamples = 0;\n\t_resumeRawSamples = 0;\n\t_resumeRawDuration = 0;\n\t_resumeFromTrimmedListen = false;\n}\n\nvoid VoiceRecordBar::clearResumeState() {\n\t_pausedRawDuration = 0;\n\tclearResumePrefix();\n}\n\nvoid VoiceRecordBar::setupResumePrefixFromCurrentData() {\n\t_resumePrefixData = _data;\n\t_resumePrefixSamples = samplesFromDuration(_resumePrefixData.duration);\n\t_resumeRawDuration = _pausedRawDuration;\n\t_resumeRawSamples = samplesFromDuration(_resumeRawDuration);\n}\n\nint VoiceRecordBar::samplesFromDuration(crl::time duration) const {\n\treturn int((duration * ::Media::Player::kDefaultFrequency)\n\t\t/ crl::time(1000));\n}\n\nUi::RoundVideoResult VoiceRecordBar::mergeWithResumePrefix(\n\t\tUi::RoundVideoResult data) {\n\tif (_recordingVideo || _resumePrefixData.content.isEmpty()) {\n\t\treturn data;\n\t}\n\tif (data.content.isEmpty()) {\n\t\treturn _resumePrefixData;\n\t}\n\tconst auto tail = (_resumeRawDuration > 0)\n\t\t? ::Media::TrimAudioToRange(\n\t\t\tdata.content,\n\t\t\t_resumeRawDuration,\n\t\t\tdata.duration)\n\t\t: ::Media::AudioEditResult();\n\tif ((_resumeRawDuration > 0) && tail.content.isEmpty()) {\n\t\treturn _resumePrefixData;\n\t}\n\tconst auto combined = ::Media::ConcatAudio(\n\t\t_resumePrefixData.content,\n\t\t(_resumeRawDuration > 0) ? tail.content : data.content);\n\tif (combined.content.isEmpty()) {\n\t\treturn _resumePrefixData;\n\t}\n\tconst auto tailDuration = (_resumeRawDuration > 0)\n\t\t? tail.duration\n\t\t: data.duration;\n\tconst auto duration = combined.duration\n\t\t? combined.duration\n\t\t: (_resumePrefixData.duration + tailDuration);\n\tauto waveform = MergeWaveformsByDuration(\n\t\t_resumePrefixData.waveform,\n\t\t_resumePrefixData.duration,\n\t\t(_resumeRawDuration > 0) ? tail.waveform : data.waveform,\n\t\ttailDuration);\n\tif (waveform.isEmpty()) {\n\t\twaveform = std::move(combined.waveform);\n\t}\n\treturn Ui::RoundVideoResult{\n\t\t.content = std::move(combined.content),\n\t\t.waveform = std::move(waveform),\n\t\t.duration = duration,\n\t};\n}\n\nvoid VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {\n\tusing namespace ::Media::Capture;\n\tif (type == StopType::Cancel) {\n\t\tclearResumeState();\n\t\tif (_videoRecorder) {\n\t\t\t_videoRecorder->hide();\n\t\t}\n\t\tinstance()->stop(crl::guard(this, [=](Result &&data) {\n\t\t\t_cancelRequests.fire({});\n\t\t}));\n\t} else if (type == StopType::Listen) {\n\t\tif (const auto recorder = _videoRecorder.get()) {\n\t\t\tconst auto weak = base::make_weak(recorder);\n\t\t\trecorder->pause([=](Ui::RoundVideoResult data) {\n\t\t\t\tcrl::on_main(weak, [=, data = std::move(data)]() mutable {\n\t\t\t\t\twindow()->raise();\n\t\t\t\t\twindow()->activateWindow();\n\n\t\t\t\t\t_paused = true;\n\t\t\t\t\t_data = std::move(data);\n\t\t\t\t\t_listen = std::make_unique<ListenWrap>(\n\t\t\t\t\t\tthis,\n\t\t\t\t\t\t_st,\n\t\t\t\t\t\t_send,\n\t\t\t\t\t\t&_show->session(),\n\t\t\t\t\t\t&_data,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t_cancelFont);\n\t\t\t\t\t_listenChanges.fire({});\n\n\t\t\t\t\tusing SilentPreview = ::Media::Streaming::RoundPreview;\n\t\t\t\t\trecorder->showPreview(\n\t\t\t\t\t\tstd::make_shared<SilentPreview>(\n\t\t\t\t\t\t\t_data.content,\n\t\t\t\t\t\t\trecorder->previewSize()),\n\t\t\t\t\t\t_listen->videoPreview());\n\t\t\t\t});\n\t\t\t});\n\t\t\tinstance()->pause(true);\n\t\t} else {\n\t\t\tinstance()->pause(true, crl::guard(this, [=](Result &&data) {\n\t\t\t\tconst auto rawDuration = data.duration;\n\t\t\t\tauto combined = mergeWithResumePrefix(\n\t\t\t\t\tToRoundVideoResult(std::move(data)));\n\t\t\t\tclearResumePrefix();\n\t\t\t\tif (combined.content.isEmpty()) {\n\t\t\t\t\t// Close everything.\n\t\t\t\t\tstop(false);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t_pausedRawDuration = rawDuration;\n\t\t\t\t_paused = true;\n\t\t\t\t_data = std::move(combined);\n\n\t\t\t\twindow()->raise();\n\t\t\t\twindow()->activateWindow();\n\t\t\t\t_listen = std::make_unique<ListenWrap>(\n\t\t\t\t\tthis,\n\t\t\t\t\t_st,\n\t\t\t\t\t_send,\n\t\t\t\t\t&_show->session(),\n\t\t\t\t\t&_data,\n\t\t\t\t\ttrue,\n\t\t\t\t\t_cancelFont);\n\t\t\t\t_listenChanges.fire({});\n\t\t\t}));\n\t\t}\n\t} else if (type == StopType::Send) {\n\t\tif (_videoRecorder) {\n\t\t\tconst auto weak = base::make_weak(this);\n\t\t\t_videoRecorder->hide([=](Ui::RoundVideoResult data) {\n\t\t\t\tcrl::on_main([=, data = std::move(data)]() mutable {\n\t\t\t\t\tif (weak) {\n\t\t\t\t\t\twindow()->raise();\n\t\t\t\t\t\twindow()->activateWindow();\n\t\t\t\t\t\tconst auto options = Api::SendOptions{\n\t\t\t\t\t\t\t.ttlSeconds = (ttlBeforeHide\n\t\t\t\t\t\t\t\t? std::numeric_limits<int>::max()\n\t\t\t\t\t\t\t\t: 0),\n\t\t\t\t\t\t};\n\t\t\t\t\t\t_sendVoiceRequests.fire({\n\t\t\t\t\t\t\t.bytes = data.content,\n\t\t\t\t\t\t\t//.waveform = {},\n\t\t\t\t\t\t\t.duration = data.duration,\n\t\t\t\t\t\t\t.options = options,\n\t\t\t\t\t\t\t.video = true,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t\tinstance()->stop(crl::guard(this, [=](Result &&data) {\n\t\t\t_pausedRawDuration = 0;\n\t\t\tauto combined = mergeWithResumePrefix(\n\t\t\t\tToRoundVideoResult(std::move(data)));\n\t\t\tclearResumePrefix();\n\t\t\tif (combined.content.isEmpty()) {\n\t\t\t\t// Close everything.\n\t\t\t\tstop(false);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_data = std::move(combined);\n\n\t\t\twindow()->raise();\n\t\t\twindow()->activateWindow();\n\t\t\tconst auto options = Api::SendOptions{\n\t\t\t\t.ttlSeconds = (ttlBeforeHide\n\t\t\t\t\t? std::numeric_limits<int>::max()\n\t\t\t\t\t: 0),\n\t\t\t};\n\t\t\t_sendVoiceRequests.fire({\n\t\t\t\t.bytes = _data.content,\n\t\t\t\t.waveform = _data.waveform,\n\t\t\t\t.duration = _data.duration,\n\t\t\t\t.options = options,\n\t\t\t});\n\t\t}));\n\t}\n}\n\nvoid VoiceRecordBar::drawDuration(QPainter &p) {\n\tconst auto duration = FormatVoiceDuration(_recordingSamples);\n\tp.setFont(_cancelFont);\n\tp.setPen(_st.durationFg);\n\n\tp.drawText(_durationRect, style::al_left, duration);\n}\n\nvoid VoiceRecordBar::startRedCircleAnimation() {\n\tif (anim::Disabled()) {\n\t\treturn;\n\t}\n\tconst auto animation\n\t\t= _recordingLifetime.make_state<Ui::Animations::Basic>();\n\tanimation->init([=](crl::time now) {\n\t\tconst auto diffTime = now - animation->started();\n\t\t_redCircleProgress = std::abs(std::sin(diffTime / 400.));\n\t\tupdate(_redCircleRect);\n\t\treturn true;\n\t});\n\tanimation->start();\n}\n\nvoid VoiceRecordBar::drawRedCircle(QPainter &p) {\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(st::historyRecordVoiceFgInactive);\n\n\tconst auto opacity = p.opacity();\n\tp.setOpacity(opacity * (1. - _redCircleProgress));\n\tconst int radii = st::historyRecordSignalRadius * showAnimationRatio();\n\tconst auto center = _redCircleRect.center() + QPoint(1, 1);\n\tp.drawEllipse(center, radii, radii);\n\tp.setOpacity(opacity);\n}\n\nvoid VoiceRecordBar::drawMessage(QPainter &p, float64 recordActive) {\n\tp.setPen(anim::pen(_st.cancel, _st.cancelActive, 1. - recordActive));\n\n\tconst auto opacity = p.opacity();\n\tp.setOpacity(opacity * (1. - _lock->lockToStopProgress()));\n\n\t_message.draw(p, {\n\t\t.position = _messageRect.topLeft(),\n\t\t.outerWidth = _messageRect.width(),\n\t\t.availableWidth = _messageRect.width(),\n\t\t.align = style::al_center,\n\t});\n\n\tp.setOpacity(opacity);\n}\n\nvoid VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) {\n\tif (isListenState()) {\n\t\tif (takeTTLState()) {\n\t\t\toptions.ttlSeconds = std::numeric_limits<int>::max();\n\t\t}\n\t\tif (_listen) {\n\t\t\t_listen->prepareForSendAnimation();\n\t\t\t_listen->applyTrimBeforeSend();\n\t\t}\n\t\t_sendVoiceRequests.fire({\n\t\t\t.bytes = _data.content,\n\t\t\t.waveform = _data.waveform,\n\t\t\t.duration = _data.duration,\n\t\t\t.options = options,\n\t\t\t.video = !_data.minithumbs.isNull(),\n\t\t});\n\t}\n}\n\nrpl::producer<SendActionUpdate> VoiceRecordBar::sendActionUpdates() const {\n\treturn _sendActionUpdates.events();\n}\n\nrpl::producer<VoiceToSend> VoiceRecordBar::sendVoiceRequests() const {\n\treturn _sendVoiceRequests.events();\n}\n\nrpl::producer<> VoiceRecordBar::cancelRequests() const {\n\treturn _cancelRequests.events();\n}\n\nbool VoiceRecordBar::isRecording() const {\n\treturn _recording.current() && !_paused.current();\n}\n\nbool VoiceRecordBar::isRecordingLocked() const {\n\treturn isRecording() && _lock->isLocked();\n}\n\nbool VoiceRecordBar::isActive() const {\n\treturn isRecording() || isListenState();\n}\n\nvoid VoiceRecordBar::hideAnimated() {\n\tif (isHidden()) {\n\t\treturn;\n\t}\n\t_lockShowing = false;\n\tvisibilityAnimate(false, [=] {\n\t\thideFast();\n\t\tstopRecording(StopType::Cancel);\n\t});\n}\n\nvoid VoiceRecordBar::finishAnimating() {\n\t_showAnimation.stop();\n}\n\nrpl::producer<bool> VoiceRecordBar::recordingStateChanges() const {\n\treturn _recording.changes();\n}\n\nrpl::producer<bool> VoiceRecordBar::lockShowStarts() const {\n\treturn _lockShowing.changes();\n}\n\nrpl::producer<not_null<QEvent*>> VoiceRecordBar::lockViewportEvents() const {\n\treturn _lock->events(\n\t\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\t\treturn e->type() == QEvent::Wheel;\n\t\t});\n}\n\nrpl::producer<> VoiceRecordBar::updateSendButtonTypeRequests() const {\n\treturn rpl::merge(\n\t\t::Media::Capture::instance()->startedChanges(\n\t\t) | rpl::filter([=] {\n\t\t\t// Perhaps a voice is recording from another place.\n\t\t\treturn !isActive();\n\t\t}) | rpl::to_empty,\n\t\t_listenChanges.events());\n}\n\nrpl::producer<> VoiceRecordBar::recordingTipRequests() const {\n\treturn _recordingTipRequests.events();\n}\n\nauto VoiceRecordBar::errors() const -> rpl::producer<Error> {\n\treturn _errors.events();\n}\n\nbool VoiceRecordBar::isLockPresent() const {\n\treturn _lockShowing.current();\n}\n\nbool VoiceRecordBar::isListenState() const {\n\treturn _listen != nullptr;\n}\n\nbool VoiceRecordBar::isTypeRecord() const {\n\treturn (_send->type() == Ui::SendButton::Type::Record)\n\t\t|| (_send->type() == Ui::SendButton::Type::Round);\n}\n\nbool VoiceRecordBar::isRecordingByAnotherBar() const {\n\treturn !isRecording() && ::Media::Capture::instance()->started();\n}\n\nbool VoiceRecordBar::isTTLButtonShown() const {\n\treturn _ttlButton && !_ttlButton->isHidden();\n}\n\nbool VoiceRecordBar::hasDuration() const {\n\treturn _recordingSamples > 0;\n}\n\nfloat64 VoiceRecordBar::activeAnimationRatio() const {\n\tif (isRecordingLocked()) {\n\t\treturn 1.;\n\t}\n\treturn _activeAnimation.value(_inField.current() ? 1. : 0.);\n}\n\nvoid VoiceRecordBar::clearListenState() {\n\tif (isListenState()) {\n\t\thideAnimated();\n\t}\n}\n\nfloat64 VoiceRecordBar::showAnimationRatio() const {\n\t// There is no reason to set the final value to zero,\n\t// because at zero this widget is hidden.\n\treturn _showAnimation.value(1.);\n}\n\nfloat64 VoiceRecordBar::showListenAnimationRatio() const {\n\tconst auto value = _showListenAnimation.value(_listen ? 1. : 0.);\n\tif (_paused.current()) {\n\t\treturn value * value;\n\t}\n\treturn value;\n}\n\nvoid VoiceRecordBar::computeAndSetLockProgress(QPoint globalPos) {\n\t_lock->requestPaintProgress(calcLockProgress(globalPos));\n}\n\nfloat64 VoiceRecordBar::calcLockProgress(QPoint globalPos) {\n\tconst auto localPos = mapFromGlobal(globalPos);\n\tconst auto lower = _lock->height();\n\tconst auto higher = 0;\n\treturn Progress(localPos.y(), higher - lower);\n}\n\nbool VoiceRecordBar::peekTTLState() const {\n\treturn _ttlButton && !_ttlButton->isDisabled();\n}\n\nbool VoiceRecordBar::takeTTLState() const {\n\tif (!_ttlButton) {\n\t\treturn false;\n\t}\n\tconst auto hasTtl = !_ttlButton->isDisabled();\n\t_ttlButton->clearState();\n\treturn hasTtl;\n}\n\nvoid VoiceRecordBar::orderControls() {\n\tstackUnder(_send.get());\n\t_lock->raise();\n\t_level->raise();\n}\n\nvoid VoiceRecordBar::installListenStateFilter() {\n\t_keyFilterInRecordingState = nullptr;\n\tauto keyFilterCallback = [=](not_null<QEvent*> e) {\n\t\tusing Result = base::EventFilterResult;\n\t\tif (!(_send->type() == Ui::SendButton::Type::Send\n\t\t\t|| _send->type() == Ui::SendButton::Type::Schedule)) {\n\t\t\treturn Result::Continue;\n\t\t}\n\t\tswitch(e->type()) {\n\t\tcase QEvent::KeyPress: {\n\t\t\tconst auto keyEvent = static_cast<QKeyEvent*>(e.get());\n\t\t\tconst auto key = keyEvent->key();\n\t\t\tconst auto isSpace = (key == Qt::Key_Space);\n\t\t\tconst auto isEnter = (key == Qt::Key_Enter\n\t\t\t\t|| key == Qt::Key_Return);\n\t\t\tif (isSpace && !keyEvent->isAutoRepeat() && _listen) {\n\t\t\t\t_listen->playPause();\n\t\t\t\treturn Result::Cancel;\n\t\t\t}\n\t\t\tif (isEnter && !_warningShown) {\n\t\t\t\trequestToSendWithOptions({});\n\t\t\t\treturn Result::Cancel;\n\t\t\t}\n\t\t\treturn Result::Continue;\n\t\t}\n\t\tdefault: return Result::Continue;\n\t\t}\n\t};\n\n\tauto keyFilter = base::install_event_filter(\n\t\tQCoreApplication::instance(),\n\t\tstd::move(keyFilterCallback));\n\n\t_listen->lifetime().make_state<base::unique_qptr<QObject>>(\n\t\tstd::move(keyFilter));\n}\n\nvoid VoiceRecordBar::showDiscardBox(\n\t\tFn<void()> &&callback,\n\t\tanim::type animated) {\n\tif (!isActive() || _showAnimation.animating()) {\n\t\treturn;\n\t}\n\tauto sure = [=, callback = std::move(callback)](Fn<void()> &&close) {\n\t\tif (animated == anim::type::instant) {\n\t\t\thideFast();\n\t\t\tstopRecording(StopType::Cancel);\n\t\t} else {\n\t\t\thideAnimated();\n\t\t}\n\t\tclose();\n\t\t_warningShown = false;\n\t\tif (callback) {\n\t\t\tcallback();\n\t\t}\n\t};\n\t_show->showBox(Ui::MakeConfirmBox({\n\t\t.text = (isListenState()\n\t\t\t? (_recordingVideo\n\t\t\t\t? tr::lng_record_listen_cancel_sure_round\n\t\t\t\t: tr::lng_record_listen_cancel_sure)\n\t\t\t: (_recordingVideo\n\t\t\t\t? tr::lng_record_lock_cancel_sure_round\n\t\t\t\t: tr::lng_record_lock_cancel_sure))(),\n\t\t.confirmed = std::move(sure),\n\t\t.confirmText = tr::lng_record_lock_discard(),\n\t\t.confirmStyle = &st::attentionBoxButton,\n\t}));\n\t_warningShown = true;\n}\n\nbool VoiceRecordBar::createVideoRecorder() {\n\tif (_videoRecorder) {\n\t\treturn true;\n\t}\n\tconst auto hiding = [=](not_null<Ui::RoundVideoRecorder*> which) {\n\t\tif (_videoRecorder.get() == which) {\n\t\t\t_videoHiding.push_back(base::take(_videoRecorder));\n\t\t}\n\t};\n\tconst auto hidden = [=](not_null<Ui::RoundVideoRecorder*> which) {\n\t\tif (_videoRecorder.get() == which) {\n\t\t\t_videoRecorder = nullptr;\n\t\t}\n\t\t_videoHiding.erase(\n\t\t\tranges::remove(\n\t\t\t\t_videoHiding,\n\t\t\t\twhich.get(),\n\t\t\t\t&std::unique_ptr<Ui::RoundVideoRecorder>::get),\n\t\t\tend(_videoHiding));\n\t};\n\tauto capturer = Core::App().calls().getVideoCapture();\n\tauto track = std::make_shared<Webrtc::VideoTrack>(\n\t\tWebrtc::VideoState::Active);\n\tcapturer->setOutput(track->sink());\n\tcapturer->setPreferredAspectRatio(1.);\n\t_videoCapturerLifetime = track->stateValue(\n\t) | rpl::on_next([=](Webrtc::VideoState state) {\n\t\tcapturer->setState((state == Webrtc::VideoState::Active)\n\t\t\t? tgcalls::VideoState::Active\n\t\t\t: tgcalls::VideoState::Inactive);\n\t});\n\t_videoRecorder = std::make_unique<Ui::RoundVideoRecorder>(\n\t\tUi::RoundVideoRecorderDescriptor{\n\t\t\t.container = _outerContainer,\n\t\t\t.hiding = hiding,\n\t\t\t.hidden = hidden,\n\t\t\t.capturer = std::move(capturer),\n\t\t\t.track = std::move(track),\n\t\t\t.placeholder = _show->session().local().readRoundPlaceholder(),\n\t\t});\n\t_videoRecorder->placeholderUpdates(\n\t) | rpl::on_next([=](QImage &&placeholder) {\n\t\t_show->session().local().writeRoundPlaceholder(placeholder);\n\t}, _videoCapturerLifetime);\n\n\treturn true;\n}\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_common.h\"\n#include \"base/timer.h\"\n#include \"history/view/controls/compose_controls_common.h\"\n#include \"media/audio/media_audio_capture_common.h\"\n#include \"ui/controls/round_video_recorder.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/round_rect.h\"\n#include \"ui/rp_widget.h\"\n\nstruct VoiceData;\n\nnamespace style {\nstruct RecordBar;\n} // namespace style\n\nnamespace Media::Capture {\nenum class Error : uchar;\n} // namespace Media::Capture\n\nnamespace Ui {\nclass AbstractButton;\nclass SendButton;\nclass RoundVideoRecorder;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace HistoryView::Controls {\n\nclass VoiceRecordButton;\nclass ListenWrap;\nclass RecordLock;\nclass CancelButton;\n\nstruct VoiceRecordBarDescriptor {\n\tnot_null<Ui::RpWidget*> outerContainer;\n\tstd::shared_ptr<ChatHelpers::Show> show;\n\tstd::shared_ptr<Ui::SendButton> send;\n\tQString customCancelText;\n\tconst style::RecordBar *stOverride = nullptr;\n\tint recorderHeight = 0;\n\tbool lockFromBottom = false;\n};\n\nclass VoiceRecordBar final : public Ui::RpWidget {\npublic:\n\tusing SendActionUpdate = Controls::SendActionUpdate;\n\tusing VoiceToSend = Controls::VoiceToSend;\n\tusing FilterCallback = Fn<bool()>;\n\tusing Error = ::Media::Capture::Error;\n\n\tVoiceRecordBar(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tVoiceRecordBarDescriptor &&descriptor);\n\tVoiceRecordBar(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tstd::shared_ptr<Ui::SendButton> send,\n\t\tint recorderHeight);\n\t~VoiceRecordBar();\n\n\tvoid showDiscardBox(\n\t\tFn<void()> &&callback,\n\t\tanim::type animated = anim::type::instant);\n\n\tvoid startRecordingAndLock(bool round);\n\n\tvoid finishAnimating();\n\tvoid hideAnimated();\n\tvoid hideFast();\n\tvoid clearListenState();\n\n\tvoid orderControls();\n\n\t[[nodiscard]] rpl::producer<SendActionUpdate> sendActionUpdates() const;\n\t[[nodiscard]] rpl::producer<VoiceToSend> sendVoiceRequests() const;\n\t[[nodiscard]] rpl::producer<> cancelRequests() const;\n\t[[nodiscard]] rpl::producer<bool> recordingStateChanges() const;\n\t[[nodiscard]] rpl::producer<bool> lockShowStarts() const;\n\t[[nodiscard]] rpl::producer<not_null<QEvent*>> lockViewportEvents() const;\n\t[[nodiscard]] rpl::producer<> updateSendButtonTypeRequests() const;\n\t[[nodiscard]] rpl::producer<> recordingTipRequests() const;\n\t[[nodiscard]] rpl::producer<Error> errors() const;\n\n\tvoid requestToSendWithOptions(Api::SendOptions options);\n\n\tvoid setStartRecordingFilter(FilterCallback &&callback);\n\tvoid setTTLFilter(FilterCallback &&callback);\n\tvoid setPauseInsteadSend(bool pauseInsteadSend);\n\n\t[[nodiscard]] bool isRecording() const;\n\t[[nodiscard]] bool isRecordingLocked() const;\n\t[[nodiscard]] bool isLockPresent() const;\n\t[[nodiscard]] bool isListenState() const;\n\t[[nodiscard]] bool isActive() const;\n\t[[nodiscard]] bool isRecordingByAnotherBar() const;\n\t[[nodiscard]] bool isTTLButtonShown() const;\n\nprivate:\n\tenum class StopType {\n\t\tCancel,\n\t\tSend,\n\t\tListen,\n\t};\n\n\tenum class TTLAnimationType {\n\t\tRightLeft,\n\t\tTopBottom,\n\t\tRightTopStatic,\n\t};\n\n\tvoid init();\n\tvoid initLockGeometry();\n\tvoid initLevelGeometry();\n\n\tvoid updateMessageGeometry();\n\tvoid updateLockGeometry();\n\tvoid updateTTLGeometry(TTLAnimationType type, float64 progress);\n\n\tvoid recordUpdated(quint16 level, int samples);\n\tvoid checkTipRequired();\n\n\tvoid stop(bool send);\n\tvoid stopRecording(StopType type, bool ttlBeforeHide = false);\n\tvoid visibilityAnimate(bool show, Fn<void()> &&callback);\n\n\tvoid drawDuration(QPainter &p);\n\tvoid drawRedCircle(QPainter &p);\n\tvoid drawMessage(QPainter &p, float64 recordActive);\n\n\tvoid startRedCircleAnimation();\n\tvoid installListenStateFilter();\n\n\tvoid startRecording();\n\tvoid prepareOnSendPress();\n\tvoid applyListenTrimForResume();\n\tvoid clearResumePrefix();\n\tvoid clearResumeState();\n\tvoid setupResumePrefixFromCurrentData();\n\t[[nodiscard]] int samplesFromDuration(crl::time duration) const;\n\t[[nodiscard]] Ui::RoundVideoResult mergeWithResumePrefix(\n\t\tUi::RoundVideoResult data);\n\n\t[[nodiscard]] bool isTypeRecord() const;\n\t[[nodiscard]] bool hasDuration() const;\n\n\tvoid finish();\n\n\tvoid activeAnimate(bool active);\n\t[[nodiscard]] float64 showAnimationRatio() const;\n\t[[nodiscard]] float64 showListenAnimationRatio() const;\n\t[[nodiscard]] float64 activeAnimationRatio() const;\n\n\tvoid computeAndSetLockProgress(QPoint globalPos);\n\t[[nodiscard]] float64 calcLockProgress(QPoint globalPos);\n\n\t[[nodiscard]] bool peekTTLState() const;\n\t[[nodiscard]] bool takeTTLState() const;\n\n\t[[nodiscard]] bool createVideoRecorder();\n\n\tconst style::RecordBar &_st;\n\tconst not_null<Ui::RpWidget*> _outerContainer;\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst std::shared_ptr<Ui::SendButton> _send;\n\tconst std::unique_ptr<RecordLock> _lock;\n\tconst std::unique_ptr<VoiceRecordButton> _level;\n\tconst std::unique_ptr<CancelButton> _cancel;\n\tstd::unique_ptr<Ui::AbstractButton> _ttlButton;\n\tstd::unique_ptr<ListenWrap> _listen;\n\n\tUi::RoundVideoResult _data;\n\tUi::RoundVideoResult _resumePrefixData;\n\tint _resumePrefixSamples = 0;\n\tint _resumeRawSamples = 0;\n\tcrl::time _pausedRawDuration = 0;\n\tcrl::time _resumeRawDuration = 0;\n\tbool _resumeFromTrimmedListen = false;\n\trpl::variable<bool> _paused;\n\n\tbase::Timer _startTimer;\n\n\trpl::event_stream<SendActionUpdate> _sendActionUpdates;\n\trpl::event_stream<VoiceToSend> _sendVoiceRequests;\n\trpl::event_stream<> _cancelRequests;\n\trpl::event_stream<> _listenChanges;\n\trpl::event_stream<Error> _errors;\n\n\tint _centerY = 0;\n\tQRect _redCircleRect;\n\tQRect _durationRect;\n\tQRect _messageRect;\n\n\tUi::Text::String _message;\n\n\tFilterCallback _startRecordingFilter;\n\tFilterCallback _hasTTLFilter;\n\n\tbase::unique_qptr<QObject> _keyFilterInRecordingState;\n\n\tbool _warningShown = false;\n\tbool _pauseInsteadSend = false;\n\n\trpl::variable<bool> _recording = false;\n\trpl::variable<bool> _inField = false;\n\trpl::variable<bool> _lockShowing = false;\n\tint _recordingSamples = 0;\n\tfloat64 _redCircleProgress = 0.;\n\n\trpl::event_stream<> _recordingTipRequests;\n\tcrl::time _recordingTipRequire = 0;\n\tbool _lockFromBottom = false;\n\n\tstd::unique_ptr<Ui::RoundVideoRecorder> _videoRecorder;\n\tstd::vector<std::unique_ptr<Ui::RoundVideoRecorder>> _videoHiding;\n\trpl::lifetime _videoCapturerLifetime;\n\tbool _recordingVideo = false;\n\tbool _fullRecord = false;\n\n\tconst style::font &_cancelFont;\n\n\trpl::lifetime _recordingLifetime;\n\n\tstd::optional<Ui::RoundRect> _backgroundRect;\n\tUi::Animations::Simple _showLockAnimation;\n\tUi::Animations::Simple _lockToStopAnimation;\n\tUi::Animations::Simple _showListenAnimation;\n\tUi::Animations::Simple _activeAnimation;\n\tUi::Animations::Simple _showAnimation;\n\n};\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/controls/history_view_voice_record_button.h\"\n\n#include \"lottie/lottie_icon.h\"\n#include \"ui/paint/blobs.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"lang/lang_keys.h\"\n\n#include <QtMath>\n\nnamespace HistoryView::Controls {\n\nnamespace {\n\nconstexpr auto kMaxLevel = 1800.;\nconstexpr auto kBlobAlpha = 76. / 255.;\nconstexpr auto kBlobMaxSpeed = 5.0;\nconstexpr auto kLevelDuration = 100. + 500. * 0.33;\nconstexpr auto kBlobsScaleEnterDuration = crl::time(250);\nconstexpr auto kVoiceIconIndex = 0;\nconstexpr auto kRoundIconIndex = 1;\n\nauto Blobs() {\n\treturn std::vector<Ui::Paint::Blobs::BlobData>{\n\t\t{\n\t\t\t.segmentsCount = 9,\n\t\t\t.minScale = 0.605229,\n\t\t\t.minRadius = (float)st::historyRecordMinorBlobMinRadius,\n\t\t\t.maxRadius = (float)st::historyRecordMinorBlobMaxRadius,\n\t\t\t.speedScale = 1.,\n\t\t\t.alpha = kBlobAlpha,\n\t\t\t.maxSpeed = kBlobMaxSpeed,\n\t\t},\n\t\t{\n\t\t\t.segmentsCount = 12,\n\t\t\t.minScale = 0.553943,\n\t\t\t.minRadius = (float)st::historyRecordMajorBlobMinRadius,\n\t\t\t.maxRadius = (float)st::historyRecordMajorBlobMaxRadius,\n\t\t\t.speedScale = 1.,\n\t\t\t.alpha = kBlobAlpha,\n\t\t\t.maxSpeed = kBlobMaxSpeed,\n\t\t},\n\t};\n}\n\n} // namespace\n\nVoiceRecordButton::VoiceRecordButton(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst style::RecordBar &st)\n: AbstractButton(parent)\n, _blobs(std::make_unique<Ui::Paint::Blobs>(\n\tBlobs(),\n\tkLevelDuration,\n\tkMaxLevel))\n, _center(_blobs->maxRadius()) {\n\tresize(_center * 2, _center * 2);\n\tinit();\n}\n\nVoiceRecordButton::~VoiceRecordButton() = default;\n\nvoid VoiceRecordButton::requestPaintLevel(quint16 level) {\n\tif (_blobsHideLastTime) {\n\t\t return;\n\t}\n\t_blobs->setLevel(level);\n\tupdate();\n}\n\nvoid VoiceRecordButton::init() {\n\tconst auto currentState = lifetime().make_state<Type>(_state.current());\n\n\trpl::single(\n\t\tanim::Disabled()\n\t) | rpl::then(\n\t\tanim::Disables()\n\t) | rpl::on_next([=](bool hide) {\n\t\tif (hide) {\n\t\t\t_blobs->setLevel(0.);\n\t\t}\n\t\t_blobsHideLastTime = hide ? crl::now() : 0;\n\t\tif (!hide && !_animation.animating() && isVisible()) {\n\t\t\t_animation.start();\n\t\t}\n\t}, lifetime());\n\n\tconst auto &mainRadiusMin = st::historyRecordMainBlobMinRadius;\n\tconst auto mainRadiusDiff = st::historyRecordMainBlobMaxRadius\n\t\t- mainRadiusMin;\n\tpaintRequest(\n\t) | rpl::on_next([=](const QRect &clip) {\n\t\tauto p = QPainter(this);\n\n\t\tconst auto hideProgress = _blobsHideLastTime\n\t\t\t? 1. - std::clamp(\n\t\t\t\t((crl::now() - _blobsHideLastTime)\n\t\t\t\t\t/ (float64)kBlobsScaleEnterDuration),\n\t\t\t\t0.,\n\t\t\t\t1.)\n\t\t\t: 1.;\n\t\tconst auto showProgress = _showProgress.current();\n\t\tconst auto complete = (showProgress == 1.);\n\n\t\tp.translate(_center, _center);\n\t\tPainterHighQualityEnabler hq(p);\n\t\tconst auto brush = QBrush(anim::color(\n\t\t\tst::historyRecordVoiceFgInactive,\n\t\t\tst::historyRecordVoiceFgActive,\n\t\t\t_colorProgress));\n\n\t\t_blobs->paint(p, brush, showProgress * hideProgress);\n\n\t\tconst auto radius = (mainRadiusMin\n\t\t\t+ (mainRadiusDiff * _blobs->currentLevel())) * showProgress;\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(brush);\n\t\tp.drawEllipse(QPointF(), radius, radius);\n\n\t\tif (!complete) {\n\t\t\tp.setOpacity(showProgress);\n\t\t}\n\n\t\t// Paint icon.\n\t\t{\n\t\t\tconst auto stateProgress = _stateChangedAnimation.value(0.);\n\t\t\tconst auto scale = (std::cos(M_PI * 2 * stateProgress) + 1.) * .5;\n\t\t\tif (scale < 1.) {\n\t\t\t\tp.scale(scale, scale);\n\t\t\t}\n\t\t\tconst auto state = *currentState;\n\t\t\tif (state == Type::Send) {\n\t\t\t\tconst auto icon = st::historySendIcon;\n\t\t\t\tconst auto position = st::historyRecordSendIconPosition;\n\t\t\t\ticon.paint(\n\t\t\t\t\tp,\n\t\t\t\t\t-icon.width() / 2 + position.x(),\n\t\t\t\t\t-icon.height() / 2 + position.y(),\n\t\t\t\t\t0,\n\t\t\t\t\tst::historyRecordVoiceFgActiveIcon->c);\n\t\t\t} else {\n\t\t\t\tconst auto index = (state == Type::Record)\n\t\t\t\t\t? kVoiceIconIndex\n\t\t\t\t\t: kRoundIconIndex;\n\t\t\t\tauto &icon = _voiceRoundIcons[index];\n\t\t\t\tif (!icon) {\n\t\t\t\t\tinitVoiceRoundIcon(index);\n\t\t\t\t}\n\t\t\t\ticon->paintInCenter(\n\t\t\t\t\tp,\n\t\t\t\t\trect().translated(-_center, -_center),\n\t\t\t\t\tst::historyRecordVoiceFgActiveIcon->c);\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\t_animation.init([=](crl::time now) {\n\t\tif (const auto &last = _blobsHideLastTime; (last > 0)\n\t\t\t&& (now - last >= kBlobsScaleEnterDuration)) {\n\t\t\t_animation.stop();\n\t\t\treturn false;\n\t\t}\n\t\t_blobs->updateLevel(now - _lastUpdateTime);\n\t\t_lastUpdateTime = now;\n\t\tupdate();\n\t\treturn true;\n\t});\n\n\trpl::merge(\n\t\tshownValue(),\n\t\t_showProgress.value(\n\t\t) | rpl::map(rpl::mappers::_1 != 0.) | rpl::distinct_until_changed()\n\t) | rpl::on_next([=](bool show) {\n\t\tsetVisible(show);\n\t\tsetMouseTracking(show);\n\t\tif (!show) {\n\t\t\t_animation.stop();\n\t\t\t_showProgress = 0.;\n\t\t\t_blobs->resetLevel();\n\t\t\t_state = Type::Record;\n\t\t} else {\n\t\t\tif (!_animation.animating()) {\n\t\t\t\t_animation.start();\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\tactives(\n\t) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](bool active) {\n\t\tsetPointerCursor(active);\n\t}, lifetime());\n\n\t_state.changes(\n\t) | rpl::on_next([=](Type newState) {\n\t\tconst auto to = 1.;\n\t\tauto callback = [=](float64 value) {\n\t\t\tif (value >= (to * .5)) {\n\t\t\t\t*currentState = newState;\n\t\t\t}\n\t\t\tupdate();\n\t\t};\n\t\tconstexpr auto kDuration = st::universalDuration * 2;\n\t\t_stateChangedAnimation.start(std::move(callback), 0., to, kDuration);\n\t}, lifetime());\n}\n\nvoid VoiceRecordButton::initVoiceRoundIcon(int index) {\n\tExpects(index >= 0 && index < 2);\n\n\t_voiceRoundIcons[index] = Lottie::MakeIcon({\n\t\t.path = ((index == kVoiceIconIndex)\n\t\t\t? u\":/animations/chat/voice_to_video.tgs\"_q\n\t\t\t: u\":/animations/chat/video_to_voice.tgs\"_q),\n\t\t.sizeOverride = st::historySend.recordSize,\n\t\t.colorizeUsingAlpha = true,\n\t});\n}\n\nrpl::producer<bool> VoiceRecordButton::actives() const {\n\treturn events(\n\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\treturn (e->type() == QEvent::MouseMove\n\t\t\t|| e->type() == QEvent::Leave\n\t\t\t|| e->type() == QEvent::Enter);\n\t}) | rpl::map([=](not_null<QEvent*> e) {\n\t\tswitch(e->type()) {\n\t\tcase QEvent::MouseMove:\n\t\t\treturn inCircle((static_cast<QMouseEvent*>(e.get()))->pos());\n\t\tcase QEvent::Leave: return false;\n\t\tcase QEvent::Enter: return inCircle(mapFromGlobal(QCursor::pos()));\n\t\tdefault: return false;\n\t\t}\n\t});\n}\n\nrpl::producer<> VoiceRecordButton::clicks() const {\n\treturn Ui::AbstractButton::clicks(\n\t) | rpl::to_empty | rpl::filter([=] {\n\t\treturn inCircle(mapFromGlobal(QCursor::pos()));\n\t});\n}\n\nbool VoiceRecordButton::inCircle(const QPoint &localPos) const {\n\tconst auto &radii = st::historyRecordMainBlobMaxRadius;\n\tconst auto dx = std::abs(localPos.x() - _center);\n\tif (dx > radii) {\n\t\treturn false;\n\t}\n\tconst auto dy = std::abs(localPos.y() - _center);\n\tif (dy > radii) {\n\t\treturn false;\n\t} else if (dx + dy <= radii) {\n\t\treturn true;\n\t}\n\treturn ((dx * dx + dy * dy) <= (radii * radii));\n}\n\nvoid VoiceRecordButton::requestPaintProgress(float64 progress) {\n\t_showProgress = progress;\n\tupdate();\n}\n\nvoid VoiceRecordButton::requestPaintColor(float64 progress) {\n\tif (_colorProgress == progress) {\n\t\treturn;\n\t}\n\t_colorProgress = progress;\n\tupdate();\n}\n\nvoid VoiceRecordButton::setType(Type state) {\n\t_state = state;\n\n\tsetAccessibleName([&] {\n\t\tswitch (state) {\n\t\tcase Type::Send:\n\t\t\treturn tr::lng_send_button(tr::now);\n\t\tcase Type::Record:\n\t\t\treturn tr::lng_send_action_record_round(tr::now);\n\t\tcase Type::Round:\n\t\t\treturn tr::lng_send_action_record_round(tr::now);\n\t\t}\n\t\tUnexpected(\"Voice record button type.\");\n\t}());\n\n}\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/abstract_button.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace style {\nstruct RecordBar;\n} // namespace style\n\nnamespace Lottie {\nclass Icon;\n} // namespace Lottie\n\nnamespace Ui::Paint {\nclass Blobs;\n} // namespace Ui::Paint\n\nnamespace HistoryView::Controls {\n\nclass VoiceRecordButton final : public Ui::AbstractButton {\npublic:\n\tVoiceRecordButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::RecordBar &st);\n\t~VoiceRecordButton();\n\n\tenum class Type {\n\t\tSend,\n\t\tRecord,\n\t\tRound,\n\t};\n\n\tvoid setType(Type state);\n\n\tvoid requestPaintColor(float64 progress);\n\tvoid requestPaintProgress(float64 progress);\n\tvoid requestPaintLevel(quint16 level);\n\n\t[[nodiscard]] rpl::producer<bool> actives() const;\n\t[[nodiscard]] rpl::producer<> clicks() const;\n\n\t[[nodiscard]] bool inCircle(const QPoint &localPos) const;\n\nprivate:\n\tvoid init();\n\tvoid initVoiceRoundIcon(int index);\n\n\tstd::unique_ptr<Ui::Paint::Blobs> _blobs;\n\tstd::array<std::unique_ptr<Lottie::Icon>, 2> _voiceRoundIcons;\n\n\tcrl::time _lastUpdateTime = 0;\n\tcrl::time _blobsHideLastTime = 0;\n\tconst int _center;\n\n\trpl::variable<float64> _showProgress = 0.;\n\tfloat64 _colorProgress = 0.;\n\trpl::variable<Type> _state = Type::Record;\n\n\t// This can animate for a very long time (like in music playing),\n\t// so it should be a Basic, not a Simple animation.\n\tUi::Animations::Basic _animation;\n\tUi::Animations::Simple _stateChangedAnimation;\n};\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/controls/history_view_webpage_processor.h\"\n\n#include \"base/unixtime.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_session.h\"\n#include \"data/data_web_page.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n\nnamespace HistoryView::Controls {\n\nWebPageText TitleAndDescriptionFromWebPage(not_null<WebPageData*> d) {\n\tQString resultTitle, resultDescription;\n\tconst auto document = d->document;\n\tconst auto author = d->author;\n\tconst auto siteName = d->siteName;\n\tconst auto title = d->title;\n\tconst auto description = d->description;\n\tconst auto filenameOrUrl = [&] {\n\t\treturn ((document && !document->filename().isEmpty())\n\t\t\t? document->filename()\n\t\t\t: d->url);\n\t};\n\tconst auto authorOrFilename = [&] {\n\t\treturn (author.isEmpty()\n\t\t\t? filenameOrUrl()\n\t\t\t: author);\n\t};\n\tconst auto descriptionOrAuthor = [&] {\n\t\treturn (description.text.isEmpty()\n\t\t\t? authorOrFilename()\n\t\t\t: description.text);\n\t};\n\tif (siteName.isEmpty()) {\n\t\tif (title.isEmpty()) {\n\t\t\tif (description.text.isEmpty()) {\n\t\t\t\tresultTitle = author;\n\t\t\t\tresultDescription = filenameOrUrl();\n\t\t\t} else {\n\t\t\t\tresultTitle = description.text;\n\t\t\t\tresultDescription = authorOrFilename();\n\t\t\t}\n\t\t} else {\n\t\t\tresultTitle = title;\n\t\t\tresultDescription = descriptionOrAuthor();\n\t\t}\n\t} else {\n\t\tresultTitle = siteName;\n\t\tresultDescription = title.isEmpty()\n\t\t\t? descriptionOrAuthor()\n\t\t\t: title;\n\t}\n\treturn { resultTitle, resultDescription };\n}\n\nbool DrawWebPageDataPreview(\n\t\tQPainter &p,\n\t\tnot_null<WebPageData*> webpage,\n\t\tnot_null<PeerData*> context,\n\t\tQRect to) {\n\tconst auto document = webpage->document;\n\tconst auto photo = webpage->photo;\n\tif ((!photo || photo->isNull())\n\t\t&& (!document\n\t\t\t|| !document->hasThumbnail()\n\t\t\t|| document->isPatternWallPaper())) {\n\t\treturn false;\n\t}\n\n\tconst auto preview = photo\n\t\t? photo->getReplyPreview(Data::FileOrigin(), context, false)\n\t\t: document->getReplyPreview(Data::FileOrigin(), context, false);\n\tif (preview) {\n\t\tconst auto w = preview->width();\n\t\tconst auto h = preview->height();\n\t\tif (w == h) {\n\t\t\tp.drawPixmap(to.x(), to.y(), preview->pix());\n\t\t} else {\n\t\t\tconst auto from = (w > h)\n\t\t\t\t? QRect((w - h) / 2, 0, h, h)\n\t\t\t\t: QRect(0, (h - w) / 2, w, w);\n\t\t\tp.drawPixmap(to, preview->pix(), from);\n\t\t}\n\t}\n\treturn true;\n}\n\n[[nodiscard]] bool ShowWebPagePreview(WebPageData *page) {\n\treturn page && !page->failed;\n}\n\nWebPageText ProcessWebPageData(WebPageData *page) {\n\tauto previewText = TitleAndDescriptionFromWebPage(page);\n\tif (previewText.title.isEmpty()) {\n\t\tif (page->document) {\n\t\t\tpreviewText.title = tr::lng_attach_file(tr::now);\n\t\t} else if (page->photo) {\n\t\t\tpreviewText.title = tr::lng_attach_photo(tr::now);\n\t\t}\n\t}\n\treturn previewText;\n}\n\nWebpageResolver::WebpageResolver(not_null<Main::Session*> session)\n: _session(session)\n, _api(&session->mtp()) {\n}\n\nstd::optional<WebPageData*> WebpageResolver::lookup(\n\t\tconst QString &link) const {\n\tconst auto i = _cache.find(link);\n\treturn (i == end(_cache))\n\t\t? std::optional<WebPageData*>()\n\t\t: (i->second && !i->second->failed)\n\t\t? i->second\n\t\t: nullptr;\n}\n\nQString WebpageResolver::find(not_null<WebPageData*> page) const {\n\tfor (const auto &[link, cached] : _cache) {\n\t\tif (cached == page) {\n\t\t\treturn link;\n\t\t}\n\t}\n\treturn QString();\n}\n\nvoid WebpageResolver::request(const QString &link, bool force) {\n\tif (_requestLink == link && !force) {\n\t\treturn;\n\t}\n\tconst auto done = [=](const MTPDmessageMediaWebPage &data) {\n\t\tconst auto page = _session->data().processWebpage(data.vwebpage());\n\t\tif (page->pendingTill > 0\n\t\t\t&& page->pendingTill < base::unixtime::now()) {\n\t\t\tpage->pendingTill = 0;\n\t\t\tpage->failed = true;\n\t\t}\n\t\t_cache.emplace(link, page->failed ? nullptr : page.get());\n\t\t_resolved.fire_copy(link);\n\t};\n\tconst auto fail = [=] {\n\t\t_cache.emplace(link, nullptr);\n\t\t_resolved.fire_copy(link);\n\t};\n\t_requestLink = link;\n\t_requestId = _api.request(\n\t\tMTPmessages_GetWebPagePreview(\n\t\t\tMTP_flags(0),\n\t\t\tMTP_string(link),\n\t\t\tMTPVector<MTPMessageEntity>()\n\t)).done([=](\n\t\t\tconst MTPmessages_WebPagePreview &result,\n\t\t\tmtpRequestId requestId) {\n\t\tif (_requestId == requestId) {\n\t\t\t_requestId = 0;\n\t\t}\n\t\tconst auto &data = result.data();\n\t\t_session->data().processUsers(data.vusers());\n\t\tdata.vmedia().match([=](const MTPDmessageMediaWebPage &data) {\n\t\t\tdone(data);\n\t\t}, [&](const auto &d) {\n\t\t\tfail();\n\t\t});\n\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\tif (_requestId == requestId) {\n\t\t\t_requestId = 0;\n\t\t}\n\t\tfail();\n\t}).send();\n}\n\nvoid WebpageResolver::cancel(const QString &link) {\n\tif (_requestLink == link) {\n\t\t_api.request(base::take(_requestId)).cancel();\n\t}\n}\n\nWebpageProcessor::WebpageProcessor(\n\tnot_null<History*> history,\n\tnot_null<Ui::InputField*> field)\n: _history(history)\n, _resolver(std::make_shared<WebpageResolver>(&history->session()))\n, _parser(field)\n, _timer([=] {\n\tif (!ShowWebPagePreview(_data) || _link.isEmpty()) {\n\t\treturn;\n\t}\n\t_resolver->request(_link, true);\n}) {\n\t_history->session().downloaderTaskFinished(\n\t) | rpl::filter([=] {\n\t\treturn _data && (_data->document || _data->photo);\n\t}) | rpl::on_next([=] {\n\t\t_repaintRequests.fire({});\n\t}, _lifetime);\n\n\t_history->owner().webPageUpdates(\n\t) | rpl::filter([=](not_null<WebPageData*> page) {\n\t\treturn (_data == page.get());\n\t}) | rpl::on_next([=] {\n\t\t_draft.id = _data->id;\n\t\t_draft.url = _data->url;\n\t\tupdateFromData();\n\t}, _lifetime);\n\n\t_parser.list().changes(\n\t) | rpl::on_next([=](QStringList &&parsed) {\n\t\t_parsedLinks = std::move(parsed);\n\t\tcheckPreview();\n\t}, _lifetime);\n\n\t_resolver->resolved() | rpl::on_next([=](QString link) {\n\t\tif (_link != link\n\t\t\t|| _draft.removed\n\t\t\t|| (_draft.manual && _draft.url != link)) {\n\t\t\treturn;\n\t\t}\n\t\t_data = _resolver->lookup(link).value_or(nullptr);\n\t\tif (_data) {\n\t\t\t_draft.id = _data->id;\n\t\t\t_draft.url = _data->url;\n\t\t\tupdateFromData();\n\t\t} else {\n\t\t\t_links = QStringList();\n\t\t\tcheckPreview();\n\t\t}\n\t}, _lifetime);\n}\n\nrpl::producer<> WebpageProcessor::repaintRequests() const {\n\treturn _repaintRequests.events();\n}\n\nData::WebPageDraft WebpageProcessor::draft() const {\n\treturn _draft;\n}\n\nstd::shared_ptr<WebpageResolver> WebpageProcessor::resolver() const {\n\treturn _resolver;\n}\n\nconst std::vector<MessageLinkRange> &WebpageProcessor::links() const {\n\treturn _parser.ranges();\n}\n\nQString WebpageProcessor::link() const {\n\treturn _link;\n}\n\nvoid WebpageProcessor::apply(Data::WebPageDraft draft, bool reparse) {\n\tconst auto was = _link;\n\tif (draft.removed) {\n\t\t_draft = draft;\n\t\t_parsedLinks = _parser.list().current();\n\t\tif (_parsedLinks.empty()) {\n\t\t\t_draft.removed = false;\n\t\t}\n\t\t_data = nullptr;\n\t\t_links = QStringList();\n\t\t_link = QString();\n\t\t_parsed = WebpageParsed();\n\t\tupdateFromData();\n\t} else if (draft.manual && !draft.url.isEmpty()) {\n\t\t_draft = draft;\n\t\t_parsedLinks = QStringList();\n\t\t_links = QStringList();\n\t\t_link = _draft.url;\n\t\tconst auto page = draft.id\n\t\t\t? _history->owner().webpage(draft.id).get()\n\t\t\t: nullptr;\n\t\tif (page && page->url == draft.url) {\n\t\t\t_data = page;\n\t\t\tif (const auto link = _resolver->find(page); !link.isEmpty()) {\n\t\t\t\t_link = link;\n\t\t\t}\n\t\t\tupdateFromData();\n\t\t} else {\n\t\t\t_resolver->request(_link);\n\t\t\treturn;\n\t\t}\n\t} else if (!draft.manual && !_draft.manual) {\n\t\t_draft = draft;\n\t\tcheckNow(reparse);\n\t}\n\tif (_link != was) {\n\t\t_resolver->cancel(was);\n\t}\n}\n\nvoid WebpageProcessor::updateFromData() {\n\t_timer.cancel();\n\tauto parsed = WebpageParsed();\n\tif (ShowWebPagePreview(_data)) {\n\t\tif (const auto till = _data->pendingTill) {\n\t\t\tparsed.drawPreview = [](QPainter &p, QRect to) {\n\t\t\t\treturn false;\n\t\t\t};\n\t\t\tparsed.title = tr::lng_preview_loading(tr::now);\n\t\t\tparsed.description = _link;\n\n\t\t\tconst auto timeout = till - base::unixtime::now();\n\t\t\t_timer.callOnce(\n\t\t\t\tstd::max(timeout, 0) * crl::time(1000));\n\t\t} else {\n\t\t\tconst auto webpage = _data;\n\t\t\tconst auto context = _history->peer;\n\t\t\tconst auto preview = ProcessWebPageData(_data);\n\t\t\tparsed.title = preview.title;\n\t\t\tparsed.description = preview.description;\n\t\t\tparsed.drawPreview = [=](QPainter &p, QRect to) {\n\t\t\t\treturn DrawWebPageDataPreview(p, webpage, context, to);\n\t\t\t};\n\t\t}\n\t}\n\t_parsed = std::move(parsed);\n\t_repaintRequests.fire({});\n}\n\nvoid WebpageProcessor::setDisabled(bool disabled) {\n\t_parser.setDisabled(disabled);\n\tif (disabled) {\n\t\tapply({ .removed = true });\n\t} else {\n\t\tcheckNow(false);\n\t}\n}\n\nvoid WebpageProcessor::checkNow(bool force) {\n\t_parser.parseNow();\n\tif (force) {\n\t\t_link = QString();\n\t\t_links = QStringList();\n\t\tif (_parsedLinks.isEmpty()) {\n\t\t\t_data = nullptr;\n\t\t\tupdateFromData();\n\t\t\treturn;\n\t\t}\n\t}\n\tcheckPreview();\n}\n\nvoid WebpageProcessor::checkPreview() {\n\tconst auto previewRestricted = _history->peer\n\t\t&& _history->peer->amRestricted(ChatRestriction::EmbedLinks);\n\tif (_parsedLinks.empty()) {\n\t\t_draft.removed = false;\n\t}\n\tif (_draft.removed) {\n\t\treturn;\n\t} else if (previewRestricted) {\n\t\tapply({ .removed = true });\n\t\t_draft.removed = false;\n\t\treturn;\n\t} else if (_draft.manual) {\n\t\treturn;\n\t} else if (_links == _parsedLinks) {\n\t\treturn;\n\t}\n\t_links = _parsedLinks;\n\n\tauto page = (WebPageData*)nullptr;\n\tauto chosen = QString();\n\tfor (const auto &link : _links) {\n\t\tconst auto value = _resolver->lookup(link);\n\t\tif (!value) {\n\t\t\tchosen = link;\n\t\t\tbreak;\n\t\t} else if (*value) {\n\t\t\tchosen = link;\n\t\t\tpage = *value;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (_link != chosen) {\n\t\t_resolver->cancel(_link);\n\t\t_link = chosen;\n\t\tif (!page && !_link.isEmpty()) {\n\t\t\t_resolver->request(_link);\n\t\t}\n\t}\n\tif (page) {\n\t\t_data = page;\n\t\t_draft.id = _data->id;\n\t\t_draft.url = _data->url;\n\t} else {\n\t\t_data = nullptr;\n\t\t_draft = {};\n\t}\n\tupdateFromData();\n}\n\nrpl::producer<WebpageParsed> WebpageProcessor::parsedValue() const {\n\treturn _parsed.value();\n}\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_drafts.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"mtproto/sender.h\"\n\nclass History;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass InputField;\n} // namespace Ui\n\nnamespace HistoryView::Controls {\n\nstruct WebPageText {\n\tQString title;\n\tQString description;\n};\n\n[[nodiscard]] WebPageText TitleAndDescriptionFromWebPage(\n\tnot_null<WebPageData*> data);\n\nbool DrawWebPageDataPreview(\n\tQPainter &p,\n\tnot_null<WebPageData*> webpage,\n\tnot_null<PeerData*> context,\n\tQRect to);\n\n[[nodiscard]] bool ShowWebPagePreview(WebPageData *page);\n[[nodiscard]] WebPageText ProcessWebPageData(WebPageData *page);\n\nstruct WebpageParsed {\n\tFn<bool(QPainter &p, QRect to)> drawPreview;\n\tQString title;\n\tQString description;\n\n\texplicit operator bool() const {\n\t\treturn drawPreview != nullptr;\n\t}\n};\n\nclass WebpageResolver final {\npublic:\n\texplicit WebpageResolver(not_null<Main::Session*> session);\n\n\t[[nodiscard]] std::optional<WebPageData*> lookup(\n\t\t\tconst QString &link) const;\n\t[[nodiscard]] rpl::producer<QString> resolved() const {\n\t\treturn _resolved.events();\n\t}\n\n\t[[nodiscard]] QString find(not_null<WebPageData*> page) const;\n\n\tvoid request(const QString &link, bool force = false);\n\tvoid cancel(const QString &link);\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\tbase::flat_map<QString, WebPageData*> _cache;\n\trpl::event_stream<QString> _resolved;\n\n\tQString _requestLink;\n\tmtpRequestId _requestId = 0;\n\n};\n\nclass WebpageProcessor final {\npublic:\n\tWebpageProcessor(\n\t\tnot_null<History*> history,\n\t\tnot_null<Ui::InputField*> field);\n\n\tvoid setDisabled(bool disabled);\n\tvoid checkNow(bool force);\n\n\t// If editing a message without a preview we don't want to show\n\t// parsed preview until links set is changed in the message.\n\t//\n\t// If writing a new message we want to parse links immediately,\n\t// unless preview was removed in the draft or manual.\n\tvoid apply(Data::WebPageDraft draft, bool reparse = true);\n\t[[nodiscard]] Data::WebPageDraft draft() const;\n\t[[nodiscard]] std::shared_ptr<WebpageResolver> resolver() const;\n\t[[nodiscard]] const std::vector<MessageLinkRange> &links() const;\n\t[[nodiscard]] QString link() const;\n\n\t[[nodiscard]] rpl::producer<> repaintRequests() const;\n\t[[nodiscard]] rpl::producer<WebpageParsed> parsedValue() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tvoid updateFromData();\n\tvoid checkPreview();\n\n\tconst not_null<History*> _history;\n\tconst std::shared_ptr<WebpageResolver> _resolver;\n\tMessageLinksParser _parser;\n\n\tQStringList _parsedLinks;\n\tQStringList _links;\n\tQString _link;\n\tWebPageData *_data = nullptr;\n\tData::WebPageDraft _draft;\n\n\trpl::event_stream<> _repaintRequests;\n\trpl::variable<WebpageParsed> _parsed;\n\n\tbase::Timer _timer;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace HistoryView::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_about_view.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_about_view.h\"\n\n#include \"api/api_peer_colors.h\"\n#include \"api/api_premium.h\"\n#include \"api/api_sending.h\"\n#include \"apiwrap.h\"\n#include \"base/random.h\"\n#include \"base/unixtime.h\"\n#include \"ui/effects/premium_stars.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\"\n#include \"countries/countries_instance.h\"\n#include \"data/business/data_business_common.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/view/history_view_group_call_bar.h\"\n#include \"history/view/media/history_view_media_generic.h\"\n#include \"history/view/media/history_view_service_box.h\"\n#include \"history/view/media/history_view_sticker_player_abstract.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"history/view/media/history_view_unique_gift.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history_item_reply_markup.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/business/settings_chat_intro.h\"\n#include \"settings/sections/settings_credits.h\" // BuyStarsHandler\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/image/image_location_factory.h\"\n#include \"ui/text/custom_emoji_instance.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/painter.h\"\n#include \"ui/top_background_gradient.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\" // GroupCallUserpics\n#include \"styles/style_credits.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kLabelOpacity = 0.85;\nconstexpr auto kMaxCommonChatsUserpics = 3;\n\nclass EmptyChatLockedBox final\n\t: public ServiceBoxContent\n\t, public base::has_weak_ptr {\npublic:\n\tenum class Type {\n\t\tPremiumRequired,\n\t\tStarsCharged,\n\t\tFreeDirect,\n\t};\n\n\tEmptyChatLockedBox(not_null<Element*> parent, Type type);\n\t~EmptyChatLockedBox();\n\n\tint width() override;\n\tint top() override;\n\tQSize size() override;\n\tTextWithEntities title() override;\n\tTextWithEntities subtitle() override;\n\tint buttonSkip() override;\n\trpl::producer<QString> button() override;\n\tstd::optional<Ui::Premium::MiniStarsType> buttonMinistars() override;\n\tvoid draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry) override;\n\tClickHandlerPtr createViewLink() override;\n\n\tbool hideServiceText() override {\n\t\treturn true;\n\t}\n\n\tvoid stickerClearLoopPlayed() override;\n\tstd::unique_ptr<StickerPlayer> stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) override;\n\n\tbool hasHeavyPart() override;\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\tconst not_null<Element*> _parent;\n\tSettings::BuyStarsHandler _buyStars;\n\trpl::variable<bool> _buyStarsLoading;\n\tType _type = {};\n\n};\n\nclass UserpicsList final : public Ui::DynamicImage {\npublic:\n\tUserpicsList(\n\t\tstd::vector<not_null<PeerData*>> peers,\n\t\tconst style::GroupCallUserpics &st,\n\t\tint countOverride = 0);\n\n\t[[nodiscard]] int width() const;\n\n\tstd::shared_ptr<Ui::DynamicImage> clone() override;\n\n\tQImage image(int size) override;\n\tvoid subscribeToUpdates(Fn<void()> callback) override;\n\nprivate:\n\tstruct Subscribed {\n\t\texplicit Subscribed(Fn<void()> callback)\n\t\t: callback(std::move(callback)) {\n\t\t}\n\n\t\tstd::vector<HistoryView::UserpicInRow> list;\n\t\tbool someNotLoaded = false;\n\t\tFn<void()> callback;\n\t\tint paletteVersion = 0;\n\t};\n\n\tconst std::vector<not_null<PeerData*>> _peers;\n\tconst style::GroupCallUserpics &_st;\n\tconst int _countOverride = 0;\n\n\tQImage _frame;\n\tstd::unique_ptr<Subscribed> _subscribed;\n\n};\n\nclass NewBotThreadDottedLine final : public MediaGenericPart {\npublic:\n\texplicit NewBotThreadDottedLine(not_null<Element*> parent);\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const override;\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\nprivate:\n\tconst not_null<Element*> _parent;\n\n};\n\nUserpicsList::UserpicsList(\n\tstd::vector<not_null<PeerData*>> peers,\n\tconst style::GroupCallUserpics &st,\n\tint countOverride)\n: _peers(std::move(peers))\n, _st(st)\n, _countOverride(countOverride) {\n}\n\nstd::shared_ptr<Ui::DynamicImage> UserpicsList::clone() {\n\treturn std::make_shared<UserpicsList>(_peers, _st);\n}\n\nQImage UserpicsList::image(int size) {\n\tExpects(_subscribed != nullptr);\n\n\tconst auto regenerate = [&] {\n\t\tconst auto version = style::PaletteVersion();\n\t\tif (_subscribed->paletteVersion != version) {\n\t\t\t_subscribed->paletteVersion = version;\n\t\t\treturn true;\n\t\t}\n\t\treturn NeedRegenerateUserpics(_frame, _subscribed->list);\n\t}();\n\tif (regenerate) {\n\t\tconst auto max = std::max(_countOverride, int(_peers.size()));\n\t\tGenerateUserpicsInRow(_frame, _subscribed->list, _st, max);\n\t}\n\treturn _frame;\n}\n\nvoid UserpicsList::subscribeToUpdates(Fn<void()> callback) {\n\tif (!callback) {\n\t\t_subscribed = nullptr;\n\t\treturn;\n\t}\n\t_subscribed = std::make_unique<Subscribed>(std::move(callback));\n\tfor (const auto &peer : _peers) {\n\t\t_subscribed->list.push_back({ .peer = peer });\n\t}\n}\n\nint UserpicsList::width() const {\n\tconst auto count = std::max(_countOverride, int(_peers.size()));\n\tif (!count) {\n\t\treturn 0;\n\t}\n\tconst auto shifted = count - 1;\n\treturn _st.size + (shifted * (_st.size - _st.shift));\n}\n\n\n\nNewBotThreadDottedLine::NewBotThreadDottedLine(not_null<Element*> parent)\n: _parent(parent) {\n}\n\nvoid NewBotThreadDottedLine::draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const {\n\tconst auto skip = st::monoforumBarUserpicSkip;\n\tauto pen = context.st->msgServiceBg()->p;\n\tpen.setWidthF(skip);\n\tpen.setCapStyle(Qt::RoundCap);\n\tpen.setDashPattern({ 2., 2. });\n\tp.setPen(pen);\n\tconst auto top = -st::newBotThreadTopSkip / 2;\n\tp.drawLine(context.viewport.x(), top, context.viewport.width(), top);\n}\n\nQSize NewBotThreadDottedLine::countOptimalSize() {\n\treturn { 0, 0 };\n}\n\nQSize NewBotThreadDottedLine::countCurrentSize(int newWidth) {\n\treturn { 0, 0 };\n}\n\nauto GenerateChatIntro(\n\tnot_null<Element*> parent,\n\tElement *replacing,\n\tconst Data::ChatIntro &data,\n\tFn<void(not_null<DocumentData*>)> helloChosen,\n\tFn<void(not_null<DocumentData*>)> sendIntroSticker)\n-> Fn<void(\n\t\tnot_null<MediaGeneric*>,\n\t\tFn<void(std::unique_ptr<MediaGenericPart>)>)> {\n\treturn [=](\n\t\t\tnot_null<MediaGeneric*> media,\n\t\t\tFn<void(std::unique_ptr<MediaGenericPart>)> push) {\n\t\tauto pushText = [&](\n\t\t\t\tTextWithEntities text,\n\t\t\t\tQMargins margins = {},\n\t\t\t\tconst base::flat_map<uint16, ClickHandlerPtr> &links = {}) {\n\t\t\tif (text.empty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpush(std::make_unique<MediaGenericTextPart>(\n\t\t\t\tstd::move(text),\n\t\t\t\tmargins,\n\t\t\t\tst::defaultTextStyle,\n\t\t\t\tlinks));\n\t\t};\n\t\tconst auto title = data.customPhrases()\n\t\t\t? data.title\n\t\t\t: tr::lng_chat_intro_default_title(tr::now);\n\t\tconst auto description = data.customPhrases()\n\t\t\t? data.description\n\t\t\t: tr::lng_chat_intro_default_message(tr::now);\n\t\tpushText(tr::bold(title), st::chatIntroTitleMargin);\n\t\tpushText({ description }, title.isEmpty()\n\t\t\t? st::chatIntroTitleMargin\n\t\t\t: st::chatIntroMargin);\n\t\tconst auto sticker = [=] {\n\t\t\tusing Tag = ChatHelpers::StickerLottieSize;\n\t\t\tauto sticker = data.sticker;\n\t\t\tif (!sticker) {\n\t\t\t\tconst auto api = &parent->history()->session().api();\n\t\t\t\tconst auto &list = api->premium().helloStickers();\n\t\t\t\tif (!list.empty()) {\n\t\t\t\t\tsticker = list[base::RandomIndex(list.size())];\n\t\t\t\t\tif (helloChosen) {\n\t\t\t\t\t\thelloChosen(sticker);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto send = [=] {\n\t\t\t\tsendIntroSticker(sticker);\n\t\t\t};\n\t\t\treturn StickerInBubblePart::Data{\n\t\t\t\t.sticker = sticker,\n\t\t\t\t.size = st::chatIntroStickerSize,\n\t\t\t\t.cacheTag = Tag::ChatIntroHelloSticker,\n\t\t\t\t.link = std::make_shared<LambdaClickHandler>(send),\n\t\t\t};\n\t\t};\n\t\tpush(std::make_unique<StickerInBubblePart>(\n\t\t\tparent,\n\t\t\treplacing,\n\t\t\tsticker,\n\t\t\tst::chatIntroStickerPadding));\n\t};\n}\n\nauto GenerateNewBotThread(\n\tnot_null<Element*> parent,\n\tElement *replacing)\n-> Fn<void(\n\t\tnot_null<MediaGeneric*>,\n\t\tFn<void(std::unique_ptr<MediaGenericPart>)>)> {\n\treturn [=](\n\t\t\tnot_null<MediaGeneric*> media,\n\t\t\tFn<void(std::unique_ptr<MediaGenericPart>)> push) {\n\t\tauto pushText = [&](\n\t\t\t\tTextWithEntities text,\n\t\t\t\tQMargins margins = {},\n\t\t\t\tconst base::flat_map<uint16, ClickHandlerPtr> &links = {}) {\n\t\t\tif (text.empty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpush(std::make_unique<MediaGenericTextPart>(\n\t\t\t\tstd::move(text),\n\t\t\t\tmargins,\n\t\t\t\tst::defaultTextStyle,\n\t\t\t\tlinks));\n\t\t};\n\t\tconst auto title = tr::lng_bot_new_thread_title(tr::now);\n\t\tconst auto description = tr::lng_bot_new_thread_about(tr::now);\n\t\tpush(std::make_unique<NewBotThreadDottedLine>(parent));\n\t\tpush(std::make_unique<LambdaGenericPart>(\n\t\t\tQSize(\n\t\t\t\tst::newThreadAboutIconOuter,\n\t\t\t\tst::newThreadAboutIconOuter + st::newThreadAboutIconSkip),\n\t\t\t[=](\n\t\t\t\tPainter &p,\n\t\t\t\tnot_null<const MediaGeneric*> owner,\n\t\t\t\tconst PaintContext &context,\n\t\t\t\tint outerWidth) {\n\t\t\t\t\tconst auto size = st::newThreadAboutIconOuter;\n\t\t\t\t\tconst auto &icon = st::newThreadAboutIcon;\n\t\t\t\t\tconst auto x = (outerWidth - icon.width()) / 2;\n\t\t\t\t\tconst auto y = (size - icon.height()) / 2\n\t\t\t\t\t\t+ st::newThreadAboutIconSkip;\n\t\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\t\tp.setBrush(context.st->msgServiceBgSelected());\n\t\t\t\t\tp.drawEllipse(\n\t\t\t\t\t\t(outerWidth - size) / 2,\n\t\t\t\t\t\tst::newThreadAboutIconSkip,\n\t\t\t\t\t\tsize,\n\t\t\t\t\t\tsize);\n\t\t\t\t\tconst auto color = context.st->msgServiceFg();\n\t\t\t\t\ticon.paint(p, x, y, outerWidth, color->c);\n\t\t\t\t}));\n\t\tpushText(tr::bold(title), st::chatIntroTitleMargin);\n\t\tpushText({ description }, st::chatIntroMargin);\n\t\tpush(std::make_unique<LambdaGenericPart>(\n\t\t\tst::newBotThreadDown.size() / 4 * 3,\n\t\t\t[=, h = st::newBotThreadDown.height() / 2 + st::lineWidth * 4](\n\t\t\t\tPainter &p,\n\t\t\t\tnot_null<const MediaGeneric*> owner,\n\t\t\t\tconst PaintContext &context,\n\t\t\t\tint outerWidth) {\n\t\t\t\t\tauto color = context.st->msgServiceFg()->c;\n\t\t\t\t\tcolor.setAlphaF(color.alphaF() * kLabelOpacity);\n\t\t\t\t\tst::newBotThreadDown.paintInCenter(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tQRect(0, 0, outerWidth, h),\n\t\t\t\t\t\tcolor);\n\t\t\t}));\n\n\t\tparent->addVerticalMargins(\n\t\t\tst::newBotThreadTopSkip - st::msgServiceMargin.top(),\n\t\t\tst::msgServiceMargin.top());\n\t};\n}\n\nauto GenerateNewPeerInfo(\n\tnot_null<Element*> parent,\n\tElement *replacing,\n\tnot_null<UserData*> user,\n\tstd::vector<not_null<PeerData*>> commonGroups)\n-> Fn<void(\n\t\tnot_null<MediaGeneric*>,\n\t\tFn<void(std::unique_ptr<MediaGenericPart>)>)> {\n\treturn [=](\n\t\t\tnot_null<MediaGeneric*> media,\n\t\t\tFn<void(std::unique_ptr<MediaGenericPart>)> push) {\n\t\tconst auto normalFg = [](const PaintContext &context) {\n\t\t\treturn context.st->msgServiceFg()->c;\n\t\t};\n\t\tconst auto fadedFg = [](const PaintContext &context) {\n\t\t\tauto result = context.st->msgServiceFg()->c;\n\t\t\tresult.setAlphaF(result.alphaF() * kLabelOpacity);\n\t\t\treturn result;\n\t\t};\n\t\tpush(std::make_unique<MediaGenericTextPart>(\n\t\t\ttr::bold(user->name()),\n\t\t\tst::newPeerTitleMargin));\n\t\tpush(std::make_unique<TextPartColored>(\n\t\t\ttr::lng_new_contact_not_contact(tr::now, tr::marked),\n\t\t\tst::newPeerSubtitleMargin,\n\t\t\tfadedFg));\n\n\t\tauto entries = std::vector<AttributeTable::Entry>();\n\t\tconst auto country = user->phoneCountryCode();\n\t\tif (!country.isEmpty()) {\n\t\t\tconst auto &countries = Countries::Instance();\n\t\t\tconst auto name = countries.countryNameByISO2(country);\n\t\t\tconst auto flag = countries.flagEmojiByISO2(country);\n\t\t\tentries.push_back({\n\t\t\t\ttr::lng_new_contact_phone_number(tr::now),\n\t\t\t\ttr::bold(flag + QChar(0xA0) + name),\n\t\t\t});\n\t\t}\n\t\tconst auto month = user->registrationMonth();\n\t\tconst auto year = user->registrationYear();\n\t\tif (month && year) {\n\t\t\tentries.push_back({\n\t\t\t\ttr::lng_new_contact_registration(tr::now),\n\t\t\t\ttr::bold(langMonthOfYearFull(month, year)),\n\t\t\t});\n\t\t}\n\n\t\tconst auto context = Core::TextContext({\n\t\t\t.session = &parent->history()->session(),\n\t\t\t.repaint = [parent] { parent->repaint(); },\n\t\t});\n\t\tconst auto kUserpicsPrefix = u\"userpics-list/\"_q;\n\t\tif (const auto count = user->commonChatsCount()) {\n\t\t\tconst auto url = u\"internal:common_groups/\"_q\n\t\t\t\t+ QString::number(user->id.value);\n\t\t\tauto ids = QStringList();\n\t\t\tconst auto userpics = std::min(count, kMaxCommonChatsUserpics);\n\t\t\tfor (auto i = 0; i != userpics; ++i) {\n\t\t\t\tids.push_back(QString::number(i < commonGroups.size()\n\t\t\t\t\t? commonGroups[i]->id.value\n\t\t\t\t\t: 0));\n\t\t\t}\n\t\t\tauto userpicsData = kUserpicsPrefix + ids.join(',');\n\t\t\tentries.push_back({\n\t\t\t\ttr::lng_new_contact_common_groups(tr::now),\n\t\t\t\tUi::Text::Wrapped(\n\t\t\t\t\ttr::lng_new_contact_groups(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tcount,\n\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\tUi::Text::SingleCustomEmoji(userpicsData),\n\t\t\t\t\t\tlt_arrow,\n\t\t\t\t\t\tUi::Text::IconEmoji(&st::textMoreIconEmoji),\n\t\t\t\t\t\ttr::bold),\n\t\t\t\t\tEntityType::CustomUrl,\n\t\t\t\t\turl),\n\t\t\t});\n\t\t}\n\n\t\tauto copy = context;\n\t\tcopy.customEmojiFactory = [=, old = copy.customEmojiFactory](\n\t\t\tQStringView data,\n\t\t\tconst Ui::Text::MarkedContext &context\n\t\t) -> std::unique_ptr<Ui::Text::CustomEmoji> {\n\t\t\tif (!data.startsWith(kUserpicsPrefix)) {\n\t\t\t\treturn old(data, context);\n\t\t\t}\n\t\t\tconst auto ids = data.mid(kUserpicsPrefix.size()).split(',');\n\t\t\tauto peers = std::vector<not_null<PeerData*>>();\n\t\t\tfor (const auto &id : ids) {\n\t\t\t\tif (const auto peerId = PeerId(id.toULongLong())) {\n\t\t\t\t\tpeers.push_back(user->owner().peer(peerId));\n\t\t\t\t}\n\t\t\t}\n\t\t\tauto image = std::make_shared<UserpicsList>(\n\t\t\t\tstd::move(peers),\n\t\t\t\tst::newPeerUserpics,\n\t\t\t\tids.size());\n\t\t\tconst auto size = image->width();\n\t\t\treturn std::make_unique<Ui::CustomEmoji::DynamicImageEmoji>(\n\t\t\t\tdata.toString(),\n\t\t\t\tstd::move(image),\n\t\t\t\tcontext.repaint,\n\t\t\t\tst::newPeerUserpicsPadding,\n\t\t\t\tsize);\n\t\t};\n\t\tpush(std::make_unique<AttributeTable>(\n\t\t\tstd::move(entries),\n\t\t\tst::newPeerSubtitleMargin,\n\t\t\tfadedFg,\n\t\t\tnormalFg,\n\t\t\tcopy));\n\n\t\tconst auto details = user->botVerifyDetails();\n\t\tconst auto text = details\n\t\t\t? Data::SingleCustomEmoji(\n\t\t\t\tdetails->iconId\n\t\t\t).append(' ').append(details->description)\n\t\t\t: Ui::Text::IconEmoji(\n\t\t\t\t&st::newPeerNonOfficial\n\t\t\t).append(' ').append(tr::lng_new_contact_not_official(tr::now));\n\t\tpush(std::make_unique<TextPartColored>(\n\t\t\ttext,\n\t\t\tst::newPeerSubtitleMargin,\n\t\t\tfadedFg,\n\t\t\tst::defaultTextStyle,\n\t\t\tbase::flat_map<uint16, ClickHandlerPtr>(),\n\t\t\tcontext));\n\t};\n}\n\nEmptyChatLockedBox::EmptyChatLockedBox(not_null<Element*> parent, Type type)\n: _parent(parent)\n, _type(type) {\n}\n\nEmptyChatLockedBox::~EmptyChatLockedBox() = default;\n\nint EmptyChatLockedBox::width() {\n\treturn (_type == Type::PremiumRequired)\n\t\t? st::premiumRequiredWidth\n\t\t: st::starsPerMessageWidth;\n}\n\nint EmptyChatLockedBox::top() {\n\treturn st::msgServiceGiftBoxButtonMargins.top();\n}\n\nQSize EmptyChatLockedBox::size() {\n\treturn { st::msgServicePhotoWidth, st::msgServicePhotoWidth };\n}\n\nTextWithEntities EmptyChatLockedBox::title() {\n\treturn {};\n}\n\nint EmptyChatLockedBox::buttonSkip() {\n\treturn st::storyMentionButtonSkip;\n}\n\nrpl::producer<QString> EmptyChatLockedBox::button() {\n\treturn (_type == Type::FreeDirect)\n\t\t? nullptr\n\t\t: (_type == Type::PremiumRequired)\n\t\t? tr::lng_send_non_premium_go()\n\t\t: tr::lng_send_charges_stars_go();\n}\n\nauto EmptyChatLockedBox::buttonMinistars()\n-> std::optional<Ui::Premium::MiniStarsType> {\n\treturn Ui::Premium::MiniStarsType::SlowStars;\n}\n\nTextWithEntities EmptyChatLockedBox::subtitle() {\n\treturn _parent->data()->notificationText();\n}\n\nClickHandlerPtr EmptyChatLockedBox::createViewLink() {\n\t_buyStarsLoading = _buyStars.loadingValue();\n\tconst auto handler = [=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tif (_type == Type::PremiumRequired) {\n\t\t\t\tSettings::ShowPremium(controller, u\"require_premium\"_q);\n\t\t\t} else if (!_buyStarsLoading.current()) {\n\t\t\t\t_buyStars.handler(controller->uiShow())();\n\t\t\t}\n\t\t}\n\t};\n\treturn std::make_shared<LambdaClickHandler>(crl::guard(this, handler));\n}\n\nvoid EmptyChatLockedBox::draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry) {\n\tp.setBrush(context.st->msgServiceBg()); // ?\n\tp.setPen(Qt::NoPen);\n\tp.drawEllipse(geometry);\n\t(_type == Type::PremiumRequired\n\t\t? st::premiumRequiredIcon\n\t\t: st::directMessagesIcon).paintInCenter(p, geometry);\n}\n\nvoid EmptyChatLockedBox::stickerClearLoopPlayed() {\n}\n\nstd::unique_ptr<StickerPlayer> EmptyChatLockedBox::stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) {\n\treturn nullptr;\n}\n\nbool EmptyChatLockedBox::hasHeavyPart() {\n\treturn false;\n}\n\nvoid EmptyChatLockedBox::unloadHeavyPart() {\n}\n\nQImage GenerateManagedBotImage(not_null<UserData*> user) {\n\tauto centerColor = QColor();\n\tauto edgeColor = QColor();\n\tif (const auto collectible = user->emojiStatusId().collectible) {\n\t\tcenterColor = collectible->centerColor;\n\t\tedgeColor = collectible->edgeColor;\n\t} else if (const auto color\n\t\t= user->session().api().peerColors().colorProfileFor(user)) {\n\t\tif (color->bg.size() > 1) {\n\t\t\tcenterColor = color->bg[1];\n\t\t\tedgeColor = color->bg[0];\n\t\t}\n\t}\n\tif (!centerColor.isValid()) {\n\t\tconst auto colorIndex = Ui::EmptyUserpic::ColorIndex(\n\t\t\tuser->id.value);\n\t\tconst auto colors = Ui::EmptyUserpic::UserpicColor(colorIndex);\n\t\tcenterColor = colors.color1->c;\n\t\tedgeColor = colors.color2->c;\n\t}\n\n\tconst auto size = QSize(\n\t\tst::managedBotImageWidth,\n\t\tst::managedBotImageHeight);\n\tauto image = Ui::CreateTopBgGradient(\n\t\tsize,\n\t\tcenterColor,\n\t\tedgeColor,\n\t\tfalse);\n\tif (image.isNull()) {\n\t\treturn image;\n\t}\n\n\tauto p = QPainter(&image);\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tauto iconColor = edgeColor.toHsv();\n\ticonColor.setHsv(\n\t\ticonColor.hsvHue(),\n\t\ticonColor.hsvSaturation(),\n\t\tstd::max(iconColor.value() - 64, 0));\n\ticonColor = iconColor.toRgb();\n\tconst auto width = size.width();\n\tconst auto height = size.height();\n\tconst auto &icon = st::menuIconBot;\n\tconst auto &points = Ui::PatternBgPoints();\n\tfor (const auto &point : points) {\n\t\tconst auto cx = point.position.x() * width;\n\t\tconst auto cy = point.position.y() * height;\n\t\tp.save();\n\t\tp.setOpacity(point.opacity);\n\t\tif (point.scale < 1.) {\n\t\t\tp.translate(cx, cy);\n\t\t\tp.scale(point.scale, point.scale);\n\t\t\tp.translate(-cx, -cy);\n\t\t}\n\t\tconst auto x = int(cx) - icon.width() / 2;\n\t\tconst auto y = int(cy) - icon.height() / 2;\n\t\ticon.paint(p, x, y, width, iconColor);\n\t\tp.restore();\n\t}\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto iheight = st::managedBotCodeIcon.height();\n\tconst auto scale = (size.height() * ratio * 100) / iheight;\n\tauto iconImage = st::managedBotCodeIcon.instance(Qt::white, scale, true);\n\ticonImage.setDevicePixelRatio(ratio);\n\tconst auto iw = iconImage.width() / ratio;\n\tconst auto ih = iconImage.height() / ratio;\n\tp.drawImage(\n\t\tQRect((width - iw) / 2, (height - ih) / 2, iw, ih),\n\t\ticonImage);\n\n\tp.end();\n\treturn image;\n}\n\n} // namespace\n\nAboutView::AboutView(\n\tnot_null<History*> history,\n\tnot_null<ElementDelegate*> delegate)\n: _history(history)\n, _delegate(delegate) {\n}\n\nAboutView::~AboutView() {\n\tsetItem({}, nullptr);\n}\n\nnot_null<History*> AboutView::history() const {\n\treturn _history;\n}\n\nElement *AboutView::view() const {\n\treturn _item.get();\n}\n\nHistoryItem *AboutView::item() const {\n\tif (const auto element = view()) {\n\t\treturn element->data();\n\t}\n\treturn nullptr;\n}\n\nbool AboutView::aboveHistory() const {\n\tif (!_history->peer->isBot() || !_history->isForum()) {\n\t\treturn true;\n\t}\n\tconst auto info = _history->peer->asUser()->botInfo.get();\n\treturn !(info->userCreatesTopics\n\t\t&& info->startToken.isEmpty()\n\t\t&& (!_history->isEmpty() || _history->lastMessage()));\n}\n\nbool AboutView::refresh() {\n\tif (_history->peer->isVerifyCodes()) {\n\t\tif (_item) {\n\t\t\treturn false;\n\t\t}\n\t\tsetItem(makeAboutVerifyCodes(), nullptr);\n\t\treturn true;\n\t}\n\tconst auto user = _history->peer->asUser();\n\tconst auto monoforum = _history->peer->isMonoforum()\n\t\t? _history->peer->asChannel()\n\t\t: nullptr;\n\tconst auto info = user ? user->botInfo.get() : nullptr;\n\tif (!info) {\n\t\tif (user\n\t\t\t&& !user->isContact()\n\t\t\t&& !user->phoneCountryCode().isEmpty()) {\n\t\t\tif (_item && !_commonGroupsStale) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tloadCommonGroups();\n\t\t\tsetItem(makeNewPeerInfo(user), nullptr);\n\t\t\treturn true;\n\t\t} else if (user && !user->isSelf() && _history->isDisplayedEmpty()) {\n\t\t\tif (_item) {\n\t\t\t\treturn false;\n\t\t\t} else if (user->requiresPremiumToWrite()\n\t\t\t\t&& !user->session().premium()) {\n\t\t\t\tsetItem(makePremiumRequired(), nullptr);\n\t\t\t} else if (user->isBlocked()) {\n\t\t\t\tsetItem(makeBlocked(), nullptr);\n\t\t\t} else if (user->businessDetails().intro) {\n\t\t\t\tmakeIntro(user);\n\t\t\t} else if (const auto stars = user->starsPerMessageChecked()) {\n\t\t\t\tsetItem(makeStarsPerMessage(stars), nullptr);\n\t\t\t} else {\n\t\t\t\tmakeIntro(user);\n\t\t\t}\n\t\t\treturn true;\n\t\t} else if (monoforum && _history->isDisplayedEmpty()) {\n\t\t\tif (_item) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tsetItem(\n\t\t\t\tmakeStarsPerMessage(monoforum->starsPerMessageChecked()),\n\t\t\t\tnullptr);\n\t\t\treturn true;\n\t\t}\n\t\tif (_item) {\n\t\t\tsetItem({}, nullptr);\n\t\t\treturn true;\n\t\t}\n\t\t_version = 0;\n\t\treturn false;\n\t} else if (_history->peer->isForum()\n\t\t\t&& info->userCreatesTopics\n\t\t\t&& info->startToken.isEmpty()\n\t\t\t&& (!_history->isEmpty() || _history->lastMessage())) {\n\t\tif (_item) {\n\t\t\treturn false;\n\t\t}\n\t\tsetItem(makeNewBotThread(), nullptr);\n\t\treturn true;\n\t} else if (user->botManagerId()\n\t\t\t&& info\n\t\t\t&& info->description.isEmpty()\n\t\t\t&& info->canEditInformation\n\t\t\t&& _history->isEmpty()\n\t\t\t&& !_history->lastMessage()) {\n\t\tif (_item) {\n\t\t\treturn false;\n\t\t}\n\t\tsetItem(makeManagedBotInfo(user), nullptr);\n\t\t_history->session().data().newItemAdded(\n\t\t) | rpl::on_next([=](not_null<HistoryItem*> item) {\n\t\t\tif (item->history() == _history) {\n\t\t\t\t_destroyRequests.fire({});\n\t\t\t}\n\t\t}, lifetime());\n\t\treturn true;\n\t}\n\tconst auto version = info->descriptionVersion;\n\tif (_version == version) {\n\t\treturn false;\n\t}\n\t_version = version;\n\tif (_history->peer->isBot() && _history->peer->isForum()) {\n\t\t_history->session().data().newItemAdded(\n\t\t) | rpl::on_next([=](not_null<HistoryItem*> item) {\n\t\t\tif (item->history() == _history) {\n\t\t\t\t_destroyRequests.fire({});\n\t\t\t}\n\t\t}, lifetime());\n\t}\n\tsetItem(makeAboutBot(info), nullptr);\n\treturn true;\n}\n\nvoid AboutView::makeIntro(not_null<UserData*> user) {\n\tmake(user->businessDetails().intro);\n}\n\nvoid AboutView::make(Data::ChatIntro data, bool preview) {\n\tconst auto text = data\n\t\t? tr::lng_action_set_chat_intro(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\t_history->peer->name())\n\t\t: QString();\n\tconst auto item = _history->makeMessage({\n\t\t.id = _history->nextNonHistoryEntryId(),\n\t\t.flags = (MessageFlag::FakeAboutView\n\t\t\t| MessageFlag::FakeHistoryItem\n\t\t\t| MessageFlag::Local),\n\t\t.from = _history->peer->id,\n\t}, PreparedServiceText{ { text } });\n\n\tif (data.sticker) {\n\t\t_helloChosen = nullptr;\n\t} else if (_helloChosen) {\n\t\tdata.sticker = _helloChosen;\n\t}\n\n\tauto owned = AdminLog::OwnedItem(_delegate, item);\n\tconst auto helloChosen = [=](not_null<DocumentData*> sticker) {\n\t\tsetHelloChosen(sticker);\n\t};\n\tconst auto handler = [=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tif (controller->session().premium()) {\n\t\t\t\tcontroller->showSettings(Settings::ChatIntroId());\n\t\t\t} else {\n\t\t\t\tShowPremiumPreviewBox(\n\t\t\t\t\tcontroller->uiShow(),\n\t\t\t\t\tPremiumFeature::ChatIntro);\n\t\t\t}\n\t\t}\n\t};\n\tconst auto sendIntroSticker = [=](not_null<DocumentData*> sticker) {\n\t\t_sendIntroSticker.fire_copy(sticker);\n\t};\n\towned->data()->setCustomServiceLink(\n\t\tstd::make_shared<LambdaClickHandler>(handler));\n\towned->overrideMedia(std::make_unique<HistoryView::MediaGeneric>(\n\t\towned.get(),\n\t\tGenerateChatIntro(\n\t\t\towned.get(),\n\t\t\t_item.get(),\n\t\t\tdata,\n\t\t\thelloChosen,\n\t\t\tsendIntroSticker),\n\t\tHistoryView::MediaGenericDescriptor{\n\t\t\t.maxWidth = st::chatIntroWidth,\n\t\t\t.service = true,\n\t\t\t.hideServiceText = preview || text.isEmpty(),\n\t\t}));\n\tif (!data.sticker && _helloChosen) {\n\t\tdata.sticker = _helloChosen;\n\t}\n\tsetItem(std::move(owned), data.sticker);\n}\n\nrpl::producer<not_null<DocumentData*>> AboutView::sendIntroSticker() const {\n\treturn _sendIntroSticker.events();\n}\n\nrpl::producer<> AboutView::refreshRequests() const {\n\treturn _refreshRequests.events();\n}\n\nrpl::producer<> AboutView::destroyRequests() const {\n\treturn _destroyRequests.events();\n}\n\nrpl::lifetime &AboutView::lifetime() {\n\treturn _lifetime;\n}\n\nvoid AboutView::toggleStickerRegistered(bool registered) {\n\tif (const auto item = _item ? _item->data().get() : nullptr) {\n\t\tif (_sticker) {\n\t\t\tconst auto owner = &item->history()->owner();\n\t\t\tif (registered) {\n\t\t\t\towner->registerDocumentItem(_sticker, item);\n\t\t\t} else {\n\t\t\t\towner->unregisterDocumentItem(_sticker, item);\n\t\t\t}\n\t\t}\n\t}\n\tif (!registered) {\n\t\t_sticker = nullptr;\n\t}\n}\n\nvoid AboutView::loadCommonGroups() {\n\tif (_commonGroupsRequested) {\n\t\treturn;\n\t}\n\t_commonGroupsRequested = true;\n\n\tconst auto user = _history->peer->asUser();\n\tif (!user) {\n\t\treturn;\n\t}\n\n\tstruct Cached {\n\t\tstd::vector<not_null<PeerData*>> list;\n\t};\n\tstruct Session {\n\t\tbase::flat_map<not_null<UserData*>, Cached> data;\n\t};\n\tstatic auto Map = base::flat_map<not_null<Main::Session*>, Session>();\n\tconst auto session = &_history->session();\n\tauto i = Map.find(session);\n\tif (i == end(Map)) {\n\t\ti = Map.emplace(session).first;\n\t\tsession->lifetime().add([session] {\n\t\t\tMap.remove(session);\n\t\t});\n\t}\n\tauto &cached = i->second.data[user];\n\n\tconst auto count = user->commonChatsCount();\n\tif (!count) {\n\t\tcached = {};\n\t\treturn;\n\t} else while (cached.list.size() > count) {\n\t\tcached.list.pop_back();\n\t}\n\t_commonGroups = cached.list;\n\tconst auto requestId = _history->session().api().request(\n\t\tMTPmessages_GetCommonChats(\n\t\t\tuser->inputUser(),\n\t\t\tMTP_long(0),\n\t\t\tMTP_int(kMaxCommonChatsUserpics))\n\t).done([=](const MTPmessages_Chats &result) {\n\t\tconst auto chats = result.match([](const auto &data) {\n\t\t\treturn &data.vchats().v;\n\t\t});\n\t\tauto &owner = user->session().data();\n\t\tauto list = std::vector<not_null<PeerData*>>();\n\t\tlist.reserve(chats->size());\n\t\tfor (const auto &chat : *chats) {\n\t\t\tif (const auto peer = owner.processChat(chat)) {\n\t\t\t\tlist.push_back(peer);\n\t\t\t\tif (list.size() == kMaxCommonChatsUserpics) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (_commonGroups != list) {\n\t\t\tMap[session].data[user].list = list;\n\t\t\t_commonGroups = std::move(list);\n\t\t\t_commonGroupsStale = true;\n\t\t\t_refreshRequests.fire({});\n\t\t}\n\t}).send();\n\n\t_lifetime.add([=] {\n\t\t_history->session().api().request(requestId).cancel();\n\t});\n}\n\nvoid AboutView::setHelloChosen(not_null<DocumentData*> sticker) {\n\t_helloChosen = sticker;\n\ttoggleStickerRegistered(false);\n\t_sticker = sticker;\n\ttoggleStickerRegistered(true);\n}\n\nvoid AboutView::setItem(AdminLog::OwnedItem item, DocumentData *sticker) {\n\ttoggleStickerRegistered(false);\n\t_item = std::move(item);\n\t_sticker = sticker;\n\ttoggleStickerRegistered(true);\n}\n\nAdminLog::OwnedItem AboutView::makeNewPeerInfo(not_null<UserData*> user) {\n\t_commonGroupsStale = false;\n\n\tconst auto text = user->name();\n\tconst auto item = _history->makeMessage({\n\t\t.id = _history->nextNonHistoryEntryId(),\n\t\t.flags = (MessageFlag::FakeAboutView\n\t\t\t| MessageFlag::FakeHistoryItem\n\t\t\t| MessageFlag::Local),\n\t\t.from = _history->peer->id,\n\t}, PreparedServiceText{ { text } });\n\n\tauto owned = AdminLog::OwnedItem(_delegate, item);\n\towned->overrideMedia(std::make_unique<HistoryView::MediaGeneric>(\n\t\towned.get(),\n\t\tGenerateNewPeerInfo(owned.get(), _item.get(), user, _commonGroups),\n\t\tHistoryView::MediaGenericDescriptor{\n\t\t\t.maxWidth = st::newPeerWidth,\n\t\t\t.service = true,\n\t\t\t.hideServiceText = true,\n\t\t}));\n\treturn owned;\n}\n\nAdminLog::OwnedItem AboutView::makeAboutVerifyCodes() {\n\treturn makeAboutSimple(\n\t\ttr::lng_verification_codes_about(tr::now, tr::rich));\n}\n\nAdminLog::OwnedItem AboutView::makeAboutBot(not_null<BotInfo*> info) {\n\treturn makeAboutSimple(\n\t\tTextUtilities::ParseEntities(\n\t\t\tinfo->description,\n\t\t\tUi::ItemTextBotNoMonoOptions().flags),\n\t\tinfo->document,\n\t\tinfo->photo);\n}\n\nAdminLog::OwnedItem AboutView::makeAboutSimple(\n\t\tTextWithEntities textWithEntities,\n\t\tDocumentData *document,\n\t\tPhotoData *photo) {\n\tconst auto make = [&](auto &&...args) {\n\t\treturn _history->makeMessage({\n\t\t\t.id = _history->nextNonHistoryEntryId(),\n\t\t\t.flags = (MessageFlag::FakeAboutView\n\t\t\t\t| MessageFlag::FakeHistoryItem\n\t\t\t\t| MessageFlag::Local),\n\t\t\t.from = _history->peer->id,\n\t\t}, std::forward<decltype(args)>(args)...);\n\t};\n\tconst auto item = document\n\t\t? make(document, textWithEntities)\n\t\t: photo\n\t\t? make(photo, textWithEntities)\n\t\t: make(textWithEntities, MTP_messageMediaEmpty());\n\treturn AdminLog::OwnedItem(_delegate, item);\n}\n\nAdminLog::OwnedItem AboutView::makePremiumRequired() {\n\tconst auto item = _history->makeMessage({\n\t\t.id = _history->nextNonHistoryEntryId(),\n\t\t.flags = (MessageFlag::FakeAboutView\n\t\t\t| MessageFlag::FakeHistoryItem\n\t\t\t| MessageFlag::Local),\n\t\t.from = _history->peer->id,\n\t}, PreparedServiceText{ tr::lng_send_non_premium_text(\n\t\ttr::now,\n\t\tlt_user,\n\t\ttr::bold(_history->peer->shortName()),\n\t\ttr::rich),\n\t});\n\tauto result = AdminLog::OwnedItem(_delegate, item);\n\tresult->overrideMedia(std::make_unique<ServiceBox>(\n\t\tresult.get(),\n\t\tstd::make_unique<EmptyChatLockedBox>(\n\t\t\tresult.get(),\n\t\t\tEmptyChatLockedBox::Type::PremiumRequired)));\n\treturn result;\n}\n\nAdminLog::OwnedItem AboutView::makeStarsPerMessage(int stars) {\n\tauto name = tr::bold(_history->peer->shortName());\n\tauto cost = Ui::Text::IconEmoji(\n\t\t&st::starIconEmoji\n\t).append(tr::bold(Lang::FormatCountDecimal(stars)));\n\tconst auto item = _history->makeMessage({\n\t\t.id = _history->nextNonHistoryEntryId(),\n\t\t.flags = (MessageFlag::FakeAboutView\n\t\t\t| MessageFlag::FakeHistoryItem\n\t\t\t| MessageFlag::Local),\n\t\t.from = _history->peer->id,\n\t}, PreparedServiceText{ !_history->peer->isMonoforum()\n\t\t? tr::lng_send_charges_stars_text(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\tstd::move(name),\n\t\t\tlt_amount,\n\t\t\tstd::move(cost),\n\t\t\ttr::rich)\n\t\t: stars\n\t\t? tr::lng_send_charges_stars_channel(\n\t\t\ttr::now,\n\t\t\tlt_channel,\n\t\t\tstd::move(name),\n\t\t\tlt_amount,\n\t\t\tstd::move(cost),\n\t\t\ttr::rich)\n\t\t: tr::lng_send_free_channel(\n\t\t\ttr::now,\n\t\t\tlt_channel,\n\t\t\tstd::move(name),\n\t\t\ttr::rich),\n\t});\n\tauto result = AdminLog::OwnedItem(_delegate, item);\n\tresult->overrideMedia(std::make_unique<ServiceBox>(\n\t\tresult.get(),\n\t\tstd::make_unique<EmptyChatLockedBox>(\n\t\t\tresult.get(),\n\t\t\t(stars\n\t\t\t\t? EmptyChatLockedBox::Type::StarsCharged\n\t\t\t\t: EmptyChatLockedBox::Type::FreeDirect))));\n\treturn result;\n}\n\nAdminLog::OwnedItem AboutView::makeBlocked() {\n\tconst auto item = _history->makeMessage({\n\t\t.id = _history->nextNonHistoryEntryId(),\n\t\t.flags = (MessageFlag::FakeAboutView\n\t\t\t| MessageFlag::FakeHistoryItem\n\t\t\t| MessageFlag::Local),\n\t\t.from = _history->peer->id,\n\t}, PreparedServiceText{\n\t\t{ tr::lng_chat_intro_default_title(tr::now) }\n\t});\n\treturn AdminLog::OwnedItem(_delegate, item);\n}\n\nAdminLog::OwnedItem AboutView::makeNewBotThread() {\n\tconst auto item = _history->makeMessage({\n\t\t.id = _history->nextNonHistoryEntryId(),\n\t\t.flags = (MessageFlag::FakeAboutView\n\t\t\t| MessageFlag::FakeHistoryItem\n\t\t\t| MessageFlag::Local),\n\t\t.from = _history->peer->id,\n\t}, PreparedServiceText{\n\t\ttr::lng_bot_new_thread_about(tr::now, tr::rich)\n\t});\n\tauto result = AdminLog::OwnedItem(_delegate, item);\n\tresult->overrideMedia(std::make_unique<MediaGeneric>(\n\t\tresult.get(),\n\t\tGenerateNewBotThread(result.get(), _item.get()),\n\t\tHistoryView::MediaGenericDescriptor{\n\t\t\t.maxWidth = st::newThreadAboutMaxWidth,\n\t\t\t.service = true,\n\t\t\t.hideServiceText = true,\n\t\t}));\n\treturn result;\n}\n\nAdminLog::OwnedItem AboutView::makeManagedBotInfo(\n\t\tnot_null<UserData*> user) {\n\tconst auto image = GenerateManagedBotImage(user);\n\tconst auto photoImage = image.isNull()\n\t\t? ImageWithLocation()\n\t\t: Images::FromImageInMemory(image, \"PNG\");\n\tconst auto photo = _history->session().data().photo(\n\t\tbase::RandomValue<PhotoId>(),\n\t\tuint64(0),\n\t\tQByteArray(),\n\t\tbase::unixtime::now(),\n\t\t0,\n\t\tfalse,\n\t\tQByteArray(),\n\t\tImageWithLocation{},\n\t\tphotoImage,\n\t\tphotoImage,\n\t\tImageWithLocation{},\n\t\tImageWithLocation{},\n\t\tcrl::time(0));\n\n\tconst auto managerId = user->botManagerId();\n\tconst auto managerUser = user->owner().userLoaded(managerId);\n\tconst auto parentName = managerUser\n\t\t? managerUser->name()\n\t\t: QString();\n\tauto text = tr::lng_managed_bot_ready(\n\t\ttr::now,\n\t\tlt_name,\n\t\ttr::bold(user->name()),\n\t\tlt_parent,\n\t\ttr::bold(parentName),\n\t\ttr::rich);\n\n\treturn makeAboutSimple(text, nullptr, photo);\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_about_view.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/admin_log/history_admin_log_item.h\"\n\nnamespace Data {\nstruct ChatIntro;\n} // namespace Data\n\nnamespace HistoryView {\n\nclass AboutView final : public ClickHandlerHost {\npublic:\n\tAboutView(\n\t\tnot_null<History*> history,\n\t\tnot_null<ElementDelegate*> delegate);\n\t~AboutView();\n\n\t[[nodiscard]] not_null<History*> history() const;\n\t[[nodiscard]] Element *view() const;\n\t[[nodiscard]] HistoryItem *item() const;\n\t[[nodiscard]] bool aboveHistory() const;\n\n\tbool refresh();\n\n\tvoid make(Data::ChatIntro data, bool preview = false);\n\n\t[[nodiscard]] auto sendIntroSticker() const\n\t\t-> rpl::producer<not_null<DocumentData*>>;\n\t[[nodiscard]] rpl::producer<> refreshRequests() const;\n\t[[nodiscard]] rpl::producer<> destroyRequests() const;\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\n\tint top = 0;\n\tint height = 0;\n\nprivate:\n\t[[nodiscard]] AdminLog::OwnedItem makeAboutVerifyCodes();\n\t[[nodiscard]] AdminLog::OwnedItem makeAboutBot(not_null<BotInfo*> info);\n\t[[nodiscard]] AdminLog::OwnedItem makeAboutSimple(\n\t\tTextWithEntities textWithEntities,\n\t\tDocumentData *document = nullptr,\n\t\tPhotoData *photo = nullptr);\n\t[[nodiscard]] AdminLog::OwnedItem makePremiumRequired();\n\t[[nodiscard]] AdminLog::OwnedItem makeStarsPerMessage(int stars);\n\t[[nodiscard]] AdminLog::OwnedItem makeNewPeerInfo(\n\t\tnot_null<UserData*> user);\n\t[[nodiscard]] AdminLog::OwnedItem makeBlocked();\n\t[[nodiscard]] AdminLog::OwnedItem makeNewBotThread();\n\t[[nodiscard]] AdminLog::OwnedItem makeManagedBotInfo(\n\t\tnot_null<UserData*> user);\n\tvoid makeIntro(not_null<UserData*> user);\n\tvoid setItem(AdminLog::OwnedItem item, DocumentData *sticker);\n\tvoid setHelloChosen(not_null<DocumentData*> sticker);\n\tvoid toggleStickerRegistered(bool registered);\n\n\tvoid loadCommonGroups();\n\n\tconst not_null<History*> _history;\n\tconst not_null<ElementDelegate*> _delegate;\n\tAdminLog::OwnedItem _item;\n\n\tDocumentData *_helloChosen = nullptr;\n\tDocumentData *_sticker = nullptr;\n\tint _version = 0;\n\n\trpl::event_stream<not_null<DocumentData*>> _sendIntroSticker;\n\n\tbool _commonGroupsStale = false;\n\tbool _commonGroupsRequested = false;\n\tstd::vector<not_null<PeerData*>> _commonGroups;\n\trpl::event_stream<> _refreshRequests;\n\trpl::event_stream<> _destroyRequests;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_add_poll_option.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_add_poll_option.h\"\n\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_element_overlay.h\"\n#include \"history/view/media/history_view_media.h\"\n#include \"history/view/media/menu/history_view_poll_menu.h\"\n#include \"api/api_polls.h\"\n#include \"apiwrap.h\"\n#include \"base/event_filter.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/file_utilities.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"poll/poll_media_upload.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_polls.h\"\n\nnamespace HistoryView {\nnamespace {\n\nclass AddPollOptionWidget final : public Ui::RpWidget {\npublic:\n\tAddPollOptionWidget(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId itemId,\n\t\tnot_null<Window::SessionController*> controller);\n\n\tvoid setPlaceholderColorOverride(const style::color &color);\n\tvoid setIconColorOverride(QColor color);\n\tvoid updatePosition(QPoint topLeft, int width);\n\tvoid triggerSubmit();\n\n\t[[nodiscard]] rpl::producer<> submitted() const;\n\t[[nodiscard]] rpl::producer<> cancelled() const;\n\nprivate:\n\tvoid setupField();\n\tvoid setupEmojiPanel();\n\tvoid setupAttach();\n\tvoid showStickerPanel();\n\tvoid subscribeToPollUpdates();\n\t[[nodiscard]] static QString mapErrorToText(const QString &error);\n\n\tconst not_null<PollData*> _poll;\n\tconst FullMsgId _itemId;\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<Main::Session*> _session;\n\n\tUi::InputField *_field = nullptr;\n\tUi::IconButton *_emoji = nullptr;\n\tPollMediaUpload::PollMediaButton *_attach = nullptr;\n\tbase::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;\n\tbase::unique_qptr<ChatHelpers::TabbedPanel> _stickerPanel;\n\tbase::unique_qptr<Ui::PopupMenu> _mediaMenu;\n\tstd::unique_ptr<PollMediaUpload::PollMediaUploader> _uploader;\n\tstd::shared_ptr<PollMediaUpload::PollMediaState> _mediaState;\n\n\trpl::event_stream<> _submittedEvents;\n\trpl::event_stream<> _cancelledEvents;\n\n};\n\nAddPollOptionWidget::AddPollOptionWidget(\n\tnot_null<QWidget*> parent,\n\tnot_null<PollData*> poll,\n\tFullMsgId itemId,\n\tnot_null<Window::SessionController*> controller)\n: RpWidget(parent)\n, _poll(poll)\n, _itemId(itemId)\n, _controller(controller)\n, _session(&controller->session())\n, _mediaState(std::make_shared<PollMediaUpload::PollMediaState>()) {\n\tconst auto item = _session->data().message(_itemId);\n\tconst auto peer = item ? item->history()->peer.get() : nullptr;\n\tif (peer) {\n\t\t_uploader = std::make_unique<PollMediaUpload::PollMediaUploader>(\n\t\t\tPollMediaUpload::PollMediaUploader::Args{\n\t\t\t\t.session = _session,\n\t\t\t\t.peer = peer,\n\t\t\t\t.showError = [=](const QString &text) {\n\t\t\t\t\tUi::Toast::Show(parentWidget(), text);\n\t\t\t\t},\n\t\t\t});\n\t}\n\tsetupField();\n\tsetupEmojiPanel();\n\tsetupAttach();\n\tsubscribeToPollUpdates();\n}\n\nvoid AddPollOptionWidget::setupField() {\n\t_field = Ui::CreateChild<Ui::InputField>(\n\t\tthis,\n\t\tst::historyPollAddOptionField,\n\t\tUi::InputField::Mode::NoNewlines,\n\t\ttr::lng_polls_add_option_placeholder());\n\n\t_emoji = Ui::CreateChild<Ui::IconButton>(\n\t\tthis,\n\t\tst::historyPollAddOptionEmoji);\n\n\t_attach = Ui::CreateChild<PollMediaUpload::PollMediaButton>(\n\t\tthis,\n\t\tst::historyPollAddOptionAttach,\n\t\t_mediaState);\n\n\t_field->setMaxLength(100);\n\t_field->setCustomTextContext(Core::TextContext({\n\t\t.session = _session,\n\t}));\n\n\tconst auto field = _field;\n\tconst auto emoji = _emoji;\n\tconst auto attach = _attach;\n\tsizeValue(\n\t) | rpl::on_next([field, emoji, attach](QSize size) {\n\t\tfield->setGeometry(0, 0, size.width(), size.height());\n\t\tconst auto bsize = st::historyPollAddOptionButtonSize;\n\t\tconst auto by = (size.height() - bsize) / 2;\n\t\temoji->moveToLeft(st::historyPollAddOptionEmojiLeft, by);\n\t\tattach->moveToRight(0, by, size.width());\n\t}, _field->lifetime());\n\n\t_field->submits(\n\t) | rpl::on_next([=] {\n\t\ttriggerSubmit();\n\t}, _field->lifetime());\n\n\tbase::install_event_filter(_field, [=](not_null<QEvent*> event) {\n\t\tif (event->type() == QEvent::KeyPress) {\n\t\t\tif (static_cast<QKeyEvent*>(event.get())->key() == Qt::Key_Escape) {\n\t\t\t\tif (_emojiPanel && !_emojiPanel->isHidden()) {\n\t\t\t\t\t_emojiPanel->hideAnimated();\n\t\t\t\t} else {\n\t\t\t\t\t_cancelledEvents.fire({});\n\t\t\t\t}\n\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t}\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\t_field->setFocusFast();\n}\n\nvoid AddPollOptionWidget::setupEmojiPanel() {\n\tusing Selector = ChatHelpers::TabbedSelector;\n\n\tconst auto parent = parentWidget();\n\t_emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(\n\t\tparent,\n\t\t_controller,\n\t\tobject_ptr<Selector>(\n\t\t\tnullptr,\n\t\t\t_controller->uiShow(),\n\t\t\tWindow::GifPauseReason::Layer,\n\t\t\tSelector::Mode::EmojiOnly));\n\n\tconst auto panel = _emojiPanel.get();\n\tpanel->setDesiredHeightValues(\n\t\t1.,\n\t\tst::emojiPanMinHeight / 2,\n\t\tst::emojiPanMinHeight);\n\tpanel->hide();\n\tpanel->selector()->setCurrentPeer(_session->user().get());\n\n\t_emoji->installEventFilter(panel);\n\t_emoji->addClickHandler([=] {\n\t\tconst auto button = QRect(\n\t\t\t_emoji->mapTo(parent, QPoint()),\n\t\t\t_emoji->size());\n\t\tconst auto isDropDown = button.y() < parent->height() / 2;\n\t\tpanel->setDropDown(isDropDown);\n\t\tif (isDropDown) {\n\t\t\tpanel->moveTopRight(\n\t\t\t\tbutton.y() + button.height(),\n\t\t\t\tbutton.x() + button.width());\n\t\t} else {\n\t\t\tpanel->moveBottomRight(\n\t\t\t\tbutton.y(),\n\t\t\t\tbutton.x() + button.width());\n\t\t}\n\t\tpanel->toggleAnimated();\n\t});\n\n\tpanel->selector()->emojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\tUi::InsertEmojiAtCursor(_field->textCursor(), data.emoji);\n\t}, panel->lifetime());\n\n\tpanel->selector()->customEmojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\tData::InsertCustomEmoji(_field, data.document);\n\t}, panel->lifetime());\n}\n\nvoid AddPollOptionWidget::setupAttach() {\n\tif (!_uploader) {\n\t\treturn;\n\t}\n\t_attach->addClickHandler([=] {\n\t\tif (ShowPollMediaPreview(_controller, _mediaState, {\n\t\t\t.choosePhotoOrVideo = [=] {\n\t\t\t\t_uploader->choosePhotoOrVideo(this, _mediaState);\n\t\t\t},\n\t\t\t.chooseDocument = [=] {\n\t\t\t\t_uploader->chooseDocument(this, _mediaState);\n\t\t\t},\n\t\t\t.chooseSticker = [=] { showStickerPanel(); },\n\t\t\t.editPhoto = [=](Ui::PreparedList list) {\n\t\t\t\t_uploader->applyPreparedPhotoList(\n\t\t\t\t\t_mediaState,\n\t\t\t\t\tstd::move(list));\n\t\t\t},\n\t\t\t.remove = [=] { _uploader->clearMedia(_mediaState); },\n\t\t})) {\n\t\t\treturn;\n\t\t}\n\t\t_mediaMenu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t_attach,\n\t\t\tst::popupMenuWithIcons);\n\t\t_mediaMenu->setForcedOrigin(\n\t\t\tUi::PanelAnimation::Origin::TopRight);\n\t\t_mediaMenu->addAction(\n\t\t\ttr::lng_attach_photo_or_video(tr::now),\n\t\t\t[=] { _uploader->choosePhotoOrVideo(this, _mediaState); },\n\t\t\t&st::menuIconPhoto);\n\t\t_mediaMenu->addAction(\n\t\t\ttr::lng_attach_file(tr::now),\n\t\t\t[=] { _uploader->chooseDocument(this, _mediaState); },\n\t\t\t&st::menuIconFile);\n\t\t_mediaMenu->addAction(\n\t\t\ttr::lng_chat_intro_choose_sticker(tr::now),\n\t\t\t[=] { showStickerPanel(); },\n\t\t\t&st::menuIconStickers);\n\t\t_mediaMenu->popup(QCursor::pos());\n\t});\n\t_uploader->installDropToField(_field, _mediaState, false);\n}\n\nvoid AddPollOptionWidget::showStickerPanel() {\n\tconst auto parent = parentWidget();\n\tif (!_stickerPanel) {\n\t\t_stickerPanel = CreatePollStickerPanel(parent, _controller);\n\t\t_stickerPanel->selector()->fileChosen(\n\t\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\t\tif (Window::ShowSendPremiumError(\n\t\t\t\t\t_controller,\n\t\t\t\t\tdata.document)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_uploader->setMedia(\n\t\t\t\t_mediaState,\n\t\t\t\tPollMedia{ .document = data.document },\n\t\t\t\tUi::MakeEmojiThumbnail(\n\t\t\t\t\t&_session->data(),\n\t\t\t\t\tData::SerializeCustomEmojiId(data.document)),\n\t\t\t\tfalse);\n\t\t\t_stickerPanel->hideAnimated();\n\t\t}, _stickerPanel->lifetime());\n\t}\n\tconst auto panel = _stickerPanel.get();\n\tconst auto button = QRect(\n\t\t_attach->mapTo(parent, QPoint()),\n\t\t_attach->size());\n\tconst auto isDropDown = button.y() < parent->height() / 2;\n\tpanel->setDropDown(isDropDown);\n\tif (isDropDown) {\n\t\tpanel->moveTopRight(\n\t\t\tbutton.y() + button.height(),\n\t\t\tbutton.x() + button.width());\n\t} else {\n\t\tpanel->moveBottomRight(\n\t\t\tbutton.y(),\n\t\t\tbutton.x() + button.width());\n\t}\n\tpanel->toggleAnimated();\n}\n\nvoid AddPollOptionWidget::subscribeToPollUpdates() {\n\t_session->data().pollUpdates(\n\t) | rpl::filter([=](not_null<PollData*> poll) {\n\t\treturn (poll == _poll);\n\t}) | rpl::on_next([=](not_null<PollData*> poll) {\n\t\tif (poll->closed()\n\t\t\t|| (int(poll->answers.size())\n\t\t\t\t>= _session->appConfig().pollOptionsLimit())) {\n\t\t\t_cancelledEvents.fire({});\n\t\t}\n\t}, lifetime());\n\n\t_session->data().itemRemoved(\n\t) | rpl::filter([=](not_null<const HistoryItem*> item) {\n\t\treturn (item->fullId() == _itemId);\n\t}) | rpl::on_next([=] {\n\t\t_cancelledEvents.fire({});\n\t}, lifetime());\n}\n\nvoid AddPollOptionWidget::triggerSubmit() {\n\tconst auto textWithTags = _field->getTextWithAppliedMarkdown();\n\tauto fullText = TextWithEntities{\n\t\ttextWithTags.text,\n\t\tTextUtilities::ConvertTextTagsToEntities(textWithTags.tags),\n\t};\n\tTextUtilities::Trim(fullText);\n\tif (fullText.text.isEmpty()) {\n\t\treturn;\n\t}\n\tif (int(_poll->answers.size())\n\t\t>= _session->appConfig().pollOptionsLimit()) {\n\t\tUi::Toast::Show(\n\t\t\tparentWidget(),\n\t\t\ttr::lng_polls_max_options_reached(tr::now));\n\t\treturn;\n\t}\n\n\t_field->setEnabled(false);\n\n\tconst auto media = _mediaState ? _mediaState->media : PollMedia();\n\t_session->api().polls().addAnswer(\n\t\t_itemId,\n\t\tfullText,\n\t\tmedia,\n\t\t[=] { _submittedEvents.fire({}); },\n\t\t[=](QString error) {\n\t\t\t_field->setEnabled(true);\n\t\t\tUi::Toast::Show(\n\t\t\t\tparentWidget(),\n\t\t\t\tmapErrorToText(error));\n\t\t});\n}\n\nQString AddPollOptionWidget::mapErrorToText(const QString &error) {\n\tif (error == u\"POLL_ANSWERS_TOO_MUCH\"_q) {\n\t\treturn tr::lng_polls_max_options_reached(tr::now);\n\t} else if (error == u\"POLL_ANSWER_DUPLICATE\"_q) {\n\t\treturn tr::lng_polls_add_option_duplicate(tr::now);\n\t} else if (error.startsWith(u\"POLL\"_q) && error.contains(u\"CLOSE\"_q)) {\n\t\treturn tr::lng_polls_add_option_closed(tr::now);\n\t}\n\treturn tr::lng_polls_add_option_error(tr::now);\n}\n\nvoid AddPollOptionWidget::updatePosition(QPoint topLeft, int w) {\n\tsetGeometry(\n\t\ttopLeft.x(),\n\t\ttopLeft.y() + st::historyPollAddOptionTop,\n\t\tw,\n\t\tst::historyPollAddOptionField.heightMin);\n}\n\nrpl::producer<> AddPollOptionWidget::submitted() const {\n\treturn _submittedEvents.events();\n}\n\nrpl::producer<> AddPollOptionWidget::cancelled() const {\n\treturn _cancelledEvents.events();\n}\n\nvoid AddPollOptionWidget::setPlaceholderColorOverride(\n\t\tconst style::color &color) {\n\t_field->setPlaceholderColorOverride(color);\n}\n\nvoid AddPollOptionWidget::setIconColorOverride(QColor color) {\n\t_emoji->setIconColorOverride(color);\n\t_attach->setIconColorOverride(color);\n\t_attach->setRippleColorOverride(anim::with_alpha(color, 0.15));\n}\n\n} // namespace\n\nvoid ShowAddPollOptionOverlay(\n\t\tElementOverlayHost &host,\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Element*> view,\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<const Ui::ChatStyle*> st) {\n\tauto widget = base::make_unique_q<AddPollOptionWidget>(\n\t\tparent,\n\t\tpoll,\n\t\tcontext,\n\t\tcontroller);\n\tconst auto raw = widget.get();\n\tconst auto &msgSt = st->messageStyle(view->hasOutLayout(), false);\n\traw->setPlaceholderColorOverride(msgSt.msgDateFg);\n\traw->setIconColorOverride(msgSt.msgDateFg->c);\n\thost.show(\n\t\tview,\n\t\tcontext,\n\t\tstd::move(widget),\n\t\trpl::merge(raw->submitted(), raw->cancelled()),\n\t\t[raw](not_null<Element*> v, int top) {\n\t\t\tconst auto media = v->media();\n\t\t\tif (!media) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto mediaPos = v->mediaTopLeft();\n\t\t\tconst auto innerWidth = v->innerGeometry().width()\n\t\t\t\t- st::msgPadding.left()\n\t\t\t\t- st::msgPadding.right();\n\t\t\tconst auto rect = media->addOptionRect(innerWidth);\n\t\t\traw->updatePosition(\n\t\t\t\tQPoint(\n\t\t\t\t\tmediaPos.x() + rect.x(),\n\t\t\t\t\ttop + mediaPos.y() + rect.y()),\n\t\t\t\trect.width());\n\t\t\treturn true;\n\t\t},\n\t\t[](not_null<Element*> v, bool active) {\n\t\t\tif (const auto media = v->media()) {\n\t\t\t\tmedia->setAddOptionActive(active);\n\t\t\t}\n\t\t},\n\t\t[raw] { raw->triggerSubmit(); });\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_add_poll_option.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nstruct PollData;\n\nnamespace Ui {\nclass ChatStyle;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace HistoryView {\n\nclass Element;\nclass ElementOverlayHost;\n\nvoid ShowAddPollOptionOverlay(\n\tElementOverlayHost &host,\n\tnot_null<QWidget*> parent,\n\tnot_null<Element*> view,\n\tnot_null<PollData*> poll,\n\tFullMsgId context,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<const Ui::ChatStyle*> st);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_bottom_info.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_bottom_info.h\"\n\n#include \"ui/chat/message_bubble.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/reaction_fly_animation.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"core/ui_integration.h\"\n#include \"lang/lang_keys.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"history/view/media/history_view_media.h\"\n#include \"history/view/history_view_message.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"base/unixtime.h\"\n#include \"chat_helpers/emoji_interactions.h\"\n#include \"core/click_handler_types.h\"\n#include \"main/main_session.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_message_reactions.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace HistoryView {\nnamespace {\n\n[[nodiscard]] QString SchedulePeriodText(TimeId period) {\n\tstruct Entry {\n\t\tTimeId period = 0;\n\t\tQString text;\n\t};\n\tconst auto map = std::vector<Entry>{\n\t\t{ 60, u\"minutely\"_q },\n\t\t{ 300, u\"5-minutely\"_q },\n\t\t{ 24 * 60 * 60, tr::lng_repeated_daily(tr::now) },\n\t\t{ 7 * 24 * 60 * 60, tr::lng_repeated_weekly(tr::now) },\n\t\t{ 14 * 24 * 60 * 60, tr::lng_repeated_biweekly(tr::now) },\n\t\t{ 30 * 24 * 60 * 60, tr::lng_repeated_monthly(tr::now) },\n\t\t{\n\t\t\t91 * 24 * 60 * 60,\n\t\t\ttr::lng_repeated_every_month(tr::now, lt_count, 3)\n\t\t},\n\t\t{\n\t\t\t182 * 24 * 60 * 60,\n\t\t\ttr::lng_repeated_every_month(tr::now, lt_count, 6)\n\t\t},\n\t\t{ 365 * 24 * 60 * 60, tr::lng_repeated_yearly(tr::now) },\n\t};\n\tfor (const auto &entry : map) {\n\t\tif (entry.period >= period) {\n\t\t\treturn entry.text;\n\t\t}\n\t}\n\treturn map.back().text;\n}\n\n} // namespace\n\nstruct BottomInfo::Effect {\n\tmutable std::unique_ptr<Ui::ReactionFlyAnimation> animation;\n\tmutable QImage image;\n\tEffectId id = 0;\n};\n\nBottomInfo::BottomInfo(\n\tnot_null<::Data::Reactions*> reactionsOwner,\n\tData &&data)\n: _reactionsOwner(reactionsOwner)\n, _data(std::move(data)) {\n\tlayout();\n}\n\nBottomInfo::~BottomInfo() = default;\n\nvoid BottomInfo::update(Data &&data, int availableWidth) {\n\t_data = std::move(data);\n\tlayout();\n\tif (width() > 0) {\n\t\tresizeGetHeight(std::min(maxWidth(), availableWidth));\n\t}\n}\n\nint BottomInfo::countEffectMaxWidth() const {\n\tauto result = 0;\n\tif (_effect) {\n\t\tresult += st::reactionInfoSize;\n\t\tresult += st::reactionInfoBetween;\n\t}\n\tif (result) {\n\t\tresult += (st::reactionInfoSkip - st::reactionInfoBetween);\n\t}\n\treturn result;\n}\n\nint BottomInfo::countEffectHeight(int newWidth) const {\n\tconst auto left = 0;\n\tauto x = 0;\n\tauto y = 0;\n\tauto widthLeft = newWidth;\n\tif (_effect) {\n\t\tconst auto add = st::reactionInfoBetween;\n\t\tconst auto width = st::reactionInfoSize;\n\t\tif (x > left && widthLeft < width) {\n\t\t\tx = left;\n\t\t\ty += st::msgDateFont->height;\n\t\t\twidthLeft = newWidth;\n\t\t}\n\t\tx += width + add;\n\t\twidthLeft -= width + add;\n\t}\n\tif (x > left) {\n\t\ty += st::msgDateFont->height;\n\t}\n\treturn y;\n}\n\nint BottomInfo::firstLineWidth() const {\n\tif (height() == minHeight()) {\n\t\treturn width();\n\t}\n\treturn maxWidth() - _effectMaxWidth;\n}\n\nbool BottomInfo::isWide() const {\n\treturn (_data.flags & Data::Flag::Edited)\n\t\t|| _data.scheduleRepeatPeriod\n\t\t|| !_data.author.isEmpty()\n\t\t|| !_views.isEmpty()\n\t\t|| !_replies.isEmpty()\n\t\t|| _effect\n\t\t|| _data.tonStake;\n}\n\nTextState BottomInfo::textState(\n\t\tnot_null<const Message*> view,\n\t\tQPoint position) const {\n\tconst auto item = view->data();\n\tauto result = TextState(item);\n\tif (const auto link = replayEffectLink(view, position)) {\n\t\tresult.link = link;\n\t\treturn result;\n\t}\n\tconst auto textWidth = _authorEditedDate.maxWidth();\n\tauto withTicksWidth = textWidth;\n\tif (_data.flags & (Data::Flag::OutLayout | Data::Flag::Sending)) {\n\t\twithTicksWidth += st::historySendStateSpace;\n\t}\n\tif (!_views.isEmpty()) {\n\t\tconst auto viewsWidth = _views.maxWidth();\n\t\tconst auto right = width()\n\t\t\t- withTicksWidth\n\t\t\t- ((_data.flags & Data::Flag::Pinned) ? st::historyPinWidth : 0)\n\t\t\t- st::historyViewsSpace\n\t\t\t- st::historyViewsWidth\n\t\t\t- viewsWidth;\n\t\tconst auto inViews = QRect(\n\t\t\tright,\n\t\t\t0,\n\t\t\twithTicksWidth + st::historyViewsWidth,\n\t\t\tst::msgDateFont->height\n\t\t).contains(position);\n\t\tif (inViews) {\n\t\t\tresult.customTooltip = true;\n\t\t\tconst auto fullViews = tr::lng_views_tooltip(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\t*_data.views);\n\t\t\tconst auto fullForwards = _data.forwardsCount\n\t\t\t\t? ('\\n' + tr::lng_forwards_tooltip(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t*_data.forwardsCount))\n\t\t\t\t: QString();\n\t\t\tresult.customTooltipText = fullViews + fullForwards;\n\t\t}\n\t}\n\tconst auto inTime = QRect(\n\t\twidth() - withTicksWidth,\n\t\t0,\n\t\twithTicksWidth,\n\t\tst::msgDateFont->height\n\t).contains(position);\n\tif (inTime) {\n\t\tresult.cursor = CursorState::Date;\n\t}\n\treturn result;\n}\n\nClickHandlerPtr BottomInfo::replayEffectLink(\n\t\tnot_null<const Message*> view,\n\t\tQPoint position) const {\n\tif (!_effect) {\n\t\treturn nullptr;\n\t}\n\tauto left = 0;\n\tauto top = 0;\n\tauto available = width();\n\tif (height() != minHeight()) {\n\t\tavailable = std::min(available, _effectMaxWidth);\n\t\tleft += width() - available;\n\t\ttop += st::msgDateFont->height;\n\t}\n\tif (_effect) {\n\t\tconst auto image = QRect(\n\t\t\tleft,\n\t\t\ttop,\n\t\t\tst::reactionInfoSize,\n\t\t\tst::msgDateFont->height);\n\t\tif (image.contains(position)) {\n\t\t\tif (!_replayLink) {\n\t\t\t\t_replayLink = replayEffectLink(view);\n\t\t\t}\n\t\t\treturn _replayLink;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nClickHandlerPtr BottomInfo::replayEffectLink(\n\t\tnot_null<const Message*> view) const {\n\tconst auto weak = base::make_weak(view);\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif ([[maybe_unused]] const auto controller = my.sessionWindow.get()) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->delegate()->elementStartEffect(strong, nullptr);\n\t\t\t}\n\t\t}\n\t});\n}\n\nbool BottomInfo::isSignedAuthorElided() const {\n\treturn _authorElided;\n}\n\nvoid BottomInfo::paint(\n\t\tPainter &p,\n\t\tQPoint position,\n\t\tint outerWidth,\n\t\tbool unread,\n\t\tbool inverted,\n\t\tconst PaintContext &context) const {\n\tconst auto st = context.st;\n\tconst auto stm = context.messageStyle();\n\n\tauto right = position.x() + width();\n\tconst auto firstLineBottom = position.y() + st::msgDateFont->height;\n\tif (_data.flags & Data::Flag::OutLayout) {\n\t\tconst auto &icon = (_data.flags & Data::Flag::Sending)\n\t\t\t? (inverted\n\t\t\t\t? st->historySendingInvertedIcon()\n\t\t\t\t: st->historySendingIcon())\n\t\t\t: unread\n\t\t\t? (inverted\n\t\t\t\t? st->historySentInvertedIcon()\n\t\t\t\t: stm->historySentIcon)\n\t\t\t: (inverted\n\t\t\t\t? st->historyReceivedInvertedIcon()\n\t\t\t\t: stm->historyReceivedIcon);\n\t\ticon.paint(\n\t\t\tp,\n\t\t\tQPoint(right, firstLineBottom) + st::historySendStatePosition,\n\t\t\touterWidth);\n\t\tright -= st::historySendStateSpace;\n\t}\n\n\tconst auto authorEditedWidth = _authorEditedDate.maxWidth();\n\tright -= authorEditedWidth;\n\t_authorEditedDate.drawLeft(\n\t\tp,\n\t\tright,\n\t\tposition.y(),\n\t\tauthorEditedWidth,\n\t\touterWidth);\n\n\tif (_data.flags & Data::Flag::Pinned) {\n\t\tconst auto &icon = inverted\n\t\t\t? st->historyPinInvertedIcon()\n\t\t\t: stm->historyPinIcon;\n\t\tright -= st::historyPinWidth;\n\t\ticon.paint(\n\t\t\tp,\n\t\t\tright,\n\t\t\tfirstLineBottom + st::historyPinTop,\n\t\t\touterWidth);\n\t}\n\tif (!_views.isEmpty()) {\n\t\tconst auto viewsWidth = _views.maxWidth();\n\t\tright -= st::historyViewsSpace + viewsWidth;\n\t\t_views.drawLeft(p, right, position.y(), viewsWidth, outerWidth);\n\n\t\tconst auto &icon = inverted\n\t\t\t? st->historyViewsInvertedIcon()\n\t\t\t: stm->historyViewsIcon;\n\t\tright -= st::historyViewsWidth;\n\t\ticon.paint(\n\t\t\tp,\n\t\t\tright,\n\t\t\tfirstLineBottom + st::historyViewsTop,\n\t\t\touterWidth);\n\t}\n\tif (!_replies.isEmpty()) {\n\t\tconst auto repliesWidth = _replies.maxWidth();\n\t\tright -= st::historyViewsSpace + repliesWidth;\n\t\t_replies.drawLeft(p, right, position.y(), repliesWidth, outerWidth);\n\n\t\tconst auto &icon = inverted\n\t\t\t? st->historyRepliesInvertedIcon()\n\t\t\t: stm->historyRepliesIcon;\n\t\tright -= st::historyViewsWidth;\n\t\ticon.paint(\n\t\t\tp,\n\t\t\tright,\n\t\t\tfirstLineBottom + st::historyViewsTop,\n\t\t\touterWidth);\n\t}\n\tif ((_data.flags & Data::Flag::Sending)\n\t\t&& !(_data.flags & Data::Flag::OutLayout)) {\n\t\tright -= st::historySendStateSpace;\n\t\tconst auto &icon = inverted\n\t\t\t? st->historyViewsSendingInvertedIcon()\n\t\t\t: st->historyViewsSendingIcon();\n\t\ticon.paint(\n\t\t\tp,\n\t\t\tright,\n\t\t\tfirstLineBottom + st::historyViewsTop,\n\t\t\touterWidth);\n\t}\n\tif (_effect) {\n\t\tauto left = position.x();\n\t\tauto top = position.y();\n\t\tauto available = width();\n\t\tif (height() != minHeight()) {\n\t\t\tavailable = std::min(available, _effectMaxWidth);\n\t\t\tleft += width() - available;\n\t\t\ttop += st::msgDateFont->height;\n\t\t}\n\t\tpaintEffect(p, position, left, top, available, context);\n\t}\n}\n\nvoid BottomInfo::paintEffect(\n\t\tPainter &p,\n\t\tQPoint origin,\n\t\tint left,\n\t\tint top,\n\t\tint availableWidth,\n\t\tconst PaintContext &context) const {\n\tstruct SingleAnimation {\n\t\tnot_null<Ui::ReactionFlyAnimation*> animation;\n\t\tQRect target;\n\t};\n\tstd::vector<SingleAnimation> animations;\n\n\tauto x = left;\n\tauto y = top;\n\tauto widthLeft = availableWidth;\n\tif (_effect) {\n\t\tconst auto animating = (_effect->animation != nullptr);\n\t\tconst auto add = st::reactionInfoBetween;\n\t\tconst auto width = st::reactionInfoSize;\n\t\tif (x > left && widthLeft < width) {\n\t\t\tx = left;\n\t\t\ty += st::msgDateFont->height;\n\t\t\twidthLeft = availableWidth;\n\t\t}\n\t\tif (_effect->image.isNull()) {\n\t\t\t_effect->image = _reactionsOwner->resolveEffectImageFor(\n\t\t\t\t_effect->id);\n\t\t}\n\t\tconst auto image = QRect(\n\t\t\tx + (st::reactionInfoSize - st::effectInfoImage) / 2,\n\t\t\ty + (st::msgDateFont->height - st::effectInfoImage) / 2,\n\t\t\tst::effectInfoImage,\n\t\t\tst::effectInfoImage);\n\t\tif (!_effect->image.isNull()) {\n\t\t\tp.drawImage(image.topLeft(), _effect->image);\n\t\t}\n\t\tif (animating) {\n\t\t\tanimations.push_back({\n\t\t\t\t.animation = _effect->animation.get(),\n\t\t\t\t.target = image,\n\t\t\t});\n\t\t}\n\t\tx += width + add;\n\t\twidthLeft -= width + add;\n\t}\n\tif (!animations.empty()) {\n\t\tconst auto now = context.now;\n\t\tcontext.reactionInfo->effectPaint = [\n\t\t\tnow,\n\t\t\torigin,\n\t\t\tlist = std::move(animations)\n\t\t](QPainter &p) {\n\t\t\tauto result = QRect();\n\t\t\tfor (const auto &single : list) {\n\t\t\t\tconst auto area = single.animation->paintGetArea(\n\t\t\t\t\tp,\n\t\t\t\t\torigin,\n\t\t\t\t\tsingle.target,\n\t\t\t\t\tQColor(255, 255, 255, 0), // Colored, for emoji status.\n\t\t\t\t\tQRect(), // Clip, for emoji status.\n\t\t\t\t\tnow);\n\t\t\t\tresult = result.isEmpty() ? area : result.united(area);\n\t\t\t}\n\t\t\treturn result;\n\t\t};\n\t}\n}\n\nQSize BottomInfo::countCurrentSize(int newWidth) {\n\tif (newWidth >= maxWidth() || (_data.flags & Data::Flag::Shortcut)) {\n\t\treturn optimalSize();\n\t}\n\tconst auto dateHeight = (_data.flags & Data::Flag::Sponsored)\n\t\t? 0\n\t\t: st::msgDateFont->height;\n\tconst auto noReactionsWidth = maxWidth() - _effectMaxWidth;\n\taccumulate_min(newWidth, std::max(noReactionsWidth, _effectMaxWidth));\n\treturn QSize(\n\t\tnewWidth,\n\t\tdateHeight + countEffectHeight(newWidth));\n}\n\nvoid BottomInfo::layout() {\n\tlayoutDateText();\n\tlayoutViewsText();\n\tlayoutRepliesText();\n\tlayoutEffectText();\n\tinitDimensions();\n}\n\nvoid BottomInfo::layoutDateText() {\n\tconst auto edited = (_data.flags & Data::Flag::Edited)\n\t\t? (tr::lng_edited(tr::now) + ' ')\n\t\t: (_data.flags & Data::Flag::EstimateDate)\n\t\t? (tr::lng_approximate(tr::now) + ' ')\n\t\t: _data.scheduleRepeatPeriod\n\t\t? (SchedulePeriodText(_data.scheduleRepeatPeriod) + ' ')\n\t\t: QString();\n\tconst auto author = _data.author;\n\tconst auto prefix = !author.isEmpty() ? u\", \"_q : QString();\n\tconst auto date = edited + ((_data.flags & Data::Flag::ForwardedDate)\n\t\t? Ui::FormatDateTimeSavedFrom(_data.date)\n\t\t: QLocale().toString(_data.date.time(), QLocale::ShortFormat));\n\tconst auto afterAuthor = prefix + date;\n\tconst auto afterAuthorWidth = st::msgDateFont->width(afterAuthor);\n\tconst auto authorWidth = st::msgDateFont->width(author);\n\tconst auto maxWidth = st::maxSignatureSize;\n\t_authorElided = !author.isEmpty()\n\t\t&& (authorWidth + afterAuthorWidth > maxWidth);\n\tconst auto name = _authorElided\n\t\t? st::msgDateFont->elided(author, maxWidth - afterAuthorWidth)\n\t\t: author;\n\tconst auto full = (_data.flags & Data::Flag::Sponsored)\n\t\t? QString()\n\t\t: (_data.flags & Data::Flag::Imported)\n\t\t? (date + ' ' + tr::lng_imported(tr::now))\n\t\t: name.isEmpty()\n\t\t? date\n\t\t: (name + afterAuthor);\n\tauto helper = Ui::Text::CustomEmojiHelper(\n\t\tCore::TextContext({ .session = &_reactionsOwner->session() }));\n\tauto marked = TextWithEntities();\n\tif (const auto count = _data.stars) {\n\t\tmarked.append(\n\t\t\tUi::Text::IconEmoji(&st::starIconEmojiSmall)\n\t\t).append(Lang::FormatCountToShort(count).string).append(u\", \"_q);\n\t}\n\tif (const auto stake = _data.tonStake) {\n\t\tmarked.append(\n\t\t\tQString::number(stake / 1e9)\n\t\t).append(helper.image({\n\t\t\t.image = Ui::Emoji::SinglePixmap(\n\t\t\t\tUi::Emoji::Find(QString::fromUtf8(\"\\xf0\\x9f\\x92\\x8e\")),\n\t\t\t\tUi::Emoji::GetSizeNormal()).toImage().scaledToHeight(\n\t\t\t\t\tst::stakeIconEmojiSize * style::DevicePixelRatio(),\n\t\t\t\t\tQt::SmoothTransformation),\n\t\t\t.margin = QMargins(0, st::stakeIconEmojiTop, 0, 0),\n\t\t\t.textColor = false,\n\t\t})).append(\"  \");\n\t}\n\tmarked.append(full);\n\t_authorEditedDate.setMarkedText(\n\t\tst::msgDateTextStyle,\n\t\tmarked,\n\t\tUi::NameTextOptions(),\n\t\thelper.context());\n}\n\nvoid BottomInfo::layoutViewsText() {\n\tif (!_data.views || (_data.flags & Data::Flag::Sending)) {\n\t\t_views.clear();\n\t\treturn;\n\t}\n\t_views.setText(\n\t\tst::msgDateTextStyle,\n\t\tLang::FormatCountToShort(std::max(*_data.views, 1)).string,\n\t\tUi::NameTextOptions());\n}\n\nvoid BottomInfo::layoutRepliesText() {\n\tif (!_data.replies\n\t\t|| !*_data.replies\n\t\t|| (_data.flags & Data::Flag::RepliesContext)\n\t\t|| (_data.flags & Data::Flag::Sending)\n\t\t|| (_data.flags & Data::Flag::Shortcut)) {\n\t\t_replies.clear();\n\t\treturn;\n\t}\n\t_replies.setText(\n\t\tst::msgDateTextStyle,\n\t\tLang::FormatCountToShort(*_data.replies).string,\n\t\tUi::NameTextOptions());\n}\n\nvoid BottomInfo::layoutEffectText() {\n\tif (!_data.effectId) {\n\t\t_effect = nullptr;\n\t\treturn;\n\t}\n\t_effect = std::make_unique<Effect>(prepareEffectWithId(_data.effectId));\n}\n\nQSize BottomInfo::countOptimalSize() {\n\tif (_data.flags & Data::Flag::Shortcut) {\n\t\treturn { st::historyShortcutStateSpace, st::msgDateFont->height };\n\t}\n\tauto width = 0;\n\tif (_data.flags & (Data::Flag::OutLayout | Data::Flag::Sending)) {\n\t\twidth += st::historySendStateSpace;\n\t}\n\twidth += _authorEditedDate.maxWidth();\n\tif (!_views.isEmpty()) {\n\t\twidth += st::historyViewsSpace\n\t\t\t+ _views.maxWidth()\n\t\t\t+ st::historyViewsWidth;\n\t}\n\tif (!_replies.isEmpty()) {\n\t\twidth += st::historyViewsSpace\n\t\t\t+ _replies.maxWidth()\n\t\t\t+ st::historyViewsWidth;\n\t}\n\tif (_data.flags & Data::Flag::Pinned) {\n\t\twidth += st::historyPinWidth;\n\t}\n\t_effectMaxWidth = countEffectMaxWidth();\n\twidth += _effectMaxWidth;\n\tconst auto dateHeight = (_data.flags & Data::Flag::Sponsored)\n\t\t? 0\n\t\t: st::msgDateFont->height;\n\treturn QSize(width, dateHeight);\n}\n\nBottomInfo::Effect BottomInfo::prepareEffectWithId(EffectId id) {\n\tauto result = Effect{ .id = id };\n\t_reactionsOwner->preloadEffectImageFor(id);\n\treturn result;\n}\n\nauto BottomInfo::takeEffectAnimation()\n-> std::unique_ptr<Ui::ReactionFlyAnimation> {\n\treturn _effect ? std::move(_effect->animation) : nullptr;\n}\n\nvoid BottomInfo::continueEffectAnimation(\n\t\tstd::unique_ptr<Ui::ReactionFlyAnimation> animation) {\n\tif (_effect) {\n\t\t_effect->animation = std::move(animation);\n\t}\n}\n\nQRect BottomInfo::effectIconGeometry() const {\n\tif (!_effect) {\n\t\treturn {};\n\t}\n\tauto left = 0;\n\tauto top = 0;\n\tauto available = width();\n\tif (height() != minHeight()) {\n\t\tavailable = std::min(available, _effectMaxWidth);\n\t\tleft += width() - available;\n\t\ttop += st::msgDateFont->height;\n\t}\n\treturn QRect(\n\t\tleft + (st::reactionInfoSize - st::effectInfoImage) / 2,\n\t\ttop + (st::msgDateFont->height - st::effectInfoImage) / 2,\n\t\tst::effectInfoImage,\n\t\tst::effectInfoImage);\n}\n\nBottomInfo::Data BottomInfoDataFromMessage(not_null<Message*> message) {\n\tusing Flag = BottomInfo::Data::Flag;\n\tconst auto item = message->data();\n\n\tauto result = BottomInfo::Data();\n\tresult.date = message->dateTime();\n\tresult.effectId = item->effectId();\n\tif (message->hasOutLayout()) {\n\t\tresult.flags |= Flag::OutLayout;\n\t}\n\tif (message->context() == Context::Replies) {\n\t\tresult.flags |= Flag::RepliesContext;\n\t}\n\tif (item->isSponsored()) {\n\t\tresult.flags |= Flag::Sponsored;\n\t}\n\tif (item->isPinned() && message->context() != Context::Pinned) {\n\t\tresult.flags |= Flag::Pinned;\n\t}\n\tif (message->context() == Context::ShortcutMessages) {\n\t\tresult.flags |= Flag::Shortcut;\n\t}\n\tif (!item->isPost()\n\t\t|| !item->hasRealFromId()\n\t\t|| !item->history()->peer->asChannel()->signatureProfiles()) {\n\t\tif (const auto msgsigned = item->Get<HistoryMessageSigned>()) {\n\t\t\tif (!msgsigned->isAnonymousRank) {\n\t\t\t\tresult.author = msgsigned->author;\n\t\t\t}\n\t\t}\n\t}\n\tif (message->displayedEditDate()) {\n\t\tresult.flags |= Flag::Edited;\n\t}\n\tif (const auto views = item->Get<HistoryMessageViews>()) {\n\t\tif (views->views.count >= 0) {\n\t\t\tresult.views = views->views.count;\n\t\t}\n\t\tif (views->replies.count >= 0 && !views->commentsMegagroupId) {\n\t\t\tresult.replies = views->replies.count;\n\t\t}\n\t\tif (views->forwardsCount > 0) {\n\t\t\tresult.forwardsCount = views->forwardsCount;\n\t\t}\n\t}\n\tif (item->isSending() || item->hasFailed()) {\n\t\tresult.flags |= Flag::Sending;\n\t}\n\tif (!item->history()->peer->isUser()) {\n\t\tconst auto mine = PaidInformation{\n\t\t\t.messages = 1,\n\t\t\t.stars = item->starsPaid(),\n\t\t};\n\t\tconst auto media = message->media();\n\t\tauto info = media ? media->paidInformation().value_or(mine) : mine;\n\t\tif (const auto total = info.stars) {\n\t\t\tresult.stars = total;\n\t\t}\n\t}\n\tif (const auto media = item->media()) {\n\t\tif (const auto outcome = media->diceGameOutcome()) {\n\t\t\tresult.tonStake = outcome.stakeNanoTon;\n\t\t}\n\t}\n\tconst auto forwarded = item->Get<HistoryMessageForwarded>();\n\tif (forwarded && forwarded->imported) {\n\t\tresult.flags |= Flag::Imported;\n\t}\n\tif (item->awaitingVideoProcessing()) {\n\t\tresult.flags |= Flag::EstimateDate;\n\t}\n\tif (item->isScheduled()) {\n\t\tresult.scheduleRepeatPeriod = item->scheduleRepeatPeriod();\n\t}\n\tif (!forwarded) {\n\t\treturn result;\n\t}\n\tif (forwarded->savedFromMsgId && forwarded->savedFromDate) {\n\t\tresult.date = base::unixtime::parse(forwarded->savedFromDate);\n\t\tresult.flags |= Flag::ForwardedDate;\n\t} else if (forwarded->originalDate\n\t\t&& (message->context() == Context::SavedSublist\n\t\t\t|| item->history()->peer->isSelf())\n\t\t&& !item->externalReply()) {\n\t\tresult.date = base::unixtime::parse(forwarded->originalDate);\n\t\tresult.flags |= Flag::ForwardedDate;\n\t}\n\t// We don't want to pass and update it in Data for now.\n\t//if (item->unread()) {\n\t//\tresult.flags |= Flag::Unread;\n\t//}\n\treturn result;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_bottom_info.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/history_view_object.h\"\n#include \"ui/text/text.h\"\n#include \"base/flags.h\"\n\nnamespace Ui {\nstruct ChatPaintContext;\nclass AnimatedIcon;\nstruct ReactionFlyAnimationArgs;\nclass ReactionFlyAnimation;\n} // namespace Ui\n\nnamespace Data {\nclass Reactions;\n} // namespace Data\n\nnamespace HistoryView {\n\nusing PaintContext = Ui::ChatPaintContext;\n\nclass Message;\nstruct TextState;\n\nclass BottomInfo final : public Object {\npublic:\n\tstruct Data {\n\t\tenum class Flag : uint16 {\n\t\t\tEdited         = 0x001,\n\t\t\tOutLayout      = 0x002,\n\t\t\tSending        = 0x004,\n\t\t\tRepliesContext = 0x008,\n\t\t\tSponsored      = 0x010,\n\t\t\tPinned         = 0x020,\n\t\t\tImported       = 0x040,\n\t\t\tShortcut       = 0x080,\n\t\t\tEstimateDate   = 0x100,\n\t\t\tForwardedDate  = 0x200,\n\t\t\t//Unread, // We don't want to pass and update it in Date for now.\n\t\t};\n\t\tfriend inline constexpr bool is_flag_type(Flag) { return true; };\n\t\tusing Flags = base::flags<Flag>;\n\n\t\tQDateTime date;\n\t\tQString author;\n\t\tEffectId effectId = 0;\n\t\tint64 tonStake = 0;\n\t\tint stars = 0;\n\t\tTimeId scheduleRepeatPeriod = 0;\n\t\tstd::optional<int> views;\n\t\tstd::optional<int> replies;\n\t\tstd::optional<int> forwardsCount;\n\t\tFlags flags;\n\t};\n\tBottomInfo(not_null<::Data::Reactions*> reactionsOwner, Data &&data);\n\t~BottomInfo();\n\n\tvoid update(Data &&data, int availableWidth);\n\n\t[[nodiscard]] int firstLineWidth() const;\n\t[[nodiscard]] bool isWide() const;\n\t[[nodiscard]] TextState textState(\n\t\tnot_null<const Message*> view,\n\t\tQPoint position) const;\n\t[[nodiscard]] bool isSignedAuthorElided() const;\n\n\tvoid paint(\n\t\tPainter &p,\n\t\tQPoint position,\n\t\tint outerWidth,\n\t\tbool unread,\n\t\tbool inverted,\n\t\tconst PaintContext &context) const;\n\n\t[[nodiscard]] auto takeEffectAnimation()\n\t\t-> std::unique_ptr<Ui::ReactionFlyAnimation>;\n\tvoid continueEffectAnimation(\n\t\tstd::unique_ptr<Ui::ReactionFlyAnimation> animation);\n\n\tQRect effectIconGeometry() const;\n\nprivate:\n\tstruct Effect;\n\n\tvoid layout();\n\tvoid layoutDateText();\n\tvoid layoutViewsText();\n\tvoid layoutRepliesText();\n\tvoid layoutEffectText();\n\n\t[[nodiscard]] int countEffectMaxWidth() const;\n\t[[nodiscard]] int countEffectHeight(int newWidth) const;\n\tvoid paintEffect(\n\t\tPainter &p,\n\t\tQPoint origin,\n\t\tint left,\n\t\tint top,\n\t\tint availableWidth,\n\t\tconst PaintContext &context) const;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\n\t[[nodiscard]] Effect prepareEffectWithId(EffectId id);\n\t[[nodiscard]] ClickHandlerPtr replayEffectLink(\n\t\tnot_null<const Message*> view,\n\t\tQPoint position) const;\n\t[[nodiscard]] ClickHandlerPtr replayEffectLink(\n\t\tnot_null<const Message*> view) const;\n\n\tconst not_null<::Data::Reactions*> _reactionsOwner;\n\tData _data;\n\tUi::Text::String _authorEditedDate;\n\tUi::Text::String _views;\n\tUi::Text::String _replies;\n\tstd::unique_ptr<Effect> _effect;\n\tmutable ClickHandlerPtr _replayLink;\n\tint _effectMaxWidth = 0;\n\tbool _authorElided = false;\n\n};\n\n[[nodiscard]] BottomInfo::Data BottomInfoDataFromMessage(\n\tnot_null<Message*> message);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_chat_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_chat_preview.h\"\n\n#include \"base/unixtime.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_history_messages.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_replies_list.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_thread.h\"\n#include \"history/view/reactions/history_view_reactions_button.h\"\n#include \"history/view/history_view_corner_buttons.h\"\n#include \"history/view/history_view_list_widget.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"info/profile/info_profile_badge.h\"\n#include \"info/profile/info_profile_cover.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/widgets/menu/menu_item_base.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/elastic_scroll.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/unread_badge.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_settings.h\"\n\n#ifdef Q_OS_WIN\n#include \"ui/platform/win/ui_windows_direct_manipulation.h\"\n#endif // Q_OS_WIN\n\nnamespace HistoryView {\nnamespace {\n\nclass Item final\n\t: public Ui::Menu::ItemBase\n\t, private ListDelegate\n\t, private CornerButtonsDelegate {\npublic:\n\tItem(not_null<Ui::Menu::Menu*> parent, not_null<Data::Thread*> thread);\n\n\t[[nodiscard]] not_null<QAction*> action() const override;\n\t[[nodiscard]] bool isEnabled() const override;\n\n\t[[nodiscard]] rpl::producer<ChatPreviewAction> actions() {\n\t\treturn _actions.events();\n\t}\n\nprivate:\n\tint contentHeight() const override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid setupTop();\n\tvoid setupMarkRead();\n\tvoid setupBackground();\n\tvoid setupHistory();\n\tvoid updateInnerVisibleArea();\n\n\t// ListDelegate delegate.\n\tContext listContext() override;\n\tbool listScrollTo(int top, bool syntetic = true) override;\n\tvoid listCancelRequest() override;\n\tvoid listDeleteRequest() override;\n\tvoid listTryProcessKeyInput(not_null<QKeyEvent*> e) override;\n\trpl::producer<Data::MessagesSlice> listSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) override;\n\tbool listAllowsMultiSelect() override;\n\tbool listIsItemGoodForSelection(not_null<HistoryItem*> item) override;\n\tbool listIsLessInOrder(\n\t\tnot_null<HistoryItem*> first,\n\t\tnot_null<HistoryItem*> second) override;\n\tvoid listSelectionChanged(SelectedItems &&items) override;\n\tvoid listMarkReadTill(not_null<HistoryItem*> item) override;\n\tvoid listMarkContentsRead(\n\t\tconst base::flat_set<not_null<HistoryItem*>> &items) override;\n\tMessagesBarData listMessagesBar(\n\t\tconst std::vector<not_null<Element*>> &elements,\n\t\tbool markLastAsRead) override;\n\tvoid listContentRefreshed() override;\n\tvoid listUpdateDateLink(\n\t\tClickHandlerPtr &link,\n\t\tnot_null<Element*> view) override;\n\tbool listElementHideReply(not_null<const Element*> view) override;\n\tbool listElementShownUnread(not_null<const Element*> view) override;\n\tbool listIsGoodForAroundPosition(\n\t\tnot_null<const Element*> view) override;\n\tvoid listSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) override;\n\tvoid listSearch(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) override;\n\tvoid listHandleViaClick(not_null<UserData*> bot) override;\n\tnot_null<Ui::ChatTheme*> listChatTheme() override;\n\tCopyRestrictionType listCopyRestrictionType(\n\t\tHistoryItem *item) override;\n\tCopyRestrictionType listCopyRestrictionType() {\n\t\treturn listCopyRestrictionType(nullptr);\n\t}\n\tCopyRestrictionType listCopyMediaRestrictionType(\n\t\tnot_null<HistoryItem*> item) override;\n\tCopyRestrictionType listSelectRestrictionType() override;\n\tauto listAllowedReactionsValue()\n\t\t-> rpl::producer<Data::AllowedReactions> override;\n\tvoid listShowPremiumToast(not_null<DocumentData*> document) override;\n\tvoid listOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) override;\n\tvoid listOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView) override;\n\tvoid listPaintEmpty(\n\t\tPainter &p,\n\t\tconst Ui::ChatPaintContext &context) override;\n\tQString listElementAuthorRank(not_null<const Element*> view) override;\n\tbool listElementHideTopicButton(not_null<const Element*> view) override;\n\tHistory *listTranslateHistory() override;\n\tvoid listAddTranslatedItems(\n\t\tnot_null<TranslateTracker*> tracker) override;\n\tnot_null<Window::SessionController*> listWindow() override;\n\tnot_null<QWidget*> listEmojiInteractionsParent() override;\n\tnot_null<const Ui::ChatStyle*> listChatStyle() override;\n\trpl::producer<bool> listChatWideValue() override;\n\tstd::unique_ptr<Reactions::Manager> listMakeReactionsManager(\n\t\tQWidget *wheelEventsTarget,\n\t\tFn<void(QRect)> update) override;\n\tvoid listVisibleAreaUpdated() override;\n\tstd::shared_ptr<Ui::Show> listUiShow() override;\n\tvoid listShowPollResults(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context) override;\n\tvoid listCancelUploadLayer(not_null<HistoryItem*> item) override;\n\tbool listAnimationsPaused() override;\n\tauto listSendingAnimation()\n\t\t-> Ui::MessageSendingAnimationController* override;\n\tUi::ChatPaintContext listPreparePaintContext(\n\t\tUi::ChatPaintContextArgs &&args) override;\n\tbool listMarkingContentRead() override;\n\tbool listIgnorePaintEvent(QWidget *w, QPaintEvent *e) override;\n\tbool listShowReactPremiumError(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst Data::ReactionId &id) override;\n\tbase::unique_qptr<Ui::PopupMenu> listFillSenderUserpicMenu(\n\t\tPeerId userpicPeerId) override;\n\tvoid listWindowSetInnerFocus() override;\n\tbool listAllowsDragForward() override;\n\tvoid listLaunchDrag(\n\t\tstd::unique_ptr<QMimeData> data,\n\t\tFn<void()> finished) override;\n\n\t// CornerButtonsDelegate delegate.\n\tvoid cornerButtonsShowAtPosition(\n\t\tData::MessagePosition position) override;\n\tData::Thread *cornerButtonsThread() override;\n\tFullMsgId cornerButtonsCurrentId() override;\n\tbool cornerButtonsIgnoreVisibility() override;\n\tstd::optional<bool> cornerButtonsDownShown() override;\n\tbool cornerButtonsUnreadMayBeShown() override;\n\tbool cornerButtonsHas(CornerButtonType type) override;\n\n\tconst not_null<QAction*> _dummyAction;\n\tconst not_null<Main::Session*> _session;\n\tconst not_null<Data::Thread*> _thread;\n\tconst std::shared_ptr<Data::RepliesList> _replies;\n\tData::SavedSublist * const _sublist = nullptr;\n\tconst not_null<History*> _history;\n\tconst not_null<PeerData*> _peer;\n\tconst std::shared_ptr<Ui::ChatTheme> _theme;\n\tconst std::unique_ptr<Ui::ChatStyle> _chatStyle;\n\tconst std::unique_ptr<Ui::AbstractButton> _top;\n\tconst std::unique_ptr<Ui::ElasticScroll> _scroll;\n\tconst std::unique_ptr<Ui::FlatButton> _markRead;\n\n\tInfo::Profile::Badge _badge;\n\n\tQPointer<ListWidget> _inner;\n\tstd::unique_ptr<CornerButtons> _cornerButtons;\n\trpl::event_stream<ChatPreviewAction> _actions;\n\n\tQImage _bg;\n\n};\n\nstruct StatusFields {\n\tQString text;\n\tbool active = false;\n};\n\n[[nodiscard]] rpl::producer<StatusFields> StatusValue(\n\t\tnot_null<PeerData*> peer) {\n\tpeer->updateFull();\n\n\tusing UpdateFlag = Data::PeerUpdate::Flag;\n\treturn peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tUpdateFlag::OnlineStatus | UpdateFlag::Members\n\t) | rpl::map([=](const Data::PeerUpdate &update)\n\t-> StatusFields {\n\t\tconst auto wrap = [](QString text) {\n\t\t\treturn StatusFields{ .text = text };\n\t\t};\n\t\tif (const auto user = peer->asUser()) {\n\t\t\tconst auto now = base::unixtime::now();\n\t\t\treturn {\n\t\t\t\t.text = Data::OnlineText(user, now),\n\t\t\t\t.active = Data::OnlineTextActive(user, now),\n\t\t\t};\n\t\t} else if (const auto chat = peer->asChat()) {\n\t\t\treturn wrap(!chat->amIn()\n\t\t\t\t? tr::lng_chat_status_unaccessible(tr::now)\n\t\t\t\t: (chat->count <= 0)\n\t\t\t\t? tr::lng_group_status(tr::now)\n\t\t\t\t: tr::lng_chat_status_members(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\tchat->count));\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\treturn wrap((channel->membersCount() > 0)\n\t\t\t\t? ((channel->isMegagroup()\n\t\t\t\t\t? tr::lng_chat_status_members\n\t\t\t\t\t: tr::lng_chat_status_subscribers)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\tchannel->membersCount()))\n\t\t\t\t: (channel->isMegagroup()\n\t\t\t\t\t? tr::lng_group_status(tr::now)\n\t\t\t\t\t: tr::lng_channel_status(tr::now)));\n\t\t}\n\t\tUnexpected(\"Peer type in ChatPreview Item.\");\n\t});\n}\n\n[[nodiscard]] rpl::producer<Info::Profile::Badge::Content> ContentForPeer(\n\t\tnot_null<PeerData*> peer) {\n\tusing namespace Info::Profile;\n\tif (peer->isSelf()\n\t\t|| peer->isRepliesChat()\n\t\t|| peer->isSavedHiddenAuthor()) {\n\t\treturn rpl::single(Badge::Content{});\n\t}\n\treturn rpl::combine(\n\t\tBadgeContentForPeer(peer),\n\t\tVerifiedContentForPeer(peer)\n\t) | rpl::map([](Badge::Content &&content, Badge::Content &&verified) {\n\t\tif (verified.badge == BadgeType::Verified) {\n\t\t\tcontent.badge = BadgeType::Verified;\n\t\t}\n\t\treturn content;\n\t});\n}\n\nItem::Item(not_null<Ui::Menu::Menu*> parent, not_null<Data::Thread*> thread)\n: Ui::Menu::ItemBase(parent, st::previewMenu.menu)\n, _dummyAction(new QAction(parent))\n, _session(&thread->session())\n, _thread(thread)\n, _replies(thread->asTopic() ? thread->asTopic()->replies() : nullptr)\n, _sublist(thread->asSublist())\n, _history(thread->owningHistory())\n, _peer(thread->peer())\n, _theme(Window::Theme::DefaultChatThemeOn(lifetime()))\n, _chatStyle(std::make_unique<Ui::ChatStyle>(_session->colorIndicesValue()))\n, _top(std::make_unique<Ui::AbstractButton>(this))\n, _scroll(std::make_unique<Ui::ElasticScroll>(this))\n, _markRead(\n\tstd::make_unique<Ui::FlatButton>(\n\t\tthis,\n\t\ttr::lng_context_mark_read(tr::now),\n\t\tst::previewMarkRead))\n, _badge(\n\t\t_top.get(),\n\t\tst::settingsInfoPeerBadge,\n\t\t_session,\n\t\tContentForPeer(_peer),\n\t\tnullptr,\n\t\tnullptr,\n\t\t1) {\n\t_chatStyle->apply(_theme.get());\n\tsetPointerCursor(false);\n\tsetMinWidth(st::previewMenu.menu.widthMin);\n\tresize(minWidth(), contentHeight());\n\tsetupTop();\n\tsetupMarkRead();\n\tsetupBackground();\n\tsetupHistory();\n}\n\nnot_null<QAction*> Item::action() const {\n\treturn _dummyAction;\n}\n\nbool Item::isEnabled() const {\n\treturn false;\n}\n\nint Item::contentHeight() const {\n\treturn st::previewMenu.maxHeight;\n}\n\nvoid Item::setupTop() {\n\t_top->setGeometry(0, 0, width(), st::previewTop.height);\n\t_top->setClickedCallback([=] {\n\t\t_actions.fire({ .openInfo = true });\n\t});\n\t_top->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(_top.get());\n\t\tp.fillRect(clip, st::topBarBg);\n\t}, _top->lifetime());\n\n\tconst auto topic = _thread->asTopic();\n\tauto nameValue = (topic\n\t\t? Info::Profile::TitleValue(topic)\n\t\t: _thread->peer()->isSelf()\n\t\t? tr::lng_saved_messages()\n\t\t: Info::Profile::NameValue(_thread->peer())\n\t) | rpl::start_spawning(_top->lifetime());\n\tconst auto name = Ui::CreateChild<Ui::FlatLabel>(\n\t\t_top.get(),\n\t\trpl::duplicate(nameValue),\n\t\tst::previewName);\n\tname->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tauto statusFields = StatusValue(\n\t\t_thread->peer()\n\t) | rpl::start_spawning(lifetime());\n\tauto statusText = rpl::duplicate(\n\t\tstatusFields\n\t) | rpl::map([](StatusFields &&fields) {\n\t\treturn fields.text;\n\t});\n\tconst auto status = _thread->peer()->isSelf()\n\t\t? nullptr\n\t\t: Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t_top.get(),\n\t\t\t(topic\n\t\t\t\t? Info::Profile::NameValue(topic->peer())\n\t\t\t\t: std::move(statusText)),\n\t\t\tst::previewStatus);\n\tif (status) {\n\t\tstd::move(\n\t\t\tstatusFields\n\t\t) | rpl::on_next([=](const StatusFields &fields) {\n\t\t\tstatus->setTextColorOverride(fields.active\n\t\t\t\t? st::windowActiveTextFg->c\n\t\t\t\t: std::optional<QColor>());\n\t\t}, status->lifetime());\n\t\tstatus->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t}\n\tconst auto userpic = topic\n\t\t? nullptr\n\t\t: Ui::CreateChild<Ui::UserpicButton>(\n\t\t\t_top.get(),\n\t\t\t_thread->peer()->userpicPaintingPeer(),\n\t\t\tst::previewUserpic,\n\t\t\t_thread->peer()->userpicShape());\n\tif (userpic) {\n\t\tuserpic->showSavedMessagesOnSelf(true);\n\t\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t}\n\tconst auto icon = topic\n\t\t? Ui::CreateChild<Info::Profile::TopicIconButton>(\n\t\t\tthis,\n\t\t\ttopic,\n\t\t\t[=] { return false; })\n\t\t: nullptr;\n\tif (icon) {\n\t\ticon->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t}\n\n\tconst auto shadow = Ui::CreateChild<Ui::PlainShadow>(this);\n\trpl::combine(\n\t\t_top->widthValue(),\n\t\tstd::move(nameValue),\n\t\trpl::single(rpl::empty) | rpl::then(_badge.updated())\n\t) | rpl::on_next([=](int width, const auto &, const auto &) {\n\t\tconst auto &st = st::previewTop;\n\t\tname->resizeToNaturalWidth(width\n\t\t\t- st.namePosition.x()\n\t\t\t- st.photoPosition.x()\n\t\t\t- (_badge.widget() ? _badge.widget()->width() : 0));\n\t\tif (status) {\n\t\t\tname->move(st::previewTop.namePosition);\n\t\t} else {\n\t\t\tname->move(\n\t\t\t\tst::previewTop.namePosition.x(),\n\t\t\t\t(st::previewTop.height - name->height()) / 2);\n\t\t}\n\t\t_badge.move(\n\t\t\tname->x() + name->width() + st::normalFont->spacew,\n\t\t\tname->y(),\n\t\t\tname->y() + name->height());\n\t}, name->lifetime());\n\n\t_top->geometryValue() | rpl::on_next([=](QRect geometry) {\n\t\tconst auto &st = st::previewTop;\n\t\tif (status) {\n\t\t\tstatus->resizeToWidth(geometry.width()\n\t\t\t\t- st.statusPosition.x()\n\t\t\t\t- st.photoPosition.x());\n\t\t\tstatus->move(st.statusPosition);\n\t\t}\n\t\tshadow->setGeometry(\n\t\t\tgeometry.x(),\n\t\t\tgeometry.y() + geometry.height(),\n\t\t\tgeometry.width(),\n\t\t\tst::lineWidth);\n\t\tif (userpic) {\n\t\t\tuserpic->move(st.photoPosition);\n\t\t} else {\n\t\t\ticon->move(\n\t\t\t\tst.photoPosition.x() + (st.photoSize - icon->width()) / 2,\n\t\t\t\tst.photoPosition.y() + (st.photoSize - icon->height()) / 2);\n\t\t}\n\t}, shadow->lifetime());\n}\n\nvoid Item::setupMarkRead() {\n\t_markRead->resizeToWidth(width());\n\t_markRead->move(0, height() - _markRead->height());\n\n\trpl::single(\n\t\trpl::empty\n\t) | rpl::then(\n\t\t_thread->owner().chatsListFor(_thread)->unreadStateChanges(\n\t\t) | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\tconst auto state = _thread->chatListBadgesState();\n\t\tconst auto unread = (state.unreadCounter || state.unread);\n\t\tconst auto hidden = (_thread->asTopic() || _thread->asSublist())\n\t\t\t? (!unread)\n\t\t\t: (_thread->peer()->isForum()\n\t\t\t\t|| _thread->peer()->amMonoforumAdmin());\n\t\tif (hidden) {\n\t\t\t_markRead->hide();\n\t\t\treturn;\n\t\t}\n\t\t_markRead->setText(unread\n\t\t\t? tr::lng_context_mark_read(tr::now)\n\t\t\t: tr::lng_context_mark_unread(tr::now));\n\t\t_markRead->setClickedCallback([=] {\n\t\t\t_actions.fire({ .markRead = unread, .markUnread = !unread });\n\t\t});\n\t\t_markRead->show();\n\t}, _markRead->lifetime());\n\n\tconst auto shadow = Ui::CreateChild<Ui::PlainShadow>(this);\n\t_markRead->geometryValue() | rpl::on_next([=](QRect geometry) {\n\t\tshadow->setGeometry(\n\t\t\tgeometry.x(),\n\t\t\tgeometry.y() - st::lineWidth,\n\t\t\tgeometry.width(),\n\t\t\tst::lineWidth);\n\t}, shadow->lifetime());\n\tshadow->showOn(_markRead->shownValue());\n}\n\nvoid Item::setupBackground() {\n\tconst auto ratio = style::DevicePixelRatio();\n\t_bg = QImage(\n\t\tsize() * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\n\tconst auto paint = [=] {\n\t\tauto p = QPainter(&_bg);\n\t\tWindow::SectionWidget::PaintBackground(\n\t\t\tp,\n\t\t\t_theme.get(),\n\t\t\tQSize(width(), height() * 2),\n\t\t\tQRect(QPoint(), size()));\n\t};\n\tpaint();\n\t_theme->repaintBackgroundRequests() | rpl::on_next([=] {\n\t\tpaint();\n\t\tupdate();\n\t}, lifetime());\n}\n\nvoid Item::setupHistory() {\n\t_inner = _scroll->setOwnedWidget(object_ptr<ListWidget>(\n\t\tthis,\n\t\t_session,\n\t\tstatic_cast<ListDelegate*>(this)));\n\t_cornerButtons = std::make_unique<CornerButtons>(\n\t\t_scroll.get(),\n\t\t_chatStyle.get(),\n\t\tstatic_cast<CornerButtonsDelegate*>(this));\n\n\t_markRead->shownValue() | rpl::on_next([=](bool shown) {\n\t\tconst auto top = _top->height();\n\t\tconst auto bottom = shown ? _markRead->height() : 0;\n\t\t_scroll->setGeometry(rect().marginsRemoved({ 0, top, 0, bottom }));\n\t\t_cornerButtons->updatePositions();\n\t}, _markRead->lifetime());\n\n\t_scroll->scrolls(\n\t) | rpl::on_next([=] {\n\t\tupdateInnerVisibleArea();\n\t}, lifetime());\n\t_scroll->setOverscrollBg(QColor(0, 0, 0, 0));\n\tusing Type = Ui::ElasticScroll::OverscrollType;\n\t_scroll->setOverscrollTypes(Type::Real, Type::Real);\n\n\t_inner->scrollKeyEvents(\n\t) | rpl::on_next([=](not_null<QKeyEvent*> e) {\n\t\t_scroll->keyPressEvent(e);\n\t}, lifetime());\n\n\t_scroll->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::MouseButtonDblClick) {\n\t\t\tconst auto button = static_cast<QMouseEvent*>(e.get())->button();\n\t\t\tif (button == Qt::LeftButton) {\n\t\t\t\tconst auto relative = Ui::MapFrom(\n\t\t\t\t\t_inner.data(),\n\t\t\t\t\t_scroll.get(),\n\t\t\t\t\tstatic_cast<QMouseEvent*>(e.get())->pos());\n\t\t\t\tif (const auto view = _inner->lookupItemByY(relative.y())) {\n\t\t\t\t\t_actions.fire(ChatPreviewAction{\n\t\t\t\t\t\t.openItemId = view->data()->fullId(),\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\t_actions.fire(ChatPreviewAction{});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\t_inner->resizeToWidth(_scroll->width(), _scroll->height());\n\n\t_inner->refreshViewer();\n\n\t_inner->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tcrl::on_main(this, [=] {\n\t\t_inner->setFocus();\n\t});\n}\n\nvoid Item::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tp.drawImage(0, 0, _bg);\n}\n\nvoid Item::updateInnerVisibleArea() {\n\tconst auto scrollTop = _scroll->scrollTop();\n\t_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());\n\t_cornerButtons->updateJumpDownVisibility();\n}\n\nContext Item::listContext() {\n\treturn Context::ChatPreview;\n}\n\nbool Item::listScrollTo(int top, bool syntetic) {\n\ttop = std::clamp(top, 0, _scroll->scrollTopMax());\n\tif (_scroll->scrollTop() == top) {\n\t\tupdateInnerVisibleArea();\n\t\treturn false;\n\t}\n\t_scroll->scrollToY(top);\n\treturn true;\n}\n\nvoid Item::listCancelRequest() {\n\t_actions.fire({ .cancel = true });\n}\n\nvoid Item::listDeleteRequest() {\n}\n\nvoid Item::listTryProcessKeyInput(not_null<QKeyEvent*> e) {\n}\n\nrpl::producer<Data::MessagesSlice> Item::listSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\treturn _replies\n\t\t? _replies->source(aroundId, limitBefore, limitAfter)\n\t\t: _sublist\n\t\t? _sublist->source(aroundId, limitBefore, limitAfter)\n\t\t: Data::HistoryMessagesViewer(\n\t\t\t_thread->asHistory(),\n\t\t\taroundId,\n\t\t\tlimitBefore,\n\t\t\tlimitAfter);\n}\n\nbool Item::listAllowsMultiSelect() {\n\treturn false;\n}\n\nbool Item::listIsItemGoodForSelection(not_null<HistoryItem*> item) {\n\treturn false;\n}\n\nbool Item::listIsLessInOrder(\n\t\tnot_null<HistoryItem*> first,\n\t\tnot_null<HistoryItem*> second) {\n\tif (first->isRegular() && second->isRegular()) {\n\t\tconst auto firstPeer = first->history()->peer;\n\t\tconst auto secondPeer = second->history()->peer;\n\t\tif (firstPeer == secondPeer) {\n\t\t\treturn first->id < second->id;\n\t\t} else if (firstPeer->isChat()) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t} else if (first->isRegular()) {\n\t\treturn true;\n\t} else if (second->isRegular()) {\n\t\treturn false;\n\t}\n\treturn first->id < second->id;\n}\n\nvoid Item::listSelectionChanged(SelectedItems &&items) {\n}\n\nvoid Item::listMarkReadTill(not_null<HistoryItem*> item) {\n}\n\nvoid Item::listMarkContentsRead(\n\tconst base::flat_set<not_null<HistoryItem*>> &items) {\n}\n\nMessagesBarData Item::listMessagesBar(\n\t\tconst std::vector<not_null<Element*>> &elements,\n\t\tbool markLastAsRead) {\n\tif (elements.empty()) {\n\t\treturn {};\n\t} else if (!_replies && !_sublist && !_history->unreadCount()) {\n\t\treturn {};\n\t}\n\tconst auto repliesTill = _replies\n\t\t? _replies->computeInboxReadTillFull()\n\t\t: MsgId();\n\tconst auto sublistTill = _sublist\n\t\t? _sublist->computeInboxReadTillFull()\n\t\t: MsgId();\n\tconst auto migrated = (_replies || _sublist)\n\t\t? nullptr\n\t\t: _history->migrateFrom();\n\tconst auto migratedTill = (migrated && migrated->unreadCount() > 0)\n\t\t? migrated->inboxReadTillId()\n\t\t: 0;\n\tconst auto historyTill = (_replies\n\t\t|| _sublist\n\t\t|| !_history->unreadCount()\n\t\t|| _history->amMonoforumAdmin())\n\t\t? 0\n\t\t: _history->inboxReadTillId();\n\tif (!_replies && !_sublist && !migratedTill && !historyTill) {\n\t\treturn {};\n\t}\n\n\tauto skipped = false;\n\tconst auto hidden = (_replies && (repliesTill < 2))\n\t\t|| (_sublist && (sublistTill < 2));\n\tfor (auto i = 0, count = int(elements.size()); i != count; ++i) {\n\t\tconst auto item = elements[i]->data();\n\t\tif (!item->isRegular()\n\t\t\t|| (_replies && !item->replyToId())\n\t\t\t|| (_sublist && !item->sublistPeerId())) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto inHistory = (item->history() == _history);\n\t\tconst auto unread = (_replies && item->id > repliesTill)\n\t\t\t|| (_sublist && item->id > sublistTill)\n\t\t\t|| (migratedTill && (inHistory || item->id > migratedTill))\n\t\t\t|| (historyTill && inHistory && item->id > historyTill);\n\t\tif (!unread) {\n\t\t\tskipped = true;\n\t\t}\n\t\tif (item->out()) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (unread) {\n\t\t\tif (!skipped) {\n\t\t\t\t// Don't show jumping unread bar if scrolling up from bottom.\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\t.bar = {\n\t\t\t\t\t.element = elements[i],\n\t\t\t\t\t.hidden = hidden,\n\t\t\t\t\t.focus = true,\n\t\t\t\t},\n\t\t\t\t.text = tr::lng_unread_bar_some(),\n\t\t\t};\n\t\t}\n\t}\n\treturn {};\n}\n\nvoid Item::listContentRefreshed() {\n}\n\nvoid Item::listUpdateDateLink(\n\tClickHandlerPtr &link,\n\tnot_null<Element*> view) {\n}\n\nbool Item::listElementHideReply(not_null<const Element*> view) {\n\tif (!view->isTopicRootReply()) {\n\t\treturn false;\n\t}\n\tconst auto reply = view->data()->Get<HistoryMessageReply>();\n\treturn reply && !reply->fields().manualQuote;\n}\n\nbool Item::listElementShownUnread(not_null<const Element*> view) {\n\treturn view->data()->unread(view->data()->history());\n}\n\nbool Item::listIsGoodForAroundPosition(not_null<const Element*> view) {\n\treturn view->data()->isRegular();\n}\n\nvoid Item::listSendBotCommand(\n\tconst QString &command,\n\tconst FullMsgId &context) {\n}\n\nvoid Item::listSearch(\n\tconst QString &query,\n\tconst FullMsgId &context) {\n}\n\nvoid Item::listHandleViaClick(not_null<UserData*> bot) {\n}\n\nnot_null<Ui::ChatTheme*> Item::listChatTheme() {\n\treturn _theme.get();\n}\n\nCopyRestrictionType Item::listCopyRestrictionType(HistoryItem *item) {\n\treturn CopyRestrictionType::None;\n}\n\nCopyRestrictionType Item::listCopyMediaRestrictionType(\n\t\tnot_null<HistoryItem*> item) {\n\treturn CopyRestrictionType::None;\n}\n\nCopyRestrictionType Item::listSelectRestrictionType() {\n\treturn CopyRestrictionType::None;\n}\n\nauto Item::listAllowedReactionsValue()\n-> rpl::producer<Data::AllowedReactions> {\n\treturn rpl::single(Data::AllowedReactions());\n}\n\nvoid Item::listShowPremiumToast(not_null<DocumentData*> document) {\n}\n\nvoid Item::listOpenPhoto(\n\tnot_null<PhotoData*> photo,\n\tFullMsgId context) {\n}\n\nvoid Item::listOpenDocument(\n\tnot_null<DocumentData*> document,\n\tFullMsgId context,\n\tbool showInMediaView) {\n}\n\nvoid Item::listPaintEmpty(\n\tPainter &p,\n\tconst Ui::ChatPaintContext &context) {\n\t// #TODO\n}\n\nQString Item::listElementAuthorRank(not_null<const Element*> view) {\n\treturn {};\n}\n\nbool Item::listElementHideTopicButton(not_null<const Element*> view) {\n\treturn _thread->asTopic() != nullptr;\n}\n\nHistory *Item::listTranslateHistory() {\n\treturn nullptr;\n}\n\nvoid Item::listAddTranslatedItems(\n\tnot_null<TranslateTracker*> tracker) {\n}\n\nnot_null<Window::SessionController*> Item::listWindow() {\n\tUnexpected(\"Item::listWindow.\");\n}\n\nnot_null<QWidget*> Item::listEmojiInteractionsParent() {\n\treturn this;\n}\n\nnot_null<const Ui::ChatStyle*> Item::listChatStyle() {\n\treturn _chatStyle.get();\n}\n\nrpl::producer<bool> Item::listChatWideValue() {\n\treturn rpl::single(false);\n}\n\nstd::unique_ptr<Reactions::Manager> Item::listMakeReactionsManager(\n\t\tQWidget *wheelEventsTarget,\n\t\tFn<void(QRect)> update) {\n\treturn nullptr;\n}\n\nvoid Item::listVisibleAreaUpdated() {\n}\n\nstd::shared_ptr<Ui::Show> Item::listUiShow() {\n\tUnexpected(\"Item::listUiShow.\");\n}\n\nvoid Item::listShowPollResults(\n\tnot_null<PollData*> poll,\n\tFullMsgId context) {\n}\n\nvoid Item::listCancelUploadLayer(not_null<HistoryItem*> item) {\n}\n\nbool Item::listAnimationsPaused() {\n\treturn false;\n}\n\nauto Item::listSendingAnimation()\n-> Ui::MessageSendingAnimationController* {\n\treturn nullptr;\n}\n\nUi::ChatPaintContext Item::listPreparePaintContext(\n\t\tUi::ChatPaintContextArgs &&args) {\n\tconst auto visibleAreaTopLocal = mapFromGlobal(\n\t\targs.visibleAreaPositionGlobal).y();\n\tconst auto area = QRect(\n\t\t0,\n\t\targs.visibleAreaTop,\n\t\targs.visibleAreaWidth,\n\t\targs.visibleAreaHeight);\n\tconst auto viewport = QRect(\n\t\t0,\n\t\targs.visibleAreaTop - visibleAreaTopLocal,\n\t\targs.visibleAreaWidth,\n\t\theight());\n\treturn args.theme->preparePaintContext(\n\t\t_chatStyle.get(),\n\t\tviewport,\n\t\tarea,\n\t\targs.clip,\n\t\tfalse);\n}\n\nbool Item::listMarkingContentRead() {\n\treturn false;\n}\n\nbool Item::listIgnorePaintEvent(QWidget *w, QPaintEvent *e) {\n\treturn false;\n}\n\nbool Item::listShowReactPremiumError(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst Data::ReactionId &id) {\n\treturn false;\n}\n\nbase::unique_qptr<Ui::PopupMenu> Item::listFillSenderUserpicMenu(\n\t\tPeerId userpicPeerId) {\n\treturn nullptr;\n}\n\nvoid Item::listWindowSetInnerFocus() {\n}\n\nbool Item::listAllowsDragForward() {\n\treturn false;\n}\n\nvoid Item::listLaunchDrag(\n\tstd::unique_ptr<QMimeData> data,\n\tFn<void()> finished) {\n}\n\nvoid Item::cornerButtonsShowAtPosition(Data::MessagePosition position) {\n\tif (position == Data::UnreadMessagePosition) {\n\t\tposition = Data::MaxMessagePosition;\n\t}\n\t_inner->showAtPosition(\n\t\tposition,\n\t\t{},\n\t\t_cornerButtons->doneJumpFrom(position.fullId, {}, true));\n}\n\nData::Thread *Item::cornerButtonsThread() {\n\treturn _thread;\n}\n\nFullMsgId Item::cornerButtonsCurrentId() {\n\treturn {};\n}\n\nbool Item::cornerButtonsIgnoreVisibility() {\n\treturn false;\n}\n\nstd::optional<bool> Item::cornerButtonsDownShown() {\n\tconst auto top = _scroll->scrollTop() + st::historyToDownShownAfter;\n\tif (top < _scroll->scrollTopMax()) {\n\t\treturn true;\n\t} else if (_inner->loadedAtBottomKnown()) {\n\t\treturn !_inner->loadedAtBottom();\n\t}\n\treturn std::nullopt;\n}\n\nbool Item::cornerButtonsUnreadMayBeShown() {\n\treturn _inner->loadedAtBottomKnown();\n}\n\nbool Item::cornerButtonsHas(CornerButtonType type) {\n\treturn (type == CornerButtonType::Down);\n}\n\n} // namespace\n\nChatPreview MakeChatPreview(\n\t\tQWidget *parent,\n\t\tnot_null<Dialogs::Entry*> entry) {\n\tconst auto thread = entry->asThread();\n\tif (!thread) {\n\t\treturn {};\n\t}\n\n\tauto result = ChatPreview{\n\t\t.menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tparent,\n\t\t\tst::previewMenu),\n\t};\n\tconst auto menu = result.menu.get();\n\n\tauto action = base::make_unique_q<Item>(menu->menu(), thread);\n\tresult.actions = action->actions();\n\tmenu->addAction(std::move(action));\n\tif (const auto topic = thread->asTopic()) {\n\t\tconst auto weak = base::make_weak(menu);\n\t\ttopic->destroyed() | rpl::on_next([weak] {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tLOG((\"Preview hidden for a destroyed topic.\"));\n\t\t\t\tstrong->hideMenu(true);\n\t\t\t}\n\t\t}, menu->lifetime());\n\t}\n\n#ifdef Q_OS_WIN\n\tUi::Platform::ActivateDirectManipulation(menu);\n#endif\n\n\treturn result;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_chat_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n\nnamespace Dialogs {\nclass Entry;\n} // namespace Dialogs\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nnamespace HistoryView {\n\nstruct ChatPreviewAction {\n\tFullMsgId openItemId;\n\tbool cancel = false;\n\tbool openInfo = false;\n\tbool markRead = false;\n\tbool markUnread = false;\n};\n\nstruct ChatPreview {\n\tbase::unique_qptr<Ui::PopupMenu> menu;\n\trpl::producer<ChatPreviewAction> actions;\n};\n\n[[nodiscard]] ChatPreview MakeChatPreview(\n\tQWidget *parent,\n\tnot_null<Dialogs::Entry*> entry);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_chat_section.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_chat_section.h\"\n\n#include \"history/view/controls/history_view_compose_controls.h\"\n#include \"history/view/controls/history_view_compose_search.h\"\n#include \"history/view/controls/history_view_draft_options.h\"\n#include \"history/view/history_view_top_bar_widget.h\"\n#include \"history/view/history_view_schedule_box.h\"\n#include \"history/view/history_view_sticker_toast.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_contact_status.h\"\n#include \"history/view/history_view_scheduled_section.h\"\n#include \"history/view/history_view_service_message.h\"\n#include \"history/view/history_view_subsection_tabs.h\"\n#include \"history/view/history_view_pinned_tracker.h\"\n#include \"history/view/history_view_pinned_section.h\"\n#include \"history/view/history_view_translate_bar.h\"\n#include \"history/view/history_view_translate_tracker.h\"\n#include \"history/view/history_view_self_forwards_tagger.h\"\n#include \"history/view/history_view_draw_to_reply.h\"\n#include \"history/history.h\"\n#include \"history/history_drag_area.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\" // GetErrorForSending.\n#include \"ui/chat/pinned_bar.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/controls/swipe_handler.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/effects/message_sending_animation_controller.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"base/timer_rpl.h\"\n#include \"api/api_bot.h\"\n#include \"api/api_chat_participants.h\"\n#include \"api/api_editing.h\"\n#include \"api/api_sending.h\"\n#include \"apiwrap.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"boxes/delete_messages_box.h\"\n#include \"boxes/send_files_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_peer_menu.h\"\n#include \"base/call_delayed.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"core/application.h\"\n#include \"core/shortcuts.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/mime_type.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"menu/menu_timecode_action.h\"\n#include \"data/components/scheduled_messages.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_replies_list.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_shared_media.h\"\n#include \"data/data_send_action.h\"\n#include \"data/data_premium_limits.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/localimageloader.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n\n#include <QtCore/QMimeData>\n\nnamespace HistoryView {\nnamespace {\n\nrpl::producer<Ui::MessageBarContent> RootViewContent(\n\t\tnot_null<History*> history,\n\t\tMsgId rootId,\n\t\tFn<void()> repaint) {\n\treturn MessageBarContentByItemId(\n\t\t&history->session(),\n\t\tFullMsgId(history->peer->id, rootId),\n\t\tstd::move(repaint)\n\t) | rpl::map([=](Ui::MessageBarContent &&content) {\n\t\tconst auto item = history->owner().message(history->peer, rootId);\n\t\tif (!item) {\n\t\t\tcontent.text = tr::link(tr::lng_deleted_message(tr::now));\n\t\t}\n\t\tconst auto sender = (item && item->discussionPostOriginalSender())\n\t\t\t? item->discussionPostOriginalSender()\n\t\t\t: history->peer.get();\n\t\tcontent.title = sender->name().isEmpty()\n\t\t\t? \"Message\"\n\t\t\t: sender->name();\n\t\treturn std::move(content);\n\t});\n}\n\n} // namespace\n\nChatMemento::ChatMemento(\n\tChatViewId id,\n\tMsgId highlightId,\n\tMessageHighlightId highlight)\n: _id(id)\n, _highlightId(highlightId)\n, _highlight(std::move(highlight)) {\n\tif (highlightId || _id.sublist) {\n\t\t_list.setAroundPosition({\n\t\t\t.fullId = FullMsgId(_id.history->peer->id, highlightId),\n\t\t\t.date = TimeId(0),\n\t\t});\n\t}\n}\n\nChatMemento::ChatMemento(\n\tComments,\n\tnot_null<HistoryItem*> commentsItem,\n\tMsgId commentId)\n: ChatMemento({\n\t.history = commentsItem->history(),\n\t.repliesRootId = commentsItem->id,\n}, commentId) {\n}\n\nvoid ChatMemento::setFromTopic(not_null<Data::ForumTopic*> topic) {\n\t_replies = topic->replies();\n\tif (!_list.aroundPosition()) {\n\t\t_list = *topic->listMemento();\n\t}\n}\n\n\nData::ForumTopic *ChatMemento::topicForRemoveRequests() const {\n\treturn _id.repliesRootId\n\t\t? _id.history->peer->forumTopicFor(_id.repliesRootId)\n\t\t: nullptr;\n}\n\nData::SavedSublist *ChatMemento::sublistForRemoveRequests() const {\n\treturn _id.sublist;\n}\n\nvoid ChatMemento::setReadInformation(\n\t\tMsgId inboxReadTillId,\n\t\tint unreadCount,\n\t\tMsgId outboxReadTillId) {\n\tif (!_id.repliesRootId) {\n\t\treturn;\n\t} else if (!_replies) {\n\t\tif (const auto forum = _id.history->asForum()) {\n\t\t\tif (const auto topic = forum->topicFor(_id.repliesRootId)) {\n\t\t\t\t_replies = topic->replies();\n\t\t\t}\n\t\t}\n\t\tif (!_replies) {\n\t\t\t_replies = std::make_shared<Data::RepliesList>(\n\t\t\t\t_id.history,\n\t\t\t\t_id.repliesRootId);\n\t\t}\n\t}\n\t_replies->setInboxReadTill(inboxReadTillId, unreadCount);\n\t_replies->setOutboxReadTill(outboxReadTillId);\n}\n\nobject_ptr<Window::SectionWidget> ChatMemento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tWindow::Column column,\n\t\tconst QRect &geometry) {\n\tif (column == Window::Column::Third) {\n\t\treturn nullptr;\n\t}\n\tif (!_list.aroundPosition().fullId\n\t\t&& _replies\n\t\t&& _replies->computeInboxReadTillFull() == MsgId(1)) {\n\t\t_list.setAroundPosition(Data::MinMessagePosition);\n\t\t_list.setScrollTopState(ListMemento::ScrollTopState{\n\t\t\tData::MinMessagePosition\n\t\t});\n\t} else if (!_list.aroundPosition().fullId\n\t\t&& _id.sublist\n\t\t&& _id.sublist->computeInboxReadTillFull() == MsgId(1)) {\n\t\t_list.setAroundPosition(Data::MinMessagePosition);\n\t\t_list.setScrollTopState(ListMemento::ScrollTopState{\n\t\t\tData::MinMessagePosition\n\t\t});\n\t}\n\tauto result = object_ptr<ChatWidget>(parent, controller, _id);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nvoid ChatMemento::setupTopicViewer() {\n\tif (_id.repliesRootId) {\n\t\t_id.history->owner().itemIdChanged(\n\t\t) | rpl::on_next([=](const Data::Session::IdChange &change) {\n\t\t\tif (_id.repliesRootId == change.oldId) {\n\t\t\t\t_id.repliesRootId = change.newId.msg;\n\t\t\t\t_replies = nullptr;\n\t\t\t}\n\t\t}, _lifetime);\n\t}\n}\n\nChatWidget::ChatWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tChatViewId id)\n: Window::SectionWidget(parent, controller, id.history->peer)\n, WindowListDelegate(controller)\n, _history(id.history)\n, _peer(_history->peer)\n, _id(id)\n, _repliesRootId(_id.repliesRootId)\n, _repliesRoot(lookupRepliesRoot())\n, _topic(lookupTopic())\n, _areComments(computeAreComments())\n, _sublist(_id.sublist)\n, _monoforumPeerId((_sublist && _sublist->parentChat())\n\t? _sublist->sublistPeer()->id\n\t: PeerId())\n, _sendAction(_repliesRootId\n\t? _history->owner().sendActionManager().repliesPainter(\n\t\t_history,\n\t\t_repliesRootId)\n\t: nullptr)\n, _topBar(this, controller)\n, _topBarShadow(this)\n, _topBars(std::make_unique<Ui::RpWidget>(this))\n, _composeControls(std::make_unique<ComposeControls>(\n\tthis,\n\tComposeControlsDescriptor{\n\t\t.show = controller->uiShow(),\n\t\t.unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) {\n\t\t\tlistShowPremiumToast(emoji);\n\t\t},\n\t\t.mode = ComposeControls::Mode::Normal,\n\t\t.sendMenuDetails = [=] { return sendMenuDetails(); },\n\t\t.regularWindow = controller,\n\t\t.stickerOrEmojiChosen = controller->stickerOrEmojiChosen(),\n\t\t.scheduledToggleValue = _topic\n\t\t\t? rpl::single(rpl::empty_value()) | rpl::then(\n\t\t\t\tsession().scheduledMessages().updates(_topic->owningHistory())\n\t\t\t) | rpl::map([=] {\n\t\t\t\treturn session().scheduledMessages().hasFor(_topic);\n\t\t\t}) | rpl::type_erased\n\t\t\t: rpl::single(false),\n\t}))\n, _translateBar(\n\tstd::make_unique<TranslateBar>(_topBars.get(), controller, _history))\n, _scroll(std::make_unique<Ui::ScrollArea>(\n\tthis,\n\tcontroller->chatStyle()->value(lifetime(), st::historyScroll),\n\tfalse))\n, _cornerButtons(\n\t\t_scroll.get(),\n\t\tcontroller->chatStyle(),\n\t\tstatic_cast<HistoryView::CornerButtonsDelegate*>(this)) {\n\tcontroller->chatStyle()->paletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_scroll->updateBars();\n\t}, _scroll->lifetime());\n\n\tWindow::ChatThemeValueFromPeer(\n\t\tcontroller,\n\t\t_peer\n\t) | rpl::on_next([=](std::shared_ptr<Ui::ChatTheme> &&theme) {\n\t\t_theme = std::move(theme);\n\t\tcontroller->setChatStyleTheme(_theme);\n\t}, lifetime());\n\n\tsetupRoot();\n\tsetupRootView();\n\tsetupOpenChatButton();\n\tsetupAboutHiddenAuthor();\n\tsetupShortcuts();\n\tsetupTranslateBar();\n\n\t_peer->updateFull();\n\tif (const auto channel = _peer->asMegagroup()) {\n\t\tif (!channel->mgInfo->adminsLoaded) {\n\t\t\tsession().api().chatParticipants().requestAdmins(channel);\n\t\t}\n\t}\n\n\trefreshTopBarActiveChat();\n\n\t_topBar->move(0, 0);\n\t_topBar->resizeToWidth(width());\n\t_topBar->show();\n\n\tif (_repliesRootView) {\n\t\t_repliesRootView->move(0, 0);\n\t}\n\n\t_topBar->deleteSelectionRequest(\n\t) | rpl::on_next([=] {\n\t\tconfirmDeleteSelected();\n\t}, _topBar->lifetime());\n\t_topBar->forwardSelectionRequest(\n\t) | rpl::on_next([=] {\n\t\tconfirmForwardSelected();\n\t}, _topBar->lifetime());\n\t_topBar->clearSelectionRequest(\n\t) | rpl::on_next([=] {\n\t\tclearSelected();\n\t}, _topBar->lifetime());\n\t_topBar->searchRequest(\n\t) | rpl::on_next([=] {\n\t\tsearchRequested();\n\t}, _topBar->lifetime());\n\tif (_sublist) {\n\t\t_topBar->setCustomTitle(tr::lng_contacts_loading(tr::now));\n\t}\n\n\tcontroller->adaptive().value(\n\t) | rpl::on_next([=] {\n\t\tupdateAdaptiveLayout();\n\t}, lifetime());\n\n\t_inner = _scroll->setOwnedWidget(object_ptr<ListWidget>(\n\t\tthis,\n\t\t&controller->session(),\n\t\tstatic_cast<ListDelegate*>(this)));\n\t_scroll->move(0, _topBar->height());\n\t_scroll->show();\n\t_scroll->scrolls(\n\t) | rpl::on_next([=] {\n\t\tonScroll();\n\t}, lifetime());\n\n\t_inner->editMessageRequested(\n\t) | rpl::filter([=] {\n\t\treturn !_joinGroup;\n\t}) | rpl::on_next([=](auto fullId) {\n\t\tif (const auto item = session().data().message(fullId)) {\n\t\t\tconst auto media = item->media();\n\t\t\tif (!media || media->webpage() || media->allowsEditCaption()) {\n\t\t\t\t_composeControls->editMessage(\n\t\t\t\t\tfullId,\n\t\t\t\t\t_inner->getSelectedTextRange(item));\n\t\t\t} else if (media->todolist()) {\n\t\t\t\tWindow::PeerMenuEditTodoList(controller, item);\n\t\t\t}\n\t\t}\n\t}, _inner->lifetime());\n\n\t_inner->replyToMessageRequested(\n\t) | rpl::on_next([=](ListWidget::ReplyToMessageRequest request) {\n\t\tconst auto canSendReply = _topic\n\t\t\t? Data::CanSendAnything(_topic)\n\t\t\t: Data::CanSendAnything(_peer);\n\t\tconst auto &to = request.to;\n\t\tconst auto still = _history->owner().message(to.messageId);\n\t\tconst auto allowInAnotherChat = still && still->allowsForward();\n\t\tif (allowInAnotherChat\n\t\t\t&& (_joinGroup || !canSendReply || request.forceAnotherChat)) {\n\t\t\tControls::ShowReplyToChatBox(controller->uiShow(), { to });\n\t\t} else if (!_joinGroup && canSendReply) {\n\t\t\treplyToMessage(to);\n\t\t\t_composeControls->focus();\n\t\t\tif (_composeSearch) {\n\t\t\t\t_composeSearch->hideAnimated();\n\t\t\t}\n\t\t}\n\t}, _inner->lifetime());\n\n\t_inner->showMessageRequested(\n\t) | rpl::on_next([=](auto fullId) {\n\t\tif (const auto item = session().data().message(fullId)) {\n\t\t\tshowAtPosition(item->position());\n\t\t}\n\t}, _inner->lifetime());\n\n\t_inner->setInsertTextCallback([=](const QString &text) {\n\t\tif (const auto field = _composeControls->fieldForMention()) {\n\t\t\tMenu::InsertTextAtCursor(field, text);\n\t\t}\n\t});\n\n\t_composeControls->sendActionUpdates(\n\t) | rpl::on_next([=](ComposeControls::SendActionUpdate &&data) {\n\t\tif (!_repliesRootId) {\n\t\t\treturn;\n\t\t} else if (!data.cancel) {\n\t\t\tsession().sendProgressManager().update(\n\t\t\t\t_history,\n\t\t\t\t_repliesRootId,\n\t\t\t\tdata.type,\n\t\t\t\tdata.progress);\n\t\t} else {\n\t\t\tsession().sendProgressManager().cancel(\n\t\t\t\t_history,\n\t\t\t\t_repliesRootId,\n\t\t\t\tdata.type);\n\t\t}\n\t}, lifetime());\n\n\t_history->session().changes().messageUpdates(\n\t\tData::MessageUpdate::Flag::Destroyed\n\t) | rpl::on_next([=](const Data::MessageUpdate &update) {\n\t\tif (update.item == _repliesRoot) {\n\t\t\t_repliesRoot = nullptr;\n\t\t\tupdatePinnedVisibility();\n\t\t\tif (!_topic) {\n\t\t\t\tcontroller->showBackFromStack();\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\tif (_sublist) {\n\t\tsubscribeToSublist();\n\t} else if (!_topic) {\n\t\t_history->session().changes().historyUpdates(\n\t\t\t_history,\n\t\t\tData::HistoryUpdate::Flag::OutboxRead\n\t\t) | rpl::on_next([=] {\n\t\t\t_inner->update();\n\t\t}, lifetime());\n\t} else {\n\t\tsession().api().sendActions(\n\t\t) | rpl::filter([=](const Api::SendAction &action) {\n\t\t\treturn (action.history == _history)\n\t\t\t\t&& (action.replyTo.topicRootId == _topic->topicRootId());\n\t\t}) | rpl::on_next([=](const Api::SendAction &action) {\n\t\t\tif (action.options.scheduled) {\n\t\t\t\t_composeControls->cancelReplyMessage();\n\t\t\t\tcrl::on_main(this, [=, t = _topic] {\n\t\t\t\t\tcontroller->showSection(\n\t\t\t\t\t\tstd::make_shared<HistoryView::ScheduledMemento>(t));\n\t\t\t\t});\n\t\t\t}\n\t\t}, lifetime());\n\t}\n\n\t_selfForwardsTagger = std::make_unique<HistoryView::SelfForwardsTagger>(\n\t\tcontroller,\n\t\tthis,\n\t\t[=] { return _inner.data(); },\n\t\t_scroll.get(),\n\t\t[=] { return _history; });\n\n\tsetupTopicViewer();\n\tsetupComposeControls();\n\tsetupSwipeReplyAndBack();\n\torderWidgets();\n\n\tif (_pinnedBar) {\n\t\t_pinnedBar->finishAnimating();\n\t}\n}\n\nChatWidget::~ChatWidget() {\n\tbase::take(_sendAction);\n\tif (_repliesRootId) {\n\t\tcontroller()->sendingAnimation().clear();\n\t}\n\tif (_subsectionTabs && !_subsectionTabs->dying()) {\n\t\t_subsectionTabsLifetime.destroy();\n\t\tcontroller()->saveSubsectionTabs(base::take(_subsectionTabs));\n\t}\n\tif (_topic) {\n\t\tif (_topic->creating()) {\n\t\t\tif (controller()->activeChatCurrent().topic() == _topic) {\n\t\t\t\tcontroller()->setActiveChatEntry(Dialogs::Key());\n\t\t\t}\n\t\t\t_emptyPainter = nullptr;\n\t\t\t_topic->discard();\n\t\t\t_topic = nullptr;\n\t\t} else {\n\t\t\t_inner->saveState(_topic->listMemento());\n\t\t}\n\t}\n\tif (_repliesRootId) {\n\t\t_history->owner().sendActionManager().repliesPainterRemoved(\n\t\t\t_history,\n\t\t\t_repliesRootId);\n\t}\n}\n\nvoid ChatWidget::orderWidgets() {\n\t_topBars->raise();\n\t_translateBar->raise();\n\tif (_topicReopenBar) {\n\t\t_topicReopenBar->bar().raise();\n\t}\n\tif (_repliesRootView) {\n\t\t_repliesRootView->raise();\n\t}\n\tif (_pinnedBar) {\n\t\t_pinnedBar->raise();\n\t}\n\tif (_subsectionTabs) {\n\t\t_subsectionTabs->raise();\n\t}\n\t_topBar->raise();\n\t_topBarShadow->raise();\n\t_composeControls->raisePanels();\n}\n\nvoid ChatWidget::setupRoot() {\n\tif (_repliesRootId && !_repliesRoot) {\n\t\tconst auto done = crl::guard(this, [=] {\n\t\t\t_repliesRoot = lookupRepliesRoot();\n\t\t\tif (_repliesRoot) {\n\t\t\t\t_areComments = computeAreComments();\n\t\t\t\t_inner->update();\n\t\t\t}\n\t\t\tupdatePinnedVisibility();\n\t\t});\n\t\t_history->session().api().requestMessageData(\n\t\t\t_peer,\n\t\t\t_repliesRootId,\n\t\t\tdone);\n\t}\n}\n\nvoid ChatWidget::setupRootView() {\n\tif (_topic || !_repliesRootId) {\n\t\treturn;\n\t}\n\t_repliesRootView = std::make_unique<Ui::PinnedBar>(_topBars.get(), [=] {\n\t\treturn controller()->isGifPausedAtLeastFor(\n\t\t\tWindow::GifPauseReason::Any);\n\t}, controller()->gifPauseLevelChanged());\n\t_repliesRootView->setContent(rpl::combine(\n\t\tRootViewContent(\n\t\t\t_history,\n\t\t\t_repliesRootId,\n\t\t\t[bar = _repliesRootView.get()] { bar->customEmojiRepaint(); }),\n\t\t_repliesRootVisible.value()\n\t) | rpl::map([=](Ui::MessageBarContent &&content, bool show) {\n\t\tconst auto shown = !content.title.isEmpty() && !content.text.empty();\n\t\t_shownPinnedItem = shown\n\t\t\t? _history->owner().message(_peer->id, _repliesRootId)\n\t\t\t: nullptr;\n\t\treturn show ? std::move(content) : Ui::MessageBarContent();\n\t}));\n\n\tcontroller()->adaptive().oneColumnValue(\n\t) | rpl::on_next([=](bool one) {\n\t\t_repliesRootView->setShadowGeometryPostprocess([=](QRect geometry) {\n\t\t\tif (!one) {\n\t\t\t\tgeometry.setLeft(geometry.left() + st::lineWidth);\n\t\t\t}\n\t\t\treturn geometry;\n\t\t});\n\t}, _repliesRootView->lifetime());\n\n\t_repliesRootView->barClicks(\n\t) | rpl::on_next([=] {\n\t\tshowAtStart();\n\t}, lifetime());\n\n\t_repliesRootViewHeight = 0;\n\t_repliesRootView->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tif (const auto delta = height - _repliesRootViewHeight) {\n\t\t\t_repliesRootViewHeight = height;\n\t\t\tsetGeometryWithTopMoved(geometry(), delta);\n\t\t}\n\t}, _repliesRootView->lifetime());\n}\n\nvoid ChatWidget::setupTopicViewer() {\n\tif (!_repliesRootId) {\n\t\treturn;\n\t}\n\tconst auto owner = &_history->owner();\n\towner->itemIdChanged(\n\t) | rpl::on_next([=](const Data::Session::IdChange &change) {\n\t\tif (_repliesRootId == change.oldId) {\n\t\t\t_repliesRootId = _id.repliesRootId = change.newId.msg;\n\t\t\t_composeControls->updateTopicRootId(_repliesRootId);\n\t\t\t_sendAction = owner->sendActionManager().repliesPainter(\n\t\t\t\t_history,\n\t\t\t\t_repliesRootId);\n\t\t\t_repliesRoot = lookupRepliesRoot();\n\t\t\tif (_topic && _topic->rootId() == change.oldId) {\n\t\t\t\tsetTopic(_topic->forum()->topicFor(change.newId.msg));\n\t\t\t} else {\n\t\t\t\trefreshReplies();\n\t\t\t\trefreshTopBarActiveChat();\n\t\t\t\tif (_topic) {\n\t\t\t\t\tsubscribeToPinnedMessages();\n\t\t\t\t}\n\t\t\t}\n\t\t\t_inner->update();\n\t\t}\n\t}, lifetime());\n\n\tif (_topic) {\n\t\tsubscribeToTopic();\n\t}\n}\n\nvoid ChatWidget::subscribeToTopic() {\n\tExpects(_topic != nullptr);\n\n\t_topicReopenBar = std::make_unique<TopicReopenBar>(\n\t\t_topBars.get(),\n\t\t_topic);\n\t_topicReopenBar->bar().setVisible(!animatingShow());\n\t_topicReopenBarHeight = _topicReopenBar->bar().height();\n\t_topicReopenBar->bar().heightValue(\n\t) | rpl::on_next([=] {\n\t\tconst auto height = _topicReopenBar->bar().height();\n\t\t_scrollTopDelta = (height - _topicReopenBarHeight);\n\t\tif (_scrollTopDelta) {\n\t\t\t_topicReopenBarHeight = height;\n\t\t\tupdateControlsGeometry();\n\t\t\t_scrollTopDelta = 0;\n\t\t}\n\t}, _topicReopenBar->bar().lifetime());\n\n\tusing Flag = Data::TopicUpdate::Flag;\n\tsession().changes().topicUpdates(\n\t\t_topic,\n\t\t(Flag::UnreadMentions\n\t\t\t| Flag::UnreadReactions\n\t\t\t| Flag::UnreadPollVotes\n\t\t\t| Flag::CloudDraft)\n\t) | rpl::on_next([=](const Data::TopicUpdate &update) {\n\t\tif (update.flags\n\t\t\t& (Flag::UnreadMentions\n\t\t\t\t| Flag::UnreadReactions\n\t\t\t\t| Flag::UnreadPollVotes)) {\n\t\t\t_cornerButtons.updateUnreadThingsVisibility();\n\t\t}\n\t\tif (update.flags & Flag::CloudDraft) {\n\t\t\t_composeControls->applyCloudDraft();\n\t\t}\n\t}, _topicLifetime);\n\n\t_topic->destroyed(\n\t) | rpl::on_next([=] {\n\t\tcloseCurrent();\n\t}, _topicLifetime);\n\n\tif (!_topic->creating()) {\n\t\tsubscribeToPinnedMessages();\n\n\t\tif (!_topic->creatorId()) {\n\t\t\t_topic->forum()->requestTopic(_topic->rootId());\n\t\t}\n\t}\n\n\t_cornerButtons.updateUnreadThingsVisibility();\n}\n\nvoid ChatWidget::closeCurrent() {\n\tconst auto thread = controller()->windowId().chat();\n\tif ((_sublist && thread == _sublist) || (_topic && thread == _topic)) {\n\t\tcontroller()->window().close();\n\t} else {\n\t\tcontroller()->showBackFromStack(Window::SectionShow(\n\t\t\tanim::type::normal,\n\t\t\tanim::activation::background));\n\t}\n}\n\nvoid ChatWidget::subscribeToPinnedMessages() {\n\tusing EntryUpdateFlag = Data::EntryUpdate::Flag;\n\tsession().changes().entryUpdates(\n\t\tEntryUpdateFlag::HasPinnedMessages\n\t) | rpl::on_next([=](const Data::EntryUpdate &update) {\n\t\tif (_pinnedTracker\n\t\t\t&& (update.flags & EntryUpdateFlag::HasPinnedMessages)\n\t\t\t&& (_topic == update.entry.get()\n\t\t\t\t|| _sublist == update.entry.get())) {\n\t\t\tcheckPinnedBarState();\n\t\t}\n\t}, lifetime());\n\n\tsetupPinnedTracker();\n}\n\nvoid ChatWidget::setTopic(Data::ForumTopic *topic) {\n\tif (_topic == topic) {\n\t\treturn;\n\t}\n\t_topicLifetime.destroy();\n\t_topic = topic;\n\trefreshReplies();\n\trefreshTopBarActiveChat();\n\tvalidateSubsectionTabs();\n\tif (_topic) {\n\t\tif (_repliesRootView) {\n\t\t\t_shownPinnedItem = nullptr;\n\t\t\t_repliesRootView = nullptr;\n\t\t\t_repliesRootViewHeight = 0;\n\t\t}\n\t\tsubscribeToTopic();\n\t}\n\tif (_topic && emptyShown()) {\n\t\tsetupEmptyPainter();\n\t} else {\n\t\t_emptyPainter = nullptr;\n\t}\n}\n\nHistoryItem *ChatWidget::lookupRepliesRoot() const {\n\treturn _repliesRootId\n\t\t? _history->owner().message(_peer, _repliesRootId)\n\t\t: nullptr;\n}\n\nData::ForumTopic *ChatWidget::lookupTopic() {\n\tif (!_repliesRootId) {\n\t\treturn nullptr;\n\t} else if (const auto forum = _history->asForum()) {\n\t\tif (const auto result = forum->topicFor(_repliesRootId)) {\n\t\t\treturn result;\n\t\t} else {\n\t\t\tforum->requestTopic(_repliesRootId, crl::guard(this, [=] {\n\t\t\t\tif (const auto forum = _history->asForum()) {\n\t\t\t\t\tsetTopic(forum->topicFor(_repliesRootId));\n\t\t\t\t}\n\t\t\t}));\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nbool ChatWidget::computeAreComments() const {\n\treturn _repliesRoot && _repliesRoot->isDiscussionPost();\n}\n\nvoid ChatWidget::setupComposeControls() {\n\tauto topicWriteRestrictions = rpl::single(\n\t) | rpl::then(session().changes().topicUpdates(\n\t\tData::TopicUpdate::Flag::Closed\n\t) | rpl::filter([=](const Data::TopicUpdate &update) {\n\t\treturn (update.topic->history() == _history)\n\t\t\t&& (update.topic->rootId() == _repliesRootId);\n\t}) | rpl::to_empty) | rpl::map([=] {\n\t\tconst auto topic = _topic\n\t\t\t? _topic\n\t\t\t: _peer->forumTopicFor(_repliesRootId);\n\t\treturn (!topic || topic->canToggleClosed() || !topic->closed())\n\t\t\t? Data::SendError()\n\t\t\t: tr::lng_forum_topic_closed(tr::now);\n\t});\n\tauto writeRestriction = rpl::combine(\n\t\tsession().frozenValue(),\n\t\tsession().changes().peerFlagsValue(\n\t\t\t_peer,\n\t\t\tData::PeerUpdate::Flag::Rights),\n\t\tData::CanSendAnythingValue(_peer),\n\t\t(_repliesRootId\n\t\t\t? std::move(topicWriteRestrictions)\n\t\t\t: (rpl::single(Data::SendError()) | rpl::type_erased))\n\t) | rpl::map([=](\n\t\t\tconst Main::FreezeInfo &info,\n\t\t\tauto,\n\t\t\tauto,\n\t\t\tData::SendError topicRestriction) {\n\t\tif (info) {\n\t\t\treturn Controls::WriteRestriction{\n\t\t\t\t.type = Controls::WriteRestrictionType::Frozen,\n\t\t\t};\n\t\t}\n\t\tconst auto allWithoutPolls = Data::AllSendRestrictions()\n\t\t\t& ~ChatRestriction::SendPolls;\n\t\tconst auto canSendAnything = _topic\n\t\t\t? Data::CanSendAnyOf(_topic, allWithoutPolls)\n\t\t\t: Data::CanSendAnyOf(_peer, allWithoutPolls);\n\t\tconst auto restriction = Data::RestrictionError(\n\t\t\t_peer,\n\t\t\tChatRestriction::SendOther);\n\t\tauto text = !canSendAnything\n\t\t\t? (restriction\n\t\t\t\t? restriction\n\t\t\t\t: topicRestriction\n\t\t\t\t? std::move(topicRestriction)\n\t\t\t\t: tr::lng_group_not_accessible(tr::now))\n\t\t\t: topicRestriction\n\t\t\t? std::move(topicRestriction)\n\t\t\t: Data::SendError();\n\t\treturn text ? Controls::WriteRestriction{\n\t\t\t.text = std::move(*text),\n\t\t\t.type = Controls::WriteRestrictionType::Rights,\n\t\t\t.boostsToLift = text.boostsToLift,\n\t\t} : Controls::WriteRestriction();\n\t});\n\n\t_composeControls->setHistory({\n\t\t.history = _history.get(),\n\t\t.topicRootId = _topic ? _topic->rootId() : MsgId(),\n\t\t.monoforumPeerId = _monoforumPeerId,\n\t\t.showSlowmodeError = [=] { return showSlowmodeError(); },\n\t\t.sendActionFactory = [=] { return prepareSendAction({}); },\n\t\t.sendWithText = [=](\n\t\t\t\tTextWithEntities &&text,\n\t\t\t\tApi::SendOptions options,\n\t\t\t\tFn<void()> done) {\n\t\t\tsendWithTextOverride(std::move(text), options, std::move(done));\n\t\t},\n\t\t.slowmodeSecondsLeft = SlowmodeSecondsLeft(_peer),\n\t\t.sendDisabledBySlowmode = SendDisabledBySlowmode(_peer),\n\t\t.writeRestriction = std::move(writeRestriction),\n\t});\n\n\t_composeControls->height(\n\t) | rpl::filter([=] {\n\t\treturn !_joinGroup;\n\t}) | rpl::on_next([=] {\n\t\tconst auto wasMax = (_scroll->scrollTopMax() == _scroll->scrollTop());\n\t\tupdateControlsGeometry();\n\t\tif (wasMax) {\n\t\t\tlistScrollTo(_scroll->scrollTopMax());\n\t\t}\n\t}, lifetime());\n\n\t_composeControls->cancelRequests(\n\t) | rpl::on_next([=] {\n\t\tlistCancelRequest();\n\t}, lifetime());\n\n\t_composeControls->sendRequests(\n\t) | rpl::on_next([=](Api::SendOptions options) {\n\t\tsend(options);\n\t}, lifetime());\n\n\t_composeControls->scrollToMaxRequests(\n\t) | rpl::on_next([=] {\n\t\tlistScrollTo(_scroll->scrollTopMax());\n\t}, lifetime());\n\n\t_composeControls->sendVoiceRequests(\n\t) | rpl::on_next([=](const ComposeControls::VoiceToSend &data) {\n\t\tsendVoice(data);\n\t}, lifetime());\n\n\t_composeControls->sendCommandRequests(\n\t) | rpl::on_next([=](const QString &command) {\n\t\tif (showSlowmodeError()) {\n\t\t\treturn;\n\t\t}\n\t\tlistSendBotCommand(command, FullMsgId());\n\t\tsession().api().finishForwarding(prepareSendAction({}));\n\t}, lifetime());\n\n\tconst auto saveEditMsgRequestId = lifetime().make_state<mtpRequestId>(0);\n\t_composeControls->editRequests(\n\t) | rpl::on_next([=](auto data) {\n\t\tif (const auto item = session().data().message(data.fullId)) {\n\t\t\tconst auto spoiler = data.spoilered;\n\t\t\tedit(item, data.options, saveEditMsgRequestId, spoiler);\n\t\t}\n\t}, lifetime());\n\n\t_composeControls->attachRequests(\n\t) | rpl::filter([=] {\n\t\treturn !_choosingAttach;\n\t}) | rpl::on_next([=](std::optional<bool> overrideCompress) {\n\t\t_choosingAttach = true;\n\t\tbase::call_delayed(\n\t\t\tst::historyAttach.ripple.hideDuration,\n\t\t\tthis,\n\t\t\t[=] { chooseAttach(overrideCompress); });\n\t}, lifetime());\n\n\t_composeControls->setSendAsFileConfirmed(crl::guard(this, [=](\n\t\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\t\tApi::SendOptions options) {\n\t\tsendingFilesConfirmed(std::move(bundle), options);\n\t}));\n\n\t_composeControls->fileChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\tcontroller()->hideLayer(anim::type::normal);\n\t\tcontroller()->sendingAnimation().appendSending(\n\t\t\tdata.messageSendingFrom);\n\t\tauto messageToSend = Api::MessageToSend(\n\t\t\tprepareSendAction(data.options));\n\t\tmessageToSend.textWithTags = base::take(data.caption);\n\t\tsendExistingDocument(\n\t\t\tdata.document,\n\t\t\tstd::move(messageToSend),\n\t\t\tdata.messageSendingFrom.localId);\n\t}, lifetime());\n\n\t_composeControls->photoChosen(\n\t) | rpl::on_next([=](ChatHelpers::PhotoChosen chosen) {\n\t\tsendExistingPhoto(chosen.photo, chosen.options);\n\t}, lifetime());\n\n\t_composeControls->inlineResultChosen(\n\t) | rpl::on_next([=](ChatHelpers::InlineChosen chosen) {\n\t\tcontroller()->sendingAnimation().appendSending(\n\t\t\tchosen.messageSendingFrom);\n\t\tconst auto localId = chosen.messageSendingFrom.localId;\n\t\tsendInlineResult(chosen.result, chosen.bot, chosen.options, localId);\n\t}, lifetime());\n\n\t_composeControls->jumpToItemRequests(\n\t) | rpl::on_next([=](FullReplyTo to) {\n\t\tif (const auto item = session().data().message(to.messageId)) {\n\t\t\tJumpToMessageClickHandler(item, {}, to.highlight())->onClick({});\n\t\t}\n\t}, lifetime());\n\n\trpl::merge(\n\t\t_composeControls->scrollKeyEvents(),\n\t\t_inner->scrollKeyEvents()\n\t) | rpl::on_next([=](not_null<QKeyEvent*> e) {\n\t\t_scroll->keyPressEvent(e);\n\t}, lifetime());\n\n\t_composeControls->editLastMessageRequests(\n\t) | rpl::on_next([=](not_null<QKeyEvent*> e) {\n\t\tif (!_inner->lastMessageEditRequestNotify()) {\n\t\t\t_scroll->keyPressEvent(e);\n\t\t}\n\t}, lifetime());\n\n\t_composeControls->replyNextRequests(\n\t) | rpl::on_next([=](ComposeControls::ReplyNextRequest &&data) {\n\t\tusing Direction = ComposeControls::ReplyNextRequest::Direction;\n\t\t_inner->replyNextMessage(\n\t\t\tdata.replyId,\n\t\t\tdata.direction == Direction::Next);\n\t}, lifetime());\n\n\t_composeControls->showScheduledRequests(\n\t) | rpl::on_next([=] {\n\t\tcontroller()->showSection(\n\t\t\t_topic\n\t\t\t\t? std::make_shared<HistoryView::ScheduledMemento>(_topic)\n\t\t\t\t: std::make_shared<HistoryView::ScheduledMemento>(_history));\n\t}, lifetime());\n\n\t_composeControls->setMimeDataHook([=](\n\t\t\tnot_null<const QMimeData*> data,\n\t\t\tUi::InputField::MimeAction action) {\n\t\tif (action == Ui::InputField::MimeAction::Check) {\n\t\t\treturn Core::CanSendFiles(data);\n\t\t} else if (action == Ui::InputField::MimeAction::Insert) {\n\t\t\treturn confirmSendingFiles(\n\t\t\t\tdata,\n\t\t\t\tstd::nullopt,\n\t\t\t\tCore::ReadMimeText(data));\n\t\t}\n\t\tUnexpected(\"action in MimeData hook.\");\n\t});\n\n\t_composeControls->lockShowStarts(\n\t) | rpl::on_next([=] {\n\t\t_cornerButtons.updateJumpDownVisibility();\n\t\t_cornerButtons.updateUnreadThingsVisibility();\n\t}, lifetime());\n\n\t_composeControls->viewportEvents(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\t_scroll->viewportEvent(e);\n\t}, lifetime());\n\n\t_composeControls->finishAnimating();\n\n\tif (const auto channel = _peer->asChannel()) {\n\t\tchannel->updateFull();\n\t\tif (!channel->isBroadcast()) {\n\t\t\trpl::combine(\n\t\t\t\tData::CanSendAnythingValue(channel),\n\t\t\t\tchannel->flagsValue()\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\trefreshJoinGroupButton();\n\t\t\t}, lifetime());\n\t\t} else {\n\t\t\trefreshJoinGroupButton();\n\t\t}\n\t}\n}\n\nvoid ChatWidget::setupSwipeReplyAndBack() {\n\tconst auto can = [=](not_null<HistoryItem*> still) {\n\t\tconst auto canSendReply = _topic\n\t\t\t? Data::CanSendAnything(_topic)\n\t\t\t: Data::CanSendAnything(_peer);\n\t\tconst auto allowInAnotherChat = still && still->allowsForward();\n\t\tif (allowInAnotherChat && (_joinGroup || !canSendReply)) {\n\t\t\treturn true;\n\t\t} else if (!_joinGroup && canSendReply) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\n\tauto update = [=](Ui::Controls::SwipeContextData data) {\n\t\tif (data.translation > 0) {\n\t\t\tif (!_swipeBackData.callback) {\n\t\t\t\t_swipeBackData = Ui::Controls::SetupSwipeBack(\n\t\t\t\t\tthis,\n\t\t\t\t\t[=]() -> std::pair<QColor, QColor> {\n\t\t\t\t\t\tconst auto context = listPreparePaintContext({\n\t\t\t\t\t\t\t.theme = listChatTheme(),\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontext.st->msgServiceBg()->c,\n\t\t\t\t\t\t\tcontext.st->msgServiceFg()->c,\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t}\n\t\t\t_swipeBackData.callback(data);\n\t\t\treturn;\n\t\t} else if (_swipeBackData.lifetime) {\n\t\t\t_swipeBackData = {};\n\t\t}\n\t\tconst auto changed = (_gestureHorizontal.msgBareId != data.msgBareId)\n\t\t\t|| (_gestureHorizontal.translation != data.translation)\n\t\t\t|| (_gestureHorizontal.reachRatio != data.reachRatio);\n\t\tif (changed) {\n\t\t\t_gestureHorizontal = data;\n\t\t\tconst auto item = _peer->owner().message(\n\t\t\t\t_peer->id,\n\t\t\t\tMsgId{ data.msgBareId });\n\t\t\tif (item) {\n\t\t\t\t_history->owner().requestItemRepaint(item);\n\t\t\t}\n\t\t}\n\t};\n\n\tauto init = [=, show = controller()->uiShow()](\n\t\t\tint cursorTop,\n\t\t\tQt::LayoutDirection direction) {\n\t\tif (direction == Qt::RightToLeft) {\n\t\t\treturn Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {\n\t\t\t\tcontroller()->showBackFromStack();\n\t\t\t});\n\t\t}\n\t\tauto result = Ui::Controls::SwipeHandlerFinishData();\n\t\tif (_inner->elementInSelectionMode(nullptr).inSelectionMode) {\n\t\t\treturn result;\n\t\t}\n\t\tconst auto view = _inner->lookupItemByY(cursorTop);\n\t\tif (!view\n\t\t\t|| !view->data()->isRegular()\n\t\t\t|| view->data()->isService()) {\n\t\t\treturn result;\n\t\t}\n\t\tif (!can(view->data())) {\n\t\t\treturn result;\n\t\t}\n\n\t\t_inner->hideElementOverlay();\n\t\tresult.msgBareId = view->data()->fullId().msg.bare;\n\t\tresult.callback = [=, itemId = view->data()->fullId()] {\n\t\t\tconst auto still = show->session().data().message(itemId);\n\t\t\tconst auto view = _inner->viewByPosition(still->position());\n\t\t\tconst auto selected = view\n\t\t\t\t? view->selectedQuote(_inner->getSelectedTextRange(still))\n\t\t\t\t: SelectedQuote();\n\t\t\tconst auto replyToItemId = (selected.item\n\t\t\t\t? selected.item\n\t\t\t\t: still)->fullId();\n\t\t\t_inner->replyToMessageRequestNotify({\n\t\t\t\t.messageId = replyToItemId,\n\t\t\t\t.quote = selected.highlight.quote,\n\t\t\t\t.quoteOffset = selected.highlight.quoteOffset,\n\t\t\t\t.todoItemId = selected.highlight.todoItemId,\n\t\t\t\t.pollOption = selected.highlight.pollOption,\n\t\t\t});\n\t\t};\n\t\treturn result;\n\t};\n\n\tUi::Controls::SetupSwipeHandler({\n\t\t.widget = _inner,\n\t\t.scroll = _scroll.get(),\n\t\t.update = std::move(update),\n\t\t.init = std::move(init),\n\t\t.dontStart = _inner->touchMaybeSelectingValue(),\n\t});\n}\n\nvoid ChatWidget::chooseAttach(\n\t\tstd::optional<bool> overrideSendImagesAsPhotos) {\n\t_choosingAttach = false;\n\tif (const auto error = Data::AnyFileRestrictionError(_peer)) {\n\t\tData::ShowSendErrorToast(controller(), _peer, error);\n\t\treturn;\n\t} else if (showSlowmodeError()) {\n\t\treturn;\n\t}\n\n\tconst auto filter = (overrideSendImagesAsPhotos == true)\n\t\t? FileDialog::PhotoVideoFilesFilter()\n\t\t: FileDialog::AllOrImagesFilter();\n\tFileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=](\n\t\t\tFileDialog::OpenResult &&result) {\n\t\tif (result.paths.isEmpty() && result.remoteContent.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!result.remoteContent.isEmpty()) {\n\t\t\tauto read = Images::Read({\n\t\t\t\t.content = result.remoteContent,\n\t\t\t});\n\t\t\tif (!read.image.isNull() && !read.animated) {\n\t\t\t\tconfirmSendingFiles(\n\t\t\t\t\tstd::move(read.image),\n\t\t\t\t\tstd::move(result.remoteContent),\n\t\t\t\t\toverrideSendImagesAsPhotos);\n\t\t\t} else {\n\t\t\t\tuploadFile(result.remoteContent, SendMediaType::File);\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto premium = controller()->session().user()->isPremium();\n\t\t\tauto list = Storage::PrepareMediaList(\n\t\t\t\tresult.paths,\n\t\t\t\tst::sendMediaPreviewSize,\n\t\t\t\tpremium);\n\t\t\tlist.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;\n\t\t\tconfirmSendingFiles(std::move(list));\n\t\t}\n\t}), nullptr);\n}\n\nbool ChatWidget::confirmSendingFiles(\n\t\tnot_null<const QMimeData*> data,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos,\n\t\tconst QString &insertTextOnCancel) {\n\tconst auto hasImage = data->hasImage();\n\tconst auto premium = controller()->session().user()->isPremium();\n\n\tif (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) {\n\t\tauto list = Storage::PrepareMediaList(\n\t\t\turls,\n\t\t\tst::sendMediaPreviewSize,\n\t\t\tpremium);\n\t\tif (list.error != Ui::PreparedList::Error::NonLocalUrl) {\n\t\t\tif (list.error == Ui::PreparedList::Error::None\n\t\t\t\t|| !hasImage) {\n\t\t\t\tconst auto emptyTextOnCancel = QString();\n\t\t\t\tlist.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;\n\t\t\t\tconfirmSendingFiles(std::move(list), emptyTextOnCancel);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (auto read = Core::ReadMimeImage(data)) {\n\t\tconfirmSendingFiles(\n\t\t\tstd::move(read.image),\n\t\t\tstd::move(read.content),\n\t\t\toverrideSendImagesAsPhotos,\n\t\t\tinsertTextOnCancel);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool ChatWidget::confirmSendingFiles(\n\t\tUi::PreparedList &&list,\n\t\tconst QString &insertTextOnCancel) {\n\tif (_composeControls->confirmMediaEdit(list)) {\n\t\treturn true;\n\t} else if (showSendingFilesError(list)) {\n\t\treturn false;\n\t}\n\n\tauto box = Box<SendFilesBox>(\n\t\tcontroller(),\n\t\tstd::move(list),\n\t\t_composeControls->getTextWithAppliedMarkdown(),\n\t\t_peer,\n\t\tApi::SendType::Normal,\n\t\tsendMenuDetails());\n\n\tbox->setConfirmedCallback(crl::guard(this, [=](\n\t\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\t\tApi::SendOptions options) {\n\t\tsendingFilesConfirmed(std::move(bundle), options);\n\t}));\n\tbox->setCancelledCallback(_composeControls->restoreTextCallback(\n\t\tinsertTextOnCancel));\n\tbox->takeTextWithTagsRequests() | rpl::on_next([=](TextWithTags &&text) {\n\t\t_composeControls->setText(std::move(text));\n\t}, box->lifetime());\n\n\t//ActivateWindow(controller());\n\tcontroller()->show(std::move(box));\n\n\treturn true;\n}\n\nbool ChatWidget::checkSendPayment(\n\t\tint messagesCount,\n\t\tApi::SendOptions options,\n\t\tFn<void(int)> withPaymentApproved) {\n\treturn _sendPayment.check(\n\t\tcontroller(),\n\t\t_peer,\n\t\toptions,\n\t\tmessagesCount,\n\t\tstd::move(withPaymentApproved));\n}\n\nvoid ChatWidget::sendingFilesConfirmed(\n\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\tApi::SendOptions options) {\n\tif (showSendingFilesError(*bundle)) {\n\t\treturn;\n\t}\n\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = options;\n\t\tcopy.starsApproved = approved;\n\t\tsendingFilesConfirmed(bundle, copy);\n\t};\n\tconst auto checked = checkSendPayment(\n\t\tbundle->totalCount,\n\t\toptions,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn;\n\t}\n\n\tconst auto compress = bundle->way.sendImagesAsPhotos();\n\tconst auto type = compress ? SendMediaType::Photo : SendMediaType::File;\n\tauto action = prepareSendAction(options);\n\taction.clearDraft = false;\n\tauto &api = session().api();\n\tfor (auto &group : bundle->groups) {\n\t\tconst auto album = (group.type != Ui::AlbumType::None)\n\t\t\t? std::make_shared<SendingAlbum>()\n\t\t\t: nullptr;\n\t\tapi.sendFiles(std::move(group.list), type, album, action);\n\t}\n\tif (_composeControls->replyingToMessage().messageId\n\t\t\t== action.replyTo.messageId) {\n\t\t_composeControls->cancelReplyMessage();\n\t\trefreshTopBarActiveChat();\n\t}\n\tfinishSending();\n}\n\nbool ChatWidget::confirmSendingFiles(\n\t\tQImage &&image,\n\t\tQByteArray &&content,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos,\n\t\tconst QString &insertTextOnCancel) {\n\tif (image.isNull()) {\n\t\treturn false;\n\t}\n\n\tauto list = Storage::PrepareMediaFromImage(\n\t\tstd::move(image),\n\t\tstd::move(content),\n\t\tst::sendMediaPreviewSize);\n\tlist.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;\n\treturn confirmSendingFiles(std::move(list), insertTextOnCancel);\n}\n\nbool ChatWidget::showSlowmodeError() {\n\tconst auto text = [&] {\n\t\tif (const auto left = _peer->slowmodeSecondsLeft()) {\n\t\t\treturn tr::lng_slowmode_enabled(\n\t\t\t\ttr::now,\n\t\t\t\tlt_left,\n\t\t\t\tUi::FormatDurationWordsSlowmode(left));\n\t\t} else if (_peer->slowmodeApplied()) {\n\t\t\tif (const auto item = _history->latestSendingMessage()) {\n\t\t\t\tshowAtPosition(item->position());\n\t\t\t\treturn tr::lng_slowmode_no_many(tr::now);\n\t\t\t}\n\t\t}\n\t\treturn QString();\n\t}();\n\tif (text.isEmpty()) {\n\t\treturn false;\n\t}\n\tcontroller()->showToast(text);\n\treturn true;\n}\n\nvoid ChatWidget::pushReplyReturn(not_null<HistoryItem*> item) {\n\tif (_repliesRootId) {\n\t\tif (item->history() == _history && item->inThread(_repliesRootId)) {\n\t\t\t_cornerButtons.pushReplyReturn(item);\n\t\t}\n\t}\n}\n\nvoid ChatWidget::checkReplyReturns() {\n\tconst auto currentTop = _scroll->scrollTop();\n\twhile (const auto replyReturn = _cornerButtons.replyReturn()) {\n\t\tconst auto position = replyReturn->position();\n\t\tconst auto scrollTop = _inner->scrollTopForPosition(position);\n\t\tconst auto below = scrollTop\n\t\t\t? (currentTop >= std::min(*scrollTop, _scroll->scrollTopMax()))\n\t\t\t: _inner->isBelowPosition(position);\n\t\tif (below) {\n\t\t\t_cornerButtons.calculateNextReplyReturn();\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nvoid ChatWidget::uploadFile(\n\t\tconst QByteArray &fileContent,\n\t\tSendMediaType type) {\n\tsession().api().sendFile(fileContent, type, prepareSendAction({}));\n}\n\nbool ChatWidget::showSendingFilesError(\n\t\tconst Ui::PreparedList &list) const {\n\tconst auto show = controller()->uiShow();\n\treturn Data::ShowSendError(show, _peer, list, std::nullopt);\n}\n\nbool ChatWidget::showSendingFilesError(\n\t\tconst Ui::PreparedBundle &bundle) const {\n\treturn Data::ShowSendError(controller()->uiShow(), _peer, bundle);\n}\n\nApi::SendAction ChatWidget::prepareSendAction(\n\t\tApi::SendOptions options) const {\n\tauto result = Api::SendAction(_history, options);\n\tresult.replyTo = replyTo();\n\tresult.options.sendAs = _composeControls->sendAsPeer();\n\treturn result;\n}\n\nvoid ChatWidget::send() {\n\tif (_composeControls->getTextWithAppliedMarkdown().text.isEmpty()) {\n\t\treturn;\n\t}\n\tsend({});\n}\n\nvoid ChatWidget::sendVoice(const ComposeControls::VoiceToSend &data) {\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = data;\n\t\tcopy.options.starsApproved = approved;\n\t\tsendVoice(copy);\n\t};\n\tconst auto checked = checkSendPayment(\n\t\t1,\n\t\tdata.options,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn;\n\t}\n\n\tauto action = prepareSendAction(data.options);\n\tsession().api().sendVoiceMessage(\n\t\tdata.bytes,\n\t\tdata.waveform,\n\t\tdata.duration,\n\t\tdata.video,\n\t\tstd::move(action));\n\n\t_composeControls->cancelReplyMessage();\n\t_composeControls->clearListenState();\n\tfinishSending();\n}\n\nvoid ChatWidget::send(Api::SendOptions options) {\n\tif (!options.scheduled && showSlowmodeError()) {\n\t\treturn;\n\t}\n\n\tsendTextWithTags(\n\t\t_composeControls->getTextWithAppliedMarkdown(),\n\t\ttrue,\n\t\toptions,\n\t\tnullptr);\n}\n\nvoid ChatWidget::sendTextWithTags(\n\t\tTextWithTags textWithTags,\n\t\tbool useCurrentWebPageDraft,\n\t\tApi::SendOptions options,\n\t\tFn<void()> done) {\n\tif (!options.scheduled) {\n\t\t_cornerButtons.clearReplyReturns();\n\t}\n\n\tauto message = Api::MessageToSend(prepareSendAction(options));\n\tmessage.textWithTags = textWithTags;\n\tif (useCurrentWebPageDraft) {\n\t\tmessage.webPage = _composeControls->webPageDraft();\n\t}\n\n\tauto request = SendingErrorRequest{\n\t\t.topicRootId = _topic ? _topic->rootId() : MsgId(0),\n\t\t.forward = &_composeControls->forwardItems(),\n\t\t.text = &message.textWithTags,\n\t\t.ignoreSlowmodeCountdown = (options.scheduled != 0),\n\t};\n\trequest.messagesCount = ComputeSendingMessagesCount(_history, request);\n\tconst auto error = GetErrorForSending(_peer, request);\n\tif (error) {\n\t\tData::ShowSendErrorToast(controller(), _peer, error);\n\t\treturn;\n\t}\n\tif (!options.scheduled) {\n\t\tconst auto withPaymentApproved = [=](int approved) {\n\t\t\tauto copy = options;\n\t\t\tcopy.starsApproved = approved;\n\t\t\tsendTextWithTags(textWithTags, useCurrentWebPageDraft, copy, done);\n\t\t};\n\t\tconst auto checked = checkSendPayment(\n\t\t\trequest.messagesCount,\n\t\t\toptions,\n\t\t\twithPaymentApproved);\n\t\tif (!checked) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tconst auto nextLocalMessageId = session().data().nextLocalMessageId();\n\tconst auto hasText = !message.textWithTags.text.trimmed().isEmpty();\n\n\tif (const auto field = _composeControls->fieldForMention(); field\n\t\t&& hasText\n\t\t&& message.webPage.url.isEmpty()\n\t\t&& (field->document()->size().height() <= field->height())) {\n\t\tcontroller()->sendingAnimation().appendSending({\n\t\t\t.type = Ui::MessageSendingAnimationFrom::Type::Text,\n\t\t\t.localId = nextLocalMessageId,\n\t\t\t.globalStartGeometry = field->mapToGlobal(\n\t\t\t\tRect(field->size())),\n\t\t});\n\t}\n\n\tsession().api().sendMessage(std::move(message), nextLocalMessageId);\n\n\t_composeControls->clear();\n\tif (_repliesRootId) {\n\t\tsession().sendProgressManager().update(\n\t\t\t_history,\n\t\t\t_repliesRootId,\n\t\t\tApi::SendProgressType::Typing,\n\t\t\t-1);\n\t}\n\n\t//_saveDraftText = true;\n\t//_saveDraftStart = crl::now();\n\t//onDraftSave();\n\n\tfinishSending();\n\tif (done) {\n\t\tdone();\n\t}\n}\n\nvoid ChatWidget::sendWithTextOverride(\n\t\tTextWithEntities text,\n\t\tApi::SendOptions options,\n\t\tFn<void()> done) {\n\tconst auto useCurrentWebPageDraft\n\t\t= (text.text == _composeControls->prepareTextForEditMsg().text);\n\tsendTextWithTags({\n\t\ttext.text,\n\t\tTextUtilities::ConvertEntitiesToTextTags(text.entities),\n\t}, useCurrentWebPageDraft, options, std::move(done));\n}\n\nvoid ChatWidget::edit(\n\t\tnot_null<HistoryItem*> item,\n\t\tApi::SendOptions options,\n\t\tmtpRequestId *const saveEditMsgRequestId,\n\t\tbool spoilered) {\n\tif (*saveEditMsgRequestId) {\n\t\treturn;\n\t}\n\tconst auto webpage = _composeControls->webPageDraft();\n\tconst auto sending = _composeControls->prepareTextForEditMsg();\n\n\tconst auto hasMediaWithCaption = item\n\t\t&& item->media()\n\t\t&& item->media()->allowsEditCaption();\n\tif (sending.text.isEmpty() && !hasMediaWithCaption) {\n\t\tif (item) {\n\t\t\tcontroller()->show(Box<DeleteMessagesBox>(item, false));\n\t\t} else {\n\t\t\tdoSetInnerFocus();\n\t\t}\n\t\treturn;\n\t} else {\n\t\tconst auto maxCaptionSize = !hasMediaWithCaption\n\t\t\t? MaxMessageSize\n\t\t\t: Data::PremiumLimits(&session()).captionLengthCurrent();\n\t\tconst auto remove = _composeControls->fieldCharacterCount()\n\t\t\t- maxCaptionSize;\n\t\tif (remove > 0) {\n\t\t\tcontroller()->showToast(\n\t\t\t\ttr::lng_edit_limit_reached(tr::now, lt_count, remove));\n\t\t\treturn;\n\t\t}\n\t}\n\n\tlifetime().add([=] {\n\t\tif (!*saveEditMsgRequestId) {\n\t\t\treturn;\n\t\t}\n\t\tsession().api().request(base::take(*saveEditMsgRequestId)).cancel();\n\t});\n\n\tconst auto done = [=](mtpRequestId requestId) {\n\t\tif (requestId == *saveEditMsgRequestId) {\n\t\t\t*saveEditMsgRequestId = 0;\n\t\t\t_composeControls->cancelEditMessage();\n\t\t}\n\t};\n\n\tconst auto fail = [=](const QString &error, mtpRequestId requestId) {\n\t\tif (requestId == *saveEditMsgRequestId) {\n\t\t\t*saveEditMsgRequestId = 0;\n\t\t}\n\n\t\tif (ranges::contains(Api::kDefaultEditMessagesErrors, error)) {\n\t\t\tcontroller()->showToast(tr::lng_edit_error(tr::now));\n\t\t} else if (error == u\"MESSAGE_NOT_MODIFIED\"_q) {\n\t\t\t_composeControls->cancelEditMessage();\n\t\t} else if (error == u\"MESSAGE_EMPTY\"_q) {\n\t\t\tdoSetInnerFocus();\n\t\t} else {\n\t\t\tcontroller()->showToast(tr::lng_edit_error(tr::now));\n\t\t}\n\t\tupdate();\n\t\treturn true;\n\t};\n\n\t*saveEditMsgRequestId = Api::EditTextMessage(\n\t\titem,\n\t\tsending,\n\t\twebpage,\n\t\toptions,\n\t\tcrl::guard(this, done),\n\t\tcrl::guard(this, fail),\n\t\tspoilered);\n\n\t_composeControls->hidePanelsAnimated();\n\tdoSetInnerFocus();\n}\n\nvoid ChatWidget::validateSubsectionTabs() {\n\tif (!_subsectionCheckLifetime && _history->peer->isMegagroup()) {\n\t\t_subsectionCheckLifetime = _history->peer->asChannel()->flagsValue(\n\t\t) | rpl::skip(\n\t\t\t1\n\t\t) | rpl::filter([=](Data::Flags<ChannelDataFlags>::Change change) {\n\t\t\tconst auto mask = ChannelDataFlag::Forum\n\t\t\t\t| ChannelDataFlag::ForumTabs\n\t\t\t\t| ChannelDataFlag::MonoforumAdmin;\n\t\t\treturn change.diff & mask;\n\t\t}) | rpl::on_next([=] {\n\t\t\tvalidateSubsectionTabs();\n\t\t});\n\t}\n\tconst auto thread = _topic ? (Data::Thread*)_topic : _sublist;\n\tif (!thread || !HistoryView::SubsectionTabs::UsedFor(_history)) {\n\t\tif (_subsectionTabs) {\n\t\t\t_subsectionTabsLifetime.destroy();\n\t\t\t_subsectionTabs = nullptr;\n\t\t\tupdateControlsGeometry();\n\t\t\tif (const auto forum = _history->asForum()) {\n\t\t\t\tcontroller()->showForum(forum, {\n\t\t\t\t\tWindow::SectionShow::Way::Backward,\n\t\t\t\t\tanim::type::normal,\n\t\t\t\t\tanim::activation::background,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\treturn;\n\t} else if (_subsectionTabs) {\n\t\treturn;\n\t}\n\t_subsectionTabs = controller()->restoreSubsectionTabsFor(this, thread);\n\tif (!_subsectionTabs) {\n\t\t_subsectionTabs = std::make_unique<HistoryView::SubsectionTabs>(\n\t\t\tcontroller(),\n\t\t\tthis,\n\t\t\tthread);\n\t}\n\t_subsectionTabs->removeRequests() | rpl::on_next([=] {\n\t\t_subsectionTabsLifetime.destroy();\n\t\t_subsectionTabs = nullptr;\n\t\tupdateControlsGeometry();\n\t}, _subsectionTabsLifetime);\n\t_subsectionTabs->layoutRequests() | rpl::on_next([=] {\n\t\t_inner->overrideChatMode((_subsectionTabs->leftSkip() > 0)\n\t\t\t? ElementChatMode::Narrow\n\t\t\t: std::optional<ElementChatMode>());\n\t\tupdateControlsGeometry();\n\t\torderWidgets();\n\t}, _subsectionTabsLifetime);\n\t_inner->overrideChatMode((_subsectionTabs->leftSkip() > 0)\n\t\t? ElementChatMode::Narrow\n\t\t: std::optional<ElementChatMode>());\n\tupdateControlsGeometry();\n\torderWidgets();\n}\n\nvoid ChatWidget::refreshJoinGroupButton() {\n\tif (!_repliesRootId || !_peer->isChannel()) {\n\t\treturn;\n\t}\n\tconst auto set = [&](std::unique_ptr<Ui::FlatButton> button) {\n\t\tif (!button && !_joinGroup) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto atMax = (_scroll->scrollTopMax() == _scroll->scrollTop());\n\t\t_joinGroup = std::move(button);\n\t\tif (!animatingShow()) {\n\t\t\tif (button) {\n\t\t\t\tbutton->show();\n\t\t\t\t_composeControls->hide();\n\t\t\t} else {\n\t\t\t\t_composeControls->show();\n\t\t\t}\n\t\t}\n\t\tupdateControlsGeometry();\n\t\tif (atMax) {\n\t\t\tlistScrollTo(_scroll->scrollTopMax());\n\t\t}\n\t};\n\tconst auto channel = _peer->asChannel();\n\tconst auto canSend = !channel->isForum()\n\t\t? Data::CanSendAnything(channel)\n\t\t: (_topic && Data::CanSendAnything(_topic));\n\tif (channel->amIn() || canSend) {\n\t\t_canSendTexts = true;\n\t\tset(nullptr);\n\t} else {\n\t\t_canSendTexts = false;\n\t\tif (!_joinGroup) {\n\t\t\tset(std::make_unique<Ui::FlatButton>(\n\t\t\t\tthis,\n\t\t\t\tQString(),\n\t\t\t\tst::historyComposeButton));\n\t\t\t_joinGroup->setClickedCallback([=] {\n\t\t\t\tsession().api().joinChannel(channel);\n\t\t\t});\n\t\t}\n\t\t_joinGroup->setText((channel->isBroadcast()\n\t\t\t? tr::lng_profile_join_channel(tr::now)\n\t\t\t: (channel->requestToJoin() && !channel->amCreator())\n\t\t\t? tr::lng_profile_apply_to_join_group(tr::now)\n\t\t\t: tr::lng_profile_join_group(tr::now)).toUpper());\n\t}\n}\n\nbool ChatWidget::sendExistingDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tApi::MessageToSend messageToSend,\n\t\tstd::optional<MsgId> localId) {\n\tconst auto error = Data::RestrictionError(\n\t\t_peer,\n\t\tChatRestriction::SendStickers);\n\tif (error) {\n\t\tData::ShowSendErrorToast(controller(), _peer, error);\n\t\treturn false;\n\t} else if (showSlowmodeError()\n\t\t|| ShowSendPremiumError(controller(), document)) {\n\t\treturn false;\n\t}\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = messageToSend;\n\t\tcopy.action.options.starsApproved = approved;\n\t\tsendExistingDocument(document, std::move(copy), localId);\n\t};\n\tconst auto checked = checkSendPayment(\n\t\t1,\n\t\tmessageToSend.action.options,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn false;\n\t}\n\n\tApi::SendExistingDocument(\n\t\tstd::move(messageToSend),\n\t\tdocument,\n\t\tlocalId);\n\n\t_composeControls->cancelReplyMessage();\n\tfinishSending();\n\treturn true;\n}\n\nvoid ChatWidget::sendExistingPhoto(not_null<PhotoData*> photo) {\n\tsendExistingPhoto(photo, {});\n}\n\nbool ChatWidget::sendExistingPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tApi::SendOptions options) {\n\tconst auto error = Data::RestrictionError(\n\t\t_peer,\n\t\tChatRestriction::SendPhotos);\n\tif (error) {\n\t\tData::ShowSendErrorToast(controller(), _peer, error);\n\t\treturn false;\n\t} else if (showSlowmodeError()) {\n\t\treturn false;\n\t}\n\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = options;\n\t\tcopy.starsApproved = approved;\n\t\tsendExistingPhoto(photo, copy);\n\t};\n\tconst auto checked = checkSendPayment(\n\t\t1,\n\t\toptions,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn false;\n\t}\n\n\tApi::SendExistingPhoto(\n\t\tApi::MessageToSend(prepareSendAction(options)),\n\t\tphoto);\n\n\t_composeControls->cancelReplyMessage();\n\tfinishSending();\n\treturn true;\n}\n\nvoid ChatWidget::sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot) {\n\tif (const auto error = result->getErrorOnSend(_history)) {\n\t\tData::ShowSendErrorToast(controller(), _peer, error);\n\t\treturn;\n\t}\n\tsendInlineResult(std::move(result), bot, {}, std::nullopt);\n\t//const auto callback = [=](Api::SendOptions options) {\n\t//\tsendInlineResult(result, bot, options);\n\t//};\n\t//Ui::show(\n\t//\tPrepareScheduleBox(this, sendMenuType(), callback),\n\t//\tUi::LayerOption::KeepOther);\n}\n\nvoid ChatWidget::sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot,\n\t\tApi::SendOptions options,\n\t\tstd::optional<MsgId> localMessageId) {\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = options;\n\t\tcopy.starsApproved = approved;\n\t\tsendInlineResult(result, bot, copy, localMessageId);\n\t};\n\tconst auto checked = checkSendPayment(\n\t\t1,\n\t\toptions,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn;\n\t}\n\n\tauto action = prepareSendAction(options);\n\taction.generateLocal = true;\n\tsession().api().sendInlineResult(\n\t\tbot,\n\t\tresult.get(),\n\t\taction,\n\t\tlocalMessageId);\n\n\t_composeControls->clear();\n\t//_saveDraftText = true;\n\t//_saveDraftStart = crl::now();\n\t//onDraftSave();\n\n\tauto &bots = cRefRecentInlineBots();\n\tconst auto index = bots.indexOf(bot);\n\tif (index) {\n\t\tif (index > 0) {\n\t\t\tbots.removeAt(index);\n\t\t} else if (bots.size() >= RecentInlineBotsLimit) {\n\t\t\tbots.resize(RecentInlineBotsLimit - 1);\n\t\t}\n\t\tbots.push_front(bot);\n\t\tbot->session().local().writeRecentHashtagsAndBots();\n\t}\n\tfinishSending();\n}\n\nSendMenu::Details ChatWidget::sendMenuDetails() const {\n\tusing Type = SendMenu::Type;\n\tconst auto type = (_topic && !_peer->starsPerMessageChecked())\n\t\t? Type::Scheduled\n\t\t: Type::SilentOnly;\n\treturn SendMenu::Details{ .type = type };\n}\n\nFullReplyTo ChatWidget::replyTo() const {\n\tif (auto custom = _composeControls->replyingToMessage()) {\n\t\tconst auto item = custom.messageId\n\t\t\t? session().data().message(custom.messageId)\n\t\t\t: nullptr;\n\t\tconst auto sublistPeerId = item ? item->sublistPeerId() : PeerId();\n\t\tif (!item\n\t\t\t|| !_monoforumPeerId\n\t\t\t|| (sublistPeerId == _monoforumPeerId)) {\n\t\t\t// Never answer to a message in a wrong monoforum peer id.\n\t\t\tcustom.topicRootId = _repliesRootId;\n\t\t\tcustom.monoforumPeerId = _monoforumPeerId;\n\t\t\treturn custom;\n\t\t}\n\t}\n\treturn FullReplyTo{\n\t\t.messageId = (_repliesRootId\n\t\t\t? FullMsgId(_peer->id, _repliesRootId)\n\t\t\t: FullMsgId()),\n\t\t.topicRootId = _repliesRootId,\n\t\t.monoforumPeerId = _monoforumPeerId,\n\t};\n}\n\nvoid ChatWidget::refreshTopBarActiveChat() {\n\tusing namespace Dialogs;\n\n\tconst auto state = EntryState{\n\t\t.key = (_sublist\n\t\t\t? Key{ _sublist }\n\t\t\t: _topic\n\t\t\t? Key{ _topic }\n\t\t\t: Key{ _history }),\n\t\t.section = _sublist\n\t\t\t? EntryState::Section::SavedSublist\n\t\t\t: EntryState::Section::Replies,\n\t\t.currentReplyTo = replyTo(),\n\t\t.currentSuggest = SuggestOptions(),\n\t};\n\t_topBar->setActiveChat(state, _sendAction.get());\n\t_composeControls->setCurrentDialogsEntryState(state);\n\tcontroller()->setDialogsEntryState(state);\n}\n\nvoid ChatWidget::refreshUnreadCountBadge(std::optional<int> count) {\n\tif (count.has_value()) {\n\t\t_cornerButtons.updateJumpDownVisibility(count);\n\t}\n}\n\nvoid ChatWidget::updatePinnedViewer() {\n\tif (_scroll->isHidden() || (!_topic && !_sublist) || !_pinnedTracker) {\n\t\treturn;\n\t}\n\tconst auto visibleBottom = _scroll->scrollTop() + _scroll->height();\n\tauto [view, offset] = _inner->findViewForPinnedTracking(visibleBottom);\n\tconst auto lessThanId = !view\n\t\t? (ServerMaxMsgId - 1)\n\t\t: (view->data()->id + (offset > 0 ? 1 : 0));\n\tconst auto lastClickedId = !_pinnedClickedId\n\t\t? (ServerMaxMsgId - 1)\n\t\t: _pinnedClickedId.msg;\n\tif (_pinnedClickedId\n\t\t&& lessThanId <= lastClickedId\n\t\t&& !_inner->animatedScrolling()) {\n\t\t_pinnedClickedId = FullMsgId();\n\t}\n\tif (_pinnedClickedId && !_minPinnedId) {\n\t\t_minPinnedId = Data::ResolveMinPinnedId(\n\t\t\t_peer,\n\t\t\t_repliesRootId,\n\t\t\t_monoforumPeerId);\n\t}\n\tif (_pinnedClickedId && _minPinnedId && _minPinnedId >= _pinnedClickedId) {\n\t\t// After click on the last pinned message we should the top one.\n\t\t_pinnedTracker->trackAround(ServerMaxMsgId - 1);\n\t} else {\n\t\t_pinnedTracker->trackAround(std::min(lessThanId, lastClickedId));\n\t}\n}\n\nvoid ChatWidget::checkLastPinnedClickedIdReset(\n\t\tint wasScrollTop,\n\t\tint nowScrollTop) {\n\tif (_scroll->isHidden() || (!_topic && !_sublist)) {\n\t\treturn;\n\t}\n\tif (wasScrollTop < nowScrollTop && _pinnedClickedId) {\n\t\t// User scrolled down.\n\t\t_pinnedClickedId = FullMsgId();\n\t\t_minPinnedId = std::nullopt;\n\t\tupdatePinnedViewer();\n\t}\n}\n\nvoid ChatWidget::setupOpenChatButton() {\n\tif (!_sublist || _sublist->sublistPeer()->isSavedHiddenAuthor()) {\n\t\treturn;\n\t} else if (_sublist->parentChat()) {\n\t\t_canSendTexts = true;\n\t\treturn;\n\t}\n\t_openChatButton = std::make_unique<Ui::FlatButton>(\n\t\tthis,\n\t\t(_sublist->sublistPeer()->isBroadcast()\n\t\t\t? tr::lng_saved_open_channel(tr::now)\n\t\t\t: _sublist->sublistPeer()->isUser()\n\t\t\t? tr::lng_saved_open_chat(tr::now)\n\t\t\t: tr::lng_saved_open_group(tr::now)),\n\t\tst::historyComposeButton);\n\n\t_openChatButton->setClickedCallback([=] {\n\t\tcontroller()->showPeerHistory(\n\t\t\t_sublist->sublistPeer(),\n\t\t\tWindow::SectionShow::Way::Forward);\n\t});\n}\n\nvoid ChatWidget::setupAboutHiddenAuthor() {\n\tif (!_sublist || !_sublist->sublistPeer()->isSavedHiddenAuthor()) {\n\t\treturn;\n\t} else if (_sublist->parentChat()) {\n\t\t_canSendTexts = true;\n\t\treturn;\n\t}\n\t_aboutHiddenAuthor = std::make_unique<Ui::RpWidget>(this);\n\t_aboutHiddenAuthor->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(_aboutHiddenAuthor.get());\n\t\tauto rect = _aboutHiddenAuthor->rect();\n\n\t\tp.fillRect(rect, st::historyReplyBg);\n\n\t\tp.setFont(st::normalFont);\n\t\tp.setPen(st::windowSubTextFg);\n\t\tp.drawText(\n\t\t\trect.marginsRemoved(\n\t\t\t\tQMargins(st::historySendPadding, 0, st::historySendPadding, 0)),\n\t\t\ttr::lng_saved_about_hidden(tr::now),\n\t\t\tstyle::al_center);\n\t}, _aboutHiddenAuthor->lifetime());\n}\n\nvoid ChatWidget::setupTranslateBar() {\n\tcontroller()->adaptive().oneColumnValue(\n\t) | rpl::on_next([=, raw = _translateBar.get()](bool one) {\n\t\traw->setShadowGeometryPostprocess([=](QRect geometry) {\n\t\t\tif (!one) {\n\t\t\t\tgeometry.setLeft(geometry.left() + st::lineWidth);\n\t\t\t}\n\t\t\treturn geometry;\n\t\t});\n\t}, _translateBar->lifetime());\n\n\t_translateBarHeight = 0;\n\t_translateBar->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tif (const auto delta = height - _translateBarHeight) {\n\t\t\t_translateBarHeight = height;\n\t\t\tsetGeometryWithTopMoved(geometry(), delta);\n\t\t}\n\t}, _translateBar->lifetime());\n\n\t_translateBar->finishAnimating();\n}\n\nvoid ChatWidget::setupPinnedTracker() {\n\tExpects(_topic || _sublist);\n\n\tconst auto thread = _topic ? (Data::Thread*)_topic : _sublist;\n\t_pinnedTracker = std::make_unique<HistoryView::PinnedTracker>(thread);\n\t_pinnedBar = nullptr;\n\n\tSharedMediaViewer(\n\t\t&session(),\n\t\tStorage::SharedMediaKey(\n\t\t\t_peer->id,\n\t\t\t_repliesRootId,\n\t\t\t_monoforumPeerId,\n\t\t\tStorage::SharedMediaType::Pinned,\n\t\t\tServerMaxMsgId - 1),\n\t\t1,\n\t\t1\n\t) | rpl::filter([=](const SparseIdsSlice &result) {\n\t\treturn result.fullCount().has_value();\n\t}) | rpl::on_next([=](const SparseIdsSlice &result) {\n\t\tthread->setHasPinnedMessages(*result.fullCount() != 0);\n\t\tif (result.skippedAfter() == 0) {\n\t\t\tauto &settings = _history->session().settings();\n\t\t\tconst auto peerId = _peer->id;\n\t\t\tconst auto hiddenId = settings.hiddenPinnedMessageId(\n\t\t\t\tpeerId,\n\t\t\t\t_repliesRootId,\n\t\t\t\t_monoforumPeerId);\n\t\t\tconst auto last = result.size() ? result[result.size() - 1] : 0;\n\t\t\tif (hiddenId && hiddenId != last) {\n\t\t\t\tsettings.setHiddenPinnedMessageId(\n\t\t\t\t\tpeerId,\n\t\t\t\t\t_repliesRootId,\n\t\t\t\t\t_monoforumPeerId,\n\t\t\t\t\t0);\n\t\t\t\t_history->session().saveSettingsDelayed();\n\t\t\t}\n\t\t}\n\t\tcheckPinnedBarState();\n\t}, lifetime());\n}\n\nvoid ChatWidget::checkPinnedBarState() {\n\tExpects(_pinnedTracker != nullptr);\n\tExpects(_inner != nullptr);\n\n\tconst auto hiddenId = _peer->canPinMessages()\n\t\t? MsgId(0)\n\t\t: _peer->session().settings().hiddenPinnedMessageId(\n\t\t\t_peer->id,\n\t\t\t_repliesRootId,\n\t\t\t_monoforumPeerId);\n\tconst auto currentPinnedId = Data::ResolveTopPinnedId(\n\t\t_peer,\n\t\t_repliesRootId,\n\t\t_monoforumPeerId);\n\tconst auto universalPinnedId = !currentPinnedId\n\t\t? MsgId(0)\n\t\t: currentPinnedId.msg;\n\tif (universalPinnedId == hiddenId) {\n\t\tif (_pinnedBar) {\n\t\t\t_pinnedBar->setContent(rpl::single(Ui::MessageBarContent()));\n\t\t\t_pinnedTracker->reset();\n\t\t\t_shownPinnedItem = nullptr;\n\t\t\t_hidingPinnedBar = base::take(_pinnedBar);\n\t\t\tconst auto raw = _hidingPinnedBar.get();\n\t\t\tbase::call_delayed(st::defaultMessageBar.duration, this, [=] {\n\t\t\t\tif (_hidingPinnedBar.get() == raw) {\n\t\t\t\t\tclearHidingPinnedBar();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\treturn;\n\t}\n\tif (_pinnedBar || !universalPinnedId) {\n\t\treturn;\n\t}\n\n\tclearHidingPinnedBar();\n\t_pinnedBar = std::make_unique<Ui::PinnedBar>(_topBars.get(), [=] {\n\t\treturn controller()->isGifPausedAtLeastFor(\n\t\t\tWindow::GifPauseReason::Any);\n\t}, controller()->gifPauseLevelChanged());\n\tauto pinnedRefreshed = Info::Profile::SharedMediaCountValue(\n\t\t_peer,\n\t\t_repliesRootId,\n\t\t_monoforumPeerId,\n\t\tnullptr,\n\t\tStorage::SharedMediaType::Pinned\n\t) | rpl::distinct_until_changed(\n\t) | rpl::map([=](int count) {\n\t\tif (_pinnedClickedId) {\n\t\t\t_pinnedClickedId = FullMsgId();\n\t\t\t_minPinnedId = std::nullopt;\n\t\t\tupdatePinnedViewer();\n\t\t}\n\t\treturn (count > 1);\n\t}) | rpl::distinct_until_changed();\n\tauto customButtonItem = HistoryView::PinnedBarItemWithCustomButton(\n\t\t&session(),\n\t\t_pinnedTracker->shownMessageId());\n\trpl::combine(\n\t\trpl::duplicate(pinnedRefreshed),\n\t\trpl::duplicate(customButtonItem)\n\t) | rpl::on_next([=](bool many, HistoryItem *item) {\n\t\trefreshPinnedBarButton(many, item);\n\t}, _pinnedBar->lifetime());\n\n\t_pinnedBar->setContent(rpl::combine(\n\t\tHistoryView::PinnedBarContent(\n\t\t\t&session(),\n\t\t\t_pinnedTracker->shownMessageId(),\n\t\t\t[bar = _pinnedBar.get()] { bar->customEmojiRepaint(); }),\n\t\tstd::move(pinnedRefreshed),\n\t\tstd::move(customButtonItem),\n\t\t_repliesRootVisible.value()\n\t) | rpl::map([=](Ui::MessageBarContent &&content, auto, auto, bool show) {\n\t\tconst auto shown = !content.title.isEmpty() && !content.text.empty();\n\t\t_shownPinnedItem = shown\n\t\t\t? _history->owner().message(\n\t\t\t\t_pinnedTracker->currentMessageId().message)\n\t\t\t: nullptr;\n\t\treturn (show || content.count > 1)\n\t\t\t? std::move(content)\n\t\t\t: Ui::MessageBarContent();\n\t}));\n\n\tcontroller()->adaptive().oneColumnValue(\n\t) | rpl::on_next([=, raw = _pinnedBar.get()](bool one) {\n\t\traw->setShadowGeometryPostprocess([=](QRect geometry) {\n\t\t\tif (!one) {\n\t\t\t\tgeometry.setLeft(geometry.left() + st::lineWidth);\n\t\t\t}\n\t\t\treturn geometry;\n\t\t});\n\t}, _pinnedBar->lifetime());\n\n\t_pinnedBar->barClicks(\n\t) | rpl::on_next([=] {\n\t\tconst auto id = _pinnedTracker->currentMessageId();\n\t\tif (const auto item = session().data().message(id.message)) {\n\t\t\tshowAtPosition(item->position());\n\t\t\tif (const auto group = session().data().groups().find(item)) {\n\t\t\t\t// Hack for the case when a non-first item of an album\n\t\t\t\t// is pinned and we still want the 'show last after first'.\n\t\t\t\t_pinnedClickedId = group->items.front()->fullId();\n\t\t\t} else {\n\t\t\t\t_pinnedClickedId = id.message;\n\t\t\t}\n\t\t\t_minPinnedId = std::nullopt;\n\t\t\tupdatePinnedViewer();\n\t\t}\n\t}, _pinnedBar->lifetime());\n\n\t_pinnedBarHeight = 0;\n\t_pinnedBar->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tif (const auto delta = height - _pinnedBarHeight) {\n\t\t\t_pinnedBarHeight = height;\n\t\t\tsetGeometryWithTopMoved(geometry(), delta);\n\t\t}\n\t}, _pinnedBar->lifetime());\n\n\torderWidgets();\n}\n\nvoid ChatWidget::clearHidingPinnedBar() {\n\tif (!_hidingPinnedBar) {\n\t\treturn;\n\t}\n\tif (const auto delta = -_pinnedBarHeight) {\n\t\t_pinnedBarHeight = 0;\n\t\tsetGeometryWithTopMoved(geometry(), delta);\n\t}\n\t_hidingPinnedBar = nullptr;\n}\n\nvoid ChatWidget::refreshPinnedBarButton(bool many, HistoryItem *item) {\n\tif (!_pinnedBar) {\n\t\treturn; // It can be in process of hiding.\n\t}\n\tconst auto openSection = [=] {\n\t\tconst auto id = _pinnedTracker\n\t\t\t? _pinnedTracker->currentMessageId()\n\t\t\t: HistoryView::PinnedId();\n\t\tif (!id.message) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto thread = _topic ? (Data::Thread*)_topic : _sublist;\n\t\tcontroller()->showSection(\n\t\t\tstd::make_shared<PinnedMemento>(thread, id.message.msg));\n\t};\n\tconst auto context = [copy = _inner](FullMsgId itemId) {\n\t\tif (const auto raw = copy.data()) {\n\t\t\treturn raw->prepareClickHandlerContext(itemId);\n\t\t}\n\t\treturn ClickHandlerContext();\n\t};\n\tauto customButton = CreatePinnedBarCustomButton(this, item, context);\n\tif (customButton) {\n\t\tstruct State {\n\t\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t\t};\n\t\tconst auto buttonRaw = customButton.data();\n\t\tconst auto state = buttonRaw->lifetime().make_state<State>();\n\t\t_pinnedBar->contextMenuRequested(\n\t\t) | rpl::on_next([=] {\n\t\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(buttonRaw);\n\t\t\tstate->menu->addAction(\n\t\t\t\ttr::lng_settings_events_pinned(tr::now),\n\t\t\t\topenSection);\n\t\t\tstate->menu->popup(QCursor::pos());\n\t\t}, buttonRaw->lifetime());\n\t\t_pinnedBar->setRightButton(std::move(customButton));\n\t\treturn;\n\t}\n\tconst auto close = !many;\n\tauto button = object_ptr<Ui::IconButton>(\n\t\tthis,\n\t\tclose ? st::historyReplyCancel : st::historyPinnedShowAll);\n\tbutton->setAccessibleName(close\n\t\t? tr::lng_cancel(tr::now)\n\t\t: tr::lng_settings_events_pinned(tr::now));\n\tbutton->clicks(\n\t) | rpl::on_next([=] {\n\t\tif (close) {\n\t\t\thidePinnedMessage();\n\t\t} else {\n\t\t\topenSection();\n\t\t}\n\t}, button->lifetime());\n\t_pinnedBar->setRightButton(std::move(button));\n}\n\nvoid ChatWidget::hidePinnedMessage() {\n\tExpects(_pinnedBar != nullptr);\n\n\tconst auto id = _pinnedTracker->currentMessageId();\n\tif (!id.message) {\n\t\treturn;\n\t}\n\tif (_peer->canPinMessages()) {\n\t\tWindow::ToggleMessagePinned(controller(), id.message, false);\n\t} else {\n\t\tconst auto callback = [=] {\n\t\t\tif (_pinnedTracker) {\n\t\t\t\tcheckPinnedBarState();\n\t\t\t}\n\t\t};\n\t\tWindow::HidePinnedBar(\n\t\t\tcontroller(),\n\t\t\t_peer,\n\t\t\t_repliesRootId,\n\t\t\t_monoforumPeerId,\n\t\t\tcrl::guard(this, callback));\n\t}\n}\n\nvoid ChatWidget::cornerButtonsShowAtPosition(\n\t\tData::MessagePosition position) {\n\tshowAtPosition(position);\n}\n\nData::Thread *ChatWidget::cornerButtonsThread() {\n\treturn _sublist\n\t\t? static_cast<Data::Thread*>(_sublist)\n\t\t: _topic\n\t\t? static_cast<Data::Thread*>(_topic)\n\t\t: _history;\n}\n\nFullMsgId ChatWidget::cornerButtonsCurrentId() {\n\treturn _lastShownAt;\n}\n\nbool ChatWidget::cornerButtonsIgnoreVisibility() {\n\treturn animatingShow();\n}\n\nstd::optional<bool> ChatWidget::cornerButtonsDownShown() {\n\tif (_composeControls->isLockPresent()\n\t\t|| _composeControls->isTTLButtonShown()) {\n\t\treturn false;\n\t}\n\tconst auto top = _scroll->scrollTop() + st::historyToDownShownAfter;\n\tif (top < _scroll->scrollTopMax() || _cornerButtons.replyReturn()) {\n\t\treturn true;\n\t} else if (_inner->loadedAtBottomKnown()) {\n\t\treturn !_inner->loadedAtBottom();\n\t}\n\treturn std::nullopt;\n}\n\nbool ChatWidget::cornerButtonsUnreadMayBeShown() {\n\treturn _loaded\n\t\t&& !_composeControls->isLockPresent()\n\t\t&& !_composeControls->isTTLButtonShown();\n}\n\nbool ChatWidget::cornerButtonsHas(CornerButtonType type) {\n\treturn _topic\n\t\t|| (_sublist && type == CornerButtonType::Reactions)\n\t\t|| (type == CornerButtonType::Down);\n}\n\nvoid ChatWidget::showAtStart() {\n\tshowAtPosition(Data::MinMessagePosition);\n}\n\nvoid ChatWidget::showAtEnd() {\n\tshowAtPosition(Data::MaxMessagePosition);\n}\n\nvoid ChatWidget::finishSending() {\n\t_composeControls->hidePanelsAnimated();\n\t//if (_previewData && _previewData->pendingTill) previewCancel();\n\tdoSetInnerFocus();\n\tshowAtEnd();\n\trefreshTopBarActiveChat();\n}\n\nvoid ChatWidget::showAtPosition(\n\t\tData::MessagePosition position,\n\t\tFullMsgId originItemId) {\n\tshowAtPosition(position, originItemId, {});\n}\n\nvoid ChatWidget::showAtPosition(\n\t\tData::MessagePosition position,\n\t\tFullMsgId originItemId,\n\t\tconst Window::SectionShow &params) {\n\t_lastShownAt = position.fullId;\n\tcontroller()->setActiveChatEntry(activeChat());\n\tconst auto ignore = _repliesRootId\n\t\t&& (position.fullId.msg == _repliesRootId);\n\t_inner->showAtPosition(\n\t\tposition,\n\t\tparams,\n\t\t_cornerButtons.doneJumpFrom(position.fullId, originItemId, ignore));\n}\n\nvoid ChatWidget::updateAdaptiveLayout() {\n\t_topBarShadow->moveToLeft(\n\t\tcontroller()->adaptive().isOneColumn() ? 0 : st::lineWidth,\n\t\t_topBar->height());\n}\n\nDialogs::RowDescriptor ChatWidget::activeChat() const {\n\tconst auto messageId = _lastShownAt\n\t\t? _lastShownAt\n\t\t: FullMsgId(_peer->id, ShowAtUnreadMsgId);\n\tif (_sublist) {\n\t\treturn { _sublist, messageId };\n\t} else if (_topic) {\n\t\treturn { _topic, messageId };\n\t}\n\treturn { _history, messageId };\n}\n\nbool ChatWidget::preventsClose(Fn<void()> &&continueCallback) const {\n\tif (_composeControls->preventsClose(base::duplicate(continueCallback))) {\n\t\treturn true;\n\t} else if (!_newTopicDiscarded\n\t\t&& _topic\n\t\t&& _topic->creating()) {\n\t\tconst auto weak = base::make_weak(this);\n\t\tauto sure = [=](Fn<void()> &&close) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->_newTopicDiscarded = true;\n\t\t\t}\n\t\t\tclose();\n\t\t\tif (continueCallback) {\n\t\t\t\tcontinueCallback();\n\t\t\t}\n\t\t};\n\t\tcontroller()->show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_forum_discard_sure(tr::now),\n\t\t\t.confirmed = std::move(sure),\n\t\t\t.confirmText = tr::lng_record_lock_discard(),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t}));\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nQPixmap ChatWidget::grabForShowAnimation(const Window::SectionSlideParams &params) {\n\t_topBar->updateControlsVisibility();\n\tif (params.withTopBarShadow) _topBarShadow->hide();\n\tif (_joinGroup) {\n\t\t_composeControls->hide();\n\t} else {\n\t\t_composeControls->showForGrab();\n\t}\n\tauto result = Ui::GrabWidget(this);\n\tif (params.withTopBarShadow) {\n\t\t_topBarShadow->show();\n\t}\n\t_topBars->hide();\n\tif (_subsectionTabs) {\n\t\t_subsectionTabs->hide();\n\t}\n\treturn result;\n}\n\nvoid ChatWidget::checkActivation() {\n\t_inner->checkActivation();\n}\n\nvoid ChatWidget::doSetInnerFocus() {\n\tif (_composeSearch\n\t\t&& _inner->getSelectedText().rich.text.isEmpty()\n\t\t&& _inner->getSelectedItems().empty()) {\n\t\t_composeSearch->setInnerFocus();\n\t} else if (!_inner->getSelectedText().rich.text.isEmpty()\n\t\t|| !_inner->getSelectedItems().empty()\n\t\t|| !_composeControls->focus()) {\n\t\t_inner->setFocus();\n\t}\n}\n\nbool ChatWidget::showInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) {\n\tif (auto logMemento = dynamic_cast<ChatMemento*>(memento.get())) {\n\t\tif (logMemento->id() == _id) {\n\t\t\tif (params.reapplyLocalDraft) {\n\t\t\t\t_composeControls->applyDraft(\n\t\t\t\t\tComposeControls::FieldHistoryAction::NewEntry);\n\t\t\t} else {\n\t\t\t\trestoreState(logMemento);\n\t\t\t\tif (!logMemento->highlightId()) {\n\t\t\t\t\tshowAtPosition(Data::UnreadMessagePosition);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool ChatWidget::sameTypeAs(not_null<Window::SectionMemento*> memento) {\n\treturn dynamic_cast<ChatMemento*>(memento.get()) != nullptr;\n}\n\nvoid ChatWidget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<ChatMemento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nbool ChatWidget::pushTabbedSelectorToThirdSection(\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst Window::SectionShow &params) {\n\treturn _composeControls->pushTabbedSelectorToThirdSection(\n\t\tthread,\n\t\tparams);\n}\n\nbool ChatWidget::returnTabbedSelector() {\n\treturn _composeControls->returnTabbedSelector();\n}\n\nstd::shared_ptr<Window::SectionMemento> ChatWidget::createMemento() {\n\tauto result = std::make_shared<ChatMemento>(_id);\n\tsaveState(result.get());\n\treturn result;\n}\n\nbool ChatWidget::showMessage(\n\t\tPeerId peerId,\n\t\tconst Window::SectionShow &params,\n\t\tMsgId messageId) {\n\tif (peerId != _peer->id) {\n\t\treturn false;\n\t}\n\tconst auto id = FullMsgId(_peer->id, messageId);\n\tconst auto message = _history->owner().message(id);\n\tif (!message) {\n\t\treturn false;\n\t} else if (_repliesRootId\n\t\t&& !message->inThread(_repliesRootId)\n\t\t&& id.msg != _repliesRootId) {\n\t\treturn false;\n\t} else if (_sublist && message->savedSublist() != _sublist) {\n\t\treturn false;\n\t}\n\tconst auto originMessage = [&]() -> HistoryItem* {\n\t\tusing OriginMessage = Window::SectionShow::OriginMessage;\n\t\tif (const auto origin = std::get_if<OriginMessage>(&params.origin)) {\n\t\t\tif (const auto returnTo = session().data().message(origin->id)) {\n\t\t\t\tif (returnTo->history() != _history) {\n\t\t\t\t\treturn nullptr;\n\t\t\t\t} else if (_repliesRootId\n\t\t\t\t\t&& returnTo->inThread(_repliesRootId)) {\n\t\t\t\t\treturn returnTo;\n\t\t\t\t} else if (_sublist\n\t\t\t\t\t&& returnTo->savedSublist() == _sublist) {\n\t\t\t\t\treturn returnTo;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t}();\n\tconst auto currentReplyReturn = _cornerButtons.replyReturn();\n\tconst auto originItemId = !originMessage\n\t\t? FullMsgId()\n\t\t: (currentReplyReturn != originMessage)\n\t\t? originMessage->fullId()\n\t\t: FullMsgId();\n\tshowAtPosition(message->position(), originItemId, params);\n\treturn true;\n}\n\nWindow::SectionActionResult ChatWidget::sendBotCommand(\n\t\tBot::SendCommandRequest request) {\n\tif (!_repliesRootId) {\n\t\treturn Window::SectionActionResult::Fallback;\n\t} else if (request.peer != _peer) {\n\t\treturn Window::SectionActionResult::Ignore;\n\t}\n\tlistSendBotCommand(request.command, request.context);\n\treturn Window::SectionActionResult::Handle;\n}\n\nbool ChatWidget::confirmSendingFiles(const QStringList &files) {\n\treturn confirmSendingFiles(files, QString());\n}\n\nbool ChatWidget::confirmSendingFiles(not_null<const QMimeData*> data) {\n\treturn confirmSendingFiles(data, std::nullopt);\n}\n\nbool ChatWidget::confirmSendingFiles(\n\t\tconst QStringList &files,\n\t\tconst QString &insertTextOnCancel) {\n\tconst auto premium = controller()->session().user()->isPremium();\n\treturn confirmSendingFiles(\n\t\tStorage::PrepareMediaList(files, st::sendMediaPreviewSize, premium),\n\t\tinsertTextOnCancel);\n}\n\nvoid ChatWidget::replyToMessage(FullReplyTo id) {\n\t_composeControls->replyToMessage(std::move(id));\n\trefreshTopBarActiveChat();\n}\n\nvoid ChatWidget::saveState(not_null<ChatMemento*> memento) {\n\tmemento->setReplies(_replies);\n\tmemento->setReplyReturns(_cornerButtons.replyReturns());\n\t_inner->saveState(memento->list());\n}\n\nvoid ChatWidget::refreshReplies() {\n\tif (!_repliesRootId) {\n\t\treturn;\n\t}\n\tauto old = base::take(_replies);\n\tsetReplies(_topic\n\t\t? _topic->replies()\n\t\t: std::make_shared<Data::RepliesList>(_history, _repliesRootId));\n\tif (old) {\n\t\t_inner->refreshViewer();\n\t}\n}\n\nvoid ChatWidget::setReplies(std::shared_ptr<Data::RepliesList> replies) {\n\t_replies = std::move(replies);\n\t_repliesLifetime.destroy();\n\n\t_replies->unreadCountValue(\n\t) | rpl::on_next([=](std::optional<int> count) {\n\t\trefreshUnreadCountBadge(count);\n\t}, lifetime());\n\n\tunreadCountUpdated();\n\n\tconst auto isTopic = (_topic != nullptr);\n\tconst auto isTopicCreating = isTopic && _topic->creating();\n\trpl::combine(\n\t\trpl::single(\n\t\t\tstd::optional<int>()\n\t\t) | rpl::then(_replies->maybeFullCount()),\n\t\t_areComments.value()\n\t) | rpl::map([=](std::optional<int> count, bool areComments) {\n\t\tconst auto sub = isTopic ? 1 : 0;\n\t\treturn (count && (*count > sub))\n\t\t\t? (isTopic\n\t\t\t\t? tr::lng_forum_messages\n\t\t\t\t: areComments\n\t\t\t\t? tr::lng_comments_header\n\t\t\t\t: tr::lng_replies_header)(\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\trpl::single(*count - sub) | tr::to_count())\n\t\t\t: (isTopic\n\t\t\t\t? ((count.has_value() || isTopicCreating)\n\t\t\t\t\t? tr::lng_forum_no_messages\n\t\t\t\t\t: tr::lng_contacts_loading)\n\t\t\t\t: areComments\n\t\t\t\t? tr::lng_comments_header_none\n\t\t\t\t: tr::lng_replies_header_none)();\n\t}) | rpl::flatten_latest(\n\t) | rpl::on_next([=](const QString &text) {\n\t\t_topBar->setCustomTitle(text);\n\t}, _repliesLifetime);\n}\n\nvoid ChatWidget::subscribeToSublist() {\n\tExpects(_sublist != nullptr);\n\n\t// Must be done before unreadCountUpdated(), or we auto-close.\n\tif (_sublist->unreadMark()) {\n\t\t_sublist->owner().histories().changeSublistUnreadMark(\n\t\t\t_sublist,\n\t\t\tfalse);\n\t}\n\n\t_sublist->unreadCountValue(\n\t) | rpl::on_next([=](std::optional<int> count) {\n\t\trefreshUnreadCountBadge(count);\n\t}, lifetime());\n\n\tusing Flag = Data::SublistUpdate::Flag;\n\tsession().changes().sublistUpdates(\n\t\t_sublist,\n\t\t(Flag::UnreadView\n\t\t\t| Flag::UnreadReactions\n\t\t\t| Flag::UnreadPollVotes\n\t\t\t| Flag::CloudDraft)\n\t) | rpl::on_next([=](const Data::SublistUpdate &update) {\n\t\tif (update.flags & Flag::UnreadView) {\n\t\t\tunreadCountUpdated();\n\t\t}\n\t\tif (update.flags\n\t\t\t& (Flag::UnreadReactions | Flag::UnreadPollVotes)) {\n\t\t\t_cornerButtons.updateUnreadThingsVisibility();\n\t\t}\n\t\tif (update.flags & Flag::CloudDraft) {\n\t\t\t_composeControls->applyCloudDraft();\n\t\t}\n\t}, lifetime());\n\n\t_sublist->destroyed(\n\t) | rpl::on_next([=] {\n\t\tcloseCurrent();\n\t}, lifetime());\n\n\tunreadCountUpdated();\n\tsubscribeToPinnedMessages();\n}\n\nvoid ChatWidget::unreadCountUpdated() {\n\tif (_sublist && _sublist->unreadMark()) {\n\t\tcrl::on_main(this, [=] {\n\t\t\tconst auto guard = base::make_weak(this);\n\t\t\tcontroller()->showPeerHistory(_sublist->owningHistory());\n\t\t\tif (guard) {\n\t\t\t\tcloseCurrent();\n\t\t\t}\n\t\t});\n\t} else {\n\t\trefreshUnreadCountBadge(_replies\n\t\t\t? (_replies->unreadCountKnown()\n\t\t\t\t? _replies->unreadCountCurrent()\n\t\t\t\t: std::optional<int>())\n\t\t\t: _sublist\n\t\t\t? (_sublist->unreadCountKnown()\n\t\t\t\t? _sublist->unreadCountCurrent()\n\t\t\t\t: std::optional<int>())\n\t\t\t: std::optional<int>());\n\t}\n}\n\nvoid ChatWidget::restoreState(not_null<ChatMemento*> memento) {\n\tif (auto replies = memento->getReplies()) {\n\t\tsetReplies(std::move(replies));\n\t} else if (!_replies && _repliesRootId) {\n\t\trefreshReplies();\n\t}\n\t_cornerButtons.setReplyReturns(memento->replyReturns());\n\t_inner->restoreState(memento->list());\n\tif (const auto highlight = memento->highlightId()) {\n\t\tauto params = Window::SectionShow(\n\t\t\tWindow::SectionShow::Way::Forward,\n\t\t\tanim::type::instant);\n\t\tparams.highlight = memento->highlight();\n\t\tshowAtPosition(Data::MessagePosition{\n\t\t\t.fullId = FullMsgId(_peer->id, highlight),\n\t\t\t.date = TimeId(0),\n\t\t}, {}, params);\n\t}\n}\n\nvoid ChatWidget::resizeEvent(QResizeEvent *e) {\n\tif (!width() || !height()) {\n\t\treturn;\n\t}\n\t_composeControls->resizeToWidth(width());\n\trecountChatWidth();\n\tupdateControlsGeometry();\n}\n\nvoid ChatWidget::recountChatWidth() {\n\tauto layout = (width() < st::adaptiveChatWideWidth)\n\t\t? Window::Adaptive::ChatLayout::Normal\n\t\t: Window::Adaptive::ChatLayout::Wide;\n\tcontroller()->adaptive().setChatLayout(layout);\n}\n\nvoid ChatWidget::updateControlsGeometry() {\n\tconst auto contentWidth = width();\n\n\tconst auto newScrollDelta = _scroll->isHidden()\n\t\t? std::nullopt\n\t\t: _scroll->scrollTop()\n\t\t? base::make_optional(topDelta() + _scrollTopDelta)\n\t\t: 0;\n\t_topBar->resizeToWidth(contentWidth);\n\t_topBarShadow->resize(contentWidth, st::lineWidth);\n\tconst auto tabsLeftSkip = _subsectionTabs\n\t\t? _subsectionTabs->leftSkip()\n\t\t: 0;\n\tconst auto tabsBottomSkip = _subsectionTabs\n\t\t? _subsectionTabs->bottomSkip()\n\t\t: 0;\n\tconst auto innerWidth = contentWidth - tabsLeftSkip;\n\tconst auto subsectionTabsTop = _topBar->bottomNoMargins();\n\t_topBars->move(tabsLeftSkip, subsectionTabsTop\n\t\t+ (_subsectionTabs ? _subsectionTabs->topSkip() : 0));\n\tif (_repliesRootView) {\n\t\t_repliesRootView->resizeToWidth(innerWidth);\n\t}\n\tauto top = _repliesRootViewHeight;\n\tif (_pinnedBar) {\n\t\t_pinnedBar->move(0, top);\n\t\t_pinnedBar->resizeToWidth(innerWidth);\n\t\ttop += _pinnedBarHeight;\n\t}\n\tif (_topicReopenBar) {\n\t\t_topicReopenBar->bar().move(0, top);\n\t\ttop += _topicReopenBar->bar().height();\n\t}\n\t_translateBar->move(0, top);\n\t_translateBar->resizeToWidth(innerWidth);\n\ttop += _translateBarHeight;\n\n\tauto bottom = height();\n\tif (_openChatButton) {\n\t\t_openChatButton->resizeToWidth(width());\n\t\tbottom -= _openChatButton->height();\n\t\t_openChatButton->move(0, bottom);\n\t} else if (_aboutHiddenAuthor) {\n\t\t_aboutHiddenAuthor->resize(width(), st::historyUnblock.height);\n\t\tbottom -= _aboutHiddenAuthor->height();\n\t\t_aboutHiddenAuthor->move(0, bottom);\n\t} else if (_joinGroup) {\n\t\t_joinGroup->resizeToWidth(width());\n\t\tbottom -= _joinGroup->height();\n\t\t_joinGroup->move(0, bottom);\n\t} else {\n\t\tbottom -= _composeControls->heightCurrent();\n\t}\n\tconst auto composeTop = bottom;\n\tbottom -= tabsBottomSkip;\n\n\t_topBars->resize(innerWidth, top + st::lineWidth);\n\ttop += _topBars->y();\n\n\tconst auto scrollHeight = bottom - top;\n\tconst auto scrollSize = QSize(innerWidth, scrollHeight);\n\tif (_scroll->size() != scrollSize) {\n\t\t_skipScrollEvent = true;\n\t\t_scroll->resize(scrollSize);\n\t\t_inner->resizeToWidth(scrollSize.width(), _scroll->height());\n\t\t_skipScrollEvent = false;\n\t}\n\t_scroll->move(tabsLeftSkip, top);\n\tif (!_scroll->isHidden()) {\n\t\tconst auto newScrollTop = (newScrollDelta && _scroll->scrollTop())\n\t\t\t? (_scroll->scrollTop() + *newScrollDelta)\n\t\t\t: std::optional<int>();\n\t\tif (newScrollTop) {\n\t\t\t_scroll->scrollToY(*newScrollTop);\n\t\t}\n\t\tupdateInnerVisibleArea();\n\t}\n\t_composeControls->move(0, composeTop);\n\t_composeControls->setAutocompleteBoundingRect(_scroll->geometry());\n\n\tif (_subsectionTabs) {\n\t\tconst auto scrollBottom = _scroll->y() + scrollHeight;\n\t\tconst auto areaHeight = scrollBottom\n\t\t\t+ tabsBottomSkip\n\t\t\t- subsectionTabsTop;\n\t\t_subsectionTabs->setBoundingRect(\n\t\t\t{ 0, subsectionTabsTop, width(), areaHeight });\n\t}\n\n\t_cornerButtons.updatePositions();\n}\n\nvoid ChatWidget::paintEvent(QPaintEvent *e) {\n\tif (animatingShow()) {\n\t\tSectionWidget::paintEvent(e);\n\t\treturn;\n\t} else if (controller()->contentOverlapped(this, e)) {\n\t\treturn;\n\t}\n\n\tconst auto aboveHeight = _topBar->height();\n\tconst auto bg = e->rect().intersected(\n\t\tQRect(0, aboveHeight, width(), height() - aboveHeight));\n\tSectionWidget::PaintBackground(controller(), _theme.get(), this, bg);\n}\n\nbool ChatWidget::emptyShown() const {\n\treturn _topic\n\t\t&& (_inner->isEmpty()\n\t\t\t|| (_topic->lastKnownServerMessageId() == _repliesRootId));\n}\n\nvoid ChatWidget::onScroll() {\n\tif (_skipScrollEvent) {\n\t\treturn;\n\t}\n\tupdateInnerVisibleArea();\n}\n\nvoid ChatWidget::updateInnerVisibleArea() {\n\tif (!_inner->animatedScrolling()) {\n\t\tcheckReplyReturns();\n\t}\n\tconst auto scrollTop = _scroll->scrollTop();\n\t_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());\n\tupdatePinnedVisibility();\n\tupdatePinnedViewer();\n\t_cornerButtons.updateJumpDownVisibility();\n\t_cornerButtons.updateUnreadThingsVisibility();\n\tif (_lastScrollTop != scrollTop) {\n\t\tif (!_synteticScrollEvent) {\n\t\t\tcheckLastPinnedClickedIdReset(_lastScrollTop, scrollTop);\n\t\t}\n\t\t_lastScrollTop = scrollTop;\n\t}\n}\n\nvoid ChatWidget::updatePinnedVisibility() {\n\tif (_sublist) {\n\t\tsetPinnedVisibility(true);\n\t\treturn;\n\t} else if (!_loaded || !_repliesRootId) {\n\t\treturn;\n\t} else if (!_topic && (!_repliesRoot || _repliesRoot->isEmpty())) {\n\t\tsetPinnedVisibility(!_repliesRoot);\n\t\treturn;\n\t}\n\tconst auto rootItem = [&] {\n\t\tif (const auto group = _history->owner().groups().find(_repliesRoot)) {\n\t\t\treturn group->items.front().get();\n\t\t}\n\t\treturn _repliesRoot;\n\t};\n\tconst auto view = _inner->viewByPosition(_topic\n\t\t? Data::MinMessagePosition\n\t\t: rootItem()->position());\n\tconst auto visible = !view\n\t\t|| (view->y() + view->height() <= _scroll->scrollTop());\n\tsetPinnedVisibility(visible || (_topic && !view->data()->isPinned()));\n}\n\nvoid ChatWidget::setPinnedVisibility(bool shown) {\n\tif (animatingShow()) {\n\t} else if (_sublist) {\n\t\t_repliesRootVisible = shown;\n\t} else if (!_repliesRootId) {\n\t\treturn;\n\t} else if (!_topic) {\n\t\tif (!_repliesRootViewInitScheduled) {\n\t\t\tconst auto height = shown ? st::historyReplyHeight : 0;\n\t\t\tif (const auto delta = height - _repliesRootViewHeight) {\n\t\t\t\t_repliesRootViewHeight = height;\n\t\t\t\tif (_scroll->scrollTop() == _scroll->scrollTopMax()) {\n\t\t\t\t\tsetGeometryWithTopMoved(geometry(), delta);\n\t\t\t\t} else {\n\t\t\t\t\tupdateControlsGeometry();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t_repliesRootVisible = shown;\n\t\tif (!_repliesRootViewInited) {\n\t\t\t_repliesRootView->finishAnimating();\n\t\t\tif (!_repliesRootViewInitScheduled) {\n\t\t\t\t_repliesRootViewInitScheduled = true;\n\t\t\t\tInvokeQueued(this, [=] {\n\t\t\t\t\t_repliesRootViewInited = true;\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t} else {\n\t\t_repliesRootVisible = shown;\n\t}\n}\n\nvoid ChatWidget::showAnimatedHook(\n\t\tconst Window::SectionSlideParams &params) {\n\t_topBar->setAnimatingMode(true);\n\tif (params.withTopBarShadow) {\n\t\t_topBarShadow->show();\n\t}\n\t_composeControls->showStarted();\n}\n\nvoid ChatWidget::showFinishedHook() {\n\t_topBar->setAnimatingMode(false);\n\tif (_joinGroup || _openChatButton || _aboutHiddenAuthor) {\n\t\tif (Ui::InFocusChain(this)) {\n\t\t\t_inner->setFocus();\n\t\t}\n\t\t_composeControls->hide();\n\t} else {\n\t\t_composeControls->showFinished();\n\t}\n\t_inner->showFinished();\n\t_topBars->show();\n\tif (_subsectionTabs) {\n\t\t_subsectionTabs->show();\n\t}\n\n\t// We should setup the drag area only after\n\t// the section animation is finished,\n\t// because after that the method showChildren() is called.\n\tsetupDragArea();\n\tupdatePinnedVisibility();\n\n\tif (_topic) {\n\t\t_topic->saveMeAsActiveSubsectionThread();\n\t} else if (_sublist) {\n\t\t_sublist->saveMeAsActiveSubsectionThread();\n\t}\n}\n\nbool ChatWidget::floatPlayerHandleWheelEvent(QEvent *e) {\n\treturn _scroll->viewportEvent(e);\n}\n\nQRect ChatWidget::floatPlayerAvailableRect() {\n\treturn mapToGlobal(_scroll->geometry());\n}\n\nContext ChatWidget::listContext() {\n\treturn !_sublist\n\t\t? Context::Replies\n\t\t: _sublist->parentChat()\n\t\t? Context::Monoforum\n\t\t: Context::SavedSublist;\n}\n\nbool ChatWidget::listScrollTo(int top, bool syntetic) {\n\ttop = std::clamp(top, 0, _scroll->scrollTopMax());\n\tconst auto scrolled = (_scroll->scrollTop() != top);\n\t_synteticScrollEvent = syntetic;\n\tif (scrolled) {\n\t\t_scroll->scrollToY(top);\n\t} else if (syntetic) {\n\t\tupdateInnerVisibleArea();\n\t}\n\t_synteticScrollEvent = false;\n\treturn scrolled;\n}\n\nvoid ChatWidget::listCancelRequest() {\n\tif (_composeSearch) {\n\t\tif (_inner &&\n\t\t\t(!_inner->getSelectedItems().empty()\n\t\t\t\t|| !_inner->getSelectedText().rich.text.isEmpty())) {\n\t\t\tclearSelected();\n\t\t} else {\n\t\t\t_composeSearch->hideAnimated();\n\t\t}\n\t\treturn;\n\t}\n\tif (_inner && !_inner->getSelectedItems().empty()) {\n\t\tclearSelected();\n\t\treturn;\n\t} else if (_composeControls->handleCancelRequest()) {\n\t\trefreshTopBarActiveChat();\n\t\treturn;\n\t}\n\tcontroller()->showBackFromStack();\n}\n\nvoid ChatWidget::listDeleteRequest() {\n\tconfirmDeleteSelected();\n}\n\nvoid ChatWidget::listTryProcessKeyInput(not_null<QKeyEvent*> e) {\n\t_composeControls->tryProcessKeyInput(e);\n}\n\nvoid ChatWidget::markLoaded() {\n\tif (!_loaded) {\n\t\t_loaded = true;\n\t\tcrl::on_main(this, [=] {\n\t\t\tupdatePinnedVisibility();\n\t\t});\n\t}\n}\n\nrpl::producer<Data::MessagesSlice> ChatWidget::listSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tif (_replies) {\n\t\treturn repliesSource(aroundId, limitBefore, limitAfter);\n\t} else if (_sublist) {\n\t\treturn sublistSource(aroundId, limitBefore, limitAfter);\n\t}\n\tUnexpected(\"ChatWidget::listSource in unknown mode\");\n}\n\nrpl::producer<Data::MessagesSlice> ChatWidget::repliesSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\treturn _replies->source(\n\t\taroundId,\n\t\tlimitBefore,\n\t\tlimitAfter\n\t) | rpl::before_next([=] { // after_next makes a copy of value.\n\t\tmarkLoaded();\n\t});\n}\n\nrpl::producer<Data::MessagesSlice> ChatWidget::sublistSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\treturn _sublist->source(\n\t\taroundId,\n\t\tlimitBefore,\n\t\tlimitAfter\n\t) | rpl::before_next([=](const Data::MessagesSlice &result) {\n\t\t // after_next makes a copy of value.\n\t\t_topBar->setCustomTitle(!result.fullCount\n\t\t\t? tr::lng_contacts_loading(tr::now)\n\t\t\t: (_sublist->parentChat()\n\t\t\t\t? tr::lng_forum_messages\n\t\t\t\t: tr::lng_profile_saved_messages)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t*result.fullCount));\n\t\tmarkLoaded();\n\t});\n}\n\nbool ChatWidget::listAllowsMultiSelect() {\n\treturn true;\n}\n\nbool ChatWidget::listIsItemGoodForSelection(\n\t\tnot_null<HistoryItem*> item) {\n\treturn item->isRegular() && !item->isService();\n}\n\nbool ChatWidget::listIsLessInOrder(\n\t\tnot_null<HistoryItem*> first,\n\t\tnot_null<HistoryItem*> second) {\n\treturn _sublist\n\t\t? (first->id < second->id)\n\t\t: first->position() < second->position();\n}\n\nvoid ChatWidget::listSelectionChanged(SelectedItems &&items) {\n\tHistoryView::TopBarWidget::SelectedState state;\n\tstate.count = items.size();\n\tfor (const auto &item : items) {\n\t\tif (item.canDelete) {\n\t\t\t++state.canDeleteCount;\n\t\t}\n\t\tif (item.canForward) {\n\t\t\t++state.canForwardCount;\n\t\t}\n\t}\n\t_topBar->showSelected(state);\n\tif ((state.count > 0) && _composeSearch) {\n\t\t_composeSearch->hideAnimated();\n\t}\n\tif (items.empty()) {\n\t\tdoSetInnerFocus();\n\t}\n}\n\nvoid ChatWidget::listMarkReadTill(not_null<HistoryItem*> item) {\n\tif (_replies) {\n\t\t_replies->readTill(item);\n\t} else if (_sublist) {\n\t\t_sublist->readTill(item);\n\t}\n}\n\nvoid ChatWidget::listMarkContentsRead(\n\t\tconst base::flat_set<not_null<HistoryItem*>> &items) {\n\tsession().api().markContentsRead(items);\n}\n\nMessagesBarData ChatWidget::listMessagesBar(\n\t\tconst std::vector<not_null<Element*>> &elements,\n\t\tbool markLastAsRead) {\n\tif ((!_sublist && !_replies) || elements.empty()) {\n\t\treturn {};\n\t}\n\tconst auto till = _replies\n\t\t? _replies->computeInboxReadTillFull()\n\t\t: _sublist->computeInboxReadTillFull();\n\tconst auto hidden = (till < 2);\n\tfor (auto i = 0, count = int(elements.size()); i != count; ++i) {\n\t\tconst auto item = elements[i]->data();\n\t\tif (item->isRegular() && item->id > till) {\n\t\t\tif (markLastAsRead\n\t\t\t\t|| item->out()\n\t\t\t\t|| (_replies && !item->replyToId())) {\n\t\t\t\tif (markLastAsRead) {\n\t\t\t\t\tif (item->isUnreadMention() && !item->isUnreadMedia()) {\n\t\t\t\t\t\tsession().api().markContentsRead(item);\n\t\t\t\t\t}\n\t\t\t\t\titem->markClientSideAsRead();\n\t\t\t\t}\n\t\t\t\tif (_replies) {\n\t\t\t\t\t_replies->readTill(item);\n\t\t\t\t} else {\n\t\t\t\t\t_sublist->readTill(item);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn {\n\t\t\t\t\t.bar = {\n\t\t\t\t\t\t.element = elements[i],\n\t\t\t\t\t\t.hidden = hidden,\n\t\t\t\t\t\t.focus = true,\n\t\t\t\t\t},\n\t\t\t\t\t.text = tr::lng_unread_bar_some(),\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\treturn {};\n}\n\nvoid ChatWidget::listContentRefreshed() {\n}\n\nvoid ChatWidget::listUpdateDateLink(\n\t\tClickHandlerPtr &link,\n\t\tnot_null<Element*> view) {\n\tif (!_topic) {\n\t\tlink = nullptr;\n\t\treturn;\n\t}\n\tconst auto date = view->dateTime().date();\n\tif (!link) {\n\t\tlink = std::make_shared<Window::DateClickHandler>(_topic, date);\n\t} else {\n\t\tstatic_cast<Window::DateClickHandler*>(link.get())->setDate(date);\n\t}\n}\n\nbool ChatWidget::listElementHideReply(not_null<const Element*> view) {\n\tif (_sublist) {\n\t\treturn false;\n\t} else if (const auto reply = view->data()->Get<HistoryMessageReply>()) {\n\t\tconst auto replyToPeerId = reply->externalPeerId()\n\t\t\t? reply->externalPeerId()\n\t\t\t: _peer->id;\n\t\tif (reply->fields().manualQuote) {\n\t\t\treturn false;\n\t\t} else if (replyToPeerId == _peer->id) {\n\t\t\treturn (_repliesRootId && reply->messageId() == _repliesRootId);\n\t\t} else if (const auto root = _repliesRoot) {\n\t\t\tconst auto forwarded = root->Get<HistoryMessageForwarded>();\n\t\t\tif (forwarded\n\t\t\t\t&& forwarded->savedFromPeer\n\t\t\t\t&& forwarded->savedFromPeer->id == replyToPeerId\n\t\t\t\t&& forwarded->savedFromMsgId == reply->messageId()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nbool ChatWidget::listElementShownUnread(not_null<const Element*> view) {\n\tconst auto item = view->data();\n\treturn _replies\n\t\t? _replies->isServerSideUnread(item)\n\t\t: _sublist\n\t\t? _sublist->isServerSideUnread(item)\n\t\t: item->unread(item->history());\n}\n\nbool ChatWidget::listIsGoodForAroundPosition(\n\t\tnot_null<const Element*> view) {\n\treturn view->data()->isRegular();\n}\n\nvoid ChatWidget::listSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) {\n\tif (!_sublist || _sublist->parentChat()) {\n\t\tsendBotCommandWithOptions(command, context, {});\n\t}\n}\n\nvoid ChatWidget::sendBotCommandWithOptions(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context,\n\t\tApi::SendOptions options) {\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = options;\n\t\tcopy.starsApproved = approved;\n\t\tsendBotCommandWithOptions(command, context, copy);\n\t};\n\tconst auto checked = checkSendPayment(\n\t\t1,\n\t\toptions,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn;\n\t}\n\n\tconst auto text = Bot::WrapCommandInChat(\n\t\t_peer,\n\t\tcommand,\n\t\tcontext);\n\tauto message = Api::MessageToSend(prepareSendAction(options));\n\tmessage.textWithTags = { text };\n\tsession().api().sendMessage(std::move(message));\n\tfinishSending();\n}\n\nvoid ChatWidget::listSearch(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) {\n\tconst auto inChat = !_sublist\n\t\t? Dialogs::Key(_history)\n\t\t: Data::SearchTagFromQuery(query)\n\t\t? Dialogs::Key(_sublist)\n\t\t: Dialogs::Key();\n\tcontroller()->searchMessages(query, inChat);\n}\n\nvoid ChatWidget::listHandleViaClick(not_null<UserData*> bot) {\n\tif (_canSendTexts) {\n\t\t_composeControls->setText({ '@' + bot->username() + ' ' });\n\t}\n}\n\nnot_null<Ui::ChatTheme*> ChatWidget::listChatTheme() {\n\treturn _theme.get();\n}\n\nCopyRestrictionType ChatWidget::listCopyRestrictionType(\n\t\tHistoryItem *item) {\n\treturn CopyRestrictionTypeFor(_peer, item);\n}\n\nCopyRestrictionType ChatWidget::listCopyMediaRestrictionType(\n\t\tnot_null<HistoryItem*> item) {\n\treturn CopyMediaRestrictionTypeFor(_peer, item);\n}\n\nCopyRestrictionType ChatWidget::listSelectRestrictionType() {\n\treturn SelectRestrictionTypeFor(_peer);\n}\n\nauto ChatWidget::listAllowedReactionsValue()\n-> rpl::producer<Data::AllowedReactions> {\n\treturn Data::PeerAllowedReactionsValue(_peer);\n}\n\nvoid ChatWidget::listShowPremiumToast(not_null<DocumentData*> document) {\n\tif (!_stickerToast) {\n\t\t_stickerToast = std::make_unique<HistoryView::StickerToast>(\n\t\t\tcontroller(),\n\t\t\tthis,\n\t\t\t[=] { _stickerToast = nullptr; });\n\t}\n\t_stickerToast->showFor(document);\n}\n\nbool ChatWidget::handleDrawToReplyRequest(Data::DrawToReplyRequest request) {\n\tif (request.messageId.peer != _peer->id) {\n\t\treturn false;\n\t}\n\tauto image = ResolveDrawToReplyImage(&session().data(), request);\n\tif (image.isNull()) {\n\t\treturn false;\n\t}\n\tconst auto replyTo = request.messageId;\n\tOpenDrawToReplyEditor(\n\t\tcontroller(),\n\t\tstd::move(image),\n\t\tcrl::guard(this, [=](QImage &&result) {\n\t\t\tif (result.isNull()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (replyTo) {\n\t\t\t\treplyToMessage({ .messageId = replyTo });\n\t\t\t}\n\t\t\tauto list = Storage::PrepareMediaFromImage(\n\t\t\t\tstd::move(result),\n\t\t\t\tQByteArray(),\n\t\t\t\tst::sendMediaPreviewSize);\n\t\t\tconfirmSendingFiles(std::move(list));\n\t\t}));\n\treturn true;\n}\n\nvoid ChatWidget::listOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) {\n\tconst auto showDrawButton = _topic\n\t\t? Data::CanSendAnyOf(_topic, Data::FilesSendRestrictions())\n\t\t: Data::CanSendAnyOf(_peer, Data::FilesSendRestrictions());\n\tcontroller()->openPhoto(\n\t\tphoto,\n\t\t{ context, _repliesRootId, _monoforumPeerId, showDrawButton });\n}\n\nvoid ChatWidget::listOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView) {\n\tconst auto showDrawButton = _topic\n\t\t? Data::CanSendAnyOf(_topic, Data::FilesSendRestrictions())\n\t\t: Data::CanSendAnyOf(_peer, Data::FilesSendRestrictions());\n\tcontroller()->openDocument(\n\t\tdocument,\n\t\tshowInMediaView,\n\t\t{ context, _repliesRootId, _monoforumPeerId, showDrawButton });\n}\n\nvoid ChatWidget::listPaintEmpty(\n\t\tPainter &p,\n\t\tconst Ui::ChatPaintContext &context) {\n\tif (!emptyShown()) {\n\t\treturn;\n\t} else if (!_emptyPainter) {\n\t\tsetupEmptyPainter();\n\t}\n\t_emptyPainter->paint(p, context.st, width(), _scroll->height());\n}\n\nQString ChatWidget::listElementAuthorRank(not_null<const Element*> view) {\n\treturn (_topic && view->data()->from()->id == _topic->creatorId())\n\t\t? tr::lng_topic_author_badge(tr::now)\n\t\t: QString();\n}\n\nbool ChatWidget::listElementHideTopicButton(\n\t\tnot_null<const Element*> view) {\n\treturn true;\n}\n\nHistory *ChatWidget::listTranslateHistory() {\n\treturn _history;\n}\n\nvoid ChatWidget::listAddTranslatedItems(\n\t\tnot_null<TranslateTracker*> tracker) {\n\tif (_shownPinnedItem) {\n\t\ttracker->add(_shownPinnedItem);\n\t}\n}\n\nUi::ChatPaintContext ChatWidget::listPreparePaintContext(\n\t\tUi::ChatPaintContextArgs &&args) {\n\tauto context = WindowListDelegate::listPreparePaintContext(\n\t\tstd::move(args));\n\tcontext.gestureHorizontal = _gestureHorizontal;\n\treturn context;\n}\n\nbase::unique_qptr<Ui::PopupMenu> ChatWidget::listFillSenderUserpicMenu(\n\t\tPeerId userpicPeerId) {\n\tconst auto searchInEntry = _topic\n\t\t? Dialogs::Key(_topic)\n\t\t: Dialogs::Key(_history);\n\tauto menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\tst::popupMenuWithIcons);\n\tconst auto senderPeer = _history->owner().peer(userpicPeerId);\n\tconst auto groupPeer = (_history->peer->isChat()\n\t\t|| _history->peer->isMegagroup())\n\t\t? _history->peer.get()\n\t\t: nullptr;\n\tWindow::FillSenderUserpicMenu(\n\t\tcontroller(),\n\t\tsenderPeer,\n\t\tgroupPeer,\n\t\t_composeControls->fieldForMention(),\n\t\tsearchInEntry,\n\t\tUi::Menu::CreateAddActionCallback(menu.get()));\n\treturn menu->empty() ? nullptr : std::move(menu);\n}\n\nUi::ScrollArea *ChatWidget::listScrollArea() const {\n\treturn _scroll.get();\n}\n\nvoid ChatWidget::setupEmptyPainter() {\n\tExpects(_topic != nullptr);\n\n\t_emptyPainter = std::make_unique<EmptyPainter>(_topic, [=] {\n\t\treturn controller()->isGifPausedAtLeastFor(\n\t\t\tWindow::GifPauseReason::Any);\n\t}, [=] {\n\t\tif (emptyShown()) {\n\t\t\tupdate();\n\t\t} else {\n\t\t\t_emptyPainter = nullptr;\n\t\t}\n\t});\n}\n\nvoid ChatWidget::confirmDeleteSelected() {\n\tConfirmDeleteSelectedItems(_inner);\n}\n\nvoid ChatWidget::confirmForwardSelected() {\n\tConfirmForwardSelectedItems(_inner);\n}\n\nvoid ChatWidget::clearSelected() {\n\t_inner->cancelSelection();\n}\n\nvoid ChatWidget::setupDragArea() {\n\tconst auto filter = [=](const auto &d) {\n\t\tif (!_history || _composeControls->isRecording()) {\n\t\t\treturn false;\n\t\t}\n\t\treturn _topic\n\t\t\t? Data::CanSendAnyOf(_topic, Data::FilesSendRestrictions())\n\t\t\t: Data::CanSendAnyOf(_peer, Data::FilesSendRestrictions());\n\t};\n\tconst auto areas = DragArea::SetupDragAreaToContainer(\n\t\tthis,\n\t\tfilter,\n\t\tnullptr,\n\t\t[=] { updateControlsGeometry(); });\n\n\tconst auto droppedCallback = [=](bool overrideSendImagesAsPhotos) {\n\t\treturn [=](const QMimeData *data) {\n\t\t\tconfirmSendingFiles(data, overrideSendImagesAsPhotos);\n\t\t\tWindow::ActivateWindow(controller());\n\t\t};\n\t};\n\tareas.document->setDroppedCallback(droppedCallback(false));\n\tareas.photo->setDroppedCallback(droppedCallback(true));\n}\n\nvoid ChatWidget::setupShortcuts() {\n\tShortcuts::Requests(\n\t) | rpl::filter([=] {\n\t\treturn Ui::AppInFocus()\n\t\t\t&& Ui::InFocusChain(this)\n\t\t\t&& !controller()->isLayerShown()\n\t\t\t&& (Core::App().activeWindow() == &controller()->window());\n\t}) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {\n\t\tusing Command = Shortcuts::Command;\n\t\trequest->check(Command::Search, 1) && request->handle([=] {\n\t\t\tsearchRequested();\n\t\t\treturn true;\n\t\t});\n\t}, lifetime());\n}\n\nvoid ChatWidget::searchRequested() {\n\tif (_sublist) {\n\t\tcontroller()->searchInChat(_sublist);\n\t} else if (!preventsClose(crl::guard(this, [=] { searchInTopic(); }))) {\n\t\tsearchInTopic();\n\t}\n}\n\nvoid ChatWidget::searchInTopic() {\n\tif (_topic) {\n\t\tcontroller()->searchInChat(_topic);\n\t} else {\n\t\tconst auto update = [=] {\n\t\t\tif (_composeSearch) {\n\t\t\t\t_composeControls->hide();\n\t\t\t} else {\n\t\t\t\t_composeControls->show();\n\t\t\t}\n\t\t\tupdateControlsGeometry();\n\t\t};\n\t\tconst auto from = (PeerData*)(nullptr);\n\t\t_composeSearch = std::make_unique<HistoryView::ComposeSearch>(\n\t\t\tthis,\n\t\t\tcontroller(),\n\t\t\t_history,\n\t\t\tfrom);\n\t\t_composeSearch->setTopMsgId(_repliesRootId);\n\n\t\tupdate();\n\t\tdoSetInnerFocus();\n\n\t\tusing Activation = HistoryView::ComposeSearch::Activation;\n\t\t_composeSearch->activations(\n\t\t) | rpl::on_next([=](Activation activation) {\n\t\t\tshowAtPosition(activation.item->position());\n\t\t}, _composeSearch->lifetime());\n\n\t\t_composeSearch->destroyRequests(\n\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t_composeSearch = nullptr;\n\n\t\t\tupdate();\n\t\t\tdoSetInnerFocus();\n\t\t}, _composeSearch->lifetime());\n\t}\n}\n\nbool ChatWidget::searchInChatEmbedded(\n\t\tQString query,\n\t\tDialogs::Key chat,\n\t\tPeerData *searchFrom) {\n\tconst auto sublist = chat.sublist();\n\tif (!sublist || sublist != _sublist) {\n\t\treturn false;\n\t} else if (_composeSearch) {\n\t\t_composeSearch->setQuery(query);\n\t\t_composeSearch->setInnerFocus();\n\t\treturn true;\n\t}\n\t_composeSearch = std::make_unique<ComposeSearch>(\n\t\tthis,\n\t\tcontroller(),\n\t\t_history,\n\t\tsublist->sublistPeer(),\n\t\tquery);\n\t_composeSearch->setCalendarChat(Dialogs::Key(sublist));\n\n\tupdateControlsGeometry();\n\tsetInnerFocus();\n\n\t_composeSearch->activations(\n\t) | rpl::on_next([=](ComposeSearch::Activation activation) {\n\t\tconst auto item = activation.item;\n\t\tauto params = ::Window::SectionShow(\n\t\t\t::Window::SectionShow::Way::ClearStack);\n\t\tparams.highlight = Window::SearchHighlightId(activation.query);\n\t\tcontroller()->showPeerHistory(\n\t\t\titem->history()->peer->id,\n\t\t\tparams,\n\t\t\titem->fullId().msg);\n\t}, _composeSearch->lifetime());\n\n\t_composeSearch->destroyRequests(\n\t) | rpl::take(\n\t\t1\n\t) | rpl::on_next([=] {\n\t\t_composeSearch = nullptr;\n\n\t\tupdateControlsGeometry();\n\t\tsetInnerFocus();\n\t}, _composeSearch->lifetime());\n\n\treturn true;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_chat_section.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"window/section_widget.h\"\n#include \"window/section_memento.h\"\n#include \"history/view/history_view_corner_buttons.h\"\n#include \"history/view/history_view_list_widget.h\"\n#include \"history/history_item_helpers.h\"\n#include \"data/data_messages.h\"\n#include \"ui/controls/swipe_handler_data.h\"\n#include \"base/timer.h\"\n\nclass History;\nenum class SendMediaType;\nstruct SendingAlbum;\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nnamespace Api {\nstruct MessageToSend;\nstruct SendOptions;\nstruct SendAction;\n} // namespace Api\n\nnamespace Storage {\n} // namespace Storage\n\nnamespace Ui {\nclass ScrollArea;\nclass PlainShadow;\nclass FlatButton;\nclass PinnedBar;\nstruct PreparedList;\nstruct PreparedBundle;\nclass SendFilesWay;\n} // namespace Ui\n\nnamespace Profile {\nclass BackButton;\n} // namespace Profile\n\nnamespace InlineBots {\nclass Result;\n} // namespace InlineBots\n\nnamespace Data {\nclass RepliesList;\nclass ForumTopic;\nstruct DrawToReplyRequest;\n} // namespace Data\n\nnamespace HistoryView {\n\nnamespace Controls {\nstruct VoiceToSend;\n} // namespace Controls\n\nclass Element;\nclass TopBarWidget;\nclass ChatMemento;\nclass ComposeControls;\nclass ComposeSearch;\nclass SendActionPainter;\nclass StickerToast;\nclass TopicReopenBar;\nclass EmptyPainter;\nclass PinnedTracker;\nclass TranslateBar;\nclass SubsectionTabs;\nclass SelfForwardsTagger;\n\nstruct ChatViewId {\n\tnot_null<History*> history;\n\tMsgId repliesRootId;\n\tData::SavedSublist *sublist = nullptr;\n\n\tfriend inline bool operator==(ChatViewId, ChatViewId) = default;\n};\n\nclass ChatWidget final\n\t: public Window::SectionWidget\n\t, private WindowListDelegate\n\t, private CornerButtonsDelegate {\npublic:\n\tChatWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tChatViewId id);\n\t~ChatWidget();\n\n\t[[nodiscard]] ChatViewId id() const {\n\t\treturn _id;\n\t}\n\tDialogs::RowDescriptor activeChat() const override;\n\tbool preventsClose(Fn<void()> &&continueCallback) const override;\n\n\tbool hasTopBarShadow() const override {\n\t\treturn true;\n\t}\n\n\tQPixmap grabForShowAnimation(\n\t\tconst Window::SectionSlideParams &params) override;\n\n\tbool showInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) override;\n\tbool sameTypeAs(not_null<Window::SectionMemento*> memento) override;\n\tstd::shared_ptr<Window::SectionMemento> createMemento() override;\n\tbool showMessage(\n\t\tPeerId peerId,\n\t\tconst Window::SectionShow &params,\n\t\tMsgId messageId) override;\n\n\tWindow::SectionActionResult sendBotCommand(\n\t\tBot::SendCommandRequest request) override;\n\n\tbool searchInChatEmbedded(\n\t\tQString query,\n\t\tDialogs::Key chat,\n\t\tPeerData *searchFrom = nullptr) override;\n\n\tbool confirmSendingFiles(const QStringList &files) override;\n\tbool confirmSendingFiles(not_null<const QMimeData*> data) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<ChatMemento*> memento);\n\n\t// Tabbed selector management.\n\tbool pushTabbedSelectorToThirdSection(\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst Window::SectionShow &params) override;\n\tbool returnTabbedSelector() override;\n\n\t// Float player interface.\n\tbool floatPlayerHandleWheelEvent(QEvent *e) override;\n\tQRect floatPlayerAvailableRect() override;\n\n\t// ListDelegate interface.\n\tContext listContext() override;\n\tbool listScrollTo(int top, bool syntetic = true) override;\n\tvoid listCancelRequest() override;\n\tvoid listDeleteRequest() override;\n\tvoid listTryProcessKeyInput(not_null<QKeyEvent*> e) override;\n\trpl::producer<Data::MessagesSlice> listSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) override;\n\tbool listAllowsMultiSelect() override;\n\tbool listIsItemGoodForSelection(not_null<HistoryItem*> item) override;\n\tbool listIsLessInOrder(\n\t\tnot_null<HistoryItem*> first,\n\t\tnot_null<HistoryItem*> second) override;\n\tvoid listSelectionChanged(SelectedItems &&items) override;\n\tvoid listMarkReadTill(not_null<HistoryItem*> item) override;\n\tvoid listMarkContentsRead(\n\t\tconst base::flat_set<not_null<HistoryItem*>> &items) override;\n\tMessagesBarData listMessagesBar(\n\t\tconst std::vector<not_null<Element*>> &elements,\n\t\tbool markLastAsRead) override;\n\tvoid listContentRefreshed() override;\n\tvoid listUpdateDateLink(\n\t\tClickHandlerPtr &link,\n\t\tnot_null<Element*> view) override;\n\tbool listElementHideReply(not_null<const Element*> view) override;\n\tbool listElementShownUnread(not_null<const Element*> view) override;\n\tbool listIsGoodForAroundPosition(not_null<const Element*> view) override;\n\tvoid listSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) override;\n\tvoid listSearch(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) override;\n\tvoid listHandleViaClick(not_null<UserData*> bot) override;\n\tnot_null<Ui::ChatTheme*> listChatTheme() override;\n\tCopyRestrictionType listCopyRestrictionType(HistoryItem *item) override;\n\tCopyRestrictionType listCopyMediaRestrictionType(\n\t\tnot_null<HistoryItem*> item) override;\n\tCopyRestrictionType listSelectRestrictionType() override;\n\tauto listAllowedReactionsValue()\n\t\t->rpl::producer<Data::AllowedReactions> override;\n\tvoid listShowPremiumToast(not_null<DocumentData*> document) override;\n\tbool handleDrawToReplyRequest(Data::DrawToReplyRequest request);\n\tvoid listOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) override;\n\tvoid listOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView) override;\n\tvoid listPaintEmpty(\n\t\tPainter &p,\n\t\tconst Ui::ChatPaintContext &context) override;\n\tQString listElementAuthorRank(not_null<const Element*> view) override;\n\tbool listElementHideTopicButton(not_null<const Element*> view) override;\n\tHistory *listTranslateHistory() override;\n\tvoid listAddTranslatedItems(\n\t\tnot_null<TranslateTracker*> tracker) override;\n\tUi::ChatPaintContext listPreparePaintContext(\n\t\tUi::ChatPaintContextArgs &&args) override;\n\tbase::unique_qptr<Ui::PopupMenu> listFillSenderUserpicMenu(\n\t\tPeerId userpicPeerId) override;\n\tUi::ScrollArea *listScrollArea() const override;\n\n\t// CornerButtonsDelegate delegate.\n\tvoid cornerButtonsShowAtPosition(\n\t\tData::MessagePosition position) override;\n\tData::Thread *cornerButtonsThread() override;\n\tFullMsgId cornerButtonsCurrentId() override;\n\tbool cornerButtonsIgnoreVisibility() override;\n\tstd::optional<bool> cornerButtonsDownShown() override;\n\tbool cornerButtonsUnreadMayBeShown() override;\n\tbool cornerButtonsHas(CornerButtonType type) override;\n\nprivate:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid showAnimatedHook(\n\t\tconst Window::SectionSlideParams &params) override;\n\tvoid showFinishedHook() override;\n\tvoid checkActivation() override;\n\tvoid doSetInnerFocus() override;\n\n\t[[nodiscard]] bool checkSendPayment(\n\t\tint messagesCount,\n\t\tApi::SendOptions options,\n\t\tFn<void(int)> withPaymentApproved);\n\n\tvoid markLoaded();\n\t[[nodiscard]] rpl::producer<Data::MessagesSlice> repliesSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter);\n\t[[nodiscard]] rpl::producer<Data::MessagesSlice> sublistSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter);\n\n\tvoid onScroll();\n\tvoid closeCurrent();\n\tvoid unreadCountUpdated();\n\tvoid updateInnerVisibleArea();\n\tvoid updateControlsGeometry();\n\tvoid updateAdaptiveLayout();\n\tvoid saveState(not_null<ChatMemento*> memento);\n\tvoid restoreState(not_null<ChatMemento*> memento);\n\tvoid setReplies(std::shared_ptr<Data::RepliesList> replies);\n\tvoid refreshReplies();\n\tvoid showAtStart();\n\tvoid showAtEnd();\n\tvoid showAtPosition(\n\t\tData::MessagePosition position,\n\t\tFullMsgId originItemId = {});\n\tvoid showAtPosition(\n\t\tData::MessagePosition position,\n\t\tFullMsgId originItemId,\n\t\tconst Window::SectionShow &params);\n\tvoid finishSending();\n\n\tvoid setupComposeControls();\n\tvoid setupSwipeReplyAndBack();\n\n\tvoid setupRoot();\n\tvoid setupRootView();\n\tvoid setupTopicViewer();\n\tvoid subscribeToTopic();\n\tvoid subscribeToSublist();\n\tvoid subscribeToPinnedMessages();\n\tvoid setTopic(Data::ForumTopic *topic);\n\n\tvoid setupOpenChatButton();\n\tvoid setupAboutHiddenAuthor();\n\n\tvoid setupDragArea();\n\tvoid setupShortcuts();\n\tvoid setupTranslateBar();\n\n\tvoid searchRequested();\n\tvoid searchInTopic();\n\tvoid updatePinnedVisibility();\n\n\tvoid confirmDeleteSelected();\n\tvoid confirmForwardSelected();\n\tvoid clearSelected();\n\tvoid setPinnedVisibility(bool shown);\n\n\t[[nodiscard]] Api::SendAction prepareSendAction(\n\t\tApi::SendOptions options) const;\n\tvoid sendTextWithTags(\n\t\tTextWithTags textWithTags,\n\t\tbool useCurrentWebPageDraft,\n\t\tApi::SendOptions options,\n\t\tFn<void()> done);\n\tvoid sendWithTextOverride(\n\t\tTextWithEntities text,\n\t\tApi::SendOptions options,\n\t\tFn<void()> done);\n\tvoid send();\n\tvoid send(Api::SendOptions options);\n\tvoid sendVoice(const Controls::VoiceToSend &data);\n\tvoid edit(\n\t\tnot_null<HistoryItem*> item,\n\t\tApi::SendOptions options,\n\t\tmtpRequestId *const saveEditMsgRequestId,\n\t\tbool spoilered);\n\tvoid chooseAttach(std::optional<bool> overrideSendImagesAsPhotos);\n\t[[nodiscard]] SendMenu::Details sendMenuDetails() const;\n\t[[nodiscard]] FullReplyTo replyTo() const;\n\t[[nodiscard]] HistoryItem *lookupRepliesRoot() const;\n\t[[nodiscard]] Data::ForumTopic *lookupTopic();\n\t[[nodiscard]] bool computeAreComments() const;\n\tvoid orderWidgets();\n\n\tvoid pushReplyReturn(not_null<HistoryItem*> item);\n\tvoid checkReplyReturns();\n\tvoid recountChatWidth();\n\tvoid replyToMessage(FullReplyTo id);\n\tvoid refreshTopBarActiveChat();\n\tvoid refreshUnreadCountBadge(std::optional<int> count);\n\n\tvoid hidePinnedMessage();\n\tvoid updatePinnedViewer();\n\tvoid setupPinnedTracker();\n\tvoid checkPinnedBarState();\n\tvoid clearHidingPinnedBar();\n\tvoid refreshPinnedBarButton(bool many, HistoryItem *item);\n\tvoid checkLastPinnedClickedIdReset(\n\t\tint wasScrollTop,\n\t\tint nowScrollTop);\n\n\tvoid uploadFile(const QByteArray &fileContent, SendMediaType type);\n\tbool confirmSendingFiles(\n\t\tQImage &&image,\n\t\tQByteArray &&content,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos = std::nullopt,\n\t\tconst QString &insertTextOnCancel = QString());\n\tbool confirmSendingFiles(\n\t\tconst QStringList &files,\n\t\tconst QString &insertTextOnCancel);\n\tbool confirmSendingFiles(\n\t\tUi::PreparedList &&list,\n\t\tconst QString &insertTextOnCancel = QString());\n\tbool confirmSendingFiles(\n\t\tnot_null<const QMimeData*> data,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos,\n\t\tconst QString &insertTextOnCancel = QString());\n\tbool showSendingFilesError(const Ui::PreparedList &list) const;\n\tbool showSendingFilesError(const Ui::PreparedBundle &bundle) const;\n\n\tvoid sendingFilesConfirmed(\n\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\tApi::SendOptions options);\n\n\tvoid sendBotCommandWithOptions(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context,\n\t\tApi::SendOptions options);\n\n\tbool sendExistingDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tApi::MessageToSend messageToSend,\n\t\tstd::optional<MsgId> localId);\n\tvoid sendExistingPhoto(not_null<PhotoData*> photo);\n\tbool sendExistingPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tApi::SendOptions options);\n\tvoid sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot);\n\tvoid sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot,\n\t\tApi::SendOptions options,\n\t\tstd::optional<MsgId> localMessageId);\n\n\tvoid validateSubsectionTabs() override;\n\tvoid setupEmptyPainter();\n\tvoid refreshJoinGroupButton();\n\t[[nodiscard]] bool emptyShown() const;\n\t[[nodiscard]] bool showSlowmodeError();\n\n\tconst not_null<History*> _history;\n\tconst not_null<PeerData*> _peer;\n\tChatViewId _id;\n\n\tMsgId _repliesRootId = 0;\n\tHistoryItem *_repliesRoot = nullptr;\n\tData::ForumTopic *_topic = nullptr;\n\tmutable bool _newTopicDiscarded = false;\n\tstd::shared_ptr<Data::RepliesList> _replies;\n\trpl::lifetime _repliesLifetime;\n\trpl::variable<bool> _areComments = false;\n\n\tData::SavedSublist *_sublist = nullptr;\n\tPeerId _monoforumPeerId;\n\n\tstd::shared_ptr<SendActionPainter> _sendAction;\n\tstd::shared_ptr<Ui::ChatTheme> _theme;\n\tQPointer<ListWidget> _inner;\n\tobject_ptr<TopBarWidget> _topBar;\n\tobject_ptr<Ui::PlainShadow> _topBarShadow;\n\tstd::unique_ptr<Ui::RpWidget> _topBars;\n\tstd::unique_ptr<ComposeControls> _composeControls;\n\tstd::unique_ptr<ComposeSearch> _composeSearch;\n\tstd::unique_ptr<Ui::FlatButton> _joinGroup;\n\tstd::unique_ptr<Ui::FlatButton> _payForMessage;\n\tstd::unique_ptr<TopicReopenBar> _topicReopenBar;\n\tstd::unique_ptr<Ui::FlatButton> _openChatButton;\n\tstd::unique_ptr<Ui::RpWidget> _aboutHiddenAuthor;\n\tstd::unique_ptr<EmptyPainter> _emptyPainter;\n\tstd::unique_ptr<SubsectionTabs> _subsectionTabs;\n\trpl::lifetime _subsectionTabsLifetime;\n\trpl::lifetime _subsectionCheckLifetime;\n\tbool _canSendTexts = false;\n\tbool _skipScrollEvent = false;\n\tbool _synteticScrollEvent = false;\n\n\tstd::unique_ptr<TranslateBar> _translateBar;\n\tint _translateBarHeight = 0;\n\n\tstd::unique_ptr<PinnedTracker> _pinnedTracker;\n\tstd::unique_ptr<Ui::PinnedBar> _pinnedBar;\n\tstd::unique_ptr<Ui::PinnedBar> _hidingPinnedBar;\n\tint _pinnedBarHeight = 0;\n\tFullMsgId _pinnedClickedId;\n\tstd::optional<FullMsgId> _minPinnedId;\n\tHistoryItem *_shownPinnedItem = nullptr;\n\n\tstd::unique_ptr<Ui::PinnedBar> _repliesRootView;\n\tint _repliesRootViewHeight = 0;\n\tbool _repliesRootViewInited = false;\n\tbool _repliesRootViewInitScheduled = false;\n\trpl::variable<bool> _repliesRootVisible = false;\n\n\tstd::unique_ptr<Ui::ScrollArea> _scroll;\n\tstd::unique_ptr<HistoryView::StickerToast> _stickerToast;\n\n\tFullMsgId _lastShownAt;\n\tHistoryView::CornerButtons _cornerButtons;\n\trpl::lifetime _topicLifetime;\n\n\tUi::Controls::SwipeContextData _gestureHorizontal;\n\tUi::Controls::SwipeBackResult _swipeBackData;\n\n\tSendPaymentHelper _sendPayment;\n\n\tint _lastScrollTop = 0;\n\tint _topicReopenBarHeight = 0;\n\tint _scrollTopDelta = 0;\n\n\tbool _choosingAttach = false;\n\n\tbool _loaded = false;\n\n\tstd::unique_ptr<HistoryView::SelfForwardsTagger> _selfForwardsTagger;\n\n};\n\nclass ChatMemento final : public Window::SectionMemento {\npublic:\n\texplicit ChatMemento(\n\t\tChatViewId id,\n\t\tMsgId highlightId = 0,\n\t\tMessageHighlightId highlight = {});\n\n\tstruct Comments {\n\t};\n\texplicit ChatMemento(\n\t\tComments,\n\t\tnot_null<HistoryItem*> commentsItem,\n\t\tMsgId commentId = 0);\n\n\tvoid setReadInformation(\n\t\tMsgId inboxReadTillId,\n\t\tint unreadCount,\n\t\tMsgId outboxReadTillId);\n\n\tobject_ptr<Window::SectionWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tWindow::Column column,\n\t\tconst QRect &geometry) override;\n\n\t[[nodiscard]] ChatViewId id() const {\n\t\treturn _id;\n\t}\n\n\tvoid setReplies(std::shared_ptr<Data::RepliesList> replies) {\n\t\t_replies = std::move(replies);\n\t}\n\t[[nodiscard]] std::shared_ptr<Data::RepliesList> getReplies() const {\n\t\treturn _replies;\n\t}\n\n\tvoid setFromTopic(not_null<Data::ForumTopic*> topic);\n\n\tvoid setReplyReturns(const QVector<FullMsgId> &list) {\n\t\t_replyReturns = list;\n\t}\n\tconst QVector<FullMsgId> &replyReturns() const {\n\t\treturn _replyReturns;\n\t}\n\n\tData::ForumTopic *topicForRemoveRequests() const override;\n\tData::SavedSublist *sublistForRemoveRequests() const override;\n\n\t[[nodiscard]] not_null<ListMemento*> list() {\n\t\treturn &_list;\n\t}\n\t[[nodiscard]] MsgId highlightId() const {\n\t\treturn _highlightId;\n\t}\n\t[[nodiscard]] const MessageHighlightId &highlight() const {\n\t\treturn _highlight;\n\t}\n\nprivate:\n\tvoid setupTopicViewer();\n\n\tChatViewId _id;\n\tconst MsgId _highlightId = 0;\n\tconst MessageHighlightId _highlight;\n\tListMemento _list;\n\tstd::shared_ptr<Data::RepliesList> _replies;\n\tQVector<FullMsgId> _replyReturns;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_contact_status.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_contact_status.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/text/format_values.h\" // Ui::FormatPhone\n#include \"ui/text/text_utilities.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"chat_helpers/message_field.h\" // PaidSendButtonText\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\"\n#include \"history/history.h\"\n#include \"data/business/data_business_chatbots.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"apiwrap.h\"\n#include \"api/api_blocked_peers.h\"\n#include \"main/main_session.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/peers/edit_contact_box.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace HistoryView {\nnamespace {\n\n[[nodiscard]] bool BarCurrentlyHidden(not_null<PeerData*> peer) {\n\tconst auto settings = peer->barSettings();\n\tif (!settings) {\n\t\treturn false;\n\t} else if (!(*settings)) {\n\t\treturn true;\n\t}\n\tif (const auto user = peer->asUser()) {\n\t\tif (user->isBlocked()) {\n\t\t\treturn true;\n\t\t} else if (user->isContact()\n\t\t\t&& !((*settings) & PeerBarSetting::ShareContact)) {\n\t\t\treturn true;\n\t\t}\n\t} else if (!((*settings) & PeerBarSetting::ReportSpam)) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid FinalizeSetBotPhotoFirstOpenState(not_null<PeerData*> peer) {\n\tusing State = BotInfo::SetBotPhotoOpenState;\n\n\tconst auto user = peer->asUser();\n\tconst auto info = user ? user->botInfo.get() : nullptr;\n\tif (!info || (info->setBotPhotoOpenState != State::Unknown)) {\n\t\treturn;\n\t}\n\tinfo->setBotPhotoOpenState = peer->owner().history(peer)->lastMessage()\n\t\t? State::OpenedWithHistory\n\t\t: State::OpenedEmpty;\n}\n\n[[nodiscard]] rpl::producer<TextWithEntities> ResolveIsCustom(\n\t\tnot_null<Data::Session*> owner,\n\t\tDocumentId id) {\n\treturn owner->customEmojiManager().resolve(\n\t\tid\n\t) | rpl::map([=](not_null<DocumentData*> document) {\n\t\tconst auto sticker = document->sticker();\n\t\tAssert(sticker != nullptr);\n\n\t\tconst auto &manager = document->owner().customEmojiManager();\n\t\tconst auto setId = manager.coloredSetId();\n\t\tconst auto text = (setId == sticker->set.id)\n\t\t\t? QString()\n\t\t\t: sticker->alt;\n\t\tif (text.isEmpty()) {\n\t\t\treturn TextWithEntities();\n\t\t}\n\t\treturn TextWithEntities{\n\t\t\t.text = text,\n\t\t\t.entities = { EntityInText(\n\t\t\t\tEntityType::CustomEmoji,\n\t\t\t\t0,\n\t\t\t\ttext.size(),\n\t\t\t\tData::SerializeCustomEmojiId(document)) },\n\t\t};\n\t}) | rpl::map_error_to_done();\n}\n\n[[nodiscard]] rpl::producer<TextWithEntities> PeerCustomStatus(\n\t\tnot_null<PeerData*> peer) {\n\tif (peer->isChat()) {\n\t\treturn rpl::single(TextWithEntities());\n\t}\n\tconst auto owner = &peer->owner();\n\treturn peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::EmojiStatus\n\t) | rpl::map([=] {\n\t\tconst auto id = peer->emojiStatusId();\n\t\treturn id.collectible\n\t\t\t? rpl::single(Ui::Text::SingleCustomEmoji(\n\t\t\t\tData::EmojiStatusCustomId(id)))\n\t\t\t: id.documentId\n\t\t\t? ResolveIsCustom(owner, id.documentId)\n\t\t\t: rpl::single(TextWithEntities());\n\t}) | rpl::flatten_latest() | rpl::distinct_until_changed();\n}\n\n[[nodiscard]] object_ptr<Ui::AbstractButton> MakeIconButton(\n\t\tQWidget *parent,\n\t\tconst style::icon &icon) {\n\tauto result = object_ptr<Ui::RippleButton>(\n\t\tparent,\n\t\tst::historyContactStatusButton.ripple);\n\tconst auto raw = result.data();\n\traw->paintRequest(\n\t) | rpl::on_next([=, &icon] {\n\t\tauto p = QPainter(raw);\n\t\tp.fillRect(raw->rect(), st::historyContactStatusButton.bgColor);\n\t\traw->paintRipple(p, 0, 0);\n\t\ticon.paintInCenter(p, raw->rect());\n\t}, raw->lifetime());\n\treturn result;\n}\n\n[[nodiscard]] object_ptr<Ui::RippleButton> SetupSetBotPhotoButton(\n\t\tobject_ptr<Ui::RippleButton> button) {\n\tconst auto raw = button.data();\n\n\tconst auto icon = Ui::CreateChild<Ui::RpWidget>(raw);\n\ticon->setAttribute(Qt::WA_TransparentForMouseEvents);\n\ticon->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(icon);\n\t\tst::historySetBotPhotoIcon.paintInCenter(p, icon->rect());\n\t}, icon->lifetime());\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\ttr::lng_managed_bot_set_photo(tr::now),\n\t\tst::historySetBotPhotoLabel);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\traw->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tconst auto closeWidth = st::historyReplyCancel.width;\n\t\tconst auto iconWidth = st::historySetBotPhotoIconSize;\n\t\tconst auto margin = st::historySetBotPhotoLabelMarginRight;\n\t\tconst auto available = size.width() - closeWidth - margin;\n\t\tconst auto labelNatural = label->naturalWidth();\n\t\tconst auto labelWidth = std::max(\n\t\t\t0,\n\t\t\tstd::min(labelNatural, available - iconWidth));\n\t\tconst auto totalContent = iconWidth + labelWidth;\n\t\tconst auto contentLeft = std::max(0, (available - totalContent) / 2);\n\t\ticon->setGeometry(\n\t\t\tcontentLeft,\n\t\t\t0,\n\t\t\ticonWidth,\n\t\t\tsize.height());\n\t\tlabel->resizeToWidth(labelWidth);\n\t\tlabel->moveToLeft(\n\t\t\tcontentLeft + iconWidth,\n\t\t\t(size.height() - label->height()) / 2,\n\t\t\tsize.width());\n\t\ticon->raise();\n\t\tlabel->raise();\n\t}, raw->lifetime());\n\n\treturn button;\n}\n\n} // namespace\n\nclass ContactStatus::BgButton final : public Ui::RippleButton {\npublic:\n\tBgButton(QWidget *parent, const style::FlatButton &st);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid onStateChanged(State was, StateChangeSource source) override;\n\nprivate:\n\tconst style::FlatButton &_st;\n\n};\n\nclass ContactStatus::Bar final : public Ui::RpWidget {\npublic:\n\tBar(QWidget *parent, const QString &name);\n\n\tvoid showState(\n\t\tState state,\n\t\tTextWithEntities status,\n\t\tUi::Text::MarkedContext context);\n\n\t[[nodiscard]] rpl::producer<> unarchiveClicks() const;\n\t[[nodiscard]] rpl::producer<> addClicks() const;\n\t[[nodiscard]] rpl::producer<> blockClicks() const;\n\t[[nodiscard]] rpl::producer<> shareClicks() const;\n\t[[nodiscard]] rpl::producer<> reportClicks() const;\n\t[[nodiscard]] rpl::producer<> closeClicks() const;\n\t[[nodiscard]] rpl::producer<> requestInfoClicks() const;\n\t[[nodiscard]] rpl::producer<> emojiStatusClicks() const;\n\t[[nodiscard]] rpl::producer<> setBotPhotoClicks() const;\n\nprivate:\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid emojiStatusRepaint();\n\n\tQString _name;\n\tobject_ptr<Ui::FlatButton> _add;\n\tobject_ptr<Ui::FlatButton> _unarchive;\n\tobject_ptr<Ui::AbstractButton> _unarchiveIcon;\n\tobject_ptr<Ui::FlatButton> _block;\n\tobject_ptr<Ui::FlatButton> _share;\n\tobject_ptr<Ui::FlatButton> _report;\n\tobject_ptr<Ui::AbstractButton> _reportIcon;\n\tobject_ptr<Ui::IconButton> _close;\n\tobject_ptr<BgButton> _requestChatBg;\n\tobject_ptr<Ui::FlatLabel> _requestChatInfo;\n\tobject_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _emojiStatusInfo;\n\tobject_ptr<Ui::PlainShadow> _emojiStatusShadow;\n\tobject_ptr<Ui::RippleButton> _setBotPhoto;\n\tbool _emojiStatusRepaintScheduled = false;\n\tbool _narrow = false;\n\trpl::event_stream<> _emojiStatusClicks;\n\n};\n\nContactStatus::BgButton::BgButton(\n\tQWidget *parent,\n\tconst style::FlatButton &st)\n: RippleButton(parent, st.ripple)\n, _st(st) {\n}\n\nvoid ContactStatus::BgButton::onStateChanged(\n\t\tState was,\n\t\tStateChangeSource source) {\n\tRippleButton::onStateChanged(was, source);\n\tupdate();\n}\n\nvoid ContactStatus::BgButton::paintEvent(QPaintEvent *e) {\n\tQPainter p(this);\n\n\tp.fillRect(e->rect(), isOver() ? _st.overBgColor : _st.bgColor);\n\tpaintRipple(p, 0, 0);\n}\n\nContactStatus::Bar::Bar(\n\tQWidget *parent,\n\tconst QString &name)\n: RpWidget(parent)\n, _name(name)\n, _add(\n\tthis,\n\tQString(),\n\tst::historyContactStatusButton)\n, _unarchive(\n\tthis,\n\ttr::lng_new_contact_unarchive(tr::now).toUpper(),\n\tst::historyContactStatusButton)\n, _unarchiveIcon(MakeIconButton(this, st::menuIconUnarchive))\n, _block(\n\tthis,\n\ttr::lng_new_contact_block(tr::now).toUpper(),\n\tst::historyContactStatusBlock)\n, _share(\n\tthis,\n\ttr::lng_new_contact_share(tr::now).toUpper(),\n\tst::historyContactStatusButton)\n, _report(\n\tthis,\n\tQString(),\n\tst::historyContactStatusBlock)\n, _reportIcon(MakeIconButton(this, st::menuIconReportAttention))\n, _close(this, st::historyReplyCancel)\n, _requestChatBg(this, st::historyContactStatusButton)\n, _requestChatInfo(\n\tthis,\n\tQString(),\n\tst::historyContactStatusLabel)\n, _emojiStatusInfo(\n\tthis,\n\tobject_ptr<Ui::FlatLabel>(this, u\"\"_q, st::historyEmojiStatusInfoLabel),\n\tQMargins(\n\t\tst::historyContactStatusMinSkip,\n\t\tst::topBarArrowPadding.top(),\n\t\tst::historyContactStatusMinSkip,\n\t\tst::topBarArrowPadding.top()))\n, _emojiStatusShadow(this)\n, _setBotPhoto(\n\tSetupSetBotPhotoButton(\n\t\tobject_ptr<BgButton>(this, st::historyContactStatusButton))) {\n\t_close->setAccessibleName(tr::lng_cancel(tr::now));\n\t_unarchiveIcon->setAccessibleName(tr::lng_new_contact_unarchive(tr::now));\n\t_reportIcon->setAccessibleName(tr::lng_report_spam(tr::now));\n\t_requestChatInfo->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_emojiStatusInfo->paintRequest(\n\t) | rpl::on_next([=, raw = _emojiStatusInfo.data()](QRect clip) {\n\t\t_emojiStatusRepaintScheduled = false;\n\t\tQPainter(raw).fillRect(clip, st::historyComposeButtonBg);\n\t}, lifetime());\n}\n\nvoid ContactStatus::Bar::showState(\n\t\tState state,\n\t\tTextWithEntities status,\n\t\tUi::Text::MarkedContext context) {\n\tusing Type = State::Type;\n\tconst auto type = state.type;\n\t_add->setVisible(type == Type::AddOrBlock || type == Type::Add);\n\tconst auto unarchive = (type == Type::UnarchiveOrBlock)\n\t\t|| (type == Type::UnarchiveOrReport);\n\t_unarchive->setVisible(!_narrow && unarchive);\n\t_unarchiveIcon->setVisible(_narrow && unarchive);\n\t_block->setVisible(type == Type::AddOrBlock\n\t\t|| type == Type::UnarchiveOrBlock);\n\t_share->setVisible(type == Type::SharePhoneNumber);\n\t_close->setVisible(\n\t\t(!_narrow && type != Type::RequestChatInfo)\n\t\t|| type == Type::SetBotPhoto);\n\tconst auto report = (type == Type::ReportSpam)\n\t\t|| (type == Type::UnarchiveOrReport);\n\t_report->setVisible(!_narrow && report);\n\t_reportIcon->setVisible(_narrow && report);\n\t_requestChatInfo->setVisible(type == Type::RequestChatInfo);\n\t_requestChatBg->setVisible(type == Type::RequestChatInfo);\n\t_setBotPhoto->setVisible(type == Type::SetBotPhoto);\n\tconst auto has = !status.empty();\n\t_emojiStatusShadow->setVisible(\n\t\thas && (type == Type::AddOrBlock || type == Type::UnarchiveOrBlock));\n\tif (has) {\n\t\tcontext.repaint = [=] { emojiStatusRepaint(); };\n\t\t_emojiStatusInfo->entity()->setMarkedText(\n\t\t\ttr::lng_new_contact_about_status(\n\t\t\t\ttr::now,\n\t\t\t\tlt_emoji,\n\t\t\t\tstatus,\n\t\t\t\tlt_link,\n\t\t\t\ttr::link(\n\t\t\t\t\ttr::lng_new_contact_about_status_link(tr::now)),\n\t\t\t\ttr::marked),\n\t\t\tcontext);\n\t\t_emojiStatusInfo->entity()->overrideLinkClickHandler([=] {\n\t\t\t_emojiStatusClicks.fire({});\n\t\t});\n\t\t_emojiStatusInfo->entity()->setAnimationsPausedCallback([] {\n\t\t\treturn Ui::FlatLabel::WhichAnimationsPaused::CustomEmoji;\n\t\t});\n\t}\n\t_emojiStatusInfo->setVisible(has);\n\t_add->setText((type == Type::Add)\n\t\t? tr::lng_new_contact_add_name(tr::now, lt_user, _name).toUpper()\n\t\t: tr::lng_new_contact_add(tr::now).toUpper());\n\t_report->setText((type == Type::ReportSpam)\n\t\t? tr::lng_report_spam_and_leave(tr::now).toUpper()\n\t\t: tr::lng_report_spam(tr::now).toUpper());\n\t_requestChatInfo->setMarkedText(\n\t\t(state.requestChatIsBroadcast\n\t\t\t? tr::lng_new_contact_from_request_channel\n\t\t\t: tr::lng_new_contact_from_request_group)(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\ttr::bold(_name),\n\t\t\tlt_name,\n\t\t\ttr::bold(state.requestChatName),\n\t\t\ttr::marked));\n\tresizeToWidth(width());\n}\n\nrpl::producer<> ContactStatus::Bar::unarchiveClicks() const {\n\treturn rpl::merge(\n\t\t_unarchive->clicks(),\n\t\t_unarchiveIcon->clicks()\n\t) | rpl::to_empty;\n}\n\nrpl::producer<> ContactStatus::Bar::addClicks() const {\n\treturn _add->clicks() | rpl::to_empty;\n}\n\nrpl::producer<> ContactStatus::Bar::blockClicks() const {\n\treturn _block->clicks() | rpl::to_empty;\n}\n\nrpl::producer<> ContactStatus::Bar::shareClicks() const {\n\treturn _share->clicks() | rpl::to_empty;\n}\n\nrpl::producer<> ContactStatus::Bar::reportClicks() const {\n\treturn rpl::merge(\n\t\t_report->clicks(),\n\t\t_reportIcon->clicks()\n\t) | rpl::to_empty;\n}\n\nrpl::producer<> ContactStatus::Bar::closeClicks() const {\n\treturn _close->clicks() | rpl::to_empty;\n}\n\nrpl::producer<> ContactStatus::Bar::requestInfoClicks() const {\n\treturn _requestChatBg->clicks() | rpl::to_empty;\n}\n\nrpl::producer<> ContactStatus::Bar::emojiStatusClicks() const {\n\treturn _emojiStatusClicks.events();\n}\n\nrpl::producer<> ContactStatus::Bar::setBotPhotoClicks() const {\n\treturn _setBotPhoto->clicks() | rpl::to_empty;\n}\n\nint ContactStatus::Bar::resizeGetHeight(int newWidth) {\n\t_close->moveToRight(0, 0, newWidth);\n\tconst auto narrow = (newWidth < _close->width() * 2);\n\tif (_narrow != narrow) {\n\t\t_narrow = narrow;\n\t\t_close->setVisible(\n\t\t\t(_requestChatInfo->isHidden() && !_narrow)\n\t\t\t|| !_setBotPhoto->isHidden());\n\t\tconst auto report = !_report->isHidden() || !_reportIcon->isHidden();\n\t\t_report->setVisible(!_narrow && report);\n\t\t_reportIcon->setVisible(_narrow && report);\n\t\tconst auto unarchive = !_unarchive->isHidden()\n\t\t\t|| !_unarchiveIcon->isHidden();\n\t\t_unarchive->setVisible(!_narrow && unarchive);\n\t\t_unarchiveIcon->setVisible(_narrow && unarchive);\n\t}\n\n\tif (!_setBotPhoto->isHidden()) {\n\t\tconst auto closeHeight = _close->height();\n\t\t_setBotPhoto->setGeometry(0, 0, newWidth, closeHeight);\n\t\t_close->raise();\n\t\treturn closeHeight;\n\t}\n\n\tif (!_unarchiveIcon->isHidden()) {\n\t\tconst auto half = newWidth / 2;\n\t\t_unarchiveIcon->setGeometry(0, 0, half, _close->height());\n\t\t_reportIcon->setGeometry(half, 0, newWidth - half, _close->height());\n\t} else if (!_reportIcon->isHidden()) {\n\t\t_reportIcon->setGeometry(0, 0, newWidth, _close->height());\n\t}\n\n\tconst auto closeWidth = _close->width();\n\tconst auto closeHeight = _close->height();\n\tconst auto available = newWidth - closeWidth;\n\tconst auto skip = st::historyContactStatusMinSkip;\n\tif (available <= 2 * skip) {\n\t\treturn closeHeight;\n\t}\n\tconst auto buttonWidth = [&](const object_ptr<Ui::FlatButton> &button) {\n\t\treturn button->textWidth() + 2 * skip;\n\t};\n\n\tauto accumulatedLeft = 0;\n\tconst auto placeButton = [&](\n\t\t\tconst object_ptr<Ui::FlatButton> &button,\n\t\t\tint buttonWidth,\n\t\t\tint rightTextMargin = 0) {\n\t\tbutton->setGeometry(accumulatedLeft, 0, buttonWidth, closeHeight);\n\t\tbutton->setTextMargins({ 0, 0, rightTextMargin, 0 });\n\t\taccumulatedLeft += buttonWidth;\n\t};\n\tconst auto placeOne = [&](const object_ptr<Ui::FlatButton> &button) {\n\t\tif (button->isHidden()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto thatWidth = buttonWidth(button);\n\t\tconst auto margin = std::clamp(\n\t\t\tthatWidth + closeWidth - available,\n\t\t\t0,\n\t\t\tcloseWidth);\n\t\tplaceButton(button, newWidth, margin);\n\t};\n\tconst auto &leftButton = _unarchive->isHidden() ? _add : _unarchive;\n\tconst auto &rightButton = _block->isHidden() ? _report : _block;\n\tif (!leftButton->isHidden() && !rightButton->isHidden()) {\n\t\tconst auto leftWidth = buttonWidth(leftButton);\n\t\tconst auto rightWidth = buttonWidth(rightButton);\n\t\tconst auto half = newWidth / 2;\n\t\tif (leftWidth <= half\n\t\t\t&& rightWidth + 2 * closeWidth <= newWidth - half) {\n\t\t\tplaceButton(leftButton, half);\n\t\t\tplaceButton(rightButton, newWidth - half);\n\t\t} else if (leftWidth + rightWidth <= available) {\n\t\t\tconst auto margin = std::clamp(\n\t\t\t\tleftWidth + rightWidth + closeWidth - available,\n\t\t\t\t0,\n\t\t\t\tcloseWidth);\n\t\t\tconst auto realBlockWidth = rightWidth + 2 * closeWidth - margin;\n\t\t\tif (leftWidth > realBlockWidth) {\n\t\t\t\tplaceButton(leftButton, leftWidth);\n\t\t\t\tplaceButton(rightButton, newWidth - leftWidth, margin);\n\t\t\t} else {\n\t\t\t\tplaceButton(leftButton, newWidth - realBlockWidth);\n\t\t\t\tplaceButton(rightButton, realBlockWidth, margin);\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto forLeft = (available * leftWidth)\n\t\t\t\t/ (leftWidth + rightWidth);\n\t\t\tplaceButton(leftButton, forLeft);\n\t\t\tplaceButton(rightButton, newWidth - forLeft, closeWidth);\n\t\t}\n\t} else {\n\t\tplaceOne(_add);\n\t\tplaceOne(_share);\n\t\tplaceOne(_report);\n\t}\n\tif (_requestChatInfo->isHidden()) {\n\t\t_emojiStatusInfo->resizeToWidth(newWidth);\n\t\t_emojiStatusInfo->move(0, _close->height());\n\t\t_emojiStatusShadow->setGeometry(\n\t\t\t0,\n\t\t\tcloseHeight,\n\t\t\tnewWidth,\n\t\t\tst::lineWidth);\n\t\t_emojiStatusShadow->move(0, _close->height());\n\t\treturn closeHeight + (_emojiStatusInfo->isHidden()\n\t\t\t? 0\n\t\t\t: _emojiStatusInfo->height());\n\t}\n\tconst auto vskip = st::topBarArrowPadding.top();\n\t_requestChatInfo->resizeToWidth(available - 2 * skip);\n\t_requestChatInfo->move(skip, vskip);\n\tconst auto newHeight = _requestChatInfo->height() + 2 * vskip;\n\t_requestChatBg->setGeometry(0, 0, newWidth, newHeight);\n\treturn newHeight;\n}\n\nvoid ContactStatus::Bar::emojiStatusRepaint() {\n\tif (_emojiStatusRepaintScheduled) {\n\t\treturn;\n\t}\n\t_emojiStatusRepaintScheduled = true;\n\t_emojiStatusInfo->entity()->update();\n}\n\nSlidingBar::SlidingBar(\n\tnot_null<Ui::RpWidget*> parent,\n\tobject_ptr<Ui::RpWidget> wrapped)\n: _wrapped(parent, std::move(wrapped))\n, _shadow(parent) {\n\tsetup(parent);\n\t_wrapped.hide(anim::type::instant);\n\t_shadow.hide();\n}\n\nvoid SlidingBar::setup(not_null<Ui::RpWidget*> parent) {\n\tparent->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_wrapped.resizeToWidth(width);\n\t}, _wrapped.lifetime());\n\n\t_wrapped.geometryValue(\n\t) | rpl::on_next([=](QRect geometry) {\n\t\t_shadow.setGeometry(\n\t\t\tgeometry.x(),\n\t\t\tgeometry.y() + geometry.height(),\n\t\t\tgeometry.width(),\n\t\t\tst::lineWidth);\n\t}, _shadow.lifetime());\n\n\t_shadow.showOn(rpl::combine(\n\t\t_wrapped.shownValue(),\n\t\t_wrapped.heightValue(),\n\t\trpl::mappers::_1 && rpl::mappers::_2 > 0\n\t) | rpl::filter([=](bool shown) {\n\t\treturn (shown == _shadow.isHidden());\n\t}));\n}\n\nvoid SlidingBar::toggleContent(bool visible) {\n\t_contentShown = visible;\n\tif (_shown) {\n\t\t_wrapped.toggle(visible, anim::type::normal);\n\t}\n}\n\nvoid SlidingBar::raise() {\n\t_wrapped.raise();\n\t_shadow.raise();\n}\n\nvoid SlidingBar::setVisible(bool visible) {\n\t_shown = visible;\n\tif (!_shown) {\n\t\t_wrapped.hide(anim::type::instant);\n\t} else if (_contentShown) {\n\t\t_wrapped.show(anim::type::instant);\n\t} else if (!_wrapped.isHidden() && !_wrapped.animating()) {\n\t\t_wrapped.hide(anim::type::instant);\n\t}\n}\n\nvoid SlidingBar::move(int x, int y) {\n\t_wrapped.move(x, y);\n\t_shadow.move(x, y + _wrapped.height());\n}\n\nint SlidingBar::height() const {\n\treturn _wrapped.height();\n}\n\nrpl::producer<int> SlidingBar::heightValue() const {\n\treturn _wrapped.heightValue();\n}\n\nContactStatus::ContactStatus(\n\tnot_null<Window::SessionController*> window,\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<PeerData*> peer,\n\tbool showInForum)\n: _controller(window)\n, _inner(Ui::CreateChild<Bar>(parent.get(), peer->shortName()))\n, _bar(parent, object_ptr<Bar>::fromRaw(_inner)) {\n\tFinalizeSetBotPhotoFirstOpenState(peer);\n\tsetupState(peer, showInForum);\n\tsetupHandlers(peer);\n}\n\nauto ContactStatus::PeerState(not_null<PeerData*> peer)\n-> rpl::producer<State> {\n\tusing SettingsChange = PeerData::BarSettings::Change;\n\tusing Type = State::Type;\n\tif (const auto user = peer->asUser()) {\n\t\tusing FlagsChange = UserData::Flags::Change;\n\t\tusing Flag = UserDataFlag;\n\n\t\tauto changes = user->flagsValue(\n\t\t) | rpl::filter([](FlagsChange flags) {\n\t\t\treturn flags.diff\n\t\t\t\t& (Flag::Contact | Flag::MutualContact | Flag::Blocked);\n\t\t});\n\t\tauto managedBotChanges = user->session().changes().peerFlagsValue(\n\t\t\tuser,\n\t\t\tData::PeerUpdate::Flag::ManagedBot);\n\t\tauto photoChanges = user->session().changes().peerFlagsValue(\n\t\t\tuser,\n\t\t\tData::PeerUpdate::Flag::Photo);\n\t\treturn rpl::combine(\n\t\t\tstd::move(changes),\n\t\t\tuser->barSettingsValue(),\n\t\t\tstd::move(photoChanges),\n\t\t\tstd::move(managedBotChanges)\n\t\t) | rpl::map([=](\n\t\t\t\tFlagsChange flags,\n\t\t\t\tSettingsChange settings,\n\t\t\t\tconst auto &,\n\t\t\t\tconst auto &) -> State {\n\t\t\tif (flags.value & Flag::Blocked) {\n\t\t\t\treturn { Type::None };\n\t\t\t} else if (user->isContact()) {\n\t\t\t\tif (settings.value & PeerBarSetting::ShareContact) {\n\t\t\t\t\treturn { Type::SharePhoneNumber };\n\t\t\t\t} else {\n\t\t\t\t\treturn { Type::None };\n\t\t\t\t}\n\t\t\t} else if (settings.value & PeerBarSetting::RequestChat) {\n\t\t\t\treturn {\n\t\t\t\t\t.type = Type::RequestChatInfo,\n\t\t\t\t\t.requestChatName = peer->requestChatTitle(),\n\t\t\t\t\t.requestDate = peer->requestChatDate(),\n\t\t\t\t\t.requestChatIsBroadcast = !!(settings.value\n\t\t\t\t\t\t& PeerBarSetting::RequestChatIsBroadcast),\n\t\t\t\t};\n\t\t\t} else if (settings.value & PeerBarSetting::AutoArchived) {\n\t\t\t\treturn { Type::UnarchiveOrBlock };\n\t\t\t} else if (settings.value & PeerBarSetting::BlockContact) {\n\t\t\t\treturn { Type::AddOrBlock };\n\t\t\t} else if (settings.value & PeerBarSetting::AddContact) {\n\t\t\t\treturn { Type::Add };\n\t\t\t} else if (user->botManagerId()) {\n\t\t\t\tif (const auto info = user->botInfo.get()) {\n\t\t\t\t\tif (info->canEditInformation) {\n\t\t\t\t\t\tconst auto hasUserpic = user->hasUserpic();\n\t\t\t\t\t\tif (hasUserpic) {\n\t\t\t\t\t\t\tinfo->setBotPhotoHidden = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tusing State = BotInfo::SetBotPhotoOpenState;\n\t\t\t\t\t\tif (info->setBotPhotoOpenState == State::OpenedEmpty\n\t\t\t\t\t\t\t&& !info->setBotPhotoHidden\n\t\t\t\t\t\t\t&& !hasUserpic) {\n\t\t\t\t\t\t\treturn { Type::SetBotPhoto };\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn { Type::None };\n\t\t\t} else {\n\t\t\t\treturn { Type::None };\n\t\t\t}\n\t\t});\n\t}\n\n\treturn peer->barSettingsValue(\n\t) | rpl::map([=](SettingsChange settings) {\n\t\tusing Type = State::Type;\n\t\treturn (settings.value & PeerBarSetting::AutoArchived)\n\t\t\t? State{ Type::UnarchiveOrReport }\n\t\t\t: (settings.value & PeerBarSetting::ReportSpam)\n\t\t\t? State{ Type::ReportSpam }\n\t\t\t: State{ Type::None };\n\t});\n}\n\nvoid ContactStatus::setupState(not_null<PeerData*> peer, bool showInForum) {\n\tif (!BarCurrentlyHidden(peer)) {\n\t\tpeer->session().api().requestPeerSettings(peer);\n\t}\n\n\t_context = Core::TextContext({ .session = &peer->session() });\n\t_inner->showState({}, {}, _context);\n\tconst auto channel = peer->asChannel();\n\trpl::combine(\n\t\tPeerState(peer),\n\t\tPeerCustomStatus(peer),\n\t\t((channel && !showInForum)\n\t\t\t? Data::PeerFlagValue(channel, ChannelData::Flag::Forum)\n\t\t\t: (rpl::single(false) | rpl::type_erased))\n\t) | rpl::on_next([=](\n\t\t\tState state,\n\t\t\tTextWithEntities status,\n\t\t\tbool hiddenByForum) {\n\t\t_state = state;\n\t\t_status = status;\n\t\t_hiddenByForum = hiddenByForum;\n\t\tif (state.type == State::Type::None || hiddenByForum) {\n\t\t\t_bar.toggleContent(false);\n\t\t} else {\n\t\t\t_inner->showState(state, std::move(status), _context);\n\t\t\t_bar.toggleContent(true);\n\t\t}\n\t}, _bar.lifetime());\n}\n\nvoid ContactStatus::setupHandlers(not_null<PeerData*> peer) {\n\tif (const auto user = peer->asUser()) {\n\t\tsetupAddHandler(user);\n\t\tsetupBlockHandler(user);\n\t\tsetupShareHandler(user);\n\t\tsetupSetBotPhotoHandler(user);\n\t}\n\tsetupUnarchiveHandler(peer);\n\tsetupReportHandler(peer);\n\tsetupCloseHandler(peer);\n\tsetupRequestInfoHandler(peer);\n\tsetupEmojiStatusHandler(peer);\n}\n\nvoid ContactStatus::setupAddHandler(not_null<UserData*> user) {\n\t_inner->addClicks(\n\t) | rpl::on_next([=] {\n\t\t_controller->window().show(Box(EditContactBox, _controller, user));\n\t}, _bar.lifetime());\n}\n\nvoid ContactStatus::setupBlockHandler(not_null<UserData*> user) {\n\t_inner->blockClicks(\n\t) | rpl::on_next([=] {\n\t\t_controller->window().show(Box(\n\t\t\tWindow::PeerMenuBlockUserBox,\n\t\t\t&_controller->window(),\n\t\t\tuser,\n\t\t\tv::null,\n\t\t\tWindow::ClearChat{}));\n\t}, _bar.lifetime());\n}\n\nvoid ContactStatus::setupShareHandler(not_null<UserData*> user) {\n\t_inner->shareClicks(\n\t) | rpl::on_next([=] {\n\t\tconst auto show = _controller->uiShow();\n\t\tconst auto share = [=](Fn<void()> &&close) {\n\t\t\tuser->setBarSettings(0);\n\t\t\tuser->session().api().request(MTPcontacts_AcceptContact(\n\t\t\t\tuser->inputUser()\n\t\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t\tuser->session().api().applyUpdates(result);\n\t\t\t\tshow->showToast(tr::lng_new_contact_share_done(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tuser->shortName()));\n\t\t\t}).send();\n\t\t\tclose();\n\t\t};\n\t\tshow->showBox(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_new_contact_share_sure(\n\t\t\t\ttr::now,\n\t\t\t\tlt_phone,\n\t\t\t\ttr::marked(\n\t\t\t\t\tUi::FormatPhone(user->session().user()->phone())),\n\t\t\t\tlt_user,\n\t\t\t\ttr::bold(user->name()),\n\t\t\t\ttr::marked),\n\t\t\t.confirmed = share,\n\t\t\t.confirmText = tr::lng_box_ok(),\n\t\t}));\n\t}, _bar.lifetime());\n}\n\nvoid ContactStatus::setupUnarchiveHandler(not_null<PeerData*> peer) {\n\t_inner->unarchiveClicks(\n\t) | rpl::on_next([=, show = _controller->uiShow()] {\n\t\tusing namespace Window;\n\t\tToggleHistoryArchived(show, peer->owner().history(peer), false);\n\t\tpeer->owner().notifySettings().resetToDefault(peer);\n\t\tif (const auto settings = peer->barSettings()) {\n\t\t\tconst auto flags = PeerBarSetting::AutoArchived\n\t\t\t\t| PeerBarSetting::BlockContact\n\t\t\t\t| PeerBarSetting::ReportSpam;\n\t\t\tpeer->setBarSettings(*settings & ~flags);\n\t\t}\n\t}, _bar.lifetime());\n}\n\nvoid ContactStatus::setupReportHandler(not_null<PeerData*> peer) {\n\t_inner->reportClicks(\n\t) | rpl::on_next([=] {\n\t\tExpects(!peer->isUser());\n\n\t\tconst auto show = _controller->uiShow();\n\t\tconst auto callback = crl::guard(_inner, [=](Fn<void()> &&close) {\n\t\t\tclose();\n\n\t\t\tpeer->session().api().request(MTPmessages_ReportSpam(\n\t\t\t\tpeer->input()\n\t\t\t)).send();\n\n\t\t\tcrl::on_main(&peer->session(), [=] {\n\t\t\t\tif (const auto from = peer->migrateFrom()) {\n\t\t\t\t\tpeer->session().api().deleteConversation(from, false);\n\t\t\t\t}\n\t\t\t\tpeer->session().api().deleteConversation(peer, false);\n\t\t\t});\n\n\t\t\tshow->showToast(tr::lng_report_spam_done(tr::now));\n\n\t\t\t// Destroys _bar.\n\t\t\t_controller->showBackFromStack();\n\t\t});\n\t\tif (const auto user = peer->asUser()) {\n\t\t\tpeer->session().api().blockedPeers().block(user);\n\t\t}\n\t\tauto text = ((peer->isChat() || peer->isMegagroup())\n\t\t\t? tr::lng_report_spam_sure_group\n\t\t\t: tr::lng_report_spam_sure_channel)();\n\t\tshow->showBox(Ui::MakeConfirmBox({\n\t\t\t.text= std::move(text),\n\t\t\t.confirmed = callback,\n\t\t\t.confirmText = tr::lng_report_spam_ok(),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t}));\n\t}, _bar.lifetime());\n}\n\nvoid ContactStatus::setupCloseHandler(not_null<PeerData*> peer) {\n\tconst auto request = _bar.lifetime().make_state<mtpRequestId>(0);\n\t_inner->closeClicks(\n\t) | rpl::filter([=] {\n\t\treturn !(*request);\n\t}) | rpl::on_next([=] {\n\t\tif (_state.type == State::Type::SetBotPhoto) {\n\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\tif (const auto info = user->botInfo.get()) {\n\t\t\t\t\tinfo->setBotPhotoHidden = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\t_state = {};\n\t\t\t_bar.toggleContent(false);\n\t\t\treturn;\n\t\t}\n\t\tpeer->setBarSettings(0);\n\t\t*request = peer->session().api().request(\n\t\t\tMTPmessages_HidePeerSettingsBar(peer->input())\n\t\t).send();\n\t}, _bar.lifetime());\n}\n\nvoid ContactStatus::setupRequestInfoHandler(not_null<PeerData*> peer) {\n\tconst auto request = _bar.lifetime().make_state<mtpRequestId>(0);\n\t_inner->requestInfoClicks(\n\t) | rpl::filter([=] {\n\t\treturn !(*request);\n\t}) | rpl::on_next([=] {\n\t\t_controller->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tbox->setTitle((_state.requestChatIsBroadcast\n\t\t\t\t? tr::lng_from_request_title_channel\n\t\t\t\t: tr::lng_from_request_title_group)());\n\n\t\t\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_from_request_body(\n\t\t\t\t\tlt_name,\n\t\t\t\t\trpl::single(tr::bold(_state.requestChatName)),\n\t\t\t\t\tlt_date,\n\t\t\t\t\trpl::single(tr::marked(langDateTimeFull(\n\t\t\t\t\t\tbase::unixtime::parse(_state.requestDate)\n\t\t\t\t\t))),\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::boxLabel));\n\n\t\t\tbox->addButton(tr::lng_from_request_understand(), [=] {\n\t\t\t\tif (*request) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tpeer->setBarSettings(0);\n\t\t\t\t*request = peer->session().api().request(\n\t\t\t\t\tMTPmessages_HidePeerSettingsBar(peer->input())\n\t\t\t\t).send();\n\t\t\t\tbox->closeBox();\n\t\t\t});\n\t\t}));\n\t}, _bar.lifetime());\n}\n\nvoid ContactStatus::setupEmojiStatusHandler(not_null<PeerData*> peer) {\n\t_inner->emojiStatusClicks(\n\t) | rpl::on_next([=] {\n\t\tSettings::ShowEmojiStatusPremium(_controller, peer);\n\t}, _bar.lifetime());\n}\n\nvoid ContactStatus::setupSetBotPhotoHandler(not_null<UserData*> user) {\n\t_inner->setBotPhotoClicks(\n\t) | rpl::on_next([=] {\n\t\t_controller->showEditPeerBox(user);\n\t}, _bar.lifetime());\n}\n\nvoid ContactStatus::show() {\n\tif (!_shown) {\n\t\t_shown = true;\n\t\tif (_state.type != State::Type::None && !_hiddenByForum) {\n\t\t\t_inner->showState(_state, _status, _context);\n\t\t\t_bar.toggleContent(true);\n\t\t}\n\t}\n\t_bar.show();\n}\n\nvoid ContactStatus::hide() {\n\t_bar.hide();\n}\n\nclass BusinessBotStatus::Bar final : public Ui::RpWidget {\npublic:\n\tBar(QWidget *parent);\n\n\tvoid showState(State state);\n\n\t[[nodiscard]] rpl::producer<> pauseClicks() const;\n\t[[nodiscard]] rpl::producer<> resumeClicks() const;\n\t[[nodiscard]] rpl::producer<> removeClicks() const;\n\t[[nodiscard]] rpl::producer<> manageClicks() const;\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid showMenu();\n\n\tobject_ptr<Ui::UserpicButton> _userpic = { nullptr };\n\tobject_ptr<Ui::FlatLabel> _name;\n\tobject_ptr<Ui::FlatLabel> _status;\n\tobject_ptr<Ui::RoundButton> _togglePaused;\n\tobject_ptr<Ui::IconButton> _settings;\n\trpl::event_stream<> _removeClicks;\n\trpl::event_stream<> _manageClicks;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\tbool _paused = false;\n\n};\n\nBusinessBotStatus::Bar::Bar(QWidget *parent)\n: RpWidget(parent)\n, _name(this, st::historyBusinessBotName)\n, _status(this, st::historyBusinessBotStatus)\n, _togglePaused(\n\tthis,\n\trpl::single(QString()),\n\tst::historyBusinessBotToggle)\n, _settings(this, st::historyBusinessBotSettings) {\n\t_name->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_status->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_togglePaused->setFullRadius(true);\n\t_settings->setClickedCallback([=] {\n\t\tshowMenu();\n\t});\n}\n\nvoid BusinessBotStatus::Bar::showState(State state) {\n\tExpects(state.bot != nullptr);\n\n\t_userpic = object_ptr<Ui::UserpicButton>(\n\t\tthis,\n\t\tstate.bot,\n\t\tst::historyBusinessBotPhoto);\n\t_userpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_userpic->show();\n\t_name->setText(state.bot->name());\n\t_status->setText(state.paused\n\t\t? tr::lng_chatbot_status_paused(tr::now)\n\t\t: state.canReply\n\t\t? tr::lng_chatbot_status_can_reply(tr::now)\n\t\t: tr::lng_chatbot_status_views(tr::now));\n\t_togglePaused->setText(state.paused\n\t\t? tr::lng_chatbot_button_resume()\n\t\t: tr::lng_chatbot_button_pause());\n\t_togglePaused->setVisible(state.canReply || state.paused);\n\t_paused = state.paused;\n\tresizeToWidth(width());\n}\n\nrpl::producer<> BusinessBotStatus::Bar::pauseClicks() const {\n\treturn _togglePaused->clicks() | rpl::filter([=] {\n\t\treturn !_paused;\n\t}) | rpl::to_empty;\n}\n\nrpl::producer<> BusinessBotStatus::Bar::resumeClicks() const {\n\treturn _togglePaused->clicks() | rpl::filter([=] {\n\t\treturn _paused;\n\t}) | rpl::to_empty;\n}\n\nrpl::producer<> BusinessBotStatus::Bar::removeClicks() const {\n\treturn _removeClicks.events();\n}\n\nrpl::producer<> BusinessBotStatus::Bar::manageClicks() const {\n\treturn _manageClicks.events();\n}\n\nvoid BusinessBotStatus::Bar::showMenu() {\n\tif (_menu) {\n\t\treturn;\n\t}\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\tst::popupMenuExpandedSeparator);\n\t_menu->setDestroyedCallback([\n\t\tweak = base::make_weak(this),\n\t\tweakButton = base::make_weak(_settings.data()),\n\t\tmenu = _menu.get()] {\n\t\tif (weak && weak->_menu == menu) {\n\t\t\tif (weakButton) {\n\t\t\t\tweakButton->setForceRippled(false);\n\t\t\t}\n\t\t}\n\t});\n\t_settings->setForceRippled(true);\n\n\tconst auto addAction = Ui::Menu::CreateAddActionCallback(_menu);\n\n\taddAction(tr::lng_chatbot_menu_manage(tr::now), crl::guard(this, [=] {\n\t\t_manageClicks.fire({});\n\t}), &st::menuIconSettings);\n\taddAction({\n\t\t.text = (_togglePaused->isHidden()\n\t\t\t? tr::lng_chatbot_menu_revoke(tr::now)\n\t\t\t: tr::lng_chatbot_menu_remove(tr::now)),\n\t\t.handler = crl::guard(this, [=] { _removeClicks.fire({}); }),\n\t\t.icon = &st::menuIconDisableAttention,\n\t\t.isAttention = true,\n\t});\n\n\t_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);\n\t_menu->popup(mapToGlobal(QPoint(\n\t\twidth() + st::topBarMenuPosition.x(),\n\t\tst::topBarMenuPosition.y())));\n}\n\nvoid BusinessBotStatus::Bar::paintEvent(QPaintEvent *e) {\n\tQPainter p(this);\n\tp.fillRect(e->rect(), st::historyContactStatusButton.bgColor);\n}\n\nint BusinessBotStatus::Bar::resizeGetHeight(int newWidth) {\n\tconst auto &st = st::defaultPeerList.item;\n\t_settings->moveToRight(0, 0, newWidth);\n\tif (_userpic) {\n\t\t_userpic->moveToLeft(\n\t\t\tst.photoPosition.x(),\n\t\t\tst.photoPosition.y(),\n\t\t\tnewWidth);\n\t}\n\tauto available = newWidth - _settings->width() - st.namePosition.x();\n\tif (!_togglePaused->isHidden()) {\n\t\t_togglePaused->moveToRight(\n\t\t\t_settings->width(),\n\t\t\t(st.height - _togglePaused->height()) / 2,\n\t\t\tnewWidth);\n\t\tavailable -= _togglePaused->width();\n\t}\n\t_name->resizeToWidth(available);\n\t_name->moveToLeft(st.namePosition.x(), st.namePosition.y(), newWidth);\n\t_status->resizeToWidth(available);\n\t_status->moveToLeft(\n\t\tst.statusPosition.x(),\n\t\tst.statusPosition.y(),\n\t\tnewWidth);\n\treturn st.height;\n}\n\nBusinessBotStatus::BusinessBotStatus(\n\tnot_null<Window::SessionController*> window,\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<PeerData*> peer)\n: _controller(window)\n, _inner(Ui::CreateChild<Bar>(parent.get()))\n, _bar(parent, object_ptr<Bar>::fromRaw(_inner)) {\n\tsetupState(peer);\n\tsetupHandlers(peer);\n}\n\nauto BusinessBotStatus::PeerState(not_null<PeerData*> peer)\n-> rpl::producer<State> {\n\tusing SettingsChange = PeerData::BarSettings::Change;\n\treturn peer->barSettingsValue(\n\t) | rpl::map([=](SettingsChange settings) -> State {\n\t\tusing Flag = PeerBarSetting;\n\t\treturn {\n\t\t\t.bot = peer->businessBot(),\n\t\t\t.manageUrl = peer->businessBotManageUrl(),\n\t\t\t.canReply = ((settings.value & Flag::BusinessBotCanReply) != 0),\n\t\t\t.paused = ((settings.value & Flag::BusinessBotPaused) != 0),\n\t\t};\n\t});\n}\n\nvoid BusinessBotStatus::setupState(not_null<PeerData*> peer) {\n\tif (!BarCurrentlyHidden(peer)) {\n\t\tpeer->session().api().requestPeerSettings(peer);\n\t}\n\tPeerState(\n\t\tpeer\n\t) | rpl::on_next([=](State state) {\n\t\t_state = state;\n\t\tif (!state.bot) {\n\t\t\t_bar.toggleContent(false);\n\t\t} else {\n\t\t\t_inner->showState(state);\n\t\t\t_bar.toggleContent(true);\n\t\t}\n\t}, _bar.lifetime());\n}\n\nvoid BusinessBotStatus::setupHandlers(not_null<PeerData*> peer) {\n\t_inner->pauseClicks(\n\t) | rpl::on_next([=] {\n\t\tpeer->owner().chatbots().togglePaused(peer, true);\n\t}, _bar.lifetime());\n\n\t_inner->resumeClicks(\n\t) | rpl::on_next([=] {\n\t\tpeer->owner().chatbots().togglePaused(peer, false);\n\t}, _bar.lifetime());\n\n\t_inner->removeClicks(\n\t) | rpl::on_next([=] {\n\t\tpeer->owner().chatbots().removeFrom(peer);\n\t}, _bar.lifetime());\n\n\t_inner->manageClicks(\n\t) | rpl::on_next([=] {\n\t\tUrlClickHandler::Open(\n\t\t\t_state.manageUrl,\n\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.sessionWindow = base::make_weak(_controller),\n\t\t\t\t.botStartAutoSubmit = true,\n\t\t\t}));\n\t}, _bar.lifetime());\n}\n\nvoid BusinessBotStatus::show() {\n\tif (!_shown) {\n\t\t_shown = true;\n\t\tif (_state.bot) {\n\t\t\t_inner->showState(_state);\n\t\t\t_bar.toggleContent(true);\n\t\t}\n\t}\n\t_bar.show();\n}\n\nvoid BusinessBotStatus::hide() {\n\t_bar.hide();\n}\n\nTopicReopenBar::TopicReopenBar(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Data::ForumTopic*> topic)\n: _topic(topic)\n, _reopen(Ui::CreateChild<Ui::FlatButton>(\n\tparent.get(),\n\ttr::lng_forum_topic_reopen(tr::now),\n\tst::historyContactStatusButton))\n, _bar(parent, object_ptr<Ui::FlatButton>::fromRaw(_reopen)) {\n\tsetupState();\n\tsetupHandler();\n}\n\nvoid TopicReopenBar::setupState() {\n\tconst auto channel = _topic->channel();\n\tauto canToggle = !channel\n\t\t? (rpl::single(false) | rpl::type_erased)\n\t\t: (_topic->my() || channel->amCreator())\n\t\t? (rpl::single(true) | rpl::type_erased)\n\t\t: channel->adminRightsValue(\n\t\t) | rpl::map([=] { return _topic->canToggleClosed(); });\n\n\trpl::combine(\n\t\t_topic->session().changes().topicFlagsValue(\n\t\t\t_topic,\n\t\t\tData::TopicUpdate::Flag::Closed),\n\t\tstd::move(canToggle)\n\t) | rpl::on_next([=](const auto &, bool can) {\n\t\t_bar.toggleContent(can && _topic->closed());\n\t}, _bar.lifetime());\n}\n\nvoid TopicReopenBar::setupHandler() {\n\t_reopen->setClickedCallback([=] {\n\t\t_topic->setClosedAndSave(false);\n\t});\n}\n\nclass PaysStatus::Bar final : public Ui::RpWidget {\npublic:\n\tBar(QWidget *parent, not_null<PeerData*> peer);\n\n\tvoid showState(State state);\n\n\t[[nodiscard]] rpl::producer<> removeClicks() const;\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tint resizeGetHeight(int newWidth) override;\n\n\tnot_null<PeerData*> _peer;\n\tobject_ptr<Ui::FlatLabel> _label;\n\tobject_ptr<Ui::LinkButton> _remove;\n\trpl::event_stream<> _removeClicks;\n\n};\n\nPaysStatus::Bar::Bar(QWidget *parent, not_null<PeerData*> peer)\n: RpWidget(parent)\n, _peer(peer)\n, _label(this, st::paysStatusLabel)\n, _remove(this, tr::lng_payment_bar_button(tr::now)) {\n\t_label->setAttribute(Qt::WA_TransparentForMouseEvents);\n}\n\nvoid PaysStatus::Bar::showState(State state) {\n\t_label->setMarkedText(tr::lng_payment_bar_text(\n\t\ttr::now,\n\t\tlt_name,\n\t\tTextWithEntities{ _peer->shortName() },\n\t\tlt_cost,\n\t\tPaidSendButtonText(tr::now, state.perMessage),\n\t\ttr::marked));\n\tresizeToWidth(width());\n}\n\nrpl::producer<> PaysStatus::Bar::removeClicks() const {\n\treturn _remove->clicks() | rpl::to_empty;\n}\n\nvoid PaysStatus::Bar::paintEvent(QPaintEvent *e) {\n\tQPainter p(this);\n\tp.fillRect(e->rect(), st::historyContactStatusButton.bgColor);\n}\n\nint PaysStatus::Bar::resizeGetHeight(int newWidth) {\n\tconst auto skip = st::defaultPeerListItem.photoPosition.y();\n\t_label->resizeToWidth(newWidth - skip);\n\t_label->moveToLeft(skip, skip, newWidth);\n\t_remove->move(\n\t\t(newWidth - _remove->width()) / 2,\n\t\tskip + _label->height() + skip);\n\treturn _remove->y() + _remove->height() + skip;\n}\n\nPaysStatus::PaysStatus(\n\tnot_null<Window::SessionController*> window,\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<UserData*> user)\n: _controller(window)\n, _user(user)\n, _paidAlready(std::make_shared<rpl::variable<int>>())\n, _inner(Ui::CreateChild<Bar>(parent.get(), user))\n, _bar(parent, object_ptr<Bar>::fromRaw(_inner)) {\n\tsetupState();\n\tsetupHandlers();\n}\n\nvoid PaysStatus::setupState() {\n\t_user->session().api().requestPeerSettings(_user);\n\n\t_user->session().changes().peerFlagsValue(\n\t\t_user,\n\t\tData::PeerUpdate::Flag::PaysPerMessage\n\t) | rpl::on_next([=] {\n\t\t_state = State{ _user->paysPerMessage() };\n\t\tif (_state.perMessage > 0) {\n\t\t\t_inner->showState(_state);\n\t\t\t_bar.toggleContent(true);\n\t\t} else {\n\t\t\t_bar.toggleContent(false);\n\t\t}\n\t}, _bar.lifetime());\n}\n\nvoid PaysStatus::setupHandlers() {\n\t_inner->removeClicks(\n\t) | rpl::on_next([=] {\n\t\tWindow::PeerMenuConfirmToggleFee(\n\t\t\t_controller,\n\t\t\t_paidAlready,\n\t\t\t_user->session().user(),\n\t\t\t_user,\n\t\t\ttrue);\n\t}, _bar.lifetime());\n}\n\nvoid PaysStatus::show() {\n\tif (!_shown) {\n\t\t_shown = true;\n\t\tif (_state.perMessage > 0) {\n\t\t\t_inner->showState(_state);\n\t\t\t_bar.toggleContent(true);\n\t\t}\n\t}\n\t_bar.show();\n}\n\nvoid PaysStatus::hide() {\n\t_bar.hide();\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_contact_status.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/widgets/shadow.h\"\n\nnamespace Data {\nclass ForumTopic;\n} // namespace Data\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass FlatButton;\nclass IconButton;\nclass FlatLabel;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass SlidingBar final {\npublic:\n\tSlidingBar(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tobject_ptr<Ui::RpWidget> wrapped);\n\n\tvoid setVisible(bool visible);\n\tvoid raise();\n\n\tvoid move(int x, int y);\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] rpl::producer<int> heightValue() const;\n\tvoid toggleContent(bool visible);\n\n\tvoid show() {\n\t\tsetVisible(true);\n\t}\n\tvoid hide() {\n\t\tsetVisible(false);\n\t}\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tvoid setup(not_null<Ui::RpWidget*> parent);\n\n\tUi::SlideWrap<Ui::RpWidget> _wrapped;\n\tUi::PlainShadow _shadow;\n\tbool _shown = false;\n\tbool _contentShown = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\nclass ContactStatus final {\npublic:\n\tContactStatus(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<PeerData*> peer,\n\t\tbool showInForum);\n\n\tvoid show();\n\tvoid hide();\n\n\t[[nodiscard]] SlidingBar &bar() {\n\t\treturn _bar;\n\t}\n\nprivate:\n\tclass Bar;\n\tclass BgButton;\n\n\tstruct State {\n\t\tenum class Type {\n\t\t\tNone,\n\t\t\tReportSpam,\n\t\t\tAdd,\n\t\t\tAddOrBlock,\n\t\t\tUnarchiveOrBlock,\n\t\t\tUnarchiveOrReport,\n\t\t\tSharePhoneNumber,\n\t\t\tRequestChatInfo,\n\t\t\tSetBotPhoto,\n\t\t};\n\t\tType type = Type::None;\n\t\tint starsPerMessage = 0;\n\t\tQString requestChatName;\n\t\tTimeId requestDate = 0;\n\t\tbool requestChatIsBroadcast = false;\n\t};\n\n\tvoid setupState(not_null<PeerData*> peer, bool showInForum);\n\tvoid setupHandlers(not_null<PeerData*> peer);\n\tvoid setupAddHandler(not_null<UserData*> user);\n\tvoid setupBlockHandler(not_null<UserData*> user);\n\tvoid setupShareHandler(not_null<UserData*> user);\n\tvoid setupUnarchiveHandler(not_null<PeerData*> peer);\n\tvoid setupReportHandler(not_null<PeerData*> peer);\n\tvoid setupCloseHandler(not_null<PeerData*> peer);\n\tvoid setupRequestInfoHandler(not_null<PeerData*> peer);\n\tvoid setupEmojiStatusHandler(not_null<PeerData*> peer);\n\tvoid setupSetBotPhotoHandler(not_null<UserData*> user);\n\n\tstatic rpl::producer<State> PeerState(not_null<PeerData*> peer);\n\n\tconst not_null<Window::SessionController*> _controller;\n\tState _state;\n\tTextWithEntities _status;\n\tUi::Text::MarkedContext _context;\n\tQPointer<Bar> _inner;\n\tSlidingBar _bar;\n\tbool _hiddenByForum = false;\n\tbool _shown = false;\n\n};\n\nclass BusinessBotStatus final {\npublic:\n\tBusinessBotStatus(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<PeerData*> peer);\n\n\tvoid show();\n\tvoid hide();\n\n\t[[nodiscard]] SlidingBar &bar() {\n\t\treturn _bar;\n\t}\n\nprivate:\n\tclass Bar;\n\n\tstruct State {\n\t\tUserData *bot = nullptr;\n\t\tQString manageUrl;\n\t\tbool canReply = false;\n\t\tbool paused = false;\n\t};\n\n\tvoid setupState(not_null<PeerData*> peer);\n\tvoid setupHandlers(not_null<PeerData*> peer);\n\n\tstatic rpl::producer<State> PeerState(not_null<PeerData*> peer);\n\n\tconst not_null<Window::SessionController*> _controller;\n\tState _state;\n\tQPointer<Bar> _inner;\n\tSlidingBar _bar;\n\tbool _shown = false;\n\n};\n\nclass TopicReopenBar final {\npublic:\n\tTopicReopenBar(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Data::ForumTopic*> topic);\n\n\t[[nodiscard]] SlidingBar &bar() {\n\t\treturn _bar;\n\t}\n\nprivate:\n\tvoid setupState();\n\tvoid setupHandler();\n\n\tconst not_null<Data::ForumTopic*> _topic;\n\tQPointer<Ui::FlatButton> _reopen;\n\tSlidingBar _bar;\n\n};\n\nclass PaysStatus final {\npublic:\n\tPaysStatus(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<UserData*> user);\n\n\tvoid show();\n\tvoid hide();\n\n\t[[nodiscard]] SlidingBar &bar() {\n\t\treturn _bar;\n\t}\n\nprivate:\n\tclass Bar;\n\n\tstruct State {\n\t\tint perMessage = 0;\n\t};\n\n\tvoid setupState();\n\tvoid setupHandlers();\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<UserData*> _user;\n\tstd::shared_ptr<rpl::variable<int>> _paidAlready;\n\tState _state;\n\tQPointer<Bar> _inner;\n\tSlidingBar _bar;\n\tbool _shown = false;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_context_menu.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_context_menu.h\"\n\n#include \"forkgram/uri_menu.h\"\n#include \"history/view/history_view_context_menu_fork.h\"\n\n#include \"api/api_attached_stickers.h\"\n#include \"api/api_editing.h\"\n#include \"api/api_global_privacy.h\"\n#include \"api/api_polls.h\"\n#include \"api/api_report.h\"\n#include \"api/api_ringtones.h\"\n#include \"api/api_transcribes.h\"\n#include \"api/api_who_reacted.h\"\n#include \"api/api_toggling_media.h\" // Api::ToggleFavedSticker\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"base/unixtime.h\"\n#include \"history/view/history_view_list_widget.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_text.h\"\n#include \"history/view/history_view_schedule_box.h\"\n#include \"history/view/media/history_view_media.h\"\n#include \"history/view/media/history_view_save_document_action.h\"\n#include \"history/view/media/history_view_web_page.h\"\n#include \"history/view/reactions/history_view_reactions_list.h\"\n#include \"info/info_memento.h\"\n#include \"info/profile/info_profile_widget.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/menu/menu_common.h\"\n#include \"ui/widgets/menu/menu_multiline_action.h\"\n#include \"ui/widgets/menu/menu_separator.h\"\n#include \"ui/image/image.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/text/format_song_document_name.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/controls/delete_message_context_action.h\"\n#include \"ui/controls/who_reacted_context_action.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/boxes/edit_factcheck_box.h\"\n#include \"ui/boxes/report_box_graphics.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/pill_tabs.h\"\n#include \"menu/menu_item_download_files.h\"\n#include \"menu/menu_item_rate_transcribe.h\"\n#include \"menu/menu_item_rate_transcribe_session.h\"\n#include \"menu/menu_timecode_action.h\"\n#include \"menu/menu_send.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/show_or_premium_box.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/power_saving.h\"\n#include \"boxes/delete_messages_box.h\"\n#include \"boxes/moderate_messages_box.h\"\n#include \"boxes/report_messages_box.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"boxes/stickers_box.h\"\n#include \"boxes/translate_box.h\"\n#include \"data/components/factchecks.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_document.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_groups.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"chat_helpers/message_field.h\" // FactcheckFieldIniter.\n#include \"core/file_utilities.h\"\n#include \"core/click_handler_types.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/call_delayed.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"lang/lang_keys.h\"\n#include \"core/application.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"spellcheck/spellcheck_types.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QClipboard>\n\n#include \"history/view/history_view_context_menu_fork.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kRescheduleLimit = 20;\nconstexpr auto kTagNameLimit = 12;\nconstexpr auto kPublicPostLinkToastDuration = 4 * crl::time(1000);\n\nbool HasEditMessageAction(\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tconst auto item = request.item;\n\tconst auto context = list->elementContext();\n\tif (!item\n\t\t|| item->isSending()\n\t\t|| item->hasFailed()\n\t\t|| item->isEditingMedia()\n\t\t|| !request.selectedItems.empty()\n\t\t|| (context != Context::History\n\t\t\t&& context != Context::Replies\n\t\t\t&& context != Context::ShortcutMessages\n\t\t\t&& context != Context::ScheduledTopic\n\t\t\t&& context != Context::Monoforum)) {\n\t\treturn false;\n\t}\n\tconst auto peer = item->history()->peer;\n\tif (const auto channel = peer->asChannel()) {\n\t\tif (!channel->isMegagroup() && !channel->canEditMessages()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid SavePhotoToFile(not_null<PhotoData*> photo) {\n\tconst auto media = photo->activeMediaView();\n\tif (photo->isNull() || !media || !media->loaded()) {\n\t\treturn;\n\t}\n\n\tconst auto image = media->image(Data::PhotoSize::Large)->original(); // clazy:exclude=unused-non-trivial-variable\n\tFileDialog::GetWritePath(\n\t\tCore::App().getFileDialogParent(),\n\t\ttr::lng_save_photo(tr::now),\n\t\tu\"JPEG Image (*.jpg);;\"_q + FileDialog::AllFilesFilter(),\n\t\tfiledialogDefaultName(u\"photo\"_q, u\".jpg\"_q),\n\t\tcrl::guard(&photo->session(), [=](const QString &result) {\n\t\t\tif (!result.isEmpty()) {\n\t\t\t\tmedia->saveToFile(result);\n\t\t\t}\n\t\t}));\n}\n\nvoid CopyImage(not_null<PhotoData*> photo) {\n\tconst auto media = photo->activeMediaView();\n\tif (photo->isNull() || !media || !media->loaded()) {\n\t\treturn;\n\t}\n\tmedia->setToClipboard();\n}\n\nvoid ShowStickerPackInfo(\n\t\tnot_null<DocumentData*> document,\n\t\tnot_null<ListWidget*> list) {\n\tStickerSetBox::Show(list->controller()->uiShow(), document);\n}\n\nvoid ToggleFavedSticker(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId contextId) {\n\tApi::ToggleFavedSticker(controller->uiShow(), document, contextId);\n}\n\nvoid AddPhotoActions(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<PhotoData*> photo,\n\t\tHistoryItem *item,\n\t\tnot_null<ListWidget*> list) {\n\tconst auto contextId = item ? item->fullId() : FullMsgId();\n\tif (!list->hasCopyMediaRestriction(item)) {\n\t\tmenu->addAction(\n\t\t\ttr::lng_context_save_image(tr::now),\n\t\t\tbase::fn_delayed(\n\t\t\t\tst::defaultDropdownMenu.menu.ripple.hideDuration,\n\t\t\t\t&photo->session(),\n\t\t\t\t[=] { SavePhotoToFile(photo); }),\n\t\t\t&st::menuIconSaveImage);\n\t\tmenu->addAction(tr::lng_context_copy_image(tr::now), [=] {\n\t\t\tconst auto item = photo->owner().message(contextId);\n\t\t\tif (!list->showCopyMediaRestriction(item)) {\n\t\t\t\tCopyImage(photo);\n\t\t\t}\n\t\t}, &st::menuIconCopy);\n\t}\n\tif (photo->hasAttachedStickers()) {\n\t\tconst auto controller = list->controller();\n\t\tauto callback = [=] {\n\t\t\tauto &attached = photo->session().api().attachedStickers();\n\t\t\tattached.requestAttachedStickerSets(controller, photo);\n\t\t};\n\t\tmenu->addAction(\n\t\t\ttr::lng_context_attached_stickers(tr::now),\n\t\t\tstd::move(callback),\n\t\t\t&st::menuIconStickers);\n\t}\n}\n\nvoid SaveGif(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFullMsgId itemId) {\n\tif (const auto item = controller->session().data().message(itemId)) {\n\t\tif (const auto media = item->media()) {\n\t\t\tif (const auto document = media->document()) {\n\t\t\t\tApi::ToggleSavedGif(\n\t\t\t\t\tcontroller->uiShow(),\n\t\t\t\t\tdocument,\n\t\t\t\t\titem->fullId(),\n\t\t\t\t\ttrue);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid OpenGif(not_null<ListWidget*> list, FullMsgId itemId) {\n\tconst auto controller = list->controller();\n\tif (const auto item = controller->session().data().message(itemId)) {\n\t\tif (const auto media = item->media()) {\n\t\t\tif (const auto document = media->document()) {\n\t\t\t\tlist->elementOpenDocument(document, itemId, true);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ShowInFolder(not_null<DocumentData*> document) {\n\tconst auto filepath = document->filepath(true);\n\tif (!filepath.isEmpty()) {\n\t\tFile::ShowInFolder(filepath);\n\t}\n}\n\nvoid AddDocumentActions(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<DocumentData*> document,\n\t\tHistoryItem *item,\n\t\tnot_null<ListWidget*> list) {\n\tif (document->loading()) {\n\t\tmenu->addAction(tr::lng_context_cancel_download(tr::now), [=] {\n\t\t\tdocument->cancel();\n\t\t}, &st::menuIconCancel);\n\t\treturn;\n\t}\n\tconst auto controller = list->controller();\n\tconst auto contextId = item ? item->fullId() : FullMsgId();\n\tconst auto session = &document->session();\n\tif (item && document->isGifv()) {\n\t\tconst auto notAutoplayedGif = !Data::AutoDownload::ShouldAutoPlay(\n\t\t\tdocument->session().settings().autoDownload(),\n\t\t\titem->history()->peer,\n\t\t\tdocument);\n\t\tif (notAutoplayedGif) {\n\t\t\tconst auto weak = base::make_weak(list.get());\n\t\t\tmenu->addAction(tr::lng_context_open_gif(tr::now), [=] {\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tOpenGif(strong, contextId);\n\t\t\t\t}\n\t\t\t}, &st::menuIconShowInChat);\n\t\t}\n\t\tif (!list->hasCopyMediaRestriction(item)) {\n\t\t\tmenu->addAction(tr::lng_context_save_gif(tr::now), [=] {\n\t\t\t\tSaveGif(list->controller(), contextId);\n\t\t\t}, &st::menuIconGif);\n\t\t}\n\t}\n\tif (document->sticker() && document->sticker()->set) {\n\t\tmenu->addAction(\n\t\t\t(document->isStickerSetInstalled()\n\t\t\t\t? tr::lng_context_pack_info(tr::now)\n\t\t\t\t: tr::lng_context_pack_add(tr::now)),\n\t\t\t[=] { ShowStickerPackInfo(document, list); },\n\t\t\t&st::menuIconStickers);\n\t}\n\tif (document->sticker()) {\n\t\tconst auto isFaved = document->owner().stickers().isFaved(document);\n\t\tmenu->addAction(\n\t\t\t(isFaved\n\t\t\t\t? tr::lng_faved_stickers_remove(tr::now)\n\t\t\t\t: tr::lng_faved_stickers_add(tr::now)),\n\t\t\t[=] { ToggleFavedSticker(controller, document, contextId); },\n\t\t\tisFaved ? &st::menuIconUnfave : &st::menuIconFave);\n\t}\n\tif (!document->filepath(true).isEmpty()) {\n\t\tmenu->addAction(\n\t\t\t(Platform::IsMac()\n\t\t\t\t? tr::lng_context_show_in_finder(tr::now)\n\t\t\t\t: tr::lng_context_show_in_folder(tr::now)),\n\t\t\t[=] { ShowInFolder(document); },\n\t\t\t&st::menuIconShowInFolder);\n\t}\n\tif (document->hasAttachedStickers()) {\n\t\tconst auto controller = list->controller();\n\t\tauto callback = [=] {\n\t\t\tauto &attached = session->api().attachedStickers();\n\t\t\tattached.requestAttachedStickerSets(controller, document);\n\t\t};\n\t\tmenu->addAction(\n\t\t\ttr::lng_context_attached_stickers(tr::now),\n\t\t\tstd::move(callback),\n\t\t\t&st::menuIconStickers);\n\t}\n\tif (item && !list->hasCopyMediaRestriction(item)) {\n\t\tconst auto controller = list->controller();\n\t\tAddSaveSoundForNotifications(menu, item, document, controller);\n\t}\n\tif ((document->isVoiceMessage()\n\t\t\t|| document->isVideoMessage())\n\t\t&& Menu::HasRateTranscribeItem(item)) {\n\t\tif (!menu->empty()) {\n\t\t\tmenu->insertAction(0, base::make_unique_q<Menu::RateTranscribe>(\n\t\t\t\tmenu,\n\t\t\t\tmenu->st().menu,\n\t\t\t\tMenu::RateTranscribeCallbackFactory(item)));\n\t\t}\n\t}\n\tAddSaveDocumentAction(menu, item, document, list);\n\tAddCopyFilename(\n\t\tmenu,\n\t\tdocument,\n\t\t[=] { return list->showCopyRestrictionForSelected(); });\n}\n\nvoid AddPostLinkAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request) {\n\tconst auto item = request.item;\n\tif (!item\n\t\t|| !item->hasDirectLink()\n\t\t|| request.pointState == PointState::Outside) {\n\t\treturn;\n\t} else if (request.link\n\t\t&& !request.link->copyToClipboardContextItemText().isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto itemId = item->fullId();\n\tconst auto context = request.view\n\t\t? request.view->context()\n\t\t: Context::History;\n\tconst auto controller = request.navigation->parentController();\n\tmenu->addAction(\n\t\t(item->history()->peer->isMegagroup()\n\t\t\t? tr::lng_context_copy_message_link\n\t\t\t: tr::lng_context_copy_post_link)(tr::now),\n\t\t[=] { CopyPostLink(controller, itemId, context); },\n\t\t&st::menuIconLink);\n}\n\nMessageIdsList ExtractIdsList(const SelectedItems &items) {\n\treturn ranges::views::all(\n\t\titems\n\t) | ranges::views::transform(\n\t\t&SelectedItem::msgId\n\t) | ranges::to_vector;\n}\n\nbool AddForwardSelectedAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tif (!request.overSelection || request.selectedItems.empty()) {\n\t\treturn false;\n\t}\n\tif (!ranges::all_of(request.selectedItems, &SelectedItem::canForward)) {\n\t\treturn false;\n\t}\n\n\tmenu->addAction(tr::lng_context_forward_selected(tr::now), [=] {\n\t\tconst auto weak = base::make_weak(list);\n\t\tconst auto callback = [=] {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->cancelSelection();\n\t\t\t}\n\t\t};\n\t\tWindow::ShowForwardMessagesBox(\n\t\t\trequest.navigation,\n\t\t\tExtractIdsList(request.selectedItems),\n\t\t\tcallback);\n\t}, &st::menuIconForward);\n\treturn true;\n}\n\nbool AddForwardMessageAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tconst auto item = request.item;\n\tif (!request.selectedItems.empty()) {\n\t\treturn false;\n\t} else if (!item || !item->allowsForward()) {\n\t\treturn false;\n\t}\n\tconst auto owner = &item->history()->owner();\n\tconst auto asGroup = (request.pointState != PointState::GroupPart);\n\tif (asGroup) {\n\t\tif (const auto group = owner->groups().find(item)) {\n\t\t\tif (!ranges::all_of(group->items, &HistoryItem::allowsForward)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto itemId = item->fullId();\n\tmenu->addAction(tr::lng_context_forward_msg(tr::now), [=] {\n\t\tif (const auto item = owner->message(itemId)) {\n\t\t\tWindow::ShowForwardMessagesBox(\n\t\t\t\trequest.navigation,\n\t\t\t\t(asGroup\n\t\t\t\t\t? owner->itemOrItsGroup(item)\n\t\t\t\t\t: MessageIdsList{ 1, itemId }));\n\t\t}\n\t}, &st::menuIconForward);\n\treturn true;\n}\n\nvoid AddForwardAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tAddForwardSelectedAction(menu, request, list);\n\tAddForwardMessageAction(menu, request, list);\n}\n\nbool AddSendNowSelectedAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tif (!request.overSelection || request.selectedItems.empty()) {\n\t\treturn false;\n\t}\n\tif (!ranges::all_of(request.selectedItems, &SelectedItem::canSendNow)) {\n\t\treturn false;\n\t}\n\n\tconst auto session = &request.navigation->session();\n\tauto histories = ranges::views::all(\n\t\trequest.selectedItems\n\t) | ranges::views::transform([&](const SelectedItem &item) {\n\t\treturn session->data().message(item.msgId);\n\t}) | ranges::views::filter([](HistoryItem *item) {\n\t\treturn item != nullptr;\n\t}) | ranges::views::transform(\n\t\t&HistoryItem::history\n\t);\n\tif (histories.begin() == histories.end()) {\n\t\treturn false;\n\t}\n\tconst auto history = *histories.begin();\n\n\tmenu->addAction(tr::lng_context_send_now_selected(tr::now), [=] {\n\t\tconst auto weak = base::make_weak(list);\n\t\tconst auto callback = [=] {\n\t\t\trequest.navigation->showBackFromStack();\n\t\t};\n\t\tWindow::ShowSendNowMessagesBox(\n\t\t\trequest.navigation,\n\t\t\thistory,\n\t\t\tExtractIdsList(request.selectedItems),\n\t\t\tcallback);\n\t}, &st::menuIconSend);\n\treturn true;\n}\n\nbool AddSendNowMessageAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tconst auto item = request.item;\n\tif (!request.selectedItems.empty()) {\n\t\treturn false;\n\t} else if (!item || !item->allowsSendNow()) {\n\t\treturn false;\n\t}\n\tconst auto owner = &item->history()->owner();\n\tconst auto asGroup = (request.pointState != PointState::GroupPart);\n\tif (asGroup) {\n\t\tif (const auto group = owner->groups().find(item)) {\n\t\t\tif (!ranges::all_of(group->items, &HistoryItem::allowsSendNow)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto itemId = item->fullId();\n\tmenu->addAction(tr::lng_context_send_now_msg(tr::now), [=] {\n\t\tif (const auto item = owner->message(itemId)) {\n\t\t\tWindow::ShowSendNowMessagesBox(\n\t\t\t\trequest.navigation,\n\t\t\t\titem->history(),\n\t\t\t\t(asGroup\n\t\t\t\t\t? owner->itemOrItsGroup(item)\n\t\t\t\t\t: MessageIdsList{ 1, itemId }));\n\t\t}\n\t}, &st::menuIconSend);\n\treturn true;\n}\n\nbool AddRescheduleAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tconst auto owner = &request.navigation->session().data();\n\n\tconst auto goodSingle = HasEditMessageAction(request, list)\n\t\t&& request.item->allowsReschedule();\n\tconst auto goodMany = [&] {\n\t\tif (goodSingle) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto &items = request.selectedItems;\n\t\tif (!request.overSelection || items.empty()) {\n\t\t\treturn false;\n\t\t}\n\t\tif (items.size() > kRescheduleLimit) {\n\t\t\treturn false;\n\t\t}\n\t\treturn ranges::all_of(items, &SelectedItem::canReschedule);\n\t}();\n\tif (!goodSingle && !goodMany) {\n\t\treturn false;\n\t}\n\tauto ids = goodSingle\n\t\t? MessageIdsList{ request.item->fullId() }\n\t\t: ExtractIdsList(request.selectedItems);\n\tranges::sort(ids, [&](const FullMsgId &a, const FullMsgId &b) {\n\t\tconst auto itemA = owner->message(a);\n\t\tconst auto itemB = owner->message(b);\n\t\treturn (itemA && itemB) && (itemA->position() < itemB->position());\n\t});\n\n\tauto text = ((ids.size() == 1)\n\t\t? tr::lng_context_reschedule\n\t\t: tr::lng_context_reschedule_selected)(tr::now);\n\n\tmenu->addAction(std::move(text), [=] {\n\t\tconst auto firstItem = owner->message(ids.front());\n\t\tif (!firstItem) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto callback = [=](Api::SendOptions options) {\n\t\t\tlist->cancelSelection();\n\t\t\tauto groupedIds = std::vector<MessageGroupId>();\n\t\t\tfor (const auto &id : ids) {\n\t\t\t\tconst auto item = owner->message(id);\n\t\t\t\tif (!item || !item->isScheduled()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (const auto groupId = item->groupId()) {\n\t\t\t\t\tif (ranges::contains(groupedIds, groupId)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tgroupedIds.push_back(groupId);\n\t\t\t\t}\n\t\t\t\tApi::RescheduleMessage(item, options);\n\t\t\t\t// Increase the scheduled date by 1s to keep the order.\n\t\t\t\toptions.scheduled += 1;\n\t\t\t}\n\t\t};\n\n\t\tconst auto peer = firstItem->history()->peer;\n\t\tconst auto sendMenuType = !peer\n\t\t\t? SendMenu::Type::Disabled\n\t\t\t: peer->starsPerMessageChecked()\n\t\t\t? SendMenu::Type::SilentOnly\n\t\t\t: peer->isSelf()\n\t\t\t? SendMenu::Type::Reminder\n\t\t\t: HistoryView::CanScheduleUntilOnline(peer)\n\t\t\t? SendMenu::Type::ScheduledToUser\n\t\t\t: SendMenu::Type::Disabled;\n\n\t\tconst auto itemDate = firstItem->date();\n\t\tconst auto date = (itemDate == Api::kScheduledUntilOnlineTimestamp)\n\t\t\t? HistoryView::DefaultScheduleTime()\n\t\t\t: itemDate + (firstItem->isScheduled() ? 0 : crl::time(600));\n\t\tconst auto repeatPeriod = firstItem->scheduleRepeatPeriod();\n\n\t\tconst auto box = request.navigation->parentController()->show(\n\t\t\tHistoryView::PrepareScheduleBox(\n\t\t\t\t&request.navigation->session(),\n\t\t\t\trequest.navigation->uiShow(),\n\t\t\t\t{ .type = sendMenuType, .effectAllowed = false },\n\t\t\t\tcallback,\n\t\t\t\t{ .scheduleRepeatPeriod = repeatPeriod },\n\t\t\t\tdate));\n\n\t\towner->itemRemoved(\n\t\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\t\tif (ranges::contains(ids, item->fullId())) {\n\t\t\t\tbox->closeBox();\n\t\t\t}\n\t\t}, box->lifetime());\n\t}, &st::menuIconReschedule);\n\treturn true;\n}\n\nbool AddReplyToMessageAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tconst auto context = list->elementContext();\n\tconst auto item = request.quote.item\n\t\t? request.quote.item\n\t\t: request.item;\n\tconst auto topic = item ? item->topic() : nullptr;\n\tconst auto peer = item ? item->history()->peer.get() : nullptr;\n\tif (!item\n\t\t|| !item->isRegular()\n\t\t|| (context != Context::History\n\t\t\t&& context != Context::Replies\n\t\t\t&& context != Context::Monoforum)) {\n\t\treturn false;\n\t}\n\tconst auto canSendReply = topic\n\t\t? Data::CanSendAnything(topic)\n\t\t: Data::CanSendAnything(peer);\n\tconst auto canReply = canSendReply || item->allowsForward();\n\tif (!canReply) {\n\t\treturn false;\n\t}\n\n\tconst auto todoListTaskId = request.link\n\t\t? request.link->property(kTodoListItemIdProperty).toInt()\n\t\t: 0;\n\tconst auto &quote = request.quote;\n\tauto text = (todoListTaskId\n\t\t? tr::lng_context_reply_to_task\n\t\t: quote.highlight.quote.empty()\n\t\t? tr::lng_context_reply_msg\n\t\t: tr::lng_context_quote_and_reply)(\n\t\t\ttr::now,\n\t\t\tUi::Text::FixAmpersandInAction);\n\tmenu->addAction(std::move(text), [=, itemId = item->fullId()] {\n\t\tlist->replyToMessageRequestNotify({\n\t\t\t.messageId = itemId,\n\t\t\t.quote = quote.highlight.quote,\n\t\t\t.quoteOffset = quote.highlight.quoteOffset,\n\t\t\t.todoItemId = todoListTaskId,\n\t\t}, base::IsCtrlPressed());\n\t}, &st::menuIconReply);\n\treturn true;\n}\n\nbool AddTodoListAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tconst auto context = list->elementContext();\n\tconst auto item = request.item;\n\tif (!item\n\t\t|| !Window::PeerMenuShowAddTodoListTasks(item)\n\t\t|| (context != Context::History\n\t\t\t&& context != Context::Replies\n\t\t\t&& context != Context::Monoforum\n\t\t\t&& context != Context::Pinned)) {\n\t\treturn false;\n\t}\n\tconst auto itemId = item->fullId();\n\tconst auto controller = list->controller();\n\tmenu->addAction(tr::lng_todo_add_title(tr::now), [=] {\n\t\tif (const auto item = controller->session().data().message(itemId)) {\n\t\t\tWindow::PeerMenuAddTodoListTasks(controller, item);\n\t\t}\n\t}, &st::menuIconAdd);\n\treturn true;\n}\n\nbool AddViewRepliesAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tconst auto context = list->elementContext();\n\tconst auto item = request.item;\n\tif (!item\n\t\t|| !item->isRegular()\n\t\t|| (context != Context::History && context != Context::Pinned)) {\n\t\treturn false;\n\t}\n\tconst auto topicRootId = item->history()->isForum()\n\t\t? item->topicRootId()\n\t\t: 0;\n\tconst auto repliesCount = item->repliesCount();\n\tconst auto withReplies = (repliesCount > 0);\n\tif (!withReplies || !item->history()->peer->isMegagroup()) {\n\t\tif (!topicRootId) {\n\t\t\treturn false;\n\t\t}\n\t}\n\tconst auto rootId = topicRootId\n\t\t? topicRootId\n\t\t: repliesCount\n\t\t? item->id\n\t\t: item->replyToTop();\n\tconst auto highlightId = topicRootId ? item->id : 0;\n\tconst auto phrase = topicRootId\n\t\t? tr::lng_replies_view_topic(tr::now)\n\t\t: (repliesCount > 0)\n\t\t? tr::lng_replies_view(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\trepliesCount)\n\t\t: tr::lng_replies_view_thread(tr::now);\n\tconst auto controller = list->controller();\n\tconst auto history = item->history();\n\tmenu->addAction(phrase, crl::guard(controller, [=] {\n\t\tcontroller->showRepliesForMessage(\n\t\t\thistory,\n\t\t\trootId,\n\t\t\thighlightId);\n\t}), &st::menuIconViewReplies);\n\treturn true;\n}\n\nbool AddEditMessageAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tif (!HasEditMessageAction(request, list)) {\n\t\treturn false;\n\t}\n\tconst auto item = request.item;\n\tif (!item->allowsEdit(base::unixtime::now())) {\n\t\treturn false;\n\t}\n\tconst auto owner = &item->history()->owner();\n\tconst auto itemId = item->fullId();\n\tmenu->addAction(tr::lng_context_edit_msg(tr::now), [=] {\n\t\tconst auto item = owner->message(itemId);\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\t\tlist->editMessageRequestNotify(item->fullId());\n\t}, &st::menuIconEdit);\n\treturn true;\n}\n\nvoid AddFactcheckAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tconst auto item = request.item;\n\tif (!item || !item->history()->session().factchecks().canEdit(item)) {\n\t\treturn;\n\t}\n\tconst auto itemId = item->fullId();\n\tconst auto text = item->factcheckText();\n\tconst auto session = &item->history()->session();\n\tconst auto phrase = text.empty()\n\t\t? tr::lng_context_add_factcheck(tr::now)\n\t\t: tr::lng_context_edit_factcheck(tr::now);\n\tmenu->addAction(phrase, [=] {\n\t\tconst auto limit = session->factchecks().lengthLimit();\n\t\tconst auto controller = request.navigation->parentController();\n\t\tcontroller->show(Box(EditFactcheckBox, text, limit, [=](\n\t\t\t\tTextWithEntities result) {\n\t\t\tconst auto show = controller->uiShow();\n\t\t\tsession->factchecks().save(itemId, text, result, show);\n\t\t}, FactcheckFieldIniter(controller->uiShow())));\n\t}, &st::menuIconFactcheck);\n}\n\nbool AddPinMessageAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tconst auto context = list->elementContext();\n\tconst auto item = request.item;\n\tif (!item || !item->isRegular()) {\n\t\treturn false;\n\t}\n\tconst auto topic = item->topic();\n\tconst auto sublist = item->savedSublist();\n\tif (context != Context::History && context != Context::Pinned) {\n\t\tif ((context != Context::Replies || !topic)\n\t\t\t&& (context != Context::Monoforum\n\t\t\t\t|| !sublist\n\t\t\t\t|| !item->history()->amMonoforumAdmin())) {\n\t\t\treturn false;\n\t\t}\n\t}\n\tconst auto group = item->history()->owner().groups().find(item);\n\tconst auto pinItem = ((item->canPin() && item->isPinned()) || !group)\n\t\t? item\n\t\t: group->items.front().get();\n\tif (!pinItem->canPin()) {\n\t\treturn false;\n\t}\n\tconst auto pinItemId = pinItem->fullId();\n\tconst auto isPinned = pinItem->isPinned();\n\tconst auto controller = list->controller();\n\tmenu->addAction(isPinned ? tr::lng_context_unpin_msg(tr::now) : tr::lng_context_pin_msg(tr::now), crl::guard(controller, [=] {\n\t\tWindow::ToggleMessagePinned(controller, pinItemId, !isPinned);\n\t}), isPinned ? &st::menuIconUnpin : &st::menuIconPin);\n\treturn true;\n}\n\nbool AddGoToMessageAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tconst auto context = list->elementContext();\n\tconst auto view = request.view;\n\tif (!view\n\t\t|| !view->data()->isRegular()\n\t\t|| (context != Context::Pinned\n\t\t\t&& context != Context::ChatPreview)\n\t\t|| (context == Context::Pinned && !view->hasOutLayout())) {\n\t\treturn false;\n\t}\n\tconst auto itemId = view->data()->fullId();\n\tconst auto controller = list->controller();\n\tmenu->addAction(tr::lng_context_to_msg(tr::now), crl::guard(controller, [=] {\n\t\tif (const auto item = controller->session().data().message(itemId)) {\n\t\t\tcontroller->showMessage(item);\n\t\t}\n\t}), &st::menuIconShowInChat);\n\treturn true;\n}\n\nvoid AddSendNowAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tAddSendNowSelectedAction(menu, request, list);\n\tAddSendNowMessageAction(menu, request, list);\n}\n\nbool AddDeleteSelectedAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tif (!request.overSelection || request.selectedItems.empty()) {\n\t\treturn false;\n\t}\n\tif (!ranges::all_of(request.selectedItems, &SelectedItem::canDelete)) {\n\t\treturn false;\n\t}\n\n\tmenu->addAction(tr::lng_context_delete_selected(tr::now), [=] {\n\t\tauto items = ExtractIdsList(request.selectedItems);\n\t\tauto box = Box<DeleteMessagesBox>(\n\t\t\t&request.navigation->session(),\n\t\t\tstd::move(items));\n\t\tbox->setDeleteConfirmedCallback(crl::guard(list, [=] {\n\t\t\tlist->cancelSelection();\n\t\t}));\n\t\trequest.navigation->parentController()->show(std::move(box));\n\t}, &st::menuIconDelete);\n\treturn true;\n}\n\nbool AddDeleteMessageAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tconst auto item = request.item;\n\tif (!request.selectedItems.empty()) {\n\t\treturn false;\n\t} else if (!item || !item->canDelete()) {\n\t\treturn false;\n\t}\n\tconst auto owner = &item->history()->owner();\n\tconst auto asGroup = (request.pointState != PointState::GroupPart);\n\tif (asGroup) {\n\t\tif (const auto group = owner->groups().find(item)) {\n\t\t\tif (ranges::any_of(group->items, [](auto item) {\n\t\t\t\treturn item->isLocal() || !item->canDelete();\n\t\t\t})) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto controller = list->controller();\n\tconst auto itemId = item->fullId();\n\tconst auto callback = crl::guard(controller, [=] {\n\t\tif (const auto item = owner->message(itemId)) {\n\t\t\tif (asGroup) {\n\t\t\t\tif (const auto group = owner->groups().find(item)) {\n\t\t\t\t\tcontroller->show(Box<DeleteMessagesBox>(\n\t\t\t\t\t\t&owner->session(),\n\t\t\t\t\t\towner->itemsToIds(group->items)));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (item->isUploading()) {\n\t\t\t\tcontroller->cancelUploadLayer(item);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto list = HistoryItemsList{ item };\n\t\t\tif (CanCreateModerateMessagesBox(list)) {\n\t\t\t\tconst auto opt = DefaultModerateMessagesBoxOptions();\n\t\t\t\tcontroller->show(\n\t\t\t\t\tBox(CreateModerateMessagesBox, list, nullptr, opt));\n\t\t\t} else {\n\t\t\t\tconst auto suggestModerateActions = false;\n\t\t\t\tcontroller->show(\n\t\t\t\t\tBox<DeleteMessagesBox>(item, suggestModerateActions));\n\t\t\t}\n\t\t}\n\t});\n\tif (item->isUploading()) {\n\t\tmenu->addAction(\n\t\t\ttr::lng_context_cancel_upload(tr::now),\n\t\t\tcallback,\n\t\t\t&st::menuIconCancel);\n\t\treturn true;\n\t}\n\tmenu->addAction(Ui::DeleteMessageContextAction(\n\t\tmenu->menu(),\n\t\tcallback,\n\t\titem->ttlDestroyAt(),\n\t\t[=] { delete menu; }));\n\treturn true;\n}\n\nvoid AddDeleteAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tif (!AddDeleteSelectedAction(menu, request, list)) {\n\t\tAddDeleteMessageAction(menu, request, list);\n\t}\n}\n\nvoid AddDownloadFilesAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tif (!request.overSelection\n\t\t|| request.selectedItems.empty()\n\t\t|| list->hasCopyRestrictionForSelected()) {\n\t\treturn;\n\t}\n\tMenu::AddDownloadFilesAction(\n\t\tmenu,\n\t\trequest.navigation->parentController(),\n\t\trequest.selectedItems,\n\t\tlist);\n}\n\nvoid AddReportAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tconst auto item = request.item;\n\tif (!request.selectedItems.empty()) {\n\t\treturn;\n\t} else if (!item || !item->suggestReport()) {\n\t\treturn;\n\t}\n\tconst auto owner = &item->history()->owner();\n\tconst auto controller = list->controller();\n\tconst auto itemId = item->fullId();\n\tconst auto callback = crl::guard(controller, [=] {\n\t\tif (const auto item = owner->message(itemId)) {\n\t\t\tconst auto group = owner->groups().find(item);\n\t\t\tconst auto ids = group\n\t\t\t\t? (ranges::views::all(\n\t\t\t\t\tgroup->items\n\t\t\t\t) | ranges::views::transform([](const auto &i) {\n\t\t\t\t\treturn i->fullId().msg;\n\t\t\t\t}) | ranges::to_vector)\n\t\t\t\t: std::vector<MsgId>{ 1, itemId.msg };\n\t\t\tconst auto peer = item->history()->peer;\n\t\t\tShowReportMessageBox(controller->uiShow(), peer, ids, {});\n\t\t}\n\t});\n\tmenu->addAction(\n\t\ttr::lng_context_report_msg(tr::now),\n\t\tcallback,\n\t\t&st::menuIconReport);\n}\n\nbool AddClearSelectionAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tif (!request.overSelection || request.selectedItems.empty()) {\n\t\treturn false;\n\t}\n\tmenu->addAction(tr::lng_context_clear_selection(tr::now), [=] {\n\t\tlist->cancelSelection();\n\t}, &st::menuIconSelect);\n\treturn true;\n}\n\nbool AddSelectMessageAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tconst auto item = request.item;\n\tif (request.overSelection && !request.selectedItems.empty()) {\n\t\treturn false;\n\t} else if (!item\n\t\t|| item->isLocal()\n\t\t|| item->isService()\n\t\t|| list->hasSelectRestriction()) {\n\t\treturn false;\n\t}\n\tconst auto owner = &item->history()->owner();\n\tconst auto itemId = item->fullId();\n\tconst auto asGroup = (request.pointState != PointState::GroupPart);\n\tmenu->addAction(tr::lng_context_select_msg(tr::now), [=] {\n\t\tif (const auto item = owner->message(itemId)) {\n\t\t\tif (asGroup) {\n\t\t\t\tlist->selectItemAsGroup(item);\n\t\t\t} else {\n\t\t\t\tlist->selectItem(item);\n\t\t\t}\n\t\t}\n\t}, &st::menuIconSelect);\n\treturn true;\n}\n\nvoid AddSelectionAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tif (!AddClearSelectionAction(menu, request, list)) {\n\t\tAddSelectMessageAction(menu, request, list);\n\t}\n\tFork::AddShowSumDurations(\n\t\tmenu,\n\t\trequest.selectedItems,\n\t\tlist->controller());\n}\n\nvoid AddTopMessageActions(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tAddGoToMessageAction(menu, request, list);\n\tAddViewRepliesAction(menu, request, list);\n\tAddEditMessageAction(menu, request, list);\n\tAddFactcheckAction(menu, request, list);\n\tAddPinMessageAction(menu, request, list);\n}\n\nvoid AddMessageActions(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ContextMenuRequest &request,\n\t\tnot_null<ListWidget*> list) {\n\tAddPostLinkAction(menu, request);\n\tAddForwardAction(menu, request, list);\n\tAddSendNowAction(menu, request, list);\n\tAddDeleteAction(menu, request, list);\n\tAddDownloadFilesAction(menu, request, list);\n\tAddReportAction(menu, request, list);\n\tAddSelectionAction(menu, request, list);\n\tAddRescheduleAction(menu, request, list);\n\tconst auto controller = request.navigation->parentController();\n\tFork::AddReplaceMedia(menu, request.item, controller);\n\tif (request.selectedItems.size() == 2) {\n\t\tconst auto item1 = controller->session().data().message(\n\t\t\trequest.selectedItems[0].msgId);\n\t\tconst auto item2 = controller->session().data().message(\n\t\t\trequest.selectedItems[1].msgId);\n\t\tif (item1 && item2) {\n\t\t\tFork::AddSwapMedia(menu, item1, item2, controller, [=] {\n\t\t\t\tlist->cancelSelection();\n\t\t\t});\n\t\t}\n\t}\n}\n\nvoid AddCopyLinkAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst ClickHandlerPtr &link) {\n\tif (!link) {\n\t\treturn;\n\t}\n\tconst auto action = link->copyToClipboardContextItemText();\n\tif (action.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto text = link->copyToClipboardText();\n\tmenu->addAction(\n\t\taction,\n\t\t[=] { QGuiApplication::clipboard()->setText(text); },\n\t\t&st::menuIconCopy);\n\n\tForkgram::FillUrlWithCustomUri(menu, text);\n}\n\nvoid EditTagBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst Data::ReactionId &id) {\n\tconst auto owner = &controller->session().data();\n\tconst auto title = owner->reactions().myTagTitle(id);\n\tbox->setTitle(title.isEmpty()\n\t\t? tr::lng_context_tag_add_name()\n\t\t: tr::lng_context_tag_edit_name());\n\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\tbox,\n\t\ttr::lng_edit_tag_about(),\n\t\tst::editTagAbout));\n\tconst auto field = box->addRow(object_ptr<Ui::InputField>(\n\t\tbox,\n\t\tst::editTagField,\n\t\ttr::lng_edit_tag_name(),\n\t\ttitle));\n\tfield->setMaxLength(kTagNameLimit * 2);\n\tbox->setFocusCallback([=] {\n\t\tfield->setFocusFast();\n\t});\n\n\tstruct State {\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji> custom;\n\t\tQImage image;\n\t};\n\tconst auto state = field->lifetime().make_state<State>();\n\n\tif (const auto customId = id.custom()) {\n\t\tstate->custom = owner->customEmojiManager().create(\n\t\t\tcustomId,\n\t\t\t[=] { field->update(); });\n\t} else {\n\t\towner->reactions().preloadReactionImageFor(id);\n\t}\n\tfield->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(field);\n\t\tconst auto top = st::editTagField.textMargins.top();\n\t\tif (const auto custom = state->custom.get()) {\n\t\t\tconst auto inactive = !field->window()->isActiveWindow();\n\t\t\tcustom->paint(p, {\n\t\t\t\t.textColor = st::windowFg->c,\n\t\t\t\t.now = crl::now(),\n\t\t\t\t.position = QPoint(0, top),\n\t\t\t\t.paused = inactive || On(PowerSaving::kEmojiChat),\n\t\t\t});\n\t\t} else {\n\t\t\tif (state->image.isNull()) {\n\t\t\t\tstate->image = owner->reactions().resolveReactionImageFor(\n\t\t\t\t\tid);\n\t\t\t}\n\t\t\tif (!state->image.isNull()) {\n\t\t\t\tconst auto size = st::reactionInlineSize;\n\t\t\t\tconst auto skip = (size - st::reactionInlineImage) / 2;\n\t\t\t\tp.drawImage(skip, top + skip, state->image);\n\t\t\t}\n\t\t}\n\t}, field->lifetime());\n\n\tUi::AddLengthLimitLabel(field, kTagNameLimit);\n\n\tconst auto save = [=] {\n\t\tconst auto text = field->getLastText();\n\t\tif (text.size() > kTagNameLimit) {\n\t\t\tfield->showError();\n\t\t\treturn;\n\t\t}\n\t\tconst auto weak = base::make_weak(box);\n\t\tcontroller->session().data().reactions().renameTag(id, text);\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t};\n\n\tfield->submits(\n\t) | rpl::on_next(save, field->lifetime());\n\n\tbox->addButton(tr::lng_settings_save(), save);\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nvoid ShowWhoReadInfo(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFullMsgId itemId,\n\t\tUi::WhoReadParticipant who) {\n\tconst auto peer = controller->session().data().peer(itemId.peer);\n\tconst auto participant = peer->owner().peer(PeerId(who.id));\n\tconst auto migrated = participant->migrateFrom();\n\tconst auto origin = who.dateReacted\n\t\t? Info::Profile::Origin{\n\t\t\tInfo::Profile::GroupReactionOrigin{ peer, itemId.msg },\n\t\t}\n\t\t: Info::Profile::Origin();\n\tauto memento = std::make_shared<Info::Memento>(\n\t\tstd::vector<std::shared_ptr<Info::ContentMemento>>{\n\t\tstd::make_shared<Info::Profile::Memento>(\n\t\t\tparticipant,\n\t\t\tmigrated ? migrated->id : PeerId(),\n\t\t\torigin),\n\t});\n\tcontroller->showSection(std::move(memento));\n}\n\n[[nodiscard]] rpl::producer<not_null<UserData*>> LookupMessageAuthor(\n\t\tnot_null<HistoryItem*> item) {\n\tstruct Author {\n\t\tUserData *user = nullptr;\n\t\tstd::vector<Fn<void(UserData*)>> callbacks;\n\t};\n\tstruct Authors {\n\t\tbase::flat_map<FullMsgId, Author> map;\n\t};\n\tstatic auto Cache = base::flat_map<not_null<Main::Session*>, Authors>();\n\n\tconst auto channel = item->history()->peer->asChannel();\n\tconst auto session = &channel->session();\n\tconst auto id = item->fullId();\n\tif (!Cache.contains(session)) {\n\t\tCache.emplace(session);\n\t\tsession->lifetime().add([session] {\n\t\t\tCache.remove(session);\n\t\t});\n\t}\n\n\treturn [channel, id](auto consumer) {\n\t\tconst auto session = &channel->session();\n\t\tauto &map = Cache[session].map;\n\t\tauto i = map.find(id);\n\t\tif (i == end(map)) {\n\t\t\ti = map.emplace(id).first;\n\t\t\tconst auto finishWith = [=](UserData *user) {\n\t\t\t\tauto &entry = Cache[session].map[id];\n\t\t\t\tentry.user = user;\n\t\t\t\tfor (const auto &callback : base::take(entry.callbacks)) {\n\t\t\t\t\tcallback(user);\n\t\t\t\t}\n\t\t\t};\n\t\t\tsession->api().request(MTPchannels_GetMessageAuthor(\n\t\t\t\tchannel->inputChannel(),\n\t\t\t\tMTP_int(id.msg.bare)\n\t\t\t)).done([=](const MTPUser &result) {\n\t\t\t\tfinishWith(session->data().processUser(result));\n\t\t\t}).fail([=] {\n\t\t\t\tfinishWith(nullptr);\n\t\t\t}).send();\n\t\t} else if (const auto user = i->second.user\n\t\t\t; user || i->second.callbacks.empty()) {\n\t\t\tif (user) {\n\t\t\t\tconsumer.put_next(not_null(user));\n\t\t\t}\n\t\t\treturn rpl::lifetime();\n\t\t}\n\n\t\tauto lifetime = rpl::lifetime();\n\t\tconst auto done = [=](UserData *result) {\n\t\t\tif (result) {\n\t\t\t\tconsumer.put_next(not_null(result));\n\t\t\t}\n\t\t};\n\t\tconst auto guard = lifetime.make_state<base::has_weak_ptr>();\n\t\ti->second.callbacks.push_back(crl::guard(guard, done));\n\t\treturn lifetime;\n\t};\n}\n\n[[nodiscard]] base::unique_qptr<Ui::Menu::ItemBase> MakeMessageAuthorAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto parent = menu->menu();\n\tconst auto user = std::make_shared<UserData*>(nullptr);\n\tconst auto action = Ui::Menu::CreateAction(\n\t\tparent,\n\t\ttr::lng_contacts_loading(tr::now),\n\t\t[=] { if (*user) { controller->showPeerInfo(*user); } });\n\taction->setDisabled(true);\n\tauto lifetime = LookupMessageAuthor(\n\t\titem\n\t) | rpl::on_next([=](not_null<UserData*> author) {\n\t\taction->setText(\n\t\t\ttr::lng_context_sent_by(tr::now, lt_user, author->name()));\n\t\taction->setDisabled(false);\n\t\t*user = author;\n\t});\n\tauto result = base::make_unique_q<Ui::Menu::Action>(\n\t\tmenu->menu(),\n\t\tst::whoSentItem,\n\t\taction,\n\t\tnullptr,\n\t\tnullptr);\n\tresult->lifetime().add(std::move(lifetime));\n\treturn result;\n}\n\n} // namespace\n\nstd::optional<QString> CurrentVoiceTimecode(FullMsgId itemId) {\n\tconst auto state = ::Media::Player::instance()->getState(\n\t\tAudioMsgId::Type::Voice);\n\tif (state.id.contextId() == itemId\n\t\t&& !::Media::Player::IsStoppedOrStopping(state.state)\n\t\t&& state.frequency > 0) {\n\t\treturn Ui::FormatDurationText(state.position / state.frequency);\n\t}\n\treturn std::nullopt;\n}\n\nrpl::producer<QString> VoiceTimecodeUpdates(FullMsgId itemId) {\n\treturn ::Media::Player::instance()->updatedNotifier(\n\t) | rpl::filter([=](const ::Media::Player::TrackState &state) {\n\t\treturn (state.id.type() == AudioMsgId::Type::Voice)\n\t\t\t&& (state.id.contextId() == itemId);\n\t}) | rpl::filter([](const ::Media::Player::TrackState &state) {\n\t\treturn !::Media::Player::IsStoppedOrStopping(state.state)\n\t\t\t&& state.frequency > 0;\n\t}) | rpl::map([](const ::Media::Player::TrackState &state) {\n\t\treturn Ui::FormatDurationText(state.position / state.frequency);\n\t}) | rpl::distinct_until_changed();\n}\n\nvoid InsertPollHiddenResultsLabel(not_null<Ui::PopupMenu*> menu) {\n\tauto label = base::make_unique_q<Ui::Menu::MultilineAction>(\n\t\tmenu->menu(),\n\t\tmenu->st().menu,\n\t\tst::historyHasCustomEmoji,\n\t\tst::historyHasCustomEmojiPosition,\n\t\ttr::lng_polls_context_ends(tr::now, tr::rich));\n\tmenu->insertAction(0, std::move(label));\n\tconst auto sepAction = new QAction(menu->menu());\n\tsepAction->setSeparator(true);\n\tauto separator = base::make_unique_q<Ui::Menu::Separator>(\n\t\tmenu->menu(),\n\t\tmenu->st().menu,\n\t\tmenu->st().menu.separator,\n\t\tsepAction);\n\tmenu->insertAction(1, std::move(separator));\n}\n\nContextMenuRequest::ContextMenuRequest(\n\tnot_null<Window::SessionNavigation*> navigation)\n: navigation(navigation) {\n}\n\nvoid FillContextMenuItems(\n\t\tnot_null<Ui::PopupMenu*> result,\n\t\tnot_null<ListWidget*> list,\n\t\tconst ContextMenuRequest &request,\n\t\tbool skipWhoReacted = false) {\n\tconst auto link = request.link;\n\tconst auto view = request.view;\n\tconst auto item = request.item;\n\tconst auto itemId = item ? item->fullId() : FullMsgId();\n\tconst auto lnkPhoto = link\n\t\t? reinterpret_cast<PhotoData*>(\n\t\t\tlink->property(kPhotoLinkMediaProperty).toULongLong())\n\t\t: nullptr;\n\tconst auto lnkDocument = link\n\t\t? reinterpret_cast<DocumentData*>(\n\t\t\tlink->property(kDocumentLinkMediaProperty).toULongLong())\n\t\t: nullptr;\n\tconst auto poll = item\n\t\t? (item->media() ? item->media()->poll() : nullptr)\n\t\t: nullptr;\n\tconst auto hasSelection = !request.selectedItems.empty()\n\t\t|| !request.selectedText.empty();\n\tconst auto hasWhoReactedItem = item\n\t\t&& Api::WhoReactedExists(item, Api::WhoReactedList::All);\n\n\tAddReplyToMessageAction(result, request, list);\n\tif (item) {\n\t\tconst auto media = item->media();\n\t\tconst auto document = media ? media->document() : nullptr;\n\t\tconst auto topic = item->topic();\n\t\tconst auto peer = item->history()->peer.get();\n\t\tconst auto canSendText = topic\n\t\t\t? Data::CanSendAnything(topic)\n\t\t\t: Data::CanSendAnything(peer);\n\t\tif (canSendText && document && document->isVoiceMessage()) {\n\t\t\tconst auto msgId = item->fullId();\n\t\t\tif (const auto timecode = CurrentVoiceTimecode(msgId)) {\n\t\t\t\tconst auto weak = base::make_weak(list.get());\n\t\t\t\tMenu::AddTimecodeAction(\n\t\t\t\t\tresult,\n\t\t\t\t\t*timecode,\n\t\t\t\t\tVoiceTimecodeUpdates(msgId),\n\t\t\t\t\t[=] {\n\t\t\t\t\t\tconst auto tc = CurrentVoiceTimecode(msgId);\n\t\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\t\tstrong->replyToMessageRequestNotify(\n\t\t\t\t\t\t\t\t{ .messageId = msgId },\n\t\t\t\t\t\t\t\tbase::IsCtrlPressed());\n\t\t\t\t\t\t\tstrong->insertTextAtCursor(\n\t\t\t\t\t\t\t\ttc.value_or(*timecode));\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\tAddTodoListAction(result, request, list);\n\n\tif (request.overSelection\n\t\t&& !list->hasCopyRestrictionForSelected()\n\t\t&& !list->getSelectedText().empty()) {\n\t\tconst auto text = request.selectedItems.empty()\n\t\t\t? tr::lng_context_copy_selected(tr::now)\n\t\t\t: tr::lng_context_copy_selected_items(tr::now);\n\t\tresult->addAction(text, [=] {\n\t\t\tif (!list->showCopyRestrictionForSelected()) {\n\t\t\t\tTextUtilities::SetClipboardText(list->getSelectedText());\n\t\t\t}\n\t\t}, &st::menuIconCopy);\n\t}\n\tif (request.overSelection\n\t\t&& !Ui::SkipTranslate(list->getSelectedText().rich)) {\n\t\tconst auto owner = &view->history()->owner();\n\t\tresult->addAction(tr::lng_context_translate_selected(tr::now), [=] {\n\t\t\tif (const auto item = owner->message(itemId)) {\n\t\t\t\tlist->controller()->show(Box(\n\t\t\t\t\tUi::TranslateBox,\n\t\t\t\t\titem->history()->peer,\n\t\t\t\t\tMsgId(),\n\t\t\t\t\tlist->getSelectedText().rich,\n\t\t\t\t\tlist->hasCopyRestrictionForSelected()));\n\t\t\t}\n\t\t}, &st::menuIconTranslate);\n\t}\n\n\tAddTopMessageActions(result, request, list);\n\tif (lnkPhoto && request.selectedItems.empty()) {\n\t\tAddPhotoActions(result, lnkPhoto, item, list);\n\t} else if (lnkDocument) {\n\t\tAddDocumentActions(result, lnkDocument, item, list);\n\t} else if (poll) {\n\t\tconst auto context = list->elementContext();\n\t\tAddPollActions(\n\t\t\tresult,\n\t\t\tpoll,\n\t\t\titem,\n\t\t\tcontext,\n\t\t\tlist->controller(),\n\t\t\tskipWhoReacted);\n\t} else if (!request.overSelection && view && !hasSelection) {\n\t\tconst auto owner = &view->history()->owner();\n\t\tconst auto media = view->media();\n\t\tconst auto mediaHasTextForCopy = media && media->hasTextForCopy();\n\t\tif (const auto document = media ? media->getDocument() : nullptr) {\n\t\t\tAddDocumentActions(result, document, view->data(), list);\n\t\t}\n\t\tif (!link && (view->hasVisibleText() || mediaHasTextForCopy)) {\n\t\t\tif (!list->hasCopyRestriction(view->data())) {\n\t\t\t\tconst auto asGroup = (request.pointState != PointState::GroupPart);\n\t\t\t\tresult->addAction(tr::lng_context_copy_text(tr::now), [=] {\n\t\t\t\t\tif (const auto item = owner->message(itemId)) {\n\t\t\t\t\t\tif (!list->showCopyRestriction(item)) {\n\t\t\t\t\t\t\tif (asGroup) {\n\t\t\t\t\t\t\t\tif (const auto group = owner->groups().find(item)) {\n\t\t\t\t\t\t\t\t\tTextUtilities::SetClipboardText(HistoryGroupText(group));\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tTextUtilities::SetClipboardText(HistoryItemText(item));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}, &st::menuIconCopy);\n\t\t\t}\n\n\t\t\tconst auto translate = mediaHasTextForCopy\n\t\t\t\t? (HistoryView::TransribedText(item)\n\t\t\t\t\t.append('\\n')\n\t\t\t\t\t.append(item->originalText()))\n\t\t\t\t: item->originalText();\n\t\t\tif ((!item->translation() || !item->history()->translatedTo())\n\t\t\t\t&& !translate.text.isEmpty()\n\t\t\t\t&& !Ui::SkipTranslate(translate)) {\n\t\t\t\tresult->addAction(tr::lng_context_translate(tr::now), [=] {\n\t\t\t\t\tif (const auto item = owner->message(itemId)) {\n\t\t\t\t\t\tlist->controller()->show(Box(\n\t\t\t\t\t\t\tUi::TranslateBox,\n\t\t\t\t\t\t\titem->history()->peer,\n\t\t\t\t\t\t\tmediaHasTextForCopy\n\t\t\t\t\t\t\t\t? MsgId()\n\t\t\t\t\t\t\t\t: item->fullId().msg,\n\t\t\t\t\t\t\ttranslate,\n\t\t\t\t\t\t\tlist->hasCopyRestriction(view->data())));\n\t\t\t\t\t}\n\t\t\t\t}, &st::menuIconTranslate);\n\t\t\t}\n\t\t}\n\t}\n\n\tAddCopyLinkAction(result, link);\n\tAddMessageActions(result, request, list);\n\n\tconst auto wasAmount = result->actions().size();\n\tif (const auto textItem = view ? view->textItem() : item) {\n\t\tAddEmojiPacksAction(\n\t\t\tresult,\n\t\t\ttextItem,\n\t\t\tHistoryView::EmojiPacksSource::Message,\n\t\t\tlist->controller());\n\t}\n\tif (item) {\n\t\tconst auto added = (result->actions().size() > wasAmount);\n\t\tAddSelectRestrictionAction(result, item, !added);\n\t}\n\tif (!skipWhoReacted) {\n\t\tif (hasWhoReactedItem) {\n\t\t\tAddWhoReactedAction(result, list, item, list->controller());\n\t\t} else if (item) {\n\t\t\tMaybeAddWhenEditedForwardedAction(\n\t\t\t\tresult,\n\t\t\t\titem,\n\t\t\t\tlist->controller());\n\t\t}\n\t}\n}\n\nbase::unique_qptr<Ui::PopupMenu> FillContextMenu(\n\t\tnot_null<ListWidget*> list,\n\t\tconst ContextMenuRequest &request) {\n\tconst auto link = request.link;\n\tconst auto item = request.item;\n\tconst auto itemId = item ? item->fullId() : FullMsgId();\n\tconst auto pollOption = link\n\t\t? link->property(kPollOptionProperty).toByteArray()\n\t\t: QByteArray();\n\tconst auto hasPollOption = !pollOption.isEmpty() && item;\n\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tlist,\n\t\tst::popupMenuWithIcons);\n\n\t// Build the full message menu.\n\tFillContextMenuItems(result, list, request, hasPollOption);\n\n\tif (item) {\n\t\tconst auto media = item->media();\n\t\tconst auto poll = media ? media->poll() : nullptr;\n\t\tif (poll && !poll->closed() && poll->hideResultsUntilClose()) {\n\t\t\tInsertPollHiddenResultsLabel(result.get());\n\t\t}\n\t}\n\n\tif (hasPollOption) {\n\t\tconst auto raw = result.get();\n\t\tconst auto owner = &item->history()->owner();\n\t\tconst auto controller = list->controller();\n\t\traw->stashContent([=](not_null<Ui::PopupMenu*> menu) {\n\t\t\tFillPollOptionPage(\n\t\t\t\tmenu,\n\t\t\t\towner,\n\t\t\t\titemId,\n\t\t\t\tpollOption,\n\t\t\t\tcontroller,\n\t\t\t\t[=] {\n\t\t\t\t\tlist->replyToMessageRequestNotify({\n\t\t\t\t\t\t.messageId = itemId,\n\t\t\t\t\t\t.pollOption = pollOption,\n\t\t\t\t\t}, base::IsCtrlPressed());\n\t\t\t\t});\n\t\t});\n\t}\n\treturn result;\n}\n\nvoid CopyPostLink(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFullMsgId itemId,\n\t\tContext context,\n\t\tstd::optional<TimeId> videoTimestamp) {\n\tCopyPostLink(controller->uiShow(), itemId, context, videoTimestamp);\n}\n\nvoid CopyPostLink(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tFullMsgId itemId,\n\t\tContext context,\n\t\tstd::optional<TimeId> videoTimestamp) {\n\tconst auto item = show->session().data().message(itemId);\n\tif (!item || !item->hasDirectLink()) {\n\t\treturn;\n\t}\n\tconst auto inRepliesContext = (context == Context::Replies);\n\tconst auto forceNonPublicLink = !videoTimestamp && base::IsCtrlPressed();\n\tQGuiApplication::clipboard()->setText(\n\t\titem->history()->session().api().exportDirectMessageLink(\n\t\t\titem,\n\t\t\tinRepliesContext,\n\t\t\tforceNonPublicLink,\n\t\t\tvideoTimestamp));\n\n\tconst auto isPublicLink = [&] {\n\t\tif (forceNonPublicLink) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto channel = item->history()->peer->asChannel();\n\t\tAssert(channel != nullptr);\n\t\tif (const auto rootId = item->replyToTop()) {\n\t\t\tconst auto root = item->history()->owner().message(\n\t\t\t\tchannel->id,\n\t\t\t\trootId);\n\t\t\tconst auto sender = root\n\t\t\t\t? root->discussionPostOriginalSender()\n\t\t\t\t: nullptr;\n\t\t\tif (sender && sender->hasUsername()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn channel->hasUsername();\n\t}();\n\tif (isPublicLink && !videoTimestamp) {\n\t\tshow->showToast({\n\t\t\t.text = tr::lng_channel_public_link_copied(\n\t\t\t\ttr::now, tr::bold\n\t\t\t).append('\\n').append(Platform::IsMac()\n\t\t\t\t? tr::lng_public_post_private_hint_cmd(tr::now)\n\t\t\t\t: tr::lng_public_post_private_hint_ctrl(tr::now)),\n\t\t\t.duration = kPublicPostLinkToastDuration,\n\t\t});\n\t} else {\n\t\tshow->showToast(isPublicLink\n\t\t\t? tr::lng_channel_public_link_copied(tr::now)\n\t\t\t: tr::lng_context_about_private_link(tr::now));\n\t}\n}\n\nvoid CopyStoryLink(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tFullStoryId storyId) {\n\tconst auto session = &show->session();\n\tconst auto maybeStory = session->data().stories().lookup(storyId);\n\tif (!maybeStory) {\n\t\treturn;\n\t}\n\tconst auto story = *maybeStory;\n\tQGuiApplication::clipboard()->setText(\n\t\tsession->api().exportDirectStoryLink(story));\n\tshow->showToast(tr::lng_channel_public_link_copied(tr::now));\n}\n\nvoid FillPollOptionPage(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<Data::Session*> owner,\n\t\tFullMsgId itemId,\n\t\tconst QByteArray &pollOption,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFn<void()> replyToOption) {\n\tconst auto item = owner->message(itemId);\n\tif (!item) {\n\t\treturn;\n\t}\n\tconst auto media = item->media();\n\tconst auto poll = media ? media->poll() : nullptr;\n\tif (!poll) {\n\t\treturn;\n\t}\n\tif (!poll->closed()\n\t\t&& !poll->quiz()\n\t\t&& poll->voted()\n\t\t&& !poll->revotingDisabled()) {\n\t\tmenu->addAction(\n\t\t\ttr::lng_polls_retract(tr::now),\n\t\t\t[=] { poll->session().api().polls().sendVotes(itemId, {}); },\n\t\t\t&st::menuIconRetractVote);\n\t}\n\tif (replyToOption) {\n\t\tmenu->addAction(\n\t\t\ttr::lng_context_reply_to_poll_option(\n\t\t\t\ttr::now,\n\t\t\t\tUi::Text::FixAmpersandInAction),\n\t\t\tstd::move(replyToOption),\n\t\t\t&st::menuIconReply);\n\t}\n\tconst auto a = poll->answerByOption(pollOption);\n\tif (!a) {\n\t\treturn;\n\t}\n\tauto text = a->text;\n\tmenu->addAction(\n\t\ttr::lng_context_copy_poll_option(tr::now),\n\t\t[text = TextForMimeData::Rich(std::move(text))] {\n\t\t\tTextUtilities::SetClipboardText(text);\n\t\t},\n\t\t&st::menuIconCopy);\n\tif (item->hasDirectLink()) {\n\t\tconst auto link = item->history()->session().api()\n\t\t\t.exportDirectMessageLink(item, false);\n\t\tconst auto separator = (link.indexOf('?') >= 0) ? u'&' : u'?';\n\t\tconst auto optionLink = link\n\t\t\t+ separator\n\t\t\t+ u\"option=\"_q\n\t\t\t+ PollOptionToLink(pollOption);\n\t\tmenu->addAction(\n\t\t\ttr::lng_context_copy_poll_option_link(tr::now),\n\t\t\t[optionLink] {\n\t\t\t\tQGuiApplication::clipboard()->setText(optionLink);\n\t\t\t},\n\t\t\t&st::menuIconLink);\n\t}\n\tconst auto canDelete = [&] {\n\t\tif (!a->addedDate) {\n\t\t\treturn false;\n\t\t}\n\t\tif (poll->creator()) {\n\t\t\treturn true;\n\t\t}\n\t\tif (a->addedBy && a->addedBy->isSelf()) {\n\t\t\tconst auto period = poll->session()\n\t\t\t\t.appConfig()\n\t\t\t\t.pollAnswerDeletePeriod();\n\t\t\treturn (base::unixtime::now() - a->addedDate) < period;\n\t\t}\n\t\treturn false;\n\t}();\n\tif (canDelete) {\n\t\tmenu->addAction(\n\t\t\ttr::lng_context_delete_poll_option(tr::now),\n\t\t\t[=] {\n\t\t\t\tif (const auto item = owner->message(itemId)) {\n\t\t\t\t\tif (const auto media = item->media()) {\n\t\t\t\t\t\tif (const auto poll = media->poll()) {\n\t\t\t\t\t\t\tpoll->session().api().polls()\n\t\t\t\t\t\t\t\t.deleteAnswer(itemId, pollOption);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t&st::menuIconDelete);\n\t}\n\tif (const auto addedBy = a->addedBy) {\n\t\tmenu->addSeparator(&st::expandedMenuSeparator);\n\t\tconst auto date = a->addedDate\n\t\t\t? Ui::FormatDateTime(\n\t\t\t\tbase::unixtime::parse(a->addedDate))\n\t\t\t: QString();\n\t\tauto action = base::make_unique_q<Ui::WhoReactedEntryAction>(\n\t\t\tmenu->menu(),\n\t\t\tnullptr,\n\t\t\tmenu->menu()->st(),\n\t\t\tUi::WhoReactedEntryData());\n\t\tconst auto raw = action.get();\n\t\tconst auto thumbnail = Ui::MakeUserpicThumbnail(addedBy);\n\t\tconst auto size = st::defaultWhoRead.photoSize;\n\t\tconst auto refresh = [=] {\n\t\t\traw->setData({\n\t\t\t\t.text = tr::lng_polls_option_added_by(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\taddedBy->shortName()),\n\t\t\t\t.date = date,\n\t\t\t\t.type = Ui::WhoReactedType::RefRecipient,\n\t\t\t\t.userpic = thumbnail->image(size),\n\t\t\t\t.callback = [=] { controller->showPeerInfo(addedBy); },\n\t\t\t});\n\t\t};\n\t\tthumbnail->subscribeToUpdates(refresh);\n\t\trefresh();\n\t\tmenu->lifetime().add([=] {\n\t\t\tthumbnail->subscribeToUpdates(nullptr);\n\t\t});\n\t\tmenu->addAction(std::move(action));\n\t}\n\t{\n\t\tauto packIds = std::vector<StickerSetIdentifier>();\n\t\tfor (const auto &entity : a->text.entities) {\n\t\t\tif (entity.type() == EntityType::CustomEmoji) {\n\t\t\t\tconst auto id = Data::ParseCustomEmojiData(entity.data());\n\t\t\t\tif (const auto set = owner->document(id)->sticker()) {\n\t\t\t\t\tif (set->set.id\n\t\t\t\t\t\t&& !ranges::contains(\n\t\t\t\t\t\t\tpackIds,\n\t\t\t\t\t\t\tset->set.id,\n\t\t\t\t\t\t\t&StickerSetIdentifier::id)) {\n\t\t\t\t\t\tpackIds.push_back(set->set);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tAddEmojiPacksAction(\n\t\t\tmenu,\n\t\t\tstd::move(packIds),\n\t\t\tEmojiPacksSource::PollOption,\n\t\t\tcontroller);\n\t}\n}\n\nvoid AttachPollOptionTabs(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tQPoint desiredPosition) {\n\tif (!menu->hasStashedContent()) {\n\t\treturn;\n\t}\n\tconst auto &tabsSt = st::popupMenuPillTabs;\n\tconst auto tabs = Ui::CreateChild<Ui::PillTabs>(\n\t\tmenu.get(),\n\t\tstd::vector<QString>{\n\t\t\ttr::lng_context_poll_option_tab(tr::now),\n\t\t\ttr::lng_context_poll_message_tab(tr::now),\n\t\t},\n\t\t0,\n\t\ttabsSt);\n\n\tconst auto height = tabsSt.height;\n\tconst auto margin = tabsSt.margin;\n\ttabs->setShadow(st::defaultBoxShadow);\n\ttabs->show();\n\n\t// Reserve space for tabs by increasing additional padding top.\n\t{\n\t\tauto padding = menu->additionalMenuPadding();\n\t\tauto margins = menu->additionalMenuMargins();\n\t\tpadding.setTop(padding.top() + height + margin);\n\t\tmenu->setAdditionalMenuPadding(padding, margins);\n\t\tmenu->prepareGeometryFor(desiredPosition);\n\t}\n\n\t// Position tabs just above _inner (in the reserved padding space).\n\tconst auto reposition = [=] {\n\t\tconst auto inner = menu->inner();\n\t\ttabs->setGeometry(tabs->shadowExtend()\n\t\t\t+ QRect(\n\t\t\t\tinner.x(),\n\t\t\t\tinner.y() - height - margin,\n\t\t\t\tinner.width(),\n\t\t\t\theight));\n\t};\n\treposition();\n\n\t// Wire tab changes to swap stashed content.\n\ttabs->activeIndexChanges(\n\t) | rpl::on_next([=](int index) {\n\t\tconst auto direction = (index > 0)\n\t\t\t? Ui::PopupMenu::SwitchDirection::LeftToRight\n\t\t\t: Ui::PopupMenu::SwitchDirection::RightToLeft;\n\t\tcrl::on_main(menu, [=] {\n\t\t\tmenu->swapStashed(direction);\n\t\t});\n\t}, tabs->lifetime());\n\n\t// Reposition after geometry is prepared.\n\tmenu->animatePhaseValue(\n\t) | rpl::on_next([=](Ui::PopupMenu::AnimatePhase phase) {\n\t\tif (phase == Ui::PopupMenu::AnimatePhase::StartShow) {\n\t\t\treposition();\n\t\t}\n\t}, tabs->lifetime());\n\n\tmenu->showStateValue(\n\t) | rpl::on_next([=](Ui::PopupMenu::ShowState showState) {\n\t\tif (showState.appearing) {\n\t\t\ttabs->show();\n\t\t\ttabs->raise();\n\t\t\tconst auto raw = showState.widthProgress;\n\t\t\tconst auto delayed = std::clamp(\n\t\t\t\t(raw - 0.4) / 0.6,\n\t\t\t\t0.,\n\t\t\t\t1.);\n\t\t\ttabs->setShowProgress(\n\t\t\t\tdelayed,\n\t\t\t\tshowState.opacity * delayed);\n\t\t} else if (showState.toggling) {\n\t\t\ttabs->setShowProgress(1., showState.opacity);\n\t\t} else {\n\t\t\ttabs->show();\n\t\t\ttabs->setShowProgress(1., 1.);\n\t\t\treposition();\n\t\t}\n\t}, tabs->lifetime());\n}\n\nvoid AddPollActions(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<PollData*> poll,\n\t\tnot_null<HistoryItem*> item,\n\t\tContext context,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tbool skipRetractVote) {\n\t{\n\t\tconstexpr auto kRadio = \"\\xf0\\x9f\\x94\\x98\";\n\t\tconst auto radio = QString::fromUtf8(kRadio);\n\t\tauto text = poll->question;\n\t\tfor (const auto &answer : poll->answers) {\n\t\t\ttext.append('\\n').append(radio).append(answer.text);\n\t\t}\n\t\tif (!Ui::SkipTranslate(text)) {\n\t\t\tmenu->addAction(tr::lng_context_translate(tr::now), [=] {\n\t\t\t\tcontroller->show(Box(\n\t\t\t\t\tUi::TranslateBox,\n\t\t\t\t\titem->history()->peer,\n\t\t\t\t\tMsgId(),\n\t\t\t\t\tstd::move(text),\n\t\t\t\t\titem->forbidsForward()));\n\t\t\t}, &st::menuIconTranslate);\n\t\t}\n\t}\n\tif ((context != Context::History)\n\t\t&& (context != Context::Replies)\n\t\t&& (context != Context::Pinned)\n\t\t&& (context != Context::ScheduledTopic)\n\t\t&& (context != Context::ChatPreview)) {\n\t\treturn;\n\t}\n\tif (poll->closed()) {\n\t\treturn;\n\t}\n\tconst auto itemId = item->fullId();\n\tif (!skipRetractVote\n\t\t&& poll->voted()\n\t\t&& !poll->quiz()\n\t\t&& !poll->revotingDisabled()) {\n\t\tmenu->addAction(tr::lng_polls_retract(tr::now), [=] {\n\t\t\tpoll->session().api().polls().sendVotes(itemId, {});\n\t\t}, &st::menuIconRetractVote);\n\t}\n\tif (item->canStopPoll()) {\n\t\tmenu->addAction(tr::lng_polls_stop(tr::now), [=] {\n\t\t\tcontroller->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_polls_stop_warning(),\n\t\t\t\t.confirmed = [=](Fn<void()> &&close) {\n\t\t\t\t\tclose();\n\t\t\t\t\tif (const auto item = poll->owner().message(itemId)) {\n\t\t\t\t\t\tcontroller->session().api().polls().close(item);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t.confirmText = tr::lng_polls_stop_sure(),\n\t\t\t\t.cancelText = tr::lng_cancel(),\n\t\t\t}));\n\t\t}, &st::menuIconRemove);\n\t}\n}\n\nvoid AddSaveSoundForNotifications(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<DocumentData*> document,\n\t\tnot_null<Window::SessionController*> controller) {\n\tif (ItemHasTtl(item)) {\n\t\treturn;\n\t}\n\tconst auto &ringtones = document->session().api().ringtones();\n\tif (document->size > ringtones.maxSize()) {\n\t\treturn;\n\t} else if (ranges::contains(ringtones.list(), document->id)) {\n\t\treturn;\n\t} else if (int(ringtones.list().size()) >= ringtones.maxSavedCount()) {\n\t\treturn;\n\t} else if (document->song()) {\n\t\tif (document->duration() > ringtones.maxDuration()) {\n\t\t\treturn;\n\t\t}\n\t} else if (document->voice()) {\n\t\tif (document->duration() > ringtones.maxDuration()) {\n\t\t\treturn;\n\t\t}\n\t} else {\n\t\treturn;\n\t}\n\tconst auto show = controller->uiShow();\n\tmenu->addAction(tr::lng_context_save_custom_sound(tr::now), [=] {\n\t\tApi::ToggleSavedRingtone(\n\t\t\tdocument,\n\t\t\titem->fullId(),\n\t\t\t[=] { show->showToast(\n\t\t\t\ttr::lng_ringtones_toast_added(tr::now)); },\n\t\t\ttrue);\n\t}, &st::menuIconSoundAdd);\n}\n\nvoid AddWhenEditedForwardedAuthorActionHelper(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tbool insertSeparator) {\n\tif (const auto forwarded = item->Get<HistoryMessageForwarded>()) {\n\t\tif (!forwarded->story && forwarded->psaType.isEmpty()) {\n\t\t\tif (insertSeparator && !menu->empty()) {\n\t\t\t\tmenu->addSeparator(&st::expandedMenuSeparator);\n\t\t\t}\n\t\t\tmenu->addAction(Ui::WhenReadContextAction(\n\t\t\t\tmenu.get(),\n\t\t\t\tApi::WhenOriginal(item->from(), forwarded->originalDate)));\n\t\t}\n\t} else if (const auto edited = item->Get<HistoryMessageEdited>()) {\n\t\tif (!item->hideEditedBadge()) {\n\t\t\tif (insertSeparator && !menu->empty()) {\n\t\t\t\tmenu->addSeparator(&st::expandedMenuSeparator);\n\t\t\t}\n\t\t\tmenu->addAction(Ui::WhenReadContextAction(\n\t\t\t\tmenu.get(),\n\t\t\t\tApi::WhenEdited(item->from(), edited->date)));\n\t\t}\n\t}\n\tif (item->canLookupMessageAuthor()) {\n\t\tif (insertSeparator && !menu->empty()) {\n\t\t\tmenu->addSeparator(&st::expandedMenuSeparator);\n\t\t}\n\t\tmenu->addAction(MakeMessageAuthorAction(menu, item, controller));\n\t}\n}\n\nvoid AddWhoReactedAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<QWidget*> context,\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto whoReadIds = std::make_shared<Api::WhoReadList>();\n\tconst auto weak = base::make_weak(menu.get());\n\tconst auto user = item->history()->peer;\n\tconst auto showOrPremium = [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->hideMenu();\n\t\t}\n\t\tconst auto type = Ui::ShowOrPremium::ReadTime;\n\t\tconst auto name = user->shortName();\n\t\tauto box = Box(Ui::ShowOrPremiumBox, type, name, [=] {\n\t\t\tconst auto api = &controller->session().api();\n\t\t\tapi->globalPrivacy().updateHideReadTime({});\n\t\t}, [=] {\n\t\t\tSettings::ShowPremium(controller, u\"revtime_hidden\"_q);\n\t\t});\n\t\tcontroller->show(std::move(box));\n\t};\n\tconst auto itemId = item->fullId();\n\tconst auto participantChosen = [=](Ui::WhoReadParticipant who) {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->hideMenu();\n\t\t}\n\t\tShowWhoReadInfo(controller, itemId, who);\n\t};\n\tconst auto showAllChosen = [=, itemId = item->fullId()]{\n\t\t// Pressing on an item that has a submenu doesn't hide it :(\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->hideMenu();\n\t\t}\n\t\tif (const auto item = controller->session().data().message(itemId)) {\n\t\t\tcontroller->showSection(\n\t\t\t\tstd::make_shared<Info::Memento>(\n\t\t\t\t\twhoReadIds,\n\t\t\t\t\titemId,\n\t\t\t\t\tHistoryView::Reactions::DefaultSelectedTab(\n\t\t\t\t\t\titem,\n\t\t\t\t\t\twhoReadIds)));\n\t\t}\n\t};\n\tif (!menu->empty()) {\n\t\tmenu->addSeparator(&st::expandedMenuSeparator);\n\t}\n\tif (item->history()->peer->isUser()) {\n\t\tAddWhenEditedForwardedAuthorActionHelper(\n\t\t\tmenu,\n\t\t\titem,\n\t\t\tcontroller,\n\t\t\tfalse);\n\t\tmenu->addAction(Ui::WhenReadContextAction(\n\t\t\tmenu.get(),\n\t\t\tApi::WhoReacted(item, context, st::defaultWhoRead, whoReadIds),\n\t\t\tshowOrPremium));\n\t} else {\n\t\tmenu->addAction(Ui::WhoReactedContextAction(\n\t\t\tmenu.get(),\n\t\t\tApi::WhoReacted(item, context, st::defaultWhoRead, whoReadIds),\n\t\t\tData::ReactedMenuFactory(&controller->session()),\n\t\t\tparticipantChosen,\n\t\t\tshowAllChosen));\n\t\tAddWhenEditedForwardedAuthorActionHelper(\n\t\t\tmenu,\n\t\t\titem,\n\t\t\tcontroller,\n\t\t\ttrue);\n\t}\n}\n\nvoid MaybeAddWhenEditedForwardedAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<Window::SessionController*> controller) {\n\tAddWhenEditedForwardedAuthorActionHelper(menu, item, controller, true);\n}\n\nvoid AddEditTagAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst Data::ReactionId &id,\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto owner = &controller->session().data();\n\tconst auto editLabel = owner->reactions().myTagTitle(id).isEmpty()\n\t\t? tr::lng_context_tag_add_name(tr::now)\n\t\t: tr::lng_context_tag_edit_name(tr::now);\n\tmenu->addAction(editLabel, [=] {\n\t\tcontroller->show(Box(EditTagBox, controller, id));\n\t}, &st::menuIconTagRename);\n}\n\nvoid AddTagPackAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst Data::ReactionId &id,\n\t\tnot_null<Window::SessionController*> controller) {\n\tif (const auto custom = id.custom()) {\n\t\tconst auto owner = &controller->session().data();\n\t\tif (const auto set = owner->document(custom)->sticker()) {\n\t\t\tif (set->set.id) {\n\t\t\t\tAddEmojiPacksAction(\n\t\t\t\t\tmenu,\n\t\t\t\t\t{ set->set },\n\t\t\t\t\tEmojiPacksSource::Tag,\n\t\t\t\t\tcontroller);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ShowTagMenu(\n\t\tnot_null<base::unique_qptr<Ui::PopupMenu>*> menu,\n\t\tQPoint position,\n\t\tnot_null<QWidget*> context,\n\t\tnot_null<HistoryItem*> item,\n\t\tconst Data::ReactionId &id,\n\t\tnot_null<Window::SessionController*> controller) {\n\tusing namespace Data;\n\tconst auto itemId = item->fullId();\n\tconst auto owner = &controller->session().data();\n\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tcontext,\n\t\tst::popupMenuExpandedSeparator);\n\t(*menu)->addAction(tr::lng_context_filter_by_tag(tr::now), [=] {\n\t\tHashtagClickHandler(SearchTagToQuery(id)).onClick({\n\t\t\t.button = Qt::LeftButton,\n\t\t\t.other = QVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.sessionWindow = controller,\n\t\t\t}),\n\t\t});\n\t}, &st::menuIconTagFilter);\n\n\tAddEditTagAction(menu->get(), id, controller);\n\n\tconst auto removeTag = [=] {\n\t\tif (const auto item = owner->message(itemId)) {\n\t\t\tconst auto &list = item->reactions();\n\t\t\tif (ranges::contains(list, id, &MessageReaction::id)) {\n\t\t\t\titem->toggleReaction(id, HistoryReactionSource::Quick);\n\t\t\t}\n\t\t}\n\t};\n\t(*menu)->addAction(base::make_unique_q<Ui::Menu::Action>(\n\t\t(*menu)->menu(),\n\t\tst::menuWithIconsAttention,\n\t\tUi::Menu::CreateAction(\n\t\t\t(*menu)->menu(),\n\t\t\ttr::lng_context_remove_tag(tr::now),\n\t\t\tremoveTag),\n\t\t&st::menuIconTagRemoveAttention,\n\t\t&st::menuIconTagRemoveAttention));\n\n\tAddTagPackAction(menu->get(), id, controller);\n\n\t(*menu)->popup(position);\n}\n\nvoid ShowTagInListMenu(\n\t\tnot_null<base::unique_qptr<Ui::PopupMenu>*> menu,\n\t\tQPoint position,\n\t\tnot_null<QWidget*> context,\n\t\tconst Data::ReactionId &id,\n\t\tnot_null<Window::SessionController*> controller) {\n\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tcontext,\n\t\tst::popupMenuExpandedSeparator);\n\n\tAddEditTagAction(menu->get(), id, controller);\n\tAddTagPackAction(menu->get(), id, controller);\n\n\t(*menu)->popup(position);\n}\n\nvoid AddCopyFilename(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<DocumentData*> document,\n\t\tFn<bool()> showCopyRestrictionForSelected) {\n\tconst auto filenameToCopy = [&] {\n\t\tif (document->isAudioFile()) {\n\t\t\treturn TextForMimeData().append(\n\t\t\t\tUi::Text::FormatSongNameFor(document).string());\n\t\t} else if (document->sticker()\n\t\t\t|| document->isAnimation()\n\t\t\t|| document->isVideoMessage()\n\t\t\t|| document->isVideoFile()\n\t\t\t|| document->isVoiceMessage()) {\n\t\t\treturn TextForMimeData();\n\t\t} else {\n\t\t\treturn TextForMimeData().append(document->filename());\n\t\t}\n\t}();\n\tif (!filenameToCopy.empty()) {\n\t\tmenu->addAction(tr::lng_context_copy_filename(tr::now), [=] {\n\t\t\tif (!showCopyRestrictionForSelected()) {\n\t\t\t\tTextUtilities::SetClipboardText(filenameToCopy);\n\t\t\t}\n\t\t}, &st::menuIconCopy);\n\t}\n}\n\nvoid ShowWhoReactedMenu(\n\t\tnot_null<base::unique_qptr<Ui::PopupMenu>*> menu,\n\t\tQPoint position,\n\t\tnot_null<QWidget*> context,\n\t\tnot_null<HistoryItem*> item,\n\t\tconst Data::ReactionId &id,\n\t\tnot_null<Window::SessionController*> controller,\n\t\trpl::lifetime &lifetime) {\n\tif (item->reactionsAreTags()) {\n\t\tShowTagMenu(menu, position, context, item, id, controller);\n\t\treturn;\n\t}\n\n\tstruct State {\n\t\tint addedToBottom = 0;\n\t};\n\tconst auto itemId = item->fullId();\n\tconst auto participantChosen = [=](Ui::WhoReadParticipant who) {\n\t\tShowWhoReadInfo(controller, itemId, who);\n\t};\n\tconst auto showAllChosen = [=, itemId = item->fullId()]{\n\t\tif (const auto item = controller->session().data().message(itemId)) {\n\t\t\tcontroller->showSection(std::make_shared<Info::Memento>(\n\t\t\t\tnullptr,\n\t\t\t\titemId,\n\t\t\t\tHistoryView::Reactions::DefaultSelectedTab(item, id)));\n\t\t}\n\t};\n\tconst auto owner = &controller->session().data();\n\tconst auto reactions = &owner->reactions();\n\tconst auto &list = reactions->list(\n\t\tData::Reactions::Type::Active);\n\tconst auto activeNonQuick = !id.paid()\n\t\t&& (id != reactions->favoriteId())\n\t\t&& (ranges::contains(list, id, &Data::Reaction::id)\n\t\t\t|| (controller->session().premium() && id.custom()));\n\tconst auto filler = lifetime.make_state<Ui::WhoReactedListMenu>(\n\t\tData::ReactedMenuFactory(&controller->session()),\n\t\tparticipantChosen,\n\t\tshowAllChosen);\n\tconst auto state = lifetime.make_state<State>();\n\tApi::WhoReacted(\n\t\titem,\n\t\tid,\n\t\tcontext,\n\t\tst::defaultWhoRead\n\t) | rpl::filter([=](const Ui::WhoReadContent &content) {\n\t\treturn content.state != Ui::WhoReadState::Unknown;\n\t}) | rpl::on_next([=, &lifetime](Ui::WhoReadContent &&content) {\n\t\tconst auto creating = !*menu;\n\t\tconst auto refillTop = [=] {\n\t\t\tif (activeNonQuick) {\n\t\t\t\t(*menu)->addAction(tr::lng_context_set_as_quick(tr::now), [=] {\n\t\t\t\t\treactions->setFavorite(id);\n\t\t\t\t}, &st::menuIconFave);\n\t\t\t\t(*menu)->addSeparator();\n\t\t\t}\n\t\t};\n\t\tconst auto appendBottom = [=] {\n\t\t\tstate->addedToBottom = 0;\n\t\t\tif (const auto custom = id.custom()) {\n\t\t\t\tif (const auto set = owner->document(custom)->sticker()) {\n\t\t\t\t\tif (set->set.id) {\n\t\t\t\t\t\tstate->addedToBottom = 2;\n\t\t\t\t\t\tAddEmojiPacksAction(\n\t\t\t\t\t\t\tmenu->get(),\n\t\t\t\t\t\t\t{ set->set },\n\t\t\t\t\t\t\tEmojiPacksSource::Reaction,\n\t\t\t\t\t\t\tcontroller);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tif (creating) {\n\t\t\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\tcontext,\n\t\t\t\tst::whoReadMenu);\n\t\t\t(*menu)->lifetime().add(base::take(lifetime));\n\t\t\trefillTop();\n\t\t}\n\t\tfiller->populate(\n\t\t\tmenu->get(),\n\t\t\tcontent,\n\t\t\trefillTop,\n\t\t\tstate->addedToBottom,\n\t\t\tappendBottom);\n\t\tif (creating) {\n\t\t\t(*menu)->popup(position);\n\t\t}\n\t}, lifetime);\n}\n\nstd::vector<StickerSetIdentifier> CollectEmojiPacks(\n\t\tnot_null<HistoryItem*> item,\n\t\tEmojiPacksSource source) {\n\tauto result = std::vector<StickerSetIdentifier>();\n\tconst auto owner = &item->history()->owner();\n\tconst auto push = [&](DocumentId id) {\n\t\tif (const auto set = owner->document(id)->sticker()) {\n\t\t\tif (set->set.id\n\t\t\t\t&& !ranges::contains(\n\t\t\t\t\tresult,\n\t\t\t\t\tset->set.id,\n\t\t\t\t\t&StickerSetIdentifier::id)) {\n\t\t\t\tresult.push_back(set->set);\n\t\t\t}\n\t\t}\n\t};\n\tswitch (source) {\n\tcase EmojiPacksSource::Message:\n\t\tfor (const auto &entity : item->originalText().entities) {\n\t\t\tif (entity.type() == EntityType::CustomEmoji) {\n\t\t\t\tconst auto data = Data::ParseCustomEmojiData(entity.data());\n\t\t\t\tpush(data);\n\t\t\t}\n\t\t}\n\t\tbreak;\n\tcase EmojiPacksSource::Reactions:\n\t\tfor (const auto &reaction : item->reactions()) {\n\t\t\tif (const auto customId = reaction.id.custom()) {\n\t\t\t\tpush(customId);\n\t\t\t}\n\t\t}\n\t\tbreak;\n\tdefault: Unexpected(\"Source in CollectEmojiPacks.\");\n\t}\n\treturn result;\n}\n\nvoid AddEmojiPacksAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tstd::vector<StickerSetIdentifier> packIds,\n\t\tEmojiPacksSource source,\n\t\tnot_null<Window::SessionController*> controller) {\n\tif (packIds.empty()) {\n\t\treturn;\n\t}\n\n\tconst auto count = int(packIds.size());\n\tconst auto manager = &controller->session().data().customEmojiManager();\n\tconst auto name = (count == 1)\n\t\t? TextWithEntities{ manager->lookupSetName(packIds[0].id) }\n\t\t: TextWithEntities();\n\tif (!menu->empty()) {\n\t\tmenu->addSeparator();\n\t}\n\tauto text = [&] {\n\t\tswitch (source) {\n\t\tcase EmojiPacksSource::Message:\n\t\t\treturn name.text.isEmpty()\n\t\t\t\t? tr::lng_context_animated_emoji_many(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tcount,\n\t\t\t\t\ttr::rich)\n\t\t\t\t: tr::lng_context_animated_emoji(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_name,\n\t\t\t\t\tTextWithEntities{ name },\n\t\t\t\t\ttr::rich);\n\t\tcase EmojiPacksSource::Tag:\n\t\t\treturn tr::lng_context_animated_tag(\n\t\t\t\ttr::now,\n\t\t\t\tlt_name,\n\t\t\t\tTextWithEntities{ name },\n\t\t\t\ttr::rich);\n\t\tcase EmojiPacksSource::Reaction:\n\t\t\tif (!name.text.isEmpty()) {\n\t\t\t\treturn tr::lng_context_animated_reaction(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_name,\n\t\t\t\t\tTextWithEntities{ name },\n\t\t\t\t\ttr::rich);\n\t\t\t}\n\t\t\t[[fallthrough]];\n\t\tcase EmojiPacksSource::Reactions:\n\t\t\treturn name.text.isEmpty()\n\t\t\t\t? tr::lng_context_animated_reactions_many(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tcount,\n\t\t\t\t\ttr::rich)\n\t\t\t\t: tr::lng_context_animated_reactions(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_name,\n\t\t\t\t\tTextWithEntities{ name },\n\t\t\t\t\ttr::rich);\n\t\tcase EmojiPacksSource::PollOption:\n\t\t\treturn name.text.isEmpty()\n\t\t\t\t? tr::lng_context_animated_poll_option_many(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tcount,\n\t\t\t\t\ttr::rich)\n\t\t\t\t: tr::lng_context_animated_poll_option(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_name,\n\t\t\t\t\tTextWithEntities{ name },\n\t\t\t\t\ttr::rich);\n\t\t}\n\t\tUnexpected(\"Source in AddEmojiPacksAction.\");\n\t}();\n\tauto button = base::make_unique_q<Ui::Menu::MultilineAction>(\n\t\tmenu->menu(),\n\t\tmenu->st().menu,\n\t\tst::historyHasCustomEmoji,\n\t\tst::historyHasCustomEmojiPosition,\n\t\tstd::move(text));\n\tconst auto weak = base::make_weak(controller);\n\tbutton->setActionTriggered([=] {\n\t\tconst auto strong = weak.get();\n\t\tif (!strong) {\n\t\t\treturn;\n\t\t} else if (packIds.size() > 1) {\n\t\t\tstrong->show(Box<StickersBox>(strong->uiShow(), packIds));\n\t\t\treturn;\n\t\t}\n\t\t// Single used emoji pack.\n\t\tstrong->show(Box<StickerSetBox>(\n\t\t\tstrong->uiShow(),\n\t\t\tpackIds.front(),\n\t\t\tData::StickersType::Emoji));\n\t});\n\tmenu->addAction(std::move(button));\n}\n\nvoid AddEmojiPacksAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<HistoryItem*> item,\n\t\tEmojiPacksSource source,\n\t\tnot_null<Window::SessionController*> controller) {\n\tAddEmojiPacksAction(\n\t\tmenu,\n\t\tCollectEmojiPacks(item, source),\n\t\tsource,\n\t\tcontroller);\n}\n\nvoid AddSelectRestrictionAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<HistoryItem*> item,\n\t\tbool addIcon) {\n\tconst auto peer = item->history()->peer;\n\tif ((peer->allowsForwarding() && !item->forbidsForward())\n\t\t|| item->isSponsored()) {\n\t\treturn;\n\t}\n\tif (addIcon && !menu->empty()) {\n\t\tmenu->addSeparator();\n\t}\n\tconst auto user = peer->asUser();\n\tauto button = base::make_unique_q<Ui::Menu::MultilineAction>(\n\t\tmenu->menu(),\n\t\tmenu->st().menu,\n\t\tst::historyHasCustomEmoji,\n\t\t((addIcon && !user)\n\t\t\t? st::historySponsoredAboutMenuLabelPosition\n\t\t\t: st::historyHasCustomEmojiPosition),\n\t\t(peer->isMegagroup()\n\t\t\t? tr::lng_context_noforwards_info_group(tr::now, tr::rich)\n\t\t\t: (peer->isChannel())\n\t\t\t? tr::lng_context_noforwards_info_channel(tr::now, tr::rich)\n\t\t\t: (user && user->isBot())\n\t\t\t? tr::lng_context_noforwards_info_bot(tr::now, tr::rich)\n\t\t\t: user\n\t\t\t? ((user->flags() & UserDataFlag::NoForwardsMyEnabled)\n\t\t\t\t? tr::lng_context_noforwards_info_mine(tr::now, tr::rich)\n\t\t\t\t: tr::lng_context_noforwards_info_his(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::bold(user->shortName()),\n\t\t\t\t\ttr::rich))\n\t\t\t: tr::lng_context_noforwards_info_channel(tr::now, tr::rich)),\n\t\t(addIcon && !user) ? &st::menuIconCopyright : nullptr);\n\tbutton->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tmenu->addAction(std::move(button));\n}\n\nTextWithEntities TransribedText(not_null<HistoryItem*> item) {\n\tconst auto media = item->media();\n\tconst auto document = media ? media->document() : nullptr;\n\tif (!document || !document->isVoiceMessage()) {\n\t\treturn {};\n\t}\n\tconst auto &entry = document->session().api().transcribes().entry(item);\n\tif (!entry.requestId\n\t\t&& entry.shown\n\t\t&& !entry.toolong\n\t\t&& !entry.failed\n\t\t&& !entry.pending\n\t\t&& !entry.result.isEmpty()) {\n\t\treturn { entry.result };\n\t}\n\treturn {};\n}\n\nbool ItemHasTtl(HistoryItem *item) {\n\treturn (item && item->media())\n\t\t? (item->media()->ttlSeconds() > 0)\n\t\t: false;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_context_menu.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n#include \"history/view/history_view_element.h\"\n\nnamespace Data {\nclass Session;\nstruct ReactionId;\n} // namespace Data\n\nnamespace Main {\nclass Session;\nclass SessionShow;\n} // namespace Main\n\nnamespace Ui {\nclass PopupMenu;\nenum class ReportReason;\n} // namespace Ui\n\nnamespace Window {\nclass SessionNavigation;\nclass SessionController;\n} // namespace Main\n\nnamespace HistoryView {\n\nenum class Context : char;\nenum class PointState : char;\nclass ListWidget;\nclass Element;\nstruct SelectedItem;\nusing SelectedItems = std::vector<SelectedItem>;\n\nstruct ContextMenuRequest {\n\texplicit ContextMenuRequest(\n\t\tnot_null<Window::SessionNavigation*> navigation);\n\n\tconst not_null<Window::SessionNavigation*> navigation;\n\tClickHandlerPtr link;\n\tElement *view = nullptr;\n\tHistoryItem *item = nullptr;\n\tSelectedItems selectedItems;\n\tTextForMimeData selectedText;\n\tSelectedQuote quote;\n\tbool overSelection = false;\n\tPointState pointState = PointState();\n};\n\nbase::unique_qptr<Ui::PopupMenu> FillContextMenu(\n\tnot_null<ListWidget*> list,\n\tconst ContextMenuRequest &request);\n\nvoid InsertPollHiddenResultsLabel(not_null<Ui::PopupMenu*> menu);\n\nvoid CopyPostLink(\n\tnot_null<Window::SessionController*> controller,\n\tFullMsgId itemId,\n\tContext context,\n\tstd::optional<TimeId> videoTimestamp = {});\nvoid CopyPostLink(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tFullMsgId itemId,\n\tContext context,\n\tstd::optional<TimeId> videoTimestamp = {});\nvoid CopyStoryLink(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tFullStoryId storyId);\nvoid FillPollOptionPage(\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<Data::Session*> owner,\n\tFullMsgId itemId,\n\tconst QByteArray &pollOption,\n\tnot_null<Window::SessionController*> controller,\n\tFn<void()> replyToOption = nullptr);\n\nvoid AttachPollOptionTabs(\n\tnot_null<Ui::PopupMenu*> menu,\n\tQPoint desiredPosition);\n\n[[nodiscard]] std::optional<QString> CurrentVoiceTimecode(FullMsgId itemId);\n[[nodiscard]] rpl::producer<QString> VoiceTimecodeUpdates(FullMsgId itemId);\n\nvoid AddPollActions(\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<PollData*> poll,\n\tnot_null<HistoryItem*> item,\n\tContext context,\n\tnot_null<Window::SessionController*> controller,\n\tbool skipRetractVote = false);\nvoid AddSaveSoundForNotifications(\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<HistoryItem*> item,\n\tnot_null<DocumentData*> document,\n\tnot_null<Window::SessionController*> controller);\nvoid AddWhoReactedAction(\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<QWidget*> context,\n\tnot_null<HistoryItem*> item,\n\tnot_null<Window::SessionController*> controller);\nvoid MaybeAddWhenEditedForwardedAction(\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<HistoryItem*> item,\n\tnot_null<Window::SessionController*> controller);\nvoid ShowWhoReactedMenu(\n\tnot_null<base::unique_qptr<Ui::PopupMenu>*> menu,\n\tQPoint position,\n\tnot_null<QWidget*> context,\n\tnot_null<HistoryItem*> item,\n\tconst Data::ReactionId &id,\n\tnot_null<Window::SessionController*> controller,\n\trpl::lifetime &lifetime);\nvoid ShowTagInListMenu(\n\tnot_null<base::unique_qptr<Ui::PopupMenu>*> menu,\n\tQPoint position,\n\tnot_null<QWidget*> context,\n\tconst Data::ReactionId &id,\n\tnot_null<Window::SessionController*> controller);\nvoid AddCopyFilename(\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<DocumentData*> document,\n\tFn<bool()> showCopyRestrictionForSelected);\n\nenum class EmojiPacksSource {\n\tMessage,\n\tReaction,\n\tReactions,\n\tTag,\n\tPollOption,\n};\n[[nodiscard]] std::vector<StickerSetIdentifier> CollectEmojiPacks(\n\tnot_null<HistoryItem*> item,\n\tEmojiPacksSource source);\nvoid AddEmojiPacksAction(\n\tnot_null<Ui::PopupMenu*> menu,\n\tstd::vector<StickerSetIdentifier> packIds,\n\tEmojiPacksSource source,\n\tnot_null<Window::SessionController*> controller);\nvoid AddEmojiPacksAction(\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<HistoryItem*> item,\n\tEmojiPacksSource source,\n\tnot_null<Window::SessionController*> controller);\nvoid AddSelectRestrictionAction(\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<HistoryItem*> item,\n\tbool addIcon);\n\n[[nodiscard]] TextWithEntities TransribedText(not_null<HistoryItem*> item);\n\n[[nodiscard]] bool ItemHasTtl(HistoryItem *item);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_context_menu_fork.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_context_menu_fork.h\"\n\n#include \"api/api_as_copy.h\"\n#include \"api/api_common.h\"\n#include \"api/api_editing.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/fork_settings.h\"\n#include \"data/data_document.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/menu/menu_common.h\"\n#include \"ui/widgets/menu/menu_multiline_action.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_giveaway.h\"\n\nnamespace Fork {\n\nnamespace MediaReplacement {\n\nstruct Entry final {\n\tcrl::time last = crl::time(0);\n\tMTPInputMedia media;\n};\n\nbase::flat_map<Main::Session*, Entry> Medias;\n\nbool HasInputMedia(not_null<HistoryItem*> item) {\n\tconst auto it = Medias.find(&item->from()->session());\n\treturn (it != end(Medias)) ? (it->second.last > 0) : false;\n}\n\nvoid UseInputMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tApi::SendOptions options) {\n\tconst auto it = Medias.find(&item->from()->session());\n\tif (it != end(Medias)) {\n\t\tif ((crl::now() - it->second.last) < 60000) {\n\t\t\tconst auto &media = it->second.media;\n\t\t\tApi::Fork::EditMessageMedia(item, options, media, [](...){});\n\t\t} else {\n\t\t\tMedias.remove(&item->from()->session());\n\t\t}\n\t}\n}\n\nvoid RememberDocumentAsInputMedia(not_null<DocumentData*> document) {\n\tMedias[&document->session()] = Entry{\n\t\t.last = crl::now(),\n\t\t.media = MTP_inputMediaDocument(\n\t\t\tMTP_flags(0),\n\t\t\tdocument->mtpInput(),\n\t\t\tdocument->goodThumbnailPhoto()\n\t\t\t\t? document->goodThumbnailPhoto()->mtpInput()\n\t\t\t\t: MTPInputPhoto(),\n\t\t\tMTPint(),\n\t\t\tMTPint(),\n\t\t\tMTPstring()),\n\t};\n}\n\nvoid RememberPhotoAsInputMedia(not_null<PhotoData*> photo) {\n\tMedias[&photo->session()] = Entry{\n\t\t.last = crl::now(),\n\t\t.media = MTP_inputMediaPhoto(\n\t\t\tMTP_flags(0),\n\t\t\tphoto->mtpInput(),\n\t\t\tMTPint(),\n\t\t\tMTPInputDocument()),\n\t};\n}\n\n} // namespace MediaReplacement\n\nvoid AddReplaceMedia(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<Window::SessionController*> controller) {\n\tif (!Core::App().settings().fork().addToMenuRememberMedia()) {\n\t\treturn;\n\t}\n\tconst auto media = item->media();\n\tconst auto photo = media ? media->photo() : nullptr;\n\tconst auto document = media ? media->document() : nullptr;\n\n\tconst auto addAction = [&](QString &&s, Fn<void()> callback) {\n\t\tauto item = base::make_unique_q<Ui::Menu::MultilineAction>(\n\t\t\tmenu->menu(),\n\t\t\tst::defaultMenu,\n\t\t\tst::historyHasCustomEmoji,\n\t\t\tst::historyHasCustomEmojiPosition,\n\t\t\tTextWithEntities{ std::move(s) });\n\t\titem->clicks() | rpl::on_next(callback, menu->lifetime());\n\t\tmenu->addAction(std::move(item));\n\t};\n\tif (photo) {\n\t\taddAction(u\"Remember photo\"_q, [=] {\n\t\t\tMediaReplacement::RememberPhotoAsInputMedia(photo);\n\t\t});\n\t}\n\tif (document) {\n\t\taddAction(u\"Remember document\"_q, [=] {\n\t\t\tMediaReplacement::RememberDocumentAsInputMedia(document);\n\t\t});\n\t}\n\tif ((photo || document) && MediaReplacement::HasInputMedia(item)) {\n\t\taddAction(u\"Replace media with remembered one\"_q, [=] {\n\t\t\tMediaReplacement::UseInputMedia(item, {});\n\t\t});\n\t}\n}\n\nvoid AddSwapMedia(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<HistoryItem*> item1,\n\t\tnot_null<HistoryItem*> item2,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFn<void()> action) {\n\tif (!Core::App().settings().fork().addToMenuRememberMedia()) {\n\t\treturn;\n\t}\n\tconst auto media1 = item1->media();\n\tconst auto photo1 = media1 ? media1->photo() : nullptr;\n\tconst auto document1 = media1 ? media1->document() : nullptr;\n\n\tconst auto media2 = item2->media();\n\tconst auto photo2 = media2 ? media2->photo() : nullptr;\n\tconst auto document2 = media2 ? media2->document() : nullptr;\n\n\tconst auto addAction = [&](QString &&s, Fn<void()> callback) {\n\t\tauto item = base::make_unique_q<Ui::Menu::MultilineAction>(\n\t\t\tmenu->menu(),\n\t\t\tst::defaultMenu,\n\t\t\tst::historyHasCustomEmoji,\n\t\t\tst::historyHasCustomEmojiPosition,\n\t\t\tTextWithEntities{ std::move(s) });\n\t\titem->clicks() | rpl::on_next(callback, menu->lifetime());\n\t\tmenu->addAction(std::move(item));\n\t};\n\tif ((photo1 || document1) && (photo2 || document2)) {\n\t\tconst auto swap = [=] {\n\t\t\tauto inputMedia1 = document1\n\t\t\t\t? MTP_inputMediaDocument(\n\t\t\t\t\tMTP_flags(0),\n\t\t\t\t\tdocument1->mtpInput(),\n\t\t\t\t\tdocument1->goodThumbnailPhoto()\n\t\t\t\t\t\t? document1->goodThumbnailPhoto()->mtpInput()\n\t\t\t\t\t\t: MTPInputPhoto(),\n\t\t\t\t\tMTPint(),\n\t\t\t\t\tMTPint(),\n\t\t\t\t\tMTPstring())\n\t\t\t\t: photo1\n\t\t\t\t? MTP_inputMediaPhoto(\n\t\t\t\t\tMTP_flags(0),\n\t\t\t\t\tphoto1->mtpInput(),\n\t\t\t\t\tMTPint(),\n\t\t\t\t\tMTPInputDocument())\n\t\t\t\t: MTP_inputMediaEmpty();\n\t\t\tauto inputMedia2 = document2\n\t\t\t\t? MTP_inputMediaDocument(\n\t\t\t\t\tMTP_flags(0),\n\t\t\t\t\tdocument2->mtpInput(),\n\t\t\t\t\tdocument2->goodThumbnailPhoto()\n\t\t\t\t\t\t? document2->goodThumbnailPhoto()->mtpInput()\n\t\t\t\t\t\t: MTPInputPhoto(),\n\t\t\t\t\tMTPint(),\n\t\t\t\t\tMTPint(),\n\t\t\t\t\tMTPstring())\n\t\t\t\t: photo2\n\t\t\t\t? MTP_inputMediaPhoto(\n\t\t\t\t\tMTP_flags(0),\n\t\t\t\t\tphoto2->mtpInput(),\n\t\t\t\t\tMTPint(),\n\t\t\t\t\tMTPInputDocument())\n\t\t\t\t: MTP_inputMediaEmpty();\n\t\t\taction();\n\t\t\tconst auto o1 = Api::SendOptions{\n\t\t\t\t.scheduled = item1->isScheduled() ? item1->date() : 0,\n\t\t\t};\n\t\t\tconst auto o2 = Api::SendOptions{\n\t\t\t\t.scheduled = item2->isScheduled() ? item2->date() : 0,\n\t\t\t};\n\t\t\tApi::Fork::EditMessageMedia(item1, o1, inputMedia2, [=](auto e) {\n\t\t\t\tcontroller->showToast(\"Message #1: \" + e);\n\t\t\t});\n\t\t\tApi::Fork::EditMessageMedia(item2, o2, inputMedia1, [=](auto e) {\n\t\t\t\tcontroller->showToast(\"Message #2: \" + e);\n\t\t\t});\n\t\t};\n\t\taddAction(u\"Try to swap media\"_q, [=] {\n\t\t\tconst auto fail = [=](const QString &from) {\n\t\t\t\tcontroller->showToast(\"Failed from: \" + from);\n\t\t\t};\n\t\t\tApi::AsCopy::UpdateFileRef({ item1, item2 }, swap, fail);\n\t\t});\n\t}\n}\n\ncrl::time DurationFromItem(HistoryItem *item) {\n\tconst auto media = item ? item->media() : nullptr;\n\tconst auto document = media ? media->document() : nullptr;\n\treturn document ? document->duration() : crl::time(0);\n}\n\ncrl::time DurationFromItem(\n\t\tFullMsgId itemId,\n\t\tnot_null<Window::SessionController*> controller) {\n\treturn DurationFromItem(controller->session().data().message(itemId));\n}\n\nvoid AddGroupSelected(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tFn<void(bool)> callback) {\n\tauto item = base::make_unique_q<Ui::Menu::Action>(\n\t\tmenu->menu(),\n\t\tmenu->menu()->st(),\n\t\tUi::Menu::CreateAction(\n\t\t\tmenu.get(),\n\t\t\ttr::lng_context_group_items(tr::now),\n\t\t\t[=] { callback(false); }),\n\t\t&st::menuIconDockBounce,\n\t\t&st::menuIconDockBounce);\n\n\t{\n\t\tconst auto rightButton = Ui::CreateChild<Ui::IconButton>(\n\t\t\titem.get(),\n\t\t\tst::startGiveawayBoxTitleClose);\n\t\titem->sizeValue(\n\t\t) | rpl::take(1) | rpl::on_next([=](const QSize &s) {\n\t\t\trightButton->moveToLeft(\n\t\t\t\ts.width() - rightButton->width(),\n\t\t\t\t(s.height() - rightButton->height()) / 2);\n\t\t\tqDebug() << rightButton->geometry() << s;\n\t\t}, rightButton->lifetime());\n\t\trightButton->setClickedCallback([=] {\n\t\t\tcallback(true);\n\t\t\tmenu->hideMenu(false);\n\t\t});\n\t}\n\n\tmenu->addAction(std::move(item));\n}\n\n} // namespace Fork\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_context_menu_fork.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_menu_icons.h\"\n\nclass HistoryItem;\n\nnamespace Fork {\n\n[[nodiscard]] crl::time DurationFromItem(HistoryItem *item);\n[[nodiscard]] crl::time DurationFromItem(\n\tFullMsgId itemId,\n\tnot_null<Window::SessionController*> controller);\n\ntemplate <typename Items>\nvoid AddShowSumDurations(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tItems items,\n\t\tnot_null<Window::SessionController*> controller) {\n\tauto result = crl::time(0);\n\n\tconstexpr bool isContainer = requires(const Items &t) {\n\t\tt.begin();\n\t};\n\tconstexpr bool isInfoSelectedItem = requires(const Items &t) {\n\t\tt.list;\n\t};\n\tif constexpr (isContainer) {\n\t\tusing T = decltype(begin(items));\n\t\tconstexpr bool isPair = requires(const T &t) {\n\t\t\tt->first;\n\t\t};\n\t\tconstexpr bool isListWidgetSelectedItem = requires(const T &t) {\n\t\t\tt->msgId;\n\t\t};\n\t\tif constexpr (isPair) {\n\t\t\tfor (const auto &[item, _] : items) {\n\t\t\t\tresult += DurationFromItem(item);\n\t\t\t}\n\t\t} else if constexpr (isListWidgetSelectedItem) {\n\t\t\tfor (const auto &selected : items) {\n\t\t\t\tresult += DurationFromItem(selected.msgId, controller);\n\t\t\t}\n\t\t}\n\t} else if constexpr (isInfoSelectedItem) {\n\t\tfor (const auto &item : items.list) {\n\t\t\tresult += DurationFromItem(item.globalId.itemId, controller);\n\t\t}\n\t} else {\n\t\tthrow std::invalid_argument(\"Can't find good type.\");\n\t}\n\n\tif (result > 0) {\n\t\tmenu->addAction(\"Calculate duration\", [=] {\n\t\t\tconst auto text = QString(\"Summary duration: %1.\").arg(\n\t\t\t\tUi::FormatDurationText(result));\n\t\t\tcontroller->show(Ui::MakeInformBox(text));\n\t\t}, &st::menuIconReschedule);\n\t}\n}\n\nvoid AddReplaceMedia(\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<HistoryItem*> item,\n\tnot_null<Window::SessionController*> controller);\n\nvoid AddSwapMedia(\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<HistoryItem*> item,\n\tnot_null<HistoryItem*> item2,\n\tnot_null<Window::SessionController*> controller,\n\tFn<void()> action);\n\nvoid AddGroupSelected(\n\tnot_null<Ui::PopupMenu*> menu,\n\tFn<void(bool)> callback);\n\n} // namespace Fork\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_corner_buttons.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_corner_buttons.h\"\n\n#include \"ui/chat/chat_style.h\"\n#include \"ui/controls/jump_down_button.h\"\n#include \"ui/widgets/elastic_scroll.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_unread_things.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_send.h\"\n#include \"apiwrap.h\"\n#include \"api/api_unread_things.h\"\n#include \"data/data_document.h\"\n#include \"data/data_messages.h\"\n#include \"data/data_session.h\"\n#include \"data/data_forum_topic.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/toast/toast.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace HistoryView {\n\nCornerButtons::CornerButtons(\n\tnot_null<Ui::ScrollArea*> parent,\n\tnot_null<const Ui::ChatStyle*> st,\n\tnot_null<CornerButtonsDelegate*> delegate)\n: CornerButtons(\n\tparent,\n\t[=](QEvent *e) { return parent->viewportEvent(e); },\n\tst,\n\tdelegate) {\n}\n\nCornerButtons::CornerButtons(\n\tnot_null<Ui::ElasticScroll*> parent,\n\tnot_null<const Ui::ChatStyle*> st,\n\tnot_null<CornerButtonsDelegate*> delegate)\n: CornerButtons(\n\tparent,\n\t[=](QEvent *e) { return parent->viewportEvent(e); },\n\tst,\n\tdelegate) {\n}\n\nCornerButtons::CornerButtons(\n\tnot_null<QWidget*> parent,\n\tFn<bool(QEvent*)> scrollViewportEvent,\n\tnot_null<const Ui::ChatStyle*> st,\n\tnot_null<CornerButtonsDelegate*> delegate)\n: _parent(parent)\n, _scrollViewportEvent(std::move(scrollViewportEvent))\n, _delegate(delegate)\n, _down(\n\tparent,\n\tst->value(_stLifetime, st::historyToDown))\n, _mentions(\n\tparent,\n\tst->value(_stLifetime, st::historyUnreadMentions))\n, _reactions(\n\t\tparent,\n\t\tst->value(_stLifetime, st::historyUnreadReactions))\n, _pollVotes(\n\t\tparent,\n\t\tst->value(_stLifetime, st::historyUnreadPollVotes)) {\n\t_down.widget->addClickHandler([=] { downClick(); });\n\t_mentions.widget->addClickHandler([=] { mentionsClick(); });\n\t_reactions.widget->addClickHandler([=] { reactionsClick(); });\n\t_pollVotes.widget->addClickHandler([=] { pollVotesClick(); });\n\n\tconst auto filterScroll = [&](CornerButton &button) {\n\t\tbutton.widget->installEventFilter(this);\n\t};\n\tfilterScroll(_down);\n\tfilterScroll(_mentions);\n\tfilterScroll(_reactions);\n\tfilterScroll(_pollVotes);\n\n\tSendMenu::SetupUnreadMentionsMenu(_mentions.widget.data(), [=] {\n\t\treturn _delegate->cornerButtonsThread();\n\t});\n\tSendMenu::SetupUnreadReactionsMenu(_reactions.widget.data(), [=] {\n\t\treturn _delegate->cornerButtonsThread();\n\t});\n\tSendMenu::SetupUnreadPollVotesMenu(_pollVotes.widget.data(), [=] {\n\t\treturn _delegate->cornerButtonsThread();\n\t});\n}\n\nbool CornerButtons::eventFilter(QObject *o, QEvent *e) {\n\tif (e->type() == QEvent::Wheel\n\t\t&& (o == _down.widget\n\t\t\t|| o == _mentions.widget\n\t\t\t|| o == _reactions.widget\n\t\t\t|| o == _pollVotes.widget)) {\n\t\treturn _scrollViewportEvent(e);\n\t}\n\treturn QObject::eventFilter(o, e);\n}\n\nvoid CornerButtons::downClick() {\n\tif (base::IsCtrlPressed() || !_replyReturn) {\n\t\t_delegate->cornerButtonsShowAtPosition(Data::UnreadMessagePosition);\n\t} else {\n\t\t_delegate->cornerButtonsShowAtPosition(_replyReturn->position());\n\t}\n}\n\nvoid CornerButtons::mentionsClick() {\n\tconst auto history = lookupHistory();\n\tif (!history) {\n\t\treturn;\n\t}\n\tconst auto thread = _delegate->cornerButtonsThread();\n\tconst auto msgId = thread->unreadMentions().minLoaded();\n\tconst auto already = (_delegate->cornerButtonsCurrentId().msg == msgId);\n\n\t// Mark mention voice/video message as read.\n\t// See https://github.com/telegramdesktop/tdesktop/issues/5623\n\tif (msgId && already) {\n\t\tif (const auto item = thread->owner().message(history->peer, msgId)) {\n\t\t\tif (const auto media = item->media()) {\n\t\t\t\tif (const auto document = media->document()) {\n\t\t\t\t\tif (!media->webpage()\n\t\t\t\t\t\t&& (document->isVoiceMessage()\n\t\t\t\t\t\t\t|| document->isVideoMessage())) {\n\t\t\t\t\t\tdocument->owner().markMediaRead(document);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tshowAt(msgId);\n}\n\nvoid CornerButtons::reactionsClick() {\n\tconst auto history = lookupHistory();\n\tif (!history) {\n\t\treturn;\n\t}\n\tconst auto thread = _delegate->cornerButtonsThread();\n\tshowAt(thread->unreadReactions().minLoaded());\n}\n\nvoid CornerButtons::pollVotesClick() {\n\tconst auto history = lookupHistory();\n\tif (!history) {\n\t\treturn;\n\t}\n\tconst auto thread = _delegate->cornerButtonsThread();\n\tshowAt(thread->unreadPollVotes().minLoaded());\n}\n\nvoid CornerButtons::clearReplyReturns() {\n\t_replyReturns.clear();\n\t_replyReturn = nullptr;\n}\n\nQVector<FullMsgId> CornerButtons::replyReturns() const {\n\treturn _replyReturns;\n}\n\nvoid CornerButtons::setReplyReturns(QVector<FullMsgId> replyReturns) {\n\t_replyReturns = std::move(replyReturns);\n\tcomputeCurrentReplyReturn();\n\tif (!_replyReturn) {\n\t\tcalculateNextReplyReturn();\n\t}\n}\n\nvoid CornerButtons::computeCurrentReplyReturn() {\n\tconst auto thread = _delegate->cornerButtonsThread();\n\t_replyReturn = (!thread || _replyReturns.empty())\n\t\t? nullptr\n\t\t: thread->owner().message(_replyReturns.back());\n}\n\nvoid CornerButtons::skipReplyReturn(FullMsgId id) {\n\twhile (_replyReturn) {\n\t\tif (_replyReturn->fullId() == id) {\n\t\t\tcalculateNextReplyReturn();\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nvoid CornerButtons::calculateNextReplyReturn() {\n\t_replyReturn = nullptr;\n\twhile (!_replyReturns.empty() && !_replyReturn) {\n\t\t_replyReturns.pop_back();\n\t\tcomputeCurrentReplyReturn();\n\t}\n\tif (!_replyReturn) {\n\t\tupdateJumpDownVisibility();\n\t\tupdateUnreadThingsVisibility();\n\t}\n}\n\nvoid CornerButtons::pushReplyReturn(not_null<HistoryItem*> item) {\n\t_replyReturns.push_back(item->fullId());\n\t_replyReturn = item;\n\n\tif (!_replyReturnStarted) {\n\t\t_replyReturnStarted = true;\n\t\titem->history()->owner().itemRemoved(\n\t\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\t\twhile (item == _replyReturn) {\n\t\t\t\tcalculateNextReplyReturn();\n\t\t\t}\n\t\t}, _down.widget->lifetime());\n\t}\n}\n\nCornerButton &CornerButtons::buttonByType(Type type) {\n\tswitch (type) {\n\tcase Type::Down: return _down;\n\tcase Type::Mentions: return _mentions;\n\tcase Type::Reactions: return _reactions;\n\tcase Type::PollVotes: return _pollVotes;\n\t}\n\tUnexpected(\"Type in CornerButtons::buttonByType.\");\n}\n\nHistory *CornerButtons::lookupHistory() const {\n\tconst auto thread = _delegate->cornerButtonsThread();\n\treturn thread ? thread->owningHistory().get() : nullptr;\n}\n\nvoid CornerButtons::showAt(MsgId id) {\n\tif (const auto history = lookupHistory()) {\n\t\tif (const auto item = history->owner().message(history->peer, id)) {\n\t\t\t_delegate->cornerButtonsShowAtPosition(item->position());\n\t\t}\n\t}\n}\n\nvoid CornerButtons::updateVisibility(Type type, bool shown) {\n\tauto &button = buttonByType(type);\n\tif (button.shown != shown) {\n\t\tbutton.shown = shown;\n\t\tbutton.animation.start(\n\t\t\t[=] { updatePositions(); },\n\t\t\tshown ? 0. : 1.,\n\t\t\tshown ? 1. : 0.,\n\t\t\tst::historyToDownDuration);\n\t}\n}\n\nvoid CornerButtons::updateUnreadThingsVisibility() {\n\tif (_delegate->cornerButtonsIgnoreVisibility()) {\n\t\treturn;\n\t}\n\tconst auto thread = _delegate->cornerButtonsThread();\n\tif (!thread) {\n\t\tupdateVisibility(Type::Mentions, false);\n\t\tupdateVisibility(Type::Reactions, false);\n\t\tupdateVisibility(Type::PollVotes, false);\n\t\treturn;\n\t}\n\tauto &unreadThings = thread->session().api().unreadThings();\n\tunreadThings.preloadEnough(thread);\n\n\tconst auto updateWithCount = [&](Type type, int count) {\n\t\tupdateVisibility(\n\t\t\ttype,\n\t\t\t(count > 0) && _delegate->cornerButtonsUnreadMayBeShown());\n\t};\n\tif (_delegate->cornerButtonsHas(Type::Mentions)\n\t\t&& unreadThings.trackMentions(thread)) {\n\t\tif (const auto count = thread->unreadMentions().count(0)) {\n\t\t\t_mentions.widget->setUnreadCount(count);\n\t\t}\n\t\tupdateWithCount(\n\t\t\tType::Mentions,\n\t\t\tthread->unreadMentions().loadedCount());\n\t} else {\n\t\tupdateVisibility(Type::Mentions, false);\n\t}\n\n\tif (_delegate->cornerButtonsHas(Type::Reactions)\n\t\t&& unreadThings.trackReactions(thread)) {\n\t\tif (const auto count = thread->unreadReactions().count(0)) {\n\t\t\t_reactions.widget->setUnreadCount(count);\n\t\t}\n\t\tupdateWithCount(\n\t\t\tType::Reactions,\n\t\t\tthread->unreadReactions().loadedCount());\n\t} else {\n\t\tupdateVisibility(Type::Reactions, false);\n\t}\n\n\tif (_delegate->cornerButtonsHas(Type::PollVotes)\n\t\t&& unreadThings.trackPollVotes(thread)) {\n\t\tif (const auto count = thread->unreadPollVotes().count(0)) {\n\t\t\t_pollVotes.widget->setUnreadCount(count);\n\t\t}\n\t\tupdateWithCount(\n\t\t\tType::PollVotes,\n\t\t\tthread->unreadPollVotes().loadedCount());\n\t} else {\n\t\tupdateVisibility(Type::PollVotes, false);\n\t}\n}\n\nvoid CornerButtons::updateJumpDownVisibility(std::optional<int> counter) {\n\tif (const auto shown = _delegate->cornerButtonsDownShown()) {\n\t\tupdateVisibility(Type::Down, *shown);\n\t}\n\tif (counter) {\n\t\t_down.widget->setUnreadCount(*counter);\n\t}\n}\n\nvoid CornerButtons::updatePositions() {\n\tconst auto checkVisibility = [](CornerButton &button) {\n\t\tconst auto shouldBeHidden = !button.shown\n\t\t\t&& !button.animation.animating();\n\t\tif (shouldBeHidden != button.widget->isHidden()) {\n\t\t\tbutton.widget->setVisible(!shouldBeHidden);\n\t\t}\n\t};\n\tconst auto shown = [](CornerButton &button) {\n\t\treturn button.animation.value(button.shown ? 1. : 0.);\n\t};\n\n\t// All corner buttons is a child widgets of _scroll, not me.\n\n\tconst auto historyDownShown = shown(_down);\n\tconst auto unreadMentionsShown = shown(_mentions);\n\tconst auto unreadReactionsShown = shown(_reactions);\n\tconst auto unreadPollVotesShown = shown(_pollVotes);\n\tconst auto skip = st::historyUnreadThingsSkip;\n\t{\n\t\tconst auto top = anim::interpolate(\n\t\t\t0,\n\t\t\t_down.widget->height() + st::historyToDownPosition.y(),\n\t\t\thistoryDownShown);\n\t\t_down.widget->moveToRight(\n\t\t\tst::historyToDownPosition.x(),\n\t\t\t_parent->height() - top);\n\t}\n\t{\n\t\tconst auto right = anim::interpolate(\n\t\t\t-_mentions.widget->width(),\n\t\t\tst::historyToDownPosition.x(),\n\t\t\tunreadMentionsShown);\n\t\tconst auto shift = anim::interpolate(\n\t\t\t0,\n\t\t\t_down.widget->height() + skip,\n\t\t\thistoryDownShown);\n\t\tconst auto top = _parent->height()\n\t\t\t- _mentions.widget->height()\n\t\t\t- st::historyToDownPosition.y()\n\t\t\t- shift;\n\t\t_mentions.widget->moveToRight(right, top);\n\t}\n\t{\n\t\tconst auto right = anim::interpolate(\n\t\t\t-_reactions.widget->width(),\n\t\t\tst::historyToDownPosition.x(),\n\t\t\tunreadReactionsShown);\n\t\tconst auto shift = anim::interpolate(\n\t\t\t0,\n\t\t\t_down.widget->height() + skip,\n\t\t\thistoryDownShown\n\t\t) + anim::interpolate(\n\t\t\t0,\n\t\t\t_mentions.widget->height() + skip,\n\t\t\tunreadMentionsShown);\n\t\tconst auto top = _parent->height()\n\t\t\t- _reactions.widget->height()\n\t\t\t- st::historyToDownPosition.y()\n\t\t\t- shift;\n\t\t_reactions.widget->moveToRight(right, top);\n\t}\n\t{\n\t\tconst auto right = anim::interpolate(\n\t\t\t-_pollVotes.widget->width(),\n\t\t\tst::historyToDownPosition.x(),\n\t\t\tunreadPollVotesShown);\n\t\tconst auto shift = anim::interpolate(\n\t\t\t0,\n\t\t\t_down.widget->height() + skip,\n\t\t\thistoryDownShown\n\t\t) + anim::interpolate(\n\t\t\t0,\n\t\t\t_mentions.widget->height() + skip,\n\t\t\tunreadMentionsShown\n\t\t) + anim::interpolate(\n\t\t\t0,\n\t\t\t_reactions.widget->height() + skip,\n\t\t\tunreadReactionsShown);\n\t\tconst auto top = _parent->height()\n\t\t\t- _pollVotes.widget->height()\n\t\t\t- st::historyToDownPosition.y()\n\t\t\t- shift;\n\t\t_pollVotes.widget->moveToRight(right, top);\n\t}\n\n\tcheckVisibility(_down);\n\tcheckVisibility(_mentions);\n\tcheckVisibility(_reactions);\n\tcheckVisibility(_pollVotes);\n}\n\nvoid CornerButtons::finishAnimations() {\n\t_down.animation.stop();\n\t_mentions.animation.stop();\n\t_reactions.animation.stop();\n\t_pollVotes.animation.stop();\n\tupdatePositions();\n}\n\nFn<void(bool found)> CornerButtons::doneJumpFrom(\n\t\tFullMsgId targetId,\n\t\tFullMsgId originId,\n\t\tbool ignoreMessageNotFound) {\n\treturn [=](bool found) {\n\t\tskipReplyReturn(targetId);\n\t\tif (originId) {\n\t\t\tif (const auto thread = _delegate->cornerButtonsThread()) {\n\t\t\t\tif (const auto item = thread->owner().message(originId)) {\n\t\t\t\t\tpushReplyReturn(item);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!found && !ignoreMessageNotFound) {\n\t\t\tUi::Toast::Show(\n\t\t\t\t_parent.get(),\n\t\t\t\ttr::lng_message_not_found(tr::now));\n\t\t}\n\t};\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_corner_buttons.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n#include \"base/object_ptr.h\"\n\nclass History;\nclass HistoryItem;\nstruct FullMsgId;\n\nnamespace Ui {\nclass ChatStyle;\nclass ScrollArea;\nclass ElasticScroll;\nclass JumpDownButton;\n} // namespace Ui\n\nnamespace Data {\nstruct MessagePosition;\nclass Thread;\n} // namespace Data\n\nnamespace HistoryView {\n\nstruct CornerButton {\n\ttemplate <typename ...Args>\n\tCornerButton(Args &&...args) : widget(std::forward<Args>(args)...) {\n\t}\n\n\tobject_ptr<Ui::JumpDownButton> widget;\n\tUi::Animations::Simple animation;\n\tbool shown = false;\n};\n\nenum class CornerButtonType {\n\tDown,\n\tMentions,\n\tReactions,\n\tPollVotes,\n};\n\nclass CornerButtonsDelegate {\npublic:\n\tvirtual void cornerButtonsShowAtPosition(\n\t\tData::MessagePosition position) = 0;\n\t[[nodiscard]] virtual Data::Thread *cornerButtonsThread() = 0;\n\t[[nodiscard]] virtual FullMsgId cornerButtonsCurrentId() = 0;\n\t[[nodiscard]] virtual bool cornerButtonsIgnoreVisibility() = 0;\n\t[[nodiscard]] virtual std::optional<bool> cornerButtonsDownShown() = 0;\n\t[[nodiscard]] virtual bool cornerButtonsUnreadMayBeShown() = 0;\n\t[[nodiscard]] virtual bool cornerButtonsHas(CornerButtonType type) = 0;\n};\n\nclass CornerButtons final : private QObject {\npublic:\n\tCornerButtons(\n\t\tnot_null<Ui::ScrollArea*> parent,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tnot_null<CornerButtonsDelegate*> delegate);\n\tCornerButtons(\n\t\tnot_null<Ui::ElasticScroll*> parent,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tnot_null<CornerButtonsDelegate*> delegate);\n\n\tusing Type = CornerButtonType;\n\n\tvoid downClick();\n\tvoid mentionsClick();\n\tvoid reactionsClick();\n\tvoid pollVotesClick();\n\n\tvoid clearReplyReturns();\n\t[[nodiscard]] QVector<FullMsgId> replyReturns() const;\n\tvoid setReplyReturns(QVector<FullMsgId> replyReturns);\n\tvoid pushReplyReturn(not_null<HistoryItem*> item);\n\tvoid skipReplyReturn(FullMsgId id);\n\tvoid calculateNextReplyReturn();\n\n\tvoid updateVisibility(Type type, bool shown);\n\tvoid updateUnreadThingsVisibility();\n\tvoid updateJumpDownVisibility(std::optional<int> counter = {});\n\tvoid updatePositions();\n\n\tvoid finishAnimations();\n\n\t[[nodiscard]] HistoryItem *replyReturn() const {\n\t\treturn _replyReturn;\n\t}\n\t[[nodiscard]] Fn<void(bool found)> doneJumpFrom(\n\t\tFullMsgId targetId,\n\t\tFullMsgId originId,\n\t\tbool ignoreMessageNotFound = false);\n\nprivate:\n\tCornerButtons(\n\t\tnot_null<QWidget*> parent,\n\t\tFn<bool(QEvent*)> scrollViewportEvent,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tnot_null<CornerButtonsDelegate*> delegate);\n\n\tbool eventFilter(QObject *o, QEvent *e) override;\n\n\tvoid computeCurrentReplyReturn();\n\n\t[[nodiscard]] CornerButton &buttonByType(Type type);\n\t[[nodiscard]] History *lookupHistory() const;\n\tvoid showAt(MsgId id);\n\n\tconst not_null<QWidget*> _parent;\n\tconst Fn<bool(QEvent*)> _scrollViewportEvent;\n\tconst not_null<CornerButtonsDelegate*> _delegate;\n\n\trpl::lifetime _stLifetime;\n\n\tCornerButton _down;\n\tCornerButton _mentions;\n\tCornerButton _reactions;\n\tCornerButton _pollVotes;\n\n\tHistoryItem *_replyReturn = nullptr;\n\tQVector<FullMsgId> _replyReturns;\n\n\tbool _replyReturnStarted = false;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_cursor_state.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_cursor_state.h\"\n\n#include \"history/history_item.h\"\n#include \"history/view/history_view_element.h\"\n\nnamespace HistoryView {\n\nTextState::TextState(not_null<const HistoryItem*> item)\n: itemId(item->fullId()) {\n}\n\nTextState::TextState(\n\tnot_null<const HistoryItem*> item,\n\tconst Ui::Text::StateResult &state)\n: itemId(item->fullId())\n, cursor(state.uponSymbol\n\t? CursorState::Text\n\t: CursorState::None)\n, link(state.link)\n, symbol(state.symbol)\n, afterSymbol(state.afterSymbol) {\n}\n\nTextState::TextState(\n\tnot_null<const HistoryItem*> item,\n\tClickHandlerPtr link)\n: itemId(item->fullId())\n, link(link) {\n}\n\nTextState::TextState(\n\tnot_null<const HistoryView::Element*> view)\n: TextState(view->data()) {\n}\n\nTextState::TextState(\n\tnot_null<const HistoryView::Element*> view,\n\tconst Ui::Text::StateResult &state)\n: TextState(view->data(), state) {\n}\n\nTextState::TextState(\n\tnot_null<const HistoryView::Element*> view,\n\tClickHandlerPtr link)\n: TextState(view->data(), link) {\n}\n\nTextState::TextState(\n\tstd::nullptr_t,\n\tconst Ui::Text::StateResult &state)\n: cursor(state.uponSymbol\n\t? CursorState::Text\n\t: CursorState::None)\n, link(state.link)\n, symbol(state.symbol)\n, afterSymbol(state.afterSymbol) {\n}\n\nTextState::TextState(std::nullptr_t, ClickHandlerPtr link)\n: link(link) {\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_cursor_state.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass HistoryItem;\n\nnamespace HistoryView {\n\nclass Element;\n\nenum class PointState : char {\n\tOutside,\n\tInside,\n\tGroupPart,\n};\nenum class CursorState : char {\n\tNone,\n\tText,\n\tDate,\n\tEnlarge,\n\tForwarded,\n\tFromPhoto,\n\tLogAdminService,\n};\n\nstruct TextState {\n\tTextState() = default;\n\tTextState(not_null<const HistoryItem*> item);\n\tTextState(\n\t\tnot_null<const HistoryItem*> item,\n\t\tconst Ui::Text::StateResult &state);\n\tTextState(\n\t\tnot_null<const HistoryItem*> item,\n\t\tClickHandlerPtr link);\n\tTextState(not_null<const HistoryView::Element*> view);\n\tTextState(\n\t\tnot_null<const HistoryView::Element*> view,\n\t\tconst Ui::Text::StateResult &state);\n\tTextState(\n\t\tnot_null<const HistoryView::Element*> view,\n\t\tClickHandlerPtr link);\n\tTextState(\n\t\tstd::nullptr_t,\n\t\tconst Ui::Text::StateResult &state);\n\tTextState(std::nullptr_t, ClickHandlerPtr link);\n\n\tFullMsgId itemId;\n\tCursorState cursor = CursorState::None;\n\tClickHandlerPtr link;\n\tuint16 symbol = 0;\n\tbool afterSymbol = false;\n\tbool overMessageText = false;\n\tbool customTooltip = false;\n\tbool horizontalScroll = false;\n\tQString customTooltipText;\n\n};\n\nstruct StateRequest {\n\tUi::Text::StateRequest::Flags flags = Ui::Text::StateRequest::Flag::LookupLink;\n\tUi::Text::StateRequest forText() const {\n\t\tUi::Text::StateRequest result;\n\t\tresult.flags = flags;\n\t\treturn result;\n\t}\n\tbool onlyMessageText = false;\n};\n\nenum class InfoDisplayType : char {\n\tDefault,\n\tImage,\n\tBackground,\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_draw_to_reply.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_draw_to_reply.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"editor/editor_layer_widget.h\"\n#include \"editor/photo_editor.h\"\n#include \"mainwidget.h\"\n#include \"ui/image/image.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace HistoryView {\n\nQImage ResolveDrawToReplyImage(\n\t\tnot_null<Data::Session*> data,\n\t\tconst Data::DrawToReplyRequest &request) {\n\tauto image = QImage();\n\tif (request.photoId) {\n\t\tconst auto photo = data->photo(request.photoId);\n\t\tconst auto media = photo->createMediaView();\n\t\tif (const auto large = media->image(Data::PhotoSize::Large)) {\n\t\t\timage = large->original();\n\t\t}\n\t}\n\tif (image.isNull() && request.documentId) {\n\t\tconst auto document = data->document(request.documentId);\n\t\tif (!document->isImage()) {\n\t\t\treturn QImage();\n\t\t}\n\t\tconst auto media = document->createMediaView();\n\t\tdocument->saveFromDataSilent();\n\t\tauto &location = document->location(true);\n\t\tif (location.accessEnable()) {\n\t\t\timage = Images::Read({ .path = location.name() }).image;\n\t\t} else {\n\t\t\timage = Images::Read({ .content = media->bytes() }).image;\n\t\t}\n\t\tlocation.accessDisable();\n\t}\n\treturn image;\n}\n\nvoid OpenDrawToReplyEditor(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tQImage image,\n\t\tFn<void(QImage &&)> done) {\n\tif (image.isNull()) {\n\t\treturn;\n\t}\n\tconst auto parent = controller->content();\n\tconst auto parentWidget = not_null<QWidget*>{ parent.get() };\n\tauto fileImage = std::make_shared<Image>(std::move(image));\n\tauto editor = base::make_unique_q<Editor::PhotoEditor>(\n\t\tparentWidget,\n\t\t&controller->window(),\n\t\tfileImage,\n\t\tEditor::PhotoModifications());\n\tconst auto raw = editor.get();\n\tauto layer = std::make_unique<Editor::LayerWidget>(\n\t\tparentWidget,\n\t\tstd::move(editor));\n\tEditor::InitEditorLayer(\n\t\tlayer.get(),\n\t\traw,\n\t\t[fileImage, done = std::move(done)](\n\t\t\t\tEditor::PhotoModifications mods) mutable {\n\t\t\tauto result = Editor::ImageModified(\n\t\t\t\tfileImage->original(),\n\t\t\t\tmods);\n\t\t\tif (result.isNull()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (done) {\n\t\t\t\tdone(std::move(result));\n\t\t\t}\n\t\t});\n\tcontroller->showLayer(std::move(layer), Ui::LayerOption::KeepOther);\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_draw_to_reply.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nclass Session;\nstruct DrawToReplyRequest;\n} // namespace Data\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace HistoryView {\n\n[[nodiscard]] QImage ResolveDrawToReplyImage(\n\tnot_null<Data::Session*> data,\n\tconst Data::DrawToReplyRequest &request);\n\nvoid OpenDrawToReplyEditor(\n\tnot_null<Window::SessionController*> controller,\n\tQImage image,\n\tFn<void(QImage &&)> done);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_element.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_element.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_transcribes.h\"\n#include \"history/view/history_view_service_message.h\"\n#include \"history/view/history_view_message.h\"\n#include \"history/view/media/history_view_media_generic.h\"\n#include \"history/view/media/history_view_media_grouped.h\"\n#include \"history/view/media/history_view_similar_channels.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"history/view/media/history_view_large_emoji.h\"\n#include \"history/view/media/history_view_custom_emoji.h\"\n#include \"history/view/media/history_view_no_forwards_request.h\"\n#include \"history/view/media/history_view_suggest_decision.h\"\n#include \"history/view/reactions/history_view_reactions_button.h\"\n#include \"history/view/history_view_reply_button.h\"\n#include \"history/view/reactions/history_view_reactions.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_reply.h\"\n#include \"history/view/history_view_text_helper.h\"\n#include \"history/history.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"payments/payments_reaction_process.h\" // TryAddingPaidReaction.\n#include \"window/window_session_controller.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/glare.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/effects/reaction_fly_animation.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/item_text_options.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/round_rect.h\"\n#include \"data/components/sponsored_messages.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_todo_list.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace HistoryView {\nnamespace {\n\n// A new message from the same sender is attached to previous within 15 minutes.\nconstexpr int kAttachMessageToPreviousSecondsDelta = 900;\nconstexpr auto kMaxShownLine = 1024 * 1024;\n\nElement *HoveredElement/* = nullptr*/;\nElement *PressedElement/* = nullptr*/;\nElement *HoveredLinkElement/* = nullptr*/;\nElement *PressedLinkElement/* = nullptr*/;\nElement *MousedElement/* = nullptr*/;\n\nclass KeyboardStyle : public ReplyKeyboard::Style {\npublic:\n\tKeyboardStyle(const style::BotKeyboardButton &st, Fn<void()> repaint);\n\n\tImages::CornersMaskRef buttonRounding(\n\t\tUi::BubbleRounding outer,\n\t\tRectParts sides) const override;\n\n\tconst style::TextStyle &textStyle() const override;\n\tvoid repaint(not_null<const HistoryItem*> item) const override;\n\nprotected:\n\tvoid paintButtonBg(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tconst QRect &rect,\n\t\tHistoryMessageMarkupButton::Color color,\n\t\tUi::BubbleRounding rounding,\n\t\tfloat64 howMuchOver) const override;\n\tvoid paintButtonStart(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tHistoryMessageMarkupButton::Color color) const override;\n\tvoid paintButtonIcon(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tconst QRect &rect,\n\t\tint outerWidth,\n\t\tHistoryMessageMarkupButton::Type type) const override;\n\tvoid paintButtonLoading(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tconst QRect &rect,\n\t\tHistoryMessageMarkupButton::Color color,\n\t\tint outerWidth,\n\t\tUi::BubbleRounding rounding) const override;\n\tint minButtonWidth(HistoryMessageMarkupButton::Type type) const override;\n\nprivate:\n\tstruct CachedBg {\n\t\tQImage image;\n\t\tQColor color;\n\t};\n\tusing BubbleRoundingKey = uchar;\n\tstruct CacheKey {\n\t\tBubbleRoundingKey rounding;\n\t\tHistoryMessageMarkupButton::Color color;\n\n\t\tfriend inline constexpr auto operator<=>(\n\t\t\tCacheKey,\n\t\t\tCacheKey) = default;\n\t\tfriend inline constexpr bool operator==(\n\t\t\tCacheKey,\n\t\t\tCacheKey) = default;\n\t};\n\tmutable base::flat_map<CacheKey, CachedBg> _cachedBg;\n\tmutable base::flat_map<CacheKey, QPainterPath> _cachedOutline;\n\tmutable std::unique_ptr<Ui::GlareEffect> _glare;\n\tFn<void()> _repaint;\n\n};\n\nKeyboardStyle::KeyboardStyle(\n\tconst style::BotKeyboardButton &st,\n\tFn<void()> repaint)\n: ReplyKeyboard::Style(st)\n, _repaint(std::move(repaint)) {\n}\n\nvoid KeyboardStyle::paintButtonStart(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tHistoryMessageMarkupButton::Color color) const {\n\tusing Color = HistoryMessageMarkupButton::Color;\n\tExpects(st != nullptr);\n\n\tp.setPen((color == Color::Normal) ? st->msgServiceFg() : st::white);\n}\n\nconst style::TextStyle &KeyboardStyle::textStyle() const {\n\treturn st::serviceTextStyle;\n}\n\nvoid KeyboardStyle::repaint(not_null<const HistoryItem*> item) const {\n\titem->history()->owner().requestItemRepaint(item);\n}\n\nImages::CornersMaskRef KeyboardStyle::buttonRounding(\n\t\tUi::BubbleRounding outer,\n\t\tRectParts sides) const {\n\tusing namespace Images;\n\tusing namespace Ui;\n\tusing Radius = CachedCornerRadius;\n\tusing Corner = BubbleCornerRounding;\n\tauto result = CornersMaskRef(CachedCornersMasks(Radius::BubbleSmall));\n\tif (sides & RectPart::Bottom) {\n\t\tconst auto &large = CachedCornersMasks(Radius::BubbleLarge);\n\t\tauto round = [&](RectPart side, int index) {\n\t\t\tif ((sides & side) && (outer[index] == Corner::Large)) {\n\t\t\t\tresult.p[index] = &large[index];\n\t\t\t}\n\t\t};\n\t\tround(RectPart::Left, kBottomLeft);\n\t\tround(RectPart::Right, kBottomRight);\n\t}\n\treturn result;\n}\n\nvoid KeyboardStyle::paintButtonBg(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tconst QRect &rect,\n\t\tHistoryMessageMarkupButton::Color color,\n\t\tUi::BubbleRounding rounding,\n\t\tfloat64 howMuchOver) const {\n\tExpects(st != nullptr);\n\n\tusing Corner = Ui::BubbleCornerRounding;\n\tconst auto key = CacheKey{ rounding.key(), color };\n\tauto &cachedBg = _cachedBg[key];\n\n\tconst auto sti = &st->imageStyle(false);\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (cachedBg.image.isNull()\n\t\t|| cachedBg.image.width() != (rect.width() * ratio)\n\t\t|| cachedBg.color != sti->msgServiceBg->c) {\n\t\tcachedBg.color = sti->msgServiceBg->c;\n\t\tcachedBg.image = QImage(\n\t\t\trect.size() * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tcachedBg.image.setDevicePixelRatio(ratio);\n\t\tcachedBg.image.fill(Qt::transparent);\n\t\t{\n\t\t\tauto painter = QPainter(&cachedBg.image);\n\n\t\t\tusing Color = HistoryMessageMarkupButton::Color;\n\t\t\tconst auto normal = (color == Color::Normal);\n\t\t\tconst auto colored = style::owned_color((color == Color::Primary)\n\t\t\t\t? st::botKbInlinePrimaryBg->c\n\t\t\t\t: (color == Color::Danger)\n\t\t\t\t? st::botKbInlineDangerBg->c\n\t\t\t\t: st::botKbInlineSuccessBg->c);\n\t\t\tconst auto smallColored = normal\n\t\t\t\t? Ui::CornersPixmaps()\n\t\t\t\t: Ui::PrepareCornerPixmaps(\n\t\t\t\t\tUi::BubbleRadiusSmall(),\n\t\t\t\t\tcolored.color());\n\t\t\tconst auto largeColored = normal\n\t\t\t\t? Ui::CornersPixmaps()\n\t\t\t\t: Ui::PrepareCornerPixmaps(\n\t\t\t\t\tUi::BubbleRadiusLarge(),\n\t\t\t\t\tcolored.color());\n\t\t\tconst auto small = normal\n\t\t\t\t? &sti->msgServiceBgCornersSmall\n\t\t\t\t: &smallColored;\n\t\t\tconst auto large = normal\n\t\t\t\t? &sti->msgServiceBgCornersLarge\n\t\t\t\t: &largeColored;\n\t\t\tauto corners = Ui::CornersPixmaps();\n\t\t\tint radiuses[4];\n\t\t\tfor (auto i = 0; i != 4; ++i) {\n\t\t\t\tconst auto isLarge = (rounding[i] == Corner::Large);\n\t\t\t\tcorners.p[i] = (isLarge ? large : small)->p[i];\n\t\t\t\tradiuses[i] = Ui::CachedCornerRadiusValue(isLarge\n\t\t\t\t\t? Ui::CachedCornerRadius::BubbleLarge\n\t\t\t\t\t: Ui::CachedCornerRadius::BubbleSmall);\n\t\t\t}\n\t\t\tconst auto r = Rect(rect.size());\n\t\t\t_cachedOutline[key] = Ui::ComplexRoundedRectPath(\n\t\t\t\tr - Margins(st::lineWidth),\n\t\t\t\tradiuses[0],\n\t\t\t\tradiuses[1],\n\t\t\t\tradiuses[2],\n\t\t\t\tradiuses[3]);\n\t\t\tUi::FillRoundRect(\n\t\t\t\tpainter,\n\t\t\t\tr,\n\t\t\t\tnormal ? sti->msgServiceBg : colored.color(),\n\t\t\t\tcorners);\n\t\t}\n\t}\n\tp.drawImage(rect.topLeft(), cachedBg.image);\n\tif (howMuchOver > 0) {\n\t\tauto o = p.opacity();\n\t\tp.setOpacity(o * howMuchOver);\n\t\tconst auto &small = st->msgBotKbOverBgAddCornersSmall();\n\t\tconst auto &large = st->msgBotKbOverBgAddCornersLarge();\n\t\tauto over = Ui::CornersPixmaps();\n\t\tfor (auto i = 0; i != 4; ++i) {\n\t\t\tover.p[i] = (rounding[i] == Corner::Large ? large : small).p[i];\n\t\t}\n\t\tUi::FillRoundRect(p, rect, st->msgBotKbOverBgAdd(), over);\n\t\tp.setOpacity(o);\n\t}\n}\n\nvoid KeyboardStyle::paintButtonIcon(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tconst QRect &rect,\n\t\tint outerWidth,\n\t\tHistoryMessageMarkupButton::Type type) const {\n\tExpects(st != nullptr);\n\n\tusing Type = HistoryMessageMarkupButton::Type;\n\tconst auto icon = [&]() -> const style::icon* {\n\t\tswitch (type) {\n\t\tcase Type::Url:\n\t\tcase Type::Auth: return &st->msgBotKbUrlIcon();\n\t\tcase Type::Buy: return &st->msgBotKbPaymentIcon();\n\t\tcase Type::SwitchInlineSame:\n\t\tcase Type::SwitchInline: return &st->msgBotKbSwitchPmIcon();\n\t\tcase Type::WebView:\n\t\tcase Type::SimpleWebView: return &st->msgBotKbWebviewIcon();\n\t\tcase Type::CopyText: return &st->msgBotKbCopyIcon();\n\t\t}\n\t\treturn nullptr;\n\t}();\n\tif (icon) {\n\t\ticon->paint(p, rect.x() + rect.width() - icon->width() - st::msgBotKbIconPadding, rect.y() + st::msgBotKbIconPadding, outerWidth);\n\t}\n}\n\nvoid KeyboardStyle::paintButtonLoading(\n\t\tQPainter &p,\n\t\tconst Ui::ChatStyle *st,\n\t\tconst QRect &rect,\n\t\tHistoryMessageMarkupButton::Color color,\n\t\tint outerWidth,\n\t\tUi::BubbleRounding rounding) const {\n\tExpects(st != nullptr);\n\n\tif (anim::Disabled()) {\n\t\tconst auto &icon = st->historySendingInvertedIcon();\n\t\ticon.paint(\n\t\t\tp,\n\t\t\trect::right(rect) - icon.width() - st::msgBotKbIconPadding,\n\t\t\trect::bottom(rect) - icon.height() - st::msgBotKbIconPadding,\n\t\t\trect.x() * 2 + rect.width());\n\t\treturn;\n\t}\n\n\tconst auto key = CacheKey{ rounding.key(), color };\n\tauto &cachedBg = _cachedBg[key];\n\tif (!cachedBg.image.isNull()) {\n\t\tif (_glare && _glare->glare.birthTime) {\n\t\t\tconst auto progress = _glare->progress(crl::now());\n\t\t\tconst auto w = _glare->width;\n\t\t\tconst auto h = rect.height();\n\t\t\tconst auto x = (-w) + (w * 2) * progress;\n\n\t\t\tauto frame = cachedBg.image;\n\t\t\tframe.fill(Qt::transparent);\n\t\t\t{\n\t\t\t\tauto painter = QPainter(&frame);\n\t\t\t\tauto hq = PainterHighQualityEnabler(painter);\n\t\t\t\tpainter.setPen(Qt::NoPen);\n\t\t\t\tpainter.drawTiledPixmap(x, 0, w, h, _glare->pixmap, 0, 0);\n\n\t\t\t\tauto path = QPainterPath();\n\t\t\t\tpath.addRect(Rect(rect.size()));\n\t\t\t\tpath -= _cachedOutline[key];\n\n\t\t\t\tconstexpr auto kBgOutlineAlpha = 0.5;\n\t\t\t\tconstexpr auto kFgOutlineAlpha = 0.8;\n\t\t\t\tconst auto &c = st::premiumButtonFg->c;\n\t\t\t\tpainter.setPen(Qt::NoPen);\n\t\t\t\tpainter.setBrush(c);\n\t\t\t\tpainter.setOpacity(kBgOutlineAlpha);\n\t\t\t\tpainter.drawPath(path);\n\t\t\t\tauto gradient = QLinearGradient(-w, 0, w * 2, 0);\n\t\t\t\t{\n\t\t\t\t\tconstexpr auto kShiftLeft = 0.01;\n\t\t\t\t\tconstexpr auto kShiftRight = 0.99;\n\t\t\t\t\tauto stops = _glare->computeGradient(c).stops();\n\t\t\t\t\tstops[1] = {\n\t\t\t\t\t\tstd::clamp(progress, kShiftLeft, kShiftRight),\n\t\t\t\t\t\tQColor(c.red(), c.green(), c.blue(), kFgOutlineAlpha),\n\t\t\t\t\t};\n\t\t\t\t\tgradient.setStops(std::move(stops));\n\t\t\t\t}\n\t\t\t\tpainter.setBrush(QBrush(gradient));\n\t\t\t\tpainter.setOpacity(1);\n\t\t\t\tpainter.drawPath(path);\n\n\t\t\t\tpainter.setCompositionMode(\n\t\t\t\t\tQPainter::CompositionMode_DestinationIn);\n\t\t\t\tpainter.drawImage(0, 0, cachedBg.image);\n\t\t\t}\n\t\t\tp.drawImage(rect.x(), rect.y(), frame);\n\t\t} else {\n\t\t\t_glare = std::make_unique<Ui::GlareEffect>();\n\t\t\t_glare->width = outerWidth;\n\n\t\t\tconstexpr auto kTimeout = crl::time(0);\n\t\t\tconstexpr auto kDuration = crl::time(1100);\n\t\t\tconst auto color = st::premiumButtonFg->c;\n\t\t\t_glare->validate(color, _repaint, kTimeout, kDuration);\n\t\t}\n\t}\n}\n\nint KeyboardStyle::minButtonWidth(\n\t\tHistoryMessageMarkupButton::Type type) const {\n\tusing Type = HistoryMessageMarkupButton::Type;\n\tint result = 2 * buttonPadding(), iconWidth = 0;\n\tswitch (type) {\n\tcase Type::Url:\n\tcase Type::Auth: iconWidth = st::msgBotKbUrlIcon.width(); break;\n\tcase Type::Buy: iconWidth = st::msgBotKbPaymentIcon.width(); break;\n\tcase Type::SwitchInlineSame:\n\tcase Type::SwitchInline: iconWidth = st::msgBotKbSwitchPmIcon.width(); break;\n\tcase Type::Callback:\n\tcase Type::CallbackWithPassword:\n\tcase Type::Game: iconWidth = st::historySendingInvertedIcon.width(); break;\n\tcase Type::WebView:\n\tcase Type::SimpleWebView: iconWidth = st::msgBotKbWebviewIcon.width(); break;\n\tcase Type::CopyText: return st::msgBotKbCopyIcon.width(); break;\n\t}\n\tif (iconWidth > 0) {\n\t\tresult = std::max(result, 2 * iconWidth + 4 * int(st::msgBotKbIconPadding));\n\t}\n\treturn result;\n}\n\n[[nodiscard]] bool IsAttachedToPreviousInSavedMessages(\n\t\tnot_null<HistoryItem*> previous,\n\t\tHistoryMessageForwarded *prevForwarded,\n\t\tnot_null<HistoryItem*> item,\n\t\tHistoryMessageForwarded *forwarded) {\n\tconst auto sender = previous->displayFrom();\n\tif ((prevForwarded != nullptr) != (forwarded != nullptr)) {\n\t\treturn false;\n\t} else if (sender != item->displayFrom()) {\n\t\treturn false;\n\t} else if (!prevForwarded || sender) {\n\t\treturn true;\n\t}\n\tconst auto previousInfo = prevForwarded->savedFromHiddenSenderInfo\n\t\t? prevForwarded->savedFromHiddenSenderInfo.get()\n\t\t: prevForwarded->originalHiddenSenderInfo.get();\n\tconst auto itemInfo = forwarded->savedFromHiddenSenderInfo\n\t\t? forwarded->savedFromHiddenSenderInfo.get()\n\t\t: forwarded->originalHiddenSenderInfo.get();\n\tAssert(previousInfo != nullptr);\n\tAssert(itemInfo != nullptr);\n\treturn (*previousInfo == *itemInfo);\n}\n\n[[nodiscard]] Window::SessionController *ContextOrSessionWindow(\n\t\tconst ClickHandlerContext &context,\n\t\tnot_null<Main::Session*> session) {\n\tif (const auto controller = context.sessionWindow.get()) {\n\t\tif (&controller->session() == session) {\n\t\t\treturn controller;\n\t\t}\n\t}\n\treturn session->tryResolveWindow();\n}\n\n[[nodiscard]] TextSelection ApplyModificationsFrom(\n\t\tTextSelection result,\n\t\tconst Ui::Text::String &text) {\n\tif (result.empty()) {\n\t\treturn result;\n\t}\n\tfor (const auto &modification : text.modifications()) {\n\t\tif (modification.position >= result.to) {\n\t\t\tbreak;\n\t\t}\n\t\tif (modification.added) {\n\t\t\tresult.to += modification.added;\n\t\t}\n\t\tconst auto shiftTo = std::min(\n\t\t\tint(modification.skipped),\n\t\t\tresult.to - modification.position);\n\t\tresult.to -= shiftTo;\n\t\tif (modification.position < result.from) {\n\t\t\tif (modification.added) {\n\t\t\t\tresult.from += modification.added;\n\t\t\t}\n\t\t\tconst auto shiftFrom = std::min(\n\t\t\t\tint(modification.skipped),\n\t\t\t\tresult.from - modification.position);\n\t\t\tresult.from -= shiftFrom;\n\t\t} else if (modification.position == result.from) {\n\t\t\tif (!modification.skipped) {\n\t\t\t\tresult.from += modification.added;\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\n} // namespace\n\nstd::unique_ptr<Ui::PathShiftGradient> MakePathShiftGradient(\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tFn<void()> update) {\n\treturn std::make_unique<Ui::PathShiftGradient>(\n\t\tst->msgServiceBg(),\n\t\tst->msgServiceBgSelected(),\n\t\tstd::move(update),\n\t\tst->paletteChanged());\n}\n\nbool DefaultElementDelegate::elementUnderCursor(\n\t\tnot_null<const Element*> view) {\n\treturn false;\n}\n\nSelectionModeResult DefaultElementDelegate::elementInSelectionMode(\n\t\tconst Element *view) {\n\treturn {};\n}\n\nbool DefaultElementDelegate::elementIntersectsRange(\n\t\tnot_null<const Element*> view,\n\t\tint from,\n\t\tint till) {\n\treturn true;\n}\n\nvoid DefaultElementDelegate::elementStartStickerLoop(\n\tnot_null<const Element*> view) {\n}\n\nvoid DefaultElementDelegate::elementShowPollResults(\n\tnot_null<PollData*> poll,\n\tFullMsgId context) {\n}\n\nvoid DefaultElementDelegate::elementShowAddPollOption(\n\tnot_null<Element*> view,\n\tnot_null<PollData*> poll,\n\tFullMsgId context,\n\tQRect optionRect) {\n}\n\nvoid DefaultElementDelegate::elementSubmitAddPollOption(FullMsgId context) {\n}\n\nvoid DefaultElementDelegate::elementOpenPhoto(\n\tnot_null<PhotoData*> photo,\n\tFullMsgId context) {\n}\n\nvoid DefaultElementDelegate::elementOpenDocument(\n\tnot_null<DocumentData*> document,\n\tFullMsgId context,\n\tbool showInMediaView) {\n}\n\nvoid DefaultElementDelegate::elementCancelUpload(const FullMsgId &context) {\n}\n\nvoid DefaultElementDelegate::elementShowTooltip(\n\tconst TextWithEntities &text,\n\tFn<void()> hiddenCallback) {\n}\n\nbool DefaultElementDelegate::elementHideReply(\n\t\tnot_null<const Element*> view) {\n\treturn false;\n}\n\nbool DefaultElementDelegate::elementShownUnread(\n\t\tnot_null<const Element*> view) {\n\treturn view->data()->unread(view->data()->history());\n}\n\nvoid DefaultElementDelegate::elementSendBotCommand(\n\tconst QString &command,\n\tconst FullMsgId &context) {\n}\n\nvoid DefaultElementDelegate::elementSearchInList(\n\tconst QString &query,\n\tconst FullMsgId &context) {\n}\n\nvoid DefaultElementDelegate::elementHandleViaClick(\n\tnot_null<UserData*> bot) {\n}\n\nElementChatMode DefaultElementDelegate::elementChatMode() {\n\treturn ElementChatMode::Default;\n}\n\nvoid DefaultElementDelegate::elementReplyTo(const FullReplyTo &to) {\n}\n\nvoid DefaultElementDelegate::elementStartInteraction(\n\tnot_null<const Element*> view) {\n}\n\nvoid DefaultElementDelegate::elementStartPremium(\n\tnot_null<const Element*> view,\n\tElement *replacing) {\n}\n\nvoid DefaultElementDelegate::elementCancelPremium(\n\tnot_null<const Element*> view) {\n}\n\nvoid DefaultElementDelegate::elementStartEffect(\n\tnot_null<const Element*> view,\n\tElement *replacing) {\n}\n\nQString DefaultElementDelegate::elementAuthorRank(\n\t\tnot_null<const Element*> view) {\n\treturn {};\n}\n\nbool DefaultElementDelegate::elementHideTopicButton(\n\t\tnot_null<const Element*> view) {\n\treturn true;\n}\n\n\nSimpleElementDelegate::SimpleElementDelegate(\n\tnot_null<Window::SessionController*> controller,\n\tFn<void()> update)\n: _controller(controller)\n, _pathGradient(\n\tMakePathShiftGradient(\n\t\tcontroller->chatStyle(),\n\t\tstd::move(update))) {\n}\n\nSimpleElementDelegate::~SimpleElementDelegate() = default;\n\nbool SimpleElementDelegate::elementAnimationsPaused() {\n\treturn _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any);\n}\n\nauto SimpleElementDelegate::elementPathShiftGradient()\n-> not_null<Ui::PathShiftGradient*> {\n\treturn _pathGradient.get();\n}\n\nTextSelection UnshiftItemSelection(\n\t\tTextSelection selection,\n\t\tuint16 byLength) {\n\treturn (selection == FullSelection)\n\t\t? selection\n\t\t: ::unshiftSelection(selection, byLength);\n}\n\nTextSelection ShiftItemSelection(\n\t\tTextSelection selection,\n\t\tuint16 byLength) {\n\treturn (selection == FullSelection)\n\t\t? selection\n\t\t: ::shiftSelection(selection, byLength);\n}\n\nTextSelection UnshiftItemSelection(\n\t\tTextSelection selection,\n\t\tconst Ui::Text::String &byText) {\n\treturn UnshiftItemSelection(selection, byText.length());\n}\n\nTextSelection ShiftItemSelection(\n\t\tTextSelection selection,\n\t\tconst Ui::Text::String &byText) {\n\treturn ShiftItemSelection(selection, byText.length());\n}\n\nQString DateTooltipText(not_null<Element*> view) {\n\tconst auto locale = QLocale();\n\tconst auto format = QLocale::LongFormat;\n\tconst auto item = view->data();\n\tauto dateText = locale.toString(view->dateTime(), format);\n\tif (item->awaitingVideoProcessing()) {\n\t\tdateText += '\\n' + tr::lng_approximate_about(tr::now);\n\t}\n\tif (const auto editedDate = view->displayedEditDate()) {\n\t\tdateText += '\\n' + tr::lng_edited_date(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\tlocale.toString(base::unixtime::parse(editedDate), format));\n\t}\n\tif (const auto forwarded = item->Get<HistoryMessageForwarded>()) {\n\t\tif (!forwarded->story && forwarded->psaType.isEmpty()) {\n\t\t\tdateText += '\\n' + tr::lng_forwarded_date(\n\t\t\t\ttr::now,\n\t\t\t\tlt_date,\n\t\t\t\tlocale.toString(\n\t\t\t\t\tbase::unixtime::parse(forwarded->originalDate),\n\t\t\t\t\tformat));\n\t\t\tif (forwarded->imported) {\n\t\t\t\tdateText = tr::lng_forwarded_imported(tr::now)\n\t\t\t\t\t+ \"\\n\\n\" + dateText;\n\t\t\t}\n\t\t\tif (forwarded->savedFromDate\n\t\t\t\t&& forwarded->savedFromDate != forwarded->originalDate) {\n\t\t\t\tconst auto parsed = base::unixtime::parse(\n\t\t\t\t\tforwarded->savedFromDate);\n\t\t\t\tif (parsed != view->dateTime()) {\n\t\t\t\t\tdateText += '\\n' + tr::lng_forwarded_forwarded_date(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_date,\n\t\t\t\t\t\tlocale.toString(parsed, format));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (view->isSignedAuthorElided()) {\n\t\tif (const auto msgsigned = item->Get<HistoryMessageSigned>()) {\n\t\t\tdateText += '\\n' + tr::lng_signed_author(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tmsgsigned->author);\n\t\t}\n\t}\n\tif (item->isScheduled() && item->isSilent()) {\n\t\tdateText += '\\n' + QChar(0xD83D) + QChar(0xDD15);\n\t}\n\tif (const auto stars = item->out() ? item->starsPaid() : 0) {\n\t\tdateText += '\\n' + tr::lng_you_paid_stars(tr::now, lt_count, stars);\n\t}\n\treturn dateText;\n}\n\nvoid UnreadBar::init(const QString &string) {\n\ttext = string;\n\twidth = st::semiboldFont->width(text);\n}\n\nint UnreadBar::height() {\n\treturn st::historyUnreadBarHeight + st::historyUnreadBarMargin;\n}\n\nint UnreadBar::marginTop() {\n\treturn st::lineWidth + st::historyUnreadBarMargin;\n}\n\nvoid UnreadBar::paint(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint y,\n\t\tint w,\n\t\tElementChatMode mode) const {\n\tconst auto previousTranslation = p.transform().dx();\n\tif (previousTranslation != 0) {\n\t\tp.translate(-previousTranslation, 0);\n\t}\n\tconst auto st = context.st;\n\tconst auto bottom = y + height();\n\ty += marginTop();\n\tp.fillRect(\n\t\t0,\n\t\ty,\n\t\tw,\n\t\theight() - marginTop() - st::lineWidth,\n\t\tst->historyUnreadBarBg());\n\tp.fillRect(\n\t\t0,\n\t\tbottom - st::lineWidth,\n\t\tw,\n\t\tst::lineWidth,\n\t\tst->historyUnreadBarBorder());\n\tp.setFont(st::historyUnreadBarFont);\n\tp.setPen(st->historyUnreadBarFg());\n\n\tint maxwidth = w;\n\tif (mode == ElementChatMode::Wide) {\n\t\tmaxwidth = qMin(\n\t\t\tmaxwidth,\n\t\t\tst::msgMaxWidth\n\t\t\t\t+ 2 * st::msgPhotoSkip\n\t\t\t\t+ 2 * st::msgMargin.left());\n\t}\n\tw = maxwidth;\n\n\tconst auto skip = st::historyUnreadBarHeight\n\t\t- 2 * st::lineWidth\n\t\t- st::historyUnreadBarFont->height;\n\tp.drawText(\n\t\t(w - width) / 2,\n\t\ty + (skip / 2) + st::historyUnreadBarFont->ascent,\n\t\ttext);\n\tif (previousTranslation != 0) {\n\t\tp.translate(previousTranslation, 0);\n\t}\n}\n\nvoid DateBadge::init(const QString &date) {\n\ttext = date;\n\twidth = st::msgServiceFont->width(text);\n}\n\nint DateBadge::height() const {\n\treturn st::msgServiceMargin.top()\n\t\t+ st::msgServicePadding.top()\n\t\t+ st::msgServiceFont->height\n\t\t+ st::msgServicePadding.bottom()\n\t\t+ st::msgServiceMargin.bottom();\n}\n\nvoid DateBadge::paint(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide) const {\n\tServiceMessagePainter::PaintDate(p, st, text, width, y, w, chatWide);\n}\n\nvoid ForumThreadBar::init(\n\t\tnot_null<PeerData*> parentChat,\n\t\tnot_null<Data::Thread*> thread) {\n\tthis->thread = thread;\n\tconst auto sublist = thread->asSublist();\n\tif (sublist) {\n\t\ttext.setText(st::semiboldTextStyle, sublist->sublistPeer()->name());\n\t} else if (const auto topic = thread->asTopic()) {\n\t\ttext.setMarkedText(\n\t\t\tst::semiboldTextStyle,\n\t\t\ttopic->titleWithIconOrLogo(),\n\t\t\tkMarkupTextOptions,\n\t\t\tCore::TextContext({\n\t\t\t\t.session = &topic->session(),\n\t\t\t\t.customEmojiLoopLimit = -1, // First frame only\n\t\t\t}));\n\t}\n\tconst auto skip = st::monoforumBarUserpicSkip;\n\tconst auto userpic = sublist\n\t\t? (st::msgServicePadding.top()\n\t\t\t+ st::msgServiceFont->height\n\t\t\t+ st::msgServicePadding.bottom()\n\t\t\t- 2 * skip)\n\t\t: (st::msgServicePadding.left() - 3 * skip);\n\n\twidth = skip\n\t\t+ userpic\n\t\t+ skip * 2\n\t\t+ text.maxWidth()\n\t\t+ st::topicButtonArrowSkip\n\t\t+ st::msgServicePadding.right();\n}\n\nint ForumThreadBar::height() const {\n\treturn st::msgServiceMargin.top()\n\t\t+ st::msgServicePadding.top()\n\t\t+ st::msgServiceFont->height\n\t\t+ st::msgServicePadding.bottom()\n\t\t+ st::msgServiceMargin.bottom();\n}\n\nvoid ForumThreadBar::paint(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide,\n\t\tbool skipPatternLine) const {\n\tif (const auto strong = thread.get()) {\n\t\tPaint(\n\t\t\tp,\n\t\t\tst,\n\t\t\tstrong,\n\t\t\ttext,\n\t\t\twidth,\n\t\t\tview,\n\t\t\ty,\n\t\t\tw,\n\t\t\tchatWide,\n\t\t\tskipPatternLine);\n\t}\n}\n\nint ForumThreadBar::PaintForGetWidth(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tnot_null<Element*> itemView,\n\t\tUi::PeerUserpicView &userpicView,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide) {\n\tconst auto item = itemView->data();\n\tconst auto topic = item->topic();\n\tconst auto sublist = item->savedSublist();\n\tconst auto sender = topic\n\t\t? (Data::Thread*)topic\n\t\t: (sublist && sublist->parentChat())\n\t\t? (Data::Thread*)sublist\n\t\t: nullptr;\n\tauto text = Ui::Text::String();\n\tif (!sender\n\t\t|| !topic\n\t\t|| (sublist && sublist->sublistPeer()->isMonoforum())) {\n\t\treturn 0;\n\t} else if (topic) {\n\t\ttext.setMarkedText(\n\t\t\tst::semiboldTextStyle,\n\t\t\ttopic->titleWithIconOrLogo(),\n\t\t\tkMarkupTextOptions,\n\t\t\tCore::TextContext({\n\t\t\t\t.session = &topic->session(),\n\t\t\t\t.customEmojiLoopLimit = -1, // First frame only\n\t\t\t}));\n\t} else {\n\t\ttext.setText(st::semiboldTextStyle, sublist->sublistPeer()->name());\n\t}\n\tconst auto skip = st::monoforumBarUserpicSkip;\n\tconst auto userpic = sublist\n\t\t? (st::msgServicePadding.top()\n\t\t\t+ st::msgServiceFont->height\n\t\t\t+ st::msgServicePadding.bottom()\n\t\t\t- 2 * skip)\n\t\t: (st::msgServicePadding.left() - 3 * skip);\n\tconst auto width = skip\n\t\t+ userpic\n\t\t+ skip * 2\n\t\t+ text.maxWidth()\n\t\t+ st::topicButtonArrowSkip\n\t\t+ st::msgServicePadding.right();\n\tPaint(p, st, sender, text, width, userpicView, y, w, chatWide, true);\n\treturn width;\n}\n\nvoid ForumThreadBar::Paint(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst Ui::Text::String &text,\n\t\tint width,\n\t\tUi::PeerUserpicView &view,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide,\n\t\tbool skipPatternLine) {\n\tint left = st::msgServiceMargin.left();\n\tconst auto maxwidth = chatWide\n\t\t? std::min(w, WideChatWidth())\n\t\t: w;\n\tw = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left();\n\n\tconst auto use = std::min(w, width);\n\n\tleft += (w - use) / 2;\n\tint h = st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom();\n\tServiceMessagePainter::PaintBubble(\n\t\tp,\n\t\tst->msgServiceBg(),\n\t\tst->serviceBgCornersNormal(),\n\t\tQRect(left, y + st::msgServiceMargin.top(), use, h));\n\n\tconst auto skip = st::monoforumBarUserpicSkip;\n\tif (!skipPatternLine) {\n\t\tauto pen = st->msgServiceBg()->p;\n\t\tpen.setWidthF(skip);\n\t\tpen.setCapStyle(Qt::RoundCap);\n\t\tpen.setDashPattern({ 2., 2. });\n\t\tp.setPen(pen);\n\t\tconst auto top = y + st::msgServiceMargin.top() + (h / 2);\n\t\tp.drawLine(0, top, left, top);\n\t\tp.drawLine(left + use, top, 2 * w, top);\n\t}\n\n\tconst auto sublist = thread->asSublist();\n\tconst auto userpic = sublist\n\t\t? (st::msgServicePadding.top()\n\t\t\t+ st::msgServiceFont->height\n\t\t\t+ st::msgServicePadding.bottom()\n\t\t\t- 2 * skip)\n\t\t: (st::msgServicePadding.left() - 3 * skip);\n\tconst auto available = use\n\t\t- (skip\n\t\t\t+ userpic\n\t\t\t+ skip * 2\n\t\t\t+ st::topicButtonArrowSkip\n\t\t\t+ st::msgServicePadding.right());\n\n\tif (sublist) {\n\t\tsublist->sublistPeer()->paintUserpic(\n\t\t\tp,\n\t\t\tview,\n\t\t\tleft + skip,\n\t\t\ty + st::msgServiceMargin.top() + skip,\n\t\t\tuserpic);\n\t}\n\n\tconst auto textLeft = left + skip + userpic + skip * 2;\n\tconst auto textTop = y\n\t\t+ st::msgServiceMargin.top()\n\t\t+ st::msgServicePadding.top();\n\tp.setFont(st::msgServiceFont);\n\tp.setPen(st->msgServiceFg());\n\ttext.draw(p, {\n\t\t.position = { textLeft, textTop },\n\t\t.availableWidth = available,\n\t\t.paused = true,\n\t\t.elisionLines = 1,\n\t});\n\n\tst::topicButtonArrow.paint(\n\t\tp,\n\t\ttextLeft + available + st::topicButtonArrowPosition.x(),\n\t\ttextTop + st::topicButtonArrowPosition.y(),\n\t\tw,\n\t\tst->msgServiceFg()->c);\n}\n\nvoid ServicePreMessage::init(\n\t\tnot_null<Element*> view,\n\t\tPreparedServiceText string,\n\t\tClickHandlerPtr fullClickHandler,\n\t\tstd::unique_ptr<Media> media,\n\t\tbool below) {\n\tthis->below = below;\n\ttext = Ui::Text::String(\n\t\tst::serviceTextStyle,\n\t\tstring.text,\n\t\tkMarkupTextOptions,\n\t\tst::msgMinWidth,\n\t\tCore::TextContext({\n\t\t\t.session = &view->history()->session(),\n\t\t\t.repaint = [=] { view->customEmojiRepaint(); },\n\t\t}));\n\thandler = std::move(fullClickHandler);\n\tfor (auto i = 0; i != int(string.links.size()); ++i) {\n\t\ttext.setLink(i + 1, string.links[i]);\n\t}\n\tthis->media = std::move(media);\n}\n\nint ServicePreMessage::resizeToWidth(int newWidth, ElementChatMode mode) {\n\twidth = newWidth;\n\tif (mode == ElementChatMode::Wide) {\n\t\taccumulate_min(\n\t\t\twidth,\n\t\t\tst::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left());\n\t}\n\n\tif (media) {\n\t\tmedia->initDimensions();\n\t\tmedia->resizeGetHeight(width);\n\t}\n\n\tif (media && media->hideServiceText()) {\n\t\theight = media->height() + st::msgServiceMargin.bottom();\n\t} else {\n\t\tauto contentWidth = width;\n\t\tcontentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.right();\n\t\tif (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) {\n\t\t\tcontentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1;\n\t\t}\n\n\t\tauto maxWidth = text.maxWidth()\n\t\t\t+ st::msgServicePadding.left()\n\t\t\t+ st::msgServicePadding.right();\n\t\tauto minHeight = text.minHeight();\n\n\t\tauto nwidth = qMax(contentWidth\n\t\t\t- st::msgServicePadding.left()\n\t\t\t- st::msgServicePadding.right(), 0);\n\t\theight = (contentWidth >= maxWidth)\n\t\t\t? minHeight\n\t\t\t: text.countHeight(nwidth);\n\t\theight += st::msgServicePadding.top()\n\t\t\t+ st::msgServicePadding.bottom()\n\t\t\t+ st::msgServiceMargin.top()\n\t\t\t+ st::msgServiceMargin.bottom();\n\t}\n\n\treturn height;\n}\n\nvoid ServicePreMessage::paint(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tQRect g,\n\t\tElementChatMode mode) const {\n\tif (media && media->hideServiceText()) {\n\t\tconst auto left = (width - media->width()) / 2;\n\t\tconst auto top = below\n\t\t\t? (g.top() + g.height() - st::msgServiceMargin.top() + st::msgServiceMargin.bottom())\n\t\t\t: (g.top() - height - st::msgMargin.bottom());\n\t\tconst auto position = QPoint(left, top);\n\t\tp.translate(position);\n\t\tmedia->draw(p, context.selected()\n\t\t\t? context.translated(-position)\n\t\t\t: context.translated(-position).withSelection({}));\n\t\tp.translate(-position);\n\t} else {\n\t\tconst auto top = below\n\t\t\t? (g.top() + g.height() - st::msgServiceMargin.top() + st::msgServiceMargin.bottom())\n\t\t\t: (g.top() - height - st::msgMargin.top());\n\t\tp.translate(0, top);\n\n\t\tconst auto rect = QRect(0, 0, width, height)\n\t\t\t- st::msgServiceMargin;\n\t\tconst auto trect = rect - st::msgServicePadding;\n\n\t\tServiceMessagePainter::PaintComplexBubble(\n\t\t\tp,\n\t\t\tcontext.st,\n\t\t\trect.left(),\n\t\t\trect.width(),\n\t\t\ttext,\n\t\t\ttrect);\n\n\t\tp.setBrush(Qt::NoBrush);\n\t\tp.setPen(context.st->msgServiceFg());\n\t\tp.setFont(st::msgServiceFont);\n\t\ttext.draw(p, {\n\t\t\t.position = trect.topLeft(),\n\t\t\t.availableWidth = trect.width(),\n\t\t\t.align = style::al_top,\n\t\t\t.palette = &context.st->serviceTextPalette(),\n\t\t\t.now = context.now,\n\t\t\t.fullWidthSelection = false,\n\t\t\t//.selection = context.selection,\n\t\t});\n\n\t\tp.translate(0, -top);\n\t}\n}\n\nClickHandlerPtr ServicePreMessage::textState(\n\t\tQPoint point,\n\t\tconst StateRequest &request,\n\t\tQRect g) const {\n\tif (media && media->hideServiceText()) {\n\t\tconst auto left = (width - media->width()) / 2;\n\t\tconst auto top = below\n\t\t\t? (g.top() + g.height() - st::msgServiceMargin.top() + st::msgServiceMargin.bottom())\n\t\t\t: (g.top() - height - st::msgMargin.bottom());\n\t\tconst auto position = QPoint(left, top);\n\t\treturn media->textState(point - position, request).link;\n\t}\n\tconst auto top = below\n\t\t? (g.top() + g.height() - st::msgServiceMargin.top() + st::msgServiceMargin.bottom())\n\t\t: (g.top() - height - st::msgMargin.top());\n\tconst auto rect = QRect(0, top, width, height)\n\t\t- st::msgServiceMargin;\n\tconst auto trect = rect - st::msgServicePadding;\n\tif (trect.contains(point)) {\n\t\tauto textRequest = request.forText();\n\t\ttextRequest.align = style::al_center;\n\t\tconst auto link = text.getState(\n\t\t\tpoint - trect.topLeft(),\n\t\t\ttrect.width(),\n\t\t\ttextRequest).link;\n\t\tif (link) {\n\t\t\treturn link;\n\t\t}\n\t}\n\tif (handler && rect.contains(point)) {\n\t\treturn handler;\n\t}\n\treturn {};\n}\n\n\n\nvoid FakeBotAboutTop::init() {\n\tif (!text.isEmpty()) {\n\t\treturn;\n\t}\n\ttext.setText(\n\t\tst::msgNameStyle,\n\t\ttr::lng_bot_description(tr::now),\n\t\tUi::NameTextOptions());\n\tmaxWidth = st::msgPadding.left()\n\t\t+ text.maxWidth()\n\t\t+ st::msgPadding.right();\n\theight = st::msgNameStyle.font->height + st::botDescSkip;\n}\n\nElement::Element(\n\tnot_null<ElementDelegate*> delegate,\n\tnot_null<HistoryItem*> data,\n\tElement *replacing,\n\tFlag serviceFlag)\n: _delegate(delegate)\n, _data(data)\n, _dateTime((IsItemScheduledUntilOnline(data) || data->shortcutId())\n\t? QDateTime()\n\t: ItemDateTime(data))\n, _text(st::msgMinWidth)\n, _flags(serviceFlag\n\t| Flag::NeedsResize\n\t| (IsItemScheduledUntilOnline(data)\n\t\t? Flag::ScheduledUntilOnline\n\t\t: Flag())\n\t| (countIsTopicRootReply() ? Flag::TopicRootReply : Flag()))\n, _context(delegate->elementContext()) {\n\thistory()->owner().registerItemView(this);\n\trefreshMedia(replacing);\n\tif (_context == Context::History) {\n\t\thistory()->setHasPendingResizedItems();\n\t}\n\tif (data->isFakeAboutView()) {\n\t\tconst auto user = data->history()->peer->asUser();\n\t\tif (user\n\t\t\t&& user->isBot()\n\t\t\t&& !user->isRepliesChat()\n\t\t\t&& !user->isVerifyCodes()\n\t\t\t&& !user->botManagerId()) {\n\t\t\tAddComponents(FakeBotAboutTop::Bit());\n\t\t}\n\t}\n}\n\nbool Element::embedReactionsInBubble() const {\n\treturn false;\n}\n\nnot_null<ElementDelegate*> Element::delegate() const {\n\treturn _delegate;\n}\n\nnot_null<HistoryItem*> Element::data() const {\n\treturn _data;\n}\n\nnot_null<History*> Element::history() const {\n\treturn _data->history();\n}\n\nuint8 Element::colorIndex() const {\n\treturn data()->colorIndex();\n}\n\nauto Element::colorCollectible() const\n-> const std::shared_ptr<Ui::ColorCollectible> & {\n\treturn data()->colorCollectible();\n}\n\nuint8 Element::contentColorIndex() const {\n\treturn data()->contentColorIndex();\n}\n\nDocumentId Element::contentBackgroundEmojiId() const {\n\treturn data()->contentBackgroundEmojiId();\n}\n\nauto Element::contentColorCollectible() const\n-> const std::shared_ptr<Ui::ColorCollectible> & {\n\treturn data()->contentColorCollectible();\n}\n\nQDateTime Element::dateTime() const {\n\treturn _dateTime;\n}\n\nMedia *Element::media() const {\n\treturn _media.get();\n}\n\nContext Element::context() const {\n\treturn _context;\n}\n\nint Element::y() const {\n\treturn _y;\n}\n\nvoid Element::setY(int y) {\n\t_y = y;\n}\n\nvoid Element::refreshDataIdHook() {\n}\n\nvoid Element::clearSpecialOnlyEmoji() {\n\tif (!(_flags & Flag::SpecialOnlyEmoji)) {\n\t\treturn;\n\t}\n\thistory()->session().emojiStickersPack().remove(this);\n\t_flags &= ~Flag::SpecialOnlyEmoji;\n}\n\nvoid Element::checkSpecialOnlyEmoji() {\n\tif (history()->session().emojiStickersPack().add(this)) {\n\t\t_flags |= Flag::SpecialOnlyEmoji;\n\t}\n}\n\nvoid Element::hideSpoilers() {\n\tif (_text.hasSpoilers()) {\n\t\t_text.setSpoilerRevealed(false, anim::type::instant);\n\t}\n\tif (_media) {\n\t\t_media->hideSpoilers();\n\t}\n}\n\nvoid Element::customEmojiRepaint() {\n\tif (!(_flags & Flag::CustomEmojiRepainting)) {\n\t\t_flags |= Flag::CustomEmojiRepainting;\n\t\thistory()->owner().requestViewRepaint(this);\n\t}\n}\n\nvoid Element::clearCustomEmojiRepaint() const {\n\t_flags &= ~Flag::CustomEmojiRepainting;\n\tdata()->_flags &= ~MessageFlag::CustomEmojiRepainting;\n}\n\nvoid Element::prepareCustomEmojiPaint(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst Ui::Text::String &text) const {\n\tif (!text.hasPersistentAnimation()) {\n\t\treturn;\n\t}\n\tclearCustomEmojiRepaint();\n\tp.setInactive(context.paused);\n\tif (!(_flags & Flag::HeavyCustomEmoji)) {\n\t\t_flags |= Flag::HeavyCustomEmoji;\n\t\thistory()->owner().registerHeavyViewPart(const_cast<Element*>(this));\n\t}\n}\n\nvoid Element::prepareCustomEmojiPaint(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst Reactions::InlineList &reactions) const {\n\tif (!reactions.hasCustomEmoji()) {\n\t\treturn;\n\t}\n\tclearCustomEmojiRepaint();\n\tp.setInactive(context.paused);\n\tif (!(_flags & Flag::HeavyCustomEmoji)) {\n\t\t_flags |= Flag::HeavyCustomEmoji;\n\t\thistory()->owner().registerHeavyViewPart(const_cast<Element*>(this));\n\t}\n}\n\nvoid Element::repaint(QRect r) const {\n\thistory()->owner().requestViewRepaint(this, r);\n}\n\nvoid Element::paintHighlight(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint geometryHeight) const {\n\tif (context.highlight.opacity == 0.) {\n\t\treturn;\n\t}\n\tconst auto top = marginTop();\n\tconst auto bottom = marginBottom();\n\tconst auto fill = qMin(top, bottom);\n\tconst auto skiptop = top - fill;\n\tconst auto fillheight = fill + geometryHeight + fill;\n\n\tpaintCustomHighlight(p, context, skiptop, fillheight, data());\n}\n\nvoid Element::paintCustomHighlight(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint y,\n\t\tint height,\n\t\tnot_null<const HistoryItem*> item) const {\n\tconst auto o = p.opacity();\n\tp.setOpacity(o * context.highlight.opacity);\n\tp.fillRect(0, y, width(), height, context.st->msgSelectOverlay());\n\tp.setOpacity(o);\n}\n\nbool Element::isUnderCursor() const {\n\treturn _delegate->elementUnderCursor(this);\n}\n\nbool Element::isLastAndSelfMessage() const {\n\tif (!hasOutLayout() || data()->_history->peer->isSelf()) {\n\t\treturn false;\n\t}\n\tif (const auto last = data()->_history->lastMessage()) {\n\t\treturn last == data();\n\t}\n\treturn false;\n}\n\nvoid Element::addVerticalMargins(int top, int bottom) {\n\tif (top || bottom) {\n\t\tAddComponents(ViewAddedMargins::Bit());\n\t\tconst auto margins = Get<ViewAddedMargins>();\n\t\tmargins->top = top;\n\t\tmargins->bottom = bottom;\n\t} else {\n\t\tRemoveComponents(ViewAddedMargins::Bit());\n\t}\n}\n\nvoid Element::setPendingResize() {\n\t_flags |= Flag::NeedsResize;\n\tif (_context == Context::History) {\n\t\tdata()->_history->setHasPendingResizedItems();\n\t}\n}\n\nbool Element::pendingResize() const {\n\treturn _flags & Flag::NeedsResize;\n}\n\nbool Element::isAttachedToPrevious() const {\n\treturn _flags & Flag::AttachedToPrevious;\n}\n\nbool Element::isAttachedToNext() const {\n\treturn _flags & Flag::AttachedToNext;\n}\n\nbool Element::isBubbleAttachedToPrevious() const {\n\treturn _flags & Flag::BubbleAttachedToPrevious;\n}\n\nbool Element::isBubbleAttachedToNext() const {\n\treturn _flags & Flag::BubbleAttachedToNext;\n}\n\nbool Element::isTopicRootReply() const {\n\treturn _flags & Flag::TopicRootReply;\n}\n\nint Element::skipBlockWidth() const {\n\treturn st::msgDateSpace + infoWidth() - st::msgDateDelta.x();\n}\n\nint Element::skipBlockHeight() const {\n\treturn st::msgDateFont->height - st::msgDateDelta.y();\n}\n\nint Element::infoWidth() const {\n\treturn 0;\n}\n\nint Element::bottomInfoFirstLineWidth() const {\n\treturn 0;\n}\n\nbool Element::bottomInfoIsWide() const {\n\treturn false;\n}\n\nbool Element::isHiddenByGroup() const {\n\treturn _flags & Flag::HiddenByGroup;\n}\n\nbool Element::isHidden() const {\n\treturn isHiddenByGroup();\n}\n\nvoid Element::overrideMedia(std::unique_ptr<Media> media) {\n\tExpects(!history()->owner().groups().find(data()));\n\n\t_text = Ui::Text::String(st::msgMinWidth);\n\tinvalidateTextSizeCache();\n\n\t_media = std::move(media);\n\tif (!pendingResize()) {\n\t\thistory()->owner().requestViewResize(this);\n\t}\n\t_flags |= Flag::MediaOverriden;\n}\n\nvoid Element::overrideRightBadge(const QString &text, BadgeRole role) {\n\tif (text.isEmpty()) {\n\t\tif (Has<RightBadge>()) {\n\t\t\tGet<RightBadge>()->overridden = false;\n\t\t\tRemoveComponents(RightBadge::Bit());\n\t\t}\n\t\treturn;\n\t}\n\tif (!Has<RightBadge>()) {\n\t\tAddComponents(RightBadge::Bit());\n\t}\n\tconst auto badge = Get<RightBadge>();\n\tbadge->overridden = true;\n\tbadge->role = role;\n\tbadge->tag.setMarkedText(\n\t\tst::defaultTextStyle,\n\t\t{ text },\n\t\tUi::NameTextOptions());\n\tbadge->boosts.clear();\n\tif (role == BadgeRole::User) {\n\t\tbadge->width = badge->tag.maxWidth();\n\t} else {\n\t\tconst auto &padding = st::msgTagBadgePadding;\n\t\tconst auto tagTextWidth = badge->tag.maxWidth();\n\t\tconst auto contentWidth = padding.left()\n\t\t\t+ tagTextWidth\n\t\t\t+ padding.right();\n\t\tconst auto pillHeight = padding.top()\n\t\t\t+ st::msgFont->height\n\t\t\t+ padding.bottom();\n\t\tbadge->width = std::max(contentWidth, pillHeight);\n\t}\n}\n\nnot_null<PurchasedTag*> Element::enforcePurchasedTag() {\n\tif (const auto purchased = Get<PurchasedTag>()) {\n\t\treturn purchased;\n\t}\n\tAddComponents(PurchasedTag::Bit());\n\treturn Get<PurchasedTag>();\n}\n\nint Element::AdditionalSpaceForSelectionCheckbox(\n\t\tnot_null<const Element*> view,\n\t\tQRect countedGeometry) {\n\tif (!view->hasOutLayout()\n\t\t|| view->delegate()->elementChatMode() == ElementChatMode::Wide) {\n\t\treturn 0;\n\t}\n\tif (countedGeometry.isEmpty()) {\n\t\tcountedGeometry = view->innerGeometry();\n\t}\n\tconst auto diff = view->width()\n\t\t- (countedGeometry.x() + countedGeometry.width())\n\t\t- st::msgPadding.right()\n\t\t- st::msgSelectionOffset\n\t\t- view->rightActionSize().value_or(QSize()).width();\n\treturn (diff < 0)\n\t\t? -(std::min(st::msgSelectionOffset, -diff))\n\t\t: 0;\n}\n\nvoid Element::refreshMedia(Element *replacing) {\n\tif (_flags & Flag::MediaOverriden) {\n\t\treturn;\n\t}\n\t_flags &= ~Flag::HiddenByGroup;\n\n\tconst auto item = data();\n\tif (!item->computeUnavailableReason().isEmpty()) {\n\t\t_media = nullptr;\n\t\treturn;\n\t}\n\tif (const auto media = item->media()) {\n\t\tif (media->canBeGrouped()) {\n\t\t\tif (const auto group = history()->owner().groups().find(item)) {\n\t\t\t\tif (group->items.front() != item) {\n\t\t\t\t\t_media = nullptr;\n\t\t\t\t\t_flags |= Flag::HiddenByGroup;\n\t\t\t\t} else {\n\t\t\t\t\t_media = std::make_unique<GroupedMedia>(\n\t\t\t\t\t\tthis,\n\t\t\t\t\t\tgroup->items);\n\t\t\t\t\tif (!pendingResize()) {\n\t\t\t\t\t\thistory()->owner().requestViewResize(this);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\t_media = media->createView(this, replacing);\n\t} else if (item->showSimilarChannels()) {\n\t\t_media = std::make_unique<SimilarChannels>(this);\n\t} else if (isOnlyCustomEmoji()\n\t\t&& Core::App().settings().largeEmoji()\n\t\t&& !item->isSponsored()) {\n\t\t_media = std::make_unique<UnwrappedMedia>(\n\t\t\tthis,\n\t\t\tstd::make_unique<CustomEmoji>(this, onlyCustomEmoji()));\n\t} else if (isIsolatedEmoji()\n\t\t&& Core::App().settings().largeEmoji()\n\t\t&& !item->isSponsored()) {\n\t\tconst auto emoji = isolatedEmoji();\n\t\tconst auto emojiStickers = &history()->session().emojiStickersPack();\n\t\tconst auto skipPremiumEffect = false;\n\t\tif (const auto sticker = emojiStickers->stickerForEmoji(emoji)) {\n\t\t\tauto content = std::make_unique<Sticker>(\n\t\t\t\tthis,\n\t\t\t\tsticker.document,\n\t\t\t\tskipPremiumEffect,\n\t\t\t\treplacing,\n\t\t\t\tsticker.replacements);\n\t\t\tcontent->setEmojiSticker();\n\t\t\t_media = std::make_unique<UnwrappedMedia>(\n\t\t\t\tthis,\n\t\t\t\tstd::move(content));\n\t\t} else {\n\t\t\t_media = std::make_unique<UnwrappedMedia>(\n\t\t\t\tthis,\n\t\t\t\tstd::make_unique<LargeEmoji>(this, emoji));\n\t\t}\n\t} else if (const auto nfr = item->Get<HistoryServiceNoForwardsRequest>()\n\t\t; nfr && (!nfr->expired || nfr->actionTaken)) {\n\t\t_media = std::make_unique<MediaGeneric>(\n\t\t\tthis,\n\t\t\tGenerateNoForwardsRequestMedia(this, nfr),\n\t\t\tMediaGenericDescriptor{\n\t\t\t\t.maxWidth = st::chatSuggestInfoWidth,\n\t\t\t\t.service = true,\n\t\t\t\t.hideServiceText = true,\n\t\t\t});\n\t} else if (const auto decision = item->Get<HistoryServiceSuggestDecision>()) {\n\t\t_media = std::make_unique<MediaGeneric>(\n\t\t\tthis,\n\t\t\tGenerateSuggestDecisionMedia(this, decision),\n\t\t\tMediaGenericDescriptor{\n\t\t\t\t.maxWidth = st::chatSuggestInfoWidth,\n\t\t\t\t.fullAreaLink = decision->lnk,\n\t\t\t\t.service = true,\n\t\t\t\t.hideServiceText = true,\n\t\t\t});\n\t} else {\n\t\t_media = nullptr;\n\t}\n}\n\nHistoryItem *Element::textItem() const {\n\treturn _textItem;\n}\n\nUi::Text::IsolatedEmoji Element::isolatedEmoji() const {\n\treturn _text.toIsolatedEmoji();\n}\n\nUi::Text::OnlyCustomEmoji Element::onlyCustomEmoji() const {\n\treturn _text.toOnlyCustomEmoji();\n}\n\nvoid Element::skipInactiveTextAppearing() {\n\tif (pendingResize()) {\n\t\t// This message isn't displayed right now,\n\t\t// so we can skip text animation.\n\t\tif (const auto appearing = Get<TextAppearing>()) {\n\t\t\tappearing->widthAnimation.stop();\n\t\t\tappearing->heightAnimation.stop();\n\t\t\tappearing->shownLine = kMaxShownLine;\n\t\t\tappearing->shownWidth\n\t\t\t\t= appearing->shownHeight\n\t\t\t\t= appearing->revealedLineWidth\n\t\t\t\t= 0;\n\t\t\tappearing->geometryValid = false;\n\t\t}\n\t}\n}\n\nconst Ui::Text::String &Element::text() const {\n\treturn _text;\n}\n\nOnlyEmojiAndSpaces Element::isOnlyEmojiAndSpaces() const {\n\tif (data()->Has<HistoryMessageTranslation>()) {\n\t\treturn OnlyEmojiAndSpaces::No;\n\t} else if (!_text.isEmpty()) {\n\t\treturn _text.hasNotEmojiAndSpaces()\n\t\t\t? OnlyEmojiAndSpaces::No\n\t\t\t: OnlyEmojiAndSpaces::Yes;\n\t} else if (data()->originalText().empty()) {\n\t\treturn OnlyEmojiAndSpaces::Yes;\n\t} else {\n\t\treturn OnlyEmojiAndSpaces::Unknown;\n\t}\n}\n\nint Element::textHeightFor(int textWidth) const {\n\tconstexpr auto kMaxWidth = (1 << 16) - 1;\n\tif (textWidth <= 0 || textWidth > kMaxWidth) {\n\t\treturn 0;\n\t}\n\tconst_cast<Element*>(this)->validateText();\n\tif (_textWidth != textWidth) {\n\t\t_textWidth = textWidth;\n\t\tconst auto result = _text.countSize(textWidth);\n\t\t_textRealWidth = std::clamp(result.width(), 0, kMaxWidth);\n\t\t_textHeight = result.height();\n\t}\n\treturn _textHeight;\n}\n\nauto Element::contextDependentServiceText() -> TextWithLinks {\n\tconst auto item = data();\n\tconst auto info = item->Get<HistoryServiceTopicInfo>();\n\tif (!info) {\n\t\treturn {};\n\t}\n\tif (_context == Context::Replies) {\n\t\tif (info->created()) {\n\t\t\treturn { { tr::lng_action_topic_created_inside(tr::now) } };\n\t\t}\n\t\treturn {};\n\t} else if (info->created()) {\n\t\treturn{};\n\t}\n\tconst auto peerId = item->history()->peer->id;\n\tconst auto topicRootId = item->topicRootId();\n\tif (!peerIsChannel(peerId)) {\n\t\treturn {};\n\t}\n\tconst auto from = item->from();\n\tconst auto topicUrl = u\"internal:url:https://t.me/c/%1/%2\"_q\n\t\t.arg(peerToChannel(peerId).bare)\n\t\t.arg(topicRootId.bare);\n\tconst auto fromLink = [&](int index) {\n\t\treturn tr::link(from->name(), index);\n\t};\n\tconst auto placeholderLink = [&] {\n\t\tconst auto linkText = history()->peer->isBot()\n\t\t\t? tr::lng_action_topic_bot_thread(tr::now)\n\t\t\t: tr::lng_action_topic_placeholder(tr::now);\n\t\treturn tr::link(linkText, topicUrl);\n\t};\n\tconst auto wrapTopic = [&](\n\t\t\tconst QString &title,\n\t\t\tstd::optional<DocumentId> iconId) {\n\t\treturn tr::link(\n\t\t\tData::ForumTopicIconWithTitle(\n\t\t\t\ttopicRootId,\n\t\t\t\ticonId.value_or(0),\n\t\t\t\ttitle),\n\t\t\ttopicUrl);\n\t};\n\tconst auto wrapParentTopic = [&] {\n\t\tconst auto forum = history()->asForum();\n\t\tif (!forum || forum->topicDeleted(topicRootId)) {\n\t\t\treturn wrapTopic(\n\t\t\t\ttr::lng_deleted_message(tr::now),\n\t\t\t\tstd::nullopt);\n\t\t} else if (const auto topic = forum->topicFor(topicRootId)) {\n\t\t\treturn wrapTopic(topic->title(), topic->iconId());\n\t\t} else {\n\t\t\tforum->requestTopic(topicRootId, crl::guard(this, [=] {\n\t\t\t\titemTextUpdated();\n\t\t\t\thistory()->owner().requestViewResize(this);\n\t\t\t}));\n\t\t\treturn wrapTopic(\n\t\t\t\ttr::lng_profile_loading(tr::now),\n\t\t\t\tstd::nullopt);\n\t\t}\n\t};\n\n\tif (info->closed) {\n\t\treturn {\n\t\t\ttr::lng_action_topic_closed_by(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLink(1),\n\t\t\t\tlt_topic,\n\t\t\t\twrapParentTopic(),\n\t\t\t\ttr::marked),\n\t\t\t{ from->createOpenLink() },\n\t\t};\n\t} else if (info->reopened) {\n\t\treturn {\n\t\t\ttr::lng_action_topic_reopened_by(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLink(1),\n\t\t\t\tlt_topic,\n\t\t\t\twrapParentTopic(),\n\t\t\t\ttr::marked),\n\t\t\t{ from->createOpenLink() },\n\t\t};\n\t} else if (info->hidden) {\n\t\treturn {\n\t\t\ttr::lng_action_topic_hidden(\n\t\t\t\ttr::now,\n\t\t\t\tlt_topic,\n\t\t\t\twrapParentTopic(),\n\t\t\t\ttr::marked),\n\t\t};\n\t} else if (info->unhidden) {\n\t\treturn {\n\t\t\ttr::lng_action_topic_unhidden(\n\t\t\t\ttr::now,\n\t\t\t\tlt_topic,\n\t\t\t\twrapParentTopic(),\n\t\t\t\ttr::marked),\n\t\t};\n\t} else if (info->renamed) {\n\t\treturn {\n\t\t\ttr::lng_action_topic_renamed(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\tfromLink(1),\n\t\t\t\tlt_link,\n\t\t\t\tplaceholderLink(),\n\t\t\t\tlt_title,\n\t\t\t\twrapTopic(\n\t\t\t\t\tinfo->title,\n\t\t\t\t\t(info->reiconed\n\t\t\t\t\t\t? info->iconId\n\t\t\t\t\t\t: std::optional<DocumentId>())),\n\t\t\t\ttr::marked),\n\t\t\t{ from->createOpenLink() },\n\t\t};\n\t} else if (info->reiconed) {\n\t\tif (const auto iconId = info->iconId) {\n\t\t\treturn {\n\t\t\t\ttr::lng_action_topic_icon_changed(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLink(1),\n\t\t\t\t\tlt_link,\n\t\t\t\t\tplaceholderLink(),\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\tData::SingleCustomEmoji(iconId),\n\t\t\t\t\ttr::marked),\n\t\t\t\t{ from->createOpenLink() },\n\t\t\t};\n\t\t} else {\n\t\t\treturn {\n\t\t\t\ttr::lng_action_topic_icon_removed(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tfromLink(1),\n\t\t\t\t\tlt_link,\n\t\t\t\t\tplaceholderLink(),\n\t\t\t\t\ttr::marked),\n\t\t\t\t{ from->createOpenLink() },\n\t\t\t};\n\t\t}\n\t} else {\n\t\treturn {};\n\t}\n}\n\nvoid Element::validateText() {\n\tconst auto item = data();\n\tconst auto media = item->media();\n\tconst auto storyMention = media && media->storyMention();\n\tconst auto storyExpired = media && media->storyExpired();\n\tconst auto storyUnsupported = media && media->storyUnsupported();\n\tif (storyExpired || storyUnsupported) {\n\t\t_media = nullptr;\n\t\t_textItem = item;\n\t\tif (!storyMention) {\n\t\t\tif (_text.isEmpty()) {\n\t\t\t\tsetTextWithLinks(tr::italic(storyUnsupported\n\t\t\t\t\t? tr::lng_stories_unsupported(tr::now)\n\t\t\t\t\t: tr::lng_forwarded_story_expired(tr::now)));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t}\n\n\t// Albums may show text of a different item than the parent one.\n\t// Media::itemForText may initialize data within the object.\n\t_textItem = _media ? _media->itemForText() : item.get();\n\n\tconst auto &summary = item->summaryEntry();\n\tconst auto summaryShownWas = (_flags & Flag::SummaryShown) != 0;\n\tconst auto summaryShownNow = !summary.result.empty() && summary.shown;\n\tconst auto summaryShownChanged = (summaryShownWas != summaryShownNow);\n\tif (summaryShownNow) {\n\t\t_flags |= Flag::SummaryShown;\n\t\tif (summaryShownChanged) {\n\t\t\tsetTextWithLinks(summary.result);\n\t\t}\n\t\treturn;\n\t} else {\n\t\t_flags &= ~Flag::SummaryShown;\n\t}\n\n\tif (!_textItem) {\n\t\tif (!_text.isEmpty()) {\n\t\t\tsetTextWithLinks({});\n\t\t}\n\t\treturn;\n\t}\n\tconst auto &text = _textItem->_text;\n\tif (!summaryShownChanged && _text.isEmpty() == text.empty()) {\n\t} else if (_flags & Flag::ServiceMessage) {\n\t\tconst auto contextDependentText = contextDependentServiceText();\n\t\tconst auto &markedText = contextDependentText.text.empty()\n\t\t\t? text\n\t\t\t: contextDependentText.text;\n\t\tconst auto &customLinks = contextDependentText.text.empty()\n\t\t\t? _textItem->customTextLinks()\n\t\t\t: contextDependentText.links;\n\t\tsetTextWithLinks(markedText, customLinks);\n\n\t\tif (const auto done = item->Get<HistoryServiceTodoCompletions>()) {\n\t\t\tif (!done->completed.empty() && !done->incompleted.empty()) {\n\t\t\t\tconst auto todoItemId = (done->incompleted.size() == 1)\n\t\t\t\t\t? done->incompleted.front()\n\t\t\t\t\t: 0;\n\t\t\t\tsetServicePreMessage(\n\t\t\t\t\titem->composeTodoIncompleted(done),\n\t\t\t\t\tJumpToMessageClickHandler(\n\t\t\t\t\t\t(done->peerId\n\t\t\t\t\t\t\t? history()->owner().peer(done->peerId)\n\t\t\t\t\t\t\t: history()->peer),\n\t\t\t\t\t\tdone->msgId,\n\t\t\t\t\t\titem->fullId(),\n\t\t\t\t\t\t{ .todoItemId = todoItemId }));\n\t\t\t} else {\n\t\t\t\tsetServicePreMessage({});\n\t\t\t}\n\t\t}\n\t} else {\n\t\tconst auto unavailable = item->computeUnavailableReason();\n\t\tif (!unavailable.isEmpty()) {\n\t\t\tsetTextWithLinks(tr::italic(unavailable));\n\t\t} else {\n\t\t\tsetTextWithLinks(_textItem->translatedTextWithLocalEntities());\n\t\t}\n\t}\n}\n\nvoid Element::setTextWithLinks(\n\t\tconst TextWithEntities &text,\n\t\tconst std::vector<ClickHandlerPtr> &links) {\n\tconst auto context = Core::TextContext({\n\t\t.session = &history()->session(),\n\t\t.repaint = [=] { customEmojiRepaint(); },\n\t});\n\tif (_flags & Flag::ServiceMessage) {\n\t\tconst auto &options = Ui::ItemTextServiceOptions();\n\t\t_text.setMarkedText(st::serviceTextStyle, text, options, context);\n\t\tauto linkIndex = 0;\n\t\tfor (const auto &link : links) {\n\t\t\t// Link indices start with 1.\n\t\t\t_text.setLink(++linkIndex, link);\n\t\t}\n\t} else {\n\t\tconst auto item = data();\n\t\tconst auto &options = Ui::ItemTextOptions(item);\n\t\tclearSpecialOnlyEmoji();\n\t\t_text.setMarkedText(st::messageTextStyle, text, options, context);\n\t\tif (!item->_text.empty() && _text.isEmpty()){\n\t\t\t// If server has allowed some text that we've trim-ed entirely,\n\t\t\t// just replace it with something so that UI won't look buggy.\n\t\t\t_text.setMarkedText(\n\t\t\t\tst::messageTextStyle,\n\t\t\t\t{ u\":-(\"_q },\n\t\t\t\tUi::ItemTextOptions(item));\n\t\t}\n\t\tif (!item->media()) {\n\t\t\tcheckSpecialOnlyEmoji();\n\t\t\trefreshMedia(nullptr);\n\t\t}\n\t}\n\tInitElementTextPart(this, _text);\n\tif (const auto next = _text.nextFormattedDateUpdate()) {\n\t\thistory()->session().data().registerFormattedDateUpdate(next, this);\n\t}\n\tinvalidateTextSizeCache();\n}\n\nvoid Element::validateTextSkipBlock(bool has, int width, int height) {\n\tvalidateText();\n\tif (!has) {\n\t\tif (_text.removeSkipBlock()) {\n\t\t\tinvalidateTextSizeCache();\n\t\t}\n\t} else if (_text.updateSkipBlock(width, height)) {\n\t\tinvalidateTextSizeCache();\n\t}\n}\n\nvoid Element::validateInlineKeyboard(HistoryMessageReplyMarkup *markup) {\n\tif (!markup\n\t\t|| markup->inlineKeyboard\n\t\t|| markup->hiddenBy(data()->media())) {\n\t\treturn;\n\t}\n\tconst auto item = data();\n\t//if (item->hideLinks()) {\n\t//\titem->setHasHiddenLinks(true);\n\t//\treturn;\n\t//}\n\tmarkup->inlineKeyboard = std::make_unique<ReplyKeyboard>(\n\t\titem,\n\t\tstd::make_unique<KeyboardStyle>(\n\t\t\tst::msgBotKbButton,\n\t\t\t[=] { item->history()->owner().requestItemRepaint(item); }));\n}\n\nvoid Element::previousInBlocksChanged() {\n\trecountThreadBarInBlocks();\n\trecountDisplayDateInBlocks();\n\trecountAttachToPreviousInBlocks();\n}\n\nvoid Element::nextInBlocksRemoved() {\n\tsetAttachToNext(false);\n}\n\nbool Element::markSponsoredViewed(int shownFromTop) const {\n\tconst auto sponsoredTextTop = height()\n\t\t- st::msgPadding.bottom()\n\t\t- st::historyViewButtonHeight;\n\treturn shownFromTop >= sponsoredTextTop;\n}\n\nvoid Element::refreshDataId() {\n\tif (const auto media = this->media()) {\n\t\tmedia->refreshParentId(data());\n\t}\n\trefreshDataIdHook();\n}\n\nbool Element::computeIsAttachToPrevious(not_null<Element*> previous) {\n\tconst auto mayBeAttached = [](not_null<Element*> view) {\n\t\tconst auto item = view->data();\n\t\treturn !item->isService()\n\t\t\t&& !item->isEmpty()\n\t\t\t&& !item->isPostHidingAuthor()\n\t\t\t&& (!item->history()->peer->isMegagroup()\n\t\t\t\t|| !view->hasOutLayout()\n\t\t\t\t|| !item->from()->isChannel());\n\t};\n\tconst auto item = data();\n\tif (!Has<DateBadge>()\n\t\t&& !Has<UnreadBar>()\n\t\t&& !Has<ServicePreMessage>()\n\t\t&& !Has<ForumThreadBar>()) {\n\t\tconst auto prev = previous->data();\n\t\tconst auto previousMarkup = prev->inlineReplyMarkup();\n\t\tconst auto possible = (std::abs(prev->date() - item->date())\n\t\t\t\t< kAttachMessageToPreviousSecondsDelta)\n\t\t\t&& mayBeAttached(this)\n\t\t\t&& mayBeAttached(previous)\n\t\t\t&& (!previousMarkup || previousMarkup->hiddenBy(prev->media()))\n\t\t\t&& (item->topicRootId() == prev->topicRootId());\n\t\tif (possible) {\n\t\t\tconst auto forwarded = item->Get<HistoryMessageForwarded>();\n\t\t\tconst auto prevForwarded = prev->Get<HistoryMessageForwarded>();\n\t\t\tconst auto peer = item->history()->peer;\n\t\t\tif (peer->isSelf()\n\t\t\t\t|| peer->isRepliesChat()\n\t\t\t\t|| peer->isVerifyCodes()\n\t\t\t\t|| (forwarded && forwarded->imported)\n\t\t\t\t|| (prevForwarded && prevForwarded->imported)) {\n\t\t\t\treturn IsAttachedToPreviousInSavedMessages(\n\t\t\t\t\tprev,\n\t\t\t\t\tprevForwarded,\n\t\t\t\t\titem,\n\t\t\t\t\tforwarded);\n\t\t\t} else {\n\t\t\t\treturn prev->from() == item->from();\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nClickHandlerPtr Element::fromLink() const {\n\tif (_fromLink) {\n\t\treturn _fromLink;\n\t}\n\tconst auto item = data();\n\tif (const auto from = item->displayFrom()) {\n\t\t_fromLink = std::make_shared<LambdaClickHandler>([=](\n\t\t\t\tClickContext context) {\n\t\t\tif (context.button != Qt::LeftButton) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\tconst auto session = &from->session();\n\t\t\tif (const auto window = ContextOrSessionWindow(my, session)) {\n\t\t\t\twindow->showPeerInfo(from);\n\t\t\t}\n\t\t});\n\t\t_fromLink->setProperty(kPeerLinkPeerIdProperty, from->id.value);\n\t\treturn _fromLink;\n\t}\n\tif (const auto forwarded = item->Get<HistoryMessageForwarded>()) {\n\t\tif (forwarded->imported) {\n\t\t\tstatic const auto imported = std::make_shared<LambdaClickHandler>([](\n\t\t\t\t\tClickContext context) {\n\t\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\t\tconst auto weak = my.sessionWindow;\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->showToast(tr::lng_forwarded_imported(tr::now));\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn imported;\n\t\t}\n\t}\n\t_fromLink = HiddenSenderInfo::ForwardClickHandler();\n\treturn _fromLink;\n}\n\nvoid Element::createUnreadBar(rpl::producer<QString> text) {\n\tif (!AddComponents(UnreadBar::Bit())) {\n\t\treturn;\n\t}\n\tconst auto bar = Get<UnreadBar>();\n\tstd::move(\n\t\ttext\n\t) | rpl::on_next([=](const QString &text) {\n\t\tif (const auto bar = Get<UnreadBar>()) {\n\t\t\tbar->init(text);\n\t\t}\n\t}, bar->lifetime);\n\tif (data()->mainView() == this) {\n\t\trecountAttachToPreviousInBlocks();\n\t}\n\thistory()->owner().requestViewResize(this);\n}\n\nvoid Element::destroyUnreadBar() {\n\tif (!Has<UnreadBar>()) {\n\t\treturn;\n\t}\n\tRemoveComponents(UnreadBar::Bit());\n\tif (data()->mainView() == this) {\n\t\trecountAttachToPreviousInBlocks();\n\t}\n\thistory()->owner().requestViewResize(this);\n}\n\nint Element::displayedDateHeight() const {\n\tif (auto date = Get<DateBadge>()) {\n\t\treturn date->height();\n\t}\n\treturn 0;\n}\n\nbool Element::displayDate() const {\n\treturn Has<DateBadge>();\n}\n\nbool Element::isInOneDayWithPrevious() const {\n\treturn !data()->isEmpty() && !displayDate();\n}\n\nbool Element::displayForumThreadBar() const {\n\treturn Has<ForumThreadBar>();\n}\n\nbool Element::isInOneBunchWithPrevious() const {\n\treturn !data()->isEmpty() && !displayForumThreadBar();\n}\n\nvoid Element::recountAttachToPreviousInBlocks() {\n\tif (isHidden() || data()->isEmpty()) {\n\t\tif (const auto next = nextDisplayedInBlocks()) {\n\t\t\tnext->recountAttachToPreviousInBlocks();\n\t\t} else if (const auto previous = previousDisplayedInBlocks()) {\n\t\t\tprevious->setAttachToNext(false);\n\t\t}\n\t\treturn;\n\t}\n\tauto attachToPrevious = false;\n\tconst auto previous = previousDisplayedInBlocks();\n\tif (previous) {\n\t\tattachToPrevious = computeIsAttachToPrevious(previous);\n\t\tprevious->setAttachToNext(attachToPrevious, this);\n\t}\n\tsetAttachToPrevious(attachToPrevious, previous);\n}\n\nvoid Element::recountThreadBarInBlocks() {\n\tconst auto item = data();\n\tconst auto topic = item->topic();\n\tconst auto sublist = item->savedSublist();\n\tconst auto parentChat = (topic && topic->peer()->useSubsectionTabs())\n\t\t? topic->peer().get()\n\t\t: sublist\n\t\t? sublist->parentChat()\n\t\t: nullptr;\n\tconst auto barThread = [&]() -> Data::Thread* {\n\t\tif (!parentChat\n\t\t\t|| isHidden()\n\t\t\t|| item->isEmpty()\n\t\t\t|| item->isSponsored()) {\n\t\t\treturn nullptr;\n\t\t}\n\t\tif (const auto previous = previousDisplayedInBlocks()) {\n\t\t\tconst auto prev = previous->data();\n\t\t\tif (const auto prevTopic = prev->topic()) {\n\t\t\t\tAssert(prevTopic->peer() == parentChat);\n\t\t\t\tconst auto topicRootId = topic->rootId();\n\t\t\t\tif (prevTopic->rootId() == topicRootId) {\n\t\t\t\t\treturn nullptr;\n\t\t\t\t}\n\t\t\t} else if (const auto prevSublist = prev->savedSublist()) {\n\t\t\t\tAssert(prevSublist->parentChat() == parentChat);\n\t\t\t\tconst auto sublistPeer = sublist->sublistPeer();\n\t\t\t\tif (prevSublist->sublistPeer() == sublistPeer) {\n\t\t\t\t\treturn nullptr;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn topic\n\t\t\t? (Data::Thread*)topic\n\t\t\t: (sublist && sublist->sublistPeer() != parentChat)\n\t\t\t? (Data::Thread*)sublist\n\t\t\t: nullptr;\n\t}();\n\tif (barThread && !Has<ForumThreadBar>()) {\n\t\tAddComponents(ForumThreadBar::Bit());\n\t\tGet<ForumThreadBar>()->init(parentChat, barThread);\n\t} else if (!barThread && Has<ForumThreadBar>()) {\n\t\tRemoveComponents(ForumThreadBar::Bit());\n\t}\n}\n\nvoid Element::recountDisplayDateInBlocks() {\n\tsetDisplayDate([&] {\n\t\tconst auto item = data();\n\t\tif (isHidden() || item->isEmpty()) {\n\t\t\treturn false;\n\t\t}\n\t\tif (item->isSponsored()) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (const auto previous = previousDisplayedInBlocks()) {\n\t\t\tconst auto prev = previous->data();\n\t\t\treturn prev->hideDisplayDate()\n\t\t\t\t|| (previous->dateTime().date() != dateTime().date());\n\t\t}\n\t\treturn true;\n\t}());\n}\n\nQSize Element::countOptimalSize() {\n\t_flags &= ~Flag::NeedsResize;\n\treturn performCountOptimalSize();\n}\n\nQSize Element::countCurrentSize(int newWidth) {\n\tif (_flags & Flag::NeedsResize) {\n\t\tinitDimensions();\n\t}\n\treturn performCountCurrentSize(newWidth);\n}\n\nbool Element::countIsTopicRootReply() const {\n\tconst auto item = data();\n\tif (!item->history()->isForum()) {\n\t\treturn false;\n\t}\n\tconst auto replyTo = item->replyToId();\n\treturn !replyTo || (item->topicRootId() == replyTo);\n}\n\nvoid Element::setDisplayDate(bool displayDate) {\n\tconst auto item = data();\n\tif (item->hideDisplayDate()) {\n\t\tdisplayDate = false;\n\t}\n\tif (displayDate && !Has<DateBadge>()) {\n\t\tAddComponents(DateBadge::Bit());\n\t\tGet<DateBadge>()->init(\n\t\t\tItemDateText(item, (_flags & Flag::ScheduledUntilOnline)));\n\t\tsetPendingResize();\n\t} else if (!displayDate && Has<DateBadge>()) {\n\t\tRemoveComponents(DateBadge::Bit());\n\t\tsetPendingResize();\n\t}\n}\n\nvoid Element::setServicePreMessage(\n\t\tPreparedServiceText text,\n\t\tClickHandlerPtr fullClickHandler,\n\t\tstd::unique_ptr<Media> media) {\n\tif (!text.text.empty() || media) {\n\t\tAddComponents(ServicePreMessage::Bit());\n\t\tconst auto service = Get<ServicePreMessage>();\n\t\tservice->init(\n\t\t\tthis,\n\t\t\tstd::move(text),\n\t\t\tstd::move(fullClickHandler),\n\t\t\tstd::move(media),\n\t\t\tfalse);\n\t\tsetPendingResize();\n\t} else if (Has<ServicePreMessage>()) {\n\t\tRemoveComponents(ServicePreMessage::Bit());\n\t\tsetPendingResize();\n\t}\n}\n\nvoid Element::setServicePostMessage(\n\t\tPreparedServiceText text,\n\t\tClickHandlerPtr fullClickHandler,\n\t\tstd::unique_ptr<Media> media) {\n\tif (!text.text.empty() || media) {\n\t\tAddComponents(ServicePreMessage::Bit());\n\t\tconst auto service = Get<ServicePreMessage>();\n\t\tservice->init(\n\t\t\tthis,\n\t\t\tstd::move(text),\n\t\t\tstd::move(fullClickHandler),\n\t\t\tstd::move(media),\n\t\t\ttrue);\n\t\tsetPendingResize();\n\t} else if (Has<ServicePreMessage>()) {\n\t\tRemoveComponents(ServicePreMessage::Bit());\n\t\tsetPendingResize();\n\t}\n}\n\nvoid Element::setAttachToNext(bool attachToNext, Element *next) {\n\tExpects(next || !attachToNext);\n\n\tauto pending = false;\n\tif (attachToNext && !(_flags & Flag::AttachedToNext)) {\n\t\t_flags |= Flag::AttachedToNext;\n\t\tpending = true;\n\t} else if (!attachToNext && (_flags & Flag::AttachedToNext)) {\n\t\t_flags &= ~Flag::AttachedToNext;\n\t\tpending = true;\n\t}\n\tconst auto bubble = attachToNext && !next->unwrapped();\n\tif (bubble && !(_flags & Flag::BubbleAttachedToNext)) {\n\t\t_flags |= Flag::BubbleAttachedToNext;\n\t\tpending = true;\n\t} else if (!bubble && (_flags & Flag::BubbleAttachedToNext)) {\n\t\t_flags &= ~Flag::BubbleAttachedToNext;\n\t\tpending = true;\n\t}\n\tif (pending) {\n\t\tsetPendingResize();\n\t}\n}\n\nvoid Element::setAttachToPrevious(bool attachToPrevious, Element *previous) {\n\tExpects(previous || !attachToPrevious);\n\n\tauto pending = false;\n\tif (attachToPrevious && !(_flags & Flag::AttachedToPrevious)) {\n\t\t_flags |= Flag::AttachedToPrevious;\n\t\tpending = true;\n\t} else if (!attachToPrevious && (_flags & Flag::AttachedToPrevious)) {\n\t\t_flags &= ~Flag::AttachedToPrevious;\n\t\tpending = true;\n\t}\n\tconst auto bubble = attachToPrevious && !previous->unwrapped();\n\tif (bubble && !(_flags & Flag::BubbleAttachedToPrevious)) {\n\t\t_flags |= Flag::BubbleAttachedToPrevious;\n\t\tpending = true;\n\t} else if (!bubble && (_flags & Flag::BubbleAttachedToPrevious)) {\n\t\t_flags &= ~Flag::BubbleAttachedToPrevious;\n\t\tpending = true;\n\t}\n\tif (pending) {\n\t\tsetPendingResize();\n\t}\n}\n\nbool Element::displayFromPhoto() const {\n\treturn false;\n}\n\nbool Element::hasFromPhoto() const {\n\treturn false;\n}\n\nbool Element::hasFromName() const {\n\treturn false;\n}\n\nbool Element::displayReply() const {\n\treturn Has<Reply>();\n}\n\nbool Element::displayFromName() const {\n\treturn false;\n}\n\nTopicButton *Element::displayedTopicButton() const {\n\treturn nullptr;\n}\n\nbool Element::displayForwardedFrom() const {\n\treturn false;\n}\n\nbool Element::hasOutLayout() const {\n\treturn false;\n}\n\nbool Element::hasRightLayout() const {\n\treturn hasOutLayout()\n\t\t&& (_delegate->elementChatMode() != ElementChatMode::Wide);\n}\n\nbool Element::drawBubble() const {\n\treturn false;\n}\n\nbool Element::hasBubble() const {\n\treturn false;\n}\n\nbool Element::unwrapped() const {\n\treturn true;\n}\n\nstd::optional<QSize> Element::rightActionSize() const {\n\treturn std::nullopt;\n}\n\nvoid Element::drawRightAction(\n\tPainter &p,\n\tconst PaintContext &context,\n\tint left,\n\tint top,\n\tint outerWidth) const {\n}\n\nClickHandlerPtr Element::rightActionLink(\n\t\tstd::optional<QPoint> pressPoint) const {\n\treturn ClickHandlerPtr();\n}\n\nTimeId Element::displayedEditDate() const {\n\treturn TimeId(0);\n}\n\nbool Element::toggleSelectionByHandlerClick(\n\t\tconst ClickHandlerPtr &handler) const {\n\treturn false;\n}\n\nbool Element::allowTextSelectionByHandler(\n\t\tconst ClickHandlerPtr &handler) const {\n\treturn false;\n}\n\nbool Element::usesBubblePattern(const PaintContext &context) const {\n\treturn (context.selection != FullSelection)\n\t\t&& hasOutLayout()\n\t\t&& context.bubblesPattern\n\t\t&& !context.viewport.isEmpty()\n\t\t&& !context.bubblesPattern->pixmap.size().isEmpty();\n}\n\nbool Element::hasVisibleText() const {\n\treturn false;\n}\n\nint Element::textualMaxWidth() const {\n\treturn st::msgPadding.left()\n\t\t+ (hasVisibleText() ? text().maxWidth() : 0)\n\t\t+ st::msgPadding.right();\n}\n\nauto Element::verticalRepaintRange() const -> VerticalRepaintRange {\n\treturn {\n\t\t.top = 0,\n\t\t.height = height()\n\t};\n}\n\nbool Element::hasHeavyPart() const {\n\treturn (_flags & Flag::HeavyCustomEmoji)\n\t\t|| (_media && _media->hasHeavyPart());\n}\n\nvoid Element::checkHeavyPart() {\n\tif (!hasHeavyPart()) {\n\t\thistory()->owner().unregisterHeavyViewPart(this);\n\t}\n}\n\nbool Element::isSignedAuthorElided() const {\n\treturn false;\n}\n\nvoid Element::setupReactions(Element *replacing) {\n\trefreshReactions();\n\tauto animations = replacing\n\t\t? replacing->takeReactionAnimations()\n\t\t: base::flat_map<\n\t\tData::ReactionId,\n\t\tstd::unique_ptr<Ui::ReactionFlyAnimation>>();\n\tif (!animations.empty()) {\n\t\tconst auto repainter = [=] { repaint(); };\n\t\tfor (const auto &[id, animation] : animations) {\n\t\t\tanimation->setRepaintCallback(repainter);\n\t\t}\n\t\tif (_reactions) {\n\t\t\t_reactions->continueAnimations(std::move(animations));\n\t\t}\n\t}\n}\n\nvoid Element::refreshReactions() {\n\tusing namespace Reactions;\n\tauto reactionsData = InlineListDataFromMessage(this);\n\tif (reactionsData.reactions.empty()) {\n\t\tsetReactions(nullptr);\n\t\treturn;\n\t}\n\tif (!_reactions) {\n\t\tconst auto handlerFactory = [=](ReactionId id) {\n\t\t\tconst auto weak = base::make_weak(this);\n\t\t\treturn std::make_shared<LambdaClickHandler>([=](\n\t\t\t\t\tClickContext context) {\n\t\t\t\tconst auto strong = weak.get();\n\t\t\t\tif (!strong) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto item = strong->data();\n\t\t\t\tconst auto controller = ExtractController(context);\n\t\t\t\tif (item->reactionsAreTags()) {\n\t\t\t\t\tif (item->history()->session().premium()) {\n\t\t\t\t\t\tconst auto tag = Data::SearchTagToQuery(id);\n\t\t\t\t\t\tHashtagClickHandler(tag).onClick(context);\n\t\t\t\t\t} else if (controller) {\n\t\t\t\t\t\tShowPremiumPreviewBox(\n\t\t\t\t\t\t\tcontroller,\n\t\t\t\t\t\t\tPremiumFeature::TagsForMessages);\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (id.paid()) {\n\t\t\t\t\tPayments::TryAddingPaidReaction(\n\t\t\t\t\t\titem,\n\t\t\t\t\t\tweak.get(),\n\t\t\t\t\t\t1,\n\t\t\t\t\t\tstd::nullopt,\n\t\t\t\t\t\tcontroller->uiShow());\n\t\t\t\t\treturn;\n\t\t\t\t} else {\n\t\t\t\t\tconst auto source = HistoryReactionSource::Existing;\n\t\t\t\t\titem->toggleReaction(id, source);\n\t\t\t\t}\n\t\t\t\tif (const auto now = weak.get()) {\n\t\t\t\t\tconst auto chosen = now->data()->chosenReactions();\n\t\t\t\t\tif (id.paid() || ranges::contains(chosen, id)) {\n\t\t\t\t\t\tnow->animateReaction({\n\t\t\t\t\t\t\t.id = id,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t};\n\t\tsetReactions(std::make_unique<InlineList>(\n\t\t\t&history()->owner().reactions(),\n\t\t\thandlerFactory,\n\t\t\t[=] { customEmojiRepaint(); },\n\t\t\tstd::move(reactionsData)));\n\t} else {\n\t\tauto was = _reactions->computeTagsList();\n\t\t_reactions->update(std::move(reactionsData), width());\n\t\tauto now = _reactions->computeTagsList();\n\t\tif (!was.empty() || !now.empty()) {\n\t\t\tauto &owner = history()->owner();\n\t\t\towner.viewTagsChanged(this, std::move(was), std::move(now));\n\t\t}\n\t}\n}\n\nvoid Element::setReactions(std::unique_ptr<Reactions::InlineList> list) {\n\tauto was = _reactions\n\t\t? _reactions->computeTagsList()\n\t\t: std::vector<Data::ReactionId>();\n\t_reactions = std::move(list);\n\tauto now = _reactions\n\t\t? _reactions->computeTagsList()\n\t\t: std::vector<Data::ReactionId>();\n\tif (!was.empty() || !now.empty()) {\n\t\tauto &owner = history()->owner();\n\t\towner.viewTagsChanged(this, std::move(was), std::move(now));\n\t}\n}\n\nbool Element::updateReactions() {\n\tconst auto wasReactions = _reactions\n\t\t? _reactions->currentSize()\n\t\t: QSize();\n\trefreshReactions();\n\tconst auto nowReactions = _reactions\n\t\t? _reactions->currentSize()\n\t\t: QSize();\n\treturn (wasReactions != nowReactions);\n}\n\nvoid Element::itemDataChanged() {\n\tif (updateReactions()) {\n\t\thistory()->owner().requestViewResize(this);\n\t} else {\n\t\trepaint();\n\t}\n}\n\nvoid Element::itemTextUpdated() {\n\tif (const auto media = _media.get()) {\n\t\tmedia->parentTextUpdated();\n\t}\n\t_flags &= ~Flag::SummaryShown;\n\tclearSpecialOnlyEmoji();\n\t_text = Ui::Text::String(st::msgMinWidth);\n\tinvalidateTextSizeCache();\n\tif (_media && !data()->media()) {\n\t\trefreshMedia(nullptr);\n\t}\n}\n\nvoid Element::blockquoteExpandChanged() {\n\tinvalidateTextSizeCache();\n\thistory()->owner().requestViewResize(this);\n}\n\nvoid Element::invalidateTextSizeCache() {\n\t_textWidth = 0;\n\t_textHeight = 0;\n\t_textRealWidth = 0;\n\tinvalidateTextDependentCache();\n}\n\nvoid Element::unloadHeavyPart() {\n\thistory()->owner().unregisterHeavyViewPart(this);\n\tif (_reactions) {\n\t\t_reactions->unloadCustomEmoji();\n\t}\n\tif (_media) {\n\t\t_media->unloadHeavyPart();\n\t}\n\tif (_flags & Flag::HeavyCustomEmoji) {\n\t\t_flags &= ~Flag::HeavyCustomEmoji;\n\t\t_text.unloadPersistentAnimation();\n\t\tif (const auto reply = Get<Reply>()) {\n\t\t\treply->unloadPersistentAnimation();\n\t\t}\n\t}\n}\n\nHistoryBlock *Element::block() {\n\treturn _block;\n}\n\nconst HistoryBlock *Element::block() const {\n\treturn _block;\n}\n\nvoid Element::attachToBlock(not_null<HistoryBlock*> block, int index) {\n\tExpects(_data->isHistoryEntry());\n\tExpects(_block == nullptr);\n\tExpects(_indexInBlock < 0);\n\tExpects(index >= 0);\n\n\t_block = block;\n\t_indexInBlock = index;\n\t_data->setMainView(this);\n\tpreviousInBlocksChanged();\n}\n\nvoid Element::removeFromBlock() {\n\tExpects(_block != nullptr);\n\n\t_block->remove(this);\n}\n\nvoid Element::refreshInBlock() {\n\tExpects(_block != nullptr);\n\n\t_block->refreshView(this);\n}\n\nvoid Element::setIndexInBlock(int index) {\n\tExpects(_block != nullptr);\n\tExpects(index >= 0);\n\n\t_indexInBlock = index;\n}\n\nint Element::indexInBlock() const {\n\tExpects((_indexInBlock >= 0) == (_block != nullptr));\n\tExpects((_block == nullptr) || (_block->messages[_indexInBlock].get() == this));\n\n\treturn _indexInBlock;\n}\n\nElement *Element::previousInBlocks() const {\n\tif (_block && _indexInBlock >= 0) {\n\t\tif (_indexInBlock > 0) {\n\t\t\treturn _block->messages[_indexInBlock - 1].get();\n\t\t}\n\t\tif (auto previous = _block->previousBlock()) {\n\t\t\tAssert(!previous->messages.empty());\n\t\t\treturn previous->messages.back().get();\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nElement *Element::previousDisplayedInBlocks() const {\n\tauto result = previousInBlocks();\n\twhile (result && (result->data()->isEmpty() || result->isHidden())) {\n\t\tresult = result->previousInBlocks();\n\t}\n\treturn result;\n}\n\nElement *Element::nextInBlocks() const {\n\tif (_block && _indexInBlock >= 0) {\n\t\tif (_indexInBlock + 1 < _block->messages.size()) {\n\t\t\treturn _block->messages[_indexInBlock + 1].get();\n\t\t}\n\t\tif (auto next = _block->nextBlock()) {\n\t\t\tAssert(!next->messages.empty());\n\t\t\treturn next->messages.front().get();\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nElement *Element::nextDisplayedInBlocks() const {\n\tauto result = nextInBlocks();\n\twhile (result && (result->data()->isEmpty() || result->isHidden())) {\n\t\tresult = result->nextInBlocks();\n\t}\n\treturn result;\n}\n\nvoid Element::drawInfo(\n\tPainter &p,\n\tconst PaintContext &context,\n\tint right,\n\tint bottom,\n\tint width,\n\tInfoDisplayType type) const {\n}\n\nTextState Element::bottomInfoTextState(\n\t\tint right,\n\t\tint bottom,\n\t\tQPoint point,\n\t\tInfoDisplayType type) const {\n\treturn TextState();\n}\n\nTextSelection Element::adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const {\n\treturn selection;\n}\n\nSelectedQuote Element::FindSelectedQuote(\n\t\tconst Ui::Text::String &text,\n\t\tTextSelection selection,\n\t\tnot_null<HistoryItem*> item) {\n\tif (selection.to > text.length()) {\n\t\treturn {};\n\t}\n\tauto modified = selection;\n\tfor (const auto &modification : text.modifications()) {\n\t\tif (modification.position >= selection.to) {\n\t\t\tbreak;\n\t\t} else if (modification.position < selection.from) {\n\t\t\tmodified.from += modification.skipped;\n\t\t\tif (modification.added) {\n\t\t\t\tmodified.from = uint16(std::max(\n\t\t\t\t\t0,\n\t\t\t\t\tint(modified.from) - int(modification.added)));\n\t\t\t}\n\t\t} else if (modification.position == selection.from) {\n\t\t\tif (!modification.added) {\n\t\t\t\tmodified.from += modification.skipped;\n\t\t\t}\n\t\t}\n\t\tmodified.to += modification.skipped;\n\t\tif (modification.added && modified.to > modified.from) {\n\t\t\tmodified.to = uint16(std::max(\n\t\t\t\tint(modified.from),\n\t\t\t\tint(modified.to) - int(modification.added)));\n\t\t}\n\t}\n\tauto result = item->originalText();\n\tif (modified.empty() || modified.to > result.text.size()) {\n\t\treturn {};\n\t}\n\tconst auto session = &item->history()->session();\n\tconst auto limit = session->appConfig().quoteLengthMax();\n\tconst auto overflown = (modified.from + limit < modified.to);\n\tif (overflown) {\n\t\tmodified.to = modified.from + limit;\n\t}\n\tresult.text = result.text.mid(\n\t\tmodified.from,\n\t\tmodified.to - modified.from);\n\tconst auto allowed = std::array{\n\t\tEntityType::Bold,\n\t\tEntityType::Italic,\n\t\tEntityType::Underline,\n\t\tEntityType::StrikeOut,\n\t\tEntityType::Spoiler,\n\t\tEntityType::CustomEmoji,\n\t\tEntityType::FormattedDate,\n\t};\n\tfor (auto i = result.entities.begin(); i != result.entities.end();) {\n\t\tconst auto offset = i->offset();\n\t\tconst auto till = offset + i->length();\n\t\tif ((till <= modified.from)\n\t\t\t|| (offset >= modified.to)\n\t\t\t|| !ranges::contains(allowed, i->type())) {\n\t\t\ti = result.entities.erase(i);\n\t\t} else {\n\t\t\tif (till > modified.to) {\n\t\t\t\ti->shrinkFromRight(till - modified.to);\n\t\t\t}\n\t\t\ti->shiftLeft(modified.from);\n\t\t\t++i;\n\t\t}\n\t}\n\treturn { item, { result, modified.from }, overflown };\n}\n\nTextSelection Element::FindSelectionFromQuote(\n\t\tconst Ui::Text::String &text,\n\t\tconst SelectedQuote &quote) {\n\tExpects(quote.item != nullptr);\n\n\tconst auto &rich = quote.highlight.quote;\n\tif (rich.empty()) {\n\t\treturn {};\n\t}\n\tconst auto &original = quote.item->originalText();\n\tif (quote.highlight.quoteOffset == kSearchQueryOffsetHint) {\n\t\treturn ApplyModificationsFrom(\n\t\t\tFindSearchQueryHighlight(original.text, rich.text),\n\t\t\ttext);\n\t}\n\tconst auto length = int(original.text.size());\n\tconst auto qlength = int(rich.text.size());\n\tconst auto checkAt = [&](int offset) {\n\t\treturn TextSelection{\n\t\t\tuint16(offset),\n\t\t\tuint16(offset + qlength),\n\t\t};\n\t};\n\tconst auto findOneAfter = [&](int offset) {\n\t\tif (offset > length - qlength) {\n\t\t\treturn TextSelection();\n\t\t}\n\t\tconst auto i = original.text.indexOf(rich.text, offset);\n\t\treturn (i >= 0) ? checkAt(i) : TextSelection();\n\t};\n\tconst auto findOneBefore = [&](int offset) {\n\t\tif (!offset) {\n\t\t\treturn TextSelection();\n\t\t}\n\t\tconst auto end = std::min(offset + qlength - 1, length);\n\t\tconst auto from = end - length - 1;\n\t\tconst auto i = original.text.lastIndexOf(rich.text, from);\n\t\treturn (i >= 0) ? checkAt(i) : TextSelection();\n\t};\n\tconst auto findAfter = [&](int offset) {\n\t\twhile (true) {\n\t\t\tconst auto result = findOneAfter(offset);\n\t\t\tif (!result.empty() || result == TextSelection()) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\toffset = result.from;\n\t\t}\n\t};\n\tconst auto findBefore = [&](int offset) {\n\t\twhile (true) {\n\t\t\tconst auto result = findOneBefore(offset);\n\t\t\tif (!result.empty() || result == TextSelection()) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\toffset = result.from - 2;\n\t\t\tif (offset < 0) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t};\n\tconst auto findTwoWays = [&](int offset) {\n\t\tconst auto after = findAfter(offset);\n\t\tif (after.empty()) {\n\t\t\treturn findBefore(offset);\n\t\t} else if (after.from == offset) {\n\t\t\treturn after;\n\t\t}\n\t\tconst auto before = findBefore(offset);\n\t\treturn before.empty()\n\t\t\t? after\n\t\t\t: (offset - before.from < after.from - offset)\n\t\t\t? before\n\t\t\t: after;\n\t};\n\tauto result = findTwoWays(quote.highlight.quoteOffset);\n\tif (result.empty()) {\n\t\treturn {};\n\t}\n\treturn ApplyModificationsFrom(result, text);\n}\n\nReactions::ButtonParameters Element::reactionButtonParameters(\n\t\tQPoint position,\n\t\tconst TextState &reactionState) const {\n\treturn {};\n}\n\nReplyButton::ButtonParameters Element::replyButtonParameters(\n\t\tQPoint position,\n\t\tconst TextState &replyState) const {\n\treturn {};\n}\n\nint Element::reactionsOptimalWidth() const {\n\treturn 0;\n}\n\nvoid Element::clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool active) {\n\tif (const auto markup = _data->Get<HistoryMessageReplyMarkup>()) {\n\t\tif (const auto keyboard = markup->inlineKeyboard.get()) {\n\t\t\tkeyboard->clickHandlerActiveChanged(handler, active);\n\t\t}\n\t}\n\tHoveredLink(active ? this : nullptr);\n\trepaint();\n\tif (const auto media = this->media()) {\n\t\tmedia->clickHandlerActiveChanged(handler, active);\n\t}\n}\n\nvoid Element::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) {\n\tPressedLink(pressed ? this : nullptr);\n\trepaint();\n\tif (const auto media = this->media()) {\n\t\tmedia->clickHandlerPressedChanged(handler, pressed);\n\t}\n}\n\nvoid Element::animateUnreadReactions() {\n\tconst auto &recent = data()->recentReactions();\n\tfor (const auto &[id, list] : recent) {\n\t\tif (ranges::contains(list, true, &Data::RecentReaction::unread)) {\n\t\t\tanimateReaction({ .id = id });\n\t\t}\n\t}\n}\n\nauto Element::takeReactionAnimations()\n-> base::flat_map<\n\t\tData::ReactionId,\n\t\tstd::unique_ptr<Ui::ReactionFlyAnimation>> {\n\tif (_reactions) {\n\t\treturn _reactions->takeAnimations();\n\t}\n\treturn {};\n}\n\nvoid Element::animateUnreadEffect() {\n}\n\nauto Element::takeEffectAnimation()\n-> std::unique_ptr<Ui::ReactionFlyAnimation> {\n\treturn nullptr;\n}\n\nQRect Element::effectIconGeometry() const {\n\treturn QRect();\n}\n\nQPoint Element::mediaTopLeft() const {\n\treturn innerGeometry().topLeft();\n}\n\nElement::~Element() {\n\tsetReactions(nullptr);\n\n\t// Delete media while owner still exists.\n\tclearSpecialOnlyEmoji();\n\tbase::take(_media);\n\tif (_flags & Flag::HeavyCustomEmoji) {\n\t\t_flags &= ~Flag::HeavyCustomEmoji;\n\t\t_text.unloadPersistentAnimation();\n\t\tcheckHeavyPart();\n\t}\n\tif (_data->mainView() == this) {\n\t\t_data->clearMainView();\n\t}\n\tif (_context == Context::History) {\n\t\thistory()->owner().notifyViewRemoved(this);\n\t}\n\thistory()->owner().unregisterItemView(this);\n}\n\nvoid Element::Hovered(Element *view) {\n\tHoveredElement = view;\n}\n\nElement *Element::Hovered() {\n\treturn HoveredElement;\n}\n\nvoid Element::Pressed(Element *view) {\n\tPressedElement = view;\n}\n\nElement *Element::Pressed() {\n\treturn PressedElement;\n}\n\nvoid Element::HoveredLink(Element *view) {\n\tHoveredLinkElement = view;\n}\n\nElement *Element::HoveredLink() {\n\treturn HoveredLinkElement;\n}\n\nvoid Element::PressedLink(Element *view) {\n\tPressedLinkElement = view;\n}\n\nElement *Element::PressedLink() {\n\treturn PressedLinkElement;\n}\n\nvoid Element::Moused(Element *view) {\n\tMousedElement = view;\n}\n\nElement *Element::Moused() {\n\treturn MousedElement;\n}\n\nvoid Element::ClearGlobal() {\n\tHoveredElement = nullptr;\n\tPressedElement = nullptr;\n\tHoveredLinkElement = nullptr;\n\tPressedLinkElement = nullptr;\n\tMousedElement = nullptr;\n}\n\nint FindViewY(not_null<Element*> view, uint16 symbol, int yfrom) {\n\tauto request = HistoryView::StateRequest();\n\trequest.flags = Ui::Text::StateRequest::Flag::LookupSymbol;\n\tconst auto single = st::messageTextStyle.font->height;\n\tconst auto inner = view->innerGeometry();\n\tconst auto origin = inner.topLeft();\n\tconst auto top = 0;\n\tconst auto bottom = view->height();\n\tif (origin.y() < top\n\t\t|| origin.y() + inner.height() > bottom\n\t\t|| inner.height() <= 0) {\n\t\treturn yfrom;\n\t}\n\tconst auto fory = [&](int y) {\n\t\treturn view->textState(origin + QPoint(0, y), request).symbol;\n\t};\n\tyfrom = std::max(yfrom - origin.y(), 0);\n\tauto ytill = inner.height() - 1;\n\tauto symbolfrom = fory(yfrom);\n\tauto symboltill = fory(ytill);\n\tif ((yfrom >= ytill) || (symbolfrom >= symbol)) {\n\t\treturn origin.y() + yfrom;\n\t} else if (symboltill <= symbol) {\n\t\treturn origin.y() + ytill;\n\t}\n\twhile (ytill - yfrom >= 2 * single) {\n\t\tconst auto middle = (yfrom + ytill) / 2;\n\t\tconst auto found = fory(middle);\n\t\tif (found == symbol\n\t\t\t|| symbolfrom > found\n\t\t\t|| symboltill < found) {\n\t\t\treturn middle;\n\t\t} else if (found < symbol) {\n\t\t\tyfrom = middle;\n\t\t\tsymbolfrom = found;\n\t\t} else {\n\t\t\tytill = middle;\n\t\t\tsymboltill = found;\n\t\t}\n\t}\n\treturn origin.y() + (yfrom + ytill) / 2;\n}\n\nint FindViewTaskY(not_null<Element*> view, int taskId, int yfrom) {\n\tauto request = HistoryView::StateRequest();\n\trequest.flags = Ui::Text::StateRequest::Flag::LookupLink;\n\tconst auto single = st::messageTextStyle.font->height;\n\tconst auto inner = view->innerGeometry();\n\tconst auto origin = inner.topLeft();\n\tconst auto top = 0;\n\tconst auto bottom = view->height();\n\tif (origin.y() < top\n\t\t|| origin.y() + inner.height() > bottom\n\t\t|| inner.height() <= 0) {\n\t\treturn yfrom;\n\t}\n\tconst auto media = view->data()->media();\n\tconst auto todolist = media ? media->todolist() : nullptr;\n\tif (!todolist) {\n\t\treturn yfrom;\n\t}\n\tconst auto &items = todolist->items;\n\tconst auto indexOf = [&](int id) -> int {\n\t\treturn ranges::find(items, id, &TodoListItem::id) - begin(items);\n\t};\n\tconst auto index = indexOf(taskId);\n\tconst auto count = int(items.size());\n\tif (index == count) {\n\t\treturn yfrom;\n\t}\n\tyfrom = std::max(yfrom - origin.y(), 0);\n\tauto ytill = inner.height() - 1;\n\tconst auto middle = (yfrom + ytill) / 2;\n\tconst auto fory = [&](int y) {\n\t\tconst auto state = view->textState(origin + QPoint(0, y), request);\n\t\tconst auto &link = state.link;\n\t\tconst auto id = link\n\t\t\t? link->property(kTodoListItemIdProperty).toInt()\n\t\t\t: -1;\n\t\tconst auto index = (id >= 0) ? indexOf(id) : int(items.size());\n\t\treturn (index < count) ? index : (y < middle) ? -1 : count;\n\t};\n\tauto indexfrom = fory(yfrom);\n\tauto indextill = fory(ytill);\n\tif ((yfrom >= ytill) || (indexfrom >= index)) {\n\t\treturn origin.y() + yfrom;\n\t} else if (indextill <= index) {\n\t\treturn origin.y() + ytill;\n\t}\n\twhile (ytill - yfrom >= 2 * single) {\n\t\tconst auto middle = (yfrom + ytill) / 2;\n\t\tconst auto found = fory(middle);\n\t\tif (found == index\n\t\t\t|| indexfrom > found\n\t\t\t|| indextill < found) {\n\t\t\treturn origin.y() + middle;\n\t\t} else if (found < index) {\n\t\t\tyfrom = middle;\n\t\t\tindexfrom = found;\n\t\t} else {\n\t\t\tytill = middle;\n\t\t\tindextill = found;\n\t\t}\n\t}\n\treturn origin.y() + (yfrom + ytill) / 2;\n}\n\nint FindViewPollOptionY(\n\t\tnot_null<Element*> view,\n\t\tconst QByteArray &option,\n\t\tint yfrom) {\n\tauto request = HistoryView::StateRequest();\n\trequest.flags = Ui::Text::StateRequest::Flag::LookupLink;\n\tconst auto single = st::messageTextStyle.font->height;\n\tconst auto inner = view->innerGeometry();\n\tconst auto origin = inner.topLeft();\n\tif (origin.y() < 0\n\t\t|| origin.y() + inner.height() > view->height()\n\t\t|| inner.height() <= 0) {\n\t\treturn yfrom;\n\t}\n\tconst auto media = view->data()->media();\n\tconst auto poll = media ? media->poll() : nullptr;\n\tif (!poll) {\n\t\treturn yfrom;\n\t}\n\tconst auto &answers = poll->answers;\n\tconst auto indexOf = [&](const QByteArray &opt) -> int {\n\t\treturn int(ranges::find(\n\t\t\tanswers, opt, &PollAnswer::option) - begin(answers));\n\t};\n\tconst auto index = indexOf(option);\n\tconst auto count = int(answers.size());\n\tif (index == count) {\n\t\treturn yfrom;\n\t}\n\tyfrom = std::max(yfrom - origin.y(), 0);\n\tauto ytill = inner.height() - 1;\n\tconst auto middle = (yfrom + ytill) / 2;\n\tconst auto fory = [&](int y) {\n\t\tconst auto state = view->textState(origin + QPoint(0, y), request);\n\t\tconst auto &link = state.link;\n\t\tconst auto opt = link\n\t\t\t? link->property(kPollOptionProperty).toByteArray()\n\t\t\t: QByteArray();\n\t\tconst auto idx = !opt.isEmpty() ? indexOf(opt) : count;\n\t\treturn (idx < count) ? idx : (y < middle) ? -1 : count;\n\t};\n\tauto indexfrom = fory(yfrom);\n\tauto indextill = fory(ytill);\n\tif ((yfrom >= ytill) || (indexfrom >= index)) {\n\t\treturn origin.y() + yfrom;\n\t} else if (indextill <= index) {\n\t\treturn origin.y() + ytill;\n\t}\n\twhile (ytill - yfrom >= 2 * single) {\n\t\tconst auto mid = (yfrom + ytill) / 2;\n\t\tconst auto found = fory(mid);\n\t\tif (found == index\n\t\t\t|| indexfrom > found\n\t\t\t|| indextill < found) {\n\t\t\treturn origin.y() + mid;\n\t\t} else if (found < index) {\n\t\t\tyfrom = mid;\n\t\t\tindexfrom = found;\n\t\t} else {\n\t\t\tytill = mid;\n\t\t\tindextill = found;\n\t\t}\n\t}\n\treturn origin.y() + (yfrom + ytill) / 2;\n}\n\nHighlightYRange FindHighlightYRange(\n\t\tnot_null<Element*> view,\n\t\tconst Ui::ChatPaintHighlight &highlight) {\n\tconst auto sel = highlight.range;\n\tconst auto single = st::messageTextStyle.font->height;\n\tif (IsSubGroupSelection(sel)) {\n\t\tconst auto index = FirstGroupItemIndex(sel);\n\t\tif (index < 0) {\n\t\t\treturn {};\n\t\t}\n\t\tconst auto media = view->media();\n\t\tif (!media) {\n\t\t\treturn {};\n\t\t}\n\t\tconst auto rect = media->groupItemRect(index);\n\t\tif (rect.isEmpty()) {\n\t\t\treturn {};\n\t\t}\n\t\tconst auto inner = view->innerGeometry();\n\t\treturn {\n\t\t\tinner.y() + rect.y() - 2 * single,\n\t\t\tinner.y() + rect.y() + rect.height() + 2 * single,\n\t\t};\n\t}\n\tif (highlight.todoItemId) {\n\t\tconst auto y = FindViewTaskY(view, highlight.todoItemId);\n\t\treturn { y - 4 * single, y + 4 * single };\n\t}\n\tif (!highlight.pollOption.isEmpty()) {\n\t\tconst auto y = FindViewPollOptionY(view, highlight.pollOption);\n\t\treturn { y - 4 * single, y + 4 * single };\n\t}\n\tif (!sel.empty()) {\n\t\tconst auto begin = FindViewY(view, sel.from) - single;\n\t\tconst auto end = FindViewY(view, sel.to, begin + single)\n\t\t\t+ 2 * single;\n\t\treturn { begin, end };\n\t}\n\treturn {};\n}\n\nint AdjustScrollForRange(\n\t\tint viewTop,\n\t\tint available,\n\t\tHighlightYRange range) {\n\tauto result = viewTop;\n\tif (range.end > available) {\n\t\tresult = std::max(result, viewTop + range.end - available);\n\t}\n\tif (viewTop + range.begin < result) {\n\t\tresult = viewTop + range.begin;\n\t}\n\treturn result;\n}\n\nWindow::SessionController *ExtractController(const ClickContext &context) {\n\tconst auto my = context.other.value<ClickHandlerContext>();\n\tif (const auto controller = my.sessionWindow.get()) {\n\t\treturn controller;\n\t}\n\treturn nullptr;\n}\n\nTextSelection FindSearchQueryHighlight(\n\t\tconst QString &text,\n\t\tconst QString &query) {\n\tconst auto lower = query.toLower();\n\treturn FindSearchQueryHighlight(text, QStringView(lower));\n}\n\nTextSelection FindSearchQueryHighlight(\n\t\tconst QString &text,\n\t\tQStringView lower) {\n\tconst auto inside = text.toLower();\n\tconst auto find = [&](QStringView part) {\n\t\tauto skip = 0;\n\t\tif (const auto from = inside.indexOf(part, skip); from >= 0) {\n\t\t\tif (!from || !inside[from - 1].isLetterOrNumber()) {\n\t\t\t\treturn int(from);\n\t\t\t}\n\t\t\tskip = from + 1;\n\t\t}\n\t\treturn -1;\n\t};\n\tif (const auto from = find(lower); from >= 0) {\n\t\tconst auto till = from + lower.size();\n\t\tif (till >= inside.size()\n\t\t\t|| !(inside.begin() + till)->isLetterOrNumber()) {\n\t\t\treturn { uint16(from), uint16(till) };\n\t\t}\n\t}\n\tconst auto tillEndOfWord = [&](int from) {\n\t\tfor (auto till = from + 1; till != inside.size(); ++till) {\n\t\t\tif (!inside[till].isLetterOrNumber()) {\n\t\t\t\treturn TextSelection{ uint16(from), uint16(till) };\n\t\t\t}\n\t\t}\n\t\treturn TextSelection{ uint16(from), uint16(inside.size()) };\n\t};\n\tconst auto words = Ui::Text::Words(lower);\n\tfor (const auto &word : words) {\n\t\tconst auto length = int(word.size());\n\t\tconst auto cut = length / 2;\n\t\tconst auto part = word.mid(0, length - cut);\n\t\tconst auto offset = find(part);\n\t\tif (offset < 0) {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (auto i = 0; i != cut; ++i) {\n\t\t\tconst auto part = word.mid(0, length - i);\n\t\t\tif (const auto from = find(part); from >= 0) {\n\t\t\t\treturn tillEndOfWord(from);\n\t\t\t}\n\t\t}\n\t\treturn tillEndOfWord(offset);\n\t}\n\treturn {};\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_element.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/history_view_object.h\"\n#include \"base/runtime_composer.h\"\n#include \"base/flags.h\"\n#include \"base/weak_ptr.h\"\n#include \"ui/userpic_view.h\"\n\nclass History;\nclass HistoryBlock;\nclass HistoryItem;\nstruct HistoryMessageReply;\nstruct PreparedServiceText;\nstruct HistoryMessageReplyMarkup;\n\nnamespace Data {\nclass Thread;\nstruct Reaction;\nstruct ReactionId;\n} // namespace Data\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass PathShiftGradient;\nstruct BubblePattern;\nstruct ChatPaintContext;\nstruct ChatPaintHighlight;\nclass ChatStyle;\nstruct ReactionFlyAnimationArgs;\nclass ReactionFlyAnimation;\nclass RippleAnimation;\nstruct ColorCollectible;\n} // namespace Ui\n\nnamespace HistoryView::Reactions {\nstruct ButtonParameters;\nclass InlineList;\n} // namespace HistoryView::Reactions\n\nnamespace HistoryView::ReplyButton {\nstruct ButtonParameters;\n} // namespace HistoryView::ReplyButton\n\nnamespace HistoryView {\n\nusing PaintContext = Ui::ChatPaintContext;\nenum class BadgeRole : uchar;\nenum class PointState : char;\nenum class InfoDisplayType : char;\nstruct StateRequest;\nstruct TextState;\nclass Media;\nclass Reply;\n\nenum class Context : char {\n\tHistory,\n\tReplies,\n\tPinned,\n\tAdminLog,\n\tContactPreview,\n\tMonoforum,\n\tSavedSublist,\n\tTTLViewer,\n\tShortcutMessages,\n\tScheduledTopic,\n\tChatPreview,\n};\n\nenum class OnlyEmojiAndSpaces : char {\n\tUnknown,\n\tYes,\n\tNo,\n};\n\nstruct SelectionModeResult {\n\tbool inSelectionMode = false;\n\tfloat64 progress = 0.0;\n};\n\nenum class ElementChatMode : char {\n\tDefault,\n\tWide,\n\tNarrow, // monoforum with left tabs\n};\n\nclass Element;\nclass ElementDelegate {\npublic:\n\tvirtual Context elementContext() = 0;\n\tvirtual bool elementUnderCursor(not_null<const Element*> view) = 0;\n\tvirtual SelectionModeResult elementInSelectionMode(\n\t\tconst Element *view) = 0;\n\tvirtual bool elementIntersectsRange(\n\t\tnot_null<const Element*> view,\n\t\tint from,\n\t\tint till) = 0;\n\tvirtual void elementStartStickerLoop(not_null<const Element*> view) = 0;\n\tvirtual void elementShowPollResults(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context) = 0;\n\tvirtual void elementShowAddPollOption(\n\t\tnot_null<Element*> view,\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context,\n\t\tQRect optionRect) = 0;\n\tvirtual void elementSubmitAddPollOption(FullMsgId context) = 0;\n\tvirtual void elementOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) = 0;\n\tvirtual void elementOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView = false) = 0;\n\tvirtual void elementCancelUpload(const FullMsgId &context) = 0;\n\tvirtual void elementShowTooltip(\n\t\tconst TextWithEntities &text,\n\t\tFn<void()> hiddenCallback) = 0;\n\tvirtual bool elementAnimationsPaused() = 0;\n\tvirtual bool elementHideReply(not_null<const Element*> view) = 0;\n\tvirtual bool elementShownUnread(not_null<const Element*> view) = 0;\n\tvirtual void elementSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) = 0;\n\tvirtual void elementSearchInList(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) = 0;\n\tvirtual void elementHandleViaClick(not_null<UserData*> bot) = 0;\n\tvirtual ElementChatMode elementChatMode() = 0;\n\tvirtual not_null<Ui::PathShiftGradient*> elementPathShiftGradient() = 0;\n\tvirtual void elementReplyTo(const FullReplyTo &to) = 0;\n\tvirtual void elementStartInteraction(not_null<const Element*> view) = 0;\n\tvirtual void elementStartPremium(\n\t\tnot_null<const Element*> view,\n\t\tElement *replacing) = 0;\n\tvirtual void elementCancelPremium(not_null<const Element*> view) = 0;\n\tvirtual void elementStartEffect(\n\t\tnot_null<const Element*> view,\n\t\tElement *replacing) = 0;\n\tvirtual QString elementAuthorRank(not_null<const Element*> view) = 0;\n\tvirtual bool elementHideTopicButton(not_null<const Element*> view) = 0;\n\n\tvirtual ~ElementDelegate() {\n\t}\n\n};\n\n[[nodiscard]] std::unique_ptr<Ui::PathShiftGradient> MakePathShiftGradient(\n\tnot_null<const Ui::ChatStyle*> st,\n\tFn<void()> update);\n\nclass DefaultElementDelegate : public ElementDelegate {\npublic:\n\tbool elementUnderCursor(not_null<const Element*> view) override;\n\tSelectionModeResult elementInSelectionMode(const Element *view) override;\n\tbool elementIntersectsRange(\n\t\tnot_null<const Element*> view,\n\t\tint from,\n\t\tint till) override;\n\tvoid elementStartStickerLoop(not_null<const Element*> view) override;\n\tvoid elementShowPollResults(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context) override;\n\tvoid elementShowAddPollOption(\n\t\tnot_null<Element*> view,\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context,\n\t\tQRect optionRect) override;\n\tvoid elementSubmitAddPollOption(FullMsgId context) override;\n\tvoid elementOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) override;\n\tvoid elementOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView = false) override;\n\tvoid elementCancelUpload(const FullMsgId &context) override;\n\tvoid elementShowTooltip(\n\t\tconst TextWithEntities &text,\n\t\tFn<void()> hiddenCallback) override;\n\tbool elementHideReply(not_null<const Element*> view) override;\n\tbool elementShownUnread(not_null<const Element*> view) override;\n\tvoid elementSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) override;\n\tvoid elementSearchInList(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) override;\n\tvoid elementHandleViaClick(not_null<UserData*> bot) override;\n\tElementChatMode elementChatMode() override;\n\tvoid elementReplyTo(const FullReplyTo &to) override;\n\tvoid elementStartInteraction(not_null<const Element*> view) override;\n\tvoid elementStartPremium(\n\t\tnot_null<const Element*> view,\n\t\tElement *replacing) override;\n\tvoid elementCancelPremium(not_null<const Element*> view) override;\n\tvoid elementStartEffect(\n\t\tnot_null<const Element*> view,\n\t\tElement *replacing) override;\n\tQString elementAuthorRank(not_null<const Element*> view) override;\n\tbool elementHideTopicButton(not_null<const Element*> view) override;\n\n};\n\nclass SimpleElementDelegate : public DefaultElementDelegate {\npublic:\n\tSimpleElementDelegate(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFn<void()> update);\n\t~SimpleElementDelegate();\n\n\tbool elementAnimationsPaused() override;\n\tnot_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;\n\nprotected:\n\t[[nodiscard]] not_null<Window::SessionController*> controller() const {\n\t\treturn _controller;\n\t}\n\nprivate:\n\tconst not_null<Window::SessionController*> _controller;\n\tconst std::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\n};\n\nTextSelection UnshiftItemSelection(\n\tTextSelection selection,\n\tuint16 byLength);\nTextSelection ShiftItemSelection(\n\tTextSelection selection,\n\tuint16 byLength);\nTextSelection UnshiftItemSelection(\n\tTextSelection selection,\n\tconst Ui::Text::String &byText);\nTextSelection ShiftItemSelection(\n\tTextSelection selection,\n\tconst Ui::Text::String &byText);\n\nQString DateTooltipText(not_null<Element*> view);\n\n// Any HistoryView::Element can have this Component for\n// displaying the unread messages bar above the message.\nstruct UnreadBar : RuntimeComponent<UnreadBar, Element> {\n\tvoid init(const QString &string);\n\n\tstatic int height();\n\tstatic int marginTop();\n\n\tvoid paint(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint y,\n\t\tint w,\n\t\tElementChatMode mode) const;\n\n\tQString text;\n\tint width = 0;\n\trpl::lifetime lifetime;\n\n};\n\n// Any HistoryView::Element can have this Component for\n// displaying the day mark above the message.\nstruct DateBadge : RuntimeComponent<DateBadge, Element> {\n\tvoid init(const QString &date);\n\n\tint height() const;\n\tvoid paint(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide) const;\n\n\tQString text;\n\tint width = 0;\n\n};\n\nstruct ForumThreadBar : RuntimeComponent<ForumThreadBar, Element> {\n\tvoid init(\n\t\tnot_null<PeerData*> parentChat,\n\t\tnot_null<Data::Thread*> thread);\n\n\tint height() const;\n\tvoid paint(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide,\n\t\tbool skipPatternLine) const;\n\tstatic int PaintForGetWidth(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tnot_null<Element*> itemView,\n\t\tUi::PeerUserpicView &userpicView,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide);\n\n\tbase::weak_ptr<Data::Thread> thread;\n\tUi::Text::String text;\n\tmutable Ui::PeerUserpicView view;\n\tint width = 0;\n\nprivate:\n\tstatic void Paint(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst Ui::Text::String &text,\n\t\tint width,\n\t\tUi::PeerUserpicView &view,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide,\n\t\tbool skipPatternLine);\n\n};\n\n// Any HistoryView::Element can have this Component for\n// displaying some text in layout of a service message above the message.\nstruct ServicePreMessage : RuntimeComponent<ServicePreMessage, Element> {\n\tvoid init(\n\t\tnot_null<Element*> view,\n\t\tPreparedServiceText string,\n\t\tClickHandlerPtr fullClickHandler,\n\t\tstd::unique_ptr<Media> media,\n\t\tbool below);\n\n\tint resizeToWidth(int newWidth, ElementChatMode mode);\n\n\tvoid paint(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tQRect g,\n\t\tElementChatMode mode) const;\n\t[[nodiscard]] ClickHandlerPtr textState(\n\t\tQPoint point,\n\t\tconst StateRequest &request,\n\t\tQRect g) const;\n\n\tstd::unique_ptr<Media> media;\n\tUi::Text::String text;\n\tClickHandlerPtr handler;\n\tint width = 0;\n\tint height = 0;\n\tbool below = false;\n\n};\n\nstruct FakeBotAboutTop : RuntimeComponent<FakeBotAboutTop, Element> {\n\tvoid init();\n\n\tUi::Text::String text;\n\tint maxWidth = 0;\n\tint height = 0;\n};\n\nstruct PurchasedTag : RuntimeComponent<PurchasedTag, Element> {\n\tUi::Text::String text;\n};\n\nstruct ViewAddedMargins : RuntimeComponent<ViewAddedMargins, Element> {\n\tint top = 0;\n\tint bottom = 0;\n};\n\nstruct TopicButton {\n\tstd::unique_ptr<Ui::RippleAnimation> ripple;\n\tClickHandlerPtr link;\n\tUi::Text::String name;\n\tQPoint lastPoint;\n\tint nameVersion = 0;\n};\n\nstruct SelectedQuote {\n\tHistoryItem *item = nullptr;\n\tMessageHighlightId highlight;\n\tbool overflown = false;\n\n\texplicit operator bool() const {\n\t\treturn item && !highlight.quote.empty();\n\t}\n\tfriend inline bool operator==(SelectedQuote, SelectedQuote) = default;\n};\n\nclass Element\n\t: public Object\n\t, public RuntimeComposer<Element>\n\t, public ClickHandlerHost\n\t, public base::has_weak_ptr {\npublic:\n\tenum class Flag : uint16 {\n\t\tServiceMessage           = 0x0001,\n\t\tNeedsResize              = 0x0002,\n\t\tAttachedToPrevious       = 0x0004,\n\t\tAttachedToNext           = 0x0008,\n\t\tBubbleAttachedToPrevious = 0x0010,\n\t\tBubbleAttachedToNext     = 0x0020,\n\t\tHiddenByGroup            = 0x0040,\n\t\tSpecialOnlyEmoji         = 0x0080,\n\t\tCustomEmojiRepainting    = 0x0100,\n\t\tScheduledUntilOnline     = 0x0200,\n\t\tTopicRootReply           = 0x0400,\n\t\tMediaOverriden           = 0x0800,\n\t\tHeavyCustomEmoji         = 0x1000,\n\t\tSummaryShown             = 0x2000,\n\t};\n\tusing Flags = base::flags<Flag>;\n\tfriend inline constexpr auto is_flag_type(Flag) { return true; }\n\n\tElement(\n\t\tnot_null<ElementDelegate*> delegate,\n\t\tnot_null<HistoryItem*> data,\n\t\tElement *replacing,\n\t\tFlag serviceFlag);\n\n\t[[nodiscard]] virtual bool embedReactionsInBubble() const;\n\n\t[[nodiscard]] not_null<ElementDelegate*> delegate() const;\n\t[[nodiscard]] not_null<HistoryItem*> data() const;\n\t[[nodiscard]] not_null<History*> history() const;\n\t[[nodiscard]] Media *media() const;\n\t[[nodiscard]] Context context() const;\n\tvoid refreshDataId();\n\n\t[[nodiscard]] uint8 colorIndex() const;\n\t[[nodiscard]] auto colorCollectible() const\n\t\t-> const std::shared_ptr<Ui::ColorCollectible> &;\n\n\t[[nodiscard]] uint8 contentColorIndex() const;\n\t[[nodiscard]] DocumentId contentBackgroundEmojiId() const;\n\t[[nodiscard]] auto contentColorCollectible() const\n\t\t-> const std::shared_ptr<Ui::ColorCollectible> &;\n\n\t[[nodiscard]] QDateTime dateTime() const;\n\n\t[[nodiscard]] int y() const;\n\tvoid setY(int y);\n\n\t[[nodiscard]] virtual int marginTop() const = 0;\n\t[[nodiscard]] virtual int marginBottom() const = 0;\n\n\tvoid addVerticalMargins(int top, int bottom);\n\n\tvoid setPendingResize();\n\t[[nodiscard]] bool pendingResize() const;\n\t[[nodiscard]] bool isUnderCursor() const;\n\n\t[[nodiscard]] bool isLastAndSelfMessage() const;\n\n\t[[nodiscard]] bool isAttachedToPrevious() const;\n\t[[nodiscard]] bool isAttachedToNext() const;\n\t[[nodiscard]] bool isBubbleAttachedToPrevious() const;\n\t[[nodiscard]] bool isBubbleAttachedToNext() const;\n\n\t[[nodiscard]] bool isTopicRootReply() const;\n\n\t[[nodiscard]] int skipBlockWidth() const;\n\t[[nodiscard]] int skipBlockHeight() const;\n\t[[nodiscard]] virtual int infoWidth() const;\n\t[[nodiscard]] virtual int bottomInfoFirstLineWidth() const;\n\t[[nodiscard]] virtual bool bottomInfoIsWide() const;\n\n\t[[nodiscard]] bool isHiddenByGroup() const;\n\t[[nodiscard]] virtual bool isHidden() const;\n\n\t[[nodiscard]] bool isIsolatedEmoji() const {\n\t\treturn (_flags & Flag::SpecialOnlyEmoji)\n\t\t\t&& _text.isIsolatedEmoji();\n\t}\n\t[[nodiscard]] bool isOnlyCustomEmoji() const {\n\t\treturn (_flags & Flag::SpecialOnlyEmoji)\n\t\t\t&& _text.isOnlyCustomEmoji();\n\t}\n\n\t[[nodiscard]] HistoryItem *textItem() const;\n\t[[nodiscard]] Ui::Text::IsolatedEmoji isolatedEmoji() const;\n\t[[nodiscard]] Ui::Text::OnlyCustomEmoji onlyCustomEmoji() const;\n\tvoid skipInactiveTextAppearing();\n\n\t[[nodiscard]] OnlyEmojiAndSpaces isOnlyEmojiAndSpaces() const;\n\n\t// For blocks context this should be called only from recountAttachToPreviousInBlocks().\n\tvoid setAttachToPrevious(bool attachToNext, Element *previous = nullptr);\n\n\t// For blocks context this should be called only from recountAttachToPreviousInBlocks()\n\t// of the next item or when the next item is removed through nextInBlocksRemoved() call.\n\tvoid setAttachToNext(bool attachToNext, Element *next = nullptr);\n\n\t// For blocks context this should be called only from recountDisplayDate().\n\tvoid setDisplayDate(bool displayDate);\n\tvoid setServicePreMessage(\n\t\tPreparedServiceText text,\n\t\tClickHandlerPtr fullClickHandler = nullptr,\n\t\tstd::unique_ptr<Media> media = nullptr);\n\tvoid setServicePostMessage(\n\t\tPreparedServiceText text,\n\t\tClickHandlerPtr fullClickHandler = nullptr,\n\t\tstd::unique_ptr<Media> media = nullptr);\n\n\tbool computeIsAttachToPrevious(not_null<Element*> previous);\n\n\tvoid createUnreadBar(rpl::producer<QString> text);\n\tvoid destroyUnreadBar();\n\n\t[[nodiscard]] int displayedDateHeight() const;\n\t[[nodiscard]] bool displayDate() const;\n\t[[nodiscard]] bool isInOneDayWithPrevious() const;\n\n\t[[nodiscard]] bool displayForumThreadBar() const;\n\t[[nodiscard]] bool isInOneBunchWithPrevious() const;\n\n\tvirtual void draw(Painter &p, const PaintContext &context) const = 0;\n\t[[nodiscard]] virtual PointState pointState(QPoint point) const = 0;\n\t[[nodiscard]] virtual TextState textState(\n\t\tQPoint point,\n\t\tStateRequest request) const = 0;\n\tvirtual void updatePressed(QPoint point) = 0;\n\tvirtual void drawInfo(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint right,\n\t\tint bottom,\n\t\tint width,\n\t\tInfoDisplayType type) const;\n\tvirtual TextState bottomInfoTextState(\n\t\tint right,\n\t\tint bottom,\n\t\tQPoint point,\n\t\tInfoDisplayType type) const;\n\tvirtual TextForMimeData selectedText(TextSelection selection) const = 0;\n\tvirtual SelectedQuote selectedQuote(\n\t\tTextSelection selection) const = 0;\n\tvirtual TextSelection selectionFromQuote(\n\t\tconst SelectedQuote &quote) const = 0;\n\t[[nodiscard]] virtual TextSelection adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const;\n\n\t[[nodiscard]] static SelectedQuote FindSelectedQuote(\n\t\tconst Ui::Text::String &text,\n\t\tTextSelection selection,\n\t\tnot_null<HistoryItem*> item);\n\t[[nodiscard]] static TextSelection FindSelectionFromQuote(\n\t\tconst Ui::Text::String &text,\n\t\tconst SelectedQuote &quote);\n\n\t[[nodiscard]] virtual auto reactionButtonParameters(\n\t\tQPoint position,\n\t\tconst TextState &reactionState) const -> Reactions::ButtonParameters;\n\t[[nodiscard]] virtual auto replyButtonParameters(\n\t\tQPoint position,\n\t\tconst TextState &replyState) const -> ReplyButton::ButtonParameters;\n\t[[nodiscard]] virtual int reactionsOptimalWidth() const;\n\n\t// ClickHandlerHost interface.\n\tvoid clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool active) override;\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) override;\n\n\t// hasFromPhoto() returns true even if we don't display the photo\n\t// but we need to skip a place at the left side for this photo\n\t[[nodiscard]] virtual bool hasFromPhoto() const;\n\t[[nodiscard]] virtual bool displayFromPhoto() const;\n\t[[nodiscard]] virtual bool hasFromName() const;\n\t[[nodiscard]] bool displayReply() const;\n\t[[nodiscard]] virtual bool displayFromName() const;\n\t[[nodiscard]] virtual TopicButton *displayedTopicButton() const;\n\t[[nodiscard]] virtual bool displayForwardedFrom() const;\n\t[[nodiscard]] virtual bool hasOutLayout() const;\n\t[[nodiscard]] bool hasRightLayout() const;\n\t[[nodiscard]] virtual bool drawBubble() const;\n\t[[nodiscard]] virtual bool hasBubble() const;\n\t[[nodiscard]] virtual bool unwrapped() const;\n\t[[nodiscard]] virtual int minWidthForMedia() const {\n\t\treturn 0;\n\t}\n\t[[nodiscard]] virtual std::optional<QSize> rightActionSize() const;\n\tvirtual void drawRightAction(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint left,\n\t\tint top,\n\t\tint outerWidth) const;\n\t[[nodiscard]] virtual ClickHandlerPtr rightActionLink(\n\t\tstd::optional<QPoint> pressPoint) const;\n\t[[nodiscard]] virtual TimeId displayedEditDate() const;\n\t[[nodiscard]] virtual bool hasVisibleText() const;\n\t[[nodiscard]] int textualMaxWidth() const;\n\tvirtual void applyGroupAdminChanges(\n\t\tconst base::flat_set<UserId> &changes) {\n\t}\n\t[[nodiscard]] virtual bool toggleSelectionByHandlerClick(\n\t\tconst ClickHandlerPtr &handler) const;\n\t[[nodiscard]] virtual bool allowTextSelectionByHandler(\n\t\tconst ClickHandlerPtr &handler) const;\n\n\t[[nodiscard]] bool usesBubblePattern(const PaintContext &context) const;\n\n\tstruct VerticalRepaintRange {\n\t\tint top = 0;\n\t\tint height = 0;\n\t};\n\t[[nodiscard]] virtual VerticalRepaintRange verticalRepaintRange() const;\n\n\t[[nodiscard]] virtual bool isSignedAuthorElided() const;\n\n\tvirtual void itemDataChanged();\n\tvoid itemTextUpdated();\n\tvoid blockquoteExpandChanged();\n\n\tvirtual void unloadHeavyPart();\n\tvoid checkHeavyPart();\n\n\tvoid paintCustomHighlight(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint y,\n\t\tint height,\n\t\tnot_null<const HistoryItem*> item) const;\n\n\t// Legacy blocks structure.\n\t[[nodiscard]] HistoryBlock *block();\n\t[[nodiscard]] const HistoryBlock *block() const;\n\tvoid attachToBlock(not_null<HistoryBlock*> block, int index);\n\tvoid removeFromBlock();\n\tvoid refreshInBlock();\n\tvoid setIndexInBlock(int index);\n\t[[nodiscard]] int indexInBlock() const;\n\t[[nodiscard]] Element *previousInBlocks() const;\n\t[[nodiscard]] Element *previousDisplayedInBlocks() const;\n\t[[nodiscard]] Element *nextInBlocks() const;\n\t[[nodiscard]] Element *nextDisplayedInBlocks() const;\n\tvoid previousInBlocksChanged();\n\tvoid nextInBlocksRemoved();\n\n\t[[nodiscard]] virtual QRect effectIconGeometry() const;\n\t[[nodiscard]] virtual QRect innerGeometry() const = 0;\n\t[[nodiscard]] virtual QPoint mediaTopLeft() const;\n\n\tvoid customEmojiRepaint();\n\tvoid prepareCustomEmojiPaint(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst Ui::Text::String &text) const;\n\tvoid prepareCustomEmojiPaint(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst Reactions::InlineList &reactions) const;\n\tvoid clearCustomEmojiRepaint() const;\n\tvoid hideSpoilers();\n\tvoid repaint(QRect r = QRect()) const;\n\n\t[[nodiscard]] ClickHandlerPtr fromPhotoLink() const {\n\t\treturn fromLink();\n\t}\n\n\t[[nodiscard]] bool markSponsoredViewed(int shownFromTop) const;\n\n\tvirtual void animateReaction(Ui::ReactionFlyAnimationArgs &&args) = 0;\n\tvoid animateUnreadReactions();\n\t[[nodiscard]] auto takeReactionAnimations()\n\t-> base::flat_map<\n\t\tData::ReactionId,\n\t\tstd::unique_ptr<Ui::ReactionFlyAnimation>>;\n\n\tvoid animateUnreadEffect();\n\t[[nodiscard]] virtual auto takeEffectAnimation()\n\t-> std::unique_ptr<Ui::ReactionFlyAnimation>;\n\n\tvoid overrideMedia(std::unique_ptr<Media> media);\n\tvoid overrideRightBadge(const QString &text, BadgeRole role);\n\n\t[[nodiscard]] not_null<PurchasedTag*> enforcePurchasedTag();\n\n\t[[nodiscard]] static int AdditionalSpaceForSelectionCheckbox(\n\t\tnot_null<const Element*> view,\n\t\tQRect countedGeometry = QRect());\n\n\tvirtual bool consumeHorizontalScroll(QPoint position, int delta) {\n\t\treturn false;\n\t}\n\n\tvirtual ~Element();\n\n\tstatic void Hovered(Element *view);\n\t[[nodiscard]] static Element *Hovered();\n\tstatic void Pressed(Element *view);\n\t[[nodiscard]] static Element *Pressed();\n\tstatic void HoveredLink(Element *view);\n\t[[nodiscard]] static Element *HoveredLink();\n\tstatic void PressedLink(Element *view);\n\t[[nodiscard]] static Element *PressedLink();\n\tstatic void Moused(Element *view);\n\t[[nodiscard]] static Element *Moused();\n\tstatic void ClearGlobal();\n\nprotected:\n\tvoid paintHighlight(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint geometryHeight) const;\n\n\t[[nodiscard]] ClickHandlerPtr fromLink() const;\n\n\t[[nodiscard]] virtual bool hasHeavyPart() const;\n\tvirtual void refreshDataIdHook();\n\n\t[[nodiscard]] const Ui::Text::String &text() const;\n\t[[nodiscard]] int textHeightFor(int textWidth) const;\n\t[[nodiscard]] int textRealWidth() const { return _textRealWidth; }\n\tvoid validateText();\n\tvoid validateTextSkipBlock(bool has, int width, int height);\n\tvoid validateInlineKeyboard(HistoryMessageReplyMarkup *markup);\n\n\tvoid clearSpecialOnlyEmoji();\n\tvoid checkSpecialOnlyEmoji();\n\n\tvoid setupReactions(Element *replacing);\n\tvoid refreshReactions();\n\tbool updateReactions();\n\n\tstd::unique_ptr<Reactions::InlineList> _reactions;\n\nprivate:\n\tvoid recountThreadBarInBlocks();\n\n\t// This should be called only from previousInBlocksChanged()\n\t// to add required bits to the Composer mask\n\t// after that always use Has<DateBadge>().\n\tvoid recountDisplayDateInBlocks();\n\n\t// This should be called only from previousInBlocksChanged() or when\n\t// DateBadge or UnreadBar or MonoforumSenderBar bit\n\t// is changed in the Composer mask then the result\n\t// should be cached in a client side flag\n\t// HistoryView::Element::Flag::AttachedToPrevious.\n\tvoid recountAttachToPreviousInBlocks();\n\n\t[[nodiscard]] bool countIsTopicRootReply() const;\n\n\tQSize countOptimalSize() final override;\n\tQSize countCurrentSize(int newWidth) final override;\n\n\tvirtual QSize performCountOptimalSize() = 0;\n\tvirtual QSize performCountCurrentSize(int newWidth) = 0;\n\tvirtual void invalidateTextDependentCache() {\n\t}\n\n\tvoid refreshMedia(Element *replacing);\n\tvoid invalidateTextSizeCache();\n\tvoid setTextWithLinks(\n\t\tconst TextWithEntities &text,\n\t\tconst std::vector<ClickHandlerPtr> &links = {});\n\tvoid setReactions(std::unique_ptr<Reactions::InlineList> list);\n\n\tstruct TextWithLinks {\n\t\tTextWithEntities text;\n\t\tstd::vector<ClickHandlerPtr> links;\n\t};\n\t[[nodiscard]] TextWithLinks contextDependentServiceText();\n\n\tconst not_null<ElementDelegate*> _delegate;\n\tconst not_null<HistoryItem*> _data;\n\tHistoryBlock *_block = nullptr;\n\tstd::unique_ptr<Media> _media;\n\tmutable ClickHandlerPtr _fromLink;\n\tconst QDateTime _dateTime;\n\n\tHistoryItem *_textItem = nullptr;\n\tmutable Ui::Text::String _text;\n\tmutable uint32 _textWidth : 16 = 0;\n\tmutable uint32 _textRealWidth : 16 = 0;\n\tmutable int _textHeight = 0;\n\n\tint _y = 0;\n\tint _indexInBlock = -1;\n\n\tmutable Flags _flags = Flag(0);\n\tContext _context = Context();\n\n};\n\n[[nodiscard]] int FindViewY(\n\tnot_null<Element*> view,\n\tuint16 symbol,\n\tint yfrom = 0);\n\n[[nodiscard]] int FindViewTaskY(\n\tnot_null<Element*> view,\n\tint taskId,\n\tint yfrom = 0);\n\n[[nodiscard]] int FindViewPollOptionY(\n\tnot_null<Element*> view,\n\tconst QByteArray &option,\n\tint yfrom = 0);\n\nstruct HighlightYRange {\n\tint begin = 0;\n\tint end = 0;\n\n\texplicit operator bool() const {\n\t\treturn begin != end;\n\t}\n};\n\n[[nodiscard]] HighlightYRange FindHighlightYRange(\n\tnot_null<Element*> view,\n\tconst Ui::ChatPaintHighlight &highlight);\n\n[[nodiscard]] int AdjustScrollForRange(\n\tint viewTop,\n\tint available,\n\tHighlightYRange range);\n\n[[nodiscard]] Window::SessionController *ExtractController(\n\tconst ClickContext &context);\n\n[[nodiscard]] TextSelection FindSearchQueryHighlight(\n\tconst QString &text,\n\tconst QString &query);\n\n[[nodiscard]] TextSelection FindSearchQueryHighlight(\n\tconst QString &text,\n\tQStringView lower);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_element_overlay.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_element_overlay.h\"\n\n#include \"history/view/history_view_element.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace HistoryView {\n\nElementOverlayHost::ElementOverlayHost(\n\tnot_null<QWidget*> container,\n\tItemTopFn itemTopFn)\n: _container(container)\n, _itemTopFn(std::move(itemTopFn)) {\n}\n\nvoid ElementOverlayHost::show(\n\t\tnot_null<Element*> view,\n\t\tFullMsgId context,\n\t\tbase::unique_qptr<Ui::RpWidget> widget,\n\t\trpl::producer<> closeRequests,\n\t\tPositionFn positionFn,\n\t\tMediaStateFn mediaStateFn,\n\t\tFn<void()> submitFn) {\n\tif (_widget && _context == context) {\n\t\thide();\n\t\treturn;\n\t}\n\thide();\n\n\t_view = view;\n\t_context = context;\n\t_positionFn = std::move(positionFn);\n\t_mediaStateFn = std::move(mediaStateFn);\n\t_submitFn = std::move(submitFn);\n\n\tif (_mediaStateFn) {\n\t\t_mediaStateFn(view, true);\n\t}\n\n\t_widget = std::move(widget);\n\n\tstd::move(\n\t\tcloseRequests\n\t) | rpl::on_next([=] {\n\t\thide();\n\t}, _closeLifetime);\n\n\tupdatePosition();\n\t_widget->show();\n}\n\nvoid ElementOverlayHost::hide() {\n\tif (!_widget) {\n\t\treturn;\n\t}\n\tcleanup(true);\n}\n\nvoid ElementOverlayHost::viewGone(not_null<const Element*> view) {\n\tif (_view != view.get()) {\n\t\treturn;\n\t}\n\tcleanup(false);\n}\n\nvoid ElementOverlayHost::cleanup(bool notifyMedia) {\n\tif (notifyMedia && _view && _mediaStateFn) {\n\t\t_mediaStateFn(_view, false);\n\t}\n\t_widget = nullptr;\n\t_view = nullptr;\n\t_context = FullMsgId();\n\t_positionFn = nullptr;\n\t_mediaStateFn = nullptr;\n\t_submitFn = nullptr;\n\t_closeLifetime.destroy();\n\tif (const auto callback = _hiddenCallback) {\n\t\tcallback();\n\t}\n}\n\nvoid ElementOverlayHost::updatePosition() {\n\tif (!_widget || !_view || !_positionFn) {\n\t\treturn;\n\t}\n\tconst auto top = _itemTopFn(_view);\n\tif (top < 0) {\n\t\treturn;\n\t}\n\tif (!_positionFn(_view, top)) {\n\t\thide();\n\t}\n}\n\nvoid ElementOverlayHost::handleClickOutside(QPoint clickPos) {\n\tif (!_widget || !_view) {\n\t\treturn;\n\t}\n\tconst auto top = _itemTopFn(_view);\n\tconst auto viewRect = (top >= 0)\n\t\t? QRect(0, top, _container->width(), _view->height())\n\t\t: QRect();\n\tif (!viewRect.contains(clickPos)) {\n\t\thide();\n\t}\n}\n\nvoid ElementOverlayHost::triggerSubmit(FullMsgId context) {\n\tif (_widget && _context == context && _submitFn) {\n\t\t_submitFn();\n\t}\n}\n\nvoid ElementOverlayHost::setHiddenCallback(Fn<void()> callback) {\n\t_hiddenCallback = std::move(callback);\n}\n\nbool ElementOverlayHost::active() const {\n\treturn _widget != nullptr;\n}\n\nFullMsgId ElementOverlayHost::context() const {\n\treturn _context;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_element_overlay.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n#include \"data/data_messages.h\"\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass Element;\n\nclass ElementOverlayHost final {\npublic:\n\tusing ItemTopFn = Fn<int(not_null<const Element*>)>;\n\tusing PositionFn = Fn<bool(not_null<Element*>, int itemTop)>;\n\tusing MediaStateFn = Fn<void(not_null<Element*>, bool active)>;\n\n\texplicit ElementOverlayHost(\n\t\tnot_null<QWidget*> container,\n\t\tItemTopFn itemTopFn);\n\n\tvoid show(\n\t\tnot_null<Element*> view,\n\t\tFullMsgId context,\n\t\tbase::unique_qptr<Ui::RpWidget> widget,\n\t\trpl::producer<> closeRequests,\n\t\tPositionFn positionFn,\n\t\tMediaStateFn mediaStateFn = nullptr,\n\t\tFn<void()> submitFn = nullptr);\n\n\tvoid hide();\n\tvoid viewGone(not_null<const Element*> view);\n\tvoid updatePosition();\n\tvoid handleClickOutside(QPoint clickPos);\n\tvoid triggerSubmit(FullMsgId context);\n\tvoid setHiddenCallback(Fn<void()> callback);\n\n\t[[nodiscard]] bool active() const;\n\t[[nodiscard]] FullMsgId context() const;\n\nprivate:\n\tvoid cleanup(bool notifyMedia);\n\n\tconst not_null<QWidget*> _container;\n\tconst ItemTopFn _itemTopFn;\n\n\tbase::unique_qptr<Ui::RpWidget> _widget;\n\tElement *_view = nullptr;\n\tFullMsgId _context;\n\tPositionFn _positionFn;\n\tMediaStateFn _mediaStateFn;\n\tFn<void()> _submitFn;\n\tFn<void()> _hiddenCallback;\n\trpl::lifetime _closeLifetime;\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_emoji_interactions.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_emoji_interactions.h\"\n\n#include \"history/view/history_view_element.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"chat_helpers/emoji_interactions.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_message_reactions.h\"\n#include \"lottie/lottie_common.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"base/random.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kPremiumShift = 21. / 240;\nconstexpr auto kMaxPlays = 5;\nconstexpr auto kMaxPlaysWithSmallDelay = 3;\nconstexpr auto kSmallDelay = crl::time(200);\nconstexpr auto kDropDelayedAfterDelay = crl::time(2000);\n\n[[nodiscard]] QPoint GenerateRandomShift(QSize emoji) {\n\t// Random shift in [-0.08 ... 0.08] of animated emoji size.\n\tconst auto maxShift = emoji * 2 / 25;\n\treturn {\n\t\tbase::RandomIndex(maxShift.width() * 2 + 1) - maxShift.width(),\n\t\tbase::RandomIndex(maxShift.height() * 2 + 1) - maxShift.height(),\n\t};\n}\n\n} // namespace\n\nEmojiInteractions::EmojiInteractions(\n\tnot_null<QWidget*> parent,\n\tnot_null<QWidget*> layerParent,\n\tnot_null<Main::Session*> session,\n\tFn<int(not_null<const Element*>)> itemTop)\n: _parent(parent)\n, _layerParent(layerParent)\n, _session(session)\n, _itemTop(std::move(itemTop)) {\n\t_session->data().viewRemoved(\n\t) | rpl::filter([=] {\n\t\treturn !_plays.empty() || !_delayed.empty();\n\t}) | rpl::on_next([=](not_null<const Element*> view) {\n\t\t_plays.erase(ranges::remove(_plays, view, &Play::view), end(_plays));\n\t\t_delayed.erase(\n\t\t\tranges::remove(_delayed, view, &Delayed::view),\n\t\t\tend(_delayed));\n\t}, _lifetime);\n\n\t_session->data().reactions().effectsUpdates(\n\t) | rpl::on_next([=] {\n\t\tcheckPendingEffects();\n\t}, _lifetime);\n}\n\nEmojiInteractions::~EmojiInteractions() = default;\n\nvoid EmojiInteractions::play(\n\t\tChatHelpers::EmojiInteractionPlayRequest request,\n\t\tnot_null<Element*> view) {\n\tif (!view->media()) {\n\t\t// Large emoji may be disabled.\n\t\treturn;\n\t} else if (_plays.empty()) {\n\t\tplay(\n\t\t\tstd::move(request.emoticon),\n\t\t\tview,\n\t\t\tstd::move(request.media),\n\t\t\trequest.incoming);\n\t} else {\n\t\tconst auto now = crl::now();\n\t\t_delayed.push_back({\n\t\t\trequest.emoticon,\n\t\t\tview,\n\t\t\tstd::move(request.media),\n\t\t\tnow,\n\t\t\trequest.incoming,\n\t\t});\n\t\tcheckDelayed();\n\t}\n}\n\nbool EmojiInteractions::playPremiumEffect(\n\t\tnot_null<const Element*> view,\n\t\tElement *replacing) {\n\tconst auto already = ranges::contains(_plays, view, &Play::view);\n\tif (replacing) {\n\t\tconst auto i = ranges::find(_plays, replacing, &Play::view);\n\t\tif (i != end(_plays)) {\n\t\t\tif (already) {\n\t\t\t\t_plays.erase(i);\n\t\t\t} else {\n\t\t\t\ti->view = view;\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t} else if (already) {\n\t\treturn false;\n\t}\n\tif (const auto media = view->media()) {\n\t\tif (const auto document = media->getDocument()) {\n\t\t\tif (document->isPremiumSticker()) {\n\t\t\t\tplay(\n\t\t\t\t\tQString(),\n\t\t\t\t\tview,\n\t\t\t\t\tdocument,\n\t\t\t\t\tdocument->createMediaView()->videoThumbnailContent(),\n\t\t\t\t\tQString(),\n\t\t\t\t\tfalse,\n\t\t\t\t\tStickers::EffectType::PremiumSticker);\n\t\t\t}\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid EmojiInteractions::cancelPremiumEffect(not_null<const Element*> view) {\n\t_plays.erase(ranges::remove_if(_plays, [&](const Play &play) {\n\t\tif (play.view != view) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}), end(_plays));\n}\n\nvoid EmojiInteractions::play(\n\t\tQString emoticon,\n\t\tnot_null<const Element*> view,\n\t\tstd::shared_ptr<Data::DocumentMedia> media,\n\t\tbool incoming) {\n\tplay(\n\t\tstd::move(emoticon),\n\t\tview,\n\t\tmedia->owner(),\n\t\tmedia->bytes(),\n\t\tmedia->owner()->filepath(),\n\t\tincoming,\n\t\tStickers::EffectType::EmojiInteraction);\n}\n\nvoid EmojiInteractions::playEffectOnRead(not_null<const Element*> view) {\n\tconst auto flag = PowerSaving::Flag::kChatEffects;\n\tif (view->data()->markEffectWatched() && !PowerSaving::On(flag)) {\n\t\tplayEffect(view);\n\t}\n}\n\nvoid EmojiInteractions::playEffect(not_null<const Element*> view) {\n\tif (const auto resolved = resolveEffect(view)) {\n\t\tplayEffect(view, resolved);\n\t} else if (view->data()->effectId()) {\n\t\tif (resolved.document && !_downloadLifetime) {\n\t\t\t_downloadLifetime = _session->downloaderTaskFinished(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tcheckPendingEffects();\n\t\t\t});\n\t\t}\n\t\taddPendingEffect(view);\n\t}\n}\n\nEmojiInteractions::ResolvedEffect EmojiInteractions::resolveEffect(\n\t\tnot_null<const Element*> view) {\n\tconst auto item = view->data();\n\tconst auto effectId = item->effectId();\n\tif (!effectId) {\n\t\treturn {};\n\t}\n\tusing Type = Data::Reactions::Type;\n\tconst auto &effects = _session->data().reactions().list(Type::Effects);\n\tconst auto i = ranges::find(\n\t\teffects,\n\t\tData::ReactionId{ effectId },\n\t\t&Data::Reaction::id);\n\tif (i == end(effects)) {\n\t\treturn {};\n\t}\n\tauto document = (DocumentData*)nullptr;\n\tauto content = QByteArray();\n\tauto filepath = QString();\n\tif ((document = i->aroundAnimation)) {\n\t\tcontent = document->createMediaView()->bytes();\n\t\tfilepath = document->filepath();\n\t} else {\n\t\tdocument = i->selectAnimation;\n\t\tcontent = document->createMediaView()->videoThumbnailContent();\n\t}\n\treturn {\n\t\t.emoticon = i->title,\n\t\t.document = document,\n\t\t.content = content,\n\t\t.filepath = filepath,\n\t};\n}\n\nvoid EmojiInteractions::playEffect(\n\t\tnot_null<const Element*> view,\n\t\tconst ResolvedEffect &resolved) {\n\tplay(\n\t\tresolved.emoticon,\n\t\tview,\n\t\tresolved.document,\n\t\tresolved.content,\n\t\tresolved.filepath,\n\t\tfalse,\n\t\tStickers::EffectType::MessageEffect);\n}\n\nvoid EmojiInteractions::addPendingEffect(not_null<const Element*> view) {\n\tauto found = false;\n\tconst auto predicate = [&](base::weak_ptr<const Element> weak) {\n\t\tconst auto strong = weak.get();\n\t\tif (strong == view) {\n\t\t\tfound = true;\n\t\t}\n\t\treturn !strong;\n\t};\n\t_pendingEffects.erase(\n\t\tranges::remove_if(_pendingEffects, predicate),\n\t\tend(_pendingEffects));\n\tif (!found) {\n\t\t_pendingEffects.push_back(view);\n\t}\n}\n\nvoid EmojiInteractions::checkPendingEffects() {\n\tauto waitingDownload = false;\n\tconst auto predicate = [&](base::weak_ptr<const Element> weak) {\n\t\tconst auto strong = weak.get();\n\t\tif (!strong) {\n\t\t\treturn true;\n\t\t}\n\t\tconst auto resolved = resolveEffect(strong);\n\t\tif (resolved) {\n\t\t\tplayEffect(strong, resolved);\n\t\t\treturn true;\n\t\t} else if (!strong->data()->effectId()) {\n\t\t\treturn true;\n\t\t} else if (resolved.document) {\n\t\t\twaitingDownload = true;\n\t\t}\n\t\treturn false;\n\t};\n\t_pendingEffects.erase(\n\t\tranges::remove_if(_pendingEffects, predicate),\n\t\tend(_pendingEffects));\n\tif (!waitingDownload) {\n\t\t_downloadLifetime.destroy();\n\t} else if (!_downloadLifetime) {\n\t\t_downloadLifetime = _session->downloaderTaskFinished(\n\t\t) | rpl::on_next([=] {\n\t\t\tcheckPendingEffects();\n\t\t});\n\t}\n}\n\nvoid EmojiInteractions::play(\n\t\tQString emoticon,\n\t\tnot_null<const Element*> view,\n\t\tnot_null<DocumentData*> document,\n\t\tQByteArray data,\n\t\tQString filepath,\n\t\tbool incoming,\n\t\tStickers::EffectType type) {\n\tconst auto top = _itemTop(view);\n\tconst auto bottom = top + view->height();\n\tif (_visibleTop >= bottom\n\t\t|| _visibleBottom <= top\n\t\t|| _visibleTop == _visibleBottom\n\t\t|| (data.isEmpty() && filepath.isEmpty())) {\n\t\treturn;\n\t}\n\n\tif (!_layer) {\n\t\t_layer = base::make_unique_q<Ui::RpWidget>(_layerParent);\n\t\tconst auto raw = _layer.get();\n\t\traw->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\traw->show();\n\t\traw->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\t\tpaint(raw, clip);\n\t\t}, raw->lifetime());\n\t}\n\trefreshLayerShift();\n\t_layer->setGeometry(_layerParent->rect());\n\n\tauto lottie = document->session().emojiStickersPack().effectPlayer(\n\t\tdocument,\n\t\tdata,\n\t\tfilepath,\n\t\ttype);\n\n\tconst auto inner = (type == Stickers::EffectType::PremiumSticker)\n\t\t? HistoryView::Sticker::Size(document)\n\t\t: HistoryView::Sticker::EmojiSize();\n\tconst auto shift = (type == Stickers::EffectType::EmojiInteraction)\n\t\t? GenerateRandomShift(inner)\n\t\t: QPoint();\n\tconst auto raw = lottie.get();\n\tlottie->updates(\n\t) | rpl::on_next([=](Lottie::Update update) {\n\t\tv::match(update.data, [&](const Lottie::Information &information) {\n\t\t}, [&](const Lottie::DisplayFrameRequest &request) {\n\t\t\tconst auto i = ranges::find(_plays, raw, [](const Play &p) {\n\t\t\t\treturn p.lottie.get();\n\t\t\t});\n\t\t\tauto update = computeRect(*i).translated(shift + _layerShift);\n\t\t\tif (!i->lastTarget.isEmpty()) {\n\t\t\t\tupdate = i->lastTarget.united(update);\n\t\t\t}\n\t\t\t_layer->update(update);\n\t\t\ti->lastTarget = QRect();\n\t\t});\n\t}, lottie->lifetime());\n\t_plays.push_back({\n\t\t.view = view,\n\t\t.lottie = std::move(lottie),\n\t\t.shift = shift,\n\t\t.inner = inner,\n\t\t.outer = ((type == Stickers::EffectType::PremiumSticker)\n\t\t\t? HistoryView::Sticker::PremiumEffectSize(document)\n\t\t\t: (type == Stickers::EffectType::EmojiInteraction)\n\t\t\t? HistoryView::Sticker::EmojiEffectSize()\n\t\t\t: HistoryView::Sticker::MessageEffectSize()),\n\t\t.type = type,\n\t});\n\tif (incoming) {\n\t\t_playStarted.fire(std::move(emoticon));\n\t}\n\tif (const auto media = view->media()) {\n\t\tif (type == Stickers::EffectType::EmojiInteraction) {\n\t\t\tmedia->stickerClearLoopPlayed();\n\t\t}\n\t}\n}\n\nvoid EmojiInteractions::refreshLayerShift() {\n\t_layerShift = Ui::MapFrom(_layerParent, _parent, QPoint(0, 0));\n}\n\nvoid EmojiInteractions::refreshLayerGeometryAndUpdate(QRect rect) {\n\tif (!rect.isEmpty()) {\n\t\t_layer->update(rect.translated(_layerShift));\n\t}\n}\n\nvoid EmojiInteractions::visibleAreaUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\t_visibleTop = visibleTop;\n\t_visibleBottom = visibleBottom;\n}\n\nQRect EmojiInteractions::computeRect(const Play &play) const {\n\tconst auto view = play.view;\n\tconst auto viewTop = _itemTop(view);\n\tif (viewTop < 0) {\n\t\treturn QRect();\n\t}\n\tif (play.type == Stickers::EffectType::MessageEffect) {\n\t\tconst auto icon = view->effectIconGeometry();\n\t\tif (icon.isEmpty()) {\n\t\t\treturn QRect();\n\t\t}\n\t\tconst auto size = play.outer;\n\t\tconst auto shift = view->hasRightLayout()\n\t\t\t? (-size.width() / 3)\n\t\t\t: (size.width() / 3);\n\t\treturn QRect(\n\t\t\tshift + icon.x() + (icon.width() - size.width()) / 2,\n\t\t\tviewTop + icon.y() + (icon.height() - size.height()) / 2,\n\t\t\tsize.width(),\n\t\t\tsize.height());\n\t}\n\tconst auto sticker = play.inner;\n\tconst auto size = play.outer;\n\tconst auto shift = (play.type == Stickers::EffectType::PremiumSticker)\n\t\t? int(sticker.width() * kPremiumShift)\n\t\t: (size.width() / 40);\n\tconst auto inner = view->innerGeometry();\n\tconst auto rightAligned = view->hasRightLayout();\n\tconst auto left = rightAligned\n\t\t? (inner.x() + inner.width() + shift - size.width())\n\t\t: (inner.x() - shift);\n\tconst auto top = viewTop\n\t\t+ inner.y()\n\t\t+ (sticker.height() - size.height()) / 2;\n\treturn QRect(QPoint(left, top), size).translated(play.shift);\n}\n\nvoid EmojiInteractions::paint(not_null<QWidget*> layer, QRect clip) {\n\trefreshLayerShift();\n\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto whole = layer->rect();\n\n\tauto p = QPainter(layer);\n\n\tauto updated = QRect();\n\tconst auto addRect = [&](QRect rect) {\n\t\tif (updated.isEmpty()) {\n\t\t\tupdated = rect;\n\t\t} else {\n\t\t\tupdated = rect.united(updated);\n\t\t}\n\t};\n\tfor (auto &play : _plays) {\n\t\tif (!play.lottie->ready()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto target = computeRect(play).translated(_layerShift);\n\t\tif (!target.intersects(whole)) {\n\t\t\tplay.finished = true;\n\t\t\taddRect(target);\n\t\t\tcontinue;\n\t\t} else if (!target.intersects(clip)) {\n\t\t\tcontinue;\n\t\t}\n\t\tauto request = Lottie::FrameRequest();\n\t\trequest.box = play.outer * factor;\n\t\tconst auto rightAligned = play.view->hasRightLayout();\n\t\tif (!rightAligned) {\n\t\t\trequest.mirrorHorizontal = true;\n\t\t}\n\t\tconst auto frame = play.lottie->frameInfo(request);\n\t\tplay.frame = frame.index;\n\t\tif (!play.framesCount) {\n\t\t\tconst auto &information = play.lottie->information();\n\t\t\tplay.framesCount = information.framesCount;\n\t\t\tplay.frameRate = information.frameRate;\n\t\t}\n\t\tif (play.started && !play.frame) {\n\t\t\tplay.finished = true;\n\t\t\taddRect(target);\n\t\t\tcontinue;\n\t\t} else if (play.frame > 0) {\n\t\t\tplay.started = true;\n\t\t}\n\t\tp.drawImage(\n\t\t\tQRect(target.topLeft(), frame.image.size() / factor),\n\t\t\tframe.image);\n\t\tplay.lottie->markFrameShown();\n\t\tplay.lastTarget = target.translated(_layerShift);\n\t}\n\t_plays.erase(ranges::remove_if(_plays, [](const Play &play) {\n\t\tif (!play.finished) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}), end(_plays));\n\tcheckDelayed();\n\n\tif (_plays.empty()) {\n\t\tlayer->hide();\n\t\tif (_layer.get() == layer) {\n\t\t\tcrl::on_main([moved = std::move(_layer)] {});\n\t\t}\n\t} else if (!updated.isEmpty()) {\n\t\tconst auto translated = updated.translated(_layerShift);\n\t\tif (translated.intersects(whole)) {\n\t\t\t_layer->update(translated);\n\t\t}\n\t}\n}\n\nvoid EmojiInteractions::checkDelayed() {\n\tif (_delayed.empty() || _plays.size() >= kMaxPlays) {\n\t\treturn;\n\t}\n\tauto withTooLittleDelay = false;\n\tauto withHalfPlayed = false;\n\tfor (const auto &play : _plays) {\n\t\tif (!play.framesCount\n\t\t\t|| !play.frameRate\n\t\t\t|| !play.frame\n\t\t\t|| (play.frame * crl::time(1000)\n\t\t\t\t< kSmallDelay * play.frameRate)) {\n\t\t\twithTooLittleDelay = true;\n\t\t\tbreak;\n\t\t} else if (play.frame * 2 > play.framesCount) {\n\t\t\twithHalfPlayed = true;\n\t\t}\n\t}\n\tif (withTooLittleDelay) {\n\t\treturn;\n\t} else if (_plays.size() >= kMaxPlaysWithSmallDelay && !withHalfPlayed) {\n\t\treturn;\n\t}\n\tconst auto now = crl::now();\n\tconst auto i = ranges::find_if(_delayed, [&](const Delayed &delayed) {\n\t\treturn (delayed.shouldHaveStartedAt + kDropDelayedAfterDelay > now);\n\t});\n\tif (i == end(_delayed)) {\n\t\t_delayed.clear();\n\t\treturn;\n\t}\n\tauto good = std::move(*i);\n\t_delayed.erase(begin(_delayed), i + 1);\n\tplay(\n\t\tstd::move(good.emoticon),\n\t\tgood.view,\n\t\tstd::move(good.media),\n\t\tgood.incoming);\n}\n\nrpl::producer<QString> EmojiInteractions::playStarted() const {\n\treturn _playStarted.events();\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_emoji_interactions.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n\nnamespace Data {\nclass DocumentMedia;\n} // namespace Data\n\nnamespace ChatHelpers {\nstruct EmojiInteractionPlayRequest;\n} // namespace ChatHelpers\n\nnamespace Lottie {\nclass SinglePlayer;\n} // namespace Lottie\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Stickers {\nenum class EffectType : uint8;\n} // namespace Stickers\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass Element;\n\nclass EmojiInteractions final {\npublic:\n\tEmojiInteractions(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<QWidget*> layerParent,\n\t\tnot_null<Main::Session*> session,\n\t\tFn<int(not_null<const Element*>)> itemTop);\n\t~EmojiInteractions();\n\n\tvoid play(\n\t\tChatHelpers::EmojiInteractionPlayRequest request,\n\t\tnot_null<Element*> view);\n\tbool playPremiumEffect(\n\t\tnot_null<const Element*> view,\n\t\tElement *replacing);\n\tvoid cancelPremiumEffect(not_null<const Element*> view);\n\tvoid visibleAreaUpdated(int visibleTop, int visibleBottom);\n\n\tvoid playEffectOnRead(not_null<const Element*> view);\n\tvoid playEffect(not_null<const Element*> view);\n\n\tvoid paint(not_null<QWidget*> layer, QRect clip);\n\t[[nodiscard]] rpl::producer<QString> playStarted() const;\n\nprivate:\n\tstruct Play {\n\t\tnot_null<const Element*> view;\n\t\tstd::unique_ptr<Lottie::SinglePlayer> lottie;\n\t\tmutable QRect lastTarget;\n\t\tQPoint shift;\n\t\tQSize inner;\n\t\tQSize outer;\n\t\tint frame = 0;\n\t\tint framesCount = 0;\n\t\tint frameRate = 0;\n\t\tStickers::EffectType type = {};\n\t\tbool started = false;\n\t\tbool finished = false;\n\t};\n\tstruct Delayed {\n\t\tQString emoticon;\n\t\tnot_null<const Element*> view;\n\t\tstd::shared_ptr<Data::DocumentMedia> media;\n\t\tcrl::time shouldHaveStartedAt = 0;\n\t\tbool incoming = false;\n\t};\n\tstruct ResolvedEffect {\n\t\tQString emoticon;\n\t\tDocumentData *document = nullptr;\n\t\tQByteArray content;\n\t\tQString filepath;\n\n\t\texplicit operator bool() const {\n\t\t\treturn document && (!content.isEmpty() || !filepath.isEmpty());\n\t\t}\n\t};\n\n\t[[nodiscard]] QRect computeRect(const Play &play) const;\n\n\tvoid play(\n\t\tQString emoticon,\n\t\tnot_null<const Element*> view,\n\t\tstd::shared_ptr<Data::DocumentMedia> media,\n\t\tbool incoming);\n\tvoid play(\n\t\tQString emoticon,\n\t\tnot_null<const Element*> view,\n\t\tnot_null<DocumentData*> document,\n\t\tQByteArray data,\n\t\tQString filepath,\n\t\tbool incoming,\n\t\tStickers::EffectType type);\n\tvoid checkDelayed();\n\tvoid addPendingEffect(not_null<const Element*> view);\n\n\t[[nodiscard]] ResolvedEffect resolveEffect(\n\t\tnot_null<const Element*> view);\n\tvoid playEffect(\n\t\tnot_null<const Element*> view,\n\t\tconst ResolvedEffect &resolved);\n\tvoid checkPendingEffects();\n\n\tvoid refreshLayerShift();\n\tvoid refreshLayerGeometryAndUpdate(QRect rect);\n\n\tconst not_null<QWidget*> _parent;\n\tconst not_null<QWidget*> _layerParent;\n\tconst not_null<Main::Session*> _session;\n\tconst Fn<int(not_null<const Element*>)> _itemTop;\n\n\tbase::unique_qptr<Ui::RpWidget> _layer;\n\tQPoint _layerShift;\n\tint _visibleTop = 0;\n\tint _visibleBottom = 0;\n\n\tstd::vector<Play> _plays;\n\tstd::vector<Delayed> _delayed;\n\trpl::event_stream<QString> _playStarted;\n\n\tstd::vector<base::weak_ptr<const Element>> _pendingEffects;\n\trpl::lifetime _downloadLifetime;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_empty_list_bubble.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_empty_list_bubble.h\"\n\n#include \"ui/chat/chat_style.h\"\n#include \"ui/painter.h\"\n#include \"history/view/history_view_service_message.h\"\n\nnamespace HistoryView {\n\nEmptyListBubbleWidget::EmptyListBubbleWidget(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<const Ui::ChatStyle*> st,\n\tconst style::margins &padding)\n: RpWidget(parent)\n, _padding(padding)\n, _st(st) {\n\tparent->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tupdateGeometry(s);\n\t}, lifetime());\n}\n\nvoid EmptyListBubbleWidget::updateGeometry(const QSize &size) {\n\tconst auto w = _forceWidth\n\t\t? _forceWidth\n\t\t: std::min(\n\t\t\t_text.maxWidth() + _padding.left() + _padding.right(),\n\t\t\tsize.width());\n\t_innerWidth = w - _padding.left() - _padding.right();\n\tconst auto h = _padding.top()\n\t\t+ _text.countHeight(_innerWidth)\n\t\t+ _padding.bottom();\n\tresize(w, h);\n\tmove((size.width() - w) / 2, (size.height() - h) / 3);\n}\n\nvoid EmptyListBubbleWidget::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tconst auto r = rect();\n\tHistoryView::ServiceMessagePainter::PaintBubble(p, _st, r);\n\n\tp.setPen(_st->msgServiceFg());\n\t_text.draw(\n\t\tp,\n\t\tr.x() + _padding.left(),\n\t\tr.y() + _padding.top(),\n\t\t_innerWidth,\n\t\tstyle::al_top);\n}\n\nvoid EmptyListBubbleWidget::setText(\n\t\tconst TextWithEntities &textWithEntities) {\n\t_text.setMarkedText(st::defaultTextStyle, textWithEntities);\n\tupdateGeometry(size());\n}\n\nvoid EmptyListBubbleWidget::setForceWidth(int width) {\n\tif (_forceWidth != width) {\n\t\t_forceWidth = width;\n\t\tupdateGeometry(size());\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_empty_list_bubble.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\nclass ChatStyle;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass EmptyListBubbleWidget : public Ui::RpWidget {\npublic:\n\tEmptyListBubbleWidget(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tconst style::margins &padding);\n\n\tvoid setText(const TextWithEntities &textWithEntities);\n\tvoid setForceWidth(int width);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid updateGeometry(const QSize &size);\n\n\tconst style::margins &_padding;\n\tconst not_null<const Ui::ChatStyle*> _st;\n\tUi::Text::String _text;\n\tint _innerWidth = 0;\n\tint _forceWidth = 0;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_fake_items.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_fake_items.h\"\n\n#include \"base/unixtime.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n\nnamespace HistoryView {\n\nAdminLog::OwnedItem GenerateItem(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate,\n\t\tnot_null<History*> history,\n\t\tPeerId from,\n\t\tFullMsgId replyTo,\n\t\tconst QString &text,\n\t\tEffectId effectId) {\n\tExpects(history->peer->isUser());\n\n\tconst auto item = history->addNewLocalMessage({\n\t\t.id = history->nextNonHistoryEntryId(),\n\t\t.flags = (MessageFlag::FakeHistoryItem\n\t\t\t| MessageFlag::HasFromId\n\t\t\t| MessageFlag::HasReplyInfo),\n\t\t.from = from,\n\t\t.replyTo = FullReplyTo{.messageId = replyTo },\n\t\t.date = base::unixtime::now(),\n\t\t.effectId = effectId,\n\t}, TextWithEntities{ .text = text }, MTP_messageMediaEmpty());\n\n\treturn AdminLog::OwnedItem(delegate, item);\n}\n\nPeerId GenerateUser(not_null<History*> history, const QString &name) {\n\tExpects(history->peer->isUser());\n\n\tconst auto peerId = Data::FakePeerIdForJustName(name);\n\thistory->owner().processUser(MTP_user(\n\t\tMTP_flags(MTPDuser::Flag::f_first_name | MTPDuser::Flag::f_min),\n\t\tpeerToBareMTPInt(peerId),\n\t\tMTP_long(0),\n\t\tMTP_string(name),\n\t\tMTPstring(), // last name\n\t\tMTPstring(), // username\n\t\tMTPstring(), // phone\n\t\tMTPUserProfilePhoto(), // profile photo\n\t\tMTPUserStatus(), // status\n\t\tMTP_int(0), // bot info version\n\t\tMTPVector<MTPRestrictionReason>(), // restrictions\n\t\tMTPstring(), // bot placeholder\n\t\tMTPstring(), // lang code\n\t\tMTPEmojiStatus(),\n\t\tMTPVector<MTPUsername>(),\n\t\tMTPRecentStory(),\n\t\tMTPPeerColor(), // color\n\t\tMTPPeerColor(), // profile_color\n\t\tMTPint(), // bot_active_users\n\t\tMTPlong(), // bot_verification_icon\n\t\tMTPlong())); // send_paid_messages_stars\n\treturn peerId;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_fake_items.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/admin_log/history_admin_log_item.h\"\n\nnamespace HistoryView {\n\n[[nodiscard]] AdminLog::OwnedItem GenerateItem(\n\tnot_null<HistoryView::ElementDelegate*> delegate,\n\tnot_null<History*> history,\n\tPeerId from,\n\tFullMsgId replyTo,\n\tconst QString &text,\n\tEffectId effectId = 0);\n\n[[nodiscard]] PeerId GenerateUser(\n\tnot_null<History*> history,\n\tconst QString &name);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_group_call_bar.h\"\n\n#include \"data/data_channel.h\"\n#include \"data/data_user.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_peer_values.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/group_call_bar.h\"\n#include \"ui/chat/group_call_userpics.h\"\n#include \"ui/painter.h\"\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/calls_instance.h\"\n#include \"core/application.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace HistoryView {\n\nvoid GenerateUserpicsInRow(\n\t\tQImage &result,\n\t\tconst std::vector<UserpicInRow> &list,\n\t\tconst style::GroupCallUserpics &st,\n\t\tint maxElements) {\n\tconst auto count = int(list.size());\n\tif (!count) {\n\t\tresult = QImage();\n\t\treturn;\n\t}\n\tconst auto limit = std::max(count, maxElements);\n\tconst auto single = st.size;\n\tconst auto shift = st.shift;\n\tconst auto width = single + (limit - 1) * (single - shift);\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (result.width() != width * ratio) {\n\t\tresult = QImage(\n\t\t\tQSize(width, single) * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t}\n\tresult.fill(Qt::transparent);\n\tresult.setDevicePixelRatio(ratio);\n\n\tauto q = Painter(&result);\n\tauto hq = PainterHighQualityEnabler(q);\n\tauto pen = QPen(Qt::transparent);\n\tpen.setWidth(st.stroke);\n\tauto x = (count - 1) * (single - shift);\n\tfor (auto i = count; i != 0;) {\n\t\tauto &entry = list[--i];\n\t\tq.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\tentry.peer->paintUserpic(q, entry.view, x, 0, single, true);\n\t\tentry.uniqueKey = entry.peer->userpicUniqueKey(entry.view);\n\t\tq.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tq.setBrush(Qt::NoBrush);\n\t\tq.setPen(pen);\n\t\tif (style::SquareUserpics()) {\n\t\t\tq.drawRect(x, 0, single, single);\n\t\t} else {\n\t\t\tq.drawEllipse(x, 0, single, single);\n\t\t}\n\t\tx -= single - shift;\n\t}\n}\n\nbool NeedRegenerateUserpics(\n\t\tconst QImage &image,\n\t\tconst std::vector<UserpicInRow> &list) {\n\tif (image.isNull()) {\n\t\treturn true;\n\t}\n\tfor (auto &entry : list) {\n\t\tconst auto peer = entry.peer;\n\t\tauto &view = entry.view;\n\t\tconst auto wasView = view.cloud.get();\n\t\tif (peer->userpicUniqueKey(view) != entry.uniqueKey\n\t\t\t|| view.cloud.get() != wasView) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nPreparedUserpicsInRow PrepareUserpicsInRow(\n\t\tconst std::vector<not_null<PeerData*>> &peers,\n\t\tconst style::GroupCallUserpics &st,\n\t\tint limit) {\n\tauto rows = std::vector<UserpicInRow>();\n\trows.reserve(peers.size());\n\tfor (const auto &peer : peers) {\n\t\trows.push_back({ .peer = peer });\n\t}\n\tauto result = PreparedUserpicsInRow();\n\tif (!rows.empty()) {\n\t\tGenerateUserpicsInRow(result.image, rows, st, limit);\n\t}\n\tresult.width = result.image.isNull()\n\t\t? 0\n\t\t: (result.image.width() / style::DevicePixelRatio());\n\treturn result;\n}\n\nrpl::producer<Ui::GroupCallBarContent> GroupCallBarContentByCall(\n\t\tnot_null<Data::GroupCall*> call,\n\t\tint userpicSize) {\n\tstruct State {\n\t\tstd::vector<UserpicInRow> userpics;\n\t\tUi::GroupCallBarContent current;\n\t\tbase::has_weak_ptr guard;\n\t\tuint64 ownerId = 0;\n\t\tbool someUserpicsNotLoaded = false;\n\t\tbool pushScheduled = false;\n\t\tbool noUserpics = false;\n\t};\n\n\t// speaking DESC, std::max(date, lastActive) DESC\n\tstatic const auto SortKey = [](const Data::GroupCallParticipant &p) {\n\t\tconst auto result = (p.speaking ? uint64(0x100000000ULL) : uint64(0))\n\t\t\t| uint64(std::max(p.lastActive, p.date));\n\t\treturn (~uint64(0)) - result; // sorting with less(), so invert.\n\t};\n\n\tstatic const auto RtmpCallTopBarParticipants = [](\n\t\t\tnot_null<Data::GroupCall*> call) {\n\t\tusing Participant = Data::GroupCallParticipant;\n\t\treturn std::vector<Participant>{ Participant{\n\t\t\t.peer = call->peer(),\n\t\t} };\n\t};\n\n\tconstexpr auto kLimit = 3;\n\tstatic const auto FillMissingUserpics = [](\n\t\t\tnot_null<State*> state,\n\t\t\tnot_null<Data::GroupCall*> call) {\n\t\tconst auto already = int(state->userpics.size());\n\t\tconst auto &participants = call->rtmp()\n\t\t\t? RtmpCallTopBarParticipants(call)\n\t\t\t: call->participants();\n\t\tif (already >= kLimit || participants.size() <= already) {\n\t\t\treturn false;\n\t\t}\n\t\tstd::array<const Data::GroupCallParticipant*, kLimit> adding{\n\t\t\t{ nullptr }\n\t\t};\n\t\tfor (const auto &participant : participants) {\n\t\t\tconst auto alreadyInList = ranges::contains(\n\t\t\t\tstate->userpics,\n\t\t\t\tparticipant.peer,\n\t\t\t\t&UserpicInRow::peer);\n\t\t\tif (alreadyInList) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (auto i = 0; i != kLimit; ++i) {\n\t\t\t\tif (!adding[i]) {\n\t\t\t\t\tadding[i] = &participant;\n\t\t\t\t\tbreak;\n\t\t\t\t} else if (SortKey(participant) < SortKey(*adding[i])) {\n\t\t\t\t\tfor (auto j = kLimit - 1; j != i; --j) {\n\t\t\t\t\t\tadding[j] = adding[j - 1];\n\t\t\t\t\t}\n\t\t\t\t\tadding[i] = &participant;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor (auto i = 0; i != kLimit - already; ++i) {\n\t\t\tif (adding[i]) {\n\t\t\t\tstate->userpics.push_back(UserpicInRow{\n\t\t\t\t\t.peer = adding[i]->peer,\n\t\t\t\t\t.speaking = adding[i]->speaking,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t};\n\n\tstatic const auto RegenerateUserpics = [](\n\t\t\tnot_null<State*> state,\n\t\t\tnot_null<Data::GroupCall*> call,\n\t\t\tint userpicSize,\n\t\t\tbool force = false) {\n\t\tconst auto result = FillMissingUserpics(state, call) || force;\n\t\tif (!result) {\n\t\t\treturn false;\n\t\t}\n\t\tstate->current.users.reserve(state->userpics.size());\n\t\tstate->current.users.clear();\n\t\tstate->someUserpicsNotLoaded = false;\n\t\tfor (auto &userpic : state->userpics) {\n\t\t\tuserpic.peer->loadUserpic();\n\t\t\tauto image = PeerData::GenerateUserpicImage(\n\t\t\t\tuserpic.peer,\n\t\t\t\tuserpic.view,\n\t\t\t\tuserpicSize * style::DevicePixelRatio());\n\t\t\tuserpic.uniqueKey = userpic.peer->userpicUniqueKey(userpic.view);\n\t\t\tstate->current.users.push_back({\n\t\t\t\t.userpic = std::move(image),\n\t\t\t\t.userpicKey = userpic.uniqueKey,\n\t\t\t\t.id = userpic.peer->id.value,\n\t\t\t\t.speaking = userpic.speaking,\n\t\t\t});\n\t\t\tif (userpic.peer->hasUserpic()\n\t\t\t\t&& userpic.peer->useEmptyUserpic(userpic.view)) {\n\t\t\t\tstate->someUserpicsNotLoaded = true;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t};\n\n\tstatic const auto RemoveUserpic = [](\n\t\t\tnot_null<State*> state,\n\t\t\tnot_null<Data::GroupCall*> call,\n\t\t\tnot_null<PeerData*> participantPeer,\n\t\t\tint userpicSize) {\n\t\tconst auto i = ranges::find(\n\t\t\tstate->userpics,\n\t\t\tparticipantPeer,\n\t\t\t&UserpicInRow::peer);\n\t\tif (i == state->userpics.end()) {\n\t\t\treturn false;\n\t\t}\n\t\tstate->userpics.erase(i);\n\t\tRegenerateUserpics(state, call, userpicSize, true);\n\t\treturn true;\n\t};\n\n\tstatic const auto CheckPushToFront = [](\n\t\t\tnot_null<State*> state,\n\t\t\tnot_null<Data::GroupCall*> call,\n\t\t\tnot_null<PeerData*> participantPeer,\n\t\t\tint userpicSize) {\n\t\tExpects(state->userpics.size() <= kLimit);\n\n\t\tif (call->rtmp()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto &participants = call->participants();\n\t\tauto i = begin(state->userpics);\n\n\t\t// Find where to put a new speaking userpic.\n\t\tfor (; i != end(state->userpics); ++i) {\n\t\t\tif (i->peer == participantPeer) {\n\t\t\t\tif (i->speaking) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst auto index = i - begin(state->userpics);\n\t\t\t\tstate->current.users[index].speaking = i->speaking = true;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst auto j = ranges::find(\n\t\t\t\tparticipants,\n\t\t\t\ti->peer,\n\t\t\t\t&Data::GroupCallParticipant::peer);\n\t\t\tif (j == end(participants) || !j->speaking) {\n\t\t\t\t// Found a non-speaking one, put the new speaking one here.\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (i - state->userpics.begin() >= kLimit) {\n\t\t\t// Full kLimit of speaking userpics already.\n\t\t\treturn false;\n\t\t}\n\n\t\t// Add the new speaking to the place we found.\n\t\tconst auto added = state->userpics.insert(i, UserpicInRow{\n\t\t\t.peer = participantPeer,\n\t\t\t.speaking = true,\n\t\t});\n\n\t\t// Remove him from the tail, if he was there.\n\t\tfor (auto i = added + 1; i != state->userpics.end(); ++i) {\n\t\t\tif (i->peer == participantPeer) {\n\t\t\t\tstate->userpics.erase(i);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (state->userpics.size() > kLimit) {\n\t\t\t// Find last non-speaking userpic to remove. It must be there.\n\t\t\tfor (auto i = state->userpics.end() - 1; i != added; --i) {\n\t\t\t\tconst auto j = ranges::find(\n\t\t\t\t\tparticipants,\n\t\t\t\t\ti->peer,\n\t\t\t\t\t&Data::GroupCallParticipant::peer);\n\t\t\t\tif (j == end(participants) || !j->speaking) {\n\t\t\t\t\t// Found a non-speaking one, remove.\n\t\t\t\t\tstate->userpics.erase(i);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tAssert(state->userpics.size() <= kLimit);\n\t\t}\n\t\tRegenerateUserpics(state, call, userpicSize, true);\n\t\treturn true;\n\t};\n\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tauto state = lifetime.make_state<State>();\n\t\tstate->noUserpics = call->listenersHidden();\n\t\tstate->ownerId = call->peer()->id.value;\n\t\tstate->current.shown = true;\n\t\tstate->current.livestream = call->peer()->isBroadcast();\n\n\t\tconst auto pushNext = [=] {\n\t\t\tif (state->pushScheduled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->pushScheduled = true;\n\t\t\tcrl::on_main(&state->guard, [=] {\n\t\t\t\tstate->pushScheduled = false;\n\t\t\t\tauto copy = state->current;\n\t\t\t\tif (state->noUserpics && copy.count > 0) {\n\t\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\t\tcopy.users,\n\t\t\t\t\t\tstate->ownerId,\n\t\t\t\t\t\t&Ui::GroupCallUser::id);\n\t\t\t\t\tif (i != end(copy.users)) {\n\t\t\t\t\t\tcopy.users.erase(i);\n\t\t\t\t\t\t--copy.count;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconsumer.put_next(std::move(copy));\n\t\t\t});\n\t\t};\n\n\t\tusing ParticipantUpdate = Data::GroupCall::ParticipantUpdate;\n\t\tcall->participantUpdated(\n\t\t) | rpl::on_next([=](const ParticipantUpdate &update) {\n\t\t\tconst auto participantPeer = update.now\n\t\t\t\t? update.now->peer\n\t\t\t\t: update.was->peer;\n\t\t\tif (!update.now) {\n\t\t\t\tconst auto removed = RemoveUserpic(\n\t\t\t\t\tstate,\n\t\t\t\t\tcall,\n\t\t\t\t\tparticipantPeer,\n\t\t\t\t\tuserpicSize);\n\t\t\t\tif (removed) {\n\t\t\t\t\tpushNext();\n\t\t\t\t}\n\t\t\t} else if (update.now->speaking\n\t\t\t\t&& (!update.was || !update.was->speaking)) {\n\t\t\t\tconst auto pushed = CheckPushToFront(\n\t\t\t\t\tstate,\n\t\t\t\t\tcall,\n\t\t\t\t\tparticipantPeer,\n\t\t\t\t\tuserpicSize);\n\t\t\t\tif (pushed) {\n\t\t\t\t\tpushNext();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tauto updateSpeakingState = update.was.has_value()\n\t\t\t\t\t&& (update.now->speaking != update.was->speaking);\n\t\t\t\tif (updateSpeakingState) {\n\t\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\t\tstate->userpics,\n\t\t\t\t\t\tparticipantPeer,\n\t\t\t\t\t\t&UserpicInRow::peer);\n\t\t\t\t\tif (i != end(state->userpics)) {\n\t\t\t\t\t\tconst auto index = i - begin(state->userpics);\n\t\t\t\t\t\tstate->current.users[index].speaking\n\t\t\t\t\t\t\t= i->speaking\n\t\t\t\t\t\t\t= update.now->speaking;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tupdateSpeakingState = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (RegenerateUserpics(state, call, userpicSize)\n\t\t\t\t\t|| updateSpeakingState) {\n\t\t\t\t\tpushNext();\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime);\n\n\t\tcall->participantsReloaded(\n\t\t) | rpl::filter([=] {\n\t\t\treturn RegenerateUserpics(state, call, userpicSize);\n\t\t}) | rpl::on_next(pushNext, lifetime);\n\n\t\tcall->peer()->session().downloaderTaskFinished(\n\t\t) | rpl::filter([=] {\n\t\t\treturn state->someUserpicsNotLoaded;\n\t\t}) | rpl::on_next([=] {\n\t\t\tfor (const auto &userpic : state->userpics) {\n\t\t\t\tif (userpic.peer->userpicUniqueKey(userpic.view)\n\t\t\t\t\t!= userpic.uniqueKey) {\n\t\t\t\t\tRegenerateUserpics(state, call, userpicSize, true);\n\t\t\t\t\tpushNext();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime);\n\n\t\tRegenerateUserpics(state, call, userpicSize);\n\n\t\trpl::combine(\n\t\t\tcall->titleValue(),\n\t\t\tcall->scheduleDateValue(),\n\t\t\tcall->fullCountValue()\n\t\t) | rpl::on_next([=](\n\t\t\t\tconst QString &title,\n\t\t\t\tTimeId scheduleDate,\n\t\t\t\tint count) {\n\t\t\tstate->current.title = title;\n\t\t\tstate->current.scheduleDate = scheduleDate;\n\t\t\tstate->current.count = count;\n\t\t\tstate->current.shown = (count > 0) || (scheduleDate != 0);\n\t\t\tconsumer.put_next_copy(state->current);\n\t\t}, lifetime);\n\n\t\treturn lifetime;\n\t};\n}\n\nrpl::producer<Ui::GroupCallBarContent> GroupCallBarContentByPeer(\n\t\tnot_null<PeerData*> peer,\n\t\tint userpicSize,\n\t\tbool showInForum) {\n\tconst auto channel = peer->asChannel();\n\treturn rpl::combine(\n\t\tpeer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tData::PeerUpdate::Flag::GroupCall),\n\t\tCore::App().calls().currentGroupCallValue(),\n\t\t((showInForum || !channel)\n\t\t\t? (rpl::single(false) | rpl::type_erased)\n\t\t\t: Data::PeerFlagValue(channel, ChannelData::Flag::Forum))\n\t) | rpl::map([=](auto, Calls::GroupCall *current, bool hiddenByForum) {\n\t\tconst auto call = peer->groupCall();\n\t\treturn (call\n\t\t\t&& !hiddenByForum\n\t\t\t&& (!current || current->peer() != peer))\n\t\t\t? call\n\t\t\t: nullptr;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::map([=](Data::GroupCall *call)\n\t-> rpl::producer<Ui::GroupCallBarContent> {\n\t\tif (!call) {\n\t\t\treturn rpl::single(Ui::GroupCallBarContent{ .shown = false });\n\t\t}\n\t\tcall->reloadIfStale();\n\t\treturn GroupCallBarContentByCall(call, userpicSize);\n\t}) | rpl::flatten_latest();\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_group_call_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/userpic_view.h\"\n\nnamespace Ui {\nstruct GroupCallBarContent;\n} // namespace Ui\n\nnamespace Data {\nclass GroupCall;\n} // namespace Data\n\nnamespace style {\nstruct GroupCallUserpics;\n} // namespace style\n\nnamespace HistoryView {\n\nstruct UserpicInRow {\n\tnot_null<PeerData*> peer;\n\tbool speaking = false;\n\tmutable Ui::PeerUserpicView view;\n\tmutable InMemoryKey uniqueKey;\n};\n\nvoid GenerateUserpicsInRow(\n\tQImage &result,\n\tconst std::vector<UserpicInRow> &list,\n\tconst style::GroupCallUserpics &st,\n\tint maxElements = 0);\n\n[[nodiscard]] bool NeedRegenerateUserpics(\n\tconst QImage &image,\n\tconst std::vector<UserpicInRow> &list);\n\nstruct PreparedUserpicsInRow {\n\tQImage image;\n\tint width = 0;\n};\n\n[[nodiscard]] PreparedUserpicsInRow PrepareUserpicsInRow(\n\tconst std::vector<not_null<PeerData*>> &peers,\n\tconst style::GroupCallUserpics &st,\n\tint limit = 0);\n\n[[nodiscard]] auto GroupCallBarContentByCall(\n\tnot_null<Data::GroupCall*> call,\n\tint userpicSize)\n-> rpl::producer<Ui::GroupCallBarContent>;\n\n[[nodiscard]] auto GroupCallBarContentByPeer(\n\tnot_null<PeerData*> peer,\n\tint userpicSize,\n\tbool showInForum)\n-> rpl::producer<Ui::GroupCallBarContent>;\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_group_members_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_group_members_widget.h\"\n\n#include \"boxes/peers/edit_participants_box.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nclass GroupMembersWidgetController : public ParticipantsBoxController {\npublic:\n\tusing ParticipantsBoxController::ParticipantsBoxController;\n\nprotected:\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override {\n\t\treturn nullptr;\n\t}\n\n};\n\n} // namespace\n\nGroupMembersWidget::GroupMembersWidget(\nnot_null<Ui::RpWidget*> parent,\nnot_null<Window::SessionNavigation*> navigation,\nnot_null<PeerData*> peer)\n: Ui::RpWidget(parent)\n, _show(navigation->uiShow())\n, _listController(std::make_unique<GroupMembersWidgetController>(\n\t\tnavigation,\n\t\tpeer,\n\t\tParticipantsBoxController::Role::Profile)) {\n\t_listController->setStoriesShown(true);\n\tsetupList();\n\tsetContent(_list.data());\n\t_listController->setDelegate(static_cast<PeerListDelegate*>(this));\n}\n\nvoid GroupMembersWidget::setupList() {\n\tconst auto topSkip = 0;\n\t_listController->setStyleOverrides(&st::groupMembersWidgetList);\n\t_listController->setStoriesShown(true);\n\t_list = object_ptr<PeerListContent>(this, _listController.get());\n\twidthValue() | rpl::on_next([this](int newWidth) {\n\t\tif (newWidth > 0) {\n\t\t\t_list->resizeToWidth(newWidth);\n\t\t}\n\t}, _list->lifetime());\n\t_list->heightValue() | rpl::on_next([=](int listHeight) {\n\t\tif (const auto newHeight = topSkip + listHeight; newHeight > 0) {\n\t\t\tresize(width(), newHeight);\n\t\t}\n\t}, _list->lifetime());\n\t_list->moveToLeft(0, topSkip);\n}\n\nvoid GroupMembersWidget::peerListSetTitle(rpl::producer<QString> title) {\n}\n\nvoid GroupMembersWidget::peerListSetAdditionalTitle(\n\trpl::producer<QString> title) {\n}\n\nbool GroupMembersWidget::peerListIsRowChecked(not_null<PeerListRow*> row) {\n\treturn false;\n}\n\nint GroupMembersWidget::peerListSelectedRowsCount() {\n\treturn 0;\n}\n\nvoid GroupMembersWidget::peerListScrollToTop() {\n}\n\nvoid GroupMembersWidget::peerListAddSelectedPeerInBunch(\n\t\tnot_null<PeerData*> peer) {\n\tUnexpected(\"Item selection in Info::Profile::Members.\");\n}\n\nvoid GroupMembersWidget::peerListAddSelectedRowInBunch(\n\t\tnot_null<PeerListRow*> row) {\n\tUnexpected(\"Item selection in Info::Profile::Members.\");\n}\n\nvoid GroupMembersWidget::peerListFinishSelectedRowsBunch() {\n}\n\nstd::shared_ptr<Main::SessionShow> GroupMembersWidget::peerListUiShow() {\n\treturn _show;\n}\n\nvoid GroupMembersWidget::peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) {\n\tdescription.destroy();\n}\n\nvoid GroupMembersWidget::peerListShowRowMenu(\n\tnot_null<PeerListRow*> row,\n\tbool highlightRow,\n\tFn<void(not_null<Ui::PopupMenu*>)> destroyed) {\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_group_members_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peer_list_box.h\"\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nclass ParticipantsBoxController;\n\nnamespace HistoryView {\n\nclass GroupMembersWidget\n\t: public Ui::RpWidget\n\t, public PeerListContentDelegate {\npublic:\n\tGroupMembersWidget(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer);\n\nprivate:\n\tvoid setupList();\n\n\t// PeerListContentDelegate interface.\n\tvoid peerListSetTitle(rpl::producer<QString> title) override;\n\tvoid peerListSetAdditionalTitle(rpl::producer<QString> title) override;\n\tbool peerListIsRowChecked(not_null<PeerListRow*> row) override;\n\tint peerListSelectedRowsCount() override;\n\tvoid peerListScrollToTop() override;\n\tvoid peerListAddSelectedPeerInBunch(\n\t\tnot_null<PeerData*> peer) override;\n\tvoid peerListAddSelectedRowInBunch(\n\t\tnot_null<PeerListRow*> row) override;\n\tvoid peerListFinishSelectedRowsBunch() override;\n\tvoid peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) override;\n\tstd::shared_ptr<Main::SessionShow> peerListUiShow() override;\n\tvoid peerListShowRowMenu(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool highlightRow,\n\t\tFn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr) override;\n\t// PeerListContentDelegate interface.\n\n\tstd::shared_ptr<Main::SessionShow> _show;\n\tobject_ptr<PeerListContent> _list = { nullptr };\n\tstd::unique_ptr<ParticipantsBoxController> _listController;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_item_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace HistoryView {\n\nstruct ItemPreviewImage {\n\tQImage data;\n\tuint64 cacheKey = 0;\n\n\t[[nodiscard]] bool hasSpoiler() const {\n\t\treturn (cacheKey & 1);\n\t}\n\t[[nodiscard]] bool isEllipse() const {\n\t\treturn (cacheKey & 2);\n\t}\n\n\texplicit operator bool() const {\n\t\treturn !data.isNull();\n\t}\n};\n\nstruct ItemPreview {\n\tenum class Icon {\n\t\tNone,\n\t\tForwardedMessage,\n\t\tReplyToStory,\n\t};\n\tTextWithEntities text;\n\tstd::vector<ItemPreviewImage> images;\n\tint arrowInTextPosition = -1;\n\tint imagesInTextPosition = 0;\n\tstd::any loadingContext;\n\tIcon icon = Icon::None;\n};\n\nstruct ToPreviewOptions {\n\tconst std::vector<ItemPreviewImage> *existing = nullptr;\n\tQStringView searchLowerText;\n\tbool hideSender = false;\n\tbool hideCaption = false;\n\tbool ignoreMessageText = false;\n\tbool generateImages = true;\n\tbool ignoreGroup = false;\n\tbool ignoreTopic = true;\n\tbool spoilerLoginCode = false;\n\tbool translated = false;\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_list_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_list_widget.h\"\n\n#include \"base/unixtime.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"base/qt/qt_common_adapters.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history_item_text.h\"\n#include \"history/history_streamed_drafts.h\"\n#include \"history/view/media/history_view_media.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"history/view/reactions/history_view_reactions.h\"\n#include \"history/view/reactions/history_view_reactions_button.h\"\n#include \"history/view/history_view_reply_button.h\"\n#include \"history/view/reactions/history_view_reactions_selector.h\"\n#include \"history/view/history_view_context_menu.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_emoji_interactions.h\"\n#include \"history/view/history_view_message.h\"\n#include \"history/view/history_view_service_message.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_translate_tracker.h\"\n#include \"history/view/history_view_read_metrics_tracker.h\"\n#include \"history/view/history_view_add_poll_option.h\"\n#include \"history/view/history_view_element_overlay.h\"\n#include \"data/data_poll.h\"\n#include \"history/view/history_view_top_peers_selector.h\"\n#include \"history/view/history_view_quick_action.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"mainwindow.h\"\n#include \"mainwidget.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/core_settings.h\"\n#include \"core/phone_click_handler.h\"\n#include \"apiwrap.h\"\n#include \"api/api_who_reacted.h\"\n#include \"api/api_views.h\"\n#include \"layout/layout_selection.h\"\n#include \"payments/payments_reaction_process.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_adaptive.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_peer_menu.h\"\n#include \"main/main_session.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/inactive_press.h\"\n#include \"ui/effects/message_sending_animation_controller.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/effects/reaction_fly_animation.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"lang/lang_keys.h\"\n#include \"boxes/delete_messages_box.h\"\n#include \"boxes/moderate_messages_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"boxes/peers/edit_participant_box.h\"\n#include \"core/crash_reports.h\"\n#include \"data/components/sponsored_messages.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_document.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_peer_values.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_window.h\" // columnMaximalWidthLeft\n\n#include <QtWidgets/QApplication>\n#include <QtCore/QMimeData>\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kPreloadedScreensCount = 4;\nconstexpr auto kPreloadIfLessThanScreens = 2;\nconstexpr auto kPreloadedScreensCountFull\n\t= kPreloadedScreensCount + 1 + kPreloadedScreensCount;\nconstexpr auto kClearUserpicsAfter = 50;\n\n[[nodiscard]] std::unique_ptr<TranslateTracker> MaybeTranslateTracker(\n\t\tHistory *history) {\n\treturn history ? std::make_unique<TranslateTracker>(history) : nullptr;\n}\n\n[[nodiscard]] std::unique_ptr<ReadMetricsTracker> MaybeReadMetricsTracker(\n\t\tContext context,\n\t\tHistory *history) {\n\tif (!history\n\t\t|| (context != Context::History && context != Context::Replies)) {\n\t\treturn nullptr;\n\t}\n\treturn std::make_unique<ReadMetricsTracker>(history->peer);\n}\n\n} // namespace\n\nWindowListDelegate::WindowListDelegate(\n\tnot_null<Window::SessionController*> window)\n: _window(window) {\n}\n\nnot_null<Window::SessionController*> WindowListDelegate::listWindow() {\n\treturn _window;\n}\n\nnot_null<QWidget*> WindowListDelegate::listEmojiInteractionsParent() {\n\treturn _window->content();\n}\n\nnot_null<const Ui::ChatStyle*> WindowListDelegate::listChatStyle() {\n\treturn _window->chatStyle();\n}\n\nrpl::producer<bool> WindowListDelegate::listChatWideValue() {\n\treturn _window->adaptive().chatWideValue();\n}\n\nauto WindowListDelegate::listMakeReactionsManager(\n\tQWidget *wheelEventsTarget,\n\tFn<void(QRect)> update)\n-> std::unique_ptr<Reactions::Manager> {\n\treturn std::make_unique<Reactions::Manager>(\n\t\twheelEventsTarget,\n\t\tstd::move(update));\n}\n\nvoid WindowListDelegate::listVisibleAreaUpdated() {\n\t_window->floatPlayerAreaUpdated();\n}\n\nstd::shared_ptr<Ui::Show> WindowListDelegate::listUiShow() {\n\treturn _window->uiShow();\n}\n\nvoid WindowListDelegate::listShowPollResults(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context) {\n\t_window->showPollResults(poll, context);\n}\n\nvoid WindowListDelegate::listCancelUploadLayer(not_null<HistoryItem*> item) {\n\t_window->cancelUploadLayer(item);\n}\n\nbool WindowListDelegate::listAnimationsPaused() {\n\treturn _window->isGifPausedAtLeastFor(Window::GifPauseReason::Any);\n}\n\nauto WindowListDelegate::listSendingAnimation()\n-> Ui::MessageSendingAnimationController * {\n\treturn &_window->sendingAnimation();\n}\n\nUi::ChatPaintContext WindowListDelegate::listPreparePaintContext(\n\t\tUi::ChatPaintContextArgs &&args) {\n\treturn _window->preparePaintContext(std::move(args));\n}\n\nbool WindowListDelegate::listMarkingContentRead() {\n\treturn _window->widget()->markingAsRead();\n}\n\nbool WindowListDelegate::listIgnorePaintEvent(QWidget *w, QPaintEvent *e) {\n\treturn _window->contentOverlapped(w, e);\n}\n\nbool WindowListDelegate::listShowReactPremiumError(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst Data::ReactionId &id) {\n\treturn Window::ShowReactPremiumError(_window, item, id);\n}\n\nauto WindowListDelegate::listFillSenderUserpicMenu(PeerId userpicPeerId)\n-> base::unique_qptr<Ui::PopupMenu> {\n\treturn nullptr;\n}\n\nvoid WindowListDelegate::listWindowSetInnerFocus() {\n\t_window->widget()->setInnerFocus();\n}\n\nbool WindowListDelegate::listAllowsDragForward() {\n\treturn _window->adaptive().isOneColumn();\n}\n\nvoid WindowListDelegate::listLaunchDrag(\n\t\tstd::unique_ptr<QMimeData> data,\n\t\tFn<void()> finished) {\n\t_window->widget()->launchDrag(std::move(data), std::move(finished));\n}\n\nListWidget::MouseState::MouseState() : pointState(PointState::Outside) {\n}\n\nListWidget::MouseState::MouseState(\n\tFullMsgId itemId,\n\tint height,\n\tQPoint point,\n\tPointState pointState)\n: itemId(itemId)\n, height(height)\n, point(point)\n, pointState(pointState) {\n}\n\ntemplate <ListWidget::EnumItemsDirection direction, typename Method>\nvoid ListWidget::enumerateItems(Method method) {\n\tconstexpr auto TopToBottom = (direction == EnumItemsDirection::TopToBottom);\n\n\t// No displayed messages in this history.\n\tif (_items.empty()) {\n\t\treturn;\n\t}\n\tif (_visibleBottom <= _itemsTop || _itemsTop + _itemsHeight <= _visibleTop) {\n\t\treturn;\n\t}\n\n\tauto collapseGapsTotal = 0;\n\tfor (const auto &gap : _collapseGaps) {\n\t\tcollapseGapsTotal += gap.height;\n\t}\n\n\tconst auto beginning = begin(_items);\n\tconst auto ending = end(_items);\n\tauto from = TopToBottom\n\t\t? std::lower_bound(\n\t\t\tbeginning,\n\t\t\tending,\n\t\t\t_visibleTop - collapseGapsTotal,\n\t\t\t[this](auto &elem, int top) {\n\t\t\t\treturn this->itemTop(elem) + elem->height() <= top;\n\t\t\t})\n\t\t: std::upper_bound(\n\t\t\tbeginning,\n\t\t\tending,\n\t\t\t_visibleBottom,\n\t\t\t[this](int bottom, auto &elem) {\n\t\t\t\treturn this->itemTop(elem) + elem->height() >= bottom;\n\t\t\t});\n\tauto wasEnd = (from == ending);\n\tif (wasEnd) {\n\t\t--from;\n\t}\n\n\tconst auto gapCount = int(_collapseGaps.size());\n\tauto nextGapIndex = 0;\n\tauto collapseShift = 0;\n\tif (TopToBottom) {\n\t\tconst auto firstTop = itemTop(from->get());\n\t\tfor (; nextGapIndex < gapCount; ++nextGapIndex) {\n\t\t\tif (firstTop < _collapseGaps[nextGapIndex].absY) break;\n\t\t\tcollapseShift += _collapseGaps[nextGapIndex].height;\n\t\t}\n\t} else {\n\t\tcollapseShift = collapseGapsTotal;\n\t\tnextGapIndex = gapCount;\n\t}\n\n\twhile (true) {\n\t\tauto view = from->get();\n\t\tauto logicalTop = itemTop(view);\n\n\t\tif (TopToBottom) {\n\t\t\twhile (nextGapIndex < gapCount) {\n\t\t\t\tconst auto &gap = _collapseGaps[nextGapIndex];\n\t\t\t\tif (logicalTop < gap.absY) break;\n\t\t\t\tcollapseShift += gap.height;\n\t\t\t\t++nextGapIndex;\n\t\t\t}\n\t\t} else {\n\t\t\twhile (nextGapIndex > 0) {\n\t\t\t\tconst auto &gap = _collapseGaps[nextGapIndex - 1];\n\t\t\t\tif (logicalTop >= gap.absY) break;\n\t\t\t\tcollapseShift -= gap.height;\n\t\t\t\t--nextGapIndex;\n\t\t\t}\n\t\t}\n\n\t\tauto itemtop = logicalTop + collapseShift;\n\t\tauto itembottom = itemtop + view->height();\n\n\t\tif (TopToBottom) {\n\t\t\tif (itembottom <= _visibleTop) {\n\t\t\t\tif (++from == ending) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t} else {\n\t\t\tif (itemtop >= _visibleBottom) {\n\t\t\t\tif (from == beginning) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t--from;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\tif (!method(view, itemtop, itembottom)) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Skip all the items that are below / above the visible area.\n\t\tif (TopToBottom) {\n\t\t\tif (itembottom >= _visibleBottom) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else {\n\t\t\tif (itemtop <= _visibleTop) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tif (TopToBottom) {\n\t\t\tif (++from == ending) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t} else {\n\t\t\tif (from == beginning) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t--from;\n\t\t}\n\t}\n}\n\ntemplate <typename Method>\nvoid ListWidget::enumerateUserpics(Method method) {\n\t// Find and remember the top of an attached messages pack\n\t// -1 means we didn't find an attached to next message yet.\n\tint lowestAttachedItemTop = -1;\n\n\tauto userpicCallback = [&](not_null<Element*> view, int itemtop, int itembottom) {\n\t\t// Skip all service messages.\n\t\tif (view->data()->isService()) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (lowestAttachedItemTop < 0 && view->isAttachedToNext()) {\n\t\t\tlowestAttachedItemTop = itemtop + view->marginTop();\n\t\t}\n\n\t\t// Call method on a userpic for all messages that have it and for those who are not showing it\n\t\t// because of their attachment to the next message if they are bottom-most visible.\n\t\tif (view->displayFromPhoto() || (view->hasFromPhoto() && itembottom >= _visibleBottom)) {\n\t\t\tif (lowestAttachedItemTop < 0) {\n\t\t\t\tlowestAttachedItemTop = itemtop + view->marginTop();\n\t\t\t}\n\t\t\t// Attach userpic to the bottom of the visible area with the same margin as the last message.\n\t\t\tauto userpicMinBottomSkip = st::historyPaddingBottom + st::msgMargin.bottom();\n\t\t\tauto userpicBottom = qMin(itembottom - view->marginBottom(), _visibleBottom - userpicMinBottomSkip);\n\n\t\t\t// Do not let the userpic go above the attached messages pack top line.\n\t\t\tuserpicBottom = qMax(userpicBottom, lowestAttachedItemTop + st::msgPhotoSize);\n\n\t\t\t// Call the template callback function that was passed\n\t\t\t// and return if it finished everything it needed.\n\t\t\tif (!method(view, userpicBottom - st::msgPhotoSize)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\t// Forget the found top of the pack, search for the next one from scratch.\n\t\tif (!view->isAttachedToNext()) {\n\t\t\tlowestAttachedItemTop = -1;\n\t\t}\n\n\t\treturn true;\n\t};\n\n\tenumerateItems<EnumItemsDirection::TopToBottom>(userpicCallback);\n}\n\ntemplate <typename Method>\nvoid ListWidget::enumerateDates(Method method) {\n\t// Find and remember the bottom of an single-day messages pack\n\t// -1 means we didn't find a same-day with previous message yet.\n\tauto lowestInOneDayItemBottom = -1;\n\n\tauto dateCallback = [&](not_null<Element*> view, int itemtop, int itembottom) {\n\t\tconst auto item = view->data();\n\t\tif (lowestInOneDayItemBottom < 0 && view->isInOneDayWithPrevious()) {\n\t\t\tlowestInOneDayItemBottom = itembottom - view->marginBottom();\n\t\t}\n\n\t\t// Call method on a date for all messages that have it and for those who are not showing it\n\t\t// because they are in a one day together with the previous message if they are top-most visible.\n\t\tif (view->displayDate() || (!item->isEmpty() && itemtop <= _visibleTop)) {\n\t\t\tif (lowestInOneDayItemBottom < 0) {\n\t\t\t\tlowestInOneDayItemBottom = itembottom - view->marginBottom();\n\t\t\t}\n\t\t\t// Attach date to the top of the visible area with the same margin as it has in service message.\n\t\t\tauto dateTop = qMax(itemtop, _visibleTop) + st::msgServiceMargin.top();\n\n\t\t\t// Do not let the date go below the single-day messages pack bottom line.\n\t\t\tauto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();\n\t\t\tdateTop = qMin(dateTop, lowestInOneDayItemBottom - dateHeight);\n\n\t\t\t// Call the template callback function that was passed\n\t\t\t// and return if it finished everything it needed.\n\t\t\tif (!method(view, itemtop, dateTop)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\t// Forget the found bottom of the pack, search for the next one from scratch.\n\t\tif (!view->isInOneDayWithPrevious()) {\n\t\t\tlowestInOneDayItemBottom = -1;\n\t\t}\n\n\t\treturn true;\n\t};\n\n\tenumerateItems<EnumItemsDirection::BottomToTop>(dateCallback);\n}\n\nListWidget::ListWidget(\n\tQWidget *parent,\n\tnot_null<Main::Session*> session,\n\tnot_null<ListDelegate*> delegate)\n: RpWidget(parent)\n, _delegate(delegate)\n, _session(session)\n, _emojiInteractions(std::make_unique<EmojiInteractions>(\n\tthis,\n\t_delegate->listEmojiInteractionsParent(),\n\tsession,\n\t[=](not_null<const Element*> view) { return itemTop(view); }))\n, _context(_delegate->listContext())\n, _itemAverageHeight(itemMinimalHeight())\n, _pathGradient(\n\tMakePathShiftGradient(\n\t\t_delegate->listChatStyle(),\n\t\t[=] { update(); }))\n, _reactionsManager(_delegate->listMakeReactionsManager(\n\tthis,\n\t[=](QRect updated) { update(updated); }))\n, _replyButtonManager(std::make_unique<ReplyButton::Manager>(\n\t[=](QRect updated) { update(updated); }))\n, _translateTracker(MaybeTranslateTracker(_delegate->listTranslateHistory()))\n, _readMetricsTracker(MaybeReadMetricsTracker(\n\t_delegate->listContext(),\n\t_delegate->listTranslateHistory()))\n, _scrollDateCheck([this] { scrollDateCheck(); })\n, _applyUpdatedScrollState([this] { applyUpdatedScrollState(); })\n, _selectEnabled(_delegate->listAllowsMultiSelect())\n, _highlighter(\n\t&_session->data(),\n\t[=](const HistoryItem *item) { return viewForItem(item); },\n\t[=](const Element *view) { repaintItem(view); })\n, _touchSelectTimer([=] { onTouchSelect(); })\n, _touchScrollTimer([=] { onTouchScrollTimer(); })\n, _middleClickAutoscroll(\n\t\t[=](int d) { _delegate->listScrollTo(_visibleTop + d, false); },\n\t\t[=](const QCursor &cursor) { setCursor(cursor); },\n\t\t[=] { mouseActionUpdate(QCursor::pos()); setCursor(_cursor); },\n\t\t[=] { return window()->isActiveWindow(); }) {\n\tsetAttribute(Qt::WA_AcceptTouchEvents);\n\tsetMouseTracking(true);\n\tif (_readMetricsTracker) {\n\t\tCore::App().inAppKeyPressed(\n\t\t) | rpl::on_next([=] {\n\t\t\tregisterReadMetricsActivity();\n\t\t}, lifetime());\n\t}\n\n\t_scrollDateHideTimer.setCallback([this] { scrollDateHideByTimer(); });\n\t_session->data().viewRepaintRequest(\n\t) | rpl::on_next([this](Data::RequestViewRepaint data) {\n\t\tif (data.view->delegate() == this) {\n\t\t\trepaintItem(data.view, data.rect);\n\t\t}\n\t}, lifetime());\n\t_session->data().viewResizeRequest(\n\t) | rpl::on_next([this](auto view) {\n\t\tif (view->delegate() == this) {\n\t\t\tresizeItem(view);\n\t\t}\n\t}, lifetime());\n\t_session->data().viewHeightAdjusted(\n\t) | rpl::on_next([this](Data::Session::ViewHeightAdjusted data) {\n\t\tif (data.view->delegate() == this) {\n\t\t\tviewHeightAdjusted(data.view);\n\t\t}\n\t}, lifetime());\n\t_session->data().itemViewRefreshRequest(\n\t) | rpl::on_next([this](auto item) {\n\t\tif (const auto view = viewForItem(item)) {\n\t\t\trefreshItem(view);\n\t\t}\n\t}, lifetime());\n\t_session->data().itemShowHighlightRequest(\n\t) | rpl::on_next([this](auto item) {\n\t\tshowItemHighlight(item);\n\t}, lifetime());\n\t_session->data().viewLayoutChanged(\n\t) | rpl::on_next([this](auto view) {\n\t\tif (view->delegate() == this) {\n\t\t\tmarkReadMetricsStale();\n\t\t\tif (view->isUnderCursor()) {\n\t\t\t\tmouseActionUpdate();\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\t_session->data().itemDataChanges(\n\t) | rpl::on_next([=](not_null<HistoryItem*> item) {\n\t\tif (const auto view = viewForItem(item)) {\n\t\t\tview->itemDataChanged();\n\t\t}\n\t}, lifetime());\n\n\t_session->downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\t_session->data().peerDecorationsUpdated(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\t_session->data().itemRemoved(\n\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\titemRemoved(item);\n\t}, lifetime());\n\n\tsetupThanosEffect();\n\n\tusing MessageUpdateFlag = Data::MessageUpdate::Flag;\n\t_session->changes().realtimeMessageUpdates(\n\t\tMessageUpdateFlag::NewUnreadReaction\n\t) | rpl::on_next([=](const Data::MessageUpdate &update) {\n\t\tmaybeMarkReactionsRead(update.item);\n\t}, lifetime());\n\n\tif (const auto history = _delegate->listTranslateHistory()) {\n\t\t_session->changes().historyUpdates(\n\t\t\thistory,\n\t\t\tData::HistoryUpdate::Flag::TranslatedTo\n\t\t) | rpl::on_next([=] {\n\t\t\tupdate();\n\t\t}, lifetime());\n\t}\n\n\t_session->data().itemVisibilityQueries(\n\t) | rpl::on_next([=](\n\t\t\tconst Data::Session::ItemVisibilityQuery &query) {\n\t\tif (const auto view = viewForItem(query.item)) {\n\t\t\tconst auto top = itemTop(view);\n\t\t\tif (top >= 0\n\t\t\t\t&& top + view->height() > _visibleTop\n\t\t\t\t&& top < _visibleBottom) {\n\t\t\t\t*query.isVisible = true;\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\tif (_reactionsManager) {\n\t\t_reactionsManager->chosen(\n\t\t) | rpl::on_next([=](ChosenReaction reaction) {\n\t\t\t_reactionsManager->updateButton({});\n\t\t\treactionChosen(reaction);\n\t\t}, lifetime());\n\n\t\tReactions::SetupManagerList(\n\t\t\t_reactionsManager.get(),\n\t\t\t_reactionsItem.value());\n\n\t\tCore::App().settings().cornerReactionValue(\n\t\t) | rpl::on_next([=](bool value) {\n\t\t\t_useCornerReaction = value;\n\t\t\tif (!value) {\n\t\t\t\t_reactionsManager->updateButton({});\n\t\t\t}\n\t\t}, lifetime());\n\t}\n\n\tif (_replyButtonManager) {\n\t\tCore::App().settings().cornerReplyValue(\n\t\t) | rpl::on_next([=](bool value) {\n\t\t\t_useCornerReply = value;\n\t\t\tif (!value) {\n\t\t\t\t_replyButtonManager->updateButton({});\n\t\t\t}\n\t\t}, lifetime());\n\t}\n\n\t_delegate->listChatWideValue(\n\t) | rpl::on_next([=](bool wide) {\n\t\t_isChatWide = wide;\n\t\tif (_overlayHost) {\n\t\t\t_overlayHost->hide();\n\t\t}\n\t}, lifetime());\n\n\t_selectScroll.scrolls(\n\t) | rpl::on_next([=](int d) {\n\t\tdelegate->listScrollTo(_visibleTop + d, false);\n\t}, lifetime());\n}\n\nMain::Session &ListWidget::session() const {\n\treturn *_session;\n}\n\nnot_null<Window::SessionController*> ListWidget::controller() const {\n\treturn _delegate->listWindow();\n}\n\nnot_null<ListDelegate*> ListWidget::delegate() const {\n\treturn _delegate;\n}\n\nvoid ListWidget::refreshViewer() {\n\t_viewerLifetime.destroy();\n\t_refreshingViewer = true;\n\t_delegate->listSource(\n\t\t_aroundPosition,\n\t\t_idsLimit,\n\t\t_idsLimit\n\t) | rpl::on_next([=](Data::MessagesSlice &&slice) {\n\t\t_refreshingViewer = false;\n\t\tstd::swap(_slice, slice);\n\t\trefreshRows(slice);\n\t}, _viewerLifetime);\n}\n\nvoid ListWidget::setGeometryCrashAnnotations(not_null<Element*> view) {\n\tCrashReports::SetAnnotation(\n\t\t\"Geometry\",\n\t\tu\"size: %1x%2, visibleTop: %3, visibleBottom: %4, top: %5\"_q\n\t\t.arg(width())\n\t\t.arg(height())\n\t\t.arg(_visibleTop)\n\t\t.arg(_visibleBottom)\n\t\t.arg(_itemsTop));\n\tconst auto logItems = [&] {\n\t\tauto items = QStringList();\n\t\tauto top = _itemsTop;\n\t\tauto index = 0;\n\t\tfor (const auto &some : _items) {\n\t\t\titems.push_back(u\"(%1)%2=%3,%4,%5\"_q\n\t\t\t\t.arg(index++)\n\t\t\t\t.arg(top)\n\t\t\t\t.arg(itemTop(some))\n\t\t\t\t.arg(some->y())\n\t\t\t\t.arg(some->height()));\n\t\t\ttop += some->height();\n\t\t}\n\t\treturn items.join(';');\n\t};\n\tCrashReports::SetAnnotation(\"Chosen\", u\"%1,%2,%3\"_q\n\t\t.arg(itemTop(view))\n\t\t.arg(view->y())\n\t\t.arg(view->height()));\n\tCrashReports::SetAnnotation(\"Before\", logItems());\n\tupdateSize();\n\tCrashReports::SetAnnotation(\"After\", logItems());\n}\n\nvoid ListWidget::refreshRows(const Data::MessagesSlice &old) {\n\tExpects(_viewsCapacity.empty());\n\n\tif (_thanosController) {\n\t\t_thanosController->clearPreCaptured();\n\t}\n\n\tsaveScrollState();\n\n\tconst auto scrolledTillEnd = _itemsKnownTillEnd\n\t\t&& (_visibleBottom == height())\n\t\t&& (_visibleBottom > _visibleTop);\n\n\tconst auto addedToEndFrom = (old.skippedAfter == 0\n\t\t&& (_slice.skippedAfter == 0)\n\t\t&& !old.ids.empty())\n\t\t? ranges::find(_slice.ids, old.ids.back())\n\t\t: end(_slice.ids);\n\tconst auto addedToEndCount = std::max(\n\t\tint(end(_slice.ids) - addedToEndFrom),\n\t\t1\n\t) - 1;\n\n\tauto destroyingBarElement = _bar.element;\n\tauto clearingOverElement = _overElement;\n\t_itemsKnownTillEnd = (_slice.skippedAfter == 0);\n\t_resizePending = true;\n\t_items.clear();\n\t_items.reserve(_slice.ids.size());\n\tstd::swap(_views, _viewsCapacity);\n\tauto nearestIndex = -1;\n\tfor (const auto &fullId : _slice.ids) {\n\t\tif (const auto item = session().data().message(fullId)) {\n\t\t\tif (_slice.nearestToAround == fullId) {\n\t\t\t\tnearestIndex = int(_items.size());\n\t\t\t}\n\t\t\tconst auto view = enforceViewForItem(item, _viewsCapacity);\n\t\t\t_items.push_back(view);\n\t\t\tif (destroyingBarElement == view) {\n\t\t\t\tdestroyingBarElement = nullptr;\n\t\t\t}\n\t\t\tif (clearingOverElement == view) {\n\t\t\t\tclearingOverElement = nullptr;\n\t\t\t}\n\t\t}\n\t}\n\tif (_translateTracker) {\n\t\t_translateTracker->addBunchFrom(_items);\n\t}\n\tfor (auto e = end(_items), i = e - addedToEndCount; i != e; ++i) {\n\t\tconst auto item = (*i)->data();\n\t\tif (!item->history()->streamedDrafts().hasFor(item)) {\n\t\t\t_itemRevealPending.emplace(*i);\n\t\t}\n\t}\n\tupdateAroundPositionFromNearest(nearestIndex);\n\n\tupdateItemsGeometry();\n\n\tif (clearingOverElement) {\n\t\t_overElement = nullptr;\n\t}\n\tif (destroyingBarElement) {\n\t\tdestroyingBarElement->destroyUnreadBar();\n\t\t_bar = {};\n\t}\n\n\tfor (const auto &[item, view] : _viewsCapacity) {\n\t\tif (const auto raw = view.get()) {\n\t\t\tviewReplaced(raw, nullptr);\n\t\t}\n\t}\n\t_viewsCapacity.clear();\n\n\tconst auto markLastAsRead = (scrolledTillEnd && markingMessagesRead());\n\tcheckUnreadBarCreation(markLastAsRead);\n\trestoreScrollState();\n\tif (!_itemsRevealHeight) {\n\t\tmouseActionUpdate(QCursor::pos());\n\t}\n\tif (_emptyInfo) {\n\t\t_emptyInfo->setVisible(isEmpty());\n\t}\n\tcheckActivation();\n}\n\nstd::optional<int> ListWidget::scrollTopForPosition(\n\t\tData::MessagePosition position) const {\n\tauto messageUnknown = !position.date && position.fullId;\n\tif (messageUnknown) {\n\t\tif (const auto item = session().data().message(position.fullId)) {\n\t\t\tposition = item->position();\n\t\t\tmessageUnknown = false;\n\t\t}\n\t}\n\tif (position == Data::UnreadMessagePosition) {\n\t\tif (_bar.element && !_bar.hidden && _bar.focus) {\n\t\t\tconst auto shift = st::lineWidth + st::historyUnreadBarMargin;\n\t\t\treturn itemTop(_bar.element) + shift;\n\t\t}\n\t\tposition = Data::MaxMessagePosition;\n\t}\n\tif (_visibleTop >= _visibleBottom) {\n\t\treturn std::nullopt;\n\t} else if (position == Data::MaxMessagePosition) {\n\t\tif (loadedAtBottom()) {\n\t\t\treturn height() - (_visibleBottom - _visibleTop);\n\t\t}\n\t\treturn std::nullopt;\n\t} else if (!_items.empty()\n\t\t&& (_aroundPosition == position\n\t\t\t|| _initialAroundPosition == position)\n\t\t&& messageUnknown) {\n\t\tif (_refreshingViewer) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\tconst auto available = _visibleBottom - _visibleTop;\n\t\treturn std::max((height() / 2) - available / 2, 0);\n\t} else if (_items.empty()\n\t\t|| isBelowPosition(position)\n\t\t|| isAbovePosition(position)) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto index = findNearestItem(position);\n\tconst auto view = _items[index];\n\treturn scrollTopForView(view);\n}\n\nstd::optional<int> ListWidget::scrollTopForView(\n\t\tnot_null<Element*> view) const {\n\tif (view->isHiddenByGroup()) {\n\t\tif (const auto group = session().data().groups().find(view->data())) {\n\t\t\tif (const auto leader = viewForItem(group->items.front())) {\n\t\t\t\tif (!leader->isHiddenByGroup()) {\n\t\t\t\t\treturn scrollTopForView(leader);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tconst auto top = view->y();\n\tconst auto height = view->height();\n\tconst auto available = _visibleBottom - _visibleTop;\n\tconst auto heightLeft = (available - height);\n\tif (heightLeft >= 0) {\n\t\treturn std::max(top - (heightLeft / 2), 0);\n\t}\n\tconst auto highlight = _highlighter.state(view->data());\n\tif (const auto range = FindHighlightYRange(view, highlight)) {\n\t\treturn AdjustScrollForRange(top, available, range);\n\t}\n\treturn top;\n}\n\nvoid ListWidget::scrollTo(\n\t\tint scrollTop,\n\t\tData::MessagePosition attachPosition,\n\t\tint delta,\n\t\tAnimatedScroll type) {\n\t_scrollToAnimation.stop();\n\tif (!delta || _items.empty() || type == AnimatedScroll::None) {\n\t\t_delegate->listScrollTo(scrollTop);\n\t\treturn;\n\t}\n\tconst auto transition = (type == AnimatedScroll::Full)\n\t\t? anim::sineInOut\n\t\t: anim::easeOutCubic;\n\tif (delta > 0 && scrollTop == height() - (_visibleBottom - _visibleTop)) {\n\t\t// Animated scroll to bottom.\n\t\t_scrollToAnimation.start(\n\t\t\t[=] { scrollToAnimationCallback(FullMsgId(), 0); },\n\t\t\t-delta,\n\t\t\t0,\n\t\t\tst::slideDuration,\n\t\t\ttransition);\n\t\treturn;\n\t}\n\tconst auto index = findNearestItem(attachPosition);\n\tAssert(index >= 0 && index < int(_items.size()));\n\tconst auto attachTo = _items[index];\n\tconst auto attachToId = attachTo->data()->fullId();\n\tconst auto initial = scrollTop - delta;\n\t_delegate->listScrollTo(initial);\n\n\tconst auto attachToTop = itemTop(attachTo);\n\tconst auto relativeStart = initial - attachToTop;\n\tconst auto relativeFinish = scrollTop - attachToTop;\n\t_scrollToAnimation.start(\n\t\t[=] { scrollToAnimationCallback(attachToId, relativeFinish); },\n\t\trelativeStart,\n\t\trelativeFinish,\n\t\tst::slideDuration,\n\t\ttransition);\n}\n\nbool ListWidget::animatedScrolling() const {\n\treturn _scrollToAnimation.animating();\n}\n\nvoid ListWidget::scrollToAnimationCallback(\n\t\tFullMsgId attachToId,\n\t\tint relativeTo) {\n\tif (!attachToId) {\n\t\t// Animated scroll to bottom.\n\t\tconst auto current = int(base::SafeRound(\n\t\t\t_scrollToAnimation.value(0)));\n\t\t_delegate->listScrollTo(height()\n\t\t\t- (_visibleBottom - _visibleTop)\n\t\t\t+ current);\n\t\treturn;\n\t}\n\tconst auto attachTo = session().data().message(attachToId);\n\tconst auto attachToView = viewForItem(attachTo);\n\tif (!attachToView) {\n\t\t_scrollToAnimation.stop();\n\t} else {\n\t\tconst auto current = int(base::SafeRound(_scrollToAnimation.value(\n\t\t\trelativeTo)));\n\t\t_delegate->listScrollTo(itemTop(attachToView) + current);\n\t}\n}\n\nbool ListWidget::isAbovePosition(Data::MessagePosition position) const {\n\tif (_items.empty() || loadedAtBottom()) {\n\t\treturn false;\n\t}\n\treturn _items.back()->data()->position() < position;\n}\n\nbool ListWidget::isBelowPosition(Data::MessagePosition position) const {\n\tif (_items.empty() || loadedAtTop()) {\n\t\treturn false;\n\t}\n\treturn _items.front()->data()->position() > position;\n}\n\nvoid ListWidget::highlightMessage(\n\t\tFullMsgId itemId,\n\t\tconst MessageHighlightId &highlight) {\n\tif (const auto view = viewForItem(itemId)) {\n\t\t_highlighter.highlight({ view->data(), highlight });\n\t}\n}\n\nvoid ListWidget::showAroundPosition(\n\t\tData::MessagePosition position,\n\t\tFn<bool()> overrideInitialScroll) {\n\t_aroundPosition = position;\n\t_aroundIndex = -1;\n\t_overrideInitialScroll = std::move(overrideInitialScroll);\n\trefreshViewer();\n}\n\nbool ListWidget::jumpToBottomInsteadOfUnread() const {\n\t// If we want to jump to unread, but we're at the unread already,\n\t// then jump to the end of the list.\n\t//\n\t// That means there is no read inbox messages below us.\n\tconst auto firstReadMessage = [&]() -> Element* {\n\t\tfor (const auto &view : ranges::views::reverse(_items)) {\n\t\t\tconst auto item = view->data();\n\t\t\tif (item->isRegular()\n\t\t\t\t&& (item->out()\n\t\t\t\t\t|| !_delegate->listElementShownUnread(view))) {\n\t\t\t\treturn view;\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t}();\n\treturn !firstReadMessage || (itemTop(firstReadMessage) < _visibleBottom);\n}\n\nvoid ListWidget::showAtPosition(\n\t\tData::MessagePosition position,\n\t\tconst Window::SectionShow &params,\n\t\tFn<void(bool found)> done) {\n\tconst auto showAtUnread = (position == Data::UnreadMessagePosition);\n\n\tif (showAtUnread && jumpToBottomInsteadOfUnread()) {\n\t\tshowAtPosition(Data::MaxMessagePosition, params, std::move(done));\n\t\treturn;\n\t}\n\n\tif (position.fullId.peer && position.fullId.msg) {\n\t\tif (const auto item = session().data().message(position.fullId)) {\n\t\t\tposition = item->position();\n\t\t}\n\t}\n\n\tif (showAtUnread) {\n\t\tshowAroundPosition(position, [=] {\n\t\t\tif (_bar.element) {\n\t\t\t\t_bar.element->destroyUnreadBar();\n\t\t\t\tconst auto i = ranges::find(_items, not_null{ _bar.element });\n\t\t\t\tAssert(i != end(_items));\n\t\t\t\trefreshAttachmentsAtIndex(i - begin(_items));\n\t\t\t\t_bar = {};\n\t\t\t}\n\t\t\tcheckUnreadBarCreation();\n\t\t\treturn showAtPositionNow(position, params, done);\n\t\t});\n\t} else if (!showAtPositionNow(position, params, done)) {\n\t\tshowAroundPosition(position, [=] {\n\t\t\treturn showAtPositionNow(position, params, done);\n\t\t});\n\t}\n}\n\nbool ListWidget::showAtPositionNow(\n\t\tData::MessagePosition position,\n\t\tconst Window::SectionShow &params,\n\t\tFn<void(bool found)> done) {\n\tauto scrollTop = scrollTopForPosition(position);\n\tif (!scrollTop.has_value()) {\n\t\treturn false;\n\t}\n\tif (position != Data::MaxMessagePosition\n\t\t&& position != Data::UnreadMessagePosition) {\n\t\tconst auto item = session().data().message(position.fullId);\n\t\tconst auto mayScrollToPart = !params.highlight.empty()\n\t\t\t|| (item && item->groupId());\n\t\thighlightMessage(position.fullId, params.highlight);\n\t\tif (mayScrollToPart) {\n\t\t\tscrollTop = scrollTopForPosition(position);\n\t\t\tAssert(scrollTop.has_value());\n\t\t}\n\t}\n\tcomputeScrollTo(*scrollTop, position, params.animated);\n\tif (done) {\n\t\tconst auto found = !position.fullId.peer\n\t\t\t|| !IsServerMsgId(position.fullId.msg)\n\t\t\t|| viewForItem(position.fullId);\n\t\tdone(found);\n\t}\n\treturn true;\n}\n\nvoid ListWidget::computeScrollTo(\n\t\tint to,\n\t\tData::MessagePosition position,\n\t\tanim::type animated) {\n\tconst auto currentScrollHeight = (_visibleBottom - _visibleTop);\n\tconst auto currentScrollTop = _visibleTop;\n\tconst auto wanted = std::max(\n\t\tstd::min(to, height() - currentScrollHeight),\n\t\t0);\n\tconst auto fullDelta = (wanted - currentScrollTop);\n\tconst auto limit = currentScrollHeight;\n\tconst auto scrollDelta = std::clamp(fullDelta, -limit, limit);\n\tconst auto type = (animated == anim::type::instant)\n\t\t? AnimatedScroll::None\n\t\t: (std::abs(fullDelta) > limit)\n\t\t? AnimatedScroll::Part\n\t\t: AnimatedScroll::Full;\n\tscrollTo(wanted, position, scrollDelta, type);\n}\n\nvoid ListWidget::checkUnreadBarCreation(bool markLastAsRead) {\n\tif (_bar.element) {\n\t\treturn;\n\t}\n\tauto data = _delegate->listMessagesBar(_items, markLastAsRead);\n\tif (!data.bar.element) {\n\t\treturn;\n\t}\n\t_bar = std::move(data.bar);\n\t_barText = std::move(data.text);\n\tif (!_bar.hidden) {\n\t\t_bar.element->createUnreadBar(_barText.value());\n\t\tconst auto i = ranges::find(_items, not_null{ _bar.element });\n\t\tAssert(i != end(_items));\n\t\trefreshAttachmentsAtIndex(i - begin(_items));\n\t}\n}\n\nvoid ListWidget::saveScrollState() {\n\tif (!_scrollTopState.item) {\n\t\t_scrollTopState = countScrollState();\n\t}\n}\n\nvoid ListWidget::restoreScrollState() {\n\tif (_items.empty()) {\n\t\treturn;\n\t} else if (_overrideInitialScroll\n\t\t&& base::take(_overrideInitialScroll)()) {\n\t\t_scrollTopState = ScrollTopState();\n\t\t_scrollInited = true;\n\t\treturn;\n\t}\n\tif (!_scrollTopState.item) {\n\t\tif (!_bar.element || _bar.hidden || !_bar.focus || _scrollInited) {\n\t\t\treturn;\n\t\t}\n\t\t_scrollInited = true;\n\t\t_scrollTopState.item = _bar.element->data()->position();\n\t\t_scrollTopState.shift = st::lineWidth\n\t\t\t+ st::historyUnreadBarMargin\n\t\t\t+ _bar.element->displayedDateHeight();\n\t}\n\tconst auto index = findNearestItem(_scrollTopState.item);\n\tif (index >= 0) {\n\t\tconst auto view = _items[index];\n\t\tauto newVisibleTop = itemTop(view) + _scrollTopState.shift;\n\t\tif (_visibleTop != newVisibleTop) {\n\t\t\t_delegate->listScrollTo(newVisibleTop);\n\t\t}\n\t}\n\t_scrollTopState = ScrollTopState();\n}\n\nElement *ListWidget::viewForItem(FullMsgId itemId) const {\n\tif (const auto item = session().data().message(itemId)) {\n\t\treturn viewForItem(item);\n\t}\n\treturn nullptr;\n}\n\nElement *ListWidget::viewForItem(const HistoryItem *item) const {\n\tif (item) {\n\t\tif (const auto i = _views.find(item); i != _views.end()) {\n\t\t\treturn i->second.get();\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nnot_null<Element*> ListWidget::enforceViewForItem(\n\t\tnot_null<HistoryItem*> item,\n\t\tViewsMap &old) {\n\tif (const auto i = old.find(item); i != end(old)) {\n\t\tif (i->second) {\n\t\t\treturn _views.emplace(\n\t\t\t\titem,\n\t\t\t\tbase::take(i->second)).first->second.get();\n\t\t} else if (const auto j = _views.find(item); j != end(_views)) {\n\t\t\treturn j->second.get();\n\t\t}\n\t}\n\tconst auto &[i, ok] = _views.emplace(\n\t\titem,\n\t\titem->createView(this));\n\treturn i->second.get();\n}\n\nvoid ListWidget::updateAroundPositionFromNearest(int nearestIndex) {\n\tif (nearestIndex < 0) {\n\t\t_aroundIndex = -1;\n\t\treturn;\n\t}\n\tconst auto isGoodIndex = [&](int index) {\n\t\tExpects(index >= 0 && index < _items.size());\n\n\t\treturn _delegate->listIsGoodForAroundPosition(_items[index]);\n\t};\n\t_aroundIndex = [&] {\n\t\tfor (auto index = nearestIndex; index < _items.size(); ++index) {\n\t\t\tif (isGoodIndex(index)) {\n\t\t\t\treturn index;\n\t\t\t}\n\t\t}\n\t\tfor (auto index = nearestIndex; index != 0;) {\n\t\t\tif (isGoodIndex(--index)) {\n\t\t\t\treturn index;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t}();\n\tif (_aroundIndex < 0) {\n\t\treturn;\n\t}\n\tconst auto newPosition = _items[_aroundIndex]->data()->position();\n\tif (_aroundPosition != newPosition) {\n\t\t_initialAroundPosition = _aroundPosition;\n\t\t_aroundPosition = newPosition;\n\t\tcrl::on_main(this, [=] { refreshViewer(); });\n\t}\n}\n\nElement *ListWidget::viewByPosition(Data::MessagePosition position) const {\n\tconst auto index = findNearestItem(position);\n\tconst auto result = (index < 0) ? nullptr : _items[index].get();\n\treturn (position == Data::MinMessagePosition\n\t\t|| position == Data::MaxMessagePosition\n\t\t|| (result && result->data()->position() == position))\n\t\t? result\n\t\t: nullptr;\n}\n\nint ListWidget::findNearestItem(Data::MessagePosition position) const {\n\tif (_items.empty()) {\n\t\treturn -1;\n\t}\n\tconst auto after = ranges::find_if(\n\t\t_items,\n\t\t[&](not_null<Element*> view) {\n\t\t\treturn (view->data()->position() >= position);\n\t\t});\n\treturn (after == end(_items))\n\t\t? int(_items.size() - 1)\n\t\t: int(after - begin(_items));\n}\n\nvoid ListWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tif (!(visibleTop < visibleBottom)) {\n\t\treturn;\n\t}\n\n\tconst auto initializing = !(_visibleTop < _visibleBottom);\n\tconst auto scrolledUp = (visibleTop < _visibleTop);\n\t_visibleTop = visibleTop;\n\t_visibleBottom = visibleBottom;\n\tmarkReadMetricsStale();\n\tregisterReadMetricsActivity();\n\n\t// Unload userpics.\n\tif (_userpics.size() > kClearUserpicsAfter) {\n\t\t_userpicsCache = std::move(_userpics);\n\t}\n\n\tif (initializing) {\n\t\tcheckUnreadBarCreation();\n\t}\n\tupdateVisibleTopItem();\n\tif (scrolledUp) {\n\t\t_scrollDateCheck.call();\n\t} else {\n\t\tscrollDateHideByTimer();\n\t}\n\t_delegate->listVisibleAreaUpdated();\n\tsession().data().itemVisibilitiesUpdated();\n\t_applyUpdatedScrollState.call();\n\n\t_emojiInteractions->visibleAreaUpdated(_visibleTop, _visibleBottom);\n\tif (_overlayHost) {\n\t\t_overlayHost->updatePosition();\n\t}\n}\n\nvoid ListWidget::applyUpdatedScrollState() {\n\tcheckMoveToOtherViewer();\n}\n\nvoid ListWidget::updateVisibleTopItem() {\n\tif (_itemsKnownTillEnd && _visibleBottom == height()) {\n\t\t_visibleTopItem = nullptr;\n\t} else if (_items.empty()) {\n\t\t_visibleTopItem = nullptr;\n\t\t_visibleTopFromItem = _visibleTop;\n\t} else {\n\t\t_visibleTopItem = findItemByY(_visibleTop);\n\t\t_visibleTopFromItem = _visibleTop - itemTop(_visibleTopItem);\n\t}\n}\n\nbool ListWidget::displayScrollDate() const {\n\treturn (_visibleTop <= height() - 2 * (_visibleBottom - _visibleTop));\n}\n\nvoid ListWidget::scrollDateCheck() {\n\tif (!_visibleTopItem) {\n\t\t_scrollDateLastItem = nullptr;\n\t\t_scrollDateLastItemTop = 0;\n\t\tscrollDateHide();\n\t} else if (_visibleTopItem != _scrollDateLastItem || _visibleTopFromItem != _scrollDateLastItemTop) {\n\t\t// Show scroll date only if it is not the initial onScroll() event (with empty _scrollDateLastItem).\n\t\tif (_scrollDateLastItem && !_scrollDateShown) {\n\t\t\ttoggleScrollDateShown();\n\t\t}\n\t\t_scrollDateLastItem = _visibleTopItem;\n\t\t_scrollDateLastItemTop = _visibleTopFromItem;\n\t\t_scrollDateHideTimer.callOnce(st::historyScrollDateHideTimeout);\n\t}\n}\n\nvoid ListWidget::scrollDateHideByTimer() {\n\t_scrollDateHideTimer.cancel();\n\tif (!_scrollDateLink || ClickHandler::getPressed() != _scrollDateLink) {\n\t\tscrollDateHide();\n\t}\n}\n\nvoid ListWidget::scrollDateHide() {\n\tif (_scrollDateShown) {\n\t\ttoggleScrollDateShown();\n\t}\n}\n\nvoid ListWidget::keepScrollDateForNow() {\n\tif (!_scrollDateShown\n\t\t&& _scrollDateLastItem\n\t\t&& _scrollDateOpacity.animating()) {\n\t\ttoggleScrollDateShown();\n\t}\n\t_scrollDateHideTimer.callOnce(st::historyScrollDateHideTimeout);\n}\n\nvoid ListWidget::toggleScrollDateShown() {\n\t_scrollDateShown = !_scrollDateShown;\n\tauto from = _scrollDateShown ? 0. : 1.;\n\tauto to = _scrollDateShown ? 1. : 0.;\n\t_scrollDateOpacity.start([this] { repaintScrollDateCallback(); }, from, to, st::historyDateFadeDuration);\n}\n\nvoid ListWidget::repaintScrollDateCallback() {\n\tauto updateTop = _visibleTop;\n\tauto updateHeight = st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom();\n\tupdate(0, updateTop, width(), updateHeight);\n}\n\nauto ListWidget::collectSelectedItems() const -> SelectedItems {\n\tauto transformation = [&](const auto &item) {\n\t\tconst auto &[itemId, selection] = item;\n\t\tauto result = SelectedItem(itemId);\n\t\tresult.canDelete = selection.canDelete;\n\t\tresult.canForward = selection.canForward;\n\t\tresult.canSendNow = selection.canSendNow;\n\t\tresult.canReschedule = selection.canReschedule;\n\t\treturn result;\n\t};\n\tauto items = SelectedItems();\n\tif (hasSelectedItems()) {\n\t\titems.reserve(_selected.size());\n\t\tstd::transform(\n\t\t\t_selected.begin(),\n\t\t\t_selected.end(),\n\t\t\tstd::back_inserter(items),\n\t\t\ttransformation);\n\t}\n\treturn items;\n}\n\nMessageIdsList ListWidget::collectSelectedIds() const {\n\tconst auto selected = collectSelectedItems();\n\treturn ranges::views::all(\n\t\tselected\n\t) | ranges::views::transform([](const SelectedItem &item) {\n\t\treturn item.msgId;\n\t}) | ranges::to_vector;\n}\n\nvoid ListWidget::pushSelectedItems() {\n\t_delegate->listSelectionChanged(collectSelectedItems());\n}\n\nvoid ListWidget::removeItemSelection(\n\t\tconst SelectedMap::const_iterator &i) {\n\tExpects(i != _selected.cend());\n\n\t_selected.erase(i);\n\tif (_selected.empty()) {\n\t\tupdate();\n\t}\n\tpushSelectedItems();\n}\n\nbool ListWidget::hasSelectedText() const {\n\treturn (_selectedTextItem != nullptr) && !hasSelectedItems();\n}\n\nbool ListWidget::hasSelectedItems() const {\n\treturn !_selected.empty();\n}\n\nSelectionModeResult ListWidget::inSelectionMode() const {\n\tconst auto now = hasSelectedItems()\n\t\t|| !_dragSelected.empty()\n\t\t|| (_mouseAction == MouseAction::Selecting && _lastInSelectionMode);\n\tif (_lastInSelectionMode != now) {\n\t\t_lastInSelectionMode = now;\n\t\tif (now && _overlayHost) {\n\t\t\t_overlayHost->hide();\n\t\t}\n\t\tif (_inSelectionModeAnimation.animating()) {\n\t\t\tconst auto progress = !now\n\t\t\t\t? _inSelectionModeAnimation.value(0.)\n\t\t\t\t: 1. - _inSelectionModeAnimation.value(0.);\n\t\t\t_inSelectionModeAnimation.change(\n\t\t\t\tnow ? 1. : 0.,\n\t\t\t\tst::universalDuration * (1. - progress));\n\t\t} else {\n\t\t\t_inSelectionModeAnimation.stop();\n\t\t\t_inSelectionModeAnimation.start(\n\t\t\t\t[this] {\n\t\t\t\t\tconst_cast<ListWidget*>(this)->update(\n\t\t\t\t\t\tQRect(\n\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t_visibleTop,\n\t\t\t\t\t\t\twidth(),\n\t\t\t\t\t\t\t_visibleBottom - _visibleTop));\n\t\t\t\t},\n\t\t\t\tnow ? 0. : 1.,\n\t\t\t\tnow ? 1. : 0.,\n\t\t\t\tst::universalDuration);\n\t\t}\n\t}\n\treturn { now, _inSelectionModeAnimation.value(now ? 1. : 0.) };\n}\n\nbool ListWidget::overSelectedItems() const {\n\tif (_overState.pointState == PointState::GroupPart) {\n\t\treturn _overItemExact\n\t\t\t&& _selected.contains(_overItemExact->fullId());\n\t} else if (_overState.pointState == PointState::Inside) {\n\t\treturn _overElement\n\t\t\t&& isSelectedAsGroup(_selected, _overElement->data());\n\t}\n\treturn false;\n}\n\nbool ListWidget::isSelectedGroup(\n\t\tconst SelectedMap &applyTo,\n\t\tnot_null<const Data::Group*> group) const {\n\tfor (const auto &other : group->items) {\n\t\tif (!applyTo.contains(other->fullId())) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nbool ListWidget::isSelectedAsGroup(\n\t\tconst SelectedMap &applyTo,\n\t\tnot_null<HistoryItem*> item) const {\n\tif (const auto group = session().data().groups().find(item)) {\n\t\treturn isSelectedGroup(applyTo, group);\n\t}\n\treturn applyTo.contains(item->fullId());\n}\n\nbool ListWidget::isGoodForSelection(\n\t\tSelectedMap &applyTo,\n\t\tnot_null<HistoryItem*> item,\n\t\tint &totalCount) const {\n\tif (!_delegate->listIsItemGoodForSelection(item)) {\n\t\treturn false;\n\t} else if (!applyTo.contains(item->fullId())) {\n\t\t++totalCount;\n\t}\n\treturn (totalCount <= MaxSelectedItems);\n}\n\nbool ListWidget::addToSelection(\n\t\tSelectedMap &applyTo,\n\t\tnot_null<HistoryItem*> item) const {\n\tconst auto itemId = item->fullId();\n\tauto [iterator, ok] = applyTo.try_emplace(\n\t\titemId,\n\t\tSelectionData());\n\tif (!ok) {\n\t\treturn false;\n\t}\n\titerator->second.canDelete = item->canDelete();\n\titerator->second.canForward = item->allowsForward();\n\titerator->second.canSendNow = item->allowsSendNow();\n\titerator->second.canReschedule = item->allowsReschedule();\n\treturn true;\n}\n\nbool ListWidget::removeFromSelection(\n\t\tSelectedMap &applyTo,\n\t\tFullMsgId itemId) const {\n\treturn applyTo.remove(itemId);\n}\n\nvoid ListWidget::changeSelection(\n\t\tSelectedMap &applyTo,\n\t\tnot_null<HistoryItem*> item,\n\t\tSelectAction action) const {\n\tconst auto itemId = item->fullId();\n\tif (action == SelectAction::Invert) {\n\t\taction = applyTo.contains(itemId)\n\t\t\t? SelectAction::Deselect\n\t\t\t: SelectAction::Select;\n\t}\n\tif (action == SelectAction::Select) {\n\t\tauto already = int(applyTo.size());\n\t\tif (isGoodForSelection(applyTo, item, already)) {\n\t\t\taddToSelection(applyTo, item);\n\t\t}\n\t} else {\n\t\tremoveFromSelection(applyTo, itemId);\n\t}\n}\n\nvoid ListWidget::changeSelectionAsGroup(\n\t\tSelectedMap &applyTo,\n\t\tnot_null<HistoryItem*> item,\n\t\tSelectAction action) const {\n\tconst auto group = session().data().groups().find(item);\n\tif (!group) {\n\t\treturn changeSelection(applyTo, item, action);\n\t}\n\tif (action == SelectAction::Invert) {\n\t\taction = isSelectedAsGroup(applyTo, item)\n\t\t\t? SelectAction::Deselect\n\t\t\t: SelectAction::Select;\n\t}\n\tauto already = int(applyTo.size());\n\tconst auto canSelect = [&] {\n\t\tfor (const auto &other : group->items) {\n\t\t\tif (!isGoodForSelection(applyTo, other, already)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}();\n\tif (action == SelectAction::Select && canSelect) {\n\t\tfor (const auto &other : group->items) {\n\t\t\taddToSelection(applyTo, other);\n\t\t}\n\t} else {\n\t\tfor (const auto &other : group->items) {\n\t\t\tremoveFromSelection(applyTo, other->fullId());\n\t\t}\n\t}\n}\n\nbool ListWidget::isItemUnderPressSelected() const {\n\treturn itemUnderPressSelection() != _selected.end();\n}\n\nauto ListWidget::itemUnderPressSelection() -> SelectedMap::iterator {\n\treturn (_pressState.itemId\n\t\t&& _pressState.pointState != PointState::Outside)\n\t\t? _selected.find(_pressState.itemId)\n\t\t: _selected.end();\n}\n\nbool ListWidget::isInsideSelection(\n\t\tnot_null<const Element*> view,\n\t\tnot_null<HistoryItem*> exactItem,\n\t\tconst MouseState &state) const {\n\tif (!_selected.empty()) {\n\t\tif (state.pointState == PointState::GroupPart) {\n\t\t\treturn _selected.contains(exactItem->fullId());\n\t\t} else {\n\t\t\treturn isSelectedAsGroup(_selected, view->data());\n\t\t}\n\t} else if (_selectedTextItem\n\t\t&& _selectedTextItem == view->data()\n\t\t&& state.pointState != PointState::Outside) {\n\t\tStateRequest stateRequest;\n\t\tstateRequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\tconst auto dragState = view->textState(\n\t\t\tstate.point,\n\t\t\tstateRequest);\n\t\tif (dragState.cursor == CursorState::Text\n\t\t\t&& base::in_range(\n\t\t\t\tdragState.symbol,\n\t\t\t\t_selectedTextRange.from,\n\t\t\t\t_selectedTextRange.to)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nauto ListWidget::itemUnderPressSelection() const\n-> SelectedMap::const_iterator {\n\treturn (_pressState.itemId\n\t\t&& _pressState.pointState != PointState::Outside)\n\t\t? _selected.find(_pressState.itemId)\n\t\t: _selected.end();\n}\n\nbool ListWidget::requiredToStartDragging(not_null<Element*> view) const {\n\treturn (_mouseCursorState == CursorState::Date);\n}\n\nbool ListWidget::isPressInSelectedText(TextState state) const {\n\tif (state.cursor != CursorState::Text) {\n\t\treturn false;\n\t}\n\tif (!hasSelectedText()\n\t\t|| !_selectedTextItem\n\t\t|| _selectedTextItem->fullId() != _pressState.itemId) {\n\t\treturn false;\n\t}\n\tauto from = _selectedTextRange.from;\n\tauto to = _selectedTextRange.to;\n\treturn (state.symbol >= from && state.symbol < to);\n}\n\nvoid ListWidget::cancelSelection() {\n\tclearSelected();\n\tclearTextSelection();\n}\n\nvoid ListWidget::selectItem(not_null<HistoryItem*> item) {\n\tif (hasSelectRestriction()) {\n\t\treturn;\n\t} else if ([[maybe_unused]] const auto view = viewForItem(item)) {\n\t\tclearTextSelection();\n\t\tchangeSelection(\n\t\t\t_selected,\n\t\t\titem,\n\t\t\tSelectAction::Select);\n\t\tpushSelectedItems();\n\t}\n}\n\nvoid ListWidget::selectItemAsGroup(not_null<HistoryItem*> item) {\n\tif (hasSelectRestriction()) {\n\t\treturn;\n\t} else if ([[maybe_unused]] const auto view = viewForItem(item)) {\n\t\tclearTextSelection();\n\t\tchangeSelectionAsGroup(\n\t\t\t_selected,\n\t\t\titem,\n\t\t\tSelectAction::Select);\n\t\tpushSelectedItems();\n\t\tupdate();\n\t}\n}\n\nvoid ListWidget::clearSelected() {\n\tif (_selected.empty()) {\n\t\treturn;\n\t}\n\tif (hasSelectedText()) {\n\t\trepaintItem(_selected.begin()->first);\n\t\t_selected.clear();\n\t} else {\n\t\t_selected.clear();\n\t\tpushSelectedItems();\n\t\tupdate();\n\t}\n}\n\nvoid ListWidget::clearTextSelection() {\n\tif (_selectedTextItem) {\n\t\tif (const auto view = viewForItem(_selectedTextItem)) {\n\t\t\trepaintItem(view);\n\t\t}\n\t\t_selectedTextItem = nullptr;\n\t\t_selectedTextRange = TextSelection();\n\t\t_selectedText = TextForMimeData();\n\t}\n}\n\nvoid ListWidget::setTextSelection(\n\t\tnot_null<Element*> view,\n\t\tTextSelection selection) {\n\tif (!selection.empty()) {\n\t\t// We started selecting text in web page preview.\n\t\tClickHandler::unpressed();\n\t}\n\tclearSelected();\n\tconst auto item = view->data();\n\tif (_selectedTextItem != item) {\n\t\tclearTextSelection();\n\t\t_selectedTextItem = view->data();\n\t}\n\t_selectedTextRange = selection;\n\t_selectedText = (selection.from != selection.to)\n\t\t? view->selectedText(selection)\n\t\t: TextForMimeData();\n\trepaintItem(view);\n\tif (!_wasSelectedText && !_selectedText.empty()) {\n\t\t_wasSelectedText = true;\n\t\tsetFocus();\n\t}\n}\n\nbool ListWidget::loadedAtTopKnown() const {\n\treturn !!_slice.skippedBefore;\n}\n\nbool ListWidget::loadedAtTop() const {\n\treturn _slice.skippedBefore && (*_slice.skippedBefore == 0);\n}\n\nbool ListWidget::loadedAtBottomKnown() const {\n\treturn !!_slice.skippedAfter;\n}\n\nbool ListWidget::loadedAtBottom() const {\n\treturn _slice.skippedAfter && (*_slice.skippedAfter == 0);\n}\n\nbool ListWidget::isEmpty() const {\n\treturn loadedAtTop()\n\t\t&& loadedAtBottom()\n\t\t&& (_itemsHeight + _itemsRevealHeight == 0);\n}\n\nbool ListWidget::hasCopyRestriction(HistoryItem *item) const {\n\treturn _delegate->listCopyRestrictionType(item)\n\t\t!= CopyRestrictionType::None;\n}\n\nbool ListWidget::hasCopyMediaRestriction(not_null<HistoryItem*> item) const {\n\treturn _delegate->listCopyMediaRestrictionType(item)\n\t\t!= CopyRestrictionType::None;\n}\n\nbool ListWidget::showCopyRestriction(HistoryItem *item) {\n\tconst auto type = _delegate->listCopyRestrictionType(item);\n\tif (type == CopyRestrictionType::None) {\n\t\treturn false;\n\t}\n\t_delegate->listUiShow()->showToast((type == CopyRestrictionType::Channel)\n\t\t? tr::lng_error_nocopy_channel(tr::now)\n\t\t: (type == CopyRestrictionType::User)\n\t\t? tr::lng_error_nocopy_user(tr::now)\n\t\t: tr::lng_error_nocopy_group(tr::now));\n\treturn true;\n}\n\nbool ListWidget::showCopyMediaRestriction(not_null<HistoryItem*> item) {\n\tconst auto type = _delegate->listCopyMediaRestrictionType(item);\n\tif (type == CopyRestrictionType::None) {\n\t\treturn false;\n\t}\n\t_delegate->listUiShow()->showToast((type == CopyRestrictionType::Channel)\n\t\t? tr::lng_error_nocopy_channel(tr::now)\n\t\t: (type == CopyRestrictionType::User)\n\t\t? tr::lng_error_nocopy_user(tr::now)\n\t\t: tr::lng_error_nocopy_group(tr::now));\n\treturn true;\n}\n\nbool ListWidget::hasCopyRestrictionForSelected() const {\n\tif (hasCopyRestriction()) {\n\t\treturn true;\n\t}\n\tif (_selected.empty()) {\n\t\tif (_selectedTextItem && _selectedTextItem->forbidsForward()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\tfor (const auto &[itemId, selection] : _selected) {\n\t\tif (const auto item = session().data().message(itemId)) {\n\t\t\tif (item->forbidsForward()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nbool ListWidget::showCopyRestrictionForSelected() {\n\tif (_selected.empty()) {\n\t\tif (_selectedTextItem && showCopyRestriction(_selectedTextItem)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\tfor (const auto &[itemId, selection] : _selected) {\n\t\tif (showCopyRestriction(session().data().message(itemId))) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool ListWidget::hasSelectRestriction() const {\n\treturn session().frozen()\n\t\t|| (_delegate->listSelectRestrictionType()\n\t\t\t!= CopyRestrictionType::None);\n}\n\nElement *ListWidget::lookupItemByY(int y) const {\n\treturn strictFindItemByY(y);\n}\n\nauto ListWidget::findViewForPinnedTracking(int top) const\n-> std::pair<Element*, int> {\n\tconst auto findScrollTopItem = [&](int top)\n\t-> std::vector<not_null<Element*>>::const_iterator {\n\t\tif (!width() || _items.empty()) {\n\t\t\treturn end(_items);\n\t\t}\n\t\tconst auto first = ranges::lower_bound(\n\t\t\t_items,\n\t\t\ttop,\n\t\t\tstd::less<>(),\n\t\t\t&Element::y);\n\t\treturn (first == end(_items) || (*first)->y() > top)\n\t\t\t? first - 1\n\t\t\t: first;\n\t};\n\tconst auto findView = [&](int top)\n\t-> std::pair<std::vector<not_null<Element*>>::const_iterator, int> {\n\t\tif (const auto i = findScrollTopItem(top); i != end(_items)) {\n\t\t\treturn { i, top - (*i)->y() };\n\t\t}\n\t\treturn { end(_items), 0 };\n\t};\n\tauto [view, offset] = findView(top);\n\twhile (view != end(_items) && !(*view)->data()->isRegular()) {\n\t\toffset -= (*view)->height();\n\t\t++view;\n\t}\n\treturn { (view != end(_items)) ? view->get() : nullptr, offset };\n}\n\nint ListWidget::itemMinimalHeight() const {\n\treturn st::msgMarginTopAttached\n\t\t+ st::msgPhotoSize\n\t\t+ st::msgMargin.bottom();\n}\n\nvoid ListWidget::checkMoveToOtherViewer() {\n\tauto visibleHeight = (_visibleBottom - _visibleTop);\n\tif (width() <= 0\n\t\t|| visibleHeight <= 0\n\t\t|| _items.empty()\n\t\t|| _aroundIndex < 0\n\t\t|| _scrollTopState.item) {\n\t\treturn;\n\t}\n\n\tauto topItemIndex = findItemIndexByY(_visibleTop);\n\tauto bottomItemIndex = findItemIndexByY(_visibleBottom);\n\tauto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;\n\tauto preloadedCount = preloadedHeight / _itemAverageHeight;\n\tauto preloadIdsLimitMin = (preloadedCount / 2) + 1;\n\tauto preloadIdsLimit = preloadIdsLimitMin\n\t\t+ (visibleHeight / _itemAverageHeight);\n\n\tauto preloadBefore = kPreloadIfLessThanScreens * visibleHeight;\n\tauto before = _slice.skippedBefore;\n\tauto preloadTop = (_visibleTop < preloadBefore);\n\tauto topLoaded = before && (*before == 0);\n\tauto after = _slice.skippedAfter;\n\tauto preloadBottom = (height() - _visibleBottom < preloadBefore);\n\tauto bottomLoaded = after && (*after == 0);\n\n\tauto minScreenDelta = kPreloadedScreensCount\n\t\t- kPreloadIfLessThanScreens;\n\tauto minUniversalIdDelta = (minScreenDelta * visibleHeight)\n\t\t/ _itemAverageHeight;\n\tconst auto preloadAroundMessage = [&](int index) {\n\t\tExpects(index >= 0 && index < _items.size());\n\n\t\tauto preloadRequired = false;\n\t\tauto itemPosition = _items[index]->data()->position();\n\n\t\tif (!preloadRequired) {\n\t\t\tpreloadRequired = (_idsLimit < preloadIdsLimitMin);\n\t\t}\n\t\tif (!preloadRequired) {\n\t\t\tAssert(_aroundIndex >= 0);\n\t\t\tauto delta = std::abs(index - _aroundIndex);\n\t\t\tpreloadRequired = (delta >= minUniversalIdDelta);\n\t\t}\n\t\tif (preloadRequired) {\n\t\t\t_idsLimit = preloadIdsLimit;\n\t\t\t_aroundPosition = itemPosition;\n\t\t\t_aroundIndex = index;\n\t\t\trefreshViewer();\n\t\t}\n\t};\n\n\tconst auto findGoodAbove = [&](int index) {\n\t\tExpects(index >= 0 && index < _items.size());\n\n\t\tfor (; index != _items.size(); ++index) {\n\t\t\tif (_delegate->listIsGoodForAroundPosition(_items[index])) {\n\t\t\t\treturn index;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t};\n\tconst auto findGoodBelow = [&](int index) {\n\t\tExpects(index >= 0 && index < _items.size());\n\n\t\tfor (++index; index != 0;) {\n\t\t\tif (_delegate->listIsGoodForAroundPosition(_items[--index])) {\n\t\t\t\treturn index;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t};\n\tif (preloadTop && !topLoaded) {\n\t\tconst auto goodAboveIndex = findGoodAbove(topItemIndex);\n\t\tconst auto goodIndex = (goodAboveIndex >= 0)\n\t\t\t? goodAboveIndex\n\t\t\t: findGoodBelow(topItemIndex);\n\t\tif (goodIndex >= 0) {\n\t\t\tpreloadAroundMessage(goodIndex);\n\t\t}\n\t} else if (preloadBottom && !bottomLoaded) {\n\t\tconst auto goodBelowIndex = findGoodBelow(bottomItemIndex);\n\t\tconst auto goodIndex = (goodBelowIndex >= 0)\n\t\t\t? goodBelowIndex\n\t\t\t: findGoodAbove(bottomItemIndex);\n\t\tif (goodIndex >= 0) {\n\t\t\tpreloadAroundMessage(goodIndex);\n\t\t}\n\t}\n}\n\nQString ListWidget::tooltipText() const {\n\tconst auto item = (_overElement && _mouseAction == MouseAction::None)\n\t\t? _overElement->data().get()\n\t\t: nullptr;\n\tif (_mouseCursorState == CursorState::Date && item) {\n\t\treturn HistoryView::DateTooltipText(_overElement);\n\t} else if (_mouseCursorState == CursorState::Forwarded && item) {\n\t\tif (const auto forwarded = item->Get<HistoryMessageForwarded>()) {\n\t\t\treturn forwarded->text.toString();\n\t\t}\n\t} else if (const auto link = ClickHandler::getActive()) {\n\t\treturn link->tooltip();\n\t}\n\treturn QString();\n}\n\nQPoint ListWidget::tooltipPos() const {\n\treturn _mousePosition;\n}\n\nbool ListWidget::tooltipWindowActive() const {\n\treturn Ui::AppInFocus() && Ui::InFocusChain(window());\n}\n\nContext ListWidget::elementContext() {\n\treturn _delegate->listContext();\n}\n\nbool ListWidget::elementUnderCursor(\n\t\tnot_null<const HistoryView::Element*> view) {\n\treturn (_overElement == view);\n}\n\nSelectionModeResult ListWidget::elementInSelectionMode(\n\t\tconst HistoryView::Element *view) {\n\tif (view && !_delegate->listIsItemGoodForSelection(view->data())) {\n\t\treturn {};\n\t}\n\treturn inSelectionMode();\n}\n\nbool ListWidget::elementIntersectsRange(\n\t\tnot_null<const Element*> view,\n\t\tint from,\n\t\tint till) {\n\tExpects(view->delegate() == this);\n\n\tconst auto top = itemTop(view);\n\tconst auto bottom = top + view->height();\n\treturn (top < till && bottom > from);\n}\n\nvoid ListWidget::elementStartStickerLoop(not_null<const Element*> view) {\n}\n\nvoid ListWidget::elementShowPollResults(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context) {\n\t_delegate->listShowPollResults(poll, context);\n}\n\nElementOverlayHost &ListWidget::ensureOverlayHost() {\n\tif (!_overlayHost) {\n\t\t_overlayHost = std::make_unique<ElementOverlayHost>(\n\t\t\tthis,\n\t\t\t[=](not_null<const Element*> view) {\n\t\t\t\treturn itemTop(view);\n\t\t\t});\n\t\t_overlayHost->setHiddenCallback([=] {\n\t\t\t_delegate->listWindowSetInnerFocus();\n\t\t});\n\t}\n\treturn *_overlayHost;\n}\n\nvoid ListWidget::elementShowAddPollOption(\n\t\tnot_null<Element*> view,\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context,\n\t\tQRect optionRect) {\n\tShowAddPollOptionOverlay(\n\t\tensureOverlayHost(),\n\t\tthis,\n\t\tview,\n\t\tpoll,\n\t\tcontext,\n\t\tcontroller(),\n\t\t_delegate->listChatStyle());\n}\n\nvoid ListWidget::elementSubmitAddPollOption(FullMsgId context) {\n\tif (_overlayHost) {\n\t\t_overlayHost->triggerSubmit(context);\n\t}\n}\n\nvoid ListWidget::hideElementOverlay() {\n\tif (_overlayHost) {\n\t\t_overlayHost->hide();\n\t}\n}\n\nvoid ListWidget::elementOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) {\n\t_delegate->listOpenPhoto(photo, context);\n}\n\nvoid ListWidget::elementOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView) {\n\t_delegate->listOpenDocument(document, context, showInMediaView);\n}\n\nvoid ListWidget::elementCancelUpload(const FullMsgId &context) {\n\tif (const auto item = session().data().message(context)) {\n\t\t_delegate->listCancelUploadLayer(item);\n\t}\n}\n\nvoid ListWidget::elementShowTooltip(\n\t\tconst TextWithEntities &text,\n\t\tFn<void()> hiddenCallback) {\n\t// Under the parent is supposed to be a scroll widget.\n\t_topToast.show(parentWidget(), &session(), text, hiddenCallback);\n}\n\nbool ListWidget::elementAnimationsPaused() {\n\treturn _delegate->listAnimationsPaused();\n}\n\nbool ListWidget::elementHideReply(not_null<const Element*> view) {\n\treturn _delegate->listElementHideReply(view);\n}\n\nbool ListWidget::elementShownUnread(not_null<const Element*> view) {\n\treturn _delegate->listElementShownUnread(view);\n}\n\nvoid ListWidget::elementSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) {\n\t_delegate->listSendBotCommand(command, context);\n}\n\nvoid ListWidget::elementSearchInList(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) {\n\t_delegate->listSearch(query, context);\n}\n\nvoid ListWidget::elementHandleViaClick(not_null<UserData*> bot) {\n\t_delegate->listHandleViaClick(bot);\n}\n\nElementChatMode ListWidget::elementChatMode() {\n\treturn _overrideChatMode.value_or(_isChatWide\n\t\t? ElementChatMode::Wide\n\t\t: ElementChatMode::Default);\n}\n\nnot_null<Ui::PathShiftGradient*> ListWidget::elementPathShiftGradient() {\n\treturn _pathGradient.get();\n}\n\nvoid ListWidget::elementReplyTo(const FullReplyTo &to) {\n\treplyToMessageRequestNotify(to, base::IsCtrlPressed());\n}\n\nvoid ListWidget::elementStartInteraction(not_null<const Element*> view) {\n}\n\nvoid ListWidget::elementStartPremium(\n\t\tnot_null<const Element*> view,\n\t\tElement *replacing) {\n\tconst auto already = !_emojiInteractions->playPremiumEffect(\n\t\tview,\n\t\treplacing);\n\tif (already) {\n\t\tshowPremiumStickerTooltip(view);\n\t}\n}\n\nvoid ListWidget::elementCancelPremium(not_null<const Element*> view) {\n\t_emojiInteractions->cancelPremiumEffect(view);\n}\n\nvoid ListWidget::elementStartEffect(\n\t\tnot_null<const Element*> view,\n\t\tElement *replacing) {\n\t_emojiInteractions->playEffect(view);\n}\n\nQString ListWidget::elementAuthorRank(not_null<const Element*> view) {\n\treturn _delegate->listElementAuthorRank(view);\n}\n\nbool ListWidget::elementHideTopicButton(not_null<const Element*> view) {\n\treturn _delegate->listElementHideTopicButton(view);\n}\n\n\nvoid ListWidget::saveState(not_null<ListMemento*> memento) {\n\tmemento->setAroundPosition(_aroundPosition);\n\tconst auto state = countScrollState();\n\tmemento->setIdsLimit(state.item ? _idsLimit : 0);\n\tmemento->setScrollTopState(state);\n}\n\nvoid ListWidget::restoreState(not_null<ListMemento*> memento) {\n\t_aroundPosition = memento->aroundPosition();\n\t_aroundIndex = -1;\n\tif (const auto limit = memento->idsLimit()) {\n\t\t_idsLimit = limit;\n\t}\n\t_scrollTopState = memento->scrollTopState();\n\trefreshViewer();\n}\n\nvoid ListWidget::updateItemsGeometry() {\n\tconst auto count = int(_items.size());\n\tconst auto first = [&] {\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tconst auto view = _items[i].get();\n\t\t\tif (view->isHidden()) {\n\t\t\t\tview->setDisplayDate(false);\n\t\t\t} else {\n\t\t\t\tview->setDisplayDate(_context != Context::ShortcutMessages);\n\t\t\t\tview->setAttachToPrevious(false);\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t\treturn count;\n\t}();\n\trefreshAttachmentsFromTill(first, count);\n}\n\nvoid ListWidget::updateSize() {\n\tresizeToWidth(width(), _minHeight);\n\tupdateVisibleTopItem();\n\t_resizePending = false;\n}\n\nvoid ListWidget::resizeToWidth(int newWidth, int minHeight) {\n\tif (width() != newWidth && _overlayHost) {\n\t\t_overlayHost->hide();\n\t}\n\t_minHeight = minHeight;\n\tRpWidget::resizeToWidth(newWidth);\n\trestoreScrollPosition();\n}\n\nvoid ListWidget::startItemRevealAnimations() {\n\tfor (const auto &view : base::take(_itemRevealPending)) {\n\t\tif (const auto height = view->height()) {\n\t\t\tstartMessageSendingAnimation(view->data());\n\t\t\tif (!_itemRevealAnimations.contains(view)) {\n\t\t\t\tauto &animation = _itemRevealAnimations[view];\n\t\t\t\tanimation.startHeight = height;\n\t\t\t\t_itemsRevealHeight += height;\n\t\t\t\tanimation.animation.start(\n\t\t\t\t\t[=] { revealItemsCallback(); },\n\t\t\t\t\t0.,\n\t\t\t\t\t1.,\n\t\t\t\t\tst::itemRevealDuration,\n\t\t\t\t\tanim::easeOutQuint);\n\t\t\t\tif (view->data()->out()) {\n\t\t\t\t\t_delegate->listChatTheme()->rotateComplexGradientBackground();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ListWidget::startMessageSendingAnimation(\n\t\tnot_null<HistoryItem*> item) {\n\tif (elementChatMode() == HistoryView::ElementChatMode::Default\n\t\t&& width() > st::columnMaximalWidthLeft\n\t\t&& !item->media()) {\n\t\treturn;\n\t}\n\tconst auto sendingAnimation = _delegate->listSendingAnimation();\n\tif (!sendingAnimation || !sendingAnimation->checkExpectedType(item)) {\n\t\treturn;\n\t}\n\n\tauto globalEndTopLeft = rpl::merge(\n\t\tsession().data().newItemAdded() | rpl::to_empty,\n\t\tgeometryValue() | rpl::to_empty\n\t) | rpl::map([=]() -> std::optional<QPoint> {\n\t\tconst auto view = viewForItem(item);\n\t\tif (!view) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\tconst auto additional = !_visibleTop ? view->height() : 0;\n\t\treturn mapToGlobal(QPoint(0, itemTop(view) - additional));\n\t});\n\n\tsendingAnimation->startAnimation({\n\t\t.globalEndTopLeft = std::move(globalEndTopLeft),\n\t\t.view = [=] { return viewForItem(item); },\n\t\t.paintContext = [=] { return preparePaintContext({}); },\n\t});\n}\n\nvoid ListWidget::showPremiumStickerTooltip(\n\t\tnot_null<const HistoryView::Element*> view) {\n\tif (const auto media = view->data()->media()) {\n\t\tif (const auto document = media->document()) {\n\t\t\t_delegate->listShowPremiumToast(document);\n\t\t}\n\t}\n}\n\nvoid ListWidget::revealItemsCallback() {\n\tauto revealHeight = 0;\n\tfor (auto i = begin(_itemRevealAnimations)\n\t\t; i != end(_itemRevealAnimations);) {\n\t\tif (!i->second.animation.animating()) {\n\t\t\ti = _itemRevealAnimations.erase(i);\n\t\t} else {\n\t\t\trevealHeight += anim::interpolate(\n\t\t\t\ti->second.startHeight,\n\t\t\t\t0,\n\t\t\t\ti->second.animation.value(1.));\n\t\t\t++i;\n\t\t}\n\t}\n\tif (_itemsRevealHeight != revealHeight) {\n\t\tupdateVisibleTopItem();\n\t\tif (_visibleTopItem) {\n\t\t\t// We're not at the bottom.\n\t\t\trevealHeight = 0;\n\t\t\t_itemRevealAnimations.clear();\n\t\t}\n\t\tconst auto old = std::exchange(_itemsRevealHeight, revealHeight);\n\t\tconst auto delta = old - _itemsRevealHeight;\n\t\t_itemsHeight += delta;\n\t\t_itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom)\n\t\t\t? (_minHeight - _itemsHeight - st::historyPaddingBottom)\n\t\t\t: 0;\n\t\tauto collapseGapTotal = 0;\n\t\tfor (const auto &gap : _collapseGaps) {\n\t\t\tcollapseGapTotal += gap.height;\n\t\t}\n\t\tconst auto wasHeight = height();\n\t\tconst auto nowHeight = _itemsTop\n\t\t\t+ _itemsHeight\n\t\t\t+ collapseGapTotal\n\t\t\t+ st::historyPaddingBottom;\n\t\tif (wasHeight != nowHeight) {\n\t\t\tresize(width(), nowHeight);\n\t\t}\n\t\tupdate();\n\t\trestoreScrollPosition();\n\t\tupdateVisibleTopItem();\n\n\t\tif (!_itemsRevealHeight) {\n\t\t\tmouseActionUpdate(QCursor::pos());\n\t\t}\n\t}\n}\n\nint ListWidget::resizeGetHeight(int newWidth) {\n\tupdate();\n\n\tconst auto resizeAllItems = (_itemsWidth != newWidth);\n\tauto newHeight = 0;\n\tfor (const auto &view : _items) {\n\t\tview->setY(newHeight);\n\t\tif (view->pendingResize() || resizeAllItems) {\n\t\t\tnewHeight += view->resizeGetHeight(newWidth);\n\t\t} else {\n\t\t\tnewHeight += view->height();\n\t\t}\n\t}\n\tif (newHeight > 0) {\n\t\t_itemAverageHeight = std::max(\n\t\t\titemMinimalHeight(),\n\t\t\tnewHeight / int(_items.size()));\n\t}\n\tstartItemRevealAnimations();\n\t_itemsWidth = newWidth;\n\t_itemsHeight = newHeight - _itemsRevealHeight;\n\tauto collapseGapTotal = 0;\n\tfor (const auto &gap : _collapseGaps) {\n\t\tcollapseGapTotal += gap.height;\n\t}\n\t_itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom)\n\t\t? (_minHeight - _itemsHeight - st::historyPaddingBottom)\n\t\t: 0;\n\tif (_emptyInfo) {\n\t\t_emptyInfo->setVisible(isEmpty());\n\t}\n\treturn _itemsTop\n\t\t+ _itemsHeight\n\t\t+ collapseGapTotal\n\t\t+ st::historyPaddingBottom;\n}\n\nvoid ListWidget::restoreScrollPosition() {\n\tauto newVisibleTop = _visibleTopItem\n\t\t? (itemTop(_visibleTopItem) + _visibleTopFromItem)\n\t\t: ScrollMax;\n\t_delegate->listScrollTo(newVisibleTop);\n}\n\nTextSelection ListWidget::computeRenderSelection(\n\t\tnot_null<const SelectedMap*> selected,\n\t\tnot_null<const Element*> view) const {\n\tconst auto itemSelection = [&](not_null<HistoryItem*> item) {\n\t\tauto i = selected->find(item->fullId());\n\t\tif (i != selected->end()) {\n\t\t\treturn FullSelection;\n\t\t}\n\t\treturn TextSelection();\n\t};\n\tconst auto item = view->data();\n\tif (const auto group = session().data().groups().find(item)) {\n\t\tif (group->items.front() != item) {\n\t\t\treturn TextSelection();\n\t\t}\n\t\tauto result = TextSelection();\n\t\tauto allFullSelected = true;\n\t\tconst auto count = int(group->items.size());\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tif (itemSelection(group->items[i]) == FullSelection) {\n\t\t\t\tresult = AddGroupItemSelection(result, i);\n\t\t\t} else {\n\t\t\t\tallFullSelected = false;\n\t\t\t}\n\t\t}\n\t\tif (allFullSelected) {\n\t\t\treturn FullSelection;\n\t\t}\n\t\tconst auto leaderSelection = itemSelection(item);\n\t\tif (leaderSelection != FullSelection\n\t\t\t&& leaderSelection != TextSelection()) {\n\t\t\treturn leaderSelection;\n\t\t}\n\t\treturn result;\n\t}\n\treturn itemSelection(item);\n}\n\nTextSelection ListWidget::itemRenderSelection(\n\t\tnot_null<const Element*> view) const {\n\tif (!_dragSelected.empty()) {\n\t\tconst auto i = _dragSelected.find(view->data()->fullId());\n\t\tif (i != _dragSelected.end()) {\n\t\t\treturn (_dragSelectAction == DragSelectAction::Selecting)\n\t\t\t\t? FullSelection\n\t\t\t\t: TextSelection();\n\t\t}\n\t}\n\tif (!_selected.empty() || !_dragSelected.empty()) {\n\t\treturn computeRenderSelection(&_selected, view);\n\t} else if (view->data() == _selectedTextItem) {\n\t\treturn _selectedTextRange;\n\t}\n\treturn TextSelection();\n}\n\nUi::ChatPaintContext ListWidget::preparePaintContext(\n\t\tconst QRect &clip) const {\n\treturn _delegate->listPreparePaintContext({\n\t\t.theme = _delegate->listChatTheme(),\n\t\t.clip = clip,\n\t\t.visibleAreaPositionGlobal = mapToGlobal(QPoint(0, _visibleTop)),\n\t\t.visibleAreaTop = _visibleTop,\n\t\t.visibleAreaWidth = width(),\n\t\t.visibleAreaHeight = _visibleBottom - _visibleTop,\n\t});\n}\n\nbool ListWidget::markingContentsRead() const {\n\treturn _showFinished\n\t\t&& !_refreshingViewer\n\t\t&& _delegate->listMarkingContentRead();\n}\n\nbool ListWidget::markingMessagesRead() const {\n\treturn markingContentsRead() && !session().supportMode();\n}\n\nvoid ListWidget::showFinished() {\n\t_showFinished = true;\n\tcheckActivation();\n}\n\nvoid ListWidget::checkActivation() {\n\tif (_resizePending\n\t\t|| _visibleTop >= _visibleBottom\n\t\t|| !markingMessagesRead()) {\n\t\treturn;\n\t}\n\tfor (const auto &view : ranges::views::reverse(_items)) {\n\t\tconst auto bottom = itemTop(view) + view->height();\n\t\tif (_visibleBottom + _itemsRevealHeight >= bottom) {\n\t\t\tconst auto item = view->data();\n\t\t\tif (item->isRegular()) {\n\t\t\t\tdelegate()->listMarkReadTill(item);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ListWidget::paintEvent(QPaintEvent *e) {\n\tconst auto overlapped = _delegate->listIgnorePaintEvent(this, e);\n\tif (_readMetricsTracker) {\n\t\t_readMetricsTracker->setScreenActive(\n\t\t\t!overlapped && markingContentsRead());\n\t}\n\tif (overlapped) {\n\t\treturn;\n\t} else if (_translateTracker) {\n\t\t_translateTracker->startBunch();\n\t}\n\tconst auto metricsStale = _readMetricsTracker\n\t\t&& base::take(_readMetricsStale);\n\tif (metricsStale) {\n\t\t_readMetricsTracker->startBatch(_visibleTop, _visibleBottom);\n\t}\n\tauto readTill = (HistoryItem*)nullptr;\n\tauto readContents = base::flat_set<not_null<HistoryItem*>>();\n\tconst auto markingAsViewed = markingMessagesRead();\n\tconst auto guard = gsl::finally([&] {\n\t\tif (_translateTracker) {\n\t\t\t_delegate->listAddTranslatedItems(_translateTracker.get());\n\t\t\t_translateTracker->finishBunch();\n\t\t}\n\t\tif (metricsStale) {\n\t\t\t_readMetricsTracker->endBatch();\n\t\t}\n\t\tif (markingAsViewed && readTill) {\n\t\t\t_delegate->listMarkReadTill(readTill);\n\t\t}\n\t\tif (!readContents.empty() && markingContentsRead()) {\n\t\t\t_delegate->listMarkContentsRead(readContents);\n\t\t}\n\t\t_userpicsCache.clear();\n\t});\n\n\tPainter p(this);\n\n\t_pathGradient->startFrame(\n\t\t0,\n\t\twidth(),\n\t\tstd::min(st::msgMaxWidth / 2, width() / 2));\n\n\tauto clip = e->rect();\n\n\tauto collapseGapsTotal = 0;\n\tfor (const auto &gap : _collapseGaps) {\n\t\tcollapseGapsTotal += gap.height;\n\t}\n\n\tauto from = std::lower_bound(begin(_items), end(_items), clip.top() - collapseGapsTotal, [this](auto &elem, int top) {\n\t\treturn this->itemTop(elem) + elem->height() <= top;\n\t});\n\tauto to = std::lower_bound(begin(_items), end(_items), clip.top() + clip.height(), [this](auto &elem, int bottom) {\n\t\treturn this->itemTop(elem) < bottom;\n\t});\n\n\tauto context = preparePaintContext(clip);\n\tcontext.highlightPathCache = &_highlightPathCache;\n\tif (from == end(_items)) {\n\t\t_delegate->listPaintEmpty(p, context);\n\t\treturn;\n\t}\n\tif (_reactionsManager) {\n\t\t_reactionsManager->startEffectsCollection();\n\t}\n\n\tconst auto session = &this->session();\n\tauto top = itemTop(from->get());\n\n\tauto nextGapIndex = 0;\n\tauto collapseShift = 0;\n\tfor (; nextGapIndex < int(_collapseGaps.size()); ++nextGapIndex) {\n\t\tconst auto &gap = _collapseGaps[nextGapIndex];\n\t\tif (top < gap.absY) break;\n\t\tcollapseShift += gap.height;\n\t}\n\ttop += collapseShift;\n\n\tcontext = context.translated(0, -top);\n\tp.translate(0, top);\n\tconst auto sendingAnimation = _delegate->listSendingAnimation();\n\tfor (auto i = from; i != to; ++i) {\n\t\twhile (nextGapIndex < int(_collapseGaps.size())) {\n\t\t\tconst auto &gap = _collapseGaps[nextGapIndex];\n\t\t\tif (top - collapseShift < gap.absY) break;\n\t\t\ttop += gap.height;\n\t\t\tcollapseShift += gap.height;\n\t\t\tcontext.translate(0, -gap.height);\n\t\t\tp.translate(0, gap.height);\n\t\t\t++nextGapIndex;\n\t\t}\n\n\t\tconst auto view = *i;\n\t\tconst auto item = view->data();\n\t\tconst auto height = view->height();\n\t\tif (!sendingAnimation\n\t\t\t|| !sendingAnimation->hasAnimatedMessage(item)) {\n\t\t\tif (_reactionsManager) {\n\t\t\t\tcontext.reactionInfo\n\t\t\t\t\t= _reactionsManager->currentReactionPaintInfo();\n\t\t\t}\n\t\t\tcontext.outbg = view->hasOutLayout();\n\t\t\tcontext.selection = itemRenderSelection(view);\n\t\t\tcontext.highlight = _highlighter.state(item);\n\t\t\tview->draw(p, context);\n\t\t}\n\t\tif (_translateTracker) {\n\t\t\t_translateTracker->add(view);\n\t\t}\n\t\tif (metricsStale && height > 0) {\n\t\t\t_readMetricsTracker->push(item, top, height);\n\t\t}\n\t\tconst auto isSponsored = item->isSponsored();\n\t\tconst auto isUnread = _delegate->listElementShownUnread(view)\n\t\t\t&& item->isRegular();\n\t\tconst auto withReaction = context.reactionInfo\n\t\t\t&& item->hasUnreadReaction();\n\t\tconst auto yShown = [&](int y) {\n\t\t\treturn (_visibleBottom >= y && _visibleTop <= y);\n\t\t};\n\t\tconst auto markShown = (_context != Context::ChatPreview)\n\t\t\t&& (isSponsored\n\t\t\t\t? view->markSponsoredViewed(_visibleBottom - top)\n\t\t\t\t: withReaction\n\t\t\t\t? yShown(top + context.reactionInfo->position.y())\n\t\t\t\t: isUnread\n\t\t\t\t? yShown(top + height)\n\t\t\t\t: yShown(top + height / 2));\n\t\tif (markShown) {\n\t\t\tif (isSponsored) {\n\t\t\t\tsession->sponsoredMessages().view(item->fullId());\n\t\t\t} else if (isUnread) {\n\t\t\t\treadTill = item;\n\t\t\t}\n\t\t\tif (markingAsViewed && item->hasViews()) {\n\t\t\t\tsession->api().views().scheduleIncrement(item);\n\t\t\t}\n\t\t\tif (withReaction) {\n\t\t\t\treadContents.insert(item);\n\t\t\t} else if (item->isUnreadMention()\n\t\t\t\t&& !item->isUnreadMedia()) {\n\t\t\t\treadContents.insert(item);\n\t\t\t\t_highlighter.enqueue({ item });\n\t\t\t}\n\t\t\tif (item->hasUnreadPollVote()) {\n\t\t\t\treadContents.insert(item);\n\t\t\t}\n\t\t}\n\t\tsession->data().reactions().poll(item, context.now);\n\t\tif (item->hasUnpaidContent()) {\n\t\t\tsession->api().views().pollExtendedMedia(item);\n\t\t}\n\t\tif (_reactionsManager) {\n\t\t\t_reactionsManager->recordCurrentReactionEffect(\n\t\t\t\titem->fullId(),\n\t\t\t\tQPoint(0, top));\n\t\t}\n\t\ttop += height;\n\t\tcontext.translate(0, -height);\n\t\tp.translate(0, height);\n\t}\n\tcontext.translate(0, top);\n\tp.translate(0, -top);\n\n\tpaintUserpics(p, context, clip);\n\tpaintDates(p, context, clip);\n\n\tif (_replyButtonManager) {\n\t\t_replyButtonManager->paint(p, context);\n\t}\n\tif (_reactionsManager) {\n\t\t_reactionsManager->paint(p, context);\n\t}\n}\n\nvoid ListWidget::paintUserpics(\n\t\tPainter &p,\n\t\tconst Ui::ChatPaintContext &context,\n\t\tQRect clip) {\n\tif (_context == Context::ShortcutMessages) {\n\t\treturn;\n\t}\n\tconst auto session = &this->session();\n\tenumerateUserpics([&](not_null<Element*> view, int userpicTop) {\n\t\t// stop the enumeration if the userpic is below the painted rect\n\t\tif (userpicTop >= clip.top() + clip.height()) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// paint the userpic if it intersects the painted rect\n\t\tif (userpicTop + st::msgPhotoSize > clip.top()) {\n\t\t\tconst auto item = view->data();\n\t\t\tconst auto hasTranslation = context.gestureHorizontal.translation\n\t\t\t\t&& (context.gestureHorizontal.msgBareId\n\t\t\t\t\t== item->fullId().msg.bare);\n\t\t\tif (hasTranslation) {\n\t\t\t\tp.translate(context.gestureHorizontal.translation, 0);\n\t\t\t\tupdate(\n\t\t\t\t\tQRect(\n\t\t\t\t\t\tst::historyPhotoLeft\n\t\t\t\t\t\t\t+ context.gestureHorizontal.translation,\n\t\t\t\t\t\tuserpicTop,\n\t\t\t\t\t\tst::msgPhotoSize\n\t\t\t\t\t\t\t- context.gestureHorizontal.translation,\n\t\t\t\t\t\tst::msgPhotoSize));\n\t\t\t}\n\t\t\tif (const auto from = item->displayFrom()) {\n\t\t\t\tfrom->paintUserpicLeft(\n\t\t\t\t\tp,\n\t\t\t\t\t_userpics[from],\n\t\t\t\t\tst::historyPhotoLeft,\n\t\t\t\t\tuserpicTop,\n\t\t\t\t\tview->width(),\n\t\t\t\t\tst::msgPhotoSize);\n\t\t\t} else if (const auto info = item->displayHiddenSenderInfo()) {\n\t\t\t\tif (info->customUserpic.empty()) {\n\t\t\t\t\tinfo->emptyUserpic.paintCircle(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tst::historyPhotoLeft,\n\t\t\t\t\t\tuserpicTop,\n\t\t\t\t\t\tview->width(),\n\t\t\t\t\t\tst::msgPhotoSize);\n\t\t\t\t} else {\n\t\t\t\t\tauto &userpic = _hiddenSenderUserpics[item->id];\n\t\t\t\t\tconst auto valid = info->paintCustomUserpic(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tuserpic,\n\t\t\t\t\t\tst::historyPhotoLeft,\n\t\t\t\t\t\tuserpicTop,\n\t\t\t\t\t\tview->width(),\n\t\t\t\t\t\tst::msgPhotoSize);\n\t\t\t\t\tif (!valid) {\n\t\t\t\t\t\tinfo->customUserpic.load(session, item->fullId());\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tUnexpected(\"Corrupt forwarded information in message.\");\n\t\t\t}\n\t\t\tif (hasTranslation) {\n\t\t\t\tp.translate(-context.gestureHorizontal.translation, 0);\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t});\n}\n\nvoid ListWidget::paintDates(\n\t\tPainter &p,\n\t\tconst Ui::ChatPaintContext &context,\n\t\tQRect clip) {\n\tif (_context == Context::ShortcutMessages) {\n\t\treturn;\n\t}\n\n\tauto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();\n\tauto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.);\n\tenumerateDates([&](not_null<Element*> view, int itemtop, int dateTop) {\n\t\t// stop the enumeration if the date is above the painted rect\n\t\tif (dateTop + dateHeight <= clip.top()) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst auto displayDate = view->displayDate();\n\t\tauto dateInPlace = displayDate;\n\t\tif (dateInPlace) {\n\t\t\tconst auto correctDateTop = itemtop + st::msgServiceMargin.top();\n\t\t\tdateInPlace = (dateTop < correctDateTop + dateHeight);\n\t\t}\n\t\t//bool noFloatingDate = (item->date.date() == lastDate && displayDate);\n\t\t//if (noFloatingDate) {\n\t\t//\tif (itemtop < showFloatingBefore) {\n\t\t//\t\tnoFloatingDate = false;\n\t\t//\t}\n\t\t//}\n\n\t\t// paint the date if it intersects the painted rect\n\t\tif (dateTop < clip.top() + clip.height()) {\n\t\t\tauto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity;\n\t\t\tif (opacity > 0.) {\n\t\t\t\tp.setOpacity(opacity);\n\t\t\t\tint dateY = /*noFloatingDate ? itemtop :*/ (dateTop - st::msgServiceMargin.top());\n\t\t\t\tint width = view->width();\n\t\t\t\tif (const auto date = view->Get<HistoryView::DateBadge>()) {\n\t\t\t\t\tdate->paint(p, context.st, dateY, width, _isChatWide);\n\t\t\t\t} else {\n\t\t\t\t\tServiceMessagePainter::PaintDate(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tcontext.st,\n\t\t\t\t\t\tItemDateText(\n\t\t\t\t\t\t\tview->data(),\n\t\t\t\t\t\t\tIsItemScheduledUntilOnline(view->data())),\n\t\t\t\t\t\tdateY,\n\t\t\t\t\t\twidth,\n\t\t\t\t\t\t_isChatWide);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t});\n}\n\nvoid ListWidget::maybeMarkReactionsRead(not_null<HistoryItem*> item) {\n\tconst auto view = viewForItem(item);\n\tif (!view || !markingContentsRead()) {\n\t\treturn;\n\t}\n\tconst auto top = itemTop(view);\n\tconst auto reactionCenter\n\t\t= view->reactionButtonParameters({}, {}).center.y();\n\tif (top + reactionCenter < _visibleTop\n\t\t|| top + view->height() > _visibleBottom) {\n\t\treturn;\n\t}\n\t_delegate->listMarkContentsRead({ item });\n}\n\nbool ListWidget::eventHook(QEvent *e) {\n\tif (e->type() == QEvent::TouchBegin\n\t\t|| e->type() == QEvent::TouchUpdate\n\t\t|| e->type() == QEvent::TouchEnd\n\t\t|| e->type() == QEvent::TouchCancel) {\n\t\tQTouchEvent *ev = static_cast<QTouchEvent*>(e);\n\t\tif (ev->device()->type() == base::TouchDevice::TouchScreen) {\n\t\t\ttouchEvent(ev);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn RpWidget::eventHook(e);\n}\n\nvoid ListWidget::applyDragSelection() {\n\tif (!hasSelectRestriction()) {\n\t\tapplyDragSelection(_selected);\n\t}\n\tclearDragSelection();\n\tpushSelectedItems();\n}\n\nvoid ListWidget::applyDragSelection(SelectedMap &applyTo) const {\n\tif (_dragSelectAction == DragSelectAction::Selecting) {\n\t\tfor (const auto &itemId : _dragSelected) {\n\t\t\tif (applyTo.size() >= MaxSelectedItems) {\n\t\t\t\tbreak;\n\t\t\t} else if (!applyTo.contains(itemId)) {\n\t\t\t\tif (const auto item = session().data().message(itemId)) {\n\t\t\t\t\taddToSelection(applyTo, item);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if (_dragSelectAction == DragSelectAction::Deselecting) {\n\t\tfor (const auto &itemId : _dragSelected) {\n\t\t\tremoveFromSelection(applyTo, itemId);\n\t\t}\n\t}\n}\n\nTextForMimeData ListWidget::getSelectedText() const {\n\tauto selected = _selected;\n\n\tif (_mouseAction == MouseAction::Selecting && !_dragSelected.empty()) {\n\t\tapplyDragSelection(selected);\n\t}\n\n\tif (selected.empty()) {\n\t\tif (const auto view = viewForItem(_selectedTextItem)) {\n\t\t\treturn view->selectedText(_selectedTextRange);\n\t\t}\n\t\treturn _selectedText;\n\t}\n\n\tauto groups = base::flat_set<not_null<const Data::Group*>>();\n\tauto fullSize = 0;\n\tauto texts = std::vector<std::pair<\n\t\tnot_null<HistoryItem*>,\n\t\tTextForMimeData>>();\n\ttexts.reserve(selected.size());\n\n\tconst auto wrapItem = [&](\n\t\t\tnot_null<HistoryItem*> item,\n\t\t\tTextForMimeData &&unwrapped) {\n\t\tauto time = QString(\"[%1] \").arg(\n\t\t\tQLocale().toString(ItemDateTime(item), QLocale::ShortFormat));\n\t\tauto part = TextForMimeData();\n\t\tauto size = time.size()\n\t\t\t+ item->author()->name().size()\n\t\t\t+ 2\n\t\t\t+ unwrapped.expanded.size();\n\t\tpart.reserve(size);\n\t\tpart.append(time).append(item->author()->name()).append(u\": \"_q);\n\t\tpart.append(std::move(unwrapped));\n\t\ttexts.emplace_back(std::move(item), std::move(part));\n\t\tfullSize += size;\n\t};\n\tconst auto addItem = [&](not_null<HistoryItem*> item) {\n\t\twrapItem(item, HistoryItemText(item));\n\t};\n\tconst auto addGroup = [&](not_null<const Data::Group*> group) {\n\t\tExpects(!group->items.empty());\n\n\t\twrapItem(group->items.back(), HistoryGroupText(group));\n\t};\n\n\tfor (const auto &[itemId, data] : selected) {\n\t\tif (const auto item = session().data().message(itemId)) {\n\t\t\tif (const auto group = session().data().groups().find(item)) {\n\t\t\t\tif (groups.contains(group)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (isSelectedGroup(selected, group)) {\n\t\t\t\t\tgroups.emplace(group);\n\t\t\t\t\taddGroup(group);\n\t\t\t\t} else {\n\t\t\t\t\taddItem(item);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\taddItem(item);\n\t\t\t}\n\t\t}\n\t}\n\tranges::sort(texts, [&](\n\t\t\tconst std::pair<not_null<HistoryItem*>, TextForMimeData> &a,\n\t\t\tconst std::pair<not_null<HistoryItem*>, TextForMimeData> &b) {\n\t\treturn _delegate->listIsLessInOrder(a.first, b.first);\n\t});\n\n\tauto result = TextForMimeData();\n\tauto sep = u\"\\n\"_q;\n\tresult.reserve(fullSize + (texts.size() - 1) * sep.size());\n\tfor (auto i = begin(texts), e = end(texts); i != e;) {\n\t\tresult.append(std::move(i->second));\n\t\tif (++i != e) {\n\t\t\tresult.append(sep);\n\t\t}\n\t}\n\treturn result;\n}\n\nMessageIdsList ListWidget::getSelectedIds() const {\n\treturn collectSelectedIds();\n}\n\nSelectedItems ListWidget::getSelectedItems() const {\n\treturn collectSelectedItems();\n}\n\nconst TextSelection &ListWidget::getSelectedTextRange(\n\t\tnot_null<HistoryItem*> item) const {\n\treturn _selectedTextRange;\n}\n\nint ListWidget::findItemIndexByY(int y) const {\n\tExpects(!_items.empty());\n\n\tif (y < _itemsTop) {\n\t\treturn 0;\n\t}\n\tauto i = std::lower_bound(\n\t\tbegin(_items),\n\t\tend(_items),\n\t\ty,\n\t\t[this](auto &elem, int top) {\n\t\treturn this->itemTop(elem) + elem->height() <= top;\n\t});\n\treturn std::min(int(i - begin(_items)), int(_items.size() - 1));\n}\n\nnot_null<Element*> ListWidget::findItemByY(int y) const {\n\treturn _items[findItemIndexByY(y)];\n}\n\nElement *ListWidget::strictFindItemByY(int y) const {\n\tif (_items.empty()) {\n\t\treturn nullptr;\n\t}\n\treturn (y >= _itemsTop && y < _itemsTop + _itemsHeight)\n\t\t? findItemByY(y).get()\n\t\t: nullptr;\n}\n\nauto ListWidget::countScrollState() const -> ScrollTopState {\n\tif (_items.empty()\n\t\t|| (_itemsKnownTillEnd && _visibleBottom == height())) {\n\t\treturn { Data::MessagePosition(), 0 };\n\t}\n\tconst auto topItem = findItemByY(_visibleTop);\n\treturn {\n\t\ttopItem->data()->position(),\n\t\t_visibleTop - itemTop(topItem)\n\t};\n}\n\nvoid ListWidget::keyPressEvent(QKeyEvent *e) {\n\tconst auto key = e->key();\n\tconst auto hasModifiers = (Qt::NoModifier !=\n\t\t(e->modifiers()\n\t\t\t& ~(Qt::KeypadModifier | Qt::GroupSwitchModifier)));\n\tif (_middleClickAutoscroll.active() && key == Qt::Key_Escape) {\n\t\t_middleClickAutoscroll.stop();\n\t\treturn;\n\t}\n\tif (key == Qt::Key_Escape || key == Qt::Key_Back) {\n\t\tif (hasSelectedText() || hasSelectedItems()) {\n\t\t\tcancelSelection();\n\t\t} else {\n\t\t\t_delegate->listCancelRequest();\n\t\t}\n\t} else if (e == QKeySequence::Copy\n\t\t&& (hasSelectedText() || hasSelectedItems())\n\t\t&& !showCopyRestriction()\n\t\t&& !hasCopyRestrictionForSelected()) {\n\t\tTextUtilities::SetClipboardText(getSelectedText());\n#ifdef Q_OS_MAC\n\t} else if (key == Qt::Key_E\n\t\t&& e->modifiers().testFlag(Qt::ControlModifier)\n\t\t&& !showCopyRestriction()\n\t\t&& !hasCopyRestrictionForSelected()) {\n\t\tTextUtilities::SetClipboardText(getSelectedText(), QClipboard::FindBuffer);\n#endif // Q_OS_MAC\n\t} else if (e == QKeySequence::Delete || key == Qt::Key_Backspace) {\n\t\t_delegate->listDeleteRequest();\n\t} else if (!hasModifiers\n\t\t&& ((key == Qt::Key_Up)\n\t\t\t|| (key == Qt::Key_Down)\n\t\t\t|| (key == Qt::Key_PageUp)\n\t\t\t|| (key == Qt::Key_PageDown))) {\n\t\t_scrollKeyEvents.fire(std::move(e));\n\t} else if (!(e->modifiers() & ~Qt::ShiftModifier)\n\t\t&& key != Qt::Key_Shift) {\n\t\t_delegate->listTryProcessKeyInput(e);\n\t} else {\n\t\te->ignore();\n\t}\n}\n\nauto ListWidget::scrollKeyEvents() const\n-> rpl::producer<not_null<QKeyEvent*>> {\n\treturn _scrollKeyEvents.events();\n}\n\nvoid ListWidget::mouseDoubleClickEvent(QMouseEvent *e) {\n\tregisterReadMetricsActivity();\n\tmouseActionStart(e->globalPos(), e->button());\n\ttrySwitchToWordSelection();\n\tif (!ClickHandler::getActive()\n\t\t&& !ClickHandler::getPressed()\n\t\t&& (_mouseCursorState == CursorState::None\n\t\t\t|| _mouseCursorState == CursorState::Date)\n\t\t&& _selected.empty()\n\t\t&& _overElement\n\t\t&& _overElement->data()->isRegular()) {\n\t\tmouseActionCancel();\n\t\tswitch (CurrentQuickAction()) {\n\t\tcase DoubleClickQuickAction::Reply: {\n\t\t\treplyToMessageRequestNotify({ _overElement->data()->fullId() });\n\t\t} break;\n\t\tcase DoubleClickQuickAction::React: {\n\t\t\ttoggleFavoriteReaction(_overElement);\n\t\t} break;\n\t\tdefault: break;\n\t\t}\n\t}\n}\n\nvoid ListWidget::toggleFavoriteReaction(not_null<Element*> view) const {\n\tconst auto item = view->data();\n\tconst auto favorite = session().data().reactions().favoriteId();\n\tif (!ranges::contains(\n\t\t\tData::LookupPossibleReactions(item).recent,\n\t\t\tfavorite,\n\t\t\t&Data::Reaction::id)\n\t\t|| _delegate->listShowReactPremiumError(item, favorite)) {\n\t\treturn;\n\t} else if (!ranges::contains(item->chosenReactions(), favorite)) {\n\t\tif (const auto top = itemTop(view); top >= 0) {\n\t\t\tview->animateReaction({ .id = favorite });\n\t\t}\n\t}\n\titem->toggleReaction(favorite, HistoryReactionSource::Quick);\n}\n\nvoid ListWidget::trySwitchToWordSelection() {\n\tauto selectingSome = (_mouseAction == MouseAction::Selecting)\n\t\t&& hasSelectedText();\n\tauto willSelectSome = (_mouseAction == MouseAction::None)\n\t\t&& !hasSelectedItems();\n\tauto checkSwitchToWordSelection = _overElement\n\t\t&& (_mouseSelectType == TextSelectType::Letters)\n\t\t&& (selectingSome || willSelectSome);\n\tif (checkSwitchToWordSelection) {\n\t\tswitchToWordSelection();\n\t}\n}\n\nvoid ListWidget::switchToWordSelection() {\n\tExpects(_overElement != nullptr);\n\n\tStateRequest request;\n\trequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;\n\tauto dragState = _overElement->textState(_pressState.point, request);\n\tif (dragState.cursor != CursorState::Text) {\n\t\treturn;\n\t}\n\t_mouseTextSymbol = dragState.symbol;\n\t_mouseSelectType = TextSelectType::Words;\n\tif (_mouseAction == MouseAction::None) {\n\t\t_mouseAction = MouseAction::Selecting;\n\t\tsetTextSelection(_overElement, TextSelection(\n\t\t\tdragState.symbol,\n\t\t\tdragState.symbol\n\t\t));\n\t}\n\tmouseActionUpdate();\n\n\t_trippleClickPoint = _mousePosition;\n\t_trippleClickStartTime = crl::now();\n}\n\nvoid ListWidget::validateTrippleClickStartTime() {\n\tif (_trippleClickStartTime) {\n\t\tconst auto elapsed = (crl::now() - _trippleClickStartTime);\n\t\tif (elapsed >= QApplication::doubleClickInterval()) {\n\t\t\t_trippleClickStartTime = 0;\n\t\t}\n\t}\n}\n\nvoid ListWidget::contextMenuEvent(QContextMenuEvent *e) {\n\tshowContextMenu(e);\n}\n\nvoid ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {\n\tif (e->reason() == QContextMenuEvent::Mouse) {\n\t\tmouseActionUpdate(e->globalPos());\n\t}\n\n\tconst auto link = ClickHandler::getActive();\n\tif (controller()->showFrozenError()) {\n\t\treturn;\n\t} else if (link\n\t\t&& !link->property(\n\t\t\tkSendReactionEmojiProperty).value<Data::ReactionId>().empty()\n\t\t&& _reactionsManager\n\t\t&& _reactionsManager->showContextMenu(\n\t\t\tthis,\n\t\t\te,\n\t\t\tsession().data().reactions().favoriteId())) {\n\t\treturn;\n\t}\n\tconst auto overItem = _overItemExact\n\t\t? _overItemExact\n\t\t: _overElement\n\t\t? _overElement->data().get()\n\t\t: nullptr;\n\tif (link\n\t\t&& link->property(kFastShareProperty).value<bool>()\n\t\t&& overItem) {\n\t\tif (const auto view = viewForItem(overItem)) {\n\t\t\tconst auto rightSize = view->rightActionSize().value_or(QSize());\n\t\t\tconst auto reactionsSkip = view->embedReactionsInBubble()\n\t\t\t\t? 0\n\t\t\t\t: view->reactionButtonParameters({}, {}).reactionsHeight;\n\t\t\tconst auto top = itemTop(view)\n\t\t\t\t+ view->height()\n\t\t\t\t- reactionsSkip\n\t\t\t\t- _visibleTop\n\t\t\t\t- rightSize.height();\n\t\t\tconst auto right = rect::right(view->innerGeometry())\n\t\t\t\t- st::historyFastShareLeft\n\t\t\t\t- rightSize.width();\n\t\t\tShowTopPeersSelector(\n\t\t\t\tthis,\n\t\t\t\tcontroller()->uiShow(),\n\t\t\t\toverItem->fullId(),\n\t\t\t\tparentWidget()->mapToGlobal(QPoint(right, top)));\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto clickedReaction = Reactions::ReactionIdOfLink(link);\n\tconst auto linkPhoneNumber = link\n\t\t? link->property(kPhoneNumberLinkProperty).toString()\n\t\t: QString();\n\tconst auto linkUserpicPeerId = (link && _overSenderUserpic)\n\t\t? PeerId(link->property(kPeerLinkPeerIdProperty).toULongLong())\n\t\t: PeerId();\n\t_whoReactedMenuLifetime.destroy();\n\tif (!clickedReaction.empty()\n\t\t&& overItem\n\t\t&& Api::WhoReactedExists(overItem, Api::WhoReactedList::One)) {\n\t\tHistoryView::ShowWhoReactedMenu(\n\t\t\t&_menu,\n\t\t\te->globalPos(),\n\t\t\tthis,\n\t\t\toverItem,\n\t\t\tclickedReaction,\n\t\t\tcontroller(),\n\t\t\t_whoReactedMenuLifetime);\n\t\te->accept();\n\t\treturn;\n\t} else if (!linkPhoneNumber.isEmpty()) {\n\t\tPhoneClickHandler(&session(), linkPhoneNumber).onClick(\n\t\t\tprepareClickContext(\n\t\t\t\tQt::LeftButton,\n\t\t\t\t_overItemExact ? _overItemExact->fullId() : FullMsgId()));\n\t\treturn;\n\t} else if (linkUserpicPeerId) {\n\t\t_menu = _delegate->listFillSenderUserpicMenu(linkUserpicPeerId);\n\t\tif (_menu) {\n\t\t\tWindow::AddSenderUserpicModerateAction(\n\t\t\t\tcontroller(),\n\t\t\t\t[&] {\n\t\t\t\t\tconst auto contextY = _visibleTop\n\t\t\t\t\t\t+ mapFromGlobal(e->globalPos()).y();\n\t\t\t\t\tconst auto contextView = strictFindItemByY(contextY);\n\t\t\t\t\treturn contextView ? contextView->data().get() : overItem;\n\t\t\t\t}(),\n\t\t\t\tUi::Menu::CreateAddActionCallback(_menu.get()));\n\t\t\t_menu->setForcedOrigin(Ui::PanelAnimation::Origin::BottomLeft);\n\t\t\t_menu->popup(e->globalPos());\n\t\t\te->accept();\n\t\t\treturn;\n\t\t}\n\t}\n\n\tauto request = ContextMenuRequest(controller());\n\n\trequest.link = link;\n\trequest.view = _overElement;\n\trequest.item = overItem;\n\trequest.pointState = _overState.pointState;\n\trequest.quote = (_overElement\n\t\t&& _selectedTextItem == _overElement->data())\n\t\t? _overElement->selectedQuote(_selectedTextRange)\n\t\t: SelectedQuote();\n\trequest.selectedText = _selectedText;\n\trequest.selectedItems = collectSelectedItems();\n\tconst auto hasSelection = !request.selectedItems.empty()\n\t\t|| !request.selectedText.empty();\n\trequest.overSelection = (showFromTouch && hasSelection)\n\t\t|| (_overElement\n\t\t\t&& isInsideSelection(\n\t\t\t\t_overElement,\n\t\t\t\t_overItemExact ? _overItemExact : _overElement->data().get(),\n\t\t\t\t_overState));\n\n\t_menu = FillContextMenu(this, request);\n\tif (_menu->empty()) {\n\t\t_menu = nullptr;\n\t\treturn;\n\t}\n\n\tusing namespace HistoryView::Reactions;\n\tconst auto desiredPosition = e->globalPos();\n\tconst auto reactItem = (_overElement\n\t\t&& _overState.pointState != PointState::Outside)\n\t\t? _overElement->data().get()\n\t\t: nullptr;\n\tconst auto attached = reactItem\n\t\t? AttachSelectorToMenu(\n\t\t\t_menu.get(),\n\t\t\tcontroller(),\n\t\t\tdesiredPosition,\n\t\t\treactItem,\n\t\t\t[=](ChosenReaction reaction) { reactionChosen(reaction); },\n\t\t\tItemReactionsAbout(reactItem))\n\t\t: AttachSelectorResult::Skipped;\n\tif (attached == AttachSelectorResult::Failed) {\n\t\t_menu = nullptr;\n\t\treturn;\n\t}\n\tHistoryView::AttachPollOptionTabs(_menu.get(), desiredPosition);\n\tif (attached == AttachSelectorResult::Attached) {\n\t\t_menu->popupPrepared();\n\t} else {\n\t\t_menu->popup(desiredPosition);\n\t}\n\te->accept();\n}\n\nvoid ListWidget::reactionChosen(ChosenReaction reaction) {\n\tconst auto item = session().data().message(reaction.context);\n\tif (!item) {\n\t\treturn;\n\t} else if (reaction.id.paid()) {\n\t\tPayments::ShowPaidReactionDetails(\n\t\t\tcontroller(),\n\t\t\titem,\n\t\t\tviewForItem(item),\n\t\t\tHistoryReactionSource::Selector);\n\t\treturn;\n\t} else if (_delegate->listShowReactPremiumError(item, reaction.id)) {\n\t\tif (_menu) {\n\t\t\t_menu->hideMenu();\n\t\t}\n\t\treturn;\n\t}\n\titem->toggleReaction(reaction.id, HistoryReactionSource::Selector);\n\tif (!ranges::contains(item->chosenReactions(), reaction.id)) {\n\t\treturn;\n\t} else if (const auto view = viewForItem(item)) {\n\t\tconst auto geometry = reaction.localGeometry.isEmpty()\n\t\t\t? mapFromGlobal(reaction.globalGeometry)\n\t\t\t: reaction.localGeometry;\n\t\tif (const auto top = itemTop(view); top >= 0) {\n\t\t\tview->animateReaction({\n\t\t\t\t.id = reaction.id,\n\t\t\t\t.flyIcon = reaction.icon,\n\t\t\t\t.flyFrom = geometry.translated(0, -top),\n\t\t\t});\n\t\t}\n\t}\n}\n\nvoid ListWidget::mousePressEvent(QMouseEvent *e) {\n\tif (_menu) {\n\t\te->accept();\n\t\treturn; // ignore mouse press, that was hiding context menu\n\t}\n\tif (_overlayHost) {\n\t\t_overlayHost->handleClickOutside(e->pos());\n\t}\n\tif (_middleClickAutoscroll.active()) {\n\t\t_middleClickAutoscroll.stop();\n\t\te->accept();\n\t\treturn;\n\t}\n\tif (e->button() == Qt::MiddleButton) {\n\t\tmouseActionCancel();\n\t\tClickHandler::unpressed();\n\t\t_middleClickAutoscroll.toggleOrBeginHold(e->globalPos());\n\t\te->accept();\n\t\treturn;\n\t}\n\t_mouseActive = true;\n\tregisterReadMetricsActivity();\n\tmouseActionStart(e->globalPos(), e->button());\n}\n\nvoid ListWidget::onTouchScrollTimer() {\n\tauto nowTime = crl::now();\n\tif (_touchScrollState == Ui::TouchScrollState::Acceleration && _touchWaitingAcceleration && (nowTime - _touchAccelerationTime) > 40) {\n\t\t_touchScrollState = Ui::TouchScrollState::Manual;\n\t\ttouchResetSpeed();\n\t} else if (_touchScrollState == Ui::TouchScrollState::Auto || _touchScrollState == Ui::TouchScrollState::Acceleration) {\n\t\tconst auto elapsed = int(nowTime - _touchTime);\n\t\tconst auto delta = _touchSpeed * elapsed / 1000;\n\t\tconst auto hasScrolled = _delegate->listScrollTo(\n\t\t\t_visibleTop - delta.y());\n\t\tif (_touchSpeed.isNull() || !hasScrolled) {\n\t\t\t_touchScrollState = Ui::TouchScrollState::Manual;\n\t\t\t_touchScroll = false;\n\t\t\t_touchScrollTimer.cancel();\n\t\t} else {\n\t\t\t_touchTime = nowTime;\n\t\t}\n\t\ttouchDeaccelerate(elapsed);\n\t}\n}\n\nvoid ListWidget::touchUpdateSpeed() {\n\tconst auto nowTime = crl::now();\n\tif (_touchPrevPosValid) {\n\t\tconst int elapsed = nowTime - _touchSpeedTime;\n\t\tif (elapsed) {\n\t\t\tconst QPoint newPixelDiff = (_touchPos - _touchPrevPos);\n\t\t\tconst QPoint pixelsPerSecond = newPixelDiff * (1000 / elapsed);\n\n\t\t\t// fingers are inacurates, we ignore small changes to avoid stopping the autoscroll because\n\t\t\t// of a small horizontal offset when scrolling vertically\n\t\t\tconst int newSpeedY = (qAbs(pixelsPerSecond.y()) > Ui::kFingerAccuracyThreshold) ? pixelsPerSecond.y() : 0;\n\t\t\tconst int newSpeedX = (qAbs(pixelsPerSecond.x()) > Ui::kFingerAccuracyThreshold) ? pixelsPerSecond.x() : 0;\n\t\t\tif (_touchScrollState == Ui::TouchScrollState::Auto) {\n\t\t\t\tconst int oldSpeedY = _touchSpeed.y();\n\t\t\t\tconst int oldSpeedX = _touchSpeed.x();\n\t\t\t\tif ((oldSpeedY <= 0 && newSpeedY <= 0) || ((oldSpeedY >= 0 && newSpeedY >= 0)\n\t\t\t\t\t&& (oldSpeedX <= 0 && newSpeedX <= 0)) || (oldSpeedX >= 0 && newSpeedX >= 0)) {\n\t\t\t\t\t_touchSpeed.setY(std::clamp(\n\t\t\t\t\t\t(oldSpeedY + (newSpeedY / 4)),\n\t\t\t\t\t\t-Ui::kMaxScrollAccelerated,\n\t\t\t\t\t\t+Ui::kMaxScrollAccelerated));\n\t\t\t\t\t_touchSpeed.setX(std::clamp(\n\t\t\t\t\t\t(oldSpeedX + (newSpeedX / 4)),\n\t\t\t\t\t\t-Ui::kMaxScrollAccelerated,\n\t\t\t\t\t\t+Ui::kMaxScrollAccelerated));\n\t\t\t\t} else {\n\t\t\t\t\t_touchSpeed = QPoint();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// we average the speed to avoid strange effects with the last delta\n\t\t\t\tif (!_touchSpeed.isNull()) {\n\t\t\t\t\t_touchSpeed.setX(std::clamp(\n\t\t\t\t\t\t(_touchSpeed.x() / 4) + (newSpeedX * 3 / 4),\n\t\t\t\t\t\t-Ui::kMaxScrollFlick,\n\t\t\t\t\t\t+Ui::kMaxScrollFlick));\n\t\t\t\t\t_touchSpeed.setY(std::clamp(\n\t\t\t\t\t\t(_touchSpeed.y() / 4) + (newSpeedY * 3 / 4),\n\t\t\t\t\t\t-Ui::kMaxScrollFlick,\n\t\t\t\t\t\t+Ui::kMaxScrollFlick));\n\t\t\t\t} else {\n\t\t\t\t\t_touchSpeed = QPoint(newSpeedX, newSpeedY);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\t_touchPrevPosValid = true;\n\t}\n\t_touchSpeedTime = nowTime;\n\t_touchPrevPos = _touchPos;\n}\n\nvoid ListWidget::touchResetSpeed() {\n\t_touchSpeed = QPoint();\n\t_touchPrevPosValid = false;\n}\n\nvoid ListWidget::touchDeaccelerate(int32 elapsed) {\n\tint32 x = _touchSpeed.x();\n\tint32 y = _touchSpeed.y();\n\t_touchSpeed.setX((x == 0) ? x : (x > 0) ? qMax(0, x - elapsed) : qMin(0, x + elapsed));\n\t_touchSpeed.setY((y == 0) ? y : (y > 0) ? qMax(0, y - elapsed) : qMin(0, y + elapsed));\n}\n\nvoid ListWidget::touchEvent(QTouchEvent *e) {\n\tif (e->type() == QEvent::TouchCancel) { // cancel\n\t\tif (!_touchInProgress) return;\n\t\t_touchInProgress = false;\n\t\t_touchSelectTimer.cancel();\n\t\t_touchScroll = _touchSelect = false;\n\t\t_touchScrollState = Ui::TouchScrollState::Manual;\n\t\t_touchMaybeSelecting = false;\n\t\tmouseActionCancel();\n\t\treturn;\n\t}\n\n\tif (!e->touchPoints().isEmpty()) {\n\t\t_touchPrevPos = _touchPos;\n\t\t_touchPos = e->touchPoints().cbegin()->screenPos().toPoint();\n\t}\n\tregisterReadMetricsActivity();\n\n\tswitch (e->type()) {\n\tcase QEvent::TouchBegin: {\n\t\tif (_menu) {\n\t\t\te->accept();\n\t\t\treturn; // ignore mouse press, that was hiding context menu\n\t\t}\n\t\tif (_touchInProgress) return;\n\t\tif (e->touchPoints().isEmpty()) return;\n\n\t\t_touchInProgress = true;\n\t\tif (_touchScrollState == Ui::TouchScrollState::Auto) {\n\t\t\t_touchMaybeSelecting = false;\n\t\t\t_touchScrollState = Ui::TouchScrollState::Acceleration;\n\t\t\t_touchWaitingAcceleration = true;\n\t\t\t_touchAccelerationTime = crl::now();\n\t\t\ttouchUpdateSpeed();\n\t\t\t_touchStart = _touchPos;\n\t\t} else {\n\t\t\t_touchScroll = false;\n\t\t\t_touchMaybeSelecting = true;\n\t\t\t_touchSelectTimer.callOnce(QApplication::startDragTime());\n\t\t}\n\t\t_touchSelect = false;\n\t\t_touchStart = _touchPrevPos = _touchPos;\n\t} break;\n\n\tcase QEvent::TouchUpdate: {\n\t\tif (!_touchInProgress) return;\n\t\tif (_touchSelect) {\n\t\t\tmouseActionUpdate(_touchPos);\n\t\t} else if (!_touchScroll && (_touchPos - _touchStart).manhattanLength() >= QApplication::startDragDistance()) {\n\t\t\t_touchSelectTimer.cancel();\n\t\t\t_touchMaybeSelecting = false;\n\t\t\t_touchScroll = true;\n\t\t\ttouchUpdateSpeed();\n\t\t}\n\t\tif (_touchScroll) {\n\t\t\tif (_touchScrollState == Ui::TouchScrollState::Manual) {\n\t\t\t\ttouchScrollUpdated(_touchPos);\n\t\t\t} else if (_touchScrollState == Ui::TouchScrollState::Acceleration) {\n\t\t\t\ttouchUpdateSpeed();\n\t\t\t\t_touchAccelerationTime = crl::now();\n\t\t\t\tif (_touchSpeed.isNull()) {\n\t\t\t\t\t_touchScrollState = Ui::TouchScrollState::Manual;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} break;\n\n\tcase QEvent::TouchEnd: {\n\t\tif (!_touchInProgress) {\n\t\t\treturn;\n\t\t}\n\t\t_touchInProgress = false;\n\t\tauto weak = base::make_weak(this);\n\t\tconst auto notMoved = (_touchPos - _touchStart).manhattanLength()\n\t\t\t< QApplication::startDragDistance();\n\t\tif (_touchSelect) {\n\t\t\tif (notMoved || _touchMaybeSelecting.current()) {\n\t\t\t\tmouseActionFinish(_touchPos, Qt::RightButton);\n\t\t\t\tauto contextMenu = QContextMenuEvent(\n\t\t\t\t\tQContextMenuEvent::Mouse,\n\t\t\t\t\tmapFromGlobal(_touchPos),\n\t\t\t\t\t_touchPos);\n\t\t\t\tshowContextMenu(&contextMenu, true);\n\t\t\t}\n\t\t\t_touchScroll = false;\n\t\t} else if (_touchScroll) {\n\t\t\tif (_touchScrollState == Ui::TouchScrollState::Manual) {\n\t\t\t\t_touchScrollState = Ui::TouchScrollState::Auto;\n\t\t\t\t_touchPrevPosValid = false;\n\t\t\t\t_touchScrollTimer.callEach(15);\n\t\t\t\t_touchTime = crl::now();\n\t\t\t} else if (_touchScrollState == Ui::TouchScrollState::Auto) {\n\t\t\t\t_touchScrollState = Ui::TouchScrollState::Manual;\n\t\t\t\t_touchScroll = false;\n\t\t\t\ttouchResetSpeed();\n\t\t\t} else if (_touchScrollState == Ui::TouchScrollState::Acceleration) {\n\t\t\t\t_touchScrollState = Ui::TouchScrollState::Auto;\n\t\t\t\t_touchWaitingAcceleration = false;\n\t\t\t\t_touchPrevPosValid = false;\n\t\t\t}\n\t\t} else if (notMoved) { // One short tap is like left mouse click.\n\t\t\tmouseActionStart(_touchPos, Qt::LeftButton);\n\t\t\tmouseActionFinish(_touchPos, Qt::LeftButton);\n\t\t}\n\t\tif (weak) {\n\t\t\t_touchSelectTimer.cancel();\n\t\t\t_touchMaybeSelecting = false;\n\t\t\t_touchSelect = false;\n\t\t}\n\t} break;\n\t}\n}\n\nvoid ListWidget::mouseMoveEvent(QMouseEvent *e) {\n\tstatic auto lastGlobalPosition = e->globalPos();\n\tauto reallyMoved = (lastGlobalPosition != e->globalPos());\n\tif (_middleClickAutoscroll.active()) {\n\t\tif (reallyMoved) {\n\t\t\t_mouseActive = true;\n\t\t\tlastGlobalPosition = e->globalPos();\n\t\t}\n\t\te->accept();\n\t\treturn;\n\t}\n\tauto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton));\n\tif (!buttonsPressed && _mouseAction != MouseAction::None) {\n\t\tmouseReleaseEvent(e);\n\t}\n\tif (reallyMoved) {\n\t\t_mouseActive = true;\n\t\tregisterReadMetricsActivity();\n\t\tlastGlobalPosition = e->globalPos();\n\t\tif (!buttonsPressed\n\t\t\t|| (_scrollDateLink\n\t\t\t\t&& ClickHandler::getPressed() == _scrollDateLink)) {\n\t\t\tkeepScrollDateForNow();\n\t\t}\n\t}\n\tmouseActionUpdate(e->globalPos());\n}\n\nvoid ListWidget::mouseReleaseEvent(QMouseEvent *e) {\n\tif (_middleClickAutoscroll.finishHold(e->button())) {\n\t\te->accept();\n\t\treturn;\n\t}\n\tif (_middleClickAutoscroll.active()) {\n\t\te->accept();\n\t\treturn;\n\t}\n\tregisterReadMetricsActivity();\n\tmouseActionFinish(e->globalPos(), e->button());\n\tif (!rect().contains(e->pos())) {\n\t\tleaveEvent(e);\n\t}\n}\n\nvoid ListWidget::markReadMetricsStale() {\n\t_readMetricsStale = true;\n\tupdate();\n}\n\nvoid ListWidget::registerReadMetricsActivity() {\n\tif (_readMetricsTracker && markingContentsRead()) {\n\t\t_readMetricsTracker->registerActivity();\n\t}\n}\n\nvoid ListWidget::touchScrollUpdated(const QPoint &screenPos) {\n\t_touchPos = screenPos;\n\t_delegate->listScrollTo(\n\t\t_visibleTop - (_touchPos - _touchPrevPos).y(),\n\t\tfalse);\n\ttouchUpdateSpeed();\n}\n\nrpl::producer<bool> ListWidget::touchMaybeSelectingValue() const {\n\treturn _touchMaybeSelecting.value();\n}\n\nvoid ListWidget::enterEventHook(QEnterEvent *e) {\n\t_mouseActive = true;\n\tmouseActionUpdate(QCursor::pos());\n\treturn RpWidget::enterEventHook(e);\n}\n\nvoid ListWidget::leaveEventHook(QEvent *e) {\n\tif (_reactionsManager) {\n\t\t_reactionsManager->updateButton({ .cursorLeft = true });\n\t}\n\tif (_replyButtonManager) {\n\t\t_replyButtonManager->updateButton({});\n\t}\n\tif (const auto view = _overElement) {\n\t\tif (_overState.pointState != PointState::Outside) {\n\t\t\trepaintItem(view);\n\t\t\t_overState.pointState = PointState::Outside;\n\t\t}\n\t}\n\tClickHandler::clearActive();\n\tUi::Tooltip::Hide();\n\tif (!ClickHandler::getPressed() && _cursor != style::cur_default) {\n\t\t_cursor = style::cur_default;\n\t\tsetCursor(_cursor);\n\t}\n\t_mouseActive = false;\n\treturn RpWidget::leaveEventHook(e);\n}\n\nvoid ListWidget::updateDragSelection() {\n\tif (!_overState.itemId\n\t\t|| !_pressState.itemId\n\t\t|| hasSelectRestriction()) {\n\t\tclearDragSelection();\n\t\treturn;\n\t} else if (_items.empty() || !_overElement || !_selectEnabled) {\n\t\treturn;\n\t}\n\tconst auto pressItem = session().data().message(_pressState.itemId);\n\tif (!pressItem) {\n\t\treturn;\n\t}\n\n\tconst auto overView = _overElement;\n\tconst auto pressView = viewForItem(pressItem);\n\tconst auto selectingUp = _delegate->listIsLessInOrder(\n\t\toverView->data(),\n\t\tpressItem);\n\tif (selectingUp != _dragSelectDirectionUp) {\n\t\t_dragSelectDirectionUp = selectingUp;\n\t\t_dragSelectAction = DragSelectAction::None;\n\t}\n\tconst auto fromView = selectingUp ? overView : pressView;\n\tconst auto tillView = selectingUp ? pressView : overView;\n\tconst auto fromState = selectingUp ? _overState : _pressState;\n\tconst auto tillState = selectingUp ? _pressState : _overState;\n\tupdateDragSelection(fromView, fromState, tillView, tillState);\n}\n\nvoid ListWidget::onTouchSelect() {\n\t_touchSelect = true;\n\t_touchMaybeSelecting = true;\n\tmouseActionStart(_touchPos, Qt::LeftButton);\n}\n\nvoid ListWidget::updateDragSelection(\n\t\tconst Element *fromView,\n\t\tconst MouseState &fromState,\n\t\tconst Element *tillView,\n\t\tconst MouseState &tillState) {\n\tExpects(fromView != nullptr || tillView != nullptr);\n\n\tconst auto delta = QApplication::startDragDistance();\n\n\tconst auto includeFrom = [&] (\n\t\t\tnot_null<const Element*> view,\n\t\t\tconst MouseState &state) {\n\t\tconst auto bottom = view->height() - view->marginBottom();\n\t\treturn (state.point.y() < bottom - delta);\n\t};\n\tconst auto includeTill = [&] (\n\t\t\tnot_null<const Element*> view,\n\t\t\tconst MouseState &state) {\n\t\tconst auto top = view->marginTop();\n\t\treturn (state.point.y() >= top + delta);\n\t};\n\tconst auto includeSingleItem = [&] (\n\t\t\tnot_null<const Element*> view,\n\t\t\tconst MouseState &state1,\n\t\t\tconst MouseState &state2) {\n\t\tconst auto top = view->marginTop();\n\t\tconst auto bottom = view->height() - view->marginBottom();\n\t\tconst auto y1 = std::min(state1.point.y(), state2.point.y());\n\t\tconst auto y2 = std::max(state1.point.y(), state2.point.y());\n\t\treturn (y1 < bottom - delta && y2 >= top + delta)\n\t\t\t? (y2 - y1 >= delta)\n\t\t\t: false;\n\t};\n\n\tconst auto from = [&] {\n\t\tconst auto result = fromView ? ranges::find(\n\t\t\t_items,\n\t\t\tfromView,\n\t\t\t[](auto view) { return view.get(); }) : end(_items);\n\t\treturn (result == end(_items))\n\t\t\t? begin(_items)\n\t\t\t: (fromView == tillView || includeFrom(fromView, fromState))\n\t\t\t? result\n\t\t\t: (result + 1);\n\t}();\n\tconst auto till = [&] {\n\t\tif (fromView == tillView) {\n\t\t\treturn (from == end(_items))\n\t\t\t\t? from\n\t\t\t\t: includeSingleItem(fromView, fromState, tillState)\n\t\t\t\t? (from + 1)\n\t\t\t\t: from;\n\t\t}\n\t\tconst auto result = tillView ? ranges::find(\n\t\t\t_items,\n\t\t\ttillView,\n\t\t\t[](auto view) { return view.get(); }) : end(_items);\n\t\treturn (result == end(_items))\n\t\t\t? end(_items)\n\t\t\t: includeTill(tillView, tillState)\n\t\t\t? (result + 1)\n\t\t\t: result;\n\t}();\n\tif (from < till) {\n\t\tupdateDragSelection(from, till);\n\t} else {\n\t\tclearDragSelection();\n\t}\n}\n\nvoid ListWidget::updateDragSelection(\n\t\tstd::vector<not_null<Element*>>::const_iterator from,\n\t\tstd::vector<not_null<Element*>>::const_iterator till) {\n\tExpects(from < till);\n\n\tconst auto &groups = session().data().groups();\n\tconst auto changeItem = [&](not_null<HistoryItem*> item, bool add) {\n\t\tconst auto itemId = item->fullId();\n\t\tif (add) {\n\t\t\t_dragSelected.emplace(itemId);\n\t\t} else {\n\t\t\t_dragSelected.remove(itemId);\n\t\t}\n\t};\n\tconst auto changeGroup = [&](not_null<HistoryItem*> item, bool add) {\n\t\tif (const auto group = groups.find(item)) {\n\t\t\tfor (const auto &item : group->items) {\n\t\t\t\tif (!_delegate->listIsItemGoodForSelection(item)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const auto &item : group->items) {\n\t\t\t\tchangeItem(item, add);\n\t\t\t}\n\t\t} else if (_delegate->listIsItemGoodForSelection(item)) {\n\t\t\tchangeItem(item, add);\n\t\t}\n\t};\n\tconst auto changeView = [&](not_null<Element*> view, bool add) {\n\t\tif (!view->isHidden()) {\n\t\t\tchangeGroup(view->data(), add);\n\t\t}\n\t};\n\tfor (auto i = begin(_items); i != from; ++i) {\n\t\tchangeView(*i, false);\n\t}\n\tfor (auto i = from; i != till; ++i) {\n\t\tchangeView(*i, true);\n\t}\n\tfor (auto i = till; i != end(_items); ++i) {\n\t\tchangeView(*i, false);\n\t}\n\n\tensureDragSelectAction(from, till);\n\tupdate();\n}\n\nvoid ListWidget::ensureDragSelectAction(\n\t\tstd::vector<not_null<Element*>>::const_iterator from,\n\t\tstd::vector<not_null<Element*>>::const_iterator till) {\n\tif (_dragSelectAction != DragSelectAction::None) {\n\t\treturn;\n\t}\n\tconst auto start = _dragSelectDirectionUp ? (till - 1) : from;\n\tconst auto startId = (*start)->data()->fullId();\n\t_dragSelectAction = _selected.contains(startId)\n\t\t? DragSelectAction::Deselecting\n\t\t: DragSelectAction::Selecting;\n\tif (!_wasSelectedText\n\t\t&& !_dragSelected.empty()\n\t\t&& _dragSelectAction == DragSelectAction::Selecting) {\n\t\t_wasSelectedText = true;\n\t\tsetFocus();\n\t}\n}\n\nvoid ListWidget::clearDragSelection() {\n\t_dragSelectAction = DragSelectAction::None;\n\tif (!_dragSelected.empty()) {\n\t\t_dragSelected.clear();\n\t\tupdate();\n\t}\n}\n\nvoid ListWidget::mouseActionStart(\n\t\tconst QPoint &globalPosition,\n\t\tQt::MouseButton button) {\n\tmouseActionUpdate(globalPosition);\n\tif (button != Qt::LeftButton) {\n\t\treturn;\n\t}\n\n\tClickHandler::pressed();\n\tif (_pressState != _overState) {\n\t\tif (_pressState.itemId != _overState.itemId) {\n\t\t\trepaintItem(_pressState.itemId);\n\t\t}\n\t\t_pressState = _overState;\n\t\trepaintItem(_overState.itemId);\n\t}\n\t_pressItemExact = _overItemExact;\n\tconst auto pressElement = _overElement;\n\n\t_mouseAction = MouseAction::None;\n\t_pressWasInactive = Ui::WasInactivePress(window());\n\tif (_pressWasInactive) {\n\t\tUi::MarkInactivePress(window(), false);\n\t}\n\n\tconst auto pressed = ClickHandler::getPressed();\n\tif (pressed\n\t\t&& (!_overElement\n\t\t\t|| _overState.pointState == PointState::Outside\n\t\t\t|| !_overElement->allowTextSelectionByHandler(pressed))) {\n\t\t_mouseAction = MouseAction::PrepareDrag;\n\t} else if (hasSelectedItems()) {\n\t\tif (overSelectedItems()) {\n\t\t\t_mouseAction = MouseAction::PrepareDrag;\n\t\t} else if (!_pressWasInactive && !hasSelectRestriction()) {\n\t\t\t_mouseAction = MouseAction::PrepareSelect;\n\t\t}\n\t}\n\tif (_mouseAction == MouseAction::None && pressElement) {\n\t\tvalidateTrippleClickStartTime();\n\t\tTextState dragState;\n\t\tauto startDistance = (globalPosition - _trippleClickPoint).manhattanLength();\n\t\tauto validStartPoint = startDistance < QApplication::startDragDistance();\n\t\tif (_trippleClickStartTime != 0 && validStartPoint) {\n\t\t\tStateRequest request;\n\t\t\trequest.flags = Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t\tdragState = pressElement->textState(_pressState.point, request);\n\t\t\tif (dragState.cursor == CursorState::Text) {\n\t\t\t\tsetTextSelection(pressElement, TextSelection(\n\t\t\t\t\tdragState.symbol,\n\t\t\t\t\tdragState.symbol\n\t\t\t\t));\n\t\t\t\t_mouseTextSymbol = dragState.symbol;\n\t\t\t\t_mouseAction = MouseAction::Selecting;\n\t\t\t\t_mouseSelectType = TextSelectType::Paragraphs;\n\t\t\t\tmouseActionUpdate();\n\t\t\t\t_trippleClickStartTime = crl::now();\n\t\t\t}\n\t\t} else if (pressElement) {\n\t\t\tStateRequest request;\n\t\t\trequest.flags = Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t\tdragState = pressElement->textState(_pressState.point, request);\n\t\t}\n\t\tif (_mouseSelectType != TextSelectType::Paragraphs) {\n\t\t\t_mouseTextSymbol = dragState.symbol;\n\t\t\tif (isPressInSelectedText(dragState)) {\n\t\t\t\t_mouseAction = MouseAction::PrepareDrag; // start text drag\n\t\t\t} else if (!_pressWasInactive) {\n\t\t\t\tif (requiredToStartDragging(pressElement)\n\t\t\t\t\t&& _pressState.pointState != PointState::Outside) {\n\t\t\t\t\t_mouseAction = MouseAction::PrepareDrag;\n\t\t\t\t} else {\n\t\t\t\t\tif (dragState.afterSymbol) ++_mouseTextSymbol;\n\t\t\t\t\tif (!hasSelectedItems()\n\t\t\t\t\t\t&& _overState.pointState != PointState::Outside) {\n\t\t\t\t\t\tsetTextSelection(pressElement, TextSelection(\n\t\t\t\t\t\t\t_mouseTextSymbol,\n\t\t\t\t\t\t\t_mouseTextSymbol));\n\t\t\t\t\t\t_mouseAction = MouseAction::Selecting;\n\t\t\t\t\t} else if (!hasSelectRestriction()) {\n\t\t\t\t\t\t_mouseAction = MouseAction::PrepareSelect;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (!pressElement) {\n\t\t_mouseAction = MouseAction::None;\n\t} else if (_mouseAction == MouseAction::None) {\n\t\tmouseActionCancel();\n\t}\n}\n\nReactions::ButtonParameters ListWidget::reactionButtonParameters(\n\t\tnot_null<const Element*> view,\n\t\tQPoint position,\n\t\tconst TextState &reactionState) const {\n\tif (!_useCornerReaction) {\n\t\treturn {};\n\t}\n\tconst auto top = itemTop(view);\n\tif (top < 0\n\t\t|| !view->data()->canReact()\n\t\t|| _mouseAction == MouseAction::Dragging\n\t\t|| inSelectionMode().inSelectionMode) {\n\t\treturn {};\n\t}\n\tauto result = view->reactionButtonParameters(\n\t\tposition,\n\t\treactionState\n\t).translated({ 0, top });\n\tresult.visibleTop = _visibleTop;\n\tresult.visibleBottom = _visibleBottom;\n\tresult.globalPointer = _mousePosition;\n\treturn result;\n}\n\nReplyButton::ButtonParameters ListWidget::replyButtonParameters(\n\t\tnot_null<const Element*> view,\n\t\tQPoint position,\n\t\tconst TextState &replyState) const {\n\tif (!_useCornerReply) {\n\t\treturn {};\n\t}\n\tconst auto top = itemTop(view);\n\tif (top < 0\n\t\t|| _mouseAction == MouseAction::Dragging\n\t\t|| inSelectionMode().inSelectionMode) {\n\t\treturn {};\n\t}\n\tauto result = view->replyButtonParameters(\n\t\tposition,\n\t\treplyState\n\t).translated({ 0, top });\n\tresult.visibleTop = _visibleTop;\n\tresult.visibleBottom = _visibleBottom;\n\tresult.globalPointer = _mousePosition;\n\treturn result;\n}\n\nvoid ListWidget::mouseActionUpdate(const QPoint &globalPosition) {\n\t_mousePosition = globalPosition;\n\tmouseActionUpdate();\n}\n\nvoid ListWidget::mouseActionCancel() {\n\t_pressState = MouseState();\n\t_pressItemExact = nullptr;\n\t_mouseAction = MouseAction::None;\n\tclearDragSelection();\n\t_wasSelectedText = false;\n\t_selectScroll.cancel();\n}\n\nvoid ListWidget::mouseActionFinish(\n\t\tconst QPoint &globalPosition,\n\t\tQt::MouseButton button) {\n\tmouseActionUpdate(globalPosition);\n\n\tauto pressState = base::take(_pressState);\n\tbase::take(_pressItemExact);\n\trepaintItem(pressState.itemId);\n\n\tconst auto toggleByHandler = [&](const ClickHandlerPtr &handler) {\n\t\t// If we are in selecting items mode perhaps we want to\n\t\t// toggle selection instead of activating the pressed link.\n\t\treturn _overElement\n\t\t\t&& _overElement->toggleSelectionByHandlerClick(handler);\n\t};\n\n\tauto activated = ClickHandler::unpressed();\n\tauto simpleSelectionChange = pressState.itemId\n\t\t&& !_pressWasInactive\n\t\t&& (button != Qt::RightButton)\n\t\t&& (_mouseAction == MouseAction::PrepareSelect\n\t\t\t|| _mouseAction == MouseAction::PrepareDrag);\n\tauto needItemSelectionToggle = simpleSelectionChange\n\t\t&& (!activated || toggleByHandler(activated))\n\t\t&& hasSelectedItems();\n\tauto needTextSelectionClear = simpleSelectionChange\n\t\t&& hasSelectedText();\n\n\t_wasSelectedText = false;\n\n\tif (_mouseAction == MouseAction::Dragging || needItemSelectionToggle) {\n\t\tactivated = nullptr;\n\t} else if (activated) {\n\t\tmouseActionCancel();\n\t\tActivateClickHandler(window(), activated, {\n\t\t\tbutton,\n\t\t\tQVariant::fromValue(\n\t\t\t\tprepareClickHandlerContext(pressState.itemId))\n\t\t});\n\t\treturn;\n\t}\n\tif (needItemSelectionToggle) {\n\t\tif (const auto item = session().data().message(pressState.itemId)) {\n\t\t\tclearTextSelection();\n\t\t\tif (pressState.pointState == PointState::GroupPart) {\n\t\t\t\tchangeSelection(\n\t\t\t\t\t_selected,\n\t\t\t\t\t_overItemExact ? _overItemExact : item,\n\t\t\t\t\tSelectAction::Invert);\n\t\t\t} else {\n\t\t\t\tchangeSelectionAsGroup(\n\t\t\t\t\t_selected,\n\t\t\t\t\titem,\n\t\t\t\t\tSelectAction::Invert);\n\t\t\t}\n\t\t\tpushSelectedItems();\n\t\t}\n\t} else if (needTextSelectionClear) {\n\t\tclearTextSelection();\n\t} else if (simpleSelectionChange\n\t\t&& _mouseCursorState == CursorState::Date\n\t\t&& !hasSelectRestriction()) {\n\t\tif (const auto item = session().data().message(pressState.itemId)) {\n\t\t\tif (_delegate->listIsItemGoodForSelection(item)) {\n\t\t\t\tclearTextSelection();\n\t\t\t\tchangeSelectionAsGroup(\n\t\t\t\t\t_selected,\n\t\t\t\t\titem,\n\t\t\t\t\tSelectAction::Select);\n\t\t\t\tpushSelectedItems();\n\t\t\t\tupdate();\n\t\t\t}\n\t\t}\n\t} else if (_mouseAction == MouseAction::Selecting) {\n\t\tif (!_dragSelected.empty()) {\n\t\t\tapplyDragSelection();\n\t\t} else if (_selectedTextItem && !_pressWasInactive) {\n\t\t\tif (_selectedTextRange.from == _selectedTextRange.to) {\n\t\t\t\tclearTextSelection();\n\t\t\t\t_delegate->listWindowSetInnerFocus();\n\t\t\t}\n\t\t}\n\t}\n\t_mouseAction = MouseAction::None;\n\t_mouseSelectType = TextSelectType::Letters;\n\t_selectScroll.cancel();\n\n\tif (QGuiApplication::clipboard()->supportsSelection()\n\t\t&& _selectedTextItem\n\t\t&& _selectedTextRange.from != _selectedTextRange.to\n\t\t&& !hasCopyRestriction(_selectedTextItem)) {\n\t\tif (const auto view = viewForItem(_selectedTextItem)) {\n\t\t\tTextUtilities::SetClipboardText(\n\t\t\t\tview->selectedText(_selectedTextRange),\n\t\t\t\tQClipboard::Selection);\n\t\t}\n\t}\n}\n\nClickHandlerContext ListWidget::prepareClickHandlerContext(FullMsgId id) {\n\treturn {\n\t\t.itemId = id,\n\t\t.elementDelegate = [weak = base::make_weak(this)] {\n\t\t\treturn (ElementDelegate*)weak.get();\n\t\t},\n\t\t.sessionWindow = base::make_weak(controller()),\n\t};\n}\n\nClickContext ListWidget::prepareClickContext(\n\t\tQt::MouseButton button,\n\t\tFullMsgId itemId) {\n\treturn {\n\t\tbutton,\n\t\tQVariant::fromValue(prepareClickHandlerContext(itemId)),\n\t};\n}\n\nint ListWidget::SelectionViewOffset(\n\t\tnot_null<const ListWidget*> inner,\n\t\tnot_null<const Element*> view) {\n\tif (inner->_lastInSelectionMode) {\n\t\tconst auto translation\n\t\t\t= Element::AdditionalSpaceForSelectionCheckbox(view);\n\t\tconst auto progress = inner->_inSelectionModeAnimation.value(1.);\n\t\treturn translation * progress;\n\t}\n\treturn 0;\n}\n\n\nvoid ListWidget::mouseActionUpdate() {\n\tif (!_mouseActive && !window()->isActiveWindow()) {\n\t\treturn;\n\t}\n\n\tauto mousePosition = mapFromGlobal(_mousePosition);\n\tauto point = QPoint(\n\t\tstd::clamp(mousePosition.x(), 0, width()),\n\t\tstd::clamp(mousePosition.y(), _visibleTop, _visibleBottom));\n\n\tconst auto reactionState = _reactionsManager\n\t\t? _reactionsManager->buttonTextState(point)\n\t\t: TextState();\n\tconst auto reactionItem = session().data().message(reactionState.itemId);\n\tconst auto reactionView = viewForItem(reactionItem);\n\tconst auto replyBtnState = (_replyButtonManager && !reactionView)\n\t\t? _replyButtonManager->buttonTextState(point)\n\t\t: TextState();\n\tconst auto replyBtnItem = session().data().message(replyBtnState.itemId);\n\tconst auto replyBtnView = viewForItem(replyBtnItem);\n\tconst auto view = reactionView\n\t\t? reactionView\n\t\t: replyBtnView\n\t\t? replyBtnView\n\t\t: strictFindItemByY(point.y());\n\tconst auto item = view ? view->data().get() : nullptr;\n\tif (view) {\n\t\tpoint -= QPoint(SelectionViewOffset(this, view), 0);\n\t}\n\tconst auto itemPoint = mapPointToItem(point, view);\n\t_overState = MouseState(\n\t\titem ? item->fullId() : FullMsgId(),\n\t\tview ? view->height() : 0,\n\t\titemPoint,\n\t\tview ? view->pointState(itemPoint) : PointState::Outside);\n\t_overItemExact = nullptr;\n\tconst auto viewChanged = (_overElement != view);\n\tif (viewChanged) {\n\t\trepaintItem(_overElement);\n\t\t_overElement = view;\n\t\trepaintItem(_overElement);\n\t}\n\tif (_reactionsManager) {\n\t\t_reactionsManager->updateButton(view\n\t\t\t? reactionButtonParameters(\n\t\t\t\tview,\n\t\t\t\titemPoint,\n\t\t\t\treactionState)\n\t\t\t: Reactions::ButtonParameters());\n\t}\n\tif (_replyButtonManager) {\n\t\t_replyButtonManager->updateButton(view\n\t\t\t? replyButtonParameters(view, itemPoint, replyBtnState)\n\t\t\t: ReplyButton::ButtonParameters());\n\t}\n\tif (viewChanged && view) {\n\t\t_reactionsItem = item;\n\t}\n\n\tTextState dragState;\n\tClickHandlerHost *lnkhost = nullptr;\n\tauto inTextSelection = (_overState.pointState != PointState::Outside)\n\t\t&& (_overState.itemId == _pressState.itemId)\n\t\t&& hasSelectedText();\n\tauto dragStateUserpic = false;\n\tconst auto overReplyBtn = replyBtnView && replyBtnState.link;\n\tconst auto overReaction = reactionView && reactionState.link;\n\tif (overReaction) {\n\t\tdragState = reactionState;\n\t\tlnkhost = reactionView;\n\t} else if (overReplyBtn) {\n\t\tdragState = replyBtnState;\n\t\tlnkhost = _replyButtonManager.get();\n\t} else if (view) {\n\t\tauto cursorDeltaLength = [&] {\n\t\t\tauto cursorDelta = (_overState.point - _pressState.point);\n\t\t\treturn cursorDelta.manhattanLength();\n\t\t};\n\t\tauto dragStartLength = [] {\n\t\t\treturn QApplication::startDragDistance();\n\t\t};\n\t\tif (_overState.itemId != _pressState.itemId\n\t\t\t|| cursorDeltaLength() >= dragStartLength()) {\n\t\t\tif (_mouseAction == MouseAction::PrepareDrag) {\n\t\t\t\t_mouseAction = MouseAction::Dragging;\n\t\t\t\tInvokeQueued(this, [this] { performDrag(); });\n\t\t\t} else if (_mouseAction == MouseAction::PrepareSelect) {\n\t\t\t\t_mouseAction = MouseAction::Selecting;\n\t\t\t}\n\t\t}\n\t\tStateRequest request;\n\t\tif (_mouseAction == MouseAction::Selecting) {\n\t\t\trequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t} else {\n\t\t\tinTextSelection = false;\n\t\t}\n\t\tif (base::IsAltPressed()) {\n\t\t\trequest.flags &= ~Ui::Text::StateRequest::Flag::LookupLink;\n\t\t}\n\n\t\tconst auto dateHeight = st::msgServicePadding.bottom()\n\t\t\t+ st::msgServiceFont->height\n\t\t\t+ st::msgServicePadding.top();\n\t\tconst auto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.);\n\t\tenumerateDates([&](not_null<Element*> view, int itemtop, int dateTop) {\n\t\t\t// stop enumeration if the date is above our point\n\t\t\tif (dateTop + dateHeight <= point.y()) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst auto displayDate = view->displayDate();\n\t\t\tauto dateInPlace = displayDate;\n\t\t\tif (dateInPlace) {\n\t\t\t\tconst auto correctDateTop = itemtop + st::msgServiceMargin.top();\n\t\t\t\tdateInPlace = (dateTop < correctDateTop + dateHeight);\n\t\t\t}\n\n\t\t\t// stop enumeration if we've found a date under the cursor\n\t\t\tif (dateTop <= point.y()) {\n\t\t\t\tauto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity;\n\t\t\t\tif (opacity > 0.) {\n\t\t\t\t\tauto dateWidth = 0;\n\t\t\t\t\tif (const auto date = view->Get<HistoryView::DateBadge>()) {\n\t\t\t\t\t\tdateWidth = date->width;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdateWidth = st::msgServiceFont->width(langDayOfMonthFull(view->dateTime().date()));\n\t\t\t\t\t}\n\t\t\t\t\tdateWidth += st::msgServicePadding.left() + st::msgServicePadding.right();\n\t\t\t\t\tauto dateLeft = st::msgServiceMargin.left();\n\t\t\t\t\tauto maxwidth = view->width();\n\t\t\t\t\tif (_isChatWide) {\n\t\t\t\t\t\tmaxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));\n\t\t\t\t\t}\n\t\t\t\t\tauto widthForDate = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left();\n\n\t\t\t\t\tdateLeft += (widthForDate - dateWidth) / 2;\n\n\t\t\t\t\tif (point.x() >= dateLeft && point.x() < dateLeft + dateWidth) {\n\t\t\t\t\t\t_delegate->listUpdateDateLink(_scrollDateLink, view);\n\t\t\t\t\t\tdragState = TextState(\n\t\t\t\t\t\t\tnullptr,\n\t\t\t\t\t\t\t_scrollDateLink);\n\t\t\t\t\t\t_overItemExact = session().data().message(dragState.itemId);\n\t\t\t\t\t\tlnkhost = view;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\t\tif (!dragState.link) {\n\t\t\tdragState = view->textState(itemPoint, request);\n\t\t\t_overItemExact = session().data().message(dragState.itemId);\n\t\t\tlnkhost = view;\n\t\t\tif (!dragState.link\n\t\t\t\t&& itemPoint.x() >= st::historyPhotoLeft\n\t\t\t\t&& itemPoint.x() < st::historyPhotoLeft + st::msgPhotoSize) {\n\t\t\t\tif (view->hasFromPhoto()) {\n\t\t\t\t\tenumerateUserpics([&](not_null<Element*> view, int userpicTop) {\n\t\t\t\t\t\t// stop enumeration if the userpic is below our point\n\t\t\t\t\t\tif (userpicTop > point.y()) {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// stop enumeration if we've found a userpic under the cursor\n\t\t\t\t\t\tif (point.y() >= userpicTop && point.y() < userpicTop + st::msgPhotoSize) {\n\t\t\t\t\t\t\tdragState = TextState(nullptr, view->fromPhotoLink());\n\t\t\t\t\t\t\tdragStateUserpic = true;\n\t\t\t\t\t\t\t_overItemExact = nullptr;\n\t\t\t\t\t\t\tlnkhost = view;\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tconst auto lnkChanged = ClickHandler::setActive(dragState.link, lnkhost);\n\t_overSenderUserpic = dragStateUserpic;\n\tif (lnkChanged || dragState.cursor != _mouseCursorState) {\n\t\tUi::Tooltip::Hide();\n\t}\n\tif (dragState.link\n\t\t|| dragState.cursor == CursorState::Date\n\t\t|| dragState.cursor == CursorState::Forwarded) {\n\t\tUi::Tooltip::Show(1000, this);\n\t}\n\n\tif (_mouseAction == MouseAction::None) {\n\t\t_mouseCursorState = dragState.cursor;\n\t\tauto cursor = computeMouseCursor();\n\t\tif (_cursor != cursor) {\n\t\t\tsetCursor((_cursor = cursor));\n\t\t}\n\t} else if (view) {\n\t\tif (_mouseAction == MouseAction::Selecting) {\n\t\t\tif (inTextSelection) {\n\t\t\t\tauto second = dragState.symbol;\n\t\t\t\tif (dragState.afterSymbol\n\t\t\t\t\t&& _mouseSelectType == TextSelectType::Letters) {\n\t\t\t\t\t++second;\n\t\t\t\t}\n\t\t\t\tauto selection = TextSelection(\n\t\t\t\t\tqMin(second, _mouseTextSymbol),\n\t\t\t\t\tqMax(second, _mouseTextSymbol)\n\t\t\t\t);\n\t\t\t\tif (_mouseSelectType != TextSelectType::Letters) {\n\t\t\t\t\tselection = view->adjustSelection(\n\t\t\t\t\t\tselection,\n\t\t\t\t\t\t_mouseSelectType);\n\t\t\t\t}\n\t\t\t\tsetTextSelection(view, selection);\n\t\t\t\tclearDragSelection();\n\t\t\t} else if (_pressState.itemId) {\n\t\t\t\tupdateDragSelection();\n\t\t\t}\n\t\t} else if (_mouseAction == MouseAction::Dragging) {\n\t\t}\n\t}\n\n\t// Voice message seek support.\n\tif (_pressState.pointState != PointState::Outside\n\t\t&& ClickHandler::getPressed()) {\n\t\tif (const auto item = session().data().message(_pressState.itemId)) {\n\t\t\tif (const auto view = viewForItem(item)) {\n\t\t\t\tauto adjustedPoint = mapPointToItem(point, view);\n\t\t\t\tview->updatePressed(adjustedPoint);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (_mouseAction == MouseAction::Selecting) {\n\t\t_selectScroll.checkDeltaScroll(\n\t\t\tmousePosition,\n\t\t\t_visibleTop,\n\t\t\t_visibleBottom);\n\t} else {\n\t\t_selectScroll.cancel();\n\t}\n}\n\nstyle::cursor ListWidget::computeMouseCursor() const {\n\tif (ClickHandler::getPressed() || ClickHandler::getActive()) {\n\t\treturn style::cur_pointer;\n\t} else if (!hasSelectedItems()\n\t\t&& (_mouseCursorState == CursorState::Text)) {\n\t\treturn style::cur_text;\n\t}\n\treturn style::cur_default;\n}\n\nstd::unique_ptr<QMimeData> ListWidget::prepareDrag() {\n\tif (_mouseAction != MouseAction::Dragging) {\n\t\treturn nullptr;\n\t}\n\tauto pressedHandler = ClickHandler::getPressed();\n\tif (dynamic_cast<VoiceSeekClickHandler*>(pressedHandler.get())\n\t\t|| hasCopyRestriction()) {\n\t\treturn nullptr;\n\t}\n\n\tconst auto pressedItem = session().data().message(_pressState.itemId);\n\tconst auto pressedView = viewForItem(pressedItem);\n\tconst auto uponSelected = pressedView && isInsideSelection(\n\t\tpressedView,\n\t\t_pressItemExact ? _pressItemExact : pressedItem,\n\t\t_pressState);\n\n\tauto urls = QList<QUrl>();\n\tconst auto selectedText = [&] {\n\t\tif (uponSelected) {\n\t\t\treturn getSelectedText();\n\t\t} else if (pressedHandler) {\n\t\t\t//if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') {\n\t\t\t//\turls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o\n\t\t\t//}\n\t\t\treturn TextForMimeData::Simple(pressedHandler->dragText());\n\t\t}\n\t\treturn TextForMimeData();\n\t}();\n\tif (auto mimeData = TextUtilities::MimeDataFromText(selectedText)) {\n\t\tclearDragSelection();\n\t\t_selectScroll.cancel();\n\n\t\tif (!urls.isEmpty()) {\n\t\t\tmimeData->setUrls(urls);\n\t\t}\n\t\tif (uponSelected && !_delegate->listAllowsDragForward()) {\n\t\t\tconst auto canForwardAll = [&] {\n\t\t\t\tfor (const auto &[itemId, data] : _selected) {\n\t\t\t\t\tif (!data.canForward) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}();\n\t\t\tauto items = canForwardAll\n\t\t\t\t? collectSelectedIds()\n\t\t\t\t: MessageIdsList();\n\t\t\tif (!items.empty()) {\n\t\t\t\tsession().data().setMimeForwardIds(std::move(items));\n\t\t\t\tmimeData->setData(u\"application/x-td-forward\"_q, \"1\");\n\t\t\t}\n\t\t}\n\t\treturn mimeData;\n\t} else if (pressedView) {\n\t\tauto forwardIds = MessageIdsList();\n\t\tconst auto exactItem = _pressItemExact\n\t\t\t? _pressItemExact\n\t\t\t: pressedItem;\n\t\tif (_mouseCursorState == CursorState::Date) {\n\t\t\tif (_overElement->data()->allowsForward()) {\n\t\t\t\tforwardIds = session().data().itemOrItsGroup(\n\t\t\t\t\t_overElement->data());\n\t\t\t}\n\t\t} else if (_pressState.pointState == PointState::GroupPart) {\n\t\t\tif (exactItem->allowsForward()) {\n\t\t\t\tforwardIds = MessageIdsList(1, exactItem->fullId());\n\t\t\t}\n\t\t} else if (const auto media = pressedView->media()) {\n\t\t\tif (pressedView->data()->allowsForward()\n\t\t\t\t&& media->dragItemByHandler(pressedHandler)) {\n\t\t\t\tforwardIds = MessageIdsList(1, exactItem->fullId());\n\t\t\t}\n\t\t}\n\n\t\tif (pressedHandler) {\n\t\t\tconst auto lnkDocument = reinterpret_cast<DocumentData*>(\n\t\t\t\tpressedHandler->property(\n\t\t\t\t\tkDocumentLinkMediaProperty).toULongLong());\n\t\t\tif (lnkDocument) {\n\t\t\t\tconst auto filepath = lnkDocument->filepath(true);\n\t\t\t\tif (!filepath.isEmpty()) {\n\t\t\t\t\turls.push_back(QUrl::fromLocalFile(filepath));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (forwardIds.empty() && urls.isEmpty()) {\n\t\t\treturn nullptr;\n\t\t}\n\n\t\tauto result = std::make_unique<QMimeData>();\n\t\tif (!forwardIds.empty()) {\n\t\t\tsession().data().setMimeForwardIds(std::move(forwardIds));\n\t\t\tresult->setData(u\"application/x-td-forward\"_q, \"1\");\n\t\t}\n\t\tif (!urls.isEmpty()) {\n\t\t\tresult->setUrls(urls);\n\t\t}\n\t\treturn result;\n\t}\n\treturn nullptr;\n}\n\nvoid ListWidget::performDrag() {\n\tif (auto mimeData = prepareDrag()) {\n\t\t// This call enters event loop and can destroy any QObject.\n\t\tif (_reactionsManager) {\n\t\t\t_reactionsManager->updateButton({});\n\t\t}\n\t\t_delegate->listLaunchDrag(\n\t\t\tstd::move(mimeData),\n\t\t\tcrl::guard(this, [=] { mouseActionUpdate(QCursor::pos()); }));\n\t}\n}\n\nint ListWidget::itemTop(not_null<const Element*> view) const {\n\treturn _itemsTop + view->y();\n}\n\nvoid ListWidget::setCollapseGaps(std::vector<Ui::CollapseGap> gaps) {\n\tif (_collapseGaps == gaps) {\n\t\treturn;\n\t}\n\t_collapseGaps = std::move(gaps);\n\tauto gapTotal = 0;\n\tfor (const auto &gap : _collapseGaps) {\n\t\tgapTotal += gap.height;\n\t}\n\tconst auto nowHeight = _itemsTop\n\t\t+ _itemsHeight\n\t\t+ gapTotal\n\t\t+ st::historyPaddingBottom;\n\tif (height() != nowHeight) {\n\t\tresize(width(), nowHeight);\n\t}\n\tupdate();\n}\n\nvoid ListWidget::setupThanosEffect() {\n\tif (!_delegate->listThanosEffectEnabled()) {\n\t\treturn;\n\t}\n\tconst auto scroll = _delegate->listScrollArea();\n\tif (!scroll) {\n\t\treturn;\n\t}\n\t_thanosController = std::make_unique<Ui::ThanosEffectController>(\n\t\t_session,\n\t\tUi::ThanosEffectController::Delegate{\n\t\t\t.viewForItem = [=](not_null<const HistoryItem*> item)\n\t\t\t\t\t-> HistoryView::Element* {\n\t\t\t\tconst auto i = _views.find(item);\n\t\t\t\treturn (i != end(_views))\n\t\t\t\t\t? i->second.get()\n\t\t\t\t\t: nullptr;\n\t\t\t},\n\t\t\t.itemTop = [=](not_null<const HistoryView::Element*> view) {\n\t\t\t\treturn itemTop(view);\n\t\t\t},\n\t\t\t.visibleAreaTop = [=] { return _visibleTop; },\n\t\t\t.visibleAreaBottom = [=] { return _visibleBottom; },\n\t\t\t.contentWidth = [=] { return width(); },\n\t\t\t.preparePaintContext = [=](QRect clip) {\n\t\t\t\treturn preparePaintContext(clip);\n\t\t\t},\n\t\t\t.window = [=]() -> QWidget* { return window(); },\n\t\t\t.scrollArea = [=]() -> not_null<Ui::ScrollArea*> {\n\t\t\t\treturn scroll;\n\t\t\t},\n\t\t\t.scrollToY = [=](int y) {\n\t\t\t\tscroll->scrollToY(y);\n\t\t\t},\n\t\t\t.setCollapseGaps = [=](std::vector<Ui::CollapseGap> gaps) {\n\t\t\t\tsetCollapseGaps(std::move(gaps));\n\t\t\t},\n\t\t},\n\t\tlifetime());\n}\n\nvoid ListWidget::repaintItem(const Element *view) {\n\tif (!view) {\n\t\treturn;\n\t}\n\tconst auto top = itemTop(view);\n\tconst auto range = view->verticalRepaintRange();\n\tupdate(0, top + range.top, width(), range.height);\n\tconst auto id = view->data()->fullId();\n\tconst auto area = _reactionsManager\n\t\t? _reactionsManager->lookupEffectArea(id)\n\t\t: std::nullopt;\n\tif (area) {\n\t\tupdate(*area);\n\t}\n}\n\nvoid ListWidget::repaintItem(const Element *view, QRect rect) {\n\tif (rect.isNull()) {\n\t\treturn repaintItem(view);\n\t}\n\tif (!view) {\n\t\treturn;\n\t}\n\tconst auto top = itemTop(view);\n\tupdate(rect.translated(0, top));\n}\n\nvoid ListWidget::repaintItem(FullMsgId itemId) {\n\tif (const auto view = viewForItem(itemId)) {\n\t\trepaintItem(view);\n\t}\n}\n\nvoid ListWidget::resizeItem(not_null<Element*> view) {\n\tconst auto index = ranges::find(_items, view) - begin(_items);\n\tif (index < int(_items.size())) {\n\t\trefreshAttachmentsAtIndex(index);\n\t}\n}\n\nvoid ListWidget::refreshAttachmentsAtIndex(int index) {\n\tExpects(index >= 0 && index < _items.size());\n\n\tconst auto from = [&] {\n\t\tif (index > 0) {\n\t\t\tfor (auto i = index - 1; i != 0; --i) {\n\t\t\t\tif (!_items[i]->isHidden()) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn index;\n\t}();\n\tconst auto till = [&] {\n\t\tconst auto count = int(_items.size());\n\t\tfor (auto i = index + 1; i != count; ++i) {\n\t\t\tif (!_items[i]->isHidden()) {\n\t\t\t\treturn i + 1;\n\t\t\t}\n\t\t}\n\t\treturn index + 1;\n\t}();\n\trefreshAttachmentsFromTill(from, till);\n}\n\nvoid ListWidget::refreshAttachmentsFromTill(int from, int till) {\n\tExpects(from >= 0 && from <= till && till <= int(_items.size()));\n\n\tconst auto guard = gsl::finally([&] {\n\t\tupdateSize();\n\t});\n\tif (from == till) {\n\t\treturn;\n\t}\n\tauto view = _items[from].get();\n\tfor (auto i = from + 1; i != till; ++i) {\n\t\tconst auto next = _items[i].get();\n\t\tif (next->isHidden()) {\n\t\t\tnext->setDisplayDate(false);\n\t\t} else {\n\t\t\tconst auto viewDate = view->dateTime();\n\t\t\tconst auto nextDate = next->dateTime();\n\t\t\tnext->setDisplayDate(_context != Context::ShortcutMessages\n\t\t\t\t&& (nextDate.date() != viewDate.date()\n\t\t\t\t\t|| view->data()->hideDisplayDate()));\n\t\t\tauto attached = next->computeIsAttachToPrevious(view);\n\t\t\tnext->setAttachToPrevious(attached, view);\n\t\t\tview->setAttachToNext(attached, next);\n\t\t\tview = next;\n\t\t}\n\t}\n\tif (till == int(_items.size())) {\n\t\t_items.back()->setAttachToNext(false);\n\t}\n}\n\nvoid ListWidget::viewHeightAdjusted(not_null<Element*> view) {\n\tconst auto i = ranges::find(_items, view);\n\tif (i == end(_items)) {\n\t\treturn;\n\t}\n\tauto next = i + 1;\n\tconst auto was = (next != end(_items))\n\t\t? (*next)->y()\n\t\t: _itemsHeight;\n\tconst auto now = view->y() + view->height();\n\tconst auto delta = now - was;\n\tfor (; next != end(_items); ++next) {\n\t\t(*next)->setY((*next)->y() + delta);\n\t}\n\t_itemsHeight += delta;\n\t_itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom)\n\t\t? (_minHeight - _itemsHeight - st::historyPaddingBottom)\n\t\t: 0;\n\tresize(width(), _itemsTop + _itemsHeight + st::historyPaddingBottom);\n\trestoreScrollPosition();\n\tupdateVisibleTopItem();\n\tupdate();\n}\n\nvoid ListWidget::refreshItem(not_null<const Element*> view) {\n\tconst auto i = ranges::find(_items, view);\n\tconst auto index = i - begin(_items);\n\tif (index < int(_items.size())) {\n\t\tconst auto item = view->data();\n\t\tconst auto was = [&]() -> std::unique_ptr<Element> {\n\t\t\tif (const auto i = _views.find(item); i != end(_views)) {\n\t\t\t\tauto result = std::move(i->second);\n\t\t\t\t_views.erase(i);\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\treturn nullptr;\n\t\t}();\n\t\tconst auto &[i, ok] = _views.emplace(\n\t\t\titem,\n\t\t\titem->createView(this, was.get()));\n\t\tconst auto now = i->second.get();\n\t\t_items[index] = now;\n\n\t\tviewReplaced(view, i->second.get());\n\n\t\trefreshAttachmentsAtIndex(index);\n\t}\n}\n\nvoid ListWidget::showItemHighlight(not_null<HistoryItem*> item) {\n\tconst auto history = _delegate->listTranslateHistory();\n\tif (history && item->history() != history) {\n\t\treturn;\n\t} else if (!history && !viewForItem(item)) {\n\t\treturn;\n\t}\n\tconst auto position = item->position();\n\tauto params = Window::SectionShow{\n\t\tWindow::SectionShow::Way::Forward\n\t};\n\tparams.animated = anim::type::normal;\n\tif (!showAtPositionNow(position, params, nullptr)) {\n\t\tshowAroundPosition(position, [=, this] {\n\t\t\treturn showAtPositionNow(position, params, nullptr);\n\t\t});\n\t}\n}\n\nvoid ListWidget::viewReplaced(not_null<const Element*> was, Element *now) {\n\tif (_overlayHost) {\n\t\t_overlayHost->viewGone(was);\n\t}\n\tif (_visibleTopItem == was) _visibleTopItem = now;\n\tif (_scrollDateLastItem == was) _scrollDateLastItem = now;\n\tif (_overElement == was) _overElement = now;\n\tif (_bar.element == was.get()) {\n\t\tconst auto bar = _bar.element->Get<UnreadBar>();\n\t\t_bar.element = now;\n\t\tif (now && bar) {\n\t\t\t_bar.element->createUnreadBar(_barText.value());\n\t\t}\n\t}\n\tconst auto i = _itemRevealPending.find(was);\n\tif (i != end(_itemRevealPending)) {\n\t\t_itemRevealPending.erase(i);\n\t\tif (now) {\n\t\t\t_itemRevealPending.emplace(now);\n\t\t}\n\t}\n\tconst auto j = _itemRevealAnimations.find(was);\n\tif (j != end(_itemRevealAnimations)) {\n\t\tauto data = std::move(j->second);\n\t\t_itemRevealAnimations.erase(j);\n\t\tif (now) {\n\t\t\t_itemRevealAnimations.emplace(now, std::move(data));\n\t\t} else {\n\t\t\trevealItemsCallback();\n\t\t}\n\t}\n}\n\nvoid ListWidget::itemRemoved(not_null<const HistoryItem*> item) {\n\tif (_reactionsItem.current() == item) {\n\t\t_reactionsItem = nullptr;\n\t}\n\tif (_selectedTextItem == item) {\n\t\tclearTextSelection();\n\t}\n\tif (_overItemExact == item) {\n\t\t_overItemExact = nullptr;\n\t}\n\tif (_pressItemExact == item) {\n\t\t_pressItemExact = nullptr;\n\t}\n\tconst auto i = _views.find(item);\n\tif (i == end(_views)) {\n\t\treturn;\n\t}\n\n\tif (_thanosController) {\n\t\t_thanosController->captureOnRemoval(item);\n\t}\n\n\tsaveScrollState();\n\tconst auto guard = gsl::finally([&] {\n\t\trestoreScrollState();\n\t});\n\n\tconst auto view = i->second.get();\n\t_items.erase(\n\t\tranges::remove(_items, view, [](auto view) { return view.get(); }),\n\t\tend(_items));\n\tviewReplaced(view, nullptr);\n\t_views.erase(i);\n\n\tif (_reactionsManager) {\n\t\t_reactionsManager->remove(item->fullId());\n\t}\n\tif (_replyButtonManager) {\n\t\t_replyButtonManager->remove(item->fullId());\n\t}\n\tupdateItemsGeometry();\n}\n\nQPoint ListWidget::mapPointToItem(\n\t\tQPoint point,\n\t\tconst Element *view) const {\n\tif (!view) {\n\t\treturn QPoint();\n\t}\n\treturn point - QPoint(0, itemTop(view));\n}\n\nrpl::producer<FullMsgId> ListWidget::editMessageRequested() const {\n\treturn _requestedToEditMessage.events();\n}\n\nvoid ListWidget::editMessageRequestNotify(FullMsgId item) const {\n\t_requestedToEditMessage.fire(std::move(item));\n}\n\nbool ListWidget::lastMessageEditRequestNotify() const {\n\tconst auto now = base::unixtime::now();\n\tauto proj = [&](not_null<Element*> view) {\n\t\treturn view->data()->allowsEdit(now)\n\t\t\t&& !view->data()->isUploading();\n\t};\n\tconst auto &list = ranges::views::reverse(_items);\n\tconst auto it = ranges::find_if(list, std::move(proj));\n\tif (it == end(list)) {\n\t\treturn false;\n\t} else {\n\t\tconst auto item\n\t\t\t= session().data().groups().findItemToEdit((*it)->data()).get();\n\t\teditMessageRequestNotify(item->fullId());\n\t\treturn true;\n\t}\n}\n\nauto ListWidget::replyToMessageRequested() const\n-> rpl::producer<ReplyToMessageRequest> {\n\treturn _requestedToReplyToMessage.events();\n}\n\nvoid ListWidget::replyToMessageRequestNotify(\n\t\tFullReplyTo to,\n\t\tbool forceAnotherChat) {\n\t_requestedToReplyToMessage.fire({ std::move(to), forceAnotherChat });\n}\n\nrpl::producer<FullMsgId> ListWidget::readMessageRequested() const {\n\treturn _requestedToReadMessage.events();\n}\n\nrpl::producer<FullMsgId> ListWidget::showMessageRequested() const {\n\treturn _requestedToShowMessage.events();\n}\n\nvoid ListWidget::setInsertTextCallback(Fn<void(QString)> callback) {\n\t_insertTextCallback = std::move(callback);\n}\n\nvoid ListWidget::insertTextAtCursor(const QString &text) {\n\tif (_insertTextCallback) {\n\t\t_insertTextCallback(text);\n\t}\n}\n\nvoid ListWidget::replyNextMessage(FullMsgId fullId, bool next) {\n\tconst auto reply = [&](Element *view) {\n\t\tif (view) {\n\t\t\tconst auto newFullId = view->data()->fullId();\n\t\t\tif (!view->data()->isRegular()) {\n\t\t\t\treturn replyNextMessage(newFullId, next);\n\t\t\t}\n\t\t\treplyToMessageRequestNotify({ newFullId });\n\t\t\t_requestedToShowMessage.fire_copy(newFullId);\n\t\t} else {\n\t\t\treplyToMessageRequestNotify({});\n\t\t\t_highlighter.clear();\n\t\t}\n\t};\n\tconst auto replyFirst = [&] {\n\t\treply(next ? nullptr : _items.back().get());\n\t};\n\tif (!fullId) {\n\t\treplyFirst();\n\t\treturn;\n\t}\n\n\tauto proj = [&](not_null<Element*> view) {\n\t\treturn view->data()->fullId() == fullId;\n\t};\n\tconst auto &list = ranges::views::reverse(_items);\n\tconst auto it = ranges::find_if(list, std::move(proj));\n\tif (it == end(list)) {\n\t\treplyFirst();\n\t\treturn;\n\t} else {\n\t\tconst auto nextIt = it + (next ? -1 : 1);\n\t\tif (nextIt == end(list)) {\n\t\t\treturn;\n\t\t} else if (next && (it == begin(list))) {\n\t\t\treply(nullptr);\n\t\t} else {\n\t\t\treply(nextIt->get());\n\t\t}\n\t}\n}\n\nvoid ListWidget::setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w) {\n\t_emptyInfo = std::move(w);\n\tif (_emptyInfo) {\n\t\t_emptyInfo->setVisible(isEmpty());\n\t}\n}\n\nvoid ListWidget::overrideChatMode(std::optional<ElementChatMode> mode) {\n\t_overrideChatMode = mode;\n}\n\nListWidget::~ListWidget() {\n\t// Destroy child widgets first, because they may invoke leaveEvent-s.\n\t_emptyInfo = nullptr;\n\tif (const auto raw = _menu.release()) {\n\t\tcrl::on_main(raw, [=] {\n\t\t\tdelete raw;\n\t\t});\n\t}\n}\n\nvoid ConfirmDeleteSelectedItems(not_null<ListWidget*> widget) {\n\tconst auto items = widget->getSelectedItems();\n\tif (items.empty()) {\n\t\treturn;\n\t}\n\tconst auto controller = widget->controller();\n\tconst auto owner = &controller->session().data();\n\tauto historyItems = std::vector<not_null<HistoryItem*>>();\n\thistoryItems.reserve(items.size());\n\tfor (const auto &item : items) {\n\t\tif (!item.canDelete) {\n\t\t\treturn;\n\t\t} else if (const auto i = owner->message(item.msgId)) {\n\t\t\thistoryItems.push_back(i);\n\t\t}\n\t}\n\tconst auto confirmed = crl::guard(widget, [=] {\n\t\twidget->cancelSelection();\n\t});\n\tif (CanCreateModerateMessagesBox(historyItems)) {\n\t\tconst auto opt = DefaultModerateMessagesBoxOptions();\n\t\tcontroller->show(\n\t\t\tBox(CreateModerateMessagesBox, historyItems, confirmed, opt));\n\t} else {\n\t\tauto box = Box<DeleteMessagesBox>(\n\t\t\t&widget->session(),\n\t\t\twidget->getSelectedIds());\n\t\tbox->setDeleteConfirmedCallback(confirmed);\n\t\tcontroller->show(std::move(box));\n\t}\n}\n\nvoid ConfirmForwardSelectedItems(not_null<ListWidget*> widget) {\n\tconst auto items = widget->getSelectedItems();\n\tif (items.empty()) {\n\t\treturn;\n\t}\n\tfor (const auto &item : items) {\n\t\tif (!item.canForward) {\n\t\t\treturn;\n\t\t}\n\t}\n\tauto ids = widget->getSelectedIds();\n\tconst auto weak = base::make_weak(widget);\n\tWindow::ShowForwardMessagesBox(widget->controller(), std::move(ids), [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->cancelSelection();\n\t\t}\n\t});\n}\n\nvoid ConfirmSendNowSelectedItems(not_null<ListWidget*> widget) {\n\tconst auto items = widget->getSelectedItems();\n\tif (items.empty()) {\n\t\treturn;\n\t}\n\tconst auto navigation = widget->controller();\n\tconst auto history = [&]() -> History* {\n\t\tauto result = (History*)nullptr;\n\t\tauto &data = navigation->session().data();\n\t\tfor (const auto &item : items) {\n\t\t\tif (!item.canSendNow) {\n\t\t\t\treturn nullptr;\n\t\t\t}\n\t\t\tconst auto message = data.message(item.msgId);\n\t\t\tif (message) {\n\t\t\t\tresult = message->history();\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}();\n\tif (!history) {\n\t\treturn;\n\t}\n\tconst auto clearSelection = [weak = base::make_weak(widget)] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->cancelSelection();\n\t\t}\n\t};\n\tWindow::ShowSendNowMessagesBox(\n\t\tnavigation,\n\t\thistory,\n\t\twidget->getSelectedIds(),\n\t\tclearSelection);\n}\n\nCopyRestrictionType CopyRestrictionTypeFor(\n\t\tnot_null<PeerData*> peer,\n\t\tHistoryItem *item) {\n\treturn (peer->allowsForwarding() && (!item || !item->forbidsForward()))\n\t\t? CopyRestrictionType::None\n\t\t: peer->isUser()\n\t\t? CopyRestrictionType::User\n\t\t: peer->isBroadcast()\n\t\t? CopyRestrictionType::Channel\n\t\t: CopyRestrictionType::Group;\n}\n\nCopyRestrictionType CopyMediaRestrictionTypeFor(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<HistoryItem*> item) {\n\tif (const auto all = CopyRestrictionTypeFor(peer, item)\n\t\t; all != CopyRestrictionType::None) {\n\t\treturn all;\n\t}\n\treturn !item->forbidsSaving()\n\t\t? CopyRestrictionType::None\n\t\t: peer->isUser()\n\t\t? CopyRestrictionType::User\n\t\t: peer->isBroadcast()\n\t\t? CopyRestrictionType::Channel\n\t\t: CopyRestrictionType::Group;\n}\n\nCopyRestrictionType SelectRestrictionTypeFor(\n\t\tnot_null<PeerData*> peer) {\n\tif (const auto chat = peer->asChat()) {\n\t\treturn chat->canDeleteMessages()\n\t\t\t? CopyRestrictionType::None\n\t\t\t: CopyRestrictionTypeFor(peer);\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn channel->canDeleteMessages()\n\t\t\t? CopyRestrictionType::None\n\t\t\t: CopyRestrictionTypeFor(peer);\n\t}\n\treturn CopyRestrictionTypeFor(peer);\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_list_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/thanos_effect_controller.h\"\n#include \"ui/dragging_scroll_manager.h\"\n#include \"ui/widgets/middle_click_autoscroll.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"mtproto/sender.h\"\n#include \"data/data_messages.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history_view_highlight_manager.h\"\n#include \"history/history_view_top_toast.h\"\n\nstruct ClickHandlerContext;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass Show;\nclass PopupMenu;\nclass ChatTheme;\nstruct ChatPaintContext;\nstruct ChatPaintContextArgs;\nenum class TouchScrollState;\nstruct PeerUserpicView;\nclass MessageSendingAnimationController;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Data {\nstruct Group;\nstruct Reaction;\nstruct AllowedReactions;\nstruct ReactionId;\n} // namespace Data\n\nnamespace HistoryView::Reactions {\nclass Manager;\nstruct ChosenReaction;\nstruct ButtonParameters;\n} // namespace HistoryView::Reactions\n\nnamespace HistoryView::ReplyButton {\nclass Manager;\nstruct ButtonParameters;\n} // namespace HistoryView::ReplyButton\n\nnamespace Window {\nstruct SectionShow;\n} // namespace Window\n\nnamespace HistoryView {\n\nstruct TextState;\nstruct StateRequest;\nclass ElementOverlayHost;\nclass EmojiInteractions;\nclass TranslateTracker;\nclass ReadMetricsTracker;\nenum class CursorState : char;\nenum class PointState : char;\nenum class Context : char;\n\nenum class CopyRestrictionType : char {\n\tNone,\n\tGroup,\n\tChannel,\n\tUser,\n};\n\nstruct SelectedItem {\n\texplicit SelectedItem(FullMsgId msgId) : msgId(msgId) {\n\t}\n\n\tFullMsgId msgId;\n\tbool canDelete = false;\n\tbool canForward = false;\n\tbool canSendNow = false;\n\tbool canReschedule = false;\n};\n\nstruct MessagesBar {\n\tElement *element = nullptr;\n\tbool hidden = false;\n\tbool focus = false;\n};\n\nstruct MessagesBarData {\n\tMessagesBar bar;\n\trpl::producer<QString> text;\n};\n\nusing SelectedItems = std::vector<SelectedItem>;\n\nclass ListDelegate {\npublic:\n\tvirtual Context listContext() = 0;\n\tvirtual bool listScrollTo(int top, bool syntetic = true) = 0;\n\tvirtual void listCancelRequest() = 0;\n\tvirtual void listDeleteRequest() = 0;\n\tvirtual void listTryProcessKeyInput(not_null<QKeyEvent*> e) = 0;\n\tvirtual rpl::producer<Data::MessagesSlice> listSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) = 0;\n\tvirtual bool listAllowsMultiSelect() = 0;\n\tvirtual bool listIsItemGoodForSelection(not_null<HistoryItem*> item) = 0;\n\tvirtual bool listIsLessInOrder(\n\t\tnot_null<HistoryItem*> first,\n\t\tnot_null<HistoryItem*> second) = 0;\n\tvirtual void listSelectionChanged(SelectedItems &&items) = 0;\n\tvirtual void listMarkReadTill(not_null<HistoryItem*> item) = 0;\n\tvirtual void listMarkContentsRead(\n\t\tconst base::flat_set<not_null<HistoryItem*>> &items) = 0;\n\tvirtual MessagesBarData listMessagesBar(\n\t\tconst std::vector<not_null<Element*>> &elements,\n\t\tbool markLastAsRead) = 0;\n\tvirtual void listContentRefreshed() = 0;\n\tvirtual void listUpdateDateLink(\n\t\tClickHandlerPtr &link,\n\t\tnot_null<Element*> view) = 0;\n\tvirtual bool listElementHideReply(not_null<const Element*> view) = 0;\n\tvirtual bool listElementShownUnread(not_null<const Element*> view) = 0;\n\tvirtual bool listIsGoodForAroundPosition(\n\t\tnot_null<const Element*> view) = 0;\n\tvirtual void listSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) = 0;\n\tvirtual void listSearch(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) = 0;\n\tvirtual void listHandleViaClick(not_null<UserData*> bot) = 0;\n\tvirtual not_null<Ui::ChatTheme*> listChatTheme() = 0;\n\tvirtual CopyRestrictionType listCopyRestrictionType(\n\t\tHistoryItem *item) = 0;\n\tCopyRestrictionType listCopyRestrictionType() {\n\t\treturn listCopyRestrictionType(nullptr);\n\t}\n\tvirtual CopyRestrictionType listCopyMediaRestrictionType(\n\t\tnot_null<HistoryItem*> item) = 0;\n\tvirtual CopyRestrictionType listSelectRestrictionType() = 0;\n\tvirtual auto listAllowedReactionsValue()\n\t\t-> rpl::producer<Data::AllowedReactions> = 0;\n\tvirtual void listShowPremiumToast(not_null<DocumentData*> document) = 0;\n\tvirtual void listOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) = 0;\n\tvirtual void listOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView) = 0;\n\tvirtual void listPaintEmpty(\n\t\tPainter &p,\n\t\tconst Ui::ChatPaintContext &context) = 0;\n\tvirtual QString listElementAuthorRank(not_null<const Element*> view) = 0;\n\tvirtual bool listElementHideTopicButton(not_null<const Element*> view) = 0;\n\tvirtual History *listTranslateHistory() = 0;\n\tvirtual void listAddTranslatedItems(\n\t\tnot_null<TranslateTracker*> tracker) = 0;\n\n\t// Methods that use Window::SessionController by default.\n\tvirtual not_null<Window::SessionController*> listWindow() = 0;\n\tvirtual not_null<QWidget*> listEmojiInteractionsParent() = 0;\n\tvirtual not_null<const Ui::ChatStyle*> listChatStyle() = 0;\n\tvirtual rpl::producer<bool> listChatWideValue() = 0;\n\tvirtual std::unique_ptr<Reactions::Manager> listMakeReactionsManager(\n\t\tQWidget *wheelEventsTarget,\n\t\tFn<void(QRect)> update) = 0;\n\tvirtual void listVisibleAreaUpdated() = 0;\n\tvirtual std::shared_ptr<Ui::Show> listUiShow() = 0;\n\tvirtual void listShowPollResults(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context) = 0;\n\tvirtual void listCancelUploadLayer(not_null<HistoryItem*> item) = 0;\n\tvirtual bool listAnimationsPaused() = 0;\n\tvirtual auto listSendingAnimation()\n\t\t-> Ui::MessageSendingAnimationController* = 0;\n\tvirtual Ui::ChatPaintContext listPreparePaintContext(\n\t\tUi::ChatPaintContextArgs &&args) = 0;\n\tvirtual bool listMarkingContentRead() = 0;\n\tvirtual bool listIgnorePaintEvent(QWidget *w, QPaintEvent *e) = 0;\n\tvirtual bool listShowReactPremiumError(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst Data::ReactionId &id) = 0;\n\tvirtual base::unique_qptr<Ui::PopupMenu> listFillSenderUserpicMenu(\n\t\tPeerId userpicPeerId) = 0;\n\tvirtual void listWindowSetInnerFocus() = 0;\n\tvirtual bool listAllowsDragForward() = 0;\n\tvirtual void listLaunchDrag(\n\t\tstd::unique_ptr<QMimeData> data,\n\t\tFn<void()> finished) = 0;\n\tvirtual Ui::ScrollArea *listScrollArea() const { return nullptr; }\n\tvirtual bool listThanosEffectEnabled() const { return true; }\n};\n\nclass WindowListDelegate : public ListDelegate {\npublic:\n\texplicit WindowListDelegate(not_null<Window::SessionController*> window);\n\n\tnot_null<Window::SessionController*> listWindow() override;\n\tnot_null<QWidget*> listEmojiInteractionsParent() override;\n\tnot_null<const Ui::ChatStyle*> listChatStyle() override;\n\trpl::producer<bool> listChatWideValue() override;\n\tstd::unique_ptr<Reactions::Manager> listMakeReactionsManager(\n\t\tQWidget *wheelEventsTarget,\n\t\tFn<void(QRect)> update) override;\n\tvoid listVisibleAreaUpdated() override;\n\tstd::shared_ptr<Ui::Show> listUiShow() override;\n\tvoid listShowPollResults(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context) override;\n\tvoid listCancelUploadLayer(not_null<HistoryItem*> item) override;\n\tbool listAnimationsPaused() override;\n\tUi::MessageSendingAnimationController *listSendingAnimation() override;\n\tUi::ChatPaintContext listPreparePaintContext(\n\t\tUi::ChatPaintContextArgs &&args) override;\n\tbool listMarkingContentRead() override;\n\tbool listIgnorePaintEvent(QWidget *w, QPaintEvent *e) override;\n\tbool listShowReactPremiumError(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst Data::ReactionId &id) override;\n\tbase::unique_qptr<Ui::PopupMenu> listFillSenderUserpicMenu(\n\t\tPeerId userpicPeerId) override;\n\tvoid listWindowSetInnerFocus() override;\n\tbool listAllowsDragForward() override;\n\tvoid listLaunchDrag(\n\t\tstd::unique_ptr<QMimeData> data,\n\t\tFn<void()> finished) override;\n\nprivate:\n\tconst not_null<Window::SessionController*> _window;\n\n};\n\nstruct SelectionData {\n\tbool canDelete = false;\n\tbool canForward = false;\n\tbool canSendNow = false;\n\tbool canReschedule = false;\n};\n\nusing SelectedMap = base::flat_map<\n\tFullMsgId,\n\tSelectionData,\n\tstd::less<>>;\n\nclass ListMemento {\npublic:\n\tstruct ScrollTopState {\n\t\tData::MessagePosition item;\n\t\tint shift = 0;\n\t};\n\n\texplicit ListMemento(\n\t\tData::MessagePosition position = Data::UnreadMessagePosition)\n\t: _aroundPosition(position) {\n\t}\n\tvoid setAroundPosition(Data::MessagePosition position) {\n\t\t_aroundPosition = position;\n\t}\n\tData::MessagePosition aroundPosition() const {\n\t\treturn _aroundPosition;\n\t}\n\tvoid setIdsLimit(int limit) {\n\t\t_idsLimit = limit;\n\t}\n\tint idsLimit() const {\n\t\treturn _idsLimit;\n\t}\n\tvoid setScrollTopState(ScrollTopState state) {\n\t\t_scrollTopState = state;\n\t}\n\tScrollTopState scrollTopState() const {\n\t\treturn _scrollTopState;\n\t}\n\nprivate:\n\tData::MessagePosition _aroundPosition;\n\tScrollTopState _scrollTopState;\n\tint _idsLimit = 0;\n\n};\n\nclass ListWidget final\n\t: public Ui::RpWidget\n\t, public ElementDelegate\n\t, public Ui::AbstractTooltipShower {\npublic:\n\tListWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<ListDelegate*> delegate);\n\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] not_null<Window::SessionController*> controller() const;\n\t[[nodiscard]] not_null<ListDelegate*> delegate() const;\n\n\t// Set the correct scroll position after being resized.\n\tvoid restoreScrollPosition();\n\n\tvoid resizeToWidth(int newWidth, int minHeight);\n\n\tvoid saveState(not_null<ListMemento*> memento);\n\tvoid restoreState(not_null<ListMemento*> memento);\n\tstd::optional<int> scrollTopForPosition(\n\t\tData::MessagePosition position) const;\n\tElement *viewByPosition(Data::MessagePosition position) const;\n\tstd::optional<int> scrollTopForView(not_null<Element*> view) const;\n\t[[nodiscard]] bool animatedScrolling() const;\n\tbool isAbovePosition(Data::MessagePosition position) const;\n\tbool isBelowPosition(Data::MessagePosition position) const;\n\tvoid highlightMessage(\n\t\tFullMsgId itemId,\n\t\tconst MessageHighlightId &highlight);\n\n\tvoid showAtPosition(\n\t\tData::MessagePosition position,\n\t\tconst Window::SectionShow &params,\n\t\tFn<void(bool found)> done = nullptr);\n\tvoid refreshViewer();\n\n\t[[nodiscard]] TextForMimeData getSelectedText() const;\n\t[[nodiscard]] MessageIdsList getSelectedIds() const;\n\t[[nodiscard]] SelectedItems getSelectedItems() const;\n\t[[nodiscard]] const TextSelection &getSelectedTextRange(\n\t\tnot_null<HistoryItem*> item) const;\n\tvoid cancelSelection();\n\tvoid selectItem(not_null<HistoryItem*> item);\n\tvoid selectItemAsGroup(not_null<HistoryItem*> item);\n\n\tvoid touchScrollUpdated(const QPoint &screenPos);\n\t[[nodiscard]] rpl::producer<bool> touchMaybeSelectingValue() const;\n\n\t[[nodiscard]] bool loadedAtTopKnown() const;\n\t[[nodiscard]] bool loadedAtTop() const;\n\t[[nodiscard]] bool loadedAtBottomKnown() const;\n\t[[nodiscard]] bool loadedAtBottom() const;\n\t[[nodiscard]] bool isEmpty() const;\n\n\t[[nodiscard]] bool markingContentsRead() const;\n\t[[nodiscard]] bool markingMessagesRead() const;\n\tvoid showFinished();\n\tvoid checkActivation();\n\n\t[[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const;\n\t[[nodiscard]] bool hasCopyMediaRestriction(\n\t\tnot_null<HistoryItem*> item) const;\n\t[[nodiscard]] bool showCopyRestriction(HistoryItem *item = nullptr);\n\t[[nodiscard]] bool showCopyMediaRestriction(not_null<HistoryItem*> item);\n\t[[nodiscard]] bool hasCopyRestrictionForSelected() const;\n\t[[nodiscard]] bool showCopyRestrictionForSelected();\n\t[[nodiscard]] bool hasSelectRestriction() const;\n\t[[nodiscard]] Element *lookupItemByY(int y) const;\n\n\t[[nodiscard]] std::pair<Element*, int> findViewForPinnedTracking(\n\t\tint top) const;\n\t[[nodiscard]] ClickHandlerContext prepareClickHandlerContext(\n\t\tFullMsgId id);\n\t[[nodiscard]] ClickContext prepareClickContext(\n\t\tQt::MouseButton button,\n\t\tFullMsgId itemId);\n\n\t// AbstractTooltipShower interface\n\tQString tooltipText() const override;\n\tQPoint tooltipPos() const override;\n\tbool tooltipWindowActive() const override;\n\n\tstruct ReplyToMessageRequest {\n\t\tFullReplyTo to;\n\t\tbool forceAnotherChat = false;\n\t};\n\t[[nodiscard]] rpl::producer<FullMsgId> editMessageRequested() const;\n\tvoid editMessageRequestNotify(FullMsgId item) const;\n\t[[nodiscard]] bool lastMessageEditRequestNotify() const;\n\t[[nodiscard]] auto replyToMessageRequested() const\n\t\t-> rpl::producer<ReplyToMessageRequest>;\n\tvoid replyToMessageRequestNotify(\n\t\tFullReplyTo to,\n\t\tbool forceAnotherChat = false);\n\t[[nodiscard]] rpl::producer<FullMsgId> readMessageRequested() const;\n\t[[nodiscard]] rpl::producer<FullMsgId> showMessageRequested() const;\n\tvoid setInsertTextCallback(Fn<void(QString)> callback);\n\tvoid insertTextAtCursor(const QString &text);\n\tvoid replyNextMessage(FullMsgId fullId, bool next = true);\n\n\t[[nodiscard]] Reactions::ButtonParameters reactionButtonParameters(\n\t\tnot_null<const Element*> view,\n\t\tQPoint position,\n\t\tconst TextState &reactionState) const;\n\t[[nodiscard]] ReplyButton::ButtonParameters replyButtonParameters(\n\t\tnot_null<const Element*> view,\n\t\tQPoint position,\n\t\tconst TextState &replyState) const;\n\tvoid toggleFavoriteReaction(not_null<Element*> view) const;\n\n\n\t[[nodiscard]] auto scrollKeyEvents() const\n\t\t-> rpl::producer<not_null<QKeyEvent*>>;\n\n\t// ElementDelegate interface.\n\tContext elementContext() override;\n\tbool elementUnderCursor(not_null<const Element*> view) override;\n\tSelectionModeResult elementInSelectionMode(const Element *view) override;\n\tbool elementIntersectsRange(\n\t\tnot_null<const Element*> view,\n\t\tint from,\n\t\tint till) override;\n\tvoid elementStartStickerLoop(not_null<const Element*> view) override;\n\tvoid elementShowPollResults(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context) override;\n\tvoid elementShowAddPollOption(\n\t\tnot_null<Element*> view,\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context,\n\t\tQRect optionRect) override;\n\tvoid elementSubmitAddPollOption(FullMsgId context) override;\n\tvoid hideElementOverlay();\n\tvoid elementOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) override;\n\tvoid elementOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView = false) override;\n\tvoid elementCancelUpload(const FullMsgId &context) override;\n\tvoid elementShowTooltip(\n\t\tconst TextWithEntities &text,\n\t\tFn<void()> hiddenCallback) override;\n\tbool elementAnimationsPaused() override;\n\tbool elementHideReply(not_null<const Element*> view) override;\n\tbool elementShownUnread(not_null<const Element*> view) override;\n\tvoid elementSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) override;\n\tvoid elementSearchInList(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) override;\n\tvoid elementHandleViaClick(not_null<UserData*> bot) override;\n\tElementChatMode elementChatMode() override;\n\tnot_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;\n\tvoid elementReplyTo(const FullReplyTo &to) override;\n\tvoid elementStartInteraction(not_null<const Element*> view) override;\n\tvoid elementStartPremium(\n\t\tnot_null<const Element*> view,\n\t\tElement *replacing) override;\n\tvoid elementCancelPremium(not_null<const Element*> view) override;\n\tvoid elementStartEffect(\n\t\tnot_null<const Element*> view,\n\t\tElement *replacing) override;\n\tQString elementAuthorRank(not_null<const Element*> view) override;\n\tbool elementHideTopicButton(not_null<const Element*> view) override;\n\n\tvoid setCollapseGaps(std::vector<Ui::CollapseGap> gaps);\n\n\tvoid setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w);\n\tvoid overrideChatMode(std::optional<ElementChatMode> mode);\n\n\t~ListWidget();\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\n\tbool eventHook(QEvent *e) override; // calls touchEvent when necessary\n\tvoid touchEvent(QTouchEvent *e);\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid mouseDoubleClickEvent(QMouseEvent *e) override;\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\n\t// Resize content and count natural widget height for the desired width.\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\t[[nodiscard]] static int SelectionViewOffset(\n\t\tnot_null<const ListWidget*> inner,\n\t\tnot_null<const Element*> view);\n\n\tusing ScrollTopState = ListMemento::ScrollTopState;\n\tusing PointState = HistoryView::PointState;\n\tusing CursorState = HistoryView::CursorState;\n\tusing ChosenReaction = HistoryView::Reactions::ChosenReaction;\n\tusing ViewsMap = base::flat_map<\n\t\tnot_null<HistoryItem*>,\n\t\tstd::unique_ptr<Element>>;\n\n\tstruct MouseState {\n\t\tMouseState();\n\t\tMouseState(\n\t\t\tFullMsgId itemId,\n\t\t\tint height,\n\t\t\tQPoint point,\n\t\t\tPointState pointState);\n\n\t\tFullMsgId itemId;\n\t\tint height = 0;\n\t\tQPoint point;\n\t\tPointState pointState;\n\n\t\tinline bool operator==(const MouseState &other) const {\n\t\t\treturn (itemId == other.itemId)\n\t\t\t\t&& (point == other.point);\n\t\t}\n\t\tinline bool operator!=(const MouseState &other) const {\n\t\t\treturn !(*this == other);\n\t\t}\n\t};\n\tstruct ItemRevealAnimation {\n\t\tUi::Animations::Simple animation;\n\t\tint startHeight = 0;\n\t};\n\tenum class Direction {\n\t\tUp,\n\t\tDown,\n\t};\n\tenum class MouseAction {\n\t\tNone,\n\t\tPrepareDrag,\n\t\tDragging,\n\t\tPrepareSelect,\n\t\tSelecting,\n\t};\n\tenum class SelectAction {\n\t\tSelect,\n\t\tDeselect,\n\t\tInvert,\n\t};\n\tenum class EnumItemsDirection {\n\t\tTopToBottom,\n\t\tBottomToTop,\n\t};\n\tenum class DragSelectAction {\n\t\tNone,\n\t\tSelecting,\n\t\tDeselecting,\n\t};\n\n\tvoid onTouchSelect();\n\tvoid onTouchScrollTimer();\n\tvoid markReadMetricsStale();\n\tvoid registerReadMetricsActivity();\n\n\tvoid updateAroundPositionFromNearest(int nearestIndex);\n\tvoid refreshRows(const Data::MessagesSlice &old);\n\tScrollTopState countScrollState() const;\n\tvoid saveScrollState();\n\tvoid restoreScrollState();\n\n\t[[nodiscard]] bool jumpToBottomInsteadOfUnread() const;\n\tvoid showAroundPosition(\n\t\tData::MessagePosition position,\n\t\tFn<bool()> overrideInitialScroll);\n\tbool showAtPositionNow(\n\t\tData::MessagePosition position,\n\t\tconst Window::SectionShow &params,\n\t\tFn<void(bool found)> done);\n\n\tUi::ChatPaintContext preparePaintContext(const QRect &clip) const;\n\n\tElement *viewForItem(FullMsgId itemId) const;\n\tElement *viewForItem(const HistoryItem *item) const;\n\tnot_null<Element*> enforceViewForItem(\n\t\tnot_null<HistoryItem*> item,\n\t\tViewsMap &old);\n\n\tvoid mouseActionStart(\n\t\tconst QPoint &globalPosition,\n\t\tQt::MouseButton button);\n\tvoid mouseActionUpdate(const QPoint &globalPosition);\n\tvoid mouseActionUpdate();\n\tvoid mouseActionFinish(\n\t\tconst QPoint &globalPosition,\n\t\tQt::MouseButton button);\n\tvoid mouseActionCancel();\n\tstd::unique_ptr<QMimeData> prepareDrag();\n\tvoid performDrag();\n\tstyle::cursor computeMouseCursor() const;\n\tint itemTop(not_null<const Element*> view) const;\n\tvoid repaintItem(FullMsgId itemId);\n\tvoid repaintItem(const Element *view);\n\tvoid repaintItem(const Element *view, QRect rect);\n\tvoid resizeItem(not_null<Element*> view);\n\tvoid refreshItem(not_null<const Element*> view);\n\tvoid viewHeightAdjusted(not_null<Element*> view);\n\tvoid showItemHighlight(not_null<HistoryItem*> item);\n\tvoid itemRemoved(not_null<const HistoryItem*> item);\n\tQPoint mapPointToItem(QPoint point, const Element *view) const;\n\n\tvoid showContextMenu(QContextMenuEvent *e, bool showFromTouch = false);\n\tvoid reactionChosen(ChosenReaction reaction);\n\n\tvoid touchResetSpeed();\n\tvoid touchUpdateSpeed();\n\tvoid touchDeaccelerate(int32 elapsed);\n\n\t[[nodiscard]] int findItemIndexByY(int y) const;\n\t[[nodiscard]] not_null<Element*> findItemByY(int y) const;\n\t[[nodiscard]] Element *strictFindItemByY(int y) const;\n\t[[nodiscard]] int findNearestItem(Data::MessagePosition position) const;\n\tvoid viewReplaced(not_null<const Element*> was, Element *now);\n\n\tvoid checkMoveToOtherViewer();\n\tvoid updateVisibleTopItem();\n\tvoid updateItemsGeometry();\n\tvoid updateSize();\n\tvoid refreshAttachmentsFromTill(int from, int till);\n\tvoid refreshAttachmentsAtIndex(int index);\n\n\tvoid toggleScrollDateShown();\n\tvoid repaintScrollDateCallback();\n\tbool displayScrollDate() const;\n\tvoid scrollDateHide();\n\tvoid scrollDateCheck();\n\tvoid scrollDateHideByTimer();\n\tvoid keepScrollDateForNow();\n\n\tvoid computeScrollTo(\n\t\tint to,\n\t\tData::MessagePosition position,\n\t\tanim::type animated);\n\tenum class AnimatedScroll {\n\t\tFull,\n\t\tPart,\n\t\tNone,\n\t};\n\tvoid scrollTo(\n\t\tint scrollTop,\n\t\tData::MessagePosition attachPosition,\n\t\tint delta,\n\t\tAnimatedScroll type);\n\n\tvoid trySwitchToWordSelection();\n\tvoid switchToWordSelection();\n\tvoid validateTrippleClickStartTime();\n\tSelectedItems collectSelectedItems() const;\n\tMessageIdsList collectSelectedIds() const;\n\tvoid pushSelectedItems();\n\tvoid removeItemSelection(\n\t\tconst SelectedMap::const_iterator &i);\n\tbool hasSelectedText() const;\n\tbool hasSelectedItems() const;\n\tSelectionModeResult inSelectionMode() const;\n\tbool overSelectedItems() const;\n\tvoid clearTextSelection();\n\tvoid clearSelected();\n\tvoid setTextSelection(\n\t\tnot_null<Element*> view,\n\t\tTextSelection selection);\n\tint itemMinimalHeight() const;\n\n\tbool isGoodForSelection(\n\t\tSelectedMap &applyTo,\n\t\tnot_null<HistoryItem*> item,\n\t\tint &totalCount) const;\n\tbool addToSelection(\n\t\tSelectedMap &applyTo,\n\t\tnot_null<HistoryItem*> item) const;\n\tbool removeFromSelection(\n\t\tSelectedMap &applyTo,\n\t\tFullMsgId itemId) const;\n\tvoid changeSelection(\n\t\tSelectedMap &applyTo,\n\t\tnot_null<HistoryItem*> item,\n\t\tSelectAction action) const;\n\tbool isSelectedGroup(\n\t\tconst SelectedMap &applyTo,\n\t\tnot_null<const Data::Group*> group) const;\n\tbool isSelectedAsGroup(\n\t\tconst SelectedMap &applyTo,\n\t\tnot_null<HistoryItem*> item) const;\n\tvoid changeSelectionAsGroup(\n\t\tSelectedMap &applyTo,\n\t\tnot_null<HistoryItem*> item,\n\t\tSelectAction action) const;\n\n\tSelectedMap::iterator itemUnderPressSelection();\n\tSelectedMap::const_iterator itemUnderPressSelection() const;\n\tbool isItemUnderPressSelected() const;\n\tbool isInsideSelection(\n\t\tnot_null<const Element*> view,\n\t\tnot_null<HistoryItem*> exactItem,\n\t\tconst MouseState &state) const;\n\tbool requiredToStartDragging(not_null<Element*> view) const;\n\tbool isPressInSelectedText(TextState state) const;\n\tvoid updateDragSelection();\n\tvoid updateDragSelection(\n\t\tconst Element *fromView,\n\t\tconst MouseState &fromState,\n\t\tconst Element *tillView,\n\t\tconst MouseState &tillState);\n\tvoid updateDragSelection(\n\t\tstd::vector<not_null<Element*>>::const_iterator from,\n\t\tstd::vector<not_null<Element*>>::const_iterator till);\n\tvoid ensureDragSelectAction(\n\t\tstd::vector<not_null<Element*>>::const_iterator from,\n\t\tstd::vector<not_null<Element*>>::const_iterator till);\n\tvoid clearDragSelection();\n\tvoid applyDragSelection();\n\tvoid applyDragSelection(SelectedMap &applyTo) const;\n\tTextSelection itemRenderSelection(\n\t\tnot_null<const Element*> view) const;\n\tTextSelection computeRenderSelection(\n\t\tnot_null<const SelectedMap*> selected,\n\t\tnot_null<const Element*> view) const;\n\tvoid checkUnreadBarCreation(bool markLastAsRead = false);\n\tvoid applyUpdatedScrollState();\n\tvoid scrollToAnimationCallback(FullMsgId attachToId, int relativeTo);\n\tvoid startItemRevealAnimations();\n\tvoid revealItemsCallback();\n\tvoid maybeMarkReactionsRead(not_null<HistoryItem*> item);\n\n\tvoid startMessageSendingAnimation(not_null<HistoryItem*> item);\n\tvoid showPremiumStickerTooltip(\n\t\tnot_null<const HistoryView::Element*> view);\n\n\tvoid paintUserpics(\n\t\tPainter &p,\n\t\tconst Ui::ChatPaintContext &context,\n\t\tQRect clip);\n\tvoid paintDates(\n\t\tPainter &p,\n\t\tconst Ui::ChatPaintContext &context,\n\t\tQRect clip);\n\n\t// This function finds all history items that are displayed and calls template method\n\t// for each found message (in given direction) in the passed history with passed top offset.\n\t//\n\t// Method has \"bool (*Method)(not_null<Element*> view, int itemtop, int itembottom)\" signature\n\t// if it returns false the enumeration stops immediately.\n\ttemplate <EnumItemsDirection direction, typename Method>\n\tvoid enumerateItems(Method method);\n\n\t// This function finds all userpics on the left that are displayed and calls template method\n\t// for each found userpic (from the top to the bottom) using enumerateItems() method.\n\t//\n\t// Method has \"bool (*Method)(not_null<Element*> view, int userpicTop)\" signature\n\t// if it returns false the enumeration stops immediately.\n\ttemplate <typename Method>\n\tvoid enumerateUserpics(Method method);\n\n\t// This function finds all date elements that are displayed and calls template method\n\t// for each found date element (from the bottom to the top) using enumerateItems() method.\n\t//\n\t// Method has \"bool (*Method)(not_null<HistoryItem*> item, int itemtop, int dateTop)\" signature\n\t// if it returns false the enumeration stops immediately.\n\ttemplate <typename Method>\n\tvoid enumerateDates(Method method);\n\n\tvoid setGeometryCrashAnnotations(not_null<Element*> view);\n\n\tstatic constexpr auto kMinimalIdsLimit = 24;\n\n\tconst not_null<ListDelegate*> _delegate;\n\tconst not_null<Main::Session*> _session;\n\tconst std::unique_ptr<EmojiInteractions> _emojiInteractions;\n\tconst Context _context;\n\n\tData::MessagePosition _aroundPosition;\n\tData::MessagePosition _shownAtPosition;\n\tData::MessagePosition _initialAroundPosition;\n\tint _aroundIndex = -1;\n\tint _idsLimit = kMinimalIdsLimit;\n\tData::MessagesSlice _slice;\n\tbool _itemsKnownTillEnd = false;\n\n\tstd::vector<not_null<Element*>> _items;\n\tViewsMap _views, _viewsCapacity;\n\tint _itemsTop = 0;\n\tint _itemsWidth = 0;\n\tint _itemsHeight = 0;\n\tint _itemAverageHeight = 0;\n\tbase::flat_set<not_null<Element*>> _itemRevealPending;\n\tbase::flat_map<\n\t\tnot_null<Element*>,\n\t\tItemRevealAnimation> _itemRevealAnimations;\n\tint _itemsRevealHeight = 0;\n\tstd::vector<Ui::CollapseGap> _collapseGaps;\n\tbase::flat_set<FullMsgId> _animatedStickersPlayed;\n\tbase::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> _userpics;\n\tbase::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> _userpicsCache;\n\tbase::flat_map<MsgId, Ui::PeerUserpicView> _hiddenSenderUserpics;\n\n\tconst std::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\tQPainterPath _highlightPathCache;\n\n\tbase::unique_qptr<Ui::RpWidget> _emptyInfo = nullptr;\n\n\tstd::unique_ptr<HistoryView::Reactions::Manager> _reactionsManager;\n\trpl::variable<HistoryItem*> _reactionsItem;\n\tbool _useCornerReply = false;\n\tbool _useCornerReaction = false;\n\n\tstd::unique_ptr<ReplyButton::Manager> _replyButtonManager;\n\n\tstd::unique_ptr<TranslateTracker> _translateTracker;\n\tstd::unique_ptr<ReadMetricsTracker> _readMetricsTracker;\n\tbool _readMetricsStale = false;\n\n\tint _minHeight = 0;\n\tint _visibleTop = 0;\n\tint _visibleBottom = 0;\n\tElement *_visibleTopItem = nullptr;\n\tint _visibleTopFromItem = 0;\n\tScrollTopState _scrollTopState;\n\tUi::Animations::Simple _scrollToAnimation;\n\tFn<bool()> _overrideInitialScroll;\n\n\tbool _scrollInited = false;\n\tbool _scrollDateShown = false;\n\tUi::Animations::Simple _scrollDateOpacity;\n\tSingleQueuedInvokation _scrollDateCheck;\n\tbase::Timer _scrollDateHideTimer;\n\tElement *_scrollDateLastItem = nullptr;\n\tint _scrollDateLastItemTop = 0;\n\tClickHandlerPtr _scrollDateLink;\n\tSingleQueuedInvokation _applyUpdatedScrollState;\n\n\tMessagesBar _bar;\n\trpl::variable<QString> _barText;\n\n\tMouseAction _mouseAction = MouseAction::None;\n\tTextSelectType _mouseSelectType = TextSelectType::Letters;\n\tQPoint _mousePosition;\n\tMouseState _overState;\n\tMouseState _pressState;\n\tElement *_overElement = nullptr;\n\tHistoryItem *_overItemExact = nullptr;\n\tHistoryItem *_pressItemExact = nullptr;\n\tCursorState _mouseCursorState = CursorState();\n\tuint16 _mouseTextSymbol = 0;\n\tbool _pressWasInactive = false;\n\tbool _overSenderUserpic = false;\n\tbool _mouseActive = false;\n\n\tbool _selectEnabled = false;\n\tHistoryItem *_selectedTextItem = nullptr;\n\tTextSelection _selectedTextRange;\n\tTextForMimeData _selectedText;\n\tSelectedMap _selected;\n\tbase::flat_set<FullMsgId> _dragSelected;\n\tDragSelectAction _dragSelectAction = DragSelectAction::None;\n\tbool _dragSelectDirectionUp = false;\n\t// Was some text selected in current drag action.\n\tbool _wasSelectedText = false;\n\tQt::CursorShape _cursor = style::cur_default;\n\n\tbool _isChatWide = false;\n\tbool _refreshingViewer = false;\n\tbool _showFinished = false;\n\tbool _resizePending = false;\n\tstd::optional<ElementChatMode> _overrideChatMode;\n\n\t// _menu must be destroyed before _whoReactedMenuLifetime.\n\trpl::lifetime _whoReactedMenuLifetime;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tQPoint _trippleClickPoint;\n\tcrl::time _trippleClickStartTime = 0;\n\n\tElementHighlighter _highlighter;\n\n\tmutable bool _lastInSelectionMode = false;\n\tmutable Ui::Animations::Simple _inSelectionModeAnimation;\n\n\t// scroll by touch support (at least Windows Surface tablets)\n\tbool _touchScroll = false;\n\tbool _touchSelect = false;\n\tbool _touchInProgress = false;\n\tQPoint _touchStart, _touchPrevPos, _touchPos;\n\trpl::variable<bool> _touchMaybeSelecting;\n\tbase::Timer _touchSelectTimer;\n\n\tUi::DraggingScrollManager _selectScroll;\n\n\tInfoTooltip _topToast;\n\n\tUi::TouchScrollState _touchScrollState = Ui::TouchScrollState();\n\tbool _touchPrevPosValid = false;\n\tbool _touchWaitingAcceleration = false;\n\tQPoint _touchSpeed;\n\tcrl::time _touchSpeedTime = 0;\n\tcrl::time _touchAccelerationTime = 0;\n\tcrl::time _touchTime = 0;\n\tbase::Timer _touchScrollTimer;\n\tUi::MiddleClickAutoscroll _middleClickAutoscroll;\n\n\trpl::event_stream<FullMsgId> _requestedToEditMessage;\n\trpl::event_stream<ReplyToMessageRequest> _requestedToReplyToMessage;\n\trpl::event_stream<FullMsgId> _requestedToReadMessage;\n\trpl::event_stream<FullMsgId> _requestedToShowMessage;\n\tFn<void(QString)> _insertTextCallback;\n\trpl::event_stream<not_null<QKeyEvent*>> _scrollKeyEvents;\n\n\t[[nodiscard]] ElementOverlayHost &ensureOverlayHost();\n\tstd::unique_ptr<ElementOverlayHost> _overlayHost;\n\n\tvoid setupThanosEffect();\n\tstd::unique_ptr<Ui::ThanosEffectController> _thanosController;\n\n\trpl::lifetime _viewerLifetime;\n\n};\n\nvoid ConfirmDeleteSelectedItems(not_null<ListWidget*> widget);\nvoid ConfirmForwardSelectedItems(not_null<ListWidget*> widget);\nvoid ConfirmSendNowSelectedItems(not_null<ListWidget*> widget);\n\n[[nodiscard]] CopyRestrictionType CopyRestrictionTypeFor(\n\tnot_null<PeerData*> peer,\n\tHistoryItem *item = nullptr);\n[[nodiscard]] CopyRestrictionType CopyMediaRestrictionTypeFor(\n\tnot_null<PeerData*> peer,\n\tnot_null<HistoryItem*> item);\n[[nodiscard]] CopyRestrictionType SelectRestrictionTypeFor(\n\tnot_null<PeerData*> peer);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_message.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_message.h\"\n\n#include \"api/api_suggest_post.h\"\n#include \"api/api_transcribes.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"base/unixtime.h\"\n#include \"core/click_handler_types.h\" // ClickHandlerContext\n#include \"core/ui_integration.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/view/media/history_view_media_generic.h\"\n#include \"history/view/media/history_view_web_page.h\"\n#include \"history/view/media/history_view_suggest_decision.h\"\n#include \"history/view/reactions/history_view_reactions.h\"\n#include \"history/view/reactions/history_view_reactions_button.h\"\n#include \"history/view/history_view_reply_button.h\"\n#include \"history/view/history_view_group_call_bar.h\" // UserpicInRow.\n#include \"history/view/history_view_reply.h\"\n#include \"history/view/history_view_transcribe_button.h\"\n#include \"history/view/history_view_summary_header.h\"\n#include \"history/view/history_view_view_button.h\" // ViewButton.\n#include \"history/history.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"boxes/share_box.h\"\n#include \"boxes/peers/tag_info_box.h\"\n#include \"ui/effects/reaction_fly_animation.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/text/text_extended_data.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/rect.h\"\n//#include \"ui/round_rect.h\"\n#include \"data/components/factchecks.h\"\n#include \"data/components/sponsored_messages.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_message_reactions.h\"\n#include \"lang/lang_keys.h\"\n#include \"mainwidget.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/painter.h\"\n#include \"window/themes/window_theme.h\" // IsNightMode.\n#include \"window/window_session_controller.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_polls.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kSummarizeThreshold = 512;\nconstexpr auto kPlayStatusLimit = 2;\nconstexpr auto kMaxWidth = (1 << 16) - 1;\nconstexpr auto kMaxNiceToReadLines = 6;\nconst auto kPsaTooltipPrefix = \"cloud_lng_tooltip_psa_\";\nconstexpr auto kFullLineAppearDuration = crl::time(300);\nconstexpr auto kFullLineAppearFinalDuration = crl::time(120);\nconstexpr auto kLineHeightAppearDuration = crl::time(100);\nconstexpr auto kLineHeightAppearFinalDuration = crl::time(60);\nconstexpr auto kMinWidthAppearDuration = crl::time(160);\n\nvoid ApplyRevealGradient(\n\t\tnot_null<const TextAppearing*> appearing,\n\t\tQImage &cache,\n\t\tint availableWidth) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto maskWidth = int(st::textRevealGradient) * ratio;\n\tif (appearing->gradientMask.width() != maskWidth) {\n\t\tauto mask = QImage(\n\t\t\tQSize(maskWidth, 1),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tmask.setDevicePixelRatio(ratio);\n\t\tmask.fill(Qt::transparent);\n\t\t{\n\t\t\tconst auto logicalWidth = int(st::textRevealGradient);\n\t\t\tauto p = QPainter(&mask);\n\t\t\tauto gradient = QLinearGradient(0, 0, logicalWidth, 0);\n\t\t\tgradient.setStops({\n\t\t\t\t{ 0., QColor(255, 255, 255, 255) },\n\t\t\t\t{ 1., QColor(255, 255, 255, 0) },\n\t\t\t});\n\t\t\tp.fillRect(0, 0, logicalWidth, 1, gradient);\n\t\t}\n\t\tappearing->gradientMask = std::move(mask);\n\t}\n\n\tconst auto cacheW = int(cache.width() / cache.devicePixelRatio());\n\tconst auto cacheH = int(cache.height() / cache.devicePixelRatio());\n\tconst auto revealedWidth = appearing->revealedLineWidth;\n\tconst auto gradientWidth = std::min(\n\t\tint(st::textRevealGradient),\n\t\trevealedWidth);\n\tconst auto lineRtl = appearing->lines[appearing->shownLine].rtl;\n\n\tauto p = QPainter(&cache);\n\tp.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\tif (lineRtl) {\n\t\tconst auto rightEdge = availableWidth - revealedWidth;\n\t\tp.fillRect(\n\t\t\tQRect(0, 0, rightEdge, cacheH),\n\t\t\tQt::transparent);\n\t\tp.save();\n\t\tp.translate(rightEdge + gradientWidth, 0);\n\t\tp.scale(-1, 1);\n\t\tp.drawImage(\n\t\t\tQRect(0, 0, gradientWidth, cacheH),\n\t\t\tappearing->gradientMask);\n\t\tp.restore();\n\t} else {\n\t\tp.fillRect(\n\t\t\tQRect(revealedWidth, 0, cacheW - revealedWidth, cacheH),\n\t\t\tQt::transparent);\n\t\tp.drawImage(\n\t\t\tQRect(revealedWidth - gradientWidth, 0, gradientWidth, cacheH),\n\t\t\tappearing->gradientMask);\n\t}\n}\n\nstruct SecondRightAction {\n\tstd::unique_ptr<Ui::RippleAnimation> ripple;\n\tClickHandlerPtr link;\n};\n\nstruct BadgePillGeometry {\n\tint textWidth = 0;\n\tint width = 0;\n\tint height = 0;\n};\n\n[[nodiscard]] bool IsRippleLink(const ClickHandlerPtr &handler) {\n\tswitch (handler->getTextEntity().type) {\n\tcase EntityType::Url:\n\tcase EntityType::CustomUrl:\n\tcase EntityType::Email:\n\tcase EntityType::Hashtag:\n\tcase EntityType::Cashtag:\n\tcase EntityType::Mention:\n\tcase EntityType::MentionName:\n\tcase EntityType::BotCommand:\n\tcase EntityType::Phone:\n\tcase EntityType::BankCard:\n\tcase EntityType::FormattedDate:\n\t\treturn true;\n\tdefault:\n\t\treturn false;\n\t}\n}\n\n[[nodiscard]] ClickHandlerPtr MakeTopicButtonLink(\n\t\tnot_null<Data::ForumTopic*> topic,\n\t\tMsgId messageId) {\n\tconst auto weak = base::make_weak(topic);\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tcontroller->showTopic(\n\t\t\t\t\tstrong,\n\t\t\t\t\tmessageId,\n\t\t\t\t\tWindow::SectionShow::Way::Forward);\n\t\t\t}\n\t\t}\n\t});\n}\n\n[[nodiscard]] BadgePillGeometry ComputeBadgePillGeometry(\n\t\tnot_null<const RightBadge*> badge) {\n\tconst auto &padding = st::msgTagBadgePadding;\n\tconst auto textWidth = badge->tag.maxWidth();\n\tconst auto contentWidth = padding.left()\n\t\t+ textWidth\n\t\t+ padding.right();\n\tconst auto height = padding.top()\n\t\t+ st::msgFont->height\n\t\t+ padding.bottom();\n\treturn {\n\t\t.textWidth = textWidth,\n\t\t.width = std::max(contentWidth, height),\n\t\t.height = height,\n\t};\n}\n\n} // namespace\n\nstruct Message::CommentsButton {\n\tstd::unique_ptr<Ui::RippleAnimation> ripple;\n\tstd::vector<UserpicInRow> userpics;\n\tQImage cachedUserpics;\n\tClickHandlerPtr link;\n\tQPoint lastPoint;\n\tint rippleShift = 0;\n};\n\nstruct Message::FromNameStatus {\n\tEmojiStatusId id;\n\tstd::unique_ptr<Ui::Text::CustomEmoji> custom;\n\tClickHandlerPtr link;\n\tint skip = 0;\n};\n\nstruct Message::RightAction {\n\tstd::unique_ptr<Ui::RippleAnimation> ripple;\n\tClickHandlerPtr link;\n\tQPoint lastPoint;\n\tstd::unique_ptr<SecondRightAction> second;\n};\n\nstruct Message::LinkRipple {\n\tstd::unique_ptr<Ui::RippleAnimation> ripple;\n\tClickHandlerPtr link;\n\tQPoint maskOffset;\n\tint cachedWidth = 0;\n};\n\nLogEntryOriginal::LogEntryOriginal() = default;\n\nLogEntryOriginal::LogEntryOriginal(LogEntryOriginal &&other)\n: page(std::move(other.page)) {\n}\n\nLogEntryOriginal &LogEntryOriginal::operator=(LogEntryOriginal &&other) {\n\tpage = std::move(other.page);\n\treturn *this;\n}\n\nLogEntryOriginal::~LogEntryOriginal() = default;\n\nMessage::Message(\n\tnot_null<ElementDelegate*> delegate,\n\tnot_null<HistoryItem*> data,\n\tElement *replacing)\n: Element(delegate, data, replacing, Flag(0))\n, _hideReply(delegate->elementHideReply(this))\n, _postShowingAuthor(data->isPostShowingAuthor() ? 1 : 0)\n, _bottomInfo(\n\t\t&data->history()->owner().reactions(),\n\t\tBottomInfoDataFromMessage(this)) {\n\tif (data->Get<HistoryMessageSuggestion>()) {\n\t\t_hideReply = 1;\n\t} else if (const auto media = data->media()) {\n\t\tif (media->giveawayResults()) {\n\t\t\t_hideReply = 1;\n\t\t}\n\t}\n\tinitLogEntryOriginal();\n\tinitPsa();\n\tsetupReactions(replacing);\n\tauto animation = replacing ? replacing->takeEffectAnimation() : nullptr;\n\tif (animation) {\n\t\t_bottomInfo.continueEffectAnimation(std::move(animation));\n\t}\n\tif (data->isSponsored()) {\n\t\tconst auto &session = data->history()->session();\n\t\tconst auto details = session.sponsoredMessages().lookupDetails(\n\t\t\tdata->fullId());\n\t\tif (details.canReport) {\n\t\t\t_rightAction = std::make_unique<RightAction>();\n\t\t\t_rightAction->second = std::make_unique<SecondRightAction>();\n\n\t\t\t_rightAction->second->link = ReportSponsoredClickHandler(data);\n\t\t}\n\t}\n\tinitPaidInformation();\n\n\tif (data->textAppearing()) {\n\t\tAddComponents(TextAppearing::Bit());\n\t\tconst auto appearing = Get<TextAppearing>();\n\t\tif (replacing) {\n\t\t\tif (const auto was = replacing->Get<TextAppearing>()) {\n\t\t\t\t*appearing = std::move(*was);\n\t\t\t\tappearing->widthAnimation.setCallback([=] {\n\t\t\t\t\ttextAppearWidthCallback();\n\t\t\t\t});\n\t\t\t\tappearing->heightAnimation.setCallback([=] {\n\t\t\t\t\ttextAppearHeightCallback();\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tif (data->textAppearingStarted()\n\t\t\t&& !appearing->widthAnimation.animating()\n\t\t\t&& !appearing->heightAnimation.animating()) {\n\t\t\tskipInactiveTextAppearing();\n\t\t}\n\t}\n}\n\nMessage::~Message() {\n\tif (_comments || (_fromNameStatus && _fromNameStatus->custom)) {\n\t\t_comments = nullptr;\n\t\t_fromNameStatus = nullptr;\n\t\tcheckHeavyPart();\n\t}\n}\n\nvoid Message::refreshSuggestedInfo(\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<const HistoryMessageSuggestion*> suggest,\n\t\tconst HistoryMessageReply *replyData) {\n\tconst auto link = (replyData && replyData->resolvedMessage)\n\t\t? JumpToMessageClickHandler(\n\t\t\treplyData->resolvedMessage.get(),\n\t\t\titem->fullId())\n\t\t: ClickHandlerPtr();\n\tsetServicePreMessage({}, link, std::make_unique<MediaGeneric>(\n\t\tthis,\n\t\tGenerateSuggestRequestMedia(this, suggest),\n\t\tMediaGenericDescriptor{\n\t\t\t.maxWidth = st::chatSuggestWidth,\n\t\t\t.fullAreaLink = link,\n\t\t\t.service = true,\n\t\t\t.hideServiceText = true,\n\t\t}));\n}\n\nvoid Message::initPaidInformation() {\n\tconst auto item = data();\n\tif (item->history()->peer->isMonoforum()) {\n\t\tif (const auto suggest = item->Get<HistoryMessageSuggestion>()) {\n\t\t\tconst auto replyData = item->Get<HistoryMessageReply>();\n\t\t\trefreshSuggestedInfo(item, suggest, replyData);\n\t\t}\n\t\treturn;\n\t} else if (!item->history()->peer->isUser()) {\n\t\treturn;\n\t}\n\tconst auto media = this->media();\n\tconst auto mine = PaidInformation{\n\t\t.messages = 1,\n\t\t.stars = item->starsPaid(),\n\t};\n\tauto info = media ? media->paidInformation().value_or(mine) : mine;\n\tif (!info) {\n\t\treturn;\n\t}\n\tconst auto action = [&] {\n\t\treturn (info.messages == 1)\n\t\t\t? tr::lng_action_paid_message_one(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked)\n\t\t\t: tr::lng_action_paid_message_some(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tinfo.messages,\n\t\t\t\ttr::marked);\n\t};\n\tauto text = PreparedServiceText{\n\t\t.text = item->out()\n\t\t\t? tr::lng_action_paid_message_sent(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tinfo.stars,\n\t\t\t\tlt_action,\n\t\t\t\taction(),\n\t\t\t\ttr::marked)\n\t\t\t: tr::lng_action_paid_message_got(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tinfo.stars,\n\t\t\t\tlt_name,\n\t\t\t\ttr::link(item->from()->shortName(), 1),\n\t\t\t\ttr::marked),\n\t};\n\tif (!item->out()) {\n\t\ttext.links.push_back(item ->from()->createOpenLink());\n\t}\n\tsetServicePreMessage(std::move(text));\n}\n\nvoid Message::refreshRightBadge() {\n\tif (const auto badge = Get<RightBadge>(); badge && badge->overridden) {\n\t\treturn;\n\t}\n\tif (hasOutLayout()) {\n\t\tif (Has<RightBadge>()) {\n\t\t\tRemoveComponents(RightBadge::Bit());\n\t\t}\n\t\treturn;\n\t}\n\tconst auto item = data();\n\tconst auto [text, role, special] = [&]() -> std::tuple<QString, BadgeRole, bool> {\n\t\tif (item->isDiscussionPost()) {\n\t\t\treturn {\n\t\t\t\t(delegate()->elementContext() == Context::Replies)\n\t\t\t\t\t? QString()\n\t\t\t\t\t: tr::lng_channel_badge(tr::now),\n\t\t\t\tBadgeRole::User,\n\t\t\t\ttrue,\n\t\t\t};\n\t\t} else if (item->author()->isMegagroup()) {\n\t\t\tif (const auto msgsigned = item->Get<HistoryMessageSigned>()) {\n\t\t\t\tif (!msgsigned->viaBusinessBot) {\n\t\t\t\t\tAssert(msgsigned->isAnonymousRank);\n\t\t\t\t\treturn { msgsigned->author, BadgeRole::User, false };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst auto channel = item->history()->peer->asMegagroup();\n\t\tconst auto user = item->author()->asUser();\n\t\tif (!channel) {\n\t\t\tif (const auto chat = item->history()->peer->asChat()) {\n\t\t\t\tif (user) {\n\t\t\t\t\tconst auto j = chat->memberRanks.find(\n\t\t\t\t\t\tpeerToUser(user->id));\n\t\t\t\t\tif (j != chat->memberRanks.end()) {\n\t\t\t\t\t\tconst auto basicRole\n\t\t\t\t\t\t\t= (peerToUser(user->id) == chat->creator)\n\t\t\t\t\t\t\t? BadgeRole::Creator\n\t\t\t\t\t\t\t: chat->admins.contains(user)\n\t\t\t\t\t\t\t? BadgeRole::Admin\n\t\t\t\t\t\t\t: BadgeRole::User;\n\t\t\t\t\t\treturn { j->second, basicRole, false };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { QString(), BadgeRole::User, false };\n\t\t}\n\t\tif (!user) {\n\t\t\treturn { QString(), BadgeRole::User, false };\n\t\t}\n\t\tconst auto info = channel->mgInfo.get();\n\t\tconst auto userId = peerToUser(user->id);\n\t\tconst auto isCreator = (info->creator == user);\n\t\tconst auto isAdmin = info->admins.contains(userId);\n\t\tif (isCreator || isAdmin) {\n\t\t\tconst auto r = info->memberRanks.find(userId);\n\t\t\tif (r != info->memberRanks.end() && !r->second.isEmpty()) {\n\t\t\t\treturn {\n\t\t\t\t\tr->second,\n\t\t\t\t\tisCreator ? BadgeRole::Creator : BadgeRole::Admin,\n\t\t\t\t\tfalse,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (isCreator) {\n\t\t\t\treturn { tr::lng_owner_badge(tr::now), BadgeRole::Creator, false };\n\t\t\t}\n\t\t\treturn { tr::lng_admin_badge(tr::now), BadgeRole::Admin, false };\n\t\t}\n\t\tconst auto fromRank = item->fromRank();\n\t\tif (!fromRank.isEmpty()) {\n\t\t\treturn { fromRank, BadgeRole::User, false };\n\t\t}\n\t\treturn { QString(), BadgeRole::User, false };\n\t}();\n\tauto tagText = TextWithEntities{\n\t\t(text.isEmpty()\n\t\t\t? delegate()->elementAuthorRank(this)\n\t\t\t: TextUtilities::RemoveEmoji(TextUtilities::SingleLine(text)))\n\t};\n\tconst auto boosts = item->boostsApplied();\n\tconst auto needBadge = !tagText.empty() || boosts;\n\tif (!needBadge) {\n\t\tif (Has<RightBadge>()) {\n\t\t\tRemoveComponents(RightBadge::Bit());\n\t\t}\n\t\treturn;\n\t}\n\tif (!Has<RightBadge>()) {\n\t\tAddComponents(RightBadge::Bit());\n\t}\n\tconst auto badge = Get<RightBadge>();\n\tbadge->role = role;\n\tbadge->special = special || (text.isEmpty() && !tagText.empty());\n\tbadge->tagLink = nullptr;\n\tbadge->ripple = nullptr;\n\tif (tagText.empty()) {\n\t\tbadge->tag.clear();\n\t} else {\n\t\tbadge->tag.setMarkedText(\n\t\t\tst::defaultTextStyle,\n\t\t\ttagText,\n\t\t\tUi::NameTextOptions());\n\t}\n\tif (boosts) {\n\t\tconst auto many = (boosts > 1);\n\t\tauto boostText = Ui::Text::IconEmoji(many\n\t\t\t? &st::boostsMessageIcon\n\t\t\t: &st::boostMessageIcon\n\t\t).append(many ? QString::number(boosts) : QString());\n\t\tbadge->boosts.setMarkedText(\n\t\t\tst::defaultTextStyle,\n\t\t\tboostText,\n\t\t\tUi::NameTextOptions());\n\t} else {\n\t\tbadge->boosts.clear();\n\t}\n\tconst auto boostWidth = badge->boosts.isEmpty()\n\t\t? 0\n\t\t: (st::msgTagBadgeBoostSkip + badge->boosts.maxWidth());\n\tif (badge->role == BadgeRole::User) {\n\t\tconst auto tagWidth = badge->tag.isEmpty()\n\t\t\t? 0\n\t\t\t: badge->tag.maxWidth();\n\t\tbadge->width = tagWidth + boostWidth;\n\t} else {\n\t\tconst auto &padding = st::msgTagBadgePadding;\n\t\tconst auto textWidth = badge->tag.maxWidth();\n\t\tconst auto contentWidth = padding.left()\n\t\t\t+ textWidth\n\t\t\t+ padding.right();\n\t\tconst auto pillHeight = padding.top()\n\t\t\t+ st::msgFont->height\n\t\t\t+ padding.bottom();\n\t\tbadge->width = std::max(contentWidth, pillHeight) + boostWidth;\n\t}\n}\n\nint Message::rightBadgeWidth() const {\n\tconst auto badge = Get<RightBadge>();\n\treturn badge ? badge->width : 0;\n}\n\nvoid Message::applyGroupAdminChanges(\n\t\tconst base::flat_set<UserId> &changes) {\n\tif (!data()->out()\n\t\t&& changes.contains(peerToUser(data()->author()->id))) {\n\t\thistory()->owner().requestViewResize(this);\n\t}\n}\n\nvoid Message::animateReaction(Ui::ReactionFlyAnimationArgs &&args) {\n\tconst auto item = data();\n\tconst auto media = this->media();\n\n\tauto g = countGeometry();\n\tif (g.width() < 1 || isHidden()) {\n\t\treturn;\n\t}\n\tconst auto repainter = [=] { repaint(); };\n\n\tconst auto bubble = drawBubble();\n\tconst auto reactionsInBubble = _reactions && embedReactionsInBubble();\n\tconst auto mediaDisplayed = media && media->isDisplayed();\n\n\tif (_reactions && !reactionsInBubble) {\n\t\tconst auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height();\n\t\tconst auto reactionsLeft = (!bubble && mediaDisplayed)\n\t\t\t? media->contentRectForReactions().x()\n\t\t\t: 0;\n\t\tg.setHeight(g.height() - reactionsHeight);\n\t\tconst auto reactionsPosition = QPoint(reactionsLeft + g.left(), g.top() + g.height() + st::mediaInBubbleSkip);\n\t\t_reactions->animate(args.translated(-reactionsPosition), repainter);\n\t\treturn;\n\t}\n\n\tconst auto keyboard = item->inlineReplyKeyboard();\n\tauto keyboardHeight = 0;\n\tif (keyboard) {\n\t\tkeyboardHeight = keyboard->naturalHeight();\n\t\tg.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight);\n\t}\n\n\tif (bubble) {\n\t\t// Entry page is always a bubble bottom.\n\t\tauto inner = g;\n\t\tif (_comments) {\n\t\t\tinner.setHeight(inner.height() - st::historyCommentsButtonHeight);\n\t\t}\n\t\tauto trect = inner.marginsRemoved(st::msgPadding);\n\t\tconst auto reactionsTop = (reactionsInBubble && !_viewButton)\n\t\t\t? st::mediaInBubbleSkip\n\t\t\t: 0;\n\t\tconst auto reactionsHeight = reactionsInBubble\n\t\t\t? (reactionsTop + _reactions->height())\n\t\t\t: 0;\n\t\tif (reactionsInBubble) {\n\t\t\ttrect.setHeight(trect.height() - reactionsHeight);\n\t\t\tconst auto reactionsPosition = QPoint(trect.left(), trect.top() + trect.height() + reactionsTop);\n\t\t\t_reactions->animate(args.translated(-reactionsPosition), repainter);\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nauto Message::takeEffectAnimation()\n-> std::unique_ptr<Ui::ReactionFlyAnimation> {\n\treturn _bottomInfo.takeEffectAnimation();\n}\n\nQRect Message::effectIconGeometry() const {\n\tconst auto item = data();\n\tconst auto media = this->media();\n\n\tauto g = countGeometry();\n\tif (g.width() < 1 || isHidden()) {\n\t\treturn {};\n\t}\n\tconst auto bubble = drawBubble();\n\tconst auto reactionsInBubble = _reactions && embedReactionsInBubble();\n\tconst auto mediaDisplayed = media && media->isDisplayed();\n\tconst auto keyboard = item->inlineReplyKeyboard();\n\tauto keyboardHeight = 0;\n\tif (keyboard) {\n\t\tkeyboardHeight = keyboard->naturalHeight();\n\t\tg.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight);\n\t}\n\n\tconst auto fromBottomInfo = [&](QPoint bottomRight) {\n\t\tconst auto size = _bottomInfo.currentSize();\n\t\treturn _bottomInfo.effectIconGeometry().translated(\n\t\t\tbottomRight - QPoint(size.width(), size.height()));\n\t};\n\tif (bubble) {\n\t\tconst auto entry = logEntryOriginal();\n\t\tconst auto check = factcheckBlock();\n\n\t\t// Entry page is always a bubble bottom.\n\t\tauto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/);\n\t\tauto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop());\n\n\t\tauto inner = g;\n\t\tif (_comments) {\n\t\t\tinner.setHeight(inner.height() - st::historyCommentsButtonHeight);\n\t\t}\n\t\tauto trect = inner.marginsRemoved(st::msgPadding);\n\t\tconst auto reactionsTop = (reactionsInBubble && !_viewButton)\n\t\t\t? st::mediaInBubbleSkip\n\t\t\t: 0;\n\t\tconst auto reactionsHeight = reactionsInBubble\n\t\t\t? (reactionsTop + _reactions->height())\n\t\t\t: 0;\n\t\tif (_viewButton) {\n\t\t\tconst auto belowInfo = _viewButton->belowMessageInfo();\n\t\t\tconst auto infoHeight = reactionsInBubble\n\t\t\t\t? (reactionsHeight + 2 * st::mediaInBubbleSkip)\n\t\t\t\t: _bottomInfo.height();\n\t\t\tconst auto heightMargins = QMargins(0, 0, 0, infoHeight);\n\t\t\tif (belowInfo) {\n\t\t\t\tinner -= heightMargins;\n\t\t\t}\n\t\t\ttrect.setHeight(trect.height() - _viewButton->height());\n\t\t\tif (reactionsInBubble) {\n\t\t\t\ttrect.setHeight(trect.height() - st::mediaInBubbleSkip + st::msgPadding.bottom());\n\t\t\t} else if (mediaDisplayed) {\n\t\t\t\ttrect.setHeight(trect.height() - st::mediaInBubbleSkip);\n\t\t\t}\n\t\t}\n\t\tif (mediaOnBottom) {\n\t\t\ttrect.setHeight(trect.height()\n\t\t\t\t+ st::msgPadding.bottom()\n\t\t\t\t- viewButtonHeight());\n\t\t}\n\t\tif (mediaOnTop) {\n\t\t\ttrect.setY(trect.y() - st::msgPadding.top());\n\t\t}\n\t\tif (mediaDisplayed && mediaOnBottom && media->customInfoLayout()) {\n\t\t\tauto mediaHeight = media->height();\n\t\t\tauto mediaLeft = trect.x() - st::msgPadding.left();\n\t\t\tauto mediaTop = (trect.y() + trect.height() - mediaHeight);\n\t\t\treturn fromBottomInfo(QPoint(mediaLeft, mediaTop) + media->resolveCustomInfoRightBottom());\n\t\t} else {\n\t\t\treturn fromBottomInfo({\n\t\t\t\tinner.left() + inner.width() - (st::msgPadding.right() - st::msgDateDelta.x()),\n\t\t\t\tinner.top() + inner.height() - (st::msgPadding.bottom() - st::msgDateDelta.y()),\n\t\t\t});\n\t\t}\n\t} else if (mediaDisplayed) {\n\t\treturn fromBottomInfo(g.topLeft() + media->resolveCustomInfoRightBottom());\n\t}\n\treturn {};\n}\n\nQSize Message::performCountOptimalSize() {\n\tconst auto item = data();\n\n\tconst auto replyData = item->Get<HistoryMessageReply>();\n\tconst auto &summary = item->summaryEntry();\n\tconst auto showSummaryReply = !summary.result.empty() && summary.shown;\n\n\tif (replyData && !_hideReply) {\n\t\tAddComponents(Reply::Bit());\n\t} else {\n\t\tRemoveComponents(Reply::Bit());\n\t}\n\tif (showSummaryReply) {\n\t\tAddComponents(SummaryHeader::Bit());\n\t} else {\n\t\tRemoveComponents(SummaryHeader::Bit());\n\t}\n\n\tif (item->history()->peer->isMonoforum()) {\n\t\tif (const auto suggest = item->Get<HistoryMessageSuggestion>()) {\n\t\t\tif (const auto service = Get<ServicePreMessage>()) {\n\t\t\t\t// Ok, we didn't have the message, but now we have.\n\t\t\t\t// That means this is not a plain post suggestion,\n\t\t\t\t// but a suggestion of changes to previous suggestion.\n\t\t\t\tif (service->media\n\t\t\t\t\t&& !service->handler\n\t\t\t\t\t&& replyData\n\t\t\t\t\t&& replyData->resolvedMessage) {\n\t\t\t\t\trefreshSuggestedInfo(item, suggest, replyData);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (const auto postSender = item->discussionPostOriginalSender()) {\n\t\tif (!postSender->isFullLoaded()) {\n\t\t\t// We need it for available reactions list.\n\t\t\tpostSender->updateFull();\n\t\t}\n\t}\n\n\tconst auto factcheck = item->Get<HistoryMessageFactcheck>();\n\tif (factcheck && !factcheck->data.text.empty()) {\n\t\tAddComponents(Factcheck::Bit());\n\t\tGet<Factcheck>()->page = history()->session().factchecks().makeMedia(\n\t\t\tthis,\n\t\t\tfactcheck);\n\t} else {\n\t\tRemoveComponents(Factcheck::Bit());\n\t}\n\trefreshRightBadge();\n\n\tconst auto markup = item->inlineReplyMarkup();\n\tconst auto reactionsKey = [&] {\n\t\treturn embedReactionsInBubble() ? 0 : 1;\n\t};\n\tconst auto oldKey = reactionsKey();\n\tif (_summarize) {\n\t\tconst auto &summary = item->summaryEntry();\n\t\tif (_summarize->loading() != summary.loading) {\n\t\t\t_summarize->setLoading(summary.loading);\n\t\t}\n\t}\n\tvalidateText();\n\tvalidateInlineKeyboard(markup);\n\tupdateViewButtonExistence();\n\trefreshTopicButton();\n\n\tconst auto media = this->media();\n\tconst auto textItem = this->textItem();\n\tconst auto defaultInvert = media && media->aboveTextByDefault();\n\tconst auto invertDefault = textItem\n\t\t&& textItem->invertMedia()\n\t\t&& !textItem->emptyText();\n\t_invertMedia = invertDefault ? !defaultInvert : defaultInvert;\n\n\tupdateMediaInBubbleState();\n\tif (oldKey != reactionsKey()) {\n\t\trefreshReactions();\n\t}\n\trefreshInfoSkipBlock(textItem);\n\n\tconst auto botTop = item->isFakeAboutView()\n\t\t? Get<FakeBotAboutTop>()\n\t\t: nullptr;\n\tconst auto bubble = drawBubble();\n\tauto withVisibleText = false;\n\tauto fullTextualWidth = 0;\n\tif (botTop) {\n\t\tbotTop->init();\n\t}\n\n\tauto maxWidth = 0;\n\tauto minHeight = 0;\n\n\tconst auto reactionsInBubble = _reactions && embedReactionsInBubble();\n\tif (_reactions) {\n\t\t_reactions->initDimensions();\n\t}\n\n\tconst auto reply = Get<Reply>();\n\tif (reply) {\n\t\treply->update(this, replyData);\n\t}\n\tconst auto summaryHeader = Get<SummaryHeader>();\n\tif (summaryHeader) {\n\t\tif (showSummaryReply) {\n\t\t\tsummaryHeader->update(this);\n\t\t}\n\t}\n\n\tif (bubble) {\n\t\tconst auto forwarded = item->Get<HistoryMessageForwarded>();\n\t\tconst auto via = item->Get<HistoryMessageVia>();\n\t\tconst auto entry = logEntryOriginal();\n\t\tconst auto check = factcheckBlock();\n\t\tif (forwarded) {\n\t\t\tforwarded->create(via, item);\n\t\t}\n\n\t\tauto mediaDisplayed = false;\n\t\tif (media) {\n\t\t\tmediaDisplayed = media->isDisplayed();\n\t\t\tmedia->initDimensions();\n\t\t}\n\t\tif (check) {\n\t\t\tcheck->initDimensions();\n\t\t}\n\t\tif (entry) {\n\t\t\tentry->initDimensions();\n\t\t}\n\n\t\t// Entry page is always a bubble bottom.\n\t\twithVisibleText = hasVisibleText();\n\t\tfullTextualWidth = textualMaxWidth();\n\t\tconst auto textualWidth = bubbleTextualWidth();\n\t\tauto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/);\n\t\tauto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop());\n\t\tmaxWidth = textualWidth;\n\t\tauto nonTextMax = 0;\n\t\tif (isCommentsRootView()) {\n\t\t\tmaxWidth = std::max(maxWidth, st::msgMaxWidth);\n\t\t\taccumulate_max(nonTextMax, st::msgMaxWidth);\n\t\t}\n\t\tminHeight = withVisibleText ? text().minHeight() : 0;\n\t\tif (reactionsInBubble) {\n\t\t\tconst auto reactionsMaxWidth = st::msgPadding.left()\n\t\t\t\t+ _reactions->maxWidth()\n\t\t\t\t+ st::msgPadding.right();\n\t\t\tconst auto reactionsLimited = std::min(\n\t\t\t\tst::msgMaxWidth,\n\t\t\t\treactionsMaxWidth);\n\t\t\taccumulate_max(maxWidth, reactionsLimited);\n\t\t\taccumulate_max(nonTextMax, reactionsLimited);\n\t\t\tif (mediaDisplayed\n\t\t\t\t&& !media->additionalInfoString().isEmpty()) {\n\t\t\t\t// In round videos in a web page status text is painted\n\t\t\t\t// in the bottom left corner, reactions should be below.\n\t\t\t\tminHeight += st::msgDateFont->height;\n\t\t\t} else {\n\t\t\t\tminHeight += st::mediaInBubbleSkip;\n\t\t\t}\n\t\t\tif (maxWidth >= reactionsMaxWidth) {\n\t\t\t\tminHeight += _reactions->minHeight();\n\t\t\t} else {\n\t\t\t\tconst auto widthForReactions = maxWidth\n\t\t\t\t\t- st::msgPadding.left()\n\t\t\t\t\t- st::msgPadding.right();\n\t\t\t\tminHeight += _reactions->resizeGetHeight(widthForReactions);\n\t\t\t}\n\t\t}\n\t\tif (!mediaOnBottom && (!_viewButton || !reactionsInBubble)) {\n\t\t\tminHeight += st::msgPadding.bottom();\n\t\t\tif (mediaDisplayed) {\n\t\t\t\tminHeight += st::mediaInBubbleSkip;\n\t\t\t}\n\t\t}\n\t\tif (!mediaOnTop) {\n\t\t\tminHeight += st::msgPadding.top();\n\t\t\tif (mediaDisplayed) minHeight += st::mediaInBubbleSkip;\n\t\t\tif (entry) minHeight += st::mediaInBubbleSkip;\n\t\t}\n\t\tif (check) minHeight += st::mediaInBubbleSkip;\n\t\tif (mediaDisplayed) {\n\t\t\t// Parts don't participate in maxWidth() in case of media message.\n\t\t\tif (media->enforceBubbleWidth()) {\n\t\t\t\tmaxWidth = media->maxWidth();\n\t\t\t\tconst auto innerWidth = maxWidth\n\t\t\t\t\t- st::msgPadding.left()\n\t\t\t\t\t- st::msgPadding.right();\n\t\t\t\tif (reactionsInBubble) {\n\t\t\t\t\tminHeight -= _reactions->minHeight();\n\t\t\t\t\tminHeight\n\t\t\t\t\t\t+= _reactions->countCurrentSize(innerWidth).height();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\taccumulate_max(maxWidth, media->maxWidth());\n\t\t\t}\n\t\t\tminHeight += media->minHeight();\n\t\t} else {\n\t\t\t// Count parts in maxWidth(), don't count them in minHeight().\n\t\t\t// They will be added in resizeGetHeight() anyway.\n\t\t\tif (displayFromName()) {\n\t\t\t\tconst auto from = item->displayFrom();\n\t\t\t\tvalidateFromNameText(from);\n\t\t\t\tconst auto &name = from\n\t\t\t\t\t? _fromName\n\t\t\t\t\t: item->displayHiddenSenderInfo()->nameText();\n\t\t\t\tauto namew = st::msgPadding.left()\n\t\t\t\t\t+ name.maxWidth()\n\t\t\t\t\t+ (_fromNameStatus\n\t\t\t\t\t\t? st::dialogsPremiumIcon.icon.width()\n\t\t\t\t\t\t: 0)\n\t\t\t\t\t+ st::msgPadding.right();\n\t\t\t\tif (via && !displayForwardedFrom()) {\n\t\t\t\t\tnamew += st::msgServiceFont->spacew + via->maxWidth\n\t\t\t\t\t\t+ (_fromNameStatus ? st::msgServiceFont->spacew : 0);\n\t\t\t\t}\n\t\t\t\tif (Has<RightBadge>()) {\n\t\t\t\t\tnamew += st::msgPadding.right() + rightBadgeWidth();\n\t\t\t\t}\n\t\t\t\taccumulate_max(maxWidth, namew);\n\t\t\t\taccumulate_max(nonTextMax, namew);\n\t\t\t} else if (via && !displayForwardedFrom()) {\n\t\t\t\tconst auto viaw = st::msgPadding.left() + via->maxWidth + st::msgPadding.right();\n\t\t\t\taccumulate_max(maxWidth, viaw);\n\t\t\t\taccumulate_max(nonTextMax, viaw);\n\t\t\t}\n\t\t\tif (displayedTopicButton()) {\n\t\t\t\tconst auto padding = st::msgPadding + st::topicButtonPadding;\n\t\t\t\tconst auto topicw = padding.left()\n\t\t\t\t\t+ _topicButton->name.maxWidth()\n\t\t\t\t\t+ st::topicButtonArrowSkip\n\t\t\t\t\t+ padding.right();\n\t\t\t\taccumulate_max(maxWidth, topicw);\n\t\t\t\taccumulate_max(nonTextMax, topicw);\n\t\t\t}\n\t\t\tif (displayForwardedFrom()) {\n\t\t\t\tconst auto skip1 = forwarded->psaType.isEmpty()\n\t\t\t\t\t? 0\n\t\t\t\t\t: st::historyPsaIconSkip1;\n\t\t\t\tauto namew = st::msgPadding.left() + forwarded->text.maxWidth() + skip1 + st::msgPadding.right();\n\t\t\t\tif (via) {\n\t\t\t\t\tnamew += st::msgServiceFont->spacew + via->maxWidth;\n\t\t\t\t}\n\t\t\t\taccumulate_max(maxWidth, namew);\n\t\t\t\taccumulate_max(nonTextMax, namew);\n\t\t\t}\n\t\t\tif (reply) {\n\t\t\t\tconst auto replyw = st::msgPadding.left()\n\t\t\t\t\t+ reply->maxWidth()\n\t\t\t\t\t+ st::msgPadding.right();\n\t\t\t\taccumulate_max(maxWidth, replyw);\n\t\t\t\taccumulate_max(nonTextMax, replyw);\n\t\t\t}\n\t\t\tif (summaryHeader) {\n\t\t\t\tconst auto summaryHeaderWidth = st::msgPadding.left()\n\t\t\t\t\t+ summaryHeader->maxWidth()\n\t\t\t\t\t+ st::msgPadding.right();\n\t\t\t\taccumulate_max(maxWidth, summaryHeaderWidth);\n\t\t\t\taccumulate_max(nonTextMax, summaryHeaderWidth);\n\t\t\t}\n\t\t\tif (check) {\n\t\t\t\taccumulate_max(maxWidth, check->maxWidth());\n\t\t\t\taccumulate_max(nonTextMax, check->maxWidth());\n\t\t\t\tminHeight += check->minHeight();\n\t\t\t}\n\t\t\tif (entry) {\n\t\t\t\taccumulate_max(maxWidth, entry->maxWidth());\n\t\t\t\taccumulate_max(nonTextMax, entry->maxWidth());\n\t\t\t\tminHeight += entry->minHeight();\n\t\t\t}\n\t\t}\n\t\tif (withVisibleText && botTop) {\n\t\t\taccumulate_max(maxWidth, botTop->maxWidth);\n\t\t\taccumulate_max(nonTextMax, botTop->maxWidth);\n\t\t\tminHeight += botTop->height;\n\t\t}\n\t\taccumulate_max(maxWidth, minWidthForMedia());\n\t\taccumulate_max(nonTextMax, minWidthForMedia());\n\t\t_nonTextMaxWidth = std::min(nonTextMax, kMaxWidth);\n\t} else if (media) {\n\t\tmedia->initDimensions();\n\t\tmaxWidth = media->maxWidth();\n\t\tminHeight = media->isDisplayed() ? media->minHeight() : 0;\n\t} else {\n\t\tmaxWidth = st::msgMinWidth;\n\t\tminHeight = 0;\n\t}\n\t// if we have a text bubble we can resize it to fit the keyboard\n\t// but if we have only media we don't do that\n\tif (markup && markup->inlineKeyboard && hasVisibleText()) {\n\t\taccumulate_max(maxWidth, markup->inlineKeyboard->naturalWidth());\n\t\tif (bubble) {\n\t\t\tconst auto kbw = markup->inlineKeyboard->naturalWidth();\n\t\t\tif (kbw > int(_nonTextMaxWidth)) {\n\t\t\t\t_nonTextMaxWidth = std::min(kbw, kMaxWidth);\n\t\t\t}\n\t\t}\n\t}\n\tif (bubble && withVisibleText && maxWidth < fullTextualWidth) {\n\t\tminHeight -= text().minHeight();\n\t\tminHeight += textHeightFor(bubbleTextWidth(maxWidth));\n\t}\n\tif (const auto appearing = Get<TextAppearing>()) {\n\t\tappearing->geometryValid = false;\n\t\tappearing->startedForText = false;\n\t\tappearing->finalizing = item->isRegular();\n\t}\n\treturn QSize(maxWidth, minHeight);\n}\n\nvoid Message::refreshTopicButton() {\n\tconst auto item = data();\n\tif (isAttachedToPrevious() || delegate()->elementHideTopicButton(this)) {\n\t\t_topicButton = nullptr;\n\t} else if (const auto topic = item->topic()) {\n\t\tif (topic->peer()->useSubsectionTabs()) {\n\t\t\t_topicButton = nullptr;\n\t\t\treturn;\n\t\t}\n\t\tif (!_topicButton) {\n\t\t\t_topicButton = std::make_unique<TopicButton>();\n\t\t}\n\t\tconst auto jumpToId = IsServerMsgId(item->id) ? item->id : MsgId();\n\t\t_topicButton->link = MakeTopicButtonLink(topic, jumpToId);\n\t\tif (_topicButton->nameVersion != topic->titleVersion()) {\n\t\t\t_topicButton->nameVersion = topic->titleVersion();\n\t\t\tconst auto context = Core::TextContext({\n\t\t\t\t.session = &history()->session(),\n\t\t\t\t.repaint = [=] { customEmojiRepaint(); },\n\t\t\t\t.customEmojiLoopLimit = 1,\n\t\t\t});\n\t\t\t_topicButton->name.setMarkedText(\n\t\t\t\tst::fwdTextStyle,\n\t\t\t\ttopic->titleWithIcon(),\n\t\t\t\tkMarkupTextOptions,\n\t\t\t\tcontext);\n\t\t}\n\t} else {\n\t\t_topicButton = nullptr;\n\t}\n}\n\nint Message::marginTop() const {\n\tauto result = 0;\n\tif (!isHidden()) {\n\t\tif (isAttachedToPrevious()) {\n\t\t\tresult += st::msgMarginTopAttached;\n\t\t} else {\n\t\t\tresult += st::msgMargin.top();\n\t\t}\n\t}\n\tresult += displayedDateHeight();\n\tif (const auto bar = Get<UnreadBar>()) {\n\t\tresult += bar->height();\n\t}\n\tif (const auto bar = Get<ForumThreadBar>()) {\n\t\tresult += bar->height();\n\t}\n\tif (const auto service = Get<ServicePreMessage>()) {\n\t\tif (!service->below) {\n\t\t\tresult += service->height;\n\t\t}\n\t}\n\tif (const auto margins = Get<ViewAddedMargins>()) {\n\t\tresult += margins->top;\n\t}\n\treturn result;\n}\n\nint Message::marginBottom() const {\n\tif (isHidden()) {\n\t\treturn 0;\n\t}\n\tauto result = st::msgMargin.bottom();\n\tif (const auto service = Get<ServicePreMessage>()) {\n\t\tif (service->below) {\n\t\t\tresult += service->height;\n\t\t}\n\t}\n\tif (const auto margins = Get<ViewAddedMargins>()) {\n\t\tresult += margins->bottom;\n\t}\n\treturn result;\n}\n\nvoid Message::draw(Painter &p, const PaintContext &context) const {\n\tauto g = countGeometry();\n\tif (g.width() < 1) {\n\t\treturn;\n\t}\n\n\tconst auto item = data();\n\tconst auto media = this->media();\n\n\tconst auto hasGesture = context.gestureHorizontal.translation\n\t\t&& (context.gestureHorizontal.msgBareId == item->fullId().msg.bare);\n\tif (hasGesture) {\n\t\tp.translate(context.gestureHorizontal.translation, 0);\n\t}\n\tconst auto selectionModeResult = delegate()->elementInSelectionMode(this);\n\tconst auto selectionTranslation = (selectionModeResult.progress > 0)\n\t\t? (selectionModeResult.progress\n\t\t\t* AdditionalSpaceForSelectionCheckbox(this, g))\n\t\t: 0;\n\tif (selectionTranslation) {\n\t\tp.translate(selectionTranslation, 0);\n\t}\n\n\tif (item->hasUnrequestedFactcheck()) {\n\t\titem->history()->session().factchecks().requestFor(item);\n\t}\n\n\tconst auto stm = context.messageStyle();\n\tconst auto bubble = drawBubble();\n\n\tif (const auto bar = Get<UnreadBar>()) {\n\t\tauto unreadbarh = bar->height();\n\t\tauto aboveh = 0;\n\t\tif (const auto date = Get<DateBadge>()) {\n\t\t\taboveh += date->height();\n\t\t}\n\t\tif (const auto bar = Get<ForumThreadBar>()) {\n\t\t\taboveh += bar->height();\n\t\t}\n\t\tif (context.clip.intersects(QRect(0, aboveh, width(), unreadbarh))) {\n\t\t\tp.translate(0, aboveh);\n\t\t\tbar->paint(p, context, 0, width(), delegate()->elementChatMode());\n\t\t\tp.translate(0, -aboveh);\n\t\t}\n\t}\n\n\tif (const auto service = Get<ServicePreMessage>()) {\n\t\tservice->paint(p, context, g, delegate()->elementChatMode());\n\t}\n\n\tif (isHidden()) {\n\t\treturn;\n\t}\n\n\tconst auto entry = logEntryOriginal();\n\tconst auto check = factcheckBlock();\n\tauto mediaDisplayed = media && media->isDisplayed();\n\n\t// Entry page is always a bubble bottom.\n\tauto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/);\n\tauto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop());\n\n\tconst auto displayInfo = needInfoDisplay();\n\tconst auto reactionsInBubble = _reactions && embedReactionsInBubble();\n\n\t// We need to count geometry without keyboard and reactions\n\t// for bubble selection intervals counting below.\n\tauto gForIntervals = g;\n\tif (_reactions && !reactionsInBubble) {\n\t\tconst auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height();\n\t\tgForIntervals.setHeight(gForIntervals.height() - reactionsHeight);\n\t}\n\tconst auto keyboard = item->inlineReplyKeyboard();\n\tif (keyboard) {\n\t\tconst auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight();\n\t\tgForIntervals.setHeight(gForIntervals.height() - keyboardHeight);\n\t}\n\n\tauto mediaSelectionIntervals = (!context.selected() && mediaDisplayed)\n\t\t? media->getBubbleSelectionIntervals(context.selection)\n\t\t: std::vector<Ui::BubbleSelectionInterval>();\n\tauto localMediaTop = 0;\n\tconst auto customHighlight = mediaDisplayed && media->customHighlight();\n\tif (!mediaSelectionIntervals.empty() || customHighlight) {\n\t\tauto localMediaBottom = gForIntervals.top() + gForIntervals.height();\n\t\tif (data()->repliesAreComments() || data()->externalReply()) {\n\t\t\tlocalMediaBottom -= st::historyCommentsButtonHeight;\n\t\t}\n\t\tif (_viewButton) {\n\t\t\tlocalMediaBottom -= st::mediaInBubbleSkip + _viewButton->height();\n\t\t}\n\t\tif (reactionsInBubble) {\n\t\t\tlocalMediaBottom -= st::mediaInBubbleSkip + _reactions->height();\n\t\t}\n\t\tif (!mediaOnBottom && (!_viewButton || !reactionsInBubble)) {\n\t\t\tlocalMediaBottom -= st::msgPadding.bottom();\n\t\t\tif (mediaDisplayed) {\n\t\t\t\tlocalMediaBottom -= st::mediaInBubbleSkip;\n\t\t\t}\n\t\t}\n\t\tif (check) {\n\t\t\tlocalMediaBottom -= check->height();\n\t\t}\n\t\tif (entry) {\n\t\t\tlocalMediaBottom -= entry->height();\n\t\t}\n\t\tlocalMediaTop = localMediaBottom - media->height();\n\t\tfor (auto &[top, height] : mediaSelectionIntervals) {\n\t\t\ttop += localMediaTop;\n\t\t}\n\t}\n\n\t{\n\t\tif (selectionTranslation) {\n\t\t\tp.translate(-selectionTranslation, 0);\n\t\t}\n\t\tif (customHighlight) {\n\t\t\tmedia->drawHighlight(p, context, localMediaTop);\n\t\t} else {\n\t\t\tpaintHighlight(p, context, g.height());\n\t\t}\n\t\tif (selectionTranslation) {\n\t\t\tp.translate(selectionTranslation, 0);\n\t\t}\n\t}\n\n\tconst auto roll = media ? media->bubbleRoll() : Media::BubbleRoll();\n\tif (roll) {\n\t\tp.save();\n\t\tp.translate(g.center());\n\t\tp.rotate(roll.rotate);\n\t\tp.scale(roll.scale, roll.scale);\n\t\tp.translate(-g.center());\n\t}\n\n\tp.setTextPalette(stm->textPalette);\n\n\tif (_reactions && !reactionsInBubble) {\n\t\tconst auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height();\n\t\tconst auto reactionsLeft = (!bubble && mediaDisplayed)\n\t\t\t? media->contentRectForReactions().x()\n\t\t\t: 0;\n\t\tg.setHeight(g.height() - reactionsHeight);\n\t\tconst auto reactionsPosition = QPoint(reactionsLeft + g.left(), g.top() + g.height() + st::mediaInBubbleSkip);\n\t\tp.translate(reactionsPosition);\n\t\tprepareCustomEmojiPaint(p, context, *_reactions);\n\t\t_reactions->paint(p, context, g.width(), context.clip.translated(-reactionsPosition));\n\t\tif (context.reactionInfo) {\n\t\t\tcontext.reactionInfo->position = reactionsPosition;\n\t\t}\n\t\tp.translate(-reactionsPosition);\n\t}\n\n\tconst auto messageRounding = countMessageRounding();\n\tif (keyboard) {\n\t\tconst auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight();\n\t\tg.setHeight(g.height() - keyboardHeight);\n\n\t\tconst auto keyboardPosition = QPoint(g.left(), g.top() + g.height() + st::msgBotKbButton.margin);\n\t\tp.translate(keyboardPosition);\n\t\tkeyboard->paint(\n\t\t\tp,\n\t\t\tcontext.st,\n\t\t\tmessageRounding,\n\t\t\tg.width(),\n\t\t\tcontext.clip.translated(-keyboardPosition),\n\t\t\tcontext.paused);\n\t\tp.translate(-keyboardPosition);\n\t}\n\n\tif (context.highlightPathCache) {\n\t\tcontext.highlightInterpolateTo = g;\n\t\tcontext.highlightPathCache->clear();\n\t}\n\tif (bubble) {\n\t\tif (displayFromName()\n\t\t\t&& item->displayFrom()\n\t\t\t&& (_fromNameVersion < item->displayFrom()->nameVersion())) {\n\t\t\tfromNameUpdated(g.width());\n\t\t}\n\t\tUi::PaintBubble(\n\t\t\tp,\n\t\t\tUi::ComplexBubble{\n\t\t\t\t.simple = Ui::SimpleBubble{\n\t\t\t\t\t.st = context.st,\n\t\t\t\t\t.geometry = g,\n\t\t\t\t\t.pattern = context.bubblesPattern,\n\t\t\t\t\t.patternViewport = context.viewport,\n\t\t\t\t\t.outerWidth = width(),\n\t\t\t\t\t.selected = context.selected(),\n\t\t\t\t\t.outbg = context.outbg,\n\t\t\t\t\t.rounding = countBubbleRounding(messageRounding),\n\t\t\t\t},\n\t\t\t\t.selection = mediaSelectionIntervals,\n\t\t\t});\n\n\t\tauto inner = g;\n\t\tpaintCommentsButton(p, inner, context);\n\n\t\tauto trect = inner.marginsRemoved(st::msgPadding);\n\n\t\tconst auto additionalInfoSkip = (mediaDisplayed\n\t\t\t&& !media->additionalInfoString().isEmpty())\n\t\t\t? st::msgDateFont->height\n\t\t\t: 0;\n\t\tconst auto reactionsTop = (reactionsInBubble && !_viewButton)\n\t\t\t? (additionalInfoSkip + st::mediaInBubbleSkip)\n\t\t\t: additionalInfoSkip;\n\t\tconst auto reactionsHeight = reactionsInBubble\n\t\t\t? (reactionsTop + _reactions->height())\n\t\t\t: 0;\n\t\tif (reactionsInBubble) {\n\t\t\ttrect.setHeight(trect.height() - reactionsHeight);\n\t\t\tconst auto reactionsPosition = QPoint(trect.left(), trect.top() + trect.height() + reactionsTop);\n\t\t\tp.translate(reactionsPosition);\n\t\t\tprepareCustomEmojiPaint(p, context, *_reactions);\n\t\t\t_reactions->paint(p, context, g.width(), context.clip.translated(-reactionsPosition));\n\t\t\tif (context.reactionInfo) {\n\t\t\t\tcontext.reactionInfo->position = reactionsPosition;\n\t\t\t}\n\t\t\tp.translate(-reactionsPosition);\n\t\t}\n\n\t\tif (_viewButton) {\n\t\t\tconst auto belowInfo = _viewButton->belowMessageInfo();\n\t\t\tconst auto infoHeight = reactionsInBubble\n\t\t\t\t? (reactionsHeight + 2 * st::mediaInBubbleSkip)\n\t\t\t\t: _bottomInfo.height();\n\t\t\tconst auto heightMargins = QMargins(0, 0, 0, infoHeight);\n\t\t\t_viewButton->draw(\n\t\t\t\tp,\n\t\t\t\t_viewButton->countRect(belowInfo\n\t\t\t\t\t? inner\n\t\t\t\t\t: inner - heightMargins),\n\t\t\t\tcontext);\n\t\t\tif (belowInfo) {\n\t\t\t\tinner.setHeight(inner.height() - _viewButton->height());\n\t\t\t}\n\t\t\ttrect.setHeight(trect.height() - _viewButton->height());\n\t\t\tif (reactionsInBubble) {\n\t\t\t\ttrect.setHeight(trect.height() - st::mediaInBubbleSkip + st::msgPadding.bottom());\n\t\t\t} else if (mediaDisplayed) {\n\t\t\t\ttrect.setHeight(trect.height() - st::mediaInBubbleSkip);\n\t\t\t}\n\t\t}\n\n\t\tif (mediaOnBottom) {\n\t\t\ttrect.setHeight(trect.height() + st::msgPadding.bottom());\n\t\t}\n\t\tif (mediaOnTop) {\n\t\t\ttrect.setY(trect.y() - st::msgPadding.top());\n\t\t} else {\n\t\t\tpaintFromName(p, trect, context);\n\t\t\tpaintTopicButton(p, trect, context);\n\t\t\tpaintForwardedInfo(p, trect, context);\n\t\t\tpaintViaBotIdInfo(p, trect, context);\n\t\t\tpaintReplyInfo(p, trect, context);\n\t\t\tpaintSummaryHeaderInfo(p, trect, context);\n\t\t}\n\t\tif (entry) {\n\t\t\ttrect.setHeight(trect.height() - entry->height());\n\t\t}\n\t\tif (check) {\n\t\t\ttrect.setHeight(trect.height() - check->height() - st::mediaInBubbleSkip);\n\t\t}\n\t\tif (displayInfo) {\n\t\t\ttrect.setHeight(trect.height()\n\t\t\t\t- (_bottomInfo.height() - st::msgDateFont->height));\n\t\t}\n\t\tauto textSelection = context.selection;\n\t\tauto highlightRange = context.highlight.range;\n\t\tconst auto mediaHeight = mediaDisplayed ? media->height() : 0;\n\t\tconst auto paintMedia = [&](int top) {\n\t\t\tif (!mediaDisplayed) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto mediaSelection = _invertMedia\n\t\t\t\t? context.selection\n\t\t\t\t: skipTextSelection(context.selection);\n\t\t\tconst auto maybeMediaHighlight = context.highlightPathCache\n\t\t\t\t&& context.highlightPathCache->isEmpty();\n\t\t\tauto mediaPosition = QPoint(inner.left(), top);\n\t\t\t_lastMediaPosition = mediaPosition;\n\t\t\tp.translate(mediaPosition);\n\t\t\tmedia->draw(p, context.translated(\n\t\t\t\t-mediaPosition\n\t\t\t).withSelection(mediaSelection));\n\t\t\tif (context.reactionInfo && !displayInfo && !_reactions) {\n\t\t\t\tconst auto add = QPoint(0, mediaHeight);\n\t\t\t\tcontext.reactionInfo->position = mediaPosition + add;\n\t\t\t\tif (context.reactionInfo->effectPaint) {\n\t\t\t\t\tcontext.reactionInfo->effectOffset -= add;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (maybeMediaHighlight\n\t\t\t\t&& !context.highlightPathCache->isEmpty()) {\n\t\t\t\tcontext.highlightPathCache->translate(mediaPosition);\n\t\t\t}\n\t\t\tp.translate(-mediaPosition);\n\t\t};\n\t\tif (mediaDisplayed && _invertMedia) {\n\t\t\tif (!mediaOnTop) {\n\t\t\t\ttrect.setY(trect.y() + st::mediaInBubbleSkip);\n\t\t\t}\n\t\t\tpaintMedia(trect.y());\n\t\t\ttrect.setY(trect.y()\n\t\t\t\t+ mediaHeight\n\t\t\t\t+ (mediaOnBottom ? 0 : st::mediaInBubbleSkip));\n\t\t\ttextSelection = media->skipSelection(textSelection);\n\t\t\thighlightRange = media->skipSelection(highlightRange);\n\t\t}\n\t\tconst auto drawText = context.skipDrawingParts\n\t\t\t!= PaintContext::SkipDrawingParts::Content;\n\t\tconst auto drawOnlyText = drawText\n\t\t\t&& (context.skipDrawingParts\n\t\t\t\t!= PaintContext::SkipDrawingParts::None);\n\t\tif (drawOnlyText) {\n\t\t\tp.save();\n\t\t\tp.setClipping(false);\n\t\t}\n\t\tif (drawText) {\n\t\t\tauto copy = context;\n\t\t\tcopy.selection = textSelection;\n\t\t\tcopy.highlight.range = highlightRange;\n\t\t\tpaintText(p, trect, copy);\n\t\t}\n\t\tif (drawOnlyText) {\n\t\t\tp.restore();\n\t\t}\n\t\tif (mediaDisplayed && !_invertMedia) {\n\t\t\tpaintMedia(trect.y() + trect.height() - mediaHeight);\n\t\t\tif (context.reactionInfo && !displayInfo && !_reactions) {\n\t\t\t\tcontext.reactionInfo->position\n\t\t\t\t\t= QPoint(inner.left(), trect.y() + trect.height());\n\t\t\t\tif (context.reactionInfo->effectPaint) {\n\t\t\t\t\tcontext.reactionInfo->effectOffset -= QPoint(0, mediaHeight);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (check) {\n\t\t\tauto checkLeft = inner.left();\n\t\t\tauto checkTop = trect.y() + trect.height() + st::mediaInBubbleSkip;\n\t\t\tp.translate(checkLeft, checkTop);\n\t\t\tauto checkContext = context.translated(checkLeft, -checkTop);\n\t\t\tcheckContext.selection = skipTextSelection(context.selection);\n\t\t\tif (mediaDisplayed) {\n\t\t\t\tcheckContext.selection = media->skipSelection(\n\t\t\t\t\tcheckContext.selection);\n\t\t\t}\n\t\t\tcheck->draw(p, checkContext);\n\t\t\tp.translate(-checkLeft, -checkTop);\n\t\t}\n\t\tif (entry) {\n\t\t\tauto entryLeft = inner.left();\n\t\t\tauto entryTop = trect.y() + trect.height();\n\t\t\tp.translate(entryLeft, entryTop);\n\t\t\tauto entryContext = context.translated(-entryLeft, -entryTop);\n\t\t\tentryContext.selection = skipTextSelection(context.selection);\n\t\t\tif (mediaDisplayed) {\n\t\t\t\tentryContext.selection = media->skipSelection(\n\t\t\t\t\tentryContext.selection);\n\t\t\t}\n\t\t\tentry->draw(p, entryContext);\n\t\t\tp.translate(-entryLeft, -entryTop);\n\t\t}\n\t\tif (displayInfo) {\n\t\t\tconst auto bottomSelected = context.selected()\n\t\t\t\t|| (!mediaSelectionIntervals.empty()\n\t\t\t\t\t&& (mediaSelectionIntervals.back().top\n\t\t\t\t\t\t+ mediaSelectionIntervals.back().height\n\t\t\t\t\t\t>= inner.y() + inner.height()));\n\t\t\tdrawInfo(\n\t\t\t\tp,\n\t\t\t\tcontext.withSelection(\n\t\t\t\t\tbottomSelected ? FullSelection : TextSelection()),\n\t\t\t\tinner.left() + inner.width(),\n\t\t\t\tinner.top() + inner.height(),\n\t\t\t\t2 * inner.left() + inner.width(),\n\t\t\t\tInfoDisplayType::Default);\n\t\t\tif (context.reactionInfo && !_reactions) {\n\t\t\t\tconst auto add = QPoint(0, inner.top() + inner.height());\n\t\t\t\tcontext.reactionInfo->position = add;\n\t\t\t\tif (context.reactionInfo->effectPaint) {\n\t\t\t\t\tcontext.reactionInfo->effectOffset -= add;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_comments) {\n\t\t\t\tconst auto o = p.opacity();\n\t\t\t\tp.setOpacity(0.3);\n\t\t\t\tp.fillRect(g.left(), g.top() + g.height() - st::historyCommentsButtonHeight - st::lineWidth, g.width(), st::lineWidth, stm->msgDateFg);\n\t\t\t\tp.setOpacity(o);\n\t\t\t}\n\t\t}\n\t\tensureSummarizeButton();\n\t\tif (const auto size = rightActionSize(); size || _summarize) {\n\t\t\tconst auto rightActionWidth = size\n\t\t\t\t? size->width()\n\t\t\t\t: _summarize->size().width();\n\t\t\tconst auto fastShareSkip = size\n\t\t\t\t? std::clamp(\n\t\t\t\t\t(g.height() - size->height()) / 2,\n\t\t\t\t\t0,\n\t\t\t\t\tst::historyFastShareBottom)\n\t\t\t\t: st::historyFastShareBottom;\n\t\t\tconst auto fastShareLeft = hasRightLayout()\n\t\t\t\t? (g.left()\n\t\t\t\t\t- (_summarize ? 0 : rightActionWidth)\n\t\t\t\t\t- st::historyFastShareLeft)\n\t\t\t\t: (g.left() + g.width() + st::historyFastShareLeft);\n\t\t\tconst auto fastShareTop = g.top() + (data()->isSponsored()\n\t\t\t\t? fastShareSkip\n\t\t\t\t: g.height() - fastShareSkip - (size ? size->height() : 0));\n\t\t\tif (size) {\n\t\t\t\tconst auto o = p.opacity();\n\t\t\t\tif (selectionModeResult.progress > 0) {\n\t\t\t\t\tp.setOpacity(1. - selectionModeResult.progress);\n\t\t\t\t}\n\t\t\t\tdrawRightAction(\n\t\t\t\t\tp,\n\t\t\t\t\tcontext,\n\t\t\t\t\tfastShareLeft,\n\t\t\t\t\tfastShareTop,\n\t\t\t\t\twidth());\n\t\t\t\tif (selectionModeResult.progress > 0) {\n\t\t\t\t\tp.setOpacity(o);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_summarize) {\n\t\t\t\tpaintSummarize(\n\t\t\t\t\tp,\n\t\t\t\t\tfastShareLeft,\n\t\t\t\t\tfastShareTop,\n\t\t\t\t\t!context.outbg,\n\t\t\t\t\tcontext,\n\t\t\t\t\tg);\n\t\t\t}\n\t\t}\n\n\t\tif (media) {\n\t\t\tmedia->paintBubbleFireworks(p, g, context.now);\n\t\t}\n\t} else if (media && media->isDisplayed()) {\n\t\tp.translate(g.topLeft());\n\t\tmedia->draw(p, context.translated(\n\t\t\t-g.topLeft()\n\t\t).withSelection(skipTextSelection(context.selection)));\n\t\tif (context.reactionInfo && !_reactions) {\n\t\t\tconst auto add = QPoint(0, g.height());\n\t\t\tcontext.reactionInfo->position = g.topLeft() + add;\n\t\t\tif (context.reactionInfo->effectPaint) {\n\t\t\t\tcontext.reactionInfo->effectOffset -= add;\n\t\t\t}\n\t\t}\n\t\tp.translate(-g.topLeft());\n\t}\n\n\tp.restoreTextPalette();\n\n\tif (context.highlightPathCache\n\t\t&& !context.highlightPathCache->isEmpty()) {\n\t\tconst auto alpha = int(0.25\n\t\t\t* context.highlight.collapsion\n\t\t\t* context.highlight.opacity\n\t\t\t* 255);\n\t\tif (alpha > 0) {\n\t\t\tcontext.highlightPathCache->setFillRule(Qt::WindingFill);\n\t\t\tauto color = context.messageStyle()->textPalette.linkFg->c;\n\t\t\tcolor.setAlpha(alpha);\n\t\t\tp.fillPath(*context.highlightPathCache, color);\n\t\t}\n\t}\n\n\tif (roll) {\n\t\tp.restore();\n\t}\n\n\tif (const auto reply = Get<Reply>()) {\n\t\tif (const auto replyData = item->Get<HistoryMessageReply>()) {\n\t\t\tif (reply->isNameUpdated(this, replyData)) {\n\t\t\t\tconst_cast<Message*>(this)->setPendingResize();\n\t\t\t}\n\t\t}\n\t}\n\tif (hasGesture) {\n\t\tp.translate(-context.gestureHorizontal.translation, 0);\n\n\t\tconstexpr auto kShiftRatio = 1.5;\n\t\tconstexpr auto kBouncePart = 0.25;\n\t\tconstexpr auto kMaxHeightRatio = 3.5;\n\t\tconstexpr auto kStrokeWidth = 2.;\n\t\tconstexpr auto kWaveWidth = 10.;\n\t\tconst auto isLeftSize = !context.outbg\n\t\t\t|| (delegate()->elementChatMode() == ElementChatMode::Wide);\n\t\tconst auto ratio = std::min(context.gestureHorizontal.ratio, 1.);\n\t\tconst auto reachRatio = context.gestureHorizontal.reachRatio;\n\t\tconst auto size = st::historyFastShareSize;\n\t\tconst auto outerWidth = st::historySwipeIconSkip\n\t\t\t+ (isLeftSize ? rect::right(g) : width())\n\t\t\t+ ((g.height() < size * kMaxHeightRatio)\n\t\t\t\t? rightActionSize().value_or(QSize()).width()\n\t\t\t\t: 0);\n\t\tconst auto shift = std::min(\n\t\t\t(size * kShiftRatio * context.gestureHorizontal.ratio),\n\t\t\t-1. * context.gestureHorizontal.translation\n\t\t) + (st::historySwipeIconSkip * ratio * (isLeftSize ? .7 : 1.));\n\t\tconst auto rect = QRectF(\n\t\t\touterWidth - shift,\n\t\t\tg.y() + (g.height() - size) / 2,\n\t\t\tsize,\n\t\t\tsize);\n\t\tconst auto center = rect::center(rect);\n\t\tconst auto spanAngle = ratio * arc::kFullLength;\n\t\tconst auto strokeWidth = style::ConvertFloatScale(kStrokeWidth);\n\n\t\tconst auto reachScale = std::clamp(\n\t\t\t(reachRatio > kBouncePart)\n\t\t\t\t? (kBouncePart * 2 - reachRatio)\n\t\t\t\t: reachRatio,\n\t\t\t0.,\n\t\t\t1.);\n\t\tauto pen = Window::Theme::IsNightMode()\n\t\t\t? QPen(anim::with_alpha(context.st->msgServiceFg()->c, 0.3))\n\t\t\t: QPen(context.st->msgServiceBg());\n\t\tpen.setWidthF(strokeWidth - (1. * (reachScale / kBouncePart)));\n\t\tconst auto arcRect = rect - Margins(strokeWidth);\n\t\tp.save();\n\t\t{\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(context.st->msgServiceBg());\n\t\t\tp.setOpacity(ratio);\n\t\t\tp.translate(center);\n\t\t\tif (reachScale) {\n\t\t\t\tp.scale(-(1. + 1. * reachScale), (1. + 1. * reachScale));\n\t\t\t} else {\n\t\t\t\tp.scale(-1., 1.);\n\t\t\t}\n\t\t\tp.translate(-center);\n\t\t\t// All the next draws are mirrored.\n\t\t\tp.drawEllipse(rect);\n\t\t\tcontext.st->historyFastShareIcon().paintInCenter(p, rect);\n\t\t\tp.setPen(pen);\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\tp.drawArc(arcRect, arc::kQuarterLength, spanAngle);\n\t\t\t// p.drawArc(arcRect, arc::kQuarterLength, spanAngle);\n\t\t\tif (reachRatio) {\n\t\t\t\tconst auto w = style::ConvertFloatScale(kWaveWidth);\n\t\t\t\tp.setOpacity(ratio - reachRatio);\n\t\t\t\tp.drawArc(\n\t\t\t\t\tarcRect + Margins(reachRatio * reachRatio * w),\n\t\t\t\t\tarc::kQuarterLength,\n\t\t\t\t\tspanAngle);\n\t\t\t}\n\t\t}\n\t\tp.restore();\n\t}\n\tif (selectionTranslation) {\n\t\tp.translate(-selectionTranslation, 0);\n\t}\n\tif (selectionModeResult.progress) {\n\t\tif (!context.skipSelectionCheck) {\n\t\t\tconst auto progress = selectionModeResult.progress;\n\t\t\tif (progress <= 1.) {\n\t\t\t\tif (context.selected()) {\n\t\t\t\t\tif (!_selectionRoundCheckbox) {\n\t\t\t\t\t\t_selectionRoundCheckbox\n\t\t\t\t\t\t\t= std::make_unique<Ui::RoundCheckbox>(\n\t\t\t\t\t\t\t\tst::msgSelectionCheck,\n\t\t\t\t\t\t\t\t[this] { repaint(); });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (_selectionRoundCheckbox) {\n\t\t\t\t\t_selectionRoundCheckbox->setChecked(\n\t\t\t\t\t\tcontext.selected(),\n\t\t\t\t\t\tanim::type::normal);\n\t\t\t\t}\n\t\t\t\tconst auto o = ScopedPainterOpacity(p, progress);\n\t\t\t\tconst auto &st = st::msgSelectionCheck;\n\t\t\t\tconst auto right = (delegate()->elementChatMode()\n\t\t\t\t\t== ElementChatMode::Wide)\n\t\t\t\t\t? std::min(\n\t\t\t\t\t\tint(_bubbleWidthLimit\n\t\t\t\t\t\t\t+ st::msgPhotoSkip\n\t\t\t\t\t\t\t+ st::msgSelectionOffset\n\t\t\t\t\t\t\t+ st::msgPadding.left()\n\t\t\t\t\t\t\t+ st.size),\n\t\t\t\t\t\twidth())\n\t\t\t\t\t: width();\n\t\t\t\tconst auto pos = QPoint(\n\t\t\t\t\t(right\n\t\t\t\t\t\t- (st::msgSelectionOffset * progress - st.size) / 2\n\t\t\t\t\t\t- st::msgPadding.right() / 2\n\t\t\t\t\t\t- st.size\n\t\t\t\t\t\t- st::historyScroll.deltax),\n\t\t\t\t\trect::bottom(g) - st.size - st::msgSelectionBottomSkip);\n\t\t\t\t{\n\t\t\t\t\tp.setPen(QPen(st.border, st.width));\n\t\t\t\t\tp.setBrush(context.st->msgServiceBg());\n\t\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\t\tp.drawEllipse(QRect(pos, Size(st.size)));\n\t\t\t\t}\n\t\t\t\tif (_selectionRoundCheckbox) {\n\t\t\t\t\t_selectionRoundCheckbox->paint(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tpos.x(),\n\t\t\t\t\t\tpos.y(),\n\t\t\t\t\t\twidth());\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t_selectionRoundCheckbox = nullptr;\n\t\t\t}\n\t\t}\n\t} else if (!context.skipSelectionCheck) {\n\t\t_selectionRoundCheckbox = nullptr;\n\t}\n}\n\nvoid Message::paintCommentsButton(\n\t\tPainter &p,\n\t\tQRect &g,\n\t\tconst PaintContext &context) const {\n\tif (!data()->repliesAreComments() && !data()->externalReply()) {\n\t\treturn;\n\t}\n\tif (!_comments) {\n\t\t_comments = std::make_unique<CommentsButton>();\n\t\thistory()->owner().registerHeavyViewPart(const_cast<Message*>(this));\n\t}\n\tconst auto stm = context.messageStyle();\n\tconst auto views = data()->Get<HistoryMessageViews>();\n\n\tg.setHeight(g.height() - st::historyCommentsButtonHeight);\n\tconst auto top = g.top() + g.height();\n\tauto left = g.left();\n\tauto width = g.width();\n\n\tif (_comments->ripple) {\n\t\tp.setOpacity(st::historyPollRippleOpacity);\n\t\tconst auto colorOverride = &stm->msgWaveformInactive->c;\n\t\t_comments->ripple->paint(\n\t\t\tp,\n\t\t\tleft - _comments->rippleShift,\n\t\t\ttop,\n\t\t\twidth,\n\t\t\tcolorOverride);\n\t\tif (_comments->ripple->empty()) {\n\t\t\t_comments->ripple.reset();\n\t\t}\n\t\tp.setOpacity(1.);\n\t}\n\n\tleft += st::historyCommentsSkipLeft;\n\twidth -= st::historyCommentsSkipLeft\n\t\t+ st::historyCommentsSkipRight;\n\n\tconst auto &open = stm->historyCommentsOpen;\n\topen.paint(p,\n\t\tleft + width - open.width(),\n\t\ttop + (st::historyCommentsButtonHeight - open.height()) / 2,\n\t\twidth);\n\n\tif (!views || views->recentRepliers.empty()) {\n\t\tconst auto &icon = stm->historyComments;\n\t\ticon.paint(\n\t\t\tp,\n\t\t\tleft,\n\t\t\ttop + (st::historyCommentsButtonHeight - icon.height()) / 2,\n\t\t\twidth);\n\t\tleft += icon.width();\n\t} else {\n\t\tauto &list = _comments->userpics;\n\t\tconst auto limit = HistoryMessageViews::kMaxRecentRepliers;\n\t\tconst auto count = std::min(int(views->recentRepliers.size()), limit);\n\t\tconst auto single = st::historyCommentsUserpics.size;\n\t\tconst auto shift = st::historyCommentsUserpics.shift;\n\t\tconst auto regenerate = [&] {\n\t\t\tif (list.size() != count) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tauto &entry = list[i];\n\t\t\t\tconst auto peer = entry.peer;\n\t\t\t\tauto &view = entry.view;\n\t\t\t\tconst auto wasView = view.cloud.get();\n\t\t\t\tif (views->recentRepliers[i] != peer->id\n\t\t\t\t\t|| peer->userpicUniqueKey(view) != entry.uniqueKey\n\t\t\t\t\t|| view.cloud.get() != wasView) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t}();\n\t\tif (regenerate) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tconst auto peerId = views->recentRepliers[i];\n\t\t\t\tif (i == list.size()) {\n\t\t\t\t\tlist.push_back(UserpicInRow{\n\t\t\t\t\t\thistory()->owner().peer(peerId)\n\t\t\t\t\t});\n\t\t\t\t} else if (list[i].peer->id != peerId) {\n\t\t\t\t\tlist[i].peer = history()->owner().peer(peerId);\n\t\t\t\t}\n\t\t\t}\n\t\t\twhile (list.size() > count) {\n\t\t\t\tlist.pop_back();\n\t\t\t}\n\t\t\tGenerateUserpicsInRow(\n\t\t\t\t_comments->cachedUserpics,\n\t\t\t\tlist,\n\t\t\t\tst::historyCommentsUserpics,\n\t\t\t\tlimit);\n\t\t}\n\t\tp.drawImage(\n\t\t\tleft,\n\t\t\ttop + (st::historyCommentsButtonHeight - single) / 2,\n\t\t\t_comments->cachedUserpics);\n\t\tleft += single + (count - 1) * (single - shift);\n\t}\n\n\tleft += st::historyCommentsSkipText;\n\tp.setPen(stm->msgFileThumbLinkFg);\n\tp.setFont(st::semiboldFont);\n\n\tconst auto textTop = top + (st::historyCommentsButtonHeight - st::semiboldFont->height) / 2;\n\tp.drawTextLeft(\n\t\tleft,\n\t\ttextTop,\n\t\twidth,\n\t\tviews ? views->replies.text : tr::lng_replies_view_original(tr::now),\n\t\tviews ? views->replies.textWidth : -1);\n\n\tif (views && data()->areCommentsUnread()) {\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(stm->msgFileBg);\n\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(style::rtlrect(left + views->replies.textWidth + st::mediaUnreadSkip, textTop + st::mediaUnreadTop, st::mediaUnreadSize, st::mediaUnreadSize, width));\n\t\t}\n\t}\n}\n\nvoid Message::paintFromName(\n\t\tPainter &p,\n\t\tQRect &trect,\n\t\tconst PaintContext &context) const {\n\tconst auto item = data();\n\tif (!displayFromName()) {\n\t\treturn;\n\t}\n\tconst auto badgeWidth = rightBadgeWidth();\n\tauto availableLeft = trect.left();\n\tauto availableWidth = trect.width();\n\tif (badgeWidth) {\n\t\tavailableWidth -= st::msgPadding.right() + badgeWidth;\n\t}\n\n\tconst auto stm = context.messageStyle();\n\tconst auto from = item->displayFrom();\n\tconst auto info = from ? nullptr : item->displayHiddenSenderInfo();\n\tAssert(from || info);\n\tconst auto nameFg = FromNameFg(\n\t\tcontext,\n\t\tcolorIndex(),\n\t\tcolorCollectible());\n\tconst auto nameText = [&] {\n\t\tif (from) {\n\t\t\tvalidateFromNameText(from);\n\t\t\treturn static_cast<const Ui::Text::String*>(&_fromName);\n\t\t}\n\t\treturn &info->nameText();\n\t}();\n\tconst auto statusWidth = _fromNameStatus\n\t\t? st::dialogsPremiumIcon.icon.width()\n\t\t: 0;\n\tif (statusWidth && availableWidth > statusWidth) {\n\t\tconst auto x = availableLeft\n\t\t\t+ std::min(availableWidth - statusWidth, nameText->maxWidth());\n\t\tconst auto y = trect.top();\n\t\tauto color = nameFg;\n\t\tcolor.setAlpha(115);\n\t\tconst auto id = from ? from->emojiStatusId() : EmojiStatusId();\n\t\tif (_fromNameStatus->id != id) {\n\t\t\tconst auto that = const_cast<Message*>(this);\n\t\t\t_fromNameStatus->custom = id\n\t\t\t\t? std::make_unique<Ui::Text::LimitedLoopsEmoji>(\n\t\t\t\t\thistory()->owner().customEmojiManager().create(\n\t\t\t\t\t\tData::EmojiStatusCustomId(id),\n\t\t\t\t\t\t[=] { that->customEmojiRepaint(); }),\n\t\t\t\t\tkPlayStatusLimit)\n\t\t\t\t: nullptr;\n\t\t\tif (id && !_fromNameStatus->id) {\n\t\t\t\thistory()->owner().registerHeavyViewPart(that);\n\t\t\t} else if (!id && _fromNameStatus->id) {\n\t\t\t\tthat->checkHeavyPart();\n\t\t\t}\n\t\t\t_fromNameStatus->id = id;\n\t\t}\n\t\tif (_fromNameStatus->custom) {\n\t\t\tclearCustomEmojiRepaint();\n\t\t\t_fromNameStatus->custom->paint(p, {\n\t\t\t\t.textColor = color,\n\t\t\t\t.now = context.now,\n\t\t\t\t.position = QPoint(\n\t\t\t\t\tx - 2 * _fromNameStatus->skip,\n\t\t\t\t\ty + _fromNameStatus->skip),\n\t\t\t\t.paused = context.paused || On(PowerSaving::kEmojiStatus),\n\t\t\t});\n\t\t} else {\n\t\t\tst::dialogsPremiumIcon.icon.paint(p, x, y, width(), color);\n\t\t}\n\t\tavailableWidth -= statusWidth;\n\t}\n\tp.setFont(st::msgNameFont);\n\tp.setPen(nameFg);\n\tconst auto nameLinkHandler = fromLink();\n\tconst auto nameWidth = std::min(\n\t\tnameText->maxWidth(),\n\t\tavailableWidth);\n\tpaintLinkRipple(\n\t\tp,\n\t\tnameLinkHandler,\n\t\tQRect(availableLeft, trect.top(), nameWidth, st::msgNameFont->height),\n\t\ttrect.topLeft());\n\tnameText->draw(p, {\n\t\t.position = { availableLeft, trect.top() },\n\t\t.availableWidth = availableWidth,\n\t\t.elisionLines = 1,\n\t});\n\tconst auto skipWidth = nameText->maxWidth()\n\t\t+ (_fromNameStatus\n\t\t\t? (st::dialogsPremiumIcon.icon.width()\n\t\t\t\t+ st::msgServiceFont->spacew)\n\t\t\t: 0)\n\t\t+ st::msgServiceFont->spacew;\n\tavailableLeft += skipWidth;\n\tavailableWidth -= skipWidth;\n\n\tauto via = item->Get<HistoryMessageVia>();\n\tif (via && !displayForwardedFrom() && availableWidth > 0) {\n\t\tp.setPen(stm->msgServiceFg);\n\t\tpaintLinkRipple(\n\t\t\tp,\n\t\t\tvia->link,\n\t\t\tQRect(availableLeft, trect.top(), via->width, st::msgServiceFont->height),\n\t\t\ttrect.topLeft());\n\t\tp.drawText(availableLeft, trect.top() + st::msgServiceFont->ascent, via->text);\n\t\tauto skipWidth = via->width + st::msgServiceFont->spacew;\n\t\tavailableLeft += skipWidth;\n\t\tavailableWidth -= skipWidth;\n\t}\n\tif (badgeWidth) {\n\t\tp.setPen(stm->msgDateFg);\n\t\tif (const auto badge = Get<RightBadge>()) {\n\t\t\tconst auto badgeColor = (badge->role == BadgeRole::Creator)\n\t\t\t\t? st::rankOwnerFg->c\n\t\t\t\t: (badge->role == BadgeRole::Admin)\n\t\t\t\t? st::rankAdminFg->c\n\t\t\t\t: st::rankUserFg->c;\n\t\t\tconst auto badgeLeft = trect.left()\n\t\t\t\t+ trect.width()\n\t\t\t\t- badge->width;\n\t\t\tif (badge->role != BadgeRole::User) {\n\t\t\t\tauto bgColor = badgeColor;\n\t\t\t\tbgColor.setAlphaF(0.15);\n\t\t\t\tconst auto pill = ComputeBadgePillGeometry(badge);\n\t\t\t\tconst auto &padding = st::msgTagBadgePadding;\n\t\t\t\tconst auto badgeTop = trect.top()\n\t\t\t\t\t+ (st::msgNameFont->height - pill.height) / 2;\n\t\t\t\tconst auto pillRect = QRect(\n\t\t\t\t\tbadgeLeft,\n\t\t\t\t\tbadgeTop,\n\t\t\t\t\tpill.width,\n\t\t\t\t\tpill.height);\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(bgColor);\n\t\t\t\t{\n\t\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\t\tp.drawRoundedRect(\n\t\t\t\t\t\tpillRect,\n\t\t\t\t\t\tpill.height / 2.,\n\t\t\t\t\t\tpill.height / 2.);\n\t\t\t\t}\n\t\t\t\tif (badge->ripple) {\n\t\t\t\t\tauto rippleColor = badgeColor;\n\t\t\t\t\trippleColor.setAlphaF(0.1);\n\t\t\t\t\tbadge->ripple->paint(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tbadgeLeft,\n\t\t\t\t\t\tbadgeTop,\n\t\t\t\t\t\twidth(),\n\t\t\t\t\t\t&rippleColor);\n\t\t\t\t\tif (badge->ripple->empty()) {\n\t\t\t\t\t\tbadge->ripple.reset();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tp.setPen(badgeColor);\n\t\t\t\tbadge->tag.draw(p, {\n\t\t\t\t\t.position = QPoint(\n\t\t\t\t\t\tbadgeLeft + (pill.width - pill.textWidth) / 2,\n\t\t\t\t\t\tbadgeTop + padding.top()),\n\t\t\t\t\t.availableWidth = pill.textWidth,\n\t\t\t\t\t.now = context.now,\n\t\t\t\t});\n\t\t\t} else if (!badge->tag.isEmpty()) {\n\t\t\t\tif (badge->ripple) {\n\t\t\t\t\tconst auto pill = ComputeBadgePillGeometry(badge);\n\t\t\t\t\tconst auto &padding = st::msgTagBadgePadding;\n\t\t\t\t\tconst auto pillLeft = badgeLeft\n\t\t\t\t\t\t- (pill.width - pill.textWidth) / 2;\n\t\t\t\t\tconst auto pillTop = trect.top() - padding.top();\n\t\t\t\t\tauto rippleColor = badgeColor;\n\t\t\t\t\trippleColor.setAlphaF(0.1);\n\t\t\t\t\tbadge->ripple->paint(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\tpillLeft,\n\t\t\t\t\t\tpillTop,\n\t\t\t\t\t\twidth(),\n\t\t\t\t\t\t&rippleColor);\n\t\t\t\t\tif (badge->ripple->empty()) {\n\t\t\t\t\t\tbadge->ripple.reset();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tp.setPen(st::rankUserFg);\n\t\t\t\tbadge->tag.draw(p, {\n\t\t\t\t\t.position = QPoint(badgeLeft, trect.top()),\n\t\t\t\t\t.availableWidth = badge->tag.maxWidth(),\n\t\t\t\t\t.now = context.now,\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (!badge->boosts.isEmpty()) {\n\t\t\t\tconst auto boostWidth = badge->boosts.maxWidth();\n\t\t\t\tp.setPen(badgeColor);\n\t\t\t\tbadge->boosts.draw(p, {\n\t\t\t\t\t.position = QPoint(\n\t\t\t\t\t\ttrect.left() + trect.width() - boostWidth,\n\t\t\t\t\t\ttrect.top()),\n\t\t\t\t\t.availableWidth = boostWidth,\n\t\t\t\t\t.now = context.now,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\ttrect.setY(trect.y() + st::msgNameFont->height);\n}\n\nvoid Message::paintTopicButton(\n\t\tPainter &p,\n\t\tQRect &trect,\n\t\tconst PaintContext &context) const {\n\tconst auto button = displayedTopicButton();\n\tif (!button) {\n\t\treturn;\n\t}\n\ttrect.setTop(trect.top() + st::topicButtonSkip);\n\tconst auto padding = st::topicButtonPadding;\n\tconst auto availableWidth = trect.width();\n\tconst auto height = padding.top()\n\t\t+ st::msgNameFont->height\n\t\t+ padding.bottom();\n\tconst auto width = std::max(\n\t\tstd::min(\n\t\t\tavailableWidth,\n\t\t\t(padding.left()\n\t\t\t\t+ button->name.maxWidth()\n\t\t\t\t+ st::topicButtonArrowSkip\n\t\t\t\t+ padding.right())),\n\t\theight);\n\tconst auto rect = QRect(trect.x(), trect.y(), width, height);\n\n\tconst auto stm = context.messageStyle();\n\tconst auto skip = padding.right() + st::topicButtonArrowSkip;\n\tauto color = stm->msgServiceFg->c;\n\tcolor.setAlpha(color.alpha() / 8);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(color);\n\t{\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.drawRoundedRect(rect, height / 2, height / 2);\n\t}\n\tif (button->ripple) {\n\t\tbutton->ripple->paint(\n\t\t\tp,\n\t\t\trect.x(),\n\t\t\trect.y(),\n\t\t\tthis->width(),\n\t\t\t&color);\n\t\tif (button->ripple->empty()) {\n\t\t\tbutton->ripple.reset();\n\t\t}\n\t}\n\tclearCustomEmojiRepaint();\n\tp.setPen(stm->msgServiceFg);\n\tp.setTextPalette(stm->fwdTextPalette);\n\tbutton->name.drawElided(\n\t\tp,\n\t\ttrect.x() + padding.left(),\n\t\ttrect.y() + padding.top(),\n\t\twidth - padding.left() - skip);\n\n\tconst auto &icon = st::topicButtonArrow;\n\ticon.paint(\n\t\tp,\n\t\trect.x() + rect.width() - skip + st::topicButtonArrowPosition.x(),\n\t\trect.y() + padding.top() + st::topicButtonArrowPosition.y(),\n\t\tthis->width(),\n\t\tstm->msgServiceFg->c);\n\n\ttrect.setY(trect.y() + height + st::topicButtonSkip);\n}\n\nvoid Message::paintForwardedInfo(\n\t\tPainter &p,\n\t\tQRect &trect,\n\t\tconst PaintContext &context) const {\n\tif (displayForwardedFrom()) {\n\t\tconst auto item = data();\n\t\tconst auto st = context.st;\n\t\tconst auto stm = context.messageStyle();\n\t\tconst auto forwarded = item->Get<HistoryMessageForwarded>();\n\n\t\tconst auto &serviceFont = st::msgServiceFont;\n\t\tconst auto skip1 = forwarded->psaType.isEmpty()\n\t\t\t? 0\n\t\t\t: st::historyPsaIconSkip1;\n\t\tconst auto skip2 = forwarded->psaType.isEmpty()\n\t\t\t? 0\n\t\t\t: st::historyPsaIconSkip2;\n\t\tconst auto fits = (forwarded->text.maxWidth() + skip1 <= trect.width());\n\t\tconst auto skip = fits ? skip1 : skip2;\n\t\tconst auto useWidth = trect.width() - skip;\n\t\tconst auto countedHeight = forwarded->text.countHeight(useWidth);\n\t\tconst auto breakEverywhere = (countedHeight > 2 * serviceFont->height);\n\t\tp.setPen(!forwarded->psaType.isEmpty()\n\t\t\t? st->boxTextFgGood()\n\t\t\t: stm->msgServiceFg);\n\t\tp.setFont(serviceFont);\n\t\tconst auto &fwdPalette = !forwarded->psaType.isEmpty()\n\t\t\t? st->historyPsaForwardPalette()\n\t\t\t: stm->fwdTextPalette;\n\t\tconst auto rippleLinkRange = (_linkRipple && _linkRipple->link)\n\t\t\t? forwarded->text.linkRangeFor(_linkRipple->link)\n\t\t\t: TextSelection();\n\t\tconst auto rippleBelongsHere = !rippleLinkRange.empty();\n\t\tif (_linkRipple\n\t\t\t&& _linkRipple->ripple\n\t\t\t&& _linkRipple->cachedWidth != useWidth\n\t\t\t&& rippleBelongsHere) {\n\t\t\t_linkRipple = nullptr;\n\t\t}\n\t\tif (_linkRipple && _linkRipple->ripple && rippleBelongsHere) {\n\t\t\tauto color = p.pen().color();\n\t\t\tcolor.setAlphaF(0.1);\n\t\t\t_linkRipple->ripple->paint(\n\t\t\t\tp,\n\t\t\t\ttrect.x() + _linkRipple->maskOffset.x(),\n\t\t\t\ttrect.y() + _linkRipple->maskOffset.y(),\n\t\t\t\twidth(),\n\t\t\t\t&color);\n\t\t\tif (_linkRipple->ripple->empty()) {\n\t\t\t\t_linkRipple = nullptr;\n\t\t\t}\n\t\t}\n\t\tconst auto needRippleMask = _linkRipple\n\t\t\t&& _linkRipple->link\n\t\t\t&& !_linkRipple->ripple\n\t\t\t&& rippleBelongsHere;\n\t\tauto highlightPath = QPainterPath();\n\t\tauto highlightRequest = Ui::Text::HighlightInfoRequest{\n\t\t\t.range = rippleLinkRange,\n\t\t\t.outPath = &highlightPath,\n\t\t};\n\t\tforwarded->text.draw(p, {\n\t\t\t.position = { trect.x(), trect.y() },\n\t\t\t.availableWidth = useWidth,\n\t\t\t.palette = &fwdPalette,\n\t\t\t.paused = p.inactive(),\n\t\t\t.highlight = needRippleMask ? &highlightRequest : nullptr,\n\t\t\t.elisionLines = 2,\n\t\t\t.elisionBreakEverywhere = breakEverywhere,\n\t\t});\n\t\tif (needRippleMask && !highlightPath.isEmpty()) {\n\t\t\tcreateLinkRippleMask(\n\t\t\t\thighlightPath,\n\t\t\t\ttrect.topLeft(),\n\t\t\t\tuseWidth,\n\t\t\t\tst::nameRipplePadding,\n\t\t\t\tst::nameRippleRadius);\n\t\t} else if (needRippleMask) {\n\t\t\t_linkRipple = nullptr;\n\t\t}\n\t\tp.setTextPalette(stm->textPalette);\n\n\t\tif (!forwarded->psaType.isEmpty()) {\n\t\t\tconst auto entry = Get<PsaTooltipState>();\n\t\t\tAssert(entry != nullptr);\n\t\t\tconst auto shown = entry->buttonVisibleAnimation.value(\n\t\t\t\tentry->buttonVisible ? 1. : 0.);\n\t\t\tif (shown > 0) {\n\t\t\t\tconst auto &icon = stm->historyPsaIcon;\n\t\t\t\tconst auto position = fits\n\t\t\t\t\t? st::historyPsaIconPosition1\n\t\t\t\t\t: st::historyPsaIconPosition2;\n\t\t\t\tconst auto x = trect.x() + trect.width() - position.x() - icon.width();\n\t\t\t\tconst auto y = trect.y() + position.y();\n\t\t\t\tif (shown == 1) {\n\t\t\t\t\ticon.paint(p, x, y, trect.width());\n\t\t\t\t} else {\n\t\t\t\t\tp.save();\n\t\t\t\t\tp.translate(x + icon.width() / 2, y + icon.height() / 2);\n\t\t\t\t\tp.scale(shown, shown);\n\t\t\t\t\tp.setOpacity(shown);\n\t\t\t\t\ticon.paint(p, -icon.width() / 2, -icon.height() / 2, width());\n\t\t\t\t\tp.restore();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttrect.setY(trect.y() + ((fits ? 1 : 2) * serviceFont->height));\n\t}\n}\n\nvoid Message::paintReplyInfo(\n\t\tPainter &p,\n\t\tQRect &trect,\n\t\tconst PaintContext &context) const {\n\tif (const auto reply = Get<Reply>()) {\n\t\treply->paint(\n\t\t\tp,\n\t\t\tthis,\n\t\t\tcontext,\n\t\t\ttrect.x(),\n\t\t\ttrect.y(),\n\t\t\ttrect.width(),\n\t\t\ttrue);\n\t\ttrect.setY(trect.y() + reply->height());\n\t}\n}\n\nvoid Message::paintSummaryHeaderInfo(\n\t\tPainter &p,\n\t\tQRect &trect,\n\t\tconst PaintContext &context) const {\n\tif (const auto summaryHeader = Get<SummaryHeader>()) {\n\t\tsummaryHeader->paint(\n\t\t\tp,\n\t\t\tthis,\n\t\t\tcontext,\n\t\t\ttrect.x(),\n\t\t\ttrect.y(),\n\t\t\ttrect.width(),\n\t\t\ttrue);\n\t\ttrect.setY(trect.y() + summaryHeader->height());\n\t}\n}\n\nvoid Message::paintViaBotIdInfo(\n\t\tPainter &p,\n\t\tQRect &trect,\n\t\tconst PaintContext &context) const {\n\tconst auto item = data();\n\tif (!displayFromName() && !displayForwardedFrom()) {\n\t\tif (auto via = item->Get<HistoryMessageVia>()) {\n\t\t\tconst auto stm = context.messageStyle();\n\t\t\tp.setFont(st::msgServiceNameFont);\n\t\t\tp.setPen(stm->msgServiceFg);\n\t\t\tpaintLinkRipple(\n\t\t\t\tp,\n\t\t\t\tvia->link,\n\t\t\t\tQRect(trect.x(), trect.y(), via->width, st::msgServiceNameFont->height),\n\t\t\t\ttrect.topLeft());\n\t\t\tp.drawTextLeft(trect.left(), trect.top(), width(), via->text);\n\t\t\ttrect.setY(trect.y() + st::msgServiceNameFont->height);\n\t\t}\n\t}\n}\n\nvoid Message::paintText(\n\t\tPainter &p,\n\t\tQRect &trect,\n\t\tconst PaintContext &context) const {\n\tif (!hasVisibleText()) {\n\t\treturn;\n\t}\n\tconst auto stm = context.messageStyle();\n\tp.setPen(stm->historyTextFg);\n\tp.setFont(st::msgFont);\n\tif (const auto botTop = Get<FakeBotAboutTop>()) {\n\t\tbotTop->text.drawLeftElided(\n\t\t\tp,\n\t\t\ttrect.x(),\n\t\t\ttrect.y(),\n\t\t\ttrect.width(),\n\t\t\twidth());\n\t\ttrect.setY(trect.y() + botTop->height);\n\t}\n\tif (!context.clip.intersects(trect)\n\t\t&& context.skipDrawingParts == PaintContext::SkipDrawingParts::None\n\t\t&& !context.gestureHorizontal.translation) {\n\t\treturn;\n\t}\n\tprepareCustomEmojiPaint(p, context, text());\n\n\tconst auto rippleLinkRange = (_linkRipple && _linkRipple->link)\n\t\t? text().linkRangeFor(_linkRipple->link)\n\t\t: TextSelection();\n\tconst auto rippleBelongsHere = !rippleLinkRange.empty();\n\tif (_linkRipple\n\t\t&& _linkRipple->ripple\n\t\t&& _linkRipple->cachedWidth != trect.width()\n\t\t&& rippleBelongsHere) {\n\t\t_linkRipple = nullptr;\n\t}\n\tif (_linkRipple && _linkRipple->ripple && rippleBelongsHere) {\n\t\tauto color = stm->textPalette.linkFg->c;\n\t\tcolor.setAlphaF(0.1);\n\t\t_linkRipple->ripple->paint(\n\t\t\tp,\n\t\t\ttrect.x() + _linkRipple->maskOffset.x(),\n\t\t\ttrect.y() + _linkRipple->maskOffset.y(),\n\t\t\twidth(),\n\t\t\t&color);\n\t\tif (_linkRipple->ripple->empty()) {\n\t\t\t_linkRipple = nullptr;\n\t\t}\n\t}\n\tconst auto needRippleMask = _linkRipple\n\t\t&& _linkRipple->link\n\t\t&& !_linkRipple->ripple\n\t\t&& rippleBelongsHere;\n\tauto ripplePath = QPainterPath();\n\tauto rippleRequest = Ui::Text::HighlightInfoRequest{\n\t\t.range = rippleLinkRange,\n\t\t.outPath = &ripplePath,\n\t};\n\n\tconst auto appearing = Get<TextAppearing>();\n\tconst auto appearingClip = appearing && appearing->use;\n\tauto linePostprocess = std::optional<Ui::Text::LinePostprocess>();\n\tif (appearingClip) {\n\t\tconst auto shown = appearing->shownLine;\n\t\tconst auto &line = appearing->lines[shown];\n\n\t\tp.save();\n\t\tp.setClipRect(QRect(\n\t\t\ttrect.x(),\n\t\t\ttrect.y(),\n\t\t\ttrect.width(),\n\t\t\tline.bottom));\n\n\t\tconst auto revealedWidth = appearing->revealedLineWidth;\n\t\tconst auto lineWidth = line.width;\n\t\tconst auto availableWidth = textRealWidth();\n\t\tlinePostprocess.emplace(Ui::Text::LinePostprocess{\n\t\t\t.method = [=](int lineIndex) -> Fn<void(QImage&)> {\n\t\t\t\tif (lineIndex != shown || revealedWidth >= lineWidth) {\n\t\t\t\t\treturn nullptr;\n\t\t\t\t}\n\t\t\t\treturn [=](QImage &cache) {\n\t\t\t\t\tApplyRevealGradient(appearing, cache, availableWidth);\n\t\t\t\t};\n\t\t\t},\n\t\t\t.cache = &appearing->lineCache,\n\t\t});\n\t}\n\n\tconst auto realWidth = textRealWidth();\n\tauto highlightRequest = context.computeHighlightCache();\n\ttext().draw(p, {\n\t\t.position = trect.topLeft(),\n\t\t.availableWidth = realWidth ? realWidth : trect.width(),\n\t\t.palette = &stm->textPalette,\n\t\t.pre = stm->preCache.get(),\n\t\t.blockquote = context.quoteCache(\n\t\t\tcontentColorCollectible(),\n\t\t\tcontentColorIndex()),\n\t\t.colors = context.st->highlightColors(),\n\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t.now = context.now,\n\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t.selection = context.selection,\n\t\t.highlight = needRippleMask\n\t\t\t? &rippleRequest\n\t\t\t: (highlightRequest ? &*highlightRequest : nullptr),\n\t\t.useFullWidth = true,\n\t\t.linePostprocess = linePostprocess ? &*linePostprocess : nullptr,\n\t});\n\tif (needRippleMask && !ripplePath.isEmpty()) {\n\t\tcreateLinkRippleMask(\n\t\t\tripplePath,\n\t\t\ttrect.topLeft(),\n\t\t\ttrect.width(),\n\t\t\tst::linkRipplePadding,\n\t\t\tst::linkRippleRadius);\n\t} else if (needRippleMask) {\n\t\t_linkRipple = nullptr;\n\t}\n\tif (appearingClip) {\n\t\tp.restore();\n\t}\n}\n\nPointState Message::pointState(QPoint point) const {\n\tauto g = countGeometry();\n\tif (g.width() < 1 || isHidden()) {\n\t\treturn PointState::Outside;\n\t}\n\n\tconst auto media = this->media();\n\tconst auto item = data();\n\tconst auto reactionsInBubble = _reactions && embedReactionsInBubble();\n\tif (drawBubble()) {\n\t\tif (!g.contains(point)) {\n\t\t\treturn PointState::Outside;\n\t\t}\n\t\tif (const auto mediaDisplayed = media && media->isDisplayed()) {\n\t\t\t// Hack for grouped media point state.\n\t\t\tconst auto entry = logEntryOriginal();\n\t\t\tconst auto check = factcheckBlock();\n\n\t\t\t// Entry page is always a bubble bottom.\n\t\t\tauto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/);\n\t\t\tauto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop());\n\n\t\t\tif (item->repliesAreComments() || item->externalReply()) {\n\t\t\t\tg.setHeight(g.height() - st::historyCommentsButtonHeight);\n\t\t\t}\n\n\t\t\tauto trect = g.marginsRemoved(st::msgPadding);\n\t\t\tif (reactionsInBubble) {\n\t\t\t\tconst auto reactionsHeight = (_viewButton ? 0 : st::mediaInBubbleSkip)\n\t\t\t\t\t+ _reactions->height();\n\t\t\t\ttrect.setHeight(trect.height() - reactionsHeight);\n\t\t\t}\n\t\t\tif (_viewButton) {\n\t\t\t\ttrect.setHeight(trect.height() - _viewButton->height());\n\t\t\t\tif (reactionsInBubble) {\n\t\t\t\t\ttrect.setHeight(trect.height() + st::msgPadding.bottom());\n\t\t\t\t} else if (mediaDisplayed) {\n\t\t\t\t\ttrect.setHeight(trect.height() - st::mediaInBubbleSkip);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (mediaOnBottom) {\n\t\t\t\ttrect.setHeight(trect.height() + st::msgPadding.bottom());\n\t\t\t}\n\t\t\t//if (mediaOnTop) {\n\t\t\t//\ttrect.setY(trect.y() - st::msgPadding.top());\n\t\t\t//} else {\n\t\t\t//\tif (getStateFromName(point, trect, &result)) return result;\n\t\t\t//\tif (getStateTopicButton(point, trect, &result)) return result;\n\t\t\t//\tif (getStateForwardedInfo(point, trect, &result, request)) return result;\n\t\t\t//\tif (getStateReplyInfo(point, trect, &result)) return result;\n\t\t\t//\tif (getStateViaBotIdInfo(point, trect, &result)) return result;\n\t\t\t//}\n\t\t\tif (check) {\n\t\t\t\tauto checkHeight = check->height();\n\t\t\t\ttrect.setHeight(trect.height() - checkHeight - st::mediaInBubbleSkip);\n\t\t\t}\n\t\t\tif (entry) {\n\t\t\t\tauto entryHeight = entry->height();\n\t\t\t\ttrect.setHeight(trect.height() - entryHeight);\n\t\t\t}\n\n\t\t\tconst auto mediaHeight = mediaDisplayed ? media->height() : 0;\n\t\t\tconst auto mediaLeft = trect.x() - st::msgPadding.left();\n\t\t\tconst auto mediaTop = (!mediaDisplayed || _invertMedia)\n\t\t\t\t? (trect.y() + (mediaOnTop ? 0 : st::mediaInBubbleSkip))\n\t\t\t\t: (trect.y() + trect.height() - mediaHeight);\n\t\t\tif (mediaDisplayed && _invertMedia) {\n\t\t\t\ttrect.setY(mediaTop\n\t\t\t\t\t+ mediaHeight\n\t\t\t\t\t+ (mediaOnBottom ? 0 : st::mediaInBubbleSkip));\n\t\t\t}\n\t\t\tif (point.y() >= mediaTop\n\t\t\t\t&& point.y() < mediaTop + mediaHeight) {\n\t\t\t\treturn media->pointState(point - QPoint(mediaLeft, mediaTop));\n\t\t\t}\n\t\t}\n\t\treturn PointState::Inside;\n\t} else if (media) {\n\t\treturn media->pointState(point - g.topLeft());\n\t}\n\treturn PointState::Outside;\n}\n\nbool Message::displayFromPhoto() const {\n\treturn hasFromPhoto() && !isAttachedToNext();\n}\n\nvoid Message::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) {\n\tconst auto startLinkRipple = [&] {\n\t\tif (!_linkRipple) {\n\t\t\tif (!pressed) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_linkRipple = std::make_unique<LinkRipple>();\n\t\t}\n\t\t_linkRipple->link = handler;\n\t\ttoggleLinkRipple(pressed);\n\t};\n\tif (const auto markup = data()->Get<HistoryMessageReplyMarkup>()) {\n\t\tif (const auto keyboard = markup->inlineKeyboard.get()) {\n\t\t\tkeyboard->clickHandlerPressedChanged(\n\t\t\t\thandler,\n\t\t\t\tpressed,\n\t\t\t\tcountMessageRounding());\n\t\t}\n\t}\n\tElement::clickHandlerPressedChanged(handler, pressed);\n\tif (const auto check = factcheckBlock()) {\n\t\tcheck->clickHandlerPressedChanged(handler, pressed);\n\t}\n\tif (!handler) {\n\t\treturn;\n\t} else if (_rightAction && (handler == _rightAction->link)) {\n\t\ttoggleRightActionRipple(pressed);\n\t} else if (_rightAction\n\t\t&& _rightAction->second\n\t\t&& (handler == _rightAction->second->link)) {\n\t\tconst auto rightSize = rightActionSize();\n\t\tAssert(rightSize != std::nullopt);\n\t\tif (pressed) {\n\t\t\tif (!_rightAction->second->ripple) {\n\t\t\t\t// Create a ripple.\n\t\t\t\t_rightAction->second->ripple\n\t\t\t\t\t= std::make_unique<Ui::RippleAnimation>(\n\t\t\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\t\t\tUi::RippleAnimation::RoundRectMask(\n\t\t\t\t\t\t\tSize(rightSize->width()),\n\t\t\t\t\t\t\trightSize->width() / 2),\n\t\t\t\t\t\t[=] { repaint(); });\n\t\t\t}\n\t\t\t_rightAction->second->ripple->add(_rightAction->lastPoint);\n\t\t} else if (_rightAction->second->ripple) {\n\t\t\t_rightAction->second->ripple->lastStop();\n\t\t}\n\t} else if (_comments && (handler == _comments->link)) {\n\t\ttoggleCommentsButtonRipple(pressed);\n\t} else if (_topicButton && (handler == _topicButton->link)) {\n\t\ttoggleTopicButtonRipple(pressed);\n\t} else if (_viewButton) {\n\t\t_viewButton->checkLink(handler, pressed);\n\t} else if (const auto reply = Get<Reply>()\n\t\t; reply && (handler == reply->link())) {\n\t\ttoggleReplyRipple(pressed);\n\t} else if (const auto summaryHeader = Get<SummaryHeader>()\n\t\t; summaryHeader && (handler == summaryHeader->link())) {\n\t\ttoggleSummaryHeaderRipple(pressed);\n\t} else if (_summarize && (handler == _summarize->link())) {\n\t\tif (pressed) {\n\t\t\t_summarize->addRipple([=] { repaint(); });\n\t\t} else {\n\t\t\t_summarize->stopRipple();\n\t\t}\n\t} else if (const auto badge = Get<RightBadge>()\n\t\t; badge && badge->tagLink && handler == badge->tagLink) {\n\t\ttoggleBadgeRipple(pressed);\n\t} else if (displayFromName() && handler == fromLink()) {\n\t\tif (_fromLinkRipplePointSet || !pressed) {\n\t\t\tstartLinkRipple();\n\t\t}\n\t} else if (const auto via = data()->Get<HistoryMessageVia>()\n\t\t; via\n\t\t&& (handler == via->link)\n\t\t&& !displayForwardedFrom()) {\n\t\tstartLinkRipple();\n\t} else if (const auto forwarded = data()->Get<HistoryMessageForwarded>()\n\t\t; forwarded\n\t\t&& displayForwardedFrom()\n\t\t&& !forwarded->text.linkRangeFor(handler).empty()) {\n\t\tstartLinkRipple();\n\t} else if (hasVisibleText()\n\t\t&& IsRippleLink(handler)\n\t\t&& !text().linkRangeFor(handler).empty()) {\n\t\tstartLinkRipple();\n\t} else if (_reactions) {\n\t\t_reactions->clickHandlerPressedChanged(\n\t\t\thandler,\n\t\t\tpressed,\n\t\t\t[=] { repaint(); });\n\t}\n}\n\nvoid Message::toggleCommentsButtonRipple(bool pressed) {\n\tExpects(_comments != nullptr);\n\n\tif (!drawBubble()) {\n\t\treturn;\n\t} else if (pressed) {\n\t\tif (!_comments->ripple) {\n\t\t\tcreateCommentsButtonRipple();\n\t\t}\n\t\t_comments->ripple->add(_comments->lastPoint\n\t\t\t+ QPoint(_comments->rippleShift, 0));\n\t} else if (_comments->ripple) {\n\t\t_comments->ripple->lastStop();\n\t}\n}\n\nvoid Message::toggleRightActionRipple(bool pressed) {\n\tExpects(_rightAction != nullptr);\n\n\tconst auto rightSize = rightActionSize();\n\tAssert(rightSize != std::nullopt);\n\n\tif (pressed) {\n\t\tif (!_rightAction->ripple) {\n\t\t\t// Create a ripple.\n\t\t\tconst auto size = _rightAction->second\n\t\t\t\t? Size(rightSize->width())\n\t\t\t\t: *rightSize;\n\t\t\t_rightAction->ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\tUi::RippleAnimation::RoundRectMask(size, size.width() / 2),\n\t\t\t\t[=] { repaint(); });\n\t\t}\n\t\t_rightAction->ripple->add(_rightAction->lastPoint);\n\t} else if (_rightAction->ripple) {\n\t\t_rightAction->ripple->lastStop();\n\t}\n}\n\nvoid Message::toggleBadgeRipple(bool pressed) {\n\tconst auto badge = Get<RightBadge>();\n\tif (!badge) {\n\t\treturn;\n\t} else if (pressed) {\n\t\tif (!badge->ripple) {\n\t\t\tconst auto pill = ComputeBadgePillGeometry(badge);\n\t\t\tauto mask = Ui::RippleAnimation::RoundRectMask(\n\t\t\t\tQSize(pill.width, pill.height),\n\t\t\t\tpill.height / 2);\n\t\t\tbadge->ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\tstd::move(mask),\n\t\t\t\t[=] { repaint(); });\n\t\t}\n\t\tbadge->ripple->add(badge->lastPoint);\n\t} else if (badge->ripple) {\n\t\tbadge->ripple->lastStop();\n\t}\n}\n\nvoid Message::toggleReplyRipple(bool pressed) {\n\tconst auto reply = Get<Reply>();\n\tif (!reply) {\n\t\treturn;\n\t}\n\n\tif (pressed) {\n\t\tif (!unwrapped()) {\n\t\t\tconst auto &padding = st::msgPadding;\n\t\t\tconst auto geometry = countGeometry();\n\t\t\tconst auto margins = reply->margins();\n\t\t\tconst auto size = QSize(\n\t\t\t\tgeometry.width() - padding.left() - padding.right(),\n\t\t\t\treply->height() - margins.top() - margins.bottom());\n\t\t\treply->createRippleAnimation(this, size);\n\t\t}\n\t\treply->addRipple();\n\t} else {\n\t\treply->stopLastRipple();\n\t}\n}\n\nvoid Message::toggleSummaryHeaderRipple(bool pressed) {\n\tconst auto summaryHeader = Get<SummaryHeader>();\n\tif (!summaryHeader) {\n\t\treturn;\n\t}\n\n\tif (pressed) {\n\t\tif (!unwrapped()) {\n\t\t\tconst auto size = QSize(\n\t\t\t\tcountGeometry().width() - rect::m::sum::h(st::msgPadding),\n\t\t\t\tsummaryHeader->height()\n\t\t\t\t\t- rect::m::sum::v(summaryHeader->margins()));\n\t\t\tsummaryHeader->createRippleAnimation(this, size);\n\t\t}\n\t\tsummaryHeader->addRipple();\n\t} else {\n\t\tsummaryHeader->stopLastRipple();\n\t}\n}\n\nBottomRippleMask Message::bottomRippleMask(int buttonHeight) const {\n\tusing namespace Ui;\n\tusing namespace Images;\n\tusing Radius = CachedCornerRadius;\n\tusing Corner = BubbleCornerRounding;\n\tconst auto g = countGeometry();\n\tconst auto buttonWidth = g.width();\n\tconst auto &large = CachedCornersMasks(Radius::BubbleLarge);\n\tconst auto &small = CachedCornersMasks(Radius::BubbleSmall);\n\tconst auto rounding = countBubbleRounding();\n\tconst auto icon = (rounding.bottomLeft == Corner::Tail)\n\t\t? &st::historyBubbleTailInLeft\n\t\t: (rounding.bottomRight == Corner::Tail)\n\t\t? &st::historyBubbleTailInRight\n\t\t: nullptr;\n\tconst auto shift = (rounding.bottomLeft == Corner::Tail)\n\t\t? icon->width()\n\t\t: 0;\n\tconst auto added = shift ? shift : icon ? icon->width() : 0;\n\tauto corners = CornersMaskRef();\n\tconst auto set = [&](int index) {\n\t\tcorners.p[index] = (rounding[index] == Corner::Large)\n\t\t\t? &large[index]\n\t\t\t: (rounding[index] == Corner::Small)\n\t\t\t? &small[index]\n\t\t\t: nullptr;\n\t};\n\tset(kBottomLeft);\n\tset(kBottomRight);\n\tconst auto drawer = [&](QPainter &p) {\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto corner = [&](int index, bool right) {\n\t\t\tif (const auto image = corners.p[index]) {\n\t\t\t\tconst auto width = image->width() / ratio;\n\t\t\t\tconst auto height = image->height() / ratio;\n\t\t\t\tp.drawImage(\n\t\t\t\t\tQRect(\n\t\t\t\t\t\tshift + (right ? (buttonWidth - width) : 0),\n\t\t\t\t\t\tbuttonHeight - height,\n\t\t\t\t\t\twidth,\n\t\t\t\t\t\theight),\n\t\t\t\t\t*image);\n\t\t\t}\n\t\t};\n\t\tcorner(kBottomLeft, false);\n\t\tcorner(kBottomRight, true);\n\t\tif (icon) {\n\t\t\tconst auto left = shift ? 0 : buttonWidth;\n\t\t\tp.fillRect(\n\t\t\t\tQRect{ left, 0, added, buttonHeight },\n\t\t\t\tQt::transparent);\n\t\t\ticon->paint(\n\t\t\t\tp,\n\t\t\t\tleft,\n\t\t\t\tbuttonHeight - icon->height(),\n\t\t\t\tbuttonWidth + shift,\n\t\t\t\tQt::white);\n\t\t}\n\t};\n\treturn {\n\t\tRippleAnimation::MaskByDrawer(\n\t\t\tQSize(buttonWidth + added, buttonHeight),\n\t\t\ttrue,\n\t\t\tdrawer),\n\t\tshift,\n\t};\n}\n\nvoid Message::createCommentsButtonRipple() {\n\tauto mask = bottomRippleMask(st::historyCommentsButtonHeight);\n\t_comments->ripple = std::make_unique<Ui::RippleAnimation>(\n\t\tst::defaultRippleAnimation,\n\t\tstd::move(mask.image),\n\t\t[=] { repaint(); });\n\t_comments->rippleShift = mask.shift;\n}\n\nvoid Message::toggleTopicButtonRipple(bool pressed) {\n\tExpects(_topicButton != nullptr);\n\n\tif (!drawBubble()) {\n\t\treturn;\n\t} else if (pressed) {\n\t\tif (!_topicButton->ripple) {\n\t\t\tcreateTopicButtonRipple();\n\t\t}\n\t\t_topicButton->ripple->add(_topicButton->lastPoint);\n\t} else if (_topicButton->ripple) {\n\t\t_topicButton->ripple->lastStop();\n\t}\n}\n\nvoid Message::paintLinkRipple(\n\t\tPainter &p,\n\t\tconst ClickHandlerPtr &handler,\n\t\tQRect linkRect,\n\t\tQPoint textPosition) const {\n\tconst auto raw = _linkRipple.get();\n\tif (!raw || raw->link != handler) {\n\t\treturn;\n\t}\n\tif (const auto ripple = raw->ripple.get()) {\n\t\tauto color = p.pen().color();\n\t\tcolor.setAlpha(25);\n\t\tripple->paint(\n\t\t\tp,\n\t\t\ttextPosition.x() + raw->maskOffset.x(),\n\t\t\ttextPosition.y() + raw->maskOffset.y(),\n\t\t\twidth(),\n\t\t\t&color);\n\t\tif (ripple->empty()) {\n\t\t\t_linkRipple = nullptr;\n\t\t}\n\t} else {\n\t\tcreateLinkRippleMask(\n\t\t\tlinkRect,\n\t\t\ttextPosition,\n\t\t\tst::nameRipplePadding,\n\t\t\tst::nameRippleRadius);\n\t}\n}\n\nvoid Message::toggleLinkRipple(bool pressed) {\n\tif (!drawBubble()) {\n\t\treturn;\n\t} else if (pressed) {\n\t\trepaint();\n\t} else if (const auto ripple = _linkRipple\n\t\t? _linkRipple->ripple.get()\n\t\t: nullptr) {\n\t\tripple->lastStop();\n\t}\n}\n\nvoid Message::recordLinkRipplePoint(\n\t\tQPoint point,\n\t\tQPoint textOrigin) const {\n\t_linkRippleLastPoint = point - textOrigin;\n}\n\nvoid Message::createLinkRippleMask(\n\t\tconst QPainterPath &path,\n\t\tQPoint textPosition,\n\t\tint useWidth,\n\t\tstyle::margins padding,\n\t\tint radius) const {\n\tauto rects = std::vector<QRect>();\n\tfor (const auto &polygon : path.toSubpathPolygons()) {\n\t\trects.push_back(polygon.boundingRect().toAlignedRect());\n\t}\n\tauto boundingRect = QRect();\n\tfor (auto &rect : rects) {\n\t\trect = rect.marginsAdded(padding);\n\t\tif (boundingRect.isEmpty()) {\n\t\t\tboundingRect = rect;\n\t\t} else {\n\t\t\tboundingRect = boundingRect.united(rect);\n\t\t}\n\t}\n\tif (boundingRect.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto topLeft = boundingRect.topLeft();\n\tconst auto maskOrigin = topLeft - textPosition;\n\tauto mask = Ui::RippleAnimation::MaskByDrawer(\n\t\tboundingRect.size(),\n\t\tfalse,\n\t\t[&](QPainter &p) {\n\t\t\tfor (const auto &rect : rects) {\n\t\t\t\tconst auto shifted = rect.translated(-topLeft);\n\t\t\t\tp.drawRoundedRect(shifted, radius, radius);\n\t\t\t}\n\t\t});\n\t_linkRipple->maskOffset = maskOrigin;\n\t_linkRipple->cachedWidth = useWidth;\n\t_linkRipple->ripple = std::make_unique<Ui::RippleAnimation>(\n\t\tst::defaultRippleAnimation,\n\t\tstd::move(mask),\n\t\t[=] { repaint(); });\n\t_linkRipple->ripple->add(_linkRippleLastPoint - maskOrigin);\n}\n\nvoid Message::createLinkRippleMask(\n\t\tQRect linkRect,\n\t\tQPoint textPosition,\n\t\tstyle::margins padding,\n\t\tint radius) const {\n\tauto rect = linkRect.marginsAdded(padding);\n\tconst auto maskOrigin = rect.topLeft() - textPosition;\n\tconst auto size = rect.size();\n\tauto mask = Ui::RippleAnimation::MaskByDrawer(\n\t\tsize,\n\t\tfalse,\n\t\t[&](QPainter &p) {\n\t\t\tp.drawRoundedRect(QRect(QPoint(), size), radius, radius);\n\t\t});\n\t_linkRipple->maskOffset = maskOrigin;\n\t_linkRipple->cachedWidth = 0;\n\t_linkRipple->ripple = std::make_unique<Ui::RippleAnimation>(\n\t\tst::defaultRippleAnimation,\n\t\tstd::move(mask),\n\t\t[=] { repaint(); });\n\t_linkRipple->ripple->add(_linkRippleLastPoint - maskOrigin);\n}\n\nvoid Message::createTopicButtonRipple() {\n\tconst auto geometry = countGeometry().marginsRemoved(st::msgPadding);\n\tconst auto availableWidth = geometry.width();\n\tconst auto padding = st::topicButtonPadding;\n\tconst auto height = padding.top()\n\t\t+ st::msgNameFont->height\n\t\t+ padding.bottom();\n\tconst auto width = std::max(\n\t\tstd::min(\n\t\t\tavailableWidth,\n\t\t\t(padding.left()\n\t\t\t\t+ _topicButton->name.maxWidth()\n\t\t\t\t+ st::topicButtonArrowSkip\n\t\t\t\t+ padding.right())),\n\t\theight);\n\tauto mask = Ui::RippleAnimation::RoundRectMask(\n\t\t{ width, height },\n\t\theight / 2);\n\t_topicButton->ripple = std::make_unique<Ui::RippleAnimation>(\n\t\tst::defaultRippleAnimation,\n\t\tstd::move(mask),\n\t\t[=] { repaint(); });\n}\n\nbool Message::hasHeavyPart() const {\n\treturn _comments\n\t\t|| (_fromNameStatus && _fromNameStatus->custom)\n\t\t|| Element::hasHeavyPart();\n}\n\nvoid Message::unloadHeavyPart() {\n\tElement::unloadHeavyPart();\n\t_comments = nullptr;\n\tif (_fromNameStatus) {\n\t\t_fromNameStatus->custom = nullptr;\n\t\t_fromNameStatus->id = EmojiStatusId();\n\t}\n\tif (const auto summaryHeader = Get<SummaryHeader>()) {\n\t\tsummaryHeader->unloadHeavyPart();\n\t}\n}\n\nbool Message::hasFromPhoto() const {\n\tif (isHidden()) {\n\t\treturn false;\n\t}\n\tswitch (context()) {\n\tcase Context::AdminLog:\n\t\treturn true;\n\tcase Context::Monoforum:\n\t\treturn (delegate()->elementChatMode() == ElementChatMode::Wide);\n\tcase Context::History:\n\tcase Context::ChatPreview:\n\tcase Context::TTLViewer:\n\tcase Context::Pinned:\n\tcase Context::Replies:\n\tcase Context::SavedSublist:\n\tcase Context::ScheduledTopic: {\n\t\tconst auto item = data();\n\t\tif (item->isSponsored()) {\n\t\t\treturn false;\n\t\t} else if (item->isPostHidingAuthor()) {\n\t\t\treturn false;\n\t\t} else if (item->isPost()) {\n\t\t\treturn true;\n\t\t} else if (item->isEmpty()\n\t\t\t|| item->isFakeAboutView()\n\t\t\t|| isCommentsRootView()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto mode = delegate()->elementChatMode();\n\t\tif (mode != ElementChatMode::Default) {\n\t\t\treturn (mode == ElementChatMode::Wide);\n\t\t} else if (item->history()->peer->isVerifyCodes()) {\n\t\t\treturn !hasOutLayout();\n\t\t} else if (item->Has<HistoryMessageForwarded>()) {\n\t\t\tconst auto peer = item->history()->peer;\n\t\t\tif (peer->isSelf() || peer->isRepliesChat()) {\n\t\t\t\treturn !hasOutLayout();\n\t\t\t}\n\t\t}\n\t\treturn !item->out() && !item->history()->peer->isUser();\n\t} break;\n\tcase Context::ContactPreview:\n\tcase Context::ShortcutMessages:\n\t\treturn false;\n\t}\n\tUnexpected(\"Context in Message::hasFromPhoto.\");\n}\n\nTextState Message::textState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\t_fromLinkRipplePointSet = 0;\n\n\tconst auto item = data();\n\tconst auto media = this->media();\n\n\tauto result = TextState(item);\n\tconst auto visibleMediaTextLen = visibleMediaTextLength();\n\tconst auto visibleTextLen = visibleTextLength();\n\tconst auto minSymbol = (_invertMedia && request.onlyMessageText)\n\t\t? visibleMediaTextLen\n\t\t: 0;\n\tresult.symbol = minSymbol;\n\n\tauto g = countGeometry();\n\tif (g.width() < 1 || isHidden()) {\n\t\treturn result;\n\t}\n\n\tif (const auto service = Get<ServicePreMessage>()) {\n\t\tresult.link = service->textState(point, request, g);\n\t\tif (result.link) {\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tconst auto bubble = drawBubble();\n\tconst auto reactionsInBubble = _reactions && embedReactionsInBubble();\n\tconst auto mediaDisplayed = media && media->isDisplayed();\n\n\tif (_reactions && !reactionsInBubble) {\n\t\tconst auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height();\n\t\tconst auto reactionsLeft = (!bubble && mediaDisplayed)\n\t\t\t? media->contentRectForReactions().x()\n\t\t\t: 0;\n\t\tg.setHeight(g.height() - reactionsHeight);\n\t\tconst auto reactionsPosition = QPoint(reactionsLeft + g.left(), g.top() + g.height() + st::mediaInBubbleSkip);\n\t\tif (_reactions->getState(point - reactionsPosition, &result)) {\n\t\t\tresult.symbol += visibleMediaTextLen + visibleTextLen;\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tconst auto keyboard = item->inlineReplyKeyboard();\n\tauto keyboardHeight = 0;\n\tif (keyboard) {\n\t\tkeyboardHeight = keyboard->naturalHeight();\n\t\tg.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight);\n\n\t\tif (item->isHistoryEntry() || item->isAdminLogEntry()) {\n\t\t\tconst auto keyboardPosition = QPoint(g.left(), g.top() + g.height() + st::msgBotKbButton.margin);\n\t\t\tif (QRect(keyboardPosition, QSize(g.width(), keyboardHeight)).contains(point)) {\n\t\t\t\tresult.symbol += visibleMediaTextLen + visibleTextLen;\n\t\t\t\tresult.link = keyboard->getLink(point - keyboardPosition);\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (bubble) {\n\t\tconst auto inBubble = g.contains(point);\n\t\tconst auto check = factcheckBlock();\n\t\tconst auto entry = logEntryOriginal();\n\n\t\t// Entry page is always a bubble bottom.\n\t\tauto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/);\n\t\tauto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop());\n\n\t\tauto inner = g;\n\t\tif (getStateCommentsButton(point, inner, &result)) {\n\t\t\tresult.symbol += visibleMediaTextLen + visibleTextLen;\n\t\t\treturn result;\n\t\t}\n\t\tauto trect = inner.marginsRemoved(st::msgPadding);\n\t\tconst auto additionalInfoSkip = (mediaDisplayed\n\t\t\t&& !media->additionalInfoString().isEmpty())\n\t\t\t? st::msgDateFont->height\n\t\t\t: 0;\n\t\tconst auto reactionsTop = (reactionsInBubble && !_viewButton)\n\t\t\t? (additionalInfoSkip + st::mediaInBubbleSkip)\n\t\t\t: additionalInfoSkip;\n\t\tconst auto reactionsHeight = reactionsInBubble\n\t\t\t? (reactionsTop + _reactions->height())\n\t\t\t: 0;\n\t\tif (reactionsInBubble) {\n\t\t\ttrect.setHeight(trect.height() - reactionsHeight);\n\t\t\tconst auto reactionsPosition = QPoint(trect.left(), trect.top() + trect.height() + reactionsTop);\n\t\t\tif (_reactions->getState(point - reactionsPosition, &result)) {\n\t\t\t\tresult.symbol += visibleMediaTextLen + visibleTextLen;\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t\tif (_viewButton) {\n\t\t\tconst auto belowInfo = _viewButton->belowMessageInfo();\n\t\t\tconst auto infoHeight = reactionsInBubble\n\t\t\t\t? (reactionsHeight + 2 * st::mediaInBubbleSkip)\n\t\t\t\t: _bottomInfo.height();\n\t\t\tconst auto heightMargins = QMargins(0, 0, 0, infoHeight);\n\t\t\tif (_viewButton->getState(\n\t\t\t\t\tpoint,\n\t\t\t\t\t_viewButton->countRect(belowInfo\n\t\t\t\t\t\t? inner\n\t\t\t\t\t\t: inner - heightMargins),\n\t\t\t\t\t&result)) {\n\t\t\t\tresult.symbol += visibleMediaTextLen + visibleTextLen;\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tif (belowInfo) {\n\t\t\t\tinner.setHeight(inner.height() - _viewButton->height());\n\t\t\t}\n\t\t\ttrect.setHeight(trect.height() - _viewButton->height());\n\t\t\tif (reactionsInBubble) {\n\t\t\t\ttrect.setHeight(trect.height() - st::mediaInBubbleSkip + st::msgPadding.bottom());\n\t\t\t} else if (mediaDisplayed) {\n\t\t\t\ttrect.setHeight(trect.height() - st::mediaInBubbleSkip);\n\t\t\t}\n\t\t}\n\t\tif (mediaOnBottom) {\n\t\t\ttrect.setHeight(trect.height() + st::msgPadding.bottom());\n\t\t}\n\t\tif (mediaOnTop) {\n\t\t\ttrect.setY(trect.y() - st::msgPadding.top());\n\t\t} else if (inBubble) {\n\t\t\tif (getStateFromName(point, trect, &result)) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tif (getStateTopicButton(point, trect, &result)) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tif (getStateForwardedInfo(point, trect, &result, request)) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tif (getStateViaBotIdInfo(point, trect, &result)) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tif (getStateReplyInfo(point, trect, &result)) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tif (getStateSummaryHeaderInfo(point, trect, &result)) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t\tif (entry) {\n\t\t\tauto entryHeight = entry->height();\n\t\t\ttrect.setHeight(trect.height() - entryHeight);\n\t\t\tauto entryLeft = inner.left();\n\t\t\tauto entryTop = trect.y() + trect.height();\n\t\t\tif (point.y() >= entryTop && point.y() < entryTop + entryHeight) {\n\t\t\t\tresult = entry->textState(\n\t\t\t\t\tpoint - QPoint(entryLeft, entryTop),\n\t\t\t\t\trequest);\n\t\t\t\tresult.symbol += visibleTextLength()\n\t\t\t\t\t+ visibleMediaTextLength();\n\t\t\t}\n\t\t}\n\t\tif (check) {\n\t\t\tauto checkHeight = check->height();\n\t\t\ttrect.setHeight(trect.height() - checkHeight - st::mediaInBubbleSkip);\n\t\t\tauto checkLeft = inner.left();\n\t\t\tauto checkTop = trect.y() + trect.height() + st::mediaInBubbleSkip;\n\t\t\tif (point.y() >= checkTop && point.y() < checkTop + checkHeight) {\n\t\t\t\tresult = check->textState(\n\t\t\t\t\tpoint - QPoint(checkLeft, checkTop),\n\t\t\t\t\trequest);\n\t\t\t\tresult.symbol += visibleTextLength()\n\t\t\t\t\t+ visibleMediaTextLength();\n\t\t\t}\n\t\t}\n\n\t\tauto checkBottomInfoState = [&] {\n\t\t\tif (mediaOnBottom\n\t\t\t\t&& (check || entry || media->customInfoLayout())) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto bottomInfoResult = bottomInfoTextState(\n\t\t\t\tinner.left() + inner.width(),\n\t\t\t\tinner.top() + inner.height(),\n\t\t\t\tpoint,\n\t\t\t\tInfoDisplayType::Default);\n\t\t\tif (bottomInfoResult.link\n\t\t\t\t|| bottomInfoResult.cursor != CursorState::None\n\t\t\t\t|| bottomInfoResult.customTooltip) {\n\t\t\t\tresult = bottomInfoResult;\n\t\t\t}\n\t\t};\n\t\tif (!inBubble) {\n\t\t\tif (point.y() >= g.y() + g.height()) {\n\t\t\t\tresult.symbol += visibleTextLen + visibleMediaTextLen;\n\t\t\t}\n\t\t} else if (result.symbol <= minSymbol) {\n\t\t\tconst auto mediaHeight = mediaDisplayed ? media->height() : 0;\n\t\t\tconst auto mediaLeft = trect.x() - st::msgPadding.left();\n\t\t\tconst auto mediaTop = (!mediaDisplayed || _invertMedia)\n\t\t\t\t? (trect.y() + (mediaOnTop ? 0 : st::mediaInBubbleSkip))\n\t\t\t\t: (trect.y() + trect.height() - mediaHeight);\n\t\t\tif (mediaDisplayed && _invertMedia) {\n\t\t\t\ttrect.setY(mediaTop\n\t\t\t\t\t+ mediaHeight\n\t\t\t\t\t+ (mediaOnBottom ? 0 : st::mediaInBubbleSkip));\n\t\t\t}\n\t\t\tif (point.y() >= mediaTop\n\t\t\t\t&& point.y() < mediaTop + mediaHeight) {\n\t\t\t\tresult = media->textState(\n\t\t\t\t\tpoint - QPoint(mediaLeft, mediaTop),\n\t\t\t\t\trequest);\n\t\t\t\tif (_invertMedia) {\n\t\t\t\t\tif (request.onlyMessageText) {\n\t\t\t\t\t\tresult.symbol = minSymbol;\n\t\t\t\t\t\tresult.afterSymbol = false;\n\t\t\t\t\t\tresult.cursor = CursorState::None;\n\t\t\t\t\t}\n\t\t\t\t} else if (request.onlyMessageText) {\n\t\t\t\t\tresult.symbol = visibleTextLen;\n\t\t\t\t\tresult.afterSymbol = false;\n\t\t\t\t\tresult.cursor = CursorState::None;\n\t\t\t\t} else {\n\t\t\t\t\tresult.symbol += visibleTextLen;\n\t\t\t\t}\n\t\t\t} else if (getStateText(point, trect, &result, request)) {\n\t\t\t\tif (_invertMedia) {\n\t\t\t\t\tresult.symbol += visibleMediaTextLen;\n\t\t\t\t}\n\t\t\t\tresult.overMessageText = true;\n\t\t\t\tcheckBottomInfoState();\n\t\t\t\treturn result;\n\t\t\t} else if (point.y() >= trect.y() + trect.height()) {\n\t\t\t\tresult.symbol = visibleTextLen + visibleMediaTextLen;\n\t\t\t}\n\t\t}\n\t\tcheckBottomInfoState();\n\t\tif (const auto size = rightActionSize(); size && _rightAction) {\n\t\t\tconst auto fastShareSkip = std::clamp(\n\t\t\t\t(g.height() - size->height()) / 2,\n\t\t\t\t0,\n\t\t\t\tst::historyFastShareBottom);\n\t\t\tconst auto fastShareLeft = hasRightLayout()\n\t\t\t\t? (g.left() - size->width() - st::historyFastShareLeft)\n\t\t\t\t: (g.left() + g.width() + st::historyFastShareLeft);\n\t\t\tconst auto fastShareTop = data()->isSponsored()\n\t\t\t\t? g.top() + fastShareSkip\n\t\t\t\t: g.top() + g.height() - fastShareSkip - size->height();\n\t\t\tif (QRect(\n\t\t\t\tfastShareLeft,\n\t\t\t\tfastShareTop,\n\t\t\t\tsize->width(),\n\t\t\t\tsize->height()\n\t\t\t).contains(point)) {\n\t\t\t\tresult.link = rightActionLink(point\n\t\t\t\t\t- QPoint(fastShareLeft, fastShareTop));\n\t\t\t}\n\t\t}\n\t\tif (_summarize && _summarize->contains(point)) {\n\t\t\tresult.link = _summarize->link();\n\t\t}\n\t} else if (media && media->isDisplayed()) {\n\t\tresult = media->textState(point - g.topLeft(), request);\n\t\tif (request.onlyMessageText) {\n\t\t\tresult.symbol = 0;\n\t\t\tresult.afterSymbol = false;\n\t\t\tresult.cursor = CursorState::None;\n\t\t}\n\t\tresult.symbol += visibleTextLength();\n\t}\n\n\treturn result;\n}\n\nbool Message::getStateCommentsButton(\n\t\tQPoint point,\n\t\tQRect &g,\n\t\tnot_null<TextState*> outResult) const {\n\tif (!_comments) {\n\t\treturn false;\n\t}\n\tg.setHeight(g.height() - st::historyCommentsButtonHeight);\n\tif (data()->isSending()\n\t\t|| !QRect(\n\t\t\tg.left(),\n\t\t\tg.top() + g.height(),\n\t\t\tg.width(),\n\t\t\tst::historyCommentsButtonHeight).contains(point)) {\n\t\treturn false;\n\t}\n\tif (!_comments->link && data()->repliesAreComments()) {\n\t\t_comments->link = createGoToCommentsLink();\n\t} else if (!_comments->link && data()->externalReply()) {\n\t\t_comments->link = prepareRightActionLink();\n\t}\n\toutResult->link = _comments->link;\n\t_comments->lastPoint = point - QPoint(g.left(), g.top() + g.height());\n\treturn true;\n}\n\nClickHandlerPtr Message::createGoToCommentsLink() const {\n\tconst auto fullId = data()->fullId();\n\tconst auto sessionId = data()->history()->session().uniqueId();\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto controller = ExtractController(context);\n\t\tif (!controller || controller->session().uniqueId() != sessionId) {\n\t\t\treturn;\n\t\t}\n\t\tif (const auto item = controller->session().data().message(fullId)) {\n\t\t\tconst auto history = item->history();\n\t\t\tif (const auto channel = history->peer->asChannel()) {\n\t\t\t\tif (channel->invitePeekExpires()) {\n\t\t\t\t\tcontroller->showToast(\n\t\t\t\t\t\ttr::lng_channel_invite_private(tr::now));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontroller->showRepliesForMessage(history, item->id);\n\t\t}\n\t});\n}\n\nbool Message::getStateFromName(\n\t\tQPoint point,\n\t\tQRect &trect,\n\t\tnot_null<TextState*> outResult) const {\n\tif (!displayFromName()) {\n\t\treturn false;\n\t}\n\tif (point.y() >= trect.top() && point.y() < trect.top() + st::msgNameFont->height) {\n\t\tauto availableLeft = trect.left();\n\t\tauto availableWidth = trect.width();\n\t\tconst auto badgeWidth = rightBadgeWidth();\n\t\tif (badgeWidth) {\n\t\t\tavailableWidth -= st::msgPadding.right() + badgeWidth;\n\t\t}\n\t\tconst auto item = data();\n\t\tconst auto from = item->displayFrom();\n\t\tconst auto nameText = [&]() -> const Ui::Text::String * {\n\t\t\tif (from) {\n\t\t\t\tvalidateFromNameText(from);\n\t\t\t\treturn &_fromName;\n\t\t\t} else if (const auto info = item->displayHiddenSenderInfo()) {\n\t\t\t\treturn &info->nameText();\n\t\t\t} else {\n\t\t\t\tUnexpected(\"Corrupt forwarded information in message.\");\n\t\t\t}\n\t\t}();\n\n\t\tconst auto statusWidth = (from && _fromNameStatus)\n\t\t\t? st::dialogsPremiumIcon.icon.width()\n\t\t\t: 0;\n\t\tif (statusWidth && availableWidth > statusWidth) {\n\t\t\tconst auto x = availableLeft + std::min(\n\t\t\t\tavailableWidth - statusWidth,\n\t\t\t\tnameText->maxWidth()\n\t\t\t) - (_fromNameStatus->custom ? (2 * _fromNameStatus->skip) : 0);\n\t\t\tconst auto checkWidth = _fromNameStatus->custom\n\t\t\t\t? (st::emojiSize - 2 * _fromNameStatus->skip)\n\t\t\t\t: statusWidth;\n\t\t\tif (point.x() >= x && point.x() < x + checkWidth) {\n\t\t\t\tensureFromNameStatusLink(from);\n\t\t\t\toutResult->link = _fromNameStatus->link;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tavailableWidth -= statusWidth;\n\t\t}\n\t\tif (point.x() >= availableLeft\n\t\t\t&& point.x() < availableLeft + availableWidth\n\t\t\t&& point.x() < availableLeft + nameText->maxWidth()) {\n\t\t\toutResult->link = fromLink();\n\t\t\trecordLinkRipplePoint(point, trect.topLeft());\n\t\t\t_fromLinkRipplePointSet = 1;\n\t\t\treturn true;\n\t\t}\n\t\tauto via = item->Get<HistoryMessageVia>();\n\t\tif (via\n\t\t\t&& !displayForwardedFrom()\n\t\t\t&& point.x() >= availableLeft + nameText->maxWidth() + st::msgServiceFont->spacew\n\t\t\t&& point.x() < availableLeft + availableWidth\n\t\t\t&& point.x() < availableLeft + nameText->maxWidth() + st::msgServiceFont->spacew + via->width) {\n\t\t\toutResult->link = via->link;\n\t\t\trecordLinkRipplePoint(point, trect.topLeft());\n\t\t\treturn true;\n\t\t}\n\t\tif (badgeWidth) {\n\t\t\tconst auto badge = Get<RightBadge>();\n\t\t\tconst auto badgeLeft = trect.left()\n\t\t\t\t+ trect.width()\n\t\t\t\t- badgeWidth;\n\t\t\tconst auto badgeRight = trect.left()\n\t\t\t\t+ trect.width()\n\t\t\t\t+ st::msgPadding.right();\n\t\t\tconst auto boostTextWidth = (badge && !badge->boosts.isEmpty())\n\t\t\t\t? badge->boosts.maxWidth()\n\t\t\t\t: 0;\n\t\t\tconst auto boostLeft = boostTextWidth\n\t\t\t\t? (trect.left() + trect.width() - boostTextWidth)\n\t\t\t\t: 0;\n\t\t\tif (boostTextWidth\n\t\t\t\t&& point.x() >= boostLeft\n\t\t\t\t&& point.x() < badgeRight) {\n\t\t\t\tif (!badge->boostsLink) {\n\t\t\t\t\tconst auto fullId = item->fullId();\n\t\t\t\t\tbadge->boostsLink = std::make_shared<LambdaClickHandler>([\n\t\t\t\t\t\tfullId\n\t\t\t\t\t](ClickContext context) {\n\t\t\t\t\t\tif (const auto controller = ExtractController(context)) {\n\t\t\t\t\t\t\tif (const auto item = controller->session().data().message(fullId)) {\n\t\t\t\t\t\t\t\tif (const auto channel = item->history()->peer->asChannel()) {\n\t\t\t\t\t\t\t\t\tcontroller->resolveBoostState(channel);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\toutResult->link = badge->boostsLink;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst auto tagRight = boostTextWidth\n\t\t\t\t? (boostLeft - st::msgTagBadgeBoostSkip)\n\t\t\t\t: badgeRight;\n\t\t\tif (point.x() >= badgeLeft && point.x() < tagRight) {\n\t\t\t\tif (badge->special) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif (!badge->tagLink) {\n\t\t\t\t\tconst auto weak = base::make_weak(this);\n\t\t\t\t\tbadge->tagLink = std::make_shared<LambdaClickHandler>([\n\t\t\t\t\t\tweak\n\t\t\t\t\t](ClickContext context) {\n\t\t\t\t\t\tif (const auto controller = ExtractController(context)) {\n\t\t\t\t\t\t\tif (const auto view = weak.get()) {\n\t\t\t\t\t\t\t\tconst auto badge = view->Get<RightBadge>();\n\t\t\t\t\t\t\t\tif (!badge) {\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tconst auto item = view->data();\n\t\t\t\t\t\t\t\tconst auto peer = item->history()->peer;\n\t\t\t\t\t\t\t\tconst auto author = item->author();\n\t\t\t\t\t\t\t\tcontroller->uiShow()->show(Box(\n\t\t\t\t\t\t\t\t\tTagInfoBox,\n\t\t\t\t\t\t\t\t\tcontroller->uiShow(),\n\t\t\t\t\t\t\t\t\tpeer,\n\t\t\t\t\t\t\t\t\tauthor,\n\t\t\t\t\t\t\t\t\tbadge->tag.toString(),\n\t\t\t\t\t\t\t\t\tbadge->role));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\t{\n\t\t\t\t\tconst auto pill = ComputeBadgePillGeometry(badge);\n\t\t\t\t\tconst auto &padding = st::msgTagBadgePadding;\n\t\t\t\t\tif (badge->role != BadgeRole::User) {\n\t\t\t\t\t\tconst auto badgeTop = trect.top()\n\t\t\t\t\t\t\t+ (st::msgNameFont->height - pill.height) / 2;\n\t\t\t\t\t\tbadge->lastPoint = point\n\t\t\t\t\t\t\t- QPoint(badgeLeft, badgeTop);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst auto pillLeft = badgeLeft\n\t\t\t\t\t\t\t- (pill.width - pill.textWidth) / 2;\n\t\t\t\t\t\tconst auto pillTop = trect.top() - padding.top();\n\t\t\t\t\t\tbadge->lastPoint = point\n\t\t\t\t\t\t\t- QPoint(pillLeft, pillTop);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\toutResult->link = badge->tagLink;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\ttrect.setTop(trect.top() + st::msgNameFont->height);\n\treturn false;\n}\n\nvoid Message::ensureFromNameStatusLink(not_null<PeerData*> peer) const {\n\tExpects(_fromNameStatus != nullptr);\n\n\tif (_fromNameStatus->link) {\n\t\treturn;\n\t}\n\t_fromNameStatus->link = std::make_shared<LambdaClickHandler>([=](\n\t\t\tClickContext context) {\n\t\tconst auto controller = ExtractController(context);\n\t\tif (controller) {\n\t\t\tSettings::ShowEmojiStatusPremium(controller, peer);\n\t\t}\n\t});\n}\n\nbool Message::getStateTopicButton(\n\t\tQPoint point,\n\t\tQRect &trect,\n\t\tnot_null<TextState*> outResult) const {\n\tif (!displayedTopicButton()) {\n\t\treturn false;\n\t}\n\ttrect.setTop(trect.top() + st::topicButtonSkip);\n\tconst auto padding = st::topicButtonPadding;\n\tconst auto availableWidth = trect.width();\n\tconst auto height = padding.top()\n\t\t+ st::msgNameFont->height\n\t\t+ padding.bottom();\n\tconst auto width = std::max(\n\t\tstd::min(\n\t\t\tavailableWidth,\n\t\t\t(padding.left()\n\t\t\t\t+ _topicButton->name.maxWidth()\n\t\t\t\t+ st::topicButtonArrowSkip\n\t\t\t\t+ padding.right())),\n\t\theight);\n\tconst auto rect = QRect(trect.x(), trect.y(), width, height);\n\tif (rect.contains(point)) {\n\t\toutResult->link = _topicButton->link;\n\t\t_topicButton->lastPoint = point - rect.topLeft();\n\t\treturn true;\n\t}\n\ttrect.setY(trect.y() + height + st::topicButtonSkip);\n\treturn false;\n}\n\nbool Message::getStateForwardedInfo(\n\t\tQPoint point,\n\t\tQRect &trect,\n\t\tnot_null<TextState*> outResult,\n\t\tStateRequest request) const {\n\tif (!displayForwardedFrom()) {\n\t\treturn false;\n\t}\n\tconst auto item = data();\n\tconst auto forwarded = item->Get<HistoryMessageForwarded>();\n\tconst auto skip1 = forwarded->psaType.isEmpty()\n\t\t? 0\n\t\t: st::historyPsaIconSkip1;\n\tconst auto skip2 = forwarded->psaType.isEmpty()\n\t\t? 0\n\t\t: st::historyPsaIconSkip2;\n\tconst auto fits = (forwarded->text.maxWidth() <= (trect.width() - skip1));\n\tconst auto fwdheight = (fits ? 1 : 2) * st::semiboldFont->height;\n\tif (point.y() >= trect.top() && point.y() < trect.top() + fwdheight) {\n\t\tif (skip1) {\n\t\t\tconst auto &icon = st::historyPsaIconIn;\n\t\t\tconst auto position = fits\n\t\t\t\t? st::historyPsaIconPosition1\n\t\t\t\t: st::historyPsaIconPosition2;\n\t\t\tconst auto iconRect = QRect(\n\t\t\t\ttrect.x() + trect.width() - position.x() - icon.width(),\n\t\t\t\ttrect.y() + position.y(),\n\t\t\t\ticon.width(),\n\t\t\t\ticon.height());\n\t\t\tif (iconRect.contains(point)) {\n\t\t\t\tif (const auto link = psaTooltipLink()) {\n\t\t\t\t\toutResult->link = link;\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst auto useWidth = trect.width() - (fits ? skip1 : skip2);\n\t\tconst auto breakEverywhere = (forwarded->text.countHeight(useWidth) > 2 * st::semiboldFont->height);\n\t\tauto textRequest = request.forText();\n\t\tif (breakEverywhere) {\n\t\t\ttextRequest.flags |= Ui::Text::StateRequest::Flag::BreakEverywhere;\n\t\t}\n\t\t*outResult = TextState(item, forwarded->text.getState(\n\t\t\tpoint - trect.topLeft(),\n\t\t\tuseWidth,\n\t\t\ttextRequest));\n\t\tif (outResult->link) {\n\t\t\trecordLinkRipplePoint(point, trect.topLeft());\n\t\t}\n\t\toutResult->symbol = 0;\n\t\toutResult->afterSymbol = false;\n\t\tif (breakEverywhere) {\n\t\t\toutResult->cursor = CursorState::Forwarded;\n\t\t} else {\n\t\t\toutResult->cursor = CursorState::None;\n\t\t}\n\t\treturn true;\n\t}\n\ttrect.setTop(trect.top() + fwdheight);\n\treturn false;\n}\n\nClickHandlerPtr Message::psaTooltipLink() const {\n\tconst auto state = Get<PsaTooltipState>();\n\tif (!state || !state->buttonVisible) {\n\t\treturn nullptr;\n\t} else if (state->link) {\n\t\treturn state->link;\n\t}\n\tconst auto type = state->type;\n\tconst auto handler = [=] {\n\t\tconst auto custom = type.isEmpty()\n\t\t\t? QString()\n\t\t\t: Lang::GetNonDefaultValue(kPsaTooltipPrefix + type.toUtf8());\n\t\tauto text = tr::rich(\n\t\t\t(custom.isEmpty()\n\t\t\t\t? tr::lng_tooltip_psa_default(tr::now)\n\t\t\t\t: custom));\n\t\tTextUtilities::ParseEntities(text, 0);\n\t\tpsaTooltipToggled(true);\n\t\tdelegate()->elementShowTooltip(text, crl::guard(this, [=] {\n\t\t\tpsaTooltipToggled(false);\n\t\t}));\n\t};\n\tstate->link = std::make_shared<LambdaClickHandler>(\n\t\tcrl::guard(this, handler));\n\treturn state->link;\n}\n\nvoid Message::psaTooltipToggled(bool tooltipShown) const {\n\tconst auto visible = !tooltipShown;\n\tconst auto state = Get<PsaTooltipState>();\n\tif (state->buttonVisible == visible) {\n\t\treturn;\n\t}\n\tstate->buttonVisible = visible;\n\thistory()->owner().notifyViewLayoutChange(this);\n\tstate->buttonVisibleAnimation.start(\n\t\t[=] { repaint(); },\n\t\tvisible ? 0. : 1.,\n\t\tvisible ? 1. : 0.,\n\t\tst::fadeWrapDuration);\n}\n\nbool Message::getStateReplyInfo(\n\t\tQPoint point,\n\t\tQRect &trect,\n\t\tnot_null<TextState*> outResult) const {\n\tif (const auto reply = Get<Reply>()) {\n\t\tconst auto margins = reply->margins();\n\t\tconst auto height = reply->height();\n\t\tif (point.y() >= trect.top() && point.y() < trect.top() + height) {\n\t\t\tconst auto g = QRect(\n\t\t\t\ttrect.x(),\n\t\t\t\ttrect.y() + margins.top(),\n\t\t\t\ttrect.width(),\n\t\t\t\theight - margins.top() - margins.bottom());\n\t\t\tif (g.contains(point)) {\n\t\t\t\tif (const auto link = reply->link()) {\n\t\t\t\t\toutResult->link = reply->link();\n\t\t\t\t\treply->saveRipplePoint(point - g.topLeft());\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\ttrect.setTop(trect.top() + height);\n\t}\n\treturn false;\n}\n\nbool Message::getStateSummaryHeaderInfo(\n\t\tQPoint point,\n\t\tQRect &trect,\n\t\tnot_null<TextState*> outResult) const {\n\tif (const auto summaryHeader = Get<SummaryHeader>()) {\n\t\tconst auto margins = summaryHeader->margins();\n\t\tconst auto height = summaryHeader->height();\n\t\tif (point.y() >= trect.top() && point.y() < trect.top() + height) {\n\t\t\tconst auto g = QRect(\n\t\t\t\ttrect.x(),\n\t\t\t\ttrect.y() + margins.top(),\n\t\t\t\ttrect.width(),\n\t\t\t\theight - margins.top() - margins.bottom());\n\t\t\tif (g.contains(point)) {\n\t\t\t\tif (const auto link = summaryHeader->link()) {\n\t\t\t\t\toutResult->link = summaryHeader->link();\n\t\t\t\t\tsummaryHeader->saveRipplePoint(point - g.topLeft());\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\ttrect.setTop(trect.top() + height);\n\t}\n\treturn false;\n}\n\nbool Message::getStateViaBotIdInfo(\n\t\tQPoint point,\n\t\tQRect &trect,\n\t\tnot_null<TextState*> outResult) const {\n\tconst auto item = data();\n\tif (const auto via = item->Get<HistoryMessageVia>()) {\n\t\tif (!displayFromName() && !displayForwardedFrom()) {\n\t\t\tif (QRect(trect.x(), trect.y(), via->width, st::msgNameFont->height).contains(point)) {\n\t\t\t\toutResult->link = via->link;\n\t\t\t\trecordLinkRipplePoint(point, trect.topLeft());\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\ttrect.setTop(trect.top() + st::msgNameFont->height);\n\t\t}\n\t}\n\treturn false;\n}\n\nbool Message::getStateText(\n\t\tQPoint point,\n\t\tQRect &trect,\n\t\tnot_null<TextState*> outResult,\n\t\tStateRequest request) const {\n\tif (!hasVisibleText()) {\n\t\treturn false;\n\t} else if (const auto botTop = Get<FakeBotAboutTop>()) {\n\t\ttrect.setY(trect.y() + botTop->height);\n\t}\n\tconst auto item = this->textItem();\n\tif (base::in_range(point.y(), trect.y(), trect.y() + trect.height())) {\n\t\t*outResult = TextState(item, text().getState(\n\t\t\tpoint - trect.topLeft(),\n\t\t\ttrect.width(),\n\t\t\trequest.forText()));\n\t\tif (outResult->link\n\t\t\t&& IsRippleLink(outResult->link)\n\t\t\t&& !text().linkRangeFor(outResult->link).empty()) {\n\t\t\trecordLinkRipplePoint(point, trect.topLeft());\n\t\t}\n\t\treturn true;\n\t}\n\treturn false;\n}\n\n// Forward to media.\nvoid Message::updatePressed(QPoint point) {\n\tconst auto item = data();\n\tconst auto media = this->media();\n\tif (!media) {\n\t\treturn;\n\t}\n\n\tauto g = countGeometry();\n\n\tconst auto reactionsInBubble = _reactions && embedReactionsInBubble();\n\tif (_reactions && !reactionsInBubble) {\n\t\tconst auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height();\n\t\tg.setHeight(g.height() - reactionsHeight);\n\t}\n\n\tconst auto keyboard = item->inlineReplyKeyboard();\n\tif (keyboard) {\n\t\tauto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight();\n\t\tg.setHeight(g.height() - keyboardHeight);\n\t}\n\n\tif (drawBubble()) {\n\t\tauto mediaDisplayed = media && media->isDisplayed();\n\t\tauto trect = g.marginsAdded(-st::msgPadding);\n\t\tif (mediaDisplayed && media->isBubbleTop()) {\n\t\t\ttrect.setY(trect.y() - st::msgPadding.top());\n\t\t} else {\n\t\t\tif (displayFromName()) {\n\t\t\t\ttrect.setTop(trect.top() + st::msgNameFont->height);\n\t\t\t}\n\t\t\tif (displayedTopicButton()) {\n\t\t\t\ttrect.setTop(trect.top()\n\t\t\t\t\t+ st::topicButtonSkip\n\t\t\t\t\t+ st::topicButtonPadding.top()\n\t\t\t\t\t+ st::msgNameFont->height\n\t\t\t\t\t+ st::topicButtonPadding.bottom()\n\t\t\t\t\t+ st::topicButtonSkip);\n\t\t\t}\n\t\t\tif (displayForwardedFrom()) {\n\t\t\t\tauto forwarded = item->Get<HistoryMessageForwarded>();\n\t\t\t\tauto fwdheight = ((forwarded->text.maxWidth() > trect.width()) ? 2 : 1) * st::semiboldFont->height;\n\t\t\t\ttrect.setTop(trect.top() + fwdheight);\n\t\t\t}\n\t\t\tif (const auto reply = Get<Reply>()) {\n\t\t\t\ttrect.setTop(trect.top() + reply->height());\n\t\t\t}\n\t\t\tif (const auto summaryHeader = Get<SummaryHeader>()) {\n\t\t\t\ttrect.setTop(trect.top() + summaryHeader->height());\n\t\t\t}\n\t\t\tif (item->Has<HistoryMessageVia>()) {\n\t\t\t\tif (!displayFromName() && !displayForwardedFrom()) {\n\t\t\t\t\ttrect.setTop(trect.top() + st::msgNameFont->height);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (mediaDisplayed && media->isBubbleBottom()) {\n\t\t\ttrect.setHeight(trect.height() + st::msgPadding.bottom());\n\t\t}\n\n\t\tif (mediaDisplayed) {\n\t\t\tauto mediaHeight = media->height();\n\t\t\tauto mediaLeft = trect.x() - st::msgPadding.left();\n\t\t\tauto mediaTop = (trect.y() + trect.height() - mediaHeight);\n\t\t\tmedia->updatePressed(point - QPoint(mediaLeft, mediaTop));\n\t\t}\n\t} else {\n\t\tmedia->updatePressed(point - g.topLeft());\n\t}\n}\n\nTextForMimeData Message::selectedText(TextSelection selection) const {\n\tconst auto media = this->media();\n\tauto logEntryOriginalResult = TextForMimeData();\n\tauto factcheckResult = TextForMimeData();\n\tconst auto mediaDisplayed = (media && media->isDisplayed());\n\tconst auto mediaBefore = mediaDisplayed && invertMedia();\n\tconst auto textSelection = mediaBefore\n\t\t? media->skipSelection(selection)\n\t\t: selection;\n\tconst auto mediaSelection = !invertMedia()\n\t\t? skipTextSelection(selection)\n\t\t: selection;\n\tauto textResult = hasVisibleText()\n\t\t? text().toTextForMimeData(textSelection)\n\t\t: TextForMimeData();\n\tauto mediaResult = (mediaDisplayed || isHiddenByGroup())\n\t\t? media->selectedText(mediaSelection)\n\t\t: TextForMimeData();\n\tif (const auto check = factcheckBlock()) {\n\t\tconst auto checkSelection = mediaBefore\n\t\t\t? skipTextSelection(textSelection)\n\t\t\t: mediaDisplayed\n\t\t\t? media->skipSelection(mediaSelection)\n\t\t\t: skipTextSelection(selection);\n\t\tfactcheckResult = check->selectedText(checkSelection);\n\t}\n\tif (const auto entry = logEntryOriginal()) {\n\t\tconst auto originalSelection = mediaBefore\n\t\t\t? skipTextSelection(textSelection)\n\t\t\t: mediaDisplayed\n\t\t\t? media->skipSelection(mediaSelection)\n\t\t\t: skipTextSelection(selection);\n\t\tlogEntryOriginalResult = entry->selectedText(originalSelection);\n\t}\n\tauto &first = mediaBefore ? mediaResult : textResult;\n\tauto &second = mediaBefore ? textResult : mediaResult;\n\tauto result = first;\n\tif (result.empty()) {\n\t\tresult = std::move(second);\n\t} else if (!second.empty()) {\n\t\tresult.append(u\"\\n\\n\"_q).append(std::move(second));\n\t}\n\tif (result.empty()) {\n\t\tresult = std::move(factcheckResult);\n\t} else if (!factcheckResult.empty()) {\n\t\tresult.append(u\"\\n\\n\"_q).append(std::move(factcheckResult));\n\t}\n\tif (result.empty()) {\n\t\tresult = std::move(logEntryOriginalResult);\n\t} else if (!logEntryOriginalResult.empty()) {\n\t\tresult.append(u\"\\n\\n\"_q).append(std::move(logEntryOriginalResult));\n\t}\n\treturn result;\n}\n\nSelectedQuote Message::selectedQuote(TextSelection selection) const {\n\tconst auto textItem = this->textItem();\n\tconst auto item = textItem ? textItem : data().get();\n\tconst auto &translated = item->translatedText();\n\tconst auto &original = item->originalText();\n\tif (&translated != &original\n\t\t|| selection.empty()\n\t\t|| selection == FullSelection) {\n\t\treturn {};\n\t} else if (hasVisibleText()) {\n\t\tconst auto media = this->media();\n\t\tconst auto mediaDisplayed = media && media->isDisplayed();\n\t\tconst auto mediaBefore = mediaDisplayed && invertMedia();\n\t\tconst auto textSelection = mediaBefore\n\t\t\t? media->skipSelection(selection)\n\t\t\t: selection;\n\t\treturn FindSelectedQuote(text(), textSelection, item);\n\t} else if (const auto media = this->media()) {\n\t\tif (media->isDisplayed() || isHiddenByGroup()) {\n\t\t\treturn media->selectedQuote(selection);\n\t\t}\n\t}\n\treturn {};\n}\n\nTextSelection Message::selectionFromQuote(\n\t\tconst SelectedQuote &quote) const {\n\tExpects(quote.item != nullptr);\n\n\tif (quote.highlight.quote.empty()) {\n\t\treturn {};\n\t}\n\tconst auto item = quote.item;\n\tconst auto &translated = item->translatedText();\n\tconst auto &original = item->originalText();\n\tif (&translated != &original) {\n\t\treturn {};\n\t} else if (hasVisibleText()) {\n\t\tconst auto media = this->media();\n\t\tconst auto mediaDisplayed = media && media->isDisplayed();\n\t\tconst auto mediaBefore = mediaDisplayed && invertMedia();\n\t\tconst auto result = FindSelectionFromQuote(text(), quote);\n\t\treturn mediaBefore ? media->unskipSelection(result) : result;\n\t} else if (const auto media = this->media()) {\n\t\tif (media->isDisplayed() || isHiddenByGroup()) {\n\t\t\treturn media->selectionFromQuote(quote);\n\t\t}\n\t}\n\treturn {};\n}\n\nTextSelection Message::adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const {\n\tconst auto media = this->media();\n\tconst auto mediaDisplayed = media && media->isDisplayed();\n\tconst auto mediaBefore = mediaDisplayed && invertMedia();\n\tconst auto textSelection = mediaBefore\n\t\t? media->skipSelection(selection)\n\t\t: selection;\n\tconst auto useSelection = [](TextSelection selection, bool skipped) {\n\t\treturn !skipped || (selection != TextSelection(uint16(), uint16()));\n\t};\n\tauto textAdjusted = (hasVisibleText()\n\t\t&& useSelection(textSelection, mediaBefore))\n\t\t? text().adjustSelection(textSelection, type)\n\t\t: textSelection;\n\tauto textResult = mediaBefore\n\t\t? media->unskipSelection(textAdjusted)\n\t\t: textAdjusted;\n\tauto mediaResult = TextSelection();\n\tauto mediaSelection = mediaBefore\n\t\t? selection\n\t\t: skipTextSelection(selection);\n\tif (mediaDisplayed) {\n\t\tauto mediaAdjusted = useSelection(mediaSelection, !mediaBefore)\n\t\t\t? media->adjustSelection(mediaSelection, type)\n\t\t\t: mediaSelection;\n\t\tmediaResult = mediaBefore\n\t\t\t? mediaAdjusted\n\t\t\t: unskipTextSelection(mediaAdjusted);\n\t}\n\tauto checkResult = TextSelection();\n\tif (const auto check = factcheckBlock()) {\n\t\tauto checkSelection = !mediaDisplayed\n\t\t\t? skipTextSelection(selection)\n\t\t\t: mediaBefore\n\t\t\t? skipTextSelection(textSelection)\n\t\t\t: media->skipSelection(mediaSelection);\n\t\tauto checkAdjusted = useSelection(checkSelection, true)\n\t\t\t? check->adjustSelection(checkSelection, type)\n\t\t\t: checkSelection;\n\t\tcheckResult = unskipTextSelection(checkAdjusted);\n\t\tif (mediaDisplayed) {\n\t\t\tcheckResult = media->unskipSelection(checkResult);\n\t\t}\n\t}\n\tauto entryResult = TextSelection();\n\tif (const auto entry = logEntryOriginal()) {\n\t\tauto entrySelection = !mediaDisplayed\n\t\t\t? skipTextSelection(selection)\n\t\t\t: mediaBefore\n\t\t\t? skipTextSelection(textSelection)\n\t\t\t: media->skipSelection(mediaSelection);\n\t\tauto entryAdjusted = useSelection(entrySelection, true)\n\t\t\t? entry->adjustSelection(entrySelection, type)\n\t\t\t: entrySelection;\n\t\tentryResult = unskipTextSelection(entryAdjusted);\n\t\tif (mediaDisplayed) {\n\t\t\tentryResult = media->unskipSelection(entryResult);\n\t\t}\n\t}\n\tauto result = textResult;\n\tif (!mediaResult.empty()) {\n\t\tresult = result.empty() ? mediaResult : TextSelection{\n\t\t\tstd::min(result.from, mediaResult.from),\n\t\t\tstd::max(result.to, mediaResult.to),\n\t\t};\n\t}\n\tif (!checkResult.empty()) {\n\t\tresult = result.empty() ? checkResult : TextSelection{\n\t\t\tstd::min(result.from, checkResult.from),\n\t\t\tstd::max(result.to, checkResult.to),\n\t\t};\n\t}\n\tif (!entryResult.empty()) {\n\t\tresult = result.empty() ? entryResult : TextSelection{\n\t\t\tstd::min(result.from, entryResult.from),\n\t\t\tstd::max(result.to, entryResult.to),\n\t\t};\n\t}\n\treturn result;\n}\n\nReactions::ButtonParameters Message::reactionButtonParameters(\n\t\tQPoint position,\n\t\tconst TextState &reactionState) const {\n\tusing namespace Reactions;\n\tauto result = ButtonParameters{ .context = data()->fullId() };\n\tconst auto outsideBubble = (!_comments && !embedReactionsInBubble());\n\tconst auto geometry = countGeometry();\n\tresult.pointer = position;\n\tconst auto onTheLeft = hasRightLayout();\n\n\tconst auto keyboard = data()->inlineReplyKeyboard();\n\tconst auto keyboardHeight = keyboard\n\t\t? (st::msgBotKbButton.margin + keyboard->naturalHeight())\n\t\t: 0;\n\tconst auto reactionsHeight = (_reactions && !embedReactionsInBubble())\n\t\t? (st::mediaInBubbleSkip + _reactions->height())\n\t\t: 0;\n\tresult.reactionsHeight = reactionsHeight;\n\tconst auto innerHeight = geometry.height()\n\t\t- keyboardHeight\n\t\t- reactionsHeight;\n\tconst auto maybeRelativeCenter = outsideBubble\n\t\t? media()->reactionButtonCenterOverride()\n\t\t: std::nullopt;\n\tconst auto addOnTheRight = [&] {\n\t\treturn (maybeRelativeCenter\n\t\t\t|| !(displayFastShare() || displayGoToOriginal()))\n\t\t\t? st::reactionCornerCenter.x()\n\t\t\t: 0;\n\t};\n\tconst auto relativeCenter = QPoint(\n\t\tmaybeRelativeCenter.value_or(onTheLeft\n\t\t\t? -st::reactionCornerCenter.x()\n\t\t\t: (geometry.width() + addOnTheRight())),\n\t\tinnerHeight + st::reactionCornerCenter.y());\n\tresult.center = geometry.topLeft() + relativeCenter;\n\tif (reactionState.itemId != result.context\n\t\t&& !geometry.contains(position)) {\n\t\tresult.outside = true;\n\t}\n\tconst auto minSkip = (st::reactionCornerShadow.left()\n\t\t+ st::reactionCornerSize.width()\n\t\t+ st::reactionCornerShadow.right()) / 2;\n\tresult.center = QPoint(\n\t\tstd::min(std::max(result.center.x(), minSkip), width() - minSkip),\n\t\tresult.center.y());\n\treturn result;\n}\n\nReplyButton::ButtonParameters Message::replyButtonParameters(\n\t\tQPoint position,\n\t\tconst TextState &replyState) const {\n\tusing namespace ReplyButton;\n\tif (!displayFastReply() || unwrapped()) {\n\t\treturn {};\n\t}\n\tauto result = ButtonParameters{ .context = data()->fullId() };\n\tconst auto geometry = countGeometry();\n\tresult.pointer = position;\n\tconst auto reactionInnerRight = st::reactionCornerCenter.x()\n\t\t+ st::reactionCornerSize.width() / 2;\n\tconst auto replyInnerWidth = ReplyButton::ComputeInnerWidth();\n\tconst auto relativeCenter = QPoint(\n\t\tgeometry.width() + reactionInnerRight - replyInnerWidth,\n\t\tst::replyCornerCenter.y());\n\tresult.center = geometry.topLeft() + relativeCenter;\n\tif (replyState.itemId != result.context\n\t\t&& !geometry.contains(position)) {\n\t\tresult.outside = true;\n\t}\n\tresult.link = fastReplyLink();\n\treturn result;\n}\n\nint Message::reactionsOptimalWidth() const {\n\treturn _reactions ? _reactions->countNiceWidth() : 0;\n}\n\nvoid Message::drawInfo(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint right,\n\t\tint bottom,\n\t\tint width,\n\t\tInfoDisplayType type) const {\n\tp.setFont(st::msgDateFont);\n\n\tconst auto st = context.st;\n\tconst auto sti = context.imageStyle();\n\tconst auto stm = context.messageStyle();\n\tbool invertedsprites = (type == InfoDisplayType::Image)\n\t\t|| (type == InfoDisplayType::Background);\n\tint32 infoRight = right, infoBottom = bottom;\n\tswitch (type) {\n\tcase InfoDisplayType::Default:\n\t\tinfoRight -= st::msgPadding.right() - st::msgDateDelta.x();\n\t\tinfoBottom -= st::msgPadding.bottom() - st::msgDateDelta.y();\n\t\tp.setPen(stm->msgDateFg);\n\tbreak;\n\tcase InfoDisplayType::Image:\n\t\tinfoRight -= st::msgDateImgDelta + st::msgDateImgPadding.x();\n\t\tinfoBottom -= st::msgDateImgDelta + st::msgDateImgPadding.y();\n\t\tp.setPen(st->msgDateImgFg());\n\tbreak;\n\tcase InfoDisplayType::Background:\n\t\tinfoRight -= st::msgDateImgPadding.x();\n\t\tinfoBottom -= st::msgDateImgPadding.y();\n\t\tp.setPen(st->msgServiceFg());\n\tbreak;\n\t}\n\n\tconst auto size = _bottomInfo.currentSize();\n\tconst auto dateX = infoRight - size.width();\n\tconst auto dateY = infoBottom - size.height();\n\tif (type == InfoDisplayType::Image) {\n\t\tconst auto dateW = size.width() + 2 * st::msgDateImgPadding.x();\n\t\tconst auto dateH = size.height() + 2 * st::msgDateImgPadding.y();\n\t\tUi::FillRoundRect(p, dateX - st::msgDateImgPadding.x(), dateY - st::msgDateImgPadding.y(), dateW, dateH, sti->msgDateImgBg, sti->msgDateImgBgCorners);\n\t} else if (type == InfoDisplayType::Background) {\n\t\tconst auto dateW = size.width() + 2 * st::msgDateImgPadding.x();\n\t\tconst auto dateH = size.height() + 2 * st::msgDateImgPadding.y();\n\t\tUi::FillRoundRect(p, dateX - st::msgDateImgPadding.x(), dateY - st::msgDateImgPadding.y(), dateW, dateH, sti->msgServiceBg, sti->msgServiceBgCornersSmall);\n\t}\n\t_bottomInfo.paint(\n\t\tp,\n\t\t{ dateX, dateY },\n\t\twidth,\n\t\tdelegate()->elementShownUnread(this),\n\t\tinvertedsprites,\n\t\tcontext);\n}\n\nTextState Message::bottomInfoTextState(\n\t\tint right,\n\t\tint bottom,\n\t\tQPoint point,\n\t\tInfoDisplayType type) const {\n\tauto infoRight = right;\n\tauto infoBottom = bottom;\n\tswitch (type) {\n\tcase InfoDisplayType::Default:\n\t\tinfoRight -= st::msgPadding.right() - st::msgDateDelta.x();\n\t\tinfoBottom -= st::msgPadding.bottom() - st::msgDateDelta.y();\n\t\tbreak;\n\tcase InfoDisplayType::Image:\n\t\tinfoRight -= st::msgDateImgDelta + st::msgDateImgPadding.x();\n\t\tinfoBottom -= st::msgDateImgDelta + st::msgDateImgPadding.y();\n\t\tbreak;\n\tcase InfoDisplayType::Background:\n\t\tinfoRight -= st::msgDateImgPadding.x();\n\t\tinfoBottom -= st::msgDateImgPadding.y();\n\t\tbreak;\n\t}\n\tconst auto size = _bottomInfo.currentSize();\n\tconst auto infoLeft = infoRight - size.width();\n\tconst auto infoTop = infoBottom - size.height();\n\treturn _bottomInfo.textState(\n\t\tthis,\n\t\tpoint - QPoint{ infoLeft, infoTop });\n}\n\nint Message::infoWidth() const {\n\treturn _bottomInfo.maxWidth();\n}\n\nint Message::bottomInfoFirstLineWidth() const {\n\treturn _bottomInfo.firstLineWidth();\n}\n\nbool Message::bottomInfoIsWide() const {\n\tif (_reactions && embedReactionsInBubble()) {\n\t\treturn false;\n\t}\n\treturn _bottomInfo.isWide();\n}\n\nbool Message::isSignedAuthorElided() const {\n\treturn _bottomInfo.isSignedAuthorElided();\n}\n\nbool Message::embedReactionsInBubble() const {\n\treturn needInfoDisplay();\n}\n\nvoid Message::validateFromNameText(PeerData *from) const {\n\tif (!from) {\n\t\tif (_fromNameStatus) {\n\t\t\t_fromNameStatus = nullptr;\n\t\t}\n\t\treturn;\n\t}\n\tconst auto version = from->nameVersion();\n\tif (_fromNameVersion < version) {\n\t\t_fromNameVersion = version;\n\t\t_fromName.setText(\n\t\t\tst::msgNameStyle,\n\t\t\tfrom->name(),\n\t\t\tUi::NameTextOptions());\n\t}\n\tif (from->isPremium()\n\t\t|| (from->isChannel()\n\t\t\t&& from->emojiStatusId()\n\t\t\t&& from != history()->peer)) {\n\t\tif (!_fromNameStatus) {\n\t\t\t_fromNameStatus = std::make_unique<FromNameStatus>();\n\t\t\tconst auto size = st::emojiSize;\n\t\t\tconst auto emoji = Ui::Text::AdjustCustomEmojiSize(size);\n\t\t\t_fromNameStatus->skip = (size - emoji) / 2;\n\t\t}\n\t} else if (_fromNameStatus) {\n\t\t_fromNameStatus = nullptr;\n\t}\n}\n\nbool Message::updateBottomInfo() {\n\tconst auto wasInfo = _bottomInfo.currentSize();\n\t_bottomInfo.update(BottomInfoDataFromMessage(this), width());\n\treturn (_bottomInfo.currentSize() != wasInfo);\n}\n\nvoid Message::itemDataChanged() {\n\tconst auto infoChanged = updateBottomInfo();\n\tconst auto reactionsChanged = updateReactions();\n\tconst auto media = this->media();\n\tconst auto mediaChanged = media && media->updateItemData();\n\tif (infoChanged || reactionsChanged || mediaChanged) {\n\t\thistory()->owner().requestViewResize(this);\n\t} else {\n\t\trepaint();\n\t}\n}\n\nauto Message::verticalRepaintRange() const -> VerticalRepaintRange {\n\tconst auto media = this->media();\n\tconst auto add = media ? media->bubbleRollRepaintMargins() : QMargins();\n\treturn {\n\t\t.top = -add.top(),\n\t\t.height = height() + add.top() + add.bottom()\n\t};\n}\n\nvoid Message::refreshDataIdHook() {\n\tif (_rightAction && base::take(_rightAction->link)) {\n\t\t_rightAction->link = rightActionLink(_rightAction->lastPoint);\n\t}\n\tif (base::take(_fastReplyLink)) {\n\t\t_fastReplyLink = fastReplyLink();\n\t}\n\tif (_viewButton) {\n\t\t_viewButton = nullptr;\n\t\tupdateViewButtonExistence();\n\t}\n\tif (_comments) {\n\t\t_comments->link = nullptr;\n\t}\n}\n\nint Message::monospaceMaxWidth() const {\n\treturn st::msgPadding.left()\n\t\t+ (hasVisibleText() ? text().countMaxMonospaceWidth() : 0)\n\t\t+ st::msgPadding.right();\n}\n\nint Message::bubbleTextWidth(int bubbleWidth) const {\n\treturn std::max(bubbleWidth, st::msgMinWidth)\n\t\t- st::msgPadding.left()\n\t\t- st::msgPadding.right();\n}\n\nint Message::bubbleTextualWidth() const {\n\tconst auto full = textualMaxWidth();\n\tconst auto media = this->media();\n\tif (!hasVisibleText()\n\t\t|| !media\n\t\t|| !media->allowsNarrowBubble()) {\n\t\treturn full;\n\t}\n\tconst auto minimum = std::max(\n\t\tmedia->minBubbleWidthForNarrowBubble(),\n\t\tst::msgMinWidth);\n\tif (_bubbleTextualWidthMinimum != minimum) {\n\t\t_bubbleTextualWidthMinimum = minimum;\n\t\tif (minimum >= full) {\n\t\t\t_bubbleTextualWidthCache = minimum;\n\t\t} else {\n\t\t\tconst auto lineHeight = text().style()->font->height;\n\t\t\tconst auto fullTextHeight = textHeightFor(bubbleTextWidth(full));\n\t\t\tif (fullTextHeight > kMaxNiceToReadLines * lineHeight) {\n\t\t\t\t_bubbleTextualWidthCache = full;\n\t\t\t} else {\n\t\t\t\tauto left = minimum;\n\t\t\t\tauto right = full;\n\t\t\t\twhile (left < right) {\n\t\t\t\t\tconst auto middle = left + (right - left) / 2;\n\t\t\t\t\tconst auto middleHeight = textHeightFor(\n\t\t\t\t\t\tbubbleTextWidth(middle));\n\t\t\t\t\tif (middleHeight <= kMaxNiceToReadLines * lineHeight) {\n\t\t\t\t\t\tright = middle;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tleft = middle + 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t_bubbleTextualWidthCache = right;\n\t\t\t}\n\t\t}\n\t}\n\treturn _bubbleTextualWidthCache;\n}\n\nint Message::viewButtonHeight() const {\n\treturn _viewButton ? _viewButton->height() : 0;\n}\n\nvoid Message::updateViewButtonExistence() {\n\tconst auto item = data();\n\tconst auto media = item->media();\n\tconst auto has = (media && ViewButton::MediaHasViewButton(media));\n\tif (!has) {\n\t\t_viewButton = nullptr;\n\t\treturn;\n\t} else if (_viewButton) {\n\t\treturn;\n\t}\n\tauto make = [=](auto &&from) {\n\t\treturn std::make_unique<ViewButton>(\n\t\t\tstd::forward<decltype(from)>(from),\n\t\t\tcolorIndex(),\n\t\t\t[=] { repaint(); });\n\t};\n\t_viewButton = make(media);\n}\n\nvoid Message::initLogEntryOriginal() {\n\tif (const auto log = data()->Get<HistoryMessageLogEntryOriginal>()) {\n\t\tAddComponents(LogEntryOriginal::Bit());\n\t\tconst auto entry = Get<LogEntryOriginal>();\n\t\tusing Flags = MediaWebPageFlags;\n\t\tentry->page = std::make_unique<WebPage>(this, log->page, Flags());\n\t}\n}\n\nvoid Message::initPsa() {\n\tif (const auto forwarded = data()->Get<HistoryMessageForwarded>()) {\n\t\tif (!forwarded->psaType.isEmpty()) {\n\t\t\tAddComponents(PsaTooltipState::Bit());\n\t\t\tGet<PsaTooltipState>()->type = forwarded->psaType;\n\t\t}\n\t}\n}\n\nWebPage *Message::logEntryOriginal() const {\n\tif (const auto entry = Get<LogEntryOriginal>()) {\n\t\treturn entry->page.get();\n\t}\n\treturn nullptr;\n}\n\nWebPage *Message::factcheckBlock() const {\n\tif (const auto entry = Get<Factcheck>()) {\n\t\treturn entry->page.get();\n\t}\n\treturn nullptr;\n}\n\nbool Message::toggleSelectionByHandlerClick(\n\t\tconst ClickHandlerPtr &handler) const {\n\tif (_comments && _comments->link == handler) {\n\t\treturn true;\n\t} else if (_viewButton && _viewButton->link() == handler) {\n\t\treturn true;\n\t} else if (const auto media = this->media()) {\n\t\tif (media->toggleSelectionByHandlerClick(handler)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool Message::allowTextSelectionByHandler(\n\t\tconst ClickHandlerPtr &handler) const {\n\tif (const auto media = this->media()) {\n\t\tif (media->allowTextSelectionByHandler(handler)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\tif (dynamic_cast<Ui::Text::BlockquoteClickHandler*>(handler.get())) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool Message::hasFromName() const {\n\tswitch (context()) {\n\tcase Context::AdminLog:\n\t\treturn true;\n\tcase Context::Monoforum:\n\t\treturn data()->out() || data()->from()->isChannel();\n\tcase Context::History:\n\tcase Context::ChatPreview:\n\tcase Context::TTLViewer:\n\tcase Context::Pinned:\n\tcase Context::Replies:\n\tcase Context::SavedSublist:\n\tcase Context::ScheduledTopic: {\n\t\tconst auto item = data();\n\t\tconst auto peer = item->history()->peer;\n\t\tif (hasOutLayout() && !item->from()->isChannel()) {\n\t\t\tif (peer->isSelf()) {\n\t\t\t\tif (const auto forwarded = item->Get<HistoryMessageForwarded>()) {\n\t\t\t\t\treturn forwarded->savedFromSender\n\t\t\t\t\t\t&& forwarded->savedFromSender->isChannel();\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t} else if (!peer->isUser()) {\n\t\t\tif (const auto media = this->media()) {\n\t\t\t\treturn !media->hideFromName();\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tif (const auto forwarded = item->Get<HistoryMessageForwarded>()) {\n\t\t\tif (forwarded->imported\n\t\t\t\t&& peer.get() == forwarded->originalSender) {\n\t\t\t\treturn false;\n\t\t\t} else if (item->showForwardsFromSender(forwarded)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t} break;\n\tcase Context::ContactPreview:\n\tcase Context::ShortcutMessages:\n\t\treturn false;\n\t}\n\tUnexpected(\"Context in Message::hasFromName.\");\n}\n\nbool Message::displayFromName() const {\n\tif (!hasFromName() || isAttachedToPrevious() || data()->isSponsored()) {\n\t\treturn false;\n\t}\n\treturn !Has<PsaTooltipState>();\n}\n\nbool Message::displayForwardedFrom() const {\n\tconst auto item = data();\n\tif (const auto forwarded = item->Get<HistoryMessageForwarded>()) {\n\t\tif (forwarded->story) {\n\t\t\treturn true;\n\t\t} else if (item->showForwardsFromSender(forwarded)) {\n\t\t\treturn forwarded->savedFromHiddenSenderInfo\n\t\t\t\t|| (forwarded->savedFromSender\n\t\t\t\t\t&& (forwarded->savedFromSender\n\t\t\t\t\t\t!= forwarded->originalSender));\n\t\t}\n\t\tif (const auto sender = item->discussionPostOriginalSender()) {\n\t\t\tif (sender == forwarded->originalSender) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\tconst auto media = item->media();\n\t\treturn !media || !media->dropForwardedInfo();\n\t}\n\treturn false;\n}\n\nbool Message::hasOutLayout() const {\n\tconst auto item = data();\n\tif (item->history()->peer->isSelf()) {\n\t\tif (const auto forwarded = item->Get<HistoryMessageForwarded>()) {\n\t\t\tif (context() == Context::ShortcutMessages) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn (context() == Context::SavedSublist\n\t\t\t\t\t|| context() == Context::History)\n\t\t\t\t&& (!forwarded->forwardOfForward()\n\t\t\t\t\t? (forwarded->originalSender\n\t\t\t\t\t\t&& forwarded->originalSender->isSelf())\n\t\t\t\t\t: ((forwarded->savedFromSender\n\t\t\t\t\t\t&& forwarded->savedFromSender->isSelf())\n\t\t\t\t\t\t|| forwarded->savedFromOutgoing));\n\t\t}\n\t\treturn true;\n\t} else if (const auto forwarded = item->Get<HistoryMessageForwarded>()) {\n\t\tif (!forwarded->imported\n\t\t\t|| !forwarded->originalSender\n\t\t\t|| !forwarded->originalSender->isSelf()) {\n\t\t\tif (item->showForwardsFromSender(forwarded)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\treturn item->out() && !item->isPost();\n}\n\nbool Message::drawBubble() const {\n\tconst auto item = data();\n\tif (isHidden()) {\n\t\treturn false;\n\t} else if (logEntryOriginal()\n\t\t|| factcheckBlock()\n\t\t|| item->isFakeAboutView()) {\n\t\treturn true;\n\t}\n\tconst auto media = this->media();\n\treturn media\n\t\t? (hasVisibleText() || media->needsBubble())\n\t\t: !item->isEmpty();\n}\n\nbool Message::hasBubble() const {\n\treturn drawBubble();\n}\n\nTopicButton *Message::displayedTopicButton() const {\n\treturn _topicButton.get();\n}\n\nbool Message::unwrapped() const {\n\tconst auto item = data();\n\tif (isHidden()) {\n\t\treturn true;\n\t} else if (logEntryOriginal() || factcheckBlock()) {\n\t\treturn false;\n\t}\n\tconst auto media = this->media();\n\treturn media\n\t\t? (!hasVisibleText() && media->unwrapped())\n\t\t: item->isEmpty();\n}\n\nint Message::minWidthForMedia() const {\n\tauto result = infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x());\n\tconst auto views = data()->Get<HistoryMessageViews>();\n\tif (data()->repliesAreComments() && !views->replies.text.isEmpty()) {\n\t\tconst auto limit = HistoryMessageViews::kMaxRecentRepliers;\n\t\tconst auto single = st::historyCommentsUserpics.size;\n\t\tconst auto shift = st::historyCommentsUserpics.shift;\n\t\tconst auto added = single\n\t\t\t+ (limit - 1) * (single - shift)\n\t\t\t+ st::historyCommentsSkipLeft\n\t\t\t+ st::historyCommentsSkipRight\n\t\t\t+ st::historyCommentsSkipText\n\t\t\t+ st::historyCommentsOpenOutSelected.width()\n\t\t\t+ st::historyCommentsSkipRight\n\t\t\t+ st::mediaUnreadSkip\n\t\t\t+ st::mediaUnreadSize;\n\t\taccumulate_max(result, added + views->replies.textWidth);\n\t} else if (data()->externalReply()) {\n\t\tconst auto added = st::historyCommentsIn.width()\n\t\t\t+ st::historyCommentsSkipLeft\n\t\t\t+ st::historyCommentsSkipRight\n\t\t\t+ st::historyCommentsSkipText\n\t\t\t+ st::historyCommentsOpenOutSelected.width()\n\t\t\t+ st::historyCommentsSkipRight;\n\t\taccumulate_max(result, added + st::semiboldFont->width(\n\t\t\ttr::lng_replies_view_original(tr::now)));\n\t}\n\tif (const auto keyboard = data()->inlineReplyKeyboard()) {\n\t\taccumulate_max(result, keyboard->naturalWidth());\n\t}\n\treturn result;\n}\n\nbool Message::hasFastReply() const {\n\tif (context() == Context::Replies) {\n\t\tif (isCommentsRootView()) {\n\t\t\treturn false;\n\t\t}\n\t} else if (context() != Context::History) {\n\t\treturn false;\n\t}\n\tconst auto peer = data()->history()->peer;\n\treturn !hasOutLayout() && (peer->isChat() || peer->isMegagroup());\n}\n\nbool Message::displayFastReply() const {\n\tconst auto canSendAnything = [&] {\n\t\tconst auto item = data();\n\t\tconst auto peer = item->history()->peer;\n\t\tconst auto topic = item->topic();\n\t\treturn topic\n\t\t\t? Data::CanSendAnything(topic)\n\t\t\t: Data::CanSendAnything(peer);\n\t};\n\n\treturn hasFastReply()\n\t\t&& data()->isRegular()\n\t\t&& canSendAnything()\n\t\t&& !delegate()->elementInSelectionMode(this).inSelectionMode;\n}\n\nbool Message::displayRightActionComments() const {\n\treturn !isPinnedContext()\n\t\t&& (context() != Context::SavedSublist)\n\t\t&& data()->repliesAreComments()\n\t\t&& media()\n\t\t&& media()->isDisplayed()\n\t\t&& !hasBubble();\n}\n\nstd::optional<QSize> Message::rightActionSize() const {\n\tif (displayRightActionComments()) {\n\t\tconst auto views = data()->Get<HistoryMessageViews>();\n\t\tAssert(views != nullptr);\n\t\treturn (views->repliesSmall.textWidth > 0)\n\t\t\t? QSize(\n\t\t\t\tstd::max(\n\t\t\t\t\tst::historyFastShareSize,\n\t\t\t\t\t2 * st::historyFastShareBottom + views->repliesSmall.textWidth),\n\t\t\t\tst::historyFastShareSize + st::historyFastShareBottom + st::semiboldFont->height)\n\t\t\t: QSize(st::historyFastShareSize, st::historyFastShareSize);\n\t}\n\treturn data()->isSponsored()\n\t\t? ((_rightAction && _rightAction->second)\n\t\t\t? QSize(st::historyFastCloseSize, st::historyFastCloseSize * 2)\n\t\t\t: QSize(st::historyFastCloseSize, st::historyFastCloseSize))\n\t\t: (displayFastShare() || displayGoToOriginal())\n\t\t? QSize(st::historyFastShareSize, st::historyFastShareSize)\n\t\t: std::optional<QSize>();\n}\n\nbool Message::displayFastShare() const {\n\tconst auto item = data();\n\tconst auto peer = item->history()->peer;\n\tif (!item->allowsForward()) {\n\t\treturn false;\n\t} else if (peer->isChannel()) {\n\t\treturn !peer->isMegagroup();\n\t} else if (const auto user = peer->asUser()) {\n\t\tif (const auto forwarded = item->Get<HistoryMessageForwarded>()) {\n\t\t\treturn !item->out()\n\t\t\t\t&& forwarded->originalSender\n\t\t\t\t&& forwarded->originalSender->isBroadcast()\n\t\t\t\t&& !item->showForwardsFromSender(forwarded);\n\t\t} else if (user->isBot() && !item->out()) {\n\t\t\tif (const auto media = this->media()) {\n\t\t\t\treturn media->allowsFastShare();\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nbool Message::displayGoToOriginal() const {\n\tif (isPinnedContext()) {\n\t\treturn !hasOutLayout();\n\t} else if (context() == Context::Monoforum) {\n\t\treturn false;\n\t}\n\tconst auto item = data();\n\tif (const auto forwarded = item->Get<HistoryMessageForwarded>()) {\n\t\treturn forwarded->savedFromPeer\n\t\t\t&& forwarded->savedFromMsgId\n\t\t\t&& (!item->externalReply() || !hasBubble())\n\t\t\t&& (context() != Context::Replies);\n\t}\n\treturn false;\n}\n\nvoid Message::drawRightAction(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint left,\n\t\tint top,\n\t\tint outerWidth) const {\n\tensureRightAction();\n\n\tconst auto size = rightActionSize();\n\tconst auto st = context.st;\n\n\tif (_rightAction->ripple) {\n\t\tconst auto &stm = context.messageStyle();\n\t\tconst auto colorOverride = &stm->msgWaveformInactive->c;\n\t\t_rightAction->ripple->paint(\n\t\t\tp,\n\t\t\tleft,\n\t\t\ttop,\n\t\t\tsize->width(),\n\t\t\tcolorOverride);\n\t\tif (_rightAction->ripple->empty()) {\n\t\t\t_rightAction->ripple.reset();\n\t\t}\n\t}\n\tif (_rightAction->second && _rightAction->second->ripple) {\n\t\tconst auto &stm = context.messageStyle();\n\t\tconst auto colorOverride = &stm->msgWaveformInactive->c;\n\t\t_rightAction->second->ripple->paint(\n\t\t\tp,\n\t\t\tleft,\n\t\t\ttop + st::historyFastCloseSize,\n\t\t\tsize->width(),\n\t\t\tcolorOverride);\n\t\tif (_rightAction->second->ripple->empty()) {\n\t\t\t_rightAction->second->ripple.reset();\n\t\t}\n\t}\n\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(st->msgServiceBg());\n\t{\n\t\tPainterHighQualityEnabler hq(p);\n\t\tconst auto rect = style::rtlrect(\n\t\t\tleft,\n\t\t\ttop,\n\t\t\tsize->width(),\n\t\t\tsize->height(),\n\t\t\touterWidth);\n\t\tconst auto usual = st::historyFastShareSize;\n\t\tif (size->width() == size->height() && size->width() == usual) {\n\t\t\tp.drawEllipse(rect);\n\t\t} else {\n\t\t\tp.drawRoundedRect(rect, usual / 2, usual / 2);\n\t\t}\n\t}\n\tif (displayRightActionComments()) {\n\t\tconst auto &icon = st->historyFastCommentsIcon();\n\t\ticon.paint(\n\t\t\tp,\n\t\t\tleft + (size->width() - icon.width()) / 2,\n\t\t\ttop + (st::historyFastShareSize - icon.height()) / 2,\n\t\t\touterWidth);\n\t\tconst auto views = data()->Get<HistoryMessageViews>();\n\t\tAssert(views != nullptr);\n\t\tif (views->repliesSmall.textWidth > 0) {\n\t\t\tp.setPen(st->msgServiceFg());\n\t\t\tp.setFont(st::semiboldFont);\n\t\t\tp.drawTextLeft(\n\t\t\t\tleft + (size->width() - views->repliesSmall.textWidth) / 2,\n\t\t\t\ttop + st::historyFastShareSize,\n\t\t\t\touterWidth,\n\t\t\t\tviews->repliesSmall.text,\n\t\t\t\tviews->repliesSmall.textWidth);\n\t\t}\n\t} else if (_rightAction->second) {\n\t\tst->historyFastCloseIcon().paintInCenter(\n\t\t\tp,\n\t\t\tQRect(left, top, size->width(), size->width()));\n\t\tst->historyFastMoreIcon().paintInCenter(\n\t\t\tp,\n\t\t\tQRect(left, size->width() + top, size->width(), size->width()));\n\t} else {\n\t\tconst auto &icon = data()->isSponsored()\n\t\t\t? st->historyFastCloseIcon()\n\t\t\t: (displayFastShare()\n\t\t\t\t&& !isPinnedContext()\n\t\t\t\t&& this->context() != Context::SavedSublist)\n\t\t\t? st->historyFastShareIcon()\n\t\t\t: st->historyGoToOriginalIcon();\n\t\ticon.paintInCenter(p, Rect(left, top, *size));\n\t}\n}\n\nClickHandlerPtr Message::rightActionLink(\n\t\tstd::optional<QPoint> pressPoint) const {\n\tif (delegate()->elementInSelectionMode(this).progress > 0) {\n\t\treturn nullptr;\n\t}\n\tensureRightAction();\n\tif (!_rightAction->link) {\n\t\t_rightAction->link = prepareRightActionLink();\n\t}\n\tif (pressPoint) {\n\t\t_rightAction->lastPoint = *pressPoint;\n\t}\n\tif (_rightAction->second\n\t\t&& (_rightAction->lastPoint.y() > st::historyFastCloseSize)) {\n\t\treturn _rightAction->second->link;\n\t}\n\treturn _rightAction->link;\n}\n\nvoid Message::ensureRightAction() const {\n\tif (_rightAction) {\n\t\treturn;\n\t}\n\tAssert(rightActionSize().has_value());\n\t_rightAction = std::make_unique<RightAction>();\n}\n\nClickHandlerPtr Message::prepareRightActionLink() const {\n\tif (data()->isSponsored()) {\n\t\treturn HideSponsoredClickHandler();\n\t} else if (isPinnedContext()) {\n\t\treturn JumpToMessageClickHandler(data());\n\t} else if ((context() != Context::SavedSublist)\n\t\t&& displayRightActionComments()) {\n\t\treturn createGoToCommentsLink();\n\t}\n\tconst auto sessionId = data()->history()->session().uniqueId();\n\tconst auto owner = &data()->history()->owner();\n\tconst auto itemId = data()->fullId();\n\tconst auto forwarded = data()->Get<HistoryMessageForwarded>();\n\tconst auto savedFromPeer = forwarded\n\t\t? forwarded->savedFromPeer\n\t\t: nullptr;\n\tconst auto savedFromMsgId = forwarded ? forwarded->savedFromMsgId : 0;\n\n\tusing Callback = FnMut<void(not_null<Window::SessionController*>)>;\n\tconst auto showByThread = std::make_shared<Callback>();\n\tconst auto showByThreadWeak = std::weak_ptr<Callback>(showByThread);\n\tif (data()->externalReply()) {\n\t\t*showByThread = [=, requested = 0](\n\t\t\t\tnot_null<Window::SessionController*> controller) mutable {\n\t\t\tconst auto original = savedFromPeer->owner().message(\n\t\t\t\tsavedFromPeer,\n\t\t\t\tsavedFromMsgId);\n\t\t\tif (original && original->replyToTop()) {\n\t\t\t\tcontroller->showRepliesForMessage(\n\t\t\t\t\toriginal->history(),\n\t\t\t\t\toriginal->replyToTop(),\n\t\t\t\t\toriginal->id,\n\t\t\t\t\tWindow::SectionShow::Way::Forward);\n\t\t\t} else if (!requested) {\n\t\t\t\tconst auto prequested = &requested;\n\t\t\t\trequested = 1;\n\t\t\t\tsavedFromPeer->session().api().requestMessageData(\n\t\t\t\t\tsavedFromPeer,\n\t\t\t\t\tsavedFromMsgId,\n\t\t\t\t\t[=, weak = base::make_weak(controller)] {\n\t\t\t\t\t\tif (const auto strong = showByThreadWeak.lock()) {\n\t\t\t\t\t\t\tif (const auto strongController = weak.get()) {\n\t\t\t\t\t\t\t\t*prequested = 2;\n\t\t\t\t\t\t\t\t(*strong)(strongController);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t} else if (requested == 2) {\n\t\t\t\tcontroller->showPeerHistory(\n\t\t\t\t\tsavedFromPeer,\n\t\t\t\t\tWindow::SectionShow::Way::Forward,\n\t\t\t\t\tsavedFromMsgId);\n\t\t\t}\n\t\t};\n\t};\n\n\tclass FastShareClickHandler : public LambdaClickHandler {\n\tpublic:\n\t\tFastShareClickHandler(Fn<void(ClickContext)> handler)\n\t\t\t: LambdaClickHandler(std::move(handler)) {}\n\t\tQString tooltip() const override {\n\t\t\treturn tr::lng_fast_share_tooltip(tr::now);\n\t\t}\n\t};\n\n\tconst auto result = std::make_shared<FastShareClickHandler>([=](\n\t\t\tClickContext context) {\n\t\tconst auto controller = ExtractController(context);\n\t\tif (!controller || controller->session().uniqueId() != sessionId) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (const auto item = owner->message(itemId)) {\n\t\t\tif (*showByThread) {\n\t\t\t\t(*showByThread)(controller);\n\t\t\t} else if (savedFromPeer && savedFromMsgId) {\n\t\t\t\tcontroller->showPeerHistory(\n\t\t\t\t\tsavedFromPeer,\n\t\t\t\t\tWindow::SectionShow::Way::Forward,\n\t\t\t\t\tsavedFromMsgId);\n\t\t\t} else if (base::IsCtrlPressed()) {\n\t\t\t\tFastShareMessageToSelf(controller->uiShow(), item);\n\t\t\t} else {\n\t\t\t\tFastShareMessage(controller, item);\n\t\t\t}\n\t\t}\n\t});\n\tresult->setProperty(kFastShareProperty, QVariant::fromValue(true));\n\treturn result;\n}\n\nClickHandlerPtr Message::fastReplyLink() const {\n\tif (_fastReplyLink) {\n\t\treturn _fastReplyLink;\n\t}\n\tconst auto itemId = data()->fullId();\n\t_fastReplyLink = std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {\n\t\tdelegate()->elementReplyTo({ itemId });\n\t}));\n\treturn _fastReplyLink;\n}\n\nbool Message::isPinnedContext() const {\n\treturn context() == Context::Pinned;\n}\n\nvoid Message::updateMediaInBubbleState() {\n\tconst auto item = data();\n\tconst auto media = this->media();\n\n\tif (media) {\n\t\tmedia->updateNeedBubbleState();\n\t}\n\tconst auto reactionsInBubble = (_reactions && embedReactionsInBubble());\n\tauto mediaHasSomethingBelow = (_viewButton != nullptr)\n\t\t|| reactionsInBubble\n\t\t|| (invertMedia() && hasVisibleText());\n\tauto mediaHasSomethingAbove = false;\n\tauto getMediaHasSomethingAbove = [&] {\n\t\treturn displayFromName()\n\t\t\t|| displayedTopicButton()\n\t\t\t|| displayForwardedFrom()\n\t\t\t|| Has<Reply>()\n\t\t\t|| item->Has<HistoryMessageVia>();\n\t};\n\tconst auto entry = logEntryOriginal();\n\tconst auto check = factcheckBlock();\n\tif (check) {\n\t\tmediaHasSomethingBelow = true;\n\t\tmediaHasSomethingAbove = getMediaHasSomethingAbove();\n\t\tauto checkState = (mediaHasSomethingAbove\n\t\t\t|| hasVisibleText()\n\t\t\t|| (media && media->isDisplayed()))\n\t\t\t? MediaInBubbleState::Bottom\n\t\t\t: MediaInBubbleState::None;\n\t\tcheck->setInBubbleState(checkState);\n\t\tif (!media) {\n\t\t\tcheck->setBubbleRounding(countBubbleRounding());\n\t\t\treturn;\n\t\t}\n\t} else if (entry) {\n\t\tmediaHasSomethingBelow = true;\n\t\tmediaHasSomethingAbove = getMediaHasSomethingAbove();\n\t\tauto entryState = (mediaHasSomethingAbove\n\t\t\t|| hasVisibleText()\n\t\t\t|| (media && media->isDisplayed()))\n\t\t\t? MediaInBubbleState::Bottom\n\t\t\t: MediaInBubbleState::None;\n\t\tentry->setInBubbleState(entryState);\n\t\tif (!media) {\n\t\t\tentry->setBubbleRounding(countBubbleRounding());\n\t\t\treturn;\n\t\t}\n\t} else if (!media) {\n\t\treturn;\n\t}\n\n\tconst auto guard = gsl::finally([&] {\n\t\tmedia->setBubbleRounding(countBubbleRounding());\n\t});\n\tif (!drawBubble()) {\n\t\tmedia->setInBubbleState(MediaInBubbleState::None);\n\t\treturn;\n\t}\n\n\tif (!check && !entry) {\n\t\tmediaHasSomethingAbove = getMediaHasSomethingAbove();\n\t}\n\tif (!invertMedia() && hasVisibleText()) {\n\t\tmediaHasSomethingAbove = true;\n\t}\n\tconst auto state = [&] {\n\t\tif (mediaHasSomethingAbove) {\n\t\t\tif (mediaHasSomethingBelow) {\n\t\t\t\treturn MediaInBubbleState::Middle;\n\t\t\t}\n\t\t\treturn MediaInBubbleState::Bottom;\n\t\t} else if (mediaHasSomethingBelow) {\n\t\t\treturn MediaInBubbleState::Top;\n\t\t}\n\t\treturn MediaInBubbleState::None;\n\t}();\n\tmedia->setInBubbleState(state);\n}\n\nvoid Message::fromNameUpdated(int width) const {\n\tconst auto item = data();\n\tif (Has<RightBadge>()) {\n\t\twidth -= st::msgPadding.right() + rightBadgeWidth();\n\t}\n\tconst auto from = item->displayFrom();\n\tvalidateFromNameText(from);\n\tif (const auto via = item->Get<HistoryMessageVia>()) {\n\t\tif (!displayForwardedFrom()) {\n\t\t\tconst auto nameText = [&]() -> const Ui::Text::String * {\n\t\t\t\tif (from) {\n\t\t\t\t\treturn &_fromName;\n\t\t\t\t} else if (const auto info = item->originalHiddenSenderInfo()) {\n\t\t\t\t\treturn &info->nameText();\n\t\t\t\t} else {\n\t\t\t\t\tUnexpected(\"Corrupted forwarded information in message.\");\n\t\t\t\t}\n\t\t\t}();\n\t\t\tvia->resize(width\n\t\t\t\t- st::msgPadding.left()\n\t\t\t\t- st::msgPadding.right()\n\t\t\t\t- nameText->maxWidth()\n\t\t\t\t- (_fromNameStatus\n\t\t\t\t\t? (st::dialogsPremiumIcon.icon.width()\n\t\t\t\t\t\t+ st::msgServiceFont->spacew)\n\t\t\t\t\t: 0)\n\t\t\t\t- st::msgServiceFont->spacew);\n\t\t}\n\t}\n}\n\nTextSelection Message::skipTextSelection(TextSelection selection) const {\n\tif (selection.from == 0xFFFF || !hasVisibleText()) {\n\t\treturn selection;\n\t}\n\treturn HistoryView::UnshiftItemSelection(selection, text());\n}\n\nTextSelection Message::unskipTextSelection(TextSelection selection) const {\n\tif (!hasVisibleText()) {\n\t\treturn selection;\n\t}\n\treturn HistoryView::ShiftItemSelection(selection, text());\n}\n\nQRect Message::innerGeometry() const {\n\tauto result = countGeometry();\n\tif (!hasOutLayout()) {\n\t\tconst auto w = std::max(\n\t\t\t(media() ? media()->resolveCustomInfoRightBottom().x() : 0),\n\t\t\tresult.width());\n\t\tresult.setWidth(std::min(\n\t\t\tw + rightActionSize().value_or(QSize(0, 0)).width() * 2,\n\t\t\twidth()));\n\t}\n\tif (hasBubble()) {\n\t\tconst auto cut = [&](int amount) {\n\t\t\tamount = std::min(amount, result.height());\n\t\t\tresult.setTop(result.top() + amount);\n\t\t};\n\t\tcut(st::msgPadding.top() + st::mediaInBubbleSkip);\n\n\t\tif (displayFromName()) {\n\t\t\t// See paintFromName().\n\t\t\tcut(st::msgNameFont->height);\n\t\t}\n\t\tif (displayedTopicButton()) {\n\t\t\tcut(st::topicButtonSkip\n\t\t\t\t+ st::topicButtonPadding.top()\n\t\t\t\t+ st::msgNameFont->height\n\t\t\t\t+ st::topicButtonPadding.bottom()\n\t\t\t\t+ st::topicButtonSkip);\n\t\t}\n\t\tif (!displayFromName() && !displayForwardedFrom()) {\n\t\t\t// See paintViaBotIdInfo().\n\t\t\tif (data()->Has<HistoryMessageVia>()) {\n\t\t\t\tcut(st::msgServiceNameFont->height);\n\t\t\t}\n\t\t}\n\t\t// Skip displayForwardedFrom() until there are no animations for it.\n\t\tif (const auto reply = Get<Reply>()) {\n\t\t\t// See paintReplyInfo().\n\t\t\tcut(reply->height());\n\t\t}\n\t}\n\treturn result;\n}\n\nQPoint Message::mediaTopLeft() const {\n\treturn _lastMediaPosition;\n}\n\nbool Message::isCommentsRootView() const {\n\treturn context() == Context::Replies\n\t\t&& data()->isDiscussionPost()\n\t\t&& !data()->history()->isForum();\n}\n\nQRect Message::countGeometry() const {\n\tconst auto item = data();\n\tconst auto centeredView = item->isFakeAboutView()\n\t\t|| isCommentsRootView();\n\tconst auto media = this->media();\n\tconst auto mediaDisplayed = media && media->isDisplayed();\n\tconst auto mediaWidth = mediaDisplayed ? media->width() : width();\n\tconst auto outbg = hasOutLayout();\n\tconst auto useMoreSpace = (delegate()->elementChatMode()\n\t\t== ElementChatMode::Narrow);\n\tconst auto wideSkip = useMoreSpace\n\t\t? st::msgMargin.left()\n\t\t: st::msgMargin.right();\n\tconst auto availableWidth = width()\n\t\t- st::msgMargin.left()\n\t\t- (centeredView ? st::msgMargin.left() : wideSkip);\n\tauto contentLeft = hasRightLayout() ? wideSkip : st::msgMargin.left();\n\tauto contentWidth = availableWidth;\n\tif (hasFromPhoto()) {\n\t\tcontentLeft += st::msgPhotoSkip;\n\t\tif (const auto size = rightActionSize()) {\n\t\t\tcontentWidth -= size->width() + (st::msgPhotoSkip - st::historyFastShareSize);\n\t\t}\n\t//} else if (!Adaptive::Wide() && !out() && !fromChannel() && st::msgPhotoSkip - (hmaxwidth - hwidth) > 0) {\n\t//\tcontentLeft += st::msgPhotoSkip - (hmaxwidth - hwidth);\n\t}\n\taccumulate_min(contentWidth, maxWidth());\n\taccumulate_min(contentWidth, int(_bubbleWidthLimit));\n\tif (mediaWidth < contentWidth) {\n\t\tconst auto textualWidth = bubbleTextualWidth();\n\t\tif (mediaWidth < textualWidth\n\t\t\t&& (!media || !media->enforceBubbleWidth())) {\n\t\t\taccumulate_min(contentWidth, textualWidth);\n\t\t} else {\n\t\t\tcontentWidth = mediaWidth;\n\t\t}\n\t} else if (!mediaDisplayed) {\n\t\tconst auto appearing = Get<TextAppearing>();\n\t\tconst auto use = (appearing && appearing->use)\n\t\t\t? appearing->shownWidth\n\t\t\t: textRealWidth();\n\t\tif (use > 0) {\n\t\t\tconst auto shrunk = std::max(\n\t\t\t\tuse + st::msgPadding.left() + st::msgPadding.right(),\n\t\t\t\tint(_nonTextMaxWidth));\n\t\t\taccumulate_min(contentWidth, shrunk);\n\t\t}\n\t}\n\tif (contentWidth < availableWidth\n\t\t&& delegate()->elementChatMode() != ElementChatMode::Wide) {\n\t\tif (outbg) {\n\t\t\tcontentLeft += availableWidth - contentWidth;\n\t\t} else if (centeredView) {\n\t\t\tcontentLeft += (availableWidth - contentWidth) / 2;\n\t\t}\n\t} else if (contentWidth < availableWidth && centeredView) {\n\t\tcontentLeft += std::max(\n\t\t\t((st::msgMaxWidth + 2 * st::msgPhotoSkip) - contentWidth) / 2,\n\t\t\t0);\n\t}\n\n\tconst auto contentTop = marginTop();\n\treturn QRect(\n\t\tcontentLeft,\n\t\tcontentTop,\n\t\tcontentWidth,\n\t\theight() - contentTop - marginBottom());\n}\n\nUi::BubbleRounding Message::countMessageRounding() const {\n\tconst auto smallTop = isBubbleAttachedToPrevious();\n\tconst auto smallBottom = isBubbleAttachedToNext();\n\tconst auto media = smallBottom ? nullptr : this->media();\n\tconst auto item = data();\n\tconst auto keyboard = item->inlineReplyKeyboard();\n\tconst auto skipTail = smallBottom\n\t\t|| (media && media->skipBubbleTail())\n\t\t|| (keyboard != nullptr)\n\t\t|| item->isFakeAboutView()\n\t\t|| isCommentsRootView();\n\tconst auto right = hasRightLayout();\n\tusing Corner = Ui::BubbleCornerRounding;\n\treturn Ui::BubbleRounding{\n\t\t.topLeft = (smallTop && !right) ? Corner::Small : Corner::Large,\n\t\t.topRight = (smallTop && right) ? Corner::Small : Corner::Large,\n\t\t.bottomLeft = ((smallBottom && !right)\n\t\t\t? Corner::Small\n\t\t\t: (!skipTail && !right)\n\t\t\t? Corner::Tail\n\t\t\t: Corner::Large),\n\t\t.bottomRight = ((smallBottom && right)\n\t\t\t? Corner::Small\n\t\t\t: (!skipTail && right)\n\t\t\t? Corner::Tail\n\t\t\t: Corner::Large),\n\t};\n}\n\nUi::BubbleRounding Message::countBubbleRounding(\n\t\tUi::BubbleRounding messageRounding) const {\n\tif ([[maybe_unused]] const auto _ = data()->inlineReplyKeyboard()) {\n\t\tmessageRounding.bottomLeft\n\t\t\t= messageRounding.bottomRight\n\t\t\t= Ui::BubbleCornerRounding::Small;\n\t}\n\treturn messageRounding;\n}\n\nUi::BubbleRounding Message::countBubbleRounding() const {\n\treturn countBubbleRounding(countMessageRounding());\n}\n\nint Message::resizeContentGetHeight(int newWidth) {\n\tif (isHidden()) {\n\t\treturn marginTop() + marginBottom();\n\t} else if (newWidth < st::msgMinWidth) {\n\t\treturn height();\n\t}\n\n\tconst auto item = data();\n\tconst auto postShowingAuthor = item->isPostShowingAuthor() ? 1 : 0;\n\tif (_postShowingAuthor != postShowingAuthor) {\n\t\t_postShowingAuthor = postShowingAuthor;\n\t\t_fromNameVersion = 0;\n\t\tpreviousInBlocksChanged();\n\n\t\tconst auto size = _bottomInfo.currentSize();\n\t\t_bottomInfo.update(BottomInfoDataFromMessage(this), newWidth);\n\t\tif (size != _bottomInfo.currentSize()) {\n\t\t\t// maxWidth may have changed, full recount required.\n\t\t\tsetPendingResize();\n\t\t\treturn resizeGetHeight(newWidth);\n\t\t}\n\t}\n\n\tconst auto media = this->media();\n\tconst auto mediaDisplayed = media ? media->isDisplayed() : false;\n\tconst auto bubble = drawBubble();\n\n\titem->resolveDependent();\n\n\t// This code duplicates countGeometry() but also resizes media.\n\tconst auto centeredView = item->isFakeAboutView()\n\t\t|| isCommentsRootView();\n\tconst auto useMoreSpace = (delegate()->elementChatMode()\n\t\t== ElementChatMode::Narrow);\n\tconst auto wideSkip = useMoreSpace\n\t\t? st::msgMargin.left()\n\t\t: st::msgMargin.right();\n\tauto contentWidth = newWidth\n\t\t- st::msgMargin.left()\n\t\t- (centeredView ? st::msgMargin.left() : wideSkip);\n\tif (hasFromPhoto()) {\n\t\tif (const auto size = rightActionSize()) {\n\t\t\tcontentWidth -= size->width() + (st::msgPhotoSkip - st::historyFastShareSize);\n\t\t}\n\t}\n\taccumulate_min(contentWidth, maxWidth());\n\t_bubbleWidthLimit = std::max(st::msgMaxWidth, monospaceMaxWidth());\n\taccumulate_min(contentWidth, int(_bubbleWidthLimit));\n\tconst auto textualWidth = bubbleTextualWidth();\n\tif (mediaDisplayed) {\n\t\tmedia->resizeGetHeight(contentWidth);\n\t\tif (media->width() < contentWidth) {\n\t\t\tif (media->width() < textualWidth\n\t\t\t\t&& !media->enforceBubbleWidth()) {\n\t\t\t\taccumulate_min(contentWidth, textualWidth);\n\t\t\t} else {\n\t\t\t\tcontentWidth = media->width();\n\t\t\t}\n\t\t}\n\t}\n\tif (!mediaDisplayed && bubble && hasVisibleText()) {\n\t\tconst auto probeTextWidth = bubbleTextWidth(contentWidth);\n\t\t[[maybe_unused]] const auto probeHeight = textHeightFor(probeTextWidth);\n\t\tif (!Get<TextAppearing>()) {\n\t\t\tconst auto use = textRealWidth();\n\t\t\tif (use > 0) {\n\t\t\t\tconst auto shrunk = std::max(\n\t\t\t\t\tuse + st::msgPadding.left() + st::msgPadding.right(),\n\t\t\t\t\tint(_nonTextMaxWidth));\n\t\t\t\taccumulate_min(contentWidth, shrunk);\n\t\t\t}\n\t\t}\n\t}\n\tconst auto bottomInfoWidth = qMax(\n\t\tcontentWidth - st::msgPadding.left() - st::msgPadding.right(),\n\t\t1);\n\tconst auto textWidth = bubble\n\t\t? bubbleTextWidth(contentWidth)\n\t\t: bottomInfoWidth;\n\n\tauto appearing = Get<TextAppearing>();\n\tif (appearing) {\n\t\tif (appearing->textWidth != textWidth) {\n\t\t\tappearing->geometryValid = false;\n\t\t\tappearing->textWidth = textWidth;\n\t\t}\n\t\t// This may invalidate composer structure by removing TextAppearing.\n\t\tif (!textAppearValidate(appearing)) {\n\t\t\tappearing = nullptr;\n\t\t}\n\t}\n\n\tconst auto reactionsInBubble = _reactions && embedReactionsInBubble();\n\tconst auto bottomInfoHeight = _bottomInfo.resizeGetHeight(\n\t\tstd::min(\n\t\t\t_bottomInfo.maxWidth(),\n\t\t\tbottomInfoWidth - 2 * st::msgDateDelta.x()));\n\n\tauto newHeight = minHeight();\n\n\tif (const auto service = Get<ServicePreMessage>()) {\n\t\tservice->resizeToWidth(newWidth, delegate()->elementChatMode());\n\t}\n\n\tconst auto botTop = item->isFakeAboutView()\n\t\t? Get<FakeBotAboutTop>()\n\t\t: nullptr;\n\tif (bubble) {\n\t\tauto reply = Get<Reply>();\n\t\tauto via = item->Get<HistoryMessageVia>();\n\t\tconst auto check = factcheckBlock();\n\t\tconst auto entry = logEntryOriginal();\n\n\t\t// Entry page is always a bubble bottom.\n\t\tauto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/);\n\t\tauto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop());\n\n\t\tif (reactionsInBubble) {\n\t\t\t_reactions->resizeGetHeight(textWidth);\n\t\t}\n\t\tif (contentWidth == maxWidth() && !appearing) {\n\t\t\tif (mediaDisplayed) {\n\t\t\t\tif (check) {\n\t\t\t\t\tnewHeight += check->resizeGetHeight(contentWidth) + st::mediaInBubbleSkip;\n\t\t\t\t}\n\t\t\t\tif (entry) {\n\t\t\t\t\tnewHeight += entry->resizeGetHeight(contentWidth);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (check) {\n\t\t\t\t\tcheck->resizeGetHeight(contentWidth);\n\t\t\t\t}\n\t\t\t\tif (entry) {\n\t\t\t\t\t// In case of text-only message it is counted in minHeight already.\n\t\t\t\t\tentry->resizeGetHeight(contentWidth);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto withVisibleText = hasVisibleText();\n\t\t\tnewHeight = 0;\n\t\t\tif (withVisibleText) {\n\t\t\t\tif (botTop) {\n\t\t\t\t\tnewHeight += botTop->height;\n\t\t\t\t}\n\t\t\t\tif (appearing) {\n\t\t\t\t\tnewHeight += appearing->shownHeight;\n\t\t\t\t} else {\n\t\t\t\t\tnewHeight += textHeightFor(textWidth);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!mediaOnBottom && (!_viewButton || !reactionsInBubble)) {\n\t\t\t\tnewHeight += st::msgPadding.bottom();\n\t\t\t\tif (mediaDisplayed) {\n\t\t\t\t\tnewHeight += st::mediaInBubbleSkip;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!mediaOnTop) {\n\t\t\t\tnewHeight += st::msgPadding.top();\n\t\t\t\tif (mediaDisplayed) newHeight += st::mediaInBubbleSkip;\n\t\t\t\tif (entry) newHeight += st::mediaInBubbleSkip;\n\t\t\t}\n\t\t\tif (mediaDisplayed) {\n\t\t\t\tnewHeight += media->height();\n\t\t\t}\n\t\t\tif (check) {\n\t\t\t\tnewHeight += check->resizeGetHeight(contentWidth) + st::mediaInBubbleSkip;\n\t\t\t}\n\t\t\tif (entry) {\n\t\t\t\tnewHeight += entry->resizeGetHeight(contentWidth);\n\t\t\t}\n\t\t\tif (reactionsInBubble) {\n\t\t\t\tif (mediaDisplayed\n\t\t\t\t\t&& !media->additionalInfoString().isEmpty()) {\n\t\t\t\t\t// In round videos in a web page status text is painted\n\t\t\t\t\t// in the bottom left corner, reactions should be below.\n\t\t\t\t\tnewHeight += st::msgDateFont->height;\n\t\t\t\t} else {\n\t\t\t\t\tnewHeight += st::mediaInBubbleSkip;\n\t\t\t\t}\n\t\t\t\tnewHeight += _reactions->height();\n\t\t\t}\n\t\t}\n\n\t\tif (displayFromName()) {\n\t\t\tfromNameUpdated(contentWidth);\n\t\t\tnewHeight += st::msgNameFont->height;\n\t\t} else if (via && !displayForwardedFrom()) {\n\t\t\tvia->resize(contentWidth - st::msgPadding.left() - st::msgPadding.right());\n\t\t\tnewHeight += st::msgNameFont->height;\n\t\t}\n\n\t\tif (displayedTopicButton()) {\n\t\t\tnewHeight += st::topicButtonSkip\n\t\t\t\t+ st::topicButtonPadding.top()\n\t\t\t\t+ st::msgNameFont->height\n\t\t\t\t+ st::topicButtonPadding.bottom()\n\t\t\t\t+ st::topicButtonSkip;\n\t\t}\n\n\t\tif (displayForwardedFrom()) {\n\t\t\tconst auto forwarded = item->Get<HistoryMessageForwarded>();\n\t\t\tconst auto skip1 = forwarded->psaType.isEmpty()\n\t\t\t\t? 0\n\t\t\t\t: st::historyPsaIconSkip1;\n\t\t\tconst auto fwdheight = ((forwarded->text.maxWidth() > (contentWidth - st::msgPadding.left() - st::msgPadding.right() - skip1)) ? 2 : 1) * st::semiboldFont->height;\n\t\t\tnewHeight += fwdheight;\n\t\t}\n\n\t\tif (reply) {\n\t\t\tnewHeight += reply->resizeToWidth(contentWidth\n\t\t\t\t- st::msgPadding.left()\n\t\t\t\t- st::msgPadding.right());\n\t\t}\n\t\tif (const auto summaryHeader = Get<SummaryHeader>()) {\n\t\t\tnewHeight += summaryHeader->resizeToWidth(contentWidth\n\t\t\t\t- st::msgPadding.left()\n\t\t\t\t- st::msgPadding.right());\n\t\t}\n\t\tif (needInfoDisplay()) {\n\t\t\tnewHeight += (bottomInfoHeight - st::msgDateFont->height);\n\t\t}\n\n\t\tif (item->repliesAreComments() || item->externalReply()) {\n\t\t\tnewHeight += st::historyCommentsButtonHeight;\n\t\t} else if (_comments) {\n\t\t\t_comments = nullptr;\n\t\t\tcheckHeavyPart();\n\t\t}\n\t\tnewHeight += viewButtonHeight();\n\t} else if (mediaDisplayed) {\n\t\tnewHeight = media->height();\n\t} else {\n\t\tnewHeight = 0;\n\t}\n\tif (_reactions && !reactionsInBubble) {\n\t\tconst auto reactionsWidth = (!bubble && mediaDisplayed)\n\t\t\t? media->contentRectForReactions().width()\n\t\t\t: contentWidth;\n\t\tnewHeight += st::mediaInBubbleSkip\n\t\t\t+ _reactions->resizeGetHeight(reactionsWidth);\n\t\tif (hasRightLayout()) {\n\t\t\t_reactions->flipToRight();\n\t\t}\n\t}\n\n\tif (const auto keyboard = item->inlineReplyKeyboard()) {\n\t\tconst auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight();\n\t\tnewHeight += keyboardHeight;\n\t\tkeyboard->resize(contentWidth, keyboardHeight - st::msgBotKbButton.margin);\n\t}\n\n\tnewHeight += marginTop() + marginBottom();\n\treturn newHeight;\n}\n\nvoid Message::invalidateTextDependentCache() {\n\t_bubbleTextualWidthMinimum = -1;\n\t_bubbleTextualWidthCache = 0;\n}\n\nbool Message::textAppearValidate(not_null<TextAppearing*> appearing) {\n\twhile (true) {\n\t\tif (!textAppearCheckLine(appearing)) {\n\t\t\treturn false;\n\t\t} else if (!appearing->use\n\t\t\t|| appearing->widthAnimation.animating()\n\t\t\t|| appearing->heightAnimation.animating()) {\n\t\t\treturn true;\n\t\t}\n\t\t++appearing->shownLine;\n\t\tappearing->revealedLineWidth = 0;\n\t}\n}\n\nbool Message::textAppearCheckLine(not_null<TextAppearing*> appearing) {\n\tconst auto recount = !appearing->geometryValid;\n\tif (recount) {\n\t\tappearing->geometryValid = true;\n\t\tappearing->lines = text().countLinesGeometry(appearing->textWidth);\n\t\tauto &lines = appearing->lines;\n\t\tif (lines.size() > 1 && text().hasSkipBlock()) {\n\t\t\tconst auto &last = lines.back();\n\t\t\tconst auto &prev = lines[lines.size() - 2];\n\t\t\tif (last.width == skipBlockWidth()\n\t\t\t\t&& last.bottom - prev.bottom == skipBlockHeight()) {\n\t\t\t\tconst auto bottom = last.bottom;\n\t\t\t\tlines.pop_back();\n\t\t\t\tlines.back().bottom = bottom;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto lines = int(appearing->lines.size());\n\tconst auto shown = appearing->shownLine;\n\tconst auto line = (shown < lines) ? &appearing->lines[shown] : nullptr;\n\tconst auto use = appearing->use = !anim::Disabled()\n\t\t&& line\n\t\t&& ((shown + 1 < lines)\n\t\t\t|| (shown + 1 == lines\n\t\t\t\t&& ((appearing->revealedLineWidth < line->width)\n\t\t\t\t\t|| (appearing->shownHeight < line->bottom))));\n\tif (!use) {\n\t\tif (data()->isRegular()) {\n\t\t\tRemoveComponents(TextAppearing::Bit());\n\t\t\treturn false;\n\t\t} else if (recount && lines) {\n\t\t\tappearing->shownLine = lines - 1;\n\t\t\tconst auto &line = appearing->lines.back();\n\t\t\tappearing->revealedLineWidth = line.width;\n\t\t\tappearing->shownWidth = textRealWidth();\n\t\t\tappearing->shownHeight = line.bottom;\n\t\t\tappearing->widthAnimation.stop();\n\t\t\tappearing->heightAnimation.stop();\n\t\t}\n\t\treturn true;\n\t}\n\tif (appearing->targetLineWidth != line->width) {\n\t\tif (appearing->revealedLineWidth >= line->width) {\n\t\t\tappearing->widthAnimation.stop();\n\t\t\tappearing->revealedLineWidth\n\t\t\t\t= appearing->targetLineWidth\n\t\t\t\t= line->width;\n\t\t} else {\n\t\t\ttextAppearStartWidthAnimation(appearing);\n\t\t}\n\t}\n\tconst auto targetHeight = textAppearTargetHeight(appearing);\n\tif (appearing->targetHeight != targetHeight) {\n\t\tif (!shown) {\n\t\t\tappearing->shownHeight = appearing->lines.front().bottom\n\t\t\t\t+ (appearing->lines.size() > 1 ? skipBlockHeight() : 0);\n\t\t}\n\t\tif (appearing->shownHeight >= targetHeight) {\n\t\t\tappearing->heightAnimation.stop();\n\t\t\tappearing->shownHeight = appearing->targetHeight = targetHeight;\n\t\t} else {\n\t\t\tconst auto widthStart = appearing->startLineWidth;\n\t\t\tconst auto widthTarget = appearing->targetLineWidth;\n\t\t\tconst auto width = appearing->revealedLineWidth;\n\t\t\tconst auto progress = (width >= widthTarget)\n\t\t\t\t? 1.\n\t\t\t\t: (width - widthStart) / float64(widthTarget - widthStart);\n\t\t\tconst auto left = (1. - progress) * appearing->widthDuration;\n\t\t\tconst auto duration = appearing->finalizing\n\t\t\t\t? kLineHeightAppearFinalDuration\n\t\t\t\t: kLineHeightAppearDuration;\n\t\t\tif (appearing->heightAnimation.animating()\n\t\t\t\t|| !appearing->widthAnimation.animating()\n\t\t\t\t|| left <= duration) {\n\t\t\t\ttextAppearStartHeightAnimation(appearing);\n\t\t\t}\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid Message::textAppearStartWidthAnimation(\n\t\tnot_null<TextAppearing*> appearing) {\n\tExpects(appearing->use);\n\n\tconst auto shown = appearing->shownLine;\n\tconst auto lines = int(appearing->lines.size());\n\tconst auto lineWidth = appearing->lines[shown].width;\n\tconst auto lineDuration = appearing->finalizing\n\t\t? kFullLineAppearFinalDuration\n\t\t: kFullLineAppearDuration;\n\tconst auto computed = (shown + 1 == lines)\n\t\t? lineDuration\n\t\t: std::max(\n\t\t\tlineDuration * lineWidth / st::msgMaxWidth,\n\t\t\tcrl::time(10));\n\tconst auto duration = std::exchange(appearing->startedForText, true)\n\t\t? computed\n\t\t: std::max(computed, kMinWidthAppearDuration);\n\tappearing->widthDuration = duration;\n\tconst auto from\n\t\t= appearing->startLineWidth\n\t\t= appearing->revealedLineWidth;\n\tconst auto to = appearing->targetLineWidth = lineWidth;\n\n\tAssert(from < to);\n\tappearing->widthAnimation.start([=] {\n\t\ttextAppearWidthCallback();\n\t}, from, to, duration, anim::linear);\n}\n\nvoid Message::textAppearStartHeightAnimation(\n\t\tnot_null<TextAppearing*> appearing) {\n\tExpects(appearing->use);\n\n\tconst auto from = appearing->shownHeight;\n\tconst auto to\n\t\t= appearing->targetHeight\n\t\t= textAppearTargetHeight(appearing);\n\tconst auto duration = appearing->finalizing\n\t\t? kLineHeightAppearFinalDuration\n\t\t: kLineHeightAppearDuration;\n\tappearing->heightAnimation.start([=] {\n\t\ttextAppearHeightCallback();\n\t}, from, to, duration, anim::easeOutCubic);\n}\n\nint Message::textAppearTargetHeight(\n\t\tnot_null<TextAppearing*> appearing) const {\n\tconst auto next = appearing->shownLine + 1;\n\tconst auto lines = int(appearing->lines.size());\n\tif (next >= lines) {\n\t\treturn appearing->lines.back().bottom;\n\t}\n\tconst auto &line = appearing->lines[next];\n\tconst auto bottom = line.bottom;\n\tconst auto nextWidth = line.width;\n\tconst auto available = std::max(\n\t\tappearing->lines[appearing->shownLine].width,\n\t\tappearing->shownWidth);\n\tif (nextWidth + skipBlockWidth() <= available && !line.rtl) {\n\t\treturn bottom;\n\t}\n\treturn bottom + skipBlockHeight();\n}\n\nvoid Message::textAppearWidthCallback() {\n\tconst auto appearing = Get<TextAppearing>();\n\tconst auto now = int(base::SafeRound(\n\t\tappearing->widthAnimation.value(appearing->targetLineWidth)));\n\tif (now != appearing->revealedLineWidth) {\n\t\tappearing->revealedLineWidth = now;\n\t\tif (appearing->lines[appearing->shownLine].rtl) {\n\t\t\tappearing->shownWidth = textRealWidth();\n\t\t} else {\n\t\t\tappearing->shownWidth = std::min(\n\t\t\t\tstd::max(\n\t\t\t\t\tappearing->shownWidth,\n\t\t\t\t\tnow + skipBlockWidth()),\n\t\t\t\ttextRealWidth());\n\t\t}\n\t\trepaint();\n\t}\n\ttextAppearValidate(appearing);\n}\n\nvoid Message::textAppearHeightCallback() {\n\tconst auto appearing = Get<TextAppearing>();\n\tconst auto now = int(base::SafeRound(\n\t\tappearing->heightAnimation.value(appearing->targetHeight)));\n\tif (const auto delta = now - appearing->shownHeight) {\n\t\tappearing->shownHeight = now;\n\t\tadjustHeight(delta);\n\t\thistory()->viewHeightAdjusted(this, delta);\n\t\trepaint();\n\t}\n\ttextAppearValidate(appearing);\n}\n\nbool Message::needInfoDisplay() const {\n\tconst auto media = this->media();\n\tconst auto mediaDisplayed = media ? media->isDisplayed() : false;\n\tconst auto check = factcheckBlock();\n\tconst auto entry = logEntryOriginal();\n\treturn entry\n\t\t? !entry->customInfoLayout()\n\t\t: check\n\t\t? !check->customInfoLayout()\n\t\t: ((mediaDisplayed && media->isBubbleBottom())\n\t\t\t? !media->customInfoLayout()\n\t\t\t: true);\n}\n\nbool Message::invertMedia() const {\n\treturn _invertMedia;\n}\n\nbool Message::hasVisibleText() const {\n\tconst auto textItem = this->textItem();\n\tif (!textItem) {\n\t\treturn false;\n\t} else if (textItem->emptyText()) {\n\t\tif (const auto media = textItem->media()) {\n\t\t\treturn media->storyExpired() || media->storyUnsupported();\n\t\t}\n\t\treturn false;\n\t}\n\tconst auto media = this->media();\n\treturn !media || !media->hideMessageText();\n}\n\nint Message::visibleTextLength() const {\n\treturn hasVisibleText() ? text().length() : 0;\n}\n\nint Message::visibleMediaTextLength() const {\n\tconst auto media = this->media();\n\treturn (media && media->isDisplayed())\n\t\t? media->fullSelectionLength()\n\t\t: 0;\n}\n\nQSize Message::performCountCurrentSize(int newWidth) {\n\tconst auto newHeight = resizeContentGetHeight(newWidth);\n\n\treturn { newWidth, newHeight };\n}\n\nvoid Message::refreshInfoSkipBlock(HistoryItem *textItem) {\n\tconst auto media = this->media();\n\tconst auto hasTextSkipBlock = [&] {\n\t\tif (!textItem || textItem->_text.empty()) {\n\t\t\tif (const auto media = data()->media()) {\n\t\t\t\treturn media->storyExpired() || media->storyUnsupported();\n\t\t\t}\n\t\t\treturn false;\n\t\t} else if (factcheckBlock()\n\t\t\t|| data()->Has<HistoryMessageLogEntryOriginal>()) {\n\t\t\treturn false;\n\t\t} else if (media && media->isDisplayed() && !_invertMedia) {\n\t\t\treturn false;\n\t\t} else if (_reactions) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}();\n\tconst auto skipWidth = skipBlockWidth();\n\tconst auto skipHeight = skipBlockHeight();\n\tif (_reactions) {\n\t\tif (needInfoDisplay()) {\n\t\t\t_reactions->updateSkipBlock(skipWidth, skipHeight);\n\t\t} else {\n\t\t\t_reactions->removeSkipBlock();\n\t\t}\n\t}\n\tvalidateTextSkipBlock(hasTextSkipBlock, skipWidth, skipHeight);\n}\n\nTimeId Message::displayedEditDate() const {\n\tconst auto item = data();\n\tconst auto overrided = media() && media()->overrideEditedDate();\n\tif (item->hideEditedBadge() && !overrided) {\n\t\treturn TimeId(0);\n\t} else if (const auto edited = displayedEditBadge()) {\n\t\treturn edited->date;\n\t}\n\treturn TimeId(0);\n}\n\nHistoryMessageEdited *Message::displayedEditBadge() {\n\tif (const auto media = this->media()) {\n\t\tif (media->overrideEditedDate()) {\n\t\t\treturn media->displayedEditBadge();\n\t\t}\n\t}\n\treturn data()->Get<HistoryMessageEdited>();\n}\n\nconst HistoryMessageEdited *Message::displayedEditBadge() const {\n\tif (const auto media = this->media()) {\n\t\tif (media->overrideEditedDate()) {\n\t\t\treturn media->displayedEditBadge();\n\t\t}\n\t}\n\treturn data()->Get<HistoryMessageEdited>();\n}\n\nvoid Message::ensureSummarizeButton() const {\n\tif (data()->canBeSummarized()\n\t\t/*&& item->originalText().text.size() >= kSummarizeThreshold*/) {\n\t\tif (!_summarize) {\n\t\t\t_summarize\n\t\t\t\t= std::make_unique<TranscribeButton>(data(), false, true);\n\t\t}\n\t} else {\n\t\t_summarize = nullptr;\n\t}\n}\n\nvoid Message::paintSummarize(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tbool right,\n\t\tconst PaintContext &context,\n\t\tQRect g) const {\n\tif (!_summarize) {\n\t\treturn;\n\t}\n\tconst auto s = _summarize->size();\n\tconst auto bottomY = y - s.height() - st::msgDateImgDelta;\n\tif (bottomY < g.top()) {\n\t\treturn;\n\t}\n\tconst auto topY = g.top();\n\tconst auto buttonY = std::min(\n\t\tstd::max(topY, context.area.y() + st::msgDateImgDelta),\n\t\tbottomY);\n\t_summarize->paint(\n\t\tp,\n\t\tx - (right ? 0 : s.width()),\n\t\tbuttonY,\n\t\tcontext);\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_message.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_bottom_info.h\"\n#include \"ui/effects/animations.h\"\n\nclass HistoryItem;\nstruct HistoryMessageEdited;\nstruct HistoryMessageForwarded;\nstruct HistoryMessageReplyMarkup;\nstruct HistoryMessageSuggestion;\nstruct HistoryMessageReply;\n\nnamespace Data {\nstruct ReactionId;\n} // namespace Data\n\nnamespace Ui {\nstruct BubbleRounding;\nclass RoundCheckbox;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass ViewButton;\nclass WebPage;\nclass TranscribeButton;\n\nnamespace Reactions {\nclass InlineList;\n} // namespace Reactions\n\nnamespace ReplyButton {\nstruct ButtonParameters;\n} // namespace ReplyButton\n\n// Special type of Component for the channel actions log.\nstruct LogEntryOriginal : RuntimeComponent<LogEntryOriginal, Element> {\n\tLogEntryOriginal();\n\tLogEntryOriginal(LogEntryOriginal &&other);\n\tLogEntryOriginal &operator=(LogEntryOriginal &&other);\n\t~LogEntryOriginal();\n\n\tstd::unique_ptr<WebPage> page;\n};\n\nstruct Factcheck : RuntimeComponent<Factcheck, Element> {\n\tstd::unique_ptr<WebPage> page;\n\tbool expanded = false;\n};\n\nstruct PsaTooltipState : RuntimeComponent<PsaTooltipState, Element> {\n\tQString type;\n\tmutable ClickHandlerPtr link;\n\tmutable Ui::Animations::Simple buttonVisibleAnimation;\n\tmutable bool buttonVisible = true;\n};\n\nenum class BadgeRole : uchar {\n\tUser,\n\tAdmin,\n\tCreator,\n};\n\nstruct RightBadge : RuntimeComponent<RightBadge, Element> {\n\tUi::Text::String tag;\n\tUi::Text::String boosts;\n\tmutable ClickHandlerPtr tagLink;\n\tmutable ClickHandlerPtr boostsLink;\n\tint width = 0;\n\tBadgeRole role = BadgeRole::User;\n\tbool overridden = false;\n\tbool special = false;\n\tmutable std::unique_ptr<Ui::RippleAnimation> ripple;\n\tmutable QPoint lastPoint;\n};\n\nstruct TextAppearing : RuntimeComponent<TextAppearing, Element> {\n\tstd::vector<Ui::Text::LineLayoutInfo> lines;\n\tint textWidth = 0;\n\tint shownLine = 0;\n\tint revealedLineWidth = 0;\n\tint startLineWidth = 0;\n\tint targetLineWidth = 0;\n\tint shownWidth = 0;\n\tint shownHeight = 0;\n\tint targetHeight = 0;\n\tcrl::time widthDuration = 0;\n\tUi::Animations::Simple widthAnimation;\n\tUi::Animations::Simple heightAnimation;\n\tbool geometryValid = false;\n\tbool startedForText = false;\n\tbool finalizing = false;\n\tbool use = false;\n\tmutable QImage lineCache;\n\tmutable QImage gradientMask;\n};\n\nstruct BottomRippleMask {\n\tQImage image;\n\tint shift = 0;\n};\n\nclass Message final : public Element {\npublic:\n\tMessage(\n\t\tnot_null<ElementDelegate*> delegate,\n\t\tnot_null<HistoryItem*> data,\n\t\tElement *replacing);\n\t~Message();\n\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) override;\n\n\t[[nodiscard]] const HistoryMessageEdited *displayedEditBadge() const;\n\t[[nodiscard]] HistoryMessageEdited *displayedEditBadge();\n\n\tbool embedReactionsInBubble() const override;\n\n\tint marginTop() const override;\n\tint marginBottom() const override;\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tPointState pointState(QPoint point) const override;\n\tTextState textState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\tvoid updatePressed(QPoint point) override;\n\tvoid drawInfo(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint right,\n\t\tint bottom,\n\t\tint width,\n\t\tInfoDisplayType type) const override;\n\tTextState bottomInfoTextState(\n\t\tint right,\n\t\tint bottom,\n\t\tQPoint point,\n\t\tInfoDisplayType type) const override;\n\tTextForMimeData selectedText(TextSelection selection) const override;\n\tSelectedQuote selectedQuote(TextSelection selection) const override;\n\tTextSelection selectionFromQuote(\n\t\tconst SelectedQuote &quote) const override;\n\tTextSelection adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const override;\n\n\tReactions::ButtonParameters reactionButtonParameters(\n\t\tQPoint position,\n\t\tconst TextState &reactionState) const override;\n\tReplyButton::ButtonParameters replyButtonParameters(\n\t\tQPoint position,\n\t\tconst TextState &replyState) const override;\n\tint reactionsOptimalWidth() const override;\n\n\tvoid unloadHeavyPart() override;\n\n\t// hasFromPhoto() returns true even if we don't display the photo\n\t// but we need to skip a place at the left side for this photo\n\tbool hasFromPhoto() const override;\n\tbool displayFromPhoto() const override;\n\tbool hasFromName() const override;\n\tbool displayFromName() const override;\n\tbool displayForwardedFrom() const override;\n\tbool hasOutLayout() const override;\n\tbool drawBubble() const override;\n\tbool hasBubble() const override;\n\tTopicButton *displayedTopicButton() const override;\n\tbool unwrapped() const override;\n\tint minWidthForMedia() const override;\n\tbool displayRightActionComments() const;\n\tstd::optional<QSize> rightActionSize() const override;\n\tvoid drawRightAction(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint left,\n\t\tint top,\n\t\tint outerWidth) const override;\n\t[[nodiscard]] ClickHandlerPtr rightActionLink(\n\t\tstd::optional<QPoint> pressPoint) const override;\n\t[[nodiscard]] TimeId displayedEditDate() const override;\n\t[[nodiscard]] bool toggleSelectionByHandlerClick(\n\t\tconst ClickHandlerPtr &handler) const override;\n\t[[nodiscard]] bool allowTextSelectionByHandler(\n\t\tconst ClickHandlerPtr &handler) const override;\n\t[[nodiscard]] int infoWidth() const override;\n\t[[nodiscard]] int bottomInfoFirstLineWidth() const override;\n\t[[nodiscard]] bool bottomInfoIsWide() const override;\n\t[[nodiscard]] bool isSignedAuthorElided() const override;\n\n\tvoid itemDataChanged() override;\n\n\tVerticalRepaintRange verticalRepaintRange() const override;\n\n\tvoid applyGroupAdminChanges(\n\t\tconst base::flat_set<UserId> &changes) override;\n\n\tvoid animateReaction(Ui::ReactionFlyAnimationArgs &&args) override;\n\n\tauto takeEffectAnimation()\n\t-> std::unique_ptr<Ui::ReactionFlyAnimation> override;\n\n\tQRect effectIconGeometry() const override;\n\tQRect innerGeometry() const override;\n\tQPoint mediaTopLeft() const override;\n\t[[nodiscard]] BottomRippleMask bottomRippleMask(int buttonHeight) const;\n\nprivate:\n\tstruct CommentsButton;\n\tstruct LinkRipple;\n\tstruct FromNameStatus;\n\tstruct RightAction;\n\n\tvoid refreshDataIdHook() override;\n\tbool hasHeavyPart() const override;\n\n\tbool updateBottomInfo();\n\n\tvoid initPaidInformation();\n\tvoid refreshSuggestedInfo(\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<const HistoryMessageSuggestion*> suggest,\n\t\tconst HistoryMessageReply *reply);\n\tvoid initLogEntryOriginal();\n\tvoid initPsa();\n\tvoid fromNameUpdated(int width) const;\n\n\t[[nodiscard]] TextSelection skipTextSelection(\n\t\tTextSelection selection) const;\n\t[[nodiscard]] TextSelection unskipTextSelection(\n\t\tTextSelection selection) const;\n\n\tvoid toggleCommentsButtonRipple(bool pressed);\n\tvoid createCommentsButtonRipple();\n\n\tvoid toggleTopicButtonRipple(bool pressed);\n\tvoid createTopicButtonRipple();\n\n\tvoid toggleLinkRipple(bool pressed);\n\tvoid recordLinkRipplePoint(\n\t\tQPoint point,\n\t\tQPoint textOrigin) const;\n\tvoid paintLinkRipple(\n\t\tPainter &p,\n\t\tconst ClickHandlerPtr &handler,\n\t\tQRect linkRect,\n\t\tQPoint textPosition) const;\n\tvoid createLinkRippleMask(\n\t\tconst QPainterPath &path,\n\t\tQPoint textPosition,\n\t\tint useWidth,\n\t\tstyle::margins padding,\n\t\tint radius) const;\n\tvoid createLinkRippleMask(\n\t\tQRect linkRect,\n\t\tQPoint textPosition,\n\t\tstyle::margins padding,\n\t\tint radius) const;\n\n\tvoid toggleRightActionRipple(bool pressed);\n\tvoid toggleBadgeRipple(bool pressed);\n\n\tvoid toggleReplyRipple(bool pressed);\n\tvoid toggleSummaryHeaderRipple(bool pressed);\n\n\tvoid paintCommentsButton(\n\t\tPainter &p,\n\t\tQRect &g,\n\t\tconst PaintContext &context) const;\n\tvoid paintFromName(\n\t\tPainter &p,\n\t\tQRect &trect,\n\t\tconst PaintContext &context) const;\n\tvoid paintTopicButton(\n\t\tPainter &p,\n\t\tQRect &trect,\n\t\tconst PaintContext &context) const;\n\tvoid paintForwardedInfo(\n\t\tPainter &p,\n\t\tQRect &trect,\n\t\tconst PaintContext &context) const;\n\tvoid paintReplyInfo(\n\t\tPainter &p,\n\t\tQRect &trect,\n\t\tconst PaintContext &context) const;\n\tvoid paintSummaryHeaderInfo(\n\t\tPainter &p,\n\t\tQRect &trect,\n\t\tconst PaintContext &context) const;\n\t// This method draws \"via @bot\" if it is not painted\n\t// in forwarded info or in from name.\n\tvoid paintViaBotIdInfo(\n\t\tPainter &p,\n\t\tQRect &trect,\n\t\tconst PaintContext &context) const;\n\tvoid paintText(\n\t\tPainter &p,\n\t\tQRect &trect,\n\t\tconst PaintContext &context) const;\n\n\tbool getStateCommentsButton(\n\t\tQPoint point,\n\t\tQRect &g,\n\t\tnot_null<TextState*> outResult) const;\n\tbool getStateFromName(\n\t\tQPoint point,\n\t\tQRect &trect,\n\t\tnot_null<TextState*> outResult) const;\n\tbool getStateTopicButton(\n\t\tQPoint point,\n\t\tQRect &trect,\n\t\tnot_null<TextState*> outResult) const;\n\tbool getStateForwardedInfo(\n\t\tQPoint point,\n\t\tQRect &trect,\n\t\tnot_null<TextState*> outResult,\n\t\tStateRequest request) const;\n\tbool getStateReplyInfo(\n\t\tQPoint point,\n\t\tQRect &trect,\n\t\tnot_null<TextState*> outResult) const;\n\tbool getStateSummaryHeaderInfo(\n\t\tQPoint point,\n\t\tQRect &trect,\n\t\tnot_null<TextState*> outResult) const;\n\tbool getStateViaBotIdInfo(\n\t\tQPoint point,\n\t\tQRect &trect,\n\t\tnot_null<TextState*> outResult) const;\n\tbool getStateText(\n\t\tQPoint point,\n\t\tQRect &trect,\n\t\tnot_null<TextState*> outResult,\n\t\tStateRequest request) const;\n\n\tvoid updateMediaInBubbleState();\n\tQRect countGeometry() const;\n\t[[nodiscard]] Ui::BubbleRounding countMessageRounding() const;\n\t[[nodiscard]] Ui::BubbleRounding countBubbleRounding(\n\t\tUi::BubbleRounding messageRounding) const;\n\t[[nodiscard]] Ui::BubbleRounding countBubbleRounding() const;\n\n\tint resizeContentGetHeight(int newWidth);\n\tQSize performCountOptimalSize() override;\n\tQSize performCountCurrentSize(int newWidth) override;\n\tbool hasVisibleText() const override;\n\t[[nodiscard]] int visibleTextLength() const;\n\t[[nodiscard]] int visibleMediaTextLength() const;\n\t[[nodiscard]] bool needInfoDisplay() const;\n\t[[nodiscard]] bool invertMedia() const;\n\t[[nodiscard]] bool hasFastReply() const;\n\t[[nodiscard]] bool displayFastReply() const;\n\n\t[[nodiscard]] bool isPinnedContext() const;\n\t[[nodiscard]] bool isCommentsRootView() const;\n\n\t[[nodiscard]] bool displayFastShare() const;\n\t[[nodiscard]] bool displayGoToOriginal() const;\n\t[[nodiscard]] ClickHandlerPtr fastReplyLink() const;\n\t[[nodiscard]] ClickHandlerPtr prepareRightActionLink() const;\n\n\tvoid ensureRightAction() const;\n\tvoid refreshTopicButton();\n\tvoid refreshInfoSkipBlock(HistoryItem *textItem);\n\t[[nodiscard]] int monospaceMaxWidth() const;\n\t[[nodiscard]] int bubbleTextWidth(int bubbleWidth) const;\n\t[[nodiscard]] int bubbleTextualWidth() const;\n\n\tvoid ensureSummarizeButton() const;\n\tvoid paintSummarize(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tbool right,\n\t\tconst PaintContext &context,\n\t\tQRect g) const;\n\n\tvoid updateViewButtonExistence();\n\t[[nodiscard]] int viewButtonHeight() const;\n\n\t[[nodiscard]] WebPage *logEntryOriginal() const;\n\t[[nodiscard]] WebPage *factcheckBlock() const;\n\n\t[[nodiscard]] ClickHandlerPtr createGoToCommentsLink() const;\n\t[[nodiscard]] ClickHandlerPtr psaTooltipLink() const;\n\tvoid psaTooltipToggled(bool shown) const;\n\tvoid invalidateTextDependentCache() override;\n\n\tbool textAppearValidate(not_null<TextAppearing*> appearing);\n\tbool textAppearCheckLine(not_null<TextAppearing*> appearing);\n\tvoid textAppearStartWidthAnimation(not_null<TextAppearing*> appearing);\n\tvoid textAppearStartHeightAnimation(not_null<TextAppearing*> appearing);\n\tvoid textAppearWidthCallback();\n\tvoid textAppearHeightCallback();\n\t[[nodiscard]] int textAppearTargetHeight(\n\t\tnot_null<TextAppearing*> appearing) const;\n\n\tvoid refreshRightBadge();\n\t[[nodiscard]] int rightBadgeWidth() const;\n\tvoid validateFromNameText(PeerData *from) const;\n\tvoid ensureFromNameStatusLink(not_null<PeerData*> peer) const;\n\n\tmutable std::unique_ptr<RightAction> _rightAction;\n\tmutable ClickHandlerPtr _fastReplyLink;\n\tmutable std::unique_ptr<ViewButton> _viewButton;\n\tstd::unique_ptr<TopicButton> _topicButton;\n\tmutable std::unique_ptr<LinkRipple> _linkRipple;\n\tmutable QPoint _linkRippleLastPoint;\n\tmutable std::unique_ptr<CommentsButton> _comments;\n\tmutable std::unique_ptr<TranscribeButton> _summarize;\n\n\tmutable Ui::Text::String _fromName;\n\tmutable std::unique_ptr<FromNameStatus> _fromNameStatus;\n\tmutable std::unique_ptr<Ui::RoundCheckbox> _selectionRoundCheckbox;\n\tmutable uint32 _fromNameVersion : 16 = 0;\n\tuint32 _nonTextMaxWidth : 16 = 0;\n\tmutable int _bubbleTextualWidthMinimum : 16 = -1;\n\tmutable int _bubbleTextualWidthCache : 16 = 0;\n\tuint32 _bubbleWidthLimit : 26 = 0;\n\tuint32 _invertMedia : 1 = 0;\n\tuint32 _hideReply : 1 = 0;\n\tuint32 _postShowingAuthor : 1 = 0;\n\tmutable uint32 _fromLinkRipplePointSet : 1 = 0;\n\n\tBottomInfo _bottomInfo;\n\tmutable QPoint _lastMediaPosition;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_object.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace HistoryView {\n\nclass Object {\npublic:\n\tObject() = default;\n\tObject(const Object &other) = delete;\n\tObject &operator=(const Object &other) = delete;\n\n\tvoid initDimensions() {\n\t\tsetOptimalSize(countOptimalSize());\n\t}\n\tint resizeGetHeight(int newWidth) {\n\t\tsetCurrentSize(countCurrentSize(newWidth));\n\t\treturn _height;\n\t}\n\n\t[[nodiscard]] QSize optimalSize() const {\n\t\treturn { _maxWidth, _minHeight };\n\t}\n\t[[nodiscard]] QSize currentSize() const {\n\t\treturn { _width, _height };\n\t}\n\n\t[[nodiscard]] int maxWidth() const {\n\t\treturn _maxWidth;\n\t}\n\t[[nodiscard]] int minHeight() const {\n\t\treturn _minHeight;\n\t}\n\t[[nodiscard]] int width() const {\n\t\treturn _width;\n\t}\n\t[[nodiscard]] int height() const {\n\t\treturn _height;\n\t}\n\n\tvirtual ~Object() = default;\n\nprotected:\n\tvoid setOptimalSize(QSize size) {\n\t\t_maxWidth = size.width();\n\t\t_minHeight = size.height();\n\t}\n\tvoid setCurrentSize(QSize size) {\n\t\t_width = size.width();\n\t\t_height = size.height();\n\t}\n\tvoid adjustHeight(int delta) {\n\t\t_height += delta;\n\t}\n\nprivate:\n\tvirtual QSize countOptimalSize() = 0;\n\tvirtual QSize countCurrentSize(int newWidth) = 0;\n\n\tint _maxWidth = 0;\n\tint _minHeight = 0;\n\tint _width = 0;\n\tint _height = 0;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_paid_reaction_toast.h\"\n\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_messages.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/numbers_animation.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/toast/toast_widget.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/painter.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_premium.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kPremiumToastDuration = 5 * crl::time(1000);\n\n[[nodiscard]] not_null<Ui::AbstractButton*> MakeUndoButton(\n\t\tnot_null<QWidget*> parent,\n\t\tint width,\n\t\tconst QString &text,\n\t\trpl::producer<crl::time> finish,\n\t\tcrl::time total,\n\t\tFn<void()> click,\n\t\tFn<void()> timeout) {\n\tconst auto result = Ui::CreateChild<Ui::AbstractButton>(parent);\n\tresult->setClickedCallback(std::move(click));\n\n\tstruct State {\n\t\texplicit State(not_null<QWidget*> button)\n\t\t: countdown(\n\t\t\tst::toastUndoFont,\n\t\t\t[=] { button->update(); }) {\n\t\t}\n\n\t\tUi::NumbersAnimation countdown;\n\t\tcrl::time finish = 0;\n\t\tint secondsLeft = 0;\n\t\tUi::Animations::Basic animation;\n\t\tFn<void()> update;\n\t\tbase::Timer timer;\n\t};\n\tconst auto state = result->lifetime().make_state<State>(result);\n\tconst auto updateLeft = [=] {\n\t\tconst auto now = crl::now();\n\t\tconst auto left = state->finish - now;\n\t\tif (left > 0) {\n\t\t\tconst auto seconds = int((left + 999) / 1000);\n\t\t\tif (state->secondsLeft != seconds) {\n\t\t\t\tstate->secondsLeft = seconds;\n\t\t\t\tstate->countdown.setText(QString::number(seconds), seconds);\n\t\t\t}\n\t\t\tstate->timer.callOnce((left % 1000) + 1);\n\t\t} else {\n\t\t\tstate->animation.stop();\n\t\t\tstate->timer.cancel();\n\t\t\ttimeout();\n\t\t}\n\t\tif (anim::Disabled()) {\n\t\t}\n\t};\n\tstate->update = [=] {\n\t\tif (anim::Disabled()) {\n\t\t\tstate->animation.stop();\n\t\t} else {\n\t\t\tif (!state->animation.animating()) {\n\t\t\t\tstate->animation.start();\n\t\t\t}\n\t\t\tstate->timer.cancel();\n\t\t}\n\t\tupdateLeft();\n\t\tresult->update();\n\t};\n\n\tresult->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(result);\n\n\t\tconst auto font = st::historyPremiumViewSet.style.font;\n\t\tconst auto top = (result->height() - font->height) / 2;\n\t\tauto pen = st::historyPremiumViewSet.textFg->p;\n\t\tp.setPen(pen);\n\t\tp.setFont(font);\n\t\tp.drawText(0, top + font->ascent, text);\n\n\t\tconst auto inner = QRect(\n\t\t\twidth - st::toastUndoSkip - st::toastUndoDiameter,\n\t\t\t(result->height() - st::toastUndoDiameter) / 2,\n\t\t\tst::toastUndoDiameter,\n\t\t\tst::toastUndoDiameter);\n\t\tstate->countdown.paint(\n\t\t\tp,\n\t\t\tinner.x() + (inner.width() - state->countdown.countWidth()) / 2,\n\t\t\tinner.y() + (inner.height() - st::toastUndoFont->height) / 2,\n\t\t\twidth);\n\n\t\tconst auto progress = (state->finish - crl::now()) / float64(total);\n\t\tconst auto len = int(base::SafeRound(arc::kFullLength * progress));\n\t\tif (len > 0) {\n\t\t\tconst auto from = arc::kFullLength / 4;\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tpen.setWidthF(st::toastUndoStroke);\n\t\t\tp.setPen(pen);\n\t\t\tp.drawArc(inner, from, len);\n\t\t}\n\t}, result->lifetime());\n\tresult->resize(width, st::historyPremiumViewSet.height);\n\n\tstd::move(finish) | rpl::on_next([=](crl::time value) {\n\t\tstate->finish = value;\n\t\tstate->update();\n\t}, result->lifetime());\n\tstate->animation.init(state->update);\n\tstate->timer.setCallback(state->update);\n\tstate->update();\n\n\tresult->show();\n\treturn result;\n}\n\n} // namespace\n\nPaidReactionToast::PaidReactionToast(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Data::Session*> owner,\n\trpl::producer<int> topOffset,\n\tFn<bool(not_null<const Element*> view)> mine)\n: _parent(parent)\n, _owner(owner)\n, _topOffset(std::move(topOffset)) {\n\t_owner->viewPaidReactionSent(\n\t) | rpl::filter(\n\t\tstd::move(mine)\n\t) | rpl::on_next([=](not_null<const Element*> view) {\n\t\tmaybeShowFor(view->data());\n\t}, _lifetime);\n}\n\nPaidReactionToast::PaidReactionToast(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Data::Session*> owner,\n\trpl::producer<int> topOffset,\n\tFn<bool(not_null<Calls::GroupCall*> call)> mine)\n: _parent(parent)\n, _owner(owner)\n, _topOffset(std::move(topOffset)) {\n\t_owner->callPaidReactionSent(\n\t) | rpl::filter(\n\t\tstd::move(mine)\n\t) | rpl::on_next([=](not_null<Calls::GroupCall*> call) {\n\t\tmaybeShowFor(call);\n\t}, _lifetime);\n}\n\nPaidReactionToast::~PaidReactionToast() {\n\t_hiding.push_back(_weak);\n\tfor (const auto &weak : base::take(_hiding)) {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tdelete strong->widget();\n\t\t}\n\t}\n}\n\nrpl::producer<FullMsgId> PaidReactionToast::shownForId() const {\n\treturn _shownForId.value();\n}\n\nrpl::producer<Calls::GroupCall*> PaidReactionToast::shownForCall() const {\n\treturn shownForId() | rpl::map([=](FullMsgId id) {\n\t\tconst auto i = _idsForCalls.find(id);\n\t\treturn (i != end(_idsForCalls)) ? i->second.get() : nullptr;\n\t});\n}\n\nbool PaidReactionToast::maybeShowFor(not_null<HistoryItem*> item) {\n\tconst auto count = item->reactionsPaidScheduled();\n\tconst auto shownPeer = item->reactionsLocalShownPeer();\n\tconst auto at = _owner->reactions().sendingScheduledPaidAt(item);\n\tif (!count || !at) {\n\t\treturn false;\n\t}\n\tconst auto total = Data::Reactions::ScheduledPaidDelay();\n\tconst auto ignore = total % 1000;\n\tif (at <= crl::now() + ignore) {\n\t\treturn false;\n\t}\n\tshowFor(item->fullId(), count, shownPeer, at - ignore, total);\n\treturn true;\n}\n\nbool PaidReactionToast::maybeShowFor(not_null<Calls::GroupCall*> call) {\n\tconst auto count = call->messages()->reactionsPaidScheduled();\n\tconst auto shownPeer = call->messagesFrom()->id;\n\tconst auto at = _owner->reactions().sendingScheduledPaidAt(call);\n\tif (!count || !at) {\n\t\treturn false;\n\t}\n\tconst auto total = Data::Reactions::ScheduledPaidDelay();\n\tconst auto ignore = total % 1000;\n\tif (at <= crl::now() + ignore) {\n\t\treturn false;\n\t}\n\tshowFor(idForCall(call), count, shownPeer, at - ignore, total);\n\treturn true;\n}\n\nvoid PaidReactionToast::showFor(\n\t\tFullMsgId itemId,\n\t\tint count,\n\t\tPeerId shownPeer,\n\t\tcrl::time finish,\n\t\tcrl::time total) {\n\tconst auto old = _weak.get();\n\tconst auto i = ranges::find(_stack, itemId);\n\tif (i != end(_stack)) {\n\t\tif (old && i + 1 == end(_stack)) {\n\t\t\t_count = count;\n\t\t\t_shownPeer = shownPeer;\n\t\t\t_timeFinish = finish;\n\t\t\treturn;\n\t\t}\n\t\t_stack.erase(i);\n\t}\n\t_stack.push_back(itemId);\n\n\tclearHiddenHiding();\n\tif (old) {\n\t\told->hideAnimated();\n\t\t_hiding.push_back(base::take(_weak));\n\t}\n\t_count.reset();\n\t_shownPeer.reset();\n\t_timeFinish.reset();\n\t_count = count;\n\t_shownPeer = shownPeer;\n\t_timeFinish = finish;\n\tauto text = rpl::combine(\n\t\trpl::conditional(\n\t\t\t_shownPeer.value() | rpl::map(rpl::mappers::_1 == PeerId()),\n\t\t\ttr::lng_paid_react_toast_anonymous(\n\t\t\t\tlt_count,\n\t\t\t\t_count.value() | tr::to_count(),\n\t\t\t\ttr::bold),\n\t\t\ttr::lng_paid_react_toast(\n\t\t\t\tlt_count,\n\t\t\t\t_count.value() | tr::to_count(),\n\t\t\t\ttr::bold)),\n\t\ttr::lng_paid_react_toast_text(\n\t\t\tlt_count_decimal,\n\t\t\t_count.value() | tr::to_count(),\n\t\t\ttr::rich)\n\t) | rpl::map([](TextWithEntities &&title, TextWithEntities &&body) {\n\t\ttitle.append('\\n').append(body);\n\t\treturn std::move(title);\n\t});\n\tconst auto &st = st::historyPremiumToast;\n\tconst auto skip = st.padding.top();\n\tconst auto size = st.style.font->height * 2;\n\tconst auto undoText = tr::lng_paid_react_undo(tr::now);\n\n\tauto content = object_ptr<Ui::RpWidget>((QWidget*)nullptr);\n\tconst auto child = Ui::CreateChild<Ui::FlatLabel>(\n\t\tcontent.data(),\n\t\tstd::move(text),\n\t\tst::paidReactToastLabel);\n\tcontent->resize(child->naturalWidth() * 1.5, child->height());\n\tchild->show();\n\n\tconst auto leftSkip = skip + size + skip - st.padding.left();\n\tconst auto undoFont = st::historyPremiumViewSet.style.font;\n\n\tconst auto rightSkip = undoFont->width(undoText)\n\t\t+ st::toastUndoSpace\n\t\t+ st::toastUndoDiameter\n\t\t+ st::toastUndoSkip\n\t\t- st.padding.right();\n\t_weak = Ui::Toast::Show(_parent, Ui::Toast::Config{\n\t\t.content = std::move(content),\n\t\t.padding = rpl::single(QMargins(leftSkip, 0, rightSkip, 0)),\n\t\t.st = &st,\n\t\t.attach = RectPart::Top,\n\t\t.addToAttachSide = _topOffset.value(),\n\t\t.acceptinput = true,\n\t\t.infinite = true,\n\t});\n\tconst auto strong = _weak.get();\n\tif (!strong) {\n\t\treturn;\n\t}\n\n\t_shownForId = itemId;\n\tconst auto widget = strong->widget();\n\tconst auto hideToast = [=, weak = _weak] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tif (strong == _weak.get()) {\n\t\t\t\t_stack.erase(ranges::remove(_stack, itemId), end(_stack));\n\t\t\t\tif (_shownForId.current() == itemId) {\n\t\t\t\t\t_shownForId = FullMsgId();\n\t\t\t\t}\n\n\t\t\t\t_hiding.push_back(base::take(_weak));\n\t\t\t\tstrong->hideAnimated();\n\n\t\t\t\twhile (!_stack.empty()) {\n\t\t\t\t\tif (const auto item = _owner->message(_stack.back())) {\n\t\t\t\t\t\tif (maybeShowFor(item)) {\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (_shownForId.current() == _stack.back()) {\n\t\t\t\t\t\t_shownForId = FullMsgId();\n\t\t\t\t\t}\n\t\t\t\t\t_stack.pop_back();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\tconst auto undo = [=] {\n\t\tconst auto i = _idsForCalls.find(itemId);\n\t\tif (i != end(_idsForCalls)) {\n\t\t\tif (const auto strong = i->second.get()) {\n\t\t\t\t_owner->reactions().undoScheduledPaid(strong);\n\t\t\t}\n\t\t} else if (const auto item = _owner->message(itemId)) {\n\t\t\t_owner->reactions().undoScheduledPaid(item);\n\t\t}\n\t\thideToast();\n\t};\n\tconst auto button = MakeUndoButton(\n\t\twidget.get(),\n\t\trightSkip + st.padding.right(),\n\t\tundoText,\n\t\t_timeFinish.value(),\n\t\ttotal,\n\t\tundo,\n\t\thideToast);\n\n\trpl::combine(\n\t\twidget->sizeValue(),\n\t\tbutton->sizeValue()\n\t) | rpl::on_next([=](QSize outer, QSize inner) {\n\t\tbutton->moveToRight(\n\t\t\t0,\n\t\t\t(outer.height() - inner.height()) / 2,\n\t\t\touter.width());\n\t}, widget->lifetime());\n\tconst auto preview = Ui::CreateChild<Ui::RpWidget>(widget.get());\n\tpreview->moveToLeft(skip, skip);\n\tpreview->resize(size, size);\n\tpreview->show();\n\n\tsetupLottiePreview(preview, size);\n}\n\nFullMsgId PaidReactionToast::idForCall(not_null<Calls::GroupCall*> call) {\n\tfor (auto i = begin(_idsForCalls); i != end(_idsForCalls);) {\n\t\tconst auto strong = i->second.get();\n\t\tif (strong == call) {\n\t\t\treturn i->first;\n\t\t} else if (!strong) {\n\t\t\ti = _idsForCalls.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tconst auto itemId = FullMsgId(\n\t\tcall->peer()->id,\n\t\tcall->peer()->owner().nextLocalMessageId());\n\t_idsForCalls.emplace(itemId, call);\n\treturn itemId;\n}\n\nvoid PaidReactionToast::clearHiddenHiding() {\n\t_hiding.erase(\n\t\tranges::remove(\n\t\t\t_hiding,\n\t\t\tnullptr,\n\t\t\t&base::weak_ptr<Ui::Toast::Instance>::get),\n\t\tend(_hiding));\n}\n\nvoid PaidReactionToast::setupLottiePreview(\n\t\tnot_null<Ui::RpWidget*> widget,\n\t\tint size) {\n\tconst auto document = _owner->reactions().paidToastAnimation();\n\n\tconst auto bytes = document->createMediaView()->bytes();\n\tconst auto filepath = document->filepath();\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto player = widget->lifetime().make_state<Lottie::SinglePlayer>(\n\t\tLottie::ReadContent(bytes, filepath),\n\t\tLottie::FrameRequest{ QSize(size, size) * ratio },\n\t\tLottie::Quality::Default);\n\n\twidget->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tif (!player->ready()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto image = player->frame();\n\t\tQPainter(widget).drawImage(\n\t\t\tQRect(QPoint(), image.size() / ratio),\n\t\t\timage);\n\t\tif (player->frameIndex() + 1 != player->framesCount()) {\n\t\t\tplayer->markFrameShown();\n\t\t}\n\t}, widget->lifetime());\n\n\tplayer->updates(\n\t) | rpl::on_next([=] {\n\t\twidget->update();\n\t}, widget->lifetime());\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"styles/style_widgets.h\"\n\nnamespace Calls {\nclass GroupCall;\n} // namespace Calls\n\nnamespace Data {\nclass Session;\n} // namespace Data\n\nnamespace Ui {\n//class Show;\nclass RpWidget;\n} // namespace Ui\n\nnamespace Ui::Toast {\nclass Instance;\n} // namespace Ui::Toast\n\nnamespace HistoryView {\n\nclass Element;\n\nclass PaidReactionToast final {\npublic:\n\tPaidReactionToast(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Data::Session*> owner,\n\t\trpl::producer<int> topOffset,\n\t\tFn<bool(not_null<const Element*> view)> mine);\n\tPaidReactionToast(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Data::Session*> owner,\n\t\trpl::producer<int> topOffset,\n\t\tFn<bool(not_null<Calls::GroupCall*> call)> mine);\n\t~PaidReactionToast();\n\n\t[[nodiscard]] rpl::producer<FullMsgId> shownForId() const;\n\t[[nodiscard]] rpl::producer<Calls::GroupCall*> shownForCall() const;\n\nprivate:\n\tbool maybeShowFor(not_null<HistoryItem*> item);\n\tbool maybeShowFor(not_null<Calls::GroupCall*> call);\n\tvoid showFor(\n\t\tFullMsgId itemId,\n\t\tint count,\n\t\tPeerId shownPeer,\n\t\tcrl::time left,\n\t\tcrl::time total);\n\n\t[[nodiscard]] FullMsgId idForCall(not_null<Calls::GroupCall*> call);\n\n\tvoid setupLottiePreview(not_null<Ui::RpWidget*> widget, int size);\n\tvoid clearHiddenHiding();\n\n\tconst not_null<Ui::RpWidget*> _parent;\n\tconst not_null<Data::Session*> _owner;\n\tconst rpl::variable<int> _topOffset;\n\n\tbase::weak_ptr<Ui::Toast::Instance> _weak;\n\tstd::vector<base::weak_ptr<Ui::Toast::Instance>> _hiding;\n\trpl::variable<int> _count;\n\trpl::variable<PeerId> _shownPeer;\n\trpl::variable<crl::time> _timeFinish;\n\n\tstd::vector<FullMsgId> _stack;\n\n\tbase::flat_map<FullMsgId, base::weak_ptr<Calls::GroupCall>> _idsForCalls;\n\n\trpl::variable<FullMsgId> _shownForId;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_pinned_bar.h\"\n\n#include \"api/api_bot.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_web_page.h\"\n#include \"history/view/history_view_pinned_tracker.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\"\n#include \"base/weak_ptr.h\"\n#include \"apiwrap.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace HistoryView {\nnamespace {\n\n[[nodiscard]] Ui::MessageBarContent ContentWithoutPreview(\n\t\tnot_null<HistoryItem*> item,\n\t\tFn<void()> repaint) {\n\treturn Ui::MessageBarContent{\n\t\t.text = item->inReplyText(),\n\t\t.context = Core::TextContext({\n\t\t\t.session = &item->history()->session(),\n\t\t\t.repaint = std::move(repaint),\n\t\t}),\n\t};\n}\n\n[[nodiscard]] Ui::MessageBarContent ContentWithPreview(\n\t\tnot_null<HistoryItem*> item,\n\t\tImage *preview,\n\t\tbool spoiler,\n\t\tFn<void()> repaint) {\n\tauto result = ContentWithoutPreview(item, repaint);\n\tif (!preview) {\n\t\tstatic const auto kEmpty = [&] {\n\t\t\tconst auto size = st::historyReplyHeight\n\t\t\t\t* style::DevicePixelRatio();\n\t\t\tauto result = QImage(\n\t\t\t\tQSize(size, size),\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tresult.fill(Qt::transparent);\n\t\t\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\treturn result;\n\t\t}();\n\t\tresult.preview = kEmpty;\n\t\tresult.spoilerRepaint = nullptr;\n\t} else {\n\t\tresult.preview = Images::Round(\n\t\t\tpreview->original(),\n\t\t\tImageRoundRadius::Small);\n\t\tresult.spoilerRepaint = spoiler ? repaint : nullptr;\n\t}\n\treturn result;\n}\n\n[[nodiscard]] rpl::producer<Ui::MessageBarContent> ContentByItem(\n\t\tnot_null<HistoryItem*> item,\n\t\tFn<void()> repaint) {\n\treturn item->history()->session().changes().messageFlagsValue(\n\t\titem,\n\t\tData::MessageUpdate::Flag::Edited\n\t) | rpl::map([=]() -> rpl::producer<Ui::MessageBarContent> {\n\t\tconst auto media = item->media();\n\t\tif (!media || !media->hasReplyPreview()) {\n\t\t\treturn rpl::single(ContentWithoutPreview(item, repaint));\n\t\t}\n\t\tconstexpr auto kFullLoaded = 2;\n\t\tconst auto loadedLevel = [=] {\n\t\t\tconstexpr auto kSomeLoaded = 1;\n\t\t\tconstexpr auto kNotLoaded = 0;\n\t\t\tconst auto preview = media->replyPreview();\n\t\t\treturn media->replyPreviewLoaded()\n\t\t\t\t? kFullLoaded\n\t\t\t\t: preview\n\t\t\t\t? kSomeLoaded\n\t\t\t\t: kNotLoaded;\n\t\t};\n\t\treturn rpl::single(\n\t\t\tloadedLevel()\n\t\t) | rpl::then(\n\t\t\titem->history()->session().downloaderTaskFinished(\n\t\t\t) | rpl::map(loadedLevel)\n\t\t) | rpl::distinct_until_changed(\n\t\t) | rpl::take_while([=](int loadLevel) {\n\t\t\treturn loadLevel < kFullLoaded;\n\t\t}) | rpl::then(\n\t\t\trpl::single(kFullLoaded)\n\t\t) | rpl::map([=] {\n\t\t\treturn ContentWithPreview(\n\t\t\t\titem,\n\t\t\t\tmedia->replyPreview(),\n\t\t\t\tmedia->hasSpoiler(),\n\t\t\t\trepaint);\n\t\t});\n\t}) | rpl::flatten_latest();\n}\n\n[[nodiscard]] rpl::producer<Ui::MessageBarContent> ContentByItemId(\n\t\tnot_null<Main::Session*> session,\n\t\tFullMsgId id,\n\t\tFn<void()> repaint,\n\t\tbool alreadyLoaded = false) {\n\tif (!id) {\n\t\treturn rpl::single(Ui::MessageBarContent());\n\t} else if (const auto item = session->data().message(id)) {\n\t\treturn ContentByItem(item, repaint);\n\t} else if (alreadyLoaded) {\n\t\treturn rpl::single(Ui::MessageBarContent()); // Deleted message?..\n\t}\n\tauto load = rpl::make_producer<Ui::MessageBarContent>([=](auto consumer) {\n\t\tconsumer.put_next(Ui::MessageBarContent{\n\t\t\t.text = { tr::lng_contacts_loading(tr::now) },\n\t\t});\n\t\tconst auto peer = session->data().peer(id.peer);\n\t\tconst auto callback = [=] { consumer.put_done(); };\n\t\tsession->api().requestMessageData(peer, id.msg, callback);\n\t\treturn rpl::lifetime();\n\t});\n\treturn std::move(\n\t\tload\n\t) | rpl::then(rpl::deferred([=] {\n\t\treturn ContentByItemId(session, id, repaint, true);\n\t}));\n}\n\nauto WithPinnedTitle(not_null<Main::Session*> session, PinnedId id) {\n\treturn [=](Ui::MessageBarContent &&content) {\n\t\tconst auto item = session->data().message(id.message);\n\t\tif (!item) {\n\t\t\treturn std::move(content);\n\t\t}\n\t\tcontent.title = (id.index + 1 >= id.count)\n\t\t\t? tr::lng_pinned_message(tr::now)\n\t\t\t: (id.count == 2)\n\t\t\t? tr::lng_pinned_previous(tr::now)\n\t\t\t: (tr::lng_pinned_message(tr::now)\n\t\t\t\t+ \" #\"\n\t\t\t\t+ QString::number(id.index + 1));\n\t\tcontent.count = std::max(id.count, 1);\n\t\tcontent.index = std::clamp(id.index, 0, content.count - 1);\n\t\treturn std::move(content);\n\t};\n}\n\n[[nodiscard]] object_ptr<Ui::RoundButton> MakePinnedBarCustomButton(\n\t\tnot_null<QWidget*> parent,\n\t\tconst QString &buttonText,\n\t\tFn<void()> clickCallback) {\n\tconst auto &stButton = st::historyPinnedBotButton;\n\tconst auto &stLabel = st::historyPinnedBotLabel;\n\n\tauto button = object_ptr<Ui::RoundButton>(\n\t\tparent,\n\t\trpl::never<QString>(), // Text is handled by the inner label.\n\t\tstButton);\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton.data(),\n\t\tbuttonText,\n\t\tstLabel);\n\n\tif (label->width() > st::historyPinnedBotButtonMaxWidth) {\n\t\tlabel->resizeToWidth(st::historyPinnedBotButtonMaxWidth);\n\t}\n\tbutton->setFullWidth(label->width()\n\t\t+ stButton.padding.left()\n\t\t+ stButton.padding.right()\n\t\t+ stButton.height); // stButton.height is likely for icon spacing.\n\n\tlabel->moveToLeft(\n\t\tstButton.padding.left() + stButton.height / 2,\n\t\t(button->height() - label->height()) / 2);\n\n\tlabel->setTextColorOverride(stButton.textFg->c); // Use button's text color for label.\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tbutton->setFullRadius(true);\n\tbutton->setClickedCallback(std::move(clickCallback));\n\n\treturn button;\n}\n\n} // namespace\n\nrpl::producer<Ui::MessageBarContent> MessageBarContentByItemId(\n\t\tnot_null<Main::Session*> session,\n\t\tFullMsgId id,\n\t\tFn<void()> repaint) {\n\treturn ContentByItemId(session, id, std::move(repaint));\n}\n\nrpl::producer<Ui::MessageBarContent> PinnedBarContent(\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<PinnedId> id,\n\t\tFn<void()> repaint) {\n\treturn std::move(\n\t\tid\n\t) | rpl::distinct_until_changed(\n\t) | rpl::map([=](PinnedId id) {\n\t\treturn ContentByItemId(\n\t\t\tsession,\n\t\t\tid.message,\n\t\t\trepaint\n\t\t) | rpl::map(WithPinnedTitle(session, id));\n\t}) | rpl::flatten_latest();\n}\n\nrpl::producer<HistoryItem*> PinnedBarItemWithCustomButton(\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<PinnedId> id) {\n\treturn rpl::make_producer<HistoryItem*>([=,\n\t\t\tid = std::move(id)](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tconsumer.put_next(nullptr);\n\n\t\tstruct State {\n\t\t\tbool hasCustomButton = false;\n\t\t\tbase::has_weak_ptr guard;\n\t\t\trpl::lifetime lifetime;\n\t\t\tFullMsgId resolvedId;\n\t\t};\n\t\tconst auto state = lifetime.make_state<State>();\n\n\t\tconst auto pushUnique = [=](not_null<HistoryItem*> item) {\n\t\t\tconst auto replyMarkup = item->inlineReplyMarkup();\n\t\t\tconst auto media = item->media();\n\t\t\tconst auto page = media ? media->webpage() : nullptr;\n\t\t\tconst auto possiblyHasCustomButton = replyMarkup\n\t\t\t\t|| (page\n\t\t\t\t\t&& (page->type == WebPageType::VoiceChat\n\t\t\t\t\t\t|| page->type == WebPageType::Livestream\n\t\t\t\t\t\t|| page->type == WebPageType::ConferenceCall));\n\t\t\tif (!state->hasCustomButton && !possiblyHasCustomButton) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->hasCustomButton = possiblyHasCustomButton;\n\t\t\tconsumer.put_next(item.get());\n\t\t};\n\n\t\trpl::duplicate(\n\t\t\tid\n\t\t) | rpl::filter([=](PinnedId current) {\n\t\t\treturn current.message && (current.message != state->resolvedId);\n\t\t}) | rpl::on_next([=](PinnedId current) {\n\t\t\tconst auto fullId = current.message;\n\t\t\tstate->lifetime.destroy();\n\t\t\tstate->resolvedId = fullId;\n\t\t\tinvalidate_weak_ptrs(&state->guard);\n\n\t\t\tconst auto messageFlag = [=](not_null<HistoryItem*> item) {\n\t\t\t\tusing Update = Data::MessageUpdate;\n\t\t\t\tsession->changes().messageUpdates(\n\t\t\t\t\titem,\n\t\t\t\t\t(Update::Flag::ReplyMarkup\n\t\t\t\t\t\t| Update::Flag::Edited\n\t\t\t\t\t\t| Update::Flag::Destroyed)\n\t\t\t\t) | rpl::on_next([=](const Update &update) {\n\t\t\t\t\tif (update.flags & Update::Flag::Destroyed) {\n\t\t\t\t\t\tstate->lifetime.destroy();\n\t\t\t\t\t\tinvalidate_weak_ptrs(&state->guard);\n\t\t\t\t\t\tstate->hasCustomButton = false;\n\t\t\t\t\t\tconsumer.put_next(nullptr);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tpushUnique(update.item);\n\t\t\t\t\t}\n\t\t\t\t}, state->lifetime);\n\t\t\t\tpushUnique(item);\n\t\t\t};\n\t\t\tif (const auto item = session->data().message(fullId)) {\n\t\t\t\tmessageFlag(item);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto resolved = crl::guard(&state->guard, [=] {\n\t\t\t\tif (const auto item = session->data().message(fullId)) {\n\t\t\t\t\tmessageFlag(item);\n\t\t\t\t}\n\t\t\t});\n\t\t\tsession->api().requestMessageData(\n\t\t\t\tsession->data().peer(fullId.peer),\n\t\t\t\tfullId.msg,\n\t\t\t\tresolved);\n\t\t}, lifetime);\n\t\treturn lifetime;\n\t});\n}\n\n[[nodiscard]] object_ptr<Ui::RoundButton> CreatePinnedBarCustomButton(\n\t\tnot_null<QWidget*> parent,\n\t\tHistoryItem *item,\n\t\tFn<ClickHandlerContext(FullMsgId)> context) {\n\tif (!item) {\n\t\treturn nullptr;\n\t} else if (const auto replyMarkup = item->inlineReplyMarkup()) {\n\t\tconst auto &rows = replyMarkup->data.rows;\n\t\tif ((rows.size() == 1) && (rows.front().size() == 1)) {\n\t\t\tconst auto text = rows.front().front().text;\n\t\t\tif (!text.isEmpty()) {\n\t\t\t\tconst auto contextId = item->fullId();\n\t\t\t\tconst auto callback = [=] {\n\t\t\t\t\tApi::ActivateBotCommand(context(contextId), 0, 0);\n\t\t\t\t};\n\t\t\t\treturn MakePinnedBarCustomButton(parent, text, callback);\n\t\t\t}\n\t\t}\n\t} else if (const auto media = item->media()) {\n\t\tif (const auto page = media->webpage()) {\n\t\t\tif (page->type == WebPageType::VoiceChat\n\t\t\t\t|| page->type == WebPageType::Livestream\n\t\t\t\t|| page->type == WebPageType::ConferenceCall) {\n\t\t\t\tconst auto url = page->url;\n\t\t\t\tconst auto contextId = item->fullId();\n\t\t\t\tconst auto callback = [=] {\n\t\t\t\t\tUrlClickHandler::Open(\n\t\t\t\t\t\turl,\n\t\t\t\t\t\tQVariant::fromValue(context(contextId)));\n\t\t\t\t};\n\t\t\t\tconst auto text = tr::lng_group_call_join(tr::now);\n\t\t\t\treturn MakePinnedBarCustomButton(parent, text, callback);\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_pinned_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"ui/chat/message_bar.h\"\n\n#include <tuple>\n\nstruct ClickHandlerContext;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass IconButton;\nclass PlainShadow;\nclass RoundButton;\nstruct MessageBarContent;\n} // namespace Ui\n\nnamespace HistoryView {\n\n[[nodiscard]] rpl::producer<Ui::MessageBarContent> MessageBarContentByItemId(\n\tnot_null<Main::Session*> session,\n\tFullMsgId id,\n\tFn<void()> repaint);\n\nenum class PinnedIdType;\nstruct PinnedId {\n\tFullMsgId message;\n\tint index = 0;\n\tint count = 1;\n\n\tbool operator<(const PinnedId &other) const {\n\t\treturn std::tie(message, index, count)\n\t\t\t< std::tie(other.message, other.index, other.count);\n\t}\n\tbool operator==(const PinnedId &other) const {\n\t\treturn std::tie(message, index, count)\n\t\t\t== std::tie(other.message, other.index, other.count);\n\t}\n\tbool operator!=(const PinnedId &other) const {\n\t\treturn !(*this == other);\n\t}\n};\n[[nodiscard]] rpl::producer<Ui::MessageBarContent> PinnedBarContent(\n\tnot_null<Main::Session*> session,\n\trpl::producer<PinnedId> id,\n\tFn<void()> repaint);\n\n[[nodiscard]] rpl::producer<HistoryItem*> PinnedBarItemWithCustomButton(\n\tnot_null<Main::Session*> session,\n\trpl::producer<PinnedId> id);\n\n[[nodiscard]] object_ptr<Ui::RoundButton> CreatePinnedBarCustomButton(\n\tnot_null<QWidget*> parent,\n\tHistoryItem *item,\n\tFn<ClickHandlerContext(FullMsgId)> context);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_pinned_section.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_pinned_section.h\"\n\n#include \"history/view/history_view_top_bar_widget.h\"\n#include \"history/view/history_view_translate_bar.h\"\n#include \"history/view/history_view_list_widget.h\"\n#include \"history/view/controls/history_view_compose_search.h\"\n#include \"api/api_messages_search.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"history/history.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item.h\"\n#include \"history/history_view_swipe_back_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/item_text_options.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/ui_utility.h\"\n#include \"base/timer_rpl.h\"\n#include \"apiwrap.h\"\n#include \"window/window_adaptive.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_peer_menu.h\"\n#include \"base/event_filter.h\"\n#include \"base/call_delayed.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"core/application.h\"\n#include \"core/file_utilities.h\"\n#include \"core/shortcuts.h\"\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_sparse_ids.h\"\n#include \"data/data_shared_media.h\"\n#include \"data/data_peer_values.h\"\n#include \"storage/storage_account.h\"\n#include \"platform/platform_specific.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_boxes.h\"\n\n#include <QtCore/QMimeData>\n\nnamespace HistoryView {\nnamespace {\n\n} // namespace\n\nPinnedMemento::PinnedMemento(\n\tnot_null<Data::Thread*> thread,\n\tUniversalMsgId highlightId)\n: _thread(thread)\n, _highlightId(highlightId) {\n\t_list.setAroundPosition({\n\t\t.fullId = FullMsgId(_thread->peer()->id, highlightId),\n\t\t.date = TimeId(0),\n\t});\n}\n\nobject_ptr<Window::SectionWidget> PinnedMemento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tWindow::Column column,\n\t\tconst QRect &geometry) {\n\tif (column == Window::Column::Third) {\n\t\treturn nullptr;\n\t}\n\tauto result = object_ptr<PinnedWidget>(\n\t\tparent,\n\t\tcontroller,\n\t\t_thread);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nData::ForumTopic *PinnedMemento::topicForRemoveRequests() const {\n\treturn _thread->asTopic();\n}\n\nData::SavedSublist *PinnedMemento::sublistForRemoveRequests() const {\n\treturn _thread->asSublist();\n}\n\nPinnedWidget::PinnedWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Data::Thread*> thread)\n: Window::SectionWidget(parent, controller, thread->peer())\n, WindowListDelegate(controller)\n, _thread(thread->migrateToOrMe())\n, _history(thread->owningHistory())\n, _migratedPeer(thread->asHistory()\n\t? thread->asHistory()->peer->migrateFrom()\n\t: nullptr)\n, _topBar(this, controller)\n, _topBarShadow(this)\n, _translateBar(std::make_unique<TranslateBar>(this, controller, _history))\n, _scroll(std::make_unique<Ui::ScrollArea>(\n\tthis,\n\tcontroller->chatStyle()->value(lifetime(), st::historyScroll),\n\tfalse))\n, _clearButton(std::make_unique<Ui::FlatButton>(\n\tthis,\n\tQString(),\n\tst::historyComposeButton))\n, _cornerButtons(\n\t\t_scroll.get(),\n\t\tcontroller->chatStyle(),\n\t\tstatic_cast<HistoryView::CornerButtonsDelegate*>(this)) {\n\tcontroller->chatStyle()->paletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_scroll->updateBars();\n\t}, _scroll->lifetime());\n\n\tWindow::ChatThemeValueFromPeer(\n\t\tcontroller,\n\t\tthread->peer()\n\t) | rpl::on_next([=](std::shared_ptr<Ui::ChatTheme> &&theme) {\n\t\t_theme = std::move(theme);\n\t\tcontroller->setChatStyleTheme(_theme);\n\t}, lifetime());\n\n\t_topBar->setActiveChat(\n\t\tTopBarWidget::ActiveChat{\n\t\t\t.key = _thread,\n\t\t\t.section = Dialogs::EntryState::Section::Pinned,\n\t\t},\n\t\tnullptr);\n\n\t_topBar->move(0, 0);\n\t_topBar->resizeToWidth(width());\n\t_topBar->show();\n\t_topBar->setCustomTitle(tr::lng_contacts_loading(tr::now));\n\n\t_topBar->deleteSelectionRequest(\n\t) | rpl::on_next([=] {\n\t\tconfirmDeleteSelected();\n\t}, _topBar->lifetime());\n\t_topBar->forwardSelectionRequest(\n\t) | rpl::on_next([=] {\n\t\tconfirmForwardSelected();\n\t}, _topBar->lifetime());\n\t_topBar->clearSelectionRequest(\n\t) | rpl::on_next([=] {\n\t\tclearSelected();\n\t}, _topBar->lifetime());\n\t_topBar->searchRequest(\n\t) | rpl::on_next([=] {\n\t\tsearchInPinned();\n\t}, _topBar->lifetime());\n\n\t_translateBar->raise();\n\t_topBarShadow->raise();\n\tcontroller->adaptive().value(\n\t) | rpl::on_next([=] {\n\t\tupdateAdaptiveLayout();\n\t}, lifetime());\n\n\t_inner = _scroll->setOwnedWidget(object_ptr<ListWidget>(\n\t\tthis,\n\t\t&controller->session(),\n\t\tstatic_cast<ListDelegate*>(this)));\n\t_scroll->move(0, _topBar->height());\n\t_scroll->show();\n\t_scroll->scrolls(\n\t) | rpl::on_next([=] {\n\t\tonScroll();\n\t}, lifetime());\n\n\t_inner->scrollKeyEvents(\n\t) | rpl::on_next([=](not_null<QKeyEvent*> e) {\n\t\t_scroll->keyPressEvent(e);\n\t}, lifetime());\n\n\tsetupClearButton();\n\tsetupTranslateBar();\n\tsetupShortcuts();\n\tWindow::SetupSwipeBackSection(this, _scroll.get(), _inner);\n}\n\nPinnedWidget::~PinnedWidget() = default;\n\nvoid PinnedWidget::setupClearButton() {\n\tData::CanPinMessagesValue(\n\t\t_history->peer\n\t) | rpl::on_next([=] {\n\t\trefreshClearButtonText();\n\t}, _clearButton->lifetime());\n\n\t_clearButton->setClickedCallback([=] {\n\t\tif (!_history->peer->canPinMessages()) {\n\t\t\tconst auto callback = [=] {\n\t\t\t\tcontroller()->showBackFromStack();\n\t\t\t};\n\t\t\tWindow::HidePinnedBar(\n\t\t\t\tcontroller(),\n\t\t\t\t_history->peer,\n\t\t\t\t_thread->topicRootId(),\n\t\t\t\t_thread->monoforumPeerId(),\n\t\t\t\tcrl::guard(this, callback));\n\t\t} else {\n\t\t\tWindow::UnpinAllMessages(controller(), _thread);\n\t\t}\n\t});\n}\n\nvoid PinnedWidget::setupTranslateBar() {\n\tcontroller()->adaptive().oneColumnValue(\n\t) | rpl::on_next([=, raw = _translateBar.get()](bool one) {\n\t\traw->setShadowGeometryPostprocess([=](QRect geometry) {\n\t\t\tif (!one) {\n\t\t\t\tgeometry.setLeft(geometry.left() + st::lineWidth);\n\t\t\t}\n\t\t\treturn geometry;\n\t\t});\n\t}, _translateBar->lifetime());\n\n\t_translateBarHeight = 0;\n\t_translateBar->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tif (const auto delta = height - _translateBarHeight) {\n\t\t\t_translateBarHeight = height;\n\t\t\tsetGeometryWithTopMoved(geometry(), delta);\n\t\t}\n\t}, _translateBar->lifetime());\n\n\t_translateBar->finishAnimating();\n}\n\nvoid PinnedWidget::setupShortcuts() {\n\tShortcuts::Requests(\n\t) | rpl::filter([=] {\n\t\treturn Ui::AppInFocus()\n\t\t\t&& Ui::InFocusChain(this)\n\t\t\t&& !controller()->isLayerShown()\n\t\t\t&& (Core::App().activeWindow() == &controller()->window())\n\t\t\t&& !_history->peer->isSelf();\n\t}) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {\n\t\tusing Command = Shortcuts::Command;\n\t\trequest->check(Command::Search, 1) && request->handle([=] {\n\t\t\tsearchInPinned();\n\t\t\treturn true;\n\t\t});\n\t}, lifetime());\n}\n\nvoid PinnedWidget::searchInPinned() {\n\tif (_history->peer->isSelf()) {\n\t\treturn;\n\t}\n\tif (_composeSearch) {\n\t\t_composeSearch->setInnerFocus();\n\t\treturn;\n\t}\n\t_composeSearch = std::make_unique<ComposeSearch>(\n\t\tthis,\n\t\tcontroller(),\n\t\t_history,\n\t\tnullptr);\n\t_composeSearch->setSearchFilter(Api::SearchFilter::Pinned);\n\t_composeSearch->setCalendarChat(Dialogs::Key(_thread));\n\t_composeSearch->setCalendarJumpHandler(crl::guard(this, [=](\n\t\t\tFullMsgId id,\n\t\t\tFn<void()> close) {\n\t\tconst auto universalId = (id.peer == _history->peer->id)\n\t\t\t? id.msg\n\t\t\t: (id.msg - ServerMaxMsgId);\n\t\tSharedMediaMergedViewer(\n\t\t\t&_thread->session(),\n\t\t\tSharedMediaMergedKey(\n\t\t\t\tSparseIdsMergedSlice::Key(\n\t\t\t\t\t_history->peer->id,\n\t\t\t\t\t_thread->topicRootId(),\n\t\t\t\t\t_thread->monoforumPeerId(),\n\t\t\t\t\t_migratedPeer ? _migratedPeer->id : 0,\n\t\t\t\t\tuniversalId),\n\t\t\t\tStorage::SharedMediaType::Pinned),\n\t\t\t1,\n\t\t\t1\n\t\t) | rpl::filter([=](const SparseIdsMergedSlice &slice) {\n\t\t\treturn (slice.size() > 0)\n\t\t\t\t|| (slice.fullCount().value_or(-1) == 0);\n\t\t}) | rpl::take(1) | rpl::on_next([=](\n\t\t\t\tconst SparseIdsMergedSlice &slice) {\n\t\t\tif (const auto nearest = slice.nearest(universalId)) {\n\t\t\t\tshowAtPosition(Data::MessagePosition{\n\t\t\t\t\t.fullId = *nearest,\n\t\t\t\t\t.date = TimeId(0),\n\t\t\t\t});\n\t\t\t}\n\t\t\tclose();\n\t\t}, lifetime());\n\t}));\n\tif (const auto topic = _thread->asTopic()) {\n\t\t_composeSearch->setTopMsgId(topic->rootId());\n\t}\n\n\t_topBarShadow->hide();\n\t_clearButton->hide();\n\tupdateControlsGeometry();\n\tdoSetInnerFocus();\n\n\t_composeSearch->activations(\n\t) | rpl::on_next([=](ComposeSearch::Activation activation) {\n\t\tshowAtPosition(activation.item->position());\n\t}, _composeSearch->lifetime());\n\n\t_composeSearch->destroyRequests(\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t_composeSearch = nullptr;\n\n\t\t_topBarShadow->show();\n\t\t_clearButton->show();\n\t\tupdateControlsGeometry();\n\t\tdoSetInnerFocus();\n\t}, _composeSearch->lifetime());\n}\n\nvoid PinnedWidget::cornerButtonsShowAtPosition(\n\t\tData::MessagePosition position) {\n\tshowAtPosition(position);\n}\n\nData::Thread *PinnedWidget::cornerButtonsThread() {\n\treturn _thread;\n}\n\nFullMsgId PinnedWidget::cornerButtonsCurrentId() {\n\treturn {};\n}\n\nbool PinnedWidget::cornerButtonsIgnoreVisibility() {\n\treturn animatingShow();\n}\n\nstd::optional<bool> PinnedWidget::cornerButtonsDownShown() {\n\tconst auto top = _scroll->scrollTop() + st::historyToDownShownAfter;\n\tif (top < _scroll->scrollTopMax() || _cornerButtons.replyReturn()) {\n\t\treturn true;\n\t} else if (_inner->loadedAtBottomKnown()) {\n\t\treturn !_inner->loadedAtBottom();\n\t}\n\treturn std::nullopt;\n}\n\nbool PinnedWidget::cornerButtonsUnreadMayBeShown() {\n\treturn _inner->loadedAtBottomKnown();\n}\n\nbool PinnedWidget::cornerButtonsHas(CornerButtonType type) {\n\treturn (type == CornerButtonType::Down)\n\t\t|| (type == CornerButtonType::PollVotes);\n}\n\nvoid PinnedWidget::showAtPosition(\n\t\tData::MessagePosition position,\n\t\tFullMsgId originId) {\n\t_inner->showAtPosition(\n\t\tposition,\n\t\t{},\n\t\t_cornerButtons.doneJumpFrom(position.fullId, originId));\n}\n\nvoid PinnedWidget::updateAdaptiveLayout() {\n\t_topBarShadow->moveToLeft(\n\t\tcontroller()->adaptive().isOneColumn() ? 0 : st::lineWidth,\n\t\t_topBar->height());\n}\n\nnot_null<Data::Thread*> PinnedWidget::thread() const {\n\treturn _thread;\n}\n\nDialogs::RowDescriptor PinnedWidget::activeChat() const {\n\treturn {\n\t\t_thread,\n\t\tFullMsgId(_history->peer->id, ShowAtUnreadMsgId)\n\t};\n}\n\nQPixmap PinnedWidget::grabForShowAnimation(const Window::SectionSlideParams &params) {\n\t_topBar->updateControlsVisibility();\n\tif (params.withTopBarShadow) _topBarShadow->hide();\n\tauto result = Ui::GrabWidget(this);\n\tif (params.withTopBarShadow) _topBarShadow->show();\n\t_translateBar->hide();\n\treturn result;\n}\n\nvoid PinnedWidget::checkActivation() {\n\t_inner->checkActivation();\n}\n\nvoid PinnedWidget::doSetInnerFocus() {\n\tif (_composeSearch) {\n\t\t_composeSearch->setInnerFocus();\n\t} else {\n\t\t_inner->setFocus();\n\t}\n}\n\nbool PinnedWidget::showInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) {\n\tif (auto logMemento = dynamic_cast<PinnedMemento*>(memento.get())) {\n\t\tif (logMemento->getThread() == thread()\n\t\t\t|| logMemento->getThread()->migrateToOrMe() == thread()) {\n\t\t\trestoreState(logMemento);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid PinnedWidget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<PinnedMemento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nstd::shared_ptr<Window::SectionMemento> PinnedWidget::createMemento() {\n\tauto result = std::make_shared<PinnedMemento>(thread());\n\tsaveState(result.get());\n\treturn result;\n}\n\nbool PinnedWidget::showMessage(\n\t\tPeerId peerId,\n\t\tconst Window::SectionShow &params,\n\t\tMsgId messageId) {\n\treturn false; // We want 'Go to original' to work.\n}\n\nvoid PinnedWidget::saveState(not_null<PinnedMemento*> memento) {\n\t_inner->saveState(memento->list());\n}\n\nvoid PinnedWidget::restoreState(not_null<PinnedMemento*> memento) {\n\t_inner->restoreState(memento->list());\n\tif (const auto highlight = memento->getHighlightId()) {\n\t\t_inner->showAtPosition(Data::MessagePosition{\n\t\t\t.fullId = ((highlight > 0 || !_migratedPeer)\n\t\t\t\t? FullMsgId(_history->peer->id, highlight)\n\t\t\t\t: FullMsgId(_migratedPeer->id, -highlight)),\n\t\t\t.date = TimeId(0),\n\t\t}, { Window::SectionShow::Way::Forward, anim::type::instant });\n\t}\n}\n\nvoid PinnedWidget::resizeEvent(QResizeEvent *e) {\n\tif (!width() || !height()) {\n\t\treturn;\n\t}\n\trecountChatWidth();\n\tupdateControlsGeometry();\n}\n\nvoid PinnedWidget::recountChatWidth() {\n\tauto layout = (width() < st::adaptiveChatWideWidth)\n\t\t? Window::Adaptive::ChatLayout::Normal\n\t\t: Window::Adaptive::ChatLayout::Wide;\n\tcontroller()->adaptive().setChatLayout(layout);\n}\n\nvoid PinnedWidget::setMessagesCount(int count) {\n\tif (_messagesCount == count) {\n\t\treturn;\n\t}\n\t_messagesCount = count;\n\t_topBar->setCustomTitle(\n\t\ttr::lng_pinned_messages_title(tr::now, lt_count, count));\n\trefreshClearButtonText();\n}\n\nvoid PinnedWidget::refreshClearButtonText() {\n\tconst auto can = _history->peer->canPinMessages();\n\t_clearButton->setText(can\n\t\t? tr::lng_pinned_unpin_all(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tstd::max(_messagesCount, 1)).toUpper()\n\t\t: tr::lng_pinned_hide_all(tr::now).toUpper());\n}\n\nvoid PinnedWidget::updateControlsGeometry() {\n\tconst auto contentWidth = width();\n\n\tconst auto newScrollTop = _scroll->isHidden()\n\t\t? std::nullopt\n\t\t: base::make_optional(_scroll->scrollTop() + topDelta());\n\t_topBar->resizeToWidth(contentWidth);\n\t_topBarShadow->resize(contentWidth, st::lineWidth);\n\n\tconst auto bottom = height() - _clearButton->height();\n\t_clearButton->resizeToWidth(width());\n\t_clearButton->move(0, bottom);\n\tconst auto controlsHeight = 0;\n\tauto top = _topBar->height();\n\t_translateBar->move(0, top);\n\t_translateBar->resizeToWidth(contentWidth);\n\ttop += _translateBarHeight;\n\tconst auto scrollHeight = bottom - top - controlsHeight;\n\tconst auto scrollSize = QSize(contentWidth, scrollHeight);\n\tif (_scroll->size() != scrollSize) {\n\t\t_skipScrollEvent = true;\n\t\t_scroll->resize(scrollSize);\n\t\t_inner->resizeToWidth(scrollSize.width(), _scroll->height());\n\t\t_skipScrollEvent = false;\n\t}\n\t_scroll->move(0, top);\n\tif (!_scroll->isHidden()) {\n\t\tif (newScrollTop) {\n\t\t\t_scroll->scrollToY(*newScrollTop);\n\t\t}\n\t\tupdateInnerVisibleArea();\n\t}\n\n\t_cornerButtons.updatePositions();\n}\n\nvoid PinnedWidget::paintEvent(QPaintEvent *e) {\n\tif (animatingShow()) {\n\t\tSectionWidget::paintEvent(e);\n\t\treturn;\n\t} else if (controller()->contentOverlapped(this, e)) {\n\t\treturn;\n\t}\n\n\tconst auto aboveHeight = _topBar->height();\n\tconst auto bg = e->rect().intersected(\n\t\tQRect(0, aboveHeight, width(), height() - aboveHeight));\n\tSectionWidget::PaintBackground(controller(), _theme.get(), this, bg);\n}\n\nvoid PinnedWidget::onScroll() {\n\tif (_skipScrollEvent) {\n\t\treturn;\n\t}\n\tupdateInnerVisibleArea();\n}\n\nvoid PinnedWidget::updateInnerVisibleArea() {\n\tconst auto scrollTop = _scroll->scrollTop();\n\t_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());\n\t_cornerButtons.updateJumpDownVisibility();\n\t_cornerButtons.updateUnreadThingsVisibility();\n}\n\nvoid PinnedWidget::showAnimatedHook(\n\t\tconst Window::SectionSlideParams &params) {\n\t_topBar->setAnimatingMode(true);\n\tif (params.withTopBarShadow) {\n\t\t_topBarShadow->show();\n\t}\n}\n\nvoid PinnedWidget::showFinishedHook() {\n\t_topBar->setAnimatingMode(false);\n\t_inner->showFinished();\n\t_translateBar->show();\n}\n\nbool PinnedWidget::floatPlayerHandleWheelEvent(QEvent *e) {\n\treturn _scroll->viewportEvent(e);\n}\n\nQRect PinnedWidget::floatPlayerAvailableRect() {\n\treturn mapToGlobal(_scroll->geometry());\n}\n\nContext PinnedWidget::listContext() {\n\treturn Context::Pinned;\n}\n\nbool PinnedWidget::listScrollTo(int top, bool syntetic) {\n\ttop = std::clamp(top, 0, _scroll->scrollTopMax());\n\tif (_scroll->scrollTop() == top) {\n\t\tupdateInnerVisibleArea();\n\t\treturn false;\n\t}\n\t_scroll->scrollToY(top);\n\treturn true;\n}\n\nvoid PinnedWidget::listCancelRequest() {\n\tif (_inner && !_inner->getSelectedIds().empty()) {\n\t\tclearSelected();\n\t\treturn;\n\t}\n\tcontroller()->showBackFromStack();\n}\n\nvoid PinnedWidget::listDeleteRequest() {\n\tconfirmDeleteSelected();\n}\n\nvoid PinnedWidget::listTryProcessKeyInput(not_null<QKeyEvent*> e) {\n}\n\nrpl::producer<Data::MessagesSlice> PinnedWidget::listSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tconst auto messageId = aroundId.fullId.msg\n\t\t? aroundId.fullId.msg\n\t\t: (ServerMaxMsgId - 1);\n\n\treturn SharedMediaMergedViewer(\n\t\t&_thread->session(),\n\t\tSharedMediaMergedKey(\n\t\t\tSparseIdsMergedSlice::Key(\n\t\t\t\t_history->peer->id,\n\t\t\t\t_thread->topicRootId(),\n\t\t\t\t_thread->monoforumPeerId(),\n\t\t\t\t_migratedPeer ? _migratedPeer->id : 0,\n\t\t\t\tmessageId),\n\t\t\tStorage::SharedMediaType::Pinned),\n\t\tlimitBefore,\n\t\tlimitAfter\n\t) | rpl::filter([=](const SparseIdsMergedSlice &slice) {\n\t\tconst auto count = slice.fullCount();\n\t\tif (!count.has_value()) {\n\t\t\treturn true;\n\t\t} else if (*count != 0) {\n\t\t\tsetMessagesCount(*count);\n\t\t\treturn true;\n\t\t} else {\n\t\t\tcontroller()->showBackFromStack();\n\t\t\treturn false;\n\t\t}\n\t}) | rpl::map([=](SparseIdsMergedSlice &&slice) {\n\t\tauto result = Data::MessagesSlice();\n\t\tresult.fullCount = slice.fullCount();\n\t\tresult.skippedAfter = slice.skippedAfter();\n\t\tresult.skippedBefore = slice.skippedBefore();\n\t\tconst auto count = slice.size();\n\t\tresult.ids.reserve(count);\n\t\tif (const auto msgId = slice.nearest(messageId)) {\n\t\t\tresult.nearestToAround = *msgId;\n\t\t}\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tresult.ids.push_back(slice[i]);\n\t\t}\n\t\treturn result;\n\t});\n}\n\nbool PinnedWidget::listAllowsMultiSelect() {\n\treturn true;\n}\n\nbool PinnedWidget::listIsItemGoodForSelection(\n\t\tnot_null<HistoryItem*> item) {\n\treturn item->isRegular() && !item->isService();\n}\n\nbool PinnedWidget::listIsLessInOrder(\n\t\tnot_null<HistoryItem*> first,\n\t\tnot_null<HistoryItem*> second) {\n\treturn first->position() < second->position();\n}\n\nvoid PinnedWidget::listSelectionChanged(SelectedItems &&items) {\n\tHistoryView::TopBarWidget::SelectedState state;\n\tstate.count = items.size();\n\tfor (const auto &item : items) {\n\t\tif (item.canDelete) {\n\t\t\t++state.canDeleteCount;\n\t\t}\n\t\tif (item.canForward) {\n\t\t\t++state.canForwardCount;\n\t\t}\n\t}\n\t_topBar->showSelected(state);\n}\n\nvoid PinnedWidget::listMarkReadTill(not_null<HistoryItem*> item) {\n}\n\nvoid PinnedWidget::listMarkContentsRead(\n\tconst base::flat_set<not_null<HistoryItem*>> &items) {\n}\n\nMessagesBarData PinnedWidget::listMessagesBar(\n\t\tconst std::vector<not_null<Element*>> &elements,\n\t\tbool markLastAsRead) {\n\treturn {};\n}\n\nvoid PinnedWidget::listContentRefreshed() {\n}\n\nvoid PinnedWidget::listUpdateDateLink(\n\tClickHandlerPtr &link,\n\tnot_null<Element*> view) {\n}\n\nbool PinnedWidget::listElementHideReply(not_null<const Element*> view) {\n\tif (const auto reply = view->data()->Get<HistoryMessageReply>()) {\n\t\treturn !reply->fields().manualQuote\n\t\t\t&& (reply->messageId() == _thread->topicRootId());\n\t}\n\treturn false;\n}\n\nbool PinnedWidget::listElementShownUnread(not_null<const Element*> view) {\n\treturn view->data()->unread(view->data()->history());\n}\n\nbool PinnedWidget::listIsGoodForAroundPosition(\n\t\tnot_null<const Element*> view) {\n\treturn view->data()->isRegular();\n}\n\nvoid PinnedWidget::listSendBotCommand(\n\tconst QString &command,\n\tconst FullMsgId &context) {\n}\n\nvoid PinnedWidget::listSearch(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) {\n\tconst auto inChat = _history->peer->isUser()\n\t\t? Dialogs::Key()\n\t\t: Dialogs::Key(_history);\n\tcontroller()->searchMessages(query, inChat);\n}\n\nvoid PinnedWidget::listHandleViaClick(not_null<UserData*> bot) {\n}\n\nnot_null<Ui::ChatTheme*> PinnedWidget::listChatTheme() {\n\treturn _theme.get();\n}\n\nCopyRestrictionType PinnedWidget::listCopyRestrictionType(\n\t\tHistoryItem *item) {\n\treturn CopyRestrictionTypeFor(_history->peer, item);\n}\n\nCopyRestrictionType PinnedWidget::listCopyMediaRestrictionType(\n\t\tnot_null<HistoryItem*> item) {\n\treturn CopyMediaRestrictionTypeFor(_history->peer, item);\n}\n\nCopyRestrictionType PinnedWidget::listSelectRestrictionType() {\n\treturn SelectRestrictionTypeFor(_history->peer);\n}\n\nauto PinnedWidget::listAllowedReactionsValue()\n-> rpl::producer<Data::AllowedReactions> {\n\treturn Data::PeerAllowedReactionsValue(_history->peer);\n}\n\nvoid PinnedWidget::listShowPremiumToast(not_null<DocumentData*> document) {\n}\n\nvoid PinnedWidget::listOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) {\n\tconst auto draw = Data::CanSendAnyOf(\n\t\t_thread,\n\t\tData::FilesSendRestrictions());\n\tcontroller()->openPhoto(photo, { .id = context, .showDrawButton = draw });\n}\n\nvoid PinnedWidget::listOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView) {\n\tconst auto draw = Data::CanSendAnyOf(\n\t\t_thread,\n\t\tData::FilesSendRestrictions());\n\tcontroller()->openDocument(\n\t\tdocument,\n\t\tshowInMediaView,\n\t\t{ .id = context, .showDrawButton = draw });\n}\n\nvoid PinnedWidget::listPaintEmpty(\n\tPainter &p,\n\tconst Ui::ChatPaintContext &context) {\n}\n\nQString PinnedWidget::listElementAuthorRank(not_null<const Element*> view) {\n\treturn {};\n}\n\nbool PinnedWidget::listElementHideTopicButton(\n\t\tnot_null<const Element*> view) {\n\treturn true;\n}\n\nHistory *PinnedWidget::listTranslateHistory() {\n\treturn _history;\n}\n\nvoid PinnedWidget::listAddTranslatedItems(\n\tnot_null<TranslateTracker*> tracker) {\n}\n\nUi::ScrollArea *PinnedWidget::listScrollArea() const {\n\treturn _scroll.get();\n}\n\nbool PinnedWidget::listThanosEffectEnabled() const {\n\treturn false;\n}\n\nvoid PinnedWidget::confirmDeleteSelected() {\n\tConfirmDeleteSelectedItems(_inner);\n}\n\nvoid PinnedWidget::confirmForwardSelected() {\n\tConfirmForwardSelectedItems(_inner);\n}\n\nvoid PinnedWidget::clearSelected() {\n\t_inner->cancelSelection();\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_pinned_section.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"window/section_widget.h\"\n#include \"window/section_memento.h\"\n#include \"history/view/history_view_list_widget.h\"\n#include \"history/view/history_view_corner_buttons.h\"\n#include \"data/data_messages.h\"\n#include \"base/weak_ptr.h\"\n#include \"base/timer.h\"\n\nclass History;\n\nnamespace Ui {\nclass ScrollArea;\nclass PlainShadow;\nclass FlatButton;\n} // namespace Ui\n\nnamespace Profile {\nclass BackButton;\n} // namespace Profile\n\nnamespace HistoryView {\n\nclass Element;\nclass TopBarWidget;\nclass ComposeSearch;\nclass PinnedMemento;\nclass TranslateBar;\n\nclass PinnedWidget final\n\t: public Window::SectionWidget\n\t, private WindowListDelegate\n\t, private CornerButtonsDelegate {\npublic:\n\tPinnedWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Data::Thread*> thread);\n\t~PinnedWidget();\n\n\t[[nodiscard]] not_null<Data::Thread*> thread() const;\n\tDialogs::RowDescriptor activeChat() const override;\n\n\tbool hasTopBarShadow() const override {\n\t\treturn true;\n\t}\n\n\tQPixmap grabForShowAnimation(\n\t\tconst Window::SectionSlideParams &params) override;\n\n\tbool showInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) override;\n\tstd::shared_ptr<Window::SectionMemento> createMemento() override;\n\tbool showMessage(\n\t\tPeerId peerId,\n\t\tconst Window::SectionShow &params,\n\t\tMsgId messageId) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<PinnedMemento*> memento);\n\n\tWindow::SectionActionResult sendBotCommand(\n\t\t\tBot::SendCommandRequest request) override {\n\t\treturn Window::SectionActionResult::Fallback;\n\t}\n\n\t// Float player interface.\n\tbool floatPlayerHandleWheelEvent(QEvent *e) override;\n\tQRect floatPlayerAvailableRect() override;\n\n\t// ListDelegate interface.\n\tContext listContext() override;\n\tbool listScrollTo(int top, bool syntetic = true) override;\n\tvoid listCancelRequest() override;\n\tvoid listDeleteRequest() override;\n\tvoid listTryProcessKeyInput(not_null<QKeyEvent*> e) override;\n\trpl::producer<Data::MessagesSlice> listSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) override;\n\tbool listAllowsMultiSelect() override;\n\tbool listIsItemGoodForSelection(not_null<HistoryItem*> item) override;\n\tbool listIsLessInOrder(\n\t\tnot_null<HistoryItem*> first,\n\t\tnot_null<HistoryItem*> second) override;\n\tvoid listSelectionChanged(SelectedItems &&items) override;\n\tvoid listMarkReadTill(not_null<HistoryItem*> item) override;\n\tvoid listMarkContentsRead(\n\t\tconst base::flat_set<not_null<HistoryItem*>> &items) override;\n\tMessagesBarData listMessagesBar(\n\t\tconst std::vector<not_null<Element*>> &elements,\n\t\tbool markLastAsRead) override;\n\tvoid listContentRefreshed() override;\n\tvoid listUpdateDateLink(\n\t\tClickHandlerPtr &link,\n\t\tnot_null<Element*> view) override;\n\tbool listElementHideReply(not_null<const Element*> view) override;\n\tbool listElementShownUnread(not_null<const Element*> view) override;\n\tbool listIsGoodForAroundPosition(\n\t\tnot_null<const Element*> view) override;\n\tvoid listSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) override;\n\tvoid listSearch(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) override;\n\tvoid listHandleViaClick(not_null<UserData*> bot) override;\n\tnot_null<Ui::ChatTheme*> listChatTheme() override;\n\tCopyRestrictionType listCopyRestrictionType(HistoryItem *item) override;\n\tCopyRestrictionType listCopyMediaRestrictionType(\n\t\tnot_null<HistoryItem*> item) override;\n\tCopyRestrictionType listSelectRestrictionType() override;\n\tauto listAllowedReactionsValue()\n\t\t-> rpl::producer<Data::AllowedReactions> override;\n\tvoid listShowPremiumToast(not_null<DocumentData*> document) override;\n\tvoid listOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) override;\n\tvoid listOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView) override;\n\tvoid listPaintEmpty(\n\t\tPainter &p,\n\t\tconst Ui::ChatPaintContext &context) override;\n\tQString listElementAuthorRank(not_null<const Element*> view) override;\n\tbool listElementHideTopicButton(not_null<const Element*> view) override;\n\tHistory *listTranslateHistory() override;\n\tvoid listAddTranslatedItems(\n\t\tnot_null<TranslateTracker*> tracker) override;\n\tUi::ScrollArea *listScrollArea() const override;\n\tbool listThanosEffectEnabled() const override;\n\n\t// CornerButtonsDelegate delegate.\n\tvoid cornerButtonsShowAtPosition(\n\t\tData::MessagePosition position) override;\n\tData::Thread *cornerButtonsThread() override;\n\tFullMsgId cornerButtonsCurrentId() override;\n\tbool cornerButtonsIgnoreVisibility() override;\n\tstd::optional<bool> cornerButtonsDownShown() override;\n\tbool cornerButtonsUnreadMayBeShown() override;\n\tbool cornerButtonsHas(CornerButtonType type) override;\n\nprivate:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid showAnimatedHook(\n\t\tconst Window::SectionSlideParams &params) override;\n\tvoid showFinishedHook() override;\n\tvoid doSetInnerFocus() override;\n\tvoid checkActivation() override;\n\n\tvoid onScroll();\n\tvoid updateInnerVisibleArea();\n\tvoid updateControlsGeometry();\n\tvoid updateAdaptiveLayout();\n\tvoid saveState(not_null<PinnedMemento*> memento);\n\tvoid restoreState(not_null<PinnedMemento*> memento);\n\tvoid showAtPosition(\n\t\tData::MessagePosition position,\n\t\tFullMsgId originId = {});\n\n\tvoid setupClearButton();\n\tvoid setupTranslateBar();\n\tvoid setupShortcuts();\n\tvoid searchInPinned();\n\n\tvoid confirmDeleteSelected();\n\tvoid confirmForwardSelected();\n\tvoid clearSelected();\n\tvoid recountChatWidth();\n\n\tvoid setMessagesCount(int count);\n\tvoid refreshClearButtonText();\n\n\tconst not_null<Data::Thread*> _thread;\n\tconst not_null<History*> _history;\n\tstd::shared_ptr<Ui::ChatTheme> _theme;\n\tPeerData *_migratedPeer = nullptr;\n\tQPointer<ListWidget> _inner;\n\tobject_ptr<TopBarWidget> _topBar;\n\tobject_ptr<Ui::PlainShadow> _topBarShadow;\n\n\tstd::unique_ptr<TranslateBar> _translateBar;\n\tint _translateBarHeight = 0;\n\n\tbool _skipScrollEvent = false;\n\tstd::unique_ptr<Ui::ScrollArea> _scroll;\n\tstd::unique_ptr<Ui::FlatButton> _clearButton;\n\tstd::unique_ptr<ComposeSearch> _composeSearch;\n\n\tCornerButtons _cornerButtons;\n\n\tint _messagesCount = -1;\n\n};\n\nclass PinnedMemento : public Window::SectionMemento {\npublic:\n\tusing UniversalMsgId = MsgId;\n\n\texplicit PinnedMemento(\n\t\tnot_null<Data::Thread*> thread,\n\t\tUniversalMsgId highlightId = 0);\n\n\tobject_ptr<Window::SectionWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tWindow::Column column,\n\t\tconst QRect &geometry) override;\n\n\t[[nodiscard]] not_null<Data::Thread*> getThread() const {\n\t\treturn _thread;\n\t}\n\n\t[[nodiscard]] not_null<ListMemento*> list() {\n\t\treturn &_list;\n\t}\n\t[[nodiscard]] UniversalMsgId getHighlightId() const {\n\t\treturn _highlightId;\n\t}\n\n\tData::ForumTopic *topicForRemoveRequests() const override;\n\tData::SavedSublist *sublistForRemoveRequests() const override;\n\nprivate:\n\tconst not_null<Data::Thread*> _thread;\n\tconst UniversalMsgId _highlightId = 0;\n\tListMemento _list;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_pinned_tracker.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_pinned_tracker.h\"\n\n#include \"data/data_changes.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_shared_media.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"storage/storage_facade.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n\nnamespace HistoryView {\nnamespace{\n\nconstexpr auto kLoadedLimit = 5;\nconstexpr auto kChangeViewerLimit = 2;\n\n} // namespace\n\nPinnedTracker::PinnedTracker(not_null<Data::Thread*> thread)\n: _thread(thread->migrateToOrMe())\n, _migratedPeer(_thread->asHistory()\n\t? _thread->asHistory()->peer->migrateFrom()\n\t: nullptr) {\n\tusing namespace rpl::mappers;\n\tconst auto has = [&](Data::Thread *thread) -> rpl::producer<bool> {\n\t\tauto &changes = _thread->session().changes();\n\t\tconst auto flag = Data::EntryUpdate::Flag::HasPinnedMessages;\n\t\tif (!thread) {\n\t\t\treturn rpl::single(false);\n\t\t}\n\t\treturn changes.entryFlagsValue(thread, flag) | rpl::map([=] {\n\t\t\treturn thread->hasPinnedMessages();\n\t\t});\n\t};\n\trpl::combine(\n\t\thas(_thread),\n\t\thas(_migratedPeer\n\t\t\t? _thread->owner().history(_migratedPeer).get()\n\t\t\t: nullptr),\n\t\t_1 || _2\n\t) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](bool has) {\n\t\tif (has) {\n\t\t\trefreshViewer();\n\t\t} else {\n\t\t\tclear();\n\t\t}\n\t}, _lifetime);\n}\n\nPinnedTracker::~PinnedTracker() = default;\n\nrpl::producer<PinnedId> PinnedTracker::shownMessageId() const {\n\treturn _current.value();\n}\n\nvoid PinnedTracker::reset() {\n\t_current.reset(currentMessageId());\n}\n\nPinnedId PinnedTracker::currentMessageId() const {\n\treturn _current.current();\n}\n\nvoid PinnedTracker::refreshViewer() {\n\tif (_viewerAroundId == _aroundId) {\n\t\treturn;\n\t}\n\t_dataLifetime.destroy();\n\t_viewerAroundId = _aroundId;\n\tconst auto peer = _thread->peer();\n\tSharedMediaMergedViewer(\n\t\t&peer->session(),\n\t\tSharedMediaMergedKey(\n\t\t\tSparseIdsMergedSlice::Key(\n\t\t\t\tpeer->id,\n\t\t\t\t_thread->topicRootId(),\n\t\t\t\t_thread->monoforumPeerId(),\n\t\t\t\t_migratedPeer ? _migratedPeer->id : 0,\n\t\t\t\t_viewerAroundId),\n\t\t\tStorage::SharedMediaType::Pinned),\n\t\tkLoadedLimit,\n\t\tkLoadedLimit\n\t) | rpl::on_next([=](const SparseIdsMergedSlice &result) {\n\t\t_slice.fullCount = result.fullCount();\n\t\t_slice.skippedBefore = result.skippedBefore();\n\t\t_slice.skippedAfter = result.skippedAfter();\n\t\t_slice.ids.clear();\n\t\tconst auto count = result.size();\n\t\t_slice.ids.reserve(count);\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t_slice.ids.push_back(result[i]);\n\t\t}\n\t\trefreshCurrentFromSlice();\n\t\tif (_slice.fullCount == 0) {\n\t\t\t_thread->setHasPinnedMessages(false);\n\t\t\tif (_migratedPeer) {\n\t\t\t\tconst auto to = _thread->owner().history(_migratedPeer);\n\t\t\t\tto->setHasPinnedMessages(false);\n\t\t\t}\n\t\t}\n\t}, _dataLifetime);\n}\n\nvoid PinnedTracker::refreshCurrentFromSlice() {\n\tconst auto proj1 = [](FullMsgId id) {\n\t\treturn peerIsChannel(id.peer) ? id.msg : (id.msg - ServerMaxMsgId);\n\t};\n\tconst auto proj2 = [](FullMsgId id) {\n\t\treturn id.msg;\n\t};\n\tconst auto i = _migratedPeer\n\t\t? ranges::lower_bound(_slice.ids, _aroundId, ranges::less(), proj1)\n\t\t: ranges::lower_bound(_slice.ids, _aroundId, ranges::less(), proj2);\n\tconst auto before = int(i - begin(_slice.ids));\n\tconst auto after = int(end(_slice.ids) - i);\n\tconst auto haveValidData = (before > 0 || _slice.skippedBefore == 0)\n\t\t&& (after > 0 || _slice.skippedAfter == 0);\n\tconst auto nearEnd = !haveValidData\n\t\t|| (before <= kChangeViewerLimit && _slice.skippedBefore != 0)\n\t\t|| (after <= kChangeViewerLimit && _slice.skippedAfter != 0);\n\tif (haveValidData) {\n\t\tconst auto count = std::max(\n\t\t\t_slice.fullCount.value_or(1),\n\t\t\tint(_slice.ids.size()));\n\t\tconst auto index = _slice.skippedBefore.has_value()\n\t\t\t? (*_slice.skippedBefore + before)\n\t\t\t: _slice.skippedAfter.has_value()\n\t\t\t? (count - *_slice.skippedAfter - after)\n\t\t\t: 1;\n\t\tif (i != begin(_slice.ids)) {\n\t\t\t_current = PinnedId{ *(i - 1), index - 1, count };\n\t\t} else if (!_slice.ids.empty()) {\n\t\t\t_current = PinnedId{ _slice.ids.front(), 0, count };\n\t\t} else {\n\t\t\t_current = PinnedId();\n\t\t}\n\t}\n\tif (nearEnd) {\n\t\trefreshViewer();\n\t}\n}\n\nvoid PinnedTracker::clear() {\n\t_dataLifetime.destroy();\n\t_viewerAroundId = 0;\n\t_current = PinnedId();\n}\n\nvoid PinnedTracker::trackAround(UniversalMsgId messageId) {\n\tif (_aroundId == messageId) {\n\t\treturn;\n\t}\n\t_aroundId = messageId;\n\tif (!_aroundId) {\n\t\tclear();\n\t} else {\n\t\trefreshCurrentFromSlice();\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_pinned_tracker.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/history_view_pinned_bar.h\"\n\nclass History;\n\nnamespace Data {\nenum class LoadDirection : char;\nclass Thread;\n} // namespace Data\n\nnamespace HistoryView {\n\nclass PinnedTracker final {\npublic:\n\tusing UniversalMsgId = MsgId;\n\n\texplicit PinnedTracker(not_null<Data::Thread*> thread);\n\t~PinnedTracker();\n\n\t[[nodiscard]] rpl::producer<PinnedId> shownMessageId() const;\n\t[[nodiscard]] PinnedId currentMessageId() const;\n\tvoid trackAround(UniversalMsgId messageId);\n\tvoid reset();\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tstruct Slice {\n\t\tstd::vector<FullMsgId> ids;\n\t\tstd::optional<int> fullCount;\n\t\tstd::optional<int> skippedBefore;\n\t\tstd::optional<int> skippedAfter;\n\t};\n\tvoid clear();\n\tvoid refreshViewer();\n\tvoid refreshCurrentFromSlice();\n\n\tconst not_null<Data::Thread*> _thread;\n\tPeerData *_migratedPeer = nullptr;\n\n\trpl::variable<PinnedId> _current;\n\trpl::lifetime _dataLifetime;\n\n\tUniversalMsgId _aroundId = 0;\n\tUniversalMsgId _viewerAroundId = 0;\n\tSlice _slice;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_quick_action.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_quick_action.h\"\n\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n\nnamespace HistoryView {\n\nDoubleClickQuickAction CurrentQuickAction() {\n\treturn Core::App().settings().chatQuickAction();\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_quick_action.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace HistoryView {\n\nenum class DoubleClickQuickAction {\n\tReply, // Default.\n\tReact,\n\tNone,\n};\n\n[[nodiscard]] DoubleClickQuickAction CurrentQuickAction();\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_reaction_preview.h\"\n\n#include \"base/call_delayed.h\"\n#include \"base/event_filter.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"data/data_document.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mainwindow.h\"\n#include \"ui/cached_special_layer_shadow_corners.h\"\n#include \"ui/effects/show_animation.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"window/window_media_preview.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n\nnamespace HistoryView {\nnamespace {\n\nvoid SetupOverlayHideOnEscape(\n\t\tnot_null<Ui::AbstractButton*> clickable,\n\t\tFn<void()> hideAll) {\n\tclickable->setClickedCallback(hideAll);\n\tbase::install_event_filter(QCoreApplication::instance(), [=](\n\t\t\tnot_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::KeyPress\n\t\t\t&& clickable->window()->isActiveWindow()) {\n\t\t\tconst auto k = static_cast<QKeyEvent*>(e.get());\n\t\t\tif (k->key() == Qt::Key_Escape) {\n\t\t\t\thideAll();\n\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t}\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t}, clickable->lifetime());\n}\n\nstruct PreviewOverlayState {\n\tbase::unique_qptr<Window::MediaPreviewWidget> mediaPreview;\n\tbase::unique_qptr<Ui::AbstractButton> clickable;\n\tbase::unique_qptr<Ui::FadeWrap<Ui::DropdownMenu>> menuWrap;\n\tbase::unique_qptr<Ui::AbstractButton> background;\n\tbase::unique_qptr<Ui::FlatLabel> label;\n\tFn<void()> extraHide;\n\trpl::lifetime shutdownGuard;\n\n\tvoid clear() {\n\t\tshutdownGuard.destroy();\n\t\tmenuWrap.reset();\n\t\tbackground.reset();\n\t\tlabel.reset();\n\t\tmediaPreview.reset();\n\t\tclickable.reset();\n\t}\n};\n\nstruct PreviewOverlay {\n\tstd::shared_ptr<PreviewOverlayState> state;\n\tFn<void()> hideAll;\n};\n\ntemplate <typename MediaData>\n[[nodiscard]] PreviewOverlay CreatePreviewOverlay(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFullMsgId origin,\n\t\tMediaData media) {\n\tconst auto state = std::make_shared<PreviewOverlayState>();\n\n\tconst auto mainwidget = controller->widget()->bodyWidget();\n\tstate->mediaPreview = base::make_unique_q<Window::MediaPreviewWidget>(\n\t\tmainwidget,\n\t\tcontroller);\n\tstate->mediaPreview->setCustomDuration(st::defaultToggle.duration);\n\tstate->clickable = base::make_unique_q<Ui::AbstractButton>(mainwidget);\n\tconst auto hideAll = [=] {\n\t\tstate->clickable->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tstate->mediaPreview->hidePreview();\n\t\tif (state->extraHide) {\n\t\t\tstate->extraHide();\n\t\t}\n\t\tbase::call_delayed(\n\t\t\tst::defaultToggle.duration,\n\t\t\t[=] { state->clear(); });\n\t};\n\tSetupOverlayHideOnEscape(state->clickable.get(), hideAll);\n\tstate->mediaPreview->showPreview(origin, media);\n\tstate->clickable->show();\n\tconst auto clickableRaw = state->clickable.get();\n\n\tmainwidget->sizeValue(\n\t) | rpl::skip(1) | rpl::on_next([=](QSize) {\n\t\thideAll();\n\t}, clickableRaw->lifetime());\n\n\tmainwidget->sizeValue() | rpl::on_next([=](QSize size) {\n\t\tclickableRaw->setGeometry(Rect(size));\n\t\tclickableRaw->raise();\n\t}, clickableRaw->lifetime());\n\n\t// Prevent running state destructor from within a child widget's\n\t// destructor, which would trigger a double-delete through unique_qptr.\n\tmainwidget->death() | rpl::on_next([s = state] {\n\t}, state->shutdownGuard);\n\n\treturn { state, hideAll };\n}\n\nvoid SetupPreviewMenu(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst PreviewOverlay &overlay,\n\t\tFn<void(not_null<Ui::DropdownMenu*>)> fillMenu) {\n\tconst auto &state = overlay.state;\n\tconst auto mainwidget = controller->widget()->bodyWidget();\n\tif (fillMenu) {\n\t\tstate->mediaPreview->setHideEmoji(true);\n\t\tauto menu = object_ptr<Ui::DropdownMenu>(\n\t\t\tmainwidget,\n\t\t\tst::dropdownMenuWithIcons);\n\t\tmenu->setAutoHiding(false);\n\t\tmenu->setHiddenCallback(\n\t\t\tcrl::guard(state->clickable.get(), overlay.hideAll));\n\t\tfillMenu(menu.data());\n\t\tstate->menuWrap = base::make_unique_q<Ui::FadeWrap<Ui::DropdownMenu>>(\n\t\t\tmainwidget,\n\t\t\tstd::move(menu));\n\t\tstate->menuWrap->setDuration(st::defaultToggle.duration);\n\t\tstate->menuWrap->hide(anim::type::instant);\n\t}\n\tconst auto wrapRaw = state->menuWrap.get();\n\tstate->extraHide = [=] {\n\t\tif (wrapRaw) {\n\t\t\twrapRaw->hide(anim::type::normal);\n\t\t}\n\t};\n\n\tconst auto mediaPreviewRaw = state->mediaPreview.get();\n\tmainwidget->sizeValue() | rpl::on_next([=](QSize size) {\n\t\tmediaPreviewRaw->setGeometry(Rect(size));\n\n\t\tif (wrapRaw) {\n\t\t\tconst auto menuRaw = wrapRaw->entity();\n\t\t\tmenuRaw->showFast();\n\t\t\tconst auto gap = st::defaultMenu.itemPadding.top();\n\t\t\tconst auto menuH = menuRaw->height();\n\t\t\tconst auto shift = -(gap + menuH) / 2;\n\t\t\tmediaPreviewRaw->setContentShift(shift);\n\n\t\t\tconst auto menuX = (size.width() - menuRaw->width()) / 2;\n\t\t\tconst auto menuY = mediaPreviewRaw->contentBottom() + gap;\n\t\t\twrapRaw->move(menuX, menuY);\n\t\t\twrapRaw->show(anim::type::normal);\n\t\t\twrapRaw->raise();\n\t\t}\n\t}, mediaPreviewRaw->lifetime());\n}\n\n} // namespace\n\nbool ShowStickerPreview(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFullMsgId origin,\n\t\tnot_null<DocumentData*> document,\n\t\tFn<void(not_null<Ui::DropdownMenu*>)> fillMenu) {\n\tSetupPreviewMenu(\n\t\tcontroller,\n\t\tCreatePreviewOverlay(controller, origin, document),\n\t\tstd::move(fillMenu));\n\treturn true;\n}\n\nbool ShowPhotoPreview(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFullMsgId origin,\n\t\tnot_null<PhotoData*> photo,\n\t\tFn<void(not_null<Ui::DropdownMenu*>)> fillMenu) {\n\tSetupPreviewMenu(\n\t\tcontroller,\n\t\tCreatePreviewOverlay(controller, origin, photo),\n\t\tstd::move(fillMenu));\n\treturn true;\n}\n\nbool ShowReactionPreview(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFullMsgId origin,\n\t\tData::ReactionId reactionId,\n\t\tbool emojiPreview) {\n\tauto document = (DocumentData*)(nullptr);\n\tif (const auto custom = reactionId.custom()) {\n\t\tdocument = controller->session().data().document(custom);\n\t} else if (const auto resolved\n\t\t\t= controller->session().data().reactions().lookupTemporary(\n\t\t\t\treactionId)) {\n\t\tdocument = resolved->selectAnimation;\n\t}\n\tif (!document) {\n\t\treturn false;\n\t}\n\tconst auto overlay = CreatePreviewOverlay(controller, origin, document);\n\tconst auto &state = overlay.state;\n\n\tconst auto mainwidget = controller->widget()->bodyWidget();\n\tconst auto shadowExtend = st::boxRoundShadow.extend;\n\n\tif (reactionId.custom() && document->sticker()) {\n\t\tconst auto setId = document->sticker()->set;\n\t\tconst auto packName\n\t\t\t= document->owner().customEmojiManager().lookupSetName(setId.id);\n\t\tif (!packName.isEmpty()) {\n\t\t\tstate->background = base::make_unique_q<Ui::AbstractButton>(\n\t\t\t\tmainwidget);\n\t\t\tconst auto show = controller->uiShow();\n\t\t\tconst auto hideAll = overlay.hideAll;\n\t\t\tstate->background->setClickedCallback([=] {\n\t\t\t\thideAll();\n\t\t\t\tshow->show(Box<StickerSetBox>(\n\t\t\t\t\tshow,\n\t\t\t\t\tsetId,\n\t\t\t\t\tData::StickersType::Emoji));\n\t\t\t});\n\t\t\tstate->label = base::make_unique_q<Ui::FlatLabel>(\n\t\t\t\tstate->background.get(),\n\t\t\t\t(emojiPreview\n\t\t\t\t\t? tr::lng_context_animated_emoji_preview\n\t\t\t\t\t: tr::lng_context_animated_reaction)(\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\trpl::single(Ui::Text::Colorized(packName)),\n\t\t\t\t\t\ttr::rich));\n\t\t\tstate->label->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\tconst auto backgroundRaw = state->background.get();\n\t\t\tconst auto labelRaw = state->label.get();\n\n\t\t\tbackgroundRaw->paintOn([=](QPainter &p) {\n\t\t\t\tconst auto innerRect = backgroundRaw->rect() - shadowExtend;\n\t\t\t\tUi::Shadow::paint(\n\t\t\t\t\tp,\n\t\t\t\t\tinnerRect,\n\t\t\t\t\tbackgroundRaw->width(),\n\t\t\t\t\tst::boxRoundShadow,\n\t\t\t\t\tUi::SpecialLayerShadowCorners(),\n\t\t\t\t\tRectPart::Full);\n\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(st::windowBg);\n\t\t\t\tp.drawRoundedRect(\n\t\t\t\t\tinnerRect,\n\t\t\t\t\tst::boxRadius,\n\t\t\t\t\tst::boxRadius);\n\t\t\t});\n\n\t\t\tlabelRaw->show();\n\t\t\tUi::Animations::ShowWidgets({ backgroundRaw });\n\t\t}\n\t}\n\tconst auto backgroundRaw = state->background.get();\n\tconst auto labelRaw = state->label.get();\n\tstate->extraHide = [=] {\n\t\tif (backgroundRaw && labelRaw) {\n\t\t\tUi::Animations::HideWidgets({\n\t\t\t\tbackgroundRaw,\n\t\t\t\tlabelRaw,\n\t\t\t});\n\t\t}\n\t};\n\n\tconst auto mediaPreviewRaw = state->mediaPreview.get();\n\tmainwidget->sizeValue() | rpl::on_next([=](QSize size) {\n\t\tmediaPreviewRaw->setGeometry(Rect(size));\n\n\t\tif (backgroundRaw && labelRaw) {\n\t\t\tconst auto maxLabelWidth = labelRaw->textMaxWidth() / 2;\n\t\t\tlabelRaw->resizeToWidth(maxLabelWidth);\n\t\t\tconst auto labelHeight = labelRaw->height() * 2;\n\t\t\tconst auto padding = st::msgServicePadding;\n\t\t\tconst auto innerWidth = maxLabelWidth + rect::m::sum::h(padding);\n\t\t\tconst auto innerHeight = labelHeight + rect::m::sum::v(padding);\n\t\t\tconst auto bgWidth = innerWidth + rect::m::sum::h(shadowExtend);\n\t\t\tconst auto bgHeight = innerHeight + rect::m::sum::v(shadowExtend);\n\t\t\tconst auto bgX = (size.width() - bgWidth) / 2;\n\t\t\tconst auto bgY = (size.height() * 3 / 4) - (bgHeight / 2);\n\n\t\t\tbackgroundRaw->setGeometry(bgX, bgY, bgWidth, bgHeight);\n\t\t\tlabelRaw->setGeometry(\n\t\t\t\tshadowExtend.left() + padding.left(),\n\t\t\t\tshadowExtend.top() + padding.top(),\n\t\t\t\tmaxLabelWidth,\n\t\t\t\tlabelHeight);\n\t\t\tbackgroundRaw->raise();\n\t\t}\n\t}, mediaPreviewRaw->lifetime());\n\treturn true;\n}\n\nvoid ShowWidgetPreview(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFn<void(not_null<Ui::RpWidget*>)> setupContent,\n\t\tFn<void(not_null<Ui::DropdownMenu*>)> fillMenu) {\n\tstruct State {\n\t\tbase::unique_qptr<Ui::RpWidget> preview;\n\t\tbase::unique_qptr<Ui::AbstractButton> clickable;\n\t\tbase::unique_qptr<Ui::FadeWrap<Ui::DropdownMenu>> menuWrap;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tconst auto mainwidget = controller->widget()->bodyWidget();\n\n\tstate->preview = base::make_unique_q<Ui::RpWidget>(mainwidget);\n\tconst auto previewRaw = state->preview.get();\n\tsetupContent(previewRaw);\n\tpreviewRaw->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tstate->clickable = base::make_unique_q<Ui::AbstractButton>(mainwidget);\n\tstate->clickable->paintOn([=](QPainter &p) {\n\t\tp.fillRect(state->clickable->rect(), st::stickerPreviewBg);\n\t});\n\n\tconst auto hideAll = [=] {\n\t\tstate->clickable->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tif (state->menuWrap) {\n\t\t\tstate->menuWrap->hide(anim::type::normal);\n\t\t}\n\t\tbase::call_delayed(\n\t\t\tst::defaultToggle.duration,\n\t\t\t[s = state] {\n\t\t\t\ts->preview.reset();\n\t\t\t\ts->menuWrap.reset();\n\t\t\t\ts->clickable.reset();\n\t\t\t});\n\t};\n\tSetupOverlayHideOnEscape(state->clickable.get(), hideAll);\n\n\tauto menu = object_ptr<Ui::DropdownMenu>(\n\t\tmainwidget,\n\t\tst::dropdownMenuWithIcons);\n\tmenu->setAutoHiding(false);\n\tmenu->setHiddenCallback(\n\t\tcrl::guard(state->clickable.get(), hideAll));\n\tfillMenu(menu.data());\n\tstate->menuWrap = base::make_unique_q<Ui::FadeWrap<Ui::DropdownMenu>>(\n\t\tmainwidget,\n\t\tstd::move(menu));\n\tstate->menuWrap->setDuration(st::defaultToggle.duration);\n\tstate->menuWrap->hide(anim::type::instant);\n\n\tconst auto wrapRaw = state->menuWrap.get();\n\tstate->clickable->show();\n\tpreviewRaw->show();\n\n\tmainwidget->sizeValue(\n\t) | rpl::skip(1) | rpl::on_next([=](QSize) {\n\t\thideAll();\n\t}, previewRaw->lifetime());\n\n\tconst auto fullW = previewRaw->width();\n\tconst auto fullH = previewRaw->height();\n\n\tmainwidget->sizeValue() | rpl::on_next([=](QSize size) {\n\t\tstate->clickable->setGeometry(Rect(size));\n\t\tstate->clickable->raise();\n\n\t\tconst auto menuRaw = wrapRaw->entity();\n\t\tmenuRaw->showFast();\n\t\tconst auto gap = st::defaultMenu.itemPadding.top();\n\t\tconst auto totalH = fullH + gap + menuRaw->height();\n\t\tconst auto previewY = (size.height() - totalH) / 2;\n\t\tpreviewRaw->move((size.width() - fullW) / 2, previewY);\n\t\tpreviewRaw->raise();\n\n\t\twrapRaw->move(\n\t\t\t(size.width() - menuRaw->width()) / 2,\n\t\t\tpreviewY + fullH + gap);\n\t\twrapRaw->show(anim::type::normal);\n\t\twrapRaw->raise();\n\t}, previewRaw->lifetime());\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_reaction_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass DocumentData;\nclass PhotoData;\n\nnamespace Ui {\nclass DropdownMenu;\nclass RpWidget;\n} // namespace Ui\n\nnamespace Data {\nstruct ReactionId;\n} // namespace Data\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace HistoryView {\n\nbool ShowStickerPreview(\n\tnot_null<Window::SessionController*> controller,\n\tFullMsgId origin,\n\tnot_null<DocumentData*> document,\n\tFn<void(not_null<Ui::DropdownMenu*>)> fillMenu = nullptr);\n\nbool ShowPhotoPreview(\n\tnot_null<Window::SessionController*> controller,\n\tFullMsgId origin,\n\tnot_null<PhotoData*> photo,\n\tFn<void(not_null<Ui::DropdownMenu*>)> fillMenu = nullptr);\n\nvoid ShowWidgetPreview(\n\tnot_null<Window::SessionController*> controller,\n\tFn<void(not_null<Ui::RpWidget*>)> setupContent,\n\tFn<void(not_null<Ui::DropdownMenu*>)> fillMenu);\n\nbool ShowReactionPreview(\n\tnot_null<Window::SessionController*> controller,\n\tFullMsgId origin,\n\tData::ReactionId reactionId,\n\tbool emojiPreview = false);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_read_metrics_tracker.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_read_metrics_tracker.h\"\n\n#include \"api/api_read_metrics.h\"\n#include \"apiwrap.h\"\n#include \"base/random.h\"\n#include \"core/application.h\"\n#include \"data/data_peer.h\"\n#include \"history/history_item.h\"\n#include \"main/main_session.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kGracePeriod = crl::time(300);\nconstexpr auto kActivityTimeout = 15 * crl::time(1000);\nconstexpr auto kMaxTrackingDuration = 5 * 60 * crl::time(1000);\nconstexpr auto kMinReportThreshold = crl::time(300);\n\n} // namespace\n\nReadMetricsTracker::ReadMetricsTracker(not_null<PeerData*> peer)\n: _peer(peer)\n, _timer([=] { onTimeout(); }) {\n\tCore::App().appDeactivatedValue(\n\t) | rpl::on_next([=](bool deactivated) {\n\t\tconst auto appActive = !deactivated;\n\t\tif (_appActive == appActive) {\n\t\t\treturn;\n\t\t}\n\t\t_appActive = appActive;\n\t\trefreshPaused(crl::now());\n\t}, _lifetime);\n}\n\nReadMetricsTracker::~ReadMetricsTracker() {\n\tfinalizeAll();\n}\n\nvoid ReadMetricsTracker::startBatch(int visibleTop, int visibleBottom) {\n\t_batchNow = crl::now();\n\tsync(_batchNow);\n\t_batchViewportHeight = visibleBottom - visibleTop;\n\t_batchVisibleTop = visibleTop;\n\t_batchVisibleBottom = visibleBottom;\n\t_batchVisible.clear();\n}\n\nvoid ReadMetricsTracker::push(\n\t\tnot_null<HistoryItem*> item,\n\t\tint itemTop,\n\t\tint itemHeight) {\n\tif (!ShouldTrack(item)) {\n\t\treturn;\n\t}\n\tconst auto msgId = item->id;\n\t_batchVisible.emplace(msgId);\n\n\tconst auto clippedTop = std::max(itemTop, _batchVisibleTop) - itemTop;\n\tconst auto clippedBottom = std::min(\n\t\titemTop + itemHeight,\n\t\t_batchVisibleBottom) - itemTop;\n\n\tconst auto addTracked = [&] {\n\t\tauto tracked = TrackedItem();\n\t\ttracked.entryGracePending = true;\n\t\ttracked.entryGraceStart = _batchNow;\n\t\ttracked.maxItemHeight = itemHeight;\n\t\ttracked.maxViewportHeight = _batchViewportHeight;\n\t\ttracked.seenTop = clippedTop;\n\t\ttracked.seenBottom = clippedBottom;\n\t\t_tracked.emplace(msgId, tracked);\n\t};\n\n\tauto it = _tracked.find(msgId);\n\tif (it == end(_tracked)) {\n\t\taddTracked();\n\t\treturn;\n\t}\n\tauto &tracked = it->second;\n\tif (tracked.exitGracePending) {\n\t\tif (_batchNow - tracked.exitGraceStart >= kGracePeriod) {\n\t\t\tfinalize(msgId, tracked);\n\t\t\t_tracked.erase(it);\n\t\t\taddTracked();\n\t\t\treturn;\n\t\t}\n\t\ttracked.exitGracePending = false;\n\t\ttracked.exitGraceStart = 0;\n\t\ttracked.lastUpdate = _batchNow;\n\t}\n\ttracked.seenTop = std::min(tracked.seenTop, clippedTop);\n\ttracked.seenBottom = std::max(tracked.seenBottom, clippedBottom);\n\taccumulate_max(tracked.maxItemHeight, itemHeight);\n\taccumulate_max(tracked.maxViewportHeight, _batchViewportHeight);\n}\n\nvoid ReadMetricsTracker::endBatch() {\n\tfor (auto it = _tracked.begin(); it != _tracked.end();) {\n\t\tif (_batchVisible.contains(it->first)) {\n\t\t\t++it;\n\t\t\tcontinue;\n\t\t}\n\t\tauto &tracked = it->second;\n\t\tif (tracked.entryGracePending) {\n\t\t\tit = _tracked.erase(it);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!tracked.exitGracePending) {\n\t\t\ttracked.exitGracePending = true;\n\t\t\ttracked.exitGraceStart = _batchNow;\n\t\t}\n\t\t++it;\n\t}\n\n\t_currentlyVisible = std::move(_batchVisible);\n\trestartTimer();\n}\n\nvoid ReadMetricsTracker::registerActivity() {\n\tif (!_appActive || !_screenActive) {\n\t\treturn;\n\t}\n\tconst auto now = crl::now();\n\tconst auto activityDeadline = activeUntil();\n\tif (activityDeadline && activityDeadline < now) {\n\t\tsync(now);\n\t}\n\t_lastActivity = now;\n\trestartTimer();\n}\n\nvoid ReadMetricsTracker::setScreenActive(bool active) {\n\tif (_screenActive == active) {\n\t\treturn;\n\t}\n\t_screenActive = active;\n\trefreshPaused(crl::now());\n}\n\nvoid ReadMetricsTracker::pauseTracking() {\n\tsetScreenActive(false);\n}\n\nvoid ReadMetricsTracker::resumeTracking() {\n\tsetScreenActive(true);\n}\n\nvoid ReadMetricsTracker::onTimeout() {\n\tsync(crl::now());\n}\n\nvoid ReadMetricsTracker::sync(crl::time now) {\n\tconst auto activeUntil = this->activeUntil();\n\tprocessTransitions(now, activeUntil);\n\taccumulate(now, activeUntil);\n\trestartTimer();\n}\n\nvoid ReadMetricsTracker::processTransitions(\n\t\tcrl::time now,\n\t\tcrl::time activeUntil) {\n\tfor (auto it = _tracked.begin(); it != _tracked.end();) {\n\t\tauto &tracked = it->second;\n\t\tif (tracked.entryGracePending\n\t\t\t&& !_paused\n\t\t\t&& now - tracked.entryGraceStart >= kGracePeriod) {\n\t\t\ttracked.entryGracePending = false;\n\t\t\tdo {\n\t\t\t\ttracked.viewId = base::RandomValue<uint64>();\n\t\t\t} while (!tracked.viewId);\n\t\t\ttracked.trackingStarted = tracked.entryGraceStart;\n\t\t\ttracked.lastUpdate = tracked.trackingStarted;\n\t\t}\n\t\tif (tracked.exitGracePending\n\t\t\t&& !_paused\n\t\t\t&& now - tracked.exitGraceStart >= kGracePeriod) {\n\t\t\tfinalize(it->first, tracked);\n\t\t\tit = _tracked.erase(it);\n\t\t\tcontinue;\n\t\t}\n\t\tif (tracked.viewId && !tracked.entryGracePending && !tracked.exitGracePending) {\n\t\t\tconst auto deadline = tracked.trackingStarted + kMaxTrackingDuration;\n\t\t\tif (now >= deadline) {\n\t\t\t\tif (!_paused\n\t\t\t\t\t&& _currentlyVisible.contains(it->first)\n\t\t\t\t\t&& tracked.lastUpdate < deadline) {\n\t\t\t\t\taddElapsed(tracked, tracked.lastUpdate, deadline, activeUntil);\n\t\t\t\t\ttracked.lastUpdate = deadline;\n\t\t\t\t}\n\t\t\t\tfinalize(it->first, tracked);\n\t\t\t\tit = _tracked.erase(it);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\t++it;\n\t}\n}\n\nvoid ReadMetricsTracker::accumulate(crl::time now, crl::time activeUntil) {\n\tfor (auto &[msgId, tracked] : _tracked) {\n\t\tif (!tracked.viewId\n\t\t\t|| tracked.entryGracePending\n\t\t\t|| tracked.exitGracePending\n\t\t\t|| tracked.lastUpdate <= 0) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (_paused || !_currentlyVisible.contains(msgId)) {\n\t\t\ttracked.lastUpdate = now;\n\t\t\tcontinue;\n\t\t}\n\t\taddElapsed(tracked, tracked.lastUpdate, now, activeUntil);\n\t\ttracked.lastUpdate = now;\n\t}\n}\n\nvoid ReadMetricsTracker::refreshPaused(crl::time now) {\n\tconst auto paused = !_appActive || !_screenActive;\n\tif (_paused == paused) {\n\t\treturn;\n\t} else if (paused) {\n\t\tconst auto activeUntil = this->activeUntil();\n\t\tprocessTransitions(now, activeUntil);\n\t\taccumulate(now, activeUntil);\n\t\t_pausedSince = now;\n\t\tfor (auto &[msgId, tracked] : _tracked) {\n\t\t\tif (tracked.entryGracePending || tracked.exitGracePending) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttracked.lastUpdate = now;\n\t\t}\n\t} else {\n\t\tconst auto delta = now - _pausedSince;\n\t\tfor (auto &[msgId, tracked] : _tracked) {\n\t\t\tif (tracked.entryGracePending) {\n\t\t\t\ttracked.entryGraceStart = (tracked.entryGraceStart < _pausedSince)\n\t\t\t\t\t? (tracked.entryGraceStart + delta)\n\t\t\t\t\t: now;\n\t\t\t} else if (tracked.exitGracePending) {\n\t\t\t\ttracked.exitGraceStart = (tracked.exitGraceStart < _pausedSince)\n\t\t\t\t\t? (tracked.exitGraceStart + delta)\n\t\t\t\t\t: now;\n\t\t\t} else if (tracked.viewId) {\n\t\t\t\ttracked.lastUpdate = now;\n\t\t\t}\n\t\t}\n\t\t_pausedSince = 0;\n\t}\n\t_paused = paused;\n\trestartTimer();\n}\n\nvoid ReadMetricsTracker::restartTimer() {\n\tif (_tracked.empty()) {\n\t\t_timer.cancel();\n\t\treturn;\n\t}\n\tconst auto now = crl::now();\n\tauto nearest = crl::time(0);\n\tconst auto updateNearest = [&](crl::time deadline) {\n\t\tif (!deadline) {\n\t\t\treturn;\n\t\t}\n\t\tif (!nearest || deadline < nearest) {\n\t\t\tnearest = deadline;\n\t\t}\n\t};\n\n\tconst auto activityDeadline = activeUntil();\n\tfor (const auto &[msgId, tracked] : _tracked) {\n\t\tif (tracked.entryGracePending && !_paused) {\n\t\t\tupdateNearest(tracked.entryGraceStart + kGracePeriod);\n\t\t} else if (tracked.exitGracePending && !_paused) {\n\t\t\tupdateNearest(tracked.exitGraceStart + kGracePeriod);\n\t\t} else if (tracked.viewId) {\n\t\t\tupdateNearest(tracked.trackingStarted + kMaxTrackingDuration);\n\t\t\tif (!_paused && _currentlyVisible.contains(msgId) && activityDeadline > now) {\n\t\t\t\tupdateNearest(activityDeadline);\n\t\t\t}\n\t\t}\n\t}\n\tif (!nearest) {\n\t\t_timer.cancel();\n\t\treturn;\n\t}\n\t_timer.callOnce(std::max(nearest - now, crl::time(0)));\n}\n\ncrl::time ReadMetricsTracker::activeUntil() const {\n\treturn _lastActivity ? (_lastActivity + kActivityTimeout) : 0;\n}\n\nvoid ReadMetricsTracker::addElapsed(\n\t\tTrackedItem &tracked,\n\t\tcrl::time from,\n\t\tcrl::time till,\n\t\tcrl::time activeUntil) const {\n\tif (till <= from) {\n\t\treturn;\n\t}\n\ttracked.totalInView += (till - from);\n\tif (activeUntil > from) {\n\t\ttracked.activeInView += std::max(std::min(till, activeUntil) - from, crl::time(0));\n\t\ttracked.activeInView = std::min(tracked.activeInView, tracked.totalInView);\n\t}\n}\n\nvoid ReadMetricsTracker::finalize(\n\t\tMsgId msgId,\n\t\tconst TrackedItem &tracked) {\n\tif (tracked.viewId == 0 || tracked.totalInView < kMinReportThreshold) {\n\t\treturn;\n\t}\n\tconst auto heightRatio = (tracked.maxViewportHeight > 0)\n\t\t? qRound((tracked.maxItemHeight * 1000.0) / tracked.maxViewportHeight)\n\t\t: 0;\n\tconst auto seenRange = (tracked.maxItemHeight > 0)\n\t\t? std::clamp(\n\t\t\tqRound(((tracked.seenBottom - tracked.seenTop) * 1000.0)\n\t\t\t\t/ tracked.maxItemHeight),\n\t\t\t0,\n\t\t\t1000)\n\t\t: 0;\n\t_peer->session().api().readMetrics().add(_peer, {\n\t\t.msgId = msgId,\n\t\t.viewId = tracked.viewId,\n\t\t.timeInViewMs = int(tracked.totalInView),\n\t\t.activeTimeInViewMs = int(tracked.activeInView),\n\t\t.heightToViewportRatioPermille = heightRatio,\n\t\t.seenRangeRatioPermille = seenRange,\n\t});\n}\n\nvoid ReadMetricsTracker::finalizeAll() {\n\tsync(crl::now());\n\tfor (const auto &[msgId, tracked] : _tracked) {\n\t\tfinalize(msgId, tracked);\n\t}\n\t_tracked.clear();\n\t_timer.cancel();\n}\n\nbool ReadMetricsTracker::ShouldTrack(not_null<HistoryItem*> item) {\n\treturn item->isRegular() && item->hasViews();\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_read_metrics_tracker.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n\nclass PeerData;\nclass HistoryItem;\n\nnamespace HistoryView {\n\nclass ReadMetricsTracker final {\npublic:\n\texplicit ReadMetricsTracker(not_null<PeerData*> peer);\n\t~ReadMetricsTracker();\n\n\tvoid startBatch(int visibleTop, int visibleBottom);\n\tvoid push(not_null<HistoryItem*> item, int itemTop, int itemHeight);\n\tvoid endBatch();\n\tvoid registerActivity();\n\tvoid setScreenActive(bool active);\n\tvoid pauseTracking();\n\tvoid resumeTracking();\n\nprivate:\n\tstruct TrackedItem {\n\t\tuint64 viewId = 0;\n\t\tcrl::time entryGraceStart = 0;\n\t\tcrl::time trackingStarted = 0;\n\t\tcrl::time lastUpdate = 0;\n\t\tcrl::time totalInView = 0;\n\t\tcrl::time activeInView = 0;\n\t\tint seenTop = std::numeric_limits<int>::max();\n\t\tint seenBottom = 0;\n\t\tint maxItemHeight = 0;\n\t\tint maxViewportHeight = 0;\n\t\tbool entryGracePending = false;\n\t\tbool exitGracePending = false;\n\t\tcrl::time exitGraceStart = 0;\n\t};\n\n\tvoid onTimeout();\n\tvoid sync(crl::time now);\n\tvoid processTransitions(crl::time now, crl::time activeUntil);\n\tvoid accumulate(crl::time now, crl::time activeUntil);\n\tvoid refreshPaused(crl::time now);\n\tvoid restartTimer();\n\t[[nodiscard]] crl::time activeUntil() const;\n\tvoid addElapsed(\n\t\tTrackedItem &tracked,\n\t\tcrl::time from,\n\t\tcrl::time till,\n\t\tcrl::time activeUntil) const;\n\tvoid finalize(MsgId msgId, const TrackedItem &tracked);\n\tvoid finalizeAll();\n\t[[nodiscard]] static bool ShouldTrack(not_null<HistoryItem*> item);\n\n\tconst not_null<PeerData*> _peer;\n\tbase::flat_map<MsgId, TrackedItem> _tracked;\n\tbase::flat_set<MsgId> _currentlyVisible;\n\tcrl::time _batchNow = 0;\n\tint _batchViewportHeight = 0;\n\tint _batchVisibleTop = 0;\n\tint _batchVisibleBottom = 0;\n\tbase::flat_set<MsgId> _batchVisible;\n\tcrl::time _lastActivity = 0;\n\tbool _appActive = true;\n\tbool _screenActive = true;\n\tbool _paused = false;\n\tcrl::time _pausedSince = 0;\n\tbase::Timer _timer;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_reply.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_reply.h\"\n\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_session.h\"\n#include \"data/data_story.h\"\n#include \"data/data_todo_list.h\"\n#include \"data/data_user.h\"\n#include \"history/view/history_view_item_preview.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_polls.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kNonExpandedLinesLimit = 5;\n\n[[nodiscard]] QImage MakeTaskImage() {\n\tconst auto diameter = st::normalFont->ascent;\n\tconst auto line = st::historyPollRadio.thickness;\n\tconst auto size = 2 * line + diameter;\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto result = QImage(\n\t\tQSize(size, size) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::transparent);\n\tresult.setDevicePixelRatio(ratio);\n\n\tauto p = QPainter(&result);\n\tPainterHighQualityEnabler hq(p);\n\n\tp.setOpacity(st::historyPollRadioOpacity);\n\n\tconst auto rect = QRectF(line, line, diameter, diameter).marginsRemoved(\n\t\tQMarginsF(line / 2., line / 2., line / 2., line / 2.));\n\tauto pen = QPen(QColor(255, 255, 255));\n\tpen.setWidth(line);\n\tp.setPen(pen);\n\tp.drawEllipse(rect);\n\n\tp.end();\n\n\treturn result;\n}\n\ntemplate <typename PaintShape>\n[[nodiscard]] QImage MakeCheckedImage(PaintShape paintShape) {\n\tconst auto white = QColor(255, 255, 255);\n\tconst auto black = QColor(0, 0, 0);\n\n\tconst auto diameter = st::normalFont->ascent;\n\tconst auto line = st::historyPollRadio.thickness;\n\tconst auto size = 2 * line + diameter;\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto result = QImage(\n\t\tQSize(size, size) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.fill(black);\n\tresult.setDevicePixelRatio(ratio);\n\n\tauto p = QPainter(&result);\n\tPainterHighQualityEnabler hq(p);\n\n\tconst auto rect = QRectF(line, line, diameter, diameter).marginsRemoved(\n\t\tQMarginsF(line / 2., line / 2., line / 2., line / 2.));\n\tpaintShape(p, rect);\n\tconst auto &icon = st::historyPollInChoiceRight;\n\ticon.paint(\n\t\tp,\n\t\tline + (diameter - icon.width()) / 2,\n\t\tline + (diameter - icon.height()) / 2,\n\t\tsize,\n\t\tblack);\n\tp.end();\n\n\treturn style::colorizeImage(result, white);\n}\n\n[[nodiscard]] QImage MakeTaskDoneImage() {\n\treturn MakeCheckedImage([](QPainter &p, QRectF rect) {\n\t\tconst auto white = QColor(255, 255, 255);\n\t\tconst auto line = st::historyPollRadio.thickness;\n\t\tauto pen = QPen(white);\n\t\tpen.setWidth(line);\n\t\tp.setPen(pen);\n\t\tp.setBrush(white);\n\t\tp.drawEllipse(rect);\n\t});\n}\n\n[[nodiscard]] QImage MakePollAnswerImage() {\n\treturn MakeCheckedImage([](QPainter &p, QRectF rect) {\n\t\tconst auto white = QColor(255, 255, 255);\n\t\tconst auto line = st::historyPollRadio.thickness;\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(white);\n\t\tp.drawRoundedRect(rect, line * 2, line * 2);\n\t});\n}\n\n} // namespace\n\nvoid ValidateBackgroundEmoji(\n\t\tDocumentId backgroundEmojiId,\n\t\tconst std::shared_ptr<Ui::ColorCollectible> &collectible,\n\t\tnot_null<Ui::BackgroundEmojiData*> data,\n\t\tnot_null<Ui::BackgroundEmojiCache*> cache,\n\t\tnot_null<Ui::Text::QuotePaintCache*> quote,\n\t\tnot_null<const Element*> view) {\n\tif (data->firstFrameMask.isNull() && !data->emoji) {\n\t\tdata->emoji = CreateBackgroundEmojiInstance(\n\t\t\t&view->history()->owner(),\n\t\t\tbackgroundEmojiId,\n\t\t\tcrl::guard(view, [=] { view->repaint(); }));\n\t}\n\tif (collectible && data->firstGiftFrame.isNull() && !data->gift) {\n\t\tdata->gift = CreateBackgroundGiftInstance(\n\t\t\t&view->history()->owner(),\n\t\t\tcollectible->giftEmojiId,\n\t\t\tcrl::guard(view, [=] { view->repaint(); }));\n\t}\n\tValidateBackgroundEmoji(data, cache, quote);\n}\n\nvoid ValidateBackgroundEmoji(\n\t\tnot_null<Ui::BackgroundEmojiData*> data,\n\t\tnot_null<Ui::BackgroundEmojiCache*> cache,\n\t\tnot_null<Ui::Text::QuotePaintCache*> quote) {\n\tExpects(!data->firstFrameMask.isNull() || data->emoji != nullptr);\n\n\tif (data->gift && data->firstGiftFrame.isNull()) {\n\t\tif (data->gift->ready()) {\n\t\t\tconst auto tag = Data::CustomEmojiSizeTag::Normal;\n\t\t\tconst auto size = Data::FrameSizeFromTag(tag);\n\t\t\tdata->firstGiftFrame = QImage(\n\t\t\t\tQSize(size, size),\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tdata->firstGiftFrame.fill(Qt::transparent);\n\t\t\tdata->firstGiftFrame.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\tauto p = Painter(&data->firstGiftFrame);\n\t\t\tdata->gift->paint(p, {\n\t\t\t\t.textColor = QColor(255, 255, 255),\n\t\t\t\t.position = QPoint(0, 0),\n\t\t\t\t.internal = {\n\t\t\t\t\t.forceFirstFrame = true,\n\t\t\t\t},\n\t\t\t});\n\t\t\tp.end();\n\n\t\t\tdata->gift = nullptr;\n\t\t}\n\t}\n\tif (data->firstFrameMask.isNull()) {\n\t\tif (!cache->frames[0].isNull()) {\n\t\t\tfor (auto &frame : cache->frames) {\n\t\t\t\tframe = QImage();\n\t\t\t}\n\t\t}\n\t\tif (!data->emoji->ready()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto tag = Data::CustomEmojiSizeTag::Isolated;\n\t\tconst auto size = Data::FrameSizeFromTag(tag);\n\t\tdata->firstFrameMask = QImage(\n\t\t\tQSize(size, size),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tdata->firstFrameMask.fill(Qt::transparent);\n\t\tdata->firstFrameMask.setDevicePixelRatio(style::DevicePixelRatio());\n\t\tauto p = Painter(&data->firstFrameMask);\n\t\tdata->emoji->paint(p, {\n\t\t\t.textColor = QColor(255, 255, 255),\n\t\t\t.position = QPoint(0, 0),\n\t\t\t.internal = {\n\t\t\t\t.forceFirstFrame = true,\n\t\t\t},\n\t\t});\n\t\tp.end();\n\n\t\tdata->emoji = nullptr;\n\t}\n\tif (!cache->frames[0].isNull() && cache->color == quote->icon) {\n\t\treturn;\n\t}\n\tcache->color = quote->icon;\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto colorized = QImage(\n\t\tdata->firstFrameMask.size(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tcolorized.setDevicePixelRatio(ratio);\n\tstyle::colorizeImage(\n\t\tdata->firstFrameMask,\n\t\tcache->color,\n\t\t&colorized,\n\t\tQRect(), // src\n\t\tQPoint(), // dst\n\t\ttrue); // use alpha\n\tconst auto make = [&](int size) {\n\t\tsize = style::ConvertScale(size) * ratio;\n\t\tauto result = colorized.scaled(\n\t\t\tsize,\n\t\t\tsize,\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t\tresult.setDevicePixelRatio(ratio);\n\t\treturn result;\n\t};\n\n\tconstexpr auto kSize1 = 12;\n\tconstexpr auto kSize2 = 16;\n\tconstexpr auto kSize3 = 20;\n\tcache->frames[0] = make(kSize1);\n\tcache->frames[1] = make(kSize2);\n\tcache->frames[2] = make(kSize3);\n}\n\nauto CreateBackgroundEmojiInstance(\n\tnot_null<Data::Session*> owner,\n\tDocumentId backgroundEmojiId,\n\tFn<void()> repaint)\n-> std::unique_ptr<Ui::Text::CustomEmoji> {\n\treturn owner->customEmojiManager().create(\n\t\tbackgroundEmojiId,\n\t\trepaint,\n\t\tData::CustomEmojiSizeTag::Isolated);\n}\n\nauto CreateBackgroundGiftInstance(\n\tnot_null<Data::Session*> owner,\n\tDocumentId giftEmojiId,\n\tFn<void()> repaint)\n-> std::unique_ptr<Ui::Text::CustomEmoji> {\n\treturn owner->customEmojiManager().create(\n\t\tgiftEmojiId,\n\t\trepaint,\n\t\tData::CustomEmojiSizeTag::Normal);\n}\n\nvoid FillBackgroundEmoji(\n\t\tQPainter &p,\n\t\tconst QRect &rect,\n\t\tbool quote,\n\t\tconst Ui::BackgroundEmojiCache &cache,\n\t\tconst QImage &firstGiftFrame) {\n\tp.setClipRect(rect);\n\n\tconst auto &frames = cache.frames;\n\tconst auto right = rect.x() + rect.width();\n\tconst auto paintImage = [&](\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tconst QImage &frame,\n\t\t\tfloat64 opacity) {\n\t\ty = style::ConvertScale(y);\n\t\tif (y >= rect.height()) {\n\t\t\treturn;\n\t\t}\n\t\tp.setOpacity(opacity);\n\t\tp.drawImage(\n\t\t\tright - style::ConvertScale(x + (quote ? 12 : 0)),\n\t\t\trect.y() + y,\n\t\t\tframe);\n\t};\n\tconst auto paint = [&](int x, int y, int index, float64 opacity) {\n\t\tpaintImage(x, y, frames[index], opacity);\n\t};\n\n\tif (firstGiftFrame.isNull()) {\n\t\tpaint(28, 4, 2, 0.32);\n\t} else {\n\t\tpaintImage(28, 4, firstGiftFrame, 1.);\n\t}\n\tpaint(51, 15, 1, 0.32);\n\tpaint(64, -2, 0, 0.28);\n\tpaint(87, 11, 1, 0.24);\n\tpaint(125, -2, 2, 0.16);\n\n\tpaint(28, 31, 1, 0.24);\n\tpaint(72, 33, 2, 0.2);\n\n\tpaint(46, 52, 1, 0.24);\n\tpaint(24, 55, 2, 0.18);\n\n\tif (quote) {\n\t\tpaint(4, 23, 1, 0.28);\n\t\tpaint(0, 48, 0, 0.24);\n\t}\n\n\tp.setClipping(false);\n\tp.setOpacity(1.);\n}\n\nReply::Reply()\n: _name(st::maxSignatureSize / 2)\n, _text(st::maxSignatureSize / 2) {\n}\n\nReply &Reply::operator=(Reply &&other) = default;\n\nReply::~Reply() = default;\n\nvoid Reply::update(\n\t\tnot_null<Element*> view,\n\t\tnot_null<HistoryMessageReply*> data) {\n\tconst auto item = view->data();\n\tconst auto &fields = data->fields();\n\tconst auto message = data->resolvedMessage.get();\n\tconst auto messageMedia = (message\n\t\t\t&& (fields.todoItemId || !fields.pollOption.isEmpty()))\n\t\t? message->media()\n\t\t: nullptr;\n\tconst auto messageTodoList = messageMedia\n\t\t? messageMedia->todolist()\n\t\t: nullptr;\n\tconst auto taskIndex = messageTodoList\n\t\t? int(ranges::find(\n\t\t\tmessageTodoList->items,\n\t\t\tfields.todoItemId,\n\t\t\t&TodoListItem::id) - begin(messageTodoList->items))\n\t\t: -1;\n\tconst auto task = (taskIndex >= 0\n\t\t&& taskIndex < messageTodoList->items.size())\n\t\t? &messageTodoList->items[taskIndex]\n\t\t: nullptr;\n\tconst auto messagePoll = messageMedia\n\t\t? messageMedia->poll()\n\t\t: message\n\t\t? (message->media() ? message->media()->poll() : nullptr)\n\t\t: nullptr;\n\tconst auto pollAnswer = (messagePoll && !fields.pollOption.isEmpty())\n\t\t? messagePoll->answerByOption(fields.pollOption)\n\t\t: nullptr;\n\tconst auto story = data->resolvedStory.get();\n\tconst auto externalMedia = fields.externalMedia.get();\n\tif (!_externalSender) {\n\t\tif (const auto id = fields.externalSenderId) {\n\t\t\t_externalSender = view->history()->owner().peer(id);\n\t\t}\n\t}\n\t_colorPeer = message\n\t\t? message->contentColorsFrom()\n\t\t: story\n\t\t? story->peer().get()\n\t\t: _externalSender\n\t\t? _externalSender\n\t\t: nullptr;\n\t_hiddenSenderColorIndexPlusOne = (!_colorPeer && message)\n\t\t? (message->originalHiddenSenderInfo()->colorIndex + 1)\n\t\t: 0;\n\tconst auto pollMediaPtr = pollAnswer\n\t\t? &pollAnswer->media\n\t\t: (messagePoll && fields.pollOption.isEmpty())\n\t\t? &messagePoll->attachedMedia\n\t\t: nullptr;\n\tconst auto hasPreview = (story && story->hasReplyPreview())\n\t\t|| (message\n\t\t\t&& message->media()\n\t\t\t&& message->media()->hasReplyPreview())\n\t\t|| (externalMedia && externalMedia->hasReplyPreview())\n\t\t|| (pollMediaPtr\n\t\t\t&& (pollMediaPtr->photo || pollMediaPtr->document));\n\t_hasPreview = hasPreview ? 1 : 0;\n\t_displaying = data->displaying() ? 1 : 0;\n\t_multiline = data->multiline() ? 1 : 0;\n\t_replyToStory = (fields.storyId != 0);\n\t_replyToPoll = (messagePoll && !pollAnswer) ? 1 : 0;\n\tconst auto hasQuoteIcon = _displaying\n\t\t&& fields.manualQuote\n\t\t&& !fields.quote.empty();\n\t_hasQuoteIcon = hasQuoteIcon ? 1 : 0;\n\n\tconst auto repaint = [=] { item->customEmojiRepaint(); };\n\tauto helper = Ui::Text::CustomEmojiHelper(Core::TextContext({\n\t\t.session = &view->history()->session(),\n\t\t.repaint = repaint,\n\t}));\n\tconst auto text = (!_displaying && data->unavailable())\n\t\t? TextWithEntities()\n\t\t: task\n\t\t? Ui::Text::Colorized(task->completionDate\n\t\t\t? helper.image({\n\t\t\t\t.image = MakeTaskDoneImage(),\n\t\t\t\t.margin = QMargins(0, st::lineWidth, st::lineWidth, 0),\n\t\t\t})\n\t\t\t: helper.image({\n\t\t\t\t.image = MakeTaskImage(),\n\t\t\t\t.margin = QMargins(0, st::lineWidth, st::lineWidth, 0),\n\t\t\t})).append(task->text)\n\t\t: pollAnswer\n\t\t? Ui::Text::Colorized(helper.image({\n\t\t\t.image = MakePollAnswerImage(),\n\t\t\t.margin = QMargins(0, st::lineWidth, st::lineWidth, 0),\n\t\t})).append(pollAnswer->text)\n\t\t: messagePoll\n\t\t? TextWithEntities().append(messagePoll->question)\n\t\t: (message && (fields.quote.empty() || !fields.manualQuote))\n\t\t? message->inReplyText()\n\t\t: !fields.quote.empty()\n\t\t? fields.quote\n\t\t: story\n\t\t? story->inReplyText()\n\t\t: externalMedia\n\t\t? externalMedia->toPreview({\n\t\t\t.hideSender = true,\n\t\t\t.hideCaption = true,\n\t\t\t.ignoreMessageText = true,\n\t\t\t.generateImages = false,\n\t\t\t.ignoreGroup = true,\n\t\t\t.ignoreTopic = true,\n\t\t}).text\n\t\t: TextWithEntities();\n\t_text.setMarkedText(\n\t\tst::defaultTextStyle,\n\t\ttext,\n\t\t_multiline ? Ui::ItemTextDefaultOptions() : Ui::DialogTextOptions(),\n\t\thelper.context());\n\n\tupdateName(view, data);\n\n\tif (_displaying) {\n\t\tsetLinkFrom(view, data);\n\t\tconst auto media = message ? message->media() : nullptr;\n\t\tif (!media || !media->hasReplyPreview() || !media->hasSpoiler()) {\n\t\t\t_spoiler = nullptr;\n\t\t} else if (!_spoiler) {\n\t\t\t_spoiler = std::make_unique<Ui::SpoilerAnimation>(repaint);\n\t\t}\n\t} else {\n\t\t_spoiler = nullptr;\n\t}\n}\n\nbool Reply::expand() {\n\tif (!_expandable || _expanded) {\n\t\treturn false;\n\t}\n\t_expanded = true;\n\treturn true;\n}\n\nvoid Reply::setLinkFrom(\n\t\tnot_null<Element*> view,\n\t\tnot_null<HistoryMessageReply*> data) {\n\tconst auto weak = base::make_weak(view);\n\tconst auto &fields = data->fields();\n\tconst auto isAdminLogEntry = view->data()->isAdminLogEntry();\n\tconst auto externalChannelId = peerToChannel(fields.externalPeerId);\n\tconst auto messageId = fields.messageId;\n\tconst auto highlight = MessageHighlightId{\n\t\t.quote = fields.manualQuote ? fields.quote : TextWithEntities(),\n\t\t.quoteOffset = int(fields.quoteOffset),\n\t\t.todoItemId = fields.todoItemId,\n\t\t.pollOption = fields.pollOption,\n\t};\n\tconst auto returnToId = view->data()->fullId();\n\tconst auto externalLink = [=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tauto error = QString();\n\t\t\tconst auto owner = &controller->session().data();\n\t\t\tif (const auto view = weak.get()) {\n\t\t\t\tif (const auto reply = view->Get<Reply>()) {\n\t\t\t\t\tif (reply->expand()) {\n\t\t\t\t\t\towner->requestViewResize(view);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (externalChannelId) {\n\t\t\t\tconst auto channel = owner->channel(externalChannelId);\n\t\t\t\tif (!channel->isForbidden()) {\n\t\t\t\t\tif (messageId) {\n\t\t\t\t\t\tJumpToMessageClickHandler(\n\t\t\t\t\t\t\tchannel,\n\t\t\t\t\t\t\tmessageId,\n\t\t\t\t\t\t\treturnToId,\n\t\t\t\t\t\t\thighlight\n\t\t\t\t\t\t)->onClick(context);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcontroller->showPeerInfo(channel);\n\t\t\t\t\t}\n\t\t\t\t} else if (channel->isBroadcast()) {\n\t\t\t\t\terror = tr::lng_channel_not_accessible(tr::now);\n\t\t\t\t} else {\n\t\t\t\t\terror = tr::lng_group_not_accessible(tr::now);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\terror = tr::lng_reply_from_private_chat(tr::now);\n\t\t\t}\n\t\t\tif (!error.isEmpty()) {\n\t\t\t\tcontroller->showToast(error);\n\t\t\t}\n\t\t}\n\t};\n\tconst auto message = data->resolvedMessage.get();\n\tconst auto story = data->resolvedStory.get();\n\t_link = isAdminLogEntry\n\t\t? std::make_shared<LambdaClickHandler>(externalLink)\n\t\t: message\n\t\t? JumpToMessageClickHandler(message, returnToId, highlight)\n\t\t: story\n\t\t? JumpToStoryClickHandler(story)\n\t\t: (data->external()\n\t\t\t&& (!fields.messageId\n\t\t\t\t|| (data->unavailable() && externalChannelId)))\n\t\t? std::make_shared<LambdaClickHandler>(externalLink)\n\t\t: nullptr;\n}\n\nPeerData *Reply::sender(\n\t\tnot_null<const Element*> view,\n\t\tnot_null<HistoryMessageReply*> data) const {\n\tconst auto message = data->resolvedMessage.get();\n\tif (const auto story = data->resolvedStory.get()) {\n\t\treturn story->peer();\n\t} else if (!message) {\n\t\treturn _externalSender;\n\t} else if (view->data()->Has<HistoryMessageForwarded>()) {\n\t\t// Forward of a reply. Show reply-to original sender.\n\t\tconst auto forwarded = message->Get<HistoryMessageForwarded>();\n\t\tif (forwarded) {\n\t\t\treturn forwarded->originalSender;\n\t\t}\n\t}\n\tif (const auto from = message->displayFrom()) {\n\t\treturn from;\n\t}\n\treturn message->author().get();\n}\n\nQString Reply::senderName(\n\t\tnot_null<const Element*> view,\n\t\tnot_null<HistoryMessageReply*> data,\n\t\tbool shorten) const {\n\tif (const auto peer = sender(view, data)) {\n\t\treturn senderName(peer, shorten);\n\t} else if (!data->resolvedMessage) {\n\t\treturn data->fields().externalSenderName;\n\t} else if (view->data()->Has<HistoryMessageForwarded>()) {\n\t\t// Forward of a reply. Show reply-to original sender.\n\t\tconst auto forwarded\n\t\t\t= data->resolvedMessage->Get<HistoryMessageForwarded>();\n\t\tif (forwarded) {\n\t\t\tAssert(forwarded->originalHiddenSenderInfo != nullptr);\n\t\t\treturn forwarded->originalHiddenSenderInfo->name;\n\t\t}\n\t}\n\treturn QString();\n}\n\nQString Reply::senderName(\n\t\tnot_null<PeerData*> peer,\n\t\tbool shorten) const {\n\tconst auto user = shorten ? peer->asUser() : nullptr;\n\treturn user ? user->firstName : peer->name();\n}\n\nbool Reply::isNameUpdated(\n\t\tnot_null<const Element*> view,\n\t\tnot_null<HistoryMessageReply*> data) const {\n\tif (const auto from = sender(view, data)) {\n\t\tif (_nameVersion < from->nameVersion()) {\n\t\t\tupdateName(view, data, from);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Reply::updateName(\n\t\tnot_null<const Element*> view,\n\t\tnot_null<HistoryMessageReply*> data,\n\t\tstd::optional<PeerData*> resolvedSender) const {\n\tauto viaBotUsername = QString();\n\tconst auto message = data->resolvedMessage.get();\n\tconst auto forwarded = message\n\t\t? message->Get<HistoryMessageForwarded>()\n\t\t: nullptr;\n\tif (message && !forwarded) {\n\t\tif (const auto bot = message->viaBot()) {\n\t\t\tviaBotUsername = bot->username();\n\t\t}\n\t}\n\tconst auto history = view->history();\n\tconst auto &fields = data->fields();\n\tconst auto sender = resolvedSender.value_or(this->sender(view, data));\n\tconst auto externalPeer = fields.externalPeerId\n\t\t? history->owner().peer(fields.externalPeerId).get()\n\t\t: nullptr;\n\tconst auto displayAsExternal = data->displayAsExternal(view->data());\n\tconst auto groupNameAdded = displayAsExternal\n\t\t&& externalPeer\n\t\t&& (externalPeer != sender)\n\t\t&& (externalPeer->isChat() || externalPeer->isMegagroup());\n\tconst auto originalNameAdded = !displayAsExternal\n\t\t&& forwarded\n\t\t&& !message->isDiscussionPost()\n\t\t&& (forwarded->forwardOfForward()\n\t\t\t|| (!message->showForwardsFromSender(forwarded)\n\t\t\t\t&& !view->data()->Has<HistoryMessageForwarded>()));\n\tconst auto shorten = !viaBotUsername.isEmpty()\n\t\t|| groupNameAdded\n\t\t|| originalNameAdded;\n\tconst auto name = sender\n\t\t? senderName(sender, shorten)\n\t\t: senderName(view, data, shorten);\n\tconst auto previewSkip = _hasPreview\n\t\t? (st::messageQuoteStyle.outline\n\t\t\t+ st::historyReplyPreviewMargin.left()\n\t\t\t+ st::historyReplyPreview\n\t\t\t+ st::historyReplyPreviewMargin.right()\n\t\t\t- st::historyReplyPadding.left())\n\t\t: 0;\n\tauto nameFull = TextWithEntities();\n\tif (displayAsExternal && !groupNameAdded && !fields.storyId) {\n\t\tnameFull.append(PeerEmoji(sender));\n\t}\n\tnameFull.append(name);\n\tif (groupNameAdded) {\n\t\tnameFull.append(' ').append(PeerEmoji(externalPeer));\n\t\tnameFull.append(externalPeer->name());\n\t} else if (originalNameAdded) {\n\t\tnameFull.append(' ').append(\n\t\t\tst::historyReplyForward\n\t\t).append(forwarded->originalSender\n\t\t\t? forwarded->originalSender->name()\n\t\t\t: forwarded->originalHiddenSenderInfo->name);\n\t}\n\tif (!viaBotUsername.isEmpty()) {\n\t\tnameFull.append(u\" @\"_q).append(viaBotUsername);\n\t}\n\tconst auto context = Core::TextContext({\n\t\t.session = &history->session(),\n\t\t.customEmojiLoopLimit = 1,\n\t});\n\t_name.setMarkedText(\n\t\tst::fwdTextStyle,\n\t\tnameFull,\n\t\tUi::NameTextOptions(),\n\t\tcontext);\n\tif (sender) {\n\t\t_nameVersion = sender->nameVersion();\n\t}\n\tconst auto nameMaxWidth = previewSkip\n\t\t+ _name.maxWidth()\n\t\t+ st::messageGiftIconSkip\n\t\t+ (_hasQuoteIcon\n\t\t\t? st::messageTextStyle.blockquote.icon.width()\n\t\t\t: 0);\n\tconst auto storySkip = fields.storyId\n\t\t? (st::dialogsMiniReplyStory.skipText\n\t\t\t+ st::dialogsMiniReplyStory.icon.icon.width())\n\t\t: 0;\n\tconst auto optimalTextSize = _multiline\n\t\t? countMultilineOptimalSize(previewSkip)\n\t\t: QSize(\n\t\t\t(previewSkip\n\t\t\t\t+ storySkip\n\t\t\t\t+ std::min(_text.maxWidth(), st::maxSignatureSize)),\n\t\t\tst::normalFont->height);\n\t_maxWidth = std::max(nameMaxWidth, optimalTextSize.width());\n\tif (!data->displaying()) {\n\t\tconst auto unavailable = data->unavailable();\n\t\t_stateText = ((fields.messageId || fields.storyId) && !unavailable)\n\t\t\t? tr::lng_profile_loading(tr::now)\n\t\t\t: fields.storyId\n\t\t\t? tr::lng_deleted_story(tr::now)\n\t\t\t: tr::lng_deleted_message(tr::now);\n\t\tconst auto phraseWidth = st::msgDateFont->width(_stateText);\n\t\t_maxWidth = unavailable\n\t\t\t? phraseWidth\n\t\t\t: std::max(_maxWidth, phraseWidth);\n\t} else {\n\t\t_stateText = QString();\n\t}\n\t_maxWidth = st::historyReplyPadding.left()\n\t\t+ _maxWidth\n\t\t+ st::historyReplyPadding.right();\n\t_minHeight = st::historyReplyPadding.top()\n\t\t+ st::msgServiceNameFont->height\n\t\t+ optimalTextSize.height()\n\t\t+ st::historyReplyPadding.bottom();\n}\n\nint Reply::resizeToWidth(int width) const {\n\t_ripple.animation = nullptr;\n\n\tconst auto previewSkip = _hasPreview\n\t\t? (st::messageQuoteStyle.outline\n\t\t\t+ st::historyReplyPreviewMargin.left()\n\t\t\t+ st::historyReplyPreview\n\t\t\t+ st::historyReplyPreviewMargin.right()\n\t\t\t- st::historyReplyPadding.left())\n\t\t: 0;\n\tif (width >= _maxWidth || !_multiline) {\n\t\t_nameTwoLines = 0;\n\t\t_expandable = _minHeightExpandable;\n\t\t_height = _minHeight;\n\t\treturn height();\n\t}\n\tconst auto innerw = width\n\t\t- st::historyReplyPadding.left()\n\t\t- st::historyReplyPadding.right();\n\tconst auto namew = innerw - previewSkip;\n\tconst auto desiredNameHeight = _name.countHeight(namew);\n\t_nameTwoLines = (desiredNameHeight > st::semiboldFont->height) ? 1 : 0;\n\tconst auto nameh = (_nameTwoLines ? 2 : 1) * st::semiboldFont->height;\n\tconst auto firstLineSkip = _nameTwoLines ? 0 : previewSkip;\n\tauto elided = false;\n\tconst auto texth = _text.countDimensions(\n\t\ttextGeometry(innerw, firstLineSkip, &elided)).height;\n\t_expandable = elided ? 1 : 0;\n\t_height = st::historyReplyPadding.top()\n\t\t+ nameh\n\t\t+ std::max(texth, st::normalFont->height)\n\t\t+ st::historyReplyPadding.bottom();\n\treturn height();\n}\n\nUi::Text::GeometryDescriptor Reply::textGeometry(\n\t\tint available,\n\t\tint firstLineSkip,\n\t\tbool *outElided) const {\n\treturn { .layout = [=](int line) {\n\t\tconst auto skip = (line ? 0 : firstLineSkip);\n\t\tconst auto elided = !_multiline\n\t\t\t|| (!_expanded && (line + 1 >= kNonExpandedLinesLimit));\n\t\treturn Ui::Text::LineGeometry{\n\t\t\t.left = skip,\n\t\t\t.width = available - skip,\n\t\t\t.elided = elided,\n\t\t};\n\t}, .outElided = outElided };\n}\n\nint Reply::height() const {\n\treturn _height + st::historyReplyTop + st::historyReplyBottom;\n}\n\nQMargins Reply::margins() const {\n\treturn QMargins(0, st::historyReplyTop, 0, st::historyReplyBottom);\n}\n\nQSize Reply::countMultilineOptimalSize(\n\t\tint previewSkip) const {\n\tauto elided = false;\n\tconst auto max = previewSkip + _text.maxWidth();\n\tconst auto result = _text.countDimensions(\n\t\ttextGeometry(max, previewSkip, &elided));\n\t_minHeightExpandable = elided ? 1 : 0;\n\treturn {\n\t\tresult.width + st::historyReplyPadding.right(),\n\t\tstd::max(result.height, st::normalFont->height),\n\t};\n}\n\nvoid Reply::paint(\n\t\tPainter &p,\n\t\tnot_null<const Element*> view,\n\t\tconst Ui::ChatPaintContext &context,\n\t\tint x,\n\t\tint y,\n\t\tint w,\n\t\tbool inBubble) const {\n\tconst auto st = context.st;\n\tconst auto stm = context.messageStyle();\n\n\ty += st::historyReplyTop;\n\tconst auto rect = QRect(x, y, w, _height);\n\tconst auto selected = context.selected();\n\tconst auto backgroundEmojiId = _colorPeer\n\t\t? _colorPeer->backgroundEmojiId()\n\t\t: DocumentId();\n\tconst auto colorIndexPlusOne = _colorPeer\n\t\t? (_colorPeer->colorIndex() + 1)\n\t\t: _hiddenSenderColorIndexPlusOne;\n\tconst auto &colorCollectible = _colorPeer\n\t\t? _colorPeer->colorCollectible()\n\t\t: nullptr;\n\tconst auto useColorCollectible = colorCollectible && !context.outbg;\n\tconst auto useColorIndex = colorIndexPlusOne && !context.outbg;\n\tconst auto colorPattern = colorCollectible\n\t\t? st->collectiblePatternIndex(colorCollectible)\n\t\t: colorIndexPlusOne\n\t\t? st->colorPatternIndex(colorIndexPlusOne - 1)\n\t\t: 0;\n\tconst auto cache = !inBubble\n\t\t? (_hasQuoteIcon\n\t\t\t? st->serviceQuoteCache(colorPattern)\n\t\t\t: st->serviceReplyCache(colorPattern)).get()\n\t\t: useColorCollectible\n\t\t? (_hasQuoteIcon\n\t\t\t? st->collectibleQuoteCache(selected, colorCollectible)\n\t\t\t: st->collectibleReplyCache(selected, colorCollectible)).get()\n\t\t: useColorIndex\n\t\t? (_hasQuoteIcon\n\t\t\t? st->coloredQuoteCache(selected, colorIndexPlusOne - 1)\n\t\t\t: st->coloredReplyCache(selected, colorIndexPlusOne - 1)).get()\n\t\t: (_hasQuoteIcon\n\t\t\t? stm->quoteCache[colorPattern]\n\t\t\t: stm->replyCache[colorPattern]).get();\n\tconst auto &quoteSt = _hasQuoteIcon\n\t\t? st::messageTextStyle.blockquote\n\t\t: st::messageQuoteStyle;\n\tconst auto backgroundEmojiData = backgroundEmojiId\n\t\t? st->backgroundEmojiData(backgroundEmojiId, colorCollectible).get()\n\t\t: nullptr;\n\tconst auto backgroundEmojiCache = !backgroundEmojiData\n\t\t? nullptr\n\t\t: useColorCollectible\n\t\t? &backgroundEmojiData->collectibleCaches[colorCollectible]\n\t\t: &backgroundEmojiData->caches[Ui::BackgroundEmojiData::CacheIndex(\n\t\t\tselected,\n\t\t\tcontext.outbg,\n\t\t\tinBubble,\n\t\t\tuseColorIndex ? colorIndexPlusOne : 0)];\n\tconst auto rippleColor = cache->bg;\n\tif (!inBubble) {\n\t\tcache->bg = QColor(0, 0, 0, 0);\n\t}\n\tUi::Text::ValidateQuotePaintCache(*cache, quoteSt);\n\tUi::Text::FillQuotePaint(p, rect, *cache, quoteSt);\n\tif (backgroundEmojiData) {\n\t\tValidateBackgroundEmoji(\n\t\t\tbackgroundEmojiId,\n\t\t\tcolorCollectible,\n\t\t\tbackgroundEmojiData,\n\t\t\tbackgroundEmojiCache,\n\t\t\tcache,\n\t\t\tview);\n\t\tif (!backgroundEmojiCache->frames[0].isNull()) {\n\t\t\tFillBackgroundEmoji(\n\t\t\t\tp,\n\t\t\t\trect,\n\t\t\t\t_hasQuoteIcon,\n\t\t\t\t*backgroundEmojiCache,\n\t\t\t\tbackgroundEmojiData->firstGiftFrame);\n\t\t}\n\t}\n\tif (!inBubble) {\n\t\tcache->bg = rippleColor;\n\t}\n\n\tif (_ripple.animation) {\n\t\t_ripple.lastPaintedPoint = inBubble ? QPoint(x, y) : QPoint();\n\t\t_ripple.animation->paint(p, x, y, w, &rippleColor);\n\t\tif (_ripple.animation->empty()) {\n\t\t\t_ripple.animation.reset();\n\t\t\t_ripple.lastPaintedPoint = {};\n\t\t}\n\t}\n\n\tauto hasPreview = (_hasPreview != 0);\n\tauto previewSkip = hasPreview\n\t\t? (st::messageQuoteStyle.outline\n\t\t\t+ st::historyReplyPreviewMargin.left()\n\t\t\t+ st::historyReplyPreview\n\t\t\t+ st::historyReplyPreviewMargin.right()\n\t\t\t- st::historyReplyPadding.left())\n\t\t: 0;\n\tif (hasPreview && w <= st::historyReplyPadding.left() + previewSkip) {\n\t\thasPreview = false;\n\t\tpreviewSkip = 0;\n\t}\n\n\tconst auto pausedSpoiler = context.paused\n\t\t|| On(PowerSaving::kChatSpoiler);\n\tauto textLeft = x + st::historyReplyPadding.left();\n\tauto textTop = y\n\t\t+ st::historyReplyPadding.top()\n\t\t+ (st::msgServiceNameFont->height * (_nameTwoLines ? 2 : 1));\n\tif (w > st::historyReplyPadding.left()) {\n\t\tif (_displaying) {\n\t\t\tif (hasPreview) {\n\t\t\t\tconst auto data = view->data()->Get<HistoryMessageReply>();\n\t\t\t\tconst auto message = data\n\t\t\t\t\t? data->resolvedMessage.get()\n\t\t\t\t\t: nullptr;\n\t\t\t\tconst auto media = message ? message->media() : nullptr;\n\t\t\t\tconst auto messagePoll = media ? media->poll() : nullptr;\n\t\t\t\tconst auto pollAnswer = messagePoll\n\t\t\t\t\t? messagePoll->answerByOption(\n\t\t\t\t\t\tdata->fields().pollOption)\n\t\t\t\t\t: nullptr;\n\t\t\t\tconst auto pollMediaPtr = pollAnswer\n\t\t\t\t\t? &pollAnswer->media\n\t\t\t\t\t: (messagePoll\n\t\t\t\t\t\t&& data->fields().pollOption.isEmpty())\n\t\t\t\t\t? &messagePoll->attachedMedia\n\t\t\t\t\t: nullptr;\n\t\t\t\tconst auto image = [&]() -> Image* {\n\t\t\t\t\tif (pollMediaPtr) {\n\t\t\t\t\t\tif (pollMediaPtr->photo) {\n\t\t\t\t\t\t\treturn pollMediaPtr->photo->getReplyPreview(\n\t\t\t\t\t\t\t\tmessage);\n\t\t\t\t\t\t} else if (pollMediaPtr->document) {\n\t\t\t\t\t\t\treturn pollMediaPtr->document->getReplyPreview(\n\t\t\t\t\t\t\t\tmessage);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (media) {\n\t\t\t\t\t\treturn media->replyPreview();\n\t\t\t\t\t}\n\t\t\t\t\tif (!data) {\n\t\t\t\t\t\treturn nullptr;\n\t\t\t\t\t}\n\t\t\t\t\tif (data->resolvedStory) {\n\t\t\t\t\t\treturn data->resolvedStory->replyPreview();\n\t\t\t\t\t}\n\t\t\t\t\tif (data->fields().externalMedia) {\n\t\t\t\t\t\treturn data->fields().externalMedia\n\t\t\t\t\t\t\t->replyPreview();\n\t\t\t\t\t}\n\t\t\t\t\treturn nullptr;\n\t\t\t\t}();\n\t\t\t\tif (image) {\n\t\t\t\t\tauto to = style::rtlrect(\n\t\t\t\t\t\tx + st::historyReplyPreviewMargin.left(),\n\t\t\t\t\t\ty + st::historyReplyPreviewMargin.top(),\n\t\t\t\t\t\tst::historyReplyPreview,\n\t\t\t\t\t\tst::historyReplyPreview,\n\t\t\t\t\t\tw + 2 * x);\n\t\t\t\t\tconst auto preview = image->pixSingle(\n\t\t\t\t\t\timage->size() / style::DevicePixelRatio(),\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t.colored = (context.selected()\n\t\t\t\t\t\t\t\t? &st->msgStickerOverlay()\n\t\t\t\t\t\t\t\t: nullptr),\n\t\t\t\t\t\t\t.options = Images::Option::RoundSmall,\n\t\t\t\t\t\t\t.outer = to.size(),\n\t\t\t\t\t\t});\n\t\t\t\t\tp.drawPixmap(to.x(), to.y(), preview);\n\t\t\t\t\tif (_spoiler) {\n\t\t\t\t\t\tview->clearCustomEmojiRepaint();\n\t\t\t\t\t\tUi::FillSpoilerRect(\n\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\tto,\n\t\t\t\t\t\t\tUi::DefaultImageSpoiler().frame(\n\t\t\t\t\t\t\t\t_spoiler->index(\n\t\t\t\t\t\t\t\t\tcontext.now,\n\t\t\t\t\t\t\t\t\tpausedSpoiler)));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto textw = w\n\t\t\t\t- st::historyReplyPadding.left()\n\t\t\t\t- st::historyReplyPadding.right();\n\t\t\tconst auto namew = textw\n\t\t\t\t- previewSkip\n\t\t\t\t- st::messageGiftIconSkip\n\t\t\t\t- (_hasQuoteIcon\n\t\t\t\t\t? st::messageTextStyle.blockquote.icon.width()\n\t\t\t\t\t: 0);\n\t\t\tauto firstLineSkip = _nameTwoLines ? 0 : previewSkip;\n\t\t\tif (namew > 0) {\n\t\t\t\tp.setPen(!inBubble\n\t\t\t\t\t? st->msgImgReplyBarColor()->c\n\t\t\t\t\t: (colorCollectible || colorIndexPlusOne)\n\t\t\t\t\t? FromNameFg(\n\t\t\t\t\t\tcontext,\n\t\t\t\t\t\tcolorIndexPlusOne - 1,\n\t\t\t\t\t\tcolorCollectible)\n\t\t\t\t\t: stm->msgServiceFg->c);\n\t\t\t\t_name.drawLeftElided(\n\t\t\t\t\tp,\n\t\t\t\t\tx + st::historyReplyPadding.left() + previewSkip,\n\t\t\t\t\ty + st::historyReplyPadding.top(),\n\t\t\t\t\tnamew,\n\t\t\t\t\tw + 2 * x,\n\t\t\t\t\t_nameTwoLines ? 2 : 1);\n\n\t\t\t\tp.setPen(inBubble\n\t\t\t\t\t? stm->historyTextFg\n\t\t\t\t\t: st->msgImgReplyBarColor());\n\t\t\t\tview->prepareCustomEmojiPaint(p, context, _text);\n\t\t\t\tauto replyToTextPalette = &(!inBubble\n\t\t\t\t\t? st->imgReplyTextPalette()\n\t\t\t\t\t: useColorCollectible\n\t\t\t\t\t? st->collectibleTextPalette(selected, colorCollectible)\n\t\t\t\t\t: useColorIndex\n\t\t\t\t\t? st->coloredTextPalette(selected, colorIndexPlusOne - 1)\n\t\t\t\t\t: stm->replyTextPalette);\n\t\t\t\tauto owned = std::optional<style::owned_color>();\n\t\t\t\tauto copy = std::optional<style::TextPalette>();\n\t\t\t\tif (inBubble && colorIndexPlusOne) {\n\t\t\t\t\tcopy.emplace(*replyToTextPalette);\n\t\t\t\t\towned.emplace(cache->icon);\n\t\t\t\t\tcopy->linkFg = owned->color();\n\t\t\t\t\treplyToTextPalette = &*copy;\n\t\t\t\t}\n\t\t\t\tif (_replyToStory) {\n\t\t\t\t\tst::dialogsMiniReplyStory.icon.icon.paint(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\ttextLeft + firstLineSkip,\n\t\t\t\t\t\ttextTop,\n\t\t\t\t\t\tw + 2 * x,\n\t\t\t\t\t\treplyToTextPalette->linkFg->c);\n\t\t\t\t\tfirstLineSkip += st::dialogsMiniReplyStory.skipText\n\t\t\t\t\t\t+ st::dialogsMiniReplyStory.icon.icon.width();\n\t\t\t\t}\n\t\t\t\tif (_replyToPoll) {\n\t\t\t\t\tst::historyPollReplyIcon.paint(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\ttextLeft + firstLineSkip,\n\t\t\t\t\t\ttextTop,\n\t\t\t\t\t\tw + 2 * x,\n\t\t\t\t\t\treplyToTextPalette->linkFg->c);\n\t\t\t\t\tfirstLineSkip += st::historyPollReplyIconSkip\n\t\t\t\t\t\t+ st::historyPollReplyIcon.width();\n\t\t\t\t}\n\t\t\t\t_text.draw(p, {\n\t\t\t\t\t.position = { textLeft, textTop },\n\t\t\t\t\t.geometry = textGeometry(textw, firstLineSkip),\n\t\t\t\t\t.palette = replyToTextPalette,\n\t\t\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t\t\t.now = context.now,\n\t\t\t\t\t.pausedEmoji = (context.paused\n\t\t\t\t\t\t|| On(PowerSaving::kEmojiChat)),\n\t\t\t\t\t.pausedSpoiler = pausedSpoiler,\n\t\t\t\t\t.elisionLines = 1,\n\t\t\t\t});\n\t\t\t\tp.setTextPalette(stm->textPalette);\n\t\t\t}\n\t\t} else {\n\t\t\tp.setFont(st::msgDateFont);\n\t\t\tp.setPen(cache->icon);\n\t\t\tp.drawTextLeft(\n\t\t\t\ttextLeft,\n\t\t\t\t(y\n\t\t\t\t\t+ st::historyReplyPadding.top()\n\t\t\t\t\t+ (st::msgDateFont->height / 2)),\n\t\t\t\tw + 2 * x,\n\t\t\t\tst::msgDateFont->elided(\n\t\t\t\t\t_stateText,\n\t\t\t\t\tx + w - textLeft - st::historyReplyPadding.right()));\n\t\t}\n\t}\n}\n\nvoid Reply::createRippleAnimation(\n\t\tnot_null<const Element*> view,\n\t\tQSize size) {\n\t_ripple.animation = std::make_unique<Ui::RippleAnimation>(\n\t\tst::defaultRippleAnimation,\n\t\tUi::RippleAnimation::RoundRectMask(\n\t\t\tsize,\n\t\t\tst::messageQuoteStyle.radius),\n\t\t[=] {\n\t\t\tview->repaint(_ripple.lastPaintedPoint.isNull()\n\t\t\t\t? QRect()\n\t\t\t\t: QRect(_ripple.lastPaintedPoint, size));\n\t\t});\n}\n\nvoid Reply::saveRipplePoint(QPoint point) const {\n\t_ripple.lastPoint = point;\n}\n\nvoid Reply::addRipple() {\n\tif (_ripple.animation) {\n\t\t_ripple.animation->add(_ripple.lastPoint);\n\t}\n}\n\nvoid Reply::stopLastRipple() {\n\tif (_ripple.animation) {\n\t\t_ripple.animation->lastStop();\n\t}\n}\n\nTextWithEntities Reply::PeerEmoji(PeerData *peer) {\n\tusing namespace std;\n\tconst auto icon = !peer\n\t\t? &st::historyReplyUser\n\t\t: peer->isBroadcast()\n\t\t? &st::historyReplyChannel\n\t\t: (peer->isChannel() || peer->isChat())\n\t\t? &st::historyReplyGroup\n\t\t: &st::historyReplyUser;\n\treturn Ui::Text::IconEmoji(icon);\n}\n\nTextWithEntities Reply::ComposePreviewName(\n\t\tnot_null<History*> history,\n\t\tnot_null<HistoryItem*> to,\n\t\tconst FullReplyTo &replyTo) {\n\tconst auto sender = [&] {\n\t\tif (const auto from = to->displayFrom()) {\n\t\t\treturn not_null(from);\n\t\t}\n\t\treturn to->author();\n\t}();\n\tif (const auto media = (replyTo.todoItemId\n\t\t\t|| !replyTo.pollOption.isEmpty())\n\t\t? to->media()\n\t\t: nullptr) {\n\t\tif (const auto todolist = media->todolist()) {\n\t\t\treturn tr::lng_preview_reply_to_task(\n\t\t\t\ttr::now,\n\t\t\t\tlt_title,\n\t\t\t\ttodolist->title,\n\t\t\t\ttr::marked);\n\t\t} else if (const auto poll = media->poll()) {\n\t\t\treturn tr::lng_preview_reply_to_poll_option(\n\t\t\t\ttr::now,\n\t\t\t\tlt_title,\n\t\t\t\tpoll->question,\n\t\t\t\ttr::marked);\n\t\t}\n\t}\n\tconst auto toPeer = to->history()->peer;\n\tconst auto displayAsExternal = (to->history() != history);\n\tconst auto groupNameAdded = displayAsExternal\n\t\t&& (toPeer != sender)\n\t\t&& (toPeer->isChat() || toPeer->isMegagroup());\n\tconst auto quote = replyTo && !replyTo.quote.empty();\n\tconst auto shorten = groupNameAdded || quote;\n\n\tauto nameFull = TextWithEntities();\n\tusing namespace HistoryView;\n\tif (displayAsExternal && !groupNameAdded) {\n\t\tnameFull.append(Reply::PeerEmoji(sender));\n\t}\n\tnameFull.append(shorten ? sender->shortName() : sender->name());\n\tif (groupNameAdded) {\n\t\tnameFull.append(' ').append(Reply::PeerEmoji(toPeer));\n\t\tnameFull.append(toPeer->name());\n\t}\n\treturn (quote\n\t\t? tr::lng_preview_reply_to_quote\n\t\t: tr::lng_preview_reply_to)(\n\t\t\ttr::now,\n\t\t\tlt_name,\n\t\t\tnameFull,\n\t\t\ttr::marked);\n\n}\n\nvoid Reply::unloadPersistentAnimation() {\n\t_text.unloadPersistentAnimation();\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_reply.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/history_view_element.h\"\n\nnamespace Data {\nclass Session;\n} // namespace Data\n\nnamespace Ui {\nclass SpoilerAnimation;\nstruct BackgroundEmojiData;\nstruct BackgroundEmojiCache;\nstruct ColorCollectible;\n} // namespace Ui\n\nnamespace Ui::Text {\nclass CustomEmoji;\nstruct QuotePaintCache;\n} // namespace Ui::Text\n\nnamespace HistoryView {\n\nvoid ValidateBackgroundEmoji(\n\tDocumentId backgroundEmojiId,\n\tconst std::shared_ptr<Ui::ColorCollectible> &collectible,\n\tnot_null<Ui::BackgroundEmojiData*> data,\n\tnot_null<Ui::BackgroundEmojiCache*> cache,\n\tnot_null<Ui::Text::QuotePaintCache*> quote,\n\tnot_null<const Element*> view);\n\n// For this one data->firstFrameMask or data->emoji must be already set.\nvoid ValidateBackgroundEmoji(\n\tnot_null<Ui::BackgroundEmojiData*> data,\n\tnot_null<Ui::BackgroundEmojiCache*> cache,\n\tnot_null<Ui::Text::QuotePaintCache*> quote);\n[[nodiscard]] auto CreateBackgroundEmojiInstance(\n\tnot_null<Data::Session*> owner,\n\tDocumentId backgroundEmojiId,\n\tFn<void()> repaint)\n-> std::unique_ptr<Ui::Text::CustomEmoji>;\n[[nodiscard]] auto CreateBackgroundGiftInstance(\n\tnot_null<Data::Session*> owner,\n\tDocumentId giftEmojiId,\n\tFn<void()> repaint)\n-> std::unique_ptr<Ui::Text::CustomEmoji>;\n\nvoid FillBackgroundEmoji(\n\tQPainter &p,\n\tconst QRect &rect,\n\tbool quote,\n\tconst Ui::BackgroundEmojiCache &cache,\n\tconst QImage &firstGiftFrame);\n\nclass Reply final : public RuntimeComponent<Reply, Element> {\npublic:\n\tReply();\n\tReply(const Reply &other) = delete;\n\tReply(Reply &&other) = delete;\n\tReply &operator=(const Reply &other) = delete;\n\tReply &operator=(Reply &&other);\n\t~Reply();\n\n\tvoid update(\n\t\tnot_null<Element*> view,\n\t\tnot_null<HistoryMessageReply*> data);\n\n\t[[nodiscard]] bool isNameUpdated(\n\t\tnot_null<const Element*> view,\n\t\tnot_null<HistoryMessageReply*> data) const;\n\tvoid updateName(\n\t\tnot_null<const Element*> view,\n\t\tnot_null<HistoryMessageReply*> data,\n\t\tstd::optional<PeerData*> resolvedSender = std::nullopt) const;\n\t[[nodiscard]] int resizeToWidth(int width) const;\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] QMargins margins() const;\n\n\tbool expand();\n\n\tvoid paint(\n\t\tPainter &p,\n\t\tnot_null<const Element*> view,\n\t\tconst Ui::ChatPaintContext &context,\n\t\tint x,\n\t\tint y,\n\t\tint w,\n\t\tbool inBubble) const;\n\tvoid unloadPersistentAnimation();\n\n\tvoid createRippleAnimation(not_null<const Element*> view, QSize size);\n\tvoid saveRipplePoint(QPoint point) const;\n\tvoid addRipple();\n\tvoid stopLastRipple();\n\n\t[[nodiscard]] int maxWidth() const {\n\t\treturn _maxWidth;\n\t}\n\t[[nodiscard]] ClickHandlerPtr link() const {\n\t\treturn _link;\n\t}\n\n\t[[nodiscard]] static TextWithEntities PeerEmoji(PeerData *peer);\n\t[[nodiscard]] static TextWithEntities ComposePreviewName(\n\t\tnot_null<History*> history,\n\t\tnot_null<HistoryItem*> to,\n\t\tconst FullReplyTo &replyTo);\n\nprivate:\n\t[[nodiscard]] Ui::Text::GeometryDescriptor textGeometry(\n\t\tint available,\n\t\tint firstLineSkip,\n\t\tbool *outElided = nullptr) const;\n\t[[nodiscard]] QSize countMultilineOptimalSize(\n\t\tint firstLineSkip) const;\n\tvoid setLinkFrom(\n\t\tnot_null<Element*> view,\n\t\tnot_null<HistoryMessageReply*> data);\n\n\t[[nodiscard]] PeerData *sender(\n\t\tnot_null<const Element*> view,\n\t\tnot_null<HistoryMessageReply*> data) const;\n\t[[nodiscard]] QString senderName(\n\t\tnot_null<const Element*> view,\n\t\tnot_null<HistoryMessageReply*> data,\n\t\tbool shorten) const;\n\t[[nodiscard]] QString senderName(\n\t\tnot_null<PeerData*> peer,\n\t\tbool shorten) const;\n\n\tClickHandlerPtr _link;\n\tstd::unique_ptr<Ui::SpoilerAnimation> _spoiler;\n\tmutable PeerData *_externalSender = nullptr;\n\tmutable PeerData *_colorPeer = nullptr;\n\tmutable struct {\n\t\tstd::unique_ptr<Ui::RippleAnimation> animation;\n\t\tQPoint lastPoint;\n\t\tQPoint lastPaintedPoint;\n\t} _ripple;\n\tmutable Ui::Text::String _name;\n\tmutable Ui::Text::String _text;\n\tmutable QString _stateText;\n\tmutable int _maxWidth = 0;\n\tmutable int _minHeight = 0;\n\tmutable int _height = 0;\n\tmutable int _nameVersion = 0;\n\tuint8 _hiddenSenderColorIndexPlusOne : 7 = 0;\n\tuint8 _hasQuoteIcon : 1 = 0;\n\tuint8 _replyToStory : 1 = 0;\n\tuint8 _replyToPoll : 1 = 0;\n\tuint8 _expanded : 1 = 0;\n\tmutable uint8 _expandable : 1 = 0;\n\tmutable uint8 _minHeightExpandable : 1 = 0;\n\tmutable uint8 _nameTwoLines : 1 = 0;\n\tmutable uint8 _hasPreview : 1 = 0;\n\tmutable uint8 _displaying : 1 = 0;\n\tmutable uint8 _multiline : 1 = 0;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_reply_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_reply_button.h\"\n\n#include \"history/view/history_view_cursor_state.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace HistoryView::ReplyButton {\nnamespace {\n\nconstexpr auto kToggleDuration = crl::time(120);\nconstexpr auto kButtonShowDelay = crl::time(0);\nconstexpr auto kButtonHideDelay = crl::time(300);\n\n[[nodiscard]] float64 ScaleForState(ButtonState state) {\n\tswitch (state) {\n\tcase ButtonState::Hidden: return 0.;\n\tcase ButtonState::Shown:\n\tcase ButtonState::Active:\n\tcase ButtonState::Inside: return 1.;\n\t}\n\tUnexpected(\"State in ReplyButton::ScaleForState.\");\n}\n\n[[nodiscard]] float64 OpacityForScale(float64 scale) {\n\treturn scale;\n}\n\n[[nodiscard]] QSize ComputeInnerSize() {\n\treturn QSize(ComputeInnerWidth(), st::replyCornerHeight);\n}\n\n[[nodiscard]] QSize ComputeOuterSize() {\n\treturn QRect(\n\t\tQPoint(),\n\t\tComputeInnerSize()\n\t).marginsAdded(st::replyCornerShadow).size();\n}\n\n} // namespace\n\nint ComputeInnerWidth() {\n\tstruct Cached {\n\t\tQString text;\n\t\tint result = 0;\n\t};\n\tstatic auto cached = Cached();\n\tconst auto &text = tr::lng_fast_reply(tr::now);\n\tif (cached.text != text) {\n\t\tconst auto &padding = st::replyCornerTextPadding;\n\t\tconst auto textWidth = st::msgDateTextStyle.font->width(text);\n\t\tcached.result = padding.left() + textWidth + padding.right();\n\t\tcached.text = text;\n\t}\n\treturn cached.result;\n}\n\nButton::Button(\n\tFn<void(QRect)> update,\n\tButtonParameters parameters,\n\tFn<void()> hide,\n\tQSize outer)\n: _update(std::move(update))\n, _finalScale(ScaleForState(_state))\n, _collapsed(QPoint(), outer)\n, _geometry(_collapsed)\n, _hideTimer(hide) {\n\tapplyParameters(parameters, nullptr);\n}\n\nButton::~Button() = default;\n\nbool Button::isHidden() const {\n\treturn (_state == ButtonState::Hidden)\n\t\t&& !_opacityAnimation.animating();\n}\n\nQRect Button::geometry() const {\n\treturn _geometry;\n}\n\nfloat64 Button::currentScale() const {\n\treturn _scaleAnimation.value(_finalScale);\n}\n\nfloat64 Button::currentOpacity() const {\n\treturn _opacityAnimation.value(\n\t\tOpacityForScale(ScaleForState(_state)));\n}\n\nvoid Button::applyParameters(ButtonParameters parameters) {\n\tapplyParameters(std::move(parameters), _update);\n}\n\nvoid Button::applyParameters(\n\t\tButtonParameters parameters,\n\t\tFn<void(QRect)> update) {\n\tconst auto shift = parameters.center - _collapsed.center();\n\t_collapsed = _collapsed.translated(shift);\n\tupdateGeometry(update);\n\tapplyState(ButtonState::Shown, update);\n\tif (parameters.outside) {\n\t\t_hideTimer.callOnce(kButtonHideDelay);\n\t} else {\n\t\t_hideTimer.cancel();\n\t}\n}\n\nvoid Button::updateGeometry(Fn<void(QRect)> update) {\n\tif (_geometry != _collapsed) {\n\t\tif (update) {\n\t\t\tupdate(_geometry);\n\t\t}\n\t\t_geometry = _collapsed;\n\t\tif (update) {\n\t\t\tupdate(_geometry);\n\t\t}\n\t}\n}\n\nvoid Button::applyState(ButtonState state) {\n\tapplyState(state, _update);\n}\n\nvoid Button::applyState(ButtonState state, Fn<void(QRect)> update) {\n\tif (state == ButtonState::Hidden) {\n\t\t_hideTimer.cancel();\n\t}\n\tupdateGeometry(update);\n\tif (_state == state) {\n\t\treturn;\n\t}\n\tconst auto finalScale = ScaleForState(state);\n\t_opacityAnimation.start(\n\t\t[=] { _update(_geometry); },\n\t\tOpacityForScale(ScaleForState(_state)),\n\t\tOpacityForScale(ScaleForState(state)),\n\t\tkToggleDuration,\n\t\tanim::sineInOut);\n\tif (state != ButtonState::Hidden && _finalScale != finalScale) {\n\t\t_scaleAnimation.start(\n\t\t\t[=] { _update(_geometry); },\n\t\t\t_finalScale,\n\t\t\tfinalScale,\n\t\t\tkToggleDuration,\n\t\t\tanim::sineInOut);\n\t\t_finalScale = finalScale;\n\t}\n\t_state = state;\n}\n\nManager::Manager(Fn<void(QRect)> buttonUpdate)\n: _outer(ComputeOuterSize())\n, _inner(QRect(QPoint(), ComputeInnerSize()))\n, _cachedRound(\n\tComputeInnerSize(),\n\tst::replyCornerShadow,\n\tComputeInnerSize().height())\n, _buttonShowTimer([=] { showButtonDelayed(); })\n, _buttonUpdate(std::move(buttonUpdate))\n, _text(st::msgDateTextStyle.font->width(tr::lng_fast_reply(tr::now))) {\n\t_text.setText(st::msgDateTextStyle, tr::lng_fast_reply(tr::now));\n\t_inner.translate(\n\t\tQRect(QPoint(), _outer).center() - _inner.center());\n}\n\nManager::~Manager() = default;\n\nvoid Manager::updateButton(ButtonParameters parameters) {\n\tconst auto contextChanged = (_buttonContext != parameters.context);\n\tif (contextChanged) {\n\t\tif (_button) {\n\t\t\t_button->applyState(ButtonState::Hidden);\n\t\t\t_buttonHiding.push_back(std::move(_button));\n\t\t}\n\t\t_buttonShowTimer.cancel();\n\t\t_scheduledParameters = std::nullopt;\n\t\t_ripple = nullptr;\n\t}\n\t_buttonContext = parameters.context;\n\t_lastPointer = parameters.pointer;\n\tif (parameters.link) {\n\t\t_link = parameters.link;\n\t}\n\tif (!_buttonContext) {\n\t\treturn;\n\t} else if (_button) {\n\t\t_button->applyParameters(parameters);\n\t\treturn;\n\t} else if (parameters.outside) {\n\t\t_buttonShowTimer.cancel();\n\t\t_scheduledParameters = std::nullopt;\n\t\treturn;\n\t}\n\tconst auto globalPositionChanged = _scheduledParameters\n\t\t&& (_scheduledParameters->globalPointer\n\t\t\t!= parameters.globalPointer);\n\tconst auto positionChanged = _scheduledParameters\n\t\t&& (_scheduledParameters->pointer != parameters.pointer);\n\t_scheduledParameters = parameters;\n\tif ((_buttonShowTimer.isActive() && positionChanged)\n\t\t|| globalPositionChanged) {\n\t\t_buttonShowTimer.callOnce(kButtonShowDelay);\n\t}\n}\n\nvoid Manager::showButtonDelayed() {\n\tclearAppearAnimations();\n\t_button = std::make_unique<Button>(\n\t\t_buttonUpdate,\n\t\t*_scheduledParameters,\n\t\t[=] { updateButton({}); },\n\t\t_outer);\n}\n\nvoid Manager::paint(QPainter &p, const PaintContext &context) {\n\tremoveStaleButtons();\n\tfor (const auto &button : _buttonHiding) {\n\t\tpaintButton(p, context, button.get());\n\t}\n\tif (const auto current = _button.get()) {\n\t\tif (context.gestureHorizontal.ratio) {\n\t\t\tcurrent->applyState(ButtonState::Hidden);\n\t\t\t_buttonHiding.push_back(std::move(_button));\n\t\t}\n\t\tpaintButton(p, context, current);\n\t}\n}\n\nvoid Manager::paintButton(\n\t\tQPainter &p,\n\t\tconst PaintContext &context,\n\t\tnot_null<Button*> button) {\n\tconst auto geometry = button->geometry();\n\tif (!context.clip.intersects(geometry)) {\n\t\treturn;\n\t}\n\tconstexpr auto kFramesCount = Ui::RoundAreaWithShadow::kFramesCount;\n\tconst auto scale = button->currentScale();\n\tconst auto scaleMin = ScaleForState(ButtonState::Hidden);\n\tconst auto scaleMax = ScaleForState(ButtonState::Shown);\n\tconst auto progress = (scale - scaleMin) / (scaleMax - scaleMin);\n\tconst auto frame = int(\n\t\tbase::SafeRound(progress * (kFramesCount - 1)));\n\tconst auto useScale = scaleMin\n\t\t+ (frame / float64(kFramesCount - 1))\n\t\t\t* (scaleMax - scaleMin);\n\tpaintButton(p, context, button, frame, useScale);\n}\n\nvoid Manager::paintButton(\n\t\tQPainter &p,\n\t\tconst PaintContext &context,\n\t\tnot_null<Button*> button,\n\t\tint frameIndex,\n\t\tfloat64 scale) {\n\tconst auto opacity = button->currentOpacity();\n\tif (opacity == 0.) {\n\t\treturn;\n\t}\n\tconst auto geometry = button->geometry();\n\tconst auto position = geometry.topLeft();\n\tif (opacity != 1.) {\n\t\tp.setOpacity(opacity);\n\t}\n\tconst auto shadow = context.st->shadowFg()->c;\n\tconst auto background = context.st->windowBg()->c;\n\t_cachedRound.setShadowColor(shadow);\n\t_cachedRound.setBackgroundColor(background);\n\tconst auto radius = _inner.height() / 2.;\n\tconst auto frame = _cachedRound.validateFrame(\n\t\tframeIndex,\n\t\tscale,\n\t\tradius);\n\tp.drawImage(position, *frame.image, frame.rect);\n\n\tif (_ripple && !_ripple->empty() && _button && button == _button.get()) {\n\t\tconst auto color = context.st->windowBgOver()->c;\n\t\t_ripple->paint(\n\t\t\tp,\n\t\t\tposition.x() + _inner.x(),\n\t\t\tposition.y() + _inner.y(),\n\t\t\t_inner.width(),\n\t\t\t&color);\n\t\tif (_ripple->empty()) {\n\t\t\t_ripple.reset();\n\t\t}\n\t}\n\n\tconst auto textLeft = position.x()\n\t\t+ _inner.x()\n\t\t+ st::replyCornerTextPadding.left();\n\tconst auto textTop = position.y()\n\t\t+ _inner.y()\n\t\t+ (_inner.height() - st::msgDateTextStyle.font->height) / 2;\n\tconst auto &incomingStyle = context.st->messageStyle(false, false);\n\tp.setPen(incomingStyle.msgDateFg);\n\tp.setFont(st::msgDateTextStyle.font);\n\t_text.draw(p, {\n\t\t.position = QPoint(textLeft, textTop),\n\t\t.availableWidth = _text.maxWidth(),\n\t});\n\tif (opacity != 1.) {\n\t\tp.setOpacity(1.);\n\t}\n}\n\nTextState Manager::buttonTextState(QPoint position) const {\n\tif (overCurrentButton(position)) {\n\t\tauto result = TextState(nullptr, _link);\n\t\tresult.itemId = _buttonContext;\n\t\treturn result;\n\t}\n\treturn {};\n}\n\nbool Manager::overCurrentButton(QPoint position) const {\n\tif (!_button) {\n\t\treturn false;\n\t}\n\treturn buttonInner().contains(position);\n}\n\nQMargins Manager::innerMargins() const {\n\treturn {\n\t\t_inner.x(),\n\t\t_inner.y(),\n\t\t_outer.width() - _inner.x() - _inner.width(),\n\t\t_outer.height() - _inner.y() - _inner.height(),\n\t};\n}\n\nQRect Manager::buttonInner() const {\n\treturn buttonInner(_button.get());\n}\n\nQRect Manager::buttonInner(not_null<Button*> button) const {\n\treturn button->geometry().marginsRemoved(innerMargins());\n}\n\nvoid Manager::remove(FullMsgId context) {\n\tif (_buttonContext == context) {\n\t\t_buttonContext = {};\n\t\t_button = nullptr;\n\t\t_ripple = nullptr;\n\t}\n}\n\nvoid Manager::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &action,\n\t\tbool pressed) {\n\tif (action != _link || !_button) {\n\t\treturn;\n\t}\n\tif (pressed) {\n\t\tconst auto inner = buttonInner();\n\t\tif (!_ripple) {\n\t\t\tconst auto mask = Ui::RippleAnimation::RoundRectMask(\n\t\t\t\tinner.size(),\n\t\t\t\tinner.height() / 2);\n\t\t\t_ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\tmask,\n\t\t\t\t[=] { if (_button) _buttonUpdate(_button->geometry()); });\n\t\t}\n\t\t_ripple->add(_lastPointer - inner.topLeft());\n\t} else if (_ripple) {\n\t\t_ripple->lastStop();\n\t}\n}\n\nvoid Manager::removeStaleButtons() {\n\t_buttonHiding.erase(\n\t\tranges::remove_if(_buttonHiding, &Button::isHidden),\n\t\tend(_buttonHiding));\n}\n\nvoid Manager::clearAppearAnimations() {\n\tfor (const auto &button : base::take(_buttonHiding)) {\n\t\tif (!button->isHidden()) {\n\t\t\tbutton->repaint();\n\t\t}\n\t}\n}\n\n} // namespace HistoryView::ReplyButton\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_reply_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/round_area_with_shadow.h\"\n#include \"ui/text/text.h\"\n\nnamespace Ui {\nclass RippleAnimation;\nstruct ChatPaintContext;\n} // namespace Ui\n\nnamespace HistoryView {\nusing PaintContext = Ui::ChatPaintContext;\nstruct TextState;\n} // namespace HistoryView\n\nnamespace HistoryView::ReplyButton {\n\nstruct ButtonParameters {\n\t[[nodiscard]] ButtonParameters translated(QPoint delta) const {\n\t\tauto result = *this;\n\t\tresult.center += delta;\n\t\tresult.pointer += delta;\n\t\treturn result;\n\t}\n\n\tFullMsgId context;\n\tQPoint center;\n\tQPoint pointer;\n\tQPoint globalPointer;\n\tClickHandlerPtr link;\n\tint visibleTop = 0;\n\tint visibleBottom = 0;\n\tbool outside = false;\n};\n\n[[nodiscard]] int ComputeInnerWidth();\n\nenum class ButtonState {\n\tHidden,\n\tShown,\n\tActive,\n\tInside,\n};\n\nclass Button final {\npublic:\n\tButton(\n\t\tFn<void(QRect)> update,\n\t\tButtonParameters parameters,\n\t\tFn<void()> hide,\n\t\tQSize outer);\n\t~Button();\n\n\tvoid applyParameters(ButtonParameters parameters);\n\tvoid applyState(ButtonState state);\n\n\t[[nodiscard]] bool isHidden() const;\n\t[[nodiscard]] QRect geometry() const;\n\t[[nodiscard]] float64 currentScale() const;\n\t[[nodiscard]] float64 currentOpacity() const;\n\n\tvoid repaint() const {\n\t\t_update(_geometry);\n\t}\n\nprivate:\n\tvoid updateGeometry(Fn<void(QRect)> update);\n\tvoid applyState(ButtonState state, Fn<void(QRect)> update);\n\tvoid applyParameters(\n\t\tButtonParameters parameters,\n\t\tFn<void(QRect)> update);\n\n\tconst Fn<void(QRect)> _update;\n\n\tButtonState _state = ButtonState::Hidden;\n\tfloat64 _finalScale = 0.;\n\tUi::Animations::Simple _scaleAnimation;\n\tUi::Animations::Simple _opacityAnimation;\n\n\tQRect _collapsed;\n\tQRect _geometry;\n\n\tbase::Timer _hideTimer;\n\n};\n\nclass Manager final\n\t: public base::has_weak_ptr\n\t, public ClickHandlerHost {\npublic:\n\tManager(Fn<void(QRect)> buttonUpdate);\n\t~Manager();\n\n\tvoid updateButton(ButtonParameters parameters);\n\tvoid paint(QPainter &p, const PaintContext &context);\n\t[[nodiscard]] TextState buttonTextState(QPoint position) const;\n\tvoid remove(FullMsgId context);\n\nprotected:\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &action,\n\t\tbool pressed) override;\n\nprivate:\n\tvoid showButtonDelayed();\n\t[[nodiscard]] bool overCurrentButton(QPoint position) const;\n\tvoid paintButton(\n\t\tQPainter &p,\n\t\tconst PaintContext &context,\n\t\tnot_null<Button*> button);\n\tvoid paintButton(\n\t\tQPainter &p,\n\t\tconst PaintContext &context,\n\t\tnot_null<Button*> button,\n\t\tint frame,\n\t\tfloat64 scale);\n\tvoid removeStaleButtons();\n\tvoid clearAppearAnimations();\n\t[[nodiscard]] QMargins innerMargins() const;\n\t[[nodiscard]] QRect buttonInner() const;\n\t[[nodiscard]] QRect buttonInner(not_null<Button*> button) const;\n\n\tQSize _outer;\n\tQRect _inner;\n\tUi::RoundAreaWithShadow _cachedRound;\n\n\tClickHandlerPtr _link;\n\tFullMsgId _buttonContext;\n\n\tstd::optional<ButtonParameters> _scheduledParameters;\n\tbase::Timer _buttonShowTimer;\n\tconst Fn<void(QRect)> _buttonUpdate;\n\tstd::unique_ptr<Button> _button;\n\tstd::vector<std::unique_ptr<Button>> _buttonHiding;\n\n\tUi::Text::String _text;\n\tstd::unique_ptr<Ui::RippleAnimation> _ripple;\n\tQPoint _lastPointer;\n\n};\n\n} // namespace HistoryView::ReplyButton\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_requests_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_requests_bar.h\"\n\n#include \"history/view/history_view_group_call_bar.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_session.h\"\n#include \"data/data_peer_values.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/requests_bar.h\"\n#include \"ui/chat/group_call_userpics.h\"\n#include \"info/profile/info_profile_values.h\"\n\nnamespace HistoryView {\n\nrpl::producer<Ui::RequestsBarContent> RequestsBarContentByPeer(\n\t\tnot_null<PeerData*> peer,\n\t\tint userpicSize,\n\t\tbool showInForum) {\n\tExpects(peer->isChat() || peer->isChannel());\n\n\tstruct State {\n\t\texplicit State(not_null<PeerData*> peer)\n\t\t: peer(peer) {\n\t\t\tcurrent.isGroup = !peer->isBroadcast();\n\t\t}\n\n\t\tnot_null<PeerData*> peer;\n\t\tstd::vector<UserpicInRow> userpics;\n\t\tstd::vector<not_null<UserData*>> users;\n\t\tUi::RequestsBarContent current;\n\t\tbase::has_weak_ptr guard;\n\t\tbool someUserpicsNotLoaded = false;\n\t\tbool pushScheduled = false;\n\t};\n\n\tstatic const auto FillUserpics = [](\n\t\t\tnot_null<State*> state) {\n\t\tconst auto &users = state->users;\n\t\tconst auto same = ranges::equal(\n\t\t\tstate->userpics,\n\t\t\tusers,\n\t\t\tranges::equal_to(),\n\t\t\t&UserpicInRow::peer);\n\t\tif (same) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (auto b = begin(users), e = end(users), i = b; i != e; ++i) {\n\t\t\tconst auto user = *i;\n\t\t\tconst auto j = ranges::find(\n\t\t\t\tstate->userpics,\n\t\t\t\tuser,\n\t\t\t\t&UserpicInRow::peer);\n\t\t\tconst auto place = begin(state->userpics) + (i - b);\n\t\t\tif (j == end(state->userpics)) {\n\t\t\t\tstate->userpics.insert(\n\t\t\t\t\tplace,\n\t\t\t\t\tUserpicInRow{ .peer = user });\n\t\t\t} else if (j > place) {\n\t\t\t\tranges::rotate(place, j, j + 1);\n\t\t\t}\n\t\t}\n\t\twhile (state->userpics.size() > users.size()) {\n\t\t\tstate->userpics.pop_back();\n\t\t}\n\t\treturn true;\n\t};\n\n\tstatic const auto RegenerateUserpics = [](\n\t\t\tnot_null<State*> state,\n\t\t\tint userpicSize,\n\t\t\tbool force = false) {\n\t\tconst auto result = FillUserpics(state) || force;\n\t\tif (!result) {\n\t\t\treturn false;\n\t\t}\n\t\tstate->current.users.reserve(state->userpics.size());\n\t\tstate->current.users.clear();\n\t\tstate->someUserpicsNotLoaded = false;\n\t\tfor (auto &userpic : state->userpics) {\n\t\t\tuserpic.peer->loadUserpic();\n\t\t\tauto image = PeerData::GenerateUserpicImage(\n\t\t\t\tuserpic.peer,\n\t\t\t\tuserpic.view,\n\t\t\t\tuserpicSize * style::DevicePixelRatio());\n\t\t\tuserpic.uniqueKey = userpic.peer->userpicUniqueKey(userpic.view);\n\t\t\tstate->current.users.push_back({\n\t\t\t\t.userpic = std::move(image),\n\t\t\t\t.userpicKey = userpic.uniqueKey,\n\t\t\t\t.id = userpic.peer->id.value,\n\t\t\t});\n\t\t\tif (userpic.peer->hasUserpic()\n\t\t\t\t&& userpic.peer->useEmptyUserpic(userpic.view)) {\n\t\t\t\tstate->someUserpicsNotLoaded = true;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t};\n\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tauto state = lifetime.make_state<State>(peer);\n\n\t\tconst auto pushNext = [=](bool now = false) {\n\t\t\tif ((!showInForum\n\t\t\t\t&& peer->isForum()\n\t\t\t\t&& !peer->useSubsectionTabs())\n\t\t\t\t|| (std::min(state->current.count, kRecentRequestsLimit)\n\t\t\t\t\t!= state->users.size())) {\n\t\t\t\treturn;\n\t\t\t} else if (now) {\n\t\t\t\tstate->pushScheduled = false;\n\t\t\t\tconsumer.put_next_copy(state->current);\n\t\t\t} else if (state->pushScheduled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->pushScheduled = true;\n\t\t\tcrl::on_main(&state->guard, [=] {\n\t\t\t\tif (state->pushScheduled) {\n\t\t\t\t\tstate->pushScheduled = false;\n\t\t\t\t\tconsumer.put_next_copy(state->current);\n\t\t\t\t}\n\t\t\t});\n\t\t};\n\n\t\tif (!showInForum) {\n\t\t\tif (const auto channel = peer->asChannel()) {\n\t\t\t\tData::PeerFlagValue(\n\t\t\t\t\tchannel,\n\t\t\t\t\tChannelData::Flag::Forum\n\t\t\t\t) | rpl::on_next([=](bool hiddenByForum) {\n\t\t\t\t\tif (hiddenByForum) {\n\t\t\t\t\t\tconsumer.put_next({});\n\t\t\t\t\t} else {\n\t\t\t\t\t\tpushNext();\n\t\t\t\t\t}\n\t\t\t\t}, lifetime);\n\t\t\t}\n\t\t}\n\n\t\tpeer->session().downloaderTaskFinished(\n\t\t) | rpl::filter([=] {\n\t\t\treturn state->someUserpicsNotLoaded;\n\t\t}) | rpl::on_next([=] {\n\t\t\tfor (const auto &userpic : state->userpics) {\n\t\t\t\tif (userpic.peer->userpicUniqueKey(userpic.view)\n\t\t\t\t\t!= userpic.uniqueKey) {\n\t\t\t\t\tRegenerateUserpics(state, userpicSize, true);\n\t\t\t\t\tpushNext();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime);\n\n\t\tInfo::Profile::PendingRequestsCountValue(\n\t\t\tpeer\n\t\t) | rpl::filter([=](int count) {\n\t\t\treturn (state->current.count != count);\n\t\t}) | rpl::on_next([=](int count) {\n\t\t\tconst auto &requesters = peer->isChat()\n\t\t\t\t? peer->asChat()->recentRequesters()\n\t\t\t\t: peer->asChannel()->recentRequesters();\n\t\t\tauto &owner = state->peer->owner();\n\t\t\tconst auto old = base::take(state->users);\n\t\t\tstate->users = std::vector<not_null<UserData*>>();\n\t\t\tconst auto use = std::min(\n\t\t\t\tint(requesters.size()),\n\t\t\t\tHistoryView::kRecentRequestsLimit);\n\t\t\tstate->users.reserve(use);\n\t\t\tfor (auto i = 0; i != use; ++i) {\n\t\t\t\tstate->users.push_back(owner.user(requesters[i]));\n\t\t\t}\n\t\t\tconst auto changed = (state->current.count != count)\n\t\t\t\t|| (count == 1\n\t\t\t\t\t&& ((state->users.size() != old.size())\n\t\t\t\t\t\t|| (old.size() == 1\n\t\t\t\t\t\t\t&& state->users.front() != old.front())));\n\t\t\tif (changed) {\n\t\t\t\tstate->current.count = count;\n\t\t\t\tif (count == 1 && !state->users.empty()) {\n\t\t\t\t\tconst auto user = state->users.front();\n\t\t\t\t\tstate->current.nameShort = user->shortName();\n\t\t\t\t\tstate->current.nameFull = user->name();\n\t\t\t\t} else {\n\t\t\t\t\tstate->current.nameShort\n\t\t\t\t\t\t= state->current.nameFull\n\t\t\t\t\t\t= QString();\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (RegenerateUserpics(state, userpicSize) || changed) {\n\t\t\t\tpushNext();\n\t\t\t}\n\t\t}, lifetime);\n\n\t\tpushNext(true);\n\t\treturn lifetime;\n\t};\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_requests_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\nstruct RequestsBarContent;\n} // namespace Ui\n\nnamespace HistoryView {\n\ninline constexpr auto kRecentRequestsLimit = 3;\n\n[[nodiscard]] rpl::producer<Ui::RequestsBarContent> RequestsBarContentByPeer(\n\tnot_null<PeerData*> peer,\n\tint userpicSize,\n\tbool showInForum);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_schedule_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_schedule_box.h\"\n\n#include \"api/api_common.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"base/event_filter.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"base/unixtime.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_send.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace HistoryView::details {\n\nnot_null<Main::Session*> SessionFromShow(\n\t\tconst std::shared_ptr<ChatHelpers::Show> &show) {\n\treturn &show->session();\n}\n\n} // namespace HistoryView::details\n\nnamespace HistoryView {\nnamespace {\n\nvoid FillSendUntilOnlineMenu(\n\t\tnot_null<Ui::IconButton*> button,\n\t\tFn<void()> callback,\n\t\tconst ScheduleBoxStyleArgs &style) {\n\tconst auto menu = std::make_shared<base::unique_qptr<Ui::PopupMenu>>();\n\tbutton->setClickedCallback([=] {\n\t\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tbutton,\n\t\t\t*style.popupMenuStyle);\n\t\t(*menu)->addAction(\n\t\t\ttr::lng_scheduled_send_until_online(tr::now),\n\t\t\tstd::move(callback),\n\t\t\t&st::menuIconWhenOnline);\n\t\t(*menu)->popup(QCursor::pos());\n\t\treturn true;\n\t});\n}\n\n} // namespace\n\nScheduleBoxStyleArgs::ScheduleBoxStyleArgs()\n: topButtonStyle(&st::infoTopBarMenu)\n, popupMenuStyle(&st::popupMenuWithIcons)\n, chooseDateTimeArgs({}) {\n}\n\nTimeId DefaultScheduleTime() {\n\treturn base::unixtime::now() + 600;\n}\n\nbool CanScheduleUntilOnline(not_null<PeerData*> peer) {\n\tif (const auto user = peer->asUser()) {\n\t\treturn !user->isSelf()\n\t\t\t&& !user->isBot()\n\t\t\t&& !user->lastseen().isHidden()\n\t\t\t&& !user->starsPerMessageChecked()\n\t\t\t&& !user->isNotificationsUser();\n\t}\n\treturn false;\n}\n\nvoid ScheduleBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tstd::shared_ptr<ChatHelpers::Show> maybeShow,\n\t\tconst Api::SendOptions &initialOptions,\n\t\tconst SendMenu::Details &details,\n\t\tFn<void(Api::SendOptions)> done,\n\t\tTimeId time,\n\t\tScheduleBoxStyleArgs style) {\n\tconst auto repeat = std::make_shared<TimeId>(\n\t\tinitialOptions.scheduleRepeatPeriod);\n\tconst auto submit = [=](Api::SendOptions options) {\n\t\tif (!options.scheduled) {\n\t\t\treturn;\n\t\t}\n\t\t// Pro tip: Hold Ctrl key to send a silent scheduled message!\n\t\tif (base::IsCtrlPressed()) {\n\t\t\toptions.silent = true;\n\t\t}\n\t\tif (repeat) {\n\t\t\toptions.scheduleRepeatPeriod = *repeat;\n\t\t}\n\t\tconst auto copy = done;\n\t\tbox->closeBox();\n\t\tcopy(options);\n\t};\n\tconst auto with = [=](TimeId scheduled) {\n\t\tauto result = initialOptions;\n\t\tresult.scheduled = scheduled;\n\t\treturn result;\n\t};\n\tauto descriptor = Ui::ChooseDateTimeBox(box, {\n\t\t.title = (details.type == SendMenu::Type::Reminder\n\t\t\t? tr::lng_remind_title()\n\t\t\t: tr::lng_schedule_title()),\n\t\t.submit = tr::lng_schedule_button(),\n\t\t.done = [=](TimeId result) { submit(with(result)); },\n\t\t.time = time,\n\t\t.style = style.chooseDateTimeArgs,\n\t});\n\n\tif (repeat) {\n\t\tconst auto boxShow = box->uiShow();\n\t\tconst auto showPremiumPromo = [=] {\n\t\t\tif (session->premium()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tSettings::ShowPremiumPromoToast(\n\t\t\t\tMain::MakeSessionShow(boxShow, session),\n\t\t\t\tChatHelpers::ResolveWindowDefault(),\n\t\t\t\ttr::lng_schedule_repeat_promo(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_link,\n\t\t\t\t\ttr::link(\n\t\t\t\t\t\ttr::bold(\n\t\t\t\t\t\t\ttr::lng_schedule_repeat_promo_link(tr::now))),\n\t\t\t\t\ttr::rich),\n\t\t\t\tu\"schedule_repeat\"_q);\n\t\t\treturn true;\n\t\t};\n\t\tauto locked = Data::AmPremiumValue(\n\t\t\tsession\n\t\t) | rpl::map([=](bool premium) {\n\t\t\treturn !premium;\n\t\t});\n\t\tconst auto row = box->addRow(Ui::ChooseRepeatPeriod(box, {\n\t\t\t.value = session->premium() ? *repeat : TimeId(),\n\t\t\t.locked = std::move(locked),\n\t\t\t.filter = showPremiumPromo,\n\t\t\t.changed = [=](TimeId value) { *repeat = value; },\n\t\t\t.test = session->isTestMode(),\n\t\t}), style::al_top);\n\t\tstd::move(descriptor.width) | rpl::on_next([=](int width) {\n\t\t\trow->setNaturalWidth(width);\n\t\t}, row->lifetime());\n\t}\n\n\tusing namespace SendMenu;\n\tconst auto childType = (details.type == Type::Disabled)\n\t\t? Type::Disabled\n\t\t: Type::SilentOnly;\n\tconst auto childDetails = Details{\n\t\t.type = childType,\n\t\t.effectAllowed = details.effectAllowed,\n\t};\n\tconst auto sendAction = crl::guard(box, [=](Action action, Details) {\n\t\tExpects(action.type == ActionType::Send);\n\n\t\tauto options = with(descriptor.collect());\n\t\tif (action.options.silent) {\n\t\t\toptions.silent = action.options.silent;\n\t\t}\n\t\tif (action.options.effectId) {\n\t\t\toptions.effectId = action.options.effectId;\n\t\t}\n\t\tsubmit(options);\n\t});\n\tSetupMenuAndShortcuts(\n\t\tdescriptor.submit.data(),\n\t\tmaybeShow,\n\t\t[=] { return childDetails; },\n\t\tsendAction);\n\n\tif (details.type == Type::ScheduledToUser) {\n\t\tconst auto sendUntilOnline = box->addTopButton(*style.topButtonStyle);\n\t\tconst auto timestamp = Api::kScheduledUntilOnlineTimestamp;\n\t\tFillSendUntilOnlineMenu(\n\t\t\tsendUntilOnline.data(),\n\t\t\t[=] { submit(with(timestamp)); },\n\t\t\tstyle);\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_schedule_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_common.h\"\n#include \"ui/boxes/choose_date_time.h\"\n\nnamespace style {\nstruct IconButton;\nstruct PopupMenu;\n} // namespace style\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nnamespace HistoryView::details {\n\n[[nodiscard]] not_null<Main::Session*> SessionFromShow(\n\tconst std::shared_ptr<ChatHelpers::Show> &show);\n\n} // namespace HistoryView::details\n\nnamespace HistoryView {\n\n[[nodiscard]] TimeId DefaultScheduleTime();\n[[nodiscard]] bool CanScheduleUntilOnline(not_null<PeerData*> peer);\n\nstruct ScheduleBoxStyleArgs {\n\tScheduleBoxStyleArgs();\n\tconst style::IconButton *topButtonStyle;\n\tconst style::PopupMenu *popupMenuStyle;\n\tUi::ChooseDateTimeStyleArgs chooseDateTimeArgs;\n};\n\nvoid ScheduleBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session,\n\tstd::shared_ptr<ChatHelpers::Show> maybeShow,\n\tconst Api::SendOptions &initialOptions,\n\tconst SendMenu::Details &details,\n\tFn<void(Api::SendOptions)> done,\n\tTimeId time,\n\tScheduleBoxStyleArgs style);\n\ntemplate <typename Guard, typename Submit>\n[[nodiscard]] object_ptr<Ui::GenericBox> PrepareScheduleBox(\n\t\tGuard &&guard,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst SendMenu::Details &details,\n\t\tSubmit &&submit,\n\t\tconst Api::SendOptions &initialOptions = {},\n\t\tTimeId scheduleTime = DefaultScheduleTime(),\n\t\tScheduleBoxStyleArgs style = ScheduleBoxStyleArgs()) {\n\tconst auto session = details::SessionFromShow(show);\n\treturn Box(\n\t\tScheduleBox,\n\t\tsession,\n\t\tstd::move(show),\n\t\tinitialOptions,\n\t\tdetails,\n\t\tcrl::guard(std::forward<Guard>(guard), std::forward<Submit>(submit)),\n\t\tscheduleTime,\n\t\tstd::move(style));\n}\n\ntemplate <typename Guard, typename Submit>\n[[nodiscard]] object_ptr<Ui::GenericBox> PrepareScheduleBox(\n\t\tGuard &&guard,\n\t\tnot_null<Main::Session*> session,\n\t\tconst SendMenu::Details &details,\n\t\tSubmit &&submit,\n\t\tconst Api::SendOptions &initialOptions = {},\n\t\tTimeId scheduleTime = DefaultScheduleTime(),\n\t\tScheduleBoxStyleArgs style = ScheduleBoxStyleArgs()) {\n\treturn Box(\n\t\tScheduleBox,\n\t\tsession,\n\t\tnullptr,\n\t\tinitialOptions,\n\t\tdetails,\n\t\tcrl::guard(std::forward<Guard>(guard), std::forward<Submit>(submit)),\n\t\tscheduleTime,\n\t\tstd::move(style));\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_scheduled_section.h\"\n\n#include \"history/view/controls/history_view_compose_controls.h\"\n#include \"history/view/history_view_empty_list_bubble.h\"\n#include \"history/view/history_view_top_bar_widget.h\"\n#include \"history/view/history_view_schedule_box.h\"\n#include \"history/view/history_view_sticker_toast.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"history/history.h\"\n#include \"history/history_drag_area.h\"\n#include \"history/history_item_helpers.h\" // GetErrorForSending.\n#include \"history/history_view_swipe_back_session.h\"\n#include \"menu/menu_send.h\" // SendMenu::Type.\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/ui_utility.h\"\n#include \"api/api_editing.h\"\n#include \"api/api_sending.h\"\n#include \"apiwrap.h\"\n#include \"boxes/delete_messages_box.h\"\n#include \"boxes/send_files_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_peer_menu.h\"\n#include \"base/call_delayed.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"core/mime_type.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"main/main_session.h\"\n#include \"mainwindow.h\"\n#include \"data/components/scheduled_messages.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_user.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_premium_limits.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/localimageloader.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_boxes.h\"\n\n#include <QtCore/QMimeData>\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kVideoProcessingInfoDuration = 4 * crl::time(1000);\n\n[[nodiscard]] DocumentData *FindVideoFile(not_null<HistoryItem*> item) {\n\tconst auto fromItem = [](not_null<HistoryItem*> item) {\n\t\tif (const auto media = item->media()) {\n\t\t\tif (const auto document = media->document()) {\n\t\t\t\tif (document->isVideoFile()) {\n\t\t\t\t\treturn document;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn (DocumentData*)nullptr;\n\t};\n\tif (const auto group = item->history()->owner().groups().find(item)) {\n\t\tfor (const auto &entry : group->items) {\n\t\t\tif (const auto result = fromItem(entry)) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t} else if (const auto result = fromItem(item)) {\n\t\treturn result;\n\t}\n\treturn nullptr;\n}\n\n} // namespace\n\nScheduledMemento::ScheduledMemento(\n\tnot_null<History*> history,\n\tMsgId sentToScheduledId)\n: _history(history)\n, _forumTopic(nullptr)\n, _sentToScheduledId(sentToScheduledId) {\n\tconst auto list = _history->session().scheduledMessages().list(_history);\n\tif (sentToScheduledId) {\n\t\t_list.setScrollTopState({\n\t\t\t.item = { .fullId = { _history->peer->id, sentToScheduledId } },\n\t\t});\n\t} else if (!list.ids.empty()) {\n\t\t_list.setScrollTopState({ .item = { .fullId = list.ids.front() } });\n\t}\n}\n\nScheduledMemento::ScheduledMemento(not_null<Data::ForumTopic*> forumTopic)\n: _history(forumTopic->owningHistory())\n, _forumTopic(forumTopic) {\n\tconst auto list = _history->session().scheduledMessages().list(\n\t\t_forumTopic);\n\tif (!list.ids.empty()) {\n\t\t_list.setScrollTopState({ .item = {.fullId = list.ids.front() } });\n\t}\n}\n\nobject_ptr<Window::SectionWidget> ScheduledMemento::createWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tWindow::Column column,\n\tconst QRect &geometry) {\n\tif (column == Window::Column::Third) {\n\t\treturn nullptr;\n\t}\n\tauto result = object_ptr<ScheduledWidget>(\n\t\tparent,\n\t\tcontroller,\n\t\t_history,\n\t\t_forumTopic);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nScheduledWidget::ScheduledWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<History*> history,\n\tconst Data::ForumTopic *forumTopic)\n: Window::SectionWidget(parent, controller, history->peer)\n, WindowListDelegate(controller)\n, _show(controller->uiShow())\n, _history(history)\n, _forumTopic(forumTopic)\n, _scroll(\n\tthis,\n\tcontroller->chatStyle()->value(lifetime(), st::historyScroll),\n\tfalse)\n, _topBar(this, controller)\n, _topBarShadow(this)\n, _composeControls(std::make_unique<ComposeControls>(\n\tthis,\n\tComposeControlsDescriptor{\n\t\t.show = controller->uiShow(),\n\t\t.unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) {\n\t\t\tlistShowPremiumToast(emoji);\n\t\t},\n\t\t.mode = ComposeControls::Mode::Scheduled,\n\t\t.sendMenuDetails = [] { return SendMenu::Details(); },\n\t\t.regularWindow = controller,\n\t\t.stickerOrEmojiChosen = controller->stickerOrEmojiChosen(),\n\t}))\n, _cornerButtons(\n\t_scroll.data(),\n\tcontroller->chatStyle(),\n\tstatic_cast<HistoryView::CornerButtonsDelegate*>(this)) {\n\tcontroller->chatStyle()->paletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_scroll->updateBars();\n\t}, _scroll->lifetime());\n\n\tWindow::ChatThemeValueFromPeer(\n\t\tcontroller,\n\t\thistory->peer\n\t) | rpl::on_next([=](std::shared_ptr<Ui::ChatTheme> &&theme) {\n\t\t_theme = std::move(theme);\n\t\tcontroller->setChatStyleTheme(_theme);\n\t}, lifetime());\n\n\tconst auto state = Dialogs::EntryState{\n\t\t.key = _history,\n\t\t.section = Dialogs::EntryState::Section::Scheduled,\n\t};\n\t_topBar->setActiveChat(state, nullptr);\n\t_composeControls->setCurrentDialogsEntryState(state);\n\tcontroller->setDialogsEntryState(state);\n\n\t_topBar->move(0, 0);\n\t_topBar->resizeToWidth(width());\n\t_topBar->show();\n\n\t_topBar->sendNowSelectionRequest(\n\t) | rpl::on_next([=] {\n\t\tconfirmSendNowSelected();\n\t}, _topBar->lifetime());\n\t_topBar->deleteSelectionRequest(\n\t) | rpl::on_next([=] {\n\t\tconfirmDeleteSelected();\n\t}, _topBar->lifetime());\n\t_topBar->clearSelectionRequest(\n\t) | rpl::on_next([=] {\n\t\tclearSelected();\n\t}, _topBar->lifetime());\n\n\t_topBarShadow->raise();\n\tcontroller->adaptive().value(\n\t) | rpl::on_next([=] {\n\t\tupdateAdaptiveLayout();\n\t}, lifetime());\n\n\t_inner = _scroll->setOwnedWidget(object_ptr<ListWidget>(\n\t\tthis,\n\t\t&controller->session(),\n\t\tstatic_cast<ListDelegate*>(this)));\n\t_scroll->move(0, _topBar->height());\n\t_scroll->show();\n\t_scroll->scrolls(\n\t) | rpl::on_next([=] {\n\t\tonScroll();\n\t}, lifetime());\n\n\t_inner->editMessageRequested(\n\t) | rpl::on_next([=](auto fullId) {\n\t\tif (const auto item = session().data().message(fullId)) {\n\t\t\tconst auto media = item->media();\n\t\t\tif (!media || media->webpage() || media->allowsEditCaption()) {\n\t\t\t\t_composeControls->editMessage(\n\t\t\t\t\tfullId,\n\t\t\t\t\t_inner->getSelectedTextRange(item));\n\t\t\t} else if (media->todolist()) {\n\t\t\t\tWindow::PeerMenuEditTodoList(controller, item);\n\t\t\t}\n\t\t}\n\t}, _inner->lifetime());\n\n\t{\n\t\tauto emptyInfo = base::make_unique_q<EmptyListBubbleWidget>(\n\t\t\t_inner,\n\t\t\tcontroller->chatStyle(),\n\t\t\tst::msgServicePadding);\n\t\tconst auto emptyText = tr::semibold(\n\t\t\ttr::lng_scheduled_messages_empty(tr::now));\n\t\temptyInfo->setText(emptyText);\n\t\t_inner->setEmptyInfoWidget(std::move(emptyInfo));\n\t}\n\tsetupComposeControls();\n\tWindow::SetupSwipeBackSection(this, _scroll, _inner);\n}\n\nScheduledWidget::~ScheduledWidget() = default;\n\nvoid ScheduledWidget::setupComposeControls() {\n\tauto writeRestriction = _forumTopic\n\t\t? [&] {\n\t\t\tauto topicWriteRestrictions = rpl::single(\n\t\t\t) | rpl::then(session().changes().topicUpdates(\n\t\t\t\tData::TopicUpdate::Flag::Closed\n\t\t\t) | rpl::filter([=](const Data::TopicUpdate &update) {\n\t\t\t\treturn (update.topic->history() == _history)\n\t\t\t\t\t&& (update.topic->rootId() == _forumTopic->rootId());\n\t\t\t}) | rpl::to_empty) | rpl::map([=] {\n\t\t\t\treturn (!_forumTopic\n\t\t\t\t\t|| _forumTopic->canToggleClosed()\n\t\t\t\t\t|| !_forumTopic->closed())\n\t\t\t\t\t? Data::SendError()\n\t\t\t\t\t: tr::lng_forum_topic_closed(tr::now);\n\t\t\t});\n\t\t\treturn rpl::combine(\n\t\t\t\tsession().frozenValue(),\n\t\t\t\tsession().changes().peerFlagsValue(\n\t\t\t\t\t_history->peer,\n\t\t\t\t\tData::PeerUpdate::Flag::Rights),\n\t\t\t\tData::CanSendAnythingValue(_history->peer),\n\t\t\t\tstd::move(topicWriteRestrictions)\n\t\t\t) | rpl::map([=](\n\t\t\t\t\tconst Main::FreezeInfo &info,\n\t\t\t\t\tauto,\n\t\t\t\t\tauto,\n\t\t\t\t\tData::SendError topicRestriction) {\n\t\t\t\tif (info) {\n\t\t\t\t\treturn Controls::WriteRestriction{\n\t\t\t\t\t\t.type = Controls::WriteRestrictionType::Frozen,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tconst auto allWithoutPolls = Data::AllSendRestrictions()\n\t\t\t\t\t& ~ChatRestriction::SendPolls;\n\t\t\t\tconst auto canSendAnything = Data::CanSendAnyOf(\n\t\t\t\t\t_forumTopic,\n\t\t\t\t\tallWithoutPolls);\n\t\t\t\tconst auto restriction = Data::RestrictionError(\n\t\t\t\t\t_history->peer,\n\t\t\t\t\tChatRestriction::SendOther);\n\t\t\t\tauto text = !canSendAnything\n\t\t\t\t\t? (restriction\n\t\t\t\t\t\t? restriction\n\t\t\t\t\t\t: topicRestriction\n\t\t\t\t\t\t? std::move(topicRestriction)\n\t\t\t\t\t\t: tr::lng_group_not_accessible(tr::now))\n\t\t\t\t\t: topicRestriction\n\t\t\t\t\t? std::move(topicRestriction)\n\t\t\t\t\t: Data::SendError();\n\t\t\t\treturn text ? Controls::WriteRestriction{\n\t\t\t\t\t.text = std::move(*text),\n\t\t\t\t\t.type = Controls::WriteRestrictionType::Rights,\n\t\t\t\t\t.boostsToLift = text.boostsToLift,\n\t\t\t\t} : Controls::WriteRestriction();\n\t\t\t}) | rpl::type_erased;\n\t\t}()\n\t\t: [&] {\n\t\t\treturn rpl::combine(\n\t\t\t\tsession().frozenValue(),\n\t\t\t\tsession().changes().peerFlagsValue(\n\t\t\t\t\t_history->peer,\n\t\t\t\t\tData::PeerUpdate::Flag::Rights),\n\t\t\t\tData::CanSendAnythingValue(_history->peer)\n\t\t\t) | rpl::map([=](const Main::FreezeInfo &info, auto, auto) {\n\t\t\t\tif (info) {\n\t\t\t\t\treturn Controls::WriteRestriction{\n\t\t\t\t\t\t.type = Controls::WriteRestrictionType::Frozen,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tconst auto allWithoutPolls = Data::AllSendRestrictions()\n\t\t\t\t\t& ~ChatRestriction::SendPolls;\n\t\t\t\tconst auto canSendAnything = Data::CanSendAnyOf(\n\t\t\t\t\t_history->peer,\n\t\t\t\t\tallWithoutPolls,\n\t\t\t\t\tfalse);\n\t\t\t\tconst auto restriction = Data::RestrictionError(\n\t\t\t\t\t_history->peer,\n\t\t\t\t\tChatRestriction::SendOther);\n\t\t\t\tauto text = !canSendAnything\n\t\t\t\t\t? (restriction\n\t\t\t\t\t\t? restriction\n\t\t\t\t\t\t: tr::lng_group_not_accessible(tr::now))\n\t\t\t\t\t: Data::SendError();\n\t\t\t\treturn text ? Controls::WriteRestriction{\n\t\t\t\t\t.text = std::move(*text),\n\t\t\t\t\t.type = Controls::WriteRestrictionType::Rights,\n\t\t\t\t\t.boostsToLift = text.boostsToLift,\n\t\t\t\t} : Controls::WriteRestriction();\n\t\t\t}) | rpl::type_erased;\n\t\t}();\n\t_composeControls->setHistory({\n\t\t.history = _history.get(),\n\t\t.writeRestriction = std::move(writeRestriction),\n\t});\n\n\t_composeControls->height(\n\t) | rpl::on_next([=] {\n\t\tconst auto wasMax = (_scroll->scrollTopMax() == _scroll->scrollTop());\n\t\tupdateControlsGeometry();\n\t\tif (wasMax) {\n\t\t\tlistScrollTo(_scroll->scrollTopMax());\n\t\t}\n\t}, lifetime());\n\n\t_composeControls->cancelRequests(\n\t) | rpl::on_next([=] {\n\t\tlistCancelRequest();\n\t}, lifetime());\n\n\t_composeControls->sendRequests(\n\t) | rpl::on_next([=] {\n\t\tsend();\n\t}, lifetime());\n\n\t_composeControls->sendVoiceRequests(\n\t) | rpl::on_next([=](ComposeControls::VoiceToSend &&data) {\n\t\tsendVoice(std::move(data));\n\t}, lifetime());\n\n\t_composeControls->sendCommandRequests(\n\t) | rpl::on_next([=](const QString &command) {\n\t\tlistSendBotCommand(command, FullMsgId());\n\t}, lifetime());\n\n\tconst auto saveEditMsgRequestId = lifetime().make_state<mtpRequestId>(0);\n\t_composeControls->editRequests(\n\t) | rpl::on_next([=](auto data) {\n\t\tif (const auto item = session().data().message(data.fullId)) {\n\t\t\tif (item->isScheduled()) {\n\t\t\t\tconst auto spoiler = data.spoilered;\n\t\t\t\tauto &options = data.options;\n\t\t\t\toptions.scheduleRepeatPeriod = item->scheduleRepeatPeriod();\n\t\t\t\tedit(item, options, saveEditMsgRequestId, spoiler);\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\t_composeControls->attachRequests(\n\t) | rpl::filter([=] {\n\t\treturn !_choosingAttach;\n\t}) | rpl::on_next([=] {\n\t\t_choosingAttach = true;\n\t\tbase::call_delayed(\n\t\t\tst::historyAttach.ripple.hideDuration,\n\t\t\tthis,\n\t\t\t[=] { _choosingAttach = false; chooseAttach(); });\n\t}, lifetime());\n\n\t_composeControls->setSendAsFileConfirmed(crl::guard(this, [=](\n\t\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\t\tApi::SendOptions options) {\n\t\tsendingFilesConfirmed(std::move(bundle), options);\n\t}));\n\n\t_composeControls->fileChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\tcontroller()->hideLayer(anim::type::normal);\n\t\tconst auto document = data.document;\n\t\tconst auto callback = crl::guard(this, [=](Api::SendOptions options) {\n\t\t\tauto messageToSend = Api::MessageToSend(\n\t\t\t\tprepareSendAction(options));\n\t\t\tmessageToSend.textWithTags = data.caption;\n\t\t\tsendExistingDocument(document, std::move(messageToSend));\n\t\t});\n\t\tcontroller()->show(\n\t\t\tPrepareScheduleBox(this, _show, sendMenuDetails(), callback));\n\t}, lifetime());\n\n\t_composeControls->photoChosen(\n\t) | rpl::on_next([=](ChatHelpers::PhotoChosen chosen) {\n\t\tsendExistingPhoto(chosen.photo);\n\t}, lifetime());\n\n\t_composeControls->inlineResultChosen(\n\t) | rpl::on_next([=](ChatHelpers::InlineChosen chosen) {\n\t\tsendInlineResult(chosen.result, chosen.bot);\n\t}, lifetime());\n\n\t_composeControls->jumpToItemRequests(\n\t) | rpl::on_next([=](FullReplyTo to) {\n\t\tif (const auto item = session().data().message(to.messageId)) {\n\t\t\tif (item->isScheduled() && item->history() == _history) {\n\t\t\t\tshowAtPosition(item->position());\n\t\t\t} else {\n\t\t\t\tconst auto highlight = to.highlight();\n\t\t\t\tJumpToMessageClickHandler(item, {}, highlight)->onClick({});\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\trpl::merge(\n\t\t_composeControls->scrollKeyEvents(),\n\t\t_inner->scrollKeyEvents()\n\t) | rpl::on_next([=](not_null<QKeyEvent*> e) {\n\t\t_scroll->keyPressEvent(e);\n\t}, lifetime());\n\n\t_composeControls->editLastMessageRequests(\n\t) | rpl::on_next([=](not_null<QKeyEvent*> e) {\n\t\tif (!_inner->lastMessageEditRequestNotify()) {\n\t\t\t_scroll->keyPressEvent(e);\n\t\t}\n\t}, lifetime());\n\n\t_composeControls->setMimeDataHook([=](\n\t\tnot_null<const QMimeData*> data,\n\t\tUi::InputField::MimeAction action) {\n\t\tif (action == Ui::InputField::MimeAction::Check) {\n\t\t\treturn Core::CanSendFiles(data);\n\t\t} else if (action == Ui::InputField::MimeAction::Insert) {\n\t\t\treturn confirmSendingFiles(\n\t\t\t\tdata,\n\t\t\t\tstd::nullopt,\n\t\t\t\tCore::ReadMimeText(data));\n\t\t}\n\t\tUnexpected(\"action in MimeData hook.\");\n\t});\n\n\t_composeControls->lockShowStarts(\n\t) | rpl::on_next([=] {\n\t\t_cornerButtons.updateJumpDownVisibility();\n\t\t_cornerButtons.updateUnreadThingsVisibility();\n\t}, lifetime());\n\n\t_composeControls->viewportEvents(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\t_scroll->viewportEvent(e);\n\t}, lifetime());\n}\n\nvoid ScheduledWidget::chooseAttach() {\n\tif (const auto error = Data::AnyFileRestrictionError(_history->peer)) {\n\t\tData::ShowSendErrorToast(controller(), _history->peer, error);\n\t\treturn;\n\t}\n\n\tconst auto filter = FileDialog::AllOrImagesFilter();\n\tFileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=](\n\t\tFileDialog::OpenResult &&result) {\n\t\tif (result.paths.isEmpty() && result.remoteContent.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!result.remoteContent.isEmpty()) {\n\t\t\tauto read = Images::Read({\n\t\t\t\t.content = result.remoteContent,\n\t\t\t\t});\n\t\t\tif (!read.image.isNull() && !read.animated) {\n\t\t\t\tconfirmSendingFiles(\n\t\t\t\t\tstd::move(read.image),\n\t\t\t\t\tstd::move(result.remoteContent));\n\t\t\t} else {\n\t\t\t\tuploadFile(result.remoteContent, SendMediaType::File);\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto premium = controller()->session().user()->isPremium();\n\t\t\tauto list = Storage::PrepareMediaList(\n\t\t\t\tresult.paths,\n\t\t\t\tst::sendMediaPreviewSize,\n\t\t\t\tpremium);\n\t\t\tconfirmSendingFiles(std::move(list));\n\t\t}\n\t}), nullptr);\n}\n\nbool ScheduledWidget::confirmSendingFiles(\n\tnot_null<const QMimeData*> data,\n\tstd::optional<bool> overrideSendImagesAsPhotos,\n\tconst QString &insertTextOnCancel) {\n\tconst auto hasImage = data->hasImage();\n\tconst auto premium = controller()->session().user()->isPremium();\n\n\tif (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) {\n\t\tauto list = Storage::PrepareMediaList(\n\t\t\turls,\n\t\t\tst::sendMediaPreviewSize,\n\t\t\tpremium);\n\t\tif (list.error != Ui::PreparedList::Error::NonLocalUrl) {\n\t\t\tif (list.error == Ui::PreparedList::Error::None\n\t\t\t\t|| !hasImage) {\n\t\t\t\tconst auto emptyTextOnCancel = QString();\n\t\t\t\tlist.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;\n\t\t\t\tconfirmSendingFiles(std::move(list), emptyTextOnCancel);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (auto read = Core::ReadMimeImage(data)) {\n\t\tconfirmSendingFiles(\n\t\t\tstd::move(read.image),\n\t\t\tstd::move(read.content),\n\t\t\toverrideSendImagesAsPhotos,\n\t\t\tinsertTextOnCancel);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool ScheduledWidget::confirmSendingFiles(\n\tUi::PreparedList &&list,\n\tconst QString &insertTextOnCancel) {\n\tif (_composeControls->confirmMediaEdit(list)) {\n\t\treturn true;\n\t} else if (showSendingFilesError(list)) {\n\t\treturn false;\n\t}\n\n\tauto box = Box<SendFilesBox>(\n\t\tcontroller(),\n\t\tstd::move(list),\n\t\t_composeControls->getTextWithAppliedMarkdown(),\n\t\t_history->peer,\n\t\t(CanScheduleUntilOnline(_history->peer)\n\t\t\t? Api::SendType::ScheduledToUser\n\t\t\t: Api::SendType::Scheduled),\n\t\tSendMenu::Details());\n\n\tbox->setConfirmedCallback(crl::guard(this, [=](\n\t\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\t\tApi::SendOptions options) {\n\t\tsendingFilesConfirmed(std::move(bundle), options);\n\t}));\n\tbox->setCancelledCallback(_composeControls->restoreTextCallback(\n\t\tinsertTextOnCancel));\n\tbox->takeTextWithTagsRequests() | rpl::on_next([=](TextWithTags &&text) {\n\t\t_composeControls->setText(std::move(text));\n\t}, box->lifetime());\n\n\t//ActivateWindow(controller());\n\tcontroller()->show(std::move(box));\n\n\treturn true;\n}\n\nvoid ScheduledWidget::sendingFilesConfirmed(\n\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\tApi::SendOptions options) {\n\tif (showSendingFilesError(*bundle)) {\n\t\treturn;\n\t}\n\tconst auto compress = bundle->way.sendImagesAsPhotos();\n\tconst auto type = compress ? SendMediaType::Photo : SendMediaType::File;\n\tauto action = prepareSendAction(options);\n\taction.clearDraft = false;\n\tauto &api = session().api();\n\tfor (auto &group : bundle->groups) {\n\t\tconst auto album = (group.type != Ui::AlbumType::None)\n\t\t\t? std::make_shared<SendingAlbum>()\n\t\t\t: nullptr;\n\t\tapi.sendFiles(std::move(group.list), type, album, action);\n\t}\n}\n\nbool ScheduledWidget::confirmSendingFiles(\n\t\tQImage &&image,\n\t\tQByteArray &&content,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos,\n\t\tconst QString &insertTextOnCancel) {\n\tif (image.isNull()) {\n\t\treturn false;\n\t}\n\n\tauto list = Storage::PrepareMediaFromImage(\n\t\tstd::move(image),\n\t\tstd::move(content),\n\t\tst::sendMediaPreviewSize);\n\tlist.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;\n\treturn confirmSendingFiles(std::move(list), insertTextOnCancel);\n}\n\nvoid ScheduledWidget::pushReplyReturn(not_null<HistoryItem*> item) {\n\tif (_inner->viewByPosition(item->position())) {\n\t\t_cornerButtons.pushReplyReturn(item);\n\t}\n}\n\nvoid ScheduledWidget::checkReplyReturns() {\n\tconst auto currentTop = _scroll->scrollTop();\n\twhile (const auto replyReturn = _cornerButtons.replyReturn()) {\n\t\tconst auto position = replyReturn->position();\n\t\tconst auto scrollTop = _inner->scrollTopForPosition(position);\n\t\tconst auto below = scrollTop\n\t\t\t? (currentTop >= std::min(*scrollTop, _scroll->scrollTopMax()))\n\t\t\t: _inner->isBelowPosition(position);\n\t\tif (below) {\n\t\t\t_cornerButtons.calculateNextReplyReturn();\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nvoid ScheduledWidget::uploadFile(\n\t\tconst QByteArray &fileContent,\n\t\tSendMediaType type) {\n\tconst auto callback = [=](Api::SendOptions options) {\n\t\tsession().api().sendFile(\n\t\t\tfileContent,\n\t\t\ttype,\n\t\t\tprepareSendAction(options));\n\t};\n\tcontroller()->show(\n\t\tPrepareScheduleBox(this, _show, sendMenuDetails(), callback));\n}\n\nbool ScheduledWidget::showSendingFilesError(\n\t\tconst Ui::PreparedList &list) const {\n\tconst auto peer = _history->peer;\n\tconst auto show = controller()->uiShow();\n\treturn Data::ShowSendError(show, peer, list, std::nullopt, true);\n}\n\nbool ScheduledWidget::showSendingFilesError(\n\t\tconst Ui::PreparedBundle &bundle) const {\n\tconst auto show = controller()->uiShow();\n\treturn Data::ShowSendError(show, _history->peer, bundle, true);\n}\n\nApi::SendAction ScheduledWidget::prepareSendAction(\n\t\tApi::SendOptions options) const {\n\tauto result = Api::SendAction(_history, options);\n\tresult.options.sendAs = _composeControls->sendAsPeer();\n\tif (_forumTopic) {\n\t\tresult.replyTo.topicRootId = _forumTopic->topicRootId();\n\t\tresult.replyTo.messageId = FullMsgId(\n\t\t\thistory()->peer->id,\n\t\t\t_forumTopic->topicRootId());\n\t}\n\treturn result;\n}\n\nvoid ScheduledWidget::send() {\n\tconst auto textWithTags = _composeControls->getTextWithAppliedMarkdown();\n\tif (textWithTags.text.isEmpty() && !_composeControls->readyToForward()) {\n\t\treturn;\n\t}\n\n\tconst auto error = GetErrorForSending(\n\t\t_history->peer,\n\t\t{\n\t\t\t.topicRootId = _forumTopic\n\t\t\t\t? _forumTopic->topicRootId()\n\t\t\t\t: history()->isForum()\n\t\t\t\t? MsgId(1)\n\t\t\t\t: MsgId(),\n\t\t\t.forward = nullptr,\n\t\t\t.text = &textWithTags,\n\t\t\t.ignoreSlowmodeCountdown = true,\n\t\t});\n\tif (error) {\n\t\tData::ShowSendErrorToast(controller(), _history->peer, error);\n\t\treturn;\n\t}\n\tconst auto callback = [=](Api::SendOptions options) { send(options); };\n\tcontroller()->show(\n\t\tPrepareScheduleBox(this, _show, sendMenuDetails(), callback));\n}\n\nvoid ScheduledWidget::send(Api::SendOptions options) {\n\tconst auto webPageDraft = _composeControls->webPageDraft();\n\n\tauto message = Api::MessageToSend(prepareSendAction(options));\n\tmessage.textWithTags = _composeControls->getTextWithAppliedMarkdown();\n\tmessage.webPage = webPageDraft;\n\n\tsession().api().sendMessage(std::move(message));\n\n\t_composeControls->cancelForward();\n\t_composeControls->clear();\n\t//_saveDraftText = true;\n\t//_saveDraftStart = crl::now();\n\t//onDraftSave();\n\n\t_composeControls->hidePanelsAnimated();\n\n\t//if (_previewData && _previewData->pendingTill) previewCancel();\n\t_composeControls->focus();\n}\n\nvoid ScheduledWidget::sendVoice(const Controls::VoiceToSend &data) {\n\tconst auto callback = [=](Api::SendOptions options) {\n\t\tsendVoice(base::duplicate(data), options);\n\t};\n\tcontroller()->show(\n\t\tPrepareScheduleBox(this, _show, sendMenuDetails(), callback));\n}\n\nvoid ScheduledWidget::sendVoice(\n\t\tconst Controls::VoiceToSend &data,\n\t\tApi::SendOptions options) {\n\tsession().api().sendVoiceMessage(\n\t\tdata.bytes,\n\t\tdata.waveform,\n\t\tdata.duration,\n\t\tdata.video,\n\t\tprepareSendAction(options));\n\t_composeControls->clearListenState();\n}\n\nvoid ScheduledWidget::edit(\n\t\tnot_null<HistoryItem*> item,\n\t\tApi::SendOptions options,\n\t\tmtpRequestId *const saveEditMsgRequestId,\n\t\tbool spoilered) {\n\tif (*saveEditMsgRequestId) {\n\t\treturn;\n\t}\n\tconst auto webpage = _composeControls->webPageDraft();\n\tconst auto sending = _composeControls->prepareTextForEditMsg();\n\n\tconst auto hasMediaWithCaption = item\n\t\t&& item->media()\n\t\t&& item->media()->allowsEditCaption();\n\tif (sending.text.isEmpty() && !hasMediaWithCaption) {\n\t\tif (item) {\n\t\t\tcontroller()->show(Box<DeleteMessagesBox>(item, false));\n\t\t} else {\n\t\t\t_composeControls->focus();\n\t\t}\n\t\treturn;\n\t} else {\n\t\tconst auto maxCaptionSize = !hasMediaWithCaption\n\t\t\t? MaxMessageSize\n\t\t\t: Data::PremiumLimits(&session()).captionLengthCurrent();\n\t\tconst auto remove = _composeControls->fieldCharacterCount()\n\t\t\t- maxCaptionSize;\n\t\tif (remove > 0) {\n\t\t\tcontroller()->showToast(\n\t\t\t\ttr::lng_edit_limit_reached(tr::now, lt_count, remove));\n\t\t\treturn;\n\t\t}\n\t}\n\n\tlifetime().add([=] {\n\t\tif (!*saveEditMsgRequestId) {\n\t\t\treturn;\n\t\t}\n\t\tsession().api().request(base::take(*saveEditMsgRequestId)).cancel();\n\t});\n\n\tconst auto done = [=](mtpRequestId requestId) {\n\t\tif (requestId == *saveEditMsgRequestId) {\n\t\t\t*saveEditMsgRequestId = 0;\n\t\t\t_composeControls->cancelEditMessage();\n\t\t}\n\t};\n\n\tconst auto fail = [=](const QString &error, mtpRequestId requestId) {\n\t\tif (requestId == *saveEditMsgRequestId) {\n\t\t\t*saveEditMsgRequestId = 0;\n\t\t}\n\n\t\tif (ranges::contains(Api::kDefaultEditMessagesErrors, error)) {\n\t\t\tcontroller()->showToast(tr::lng_edit_error(tr::now));\n\t\t} else if (error == u\"MESSAGE_NOT_MODIFIED\"_q) {\n\t\t\t_composeControls->cancelEditMessage();\n\t\t} else if (error == u\"MESSAGE_EMPTY\"_q) {\n\t\t\t_composeControls->focus();\n\t\t} else {\n\t\t\tcontroller()->showToast(tr::lng_edit_error(tr::now));\n\t\t}\n\t\tupdate();\n\t\treturn true;\n\t};\n\n\t*saveEditMsgRequestId = Api::EditTextMessage(\n\t\titem,\n\t\tsending,\n\t\twebpage,\n\t\toptions,\n\t\tcrl::guard(this, done),\n\t\tcrl::guard(this, fail),\n\t\tspoilered);\n\n\t_composeControls->hidePanelsAnimated();\n\t_composeControls->focus();\n}\n\nbool ScheduledWidget::sendExistingDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tApi::MessageToSend messageToSend) {\n\tconst auto error = Data::RestrictionError(\n\t\t_history->peer,\n\t\tChatRestriction::SendStickers);\n\tif (error) {\n\t\tData::ShowSendErrorToast(controller(), _history->peer, error);\n\t\treturn false;\n\t} else if (ShowSendPremiumError(controller(), document)) {\n\t\treturn false;\n\t}\n\n\tApi::SendExistingDocument(std::move(messageToSend), document);\n\n\t_composeControls->hidePanelsAnimated();\n\t_composeControls->focus();\n\treturn true;\n}\n\nvoid ScheduledWidget::sendExistingPhoto(not_null<PhotoData*> photo) {\n\tconst auto callback = [=](Api::SendOptions options) {\n\t\tsendExistingPhoto(photo, options);\n\t};\n\tcontroller()->show(\n\t\tPrepareScheduleBox(this, _show, sendMenuDetails(), callback));\n}\n\nbool ScheduledWidget::sendExistingPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tApi::SendOptions options) {\n\tconst auto error = Data::RestrictionError(\n\t\t_history->peer,\n\t\tChatRestriction::SendPhotos);\n\tif (error) {\n\t\tData::ShowSendErrorToast(controller(), _history->peer, error);\n\t\treturn false;\n\t}\n\n\tApi::SendExistingPhoto(\n\t\tApi::MessageToSend(prepareSendAction(options)),\n\t\tphoto);\n\n\t_composeControls->hidePanelsAnimated();\n\t_composeControls->focus();\n\treturn true;\n}\n\nvoid ScheduledWidget::sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot) {\n\tif (const auto error = result->getErrorOnSend(_history)) {\n\t\tData::ShowSendErrorToast(controller(), _history->peer, error);\n\t\treturn;\n\t}\n\tconst auto callback = [=](Api::SendOptions options) {\n\t\tsendInlineResult(result, bot, options);\n\t};\n\tcontroller()->show(\n\t\tPrepareScheduleBox(this, _show, sendMenuDetails(), callback));\n}\n\nvoid ScheduledWidget::sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot,\n\t\tApi::SendOptions options) {\n\tauto action = prepareSendAction(options);\n\taction.generateLocal = true;\n\tsession().api().sendInlineResult(\n\t\tbot,\n\t\tresult.get(),\n\t\taction,\n\t\tstd::nullopt);\n\n\t_composeControls->clear();\n\t//_saveDraftText = true;\n\t//_saveDraftStart = crl::now();\n\t//onDraftSave();\n\n\tauto &bots = cRefRecentInlineBots();\n\tconst auto index = bots.indexOf(bot);\n\tif (index) {\n\t\tif (index > 0) {\n\t\t\tbots.removeAt(index);\n\t\t} else if (bots.size() >= RecentInlineBotsLimit) {\n\t\t\tbots.resize(RecentInlineBotsLimit - 1);\n\t\t}\n\t\tbots.push_front(bot);\n\t\tbot->session().local().writeRecentHashtagsAndBots();\n\t}\n\n\t_composeControls->hidePanelsAnimated();\n\t_composeControls->focus();\n}\n\nSendMenu::Details ScheduledWidget::sendMenuDetails() const {\n\tconst auto type = _history->peer->isSelf()\n\t\t? SendMenu::Type::Reminder\n\t\t: HistoryView::CanScheduleUntilOnline(_history->peer)\n\t\t? SendMenu::Type::ScheduledToUser\n\t\t: SendMenu::Type::Scheduled;\n\tconst auto effectAllowed = _history->peer->isUser();\n\treturn { .type = type, .effectAllowed = effectAllowed };\n}\n\nvoid ScheduledWidget::cornerButtonsShowAtPosition(\n\t\tData::MessagePosition position) {\n\tshowAtPosition(position);\n}\n\nData::Thread *ScheduledWidget::cornerButtonsThread() {\n\treturn _history;\n}\n\nFullMsgId ScheduledWidget::cornerButtonsCurrentId() {\n\treturn {};\n}\n\nbool ScheduledWidget::cornerButtonsIgnoreVisibility() {\n\treturn animatingShow();\n}\n\nstd::optional<bool> ScheduledWidget::cornerButtonsDownShown() {\n\tif (_composeControls->isLockPresent()\n\t\t|| _composeControls->isTTLButtonShown()) {\n\t\treturn false;\n\t}\n\tconst auto top = _scroll->scrollTop() + st::historyToDownShownAfter;\n\tif (top < _scroll->scrollTopMax() || _cornerButtons.replyReturn()) {\n\t\treturn true;\n\t} else if (_inner->loadedAtBottomKnown()) {\n\t\treturn !_inner->loadedAtBottom();\n\t}\n\treturn std::nullopt;\n}\n\nbool ScheduledWidget::cornerButtonsUnreadMayBeShown() {\n\treturn _inner->loadedAtBottomKnown()\n\t\t&& !_composeControls->isLockPresent()\n\t\t&& !_composeControls->isTTLButtonShown();\n}\n\nbool ScheduledWidget::cornerButtonsHas(CornerButtonType type) {\n\treturn (type == CornerButtonType::Down);\n}\n\nvoid ScheduledWidget::showAtPosition(\n\t\tData::MessagePosition position,\n\t\tFullMsgId originId) {\n\t_inner->showAtPosition(\n\t\tposition,\n\t\t{},\n\t\t_cornerButtons.doneJumpFrom(position.fullId, originId));\n}\n\nvoid ScheduledWidget::updateAdaptiveLayout() {\n\t_topBarShadow->moveToLeft(\n\t\tcontroller()->adaptive().isOneColumn() ? 0 : st::lineWidth,\n\t\t_topBar->height());\n}\n\nnot_null<History*> ScheduledWidget::history() const {\n\treturn _history;\n}\n\nDialogs::RowDescriptor ScheduledWidget::activeChat() const {\n\treturn {\n\t\t_history,\n\t\tFullMsgId(_history->peer->id, ShowAtUnreadMsgId)\n\t};\n}\n\nbool ScheduledWidget::preventsClose(Fn<void()> &&continueCallback) const {\n\treturn _composeControls->preventsClose(std::move(continueCallback));\n}\n\nQPixmap ScheduledWidget::grabForShowAnimation(const Window::SectionSlideParams &params) {\n\t_topBar->updateControlsVisibility();\n\tif (params.withTopBarShadow) _topBarShadow->hide();\n\t_composeControls->showForGrab();\n\tauto result = Ui::GrabWidget(this);\n\tif (params.withTopBarShadow) _topBarShadow->show();\n\treturn result;\n}\n\nvoid ScheduledWidget::checkActivation() {\n\t_inner->checkActivation();\n}\n\nvoid ScheduledWidget::doSetInnerFocus() {\n\t_composeControls->focus();\n}\n\nbool ScheduledWidget::showInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) {\n\tif (auto logMemento = dynamic_cast<ScheduledMemento*>(memento.get())) {\n\t\tif (logMemento->getHistory() == history()) {\n\t\t\trestoreState(logMemento);\n\t\t\tif (params.reapplyLocalDraft) {\n\t\t\t\t_composeControls->applyDraft(\n\t\t\t\t\tComposeControls::FieldHistoryAction::NewEntry);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid ScheduledWidget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<ScheduledMemento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nbool ScheduledWidget::pushTabbedSelectorToThirdSection(\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst Window::SectionShow &params) {\n\treturn _composeControls->pushTabbedSelectorToThirdSection(\n\t\tthread,\n\t\tparams);\n}\n\nbool ScheduledWidget::returnTabbedSelector() {\n\treturn _composeControls->returnTabbedSelector();\n}\n\nstd::shared_ptr<Window::SectionMemento> ScheduledWidget::createMemento() {\n\tif (_forumTopic) {\n\t\tif (const auto forum = history()->asForum()) {\n\t\t\tconst auto rootId = _forumTopic->topicRootId();\n\t\t\tif (const auto topic = forum->topicFor(rootId)) {\n\t\t\t\tauto result = std::make_shared<ScheduledMemento>(topic);\n\t\t\t\tsaveState(result.get());\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\tauto result = std::make_shared<ScheduledMemento>(history());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid ScheduledWidget::saveState(not_null<ScheduledMemento*> memento) {\n\t_inner->saveState(memento->list());\n}\n\nvoid ScheduledWidget::restoreState(not_null<ScheduledMemento*> memento) {\n\t_inner->restoreState(memento->list());\n\tif (const auto id = memento->sentToScheduledId()) {\n\t\tconst auto item = _history->owner().message(_history->peer, id);\n\t\tif (item) {\n\t\t\tcontroller()->showToast({\n\t\t\t\t.title = tr::lng_scheduled_video_tip_title(tr::now),\n\t\t\t\t.text = { tr::lng_scheduled_video_tip_text(tr::now) },\n\t\t\t\t.attach = RectPart::Top,\n\t\t\t\t.duration = kVideoProcessingInfoDuration,\n\t\t\t});\n\t\t\tclearProcessingVideoTracking(false);\n\t\t\t_processingVideoPosition = item->position();\n\t\t\t_processingVideoTipTimer.setCallback([=] {\n\t\t\t\t_processingVideoCanShow = true;\n\t\t\t\tupdateInnerVisibleArea();\n\t\t\t});\n\t\t\t_processingVideoTipTimer.callOnce(kVideoProcessingInfoDuration);\n\t\t}\n\t}\n}\n\nvoid ScheduledWidget::resizeEvent(QResizeEvent *e) {\n\tif (!width() || !height()) {\n\t\treturn;\n\t}\n\t_composeControls->resizeToWidth(width());\n\tupdateControlsGeometry();\n}\n\nvoid ScheduledWidget::updateControlsGeometry() {\n\tconst auto contentWidth = width();\n\n\tconst auto newScrollTop = _scroll->isHidden()\n\t\t? std::nullopt\n\t\t: base::make_optional(_scroll->scrollTop() + topDelta());\n\t_topBar->resizeToWidth(contentWidth);\n\t_topBarShadow->resize(contentWidth, st::lineWidth);\n\n\tconst auto bottom = height();\n\tconst auto controlsHeight = _composeControls->heightCurrent();\n\tconst auto scrollHeight = bottom - _topBar->height() - controlsHeight;\n\tconst auto scrollSize = QSize(contentWidth, scrollHeight);\n\tif (_scroll->size() != scrollSize) {\n\t\t_skipScrollEvent = true;\n\t\t_scroll->resize(scrollSize);\n\t\t_inner->resizeToWidth(scrollSize.width(), _scroll->height());\n\t\t_skipScrollEvent = false;\n\t}\n\tif (!_scroll->isHidden()) {\n\t\tif (newScrollTop) {\n\t\t\t_scroll->scrollToY(*newScrollTop);\n\t\t}\n\t\tupdateInnerVisibleArea();\n\t}\n\t_composeControls->move(0, bottom - controlsHeight);\n\t_composeControls->setAutocompleteBoundingRect(_scroll->geometry());\n\n\t_cornerButtons.updatePositions();\n}\n\nvoid ScheduledWidget::paintEvent(QPaintEvent *e) {\n\tif (animatingShow()) {\n\t\tSectionWidget::paintEvent(e);\n\t\treturn;\n\t} else if (controller()->contentOverlapped(this, e)) {\n\t\treturn;\n\t}\n\t//if (hasPendingResizedItems()) {\n\t//\tupdateListSize();\n\t//}\n\n\t//auto ms = crl::now();\n\t//_historyDownShown.step(ms);\n\n\tconst auto clip = e->rect();\n\tSectionWidget::PaintBackground(controller(), _theme.get(), this, clip);\n}\n\nvoid ScheduledWidget::onScroll() {\n\tif (_skipScrollEvent) {\n\t\treturn;\n\t}\n\tupdateInnerVisibleArea();\n}\n\nvoid ScheduledWidget::updateInnerVisibleArea() {\n\tif (!_inner->animatedScrolling()) {\n\t\tcheckReplyReturns();\n\t}\n\tconst auto scrollTop = _scroll->scrollTop();\n\tconst auto scrollBottom = scrollTop + _scroll->height();\n\t_inner->setVisibleTopBottom(scrollTop, scrollBottom);\n\t_cornerButtons.updateJumpDownVisibility();\n\t_cornerButtons.updateUnreadThingsVisibility();\n\tif (!_processingVideoLifetime) {\n\t\tif (const auto &position = _processingVideoPosition) {\n\t\t\tif (const auto view = _inner->viewByPosition(position)) {\n\t\t\t\tinitProcessingVideoView(view);\n\t\t\t}\n\t\t}\n\t}\n\tcheckProcessingVideoTooltip(scrollTop, scrollBottom);\n}\n\nvoid ScheduledWidget::initProcessingVideoView(not_null<Element*> view) {\n\t_processingVideoView = view;\n\n\tcontroller()->session().data().sentFromScheduled(\n\t) | rpl::on_next([=](const Data::SentFromScheduled &value) {\n\t\tif (value.item->position() == _processingVideoPosition) {\n\t\t\tcontroller()->showPeerHistory(\n\t\t\t\tvalue.item->history(),\n\t\t\t\tWindow::SectionShow::Way::Backward,\n\t\t\t\tvalue.sentId);\n\t\t}\n\t}, _processingVideoLifetime);\n\n\tcontroller()->session().data().viewRemoved(\n\t) | rpl::on_next([=](not_null<const Element*> view) {\n\t\tif (view == _processingVideoView.get()) {\n\t\t\tconst auto position = _processingVideoPosition;\n\t\t\tif (const auto now = _inner->viewByPosition(position)) {\n\t\t\t\t_processingVideoView = now;\n\t\t\t\tupdateProcessingVideoTooltipPosition();\n\t\t\t} else {\n\t\t\t\tclearProcessingVideoTracking(true);\n\t\t\t}\n\t\t}\n\t}, _processingVideoLifetime);\n\n\tcontroller()->session().data().viewResizeRequest(\n\t) | rpl::on_next([this](not_null<const Element*> view) {\n\t\tif (view->delegate() == _inner.data()) {\n\t\t\tif (!_processingVideoUpdateScheduled) {\n\t\t\t\tif (const auto tooltip = _processingVideoTooltip.get()) {\n\t\t\t\t\t_processingVideoUpdateScheduled = true;\n\t\t\t\t\tcrl::on_main(tooltip, [=] {\n\t\t\t\t\t\t_processingVideoUpdateScheduled = false;\n\t\t\t\t\t\tupdateProcessingVideoTooltipPosition();\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, _processingVideoLifetime);\n}\n\nvoid ScheduledWidget::clearProcessingVideoTracking(bool fast) {\n\tif (const auto tooltip = _processingVideoTooltip.release()) {\n\t\ttooltip->toggleAnimated(false);\n\t}\n\t_processingVideoPosition = {};\n\tif (const auto tooltip = _processingVideoTooltip.release()) {\n\t\tif (fast) {\n\t\t\ttooltip->toggleFast(false);\n\t\t} else {\n\t\t\ttooltip->toggleAnimated(false);\n\t\t}\n\t}\n\t_processingVideoTooltipShown = false;\n\t_processingVideoCanShow = false;\n\t_processingVideoView = nullptr;\n\t_processingVideoTipTimer.cancel();\n\t_processingVideoLifetime.destroy();\n}\n\nvoid ScheduledWidget::checkProcessingVideoTooltip(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tif (_processingVideoTooltip\n\t\t|| _processingVideoTooltipShown\n\t\t|| !_processingVideoCanShow) {\n\t\treturn;\n\t}\n\tconst auto view = _processingVideoView.get();\n\tif (!view) {\n\t\t_processingVideoCanShow = false;\n\t\treturn;\n\t}\n\tconst auto rect = view->effectIconGeometry();\n\tif (rect.top() > visibleTop\n\t\t&& rect.top() + rect.height() <= visibleBottom) {\n\t\tshowProcessingVideoTooltip();\n\t}\n}\n\nvoid ScheduledWidget::updateProcessingVideoTooltipPosition() {\n\tconst auto tooltip = _processingVideoTooltip.get();\n\tif (!tooltip) {\n\t\treturn;\n\t}\n\tconst auto view = _processingVideoView.get();\n\tif (!view) {\n\t\tclearProcessingVideoTracking(true);\n\t\treturn;\n\t}\n\tconst auto shift = view->skipBlockWidth() / 2;\n\tconst auto rect = view->effectIconGeometry().translated(shift, 0);\n\tconst auto countPosition = [=](QSize size) {\n\t\tconst auto origin = rect.bottomLeft();\n\t\treturn origin - QPoint(\n\t\t\tsize.width() / 2,\n\t\t\tsize.height() + st::processingVideoTipShift);\n\t};\n\ttooltip->pointAt(rect, RectPart::Top, countPosition);\n}\n\nvoid ScheduledWidget::showProcessingVideoTooltip() {\n\t_processingVideoTooltipShown = true;\n\t_processingVideoTooltip = std::make_unique<Ui::ImportantTooltip>(\n\t\t_inner.data(),\n\t\tUi::MakeNiceTooltipLabel(\n\t\t\t_inner.data(),\n\t\t\ttr::lng_scheduled_video_tip(tr::marked),\n\t\t\tst::processingVideoTipMaxWidth,\n\t\t\tst::defaultImportantTooltipLabel),\n\t\tst::defaultImportantTooltip);\n\tconst auto tooltip = _processingVideoTooltip.get();\n\tconst auto weak = base::make_weak(tooltip);\n\tconst auto destroy = [=] {\n\t\tdelete weak.get();\n\t};\n\ttooltip->setAttribute(Qt::WA_TransparentForMouseEvents);\n\ttooltip->setHiddenCallback([=] {\n\t\tconst auto tip = _processingVideoTooltip.get();\n\t\tif (tooltip == tip) {\n\t\t\t_processingVideoTooltip.release();\n\t\t}\n\t\tcrl::on_main(tip, [=] {\n\t\t\tdelete tip;\n\t\t});\n\t});\n\tupdateProcessingVideoTooltipPosition();\n\ttooltip->toggleAnimated(true);\n\t_processingVideoTipTimer.setCallback(crl::guard(tooltip, [=] {\n\t\ttooltip->toggleAnimated(false);\n\t}));\n\t_processingVideoTipTimer.callOnce(kVideoProcessingInfoDuration);\n}\n\nvoid ScheduledWidget::showAnimatedHook(\n\t\tconst Window::SectionSlideParams &params) {\n\t_topBar->setAnimatingMode(true);\n\tif (params.withTopBarShadow) {\n\t\t_topBarShadow->show();\n\t}\n\t_composeControls->showStarted();\n}\n\nvoid ScheduledWidget::showFinishedHook() {\n\t_topBar->setAnimatingMode(false);\n\t_composeControls->showFinished();\n\t_inner->showFinished();\n\n\t// We should setup the drag area only after\n\t// the section animation is finished,\n\t// because after that the method showChildren() is called.\n\tsetupDragArea();\n}\n\nbool ScheduledWidget::floatPlayerHandleWheelEvent(QEvent *e) {\n\treturn _scroll->viewportEvent(e);\n}\n\nQRect ScheduledWidget::floatPlayerAvailableRect() {\n\treturn mapToGlobal(_scroll->geometry());\n}\n\nContext ScheduledWidget::listContext() {\n\treturn _forumTopic ? Context::ScheduledTopic : Context::History;\n}\n\nbool ScheduledWidget::listScrollTo(int top, bool syntetic) {\n\ttop = std::clamp(top, 0, _scroll->scrollTopMax());\n\tif (_scroll->scrollTop() == top) {\n\t\tupdateInnerVisibleArea();\n\t\treturn false;\n\t}\n\t_scroll->scrollToY(top);\n\treturn true;\n}\n\nvoid ScheduledWidget::listCancelRequest() {\n\tif (_inner && !_inner->getSelectedItems().empty()) {\n\t\tclearSelected();\n\t\treturn;\n\t} else if (_composeControls->handleCancelRequest()) {\n\t\treturn;\n\t}\n\tcontroller()->showBackFromStack();\n}\n\nvoid ScheduledWidget::listDeleteRequest() {\n\tconfirmDeleteSelected();\n}\n\nvoid ScheduledWidget::listTryProcessKeyInput(not_null<QKeyEvent*> e) {\n\t_composeControls->tryProcessKeyInput(e);\n}\n\nrpl::producer<Data::MessagesSlice> ScheduledWidget::listSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tconst auto session = &controller()->session();\n\treturn rpl::single(rpl::empty) | rpl::then(\n\t\tsession->scheduledMessages().updates(_history)\n\t) | rpl::map([=] {\n\t\treturn _forumTopic\n\t\t\t? session->scheduledMessages().list(_forumTopic)\n\t\t\t: session->scheduledMessages().list(_history);\n\t}) | rpl::after_next([=](const Data::MessagesSlice &slice) {\n\t\thighlightSingleNewMessage(slice);\n\t});\n}\n\nvoid ScheduledWidget::highlightSingleNewMessage(\n\t\tconst Data::MessagesSlice &slice) {\n\tconst auto guard = gsl::finally([&] { _lastSlice = slice; });\n\tif (_lastSlice.ids.empty()\n\t\t|| (slice.ids.size() != _lastSlice.ids.size() + 1)) {\n\t\treturn;\n\t}\n\tauto firstDifferent = 0;\n\twhile (firstDifferent != _lastSlice.ids.size()) {\n\t\tif (slice.ids[firstDifferent] != _lastSlice.ids[firstDifferent]) {\n\t\t\tbreak;\n\t\t}\n\t\t++firstDifferent;\n\t}\n\tauto lastDifferent = slice.ids.size() - 1;\n\twhile (lastDifferent != firstDifferent) {\n\t\tif (slice.ids[lastDifferent] != _lastSlice.ids[lastDifferent - 1]) {\n\t\t\tbreak;\n\t\t}\n\t\t--lastDifferent;\n\t}\n\tif (firstDifferent != lastDifferent) {\n\t\treturn;\n\t}\n\tconst auto newId = slice.ids[firstDifferent];\n\tif (const auto item = session().data().message(newId)) {\n\t\tshowAtPosition(item->position());\n\t}\n}\n\nbool ScheduledWidget::listAllowsMultiSelect() {\n\treturn true;\n}\n\nbool ScheduledWidget::listIsItemGoodForSelection(\n\t\tnot_null<HistoryItem*> item) {\n\treturn !item->isSending() && !item->hasFailed();\n}\n\nbool ScheduledWidget::listIsLessInOrder(\n\t\tnot_null<HistoryItem*> first,\n\t\tnot_null<HistoryItem*> second) {\n\treturn first->position() < second->position();\n}\n\nvoid ScheduledWidget::listSelectionChanged(SelectedItems &&items) {\n\tHistoryView::TopBarWidget::SelectedState state;\n\tstate.count = items.size();\n\tfor (const auto &item : items) {\n\t\tif (item.canDelete) {\n\t\t\t++state.canDeleteCount;\n\t\t}\n\t\tif (item.canSendNow) {\n\t\t\t++state.canSendNowCount;\n\t\t}\n\t}\n\t_topBar->showSelected(state);\n\tif (items.empty()) {\n\t\tdoSetInnerFocus();\n\t}\n}\n\nvoid ScheduledWidget::listMarkReadTill(not_null<HistoryItem*> item) {\n}\n\nvoid ScheduledWidget::listMarkContentsRead(\n\tconst base::flat_set<not_null<HistoryItem*>> &items) {\n}\n\nMessagesBarData ScheduledWidget::listMessagesBar(\n\t\tconst std::vector<not_null<Element*>> &elements,\n\t\tbool markLastAsRead) {\n\treturn {};\n}\n\nvoid ScheduledWidget::listContentRefreshed() {\n}\n\nvoid ScheduledWidget::listUpdateDateLink(\n\tClickHandlerPtr &link,\n\tnot_null<Element*> view) {\n}\n\nbool ScheduledWidget::listElementHideReply(not_null<const Element*> view) {\n\tif (const auto root = view->data()->topicRootId()) {\n\t\treturn root == view->data()->replyTo().messageId.msg;\n\t}\n\treturn false;\n}\n\nbool ScheduledWidget::listElementShownUnread(not_null<const Element*> view) {\n\treturn true;\n}\n\nbool ScheduledWidget::listIsGoodForAroundPosition(\n\t\tnot_null<const Element*> view) {\n\treturn true;\n}\n\nbool ScheduledWidget::showMessage(\n\t\tPeerId peerId,\n\t\tconst Window::SectionShow &params,\n\t\tMsgId messageId) {\n\tif (peerId != _history->peer->id) {\n\t\treturn false;\n\t}\n\tconst auto id = FullMsgId(_history->peer->id, messageId);\n\tconst auto message = _history->owner().message(id);\n\tif (!message || !_inner->viewByPosition(message->position())) {\n\t\treturn false;\n\t}\n\n\tconst auto originItem = [&]() -> HistoryItem* {\n\t\tusing OriginMessage = Window::SectionShow::OriginMessage;\n\t\tif (const auto origin = std::get_if<OriginMessage>(&params.origin)) {\n\t\t\tif (const auto returnTo = session().data().message(origin->id)) {\n\t\t\t\tif (_inner->viewByPosition(returnTo->position())\n\t\t\t\t\t&& _cornerButtons.replyReturn() != returnTo) {\n\t\t\t\t\treturn returnTo;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t}();\n\tshowAtPosition(\n\t\tmessage->position(),\n\t\toriginItem ? originItem->fullId() : FullMsgId());\n\treturn true;\n}\n\nWindow::SectionActionResult ScheduledWidget::sendBotCommand(\n\t\tBot::SendCommandRequest request) {\n\tif (request.peer != _history->peer) {\n\t\treturn Window::SectionActionResult::Ignore;\n\t}\n\tlistSendBotCommand(request.command, request.context);\n\treturn Window::SectionActionResult::Handle;\n}\n\nvoid ScheduledWidget::listSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) {\n\tconst auto callback = [=](Api::SendOptions options) {\n\t\tconst auto text = Bot::WrapCommandInChat(\n\t\t\t_history->peer,\n\t\t\tcommand,\n\t\t\tcontext);\n\t\tauto message = Api::MessageToSend(prepareSendAction(options));\n\t\tmessage.textWithTags = { text };\n\t\tsession().api().sendMessage(std::move(message));\n\t};\n\tcontroller()->show(\n\t\tPrepareScheduleBox(this, _show, sendMenuDetails(), callback));\n}\n\nvoid ScheduledWidget::listSearch(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) {\n\tconst auto inChat = _history->peer->isUser()\n\t\t? Dialogs::Key()\n\t\t: Dialogs::Key(_history);\n\tcontroller()->searchMessages(query, inChat);\n}\n\nvoid ScheduledWidget::listHandleViaClick(not_null<UserData*> bot) {\n\t_composeControls->setText({ '@' + bot->username() + ' ' });\n}\n\nnot_null<Ui::ChatTheme*> ScheduledWidget::listChatTheme() {\n\treturn _theme.get();\n}\n\nCopyRestrictionType ScheduledWidget::listCopyRestrictionType(\n\t\tHistoryItem *item) {\n\treturn CopyRestrictionType::None;\n}\n\nCopyRestrictionType ScheduledWidget::listCopyMediaRestrictionType(\n\t\tnot_null<HistoryItem*> item) {\n\tif (const auto media = item->media()) {\n\t\tif (const auto invoice = media->invoice()) {\n\t\t\tif (HasExtendedMedia(*invoice)) {\n\t\t\t\treturn CopyMediaRestrictionTypeFor(_history->peer, item);\n\t\t\t}\n\t\t}\n\t}\n\treturn CopyRestrictionType::None;\n}\n\nCopyRestrictionType ScheduledWidget::listSelectRestrictionType() {\n\treturn CopyRestrictionType::None;\n}\n\nauto ScheduledWidget::listAllowedReactionsValue()\n-> rpl::producer<Data::AllowedReactions> {\n\treturn rpl::single(Data::AllowedReactions());\n}\n\nvoid ScheduledWidget::listShowPremiumToast(\n\t\tnot_null<DocumentData*> document) {\n\tif (!_stickerToast) {\n\t\t_stickerToast = std::make_unique<HistoryView::StickerToast>(\n\t\t\tcontroller(),\n\t\t\tthis,\n\t\t\t[=] { _stickerToast = nullptr; });\n\t}\n\t_stickerToast->showFor(document);\n}\n\nvoid ScheduledWidget::listOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) {\n\tconst auto draw = _forumTopic\n\t\t? Data::CanSendAnyOf(_forumTopic, Data::FilesSendRestrictions())\n\t\t: Data::CanSendAnyOf(\n\t\t\t_history->peer,\n\t\t\tData::FilesSendRestrictions());\n\tcontroller()->openPhoto(photo, { .id = context, .showDrawButton = draw });\n}\n\nvoid ScheduledWidget::listOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView) {\n\tconst auto draw = _forumTopic\n\t\t? Data::CanSendAnyOf(_forumTopic, Data::FilesSendRestrictions())\n\t\t: Data::CanSendAnyOf(\n\t\t\t_history->peer,\n\t\t\tData::FilesSendRestrictions());\n\tcontroller()->openDocument(\n\t\tdocument,\n\t\tshowInMediaView,\n\t\t{ .id = context, .showDrawButton = draw });\n}\n\nvoid ScheduledWidget::listPaintEmpty(\n\tPainter &p,\n\tconst Ui::ChatPaintContext &context) {\n}\n\nQString ScheduledWidget::listElementAuthorRank(\n\t\tnot_null<const Element*> view) {\n\treturn {};\n}\n\nbool ScheduledWidget::listElementHideTopicButton(\n\t\tnot_null<const Element*> view) {\n\treturn true;\n}\n\nHistory *ScheduledWidget::listTranslateHistory() {\n\treturn nullptr;\n}\n\nvoid ScheduledWidget::listAddTranslatedItems(\n\tnot_null<TranslateTracker*> tracker) {\n}\n\nUi::ScrollArea *ScheduledWidget::listScrollArea() const {\n\treturn _scroll.data();\n}\n\nbool ScheduledWidget::listThanosEffectEnabled() const {\n\treturn false;\n}\n\nvoid ScheduledWidget::confirmSendNowSelected() {\n\tConfirmSendNowSelectedItems(_inner);\n}\n\nvoid ScheduledWidget::confirmDeleteSelected() {\n\tConfirmDeleteSelectedItems(_inner);\n}\n\nvoid ScheduledWidget::clearSelected() {\n\t_inner->cancelSelection();\n}\n\nvoid ScheduledWidget::setupDragArea() {\n\tconst auto areas = DragArea::SetupDragAreaToContainer(\n\t\tthis,\n\t\t[=](auto d) { return _history && !_composeControls->isRecording(); },\n\t\tnullptr,\n\t\t[=] { updateControlsGeometry(); });\n\n\tconst auto droppedCallback = [=](bool overrideSendImagesAsPhotos) {\n\t\treturn [=](const QMimeData *data) {\n\t\t\tconfirmSendingFiles(data, overrideSendImagesAsPhotos);\n\t\t\tWindow::ActivateWindow(controller());\n\t\t};\n\t};\n\tareas.document->setDroppedCallback(droppedCallback(false));\n\tareas.photo->setDroppedCallback(droppedCallback(true));\n}\n\nbool ShowScheduledVideoPublished(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst Data::SentFromScheduled &info,\n\t\tFn<void()> hidden) {\n\tif (!controller->widget()->isActive()) {\n\t\treturn false;\n\t}\n\tconst auto document = FindVideoFile(info.item);\n\tif (!document) {\n\t\treturn false;\n\t}\n\tconst auto history = info.item->history();\n\tconst auto itemId = info.sentId;\n\n\tconst auto text = tr::lng_scheduled_video_published(\n\t\ttr::now,\n\t\ttr::bold);\n\tconst auto &st = st::processingVideoToast;\n\tconst auto skip = st::processingVideoPreviewSkip;\n\tconst auto size = st.style.font->height * 2;\n\tconst auto view = tr::lng_scheduled_video_view(tr::now);\n\tconst auto additional = QMargins(\n\t\tskip + size,\n\t\t0,\n\t\t(st::processingVideoView.style.font->width(view)\n\t\t\t- (st::processingVideoView.width / 2)),\n\t\t0);\n\n\tconst auto parent = controller->uiShow()->toastParent();\n\tconst auto weak = Ui::Toast::Show(parent, Ui::Toast::Config{\n\t\t.text = text,\n\t\t.padding = rpl::single(additional),\n\t\t.st = &st,\n\t\t.attach = RectPart::Top,\n\t\t.acceptinput = true,\n\t\t.duration = kVideoProcessingInfoDuration,\n\t});\n\tconst auto strong = weak.get();\n\tif (!strong) {\n\t\treturn false;\n\t}\n\tconst auto widget = strong->widget();\n\tconst auto hideToast = [weak] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->hideAnimated();\n\t\t}\n\t};\n\n\tconst auto clickableBackground = Ui::CreateChild<Ui::AbstractButton>(\n\t\twidget.get());\n\tclickableBackground->setPointerCursor(false);\n\tclickableBackground->setAcceptBoth();\n\tclickableBackground->show();\n\tclickableBackground->addClickHandler([=](Qt::MouseButton button) {\n\t\tif (button == Qt::RightButton) {\n\t\t\thideToast();\n\t\t}\n\t});\n\n\tconst auto button = Ui::CreateChild<Ui::RoundButton>(\n\t\twidget.get(),\n\t\trpl::single(view),\n\t\tst::processingVideoView);\n\tbutton->show();\n\trpl::combine(\n\t\twidget->sizeValue(),\n\t\tbutton->sizeValue()\n\t) | rpl::on_next([=](QSize outer, QSize inner) {\n\t\tbutton->moveToRight(\n\t\t\t0,\n\t\t\t(outer.height() - inner.height()) / 2,\n\t\t\touter.width());\n\t\tclickableBackground->resize(outer);\n\t}, widget->lifetime());\n\tconst auto preview = Ui::CreateChild<Ui::RpWidget>(widget.get());\n\tpreview->moveToLeft(skip, skip);\n\tpreview->resize(size, size);\n\tpreview->show();\n\n\tconst auto thumbnail = Ui::MakeDocumentThumbnail(document, FullMsgId(\n\t\thistory->peer->id,\n\t\titemId));\n\tthumbnail->subscribeToUpdates([=] {\n\t\tpreview->update();\n\t});\n\tpreview->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(preview);\n\t\tconst auto image = Images::Round(\n\t\t\tthumbnail->image(size),\n\t\t\tImageRoundRadius::Small);\n\t\tp.drawImage(QRect(0, 0, size, size), image);\n\t}, preview->lifetime());\n\n\tbutton->setClickedCallback([=] {\n\t\tcontroller->showPeerHistory(\n\t\t\thistory,\n\t\t\tWindow::SectionShow::Way::Forward,\n\t\t\titemId);\n\t\thideToast();\n\t});\n\n\tif (hidden) {\n\t\twidget->lifetime().add(std::move(hidden));\n\t}\n\treturn true;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_scheduled_section.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"window/section_widget.h\"\n#include \"window/section_memento.h\"\n#include \"history/view/history_view_list_widget.h\"\n#include \"history/view/history_view_corner_buttons.h\"\n#include \"data/data_messages.h\"\n\nclass History;\nenum class SendMediaType;\nstruct SendingAlbum;\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nstruct SentFromScheduled;\n} // namespace Data\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nnamespace Api {\nstruct MessageToSend;\nstruct SendOptions;\nstruct SendAction;\n} // namespace Api\n\nnamespace Ui {\nclass ScrollArea;\nclass PlainShadow;\nclass FlatButton;\nstruct PreparedList;\nstruct PreparedBundle;\nclass SendFilesWay;\nclass ImportantTooltip;\n} // namespace Ui\n\nnamespace Profile {\nclass BackButton;\n} // namespace Profile\n\nnamespace InlineBots {\nclass Result;\n} // namespace InlineBots\n\nnamespace HistoryView::Controls {\nstruct VoiceToSend;\n} // namespace HistoryView::Controls\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace HistoryView {\n\nclass Element;\nclass TopBarWidget;\nclass ScheduledMemento;\nclass ComposeControls;\nclass StickerToast;\n\nclass ScheduledWidget final\n\t: public Window::SectionWidget\n\t, private WindowListDelegate\n\t, private CornerButtonsDelegate {\npublic:\n\tScheduledWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<History*> history,\n\t\tconst Data::ForumTopic *forumTopic);\n\t~ScheduledWidget();\n\n\tnot_null<History*> history() const;\n\tDialogs::RowDescriptor activeChat() const override;\n\tbool preventsClose(Fn<void()> &&continueCallback) const override;\n\n\tbool hasTopBarShadow() const override {\n\t\treturn true;\n\t}\n\n\tQPixmap grabForShowAnimation(\n\t\tconst Window::SectionSlideParams &params) override;\n\n\tbool showInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) override;\n\tstd::shared_ptr<Window::SectionMemento> createMemento() override;\n\tbool showMessage(\n\t\tPeerId peerId,\n\t\tconst Window::SectionShow &params,\n\t\tMsgId messageId) override;\n\n\tWindow::SectionActionResult sendBotCommand(\n\t\tBot::SendCommandRequest request) override;\n\tusing SectionWidget::confirmSendingFiles;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<ScheduledMemento*> memento);\n\n\t// Tabbed selector management.\n\tbool pushTabbedSelectorToThirdSection(\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst Window::SectionShow &params) override;\n\tbool returnTabbedSelector() override;\n\n\t// Float player interface.\n\tbool floatPlayerHandleWheelEvent(QEvent *e) override;\n\tQRect floatPlayerAvailableRect() override;\n\n\t// ListDelegate interface.\n\tContext listContext() override;\n\tbool listScrollTo(int top, bool syntetic = true) override;\n\tvoid listCancelRequest() override;\n\tvoid listDeleteRequest() override;\n\tvoid listTryProcessKeyInput(not_null<QKeyEvent*> e) override;\n\trpl::producer<Data::MessagesSlice> listSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) override;\n\tbool listAllowsMultiSelect() override;\n\tbool listIsItemGoodForSelection(not_null<HistoryItem*> item) override;\n\tbool listIsLessInOrder(\n\t\tnot_null<HistoryItem*> first,\n\t\tnot_null<HistoryItem*> second) override;\n\tvoid listSelectionChanged(SelectedItems &&items) override;\n\tvoid listMarkReadTill(not_null<HistoryItem*> item) override;\n\tvoid listMarkContentsRead(\n\t\tconst base::flat_set<not_null<HistoryItem*>> &items) override;\n\tMessagesBarData listMessagesBar(\n\t\tconst std::vector<not_null<Element*>> &elements,\n\t\tbool markLastAsRead) override;\n\tvoid listContentRefreshed() override;\n\tvoid listUpdateDateLink(\n\t\tClickHandlerPtr &link,\n\t\tnot_null<Element*> view) override;\n\tbool listElementHideReply(not_null<const Element*> view) override;\n\tbool listElementShownUnread(not_null<const Element*> view) override;\n\tbool listIsGoodForAroundPosition(\n\t\tnot_null<const Element *> view) override;\n\tvoid listSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) override;\n\tvoid listSearch(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) override;\n\tvoid listHandleViaClick(not_null<UserData*> bot) override;\n\tnot_null<Ui::ChatTheme*> listChatTheme() override;\n\tCopyRestrictionType listCopyRestrictionType(HistoryItem *item) override;\n\tCopyRestrictionType listCopyMediaRestrictionType(\n\t\tnot_null<HistoryItem*> item) override;\n\tCopyRestrictionType listSelectRestrictionType() override;\n\tauto listAllowedReactionsValue()\n\t\t-> rpl::producer<Data::AllowedReactions> override;\n\tvoid listShowPremiumToast(not_null<DocumentData*> document) override;\n\tvoid listOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) override;\n\tvoid listOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView) override;\n\tvoid listPaintEmpty(\n\t\tPainter &p,\n\t\tconst Ui::ChatPaintContext &context) override;\n\tQString listElementAuthorRank(not_null<const Element*> view) override;\n\tbool listElementHideTopicButton(not_null<const Element*> view) override;\n\tHistory *listTranslateHistory() override;\n\tvoid listAddTranslatedItems(\n\t\tnot_null<TranslateTracker*> tracker) override;\n\tUi::ScrollArea *listScrollArea() const override;\n\tbool listThanosEffectEnabled() const override;\n\n\t// CornerButtonsDelegate delegate.\n\tvoid cornerButtonsShowAtPosition(\n\t\tData::MessagePosition position) override;\n\tData::Thread *cornerButtonsThread() override;\n\tFullMsgId cornerButtonsCurrentId() override;\n\tbool cornerButtonsIgnoreVisibility() override;\n\tstd::optional<bool> cornerButtonsDownShown() override;\n\tbool cornerButtonsUnreadMayBeShown() override;\n\tbool cornerButtonsHas(CornerButtonType type) override;\n\nprivate:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid showAnimatedHook(\n\t\tconst Window::SectionSlideParams &params) override;\n\tvoid showFinishedHook() override;\n\tvoid doSetInnerFocus() override;\n\tvoid checkActivation() override;\n\n\tvoid onScroll();\n\tvoid updateInnerVisibleArea();\n\tvoid updateControlsGeometry();\n\tvoid updateAdaptiveLayout();\n\tvoid saveState(not_null<ScheduledMemento*> memento);\n\tvoid restoreState(not_null<ScheduledMemento*> memento);\n\tvoid showAtPosition(\n\t\tData::MessagePosition position,\n\t\tFullMsgId originId = {});\n\n\tvoid initProcessingVideoView(not_null<Element*> view);\n\tvoid checkProcessingVideoTooltip(int visibleTop, int visibleBottom);\n\tvoid showProcessingVideoTooltip();\n\tvoid updateProcessingVideoTooltipPosition();\n\tvoid clearProcessingVideoTracking(bool fast);\n\n\tvoid setupComposeControls();\n\n\tvoid setupDragArea();\n\n\tvoid confirmSendNowSelected();\n\tvoid confirmDeleteSelected();\n\tvoid clearSelected();\n\n\t[[nodiscard]] Api::SendAction prepareSendAction(\n\t\tApi::SendOptions options) const;\n\tvoid send();\n\tvoid send(Api::SendOptions options);\n\tvoid sendVoice(const Controls::VoiceToSend &data);\n\tvoid sendVoice(\n\t\tconst Controls::VoiceToSend &data,\n\t\tApi::SendOptions options);\n\tvoid edit(\n\t\tnot_null<HistoryItem*> item,\n\t\tApi::SendOptions options,\n\t\tmtpRequestId *const saveEditMsgRequestId,\n\t\tbool spoilered);\n\tvoid highlightSingleNewMessage(const Data::MessagesSlice &slice);\n\tvoid chooseAttach();\n\t[[nodiscard]] SendMenu::Details sendMenuDetails() const;\n\n\tvoid pushReplyReturn(not_null<HistoryItem*> item);\n\tvoid checkReplyReturns();\n\n\tvoid uploadFile(const QByteArray &fileContent, SendMediaType type);\n\tbool confirmSendingFiles(\n\t\tQImage &&image,\n\t\tQByteArray &&content,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos = std::nullopt,\n\t\tconst QString &insertTextOnCancel = QString());\n\tbool confirmSendingFiles(\n\t\tUi::PreparedList &&list,\n\t\tconst QString &insertTextOnCancel = QString());\n\tbool confirmSendingFiles(\n\t\tnot_null<const QMimeData*> data,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos,\n\t\tconst QString &insertTextOnCancel = QString());\n\tbool showSendingFilesError(const Ui::PreparedList &list) const;\n\tbool showSendingFilesError(const Ui::PreparedBundle &bundle) const;\n\n\tvoid sendingFilesConfirmed(\n\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\tApi::SendOptions options);\n\n\tbool sendExistingDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tApi::MessageToSend messageToSend);\n\tvoid sendExistingPhoto(not_null<PhotoData*> photo);\n\tbool sendExistingPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tApi::SendOptions options);\n\tvoid sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot);\n\tvoid sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot,\n\t\tApi::SendOptions options);\n\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst not_null<History*> _history;\n\tconst Data::ForumTopic *_forumTopic;\n\tstd::shared_ptr<Ui::ChatTheme> _theme;\n\tobject_ptr<Ui::ScrollArea> _scroll;\n\tQPointer<ListWidget> _inner;\n\tobject_ptr<TopBarWidget> _topBar;\n\tobject_ptr<Ui::PlainShadow> _topBarShadow;\n\tstd::unique_ptr<ComposeControls> _composeControls;\n\tbool _skipScrollEvent = false;\n\n\tData::MessagePosition _processingVideoPosition;\n\tbase::weak_ptr<Element> _processingVideoView;\n\trpl::lifetime _processingVideoLifetime;\n\n\tstd::unique_ptr<HistoryView::StickerToast> _stickerToast;\n\tstd::unique_ptr<Ui::ImportantTooltip> _processingVideoTooltip;\n\tbase::Timer _processingVideoTipTimer;\n\tbool _processingVideoUpdateScheduled = false;\n\tbool _processingVideoTooltipShown = false;\n\tbool _processingVideoCanShow = false;\n\n\tCornerButtons _cornerButtons;\n\n\tData::MessagesSlice _lastSlice;\n\tbool _choosingAttach = false;\n\n};\n\nclass ScheduledMemento final : public Window::SectionMemento {\npublic:\n\tScheduledMemento(\n\t\tnot_null<History*> history,\n\t\tMsgId sentToScheduledId = 0);\n\tScheduledMemento(not_null<Data::ForumTopic*> forumTopic);\n\n\tobject_ptr<Window::SectionWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tWindow::Column column,\n\t\tconst QRect &geometry) override;\n\n\t[[nodiscard]] not_null<History*> getHistory() const {\n\t\treturn _history;\n\t}\n\n\t[[nodiscard]] not_null<ListMemento*> list() {\n\t\treturn &_list;\n\t}\n\n\t[[nodiscard]] MsgId sentToScheduledId() const {\n\t\treturn _sentToScheduledId;\n\t}\n\nprivate:\n\tconst not_null<History*> _history;\n\tconst Data::ForumTopic *_forumTopic;\n\tListMemento _list;\n\tMsgId _sentToScheduledId = 0;\n\n};\n\nbool ShowScheduledVideoPublished(\n\tnot_null<Window::SessionController*> controller,\n\tconst Data::SentFromScheduled &info,\n\tFn<void()> hidden = nullptr);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_self_forwards_tagger.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_self_forwards_tagger.h\"\n\n#include \"base/call_delayed.h\"\n#include \"base/event_filter.h\"\n#include \"base/timer_rpl.h\"\n#include \"boxes/choose_filter_box.h\"\n#include \"chat_helpers/share_message_phrase_factory.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/reactions/history_view_reactions_selector.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/rect.h\"\n#include \"ui/effects/show_animation.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast_widget.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_info.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kInitTimer = crl::time(3000);\nconstexpr auto kTimerOnLeave = crl::time(2000);\n\n} // namespace\n\nSelfForwardsTagger::SelfForwardsTagger(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::RpWidget*> parent,\n\tFn<Ui::RpWidget*()> listWidget,\n\tnot_null<QWidget*> scroll,\n\tFn<History*()> history)\n: _controller(controller)\n, _parent(parent)\n, _listWidget(std::move(listWidget))\n, _scroll(scroll)\n, _history(std::move(history)) {\n\tsetup();\n}\n\nSelfForwardsTagger::~SelfForwardsTagger() = default;\n\nvoid SelfForwardsTagger::setup() {\n\t_controller->session().data().recentSelfForwards(\n\t) | rpl::on_next([=](const Data::RecentSelfForwards &data) {\n\t\tconst auto history = _history ? _history() : nullptr;\n\t\tif (!history || history->peer->id != data.fromPeerId) {\n\t\t\treturn;\n\t\t}\n\t\tshowSelectorForMessages(data.ids);\n\t}, _lifetime);\n\t_controller->session().data().recentJoinChat(\n\t) | rpl::on_next([=](const Data::RecentJoinChat &data) {\n\t\tif (!_controller->session().data().chatsFilters().has()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto history = _history ? _history() : nullptr;\n\t\tif (!history || history->peer->id != data.fromPeerId) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto peerId = data.joinedPeerId;\n\t\tif (const auto peer = _controller->session().data().peer(peerId)) {\n\t\t\tshowChannelFilterToast(peer);\n\t\t}\n\t}, _lifetime);\n}\n\nvoid SelfForwardsTagger::showSelectorForMessages(\n\t\tconst MessageIdsList &ids) {\n\tif (ids.empty()) {\n\t\treturn;\n\t}\n\tconst auto lastId = ids.back();\n\tconst auto item = _controller->session().data().message(lastId);\n\tif (!item) {\n\t\treturn;\n\t}\n\tusing namespace Reactions;\n\tconst auto reactions = Data::LookupPossibleReactions(item, true);\n\tif (reactions.recent.empty()) {\n\t\treturn;\n\t}\n\n\tshowToast(\n\t\trpl::variable<TextWithEntities>(\n\t\t\tChatHelpers::ForwardedMessagePhrase({\n\t\t\t.toCount = 1,\n\t\t\t.singleMessage = (ids.size() == 1),\n\t\t\t.to1 = _controller->session().user(),\n\t\t\t.toSelfWithPremiumIsEmpty = false,\n\t\t})).current(),\n\t\tnullptr);\n\n\tconst auto toastWidget = [&]() -> Ui::RpWidget* {\n\t\tif (const auto toast = _toast.get()) {\n\t\t\treturn toast->widget();\n\t\t}\n\t\treturn nullptr;\n\t}();\n\tif (!toastWidget) {\n\t\treturn;\n\t}\n\n\tconst auto toastWidth = toastWidget->width();\n\tconst auto selector = Ui::CreateChild<Selector>(\n\t\ttoastWidget->parentWidget(),\n\t\tst::reactPanelEmojiPan,\n\t\t_controller->uiShow(),\n\t\treactions,\n\t\ttr::lng_add_tag_selector(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tfloat64(ids.size()),\n\t\t\tTextWithEntities::Simple),\n\t\t[](bool) {},\n\t\tIconFactory(),\n\t\t[] { return false; },\n\t\tfalse);\n\tselector->setBubbleUp(true);\n\n\tconst auto hideAndDestroy = [\n\t\t\tselectorWeak = base::make_weak(selector),\n\t\t\ttoastWidgetWeak = _toast] {\n\t\tconst auto selector = selectorWeak.get();\n\t\tconst auto toastWidget = toastWidgetWeak.get();\n\t\tif (!selector || !toastWidget) {\n\t\t\treturn;\n\t\t}\n\t\tUi::Animations::HideWidgets({ toastWidget->widget(), selector });\n\t\tselector->shownValue(\n\t\t) | rpl::on_next([toastWidgetWeak](bool shown) {\n\t\t\tif (!shown) {\n\t\t\t\tif (const auto toast = toastWidgetWeak.get()) {\n\t\t\t\t\tdelete toast->widget();\n\t\t\t\t}\n\t\t\t}\n\t\t}, selector->lifetime());\n\t};\n\n\tselector->chosen(\n\t) | rpl::on_next([=](ChosenReaction reaction) {\n\t\tselector->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tfor (const auto &id : ids) {\n\t\t\tif (const auto item = _controller->session().data().message(id)) {\n\t\t\t\titem->toggleReaction(\n\t\t\t\t\treaction.id,\n\t\t\t\t\tHistoryReactionSource::Selector);\n\t\t\t}\n\t\t}\n\t\thideAndDestroy();\n\t\tbase::call_delayed(st::defaultToggle.duration, _parent, [=] {\n\t\t\tshowTaggedToast(reaction.id.custom());\n\t\t});\n\t}, selector->lifetime());\n\n\tconst auto eventFilterCallback = [=](not_null<QEvent*> event) {\n\t\tif (event->type() == QEvent::MouseButtonPress) {\n\t\t\thideAndDestroy();\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t};\n\tbase::install_event_filter(selector, _parent, eventFilterCallback);\n\tif (const auto list = _listWidget()) {\n\t\tlist->lifetime().add([=] {\n\t\t\thideAndDestroy();\n\t\t});\n\t\tbase::install_event_filter(selector, list, eventFilterCallback);\n\t}\n\n\tconst auto state = selector->lifetime().make_state<ToastTimerState>();\n\n\tselector->willExpand() | rpl::on_next([=] {\n\t\tstate->expanded = true;\n\t}, selector->lifetime());\n\n\tsetupToastTimer(selector, state, hideAndDestroy);\n\n\tQObject::connect(\n\t\t_toast->widget(),\n\t\t&QObject::destroyed,\n\t\tselector,\n\t\t[=] { delete selector; });\n\n\tconst auto selectorWidth = toastWidth;\n\tselector->countWidth(selectorWidth, selectorWidth);\n\tselector->initGeometry(_parent->height() / 2);\n\n\t_toast->widget()->geometryValue(\n\t) | rpl::on_next([=](const QRect &rect) {\n\t\tif (rect.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tselector->moveToLeft(\n\t\t\trect.x() + (rect.width() - selector->width()) / 2,\n\t\t\trect::bottom(rect) - st::selfForwardsTaggerStripSkip);\n\t}, selector->lifetime());\n\tselector->show();\n}\n\nvoid SelfForwardsTagger::showToast(\n\t\tconst TextWithEntities &text,\n\t\tFn<void()> callback) {\n\thideToast();\n\t_toast = Ui::Toast::Show(_scroll, Ui::Toast::Config{\n\t\t.text = text,\n\t\t.textContext = Core::TextContext({\n\t\t\t.session = &_controller->session(),\n\t\t}),\n\t\t.iconLottie = u\"toast/saved_messages\"_q,\n\t\t.iconPadding = st::selfForwardsTaggerIconPadding,\n\t\t.st = &st::selfForwardsTaggerToast,\n\t\t.attach = RectPart::Top,\n\t\t.infinite = true,\n\t});\n\tif (const auto strong = _toast.get()) {\n\t\tif (callback) {\n\t\t\tQObject::connect(strong->widget(), &QObject::destroyed, callback);\n\t\t}\n\t} else if (callback) {\n\t\tcallback();\n\t}\n}\n\n\nvoid SelfForwardsTagger::showTaggedToast(DocumentId reaction) {\n\tauto text = tr::lng_message_tagged_with(\n\t\ttr::now,\n\t\tlt_emoji,\n\t\tData::SingleCustomEmoji(reaction),\n\t\ttr::marked);\n\thideToast();\n\n\tconst auto &st = st::selfForwardsTaggerToast;\n\tconst auto viewText = tr::lng_tagged_view_saved(tr::now);\n\tconst auto viewFont = st::historyPremiumViewSet.style.font;\n\tconst auto rightSkip = viewFont->width(viewText)\n\t\t+ st::toastUndoSpace;\n\n\t_toast = Ui::Toast::Show(_scroll, Ui::Toast::Config{\n\t\t.text = text,\n\t\t.textContext = Core::TextContext({\n\t\t\t.session = &_controller->session(),\n\t\t}),\n\t\t.iconLottie = u\"toast/tagged\"_q,\n\t\t.iconPadding = st::selfForwardsTaggerIconPadding,\n\t\t.padding = rpl::single(QMargins(0, 0, rightSkip, 0)),\n\t\t.st = &st,\n\t\t.attach = RectPart::Top,\n\t\t.acceptinput = true,\n\t\t.duration = crl::time(3000),\n\t});\n\tif (const auto strong = _toast.get()) {\n\t\tconst auto widget = strong->widget();\n\n\t\tconst auto button = Ui::CreateChild<Ui::AbstractButton>(widget.get());\n\t\tbutton->setClickedCallback([=] {\n\t\t\t_controller->showPeerHistory(_controller->session().user());\n\t\t\thideToast();\n\t\t});\n\n\t\tbutton->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(button);\n\t\t\tconst auto font = st::historyPremiumViewSet.style.font;\n\t\t\tconst auto top = (button->height() - font->height) / 2;\n\t\t\tp.setPen(st::historyPremiumViewSet.textFg);\n\t\t\tp.setFont(font);\n\t\t\tp.drawText(0, top + font->ascent, viewText);\n\t\t}, button->lifetime());\n\n\t\tbutton->resize(\n\t\t\tviewFont->width(viewText),\n\t\t\tst::historyPremiumViewSet.height);\n\n\t\trpl::combine(\n\t\t\twidget->sizeValue(),\n\t\t\tbutton->sizeValue()\n\t\t) | rpl::on_next([=](const QSize &outer, const QSize &inner) {\n\t\t\tbutton->moveToRight(\n\t\t\t\tst.padding.right(),\n\t\t\t\t(outer.height() - inner.height()) / 2,\n\t\t\t\touter.width());\n\t\t}, widget->lifetime());\n\n\t\tbutton->show();\n\t}\n}\n\nvoid SelfForwardsTagger::showChannelFilterToast(not_null<PeerData*> peer) {\n\thideToast();\n\tconst auto toastText = peer->isChannel() && !peer->isMegagroup()\n\t\t? tr::lng_add_channel_to_filter_selector(tr::now)\n\t\t: tr::lng_add_group_to_filter_selector(tr::now);\n\t_toast = Ui::Toast::Show(_scroll, Ui::Toast::Config{\n\t\t.text = { .text = toastText },\n\t\t.iconLottie = u\"toast/chats_filter_in\"_q,\n\t\t.iconPadding = st::selfForwardsTaggerIconPadding,\n\t\t.st = &st::joinChatAddToFilterToast,\n\t\t.attach = RectPart::Top,\n\t\t.acceptinput = true,\n\t\t.infinite = true,\n\t});\n\tif (const auto strong = _toast.get()) {\n\t\tconst auto widget = strong->widget();\n\t\tconst auto rightButton = createRightButton(widget);\n\t\tconst auto history = peer->owner().history(peer);\n\n\t\tconst auto state = widget->lifetime().make_state<ToastTimerState>();\n\n\t\trightButton->setClickedCallback([=] {\n\t\t\tstate->expanded = true;\n\t\t\tstate->timerLifetime.destroy();\n\t\t\tconst auto menu = Ui::CreateChild<Ui::PopupMenu>(\n\t\t\t\trightButton,\n\t\t\t\tst::foldersMenu);\n\t\t\tmenu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);\n\t\t\tFillChooseFilterMenu(_controller, menu, history);\n\t\t\tif (!menu->empty()) {\n\t\t\t\tmenu->popup(\n\t\t\t\t\trightButton->mapToGlobal(\n\t\t\t\t\t\tQPoint(\n\t\t\t\t\t\t\trightButton->width(),\n\t\t\t\t\t\t\trightButton->height() + rightButton->y())));\n\t\t\t\tQObject::connect(menu, &QObject::destroyed, [=] {\n\t\t\t\t\thideToast();\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\thideToast();\n\t\t\t}\n\t\t});\n\n\t\tsetupToastTimer(widget, state, [=] { hideToast(); });\n\t}\n}\n\nnot_null<Ui::AbstractButton*> SelfForwardsTagger::createRightButton(\n\t\tnot_null<Ui::RpWidget*> widget) {\n\tconst auto button = Ui::CreateChild<Ui::IconButton>(\n\t\twidget.get(),\n\t\tst::joinChatAddToFilterToastButton);\n\twidget->sizeValue() | rpl::on_next([=](const QSize &size) {\n\t\tbutton->moveToRight(\n\t\t\tst::lineWidth * 4,\n\t\t\t(size.height() - button->height()) / 2);\n\t}, button->lifetime());\n\n\tbutton->show();\n\treturn button;\n}\n\nvoid SelfForwardsTagger::setupToastTimer(\n\t\tnot_null<Ui::RpWidget*> widget,\n\t\tnot_null<ToastTimerState*> state,\n\t\tFn<void()> hideCallback) {\n\tconst auto restartTimer = [=](crl::time ms) {\n\t\tstate->timerLifetime.destroy();\n\t\tbase::timer_once(ms) | rpl::on_next([=] {\n\t\t\thideCallback();\n\t\t}, state->timerLifetime);\n\t};\n\n\tbase::install_event_filter(widget, [=](not_null<QEvent*> event) {\n\t\tif (event->type() == QEvent::MouseButtonPress) {\n\t\t\tstate->timerLifetime.destroy();\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t} else if (!state->expanded && event->type() == QEvent::Enter) {\n\t\t\tstate->timerLifetime.destroy();\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t} else if (!state->expanded && event->type() == QEvent::Leave) {\n\t\t\trestartTimer(kTimerOnLeave);\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t}, widget->lifetime());\n\n\trestartTimer(kInitTimer);\n}\n\nvoid SelfForwardsTagger::hideToast() {\n\tif (const auto strong = _toast.get()) {\n\t\tstrong->hideAnimated();\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_self_forwards_tagger.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n\nclass History;\n\nnamespace Data {\nclass Session;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass RpWidget;\nclass AbstractButton;\n} // namespace Ui\n\nnamespace Ui::Toast {\nclass Instance;\n} // namespace Ui::Toast\n\nnamespace Reactions {\nstruct ChosenReaction;\n} // namespace Reactions\n\nnamespace HistoryView {\n\nclass SelfForwardsTagger final : public base::has_weak_ptr {\npublic:\n\tSelfForwardsTagger(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tFn<Ui::RpWidget*()> listWidget,\n\t\tnot_null<QWidget*> scroll,\n\t\tFn<History*()> history);\n\n\t~SelfForwardsTagger();\n\nprivate:\n\tstruct ToastTimerState {\n\t\trpl::lifetime timerLifetime;\n\t\tbool expanded = false;\n\t};\n\n\tvoid setup();\n\tvoid showSelectorForMessages(const MessageIdsList &ids);\n\tvoid showToast(const TextWithEntities &text, Fn<void()> callback);\n\tvoid showTaggedToast(DocumentId);\n\tvoid showChannelFilterToast(not_null<PeerData*> peer);\n\tnot_null<Ui::AbstractButton*> createRightButton(\n\t\tnot_null<Ui::RpWidget*> widget);\n\tvoid setupToastTimer(\n\t\tnot_null<Ui::RpWidget*> widget,\n\t\tnot_null<ToastTimerState*> state,\n\t\tFn<void()> hideCallback);\n\tvoid hideToast();\n\t[[nodiscard]] QRect toastGeometry() const;\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<Ui::RpWidget*> _parent;\n\tconst Fn<Ui::RpWidget*()> _listWidget;\n\tconst not_null<QWidget*> _scroll;\n\tconst Fn<History*()> _history;\n\n\tbase::weak_ptr<Ui::Toast::Instance> _toast;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_send_action.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_send_action.h\"\n\n#include \"data/data_user.h\"\n#include \"data/data_send_action.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"history/history.h\"\n#include \"lang/lang_instance.h\" // Instance::supportChoosingStickerReplacement\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kStatusShowClientsideTyping = 6 * crl::time(1000);\nconstexpr auto kStatusShowClientsideRecordVideo = 6 * crl::time(1000);\nconstexpr auto kStatusShowClientsideUploadVideo = 6 * crl::time(1000);\nconstexpr auto kStatusShowClientsideRecordVoice = 6 * crl::time(1000);\nconstexpr auto kStatusShowClientsideUploadVoice = 6 * crl::time(1000);\nconstexpr auto kStatusShowClientsideRecordRound = 6 * crl::time(1000);\nconstexpr auto kStatusShowClientsideUploadRound = 6 * crl::time(1000);\nconstexpr auto kStatusShowClientsideUploadPhoto = 6 * crl::time(1000);\nconstexpr auto kStatusShowClientsideUploadFile = 6 * crl::time(1000);\nconstexpr auto kStatusShowClientsideChooseLocation = 6 * crl::time(1000);\nconstexpr auto kStatusShowClientsideChooseContact = 6 * crl::time(1000);\nconstexpr auto kStatusShowClientsideChooseSticker = 6 * crl::time(1000);\nconstexpr auto kStatusShowClientsidePlayGame = 10 * crl::time(1000);\nconstexpr auto kStatusShowClientsideSpeaking = 6 * crl::time(1000);\n\n} // namespace\n\nSendActionPainter::SendActionPainter(\n\tnot_null<History*> history,\n\tMsgId rootId)\n: _history(history)\n, _rootId(rootId)\n, _weak(&_history->session())\n, _st(st::dialogsTextStyle)\n, _sendActionText(st::dialogsTextWidthMin) {\n}\n\nvoid SendActionPainter::setTopic(Data::ForumTopic *topic) {\n\t_topic = topic;\n}\n\nbool SendActionPainter::updateNeedsAnimating(\n\t\tnot_null<UserData*> user,\n\t\tconst MTPSendMessageAction &action) {\n\tusing Type = Api::SendProgressType;\n\tif (action.type() == mtpc_sendMessageCancelAction) {\n\t\tclear(user);\n\t\treturn false;\n\t}\n\n\tconst auto now = crl::now();\n\tconst auto emplaceAction = [&](\n\t\t\tType type,\n\t\t\tcrl::time duration,\n\t\t\tint progress = 0) {\n\t\t_sendActions.emplace_or_assign(user, type, now + duration, progress);\n\t};\n\taction.match([&](const MTPDsendMessageTypingAction &) {\n\t\t_typing.emplace_or_assign(user, now + kStatusShowClientsideTyping);\n\t}, [&](const MTPDsendMessageRecordVideoAction &) {\n\t\templaceAction(Type::RecordVideo, kStatusShowClientsideRecordVideo);\n\t}, [&](const MTPDsendMessageRecordAudioAction &) {\n\t\templaceAction(Type::RecordVoice, kStatusShowClientsideRecordVoice);\n\t}, [&](const MTPDsendMessageRecordRoundAction &) {\n\t\templaceAction(Type::RecordRound, kStatusShowClientsideRecordRound);\n\t}, [&](const MTPDsendMessageGeoLocationAction &) {\n\t\templaceAction(\n\t\t\tType::ChooseLocation,\n\t\t\tkStatusShowClientsideChooseLocation);\n\t}, [&](const MTPDsendMessageChooseContactAction &) {\n\t\templaceAction(\n\t\t\tType::ChooseContact,\n\t\t\tkStatusShowClientsideChooseContact);\n\t}, [&](const MTPDsendMessageUploadVideoAction &data) {\n\t\templaceAction(\n\t\t\tType::UploadVideo,\n\t\t\tkStatusShowClientsideUploadVideo,\n\t\t\tdata.vprogress().v);\n\t}, [&](const MTPDsendMessageUploadAudioAction &data) {\n\t\templaceAction(\n\t\t\tType::UploadVoice,\n\t\t\tkStatusShowClientsideUploadVoice,\n\t\t\tdata.vprogress().v);\n\t}, [&](const MTPDsendMessageUploadRoundAction &data) {\n\t\templaceAction(\n\t\t\tType::UploadRound,\n\t\t\tkStatusShowClientsideUploadRound,\n\t\t\tdata.vprogress().v);\n\t}, [&](const MTPDsendMessageUploadPhotoAction &data) {\n\t\templaceAction(\n\t\t\tType::UploadPhoto,\n\t\t\tkStatusShowClientsideUploadPhoto,\n\t\t\tdata.vprogress().v);\n\t}, [&](const MTPDsendMessageUploadDocumentAction &data) {\n\t\templaceAction(\n\t\t\tType::UploadFile,\n\t\t\tkStatusShowClientsideUploadFile,\n\t\t\tdata.vprogress().v);\n\t}, [&](const MTPDsendMessageGamePlayAction &) {\n\t\tconst auto i = _sendActions.find(user);\n\t\tif ((i == end(_sendActions))\n\t\t\t|| (i->second.type == Type::PlayGame)\n\t\t\t|| (i->second.until <= now)) {\n\t\t\templaceAction(Type::PlayGame, kStatusShowClientsidePlayGame);\n\t\t}\n\t}, [&](const MTPDspeakingInGroupCallAction &) {\n\t\t_speaking.emplace_or_assign(\n\t\t\tuser,\n\t\t\tnow + kStatusShowClientsideSpeaking);\n\t}, [&](const MTPDsendMessageHistoryImportAction &) {\n\t}, [&](const MTPDsendMessageChooseStickerAction &) {\n\t\templaceAction(\n\t\t\tType::ChooseSticker,\n\t\t\tkStatusShowClientsideChooseSticker);\n\t}, [&](const MTPDsendMessageEmojiInteraction &) {\n\t\tUnexpected(\"EmojiInteraction here.\");\n\t}, [&](const MTPDsendMessageEmojiInteractionSeen &) {\n\t\t// #TODO interaction\n\t}, [&](const MTPDsendMessageTextDraftAction &) {\n\t}, [&](const MTPDsendMessageCancelAction &) {\n\t\tUnexpected(\"CancelAction here.\");\n\t});\n\treturn updateNeedsAnimating(now, true);\n}\n\nbool SendActionPainter::paint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tstyle::color color,\n\t\tcrl::time ms) {\n\tif (_sendActionAnimation) {\n\t\tconst auto animationWidth = _sendActionAnimation.width();\n\t\tconst auto extraAnimationWidth = _animationLeft\n\t\t\t? animationWidth * 2\n\t\t\t: 0;\n\t\tconst auto left\n\t\t\t= (availableWidth < _animationLeft + extraAnimationWidth)\n\t\t\t\t? 0\n\t\t\t\t: _animationLeft;\n\t\t_sendActionAnimation.paint(\n\t\t\tp,\n\t\t\tcolor,\n\t\t\tleft + x,\n\t\t\ty + st::normalFont->ascent,\n\t\t\touterWidth,\n\t\t\tms);\n\t\t// availableWidth should be the same\n\t\t// if an animation is in the middle of text.\n\t\tif (!left) {\n\t\t\tx += animationWidth;\n\t\t\tavailableWidth -= _animationLeft\n\t\t\t\t? extraAnimationWidth\n\t\t\t\t: animationWidth;\n\t\t}\n\t\tp.setPen(color);\n\t\t_sendActionText.drawElided(p, x, y, availableWidth);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid SendActionPainter::paintSpeaking(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tstyle::color color,\n\t\tcrl::time ms) {\n\tif (_speakingAnimation) {\n\t\t_speakingAnimation.paint(\n\t\t\tp,\n\t\t\tcolor,\n\t\t\tx,\n\t\t\ty,\n\t\t\touterWidth,\n\t\t\tms);\n\t} else {\n\t\tUi::SendActionAnimation::PaintSpeakingIdle(\n\t\t\tp,\n\t\t\tcolor,\n\t\t\tx,\n\t\t\ty,\n\t\t\touterWidth);\n\t}\n}\n\nbool SendActionPainter::updateNeedsAnimating(crl::time now, bool force) {\n\tif (!_weak) {\n\t\treturn false;\n\t}\n\tauto sendActionChanged = false;\n\tauto speakingChanged = false;\n\tfor (auto i = begin(_typing); i != end(_typing);) {\n\t\tif (now >= i->second) {\n\t\t\ti = _typing.erase(i);\n\t\t\tsendActionChanged = true;\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tfor (auto i = begin(_speaking); i != end(_speaking);) {\n\t\tif (now >= i->second) {\n\t\t\ti = _speaking.erase(i);\n\t\t\tspeakingChanged = true;\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tfor (auto i = begin(_sendActions); i != end(_sendActions);) {\n\t\tif (now >= i->second.until) {\n\t\t\ti = _sendActions.erase(i);\n\t\t\tsendActionChanged = true;\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tconst auto wasSpeakingAnimation = !!_speakingAnimation;\n\tif (force || sendActionChanged || speakingChanged) {\n\t\tQString newTypingString;\n\t\tauto animationLeft = 0;\n\t\tauto typingCount = _typing.size();\n\t\tif (typingCount > 2) {\n\t\t\tnewTypingString = tr::lng_many_typing(tr::now, lt_count, typingCount);\n\t\t} else if (typingCount > 1) {\n\t\t\tnewTypingString = tr::lng_users_typing(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tbegin(_typing)->first->firstName,\n\t\t\t\tlt_second_user,\n\t\t\t\t(end(_typing) - 1)->first->firstName);\n\t\t} else if (typingCount) {\n\t\t\tnewTypingString = _history->peer->isUser()\n\t\t\t\t? tr::lng_typing(tr::now)\n\t\t\t\t: tr::lng_user_typing(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tbegin(_typing)->first->firstName);\n\t\t} else if (!_sendActions.empty()) {\n\t\t\t// Handles all actions except game playing.\n\t\t\tusing Type = Api::SendProgressType;\n\t\t\tconst auto sendActionString = [](\n\t\t\t\t\tType type,\n\t\t\t\t\tconst QString &name) -> QString {\n\t\t\t\tswitch (type) {\n\t\t\t\tcase Type::RecordVideo: return name.isEmpty()\n\t\t\t\t\t? tr::lng_send_action_record_video({})\n\t\t\t\t\t: tr::lng_user_action_record_video({}, lt_user, name);\n\t\t\t\tcase Type::UploadVideo: return name.isEmpty()\n\t\t\t\t\t? tr::lng_send_action_upload_video({})\n\t\t\t\t\t: tr::lng_user_action_upload_video({}, lt_user, name);\n\t\t\t\tcase Type::RecordVoice: return name.isEmpty()\n\t\t\t\t\t? tr::lng_send_action_record_audio({})\n\t\t\t\t\t: tr::lng_user_action_record_audio({}, lt_user, name);\n\t\t\t\tcase Type::UploadVoice: return name.isEmpty()\n\t\t\t\t\t? tr::lng_send_action_upload_audio({})\n\t\t\t\t\t: tr::lng_user_action_upload_audio({}, lt_user, name);\n\t\t\t\tcase Type::RecordRound: return name.isEmpty()\n\t\t\t\t\t? tr::lng_send_action_record_round({})\n\t\t\t\t\t: tr::lng_user_action_record_round({}, lt_user, name);\n\t\t\t\tcase Type::UploadRound: return name.isEmpty()\n\t\t\t\t\t? tr::lng_send_action_upload_round({})\n\t\t\t\t\t: tr::lng_user_action_upload_round({}, lt_user, name);\n\t\t\t\tcase Type::UploadPhoto: return name.isEmpty()\n\t\t\t\t\t? tr::lng_send_action_upload_photo({})\n\t\t\t\t\t: tr::lng_user_action_upload_photo({}, lt_user, name);\n\t\t\t\tcase Type::UploadFile: return name.isEmpty()\n\t\t\t\t\t? tr::lng_send_action_upload_file({})\n\t\t\t\t\t: tr::lng_user_action_upload_file({}, lt_user, name);\n\t\t\t\tcase Type::ChooseLocation:\n\t\t\t\tcase Type::ChooseContact: return name.isEmpty()\n\t\t\t\t\t? tr::lng_typing({})\n\t\t\t\t\t: tr::lng_user_typing({}, lt_user, name);\n\t\t\t\tcase Type::ChooseSticker: return name.isEmpty()\n\t\t\t\t\t? tr::lng_send_action_choose_sticker({})\n\t\t\t\t\t: tr::lng_user_action_choose_sticker({}, lt_user, name);\n\t\t\t\tdefault: break;\n\t\t\t\t};\n\t\t\t\treturn QString();\n\t\t\t};\n\t\t\tfor (const auto &[user, action] : _sendActions) {\n\t\t\t\tconst auto isNamed = !_history->peer->isUser();\n\t\t\t\tnewTypingString = sendActionString(\n\t\t\t\t\taction.type,\n\t\t\t\t\tisNamed ? user->firstName : QString());\n\t\t\t\tif (!newTypingString.isEmpty()) {\n\t\t\t\t\t_sendActionAnimation.start(action.type);\n\n\t\t\t\t\t// Add an animation to the middle of text.\n\t\t\t\t\tconst auto &lang = Lang::GetInstance();\n\t\t\t\t\tif (lang.supportChoosingStickerReplacement()\n\t\t\t\t\t\t\t&& (action.type == Type::ChooseSticker)) {\n\t\t\t\t\t\tconst auto index = newTypingString.size()\n\t\t\t\t\t\t\t- lang.rightIndexChoosingStickerReplacement(\n\t\t\t\t\t\t\t\tisNamed);\n\t\t\t\t\t\tanimationLeft = Ui::Text::String(\n\t\t\t\t\t\t\t_st,\n\t\t\t\t\t\t\tnewTypingString.mid(0, index)).maxWidth();\n\n\t\t\t\t\t\tif (!_spacesCount) {\n\t\t\t\t\t\t\t// We have to use QFontMetricsF instead of\n\t\t\t\t\t\t\t// FontData::spacew for more precise calculation.\n\t\t\t\t\t\t\tconst auto mf = QFontMetricsF(_st.font->f);\n\t\t\t\t\t\t\t_spacesCount = base::SafeRound(\n\t\t\t\t\t\t\t\t_sendActionAnimation.widthNoMargins()\n\t\t\t\t\t\t\t\t\t/ mf.horizontalAdvance(' '));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tnewTypingString = newTypingString.replace(\n\t\t\t\t\t\t\tindex,\n\t\t\t\t\t\t\tLang::kChoosingStickerReplacement.utf8().size(),\n\t\t\t\t\t\t\tQString().fill(' ', _spacesCount).constData(),\n\t\t\t\t\t\t\t_spacesCount);\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Everyone in sendActions are playing a game.\n\t\t\tif (newTypingString.isEmpty()) {\n\t\t\t\tint playingCount = _sendActions.size();\n\t\t\t\tif (playingCount > 2) {\n\t\t\t\t\tnewTypingString = tr::lng_many_playing_game(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tplayingCount);\n\t\t\t\t} else if (playingCount > 1) {\n\t\t\t\t\tnewTypingString = tr::lng_users_playing_game(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\tbegin(_sendActions)->first->firstName,\n\t\t\t\t\t\tlt_second_user,\n\t\t\t\t\t\t(end(_sendActions) - 1)->first->firstName);\n\t\t\t\t} else {\n\t\t\t\t\tnewTypingString = _history->peer->isUser()\n\t\t\t\t\t\t? tr::lng_playing_game(tr::now)\n\t\t\t\t\t\t: tr::lng_user_playing_game(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\t\tbegin(_sendActions)->first->firstName);\n\t\t\t\t}\n\t\t\t\t_sendActionAnimation.start(Type::PlayGame);\n\t\t\t}\n\t\t}\n\t\tif (typingCount > 0) {\n\t\t\t_sendActionAnimation.start(Api::SendProgressType::Typing);\n\t\t} else if (newTypingString.isEmpty()) {\n\t\t\t_sendActionAnimation.tryToFinish();\n\t\t}\n\t\tif (_sendActionString != newTypingString) {\n\t\t\t_sendActionString = newTypingString;\n\t\t\t_sendActionText.setText(\n\t\t\t\tst::dialogsTextStyle,\n\t\t\t\t_sendActionString,\n\t\t\t\tUi::NameTextOptions());\n\t\t}\n\t\tif (_animationLeft != animationLeft) {\n\t\t\t_animationLeft = animationLeft;\n\t\t}\n\t\tif (_speaking.empty()) {\n\t\t\t_speakingAnimation.tryToFinish();\n\t\t} else {\n\t\t\t_speakingAnimation.start(Api::SendProgressType::Speaking);\n\t\t}\n\t} else if (_speaking.empty() && _speakingAnimation) {\n\t\t_speakingAnimation.tryToFinish();\n\t}\n\tconst auto sendActionResult = !_typing.empty() || !_sendActions.empty();\n\tconst auto speakingResult = !_speaking.empty() || wasSpeakingAnimation;\n\tif (force\n\t\t|| sendActionChanged\n\t\t|| (sendActionResult && !anim::Disabled())) {\n\t\tconst auto left = 0;\n\t\tconst auto top = Ui::Emoji::GetCustomSkipNormal();\n\t\tconst auto width = _sendActionAnimation.width() + _animationLeft;\n\t\tconst auto height = std::max({\n\t\t\tst::normalFont->height - top,\n\t\t\tst::dialogsMiniPreviewTop + st::dialogsMiniPreview - top,\n\t\t\tUi::Emoji::GetCustomSizeNormal(),\n\t\t});\n\t\t_history->peer->owner().sendActionManager().updateAnimation({\n\t\t\t_topic ? ((Data::Thread*)_topic) : _history,\n\t\t\t{ left, top, width, height },\n\t\t\t(force || sendActionChanged)\n\t\t});\n\t}\n\tif (force\n\t\t|| speakingChanged\n\t\t|| (speakingResult && !anim::Disabled())) {\n\t\t_history->peer->owner().sendActionManager().updateSpeakingAnimation({\n\t\t\t_history\n\t\t});\n\t}\n\treturn sendActionResult || speakingResult;\n}\n\nvoid SendActionPainter::clear(not_null<UserData*> from) {\n\tauto updateAtMs = crl::time(0);\n\tauto i = _typing.find(from);\n\tif (i != _typing.cend()) {\n\t\tupdateAtMs = crl::now();\n\t\ti->second = updateAtMs;\n\t}\n\tauto j = _sendActions.find(from);\n\tif (j != _sendActions.cend()) {\n\t\tif (!updateAtMs) updateAtMs = crl::now();\n\t\tj->second.until = updateAtMs;\n\t}\n\tif (updateAtMs) {\n\t\tupdateNeedsAnimating(updateAtMs, true);\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_send_action.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/send_action_animations.h\"\n#include \"api/api_send_progress.h\"\n\nclass UserData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\nclass ForumTopic;\n} // namespace Data\n\nnamespace Api {\nenum class SendProgressType;\nstruct SendProgress;\n} // namespace Api\n\nnamespace HistoryView {\n\nclass SendActionPainter final {\npublic:\n\texplicit SendActionPainter(not_null<History*> history, MsgId rootId = 0);\n\n\tvoid setTopic(Data::ForumTopic *topic);\n\n\tbool paint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tstyle::color color,\n\t\tcrl::time now);\n\tvoid paintSpeaking(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tstyle::color color,\n\t\tcrl::time now);\n\n\tbool updateNeedsAnimating(\n\t\tcrl::time now,\n\t\tbool force = false);\n\tbool updateNeedsAnimating(\n\t\tnot_null<UserData*> user,\n\t\tconst MTPSendMessageAction &action);\n\tvoid clear(not_null<UserData*> from);\n\nprivate:\n\tconst not_null<History*> _history;\n\tconst MsgId _rootId = 0;\n\tconst base::weak_ptr<Main::Session> _weak;\n\tconst style::TextStyle &_st;\n\tData::ForumTopic *_topic = nullptr;\n\tbase::flat_map<not_null<UserData*>, crl::time> _typing;\n\tbase::flat_map<not_null<UserData*>, crl::time> _speaking;\n\tbase::flat_map<not_null<UserData*>, Api::SendProgress> _sendActions;\n\tQString _sendActionString;\n\tUi::Text::String _sendActionText;\n\tUi::SendActionAnimation _sendActionAnimation;\n\tUi::SendActionAnimation _speakingAnimation;\n\n\tint _animationLeft = 0;\n\tint _spacesCount = 0;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_service_message.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_service_message.h\"\n\n#include \"history/view/media/history_view_media.h\"\n#include \"history/view/reactions/history_view_reactions.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"data/data_abstract_structure.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_todo_list.h\"\n#include \"info/profile/info_profile_cover.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/reaction_fly_animation.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/ui_utility.h\"\n#include \"mainwidget.h\"\n#include \"menu/menu_ttl_validator.h\"\n#include \"data/data_forum_topic.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_info.h\"\n\nnamespace HistoryView {\nnamespace {\n\nTextParseOptions EmptyLineOptions = {\n\tTextParseMultiline, // flags\n\t4096, // maxw\n\t1, // maxh\n\tQt::LayoutDirectionAuto, // lang-dependent\n};\n\nenum CircleMask {\n\tNormalMask     = 0x00,\n\tInvertedMask   = 0x01,\n};\nenum CircleMaskMultiplier {\n\tMaskMultiplier = 0x04,\n};\nenum CornerVerticalSide {\n\tCornerTop      = 0x00,\n\tCornerBottom   = 0x02,\n};\nenum CornerHorizontalSide {\n\tCornerLeft     = 0x00,\n\tCornerRight    = 0x01,\n};\n\nenum class SideStyle {\n\tRounded,\n\tPlain,\n\tInverted,\n};\n\n// Returns amount of pixels already painted vertically (so you can skip them in the complex rect shape).\nint PaintBubbleSide(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tint x,\n\t\tint y,\n\t\tint width,\n\t\tSideStyle style,\n\t\tCornerVerticalSide side) {\n\tif (style == SideStyle::Rounded) {\n\t\tconst auto &corners = st->serviceBgCornersNormal();\n\t\tconst auto left = corners.p[(side == CornerTop) ? 0 : 2];\n\t\tconst auto leftWidth = left.width() / style::DevicePixelRatio();\n\t\tp.drawPixmap(x, y, left);\n\n\t\tconst auto right = corners.p[(side == CornerTop) ? 1 : 3];\n\t\tconst auto rightWidth = right.width() / style::DevicePixelRatio();\n\t\tp.drawPixmap(x + width - rightWidth, y, right);\n\n\t\tconst auto cornerHeight = left.height() / style::DevicePixelRatio();\n\t\tp.fillRect(\n\t\t\tx + leftWidth,\n\t\t\ty,\n\t\t\twidth - leftWidth - rightWidth,\n\t\t\tcornerHeight,\n\t\t\tst->msgServiceBg());\n\t\treturn cornerHeight;\n\t} else if (style == SideStyle::Inverted) {\n\t\t// CornerLeft and CornerRight are inverted in the top part.\n\t\tconst auto &corners = st->serviceBgCornersInverted();\n\t\tconst auto left = corners.p[(side == CornerTop) ? 1 : 2];\n\t\tconst auto leftWidth = left.width() / style::DevicePixelRatio();\n\t\tp.drawPixmap(x - leftWidth, y, left);\n\n\t\tconst auto right = corners.p[(side == CornerTop) ? 0 : 3];\n\t\tp.drawPixmap(x + width, y, right);\n\t}\n\treturn 0;\n}\n\nvoid PaintBubblePart(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tint x,\n\t\tint y,\n\t\tint width,\n\t\tint height,\n\t\tSideStyle topStyle,\n\t\tSideStyle bottomStyle,\n\t\tbool forceShrink = false) {\n\tif ((topStyle == SideStyle::Inverted)\n\t\t|| (bottomStyle == SideStyle::Inverted)\n\t\t|| forceShrink) {\n\t\twidth -= Ui::HistoryServiceMsgInvertedShrink() * 2;\n\t\tx += Ui::HistoryServiceMsgInvertedShrink();\n\t}\n\n\tif (int skip = PaintBubbleSide(p, st, x, y, width, topStyle, CornerTop)) {\n\t\ty += skip;\n\t\theight -= skip;\n\t}\n\tint bottomSize = 0;\n\tif (bottomStyle == SideStyle::Rounded) {\n\t\tbottomSize = Ui::HistoryServiceMsgRadius();\n\t} else if (bottomStyle == SideStyle::Inverted) {\n\t\tbottomSize = Ui::HistoryServiceMsgInvertedRadius();\n\t}\n\tconst auto skip = PaintBubbleSide(\n\t\tp,\n\t\tst,\n\t\tx,\n\t\ty + height - bottomSize,\n\t\twidth,\n\t\tbottomStyle,\n\t\tCornerBottom);\n\tif (skip) {\n\t\theight -= skip;\n\t}\n\n\tp.fillRect(x, y, width, height, st->msgServiceBg());\n}\n\nvoid PaintPreparedDate(\n\t\tPainter &p,\n\t\tconst style::color &bg,\n\t\tconst Ui::CornersPixmaps &corners,\n\t\tconst style::color &fg,\n\t\tconst QString &dateText,\n\t\tint dateTextWidth,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide) {\n\tint left = st::msgServiceMargin.left();\n\tconst auto maxwidth = chatWide\n\t\t? std::min(w, WideChatWidth())\n\t\t: w;\n\tw = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left();\n\n\tleft += (w - dateTextWidth - st::msgServicePadding.left() - st::msgServicePadding.right()) / 2;\n\tint height = st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom();\n\tServiceMessagePainter::PaintBubble(\n\t\tp,\n\t\tbg,\n\t\tcorners,\n\t\tQRect(\n\t\t\tleft,\n\t\t\ty + st::msgServiceMargin.top(),\n\t\t\tdateTextWidth\n\t\t\t\t+ st::msgServicePadding.left()\n\t\t\t\t+ st::msgServicePadding.left(),\n\t\t\theight));\n\n\tp.setFont(st::msgServiceFont);\n\tp.setPen(fg);\n\tp.drawText(\n\t\tleft + st::msgServicePadding.left(),\n\t\t(y\n\t\t\t+ st::msgServiceMargin.top()\n\t\t\t+ st::msgServicePadding.top()\n\t\t\t+ st::msgServiceFont->ascent),\n\t\tdateText);\n}\n\nbool NeedAboutGroup(not_null<History*> history) {\n\tif (const auto chat = history->peer->asChat()) {\n\t\treturn chat->amCreator();\n\t} else if (const auto channel = history->peer->asMegagroup()) {\n\t\treturn channel->amCreator();\n\t}\n\treturn false;\n}\n\nvoid SetText(Ui::Text::String &text, const QString &content) {\n\ttext.setText(st::serviceTextStyle, content, EmptyLineOptions);\n}\n\n[[nodiscard]] Ui::BubbleRounding KeyboardRounding() {\n\treturn Ui::BubbleRounding{\n\t\t.topLeft = Ui::BubbleCornerRounding::Large,\n\t\t.topRight = Ui::BubbleCornerRounding::Large,\n\t\t.bottomLeft = Ui::BubbleCornerRounding::Large,\n\t\t.bottomRight = Ui::BubbleCornerRounding::Large,\n\t};\n}\n\n} // namespace\n\nint WideChatWidth() {\n\treturn st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left();\n}\n\nvoid ServiceMessagePainter::PaintDate(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tconst QDateTime &date,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide) {\n\tPaintDate(\n\t\tp,\n\t\tst,\n\t\tlangDayOfMonthFull(date.date()),\n\t\ty,\n\t\tw,\n\t\tchatWide);\n}\n\nvoid ServiceMessagePainter::PaintDate(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tconst QString &dateText,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide) {\n\tPaintDate(\n\t\tp,\n\t\tst,\n\t\tdateText,\n\t\tst::msgServiceFont->width(dateText),\n\t\ty,\n\t\tw,\n\t\tchatWide);\n}\n\nvoid ServiceMessagePainter::PaintDate(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tconst QString &dateText,\n\t\tint dateTextWidth,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide) {\n\tPaintPreparedDate(\n\t\tp,\n\t\tst->msgServiceBg(),\n\t\tst->serviceBgCornersNormal(),\n\t\tst->msgServiceFg(),\n\t\tdateText,\n\t\tdateTextWidth,\n\t\ty,\n\t\tw,\n\t\tchatWide);\n}\n\nvoid ServiceMessagePainter::PaintDate(\n\t\tPainter &p,\n\t\tconst style::color &bg,\n\t\tconst Ui::CornersPixmaps &corners,\n\t\tconst style::color &fg,\n\t\tconst QString &dateText,\n\t\tint dateTextWidth,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide) {\n\tPaintPreparedDate(\n\t\tp,\n\t\tbg,\n\t\tcorners,\n\t\tfg,\n\t\tdateText,\n\t\tdateTextWidth,\n\t\ty,\n\t\tw,\n\t\tchatWide);\n}\n\nvoid ServiceMessagePainter::PaintBubble(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tQRect rect) {\n\tPaintBubble(p, st->msgServiceBg(), st->serviceBgCornersNormal(), rect);\n}\n\nvoid ServiceMessagePainter::PaintBubble(\n\t\tPainter &p,\n\t\tconst style::color &bg,\n\t\tconst Ui::CornersPixmaps &corners,\n\t\tQRect rect) {\n\tUi::FillRoundRect(p, rect, bg, corners);\n}\n\nvoid ServiceMessagePainter::PaintComplexBubble(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tint left,\n\t\tint width,\n\t\tconst Ui::Text::String &text,\n\t\tconst QRect &textRect) {\n\tconst auto lineWidths = CountLineWidths(text, textRect);\n\n\tint y = st::msgServiceMargin.top(), previousRichWidth = 0;\n\tbool previousShrink = false, forceShrink = false;\n\tSideStyle topStyle = SideStyle::Rounded, bottomStyle;\n\tfor (int i = 0, count = lineWidths.size(); i < count; ++i) {\n\t\tconst auto lineWidth = lineWidths[i];\n\t\tif (i + 1 < count) {\n\t\t\tconst auto nextLineWidth = lineWidths[i + 1];\n\t\t\tif (nextLineWidth > lineWidth) {\n\t\t\t\tbottomStyle = SideStyle::Inverted;\n\t\t\t} else if (nextLineWidth < lineWidth) {\n\t\t\t\tbottomStyle = SideStyle::Rounded;\n\t\t\t} else {\n\t\t\t\tbottomStyle = SideStyle::Plain;\n\t\t\t}\n\t\t} else {\n\t\t\tbottomStyle = SideStyle::Rounded;\n\t\t}\n\n\t\tauto richWidth = lineWidth + st::msgServicePadding.left() + st::msgServicePadding.right();\n\t\tauto richHeight = st::msgServiceFont->height;\n\t\tif (topStyle == SideStyle::Rounded) {\n\t\t\trichHeight += st::msgServicePadding.top();\n\t\t} else if (topStyle == SideStyle::Inverted) {\n\t\t\trichHeight -= st::msgServicePadding.bottom();\n\t\t}\n\t\tif (bottomStyle == SideStyle::Rounded) {\n\t\t\trichHeight += st::msgServicePadding.bottom();\n\t\t} else if (bottomStyle == SideStyle::Inverted) {\n\t\t\trichHeight -= st::msgServicePadding.top();\n\t\t}\n\t\tforceShrink = previousShrink && (richWidth == previousRichWidth);\n\t\tPaintBubblePart(\n\t\t\tp,\n\t\t\tst,\n\t\t\tleft + ((width - richWidth) / 2),\n\t\t\ty,\n\t\t\trichWidth,\n\t\t\trichHeight,\n\t\t\ttopStyle,\n\t\t\tbottomStyle,\n\t\t\tforceShrink);\n\t\ty += richHeight;\n\n\t\tpreviousShrink = forceShrink || (topStyle == SideStyle::Inverted) || (bottomStyle == SideStyle::Inverted);\n\t\tpreviousRichWidth = richWidth;\n\n\t\tif (bottomStyle == SideStyle::Inverted) {\n\t\t\ttopStyle = SideStyle::Rounded;\n\t\t} else if (bottomStyle == SideStyle::Rounded) {\n\t\t\ttopStyle = SideStyle::Inverted;\n\t\t} else {\n\t\t\ttopStyle = SideStyle::Plain;\n\t\t}\n\t}\n}\n\nstd::vector<int> ServiceMessagePainter::CountLineWidths(\n\t\tconst Ui::Text::String &text,\n\t\tconst QRect &textRect) {\n\tconst auto linesCount = qMax(\n\t\ttextRect.height() / st::msgServiceFont->height,\n\t\t1);\n\tauto result = text.countLineWidths(textRect.width(), {\n\t\t.reserve = linesCount,\n\t});\n\n\tconst auto minDelta = 2 * (Ui::HistoryServiceMsgRadius()\n\t\t+ Ui::HistoryServiceMsgInvertedRadius()\n\t\t- Ui::HistoryServiceMsgInvertedShrink());\n\tfor (int i = 0, count = result.size(); i != count; ++i) {\n\t\tauto width = qMax(result[i], 0);\n\t\tif (i > 0) {\n\t\t\tconst auto widthBefore = result[i - 1];\n\t\t\tif (width < widthBefore && width + minDelta > widthBefore) {\n\t\t\t\twidth = widthBefore;\n\t\t\t}\n\t\t}\n\t\tif (i + 1 < count) {\n\t\t\tconst auto widthAfter = result[i + 1];\n\t\t\tif (width < widthAfter && width + minDelta > widthAfter) {\n\t\t\t\twidth = widthAfter;\n\t\t\t}\n\t\t}\n\t\tif (width > result[i]) {\n\t\t\tresult[i] = width;\n\t\t\tif (i > 0) {\n\t\t\t\tint widthBefore = result[i - 1];\n\t\t\t\tif (widthBefore != width\n\t\t\t\t\t&& widthBefore < width + minDelta\n\t\t\t\t\t&& widthBefore + minDelta > width) {\n\t\t\t\t\ti -= 2;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nService::Service(\n\tnot_null<ElementDelegate*> delegate,\n\tnot_null<HistoryItem*> data,\n\tElement *replacing)\n: Element(delegate, data, replacing, Flag::ServiceMessage) {\n\tsetupReactions(replacing);\n}\n\nvoid Service::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) {\n\tif (const auto markup = data()->Get<HistoryMessageReplyMarkup>()) {\n\t\tif (const auto keyboard = markup->inlineKeyboard.get()) {\n\t\t\tkeyboard->clickHandlerPressedChanged(\n\t\t\t\thandler,\n\t\t\t\tpressed,\n\t\t\t\tKeyboardRounding());\n\t\t}\n\t}\n\tElement::clickHandlerPressedChanged(handler, pressed);\n}\n\nQRect Service::innerGeometry() const {\n\treturn countGeometry();\n}\n\nbool Service::consumeHorizontalScroll(QPoint position, int delta) {\n\tif (const auto media = this->media()) {\n\t\treturn media->consumeHorizontalScroll(position, delta);\n\t}\n\treturn false;\n}\n\nQRect Service::countGeometry() const {\n\tauto result = QRect(0, 0, width(), height());\n\tif (delegate()->elementChatMode() == ElementChatMode::Wide) {\n\t\tresult.setWidth(qMin(result.width(), st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));\n\t}\n\tauto margins = st::msgServiceMargin;\n\tmargins.setTop(marginTop());\n\treturn result.marginsRemoved(margins);\n}\n\nvoid Service::animateReaction(Ui::ReactionFlyAnimationArgs &&args) {\n\tauto g = countGeometry();\n\tif (g.width() < 1 || isHidden()) {\n\t\treturn;\n\t}\n\tconst auto repainter = [=] { repaint(); };\n\n\tif (_reactions) {\n\t\tconst auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height();\n\t\tconst auto reactionsLeft = 0;\n\t\tg.setHeight(g.height() - reactionsHeight);\n\t\tconst auto reactionsPosition = QPoint(reactionsLeft + g.left(), g.top() + g.height() + st::mediaInBubbleSkip);\n\t\t_reactions->animate(args.translated(-reactionsPosition), repainter);\n\t}\n}\n\nQSize Service::performCountCurrentSize(int newWidth) {\n\tauto newHeight = marginTop();\n\n\tdata()->resolveDependent();\n\n\tif (const auto service = Get<ServicePreMessage>()) {\n\t\tservice->resizeToWidth(newWidth, delegate()->elementChatMode());\n\t}\n\n\tif (isHidden()) {\n\t\treturn { newWidth, newHeight };\n\t}\n\tconst auto media = this->media();\n\tconst auto mediaDisplayed = media && media->isDisplayed();\n\tauto contentWidth = newWidth;\n\tif (delegate()->elementChatMode() == ElementChatMode::Wide) {\n\t\taccumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left());\n\t}\n\tcontentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins\n\tif (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) {\n\t\tcontentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1;\n\t}\n\tif (mediaDisplayed && media->hideServiceText()) {\n\t\tnewHeight += media->resizeGetHeight(newWidth) + marginBottom();\n\t} else if (!text().isEmpty()) {\n\t\tauto nwidth = qMax(contentWidth - st::msgServicePadding.left() - st::msgServicePadding.right(), 0);\n\t\tnewHeight += (contentWidth >= maxWidth())\n\t\t\t? minHeight()\n\t\t\t: textHeightFor(nwidth);\n\t\tnewHeight += st::msgServicePadding.top() + st::msgServicePadding.bottom();\n\t\tif (mediaDisplayed) {\n\t\t\tconst auto mediaWidth = std::min(media->maxWidth(), nwidth);\n\t\t\tnewHeight += st::msgServiceMargin.top()\n\t\t\t\t+ media->resizeGetHeight(mediaWidth);\n\t\t}\n\t\tnewHeight += marginBottom();\n\t} else {\n\t\tnewHeight -= st::msgServiceMargin.top();\n\t}\n\n\tif (_reactions) {\n\t\tnewHeight += st::mediaInBubbleSkip\n\t\t\t+ _reactions->resizeGetHeight(contentWidth);\n\t\tif (hasRightLayout()) {\n\t\t\t_reactions->flipToRight();\n\t\t}\n\t}\n\n\tconst auto item = data();\n\tif (const auto keyboard = item->inlineReplyKeyboard()) {\n\t\tconst auto keyboardWidth = mediaDisplayed ? media->width() : contentWidth;\n\t\tconst auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight();\n\t\tnewHeight += keyboardHeight;\n\t\tkeyboard->resize(keyboardWidth, keyboardHeight - st::msgBotKbButton.margin);\n\t}\n\n\treturn { newWidth, newHeight };\n}\n\nQSize Service::performCountOptimalSize() {\n\tconst auto markup = data()->inlineReplyMarkup();\n\tvalidateText();\n\tvalidateInlineKeyboard(markup);\n\n\tif (_reactions) {\n\t\t_reactions->initDimensions();\n\t}\n\n\tif (const auto media = this->media()) {\n\t\tmedia->initDimensions();\n\t\tif (media->hideServiceText()) {\n\t\t\treturn { media->maxWidth(), media->minHeight() };\n\t\t}\n\t}\n\tauto maxWidth = text().maxWidth() + st::msgServicePadding.left() + st::msgServicePadding.right();\n\tauto minHeight = text().minHeight();\n\treturn { maxWidth, minHeight };\n}\n\nbool Service::isHidden() const {\n\treturn Element::isHidden();\n}\n\nint Service::marginTop() const {\n\tauto result = isHidden() ? 0 : st::msgServiceMargin.top();\n\tresult += displayedDateHeight();\n\tif (const auto bar = Get<UnreadBar>()) {\n\t\tresult += bar->height();\n\t}\n\tif (const auto bar = Get<ForumThreadBar>()) {\n\t\tresult += bar->height();\n\t}\n\tif (const auto service = Get<ServicePreMessage>()) {\n\t\tresult += service->height;\n\t}\n\tif (const auto margins = Get<ViewAddedMargins>()) {\n\t\tresult += margins->top;\n\t}\n\treturn result;\n}\n\nint Service::marginBottom() const {\n\tauto result = st::msgServiceMargin.bottom();\n\tif (const auto margins = Get<ViewAddedMargins>()) {\n\t\tresult += margins->bottom;\n\t}\n\treturn result;\n}\n\nvoid Service::draw(Painter &p, const PaintContext &context) const {\n\tauto g = countGeometry();\n\tif (g.width() < 1) {\n\t\treturn;\n\t}\n\n\tconst auto st = context.st;\n\tif (const auto bar = Get<UnreadBar>()) {\n\t\tauto unreadbarh = bar->height();\n\t\tauto aboveh = 0;\n\t\tif (const auto date = Get<DateBadge>()) {\n\t\t\taboveh += date->height();\n\t\t}\n\t\tif (const auto bar = Get<ForumThreadBar>()) {\n\t\t\taboveh += bar->height();\n\t\t}\n\t\tif (context.clip.intersects(QRect(0, aboveh, width(), unreadbarh))) {\n\t\t\tp.translate(0, aboveh);\n\t\t\tbar->paint(\n\t\t\t\tp,\n\t\t\t\tcontext,\n\t\t\t\t0,\n\t\t\t\twidth(),\n\t\t\t\tdelegate()->elementChatMode());\n\t\t\tp.translate(0, -aboveh);\n\t\t}\n\t}\n\n\tif (const auto service = Get<ServicePreMessage>()) {\n\t\tservice->paint(p, context, g, delegate()->elementChatMode());\n\t}\n\n\tif (isHidden()) {\n\t\treturn;\n\t}\n\n\tpaintHighlight(p, context, g.height());\n\n\tp.setTextPalette(st->serviceTextPalette());\n\n\tconst auto media = this->media();\n\tconst auto mediaDisplayed = media && media->isDisplayed();\n\tconst auto onlyMedia = (mediaDisplayed && media->hideServiceText());\n\n\tif (_reactions) {\n\t\tconst auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height();\n\t\tconst auto reactionsLeft = 0;\n\t\tg.setHeight(g.height() - reactionsHeight);\n\t\tconst auto reactionsPosition = QPoint(reactionsLeft + g.left(), g.top() + g.height() + st::mediaInBubbleSkip);\n\t\tp.translate(reactionsPosition);\n\t\tprepareCustomEmojiPaint(p, context, *_reactions);\n\t\t_reactions->paint(p, context, g.width(), context.clip.translated(-reactionsPosition));\n\t\tif (context.reactionInfo) {\n\t\t\tcontext.reactionInfo->position = reactionsPosition;\n\t\t}\n\t\tp.translate(-reactionsPosition);\n\t}\n\n\tconst auto item = data();\n\tconst auto keyboard = item->inlineReplyKeyboard();\n\tif (keyboard) {\n\t\t// We need to count geometry without keyboard for bubble selection\n\t\t// intervals counting below.\n\t\tconst auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight();\n\t\tg.setHeight(g.height() - keyboardHeight);\n\n\t\tconst auto keyboardWidth = mediaDisplayed ? media->width() : g.width();\n\t\tconst auto keyboardPosition = QPoint(\n\t\t\tg.left() + (g.width() - keyboardWidth) / 2,\n\t\t\tg.top() + g.height() + st::msgBotKbButton.margin);\n\t\tp.translate(keyboardPosition);\n\t\tkeyboard->paint(\n\t\t\tp,\n\t\t\tcontext.st,\n\t\t\tKeyboardRounding(),\n\t\t\tkeyboardWidth,\n\t\t\tcontext.clip.translated(-keyboardPosition),\n\t\t\tcontext.paused);\n\t\tp.translate(-keyboardPosition);\n\t}\n\n\tif (!onlyMedia) {\n\t\tconst auto mediaSkip = mediaDisplayed ? (st::msgServiceMargin.top() + media->height()) : 0;\n\t\tconst auto trect = QRect(g.left(), g.top(), g.width(), g.height() - mediaSkip)\n\t\t\t- st::msgServicePadding;\n\n\t\tp.translate(0, g.top() - st::msgServiceMargin.top());\n\t\tServiceMessagePainter::PaintComplexBubble(\n\t\t\tp,\n\t\t\tcontext.st,\n\t\t\tg.left(),\n\t\t\tg.width(),\n\t\t\ttext(),\n\t\t\ttrect);\n\t\tp.translate(0, -g.top() + st::msgServiceMargin.top());\n\n\t\tp.setBrush(Qt::NoBrush);\n\t\tp.setPen(st->msgServiceFg());\n\t\tp.setFont(st::msgServiceFont);\n\t\tprepareCustomEmojiPaint(p, context, text());\n\t\ttext().draw(p, {\n\t\t\t.position = trect.topLeft(),\n\t\t\t.availableWidth = trect.width(),\n\t\t\t.align = style::al_top,\n\t\t\t.palette = &st->serviceTextPalette(),\n\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t.now = context.now,\n\t\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t\t.fullWidthSelection = false,\n\t\t\t.selection = context.selection,\n\t\t});\n\t}\n\tif (mediaDisplayed) {\n\t\tconst auto left = g.left() + (g.width() - media->width()) / 2;\n\t\tconst auto top = g.top() + (onlyMedia ? 0 : (g.height() - media->height()));\n\t\tconst auto position = QPoint(left, top);\n\t\tp.translate(position);\n\t\tmedia->draw(p, context.translated(-position).withSelection({}));\n\t\tp.translate(-position);\n\t}\n}\n\nPointState Service::pointState(QPoint point) const {\n\tconst auto media = this->media();\n\tconst auto mediaDisplayed = media && media->isDisplayed();\n\n\tauto g = countGeometry();\n\tif (g.width() < 1 || isHidden()) {\n\t\treturn PointState::Outside;\n\t}\n\n\tif (mediaDisplayed) {\n\t\tconst auto centerPadding = (g.width() - media->width()) / 2;\n\t\tconst auto r = g - QMargins(centerPadding, 0, centerPadding, 0);\n\t\tif (!r.contains(point)) {\n\t\t\tg.setHeight(g.height()\n\t\t\t\t- (st::msgServiceMargin.top() + media->height()));\n\t\t}\n\t}\n\treturn g.contains(point) ? PointState::Inside : PointState::Outside;\n}\n\nTextState Service::textState(QPoint point, StateRequest request) const {\n\tconst auto item = data();\n\tconst auto media = this->media();\n\tconst auto mediaDisplayed = media && media->isDisplayed();\n\tconst auto onlyMedia = (mediaDisplayed && media->hideServiceText());\n\n\tauto result = TextState(item);\n\n\tauto g = countGeometry();\n\tif (g.width() < 1 || isHidden()) {\n\t\treturn result;\n\t}\n\n\tif (const auto service = Get<ServicePreMessage>()) {\n\t\tresult.link = service->textState(point, request, g);\n\t\tif (result.link) {\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tif (_reactions) {\n\t\tconst auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height();\n\t\tconst auto reactionsLeft = 0;\n\t\tg.setHeight(g.height() - reactionsHeight);\n\t\tconst auto reactionsPosition = QPoint(reactionsLeft + g.left(), g.top() + g.height() + st::mediaInBubbleSkip);\n\t\tif (_reactions->getState(point - reactionsPosition, &result)) {\n\t\t\t//result.symbol += visibleMediaTextLen + visibleTextLen;\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tauto keyboard = item->inlineReplyKeyboard();\n\tauto keyboardHeight = 0;\n\tif (keyboard) {\n\t\tkeyboardHeight = keyboard->naturalHeight();\n\t\tg.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight);\n\n\t\tif (item->isHistoryEntry()) {\n\t\t\tconst auto keyboardWidth = mediaDisplayed ? media->width() : g.width();\n\t\t\tconst auto keyboardPosition = QPoint(\n\t\t\t\tg.left() + (g.width() - keyboardWidth) / 2,\n\t\t\t\tg.top() + g.height() + st::msgBotKbButton.margin);\n\t\t\tif (QRect(keyboardPosition, QSize(keyboardWidth, keyboardHeight)).contains(point)) {\n\t\t\t\tresult.link = keyboard->getLink(point - keyboardPosition);\n\t\t\t\tif (result.link) {\n\t\t\t\t\treturn result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (onlyMedia) {\n\t\treturn media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->width()) / 2, g.top()), request);\n\t} else if (mediaDisplayed) {\n\t\tg.setHeight(g.height() - (st::msgServiceMargin.top() + media->height()));\n\t}\n\tconst auto mediaLeft = st::msgServiceMargin.left()\n\t\t+ (media ? ((g.width() - media->width()) / 2) : 0);\n\tconst auto mediaTop = g.top()\n\t\t+ g.height()\n\t\t+ st::msgServiceMargin.top();\n\tconst auto mediaPoint = point - QPoint(mediaLeft, mediaTop);\n\tauto trect = g.marginsAdded(-st::msgServicePadding);\n\tif (trect.contains(point)) {\n\t\tauto textRequest = request.forText();\n\t\ttextRequest.align = style::al_center;\n\t\tresult = TextState(item, text().getState(\n\t\t\tpoint - trect.topLeft(),\n\t\t\ttrect.width(),\n\t\t\ttextRequest));\n\t\tif (!result.link\n\t\t\t&& result.cursor == CursorState::Text\n\t\t\t&& g.contains(point)) {\n\t\t\tif (const auto gamescore = item->Get<HistoryServiceGameScore>()) {\n\t\t\t\tresult.link = gamescore->lnk;\n\t\t\t} else if (const auto payment = item->Get<HistoryServicePayment>()) {\n\t\t\t\tresult.link = payment->invoiceLink;\n\t\t\t} else if (const auto call = item->Get<HistoryServiceOngoingCall>()) {\n\t\t\t\tconst auto peer = history()->peer;\n\t\t\t\tif (PeerHasThisCall(peer, call->id).value_or(false)) {\n\t\t\t\t\tresult.link = call->link;\n\t\t\t\t}\n\t\t\t} else if (const auto theme = item->Get<HistoryServiceChatThemeChange>()) {\n\t\t\t\tresult.link = theme->link;\n\t\t\t} else if (const auto ttl = item->Get<HistoryServiceTTLChange>()) {\n\t\t\t\tif (TTLMenu::TTLValidator(nullptr, history()->peer).can()) {\n\t\t\t\t\tresult.link = ttl->link;\n\t\t\t\t}\n\t\t\t} else if (const auto same = item->Get<HistoryServiceSameBackground>()) {\n\t\t\t\tresult.link = same->lnk;\n\t\t\t} else if (const auto results = item->Get<HistoryServiceGiveawayResults>()) {\n\t\t\t\tresult.link = results->lnk;\n\t\t\t} else if (const auto custom = item->Get<HistoryServiceCustomLink>()) {\n\t\t\t\tresult.link = custom->link;\n\t\t\t} else if (const auto payment = item->Get<HistoryServicePaymentRefund>()) {\n\t\t\t\tresult.link = payment->link;\n\t\t\t} else if (const auto done = item->Get<HistoryServiceTodoCompletions>()) {\n\t\t\t\tresult.link = done->lnk;\n\t\t\t} else if (const auto append = item->Get<HistoryServiceTodoAppendTasks>()) {\n\t\t\t\tresult.link = append->lnk;\n\t\t\t} else if (const auto pollAppend = item->Get<HistoryServicePollAppendAnswer>()) {\n\t\t\t\tresult.link = pollAppend->lnk;\n\t\t\t} else if (const auto pollDelete = item->Get<HistoryServicePollDeleteAnswer>()) {\n\t\t\t\tresult.link = pollDelete->lnk;\n\t\t\t} else if (const auto finish = item->Get<HistoryServiceSuggestFinish>()) {\n\t\t\t\tresult.link = finish->lnk;\n\t\t\t} else if (media && data()->showSimilarChannels()) {\n\t\t\t\tresult = media->textState(mediaPoint, request);\n\t\t\t}\n\t\t}\n\t} else if (mediaDisplayed && point.y() >= mediaTop) {\n\t\tresult = media->textState(mediaPoint, request);\n\t}\n\treturn result;\n}\n\nvoid Service::updatePressed(QPoint point) {\n}\n\nTextForMimeData Service::selectedText(TextSelection selection) const {\n\treturn text().toTextForMimeData(selection);\n}\n\nSelectedQuote Service::selectedQuote(TextSelection selection) const {\n\treturn {};\n}\n\nTextSelection Service::selectionFromQuote(\n\t\tconst SelectedQuote &quote) const {\n\treturn {};\n}\n\nTextSelection Service::adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const {\n\treturn text().adjustSelection(selection, type);\n}\n\nEmptyPainter::EmptyPainter(not_null<History*> history)\n: _history(history)\n, _header(st::msgMinWidth)\n, _text(st::msgMinWidth) {\n\tif (NeedAboutGroup(_history)) {\n\t\tfillAboutGroup();\n\t}\n}\n\nEmptyPainter::EmptyPainter(\n\tnot_null<Data::ForumTopic*> topic,\n\tFn<bool()> paused,\n\tFn<void()> update)\n: _history(topic->history())\n, _topic(topic)\n, _icon(\n\tstd::make_unique<Info::Profile::TopicIconView>(\n\t\ttopic,\n\t\tpaused,\n\t\tupdate,\n\t\tst::msgServiceFg))\n, _header(st::msgMinWidth)\n, _text(st::msgMinWidth) {\n\tfillAboutTopic();\n}\n\nEmptyPainter::~EmptyPainter() = default;\n\nvoid EmptyPainter::fillAboutGroup() {\n\tconst auto phrases = {\n\t\ttr::lng_group_about1(tr::now),\n\t\ttr::lng_group_about2(tr::now),\n\t\ttr::lng_group_about3(tr::now),\n\t\ttr::lng_group_about4(tr::now),\n\t};\n\tSetText(_header, tr::lng_group_about_header(tr::now));\n\tSetText(_text, tr::lng_group_about_text(tr::now));\n\tfor (const auto &text : phrases) {\n\t\t_phrases.emplace_back(st::msgMinWidth);\n\t\tSetText(_phrases.back(), text);\n\t}\n}\n\nvoid EmptyPainter::fillAboutTopic() {\n\tSetText(_header, _topic->my()\n\t\t? tr::lng_forum_topic_created_title_my(tr::now)\n\t\t: tr::lng_forum_topic_created_title(tr::now));\n\tSetText(_text, _topic->my()\n\t\t? tr::lng_forum_topic_created_body_my(tr::now)\n\t\t: tr::lng_forum_topic_created_body(tr::now));\n}\n\nvoid EmptyPainter::paint(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tint width,\n\t\tint height) {\n\tif (_phrases.empty() && _text.isEmpty()) {\n\t\treturn;\n\t}\n\tconstexpr auto kMaxTextLines = 3;\n\tconst auto maxPhraseWidth = _phrases.empty()\n\t\t? 0\n\t\t: ranges::max_element(\n\t\t\t_phrases,\n\t\t\tranges::less(),\n\t\t\t&Ui::Text::String::maxWidth)->maxWidth();\n\n\tconst auto &font = st::serviceTextStyle.font;\n\tconst auto maxBubbleWidth = width - 2 * st::historyGroupAboutMargin;\n\tconst auto padding = st::historyGroupAboutPadding;\n\tconst auto bubbleWidth = std::min(\n\t\tmaxBubbleWidth,\n\t\tstd::max({\n\t\t\tmaxPhraseWidth + st::historyGroupAboutBulletSkip,\n\t\t\t_header.maxWidth(),\n\t\t\t_text.maxWidth() }) + padding.left() + padding.right());\n\tconst auto innerWidth = bubbleWidth - padding.left() - padding.right();\n\tconst auto textHeight = [&](const Ui::Text::String &text) {\n\t\treturn std::min(\n\t\t\ttext.countHeight(innerWidth),\n\t\t\tkMaxTextLines * font->height);\n\t};\n\tconst auto iconHeight = _icon\n\t\t? st::infoTopicCover.photo.size.height()\n\t\t: 0;\n\tconst auto bubbleHeight = padding.top()\n\t\t+ (_icon ? (iconHeight + st::historyGroupAboutHeaderSkip) : 0)\n\t\t+ textHeight(_header)\n\t\t+ st::historyGroupAboutHeaderSkip\n\t\t+ textHeight(_text)\n\t\t+ st::historyGroupAboutTextSkip\n\t\t+ ranges::accumulate(_phrases, 0, ranges::plus(), textHeight)\n\t\t+ st::historyGroupAboutSkip * std::max(int(_phrases.size()) - 1, 0)\n\t\t+ padding.bottom();\n\tconst auto bubbleLeft = (width - bubbleWidth) / 2;\n\tconst auto bubbleTop = (height - bubbleHeight) / 2;\n\n\tServiceMessagePainter::PaintBubble(\n\t\tp,\n\t\tst->msgServiceBg(),\n\t\tst->serviceBgCornersNormal(),\n\t\tQRect(bubbleLeft, bubbleTop, bubbleWidth, bubbleHeight));\n\n\tp.setPen(st->msgServiceFg());\n\tp.setBrush(st->msgServiceFg());\n\n\tconst auto left = bubbleLeft + padding.left();\n\tauto top = bubbleTop + padding.top();\n\n\tif (_icon) {\n\t\t_icon->paintInRect(\n\t\t\tp,\n\t\t\tQRect(bubbleLeft, top, bubbleWidth, iconHeight),\n\t\t\tst->msgServiceFg()->c);\n\t\ttop += iconHeight + st::historyGroupAboutHeaderSkip;\n\t}\n\n\t_header.drawElided(\n\t\tp,\n\t\tleft,\n\t\ttop,\n\t\tinnerWidth,\n\t\tkMaxTextLines,\n\t\tstyle::al_top);\n\ttop += textHeight(_header) + st::historyGroupAboutHeaderSkip;\n\n\t_text.drawElided(\n\t\tp,\n\t\tleft,\n\t\ttop,\n\t\tinnerWidth,\n\t\tkMaxTextLines,\n\t\t_topic ? style::al_top : style::al_topleft);\n\ttop += textHeight(_text) + st::historyGroupAboutTextSkip;\n\n\tfor (const auto &text : _phrases) {\n\t\tp.setPen(st->msgServiceFg());\n\t\ttext.drawElided(\n\t\t\tp,\n\t\t\tleft + st::historyGroupAboutBulletSkip,\n\t\t\ttop,\n\t\t\tinnerWidth,\n\t\t\tkMaxTextLines);\n\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawEllipse(\n\t\t\tleft,\n\t\t\ttop + (font->height - st::mediaUnreadSize) / 2,\n\t\t\tst::mediaUnreadSize,\n\t\t\tst::mediaUnreadSize);\n\t\ttop += textHeight(text) + st::historyGroupAboutSkip;\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_service_message.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/history_view_element.h\"\n\nclass HistoryItem;\n\nnamespace Ui {\nclass ChatStyle;\nstruct CornersPixmaps;\n} // namespace Ui\n\nnamespace Data {\nclass ForumTopic;\n} // namespace Data\n\nnamespace Info::Profile {\nclass TopicIconView;\n} // namespace Info::Profile\n\nnamespace HistoryView {\n\nclass Service final : public Element {\npublic:\n\tService(\n\t\tnot_null<ElementDelegate*> delegate,\n\t\tnot_null<HistoryItem*> data,\n\t\tElement *replacing);\n\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) override;\n\n\tint marginTop() const override;\n\tint marginBottom() const override;\n\tbool isHidden() const override;\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tPointState pointState(QPoint point) const override;\n\tTextState textState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\tvoid updatePressed(QPoint point) override;\n\tTextForMimeData selectedText(TextSelection selection) const override;\n\tSelectedQuote selectedQuote(TextSelection selection) const override;\n\tTextSelection selectionFromQuote(\n\t\tconst SelectedQuote &quote) const override;\n\tTextSelection adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const override;\n\n\tQRect innerGeometry() const override;\n\n\tbool consumeHorizontalScroll(QPoint position, int delta) override;\n\n\tvoid animateReaction(Ui::ReactionFlyAnimationArgs &&args) override;\n\nprivate:\n\t[[nodiscard]] QRect countGeometry() const;\n\n\tQSize performCountOptimalSize() override;\n\tQSize performCountCurrentSize(int newWidth) override;\n\n};\n\nint WideChatWidth();\n\nclass ServiceMessagePainter {\npublic:\n\tstatic void PaintDate(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tconst QDateTime &date,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide);\n\tstatic void PaintDate(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tconst QString &dateText,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide);\n\tstatic void PaintDate(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tconst QString &dateText,\n\t\tint dateTextWidth,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide);\n\tstatic void PaintDate(\n\t\tPainter &p,\n\t\tconst style::color &bg,\n\t\tconst Ui::CornersPixmaps &corners,\n\t\tconst style::color &fg,\n\t\tconst QString &dateText,\n\t\tint dateTextWidth,\n\t\tint y,\n\t\tint w,\n\t\tbool chatWide);\n\n\tstatic void PaintBubble(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tQRect rect);\n\tstatic void PaintBubble(\n\t\tPainter &p,\n\t\tconst style::color &bg,\n\t\tconst Ui::CornersPixmaps &corners,\n\t\tQRect rect);\n\n\tstatic void PaintComplexBubble(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tint left,\n\t\tint width,\n\t\tconst Ui::Text::String &text,\n\t\tconst QRect &textRect);\n\nprivate:\n\tstatic std::vector<int> CountLineWidths(\n\t\tconst Ui::Text::String &text,\n\t\tconst QRect &textRect);\n\n};\n\nclass EmptyPainter {\npublic:\n\texplicit EmptyPainter(not_null<History*> history);\n\tEmptyPainter(\n\t\tnot_null<Data::ForumTopic*> topic,\n\t\tFn<bool()> paused,\n\t\tFn<void()> update);\n\t~EmptyPainter();\n\n\tvoid paint(\n\t\tPainter &p,\n\t\tnot_null<const Ui::ChatStyle*> st,\n\t\tint width,\n\t\tint height);\n\nprivate:\n\tvoid fillAboutGroup();\n\tvoid fillAboutTopic();\n\n\tnot_null<History*> _history;\n\tData::ForumTopic *_topic = nullptr;\n\tstd::unique_ptr<Info::Profile::TopicIconView> _icon;\n\tUi::Text::String _header;\n\tUi::Text::String _text;\n\tstd::vector<Ui::Text::String> _phrases;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_sponsored_click_handler.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_sponsored_click_handler.h\"\n\n#include \"core/click_handler_types.h\"\n\nnamespace HistoryView {\n\nClickHandlerPtr SponsoredLink(const QString &link, bool isInternal) {\n\tclass ClickHandler final : public UrlClickHandler {\n\tpublic:\n\t\tClickHandler(const QString &link, bool isInternal)\n\t\t: UrlClickHandler(link, false)\n\t\t, _isInternal(isInternal) {\n\t\t}\n\n\t\tQString copyToClipboardContextItemText() const override final {\n\t\t\treturn QString();\n\t\t}\n\n\t\tQString tooltip() const override final {\n\t\t\treturn _isInternal ? QString() : url();\n\t\t}\n\n\tprivate:\n\t\tconst bool _isInternal;\n\n\t};\n\n\treturn std::make_shared<ClickHandler>(link, isInternal);\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_sponsored_click_handler.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass ClickHandler;\n\nnamespace HistoryView {\n\n[[nodiscard]] std::shared_ptr<ClickHandler> SponsoredLink(\n\tconst QString &link,\n\tbool isInternal);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_sticker_toast.h\"\n\n#include \"ui/toast/toast.h\"\n#include \"ui/toast/toast_widget.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"window/window_session_controller.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kPremiumToastDuration = 5 * crl::time(1000);\n\n} // namespace\n\nStickerToast::StickerToast(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<QWidget*> parent,\n\tFn<void()> destroy)\n: StickerToast(controller->uiShow(), parent, std::move(destroy)) {\n}\n\nStickerToast::StickerToast(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<QWidget*> parent,\n\tFn<void()> destroy)\n: _show(std::move(show))\n, _parent(parent)\n, _destroy(std::move(destroy)) {\n}\n\nStickerToast::~StickerToast() {\n\tcancelRequest();\n\t_hiding.push_back(_weak);\n\tfor (const auto &weak : base::take(_hiding)) {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tdelete strong->widget();\n\t\t}\n\t}\n}\n\nvoid StickerToast::showFor(\n\t\tnot_null<DocumentData*> document,\n\t\tSection section) {\n\tconst auto sticker = document->sticker();\n\tif (!sticker || !document->session().premiumPossible()) {\n\t\treturn;\n\t} else if (const auto strong = _weak.get()) {\n\t\tif (_for == document) {\n\t\t\treturn;\n\t\t}\n\t\tstrong->hideAnimated();\n\t} else if (_setRequestId) {\n\t\tif (_for == document) {\n\t\t\treturn;\n\t\t}\n\t\tcancelRequest();\n\t}\n\t_for = document;\n\t_section = section;\n\n\tconst auto title = lookupTitle();\n\tif (!title.isEmpty()) {\n\t\tshowWithTitle(title);\n\t} else {\n\t\trequestSet();\n\t}\n}\n\nQString StickerToast::lookupTitle() const {\n\tExpects(_for != nullptr);\n\n\tconst auto sticker = _for->sticker();\n\tif (!sticker) {\n\t\treturn {};\n\t}\n\n\tconst auto id = sticker->set.id;\n\tif (!id) {\n\t\treturn {};\n\t}\n\n\tconst auto &sets = _for->owner().stickers().sets();\n\tconst auto i = sets.find(id);\n\tif (i == end(sets)) {\n\t\treturn {};\n\t}\n\treturn i->second->title;\n}\n\nvoid StickerToast::requestSet() {\n\tExpects(_for != nullptr);\n\n\tif (const auto sticker = _for->sticker()) {\n\t\tconst auto api = &_show->session().api();\n\t\t_setRequestId = api->request(MTPmessages_GetStickerSet(\n\t\t\tData::InputStickerSet(sticker->set),\n\t\t\tMTP_int(0) // hash\n\t\t)).done([=](const MTPmessages_StickerSet &result) {\n\t\t\t_setRequestId = 0;\n\t\t\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\t\tdata.vset().match([&](const MTPDstickerSet &data) {\n\t\t\t\t\tconst auto owner = &_show->session().data();\n\t\t\t\t\tshowWithTitle(owner->stickers().getSetTitle(data));\n\t\t\t\t});\n\t\t\t}, [&](const MTPDmessages_stickerSetNotModified &) {\n\t\t\t\tLOG((\"API Error: Got messages.stickerSetNotModified.\"));\n\t\t\t});\n\t\t}).fail([=] {\n\t\t\t_setRequestId = 0;\n\t\t}).send();\n\t}\n}\n\nvoid StickerToast::cancelRequest() {\n\t_show->session().api().request(base::take(_setRequestId)).cancel();\n}\n\nvoid StickerToast::showWithTitle(const QString &title) {\n\tExpects(_for != nullptr);\n\n\tstatic auto counter = 0;\n\tconst auto setType = _for->sticker()->setType;\n\tconst auto isEmoji = (_section == Section::TopicIcon)\n\t\t|| (setType == Data::StickersType::Emoji);\n\tconst auto toSaved = isEmoji\n\t\t&& (_section == Section::Message)\n\t\t&& !(++counter % 2);\n\tconst auto text = tr::bold(\n\t\ttitle\n\t).append('\\n').append(\n\t\t(toSaved\n\t\t\t? tr::lng_animated_emoji_saved(tr::now, tr::rich)\n\t\t\t: isEmoji\n\t\t\t? tr::lng_animated_emoji_text(tr::now, tr::rich)\n\t\t\t: tr::lng_sticker_premium_text(tr::now, tr::rich))\n\t);\n\t_st = st::historyPremiumToast;\n\tconst auto skip = _st.padding.top();\n\tconst auto size = _st.style.font->height * 2;\n\tconst auto view = toSaved\n\t\t? tr::lng_animated_emoji_saved_open(tr::now)\n\t\t: tr::lng_sticker_premium_view(tr::now);\n\t_st.padding.setLeft(skip + size + skip);\n\t_st.padding.setRight(st::historyPremiumViewSet.style.font->width(view)\n\t\t- st::historyPremiumViewSet.width);\n\n\tclearHiddenHiding();\n\tif (_weak.get()) {\n\t\t_hiding.push_back(_weak);\n\t}\n\n\t_weak = Ui::Toast::Show(_parent, Ui::Toast::Config{\n\t\t.text = text,\n\t\t.st = &_st,\n\t\t.attach = RectPart::Bottom,\n\t\t.acceptinput = true,\n\t\t.duration = kPremiumToastDuration,\n\t});\n\tconst auto strong = _weak.get();\n\tif (!strong) {\n\t\treturn;\n\t}\n\tconst auto widget = strong->widget();\n\tconst auto hideToast = [weak = _weak] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->hideAnimated();\n\t\t}\n\t};\n\n\tconst auto clickableBackground = Ui::CreateChild<Ui::AbstractButton>(\n\t\twidget.get());\n\tclickableBackground->setPointerCursor(false);\n\tclickableBackground->setAcceptBoth();\n\tclickableBackground->show();\n\tclickableBackground->addClickHandler([=](Qt::MouseButton button) {\n\t\tif (button == Qt::RightButton) {\n\t\t\thideToast();\n\t\t}\n\t});\n\n\tconst auto button = Ui::CreateChild<Ui::RoundButton>(\n\t\twidget.get(),\n\t\trpl::single(view),\n\t\tst::historyPremiumViewSet);\n\tbutton->show();\n\trpl::combine(\n\t\twidget->sizeValue(),\n\t\tbutton->sizeValue()\n\t) | rpl::on_next([=](QSize outer, QSize inner) {\n\t\tbutton->moveToRight(\n\t\t\t0,\n\t\t\t(outer.height() - inner.height()) / 2,\n\t\t\touter.width());\n\t\tclickableBackground->resize(outer);\n\t}, widget->lifetime());\n\tconst auto preview = Ui::CreateChild<Ui::RpWidget>(widget.get());\n\tpreview->moveToLeft(skip, skip);\n\tpreview->resize(size, size);\n\tpreview->show();\n\n\tif (isEmoji) {\n\t\tsetupEmojiPreview(preview, size);\n\t} else {\n\t\tsetupLottiePreview(preview, size);\n\t}\n\tbutton->setClickedCallback([=] {\n\t\tif (toSaved) {\n\t\t\tif (const auto window = _show->resolveWindow()) {\n\t\t\t\twindow->showPeerHistory(\n\t\t\t\t\twindow->session().userPeerId(),\n\t\t\t\t\tWindow::SectionShow::Way::Forward);\n\t\t\t}\n\t\t\thideToast();\n\t\t\treturn;\n\t\t} else if (_section == Section::TopicIcon) {\n\t\t\tif (const auto window = _show->resolveWindow()) {\n\t\t\t\tSettings::ShowPremium(window, u\"forum_topic_icon\"_q);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst auto id = _for->sticker()->set;\n\t\tconst auto &sets = _for->owner().stickers().sets();\n\t\tconst auto i = sets.find(id.id);\n\t\tif (isEmoji\n\t\t\t&& (i != end(sets))\n\t\t\t&& (i->second->flags & Data::StickersSetFlag::Installed)) {\n\t\t\tShowPremiumPreviewBox(_show, PremiumFeature::AnimatedEmoji);\n\t\t} else {\n\t\t\t_show->show(Box<StickerSetBox>(_show, id, setType));\n\t\t}\n\t\thideToast();\n\t});\n}\n\nvoid StickerToast::clearHiddenHiding() {\n\t_hiding.erase(\n\t\tranges::remove(\n\t\t\t_hiding,\n\t\t\tnullptr,\n\t\t\t&base::weak_ptr<Ui::Toast::Instance>::get),\n\t\tend(_hiding));\n}\n\nvoid StickerToast::setupEmojiPreview(\n\t\tnot_null<Ui::RpWidget*> widget,\n\t\tint size) {\n\tExpects(_for != nullptr);\n\n\tstruct Instance {\n\t\tInstance(\n\t\t\tstd::unique_ptr<Ui::CustomEmoji::Loader> loader,\n\t\t\tFn<void(\n\t\t\t\tnot_null<Ui::CustomEmoji::Instance*>,\n\t\t\t\tUi::CustomEmoji::RepaintRequest)> repaintLater,\n\t\t\tFn<void()> repaint)\n\t\t: emoji(\n\t\t\tUi::CustomEmoji::Loading(\n\t\t\t\tstd::move(loader),\n\t\t\t\tUi::CustomEmoji::Preview()),\n\t\t\tstd::move(repaintLater))\n\t\t, object(&emoji, repaint)\n\t\t, timer(repaint) {\n\t\t}\n\n\t\tUi::CustomEmoji::Instance emoji;\n\t\tUi::CustomEmoji::Object object;\n\t\tbase::Timer timer;\n\t};\n\n\tconst auto repaintDelayed = [=](\n\t\t\tnot_null<Ui::CustomEmoji::Instance*> instance,\n\t\t\tUi::CustomEmoji::RepaintRequest request) {\n\t\tif (!request.when) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto now = crl::now();\n\t\tif (now > request.when) {\n\t\t\treinterpret_cast<Instance*>(instance.get())->timer.callOnce(\n\t\t\t\tnow - request.when);\n\t\t} else {\n\t\t\twidget->update();\n\t\t}\n\t};\n\tconst auto instance = widget->lifetime().make_state<Instance>(\n\t\t_for->owner().customEmojiManager().createLoader(\n\t\t\t_for,\n\t\t\tData::CustomEmojiManager::SizeTag::Large),\n\t\tstd::move(repaintDelayed),\n\t\t[=] { widget->update(); });\n\n\twidget->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(widget);\n\t\tconst auto size = Ui::Emoji::GetSizeLarge()\n\t\t\t/ style::DevicePixelRatio();\n\t\tinstance->object.paint(p, Ui::Text::CustomEmoji::Context{\n\t\t\t.textColor = st::toastFg->c,\n\t\t\t.now = crl::now(),\n\t\t\t.position = QPoint(\n\t\t\t\t(widget->width() - size) / 2,\n\t\t\t\t(widget->height() - size) / 2),\n\t\t});\n\t}, widget->lifetime());\n}\n\nvoid StickerToast::setupLottiePreview(not_null<Ui::RpWidget*> widget, int size) {\n\tExpects(_for != nullptr);\n\n\tconst auto bytes = _for->createMediaView()->bytes();\n\tconst auto filepath = _for->filepath();\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto player = widget->lifetime().make_state<Lottie::SinglePlayer>(\n\t\tLottie::ReadContent(bytes, filepath),\n\t\tLottie::FrameRequest{ QSize(size, size) * ratio },\n\t\tLottie::Quality::Default);\n\n\twidget->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tif (!player->ready()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto image = player->frame();\n\t\tQPainter(widget).drawImage(\n\t\t\tQRect(QPoint(), image.size() / ratio),\n\t\t\timage);\n\t\tplayer->markFrameShown();\n\t}, widget->lifetime());\n\n\tplayer->updates(\n\t) | rpl::on_next([=] {\n\t\twidget->update();\n\t}, widget->lifetime());\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_sticker_toast.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"styles/style_widgets.h\"\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Ui {\nclass Show;\nclass RpWidget;\n} // namespace Ui\n\nnamespace Ui::Toast {\nclass Instance;\n} // namespace Ui::Toast\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace HistoryView {\n\nclass StickerToast final {\npublic:\n\tStickerToast(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\tFn<void()> destroy);\n\tStickerToast(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<QWidget*> parent,\n\t\tFn<void()> destroy);\n\t~StickerToast();\n\n\tenum class Section {\n\t\tMessage,\n\t\tTopicIcon,\n\t};\n\tvoid showFor(\n\t\tnot_null<DocumentData*> document,\n\t\tSection section = Section::Message);\n\nprivate:\n\tvoid requestSet();\n\tvoid cancelRequest();\n\tvoid showWithTitle(const QString &title);\n\t[[nodiscard]] QString lookupTitle() const;\n\n\tvoid setupEmojiPreview(not_null<Ui::RpWidget*> widget, int size);\n\tvoid setupLottiePreview(not_null<Ui::RpWidget*> widget, int size);\n\tvoid clearHiddenHiding();\n\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst not_null<QWidget*> _parent;\n\tSection _section = {};\n\tstyle::Toast _st;\n\tbase::weak_ptr<Ui::Toast::Instance> _weak;\n\tstd::vector<base::weak_ptr<Ui::Toast::Instance>> _hiding;\n\tDocumentData *_for = nullptr;\n\tFn<void()> _destroy;\n\n\tmtpRequestId _setRequestId = 0;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_subsection_tabs.h\"\n\n#include \"apiwrap.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_thread.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"dialogs/dialogs_main_list.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session_settings.h\"\n#include \"main/main_session.h\"\n#include \"ui/controls/subsection_tabs_slider_reorder.h\"\n#include \"ui/controls/subsection_tabs_slider.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/discrete_sliders.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_separate_id.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kDefaultLimit = 12;\n\n} // namespace\n\nSubsectionTabs::SubsectionTabs(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Data::Thread*> thread)\n: _controller(controller)\n, _history(thread->owningHistory())\n, _active(thread)\n, _around(thread)\n, _beforeLimit(kDefaultLimit)\n, _afterLimit(kDefaultLimit) {\n\ttrack();\n\trefreshSlice();\n\tsetup(parent);\n\n\tsession().data().pinnedDialogsOrderUpdated() | rpl::on_next([=] {\n\t\t_refreshed.fire({});\n\t}, _lifetime);\n\n\tdataChanged() | rpl::on_next([=] {\n\t\tif (_loading) {\n\t\t\t_loading = false;\n\t\t\trefreshSlice();\n\t\t}\n\t}, _lifetime);\n}\n\nSubsectionTabs::~SubsectionTabs() {\n\tdelete base::take(_horizontal);\n\tdelete base::take(_vertical);\n\tdelete base::take(_bottom);\n\tdelete base::take(_shadow);\n}\n\nvoid SubsectionTabs::setup(not_null<Ui::RpWidget*> parent) {\n\tconst auto peerId = _history->peer->id;\n\tconst auto mode = session().settings().subsectionTabsMode(peerId);\n\tif (mode == qint32(SubsectionTabsMode::Left)) {\n\t\tsetupVertical(parent);\n\t} else if (mode == qint32(SubsectionTabsMode::Bottom)) {\n\t\tsetupHorizontal(parent, true);\n\t} else {\n\t\tsetupHorizontal(parent, false);\n\t}\n}\n\nvoid SubsectionTabs::setupHorizontal(\n\t\tnot_null<QWidget*> parent,\n\t\tbool bottom) {\n\tdelete base::take(_vertical);\n\tdelete base::take(bottom ? _horizontal : _bottom);\n\tauto &widgetRef = bottom ? _bottom : _horizontal;\n\twidgetRef = Ui::CreateChild<Ui::RpWidget>(parent);\n\twidgetRef->show();\n\tconst auto widget = widgetRef;\n\n\tif (!_shadow) {\n\t\t_shadow = Ui::CreateChild<Ui::PlainShadow>(parent);\n\t\t_shadow->show();\n\t} else {\n\t\t_shadow->raise();\n\t}\n\n\tconst auto toggle = Ui::CreateChild<Ui::IconButton>(\n\t\twidget,\n\t\tst::chatTabsToggle);\n\ttoggle->show();\n\ttoggle->setIconOverride(\n\t\tbottom ? &st::chatTabsToggleIconBottom : &st::chatTabsToggleIconTop,\n\t\t(bottom\n\t\t\t? &st::chatTabsToggleIconBottomOver\n\t\t\t: &st::chatTabsToggleIconTopOver));\n\ttoggle->setClickedCallback([=] {\n\t\ttoggleModes();\n\t});\n\ttoggle->move(0, 0);\n\tconst auto scroll = Ui::CreateChild<Ui::ScrollArea>(\n\t\twidget,\n\t\tst::chatTabsScroll,\n\t\ttrue);\n\tscroll->show();\n\tconst auto shadow = Ui::CreateChild<Ui::PlainShadow>(widget);\n\tconst auto slider = scroll->setOwnedWidget(\n\t\tobject_ptr<Ui::HorizontalSlider>(scroll));\n\t_reorder = std::make_unique<Ui::SubsectionSliderReorder>(slider, scroll);\n\tsetupSlider(scroll, slider, false);\n\n\tshadow->showOn(rpl::single(\n\t\trpl::empty\n\t) | rpl::then(\n\t\tscroll->scrolls()\n\t) | rpl::map([=] { return scroll->scrollLeft() > 0; }));\n\tshadow->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\twidget->resize(\n\t\twidget->width(),\n\t\tstd::max(toggle->height(), slider->height()));\n\n\tscroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) {\n\t\tconst auto pixelDelta = e->pixelDelta();\n\t\tconst auto angleDelta = e->angleDelta();\n\t\tif (std::abs(pixelDelta.x()) + std::abs(angleDelta.x())) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto y = pixelDelta.y() ? pixelDelta.y() : angleDelta.y();\n\t\tscroll->scrollToX(scroll->scrollLeft() - y);\n\t\treturn true;\n\t});\n\n\twidget->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tconst auto togglew = toggle->width();\n\t\tconst auto height = size.height();\n\t\tscroll->setGeometry(togglew, 0, size.width() - togglew, height);\n\t\tshadow->setGeometry(togglew, 0, st::lineWidth, height);\n\t}, scroll->lifetime());\n\n\twidget->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(widget);\n\n\t\tconst auto line = st::lineWidth;\n\t\tp.fillRect(\n\t\t\tclip.intersected(\n\t\t\t\twidget->rect().marginsRemoved(\n\t\t\t\t\t{ 0, bottom ? line : 0, 0, bottom ? 0 : line })),\n\t\t\tst::windowBg);\n\t\tif (bottom) {\n\t\t\tconst auto shadow = QRect(\n\t\t\t\t0,\n\t\t\t\twidget->height() - line,\n\t\t\t\twidget->width(),\n\t\t\t\tline);\n\t\t\tif (clip.intersects(shadow)) {\n\t\t\t\tp.fillRect(clip.intersected(shadow), st::shadowFg);\n\t\t\t}\n\t\t}\n\t}, widget->lifetime());\n\n\t_layoutRequests.fire({});\n\n\tstartFillingSlider(scroll, slider, false);\n}\n\nvoid SubsectionTabs::setupVertical(not_null<QWidget*> parent) {\n\tdelete base::take(_horizontal);\n\tdelete base::take(_bottom);\n\t_vertical = Ui::CreateChild<Ui::RpWidget>(parent);\n\t_vertical->show();\n\n\tif (!_shadow) {\n\t\t_shadow = Ui::CreateChild<Ui::PlainShadow>(parent);\n\t\t_shadow->show();\n\t}\n\n\tconst auto toggle = Ui::CreateChild<Ui::IconButton>(\n\t\t_vertical,\n\t\tst::chatTabsToggle);\n\ttoggle->show();\n\ttoggle->setIconOverride(\n\t\t&st::chatTabsToggleIconLeft,\n\t\t&st::chatTabsToggleIconLeftOver);\n\ttoggle->setClickedCallback([=] {\n\t\ttoggleModes();\n\t});\n\ttoggle->move(0, 0);\n\tconst auto scroll = Ui::CreateChild<Ui::ScrollArea>(\n\t\t_vertical,\n\t\tst::chatTabsScroll);\n\tscroll->show();\n\tconst auto shadow = Ui::CreateChild<Ui::PlainShadow>(_vertical);\n\tconst auto slider = scroll->setOwnedWidget(\n\t\tobject_ptr<Ui::VerticalSlider>(scroll));\n\t_reorder = std::make_unique<Ui::SubsectionSliderReorder>(slider, scroll);\n\tsetupSlider(scroll, slider, true);\n\n\tshadow->showOn(rpl::single(\n\t\trpl::empty\n\t) | rpl::then(\n\t\tscroll->scrolls()\n\t) | rpl::map([=] { return scroll->scrollTop() > 0; }));\n\tshadow->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t_vertical->resize(\n\t\tstd::max(toggle->width(), slider->width()),\n\t\t_vertical->height());\n\n\t_vertical->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tconst auto toggleh = toggle->height();\n\t\tconst auto width = size.width();\n\t\tscroll->setGeometry(0, toggleh, width, size.height() - toggleh);\n\t\tshadow->setGeometry(0, toggleh, width, st::lineWidth);\n\t}, scroll->lifetime());\n\n\t_vertical->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tQPainter(_vertical).fillRect(clip, st::windowBg);\n\t}, _vertical->lifetime());\n\n\t_layoutRequests.fire({});\n\n\tstartFillingSlider(scroll, slider, true);\n}\n\nvoid SubsectionTabs::setupSlider(\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\tnot_null<Ui::SubsectionSlider*> slider,\n\t\tbool vertical) {\n\tslider->sectionActivated() | rpl::on_next([=](int active) {\n\t\tif (_reordering) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto guard = base::make_weak(slider);\n\t\tconst auto newWindow = base::IsCtrlPressed();\n\t\tif (active >= 0 && active < _slice.size()) {\n\t\t\tconst auto thread = _slice[active].thread;\n\t\t\tif (newWindow) {\n\t\t\t\t_controller->showInNewWindow(Window::SeparateId(thread));\n\t\t\t\tif (guard) {\n\t\t\t\t\t// This should activate current section.\n\t\t\t\t\t_refreshed.fire({});\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tauto params = Window::SectionShow();\n\t\t\t\tparams.way = Window::SectionShow::Way::ClearStack;\n\t\t\t\tparams.animated = anim::type::instant;\n\t\t\t\t_controller->showThread(thread, ShowAtUnreadMsgId, params);\n\t\t\t}\n\t\t}\n\t\tif (guard) {\n\t\t\t_reorder->finishReordering();\n\t\t}\n\t}, slider->lifetime());\n\n\t_reorder->updates(\n\t) | rpl::on_next([=](Ui::SubsectionSliderReorder::Single data) {\n\t\tusing State = Ui::SubsectionSliderReorder::State;\n\t\tif (data.state == State::Started) {\n\t\t\t++_reordering;\n\t\t} else {\n\t\t\tUi::PostponeCall(slider, [=] {\n\t\t\t\t--_reordering;\n\t\t\t});\n\t\t\tif (data.state == State::Applied) {\n\t\t\t\tapplyReorder(data.widget, data.oldPosition, data.newPosition);\n\t\t\t\tslider->recalculatePinnedPositions();\n\t\t\t}\n\t\t}\n\t}, slider->lifetime());\n\n\tslider->setIsReorderingCallback([=] {\n\t\treturn _reordering > 0;\n\t});\n\n\tslider->sectionContextMenu() | rpl::on_next([=](int index) {\n\t\tif (index >= 0 && index < _slice.size()) {\n\t\t\tshowThreadContextMenu(_slice[index].thread);\n\t\t}\n\t}, slider->lifetime());\n\n\tslider->requestShown(\n\t) | rpl::on_next([=](Ui::ScrollToRequest request) {\n\t\tconst auto full = vertical ? scroll->height() : scroll->width();\n\t\tconst auto tab = request.ymax - request.ymin;\n\t\tif (tab < full) {\n\t\t\tconst auto add = std::min(full - tab, tab) / 2;\n\t\t\trequest.ymax += add;\n\t\t\trequest.ymin -= add;\n\t\t}\n\t\tconst auto scrollValue = vertical\n\t\t\t? scroll->scrollTop()\n\t\t\t: scroll->scrollLeft();\n\t\tif (request.ymin < scrollValue) {\n\t\t\tif (vertical) {\n\t\t\t\tscroll->scrollToY(request.ymin);\n\t\t\t} else {\n\t\t\t\tscroll->scrollToX(request.ymin);\n\t\t\t}\n\t\t} else if (request.ymax > scrollValue + full) {\n\t\t\tconst auto value = std::min(request.ymin, request.ymax - full);\n\t\t\tif (vertical) {\n\t\t\t\tscroll->scrollToY(value);\n\t\t\t} else {\n\t\t\t\tscroll->scrollToX(value);\n\t\t\t}\n\t\t}\n\t}, slider->lifetime());\n}\n\nvoid SubsectionTabs::startScrollChecking(\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\tnot_null<Ui::SubsectionSlider*> slider,\n\t\tbool vertical) {\n\trpl::merge(\n\t\tscroll->scrolls(),\n\t\t_scrollCheckRequests.events(),\n\t\t(vertical\n\t\t\t? scroll->heightValue()\n\t\t\t: scroll->widthValue()) | rpl::skip(1) | rpl::map_to(rpl::empty)\n\t) | rpl::on_next([=] {\n\t\tconst auto full = vertical ? scroll->height() : scroll->width();\n\t\tconst auto scrollValue = vertical\n\t\t\t? scroll->scrollTop()\n\t\t\t: scroll->scrollLeft();\n\t\tconst auto scrollMax = vertical\n\t\t\t? scroll->scrollTopMax()\n\t\t\t: scroll->scrollLeftMax();\n\t\tconst auto availableFrom = scrollValue;\n\t\tconst auto availableTill = (scrollMax - scrollValue);\n\t\tconst auto needMore = (scrollMax <= 3 * full && _afterAvailable > 0);\n\t\tif (needMore) {\n\t\t\t_beforeLimit *= 2;\n\t\t\t_afterLimit *= 2;\n\t\t}\n\t\tif (availableFrom < full\n\t\t\t&& _beforeSkipped.value_or(0) > 0\n\t\t\t&& !_slice.empty()) {\n\t\t\trefreshAroundMiddle(scroll, slider);\n\t\t} else if (availableTill < full) {\n\t\t\tif (_afterAvailable > 0) {\n\t\t\t\trefreshAroundMiddle(scroll, slider);\n\t\t\t} else if (!_afterSkipped.has_value()) {\n\t\t\t\t_loading = true;\n\t\t\t\tloadMore();\n\t\t\t}\n\t\t} else if (needMore) {\n\t\t\trefreshAroundMiddle(scroll, slider);\n\t\t}\n\t}, scroll->lifetime());\n\n\tsession().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::Rights\n\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\treturn (update.peer == _history->peer);\n\t}) | rpl::on_next([=] {\n\t\tif (_reorder) {\n\t\t\t_reorder->cancel();\n\t\t\tif (_history->peer->canManageTopics()) {\n\t\t\t\t_reorder->start();\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n}\n\nvoid SubsectionTabs::startFillingSlider(\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\tnot_null<Ui::SubsectionSlider*> slider,\n\t\tbool vertical) {\n\t// We need to fill the content after the initial size is set.\n\n\tusing ImagePointer = std::shared_ptr<Ui::DynamicImage>;\n\tstruct Cache {\n\t\tbase::flat_map<not_null<PeerData*>, ImagePointer> userpics;\n\t};\n\tconst auto cache = std::make_shared<Cache>();\n\n\t_refreshed.events_starting_with_copy(\n\t\trpl::empty\n\t) | rpl::on_next([=] {\n\t\tconst auto manager = &_history->owner().customEmojiManager();\n\t\tconst auto paused = [=] {\n\t\t\treturn _controller->isGifPausedAtLeastFor(\n\t\t\t\tWindow::GifPauseReason::Any);\n\t\t};\n\t\tauto updated = Cache();\n\t\tauto sections = std::vector<Ui::SubsectionTab>();\n\t\tauto activeIndex = -1;\n\t\tauto fixedCount = 1; // 1 is the first button.\n\t\tauto pinnedCount = 0;\n\t\tfor (const auto &item : _slice) {\n\t\t\tconst auto index = int(sections.size());\n\t\t\tif (item.thread == _active) {\n\t\t\t\tactiveIndex = index;\n\t\t\t}\n\t\t\tif (item.thread->fixedOnTopIndex()) {\n\t\t\t\t++fixedCount;\n\t\t\t}\n\t\t\tif (item.thread->isPinnedDialog(FilterId())\n\t\t\t\t&& item.thread->asTopic()) {\n\t\t\t\t++pinnedCount;\n\t\t\t}\n\t\t\tconst auto textFg = [=] {\n\t\t\t\treturn anim::color(\n\t\t\t\t\tst::windowSubTextFg,\n\t\t\t\t\tst::windowActiveTextFg,\n\t\t\t\t\tslider->buttonActive(slider->buttonAt(index)));\n\t\t\t};\n\t\t\tif (const auto topic = item.thread->asTopic()) {\n\t\t\t\tif (vertical) {\n\t\t\t\t\tconst auto general = topic->isGeneral();\n\t\t\t\t\tsections.push_back({\n\t\t\t\t\t\t.text = { item.name },\n\t\t\t\t\t\t.userpic = (item.iconId\n\t\t\t\t\t\t\t? Ui::MakeEmojiThumbnail(\n\t\t\t\t\t\t\t\t&topic->owner(),\n\t\t\t\t\t\t\t\tData::SerializeCustomEmojiId(item.iconId),\n\t\t\t\t\t\t\t\tpaused,\n\t\t\t\t\t\t\t\ttextFg)\n\t\t\t\t\t\t\t: Ui::MakeEmojiThumbnail(\n\t\t\t\t\t\t\t\t&topic->owner(),\n\t\t\t\t\t\t\t\tData::TopicIconEmojiEntity({\n\t\t\t\t\t\t\t\t\t.title = (general\n\t\t\t\t\t\t\t\t\t\t? Data::ForumGeneralIconTitle()\n\t\t\t\t\t\t\t\t\t\t: item.name),\n\t\t\t\t\t\t\t\t\t.colorId = (general\n\t\t\t\t\t\t\t\t\t\t? Data::ForumGeneralIconColor(\n\t\t\t\t\t\t\t\t\t\t\tst::windowSubTextFg->c)\n\t\t\t\t\t\t\t\t\t\t: topic->colorId()),\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\tpaused,\n\t\t\t\t\t\t\t\ttextFg)),\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tsections.push_back({\n\t\t\t\t\t\t.text = topic->titleWithIcon(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else if (const auto sublist = item.thread->asSublist()) {\n\t\t\t\tconst auto peer = sublist->sublistPeer();\n\t\t\t\tif (vertical) {\n\t\t\t\t\tauto was = cache->userpics[peer];\n\t\t\t\t\tauto userpic = updated.userpics[peer] = was\n\t\t\t\t\t\t? was\n\t\t\t\t\t\t: Ui::MakeUserpicThumbnail(peer);\n\t\t\t\t\tsections.push_back({\n\t\t\t\t\t\t.text = { peer->shortName() },\n\t\t\t\t\t\t.userpic = std::move(userpic),\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tsections.push_back({\n\t\t\t\t\t\t.text = TextWithEntities().append(\n\t\t\t\t\t\t\tUi::Text::SingleCustomEmoji(\n\t\t\t\t\t\t\t\tmanager->peerUserpicEmojiData(peer),\n\t\t\t\t\t\t\t\tu\"@\"_q)\n\t\t\t\t\t\t).append(' ').append(peer->shortName()),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t// } else if (Data::IsBotUserCreatesTopics(item.thread->peer())) {\n\t\t\t// \tsections.push_back({\n\t\t\t// \t\t.text = { tr::lng_bot_new_chat(tr::now) },\n\t\t\t// \t});\n\t\t\t// \tif (vertical) {\n\t\t\t// \t\tauto &last = sections.back();\n\t\t\t// \t\tlast.userpic = Ui::MakeNewChatSubsectionsThumbnail(\n\t\t\t// \t\t\ttextFg);\n\t\t\t// \t}\n\t\t\t} else {\n\t\t\t\tsections.push_back({\n\t\t\t\t\t.text = { tr::lng_filters_all_short(tr::now) },\n\t\t\t\t});\n\t\t\t\tif (vertical) {\n\t\t\t\t\tauto &last = sections.back();\n\t\t\t\t\tlast.userpic = Ui::MakeAllSubsectionsThumbnail(textFg);\n\t\t\t\t}\n\t\t\t}\n\t\t\tauto &section = sections.back();\n\t\t\tsection.badges = item.badges;\n\t\t}\n\t\t*cache = std::move(updated);\n\n\t\tauto scrollSavingThread = (Data::Thread*)nullptr;\n\t\tauto scrollSavingShift = 0;\n\t\tauto scrollSavingIndex = -1;\n\t\tif (const auto count = slider->sectionsCount()) {\n\t\t\tconst auto scrollValue = vertical\n\t\t\t\t? scroll->scrollTop()\n\t\t\t\t: scroll->scrollLeft();\n\t\t\tauto indexPosition = slider->lookupSectionPosition(0);\n\t\t\tfor (auto index = 0; index != count; ++index) {\n\t\t\t\tconst auto nextPosition = (index + 1 != count)\n\t\t\t\t\t? slider->lookupSectionPosition(index + 1)\n\t\t\t\t\t: (indexPosition + scrollValue + 1);\n\t\t\t\tif (indexPosition <= scrollValue && nextPosition > scrollValue) {\n\t\t\t\t\tscrollSavingThread = _sectionsSlice[index].thread;\n\t\t\t\t\tscrollSavingShift = scrollValue - indexPosition;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tindexPosition = nextPosition;\n\t\t\t}\n\t\t\tscrollSavingIndex = scrollSavingThread\n\t\t\t\t? int(ranges::find(\n\t\t\t\t\t_slice,\n\t\t\t\t\tnot_null(scrollSavingThread),\n\t\t\t\t\t&Item::thread\n\t\t\t\t) - begin(_slice))\n\t\t\t\t: -1;\n\t\t\tif (scrollSavingIndex == _slice.size()) {\n\t\t\t\tscrollSavingIndex = -1;\n\t\t\t\tfor (auto index = 0; index != count; ++index) {\n\t\t\t\t\tconst auto thread = _sectionsSlice[index].thread;\n\t\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\t\t_slice,\n\t\t\t\t\t\tthread,\n\t\t\t\t\t\t&Item::thread);\n\t\t\t\t\tif (i != end(_slice)) {\n\t\t\t\t\t\tscrollSavingThread = thread;\n\t\t\t\t\t\tscrollSavingShift = scrollValue\n\t\t\t\t\t\t\t- slider->lookupSectionPosition(index);\n\t\t\t\t\t\tscrollSavingIndex = int(i - begin(_slice));\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tslider->setSections({\n\t\t\t.tabs = std::move(sections),\n\t\t\t.context = Core::TextContext({\n\t\t\t\t.session = &session(),\n\t\t\t}),\n\t\t\t.fixed = fixedCount,\n\t\t\t.pinned = pinnedCount,\n\t\t}, paused);\n\t\t_reorder->clearPinnedIntervals();\n\t\t_reorder->addPinnedInterval(0, 1);\n\t\tif (pinnedCount > 1) {\n\t\t\tconst auto from = 1 + pinnedCount;\n\t\t\t_reorder->addPinnedInterval(from, slider->sectionsCount() - from);\n\t\t}\n\n\t\tconst auto ignoreActiveScroll = (scrollSavingIndex >= 0);\n\t\tslider->setActiveSectionFast(activeIndex, ignoreActiveScroll);\n\n\t\t_sectionsSlice = _slice;\n\t\tAssert(slider->sectionsCount() == _slice.size());\n\n\t\t_reorder->cancel();\n\t\tif ((pinnedCount > 1) && _history->peer->canManageTopics()) {\n\t\t\t_reorder->start();\n\t\t}\n\n\t\tif (ignoreActiveScroll) {\n\t\t\tAssert(scrollSavingIndex < slider->sectionsCount());\n\t\t\tconst auto position = scrollSavingShift\n\t\t\t\t+ slider->lookupSectionPosition(scrollSavingIndex);\n\t\t\tif (vertical) {\n\t\t\t\tscroll->scrollToY(position);\n\t\t\t} else {\n\t\t\t\tscroll->scrollToX(position);\n\t\t\t}\n\t\t}\n\n\t\t_scrollCheckRequests.fire({});\n\t}, scroll->lifetime());\n\n\tstartScrollChecking(scroll, slider, vertical);\n}\n\nvoid SubsectionTabs::showThreadContextMenu(not_null<Data::Thread*> thread) {\n\t_menu = nullptr;\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tactiveWidget(),\n\t\tst::popupMenuExpandedSeparator);\n\n\tconst auto addAction = Ui::Menu::CreateAddActionCallback(_menu);\n\tWindow::FillDialogsEntryMenu(\n\t\t_controller,\n\t\tDialogs::EntryState{\n\t\t\t.key = Dialogs::Key{ thread },\n\t\t\t.section = Dialogs::EntryState::Section::SubsectionTabsMenu,\n\t\t},\n\t\taddAction);\n\tif (_menu->empty()) {\n\t\t_menu = nullptr;\n\t} else {\n\t\t_menu->popup(QCursor::pos());\n\t}\n}\n\nvoid SubsectionTabs::loadMore() {\n\tif (const auto forum = _history->peer->forum()) {\n\t\tforum->requestTopics();\n\t} else if (const auto monoforum = _history->peer->monoforum()) {\n\t\tmonoforum->loadMore();\n\t} else {\n\t\tUnexpected(\"Peer in SubsectionTabs::loadMore.\");\n\t}\n}\n\nrpl::producer<> SubsectionTabs::dataChanged() const {\n\tif (const auto forum = _history->peer->forum()) {\n\t\treturn forum->chatsListChanges();\n\t} else if (const auto monoforum = _history->peer->monoforum()) {\n\t\treturn monoforum->chatsListChanges();\n\t} else {\n\t\tUnexpected(\"Peer in SubsectionTabs::dataChanged.\");\n\t}\n}\n\nvoid SubsectionTabs::toggleModes() {\n\tExpects((_horizontal || _vertical || _bottom) && _shadow);\n\n\tconst auto peerId = _history->peer->id;\n\tconst auto parent = activeWidget()->parentWidget();\n\tconst auto current = session().settings().subsectionTabsMode(peerId);\n\tconst auto next = (current == qint32(SubsectionTabsMode::Top))\n\t\t? SubsectionTabsMode::Bottom\n\t\t: (current == qint32(SubsectionTabsMode::Bottom))\n\t\t? SubsectionTabsMode::Left\n\t\t: SubsectionTabsMode::Top;\n\tsession().settings().setSubsectionTabsMode(\n\t\tpeerId,\n\t\tqint32(next));\n\tsession().saveSettingsDelayed();\n\n\tif (next == SubsectionTabsMode::Left) {\n\t\tsetupVertical(parent);\n\t} else if (next == SubsectionTabsMode::Bottom) {\n\t\tsetupHorizontal(parent, true);\n\t} else {\n\t\tsetupHorizontal(parent, false);\n\t}\n}\n\nbool SubsectionTabs::dying() const {\n\treturn !UsedFor(_history);\n}\n\nrpl::producer<> SubsectionTabs::removeRequests() const {\n\tif (const auto forum = _history->peer->forum()) {\n\t\treturn forum->destroyed();\n\t} else if (const auto monoforum = _history->peer->monoforum()) {\n\t\treturn monoforum->destroyed();\n\t} else {\n\t\tUnexpected(\"Peer in SubsectionTabs::removeRequests.\");\n\t}\n}\n\nvoid SubsectionTabs::extractToParent(not_null<Ui::RpWidget*> parent) {\n\tExpects((_horizontal || _vertical || _bottom) && _shadow);\n\n\tconst auto widget = activeWidget();\n\twidget->hide();\n\twidget->setParent(parent);\n\t_shadow->hide();\n\t_shadow->setParent(parent);\n}\n\nvoid SubsectionTabs::setBoundingRect(QRect boundingRect) {\n\tExpects((_horizontal || _vertical || _bottom) && _shadow);\n\n\tif (_horizontal) {\n\t\t_horizontal->setGeometry(\n\t\t\tboundingRect.x(),\n\t\t\tboundingRect.y(),\n\t\t\tboundingRect.width(),\n\t\t\t_horizontal->height());\n\t\t_shadow->setGeometry(\n\t\t\tboundingRect.x(),\n\t\t\t_horizontal->y() + _horizontal->height() - st::lineWidth,\n\t\t\tboundingRect.width(),\n\t\t\tst::lineWidth);\n\t} else if (_vertical) {\n\t\t_vertical->setGeometry(\n\t\t\tboundingRect.x(),\n\t\t\tboundingRect.y(),\n\t\t\t_vertical->width(),\n\t\t\tboundingRect.height());\n\t\t_shadow->setGeometry(\n\t\t\t_vertical->x() + _vertical->width(),\n\t\t\tboundingRect.y(),\n\t\t\tst::lineWidth,\n\t\t\tboundingRect.height());\n\t} else {\n\t\t_bottom->setGeometry(\n\t\t\tboundingRect.x(),\n\t\t\tboundingRect.y() + boundingRect.height()\n\t\t\t\t- _bottom->height(),\n\t\t\tboundingRect.width(),\n\t\t\t_bottom->height());\n\t\t_shadow->setGeometry(\n\t\t\tboundingRect.x(),\n\t\t\t_bottom->y(),\n\t\t\tboundingRect.width(),\n\t\t\tst::lineWidth);\n\t}\n}\n\nrpl::producer<> SubsectionTabs::layoutRequests() const {\n\treturn _layoutRequests.events();\n}\n\nint SubsectionTabs::leftSkip() const {\n\treturn _vertical ? _vertical->width() : 0;\n}\n\nint SubsectionTabs::topSkip() const {\n\treturn _horizontal ? (_horizontal->height() - st::lineWidth) : 0;\n}\n\nint SubsectionTabs::bottomSkip() const {\n\treturn _bottom ? (_bottom->height() - st::lineWidth) : 0;\n}\n\nvoid SubsectionTabs::raise() {\n\tExpects((_horizontal || _vertical || _bottom) && _shadow);\n\n\tactiveWidget()->raise();\n\t_shadow->raise();\n}\n\nvoid SubsectionTabs::show() {\n\tsetVisible(true);\n}\n\nvoid SubsectionTabs::hide() {\n\tsetVisible(false);\n}\n\nvoid SubsectionTabs::setVisible(bool shown) {\n\tExpects((_horizontal || _vertical || _bottom) && _shadow);\n\n\tactiveWidget()->setVisible(shown);\n\t_shadow->setVisible(shown);\n}\n\nvoid SubsectionTabs::track() {\n\tusing Event = Data::Session::ChatListEntryRefresh;\n\tif (const auto forum = _history->peer->forum()) {\n\t\tforum->topicDestroyed(\n\t\t) | rpl::on_next([=](not_null<Data::ForumTopic*> topic) {\n\t\t\tif (_around == topic) {\n\t\t\t\t_around = _history;\n\t\t\t\trefreshSlice();\n\t\t\t}\n\t\t}, _lifetime);\n\n\t\tforum->topicsList()->unreadStateChanges(\n\t\t) | rpl::on_next([=] {\n\t\t\tscheduleRefresh();\n\t\t}, _lifetime);\n\n\t\tforum->owner().chatListEntryRefreshes(\n\t\t) | rpl::filter([=](const Event &event) {\n\t\t\tconst auto topic = event.filterId ? nullptr : event.key.topic();\n\t\t\treturn (topic && topic->forum() == forum);\n\t\t}) | rpl::on_next([=] {\n\t\t\tscheduleRefresh();\n\t\t}, _lifetime);\n\n\t\tforum->session().changes().topicUpdates(\n\t\t\tData::TopicUpdate::Flag::Title\n\t\t\t| Data::TopicUpdate::Flag::IconId\n\t\t\t| Data::TopicUpdate::Flag::ColorId\n\t\t) | rpl::filter([=](const Data::TopicUpdate &update) {\n\t\t\treturn update.topic->forum() == forum;\n\t\t}) | rpl::on_next([=] {\n\t\t\tscheduleRefresh();\n\t\t}, _lifetime);\n\t} else if (const auto monoforum = _history->peer->monoforum()) {\n\t\tmonoforum->sublistDestroyed(\n\t\t) | rpl::on_next([=](not_null<Data::SavedSublist*> sublist) {\n\t\t\tif (_around == sublist) {\n\t\t\t\t_around = _history;\n\t\t\t\trefreshSlice();\n\t\t\t}\n\t\t}, _lifetime);\n\n\t\tmonoforum->chatsList()->unreadStateChanges(\n\t\t) | rpl::on_next([=] {\n\t\t\tscheduleRefresh();\n\t\t}, _lifetime);\n\n\t\tmonoforum->owner().chatListEntryRefreshes(\n\t\t) | rpl::filter([=](const Event &event) {\n\t\t\tconst auto sublist = event.filterId\n\t\t\t\t? nullptr\n\t\t\t\t: event.key.sublist();\n\t\t\treturn (sublist && sublist->parent() == monoforum);\n\t\t}) | rpl::on_next([=] {\n\t\t\tscheduleRefresh();\n\t\t}, _lifetime);\n\t} else {\n\t\tUnexpected(\"Peer in SubsectionTabs::track.\");\n\t}\n}\n\nvoid SubsectionTabs::refreshAroundMiddle(\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\tnot_null<Ui::SubsectionSlider*> slider) {\n\tExpects(!_slice.empty());\n\n\tconst auto full = _vertical ? scroll->height() : scroll->width();\n\tconst auto scrollValue = _vertical\n\t\t? scroll->scrollTop()\n\t\t: scroll->scrollLeft();\n\tconst auto scrollMax = _vertical\n\t\t? scroll->scrollTopMax()\n\t\t: scroll->scrollLeftMax();\n\n\tauto best = -1;\n\tauto bestDistance = -1;\n\tconst auto ideal = scrollValue + (full / 2);\n\tfor (auto i = 0, count = int(_slice.size()); i != count; ++i) {\n\t\tconst auto a = slider->lookupSectionPosition(i);\n\t\tconst auto b = (i + 1 == count)\n\t\t\t? (full + scrollMax)\n\t\t\t: slider->lookupSectionPosition(i + 1);\n\t\tconst auto middle = (a + b) / 2;\n\t\tconst auto distance = std::abs(middle - ideal);\n\t\tif (best < 0 || distance < bestDistance) {\n\t\t\tbest = i;\n\t\t\tbestDistance = distance;\n\t\t}\n\t}\n\n\tAssert(best >= 0 && best < _slice.size());\n\n\t_around = _slice[best].thread;\n\trefreshSlice();\n}\n\nvoid SubsectionTabs::refreshSlice() {\n\t_refreshScheduled = false;\n\n\tconst auto forum = _history->peer->forum();\n\tconst auto monoforum = _history->peer->monoforum();\n\tAssert(forum || monoforum);\n\n\tconst auto list = forum\n\t\t? forum->topicsList()\n\t\t: monoforum->chatsList();\n\tauto slice = std::vector<Item>();\n\tslice.reserve(_slice.size() + 10);\n\tconst auto guard = gsl::finally([&] {\n\t\tif (_slice != slice) {\n\t\t\t_slice = std::move(slice);\n\t\t\t_refreshed.fire({});\n\t\t\tAssert((!_horizontal && !_vertical && !_bottom)\n\t\t\t\t|| (_slice.size() == _sectionsSlice.size()));\n\t\t}\n\t});\n\tconst auto push = [&](not_null<Data::Thread*> thread) {\n\t\tconst auto topic = thread->asTopic();\n\t\tconst auto sublist = thread->asSublist();\n\t\tauto badges = [&] {\n\t\t\tif (!topic && !sublist) {\n\t\t\t\treturn Dialogs::BadgesState();\n\t\t\t} else if (thread->chatListUnreadState().known) {\n\t\t\t\treturn thread->chatListBadgesState();\n\t\t\t}\n\t\t\tconst auto i = ranges::find(_slice, thread, &Item::thread);\n\t\t\tif (i != end(_slice)) {\n\t\t\t\t// While the unread count is unknown (possibly loading)\n\t\t\t\t// we can preserve the old badges state, because it won't\n\t\t\t\t// glitch that way when we stop knowing it for a moment.\n\t\t\t\treturn i->badges;\n\t\t\t}\n\t\t\treturn thread->chatListBadgesState();\n\t\t}();\n\t\tif (topic && badges.unreadCounter <= 0) {\n\t\t\t// Don't show the small indicators for non-visited unread topics.\n\t\t\tbadges.unread = false;\n\t\t}\n\t\tslice.push_back({\n\t\t\t.thread = thread,\n\t\t\t.badges = badges,\n\t\t\t.iconId = topic ? topic->iconId() : DocumentId(),\n\t\t\t.name = thread->chatListName(),\n\t\t});\n\t};\n\tif (!list) {\n\t\tpush(_history);\n\t\t_beforeSkipped = _afterSkipped = 0;\n\t\t_afterAvailable = 0;\n\t\treturn;\n\t}\n\tconst auto &chats = list->indexed()->all();\n\tauto i = (_around == _history)\n\t\t? chats.end()\n\t\t: ranges::find(chats, _around, [](not_null<Dialogs::Row*> row) {\n\t\t\treturn not_null(row->thread());\n\t\t});\n\tif (i == chats.end()) {\n\t\ti = chats.begin();\n\t}\n\tconst auto takeBefore = std::min(_beforeLimit, int(i - chats.begin()));\n\tconst auto takeAfter = std::min(_afterLimit, int(chats.end() - i));\n\tconst auto from = i - takeBefore;\n\tconst auto till = i + takeAfter;\n\t_beforeSkipped = std::max(0, int(from - chats.begin()));\n\t_afterAvailable = std::max(0, int(chats.end() - till));\n\t_afterSkipped = list->loaded() ? _afterAvailable : std::optional<int>();\n\tif (from == chats.begin()) {\n\t\tpush(_history);\n\t}\n\tfor (auto i = from; i != till; ++i) {\n\t\tpush((*i)->thread());\n\t}\n}\n\nvoid SubsectionTabs::scheduleRefresh() {\n\tif (_refreshScheduled) {\n\t\treturn;\n\t}\n\t_refreshScheduled = true;\n\tInvokeQueued(_shadow, [=] {\n\t\tif (_refreshScheduled) {\n\t\t\trefreshSlice();\n\t\t}\n\t});\n}\n\nMain::Session &SubsectionTabs::session() {\n\treturn _history->session();\n}\n\nUi::RpWidget *SubsectionTabs::activeWidget() const {\n\treturn _horizontal ? _horizontal : _vertical ? _vertical : _bottom;\n}\n\nbool SubsectionTabs::switchTo(\n\t\tnot_null<Data::Thread*> thread,\n\t\tnot_null<Ui::RpWidget*> parent) {\n\tExpects((_horizontal || _vertical || _bottom) && _shadow);\n\n\tif (thread->owningHistory() != _history) {\n\t\treturn false;\n\t}\n\t_active = thread;\n\tconst auto widget = activeWidget();\n\twidget->setParent(parent);\n\twidget->show();\n\t_shadow->setParent(parent);\n\t_shadow->show();\n\t_refreshed.fire({});\n\treturn true;\n}\n\nbool SubsectionTabs::UsedFor(not_null<Data::Thread*> thread) {\n\tconst auto history = thread->owningHistory();\n\treturn history->amMonoforumAdmin()\n\t\t|| history->peer->useSubsectionTabs();\n}\n\nvoid SubsectionTabs::applyReorder(\n\t\tnot_null<Ui::RpWidget*> widget,\n\t\tint oldPosition,\n\t\tint newPosition) {\n\tif (newPosition == oldPosition) {\n\t\treturn;\n\t}\n\n\tAssert(oldPosition >= 0 && oldPosition < _slice.size());\n\tAssert(newPosition >= 0 && newPosition < _slice.size());\n\n\tbase::reorder(_slice, oldPosition, newPosition);\n\n\tif (const auto forum = _history->asForum()) {\n\t\tauto topicIds = QVector<MTPint>();\n\t\tfor (const auto &item : _slice) {\n\t\t\tif (item.thread->isPinnedDialog(FilterId())) {\n\t\t\t\tif (const auto topic = item.thread->asTopic()) {\n\t\t\t\t\ttopicIds.push_back(MTP_int(topic->rootId()));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tforum->topicsList()->pinned()->applyList(forum, topicIds);\n\t\tsession().api().savePinnedOrder(forum);\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_subsection_tabs.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n#include \"dialogs/dialogs_common.h\"\n\nclass History;\n\nnamespace Data {\nclass Thread;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass RpWidget;\nclass PopupMenu;\nclass ScrollArea;\nclass SubsectionSlider;\nclass SubsectionSliderReorder;\n} // namespace Ui\n\nnamespace HistoryView {\n\nenum class SubsectionTabsMode : qint32 {\n\tTop = 0,\n\tLeft = 1,\n\tBottom = 2,\n};\n\nclass SubsectionTabs final {\npublic:\n\tSubsectionTabs(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Data::Thread*> thread);\n\t~SubsectionTabs();\n\n\t[[nodiscard]] Main::Session &session();\n\n\t[[nodiscard]] bool switchTo(\n\t\tnot_null<Data::Thread*> thread,\n\t\tnot_null<Ui::RpWidget*> parent);\n\n\t[[nodiscard]] static bool UsedFor(not_null<Data::Thread*> thread);\n\n\t[[nodiscard]] bool dying() const;\n\t[[nodiscard]] rpl::producer<> removeRequests() const;\n\n\tvoid extractToParent(not_null<Ui::RpWidget*> parent);\n\n\tvoid setBoundingRect(QRect boundingRect);\n\t[[nodiscard]] rpl::producer<> layoutRequests() const;\n\t[[nodiscard]] int leftSkip() const;\n\t[[nodiscard]] int topSkip() const;\n\t[[nodiscard]] int bottomSkip() const;\n\n\tvoid raise();\n\tvoid show();\n\tvoid hide();\n\nprivate:\n\tstruct Item {\n\t\tnot_null<Data::Thread*> thread;\n\t\tDialogs::BadgesState badges;\n\t\tDocumentId iconId = 0;\n\t\tQString name;\n\n\t\tfriend inline auto operator<=>(\n\t\t\tconst Item &,\n\t\t\tconst Item &) = default;\n\t\tfriend inline bool operator==(\n\t\t\tconst Item &,\n\t\t\tconst Item &) = default;\n\t};\n\n\tvoid track();\n\tvoid setupHorizontal(not_null<QWidget*> parent, bool bottom);\n\tvoid setupVertical(not_null<QWidget*> parent);\n\tvoid toggleModes();\n\tvoid setVisible(bool shown);\n\tvoid refreshSlice();\n\tvoid refreshAroundMiddle(\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\tnot_null<Ui::SubsectionSlider*> slider);\n\tvoid scheduleRefresh();\n\tvoid loadMore();\n\tvoid setup(not_null<Ui::RpWidget*> parent);\n\t[[nodiscard]] rpl::producer<> dataChanged() const;\n\t[[nodiscard]] Ui::RpWidget *activeWidget() const;\n\n\tvoid setupSlider(\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\tnot_null<Ui::SubsectionSlider*> slider,\n\t\tbool vertical);\n\tvoid startScrollChecking(\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\tnot_null<Ui::SubsectionSlider*> slider,\n\t\tbool vertical);\n\tvoid startFillingSlider(\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\tnot_null<Ui::SubsectionSlider*> slider,\n\t\tbool vertical);\n\tvoid showThreadContextMenu(not_null<Data::Thread*> thread);\n\tvoid applyReorder(\n\t\tnot_null<Ui::RpWidget*> widget,\n\t\tint oldPosition,\n\t\tint newPosition);\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<History*> _history;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tUi::RpWidget *_horizontal = nullptr;\n\tUi::RpWidget *_vertical = nullptr;\n\tUi::RpWidget *_bottom = nullptr;\n\tUi::RpWidget *_shadow = nullptr;\n\tstd::unique_ptr<Ui::SubsectionSliderReorder> _reorder;\n\tint _reordering = 0;\n\n\tstd::vector<Item> _slice;\n\tstd::vector<Item> _sectionsSlice;\n\n\tnot_null<Data::Thread*> _active;\n\tnot_null<Data::Thread*> _around;\n\tint _beforeLimit = 0;\n\tint _afterLimit = 0;\n\tint _afterAvailable = 0;\n\tbool _loading = false;\n\tbool _refreshScheduled = false;\n\tstd::optional<int> _beforeSkipped;\n\tstd::optional<int> _afterSkipped;\n\n\trpl::event_stream<> _layoutRequests;\n\trpl::event_stream<> _refreshed;\n\trpl::event_stream<> _scrollCheckRequests;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_summary_header.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_summary_header.h\"\n\n#include \"api/api_transcribes.h\"\n#include \"apiwrap.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_session.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/about_cocoon_box.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\n\nSummaryHeader::SummaryHeader()\n: _name(st::maxSignatureSize / 2)\n, _text(st::maxSignatureSize / 2) {\n}\n\nSummaryHeader &SummaryHeader::operator=(SummaryHeader &&other) = default;\n\nSummaryHeader::~SummaryHeader() = default;\n\nvoid SummaryHeader::update(not_null<Element*> view) {\n\tconst auto item = view->data();\n\n\tif (!_animation) {\n\t\tensureAnimation();\n\t}\n\tif (!_lottie) {\n\t\tensureLottie();\n\t}\n\n\t_text.setText(\n\t\tst::defaultTextStyle,\n\t\ttr::lng_summarize_header_about(tr::now));\n\n\t_name.setText(\n\t\tst::msgNameStyle,\n\t\ttr::lng_summarize_header_title(tr::now));\n\n\t_maxWidth = st::historyReplyPadding.left()\n\t\t+ st::maxSignatureSize / 2\n\t\t+ st::historyReplyPadding.right();\n\n\tconst auto session = &item->history()->session();\n\tconst auto itemId = item->fullId();\n\t_link = std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tif (Rect(iconRect().size()).contains(_iconRipple.lastPoint)) {\n\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\t\tcontroller->show(Box(Ui::AboutCocoonBox));\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (const auto item = session->data().message(itemId)) {\n\t\t\tsession->api().transcribes().toggleSummary(item);\n\t\t}\n\t});\n}\n\nbool SummaryHeader::isNameUpdated(not_null<const Element*> view) const {\n\treturn false;\n}\n\nint SummaryHeader::resizeToWidth(int width) const {\n\t_ripple.animation = nullptr;\n\t_height = st::historyReplyPadding.top()\n\t\t+ st::msgServiceNameFont->height * 2\n\t\t+ st::historyReplyPadding.bottom();\n\t_width = width;\n\treturn _height;\n}\n\nint SummaryHeader::height() const {\n\treturn _height + st::historyReplyTop + st::historyReplyBottom;\n}\n\nQMargins SummaryHeader::margins() const {\n\treturn QMargins(0, st::historyReplyTop, 0, st::historyReplyBottom);\n}\n\nvoid SummaryHeader::paint(\n\t\tPainter &p,\n\t\tnot_null<const Element*> view,\n\t\tconst Ui::ChatPaintContext &context,\n\t\tint x,\n\t\tint y,\n\t\tint w,\n\t\tbool inBubble) const {\n\tconst auto st = context.st;\n\tconst auto stm = context.messageStyle();\n\n\ty += st::historyReplyTop;\n\tconst auto rect = QRect(x, y, w, _height);\n\tconst auto colorPattern = 0;\n\tconst auto cache = !inBubble\n\t\t? st->serviceReplyCache(colorPattern).get()\n\t\t: stm->replyCache[colorPattern].get();\n\tconst auto &quoteSt = st::messageQuoteStyle;\n\tconst auto rippleColor = cache->bg;\n\tconst auto nameColor = !inBubble\n\t\t? st->msgImgReplyBarColor()->c\n\t\t: stm->msgServiceFg->c;\n\tif (!inBubble) {\n\t\tcache->bg = QColor(0, 0, 0, 0);\n\t}\n\tUi::Text::ValidateQuotePaintCache(*cache, quoteSt);\n\tUi::Text::FillQuotePaint(p, rect, *cache, quoteSt);\n\tif (!inBubble) {\n\t\tcache->bg = rippleColor;\n\t}\n\n\tif (!_lottie) {\n\t\tensureLottie();\n\t}\n\t{\n\t\tconst auto r = iconRect().translated(x, y);\n\t\tconst auto lottieX = r.x() + st::historySummaryHeaderIconSizeInner;\n\t\tconst auto lottieY = r.y() + st::historySummaryHeaderIconSizeInner;\n\t\t_lottie->paint(p, lottieX, lottieY, nameColor);\n\t\tif (_iconRipple.animation) {\n\t\t\t_iconRipple.animation->paint(\n\t\t\t\tp,\n\t\t\t\tr.x(),\n\t\t\t\tr.y(),\n\t\t\t\tr.width(),\n\t\t\t\t&rippleColor);\n\t\t\tif (_iconRipple.animation->empty()) {\n\t\t\t\t_iconRipple.animation.reset();\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!_animation) {\n\t\tensureAnimation();\n\t}\n\t{\n\t\tconst auto size = QSize(w, _height);\n\t\tif (_animation->cachedSize != size) {\n\t\t\t_animation->path = QPainterPath();\n\t\t\t_animation->path.addRoundedRect(\n\t\t\t\tQRect(0, 0, w, _height),\n\t\t\t\tquoteSt.radius,\n\t\t\t\tquoteSt.radius);\n\t\t\t_animation->cachedSize = size;\n\t\t}\n\t\tp.translate(x, y);\n\t\tp.setClipPath(_animation->path);\n\t\t_animation->particles.setColor(nameColor);\n\t\tconst auto paused = context.paused || On(PowerSaving::kChatEffects);\n\t\t_animation->particles.paint(\n\t\t\tp,\n\t\t\tQRect(0, 0, w, _height),\n\t\t\tcontext.now,\n\t\t\tpaused);\n\t\tif (!paused) {\n\t\t\tconst auto session = &view->history()->session();\n\t\t\tconst auto r = QRect(x, y, w, _height);\n\t\t\tUi::PostponeCall(session, [=, itemId = view->data()->fullId()] {\n\t\t\t\tif (const auto i = session->data().message(itemId)) {\n\t\t\t\t\tsession->data().requestItemRepaint(i, r);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\tp.setClipping(false);\n\t\tp.translate(-x, -y);\n\t}\n\n\tauto textLeft = x + st::historyReplyPadding.left();\n\tauto textTop = y + st::historyReplyPadding.top()\n\t\t+ st::msgServiceNameFont->height;\n\tif (w > st::historyReplyPadding.left()) {\n\t\tconst auto iconSpace = st::historySummaryHeaderIconSize\n\t\t\t+ st::historySummaryHeaderIconSizeInner * 2;\n\t\tconst auto textw = w\n\t\t\t- st::historyReplyPadding.left()\n\t\t\t- st::historyReplyPadding.right()\n\t\t\t- iconSpace;\n\t\tconst auto namew = textw;\n\t\tif (namew > 0) {\n\t\t\tp.setPen(nameColor);\n\t\t\t_name.drawLeftElided(\n\t\t\t\tp,\n\t\t\t\tx + st::historyReplyPadding.left(),\n\t\t\t\ty + st::historyReplyPadding.top(),\n\t\t\t\tnamew,\n\t\t\t\tw + 2 * x,\n\t\t\t\t1);\n\n\t\t\tp.setPen(inBubble\n\t\t\t\t? stm->historyTextFg\n\t\t\t\t: st->msgImgReplyBarColor());\n\t\t\tview->prepareCustomEmojiPaint(p, context, _text);\n\t\t\tauto replyToTextPalette = &(!inBubble\n\t\t\t\t? st->imgReplyTextPalette()\n\t\t\t\t: stm->replyTextPalette);\n\t\t\t_text.draw(p, {\n\t\t\t\t.position = { textLeft, textTop },\n\t\t\t\t.availableWidth = textw,\n\t\t\t\t.palette = replyToTextPalette,\n\t\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t\t.now = context.now,\n\t\t\t\t.pausedEmoji = (context.paused\n\t\t\t\t\t|| On(PowerSaving::kEmojiChat)),\n\t\t\t\t.pausedSpoiler = (context.paused\n\t\t\t\t\t|| On(PowerSaving::kChatSpoiler)),\n\t\t\t\t.elisionLines = 1,\n\t\t\t});\n\t\t\tp.setTextPalette(stm->textPalette);\n\t\t}\n\t}\n\n\tif (_ripple.animation) {\n\t\t_ripple.animation->paint(p, x, y, w, &rippleColor);\n\t\tif (_ripple.animation->empty()) {\n\t\t\t_ripple.animation.reset();\n\t\t}\n\t}\n}\n\nvoid SummaryHeader::createRippleAnimation(\n\t\tnot_null<const Element*> view,\n\t\tQSize size) {\n\t_ripple.animation = std::make_unique<Ui::RippleAnimation>(\n\t\tst::defaultRippleAnimation,\n\t\tUi::RippleAnimation::RoundRectMask(\n\t\t\tsize,\n\t\t\tst::messageQuoteStyle.radius),\n\t\t[=] { view->repaint(); });\n\tconst auto rippleIconSize = st::historySummaryHeaderIconSize;\n\t_iconRipple.animation = std::make_unique<Ui::RippleAnimation>(\n\t\tst::defaultRippleAnimation,\n\t\tUi::RippleAnimation::EllipseMask(Size(rippleIconSize)),\n\t\t[=] { view->repaint(); });\n}\n\nvoid SummaryHeader::saveRipplePoint(QPoint point) const {\n\t_ripple.lastPoint = point;\n\tconst auto rect = iconRect();\n\tif (rect.contains(point)) {\n\t\t_iconRipple.lastPoint = point - rect.topLeft();\n\t} else {\n\t\t_iconRipple.lastPoint = QPoint(-1, -1);\n\t}\n}\n\nvoid SummaryHeader::addRipple() {\n\tif (_iconRipple.lastPoint.x() >= 0 && _iconRipple.animation) {\n\t\t_iconRipple.animation->add(_iconRipple.lastPoint);\n\t} else if (_ripple.animation) {\n\t\t_ripple.animation->add(_ripple.lastPoint);\n\t}\n}\n\nvoid SummaryHeader::stopLastRipple() {\n\tif (_ripple.animation) {\n\t\t_ripple.animation->lastStop();\n\t}\n\tif (_iconRipple.animation) {\n\t\t_iconRipple.animation->lastStop();\n\t}\n}\n\nvoid SummaryHeader::unloadHeavyPart() {\n\t_unloadTime = crl::now();\n\t_animation = nullptr;\n\t_ripple.animation = nullptr;\n\t_iconRipple.animation = nullptr;\n\t_lottie = nullptr;\n}\n\nQRect SummaryHeader::iconRect() const {\n\tconst auto size = st::historySummaryHeaderIconSize;\n\tconst auto shift = st::historySummaryHeaderIconSizeInner;\n\treturn QRect(_width - size - shift, (_height - size) / 2, size, size);\n}\n\nvoid SummaryHeader::ensureAnimation() const {\n\tusing namespace Ui;\n\t_animation = std::make_unique<Animation>(Animation{\n\t\t.particles = StarParticles(\n\t\t\tStarParticles::Type::Right,\n\t\t\t15,\n\t\t\tst::lineWidth * 8),\n\t});\n\t_animation->particles.setSpeed(0.05);\n}\n\nvoid SummaryHeader::ensureLottie() const {\n\t_lottie = Lottie::MakeIcon(Lottie::IconDescriptor{\n\t\t.name = u\"cocoon\"_q,\n\t\t.color = &st::attentionButtonFg,\n\t\t.sizeOverride = Size(st::historySummaryHeaderIconSize\n\t\t\t- st::historySummaryHeaderIconSizeInner * 2),\n\t\t.colorizeUsingAlpha = true,\n\t});\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_summary_header.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/history_view_element.h\"\n#include \"ui/effects/ministar_particles.h\"\n#include \"lottie/lottie_icon.h\"\n\nnamespace HistoryView {\n\nclass SummaryHeader final : public RuntimeComponent<SummaryHeader, Element> {\npublic:\n\tSummaryHeader();\n\tSummaryHeader(const SummaryHeader &other) = delete;\n\tSummaryHeader(SummaryHeader &&other) = delete;\n\tSummaryHeader &operator=(const SummaryHeader &other) = delete;\n\tSummaryHeader &operator=(SummaryHeader &&other);\n\t~SummaryHeader();\n\n\tvoid update(not_null<Element*> view);\n\n\t[[nodiscard]] bool isNameUpdated(not_null<const Element*> view) const;\n\t[[nodiscard]] int resizeToWidth(int width) const;\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] QMargins margins() const;\n\n\tvoid paint(\n\t\tPainter &p,\n\t\tnot_null<const Element*> view,\n\t\tconst Ui::ChatPaintContext &context,\n\t\tint x,\n\t\tint y,\n\t\tint w,\n\t\tbool inBubble) const;\n\n\tvoid createRippleAnimation(not_null<const Element*> view, QSize size);\n\tvoid saveRipplePoint(QPoint point) const;\n\tvoid addRipple();\n\tvoid stopLastRipple();\n\n\t[[nodiscard]] int maxWidth() const {\n\t\treturn _maxWidth;\n\t}\n\t[[nodiscard]] ClickHandlerPtr link() const {\n\t\treturn _link;\n\t}\n\n\tvoid unloadHeavyPart();\n\nprivate:\n\tvoid ensureAnimation() const;\n\tvoid ensureLottie() const;\n\n\t[[nodiscard]] QRect iconRect() const;\n\tstruct Animation {\n\t\tUi::StarParticles particles;\n\t\tQPainterPath path;\n\t\tQSize cachedSize;\n\t};\n\n\tClickHandlerPtr _link;\n\tmutable std::unique_ptr<Animation> _animation;\n\tmutable struct {\n\t\tmutable std::unique_ptr<Ui::RippleAnimation> animation;\n\t\tQPoint lastPoint;\n\t} _ripple;\n\tmutable struct {\n\t\tmutable std::unique_ptr<Ui::RippleAnimation> animation;\n\t\tQPoint lastPoint;\n\t} _iconRipple;\n\tmutable Ui::Text::String _name;\n\tmutable Ui::Text::String _text;\n\tmutable int _maxWidth = 0;\n\tmutable int _height = 0;\n\tmutable int _width = 0;\n\tmutable std::unique_ptr<Lottie::Icon> _lottie;\n\tmutable crl::time _unloadTime = 0;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_text_helper.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_text_helper.h\"\n\n#include \"core/click_handler_types.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_reaction_preview.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n#include \"base/weak_ptr.h\"\n\nnamespace HistoryView {\n\nvoid InitElementTextPart(not_null<Element*> view, Ui::Text::String &text) {\n\tif (text.hasSpoilers()) {\n\t\ttext.setSpoilerLinkFilter([weak = base::make_weak(view)](\n\t\t\t\tconst ClickContext &context) {\n\t\t\tconst auto button = context.button;\n\t\t\tconst auto view = weak.get();\n\t\t\tif (button != Qt::LeftButton || !view) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tview->history()->owner().registerShownSpoiler(view);\n\t\t\treturn true;\n\t\t});\n\t}\n\tif (text.hasCollapsedBlockquots()) {\n\t\tconst auto weak = base::make_weak(view);\n\t\ttext.setBlockquoteExpandCallback([=](int quoteIndex, bool expanded) {\n\t\t\tif (const auto view = weak.get()) {\n\t\t\t\tview->blockquoteExpandChanged();\n\t\t\t}\n\t\t});\n\t}\n\tif (text.hasCustomEmoji()) {\n\t\ttext.setCustomEmojiClickHandler(\n\t\t\t[](QStringView entityData) {\n\t\t\t\treturn Data::ParseCustomEmojiData(entityData) != 0;\n\t\t\t},\n\t\t\t[weak = base::make_weak(view)](\n\t\t\t\t\tQStringView entityData,\n\t\t\t\t\tClickContext context) {\n\t\t\t\tconst auto view = weak.get();\n\t\t\t\tif (!view) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\t\t\tconst auto documentId = Data::ParseCustomEmojiData(entityData);\n\t\t\t\t\tif (documentId) {\n\t\t\t\t\t\tShowReactionPreview(\n\t\t\t\t\t\t\tcontroller,\n\t\t\t\t\t\t\tmy.itemId,\n\t\t\t\t\t\t\tData::ReactionId{ documentId },\n\t\t\t\t\t\t\ttrue);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_text_helper.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace HistoryView {\n\nclass Element;\n\nvoid InitElementTextPart(not_null<Element*> view, Ui::Text::String &text);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_top_bar_widget.h\"\n\n#include \"history/history.h\"\n#include \"history/view/history_view_send_action.h\"\n#include \"boxes/add_contact_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"info/info_memento.h\"\n#include \"info/info_controller.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"lang/lang_keys.h\"\n#include \"core/shortcuts.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/boxes/report_box_graphics.h\" // Ui::ReportReason\n#include \"ui/text/text.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/painter.h\"\n#include \"ui/unread_badge.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_adaptive.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_peer_menu.h\"\n#include \"calls/calls_instance.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_group_call.h\" // GroupCall::input.\n#include \"data/data_folder.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_send_action.h\"\n#include \"dialogs/dialogs_main_list.h\"\n#include \"chat_helpers/emoji_interactions.h\"\n#include \"base/call_delayed.h\"\n#include \"base/unixtime.h\"\n#include \"support/support_helper.h\"\n#include \"apiwrap.h\"\n#include \"api/api_chat_participants.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtGui/QWindow>\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kEmojiInteractionSeenDuration = 3 * crl::time(1000);\n\n[[nodiscard]] inline bool HasGroupCallMenu(not_null<PeerData*> peer) {\n\treturn !peer->isUser()\n\t\t&& !peer->groupCall()\n\t\t&& peer->canManageGroupCall();\n}\n\nQString TopBarNameText(\n\t\tnot_null<PeerData*> peer,\n\t\tconst Dialogs::EntryState &state) {\n\tif (state.section == Dialogs::EntryState::Section::SavedSublist\n\t\t&& state.key.sublist()\n\t\t&& state.key.sublist()->owningHistory()->peer->isSelf()) {\n\t\tif (peer->isSelf()) {\n\t\t\treturn tr::lng_my_notes(tr::now);\n\t\t} else if (peer->isSavedHiddenAuthor()) {\n\t\t\treturn tr::lng_hidden_author_messages(tr::now);\n\t\t}\n\t}\n\treturn peer->topBarNameText();\n}\n\n} // namespace\n\nstruct TopBarWidget::EmojiInteractionSeenAnimation {\n\tUi::SendActionAnimation animation;\n\tUi::Animations::Basic scheduler;\n\tUi::Text::String text = { st::dialogsTextWidthMin };\n\tcrl::time till = 0;\n};\n\nQString SwitchToChooseFromQuery() {\n\treturn u\"from:\"_q;\n}\n\nTopBarWidget::TopBarWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _primaryWindow(controller->isPrimary())\n, _clear(this, tr::lng_selected_clear(), st::topBarClearButton)\n, _forward(this, tr::lng_selected_forward(), st::defaultActiveButton)\n, _sendNow(this, tr::lng_selected_send_now(), st::defaultActiveButton)\n, _delete(this, tr::lng_selected_delete(), st::defaultActiveButton)\n, _forwardAndDelete(this, rpl::single(u\"Both\"_q), st::defaultActiveButton)\n, _back(this, st::historyTopBarBack)\n, _cancelChoose(this, st::topBarCloseChoose)\n, _call(this, st::topBarCall)\n, _groupCall(this, st::topBarGroupCall)\n, _search(this, st::topBarSearch)\n, _infoToggle(this, st::topBarInfo)\n, _menuToggle(this, st::topBarMenuToggle)\n, _titlePeerText(st::windowMinWidth / 3)\n, _onlineUpdater([=] { updateOnlineDisplay(); }) {\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\n\t_clear->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\t_forward->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\t_sendNow->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\t_delete->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\n\tLang::Updated(\n\t) | rpl::on_next([=] {\n\t\trefreshLang();\n\t}, lifetime());\n\n\t_forward->setClickedCallback([=] { _forwardSelection.fire({}); });\n\t_forward->setAcceptBoth();\n\t_forward->addClickHandler([=](Qt::MouseButton b) {\n\t\tif (b == Qt::MiddleButton) {\n\t\t\t_forwardAndDeleteSelection.fire({});\n\t\t}\n\t});\n\t_forward->setWidthChangedCallback([=] { updateControlsGeometry(); });\n\t_forwardAndDelete->setClickedCallback([=] { _forwardAndDeleteSelection.fire({}); });\n\t_forwardAndDelete->setWidthChangedCallback([=] { updateControlsGeometry(); });\n\t_sendNow->setClickedCallback([=] { _sendNowSelection.fire({}); });\n\t_sendNow->setWidthChangedCallback([=] { updateControlsGeometry(); });\n\t_delete->setClickedCallback([=] { _deleteSelection.fire({}); });\n\t_delete->setWidthChangedCallback([=] { updateControlsGeometry(); });\n\t_clear->setClickedCallback([=] { _clearSelection.fire({}); });\n\t_call->setClickedCallback([=] { call({}); });\n\t_call->setAcceptBoth(true, true);\n\t_call->addClickHandler([=](Qt::MouseButton button) {\n\t\tif (button == Qt::RightButton) {\n\t\t\tshowCallMenu();\n\t\t}\n\t});\n\t_groupCall->setClickedCallback([=] { groupCall(); });\n\t_menuToggle->addClickHandler([=](auto) { showPeerMenu(); });\n\t_menuToggle->setAcceptBoth(true, true);\n\t_infoToggle->setClickedCallback([=] { toggleInfoSection(); });\n\t_back->setAcceptBoth();\n\t_back->addClickHandler([=](Qt::MouseButton) {\n\t\tInvokeQueued(_back.data(), [=] { backClicked(); });\n\t});\n\t_cancelChoose->setClickedCallback(\n\t\t[=] { _cancelChooseForReport.fire({}); });\n\n\trpl::combine(\n\t\t_controller->activeChatValue(),\n\t\t_controller->searchInChatValue()\n\t) | rpl::combine_previous(\n\t\tstd::make_tuple(Dialogs::Key(), Dialogs::Key())\n\t) | rpl::map([](\n\t\t\tconst std::tuple<Dialogs::Key, Dialogs::Key> &previous,\n\t\t\tconst std::tuple<Dialogs::Key, Dialogs::Key> &current) {\n\t\tconst auto &active = std::get<0>(current);\n\t\tconst auto &search = std::get<1>(current);\n\t\tconst auto activeChanged = (active != std::get<0>(previous));\n\t\tconst auto searchInChat = search && (active == search);\n\t\treturn std::make_tuple(searchInChat, activeChanged);\n\t}) | rpl::on_next([=](\n\t\t\tbool searchInActiveChat,\n\t\t\tbool activeChanged) {\n\t\tauto animated = activeChanged\n\t\t\t? anim::type::instant\n\t\t\t: anim::type::normal;\n\t\t_search->setForceRippled(searchInActiveChat, animated);\n\t}, lifetime());\n\n\tcontroller->adaptive().changes(\n\t) | rpl::on_next([=] {\n\t\tupdateAdaptiveLayout();\n\t}, lifetime());\n\n\trefreshUnreadBadge();\n\t{\n\t\tusing AnimationUpdate = Data::SendActionManager::AnimationUpdate;\n\t\tsession().data().sendActionManager().animationUpdated(\n\t\t) | rpl::filter([=](const AnimationUpdate &update) {\n\t\t\treturn (update.thread == _activeChat.key.thread());\n\t\t}) | rpl::on_next([=] {\n\t\t\tupdate();\n\t\t}, lifetime());\n\t}\n\n\tusing UpdateFlag = Data::PeerUpdate::Flag;\n\tsession().changes().peerUpdates(\n\t\tUpdateFlag::HasCalls\n\t\t| UpdateFlag::OnlineStatus\n\t\t| UpdateFlag::Members\n\t\t| UpdateFlag::SupportInfo\n\t\t| UpdateFlag::Rights\n\t\t| UpdateFlag::EmojiStatus\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tif (update.flags & UpdateFlag::HasCalls) {\n\t\t\tif (update.peer->isUser()\n\t\t\t\t&& (update.peer->isSelf()\n\t\t\t\t\t|| _activeChat.key.peer() == update.peer)) {\n\t\t\t\tupdateControlsVisibility();\n\t\t\t}\n\t\t} else if ((update.flags & UpdateFlag::Rights)\n\t\t\t&& (_activeChat.key.peer() == update.peer)) {\n\t\t\tupdateControlsVisibility();\n\t\t}\n\t\tif ((update.flags & UpdateFlag::OnlineStatus)\n\t\t\t&& trackOnlineOf(update.peer)) {\n\t\t\tupdateOnlineDisplay();\n\t\t} else if (update.flags\n\t\t\t& (UpdateFlag::Members | UpdateFlag::SupportInfo)) {\n\t\t\tif (update.peer == _activeChat.key.peer()\n\t\t\t\t&& !_activeChat.key.topic()) {\n\t\t\t\tupdateOnlineDisplay();\n\t\t\t}\n\t\t}\n\t\tif ((update.flags & UpdateFlag::EmojiStatus)\n\t\t\t&& (_activeChat.key.peer() == update.peer)) {\n\t\t\tthis->update();\n\t\t}\n\t}, lifetime());\n\n\trpl::combine(\n\t\tCore::App().settings().thirdSectionInfoEnabledValue(),\n\t\tCore::App().settings().tabbedReplacedWithInfoValue()\n\t) | rpl::on_next([=] {\n\t\tupdateInfoToggleActive();\n\t}, lifetime());\n\n\tCore::App().settings().proxy().connectionTypeValue(\n\t) | rpl::on_next([=] {\n\t\tupdateConnectingState();\n\t}, lifetime());\n\n\tsetCursor(style::cur_pointer);\n\t_call->setAccessibleName(tr::lng_profile_action_short_call(tr::now));\n\t_groupCall->setAccessibleName(tr::lng_group_call_title(tr::now));\n\t_search->setAccessibleName(tr::lng_shortcuts_search(tr::now));\n\t_infoToggle->setAccessibleName(tr::lng_settings_section_info(tr::now));\n\t_menuToggle->setAccessibleName(tr::lng_chat_menu(tr::now));\n\t_back->setAccessibleName(tr::lng_go_back(tr::now));\n\t_cancelChoose->setAccessibleName(tr::lng_cancel(tr::now));\n}\n\nTopBarWidget::~TopBarWidget() = default;\n\nMain::Session &TopBarWidget::session() const {\n\treturn _controller->session();\n}\n\nvoid TopBarWidget::updateConnectingState() {\n\tconst auto state = _controller->session().mtp().dcstate();\n\tconst auto exposed = window()->windowHandle()\n\t\t&& window()->windowHandle()->isExposed();\n\tif (state == MTP::ConnectedState || !exposed) {\n\t\tif (_connecting) {\n\t\t\t_connecting = nullptr;\n\t\t\tupdate();\n\t\t}\n\t} else if (!_connecting) {\n\t\t_connecting = std::make_unique<Ui::InfiniteRadialAnimation>(\n\t\t\t[=] { connectingAnimationCallback(); },\n\t\t\tst::topBarConnectingAnimation);\n\t\t_connecting->start();\n\t\tupdate();\n\t}\n}\n\nvoid TopBarWidget::connectingAnimationCallback() {\n\tif (!anim::Disabled()) {\n\t\tupdateConnectingState();\n\t\tupdate();\n\t}\n}\n\nvoid TopBarWidget::refreshLang() {\n\tInvokeQueued(this, [this] { updateControlsGeometry(); });\n}\n\nvoid TopBarWidget::call(Calls::StartOutgoingCallArgs args) {\n\tif (_controller->showFrozenError()) {\n\t\treturn;\n\t} else if (const auto peer = _activeChat.key.peer()) {\n\t\tif (const auto user = peer->asUser()) {\n\t\t\tCore::App().calls().startOutgoingCall(user, std::move(args));\n\t\t}\n\t}\n}\n\nvoid TopBarWidget::groupCall() {\n\tif (_controller->showFrozenError()) {\n\t\treturn;\n\t} else if (const auto peer = _activeChat.key.peer()) {\n\t\tif (HasGroupCallMenu(peer)) {\n\t\t\tshowGroupCallMenu(peer);\n\t\t} else {\n\t\t\t_controller->startOrJoinGroupCall(peer, {});\n\t\t}\n\t}\n}\n\nvoid TopBarWidget::showChooseMessagesForReport(Data::ReportInput input) {\n\tsetChooseForReportReason(input);\n}\n\nvoid TopBarWidget::clearChooseMessagesForReport() {\n\tsetChooseForReportReason(std::nullopt);\n}\n\nrpl::producer<> TopBarWidget::searchRequest() const {\n\treturn _search->clicks() | rpl::to_empty;\n}\n\nvoid TopBarWidget::setChooseForReportReason(\n\t\tstd::optional<Data::ReportInput> reportInput) {\n\tif (_chooseForReportReason == reportInput) {\n\t\treturn;\n\t}\n\tconst auto wasNoReason = !_chooseForReportReason;\n\t_chooseForReportReason = reportInput;\n\tconst auto nowNoReason = !_chooseForReportReason;\n\tupdateControlsVisibility();\n\tupdateControlsGeometry();\n\tupdate();\n\tif (wasNoReason != nowNoReason && showSelectedState()) {\n\t\ttoggleSelectedControls(false);\n\t\tfinishAnimating();\n\t}\n\tsetCursor((nowNoReason && !showSelectedState())\n\t\t? style::cur_pointer\n\t\t: style::cur_default);\n}\n\nbool TopBarWidget::createMenu(\n\t\tnot_null<Ui::IconButton*> button,\n\t\tbool withIcons) {\n\tif (!_activeChat.key || _menu) {\n\t\treturn false;\n\t}\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\twithIcons\n\t\t\t? st::popupMenuExpandedSeparator\n\t\t\t: st::defaultPopupMenu);\n\t_menu->setDestroyedCallback([\n\t\t\tweak = base::make_weak(this),\n\t\t\tweakButton = base::make_weak(button),\n\t\t\tmenu = _menu.get()] {\n\t\tif (weak && weak->_menu == menu) {\n\t\t\tif (weakButton) {\n\t\t\t\tweakButton->setForceRippled(false);\n\t\t\t}\n\t\t}\n\t});\n\tbutton->setForceRippled(true);\n\treturn true;\n}\n\nvoid TopBarWidget::showPeerMenu() {\n\tconst auto created = createMenu(_menuToggle);\n\tif (!created) {\n\t\treturn;\n\t}\n\tconst auto addAction = Ui::Menu::CreateAddActionCallback(_menu);\n\tWindow::FillDialogsEntryMenu(_controller, _activeChat, addAction);\n\tif (_menu->empty()) {\n\t\t_menu = nullptr;\n\t} else {\n\t\t_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);\n\t\t_menu->popup(Ui::PopupMenu::ConstrainToParentScreen(\n\t\t\t_menu,\n\t\t\tmapToGlobal(\n\t\t\t\tQPoint(\n\t\t\t\t\twidth()\n\t\t\t\t\t\t+ st::topBarMenuPosition.x()\n\t\t\t\t\t\t+ Ui::BoxShadow::ExtendFor(\n\t\t\t\t\t\t\t_menu->st().shadow).right(),\n\t\t\t\t\tst::topBarMenuPosition.y()))));\n\t}\n}\n\nvoid TopBarWidget::showGroupCallMenu(not_null<PeerData*> peer) {\n\tconst auto created = createMenu(_groupCall);\n\tif (!created) {\n\t\treturn;\n\t}\n\tconst auto addAction = Ui::Menu::CreateAddActionCallback(_menu);\n\tWindow::FillVideoChatMenu(_controller, _activeChat, addAction);\n\t_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);\n\t_menu->popup(mapToGlobal(QPoint(\n\t\t_groupCall->x() + _groupCall->width() + st::topBarMenuGroupCallSkip,\n\t\tst::topBarMenuPosition.y())));\n}\n\nvoid TopBarWidget::showCallMenu() {\n\tconst auto created = createMenu(_call, false);\n\tif (!created) {\n\t\treturn;\n\t}\n\tconst auto perform = [&](bool video) {\n\t\treturn [=] {\n\t\t\tbase::call_delayed(st::defaultPopupMenu.showDuration, this, [=] {\n\t\t\t\tcall({ .video = video, .isConfirmed = true });\n\t\t\t});\n\t\t};\n\t};\n\t_menu->addAction(\n\t\ttr::lng_profile_action_short_call(tr::now),\n\t\tperform(false));\n\t_menu->addAction(\n\t\ttr::lng_call_start_video(tr::now),\n\t\tperform(true));\n\t_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);\n\t_menu->popup(mapToGlobal(QPoint(\n\t\t_call->x() + _call->width() + st::topBarMenuGroupCallSkip,\n\t\tst::topBarMenuPosition.y())));\n}\n\nvoid TopBarWidget::toggleInfoSection() {\n\tconst auto isThreeColumn = _controller->adaptive().isThreeColumn();\n\tif (isThreeColumn\n\t\t&& (Core::App().settings().thirdSectionInfoEnabled()\n\t\t\t|| Core::App().settings().tabbedReplacedWithInfo())) {\n\t\t_controller->closeThirdSection();\n\t} else if (_activeChat.key.peer()) {\n\t\tif (_controller->canShowThirdSection()) {\n\t\t\tCore::App().settings().setThirdSectionInfoEnabled(true);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\tif (isThreeColumn) {\n\t\t\t\t_controller->showSection(\n\t\t\t\t\t(_activeChat.key.topic()\n\t\t\t\t\t\t? std::make_shared<Info::Memento>(\n\t\t\t\t\t\t\t_activeChat.key.topic())\n\t\t\t\t\t\t: (_activeChat.key.sublist()\n\t\t\t\t\t\t\t&& _activeChat.key.sublist()->parentChat())\n\t\t\t\t\t\t? std::make_shared<Info::Memento>(\n\t\t\t\t\t\t\t_activeChat.key.sublist())\n\t\t\t\t\t\t: Info::Memento::Default(_activeChat.key.peer())),\n\t\t\t\t\tWindow::SectionShow().withThirdColumn());\n\t\t\t} else {\n\t\t\t\t_controller->resizeForThirdSection();\n\t\t\t\t_controller->updateColumnLayout();\n\t\t\t}\n\t\t} else {\n\t\t\tinfoClicked();\n\t\t}\n\t} else {\n\t\tupdateControlsVisibility();\n\t}\n}\n\nbool TopBarWidget::eventFilter(QObject *obj, QEvent *e) {\n\tif (obj == _membersShowArea) {\n\t\tswitch (e->type()) {\n\t\tcase QEvent::MouseButtonPress:\n\t\t\tmousePressEvent(static_cast<QMouseEvent*>(e));\n\t\t\treturn true;\n\n\t\tcase QEvent::Enter:\n\t\t\t_membersShowAreaActive.fire(true);\n\t\t\tbreak;\n\n\t\tcase QEvent::Leave:\n\t\t\t_membersShowAreaActive.fire(false);\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn RpWidget::eventFilter(obj, e);\n}\n\nint TopBarWidget::resizeGetHeight(int newWidth) {\n\treturn st::topBarHeight;\n}\n\nvoid TopBarWidget::paintEvent(QPaintEvent *e) {\n\tif (_animatingMode) {\n\t\treturn;\n\t}\n\tupdateConnectingState();\n\tPainter p(this);\n\n\tconst auto selectedButtonsTop = countSelectedButtonsTop(\n\t\t_selectedShown.value(showSelectedActions() ? 1. : 0.));\n\tconst auto searchFieldTop = _searchField\n\t\t? countSelectedButtonsTop(_searchShown.value(_searchMode ? 1. : 0.))\n\t\t: -st::topBarHeight;\n\tconst auto slidingTop = std::max(selectedButtonsTop, searchFieldTop);\n\n\tp.fillRect(QRect(0, 0, width(), st::topBarHeight), st::topBarBg);\n\tif (slidingTop < 0) {\n\t\tp.translate(0, slidingTop + st::topBarHeight);\n\t\tpaintTopBar(p);\n\t}\n}\n\nvoid TopBarWidget::paintTopBar(Painter &p) {\n\tif (!_activeChat.key || _narrowRatio == 1.) {\n\t\treturn;\n\t}\n\tauto nameleft = _leftTaken;\n\tauto statusleft = nameleft;\n\tauto nametop = st::topBarArrowPadding.top();\n\tauto statustop = st::topBarHeight - st::topBarArrowPadding.bottom() - st::dialogsTextFont->height;\n\tauto namewidth = width()\n\t\t- _rightTaken\n\t\t- nameleft\n\t\t- st::topBarNameRightPadding;\n\tauto statuswidth = namewidth;\n\n\tif (_chooseForReportReason) {\n\t\tconst auto text = _chooseForReportReason->optionText;\n\t\tp.setPen(st::dialogsNameFg);\n\t\tp.setFont(st::semiboldFont);\n\t\tp.drawTextLeft(nameleft, nametop, width(), text);\n\n\t\tp.setFont(st::dialogsTextFont);\n\t\tp.setPen(st::historyStatusFg);\n\t\tp.drawTextLeft(\n\t\t\tnameleft,\n\t\t\tstatustop,\n\t\t\twidth(),\n\t\t\ttr::lng_report_select_messages(tr::now));\n\t\treturn;\n\t}\n\n\tconst auto now = crl::now();\n\tconst auto peer = _activeChat.key.owningHistory()\n\t\t? _activeChat.key.owningHistory()->peer.get()\n\t\t: nullptr;\n\tconst auto folder = _activeChat.key.folder();\n\tconst auto sublist = _activeChat.key.sublist();\n\tconst auto topic = _activeChat.key.topic();\n\tconst auto history = _activeChat.key.history();\n\tconst auto broadcastForMonoforum = history\n\t\t? history->peer->monoforumBroadcast()\n\t\t: nullptr;\n\tconst auto namePeer = broadcastForMonoforum\n\t\t? broadcastForMonoforum\n\t\t: history\n\t\t? history->peer.get()\n\t\t: sublist\n\t\t? sublist->sublistPeer().get()\n\t\t: nullptr;\n\tif (topic && _activeChat.section == Section::Replies) {\n\t\tp.setPen(st::dialogsNameFg);\n\t\ttopic->chatListNameText().drawElided(\n\t\t\tp,\n\t\t\tnameleft,\n\t\t\tnametop,\n\t\t\tnamewidth);\n\n\t\tp.setFont(st::dialogsTextFont);\n\t\tif (!paintConnectingState(p, nameleft, statustop, width())\n\t\t\t&& !paintSendAction(\n\t\t\t\tp,\n\t\t\t\tnameleft,\n\t\t\t\tstatustop,\n\t\t\t\tnamewidth,\n\t\t\t\twidth(),\n\t\t\t\tst::historyStatusFgTyping,\n\t\t\t\tnow)) {\n\t\t\tp.setPen(st::historyStatusFg);\n\t\t\tp.drawTextLeft(nameleft, statustop, width(), _customTitleText);\n\t\t}\n\t} else if (folder\n\t\t|| (peer\n\t\t\t&& (peer->sharedMediaInfo() || peer->isVerifyCodes())\n\t\t\t&& _activeChat.section != Section::SavedSublist)\n\t\t|| (_activeChat.section == Section::Scheduled)\n\t\t|| (_activeChat.section == Section::Pinned)) {\n\t\tauto text = (_activeChat.section == Section::Scheduled)\n\t\t\t? ((peer && peer->isSelf())\n\t\t\t\t? tr::lng_reminder_messages(tr::now)\n\t\t\t\t: tr::lng_scheduled_messages(tr::now))\n\t\t\t: (_activeChat.section == Section::Pinned)\n\t\t\t? _customTitleText\n\t\t\t: folder\n\t\t\t? folder->chatListName()\n\t\t\t: peer->isSelf()\n\t\t\t? tr::lng_saved_messages(tr::now)\n\t\t\t: peer->isRepliesChat()\n\t\t\t? tr::lng_replies_messages(tr::now)\n\t\t\t: peer->isVerifyCodes()\n\t\t\t? tr::lng_verification_codes(tr::now)\n\t\t\t: peer->name();\n\t\tconst auto textWidth = st::historySavedFont->width(text);\n\t\tif (namewidth < textWidth) {\n\t\t\ttext = st::historySavedFont->elided(text, namewidth);\n\t\t}\n\t\tp.setPen(st::dialogsNameFg);\n\t\tp.setFont(st::historySavedFont);\n\t\tp.drawTextLeft(\n\t\t\tnameleft,\n\t\t\t(height() - st::historySavedFont->height) / 2,\n\t\t\twidth(),\n\t\t\ttext);\n\t} else if (_activeChat.section == Section::Replies) {\n\t\tp.setPen(st::dialogsNameFg);\n\t\tp.setFont(st::semiboldFont);\n\t\tp.drawTextLeft(\n\t\t\tnameleft,\n\t\t\tnametop,\n\t\t\twidth(),\n\t\t\ttr::lng_manage_discussion_group(tr::now));\n\n\t\tp.setFont(st::dialogsTextFont);\n\t\tif (!paintConnectingState(p, statusleft, statustop, width())\n\t\t\t&& !paintSendAction(\n\t\t\t\tp,\n\t\t\t\tstatusleft,\n\t\t\t\tstatustop,\n\t\t\t\tstatuswidth,\n\t\t\t\twidth(),\n\t\t\t\tst::historyStatusFgTyping,\n\t\t\t\tnow)) {\n\t\t\tpaintStatus(p, statusleft, statustop, statuswidth, width());\n\t\t}\n\t} else if (namePeer) {\n\t\tif (_titleNameVersion < namePeer->nameVersion()) {\n\t\t\t_titleNameVersion = namePeer->nameVersion();\n\t\t\t_title.setText(\n\t\t\t\tst::msgNameStyle,\n\t\t\t\tTopBarNameText(namePeer, _activeChat),\n\t\t\t\tUi::NameTextOptions());\n\t\t}\n\t\tif (const auto info = namePeer->botVerifyDetails()) {\n\t\t\tif (!_titleBadge.ready(info)) {\n\t\t\t\t_titleBadge.set(\n\t\t\t\t\tinfo,\n\t\t\t\t\tnamePeer->owner().customEmojiManager().factory(),\n\t\t\t\t\t[=] { update(); });\n\t\t\t}\n\t\t\tconst auto position = QPoint{ nameleft, nametop };\n\t\t\tconst auto skip = _titleBadge.drawVerified(p, position, st::dialogsVerifiedColors);\n\t\t\tnameleft += skip + st::dialogsChatTypeSkip;\n\t\t\tnamewidth -= skip + st::dialogsChatTypeSkip;\n\t\t}\n\t\tconst auto badgeWidth = _titleBadge.drawGetWidth(p, {\n\t\t\t.peer = namePeer,\n\t\t\t.rectForName = QRect(\n\t\t\t\tnameleft,\n\t\t\t\tnametop,\n\t\t\t\tnamewidth,\n\t\t\t\tst::msgNameStyle.font->height),\n\t\t\t.nameWidth = _title.maxWidth(),\n\t\t\t.outerWidth = width(),\n\t\t\t.verified = &st::dialogsVerifiedIcon,\n\t\t\t.premium = &st::dialogsPremiumIcon.icon,\n\t\t\t.scam = &st::attentionButtonFg,\n\t\t\t.direct = &st::windowSubTextFg,\n\t\t\t.premiumFg = &st::dialogsVerifiedIconBg,\n\t\t\t.customEmojiRepaint = [=] { update(); },\n\t\t\t.now = now,\n\t\t\t.bothVerifyAndStatus = true,\n\t\t\t.paused = _controller->isGifPausedAtLeastFor(\n\t\t\t\tWindow::GifPauseReason::Any),\n\t\t});\n\t\tnamewidth -= badgeWidth;\n\n\t\tp.setPen(st::dialogsNameFg);\n\t\t_title.draw(p, {\n\t\t\t.position = { nameleft, nametop },\n\t\t\t.availableWidth = namewidth,\n\t\t\t.elisionLines = 1,\n\t\t});\n\n\t\tp.setFont(st::dialogsTextFont);\n\t\tif (!paintConnectingState(p, statusleft, statustop, width())\n\t\t\t&& !paintSendAction(\n\t\t\t\tp,\n\t\t\t\tstatusleft,\n\t\t\t\tstatustop,\n\t\t\t\tstatuswidth,\n\t\t\t\twidth(),\n\t\t\t\tst::historyStatusFgTyping,\n\t\t\t\tnow)) {\n\t\t\tpaintStatus(p, statusleft, statustop, statuswidth, width());\n\t\t}\n\t}\n}\n\nbool TopBarWidget::paintSendAction(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tstyle::color fg,\n\t\tcrl::time now) {\n\tif (!_sendAction) {\n\t\treturn false;\n\t}\n\tconst auto seen = _emojiInteractionSeen.get();\n\tif (!seen || seen->till <= now) {\n\t\treturn _sendAction->paint(p, x, y, availableWidth, outerWidth, fg, now);\n\t}\n\tconst auto animationWidth = seen->animation.width();\n\tconst auto extraAnimationWidth = animationWidth * 2;\n\tseen->animation.paint(\n\t\tp,\n\t\tfg,\n\t\tx,\n\t\ty + st::normalFont->ascent,\n\t\touterWidth,\n\t\tnow);\n\n\tx += animationWidth;\n\tavailableWidth -= extraAnimationWidth;\n\tp.setPen(fg);\n\tseen->text.drawElided(p, x, y, availableWidth);\n\treturn true;\n}\n\nbool TopBarWidget::paintConnectingState(\n\t\tPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint outerWidth) {\n\tif (!_connecting) {\n\t\treturn false;\n\t}\n\t_connecting->draw(\n\t\tp,\n\t\t{\n\t\t\tst::topBarConnectingPosition.x() + left,\n\t\t\tst::topBarConnectingPosition.y() + top\n\t\t},\n\t\touterWidth);\n\tleft += st::topBarConnectingPosition.x()\n\t\t+ st::topBarConnectingAnimation.size.width()\n\t\t+ st::topBarConnectingSkip;\n\tp.setPen(st::historyStatusFg);\n\tp.drawTextLeft(left, top, outerWidth, tr::lng_status_connecting(tr::now));\n\treturn true;\n}\n\nvoid TopBarWidget::paintStatus(\n\t\tPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint availableWidth,\n\t\tint outerWidth) {\n\tusing Section = Dialogs::EntryState::Section;\n\tconst auto section = _activeChat.section;\n\tif (section == Section::Replies || section == Section::SavedSublist) {\n\t\tp.setPen(st::historyStatusFg);\n\t\tp.drawTextLeft(left, top, outerWidth, _customTitleText);\n\t} else {\n\t\tp.setPen(_titlePeerTextOnline\n\t\t\t? st::historyStatusFgActive\n\t\t\t: st::historyStatusFg);\n\t\t_titlePeerText.drawLeftElided(\n\t\t\tp,\n\t\t\tleft,\n\t\t\ttop,\n\t\t\tavailableWidth,\n\t\t\touterWidth);\n\t}\n}\n\nQRect TopBarWidget::getMembersShowAreaGeometry() const {\n\tint membersTextLeft = _leftTaken;\n\tint membersTextTop = st::topBarHeight - st::topBarArrowPadding.bottom() - st::dialogsTextFont->height;\n\tint membersTextWidth = _titlePeerText.maxWidth();\n\tint membersTextHeight = st::topBarHeight - membersTextTop;\n\n\treturn myrtlrect(membersTextLeft, membersTextTop, membersTextWidth, membersTextHeight);\n}\n\nvoid TopBarWidget::mousePressEvent(QMouseEvent *e) {\n\tconst auto handleClick = (e->button() == Qt::LeftButton)\n\t\t&& (e->pos().y() < st::topBarHeight)\n\t\t&& !showSelectedState()\n\t\t&& !_chooseForReportReason;\n\tif (handleClick) {\n\t\tconst auto archiveTop = (_activeChat.section == Section::ChatsList)\n\t\t\t&& _activeChat.key.folder();\n\t\tif ((_animatingMode && _back->rect().contains(e->pos()))\n\t\t\t|| archiveTop) {\n\t\t\tif (!rootChatsListBar()) {\n\t\t\t\tbackClicked();\n\t\t\t}\n\t\t} else {\n\t\t\tinfoClicked();\n\t\t}\n\t}\n}\n\nvoid TopBarWidget::infoClicked() {\n\tconst auto key = _activeChat.key;\n\tif (!key) {\n\t\treturn;\n\t} else if (const auto topic = key.topic()) {\n\t\t_controller->showSection(std::make_shared<Info::Memento>(topic));\n\t} else if (const auto sublist = key.sublist()) {\n\t\t_controller->showSection(std::make_shared<Info::Memento>(sublist));\n\t} else if (key.peer()->savedSublistsInfo()) {\n\t\t_controller->showSection(std::make_shared<Info::Memento>(\n\t\t\tkey.peer(),\n\t\t\tInfo::Section::Type::SavedSublists));\n\t} else if (key.peer()->sharedMediaInfo()) {\n\t\t_controller->showSection(std::make_shared<Info::Memento>(\n\t\t\tkey.peer(),\n\t\t\tInfo::Section(Storage::SharedMediaType::Photo)));\n\t} else {\n\t\t_controller->showPeerInfo(key.peer());\n\t}\n}\n\nvoid TopBarWidget::backClicked() {\n\tif (_activeChat.key.folder()) {\n\t\t_controller->closeFolder();\n\t} else if (_activeChat.section == Section::ChatsList\n\t\t&& _activeChat.key.history()\n\t\t&& _activeChat.key.history()->isForum()) {\n\t\t_controller->closeForum();\n\t} else {\n\t\t_controller->showBackFromStack();\n\t}\n}\n\nvoid TopBarWidget::setActiveChat(\n\t\tActiveChat activeChat,\n\t\tSendActionPainter *sendAction) {\n\t_sendAction = sendAction;\n\tif (_activeChat.key == activeChat.key\n\t\t&& _activeChat.section == activeChat.section) {\n\t\t_activeChat = activeChat;\n\t\treturn;\n\t}\n\tconst auto topicChanged = (_activeChat.key.topic()\n\t\t!= activeChat.key.topic());\n\tconst auto peerChanged = (_activeChat.key.history()\n\t\t!= activeChat.key.history());\n\n\t_activeChat = activeChat;\n\t_titlePeerText.clear();\n\t_back->clearState();\n\tupdate();\n\n\tif (peerChanged || topicChanged) {\n\t\t_titleBadge.unload();\n\t\t_titleNameVersion = 0;\n\t\t_emojiInteractionSeen = nullptr;\n\t\t_activeChatLifetime.destroy();\n\t\tif (const auto peer = _activeChat.key.peer()) {\n\t\t\tsession().changes().peerFlagsValue(\n\t\t\t\tpeer,\n\t\t\t\tData::PeerUpdate::Flag::GroupCall\n\t\t\t) | rpl::map([=] {\n\t\t\t\treturn peer->groupCall();\n\t\t\t}) | rpl::distinct_until_changed(\n\t\t\t) | rpl::map([](Data::GroupCall *call) {\n\t\t\t\treturn call ? call->fullCountValue() : rpl::single(-1);\n\t\t\t}) | rpl::flatten_latest(\n\t\t\t) | rpl::map([](int count) {\n\t\t\t\treturn (count == 0);\n\t\t\t}) | rpl::distinct_until_changed(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tupdateControlsVisibility();\n\t\t\t\tupdateControlsGeometry();\n\t\t\t}, _activeChatLifetime);\n\n\t\t\tif (const auto channel = peer->asChannel()) {\n\t\t\t\tif (channel->canEditStories()\n\t\t\t\t\t&& !channel->owner().stories().albumIdsCountKnown(\n\t\t\t\t\t\tchannel->id,\n\t\t\t\t\t\tData::kStoriesAlbumIdArchive)) {\n\t\t\t\t\tchannel->owner().stories().albumIdsLoadMore(\n\t\t\t\t\t\tchannel->id,\n\t\t\t\t\t\tData::kStoriesAlbumIdArchive);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (const auto history = _activeChat.key.history()) {\n\t\t\tusing InteractionSeen = ChatHelpers::EmojiInteractionSeen;\n\t\t\t_controller->emojiInteractions().seen(\n\t\t\t) | rpl::filter([=](const InteractionSeen &seen) {\n\t\t\t\treturn (seen.peer == history->peer);\n\t\t\t}) | rpl::on_next([=](const InteractionSeen &seen) {\n\t\t\t\thandleEmojiInteractionSeen(seen.emoticon);\n\t\t\t}, _activeChatLifetime);\n\t\t}\n\n\t\tif (const auto topic = _activeChat.key.topic()) {\n\t\t\tInfo::Profile::NameValue(\n\t\t\t\ttopic->peer()\n\t\t\t) | rpl::on_next([=](const QString &name) {\n\t\t\t\t_titlePeerText.setText(st::dialogsTextStyle, name);\n\t\t\t\t_titlePeerTextOnline = false;\n\t\t\t\tupdate();\n\t\t\t}, _activeChatLifetime);\n\n\t\t\t// _menuToggle visibility depends on \"View topic info\",\n\t\t\t// \"View topic info\" visibility depends on activeChatCurrent.\n\t\t\t_controller->activeChatChanges(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tupdateControlsVisibility();\n\t\t\t}, _activeChatLifetime);\n\t\t}\n\t}\n\tupdateUnreadBadge();\n\trefreshInfoButton();\n\tif (_menu) {\n\t\t_menu = nullptr;\n\t}\n\tupdateOnlineDisplay();\n\tupdateControlsVisibility();\n\trefreshUnreadBadge();\n\tsetupDragOnBackButton();\n}\n\nvoid TopBarWidget::handleEmojiInteractionSeen(const QString &emoticon) {\n\tauto seen = _emojiInteractionSeen.get();\n\tif (!seen) {\n\t\t_emojiInteractionSeen\n\t\t\t= std::make_unique<EmojiInteractionSeenAnimation>();\n\t\tseen = _emojiInteractionSeen.get();\n\t\tseen->animation.start(Ui::SendActionAnimation::Type::ChooseSticker);\n\t\tseen->scheduler.init([=] {\n\t\t\tif (seen->till <= crl::now()) {\n\t\t\t\tcrl::on_main(this, [=] {\n\t\t\t\t\tif (_emojiInteractionSeen\n\t\t\t\t\t\t&& _emojiInteractionSeen->till <= crl::now()) {\n\t\t\t\t\t\t_emojiInteractionSeen = nullptr;\n\t\t\t\t\t\tupdate();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconst auto skip = st::topBarArrowPadding.bottom();\n\t\t\t\tupdate(\n\t\t\t\t\t_leftTaken,\n\t\t\t\t\tst::topBarHeight - skip - st::dialogsTextFont->height,\n\t\t\t\t\tseen->animation.width(),\n\t\t\t\t\tst::dialogsTextFont->height);\n\t\t\t}\n\t\t});\n\t\tseen->scheduler.start();\n\t}\n\tseen->till = crl::now() + kEmojiInteractionSeenDuration;\n\tseen->text.setText(\n\t\tst::dialogsTextStyle,\n\t\ttr::lng_user_action_watching_animations(tr::now, lt_emoji, emoticon),\n\t\tUi::NameTextOptions());\n\tupdate();\n}\n\nvoid TopBarWidget::setCustomTitle(const QString &title) {\n\tif (_customTitleText != title) {\n\t\t_customTitleText = title;\n\t\tupdate();\n\t}\n}\n\nbool TopBarWidget::rootChatsListBar() const {\n\tif (_activeChat.section != Section::ChatsList) {\n\t\treturn false;\n\t}\n\tconst auto id = _controller->windowId();\n\tconst auto separateFolder = id.folder();\n\tconst auto separateForum = id.forum();\n\tconst auto active = _activeChat.key;\n\treturn (separateForum && separateForum->history() == active.history())\n\t\t|| (separateFolder && separateFolder == active.folder());\n}\n\nvoid TopBarWidget::refreshInfoButton() {\n\tif (_activeChat.key.topic()\n\t\t|| (_activeChat.section == Section::ChatsList\n\t\t\t&& !rootChatsListBar())) {\n\t\t_info.destroy();\n\t} else if (const auto peer = _activeChat.key.peer()) {\n\t\tconst auto sublist = _activeChat.key.sublist();\n\t\tconst auto infoPeer = sublist ? sublist->sublistPeer().get() : peer;\n\t\tauto info = object_ptr<Ui::UserpicButton>(\n\t\t\tthis,\n\t\t\t_controller,\n\t\t\tinfoPeer->userpicPaintingPeer(),\n\t\t\tUi::UserpicButton::Role::Custom,\n\t\t\tUi::UserpicButton::Source::PeerPhoto,\n\t\t\tst::topBarInfoButton,\n\t\t\tinfoPeer->userpicShape());\n\t\tinfo->showSavedMessagesOnSelf(true);\n\t\tinfo->showMyNotesOnSelf(true);\n\t\t_info.destroy();\n\t\t_info = std::move(info);\n\t}\n\tif (_info) {\n\t\t_info->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t_info->setAccessibleName(tr::lng_settings_section_info(tr::now));\n\t\tif (_back && _info) {\n\t\t\tQWidget::setTabOrder(_back.data(), _info.data());\n\t\t}\n\t\tif (_info && _search) {\n\t\t\tQWidget::setTabOrder(_info.data(), _search.data());\n\t\t}\n\t}\n}\n\nvoid TopBarWidget::resizeEvent(QResizeEvent *e) {\n\tupdateSearchVisibility();\n\tupdateControlsGeometry();\n}\n\nint TopBarWidget::countSelectedButtonsTop(float64 selectedShown) {\n\treturn (1. - selectedShown) * (-st::topBarHeight);\n}\n\nvoid TopBarWidget::updateSearchVisibility() {\n\tconst auto pinnedInSavedMessages = (_activeChat.section == Section::Pinned)\n\t\t&& _activeChat.key.peer()\n\t\t&& _activeChat.key.peer()->isSelf();\n\tconst auto searchAllowedMode = (_activeChat.section == Section::History)\n\t\t|| (_activeChat.section == Section::Replies)\n\t\t|| (_activeChat.section == Section::Pinned\n\t\t\t&& !pinnedInSavedMessages)\n\t\t|| (_activeChat.section == Section::SavedSublist\n\t\t\t&& _activeChat.key.sublist());\n\t_search->setVisible(searchAllowedMode && !_chooseForReportReason);\n}\n\nvoid TopBarWidget::updateControlsGeometry() {\n\tif (!_activeChat.key) {\n\t\treturn;\n\t}\n\tconst auto hasSelected = showSelectedActions();\n\tauto selectedButtonsTop = countSelectedButtonsTop(\n\t\t_selectedShown.value(hasSelected ? 1. : 0.));\n\tif (!_searchMode && !_searchShown.animating() && _searchField) {\n\t\t_searchField.destroy();\n\t\t_searchCancel.destroy();\n\t\t_jumpToDate.destroy();\n\t\t_chooseFromUser.destroy();\n\t}\n\tauto searchFieldTop = _searchField\n\t\t? countSelectedButtonsTop(_searchShown.value(_searchMode ? 1. : 0.))\n\t\t: -st::topBarHeight;\n\tconst auto otherButtonsTop = std::max(selectedButtonsTop, searchFieldTop)\n\t\t+ st::topBarHeight;\n\tconst auto backButtonTop = selectedButtonsTop + st::topBarHeight;\n\tauto buttonsLeft = st::topBarActionSkip\n\t\t+ (_controller->adaptive().isOneColumn() ? 0 : st::lineWidth);\n\tauto buttonsWidth = (_forward->isHidden() ? 0 : _forward->contentWidth())\n\t\t+ (_sendNow->isHidden() ? 0 : _sendNow->contentWidth())\n\t\t+ (_delete->isHidden() ? 0 : _delete->contentWidth())\n\t\t+ (_forwardAndDelete->isHidden() ? 0 : _forwardAndDelete->contentWidth())\n\t\t+ _clear->width();\n\tbuttonsWidth += buttonsLeft + st::topBarActionSkip * 3;\n\n\tauto widthLeft = qMin(width() - buttonsWidth, -2 * st::defaultActiveButton.width);\n\tauto buttonFullWidth = qMin(-(widthLeft / 2), 0);\n\t_forward->setFullWidth(buttonFullWidth);\n\t_forwardAndDelete->setFullWidth(buttonFullWidth);\n\t_sendNow->setFullWidth(buttonFullWidth);\n\t_delete->setFullWidth(buttonFullWidth);\n\n\tselectedButtonsTop += (height() - _forward->height()) / 2;\n\n\t_forward->moveToLeft(buttonsLeft, selectedButtonsTop);\n\tif (!_forward->isHidden()) {\n\t\tbuttonsLeft += _forward->width() + st::topBarActionSkip;\n\t}\n\n\t_sendNow->moveToLeft(buttonsLeft, selectedButtonsTop);\n\tif (!_sendNow->isHidden()) {\n\t\tbuttonsLeft += _sendNow->width() + st::topBarActionSkip;\n\t}\n\n\t_delete->moveToLeft(buttonsLeft, selectedButtonsTop);\n\t{\n\t\tconst auto large = st::topBarActionButtonLargeRadius;\n\t\tconst auto &buttonSt = st::defaultActiveButton;\n\t\tconst auto small = buttonSt.radius\n\t\t\t? buttonSt.radius\n\t\t\t: st::buttonRadius;\n\t\tconst auto buttons = std::array{\n\t\t\t_forward.data(),\n\t\t\t_sendNow.data(),\n\t\t\t_delete.data(),\n\t\t};\n\t\tauto first = (Ui::RoundButton*)(nullptr);\n\t\tauto last = (Ui::RoundButton*)(nullptr);\n\t\tfor (const auto button : buttons) {\n\t\t\tif (!button->isHidden()) {\n\t\t\t\tif (!first) {\n\t\t\t\t\tfirst = button;\n\t\t\t\t}\n\t\t\t\tlast = button;\n\t\t\t}\n\t\t}\n\t\tfor (const auto button : buttons) {\n\t\t\tif (button->isHidden()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto left = (button == first) ? large : small;\n\t\t\tconst auto right = (button == last) ? large : small;\n\t\t\tbutton->setCornerRadii(left, right, left, right);\n\t\t}\n\t}\n\t_forwardAndDelete->moveToLeft(\n\t\tbuttonsLeft + _delete->width() + st::topBarActionSkip,\n\t\tselectedButtonsTop);\n\t_clear->moveToRight(st::topBarActionSkip, selectedButtonsTop);\n\n\tif (!_cancelChoose->isHidden()) {\n\t\t_leftTaken = 0;\n\t\t_cancelChoose->moveToLeft(_leftTaken, otherButtonsTop);\n\t\t_leftTaken += _cancelChoose->width();\n\t} else if (_back->isHidden()) {\n\t\t_leftTaken = st::topBarArrowPadding.right();\n\t} else {\n\t\t_leftTaken = anim::interpolate(\n\t\t\t0,\n\t\t\t(_narrowWidth - _back->width()) / 2,\n\t\t\t_narrowRatio);\n\t\t_back->moveToLeft(_leftTaken, backButtonTop);\n\t\t_leftTaken += _back->width();\n\t}\n\tif (_info && !_info->isHidden()) {\n\t\tif (_back->isHidden() && _narrowRatio > 0.) {\n\t\t\tconst auto &infoSt = st::topBarInfoButton;\n\t\t\tconst auto middle = (_narrowWidth - infoSt.photoSize) / 2;\n\t\t\t_leftTaken = anim::interpolate(\n\t\t\t\t_leftTaken,\n\t\t\t\tmiddle - infoSt.photoPosition.x(),\n\t\t\t\t_narrowRatio);\n\t\t}\n\t\t_info->moveToLeft(_leftTaken, otherButtonsTop);\n\t\t_leftTaken += _info->width();\n\t} else if (_activeChat.key.topic()\n\t\t|| _activeChat.section == Section::ChatsList) {\n\t\t_leftTaken += st::normalFont->spacew;\n\t}\n\n\tif (_searchField) {\n\t\tconst auto fieldLeft = _back->isHidden()\n\t\t\t? st::topBarArrowPadding.right()\n\t\t\t: _leftTaken;\n\t\tconst auto fieldTop = searchFieldTop\n\t\t\t+ (height() - _searchField->height()) / 2;\n\t\tconst auto fieldRight = st::dialogsFilterSkip\n\t\t\t+ st::dialogsFilterPadding.x();\n\t\tconst auto fieldWidth = width() - fieldLeft - fieldRight;\n\t\t_searchField->setGeometryToLeft(\n\t\t\tfieldLeft,\n\t\t\tfieldTop,\n\t\t\tfieldWidth,\n\t\t\t_searchField->height());\n\n\t\tauto right = fieldLeft + fieldWidth;\n\t\t_searchCancel->moveToLeft(\n\t\t\tright - _searchCancel->width(),\n\t\t\t_searchField->y());\n\t\tright -= st::dialogsCalendar.width;\n\t\tif (_jumpToDate) {\n\t\t\t_jumpToDate->moveToLeft(right, _searchField->y());\n\t\t}\n\t\tright -= st::dialogsSearchFrom.width;\n\t\tif (_chooseFromUser) {\n\t\t\t_chooseFromUser->moveToLeft(right, _searchField->y());\n\t\t}\n\t}\n\n\t_rightTaken = 0;\n\t_menuToggle->moveToRight(_rightTaken, otherButtonsTop);\n\tif (_menuToggle->isHidden()) {\n\t\t_rightTaken += (_menuToggle->width() - _search->width());\n\t} else {\n\t\t_rightTaken += _menuToggle->width() + st::topBarSkip;\n\t}\n\t_infoToggle->moveToRight(_rightTaken, otherButtonsTop);\n\tif (!_infoToggle->isHidden()) {\n\t\t_infoToggle->moveToRight(_rightTaken, otherButtonsTop);\n\t\t_rightTaken += _infoToggle->width();\n\t}\n\tif (!_call->isHidden() || !_groupCall->isHidden()) {\n\t\t_call->moveToRight(_rightTaken, otherButtonsTop);\n\t\t_groupCall->moveToRight(_rightTaken, otherButtonsTop);\n\t\t_rightTaken += _call->width();\n\t}\n\t_search->moveToRight(_rightTaken, otherButtonsTop);\n\tif (!_search->isHidden()) {\n\t\t_rightTaken += _search->width() + st::topBarCallSkip;\n\t}\n\n\tupdateMembersShowArea();\n}\n\nvoid TopBarWidget::finishAnimating() {\n\t_selectedShown.stop();\n\tupdateControlsVisibility();\n\tupdate();\n}\n\nvoid TopBarWidget::setAnimatingMode(bool enabled) {\n\tif (_animatingMode != enabled) {\n\t\t_animatingMode = enabled;\n\t\tsetAttribute(Qt::WA_OpaquePaintEvent, !_animatingMode);\n\t\tfinishAnimating();\n\t}\n}\n\nvoid TopBarWidget::updateControlsVisibility() {\n\tif (!_activeChat.key) {\n\t\treturn;\n\t} else if (_animatingMode) {\n\t\thideChildren();\n\t\treturn;\n\t}\n\tconst auto visible = showSelectedState() || _selectedShown.animating();\n\t_clear->setVisible(visible);\n\t_delete->setVisible(_canDelete && visible);\n\t_forward->setVisible(_canForward && visible);\n\t_forwardAndDelete->setVisible(_canForward\n\t\t&& _canDelete\n\t\t&& Core::App().settings().fork().thirdButtonTopBar());\n\t_sendNow->setVisible(_canSendNow && visible);\n\n\n\tconst auto isOneColumn = _controller->adaptive().isOneColumn();\n\tconst auto backVisible = !rootChatsListBar()\n\t\t&& (isOneColumn\n\t\t\t|| (_activeChat.section == Section::ChatsList)\n\t\t\t|| !_controller->content()->stackIsEmpty());\n\t_back->setVisible(backVisible && !_chooseForReportReason);\n\t_cancelChoose->setVisible(_chooseForReportReason.has_value());\n\tif (_info) {\n\t\t_info->setVisible(!_chooseForReportReason\n\t\t\t&& (isOneColumn || !_primaryWindow));\n\t}\n\tif (_unreadBadge) {\n\t\t_unreadBadge->setVisible(!_chooseForReportReason\n\t\t\t&& !rootChatsListBar());\n\t}\n\tconst auto topic = _activeChat.key.topic();\n\tconst auto section = _activeChat.section;\n\tconst auto historyMode = (section == Section::History);\n\tconst auto hasPollsMenu = (_activeChat.key.peer()\n\t\t&& _activeChat.key.peer()->canCreatePolls())\n\t\t|| (topic && Data::CanSend(topic, ChatRestriction::SendPolls));\n\tconst auto hasTodoListsMenu = (_activeChat.key.peer()\n\t\t&& _activeChat.key.peer()->canCreateTodoLists())\n\t\t|| (topic && Data::CanSend(topic, ChatRestriction::SendPolls));\n\tconst auto hasTopicMenu = [&] {\n\t\tif (!topic || section != Section::Replies) {\n\t\t\treturn false;\n\t\t}\n\t\tauto empty = true;\n\t\tconst auto callback = [&](const Ui::Menu::MenuCallback::Args&) {\n\t\t\tempty = false;\n\t\t\treturn (QAction*)nullptr;\n\t\t};\n\t\tWindow::FillDialogsEntryMenu(\n\t\t\t_controller,\n\t\t\t_activeChat,\n\t\t\tUi::Menu::MenuCallback(callback));\n\t\treturn !empty;\n\t}();\n\tconst auto hasMenu = !_activeChat.key.folder()\n\t\t&& (section == Section::History\n\t\t\t? true\n\t\t\t: (section == Section::Scheduled)\n\t\t\t? (hasPollsMenu || hasTodoListsMenu)\n\t\t\t: (section == Section::Replies)\n\t\t\t? (hasPollsMenu || hasTodoListsMenu || hasTopicMenu)\n\t\t\t: (section == Section::ChatsList)\n\t\t\t? (_activeChat.key.peer() && _activeChat.key.peer()->isForum())\n\t\t\t: (section == Section::SavedSublist)\n\t\t\t? (_activeChat.key.peer()\n\t\t\t\t&& _activeChat.key.peer()->isChannel()\n\t\t\t\t&& _activeChat.key.peer()->owner().commonStarsPerMessage(\n\t\t\t\t\t_activeChat.key.peer()->asChannel()))\n\t\t\t: false);\n\tconst auto hasInfo = !_activeChat.key.folder()\n\t\t&& (section == Section::History\n\t\t\t? true\n\t\t\t: (section == Section::Replies)\n\t\t\t? (_activeChat.key.topic() != nullptr)\n\t\t\t: (section == Section::SavedSublist)\n\t\t\t? (_activeChat.key.sublist() != nullptr\n\t\t\t\t&& _activeChat.key.sublist()->parentChat())\n\t\t\t: false);\n\tupdateSearchVisibility();\n\tif (_searchMode) {\n\t\tconst auto hasSearchQuery = _searchField\n\t\t\t&& !_searchField->getLastText().isEmpty();\n\t\tif (!_jumpToDate || hasSearchQuery) {\n\t\t\t_searchCancel->show(anim::type::normal);\n\t\t\tif (_jumpToDate) {\n\t\t\t\t_jumpToDate->hide(anim::type::normal);\n\t\t\t}\n\t\t} else {\n\t\t\t_searchCancel->hide(anim::type::normal);\n\t\t\t_jumpToDate->show(anim::type::normal);\n\t\t}\n\t}\n\t_menuToggle->setVisible(hasMenu\n\t\t&& !_chooseForReportReason\n\t\t&& (_narrowRatio < 1.));\n\t_infoToggle->setVisible(hasInfo\n\t\t&& !isOneColumn\n\t\t&& _controller->canShowThirdSection()\n\t\t&& !_chooseForReportReason);\n\tconst auto callsEnabled = [&] {\n\t\tif (const auto peer = _activeChat.key.peer()) {\n\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\treturn !user->isSelf()\n\t\t\t\t\t&& !user->isBot()\n\t\t\t\t\t&& !user->isInaccessible()\n\t\t\t\t\t&& !peer->isServiceUser();\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}();\n\t_call->setVisible(historyMode\n\t\t&& callsEnabled\n\t\t&& !_chooseForReportReason);\n\tconst auto groupCallsEnabled = [&] {\n\t\tif (const auto peer = _activeChat.key.peer()) {\n\t\t\tif (!peer->isUser() && peer->canManageGroupCall()) {\n\t\t\t\treturn true;\n\t\t\t} else if (const auto call = peer->groupCall()) {\n\t\t\t\treturn (call->fullCount() == 0);\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\treturn false;\n\t}();\n\t_groupCall->setVisible(historyMode\n\t\t&& groupCallsEnabled\n\t\t&& !_chooseForReportReason);\n\n\tif (_membersShowArea) {\n\t\t_membersShowArea->setVisible(!_chooseForReportReason);\n\t}\n\tupdateControlsGeometry();\n}\n\nvoid TopBarWidget::updateMembersShowArea() {\n\tconst auto membersShowAreaNeeded = [&] {\n\t\tconst auto peer = _activeChat.key.peer();\n\t\tif (showSelectedState()\n\t\t\t|| !peer\n\t\t\t|| _activeChat.section == Section::ChatsList\n\t\t\t|| _activeChat.key.topic()) {\n\t\t\treturn false;\n\t\t} else if (const auto chat = peer->asChat()) {\n\t\t\treturn chat->amIn();\n\t\t} else if (const auto megagroup = peer->asMegagroup()) {\n\t\t\treturn !megagroup->isMonoforum()\n\t\t\t\t&& megagroup->canViewMembers()\n\t\t\t\t&& (megagroup->membersCount()\n\t\t\t\t\t< megagroup->session().serverConfig().chatSizeMax);\n\t\t}\n\t\treturn false;\n\t}();\n\tif (!membersShowAreaNeeded) {\n\t\tif (_membersShowArea) {\n\t\t\t_membersShowAreaActive.fire(false);\n\t\t\t_membersShowArea.destroy();\n\t\t}\n\t\treturn;\n\t} else if (!_membersShowArea) {\n\t\t_membersShowArea.create(this);\n\t\t_membersShowArea->show();\n\t\t_membersShowArea->installEventFilter(this);\n\t}\n\t_membersShowArea->setGeometry(getMembersShowAreaGeometry());\n}\n\nbool TopBarWidget::showSelectedState() const {\n\treturn (_selectedCount > 0)\n\t\t&& (_canDelete || _canForward || _canSendNow);\n}\n\nvoid TopBarWidget::showSelected(SelectedState state) {\n\tauto canDelete = (state.count > 0 && state.count == state.canDeleteCount);\n\tauto canForward = (state.count > 0 && state.count == state.canForwardCount);\n\tauto canSendNow = (state.count > 0 && state.count == state.canSendNowCount);\n\tauto count = (!canDelete && !canForward && !canSendNow) ? 0 : state.count;\n\tif (_selectedCount == count\n\t\t&& _canDelete == canDelete\n\t\t&& _canForward == canForward\n\t\t&& _canSendNow == canSendNow) {\n\t\treturn;\n\t}\n\tif (count == 0) {\n\t\t// Don't change the visible buttons if the selection is cancelled.\n\t\tcanDelete = _canDelete;\n\t\tcanForward = _canForward;\n\t\tcanSendNow = _canSendNow;\n\t}\n\n\tconst auto wasSelectedState = showSelectedState();\n\tconst auto visibilityChanged = (_canDelete != canDelete)\n\t\t|| (_canForward != canForward)\n\t\t|| (_canSendNow != canSendNow);\n\t_selectedCount = count;\n\t_canDelete = canDelete;\n\t_canForward = canForward;\n\t_canSendNow = canSendNow;\n\tconst auto nowSelectedState = showSelectedState();\n\tif (nowSelectedState) {\n\t\t_forward->setNumbersText(_selectedCount);\n\t\t_forwardAndDelete->setNumbersText(_selectedCount);\n\t\t_sendNow->setNumbersText(_selectedCount);\n\t\t_delete->setNumbersText(_selectedCount);\n\t\tif (!wasSelectedState) {\n\t\t\t_forward->finishNumbersAnimation();\n\t\t\t_forwardAndDelete->finishNumbersAnimation();\n\t\t\t_sendNow->finishNumbersAnimation();\n\t\t\t_delete->finishNumbersAnimation();\n\t\t}\n\t}\n\tif (visibilityChanged\n\t\t|| (!wasSelectedState && nowSelectedState)) {\n\t\tupdateControlsVisibility();\n\t}\n\tif (wasSelectedState != nowSelectedState && !_chooseForReportReason) {\n\t\tsetCursor(nowSelectedState\n\t\t\t? style::cur_default\n\t\t\t: style::cur_pointer);\n\n\t\tupdateMembersShowArea();\n\t\ttoggleSelectedControls(nowSelectedState);\n\t} else {\n\t\tupdateControlsGeometry();\n\t}\n}\n\nbool TopBarWidget::toggleSearch(bool shown, anim::type animated) {\n\tif (_searchMode == shown) {\n\t\tif (animated == anim::type::instant) {\n\t\t\t_searchShown.stop();\n\t\t}\n\t\treturn false;\n\t}\n\t_searchMode = shown;\n\tif (shown && !_searchField) {\n\t\t_searchField.create(this, st::dialogsFilter, tr::lng_dlg_filter());\n\t\t_searchField->setFocusPolicy(Qt::StrongFocus);\n\t\t_searchField->customUpDown(true);\n\t\t_searchField->show();\n\t\t_searchCancel.create(this, st::dialogsCancelSearch);\n\t\t_searchCancel->show(anim::type::instant);\n\t\t_searchCancel->setClickedCallback([=] { _searchCancelled.fire({}); });\n\t\t_searchField->submits(\n\t\t) | rpl::on_next([=] {\n\t\t\t_searchSubmitted.fire({});\n\t\t}, _searchField->lifetime());\n\t\t_searchField->changes(\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto was = _searchQuery.current();\n\t\t\tconst auto now = _searchField->getLastText();\n\t\t\tif (_jumpToDate && was.isEmpty() != now.isEmpty()) {\n\t\t\t\tupdateControlsVisibility();\n\t\t\t}\n\t\t\tif (_chooseFromUser) {\n\t\t\t\tauto switchToChooseFrom = SwitchToChooseFromQuery();\n\t\t\t\tif (was != switchToChooseFrom\n\t\t\t\t\t&& switchToChooseFrom.startsWith(was)\n\t\t\t\t\t&& now == switchToChooseFrom) {\n\t\t\t\t\t_chooseFromUserRequests.fire({});\n\t\t\t\t}\n\t\t\t}\n\t\t\t_searchQuery = now;\n\t\t}, _searchField->lifetime());\n\t} else {\n\t\tAssert(_searchField != nullptr);\n\t}\n\t_searchQuery = shown ? _searchField->getLastText() : QString();\n\tif (animated == anim::type::normal) {\n\t\t_searchShown.start(\n\t\t\t[=] { slideAnimationCallback(); },\n\t\t\tshown ? 0. : 1.,\n\t\t\tshown ? 1. : 0.,\n\t\t\tst::slideWrapDuration,\n\t\t\tanim::easeOutCirc);\n\t} else {\n\t\t_searchShown.stop();\n\t\tslideAnimationCallback();\n\t}\n\tif (shown) {\n\t\t_searchField->setFocusFast();\n\t}\n\treturn true;\n}\n\nvoid TopBarWidget::searchEnableJumpToDate(bool enable) {\n\tif (!_searchMode) {\n\t\treturn;\n\t} else if (!enable) {\n\t\t_jumpToDate.destroy();\n\t} else if (!_jumpToDate) {\n\t\t_jumpToDate.create(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::IconButton>(this, st::dialogsCalendar));\n\t\t_jumpToDate->toggle(\n\t\t\t_searchField->getLastText().isEmpty(),\n\t\t\tanim::type::instant);\n\t\t_jumpToDate->entity()->clicks(\n\t\t) | rpl::to_empty | rpl::start_to_stream(\n\t\t\t_jumpToDateRequests,\n\t\t\t_jumpToDate->lifetime());\n\t}\n\tupdateControlsVisibility();\n\tupdateControlsGeometry();\n}\n\nvoid TopBarWidget::searchEnableChooseFromUser(bool enable, bool visible) {\n\tif (!_searchMode) {\n\t\treturn;\n\t} else if (!enable) {\n\t\t_chooseFromUser.destroy();\n\t} else if (!_chooseFromUser) {\n\t\t_chooseFromUser.create(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::IconButton>(this, st::dialogsSearchFrom));\n\t\t_chooseFromUser->toggle(visible, anim::type::instant);\n\t\t_chooseFromUser->entity()->clicks(\n\t\t) | rpl::to_empty | rpl::start_to_stream(\n\t\t\t_chooseFromUserRequests,\n\t\t\t_chooseFromUser->lifetime());\n\t} else {\n\t\t_chooseFromUser->toggle(visible, anim::type::normal);\n\t}\n\tauto additional = QMargins();\n\tif (_chooseFromUser && _chooseFromUser->toggled()) {\n\t\tadditional.setRight(_chooseFromUser->width());\n\t}\n\t_searchField->setAdditionalMargins(additional);\n\tupdateControlsVisibility();\n\tupdateControlsGeometry();\n}\n\nbool TopBarWidget::searchSetFocus() {\n\tif (!_searchMode) {\n\t\treturn false;\n\t}\n\t_searchField->setFocus();\n\treturn true;\n}\n\nbool TopBarWidget::searchMode() const {\n\treturn _searchMode;\n}\n\nbool TopBarWidget::searchHasFocus() const {\n\treturn _searchMode && _searchField->hasFocus();\n}\n\nrpl::producer<> TopBarWidget::searchCancelled() const {\n\treturn _searchCancelled.events();\n}\n\nrpl::producer<> TopBarWidget::searchSubmitted() const {\n\treturn _searchSubmitted.events();\n}\n\nrpl::producer<QString> TopBarWidget::searchQuery() const {\n\treturn _searchQuery.value();\n}\n\nQString TopBarWidget::searchQueryCurrent() const {\n\treturn _searchQuery.current();\n}\n\nint TopBarWidget::searchQueryCursorPosition() const {\n\treturn _searchMode\n\t\t? _searchField->textCursor().position()\n\t\t: _searchQuery.current().size();\n}\n\nvoid TopBarWidget::searchClear() {\n\tif (_searchMode) {\n\t\t_searchField->clear();\n\t}\n}\n\nvoid TopBarWidget::searchSetText(const QString &query, int cursorPosition) {\n\tif (_searchMode) {\n\t\tif (cursorPosition < 0) {\n\t\t\tcursorPosition = query.size();\n\t\t}\n\t\t_searchField->setText(query);\n\t\t_searchField->setCursorPosition(cursorPosition);\n\t}\n}\n\nvoid TopBarWidget::toggleSelectedControls(bool shown) {\n\t_selectedShown.start(\n\t\t[this] { slideAnimationCallback(); },\n\t\tshown ? 0. : 1.,\n\t\tshown ? 1. : 0.,\n\t\tst::slideWrapDuration,\n\t\tanim::easeOutCirc);\n}\n\nvoid TopBarWidget::setGeometryWithNarrowRatio(\n\t\tQRect geometry,\n\t\tint narrowWidth,\n\t\tfloat64 narrowRatio) {\n\tif (_activeChat.section != Section::ChatsList) {\n\t\tnarrowRatio = 0.;\n\t\tnarrowWidth = 0;\n\t}\n\tconst auto changed = (_narrowRatio != narrowRatio);\n\tconst auto started = (_narrowRatio == 0.) != (narrowRatio == 0.);\n\tconst auto finished = (_narrowRatio == 1.) != (narrowRatio == 1.);\n\tconst auto resized = (size() != geometry.size());\n\t_narrowRatio = narrowRatio;\n\t_narrowWidth = narrowWidth;\n\tif (started || finished) {\n\t\tupdateControlsVisibility();\n\t}\n\tsetGeometry(geometry);\n\tif (changed && !resized) {\n\t\tupdateSearchVisibility();\n\t\tupdateControlsGeometry();\n\t}\n}\n\nbool TopBarWidget::showSelectedActions() const {\n\treturn showSelectedState() && !_chooseForReportReason;\n}\n\nvoid TopBarWidget::slideAnimationCallback() {\n\tif (!_selectedShown.animating() && !_searchShown.animating()) {\n\t\tupdateControlsVisibility();\n\t}\n\tupdateControlsGeometry();\n\tupdate();\n}\n\nvoid TopBarWidget::updateAdaptiveLayout() {\n\tupdateControlsVisibility();\n\tupdateInfoToggleActive();\n\trefreshUnreadBadge();\n}\n\nvoid TopBarWidget::refreshUnreadBadge() {\n\tif (!_controller->adaptive().isOneColumn() && !_activeChat.key.folder()) {\n\t\t_unreadBadge.destroy();\n\t\treturn;\n\t} else if (_unreadBadge) {\n\t\treturn;\n\t}\n\t_unreadBadge.create(this);\n\n\trpl::combine(\n\t\t_back->geometryValue(),\n\t\t_unreadBadge->widthValue()\n\t) | rpl::on_next([=](QRect geometry, int width) {\n\t\t_unreadBadge->move(\n\t\t\tgeometry.x() + geometry.width() - width,\n\t\t\tgeometry.y() + st::titleUnreadCounterTop);\n\t}, _unreadBadge->lifetime());\n\n\t_unreadBadge->setVisible(!rootChatsListBar());\n\t_unreadBadge->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_controller->session().data().unreadBadgeChanges(\n\t) | rpl::on_next([=] {\n\t\tupdateUnreadBadge();\n\t}, _unreadBadge->lifetime());\n\tupdateUnreadBadge();\n}\n\nvoid TopBarWidget::updateUnreadBadge() {\n\tif (!_unreadBadge) return;\n\n\tconst auto key = _activeChat.key;\n\tconst auto muted = session().data().unreadBadgeMutedIgnoreOne(key);\n\tconst auto counter = session().data().unreadBadgeIgnoreOne(key);\n\tconst auto text = [&] {\n\t\tif (!counter) {\n\t\t\treturn QString();\n\t\t}\n\t\treturn (counter > 999)\n\t\t\t? u\"..%1\"_q.arg(counter % 100, 2, 10, QChar('0'))\n\t\t\t: QString::number(counter);\n\t}();\n\t_unreadBadge->setText(text, !muted);\n}\n\nvoid TopBarWidget::updateInfoToggleActive() {\n\tauto infoThirdActive = _controller->adaptive().isThreeColumn()\n\t\t&& (Core::App().settings().thirdSectionInfoEnabled()\n\t\t\t|| Core::App().settings().tabbedReplacedWithInfo());\n\tauto iconOverride = infoThirdActive\n\t\t? &st::topBarInfoActive\n\t\t: nullptr;\n\tauto rippleOverride = infoThirdActive\n\t\t? &st::lightButtonBgOver\n\t\t: nullptr;\n\t_infoToggle->setIconOverride(iconOverride, iconOverride);\n\t_infoToggle->setRippleColorOverride(rippleOverride);\n}\n\nvoid TopBarWidget::setupDragOnBackButton() {\n\t_backLifetime.destroy();\n\tif (_activeChat.section != Section::ChatsList) {\n\t\t_back->setAcceptDrops(false);\n\t\treturn;\n\t}\n\tconst auto lifetime = _backLifetime.make_state<rpl::lifetime>();\n\t_back->setAcceptDrops(true);\n\t_back->events(\n\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\treturn e->type() == QEvent::DragEnter;\n\t}) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tusing namespace Storage;\n\t\tconst auto d = static_cast<QDragEnterEvent*>(e.get());\n\t\tconst auto data = d->mimeData();\n\t\tif (ComputeMimeDataState(data) == MimeDataState::None) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto timer = _backLifetime.make_state<base::Timer>([=] {\n\t\t\tbackClicked();\n\t\t});\n\t\ttimer->callOnce(ChoosePeerByDragTimeout);\n\t\td->setDropAction(Qt::CopyAction);\n\t\td->accept();\n\t\t_back->events(\n\t\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\t\treturn e->type() == QEvent::DragMove\n\t\t\t\t|| e->type() == QEvent::DragLeave;\n\t\t}) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\t\tif (e->type() == QEvent::DragMove) {\n\t\t\t\ttimer->callOnce(ChoosePeerByDragTimeout);\n\t\t\t} else if (e->type() == QEvent::DragLeave) {\n\t\t\t\ttimer->cancel();\n\t\t\t\tlifetime->destroy();\n\t\t\t}\n\t\t}, *lifetime);\n\t}, _backLifetime);\n}\n\nbool TopBarWidget::trackOnlineOf(not_null<PeerData*> user) const {\n\tconst auto peer = _activeChat.key.peer();\n\tif (!peer || _activeChat.key.topic() || !user->isUser()) {\n\t\treturn false;\n\t} else if (peer->isUser()) {\n\t\treturn (peer == user);\n\t} else if (const auto chat = peer->asChat()) {\n\t\treturn chat->participants.contains(user->asUser());\n\t} else if (const auto channel = peer->asMegagroup()) {\n\t\treturn channel->canViewMembers()\n\t\t\t&& ranges::contains(\n\t\t\t\tchannel->mgInfo->lastParticipants,\n\t\t\t\tnot_null{ user->asUser() });\n\t}\n\treturn false;\n}\n\nvoid TopBarWidget::updateOnlineDisplay() {\n\tconst auto peer = _activeChat.key.peer();\n\tif (!peer || _activeChat.key.topic()) {\n\t\treturn;\n\t}\n\n\tQString text;\n\tconst auto now = base::unixtime::now();\n\tbool titlePeerTextOnline = false;\n\tif (const auto user = peer->asUser()) {\n\t\tif (session().supportMode()\n\t\t\t&& !session().supportHelper().infoCurrent(user).text.empty()) {\n\t\t\ttext = QString::fromUtf8(\"\\xe2\\x9a\\xa0\\xef\\xb8\\x8f check info\");\n\t\t\ttitlePeerTextOnline = false;\n\t\t} else {\n\t\t\ttext = Data::OnlineTextFull(user, now);\n\t\t\ttitlePeerTextOnline = Data::OnlineTextActive(user, now);\n\t\t}\n\t} else if (const auto chat = peer->asChat()) {\n\t\tif (!chat->amIn()) {\n\t\t\ttext = tr::lng_chat_status_unaccessible(tr::now);\n\t\t} else if (chat->participants.empty()) {\n\t\t\tif (!_titlePeerText.isEmpty()) {\n\t\t\t\ttext = _titlePeerText.toString();\n\t\t\t} else if (chat->count <= 0) {\n\t\t\t\ttext = tr::lng_group_status(tr::now);\n\t\t\t} else {\n\t\t\t\ttext = tr::lng_chat_status_members(tr::now, lt_count_decimal, chat->count);\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto self = session().user();\n\t\t\tauto online = 0;\n\t\t\tauto onlyMe = true;\n\t\t\tfor (const auto &user : chat->participants) {\n\t\t\t\tif (user->lastseen().isOnline(now)) {\n\t\t\t\t\t++online;\n\t\t\t\t\tif (onlyMe && user != self) onlyMe = false;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (online > 0 && !onlyMe) {\n\t\t\t\tauto membersCount = tr::lng_chat_status_members(tr::now, lt_count_decimal, chat->participants.size());\n\t\t\t\tauto onlineCount = tr::lng_chat_status_online(tr::now, lt_count, online);\n\t\t\t\ttext = tr::lng_chat_status_members_online(tr::now, lt_members_count, membersCount, lt_online_count, onlineCount);\n\t\t\t} else if (chat->participants.size() > 0) {\n\t\t\t\ttext = tr::lng_chat_status_members(tr::now, lt_count_decimal, chat->participants.size());\n\t\t\t} else {\n\t\t\t\ttext = tr::lng_group_status(tr::now);\n\t\t\t}\n\t\t}\n\t} else if (const auto monoforum = peer->monoforum()) {\n\t\tconst auto chats = monoforum->chatsList();\n\t\tconst auto count = chats->fullSize().current();\n\t\ttext = (count > 0)\n\t\t\t? tr::lng_filters_chats_count(tr::now, lt_count, count)\n\t\t\t: tr::lng_filters_no_chats(tr::now);\n\t} else if (peer->isMonoforum()) {\n\t\ttext = tr::lng_chat_status_direct(tr::now);\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tif (channel->isMegagroup()\n\t\t\t&& channel->canViewMembers()\n\t\t\t&& (channel->membersCount() > 0)\n\t\t\t&& (channel->membersCount()\n\t\t\t\t<= channel->session().serverConfig().chatSizeMax)) {\n\t\t\tif (channel->lastParticipantsRequestNeeded()) {\n\t\t\t\tsession().api().chatParticipants().requestLast(channel);\n\t\t\t}\n\t\t\tconst auto self = session().user();\n\t\t\tauto online = 0;\n\t\t\tauto onlyMe = true;\n\t\t\tfor (auto &participant : std::as_const(channel->mgInfo->lastParticipants)) {\n\t\t\t\tif (participant->lastseen().isOnline(now)) {\n\t\t\t\t\t++online;\n\t\t\t\t\tif (onlyMe && participant != self) {\n\t\t\t\t\t\tonlyMe = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (online && !onlyMe) {\n\t\t\t\tauto membersCount = tr::lng_chat_status_members(tr::now, lt_count_decimal, channel->membersCount());\n\t\t\t\tauto onlineCount = tr::lng_chat_status_online(tr::now, lt_count, online);\n\t\t\t\ttext = tr::lng_chat_status_members_online(tr::now, lt_members_count, membersCount, lt_online_count, onlineCount);\n\t\t\t} else if (channel->membersCount() > 0) {\n\t\t\t\ttext = tr::lng_chat_status_members(tr::now, lt_count_decimal, channel->membersCount());\n\t\t\t} else {\n\t\t\t\ttext = tr::lng_group_status(tr::now);\n\t\t\t}\n\t\t} else if (channel->membersCount() > 0) {\n\t\t\ttext = channel->isMegagroup()\n\t\t\t\t? tr::lng_chat_status_members(tr::now, lt_count_decimal, channel->membersCount())\n\t\t\t\t: tr::lng_chat_status_subscribers(tr::now, lt_count_decimal, channel->membersCount());\n\n\t\t} else {\n\t\t\ttext = channel->isMegagroup() ? tr::lng_group_status(tr::now) : tr::lng_channel_status(tr::now);\n\t\t}\n\t}\n\tif (_titlePeerText.toString() != text) {\n\t\t_titlePeerText.setText(st::dialogsTextStyle, text);\n\t\t_titlePeerTextOnline = titlePeerTextOnline;\n\t\tupdateMembersShowArea();\n\t\tupdate();\n\t}\n\tupdateOnlineDisplayTimer();\n}\n\nvoid TopBarWidget::updateOnlineDisplayTimer() {\n\tconst auto peer = _activeChat.key.peer();\n\tif (!peer) {\n\t\treturn;\n\t}\n\n\tconst auto now = base::unixtime::now();\n\tauto minTimeout = 86400 * crl::time(1000);\n\tconst auto handleUser = [&](not_null<UserData*> user) {\n\t\tconst auto hisTimeout = Data::OnlineChangeTimeout(user, now);\n\t\taccumulate_min(minTimeout, hisTimeout);\n\t};\n\tif (const auto user = peer->asUser()) {\n\t\thandleUser(user);\n\t} else if (const auto chat = peer->asChat()) {\n\t\tfor (const auto &user : chat->participants) {\n\t\t\thandleUser(user);\n\t\t}\n\t} else if (peer->isChannel()) {\n\t}\n\tupdateOnlineDisplayIn(minTimeout);\n}\n\nvoid TopBarWidget::updateOnlineDisplayIn(crl::time timeout) {\n\t_onlineUpdater.callOnce(timeout);\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_top_bar_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/unread_badge.h\"\n#include \"ui/effects/animations.h\"\n#include \"base/timer.h\"\n#include \"base/object_ptr.h\"\n#include \"data/data_report.h\"\n#include \"dialogs/dialogs_key.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass AbstractButton;\nclass RoundButton;\nclass IconButton;\nclass PopupMenu;\nclass UnreadBadge;\nclass InputField;\nclass CrossButton;\nclass InfiniteRadialAnimation;\ntemplate <typename Widget>\nclass FadeWrapScaled;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Calls {\nstruct StartOutgoingCallArgs;\n} // namespace Calls\n\nnamespace HistoryView {\n\nclass SendActionPainter;\n\n[[nodiscard]] QString SwitchToChooseFromQuery();\n\nclass TopBarWidget final : public Ui::RpWidget {\npublic:\n\tstruct SelectedState {\n\t\tbool textSelected = false;\n\t\tint count = 0;\n\t\tint canDeleteCount = 0;\n\t\tint canForwardCount = 0;\n\t\tint canSendNowCount = 0;\n\t};\n\tusing ActiveChat = Dialogs::EntryState;\n\tusing Section = ActiveChat::Section;\n\n\tTopBarWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~TopBarWidget();\n\n\t[[nodiscard]] Main::Session &session() const;\n\n\tvoid updateControlsVisibility();\n\tvoid finishAnimating();\n\tvoid showSelected(SelectedState state);\n\t[[nodiscard]] bool showSelectedState() const;\n\trpl::producer<bool> membersShowAreaActive() const {\n\t\treturn _membersShowAreaActive.events();\n\t}\n\tvoid setAnimatingMode(bool enabled);\n\n\tvoid setActiveChat(\n\t\tActiveChat activeChat,\n\t\tSendActionPainter *sendAction);\n\tvoid setCustomTitle(const QString &title);\n\n\tvoid showChooseMessagesForReport(Data::ReportInput reportInput);\n\tvoid clearChooseMessagesForReport();\n\n\tbool toggleSearch(bool shown, anim::type animated);\n\tvoid searchEnableJumpToDate(bool enable);\n\tvoid searchEnableChooseFromUser(bool enable, bool visible);\n\tbool searchSetFocus();\n\t[[nodiscard]] bool searchMode() const;\n\t[[nodiscard]] bool searchHasFocus() const;\n\t[[nodiscard]] rpl::producer<> searchCancelled() const;\n\t[[nodiscard]] rpl::producer<> searchSubmitted() const;\n\t[[nodiscard]] rpl::producer<QString> searchQuery() const;\n\t[[nodiscard]] QString searchQueryCurrent() const;\n\t[[nodiscard]] int searchQueryCursorPosition() const;\n\tvoid searchClear();\n\tvoid searchSetText(const QString &query, int cursorPosition = -1);\n\n\t[[nodiscard]] rpl::producer<> forwardSelectionRequest() const {\n\t\treturn _forwardSelection.events();\n\t}\n\t[[nodiscard]] rpl::producer<> forwardAndDeleteSelectionRequest() const {\n\t\treturn _forwardAndDeleteSelection.events();\n\t}\n\t[[nodiscard]] rpl::producer<> sendNowSelectionRequest() const {\n\t\treturn _sendNowSelection.events();\n\t}\n\t[[nodiscard]] rpl::producer<> deleteSelectionRequest() const {\n\t\treturn _deleteSelection.events();\n\t}\n\t[[nodiscard]] rpl::producer<> clearSelectionRequest() const {\n\t\treturn _clearSelection.events();\n\t}\n\t[[nodiscard]] rpl::producer<> cancelChooseForReportRequest() const {\n\t\treturn _cancelChooseForReport.events();\n\t}\n\t[[nodiscard]] rpl::producer<> jumpToDateRequest() const {\n\t\treturn _jumpToDateRequests.events();\n\t}\n\t[[nodiscard]] rpl::producer<> chooseFromUserRequest() const {\n\t\treturn _chooseFromUserRequests.events();\n\t}\n\t[[nodiscard]] rpl::producer<> searchRequest() const;\n\n\tvoid setGeometryWithNarrowRatio(\n\t\tQRect geometry,\n\t\tint narrowWidth,\n\t\tfloat64 narrowRatio);\n\tvoid showPeerMenu();\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tbool eventFilter(QObject *obj, QEvent *e) override;\n\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tstruct EmojiInteractionSeenAnimation;\n\n\t[[nodiscard]] bool rootChatsListBar() const;\n\tvoid refreshInfoButton();\n\tvoid refreshLang();\n\tvoid updateSearchVisibility();\n\tvoid updateControlsGeometry();\n\tvoid slideAnimationCallback();\n\tvoid updateInfoToggleActive();\n\tvoid setupDragOnBackButton();\n\n\tvoid call(Calls::StartOutgoingCallArgs);\n\tvoid groupCall();\n\tvoid showGroupCallMenu(not_null<PeerData*> peer);\n\tvoid showCallMenu();\n\tvoid toggleInfoSection();\n\n\t[[nodiscard]] bool createMenu(\n\t\tnot_null<Ui::IconButton*> button,\n\t\tbool withIcons = true);\n\n\tvoid handleEmojiInteractionSeen(const QString &emoticon);\n\tbool paintSendAction(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tstyle::color fg,\n\t\tcrl::time now);\n\n\tvoid updateConnectingState();\n\tvoid updateAdaptiveLayout();\n\tint countSelectedButtonsTop(float64 selectedShown);\n\tvoid connectingAnimationCallback();\n\n\tvoid paintTopBar(Painter &p);\n\tvoid paintStatus(\n\t\tPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint availableWidth,\n\t\tint outerWidth);\n\tbool paintConnectingState(Painter &p, int left, int top, int outerWidth);\n\t[[nodiscard]] QRect getMembersShowAreaGeometry() const;\n\t[[nodiscard]] bool trackOnlineOf(not_null<PeerData*> user) const;\n\tvoid updateMembersShowArea();\n\tvoid updateOnlineDisplay();\n\tvoid updateOnlineDisplayTimer();\n\tvoid updateOnlineDisplayIn(crl::time timeout);\n\n\tvoid infoClicked();\n\tvoid backClicked();\n\n\tvoid refreshUnreadBadge();\n\tvoid updateUnreadBadge();\n\tvoid setChooseForReportReason(std::optional<Data::ReportInput>);\n\tvoid toggleSelectedControls(bool shown);\n\t[[nodiscard]] bool showSelectedActions() const;\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst bool _primaryWindow = false;\n\tActiveChat _activeChat;\n\tQString _customTitleText;\n\tstd::unique_ptr<EmojiInteractionSeenAnimation> _emojiInteractionSeen;\n\trpl::lifetime _activeChatLifetime;\n\n\tUi::PeerBadge _titleBadge;\n\tUi::Text::String _title;\n\tint _titleNameVersion = 0;\n\n\tint _selectedCount = 0;\n\tbool _canDelete = false;\n\tbool _canForward = false;\n\tbool _canSendNow = false;\n\tbool _searchMode = false;\n\n\tUi::Animations::Simple _selectedShown;\n\tUi::Animations::Simple _searchShown;\n\n\tobject_ptr<Ui::RoundButton> _clear;\n\tobject_ptr<Ui::RoundButton> _forward, _sendNow, _delete, _forwardAndDelete;\n\tobject_ptr<Ui::InputField> _searchField = { nullptr };\n\tobject_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _chooseFromUser\n\t\t= { nullptr };\n\tobject_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _jumpToDate\n\t\t= { nullptr };\n\tobject_ptr<Ui::CrossButton> _searchCancel = { nullptr };\n\trpl::variable<QString> _searchQuery;\n\trpl::event_stream<> _searchCancelled;\n\trpl::event_stream<> _searchSubmitted;\n\trpl::event_stream<> _jumpToDateRequests;\n\trpl::event_stream<> _chooseFromUserRequests;\n\n\tobject_ptr<Ui::IconButton> _back;\n\tobject_ptr<Ui::IconButton> _cancelChoose;\n\tobject_ptr<Ui::UnreadBadge> _unreadBadge = { nullptr };\n\tobject_ptr<Ui::AbstractButton> _info = { nullptr };\n\n\tobject_ptr<Ui::IconButton> _call;\n\tobject_ptr<Ui::IconButton> _groupCall;\n\tobject_ptr<Ui::IconButton> _search;\n\tobject_ptr<Ui::IconButton> _infoToggle;\n\tobject_ptr<Ui::IconButton> _menuToggle;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tobject_ptr<RpWidget> _membersShowArea = { nullptr };\n\trpl::event_stream<bool> _membersShowAreaActive;\n\n\tfloat64 _narrowRatio = 0.;\n\tint _narrowWidth = 0;\n\n\tUi::Text::String _titlePeerText;\n\tbool _titlePeerTextOnline = false;\n\tint _leftTaken = 0;\n\tint _rightTaken = 0;\n\tbool _animatingMode = false;\n\tstd::unique_ptr<Ui::InfiniteRadialAnimation> _connecting;\n\n\tSendActionPainter *_sendAction = nullptr;\n\tstd::optional<Data::ReportInput> _chooseForReportReason;\n\n\tbase::Timer _onlineUpdater;\n\n\trpl::event_stream<> _forwardSelection;\n\trpl::event_stream<> _forwardAndDeleteSelection;\n\trpl::event_stream<> _sendNowSelection;\n\trpl::event_stream<> _deleteSelection;\n\trpl::event_stream<> _clearSelection;\n\trpl::event_stream<> _cancelChooseForReport;\n\n\trpl::lifetime _backLifetime;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp",
    "content": "// This file is part of Telegram Desktop,\n// the official desktop application for the Telegram messaging service.\n//\n// For license and copyright information please follow this link:\n// https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n//\n#include \"history/view/history_view_top_peers_selector.h\"\n\n#include \"apiwrap.h\"\n#include \"base/unique_qptr.h\"\n#include \"chat_helpers/share_message_phrase_factory.h\"\n#include \"data/components/top_peers.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"main/session/session_show.h\"\n#include \"ui/controls/dynamic_images_strip.h\"\n#include \"ui/controls/popup_selector.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/rect.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kMaxPeers = 5;\n\n[[nodiscard]] std::vector<not_null<PeerData*>> CollectPeers(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto user = session->user();\n\tauto topPeers = session->topPeers().list();\n\tconst auto it = ranges::find(topPeers, user);\n\tif (it != topPeers.end()) {\n\t\ttopPeers.erase(it);\n\t}\n\tauto result = std::vector<not_null<PeerData*>>();\n\tresult.push_back(user);\n\tfor (const auto &peer : topPeers | ranges::views::take(kMaxPeers - 1)) {\n\t\tresult.push_back(peer);\n\t}\n\treturn result;\n}\n\n} // namespace\n\nvoid ShowTopPeersSelector(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tFullMsgId fullId,\n\t\tQPoint globalPos) {\n\tconst auto session = &show->session();\n\tconst auto peers = CollectPeers(session);\n\tauto thumbnails = std::vector<std::shared_ptr<Ui::DynamicImage>>();\n\tthumbnails.reserve(peers.size());\n\tfor (const auto &peer : peers) {\n\t\tthumbnails.push_back(peer->isSelf()\n\t\t\t? Ui::MakeSavedMessagesThumbnail()\n\t\t\t: Ui::MakeUserpicThumbnail(peer));\n\t}\n\n\tconst auto send = [=](not_null<PeerData*> peer) {\n\t\tif (const auto item = session->data().message(fullId)) {\n\t\t\tconst auto items = session->data().idsToItems(\n\t\t\t\tsession->data().itemOrItsGroup(item));\n\t\t\tconst auto single = (items.size() == 1);\n\t\t\tsession->api().forwardMessages(\n\t\t\t\tData::ResolvedForwardDraft{ .items = items },\n\t\t\t\tApi::SendAction(session->data().history(peer)),\n\t\t\t\t[=] {\n\t\t\t\t\tusing namespace ChatHelpers;\n\t\t\t\t\tauto text = rpl::variable<TextWithEntities>(\n\t\t\t\t\t\tForwardedMessagePhrase({\n\t\t\t\t\t\t\t.toCount = 1,\n\t\t\t\t\t\t\t.singleMessage = single,\n\t\t\t\t\t\t\t.to1 = peer,\n\t\t\t\t\t\t})).current();\n\t\t\t\t\tshow->showToast(std::move(text));\n\t\t\t\t});\n\t\t}\n\t};\n\n\tconst auto contentWidth = peers.size() * st::topPeersSelectorUserpicSize\n\t\t+ (peers.size() - 1) * st::topPeersSelectorUserpicGap;\n\tconst auto contentHeight = int(\n\t\tst::topPeersSelectorUserpicSize\n\t\t\t* (1. + st::topPeersSelectorUserpicExpand));\n\tconst auto selectorHeight = contentHeight\n\t\t+ 2 * st::topPeersSelectorPadding;\n\tconst auto selectorWidth = (peers.size() == 1)\n\t\t? selectorHeight\n\t\t: (contentWidth + 2 * st::topPeersSelectorPadding);\n\n\tstruct State {\n\t\tbase::unique_qptr<Ui::PopupSelector> selector;\n\t\tbase::unique_qptr<Ui::ImportantTooltip> tooltip;\n\t\tUi::Animations::Simple animation;\n\t\tbool finishing = false;\n\t};\n\tconst auto state = std::make_shared<State>();\n\n\tstate->selector = base::make_unique_q<Ui::PopupSelector>(\n\t\tparent,\n\t\tQSize(selectorWidth, selectorHeight));\n\tconst auto selector = state->selector.get();\n\tselector->setHideFinishedCallback([=, state = std::weak_ptr(state)] {\n\t\tif (const auto s = state.lock()) {\n\t\t\ts->selector = nullptr;\n\t\t\ts->tooltip = nullptr;\n\t\t}\n\t});\n\tconst auto userpicsWidget = Ui::CreateChild<Ui::DynamicImagesStrip>(\n\t\tselector,\n\t\tstd::move(thumbnails),\n\t\tst::topPeersSelectorUserpicSize,\n\t\tst::topPeersSelectorUserpicGap);\n\tconst auto margins = selector->marginsForShadow();\n\tconst auto x = (selectorWidth - contentWidth) / 2 + margins.left();\n\tconst auto y = (selectorHeight - contentHeight) / 2 + margins.top();\n\tuserpicsWidget->setGeometry(\n\t\tQRect(x, y, contentWidth, contentHeight)\n\t\t\t+ Margins(int(st::topPeersSelectorUserpicSize\n\t\t\t\t* st::topPeersSelectorUserpicExpand)));\n\tuserpicsWidget->setCursor(style::cur_pointer);\n\n\tconst auto hideAll = [=] {\n\t\tstate->finishing = true;\n\t\tif (state->tooltip) {\n\t\t\tstate->tooltip->toggleAnimated(false);\n\t\t}\n\t\tselector->setAttribute(Qt::WA_TransparentForMouseEvents, true);\n\t\tselector->hideAnimated();\n\t};\n\n\tuserpicsWidget->setClickCallback([=](int index) {\n\t\tif (state->finishing) {\n\t\t\treturn;\n\t\t}\n\t\tsend(peers[index]);\n\t\thideAll();\n\t});\n\tuserpicsWidget->hoveredItemValue(\n\t) | rpl::on_next([=](Ui::HoveredItemInfo info) {\n\t\tif (info.index < 0) {\n\t\t\tstate->tooltip = nullptr;\n\t\t\treturn;\n\t\t}\n\t\tusing namespace Info::Profile;\n\t\tstate->tooltip = base::make_unique_q<Ui::ImportantTooltip>(\n\t\t\tparent,\n\t\t\tobject_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\t\t\tselector,\n\t\t\t\tUi::MakeNiceTooltipLabel(\n\t\t\t\t\tparent,\n\t\t\t\t\tpeers[info.index]->isSelf()\n\t\t\t\t\t\t? tr::lng_saved_messages(tr::rich)\n\t\t\t\t\t\t: NameValue(peers[info.index]) | rpl::map(tr::rich),\n\t\t\t\t\tstd::max(\n\t\t\t\t\t\tuserpicsWidget->width(),\n\t\t\t\t\t\tst::topPeersSelectorImportantTooltipLabel.minWidth\n\t\t\t\t\t\t\t+ st::lineWidth),\n\t\t\t\t\tst::topPeersSelectorImportantTooltipLabel),\n\t\t\t\tst::topPeersSelectorImportantTooltip.padding),\n\t\t\tst::topPeersSelectorImportantTooltip);\n\t\tstate->tooltip->setWindowFlags(Qt::WindowFlags(Qt::ToolTip)\n\t\t\t| Qt::BypassWindowManagerHint\n\t\t\t| Qt::NoDropShadowWindowHint\n\t\t\t| Qt::FramelessWindowHint);\n\t\tstate->tooltip->setAttribute(Qt::WA_NoSystemBackground, true);\n\t\tstate->tooltip->setAttribute(Qt::WA_TranslucentBackground, true);\n\t\tstate->tooltip->setAttribute(Qt::WA_TransparentForMouseEvents, true);\n\t\tconst auto step = st::topPeersSelectorUserpicSize\n\t\t\t+ st::topPeersSelectorUserpicGap;\n\t\tconst auto shift = (userpicsWidget->height()\n\t\t\t- st::topPeersSelectorUserpicSize) / 2;\n\t\tconst auto localX = info.index * step + shift;\n\t\tconst auto avatarRect = QRect(\n\t\t\tlocalX,\n\t\t\t-shift,\n\t\t\tst::topPeersSelectorUserpicSize,\n\t\t\tst::topPeersSelectorUserpicSize);\n\t\tconst auto globalRect = QRect(\n\t\t\tuserpicsWidget->mapToGlobal(avatarRect.topLeft()),\n\t\t\tavatarRect.size());\n\t\tstate->tooltip->pointAt(globalRect, RectPart::Top);\n\t\tstate->tooltip->toggleAnimated(true);\n\t}, selector->lifetime());\n\tselector->updateShowState(0, 0, true);\n\tselector->popup((!globalPos.isNull() ? globalPos : QCursor::pos())\n\t\t- QPoint(selector->width() / 2, selector->height())\n\t\t+ st::topPeersSelectorSkip);\n\tselector->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::KeyPress) {\n\t\t\tconst auto key = static_cast<QKeyEvent*>(e.get());\n\t\t\tif (key->key() == Qt::Key_Escape) {\n\t\t\t\thideAll();\n\t\t\t} else {\n\t\t\t\tuserpicsWidget->handleKeyPressEvent(key);\n\t\t\t}\n\t\t}\n\t}, selector->lifetime());\n\tcrl::on_main(selector, [=] {\n\t\tselector->setFocus();\n\t});\n\n\tconstexpr auto kShift = 0.15;\n\tstate->animation.start([=](float64 value) {\n\t\tconst auto userpicsProgress = std::clamp((value - kShift), 0., 1.);\n\t\tuserpicsWidget->setProgress(anim::easeInQuint(1, userpicsProgress));\n\t\tvalue = std::clamp(value, 0., 1.);\n\t\tselector->updateShowState(value, value, true);\n\t}, 0., 1. + kShift, st::fadeWrapDuration * 3, anim::easeOutQuint);\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_top_peers_selector.h",
    "content": "// This file is part of Telegram Desktop,\n// the official desktop application for the Telegram messaging service.\n//\n// For license and copyright information please follow this link:\n// https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n//\n#pragma once\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Main {\nclass SessionShow;\n} // namespace Main\n\nnamespace HistoryView {\n\nvoid ShowTopPeersSelector(\n\tnot_null<Ui::RpWidget*> parent,\n\tstd::shared_ptr<Main::SessionShow> show,\n\tFullMsgId fullId,\n\tQPoint globalPos);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_transcribe_button.h\"\n\n#include \"base/unixtime.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"core/click_handler_types.h\" // ClickHandlerContext\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"api/api_transcribes.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_chat.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kInNonChosenOpacity = 0.12;\nconstexpr auto kOutNonChosenOpacity = 0.18;\nconstexpr auto kArrowPivotNear = 0.349;\nconstexpr auto kArrowPivotFar = 1. - kArrowPivotNear;\n\nvoid ClipPainterForLock(QPainter &p, bool roundview, const QRect &r) {\n\tconst auto &pos = roundview\n\t\t? st::historyFastTranscribeLockOverlayPos\n\t\t: st::historyTranscribeLockOverlayPos;\n\tconst auto &size = roundview\n\t\t? st::historyFastTranscribeLockOverlaySize\n\t\t: st::historyTranscribeLockOverlaySize;\n\n\tauto clipPath = QPainterPath();\n\tclipPath.addRect(r);\n\tconst auto clear = QRect(pos + r.topLeft(), size);\n\tclipPath.addRoundedRect(clear, clear.width() * 0.5, clear.height() * 0.5);\n\tp.setClipPath(clipPath);\n}\n\n} // namespace\n\nTranscribeButton::TranscribeButton(\n\tnot_null<HistoryItem*> item,\n\tbool roundview,\n\tbool summarize)\n: _item(item)\n, _roundview(roundview)\n, _summarize(summarize)\n, _size(!roundview && !_summarize\n\t? st::historyTranscribeSize\n\t: QSize(st::historyFastShareSize, st::historyFastShareSize)) {\n}\n\nTranscribeButton::~TranscribeButton() = default;\n\nQSize TranscribeButton::size() const {\n\treturn _size;\n}\n\nvoid TranscribeButton::setLoading(bool loading) {\n\tif (_loading == loading) {\n\t\treturn;\n\t}\n\t_loading = loading;\n\tif (_loading) {\n\t\tconst auto session = &_item->history()->session();\n\t\t_animation = std::make_unique<Ui::InfiniteRadialAnimation>(\n\t\t\t[=, itemId = _item->fullId()] {\n\t\t\t\tif (const auto item = session->data().message(itemId)) {\n\t\t\t\t\tsession->data().requestItemRepaint(\n\t\t\t\t\t\titem,\n\t\t\t\t\t\t_lastPaintedPoint.isNull()\n\t\t\t\t\t\t\t? QRect()\n\t\t\t\t\t\t\t: (QRect(_lastPaintedPoint, size()))\n\t\t\t\t\t\t\t\t+ Margins(st::lineWidth));\n\t\t\t\t}\n\t\t\t},\n\t\t\tst::historyTranscribeRadialAnimation);\n\t\t_animation->start();\n\t} else if (_animation) {\n\t\t_animation->stopWithFade();\n\t}\n}\n\nbool TranscribeButton::loading() const {\n\treturn _loading;\n}\n\nvoid TranscribeButton::paint(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tconst PaintContext &context) {\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto opened = _openedAnimation.value(_opened ? 1. : 0.);\n\tconst auto stm = context.messageStyle();\n\tif (_roundview || _summarize) {\n\t\t_lastPaintedPoint = { x, y };\n\t\tconst auto r = QRect(QPoint(x, y), size());\n\n\t\tif (_ripple) {\n\t\t\tconst auto colorOverride = &stm->msgWaveformInactive->c;\n\t\t\t_ripple->paint(\n\t\t\t\tp,\n\t\t\t\tx,\n\t\t\t\ty,\n\t\t\t\tr.width(),\n\t\t\t\tcolorOverride);\n\t\t\tif (_ripple->empty()) {\n\t\t\t\t_ripple.reset();\n\t\t\t}\n\t\t}\n\n\t\tconst auto state = _animation\n\t\t\t? _animation->computeState()\n\t\t\t: Ui::RadialState();\n\t\tconst auto staticLoading = anim::Disabled() && state.shown > 0;\n\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(context.st->msgServiceBg());\n\n\t\tp.drawEllipse(r);\n\t\tif (_summarize) {\n\t\t\tif (hasLock()) {\n\t\t\t\tcontext.st->historyFastTranscribeLock().paint(\n\t\t\t\t\tp,\n\t\t\t\t\tr.topLeft() + st::historyFastSummaryLockPos,\n\t\t\t\t\tr.width());\n\t\t\t}\n\t\t\tif (!staticLoading) [[likely]] {\n\t\t\t\tconst auto shown = _item->history()\n\t\t\t\t\t->session().api().transcribes().summary(_item).shown;\n\t\t\t\tif (_summaryShown != shown) {\n\t\t\t\t\t_summaryShown = shown;\n\t\t\t\t\tconst auto session = &_item->history()->session();\n\t\t\t\t\t_openedAnimation.start(\n\t\t\t\t\t\t[=, itemId = _item->fullId()] {\n\t\t\t\t\t\t\tif (const auto i = session->data().message(\n\t\t\t\t\t\t\t\t\titemId)) {\n\t\t\t\t\t\t\t\tsession->data().requestItemRepaint(i);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\tshown ? 0. : 1.,\n\t\t\t\t\t\tshown ? 1. : 0.,\n\t\t\t\t\t\tst::fadeWrapDuration);\n\t\t\t\t}\n\t\t\t\tconst auto t\n\t\t\t\t\t= _openedAnimation.value(_summaryShown ? 1. : 0.);\n\n\t\t\t\tconst auto fg = context.st->msgServiceFg()->c;\n\t\t\t\tst::historySummaryStars.paintInCenter(p, r, fg);\n\n\t\t\t\tconst auto &arrow = st::historySummaryArrows;\n\t\t\t\tconst auto sz = r.width();\n\t\t\t\tconst auto cx = r.x() + sz / 2.;\n\t\t\t\tconst auto cy = r.y() + sz / 2.;\n\n\t\t\t\t// First arrow.\n\t\t\t\t{\n\t\t\t\t\tp.save();\n\t\t\t\t\tif (t < 0.5) {\n\t\t\t\t\t\tconst auto s = std::abs(t - 0.5) + 0.5;\n\t\t\t\t\t\tp.translate(cx, cy);\n\t\t\t\t\t\tp.scale(s, s);\n\t\t\t\t\t\tp.translate(-cx, -cy);\n\t\t\t\t\t}\n\t\t\t\t\tif (t > 0.5) {\n\t\t\t\t\t\tconst auto s = std::abs(t - 0.5) + 0.5;\n\t\t\t\t\t\tconst auto px = r.x() + sz * kArrowPivotNear;\n\t\t\t\t\t\tconst auto py = r.y() + sz * kArrowPivotFar;\n\t\t\t\t\t\tp.translate(px, py);\n\t\t\t\t\t\tp.scale(-s, -s);\n\t\t\t\t\t\tp.translate(-px, -py);\n\t\t\t\t\t\tp.translate(\n\t\t\t\t\t\t\t-sz * (1. - s) * 0.4,\n\t\t\t\t\t\t\tsz * (1. - s) * 0.4);\n\t\t\t\t\t}\n\t\t\t\t\tarrow.paintInCenter(p, QRectF(r), fg);\n\t\t\t\t\tp.restore();\n\t\t\t\t}\n\n\t\t\t\t// Second arrow (rotated 180 degrees).\n\t\t\t\t{\n\t\t\t\t\tp.save();\n\t\t\t\t\tif (t < 0.5) {\n\t\t\t\t\t\tconst auto s = std::abs(t - 0.5) + 0.5;\n\t\t\t\t\t\tp.translate(cx, cy);\n\t\t\t\t\t\tp.scale(s, s);\n\t\t\t\t\t\tp.translate(-cx, -cy);\n\t\t\t\t\t}\n\t\t\t\t\tif (t > 0.5) {\n\t\t\t\t\t\tconst auto s = std::abs(t - 0.5) + 0.5;\n\t\t\t\t\t\tconst auto px = r.x() + sz * kArrowPivotFar;\n\t\t\t\t\t\tconst auto py = r.y() + sz * kArrowPivotNear;\n\t\t\t\t\t\tp.translate(px, py);\n\t\t\t\t\t\tp.scale(-s, -s);\n\t\t\t\t\t\tp.translate(-px, -py);\n\t\t\t\t\t}\n\t\t\t\t\tp.translate(cx, cy);\n\t\t\t\t\tp.rotate(180.);\n\t\t\t\t\tp.translate(-cx, -cy);\n\t\t\t\t\tif (t > 0.5) {\n\t\t\t\t\t\tconst auto s = std::abs(t - 0.5) + 0.5;\n\t\t\t\t\t\tp.translate(\n\t\t\t\t\t\t\t-sz * (1. - s) * 0.4,\n\t\t\t\t\t\t\tsz * (1. - s) * 0.4);\n\t\t\t\t\t}\n\t\t\t\t\tarrow.paintInCenter(p, QRectF(r), fg);\n\t\t\t\t\tp.restore();\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (!_loading && hasLock()) {\n\t\t\tClipPainterForLock(p, true, r);\n\t\t\tcontext.st->historyFastTranscribeIcon().paintInCenter(p, r);\n\t\t\tp.setClipping(false);\n\t\t\tcontext.st->historyFastTranscribeLock().paint(\n\t\t\t\tp,\n\t\t\t\tr.topLeft() + st::historyFastTranscribeLockPos,\n\t\t\t\tr.width());\n\t\t} else {\n\t\t\tcontext.st->historyFastTranscribeIcon().paintInCenter(p, r);\n\t\t}\n\n\t\tauto pen = QPen(st::msgServiceFg);\n\t\tpen.setCapStyle(Qt::RoundCap);\n\t\tp.setPen(pen);\n\t\tif (staticLoading) [[unlikely]] {\n\t\t\tconst auto _st = &st::defaultRadio;\n\t\t\tanim::DrawStaticLoading(\n\t\t\t\tp,\n\t\t\t\tr,\n\t\t\t\t_st->thickness,\n\t\t\t\tpen.color(),\n\t\t\t\t_st->bg);\n\t\t} else if (state.arcLength < arc::kFullLength) {\n\t\t\tconst auto opacity = p.opacity();\n\t\t\tp.setOpacity(state.shown * (1. - opened));\n\t\t\tp.drawArc(r, state.arcFrom, state.arcLength);\n\t\t\tp.setOpacity(opacity);\n\t\t}\n\n\t\treturn;\n\t}\n\tauto bg = stm->msgFileBg->c;\n\tbg.setAlphaF(bg.alphaF() * (context.outbg\n\t\t? kOutNonChosenOpacity\n\t\t: kInNonChosenOpacity));\n\tp.setBrush(bg);\n\tconst auto radius = st::historyTranscribeRadius;\n\tconst auto state = _animation\n\t\t? _animation->computeState()\n\t\t: Ui::RadialState();\n\tif (state.shown > 0.) {\n\t\tauto fg = stm->msgWaveformActive->c;\n\t\tfg.setAlphaF(fg.alphaF() * state.shown * (1. - opened));\n\t\tauto pen = QPen(fg);\n\t\tconst auto thickness = style::ConvertScaleExact(2.);\n\t\tconst auto widthNoRadius = size().width() - 2 * radius;\n\t\tconst auto heightNoRadius = size().height() - 2 * radius;\n\t\tconst auto length = 2 * (widthNoRadius + heightNoRadius)\n\t\t\t+ 2 * M_PI * radius;\n\t\tpen.setWidthF(thickness);\n\t\tpen.setCapStyle(Qt::RoundCap);\n\t\tconst auto ratio = length / (Ui::RadialState::kFull * thickness);\n\t\tconst auto filled = ratio * state.arcLength;\n\t\tpen.setDashPattern({ filled, (length / thickness) - filled });\n\t\tpen.setDashOffset(ratio * (state.arcFrom + state.arcLength));\n\t\tp.setPen(pen);\n\t} else {\n\t\tp.setPen(Qt::NoPen);\n\t\tif (!_loading) {\n\t\t\t_animation = nullptr;\n\t\t}\n\t}\n\tconst auto r = QRect{ QPoint(x, y), size() };\n\tp.drawRoundedRect(r, radius, radius);\n\tif (opened > 0.) {\n\t\tif (opened != 1.) {\n\t\t\tp.save();\n\t\t\tp.setOpacity(opened);\n\t\t\tp.translate(r.center());\n\t\t\tp.scale(opened, opened);\n\t\t\tp.translate(-r.center());\n\t\t}\n\t\tstm->historyTranscribeHide.paintInCenter(p, r);\n\t\tif (opened != 1.) {\n\t\t\tp.restore();\n\t\t}\n\t}\n\tif (opened < 1.) {\n\t\tif (opened != 0.) {\n\t\t\tp.save();\n\t\t\tp.setOpacity(1. - opened);\n\t\t\tp.translate(r.center());\n\t\t\tp.scale(1. - opened, 1. - opened);\n\t\t\tp.translate(-r.center());\n\t\t}\n\n\t\tif (!_loading && hasLock()) {\n\t\t\tClipPainterForLock(p, false, r);\n\t\t\tstm->historyTranscribeIcon.paintInCenter(p, r);\n\t\t\tp.setClipping(false);\n\t\t\tstm->historyTranscribeLock.paint(\n\t\t\t\tp,\n\t\t\t\tr.topLeft() + st::historyTranscribeLockPos,\n\t\t\t\tr.width());\n\t\t} else {\n\t\t\tstm->historyTranscribeIcon.paintInCenter(p, r);\n\t\t}\n\n\t\tif (opened != 0.) {\n\t\t\tp.restore();\n\t\t}\n\t}\n\tp.setOpacity(1.);\n}\n\nbool TranscribeButton::hasLock() const {\n\tconst auto session = &_item->history()->session();\n\tif (session->premium()) {\n\t\treturn false;\n\t}\n\tconst auto transcribes = &session->api().transcribes();\n\tif (_summarize) {\n\t\treturn transcribes->summary(_item).premiumRequired;\n\t}\n\tif (transcribes->freeFor(_item) || transcribes->trialsCount()) {\n\t\treturn false;\n\t}\n\tconst auto until = transcribes->trialsRefreshAt();\n\tif (!until || base::unixtime::now() >= until) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nvoid TranscribeButton::setOpened(bool opened, Fn<void()> update) {\n\tif (_opened == opened) {\n\t\treturn;\n\t}\n\t_opened = opened;\n\tif (update) {\n\t\t_openedAnimation.start(\n\t\t\tstd::move(update),\n\t\t\t_opened ? 0. : 1.,\n\t\t\t_opened ? 1. : 0.,\n\t\t\tst::fadeWrapDuration);\n\t} else {\n\t\t_openedAnimation.stop();\n\t}\n}\n\nClickHandlerPtr TranscribeButton::link() {\n\tif (!_item->isHistoryEntry() || _item->isLocal()) {\n\t\treturn nullptr;\n\t} else if (_link) {\n\t\treturn _link;\n\t}\n\tconst auto session = &_item->history()->session();\n\tconst auto id = _item->fullId();\n\tconst auto summarize = _summarize;\n\t_link = std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto item = session->data().message(id);\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\t\tif (session->premium()) {\n\t\t\tauto &transcribes = session->api().transcribes();\n\t\t\treturn summarize\n\t\t\t\t? transcribes.toggleSummary(item)\n\t\t\t\t: transcribes.toggle(item);\n\t\t}\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (hasLock()) {\n\t\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\t\tif (summarize) {\n\t\t\t\t\tSettings::ShowPremium(controller, u\"summary\"_q);\n\t\t\t\t} else {\n\t\t\t\t\tShowPremiumPreviewBox(\n\t\t\t\t\t\tcontroller,\n\t\t\t\t\t\tPremiumFeature::VoiceToText);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto max = session->api().transcribes().trialsMaxLengthMs();\n\t\t\tconst auto doc = _item->media()\n\t\t\t\t? _item->media()->document()\n\t\t\t\t: nullptr;\n\t\t\tif (doc && (doc->isVoiceMessage() || doc->isVideoMessage())) {\n\t\t\t\tif (doc->duration() > max) {\n\t\t\t\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\t\t\t\tcontroller->uiShow()->showToast(\n\t\t\t\t\t\t\ttr::lng_audio_transcribe_long(tr::now));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (summarize) {\n\t\t\t\tsession->api().transcribes().toggleSummary(item);\n\t\t\t} else {\n\t\t\t\tsession->api().transcribes().toggle(item);\n\t\t\t}\n\t\t}\n\t});\n\treturn _link;\n}\n\nbool TranscribeButton::contains(const QPoint &p) {\n\t_lastStatePoint = p - _lastPaintedPoint;\n\tif (_summarize) {\n\t\t_summarizeHovered = QRect(_lastPaintedPoint, size()).contains(p);\n\t\treturn _summarizeHovered;\n\t} else {\n\t\treturn QRect(_lastPaintedPoint, size()).contains(p);\n\t}\n}\n\nvoid TranscribeButton::addRipple(Fn<void()> callback) {\n\tif (!_ripple) {\n\t\t_ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\tst::defaultRippleAnimation,\n\t\t\tUi::RippleAnimation::EllipseMask(size()),\n\t\t\tstd::move(callback));\n\t}\n\t_ripple->add(_lastStatePoint);\n}\n\nvoid TranscribeButton::stopRipple() const {\n\tif (_ripple) {\n\t\t_ripple->lastStop();\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_transcribe_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n\nnamespace Ui {\nstruct ChatPaintContext;\nclass InfiniteRadialAnimation;\nclass RippleAnimation;\n} // namespace Ui\n\nnamespace HistoryView {\n\nusing PaintContext = Ui::ChatPaintContext;\n\nclass TranscribeButton final {\npublic:\n\texplicit TranscribeButton(\n\t\tnot_null<HistoryItem*> item,\n\t\tbool roundview,\n\t\tbool summarize = false);\n\t~TranscribeButton();\n\n\t[[nodiscard]] QSize size() const;\n\n\tvoid setOpened(bool opened, Fn<void()> update);\n\tvoid setLoading(bool loading);\n\t[[nodiscard]] bool loading() const;\n\tvoid paint(QPainter &p, int x, int y, const PaintContext &context);\n\tvoid addRipple(Fn<void()> callback);\n\tvoid stopRipple() const;\n\n\t[[nodiscard]] ClickHandlerPtr link();\n\t[[nodiscard]] bool contains(const QPoint &p);\n\nprivate:\n\t[[nodiscard]] bool hasLock() const;\n\n\tconst not_null<HistoryItem*> _item;\n\tconst bool _roundview = false;\n\tconst bool _summarize = false;\n\tconst QSize _size;\n\n\tClickHandlerPtr _link;\n\tstd::unique_ptr<Ui::InfiniteRadialAnimation> _animation;\n\tstd::unique_ptr<Ui::RippleAnimation> _ripple;\n\tUi::Animations::Simple _openedAnimation;\n\tQString _text;\n\tQPoint _lastPaintedPoint;\n\tQPoint _lastStatePoint;\n\tbool _summarizeHovered = false;\n\tbool _loading = false;\n\tbool _opened = false;\n\tbool _summaryShown = false;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_translate_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_translate_bar.h\"\n\n#include \"boxes/translate_box.h\"\n#include \"ui/boxes/about_cocoon_box.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/ui_integration.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_document.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_credits_graphics.h\" // CreditsEntryBoxStyleOverrides\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/boxes/choose_language_box.h\" // EditSkipTranslationLanguages.\n#include \"ui/layers/box_content.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/menu/menu_item_base.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/menu/menu_multiline_action.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/painter.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtGui/QtEvents>\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kToastDuration = 4 * crl::time(1000);\n\nclass TwoTextAction final : public Ui::Menu::ItemBase {\npublic:\n\tTwoTextAction(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::Menu &st,\n\t\tconst QString &text1,\n\t\tconst QString &text2,\n\t\tFn<void()> callback,\n\t\tconst style::icon *icon,\n\t\tconst style::icon *iconOver);\n\n\tbool isEnabled() const override;\n\tnot_null<QAction*> action() const override;\n\n\tvoid handleKeyPress(not_null<QKeyEvent*> e) override;\n\nprivate:\n\tQPoint prepareRippleStartPosition() const override;\n\tQImage prepareRippleMask() const override;\n\n\tint contentHeight() const override;\n\n\tvoid paint(Painter &p);\n\tvoid prepare(const QString &text1);\n\n\tconst not_null<QAction*> _dummyAction;\n\tconst style::Menu &_st;\n\tconst style::icon *_icon;\n\tconst style::icon *_iconOver;\n\tUi::Text::String _text1;\n\tQString _text2;\n\tint _textWidth1 = 0;\n\tint _textWidth2 = 0;\n\tconst int _height;\n\n};\n\nTextParseOptions MenuTextOptions = {\n\tTextParseLinks, // flags\n\t0, // maxw\n\t0, // maxh\n\tQt::LayoutDirectionAuto, // dir\n};\n\nTwoTextAction::TwoTextAction(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::Menu &st,\n\tconst QString &text1,\n\tconst QString &text2,\n\tFn<void()> callback,\n\tconst style::icon *icon,\n\tconst style::icon *iconOver)\n: ItemBase(parent, st)\n, _dummyAction(Ui::CreateChild<QAction>(parent))\n, _st(st)\n, _icon(icon)\n, _iconOver(iconOver)\n, _text2(text2)\n, _height(st::ttlItemPadding.top()\n\t+ _st.itemStyle.font->height\n\t+ st::ttlItemTimerFont->height\n\t+ st::ttlItemPadding.bottom()) {\n\tfitToMenuWidth();\n\tsetActionTriggered(std::move(callback));\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(this);\n\t\tpaint(p);\n\t}, lifetime());\n\n\tenableMouseSelecting();\n\tprepare(text1);\n}\n\nvoid TwoTextAction::paint(Painter &p) {\n\tconst auto selected = isSelected();\n\tif (selected && _st.itemBgOver->c.alpha() < 255) {\n\t\tp.fillRect(0, 0, width(), _height, _st.itemBg);\n\t}\n\tp.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg);\n\tif (isEnabled()) {\n\t\tpaintRipple(p, 0, 0);\n\t}\n\n\tconst auto normalHeight = _st.itemPadding.top()\n\t\t+ _st.itemStyle.font->height\n\t\t+ _st.itemPadding.bottom();\n\tconst auto deltaHeight = _height - normalHeight;\n\tif (const auto icon = selected ? _iconOver : _icon) {\n\t\ticon->paint(\n\t\t\tp,\n\t\t\t_st.itemIconPosition + QPoint(0, deltaHeight / 2),\n\t\t\twidth());\n\t}\n\n\tp.setPen(selected ? _st.itemFgOver : _st.itemFg);\n\t_text1.drawLeftElided(\n\t\tp,\n\t\t_st.itemPadding.left(),\n\t\tst::ttlItemPadding.top(),\n\t\t_textWidth1,\n\t\twidth());\n\n\tp.setFont(st::ttlItemTimerFont);\n\tp.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);\n\tp.drawTextLeft(\n\t\t_st.itemPadding.left(),\n\t\tst::ttlItemPadding.top() + _st.itemStyle.font->height,\n\t\twidth(),\n\t\t_text2);\n}\n\nvoid TwoTextAction::prepare(const QString &text1) {\n\t_text1.setMarkedText(_st.itemStyle, { text1 }, MenuTextOptions);\n\tconst auto textWidth1 = _text1.maxWidth();\n\tconst auto textWidth2 = st::ttlItemTimerFont->width(_text2);\n\tconst auto &padding = _st.itemPadding;\n\n\tconst auto goodWidth = padding.left()\n\t\t+ std::max(textWidth1, textWidth2)\n\t\t+ padding.right();\n\tconst auto ttlMaxWidth = [&](const QString &duration) {\n\t\treturn padding.left()\n\t\t\t+ st::ttlItemTimerFont->width(tr::lng_context_auto_delete_in(\n\t\t\t\ttr::now,\n\t\t\t\tlt_duration,\n\t\t\t\tduration))\n\t\t\t+ padding.right();\n\t};\n\tconst auto maxWidth1 = ttlMaxWidth(\"23:59:59\");\n\tconst auto maxWidth2 = ttlMaxWidth(tr::lng_days(tr::now, lt_count, 7));\n\n\tconst auto w = std::clamp(\n\t\tstd::max({ goodWidth, maxWidth1, maxWidth2 }),\n\t\t_st.widthMin,\n\t\t_st.widthMax);\n\t_textWidth1 = w - (goodWidth - textWidth1);\n\t_textWidth2 = w - (goodWidth - textWidth2);\n\tsetMinWidth(w);\n\tupdate();\n}\n\nbool TwoTextAction::isEnabled() const {\n\treturn true;\n}\n\nnot_null<QAction*> TwoTextAction::action() const {\n\treturn _dummyAction;\n}\n\nQPoint TwoTextAction::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos());\n}\n\nQImage TwoTextAction::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::RectMask(size());\n}\n\nint TwoTextAction::contentHeight() const {\n\treturn _height;\n}\n\nvoid TwoTextAction::handleKeyPress(not_null<QKeyEvent*> e) {\n\tif (!isSelected()) {\n\t\treturn;\n\t}\n\tconst auto key = e->key();\n\tif (key == Qt::Key_Enter || key == Qt::Key_Return) {\n\t\tsetClicked(Ui::Menu::TriggeredSource::Keyboard);\n\t}\n}\n\n[[nodiscard]] base::unique_qptr<Ui::Menu::ItemBase> MakeTranslateToItem(\n\t\tnot_null<Ui::Menu::Menu*> menu,\n\t\tconst QString &language,\n\t\tFn<void()> callback) {\n\treturn base::make_unique_q<TwoTextAction>(\n\t\tmenu,\n\t\tmenu->st(),\n\t\ttr::lng_translate_menu_to(tr::now),\n\t\tlanguage,\n\t\tstd::move(callback),\n\t\t&st::menuIconTranslate,\n\t\t&st::menuIconTranslate);\n}\n\n} // namespace\n\nTranslateBar::TranslateBar(\n\tnot_null<QWidget*> parent,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<History*> history)\n: _controller(controller)\n, _history(history)\n, _wrap(parent, object_ptr<Ui::AbstractButton>(parent))\n, _shadow(std::make_unique<Ui::PlainShadow>(parent)) {\n\t_wrap.hide(anim::type::instant);\n\t_shadow->hide();\n\n\t_shadow->showOn(rpl::combine(\n\t\t_wrap.shownValue(),\n\t\t_wrap.heightValue(),\n\t\trpl::mappers::_1 && rpl::mappers::_2 > 0\n\t) | rpl::filter([=](bool shown) {\n\t\treturn (shown == _shadow->isHidden());\n\t}));\n\n\tsetup(history);\n}\n\nTranslateBar::~TranslateBar() = default;\n\nvoid TranslateBar::setShadowGeometryPostprocess(\n\t\tFn<QRect(QRect)> postprocess) {\n\t_shadowGeometryPostprocess = std::move(postprocess);\n\tupdateShadowGeometry(_wrap.geometry());\n}\n\nvoid TranslateBar::updateShadowGeometry(QRect wrapGeometry) {\n\tconst auto regular = QRect(\n\t\twrapGeometry.x(),\n\t\twrapGeometry.y() + wrapGeometry.height(),\n\t\twrapGeometry.width(),\n\t\tst::lineWidth);\n\t_shadow->setGeometry(_shadowGeometryPostprocess\n\t\t? _shadowGeometryPostprocess(regular)\n\t\t: regular);\n}\n\nvoid TranslateBar::setup(not_null<History*> history) {\n\t_wrap.geometryValue(\n\t) | rpl::on_next([=](QRect rect) {\n\t\tupdateShadowGeometry(rect);\n\t}, _wrap.lifetime());\n\n\tconst auto translateTo = [=](LanguageId id) {\n\t\thistory->translateTo(id);\n\t\tif (const auto migrated = history->migrateFrom()) {\n\t\t\tmigrated->translateTo(id);\n\t\t}\n\t};\n\tconst auto button = static_cast<Ui::AbstractButton*>(_wrap.entity());\n\tbutton->resize(0, st::historyTranslateBarHeight);\n\tbutton->setAttribute(Qt::WA_OpaquePaintEvent);\n\n\tbutton->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tQPainter(button).fillRect(clip, st::historyComposeButtonBg);\n\t}, button->lifetime());\n\n\tbutton->setClickedCallback([=] {\n\t\ttranslateTo(history->translatedTo() ? LanguageId() : _to.current());\n\t});\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton,\n\t\tst::historyTranslateLabel);\n\tconst auto icon = Ui::CreateChild<Ui::RpWidget>(button);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\ticon->setAttribute(Qt::WA_TransparentForMouseEvents);\n\ticon->resize(st::historyTranslateIcon.size());\n\ticon->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(icon);\n\t\tst::historyTranslateIcon.paint(p, 0, 0, icon->width());\n\t}, icon->lifetime());\n\tconst auto settings = Ui::CreateChild<Ui::IconButton>(\n\t\tbutton,\n\t\tst::historyTranslateSettings);\n\tsettings->setClickedCallback([=] { showMenu(createMenu(settings)); });\n\n\tconst auto updateLabelGeometry = [=] {\n\t\tconst auto full = _wrap.width() - icon->width();\n\t\tconst auto skip = st::semiboldFont->spacew * 2;\n\t\tconst auto natural = label->textMaxWidth();\n\t\tconst auto top = [&] {\n\t\t\treturn (_wrap.height() - label->height()) / 2;\n\t\t};\n\t\tif (natural <= full - 2 * (settings->width() + skip)) {\n\t\t\tlabel->resizeToWidth(natural);\n\t\t\tlabel->moveToRight((full - label->width()) / 2, top());\n\t\t} else {\n\t\t\tconst auto available = full - settings->width() - 2 * skip;\n\t\t\tlabel->resizeToWidth(std::min(natural, available));\n\t\t\tlabel->moveToRight(settings->width() + skip, top());\n\t\t}\n\t\ticon->move(\n\t\t\tlabel->x() - icon->width(),\n\t\t\t(_wrap.height() - icon->height()) / 2);\n\t};\n\n\t_wrap.sizeValue() | rpl::on_next([=](QSize size) {\n\t\tsettings->moveToRight(0, 0, size.width());\n\t\tupdateLabelGeometry();\n\t}, lifetime());\n\n\t_overridenTo = history->translatedTo();\n\t_to = rpl::combine(\n\t\tCore::App().settings().translateToValue(),\n\t\tCore::App().settings().skipTranslationLanguagesValue(),\n\t\thistory->session().changes().historyFlagsValue(\n\t\t\thistory,\n\t\t\tData::HistoryUpdate::Flag::TranslateFrom),\n\t\t_overridenTo.value()\n\t) | rpl::map([=](\n\t\t\tLanguageId to,\n\t\t\tconst std::vector<LanguageId> &skip,\n\t\t\tconst auto &,\n\t\t\tLanguageId overridenTo) {\n\t\treturn overridenTo\n\t\t\t? overridenTo\n\t\t\t: Ui::ChooseTranslateTo(history, to, skip);\n\t}) | rpl::distinct_until_changed();\n\n\t_to.value(\n\t) | rpl::filter([=](LanguageId should) {\n\t\tconst auto now = history->translatedTo();\n\t\treturn now && (now != should);\n\t}) | rpl::on_next([=](LanguageId should) {\n\t\ttranslateTo(should);\n\t}, _wrap.lifetime());\n\n\trpl::combine(\n\t\t_to.value(),\n\t\thistory->session().changes().historyFlagsValue(\n\t\t\thistory,\n\t\t\t(Data::HistoryUpdate::Flag::TranslatedTo\n\t\t\t\t| Data::HistoryUpdate::Flag::TranslateFrom)),\n\t\thistory->session().changes().peerFlagsValue(\n\t\t\thistory->peer,\n\t\t\tData::PeerUpdate::Flag::TranslationDisabled)\n\t) | rpl::map([=](\n\t\t\tLanguageId to,\n\t\t\tconst auto&,\n\t\t\tconst auto&) {\n\t\tusing Flag = PeerData::TranslationFlag;\n\t\tconst auto automatic = history->peer->autoTranslation();\n\t\treturn (history->peer->translationFlag() != Flag::Enabled)\n\t\t\t? rpl::single(QString())\n\t\t\t: history->translatedTo()\n\t\t\t? (automatic\n\t\t\t\t? tr::lng_translate_return_original(\n\t\t\t\t\tlt_language,\n\t\t\t\t\trpl::single(Ui::LanguageName(\n\t\t\t\t\t\thistory->translateOfferedFrom())))\n\t\t\t\t: tr::lng_translate_show_original())\n\t\t\t: history->translateOfferedFrom()\n\t\t\t? Ui::TranslateBarTo(to)\n\t\t\t: rpl::single(QString());\n\t}) | rpl::flatten_latest(\n\t) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](QString phrase) {\n\t\t_shouldBeShown = !phrase.isEmpty();\n\t\tif (_shouldBeShown) {\n\t\t\tlabel->setText(phrase);\n\t\t\tupdateLabelGeometry();\n\t\t}\n\t\tif (!_forceHidden) {\n\t\t\t_wrap.toggle(_shouldBeShown, anim::type::normal);\n\t\t}\n\t}, lifetime());\n}\n\nbase::unique_qptr<Ui::PopupMenu> TranslateBar::createMenu(\n\t\tnot_null<Ui::IconButton*> button) {\n\tif (_menu) {\n\t\treturn nullptr;\n\t}\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\t&_wrap,\n\t\tst::popupMenuExpandedSeparator);\n\tresult->setDestroyedCallback([\n\t\tthis,\n\t\tweak = base::make_weak(&_wrap),\n\t\tweakButton = base::make_weak(button),\n\t\tmenu = result.get()\n\t] {\n\t\tif (weak && _menu == menu) {\n\t\t\tif (weakButton) {\n\t\t\t\tweakButton->setForceRippled(false);\n\t\t\t}\n\t\t}\n\t});\n\tbutton->setForceRippled(true);\n\treturn result;\n}\n\nvoid TranslateBar::showMenu(base::unique_qptr<Ui::PopupMenu> menu) {\n\tif (!menu) {\n\t\treturn;\n\t}\n\t_menu = std::move(menu);\n\t_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);\n\n\tconst auto guard = base::make_weak(&_wrap);\n\tconst auto now = _history->translatedTo();\n\tconst auto to = now ? now : Ui::ChooseTranslateTo(_history);\n\tconst auto weak = base::make_weak(_controller);\n\tconst auto chooseCallback = [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->show(Ui::ChooseTranslateToBox(\n\t\t\t\tto,\n\t\t\t\tcrl::guard(guard, [=](LanguageId id) { _overridenTo = id; })\n\t\t\t));\n\t\t}\n\t};\n\t_menu->addAction(MakeTranslateToItem(\n\t\t_menu->menu(),\n\t\tUi::LanguageName(to ? to : Ui::ChooseTranslateTo(_history)),\n\t\tchooseCallback));\n\t_menu->addSeparator();\n\tconst auto history = _history;\n\tif (const auto translateOfferedFrom = _history->translateOfferedFrom()) {\n\t\tconst auto addToIgnoreList = [=] {\n\t\t\tshowSettingsToast(history->peer, translateOfferedFrom);\n\n\t\t\thistory->peer->saveTranslationDisabled(true);\n\n\t\t\tauto &settings = Core::App().settings();\n\t\t\tauto skip = settings.skipTranslationLanguages();\n\t\t\tif (!ranges::contains(skip, translateOfferedFrom)) {\n\t\t\t\tskip.push_back(translateOfferedFrom);\n\t\t\t}\n\t\t\tsettings.setSkipTranslationLanguages(std::move(skip));\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t};\n\t\t_menu->addAction(\n\t\t\tUi::TranslateMenuDont(tr::now, translateOfferedFrom),\n\t\t\taddToIgnoreList,\n\t\t\t&st::menuIconBlock);\n\t}\n\tconst auto hideBar = [=] {\n\t\tshowHiddenToast(history->peer);\n\n\t\thistory->peer->saveTranslationDisabled(true);\n\t};\n\t_menu->addAction(\n\t\ttr::lng_translate_menu_hide(tr::now),\n\t\thideBar,\n\t\t&st::menuIconCancel);\n\t_menu->addSeparator();\n\n\tconst auto cocoon = ChatHelpers::GenerateLocalTgsSticker(\n\t\t&_history->session(),\n\t\tu\"cocoon\"_q,\n\t\ttrue);\n\tauto item = base::make_unique_q<Ui::Menu::MultilineAction>(\n\t\t_menu->menu(),\n\t\tst::defaultMenu,\n\t\tst::historyTranslateCocoonLabel,\n\t\tQPoint(\n\t\t\tst::defaultMenu.itemPadding.left(),\n\t\t\tst::defaultMenu.itemPadding.top()),\n\t\ttr::lng_translate_cocoon_menu(\n\t\t\ttr::now,\n\t\t\tlt_emoji,\n\t\t\tData::SingleCustomEmoji(cocoon),\n\t\t\tlt_link,\n\t\t\ttr::link(tr::lng_translate_cocoon_link(tr::now, tr::bold)),\n\t\t\ttr::rich),\n\t\tCore::TextContext({\n\t\t\t.session = &_history->session(),\n\t\t\t.customEmojiLoopLimit = -1,\n\t\t}));\n\titem->clicks(\n\t) | rpl::on_next([controller = _controller] {\n\t\tcontroller->show(Box(Ui::AboutCocoonBox));\n\t}, item->lifetime());\n\t_menu->addAction(std::move(item));\n\n\t_menu->popup(_wrap.mapToGlobal(\n\t\tQPoint(_wrap.width(), 0) + st::historyTranslateMenuPosition));\n}\n\nvoid TranslateBar::showSettingsToast(\n\t\tnot_null<PeerData*> peer,\n\t\tLanguageId ignored) {\n\tconst auto weak = base::make_weak(_controller);\n\tconst auto text = tr::lng_translate_dont_added(\n\t\ttr::now,\n\t\tlt_name,\n\t\ttr::bold(Ui::LanguageName(ignored)),\n\t\ttr::marked);\n\tshowToast(text, tr::lng_translate_settings(tr::now), [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tconst auto box = strong->show(\n\t\t\t\tUi::EditSkipTranslationLanguages());\n\t\t\tif (box) {\n\t\t\t\tbox->boxClosing() | rpl::on_next([=] {\n\t\t\t\t\tconst auto in = ranges::contains(\n\t\t\t\t\t\tCore::App().settings().skipTranslationLanguages(),\n\t\t\t\t\t\tignored);\n\t\t\t\t\tif (!in && weak) {\n\t\t\t\t\t\tpeer->saveTranslationDisabled(false);\n\t\t\t\t\t}\n\t\t\t\t}, box->lifetime());\n\t\t\t}\n\t\t}\n\t});\n}\n\nvoid TranslateBar::showHiddenToast(not_null<PeerData*> peer) {\n\tconst auto &phrase = peer->isUser()\n\t\t? tr::lng_translate_hidden_user\n\t\t: peer->isBroadcast()\n\t\t? tr::lng_translate_hidden_channel\n\t\t: tr::lng_translate_hidden_group;\n\tconst auto proj = tr::marked;\n\tshowToast(phrase(tr::now, proj), tr::lng_translate_undo(tr::now), [=] {\n\t\tpeer->saveTranslationDisabled(false);\n\t});\n}\n\nvoid TranslateBar::showToast(\n\t\tTextWithEntities text,\n\t\tconst QString &buttonText,\n\t\tFn<void()> buttonCallback) {\n\tconst auto st = std::make_shared<style::Toast>(st::historyPremiumToast);\n\tst->padding.setRight(st::historyPremiumViewSet.style.font->width(buttonText)\n\t\t- st::historyPremiumViewSet.width);\n\n\tconst auto weak = Ui::Toast::Show(_wrap.window(), Ui::Toast::Config{\n\t\t.text = std::move(text),\n\t\t.st = st.get(),\n\t\t.attach = RectPart::Bottom,\n\t\t.acceptinput = true,\n\t\t.duration = kToastDuration,\n\t});\n\tconst auto strong = weak.get();\n\tif (!strong) {\n\t\treturn;\n\t}\n\tconst auto widget = strong->widget();\n\twidget->lifetime().add([st] {});\n\tconst auto hideToast = [weak] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->hideAnimated();\n\t\t}\n\t};\n\n\tconst auto clickableBackground = Ui::CreateChild<Ui::AbstractButton>(\n\t\twidget.get());\n\tclickableBackground->setPointerCursor(false);\n\tclickableBackground->setAcceptBoth();\n\tclickableBackground->show();\n\tclickableBackground->addClickHandler([=](Qt::MouseButton button) {\n\t\tif (button == Qt::RightButton) {\n\t\t\thideToast();\n\t\t}\n\t});\n\n\tconst auto button = Ui::CreateChild<Ui::RoundButton>(\n\t\twidget.get(),\n\t\trpl::single(buttonText),\n\t\tst::historyPremiumViewSet);\n\tbutton->show();\n\trpl::combine(\n\t\twidget->sizeValue(),\n\t\tbutton->sizeValue()\n\t) | rpl::on_next([=](QSize outer, QSize inner) {\n\t\tbutton->moveToRight(\n\t\t\t0,\n\t\t\t(outer.height() - inner.height()) / 2,\n\t\t\touter.width());\n\t\tclickableBackground->resize(outer);\n\t}, widget->lifetime());\n\n\tbutton->setClickedCallback([=] {\n\t\tbuttonCallback();\n\t\thideToast();\n\t});\n}\n\nvoid TranslateBar::show() {\n\tif (!_forceHidden) {\n\t\treturn;\n\t}\n\t_forceHidden = false;\n\tif (_shouldBeShown) {\n\t\t_wrap.show(anim::type::instant);\n\t} else if (!_wrap.isHidden() && !_wrap.animating()) {\n\t\t_wrap.hide(anim::type::instant);\n\t}\n}\n\nvoid TranslateBar::hide() {\n\tif (_forceHidden) {\n\t\treturn;\n\t}\n\t_forceHidden = true;\n\t_wrap.hide(anim::type::instant);\n}\n\nvoid TranslateBar::raise() {\n\t_wrap.raise();\n\t_shadow->raise();\n}\n\nvoid TranslateBar::finishAnimating() {\n\t_wrap.finishAnimating();\n}\n\nvoid TranslateBar::move(int x, int y) {\n\t_wrap.move(x, y);\n}\n\nvoid TranslateBar::resizeToWidth(int width) {\n\t_wrap.entity()->resizeToWidth(width);\n}\n\nint TranslateBar::height() const {\n\treturn !_forceHidden\n\t\t? _wrap.height()\n\t\t: _shouldBeShown\n\t\t? st::historyTranslateBarHeight\n\t\t: 0;\n}\n\nrpl::producer<int> TranslateBar::heightValue() const {\n\treturn _wrap.heightValue();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_translate_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/wrap/slide_wrap.h\"\n#include \"spellcheck/spellcheck_types.h\"\n\nclass History;\nstruct LanguageId;\n\nnamespace Ui {\nclass PlainShadow;\nclass PopupMenu;\nclass IconButton;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace HistoryView {\n\nclass TranslateBar final {\npublic:\n\tTranslateBar(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<History*> history);\n\t~TranslateBar();\n\n\tvoid show();\n\tvoid hide();\n\tvoid raise();\n\tvoid finishAnimating();\n\n\tvoid setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess);\n\n\tvoid move(int x, int y);\n\tvoid resizeToWidth(int width);\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] rpl::producer<int> heightValue() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _wrap.lifetime();\n\t}\n\nprivate:\n\tvoid setup(not_null<History*> history);\n\tvoid updateShadowGeometry(QRect wrapGeometry);\n\t[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createMenu(\n\t\tnot_null<Ui::IconButton*> button);\n\tvoid showMenu(base::unique_qptr<Ui::PopupMenu> menu);\n\n\tvoid showHiddenToast(not_null<PeerData*> peer);\n\tvoid showSettingsToast(not_null<PeerData*> peer, LanguageId ignored);\n\tvoid showToast(\n\t\tTextWithEntities text,\n\t\tconst QString &buttonText,\n\t\tFn<void()> buttonCallback);\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<History*> _history;\n\tUi::SlideWrap<> _wrap;\n\tstd::unique_ptr<Ui::PlainShadow> _shadow;\n\tFn<QRect(QRect)> _shadowGeometryPostprocess;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\trpl::variable<LanguageId> _overridenTo;\n\trpl::variable<LanguageId> _to;\n\tbool _shouldBeShown = false;\n\tbool _forceHidden = false;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_translate_tracker.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_transcribes.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_flags.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_peer_values.h\" // Data::AmPremiumValue.\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/view/history_view_element.h\"\n#include \"lang/translate_provider.h\"\n#include \"main/main_session.h\"\n#include \"spellcheck/platform/platform_language.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kEnoughForRecognition = 10;\nconstexpr auto kEnoughForTranslation = 6;\nconstexpr auto kMaxCheckInBunch = 100;\nconstexpr auto kRequestLengthLimit = 24 * 1024;\nconstexpr auto kRequestCountLimit = 20;\n\n} // namespace\n\nTranslateTracker::TranslateTracker(not_null<History*> history)\n: _history(history)\n, _provider(Ui::CreateTranslateProvider(&_history->session()))\n, _limit(kEnoughForRecognition) {\n\tsetup();\n}\n\nTranslateTracker::~TranslateTracker() {\n\tcancelToRequest();\n\tcancelSentRequest();\n}\n\nrpl::producer<bool> TranslateTracker::trackingLanguage() const {\n\treturn _trackingLanguage.value();\n}\n\nvoid TranslateTracker::setup() {\n\tconst auto peer = _history->peer;\n\tpeer->updateFull();\n\n\tconst auto channel = peer->asChannel();\n\tauto autoTranslationValue = (channel\n\t\t? (channel->flagsValue() | rpl::type_erased)\n\t\t: rpl::single(Data::Flags<ChannelDataFlags>::Change({}, {}))\n\t\t) | rpl::map([=](Data::Flags<ChannelDataFlags>::Change data) {\n\t\treturn (data.value & ChannelDataFlag::AutoTranslation);\n\t}) | rpl::distinct_until_changed();\n\n\tusing namespace rpl::mappers;\n\t_trackingLanguage = rpl::combine(\n\t\tCore::App().settings().translateChatEnabledValue(),\n\t\tData::AmPremiumValue(&_history->session()),\n\t\tstd::move(autoTranslationValue),\n\t\t_1 && (_2 || _3));\n\t_trackingLanguage.value() | rpl::on_next([=](bool tracking) {\n\t\t_trackingLifetime.destroy();\n\t\tif (tracking) {\n\t\t\trecognizeCollected();\n\t\t\ttrackSkipLanguages();\n\t\t\ttrackTranslationDisabled();\n\t\t} else {\n\t\t\tcheckRecognized({});\n\t\t\tstopAndRevert();\n\t\t}\n\t}, _lifetime);\n}\n\nbool TranslateTracker::enoughForRecognition() const {\n\treturn _itemsForRecognize.size() >= kEnoughForRecognition;\n}\n\nvoid TranslateTracker::startBunch() {\n\t_addedInBunch = 0;\n\t_bunchTranslatedTo = _history->translatedTo();\n\t++_generation;\n}\n\nbool TranslateTracker::add(not_null<Element*> view) {\n\tconst auto item = view->data();\n\tconst auto only = view->isOnlyEmojiAndSpaces();\n\tif (only != OnlyEmojiAndSpaces::Unknown) {\n\t\titem->cacheOnlyEmojiAndSpaces(only == OnlyEmojiAndSpaces::Yes);\n\t}\n\treturn add(item, false);\n}\n\nbool TranslateTracker::add(not_null<HistoryItem*> item) {\n\treturn add(item, false);\n}\n\nbool TranslateTracker::add(\n\t\tnot_null<HistoryItem*> item,\n\t\tbool skipDependencies) {\n\tExpects(_addedInBunch >= 0);\n\n\tif ((item->out() && !item->history()->peer->autoTranslation())\n\t\t|| item->isService()\n\t\t|| !item->isRegular()\n\t\t|| item->isOnlyEmojiAndSpaces()) {\n\t\treturn false;\n\t}\n\tif (item->translationShowRequiresCheck(_bunchTranslatedTo)) {\n\t\t_switchTranslations[item] = _bunchTranslatedTo;\n\t}\n\tif (!skipDependencies) {\n\t\tif (const auto reply = item->Get<HistoryMessageReply>()) {\n\t\t\tif (const auto to = reply->resolvedMessage.get()) {\n\t\t\t\tadd(to, true);\n\t\t\t}\n\t\t}\n#if 0 // I hope this is not needed, although I'm not sure.\n\t\tif (item->groupId()) {\n\t\t\tif (const auto group = _history->owner().groups().find(item)) {\n\t\t\t\tfor (const auto &other : group->items) {\n\t\t\t\t\tif (other != item) {\n\t\t\t\t\t\tadd(other, true);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n#endif\n\t}\n\tconst auto id = item->fullId();\n\tconst auto i = _itemsForRecognize.find(id);\n\tif (i != end(_itemsForRecognize)) {\n\t\ti->second.generation = _generation;\n\t\treturn true;\n\t}\n\tconst auto &text = item->originalText().text;\n\t_itemsForRecognize.emplace(id, ItemForRecognize{\n\t\t.generation = _generation,\n\t\t.id = (_trackingLanguage.current()\n\t\t\t? Platform::Language::Recognize(text)\n\t\t\t: MaybeLanguageId{ text }),\n\t});\n\t++_addedInBunch;\n\treturn true;\n}\n\nvoid TranslateTracker::switchTranslation(\n\t\tnot_null<HistoryItem*> item,\n\t\tLanguageId id) {\n\t_history->session().api().transcribes().checkSummaryToTranslate(\n\t\titem->fullId());\n\tif (item->translationShowRequiresRequest(id)) {\n\t\t_itemsToRequest.emplace(\n\t\t\titem->fullId(),\n\t\t\tItemToRequest{ int(item->originalText().text.size()) });\n\t}\n}\n\nvoid TranslateTracker::finishBunch() {\n\tif (_addedInBunch > 0) {\n\t\taccumulate_max(_limit, _addedInBunch + kEnoughForRecognition);\n\t\t_addedInBunch = -1;\n\t\tapplyLimit();\n\t\tif (_trackingLanguage.current()) {\n\t\t\tcheckRecognized();\n\t\t}\n\t}\n\tif (!_switchTranslations.empty()) {\n\t\tauto switching = base::take(_switchTranslations);\n\t\tfor (const auto &[item, id] : switching) {\n\t\t\tswitchTranslation(item, id);\n\t\t}\n\t\t_switchTranslations = std::move(switching);\n\t\t_switchTranslations.clear();\n\t}\n\trequestSome();\n}\n\nvoid TranslateTracker::addBunchFromBlocks() {\n\tif (enoughForRecognition()) {\n\t\treturn;\n\t}\n\tstartBunch();\n\tconst auto guard = gsl::finally([&] {\n\t\tfinishBunch();\n\t});\n\n\tauto check = kMaxCheckInBunch;\n\tfor (const auto &block : _history->blocks) {\n\t\tfor (const auto &view : block->messages) {\n\t\t\tif (!check-- || (add(view.get()) && enoughForRecognition())) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid TranslateTracker::addBunchFrom(\n\t\tconst std::vector<not_null<Element*>> &views) {\n\tif (enoughForRecognition()) {\n\t\treturn;\n\t}\n\tstartBunch();\n\tconst auto guard = gsl::finally([&] {\n\t\tfinishBunch();\n\t});\n\n\tauto check = kMaxCheckInBunch;\n\tfor (const auto &view : views) {\n\t\tif (!check-- || (add(view.get()) && enoughForRecognition())) {\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nvoid TranslateTracker::cancelToRequest() {\n\tif (!_itemsToRequest.empty()) {\n\t\tconst auto owner = &_history->owner();\n\t\tfor (const auto &[id, entry] : base::take(_itemsToRequest)) {\n\t\t\tif (const auto item = owner->message(id)) {\n\t\t\t\titem->translationShowRequiresRequest({});\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid TranslateTracker::cancelSentRequest() {\n\tif (_requestInProcess) {\n\t\tconst auto owner = &_history->owner();\n\t\tfor (const auto &id : base::take(_requested)) {\n\t\t\tif (const auto item = owner->message(id)) {\n\t\t\t\titem->translationShowRequiresRequest({});\n\t\t\t}\n\t\t}\n\t\t++_requestToken;\n\t\t_requestInProcess = false;\n\t}\n}\n\nvoid TranslateTracker::stopAndRevert() {\n\tcancelToRequest();\n\tcancelSentRequest();\n\tconst auto owner = &_history->owner();\n\tfor (const auto &[id, entry] : _itemsForRecognize) {\n\t\tif (const auto item = owner->message(id)) {\n\t\t\tif (item->translation()\n\t\t\t\t&& item->translationShowRequiresCheck({})) {\n\t\t\t\titem->translationShowRequiresRequest({});\n\t\t\t}\n\t\t}\n\t}\n\t_history->translateTo({});\n\tif (const auto migrated = _history->migrateFrom()) {\n\t\tmigrated->translateTo({});\n\t}\n}\n\nvoid TranslateTracker::requestSome() {\n\tif (_requestInProcess || _itemsToRequest.empty()) {\n\t\treturn;\n\t}\n\tconst auto to = _history->translatedTo();\n\tif (!to) {\n\t\tcancelToRequest();\n\t\treturn;\n\t}\n\t_requested.clear();\n\t_requested.reserve(_itemsToRequest.size());\n\tconst auto session = &_history->session();\n\tconst auto peerId = _itemsToRequest.back().first.peer;\n\tauto length = 0;\n\tfor (auto i = _itemsToRequest.end(); i != _itemsToRequest.begin();) {\n\t\tif ((--i)->first.peer != peerId) {\n\t\t\tbreak;\n\t\t}\n\t\tlength += i->second.length;\n\t\t_requested.push_back(i->first);\n\t\ti = _itemsToRequest.erase(i);\n\t\tif (_requested.size() >= kRequestCountLimit\n\t\t\t|| length >= kRequestLengthLimit) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (_requested.empty()) {\n\t\treturn;\n\t}\n\tconst auto owner = &session->data();\n\tauto requests = std::vector<Ui::TranslateProviderRequest>();\n\trequests.reserve(_requested.size());\n\tauto ids = std::vector<FullMsgId>();\n\tids.reserve(_requested.size());\n\tfor (const auto &id : _requested) {\n\t\tif (const auto item = owner->message(id)) {\n\t\t\trequests.push_back(Ui::PrepareTranslateProviderRequest(\n\t\t\t\t_provider.get(),\n\t\t\t\tsession->data().peer(id.peer),\n\t\t\t\tid.msg,\n\t\t\t\titem->originalText()));\n\t\t\tids.push_back(id);\n\t\t}\n\t}\n\t_requested = std::move(ids);\n\tif (_requested.empty()) {\n\t\trequestSome();\n\t\treturn;\n\t}\n\t_requestInProcess = true;\n\tconst auto requestToken = ++_requestToken;\n\t_provider->requestBatch(\n\t\tstd::move(requests),\n\t\tto,\n\t\t[=](int index, Ui::TranslateProviderResult result) {\n\t\t\tif (!_requestInProcess || (_requestToken != requestToken)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (index < 0 || index >= _requested.size()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto &id = _requested[index];\n\t\t\tif (const auto item = owner->message(id)) {\n\t\t\t\titem->translationDone(\n\t\t\t\t\tto,\n\t\t\t\t\tresult.text.value_or(TextWithEntities()));\n\t\t\t}\n\t\t},\n\t\t[=] {\n\t\t\tif (!_requestInProcess || (_requestToken != requestToken)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_requestInProcess = false;\n\t\t\t_requested.clear();\n\t\t\trequestSome();\n\t\t});\n}\n\nvoid TranslateTracker::applyLimit() {\n\tconst auto generationProjection = [](const auto &pair) {\n\t\treturn pair.second.generation;\n\t};\n\tconst auto owner = &_history->owner();\n\n\t// Erase starting with oldest generation till items count is not too big.\n\twhile (_itemsForRecognize.size() > _limit) {\n\t\tconst auto oldest = ranges::min_element(\n\t\t\t_itemsForRecognize,\n\t\t\tranges::less(),\n\t\t\tgenerationProjection\n\t\t)->second.generation;\n\t\tfor (auto i = begin(_itemsForRecognize)\n\t\t\t; i != end(_itemsForRecognize);) {\n\t\t\tif (i->second.generation == oldest) {\n\t\t\t\tif (const auto j = _itemsToRequest.find(i->first)\n\t\t\t\t\t; j != end(_itemsToRequest)) {\n\t\t\t\t\tif (const auto item = owner->message(i->first)) {\n\t\t\t\t\t\titem->translationShowRequiresRequest({});\n\t\t\t\t\t}\n\t\t\t\t\t_itemsToRequest.erase(j);\n\t\t\t\t}\n\t\t\t\ti = _itemsForRecognize.erase(i);\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid TranslateTracker::recognizeCollected() {\n\tfor (auto &[id, entry] : _itemsForRecognize) {\n\t\tif (const auto text = std::get_if<QString>(&entry.id)) {\n\t\t\tentry.id = Platform::Language::Recognize(*text);\n\t\t}\n\t}\n}\n\nvoid TranslateTracker::trackSkipLanguages() {\n\tCore::App().settings().skipTranslationLanguagesValue(\n\t) | rpl::on_next([=](const std::vector<LanguageId> &skip) {\n\t\tconst auto wasOfferedFrom = _history->translateOfferedFrom();\n\t\tconst auto wasTranslatedTo = _history->translatedTo();\n\t\tcheckRecognized(skip);\n\t\tif (wasTranslatedTo\n\t\t\t&& wasOfferedFrom\n\t\t\t&& !_history->translateOfferedFrom()) {\n\t\t\tstopAndRevert();\n\t\t}\n\t}, _trackingLifetime);\n}\n\nvoid TranslateTracker::trackTranslationDisabled() {\n\tusing PeerFlag = Data::PeerUpdate::Flag;\n\t_history->session().changes().peerFlagsValue(\n\t\t_history->peer,\n\t\tPeerFlag::TranslationDisabled\n\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\tusing TranslationFlag = PeerData::TranslationFlag;\n\t\tif (_history->peer->translationFlag() == TranslationFlag::Disabled\n\t\t\t&& _history->translatedTo()) {\n\t\t\tstopAndRevert();\n\t\t}\n\t}, _trackingLifetime);\n}\n\nvoid TranslateTracker::checkRecognized() {\n\tcheckRecognized(Core::App().settings().skipTranslationLanguages());\n}\n\nvoid TranslateTracker::checkRecognized(const std::vector<LanguageId> &skip) {\n\tif (!_trackingLanguage.current()) {\n\t\t_history->translateOfferFrom({});\n\t\treturn;\n\t}\n\tauto languages = base::flat_map<LanguageId, int>();\n\tfor (const auto &[id, entry] : _itemsForRecognize) {\n\t\tif (const auto id = std::get_if<LanguageId>(&entry.id)) {\n\t\t\tif (*id && !ranges::contains(skip, *id)) {\n\t\t\t\t++languages[*id];\n\t\t\t}\n\t\t}\n\t}\n\tusing namespace base;\n\tconst auto count = int(_itemsForRecognize.size());\n\tconstexpr auto p = &flat_multi_map_pair_type<LanguageId, int>::second;\n\tconst auto threshold = (count > kEnoughForRecognition)\n\t\t? (count * kEnoughForTranslation / kEnoughForRecognition)\n\t\t: _allLoaded\n\t\t? std::min(count, kEnoughForTranslation)\n\t\t: kEnoughForTranslation;\n\tconst auto translatable = ranges::accumulate(\n\t\tlanguages,\n\t\t0,\n\t\tranges::plus(),\n\t\tp);\n\tif (count < kEnoughForTranslation) {\n\t\t// Don't change offer by small amount of messages.\n\t} else if (translatable >= threshold) {\n\t\t_history->translateOfferFrom(\n\t\t\tranges::max_element(languages, ranges::less(), p)->first);\n\t} else {\n\t\t_history->translateOfferFrom({});\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_translate_tracker.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"spellcheck/spellcheck_types.h\"\n\nclass History;\nclass HistoryItem;\nnamespace Ui {\nclass TranslateProvider;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass Element;\n\nclass TranslateTracker final {\npublic:\n\texplicit TranslateTracker(not_null<History*> history);\n\t~TranslateTracker();\n\n\t[[nodiscard]] bool enoughForRecognition() const;\n\tvoid startBunch();\n\tbool add(not_null<Element*> view);\n\tbool add(not_null<HistoryItem*> item);\n\tvoid finishBunch();\n\n\tvoid addBunchFromBlocks();\n\tvoid addBunchFrom(const std::vector<not_null<Element*>> &views);\n\n\t[[nodiscard]] rpl::producer<bool> trackingLanguage() const;\n\nprivate:\n\tusing MaybeLanguageId = std::variant<QString, LanguageId>;\n\tstruct ItemForRecognize {\n\t\tuint64 generation = 0;\n\t\tMaybeLanguageId id;\n\t};\n\tstruct ItemToRequest {\n\t\tint length = 0;\n\t};\n\n\tvoid setup();\n\tbool add(not_null<HistoryItem*> item, bool skipDependencies);\n\tvoid recognizeCollected();\n\tvoid trackSkipLanguages();\n\tvoid trackTranslationDisabled();\n\tvoid checkRecognized();\n\tvoid checkRecognized(const std::vector<LanguageId> &skip);\n\tvoid applyLimit();\n\tvoid requestSome();\n\tvoid cancelToRequest();\n\tvoid cancelSentRequest();\n\tvoid stopAndRevert();\n\tvoid switchTranslation(not_null<HistoryItem*> item, LanguageId id);\n\n\tconst not_null<History*> _history;\n\tconst std::unique_ptr<Ui::TranslateProvider> _provider;\n\trpl::variable<bool> _trackingLanguage = false;\n\tbase::flat_map<FullMsgId, ItemForRecognize> _itemsForRecognize;\n\tuint64 _generation = 0;\n\tLanguageId _bunchTranslatedTo;\n\tint _limit = 0;\n\tint _addedInBunch = -1;\n\tbool _allLoaded = false;\n\n\tbase::flat_map<not_null<HistoryItem*>, LanguageId> _switchTranslations;\n\tbase::flat_map<FullMsgId, ItemToRequest> _itemsToRequest;\n\tstd::vector<FullMsgId> _requested;\n\tuint64 _requestToken = 0;\n\tbool _requestInProcess = false;\n\n\trpl::lifetime _trackingLifetime;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace HistoryView\n\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_view_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_view_button.h\"\n\n#include \"boxes/gift_premium_box.h\"\n#include \"core/click_handler_types.h\"\n#include \"history/history.h\"\n#include \"history/history_item_components.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"iv/iv_data.h\"\n#include \"iv/iv_controller.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n\n#include \"core/application.h\"\n#include \"iv/iv_instance.h\"\n\nnamespace HistoryView {\nnamespace {\n\n[[nodiscard]] ClickHandlerPtr MakeMediaButtonClickHandler(\n\t\tnot_null<Data::Media*> media) {\n\tconst auto start = media->giveawayStart();\n\tconst auto results = media->giveawayResults();\n\tAssert(start || results);\n\n\tconst auto peer = media->parent()->history()->peer;\n\tconst auto messageId = media->parent()->id;\n\tif (media->parent()->isSending() || media->parent()->hasFailed()) {\n\t\treturn nullptr;\n\t}\n\tconst auto maybeStart = start\n\t\t? *start\n\t\t: std::optional<Data::GiveawayStart>();\n\tconst auto maybeResults = results\n\t\t? *results\n\t\t: std::optional<Data::GiveawayResults>();\n\treturn std::make_shared<LambdaClickHandler>([=](\n\t\t\tClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tconst auto controller = my.sessionWindow.get();\n\t\tif (!controller) {\n\t\t\treturn;\n\t\t}\n\t\tResolveGiveawayInfo(\n\t\t\tcontroller,\n\t\t\tpeer,\n\t\t\tmessageId,\n\t\t\tmaybeStart,\n\t\t\tmaybeResults);\n\t});\n}\n\n[[nodiscard]] QString MakeMediaButtonText(not_null<Data::Media*> media) {\n\tExpects(media->giveawayStart() || media->giveawayResults());\n\n\treturn tr::lng_prizes_how_works(tr::now, tr::upper);\n}\n\n} // namespace\n\nstruct ViewButton::Inner {\n\tInner(\n\t\tnot_null<Data::Media*> media,\n\t\tuint8 colorIndex,\n\t\tFn<void()> updateCallback);\n\n\tvoid updateMask(int height);\n\tvoid toggleRipple(bool pressed);\n\n\tconst style::margins &margins;\n\tconst ClickHandlerPtr link;\n\tconst Fn<void()> updateCallback;\n\tuint32 lastWidth : 24 = 0;\n\tuint32 colorIndex : 6 = 0;\n\tuint32 aboveInfo : 1 = 0;\n\tuint32 externalLink : 1 = 0;\n\tQPoint lastPoint;\n\tstd::unique_ptr<Ui::RippleAnimation> ripple;\n\tUi::Text::String text;\n};\n\nbool ViewButton::MediaHasViewButton(not_null<Data::Media*> media) {\n\treturn media->giveawayStart() || media->giveawayResults();\n}\n\nViewButton::Inner::Inner(\n\tnot_null<Data::Media*> media,\n\tuint8 colorIndex,\n\tFn<void()> updateCallback)\n: margins(st::historyViewButtonMargins)\n, link(MakeMediaButtonClickHandler(media))\n, updateCallback(std::move(updateCallback))\n, colorIndex(colorIndex)\n, aboveInfo(1)\n, text(st::historyViewButtonTextStyle, MakeMediaButtonText(media)) {\n}\n\nvoid ViewButton::Inner::updateMask(int height) {\n\tripple = std::make_unique<Ui::RippleAnimation>(\n\t\tst::defaultRippleAnimation,\n\t\tUi::RippleAnimation::RoundRectMask(\n\t\t\tQSize(lastWidth, height - margins.top() - margins.bottom()),\n\t\t\tst::roundRadiusLarge),\n\t\tupdateCallback);\n}\n\nvoid ViewButton::Inner::toggleRipple(bool pressed) {\n\tif (ripple) {\n\t\tif (pressed) {\n\t\t\tripple->add(lastPoint);\n\t\t} else {\n\t\t\tripple->lastStop();\n\t\t}\n\t}\n}\n\nViewButton::ViewButton(\n\tnot_null<Data::Media*> media,\n\tuint8 colorIndex,\n\tFn<void()> updateCallback)\n: _inner(std::make_unique<Inner>(\n\tmedia,\n\tcolorIndex,\n\tstd::move(updateCallback))) {\n}\n\nViewButton::~ViewButton() {\n}\n\nvoid ViewButton::resized() const {\n\t_inner->updateMask(height());\n}\n\nint ViewButton::height() const {\n\treturn st::historyViewButtonHeight;\n}\n\nbool ViewButton::belowMessageInfo() const {\n\treturn !_inner->aboveInfo;\n}\n\nvoid ViewButton::draw(\n\t\tPainter &p,\n\t\tconst QRect &r,\n\t\tconst Ui::ChatPaintContext &context) {\n\tconst auto st = context.st;\n\tconst auto stm = context.messageStyle();\n\tconst auto selected = context.selected();\n\tconst auto cache = context.outbg\n\t\t? stm->replyCache[st->colorPatternIndex(_inner->colorIndex)].get()\n\t\t: st->coloredReplyCache(selected, _inner->colorIndex).get();\n\tconst auto radius = st::historyPagePreview.radius;\n\n\tif (_inner->ripple && !_inner->ripple->empty()) {\n\t\t_inner->ripple->paint(p, r.left(), r.top(), r.width(), &cache->bg);\n\t}\n\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(cache->bg);\n\tp.drawRoundedRect(r, radius, radius);\n\n\tp.setPen(cache->icon);\n\t_inner->text.drawElided(\n\t\tp,\n\t\tr.left(),\n\t\tr.top() + (r.height() - _inner->text.minHeight()) / 2,\n\t\tr.width(),\n\t\t1,\n\t\tstyle::al_top);\n\n\tif (_inner->externalLink) {\n\t\tconst auto &icon = st::msgBotKbUrlIcon;\n\t\tconst auto padding = st::msgBotKbIconPadding;\n\t\ticon.paint(\n\t\t\tp,\n\t\t\tr.left() + r.width() - icon.width() - padding,\n\t\t\tr.top() + padding,\n\t\t\tr.width(),\n\t\t\tcache->icon);\n\t}\n\tif (_inner->lastWidth != r.width()) {\n\t\t_inner->lastWidth = r.width();\n\t\tresized();\n\t}\n}\n\nconst ClickHandlerPtr &ViewButton::link() const {\n\treturn _inner->link;\n}\n\nbool ViewButton::checkLink(const ClickHandlerPtr &other, bool pressed) {\n\tif (_inner->link != other) {\n\t\treturn false;\n\t}\n\t_inner->toggleRipple(pressed);\n\treturn true;\n}\n\nbool ViewButton::getState(\n\t\tQPoint point,\n\t\tconst QRect &g,\n\t\tnot_null<TextState*> outResult) const {\n\tif (!g.contains(point)) {\n\t\treturn false;\n\t}\n\toutResult->link = _inner->link;\n\t_inner->lastPoint = point - g.topLeft();\n\treturn true;\n}\n\nQRect ViewButton::countRect(const QRect &r) const {\n\treturn QRect(\n\t\tr.left(),\n\t\tr.top() + r.height() - height(),\n\t\tr.width(),\n\t\theight()) - _inner->margins;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_view_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/chat/chat_style.h\"\n\nnamespace Data {\nclass Media;\n} // namespace Data\n\nstruct WebPageData;\n\nnamespace HistoryView {\n\nstruct TextState;\n\nclass ViewButton {\npublic:\n\tViewButton(\n\t\tnot_null<Data::Media*> media,\n\t\tuint8 colorIndex,\n\t\tFn<void()> updateCallback);\n\t~ViewButton();\n\n\t[[nodiscard]] static bool MediaHasViewButton(\n\t\tnot_null<Data::Media*> media);\n\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] bool belowMessageInfo() const;\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tconst QRect &r,\n\t\tconst Ui::ChatPaintContext &context);\n\n\t[[nodiscard]] const ClickHandlerPtr &link() const;\n\tbool checkLink(const ClickHandlerPtr &other, bool pressed);\n\n\t[[nodiscard]] QRect countRect(const QRect &r) const;\n\n\t[[nodiscard]] bool getState(\n\t\tQPoint point,\n\t\tconst QRect &g,\n\t\tnot_null<TextState*> outResult) const;\n\nprivate:\n\tvoid resized() const;\n\n\tstruct Inner;\n\tconst std::unique_ptr<Inner> _inner;\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_webpage_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/history_view_webpage_preview.h\"\n\n#include \"data/data_file_origin.h\"\n#include \"data/data_web_page.h\"\n\nnamespace HistoryView {\n\nWebPageText TitleAndDescriptionFromWebPage(not_null<WebPageData*> d) {\n\tQString resultTitle, resultDescription;\n\tconst auto document = d->document;\n\tconst auto author = d->author;\n\tconst auto siteName = d->siteName;\n\tconst auto title = d->title;\n\tconst auto description = d->description;\n\tconst auto filenameOrUrl = [&] {\n\t\treturn ((document && !document->filename().isEmpty())\n\t\t\t? document->filename()\n\t\t\t: d->url);\n\t};\n\tconst auto authorOrFilename = [&] {\n\t\treturn (author.isEmpty()\n\t\t\t? filenameOrUrl()\n\t\t\t: author);\n\t};\n\tconst auto descriptionOrAuthor = [&] {\n\t\treturn (description.text.isEmpty()\n\t\t\t? authorOrFilename()\n\t\t\t: description.text);\n\t};\n\tif (siteName.isEmpty()) {\n\t\tif (title.isEmpty()) {\n\t\t\tif (description.text.isEmpty()) {\n\t\t\t\tresultTitle = author;\n\t\t\t\tresultDescription = filenameOrUrl();\n\t\t\t} else {\n\t\t\t\tresultTitle = description.text;\n\t\t\t\tresultDescription = authorOrFilename();\n\t\t\t}\n\t\t} else {\n\t\t\tresultTitle = title;\n\t\t\tresultDescription = descriptionOrAuthor();\n\t\t}\n\t} else {\n\t\tresultTitle = siteName;\n\t\tresultDescription = title.isEmpty()\n\t\t\t? descriptionOrAuthor()\n\t\t\t: title;\n\t}\n\treturn { resultTitle, resultDescription };\n}\n\nbool DrawWebPageDataPreview(\n\t\tQPainter &p,\n\t\tnot_null<WebPageData*> webpage,\n\t\tnot_null<PeerData*> context,\n\t\tQRect to) {\n\tconst auto document = webpage->document;\n\tconst auto photo = webpage->photo;\n\tif ((!photo || photo->isNull())\n\t\t&& (!document\n\t\t\t|| !document->hasThumbnail()\n\t\t\t|| document->isPatternWallPaper())) {\n\t\treturn false;\n\t}\n\n\tconst auto preview = photo\n\t\t? photo->getReplyPreview(Data::FileOrigin(), context, false)\n\t\t: document->getReplyPreview(Data::FileOrigin(), context, false);\n\tif (preview) {\n\t\tconst auto w = preview->width();\n\t\tconst auto h = preview->height();\n\t\tif (w == h) {\n\t\t\tp.drawPixmap(to.x(), to.y(), preview->pix());\n\t\t} else {\n\t\t\tconst auto from = (w > h)\n\t\t\t\t? QRect((w - h) / 2, 0, h, h)\n\t\t\t\t: QRect(0, (h - w) / 2, w, w);\n\t\t\tp.drawPixmap(to, preview->pix(), from);\n\t\t}\n\t}\n\treturn true;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/history_view_webpage_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace HistoryView {\n\nstruct WebPageText {\n\tQString title;\n\tQString description;\n};\n\nWebPageText TitleAndDescriptionFromWebPage(not_null<WebPageData*> d);\nbool DrawWebPageDataPreview(\n\tQPainter &p,\n\tnot_null<WebPageData*> webpage,\n\tnot_null<PeerData*> context,\n\tQRect to);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_birthday_suggestion.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_birthday_suggestion.h\"\n\n#include \"boxes/star_gift_box.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_birthday.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_session.h\"\n#include \"data/data_star_gift.h\"\n#include \"history/view/media/history_view_media_generic.h\"\n#include \"history/view/media/history_view_premium_gift.h\"\n#include \"history/view/media/history_view_unique_gift.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"info/peer_gifts/info_peer_gifts_common.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/premium_stars_colored.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/rect.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n\nnamespace HistoryView {\n\n[[nodiscard]] auto GenerateSuggetsBirthdayMedia(\n\tnot_null<Element*> parent,\n\tElement *replacing,\n\tData::Birthday birthday)\n-> Fn<void(\n\t\tnot_null<MediaGeneric*>,\n\t\tFn<void(std::unique_ptr<MediaGenericPart>)>)> {\n\treturn [=](\n\t\t\tnot_null<MediaGeneric*> media,\n\t\t\tFn<void(std::unique_ptr<MediaGenericPart>)> push) {\n\t\tconst auto session = &media->parent()->history()->session();\n\t\tconst auto document = ChatHelpers::GenerateLocalTgsSticker(\n\t\t\tsession,\n\t\t\tu\"cake\"_q);\n\t\tconst auto sticker = [=] {\n\t\t\tusing Tag = ChatHelpers::StickerLottieSize;\n\t\t\treturn StickerInBubblePart::Data{\n\t\t\t\t.sticker = document,\n\t\t\t\t.size = st::birthdaySuggestStickerSize,\n\t\t\t\t.cacheTag = Tag::ChatIntroHelloSticker,\n\t\t\t\t.stopOnLastFrame = true,\n\t\t\t};\n\t\t};\n\t\tpush(std::make_unique<StickerInBubblePart>(\n\t\t\tparent,\n\t\t\treplacing,\n\t\t\tsticker,\n\t\t\tst::birthdaySuggestStickerPadding));\n\n\t\tconst auto from = media->parent()->data()->from();\n\t\tconst auto isSelf = (from->id == from->session().userPeerId());\n\t\tconst auto peer = isSelf ? media->parent()->history()->peer : from;\n\t\tpush(std::make_unique<MediaGenericTextPart>(\n\t\t\t(isSelf\n\t\t\t\t? tr::lng_action_suggested_birthday_me\n\t\t\t\t: tr::lng_action_suggested_birthday)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tTextWithEntities{ peer->shortName() },\n\t\t\t\t\ttr::marked),\n\t\t\tst::birthdaySuggestTextPadding));\n\n\t\tpush(std::make_unique<BirthdayTable>(\n\t\t\tbirthday,\n\t\t\t(isSelf\n\t\t\t\t? st::birthdaySuggestTableLastPadding\n\t\t\t\t: st::birthdaySuggestTablePadding)));\n\t\tif (!isSelf) {\n\t\t\tauto link = std::make_shared<LambdaClickHandler>([=](\n\t\t\t\t\tClickContext context) {\n\t\t\t\tCore::App().openInternalUrl(\n\t\t\t\t\t(u\"internal:edit_birthday:suggestion_\"_q\n\t\t\t\t\t\t+ QString::number(birthday.serialize())),\n\t\t\t\t\tcontext.other);\n\t\t\t});\n\t\t\tpush(MakeGenericButtonPart(\n\t\t\t\ttr::lng_sticker_premium_view(tr::now),\n\t\t\t\tst::chatUniqueButtonPadding,\n\t\t\t\t[=] { parent->repaint(); },\n\t\t\t\tstd::move(link)));\n\t\t}\n\t};\n}\n\nBirthdayTable::BirthdayTable(Data::Birthday birthday, QMargins margins)\n: _margins(margins) {\n\tconst auto push = [&](QString label, QString value) {\n\t\t_parts.push_back({\n\t\t\t.label = Ui::Text::String(st::defaultTextStyle, label),\n\t\t\t.value = Ui::Text::String(\n\t\t\t\tst::defaultTextStyle,\n\t\t\t\ttr::bold(value)),\n\t\t});\n\t};\n\tpush(tr::lng_date_input_day(tr::now), QString::number(birthday.day()));\n\tpush(\n\t\ttr::lng_date_input_month(tr::now),\n\t\tLang::Month(birthday.month())(tr::now));\n\tif (const auto year = birthday.year()) {\n\t\tpush(tr::lng_date_input_year(tr::now), QString::number(year));\n\t}\n}\n\nvoid BirthdayTable::draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const {\n\tconst auto top = _margins.top();\n\tconst auto palette = &context.st->serviceTextPalette();\n\tconst auto paint = [&](\n\t\t\tconst Ui::Text::String &text,\n\t\t\tint left,\n\t\t\tint yskip = 0) {\n\t\ttext.draw(p, {\n\t\t\t.position = { left, top + yskip},\n\t\t\t.outerWidth = outerWidth,\n\t\t\t.availableWidth = text.maxWidth(),\n\t\t\t.palette = palette,\n\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t.now = context.now,\n\t\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t\t.elisionLines = 1,\n\t\t});\n\t};\n\n\tp.setPen(context.st->msgServiceFg()->c);\n\tfor (const auto &part : _parts) {\n\t\tp.setOpacity(0.7);\n\t\tpaint(part.label, part.labelLeft);\n\n\t\tp.setOpacity(1.);\n\t\tpaint(\n\t\t\tpart.value,\n\t\t\tpart.valueLeft,\n\t\t\tst::normalFont->height + st::birthdaySuggestTableSkip);\n\t}\n}\n\nTextState BirthdayTable::textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const {\n\treturn {};\n}\n\nQSize BirthdayTable::countOptimalSize() {\n\tauto width = 0;\n\tfor (const auto &part : _parts) {\n\t\twidth += std::max(part.label.maxWidth(), part.value.maxWidth());\n\t}\n\twidth += st::normalFont->spacew * (_parts.size() - 1);\n\n\tconst auto height = st::normalFont->height * 2\n\t\t+ st::birthdaySuggestTableSkip;\n\treturn {\n\t\t_margins.left() + width + _margins.right(),\n\t\t_margins.top() + height + _margins.bottom(),\n\t};\n}\n\nQSize BirthdayTable::countCurrentSize(int newWidth) {\n\tauto available = newWidth - _margins.left() - _margins.right();\n\tfor (const auto &part : _parts) {\n\t\tavailable -= std::max(part.label.maxWidth(), part.value.maxWidth());\n\t}\n\tconst auto skip = available / int(_parts.size() + 1);\n\tauto left = _margins.left() + skip;\n\tfor (auto &part : _parts) {\n\t\tauto full = std::max(part.label.maxWidth(), part.value.maxWidth());\n\t\tpart.labelLeft = left + (full - part.label.maxWidth()) / 2;\n\t\tpart.valueLeft = left + (full - part.value.maxWidth()) / 2;\n\t\tleft += full + skip;\n\t}\n\treturn { newWidth, minHeight() };\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_birthday_suggestion.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media_generic.h\"\n\nclass Painter;\n\nnamespace Data {\nclass MediaGiftBox;\nstruct UniqueGift;\nclass Birthday;\n} // namespace Data\n\nnamespace Ui {\nstruct ChatPaintContext;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass Element;\nclass MediaGeneric;\nclass MediaGenericPart;\n\n[[nodiscard]] auto GenerateSuggetsBirthdayMedia(\n\tnot_null<Element*> parent,\n\tElement *replacing,\n\tData::Birthday birthday)\n-> Fn<void(\n\tnot_null<MediaGeneric*>,\n\tFn<void(std::unique_ptr<MediaGenericPart>)>)>;\n\nclass BirthdayTable final : public MediaGenericPart {\npublic:\n\tBirthdayTable(Data::Birthday birthday, QMargins margins);\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const override;\n\tTextState textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const override;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\nprivate:\n\tstruct Part {\n\t\tUi::Text::String label;\n\t\tUi::Text::String value;\n\t\tint labelLeft = 0;\n\t\tint valueLeft = 0;\n\t};\n\n\tstd::vector<Part> _parts;\n\tQMargins _margins;\n\tFn<QColor(const PaintContext &)> _labelColor;\n\tFn<QColor(const PaintContext &)> _valueColor;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_call.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_call.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/painter.h\"\n#include \"layout/layout_selection.h\" // FullSelection\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"calls/calls_instance.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_user.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nusing State = Data::CallState;\n\n[[nodiscard]] int ComputeDuration(State state, int duration) {\n\treturn (state != State::Missed && state != State::Busy)\n\t\t? duration\n\t\t: 0;\n}\n\n} // namespace\n\nCall::Call(\n\tnot_null<Element*> parent,\n\tnot_null<Data::Call*> call)\n: Media(parent)\n, _duration(ComputeDuration(call->state, call->duration))\n, _state(call->state)\n, _conference(call->conferenceId != 0)\n, _video(call->video) {\n\tconst auto item = parent->data();\n\t_text = Data::MediaCall::Text(item, _state, _conference, _video);\n\t_status = QLocale().toString(\n\t\tparent->dateTime().time(),\n\t\tQLocale::ShortFormat);\n\tif (_duration) {\n\t\t_status = tr::lng_call_duration_info(\n\t\t\ttr::now,\n\t\t\tlt_time,\n\t\t\t_status,\n\t\t\tlt_duration,\n\t\t\tUi::FormatDurationWords(_duration));\n\t}\n}\n\nQSize Call::countOptimalSize() {\n\tconst auto user = _parent->history()->peer->asUser();\n\tconst auto conference = _conference;\n\tconst auto video = _video;\n\tconst auto contextId = _parent->data()->fullId();\n\tconst auto id = _parent->data()->id;\n\t_link = std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tif (conference) {\n\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\tconst auto weak = my.sessionWindow;\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tQSize();\n\t\t\t\tstrong->resolveConferenceCall(id, contextId);\n\t\t\t}\n\t\t} else if (user) {\n\t\t\tCore::App().calls().startOutgoingCall(user, { video });\n\t\t}\n\t});\n\tauto maxWidth = st::historyCallWidth;\n\tauto minHeight = st::historyCallHeight;\n\tif (!isBubbleTop()) {\n\t\tminHeight -= st::msgFileTopMinus;\n\t}\n\treturn { maxWidth, minHeight };\n}\n\nvoid Call::draw(Painter &p, const PaintContext &context) const {\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;\n\tauto paintw = width();\n\n\tconst auto stm = context.messageStyle();\n\n\taccumulate_min(paintw, maxWidth());\n\n\tauto nameleft = 0, nametop = 0, statustop = 0;\n\tauto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus;\n\n\tnameleft = st::historyCallLeft;\n\tnametop = st::historyCallTop - topMinus;\n\tstatustop = st::historyCallStatusTop - topMinus;\n\n\tp.setFont(st::semiboldFont);\n\tp.setPen(stm->historyFileNameFg);\n\tp.drawTextLeft(nameleft, nametop, paintw, _text);\n\n\tauto statusleft = nameleft;\n\tauto missed = (_state == State::Missed) || (_state == State::Busy);\n\tconst auto &arrow = missed\n\t\t? stm->historyCallArrowMissed\n\t\t: stm->historyCallArrow;\n\tarrow.paint(p, statusleft + st::historyCallArrowPosition.x(), statustop + st::historyCallArrowPosition.y(), paintw);\n\tstatusleft += arrow.width() + st::historyCallStatusSkip;\n\n\tp.setFont(st::normalFont);\n\tp.setPen(stm->mediaFg);\n\tp.drawTextLeft(statusleft, statustop, paintw, _status);\n\n\tconst auto &icon = _video\n\t\t? stm->historyCallCameraIcon\n\t\t: _conference\n\t\t? stm->historyCallGroupIcon\n\t\t: stm->historyCallIcon;\n\ticon.paint(p, paintw - st::historyCallIconPosition.x() - icon.width(), st::historyCallIconPosition.y() - topMinus, paintw);\n}\n\nTextState Call::textState(QPoint point, StateRequest request) const {\n\tauto result = TextState(_parent);\n\tif (QRect(0, 0, width(), height()).contains(point)) {\n\t\tresult.link = _link;\n\t\treturn result;\n\t}\n\treturn result;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_call.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n\nnamespace Data {\nenum class CallState : char;\nstruct Call;\n} // namespace Data\n\nnamespace HistoryView {\n\nclass Call : public Media {\npublic:\n\tCall(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<Data::Call*> call);\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\n\tbool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {\n\t\treturn true;\n\t}\n\tbool dragItemByHandler(const ClickHandlerPtr &p) const override {\n\t\treturn false;\n\t}\n\n\tbool needsBubble() const override {\n\t\treturn true;\n\t}\n\tbool customInfoLayout() const override {\n\t\treturn true;\n\t}\n\nprivate:\n\tusing State = Data::CallState;\n\n\tQSize countOptimalSize() override;\n\n\tconst int _duration = 0;\n\tconst State _state = {};\n\tconst bool _conference = false;\n\tconst bool _video = false;\n\n\tQString _text;\n\tQString _status;\n\n\tClickHandlerPtr _link;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_contact.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_contact.h\"\n\n#include \"boxes/add_contact_box.h\"\n#include \"core/click_handler_types.h\" // ClickHandlerContext\n#include \"data/data_media_types.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"history/history_item_components.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_reply.h\"\n#include \"history/view/media/history_view_media_common.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/format_values.h\" // Ui::FormatPhone\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\" // Ui::Text::Wrapped.\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n\nnamespace HistoryView {\nnamespace {\n\nclass ContactClickHandler final : public LambdaClickHandler {\npublic:\n\tusing LambdaClickHandler::LambdaClickHandler;\n\n\tvoid setDragText(const QString &t) {\n\t\t_dragText = t;\n\t}\n\n\tQString dragText() const override final {\n\t\treturn _dragText;\n\t}\n\nprivate:\n\tQString _dragText;\n\n};\n\nClickHandlerPtr SendMessageClickHandler(not_null<PeerData*> peer) {\n\tconst auto clickHandlerPtr = std::make_shared<ContactClickHandler>([peer](\n\t\t\tClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tif (controller->session().uniqueId()\n\t\t\t\t\t!= peer->session().uniqueId()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcontroller->showPeerHistory(\n\t\t\t\tpeer->id,\n\t\t\t\tWindow::SectionShow::Way::Forward);\n\t\t}\n\t});\n\tif (const auto user = peer->asUser()) {\n\t\tclickHandlerPtr->setDragText(user->phone().isEmpty()\n\t\t\t? peer->name()\n\t\t\t: Ui::FormatPhone(user->phone()));\n\t}\n\treturn clickHandlerPtr;\n}\n\nClickHandlerPtr AddContactClickHandler(not_null<HistoryItem*> item) {\n\tconst auto session = &item->history()->session();\n\tconst auto sharedContact = [=, fullId = item->fullId()] {\n\t\tif (const auto item = session->data().message(fullId)) {\n\t\t\tif (const auto media = item->media()) {\n\t\t\t\treturn media->sharedContact();\n\t\t\t}\n\t\t}\n\t\treturn (const Data::SharedContact *)nullptr;\n\t};\n\tconst auto clickHandlerPtr = std::make_shared<ContactClickHandler>([=](\n\t\t\tClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tif (controller->session().uniqueId() != session->uniqueId()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (const auto contact = sharedContact()) {\n\t\t\t\tcontroller->show(Box<AddContactBox>(\n\t\t\t\t\tsession,\n\t\t\t\t\tcontact->firstName,\n\t\t\t\t\tcontact->lastName,\n\t\t\t\t\tcontact->phoneNumber));\n\t\t\t}\n\t\t}\n\t});\n\tif (const auto contact = sharedContact()) {\n\t\tclickHandlerPtr->setDragText(Ui::FormatPhone(contact->phoneNumber));\n\t}\n\treturn clickHandlerPtr;\n}\n\n[[nodiscard]] Fn<void(not_null<Ui::GenericBox*>)> VcardBoxFactory(\n\t\tconst Data::SharedContact::VcardItems &vcardItems) {\n\tif (vcardItems.empty()) {\n\t\treturn nullptr;\n\t}\n\treturn [=](not_null<Ui::GenericBox*> box) {\n\t\tbox->setTitle(tr::lng_contact_details_title());\n\t\tconst auto &stL = st::proxyApplyBoxLabel;\n\t\tconst auto &stSubL = st::boxDividerLabel;\n\t\tconst auto add = [&](const QString &s, tr::phrase<> phrase) {\n\t\t\tif (!s.isEmpty()) {\n\t\t\t\tconst auto label = box->addRow(\n\t\t\t\t\tobject_ptr<Ui::FlatLabel>(box, s, stL));\n\t\t\t\tbox->addRow(object_ptr<Ui::FlatLabel>(box, phrase(), stSubL));\n\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\treturn label;\n\t\t\t}\n\t\t\treturn (Ui::FlatLabel*)(nullptr);\n\t\t};\n\t\tfor (const auto &[type, value] : vcardItems) {\n\t\t\tusing Type = Data::SharedContact::VcardItemType;\n\t\t\tconst auto isPhoneType = (type == Type::Phone)\n\t\t\t\t|| (type == Type::PhoneMain)\n\t\t\t\t|| (type == Type::PhoneHome)\n\t\t\t\t|| (type == Type::PhoneMobile)\n\t\t\t\t|| (type == Type::PhoneWork)\n\t\t\t\t|| (type == Type::PhoneOther);\n\t\t\tconst auto typePhrase = (type == Type::Phone)\n\t\t\t\t? tr::lng_contact_details_phone\n\t\t\t\t: (type == Type::PhoneMain)\n\t\t\t\t? tr::lng_contact_details_phone_main\n\t\t\t\t: (type == Type::PhoneHome)\n\t\t\t\t? tr::lng_contact_details_phone_home\n\t\t\t\t: (type == Type::PhoneMobile)\n\t\t\t\t? tr::lng_contact_details_phone_mobile\n\t\t\t\t: (type == Type::PhoneWork)\n\t\t\t\t? tr::lng_contact_details_phone_work\n\t\t\t\t: (type == Type::PhoneOther)\n\t\t\t\t? tr::lng_contact_details_phone_other\n\t\t\t\t: (type == Type::Email)\n\t\t\t\t? tr::lng_contact_details_email\n\t\t\t\t: (type == Type::Address)\n\t\t\t\t? tr::lng_contact_details_address\n\t\t\t\t: (type == Type::Url)\n\t\t\t\t? tr::lng_contact_details_url\n\t\t\t\t: (type == Type::Note)\n\t\t\t\t? tr::lng_contact_details_note\n\t\t\t\t: (type == Type::Birthday)\n\t\t\t\t? tr::lng_contact_details_birthday\n\t\t\t\t: (type == Type::Organization)\n\t\t\t\t? tr::lng_contact_details_organization\n\t\t\t\t: tr::lng_payments_info_name;\n\t\t\tif (const auto label = add(value, typePhrase)) {\n\t\t\t\tconst auto copyText = isPhoneType\n\t\t\t\t\t? tr::lng_profile_copy_phone\n\t\t\t\t\t: (type == Type::Email)\n\t\t\t\t\t? tr::lng_context_copy_email\n\t\t\t\t\t: (type == Type::Url)\n\t\t\t\t\t? tr::lng_context_copy_link\n\t\t\t\t\t: (type == Type::Name)\n\t\t\t\t\t? tr::lng_profile_copy_fullname\n\t\t\t\t\t: tr::lng_context_copy_text;\n\t\t\t\tlabel->setContextCopyText(copyText(tr::now));\n\t\t\t\tif (type == Type::Email) {\n\t\t\t\t\tlabel->setMarkedText(\n\t\t\t\t\t\tUi::Text::Wrapped({ value }, EntityType::Email));\n\t\t\t\t} else if (type == Type::Url) {\n\t\t\t\t\tlabel->setMarkedText(\n\t\t\t\t\t\tUi::Text::Wrapped({ value }, EntityType::Url));\n\t\t\t\t} else if (isPhoneType) {\n\t\t\t\t\tlabel->setText(Ui::FormatPhone(value));\n\t\t\t\t}\n\t\t\t\tusing Request = Ui::FlatLabel::ContextMenuRequest;\n\t\t\t\tlabel->setContextMenuHook([=](Request r) {\n\t\t\t\t\tlabel->fillContextMenu(r.link\n\t\t\t\t\t\t? r\n\t\t\t\t\t\t: Request{ .menu = r.menu, .fullSelection = true });\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\t{\n\t\t\tconst auto inner = box->verticalLayout();\n\t\t\tif (inner->count() > 2) {\n\t\t\t\tdelete inner->widgetAt(inner->count() - 1);\n\t\t\t\tdelete inner->widgetAt(inner->count() - 1);\n\t\t\t}\n\t\t}\n\n\t\tbox->addButton(tr::lng_close(), [=] { box->closeBox(); });\n\t};\n}\n\n} // namespace\n\nContact::Contact(\n\tnot_null<Element*> parent,\n\tconst Data::SharedContact &data)\n: Media(parent)\n, _st(st::historyPagePreview)\n, _pixh(st::contactsPhotoSize)\n, _userId(data.userId)\n, _vcardBoxFactory(VcardBoxFactory(data.vcardItems)) {\n\thistory()->owner().registerContactView(data.userId, parent);\n\n\t_nameLine.setText(\n\t\tst::webPageTitleStyle,\n\t\ttr::lng_full_name(\n\t\t\ttr::now,\n\t\t\tlt_first_name,\n\t\t\tdata.firstName,\n\t\t\tlt_last_name,\n\t\t\tdata.lastName).trimmed(),\n\t\tUi::WebpageTextTitleOptions());\n\n\t_phoneLine.setText(\n\t\tst::webPageDescriptionStyle,\n\t\tUi::FormatPhone(data.phoneNumber),\n\t\tUi::WebpageTextTitleOptions());\n}\n\nContact::~Contact() {\n\thistory()->owner().unregisterContactView(_userId, _parent);\n\tif (!_userpic.null()) {\n\t\t_userpic = {};\n\t\t_parent->checkHeavyPart();\n\t}\n}\n\nvoid Contact::updateSharedContactUserId(UserId userId) {\n\tif (_userId != userId) {\n\t\thistory()->owner().unregisterContactView(_userId, _parent);\n\t\t_userId = userId;\n\t\thistory()->owner().registerContactView(_userId, _parent);\n\t}\n}\n\nQSize Contact::countOptimalSize() {\n\t_contact = _userId\n\t\t? _parent->data()->history()->owner().userLoaded(_userId)\n\t\t: nullptr;\n\tif (_contact) {\n\t\t_contact->loadUserpic();\n\t} else {\n\t\tconst auto full = _nameLine.toString();\n\t\t_photoEmpty = std::make_unique<Ui::EmptyUserpic>(\n\t\t\tUi::EmptyUserpic::UserpicColor(Data::DecideColorIndex(_userId\n\t\t\t\t? peerFromUser(_userId)\n\t\t\t\t: Data::FakePeerIdForJustName(full))),\n\t\t\tfull);\n\t}\n\n\tconst auto vcardBoxFactory = _vcardBoxFactory;\n\t_buttons.clear();\n\tif (_contact) {\n\t\tconst auto message = tr::lng_contact_send_message(tr::now).toUpper();\n\t\t_buttons.push_back({\n\t\t\tmessage,\n\t\t\tst::semiboldFont->width(message),\n\t\t\tSendMessageClickHandler(_contact),\n\t\t});\n\t\tif (!_contact->isContact()) {\n\t\t\tconst auto add = tr::lng_contact_add(tr::now).toUpper();\n\t\t\t_buttons.push_back({\n\t\t\t\tadd,\n\t\t\t\tst::semiboldFont->width(add),\n\t\t\t\tAddContactClickHandler(_parent->data()),\n\t\t\t});\n\t\t}\n\t\t_mainButton.link = _buttons.front().link;\n\t} else if (vcardBoxFactory) {\n\t\tconst auto view = tr::lng_contact_details_button(tr::now).toUpper();\n\t\t_buttons.push_back({\n\t\t\tview,\n\t\t\tst::semiboldFont->width(view),\n\t\t\tAddContactClickHandler(_parent->data()),\n\t\t});\n\t}\n\tif (vcardBoxFactory) {\n\t\t_mainButton.link = std::make_shared<LambdaClickHandler>([=](\n\t\t\t\tconst ClickContext &context) {\n\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\t\tcontroller->uiShow()->show(Box(vcardBoxFactory));\n\t\t\t}\n\t\t});\n\t}\n\n\tconst auto padding = inBubblePadding() + innerMargin();\n\tconst auto full = Rect(currentSize());\n\tconst auto outer = full - inBubblePadding();\n\tconst auto inner = outer - innerMargin();\n\tconst auto lineLeft = inner.left() + _pixh + inner.left() - outer.left();\n\tconst auto lineHeight = UnitedLineHeight();\n\n\tauto maxWidth = _parent->skipBlockWidth();\n\tauto minHeight = 0;\n\n\tauto textMinHeight = 0;\n\tif (!_nameLine.isEmpty()) {\n\t\taccumulate_max(maxWidth, lineLeft + _nameLine.maxWidth());\n\t\ttextMinHeight += 1 * lineHeight;\n\t}\n\tif (!_phoneLine.isEmpty()) {\n\t\taccumulate_max(maxWidth, lineLeft + _phoneLine.maxWidth());\n\t\ttextMinHeight += 1 * lineHeight;\n\t}\n\tminHeight = std::max(textMinHeight, st::contactsPhotoSize);\n\n\tif (!_buttons.empty()) {\n\t\tauto buttonsWidth = rect::m::sum::h(st::historyPageButtonPadding);\n\t\tfor (const auto &button : _buttons) {\n\t\t\tbuttonsWidth += button.width;\n\t\t}\n\t\taccumulate_max(maxWidth, buttonsWidth);\n\t}\n\tmaxWidth += rect::m::sum::h(padding);\n\tminHeight += rect::m::sum::v(padding);\n\n\treturn { maxWidth, minHeight };\n}\n\nvoid Contact::draw(Painter &p, const PaintContext &context) const {\n\tif (width() < rect::m::sum::h(st::msgPadding) + 1) {\n\t\treturn;\n\t}\n\n\tconst auto st = context.st;\n\tconst auto stm = context.messageStyle();\n\n\tconst auto full = Rect(currentSize());\n\tconst auto outer = full - inBubblePadding();\n\tconst auto inner = outer - innerMargin();\n\tauto tshift = inner.top();\n\n\tconst auto selected = context.selected();\n\tconst auto view = parent();\n\tconst auto colorIndex = _contact\n\t\t? _contact->colorIndex()\n\t\t: Data::DecideColorIndex(\n\t\t\tData::FakePeerIdForJustName(_nameLine.toString()));\n\tconst auto &colorCollectible = _contact\n\t\t? _contact->colorCollectible()\n\t\t: nullptr;\n\tconst auto colorPattern = colorCollectible\n\t\t? st->collectiblePatternIndex(colorCollectible)\n\t\t: st->colorPatternIndex(colorIndex);\n\tconst auto useColorCollectible = colorCollectible && !context.outbg;\n\tconst auto useColorIndex = !context.outbg;\n\tconst auto cache = useColorCollectible\n\t\t? st->collectibleReplyCache(selected, colorCollectible).get()\n\t\t: useColorIndex\n\t\t? st->coloredReplyCache(selected, colorIndex).get()\n\t\t: stm->replyCache[colorPattern].get();\n\tconst auto backgroundEmojiId = _contact\n\t\t? _contact->backgroundEmojiId()\n\t\t: DocumentId();\n\tconst auto backgroundEmojiData = backgroundEmojiId\n\t\t? st->backgroundEmojiData(backgroundEmojiId, colorCollectible).get()\n\t\t: nullptr;\n\tconst auto backgroundEmojiCache = !backgroundEmojiData\n\t\t? nullptr\n\t\t: useColorCollectible\n\t\t? &backgroundEmojiData->collectibleCaches[colorCollectible]\n\t\t: &backgroundEmojiData->caches[Ui::BackgroundEmojiData::CacheIndex(\n\t\t\tselected,\n\t\t\tcontext.outbg,\n\t\t\ttrue,\n\t\t\tuseColorIndex ? (colorIndex + 1) : 0)];\n\tUi::Text::ValidateQuotePaintCache(*cache, _st);\n\tUi::Text::FillQuotePaint(p, outer, *cache, _st);\n\tif (backgroundEmojiData) {\n\t\tValidateBackgroundEmoji(\n\t\t\tbackgroundEmojiId,\n\t\t\tcolorCollectible,\n\t\t\tbackgroundEmojiData,\n\t\t\tbackgroundEmojiCache,\n\t\t\tcache,\n\t\t\tview);\n\t\tif (!backgroundEmojiCache->frames[0].isNull()) {\n\t\t\tconst auto end = rect::bottom(inner) + _st.padding.bottom();\n\t\t\tconst auto r = outer\n\t\t\t\t- QMargins(0, 0, 0, rect::bottom(outer) - end);\n\t\t\tFillBackgroundEmoji(\n\t\t\t\tp,\n\t\t\t\tr,\n\t\t\t\tfalse,\n\t\t\t\t*backgroundEmojiCache,\n\t\t\t\tbackgroundEmojiData->firstGiftFrame);\n\t\t}\n\t}\n\n\tif (_mainButton.ripple) {\n\t\t_mainButton.ripple->paint(\n\t\t\tp,\n\t\t\touter.x(),\n\t\t\touter.y(),\n\t\t\twidth(),\n\t\t\t&cache->bg);\n\t\tif (_mainButton.ripple->empty()) {\n\t\t\t_mainButton.ripple = nullptr;\n\t\t}\n\t}\n\n\t{\n\t\tconst auto left = inner.left();\n\t\tconst auto top = tshift;\n\t\tif (_userId) {\n\t\t\tif (_contact) {\n\t\t\t\tconst auto was = !_userpic.null();\n\t\t\t\t_contact->paintUserpic(p, _userpic, left, top, _pixh);\n\t\t\t\tif (!was && !_userpic.null()) {\n\t\t\t\t\thistory()->owner().registerHeavyViewPart(_parent);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t_photoEmpty->paintCircle(p, left, top, _pixh, _pixh);\n\t\t\t}\n\t\t} else {\n\t\t\t_photoEmpty->paintCircle(p, left, top, _pixh, _pixh);\n\t\t}\n\t\tif (context.selected()) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setBrush(p.textPalette().selectOverlay);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.drawEllipse(left, top, _pixh, _pixh);\n\t\t}\n\t}\n\n\tconst auto lineHeight = UnitedLineHeight();\n\tconst auto lineLeft = inner.left() + _pixh + inner.left() - outer.left();\n\tconst auto lineWidth = rect::right(inner) - lineLeft;\n\n\t{\n\t\tp.setPen(cache->icon);\n\t\tp.setTextPalette(useColorCollectible\n\t\t\t? st->collectibleTextPalette(selected, colorCollectible)\n\t\t\t: useColorIndex\n\t\t\t? st->coloredTextPalette(selected, colorIndex)\n\t\t\t: stm->semiboldPalette);\n\n\t\tconst auto endskip = _nameLine.hasSkipBlock()\n\t\t\t? _parent->skipBlockWidth()\n\t\t\t: 0;\n\t\t_nameLine.drawLeftElided(\n\t\t\tp,\n\t\t\tlineLeft,\n\t\t\ttshift,\n\t\t\tlineWidth,\n\t\t\twidth(),\n\t\t\t1,\n\t\t\tstyle::al_left,\n\t\t\t0,\n\t\t\t-1,\n\t\t\tendskip,\n\t\t\tfalse,\n\t\t\tcontext.selection);\n\t\ttshift += lineHeight;\n\n\t\tp.setTextPalette(stm->textPalette);\n\t}\n\tp.setPen(stm->historyTextFg);\n\t{\n\t\ttshift += st::lineWidth * 3; // Additional skip.\n\t\tconst auto endskip = _phoneLine.hasSkipBlock()\n\t\t\t? _parent->skipBlockWidth()\n\t\t\t: 0;\n\t\t_phoneLine.drawLeftElided(\n\t\t\tp,\n\t\t\tlineLeft,\n\t\t\ttshift,\n\t\t\tlineWidth,\n\t\t\twidth(),\n\t\t\t1,\n\t\t\tstyle::al_left,\n\t\t\t0,\n\t\t\t-1,\n\t\t\tendskip,\n\t\t\tfalse,\n\t\t\ttoTitleSelection(context.selection));\n\t\ttshift += 1 * lineHeight;\n\t}\n\n\tif (!_buttons.empty()) {\n\t\tp.setFont(st::semiboldFont);\n\t\tp.setPen(cache->icon);\n\t\tconst auto end = rect::bottom(inner) + _st.padding.bottom();\n\t\tconst auto line = st::historyPageButtonLine;\n\t\tauto color = cache->icon;\n\t\tcolor.setAlphaF(color.alphaF() * 0.3);\n\t\tconst auto top = end + st::historyPageButtonPadding.top();\n\t\tconst auto buttonWidth = inner.width() / float64(_buttons.size());\n\t\tp.fillRect(inner.x(), end, inner.width(), line, color);\n\t\tfor (auto i = 0; i < _buttons.size(); i++) {\n\t\t\tconst auto &button = _buttons[i];\n\t\t\tconst auto left = inner.x() + i * buttonWidth;\n\t\t\tif (button.ripple) {\n\t\t\t\tbutton.ripple->paint(p, left, end, buttonWidth, &cache->bg);\n\t\t\t\tif (button.ripple->empty()) {\n\t\t\t\t\t_buttons[i].ripple = nullptr;\n\t\t\t\t}\n\t\t\t}\n\t\t\tp.drawText(\n\t\t\t\tleft + (buttonWidth - button.width) / 2,\n\t\t\t\ttop + st::semiboldFont->ascent,\n\t\t\t\tbutton.text);\n\t\t}\n\t}\n}\n\nTextState Contact::textState(QPoint point, StateRequest request) const {\n\tauto result = TextState(_parent);\n\n\tconst auto full = Rect(currentSize());\n\tconst auto outer = full - inBubblePadding();\n\tconst auto inner = outer - innerMargin();\n\n\t_lastPoint = point;\n\n\tif (!hasSingleLink()) {\n\t\tconst auto end = rect::bottom(inner) + _st.padding.bottom();\n\t\tconst auto bWidth = inner.width() / float64(_buttons.size());\n\t\tconst auto bHeight = rect::bottom(outer) - end;\n\t\tfor (auto i = 0; i < _buttons.size(); i++) {\n\t\t\tconst auto left = inner.x() + i * bWidth;\n\t\t\tif (QRectF(left, end, bWidth, bHeight).contains(point)) {\n\t\t\t\tresult.link = _buttons[i].link;\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\tif (outer.contains(point)) {\n\t\tresult.link = _mainButton.link;\n\t\treturn result;\n\t}\n\treturn result;\n}\n\nvoid Contact::unloadHeavyPart() {\n\t_userpic = {};\n}\n\nbool Contact::hasHeavyPart() const {\n\treturn !_userpic.null();\n}\n\nbool Contact::hasSingleLink() const {\n\treturn (_buttons.size() > 1)\n\t\t? false\n\t\t: (_buttons.size() == 1 && _buttons.front().link == _mainButton.link)\n\t\t? true\n\t\t: (_buttons.empty() && _mainButton.link);\n}\n\nvoid Contact::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed) {\n\tconst auto full = Rect(currentSize());\n\tconst auto outer = full - inBubblePadding();\n\tconst auto inner = outer - innerMargin();\n\tconst auto end = rect::bottom(inner) + _st.padding.bottom();\n\tif ((_lastPoint.y() < end) || hasSingleLink()) {\n\t\tif (p != _mainButton.link) {\n\t\t\treturn;\n\t\t}\n\t\tif (pressed) {\n\t\t\tif (!_mainButton.ripple) {\n\t\t\t\tconst auto owner = &parent()->history()->owner();\n\t\t\t\t_mainButton.ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\t\tUi::RippleAnimation::RoundRectMask(\n\t\t\t\t\t\touter.size(),\n\t\t\t\t\t\t_st.radius),\n\t\t\t\t\t[=] { owner->requestViewRepaint(parent()); });\n\t\t\t}\n\t\t\t_mainButton.ripple->add(_lastPoint - outer.topLeft());\n\t\t} else if (_mainButton.ripple) {\n\t\t\t_mainButton.ripple->lastStop();\n\t\t}\n\t\treturn;\n\t} else if (_buttons.empty()) {\n\t\treturn;\n\t}\n\tconst auto bWidth = inner.width() / float64(_buttons.size());\n\tconst auto bHeight = rect::bottom(outer) - end;\n\tfor (auto i = 0; i < _buttons.size(); i++) {\n\t\tconst auto &button = _buttons[i];\n\t\tif (p != button.link) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (pressed) {\n\t\t\tif (!button.ripple) {\n\t\t\t\tconst auto owner = &parent()->history()->owner();\n\n\t\t\t\t_buttons[i].ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\t\tUi::RippleAnimation::MaskByDrawer(\n\t\t\t\t\t\tQSize(bWidth, bHeight),\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t[=](QPainter &p) {\n\t\t\t\t\t\t\tp.drawRect(0, 0, bWidth, bHeight);\n\t\t\t\t\t\t}),\n\t\t\t\t\t[=] { owner->requestViewRepaint(parent()); });\n\t\t\t}\n\t\t\tbutton.ripple->add(_lastPoint\n\t\t\t\t- QPoint(inner.x() + i * bWidth, end));\n\t\t} else if (button.ripple) {\n\t\t\tbutton.ripple->lastStop();\n\t\t}\n\t}\n}\n\nQMargins Contact::inBubblePadding() const {\n\treturn {\n\t\tst::msgPadding.left(),\n\t\tisBubbleTop() ? st::msgPadding.left() : 0,\n\t\tst::msgPadding.right(),\n\t\tisBubbleBottom() ? (st::msgPadding.left() + bottomInfoPadding()) : 0\n\t};\n}\n\nQMargins Contact::innerMargin() const {\n\tconst auto button = _buttons.empty() ? 0 : st::historyPageButtonHeight;\n\treturn _st.padding + QMargins(0, 0, 0, button);\n}\n\nint Contact::bottomInfoPadding() const {\n\tif (!isBubbleBottom()) {\n\t\treturn 0;\n\t}\n\n\tauto result = st::msgDateFont->height;\n\n\t// We use padding greater than st::msgPadding.bottom() in the\n\t// bottom of the bubble so that the left line looks pretty.\n\t// but if we have bottom skip because of the info display\n\t// we don't need that additional padding so we replace it\n\t// back with st::msgPadding.bottom() instead of left().\n\tresult += st::msgPadding.bottom() - st::msgPadding.left();\n\treturn result;\n}\n\nTextSelection Contact::toTitleSelection(TextSelection selection) const {\n\treturn UnshiftItemSelection(selection, _nameLine);\n}\n\nTextSelection Contact::toDescriptionSelection(TextSelection selection) const {\n\treturn UnshiftItemSelection(toTitleSelection(selection), _phoneLine);\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_contact.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n#include \"ui/userpic_view.h\"\n\nnamespace Data {\nstruct SharedContact;\n} // namespace Data\n\nnamespace Ui {\nclass EmptyUserpic;\nclass GenericBox;\nclass RippleAnimation;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass Contact final : public Media {\npublic:\n\tContact(\n\t\tnot_null<Element*> parent,\n\t\tconst Data::SharedContact &data);\n\t~Contact();\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\n\tbool toggleSelectionByHandlerClick(\n\t\t\tconst ClickHandlerPtr &p) const override {\n\t\treturn true;\n\t}\n\tbool dragItemByHandler(const ClickHandlerPtr &p) const override {\n\t\treturn true;\n\t}\n\n\tbool needsBubble() const override {\n\t\treturn true;\n\t}\n\tbool customInfoLayout() const override {\n\t\treturn false;\n\t}\n\n\t// Should be called only by Data::Session.\n\tvoid updateSharedContactUserId(UserId userId) override;\n\n\tvoid unloadHeavyPart() override;\n\tbool hasHeavyPart() const override;\n\nprivate:\n\tQSize countOptimalSize() override;\n\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p, bool pressed) override;\n\n\t[[nodiscard]] QMargins inBubblePadding() const;\n\t[[nodiscard]] QMargins innerMargin() const;\n\t[[nodiscard]] int bottomInfoPadding() const;\n\n\t[[nodiscard]] TextSelection toTitleSelection(\n\t\tTextSelection selection) const;\n\t[[nodiscard]] TextSelection toDescriptionSelection(\n\t\tTextSelection selection) const;\n\n\t[[nodiscard]] bool hasSingleLink() const;\n\n\tconst style::QuoteStyle &_st;\n\tconst int _pixh;\n\n\tUserId _userId = 0;\n\tUserData *_contact = nullptr;\n\n\tUi::Text::String _nameLine;\n\tUi::Text::String _phoneLine;\n\n\tFn<void(not_null<Ui::GenericBox*>)> _vcardBoxFactory;\n\n\tstruct Button {\n\t\tQString text;\n\t\tint width = 0;\n\t\tClickHandlerPtr link;\n\t\tmutable std::unique_ptr<Ui::RippleAnimation> ripple;\n\t};\n\tstd::vector<Button> _buttons;\n\tButton _mainButton;\n\n\tstd::unique_ptr<Ui::EmptyUserpic> _photoEmpty;\n\tmutable Ui::PeerUserpicView _userpic;\n\tmutable QPoint _lastPoint;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_custom_emoji.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_custom_emoji.h\"\n\n#include \"history/view/media/history_view_sticker.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"main/main_session.h\"\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/text/text_isolated_emoji.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nusing SizeTag = Data::CustomEmojiManager::SizeTag;\nusing LottieSize = ChatHelpers::StickerLottieSize;\nusing CustomPtr = std::unique_ptr<Ui::Text::CustomEmoji>;\nusing StickerPtr = std::unique_ptr<Sticker>;\n\nstruct CustomEmojiSizeInfo {\n\tLottieSize tag = LottieSize::MessageHistory;\n\tfloat64 scale = 1.;\n};\n\n[[nodiscard]] const base::flat_map<int, CustomEmojiSizeInfo> &SizesInfo() {\n\t// size = i->second.scale * Sticker::EmojiSize().width()\n\t// CustomEmojiManager::SizeTag caching uses first ::EmojiInteraction-s.\n\tusing Info = CustomEmojiSizeInfo;\n\tstatic auto result = base::flat_map<int, Info>{\n\t\t{ 1, Info{ LottieSize::EmojiInteractionReserved7, 1. } },\n\t\t{ 2, Info{ LottieSize::EmojiInteractionReserved6, 0.7 } },\n\t\t{ 3, Info{ LottieSize::EmojiInteractionReserved5, 0.52 } },\n\t};\n\treturn result;\n}\n\n[[nodiscard]] SizeTag EmojiSize(int dimension) {\n\treturn (dimension == 4 || dimension == 5)\n\t\t? SizeTag::Isolated\n\t\t: (dimension == 6 || dimension == 7)\n\t\t? SizeTag::Large\n\t\t: SizeTag::Normal;\n}\n\n} //namespace\n\nCustomEmoji::CustomEmoji(\n\tnot_null<Element*> parent,\n\tconst Ui::Text::OnlyCustomEmoji &emoji)\n: _parent(parent) {\n\tExpects(!emoji.lines.empty());\n\n\tconst auto owner = &parent->history()->owner();\n\tconst auto manager = &owner->customEmojiManager();\n\tconst auto max = ranges::max_element(\n\t\temoji.lines,\n\t\tstd::less<>(),\n\t\t&std::vector<Ui::Text::OnlyCustomEmoji::Item>::size);\n\tconst auto dimension = int(std::max(emoji.lines.size(), max->size()));\n\tconst auto &sizes = SizesInfo();\n\tconst auto i = sizes.find(dimension);\n\tconst auto useCustomEmoji = (i == end(sizes));\n\tconst auto tag = EmojiSize(dimension);\n\t_singleSize = !useCustomEmoji\n\t\t? int(base::SafeRound(\n\t\t\ti->second.scale * Sticker::EmojiSize().width()))\n\t\t: (Data::FrameSizeFromTag(tag) / style::DevicePixelRatio());\n\tif (!useCustomEmoji) {\n\t\t_cachingTag = i->second.tag;\n\t}\n\tfor (const auto &line : emoji.lines) {\n\t\t_lines.emplace_back();\n\t\tfor (const auto &element : line) {\n\t\t\tif (useCustomEmoji) {\n\t\t\t\t_lines.back().push_back(\n\t\t\t\t\tmanager->create(\n\t\t\t\t\t\telement.entityData,\n\t\t\t\t\t\t[=] { parent->customEmojiRepaint(); },\n\t\t\t\t\t\ttag));\n\t\t\t} else {\n\t\t\t\tconst auto &data = element.entityData;\n\t\t\t\tconst auto id = Data::ParseCustomEmojiData(data);\n\t\t\t\tconst auto document = owner->document(id);\n\t\t\t\tif (document->sticker()) {\n\t\t\t\t\t_lines.back().push_back(createStickerPart(document));\n\t\t\t\t} else {\n\t\t\t\t\t_lines.back().push_back(id);\n\t\t\t\t\tmanager->resolve(id, listener());\n\t\t\t\t\t_resolving = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid CustomEmoji::customEmojiResolveDone(not_null<DocumentData*> document) {\n\tif (!document->sticker()) {\n\t\treturn;\n\t}\n\t_resolving = false;\n\tconst auto id = document->id;\n\tfor (auto &line : _lines) {\n\t\tfor (auto &entry : line) {\n\t\t\tif (entry == id) {\n\t\t\t\tentry = createStickerPart(document);\n\t\t\t} else if (v::is<DocumentId>(entry)) {\n\t\t\t\t_resolving = true;\n\t\t\t}\n\t\t}\n\t}\n}\n\nstd::unique_ptr<Sticker> CustomEmoji::createStickerPart(\n\tnot_null<DocumentData*> document) const {\n\tconst auto skipPremiumEffect = false;\n\tauto result = std::make_unique<Sticker>(\n\t\t_parent,\n\t\tdocument,\n\t\tskipPremiumEffect);\n\tresult->initSize(_singleSize);\n\tresult->setCustomCachingTag(_cachingTag);\n\tresult->setCustomEmojiPart();\n\treturn result;\n}\n\nvoid CustomEmoji::refreshInteractionLink() {\n\tif (_lines.size() != 1 || _lines.front().size() != 1) {\n\t\treturn;\n\t}\n\tconst auto &pack = _parent->history()->session().emojiStickersPack();\n\tconst auto version = pack.animationsVersion();\n\tif (_animationsCheckVersion == version) {\n\t\treturn;\n\t}\n\t_animationsCheckVersion = version;\n\tif (pack.hasAnimationsFor(_parent->data())) {\n\t\tconst auto weak = base::make_weak(this);\n\t\t_interactionLink = std::make_shared<LambdaClickHandler>([weak] {\n\t\t\tif (const auto that = weak.get()) {\n\t\t\t\tthat->interactionLinkClicked();\n\t\t\t}\n\t\t});\n\t} else {\n\t\t_interactionLink = nullptr;\n\t}\n}\n\nClickHandlerPtr CustomEmoji::link() {\n\trefreshInteractionLink();\n\treturn _interactionLink;\n}\n\nvoid CustomEmoji::interactionLinkClicked() {\n\tconst auto &entry = _lines.front().front();\n\tif (const auto sticker = std::get_if<StickerPtr>(&entry)) {\n\t\tif ((*sticker)->ready()) {\n\t\t\t_parent->delegate()->elementStartInteraction(_parent);\n\t\t}\n\t}\n}\n\nCustomEmoji::~CustomEmoji() {\n\tif (_hasHeavyPart) {\n\t\tunloadHeavyPart();\n\t\t_parent->checkHeavyPart();\n\t}\n\tif (_resolving) {\n\t\tconst auto owner = &_parent->history()->owner();\n\t\towner->customEmojiManager().unregisterListener(listener());\n\t}\n}\n\nQSize CustomEmoji::countOptimalSize() {\n\tExpects(!_lines.empty());\n\n\tconst auto max = ranges::max_element(\n\t\t_lines,\n\t\tstd::less<>(),\n\t\t&std::vector<LargeCustomEmoji>::size);\n\treturn {\n\t\t_singleSize * int(max->size()),\n\t\t_singleSize * int(_lines.size()),\n\t};\n}\n\nQSize CustomEmoji::countCurrentSize(int newWidth) {\n\tconst auto perRow = std::max(newWidth / _singleSize, 1);\n\tauto width = 0;\n\tauto height = 0;\n\tfor (const auto &line : _lines) {\n\t\tconst auto count = int(line.size());\n\t\taccumulate_max(width, std::min(perRow, count) * _singleSize);\n\t\theight += std::max((count + perRow - 1) / perRow, 1) * _singleSize;\n\t}\n\treturn { width, height };\n}\n\nvoid CustomEmoji::draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &r) {\n\t_parent->clearCustomEmojiRepaint();\n\n\tauto x = r.x();\n\tauto y = r.y();\n\tconst auto perRow = std::max(r.width() / _singleSize, 1);\n\tfor (auto &line : _lines) {\n\t\tconst auto count = int(line.size());\n\t\tconst auto rows = std::max((count + perRow - 1) / perRow, 1);\n\t\tfor (auto row = 0; row != rows; ++row) {\n\t\t\tfor (auto column = 0; column != perRow; ++column) {\n\t\t\t\tconst auto index = row * perRow + column;\n\t\t\t\tif (index >= count) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tpaintElement(p, x, y, line[index], context);\n\t\t\t\tx += _singleSize;\n\t\t\t}\n\t\t\tx = r.x();\n\t\t\ty += _singleSize;\n\t\t}\n\t}\n}\n\nvoid CustomEmoji::paintElement(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tLargeCustomEmoji &element,\n\t\tconst PaintContext &context) {\n\tif (const auto sticker = std::get_if<StickerPtr>(&element)) {\n\t\tpaintSticker(p, x, y, sticker->get(), context);\n\t} else if (const auto custom = std::get_if<CustomPtr>(&element)) {\n\t\tpaintCustom(p, x, y, custom->get(), context);\n\t}\n}\n\nvoid CustomEmoji::paintSticker(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tnot_null<Sticker*> sticker,\n\t\tconst PaintContext &context) {\n\tsticker->draw(p, context, { QPoint(x, y), sticker->countOptimalSize() });\n}\n\nvoid CustomEmoji::paintCustom(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tnot_null<Ui::Text::CustomEmoji*> emoji,\n\t\tconst PaintContext &context) {\n\tif (!_hasHeavyPart) {\n\t\t_hasHeavyPart = true;\n\t\t_parent->history()->owner().registerHeavyViewPart(_parent);\n\t}\n\t//const auto preview = context.imageStyle()->msgServiceBg->c;\n\tauto &textst = context.st->messageStyle(false, false);\n\tconst auto paused = context.paused || On(PowerSaving::kEmojiChat);\n\tif (context.selected()) {\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tconst auto size = QSize(_singleSize, _singleSize) * factor;\n\t\tif (_selectedFrame.size() != size) {\n\t\t\t_selectedFrame = QImage(\n\t\t\t\tsize,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_selectedFrame.setDevicePixelRatio(factor);\n\t\t}\n\t\t_selectedFrame.fill(Qt::transparent);\n\t\tauto q = QPainter(&_selectedFrame);\n\t\temoji->paint(q, {\n\t\t\t.textColor = textst.historyTextFg->c,\n\t\t\t.now = context.now,\n\t\t\t.paused = paused,\n\t\t});\n\t\tq.end();\n\n\t\t_selectedFrame = Images::Colored(\n\t\t\tstd::move(_selectedFrame),\n\t\t\tcontext.st->msgStickerOverlay()->c);\n\t\tp.drawImage(x, y, _selectedFrame);\n\t} else {\n\t\temoji->paint(p, {\n\t\t\t.textColor = textst.historyTextFg->c,\n\t\t\t.now = context.now,\n\t\t\t.position = { x, y },\n\t\t\t.paused = paused,\n\t\t});\n\t}\n}\n\nbool CustomEmoji::alwaysShowOutTimestamp() {\n\treturn (_lines.size() == 1) && _lines.back().size() > 3;\n}\n\nbool CustomEmoji::hasHeavyPart() const {\n\treturn _hasHeavyPart;\n}\n\nvoid CustomEmoji::unloadHeavyPart() {\n\tif (!_hasHeavyPart) {\n\t\treturn;\n\t}\n\tconst auto unload = [&](const LargeCustomEmoji &element) {\n\t\tif (const auto sticker = std::get_if<StickerPtr>(&element)) {\n\t\t\t(*sticker)->unloadHeavyPart();\n\t\t} else if (const auto custom = std::get_if<CustomPtr>(&element)) {\n\t\t\t(*custom)->unload();\n\t\t}\n\t};\n\t_hasHeavyPart = false;\n\tfor (const auto &line : _lines) {\n\t\tfor (const auto &element : line) {\n\t\t\tunload(element);\n\t\t}\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_custom_emoji.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media_unwrapped.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"base/weak_ptr.h\"\n\nnamespace Ui::Text {\nstruct OnlyCustomEmoji;\n} // namespace Ui::Text\n\nnamespace Stickers {\nstruct LargeEmojiImage;\n} // namespace Stickers\n\nnamespace ChatHelpers {\nenum class StickerLottieSize : uint8;\n} // namespace ChatHelpers\n\nnamespace HistoryView {\n\nclass Sticker;\n\nusing LargeCustomEmoji = std::variant<\n\tDocumentId,\n\tstd::unique_ptr<Sticker>,\n\tstd::unique_ptr<Ui::Text::CustomEmoji>>;\n\nclass CustomEmoji final\n\t: public UnwrappedMedia::Content\n\t, public base::has_weak_ptr\n\t, private Data::CustomEmojiManager::Listener {\npublic:\n\tCustomEmoji(\n\t\tnot_null<Element*> parent,\n\t\tconst Ui::Text::OnlyCustomEmoji &emoji);\n\t~CustomEmoji();\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\tvoid draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &r) override;\n\tClickHandlerPtr link() override;\n\n\tbool alwaysShowOutTimestamp() override;\n\tbool hasTextForCopy() const override {\n\t\treturn true;\n\t}\n\n\tbool hasHeavyPart() const override;\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\tvoid paintElement(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tLargeCustomEmoji &element,\n\t\tconst PaintContext &context);\n\tvoid paintSticker(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tnot_null<Sticker*> sticker,\n\t\tconst PaintContext &context);\n\tvoid paintCustom(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tnot_null<Ui::Text::CustomEmoji*> emoji,\n\t\tconst PaintContext &context);\n\n\t[[nodiscard]] not_null<Data::CustomEmojiManager::Listener*> listener() {\n\t\treturn this;\n\t}\n\tvoid customEmojiResolveDone(not_null<DocumentData*> document) override;\n\n\t[[nodiscard]] std::unique_ptr<Sticker> createStickerPart(\n\t\tnot_null<DocumentData*> document) const;\n\n\tvoid refreshInteractionLink();\n\tvoid interactionLinkClicked();\n\n\tconst not_null<Element*> _parent;\n\tstd::vector<std::vector<LargeCustomEmoji>> _lines;\n\tClickHandlerPtr _interactionLink;\n\tQImage _selectedFrame;\n\tint _singleSize = 0;\n\tint _animationsCheckVersion = -1;\n\tChatHelpers::StickerLottieSize _cachingTag = {};\n\tbool _hasHeavyPart = false;\n\tbool _resolving = false;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_dice.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_dice.h\"\n\n#include \"data/data_session.h\"\n#include \"chat_helpers/stickers_dice_pack.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/view/history_view_element.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\n[[nodiscard]] DocumentData *Lookup(\n\t\tnot_null<Element*> view,\n\t\tconst QString &emoji,\n\t\tint value) {\n\tconst auto &session = view->history()->session();\n\treturn session.diceStickersPacks().lookup(emoji, value);\n}\n\n} // namespace\n\nDice::Dice(not_null<Element*> parent, not_null<Data::MediaDice*> dice)\n: _parent(parent)\n, _dice(dice)\n, _link(dice->makeHandler()) {\n\tif (const auto document = Lookup(parent, dice->emoji(), 0)) {\n\t\tconst auto skipPremiumEffect = false;\n\t\t_start.emplace(parent, document, skipPremiumEffect);\n\t\t_start->setDiceIndex(_dice->emoji(), 0);\n\t}\n\t_showLastFrame = _parent->data()->Has<HistoryMessageForwarded>();\n\tif (_showLastFrame) {\n\t\t_drawingEnd = true;\n\t}\n\tif (const auto outcome = _dice->diceGameOutcome()) {\n\t\t_outcomeSet = true;\n\t\t_outcomeValue = _dice->value();\n\t\t_outcomeNanoTon = outcome.nanoTon;\n\t\t_outcomeStakeNanoTon = outcome.stakeNanoTon;\n\t\t_outcomeStartedUnknown = (_outcomeValue == 0);\n\t\tupdateOutcomeMessage();\n\t}\n}\n\nDice::~Dice() = default;\n\nvoid Dice::updateOutcomeMessage() {\n\tif ((_outcomeStartedUnknown && !_outcomeLastPainted)\n\t\t|| (!_outcomeValue && !_outcomeNanoTon)) {\n\t\t_parent->setServicePostMessage({});\n\t\treturn;\n\t}\n\tconst auto item = _parent->data();\n\tconst auto forwarded = item->Get<HistoryMessageForwarded>();\n\tconst auto originalSender = forwarded\n\t\t? forwarded->originalSender\n\t\t: nullptr;\n\tconst auto from = originalSender ? originalSender : item->from().get();\n\tconst auto originalPostAuthor = item->originalPostAuthor();\n\tconst auto fromName = !originalPostAuthor.isEmpty()\n\t\t? originalPostAuthor\n\t\t: forwarded && forwarded->originalHiddenSenderInfo\n\t\t? forwarded->originalHiddenSenderInfo->name\n\t\t: from->name();\n\tconst auto out = (item->out() || from->isSelf()) && !forwarded;\n\tconst auto won = (_outcomeNanoTon - _outcomeStakeNanoTon);\n\tconst auto amount = tr::marked(QString::fromUtf8(\"\\xf0\\x9f\\x92\\x8e\")\n\t\t+ \" \"\n\t\t+ QString::number(std::abs(won) / 1e9));\n\tconst auto text = out\n\t\t? (won >= 0\n\t\t\t? tr::lng_action_stake_game_won_you\n\t\t\t: tr::lng_action_stake_game_lost_you)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_amount,\n\t\t\t\tamount,\n\t\t\t\ttr::marked)\n\t\t: (won >= 0\n\t\t\t? tr::lng_action_stake_game_won\n\t\t\t: tr::lng_action_stake_game_lost)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from,\n\t\t\t\ttr::link(st::wrap_rtl(fromName), 1),\n\t\t\t\tlt_amount,\n\t\t\t\tamount,\n\t\t\t\ttr::marked);\n\tauto prepared = PreparedServiceText{ text };\n\tif (!out) {\n\t\tif (const auto link = forwarded ? originalSender : from) {\n\t\t\tprepared.links.push_back(link->createOpenLink());\n\t\t}\n\t}\n\t_parent->setServicePostMessage(prepared, _link);\n}\n\nQSize Dice::countOptimalSize() {\n\treturn _start ? _start->countOptimalSize() : Sticker::EmojiSize();\n}\n\nClickHandlerPtr Dice::link() {\n\treturn _link;\n}\n\nbool Dice::updateItemData() {\n\tconst auto outcome = _dice->diceGameOutcome();\n\tconst auto outcomeSet = !!outcome;\n\tconst auto outcomeNanoTon = outcomeSet ? outcome.nanoTon : 0;\n\tconst auto outcomeStakeNanoTon = outcomeSet ? outcome.stakeNanoTon : 0;\n\tconst auto outcomeValue = _dice->value();\n\tif (_outcomeSet == outcomeSet\n\t\t&& _outcomeNanoTon == outcomeNanoTon\n\t\t&& _outcomeStakeNanoTon == outcomeStakeNanoTon\n\t\t&& _outcomeValue == outcomeValue) {\n\t\treturn false;\n\t}\n\t_outcomeSet = outcomeSet;\n\t_outcomeNanoTon = outcomeNanoTon;\n\t_outcomeStakeNanoTon = outcomeStakeNanoTon;\n\t_outcomeValue = outcomeValue;\n\tif (_outcomeSet) {\n\t\tupdateOutcomeMessage();\n\t}\n\treturn true;\n}\n\nvoid Dice::draw(Painter &p, const PaintContext &context, const QRect &r) {\n\tif (!_start) {\n\t\tif (const auto document = Lookup(_parent, _dice->emoji(), 0)) {\n\t\t\tconst auto skipPremiumEffect = false;\n\t\t\t_start.emplace(_parent, document, skipPremiumEffect);\n\t\t\t_start->setDiceIndex(_dice->emoji(), 0);\n\t\t\t_start->initSize();\n\t\t}\n\t}\n\tif (const auto value = _end ? 0 : _dice->value()) {\n\t\tif (const auto document = Lookup(_parent, _dice->emoji(), value)) {\n\t\t\tconst auto skipPremiumEffect = false;\n\t\t\t_end.emplace(_parent, document, skipPremiumEffect);\n\t\t\t_end->setDiceIndex(_dice->emoji(), value);\n\t\t\t_end->initSize();\n\t\t}\n\t}\n\tif (!_end) {\n\t\t_drawingEnd = false;\n\t}\n\tif (_drawingEnd) {\n\t\t_end->draw(p, context, r);\n\t\tif (!_outcomeLastPainted && _end->stoppedOnLastFrame()) {\n\t\t\t_outcomeLastPainted = true;\n\t\t\tif (_outcomeSet) {\n\t\t\t\tcrl::on_main(this, [=] {\n\t\t\t\t\tupdateOutcomeMessage();\n\t\t\t\t\t_parent->history()->owner().requestViewResize(_parent);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t} else if (_start) {\n\t\t_start->draw(p, context, r);\n\t\tif (_end\n\t\t\t&& _end->readyToDrawAnimationFrame()\n\t\t\t&& _start->atTheEnd()) {\n\t\t\t_drawingEnd = true;\n\t\t}\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_dice.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"history/view/media/history_view_media_unwrapped.h\"\n#include \"history/view/media/history_view_sticker.h\"\n\nnamespace Data {\nclass MediaDice;\n} // namespace Data\n\nnamespace HistoryView {\n\nclass Dice final\n\t: public UnwrappedMedia::Content\n\t, public base::has_weak_ptr {\npublic:\n\tDice(not_null<Element*> parent, not_null<Data::MediaDice*> dice);\n\t~Dice();\n\n\tQSize countOptimalSize() override;\n\tvoid draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &r) override;\n\n\tClickHandlerPtr link() override;\n\n\tbool hasHeavyPart() const override {\n\t\treturn (_start ? _start->hasHeavyPart() : false)\n\t\t\t|| (_end ? _end->hasHeavyPart() : false);\n\t}\n\tvoid unloadHeavyPart() override {\n\t\tif (_start) {\n\t\t\t_start->unloadHeavyPart();\n\t\t}\n\t\tif (_end) {\n\t\t\t_end->unloadHeavyPart();\n\t\t}\n\t}\n\n\tbool updateItemData() override;\n\nprivate:\n\tvoid updateOutcomeMessage();\n\n\tconst not_null<Element*> _parent;\n\tconst not_null<Data::MediaDice*> _dice;\n\tClickHandlerPtr _link;\n\tstd::optional<Sticker> _start;\n\tstd::optional<Sticker> _end;\n\tint64 _outcomeNanoTon = 0;\n\tint64 _outcomeStakeNanoTon = 0;\n\tint _outcomeValue = 0;\n\tmutable bool _showLastFrame = false;\n\tmutable bool _drawingEnd = false;\n\tbool _outcomeSet = false;\n\tbool _outcomeLastPainted = false;\n\tbool _outcomeStartedUnknown = false;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_document.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_document.h\"\n\n#include \"base/random.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"storage/localstorage.h\"\n#include \"main/main_session.h\"\n#include \"media/player/media_player_float.h\" // Media::Player::RoundPainter.\n#include \"media/audio/media_audio.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"core/click_handler_types.h\" // kDocumentFilenameTooltipProperty.\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_transcribe_button.h\"\n#include \"history/view/media/history_view_media_common.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/format_song_document_name.h\"\n#include \"ui/text/text_lottie_custom_emoji.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/rect.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_document_resolver.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"api/api_transcribes.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kAudioVoiceMsgUpdateView = crl::time(100);\n\n[[nodiscard]] QRect TTLRectFromInner(const QRect &inner) {\n\treturn QRect(\n\t\trect::right(inner)\n\t\t\t- st::dialogsTTLBadgeSize\n\t\t\t+ rect::m::sum::h(st::dialogsTTLBadgeInnerMargins)\n\t\t\t- st::dialogsTTLBadgeSkip.x(),\n\t\trect::bottom(inner)\n\t\t\t- st::dialogsTTLBadgeSize\n\t\t\t+ rect::m::sum::v(st::dialogsTTLBadgeInnerMargins)\n\t\t\t- st::dialogsTTLBadgeSkip.y(),\n\t\tst::dialogsTTLBadgeSize,\n\t\tst::dialogsTTLBadgeSize);\n}\n\n[[nodiscard]] HistoryView::TtlPaintCallback CreateTtlPaintCallback(\n\t\tstd::shared_ptr<rpl::lifetime> lifetime,\n\t\tFn<void()> update) {\n\tstruct State final {\n\t\tstd::unique_ptr<Lottie::Icon> start;\n\t\tstd::unique_ptr<Lottie::Icon> idle;\n\t\tbool started = false;\n\t};\n\tconst auto iconSize = Size(std::min(\n\t\tst::historyFileInPause.width(),\n\t\tst::historyFileInPause.height()));\n\tconst auto state = lifetime->make_state<State>();\n\t//state->start = Lottie::MakeIcon({\n\t//\t.name = u\"voice_ttl_start\"_q,\n\t//\t.color = &st::historyFileInIconFg,\n\t//\t.sizeOverride = iconSize,\n\t//});\n\tstate->idle = Lottie::MakeIcon({\n\t\t.name = u\"voice_ttl_idle\"_q,\n\t\t.color = &st::historyFileInIconFg,\n\t\t.sizeOverride = iconSize,\n\t});\n\n\tconst auto weak = std::weak_ptr(lifetime);\n\treturn [=](QPainter &p, QRect r, QColor c) {\n\t\tif (weak.expired()) {\n\t\t\treturn;\n\t\t}\n\t\t{\n\t\t\tconst auto &icon = state->idle;\n\t\t\tif (icon) {\n\t\t\t\ticon->paintInCenter(p, r, c);\n\t\t\t\tif (!icon->animating()) {\n\t\t\t\t\ticon->animate(update, 0, icon->framesCount());\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\t{\n\t\t\tconst auto &icon = state->start;\n\t\t\ticon->paintInCenter(p, r, c);\n\t\t\tif (!icon->animating()) {\n\t\t\t\tif (!state->started) {\n\t\t\t\t\ticon->animate(update, 0, icon->framesCount());\n\t\t\t\t\tstate->started = true;\n\t\t\t\t} else {\n\t\t\t\t\tstate->idle = Lottie::MakeIcon({\n\t\t\t\t\t\t.name = u\"voice_ttl_idle\"_q,\n\t\t\t\t\t\t.color = &st::historyFileInIconFg,\n\t\t\t\t\t\t.sizeOverride = iconSize,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n\nvoid FillThumbnailOverlay(\n\t\tQPainter &p,\n\t\tQRect rect,\n\t\tUi::BubbleRounding rounding,\n\t\tconst PaintContext &context) {\n\tusing Corner = Ui::BubbleCornerRounding;\n\tusing Radius = Ui::CachedCornerRadius;\n\tauto corners = Ui::CornersPixmaps();\n\tconst auto &st = context.st;\n\tconst auto lookup = [&](Corner corner) {\n\t\tswitch (corner) {\n\t\tcase Corner::None: return Radius::Small;\n\t\tcase Corner::Small: return Radius::ThumbSmall;\n\t\tcase Corner::Large: return Radius::ThumbLarge;\n\t\t}\n\t\tUnexpected(\"Corner value in FillThumbnailOverlay.\");\n\t};\n\tfor (auto i = 0; i != 4; ++i) {\n\t\tcorners.p[i] = st->msgSelectOverlayCorners(lookup(rounding[i])).p[i];\n\t}\n\tUi::FillComplexOverlayRect(p, rect, st->msgSelectOverlay(), corners);\n}\n\n[[nodiscard]] QString CleanTagSymbols(const QString &value) {\n\tauto result = QString();\n\tconst auto begin = value.begin(), end = value.end();\n\tauto from = begin;\n\tfor (auto ch = begin; ch != end; ++ch) {\n\t\tif (ch->isHighSurrogate()\n\t\t\t&& (ch + 1) != end\n\t\t\t&& (ch + 1)->isLowSurrogate()\n\t\t\t&& QChar::surrogateToUcs4(\n\t\t\t\tch->unicode(),\n\t\t\t\t(ch + 1)->unicode()) >= 0xe0000) {\n\t\t\tif (ch > from) {\n\t\t\t\tif (result.isEmpty()) {\n\t\t\t\t\tresult.reserve(value.size());\n\t\t\t\t}\n\t\t\t\tresult.append(from, ch - from);\n\t\t\t}\n\t\t\t++ch;\n\t\t\tfrom = ch + 1;\n\t\t}\n\t}\n\tif (from == begin) {\n\t\treturn value;\n\t} else if (end > from) {\n\t\tresult.append(from, end - from);\n\t}\n\treturn result;\n}\n\nvoid FillWaveform(VoiceData *roundData) {\n\tif (!roundData->waveform.empty()) {\n\t\treturn;\n\t}\n\tconst auto &size = ::Media::Player::kWaveformSamplesCount;\n\tauto randomBytes = bytes::vector(size);\n\tbase::RandomFill(randomBytes.data(), randomBytes.size());\n\troundData->waveform.resize(size);\n\tfor (auto i = 1; i < size; i += 2) {\n\t\tconst auto peak = uchar(randomBytes[i]) % 31;\n\t\troundData->waveform[i - 1] = char(std::max(\n\t\t\t0,\n\t\t\tpeak - (uchar(randomBytes[i - 1]) % 3 + 2)));\n\t\troundData->waveform[i] = char(peak);\n\t}\n\troundData->wavemax = *ranges::max_element(roundData->waveform);\n}\n\nvoid PaintWaveform(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst VoiceData *voiceData,\n\t\tint availableWidth,\n\t\tfloat64 progress,\n\t\tbool ttl,\n\t\tfloat64 hoverProgress = -1) {\n\tconst auto wf = [&]() -> const VoiceWaveform* {\n\t\tif (!voiceData) {\n\t\t\treturn nullptr;\n\t\t}\n\t\tif (voiceData->waveform.isEmpty()) {\n\t\t\treturn nullptr;\n\t\t} else if (voiceData->waveform.at(0) < 0) {\n\t\t\treturn nullptr;\n\t\t}\n\t\treturn &voiceData->waveform;\n\t}();\n\tif (ttl) {\n\t\tprogress = 1. - progress;\n\t}\n\tconst auto stm = context.messageStyle();\n\n\t// Rescale waveform by going in waveform.size * bar_count 1D grid.\n\tconst auto active = stm->msgWaveformActive;\n\tconst auto inactive = ttl ? stm->msgBg : stm->msgWaveformInactive;\n\tconst auto wfSize = wf\n\t\t? int(wf->size())\n\t\t: ::Media::Player::kWaveformSamplesCount;\n\tconst auto activeWidth = base::SafeRound(availableWidth * progress);\n\tconst auto hoverWidth = (hoverProgress >= 0)\n\t\t? base::SafeRound(availableWidth * hoverProgress)\n\t\t: -1.;\n\n\tconst auto &barWidth = st::msgWaveformBar;\n\tconst auto barCount = std::min(\n\t\tavailableWidth / (barWidth + st::msgWaveformSkip),\n\t\twfSize);\n\tconst auto barNormValue = (wf ? voiceData->wavemax : 0) + 1;\n\tconst auto maxDelta = st::msgWaveformMax - st::msgWaveformMin;\n\tp.setPen(Qt::NoPen);\n\tauto hq = PainterHighQualityEnabler(p);\n\tfor (auto i = 0, barLeft = 0, sum = 0, maxValue = 0; i < wfSize; ++i) {\n\t\tconst auto value = wf ? wf->at(i) : 0;\n\t\tif (sum + barCount < wfSize) {\n\t\t\tmaxValue = std::max(maxValue, value);\n\t\t\tsum += barCount;\n\t\t\tcontinue;\n\t\t}\n\t\t// Draw bar.\n\t\tsum = sum + barCount - wfSize;\n\t\tif (sum < (barCount + 1) / 2) {\n\t\t\tmaxValue = std::max(maxValue, value);\n\t\t}\n\t\tconst auto barValue = ((maxValue * maxDelta) + (barNormValue / 2))\n\t\t\t/ barNormValue;\n\t\tconst auto barHeight = st::msgWaveformMin + barValue;\n\t\tconst auto barTop = st::lineWidth + (st::msgWaveformMax - barValue) / 2.;\n\n\t\tif ((barLeft < activeWidth) && (barLeft + barWidth > activeWidth)) {\n\t\t\tconst auto leftWidth = activeWidth - barLeft;\n\t\t\tconst auto rightWidth = barWidth - leftWidth;\n\t\t\tp.fillRect(\n\t\t\t\tQRectF(barLeft, barTop, leftWidth, barHeight),\n\t\t\t\tactive);\n\t\t\tif (!ttl) {\n\t\t\t\tp.fillRect(\n\t\t\t\t\tQRectF(activeWidth, barTop, rightWidth, barHeight),\n\t\t\t\t\tinactive);\n\t\t\t}\n\t\t} else if (!ttl || barLeft < activeWidth) {\n\t\t\tconst auto &color = (barLeft >= activeWidth) ? inactive : active;\n\t\t\tp.fillRect(QRectF(barLeft, barTop, barWidth, barHeight), color);\n\t\t}\n\t\tif (hoverWidth >= 0) {\n\t\t\tconst auto hoverFrom = std::min(activeWidth, hoverWidth);\n\t\t\tconst auto hoverTo = std::max(activeWidth, hoverWidth);\n\t\t\tif (barLeft < hoverTo && barLeft + barWidth > hoverFrom) {\n\t\t\t\tconst auto left = std::max(double(barLeft), hoverFrom);\n\t\t\t\tconst auto right = std::min(\n\t\t\t\t\tdouble(barLeft + barWidth),\n\t\t\t\t\thoverTo);\n\t\t\t\tp.fillRect(\n\t\t\t\t\tQRectF(left, barTop, right - left, barHeight),\n\t\t\t\t\tanim::with_alpha(active->c, 0.30));\n\t\t\t}\n\t\t}\n\t\tbarLeft += barWidth + st::msgWaveformSkip;\n\n\t\tmaxValue = (sum < (barCount + 1) / 2) ? 0 : value;\n\t}\n}\n\n[[nodiscard]] int MaxStatusWidth(not_null<DocumentData*> document) {\n\tusing namespace Ui;\n\tauto result = 0;\n\tconst auto add = [&](const QString &text) {\n\t\taccumulate_max(result, st::normalFont->width(text));\n\t};\n\tadd(FormatDownloadText(document->size, document->size));\n\tconst auto duration = document->duration() / 1000;\n\tif (document->song()) {\n\t\tadd(FormatPlayedText(duration, duration));\n\t\tadd(FormatDurationAndSizeText(duration, document->size));\n\t} else if (document->voice() ? document->voice() : document->round()) {\n\t\tadd(FormatPlayedText(duration, duration));\n\t\tadd(FormatDurationAndSizeText(duration, document->size));\n\t} else if (document->isVideoFile()) {\n\t\tadd(FormatDurationAndSizeText(duration, document->size));\n\t} else {\n\t\tadd(FormatSizeText(document->size));\n\t}\n\treturn result;\n}\n\n} // namespace\n\nDocument::Document(\n\tnot_null<Element*> parent,\n\tnot_null<HistoryItem*> realParent,\n\tnot_null<DocumentData*> document)\n: File(parent, realParent)\n, _data(document) {\n\tconst auto isRound = _data->isVideoMessage();\n\tif (isRound) {\n\t\tconst auto &entry = _data->session().api().transcribes().entry(\n\t\t\trealParent);\n\t\t_transcribedRound = entry.shown;\n\t}\n\n\tcreateComponents();\n\tif (const auto named = Get<HistoryDocumentNamed>()) {\n\t\tfillNamedFromData(named);\n\t\t_tooltipFilename.setTooltipText(named->name.toString());\n\t}\n\n\tif ((_data->isVoiceMessage() || isRound)\n\t\t&& _parent->data()->media()->ttlSeconds()) {\n\t\tconst auto fullId = _realParent->fullId();\n\t\tif (_parent->delegate()->elementContext() == Context::TTLViewer) {\n\t\t\tauto lifetime = std::make_shared<rpl::lifetime>();\n\t\t\tTTLVoiceStops(fullId) | rpl::on_next([=]() mutable {\n\t\t\t\tif (lifetime) {\n\t\t\t\t\tbase::take(lifetime)->destroy();\n\t\t\t\t}\n\t\t\t}, *lifetime);\n\t\t\t_drawTtl = CreateTtlPaintCallback(lifetime, [=] { repaint(); });\n\t\t} else if (!_parent->data()->out()) {\n\t\t\tconst auto &data = &_parent->data()->history()->owner();\n\t\t\t_parent->data()->removeFromSharedMediaIndex();\n\t\t\tsetDocumentLinks(_data, realParent, [=] {\n\t\t\t\t_openl = nullptr;\n\n\t\t\t\tauto lifetime = std::make_shared<rpl::lifetime>();\n\t\t\t\tTTLVoiceStops(fullId) | rpl::on_next([=]() mutable {\n\t\t\t\t\tif (lifetime) {\n\t\t\t\t\t\tbase::take(lifetime)->destroy();\n\t\t\t\t\t}\n\t\t\t\t\tif (const auto item = data->message(fullId)) {\n\t\t\t\t\t\t// Destroys this.\n\t\t\t\t\t\titem->clearMediaAsExpired();\n\t\t\t\t\t}\n\t\t\t\t}, *lifetime);\n\n\t\t\t\treturn false;\n\t\t\t});\n\t\t} else {\n\t\t\tsetDocumentLinks(_data, realParent);\n\t\t}\n\t} else {\n\t\tsetDocumentLinks(_data, realParent);\n\t}\n\n\tsetStatusSize(Ui::FileStatusSizeReady);\n}\n\nDocument::~Document() {\n\tif (_dataMedia) {\n\t\t_data->owner().keepAlive(base::take(_dataMedia));\n\t\t_parent->checkHeavyPart();\n\t}\n}\n\nfloat64 Document::dataProgress() const {\n\tensureDataMediaCreated();\n\treturn _dataMedia->progress();\n}\n\nbool Document::dataFinished() const {\n\treturn !_data->loading()\n\t\t&& (!_data->uploading() || _data->waitingForAlbum());\n}\n\nbool Document::dataLoaded() const {\n\tensureDataMediaCreated();\n\treturn _dataMedia->loaded();\n}\n\nvoid Document::createComponents() {\n\tuint64 mask = 0;\n\tif (_data->isVoiceMessage() || _transcribedRound) {\n\t\tmask |= HistoryDocumentVoice::Bit();\n\t} else {\n\t\tmask |= HistoryDocumentNamed::Bit();\n\t\tif (_data->hasThumbnail() && !_data->isSong()) {\n\t\t\t_data->loadThumbnail(_realParent->fullId());\n\t\t\tmask |= HistoryDocumentThumbed::Bit();\n\t\t} else if (_data->isSvgImage()) {\n\t\t\tmask |= HistoryDocumentThumbed::Bit();\n\t\t}\n\t}\n\tUpdateComponents(mask);\n\tif (const auto thumbed = Get<HistoryDocumentThumbed>()) {\n\t\tthumbed->linksavel = std::make_shared<DocumentSaveClickHandler>(\n\t\t\t_data,\n\t\t\t_realParent->fullId());\n\t\tthumbed->linkopenwithl = std::make_shared<DocumentOpenWithClickHandler>(\n\t\t\t_data,\n\t\t\t_realParent->fullId());\n\t\tthumbed->linkcancell = std::make_shared<DocumentCancelClickHandler>(\n\t\t\t_data,\n\t\t\tcrl::guard(this, [=](FullMsgId id) {\n\t\t\t\t_parent->delegate()->elementCancelUpload(id);\n\t\t\t}),\n\t\t\t_realParent->fullId());\n\t}\n\tif (const auto voice = Get<HistoryDocumentVoice>()) {\n\t\tvoice->seekl = !_parent->data()->media()->ttlSeconds()\n\t\t\t? std::make_shared<VoiceSeekClickHandler>(_data, [](FullMsgId) {})\n\t\t\t: nullptr;\n\t\tif (_transcribedRound) {\n\t\t\tvoice->round = std::make_unique<::Media::Player::RoundPainter>(\n\t\t\t\t_realParent);\n\t\t}\n\t}\n}\n\nvoid Document::fillNamedFromData(not_null<HistoryDocumentNamed*> named) {\n\tnamed->name.setText(\n\t\tst::semiboldTextStyle,\n\t\tCleanTagSymbols(Ui::Text::FormatSongNameFor(_data).string()));\n}\n\nQSize Document::countOptimalSize() {\n\tauto hasTranscribe = false;\n\tconst auto voice = Get<HistoryDocumentVoice>();\n\tif (voice) {\n\t\tconst auto history = _realParent->history();\n\t\tconst auto session = &history->session();\n\t\tconst auto transcribes = &session->api().transcribes();\n\t\tif (_parent->data()->media()->ttlSeconds()\n\t\t\t|| _realParent->isScheduled()\n\t\t\t|| _realParent->isAdminLogEntry()\n\t\t\t|| (!session->premium()\n\t\t\t\t&& !transcribes->freeFor(_realParent)\n\t\t\t\t&& !transcribes->trialsSupport())\n\t\t\t|| (!session->premium()\n\t\t\t\t&& _data->duration() > transcribes->trialsMaxLengthMs())) {\n\t\t\tvoice->transcribe = nullptr;\n\t\t\tvoice->transcribeText = {};\n\t\t} else {\n\t\t\tconst auto creating = !voice->transcribe;\n\t\t\tif (creating) {\n\t\t\t\tvoice->transcribe = std::make_unique<TranscribeButton>(\n\t\t\t\t\t_realParent,\n\t\t\t\t\tfalse);\n\t\t\t}\n\t\t\tconst auto &entry = transcribes->entry(_realParent);\n\t\t\tconst auto update = [=] { repaint(); };\n\t\t\tvoice->transcribe->setLoading(\n\t\t\t\tentry.shown && (entry.requestId || entry.pending));\n\t\t\tconst auto pending = entry.pending;\n\t\t\tauto descriptor = pending\n\t\t\t\t? Lottie::IconDescriptor{\n\t\t\t\t\t.name = u\"transcribe_loading\"_q,\n\t\t\t\t\t.color = &st::attentionButtonFg, // Any contrast.\n\t\t\t\t\t.sizeOverride = Size(st::historyTranscribeLoadingSize),\n\t\t\t\t\t.colorizeUsingAlpha = true,\n\t\t\t\t}\n\t\t\t\t: Lottie::IconDescriptor();\n\t\t\tauto text = (entry.requestId || !entry.shown)\n\t\t\t\t? TextWithEntities()\n\t\t\t\t: entry.toolong\n\t\t\t\t? tr::italic(tr::lng_audio_transcribe_long(tr::now))\n\t\t\t\t: entry.failed\n\t\t\t\t? tr::italic(tr::lng_attach_failed(tr::now))\n\t\t\t\t: TextWithEntities{ entry.result }.append(\n\t\t\t\t\tpending\n\t\t\t\t\t\t? Ui::Text::LottieEmoji(descriptor)\n\t\t\t\t\t\t: TextWithEntities());\n\t\t\tvoice->transcribe->setOpened(\n\t\t\t\t!text.empty(),\n\t\t\t\tcreating ? Fn<void()>() : update);\n\t\t\tif (text.empty()) {\n\t\t\t\tvoice->transcribeText = {};\n\t\t\t} else {\n\t\t\t\tconst auto minResizeWidth = st::minPhotoSize\n\t\t\t\t\t- st::msgPadding.left()\n\t\t\t\t\t- st::msgPadding.right();\n\t\t\t\tvoice->transcribeText = Ui::Text::String(minResizeWidth);\n\t\t\t\tvoice->transcribeText.setMarkedText(\n\t\t\t\t\tst::messageTextStyle,\n\t\t\t\t\ttext,\n\t\t\t\t\tkMarkupTextOptions,\n\t\t\t\t\tpending\n\t\t\t\t\t\t? Ui::Text::LottieEmojiContext(std::move(descriptor))\n\t\t\t\t\t\t: Ui::Text::MarkedContext());\n\t\t\t\thasTranscribe = true;\n\t\t\t\tif (const auto skipBlockWidth = _parent->hasVisibleText()\n\t\t\t\t\t? 0\n\t\t\t\t\t: _parent->skipBlockWidth()) {\n\t\t\t\t\tvoice->transcribeText.updateSkipBlock(\n\t\t\t\t\t\tskipBlockWidth,\n\t\t\t\t\t\t_parent->skipBlockHeight());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tauto thumbed = Get<HistoryDocumentThumbed>();\n\tconst auto &st = thumbed ? st::msgFileThumbLayout : st::msgFileLayout;\n\tif (thumbed) {\n\t\tif (_data->hasThumbnail()) {\n\t\t\tconst auto &location = _data->thumbnailLocation();\n\t\t\tauto tw = style::ConvertScale(location.width());\n\t\t\tauto th = style::ConvertScale(location.height());\n\t\t\tif (tw > th) {\n\t\t\t\tthumbed->thumbw = (tw * st.thumbSize) / th;\n\t\t\t} else {\n\t\t\t\tthumbed->thumbw = st.thumbSize;\n\t\t\t}\n\t\t} else {\n\t\t\tthumbed->thumbw = st.thumbSize;\n\t\t}\n\t}\n\n\tauto maxWidth = st::msgFileMinWidth;\n\n\tconst auto tleft = st.padding.left() + st.thumbSize + st.thumbSkip;\n\tconst auto tright = st.padding.right();\n\tif (thumbed) {\n\t\taccumulate_max(maxWidth, tleft + MaxStatusWidth(_data) + tright);\n\t} else {\n\t\tauto unread = (_data->isVoiceMessage() || _transcribedRound)\n\t\t\t? (st::mediaUnreadSkip + st::mediaUnreadSize)\n\t\t\t: 0;\n\t\taccumulate_max(maxWidth, tleft + MaxStatusWidth(_data) + unread + _parent->skipBlockWidth() + st::msgPadding.right());\n\t}\n\n\tif (const auto named = Get<HistoryDocumentNamed>()) {\n\t\taccumulate_max(maxWidth, tleft + named->name.maxWidth() + tright);\n\t\taccumulate_min(maxWidth, st::msgMaxWidth);\n\t}\n\tif (voice) {\n\t\tconst auto maxWaveformWidth = ::Media::Player::kWaveformSamplesCount *\n\t\t\t(st::msgWaveformBar + st::msgWaveformSkip);\n\t\tconst auto transcribeWidth = voice->transcribe\n\t\t\t? (voice->transcribe->size().width() + st::historyTranscribeSkip)\n\t\t\t: 0;\n\t\taccumulate_max(\n\t\t\tmaxWidth,\n\t\t\tmaxWaveformWidth\n\t\t\t\t+ rect::m::sum::h(st.padding)\n\t\t\t\t+ st.thumbSize\n\t\t\t\t+ st.thumbSkip\n\t\t\t\t+ transcribeWidth);\n\t}\n\n\tauto minHeight = st.padding.top() + st.thumbSize + st.padding.bottom();\n\tif (isBubbleBottom() && !hasTranscribe) {\n\t\tif (const auto link = thumbedLinkMaxWidth()) {\n\t\t\taccumulate_max(\n\t\t\t\tmaxWidth,\n\t\t\t\t(tleft\n\t\t\t\t\t+ link\n\t\t\t\t\t+ st.thumbSkip\n\t\t\t\t\t+ _parent->bottomInfoFirstLineWidth()\n\t\t\t\t\t+ tright));\n\t\t}\n\t}\n\tif (!isBubbleTop()) {\n\t\tminHeight -= st::msgFileTopMinus;\n\t}\n\n\tif (hasTranscribe) {\n\t\tauto captionw = maxWidth\n\t\t\t- st::msgPadding.left()\n\t\t\t- st::msgPadding.right();\n\t\tminHeight += voice->transcribeText.countHeight(captionw);\n\t\tif (isBubbleBottom()) {\n\t\t\tminHeight += st::msgPadding.bottom();\n\t\t}\n\t}\n\treturn { maxWidth, minHeight };\n}\n\nQSize Document::countCurrentSize(int newWidth) {\n\tconst auto captioned = Get<HistoryDocumentCaptioned>();\n\tconst auto voice = Get<HistoryDocumentVoice>();\n\tconst auto hasTranscribe = voice && !voice->transcribeText.isEmpty();\n\tconst auto thumbed = Get<HistoryDocumentThumbed>();\n\tconst auto &st = thumbed ? st::msgFileThumbLayout : st::msgFileLayout;\n\tif (!captioned && !hasTranscribe) {\n\t\tauto result = File::countCurrentSize(newWidth);\n\t\tif (isBubbleBottom()) {\n\t\t\tconst auto thumbedWidth = thumbedLinkMaxWidth();\n\t\t\tconst auto statusWidth = thumbedWidth\n\t\t\t\t? 0\n\t\t\t\t: st::normalFont->width(_statusText);\n\t\t\tif (thumbedWidth || statusWidth) {\n\t\t\t\tconst auto needed = st.padding.left()\n\t\t\t\t\t+ (thumbedWidth\n\t\t\t\t\t\t? st.thumbSize + st.thumbSkip\n\t\t\t\t\t\t: st::msgFileLayout.thumbSize\n\t\t\t\t\t\t\t+ st::mediaUnreadSkip)\n\t\t\t\t\t+ (thumbedWidth + statusWidth)\n\t\t\t\t\t+ st.thumbSkip\n\t\t\t\t\t+ (_realParent->hasUnreadMediaFlag()\n\t\t\t\t\t\t? st::mediaUnreadSkip + st::mediaUnreadSize\n\t\t\t\t\t\t: 0)\n\t\t\t\t\t+ _parent->bottomInfoFirstLineWidth()\n\t\t\t\t\t+ st.padding.right();\n\t\t\t\tif (result.width() < needed) {\n\t\t\t\t\tresult.setHeight(result.height()\n\t\t\t\t\t\t+ st::msgDateFont->height\n\t\t\t\t\t\t- st::msgDateDelta.y());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\taccumulate_min(newWidth, maxWidth());\n\tauto newHeight = st.padding.top() + st.thumbSize + st.padding.bottom();\n\tif (!isBubbleTop()) {\n\t\tnewHeight -= st::msgFileTopMinus;\n\t}\n\tauto captionw = newWidth - st::msgPadding.left() - st::msgPadding.right();\n\tif (hasTranscribe) {\n\t\tnewHeight += voice->transcribeText.countHeight(captionw);\n\t\tif (captioned) {\n\t\t\tnewHeight += st::mediaCaptionSkip;\n\t\t} else if (isBubbleBottom()) {\n\t\t\tnewHeight += st::msgPadding.bottom();\n\t\t}\n\t}\n\tif (captioned) {\n\t\tnewHeight += captioned->caption.countHeight(captionw);\n\t\tif (isBubbleBottom()) {\n\t\t\tnewHeight += st::msgPadding.bottom();\n\t\t}\n\t}\n\n\treturn { newWidth, newHeight };\n}\n\nvoid Document::draw(Painter &p, const PaintContext &context) const {\n\tdraw(p, context, width(), LayoutMode::Full, adjustedBubbleRounding());\n}\n\nvoid Document::draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint width,\n\t\tLayoutMode mode,\n\t\tUi::BubbleRounding outsideRounding) const {\n\tif (width < st::msgPadding.left() + st::msgPadding.right() + 1) return;\n\n\tensureDataMediaCreated();\n\n\tconst auto cornerDownload = downloadInCorner();\n\n\tif (!_dataMedia->canBePlayed()) {\n\t\t_dataMedia->automaticLoad(_realParent->fullId(), _realParent);\n\t}\n\tbool loaded = dataLoaded(), displayLoading = _data->displayLoading();\n\tconst auto sti = context.imageStyle();\n\tconst auto stm = context.messageStyle();\n\n\tint captionw = width - st::msgPadding.left() - st::msgPadding.right();\n\n\tif (displayLoading) {\n\t\tensureAnimation();\n\t\tif (!_animation->radial.animating()) {\n\t\t\t_animation->radial.start(dataProgress());\n\t\t}\n\t}\n\tconst auto showPause = updateStatusText();\n\tconst auto radial = isRadialAnimation();\n\n\tconst auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus;\n\tconst auto thumbed = Get<HistoryDocumentThumbed>();\n\tconst auto &st = (mode == LayoutMode::Full)\n\t\t? (thumbed ? st::msgFileThumbLayout : st::msgFileLayout)\n\t\t: (thumbed ? st::msgFileThumbLayoutGrouped : st::msgFileLayoutGrouped);\n\tconst auto nameleft = st.padding.left() + st.thumbSize + st.thumbSkip;\n\tconst auto nametop = st.nameTop - topMinus;\n\tconst auto nameright = st.padding.right();\n\tconst auto statustop = st.statusTop - topMinus;\n\tconst auto linktop = st.linkTop - topMinus;\n\tconst auto bottom = st.padding.top() + st.thumbSize + st.padding.bottom() - topMinus;\n\tconst auto rthumb = style::rtlrect(st.padding.left(), st.padding.top() - topMinus, st.thumbSize, st.thumbSize, width);\n\tconst auto innerSize = st::msgFileLayout.thumbSize;\n\tconst auto inner = QRect(rthumb.x() + (rthumb.width() - innerSize) / 2, rthumb.y() + (rthumb.height() - innerSize) / 2, innerSize, innerSize);\n\tconst auto radialOpacity = radial ? _animation->radial.opacity() : 1.;\n\tif (thumbed) {\n\t\tconst auto rounding = thumbRounding(mode, outsideRounding);\n\t\tvalidateThumbnail(thumbed, st.thumbSize, rounding);\n\t\tp.drawImage(rthumb, thumbed->thumbnail);\n\t\tif (context.selected()) {\n\t\t\tFillThumbnailOverlay(p, rthumb, rounding, context);\n\t\t}\n\n\t\tif (radial || (!loaded && !_data->loading()) || _data->waitingForAlbum()) {\n\t\t\tconst auto backOpacity = (loaded && !_data->uploading()) ? radialOpacity : 1.;\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(sti->msgDateImgBg);\n\t\t\tp.setOpacity(backOpacity * p.opacity());\n\n\t\t\t{\n\t\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\t\tp.drawEllipse(inner);\n\t\t\t}\n\n\t\t\tconst auto &icon = _data->waitingForAlbum()\n\t\t\t\t? sti->historyFileThumbWaiting\n\t\t\t\t: (radial || _data->loading())\n\t\t\t\t? sti->historyFileThumbCancel\n\t\t\t\t: sti->historyFileThumbDownload;\n\t\t\tconst auto previous = _data->waitingForAlbum()\n\t\t\t\t? &sti->historyFileThumbCancel\n\t\t\t\t: nullptr;\n\t\t\tp.setOpacity(backOpacity);\n\t\t\tif (previous && radialOpacity > 0. && radialOpacity < 1.) {\n\t\t\t\tPaintInterpolatedIcon(p, icon, *previous, radialOpacity, inner);\n\t\t\t} else {\n\t\t\t\ticon.paintInCenter(p, inner);\n\t\t\t}\n\t\t\tp.setOpacity(1.);\n\t\t\tif (radial) {\n\t\t\t\tQRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));\n\t\t\t\t_animation->radial.draw(p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg);\n\t\t\t}\n\t\t}\n\n\t\tif (_data->status != FileUploadFailed) {\n\t\t\tconst auto &lnk = (_data->loading() || _data->uploading())\n\t\t\t\t? thumbed->linkcancell\n\t\t\t\t: dataLoaded()\n\t\t\t\t? thumbed->linkopenwithl\n\t\t\t\t: thumbed->linksavel;\n\t\t\tbool over = ClickHandler::showAsActive(lnk);\n\t\t\tp.setFont(over ? st::semiboldFont->underline() : st::semiboldFont);\n\t\t\tp.setPen(stm->msgFileThumbLinkFg);\n\t\t\tp.drawTextLeft(nameleft, linktop, width, thumbed->link, thumbed->linkw);\n\t\t}\n\t} else {\n\t\tp.setPen(Qt::NoPen);\n\n\t\tconst auto hasTtlBadge = _parent->data()->media()\n\t\t\t&& _parent->data()->media()->ttlSeconds()\n\t\t\t&& _openl;\n\t\tconst auto ttlRect = hasTtlBadge ? TTLRectFromInner(inner) : QRect();\n\n\t\tconst auto coverDrawn = _data->isSongWithCover()\n\t\t\t&& DrawThumbnailAsSongCover(\n\t\t\t\tp,\n\t\t\t\tcontext.st->songCoverOverlayFg(),\n\t\t\t\t_dataMedia,\n\t\t\t\tinner,\n\t\t\t\tcontext.selected());\n\t\tif (!coverDrawn) {\n\t\t\tif (_transcribedRound) {\n\t\t\t\tif (const auto voice = Get<HistoryDocumentVoice>()) {\n\t\t\t\t\tif (const auto &round = voice->round) {\n\t\t\t\t\t\tif (round->fillFrame(inner.size())) {\n\t\t\t\t\t\t\tp.drawImage(inner.topLeft(), round->frame());\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tDrawThumbnailAsSongCover(\n\t\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\t\tst::transparent,\n\t\t\t\t\t\t\t\t_dataMedia,\n\t\t\t\t\t\t\t\tinner,\n\t\t\t\t\t\t\t\tcontext.selected());\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\tp.setBrush(stm->msgFileBg);\n\t\t\t\tp.drawEllipse(inner);\n\t\t\t}\n\t\t}\n\n\t\tconst auto &icon = [&]() -> const style::icon& {\n\t\t\tif (_data->waitingForAlbum()) {\n\t\t\t\treturn _data->isSongWithCover()\n\t\t\t\t\t? sti->historyFileThumbWaiting\n\t\t\t\t\t: stm->historyFileWaiting;\n\t\t\t} else if (!cornerDownload\n\t\t\t\t&& (_data->loading() || _data->uploading())) {\n\t\t\t\treturn _data->isSongWithCover()\n\t\t\t\t\t? sti->historyFileThumbCancel\n\t\t\t\t\t: stm->historyFileCancel;\n\t\t\t} else if (showPause) {\n\t\t\t\treturn _data->isSongWithCover()\n\t\t\t\t\t? sti->historyFileThumbPause\n\t\t\t\t\t: stm->historyFilePause;\n\t\t\t} else if (loaded || _dataMedia->canBePlayed()) {\n\t\t\t\treturn _dataMedia->canBePlayed()\n\t\t\t\t\t? (_data->isSongWithCover()\n\t\t\t\t\t\t? sti->historyFileThumbPlay\n\t\t\t\t\t\t: stm->historyFilePlay)\n\t\t\t\t\t: _data->isImage()\n\t\t\t\t\t? stm->historyFileImage\n\t\t\t\t\t: stm->historyFileDocument;\n\t\t\t} else {\n\t\t\t\treturn _data->isSongWithCover()\n\t\t\t\t\t? sti->historyFileThumbDownload\n\t\t\t\t\t: stm->historyFileDownload;\n\t\t\t}\n\t\t}();\n\t\tconst auto previous = _data->waitingForAlbum()\n\t\t\t? &stm->historyFileCancel\n\t\t\t: nullptr;\n\n\t\tconst auto paintContent = [&](QPainter &q) {\n\t\t\tconstexpr auto kPenWidth = 1.5;\n\t\t\tif (_drawTtl) {\n\t\t\t\t_drawTtl(q, inner, context.st->historyFileInIconFg()->c);\n\n\t\t\t\tconst auto voice = Get<HistoryDocumentVoice>();\n\t\t\t\tconst auto progress = (voice && voice->playback)\n\t\t\t\t\t? voice->playback->progress.current()\n\t\t\t\t\t: 0.;\n\n\t\t\t\tif (progress > 0.) {\n\t\t\t\t\tauto pen = stm->msgBg->p;\n\t\t\t\t\tpen.setWidthF(style::ConvertScaleExact(kPenWidth));\n\t\t\t\t\tpen.setCapStyle(Qt::RoundCap);\n\t\t\t\t\tq.setPen(pen);\n\n\t\t\t\t\tconst auto from = arc::kQuarterLength;\n\t\t\t\t\tconst auto len = std::round(arc::kFullLength\n\t\t\t\t\t\t* (1. - progress));\n\t\t\t\t\tconst auto stepInside = pen.widthF() * 2;\n\t\t\t\t\tauto hq = PainterHighQualityEnabler(q);\n\t\t\t\t\tq.drawArc(inner - Margins(stepInside), from, len);\n\t\t\t\t}\n\t\t\t} else if (previous && radialOpacity > 0. && radialOpacity < 1.) {\n\t\t\t\tPaintInterpolatedIcon(q, icon, *previous, radialOpacity, inner);\n\t\t\t} else {\n\t\t\t\ticon.paintInCenter(q, inner);\n\t\t\t}\n\n\t\t\tif (radial && !cornerDownload) {\n\t\t\t\tQRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));\n\t\t\t\t_animation->radial.draw(q, rinner, st::msgFileRadialLine, stm->historyFileRadialFg);\n\t\t\t}\n\t\t\tif (hasTtlBadge) {\n\t\t\t\t{\n\t\t\t\t\tauto hq = PainterHighQualityEnabler(q);\n\t\t\t\t\tp.setBrush(stm->msgFileBg);\n\t\t\t\t\tq.setPen(Qt::NoPen);\n\t\t\t\t\tp.drawEllipse(ttlRect);\n\t\t\t\t\tauto pen = stm->msgBg->p;\n\t\t\t\t\tpen.setWidthF(style::ConvertScaleExact(kPenWidth));\n\t\t\t\t\tq.setPen(pen);\n\t\t\t\t\tq.setBrush(Qt::NoBrush);\n\t\t\t\t\tq.drawEllipse(ttlRect);\n\t\t\t\t}\n\t\t\t\tstm->historyVoiceMessageTTL.paintInCenter(q, ttlRect);\n\t\t\t}\n\t\t};\n\t\tif (_data->isSongWithCover() || !usesBubblePattern(context)) {\n\t\t\tpaintContent(p);\n\t\t} else {\n\t\t\tUi::PaintPatternBubblePart(\n\t\t\t\tp,\n\t\t\t\tcontext.viewport,\n\t\t\t\tcontext.bubblesPattern->pixmap,\n\t\t\t\thasTtlBadge ? inner.united(ttlRect) : inner,\n\t\t\t\tpaintContent,\n\t\t\t\t_iconCache);\n\t\t}\n\n\t\tdrawCornerDownload(p, context, mode);\n\t}\n\tauto namewidth = width - nameleft - nameright;\n\tauto statuswidth = namewidth;\n\n\tauto voiceStatusOverride = QString();\n\tconst auto voice = Get<HistoryDocumentVoice>();\n\tif (voice) {\n\t\tensureDataMediaCreated();\n\n\t\t{\n\t\t\tconst auto voiceData = _data->isVideoMessage()\n\t\t\t\t? _data->round()\n\t\t\t\t: _data->voice();\n\t\t\tif (voiceData && voiceData->waveform.isEmpty()) {\n\t\t\t\tif (loaded) {\n\t\t\t\t\tLocal::countVoiceWaveform(_dataMedia.get());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst auto progress = [&] {\n\t\t\tif (!context.outbg\n\t\t\t\t&& !voice->playback\n\t\t\t\t&& _realParent->hasUnreadMediaFlag()) {\n\t\t\t\treturn 1.;\n\t\t\t}\n\t\t\tif (voice->seeking()) {\n\t\t\t\treturn voice->seekingCurrent();\n\t\t\t} else if (voice->playback) {\n\t\t\t\treturn voice->playback->progress.current();\n\t\t\t}\n\t\t\treturn 0.;\n\t\t}();\n\t\tif (voice->seeking()) {\n\t\t\tvoiceStatusOverride = Ui::FormatPlayedText(\n\t\t\t\tbase::SafeRound(progress * voice->lastDurationMs) / 1000,\n\t\t\t\tvoice->lastDurationMs / 1000);\n\t\t}\n\t\tif (voice->transcribe) {\n\t\t\tconst auto size = voice->transcribe->size();\n\t\t\tnamewidth -= st::historyTranscribeSkip + size.width();\n\t\t\tconst auto x = nameleft + namewidth + st::historyTranscribeSkip;\n\t\t\tconst auto y = st.padding.top() - topMinus;\n\t\t\tvoice->transcribe->paint(p, x, y, context);\n\t\t}\n\t\tp.save();\n\t\tp.translate(nameleft, st.padding.top() - topMinus);\n\n\t\tif (_transcribedRound) {\n\t\t\tFillWaveform(_data->round());\n\t\t}\n\t\tconst auto inTTLViewer = _parent->delegate()->elementContext()\n\t\t\t== Context::TTLViewer;\n\t\tPaintWaveform(p,\n\t\t\tcontext,\n\t\t\t_transcribedRound ? _data->round() : _data->voice(),\n\t\t\tnamewidth + st::msgWaveformSkip,\n\t\t\tprogress,\n\t\t\tinTTLViewer,\n\t\t\t_voiceHoverProgress);\n\t\tp.restore();\n\t} else if (const auto named = Get<HistoryDocumentNamed>()) {\n\t\tp.setPen(stm->historyFileNameFg);\n\t\tnamed->name.draw(p, {\n\t\t\t.position = QPoint(nameleft, nametop),\n\t\t\t.outerWidth = width,\n\t\t\t.availableWidth = namewidth,\n\t\t\t.elisionLines = 1,\n\t\t\t.elisionMiddle = true,\n\t\t});\n\t\t_tooltipFilename.setElided(namewidth < named->name.maxWidth());\n\t}\n\n\tauto statusText = voiceStatusOverride.isEmpty() ? _statusText : voiceStatusOverride;\n\tp.setFont(st::normalFont);\n\tp.setPen(stm->mediaFg);\n\tp.drawTextLeft(nameleft, statustop, width, statusText);\n\n\tif (_realParent->hasUnreadMediaFlag()) {\n\t\tauto w = st::normalFont->width(statusText);\n\t\tif (w + st::mediaUnreadSkip + st::mediaUnreadSize <= statuswidth) {\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(stm->msgFileBg);\n\n\t\t\t{\n\t\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\t\tp.drawEllipse(style::rtlrect(nameleft + w + st::mediaUnreadSkip, statustop + st::mediaUnreadTop, st::mediaUnreadSize, st::mediaUnreadSize, width));\n\t\t\t}\n\t\t}\n\t}\n\n\tauto selection = context.selection;\n\tauto captiontop = bottom;\n\tif (voice && !voice->transcribeText.isEmpty()) {\n\t\tp.setPen(stm->historyTextFg);\n\t\tvoice->transcribeText.draw(p, st::msgPadding.left(), bottom, captionw, style::al_left, 0, -1, selection);\n\t\tcaptiontop += voice->transcribeText.countHeight(captionw) + st::mediaCaptionSkip;\n\t\tselection = HistoryView::UnshiftItemSelection(selection, voice->transcribeText);\n\t}\n\tif (const auto captioned = Get<HistoryDocumentCaptioned>()) {\n\t\tp.setPen(stm->historyTextFg);\n\t\t_parent->prepareCustomEmojiPaint(p, context, captioned->caption);\n\n\t\tauto highlightRequest = context.computeHighlightCache();\n\t\tcaptioned->caption.draw(p, {\n\t\t\t.position = { st::msgPadding.left(), captiontop },\n\t\t\t.availableWidth = captionw,\n\t\t\t.palette = &stm->textPalette,\n\t\t\t.pre = stm->preCache.get(),\n\t\t\t.blockquote = context.quoteCache(\n\t\t\t\tparent()->contentColorCollectible(),\n\t\t\t\tparent()->contentColorIndex()),\n\t\t\t.colors = context.st->highlightColors(),\n\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t.now = context.now,\n\t\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t\t.selection = selection,\n\t\t\t.highlight = highlightRequest ? &*highlightRequest : nullptr,\n\t\t\t.useFullWidth = true,\n\t\t});\n\t}\n}\n\nUi::BubbleRounding Document::thumbRounding(\n\t\tLayoutMode mode,\n\t\tUi::BubbleRounding outsideRounding) const {\n\tusing Corner = Ui::BubbleCornerRounding;\n\tif (mode != LayoutMode::Grouped && _parent->media() != this) {\n\t\treturn Ui::BubbleRounding(); // In a WebPage preview.\n\t}\n\tconst auto hasCaption = Has<HistoryDocumentCaptioned>();\n\tconst auto adjust = [&](Corner already, bool skip = false) {\n\t\treturn (already == Corner::Large && !skip)\n\t\t\t? Corner::Large\n\t\t\t: Corner::Small;\n\t};\n\tauto result = Ui::BubbleRounding();\n\tresult.topLeft = adjust(outsideRounding.topLeft);\n\tresult.bottomLeft = adjust(outsideRounding.bottomLeft, hasCaption);\n\tresult.topRight = result.bottomRight = Corner::Small;\n\treturn result;\n}\n\nvoid Document::validateThumbnail(\n\t\tnot_null<const HistoryDocumentThumbed*> thumbed,\n\t\tint size,\n\t\tUi::BubbleRounding rounding) const {\n\tconst auto good = _data->isSvgImage()\n\t\t? _dataMedia->goodThumbnail()\n\t\t: nullptr;\n\tconst auto normal = good ? good : _dataMedia->thumbnail();\n\tconst auto blurred = _dataMedia->thumbnailInline();\n\tif (!normal && !blurred) {\n\t\tif (_data->isSvgImage()) {\n\t\t\t_dataMedia->goodThumbnailWanted();\n\t\t\tData::DocumentMedia::CheckGoodThumbnail(_data);\n\t\t}\n\t\treturn;\n\t}\n\tconst auto outer = QSize(size, size);\n\tif ((thumbed->thumbnail.size() == outer * style::DevicePixelRatio())\n\t\t&& (thumbed->blurred == !normal)\n\t\t&& (thumbed->rounding == rounding)) {\n\t\treturn;\n\t}\n\tconst auto small = (rounding == Ui::BubbleRounding());\n\tauto image = normal ? normal : blurred;\n\tconst auto imageWidth = thumbed->thumbw * style::DevicePixelRatio();\n\tauto thumbnail = Images::Prepare(image->original(), imageWidth, {\n\t\t.options = (normal ? Images::Option() : Images::Option::Blur)\n\t\t\t| (small ? Images::Option::RoundSmall : Images::Option()),\n\t\t.outer = outer,\n\t});\n\tif (!small) {\n\t\tusing Corner = Ui::BubbleCornerRounding;\n\t\tusing Radius = Ui::CachedCornerRadius;\n\t\tauto corners = std::array<QImage, 4>();\n\t\tconst auto &small = Ui::CachedCornersMasks(Radius::ThumbSmall);\n\t\tconst auto &large = Ui::CachedCornersMasks(Radius::ThumbLarge);\n\t\tfor (auto i = 0; i != 4; ++i) {\n\t\t\tswitch (rounding[i]) {\n\t\t\tcase Corner::Small: corners[i] = small[i]; break;\n\t\t\tcase Corner::Large: corners[i] = large[i]; break;\n\t\t\t}\n\t\t}\n\t\tthumbnail = Images::Round(std::move(thumbnail), corners);\n\t}\n\tthumbed->thumbnail = std::move(thumbnail);\n\tthumbed->blurred = !normal;\n\tthumbed->rounding = rounding;\n}\n\nbool Document::hasHeavyPart() const {\n\treturn (_dataMedia != nullptr);\n}\n\nvoid Document::unloadHeavyPart() {\n\t_dataMedia = nullptr;\n\tif (const auto captioned = Get<HistoryDocumentCaptioned>()) {\n\t\tcaptioned->caption.unloadPersistentAnimation();\n\t}\n}\n\nvoid Document::ensureDataMediaCreated() const {\n\tif (_dataMedia) {\n\t\treturn;\n\t}\n\t_dataMedia = _data->createMediaView();\n\tif (Get<HistoryDocumentThumbed>()\n\t\t|| _data->isSongWithCover()\n\t\t|| _transcribedRound) {\n\t\t_dataMedia->thumbnailWanted(_realParent->fullId());\n\t}\n\tif (_data->isSvgImage()) {\n\t\t_dataMedia->goodThumbnailWanted();\n\t\tData::DocumentMedia::CheckGoodThumbnail(_data);\n\t}\n\thistory()->owner().registerHeavyViewPart(_parent);\n}\n\nbool Document::downloadInCorner() const {\n\treturn _data->isAudioFile()\n\t\t&& _realParent->allowsForward()\n\t\t&& _data->canBeStreamed()\n\t\t&& !_data->inappPlaybackFailed();\n}\n\nvoid Document::drawCornerDownload(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tLayoutMode mode) const {\n\tif (dataLoaded()\n\t\t|| _data->loadedInMediaCache()\n\t\t|| !downloadInCorner()) {\n\t\treturn;\n\t}\n\tauto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus;\n\tconst auto stm = context.messageStyle();\n\tconst auto thumbed = false;\n\tconst auto &st = (mode == LayoutMode::Full)\n\t\t? (thumbed ? st::msgFileThumbLayout : st::msgFileLayout)\n\t\t: (thumbed ? st::msgFileThumbLayoutGrouped : st::msgFileLayoutGrouped);\n\tconst auto shift = st::historyAudioDownloadShift;\n\tconst auto size = st::historyAudioDownloadSize;\n\tconst auto inner = style::rtlrect(st.padding.left() + shift, st.padding.top() - topMinus + shift, size, size, width());\n\tconst auto bubblePattern = usesBubblePattern(context);\n\tif (bubblePattern) {\n\t\tp.setPen(Qt::NoPen);\n\t} else {\n\t\tauto pen = stm->msgBg->p;\n\t\tpen.setWidth(st::lineWidth);\n\t\tp.setPen(pen);\n\t}\n\tp.setBrush(stm->msgFileBg);\n\t{\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.drawEllipse(inner);\n\t}\n\tconst auto &icon = _data->loading()\n\t\t? stm->historyAudioCancel\n\t\t: stm->historyAudioDownload;\n\tconst auto paintContent = [&](QPainter &q) {\n\t\tif (bubblePattern) {\n\t\t\tauto hq = PainterHighQualityEnabler(q);\n\t\t\tauto pen = stm->msgBg->p;\n\t\t\tpen.setWidth(st::lineWidth);\n\t\t\tq.setPen(pen);\n\t\t\tq.setBrush(Qt::NoBrush);\n\t\t\tq.drawEllipse(inner);\n\t\t}\n\t\ticon.paintInCenter(q, inner);\n\t\tif (_animation && _animation->radial.animating()) {\n\t\t\tconst auto rinner = inner.marginsRemoved(QMargins(st::historyAudioRadialLine, st::historyAudioRadialLine, st::historyAudioRadialLine, st::historyAudioRadialLine));\n\t\t\t_animation->radial.draw(q, rinner, st::historyAudioRadialLine, stm->historyFileRadialFg);\n\t\t}\n\t};\n\tif (bubblePattern) {\n\t\tconst auto add = st::lineWidth * 2;\n\t\tconst auto target = inner.marginsAdded({ add, add, add, add });\n\t\tUi::PaintPatternBubblePart(\n\t\t\tp,\n\t\t\tcontext.viewport,\n\t\t\tcontext.bubblesPattern->pixmap,\n\t\t\ttarget,\n\t\t\tpaintContent,\n\t\t\t_cornerDownloadCache);\n\t} else {\n\t\tpaintContent(p);\n\t}\n}\n\nTextState Document::cornerDownloadTextState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tLayoutMode mode) const {\n\tauto result = TextState(_parent);\n\tif (dataLoaded()\n\t\t|| _data->loadedInMediaCache()\n\t\t|| !downloadInCorner()) {\n\t\treturn result;\n\t}\n\tauto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus;\n\tconst auto thumbed = false;\n\tconst auto &st = (mode == LayoutMode::Full)\n\t\t? (thumbed ? st::msgFileThumbLayout : st::msgFileLayout)\n\t\t: (thumbed ? st::msgFileThumbLayoutGrouped : st::msgFileLayoutGrouped);\n\tconst auto shift = st::historyAudioDownloadShift;\n\tconst auto size = st::historyAudioDownloadSize;\n\tconst auto inner = style::rtlrect(st.padding.left() + shift, st.padding.top() - topMinus + shift, size, size, width());\n\tif (inner.contains(point)) {\n\t\tresult.link = _data->loading() ? _cancell : _savel;\n\t}\n\treturn result;\n}\n\nTextState Document::textState(QPoint point, StateRequest request) const {\n\treturn textState(point, { width(), height() }, request, LayoutMode::Full);\n}\n\nTextState Document::textState(\n\t\tQPoint point,\n\t\tQSize layout,\n\t\tStateRequest request,\n\t\tLayoutMode mode) const {\n\tconst auto width = layout.width();\n\n\tauto result = TextState(_parent);\n\n\tif (width < st::msgPadding.left() + st::msgPadding.right() + 1) {\n\t\treturn result;\n\t}\n\n\tensureDataMediaCreated();\n\tbool loaded = dataLoaded();\n\n\tupdateStatusText();\n\n\tconst auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus;\n\tconst auto thumbed = Get<HistoryDocumentThumbed>();\n\tconst auto &st = (mode == LayoutMode::Full)\n\t\t? (thumbed ? st::msgFileThumbLayout : st::msgFileLayout)\n\t\t: (thumbed ? st::msgFileThumbLayoutGrouped : st::msgFileLayoutGrouped);\n\tconst auto nameleft = st.padding.left() + st.thumbSize + st.thumbSkip;\n\tconst auto nametop = st.nameTop - topMinus;\n\tconst auto nameright = st.padding.right();\n\tauto namewidth = width - nameleft - nameright;\n\tconst auto linktop = st.linkTop - topMinus;\n\tauto bottom = st.padding.top() + st.thumbSize + st.padding.bottom() - topMinus;\n\tconst auto rthumb = style::rtlrect(st.padding.left(), st.padding.top() - topMinus, st.thumbSize, st.thumbSize, width);\n\tconst auto innerSize = st::msgFileLayout.thumbSize;\n\tconst auto inner = QRect(rthumb.x() + (rthumb.width() - innerSize) / 2, rthumb.y() + (rthumb.height() - innerSize) / 2, innerSize, innerSize);\n\n\tconst auto filenameMoused = QRect(nameleft, nametop, namewidth, st::semiboldFont->height).contains(point);\n\t_tooltipFilename.setMoused(filenameMoused);\n\tif (const auto thumbed = Get<HistoryDocumentThumbed>()) {\n\t\tif ((_data->loading() || _data->uploading()) && rthumb.contains(point)) {\n\t\t\tresult.link = _cancell;\n\t\t\treturn result;\n\t\t}\n\n\t\tif (_data->status != FileUploadFailed) {\n\t\t\tif (style::rtlrect(nameleft, linktop, thumbed->linkw, st::semiboldFont->height, width).contains(point)) {\n\t\t\t\tresult.link = (_data->loading() || _data->uploading())\n\t\t\t\t\t? thumbed->linkcancell\n\t\t\t\t\t: dataLoaded()\n\t\t\t\t\t? thumbed->linkopenwithl\n\t\t\t\t\t: thumbed->linksavel;\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (const auto state = cornerDownloadTextState(point, request, mode); state.link) {\n\t\t\treturn state;\n\t\t}\n\t\tif ((_data->loading() || _data->uploading()) && inner.contains(point) && !downloadInCorner()) {\n\t\t\tresult.link = _cancell;\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tconst auto voice = Get<HistoryDocumentVoice>();\n\tauto transcribeLength = 0;\n\tauto transcribeHeight = 0;\n\tauto painth = layout.height();\n\tif (voice) {\n\t\tauto waveformbottom = st.padding.top() - topMinus + st::msgWaveformMax + st::msgWaveformMin;\n\t\tif (voice->transcribe) {\n\t\t\tconst auto size = voice->transcribe->size();\n\t\t\tnamewidth -= st::historyTranscribeSkip + size.width();\n\t\t\tconst auto x = nameleft + namewidth + st::historyTranscribeSkip;\n\t\t\tconst auto y = st.padding.top() - topMinus;\n\t\t\tif (QRect(QPoint(x, y), size).contains(point)) {\n\t\t\t\tresult.link = voice->transcribe->link();\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t\tif (QRect(nameleft, nametop, namewidth, waveformbottom - nametop).contains(point)) {\n\t\t\tconst auto state = ::Media::Player::instance()->getState(AudioMsgId::Type::Voice);\n\t\t\tif (state.id == AudioMsgId(_data, _realParent->fullId(), state.id.externalPlayId())\n\t\t\t\t&& !::Media::Player::IsStoppedOrStopping(state.state)) {\n\t\t\t\tconst auto hover = std::clamp(\n\t\t\t\t\t(point.x() - nameleft) / float64(namewidth),\n\t\t\t\t\t0.,\n\t\t\t\t\t1.);\n\t\t\t\tif (_voiceHoverProgress != hover) {\n\t\t\t\t\t_voiceHoverProgress = hover;\n\t\t\t\t\trepaint();\n\t\t\t\t}\n\t\t\t\tif (!voice->seeking()) {\n\t\t\t\t\tvoice->setSeekingStart((point.x() - nameleft) / float64(namewidth));\n\t\t\t\t}\n\t\t\t\tresult.link = voice->seekl;\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t\tif (_voiceHoverProgress >= 0) {\n\t\t\t_voiceHoverProgress = -1;\n\t\t\trepaint();\n\t\t}\n\t\ttranscribeLength = voice->transcribeText.length();\n\t\tif (transcribeLength > 0) {\n\t\t\tauto captionw = width - st::msgPadding.left() - st::msgPadding.right();\n\t\t\ttranscribeHeight = voice->transcribeText.countHeight(captionw);\n\t\t\tpainth -= transcribeHeight;\n\t\t\tif (point.y() >= bottom && point.y() < bottom + transcribeHeight) {\n\t\t\t\tresult = TextState(_parent, voice->transcribeText.getState(\n\t\t\t\t\tpoint - QPoint(st::msgPadding.left(), bottom),\n\t\t\t\t\twidth - st::msgPadding.left() - st::msgPadding.right(),\n\t\t\t\t\trequest.forText()));\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tbottom += transcribeHeight;\n\t\t}\n\t}\n\n\tif (const auto captioned = Get<HistoryDocumentCaptioned>()) {\n\t\tif (point.y() >= bottom) {\n\t\t\tresult.symbol += transcribeLength;\n\t\t}\n\t\tif (transcribeHeight) {\n\t\t\tpainth -= st::mediaCaptionSkip;\n\t\t\tbottom += st::mediaCaptionSkip;\n\t\t}\n\t\tif (point.y() >= bottom) {\n\t\t\tresult = TextState(_parent, captioned->caption.getState(\n\t\t\t\tpoint - QPoint(st::msgPadding.left(), bottom),\n\t\t\t\twidth - st::msgPadding.left() - st::msgPadding.right(),\n\t\t\t\trequest.forText()));\n\t\t\tresult.symbol += transcribeLength;\n\t\t\treturn result;\n\t\t}\n\t\tauto captionw = width - st::msgPadding.left() - st::msgPadding.right();\n\t\tpainth -= captioned->caption.countHeight(captionw);\n\t\tif (isBubbleBottom()) {\n\t\t\tpainth -= st::msgPadding.bottom();\n\t\t}\n\t} else if (transcribeHeight && isBubbleBottom()) {\n\t\tpainth -= st::msgPadding.bottom();\n\t}\n\tconst auto till = voice ? (nameleft + namewidth) : width;\n\tif (QRect(0, 0, till, painth).contains(point)\n\t\t&& (!_data->loading() || downloadInCorner())\n\t\t&& !_data->uploading()\n\t\t&& !_data->isNull()) {\n\t\tif (loaded || _dataMedia->canBePlayed()) {\n\t\t\tresult.link = _openl;\n\t\t} else {\n\t\t\tresult.link = _savel;\n\t\t}\n\t\t_tooltipFilename.updateTooltipForLink(result.link.get());\n\t\treturn result;\n\t}\n\t_tooltipFilename.updateTooltipForState(result);\n\treturn result;\n}\n\nvoid Document::updatePressed(QPoint point) {\n\t// LayoutMode should be passed here.\n\tif (const auto voice = Get<HistoryDocumentVoice>()) {\n\t\tif (!voice->seeking()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto thumbed = Get<HistoryDocumentThumbed>();\n\t\tconst auto &st = thumbed ? st::msgFileThumbLayout : st::msgFileLayout;\n\t\tconst auto nameleft = st.padding.left() + st.thumbSize + st.thumbSkip;\n\t\tconst auto nameright = st.padding.right();\n\t\tconst auto transcribeWidth = voice->transcribe\n\t\t\t? (st::historyTranscribeSkip + voice->transcribe->size().width())\n\t\t\t: 0;\n\t\tvoice->setSeekingCurrent(std::clamp(\n\t\t\t(point.x() - nameleft)\n\t\t\t\t/ float64(width() - transcribeWidth - nameleft - nameright),\n\t\t\t0.,\n\t\t\t1.));\n\t\trepaint();\n\t}\n}\n\nTextSelection Document::adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const {\n\tauto transcribe = (const Ui::Text::String*)nullptr;\n\tauto caption = (const Ui::Text::String*)nullptr;\n\tif (const auto voice = Get<HistoryDocumentVoice>()) {\n\t\ttranscribe = &voice->transcribeText;\n\t}\n\tif (const auto captioned = Get<HistoryDocumentCaptioned>()) {\n\t\tcaption = &captioned->caption;\n\t}\n\tconst auto transcribeLength = transcribe ? transcribe->length() : 0;\n\tif (transcribe && selection.from < transcribeLength) {\n\t\tconst auto adjusted = transcribe->adjustSelection(selection, type);\n\t\tif (selection.to <= transcribeLength) {\n\t\t\treturn adjusted;\n\t\t}\n\t\tselection = TextSelection(adjusted.from, selection.to);\n\t}\n\tif (caption && selection.to > transcribeLength) {\n\t\tauto unshifted = transcribe\n\t\t\t? HistoryView::UnshiftItemSelection(selection, *transcribe)\n\t\t\t: selection;\n\t\tconst auto adjusted = caption->adjustSelection(unshifted, type);\n\t\tconst auto shifted = transcribe\n\t\t\t? HistoryView::ShiftItemSelection(adjusted, *transcribe)\n\t\t\t: adjusted;\n\t\tif (selection.from >= transcribeLength) {\n\t\t\treturn shifted;\n\t\t}\n\t\tselection = TextSelection(selection.from, shifted.to);\n\t}\n\treturn selection;\n}\n\nuint16 Document::fullSelectionLength() const {\n\tauto result = uint16();\n\tif (const auto voice = Get<HistoryDocumentVoice>()) {\n\t\tresult += voice->transcribeText.length();\n\t}\n\tif (const auto captioned = Get<HistoryDocumentCaptioned>()) {\n\t\tresult += captioned->caption.length();\n\t}\n\treturn result;\n}\n\nbool Document::hasTextForCopy() const {\n\tif (const auto voice = Get<HistoryDocumentVoice>()) {\n\t\tif (!voice->transcribeText.isEmpty()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn Has<HistoryDocumentCaptioned>();\n}\n\nTextForMimeData Document::selectedText(TextSelection selection) const {\n\tauto result = TextForMimeData();\n\tif (const auto voice = Get<HistoryDocumentVoice>()) {\n\t\tconst auto length = voice->transcribeText.length();\n\t\tif (selection.from < length) {\n\t\t\tresult.append(\n\t\t\t\tvoice->transcribeText.toTextForMimeData(selection));\n\t\t}\n\t\tif (selection.to <= length) {\n\t\t\treturn result;\n\t\t}\n\t\tselection = HistoryView::UnshiftItemSelection(\n\t\t\tselection,\n\t\t\tvoice->transcribeText);\n\t}\n\tif (const auto captioned = Get<HistoryDocumentCaptioned>()) {\n\t\tif (!result.empty()) {\n\t\t\tresult.append(\"\\n\\n\");\n\t\t}\n\t\tresult.append(captioned->caption.toTextForMimeData(selection));\n\t}\n\treturn result;\n}\n\nSelectedQuote Document::selectedQuote(TextSelection selection) const {\n\tif (const auto voice = Get<HistoryDocumentVoice>()) {\n\t\tconst auto length = voice->transcribeText.length();\n\t\tif (selection.from < length) {\n\t\t\treturn {};\n\t\t}\n\t\tselection = HistoryView::UnshiftItemSelection(\n\t\t\tselection,\n\t\t\tvoice->transcribeText);\n\t}\n\tif (const auto captioned = Get<HistoryDocumentCaptioned>()) {\n\t\treturn Element::FindSelectedQuote(\n\t\t\tcaptioned->caption,\n\t\t\tselection,\n\t\t\t_realParent);\n\t}\n\treturn {};\n}\n\nTextSelection Document::selectionFromQuote(\n\t\tconst SelectedQuote &quote) const {\n\tif (const auto captioned = Get<HistoryDocumentCaptioned>()) {\n\t\tconst auto result = Element::FindSelectionFromQuote(\n\t\t\tcaptioned->caption,\n\t\t\tquote);\n\t\tif (result.empty()) {\n\t\t\treturn {};\n\t\t} else if (const auto voice = Get<HistoryDocumentVoice>()) {\n\t\t\treturn HistoryView::ShiftItemSelection(\n\t\t\t\tresult,\n\t\t\t\tvoice->transcribeText);\n\t\t}\n\t\treturn result;\n\t}\n\treturn {};\n}\n\nbool Document::uploading() const {\n\treturn _data->uploading();\n}\n\n[[nodiscard]] int Document::thumbedLinkMaxWidth() const {\n\tif (Has<HistoryDocumentThumbed>()) {\n\t\tconst auto w = [](const QString &text) {\n\t\t\treturn st::semiboldFont->width(text.toUpper());\n\t\t};\n\t\treturn std::max({\n\t\t\tw(tr::lng_media_download(tr::now)),\n\t\t\tw(tr::lng_media_open_with(tr::now)),\n\t\t\tw(tr::lng_media_cancel(tr::now)),\n\t\t});\n\t}\n\treturn 0;\n}\n\nvoid Document::setStatusSize(int64 newSize, TimeId realDuration) const {\n\tconst auto duration = (_data->isSong()\n\t\t|| _data->isVoiceMessage()\n\t\t|| _transcribedRound)\n\t\t? _data->duration()\n\t\t: -1;\n\tFile::setStatusSize(\n\t\tnewSize,\n\t\t_data->size,\n\t\t(duration >= 0) ? duration / 1000 : -1,\n\t\trealDuration);\n\tif (auto thumbed = Get<HistoryDocumentThumbed>()) {\n\t\tif (_statusSize == Ui::FileStatusSizeReady) {\n\t\t\tthumbed->link = tr::lng_media_download(tr::now).toUpper();\n\t\t} else if (_statusSize == Ui::FileStatusSizeLoaded) {\n\t\t\tthumbed->link = tr::lng_media_open_with(tr::now).toUpper();\n\t\t} else if (_statusSize == Ui::FileStatusSizeFailed) {\n\t\t\tthumbed->link = tr::lng_media_download(tr::now).toUpper();\n\t\t} else if (_statusSize >= 0) {\n\t\t\tthumbed->link = tr::lng_media_cancel(tr::now).toUpper();\n\t\t} else {\n\t\t\tthumbed->link = tr::lng_media_open_with(tr::now).toUpper();\n\t\t}\n\t\tthumbed->linkw = st::semiboldFont->width(thumbed->link);\n\t}\n}\n\nbool Document::updateStatusText() const {\n\tauto showPause = false;\n\tauto statusSize = int64();\n\tauto realDuration = TimeId();\n\tif (_data->status == FileDownloadFailed || _data->status == FileUploadFailed) {\n\t\tstatusSize = Ui::FileStatusSizeFailed;\n\t} else if (_data->uploading()) {\n\t\tstatusSize = _data->uploadingData->offset;\n\t} else if (_data->loading()) {\n\t\tstatusSize = _data->loadOffset();\n\t} else if (dataLoaded()) {\n\t\tstatusSize = Ui::FileStatusSizeLoaded;\n\t} else {\n\t\tstatusSize = Ui::FileStatusSizeReady;\n\t}\n\n\tif (_data->isVoiceMessage() || _transcribedRound) {\n\t\tconst auto state = ::Media::Player::instance()->getState(AudioMsgId::Type::Voice);\n\t\tif (state.id == AudioMsgId(_data, _realParent->fullId(), state.id.externalPlayId())\n\t\t\t&& !::Media::Player::IsStoppedOrStopping(state.state)) {\n\t\t\tif (auto voice = Get<HistoryDocumentVoice>()) {\n\t\t\t\tbool was = (voice->playback != nullptr);\n\t\t\t\tvoice->ensurePlayback(this);\n\t\t\t\tif (!was || state.position != voice->playback->position) {\n\t\t\t\t\tauto prg = state.length\n\t\t\t\t\t\t? std::clamp(\n\t\t\t\t\t\t\tfloat64(state.position) / state.length,\n\t\t\t\t\t\t\t0.,\n\t\t\t\t\t\t\t1.)\n\t\t\t\t\t\t: 0.;\n\t\t\t\t\tif (voice->playback->position < state.position) {\n\t\t\t\t\t\tvoice->playback->progress.start(prg);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvoice->playback->progress = anim::value(0., prg);\n\t\t\t\t\t}\n\t\t\t\t\tvoice->playback->position = state.position;\n\t\t\t\t\tvoice->playback->progressAnimation.start();\n\t\t\t\t}\n\t\t\t\tvoice->lastDurationMs = static_cast<int>((state.length * 1000LL) / state.frequency); // Bad :(\n\t\t\t}\n\n\t\t\tstatusSize = -1 - (state.position / state.frequency);\n\t\t\trealDuration = (state.length / state.frequency);\n\t\t\tshowPause = ::Media::Player::ShowPauseIcon(state.state);\n\t\t} else {\n\t\t\tif (auto voice = Get<HistoryDocumentVoice>()) {\n\t\t\t\tvoice->checkPlaybackFinished();\n\t\t\t}\n\t\t}\n\t\tif (!showPause && (state.id == AudioMsgId(_data, _realParent->fullId(), state.id.externalPlayId()))) {\n\t\t\tshowPause = ::Media::Player::instance()->isSeeking(AudioMsgId::Type::Voice);\n\t\t}\n\t} else if (_data->isAudioFile()) {\n\t\tconst auto state = ::Media::Player::instance()->getState(AudioMsgId::Type::Song);\n\t\tif (state.id == AudioMsgId(_data, _realParent->fullId(), state.id.externalPlayId())\n\t\t\t&& !::Media::Player::IsStoppedOrStopping(state.state)) {\n\t\t\tstatusSize = -1 - (state.position / state.frequency);\n\t\t\trealDuration = (state.length / state.frequency);\n\t\t\tshowPause = ::Media::Player::ShowPauseIcon(state.state);\n\t\t} else {\n\t\t}\n\t\tif (!showPause && (state.id == AudioMsgId(_data, _realParent->fullId(), state.id.externalPlayId()))) {\n\t\t\tshowPause = ::Media::Player::instance()->isSeeking(AudioMsgId::Type::Song);\n\t\t}\n\t}\n\n\tif (statusSize != _statusSize) {\n\t\tsetStatusSize(statusSize, realDuration);\n\t}\n\treturn showPause;\n}\n\nQMargins Document::bubbleMargins() const {\n\tif (!Has<HistoryDocumentThumbed>()) {\n\t\treturn st::msgPadding;\n\t}\n\tconst auto padding = st::msgFileThumbLayout.padding;\n\treturn QMargins(padding.left(), padding.top(), padding.right(), padding.bottom());\n}\n\nvoid Document::refreshCaption(bool last) {\n\tconst auto now = Get<HistoryDocumentCaptioned>();\n\tauto caption = createCaption();\n\tif (!caption.isEmpty()) {\n\t\tif (now) {\n\t\t\treturn;\n\t\t}\n\t\tAddComponents(HistoryDocumentCaptioned::Bit());\n\t\tauto captioned = Get<HistoryDocumentCaptioned>();\n\t\tcaptioned->caption = std::move(caption);\n\t\tconst auto skip = last ? _parent->skipBlockWidth() : 0;\n\t\tif (skip) {\n\t\t\tcaptioned->caption.updateSkipBlock(\n\t\t\t\t_parent->skipBlockWidth(),\n\t\t\t\t_parent->skipBlockHeight());\n\t\t} else {\n\t\t\tcaptioned->caption.removeSkipBlock();\n\t\t}\n\t} else if (now) {\n\t\tRemoveComponents(HistoryDocumentCaptioned::Bit());\n\t}\n}\n\nQSize Document::sizeForGroupingOptimal(int maxWidth, bool last) const {\n\tconst auto thumbed = Get<HistoryDocumentThumbed>();\n\tconst auto &st = (thumbed ? st::msgFileThumbLayoutGrouped : st::msgFileLayoutGrouped);\n\tauto height = st.padding.top() + st.thumbSize + st.padding.bottom();\n\n\tconst_cast<Document*>(this)->refreshCaption(last);\n\n\tif (const auto captioned = Get<HistoryDocumentCaptioned>()) {\n\t\tauto captionw = maxWidth\n\t\t\t- st::msgPadding.left()\n\t\t\t- st::msgPadding.right();\n\t\theight += captioned->caption.countHeight(captionw);\n\t}\n\treturn { maxWidth, height };\n}\n\nQSize Document::sizeForGrouping(int width) const {\n\tconst auto thumbed = Get<HistoryDocumentThumbed>();\n\tconst auto &st = (thumbed ? st::msgFileThumbLayoutGrouped : st::msgFileLayoutGrouped);\n\tauto height = st.padding.top() + st.thumbSize + st.padding.bottom();\n\tif (const auto captioned = Get<HistoryDocumentCaptioned>()) {\n\t\tauto captionw = width\n\t\t\t- st::msgPadding.left()\n\t\t\t- st::msgPadding.right();\n\t\theight += captioned->caption.countHeight(captionw);\n\t}\n\treturn { maxWidth(), height };\n}\n\nvoid Document::drawGrouped(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry,\n\t\tRectParts sides,\n\t\tUi::BubbleRounding rounding,\n\t\tfloat64 highlightOpacity,\n\t\tnot_null<uint64*> cacheKey,\n\t\tnot_null<QPixmap*> cache) const {\n\tconst auto maybeMediaHighlight = context.highlightPathCache\n\t\t&& context.highlightPathCache->isEmpty();\n\tp.translate(geometry.topLeft());\n\tdraw(\n\t\tp,\n\t\tcontext.translated(-geometry.topLeft()),\n\t\tgeometry.width(),\n\t\tLayoutMode::Grouped,\n\t\trounding);\n\tif (maybeMediaHighlight\n\t\t&& !context.highlightPathCache->isEmpty()) {\n\t\tcontext.highlightPathCache->translate(geometry.topLeft());\n\t}\n\tp.translate(-geometry.topLeft());\n}\n\nTextState Document::getStateGrouped(\n\t\tconst QRect &geometry,\n\t\tRectParts sides,\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tpoint -= geometry.topLeft();\n\treturn textState(\n\t\tpoint,\n\t\tgeometry.size(),\n\t\trequest,\n\t\tLayoutMode::Grouped);\n}\n\nbool Document::voiceProgressAnimationCallback(crl::time now) {\n\tif (anim::Disabled()) {\n\t\tnow += (2 * kAudioVoiceMsgUpdateView);\n\t}\n\tif (const auto voice = Get<HistoryDocumentVoice>()) {\n\t\tif (voice->playback) {\n\t\t\tconst auto dt = (now - voice->playback->progressAnimation.started())\n\t\t\t\t/ float64(2 * kAudioVoiceMsgUpdateView);\n\t\t\tif (dt >= 1.) {\n\t\t\t\tvoice->playback->progressAnimation.stop();\n\t\t\t\tvoice->playback->progress.finish();\n\t\t\t} else {\n\t\t\t\tvoice->playback->progress.update(qMin(dt, 1.), anim::linear);\n\t\t\t}\n\t\t\trepaint();\n\t\t\treturn (dt < 1.);\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Document::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {\n\tif (auto voice = Get<HistoryDocumentVoice>()) {\n\t\tif (pressed && p == voice->seekl && !voice->seeking()) {\n\t\t\tvoice->startSeeking();\n\t\t} else if (!pressed && voice->seeking()) {\n\t\t\tconst auto type = AudioMsgId::Type::Voice;\n\t\t\tconst auto state = ::Media::Player::instance()->getState(type);\n\t\t\tif (state.id == AudioMsgId(_data, _realParent->fullId(), state.id.externalPlayId()) && state.length) {\n\t\t\t\tconst auto currentProgress = voice->seekingCurrent();\n\t\t\t\t::Media::Player::instance()->finishSeeking(\n\t\t\t\t\tAudioMsgId::Type::Voice,\n\t\t\t\t\tcurrentProgress);\n\n\t\t\t\tvoice->ensurePlayback(this);\n\t\t\t\tvoice->playback->position = 0;\n\t\t\t\tvoice->playback->progress = anim::value(currentProgress, currentProgress);\n\t\t\t}\n\t\t\tvoice->stopSeeking();\n\t\t}\n\t}\n\tFile::clickHandlerPressedChanged(p, pressed);\n}\n\nvoid Document::refreshParentId(not_null<HistoryItem*> realParent) {\n\tFile::refreshParentId(realParent);\n\n\tconst auto fullId = realParent->fullId();\n\tif (auto thumbed = Get<HistoryDocumentThumbed>()) {\n\t\tif (thumbed->linksavel) {\n\t\t\tthumbed->linksavel->setMessageId(fullId);\n\t\t\tthumbed->linkcancell->setMessageId(fullId);\n\t\t}\n\t}\n\tif (auto voice = Get<HistoryDocumentVoice>()) {\n\t\tif (voice->seekl) {\n\t\t\tvoice->seekl->setMessageId(fullId);\n\t\t}\n\t}\n}\n\nvoid Document::parentTextUpdated() {\n\tRemoveComponents(HistoryDocumentCaptioned::Bit());\n}\n\nvoid Document::hideSpoilers() {\n\tif (const auto captioned = Get<HistoryDocumentCaptioned>()) {\n\t\tcaptioned->caption.setSpoilerRevealed(false, anim::type::instant);\n\t}\n}\n\nUi::Text::String Document::createCaption() const {\n\treturn File::createCaption(_realParent);\n}\n\nvoid Document::TooltipFilename::setElided(bool value) {\n\tif (_elided != value) {\n\t\t_elided = value;\n\t\t_stale = true;\n\t}\n}\n\nvoid Document::TooltipFilename::setMoused(bool value) {\n\tif (_moused != value) {\n\t\t_moused = value;\n\t\t_stale = true;\n\t}\n}\n\nvoid Document::TooltipFilename::setTooltipText(QString text) {\n\tif (_tooltip != text) {\n\t\t_tooltip = text;\n\t\t_stale = true;\n\t}\n}\n\nvoid Document::TooltipFilename::updateTooltipForLink(ClickHandler *link) {\n\tif (_lastLink != link) {\n\t\t_lastLink = link;\n\t\t_stale = true;\n\t}\n\tif (_stale && link) {\n\t\t_stale = false;\n\t\tlink->setProperty(\n\t\t\tkDocumentFilenameTooltipProperty,\n\t\t\t(_elided && _moused) ? _tooltip : QString());\n\t}\n}\n\nvoid Document::TooltipFilename::updateTooltipForState(\n\t\tTextState &state) const {\n\tif (_elided && _moused) {\n\t\tstate.customTooltip = true;\n\t\tstate.customTooltipText = _tooltip;\n\t}\n}\n\nbool DrawThumbnailAsSongCover(\n\t\tPainter &p,\n\t\tconst style::color &colored,\n\t\tconst std::shared_ptr<Data::DocumentMedia> &dataMedia,\n\t\tconst QRect &rect,\n\t\tbool selected) {\n\tif (!dataMedia) {\n\t\treturn false;\n\t}\n\n\tauto cover = QPixmap();\n\tconst auto scaled = [&](not_null<Image*> image) {\n\t\tconst auto aspectRatio = Qt::KeepAspectRatioByExpanding;\n\t\treturn image->size().scaled(rect.size(), aspectRatio);\n\t};\n\tconst auto args = Images::PrepareArgs{\n\t\t.colored = &colored,\n\t\t.options = Images::Option::RoundCircle,\n\t\t.outer = rect.size(),\n\t};\n\tif (const auto normal = dataMedia->thumbnail()) {\n\t\tcover = normal->pixSingle(scaled(normal), args);\n\t} else if (const auto blurred = dataMedia->thumbnailInline()) {\n\t\tcover = blurred->pixSingle(scaled(blurred), args.blurred());\n\t} else {\n\t\treturn false;\n\t}\n\tif (selected) {\n\t\tauto selectedCover = Images::Colored(\n\t\t\tcover.toImage(),\n\t\t\tp.textPalette().selectOverlay);\n\t\tcover = QPixmap::fromImage(\n\t\t\tstd::move(selectedCover),\n\t\t\tQt::ColorOnly);\n\t}\n\tp.drawPixmap(rect.topLeft(), cover);\n\n\treturn true;\n}\n\nrpl::producer<> TTLVoiceStops(FullMsgId fullId) {\n\treturn rpl::merge(\n\t\t::Media::Player::instance()->updatedNotifier(\n\t\t) | rpl::filter([=](::Media::Player::TrackState state) {\n\t\t\tusing State = ::Media::Player::State;\n\t\t\tconst auto badState = state.state == State::Stopped\n\t\t\t\t|| state.state == State::StoppedAtEnd\n\t\t\t\t|| state.state == State::StoppedAtError\n\t\t\t\t|| state.state == State::StoppedAtStart;\n\t\t\treturn (state.id.contextId() != fullId) && !badState;\n\t\t}) | rpl::to_empty,\n\t\t::Media::Player::instance()->tracksFinished(\n\t\t) | rpl::filter([=](AudioMsgId::Type type) {\n\t\t\treturn (type == AudioMsgId::Type::Voice);\n\t\t}) | rpl::to_empty,\n\t\t::Media::Player::instance()->stops(AudioMsgId::Type::Voice)\n\t);\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_document.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_file.h\"\n#include \"base/runtime_composer.h\"\n\nstruct HistoryDocumentNamed;\nstruct HistoryDocumentThumbed;\n\nnamespace Data {\nclass DocumentMedia;\n} // namespace Data\n\nnamespace Ui::Text {\nclass String;\n} // namespace Ui::Text\n\nnamespace HistoryView {\n\nusing TtlPaintCallback = Fn<void(QPainter&, QRect, QColor)>;\n\nclass Document final\n\t: public File\n\t, public RuntimeComposer<Document> {\npublic:\n\tDocument(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tnot_null<DocumentData*> document);\n\t~Document();\n\n\tbool hideMessageText() const override {\n\t\treturn false;\n\t}\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\tvoid updatePressed(QPoint point) override;\n\n\t[[nodiscard]] TextSelection adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const override;\n\tuint16 fullSelectionLength() const override;\n\tbool hasTextForCopy() const override;\n\n\tTextForMimeData selectedText(TextSelection selection) const override;\n\tSelectedQuote selectedQuote(TextSelection selection) const override;\n\tTextSelection selectionFromQuote(\n\t\tconst SelectedQuote &quote) const override;\n\n\tbool uploading() const override;\n\n\tDocumentData *getDocument() const override {\n\t\treturn _data;\n\t}\n\n\tvoid hideSpoilers() override;\n\tbool needsBubble() const override {\n\t\treturn true;\n\t}\n\tbool customInfoLayout() const override {\n\t\treturn false;\n\t}\n\tQMargins bubbleMargins() const override;\n\n\tQSize sizeForGroupingOptimal(int maxWidth, bool last) const override;\n\tQSize sizeForGrouping(int width) const override;\n\tvoid drawGrouped(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry,\n\t\tRectParts sides,\n\t\tUi::BubbleRounding rounding,\n\t\tfloat64 highlightOpacity,\n\t\tnot_null<uint64*> cacheKey,\n\t\tnot_null<QPixmap*> cache) const override;\n\tTextState getStateGrouped(\n\t\tconst QRect &geometry,\n\t\tRectParts sides,\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\tbool voiceProgressAnimationCallback(crl::time now);\n\n\tvoid clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;\n\n\tvoid refreshParentId(not_null<HistoryItem*> realParent) override;\n\tvoid parentTextUpdated() override;\n\n\tbool hasHeavyPart() const override;\n\tvoid unloadHeavyPart() override;\n\nprotected:\n\tfloat64 dataProgress() const override;\n\tbool dataFinished() const override;\n\tbool dataLoaded() const override;\n\nprivate:\n\tenum class LayoutMode {\n\t\tFull,\n\t\tGrouped,\n\t};\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint width,\n\t\tLayoutMode mode,\n\t\tUi::BubbleRounding outsideRounding) const;\n\t[[nodiscard]] TextState textState(\n\t\tQPoint point,\n\t\tQSize layout,\n\t\tStateRequest request,\n\t\tLayoutMode mode) const;\n\tvoid ensureDataMediaCreated() const;\n\n\t[[nodiscard]] Ui::Text::String createCaption() const;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\n\tvoid refreshCaption(bool last);\n\tvoid createComponents();\n\tvoid fillNamedFromData(not_null<HistoryDocumentNamed*> named);\n\n\t[[nodiscard]] Ui::BubbleRounding thumbRounding(\n\t\tLayoutMode mode,\n\t\tUi::BubbleRounding outsideRounding) const;\n\tvoid validateThumbnail(\n\t\tnot_null<const HistoryDocumentThumbed*> thumbed,\n\t\tint size,\n\t\tUi::BubbleRounding rounding) const;\n\n\tvoid setStatusSize(int64 newSize, TimeId realDuration = 0) const;\n\tbool updateStatusText() const; // returns showPause\n\t[[nodiscard]] int thumbedLinkMaxWidth() const;\n\n\t[[nodiscard]] bool downloadInCorner() const;\n\tvoid drawCornerDownload(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tLayoutMode mode) const;\n\t[[nodiscard]] TextState cornerDownloadTextState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tLayoutMode mode) const;\n\n\tnot_null<DocumentData*> _data;\n\tmutable std::shared_ptr<Data::DocumentMedia> _dataMedia;\n\tmutable QImage _iconCache;\n\tmutable QImage _cornerDownloadCache;\n\n\tclass TooltipFilename {\n\tpublic:\n\t\tvoid setElided(bool value);\n\t\tvoid setMoused(bool value);\n\t\tvoid setTooltipText(QString text);\n\t\tvoid updateTooltipForLink(ClickHandler *link);\n\t\tvoid updateTooltipForState(TextState &state) const;\n\tprivate:\n\t\tClickHandler *_lastLink = nullptr;\n\t\tbool _elided = false;\n\t\tbool _moused = false;\n\t\tbool _stale = false;\n\t\tQString _tooltip;\n\t};\n\n\tmutable TooltipFilename _tooltipFilename;\n\n\tTtlPaintCallback _drawTtl;\n\n\tmutable float64 _voiceHoverProgress = -1;\n\n\tbool _transcribedRound = false;\n\n};\n\nbool DrawThumbnailAsSongCover(\n\tPainter &p,\n\tconst style::color &colored,\n\tconst std::shared_ptr<Data::DocumentMedia> &dataMedia,\n\tconst QRect &rect,\n\tbool selected = false);\n\nrpl::producer<> TTLVoiceStops(FullMsgId fullId);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_file.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_file.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/text/format_values.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_element.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_session.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\n\nbool File::toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const {\n\treturn p == _openl || p == _savel || p == _cancell;\n}\n\nbool File::dragItemByHandler(const ClickHandlerPtr &p) const {\n\treturn p == _openl || p == _savel || p == _cancell;\n}\n\nvoid File::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {\n\tif (p == _savel || p == _cancell) {\n\t\tif (active && !dataLoaded()) {\n\t\t\tensureAnimation();\n\t\t\t_animation->a_thumbOver.start([=] { repaint(); }, 0., 1., st::msgFileOverDuration);\n\t\t} else if (!active && _animation && !dataLoaded()) {\n\t\t\t_animation->a_thumbOver.start([=] { repaint(); }, 1., 0., st::msgFileOverDuration);\n\t\t}\n\t}\n}\n\nvoid File::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) {\n\trepaint();\n}\n\nvoid File::setLinks(\n\t\tFileClickHandlerPtr &&openl,\n\t\tFileClickHandlerPtr &&savel,\n\t\tFileClickHandlerPtr &&cancell) {\n\t_openl = std::move(openl);\n\t_savel = std::move(savel);\n\t_cancell = std::move(cancell);\n}\n\nvoid File::refreshParentId(not_null<HistoryItem*> realParent) {\n\tconst auto contextId = realParent->fullId();\n\tif (_openl) {\n\t\t_openl->setMessageId(contextId);\n\t}\n\tif (_savel) {\n\t\t_savel->setMessageId(contextId);\n\t}\n\tif (_cancell) {\n\t\t_cancell->setMessageId(contextId);\n\t}\n}\n\nvoid File::setStatusSize(\n\t\tint64 newSize,\n\t\tint64 fullSize,\n\t\tTimeId duration,\n\t\tTimeId realDuration) const {\n\t_statusSize = newSize;\n\tif (_statusSize == Ui::FileStatusSizeReady) {\n\t\t_statusText = (duration >= 0) ? Ui::FormatDurationAndSizeText(duration, fullSize) : (duration < -1 ? Ui::FormatGifAndSizeText(fullSize) : Ui::FormatSizeText(fullSize));\n\t} else if (_statusSize == Ui::FileStatusSizeLoaded) {\n\t\t_statusText = (duration >= 0) ? Ui::FormatDurationText(duration) : (duration < -1 ? u\"GIF\"_q : Ui::FormatSizeText(fullSize));\n\t} else if (_statusSize == Ui::FileStatusSizeFailed) {\n\t\t_statusText = tr::lng_attach_failed(tr::now);\n\t} else if (_statusSize >= 0) {\n\t\t_statusText = Ui::FormatDownloadText(_statusSize, fullSize);\n\t} else {\n\t\t_statusText = Ui::FormatPlayedText(-_statusSize - 1, realDuration);\n\t}\n}\n\nvoid File::radialAnimationCallback(crl::time now) const {\n\tconst auto updated = [&] {\n\t\treturn _animation->radial.update(\n\t\t\tdataProgress(),\n\t\t\tdataFinished(),\n\t\t\tnow);\n\t}();\n\tif (!anim::Disabled() || updated) {\n\t\trepaint();\n\t}\n\tif (!_animation->radial.animating()) {\n\t\tcheckAnimationFinished();\n\t}\n}\n\nvoid File::ensureAnimation() const {\n\tif (!_animation) {\n\t\t_animation = std::make_unique<AnimationData>([=](crl::time now) {\n\t\t\tradialAnimationCallback(now);\n\t\t});\n\t}\n}\n\nvoid File::checkAnimationFinished() const {\n\tif (_animation && !_animation->a_thumbOver.animating() && !_animation->radial.animating()) {\n\t\tif (dataLoaded()) {\n\t\t\t_animation.reset();\n\t\t}\n\t}\n}\nvoid File::setDocumentLinks(\n\t\tnot_null<DocumentData*> document,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tFn<bool()> openHook) {\n\tconst auto context = realParent->fullId();\n\tsetLinks(\n\t\tstd::make_shared<DocumentOpenClickHandler>(\n\t\t\tdocument,\n\t\t\tcrl::guard(this, [=](FullMsgId id) {\n\t\t\t\tif (!openHook || !openHook()) {\n\t\t\t\t\t_parent->delegate()->elementOpenDocument(document, id);\n\t\t\t\t}\n\t\t\t}),\n\t\t\tcontext),\n\t\tstd::make_shared<DocumentSaveClickHandler>(document, context),\n\t\tstd::make_shared<DocumentCancelClickHandler>(\n\t\t\tdocument,\n\t\t\tcrl::guard(this, [=](FullMsgId id) {\n\t\t\t\t_parent->delegate()->elementCancelUpload(id);\n\t\t\t}),\n\t\t\tcontext));\n}\n\nFile::~File() = default;\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_file.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/radial_animation.h\"\n\nclass FileClickHandler;\n\nnamespace HistoryView {\n\nclass File : public Media {\npublic:\n\tFile(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<HistoryItem*> realParent)\n\t: Media(parent)\n\t, _realParent(realParent) {\n\t}\n\n\t[[nodiscard]] bool toggleSelectionByHandlerClick(\n\t\tconst ClickHandlerPtr &p) const override;\n\t[[nodiscard]] bool dragItemByHandler(\n\t\tconst ClickHandlerPtr &p) const override;\n\n\tvoid clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;\n\tvoid clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;\n\n\tvoid refreshParentId(not_null<HistoryItem*> realParent) override;\n\n\t[[nodiscard]] bool allowsFastShare() const override {\n\t\treturn true;\n\t}\n\n\t~File();\n\nprotected:\n\tusing FileClickHandlerPtr = std::shared_ptr<FileClickHandler>;\n\n\tnot_null<HistoryItem*> _realParent;\n\tFileClickHandlerPtr _openl, _savel, _cancell;\n\n\tvoid setLinks(\n\t\tFileClickHandlerPtr &&openl,\n\t\tFileClickHandlerPtr &&savel,\n\t\tFileClickHandlerPtr &&cancell);\n\tvoid setDocumentLinks(\n\t\tnot_null<DocumentData*> document,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tFn<bool()> openHook = nullptr);\n\n\t// >= 0 will contain download / upload string, _statusSize = loaded bytes\n\t// < 0 will contain played string, _statusSize = -(seconds + 1) played\n\t// 0xFFFFFFF0LL will contain status for not yet downloaded file\n\t// 0xFFFFFFF1LL will contain status for already downloaded file\n\t// 0xFFFFFFF2LL will contain status for failed to download / upload file\n\tmutable int64 _statusSize = 0;\n\tmutable QString _statusText;\n\n\t// duration = -1 - no duration, duration = -2 - \"GIF\" duration\n\tvoid setStatusSize(int64 newSize, int64 fullSize, TimeId duration, TimeId realDuration) const;\n\n\tvoid radialAnimationCallback(crl::time now) const;\n\n\tvoid ensureAnimation() const;\n\tvoid checkAnimationFinished() const;\n\n\tbool isRadialAnimation() const {\n\t\tif (_animation) {\n\t\t\tif (_animation->radial.animating()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tcheckAnimationFinished();\n\t\t}\n\t\treturn false;\n\t}\n\tbool isThumbAnimation() const {\n\t\tif (_animation) {\n\t\t\tif (_animation->a_thumbOver.animating()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tcheckAnimationFinished();\n\t\t}\n\t\treturn false;\n\t}\n\n\tvirtual float64 dataProgress() const = 0;\n\tvirtual bool dataFinished() const = 0;\n\tvirtual bool dataLoaded() const = 0;\n\n\tstruct AnimationData {\n\t\ttemplate <typename Callback>\n\t\tAnimationData(Callback &&radialCallback)\n\t\t: radial(std::forward<Callback>(radialCallback)) {\n\t\t}\n\n\t\tUi::Animations::Simple a_thumbOver;\n\t\tUi::RadialAnimation radial;\n\t};\n\tmutable std::unique_ptr<AnimationData> _animation;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_game.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_game.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/media/history_view_media_common.h\"\n#include \"ui/item_text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_session.h\"\n#include \"data/data_game.h\"\n#include \"data/data_media_types.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\n\nGame::Game(\n\tnot_null<Element*> parent,\n\tnot_null<GameData*> data,\n\tconst TextWithEntities &consumed)\n: Media(parent)\n, _st(st::historyPagePreview)\n, _data(data)\n, _title(st::msgMinWidth - _st.padding.left() - _st.padding.right())\n, _description(st::msgMinWidth - _st.padding.left() - _st.padding.right()) {\n\tif (!consumed.text.isEmpty()) {\n\t\tconst auto context = Core::TextContext({\n\t\t\t.session = &history()->session(),\n\t\t\t.repaint = [=] { _parent->customEmojiRepaint(); },\n\t\t});\n\t\t_description.setMarkedText(\n\t\t\tst::webPageDescriptionStyle,\n\t\t\tconsumed,\n\t\t\tUi::ItemTextOptions(parent->data()),\n\t\t\tcontext);\n\t}\n\thistory()->owner().registerGameView(_data, _parent);\n}\n\nQSize Game::countOptimalSize() {\n\tauto lineHeight = UnitedLineHeight();\n\n\tconst auto item = _parent->data();\n\tif (!_openl && item->isRegular()) {\n\t\tconst auto row = 0;\n\t\tconst auto column = 0;\n\t\t_openl = std::make_shared<ReplyMarkupClickHandler>(\n\t\t\t&item->history()->owner(),\n\t\t\trow,\n\t\t\tcolumn,\n\t\t\titem->fullId());\n\t}\n\n\tauto title = TextUtilities::SingleLine(_data->title);\n\n\t// init attach\n\tif (!_attach) {\n\t\t_attach = CreateAttach(\n\t\t\t_parent,\n\t\t\t_data->document,\n\t\t\t_data->document ? nullptr : _data->photo);\n\t}\n\n\t// init strings\n\tif (_description.isEmpty() && !_data->description.isEmpty()) {\n\t\tauto text = _data->description;\n\t\tif (!text.isEmpty()) {\n\t\t\tauto marked = TextWithEntities { text };\n\t\t\tauto parseFlags = TextParseLinks | TextParseMultiline;\n\t\t\tTextUtilities::ParseEntities(marked, parseFlags);\n\t\t\t_description.setMarkedText(\n\t\t\t\tst::webPageDescriptionStyle,\n\t\t\t\tmarked,\n\t\t\t\tUi::WebpageTextDescriptionOptions());\n\t\t\tif (!_attach) {\n\t\t\t\t_description.updateSkipBlock(\n\t\t\t\t\t_parent->skipBlockWidth(),\n\t\t\t\t\t_parent->skipBlockHeight());\n\t\t\t}\n\t\t}\n\t}\n\tif (_title.isEmpty() && !title.isEmpty()) {\n\t\t_title.setText(\n\t\t\tst::webPageTitleStyle,\n\t\t\ttitle,\n\t\t\tUi::WebpageTextTitleOptions());\n\t}\n\n\t// init dimensions\n\tauto skipBlockWidth = _parent->skipBlockWidth();\n\tauto maxWidth = skipBlockWidth;\n\tauto minHeight = 0;\n\n\tauto titleMinHeight = _title.isEmpty() ? 0 : lineHeight;\n\t// enable any count of lines in game description / message\n\tauto descMaxLines = 4096;\n\tauto descriptionMinHeight = _description.isEmpty() ? 0 : qMin(_description.minHeight(), descMaxLines * lineHeight);\n\n\tif (!_title.isEmpty()) {\n\t\taccumulate_max(maxWidth, _title.maxWidth());\n\t\tminHeight += titleMinHeight;\n\t}\n\tif (!_description.isEmpty()) {\n\t\taccumulate_max(maxWidth, _description.maxWidth());\n\t\tminHeight += descriptionMinHeight;\n\t}\n\tif (_attach) {\n\t\tauto attachAtTop = !_titleLines && !_descriptionLines;\n\t\tif (!attachAtTop) minHeight += st::mediaInBubbleSkip;\n\n\t\t_attach->initDimensions();\n\t\tQMargins bubble(_attach->bubbleMargins());\n\t\tauto maxMediaWidth = _attach->maxWidth() - bubble.left() - bubble.right();\n\t\tif (isBubbleBottom() && _attach->customInfoLayout()) {\n\t\t\tmaxMediaWidth += skipBlockWidth;\n\t\t}\n\t\taccumulate_max(maxWidth, maxMediaWidth);\n\t\tminHeight += _attach->minHeight() - bubble.top() - bubble.bottom();\n\t}\n\tauto padding = inBubblePadding() + innerMargin();\n\tmaxWidth += padding.left() + padding.right();\n\tminHeight += padding.top() + padding.bottom();\n\n\tif (!_gameTagWidth) {\n\t\t_gameTagWidth = st::msgDateFont->width(tr::lng_game_tag(tr::now).toUpper());\n\t}\n\treturn { maxWidth, minHeight };\n}\n\nvoid Game::refreshParentId(not_null<HistoryItem*> realParent) {\n\tif (_openl) {\n\t\t_openl->setMessageId(realParent->fullId());\n\t}\n\tif (_attach) {\n\t\t_attach->refreshParentId(realParent);\n\t}\n}\n\nQSize Game::countCurrentSize(int newWidth) {\n\taccumulate_min(newWidth, maxWidth());\n\tconst auto padding = inBubblePadding() + innerMargin();\n\tauto innerWidth = newWidth - padding.left() - padding.right();\n\n\t// enable any count of lines in game description / message\n\tauto linesMax = 4096;\n\tauto lineHeight = UnitedLineHeight();\n\tauto newHeight = 0;\n\tif (_title.isEmpty()) {\n\t\t_titleLines = 0;\n\t} else {\n\t\tif (_title.countHeight(innerWidth) < 2 * st::webPageTitleFont->height) {\n\t\t\t_titleLines = 1;\n\t\t} else {\n\t\t\t_titleLines = 2;\n\t\t}\n\t\tnewHeight += _titleLines * lineHeight;\n\t}\n\n\tif (_description.isEmpty()) {\n\t\t_descriptionLines = 0;\n\t} else {\n\t\tauto descriptionHeight = _description.countHeight(innerWidth);\n\t\tif (descriptionHeight < (linesMax - _titleLines) * st::webPageDescriptionFont->height) {\n\t\t\t_descriptionLines = (descriptionHeight / st::webPageDescriptionFont->height);\n\t\t} else {\n\t\t\t_descriptionLines = (linesMax - _titleLines);\n\t\t}\n\t\tnewHeight += _descriptionLines * lineHeight;\n\t}\n\n\tif (_attach) {\n\t\tauto attachAtTop = !_titleLines && !_descriptionLines;\n\t\tif (!attachAtTop) newHeight += st::mediaInBubbleSkip;\n\n\t\tQMargins bubble(_attach->bubbleMargins());\n\n\t\t_attach->resizeGetHeight(innerWidth + bubble.left() + bubble.right());\n\t\tnewHeight += _attach->height() - bubble.top() - bubble.bottom();\n\t}\n\tnewHeight += padding.top() + padding.bottom();\n\n\treturn { newWidth, newHeight };\n}\n\nTextSelection Game::toDescriptionSelection(\n\t\tTextSelection selection) const {\n\treturn UnshiftItemSelection(selection, _title);\n}\n\nTextSelection Game::fromDescriptionSelection(\n\t\tTextSelection selection) const {\n\treturn ShiftItemSelection(selection, _title);\n}\n\nvoid Game::draw(Painter &p, const PaintContext &context) const {\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {\n\t\treturn;\n\t}\n\n\tconst auto st = context.st;\n\tconst auto sti = context.imageStyle();\n\tconst auto stm = context.messageStyle();\n\n\tconst auto bubble = _attach ? _attach->bubbleMargins() : QMargins();\n\tconst auto full = QRect(0, 0, width(), height());\n\tauto outer = full.marginsRemoved(inBubblePadding());\n\tauto inner = outer.marginsRemoved(innerMargin());\n\tauto tshift = inner.top();\n\tauto paintw = inner.width();\n\tconst auto selected = context.selected();\n\tconst auto colorIndex = parent()->contentColorIndex();\n\tconst auto &colorCollectible = parent()->contentColorCollectible();\n\tconst auto colorPattern = colorCollectible\n\t\t? st->collectiblePatternIndex(colorCollectible)\n\t\t: st->colorPatternIndex(colorIndex);\n\tconst auto useColorCollectible = colorCollectible && !context.outbg;\n\tconst auto useColorIndex = !context.outbg;\n\tconst auto cache = useColorCollectible\n\t\t? st->collectibleReplyCache(selected, colorCollectible).get()\n\t\t: useColorIndex\n\t\t? st->coloredReplyCache(selected, colorIndex).get()\n\t\t: stm->replyCache[colorPattern].get();\n\tUi::Text::ValidateQuotePaintCache(*cache, _st);\n\tUi::Text::FillQuotePaint(p, outer, *cache, _st);\n\n\tif (_ripple) {\n\t\t_ripple->paint(p, outer.x(), outer.y(), width(), &cache->bg);\n\t\tif (_ripple->empty()) {\n\t\t\t_ripple = nullptr;\n\t\t}\n\t}\n\n\tauto lineHeight = UnitedLineHeight();\n\tif (_titleLines) {\n\t\tp.setPen(cache->icon);\n\t\tp.setTextPalette(useColorCollectible\n\t\t\t? st->collectibleTextPalette(selected, colorCollectible)\n\t\t\t: useColorIndex\n\t\t\t? st->coloredTextPalette(selected, colorIndex)\n\t\t\t: stm->semiboldPalette);\n\n\t\tauto endskip = 0;\n\t\tif (_title.hasSkipBlock()) {\n\t\t\tendskip = _parent->skipBlockWidth();\n\t\t}\n\t\t_title.drawLeftElided(\n\t\t\tp,\n\t\t\tinner.left(),\n\t\t\ttshift,\n\t\t\tpaintw,\n\t\t\twidth(),\n\t\t\t_titleLines,\n\t\t\tstyle::al_left,\n\t\t\t0,\n\t\t\t-1,\n\t\t\tendskip,\n\t\t\tfalse,\n\t\t\tcontext.selection);\n\t\ttshift += _titleLines * lineHeight;\n\n\t\tp.setTextPalette(stm->textPalette);\n\t}\n\tif (_descriptionLines) {\n\t\tp.setPen(stm->historyTextFg);\n\t\tauto endskip = 0;\n\t\tif (_description.hasSkipBlock()) {\n\t\t\tendskip = _parent->skipBlockWidth();\n\t\t}\n\t\t_parent->prepareCustomEmojiPaint(p, context, _description);\n\t\t_description.draw(p, {\n\t\t\t.position = { inner.left(), tshift },\n\t\t\t.outerWidth = width(),\n\t\t\t.availableWidth = paintw,\n\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t.now = context.now,\n\t\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t\t.selection = toDescriptionSelection(context.selection),\n\t\t\t.elisionHeight = _descriptionLines * lineHeight,\n\t\t\t.elisionRemoveFromEnd = endskip,\n\t\t\t.useFullWidth = true,\n\t\t});\n\t\ttshift += _descriptionLines * lineHeight;\n\t}\n\tif (_attach) {\n\t\tauto attachAtTop = !_titleLines && !_descriptionLines;\n\t\tif (!attachAtTop) tshift += st::mediaInBubbleSkip;\n\n\t\tauto attachLeft = inner.left() - bubble.left();\n\t\tauto attachTop = tshift - bubble.top();\n\t\tif (rtl()) attachLeft = width() - attachLeft - _attach->width();\n\n\t\tp.translate(attachLeft, attachTop);\n\t\t_attach->draw(p, context.translated(\n\t\t\t-attachLeft,\n\t\t\t-attachTop\n\t\t).withSelection(context.selected()\n\t\t\t? FullSelection\n\t\t\t: TextSelection()));\n\t\tauto pixwidth = _attach->width();\n\t\tauto pixheight = _attach->height();\n\n\t\tauto gameW = _gameTagWidth + 2 * st::msgDateImgPadding.x();\n\t\tauto gameH = st::msgDateFont->height + 2 * st::msgDateImgPadding.y();\n\t\tauto gameX = pixwidth - st::msgDateImgDelta - gameW;\n\t\tauto gameY = pixheight - st::msgDateImgDelta - gameH;\n\n\t\tUi::FillRoundRect(p, style::rtlrect(gameX, gameY, gameW, gameH, pixwidth), sti->msgDateImgBg, sti->msgDateImgBgCorners);\n\n\t\tp.setFont(st::msgDateFont);\n\t\tp.setPen(st->msgDateImgFg());\n\t\tp.drawTextLeft(gameX + st::msgDateImgPadding.x(), gameY + st::msgDateImgPadding.y(), pixwidth, tr::lng_game_tag(tr::now).toUpper());\n\n\t\tp.translate(-attachLeft, -attachTop);\n\t}\n}\n\nTextState Game::textState(QPoint point, StateRequest request) const {\n\tauto result = TextState(_parent);\n\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {\n\t\treturn result;\n\t}\n\n\tconst auto bubble = _attach ? _attach->bubbleMargins() : QMargins();\n\tconst auto full = QRect(0, 0, width(), height());\n\tauto outer = full.marginsRemoved(inBubblePadding());\n\tauto inner = outer.marginsRemoved(innerMargin());\n\tauto tshift = inner.top();\n\tauto paintw = inner.width();\n\n\tauto symbolAdd = 0;\n\tauto lineHeight = UnitedLineHeight();\n\tif (_titleLines) {\n\t\tif (point.y() >= tshift && point.y() < tshift + _titleLines * lineHeight) {\n\t\t\tUi::Text::StateRequestElided titleRequest = request.forText();\n\t\t\ttitleRequest.lines = _titleLines;\n\t\t\tresult = TextState(_parent, _title.getStateElidedLeft(\n\t\t\t\tpoint - QPoint(inner.left(), tshift),\n\t\t\t\tpaintw,\n\t\t\t\twidth(),\n\t\t\t\ttitleRequest));\n\t\t} else if (point.y() >= tshift + _titleLines * lineHeight) {\n\t\t\tsymbolAdd += _title.length();\n\t\t}\n\t\ttshift += _titleLines * lineHeight;\n\t}\n\tif (_descriptionLines) {\n\t\tif (point.y() >= tshift && point.y() < tshift + _descriptionLines * lineHeight) {\n\t\t\tUi::Text::StateRequestElided descriptionRequest = request.forText();\n\t\t\tdescriptionRequest.lines = _descriptionLines;\n\t\t\tresult = TextState(_parent, _description.getStateElidedLeft(\n\t\t\t\tpoint - QPoint(inner.left(), tshift),\n\t\t\t\tpaintw,\n\t\t\t\twidth(),\n\t\t\t\tdescriptionRequest));\n\t\t} else if (point.y() >= tshift + _descriptionLines * lineHeight) {\n\t\t\tsymbolAdd += _description.length();\n\t\t}\n\t\ttshift += _descriptionLines * lineHeight;\n\t}\n\tif (_attach) {\n\t\tauto attachAtTop = !_titleLines && !_descriptionLines;\n\t\tif (!attachAtTop) tshift += st::mediaInBubbleSkip;\n\n\t\tauto attachLeft = inner.left() - bubble.left();\n\t\tauto attachTop = tshift - bubble.top();\n\t\tif (rtl()) attachLeft = width() - attachLeft - _attach->width();\n\n\t\tif (QRect(attachLeft, tshift, _attach->width(), inner.top() + inner.height() - tshift).contains(point)) {\n\t\t\tif (_attach->isReadyForOpen()) {\n\t\t\t\tif (_parent->data()->isHistoryEntry()) {\n\t\t\t\t\tresult.link = _openl;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tresult = _attach->textState(point - QPoint(attachLeft, attachTop), request);\n\t\t\t}\n\t\t}\n\t}\n\tif (_parent->data()->isHistoryEntry()) {\n\t\tif (!result.link && outer.contains(point)) {\n\t\t\tresult.link = _openl;\n\t\t}\n\t}\n\t_lastPoint = point - outer.topLeft();\n\n\tresult.symbol += symbolAdd;\n\treturn result;\n}\n\nTextSelection Game::adjustSelection(TextSelection selection, TextSelectType type) const {\n\tif (!_descriptionLines || selection.to <= _title.length()) {\n\t\treturn _title.adjustSelection(selection, type);\n\t}\n\tauto descriptionSelection = _description.adjustSelection(toDescriptionSelection(selection), type);\n\tif (selection.from >= _title.length()) {\n\t\treturn fromDescriptionSelection(descriptionSelection);\n\t}\n\tauto titleSelection = _title.adjustSelection(selection, type);\n\treturn { titleSelection.from, fromDescriptionSelection(descriptionSelection).to };\n}\n\nvoid Game::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {\n\tif (_attach) {\n\t\t_attach->clickHandlerActiveChanged(p, active);\n\t}\n}\n\nvoid Game::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {\n\tif (p == _openl) {\n\t\tif (pressed) {\n\t\t\tif (!_ripple) {\n\t\t\t\tconst auto full = QRect(0, 0, width(), height());\n\t\t\t\tconst auto outer = full.marginsRemoved(inBubblePadding());\n\t\t\t\t_ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\t\tUi::RippleAnimation::RoundRectMask(\n\t\t\t\t\t\touter.size(),\n\t\t\t\t\t\t_st.radius),\n\t\t\t\t\t[=] { repaint(); });\n\t\t\t}\n\t\t\t_ripple->add(_lastPoint);\n\t\t} else if (_ripple) {\n\t\t\t_ripple->lastStop();\n\t\t}\n\t}\n\tif (_attach) {\n\t\t_attach->clickHandlerPressedChanged(p, pressed);\n\t}\n}\n\nbool Game::toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const {\n\treturn _attach && _attach->toggleSelectionByHandlerClick(p);\n}\n\nbool Game::allowTextSelectionByHandler(const ClickHandlerPtr &p) const {\n\treturn (p == _openl);\n}\n\nbool Game::dragItemByHandler(const ClickHandlerPtr &p) const {\n\treturn _attach && _attach->dragItemByHandler(p);\n}\n\nTextForMimeData Game::selectedText(TextSelection selection) const {\n\tauto titleResult = _title.toTextForMimeData(selection);\n\tauto descriptionResult = _description.toTextForMimeData(\n\t\ttoDescriptionSelection(selection));\n\tif (titleResult.empty()) {\n\t\treturn descriptionResult;\n\t} else if (descriptionResult.empty()) {\n\t\treturn titleResult;\n\t}\n\treturn titleResult.append('\\n').append(std::move(descriptionResult));\n}\n\nvoid Game::playAnimation(bool autoplay) {\n\tif (_attach) {\n\t\tif (autoplay) {\n\t\t\t_attach->autoplayAnimation();\n\t\t} else {\n\t\t\t_attach->playAnimation();\n\t\t}\n\t}\n}\n\nQMargins Game::inBubblePadding() const {\n\treturn {\n\t\tst::msgPadding.left(),\n\t\tisBubbleTop() ? st::msgPadding.left() : st::mediaInBubbleSkip,\n\t\tst::msgPadding.right(),\n\t\t(isBubbleBottom()\n\t\t\t? (st::msgPadding.left() + bottomInfoPadding())\n\t\t\t: st::mediaInBubbleSkip),\n\t};\n}\n\nQMargins Game::innerMargin() const {\n\treturn _st.padding;\n}\n\nint Game::bottomInfoPadding() const {\n\tif (!isBubbleBottom()) {\n\t\treturn 0;\n\t}\n\n\tauto result = st::msgDateFont->height;\n\n\t// we use padding greater than st::msgPadding.bottom() in the\n\t// bottom of the bubble so that the left line looks pretty.\n\t// but if we have bottom skip because of the info display\n\t// we don't need that additional padding so we replace it\n\t// back with st::msgPadding.bottom() instead of left().\n\tresult += st::msgPadding.bottom() - st::msgPadding.left();\n\treturn result;\n}\n\nvoid Game::parentTextUpdated() {\n\tif (const auto media = _parent->data()->media()) {\n\t\tconst auto consumed = media->consumedMessageText();\n\t\tif (!consumed.text.isEmpty()) {\n\t\t\tconst auto context = Core::TextContext({\n\t\t\t\t.session = &history()->session(),\n\t\t\t\t.repaint = [=] { _parent->customEmojiRepaint(); },\n\t\t\t});\n\t\t\t_description.setMarkedText(\n\t\t\t\tst::webPageDescriptionStyle,\n\t\t\t\tconsumed,\n\t\t\t\tUi::ItemTextOptions(_parent->data()),\n\t\t\t\tcontext);\n\t\t} else {\n\t\t\t_description = Ui::Text::String(st::msgMinWidth\n\t\t\t\t- _st.padding.left()\n\t\t\t\t- _st.padding.right());\n\t\t}\n\t\thistory()->owner().requestViewResize(_parent);\n\t}\n}\n\nbool Game::hasHeavyPart() const {\n\treturn _attach ? _attach->hasHeavyPart() : false;\n}\n\nvoid Game::unloadHeavyPart() {\n\tif (_attach) {\n\t\t_attach->unloadHeavyPart();\n\t}\n\t_description.unloadPersistentAnimation();\n}\n\nGame::~Game() {\n\thistory()->owner().unregisterGameView(_data, _parent);\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_game.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n\nclass ReplyMarkupClickHandler;\n\nnamespace Ui {\nclass RippleAnimation;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass Game : public Media {\npublic:\n\tGame(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<GameData*> data,\n\t\tconst TextWithEntities &consumed);\n\n\tvoid refreshParentId(not_null<HistoryItem*> realParent) override;\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\n\t[[nodiscard]] TextSelection adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const override;\n\tuint16 fullSelectionLength() const override {\n\t\treturn _title.length() + _description.length();\n\t}\n\tbool hasTextForCopy() const override {\n\t\treturn false; // we do not add _title and _description in FullSelection text copy.\n\t}\n\n\tbool toggleSelectionByHandlerClick(\n\t\tconst ClickHandlerPtr &p) const override;\n\tbool allowTextSelectionByHandler(\n\t\tconst ClickHandlerPtr &p) const override;\n\tbool dragItemByHandler(const ClickHandlerPtr &p) const override;\n\n\tTextForMimeData selectedText(TextSelection selection) const override;\n\n\tvoid clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;\n\tvoid clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;\n\n\tPhotoData *getPhoto() const override {\n\t\treturn _attach ? _attach->getPhoto() : nullptr;\n\t}\n\tDocumentData *getDocument() const override {\n\t\treturn _attach ? _attach->getDocument() : nullptr;\n\t}\n\tvoid stopAnimation() override {\n\t\tif (_attach) _attach->stopAnimation();\n\t}\n\tvoid checkAnimation() override {\n\t\tif (_attach) _attach->checkAnimation();\n\t}\n\n\tnot_null<GameData*> game() {\n\t\treturn _data;\n\t}\n\n\tbool needsBubble() const override {\n\t\treturn true;\n\t}\n\tbool customInfoLayout() const override {\n\t\treturn false;\n\t}\n\tbool allowsFastShare() const override {\n\t\treturn true;\n\t}\n\n\tMedia *attach() const {\n\t\treturn _attach.get();\n\t}\n\n\tvoid parentTextUpdated() override;\n\n\tbool hasHeavyPart() const override;\n\tvoid unloadHeavyPart() override;\n\n\t~Game();\n\nprivate:\n\tvoid playAnimation(bool autoplay) override;\n\t[[nodiscard]] QSize countOptimalSize() override;\n\t[[nodiscard]] QSize countCurrentSize(int newWidth) override;\n\n\t[[nodiscard]] TextSelection toDescriptionSelection(\n\t\tTextSelection selection) const;\n\t[[nodiscard]] TextSelection fromDescriptionSelection(\n\t\tTextSelection selection) const;\n\t[[nodiscard]] QMargins inBubblePadding() const;\n\t[[nodiscard]] QMargins innerMargin() const;\n\t[[nodiscard]] int bottomInfoPadding() const;\n\n\tconst style::QuoteStyle &_st;\n\tconst not_null<GameData*> _data;\n\tstd::shared_ptr<ReplyMarkupClickHandler> _openl;\n\tstd::unique_ptr<Media> _attach;\n\tmutable std::unique_ptr<Ui::RippleAnimation> _ripple;\n\n\tmutable QPoint _lastPoint;\n\tint _gameTagWidth = 0;\n\tint _descriptionLines = 0;\n\tint _titleLines = 0;\n\n\tUi::Text::String _title;\n\tUi::Text::String _description;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_gif.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_gif.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_transcribes.h\"\n#include \"lang/lang_keys.h\"\n#include \"mainwindow.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/clip/media_clip_reader.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"media/streaming/media_streaming_instance.h\"\n#include \"media/streaming/media_streaming_player.h\"\n#include \"media/streaming/media_streaming_utility.h\"\n#include \"media/view/media_view_open_common.h\"\n#include \"media/view/media_view_playback_progress.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_reply.h\"\n#include \"history/view/history_view_transcribe_button.h\"\n#include \"history/view/media/history_view_document.h\" // TTLVoiceStops\n#include \"history/view/media/history_view_media_common.h\"\n#include \"history/view/media/history_view_media_spoiler.h\"\n#include \"window/window_session_controller.h\"\n#include \"core/application.h\" // Application::showDocument.\n#include \"core/core_settings.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/image/image.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/grouped_layout.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_streaming.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_web_page.h\"\n#include \"storage/storage_account.h\"\n#include \"styles/style_chat.h\"\n\n#include <QSvgRenderer>\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kMaxGifForwardedBarLines = 4;\nconstexpr auto kUseNonBlurredThreshold = 240;\nconstexpr auto kMaxInlineArea = 1920 * 1080;\n\n[[nodiscard]] int GifMaxStatusWidth(not_null<DocumentData*> document) {\n\tauto result = st::normalFont->width(\n\t\tUi::FormatDownloadText(document->size, document->size));\n\taccumulate_max(\n\t\tresult,\n\t\tst::normalFont->width(Ui::FormatGifAndSizeText(document->size)));\n\treturn result;\n}\n\n[[nodiscard]] HistoryView::TtlRoundPaintCallback CreateTtlPaintCallback(\n\t\tFn<void()> update) {\n\tconst auto centerMargins = Margins(st::historyFileInPause.width() * 3);\n\n\tconst auto renderer = std::make_shared<QSvgRenderer>(\n\t\tu\":/gui/ttl/video_message_icon.svg\"_q);\n\n\treturn [=](QPainter &p, QRect r, const PaintContext &context) {\n\t\tconst auto centerRect = r - centerMargins;\n\t\tconst auto &icon = context.imageStyle()->historyVideoMessageTtlIcon;\n\t\tconst auto iconRect = QRect(\n\t\t\trect::right(centerRect) - icon.width() * 0.75,\n\t\t\trect::bottom(centerRect) - icon.height() * 0.75,\n\t\t\ticon.width(),\n\t\t\ticon.height());\n\t\t{\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tauto path = QPainterPath();\n\t\t\tpath.setFillRule(Qt::WindingFill);\n\t\t\tpath.addEllipse(centerRect);\n\t\t\tpath.addEllipse(iconRect);\n\t\t\tp.fillPath(path, st::shadowFg);\n\t\t\tp.fillPath(path, st::shadowFg);\n\t\t\tp.fillPath(path, st::shadowFg);\n\t\t}\n\n\t\trenderer->render(&p, centerRect - Margins(centerRect.width() / 4));\n\n\t\ticon.paint(p, iconRect.topLeft(), centerRect.width());\n\t};\n}\n\n} // namespace\n\nstruct Gif::Streamed {\n\tStreamed(\n\t\tnot_null<DocumentData*> chosen,\n\t\tstd::shared_ptr<::Media::Streaming::Document> shared,\n\t\tFn<void()> waitingCallback);\n\tconst not_null<DocumentData*> chosen;\n\t::Media::Streaming::Instance instance;\n\t::Media::Streaming::FrameRequest frozenRequest;\n\tQImage frozenFrame;\n\tQString frozenStatusText;\n};\n\nGif::Streamed::Streamed(\n\tnot_null<DocumentData*> chosen,\n\tstd::shared_ptr<::Media::Streaming::Document> shared,\n\tFn<void()> waitingCallback)\n: chosen(chosen)\n, instance(std::move(shared), std::move(waitingCallback)) {\n}\n\n[[nodiscard]] bool IsHiddenRoundMessage(not_null<Element*> parent) {\n\treturn parent->delegate()->elementContext() != Context::TTLViewer\n\t\t&& parent->data()->media()\n\t\t&& parent->data()->media()->ttlSeconds();\n}\n\nGif::Gif(\n\tnot_null<Element*> parent,\n\tnot_null<HistoryItem*> realParent,\n\tnot_null<DocumentData*> document,\n\tbool spoiler)\n: File(parent, realParent)\n, _data(document)\n, _videoCover(LookupVideoCover(document, realParent))\n, _storyId(realParent->media()\n\t? realParent->media()->storyId()\n\t: FullStoryId())\n, _spoiler((spoiler\n\t|| IsHiddenRoundMessage(_parent)\n\t|| realParent->isMediaSensitive())\n\t? std::make_unique<MediaSpoiler>()\n\t: nullptr)\n, _downloadSize(Ui::FormatSizeText(_data->size))\n, _videoTimestamp(::Media::View::ExtractVideoTimestamp(realParent))\n, _sensitiveSpoiler(realParent->isMediaSensitive())\n, _hasVideoCover(realParent->media() && realParent->media()->videoCover()) {\n\tif (_data->isVideoMessage() && _parent->data()->media()->ttlSeconds()) {\n\t\tif (_spoiler) {\n\t\t\t_drawTtl = CreateTtlPaintCallback([=] { repaint(); });\n\t\t}\n\t\tconst auto fullId = _realParent->fullId();\n\t\tconst auto &data = &_parent->data()->history()->owner();\n\t\tconst auto isOut = _parent->data()->out();\n\t\t_parent->data()->removeFromSharedMediaIndex();\n\t\tsetDocumentLinks(_data, realParent, [=] {\n\t\t\tauto lifetime = std::make_shared<rpl::lifetime>();\n\t\t\tTTLVoiceStops(fullId) | rpl::on_next([=]() mutable {\n\t\t\t\tif (lifetime) {\n\t\t\t\t\tbase::take(lifetime)->destroy();\n\t\t\t\t}\n\t\t\t\tif (!isOut) {\n\t\t\t\t\tif (const auto item = data->message(fullId)) {\n\t\t\t\t\t\t// Destroys this.\n\t\t\t\t\t\titem->clearMediaAsExpired();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, *lifetime);\n\n\t\t\treturn false;\n\t\t});\n\t} else {\n\t\tsetDocumentLinks(_data, realParent, [=] {\n\t\t\tif (!_data->createMediaView()->canBePlayed()\n\t\t\t\t|| !_data->isAnimation()\n\t\t\t\t|| _data->isVideoMessage()\n\t\t\t\t|| !CanPlayInline(_data)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tplayAnimation(false);\n\t\t\treturn true;\n\t\t});\n\t}\n\n\tsetStatusSize(Ui::FileStatusSizeReady);\n\n\tif (_spoiler) {\n\t\tcreateSpoilerLink(_spoiler.get());\n\t}\n\n\tif ((_dataMedia = _data->activeMediaView())) {\n\t\tdataMediaCreated();\n\t} else if (_videoCover) {\n\t\tif (_videoCover->inlineThumbnailBytes().isEmpty()\n\t\t\t&& (_videoCover->hasExact(Data::PhotoSize::Small)\n\t\t\t\t|| _videoCover->hasExact(Data::PhotoSize::Thumbnail))) {\n\t\t\t_videoCover->load(Data::PhotoSize::Small, realParent->fullId());\n\t\t}\n\t} else {\n\t\t_data->loadThumbnail(realParent->fullId());\n\t\tif (!autoplayEnabled()) {\n\t\t\t_data->loadVideoThumbnail(realParent->fullId());\n\t\t}\n\t}\n\tensureTranscribeButton();\n\n\t_purchasedPriceTag = hasPurchasedTag();\n}\n\nGif::~Gif() {\n\tif (_streamed || _dataMedia) {\n\t\tif (_streamed) {\n\t\t\t_data->owner().streaming().keepAlive(_data);\n\t\t\tsetStreamed(nullptr);\n\t\t}\n\t\tif (_dataMedia) {\n\t\t\t_data->owner().keepAlive(base::take(_dataMedia));\n\t\t\t_parent->checkHeavyPart();\n\t\t}\n\t}\n\ttogglePollingStory(false);\n}\n\nbool Gif::CanPlayInline(not_null<DocumentData*> document) {\n\tconst auto dimensions = document->dimensions;\n\treturn dimensions.width() * dimensions.height() <= kMaxInlineArea;\n}\n\nQSize Gif::sizeForAspectRatio() const {\n\t// We use size only for aspect ratio and we want to have it\n\t// as close to the thumbnail as possible.\n\t//if (!_data->dimensions.isEmpty()) {\n\t//\treturn _data->dimensions;\n\t//}\n\tif (_data->hasThumbnail()) {\n\t\tconst auto &location = _data->thumbnailLocation();\n\t\treturn { location.width(), location.height() };\n\t}\n\treturn { 1, 1 };\n}\n\nQSize Gif::countThumbSize(int &inOutWidthMax) const {\n\tconst auto maxSize = _data->isVideoFile()\n\t\t? st::maxMediaSize\n\t\t: _data->isVideoMessage()\n\t\t? st::maxVideoMessageSize\n\t\t: st::maxGifSize;\n\tconst auto size = style::ConvertScale(videoSize());\n\taccumulate_min(inOutWidthMax, maxSize);\n\treturn DownscaledSize(size, { inOutWidthMax, maxSize });\n}\n\nQSize Gif::countOptimalSize() {\n\tif (_data->isVideoMessage() && _transcribe) {\n\t\tconst auto &entry = _data->session().api().transcribes().entry(\n\t\t\t_realParent);\n\t\t_transcribe->setLoading(\n\t\t\tentry.shown && (entry.requestId || entry.pending));\n\t}\n\n\tconst auto minWidth = std::clamp(\n\t\t_parent->minWidthForMedia(),\n\t\t(_parent->hasBubble()\n\t\t\t? st::historyPhotoBubbleMinWidth\n\t\t\t: st::minPhotoSize),\n\t\tst::maxMediaSize);\n\tauto thumbMaxWidth = st::msgMaxWidth;\n\tconst auto scaled = countThumbSize(thumbMaxWidth);\n\tauto maxWidth = std::min(\n\t\tstd::max(scaled.width(), minWidth),\n\t\tthumbMaxWidth);\n\tauto minHeight = qMax(scaled.height(), st::minPhotoSize);\n\tif (!activeCurrentStreamed()) {\n\t\taccumulate_max(\n\t\t\tmaxWidth,\n\t\t\tGifMaxStatusWidth(_data)\n\t\t\t\t+ 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x()));\n\t}\n\tif (_parent->hasBubble()) {\n\t\tmaxWidth = qMax(maxWidth, _parent->textualMaxWidth());\n\t\tminHeight = adjustHeightForLessCrop(\n\t\t\tscaled,\n\t\t\t{ maxWidth, minHeight });\n\t} else if (isUnwrapped()) {\n\t\tconst auto item = _parent->data();\n\t\tauto via = item->Get<HistoryMessageVia>();\n\t\tauto reply = _parent->Get<Reply>();\n\t\tauto forwarded = item->Get<HistoryMessageForwarded>();\n\t\tif (forwarded) {\n\t\t\tforwarded->create(via, item);\n\t\t}\n\t\tmaxWidth += additionalWidth(reply, via, forwarded);\n\t\taccumulate_max(maxWidth, _parent->reactionsOptimalWidth());\n\t}\n\treturn { maxWidth, minHeight };\n}\n\nQSize Gif::countCurrentSize(int newWidth) {\n\tauto availableWidth = newWidth;\n\n\tauto thumbMaxWidth = newWidth;\n\tconst auto scaled = countThumbSize(thumbMaxWidth);\n\tconst auto minWidthByInfo = _parent->infoWidth()\n\t\t+ 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x());\n\tnewWidth = std::clamp(\n\t\tstd::max(scaled.width(), minWidthByInfo),\n\t\tst::minPhotoSize,\n\t\tthumbMaxWidth);\n\tauto newHeight = qMax(scaled.height(), st::minPhotoSize);\n\tif (!activeCurrentStreamed()) {\n\t\taccumulate_max(\n\t\t\tnewWidth,\n\t\t\tGifMaxStatusWidth(_data)\n\t\t\t\t+ 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x()));\n\t}\n\tif (_parent->hasBubble()) {\n\t\taccumulate_max(newWidth, _parent->minWidthForMedia());\n\t\tauto captionMaxWidth = _parent->textualMaxWidth();\n\t\tconst auto botTop = _parent->Get<FakeBotAboutTop>();\n\t\tif (botTop) {\n\t\t\taccumulate_max(captionMaxWidth, botTop->maxWidth);\n\t\t}\n\t\tconst auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth);\n\t\tnewWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth);\n\t\tnewHeight = adjustHeightForLessCrop(\n\t\t\tscaled,\n\t\t\t{ newWidth, newHeight });\n\t} else if (isUnwrapped()) {\n\t\taccumulate_max(newWidth, _parent->reactionsOptimalWidth());\n\n\t\tconst auto item = _parent->data();\n\t\tauto via = item->Get<HistoryMessageVia>();\n\t\tauto reply = _parent->Get<Reply>();\n\t\tauto forwarded = item->Get<HistoryMessageForwarded>();\n\t\tif (via || reply || forwarded) {\n\t\t\tauto additional = additionalWidth(reply, via, forwarded);\n\t\t\tnewWidth += additional;\n\t\t\taccumulate_min(newWidth, availableWidth);\n\t\t\tauto usew = maxWidth() - additional;\n\t\t\tauto availw = newWidth - usew - st::msgReplyPadding.left() - st::msgReplyPadding.left() - st::msgReplyPadding.left();\n\t\t\tif (!forwarded && via) {\n\t\t\t\tvia->resize(availw);\n\t\t\t}\n\t\t\tif (reply) {\n\t\t\t\t[[maybe_unused]] int height = reply->resizeToWidth(availw);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { newWidth, newHeight };\n}\n\nint Gif::adjustHeightForLessCrop(QSize dimensions, QSize current) const {\n\tif (dimensions.isEmpty()) {\n\t\treturn current.height();\n\t}\n\t// Allow some more vertical space for less cropping,\n\t// but not more than 1.33 * existing height.\n\treturn qMax(\n\t\tcurrent.height(),\n\t\tqMin(\n\t\t\tcurrent.width() * dimensions.height() / dimensions.width(),\n\t\t\tcurrent.height() * 4 / 3));\n}\n\nQSize Gif::videoSize() const {\n\tif (const auto streamed = activeCurrentStreamed()) {\n\t\treturn streamed->player().videoSize();\n\t} else if (!_data->dimensions.isEmpty()) {\n\t\treturn _data->dimensions;\n\t} else if (_data->hasThumbnail()) {\n\t\tconst auto &location = _data->thumbnailLocation();\n\t\treturn QSize(location.width(), location.height());\n\t} else {\n\t\treturn QSize(1, 1);\n\t}\n}\n\nvoid Gif::validateRoundingMask(QSize size) const {\n\tif (_roundingMask.size() != size) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t_roundingMask = Images::EllipseMask(size / ratio);\n\t}\n}\n\nbool Gif::downloadInCorner() const {\n\treturn _data->isVideoFile()\n\t\t&& (_data->loading() || !autoplayEnabled())\n\t\t&& _realParent->allowsForward()\n\t\t&& _data->canBeStreamed()\n\t\t&& !_data->inappPlaybackFailed();\n}\n\nbool Gif::autoplayUnderCursor() const {\n\treturn (_videoTimestamp || _hasVideoCover);\n}\n\nbool Gif::underCursor() const {\n\treturn ClickHandler::getActive() == currentVideoLink();\n}\n\nbool Gif::autoplayEnabled() const {\n\tif (_realParent->isSponsored()) {\n\t\treturn true;\n\t}\n\treturn Data::AutoDownload::ShouldAutoPlay(\n\t\t_data->session().settings().autoDownload(),\n\t\t_realParent->history()->peer,\n\t\t_data);\n}\n\nbool Gif::hideMessageText() const {\n\treturn _data->isVideoMessage();\n}\n\nvoid Gif::draw(Painter &p, const PaintContext &context) const {\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;\n\n\t_smallGroupPart = false;\n\n\tensureDataMediaCreated();\n\tconst auto item = _parent->data();\n\tconst auto loaded = dataLoaded();\n\tconst auto displayLoading = (item->isSending() || _data->displayLoading());\n\tconst auto st = context.st;\n\tconst auto sti = context.imageStyle();\n\tconst auto cornerDownload = downloadInCorner();\n\tconst auto canBePlayed = _dataMedia->canBePlayed();\n\tconst auto autoplay = autoplayEnabled()\n\t\t&& canBePlayed\n\t\t&& CanPlayInline(_data);\n\tconst auto activeRoundPlaying = activeRoundStreamed();\n\n\tauto paintx = 0, painty = 0, paintw = width(), painth = height();\n\tconst bool bubble = _parent->hasBubble();\n\tconst auto rightLayout = _parent->hasRightLayout();\n\tconst auto inWebPage = (_parent->media() != this);\n\tconst auto isRound = _data->isVideoMessage();\n\n\tconst auto rounding = (inWebPage\n\t\t\t// Dangerous change.\n\t\t\t&& bubbleRounding() == Ui::BubbleRounding())\n\t\t? std::optional<Ui::BubbleRounding>()\n\t\t: adjustedBubbleRounding();\n\n\tauto usex = 0, usew = paintw;\n\tconst auto unwrapped = isUnwrapped();\n\tconst auto via = unwrapped ? item->Get<HistoryMessageVia>() : nullptr;\n\tconst auto reply = unwrapped ? _parent->Get<Reply>() : nullptr;\n\tconst auto forwarded = unwrapped ? item->Get<HistoryMessageForwarded>() : nullptr;\n\tconst auto rightAligned = unwrapped && rightLayout;\n\tif (via || reply || forwarded) {\n\t\tusew = maxWidth() - additionalWidth(reply, via, forwarded);\n\t\tif (rightAligned) {\n\t\t\tusex = width() - usew;\n\t\t}\n\t}\n\tif (isRound) {\n\t\taccumulate_min(usew, painth);\n\t}\n\tif (rtl()) usex = width() - usex - usew;\n\n\tQRect rthumb(style::rtlrect(usex + paintx, painty, usew, painth, width()));\n\n\tconst auto inTTLViewer = _parent->delegate()->elementContext()\n\t\t== Context::TTLViewer;\n\tconst auto revealed = (isRound\n\t\t\t&& item->media()->ttlSeconds()\n\t\t\t&& !inTTLViewer)\n\t\t? 0\n\t\t: (!isRound && _spoiler)\n\t\t? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.)\n\t\t: 1.;\n\tconst auto fullHiddenBySpoiler = (revealed == 0.);\n\tif (revealed < 1.) {\n\t\tvalidateSpoilerImageCache(rthumb.size(), rounding);\n\t}\n\n\tconst auto canStartPlay = autoplay\n\t\t&& !_streamed\n\t\t&& !activeRoundPlaying\n\t\t&& !fullHiddenBySpoiler;\n\tconst auto shouldBePlaying = !autoplayUnderCursor() || underCursor();\n\tif (!shouldBePlaying && _videoTimestamp != 0) {\n\t\tconst_cast<Gif*>(this)->stopAnimation();\n\t} else if (canStartPlay) {\n\t\tconst_cast<Gif*>(this)->playAnimation(true);\n\t} else {\n\t\tcheckStreamedIsStarted();\n\t}\n\tconst auto streamingMode = _streamed || activeRoundPlaying || autoplay;\n\tconst auto activeOwnPlaying = activeOwnStreamed();\n\n\tauto displayMute = false;\n\tconst auto streamed = activeRoundPlaying\n\t\t? activeRoundPlaying\n\t\t: activeOwnPlaying\n\t\t? &activeOwnPlaying->instance\n\t\t: nullptr;\n\tconst auto streamedForWaiting = activeRoundPlaying\n\t\t? activeRoundPlaying\n\t\t: _streamed\n\t\t? &_streamed->instance\n\t\t: nullptr;\n\n\tif (displayLoading\n\t\t&& (!streamedForWaiting\n\t\t\t|| item->isSending()\n\t\t\t|| _data->uploading()\n\t\t\t|| (cornerDownload && _data->loading()))) {\n\t\tensureAnimation();\n\t\tif (!_animation->radial.animating()) {\n\t\t\t_animation->radial.start(dataProgress());\n\t\t}\n\t}\n\tupdateStatusText();\n\tconst auto radial = isRadialAnimation()\n\t\t|| (streamedForWaiting && streamedForWaiting->waitingShown());\n\n\tif (!bubble && !unwrapped) {\n\t\tAssert(rounding.has_value());\n\t\tfillImageShadow(p, rthumb, *rounding, context);\n\t}\n\n\tconst auto skipDrawingContent = context.skipDrawingParts\n\t\t== PaintContext::SkipDrawingParts::Content;\n\tconst auto drawStreamed = streamed && (shouldBePlaying || !_videoCover);\n\tif (drawStreamed && !skipDrawingContent && !fullHiddenBySpoiler) {\n\t\tauto paused = context.paused || !shouldBePlaying;\n\t\tauto request = ::Media::Streaming::FrameRequest{\n\t\t\t.outer = QSize(usew, painth) * style::DevicePixelRatio(),\n\t\t\t.blurredBackground = true,\n\t\t};\n\t\tif (isRound) {\n\t\t\tif (activeRoundStreamed()) {\n\t\t\t\tpaused = false;\n\t\t\t} else {\n\t\t\t\tdisplayMute = true;\n\t\t\t}\n\t\t\tvalidateRoundingMask(request.outer);\n\t\t\trequest.mask = _roundingMask;\n\t\t} else {\n\t\t\trequest.rounding = MediaRoundingMask(rounding);\n\t\t}\n\t\tif (!activeRoundPlaying && activeOwnPlaying->instance.playerLocked()) {\n\t\t\tif (activeOwnPlaying->frozenFrame.isNull()) {\n\t\t\t\tactiveOwnPlaying->frozenRequest = request;\n\t\t\t\tactiveOwnPlaying->frozenFrame = streamed->frame(request);\n\t\t\t\tactiveOwnPlaying->frozenStatusText = _statusText;\n\t\t\t} else if (activeOwnPlaying->frozenRequest != request) {\n\t\t\t\tactiveOwnPlaying->frozenRequest = request;\n\t\t\t\tactiveOwnPlaying->frozenFrame = streamed->frame(request);\n\t\t\t}\n\t\t\tp.drawImage(rthumb, activeOwnPlaying->frozenFrame);\n\t\t} else {\n\t\t\tif (activeOwnPlaying\n\t\t\t\t&& !activeOwnPlaying->frozenFrame.isNull()) {\n\t\t\t\tactiveOwnPlaying->frozenFrame = QImage();\n\t\t\t\tactiveOwnPlaying->frozenStatusText = QString();\n\t\t\t}\n\n\t\t\tconst auto frame = streamed->frameWithInfo(request);\n\t\t\tp.drawImage(rthumb, frame.image);\n\t\t\tif (!paused) {\n\t\t\t\tstreamed->markFrameShown();\n\t\t\t}\n\t\t}\n\n\t\tif (const auto playback = videoPlayback()) {\n\t\t\tconst auto value = playback->value();\n\t\t\tif (value > 0.) {\n\t\t\t\tauto pen = st->historyVideoMessageProgressFg()->p;\n\t\t\t\tconst auto was = p.pen();\n\t\t\t\tpen.setWidth(st::radialLine);\n\t\t\t\tpen.setCapStyle(Qt::RoundCap);\n\t\t\t\tp.setPen(pen);\n\t\t\t\tp.setOpacity(st::historyVideoMessageProgressOpacity);\n\n\t\t\t\tconst auto from = arc::kQuarterLength;\n\t\t\t\tconst auto len = std::round(arc::kFullLength\n\t\t\t\t\t* (inTTLViewer ? (1. - value) : -value));\n\t\t\t\tconst auto stepInside = st::radialLine / 2;\n\t\t\t\t{\n\t\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\t\tp.drawArc(rthumb - Margins(stepInside), from, len);\n\t\t\t\t}\n\n\t\t\t\tp.setPen(was);\n\t\t\t\tp.setOpacity(1.);\n\t\t\t}\n\t\t}\n\t} else if (!skipDrawingContent && !fullHiddenBySpoiler) {\n\t\tensureDataMediaCreated();\n\t\tvalidateThumbCache({ usew, painth }, isRound, rounding);\n\t\tp.drawImage(rthumb, _thumbCache);\n\t}\n\tif (!isRound) {\n\t\tpaintTimestampMark(p, rthumb, rounding);\n\t}\n\n\tif (revealed < 1.) {\n\t\tp.setOpacity(1. - revealed);\n\t\tif (!isRound) {\n\t\t\tp.drawImage(rthumb.topLeft(), _spoiler->background);\n\t\t\tfillImageSpoiler(p, _spoiler.get(), rthumb, context);\n\t\t} else {\n\t\t\tauto frame = _spoiler->background;\n\t\t\t{\n\t\t\t\tauto q = QPainter(&frame);\n\t\t\t\tfillImageSpoiler(q, _spoiler.get(), rthumb, context);\n\t\t\t}\n\t\t\tp.drawImage(rthumb.topLeft(), Images::Circle(std::move(frame)));\n\t\t}\n\t\tp.setOpacity(1.);\n\t}\n\tif (context.selected()) {\n\t\tif (isRound) {\n\t\t\tUi::FillComplexEllipse(p, st, rthumb);\n\t\t} else {\n\t\t\tfillImageOverlay(p, rthumb, rounding, context);\n\t\t}\n\t}\n\n\tconst auto paintInCenter = !_sensitiveSpoiler\n\t\t&& (radial\n\t\t\t|| (!streamingMode\n\t\t\t\t&& ((!loaded && !_data->loading()) || !autoplay)));\n\tif (paintInCenter) {\n\t\tconst auto radialRevealed = 1.;\n\t\tconst auto opacity = (item->isSending() || _data->uploading())\n\t\t\t? 1.\n\t\t\t: streamedForWaiting\n\t\t\t? streamedForWaiting->waitingOpacity()\n\t\t\t: (radial && loaded)\n\t\t\t? _animation->radial.opacity()\n\t\t\t: 1.;\n\t\tconst auto radialOpacity = opacity * radialRevealed;\n\t\tconst auto innerSize = st::msgFileLayout.thumbSize;\n\t\tauto inner = QRect(rthumb.x() + (rthumb.width() - innerSize) / 2, rthumb.y() + (rthumb.height() - innerSize) / 2, innerSize, innerSize);\n\t\tp.setPen(Qt::NoPen);\n\t\tif (context.selected()) {\n\t\t\tp.setBrush(st->msgDateImgBgSelected());\n\t\t} else if (isThumbAnimation()) {\n\t\t\tauto over = _animation->a_thumbOver.value(1.);\n\t\t\tp.setBrush(anim::brush(st->msgDateImgBg(), st->msgDateImgBgOver(), over));\n\t\t} else {\n\t\t\tconst auto over = ClickHandler::showAsActive(\n\t\t\t\t(_data->loading() || _data->uploading()) ? _cancell : _savel);\n\t\t\tp.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());\n\t\t}\n\t\tp.setOpacity(radialOpacity * p.opacity());\n\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(inner);\n\t\t}\n\n\t\tp.setOpacity(radialOpacity);\n\t\tconst auto icon = [&]() -> const style::icon * {\n\t\t\tif (streamingMode && !_data->uploading()) {\n\t\t\t\treturn nullptr;\n\t\t\t} else if ((loaded || canBePlayed) && (!radial || cornerDownload)) {\n\t\t\t\treturn &sti->historyFileThumbPlay;\n\t\t\t} else if (radial || _data->loading()) {\n\t\t\t\tif (!item->isSending() || _data->uploading()) {\n\t\t\t\t\treturn &sti->historyFileThumbCancel;\n\t\t\t\t}\n\t\t\t\treturn nullptr;\n\t\t\t}\n\t\t\treturn &sti->historyFileThumbDownload;\n\t\t}();\n\t\tif (icon) {\n\t\t\ticon->paintInCenter(p, inner);\n\t\t}\n\t\tp.setOpacity(radialRevealed);\n\t\tif (radial) {\n\t\t\tQRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));\n\t\t\tif (streamedForWaiting && !_data->uploading()) {\n\t\t\t\tUi::InfiniteRadialAnimation::Draw(\n\t\t\t\t\tp,\n\t\t\t\t\tstreamedForWaiting->waitingState(),\n\t\t\t\t\trinner.topLeft(),\n\t\t\t\t\trinner.size(),\n\t\t\t\t\twidth(),\n\t\t\t\t\tsti->historyFileThumbRadialFg,\n\t\t\t\t\tst::msgFileRadialLine);\n\t\t\t} else if (!cornerDownload) {\n\t\t\t\t_animation->radial.draw(\n\t\t\t\t\tp,\n\t\t\t\t\trinner,\n\t\t\t\t\tst::msgFileRadialLine,\n\t\t\t\t\tsti->historyFileThumbRadialFg);\n\t\t\t}\n\t\t}\n\t\tp.setOpacity(1.);\n\t} else if (_sensitiveSpoiler) {\n\t\tdrawSpoilerTag(p, rthumb, context, [&] {\n\t\t\treturn spoilerTagBackground();\n\t\t});\n\t}\n\tif (displayMute) {\n\t\tauto muteRect = style::rtlrect(rthumb.x() + (rthumb.width() - st::historyVideoMessageMuteSize) / 2, rthumb.y() + st::msgDateImgDelta, st::historyVideoMessageMuteSize, st::historyVideoMessageMuteSize, width());\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(sti->msgDateImgBg);\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.drawEllipse(muteRect);\n\t\tsti->historyVideoMessageMute.paintInCenter(p, muteRect);\n\t}\n\n\tconst auto skipDrawingSurrounding = context.skipDrawingParts\n\t\t== PaintContext::SkipDrawingParts::Surrounding;\n\n\tif (!skipDrawingSurrounding && _purchasedPriceTag) {\n\t\tdrawPurchasedTag(p, rthumb, context);\n\t}\n\n\tif (!unwrapped && !skipDrawingSurrounding) {\n\t\tconst auto sponsoredSkip = !_data->isVideoFile()\n\t\t\t&& _realParent->isSponsored();\n\t\tif ((!isRound || !inWebPage) && !sponsoredSkip) {\n\t\t\tdrawCornerStatus(p, context, QPoint());\n\t\t}\n\t} else if (!skipDrawingSurrounding) {\n\t\tif (isRound) {\n\t\t\tconst auto mediaUnread = item->hasUnreadMediaFlag();\n\t\t\tauto statusW = st::normalFont->width(_statusText) + 2 * st::msgDateImgPadding.x();\n\t\t\tauto statusH = st::normalFont->height + 2 * st::msgDateImgPadding.y();\n\t\t\tauto statusX = usex + paintx + st::msgDateImgDelta + st::msgDateImgPadding.x();\n\t\t\tauto statusY = painty + painth - st::msgDateImgDelta - statusH + st::msgDateImgPadding.y();\n\t\t\tif (mediaUnread) {\n\t\t\t\tstatusW += st::mediaUnreadSkip + st::mediaUnreadSize;\n\t\t\t}\n\t\t\tUi::FillRoundRect(p, style::rtlrect(statusX - st::msgDateImgPadding.x(), statusY - st::msgDateImgPadding.y(), statusW, statusH, width()), sti->msgServiceBg, sti->msgServiceBgCornersSmall);\n\t\t\tp.setFont(st::normalFont);\n\t\t\tp.setPen(st->msgServiceFg());\n\t\t\tp.drawTextLeft(statusX, statusY, width(), _statusText, statusW - 2 * st::msgDateImgPadding.x());\n\t\t\tif (mediaUnread) {\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(st->msgServiceFg());\n\n\t\t\t\t{\n\t\t\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\t\t\tp.drawEllipse(style::rtlrect(statusX - st::msgDateImgPadding.x() + statusW - st::msgDateImgPadding.x() - st::mediaUnreadSize, statusY + st::mediaUnreadTop, st::mediaUnreadSize, st::mediaUnreadSize, width()));\n\t\t\t\t}\n\t\t\t}\n\t\t\tensureTranscribeButton();\n\t\t}\n\t\tif (via || reply || forwarded) {\n\t\t\tauto rectw = width() - usew - st::msgReplyPadding.left();\n\t\t\tauto innerw = rectw - (st::msgReplyPadding.left() + st::msgReplyPadding.right());\n\t\t\tauto recth = 0;\n\t\t\tauto forwardedHeightReal = forwarded ? forwarded->text.countHeight(innerw) : 0;\n\t\t\tauto forwardedHeight = qMin(forwardedHeightReal, kMaxGifForwardedBarLines * st::msgServiceNameFont->height);\n\t\t\tif (forwarded) {\n\t\t\t\trecth += st::msgReplyPadding.top() + forwardedHeight;\n\t\t\t} else if (via) {\n\t\t\t\trecth += st::msgReplyPadding.top() + st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0);\n\t\t\t}\n\t\t\tif (reply) {\n\t\t\t\tconst auto replyMargins = reply->margins();\n\t\t\t\trecth += reply->height()\n\t\t\t\t\t- ((forwarded || via) ? 0 : replyMargins.top())\n\t\t\t\t\t- replyMargins.bottom();\n\t\t\t} else {\n\t\t\t\trecth += st::msgReplyPadding.bottom();\n\t\t\t}\n\t\t\tint rectx = rightAligned ? 0 : (usew + st::msgReplyPadding.left());\n\t\t\tint recty = painty;\n\t\t\tif (rtl()) rectx = width() - rectx - rectw;\n\n\t\t\tUi::FillRoundRect(p, rectx, recty, rectw, recth, sti->msgServiceBg, sti->msgServiceBgCornersSmall);\n\t\t\tp.setPen(st->msgServiceFg());\n\t\t\tconst auto textx = rectx + st::msgReplyPadding.left();\n\t\t\tconst auto textw = rectw - st::msgReplyPadding.left() - st::msgReplyPadding.right();\n\t\t\tif (forwarded) {\n\t\t\t\tp.setTextPalette(st->serviceTextPalette());\n\t\t\t\tauto breakEverywhere = (forwardedHeightReal > forwardedHeight);\n\t\t\t\tforwarded->text.drawElided(p, textx, recty + st::msgReplyPadding.top(), textw, kMaxGifForwardedBarLines, style::al_left, 0, -1, 0, breakEverywhere);\n\t\t\t\tp.restoreTextPalette();\n\n\t\t\t\tconst auto skip = std::min(\n\t\t\t\t\tforwarded->text.countHeight(textw),\n\t\t\t\t\tkMaxGifForwardedBarLines * st::msgServiceNameFont->height);\n\t\t\t\trecty += skip;\n\t\t\t} else if (via) {\n\t\t\t\tp.setFont(st::msgServiceNameFont);\n\t\t\t\tp.drawTextLeft(textx, recty + st::msgReplyPadding.top(), 2 * textx + textw, via->text);\n\t\t\t\tint skip = st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0);\n\t\t\t\trecty += skip;\n\t\t\t}\n\t\t\tif (reply) {\n\t\t\t\tif (forwarded || via) {\n\t\t\t\t\trecty += st::msgReplyPadding.top();\n\t\t\t\t\trecth -= st::msgReplyPadding.top();\n\t\t\t\t} else {\n\t\t\t\t\trecty -= reply->margins().top();\n\t\t\t\t}\n\t\t\t\treply->paint(p, _parent, context, rectx, recty, rectw, false);\n\t\t\t}\n\t\t}\n\t}\n\tif (!inWebPage && !skipDrawingSurrounding) {\n\t\tauto fullRight = paintx + usex + usew;\n\t\tauto fullBottom = painty + painth;\n\t\tauto maxRight = _parent->width() - st::msgMargin.left();\n\t\tif (_parent->hasFromPhoto()) {\n\t\t\tmaxRight -= st::msgMargin.right();\n\t\t} else {\n\t\t\tmaxRight -= st::msgMargin.left();\n\t\t}\n\t\tif (unwrapped && !rightAligned) {\n\t\t\tauto infoWidth = _parent->infoWidth();\n\n\t\t\t// This is just some arbitrary point,\n\t\t\t// the main idea is to make info left aligned here.\n\t\t\tfullRight += infoWidth - st::normalFont->height;\n\t\t\tif (fullRight > maxRight) {\n\t\t\t\tfullRight = maxRight;\n\t\t\t}\n\t\t}\n\t\tif (isRound\n\t\t\t|| ((!bubble || isBubbleBottom()) && needInfoDisplay())) {\n\t\t\t_parent->drawInfo(\n\t\t\t\tp,\n\t\t\t\tcontext,\n\t\t\t\tfullRight,\n\t\t\t\tfullBottom - st::msgDateImgDelta,\n\t\t\t\t2 * paintx + paintw,\n\t\t\t\t(unwrapped\n\t\t\t\t\t? InfoDisplayType::Background\n\t\t\t\t\t: InfoDisplayType::Image));\n\t\t}\n\t\tif (const auto size = bubble ? std::nullopt : _parent->rightActionSize()\n\t\t\t; size || (_transcribe && !rightAligned)) {\n\t\t\tconst auto rightActionWidth = size\n\t\t\t\t? size->width()\n\t\t\t\t: _transcribe->size().width();\n\t\t\tauto fastShareLeft = rightLayout\n\t\t\t\t? (paintx + usex - size->width() - st::historyFastShareLeft)\n\t\t\t\t: (fullRight + st::historyFastShareLeft);\n\t\t\tauto fastShareTop = fullBottom\n\t\t\t\t- st::historyFastShareBottom\n\t\t\t\t- (size ? size->height() : 0);\n\t\t\tif (fastShareLeft + rightActionWidth > maxRight) {\n\t\t\t\tfastShareLeft = fullRight\n\t\t\t\t\t- rightActionWidth\n\t\t\t\t\t- st::msgDateImgDelta;\n\t\t\t\tfastShareTop -= st::msgDateImgDelta\n\t\t\t\t\t+ st::msgDateImgPadding.y()\n\t\t\t\t\t+ st::msgDateFont->height\n\t\t\t\t\t+ st::msgDateImgPadding.y();\n\t\t\t}\n\t\t\tif (size) {\n\t\t\t\t_parent->drawRightAction(p, context, fastShareLeft, fastShareTop, 2 * paintx + paintw);\n\t\t\t}\n\t\t\tif (_transcribe) {\n\t\t\t\tpaintTranscribe(p, fastShareLeft, fastShareTop, true, context);\n\t\t\t}\n\t\t} else if (rightAligned && _transcribe) {\n\t\t\tpaintTranscribe(p, usex, fullBottom, false, context);\n\t\t}\n\t}\n\tif (_drawTtl) {\n\t\t_drawTtl(p, rthumb, context);\n\t}\n}\n\nvoid Gif::paintTranscribe(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tbool right,\n\t\tconst PaintContext &context) const {\n\tif (!_transcribe) {\n\t\treturn;\n\t}\n\tconst auto s = _transcribe->size();\n\t_transcribe->paint(\n\t\tp,\n\t\tx - (right ? 0 : s.width()),\n\t\ty - s.height() - st::msgDateImgDelta,\n\t\tcontext);\n}\n\nvoid Gif::paintTimestampMark(\n\t\tPainter &p,\n\t\tQRect rthumb,\n\t\tstd::optional<Ui::BubbleRounding> rounding) const {\n\tif (_videoTimestamp <= 0 && _videoPosition < crl::time(200)) {\n\t\treturn;\n\t}\n\tconst auto convert = [](Ui::BubbleCornerRounding rounding) {\n\t\treturn (rounding == Ui::BubbleCornerRounding::Small)\n\t\t\t? Ui::BubbleRadiusSmall()\n\t\t\t: (rounding == Ui::BubbleCornerRounding::Large)\n\t\t\t? Ui::BubbleRadiusLarge()\n\t\t\t: 0;\n\t};\n\tconst auto radiusl = rounding\n\t\t? convert(rounding->bottomLeft)\n\t\t: st::roundRadiusSmall;\n\tconst auto radiusr = rounding\n\t\t? convert(rounding->bottomRight)\n\t\t: st::roundRadiusSmall;\n\tconst auto line = st::historyVideoTimestampProgressLine;\n\tconst auto duration = _data->duration();\n\tconst auto position = (_videoPosition > 0)\n\t\t? _videoPosition\n\t\t: (_videoTimestamp * crl::time(1000));\n\tif (rthumb.height() <= line\n\t\t|| rthumb.width() <= radiusl + radiusr\n\t\t|| position > duration) {\n\t\treturn;\n\t}\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto used = rthumb.width() - radiusl - radiusr;\n\tconst auto progress = position / float64(duration);\n\tconst auto edge = radiusl + int(base::SafeRound(used * progress));\n\tconst auto top = rthumb.y() + rthumb.height() - line;\n\tp.save();\n\tp.setPen(Qt::NoPen);\n\tif (edge > 0) {\n\t\tp.setBrush(st::windowBgActive);\n\n\t\tp.setClipRect(rthumb.x(), top, edge, line);\n\t\tp.drawRoundedRect(\n\t\t\trthumb.x(),\n\t\t\ttop - 2 * radiusl,\n\t\t\tedge + radiusl,\n\t\t\tline + 2 * radiusl,\n\t\t\tradiusl,\n\t\t\tradiusl);\n\t}\n\tif (const auto width = rthumb.width() - edge; width > 0) {\n\t\tconst auto left = rthumb.x() + edge;\n\t\tp.setBrush(st::mediaviewPlaybackProgressFg);\n\t\tp.setClipRect(left, top, width, line);\n\t\tp.drawRoundedRect(\n\t\t\tleft - radiusr,\n\t\t\ttop - 2 * radiusr,\n\t\t\twidth + radiusr,\n\t\t\tline + 2 * radiusr,\n\t\t\tradiusr,\n\t\t\tradiusr);\n\t}\n\tp.restore();\n}\n\nvoid Gif::drawSpoilerTag(\n\t\tPainter &p,\n\t\tQRect rthumb,\n\t\tconst PaintContext &context,\n\t\tFn<QImage()> generateBackground) const {\n\tMedia::drawSpoilerTag(\n\t\tp,\n\t\t_spoiler.get(),\n\t\t_spoilerTag,\n\t\trthumb,\n\t\tcontext,\n\t\tstd::move(generateBackground));\n}\n\nClickHandlerPtr Gif::spoilerTagLink() const {\n\treturn Media::spoilerTagLink(_spoiler.get(), _spoilerTag);\n}\n\nQImage Gif::spoilerTagBackground() const {\n\treturn _spoiler ? _spoiler->background : QImage();\n}\n\nvoid Gif::validateVideoThumbnail() const {\n\tExpects(!_videoCover);\n\n\tconst auto content = _dataMedia->videoThumbnailContent();\n\tif (_videoThumbnailFrame || content.isEmpty()) {\n\t\treturn;\n\t}\n\tauto info = v::get<Ui::PreparedFileInformation::Video>(\n\t\t::Media::Clip::PrepareForSending(QString(), content).media);\n\t_videoThumbnailFrame = std::make_unique<Image>(info.thumbnail.isNull()\n\t\t? Image::BlankMedia()->original()\n\t\t: info.thumbnail);\n}\n\nvoid Gif::validateThumbCache(\n\t\tQSize outer,\n\t\tbool isEllipse,\n\t\tstd::optional<Ui::BubbleRounding> rounding) const {\n\tconst auto good = _videoCoverMedia\n\t\t? _videoCoverMedia->image(Data::PhotoSize::Large)\n\t\t: _dataMedia->goodThumbnail();\n\tconst auto normal = good\n\t\t? good\n\t\t: _videoCoverMedia\n\t\t? nullptr\n\t\t: _dataMedia->thumbnail();\n\tif (!normal) {\n\t\tif (_videoCoverMedia) {\n\t\t\t_videoCover->load(Data::PhotoSize::Small, _realParent->fullId());\n\t\t} else {\n\t\t\t_data->loadThumbnail(_realParent->fullId());\n\t\t\tvalidateVideoThumbnail();\n\t\t}\n\t}\n\tconst auto videothumb = (normal || _videoCoverMedia)\n\t\t? nullptr\n\t\t: _videoThumbnailFrame.get();\n\tconst auto blurred = normal\n\t\t? (!good\n\t\t\t&& (normal->width() < kUseNonBlurredThreshold)\n\t\t\t&& (normal->height() < kUseNonBlurredThreshold))\n\t\t: !videothumb;\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (_thumbCache.size() == (outer * ratio)\n\t\t&& _thumbCacheRounding == rounding\n\t\t&& _thumbCacheBlurred == blurred\n\t\t&& _thumbIsEllipse == isEllipse) {\n\t\treturn;\n\t}\n\tauto cache = prepareThumbCache(outer);\n\t_thumbCache = isEllipse\n\t\t? Images::Circle(std::move(cache))\n\t\t: Images::Round(std::move(cache), MediaRoundingMask(rounding));\n\t_thumbCacheRounding = rounding;\n\t_thumbCacheBlurred = blurred;\n}\n\nQImage Gif::prepareThumbCache(QSize outer) const {\n\tconst auto good = _videoCoverMedia\n\t\t? _videoCoverMedia->image(Data::PhotoSize::Large)\n\t\t: _dataMedia->goodThumbnail();\n\tconst auto normal = good\n\t\t? good\n\t\t: _videoCoverMedia\n\t\t? nullptr\n\t\t: _dataMedia->thumbnail();\n\tconst auto videothumb = (normal || _videoCoverMedia)\n\t\t? nullptr\n\t\t: _videoThumbnailFrame.get();\n\tauto blurred = (!good\n\t\t&& normal\n\t\t&& (normal->width() < kUseNonBlurredThreshold)\n\t\t&& (normal->height() < kUseNonBlurredThreshold))\n\t\t? normal\n\t\t: nullptr;\n\tconst auto blurFromLarge = good || (normal && !blurred);\n\tconst auto large = blurFromLarge ? normal : videothumb;\n\tif (videothumb) {\n\t} else if (_videoCoverMedia) {\n\t\tif (const auto embedded = _videoCoverMedia->thumbnailInline()) {\n\t\t\tblurred = embedded;\n\t\t}\n\t} else if (const auto embedded = _dataMedia->thumbnailInline()) {\n\t\tblurred = embedded;\n\t}\n\tconst auto resize = large\n\t\t? ::Media::Streaming::DecideVideoFrameResize(\n\t\t\touter,\n\t\t\tgood ? large->size() : _data->dimensions)\n\t\t: ::Media::Streaming::ExpandDecision();\n\treturn PrepareWithBlurredBackground(\n\t\touter,\n\t\tresize,\n\t\tlarge,\n\t\tblurFromLarge ? large : blurred);\n}\n\nvoid Gif::validateSpoilerImageCache(\n\t\tQSize outer,\n\t\tstd::optional<Ui::BubbleRounding> rounding) const {\n\tExpects(_spoiler != nullptr);\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (_spoiler->background.size() == (outer * ratio)\n\t\t&& _spoiler->backgroundRounding == rounding) {\n\t\treturn;\n\t}\n\tconst auto normal = _videoCoverMedia\n\t\t? _videoCoverMedia->image(Data::PhotoSize::Small)\n\t\t: _dataMedia->thumbnail();\n\tauto container = std::optional<Image>();\n\tconst auto downscale = [&](Image *image) {\n\t\tif (!image || (image->width() <= 40 && image->height() <= 40)) {\n\t\t\treturn image;\n\t\t}\n\t\tcontainer.emplace(image->original().scaled(\n\t\t\t{ 40, 40 },\n\t\t\tQt::KeepAspectRatio,\n\t\t\tQt::SmoothTransformation));\n\t\treturn &*container;\n\t};\n\tconst auto embedded = _videoCoverMedia\n\t\t? _videoCoverMedia->thumbnailInline()\n\t\t: _dataMedia->thumbnailInline();\n\tconst auto blurred = embedded ? embedded : downscale(normal);\n\t_spoiler->background = Images::Round(\n\t\tPrepareWithBlurredBackground(\n\t\t\touter,\n\t\t\t::Media::Streaming::ExpandDecision(),\n\t\t\tnullptr,\n\t\t\tblurred),\n\t\tMediaRoundingMask(rounding));\n\t_spoiler->backgroundRounding = rounding;\n}\n\nvoid Gif::drawCornerStatus(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tQPoint position) const {\n\tif (!needCornerStatusDisplay()) {\n\t\treturn;\n\t}\n\tconst auto own = activeOwnStreamed();\n\tconst auto st = context.st;\n\tconst auto sti = context.imageStyle();\n\tconst auto text = (own && !own->frozenStatusText.isEmpty())\n\t\t? own->frozenStatusText\n\t\t: _statusText;\n\tconst auto padding = st::msgDateImgPadding;\n\tconst auto radial = _animation && _animation->radial.animating();\n\tconst auto cornerDownload = downloadInCorner() && !dataLoaded() && !_data->loadedInMediaCache();\n\tconst auto cornerMute = _streamed && _data->isVideoFile() && !cornerDownload;\n\tconst auto addLeft = cornerDownload ? (st::historyVideoDownloadSize + 2 * padding.y()) : 0;\n\tconst auto addRight = cornerMute ? st::historyVideoMuteSize : 0;\n\tconst auto downloadWidth = cornerDownload ? st::normalFont->width(_downloadSize) : 0;\n\tconst auto statusW = std::max(downloadWidth, st::normalFont->width(text)) + 2 * padding.x() + addLeft + addRight;\n\tconst auto statusH = cornerDownload ? (st::historyVideoDownloadSize + 2 * padding.y()) : (st::normalFont->height + 2 * padding.y());\n\tconst auto statusX = position.x() + st::msgDateImgDelta + padding.x();\n\tconst auto statusY = position.y() + st::msgDateImgDelta + padding.y();\n\tconst auto around = style::rtlrect(statusX - padding.x(), statusY - padding.y(), statusW, statusH, width());\n\tconst auto statusTextTop = statusY + (cornerDownload ? (((statusH - 2 * st::normalFont->height) / 3) - padding.y()) : 0);\n\tUi::FillRoundRect(p, around, sti->msgDateImgBg, sti->msgDateImgBgCorners);\n\tp.setFont(st::normalFont);\n\tp.setPen(st->msgDateImgFg());\n\tp.drawTextLeft(statusX + addLeft, statusTextTop, width(), text, statusW - 2 * padding.x());\n\tif (cornerDownload) {\n\t\tconst auto downloadTextTop = statusY + st::normalFont->height + (2 * (statusH - 2 * st::normalFont->height) / 3) - padding.y();\n\t\tp.drawTextLeft(statusX + addLeft, downloadTextTop, width(), _downloadSize, statusW - 2 * padding.x());\n\t\tconst auto inner = QRect(statusX + padding.y() - padding.x(), statusY, st::historyVideoDownloadSize, st::historyVideoDownloadSize);\n\t\tconst auto &icon = _data->loading()\n\t\t\t? sti->historyVideoCancel\n\t\t\t: sti->historyVideoDownload;\n\t\ticon.paintInCenter(p, inner);\n\t\tif (radial) {\n\t\t\tQRect rinner(inner.marginsRemoved(QMargins(st::historyVideoRadialLine, st::historyVideoRadialLine, st::historyVideoRadialLine, st::historyVideoRadialLine)));\n\t\t\t_animation->radial.draw(p, rinner, st::historyVideoRadialLine, sti->historyFileThumbRadialFg);\n\t\t}\n\t} else if (cornerMute) {\n\t\tsti->historyVideoMessageMute.paint(p, statusX - padding.x() - padding.y() + statusW - addRight, statusY - padding.y() + (statusH - st::historyVideoMessageMute.height()) / 2, width());\n\t}\n}\n\nTextState Gif::cornerStatusTextState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tQPoint position) const {\n\tauto result = TextState(_parent);\n\tif (!needCornerStatusDisplay() || !downloadInCorner() || dataLoaded()) {\n\t\treturn result;\n\t}\n\tconst auto padding = st::msgDateImgPadding;\n\tconst auto statusX = position.x() + st::msgDateImgDelta + padding.x();\n\tconst auto statusY = position.y() + st::msgDateImgDelta + padding.y();\n\tconst auto inner = QRect(statusX + padding.y() - padding.x(), statusY, st::historyVideoDownloadSize, st::historyVideoDownloadSize);\n\tif (inner.contains(point)) {\n\t\tresult.link = _data->loading() ? _cancell : _savel;\n\t}\n\treturn result;\n}\n\nTextState Gif::textState(QPoint point, StateRequest request) const {\n\tauto result = TextState(_parent);\n\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {\n\t\treturn result;\n\t}\n\tauto paintx = 0, painty = 0, paintw = width(), painth = height();\n\tauto bubble = _parent->hasBubble();\n\n\tconst auto rightLayout = _parent->hasRightLayout();\n\tconst auto inWebPage = (_parent->media() != this);\n\tconst auto isRound = _data->isVideoMessage();\n\tconst auto unwrapped = isUnwrapped();\n\tconst auto item = _parent->data();\n\tauto usew = paintw, usex = 0;\n\tconst auto via = unwrapped ? item->Get<HistoryMessageVia>() : nullptr;\n\tconst auto reply = unwrapped ? _parent->Get<Reply>() : nullptr;\n\tconst auto forwarded = unwrapped ? item->Get<HistoryMessageForwarded>() : nullptr;\n\tconst auto rightAligned = unwrapped && rightLayout;\n\tif (via || reply || forwarded) {\n\t\tusew = maxWidth() - additionalWidth(reply, via, forwarded);\n\t\tif (rightAligned) {\n\t\t\tusex = width() - usew;\n\t\t}\n\t}\n\tif (isRound) {\n\t\taccumulate_min(usew, painth);\n\t}\n\tif (rtl()) usex = width() - usex - usew;\n\n\tif (via || reply || forwarded) {\n\t\tauto rectw = paintw - usew - st::msgReplyPadding.left();\n\t\tauto innerw = rectw - (st::msgReplyPadding.left() + st::msgReplyPadding.right());\n\t\tauto recth = 0;\n\t\tauto forwardedHeightReal = forwarded ? forwarded->text.countHeight(innerw) : 0;\n\t\tauto forwardedHeight = qMin(forwardedHeightReal, kMaxGifForwardedBarLines * st::msgServiceNameFont->height);\n\t\tif (forwarded) {\n\t\t\trecth += st::msgReplyPadding.top() + forwardedHeight;\n\t\t} else if (via) {\n\t\t\trecth += st::msgReplyPadding.top() + st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0);\n\t\t}\n\t\tif (reply) {\n\t\t\tconst auto replyMargins = reply->margins();\n\t\t\trecth += reply->height()\n\t\t\t\t- ((forwarded || via) ? 0 : replyMargins.top())\n\t\t\t\t- replyMargins.bottom();\n\t\t} else {\n\t\t\trecth += st::msgReplyPadding.bottom();\n\t\t}\n\t\tauto rectx = rightAligned ? 0 : (usew + st::msgReplyPadding.left());\n\t\tauto recty = painty;\n\t\tif (rtl()) rectx = width() - rectx - rectw;\n\n\t\tif (forwarded) {\n\t\t\tif (QRect(rectx, recty, rectw, st::msgReplyPadding.top() + forwardedHeight).contains(point)) {\n\t\t\t\tauto breakEverywhere = (forwardedHeightReal > forwardedHeight);\n\t\t\t\tauto textRequest = request.forText();\n\t\t\t\tif (breakEverywhere) {\n\t\t\t\t\ttextRequest.flags |= Ui::Text::StateRequest::Flag::BreakEverywhere;\n\t\t\t\t}\n\t\t\t\tresult = TextState(_parent, forwarded->text.getState(\n\t\t\t\t\tpoint - QPoint(rectx + st::msgReplyPadding.left(), recty + st::msgReplyPadding.top()),\n\t\t\t\t\tinnerw,\n\t\t\t\t\ttextRequest));\n\t\t\t\tresult.symbol = 0;\n\t\t\t\tresult.afterSymbol = false;\n\t\t\t\tif (breakEverywhere) {\n\t\t\t\t\tresult.cursor = CursorState::Forwarded;\n\t\t\t\t} else {\n\t\t\t\t\tresult.cursor = CursorState::None;\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\trecty += forwardedHeight;\n\t\t\trecth -= forwardedHeight;\n\t\t} else if (via) {\n\t\t\tauto viah = st::msgReplyPadding.top() + st::msgServiceNameFont->height + (reply ? 0 : st::msgReplyPadding.bottom());\n\t\t\tif (QRect(rectx, recty, rectw, viah).contains(point)) {\n\t\t\t\tresult.link = via->link;\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tauto skip = st::msgServiceNameFont->height + (reply ? 2 * st::msgReplyPadding.top() : 0);\n\t\t\trecty += skip;\n\t\t\trecth -= skip;\n\t\t}\n\t\tif (reply) {\n\t\t\tif (forwarded || via) {\n\t\t\t\trecty += st::msgReplyPadding.top();\n\t\t\t\trecth -= st::msgReplyPadding.top() + reply->margins().top();\n\t\t\t} else {\n\t\t\t\trecty -= reply->margins().top();\n\t\t\t}\n\t\t\tconst auto replyRect = QRect(rectx, recty, rectw, recth);\n\t\t\tif (replyRect.contains(point)) {\n\t\t\t\tresult.link = reply->link();\n\t\t\t\treply->saveRipplePoint(point - replyRect.topLeft());\n\t\t\t\treply->createRippleAnimation(_parent, replyRect.size());\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\tif (!unwrapped) {\n\t\tif (const auto state = cornerStatusTextState(point, request, QPoint()); state.link) {\n\t\t\treturn state;\n\t\t}\n\t}\n\tif (QRect(usex + paintx, painty, usew, painth).contains(point)) {\n\t\tensureDataMediaCreated();\n\t\tresult.link = (_spoiler && !_spoiler->revealed)\n\t\t\t? (_sensitiveSpoiler\n\t\t\t\t? spoilerTagLink()\n\t\t\t\t: (isRound && _parent->data()->media()->ttlSeconds())\n\t\t\t\t? _openl // Overriden.\n\t\t\t\t: _spoiler->link)\n\t\t\t: currentVideoLink();\n\t}\n\tconst auto checkBottomInfo = !inWebPage\n\t\t&& (unwrapped || !bubble || isBubbleBottom());\n\tif (checkBottomInfo) {\n\t\tauto fullRight = usex + paintx + usew;\n\t\tauto fullBottom = painty + painth;\n\t\tauto maxRight = _parent->width() - st::msgMargin.left();\n\t\tif (_parent->hasFromPhoto()) {\n\t\t\tmaxRight -= st::msgMargin.right();\n\t\t} else {\n\t\t\tmaxRight -= st::msgMargin.left();\n\t\t}\n\t\tif (unwrapped && !rightAligned) {\n\t\t\tauto infoWidth = _parent->infoWidth();\n\n\t\t\t// This is just some arbitrary point,\n\t\t\t// the main idea is to make info left aligned here.\n\t\t\tfullRight += infoWidth - st::normalFont->height;\n\t\t\tif (fullRight > maxRight) {\n\t\t\t\tfullRight = maxRight;\n\t\t\t}\n\t\t}\n\t\tconst auto bottomInfoResult = _parent->bottomInfoTextState(\n\t\t\tfullRight,\n\t\t\tfullBottom,\n\t\t\tpoint,\n\t\t\t(unwrapped\n\t\t\t\t? InfoDisplayType::Background\n\t\t\t\t: InfoDisplayType::Image));\n\t\tif (bottomInfoResult.link\n\t\t\t|| bottomInfoResult.cursor != CursorState::None\n\t\t\t|| bottomInfoResult.customTooltip) {\n\t\t\treturn bottomInfoResult;\n\t\t}\n\t\tif (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) {\n\t\t\tconst auto rightActionWidth = size->width();\n\t\t\tauto fastShareLeft = _parent->hasRightLayout()\n\t\t\t\t? (paintx + usex - size->width() - st::historyFastShareLeft)\n\t\t\t\t: (fullRight + st::historyFastShareLeft);\n\t\t\tauto fastShareTop = fullBottom\n\t\t\t\t- st::historyFastShareBottom\n\t\t\t\t- size->height();\n\t\t\tif (fastShareLeft + rightActionWidth > maxRight) {\n\t\t\t\tfastShareLeft = fullRight\n\t\t\t\t\t- rightActionWidth\n\t\t\t\t\t- st::msgDateImgDelta;\n\t\t\t\tfastShareTop -= st::msgDateImgDelta\n\t\t\t\t\t+ st::msgDateImgPadding.y()\n\t\t\t\t\t+ st::msgDateFont->height\n\t\t\t\t\t+ st::msgDateImgPadding.y();\n\t\t\t}\n\t\t\tif (QRect(QPoint(fastShareLeft, fastShareTop), *size).contains(point)) {\n\t\t\t\tresult.link = _parent->rightActionLink(point\n\t\t\t\t\t- QPoint(fastShareLeft, fastShareTop));\n\t\t\t}\n\t\t}\n\t\tif (_transcribe && _transcribe->contains(point)) {\n\t\t\tresult.link = _transcribe->link();\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid Gif::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) {\n\tFile::clickHandlerPressedChanged(handler, pressed);\n\tif (!handler) {\n\t\treturn;\n\t} else if (_transcribe && (handler == _transcribe->link())) {\n\t\tif (pressed) {\n\t\t\t_transcribe->addRipple([=] { repaint(); });\n\t\t} else {\n\t\t\t_transcribe->stopRipple();\n\t\t}\n\t}\n}\n\nbool Gif::fullFeaturedGrouped(RectParts sides) const {\n\treturn (sides & RectPart::Left) && (sides & RectPart::Right);\n}\n\nQSize Gif::sizeForGroupingOptimal(int maxWidth, bool last) const {\n\treturn sizeForAspectRatio();\n}\n\nQSize Gif::sizeForGrouping(int width) const {\n\treturn sizeForAspectRatio();\n}\n\nvoid Gif::drawGrouped(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry,\n\t\tRectParts sides,\n\t\tUi::BubbleRounding rounding,\n\t\tfloat64 highlightOpacity,\n\t\tnot_null<uint64*> cacheKey,\n\t\tnot_null<QPixmap*> cache) const {\n\tensureDataMediaCreated();\n\tconst auto item = _parent->data();\n\tconst auto loaded = dataLoaded();\n\tconst auto displayLoading = item->isSending()\n\t\t|| item->hasFailed()\n\t\t|| _data->displayLoading();\n\tconst auto st = context.st;\n\tconst auto sti = context.imageStyle();\n\t_smallGroupPart = !fullFeaturedGrouped(sides);\n\tconst auto cornerDownload = !_smallGroupPart && downloadInCorner();\n\tconst auto canBePlayed = _dataMedia->canBePlayed();\n\n\tconst auto revealed = _spoiler\n\t\t? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.)\n\t\t: 1.;\n\tconst auto fullHiddenBySpoiler = (revealed == 0.);\n\tif (revealed < 1.) {\n\t\tvalidateSpoilerImageCache(geometry.size(), rounding);\n\t}\n\n\tconst auto autoplay = !_smallGroupPart\n\t\t&& autoplayEnabled()\n\t\t&& canBePlayed\n\t\t&& CanPlayInline(_data);\n\tconst auto canStartPlay = autoplay\n\t\t&& !_streamed\n\t\t&& !fullHiddenBySpoiler;\n\tconst auto shouldBePlaying = !autoplayUnderCursor() || underCursor();\n\tif (!shouldBePlaying && _videoTimestamp != 0) {\n\t\tconst_cast<Gif*>(this)->stopAnimation();\n\t} else if (canStartPlay) {\n\t\tconst_cast<Gif*>(this)->playAnimation(true);\n\t} else {\n\t\tcheckStreamedIsStarted();\n\t}\n\n\tconst auto streamingMode = _streamed || autoplay;\n\tconst auto activeOwnPlaying = activeOwnStreamed();\n\n\tconst auto streamed = activeOwnPlaying\n\t\t? &activeOwnPlaying->instance\n\t\t: nullptr;\n\tconst auto streamedForWaiting = _streamed\n\t\t? &_streamed->instance\n\t\t: nullptr;\n\n\tif (displayLoading\n\t\t&& (!streamedForWaiting\n\t\t\t|| item->isSending()\n\t\t\t|| _data->uploading()\n\t\t\t|| (cornerDownload && _data->loading()))) {\n\t\tensureAnimation();\n\t\tif (!_animation->radial.animating()) {\n\t\t\t_animation->radial.start(dataProgress());\n\t\t}\n\t}\n\tupdateStatusText();\n\tconst auto radial = isRadialAnimation()\n\t\t|| (streamedForWaiting && streamedForWaiting->waitingShown());\n\n\tif (streamed && !fullHiddenBySpoiler) {\n\t\tconst auto original = sizeForAspectRatio();\n\t\tconst auto originalWidth = style::ConvertScale(original.width());\n\t\tconst auto originalHeight = style::ConvertScale(original.height());\n\t\tconst auto pixSize = Ui::GetImageScaleSizeForGeometry(\n\t\t\t{ originalWidth, originalHeight },\n\t\t\t{ geometry.width(), geometry.height() });\n\t\tauto request = ::Media::Streaming::FrameRequest{\n\t\t\t.resize = pixSize * style::DevicePixelRatio(),\n\t\t\t.outer = geometry.size() * style::DevicePixelRatio(),\n\t\t\t.rounding = MediaRoundingMask(rounding),\n\t\t};\n\t\tif (activeOwnPlaying->instance.playerLocked()) {\n\t\t\tif (activeOwnPlaying->frozenFrame.isNull()) {\n\t\t\t\tactiveOwnPlaying->frozenRequest = request;\n\t\t\t\tactiveOwnPlaying->frozenFrame = streamed->frame(request);\n\t\t\t\tactiveOwnPlaying->frozenStatusText = _statusText;\n\t\t\t} else if (activeOwnPlaying->frozenRequest != request) {\n\t\t\t\tactiveOwnPlaying->frozenRequest = request;\n\t\t\t\tactiveOwnPlaying->frozenFrame = streamed->frame(request);\n\t\t\t}\n\t\t\tp.drawImage(geometry, activeOwnPlaying->frozenFrame);\n\t\t} else {\n\t\t\tif (activeOwnPlaying) {\n\t\t\t\tactiveOwnPlaying->frozenFrame = QImage();\n\t\t\t\tactiveOwnPlaying->frozenStatusText = QString();\n\t\t\t}\n\t\t\tp.drawImage(geometry, streamed->frame(request));\n\t\t\tconst auto paused = context.paused\n\t\t\t\t|| (autoplayUnderCursor() && !underCursor());\n\t\t\tif (!paused) {\n\t\t\t\tstreamed->markFrameShown();\n\t\t\t}\n\t\t}\n\t} else if (!fullHiddenBySpoiler) {\n\t\tvalidateGroupedCache(geometry, rounding, cacheKey, cache);\n\t\tp.drawPixmap(geometry, *cache);\n\t}\n\n\tif (revealed < 1.) {\n\t\tp.setOpacity(1. - revealed);\n\t\tp.drawImage(geometry.topLeft(), _spoiler->background);\n\t\tfillImageSpoiler(p, _spoiler.get(), geometry, context);\n\t\tp.setOpacity(1.);\n\t}\n\n\tconst auto overlayOpacity = context.selected()\n\t\t? (1. - highlightOpacity)\n\t\t: highlightOpacity;\n\tif (overlayOpacity > 0.) {\n\t\tp.setOpacity(overlayOpacity);\n\t\tfillImageOverlay(p, geometry, rounding, context);\n\t\tif (!context.selected()) {\n\t\t\tfillImageOverlay(p, geometry, rounding, context);\n\t\t}\n\t\tp.setOpacity(1.);\n\t}\n\n\tconst auto paintInCenter = !_sensitiveSpoiler\n\t\t&& (radial\n\t\t\t|| (!streamingMode\n\t\t\t\t&& ((!loaded && !_data->loading()) || !autoplay)));\n\tif (paintInCenter) {\n\t\tconst auto radialRevealed = 1.;\n\t\tconst auto opacity = (item->isSending() || _data->uploading())\n\t\t\t? 1.\n\t\t\t: streamedForWaiting\n\t\t\t? streamedForWaiting->waitingOpacity()\n\t\t\t: (radial && loaded)\n\t\t\t? _animation->radial.opacity()\n\t\t\t: 1.;\n\t\tconst auto radialOpacity = opacity * radialRevealed;\n\t\tconst auto radialSize = st::historyGroupRadialSize;\n\t\tconst auto inner = QRect(\n\t\t\tgeometry.x() + (geometry.width() - radialSize) / 2,\n\t\t\tgeometry.y() + (geometry.height() - radialSize) / 2,\n\t\t\tradialSize,\n\t\t\tradialSize);\n\t\tp.setPen(Qt::NoPen);\n\t\tif (context.selected()) {\n\t\t\tp.setBrush(st->msgDateImgBgSelected());\n\t\t} else if (isThumbAnimation()) {\n\t\t\tauto over = _animation->a_thumbOver.value(1.);\n\t\t\tp.setBrush(anim::brush(st->msgDateImgBg(), st->msgDateImgBgOver(), over));\n\t\t} else {\n\t\t\tauto over = ClickHandler::showAsActive(\n\t\t\t\t(_data->loading() || _data->uploading()) ? _cancell : _savel);\n\t\t\tp.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());\n\t\t}\n\t\tp.setOpacity(radialOpacity * p.opacity());\n\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(inner);\n\t\t}\n\n\t\tp.setOpacity(radialOpacity);\n\t\tconst auto icon = [&]() -> const style::icon * {\n\t\t\tif (_data->waitingForAlbum()) {\n\t\t\t\treturn &sti->historyFileThumbWaiting;\n\t\t\t} else if (streamingMode && !_data->uploading()) {\n\t\t\t\treturn nullptr;\n\t\t\t} else if ((loaded || canBePlayed) && (!radial || cornerDownload)) {\n\t\t\t\treturn &sti->historyFileThumbPlay;\n\t\t\t} else if (radial || _data->loading()) {\n\t\t\t\tif (!item->isSending() || _data->uploading()) {\n\t\t\t\t\treturn &sti->historyFileThumbCancel;\n\t\t\t\t}\n\t\t\t\treturn nullptr;\n\t\t\t}\n\t\t\treturn &sti->historyFileThumbDownload;\n\t\t}();\n\t\tconst auto previous = _data->waitingForAlbum()\n\t\t\t? &sti->historyFileThumbCancel\n\t\t\t: nullptr;\n\t\tif (icon) {\n\t\t\tif (previous && radialOpacity > 0. && radialOpacity < 1.) {\n\t\t\t\tPaintInterpolatedIcon(p, *icon, *previous, radialOpacity, inner);\n\t\t\t} else {\n\t\t\t\ticon->paintInCenter(p, inner);\n\t\t\t}\n\t\t}\n\t\tp.setOpacity(radialRevealed);\n\t\tif (radial) {\n\t\t\tconst auto line = st::historyGroupRadialLine;\n\t\t\tconst auto rinner = inner.marginsRemoved({ line, line, line, line });\n\t\t\tif (streamedForWaiting && !_data->uploading()) {\n\t\t\t\tUi::InfiniteRadialAnimation::Draw(\n\t\t\t\t\tp,\n\t\t\t\t\tstreamedForWaiting->waitingState(),\n\t\t\t\t\trinner.topLeft(),\n\t\t\t\t\trinner.size(),\n\t\t\t\t\twidth(),\n\t\t\t\t\tsti->historyFileThumbRadialFg,\n\t\t\t\t\tst::msgFileRadialLine);\n\t\t\t} else if (!cornerDownload) {\n\t\t\t\t_animation->radial.draw(\n\t\t\t\t\tp,\n\t\t\t\t\trinner,\n\t\t\t\t\tst::msgFileRadialLine,\n\t\t\t\t\tsti->historyFileThumbRadialFg);\n\t\t\t}\n\t\t}\n\t\tp.setOpacity(1.);\n\t}\n\tif (!_smallGroupPart) {\n\t\tdrawCornerStatus(p, context, geometry.topLeft());\n\t}\n}\n\nTextState Gif::getStateGrouped(\n\t\tconst QRect &geometry,\n\t\tRectParts sides,\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tif (!geometry.contains(point)) {\n\t\treturn {};\n\t}\n\tif (!_smallGroupPart) {\n\t\tconst auto state = cornerStatusTextState(\n\t\t\tpoint,\n\t\t\trequest,\n\t\t\tgeometry.topLeft());\n\t\tif (state.link) {\n\t\t\treturn state;\n\t\t}\n\t}\n\tensureDataMediaCreated();\n\n\tauto link = (_spoiler && !_spoiler->revealed)\n\t\t? (_sensitiveSpoiler ? spoilerTagLink() : _spoiler->link)\n\t\t: currentVideoLink();\n\treturn TextState(_parent, std::move(link));\n}\n\nClickHandlerPtr Gif::currentVideoLink() const {\n\treturn _data->uploading()\n\t\t? _cancell\n\t\t: _realParent->isSending()\n\t\t? nullptr\n\t\t: dataLoaded()\n\t\t? _openl\n\t\t: (_data->loading() && _smallGroupPart)\n\t\t? _cancell\n\t\t: _dataMedia->canBePlayed()\n\t\t? _openl\n\t\t: _data->loading()\n\t\t? _cancell\n\t\t: _savel;\n}\n\nvoid Gif::ensureDataMediaCreated() const {\n\tif (_dataMedia && (!_videoCover || _videoCoverMedia)) {\n\t\treturn;\n\t}\n\t_dataMedia = _data->createMediaView();\n\t_videoCoverMedia = _videoCover\n\t\t? _videoCover->createMediaView()\n\t\t: nullptr;\n\tdataMediaCreated();\n}\n\nvoid Gif::dataMediaCreated() const {\n\tExpects(_dataMedia != nullptr);\n\n\tif (_videoCoverMedia) {\n\t\t_videoCoverMedia->wanted(\n\t\t\tData::PhotoSize::Large,\n\t\t\t_realParent->fullId());\n\t} else {\n\t\t_dataMedia->goodThumbnailWanted();\n\t\t_dataMedia->thumbnailWanted(_realParent->fullId());\n\t\tif (!autoplayEnabled()) {\n\t\t\t_dataMedia->videoThumbnailWanted(_realParent->fullId());\n\t\t}\n\t}\n\thistory()->owner().registerHeavyViewPart(_parent);\n\ttogglePollingStory(true);\n}\n\nvoid Gif::togglePollingStory(bool enabled) const {\n\tif (!_storyId || _pollingStory == enabled) {\n\t\treturn;\n\t}\n\tconst auto polling = Data::Stories::Polling::Chat;\n\tif (!enabled) {\n\t\t_data->owner().stories().unregisterPolling(_storyId, polling);\n\t} else if (\n\t\t\t!_data->owner().stories().registerPolling(_storyId, polling)) {\n\t\treturn;\n\t}\n\t_pollingStory = enabled;\n}\n\nbool Gif::uploading() const {\n\treturn _data->uploading();\n}\n\nvoid Gif::hideSpoilers() {\n\tif (_spoiler) {\n\t\t_spoiler->revealed = false;\n\t}\n}\n\nbool Gif::needsBubble() const {\n\tif (_storyId) {\n\t\treturn true;\n\t} else if (_data->isVideoMessage()) {\n\t\treturn false;\n\t}\n\tconst auto item = _parent->data();\n\treturn item->repliesAreComments()\n\t\t|| item->externalReply()\n\t\t|| item->viaBot()\n\t\t|| !item->emptyText()\n\t\t|| _parent->displayReply()\n\t\t|| _parent->displayForwardedFrom()\n\t\t|| _parent->displayFromName()\n\t\t|| _parent->displayedTopicButton();\n\treturn false;\n}\n\nbool Gif::unwrapped() const {\n\treturn isUnwrapped();\n}\n\nQRect Gif::contentRectForReactions() const {\n\tif (!isUnwrapped()) {\n\t\treturn QRect(0, 0, width(), height());\n\t}\n\tauto paintx = 0, painty = 0, paintw = width(), painth = height();\n\tauto usex = 0, usew = paintw;\n\tconst auto rightAligned = _parent->hasRightLayout();\n\tconst auto item = _parent->data();\n\tconst auto via = item->Get<HistoryMessageVia>();\n\tconst auto reply = _parent->Get<Reply>();\n\tconst auto forwarded = item->Get<HistoryMessageForwarded>();\n\tif (via || reply || forwarded) {\n\t\tusew = maxWidth() - additionalWidth(reply, via, forwarded);\n\t}\n\taccumulate_max(usew, _parent->reactionsOptimalWidth());\n\tif (rightAligned) {\n\t\tusex = width() - usew;\n\t}\n\tif (rtl()) usex = width() - usex - usew;\n\treturn style::rtlrect(usex + paintx, painty, usew, painth, width());\n}\n\nstd::optional<int> Gif::reactionButtonCenterOverride() const {\n\tif (!isUnwrapped()) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto right = resolveCustomInfoRightBottom().x()\n\t\t- _parent->infoWidth()\n\t\t- 3 * st::msgDateImgPadding.x();\n\treturn right - st::reactionCornerSize.width() / 2;\n}\n\nQPoint Gif::resolveCustomInfoRightBottom() const {\n\tconst auto inner = contentRectForReactions();\n\tauto fullBottom = inner.y() + inner.height();\n\tauto fullRight = inner.x() + inner.width();\n\tconst auto unwrapped = isUnwrapped();\n\tif (unwrapped) {\n\t\tauto maxRight = _parent->width() - st::msgMargin.left();\n\t\tif (_parent->hasFromPhoto()) {\n\t\t\tmaxRight -= st::msgMargin.right();\n\t\t} else {\n\t\t\tmaxRight -= st::msgMargin.left();\n\t\t}\n\t\tconst auto infoWidth = _parent->infoWidth();\n\t\tconst auto rightAligned = _parent->hasRightLayout();\n\t\tif (!rightAligned) {\n\t\t\t// This is just some arbitrary point,\n\t\t\t// the main idea is to make info left aligned here.\n\t\t\tfullRight += infoWidth - st::normalFont->height;\n\t\t\tif (fullRight > maxRight) {\n\t\t\t\tfullRight = maxRight;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto skipx = unwrapped\n\t\t? st::msgDateImgPadding.x()\n\t\t: (st::msgDateImgDelta + st::msgDateImgPadding.x());\n\tconst auto skipy = unwrapped\n\t\t? st::msgDateImgPadding.y()\n\t\t: (st::msgDateImgDelta + st::msgDateImgPadding.y());\n\treturn QPoint(fullRight - skipx, fullBottom - skipy);\n}\n\nint Gif::additionalWidth() const {\n\tconst auto item = _parent->data();\n\treturn additionalWidth(\n\t\t_parent->Get<Reply>(),\n\t\titem->Get<HistoryMessageVia>(),\n\t\titem->Get<HistoryMessageForwarded>());\n}\n\nbool Gif::isUnwrapped() const {\n\treturn _data->isVideoMessage() && (_parent->media() == this);\n}\n\nvoid Gif::validateGroupedCache(\n\t\tconst QRect &geometry,\n\t\tUi::BubbleRounding rounding,\n\t\tnot_null<uint64*> cacheKey,\n\t\tnot_null<QPixmap*> cache) const {\n\tusing Option = Images::Option;\n\n\tensureDataMediaCreated();\n\n\tconst auto good = _videoCoverMedia\n\t\t? _videoCoverMedia->image(Data::PhotoSize::Large)\n\t\t: _dataMedia->goodThumbnail();\n\tconst auto thumb = _videoCoverMedia\n\t\t? nullptr\n\t\t: _dataMedia->thumbnail();\n\tconst auto image = good\n\t\t? good\n\t\t: thumb\n\t\t? thumb\n\t\t: _videoCoverMedia\n\t\t? _videoCoverMedia->thumbnailInline()\n\t\t: _dataMedia->thumbnailInline();\n\tconst auto blur = !good\n\t\t&& (!thumb\n\t\t\t|| (thumb->width() < kUseNonBlurredThreshold\n\t\t\t\t&& thumb->height() < kUseNonBlurredThreshold));\n\n\tconst auto loadLevel = good ? 3 : thumb ? 2 : image ? 1 : 0;\n\tconst auto width = geometry.width();\n\tconst auto height = geometry.height();\n\tconst auto options = (blur ? Option::Blur : Option(0));\n\tconst auto key = (uint64(width) << 48)\n\t\t| (uint64(height) << 32)\n\t\t| (uint64(options) << 16)\n\t\t| (uint64(rounding.key()) << 8)\n\t\t| (uint64(loadLevel));\n\tif (*cacheKey == key) {\n\t\treturn;\n\t}\n\n\tconst auto original = sizeForAspectRatio();\n\tconst auto originalWidth = style::ConvertScale(original.width());\n\tconst auto originalHeight = style::ConvertScale(original.height());\n\tconst auto pixSize = Ui::GetImageScaleSizeForGeometry(\n\t\t{ originalWidth, originalHeight },\n\t\t{ width, height });\n\tconst auto ratio = style::DevicePixelRatio();\n\n\t*cacheKey = key;\n\tauto scaled = Images::Prepare(\n\t\t(image ? image : Image::BlankMedia().get())->original(),\n\t\tpixSize * ratio,\n\t\t{ .options = options, .outer = { width, height } });\n\tauto rounded = Images::Round(\n\t\tstd::move(scaled),\n\t\tMediaRoundingMask(rounding));\n\t*cache = Ui::PixmapFromImage(std::move(rounded));\n}\n\nvoid Gif::setStatusSize(int64 newSize) const {\n\tif (newSize < 0) {\n\t\t_statusSize = newSize;\n\t\t_statusText = Ui::FormatDurationText(-newSize - 1);\n\t} else if (_data->isVideoMessage()) {\n\t\t_statusSize = newSize;\n\t\t_statusText = Ui::FormatDurationText(_data->duration() / 1000);\n\t} else {\n\t\tFile::setStatusSize(\n\t\t\tnewSize,\n\t\t\t_data->size,\n\t\t\t_data->isVideoFile() ? (_data->duration() / 1000) : -2,\n\t\t\t0);\n\t}\n}\n\nvoid Gif::updateStatusText() const {\n\tensureDataMediaCreated();\n\tauto statusSize = int64();\n\tif (_data->status == FileDownloadFailed || _data->status == FileUploadFailed) {\n\t\tstatusSize = Ui::FileStatusSizeFailed;\n\t} else if (_data->uploading()) {\n\t\tstatusSize = _data->uploadingData->offset;\n\t} else if (!downloadInCorner() && _data->loading()) {\n\t\tstatusSize = _data->loadOffset();\n\t} else if (dataLoaded() || _dataMedia->canBePlayed()) {\n\t\tstatusSize = Ui::FileStatusSizeLoaded;\n\t} else {\n\t\tstatusSize = Ui::FileStatusSizeReady;\n\t}\n\tconst auto round = activeRoundStreamed();\n\tconst auto own = activeOwnStreamed();\n\tif (round || (own && _data->isVideoFile())) {\n\t\tconst auto frozen = own && !own->frozenFrame.isNull();\n\t\tconst auto streamed = round ? round : &own->instance;\n\t\tconst auto state = streamed->player().prepareLegacyState();\n\t\tif (state.length) {\n\t\t\tauto position = int64(0);\n\t\t\tif (::Media::Player::IsStoppedAtEnd(state.state)) {\n\t\t\t\tposition = state.length;\n\t\t\t} else if (!::Media::Player::IsStoppedOrStopping(state.state)) {\n\t\t\t\tposition = state.position;\n\t\t\t}\n\t\t\tif (!frozen) {\n\t\t\t\tstatusSize = -1 - int((state.length - position) / state.frequency + 1);\n\t\t\t}\n\t\t\t_videoPosition = std::max(\n\t\t\t\tcrl::time(position * crl::time(1000) / state.frequency),\n\t\t\t\tcrl::time(1));\n\t\t} else {\n\t\t\tif (!frozen) {\n\t\t\t\tstatusSize = -1 - (_data->duration() / 1000);\n\t\t\t}\n\t\t\t_videoPosition = 0;\n\t\t}\n\t}\n\tif (statusSize != _statusSize) {\n\t\tsetStatusSize(statusSize);\n\t}\n}\n\nQString Gif::additionalInfoString() const {\n\tif (_data->isVideoMessage()) {\n\t\tupdateStatusText();\n\t\treturn _statusText;\n\t}\n\treturn QString();\n}\n\nbool Gif::isReadyForOpen() const {\n\treturn true;\n}\n\nbool Gif::hasHeavyPart() const {\n\treturn (_spoiler && _spoiler->animation) || _streamed || _dataMedia;\n}\n\nvoid Gif::unloadHeavyPart() {\n\tstopAnimation();\n\t_dataMedia = nullptr;\n\tif (_spoiler) {\n\t\t_spoiler->background = _spoiler->cornerCache = QImage();\n\t\t_spoiler->animation = nullptr;\n\t}\n\t_thumbCache = QImage();\n\t_videoThumbnailFrame = nullptr;\n\ttogglePollingStory(false);\n}\n\nbool Gif::enforceBubbleWidth() const {\n\treturn true;\n}\n\nint Gif::additionalWidth(\n\t\tconst Reply *reply,\n\t\tconst HistoryMessageVia *via,\n\t\tconst HistoryMessageForwarded *forwarded) const {\n\tint result = 0;\n\tif (forwarded) {\n\t\taccumulate_max(result, st::msgReplyPadding.left() + st::msgReplyPadding.left() + forwarded->text.maxWidth() + st::msgReplyPadding.right());\n\t} else if (via) {\n\t\taccumulate_max(result, st::msgReplyPadding.left() + st::msgReplyPadding.left() + via->maxWidth + st::msgReplyPadding.left());\n\t}\n\tif (reply) {\n\t\taccumulate_max(result, st::msgReplyPadding.left() + reply->maxWidth());\n\t}\n\treturn result;\n}\n\n::Media::Streaming::Instance *Gif::activeRoundStreamed() const {\n\treturn ::Media::Player::instance()->roundVideoStreamed(_parent->data());\n}\n\nGif::Streamed *Gif::activeOwnStreamed() const {\n\treturn (_streamed\n\t\t&& _streamed->instance.player().ready()\n\t\t&& !_streamed->instance.player().videoSize().isEmpty())\n\t\t? _streamed.get()\n\t\t: nullptr;\n}\n\n::Media::Streaming::Instance *Gif::activeCurrentStreamed() const {\n\tif (const auto streamed = activeRoundStreamed()) {\n\t\treturn streamed;\n\t} else if (const auto owned = activeOwnStreamed()) {\n\t\treturn &owned->instance;\n\t}\n\treturn nullptr;\n}\n\n::Media::View::PlaybackProgress *Gif::videoPlayback() const {\n\treturn ::Media::Player::instance()->roundVideoPlayback(_parent->data());\n}\n\nvoid Gif::playAnimation(bool autoplay) {\n\tensureDataMediaCreated();\n\tif (_data->isVideoMessage() && !autoplay) {\n\t\treturn;\n\t} else if (_streamed && autoplay) {\n\t\treturn;\n\t} else if ((_streamed && autoplayEnabled())\n\t\t|| (!autoplay && _data->isVideoFile())) {\n\t\t_parent->delegate()->elementOpenDocument(\n\t\t\t_data,\n\t\t\t_parent->data()->fullId(),\n\t\t\ttrue);\n\t\treturn;\n\t}\n\tif (_streamed) {\n\t\tstopAnimation();\n\t} else if (_dataMedia->canBePlayed()) {\n\t\tif (!autoplayEnabled()) {\n\t\t\thistory()->owner().checkPlayingAnimations();\n\t\t}\n\t\tcreateStreamedPlayer();\n\t}\n}\n\nvoid Gif::createStreamedPlayer() {\n\tconst auto quality = Core::App().settings().videoQuality();\n\tconst auto chosen = _data->chooseQuality(_realParent, quality);\n\tif (_streamed && _streamed->chosen == chosen) {\n\t\treturn;\n\t}\n\tauto shared = _data->owner().streaming().sharedDocument(\n\t\tchosen,\n\t\t_data,\n\t\t_realParent,\n\t\t_realParent->fullId());\n\tif (!shared) {\n\t\treturn;\n\t}\n\tsetStreamed(std::make_unique<Streamed>(\n\t\tchosen,\n\t\tstd::move(shared),\n\t\t[=] { repaintStreamedContent(); }));\n\n\t_streamed->instance.player().updates(\n\t) | rpl::on_next_error([=](::Media::Streaming::Update &&update) {\n\t\thandleStreamingUpdate(std::move(update));\n\t}, [=](::Media::Streaming::Error &&error) {\n\t\thandleStreamingError(std::move(error));\n\t}, _streamed->instance.lifetime());\n\n\t_streamed->instance.switchQualityRequests(\n\t) | rpl::on_next([=](int quality) {\n\t\tauto now = Core::App().settings().videoQuality();\n\t\tif (now.manual || now.height == quality) {\n\t\t\treturn;\n\t\t}\n\t\tCore::App().settings().setVideoQuality({\n\t\t\t.manual = 0,\n\t\t\t.height = uint32(quality),\n\t\t});\n\t\tCore::App().saveSettingsDelayed();\n\t\tcreateStreamedPlayer();\n\t}, _streamed->instance.lifetime());\n\n\tif (_streamed->instance.ready()) {\n\t\tstreamingReady(base::duplicate(_streamed->instance.info()));\n\t}\n\tcheckStreamedIsStarted();\n}\n\nvoid Gif::startStreamedPlayer() const {\n\tExpects(_streamed != nullptr);\n\n\tauto options = ::Media::Streaming::PlaybackOptions();\n\toptions.audioId = AudioMsgId(_data, _realParent->fullId());\n\toptions.waitForMarkAsShown = true;\n\t//if (!_streamed->withSound) {\n\toptions.mode = ::Media::Streaming::Mode::Video;\n\toptions.loop = true;\n\toptions.position = _videoTimestamp\n\t\t? (_videoTimestamp * crl::time(1000))\n\t\t: _parent->history()->session().local().mediaLastPlaybackPosition(\n\t\t\t_data->id);\n\t//}\n\t_streamed->instance.play(options);\n}\n\nvoid Gif::checkStreamedIsStarted() const {\n\tif (!_streamed || _streamed->instance.playerLocked()) {\n\t\treturn;\n\t}\n\tif (_streamed->instance.active()) {\n\t\tif (_streamed->instance.paused()) {\n\t\t\t_streamed->instance.resume();\n\t\t}\n\t} else if (!_streamed->instance.failed()) {\n\t\tstartStreamedPlayer();\n\t}\n}\n\nvoid Gif::setStreamed(std::unique_ptr<Streamed> value) {\n\tconst auto removed = (_streamed && !value);\n\tconst auto set = (!_streamed && value);\n\t_streamed = std::move(value);\n\tif (set) {\n\t\thistory()->owner().registerHeavyViewPart(_parent);\n\t\ttogglePollingStory(true);\n\t} else if (removed) {\n\t\t_videoPosition = 0;\n\t\t_parent->checkHeavyPart();\n\t}\n}\n\nvoid Gif::handleStreamingUpdate(::Media::Streaming::Update &&update) {\n\tusing namespace ::Media::Streaming;\n\n\tv::match(update.data, [&](Information &update) {\n\t\tstreamingReady(std::move(update));\n\t}, [](PreloadedVideo) {\n\t}, [&](UpdateVideo) {\n\t\trepaintStreamedContent();\n\t}, [](PreloadedAudio) {\n\t}, [](UpdateAudio) {\n\t}, [](WaitingForData) {\n\t}, [](SpeedEstimate) {\n\t}, [](MutedByOther) {\n\t}, [](Finished) {\n\t});\n}\n\nvoid Gif::handleStreamingError(::Media::Streaming::Error &&error) {\n}\n\nvoid Gif::repaintStreamedContent() {\n\tconst auto own = activeOwnStreamed();\n\tif (own && !own->frozenFrame.isNull()) {\n\t\treturn;\n\t} else if (_parent->delegate()->elementAnimationsPaused()\n\t\t&& !activeRoundStreamed()) {\n\t\treturn;\n\t}\n\trepaint();\n}\n\nvoid Gif::streamingReady(::Media::Streaming::Information &&info) {\n\tif (info.video.size.width() * info.video.size.height()\n\t\t> kMaxInlineArea) {\n\t\t_data->dimensions = info.video.size;\n\t\tstopAnimation();\n\t} else {\n\t\thistory()->owner().requestViewResize(_parent);\n\t}\n}\n\nvoid Gif::stopAnimation() {\n\tif (_streamed) {\n\t\tsetStreamed(nullptr);\n\t\thistory()->owner().requestViewResize(_parent);\n\t}\n}\n\nvoid Gif::checkAnimation() {\n\tif (_streamed && !autoplayEnabled()) {\n\t\tstopAnimation();\n\t}\n}\n\nfloat64 Gif::dataProgress() const {\n\tensureDataMediaCreated();\n\treturn (_data->uploading()\n\t\t|| (!_parent->data()->isSending() && !_parent->data()->hasFailed()))\n\t\t? _dataMedia->progress()\n\t\t: 0;\n}\n\nbool Gif::dataFinished() const {\n\treturn (!_parent->data()->isSending() && !_parent->data()->hasFailed())\n\t\t? (!_data->loading() && !_data->uploading())\n\t\t: false;\n}\n\nbool Gif::dataLoaded() const {\n\tensureDataMediaCreated();\n\treturn !_parent->data()->isSending()\n\t\t&& !_parent->data()->hasFailed()\n\t\t&& _dataMedia->loaded();\n}\n\nbool Gif::needInfoDisplay() const {\n\tconst auto item = _parent->data();\n\tif (item->isFakeAboutView()) {\n\t\treturn false;\n\t}\n\treturn item->isSending()\n\t\t|| item->awaitingVideoProcessing()\n\t\t|| _data->uploading()\n\t\t|| _parent->isUnderCursor()\n\t\t|| (_parent->delegate()->elementContext() == Context::ChatPreview)\n\t\t// Don't show the GIF badge if this message has text.\n\t\t|| (!_parent->hasBubble() && _parent->isLastAndSelfMessage());\n}\n\nbool Gif::needCornerStatusDisplay() const {\n\treturn _data->isVideoFile()\n\t\t|| needInfoDisplay();\n}\n\nvoid Gif::ensureTranscribeButton() const {\n\tif (_data->isVideoMessage()\n\t\t&& !_parent->data()->media()->ttlSeconds()\n\t\t&& !_parent->data()->isScheduled()\n\t\t&& !_parent->data()->isAdminLogEntry()\n\t\t&& (_data->session().premium()\n\t\t\t|| _data->session().api().transcribes().trialsSupport())) {\n\t\tif (!_transcribe) {\n\t\t\t_transcribe = std::make_unique<TranscribeButton>(\n\t\t\t\t_realParent,\n\t\t\t\ttrue);\n\t\t}\n\t} else {\n\t\t_transcribe = nullptr;\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_gif.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_file.h\"\n#include \"media/streaming/media_streaming_common.h\"\n\nclass Image;\nstruct HistoryMessageVia;\nstruct HistoryMessageReply;\nstruct HistoryMessageForwarded;\nclass Painter;\nclass PhotoData;\n\nnamespace Data {\nclass DocumentMedia;\nclass PhotoMedia;\n} // namespace Data\n\nnamespace Media {\nnamespace View {\nclass PlaybackProgress;\n} // namespace View\n} // namespace Media\n\nnamespace Media {\nnamespace Streaming {\nclass Instance;\nstruct Update;\nstruct Information;\nenum class Error;\n} // namespace Streaming\n} // namespace Media\n\nnamespace HistoryView {\n\nclass Photo;\nclass Reply;\nclass TranscribeButton;\n\nusing TtlRoundPaintCallback = Fn<void(\n\tQPainter&,\n\tQRect,\n\tconst PaintContext &context)>;\n\nclass Gif final : public File {\npublic:\n\tGif(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tnot_null<DocumentData*> document,\n\t\tbool spoiler);\n\t~Gif();\n\n\tbool hideMessageText() const override;\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed) override;\n\n\tbool uploading() const override;\n\n\tDocumentData *getDocument() const override {\n\t\treturn _data;\n\t}\n\n\tbool fullFeaturedGrouped(RectParts sides) const;\n\tQSize sizeForGroupingOptimal(int maxWidth, bool last) const override;\n\tQSize sizeForGrouping(int width) const override;\n\tvoid drawGrouped(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry,\n\t\tRectParts sides,\n\t\tUi::BubbleRounding rounding,\n\t\tfloat64 highlightOpacity,\n\t\tnot_null<uint64*> cacheKey,\n\t\tnot_null<QPixmap*> cache) const override;\n\tTextState getStateGrouped(\n\t\tconst QRect &geometry,\n\t\tRectParts sides,\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\tvoid stopAnimation() override;\n\tvoid checkAnimation() override;\n\n\tvoid drawSpoilerTag(\n\t\tPainter &p,\n\t\tQRect rthumb,\n\t\tconst PaintContext &context,\n\t\tFn<QImage()> generateBackground) const override;\n\tClickHandlerPtr spoilerTagLink() const override;\n\tQImage spoilerTagBackground() const override;\n\n\tvoid hideSpoilers() override;\n\tbool needsBubble() const override;\n\tbool unwrapped() const override;\n\tbool customInfoLayout() const override {\n\t\treturn true;\n\t}\n\tQRect contentRectForReactions() const override;\n\tstd::optional<int> reactionButtonCenterOverride() const override;\n\tQPoint resolveCustomInfoRightBottom() const override;\n\tQString additionalInfoString() const override;\n\n\tbool skipBubbleTail() const override {\n\t\treturn isRoundedInBubbleBottom();\n\t}\n\tbool isReadyForOpen() const override;\n\n\tbool hasHeavyPart() const override;\n\tvoid unloadHeavyPart() override;\n\tbool enforceBubbleWidth() const override;\n\n\t[[nodiscard]] static bool CanPlayInline(not_null<DocumentData*> document);\n\nprivate:\n\tstruct Streamed;\n\n\tvoid validateVideoThumbnail() const;\n\t[[nodiscard]] QSize countThumbSize(int &inOutWidthMax) const;\n\t[[nodiscard]] int adjustHeightForLessCrop(\n\t\tQSize dimensions,\n\t\tQSize current) const;\n\n\tfloat64 dataProgress() const override;\n\tbool dataFinished() const override;\n\tbool dataLoaded() const override;\n\n\tvoid ensureDataMediaCreated() const;\n\tvoid dataMediaCreated() const;\n\n\t[[nodiscard]] bool autoplayEnabled() const;\n\t[[nodiscard]] bool autoplayUnderCursor() const;\n\t[[nodiscard]] bool underCursor() const;\n\n\tvoid playAnimation(bool autoplay) override;\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\tQSize videoSize() const;\n\t::Media::Streaming::Instance *activeRoundStreamed() const;\n\tStreamed *activeOwnStreamed() const;\n\t::Media::Streaming::Instance *activeCurrentStreamed() const;\n\t::Media::View::PlaybackProgress *videoPlayback() const;\n\n\tvoid createStreamedPlayer();\n\tvoid checkStreamedIsStarted() const;\n\tvoid startStreamedPlayer() const;\n\tvoid setStreamed(std::unique_ptr<Streamed> value);\n\tvoid handleStreamingUpdate(::Media::Streaming::Update &&update);\n\tvoid handleStreamingError(::Media::Streaming::Error &&error);\n\tvoid streamingReady(::Media::Streaming::Information &&info);\n\tvoid repaintStreamedContent();\n\tvoid ensureTranscribeButton() const;\n\n\tvoid paintTranscribe(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tbool right,\n\t\tconst PaintContext &context) const;\n\tvoid paintTimestampMark(\n\t\tPainter &p,\n\t\tQRect rthumb,\n\t\tstd::optional<Ui::BubbleRounding> rounding) const;\n\n\t[[nodiscard]] bool needInfoDisplay() const;\n\t[[nodiscard]] bool needCornerStatusDisplay() const;\n\t[[nodiscard]] int additionalWidth(\n\t\tconst Reply *reply,\n\t\tconst HistoryMessageVia *via,\n\t\tconst HistoryMessageForwarded *forwarded) const;\n\t[[nodiscard]] int additionalWidth() const;\n\t[[nodiscard]] bool isUnwrapped() const;\n\n\tvoid validateThumbCache(\n\t\tQSize outer,\n\t\tbool isEllipse,\n\t\tstd::optional<Ui::BubbleRounding> rounding) const;\n\t[[nodiscard]] QImage prepareThumbCache(QSize outer) const;\n\tvoid validateSpoilerImageCache(\n\t\tQSize outer,\n\t\tstd::optional<Ui::BubbleRounding> rounding) const;\n\n\tvoid validateGroupedCache(\n\t\tconst QRect &geometry,\n\t\tUi::BubbleRounding rounding,\n\t\tnot_null<uint64*> cacheKey,\n\t\tnot_null<QPixmap*> cache) const;\n\tvoid setStatusSize(int64 newSize) const;\n\tvoid updateStatusText() const;\n\t[[nodiscard]] QSize sizeForAspectRatio() const;\n\n\tvoid validateRoundingMask(QSize size) const;\n\n\t[[nodiscard]] bool downloadInCorner() const;\n\tvoid drawCornerStatus(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tQPoint position) const;\n\t[[nodiscard]] TextState cornerStatusTextState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tQPoint position) const;\n\t[[nodiscard]] ClickHandlerPtr currentVideoLink() const;\n\n\tvoid togglePollingStory(bool enabled) const;\n\n\tTtlRoundPaintCallback _drawTtl;\n\n\tconst not_null<DocumentData*> _data;\n\tPhotoData *_videoCover = nullptr;\n\tconst FullStoryId _storyId;\n\tstd::unique_ptr<Streamed> _streamed;\n\tconst std::unique_ptr<MediaSpoiler> _spoiler;\n\tmutable std::unique_ptr<MediaSpoilerTag> _spoilerTag;\n\tmutable std::unique_ptr<TranscribeButton> _transcribe;\n\tmutable std::shared_ptr<Data::DocumentMedia> _dataMedia;\n\tmutable std::shared_ptr<Data::PhotoMedia> _videoCoverMedia;\n\tmutable std::unique_ptr<Image> _videoThumbnailFrame;\n\tQString _downloadSize;\n\tmutable QImage _thumbCache;\n\tmutable QImage _roundingMask;\n\tmutable crl::time _videoPosition = 0;\n\tmutable TimeId _videoTimestamp = 0;\n\tmutable std::optional<Ui::BubbleRounding> _thumbCacheRounding;\n\tmutable bool _thumbCacheBlurred : 1 = false;\n\tmutable bool _thumbIsEllipse : 1 = false;\n\tmutable bool _pollingStory : 1 = false;\n\tmutable bool _purchasedPriceTag : 1 = false;\n\tmutable bool _smallGroupPart : 1 = false;\n\tconst bool _sensitiveSpoiler : 1 = false;\n\tconst bool _hasVideoCover : 1 = false;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_giveaway.h\"\n\n#include \"base/unixtime.h\"\n#include \"boxes/gift_premium_box.h\"\n#include \"chat_helpers/stickers_gift_box_pack.h\"\n#include \"chat_helpers/stickers_dice_pack.h\"\n#include \"countries/countries_instance.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_media_types.h\"\n#include \"history/view/media/history_view_media_generic.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\n\nconstexpr auto kOutlineRatio = 0.85;\n\nauto GenerateGiveawayStart(\n\tnot_null<Element*> parent,\n\tnot_null<Data::GiveawayStart*> data)\n-> Fn<void(\n\t\tnot_null<MediaGeneric*>,\n\t\tFn<void(std::unique_ptr<MediaGenericPart>)>)> {\n\treturn [=](\n\t\t\tnot_null<MediaGeneric*> media,\n\t\t\tFn<void(std::unique_ptr<MediaGenericPart>)> push) {\n\t\tconst auto months = data->months;\n\t\tconst auto quantity = data->quantity;\n\n\t\tusing Data = StickerWithBadgePart::Data;\n\t\tconst auto sticker = [=] {\n\t\t\tconst auto &session = parent->history()->session();\n\t\t\tauto &packs = session.giftBoxStickersPacks();\n\t\t\treturn Data{\n\t\t\t\t.sticker = packs.lookup(months),\n\t\t\t\t.size = st::msgServiceGiftBoxStickerSize,\n\t\t\t\t.stopOnLastFrame = true,\n\t\t\t};\n\t\t};\n\t\tpush(std::make_unique<StickerWithBadgePart>(\n\t\t\tparent,\n\t\t\tnullptr,\n\t\t\tsticker,\n\t\t\tst::chatGiveawayStickerPadding,\n\t\t\tdata->credits\n\t\t\t\t? QString::number(data->credits)\n\t\t\t\t: tr::lng_prizes_badge(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_amount,\n\t\t\t\t\tQString::number(quantity)),\n\t\t\tdata->credits\n\t\t\t\t? Ui::CreditsWhiteDoubledIcon(\n\t\t\t\t\tst::chatGiveawayCreditsIconHeight,\n\t\t\t\t\tkOutlineRatio)\n\t\t\t\t: QImage(),\n\t\t\tdata->credits\n\t\t\t\t? std::make_optional(st::creditsBg3->c)\n\t\t\t\t: std::nullopt));\n\n\t\tauto pushText = [&](\n\t\t\t\tTextWithEntities text,\n\t\t\t\tQMargins margins = {},\n\t\t\t\tconst base::flat_map<uint16, ClickHandlerPtr> &links = {}) {\n\t\t\tpush(std::make_unique<MediaGenericTextPart>(\n\t\t\t\tstd::move(text),\n\t\t\t\tmargins,\n\t\t\t\tst::defaultTextStyle,\n\t\t\t\tlinks));\n\t\t};\n\t\tpushText(\n\t\t\ttr::bold(\n\t\t\t\ttr::lng_prizes_title(tr::now, lt_count, quantity)),\n\t\t\tst::chatGiveawayPrizesTitleMargin);\n\n\t\tif (!data->additionalPrize.isEmpty()) {\n\t\t\tpushText(\n\t\t\t\ttr::lng_prizes_additional(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tquantity,\n\t\t\t\t\tlt_prize,\n\t\t\t\t\tTextWithEntities{ data->additionalPrize },\n\t\t\t\t\ttr::rich),\n\t\t\t\tst::chatGiveawayPrizesMargin);\n\t\t\tpush(std::make_unique<TextDelimeterPart>(\n\t\t\t\ttr::lng_prizes_additional_with(tr::now),\n\t\t\t\tst::chatGiveawayPrizesWithPadding));\n\t\t}\n\n\t\tpushText((data->credits && (quantity == 1))\n\t\t\t? tr::lng_prizes_credits_about_single(\n\t\t\t\ttr::now,\n\t\t\t\tlt_amount,\n\t\t\t\ttr::lng_prizes_credits_about_amount(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tdata->credits,\n\t\t\t\t\ttr::rich),\n\t\t\t\ttr::rich)\n\t\t\t: (data->credits && (quantity > 1))\n\t\t\t? tr::lng_prizes_credits_about(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tquantity,\n\t\t\t\tlt_amount,\n\t\t\t\ttr::lng_prizes_credits_about_amount(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tdata->credits,\n\t\t\t\t\ttr::rich),\n\t\t\t\ttr::rich)\n\t\t\t: tr::lng_prizes_about(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tquantity,\n\t\t\t\tlt_duration,\n\t\t\t\ttr::bold(GiftDuration(months * 30)),\n\t\t\t\ttr::rich),\n\t\t\tst::chatGiveawayPrizesMargin);\n\t\tpushText(\n\t\t\ttr::bold(tr::lng_prizes_participants(tr::now)),\n\t\t\tst::chatGiveawayPrizesTitleMargin);\n\n\t\tconst auto hasChannel = ranges::any_of(\n\t\t\tdata->channels,\n\t\t\t&ChannelData::isBroadcast);\n\t\tconst auto hasGroup = ranges::any_of(\n\t\t\tdata->channels,\n\t\t\t&ChannelData::isMegagroup);\n\t\tconst auto mixed = (hasChannel && hasGroup);\n\t\tpushText({ (data->all\n\t\t\t? (mixed\n\t\t\t\t? tr::lng_prizes_participants_all_mixed\n\t\t\t\t: hasGroup\n\t\t\t\t? tr::lng_prizes_participants_all_group\n\t\t\t\t: tr::lng_prizes_participants_all)\n\t\t\t: (mixed\n\t\t\t\t? tr::lng_prizes_participants_new_mixed\n\t\t\t\t: hasGroup\n\t\t\t\t? tr::lng_prizes_participants_new_group\n\t\t\t\t: tr::lng_prizes_participants_new))(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tdata->channels.size()),\n\t\t}, st::chatGiveawayParticipantsMargin);\n\n\t\tauto list = ranges::views::all(\n\t\t\tdata->channels\n\t\t) | ranges::views::transform([](not_null<ChannelData*> channel) {\n\t\t\treturn not_null<PeerData*>(channel);\n\t\t}) | ranges::to_vector;\n\t\tpush(std::make_unique<PeerBubbleListPart>(\n\t\t\tparent,\n\t\t\tstd::move(list)));\n\n\t\tconst auto &instance = Countries::Instance();\n\t\tauto countries = QStringList();\n\t\tfor (const auto &country : data->countries) {\n\t\t\tconst auto name = instance.countryNameByISO2(country);\n\t\t\tconst auto flag = instance.flagEmojiByISO2(country);\n\t\t\tcountries.push_back(flag + QChar(0xA0) + name);\n\t\t}\n\t\tif (const auto count = countries.size()) {\n\t\t\tauto united = countries.front();\n\t\t\tfor (auto i = 1; i != count; ++i) {\n\t\t\t\tunited = ((i + 1 == count)\n\t\t\t\t\t? tr::lng_prizes_countries_and_last\n\t\t\t\t\t: tr::lng_prizes_countries_and_one)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_countries,\n\t\t\t\t\t\tunited,\n\t\t\t\t\t\tlt_country,\n\t\t\t\t\t\tcountries[i]);\n\t\t\t}\n\t\t\tpushText({\n\t\t\t\ttr::lng_prizes_countries(tr::now, lt_countries, united),\n\t\t\t}, st::chatGiveawayPrizesMargin);\n\t\t}\n\n\t\tpushText(\n\t\t\ttr::bold(tr::lng_prizes_date(tr::now)),\n\t\t\t(countries.empty()\n\t\t\t\t? st::chatGiveawayNoCountriesTitleMargin\n\t\t\t\t: st::chatGiveawayPrizesMargin));\n\t\tpushText({\n\t\t\tlangDateTime(base::unixtime::parse(data->untilDate)),\n\t\t}, st::chatGiveawayEndDateMargin);\n\t};\n}\n\nauto GenerateGiveawayResults(\n\tnot_null<Element*> parent,\n\tnot_null<Data::GiveawayResults*> data)\n-> Fn<void(\n\t\tnot_null<MediaGeneric*>,\n\t\tFn<void(std::unique_ptr<MediaGenericPart>)>)> {\n\treturn [=](\n\t\t\tnot_null<MediaGeneric*> media,\n\t\t\tFn<void(std::unique_ptr<MediaGenericPart>)> push) {\n\t\tconst auto quantity = data->winnersCount;\n\n\t\tusing Data = StickerWithBadgePart::Data;\n\t\tconst auto sticker = [=] {\n\t\t\tconst auto &session = parent->history()->session();\n\t\t\tauto &packs = session.diceStickersPacks();\n\t\t\tconst auto &emoji = Stickers::DicePacks::kPartyPopper;\n\t\t\treturn Data{\n\t\t\t\t.sticker = packs.lookup(emoji, 0),\n\t\t\t\t.skipTop = st::chatGiveawayWinnersTopSkip,\n\t\t\t\t.size = st::maxAnimatedEmojiSize,\n\t\t\t\t.stopOnLastFrame = true,\n\t\t\t};\n\t\t};\n\t\tpush(std::make_unique<StickerWithBadgePart>(\n\t\t\tparent,\n\t\t\tnullptr,\n\t\t\tsticker,\n\t\t\tst::chatGiveawayStickerPadding,\n\t\t\tdata->credits\n\t\t\t\t? QString::number(data->credits)\n\t\t\t\t: tr::lng_prizes_badge(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_amount,\n\t\t\t\t\tQString::number(quantity)),\n\t\t\tdata->credits\n\t\t\t\t? Ui::CreditsWhiteDoubledIcon(\n\t\t\t\t\tst::chatGiveawayCreditsIconHeight,\n\t\t\t\t\tkOutlineRatio)\n\t\t\t\t: QImage(),\n\t\t\tdata->credits\n\t\t\t\t? std::make_optional(st::creditsBg3->c)\n\t\t\t\t: std::nullopt));\n\n\t\tauto pushText = [&](\n\t\t\t\tTextWithEntities text,\n\t\t\t\tQMargins margins = {},\n\t\t\t\tconst base::flat_map<uint16, ClickHandlerPtr> &links = {}) {\n\t\t\tpush(std::make_unique<MediaGenericTextPart>(\n\t\t\t\tstd::move(text),\n\t\t\t\tmargins,\n\t\t\t\tst::defaultTextStyle,\n\t\t\t\tlinks));\n\t\t};\n\t\tconst auto isSingleWinner = (data->winnersCount == 1);\n\t\tpushText(\n\t\t\t(isSingleWinner\n\t\t\t\t? tr::lng_prizes_results_title_one\n\t\t\t\t: tr::lng_prizes_results_title)(tr::now, tr::bold),\n\t\t\tst::chatGiveawayPrizesTitleMargin);\n\t\tconst auto showGiveawayHandler = JumpToMessageClickHandler(\n\t\t\tdata->channel,\n\t\t\tdata->launchId,\n\t\t\tparent->data()->fullId());\n\t\tpushText(\n\t\t\ttr::lng_prizes_results_about(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tquantity,\n\t\t\t\tlt_link,\n\t\t\t\ttr::link(tr::lng_prizes_results_link(tr::now)),\n\t\t\t\ttr::rich),\n\t\t\tst::chatGiveawayPrizesMargin,\n\t\t\t{ { 1, showGiveawayHandler } });\n\t\tpushText(\n\t\t\t(isSingleWinner\n\t\t\t\t? tr::lng_prizes_results_winner\n\t\t\t\t: tr::lng_prizes_results_winners)(tr::now, tr::bold),\n\t\t\tst::chatGiveawayPrizesTitleMargin);\n\n\t\tpush(std::make_unique<PeerBubbleListPart>(\n\t\t\tparent,\n\t\t\tdata->winners));\n\t\tif (data->winnersCount > data->winners.size()) {\n\t\t\tpushText(\n\t\t\t\ttr::bold(tr::lng_prizes_results_more(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tdata->winnersCount - data->winners.size())),\n\t\t\t\tst::chatGiveawayNoCountriesTitleMargin);\n\t\t}\n\t\tpushText({ (data->credits && isSingleWinner)\n\t\t\t? tr::lng_prizes_credits_results_one(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tdata->credits)\n\t\t\t: (data->credits && !isSingleWinner)\n\t\t\t? tr::lng_prizes_credits_results_all(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tdata->credits)\n\t\t\t: data->unclaimedCount\n\t\t\t? tr::lng_prizes_results_some(tr::now)\n\t\t\t: isSingleWinner\n\t\t\t? tr::lng_prizes_results_one(tr::now)\n\t\t\t: tr::lng_prizes_results_all(tr::now)\n\t\t}, st::chatGiveawayEndDateMargin);\n\t};\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_giveaway.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nstruct GiveawayStart;\nstruct GiveawayResults;\n} // namespace Data\n\nnamespace HistoryView {\n\nclass Element;\nclass MediaGeneric;\nclass MediaGenericPart;\n\n[[nodiscard]] auto GenerateGiveawayStart(\n\tnot_null<Element*> parent,\n\tnot_null<Data::GiveawayStart*> data)\n-> Fn<void(\n\tnot_null<MediaGeneric*>,\n\tFn<void(std::unique_ptr<MediaGenericPart>)>)>;\n\n[[nodiscard]] auto GenerateGiveawayResults(\n\tnot_null<Element*> parent,\n\tnot_null<Data::GiveawayResults*> data)\n-> Fn<void(\n\tnot_null<MediaGeneric*>,\n\tFn<void(std::unique_ptr<MediaGenericPart>)>)>;\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_invoice.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_invoice.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/media/history_view_photo.h\"\n#include \"history/view/media/history_view_media_common.h\"\n#include \"ui/item_text_options.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"data/data_media_types.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\n\nInvoice::Invoice(\n\tnot_null<Element*> parent,\n\tnot_null<Data::Invoice*> invoice)\n: Media(parent)\n, _title(st::msgMinWidth)\n, _description(st::msgMinWidth)\n, _status(st::msgMinWidth) {\n\tfillFromData(invoice);\n}\n\nvoid Invoice::fillFromData(not_null<Data::Invoice*> invoice) {\n\tconst auto isCreditsCurrency = false;\n\tif (invoice->photo && !isCreditsCurrency) {\n\t\tconst auto spoiler = false;\n\t\t_attach = std::make_unique<Photo>(\n\t\t\t_parent,\n\t\t\t_parent->data(),\n\t\t\tinvoice->photo,\n\t\t\tspoiler);\n\t} else {\n\t\t_attach = nullptr;\n\t}\n\tauto labelText = [&] {\n\t\tif (invoice->receiptMsgId) {\n\t\t\tif (invoice->isTest) {\n\t\t\t\treturn tr::lng_payments_receipt_label_test(tr::now);\n\t\t\t}\n\t\t\treturn tr::lng_payments_receipt_label(tr::now);\n\t\t} else if (invoice->isTest) {\n\t\t\treturn tr::lng_payments_invoice_label_test(tr::now);\n\t\t}\n\t\treturn tr::lng_payments_invoice_label(tr::now);\n\t};\n\tauto statusText = TextWithEntities {\n\t\tUi::FillAmountAndCurrency(invoice->amount, invoice->currency),\n\t\tEntitiesInText()\n\t};\n\tstatusText.entities.push_back({\n\t\tEntityType::Bold,\n\t\t0,\n\t\tint(statusText.text.size()) });\n\tstatusText.text += ' ' + labelText().toUpper();\n\tif (isCreditsCurrency) {\n\t\tstatusText = {};\n\t}\n\t_status.setMarkedText(\n\t\tst::defaultTextStyle,\n\t\tstatusText,\n\t\tUi::ItemTextOptions(_parent->data()));\n\n\t_receiptMsgId = invoice->receiptMsgId;\n\n\t// init strings\n\tif (!invoice->description.empty()) {\n\t\t_description.setMarkedText(\n\t\t\tst::webPageDescriptionStyle,\n\t\t\tinvoice->description,\n\t\t\tUi::WebpageTextDescriptionOptions());\n\t}\n\tif (!invoice->title.isEmpty()) {\n\t\t_title.setText(\n\t\t\tst::webPageTitleStyle,\n\t\t\tinvoice->title,\n\t\t\tUi::WebpageTextTitleOptions());\n\t}\n}\n\nQSize Invoice::countOptimalSize() {\n\tauto lineHeight = UnitedLineHeight();\n\n\tif (_attach) {\n\t\tif (_status.hasSkipBlock()) {\n\t\t\t_status.removeSkipBlock();\n\t\t}\n\t} else {\n\t\t_status.updateSkipBlock(\n\t\t\t_parent->skipBlockWidth(),\n\t\t\t_parent->skipBlockHeight());\n\t}\n\n\t// init dimensions\n\tauto skipBlockWidth = _parent->skipBlockWidth();\n\tauto maxWidth = skipBlockWidth;\n\tauto minHeight = 0;\n\n\tauto titleMinHeight = _title.isEmpty() ? 0 : lineHeight;\n\t// enable any count of lines in game description / message\n\tauto descMaxLines = 4096;\n\tauto descriptionMinHeight = _description.isEmpty() ? 0 : qMin(_description.minHeight(), descMaxLines * lineHeight);\n\n\tif (!_title.isEmpty()) {\n\t\taccumulate_max(maxWidth, _title.maxWidth());\n\t\tminHeight += titleMinHeight;\n\t}\n\tif (!_description.isEmpty()) {\n\t\taccumulate_max(maxWidth, _description.maxWidth());\n\t\tminHeight += descriptionMinHeight;\n\t}\n\tif (_attach) {\n\t\tauto attachAtTop = _title.isEmpty() && _description.isEmpty();\n\t\tif (!attachAtTop) minHeight += st::mediaInBubbleSkip;\n\n\t\t_attach->initDimensions();\n\t\tauto bubble = _attach->bubbleMargins();\n\t\tauto maxMediaWidth = _attach->maxWidth() - bubble.left() - bubble.right();\n\t\tif (isBubbleBottom() && _attach->customInfoLayout()) {\n\t\t\tmaxMediaWidth += skipBlockWidth;\n\t\t}\n\t\taccumulate_max(maxWidth, maxMediaWidth);\n\t\tminHeight += _attach->minHeight() - bubble.top() - bubble.bottom();\n\t} else {\n\t\taccumulate_max(maxWidth, _status.maxWidth());\n\t\tminHeight += st::mediaInBubbleSkip + _status.minHeight();\n\t}\n\tauto padding = inBubblePadding();\n\tmaxWidth += padding.left() + padding.right();\n\tminHeight += padding.top() + padding.bottom();\n\treturn { maxWidth, minHeight };\n}\n\nQSize Invoice::countCurrentSize(int newWidth) {\n\taccumulate_min(newWidth, maxWidth());\n\tauto innerWidth = newWidth - st::msgPadding.left() - st::msgPadding.right();\n\n\tauto lineHeight = UnitedLineHeight();\n\n\tauto newHeight = 0;\n\tif (_title.isEmpty()) {\n\t\t_titleHeight = 0;\n\t} else {\n\t\tif (_title.countHeight(innerWidth) < 2 * st::webPageTitleFont->height) {\n\t\t\t_titleHeight = lineHeight;\n\t\t} else {\n\t\t\t_titleHeight = 2 * lineHeight;\n\t\t}\n\t\tnewHeight += _titleHeight;\n\t}\n\n\tif (_description.isEmpty()) {\n\t\t_descriptionHeight = 0;\n\t} else {\n\t\t_descriptionHeight = _description.countHeight(innerWidth);\n\t\tnewHeight += _descriptionHeight;\n\t}\n\n\tif (_attach) {\n\t\tauto attachAtTop = !_title.isEmpty() && _description.isEmpty();\n\t\tif (!attachAtTop) newHeight += st::mediaInBubbleSkip;\n\n\t\tQMargins bubble(_attach->bubbleMargins());\n\n\t\t_attach->resizeGetHeight(innerWidth + bubble.left() + bubble.right());\n\t\tnewHeight += _attach->height() - bubble.top() - bubble.bottom();\n\t\tif (isBubbleBottom() && _attach->customInfoLayout() && _attach->width() + _parent->skipBlockWidth() > innerWidth + bubble.left() + bubble.right()) {\n\t\t\tnewHeight += bottomInfoPadding();\n\t\t}\n\t} else {\n\t\tnewHeight += st::mediaInBubbleSkip + _status.countHeight(innerWidth);\n\t}\n\tauto padding = inBubblePadding();\n\tnewHeight += padding.top() + padding.bottom();\n\n\treturn { newWidth, newHeight };\n}\n\nTextSelection Invoice::toDescriptionSelection(\n\t\tTextSelection selection) const {\n\treturn UnshiftItemSelection(selection, _title);\n}\n\nTextSelection Invoice::fromDescriptionSelection(\n\t\tTextSelection selection) const {\n\treturn ShiftItemSelection(selection, _title);\n}\n\nvoid Invoice::refreshParentId(not_null<HistoryItem*> realParent) {\n\tif (_attach) {\n\t\t_attach->refreshParentId(realParent);\n\t}\n}\n\nvoid Invoice::draw(Painter &p, const PaintContext &context) const {\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;\n\tauto paintw = width();\n\n\tconst auto st = context.st;\n\tconst auto sti = context.imageStyle();\n\tconst auto stm = context.messageStyle();\n\n\tauto &semibold = stm->msgServiceFg;\n\n\tQMargins bubble(_attach ? _attach->bubbleMargins() : QMargins());\n\tauto padding = inBubblePadding();\n\tauto tshift = padding.top();\n\tpaintw -= padding.left() + padding.right();\n\n\tauto lineHeight = UnitedLineHeight();\n\tif (_titleHeight) {\n\t\tp.setPen(semibold);\n\t\tp.setTextPalette(stm->semiboldPalette);\n\n\t\tauto endskip = 0;\n\t\tif (_title.hasSkipBlock()) {\n\t\t\tendskip = _parent->skipBlockWidth();\n\t\t}\n\t\t_title.drawLeftElided(p, padding.left(), tshift, paintw, width(), _titleHeight / lineHeight, style::al_left, 0, -1, endskip, false, context.selection);\n\t\ttshift += _titleHeight;\n\n\t\tp.setTextPalette(stm->textPalette);\n\t}\n\tif (_descriptionHeight) {\n\t\tp.setPen(stm->historyTextFg);\n\t\t_parent->prepareCustomEmojiPaint(p, context, _description);\n\t\t_description.draw(p, {\n\t\t\t.position = { padding.left(), tshift },\n\t\t\t.outerWidth = width(),\n\t\t\t.availableWidth = paintw,\n\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t.now = context.now,\n\t\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t\t.selection = toDescriptionSelection(context.selection),\n\t\t\t.useFullWidth = true,\n\t\t});\n\t\ttshift += _descriptionHeight;\n\t}\n\tif (_attach) {\n\t\tauto attachAtTop = !_titleHeight && !_descriptionHeight;\n\t\tif (!attachAtTop) tshift += st::mediaInBubbleSkip;\n\n\t\tauto attachLeft = padding.left() - bubble.left();\n\t\tauto attachTop = tshift - bubble.top();\n\t\tif (rtl()) attachLeft = width() - attachLeft - _attach->width();\n\n\t\tp.translate(attachLeft, attachTop);\n\t\t_attach->draw(p, context.translated(\n\t\t\t-attachLeft,\n\t\t\t-attachTop\n\t\t).withSelection(context.selected()\n\t\t\t? FullSelection\n\t\t\t: TextSelection()));\n\t\tauto pixwidth = _attach->width();\n\n\t\tauto available = _status.maxWidth();\n\t\tauto statusW = available + 2 * st::msgDateImgPadding.x();\n\t\tauto statusH = st::msgDateFont->height + 2 * st::msgDateImgPadding.y();\n\t\tauto statusX = st::msgDateImgDelta;\n\t\tauto statusY = st::msgDateImgDelta;\n\n\t\tUi::FillRoundRect(p, style::rtlrect(statusX, statusY, statusW, statusH, pixwidth), sti->msgDateImgBg, sti->msgDateImgBgCorners);\n\n\t\tp.setFont(st::msgDateFont);\n\t\tp.setPen(st->msgDateImgFg());\n\t\t_status.drawLeftElided(p, statusX + st::msgDateImgPadding.x(), statusY + st::msgDateImgPadding.y(), available, pixwidth);\n\n\t\tp.translate(-attachLeft, -attachTop);\n\t} else {\n\t\tp.setPen(stm->historyTextFg);\n\t\t_status.drawLeft(p, padding.left(), tshift + st::mediaInBubbleSkip, paintw, width());\n\t}\n}\n\nTextState Invoice::textState(QPoint point, StateRequest request) const {\n\tauto result = TextState(_parent);\n\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {\n\t\treturn result;\n\t}\n\tauto paintw = width();\n\n\tQMargins bubble(_attach ? _attach->bubbleMargins() : QMargins());\n\tauto padding = inBubblePadding();\n\tauto tshift = padding.top();\n\tauto bshift = padding.bottom();\n\tif (isBubbleBottom() && _attach && _attach->customInfoLayout() && _attach->width() + _parent->skipBlockWidth() > paintw + bubble.left() + bubble.right()) {\n\t\tbshift += bottomInfoPadding();\n\t}\n\tpaintw -= padding.left() + padding.right();\n\n\tauto lineHeight = UnitedLineHeight();\n\tauto symbolAdd = 0;\n\tif (_titleHeight) {\n\t\tif (point.y() >= tshift && point.y() < tshift + _titleHeight) {\n\t\t\tUi::Text::StateRequestElided titleRequest = request.forText();\n\t\t\ttitleRequest.lines = _titleHeight / lineHeight;\n\t\t\tresult = TextState(_parent, _title.getStateElidedLeft(\n\t\t\t\tpoint - QPoint(padding.left(), tshift),\n\t\t\t\tpaintw,\n\t\t\t\twidth(),\n\t\t\t\ttitleRequest));\n\t\t} else if (point.y() >= tshift + _titleHeight) {\n\t\t\tsymbolAdd += _title.length();\n\t\t}\n\t\ttshift += _titleHeight;\n\t}\n\tif (_descriptionHeight) {\n\t\tif (point.y() >= tshift && point.y() < tshift + _descriptionHeight) {\n\t\t\tresult = TextState(_parent, _description.getStateLeft(\n\t\t\t\tpoint - QPoint(padding.left(), tshift),\n\t\t\t\tpaintw,\n\t\t\t\twidth(),\n\t\t\t\trequest.forText()));\n\t\t} else if (point.y() >= tshift + _descriptionHeight) {\n\t\t\tsymbolAdd += _description.length();\n\t\t}\n\t\ttshift += _descriptionHeight;\n\t}\n\tif (_attach) {\n\t\tauto attachAtTop = !_titleHeight && !_descriptionHeight;\n\t\tif (!attachAtTop) tshift += st::mediaInBubbleSkip;\n\n\t\tauto attachLeft = padding.left() - bubble.left();\n\t\tauto attachTop = tshift - bubble.top();\n\t\tif (rtl()) attachLeft = width() - attachLeft - _attach->width();\n\n\t\tif (QRect(attachLeft, tshift, _attach->width(), height() - tshift - bshift).contains(point)) {\n\t\t\tresult = _attach->textState(point - QPoint(attachLeft, attachTop), request);\n\t\t}\n\t}\n\n\tresult.symbol += symbolAdd;\n\treturn result;\n}\n\nTextSelection Invoice::adjustSelection(TextSelection selection, TextSelectType type) const {\n\tif (!_descriptionHeight || selection.to <= _title.length()) {\n\t\treturn _title.adjustSelection(selection, type);\n\t}\n\tauto descriptionSelection = _description.adjustSelection(toDescriptionSelection(selection), type);\n\tif (selection.from >= _title.length()) {\n\t\treturn fromDescriptionSelection(descriptionSelection);\n\t}\n\tauto titleSelection = _title.adjustSelection(selection, type);\n\treturn { titleSelection.from, fromDescriptionSelection(descriptionSelection).to };\n}\n\nvoid Invoice::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {\n\tif (_attach) {\n\t\t_attach->clickHandlerActiveChanged(p, active);\n\t}\n}\n\nvoid Invoice::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {\n\tif (_attach) {\n\t\t_attach->clickHandlerPressedChanged(p, pressed);\n\t}\n}\n\nbool Invoice::hasHeavyPart() const {\n\treturn _attach ? _attach->hasHeavyPart() : false;\n}\n\nvoid Invoice::unloadHeavyPart() {\n\tif (_attach) {\n\t\t_attach->unloadHeavyPart();\n\t}\n\t_description.unloadPersistentAnimation();\n}\n\nTextForMimeData Invoice::selectedText(TextSelection selection) const {\n\tauto titleResult = _title.toTextForMimeData(selection);\n\tauto descriptionResult = _description.toTextForMimeData(\n\t\ttoDescriptionSelection(selection));\n\tif (titleResult.empty()) {\n\t\treturn descriptionResult;\n\t} else if (descriptionResult.empty()) {\n\t\treturn titleResult;\n\t}\n\treturn titleResult.append('\\n').append(std::move(descriptionResult));\n}\n\nQMargins Invoice::inBubblePadding() const {\n\tauto lshift = st::msgPadding.left();\n\tauto rshift = st::msgPadding.right();\n\tauto bshift = isBubbleBottom() ? st::msgPadding.top() : st::mediaInBubbleSkip;\n\tauto tshift = isBubbleTop() ? st::msgPadding.bottom() : st::mediaInBubbleSkip;\n\treturn QMargins(lshift, tshift, rshift, bshift);\n}\n\nint Invoice::bottomInfoPadding() const {\n\tif (!isBubbleBottom()) return 0;\n\n\tauto result = st::msgDateFont->height;\n\treturn result;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_invoice.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n\nnamespace Data {\nstruct Invoice;\n} // namespace Data\n\nnamespace HistoryView {\n\nclass Invoice : public Media {\npublic:\n\tInvoice(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<Data::Invoice*> invoice);\n\n\tvoid refreshParentId(not_null<HistoryItem*> realParent) override;\n\n\tMsgId getReceiptMsgId() const {\n\t\treturn _receiptMsgId;\n\t}\n\tQString getTitle() const {\n\t\treturn _title.toString();\n\t}\n\n\tbool aboveTextByDefault() const override {\n\t\treturn false;\n\t}\n\tbool hideMessageText() const override {\n\t\treturn false;\n\t}\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\n\t[[nodiscard]] TextSelection adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const override;\n\tuint16 fullSelectionLength() const override {\n\t\treturn _title.length() + _description.length();\n\t}\n\tbool hasTextForCopy() const override {\n\t\treturn false; // we do not add _title and _description in FullSelection text copy.\n\t}\n\n\tbool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {\n\t\treturn _attach && _attach->toggleSelectionByHandlerClick(p);\n\t}\n\tbool dragItemByHandler(const ClickHandlerPtr &p) const override {\n\t\treturn _attach && _attach->dragItemByHandler(p);\n\t}\n\n\tTextForMimeData selectedText(TextSelection selection) const override;\n\n\tvoid clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;\n\tvoid clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;\n\n\tbool needsBubble() const override {\n\t\treturn true;\n\t}\n\tbool customInfoLayout() const override {\n\t\treturn false;\n\t}\n\n\tMedia *attach() const {\n\t\treturn _attach.get();\n\t}\n\n\tbool hasHeavyPart() const override;\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\n\tvoid fillFromData(not_null<Data::Invoice*> invoice);\n\n\tTextSelection toDescriptionSelection(TextSelection selection) const;\n\tTextSelection fromDescriptionSelection(TextSelection selection) const;\n\tQMargins inBubblePadding() const;\n\tint bottomInfoPadding() const;\n\n\tstd::unique_ptr<Media> _attach;\n\n\tint _titleHeight = 0;\n\tint _descriptionHeight = 0;\n\tUi::Text::String _title;\n\tUi::Text::String _description;\n\tUi::Text::String _status;\n\n\tMsgId _receiptMsgId = 0;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_large_emoji.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_large_emoji.h\"\n\n#include \"main/main_session.h\"\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"ui/image/image.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/painter.h\"\n#include \"data/data_session.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nusing Stickers::LargeEmojiImage;\nusing ImagePtr = std::shared_ptr<Stickers::LargeEmojiImage>;\nusing CustomPtr = std::unique_ptr<Ui::Text::CustomEmoji>;\n\nauto ResolveImages(\n\tnot_null<Main::Session*> session,\n\tFn<void()> customEmojiRepaint,\n\tconst Ui::Text::IsolatedEmoji &emoji)\n-> std::array<LargeEmojiMedia, Ui::Text::kIsolatedEmojiLimit> {\n\tconst auto single = [&](Ui::Text::IsolatedEmoji::Item item)\n\t-> LargeEmojiMedia {\n\t\tif (const auto regular = std::get_if<EmojiPtr>(&item)) {\n\t\t\treturn session->emojiStickersPack().image(*regular);\n\t\t} else if (const auto custom = std::get_if<QString>(&item)) {\n\t\t\treturn session->data().customEmojiManager().create(\n\t\t\t\t*custom,\n\t\t\t\tcustomEmojiRepaint,\n\t\t\t\tData::CustomEmojiManager::SizeTag::Isolated);\n\t\t}\n\t\treturn v::null;\n\t};\n\treturn { {\n\t\tsingle(emoji.items[0]),\n\t\tsingle(emoji.items[1]),\n\t\tsingle(emoji.items[2]) } };\n}\n\n} // namespace\n\nLargeEmoji::LargeEmoji(\n\tnot_null<Element*> parent,\n\tconst Ui::Text::IsolatedEmoji &emoji)\n: _parent(parent)\n, _images(ResolveImages(\n\t&parent->history()->session(),\n\t[=] { parent->customEmojiRepaint(); },\n\temoji)) {\n}\n\nLargeEmoji::~LargeEmoji() {\n\tif (_hasHeavyPart) {\n\t\tunloadHeavyPart();\n\t\t_parent->checkHeavyPart();\n\t}\n}\n\nQSize LargeEmoji::countOptimalSize() {\n\tusing namespace rpl::mappers;\n\n\tconst auto count = _images.size()\n\t\t- ranges::count(_images, LargeEmojiMedia());\n\n\tconst auto single = LargeEmojiImage::Size() / style::DevicePixelRatio();\n\tconst auto skip = st::largeEmojiSkip - 2 * st::largeEmojiOutline;\n\tconst auto inner = count * single.width() + (count - 1) * skip;\n\tconst auto &padding = st::largeEmojiPadding;\n\t_size = QSize(\n\t\tpadding.left() + inner + padding.right(),\n\t\tpadding.top() + single.height() + padding.bottom());\n\treturn _size;\n}\n\nvoid LargeEmoji::draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &r) {\n\t_parent->clearCustomEmojiRepaint();\n\n\tconst auto &padding = st::largeEmojiPadding;\n\tauto x = r.x() + (r.width() - _size.width()) / 2 + padding.left();\n\tconst auto y = r.y() + (r.height() - _size.height()) / 2 + padding.top();\n\tconst auto skip = st::largeEmojiSkip - 2 * st::largeEmojiOutline;\n\tconst auto size = LargeEmojiImage::Size() / style::DevicePixelRatio();\n\tconst auto selected = context.selected();\n\tif (!selected) {\n\t\t_selectedFrame = QImage();\n\t}\n\tfor (const auto &media : _images) {\n\t\tif (const auto image = std::get_if<ImagePtr>(&media)) {\n\t\t\tif (const auto &prepared = (*image)->image) {\n\t\t\t\tconst auto colored = selected\n\t\t\t\t\t? &context.st->msgStickerOverlay()\n\t\t\t\t\t: nullptr;\n\t\t\t\tp.drawPixmap(\n\t\t\t\t\tx,\n\t\t\t\t\ty,\n\t\t\t\t\tprepared->pix(size, { .colored = colored }));\n\t\t\t} else if ((*image)->load) {\n\t\t\t\t(*image)->load();\n\t\t\t}\n\t\t} else if (const auto custom = std::get_if<CustomPtr>(&media)) {\n\t\t\tpaintCustom(p, x, y, custom->get(), context);\n\t\t} else {\n\t\t\tcontinue;\n\t\t}\n\t\tx += size.width() + skip;\n\t}\n}\n\nvoid LargeEmoji::paintCustom(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tnot_null<Ui::Text::CustomEmoji*> emoji,\n\t\tconst PaintContext &context) {\n\tif (!_hasHeavyPart) {\n\t\t_hasHeavyPart = true;\n\t\t_parent->history()->owner().registerHeavyViewPart(_parent);\n\t}\n\tconst auto inner = st::largeEmojiSize + 2 * st::largeEmojiOutline;\n\tconst auto outer = Ui::Text::AdjustCustomEmojiSize(inner);\n\tconst auto skip = (inner - outer) / 2;\n\t//const auto preview = context.imageStyle()->msgServiceBg->c;\n\tauto &textst = context.st->messageStyle(false, false);\n\tif (context.selected()) {\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tconst auto size = QSize(outer, outer) * factor;\n\t\tif (_selectedFrame.size() != size) {\n\t\t\t_selectedFrame = QImage(\n\t\t\t\tsize,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_selectedFrame.setDevicePixelRatio(factor);\n\t\t}\n\t\t_selectedFrame.fill(Qt::transparent);\n\t\tauto q = QPainter(&_selectedFrame);\n\t\temoji->paint(q, {\n\t\t\t.textColor = textst.historyTextFg->c,\n\t\t\t.now = context.now,\n\t\t\t.paused = context.paused,\n\t\t});\n\t\tq.end();\n\n\t\t_selectedFrame = Images::Colored(\n\t\t\tstd::move(_selectedFrame),\n\t\t\tcontext.st->msgStickerOverlay()->c);\n\t\tp.drawImage(x + skip, y + skip, _selectedFrame);\n\t} else {\n\t\temoji->paint(p, {\n\t\t\t.textColor = textst.historyTextFg->c,\n\t\t\t.now = context.now,\n\t\t\t.position = { x + skip, y + skip },\n\t\t\t.paused = context.paused,\n\t\t});\n\t}\n}\n\nbool LargeEmoji::hasHeavyPart() const {\n\treturn _hasHeavyPart;\n}\n\nvoid LargeEmoji::unloadHeavyPart() {\n\tif (_hasHeavyPart) {\n\t\t_hasHeavyPart = false;\n\t\tfor (auto &media : _images) {\n\t\t\tif (const auto custom = std::get_if<CustomPtr>(&media)) {\n\t\t\t\t(*custom)->unload();\n\t\t\t}\n\t\t}\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_large_emoji.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media_unwrapped.h\"\n#include \"ui/text/text_isolated_emoji.h\"\n\nnamespace Stickers {\nstruct LargeEmojiImage;\n} // namespace Stickers\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nnamespace HistoryView {\n\nusing LargeEmojiMedia = std::variant<\n\tv::null_t,\n\tstd::shared_ptr<Stickers::LargeEmojiImage>,\n\tstd::unique_ptr<Ui::Text::CustomEmoji>>;\n\nclass LargeEmoji final : public UnwrappedMedia::Content {\npublic:\n\tLargeEmoji(\n\t\tnot_null<Element*> parent,\n\t\tconst Ui::Text::IsolatedEmoji &emoji);\n\t~LargeEmoji();\n\n\tQSize countOptimalSize() override;\n\tvoid draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &r) override;\n\n\tbool alwaysShowOutTimestamp() override {\n\t\treturn true;\n\t}\n\tbool hasTextForCopy() const override {\n\t\treturn true;\n\t}\n\n\tbool hasHeavyPart() const override;\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\tvoid paintCustom(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tnot_null<Ui::Text::CustomEmoji*> emoji,\n\t\tconst PaintContext &context);\n\n\tconst not_null<Element*> _parent;\n\tconst std::array<LargeEmojiMedia, Ui::Text::kIsolatedEmojiLimit> _images;\n\tQImage _selectedFrame;\n\tQSize _size;\n\tbool _hasHeavyPart = false;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_location.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_location.h\"\n\n#include \"base/unixtime.h\"\n#include \"history/history.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item.h\"\n#include \"history/history_location_manager.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/image/image.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/painter.h\"\n#include \"data/data_session.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_cloud_file.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kUntilOffPeriod = std::numeric_limits<TimeId>::max();\nconstexpr auto kLiveElapsedPartOpacity = 0.2;\n\n[[nodiscard]] TimeId ResolveUpdateDate(not_null<Element*> view) {\n\tconst auto item = view->data();\n\tconst auto edited = item->Get<HistoryMessageEdited>();\n\treturn edited ? edited->date : item->date();\n}\n\n[[nodiscard]] QString RemainingTimeText(\n\t\tnot_null<Element*> view,\n\t\tTimeId period) {\n\tif (period == kUntilOffPeriod) {\n\t\treturn QString(1, QChar(0x221E));\n\t}\n\tconst auto elapsed = base::unixtime::now() - view->data()->date();\n\tconst auto remaining = std::clamp(period - elapsed, 0, period);\n\tif (remaining < 10) {\n\t\treturn tr::lng_seconds_tiny(tr::now, lt_count, remaining);\n\t} else if (remaining < 600) {\n\t\treturn tr::lng_minutes_tiny(tr::now, lt_count, remaining / 60);\n\t} else if (remaining < 3600) {\n\t\treturn QString::number(remaining / 60);\n\t} else if (remaining < 86400) {\n\t\treturn tr::lng_hours_tiny(tr::now, lt_count, remaining / 3600);\n\t}\n\treturn tr::lng_days_tiny(tr::now, lt_count, remaining / 86400);\n}\n\n[[nodiscard]] float64 RemainingTimeProgress(\n\t\tnot_null<Element*> view,\n\t\tTimeId period) {\n\tif (period == kUntilOffPeriod) {\n\t\treturn 1.;\n\t} else if (period < 1) {\n\t\treturn 0.;\n\t}\n\tconst auto elapsed = base::unixtime::now() - view->data()->date();\n\treturn std::clamp(period - elapsed, 0, period) / float64(period);\n}\n\n} // namespace\n\nstruct Location::Live {\n\texplicit Live(TimeId period) : period(period) {\n\t}\n\n\tbase::Timer updateStatusTimer;\n\tbase::Timer updateRemainingTimer;\n\tQImage previous;\n\tQImage previousCache;\n\tUi::BubbleRounding previousRounding;\n\tUi::Animations::Simple crossfade;\n\tTimeId period = 0;\n\tint thumbnailHeight = 0;\n};\n\nLocation::Location(\n\tnot_null<Element*> parent,\n\tnot_null<Data::CloudImage*> data,\n\tData::LocationPoint point,\n\tElement *replacing,\n\tTimeId livePeriod)\n: Media(parent)\n, _data(data)\n, _live(CreateLiveTracker(parent, livePeriod))\n, _title(st::msgMinWidth)\n, _description(st::msgMinWidth)\n, _link(std::make_shared<LocationClickHandler>(point))\n, _liveLocation(livePeriod > 0) {\n\tif (_live) {\n\t\t_title.setText(\n\t\t\tst::webPageTitleStyle,\n\t\t\ttr::lng_live_location(tr::now),\n\t\t\tUi::WebpageTextTitleOptions());\n\t\t_live->updateStatusTimer.setCallback([=] {\n\t\t\tupdateLiveStatus();\n\t\t\tcheckLiveFinish();\n\t\t});\n\t\t_live->updateRemainingTimer.setCallback([=] {\n\t\t\tcheckLiveFinish();\n\t\t});\n\t\tupdateLiveStatus();\n\t\tif (const auto media = replacing ? replacing->media() : nullptr) {\n\t\t\t_live->previous = media->locationTakeImage();\n\t\t\tif (!_live->previous.isNull()) {\n\t\t\t\thistory()->owner().registerHeavyViewPart(_parent);\n\t\t\t}\n\t\t}\n\t}\n}\n\nLocation::Location(\n\tnot_null<Element*> parent,\n\tnot_null<Data::CloudImage*> data,\n\tData::LocationPoint point,\n\tconst QString &title,\n\tconst QString &description)\n: Media(parent)\n, _data(data)\n, _title(st::msgMinWidth)\n, _description(st::msgMinWidth)\n, _link(std::make_shared<LocationClickHandler>(point)) {\n\tif (!title.isEmpty()) {\n\t\t_title.setText(\n\t\t\tst::webPageTitleStyle,\n\t\t\ttitle,\n\t\t\tUi::WebpageTextTitleOptions());\n\t}\n\tif (!description.isEmpty()) {\n\t\t_description.setMarkedText(\n\t\t\tst::webPageDescriptionStyle,\n\t\t\tTextUtilities::ParseEntities(\n\t\t\t\tdescription,\n\t\t\t\tTextParseLinks | TextParseMultiline),\n\t\t\tUi::WebpageTextDescriptionOptions());\n\t}\n}\n\nLocation::~Location() {\n\tif (hasHeavyPart()) {\n\t\tunloadHeavyPart();\n\t\t_parent->checkHeavyPart();\n\t}\n}\n\nvoid Location::checkLiveFinish() {\n\tExpects(_live != nullptr);\n\n\tconst auto now = base::unixtime::now();\n\tconst auto item = _parent->data();\n\tconst auto start = item->date();\n\tif (_live->period != kUntilOffPeriod && now - start >= _live->period) {\n\t\tconst auto had = hasHeavyPart();\n\t\t_title.clear();\n\t\t_description.clear();\n\t\t_live = nullptr;\n\t\tif (had && !hasHeavyPart()) {\n\t\t\t_parent->checkHeavyPart();\n\t\t}\n\t\titem->history()->owner().requestViewResize(_parent);\n\t} else {\n\t\t_parent->repaint();\n\t}\n}\n\nstd::unique_ptr<Location::Live> Location::CreateLiveTracker(\n\t\tnot_null<Element*> parent,\n\t\tTimeId period) {\n\tif (!period) {\n\t\treturn nullptr;\n\t}\n\tconst auto now = base::unixtime::now();\n\tconst auto date = parent->data()->date();\n\treturn (now < date || now - date < period)\n\t\t? std::make_unique<Live>(period)\n\t\t: nullptr;\n}\n\nvoid Location::updateLiveStatus() {\n\tconst auto date = ResolveUpdateDate(_parent);\n\tconst auto now = base::unixtime::now();\n\tconst auto elapsed = now - date;\n\tauto next = TimeId();\n\tconst auto text = [&] {\n\t\tif (elapsed < 60) {\n\t\t\tnext = 60 - elapsed;\n\t\t\treturn tr::lng_live_location_now(tr::now);\n\t\t} else if (const auto minutes = elapsed / 60; minutes < 60) {\n\t\t\tnext = 60 - (elapsed % 60);\n\t\t\treturn tr::lng_live_location_minutes(tr::now, lt_count, minutes);\n\t\t} else if (const auto hours = elapsed / 3600; hours < 12) {\n\t\t\tnext = 3600 - (elapsed % 3600);\n\t\t\treturn tr::lng_live_location_hours(tr::now, lt_count, hours);\n\t\t}\n\t\tconst auto dateFull = base::unixtime::parse(date);\n\t\tconst auto nowFull = base::unixtime::parse(now);\n\t\tconst auto nextTomorrow = [&] {\n\t\t\tconst auto tomorrow = nowFull.date().addDays(1);\n\t\t\tnext = nowFull.secsTo(QDateTime(tomorrow, QTime(0, 0)));\n\t\t};\n\t\tconst auto locale = QLocale();\n\t\tconst auto format = QLocale::ShortFormat;\n\t\tif (dateFull.date() == nowFull.date()) {\n\t\t\tnextTomorrow();\n\t\t\tconst auto time = locale.toString(dateFull.time(), format);\n\t\t\treturn tr::lng_live_location_today(tr::now, lt_time, time);\n\t\t} else if (dateFull.date().addDays(1) == nowFull.date()) {\n\t\t\tnextTomorrow();\n\t\t\tconst auto time = locale.toString(dateFull.time(), format);\n\t\t\treturn tr::lng_live_location_yesterday(tr::now, lt_time, time);\n\t\t}\n\t\treturn tr::lng_live_location_date_time(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\tlocale.toString(dateFull.date(), format),\n\t\t\tlt_time,\n\t\t\tlocale.toString(dateFull.time(), format));\n\t}();\n\t_description.setMarkedText(\n\t\tst::webPageDescriptionStyle,\n\t\t{ text },\n\t\tUi::WebpageTextDescriptionOptions());\n\tif (next > 0 && next < 86400) {\n\t\t_live->updateStatusTimer.callOnce(next * crl::time(1000));\n\t}\n}\n\nQImage Location::locationTakeImage() {\n\tif (_media && !_media->isNull()) {\n\t\treturn *_media;\n\t} else if (_live && !_live->previous.isNull()) {\n\t\treturn _live->previous;\n\t}\n\treturn {};\n}\n\nvoid Location::unloadHeavyPart() {\n\t_media = nullptr;\n\tif (_userpic) {\n\t\t_userpic->subscribeToUpdates(nullptr);\n\t\t_userpic = nullptr;\n\t}\n\tif (_live) {\n\t\t_live->previous = QImage();\n\t}\n}\n\nbool Location::hasHeavyPart() const {\n\treturn (_media != nullptr)\n\t\t|| (_userpic != nullptr)\n\t\t|| (_live && !_live->previous.isNull());\n}\n\nvoid Location::ensureMediaCreated() const {\n\tif (_media) {\n\t\treturn;\n\t}\n\t_media = _data->createView();\n\t_data->load(&history()->session(), _parent->data()->fullId());\n\thistory()->owner().registerHeavyViewPart(_parent);\n}\n\nvoid Location::ensureUserpicCreated() const {\n\tif (_userpic) {\n\t\treturn;\n\t}\n\tconst auto peer = _parent->data()->from();\n\t_userpic = Ui::MakeUserpicThumbnail(peer, true);\n\t_userpic->subscribeToUpdates([parent = _parent] {\n\t\tparent->repaint();\n\t});\n\thistory()->owner().registerHeavyViewPart(_parent);\n}\n\nQSize Location::countOptimalSize() {\n\tauto tw = fullWidth();\n\tauto th = fullHeight();\n\tif (tw > st::maxMediaSize) {\n\t\tth = (st::maxMediaSize * th) / tw;\n\t\ttw = st::maxMediaSize;\n\t}\n\tauto minWidth = std::clamp(\n\t\t_parent->minWidthForMedia(),\n\t\tst::minPhotoSize,\n\t\tst::maxMediaSize);\n\tauto maxWidth = qMax(tw, minWidth);\n\tauto minHeight = qMax(th, st::minPhotoSize);\n\n\tif (_parent->hasBubble()) {\n\t\tif (!_title.isEmpty()) {\n\t\t\tminHeight += qMin(_title.countHeight(maxWidth - st::msgPadding.left() - st::msgPadding.right()), 2 * st::webPageTitleFont->height);\n\t\t}\n\t\tif (!_description.isEmpty()) {\n\t\t\tminHeight += qMin(_description.countHeight(maxWidth - st::msgPadding.left() - st::msgPadding.right()), 3 * st::webPageDescriptionFont->height);\n\t\t}\n\t\tif (!_title.isEmpty() || !_description.isEmpty()) {\n\t\t\tminHeight += st::mediaInBubbleSkip;\n\t\t\tif (isBubbleBottom()) {\n\t\t\t\tminHeight += st::msgPadding.bottom();\n\t\t\t}\n\t\t}\n\t}\n\treturn { maxWidth, minHeight };\n}\n\nQSize Location::countCurrentSize(int newWidth) {\n\taccumulate_min(newWidth, maxWidth());\n\n\tauto tw = fullWidth();\n\tauto th = fullHeight();\n\tif (tw > st::maxMediaSize) {\n\t\tth = (st::maxMediaSize * th) / tw;\n\t\ttw = st::maxMediaSize;\n\t}\n\tauto newHeight = th;\n\tif (tw > newWidth) {\n\t\tnewHeight = (newWidth * newHeight / tw);\n\t} else {\n\t\tnewWidth = tw;\n\t}\n\tauto minWidth = std::clamp(\n\t\t_parent->minWidthForMedia(),\n\t\tst::minPhotoSize,\n\t\tstd::min(newWidth, st::maxMediaSize));\n\taccumulate_max(newWidth, minWidth);\n\taccumulate_max(newHeight, st::minPhotoSize);\n\t_thumbnailHeight = newHeight;\n\tif (_live) {\n\t\t_live->thumbnailHeight = newHeight;\n\t}\n\tif (_parent->hasBubble()) {\n\t\tif (!_title.isEmpty()) {\n\t\t\tnewHeight += qMin(_title.countHeight(newWidth - st::msgPadding.left() - st::msgPadding.right()), st::webPageTitleFont->height * 2);\n\t\t}\n\t\tif (!_description.isEmpty()) {\n\t\t\tnewHeight += qMin(_description.countHeight(newWidth - st::msgPadding.left() - st::msgPadding.right()), st::webPageDescriptionFont->height * 3);\n\t\t}\n\t\tif (!_title.isEmpty() || !_description.isEmpty()) {\n\t\t\tnewHeight += st::mediaInBubbleSkip;\n\t\t\tif (isBubbleBottom()) {\n\t\t\t\tnewHeight += st::msgPadding.bottom();\n\t\t\t}\n\t\t}\n\t}\n\treturn { newWidth, newHeight };\n}\n\nTextSelection Location::toDescriptionSelection(\n\t\tTextSelection selection) const {\n\treturn UnshiftItemSelection(selection, _title);\n}\n\nTextSelection Location::fromDescriptionSelection(\n\t\tTextSelection selection) const {\n\treturn ShiftItemSelection(selection, _title);\n}\n\nvoid Location::draw(Painter &p, const PaintContext &context) const {\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {\n\t\treturn;\n\t}\n\tauto paintx = 0, painty = 0, paintw = width(), painth = height();\n\tbool bubble = _parent->hasBubble();\n\tconst auto st = context.st;\n\tconst auto stm = context.messageStyle();\n\n\tconst auto hasText = !_title.isEmpty() || !_description.isEmpty();\n\tconst auto rounding = adjustedBubbleRounding(hasText\n\t\t? RectPart::FullBottom\n\t\t: RectPart());\n\tconst auto paintText = [&] {\n\t\tif (!hasText && !_live) {\n\t\t\treturn;\n\t\t}\n\t\tpainty += st::mediaInBubbleSkip;\n\n\t\tauto textw = width() - st::msgPadding.left() - st::msgPadding.right();\n\n\t\tp.setPen(stm->historyTextFg);\n\t\tif (!_title.isEmpty()) {\n\t\t\t_title.drawLeftElided(p, paintx + st::msgPadding.left(), painty, textw, width(), 2, style::al_left, 0, -1, 0, false, context.selection);\n\t\t\tpainty += qMin(_title.countHeight(textw), 2 * st::webPageTitleFont->height);\n\t\t}\n\t\tif (!_description.isEmpty()) {\n\t\t\tif (_live) {\n\t\t\t\tp.setPen(stm->msgDateFg);\n\t\t\t}\n\t\t\t_description.drawLeftElided(p, paintx + st::msgPadding.left(), painty, textw, width(), 3, style::al_left, 0, -1, 0, false, toDescriptionSelection(context.selection));\n\t\t\tpainty += qMin(_description.countHeight(textw), 3 * st::webPageDescriptionFont->height);\n\t\t}\n\t};\n\tconst auto thumbh = _thumbnailHeight;\n\tauto rthumb = QRect(paintx, painty, paintw, thumbh);\n\tif (!bubble) {\n\t\tfillImageShadow(p, rthumb, rounding, context);\n\t}\n\n\tensureMediaCreated();\n\tvalidateImageCache(rthumb.size(), rounding);\n\tconst auto paintPrevious = _live && !_live->previous.isNull();\n\tauto opacity = _imageCache.isNull() ? 0. : 1.;\n\tif (paintPrevious) {\n\t\topacity = _live->crossfade.value(opacity);\n\t\tif (opacity < 1.) {\n\t\t\tp.drawImage(rthumb.topLeft(), _live->previousCache);\n\t\t\tif (opacity > 0.) {\n\t\t\t\tp.setOpacity(opacity);\n\t\t\t}\n\t\t}\n\t}\n\tif (!_imageCache.isNull() && opacity > 0.) {\n\t\tp.drawImage(rthumb.topLeft(), _imageCache);\n\t\tif (opacity < 1.) {\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t} else if (!bubble && !paintPrevious) {\n\t\tUi::PaintBubble(\n\t\t\tp,\n\t\t\tUi::SimpleBubble{\n\t\t\t\t.st = context.st,\n\t\t\t\t.geometry = rthumb,\n\t\t\t\t.pattern = context.bubblesPattern,\n\t\t\t\t.patternViewport = context.viewport,\n\t\t\t\t.outerWidth = width(),\n\t\t\t\t.selected = context.selected(),\n\t\t\t\t.outbg = context.outbg,\n\t\t\t\t.rounding = rounding,\n\t\t\t});\n\t}\n\tif (_liveLocation) {\n\t\tensureUserpicCreated();\n\n\t\tconst auto pinRadius = st::historyMapPinRadius;\n\t\tconst auto userpicSize = st::historyMapPinUserpicSize;\n\t\tconst auto tailHeight = st::historyMapPinTailHeight;\n\t\tconst auto tailHalfWidth = st::historyMapPinTailHalfWidth;\n\n\t\tconst auto cx = float64(rthumb.x() + rthumb.width() / 2);\n\t\tconst auto tipY = float64(rthumb.y() + rthumb.height() / 2);\n\t\tconst auto circleY = tipY - tailHeight - pinRadius;\n\n\t\tconst auto r = float64(pinRadius);\n\t\tconst auto w = float64(tailHalfWidth);\n\t\tconst auto attachAngle = std::asin(w / r) * 180.0 / M_PI;\n\n\t\tconst auto circleRect = QRectF(cx - r, circleY - r, 2.0 * r, 2.0 * r);\n\n\t\tauto pin = QPainterPath();\n\t\tpin.arcMoveTo(circleRect, 270.0 - attachAngle);\n\t\tpin.arcTo(\n\t\t\tcircleRect,\n\t\t\t270.0 - attachAngle,\n\t\t\t-(360.0 - 2.0 * attachAngle));\n\t\tpin.lineTo(cx, tipY);\n\t\tpin.closeSubpath();\n\n\t\tif (!_pinShadow) {\n\t\t\t_pinShadow = std::make_unique<Ui::BoxShadow>(\n\t\t\t\tst::historyMapPinShadow);\n\t\t}\n\t\t_pinShadow->paint(\n\t\t\tp,\n\t\t\tcircleRect.toAlignedRect(),\n\t\t\tpinRadius);\n\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\n\t\tconst auto dotRadius = st::historyMapPinDotRadius;\n\t\tconst auto dotStroke = st::historyMapPinDotStroke;\n\t\tp.setBrush(st::mapPointDot);\n\t\tp.drawEllipse(\n\t\t\tQPointF(cx, tipY + dotRadius + dotStroke),\n\t\t\tdotRadius + dotStroke,\n\t\t\tdotRadius + dotStroke);\n\t\tp.setBrush(st::mapPointDrop);\n\t\tp.drawEllipse(\n\t\t\tQPointF(cx, tipY + dotRadius + dotStroke),\n\t\t\tdotRadius,\n\t\t\tdotRadius);\n\n\t\tp.setBrush(st::mapPointDot);\n\t\tp.drawPath(pin);\n\n\t\tconst auto userpicImage = _userpic->image(userpicSize);\n\t\tp.drawImage(\n\t\t\tQRectF(\n\t\t\t\tcx - userpicSize / 2.0,\n\t\t\t\tcircleY - userpicSize / 2.0,\n\t\t\t\tuserpicSize,\n\t\t\t\tuserpicSize),\n\t\t\tuserpicImage);\n\t} else {\n\t\tconst auto paintMarker = [&](const style::icon &icon) {\n\t\t\ticon.paint(\n\t\t\t\tp,\n\t\t\t\trthumb.x() + ((rthumb.width() - icon.width()) / 2),\n\t\t\t\trthumb.y() + (rthumb.height() / 2) - icon.height(),\n\t\t\t\twidth());\n\t\t};\n\t\tpaintMarker(st->historyMapPoint());\n\t\tpaintMarker(st->historyMapPointInner());\n\t}\n\tif (context.selected()) {\n\t\tfillImageOverlay(p, rthumb, rounding, context);\n\t}\n\tpainty += thumbh;\n\tif (_live) {\n\t\tpainth -= thumbh;\n\t\tpaintLiveRemaining(p, context, { paintx, painty, paintw, painth });\n\t}\n\tpaintText();\n\tif (!_live && !hasText && _parent->media() == this) {\n\t\tauto fullRight = paintx + paintw;\n\t\tauto fullBottom = height();\n\t\t_parent->drawInfo(\n\t\t\tp,\n\t\t\tcontext,\n\t\t\tfullRight,\n\t\t\tfullBottom,\n\t\t\tpaintx * 2 + paintw,\n\t\t\tInfoDisplayType::Image);\n\t\tif (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) {\n\t\t\tauto fastShareLeft = _parent->hasRightLayout()\n\t\t\t\t? (paintx - size->width() - st::historyFastShareLeft)\n\t\t\t\t: (fullRight + st::historyFastShareLeft);\n\t\t\tauto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());\n\t\t\t_parent->drawRightAction(p, context, fastShareLeft, fastShareTop, 2 * paintx + paintw);\n\t\t}\n\t}\n}\n\nvoid Location::paintLiveRemaining(\n\t\tQPainter &p,\n\t\tconst PaintContext &context,\n\t\tQRect bottom) const {\n\tconst auto size = st::liveLocationRemainingSize;\n\tconst auto skip = (bottom.height() - size) / 2;\n\tconst auto rect = QRect(\n\t\tbottom.x() + bottom.width() - size - skip,\n\t\tbottom.y() + skip,\n\t\tsize,\n\t\tsize);\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto stm = context.messageStyle();\n\tconst auto color = stm->msgServiceFg->c;\n\tconst auto untiloff = (_live->period == kUntilOffPeriod);\n\tconst auto progress = RemainingTimeProgress(_parent, _live->period);\n\tconst auto part = 1. / 360;\n\tconst auto full = (progress >= 1. - part);\n\tauto elapsed = color;\n\tif (!full) {\n\t\telapsed.setAlphaF(elapsed.alphaF() * kLiveElapsedPartOpacity);\n\t}\n\tauto pen = QPen(elapsed);\n\tconst auto stroke = style::ConvertScaleExact(2.);\n\tpen.setWidthF(stroke);\n\tp.setPen(pen);\n\tp.setBrush(Qt::NoBrush);\n\tp.drawEllipse(rect);\n\n\tif (untiloff) {\n\t\tstm->liveLocationLongIcon.paintInCenter(p, rect);\n\t} else {\n\t\tif (!full && progress > part) {\n\t\t\tauto pen = QPen(color);\n\t\t\tpen.setWidthF(stroke);\n\t\t\tp.setPen(pen);\n\t\t\tp.drawArc(\n\t\t\t\trect,\n\t\t\t\tarc::kQuarterLength,\n\t\t\t\tint(base::SafeRound(arc::kFullLength * progress)));\n\t\t}\n\n\t\tp.setPen(stm->msgServiceFg);\n\t\tp.setFont(st::semiboldFont);\n\t\tconst auto text = RemainingTimeText(_parent, _live->period);\n\t\tp.drawText(rect, text, style::al_center);\n\t\tconst auto each = std::clamp(_live->period / 360, 1, 86400);\n\t\t_live->updateRemainingTimer.callOnce(each * crl::time(1000));\n\t}\n}\n\nvoid Location::validateImageCache(\n\t\tQSize outer,\n\t\tUi::BubbleRounding rounding) const {\n\tExpects(_media != nullptr);\n\n\tif (_live && !_live->previous.isNull()) {\n\t\tvalidateImageCache(\n\t\t\t_live->previous,\n\t\t\t_live->previousCache,\n\t\t\t_live->previousRounding,\n\t\t\touter,\n\t\t\trounding);\n\t}\n\tvalidateImageCache(\n\t\t*_media,\n\t\t_imageCache,\n\t\t_imageCacheRounding,\n\t\touter,\n\t\trounding);\n\tcheckLiveCrossfadeStart();\n}\n\nvoid Location::checkLiveCrossfadeStart() const {\n\tif (!_live\n\t\t|| _live->previous.isNull()\n\t\t|| !_media\n\t\t|| _media->isNull()\n\t\t|| _live->crossfade.animating()) {\n\t\treturn;\n\t}\n\t_live->crossfade.start([=] {\n\t\tif (!_live->crossfade.animating()) {\n\t\t\t_live->previous = QImage();\n\t\t\t_live->previousCache = QImage();\n\t\t}\n\t\t_parent->repaint();\n\t}, 0., 1., st::fadeWrapDuration);\n}\n\nvoid Location::validateImageCache(\n\t\tconst QImage &source,\n\t\tQImage &cache,\n\t\tUi::BubbleRounding &cacheRounding,\n\t\tQSize outer,\n\t\tUi::BubbleRounding rounding) const {\n\tif (source.isNull()) {\n\t\treturn;\n\t}\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (cache.size() == (outer * ratio) && cacheRounding == rounding) {\n\t\treturn;\n\t}\n\tcache = Images::Round(\n\t\tsource.scaled(\n\t\t\touter * ratio,\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation),\n\t\tMediaRoundingMask(rounding));\n\tcache.setDevicePixelRatio(ratio);\n\tcacheRounding = rounding;\n}\n\nTextState Location::textState(QPoint point, StateRequest request) const {\n\tauto result = TextState(_parent);\n\tauto symbolAdd = 0;\n\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {\n\t\treturn result;\n\t}\n\tauto paintx = 0, painty = 0, paintw = width();\n\tbool bubble = _parent->hasBubble();\n\n\tconst auto hasText = !_title.isEmpty() || !_description.isEmpty();\n\tauto checkText = [&] {\n\t\tif (!hasText && !_live) {\n\t\t\treturn false;\n\t\t}\n\t\tpainty += st::mediaInBubbleSkip;\n\n\t\tauto textw = width() - st::msgPadding.left() - st::msgPadding.right();\n\n\t\tif (!_title.isEmpty()) {\n\t\t\tauto titleh = qMin(_title.countHeight(textw), 2 * st::webPageTitleFont->height);\n\t\t\tif (point.y() >= painty && point.y() < painty + titleh) {\n\t\t\t\tresult = TextState(_parent, _title.getStateLeft(\n\t\t\t\t\tpoint - QPoint(paintx + st::msgPadding.left(), painty),\n\t\t\t\t\ttextw,\n\t\t\t\t\twidth(),\n\t\t\t\t\trequest.forText()));\n\t\t\t\treturn true;\n\t\t\t} else if (point.y() >= painty + titleh) {\n\t\t\t\tsymbolAdd += _title.length();\n\t\t\t}\n\t\t\tpainty += titleh;\n\t\t}\n\t\tif (!_description.isEmpty()) {\n\t\t\tauto descriptionh = qMin(_description.countHeight(textw), 3 * st::webPageDescriptionFont->height);\n\t\t\tif (point.y() >= painty && point.y() < painty + descriptionh) {\n\t\t\t\tresult = TextState(_parent, _description.getStateLeft(\n\t\t\t\t\tpoint - QPoint(paintx + st::msgPadding.left(), painty),\n\t\t\t\t\ttextw,\n\t\t\t\t\twidth(),\n\t\t\t\t\trequest.forText()));\n\t\t\t\tresult.symbol += symbolAdd;\n\t\t\t\treturn true;\n\t\t\t} else if (point.y() >= painty + descriptionh) {\n\t\t\t\tsymbolAdd += _description.length();\n\t\t\t}\n\t\t\tpainty += descriptionh;\n\t\t}\n\t\treturn false;\n\t};\n\tconst auto thumbh = _thumbnailHeight;\n\tif (QRect(paintx, painty, paintw, thumbh).contains(point) && _data) {\n\t\tresult.link = _link;\n\t}\n\tpainty += thumbh;\n\tif (checkText()) {\n\t\treturn result;\n\t}\n\tif (!_live && !hasText && _parent->media() == this) {\n\t\tauto fullRight = paintx + paintw;\n\t\tauto fullBottom = height();\n\t\tconst auto bottomInfoResult = _parent->bottomInfoTextState(\n\t\t\tfullRight,\n\t\t\tfullBottom,\n\t\t\tpoint,\n\t\t\tInfoDisplayType::Image);\n\t\tif (bottomInfoResult.link\n\t\t\t|| bottomInfoResult.cursor != CursorState::None\n\t\t\t|| bottomInfoResult.customTooltip) {\n\t\t\treturn bottomInfoResult;\n\t\t}\n\t\tif (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) {\n\t\t\tauto fastShareLeft = _parent->hasRightLayout()\n\t\t\t\t? (paintx - size->width() - st::historyFastShareLeft)\n\t\t\t\t: (fullRight + st::historyFastShareLeft);\n\t\t\tauto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());\n\t\t\tif (QRect(fastShareLeft, fastShareTop, size->width(), size->height()).contains(point)) {\n\t\t\t\tresult.link = _parent->rightActionLink(point\n\t\t\t\t\t- QPoint(fastShareLeft, fastShareTop));\n\t\t\t}\n\t\t}\n\t}\n\tresult.symbol += symbolAdd;\n\treturn result;\n}\n\nTextSelection Location::adjustSelection(TextSelection selection, TextSelectType type) const {\n\tif (_description.isEmpty() || selection.to <= _title.length()) {\n\t\treturn _title.adjustSelection(selection, type);\n\t}\n\tauto descriptionSelection = _description.adjustSelection(toDescriptionSelection(selection), type);\n\tif (selection.from >= _title.length()) {\n\t\treturn fromDescriptionSelection(descriptionSelection);\n\t}\n\tauto titleSelection = _title.adjustSelection(selection, type);\n\treturn { titleSelection.from, fromDescriptionSelection(descriptionSelection).to };\n}\n\nTextForMimeData Location::selectedText(TextSelection selection) const {\n\tauto titleResult = _title.toTextForMimeData(selection);\n\tauto descriptionResult = _description.toTextForMimeData(\n\t\ttoDescriptionSelection(selection));\n\tif (titleResult.empty()) {\n\t\treturn descriptionResult;\n\t} else if (descriptionResult.empty()) {\n\t\treturn titleResult;\n\t}\n\treturn titleResult.append('\\n').append(std::move(descriptionResult));\n}\n\nbool Location::needsBubble() const {\n\tif (!_title.isEmpty() || !_description.isEmpty()) {\n\t\treturn true;\n\t}\n\tconst auto item = _parent->data();\n\treturn item->repliesAreComments()\n\t\t|| item->externalReply()\n\t\t|| item->viaBot()\n\t\t|| _parent->displayReply()\n\t\t|| _parent->displayForwardedFrom()\n\t\t|| _parent->displayFromName()\n\t\t|| _parent->displayedTopicButton();\n}\n\nQPoint Location::resolveCustomInfoRightBottom() const {\n\tconst auto skipx = (st::msgDateImgDelta + st::msgDateImgPadding.x());\n\tconst auto skipy = (st::msgDateImgDelta + st::msgDateImgPadding.y());\n\treturn QPoint(width() - skipx, height() - skipy);\n}\n\nint Location::fullWidth() const {\n\treturn st::locationSize.width();\n}\n\nint Location::fullHeight() const {\n\treturn st::locationSize.height();\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_location.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n#include \"data/data_location.h\"\n\nnamespace Data {\nclass CloudImage;\n} // namespace Data\n\nnamespace Ui {\nclass BoxShadow;\nclass DynamicImage;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass Location : public Media {\npublic:\n\tLocation(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<Data::CloudImage*> data,\n\t\tData::LocationPoint point,\n\t\tElement *replacing = nullptr,\n\t\tTimeId livePeriod = 0);\n\tLocation(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<Data::CloudImage*> data,\n\t\tData::LocationPoint point,\n\t\tconst QString &title,\n\t\tconst QString &description);\n\t~Location();\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\n\t[[nodiscard]] TextSelection adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const override;\n\tuint16 fullSelectionLength() const override {\n\t\treturn _title.length() + _description.length();\n\t}\n\tbool hasTextForCopy() const override {\n\t\treturn !_title.isEmpty() || !_description.isEmpty();\n\t}\n\n\tbool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {\n\t\treturn p == _link;\n\t}\n\tbool dragItemByHandler(const ClickHandlerPtr &p) const override {\n\t\treturn p == _link;\n\t}\n\n\tTextForMimeData selectedText(TextSelection selection) const override;\n\n\tbool needsBubble() const override;\n\tbool customInfoLayout() const override {\n\t\treturn _live || (_title.isEmpty() && _description.isEmpty());\n\t}\n\tQPoint resolveCustomInfoRightBottom() const override;\n\n\tbool skipBubbleTail() const override {\n\t\treturn _title.isEmpty()\n\t\t\t&& _description.isEmpty()\n\t\t\t&& isRoundedInBubbleBottom();\n\t}\n\n\tQImage locationTakeImage() override;\n\n\tvoid unloadHeavyPart() override;\n\tbool hasHeavyPart() const override;\n\nprivate:\n\tstruct Live;\n\t[[nodiscard]] static std::unique_ptr<Live> CreateLiveTracker(\n\t\tnot_null<Element*> parent,\n\t\tTimeId period);\n\n\tvoid ensureMediaCreated() const;\n\tvoid ensureUserpicCreated() const;\n\n\tvoid validateImageCache(\n\t\tQSize outer,\n\t\tUi::BubbleRounding rounding) const;\n\tvoid validateImageCache(\n\t\tconst QImage &source,\n\t\tQImage &cache,\n\t\tUi::BubbleRounding &cacheRounding,\n\t\tQSize outer,\n\t\tUi::BubbleRounding rounding) const;\n\n\tvoid paintLiveRemaining(\n\t\tQPainter &p,\n\t\tconst PaintContext &context,\n\t\tQRect bottom) const;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\n\t[[nodiscard]] TextSelection toDescriptionSelection(\n\t\tTextSelection selection) const;\n\t[[nodiscard]] TextSelection fromDescriptionSelection(\n\t\tTextSelection selection) const;\n\n\t[[nodiscard]] int fullWidth() const;\n\t[[nodiscard]] int fullHeight() const;\n\n\tvoid checkLiveCrossfadeStart() const;\n\tvoid updateLiveStatus();\n\tvoid checkLiveFinish();\n\n\tconst not_null<Data::CloudImage*> _data;\n\tmutable std::unique_ptr<Live> _live;\n\tmutable std::shared_ptr<QImage> _media;\n\tmutable std::shared_ptr<Ui::DynamicImage> _userpic;\n\tmutable std::unique_ptr<Ui::BoxShadow> _pinShadow;\n\tUi::Text::String _title, _description;\n\tClickHandlerPtr _link;\n\tbool _liveLocation = false;\n\n\tint _thumbnailHeight = 0;\n\tmutable QImage _imageCache;\n\tmutable Ui::BubbleRounding _imageCacheRounding;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_media.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_media.h\"\n\n#include \"boxes/send_credits_box.h\" // CreditsEmoji.\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_text_helper.h\"\n#include \"history/view/media/history_view_media_common.h\"\n#include \"history/view/media/history_view_media_spoiler.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_web_page.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/item_text_options.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/message_bubble.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"core/ui_integration.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\" // mediaMenuIconStealth.\n\nnamespace HistoryView {\nnamespace {\n\n[[nodiscard]] TimeId TimeFromMatch(\n\t\tQStringView hours,\n\t\tQStringView minutes1,\n\t\tQStringView minutes2,\n\t\tQStringView seconds) {\n\tauto ok1 = true;\n\tauto ok2 = true;\n\tauto ok3 = true;\n\tauto minutes = minutes1.toString();\n\tminutes += minutes2;\n\tconst auto value1 = (hours.isEmpty() ? 0 : hours.toInt(&ok1));\n\tconst auto value2 = minutes.toInt(&ok2);\n\tconst auto value3 = seconds.toInt(&ok3);\n\tconst auto ok = ok1 && ok2 && ok3;\n\treturn (ok && value3 < 60 && (hours.isEmpty() || value2 < 60))\n\t\t? (value1 * 3600 + value2 * 60 + value3)\n\t\t: -1;\n}\n\n} // namespace\n\nTimeId DurationForTimestampLinks(not_null<DocumentData*> document) {\n\tif (!document->isVideoFile()\n\t\t&& !document->isSong()\n\t\t&& !document->isVoiceMessage()) {\n\t\treturn TimeId(0);\n\t}\n\treturn std::max(document->duration(), crl::time(0)) / 1000;\n}\n\nQString TimestampLinkBase(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context) {\n\treturn QString(\n\t\t\"media_timestamp?base=doc%1_%2_%3&t=\"\n\t).arg(document->id).arg(context.peer.value).arg(context.msg.bare);\n}\n\nTimeId DurationForTimestampLinks(not_null<WebPageData*> webpage) {\n\tif (!webpage->collage.items.empty()) {\n\t\treturn 0;\n\t} else if (const auto document = webpage->document) {\n\t\treturn DurationForTimestampLinks(document);\n\t} else if (webpage->type != WebPageType::Video\n\t\t|| webpage->siteName != u\"YouTube\"_q) {\n\t\treturn TimeId(0);\n\t} else if (webpage->duration > 0) {\n\t\treturn webpage->duration;\n\t}\n\tconstexpr auto kMaxYouTubeTimestampDuration = 100 * 60 * TimeId(60);\n\treturn kMaxYouTubeTimestampDuration;\n}\n\nQString TimestampLinkBase(\n\t\tnot_null<WebPageData*> webpage,\n\t\tFullMsgId context) {\n\tconst auto url = webpage->url;\n\tif (url.isEmpty()) {\n\t\treturn QString();\n\t}\n\tauto parts = url.split(QChar('#'));\n\tconst auto base = parts[0];\n\tparts.pop_front();\n\tconst auto use = [&] {\n\t\tconst auto query = base.indexOf(QChar('?'));\n\t\tif (query < 0) {\n\t\t\treturn base + QChar('?');\n\t\t}\n\t\tauto params = base.mid(query + 1).split(QChar('&'));\n\t\tfor (auto i = params.begin(); i != params.end();) {\n\t\t\tif (i->startsWith(\"t=\")) {\n\t\t\t\ti = params.erase(i);\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t\treturn base.mid(0, query)\n\t\t\t+ (params.empty() ? \"?\" : (\"?\" + params.join(QChar('&')) + \"&\"));\n\t}();\n\treturn \"url:\"\n\t\t+ use\n\t\t+ \"t=\"\n\t\t+ (parts.empty() ? QString() : (\"#\" + parts.join(QChar('#'))));\n}\n\nTextWithEntities AddTimestampLinks(\n\t\tTextWithEntities text,\n\t\tTimeId duration,\n\t\tconst QString &base) {\n\tif (base.isEmpty()) {\n\t\treturn text;\n\t}\n\tstatic const auto expression = QRegularExpression(\n\t\t\"(?<![^\\\\s\\\\(\\\\)\\\\[\\\\]\\\"\\\\,\\\\.\\\\-])\"\n\t\t\"(?:(?:(\\\\d{1,2}):)?(\\\\d))?(\\\\d):(\\\\d\\\\d)\"\n\t\t\"(?![^\\\\s\\\\(\\\\)\\\\[\\\\]\\\",\\\\.\\\\-\\\\+])\");\n\tconst auto &string = text.text;\n\tauto offset = 0;\n\twhile (true) {\n\t\tconst auto m = expression.match(string, offset);\n\t\tif (!m.hasMatch()) {\n\t\t\tbreak;\n\t\t}\n\n\t\tconst auto from = m.capturedStart();\n\t\tconst auto till = from + m.capturedLength();\n\t\toffset = till;\n\n\t\tconst auto time = TimeFromMatch(\n\t\t\tm.capturedView(1),\n\t\t\tm.capturedView(2),\n\t\t\tm.capturedView(3),\n\t\t\tm.capturedView(4));\n\t\tif (time < 0 || time > duration) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto &entities = text.entities;\n\t\tauto i = ranges::lower_bound(\n\t\t\tentities,\n\t\t\tfrom,\n\t\t\tstd::less<>(),\n\t\t\t&EntityInText::offset);\n\t\twhile (i != entities.end()\n\t\t\t&& i->offset() < till\n\t\t\t&& i->type() == EntityType::Spoiler) {\n\t\t\t++i;\n\t\t}\n\t\tif (i != entities.end() && i->offset() < till) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto intersects = [&](const EntityInText &entity) {\n\t\t\treturn (entity.offset() + entity.length() > from)\n\t\t\t\t&& (entity.type() != EntityType::Spoiler);\n\t\t};\n\t\tauto j = std::make_reverse_iterator(i);\n\t\tconst auto e = std::make_reverse_iterator(entities.begin());\n\t\tif (std::find_if(j, e, intersects) != e) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tentities.insert(\n\t\t\ti,\n\t\t\tEntityInText(\n\t\t\t\tEntityType::CustomUrl,\n\t\t\t\tfrom,\n\t\t\t\ttill - from,\n\t\t\t\t(\"internal:\" + base + QString::number(time))));\n\t}\n\treturn text;\n}\n\nStorage::SharedMediaTypesMask Media::sharedMediaTypes() const {\n\treturn {};\n}\n\nbool Media::allowTextSelectionByHandler(\n\t\tconst ClickHandlerPtr &handler) const {\n\treturn false;\n}\n\nnot_null<Element*> Media::parent() const {\n\treturn _parent;\n}\n\nnot_null<History*> Media::history() const {\n\treturn _parent->history();\n}\n\nSelectedQuote Media::selectedQuote(TextSelection selection) const {\n\treturn {};\n}\n\nQSize Media::countCurrentSize(int newWidth) {\n\treturn QSize(qMin(newWidth, maxWidth()), minHeight());\n}\n\nbool Media::hasPurchasedTag() const {\n\tif (const auto media = parent()->data()->media()) {\n\t\tif (const auto invoice = media->invoice()) {\n\t\t\tif (invoice->isPaidMedia && !invoice->extendedMedia.empty()) {\n\t\t\t\tconst auto photo = invoice->extendedMedia.front()->photo();\n\t\t\t\treturn !photo || !photo->extendedMediaPreview();\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Media::drawPurchasedTag(\n\t\tPainter &p,\n\t\tQRect outer,\n\t\tconst PaintContext &context) const {\n\tconst auto purchased = parent()->enforcePurchasedTag();\n\tif (purchased->text.isEmpty()) {\n\t\tconst auto item = parent()->data();\n\t\tconst auto media = item->media();\n\t\tconst auto invoice = media ? media->invoice() : nullptr;\n\t\tconst auto amount = invoice ? invoice->amount : 0;\n\t\tif (!amount) {\n\t\t\treturn;\n\t\t}\n\t\tauto text = Ui::Text::Colorized(Ui::CreditsEmojiSmall());\n\t\ttext.append(Lang::FormatCountDecimal(amount));\n\t\tpurchased->text.setMarkedText(\n\t\t\tst::defaultTextStyle,\n\t\t\ttext,\n\t\t\tkMarkupTextOptions);\n\t}\n\n\tconst auto st = context.st;\n\tconst auto sti = context.imageStyle();\n\tconst auto &padding = st::purchasedTagPadding;\n\tauto right = outer.x() + outer.width();\n\tauto top = outer.y();\n\tright -= st::msgDateImgDelta + padding.right();\n\ttop += st::msgDateImgDelta + padding.top();\n\n\tconst auto size = QSize(\n\t\tpurchased->text.maxWidth(),\n\t\tst::normalFont->height);\n\tconst auto tagX = right - size.width();\n\tconst auto tagY = top;\n\tconst auto tagW = padding.left() + size.width() + padding.right();\n\tconst auto tagH = padding.top() + size.height() + padding.bottom();\n\tUi::FillRoundRect(\n\t\tp,\n\t\ttagX - padding.left(),\n\t\ttagY - padding.top(),\n\t\ttagW,\n\t\ttagH,\n\t\tsti->msgDateImgBg,\n\t\tsti->msgDateImgBgCorners);\n\n\tp.setPen(st->msgDateImgFg());\n\tpurchased->text.draw(p, {\n\t\t.position = { tagX, tagY },\n\t\t.outerWidth = width(),\n\t\t.availableWidth = size.width(),\n\t\t.palette = &st->priceTagTextPalette(),\n\t});\n}\n\nvoid Media::fillImageShadow(\n\t\tQPainter &p,\n\t\tQRect rect,\n\t\tUi::BubbleRounding rounding,\n\t\tconst PaintContext &context) const {\n\tconst auto sti = context.imageStyle();\n\tauto corners = Ui::CornersPixmaps();\n\tconst auto choose = [&](int index) -> QPixmap {\n\t\tusing Corner = Ui::BubbleCornerRounding;\n\t\tswitch (rounding[index]) {\n\t\tcase Corner::Large: return sti->msgShadowCornersLarge.p[index];\n\t\tcase Corner::Small: return sti->msgShadowCornersSmall.p[index];\n\t\t}\n\t\treturn QPixmap();\n\t};\n\tcorners.p[2] = choose(2);\n\tcorners.p[3] = choose(3);\n\tUi::FillRoundShadow(p, rect, sti->msgShadow, corners);\n}\n\nvoid Media::fillImageOverlay(\n\t\tQPainter &p,\n\t\tQRect rect,\n\t\tstd::optional<Ui::BubbleRounding> rounding,\n\t\tconst PaintContext &context) const {\n\tusing Radius = Ui::CachedCornerRadius;\n\tconst auto &st = context.st;\n\tif (!rounding) {\n\t\tUi::FillComplexOverlayRect(\n\t\t\tp,\n\t\t\trect,\n\t\t\tst->msgSelectOverlay(),\n\t\t\tst->msgSelectOverlayCorners(Radius::Small));\n\t\treturn;\n\t}\n\tusing Corner = Ui::BubbleCornerRounding;\n\tauto corners = Ui::CornersPixmaps();\n\tconst auto lookup = [&](Corner corner) {\n\t\tswitch (corner) {\n\t\tcase Corner::None: return Radius::kCount;\n\t\tcase Corner::Small: return Radius::BubbleSmall;\n\t\tcase Corner::Large: return Radius::BubbleLarge;\n\t\t}\n\t\tUnexpected(\"Corner value in Document::fillThumbnailOverlay.\");\n\t};\n\tfor (auto i = 0; i != 4; ++i) {\n\t\tconst auto radius = lookup((*rounding)[i]);\n\t\tcorners.p[i] = (radius == Radius::kCount)\n\t\t\t? QPixmap()\n\t\t\t: st->msgSelectOverlayCorners(radius).p[i];\n\t}\n\tUi::FillComplexOverlayRect(p, rect, st->msgSelectOverlay(), corners);\n}\n\nvoid Media::fillImageSpoiler(\n\t\tQPainter &p,\n\t\tnot_null<MediaSpoiler*> spoiler,\n\t\tQRect rect,\n\t\tconst PaintContext &context) const {\n\tif (!spoiler->animation) {\n\t\tspoiler->animation = std::make_unique<Ui::SpoilerAnimation>([=] {\n\t\t\t_parent->customEmojiRepaint();\n\t\t});\n\t\thistory()->owner().registerHeavyViewPart(_parent);\n\t}\n\t_parent->clearCustomEmojiRepaint();\n\tconst auto pausedSpoiler = context.paused\n\t\t|| On(PowerSaving::kChatSpoiler);\n\tUi::FillSpoilerRect(\n\t\tp,\n\t\trect,\n\t\tMediaRoundingMask(spoiler->backgroundRounding),\n\t\tUi::DefaultImageSpoiler().frame(\n\t\t\tspoiler->animation->index(context.now, pausedSpoiler)),\n\t\tspoiler->cornerCache);\n}\n\nvoid Media::drawSpoilerTag(\n\t\tPainter &p,\n\t\tnot_null<MediaSpoiler*> spoiler,\n\t\tstd::unique_ptr<MediaSpoilerTag> &tag,\n\t\tQRect rthumb,\n\t\tconst PaintContext &context,\n\t\tFn<QImage()> generateBackground) const {\n\tif (!tag) {\n\t\tsetupSpoilerTag(tag);\n\t\tif (!tag) {\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto revealed = spoiler->revealAnimation.value(\n\t\tspoiler->revealed ? 1. : 0.);\n\tif (revealed == 1.) {\n\t\treturn;\n\t}\n\tp.setOpacity(1. - revealed);\n\tconst auto st = context.st;\n\tconst auto darken = st->msgDateImgBg()->c;\n\tconst auto fg = st->msgDateImgFg()->c;\n\tconst auto star = st->creditsBg1()->c;\n\tif (tag->cache.isNull()\n\t\t|| tag->darken != darken\n\t\t|| tag->fg != fg\n\t\t|| tag->star != star) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tauto bg = generateBackground();\n\t\tif (bg.isNull()) {\n\t\t\tbg = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied);\n\t\t\tbg.fill(Qt::black);\n\t\t}\n\n\t\tauto text = Ui::Text::String();\n\t\tauto iconSkip = 0;\n\t\tif (tag->sensitive) {\n\t\t\ttext.setText(\n\t\t\t\tst::semiboldTextStyle,\n\t\t\t\ttr::lng_sensitive_tag(tr::now));\n\t\t\ticonSkip = st::mediaMenuIconStealth.width() * 1.4;\n\t\t} else {\n\t\t\tauto price = Ui::Text::Colorized(Ui::CreditsEmoji());\n\t\t\tprice.append(Lang::FormatCountDecimal(tag->price));\n\t\t\ttext.setMarkedText(\n\t\t\t\tst::semiboldTextStyle,\n\t\t\t\ttr::lng_paid_price(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_price,\n\t\t\t\t\tprice,\n\t\t\t\t\ttr::marked),\n\t\t\t\tkMarkupTextOptions);\n\t\t}\n\t\tconst auto width = iconSkip + text.maxWidth();\n\t\tconst auto inner = QRect(0, 0, width, text.minHeight());\n\t\tconst auto outer = inner.marginsAdded(st::paidTagPadding);\n\t\tconst auto size = outer.size();\n\t\tconst auto radius = std::min(size.width(), size.height()) / 2;\n\t\tauto cache = QImage(\n\t\t\tsize * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tcache.setDevicePixelRatio(ratio);\n\t\tcache.fill(Qt::black);\n\t\tauto p = Painter(&cache);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.drawImage(\n\t\t\tQRect(\n\t\t\t\t(size.width() - rthumb.width()) / 2,\n\t\t\t\t(size.height() - rthumb.height()) / 2,\n\t\t\t\trthumb.width(),\n\t\t\t\trthumb.height()),\n\t\t\tbg);\n\t\tp.fillRect(QRect(QPoint(), size), darken);\n\t\tp.setPen(fg);\n\t\tp.setTextPalette(st->priceTagTextPalette());\n\t\tif (iconSkip) {\n\t\t\tst::mediaMenuIconStealth.paint(\n\t\t\t\tp,\n\t\t\t\t-outer.x(),\n\t\t\t\t(size.height() - st::mediaMenuIconStealth.height()) / 2,\n\t\t\t\tsize.width(),\n\t\t\t\tfg);\n\t\t}\n\t\ttext.draw(p, iconSkip - outer.x(), -outer.y(), width);\n\t\tp.end();\n\n\t\ttag->darken = darken;\n\t\ttag->fg = fg;\n\t\ttag->cache = Images::Round(\n\t\t\tstd::move(cache),\n\t\t\tImages::CornersMask(radius));\n\t}\n\tconst auto &cache = tag->cache;\n\tconst auto size = cache.size() / cache.devicePixelRatio();\n\tconst auto left = rthumb.x() + (rthumb.width() - size.width()) / 2;\n\tconst auto top = rthumb.y() + (rthumb.height() - size.height()) / 2;\n\tp.drawImage(left, top, cache);\n\tif (context.selected()) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto radius = std::min(size.width(), size.height()) / 2;\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st->msgSelectOverlay());\n\t\tp.drawRoundedRect(\n\t\t\tQRect(left, top, size.width(), size.height()),\n\t\t\tradius,\n\t\t\tradius);\n\t}\n\tp.setOpacity(1.);\n}\n\nvoid Media::setupSpoilerTag(std::unique_ptr<MediaSpoilerTag> &tag) const {\n\tconst auto item = parent()->data();\n\tif (item->isMediaSensitive()) {\n\t\ttag = std::make_unique<MediaSpoilerTag>();\n\t\ttag->sensitive = 1;\n\t\treturn;\n\t}\n\tconst auto media = parent()->data()->media();\n\tconst auto invoice = media ? media->invoice() : nullptr;\n\tif (const auto price = invoice->isPaidMedia ? invoice->amount : 0) {\n\t\ttag = std::make_unique<MediaSpoilerTag>();\n\t\ttag->price = price;\n\t}\n}\n\nClickHandlerPtr Media::spoilerTagLink(\n\t\tnot_null<MediaSpoiler*> spoiler,\n\t\tstd::unique_ptr<MediaSpoilerTag> &tag) const {\n\tconst auto item = parent()->data();\n\tif (!item->isRegular() || spoiler->revealed) {\n\t\treturn nullptr;\n\t} else if (!tag) {\n\t\tsetupSpoilerTag(tag);\n\t\tif (!tag) {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\tif (!tag->link) {\n\t\ttag->link = tag->sensitive\n\t\t\t? MakeSensitiveMediaLink(spoiler->link, item)\n\t\t\t: MakePaidMediaLink(item);\n\t}\n\treturn tag->link;\n}\n\nvoid Media::createSpoilerLink(not_null<MediaSpoiler*> spoiler) {\n\tconst auto weak = base::make_weak(this);\n\tspoiler->link = std::make_shared<LambdaClickHandler>([weak, spoiler](\n\t\t\tconst ClickContext &context) {\n\t\tconst auto button = context.button;\n\t\tconst auto media = weak.get();\n\t\tif (button != Qt::LeftButton || !media || spoiler->revealed) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto view = media->parent();\n\t\tspoiler->revealed = true;\n\t\tspoiler->revealAnimation.start([=] {\n\t\t\tview->repaint();\n\t\t}, 0., 1., st::fadeWrapDuration);\n\t\tview->repaint();\n\t\tmedia->history()->owner().registerShownSpoiler(view);\n\t});\n}\n\nvoid Media::repaint() const {\n\t_parent->repaint();\n}\n\nUi::Text::String Media::createCaption(not_null<HistoryItem*> item) const {\n\tif (item->emptyText()) {\n\t\treturn {};\n\t}\n\tconst auto minResizeWidth = st::minPhotoSize\n\t\t- st::msgPadding.left()\n\t\t- st::msgPadding.right();\n\tauto result = Ui::Text::String(minResizeWidth);\n\tconst auto context = Core::TextContext({\n\t\t.session = &history()->session(),\n\t\t.repaint = [=] { _parent->customEmojiRepaint(); },\n\t});\n\tresult.setMarkedText(\n\t\tst::messageTextStyle,\n\t\titem->translatedTextWithLocalEntities(),\n\t\tUi::ItemTextOptions(item),\n\t\tcontext);\n\tInitElementTextPart(_parent, result);\n\tif (const auto width = _parent->skipBlockWidth()) {\n\t\tresult.updateSkipBlock(width, _parent->skipBlockHeight());\n\t}\n\treturn result;\n}\n\nTextSelection Media::skipSelection(TextSelection selection) const {\n\treturn UnshiftItemSelection(selection, fullSelectionLength());\n}\n\nTextSelection Media::unskipSelection(TextSelection selection) const {\n\treturn ShiftItemSelection(selection, fullSelectionLength());\n}\n\nauto Media::getBubbleSelectionIntervals(\n\tTextSelection selection) const\n-> std::vector<Ui::BubbleSelectionInterval> {\n\treturn {};\n}\n\nbool Media::usesBubblePattern(const PaintContext &context) const {\n\treturn _parent->usesBubblePattern(context);\n}\n\nPointState Media::pointState(QPoint point) const {\n\treturn QRect(0, 0, width(), height()).contains(point)\n\t\t? PointState::Inside\n\t\t: PointState::Outside;\n}\n\nstd::unique_ptr<StickerPlayer> Media::stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) {\n\treturn nullptr;\n}\n\nQImage Media::locationTakeImage() {\n\treturn QImage();\n}\n\nstd::vector<Media::TodoTaskInfo> Media::takeTasksInfo() {\n\treturn {};\n}\n\nTextState Media::getStateGrouped(\n\t\tconst QRect &geometry,\n\t\tRectParts sides,\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tUnexpected(\"Grouping method call.\");\n}\n\nUi::BubbleRounding Media::adjustedBubbleRounding(RectParts square) const {\n\tauto result = bubbleRounding();\n\tusing Corner = Ui::BubbleCornerRounding;\n\tconst auto adjust = [&](bool round, Corner already, RectPart corner) {\n\t\treturn (already == Corner::Tail || !round || (square & corner))\n\t\t\t? Corner::None\n\t\t\t: already;\n\t};\n\tconst auto top = isBubbleTop();\n\tconst auto bottom = isRoundedInBubbleBottom();\n\tresult.topLeft = adjust(top, result.topLeft, RectPart::TopLeft);\n\tresult.topRight = adjust(top, result.topRight, RectPart::TopRight);\n\tresult.bottomLeft = adjust(\n\t\tbottom,\n\t\tresult.bottomLeft,\n\t\tRectPart::BottomLeft);\n\tresult.bottomRight = adjust(\n\t\tbottom,\n\t\tresult.bottomRight,\n\t\tRectPart::BottomRight);\n\treturn result;\n}\n\nHistoryItem *Media::itemForText() const {\n\treturn _parent->data();\n}\n\nbool Media::isRoundedInBubbleBottom() const {\n\treturn isBubbleBottom()\n\t\t&& !_parent->data()->repliesAreComments()\n\t\t&& !_parent->data()->externalReply();\n}\n\nImages::CornersMaskRef MediaRoundingMask(\n\t\tstd::optional<Ui::BubbleRounding> rounding) {\n\tusing Radius = Ui::CachedCornerRadius;\n\tif (!rounding) {\n\t\treturn Images::CornersMaskRef(Ui::CachedCornersMasks(Radius::Small));\n\t}\n\tusing Corner = Ui::BubbleCornerRounding;\n\tauto result = Images::CornersMaskRef();\n\tconst auto &small = Ui::CachedCornersMasks(Radius::BubbleSmall);\n\tconst auto &large = Ui::CachedCornersMasks(Radius::BubbleLarge);\n\tfor (auto i = 0; i != 4; ++i) {\n\t\tswitch ((*rounding)[i]) {\n\t\tcase Corner::Small: result.p[i] = &small[i]; break;\n\t\tcase Corner::Large: result.p[i] = &large[i]; break;\n\t\t}\n\t}\n\treturn result;\n\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_media.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/history_view_object.h\"\n#include \"ui/chat/message_bubble.h\"\n#include \"ui/rect_part.h\"\n\nclass History;\nstruct HistoryMessageEdited;\nstruct TextSelection;\n\nnamespace base {\ntemplate <typename Enum>\nclass enum_mask;\n} // namespace base\n\nnamespace Storage {\nenum class SharedMediaType : signed char;\nusing SharedMediaTypesMask = base::enum_mask<SharedMediaType>;\n} // namespace Storage\n\nnamespace Lottie {\nstruct ColorReplacements;\n} // namespace Lottie\n\nnamespace Ui {\nstruct BubbleSelectionInterval;\nstruct ChatPaintContext;\nclass SpoilerAnimation;\n} // namespace Ui\n\nnamespace Images {\nstruct CornersMaskRef;\n} // namespace Images\n\nnamespace HistoryView {\n\nenum class PointState : char;\nenum class CursorState : char;\nenum class InfoDisplayType : char;\nstruct TextState;\nstruct StateRequest;\nstruct MediaSpoiler;\nstruct MediaSpoilerTag;\nclass StickerPlayer;\nclass Element;\nstruct SelectedQuote;\n\nusing PaintContext = Ui::ChatPaintContext;\n\nenum class MediaInBubbleState : uchar {\n\tNone,\n\tTop,\n\tMiddle,\n\tBottom,\n};\n\n[[nodiscard]] TimeId DurationForTimestampLinks(\n\tnot_null<DocumentData*> document);\n[[nodiscard]] QString TimestampLinkBase(\n\tnot_null<DocumentData*> document,\n\tFullMsgId context);\n\n[[nodiscard]] TimeId DurationForTimestampLinks(\n\tnot_null<WebPageData*> webpage);\n[[nodiscard]] QString TimestampLinkBase(\n\tnot_null<WebPageData*> webpage,\n\tFullMsgId context);\n\n[[nodiscard]] TextWithEntities AddTimestampLinks(\n\tTextWithEntities text,\n\tTimeId duration,\n\tconst QString &base);\n\nstruct PaidInformation {\n\tint messages = 0;\n\tint stars = 0;\n\n\texplicit operator bool() const {\n\t\treturn stars != 0;\n\t}\n};\n\nclass Media : public Object, public base::has_weak_ptr {\npublic:\n\texplicit Media(not_null<Element*> parent) : _parent(parent) {\n\t}\n\n\t[[nodiscard]] not_null<Element*> parent() const;\n\t[[nodiscard]] not_null<History*> history() const;\n\n\t[[nodiscard]] virtual TextForMimeData selectedText(\n\t\t\tTextSelection selection) const {\n\t\treturn {};\n\t}\n\t[[nodiscard]] virtual SelectedQuote selectedQuote(\n\t\tTextSelection selection) const;\n\t[[nodiscard]] virtual TextSelection selectionFromQuote(\n\t\t\tconst SelectedQuote &quote) const {\n\t\treturn {};\n\t}\n\n\t[[nodiscard]] virtual bool isDisplayed() const {\n\t\treturn true;\n\t}\n\tvirtual void updateNeedBubbleState() {\n\t}\n\t[[nodiscard]] virtual bool hasTextForCopy() const {\n\t\treturn false;\n\t}\n\t[[nodiscard]] virtual bool aboveTextByDefault() const {\n\t\treturn true;\n\t}\n\t[[nodiscard]] virtual HistoryItem *itemForText() const;\n\t[[nodiscard]] virtual bool hideMessageText() const {\n\t\treturn true;\n\t}\n\t[[nodiscard]] virtual bool hideServiceText() const {\n\t\treturn false;\n\t}\n\t[[nodiscard]] virtual bool hideFromName() const {\n\t\treturn false;\n\t}\n\t[[nodiscard]] virtual bool allowsFastShare() const {\n\t\treturn false;\n\t}\n\t[[nodiscard]] virtual auto paidInformation() const\n\t-> std::optional<PaidInformation> {\n\t\treturn {};\n\t}\n\tvirtual void refreshParentId(not_null<HistoryItem*> realParent) {\n\t}\n\tvirtual void drawHighlight(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint top) const {\n\t}\n\t[[nodiscard]] virtual QRect groupItemRect(int index) const {\n\t\treturn {};\n\t}\n\tvirtual void draw(Painter &p, const PaintContext &context) const = 0;\n\t[[nodiscard]] virtual PointState pointState(QPoint point) const;\n\t[[nodiscard]] virtual TextState textState(\n\t\tQPoint point,\n\t\tStateRequest request) const = 0;\n\tvirtual void updatePressed(QPoint point) {\n\t}\n\n\t[[nodiscard]] virtual Storage::SharedMediaTypesMask sharedMediaTypes() const;\n\n\t// if we are in selecting items mode perhaps we want to\n\t// toggle selection instead of activating the pressed link\n\t[[nodiscard]] virtual bool toggleSelectionByHandlerClick(\n\t\tconst ClickHandlerPtr &p) const = 0;\n\t[[nodiscard]] virtual bool allowTextSelectionByHandler(\n\t\tconst ClickHandlerPtr &p) const;\n\n\t[[nodiscard]] virtual TextSelection adjustSelection(\n\t\t\tTextSelection selection,\n\t\t\tTextSelectType type) const {\n\t\treturn selection;\n\t}\n\t[[nodiscard]] virtual uint16 fullSelectionLength() const {\n\t\treturn 0;\n\t}\n\t[[nodiscard]] TextSelection skipSelection(\n\t\tTextSelection selection) const;\n\t[[nodiscard]] TextSelection unskipSelection(\n\t\tTextSelection selection) const;\n\n\t[[nodiscard]] virtual auto getBubbleSelectionIntervals(\n\t\tTextSelection selection) const\n\t-> std::vector<Ui::BubbleSelectionInterval>;\n\n\t// if we press and drag this link should we drag the item\n\t[[nodiscard]] virtual bool dragItemByHandler(\n\t\tconst ClickHandlerPtr &p) const = 0;\n\n\tvirtual void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {\n\t}\n\tvirtual void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {\n\t}\n\n\t[[nodiscard]] virtual QRect addOptionRect(int innerWidth) const {\n\t\treturn {};\n\t}\n\tvirtual void setAddOptionActive(bool active) {\n\t}\n\n\t[[nodiscard]] virtual bool uploading() const {\n\t\treturn false;\n\t}\n\n\t[[nodiscard]] virtual PhotoData *getPhoto() const {\n\t\treturn nullptr;\n\t}\n\t[[nodiscard]] virtual DocumentData *getDocument() const {\n\t\treturn nullptr;\n\t}\n\n\tvoid playAnimation() {\n\t\tplayAnimation(false);\n\t}\n\tvoid autoplayAnimation() {\n\t\tplayAnimation(true);\n\t}\n\tvirtual void stopAnimation() {\n\t}\n\tvirtual void stickerClearLoopPlayed() {\n\t}\n\tvirtual std::unique_ptr<StickerPlayer> stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements);\n\tvirtual QImage locationTakeImage();\n\n\tstruct TodoTaskInfo {\n\t\tint id = 0;\n\t\tPeerData *completedBy = nullptr;\n\t\tTimeId completionDate = TimeId();\n\t};\n\tvirtual std::vector<TodoTaskInfo> takeTasksInfo();\n\n\tvirtual void checkAnimation() {\n\t}\n\n\t[[nodiscard]] virtual QSize sizeForGroupingOptimal(\n\t\t\tint maxWidth,\n\t\t\tbool last) const {\n\t\tUnexpected(\"Grouping method call.\");\n\t}\n\t[[nodiscard]] virtual QSize sizeForGrouping(int width) const {\n\t\tUnexpected(\"Grouping method call.\");\n\t}\n\tvirtual void drawGrouped(\n\t\t\tPainter &p,\n\t\t\tconst PaintContext &context,\n\t\t\tconst QRect &geometry,\n\t\t\tRectParts sides,\n\t\t\tUi::BubbleRounding rounding,\n\t\t\tfloat64 highlightOpacity,\n\t\t\tnot_null<uint64*> cacheKey,\n\t\t\tnot_null<QPixmap*> cache) const {\n\t\tUnexpected(\"Grouping method call.\");\n\t}\n\t[[nodiscard]] virtual TextState getStateGrouped(\n\t\tconst QRect &geometry,\n\t\tRectParts sides,\n\t\tQPoint point,\n\t\tStateRequest request) const;\n\n\tvirtual void drawSpoilerTag(\n\t\t\tPainter &p,\n\t\t\tQRect rthumb,\n\t\t\tconst PaintContext &context,\n\t\t\tFn<QImage()> generateBackground) const {\n\t\tUnexpected(\"Spoiler tag method call.\");\n\t}\n\t[[nodiscard]] virtual ClickHandlerPtr spoilerTagLink() const {\n\t\tUnexpected(\"Spoiler tag method call.\");\n\t}\n\t[[nodiscard]] virtual QImage spoilerTagBackground() const {\n\t\tUnexpected(\"Spoiler tag method call.\");\n\t}\n\n\t[[nodiscard]] virtual bool animating() const {\n\t\treturn false;\n\t}\n\n\tvirtual void hideSpoilers() {\n\t}\n\t[[nodiscard]] virtual bool needsBubble() const = 0;\n\t[[nodiscard]] virtual bool unwrapped() const {\n\t\treturn false;\n\t}\n\t[[nodiscard]] virtual bool customInfoLayout() const = 0;\n\t[[nodiscard]] virtual QRect contentRectForReactions() const {\n\t\treturn QRect(0, 0, width(), height());\n\t}\n\t[[nodiscard]] virtual auto reactionButtonCenterOverride() const\n\t-> std::optional<int> {\n\t\treturn std::nullopt;\n\t}\n\t[[nodiscard]] virtual QPoint resolveCustomInfoRightBottom() const {\n\t\treturn QPoint();\n\t}\n\t[[nodiscard]] virtual QMargins bubbleMargins() const {\n\t\treturn QMargins();\n\t}\n\n\t[[nodiscard]] virtual bool overrideEditedDate() const {\n\t\treturn false;\n\t}\n\t[[nodiscard]] virtual HistoryMessageEdited *displayedEditBadge() const {\n\t\tUnexpected(\"displayedEditBadge() on non-grouped media.\");\n\t}\n\n\t// An attach media in a web page can provide an\n\t// additional text to be displayed below the attach.\n\t// For example duration / progress for video messages.\n\t[[nodiscard]] virtual QString additionalInfoString() const {\n\t\treturn QString();\n\t}\n\n\tvoid setInBubbleState(MediaInBubbleState state) {\n\t\t_inBubbleState = state;\n\t}\n\t[[nodiscard]] MediaInBubbleState inBubbleState() const {\n\t\treturn _inBubbleState;\n\t}\n\tvoid setBubbleRounding(Ui::BubbleRounding rounding) {\n\t\t_bubbleRounding = rounding;\n\t}\n\t[[nodiscard]] Ui::BubbleRounding bubbleRounding() const {\n\t\treturn _bubbleRounding;\n\t}\n\t[[nodiscard]] Ui::BubbleRounding adjustedBubbleRounding(\n\t\tRectParts square = {}) const;\n\t[[nodiscard]] bool isBubbleTop() const {\n\t\treturn (_inBubbleState == MediaInBubbleState::Top)\n\t\t\t|| (_inBubbleState == MediaInBubbleState::None);\n\t}\n\t[[nodiscard]] bool isBubbleBottom() const {\n\t\treturn (_inBubbleState == MediaInBubbleState::Bottom)\n\t\t\t|| (_inBubbleState == MediaInBubbleState::None);\n\t}\n\t[[nodiscard]] bool isRoundedInBubbleBottom() const;\n\t[[nodiscard]] virtual bool skipBubbleTail() const {\n\t\treturn false;\n\t}\n\n\t// Sometimes webpages can force the bubble to fit their size instead of\n\t// allowing message text to be as wide as possible (like wallpapers).\n\t[[nodiscard]] virtual bool enforceBubbleWidth() const {\n\t\treturn false;\n\t}\n\t[[nodiscard]] virtual bool allowsNarrowBubble() const {\n\t\treturn false;\n\t}\n\t[[nodiscard]] virtual int minBubbleWidthForNarrowBubble() const {\n\t\treturn 0;\n\t}\n\n\t// Sometimes click on media in message is overloaded by the message:\n\t// (for example it can open a link or a game instead of opening media)\n\t// But the overloading click handler should be used only when media\n\t// is already loaded (not a photo or GIF waiting for load with auto\n\t// load being disabled - in such case media should handle the click).\n\t[[nodiscard]] virtual bool isReadyForOpen() const {\n\t\treturn true;\n\t}\n\n\tstruct BubbleRoll {\n\t\tfloat64 rotate = 0.;\n\t\tfloat64 scale = 1.;\n\n\t\texplicit operator bool() const {\n\t\t\treturn (rotate != 0.) || (scale != 1.);\n\t\t}\n\t};\n\t[[nodiscard]] virtual BubbleRoll bubbleRoll() const {\n\t\treturn BubbleRoll();\n\t}\n\t[[nodiscard]] virtual QMargins bubbleRollRepaintMargins() const {\n\t\treturn QMargins();\n\t}\n\tvirtual bool updateItemData() {\n\t\treturn false;\n\t}\n\tvirtual void paintBubbleFireworks(\n\t\tPainter &p,\n\t\tconst QRect &bubble,\n\t\tcrl::time ms) const {\n\t}\n\t[[nodiscard]] virtual bool customHighlight() const {\n\t\treturn false;\n\t}\n\n\tvirtual bool hasHeavyPart() const {\n\t\treturn false;\n\t}\n\tvirtual void unloadHeavyPart() {\n\t}\n\n\t// Should be called only by Data::Session.\n\tvirtual void updateSharedContactUserId(UserId userId) {\n\t}\n\tvirtual void parentTextUpdated() {\n\t}\n\n\tvirtual bool consumeHorizontalScroll(QPoint position, int delta) {\n\t\treturn false;\n\t}\n\n\t[[nodiscard]] bool hasPurchasedTag() const;\n\tvoid drawPurchasedTag(\n\t\tPainter &p,\n\t\tQRect outer,\n\t\tconst PaintContext &context) const;\n\n\tvirtual ~Media() = default;\n\nprotected:\n\t[[nodiscard]] QSize countCurrentSize(int newWidth) override;\n\t[[nodiscard]] Ui::Text::String createCaption(\n\t\tnot_null<HistoryItem*> item) const;\n\n\tvirtual void playAnimation(bool autoplay) {\n\t}\n\n\t[[nodiscard]] bool usesBubblePattern(const PaintContext &context) const;\n\n\tvoid fillImageShadow(\n\t\tQPainter &p,\n\t\tQRect rect,\n\t\tUi::BubbleRounding rounding,\n\t\tconst PaintContext &context) const;\n\tvoid fillImageOverlay(\n\t\tQPainter &p,\n\t\tQRect rect,\n\t\tstd::optional<Ui::BubbleRounding> rounding, // nullopt if in WebPage.\n\t\tconst PaintContext &context) const;\n\tvoid fillImageSpoiler(\n\t\tQPainter &p,\n\t\tnot_null<MediaSpoiler*> spoiler,\n\t\tQRect rect,\n\t\tconst PaintContext &context) const;\n\tvoid drawSpoilerTag(\n\t\tPainter &p,\n\t\tnot_null<MediaSpoiler*> spoiler,\n\t\tstd::unique_ptr<MediaSpoilerTag> &tag,\n\t\tQRect rthumb,\n\t\tconst PaintContext &context,\n\t\tFn<QImage()> generateBackground) const;\n\tvoid setupSpoilerTag(std::unique_ptr<MediaSpoilerTag> &tag) const;\n\t[[nodiscard]] ClickHandlerPtr spoilerTagLink(\n\t\tnot_null<MediaSpoiler*> spoiler,\n\t\tstd::unique_ptr<MediaSpoilerTag> &tag) const;\n\tvoid createSpoilerLink(not_null<MediaSpoiler*> spoiler);\n\n\tvoid repaint() const;\n\n\tconst not_null<Element*> _parent;\n\tMediaInBubbleState _inBubbleState = MediaInBubbleState::None;\n\tUi::BubbleRounding _bubbleRounding;\n\n};\n\n[[nodiscard]] Images::CornersMaskRef MediaRoundingMask(\n\tstd::optional<Ui::BubbleRounding> rounding);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_media_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_media_common.h\"\n\n#include \"api/api_sensitive_content.h\"\n#include \"api/api_views.h\"\n#include \"apiwrap.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/painter.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_wall_paper.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_user.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/media/history_view_media_grouped.h\"\n#include \"history/view/media/history_view_photo.h\"\n#include \"history/view/media/history_view_gif.h\"\n#include \"history/view/media/history_view_document.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"history/view/media/history_view_theme_document.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"mainwindow.h\"\n#include \"media/streaming/media_streaming_utility.h\"\n#include \"payments/payments_checkout_process.h\"\n#include \"payments/payments_non_panel_process.h\"\n#include \"settings/settings_common.h\"\n#include \"webrtc/webrtc_environment.h\"\n#include \"webview/webview_interface.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kMediaUnlockedTooltipDuration = 5 * crl::time(1000);\nconst auto kVerifyAgeAboutPrefix = \"cloud_lng_age_verify_about_\";\n\nrpl::producer<TextWithEntities> AgeVerifyAbout(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto appConfig = &session->appConfig();\n\treturn rpl::single(\n\t\trpl::empty\n\t) | rpl::then(\n\t\tLang::Updated()\n\t) | rpl::map([=] {\n\t\tconst auto country = appConfig->ageVerifyCountry().toLower();\n\t\tconst auto age = appConfig->ageVerifyMinAge();\n\t\tconst auto [shift, string] = Lang::Plural(\n\t\t\tLang::kPluralKeyBaseForCloudValue,\n\t\t\tage,\n\t\t\tlt_count);\n\t\tconst auto postfixes = {\n\t\t\t\"#zero\",\n\t\t\t\"#one\",\n\t\t\t\"#two\",\n\t\t\t\"#few\",\n\t\t\t\"#many\",\n\t\t\t\"#other\"\n\t\t};\n\t\tAssert(shift >= 0 && shift < postfixes.size());\n\t\tconst auto postfix = *(begin(postfixes) + shift);\n\t\treturn tr::rich(Lang::GetNonDefaultValue(\n\t\t\tkVerifyAgeAboutPrefix + country.toUtf8() + postfix\n\t\t).replace(u\"{count}\"_q, string));\n\t});\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> AgeVerifyIcon(\n\t\tnot_null<QWidget*> parent) {\n\tconst auto padding = st::settingsAgeVerifyIconPadding;\n\tconst auto full = st::settingsAgeVerifyIcon.size().grownBy(padding);\n\tauto result = object_ptr<Ui::RpWidget>(parent);\n\tconst auto raw = result.data();\n\traw->resize(full);\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\tconst auto x = (raw->width() - full.width()) / 2;\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setBrush(st::windowBgActive);\n\t\tp.setPen(Qt::NoPen);\n\t\tconst auto inner = QRect(QPoint(x, 0), full);\n\t\tp.drawEllipse(inner);\n\t\tst::settingsAgeVerifyIcon.paintInCenter(p, inner);\n\t}, raw->lifetime());\n\treturn result;\n}\n\n} // namespace\n\nvoid PaintInterpolatedIcon(\n\t\tQPainter &p,\n\t\tconst style::icon &a,\n\t\tconst style::icon &b,\n\t\tfloat64 b_ratio,\n\t\tQRect rect) {\n\tPainterHighQualityEnabler hq(p);\n\tp.save();\n\tp.translate(rect.center());\n\tp.setOpacity(b_ratio);\n\tp.scale(b_ratio, b_ratio);\n\tb.paintInCenter(p, rect.translated(-rect.center()));\n\tp.restore();\n\n\tp.save();\n\tp.translate(rect.center());\n\tp.setOpacity(1. - b_ratio);\n\tp.scale(1. - b_ratio, 1. - b_ratio);\n\ta.paintInCenter(p, rect.translated(-rect.center()));\n\tp.restore();\n}\n\nstd::unique_ptr<Media> CreateAttach(\n\t\tnot_null<Element*> parent,\n\t\tDocumentData *document,\n\t\tPhotoData *photo) {\n\treturn CreateAttach(parent, document, photo, {}, {});\n}\n\nstd::unique_ptr<Media> CreateAttach(\n\t\tnot_null<Element*> parent,\n\t\tDocumentData *document,\n\t\tPhotoData *photo,\n\t\tconst std::vector<std::unique_ptr<Data::Media>> &collage,\n\t\tconst QString &webpageUrl) {\n\tif (!collage.empty()) {\n\t\treturn std::make_unique<GroupedMedia>(parent, collage);\n\t} else if (document) {\n\t\tconst auto spoiler = false;\n\t\tif (document->sticker()) {\n\t\t\tconst auto skipPremiumEffect = true;\n\t\t\treturn std::make_unique<UnwrappedMedia>(\n\t\t\t\tparent,\n\t\t\t\tstd::make_unique<Sticker>(\n\t\t\t\t\tparent,\n\t\t\t\t\tdocument,\n\t\t\t\t\tskipPremiumEffect));\n\t\t} else if (document->isAnimation() || document->isVideoFile()) {\n\t\t\treturn std::make_unique<Gif>(\n\t\t\t\tparent,\n\t\t\t\tparent->data(),\n\t\t\t\tdocument,\n\t\t\t\tspoiler);\n\t\t} else if (document->isWallPaper() || document->isTheme()) {\n\t\t\treturn std::make_unique<ThemeDocument>(\n\t\t\t\tparent,\n\t\t\t\tdocument,\n\t\t\t\tThemeDocument::ParamsFromUrl(webpageUrl));\n\t\t}\n\t\treturn std::make_unique<Document>(parent, parent->data(), document);\n\t} else if (photo) {\n\t\tconst auto spoiler = false;\n\t\treturn std::make_unique<Photo>(\n\t\t\tparent,\n\t\t\tparent->data(),\n\t\t\tphoto,\n\t\t\tspoiler);\n\t} else if (const auto params = ThemeDocument::ParamsFromUrl(webpageUrl)) {\n\t\treturn std::make_unique<ThemeDocument>(parent, nullptr, params);\n\t}\n\treturn nullptr;\n}\n\nint UnitedLineHeight() {\n\treturn std::max(st::semiboldFont->height, st::normalFont->height);\n}\n\nQImage PrepareWithBlurredBackground(\n\t\tQSize outer,\n\t\t::Media::Streaming::ExpandDecision resize,\n\t\tImage *large,\n\t\tImage *blurred) {\n\treturn PrepareWithBlurredBackground(\n\t\touter,\n\t\tresize,\n\t\tlarge ? large->original() : QImage(),\n\t\tblurred ? blurred->original() : QImage());\n}\n\nQImage PrepareWithBlurredBackground(\n\t\tQSize outer,\n\t\t::Media::Streaming::ExpandDecision resize,\n\t\tQImage large,\n\t\tQImage blurred) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (resize.expanding) {\n\t\treturn Images::Prepare(std::move(large), resize.result * ratio, {\n\t\t\t.outer = outer,\n\t\t});\n\t}\n\tauto background = QImage(\n\t\touter * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tbackground.setDevicePixelRatio(ratio);\n\tif (blurred.isNull()) {\n\t\tbackground.fill(Qt::black);\n\t\tif (large.isNull()) {\n\t\t\treturn background;\n\t\t}\n\t}\n\tauto p = QPainter(&background);\n\tif (!blurred.isNull()) {\n\t\tusing namespace ::Media::Streaming;\n\t\tFillBlurredBackground(p, outer, std::move(blurred));\n\t}\n\tif (!large.isNull()) {\n\t\tauto image = large.scaled(\n\t\t\tresize.result * ratio,\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t\timage.setDevicePixelRatio(ratio);\n\t\tp.drawImage(\n\t\t\t(outer.width() - resize.result.width()) / 2,\n\t\t\t(outer.height() - resize.result.height()) / 2,\n\t\t\timage);\n\t}\n\tp.end();\n\treturn background;\n}\n\nQSize CountDesiredMediaSize(QSize original) {\n\treturn DownscaledSize(\n\t\tstyle::ConvertScale(original),\n\t\t{ st::maxMediaSize, st::maxMediaSize });\n}\n\nQSize CountMediaSize(QSize desired, int newWidth) {\n\tExpects(!desired.isEmpty());\n\n\treturn (desired.width() <= newWidth)\n\t\t? desired\n\t\t: NonEmptySize(\n\t\t\tdesired.scaled(newWidth, desired.height(), Qt::KeepAspectRatio));\n}\n\nQSize CountPhotoMediaSize(\n\t\tQSize desired,\n\t\tint newWidth,\n\t\tint maxWidth) {\n\tconst auto media = CountMediaSize(desired, qMin(newWidth, maxWidth));\n\treturn (media.height() <= newWidth)\n\t\t? media\n\t\t: NonEmptySize(\n\t\t\tmedia.scaled(media.width(), newWidth, Qt::KeepAspectRatio));\n}\n\nvoid ShowPaidMediaUnlockedToast(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto media = item->media();\n\tconst auto invoice = media ? media->invoice() : nullptr;\n\tif (!invoice || !invoice->isPaidMedia) {\n\t\treturn;\n\t}\n\tconst auto sender = item->originalSender();\n\tconst auto broadcast = (sender && sender->isBroadcast())\n\t\t? sender\n\t\t: item->history()->peer.get();\n\tconst auto user = item->viaBot()\n\t\t? item->viaBot()\n\t\t: item->originalSender()\n\t\t? item->originalSender()->asUser()\n\t\t: nullptr;\n\tauto text = tr::lng_credits_media_done_title(\n\t\ttr::now,\n\t\ttr::bold\n\t).append('\\n').append(user\n\t\t? tr::lng_credits_media_done_text_user(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tinvoice->amount,\n\t\t\tlt_user,\n\t\t\ttr::bold(user->shortName()),\n\t\t\ttr::rich)\n\t\t: tr::lng_credits_media_done_text(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tinvoice->amount,\n\t\t\tlt_chat,\n\t\t\ttr::bold(broadcast->name()),\n\t\t\ttr::rich));\n\tcontroller->showToast(std::move(text), kMediaUnlockedTooltipDuration);\n}\n\nClickHandlerPtr MakePaidMediaLink(not_null<HistoryItem*> item) {\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tconst auto controller = my.sessionWindow.get();\n\t\tconst auto weak = my.sessionWindow;\n\t\tconst auto itemId = item->fullId();\n\t\tconst auto session = &item->history()->session();\n\t\tusing Result = Payments::CheckoutResult;\n\t\tconst auto done = crl::guard(session, [=](Result result) {\n\t\t\tif (result != Result::Paid) {\n\t\t\t\treturn;\n\t\t\t} else if (const auto item = session->data().message(itemId)) {\n\t\t\t\tsession->api().views().pollExtendedMedia(item, true);\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tShowPaidMediaUnlockedToast(strong, item);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tconst auto reactivate = controller\n\t\t\t? crl::guard(\n\t\t\t\tcontroller,\n\t\t\t\t[=](auto) { controller->widget()->activate(); })\n\t\t\t: Fn<void(Payments::CheckoutResult)>();\n\t\tconst auto credits = Payments::IsCreditsInvoice(item);\n\t\tconst auto nonPanelPaymentFormProcess = (controller && credits)\n\t\t\t? Payments::ProcessNonPanelPaymentFormFactory(controller, done)\n\t\t\t: nullptr;\n\t\tPayments::CheckoutProcess::Start(\n\t\t\titem,\n\t\t\tPayments::Mode::Payment,\n\t\t\treactivate,\n\t\t\tnonPanelPaymentFormProcess);\n\t});\n}\n\nvoid ShowAgeVerification(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<UserData*> bot,\n\t\tFn<void()> reveal) {\n\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tbox->setNoContentMargin(true);\n\t\tbox->setStyle(st::settingsAgeVerifyBox);\n\t\tbox->setWidth(st::boxWideWidth);\n\n\t\tbox->addRow(AgeVerifyIcon(box), st::settingsAgeVerifyIconMargin);\n\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_age_verify_title(),\n\t\t\t\tst::settingsAgeVerifyTitle),\n\t\t\tst::boxRowPadding + st::settingsAgeVerifyMargin,\n\t\t\tstyle::al_top);\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\tAgeVerifyAbout(&bot->session()),\n\t\t\t\tst::settingsAgeVerifyText),\n\t\t\tst::boxRowPadding + st::settingsAgeVerifyMargin,\n\t\t\tstyle::al_top);\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_age_verify_here(tr::rich),\n\t\t\t\tst::settingsAgeVerifyText),\n\t\t\tst::boxRowPadding + st::settingsAgeVerifyMargin,\n\t\t\tstyle::al_top);\n\n\t\tconst auto weak = QPointer<Ui::GenericBox>(box);\n\t\tconst auto done = crl::guard(&bot->session(), [=](int age) {\n\t\t\tconst auto min = bot->session().appConfig().ageVerifyMinAge();\n\t\t\tif (age >= min) {\n\t\t\t\treveal();\n\t\t\t\tbot->session().api().sensitiveContent().update(true);\n\t\t\t} else {\n\t\t\t\tshow->showToast({\n\t\t\t\t\t.title = tr::lng_age_verify_sorry_title(tr::now),\n\t\t\t\t\t.text = { tr::lng_age_verify_sorry_text(tr::now) },\n\t\t\t\t\t.duration = Ui::Toast::kDefaultDuration * 3,\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (const auto strong = weak.data()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t});\n\t\tconst auto button = box->addButton(tr::lng_age_verify_button(), [=] {\n\t\t\tbot->session().attachWebView().open({\n\t\t\t\t.bot = bot,\n\t\t\t\t.parentShow = box->uiShow(),\n\t\t\t\t.context = { .maySkipConfirmation = true },\n\t\t\t\t.source = InlineBots::WebViewSourceAgeVerification{\n\t\t\t\t\t.done = done,\n\t\t\t\t},\n\t\t\t});\n\t\t});\n\t\tbox->widthValue(\n\t\t) | rpl::on_next([=](int width) {\n\t\t\tconst auto &padding = st::settingsAgeVerifyBox.buttonPadding;\n\t\t\tbutton->resizeToWidth(width\n\t\t\t\t- padding.left()\n\t\t\t\t- padding.right());\n\t\t\tbutton->moveToLeft(padding.left(), padding.top());\n\t\t}, button->lifetime());\n\n\t\tconst auto close = Ui::CreateChild<Ui::IconButton>(\n\t\t\tbox.get(),\n\t\t\tst::boxTitleClose);\n\t\tclose->setClickedCallback([=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t\tbox->widthValue(\n\t\t) | rpl::on_next([=](int width) {\n\t\t\tclose->moveToRight(0, 0);\n\t\t}, box->lifetime());\n\t\tcrl::on_main(close, [=] { close->raise(); });\n\t}));\n}\n\nvoid ShowAgeVerificationMobile(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<Main::Session*> session) {\n\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tbox->setTitle(tr::lng_age_verify_title());\n\t\tbox->setWidth(st::boxWideWidth);\n\n\t\tconst auto size = st::settingsCloudPasswordIconSize;\n\t\tauto icon = Settings::CreateLottieIcon(\n\t\t\tbox->verticalLayout(),\n\t\t\t{\n\t\t\t\t.name = u\"phone\"_q,\n\t\t\t\t.sizeOverride = { size, size },\n\t\t\t},\n\t\t\tst::peerAppearanceIconPadding);\n\n\t\tbox->showFinishes(\n\t\t) | rpl::on_next([animate = std::move(icon.animate)] {\n\t\t\tanimate(anim::repeat::once);\n\t\t}, box->lifetime());\n\n\t\tbox->addRow(std::move(icon.widget));\n\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\tAgeVerifyAbout(session),\n\t\t\t\tst::settingsAgeVerifyText),\n\t\t\tst::boxRowPadding + st::settingsAgeVerifyMargin,\n\t\t\tstyle::al_top);\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_age_verify_mobile(tr::rich),\n\t\t\t\tst::settingsAgeVerifyText),\n\t\t\tst::boxRowPadding + st::settingsAgeVerifyMargin,\n\t\t\tstyle::al_top);\n\n\t\tbox->addButton(tr::lng_box_ok(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t}));\n}\n\nvoid ShowAgeVerificationRequired(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<Main::Session*> session,\n\t\tFn<void()> reveal) {\n\tstruct State {\n\t\tFn<void()> check;\n\t\trpl::lifetime lifetime;\n\t\tstd::optional<PeerData*> bot;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tconst auto username = session->appConfig().ageVerifyBotUsername();\n\tconst auto bot = session->data().peerByUsername(username);\n\tif (username.isEmpty() || bot) {\n\t\tstate->bot = bot;\n\t} else {\n\t\tsession->api().request(MTPcontacts_ResolveUsername(\n\t\t\tMTP_flags(0),\n\t\t\tMTP_string(username),\n\t\t\tMTPstring()\n\t\t)).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\t\tconst auto &data = result.data();\n\t\t\tsession->data().processUsers(data.vusers());\n\t\t\tsession->data().processChats(data.vchats());\n\t\t\tconst auto botId = peerFromMTP(data.vpeer());\n\t\t\tstate->bot = session->data().peerLoaded(botId);\n\t\t\tstate->check();\n\t\t}).fail([=] {\n\t\t\tstate->bot = nullptr;\n\t\t\tstate->check();\n\t\t}).send();\n\t}\n\tstate->check = [=] {\n\t\tconst auto sensitive = &session->api().sensitiveContent();\n\t\tconst auto ready = sensitive->loaded();\n\t\tif (!ready) {\n\t\t\tstate->lifetime = sensitive->loadedValue(\n\t\t\t) | rpl::filter(\n\t\t\t\trpl::mappers::_1\n\t\t\t) | rpl::take(1) | rpl::on_next(state->check);\n\t\t\treturn;\n\t\t} else if (!state->bot.has_value()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto has = Core::App().mediaDevices().recordAvailability();\n\t\tconst auto available = Webview::Availability();\n\t\tconst auto bot = (*state->bot)->asUser();\n\t\tif (available.error == Webview::Available::Error::None\n\t\t\t&& has == Webrtc::RecordAvailability::VideoAndAudio\n\t\t\t&& sensitive->canChangeCurrent()\n\t\t\t&& bot\n\t\t\t&& bot->isBot()\n\t\t\t&& bot->botInfo->hasMainApp) {\n\t\t\tShowAgeVerification(show, bot, reveal);\n\t\t} else {\n\t\t\tShowAgeVerificationMobile(show, session);\n\t\t}\n\t\tstate->lifetime.destroy();\n\t\tstate->check = nullptr;\n\t};\n\tstate->check();\n}\n\nvoid ShowSensitiveConfirm(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<Main::Session*> session,\n\t\tFn<void()> reveal) {\n\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tstruct State {\n\t\t\trpl::variable<bool> canChange;\n\t\t\tUi::Checkbox *checkbox = nullptr;\n\t\t};\n\t\tconst auto state = box->lifetime().make_state<State>();\n\t\tconst auto sensitive = &session->api().sensitiveContent();\n\t\tstate->canChange = sensitive->canChange();\n\t\tconst auto done = [=](Fn<void()> close) {\n\t\t\tif (state->canChange.current()\n\t\t\t\t&& state->checkbox->checked()) {\n\t\t\t\tshow->showToast({\n\t\t\t\t\t.text = tr::lng_sensitive_toast(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t.adaptive = true,\n\t\t\t\t\t.duration = 5 * crl::time(1000),\n\t\t\t\t});\n\t\t\t\tsensitive->update(true);\n\t\t\t} else {\n\t\t\t\treveal();\n\t\t\t}\n\t\t\tclose();\n\t\t};\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = tr::lng_sensitive_text(tr::rich),\n\t\t\t.confirmed = done,\n\t\t\t.confirmText = tr::lng_sensitive_view(),\n\t\t\t.title = tr::lng_sensitive_title(),\n\t\t});\n\t\tconst auto skip = st::defaultCheckbox.margin.bottom();\n\t\tconst auto wrap = box->addRow(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::Checkbox>>(\n\t\t\t\tbox,\n\t\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\t\tbox,\n\t\t\t\t\ttr::lng_sensitive_always(tr::now),\n\t\t\t\t\tfalse)),\n\t\t\tst::boxRowPadding + QMargins(0, 0, 0, skip));\n\t\twrap->toggleOn(state->canChange.value());\n\t\twrap->finishAnimating();\n\t\tstate->checkbox = wrap->entity();\n\t}));\n}\n\nClickHandlerPtr MakeSensitiveMediaLink(\n\t\tClickHandlerPtr reveal,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto session = &item->history()->session();\n\tsession->api().sensitiveContent().preload();\n\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto plain = [reveal, context] {\n\t\t\tif (const auto raw = reveal.get()) {\n\t\t\t\traw->onClick(context);\n\t\t\t}\n\t\t};\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tconst auto controller = my.sessionWindow.get();\n\t\tconst auto show = controller ? controller->uiShow() : my.show;\n\t\tif (!show) {\n\t\t\tplain();\n\t\t} else if (session->appConfig().ageVerifyNeeded()) {\n\t\t\tShowAgeVerificationRequired(show, session, plain);\n\t\t} else {\n\t\t\tShowSensitiveConfirm(show, session, plain);\n\t\t}\n\t});\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_media_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass DocumentData;\nclass PhotoData;\nclass Image;\n\nnamespace HistoryView {\nclass Element;\n} // namespace HistoryView\n\nnamespace Data {\nclass Media;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Media::Streaming {\nstruct ExpandDecision;\n} // namespace Media::Streaming\n\nnamespace Ui {\nclass Show;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass Media;\n\nvoid PaintInterpolatedIcon(\n\tQPainter &p,\n\tconst style::icon &a,\n\tconst style::icon &b,\n\tfloat64 b_ratio,\n\tQRect rect);\n\n[[nodiscard]] std::unique_ptr<Media> CreateAttach(\n\tnot_null<Element*> parent,\n\tDocumentData *document,\n\tPhotoData *photo);\n[[nodiscard]] std::unique_ptr<Media> CreateAttach(\n\tnot_null<Element*> parent,\n\tDocumentData *document,\n\tPhotoData *photo,\n\tconst std::vector<std::unique_ptr<Data::Media>> &collage,\n\tconst QString &webpageUrl);\n[[nodiscard]] int UnitedLineHeight();\n\n[[nodiscard]] inline QSize NonEmptySize(QSize size) {\n\treturn QSize(std::max(size.width(), 1), std::max(size.height(), 1));\n}\n\n[[nodiscard]] inline QSize DownscaledSize(QSize size, QSize box) {\n\treturn NonEmptySize(\n\t\t((size.width() > box.width() || size.height() > box.height())\n\t\t\t? size.scaled(box, Qt::KeepAspectRatio)\n\t\t\t: size));\n}\n\n[[nodiscard]] QImage PrepareWithBlurredBackground(\n\tQSize outer,\n\t::Media::Streaming::ExpandDecision resize,\n\tImage *large,\n\tImage *blurred);\n[[nodiscard]] QImage PrepareWithBlurredBackground(\n\tQSize outer,\n\t::Media::Streaming::ExpandDecision resize,\n\tQImage large,\n\tQImage blurred);\n\n[[nodiscard]] QSize CountDesiredMediaSize(QSize original);\n[[nodiscard]] QSize CountMediaSize(QSize desired, int newWidth);\n[[nodiscard]] QSize CountPhotoMediaSize(\n\tQSize desired,\n\tint newWidth,\n\tint maxWidth);\n\nvoid ShowAgeVerificationRequired(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<Main::Session*> session,\n\tFn<void()> reveal);\n\n[[nodiscard]] ClickHandlerPtr MakePaidMediaLink(\n\tnot_null<HistoryItem*> item);\n[[nodiscard]] ClickHandlerPtr MakeSensitiveMediaLink(\n\tClickHandlerPtr reveal,\n\tnot_null<HistoryItem*> item);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_media_generic.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_peer.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/rect.h\"\n#include \"ui/round_rect.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kAdditionalPrizesWithLineOpacity = 0.6;\n\n} // namespace\n\nTextState MediaGenericPart::textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const {\n\treturn {};\n}\n\nvoid MediaGenericPart::clickHandlerPressedChanged(\n\tconst ClickHandlerPtr &p,\n\tbool pressed) {\n}\n\nbool MediaGenericPart::hasHeavyPart() {\n\treturn false;\n}\n\nvoid MediaGenericPart::unloadHeavyPart() {\n}\n\nauto MediaGenericPart::stickerTakePlayer(\n\tnot_null<DocumentData*> data,\n\tconst Lottie::ColorReplacements *replacements\n) -> std::unique_ptr<StickerPlayer> {\n\treturn nullptr;\n}\n\nMediaGeneric::MediaGeneric(\n\tnot_null<Element*> parent,\n\tFn<void(\n\t\tnot_null<MediaGeneric*>,\n\t\tFn<void(std::unique_ptr<Part>)>)> generate,\n\tMediaGenericDescriptor &&descriptor)\n: Media(parent)\n, _paintBgFactory(std::move(descriptor.paintBgFactory))\n, _paintBg(_paintBgFactory ? _paintBgFactory() : nullptr)\n, _fullAreaLink(descriptor.fullAreaLink)\n, _maxWidthCap(descriptor.maxWidth)\n, _expandCurrentWidth(descriptor.expandCurrentWidth)\n, _service(descriptor.service)\n, _hideServiceText(descriptor.hideServiceText) {\n\tgenerate(this, [&](std::unique_ptr<Part> part) {\n\t\t_entries.push_back({\n\t\t\t.object = std::move(part),\n\t\t});\n\t});\n}\n\nMediaGeneric::~MediaGeneric() {\n\tif (hasHeavyPart()) {\n\t\tunloadHeavyPart();\n\t\t_parent->checkHeavyPart();\n\t}\n}\n\nQSize MediaGeneric::countOptimalSize() {\n\tconst auto maxWidth = _maxWidthCap\n\t\t? _maxWidthCap\n\t\t: st::chatGiveawayWidth;\n\n\tauto top = 0;\n\tfor (auto &entry : _entries) {\n\t\tconst auto raw = entry.object.get();\n\t\traw->initDimensions();\n\t\ttop += raw->resizeGetHeight(maxWidth);\n\t}\n\treturn { maxWidth, top };\n}\n\nQSize MediaGeneric::countCurrentSize(int newWidth) {\n\tif (!_expandCurrentWidth && newWidth > maxWidth()) {\n\t\tnewWidth = maxWidth();\n\t}\n\tauto top = 0;\n\tfor (auto &entry : _entries) {\n\t\ttop += entry.object->resizeGetHeight(newWidth);\n\t}\n\treturn { newWidth, top };\n}\n\nvoid MediaGeneric::draw(Painter &p, const PaintContext &context) const {\n\tconst auto outer = width();\n\tif (outer < st::msgPadding.left() + st::msgPadding.right() + 1) {\n\t\treturn;\n\t}\n\tif (!_paintBg && _paintBgFactory) {\n\t\t_paintBg = _paintBgFactory();\n\t}\n\tif (_paintBg) {\n\t\t_paintBg(p, context, this);\n\t} else if (_service) {\n\t\tPainterHighQualityEnabler hq(p);\n\t\tconst auto radius = st::msgServiceGiftBoxRadius;\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(context.st->msgServiceBg());\n\t\tconst auto rect = QRect(0, 0, width(), height());\n\t\tif (parent()->data()->inlineReplyKeyboard()) {\n\t\t\tconst auto half = rect.height() / 2;\n\t\t\tp.setClipRect(rect - QMargins(0, 0, 0, half));\n\t\t\tp.drawRoundedRect(rect, radius, radius);\n\t\t\tp.setClipRect(rect - QMargins(0, rect.height() - half, 0, 0));\n\t\t\tconst auto small = Ui::BubbleRadiusSmall();\n\t\t\tp.drawRoundedRect(rect, small, small);\n\t\t\tp.setClipping(false);\n\t\t} else {\n\t\t\tp.drawRoundedRect(rect, radius, radius);\n\t\t}\n\t}\n\n\tauto translated = 0;\n\tfor (const auto &entry : _entries) {\n\t\tconst auto raw = entry.object.get();\n\t\tconst auto height = raw->height();\n\t\traw->draw(p, this, context, outer);\n\t\ttranslated += height;\n\t\tp.translate(0, height);\n\t}\n\tp.translate(0, -translated);\n}\n\nTextState MediaGeneric::textState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tauto result = TextState(_parent);\n\n\tconst auto outer = width();\n\tif (outer < st::msgPadding.left() + st::msgPadding.right() + 1) {\n\t\treturn result;\n\t}\n\n\tif (_fullAreaLink && QRect(0, 0, width(), height()).contains(point)) {\n\t\tresult.link = _fullAreaLink;\n\t\treturn result;\n\t}\n\n\tfor (const auto &entry : _entries) {\n\t\tconst auto raw = entry.object.get();\n\t\tconst auto height = raw->height();\n\t\tif (point.y() >= 0 && point.y() < height) {\n\t\t\tconst auto part = raw->textState(point, request, outer);\n\t\t\tresult.link = part.link;\n\t\t\treturn result;\n\t\t}\n\t\tpoint.setY(point.y() - height);\n\t}\n\treturn result;\n}\n\nvoid MediaGeneric::clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool active) {\n}\n\nvoid MediaGeneric::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed) {\n\tfor (const auto &entry : _entries) {\n\t\tentry.object->clickHandlerPressedChanged(p, pressed);\n\t}\n}\n\nstd::unique_ptr<StickerPlayer> MediaGeneric::stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) {\n\tfor (const auto &entry : _entries) {\n\t\tif (auto result = entry.object->stickerTakePlayer(\n\t\t\t\tdata,\n\t\t\t\treplacements)) {\n\t\t\treturn result;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nbool MediaGeneric::hideFromName() const {\n\treturn !parent()->data()->Has<HistoryMessageForwarded>();\n}\n\nbool MediaGeneric::hideServiceText() const {\n\treturn _hideServiceText;\n}\n\nbool MediaGeneric::hasHeavyPart() const {\n\tfor (const auto &entry : _entries) {\n\t\tif (entry.object->hasHeavyPart()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid MediaGeneric::unloadHeavyPart() {\n\t_paintBg = nullptr;\n\tfor (const auto &entry : _entries) {\n\t\tentry.object->unloadHeavyPart();\n\t}\n}\n\nQMargins MediaGeneric::inBubblePadding() const {\n\tauto lshift = st::msgPadding.left();\n\tauto rshift = st::msgPadding.right();\n\tauto bshift = isBubbleBottom()\n\t\t? st::msgPadding.top()\n\t\t: st::mediaInBubbleSkip;\n\tauto tshift = isBubbleTop()\n\t\t? st::msgPadding.bottom()\n\t\t: st::mediaInBubbleSkip;\n\treturn QMargins(lshift, tshift, rshift, bshift);\n}\n\nMediaGenericTextPart::MediaGenericTextPart(\n\tTextWithEntities text,\n\tQMargins margins,\n\tconst style::TextStyle &st,\n\tconst base::flat_map<uint16, ClickHandlerPtr> &links,\n\tconst Ui::Text::MarkedContext &context,\n\tstyle::align align)\n: _text(st::msgMinWidth)\n, _margins(margins)\n, _align(align) {\n\t_text.setMarkedText(\n\t\tst,\n\t\ttext,\n\t\tkMarkupTextOptions,\n\t\tcontext);\n\tfor (const auto &[index, link] : links) {\n\t\t_text.setLink(index, link);\n\t}\n}\n\nvoid MediaGenericTextPart::draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const {\n\tconst auto use = (width() - _margins.left() - _margins.right());\n\tsetupPen(p, owner, context);\n\t_text.draw(p, {\n\t\t.position = {\n\t\t\t((_align == style::al_top)\n\t\t\t\t? ((outerWidth - use) / 2)\n\t\t\t\t: _margins.left()),\n\t\t\t_margins.top(),\n\t\t},\n\t\t.outerWidth = outerWidth,\n\t\t.availableWidth = use,\n\t\t.align = _align,\n\t\t.palette = &(owner->service()\n\t\t\t? context.st->serviceTextPalette()\n\t\t\t: context.messageStyle()->textPalette),\n\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t.now = context.now,\n\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t.elisionLines = elisionLines(),\n\t});\n}\n\nvoid MediaGenericTextPart::setupPen(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context) const {\n\tconst auto service = owner->service();\n\tp.setPen(service\n\t\t? context.st->msgServiceFg()\n\t\t: context.messageStyle()->historyTextFg);\n}\n\nint MediaGenericTextPart::elisionLines() const {\n\treturn 0;\n}\n\nTextState MediaGenericTextPart::textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const {\n\tconst auto use = (width() - _margins.left() - _margins.right());\n\tpoint -= QPoint{\n\t\t((_align == style::al_top)\n\t\t\t? ((outerWidth - use) / 2)\n\t\t\t: _margins.left()),\n\t\t_margins.top(),\n\t};\n\tauto result = TextState();\n\tauto forText = request.forText();\n\tforText.align = _align;\n\tresult.link = _text.getState(point, use, forText).link;\n\treturn result;\n}\n\nQSize MediaGenericTextPart::countOptimalSize() {\n\tconst auto lines = elisionLines();\n\tconst auto height = lines\n\t\t? std::min(_text.minHeight(), lines * _text.style()->font->height)\n\t\t: _text.minHeight();\n\treturn {\n\t\t_margins.left() + _text.maxWidth() + _margins.right(),\n\t\t_margins.top() + height + _margins.bottom(),\n\t};\n}\n\nQSize MediaGenericTextPart::countCurrentSize(int newWidth) {\n\tauto skip = _margins.left() + _margins.right();\n\tconst auto size = (_align == style::al_top)\n\t\t? Ui::Text::CountOptimalTextSize(\n\t\t\t_text,\n\t\t\tst::msgMinWidth,\n\t\t\tstd::max(st::msgMinWidth, newWidth - skip))\n\t\t: QSize(newWidth - skip, _text.countHeight(newWidth - skip));\n\tconst auto lines = elisionLines();\n\tconst auto height = lines\n\t\t? std::min(size.height(), lines * _text.style()->font->height)\n\t\t: size.height();\n\treturn {\n\t\tsize.width() + skip,\n\t\t_margins.top() + height + _margins.bottom(),\n\t};\n}\n\nTextDelimeterPart::TextDelimeterPart(\n\tconst QString &text,\n\tQMargins margins)\n: _margins(margins) {\n\t_text.setText(st::defaultTextStyle, text);\n}\n\nvoid TextDelimeterPart::draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const {\n\tconst auto stm = context.messageStyle();\n\tconst auto available = outerWidth - _margins.left() - _margins.right();\n\tp.setPen(stm->msgDateFg);\n\t_text.draw(p, {\n\t\t.position = { _margins.left(), _margins.top() },\n\t\t.outerWidth = outerWidth,\n\t\t.availableWidth = available,\n\t\t.align = style::al_top,\n\t\t.palette = &stm->textPalette,\n\t\t.now = context.now,\n\t\t.elisionLines = 1,\n\t});\n\tconst auto skip = st::chatGiveawayPrizesWithSkip;\n\tconst auto inner = available - 2 * skip;\n\tconst auto sub = _text.maxWidth();\n\tif (inner > sub + 1) {\n\t\tconst auto fill = (inner - sub) / 2;\n\t\tconst auto stroke = st::lineWidth;\n\t\tconst auto top = _margins.top()\n\t\t\t+ st::chatGiveawayPrizesWithLineTop;\n\t\tp.setOpacity(kAdditionalPrizesWithLineOpacity);\n\t\tp.fillRect(_margins.left(), top, fill, stroke, stm->msgDateFg);\n\t\tconst auto start = outerWidth - _margins.right() - fill;\n\t\tp.fillRect(start, top, fill, stroke, stm->msgDateFg);\n\t\tp.setOpacity(1.);\n\t}\n}\n\nQSize TextDelimeterPart::countOptimalSize() {\n\treturn {\n\t\t_margins.left() + _text.maxWidth() + _margins.right(),\n\t\t_margins.top() + st::normalFont->height + _margins.bottom(),\n\t};\n}\n\nQSize TextDelimeterPart::countCurrentSize(int newWidth) {\n\treturn { newWidth, minHeight() };\n}\n\nLambdaGenericPart::LambdaGenericPart(\n\tQSize size,\n\tFn<void(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth)> draw)\n: _size(size)\n, _draw(std::move(draw)) {\n}\n\nvoid LambdaGenericPart::draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const {\n\tif (_draw) {\n\t\t_draw(p, owner, context, outerWidth);\n\t}\n}\n\nQSize LambdaGenericPart::countOptimalSize() {\n\treturn _size;\n}\n\nQSize LambdaGenericPart::countCurrentSize(int newWidth) {\n\treturn { newWidth, _size.height() };\n}\n\nStickerInBubblePart::StickerInBubblePart(\n\tnot_null<Element*> parent,\n\tElement *replacing,\n\tFn<Data()> lookup,\n\tQMargins padding)\n: _parent(parent)\n, _lookup(std::move(lookup))\n, _padding(padding) {\n\tensureCreated(replacing);\n}\n\nvoid StickerInBubblePart::draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const {\n\tensureCreated();\n\tif (_sticker) {\n\t\tconst auto stickerSize = _sticker->countOptimalSize();\n\t\tconst auto sticker = QRect(\n\t\t\t(outerWidth - stickerSize.width()) / 2,\n\t\t\t_padding.top() + _skipTop,\n\t\t\tstickerSize.width(),\n\t\t\tstickerSize.height());\n\t\t_sticker->draw(p, context, sticker);\n\t}\n}\n\nTextState StickerInBubblePart::textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const {\n\tauto result = TextState(_parent);\n\tif (_sticker) {\n\t\tconst auto stickerSize = _sticker->countOptimalSize();\n\t\tconst auto sticker = QRect(\n\t\t\t(outerWidth - stickerSize.width()) / 2,\n\t\t\t_padding.top() + _skipTop,\n\t\t\tstickerSize.width(),\n\t\t\tstickerSize.height());\n\t\tif (sticker.contains(point)) {\n\t\t\tresult.link = _link;\n\t\t}\n\t}\n\treturn result;\n}\n\nbool StickerInBubblePart::hasHeavyPart() {\n\treturn _sticker && _sticker->hasHeavyPart();\n}\n\nvoid StickerInBubblePart::unloadHeavyPart() {\n\tif (_sticker) {\n\t\t_sticker->unloadHeavyPart();\n\t}\n}\n\nstd::unique_ptr<StickerPlayer> StickerInBubblePart::stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) {\n\treturn _sticker\n\t\t? _sticker->stickerTakePlayer(data, replacements)\n\t\t: nullptr;\n}\n\nQSize StickerInBubblePart::countOptimalSize() {\n\tensureCreated();\n\tconst auto size = _sticker ? _sticker->countOptimalSize() : [&] {\n\t\tconst auto fallback = _lookup().size;\n\t\treturn QSize{ fallback, fallback };\n\t}();\n\treturn {\n\t\t_padding.left() + size.width() + _padding.right(),\n\t\t_padding.top() + size.height() + _padding.bottom(),\n\t};\n}\n\nQSize StickerInBubblePart::countCurrentSize(int newWidth) {\n\treturn { newWidth, minHeight() };\n}\n\nvoid StickerInBubblePart::ensureCreated(Element *replacing) const {\n\tif (_sticker) {\n\t\treturn;\n\t} else if (const auto data = _lookup()) {\n\t\tconst auto sticker = data.sticker;\n\t\tif (sticker->sticker()) {\n\t\t\tconst auto skipPremiumEffect = true;\n\t\t\t_link = data.link;\n\t\t\t_skipTop = data.skipTop;\n\t\t\t_sticker.emplace(_parent, sticker, skipPremiumEffect, replacing);\n\t\t\tif (data.stopOnLastFrame) {\n\t\t\t\t_sticker->setStopOnLastFrame(true);\n\t\t\t}\n\t\t\t_sticker->initSize(data.size);\n\t\t\t_sticker->setCustomCachingTag(data.cacheTag);\n\t\t}\n\t}\n}\n\nStickerWithBadgePart::StickerWithBadgePart(\n\tnot_null<Element*> parent,\n\tElement *replacing,\n\tFn<Data()> lookup,\n\tQMargins padding,\n\tQString badge,\n\tQImage customLeftIcon,\n\tstd::optional<QColor> colorOverride)\n: _customLeftIcon(std::move(customLeftIcon))\n, _sticker(parent, replacing, std::move(lookup), padding)\n, _badgeText(badge)\n, _colorOverride(std::move(colorOverride)) {\n}\n\nvoid StickerWithBadgePart::draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const {\n\t_sticker.draw(p, owner, context, outerWidth);\n\tif (_sticker.resolved()) {\n\t\tpaintBadge(p, context);\n\t}\n}\n\nTextState StickerWithBadgePart::textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const {\n\treturn _sticker.textState(point, request, outerWidth);\n}\n\nbool StickerWithBadgePart::hasHeavyPart() {\n\treturn _sticker.hasHeavyPart();\n}\n\nvoid StickerWithBadgePart::unloadHeavyPart() {\n\t_sticker.unloadHeavyPart();\n}\n\nstd::unique_ptr<StickerPlayer> StickerWithBadgePart::stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) {\n\treturn _sticker.stickerTakePlayer(data, replacements);\n}\n\nQSize StickerWithBadgePart::countOptimalSize() {\n\t_sticker.initDimensions();\n\treturn { _sticker.maxWidth(), _sticker.minHeight() };\n}\n\nQSize StickerWithBadgePart::countCurrentSize(int newWidth) {\n\treturn _sticker.countCurrentSize(newWidth);\n}\n\nvoid StickerWithBadgePart::paintBadge(\n\t\tPainter &p,\n\t\tconst PaintContext &context) const {\n\tvalidateBadge(context);\n\n\tconst auto badge = _badge.size() / _badge.devicePixelRatio();\n\tconst auto left = (width() - badge.width()) / 2;\n\tconst auto top = st::chatGiveawayBadgeTop;\n\tconst auto rect = QRect(left, top, badge.width(), badge.height());\n\tconst auto paintContent = [&](QPainter &q) {\n\t\tq.drawImage(rect.topLeft(), _badge);\n\t};\n\n\t{\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tif (_colorOverride) {\n\t\t\tp.setBrush(*_colorOverride);\n\t\t} else {\n\t\t\tp.setBrush(context.messageStyle()->msgFileBg);\n\t\t}\n\t\tconst auto half = st::chatGiveawayBadgeStroke / 2.;\n\t\tconst auto inner = QRectF(rect) - Margins(half);\n\t\tconst auto radius = inner.height() / 2.;\n\t\tp.drawRoundedRect(inner, radius, radius);\n\t\tif (_colorOverride && context.selected()) {\n\t\t\tp.setBrush(context.st->msgStickerOverlay());\n\t\t\tp.drawRoundedRect(inner, radius, radius);\n\t\t}\n\t}\n\n\tif (!_sticker.parent()->usesBubblePattern(context)) {\n\t\tpaintContent(p);\n\t} else {\n\t\tUi::PaintPatternBubblePart(\n\t\t\tp,\n\t\t\tcontext.viewport,\n\t\t\tcontext.bubblesPattern->pixmap,\n\t\t\trect,\n\t\t\tpaintContent,\n\t\t\t_badgeCache);\n\t}\n}\n\nvoid StickerWithBadgePart::validateBadge(\n\t\tconst PaintContext &context) const {\n\tconst auto stm = context.messageStyle();\n\tconst auto &badgeFg = st::premiumButtonFg->c;\n\tconst auto &badgeBorder = stm->msgBg->c;\n\tif (!_badge.isNull()\n\t\t&& _badgeFg == badgeFg\n\t\t&& _badgeBorder == badgeBorder) {\n\t\treturn;\n\t}\n\tconst auto &font = st::chatGiveawayBadgeFont;\n\t_badgeFg = badgeFg;\n\t_badgeBorder = badgeBorder;\n\tconst auto iconWidth = _customLeftIcon.isNull()\n\t\t? 0\n\t\t: (_customLeftIcon.width() / style::DevicePixelRatio());\n\tconst auto width = font->width(_badgeText) + iconWidth;\n\tconst auto inner = QRect(0, 0, width, font->height);\n\tconst auto rect = inner + st::chatGiveawayBadgePadding;\n\tconst auto size = rect.size();\n\tconst auto ratio = style::DevicePixelRatio();\n\t_badge = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied);\n\t_badge.setDevicePixelRatio(ratio);\n\t_badge.fill(Qt::transparent);\n\n\tauto p = QPainter(&_badge);\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setPen(QPen(_badgeBorder, st::chatGiveawayBadgeStroke * 1.));\n\tp.setBrush(Qt::NoBrush);\n\tconst auto half = st::chatGiveawayBadgeStroke / 2.;\n\tconst auto left = _customLeftIcon.isNull()\n\t\t? st::chatGiveawayBadgePadding.left()\n\t\t: (st::chatGiveawayBadgePadding.left() - half * 2);\n\tconst auto smaller = QRectF(rect.translated(-rect.topLeft()))\n\t\t- Margins(half);\n\tconst auto radius = smaller.height() / 2.;\n\tp.drawRoundedRect(smaller, radius, radius);\n\tp.setPen(_badgeFg);\n\tp.setFont(font);\n\tp.drawText(\n\t\tleft + iconWidth,\n\t\tst::chatGiveawayBadgePadding.top() + font->ascent,\n\t\t_badgeText);\n\tif (!_customLeftIcon.isNull()) {\n\t\tconst auto iconHeight = _customLeftIcon.height()\n\t\t\t/ style::DevicePixelRatio();\n\t\tp.drawImage(\n\t\t\tleft,\n\t\t\thalf + (inner.height() - iconHeight) / 2,\n\t\t\tcontext.selected()\n\t\t\t\t? Images::Colored(base::duplicate(_customLeftIcon), _badgeFg)\n\t\t\t\t: _customLeftIcon);\n\t}\n}\n\nPeerBubbleListPart::PeerBubbleListPart(\n\tnot_null<Element*> parent,\n\tconst std::vector<not_null<PeerData*>> &list)\n: _parent(parent) {\n\tfor (const auto &peer : list) {\n\t\t_peers.push_back({\n\t\t\t.name = Ui::Text::String(\n\t\t\t\tst::semiboldTextStyle,\n\t\t\t\tpeer->name(),\n\t\t\t\tkDefaultTextOptions,\n\t\t\t\tst::msgMinWidth),\n\t\t\t.thumbnail = Ui::MakeUserpicThumbnail(peer),\n\t\t\t.link = peer->openLink(),\n\t\t\t.colorIndex = peer->colorIndex(),\n\t\t});\n\t}\n}\n\nPeerBubbleListPart::~PeerBubbleListPart() = default;\n\nvoid PeerBubbleListPart::draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const {\n\tif (_peers.empty()) {\n\t\treturn;\n\t}\n\n\tconst auto size = _peers[0].geometry.height();\n\tconst auto st = context.st;\n\tconst auto stm = context.messageStyle();\n\tconst auto selected = context.selected();\n\tconst auto padding = st::chatGiveawayPeerPadding;\n\tfor (const auto &peer : _peers) {\n\t\tconst auto &thumbnail = peer.thumbnail;\n\t\tconst auto &geometry = peer.geometry;\n\t\tif (!_subscribed) {\n\t\t\tthumbnail->subscribeToUpdates([=] { _parent->repaint(); });\n\t\t}\n\n\t\tconst auto colorIndex = peer.colorIndex;\n\t\tconst auto cache = context.outbg\n\t\t\t? stm->replyCache[st->colorPatternIndex(colorIndex)].get()\n\t\t\t: st->coloredReplyCache(selected, colorIndex).get();\n\t\tif (peer.corners[0].isNull() || peer.bg != cache->bg) {\n\t\t\tpeer.bg = cache->bg;\n\t\t\tpeer.corners = Images::CornersMask(size / 2);\n\t\t\tfor (auto &image : peer.corners) {\n\t\t\t\tstyle::colorizeImage(image, cache->bg, &image);\n\t\t\t}\n\t\t}\n\t\tp.setPen(cache->icon);\n\t\tUi::DrawRoundedRect(p, geometry, peer.bg, peer.corners);\n\t\tif (peer.ripple) {\n\t\t\tpeer.ripple->paint(\n\t\t\t\tp,\n\t\t\t\tgeometry.x(),\n\t\t\t\tgeometry.y(),\n\t\t\t\twidth(),\n\t\t\t\t&cache->bg);\n\t\t\tif (peer.ripple->empty()) {\n\t\t\t\tpeer.ripple = nullptr;\n\t\t\t}\n\t\t}\n\n\t\tp.drawImage(geometry.topLeft(), thumbnail->image(size));\n\t\tconst auto left = size + padding.left();\n\t\tconst auto top = padding.top();\n\t\tconst auto available = geometry.width() - left - padding.right();\n\t\tpeer.name.draw(p, {\n\t\t\t.position = { geometry.left() + left, geometry.top() + top },\n\t\t\t.outerWidth = width(),\n\t\t\t.availableWidth = available,\n\t\t\t.align = style::al_left,\n\t\t\t.palette = &stm->textPalette,\n\t\t\t.now = context.now,\n\t\t\t.elisionLines = 1,\n\t\t\t.elisionBreakEverywhere = true,\n\t\t});\n\t}\n\t_subscribed = true;\n}\n\nint PeerBubbleListPart::layout(int x, int y, int available) {\n\tconst auto size = st::chatGiveawayPeerSize;\n\tconst auto skip = st::chatGiveawayPeerSkip;\n\tconst auto padding = st::chatGiveawayPeerPadding;\n\tauto left = available;\n\tconst auto shiftRow = [&](int i, int top, int shift) {\n\t\tfor (auto j = i; j != 0; --j) {\n\t\t\tauto &geometry = _peers[j - 1].geometry;\n\t\t\tif (geometry.top() != top) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tgeometry.moveLeft(geometry.x() + shift);\n\t\t}\n\t};\n\tconst auto count = int(_peers.size());\n\tfor (auto i = 0; i != count; ++i) {\n\t\tconst auto desired = size\n\t\t\t+ padding.left()\n\t\t\t+ _peers[i].name.maxWidth()\n\t\t\t+ padding.right();\n\t\tconst auto width = std::min(desired, available);\n\t\tif (left < width) {\n\t\t\tshiftRow(i, y, (left + skip) / 2);\n\t\t\tleft = available;\n\t\t\ty += size + skip;\n\t\t}\n\t\t_peers[i].geometry = { x + available - left, y, width, size };\n\t\tleft -= width + skip;\n\t}\n\tshiftRow(count, y, (left + skip) / 2);\n\treturn y + size + skip;\n}\n\nTextState PeerBubbleListPart::textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const {\n\tauto result = TextState(_parent);\n\tfor (const auto &peer : _peers) {\n\t\tif (peer.geometry.contains(point)) {\n\t\t\tresult.link = peer.link;\n\t\t\t_lastPoint = point;\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid PeerBubbleListPart::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed) {\n\tfor (auto &peer : _peers) {\n\t\tif (peer.link != p) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (pressed) {\n\t\t\tif (!peer.ripple) {\n\t\t\t\tpeer.ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\tUi::RippleAnimation::RoundRectMask(\n\t\t\t\t\tpeer.geometry.size(),\n\t\t\t\t\tpeer.geometry.height() / 2),\n\t\t\t\t\t[=] { _parent->repaint(); });\n\t\t\t}\n\t\t\tpeer.ripple->add(_lastPoint - peer.geometry.topLeft());\n\t\t} else if (peer.ripple) {\n\t\t\tpeer.ripple->lastStop();\n\t\t}\n\t\tbreak;\n\t}\n}\n\nbool PeerBubbleListPart::hasHeavyPart() {\n\treturn _subscribed;\n}\n\nvoid PeerBubbleListPart::unloadHeavyPart() {\n\tif (_subscribed) {\n\t\t_subscribed = false;\n\t\tfor (const auto &peer : _peers) {\n\t\t\tpeer.thumbnail->subscribeToUpdates(nullptr);\n\t\t}\n\t}\n}\n\nQSize PeerBubbleListPart::countOptimalSize() {\n\tif (_peers.empty()) {\n\t\treturn {};\n\t}\n\tconst auto size = st::chatGiveawayPeerSize;\n\tconst auto skip = st::chatGiveawayPeerSkip;\n\tconst auto padding = st::chatGiveawayPeerPadding;\n\tauto left = st::msgPadding.left();\n\tfor (const auto &peer : _peers) {\n\t\tconst auto desired = size\n\t\t\t+ padding.left()\n\t\t\t+ peer.name.maxWidth()\n\t\t\t+ padding.right();\n\t\tleft += desired + skip;\n\t}\n\treturn { left - skip + st::msgPadding.right(), size };\n}\n\nQSize PeerBubbleListPart::countCurrentSize(int newWidth) {\n\tif (_peers.empty()) {\n\t\treturn {};\n\t}\n\tconst auto padding = st::msgPadding;\n\tconst auto available = newWidth - padding.left() - padding.right();\n\tconst auto channelsBottom = layout(\n\t\tpadding.left(),\n\t\t0,\n\t\tavailable);\n\treturn { newWidth, channelsBottom };\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_media_generic.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n#include \"history/view/media/history_view_sticker.h\"\n\nnamespace Ui {\nclass DynamicImage;\nclass RippleAnimation;\n} // namespace Ui\n\nnamespace style {\nstruct TextStyle;\n} // namespace style\n\nnamespace st {\nextern const style::TextStyle &defaultTextStyle;\n} // namespace st\n\nnamespace HistoryView {\n\nclass MediaGeneric;\n\nclass MediaGenericPart : public Object {\npublic:\n\tusing PaintBg = Fn<void(\n\t\tPainter&,\n\t\tconst PaintContext&,\n\t\tnot_null<const MediaGeneric*>)>;\n\tusing PaintBgFactory = Fn<PaintBg()>;\n\n\tvirtual ~MediaGenericPart() = default;\n\n\tvirtual void draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const = 0;\n\t[[nodiscard]] virtual TextState textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const;\n\tvirtual void clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed);\n\t[[nodiscard]] virtual bool hasHeavyPart();\n\tvirtual void unloadHeavyPart();\n\t[[nodiscard]] virtual auto stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements\n\t) -> std::unique_ptr<StickerPlayer>;\n};\n\nstruct MediaGenericDescriptor {\n\tint maxWidth = 0;\n\tMediaGenericPart::PaintBgFactory paintBgFactory;\n\tClickHandlerPtr fullAreaLink;\n\tbool expandCurrentWidth = false;\n\tbool service = false;\n\tbool hideServiceText = false;\n};\n\nclass MediaGeneric final : public Media {\npublic:\n\tusing Part = MediaGenericPart;\n\n\tMediaGeneric(\n\t\tnot_null<Element*> parent,\n\t\tFn<void(\n\t\t\tnot_null<MediaGeneric*>,\n\t\t\tFn<void(std::unique_ptr<Part>)>)> generate,\n\t\tMediaGenericDescriptor &&descriptor = {});\n\t~MediaGeneric();\n\n\t[[nodiscard]] bool service() const {\n\t\treturn _service;\n\t}\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\n\tvoid clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool active) override;\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed) override;\n\n\tbool needsBubble() const override {\n\t\treturn !_service;\n\t}\n\tbool customInfoLayout() const override {\n\t\treturn false;\n\t}\n\n\tstd::unique_ptr<StickerPlayer> stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) override;\n\n\tbool toggleSelectionByHandlerClick(\n\t\tconst ClickHandlerPtr &p) const override {\n\t\treturn true;\n\t}\n\tbool dragItemByHandler(const ClickHandlerPtr &p) const override {\n\t\treturn true;\n\t}\n\n\tbool hideFromName() const override;\n\tbool hideServiceText() const override;\n\n\tvoid unloadHeavyPart() override;\n\tbool hasHeavyPart() const override;\n\nprivate:\n\tstruct Entry {\n\t\tstd::unique_ptr<Part> object;\n\t};\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\n\t[[nodiscard]] QMargins inBubblePadding() const;\n\n\tstd::vector<Entry> _entries;\n\tPart::PaintBgFactory _paintBgFactory;\n\tmutable Part::PaintBg _paintBg;\n\tClickHandlerPtr _fullAreaLink;\n\tint _maxWidthCap = 0;\n\tbool _expandCurrentWidth : 1 = false;\n\tbool _service : 1 = false;\n\tbool _hideServiceText : 1 = false;\n\n};\n\nclass MediaGenericTextPart : public MediaGenericPart {\npublic:\n\tMediaGenericTextPart(\n\t\tTextWithEntities text,\n\t\tQMargins margins,\n\t\tconst style::TextStyle &st = st::defaultTextStyle,\n\t\tconst base::flat_map<uint16, ClickHandlerPtr> &links = {},\n\t\tconst Ui::Text::MarkedContext &context = {},\n\t\tstyle::align align = style::al_top);\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const override;\n\tTextState textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const override;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\nprotected:\n\tvirtual void setupPen(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context) const;\n\tvirtual int elisionLines() const;\n\nprivate:\n\tUi::Text::String _text;\n\tQMargins _margins;\n\tstyle::align _align = {};\n\n};\n\nclass TextDelimeterPart final : public MediaGenericPart {\npublic:\n\tTextDelimeterPart(const QString &text, QMargins margins);\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const override;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\nprivate:\n\tUi::Text::String _text;\n\tQMargins _margins;\n\n};\n\nclass LambdaGenericPart final : public MediaGenericPart {\npublic:\n\tLambdaGenericPart(\n\t\tQSize size,\n\t\tFn<void(\n\t\t\tPainter &p,\n\t\t\tnot_null<const MediaGeneric*> owner,\n\t\t\tconst PaintContext &context,\n\t\t\tint outerWidth)> draw);\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const override;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\nprivate:\n\tQSize _size;\n\tFn<void(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth)> _draw;\n\n};\n\nclass StickerInBubblePart final : public MediaGenericPart {\npublic:\n\tstruct Data {\n\t\tDocumentData *sticker = nullptr;\n\t\tint skipTop = 0;\n\t\tint size = 0;\n\t\tChatHelpers::StickerLottieSize cacheTag = {};\n\t\tbool stopOnLastFrame = false;\n\t\tClickHandlerPtr link;\n\n\t\texplicit operator bool() const {\n\t\t\treturn sticker != nullptr;\n\t\t}\n\t};\n\tStickerInBubblePart(\n\t\tnot_null<Element*> parent,\n\t\tElement *replacing,\n\t\tFn<Data()> lookup,\n\t\tQMargins padding);\n\n\t[[nodiscard]] not_null<Element*> parent() const {\n\t\treturn _parent;\n\t}\n\t[[nodiscard]] bool resolved() const {\n\t\treturn _sticker.has_value();\n\t}\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const override;\n\tTextState textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const override;\n\tbool hasHeavyPart() override;\n\tvoid unloadHeavyPart() override;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\n\tstd::unique_ptr<StickerPlayer> stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) override;\n\nprivate:\n\tvoid ensureCreated(Element *replacing = nullptr) const;\n\n\tconst not_null<Element*> _parent;\n\tFn<Data()> _lookup;\n\tmutable int _skipTop = 0;\n\tmutable QMargins _padding;\n\tmutable std::optional<Sticker> _sticker;\n\tmutable ClickHandlerPtr _link;\n\n};\n\nclass StickerWithBadgePart final : public MediaGenericPart {\npublic:\n\tusing Data = StickerInBubblePart::Data;\n\tStickerWithBadgePart(\n\t\tnot_null<Element*> parent,\n\t\tElement *replacing,\n\t\tFn<Data()> lookup,\n\t\tQMargins padding,\n\t\tQString badge,\n\t\tQImage customLeftIcon,\n\t\tstd::optional<QColor> colorOverride);\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const override;\n\tTextState textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const override;\n\tbool hasHeavyPart() override;\n\tvoid unloadHeavyPart() override;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\n\tstd::unique_ptr<StickerPlayer> stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) override;\n\nprivate:\n\tvoid validateBadge(const PaintContext &context) const;\n\tvoid paintBadge(Painter &p, const PaintContext &context) const;\n\n\tconst QImage _customLeftIcon;\n\tStickerInBubblePart _sticker;\n\tQString _badgeText;\n\tmutable QColor _badgeFg;\n\tmutable QColor _badgeBorder;\n\tmutable QImage _badge;\n\tmutable QImage _badgeCache;\n\tstd::optional<QColor> _colorOverride;\n\n};\n\nclass PeerBubbleListPart final : public MediaGenericPart {\npublic:\n\tPeerBubbleListPart(\n\t\tnot_null<Element*> parent,\n\t\tconst std::vector<not_null<PeerData*>> &list);\n\t~PeerBubbleListPart();\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const override;\n\tTextState textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const override;\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed) override;\n\tbool hasHeavyPart() override;\n\tvoid unloadHeavyPart() override;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\nprivate:\n\tint layout(int x, int y, int available);\n\n\tstruct Peer {\n\t\tUi::Text::String name;\n\t\tstd::shared_ptr<Ui::DynamicImage> thumbnail;\n\t\tQRect geometry;\n\t\tClickHandlerPtr link;\n\t\tmutable std::unique_ptr<Ui::RippleAnimation> ripple;\n\t\tmutable std::array<QImage, 4> corners;\n\t\tmutable QColor bg;\n\t\tuint8 colorIndex = 0;\n\t};\n\n\tconst not_null<Element*> _parent;\n\tstd::vector<Peer> _peers;\n\tmutable QPoint _lastPoint;\n\tmutable bool _subscribed = false;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_media_grouped.h\"\n\n#include \"history/history_item_components.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"data/data_document.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_session.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"lang/lang_keys.h\"\n#include \"media/streaming/media_streaming_utility.h\"\n#include \"ui/grouped_layout.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/message_bubble.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"layout/layout_selection.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nstd::vector<Ui::GroupMediaLayout> LayoutPlaylist(\n\t\tconst std::vector<QSize> &sizes) {\n\tExpects(!sizes.empty());\n\n\tauto result = std::vector<Ui::GroupMediaLayout>();\n\tresult.reserve(sizes.size());\n\tconst auto width = ranges::max_element(\n\t\tsizes,\n\t\tstd::less<>(),\n\t\t&QSize::width)->width();\n\tauto top = 0;\n\tfor (const auto &size : sizes) {\n\t\tresult.push_back({\n\t\t\t.geometry = QRect(0, top, width, size.height()),\n\t\t\t.sides = RectPart::Left | RectPart::Right\n\t\t});\n\t\ttop += size.height();\n\t}\n\tresult.front().sides |= RectPart::Top;\n\tresult.back().sides |= RectPart::Bottom;\n\treturn result;\n}\n\n} // namespace\n\nGroupedMedia::Part::Part(\n\tnot_null<Element*> parent,\n\tnot_null<Data::Media*> media)\n: item(media->parent())\n, content(media->createView(parent, item)) {\n\tAssert(media->canBeGrouped());\n}\n\nGroupedMedia::GroupedMedia(\n\tnot_null<Element*> parent,\n\tconst std::vector<std::unique_ptr<Data::Media>> &medias)\n: Media(parent) {\n\tconst auto truncated = ranges::views::all(\n\t\tmedias\n\t) | ranges::views::transform([](const std::unique_ptr<Data::Media> &v) {\n\t\treturn v.get();\n\t}) | ranges::views::take(kMaxSize);\n\tconst auto result = applyGroup(truncated);\n\n\tEnsures(result);\n}\n\nGroupedMedia::GroupedMedia(\n\tnot_null<Element*> parent,\n\tconst std::vector<not_null<HistoryItem*>> &items)\n: Media(parent) {\n\tconst auto medias = ranges::views::all(\n\t\titems\n\t) | ranges::views::transform([](not_null<HistoryItem*> item) {\n\t\treturn item->media();\n\t}) | ranges::views::take(kMaxSize);\n\tconst auto result = applyGroup(medias);\n\n\tEnsures(result);\n}\n\nGroupedMedia::~GroupedMedia() {\n\t// Destroy all parts while the media object is still not destroyed.\n\tbase::take(_parts);\n}\n\nHistoryItem *GroupedMedia::itemForText() const {\n\tif (_mode == Mode::Column) {\n\t\treturn Media::itemForText();\n\t} else if (!_captionItem) {\n\t\t_captionItem = [&]() -> HistoryItem* {\n\t\t\tauto result = (HistoryItem*)nullptr;\n\t\t\tfor (const auto &part : _parts) {\n\t\t\t\tif (!part.item->emptyText()) {\n\t\t\t\t\tif (result == part.item) {\n\t\t\t\t\t\t// All parts are from the same message, that means\n\t\t\t\t\t\t// this is an album with a single item, single text.\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t} else if (result) {\n\t\t\t\t\t\treturn nullptr;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresult = part.item;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn result;\n\t\t}();\n\t}\n\treturn *_captionItem;\n}\n\nbool GroupedMedia::hideMessageText() const {\n\treturn (_mode == Mode::Column);\n}\n\nGroupedMedia::Mode GroupedMedia::DetectMode(not_null<Data::Media*> media) {\n\tconst auto document = media->document();\n\treturn (document && !document->isVideoFile())\n\t\t? Mode::Column\n\t\t: Mode::Grid;\n}\n\nQSize GroupedMedia::countOptimalSize() {\n\t_purchasedPriceTag = hasPurchasedTag();\n\n\tstd::vector<QSize> sizes;\n\tconst auto partsCount = _parts.size();\n\tsizes.reserve(partsCount);\n\tauto maxWidth = 0;\n\tif (_mode == Mode::Column) {\n\t\tfor (const auto &part : _parts) {\n\t\t\tconst auto &media = part.content;\n\t\t\tmedia->setBubbleRounding(bubbleRounding());\n\t\t\tmedia->initDimensions();\n\t\t\taccumulate_max(maxWidth, media->maxWidth());\n\t\t}\n\t}\n\tauto index = 0;\n\tfor (const auto &part : _parts) {\n\t\tconst auto last = (++index == _parts.size());\n\t\tsizes.push_back(\n\t\t\tpart.content->sizeForGroupingOptimal(maxWidth, last));\n\t}\n\n\tconst auto layout = (_mode == Mode::Grid)\n\t\t? Ui::LayoutMediaGroup(\n\t\t\tsizes,\n\t\t\tst::historyGroupWidthMax,\n\t\t\tst::historyGroupWidthMin,\n\t\t\tst::historyGroupSkip)\n\t\t: LayoutPlaylist(sizes);\n\tAssert(layout.size() == _parts.size());\n\n\tauto minHeight = 0;\n\tfor (auto i = 0, count = int(layout.size()); i != count; ++i) {\n\t\tconst auto &item = layout[i];\n\t\taccumulate_max(maxWidth, item.geometry.x() + item.geometry.width());\n\t\taccumulate_max(minHeight, item.geometry.y() + item.geometry.height());\n\t\t_parts[i].initialGeometry = item.geometry;\n\t\t_parts[i].sides = item.sides;\n\t}\n\n\tif (_mode == Mode::Column\n\t\t&& isBubbleBottom()\n\t\t&& _parts.back().item->emptyText()) {\n\t\tconst auto item = _parent->data();\n\t\tconst auto msgsigned = item->Get<HistoryMessageSigned>();\n\t\tconst auto views = item->Get<HistoryMessageViews>();\n\t\tif ((msgsigned && !msgsigned->isAnonymousRank)\n\t\t\t|| (views\n\t\t\t\t&& (views->views.count >= 0 || views->replies.count > 0))\n\t\t\t|| displayedEditBadge()) {\n\t\t\tminHeight += st::msgDateFont->height - st::msgDateDelta.y();\n\t\t}\n\t}\n\n\tconst auto groupPadding = groupedPadding();\n\tminHeight += groupPadding.top() + groupPadding.bottom();\n\n\treturn { maxWidth, minHeight };\n}\n\nQSize GroupedMedia::countCurrentSize(int newWidth) {\n\taccumulate_min(newWidth, maxWidth());\n\tauto newHeight = 0;\n\tif (_mode == Mode::Grid && newWidth < st::historyGroupWidthMin) {\n\t\treturn { newWidth, newHeight };\n\t} else if (_mode == Mode::Column) {\n\t\tauto top = 0;\n\t\tfor (auto &part : _parts) {\n\t\t\tconst auto size = part.content->sizeForGrouping(newWidth);\n\t\t\tpart.geometry = QRect(0, top, newWidth, size.height());\n\t\t\ttop += size.height();\n\t\t}\n\t\tnewHeight = top;\n\t} else {\n\t\tconst auto initialSpacing = st::historyGroupSkip;\n\t\tconst auto factor = newWidth / float64(maxWidth());\n\t\tconst auto scale = [&](int value) {\n\t\t\treturn int(base::SafeRound(value * factor));\n\t\t};\n\t\tconst auto spacing = scale(initialSpacing);\n\t\tfor (auto &part : _parts) {\n\t\t\tconst auto sides = part.sides;\n\t\t\tconst auto initialGeometry = part.initialGeometry;\n\t\t\tconst auto needRightSkip = !(sides & RectPart::Right);\n\t\t\tconst auto needBottomSkip = !(sides & RectPart::Bottom);\n\t\t\tconst auto initialLeft = initialGeometry.x();\n\t\t\tconst auto initialTop = initialGeometry.y();\n\t\t\tconst auto initialRight = initialLeft\n\t\t\t\t+ initialGeometry.width()\n\t\t\t\t+ (needRightSkip ? initialSpacing : 0);\n\t\t\tconst auto initialBottom = initialTop\n\t\t\t\t+ initialGeometry.height()\n\t\t\t\t+ (needBottomSkip ? initialSpacing : 0);\n\t\t\tconst auto left = scale(initialLeft);\n\t\t\tconst auto top = scale(initialTop);\n\t\t\tconst auto width = scale(initialRight)\n\t\t\t\t- left\n\t\t\t\t- (needRightSkip ? spacing : 0);\n\t\t\tconst auto height = scale(initialBottom)\n\t\t\t\t- top\n\t\t\t\t- (needBottomSkip ? spacing : 0);\n\t\t\tpart.geometry = QRect(left, top, width, height);\n\n\t\t\taccumulate_max(newHeight, top + height);\n\t\t}\n\t}\n\tif (_mode == Mode::Column\n\t\t&& isBubbleBottom()\n\t\t&& _parts.back().item->emptyText()) {\n\t\tconst auto item = _parent->data();\n\t\tconst auto msgsigned = item->Get<HistoryMessageSigned>();\n\t\tconst auto views = item->Get<HistoryMessageViews>();\n\t\tif ((msgsigned && !msgsigned->isAnonymousRank)\n\t\t\t|| (views\n\t\t\t\t&& (views->views.count >= 0 || views->replies.count > 0))\n\t\t\t|| displayedEditBadge()) {\n\t\t\tnewHeight += st::msgDateFont->height - st::msgDateDelta.y();\n\t\t}\n\t}\n\n\tconst auto groupPadding = groupedPadding();\n\tnewHeight += groupPadding.top() + groupPadding.bottom();\n\n\treturn { newWidth, newHeight };\n}\n\nvoid GroupedMedia::refreshParentId(\n\t\tnot_null<HistoryItem*> realParent) {\n\tfor (const auto &part : _parts) {\n\t\tpart.content->refreshParentId(part.item);\n\t}\n}\n\nUi::BubbleRounding GroupedMedia::applyRoundingSides(\n\t\tUi::BubbleRounding already,\n\t\tRectParts sides) const {\n\tauto result = Ui::GetCornersFromSides(sides);\n\tif (!(result & RectPart::TopLeft)) {\n\t\talready.topLeft = Ui::BubbleCornerRounding::None;\n\t}\n\tif (!(result & RectPart::TopRight)) {\n\t\talready.topRight = Ui::BubbleCornerRounding::None;\n\t}\n\tif (!(result & RectPart::BottomLeft)) {\n\t\talready.bottomLeft = Ui::BubbleCornerRounding::None;\n\t}\n\tif (!(result & RectPart::BottomRight)) {\n\t\talready.bottomRight = Ui::BubbleCornerRounding::None;\n\t}\n\treturn already;\n}\n\nQMargins GroupedMedia::groupedPadding() const {\n\tif (_mode != Mode::Column) {\n\t\treturn QMargins();\n\t}\n\tconst auto normal = st::msgFileLayout.padding;\n\tconst auto grouped = st::msgFileLayoutGrouped.padding;\n\tconst auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus;\n\tconst auto lastHasCaption = isBubbleBottom()\n\t\t&& !_parts.back().item->emptyText();\n\tconst auto addToBottom = lastHasCaption ? st::msgPadding.bottom() : 0;\n\treturn QMargins(\n\t\t0,\n\t\t(normal.top() - grouped.top()) - topMinus,\n\t\t0,\n\t\t(normal.bottom() - grouped.bottom()) + addToBottom);\n}\n\nQRect GroupedMedia::groupItemRect(int index) const {\n\tif (index >= 0 && index < int(_parts.size())) {\n\t\treturn _parts[index].geometry.translated(\n\t\t\t0,\n\t\t\tgroupedPadding().top());\n\t}\n\treturn {};\n}\n\nMedia *GroupedMedia::lookupSpoilerTagMedia() const {\n\tif (_parts.empty()) {\n\t\treturn nullptr;\n\t}\n\tconst auto media = _parts.front().content.get();\n\tif (media && _parts.front().item->isMediaSensitive()) {\n\t\treturn media;\n\t}\n\tconst auto photo = media ? media->getPhoto() : nullptr;\n\treturn (photo && photo->extendedMediaPreview()) ? media : nullptr;\n}\n\nQImage GroupedMedia::generateSpoilerTagBackground(QRect full) const {\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto result = QImage(\n\t\tfull.size() * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\tauto p = QPainter(&result);\n\tconst auto shift = -full.topLeft();\n\tconst auto skip1 = st::historyGroupSkip / 2;\n\tconst auto skip2 = st::historyGroupSkip - skip1;\n\tfor (const auto &part : _parts) {\n\t\tauto background = part.content->spoilerTagBackground();\n\t\tconst auto extended = part.geometry.translated(shift).marginsAdded(\n\t\t\t{ skip1, skip1, skip2, skip2 });\n\t\tif (background.isNull()) {\n\t\t\tp.fillRect(extended, Qt::black);\n\t\t} else {\n\t\t\tp.drawImage(extended, background);\n\t\t}\n\t}\n\tp.end();\n\n\treturn ::Media::Streaming::PrepareBlurredBackground(\n\t\tfull.size(),\n\t\tstd::move(result));\n}\n\nvoid GroupedMedia::drawHighlight(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint top) const {\n\tif (context.highlight.opacity == 0.) {\n\t\treturn;\n\t}\n\tauto selection = context.highlight.range;\n\tif (_mode != Mode::Column) {\n\t\tif (!selection.empty() && !IsSubGroupSelection(selection)) {\n\t\t\t_parent->paintCustomHighlight(\n\t\t\t\tp,\n\t\t\t\tcontext,\n\t\t\t\ttop,\n\t\t\t\theight(),\n\t\t\t\t_parent->data().get());\n\t\t}\n\t\treturn;\n\t}\n\tconst auto empty = selection.empty();\n\tconst auto subpart = IsSubGroupSelection(selection);\n\tconst auto skip = top + groupedPadding().top();\n\tfor (auto i = 0, count = int(_parts.size()); i != count; ++i) {\n\t\tconst auto &part = _parts[i];\n\t\tconst auto rect = part.geometry.translated(0, skip);\n\t\tconst auto full = (!i && empty)\n\t\t\t|| (subpart && IsGroupItemSelection(selection, i))\n\t\t\t|| (!subpart\n\t\t\t\t&& !selection.empty()\n\t\t\t\t&& (selection.from < part.content->fullSelectionLength()));\n\t\tif (!subpart) {\n\t\t\tselection = part.content->skipSelection(selection);\n\t\t}\n\t\tif (full) {\n\t\t\tauto copy = context;\n\t\t\tcopy.highlight.range = {};\n\t\t\t_parent->paintCustomHighlight(\n\t\t\t\tp,\n\t\t\t\tcopy,\n\t\t\t\trect.y(),\n\t\t\t\trect.height(),\n\t\t\t\tpart.item);\n\t\t}\n\t}\n}\n\nvoid GroupedMedia::draw(Painter &p, const PaintContext &context) const {\n\tauto wasCache = false;\n\tauto nowCache = false;\n\tconst auto groupPadding = groupedPadding();\n\tauto selection = context.selection;\n\tconst auto fullSelection = (selection == FullSelection);\n\tconst auto textSelection = (_mode == Mode::Column)\n\t\t&& !fullSelection\n\t\t&& !IsSubGroupSelection(selection);\n\tconst auto inWebPage = (_parent->media() != this);\n\tconstexpr auto kSmall = Ui::BubbleCornerRounding::Small;\n\tconst auto rounding = inWebPage\n\t\t? Ui::BubbleRounding{ kSmall, kSmall, kSmall, kSmall }\n\t\t: adjustedBubbleRounding();\n\tauto highlight = context.highlight.range;\n\tconst auto tagged = lookupSpoilerTagMedia();\n\tauto fullRect = QRect();\n\tconst auto subpartHighlight = IsSubGroupSelection(highlight);\n\tfor (auto i = 0, count = int(_parts.size()); i != count; ++i) {\n\t\tconst auto &part = _parts[i];\n\t\tauto partContext = context.withSelection(fullSelection\n\t\t\t? FullSelection\n\t\t\t: textSelection\n\t\t\t? selection\n\t\t\t: IsGroupItemSelection(selection, i)\n\t\t\t? FullSelection\n\t\t\t: TextSelection());\n\t\tconst auto highlighted = (highlight.empty() && !i)\n\t\t\t|| IsGroupItemSelection(highlight, i);\n\t\tconst auto highlightOpacity = highlighted\n\t\t\t? context.highlight.opacity\n\t\t\t: 0.;\n\t\tpartContext.highlight.range = highlighted\n\t\t\t? TextSelection()\n\t\t\t: highlight;\n\t\tif (textSelection) {\n\t\t\tselection = part.content->skipSelection(selection);\n\t\t}\n\t\tif (!subpartHighlight) {\n\t\t\thighlight = part.content->skipSelection(highlight);\n\t\t}\n\t\tif (!part.cache.isNull()) {\n\t\t\twasCache = true;\n\t\t}\n\t\tpart.content->drawGrouped(\n\t\t\tp,\n\t\t\tpartContext,\n\t\t\tpart.geometry.translated(0, groupPadding.top()),\n\t\t\tpart.sides,\n\t\t\tapplyRoundingSides(rounding, part.sides),\n\t\t\thighlightOpacity,\n\t\t\t&part.cacheKey,\n\t\t\t&part.cache);\n\t\tif (!part.cache.isNull()) {\n\t\t\tnowCache = true;\n\t\t}\n\t\tif (tagged || _purchasedPriceTag) {\n\t\t\tfullRect = fullRect.united(part.geometry);\n\t\t}\n\t}\n\tif (nowCache && !wasCache) {\n\t\thistory()->owner().registerHeavyViewPart(_parent);\n\t}\n\n\tif (tagged) {\n\t\ttagged->drawSpoilerTag(p, fullRect, context, [&] {\n\t\t\treturn generateSpoilerTagBackground(fullRect);\n\t\t});\n\t} else if (_purchasedPriceTag) {\n\t\tdrawPurchasedTag(p, fullRect, context);\n\t}\n\n\t// date\n\tif (_parent->media() == this && (!_parent->hasBubble() || isBubbleBottom())) {\n\t\tauto fullRight = width();\n\t\tauto fullBottom = height();\n\t\tif (needInfoDisplay()) {\n\t\t\t_parent->drawInfo(\n\t\t\t\tp,\n\t\t\t\tcontext,\n\t\t\t\tfullRight,\n\t\t\t\tfullBottom,\n\t\t\t\twidth(),\n\t\t\t\tInfoDisplayType::Image);\n\t\t}\n\t\tif (const auto size = _parent->hasBubble() ? std::nullopt : _parent->rightActionSize()) {\n\t\t\tauto fastShareLeft = _parent->hasRightLayout()\n\t\t\t\t? (-size->width() - st::historyFastShareLeft)\n\t\t\t\t: (fullRight + st::historyFastShareLeft);\n\t\t\tauto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());\n\t\t\t_parent->drawRightAction(p, context, fastShareLeft, fastShareTop, width());\n\t\t}\n\t}\n}\n\nTextState GroupedMedia::getPartState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tauto shift = 0;\n\tfor (const auto &part : _parts) {\n\t\tif (part.geometry.contains(point)) {\n\t\t\tauto result = part.content->getStateGrouped(\n\t\t\t\tpart.geometry,\n\t\t\t\tpart.sides,\n\t\t\t\tpoint,\n\t\t\t\trequest);\n\t\t\tresult.symbol += shift;\n\t\t\tresult.itemId = part.item->fullId();\n\t\t\treturn result;\n\t\t}\n\t\tshift += part.content->fullSelectionLength();\n\t}\n\treturn TextState(_parent->data());\n}\n\nPointState GroupedMedia::pointState(QPoint point) const {\n\tif (!QRect(0, 0, width(), height()).contains(point)) {\n\t\treturn PointState::Outside;\n\t}\n\tconst auto groupPadding = groupedPadding();\n\tpoint -= QPoint(0, groupPadding.top());\n\tfor (const auto &part : _parts) {\n\t\tif (part.geometry.contains(point)) {\n\t\t\treturn PointState::GroupPart;\n\t\t}\n\t}\n\treturn PointState::Inside;\n}\n\nTextState GroupedMedia::textState(QPoint point, StateRequest request) const {\n\tconst auto groupPadding = groupedPadding();\n\tauto result = getPartState(point - QPoint(0, groupPadding.top()), request);\n\tif (const auto tagged = lookupSpoilerTagMedia()) {\n\t\tif (QRect(0, 0, width(), height()).contains(point)) {\n\t\t\tif (auto link = tagged->spoilerTagLink()) {\n\t\t\t\tresult.link = std::move(link);\n\t\t\t}\n\t\t}\n\t}\n\tif (_parent->media() == this && (!_parent->hasBubble() || isBubbleBottom())) {\n\t\tauto fullRight = width();\n\t\tauto fullBottom = height();\n\t\tconst auto bottomInfoResult = _parent->bottomInfoTextState(\n\t\t\tfullRight,\n\t\t\tfullBottom,\n\t\t\tpoint,\n\t\t\tInfoDisplayType::Image);\n\t\tif (bottomInfoResult.link\n\t\t\t|| bottomInfoResult.cursor != CursorState::None\n\t\t\t|| bottomInfoResult.customTooltip) {\n\t\t\treturn bottomInfoResult;\n\t\t}\n\t\tif (const auto size = _parent->hasBubble() ? std::nullopt : _parent->rightActionSize()) {\n\t\t\tauto fastShareLeft = _parent->hasRightLayout()\n\t\t\t\t? (-size->width() - st::historyFastShareLeft)\n\t\t\t\t: (fullRight + st::historyFastShareLeft);\n\t\t\tauto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());\n\t\t\tif (QRect(fastShareLeft, fastShareTop, size->width(), size->height()).contains(point)) {\n\t\t\t\tresult.link = _parent->rightActionLink(point\n\t\t\t\t\t- QPoint(fastShareLeft, fastShareTop));\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nbool GroupedMedia::toggleSelectionByHandlerClick(\n\t\tconst ClickHandlerPtr &p) const {\n\tfor (const auto &part : _parts) {\n\t\tif (part.content->toggleSelectionByHandlerClick(p)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool GroupedMedia::dragItemByHandler(const ClickHandlerPtr &p) const {\n\tfor (const auto &part : _parts) {\n\t\tif (part.content->dragItemByHandler(p)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nTextSelection GroupedMedia::adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const {\n\tif (_mode != Mode::Column) {\n\t\treturn {};\n\t}\n\tauto checked = 0;\n\tfor (const auto &part : _parts) {\n\t\tconst auto modified = ShiftItemSelection(\n\t\t\tpart.content->adjustSelection(\n\t\t\t\tUnshiftItemSelection(selection, checked),\n\t\t\t\ttype),\n\t\t\tchecked);\n\t\tconst auto till = checked + part.content->fullSelectionLength();\n\t\tif (selection.from >= checked && selection.from < till) {\n\t\t\tselection.from = modified.from;\n\t\t}\n\t\tif (selection.to <= till) {\n\t\t\tselection.to = modified.to;\n\t\t\treturn selection;\n\t\t}\n\t\tchecked = till;\n\t}\n\treturn selection;\n}\n\nuint16 GroupedMedia::fullSelectionLength() const {\n\tif (_mode != Mode::Column) {\n\t\treturn {};\n\t}\n\tauto result = 0;\n\tfor (const auto &part : _parts) {\n\t\tresult += part.content->fullSelectionLength();\n\t}\n\treturn result;\n}\n\nbool GroupedMedia::hasTextForCopy() const {\n\tif (_mode != Mode::Column) {\n\t\treturn {};\n\t}\n\tfor (const auto &part : _parts) {\n\t\tif (part.content->hasTextForCopy()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nTextForMimeData GroupedMedia::selectedText(\n\t\tTextSelection selection) const {\n\tif (_mode != Mode::Column) {\n\t\treturn {};\n\t}\n\tauto result = TextForMimeData();\n\tfor (const auto &part : _parts) {\n\t\tauto text = part.content->selectedText(selection);\n\t\tif (!text.empty()) {\n\t\t\tif (result.empty()) {\n\t\t\t\tresult = std::move(text);\n\t\t\t} else {\n\t\t\t\tresult.append(u\"\\n\\n\"_q).append(std::move(text));\n\t\t\t}\n\t\t}\n\t\tselection = part.content->skipSelection(selection);\n\t}\n\treturn result;\n}\n\nSelectedQuote GroupedMedia::selectedQuote(TextSelection selection) const {\n\tif (_mode != Mode::Column) {\n\t\treturn {};\n\t}\n\tfor (const auto &part : _parts) {\n\t\tconst auto next = part.content->skipSelection(selection);\n\t\tif (next.to - next.from != selection.to - selection.from) {\n\t\t\tif (!next.empty()) {\n\t\t\t\treturn SelectedQuote();\n\t\t\t}\n\t\t\tauto result = part.content->selectedQuote(selection);\n\t\t\tresult.item = part.item;\n\t\t\treturn result;\n\t\t}\n\t\tselection = next;\n\t}\n\treturn {};\n}\n\nTextSelection GroupedMedia::selectionFromQuote(\n\t\tconst SelectedQuote &quote) const {\n\tExpects(quote.item != nullptr);\n\n\tif (_mode != Mode::Column) {\n\t\treturn {};\n\t}\n\tconst auto i = ranges::find(_parts, not_null(quote.item), &Part::item);\n\tif (i == end(_parts)) {\n\t\treturn {};\n\t}\n\tconst auto index = int(i - begin(_parts));\n\tauto result = i->content->selectionFromQuote(quote);\n\tif (result.empty()) {\n\t\treturn AddGroupItemSelection({}, index);\n\t}\n\tfor (auto j = i; j != begin(_parts);) {\n\t\tresult = (--j)->content->unskipSelection(result);\n\t}\n\treturn result;\n}\n\nauto GroupedMedia::getBubbleSelectionIntervals(\n\tTextSelection selection) const\n-> std::vector<Ui::BubbleSelectionInterval> {\n\tif (_mode != Mode::Column) {\n\t\treturn {};\n\t}\n\tauto result = std::vector<Ui::BubbleSelectionInterval>();\n\tfor (auto i = 0, count = int(_parts.size()); i != count; ++i) {\n\t\tconst auto &part = _parts[i];\n\t\tif (!IsGroupItemSelection(selection, i)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto &geometry = part.geometry;\n\t\tif (result.empty()\n\t\t\t|| (result.back().top + result.back().height\n\t\t\t\t< geometry.top())\n\t\t\t|| (result.back().top > geometry.top() + geometry.height())) {\n\t\t\tresult.push_back({ geometry.top(), geometry.height() });\n\t\t} else {\n\t\t\tauto &last = result.back();\n\t\t\tconst auto newTop = std::min(last.top, geometry.top());\n\t\t\tconst auto newHeight = std::max(\n\t\t\t\tlast.top + last.height - newTop,\n\t\t\t\tgeometry.top() + geometry.height() - newTop);\n\t\t\tlast = Ui::BubbleSelectionInterval{ newTop, newHeight };\n\t\t}\n\t}\n\tconst auto groupPadding = groupedPadding();\n\tfor (auto &part : result) {\n\t\tpart.top += groupPadding.top();\n\t}\n\tif (IsGroupItemSelection(selection, 0)) {\n\t\tresult.front().top -= groupPadding.top();\n\t\tresult.front().height += groupPadding.top();\n\t}\n\tif (IsGroupItemSelection(selection, _parts.size() - 1)) {\n\t\tresult.back().height = height() - result.back().top;\n\t}\n\treturn result;\n}\n\nvoid GroupedMedia::clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool active) {\n\tfor (const auto &part : _parts) {\n\t\tpart.content->clickHandlerActiveChanged(p, active);\n\t}\n}\n\nvoid GroupedMedia::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed) {\n\tfor (const auto &part : _parts) {\n\t\tpart.content->clickHandlerPressedChanged(p, pressed);\n\t\tif (pressed && part.content->dragItemByHandler(p)) {\n\t\t\t// #TODO drag by item from album\n\t\t\t// App::pressedLinkItem(part.view);\n\t\t}\n\t}\n}\n\ntemplate <typename DataMediaRange>\nbool GroupedMedia::applyGroup(const DataMediaRange &medias) {\n\tif (validateGroupParts(medias)) {\n\t\treturn true;\n\t}\n\n\tauto modeChosen = false;\n\tfor (const auto media : medias) {\n\t\tconst auto mediaMode = DetectMode(media);\n\t\tif (!modeChosen) {\n\t\t\t_mode = mediaMode;\n\t\t\tmodeChosen = true;\n\t\t} else if (mediaMode != _mode) {\n\t\t\tcontinue;\n\t\t}\n\t\t_parts.push_back(Part(_parent, media));\n\t}\n\tif (_parts.empty()) {\n\t\treturn false;\n\t}\n\n\tEnsures(_parts.size() <= kMaxSize);\n\treturn true;\n}\n\ntemplate <typename DataMediaRange>\nbool GroupedMedia::validateGroupParts(\n\t\tconst DataMediaRange &medias) const {\n\tauto i = 0;\n\tconst auto count = _parts.size();\n\tfor (const auto media : medias) {\n\t\tif (i >= count || _parts[i].item != media->parent()) {\n\t\t\treturn false;\n\t\t}\n\t\t++i;\n\t}\n\treturn (i == count);\n}\n\nnot_null<Media*> GroupedMedia::main() const {\n\tExpects(!_parts.empty());\n\n\treturn _parts.back().content.get();\n}\n\nvoid GroupedMedia::hideSpoilers() {\n\tfor (const auto &part : _parts) {\n\t\tpart.content->hideSpoilers();\n\t}\n}\n\nStorage::SharedMediaTypesMask GroupedMedia::sharedMediaTypes() const {\n\treturn main()->sharedMediaTypes();\n}\n\nPhotoData *GroupedMedia::getPhoto() const {\n\treturn main()->getPhoto();\n}\n\nDocumentData *GroupedMedia::getDocument() const {\n\treturn main()->getDocument();\n}\n\nHistoryMessageEdited *GroupedMedia::displayedEditBadge() const {\n\tfor (const auto &part : _parts) {\n\t\tif (!part.item->hideEditedBadge()) {\n\t\t\tif (const auto edited = part.item->Get<HistoryMessageEdited>()) {\n\t\t\t\treturn edited;\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid GroupedMedia::updateNeedBubbleState() {\n\t_needBubble = computeNeedBubble();\n}\n\nvoid GroupedMedia::stopAnimation() {\n\tfor (const auto &part : _parts) {\n\t\tpart.content->stopAnimation();\n\t}\n}\n\nvoid GroupedMedia::checkAnimation() {\n\tfor (const auto &part : _parts) {\n\t\tpart.content->checkAnimation();\n\t}\n}\n\nbool GroupedMedia::hasHeavyPart() const {\n\tfor (const auto &part : _parts) {\n\t\tif (!part.cache.isNull() || part.content->hasHeavyPart()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid GroupedMedia::unloadHeavyPart() {\n\tfor (const auto &part : _parts) {\n\t\tpart.content->unloadHeavyPart();\n\t\tpart.cacheKey = 0;\n\t\tpart.cache = QPixmap();\n\t}\n}\n\nvoid GroupedMedia::parentTextUpdated() {\n\tif (_parent->media() == this) {\n\t\tif (_mode == Mode::Column) {\n\t\t\tfor (const auto &part : _parts) {\n\t\t\t\tpart.content->parentTextUpdated();\n\t\t\t}\n\t\t} else {\n\t\t\t_captionItem = std::nullopt;\n\t\t}\n\t}\n}\n\nbool GroupedMedia::needsBubble() const {\n\treturn _needBubble;\n}\n\nQPoint GroupedMedia::resolveCustomInfoRightBottom() const {\n\tconst auto skipx = (st::msgDateImgDelta + st::msgDateImgPadding.x());\n\tconst auto skipy = (st::msgDateImgDelta + st::msgDateImgPadding.y());\n\treturn QPoint(width() - skipx, height() - skipy);\n}\n\nstd::optional<PaidInformation> GroupedMedia::paidInformation() const {\n\tauto result = PaidInformation();\n\tfor (const auto &part : _parts) {\n\t\t++result.messages;\n\t\tresult.stars += part.item->starsPaid();\n\t}\n\treturn result;\n}\n\nbool GroupedMedia::enforceBubbleWidth() const {\n\treturn _mode == Mode::Grid;\n}\n\nbool GroupedMedia::computeNeedBubble() const {\n\tExpects(_mode == Mode::Column || _captionItem.has_value());\n\n\tif (_mode == Mode::Column || *_captionItem) {\n\t\treturn true;\n\t}\n\tif (const auto item = _parent->data()) {\n\t\tif (item->repliesAreComments()\n\t\t\t|| item->externalReply()\n\t\t\t|| item->viaBot()\n\t\t\t|| _parent->displayReply()\n\t\t\t|| _parent->displayForwardedFrom()\n\t\t\t|| _parent->displayFromName()\n\t\t\t|| _parent->displayedTopicButton()\n\t\t\t) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool GroupedMedia::needInfoDisplay() const {\n\tconst auto item = _parent->data();\n\treturn (_mode != Mode::Column)\n\t\t&& (item->isSending()\n\t\t\t|| item->awaitingVideoProcessing()\n\t\t\t|| item->hasFailed()\n\t\t\t|| _parent->isUnderCursor()\n\t\t\t|| (_parent->delegate()->elementContext() == Context::ChatPreview)\n\t\t\t|| _parent->isLastAndSelfMessage());\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_media_grouped.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n#include \"data/data_document.h\"\n#include \"data/data_photo.h\"\n\nnamespace Data {\nclass Media;\n} // namespace Data\n\nnamespace HistoryView {\n\nclass GroupedMedia : public Media {\npublic:\n\tstatic constexpr auto kMaxSize = 10;\n\n\tGroupedMedia(\n\t\tnot_null<Element*> parent,\n\t\tconst std::vector<std::unique_ptr<Data::Media>> &medias);\n\tGroupedMedia(\n\t\tnot_null<Element*> parent,\n\t\tconst std::vector<not_null<HistoryItem*>> &items);\n\t~GroupedMedia();\n\n\tvoid refreshParentId(not_null<HistoryItem*> realParent) override;\n\n\tHistoryItem *itemForText() const override;\n\tbool hideMessageText() const override;\n\n\tvoid drawHighlight(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint top) const override;\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tPointState pointState(QPoint point) const override;\n\tTextState textState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\tbool toggleSelectionByHandlerClick(\n\t\tconst ClickHandlerPtr &p) const override;\n\tbool dragItemByHandler(const ClickHandlerPtr &p) const override;\n\n\t[[nodiscard]] TextSelection adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const override;\n\tuint16 fullSelectionLength() const override;\n\tbool hasTextForCopy() const override;\n\n\tPhotoData *getPhoto() const override;\n\tDocumentData *getDocument() const override;\n\n\tTextForMimeData selectedText(TextSelection selection) const override;\n\tSelectedQuote selectedQuote(TextSelection selection) const override;\n\tTextSelection selectionFromQuote(\n\t\tconst SelectedQuote &quote) const override;\n\n\tstd::vector<Ui::BubbleSelectionInterval> getBubbleSelectionIntervals(\n\t\tTextSelection selection) const override;\n\n\tvoid clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool active) override;\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed) override;\n\n\tvoid hideSpoilers() override;\n\tStorage::SharedMediaTypesMask sharedMediaTypes() const override;\n\n\tbool overrideEditedDate() const override {\n\t\treturn true;\n\t}\n\tHistoryMessageEdited *displayedEditBadge() const override;\n\n\tbool skipBubbleTail() const override {\n\t\treturn (_mode == Mode::Grid) && isRoundedInBubbleBottom();\n\t}\n\tvoid updateNeedBubbleState() override;\n\tbool needsBubble() const override;\n\tbool customInfoLayout() const override {\n\t\treturn (_mode != Mode::Column);\n\t}\n\tQPoint resolveCustomInfoRightBottom() const override;\n\n\tbool allowsFastShare() const override {\n\t\treturn true;\n\t}\n\tstd::optional<PaidInformation> paidInformation() const override;\n\tbool customHighlight() const override {\n\t\treturn true;\n\t}\n\tQRect groupItemRect(int index) const override;\n\tbool enforceBubbleWidth() const override;\n\n\tvoid stopAnimation() override;\n\tvoid checkAnimation() override;\n\tbool hasHeavyPart() const override;\n\tvoid unloadHeavyPart() override;\n\n\tvoid parentTextUpdated() override;\n\nprivate:\n\tenum class Mode : char {\n\t\tGrid,\n\t\tColumn,\n\t};\n\tstruct Part {\n\t\tPart(\n\t\t\tnot_null<Element*> parent,\n\t\t\tnot_null<Data::Media*> media);\n\n\t\tnot_null<HistoryItem*> item;\n\t\tstd::unique_ptr<Media> content;\n\n\t\tRectParts sides = RectPart::None;\n\t\tQRect initialGeometry;\n\t\tQRect geometry;\n\t\tmutable uint64 cacheKey = 0;\n\t\tmutable QPixmap cache;\n\n\t};\n\n\t[[nodiscard]] static Mode DetectMode(not_null<Data::Media*> media);\n\n\ttemplate <typename DataMediaRange>\n\tbool applyGroup(const DataMediaRange &medias);\n\n\ttemplate <typename DataMediaRange>\n\tbool validateGroupParts(const DataMediaRange &medias) const;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\n\tbool needInfoDisplay() const;\n\tbool computeNeedBubble() const;\n\tnot_null<Media*> main() const;\n\tTextState getPartState(\n\t\tQPoint point,\n\t\tStateRequest request) const;\n\n\t[[nodiscard]] Ui::BubbleRounding applyRoundingSides(\n\t\tUi::BubbleRounding already,\n\t\tRectParts sides) const;\n\t[[nodiscard]] QMargins groupedPadding() const;\n\n\t[[nodiscard]] Media *lookupSpoilerTagMedia() const;\n\t[[nodiscard]] QImage generateSpoilerTagBackground(QRect full) const;\n\n\tmutable std::optional<HistoryItem*> _captionItem;\n\tstd::vector<Part> _parts;\n\tMode _mode = Mode::Grid;\n\tbool _needBubble : 1 = false;\n\tbool _purchasedPriceTag : 1 = false;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_media_spoiler.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_media_spoiler.h\"\n\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_media_spoiler.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/chat/message_bubble.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace Ui {\nclass SpoilerAnimation;\n} // namespace Ui\n\nnamespace HistoryView {\n\nstruct MediaSpoiler {\n\tClickHandlerPtr link;\n\tstd::unique_ptr<Ui::SpoilerAnimation> animation;\n\tQImage cornerCache;\n\tQImage background;\n\tstd::optional<Ui::BubbleRounding> backgroundRounding;\n\tUi::Animations::Simple revealAnimation;\n\tbool revealed = false;\n};\n\nstruct MediaSpoilerTag {\n\tuint64 price : 63 = 0;\n\tuint64 sensitive : 1 = 0;\n\tQImage cache;\n\tQColor darken;\n\tQColor fg;\n\tQColor star;\n\tClickHandlerPtr link;\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_media_unwrapped.h\"\n\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/view/media/history_view_media_common.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_reply.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kMaxForwardedBarLines = 4;\n\n} // namespace\n\nstd::unique_ptr<StickerPlayer> UnwrappedMedia::Content::stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) {\n\treturn nullptr;\n}\n\nQSize UnwrappedMedia::Content::countCurrentSize(int newWidth) {\n\treturn countOptimalSize();\n}\n\nUnwrappedMedia::UnwrappedMedia(\n\tnot_null<Element*> parent,\n\tstd::unique_ptr<Content> content)\n: Media(parent)\n, _content(std::move(content)) {\n}\n\nQSize UnwrappedMedia::countOptimalSize() {\n\t_content->refreshLink();\n\tconst auto optimal = _content->countOptimalSize();\n\tauto maxWidth = optimal.width();\n\tconst auto minimal = std::max(st::emojiSize, st::msgPhotoSize);\n\tauto minHeight = std::max(optimal.height(), minimal);\n\tif (_parent->media() == this) {\n\t\tconst auto item = _parent->data();\n\t\tconst auto via = item->Get<HistoryMessageVia>();\n\t\tconst auto reply = _parent->Get<Reply>();\n\t\tconst auto topic = _parent->displayedTopicButton();\n\t\tconst auto forwarded = getDisplayedForwardedInfo();\n\t\tif (forwarded) {\n\t\t\tforwarded->create(via, item);\n\t\t}\n\t\tmaxWidth += additionalWidth(topic, reply, via, forwarded);\n\t\taccumulate_max(maxWidth, _parent->reactionsOptimalWidth());\n\t\tif (const auto size = _parent->rightActionSize()) {\n\t\t\tminHeight = std::max(\n\t\t\t\tminHeight,\n\t\t\t\tst::historyFastShareBottom + size->height());\n\t\t}\n\t}\n\treturn { maxWidth, minHeight };\n}\n\nQSize UnwrappedMedia::countCurrentSize(int newWidth) {\n\tconst auto item = _parent->data();\n\taccumulate_min(newWidth, maxWidth());\n\t_contentSize = _content->countCurrentSize(newWidth);\n\tauto newHeight = std::max(minHeight(), _contentSize.height());\n\t_additionalOnTop = false;\n\tif (_parent->media() != this) {\n\t\treturn { newWidth, newHeight };\n\t}\n\tif (_parent->hasRightLayout()) {\n\t\t// Add some height to isolated emoji for the timestamp info.\n\t\tconst auto infoHeight = st::msgDateImgPadding.y() * 2\n\t\t\t+ st::msgDateFont->height;\n\t\tconst auto minimal = std::min(\n\t\t\tst::largeEmojiSize + 2 * st::largeEmojiOutline,\n\t\t\t_contentSize.height());\n\t\taccumulate_max(newHeight, minimal + st::msgDateImgDelta + infoHeight);\n\t}\n\taccumulate_max(newWidth, _parent->reactionsOptimalWidth());\n\t_topAdded = 0;\n\tconst auto via = item->Get<HistoryMessageVia>();\n\tconst auto reply = _parent->Get<Reply>();\n\tconst auto topic = _parent->displayedTopicButton();\n\tconst auto forwarded = getDisplayedForwardedInfo();\n\tif (topic || via || reply || forwarded) {\n\t\tconst auto additional = additionalWidth(topic, reply, via, forwarded);\n\t\tconst auto optimalw = maxWidth() - additional;\n\t\tconst auto additionalMinWidth = std::min(additional, st::msgReplyPadding.left() + st::msgMinWidth / 2);\n\t\t_additionalOnTop = (optimalw + additionalMinWidth) > newWidth;\n\t\tconst auto surroundingWidth = _additionalOnTop\n\t\t\t? std::min(newWidth - st::msgReplyPadding.left(), additional)\n\t\t\t: (newWidth - _contentSize.width() - st::msgReplyPadding.left());\n\t\tif (reply) {\n\t\t\t[[maybe_unused]] auto h = reply->resizeToWidth(surroundingWidth);\n\t\t}\n\t\tconst auto surrounding = surroundingInfo(topic, reply, via, forwarded, surroundingWidth);\n\t\tif (_additionalOnTop) {\n\t\t\t_topAdded = surrounding.height + st::msgMargin.bottom();\n\t\t\tnewHeight += _topAdded;\n\t\t} else {\n\t\t\tconst auto infoHeight = st::msgDateImgPadding.y() * 2\n\t\t\t\t+ st::msgDateFont->height;\n\t\t\tconst auto minimal = surrounding.height\n\t\t\t\t+ st::msgDateImgDelta\n\t\t\t\t+ infoHeight;\n\t\t\tnewHeight = std::max(newHeight, minimal);\n\t\t}\n\t\tconst auto availw = newWidth\n\t\t\t- (_additionalOnTop ? 0 : optimalw + st::msgReplyPadding.left())\n\t\t\t- 2 * st::msgReplyPadding.left();\n\t\tif (via) {\n\t\t\tvia->resize(availw);\n\t\t}\n\t}\n\treturn { newWidth, newHeight };\n}\n\nvoid UnwrappedMedia::draw(Painter &p, const PaintContext &context) const {\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {\n\t\treturn;\n\t}\n\tconst auto rightAligned = _parent->hasRightLayout();\n\tconst auto inWebPage = (_parent->media() != this);\n\tconst auto item = _parent->data();\n\tauto usex = 0;\n\tauto usew = _contentSize.width();\n\tif (!inWebPage && rightAligned) {\n\t\tusex = width() - usew;\n\t}\n\tif (rtl()) {\n\t\tusex = width() - usex - usew;\n\t}\n\n\tconst auto usey = rightAligned ? _topAdded : (height() - _contentSize.height());\n\tconst auto useh = rightAligned\n\t\t? std::max(\n\t\t\t_contentSize.height(),\n\t\t\t(height()\n\t\t\t\t- _topAdded\n\t\t\t\t- st::msgDateImgPadding.y() * 2\n\t\t\t\t- st::msgDateFont->height))\n\t\t: _contentSize.height();\n\tconst auto inner = QRect(usex, usey, usew, useh);\n\tif (context.skipDrawingParts != PaintContext::SkipDrawingParts::Content) {\n\t\t_content->draw(p, context, inner);\n\t}\n\n\tif (!inWebPage && (context.skipDrawingParts\n\t\t\t!= PaintContext::SkipDrawingParts::Surrounding)) {\n\t\tconst auto via = inWebPage ? nullptr : item->Get<HistoryMessageVia>();\n\t\tconst auto reply = inWebPage ? nullptr : _parent->Get<Reply>();\n\t\tconst auto topic = inWebPage ? nullptr : _parent->displayedTopicButton();\n\t\tconst auto forwarded = inWebPage ? nullptr : getDisplayedForwardedInfo();\n\t\tdrawSurrounding(p, inner, context, topic, reply, via, forwarded);\n\t}\n}\n\nUnwrappedMedia::SurroundingInfo UnwrappedMedia::surroundingInfo(\n\t\tconst TopicButton *topic,\n\t\tconst Reply *reply,\n\t\tconst HistoryMessageVia *via,\n\t\tconst HistoryMessageForwarded *forwarded,\n\t\tint outerw) const {\n\tif (!topic && !via && !reply && !forwarded) {\n\t\treturn {};\n\t}\n\tconst auto innerw = outerw - st::msgReplyPadding.left() - st::msgReplyPadding.right();\n\n\tauto topicSize = QSize();\n\tif (topic) {\n\t\tconst auto padding = st::topicButtonPadding;\n\t\tconst auto height = padding.top()\n\t\t\t+ st::msgNameFont->height\n\t\t\t+ padding.bottom();\n\t\tconst auto width = std::max(\n\t\t\tstd::min(\n\t\t\t\touterw,\n\t\t\t\t(st::msgReplyPadding.left()\n\t\t\t\t\t+ topic->name.maxWidth()\n\t\t\t\t\t+ st::topicButtonArrowSkip\n\t\t\t\t\t+ st::topicButtonPadding.right())),\n\t\t\theight);\n\t\ttopicSize = { width, height };\n\t}\n\tauto panelHeight = 0;\n\tauto forwardedHeightReal = forwarded\n\t\t? forwarded->text.countHeight(innerw)\n\t\t: 0;\n\tauto forwardedHeight = std::min(\n\t\tforwardedHeightReal,\n\t\tkMaxForwardedBarLines * st::msgServiceNameFont->height);\n\tconst auto breakEverywhere = (forwardedHeightReal > forwardedHeight);\n\tif (forwarded) {\n\t\tpanelHeight += forwardedHeight;\n\t} else if (via) {\n\t\tpanelHeight += st::msgServiceNameFont->height\n\t\t\t+ (reply ? st::msgReplyPadding.top() : 0);\n\t}\n\tif (panelHeight) {\n\t\tpanelHeight += st::msgReplyPadding.top();\n\t}\n\tif (reply) {\n\t\tconst auto replyMargins = reply->margins();\n\t\tpanelHeight += reply->height()\n\t\t\t- ((forwarded || via) ? 0 : replyMargins.top())\n\t\t\t- replyMargins.bottom();\n\t} else if (panelHeight) {\n\t\tpanelHeight += st::msgReplyPadding.bottom();\n\t}\n\tconst auto total = (topicSize.isEmpty() ? 0 : topicSize.height())\n\t\t+ ((panelHeight || !topicSize.height()) ? st::topicButtonSkip : 0)\n\t\t+ panelHeight;\n\treturn {\n\t\t.topicSize = topicSize,\n\t\t.height = total,\n\t\t.panelHeight = panelHeight,\n\t\t.forwardedHeight = forwardedHeight,\n\t\t.forwardedBreakEverywhere = breakEverywhere,\n\t};\n}\n\nvoid UnwrappedMedia::drawSurrounding(\n\t\tPainter &p,\n\t\tconst QRect &inner,\n\t\tconst PaintContext &context,\n\t\tconst TopicButton *topic,\n\t\tconst Reply *reply,\n\t\tconst HistoryMessageVia *via,\n\t\tconst HistoryMessageForwarded *forwarded) const {\n\tconst auto st = context.st;\n\tconst auto sti = context.imageStyle();\n\tconst auto rightAligned = _parent->hasRightLayout();\n\tconst auto rightActionSize = _parent->rightActionSize();\n\tconst auto fullRight = calculateFullRight(inner);\n\tauto fullBottom = height();\n\tif (needInfoDisplay()) {\n\t\t_parent->drawInfo(\n\t\t\tp,\n\t\t\tcontext,\n\t\t\tfullRight,\n\t\t\tfullBottom,\n\t\t\tinner.x() * 2 + inner.width(),\n\t\t\tInfoDisplayType::Background);\n\t}\n\tauto replyLeft = 0;\n\tauto replyRight = 0;\n\tauto rectw = _additionalOnTop\n\t\t? std::min(width() - st::msgReplyPadding.left(), additionalWidth(topic, reply, via, forwarded))\n\t\t: (width() - inner.width() - st::msgReplyPadding.left());\n\tif (const auto surrounding = surroundingInfo(topic, reply, via, forwarded, rectw)) {\n\t\tauto recth = surrounding.panelHeight;\n\t\tif (!surrounding.topicSize.isEmpty()) {\n\t\t\tauto rectw = surrounding.topicSize.width();\n\t\t\tint rectx = _additionalOnTop\n\t\t\t\t? (rightAligned ? (inner.x() + inner.width() - rectw) : 0)\n\t\t\t\t: (rightAligned ? 0 : (inner.width() + st::msgReplyPadding.left()));\n\t\t\tint recty = 0;\n\t\t\tif (rtl()) rectx = width() - rectx - rectw;\n\n\t\t\t{\n\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(sti->msgServiceBg);\n\t\t\t\tconst auto recth = surrounding.topicSize.height();\n\t\t\t\tp.drawRoundedRect(\n\t\t\t\t\tQRect{ rectx, recty, rectw, recth },\n\t\t\t\t\trecth / 2,\n\t\t\t\t\trecth / 2);\n\t\t\t}\n\n\t\t\tp.setPen(st->msgServiceFg());\n\t\t\trectx += st::msgReplyPadding.left();\n\t\t\trecty += st::topicButtonPadding.top();\n\t\t\trectw -= st::msgReplyPadding.left() + st::topicButtonPadding.right() + st::topicButtonArrowSkip;\n\t\t\tp.setTextPalette(st->serviceTextPalette());\n\t\t\ttopic->name.drawElided(p, rectx, recty, rectw);\n\t\t\tp.restoreTextPalette();\n\n\t\t\tconst auto &icon = st::topicButtonArrow;\n\t\t\ticon.paint(\n\t\t\t\tp,\n\t\t\t\trectx + rectw + st::topicButtonArrowPosition.x(),\n\t\t\t\trecty + st::topicButtonArrowPosition.y(),\n\t\t\t\twidth(),\n\t\t\t\tst->msgServiceFg()->c);\n\t\t}\n\t\tif (recth) {\n\t\t\tint rectx = _additionalOnTop\n\t\t\t\t? (rightAligned ? (inner.x() + inner.width() - rectw) : 0)\n\t\t\t\t: (rightAligned ? 0 : (inner.width() + st::msgReplyPadding.left()));\n\t\t\tint recty = surrounding.height - recth;\n\t\t\tif (rtl()) rectx = width() - rectx - rectw;\n\n\t\t\tUi::FillRoundRect(p, rectx, recty, rectw, recth, sti->msgServiceBg, sti->msgServiceBgCornersSmall);\n\t\t\tp.setPen(st->msgServiceFg());\n\t\t\tconst auto textx = rectx + st::msgReplyPadding.left();\n\t\t\tconst auto textw = rectw - st::msgReplyPadding.left() - st::msgReplyPadding.right();\n\t\t\tif (forwarded) {\n\t\t\t\tp.setTextPalette(st->serviceTextPalette());\n\t\t\t\tforwarded->text.drawElided(p, textx, recty + st::msgReplyPadding.top(), textw, kMaxForwardedBarLines, style::al_left, 0, -1, 0, surrounding.forwardedBreakEverywhere);\n\t\t\t\tp.restoreTextPalette();\n\n\t\t\t\tconst auto skip = std::min(\n\t\t\t\t\tforwarded->text.countHeight(textw),\n\t\t\t\t\tkMaxForwardedBarLines * st::msgServiceNameFont->height);\n\t\t\t\trecty += skip;\n\t\t\t} else if (via) {\n\t\t\t\tp.setFont(st::msgDateFont);\n\t\t\t\tp.drawTextLeft(textx, recty + st::msgReplyPadding.top(), 2 * textx + textw, via->text);\n\n\t\t\t\tconst auto skip = st::msgServiceNameFont->height\n\t\t\t\t\t+ (reply ? st::msgReplyPadding.top() : 0);\n\t\t\t\trecty += skip;\n\t\t\t}\n\t\t\tif (reply) {\n\t\t\t\tif (forwarded || via) {\n\t\t\t\t\trecty += st::msgReplyPadding.top();\n\t\t\t\t\trecth -= st::msgReplyPadding.top();\n\t\t\t\t} else {\n\t\t\t\t\trecty -= reply->margins().top();\n\t\t\t\t}\n\t\t\t\treply->paint(p, _parent, context, rectx, recty, rectw, false);\n\t\t\t}\n\t\t\treplyLeft = rectx;\n\t\t\treplyRight = rectx + rectw;\n\t\t}\n\t}\n\tif (rightActionSize) {\n\t\tconst auto position = calculateFastActionPosition(\n\t\t\tinner,\n\t\t\trightAligned,\n\t\t\treplyLeft,\n\t\t\treplyRight,\n\t\t\treply ? reply->height() : 0,\n\t\t\tfullBottom,\n\t\t\tfullRight,\n\t\t\t*rightActionSize);\n\t\tconst auto outer = 2 * inner.x() + inner.width();\n\t\t_parent->drawRightAction(p, context, position.x(), position.y(), outer);\n\t}\n}\n\nPointState UnwrappedMedia::pointState(QPoint point) const {\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {\n\t\treturn PointState::Outside;\n\t}\n\n\tconst auto rightAligned = _parent->hasRightLayout();\n\tconst auto inWebPage = (_parent->media() != this);\n\tauto usex = 0;\n\tauto usew = _contentSize.width();\n\tif (!inWebPage && rightAligned) {\n\t\tusex = width() - usew;\n\t}\n\tif (rtl()) {\n\t\tusex = width() - usex - usew;\n\t}\n\n\tconst auto datey = height() - st::msgDateImgPadding.y() * 2\n\t\t- st::msgDateFont->height;\n\tconst auto usey = rightAligned ? _topAdded : (height() - _contentSize.height());\n\tconst auto useh = rightAligned\n\t\t? std::max(_contentSize.height(), datey)\n\t\t: _contentSize.height();\n\tconst auto inner = QRect(usex, usey, usew, useh);\n\n\t// Rectangle of date bubble.\n\tif (point.x() < calculateFullRight(inner) && point.y() > datey) {\n\t\treturn PointState::Inside;\n\t}\n\n\treturn inner.contains(point) ? PointState::Inside : PointState::Outside;\n}\n\nTextState UnwrappedMedia::textState(QPoint point, StateRequest request) const {\n\tauto result = TextState(_parent);\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {\n\t\treturn result;\n\t}\n\n\tconst auto rightAligned = _parent->hasRightLayout();\n\tconst auto inWebPage = (_parent->media() != this);\n\tconst auto item = _parent->data();\n\tauto usex = 0;\n\tauto usew = _contentSize.width();\n\tif (!inWebPage && rightAligned) {\n\t\tusex = width() - usew;\n\t}\n\tif (rtl()) {\n\t\tusex = width() - usex - usew;\n\t}\n\n\tconst auto usey = rightAligned ? _topAdded : (height() - _contentSize.height());\n\tconst auto useh = rightAligned\n\t\t? std::max(\n\t\t\t_contentSize.height(),\n\t\t\theight() - st::msgDateImgPadding.y() * 2 - st::msgDateFont->height)\n\t\t: _contentSize.height();\n\tconst auto inner = QRect(usex, usey, usew, useh);\n\n\tif (_parent->media() == this) {\n\t\tconst auto via = inWebPage ? nullptr : item->Get<HistoryMessageVia>();\n\t\tconst auto reply = inWebPage ? nullptr : _parent->Get<Reply>();\n\t\tconst auto topic = inWebPage ? nullptr : _parent->displayedTopicButton();\n\t\tconst auto forwarded = inWebPage ? nullptr : getDisplayedForwardedInfo();\n\t\tauto replyLeft = 0;\n\t\tauto replyRight = 0;\n\t\tauto rectw = _additionalOnTop\n\t\t\t? std::min(width() - st::msgReplyPadding.left(), additionalWidth(topic, reply, via, forwarded))\n\t\t\t: (width() - inner.width() - st::msgReplyPadding.left());\n\t\tif (const auto surrounding = surroundingInfo(topic, reply, via, forwarded, rectw)) {\n\t\t\tauto recth = surrounding.panelHeight;\n\t\t\tif (!surrounding.topicSize.isEmpty()) {\n\t\t\t\tauto rectw = surrounding.topicSize.width();\n\t\t\t\tint rectx = _additionalOnTop\n\t\t\t\t\t? (rightAligned ? (inner.x() + inner.width() - rectw) : 0)\n\t\t\t\t\t: (rightAligned ? 0 : (inner.width() + st::msgReplyPadding.left()));\n\t\t\t\tint recty = 0;\n\t\t\t\tif (rtl()) rectx = width() - rectx - rectw;\n\t\t\t\tif (QRect(QPoint(rectx, recty), surrounding.topicSize).contains(point)) {\n\t\t\t\t\tresult.link = topic->link;\n\t\t\t\t\treturn result;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (recth) {\n\t\t\t\tint rectx = _additionalOnTop\n\t\t\t\t\t? (rightAligned ? (inner.x() + inner.width() - rectw) : 0)\n\t\t\t\t\t: (rightAligned ? 0 : (inner.width() + st::msgReplyPadding.left()));\n\t\t\t\tint recty = surrounding.height - recth;\n\t\t\t\tif (rtl()) rectx = width() - rectx - rectw;\n\n\t\t\t\tif (forwarded) {\n\t\t\t\t\tif (QRect(rectx, recty, rectw, st::msgReplyPadding.top() + surrounding.forwardedHeight).contains(point)) {\n\t\t\t\t\t\tauto textRequest = request.forText();\n\t\t\t\t\t\tif (surrounding.forwardedBreakEverywhere) {\n\t\t\t\t\t\t\ttextRequest.flags |= Ui::Text::StateRequest::Flag::BreakEverywhere;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst auto innerw = rectw - st::msgReplyPadding.left() - st::msgReplyPadding.right();\n\t\t\t\t\t\tresult = TextState(_parent, forwarded->text.getState(\n\t\t\t\t\t\t\tpoint - QPoint(rectx + st::msgReplyPadding.left(), recty + st::msgReplyPadding.top()),\n\t\t\t\t\t\t\tinnerw,\n\t\t\t\t\t\t\ttextRequest));\n\t\t\t\t\t\tresult.symbol = 0;\n\t\t\t\t\t\tresult.afterSymbol = false;\n\t\t\t\t\t\tif (surrounding.forwardedBreakEverywhere) {\n\t\t\t\t\t\t\tresult.cursor = CursorState::Forwarded;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tresult.cursor = CursorState::None;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t}\n\t\t\t\t\trecty += surrounding.forwardedHeight;\n\t\t\t\t\trecth -= surrounding.forwardedHeight;\n\t\t\t\t} else if (via) {\n\t\t\t\t\tint viah = st::msgReplyPadding.top() + st::msgServiceNameFont->height + (reply ? 0 : st::msgReplyPadding.bottom());\n\t\t\t\t\tif (QRect(rectx, recty, rectw, viah).contains(point)) {\n\t\t\t\t\t\tresult.link = via->link;\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t}\n\t\t\t\t\tint skip = st::msgServiceNameFont->height + (reply ? 2 * st::msgReplyPadding.top() : 0);\n\t\t\t\t\trecty += skip;\n\t\t\t\t\trecth -= skip;\n\t\t\t\t}\n\t\t\t\tif (reply) {\n\t\t\t\t\tif (forwarded || via) {\n\t\t\t\t\t\trecty += st::msgReplyPadding.top();\n\t\t\t\t\t\trecth -= st::msgReplyPadding.top() + reply->margins().top();\n\t\t\t\t\t} else {\n\t\t\t\t\t\trecty -= reply->margins().top();\n\t\t\t\t\t}\n\t\t\t\t\tconst auto replyRect = QRect(rectx, recty, rectw, recth);\n\t\t\t\t\tif (replyRect.contains(point)) {\n\t\t\t\t\t\tresult.link = reply->link();\n\t\t\t\t\t\treply->saveRipplePoint(point - replyRect.topLeft());\n\t\t\t\t\t\treply->createRippleAnimation(_parent, replyRect.size());\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treplyLeft = rectx;\n\t\t\t\treplyRight = rectx + rectw;\n\t\t\t}\n\t\t}\n\t\tconst auto fullRight = calculateFullRight(inner);\n\t\tconst auto rightActionSize = _parent->rightActionSize();\n\t\tauto fullBottom = height();\n\t\tconst auto bottomInfoResult = _parent->bottomInfoTextState(\n\t\t\tfullRight,\n\t\t\tfullBottom,\n\t\t\tpoint,\n\t\t\tInfoDisplayType::Background);\n\t\tif (bottomInfoResult.link\n\t\t\t|| bottomInfoResult.cursor != CursorState::None\n\t\t\t|| bottomInfoResult.customTooltip) {\n\t\t\treturn bottomInfoResult;\n\t\t}\n\t\tif (rightActionSize) {\n\t\t\tconst auto position = calculateFastActionPosition(\n\t\t\t\tinner,\n\t\t\t\trightAligned,\n\t\t\t\treplyLeft,\n\t\t\t\treplyRight,\n\t\t\t\treply ? reply->height() : 0,\n\t\t\t\tfullBottom,\n\t\t\t\tfullRight,\n\t\t\t\t*rightActionSize);\n\t\t\tif (QRect(position.x(), position.y(), rightActionSize->width(), rightActionSize->height()).contains(point)) {\n\t\t\t\tresult.link = _parent->rightActionLink(point - position);\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Link of content can be nullptr (e.g. sticker without stickerpack).\n\t// So we have to process it to avoid overriding the previous result.\n\tif (_content->link() && inner.contains(point)) {\n\t\tresult.link = _content->link();\n\t\treturn result;\n\t}\n\treturn result;\n}\n\nbool UnwrappedMedia::hasTextForCopy() const {\n\treturn _content->hasTextForCopy();\n}\n\nbool UnwrappedMedia::dragItemByHandler(const ClickHandlerPtr &p) const {\n\tconst auto reply = _parent->Get<Reply>();\n\treturn !reply || (reply->link() != p);\n}\n\nQRect UnwrappedMedia::contentRectForReactions() const {\n\tconst auto inWebPage = (_parent->media() != this);\n\tif (inWebPage) {\n\t\treturn QRect(0, 0, width(), height());\n\t}\n\tconst auto rightAligned = _parent->hasRightLayout();\n\tauto usex = 0;\n\tauto usew = _contentSize.width();\n\taccumulate_max(usew, _parent->reactionsOptimalWidth());\n\tif (rightAligned) {\n\t\tusex = width() - usew;\n\t}\n\tif (rtl()) {\n\t\tusex = width() - usex - usew;\n\t}\n\tconst auto usey = rightAligned ? _topAdded : (height() - _contentSize.height());\n\tconst auto useh = rightAligned\n\t\t? std::max(\n\t\t\t_contentSize.height(),\n\t\t\theight() - st::msgDateImgPadding.y() * 2 - st::msgDateFont->height)\n\t\t: _contentSize.height();\n\treturn QRect(usex, usey, usew, useh);\n}\n\nstd::optional<int> UnwrappedMedia::reactionButtonCenterOverride() const {\n\tconst auto fullRight = calculateFullRight(contentRectForReactions());\n\tconst auto right = fullRight\n\t\t- _parent->infoWidth()\n\t\t- st::msgDateImgPadding.x() * 2\n\t\t- st::msgReplyPadding.left();\n\treturn right - st::reactionCornerSize.width() / 2;\n}\n\nQPoint UnwrappedMedia::resolveCustomInfoRightBottom() const {\n\tconst auto inner = contentRectForReactions();\n\tconst auto fullBottom = inner.y() + inner.height();\n\tconst auto fullRight = calculateFullRight(inner);\n\tconst auto skipx = st::msgDateImgPadding.x();\n\tconst auto skipy = st::msgDateImgPadding.y();\n\tconst auto infoWidth = _parent->infoWidth()\n\t\t+ st::msgDateImgPadding.x() * 2\n\t\t+ st::msgReplyPadding.left();\n\treturn QPoint(fullRight - skipx - infoWidth, fullBottom - skipy);\n}\n\nstd::unique_ptr<StickerPlayer> UnwrappedMedia::stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) {\n\treturn _content->stickerTakePlayer(data, replacements);\n}\n\nint UnwrappedMedia::calculateFullRight(const QRect &inner) const {\n\tconst auto rightAligned = _parent->hasRightLayout();\n\tconst auto infoWidth = _parent->infoWidth()\n\t\t+ st::msgDateImgPadding.x() * 2\n\t\t+ st::msgReplyPadding.left();\n\tconst auto rightActionSize = _parent->rightActionSize();\n\tconst auto rightSkip = st::msgPadding.left()\n\t\t+ (_parent->hasFromPhoto()\n\t\t\t? st::msgMargin.right()\n\t\t\t: st::msgPadding.right());\n\tconst auto rightActionWidth = rightActionSize\n\t\t? (st::historyFastShareLeft * 2\n\t\t\t+ rightActionSize->width())\n\t\t: 0;\n\tauto fullRight = inner.x()\n\t\t+ inner.width()\n\t\t+ (rightAligned ? 0 : infoWidth);\n\tconst auto rightActionSkip = rightAligned ? 0 : rightActionWidth;\n\tif (fullRight + rightActionSkip + rightSkip > _parent->width()) {\n\t\tfullRight = _parent->width()\n\t\t\t- (rightAligned ? 0 : rightActionSkip)\n\t\t\t- rightSkip;\n\t}\n\treturn fullRight;\n}\n\nQPoint UnwrappedMedia::calculateFastActionPosition(\n\t\tQRect inner,\n\t\tbool rightAligned,\n\t\tint replyLeft,\n\t\tint replyRight,\n\t\tint replyHeight,\n\t\tint fullBottom,\n\t\tint fullRight,\n\t\tQSize size) const {\n\tconst auto fastShareTop = (fullBottom\n\t\t- st::historyFastShareBottom\n\t\t- size.height());\n\tconst auto doesRightActionHitReply = replyRight\n\t\t&& (fastShareTop < replyHeight);\n\tconst auto fastShareLeft = rightAligned\n\t\t? ((doesRightActionHitReply ? replyLeft : inner.x())\n\t\t\t- size.width()\n\t\t\t- st::historyFastShareLeft)\n\t\t: ((doesRightActionHitReply ? replyRight : fullRight)\n\t\t\t+ st::historyFastShareLeft);\n\treturn QPoint(fastShareLeft, fastShareTop);\n}\n\nbool UnwrappedMedia::needInfoDisplay() const {\n\treturn _parent->data()->isSending()\n\t\t|| _parent->data()->hasFailed()\n\t\t|| _parent->isUnderCursor()\n\t\t|| _parent->rightActionSize()\n\t\t|| _parent->isLastAndSelfMessage()\n\t\t|| (_parent->delegate()->elementContext() == Context::ChatPreview)\n\t\t|| (_parent->hasRightLayout()\n\t\t\t&& _content->alwaysShowOutTimestamp());\n}\n\nint UnwrappedMedia::additionalWidth(\n\t\tconst TopicButton *topic,\n\t\tconst Reply *reply,\n\t\tconst HistoryMessageVia *via,\n\t\tconst HistoryMessageForwarded *forwarded) const {\n\tauto result = st::msgReplyPadding.left() + _parent->infoWidth() + 2 * st::msgDateImgPadding.x();\n\tif (topic) {\n\t\taccumulate_max(result, 2 * st::msgReplyPadding.left() + topic->name.maxWidth() + st::topicButtonArrowSkip + st::topicButtonPadding.right());\n\t}\n\tif (forwarded) {\n\t\taccumulate_max(result, 2 * st::msgReplyPadding.left() + forwarded->text.maxWidth() + st::msgReplyPadding.right());\n\t} else if (via) {\n\t\taccumulate_max(result, 2 * st::msgReplyPadding.left() + via->maxWidth + st::msgReplyPadding.right());\n\t}\n\tif (reply) {\n\t\taccumulate_max(result, st::msgReplyPadding.left() + reply->maxWidth());\n\t}\n\treturn result;\n}\n\nauto UnwrappedMedia::getDisplayedForwardedInfo() const\n-> const HistoryMessageForwarded * {\n\treturn _parent->displayForwardedFrom()\n\t\t? _parent->data()->Get<HistoryMessageForwarded>()\n\t\t: nullptr;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n#include \"base/weak_ptr.h\"\n#include \"base/timer.h\"\n\nstruct HistoryMessageVia;\nstruct HistoryMessageReply;\nstruct HistoryMessageForwarded;\n\nnamespace HistoryView {\n\nclass Reply;\nstruct TopicButton;\n\nclass UnwrappedMedia final : public Media {\npublic:\n\tclass Content {\n\tpublic:\n\t\t[[nodiscard]] virtual QSize countOptimalSize() = 0;\n\t\t[[nodiscard]] virtual QSize countCurrentSize(int newWidth);\n\n\t\tvirtual void draw(\n\t\t\tPainter &p,\n\t\t\tconst PaintContext &context,\n\t\t\tconst QRect &r) = 0;\n\n\t\t[[nodiscard]] virtual ClickHandlerPtr link() {\n\t\t\treturn nullptr;\n\t\t}\n\n\t\t[[nodiscard]] virtual DocumentData *document() {\n\t\t\treturn nullptr;\n\t\t}\n\t\tvirtual void stickerClearLoopPlayed() {\n\t\t}\n\t\tvirtual std::unique_ptr<StickerPlayer> stickerTakePlayer(\n\t\t\tnot_null<DocumentData*> data,\n\t\t\tconst Lottie::ColorReplacements *replacements);\n\n\t\tvirtual bool hasHeavyPart() const {\n\t\t\treturn false;\n\t\t}\n\t\tvirtual void unloadHeavyPart() {\n\t\t}\n\t\tvirtual void refreshLink() {\n\t\t}\n\t\t[[nodiscard]] virtual bool alwaysShowOutTimestamp() {\n\t\t\treturn false;\n\t\t}\n\t\tvirtual bool hasTextForCopy() const {\n\t\t\treturn false;\n\t\t}\n\t\tvirtual bool updateItemData() {\n\t\t\treturn false;\n\t\t}\n\t\tvirtual ~Content() = default;\n\t};\n\n\tUnwrappedMedia(\n\t\tnot_null<Element*> parent,\n\t\tstd::unique_ptr<Content> content);\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tPointState pointState(QPoint point) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\n\tbool hasTextForCopy() const override;\n\n\tbool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {\n\t\treturn true;\n\t}\n\tbool dragItemByHandler(const ClickHandlerPtr &p) const override;\n\n\tDocumentData *getDocument() const override {\n\t\treturn _content->document();\n\t}\n\n\tbool needsBubble() const override {\n\t\treturn false;\n\t}\n\tbool unwrapped() const override {\n\t\treturn true;\n\t}\n\tbool customInfoLayout() const override {\n\t\treturn true;\n\t}\n\tQRect contentRectForReactions() const override;\n\tstd::optional<int> reactionButtonCenterOverride() const override;\n\tQPoint resolveCustomInfoRightBottom() const override;\n\n\tbool updateItemData() override {\n\t\treturn _content->updateItemData();\n\t}\n\n\tvoid stickerClearLoopPlayed() override {\n\t\t_content->stickerClearLoopPlayed();\n\t}\n\tstd::unique_ptr<StickerPlayer> stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) override;\n\n\tbool hasHeavyPart() const override {\n\t\treturn _content->hasHeavyPart();\n\t}\n\tvoid unloadHeavyPart() override {\n\t\t_content->unloadHeavyPart();\n\t}\n\nprivate:\n\tstruct SurroundingInfo {\n\t\tQSize topicSize;\n\t\tint height = 0;\n\t\tint panelHeight = 0;\n\t\tint forwardedHeight = 0;\n\t\tbool forwardedBreakEverywhere = false;\n\n\t\texplicit operator bool() const {\n\t\t\treturn (height > 0);\n\t\t}\n\t};\n\t[[nodiscard]] SurroundingInfo surroundingInfo(\n\t\tconst TopicButton *topic,\n\t\tconst Reply *reply,\n\t\tconst HistoryMessageVia *via,\n\t\tconst HistoryMessageForwarded *forwarded,\n\t\tint outerw) const;\n\tvoid drawSurrounding(\n\t\tPainter &p,\n\t\tconst QRect &inner,\n\t\tconst PaintContext &context,\n\t\tconst TopicButton *topic,\n\t\tconst Reply *reply,\n\t\tconst HistoryMessageVia *via,\n\t\tconst HistoryMessageForwarded *forwarded) const;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\n\tbool needInfoDisplay() const;\n\tint additionalWidth(\n\t\tconst TopicButton *topic,\n\t\tconst Reply *reply,\n\t\tconst HistoryMessageVia *via,\n\t\tconst HistoryMessageForwarded *forwarded) const;\n\n\tint calculateFullRight(const QRect &inner) const;\n\tQPoint calculateFastActionPosition(\n\t\tQRect inner,\n\t\tbool rightAligned,\n\t\tint replyLeft,\n\t\tint replyRight,\n\t\tint replyHeight,\n\t\tint fullBottom,\n\t\tint fullRight,\n\t\tQSize size) const;\n\n\tconst HistoryMessageForwarded *getDisplayedForwardedInfo() const;\n\n\tstd::unique_ptr<Content> _content;\n\tQSize _contentSize;\n\tint _topAdded = 0;\n\tbool _additionalOnTop = false;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_no_forwards_request.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_no_forwards_request.h\"\n\n#include \"data/data_peer.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/media/history_view_media_generic.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nclass BulletPointPart final : public MediaGenericTextPart {\npublic:\n\tBulletPointPart(\n\t\tTextWithEntities text,\n\t\tQMargins margins);\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const override;\n\nprivate:\n\tint _bulletLeft = 0;\n\tint _bulletTop = 0;\n\n};\n\nBulletPointPart::BulletPointPart(\n\tTextWithEntities text,\n\tQMargins margins)\n: MediaGenericTextPart(\n\tstd::move(text),\n\tQMargins(\n\t\tmargins.left() + st::chatSuggestBulletLeftExtra + st::historyGroupAboutBulletSkip,\n\t\tmargins.top(),\n\t\tmargins.right(),\n\t\tmargins.bottom()),\n\tst::serviceTextStyle,\n\t{},\n\t{},\n\tstyle::al_left)\n, _bulletLeft(margins.left() + st::chatSuggestBulletLeftExtra)\n, _bulletTop(margins.top()) {\n}\n\nvoid BulletPointPart::draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const {\n\tMediaGenericTextPart::draw(p, owner, context, outerWidth);\n\n\tconst auto &font = st::serviceTextStyle.font;\n\tconst auto size = st::mediaUnreadSize;\n\tconst auto top = _bulletTop + (font->height - size) / 2;\n\n\tp.setBrush(context.st->msgServiceFg());\n\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tp.drawEllipse(_bulletLeft, top, size, size);\n}\n\n} // namespace\n\nauto GenerateNoForwardsRequestMedia(\n\tnot_null<Element*> parent,\n\tnot_null<const HistoryServiceNoForwardsRequest*> request)\n-> Fn<void(\n\tnot_null<MediaGeneric*>,\n\tFn<void(std::unique_ptr<MediaGenericPart>)>)> {\n\treturn [=](\n\t\t\tnot_null<MediaGeneric*> media,\n\t\t\tFn<void(std::unique_ptr<MediaGenericPart>)> push) {\n\t\tconst auto item = parent->data();\n\t\tconst auto from = item->from();\n\n\t\tauto pushText = [&](\n\t\t\t\tTextWithEntities text,\n\t\t\t\tQMargins margins,\n\t\t\t\tstyle::align align = style::al_left) {\n\t\t\tpush(std::make_unique<MediaGenericTextPart>(\n\t\t\t\tstd::move(text),\n\t\t\t\tmargins,\n\t\t\t\tst::serviceTextStyle,\n\t\t\t\tbase::flat_map<uint16, ClickHandlerPtr>(),\n\t\t\t\tUi::Text::MarkedContext(),\n\t\t\t\talign));\n\t\t};\n\n\t\tpushText(\n\t\t\t(from->isSelf()\n\t\t\t\t? tr::lng_action_no_forwards_request_you(\n\t\t\t\t\ttr::now,\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_action_no_forwards_request(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\ttr::bold(from->shortName()),\n\t\t\t\t\ttr::marked)),\n\t\t\tst::chatSuggestInfoTitleMargin,\n\t\t\tstyle::al_top);\n\n\t\tconst auto features = {\n\t\t\ttr::lng_action_no_forwards_feature_forwarding(tr::now),\n\t\t\ttr::lng_action_no_forwards_feature_saving(tr::now),\n\t\t\ttr::lng_action_no_forwards_feature_copying(tr::now),\n\t\t};\n\t\tauto isLast = false;\n\t\tauto index = 0;\n\t\tconst auto count = int(features.size());\n\t\tfor (const auto &feature : features) {\n\t\t\tisLast = (++index == count);\n\t\t\tpush(std::make_unique<BulletPointPart>(\n\t\t\t\tTextWithEntities{ feature },\n\t\t\t\tisLast\n\t\t\t\t\t? st::chatSuggestInfoLastMargin\n\t\t\t\t\t: st::chatSuggestInfoMiddleMargin));\n\t\t}\n\t};\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_no_forwards_request.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nstruct HistoryServiceNoForwardsRequest;\n\nnamespace HistoryView {\n\nclass Element;\nclass MediaGeneric;\nclass MediaGenericPart;\n\nauto GenerateNoForwardsRequestMedia(\n\tnot_null<Element*> parent,\n\tnot_null<const HistoryServiceNoForwardsRequest*> request\n) -> Fn<void(\n\tnot_null<MediaGeneric*>,\n\tFn<void(std::unique_ptr<MediaGenericPart>)>)>;\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_photo.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_photo.h\"\n\n#include \"boxes/send_credits_box.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/media/history_view_media_common.h\"\n#include \"history/view/media/history_view_media_spoiler.h\"\n#include \"lang/lang_keys.h\"\n#include \"media/streaming/media_streaming_instance.h\"\n#include \"media/streaming/media_streaming_player.h\"\n#include \"media/streaming/media_streaming_document.h\"\n#include \"media/streaming/media_streaming_utility.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"ui/image/image.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/grouped_layout.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/ui_utility.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_streaming.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_auto_download.h\"\n#include \"data/data_web_page.h\"\n#include \"core/application.h\"\n#include \"core/ui_integration.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kStoryWidth = 720;\nconstexpr auto kStoryHeight = 1280;\n\nusing Data::PhotoSize;\n\n} // namespace\n\nstruct Photo::Streamed {\n\texplicit Streamed(std::shared_ptr<::Media::Streaming::Document> shared);\n\t::Media::Streaming::Instance instance;\n\t::Media::Streaming::FrameRequest frozenRequest;\n\tQImage frozenFrame;\n\tstd::array<QImage, 4> roundingCorners;\n\tQImage roundingMask;\n};\n\nPhoto::Streamed::Streamed(\n\tstd::shared_ptr<::Media::Streaming::Document> shared)\n: instance(std::move(shared), nullptr) {\n}\n\nPhoto::Photo(\n\tnot_null<Element*> parent,\n\tnot_null<HistoryItem*> realParent,\n\tnot_null<PhotoData*> photo,\n\tbool spoiler)\n: File(parent, realParent)\n, _data(photo)\n, _storyId(realParent->media()\n\t? realParent->media()->storyId()\n\t: FullStoryId())\n, _spoiler((spoiler || realParent->isMediaSensitive())\n\t? std::make_unique<MediaSpoiler>()\n\t: nullptr)\n, _sensitiveSpoiler(realParent->isMediaSensitive() ? 1 : 0) {\n\tcreate(realParent->fullId());\n}\n\nPhoto::Photo(\n\tnot_null<Element*> parent,\n\tnot_null<PeerData*> chat,\n\tnot_null<PhotoData*> photo,\n\tint width)\n: File(parent, parent->data())\n, _data(photo)\n, _serviceWidth(width) {\n\tcreate(parent->data()->fullId(), chat);\n}\n\nPhoto::~Photo() {\n\tif (_streamed || _dataMedia) {\n\t\tif (_streamed) {\n\t\t\t_data->owner().streaming().keepAlive(_data);\n\t\t\tstopAnimation();\n\t\t}\n\t\tif (_dataMedia) {\n\t\t\t_data->owner().keepAlive(base::take(_dataMedia));\n\t\t\t_parent->checkHeavyPart();\n\t\t}\n\t}\n\ttogglePollingStory(false);\n}\n\nvoid Photo::create(FullMsgId contextId, PeerData *chat) {\n\tsetLinks(\n\t\tstd::make_shared<PhotoOpenClickHandler>(\n\t\t\t_data,\n\t\t\tcrl::guard(this, [=](FullMsgId id) { showPhoto(id); }),\n\t\t\tcontextId),\n\t\tstd::make_shared<PhotoSaveClickHandler>(_data, contextId, chat),\n\t\tstd::make_shared<PhotoCancelClickHandler>(\n\t\t\t_data,\n\t\t\tcrl::guard(this, [=](FullMsgId id) {\n\t\t\t\t_parent->delegate()->elementCancelUpload(id);\n\t\t\t}),\n\t\t\tcontextId));\n\tif ((_dataMedia = _data->activeMediaView())) {\n\t\tdataMediaCreated();\n\t} else if (_data->inlineThumbnailBytes().isEmpty()\n\t\t&& (_data->hasExact(PhotoSize::Small)\n\t\t\t|| _data->hasExact(PhotoSize::Thumbnail))) {\n\t\t_data->load(PhotoSize::Small, contextId);\n\t}\n\tif (_spoiler) {\n\t\tcreateSpoilerLink(_spoiler.get());\n\t}\n\t_purchasedPriceTag = hasPurchasedTag() ? 1 : 0;\n}\n\nvoid Photo::ensureDataMediaCreated() const {\n\tif (_dataMedia) {\n\t\treturn;\n\t}\n\t_dataMedia = _data->createMediaView();\n\tdataMediaCreated();\n}\n\nvoid Photo::dataMediaCreated() const {\n\tExpects(_dataMedia != nullptr);\n\n\tif (_data->inlineThumbnailBytes().isEmpty()\n\t\t&& !_dataMedia->image(PhotoSize::Large)\n\t\t&& !_dataMedia->image(PhotoSize::Thumbnail)\n\t\t&& !_data->extendedMediaPreview()) {\n\t\t_dataMedia->wanted(PhotoSize::Small, _realParent->fullId());\n\t}\n\thistory()->owner().registerHeavyViewPart(_parent);\n\ttogglePollingStory(true);\n}\n\nbool Photo::hasHeavyPart() const {\n\treturn (_spoiler && _spoiler->animation) || _streamed || _dataMedia;\n}\n\nvoid Photo::unloadHeavyPart() {\n\tstopAnimation();\n\t_dataMedia = nullptr;\n\tif (_spoiler) {\n\t\t_spoiler->background = _spoiler->cornerCache = QImage();\n\t\t_spoiler->animation = nullptr;\n\t}\n\t_imageCache = QImage();\n\ttogglePollingStory(false);\n}\n\nbool Photo::enforceBubbleWidth() const {\n\treturn true;\n}\n\nvoid Photo::togglePollingStory(bool enabled) const {\n\tconst auto pollingStory = (enabled ? 1 : 0);\n\tif (!_storyId || _pollingStory == pollingStory) {\n\t\treturn;\n\t}\n\tconst auto polling = Data::Stories::Polling::Chat;\n\tif (!enabled) {\n\t\t_data->owner().stories().unregisterPolling(_storyId, polling);\n\t} else if (\n\t\t\t!_data->owner().stories().registerPolling(_storyId, polling)) {\n\t\treturn;\n\t}\n\t_pollingStory = pollingStory;\n}\n\nQSize Photo::countOptimalSize() {\n\tif (_serviceWidth > 0) {\n\t\treturn { int(_serviceWidth), int(_serviceWidth) };\n\t}\n\tconst auto dimensions = photoSize();\n\tconst auto scaled = CountDesiredMediaSize(dimensions);\n\tconst auto minWidth = std::clamp(\n\t\t_parent->minWidthForMedia(),\n\t\t(_parent->hasBubble()\n\t\t\t? st::historyPhotoBubbleMinWidth\n\t\t\t: st::minPhotoSize),\n\t\tst::maxMediaSize);\n\tconst auto maxActualWidth = qMax(scaled.width(), minWidth);\n\tauto maxWidth = qMax(maxActualWidth, scaled.height());\n\tauto minHeight = qMax(scaled.height(), st::minPhotoSize);\n\tif (_parent->hasBubble()) {\n\t\tconst auto botTop = _parent->Get<FakeBotAboutTop>();\n\t\tconst auto captionMaxWidth = _parent->textualMaxWidth();\n\t\tif (botTop || !_parent->data()->isFakeAboutView()) {\n\t\t\tconst auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth);\n\t\t\tmaxWidth = qMin(qMax(maxWidth, maxWithCaption), st::msgMaxWidth);\n\t\t\tminHeight = adjustHeightForLessCrop(\n\t\t\t\tdimensions,\n\t\t\t\t{ maxWidth, minHeight });\n\t\t}\n\t}\n\treturn { maxWidth, minHeight };\n}\n\nQSize Photo::countCurrentSize(int newWidth) {\n\tif (_serviceWidth) {\n\t\treturn { int(_serviceWidth), int(_serviceWidth) };\n\t}\n\tconst auto thumbMaxWidth = qMin(newWidth, st::maxMediaSize);\n\tconst auto minWidth = std::clamp(\n\t\t_parent->minWidthForMedia(),\n\t\tqMin(thumbMaxWidth, _parent->hasBubble()\n\t\t\t? st::historyPhotoBubbleMinWidth\n\t\t\t: st::minPhotoSize),\n\t\tthumbMaxWidth);\n\tconst auto dimensions = photoSize();\n\tauto pix = _data->extendedMediaVideoDuration()\n\t\t? CountMediaSize(\n\t\t\tCountDesiredMediaSize(dimensions),\n\t\t\tnewWidth)\n\t\t: CountPhotoMediaSize(\n\t\t\tCountDesiredMediaSize(dimensions),\n\t\t\tnewWidth,\n\t\t\tmaxWidth());\n\tnewWidth = qMax(pix.width(), minWidth);\n\tauto newHeight = qMax(pix.height(), st::minPhotoSize);\n\tif (_parent->hasBubble()) {\n\t\tauto captionMaxWidth = _parent->textualMaxWidth();\n\t\tconst auto botTop = _parent->Get<FakeBotAboutTop>();\n\t\tif (botTop) {\n\t\t\taccumulate_max(captionMaxWidth, botTop->maxWidth);\n\t\t}\n\t\tif (botTop || !_parent->data()->isFakeAboutView()) {\n\t\t\tconst auto maxWithCaption = qMin(\n\t\t\t\tst::msgMaxWidth,\n\t\t\t\tcaptionMaxWidth);\n\t\t\tnewWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth);\n\t\t\tnewHeight = adjustHeightForLessCrop(\n\t\t\t\tdimensions,\n\t\t\t\t{ newWidth, newHeight });\n\t\t}\n\t}\n\tif (newWidth >= maxWidth()) {\n\t\tnewHeight = qMin(newHeight, minHeight());\n\t}\n\tconst auto enlargeInner = st::historyPageEnlargeSize;\n\tconst auto enlargeOuter = 2 * st::historyPageEnlargeSkip + enlargeInner;\n\tconst auto showEnlarge = (_parent->media() != this)\n\t\t&& _parent->data()->media()\n\t\t&& !_parent->data()->isSponsored()\n\t\t&& _parent->data()->media()->webpage()\n\t\t&& _parent->data()->media()->webpage()->suggestEnlargePhoto()\n\t\t&& (newWidth >= enlargeOuter)\n\t\t&& (newHeight >= enlargeOuter);\n\t_showEnlarge = showEnlarge ? 1 : 0;\n\treturn { newWidth, newHeight };\n}\n\nint Photo::adjustHeightForLessCrop(QSize dimensions, QSize current) const {\n\tif (dimensions.isEmpty()\n\t\t|| !::Media::Streaming::FrameResizeMayExpand(current, dimensions)) {\n\t\treturn current.height();\n\t}\n\treturn qMax(\n\t\tcurrent.height(),\n\t\tcurrent.width() * dimensions.height() / dimensions.width());\n}\n\nvoid Photo::draw(Painter &p, const PaintContext &context) const {\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {\n\t\treturn;\n\t} else if (_storyId && _data->isNull()) {\n\t\treturn;\n\t}\n\n\tensureDataMediaCreated();\n\t_dataMedia->automaticLoad(_realParent->fullId(), _parent->data());\n\tconst auto st = context.st;\n\tconst auto sti = context.imageStyle();\n\tconst auto preview = _data->extendedMediaPreview();\n\tconst auto loaded = preview || _dataMedia->loaded();\n\tconst auto displayLoading = !preview && _data->displayLoading();\n\n\tauto inWebPage = (_parent->media() != this);\n\tauto paintx = 0, painty = 0, paintw = width(), painth = height();\n\tauto bubble = _parent->hasBubble();\n\n\tif (displayLoading) {\n\t\tensureAnimation();\n\t\tif (!_animation->radial.animating()) {\n\t\t\t_animation->radial.start(_dataMedia->progress());\n\t\t}\n\t}\n\tconst auto radial = isRadialAnimation();\n\n\tauto rthumb = style::rtlrect(paintx, painty, paintw, painth, width());\n\tif (_serviceWidth > 0) {\n\t\tpaintUserpicFrame(p, context, rthumb.topLeft());\n\t} else {\n\t\tconst auto rounding = inWebPage\n\t\t\t? std::optional<Ui::BubbleRounding>()\n\t\t\t: adjustedBubbleRounding();\n\t\tif (!bubble) {\n\t\t\tAssert(rounding.has_value());\n\t\t\tfillImageShadow(p, rthumb, *rounding, context);\n\t\t}\n\t\tconst auto revealed = _spoiler\n\t\t\t? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.)\n\t\t\t: 1.;\n\t\tif (revealed < 1.) {\n\t\t\tvalidateSpoilerImageCache(rthumb.size(), rounding);\n\t\t}\n\t\tif (revealed > 0.) {\n\t\t\tvalidateImageCache(rthumb.size(), rounding);\n\t\t\tp.drawImage(rthumb.topLeft(), _imageCache);\n\t\t}\n\t\tif (revealed < 1.) {\n\t\t\tp.setOpacity(1. - revealed);\n\t\t\tp.drawImage(rthumb.topLeft(), _spoiler->background);\n\t\t\tfillImageSpoiler(p, _spoiler.get(), rthumb, context);\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t\tif (context.selected()) {\n\t\t\tfillImageOverlay(p, rthumb, rounding, context);\n\t\t}\n\t}\n\n\tconst auto showEnlarge = loaded && _showEnlarge;\n\tconst auto paintInCenter = !_sensitiveSpoiler\n\t\t&& (radial || (!loaded && !_data->loading()));\n\tif (paintInCenter || showEnlarge) {\n\t\tp.setPen(Qt::NoPen);\n\t\tif (context.selected()) {\n\t\t\tp.setBrush(st->msgDateImgBgSelected());\n\t\t} else if (showEnlarge) {\n\t\t\tconst auto over = ClickHandler::showAsActive(_openl);\n\t\t\tp.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());\n\t\t} else if (isThumbAnimation()) {\n\t\t\tconst auto over = _animation->a_thumbOver.value(1.);\n\t\t\tp.setBrush(anim::brush(st->msgDateImgBg(), st->msgDateImgBgOver(), over));\n\t\t} else {\n\t\t\tconst auto over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel);\n\t\t\tp.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());\n\t\t}\n\t}\n\tif (paintInCenter) {\n\t\tconst auto radialOpacity = (radial && loaded && !_data->uploading())\n\t\t\t? _animation->radial.opacity() :\n\t\t\t1.;\n\t\tconst auto innerSize = st::msgFileLayout.thumbSize;\n\t\tQRect inner(rthumb.x() + (rthumb.width() - innerSize) / 2, rthumb.y() + (rthumb.height() - innerSize) / 2, innerSize, innerSize);\n\n\t\tp.setOpacity(radialOpacity * p.opacity());\n\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(inner);\n\t\t}\n\n\t\tp.setOpacity(radialOpacity);\n\t\tconst auto &icon = (radial || _data->loading())\n\t\t\t? sti->historyFileThumbCancel\n\t\t\t: sti->historyFileThumbDownload;\n\t\ticon.paintInCenter(p, inner);\n\t\tp.setOpacity(1);\n\t\tif (radial) {\n\t\t\tQRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));\n\t\t\t_animation->radial.draw(p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg);\n\t\t}\n\t} else if (_sensitiveSpoiler || preview) {\n\t\tdrawSpoilerTag(p, rthumb, context, [&] {\n\t\t\treturn spoilerTagBackground();\n\t\t});\n\t}\n\tif (showEnlarge) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto rect = enlargeRect();\n\t\tconst auto radius = st::historyPageEnlargeRadius;\n\t\tp.drawRoundedRect(rect, radius, radius);\n\t\tsti->historyPageEnlarge.paintInCenter(p, rect);\n\t}\n\tif (_purchasedPriceTag) {\n\t\tauto geometry = rthumb;\n\t\tif (showEnlarge) {\n\t\t\tconst auto rect = enlargeRect();\n\t\t\tgeometry.setY(rect.y() + rect.height());\n\t\t}\n\t\tdrawPurchasedTag(p, geometry, context);\n\t}\n\n\t// date\n\tif (!inWebPage && (!bubble || isBubbleBottom())) {\n\t\tauto fullRight = paintx + paintw;\n\t\tauto fullBottom = painty + painth;\n\t\tif (needInfoDisplay()) {\n\t\t\t_parent->drawInfo(\n\t\t\t\tp,\n\t\t\t\tcontext,\n\t\t\t\tfullRight,\n\t\t\t\tfullBottom,\n\t\t\t\t2 * paintx + paintw,\n\t\t\t\tInfoDisplayType::Image);\n\t\t}\n\t\tif (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) {\n\t\t\tauto fastShareLeft = _parent->hasRightLayout()\n\t\t\t\t? (paintx - size->width() - st::historyFastShareLeft)\n\t\t\t\t: (fullRight + st::historyFastShareLeft);\n\t\t\tauto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());\n\t\t\t_parent->drawRightAction(p, context, fastShareLeft, fastShareTop, 2 * paintx + paintw);\n\t\t}\n\t}\n}\n\nvoid Photo::drawSpoilerTag(\n\t\tPainter &p,\n\t\tQRect rthumb,\n\t\tconst PaintContext &context,\n\t\tFn<QImage()> generateBackground) const {\n\tMedia::drawSpoilerTag(\n\t\tp,\n\t\t_spoiler.get(),\n\t\t_spoilerTag,\n\t\trthumb,\n\t\tcontext,\n\t\tstd::move(generateBackground));\n}\n\nvoid Photo::validateUserpicImageCache(QSize size, bool forum) const {\n\tconst auto forumValue = forum ? 1 : 0;\n\tconst auto large = _dataMedia->image(PhotoSize::Large);\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto blurredValue = large ? 0 : 1;\n\tif (_imageCache.size() == (size * ratio)\n\t\t&& _imageCacheForum == forumValue\n\t\t&& _imageCacheBlurred == blurredValue) {\n\t\treturn;\n\t}\n\tauto original = [&] {\n\t\tif (large) {\n\t\t\treturn large->original();\n\t\t} else if (const auto thumbnail = _dataMedia->image(\n\t\t\t\tPhotoSize::Thumbnail)) {\n\t\t\treturn thumbnail->original();\n\t\t} else if (const auto small = _dataMedia->image(\n\t\t\t\tPhotoSize::Small)) {\n\t\t\treturn small->original();\n\t\t} else if (const auto blurred = _dataMedia->thumbnailInline()) {\n\t\t\treturn blurred->original();\n\t\t} else {\n\t\t\treturn Image::Empty()->original();\n\t\t}\n\t}();\n\tauto args = Images::PrepareArgs();\n\tif (blurredValue) {\n\t\targs = args.blurred();\n\t}\n\toriginal = Images::Prepare(std::move(original), size * ratio, args);\n\tif (forumValue) {\n\t\toriginal = Images::Round(\n\t\t\tstd::move(original),\n\t\t\tImages::CornersMask(std::min(size.width(), size.height())\n\t\t\t\t* Ui::ForumUserpicRadiusMultiplier()));\n\t} else {\n\t\toriginal = Images::Circle(std::move(original));\n\t}\n\t_imageCache = std::move(original);\n\t_imageCacheForum = forumValue;\n\t_imageCacheBlurred = blurredValue;\n}\n\nvoid Photo::validateImageCache(\n\t\tQSize outer,\n\t\tstd::optional<Ui::BubbleRounding> rounding) const {\n\tconst auto large = _dataMedia->image(PhotoSize::Large);\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto blurredValue = large ? 0 : 1;\n\tif (_imageCache.size() == (outer * ratio)\n\t\t&& _imageCacheRounding == rounding\n\t\t&& _imageCacheBlurred == blurredValue) {\n\t\treturn;\n\t}\n\t_imageCache = Images::Round(\n\t\tprepareImageCache(outer),\n\t\tMediaRoundingMask(rounding));\n\t_imageCacheRounding = rounding;\n\t_imageCacheBlurred = blurredValue;\n}\n\nvoid Photo::validateSpoilerImageCache(\n\t\tQSize outer,\n\t\tstd::optional<Ui::BubbleRounding> rounding) const {\n\tExpects(_spoiler != nullptr);\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (_spoiler->background.size() == (outer * ratio)\n\t\t&& _spoiler->backgroundRounding == rounding) {\n\t\treturn;\n\t}\n\t_spoiler->background = Images::Round(\n\t\tprepareImageCacheWithLarge(outer, nullptr),\n\t\tMediaRoundingMask(rounding));\n\t_spoiler->backgroundRounding = rounding;\n}\n\nQImage Photo::prepareImageCache(QSize outer) const {\n\treturn prepareImageCacheWithLarge(\n\t\touter,\n\t\t_dataMedia->image(PhotoSize::Large));\n}\n\nQImage Photo::prepareImageCacheWithLarge(QSize outer, Image *large) const {\n\tusing Size = PhotoSize;\n\tauto blurred = (Image*)nullptr;\n\tif (const auto embedded = _dataMedia->thumbnailInline()) {\n\t\tblurred = embedded;\n\t} else if (const auto thumbnail = _dataMedia->image(Size::Thumbnail)) {\n\t\tblurred = thumbnail;\n\t} else if (const auto small = _dataMedia->image(Size::Small)) {\n\t\tblurred = small;\n\t\t} else {\n\t\t\tblurred = large;\n\t}\n\tconst auto resize = large\n\t\t? ::Media::Streaming::DecideFrameResize(outer, large->size())\n\t\t: ::Media::Streaming::ExpandDecision();\n\treturn PrepareWithBlurredBackground(outer, resize, large, blurred);\n}\n\nvoid Photo::paintUserpicFrame(\n\tPainter &p,\n\tQPoint photoPosition,\n\tbool markFrameShown) const {\n\tconst auto autoplay = _data->videoCanBePlayed()\n\t\t&& videoAutoplayEnabled();\n\tconst auto startPlay = autoplay && !_streamed;\n\tif (startPlay) {\n\t\tconst_cast<Photo*>(this)->playAnimation(true);\n\t} else {\n\t\tcheckStreamedIsStarted();\n\t}\n\n\tconst auto size = QSize(width(), height());\n\tconst auto rect = QRect(photoPosition, size);\n\tconst auto forum = _parent->data()->history()->isForum();\n\n\tif (_streamed\n\t\t&& _streamed->instance.player().ready()\n\t\t&& !_streamed->instance.player().videoSize().isEmpty()) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tauto request = ::Media::Streaming::FrameRequest();\n\t\trequest.outer = request.resize = size * ratio;\n\t\tif (forum) {\n\t\t\tconst auto radius = int(std::min(size.width(), size.height())\n\t\t\t\t* Ui::ForumUserpicRadiusMultiplier());\n\t\t\tif (_streamed->roundingCorners[0].width() != radius * ratio) {\n\t\t\t\t_streamed->roundingCorners = Images::CornersMask(radius);\n\t\t\t}\n\t\t\trequest.rounding = Images::CornersMaskRef(\n\t\t\t\t_streamed->roundingCorners);\n\t\t} else {\n\t\t\tif (_streamed->roundingMask.size() != request.outer) {\n\t\t\t\t_streamed->roundingMask = Images::EllipseMask(size);\n\t\t\t}\n\t\t\trequest.mask = _streamed->roundingMask;\n\t\t}\n\t\tif (_streamed->instance.playerLocked()) {\n\t\t\tif (_streamed->frozenFrame.isNull()\n\t\t\t\t|| _streamed->frozenRequest != request) {\n\t\t\t\t_streamed->frozenRequest = request;\n\t\t\t\t_streamed->frozenFrame = _streamed->instance.frame(request);\n\t\t\t}\n\t\t\tp.drawImage(rect, _streamed->frozenFrame);\n\t\t} else {\n\t\t\t_streamed->frozenFrame = QImage();\n\t\t\tp.drawImage(rect, _streamed->instance.frame(request));\n\t\t\tif (markFrameShown) {\n\t\t\t\t_streamed->instance.markFrameShown();\n\t\t\t}\n\t\t}\n\t\treturn;\n\t}\n\tvalidateUserpicImageCache(size, forum);\n\tp.drawImage(rect, _imageCache);\n}\n\nvoid Photo::paintUserpicFrame(\n\tPainter &p,\n\tconst PaintContext &context,\n\tQPoint photoPosition) const {\n\tpaintUserpicFrame(p, photoPosition, !context.paused);\n\n\tif (_data->videoCanBePlayed() && !_streamed) {\n\t\tconst auto st = context.st;\n\t\tconst auto sti = context.imageStyle();\n\t\tconst auto innerSize = st::msgFileLayout.thumbSize;\n\t\tauto inner = QRect(\n\t\t\tphotoPosition.x() + (width() - innerSize) / 2,\n\t\t\tphotoPosition.y() + (height() - innerSize) / 2,\n\t\t\tinnerSize,\n\t\t\tinnerSize);\n\t\tp.setPen(Qt::NoPen);\n\t\tif (context.selected()) {\n\t\t\tp.setBrush(st->msgDateImgBgSelected());\n\t\t} else {\n\t\t\tconst auto over = ClickHandler::showAsActive(_openl);\n\t\t\tp.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());\n\t\t}\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(inner);\n\t\t}\n\t\tsti->historyFileThumbPlay.paintInCenter(p, inner);\n\t}\n}\n\nQSize Photo::photoSize() const {\n\tif (_storyId) {\n\t\treturn { kStoryWidth, kStoryHeight };\n\t} else if (_parent->data()->isFakeAboutView()\n\t\t&& !_parent->Get<FakeBotAboutTop>()) {\n\t\treturn { st::managedBotImageWidth, st::managedBotImageHeight };\n\t}\n\treturn QSize(_data->width(), _data->height());\n}\n\nQRect Photo::enlargeRect() const {\n\tconst auto skip = st::historyPageEnlargeSkip;\n\tconst auto enlargeInner = st::historyPageEnlargeSize;\n\tconst auto enlargeOuter = 2 * skip + enlargeInner;\n\treturn {\n\t\twidth() - enlargeOuter + skip,\n\t\tskip,\n\t\tenlargeInner,\n\t\tenlargeInner,\n\t};\n}\n\nClickHandlerPtr Photo::spoilerTagLink() const {\n\treturn Media::spoilerTagLink(_spoiler.get(), _spoilerTag);\n}\n\nQImage Photo::spoilerTagBackground() const {\n\treturn _spoiler ? _spoiler->background : QImage();\n}\n\nTextState Photo::textState(QPoint point, StateRequest request) const {\n\tauto result = TextState(_parent);\n\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {\n\t\treturn result;\n\t} else if (_storyId && _data->isNull()) {\n\t\treturn result;\n\t}\n\tauto paintx = 0, painty = 0, paintw = width(), painth = height();\n\tauto bubble = _parent->hasBubble();\n\n\tif (QRect(paintx, painty, paintw, painth).contains(point)) {\n\t\tensureDataMediaCreated();\n\t\tresult.link = (_spoiler && !_spoiler->revealed)\n\t\t\t? ((_data->extendedMediaPreview() || _sensitiveSpoiler)\n\t\t\t\t? spoilerTagLink()\n\t\t\t\t: _spoiler->link)\n\t\t\t: _data->uploading()\n\t\t\t? _cancell\n\t\t\t: _dataMedia->loaded()\n\t\t\t? _openl\n\t\t\t: _data->loading()\n\t\t\t? _cancell\n\t\t\t: _savel;\n\t\tif (_showEnlarge\n\t\t\t&& result.link == _openl\n\t\t\t&& enlargeRect().contains(point)) {\n\t\t\tresult.cursor = CursorState::Enlarge;\n\t\t}\n\t}\n\tif (_parent->media() == this && (!_parent->hasBubble() || isBubbleBottom())) {\n\t\tauto fullRight = paintx + paintw;\n\t\tauto fullBottom = painty + painth;\n\t\tconst auto bottomInfoResult = _parent->bottomInfoTextState(\n\t\t\tfullRight,\n\t\t\tfullBottom,\n\t\t\tpoint,\n\t\t\tInfoDisplayType::Image);\n\t\tif (bottomInfoResult.link\n\t\t\t|| bottomInfoResult.cursor != CursorState::None\n\t\t\t|| bottomInfoResult.customTooltip) {\n\t\t\treturn bottomInfoResult;\n\t\t}\n\t\tif (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) {\n\t\t\tauto fastShareLeft = _parent->hasRightLayout()\n\t\t\t\t? (paintx - size->width() - st::historyFastShareLeft)\n\t\t\t\t: (fullRight + st::historyFastShareLeft);\n\t\t\tauto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());\n\t\t\tif (QRect(fastShareLeft, fastShareTop, size->width(), size->height()).contains(point)) {\n\t\t\t\tresult.link = _parent->rightActionLink(point\n\t\t\t\t\t- QPoint(fastShareLeft, fastShareTop));\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nQSize Photo::sizeForGroupingOptimal(int maxWidth, bool last) const {\n\tconst auto size = photoSize();\n\treturn { std::max(size.width(), 1), std::max(size.height(), 1)};\n}\n\nQSize Photo::sizeForGrouping(int width) const {\n\treturn sizeForGroupingOptimal(width, false);\n}\n\nvoid Photo::drawGrouped(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry,\n\t\tRectParts sides,\n\t\tUi::BubbleRounding rounding,\n\t\tfloat64 highlightOpacity,\n\t\tnot_null<uint64*> cacheKey,\n\t\tnot_null<QPixmap*> cache) const {\n\tensureDataMediaCreated();\n\t_dataMedia->automaticLoad(_realParent->fullId(), _parent->data());\n\n\tconst auto st = context.st;\n\tconst auto sti = context.imageStyle();\n\tconst auto preview = _data->extendedMediaPreview();\n\tconst auto loaded = preview || _dataMedia->loaded();\n\tconst auto displayLoading = !preview && _data->displayLoading();\n\n\tif (displayLoading) {\n\t\tensureAnimation();\n\t\tif (!_animation->radial.animating()) {\n\t\t\t_animation->radial.start(_dataMedia->progress());\n\t\t}\n\t}\n\tconst auto radial = isRadialAnimation();\n\n\tconst auto revealed = _spoiler\n\t\t? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.)\n\t\t: 1.;\n\tif (revealed < 1.) {\n\t\tvalidateSpoilerImageCache(geometry.size(), rounding);\n\t}\n\tif (revealed > 0.) {\n\t\tvalidateGroupedCache(geometry, rounding, cacheKey, cache);\n\t\tp.drawPixmap(geometry.topLeft(), *cache);\n\t}\n\tif (revealed < 1.) {\n\t\tp.setOpacity(1. - revealed);\n\t\tp.drawImage(geometry.topLeft(), _spoiler->background);\n\t\tfillImageSpoiler(p, _spoiler.get(), geometry, context);\n\t\tp.setOpacity(1.);\n\t}\n\n\tconst auto overlayOpacity = context.selected()\n\t\t? (1. - highlightOpacity)\n\t\t: highlightOpacity;\n\tif (overlayOpacity > 0.) {\n\t\tp.setOpacity(overlayOpacity);\n\t\tfillImageOverlay(p, geometry, rounding, context);\n\t\tif (!context.selected()) {\n\t\t\tfillImageOverlay(p, geometry, rounding, context);\n\t\t}\n\t\tp.setOpacity(1.);\n\t}\n\n\tconst auto paintInCenter = !_sensitiveSpoiler\n\t\t&& (radial\n\t\t\t|| (!loaded && !_data->loading())\n\t\t\t|| _data->waitingForAlbum());\n\tif (paintInCenter) {\n\t\tconst auto radialOpacity = radial\n\t\t\t? _animation->radial.opacity()\n\t\t\t: 1.;\n\t\tconst auto backOpacity = (loaded && !_data->uploading())\n\t\t\t? radialOpacity\n\t\t\t: 1.;\n\t\tconst auto radialSize = st::historyGroupRadialSize;\n\t\tconst auto inner = QRect(\n\t\t\tgeometry.x() + (geometry.width() - radialSize) / 2,\n\t\t\tgeometry.y() + (geometry.height() - radialSize) / 2,\n\t\t\tradialSize,\n\t\t\tradialSize);\n\t\tp.setPen(Qt::NoPen);\n\t\tif (context.selected()) {\n\t\t\tp.setBrush(st->msgDateImgBgSelected());\n\t\t} else if (isThumbAnimation()) {\n\t\t\tauto over = _animation->a_thumbOver.value(1.);\n\t\t\tp.setBrush(anim::brush(st->msgDateImgBg(), st->msgDateImgBgOver(), over));\n\t\t} else {\n\t\t\tauto over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel);\n\t\t\tp.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());\n\t\t}\n\n\t\tp.setOpacity(backOpacity * p.opacity());\n\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(inner);\n\t\t}\n\n\t\tconst auto &icon = _data->waitingForAlbum()\n\t\t\t? sti->historyFileThumbWaiting\n\t\t\t: (radial || _data->loading())\n\t\t\t? sti->historyFileThumbCancel\n\t\t\t: sti->historyFileThumbDownload;\n\t\tconst auto previous = _data->waitingForAlbum()\n\t\t\t? &sti->historyFileThumbCancel\n\t\t\t: nullptr;\n\t\tp.setOpacity(backOpacity);\n\t\tif (previous && radialOpacity > 0. && radialOpacity < 1.) {\n\t\t\tPaintInterpolatedIcon(p, icon, *previous, radialOpacity, inner);\n\t\t} else {\n\t\t\ticon.paintInCenter(p, inner);\n\t\t}\n\t\tp.setOpacity(1);\n\t\tif (radial) {\n\t\t\tconst auto line = st::historyGroupRadialLine;\n\t\t\tconst auto rinner = inner.marginsRemoved({ line, line, line, line });\n\t\t\t_animation->radial.draw(p, rinner, line, sti->historyFileThumbRadialFg);\n\t\t}\n\t}\n}\n\nTextState Photo::getStateGrouped(\n\t\tconst QRect &geometry,\n\t\tRectParts sides,\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tif (!geometry.contains(point)) {\n\t\treturn {};\n\t}\n\tensureDataMediaCreated();\n\tauto link = (_spoiler && !_spoiler->revealed)\n\t\t? ((_data->extendedMediaPreview() || _sensitiveSpoiler)\n\t\t\t? spoilerTagLink()\n\t\t\t: _spoiler->link)\n\t\t: _data->uploading()\n\t\t? _cancell\n\t\t: _dataMedia->loaded()\n\t\t? _openl\n\t\t: _data->loading()\n\t\t? _cancell\n\t\t: _savel;\n\treturn TextState(_parent, std::move(link));\n}\n\nfloat64 Photo::dataProgress() const {\n\tensureDataMediaCreated();\n\treturn _dataMedia->progress();\n}\n\nbool Photo::dataFinished() const {\n\treturn !_data->loading()\n\t\t&& (!_data->uploading() || _data->waitingForAlbum());\n}\n\nbool Photo::dataLoaded() const {\n\tensureDataMediaCreated();\n\treturn _dataMedia->loaded();\n}\n\nbool Photo::needInfoDisplay() const {\n\tif (_parent->data()->isFakeAboutView()) {\n\t\treturn false;\n\t}\n\treturn _parent->data()->isSending()\n\t\t|| _parent->data()->hasFailed()\n\t\t|| _parent->isUnderCursor()\n\t\t|| (_parent->delegate()->elementContext() == Context::ChatPreview)\n\t\t|| _parent->isLastAndSelfMessage();\n}\n\nvoid Photo::validateGroupedCache(\n\t\tconst QRect &geometry,\n\t\tUi::BubbleRounding rounding,\n\t\tnot_null<uint64*> cacheKey,\n\t\tnot_null<QPixmap*> cache) const {\n\tusing Option = Images::Option;\n\n\tensureDataMediaCreated();\n\n\tconst auto preview = _data->extendedMediaPreview();\n\tconst auto loaded = preview || _dataMedia->loaded();\n\tconst auto loadLevel = loaded\n\t\t? 2\n\t\t: (_dataMedia->thumbnailInline()\n\t\t\t|| _dataMedia->image(PhotoSize::Small)\n\t\t\t|| _dataMedia->image(PhotoSize::Thumbnail))\n\t\t? 1\n\t\t: 0;\n\tconst auto width = geometry.width();\n\tconst auto height = geometry.height();\n\tconst auto options = (loaded ? Option() : Option::Blur);\n\tconst auto key = (uint64(width) << 48)\n\t\t| (uint64(height) << 32)\n\t\t| (uint64(options) << 16)\n\t\t| (uint64(rounding.key()) << 8)\n\t\t| (uint64(loadLevel));\n\tif (*cacheKey == key) {\n\t\treturn;\n\t}\n\n\tconst auto unscaled = photoSize();\n\tconst auto originalWidth = style::ConvertScale(unscaled.width());\n\tconst auto originalHeight = style::ConvertScale(unscaled.height());\n\tconst auto pixSize = Ui::GetImageScaleSizeForGeometry(\n\t\t{ originalWidth, originalHeight },\n\t\t{ width, height });\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto image = _dataMedia->image(PhotoSize::Large)\n\t\t? _dataMedia->image(PhotoSize::Large)\n\t\t: _dataMedia->image(PhotoSize::Thumbnail)\n\t\t? _dataMedia->image(PhotoSize::Thumbnail)\n\t\t: _dataMedia->image(PhotoSize::Small)\n\t\t? _dataMedia->image(PhotoSize::Small)\n\t\t: _dataMedia->thumbnailInline()\n\t\t? _dataMedia->thumbnailInline()\n\t\t: Image::BlankMedia().get();\n\n\t*cacheKey = key;\n\tauto scaled = Images::Prepare(\n\t\timage->original(),\n\t\tpixSize * ratio,\n\t\t{ .options = options, .outer = { width, height } });\n\tauto rounded = Images::Round(\n\t\tstd::move(scaled),\n\t\tMediaRoundingMask(rounding));\n\t*cache = Ui::PixmapFromImage(std::move(rounded));\n}\n\nbool Photo::createStreamingObjects() {\n\tusing namespace ::Media::Streaming;\n\n\tsetStreamed(std::make_unique<Streamed>(\n\t\thistory()->owner().streaming().sharedDocument(\n\t\t\t_data,\n\t\t\t_realParent->fullId())));\n\t_streamed->instance.player().updates(\n\t) | rpl::on_next_error([=](Update &&update) {\n\t\thandleStreamingUpdate(std::move(update));\n\t}, [=](Error &&error) {\n\t\thandleStreamingError(std::move(error));\n\t}, _streamed->instance.lifetime());\n\tif (_streamed->instance.ready()) {\n\t\tstreamingReady(base::duplicate(_streamed->instance.info()));\n\t}\n\tif (!_streamed->instance.valid()) {\n\t\tstopAnimation();\n\t\treturn false;\n\t}\n\tcheckStreamedIsStarted();\n\treturn true;\n}\n\nvoid Photo::setStreamed(std::unique_ptr<Streamed> value) {\n\tconst auto removed = (_streamed && !value);\n\tconst auto set = (!_streamed && value);\n\t_streamed = std::move(value);\n\tif (set) {\n\t\thistory()->owner().registerHeavyViewPart(_parent);\n\t\ttogglePollingStory(true);\n\t} else if (removed) {\n\t\t_parent->checkHeavyPart();\n\t}\n}\n\nvoid Photo::handleStreamingUpdate(::Media::Streaming::Update &&update) {\n\tusing namespace ::Media::Streaming;\n\n\tv::match(update.data, [&](Information &update) {\n\t\tstreamingReady(std::move(update));\n\t}, [](PreloadedVideo) {\n\t}, [&](UpdateVideo) {\n\t\trepaintStreamedContent();\n\t}, [](PreloadedAudio) {\n\t}, [](UpdateAudio) {\n\t}, [](WaitingForData) {\n\t}, [](SpeedEstimate) {\n\t}, [](MutedByOther) {\n\t}, [](Finished) {\n\t});\n}\n\nvoid Photo::handleStreamingError(::Media::Streaming::Error &&error) {\n\t_data->setVideoPlaybackFailed();\n\tstopAnimation();\n}\n\nvoid Photo::repaintStreamedContent() {\n\tif (_streamed && !_streamed->frozenFrame.isNull()) {\n\t\treturn;\n\t} else if (_parent->delegate()->elementAnimationsPaused()) {\n\t\treturn;\n\t}\n\trepaint();\n}\n\nvoid Photo::streamingReady(::Media::Streaming::Information &&info) {\n\trepaint();\n}\n\nvoid Photo::checkAnimation() {\n\tif (_streamed && !videoAutoplayEnabled()) {\n\t\tstopAnimation();\n\t}\n}\n\nvoid Photo::stopAnimation() {\n\tsetStreamed(nullptr);\n}\n\nvoid Photo::playAnimation(bool autoplay) {\n\tensureDataMediaCreated();\n\tif (_streamed && autoplay) {\n\t\treturn;\n\t} else if (_streamed && videoAutoplayEnabled()) {\n\t\tshowPhoto(_parent->data()->fullId());\n\t\treturn;\n\t}\n\tif (_streamed) {\n\t\tstopAnimation();\n\t} else if (_data->videoCanBePlayed()) {\n\t\tif (!videoAutoplayEnabled()) {\n\t\t\thistory()->owner().checkPlayingAnimations();\n\t\t}\n\t\tif (!createStreamingObjects()) {\n\t\t\t_data->setVideoPlaybackFailed();\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nvoid Photo::checkStreamedIsStarted() const {\n\tif (!_streamed) {\n\t\treturn;\n\t} else if (_streamed->instance.paused()) {\n\t\t_streamed->instance.resume();\n\t}\n\tif (_streamed\n\t\t&& !_streamed->instance.active()\n\t\t&& !_streamed->instance.failed()) {\n\t\tconst auto position = _data->videoStartPosition();\n\t\tauto options = ::Media::Streaming::PlaybackOptions();\n\t\toptions.position = position;\n\t\toptions.mode = ::Media::Streaming::Mode::Video;\n\t\toptions.loop = true;\n\t\t_streamed->instance.play(options);\n\t}\n}\n\nbool Photo::videoAutoplayEnabled() const {\n\treturn Data::AutoDownload::ShouldAutoPlay(\n\t\t_data->session().settings().autoDownload(),\n\t\t_realParent->history()->peer,\n\t\t_data);\n}\n\nvoid Photo::hideSpoilers() {\n\tif (_spoiler) {\n\t\t_spoiler->revealed = false;\n\t}\n}\n\nbool Photo::needsBubble() const {\n\tif (_storyId) {\n\t\treturn true;\n\t}\n\tconst auto item = _parent->data();\n\treturn !item->isService()\n\t\t&& (item->repliesAreComments()\n\t\t\t|| item->externalReply()\n\t\t\t|| item->viaBot()\n\t\t\t|| !item->emptyText()\n\t\t\t|| _parent->displayReply()\n\t\t\t|| _parent->displayForwardedFrom()\n\t\t\t|| _parent->displayFromName()\n\t\t\t|| _parent->displayedTopicButton());\n}\n\nQPoint Photo::resolveCustomInfoRightBottom() const {\n\tconst auto skipx = (st::msgDateImgDelta + st::msgDateImgPadding.x());\n\tconst auto skipy = (st::msgDateImgDelta + st::msgDateImgPadding.y());\n\treturn QPoint(width() - skipx, height() - skipy);\n}\n\nbool Photo::isReadyForOpen() const {\n\tensureDataMediaCreated();\n\treturn _dataMedia->loaded();\n}\n\nvoid Photo::showPhoto(FullMsgId id) {\n\t_parent->delegate()->elementOpenPhoto(_data, id);\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_photo.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_file.h\"\n\nclass Image;\nenum class ImageRoundRadius;\n\nnamespace Data {\nclass PhotoMedia;\n} // namespace Data\n\nnamespace Media {\nnamespace Streaming {\nclass Instance;\nstruct Update;\nenum class Error;\nstruct Information;\n} // namespace Streaming\n} // namespace Media\n\nnamespace HistoryView {\n\nclass Photo final : public File {\npublic:\n\tPhoto(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<HistoryItem*> realParent,\n\t\tnot_null<PhotoData*> photo,\n\t\tbool spoiler);\n\tPhoto(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<PeerData*> chat,\n\t\tnot_null<PhotoData*> photo,\n\t\tint width);\n\t~Photo();\n\n\tbool hideMessageText() const override {\n\t\treturn false;\n\t}\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\n\tPhotoData *getPhoto() const override {\n\t\treturn _data;\n\t}\n\tvoid showPhoto(FullMsgId id);\n\n\tvoid paintUserpicFrame(\n\t\tPainter &p,\n\t\tQPoint photoPosition,\n\t\tbool markFrameShown) const;\n\n\tQSize sizeForGroupingOptimal(int maxWidth, bool last) const override;\n\tQSize sizeForGrouping(int width) const override;\n\tvoid drawGrouped(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry,\n\t\tRectParts sides,\n\t\tUi::BubbleRounding rounding,\n\t\tfloat64 highlightOpacity,\n\t\tnot_null<uint64*> cacheKey,\n\t\tnot_null<QPixmap*> cache) const override;\n\tTextState getStateGrouped(\n\t\tconst QRect &geometry,\n\t\tRectParts sides,\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\tvoid drawSpoilerTag(\n\t\tPainter &p,\n\t\tQRect rthumb,\n\t\tconst PaintContext &context,\n\t\tFn<QImage()> generateBackground) const override;\n\tClickHandlerPtr spoilerTagLink() const override;\n\tQImage spoilerTagBackground() const override;\n\n\tvoid hideSpoilers() override;\n\tbool needsBubble() const override;\n\tbool customInfoLayout() const override {\n\t\treturn true;\n\t}\n\tQPoint resolveCustomInfoRightBottom() const override;\n\tbool skipBubbleTail() const override {\n\t\treturn isRoundedInBubbleBottom();\n\t}\n\tbool isReadyForOpen() const override;\n\n\tbool hasHeavyPart() const override;\n\tvoid unloadHeavyPart() override;\n\tbool enforceBubbleWidth() const override;\n\nprotected:\n\tfloat64 dataProgress() const override;\n\tbool dataFinished() const override;\n\tbool dataLoaded() const override;\n\nprivate:\n\tstruct Streamed;\n\n\tvoid create(FullMsgId contextId, PeerData *chat = nullptr);\n\n\tvoid playAnimation(bool autoplay) override;\n\tvoid stopAnimation() override;\n\tvoid checkAnimation() override;\n\n\tvoid ensureDataMediaCreated() const;\n\tvoid dataMediaCreated() const;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\t[[nodiscard]] int adjustHeightForLessCrop(\n\t\tQSize dimensions,\n\t\tQSize current) const;\n\n\tbool needInfoDisplay() const;\n\tvoid validateGroupedCache(\n\t\tconst QRect &geometry,\n\t\tUi::BubbleRounding rounding,\n\t\tnot_null<uint64*> cacheKey,\n\t\tnot_null<QPixmap*> cache) const;\n\tvoid validateImageCache(\n\t\tQSize outer,\n\t\tstd::optional<Ui::BubbleRounding> rounding) const;\n\tvoid validateUserpicImageCache(QSize size, bool forum) const;\n\t[[nodiscard]] QImage prepareImageCache(QSize outer) const;\n\tvoid validateSpoilerImageCache(\n\t\tQSize outer,\n\t\tstd::optional<Ui::BubbleRounding> rounding) const;\n\t[[nodiscard]] QImage prepareImageCacheWithLarge(\n\t\tQSize outer,\n\t\tImage *large) const;\n\n\tbool videoAutoplayEnabled() const;\n\tvoid setStreamed(std::unique_ptr<Streamed> value);\n\tvoid repaintStreamedContent();\n\tvoid checkStreamedIsStarted() const;\n\tbool createStreamingObjects();\n\tvoid handleStreamingUpdate(::Media::Streaming::Update &&update);\n\tvoid handleStreamingError(::Media::Streaming::Error &&error);\n\tvoid streamingReady(::Media::Streaming::Information &&info);\n\tvoid paintUserpicFrame(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tQPoint photoPosition) const;\n\n\t[[nodiscard]] QSize photoSize() const;\n\t[[nodiscard]] QRect enlargeRect() const;\n\n\tvoid togglePollingStory(bool enabled) const;\n\n\tconst not_null<PhotoData*> _data;\n\tconst FullStoryId _storyId;\n\tmutable std::shared_ptr<Data::PhotoMedia> _dataMedia;\n\tmutable std::unique_ptr<Streamed> _streamed;\n\tconst std::unique_ptr<MediaSpoiler> _spoiler;\n\tmutable std::unique_ptr<MediaSpoilerTag> _spoilerTag;\n\tmutable QImage _imageCache;\n\tmutable std::optional<Ui::BubbleRounding> _imageCacheRounding;\n\tuint32 _serviceWidth : 26 = 0;\n\tuint32 _purchasedPriceTag : 1 = 0;\n\tconst uint32 _sensitiveSpoiler : 1 = 0;\n\tmutable uint32 _imageCacheForum : 1 = 0;\n\tmutable uint32 _imageCacheBlurred : 1 = 0;\n\tmutable uint32 _pollingStory : 1 = 0;\n\tmutable uint32 _showEnlarge : 1 = 0;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_poll.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_poll.h\"\n\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\" // TextContext\n#include \"data/data_cloud_file.h\"\n#include \"data/data_location.h\"\n#include \"lang/lang_keys.h\"\n#include \"lang/lang_tag.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/view/history_view_message.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_reaction_preview.h\"\n#include \"history/view/history_view_text_helper.h\"\n#include \"history/view/media/menu/history_view_poll_menu.h\"\n#include \"calls/calls_instance.h\"\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/chat/message_bubble.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/image/image.h\"\n#include \"ui/item_text_options.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/fireworks_animation.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"history/view/media/history_view_location.h\"\n#include \"history/view/media/history_view_media_common.h\"\n#include \"history/view/history_view_group_call_bar.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_document.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"base/crc32hash.h\"\n#include \"base/unixtime.h\"\n#include \"base/timer.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"api/api_polls.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_polls.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_window.h\"\n\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kShowRecentVotersCount = 3;\nconstexpr auto kRotateSegments = 8;\nconstexpr auto kRotateAmplitude = 3.;\nconstexpr auto kScaleSegments = 2;\nconstexpr auto kScaleAmplitude = 0.03;\nconstexpr auto kRollDuration = crl::time(400);\n\n[[nodiscard]] int PollAnswerMediaSize() {\n\treturn st::historyPollRadio.diameter * 2;\n}\n\n[[nodiscard]] int PollAnswerMediaSkip() {\n\treturn st::historyPollPercentSkip * 2;\n}\n\nenum class PollThumbnailKind {\n\tNone,\n\tPhoto,\n\tDocument,\n\tAudio,\n\tEmoji,\n\tGeo,\n};\n\nstruct PercentCounterItem {\n\tint index = 0;\n\tint percent = 0;\n\tint remainder = 0;\n\n\tinline bool operator==(const PercentCounterItem &o) const {\n\t\treturn remainder == o.remainder && percent == o.percent;\n\t}\n\n\tinline bool operator<(const PercentCounterItem &other) const {\n\t\tif (remainder > other.remainder) {\n\t\t\treturn true;\n\t\t} else if (remainder < other.remainder) {\n\t\t\treturn false;\n\t\t}\n\t\treturn percent < other.percent;\n\t}\n};\n\nvoid AdjustPercentCount(gsl::span<PercentCounterItem> items, int left) {\n\tranges::sort(items, std::less<>());\n\tfor (auto i = 0, count = int(items.size()); i != count;) {\n\t\tconst auto &item = items[i];\n\t\tauto j = i + 1;\n\t\tfor (; j != count; ++j) {\n\t\t\tif (items[j].percent != item.percent\n\t\t\t\t|| items[j].remainder != item.remainder) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (!items[i].remainder) {\n\t\t\t// If this item has correct value in 'percent' we don't want\n\t\t\t// to increment it to an incorrect one. This fixes a case with\n\t\t\t// four items with three votes for three different items.\n\t\t\tbreak;\n\t\t}\n\t\tconst auto equal = j - i;\n\t\tif (equal <= left) {\n\t\t\tleft -= equal;\n\t\t\tfor (; i != j; ++i) {\n\t\t\t\t++items[i].percent;\n\t\t\t}\n\t\t} else {\n\t\t\ti = j;\n\t\t}\n\t}\n}\n\nvoid CountNicePercent(\n\t\tgsl::span<const int> votes,\n\t\tint total,\n\t\tgsl::span<int> result) {\n\tExpects(result.size() >= votes.size());\n\tExpects(votes.size() <= PollData::kMaxOptions);\n\n\tconst auto count = size_type(votes.size());\n\tPercentCounterItem ItemsStorage[PollData::kMaxOptions];\n\tconst auto items = gsl::make_span(ItemsStorage).subspan(0, count);\n\tauto left = 100;\n\tauto &&zipped = ranges::views::zip(\n\t\tvotes,\n\t\titems,\n\t\tranges::views::ints(0, int(items.size())));\n\tfor (auto &&[votes, item, index] : zipped) {\n\t\titem.index = index;\n\t\titem.percent = (votes * 100) / total;\n\t\titem.remainder = (votes * 100) - (item.percent * total);\n\t\tleft -= item.percent;\n\t}\n\tif (left > 0 && left <= count) {\n\t\tAdjustPercentCount(items, left);\n\t}\n\tfor (const auto &item : items) {\n\t\tresult[item.index] = item.percent;\n\t}\n}\n\n[[nodiscard]] uint32 HashPollShuffleValue(\n\t\tUserId userId,\n\t\tPollId pollId,\n\t\tconst QByteArray &option) {\n\tauto hash = QByteArray::number(quint64(userId.bare))\n\t\t+ option\n\t\t+ QByteArray::number(quint64(pollId));\n\treturn uint32(base::crc32(hash.constData(), hash.size()));\n}\n\nstruct PollThumbnailData {\n\tstd::shared_ptr<Ui::DynamicImage> thumbnail;\n\tClickHandlerPtr handler;\n\tPollThumbnailKind kind = PollThumbnailKind::None;\n\tbool rounded = false;\n\tbool isVideo = false;\n\tuint64 id = 0;\n};\n\n[[nodiscard]] PollThumbnailData MakePollThumbnail(\n\t\tnot_null<PollData*> poll,\n\t\tconst PollAnswer &answer,\n\t\tWindow::SessionController::MessageContext messageContext,\n\t\tFn<bool()> paused = nullptr);\n\n[[nodiscard]] PollThumbnailData MakePollThumbnail(\n\t\tnot_null<PollData*> poll,\n\t\tconst PollMedia &media,\n\t\tWindow::SessionController::MessageContext messageContext,\n\t\tFn<bool()> paused = nullptr) {\n\tauto result = PollThumbnailData();\n\tif (!media) {\n\t\treturn result;\n\t}\n\tif (media.photo) {\n\t\tresult.id = uint64(media.photo->id);\n\t\tresult.thumbnail = Ui::MakePhotoThumbnailCenterCrop(\n\t\t\tmedia.photo,\n\t\t\tmessageContext.id);\n\t\tresult.rounded = true;\n\t\tresult.kind = PollThumbnailKind::Photo;\n\t} else if (media.document) {\n\t\tresult.id = uint64(media.document->id);\n\t\tif (media.document->sticker()) {\n\t\t\tresult.thumbnail = Ui::MakeEmojiThumbnail(\n\t\t\t\t&poll->owner(),\n\t\t\t\tData::SerializeCustomEmojiId(media.document),\n\t\t\t\tpaused);\n\t\t\tresult.kind = PollThumbnailKind::Emoji;\n\t\t} else if (media.document->isSong()\n\t\t\t|| media.document->isVoiceMessage()) {\n\t\t\tresult.thumbnail = Ui::MakeDocumentFilePreviewThumbnail(\n\t\t\t\tmedia.document,\n\t\t\t\tmessageContext.id);\n\t\t\tresult.kind = PollThumbnailKind::Audio;\n\t\t} else {\n\t\t\tresult.thumbnail = Ui::MakeDocumentThumbnailCenterCrop(\n\t\t\t\tmedia.document,\n\t\t\t\tmessageContext.id);\n\t\t\tresult.rounded = true;\n\t\t\tresult.kind = PollThumbnailKind::Document;\n\t\t\tresult.isVideo = media.document->isVideoFile()\n\t\t\t\t|| media.document->isVideoMessage()\n\t\t\t\t|| media.document->isAnimation();\n\t\t}\n\t} else if (media.geo) {\n\t\tresult.id = uint64(media.geo->hash());\n\t\tconst auto cloudImage = poll->owner().location(*media.geo);\n\t\tresult.thumbnail = Ui::MakeGeoThumbnailWithPin(\n\t\t\tcloudImage,\n\t\t\t&poll->session(),\n\t\t\tData::FileOrigin());\n\t\tresult.rounded = true;\n\t\tresult.kind = PollThumbnailKind::Geo;\n\t}\n\tif (result.kind == PollThumbnailKind::Photo && result.id) {\n\t\tconst auto photo = media.photo;\n\t\tconst auto session = &poll->session();\n\t\tresult.handler = std::make_shared<LambdaClickHandler>(\n\t\t\t[=](ClickContext context) {\n\t\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\t\tconst auto controller = my.sessionWindow.get();\n\t\t\t\tif (!controller || (&controller->session() != session)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcontroller->openPhoto(photo, messageContext);\n\t\t\t});\n\t} else if ((result.kind == PollThumbnailKind::Document\n\t\t|| result.kind == PollThumbnailKind::Audio) && result.id) {\n\t\tconst auto document = media.document;\n\t\tconst auto session = &poll->session();\n\t\tresult.handler = std::make_shared<LambdaClickHandler>(\n\t\t\t[=](ClickContext context) {\n\t\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\t\tconst auto controller = my.sessionWindow.get();\n\t\t\t\tif (!controller || (&controller->session() != session)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcontroller->openDocument(document, true, messageContext);\n\t\t\t});\n\t} else if (result.kind == PollThumbnailKind::Geo && media.geo) {\n\t\tconst auto point = *media.geo;\n\t\tconst auto session = &poll->session();\n\t\tresult.handler = std::make_shared<LambdaClickHandler>(\n\t\t\t[=](ClickContext context) {\n\t\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\t\tconst auto controller = my.sessionWindow.get();\n\t\t\t\tif (!controller || (&controller->session() != session)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tHistoryView::ShowPollGeoPreview(controller, point);\n\t\t\t});\n\t}\n\treturn result;\n}\n\nPollThumbnailData MakePollThumbnail(\n\t\tnot_null<PollData*> poll,\n\t\tconst PollAnswer &answer,\n\t\tWindow::SessionController::MessageContext messageContext,\n\t\tFn<bool()> paused) {\n\tauto result\n\t\t= MakePollThumbnail(poll, answer.media, messageContext, paused);\n\tif (result.kind == PollThumbnailKind::Emoji && result.id) {\n\t\tconst auto documentId = DocumentId(result.id);\n\t\tconst auto option = answer.option;\n\t\tconst auto session = &poll->session();\n\t\tresult.handler = std::make_shared<LambdaClickHandler>(\n\t\t\t[=](ClickContext context) {\n\t\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\t\tconst auto controller = my.sessionWindow.get();\n\t\t\t\tif (!controller || (&controller->session() != session)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto document = poll->owner().document(documentId);\n\t\t\t\tconst auto itemId = messageContext.id;\n\t\t\t\tShowStickerPreview(\n\t\t\t\t\tcontroller,\n\t\t\t\t\titemId,\n\t\t\t\t\tdocument,\n\t\t\t\t\t[=](not_null<Ui::DropdownMenu*> menu) {\n\t\t\t\t\t\tFillPollAnswerMenu(\n\t\t\t\t\t\t\tmenu,\n\t\t\t\t\t\t\tpoll,\n\t\t\t\t\t\t\toption,\n\t\t\t\t\t\t\tdocument,\n\t\t\t\t\t\t\titemId,\n\t\t\t\t\t\t\tcontroller);\n\t\t\t\t\t});\n\t\t\t});\n\t}\n\tif (result.handler) {\n\t\tresult.handler->setProperty(\n\t\t\tkPollOptionProperty,\n\t\t\tanswer.option);\n\t}\n\treturn result;\n}\n\n} // namespace\n\nstruct Poll::AnswerAnimation {\n\tanim::value percent;\n\tanim::value filling;\n\tanim::value opacity;\n\tbool chosen = false;\n\tbool correct = false;\n};\n\nstruct Poll::AnswersAnimation {\n\tstd::vector<AnswerAnimation> data;\n\tUi::Animations::Simple progress;\n};\n\nstruct Poll::SendingAnimation {\n\ttemplate <typename Callback>\n\tSendingAnimation(\n\t\tconst QByteArray &option,\n\t\tCallback &&callback);\n\n\tQByteArray option;\n\tUi::InfiniteRadialAnimation animation;\n};\n\nstruct Poll::Answer {\n\tAnswer();\n\n\tvoid fillData(\n\t\tnot_null<PollData*> poll,\n\t\tconst PollAnswer &original,\n\t\tUi::Text::MarkedContext context);\n\tvoid fillMedia(\n\t\tnot_null<PollData*> poll,\n\t\tconst PollAnswer &original,\n\t\tWindow::SessionController::MessageContext messageContext,\n\t\tFn<void()> repaint,\n\t\tFn<bool()> paused);\n\n\tUi::Text::String text;\n\tQByteArray option;\n\tint votes = 0;\n\tint votesPercent = 0;\n\tint votesPercentWidth = 0;\n\tfloat64 filling = 0.;\n\tQString votesPercentString;\n\tQString votesCountString;\n\tint votesCountWidth = 0;\n\tbool chosen = false;\n\tbool correct = false;\n\tbool selected = false;\n\tClickHandlerPtr handler;\n\tClickHandlerPtr mediaHandler;\n\tUi::Animations::Simple selectedAnimation;\n\tstd::shared_ptr<Ui::DynamicImage> thumbnail;\n\tbool thumbnailRounded = false;\n\tbool thumbnailIsVideo = false;\n\tPollThumbnailKind thumbnailKind = PollThumbnailKind::None;\n\tuint64 thumbnailId = 0;\n\tmutable std::unique_ptr<Ui::RippleAnimation> ripple;\n\tstd::vector<UserpicInRow> recentVoters;\n\tmutable QImage recentVotersImage;\n};\n\nstruct Poll::AttachedMedia {\n\tstd::shared_ptr<Ui::DynamicImage> thumbnail;\n\tClickHandlerPtr handler;\n\tPhotoData *photo = nullptr;\n\tstd::shared_ptr<Data::PhotoMedia> photoMedia;\n\tQSize photoSize;\n\tPollThumbnailKind kind = PollThumbnailKind::None;\n\tbool rounded = false;\n\tuint64 id = 0;\n};\n\nstruct Poll::SolutionMedia {\n\tPollThumbnailKind kind = PollThumbnailKind::None;\n\tuint64 id = 0;\n};\n\nstruct Poll::RecentVoter {\n\tnot_null<PeerData*> peer;\n\tmutable Ui::PeerUserpicView userpic;\n};\n\nstruct Poll::Part {\n\texplicit Part(not_null<Poll*> owner) : _owner(owner) {}\n\tvirtual ~Part() = default;\n\n\t[[nodiscard]] virtual int countHeight(int innerWidth) const = 0;\n\tvirtual void draw(\n\t\tPainter &p,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tconst PaintContext &context) const = 0;\n\t[[nodiscard]] virtual TextState textState(\n\t\tQPoint point,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tStateRequest request) const = 0;\n\tvirtual void clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) {}\n\tvirtual void clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool active) {}\n\t[[nodiscard]] virtual bool hasHeavyPart() const { return false; }\n\tvirtual void unloadHeavyPart() {}\n\t[[nodiscard]] virtual uint16 selectionLength() const { return 0; }\n\nprotected:\n\tconst not_null<Poll*> _owner;\n};\n\nstruct Poll::Footer : public Poll::Part {\n\texplicit Footer(not_null<Poll*> owner);\n\n\tint countHeight(int innerWidth) const override;\n\tvoid draw(\n\t\tPainter &p,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tconst PaintContext &context) const override;\n\tTextState textState(\n\t\tQPoint point,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tStateRequest request) const override;\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) override;\n\n\tvoid updateTotalVotes();\n\n\tUi::Text::String _totalVotesLabel;\n\tUi::Text::String _adminVotesLabel;\n\tUi::Text::String _adminBackVoteLabel;\n\tClickHandlerPtr _showResultsLink;\n\tClickHandlerPtr _sendVotesLink;\n\tClickHandlerPtr _adminVotesLink;\n\tClickHandlerPtr _adminBackVoteLink;\n\tClickHandlerPtr _saveOptionLink;\n\tmutable std::unique_ptr<Ui::RippleAnimation> _linkRipple;\n\tmutable int _linkRippleShift = 0;\n\tmutable base::Timer _closeTimer;\n\nprivate:\n\t[[nodiscard]] int topSkip() const;\n\t[[nodiscard]] int textTop() const;\n\t[[nodiscard]] bool hasTimerLine(int innerWidth) const;\n\t[[nodiscard]] bool hasCloseDate() const;\n\t[[nodiscard]] QString closeTimerText() const;\n\t[[nodiscard]] bool timerFooterMultiline(int paintw) const;\n\t[[nodiscard]] bool centeredOverlapsInfo(\n\t\tint textWidth,\n\t\tint innerWidth) const;\n\t[[nodiscard]] int bottomLineWidth(int innerWidth) const;\n\t[[nodiscard]] int dateInfoPadding(int innerWidth) const;\n\tvoid toggleLinkRipple(bool pressed);\n};\n\nint Poll::Footer::topSkip() const {\n\treturn _owner->canAddOption()\n\t\t? 0\n\t\t: st::historyPollTotalVotesSkip;\n}\n\nint Poll::Footer::textTop() const {\n\treturn topSkip()\n\t\t+ st::msgPadding.bottom()\n\t\t+ st::historyPollBottomButtonTop;\n}\n\nbool Poll::Footer::hasCloseDate() const {\n\treturn _owner->_poll->closeDate > 0\n\t\t&& !(_owner->_flags & PollData::Flag::Closed);\n}\n\nbool Poll::Footer::hasTimerLine(int innerWidth) const {\n\tif (_owner->inlineFooter() || _owner->showVotersCount()) {\n\t\treturn timerFooterMultiline(innerWidth);\n\t}\n\treturn hasCloseDate();\n}\n\nint Poll::Footer::countHeight(int innerWidth) const {\n\tconst auto inline_ = _owner->inlineFooter();\n\tconst auto top = topSkip();\n\tconst auto buttonSkip = inline_\n\t\t? 0\n\t\t: st::historyPollBottomButtonSkip;\n\tconst auto timerLine = hasTimerLine(innerWidth);\n\treturn top\n\t\t+ buttonSkip\n\t\t+ st::msgDateFont->height\n\t\t+ (timerLine ? st::msgDateFont->height : 0)\n\t\t+ dateInfoPadding(innerWidth)\n\t\t+ st::msgPadding.bottom();\n}\n\nvoid Poll::Footer::draw(\n\t\tPainter &p,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tconst PaintContext &context) const {\n\tconst auto stm = context.messageStyle();\n\tconst auto inline_ = _owner->inlineFooter();\n\n\tif (inline_) {\n\t\tconst auto top = st::msgPadding.bottom();\n\t\tp.setPen(stm->msgDateFg);\n\t\tconst auto timerText = closeTimerText();\n\t\tif (timerText.isEmpty()) {\n\t\t\tconst auto labelWidth = _totalVotesLabel.maxWidth();\n\t\t\tconst auto labelLeft = left + (innerWidth - labelWidth) / 2;\n\t\t\t_totalVotesLabel.drawLeftElided(\n\t\t\t\tp,\n\t\t\t\tlabelLeft,\n\t\t\t\ttop,\n\t\t\t\tlabelWidth,\n\t\t\t\touterWidth);\n\t\t} else if (timerFooterMultiline(innerWidth)) {\n\t\t\tconst auto labelWidth = _totalVotesLabel.maxWidth();\n\t\t\tconst auto labelLeft = left + (innerWidth - labelWidth) / 2;\n\t\t\t_totalVotesLabel.drawLeftElided(\n\t\t\t\tp,\n\t\t\t\tlabelLeft,\n\t\t\t\ttop,\n\t\t\t\tlabelWidth,\n\t\t\t\touterWidth);\n\t\t\tp.setFont(st::msgDateFont);\n\t\t\tconst auto timerw = st::msgDateFont->width(timerText);\n\t\t\tp.drawTextLeft(\n\t\t\t\tleft + (innerWidth - timerw) / 2,\n\t\t\t\ttop + st::msgDateFont->height,\n\t\t\t\touterWidth,\n\t\t\t\ttimerText,\n\t\t\t\ttimerw);\n\t\t} else {\n\t\t\tp.setFont(st::msgDateFont);\n\t\t\tconst auto sep = QString::fromUtf8(\" \\xC2\\xB7 \");\n\t\t\tconst auto full = _totalVotesLabel.toString()\n\t\t\t\t+ sep\n\t\t\t\t+ timerText;\n\t\t\tconst auto fullw = st::msgDateFont->width(full);\n\t\t\tp.drawTextLeft(\n\t\t\t\tleft + (innerWidth - fullw) / 2,\n\t\t\t\ttop,\n\t\t\t\touterWidth,\n\t\t\t\tfull,\n\t\t\t\tfullw);\n\t\t}\n\t\treturn;\n\t}\n\n\tconst auto stringtop = textTop();\n\n\tif (_linkRipple) {\n\t\tconst auto rippleTop = topSkip();\n\t\tp.setOpacity(st::historyPollRippleOpacity);\n\t\t_linkRipple->paint(\n\t\t\tp,\n\t\t\tleft - st::msgPadding.left() - _linkRippleShift,\n\t\t\trippleTop,\n\t\t\touterWidth,\n\t\t\t&stm->msgWaveformInactive->c);\n\t\tif (_linkRipple->empty()) {\n\t\t\t_linkRipple.reset();\n\t\t}\n\t\tp.setOpacity(1.);\n\t}\n\tif (_owner->_addOptionActive) {\n\t\tp.setFont(st::semiboldFont);\n\t\tp.setPen(stm->msgFileThumbLinkFg);\n\t\tconst auto text = tr::lng_polls_add_option_save(tr::now);\n\t\tconst auto textw = st::semiboldFont->width(text);\n\t\tp.drawTextLeft(\n\t\t\tleft + (innerWidth - textw) / 2,\n\t\t\tstringtop,\n\t\t\touterWidth,\n\t\t\ttext,\n\t\t\ttextw);\n\t\treturn;\n\t}\n\tif (_owner->isAuthorNotVoted()\n\t\t&& !_owner->_adminShowResults\n\t\t&& !_owner->canSendVotes()) {\n\t\tif (_owner->_totalVotes > 0) {\n\t\t\tp.setPen(stm->msgFileThumbLinkFg);\n\t\t\tconst auto labelWidth = _adminVotesLabel.maxWidth();\n\t\t\t_adminVotesLabel.drawLeft(\n\t\t\t\tp,\n\t\t\t\tleft + (innerWidth - labelWidth) / 2,\n\t\t\t\tstringtop,\n\t\t\t\tlabelWidth,\n\t\t\t\touterWidth);\n\t\t} else {\n\t\t\tp.setPen(stm->msgDateFg);\n\t\t\tconst auto textw = _totalVotesLabel.maxWidth();\n\t\t\t_totalVotesLabel.drawLeft(\n\t\t\t\tp,\n\t\t\t\tleft + (innerWidth - textw) / 2,\n\t\t\t\tstringtop,\n\t\t\t\ttextw,\n\t\t\t\touterWidth);\n\t\t}\n\t} else if (_owner->_adminShowResults && _owner->isAuthorNotVoted()) {\n\t\tp.setPen(stm->msgFileThumbLinkFg);\n\t\tconst auto backw = _adminBackVoteLabel.maxWidth();\n\t\t_adminBackVoteLabel.drawLeft(\n\t\t\tp,\n\t\t\tleft + (innerWidth - backw) / 2,\n\t\t\tstringtop,\n\t\t\tbackw,\n\t\t\touterWidth);\n\t} else if (_owner->showVotersCount()) {\n\t\t_linkRipple.reset();\n\t\tp.setPen(stm->msgDateFg);\n\t\tconst auto timerText = closeTimerText();\n\t\tif (timerText.isEmpty()) {\n\t\t\tconst auto labelWidth = _totalVotesLabel.maxWidth();\n\t\t\tconst auto labelLeft = left + (innerWidth - labelWidth) / 2;\n\t\t\t_totalVotesLabel.draw(\n\t\t\t\tp,\n\t\t\t\tlabelLeft,\n\t\t\t\tstringtop,\n\t\t\t\tlabelWidth,\n\t\t\t\tstyle::al_top);\n\t\t} else if (timerFooterMultiline(innerWidth)) {\n\t\t\tconst auto labelWidth = _totalVotesLabel.maxWidth();\n\t\t\tconst auto labelLeft = left + (innerWidth - labelWidth) / 2;\n\t\t\t_totalVotesLabel.draw(\n\t\t\t\tp,\n\t\t\t\tlabelLeft,\n\t\t\t\tstringtop,\n\t\t\t\tlabelWidth,\n\t\t\t\tstyle::al_top);\n\t\t\tp.setFont(st::msgDateFont);\n\t\t\tconst auto timerw = st::msgDateFont->width(timerText);\n\t\t\tp.drawTextLeft(\n\t\t\t\tleft + (innerWidth - timerw) / 2,\n\t\t\t\tstringtop + st::msgDateFont->height,\n\t\t\t\touterWidth,\n\t\t\t\ttimerText,\n\t\t\t\ttimerw);\n\t\t} else {\n\t\t\tp.setFont(st::msgDateFont);\n\t\t\tconst auto sep = QString::fromUtf8(\" \\xC2\\xB7 \");\n\t\t\tconst auto full = _totalVotesLabel.toString()\n\t\t\t\t+ sep\n\t\t\t\t+ timerText;\n\t\t\tconst auto fullw = st::msgDateFont->width(full);\n\t\t\tp.drawTextLeft(\n\t\t\t\tleft + (innerWidth - fullw) / 2,\n\t\t\t\tstringtop,\n\t\t\t\touterWidth,\n\t\t\t\tfull,\n\t\t\t\tfullw);\n\t\t}\n\t} else {\n\t\tconst auto votedPublic = _owner->_voted\n\t\t\t&& (_owner->_flags & PollData::Flag::PublicVotes);\n\t\tconst auto link = (_owner->showVotes() || votedPublic)\n\t\t\t? _showResultsLink\n\t\t\t: _owner->canSendVotes()\n\t\t\t? _sendVotesLink\n\t\t\t: nullptr;\n\t\tp.setFont(st::semiboldFont);\n\t\tp.setPen(link ? stm->msgFileThumbLinkFg : stm->msgDateFg);\n\t\tconst auto string = (_owner->showVotes() || votedPublic)\n\t\t\t? ((_owner->_flags & PollData::Flag::PublicVotes)\n\t\t\t\t? tr::lng_polls_view_votes(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\t_owner->_totalVotes)\n\t\t\t\t: tr::lng_polls_view_results(tr::now))\n\t\t\t: tr::lng_polls_submit_votes(tr::now);\n\t\tconst auto stringw = st::semiboldFont->width(string);\n\t\tp.drawTextLeft(\n\t\t\tleft + (innerWidth - stringw) / 2,\n\t\t\tstringtop,\n\t\t\touterWidth,\n\t\t\tstring,\n\t\t\tstringw);\n\t\tconst auto timerText = closeTimerText();\n\t\tif (!timerText.isEmpty()) {\n\t\t\tp.setFont(st::msgDateFont);\n\t\t\tp.setPen(stm->msgDateFg);\n\t\t\tconst auto timerw = st::msgDateFont->width(timerText);\n\t\t\tp.drawTextLeft(\n\t\t\t\tleft + (innerWidth - timerw) / 2,\n\t\t\t\tstringtop + st::semiboldFont->height,\n\t\t\t\touterWidth,\n\t\t\t\ttimerText,\n\t\t\t\ttimerw);\n\t\t}\n\t}\n}\n\nTextState Poll::Footer::textState(\n\t\tQPoint point,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tStateRequest request) const {\n\tTextState result;\n\tif (_owner->inlineFooter()) {\n\t\treturn result;\n\t}\n\tconst auto top = topSkip();\n\tconst auto h = countHeight(innerWidth);\n\tif (point.y() < top || point.y() >= h) {\n\t\treturn result;\n\t}\n\t_owner->_lastLinkPoint = point;\n\tif (_owner->_addOptionActive) {\n\t\tresult.link = _saveOptionLink;\n\t} else if (_owner->isAuthorNotVoted()\n\t\t&& !_owner->_adminShowResults\n\t\t&& !_owner->canSendVotes()) {\n\t\tif (_owner->_totalVotes > 0) {\n\t\t\tresult.link = _adminVotesLink;\n\t\t}\n\t} else if (_owner->_adminShowResults && _owner->isAuthorNotVoted()) {\n\t\tresult.link = _adminBackVoteLink;\n\t} else if (!_owner->showVotersCount()) {\n\t\tconst auto votedPublic = _owner->_voted\n\t\t\t&& (_owner->_flags & PollData::Flag::PublicVotes);\n\t\tresult.link = (_owner->showVotes() || votedPublic)\n\t\t\t? _showResultsLink\n\t\t\t: _owner->canSendVotes()\n\t\t\t? _sendVotesLink\n\t\t\t: nullptr;\n\t}\n\treturn result;\n}\n\nvoid Poll::Footer::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) {\n\tif (handler == _sendVotesLink\n\t\t|| handler == _showResultsLink\n\t\t|| handler == _adminVotesLink\n\t\t|| handler == _adminBackVoteLink\n\t\t|| handler == _saveOptionLink) {\n\t\ttoggleLinkRipple(pressed);\n\t}\n}\n\nvoid Poll::Footer::toggleLinkRipple(bool pressed) {\n\tif (pressed) {\n\t\tconst auto outerWidth = _owner->width();\n\t\tconst auto h = countHeight(\n\t\t\touterWidth - st::msgPadding.left() - st::msgPadding.right());\n\t\tconst auto rippleTop = topSkip();\n\t\tconst auto linkHeight = h - rippleTop;\n\t\tif (!_linkRipple) {\n\t\t\tauto mask = _owner->isRoundedInBubbleBottom()\n\t\t\t\t? static_cast<Message*>(_owner->_parent.get())\n\t\t\t\t\t->bottomRippleMask(linkHeight)\n\t\t\t\t: BottomRippleMask{\n\t\t\t\t\tUi::RippleAnimation::RectMask(\n\t\t\t\t\t\t{ outerWidth, linkHeight }),\n\t\t\t\t};\n\t\t\t_linkRipple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\tstd::move(mask.image),\n\t\t\t\t[owner = _owner] { owner->repaint(); });\n\t\t\t_linkRippleShift = mask.shift;\n\t\t}\n\t\t_linkRipple->add(\n\t\t\t_owner->_lastLinkPoint\n\t\t\t\t+ QPoint(_linkRippleShift, -rippleTop));\n\t} else if (_linkRipple) {\n\t\t_linkRipple->lastStop();\n\t}\n}\n\nvoid Poll::Footer::updateTotalVotes() {\n\tif (_owner->_totalVotes == _owner->_poll->totalVoters\n\t\t&& !_totalVotesLabel.isEmpty()) {\n\t\treturn;\n\t}\n\t_owner->_totalVotes = _owner->_poll->totalVoters;\n\tconst auto quiz = _owner->_poll->quiz();\n\tconst auto string = !_owner->_totalVotes\n\t\t? (quiz\n\t\t\t? tr::lng_polls_answers_none\n\t\t\t: tr::lng_polls_votes_none)(tr::now)\n\t\t: (quiz\n\t\t\t? tr::lng_polls_answers_count\n\t\t\t: tr::lng_polls_votes_count)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_short,\n\t\t\t\t_owner->_totalVotes);\n\t_totalVotesLabel.setText(st::msgDateTextStyle, string);\n\t_adminVotesLabel.setMarkedText(\n\t\tst::semiboldTextStyle,\n\t\ttr::lng_polls_admin_votes(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\t_owner->_totalVotes,\n\t\t\tlt_arrow,\n\t\t\tUi::Text::IconEmoji(&st::textMoreIconEmoji),\n\t\t\ttr::marked));\n}\n\nstruct Poll::AddOption : public Poll::Part {\n\texplicit AddOption(not_null<Poll*> owner);\n\n\tint countHeight(int innerWidth) const override;\n\tvoid draw(\n\t\tPainter &p,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tconst PaintContext &context) const override;\n\tTextState textState(\n\t\tQPoint point,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tStateRequest request) const override;\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) override;\n\n\tClickHandlerPtr _addOptionLink;\n\tmutable std::unique_ptr<Ui::RippleAnimation> _addOptionRipple;\n\nprivate:\n\t[[nodiscard]] int rowHeight() const;\n\tvoid toggleRipple(bool pressed);\n};\n\nPoll::AddOption::AddOption(not_null<Poll*> owner)\n: Part(owner)\n, _addOptionLink(\n\tstd::make_shared<LambdaClickHandler>(crl::guard(\n\t\towner.get(),\n\t\t[=](ClickContext context) {\n\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\tif (const auto delegate = my.elementDelegate\n\t\t\t\t? my.elementDelegate()\n\t\t\t\t: nullptr) {\n\t\t\t\tconst auto innerWidth = owner->width()\n\t\t\t\t\t- st::msgPadding.left()\n\t\t\t\t\t- st::msgPadding.right();\n\t\t\t\tdelegate->elementShowAddPollOption(\n\t\t\t\t\towner->_parent,\n\t\t\t\t\towner->_poll,\n\t\t\t\t\towner->_parent->data()->fullId(),\n\t\t\t\t\towner->addOptionRect(innerWidth));\n\t\t\t}\n\t\t}))) {\n}\n\nint Poll::AddOption::rowHeight() const {\n\treturn st::historyPollAnswerPaddingNoMedia.top()\n\t\t+ st::msgDateFont->height\n\t\t+ st::historyPollAnswerPaddingNoMedia.bottom();\n}\n\nint Poll::AddOption::countHeight(int innerWidth) const {\n\treturn _owner->canAddOption() ? rowHeight() : 0;\n}\n\nvoid Poll::AddOption::draw(\n\t\tPainter &p,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tconst PaintContext &context) const {\n\tif (!_owner->canAddOption() || _owner->_addOptionActive) {\n\t\treturn;\n\t}\n\tconst auto stm = context.messageStyle();\n\tconst auto &padding = st::historyPollAnswerPaddingNoMedia;\n\tconst auto textTop = padding.top();\n\n\tif (_addOptionRipple) {\n\t\tp.setOpacity(st::historyPollRippleOpacity);\n\t\t_addOptionRipple->paint(\n\t\t\tp,\n\t\t\tleft - st::msgPadding.left(),\n\t\t\t0,\n\t\t\touterWidth,\n\t\t\t&stm->msgWaveformInactive->c);\n\t\tif (_addOptionRipple->empty()) {\n\t\t\t_addOptionRipple.reset();\n\t\t}\n\t\tp.setOpacity(1.);\n\t}\n\n\tconst auto color = stm->msgDateFg->c;\n\tconst auto &icon = st::pollBoxOutlinePollAddIcon;\n\tconst auto &radio = st::historyPollRadio;\n\tconst auto iconLeft = left\n\t\t+ (radio.diameter - icon.width()) / 2;\n\tconst auto iconTop = textTop\n\t\t+ (st::msgDateFont->height - icon.height()) / 2;\n\ticon.paint(p, iconLeft, iconTop, outerWidth, color);\n\n\tp.setFont(st::normalFont);\n\tp.setPen(stm->msgDateFg);\n\tconst auto text = tr::lng_polls_add_option(tr::now);\n\tconst auto textw = st::normalFont->width(text);\n\tp.drawTextLeft(\n\t\tleft + st::historyPollAnswerPadding.left(),\n\t\ttextTop,\n\t\touterWidth,\n\t\ttext,\n\t\ttextw);\n}\n\nTextState Poll::AddOption::textState(\n\t\tQPoint point,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tStateRequest request) const {\n\tTextState result;\n\tif (!_owner->canAddOption()) {\n\t\treturn result;\n\t}\n\tconst auto h = rowHeight();\n\tif (point.y() >= 0 && point.y() < h) {\n\t\t_owner->_lastLinkPoint = point;\n\t\tresult.link = _addOptionLink;\n\t}\n\treturn result;\n}\n\nvoid Poll::AddOption::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) {\n\tif (handler == _addOptionLink) {\n\t\ttoggleRipple(pressed);\n\t}\n}\n\nvoid Poll::AddOption::toggleRipple(bool pressed) {\n\tif (pressed) {\n\t\tconst auto outerWidth = _owner->width();\n\t\tconst auto h = rowHeight();\n\t\tif (!_addOptionRipple) {\n\t\t\tauto mask = Ui::RippleAnimation::RectMask(\n\t\t\t\tQSize(outerWidth, h));\n\t\t\t_addOptionRipple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\tstd::move(mask),\n\t\t\t\t[owner = _owner] { owner->repaint(); });\n\t\t}\n\t\t_addOptionRipple->add(_owner->_lastLinkPoint);\n\t} else if (_addOptionRipple) {\n\t\t_addOptionRipple->lastStop();\n\t}\n}\n\nstruct Poll::Header : public Poll::Part {\n\texplicit Header(not_null<Poll*> owner)\n\t: Part(owner)\n\t, _description(st::msgMinWidth / 2)\n\t, _question(st::msgMinWidth / 2)\n\t, _attachedMedia(std::make_unique<AttachedMedia>()) {\n\t}\n\n\tint countHeight(int innerWidth) const override;\n\tvoid draw(\n\t\tPainter &p,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tconst PaintContext &context) const override;\n\tTextState textState(\n\t\tQPoint point,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tStateRequest request) const override;\n\tvoid clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool active) override;\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) override;\n\tbool hasHeavyPart() const override;\n\tvoid unloadHeavyPart() override;\n\tuint16 selectionLength() const override;\n\n\tvoid updateDescription();\n\tvoid updateAttachedMedia();\n\t[[nodiscard]] int countTopContentSkip(int pollWidth = 0) const;\n\t[[nodiscard]] int countTopMediaHeight(int pollWidth = 0) const;\n\t[[nodiscard]] int countAttachHeight(int pollWidth = 0) const;\n\t[[nodiscard]] QRect countTopMediaRect(int top) const;\n\t[[nodiscard]] Ui::BubbleRounding topMediaRounding() const;\n\tvoid validateTopMediaCache(QSize size) const;\n\t[[nodiscard]] int countDescriptionHeight(int innerWidth) const;\n\t[[nodiscard]] int countQuestionTop(\n\t\tint innerWidth,\n\t\tint pollWidth = 0) const;\n\t[[nodiscard]] uint16 solutionSelectionLength() const;\n\t[[nodiscard]] TextSelection toSolutionSelection(\n\t\tTextSelection selection) const;\n\t[[nodiscard]] TextSelection fromSolutionSelection(\n\t\tTextSelection selection) const;\n\t[[nodiscard]] TextSelection toQuestionSelection(\n\t\tTextSelection selection) const;\n\t[[nodiscard]] TextSelection fromQuestionSelection(\n\t\tTextSelection selection) const;\n\tvoid updateRecentVoters();\n\tvoid paintRecentVoters(\n\t\tPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tconst PaintContext &context) const;\n\tvoid paintShowSolution(\n\t\tPainter &p,\n\t\tint right,\n\t\tint top,\n\t\tconst PaintContext &context) const;\n\tvoid showSolution() const;\n\tvoid solutionToggled(\n\t\tbool solutionShown,\n\t\tanim::type animated = anim::type::normal) const;\n\t[[nodiscard]] bool canShowSolution() const;\n\t[[nodiscard]] bool inShowSolution(\n\t\tQPoint point,\n\t\tint right,\n\t\tint top) const;\n\tvoid updateSolutionText();\n\tvoid updateSolutionMedia();\n\t[[nodiscard]] int countSolutionBlockHeight(int innerWidth) const;\n\t[[nodiscard]] int countSolutionMediaHeight(int mediaWidth) const;\n\tvoid paintSolutionBlock(\n\t\tPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint paintw,\n\t\tconst PaintContext &context) const;\n\n\tUi::Text::String _description;\n\tUi::Text::String _question;\n\tUi::Text::String _subtitle;\n\tstd::unique_ptr<AttachedMedia> _attachedMedia;\n\tstd::unique_ptr<Media> _attachedMediaAttach;\n\tmutable QImage _attachedMediaCache;\n\tmutable Ui::BubbleRounding _attachedMediaCacheRounding;\n\tstd::vector<RecentVoter> _recentVoters;\n\tQImage _recentVotersImage;\n\tmutable ClickHandlerPtr _showSolutionLink;\n\tUi::Text::String _solutionText;\n\tmutable ClickHandlerPtr _closeSolutionLink;\n\tstd::unique_ptr<SolutionMedia> _solutionMedia;\n\tstd::unique_ptr<Media> _solutionAttach;\n\tmutable Ui::Animations::Simple _solutionButtonAnimation;\n\tmutable bool _solutionShown = false;\n\tmutable bool _solutionButtonVisible = false;\n\tmutable QImage _userpicCircleCache;\n};\n\nint Poll::Header::countHeight(int innerWidth) const {\n\tconst auto pollWidth = innerWidth\n\t\t+ st::msgPadding.left()\n\t\t+ st::msgPadding.right();\n\treturn countQuestionTop(innerWidth, pollWidth)\n\t\t+ _question.countHeight(innerWidth)\n\t\t+ st::historyPollSubtitleSkip\n\t\t+ st::msgDateFont->height\n\t\t+ st::historyPollAnswersSkip;\n}\n\nvoid Poll::Header::draw(\n\t\tPainter &p,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tconst PaintContext &context) const {\n\tconst auto stm = context.messageStyle();\n\tauto tshift = countTopContentSkip();\n\n\tif (const auto mediaHeight = countTopMediaHeight()) {\n\t\tif (_attachedMediaAttach) {\n\t\t\tconst auto sideSkip = st::historyPollMediaSideSkip;\n\t\t\t_attachedMediaAttach->setBubbleRounding(\n\t\t\t\ttopMediaRounding());\n\t\t\tp.translate(sideSkip, tshift);\n\t\t\t_attachedMediaAttach->draw(\n\t\t\t\tp,\n\t\t\t\tcontext.translated(-sideSkip, -tshift)\n\t\t\t\t\t.withSelection(TextSelection()));\n\t\t\tp.translate(-sideSkip, -tshift);\n\t\t} else {\n\t\t\tconst auto target = countTopMediaRect(tshift);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(stm->msgFileBg);\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tif (_attachedMedia->kind\n\t\t\t\t\t== PollThumbnailKind::Emoji) {\n\t\t\t\tp.drawRoundedRect(\n\t\t\t\t\ttarget,\n\t\t\t\t\tst::roundRadiusLarge,\n\t\t\t\t\tst::roundRadiusLarge);\n\t\t\t\tconst auto image\n\t\t\t\t\t= _attachedMedia->thumbnail->image(\n\t\t\t\t\t\tstd::max(target.width(), target.height()));\n\t\t\t\tif (!image.isNull()) {\n\t\t\t\t\tconst auto source = QRectF(\n\t\t\t\t\t\tQPointF(),\n\t\t\t\t\t\tQSizeF(image.size()));\n\t\t\t\t\tconst auto kx = target.width() / source.width();\n\t\t\t\t\tconst auto ky = target.height() / source.height();\n\t\t\t\t\tconst auto scale = std::min(kx, ky);\n\t\t\t\t\tconst auto imageSize = QSizeF(\n\t\t\t\t\t\tsource.width() * scale,\n\t\t\t\t\t\tsource.height() * scale);\n\t\t\t\t\tconst auto geometry = QRectF(\n\t\t\t\t\t\ttarget.x()\n\t\t\t\t\t\t\t+ (target.width()\n\t\t\t\t\t\t\t\t- imageSize.width()) / 2.,\n\t\t\t\t\t\ttarget.y()\n\t\t\t\t\t\t\t+ (target.height()\n\t\t\t\t\t\t\t\t- imageSize.height()) / 2.,\n\t\t\t\t\t\timageSize.width(),\n\t\t\t\t\t\timageSize.height());\n\t\t\t\t\tp.save();\n\t\t\t\t\tauto path = QPainterPath();\n\t\t\t\t\tpath.addRoundedRect(\n\t\t\t\t\t\ttarget,\n\t\t\t\t\t\tst::roundRadiusLarge,\n\t\t\t\t\t\tst::roundRadiusLarge);\n\t\t\t\t\tp.setClipPath(path);\n\t\t\t\t\tp.drawImage(geometry, image, source);\n\t\t\t\t\tp.restore();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tvalidateTopMediaCache(target.size());\n\t\t\t\tif (!_attachedMediaCache.isNull()) {\n\t\t\t\t\tp.drawImage(target.topLeft(),\n\t\t\t\t\t\t_attachedMediaCache);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ttshift += mediaHeight + st::historyPollMediaSkip;\n\t}\n\n\tif (const auto descriptionHeight\n\t\t\t= countDescriptionHeight(innerWidth)) {\n\t\tp.setPen(stm->historyTextFg);\n\t\t_owner->_parent->prepareCustomEmojiPaint(\n\t\t\tp, context, _description);\n\t\t_description.draw(p, {\n\t\t\t.position = { left, tshift },\n\t\t\t.outerWidth = outerWidth,\n\t\t\t.availableWidth = innerWidth,\n\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t.now = context.now,\n\t\t\t.pausedEmoji = context.paused,\n\t\t\t.pausedSpoiler = context.paused,\n\t\t\t.selection = context.selection,\n\t\t\t.useFullWidth = true,\n\t\t});\n\t\ttshift += descriptionHeight + st::historyPollDescriptionSkip;\n\t}\n\n\tif (const auto solutionHeight\n\t\t\t= countSolutionBlockHeight(innerWidth)) {\n\t\tpaintSolutionBlock(\n\t\t\tp, left, tshift, innerWidth, context);\n\t\ttshift += solutionHeight + st::historyPollExplanationSkip;\n\t}\n\n\tp.setPen(stm->historyTextFg);\n\t_owner->_parent->prepareCustomEmojiPaint(p, context, _question);\n\t_question.draw(p, {\n\t\t.position = { left, tshift },\n\t\t.outerWidth = outerWidth,\n\t\t.availableWidth = innerWidth,\n\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t.now = context.now,\n\t\t.pausedEmoji = context.paused,\n\t\t.pausedSpoiler = context.paused,\n\t\t.selection = toQuestionSelection(context.selection),\n\t});\n\ttshift += _question.countHeight(innerWidth)\n\t\t+ st::historyPollSubtitleSkip;\n\n\tp.setPen(stm->msgDateFg);\n\t_subtitle.drawLeftElided(\n\t\tp, left, tshift, innerWidth, outerWidth);\n\tpaintRecentVoters(\n\t\tp, left + _subtitle.maxWidth(), tshift, context);\n\tpaintShowSolution(\n\t\tp, left + innerWidth, tshift, context);\n}\n\nTextState Poll::Header::textState(\n\t\tQPoint point,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tStateRequest request) const {\n\tTextState result(_owner->_parent);\n\tauto tshift = countTopContentSkip();\n\n\tif (const auto mediaHeight = countTopMediaHeight()) {\n\t\tif (_attachedMediaAttach) {\n\t\t\tconst auto sideSkip = st::historyPollMediaSideSkip;\n\t\t\tif (QRect(\n\t\t\t\t\tsideSkip,\n\t\t\t\t\ttshift,\n\t\t\t\t\t_attachedMediaAttach->width(),\n\t\t\t\t\tmediaHeight).contains(point)) {\n\t\t\t\tresult = _attachedMediaAttach->textState(\n\t\t\t\t\tpoint - QPoint(sideSkip, tshift),\n\t\t\t\t\trequest);\n\t\t\t\tresult.symbol = 0;\n\t\t\t\treturn result;\n\t\t\t}\n\t\t} else if (_attachedMedia\n\t\t\t&& _attachedMedia->handler\n\t\t\t&& QRect(0, tshift, outerWidth, mediaHeight)\n\t\t\t\t.contains(point)) {\n\t\t\tresult.link = _attachedMedia->handler;\n\t\t\treturn result;\n\t\t}\n\t\ttshift += mediaHeight + st::historyPollMediaSkip;\n\t}\n\n\tauto symbolAdd = uint16(0);\n\tif (const auto descriptionHeight\n\t\t\t= countDescriptionHeight(innerWidth)) {\n\t\tif (QRect(left, tshift, innerWidth, descriptionHeight)\n\t\t\t\t.contains(point)) {\n\t\t\tresult = TextState(\n\t\t\t\t_owner->_parent,\n\t\t\t\t_description.getStateLeft(\n\t\t\t\t\tpoint - QPoint(left, tshift),\n\t\t\t\t\tinnerWidth,\n\t\t\t\t\touterWidth,\n\t\t\t\t\trequest.forText()));\n\t\t\treturn result;\n\t\t}\n\t\tif (point.y() >= tshift + descriptionHeight) {\n\t\t\tsymbolAdd += _description.length();\n\t\t}\n\t\ttshift += descriptionHeight + st::historyPollDescriptionSkip;\n\t}\n\n\tif (const auto solutionHeight\n\t\t\t= countSolutionBlockHeight(innerWidth)) {\n\t\tif (QRect(left, tshift, innerWidth, solutionHeight)\n\t\t\t\t.contains(point)) {\n\t\t\tconst auto &qst = st::historyPagePreview;\n\t\t\tconst auto innerLeft = left + qst.padding.left();\n\t\t\tconst auto innerRight = left\n\t\t\t\t+ innerWidth\n\t\t\t\t- qst.padding.right();\n\t\t\tconst auto closeArea\n\t\t\t\t= st::historyPollExplanationCloseSize;\n\t\t\tconst auto closeLeft = innerRight - closeArea;\n\t\t\tconst auto closeTop = tshift + qst.padding.top();\n\t\t\tif (QRect(\n\t\t\t\tcloseLeft,\n\t\t\t\tcloseTop,\n\t\t\t\tcloseArea,\n\t\t\t\tst::semiboldFont->height).contains(point)) {\n\t\t\t\tresult.link = _closeSolutionLink;\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tconst auto textTop = tshift\n\t\t\t\t+ qst.padding.top()\n\t\t\t\t+ st::semiboldFont->height\n\t\t\t\t+ st::historyPollExplanationTitleSkip;\n\t\t\tconst auto textWidth = innerRight - innerLeft;\n\t\t\tconst auto textHeight\n\t\t\t\t= _solutionText.countHeight(textWidth);\n\t\t\tif (QRect(\n\t\t\t\tinnerLeft,\n\t\t\t\ttextTop,\n\t\t\t\ttextWidth,\n\t\t\t\ttextHeight).contains(point)) {\n\t\t\t\tresult = TextState(\n\t\t\t\t\t_owner->_parent,\n\t\t\t\t\t_solutionText.getStateLeft(\n\t\t\t\t\t\tpoint - QPoint(innerLeft, textTop),\n\t\t\t\t\t\ttextWidth,\n\t\t\t\t\t\touterWidth,\n\t\t\t\t\t\trequest.forText()));\n\t\t\t\tresult.symbol += symbolAdd;\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tif (_solutionAttach) {\n\t\t\t\tif (const auto mh\n\t\t\t\t\t\t= countSolutionMediaHeight(\n\t\t\t\t\t\t\ttextWidth)) {\n\t\t\t\t\tconst auto mediaTop = textTop\n\t\t\t\t\t\t+ textHeight\n\t\t\t\t\t\t+ st::historyPollExplanationMediaSkip;\n\t\t\t\t\tconst auto isDocument = _solutionMedia\n\t\t\t\t\t\t&& (_solutionMedia->kind\n\t\t\t\t\t\t\t\t== PollThumbnailKind::Document\n\t\t\t\t\t\t\t|| _solutionMedia->kind\n\t\t\t\t\t\t\t\t== PollThumbnailKind::Audio);\n\t\t\t\t\tconst auto isThumbed = isDocument\n\t\t\t\t\t\t&& _owner->_poll->solutionMedia.document\n\t\t\t\t\t\t&& _owner->_poll->solutionMedia.document\n\t\t\t\t\t\t\t->hasThumbnail()\n\t\t\t\t\t\t&& !_owner->_poll->solutionMedia.document\n\t\t\t\t\t\t\t->isSong();\n\t\t\t\t\tconst auto &fileSt = isThumbed\n\t\t\t\t\t\t? st::msgFileThumbLayout\n\t\t\t\t\t\t: st::msgFileLayout;\n\t\t\t\t\tconst auto shift = isDocument\n\t\t\t\t\t\t? fileSt.padding.left()\n\t\t\t\t\t\t: 0;\n\t\t\t\t\tconst auto mediaLeft = innerLeft - shift;\n\t\t\t\t\tif (QRect(\n\t\t\t\t\t\t\tmediaLeft,\n\t\t\t\t\t\t\tmediaTop,\n\t\t\t\t\t\t\t_solutionAttach->width(),\n\t\t\t\t\t\t\tmh).contains(point)) {\n\t\t\t\t\t\tresult = _solutionAttach->textState(\n\t\t\t\t\t\t\tpoint - QPoint(mediaLeft, mediaTop),\n\t\t\t\t\t\t\trequest);\n\t\t\t\t\t\tresult.symbol = 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\t\tif (point.y() >= tshift + solutionHeight) {\n\t\t\tsymbolAdd += _solutionText.length();\n\t\t}\n\t\ttshift += solutionHeight + st::historyPollExplanationSkip;\n\t}\n\n\tconst auto questionH = _question.countHeight(innerWidth);\n\tif (QRect(left, tshift, innerWidth, questionH).contains(point)) {\n\t\tresult = TextState(\n\t\t\t_owner->_parent,\n\t\t\t_question.getState(\n\t\t\t\tpoint - QPoint(left, tshift),\n\t\t\t\tinnerWidth,\n\t\t\t\trequest.forText()));\n\t\tresult.symbol += symbolAdd;\n\t\treturn result;\n\t}\n\tif (point.y() >= tshift + questionH) {\n\t\tsymbolAdd += _question.length();\n\t}\n\ttshift += questionH + st::historyPollSubtitleSkip;\n\tif (inShowSolution(\n\t\t\tpoint, left + innerWidth, tshift)) {\n\t\tresult.link = _showSolutionLink;\n\t\treturn result;\n\t}\n\n\treturn result;\n}\n\nvoid Poll::Header::clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool active) {\n\tif (_attachedMediaAttach) {\n\t\t_attachedMediaAttach->clickHandlerActiveChanged(\n\t\t\thandler, active);\n\t}\n}\n\nvoid Poll::Header::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) {\n\tif (_attachedMediaAttach) {\n\t\t_attachedMediaAttach->clickHandlerPressedChanged(\n\t\t\thandler, pressed);\n\t}\n\tif (_solutionAttach) {\n\t\t_solutionAttach->clickHandlerPressedChanged(\n\t\t\thandler, pressed);\n\t}\n}\n\nbool Poll::Header::hasHeavyPart() const {\n\tfor (const auto &recent : _recentVoters) {\n\t\tif (!recent.userpic.null()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Poll::Header::unloadHeavyPart() {\n\tfor (auto &recent : _recentVoters) {\n\t\trecent.userpic = {};\n\t}\n}\n\nuint16 Poll::Header::selectionLength() const {\n\treturn _owner->fullSelectionLength();\n}\n\nstruct Poll::Options : public Poll::Part {\n\tusing Part::Part;\n\n\tint countHeight(int innerWidth) const override;\n\tvoid draw(\n\t\tPainter &p,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tconst PaintContext &context) const override;\n\tTextState textState(\n\t\tQPoint point,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tStateRequest request) const override;\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) override;\n\tbool hasHeavyPart() const override;\n\n\tvoid checkSendingAnimation() const;\n\tvoid unloadHeavyPart() override;\n\n\t[[nodiscard]] int countAnswerContentWidth(\n\t\tconst Answer &answer,\n\t\tint innerWidth) const;\n\t[[nodiscard]] int countVotesExtraHeight(\n\t\tconst Answer &answer,\n\t\tint textWidth) const;\n\t[[nodiscard]] int countAnswerHeight(\n\t\tconst Answer &answer,\n\t\tint innerWidth) const;\n\tvoid resetAnswersAnimation() const;\n\tvoid radialAnimationCallback() const;\n\tint paintAnswer(\n\t\tPainter &p,\n\t\tconst Answer &answer,\n\t\tconst AnswerAnimation *animation,\n\t\tint left,\n\t\tint top,\n\t\tint width,\n\t\tint outerWidth,\n\t\tconst PaintContext &context) const;\n\tvoid paintRadio(\n\t\tPainter &p,\n\t\tconst Answer &answer,\n\t\tint left,\n\t\tint top,\n\t\tconst PaintContext &context) const;\n\tvoid paintPercent(\n\t\tPainter &p,\n\t\tconst QString &percent,\n\t\tint percentWidth,\n\t\tint left,\n\t\tint top,\n\t\tint topPadding,\n\t\tint outerWidth,\n\t\tconst PaintContext &context) const;\n\tvoid paintFilling(\n\t\tPainter &p,\n\t\tbool chosen,\n\t\tbool correct,\n\t\tfloat64 filling,\n\t\tint left,\n\t\tint top,\n\t\tint topPadding,\n\t\tint width,\n\t\tint contentWidth,\n\t\tint contentHeight,\n\t\tconst PaintContext &context) const;\n\t[[nodiscard]] bool checkAnimationStart() const;\n\t[[nodiscard]] bool answerVotesChanged() const;\n\tvoid saveStateInAnimation() const;\n\tvoid startAnswersAnimation() const;\n\tvoid toggleRipple(Answer &answer, bool pressed);\n\tvoid toggleMultiOption(const QByteArray &option);\n\tvoid sendMultiOptions();\n\tvoid showResults();\n\tvoid showAnswerVotesTooltip(const QByteArray &option);\n\tvoid checkQuizAnswered();\n\t[[nodiscard]] ClickHandlerPtr createAnswerClickHandler(\n\t\tconst Answer &answer);\n\tvoid updateAnswers();\n\tvoid updateAnswerVotes();\n\tvoid updateAnswerVotesFromOriginal(\n\t\tAnswer &answer,\n\t\tconst PollAnswer &original,\n\t\tint percent,\n\t\tint maxVotes,\n\t\tbool showPercent);\n\n\tstd::vector<Answer> _answers;\n\tmutable std::unique_ptr<AnswersAnimation> _answersAnimation;\n\tmutable std::unique_ptr<SendingAnimation> _sendingAnimation;\n\tmutable QImage _fillingIconCache;\n\tbool _anyAnswerHasMedia = false;\n\tbool _hasSelected = false;\n\tbool _votedFromHere = false;\n};\n\nint Poll::Options::countHeight(int innerWidth) const {\n\tauto result = ranges::accumulate(ranges::views::all(\n\t\t_answers\n\t) | ranges::views::transform([&](const Answer &answer) {\n\t\treturn countAnswerHeight(answer, innerWidth);\n\t}), 0);\n\tif (_owner->canAddOption()) {\n\t\tresult += (st::historyPollChoiceRight.height()\n\t\t\t- st::historyPollFillingHeight) / 2;\n\t}\n\treturn result;\n}\n\nvoid Poll::Options::draw(\n\t\tPainter &p,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tconst PaintContext &context) const {\n\tcheckSendingAnimation();\n\n\tconst auto progress = _answersAnimation\n\t\t? _answersAnimation->progress.value(1.)\n\t\t: 1.;\n\tif (progress == 1.) {\n\t\tresetAnswersAnimation();\n\t}\n\n\tauto tshift = 0;\n\tauto &&answers = ranges::views::zip(\n\t\t_answers,\n\t\tranges::views::ints(0, int(_answers.size())));\n\tfor (const auto &[answer, index] : answers) {\n\t\tconst auto animation = _answersAnimation\n\t\t\t? &_answersAnimation->data[index]\n\t\t\t: nullptr;\n\t\tif (animation) {\n\t\t\tanimation->percent.update(progress, anim::linear);\n\t\t\tanimation->filling.update(\n\t\t\t\tprogress,\n\t\t\t\t_owner->showVotes()\n\t\t\t\t\t? anim::easeOutCirc\n\t\t\t\t\t: anim::linear);\n\t\t\tanimation->opacity.update(progress, anim::linear);\n\t\t}\n\t\tconst auto height = paintAnswer(\n\t\t\tp,\n\t\t\tanswer,\n\t\t\tanimation,\n\t\t\tleft,\n\t\t\ttshift,\n\t\t\tinnerWidth,\n\t\t\touterWidth,\n\t\t\tcontext);\n\t\ttshift += height;\n\t}\n}\n\nTextState Poll::Options::textState(\n\t\tQPoint point,\n\t\tint left,\n\t\tint innerWidth,\n\t\tint outerWidth,\n\t\tStateRequest request) const {\n\tTextState result;\n\tconst auto can = _owner->canVote();\n\tconst auto show = _owner->showVotes();\n\n\tauto tshift = 0;\n\tfor (const auto &answer : _answers) {\n\t\tconst auto height = countAnswerHeight(answer, innerWidth);\n\t\tif (point.y() >= tshift && point.y() < tshift + height) {\n\t\t\tconst auto media = answer.thumbnail\n\t\t\t\t? PollAnswerMediaSize()\n\t\t\t\t: 0;\n\t\t\tif (media\n\t\t\t\t&& answer.mediaHandler\n\t\t\t\t&& QRect(\n\t\t\t\t\tleft + innerWidth\n\t\t\t\t\t\t- st::historyPollAnswerPadding.right()\n\t\t\t\t\t\t- media,\n\t\t\t\t\ttshift + (answer.thumbnail\n\t\t\t\t\t\t? st::historyPollAnswerPadding\n\t\t\t\t\t\t: st::historyPollAnswerPaddingNoMedia).top(),\n\t\t\t\t\tmedia,\n\t\t\t\t\tmedia).contains(point)) {\n\t\t\t\tresult.link = answer.mediaHandler;\n\t\t\t} else {\n\t\t\t\tconst auto &answerPadding = answer.thumbnail\n\t\t\t\t\t? st::historyPollAnswerPadding\n\t\t\t\t\t: st::historyPollAnswerPaddingNoMedia;\n\t\t\t\tconst auto aleft = left\n\t\t\t\t\t+ st::historyPollAnswerPadding.left();\n\t\t\t\tconst auto atop = tshift + answerPadding.top();\n\t\t\t\tconst auto textWidth = countAnswerContentWidth(\n\t\t\t\t\tanswer,\n\t\t\t\t\tinnerWidth);\n\t\t\t\tconst auto textState = answer.text.getStateLeft(\n\t\t\t\t\tpoint - QPoint(aleft, atop),\n\t\t\t\t\ttextWidth,\n\t\t\t\t\touterWidth,\n\t\t\t\t\trequest.forText());\n\t\t\t\tif (textState.link) {\n\t\t\t\t\tresult.link = textState.link;\n\t\t\t\t} else {\n\t\t\t\t\tif (can) {\n\t\t\t\t\t\t_owner->_lastLinkPoint = point;\n\t\t\t\t\t}\n\t\t\t\t\tresult.link = answer.handler;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!can && show) {\n\t\t\t\tresult.customTooltip = true;\n\t\t\t\tusing Flag = Ui::Text::StateRequest::Flag;\n\t\t\t\tif (request.flags & Flag::LookupCustomTooltip) {\n\t\t\t\t\tconst auto quiz = _owner->_poll->quiz();\n\t\t\t\t\tresult.customTooltipText = answer.votes\n\t\t\t\t\t\t? (quiz\n\t\t\t\t\t\t\t? tr::lng_polls_answers_count\n\t\t\t\t\t\t\t: tr::lng_polls_votes_count)(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\t\t\tanswer.votes)\n\t\t\t\t\t\t: (quiz\n\t\t\t\t\t\t\t? tr::lng_polls_answers_none\n\t\t\t\t\t\t\t: tr::lng_polls_votes_none)(tr::now);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\t\ttshift += height;\n\t}\n\treturn result;\n}\n\nvoid Poll::Options::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) {\n\tconst auto i = ranges::find(\n\t\t_answers,\n\t\thandler,\n\t\t&Answer::handler);\n\tif (i != end(_answers)) {\n\t\tif (_owner->canVote()) {\n\t\t\ttoggleRipple(*i, pressed);\n\t\t}\n\t}\n}\n\nbool Poll::Options::hasHeavyPart() const {\n\tfor (const auto &answer : _answers) {\n\t\tif (!answer.recentVotersImage.isNull()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Poll::Options::unloadHeavyPart() {\n\tfor (auto &answer : _answers) {\n\t\tfor (auto &recent : answer.recentVoters) {\n\t\t\trecent.view = {};\n\t\t}\n\t\tanswer.recentVotersImage = QImage();\n\t}\n}\n\ntemplate <typename Callback>\nPoll::SendingAnimation::SendingAnimation(\n\tconst QByteArray &option,\n\tCallback &&callback)\n: option(option)\n, animation(\n\tstd::forward<Callback>(callback),\n\tst::historyPollRadialAnimation) {\n}\n\nPoll::Answer::Answer() : text(st::msgMinWidth / 2) {\n}\n\nvoid Poll::Answer::fillData(\n\t\tnot_null<PollData*> poll,\n\t\tconst PollAnswer &original,\n\t\tUi::Text::MarkedContext context) {\n\tchosen = original.chosen;\n\tcorrect = poll->quiz() ? original.correct : chosen;\n\tif (!text.isEmpty() && text.toTextWithEntities() == original.text) {\n\t\treturn;\n\t}\n\ttext.setMarkedText(\n\t\tst::historyPollAnswerStyle,\n\t\toriginal.text,\n\t\tUi::WebpageTextTitleOptions(),\n\t\tcontext);\n}\n\nvoid Poll::Answer::fillMedia(\n\t\tnot_null<PollData*> poll,\n\t\tconst PollAnswer &original,\n\t\tWindow::SessionController::MessageContext messageContext,\n\t\tFn<void()> repaint,\n\t\tFn<bool()> paused) {\n\tconst auto updated = MakePollThumbnail(\n\t\tpoll,\n\t\toriginal,\n\t\tmessageContext,\n\t\tpaused);\n\tconst auto same = (updated.kind == thumbnailKind)\n\t\t&& (updated.id == thumbnailId)\n\t\t&& (updated.rounded == thumbnailRounded);\n\tif (same) {\n\t\treturn;\n\t}\n\tif (thumbnail) {\n\t\tthumbnail->subscribeToUpdates(nullptr);\n\t}\n\tthumbnail = updated.thumbnail;\n\tmediaHandler = updated.handler;\n\tthumbnailRounded = updated.rounded;\n\tthumbnailIsVideo = updated.isVideo;\n\tthumbnailKind = updated.kind;\n\tthumbnailId = updated.id;\n\tif (thumbnail) {\n\t\tthumbnail->subscribeToUpdates(std::move(repaint));\n\t}\n}\n\nPoll::Footer::Footer(not_null<Poll*> owner)\n: Part(owner)\n, _showResultsLink(\n\tstd::make_shared<LambdaClickHandler>(crl::guard(\n\t\towner,\n\t\t[=] { owner->_optionsPart->showResults(); })))\n, _sendVotesLink(\n\tstd::make_shared<LambdaClickHandler>(crl::guard(\n\t\towner,\n\t\t[=] { owner->_optionsPart->sendMultiOptions(); })))\n, _adminVotesLink(\n\tstd::make_shared<LambdaClickHandler>(crl::guard(\n\t\towner,\n\t\t[=] {\n\t\t\tif (owner->_flags & PollData::Flag::PublicVotes) {\n\t\t\t\towner->_optionsPart->showResults();\n\t\t\t} else {\n\t\t\t\towner->_optionsPart->saveStateInAnimation();\n\t\t\t\towner->_adminShowResults = true;\n\t\t\t\towner->_optionsPart->updateAnswerVotes();\n\t\t\t\towner->_optionsPart->startAnswersAnimation();\n\t\t\t}\n\t\t})))\n, _adminBackVoteLink(\n\tstd::make_shared<LambdaClickHandler>(crl::guard(\n\t\towner,\n\t\t[=] {\n\t\t\towner->_optionsPart->saveStateInAnimation();\n\t\t\towner->_adminShowResults = false;\n\t\t\towner->_optionsPart->updateAnswerVotes();\n\t\t\towner->_optionsPart->startAnswersAnimation();\n\t\t})))\n, _saveOptionLink(\n\tstd::make_shared<LambdaClickHandler>(crl::guard(\n\t\towner,\n\t\t[=](ClickContext context) {\n\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\tif (const auto delegate = my.elementDelegate\n\t\t\t\t? my.elementDelegate()\n\t\t\t\t: nullptr) {\n\t\t\t\tdelegate->elementSubmitAddPollOption(\n\t\t\t\t\towner->_parent->data()->fullId());\n\t\t\t}\n\t\t})))\n, _closeTimer([=] { owner->repaint(); }) {\n}\n\nPoll::Poll(\n\tnot_null<Element*> parent,\n\tnot_null<PollData*> poll,\n\tconst TextWithEntities &consumed)\n: Media(parent)\n, _poll(poll)\n{\n\t_headerPart = std::make_unique<Header>(this);\n\t_optionsPart = std::make_unique<Options>(this);\n\t_addOptionPart = std::make_unique<AddOption>(this);\n\t_footerPart = std::make_unique<Footer>(this);\n\tif (!consumed.text.isEmpty()) {\n\t\t_headerPart->updateDescription();\n\t}\n\thistory()->owner().registerPollView(_poll, _parent);\n}\n\nQSize Poll::countOptimalSize() {\n\tupdateTexts();\n\n\tconst auto paddings = st::msgPadding.left() + st::msgPadding.right();\n\n\tauto maxWidth = st::msgFileMinWidth;\n\taccumulate_max(maxWidth, paddings + _headerPart->_description.maxWidth());\n\taccumulate_max(maxWidth, paddings + _headerPart->_question.maxWidth());\n\tfor (const auto &answer : _optionsPart->_answers) {\n\t\tconst auto media = answer.thumbnail\n\t\t\t? (PollAnswerMediaSize() + PollAnswerMediaSkip())\n\t\t\t: 0;\n\t\taccumulate_max(\n\t\t\tmaxWidth,\n\t\t\tpaddings\n\t\t\t+ st::historyPollAnswerPadding.left()\n\t\t\t+ answer.text.maxWidth()\n\t\t\t+ media\n\t\t\t+ st::historyPollAnswerPadding.right());\n\t}\n\n\tconst auto innerWidth = maxWidth - paddings;\n\tauto minHeight = _headerPart->countHeight(innerWidth)\n\t\t+ _optionsPart->countHeight(innerWidth)\n\t\t+ _addOptionPart->countHeight(innerWidth)\n\t\t+ _footerPart->countHeight(innerWidth);\n\treturn { maxWidth, minHeight };\n}\n\nbool Poll::showVotes() const {\n\tif (_adminShowResults) {\n\t\treturn true;\n\t}\n\tif (_flags & PollData::Flag::HideResultsUntilClose) {\n\t\treturn (_flags & PollData::Flag::Closed) || _parent->data()->out();\n\t}\n\treturn _voted || (_flags & PollData::Flag::Closed);\n}\n\nbool Poll::isAuthorNotVoted() const {\n\treturn _parent->data()->out()\n\t\t&& !_voted\n\t\t&& !(_flags & PollData::Flag::Closed);\n}\n\nbool Poll::canVote() const {\n\treturn !showVotes() && !_voted && _parent->data()->isRegular();\n}\n\nbool Poll::canSendVotes() const {\n\treturn canVote() && _optionsPart->_hasSelected;\n}\n\nbool Poll::showVotersCount() const {\n\tif (_voted && !showVotes()) {\n\t\treturn !(_flags & PollData::Flag::PublicVotes);\n\t}\n\treturn showVotes()\n\t\t? (!_totalVotes || !(_flags & PollData::Flag::PublicVotes))\n\t\t: !(_flags & PollData::Flag::MultiChoice);\n}\n\nbool Poll::inlineFooter() const {\n\treturn !(_flags\n\t\t& (PollData::Flag::PublicVotes | PollData::Flag::MultiChoice));\n}\n\nbool Poll::canAddOption() const {\n\treturn (_flags & PollData::Flag::OpenAnswers)\n\t\t&& !(_flags & PollData::Flag::Closed)\n\t\t&& !_parent->data()->Has<HistoryMessageForwarded>()\n\t\t&& (int(_poll->answers.size())\n\t\t\t< _poll->session().appConfig().pollOptionsLimit());\n}\n\nQRect Poll::addOptionRect(int innerWidth) const {\n\tconst auto answersHeight = ranges::accumulate(ranges::views::all(\n\t\t_optionsPart->_answers\n\t) | ranges::views::transform([&](const Answer &answer) {\n\t\treturn _optionsPart->countAnswerHeight(answer, innerWidth);\n\t}), 0);\n\tconst auto top = _headerPart->countQuestionTop(innerWidth)\n\t\t+ _headerPart->_question.countHeight(innerWidth)\n\t\t+ st::historyPollSubtitleSkip\n\t\t+ st::msgDateFont->height\n\t\t+ st::historyPollAnswersSkip\n\t\t+ answersHeight\n\t\t+ (st::historyPollChoiceRight.height()\n\t\t\t- st::historyPollFillingHeight) / 2;\n\treturn QRect(\n\t\tst::msgPadding.left(),\n\t\ttop,\n\t\tinnerWidth,\n\t\t_addOptionPart->countHeight(innerWidth));\n}\n\nvoid Poll::setAddOptionActive(bool active) {\n\tif (_addOptionActive != active) {\n\t\t_addOptionActive = active;\n\t\thistory()->owner().requestViewResize(_parent);\n\t}\n}\n\nint Poll::Options::countAnswerContentWidth(\n\t\tconst Answer &answer,\n\t\tint innerWidth) const {\n\tconst auto answerWidth = innerWidth\n\t\t- st::historyPollAnswerPadding.left()\n\t\t- st::historyPollAnswerPadding.right();\n\tconst auto mediaWidth = answer.thumbnail\n\t\t? (PollAnswerMediaSize() + PollAnswerMediaSkip())\n\t\t: 0;\n\treturn std::max(1, answerWidth - mediaWidth);\n}\n\nint Poll::Options::countVotesExtraHeight(\n\t\tconst Answer &answer,\n\t\tint textWidth) const {\n\tif (answer.votesCountString.isEmpty()\n\t\t&& answer.recentVoters.empty()) {\n\t\treturn 0;\n\t}\n\tconst auto lineWidths = answer.text.countLineWidths(textWidth);\n\tif (lineWidths.empty()) {\n\t\treturn 0;\n\t}\n\tconst auto voterCount = int(answer.recentVoters.size());\n\tconst auto &ust = st::historyPollAnswerUserpics;\n\tconst auto userpicsWidth = voterCount\n\t\t? (ust.size + (voterCount - 1) * (ust.size - ust.shift))\n\t\t: 0;\n\tconst auto userpicsExtra = userpicsWidth\n\t\t? (st::historyPollAnswerUserpicsSkip + userpicsWidth)\n\t\t: 0;\n\tconst auto votesWidth = answer.votesCountWidth\n\t\t+ userpicsExtra\n\t\t+ st::historyPollFillingRight\n\t\t+ st::historyPollPercentSkip;\n\tconst auto lastLineWidth = lineWidths.back();\n\tif (lastLineWidth + votesWidth <= textWidth) {\n\t\treturn 0;\n\t}\n\treturn st::normalFont->height;\n}\n\nint Poll::Options::countAnswerHeight(\n\t\tconst Answer &answer,\n\t\tint innerWidth) const {\n\tconst auto media = answer.thumbnail ? PollAnswerMediaSize() : 0;\n\tconst auto textWidth = countAnswerContentWidth(answer, innerWidth);\n\tconst auto &padding = answer.thumbnail\n\t\t? st::historyPollAnswerPadding\n\t\t: st::historyPollAnswerPaddingNoMedia;\n\tconst auto textHeight = answer.text.countHeight(textWidth);\n\tconst auto multiline = (textHeight\n\t\t> st::historyPollPercentFont->height);\n\tconst auto votesExtra = _owner->showVotes()\n\t\t? countVotesExtraHeight(answer, textWidth)\n\t\t: 0;\n\tconst auto fillingWithChoice = (_owner->showVotes()\n\t\t&& (media || multiline || votesExtra))\n\t\t? (std::max(textHeight, st::historyPollPercentFont->height)\n\t\t\t+ votesExtra\n\t\t\t+ st::historyPollFillingTop\n\t\t\t+ (st::historyPollFillingHeight\n\t\t\t\t+ st::historyPollChoiceRight.height()) / 2)\n\t\t: 0;\n\treturn padding.top()\n\t\t+ std::max({\n\t\t\ttextHeight,\n\t\t\tmedia,\n\t\t\tfillingWithChoice,\n\t\t})\n\t\t+ padding.bottom();\n}\n\nQSize Poll::countCurrentSize(int newWidth) {\n\taccumulate_min(newWidth, maxWidth());\n\tconst auto innerWidth = newWidth\n\t\t- st::msgPadding.left()\n\t\t- st::msgPadding.right();\n\n\tauto newHeight = _headerPart->countHeight(innerWidth)\n\t\t+ _optionsPart->countHeight(innerWidth)\n\t\t+ _addOptionPart->countHeight(innerWidth)\n\t\t+ _footerPart->countHeight(innerWidth);\n\treturn { newWidth, newHeight };\n}\n\nvoid Poll::updateTexts() {\n\tconst auto current = _poll->owner().poll(_poll->id);\n\tif (_poll != current) {\n\t\t_poll = current;\n\t\t_pollVersion = 0;\n\t}\n\tif (_pollVersion == _poll->version) {\n\t\treturn;\n\t}\n\tconst auto first = !_pollVersion;\n\t_pollVersion = _poll->version;\n\n\tconst auto willStartAnimation = _optionsPart->checkAnimationStart();\n\tconst auto voted = _voted;\n\n\t_headerPart->updateDescription();\n\tif (_headerPart->_question.toTextWithEntities() != _poll->question) {\n\t\tauto options = Ui::WebpageTextTitleOptions();\n\t\toptions.maxw = options.maxh = 0;\n\t\t_headerPart->_question.setMarkedText(\n\t\t\tst::historyPollQuestionStyle,\n\t\t\t_poll->question,\n\t\t\toptions,\n\t\t\tCore::TextContext({\n\t\t\t\t.session = &_poll->session(),\n\t\t\t\t.repaint = [=] {\n\t\t\t\t\tif (!_parent->delegate()->elementAnimationsPaused()) {\n\t\t\t\t\t\trepaint();\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t.customEmojiLoopLimit = 2,\n\t\t\t}));\n\t}\n\tif (_flags != _poll->flags() || _headerPart->_subtitle.isEmpty()) {\n\t\tusing Flag = PollData::Flag;\n\t\t_flags = _poll->flags();\n\t\t_headerPart->_subtitle.setText(\n\t\t\tst::msgDateTextStyle,\n\t\t\t((_flags & Flag::Closed)\n\t\t\t\t? tr::lng_polls_closed(tr::now)\n\t\t\t\t: (_flags & Flag::Quiz)\n\t\t\t\t? ((_flags & Flag::PublicVotes)\n\t\t\t\t\t? tr::lng_polls_public_quiz(tr::now)\n\t\t\t\t\t: tr::lng_polls_anonymous_quiz(tr::now))\n\t\t\t\t: ((_flags & Flag::PublicVotes)\n\t\t\t\t\t? tr::lng_polls_public(tr::now)\n\t\t\t\t\t: tr::lng_polls_anonymous(tr::now))));\n\t}\n\tif (_footerPart->_adminBackVoteLabel.isEmpty()) {\n\t\t_footerPart->_adminBackVoteLabel.setMarkedText(\n\t\t\tst::semiboldTextStyle,\n\t\t\ttr::lng_polls_admin_back_vote(\n\t\t\t\ttr::now,\n\t\t\t\tlt_arrow,\n\t\t\t\tUi::Text::IconEmoji(&st::textBackIconEmoji),\n\t\t\t\ttr::marked));\n\t}\n\t_headerPart->updateRecentVoters();\n\t_optionsPart->updateAnswers();\n\t_headerPart->updateAttachedMedia();\n\t_headerPart->updateSolutionText();\n\t_headerPart->updateSolutionMedia();\n\tupdateVotes();\n\n\tif (willStartAnimation) {\n\t\t_optionsPart->startAnswersAnimation();\n\t\tif (!voted) {\n\t\t\t_optionsPart->checkQuizAnswered();\n\t\t}\n\t}\n\t_headerPart->solutionToggled(\n\t\t_headerPart->_solutionShown,\n\t\tfirst ? anim::type::instant : anim::type::normal);\n}\n\nvoid Poll::Header::updateDescription() {\n\tconst auto media = _owner->_parent->data()->media();\n\tconst auto consumed = media\n\t\t? media->consumedMessageText()\n\t\t: TextWithEntities();\n\tif (consumed.text.isEmpty()) {\n\t\t_description = Ui::Text::String(st::msgMinWidth / 2);\n\t\treturn;\n\t}\n\tif (_description.toTextWithEntities() == consumed) {\n\t\treturn;\n\t}\n\tconst auto context = Core::TextContext({\n\t\t.session = &_owner->_poll->session(),\n\t\t.repaint = [=] {\n\t\t\tif (!_owner->_parent->delegate()->elementAnimationsPaused()) {\n\t\t\t\t_owner->_parent->customEmojiRepaint();\n\t\t\t}\n\t\t},\n\t\t.customEmojiLoopLimit = 2,\n\t});\n\t_description.setMarkedText(\n\t\tst::webPageDescriptionStyle,\n\t\tconsumed,\n\t\tUi::ItemTextOptions(_owner->_parent->data()),\n\t\tcontext);\n\tInitElementTextPart(_owner->_parent, _description);\n}\n\nvoid Poll::Header::updateSolutionText() {\n\tif (_owner->_poll->solution.text.isEmpty()) {\n\t\t_solutionText = Ui::Text::String();\n\t\treturn;\n\t}\n\tif (_solutionText.toTextWithEntities() == _owner->_poll->solution) {\n\t\treturn;\n\t}\n\t_solutionText = Ui::Text::String(st::msgMinWidth);\n\t_solutionText.setMarkedText(\n\t\tst::webPageDescriptionStyle,\n\t\t_owner->_poll->solution,\n\t\tUi::ItemTextOptions(_owner->_parent->data()),\n\t\tCore::TextContext({\n\t\t\t.session = &_owner->_poll->session(),\n\t\t\t.repaint = [=] {\n\t\t\t\tif (!_owner->_parent->delegate()->elementAnimationsPaused()) {\n\t\t\t\t\t_owner->repaint();\n\t\t\t\t}\n\t\t\t},\n\t\t}));\n\tInitElementTextPart(_owner->_parent, _solutionText);\n}\n\nvoid Poll::Header::updateSolutionMedia() {\n\tconst auto item = _owner->_parent->data();\n\tconst auto messageContext = Window::SessionController::MessageContext{\n\t\t.id = item->fullId(),\n\t\t.topicRootId = item->topicRootId(),\n\t\t.monoforumPeerId = item->sublistPeerId(),\n\t};\n\tconst auto paused = [=] {\n\t\treturn _owner->_parent->delegate()->elementAnimationsPaused();\n\t};\n\tconst auto updated = MakePollThumbnail(\n\t\t_owner->_poll,\n\t\t_owner->_poll->solutionMedia,\n\t\tmessageContext,\n\t\tpaused);\n\tif (!updated.thumbnail) {\n\t\t_solutionMedia = nullptr;\n\t\t_solutionAttach = nullptr;\n\t\treturn;\n\t}\n\tif (_solutionMedia\n\t\t&& _solutionMedia->kind == updated.kind\n\t\t&& _solutionMedia->id == updated.id) {\n\t\treturn;\n\t}\n\tif (!_solutionMedia) {\n\t\t_solutionMedia = std::make_unique<SolutionMedia>();\n\t}\n\t_solutionMedia->kind = updated.kind;\n\t_solutionMedia->id = updated.id;\n\tauto photo = (PhotoData*)(nullptr);\n\tauto document = (DocumentData*)(nullptr);\n\tif (updated.kind == PollThumbnailKind::Photo && updated.id) {\n\t\tphoto = _owner->_poll->owner().photo(PhotoId(updated.id));\n\t} else if (updated.kind == PollThumbnailKind::Document && updated.id) {\n\t\tdocument = _owner->_poll->owner().document(DocumentId(updated.id));\n\t} else if (updated.kind == PollThumbnailKind::Audio && updated.id) {\n\t\tdocument = _owner->_poll->owner().document(DocumentId(updated.id));\n\t} else if (updated.kind == PollThumbnailKind::Geo\n\t\t&& _owner->_poll->solutionMedia.geo) {\n\t\tconst auto &point = *_owner->_poll->solutionMedia.geo;\n\t\tconst auto cloudImage = _owner->_poll->owner().location(point);\n\t\t_solutionAttach = std::make_unique<Location>(\n\t\t\t_owner->_parent,\n\t\t\tcloudImage,\n\t\t\tpoint,\n\t\t\tQString(),\n\t\t\tQString());\n\t\treturn;\n\t}\n\t_solutionAttach = (photo || document)\n\t\t? CreateAttach(_owner->_parent, document, photo)\n\t\t: nullptr;\n}\n\nvoid Poll::Header::updateAttachedMedia() {\n\tconst auto item = _owner->_parent->data();\n\tconst auto messageContext = Window::SessionController::MessageContext{\n\t\t.id = item->fullId(),\n\t\t.topicRootId = item->topicRootId(),\n\t\t.monoforumPeerId = item->sublistPeerId(),\n\t};\n\tconst auto paused = [=] {\n\t\treturn _owner->_parent->delegate()->elementAnimationsPaused();\n\t};\n\tconst auto updated = MakePollThumbnail(\n\t\t_owner->_poll,\n\t\t_owner->_poll->attachedMedia,\n\t\tmessageContext,\n\t\tpaused);\n\tconst auto same = (_attachedMedia->kind == updated.kind)\n\t\t&& (_attachedMedia->id == updated.id)\n\t\t&& (_attachedMedia->rounded == updated.rounded);\n\tif (same) {\n\t\treturn;\n\t}\n\tif (_attachedMedia->thumbnail) {\n\t\t_attachedMedia->thumbnail->subscribeToUpdates(nullptr);\n\t}\n\t_attachedMediaCache = QImage();\n\t_attachedMedia->thumbnail = updated.thumbnail;\n\t_attachedMedia->handler = updated.handler;\n\t_attachedMedia->kind = updated.kind;\n\t_attachedMedia->rounded = updated.rounded;\n\t_attachedMedia->id = updated.id;\n\t_attachedMedia->photo = nullptr;\n\t_attachedMedia->photoMedia = nullptr;\n\t_attachedMedia->photoSize = QSize();\n\tif (updated.kind == PollThumbnailKind::Photo && updated.id) {\n\t\tconst auto photo = _owner->_poll->owner().photo(PhotoId(updated.id));\n\t\t_attachedMedia->photo = photo;\n\t\t_attachedMedia->photoMedia = photo->createMediaView();\n\t\t_attachedMedia->photoMedia->wanted(\n\t\t\tData::PhotoSize::Large,\n\t\t\t_owner->_parent->data()->fullId());\n\t\tif (const auto size = photo->size(Data::PhotoSize::Large)) {\n\t\t\t_attachedMedia->photoSize = *size;\n\t\t} else if (const auto s = photo->size(Data::PhotoSize::Thumbnail)) {\n\t\t\t_attachedMedia->photoSize = *s;\n\t\t}\n\t}\n\tif ((updated.kind == PollThumbnailKind::Document\n\t\t|| updated.kind == PollThumbnailKind::Audio) && updated.id) {\n\t\tconst auto document = _owner->_poll->owner().document(\n\t\t\tDocumentId(updated.id));\n\t\t_attachedMediaAttach = CreateAttach(\n\t\t\t_owner->_parent,\n\t\t\tdocument,\n\t\t\tnullptr);\n\t} else {\n\t\t_attachedMediaAttach = nullptr;\n\t}\n\tif (_attachedMedia->thumbnail) {\n\t\t_attachedMedia->thumbnail->subscribeToUpdates(\n\t\t\tcrl::guard(_owner, [=] {\n\t\t\t\tif (!_owner->_parent->delegate()->elementAnimationsPaused()) {\n\t\t\t\t\t_attachedMediaCache = QImage();\n\t\t\t\t\t_owner->repaint();\n\t\t\t\t}\n\t\t\t}));\n\t}\n}\n\nint Poll::Header::countTopContentSkip(int pollWidth) const {\n\treturn countTopMediaHeight(pollWidth)\n\t\t? st::historyPollMediaTopSkip\n\t\t: _owner->isBubbleTop()\n\t\t? st::historyPollQuestionTop\n\t\t: (st::historyPollQuestionTop - st::msgFileTopMinus);\n}\n\nint Poll::Header::countTopMediaHeight(int pollWidth) const {\n\tif (!_attachedMedia || !_attachedMedia->thumbnail) {\n\t\treturn 0;\n\t}\n\tif (_attachedMediaAttach) {\n\t\treturn countAttachHeight(pollWidth);\n\t}\n\tif (_attachedMedia->kind == PollThumbnailKind::Photo\n\t\t&& !_attachedMedia->photoSize.isEmpty()) {\n\t\tconst auto w = pollWidth > 0 ? pollWidth : _owner->width();\n\t\tconst auto sideSkip = st::historyPollMediaSideSkip;\n\t\tconst auto availableWidth = w - 2 * sideSkip;\n\t\tconst auto &original = _attachedMedia->photoSize;\n\t\treturn std::max(\n\t\t\t1,\n\t\t\tint(original.height() * availableWidth / original.width()));\n\t}\n\treturn st::historyPollMediaHeight;\n}\n\nint Poll::Header::countAttachHeight(int pollWidth) const {\n\tif (!_attachedMediaAttach) {\n\t\treturn 0;\n\t}\n\t_attachedMediaAttach->initDimensions();\n\tconst auto w = pollWidth > 0 ? pollWidth : _owner->width();\n\tconst auto sideSkip = st::historyPollMediaSideSkip;\n\tconst auto innerWidth = w - 2 * sideSkip;\n\treturn _attachedMediaAttach->resizeGetHeight(\n\t\tstd::max(1, innerWidth));\n}\n\nQRect Poll::Header::countTopMediaRect(int top) const {\n\tconst auto sideSkip = st::historyPollMediaSideSkip;\n\tconst auto mediaHeight = countTopMediaHeight();\n\treturn mediaHeight\n\t\t? QRect(\n\t\t\tsideSkip,\n\t\t\ttop,\n\t\t\tstd::max(1, _owner->width() - 2 * sideSkip),\n\t\t\tmediaHeight)\n\t\t: QRect();\n}\n\nUi::BubbleRounding Poll::Header::topMediaRounding() const {\n\tusing Corner = Ui::BubbleCornerRounding;\n\tauto result = _owner->adjustedBubbleRounding(\n\t\tRectPart::BottomLeft | RectPart::BottomRight);\n\tconst auto normalize = [](Corner value) {\n\t\treturn (value == Corner::Large)\n\t\t\t? Corner::Large\n\t\t\t: (value == Corner::None)\n\t\t\t? Corner::None\n\t\t\t: Corner::Small;\n\t};\n\tresult.topLeft = normalize(result.topLeft);\n\tresult.topRight = normalize(result.topRight);\n\tresult.bottomLeft = Corner::Small;\n\tresult.bottomRight = Corner::Small;\n\treturn result;\n}\n\nvoid Poll::Header::validateTopMediaCache(QSize size) const {\n\tif (!_attachedMedia || !_attachedMedia->thumbnail || size.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto rounding = topMediaRounding();\n\tif ((_attachedMediaCache.size() == (size * ratio))\n\t\t&& (_attachedMediaCacheRounding == rounding)) {\n\t\treturn;\n\t}\n\tauto source = QImage();\n\tif (_attachedMedia->photoMedia) {\n\t\tif (const auto image\n\t\t\t= _attachedMedia->photoMedia->image(Data::PhotoSize::Large)) {\n\t\t\tsource = image->original();\n\t\t} else if (const auto image\n\t\t\t= _attachedMedia->photoMedia->image(\n\t\t\t\tData::PhotoSize::Thumbnail)) {\n\t\t\tsource = image->original();\n\t\t} else if (const auto image\n\t\t\t= _attachedMedia->photoMedia->thumbnailInline()) {\n\t\t\tsource = image->original();\n\t\t}\n\t}\n\tif (source.isNull()) {\n\t\tsource = _attachedMedia->thumbnail->image(\n\t\t\tstd::max(size.width(), size.height()) * ratio);\n\t}\n\tif (source.isNull()) {\n\t\treturn;\n\t}\n\tconst auto sw = source.width();\n\tconst auto sh = source.height();\n\tconst auto tw = size.width() * ratio;\n\tconst auto th = size.height() * ratio;\n\tif (sw * th != sh * tw) {\n\t\tconst auto cropW = std::min(sw, sh * tw / th);\n\t\tconst auto cropH = std::min(sh, sw * th / tw);\n\t\tsource = source.copy(\n\t\t\t(sw - cropW) / 2,\n\t\t\t(sh - cropH) / 2,\n\t\t\tcropW,\n\t\t\tcropH);\n\t}\n\tauto prepared = Images::Prepare(\n\t\tsource,\n\t\tsize * ratio,\n\t\t{ .outer = size });\n\tprepared = Images::Round(\n\t\tstd::move(prepared),\n\t\tMediaRoundingMask(rounding));\n\tprepared.setDevicePixelRatio(ratio);\n\t_attachedMediaCache = std::move(prepared);\n\t_attachedMediaCacheRounding = rounding;\n}\n\nint Poll::Header::countDescriptionHeight(int innerWidth) const {\n\treturn _description.isEmpty() ? 0 : _description.countHeight(innerWidth);\n}\n\nint Poll::Header::countSolutionMediaHeight(int mediaWidth) const {\n\tif (!_solutionAttach) {\n\t\treturn 0;\n\t}\n\t_solutionAttach->initDimensions();\n\treturn _solutionAttach->resizeGetHeight(mediaWidth);\n}\n\nint Poll::Header::countSolutionBlockHeight(int innerWidth) const {\n\tif (!_solutionShown || !canShowSolution()) {\n\t\treturn 0;\n\t}\n\tconst auto &qst = st::historyPagePreview;\n\tconst auto textWidth = innerWidth\n\t\t- qst.padding.left()\n\t\t- qst.padding.right();\n\tauto height = qst.padding.top();\n\theight += st::semiboldFont->height;\n\theight += st::historyPollExplanationTitleSkip;\n\theight += _solutionText.countHeight(textWidth);\n\tif (const auto mediaHeight = countSolutionMediaHeight(textWidth)) {\n\t\theight += st::historyPollExplanationMediaSkip + mediaHeight;\n\t}\n\theight += qst.padding.bottom();\n\treturn height;\n}\n\nint Poll::Header::countQuestionTop(int innerWidth, int pollWidth) const {\n\tauto result = countTopContentSkip(pollWidth);\n\tif (const auto mediaHeight = countTopMediaHeight(pollWidth)) {\n\t\tresult += mediaHeight + st::historyPollMediaSkip;\n\t}\n\tif (const auto descriptionHeight = countDescriptionHeight(innerWidth)) {\n\t\tresult += descriptionHeight + st::historyPollDescriptionSkip;\n\t}\n\tif (const auto solutionHeight = countSolutionBlockHeight(innerWidth)) {\n\t\tresult += solutionHeight + st::historyPollExplanationSkip;\n\t}\n\treturn result;\n}\n\nuint16 Poll::Header::solutionSelectionLength() const {\n\treturn (_solutionShown && canShowSolution())\n\t\t? _solutionText.length()\n\t\t: uint16(0);\n}\n\nTextSelection Poll::Header::toSolutionSelection(\n\t\tTextSelection selection) const {\n\treturn UnshiftItemSelection(selection, _description);\n}\n\nTextSelection Poll::Header::fromSolutionSelection(\n\t\tTextSelection selection) const {\n\treturn ShiftItemSelection(selection, _description);\n}\n\nTextSelection Poll::Header::toQuestionSelection(\n\t\tTextSelection selection) const {\n\treturn UnshiftItemSelection(\n\t\tselection,\n\t\tuint16(_description.length() + solutionSelectionLength()));\n}\n\nTextSelection Poll::Header::fromQuestionSelection(\n\t\tTextSelection selection) const {\n\treturn ShiftItemSelection(\n\t\tselection,\n\t\tuint16(_description.length() + solutionSelectionLength()));\n}\n\nvoid Poll::Options::checkQuizAnswered() {\n\tif (!_owner->_voted\n\t\t|| !_votedFromHere\n\t\t|| !_owner->_poll->quiz()\n\t\t|| anim::Disabled()) {\n\t\treturn;\n\t}\n\tconst auto i = ranges::find(_answers, true, &Answer::chosen);\n\tif (i == end(_answers)) {\n\t\treturn;\n\t}\n\tif (i->correct) {\n\t\t_owner->_fireworksAnimation = std::make_unique<Ui::FireworksAnimation>(\n\t\t\t[=] { _owner->repaint(); });\n\t} else {\n\t\t_owner->_wrongAnswerAnimation.start(\n\t\t\t[=] { _owner->repaint(); },\n\t\t\t0.,\n\t\t\t1.,\n\t\t\tkRollDuration,\n\t\t\tanim::linear);\n\t\t_owner->_headerPart->showSolution();\n\t}\n}\n\nvoid Poll::Header::showSolution() const {\n\tif (!_owner->_poll->solution.text.isEmpty()) {\n\t\tsolutionToggled(true);\n\t}\n}\n\nvoid Poll::Header::solutionToggled(\n\t\tbool solutionShown,\n\t\tanim::type animated) const {\n\t_solutionShown = solutionShown;\n\tconst auto visible = canShowSolution() && !_solutionShown;\n\tif (_solutionButtonVisible == visible) {\n\t\tif (animated == anim::type::instant\n\t\t\t&& _solutionButtonAnimation.animating()) {\n\t\t\t_solutionButtonAnimation.stop();\n\t\t\t_owner->repaint();\n\t\t}\n\t\treturn;\n\t}\n\t_solutionButtonVisible = visible;\n\tif (animated == anim::type::instant) {\n\t\t_solutionButtonAnimation.stop();\n\t} else {\n\t\t_solutionButtonAnimation.start(\n\t\t\t[=] { _owner->repaint(); },\n\t\t\tvisible ? 0. : 1.,\n\t\t\tvisible ? 1. : 0.,\n\t\t\tst::fadeWrapDuration);\n\t}\n\t_owner->history()->owner().requestViewResize(_owner->_parent);\n}\n\nvoid Poll::Header::updateRecentVoters() {\n\tauto &&sliced = ranges::views::all(\n\t\t_owner->_poll->recentVoters\n\t) | ranges::views::take(kShowRecentVotersCount);\n\tconst auto changed = !ranges::equal(\n\t\t_recentVoters,\n\t\tsliced,\n\t\tranges::equal_to(),\n\t\t&RecentVoter::peer);\n\tif (changed) {\n\t\tauto updated = ranges::views::all(\n\t\t\tsliced\n\t\t) | ranges::views::transform([](not_null<PeerData*> peer) {\n\t\t\treturn RecentVoter{ peer };\n\t\t}) | ranges::to_vector;\n\t\tconst auto has = _owner->hasHeavyPart();\n\t\tif (has) {\n\t\t\tfor (auto &voter : updated) {\n\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\t_recentVoters,\n\t\t\t\t\tvoter.peer,\n\t\t\t\t\t&RecentVoter::peer);\n\t\t\t\tif (i != end(_recentVoters)) {\n\t\t\t\t\tvoter.userpic = std::move(i->userpic);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t_recentVoters = std::move(updated);\n\t\tif (has && !_owner->hasHeavyPart()) {\n\t\t\t_owner->_parent->checkHeavyPart();\n\t\t}\n\t}\n}\n\nvoid Poll::Options::updateAnswers() {\n\tconst auto context = Core::TextContext({\n\t\t.session = &_owner->_poll->session(),\n\t\t.repaint = [=] {\n\t\t\tif (!_owner->_parent->delegate()->elementAnimationsPaused()) {\n\t\t\t\t_owner->repaint();\n\t\t\t}\n\t\t},\n\t\t.customEmojiLoopLimit = 2,\n\t});\n\tconst auto repaintThumbnail = crl::guard(_owner, [=] {\n\t\tif (!_owner->_parent->delegate()->elementAnimationsPaused()) {\n\t\t\t_owner->repaint();\n\t\t}\n\t});\n\tconst auto paused = [=] {\n\t\treturn _owner->_parent->delegate()->elementAnimationsPaused();\n\t};\n\tconst auto item = _owner->_parent->data();\n\tconst auto messageContext = Window::SessionController::MessageContext{\n\t\t.id = item->fullId(),\n\t\t.topicRootId = item->topicRootId(),\n\t\t.monoforumPeerId = item->sublistPeerId(),\n\t};\n\tauto options = ranges::views::all(\n\t\t_owner->_poll->answers\n\t) | ranges::views::transform(&PollAnswer::option) | ranges::to_vector;\n\tif ((_owner->_flags & PollData::Flag::ShuffleAnswers)\n\t\t&& !(_owner->_flags & PollData::Flag::Creator)) {\n\t\tconst auto userId = _owner->_poll->session().userId();\n\t\tconst auto pollId = _owner->_poll->id;\n\t\tranges::sort(options, [&](const QByteArray &a, const QByteArray &b) {\n\t\t\tconst auto hashA = HashPollShuffleValue(userId, pollId, a);\n\t\t\tconst auto hashB = HashPollShuffleValue(userId, pollId, b);\n\t\t\treturn (hashA == hashB) ? (a < b) : (hashA < hashB);\n\t\t});\n\t}\n\tconst auto changed = (_answers.size() != options.size())\n\t\t|| !ranges::equal(\n\t\t\t_answers,\n\t\t\toptions,\n\t\t\tranges::equal_to(),\n\t\t\t&Answer::option);\n\tif (!changed) {\n\t\tfor (auto &answer : _answers) {\n\t\t\tconst auto i = ranges::find(\n\t\t\t\t_owner->_poll->answers,\n\t\t\t\tanswer.option,\n\t\t\t\t&PollAnswer::option);\n\t\t\tAssert(i != end(_owner->_poll->answers));\n\t\t\tanswer.fillData(_owner->_poll, *i, context);\n\t\t\tanswer.fillMedia(\n\t\t\t\t_owner->_poll,\n\t\t\t\t*i,\n\t\t\t\tmessageContext,\n\t\t\t\trepaintThumbnail,\n\t\t\t\tpaused);\n\t\t}\n\t\t_anyAnswerHasMedia = ranges::any_of(_answers, [](const Answer &a) {\n\t\t\treturn a.thumbnail != nullptr;\n\t\t});\n\t\treturn;\n\t}\n\t_answers = ranges::views::all(options) | ranges::views::transform([&](\n\t\t\tconst QByteArray &option) {\n\t\tauto result = Answer();\n\t\tresult.option = option;\n\t\tconst auto i = ranges::find(\n\t\t\t_owner->_poll->answers,\n\t\t\toption,\n\t\t\t&PollAnswer::option);\n\t\tAssert(i != end(_owner->_poll->answers));\n\t\tresult.fillData(_owner->_poll, *i, context);\n\t\tresult.fillMedia(\n\t\t\t_owner->_poll,\n\t\t\t*i,\n\t\t\tmessageContext,\n\t\t\trepaintThumbnail,\n\t\t\tpaused);\n\t\treturn result;\n\t}) | ranges::to_vector;\n\n\tif ((_owner->_flags & PollData::Flag::ShuffleAnswers)\n\t\t&& !(_owner->_flags & PollData::Flag::Creator)) {\n\t\tconst auto visitorId = _owner->_poll->session().userId();\n\t\tconst auto pollId = _owner->_poll->id;\n\t\tranges::sort(_answers, [&](const Answer &a, const Answer &b) {\n\t\t\treturn HashPollShuffleValue(visitorId, pollId, a.option)\n\t\t\t\t< HashPollShuffleValue(visitorId, pollId, b.option);\n\t\t});\n\t}\n\n\tfor (auto &answer : _answers) {\n\t\tanswer.handler = createAnswerClickHandler(answer);\n\t}\n\t_anyAnswerHasMedia = ranges::any_of(_answers, [](const Answer &a) {\n\t\treturn a.thumbnail != nullptr;\n\t});\n\n\tresetAnswersAnimation();\n}\n\nClickHandlerPtr Poll::Options::createAnswerClickHandler(\n\t\tconst Answer &answer) {\n\tconst auto option = answer.option;\n\tauto result = ClickHandlerPtr();\n\tif (_owner->_flags & PollData::Flag::MultiChoice) {\n\t\tresult = std::make_shared<LambdaClickHandler>(crl::guard(_owner, [=] {\n\t\t\tif (Logs::DebugEnabled() && base::IsCtrlPressed()) {\n\t\t\t\tTextUtilities::SetClipboardText(\n\t\t\t\t\tTextForMimeData::Simple(_owner->_poll->debugString()));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (_owner->canVote()) {\n\t\t\t\t_owner->_optionsPart->toggleMultiOption(option);\n\t\t\t} else if (_owner->showVotes()) {\n\t\t\t\t_owner->_optionsPart->showAnswerVotesTooltip(option);\n\t\t\t}\n\t\t}));\n\t} else {\n\t\tresult = std::make_shared<LambdaClickHandler>(crl::guard(_owner, [=] {\n\t\t\tif (Logs::DebugEnabled() && base::IsCtrlPressed()) {\n\t\t\t\tTextUtilities::SetClipboardText(\n\t\t\t\t\tTextForMimeData::Simple(_owner->_poll->debugString()));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (_owner->canVote()) {\n\t\t\t\t_owner->_optionsPart->_votedFromHere = true;\n\t\t\t\t_owner->history()->session().api().polls().sendVotes(\n\t\t\t\t\t_owner->_parent->data()->fullId(),\n\t\t\t\t\t{ option });\n\t\t\t} else if (_owner->showVotes()) {\n\t\t\t\t_owner->_optionsPart->showAnswerVotesTooltip(option);\n\t\t\t}\n\t\t}));\n\t}\n\tresult->setProperty(kPollOptionProperty, option);\n\treturn result;\n}\n\nvoid Poll::Options::toggleMultiOption(const QByteArray &option) {\n\tconst auto i = ranges::find(\n\t\t_answers,\n\t\toption,\n\t\t&Answer::option);\n\tif (i != end(_answers)) {\n\t\tconst auto selected = i->selected;\n\t\ti->selected = !selected;\n\t\ti->selectedAnimation.start(\n\t\t\t[=] { _owner->repaint(); },\n\t\t\tselected ? 1. : 0.,\n\t\t\tselected ? 0. : 1.,\n\t\t\tst::defaultCheck.duration);\n\t\tif (selected) {\n\t\t\tconst auto j = ranges::find(\n\t\t\t\t_answers,\n\t\t\t\ttrue,\n\t\t\t\t&Answer::selected);\n\t\t\t_hasSelected = (j != end(_answers));\n\t\t} else {\n\t\t\t_hasSelected = true;\n\t\t}\n\t\t_owner->repaint();\n\t}\n}\n\nvoid Poll::Options::sendMultiOptions() {\n\tauto chosen = _answers | ranges::views::filter(\n\t\t&Answer::selected\n\t) | ranges::views::transform(\n\t\t&Answer::option\n\t) | ranges::to_vector;\n\tif (!chosen.empty()) {\n\t\t_votedFromHere = true;\n\t\t_owner->history()->session().api().polls().sendVotes(\n\t\t\t_owner->_parent->data()->fullId(),\n\t\t\tstd::move(chosen));\n\t}\n}\n\nvoid Poll::Options::showAnswerVotesTooltip(const QByteArray &option) {\n\tconst auto answer = _owner->_poll->answerByOption(option);\n\tif (!answer) {\n\t\treturn;\n\t}\n\tconst auto quiz = _owner->_poll->quiz();\n\tconst auto text = answer->votes\n\t\t? (quiz\n\t\t\t? tr::lng_polls_answers_count\n\t\t\t: tr::lng_polls_votes_count)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\tanswer->votes)\n\t\t: (quiz\n\t\t\t? tr::lng_polls_answers_none\n\t\t\t: tr::lng_polls_votes_none)(tr::now);\n\t_owner->_parent->delegate()->elementShowTooltip({ text }, [] {});\n}\n\nvoid Poll::Options::showResults() {\n\t_owner->_parent->delegate()->elementShowPollResults(\n\t\t_owner->_poll,\n\t\t_owner->_parent->data()->fullId());\n}\n\nvoid Poll::updateVotes() {\n\tconst auto voted = _poll->voted();\n\tif (_voted != voted) {\n\t\t_voted = voted;\n\t\tif (_voted) {\n\t\t\tfor (auto &answer : _optionsPart->_answers) {\n\t\t\t\tanswer.selected = false;\n\t\t\t}\n\t\t\tif (_optionsPart->_votedFromHere\n\t\t\t\t&& (_flags & PollData::Flag::HideResultsUntilClose)\n\t\t\t\t&& !(_flags & PollData::Flag::Closed)) {\n\t\t\t\tUi::Toast::Show({\n\t\t\t\t\t.text = tr::lng_polls_results_after_close(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\t.iconLottie = u\"toast_hide_results\"_q,\n\t\t\t\t\t.iconLottieSize = st::pollToastIconSize,\n\t\t\t\t\t.duration = crl::time(3000),\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\t_optionsPart->_votedFromHere = false;\n\t\t\t_optionsPart->_hasSelected = false;\n\t\t}\n\t}\n\t_optionsPart->updateAnswerVotes();\n\t_footerPart->updateTotalVotes();\n}\n\nvoid Poll::Options::checkSendingAnimation() const {\n\tconst auto &sending = _owner->_poll->sendingVotes;\n\tconst auto sendingRadial = (sending.size() == 1)\n\t\t&& !(_owner->_flags & PollData::Flag::MultiChoice);\n\tif (sendingRadial == (_sendingAnimation != nullptr)) {\n\t\tif (_sendingAnimation) {\n\t\t\t_sendingAnimation->option = sending.front();\n\t\t}\n\t\treturn;\n\t}\n\tif (!sendingRadial) {\n\t\tif (!_answersAnimation) {\n\t\t\t_sendingAnimation = nullptr;\n\t\t}\n\t\treturn;\n\t}\n\t_sendingAnimation = std::make_unique<SendingAnimation>(\n\t\tsending.front(),\n\t\t[=] { radialAnimationCallback(); });\n\t_sendingAnimation->animation.start();\n}\n\nvoid Poll::Options::updateAnswerVotesFromOriginal(\n\t\tAnswer &answer,\n\t\tconst PollAnswer &original,\n\t\tint percent,\n\t\tint maxVotes,\n\t\tbool showPercent) {\n\tif (!_owner->showVotes() || !showPercent) {\n\t\tanswer.votesPercent = 0;\n\t\tanswer.votesPercentString.clear();\n\t\tanswer.votesPercentWidth = 0;\n\t} else if (answer.votesPercentString.isEmpty()\n\t\t|| answer.votesPercent != percent) {\n\t\tanswer.votesPercent = percent;\n\t\tanswer.votesPercentString = QString::number(percent) + '%';\n\t\tanswer.votesPercentWidth = st::historyPollPercentFont->width(\n\t\t\tanswer.votesPercentString);\n\t}\n\tanswer.chosen = original.chosen;\n\tanswer.votes = original.votes;\n\tanswer.filling = percent / 100.;\n\tif (_owner->showVotes() && answer.votes) {\n\t\tanswer.votesCountString = Lang::FormatCountDecimal(answer.votes);\n\t\tanswer.votesCountWidth = st::normalFont->width(\n\t\t\tanswer.votesCountString);\n\t} else {\n\t\tanswer.votesCountString.clear();\n\t\tanswer.votesCountWidth = 0;\n\t}\n\tconst auto changed = !ranges::equal(\n\t\tanswer.recentVoters,\n\t\toriginal.recentVoters,\n\t\tranges::equal_to(),\n\t\t&UserpicInRow::peer);\n\tif (changed) {\n\t\tauto updated = std::vector<UserpicInRow>();\n\t\tupdated.reserve(original.recentVoters.size());\n\t\tfor (const auto &peer : original.recentVoters) {\n\t\t\tconst auto i = ranges::find(\n\t\t\t\tanswer.recentVoters,\n\t\t\t\tpeer,\n\t\t\t\t&UserpicInRow::peer);\n\t\t\tif (i != end(answer.recentVoters)) {\n\t\t\t\tupdated.push_back(std::move(*i));\n\t\t\t} else {\n\t\t\t\tupdated.push_back({ .peer = peer });\n\t\t\t}\n\t\t}\n\t\tanswer.recentVoters = std::move(updated);\n\t\tanswer.recentVotersImage = QImage();\n\t}\n}\n\nvoid Poll::Options::updateAnswerVotes() {\n\tif (_owner->_poll->answers.size() != _answers.size()\n\t\t|| _owner->_poll->answers.empty()) {\n\t\treturn;\n\t}\n\tconst auto totalVotes = _owner->_poll->totalVoters;\n\tconst auto showPercent = (totalVotes > 0)\n\t\t&& ranges::all_of(_owner->_poll->answers, [=](const PollAnswer &a) {\n\t\t\treturn a.votes <= totalVotes;\n\t\t});\n\tconst auto maxVotes = std::max(1, ranges::max_element(\n\t\t_owner->_poll->answers,\n\t\tranges::less(),\n\t\t&PollAnswer::votes)->votes);\n\n\tconstexpr auto kMaxCount = PollData::kMaxOptions;\n\tconst auto count = size_type(_owner->_poll->answers.size());\n\tAssert(count <= kMaxCount);\n\tint PercentsStorage[kMaxCount] = { 0 };\n\tint VotesStorage[kMaxCount] = { 0 };\n\n\tranges::copy(\n\t\tranges::views::all(\n\t\t\t_owner->_poll->answers\n\t\t) | ranges::views::transform(&PollAnswer::votes),\n\t\tranges::begin(VotesStorage));\n\n\tif (showPercent) {\n\t\tCountNicePercent(\n\t\t\tgsl::make_span(VotesStorage).subspan(0, count),\n\t\t\ttotalVotes,\n\t\t\tgsl::make_span(PercentsStorage).subspan(0, count));\n\t}\n\n\tfor (auto &answer : _answers) {\n\t\tconst auto i = ranges::find(\n\t\t\t_owner->_poll->answers,\n\t\t\tanswer.option,\n\t\t\t&PollAnswer::option);\n\t\tAssert(i != end(_owner->_poll->answers));\n\t\tconst auto index = int(i - begin(_owner->_poll->answers));\n\t\tupdateAnswerVotesFromOriginal(\n\t\t\tanswer,\n\t\t\t*i,\n\t\t\tPercentsStorage[index],\n\t\t\tmaxVotes,\n\t\t\tshowPercent);\n\t}\n}\n\nvoid Poll::draw(Painter &p, const PaintContext &context) const {\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;\n\tauto paintw = width();\n\n\tif (_poll->checkResultsReload(context.now)) {\n\t\thistory()->session().api().polls().reloadResults(_parent->data());\n\t}\n\n\tconst auto padding = st::msgPadding;\n\tpaintw -= padding.left() + padding.right();\n\n\tconst Part *parts[] = {\n\t\t_headerPart.get(),\n\t\t_optionsPart.get(),\n\t\t_addOptionPart.get(),\n\t\t_footerPart.get(),\n\t};\n\tauto tshift = 0;\n\tfor (const auto part : parts) {\n\t\tconst auto h = part->countHeight(paintw);\n\t\tif (h > 0) {\n\t\t\tconst auto saved = p.transform();\n\t\t\tp.translate(0, tshift);\n\t\t\tpart->draw(p, padding.left(), paintw, width(), context);\n\t\t\tp.setTransform(saved);\n\t\t}\n\t\ttshift += h;\n\t}\n}\n\nvoid Poll::Options::resetAnswersAnimation() const {\n\t_answersAnimation = nullptr;\n\tif (_owner->_poll->sendingVotes.size() != 1\n\t\t|| (_owner->_flags & PollData::Flag::MultiChoice)) {\n\t\t_sendingAnimation = nullptr;\n\t}\n}\n\nvoid Poll::Options::radialAnimationCallback() const {\n\tif (!anim::Disabled()) {\n\t\t_owner->repaint();\n\t}\n}\n\nvoid Poll::Header::paintRecentVoters(\n\t\tPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tconst PaintContext &context) const {\n\tconst auto count = int(_recentVoters.size());\n\tif (!count) {\n\t\treturn;\n\t}\n\tauto x = left\n\t\t+ st::historyPollRecentVotersSkip\n\t\t+ (count - 1) * st::historyPollRecentVoterSkip;\n\tauto y = top;\n\tconst auto size = st::historyPollRecentVoterSize;\n\tconst auto stm = context.messageStyle();\n\tauto pen = stm->msgBg->p;\n\tpen.setWidth(st::lineWidth);\n\n\tauto created = false;\n\tfor (const auto &recent : ranges::views::reverse(_recentVoters)) {\n\t\tconst auto was = !recent.userpic.null();\n\t\trecent.peer->paintUserpic(p, recent.userpic, x, y, size);\n\t\tif (!was && !recent.userpic.null()) {\n\t\t\tcreated = true;\n\t\t}\n\t\tconst auto paintContent = [&](QPainter &p) {\n\t\t\tp.setPen(pen);\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(x, y, size, size);\n\t\t};\n\t\tif (_owner->usesBubblePattern(context)) {\n\t\t\tconst auto add = st::lineWidth * 2;\n\t\t\tconst auto target = QRect(x, y, size, size).marginsAdded(\n\t\t\t\t{ add, add, add, add });\n\t\t\tUi::PaintPatternBubblePart(\n\t\t\t\tp,\n\t\t\t\tcontext.viewport,\n\t\t\t\tcontext.bubblesPattern->pixmap,\n\t\t\t\ttarget,\n\t\t\t\tpaintContent,\n\t\t\t\t_userpicCircleCache);\n\t\t} else {\n\t\t\tpaintContent(p);\n\t\t}\n\t\tx -= st::historyPollRecentVoterSkip;\n\t}\n\tif (created) {\n\t\t_owner->history()->owner().registerHeavyViewPart(_owner->_parent);\n\t}\n}\n\nvoid Poll::Header::paintShowSolution(\n\t\tPainter &p,\n\t\tint right,\n\t\tint top,\n\t\tconst PaintContext &context) const {\n\tconst auto shown = _solutionButtonAnimation.value(\n\t\t_solutionButtonVisible ? 1. : 0.);\n\tif (!shown) {\n\t\treturn;\n\t}\n\tif (!_showSolutionLink) {\n\t\t_showSolutionLink = std::make_shared<LambdaClickHandler>(\n\t\t\tcrl::guard(_owner, [=] { _owner->_headerPart->showSolution(); }));\n\t}\n\tconst auto stm = context.messageStyle();\n\tconst auto &icon = stm->historyQuizExplain;\n\tconst auto x = right - icon.width();\n\tconst auto y = top + (st::normalFont->height - icon.height()) / 2;\n\tif (shown == 1.) {\n\t\ticon.paint(p, x, y, _owner->width());\n\t} else {\n\t\tp.save();\n\t\tp.translate(x + icon.width() / 2, y + icon.height() / 2);\n\t\tp.scale(shown, shown);\n\t\tp.setOpacity(shown);\n\t\ticon.paint(p, -icon.width() / 2, -icon.height() / 2, _owner->width());\n\t\tp.restore();\n\t}\n}\n\nvoid Poll::Header::paintSolutionBlock(\n\t\tPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint paintw,\n\t\tconst PaintContext &context) const {\n\tif (!_solutionShown || !canShowSolution()) {\n\t\treturn;\n\t}\n\tif (!_closeSolutionLink) {\n\t\t_closeSolutionLink = std::make_shared<LambdaClickHandler>(\n\t\t\tcrl::guard(\n\t\t\t\t_owner,\n\t\t\t\t[=] { _owner->_headerPart->solutionToggled(false); }));\n\t}\n\n\tconst auto &qst = st::historyPagePreview;\n\tconst auto blockHeight = countSolutionBlockHeight(paintw);\n\tconst auto outer = QRect(left, top, paintw, blockHeight);\n\n\tconst auto stm = context.messageStyle();\n\tconst auto view = _owner->_parent;\n\tconst auto selected = context.selected();\n\tconst auto colorIndex = view->contentColorIndex();\n\tconst auto &chatSt = *context.st;\n\tconst auto colorPattern = chatSt.colorPatternIndex(colorIndex);\n\tconst auto useColorIndex = !context.outbg;\n\tconst auto cache = useColorIndex\n\t\t? chatSt.coloredReplyCache(selected, colorIndex).get()\n\t\t: stm->replyCache[colorPattern].get();\n\n\tUi::Text::ValidateQuotePaintCache(*cache, qst);\n\tUi::Text::FillQuotePaint(p, outer, *cache, qst);\n\n\tconst auto innerLeft = left + qst.padding.left();\n\tconst auto innerRight = left + paintw - qst.padding.right();\n\tconst auto textWidth = innerRight - innerLeft;\n\tauto yshift = top + qst.padding.top();\n\n\tp.setPen(cache->outlines[0]);\n\tp.setFont(st::semiboldFont);\n\tconst auto closeArea = st::historyPollExplanationCloseSize;\n\tp.drawTextLeft(\n\t\tinnerLeft,\n\t\tyshift,\n\t\t_owner->width(),\n\t\ttr::lng_polls_solution_title(tr::now),\n\t\ttextWidth - closeArea);\n\n\t{\n\t\tconst auto iconSize = st::historyPollExplanationCloseIconSize;\n\t\tconst auto centerX = innerRight - closeArea / 2;\n\t\tconst auto centerY = yshift + st::semiboldFont->height / 2;\n\t\tconst auto half = iconSize / 2;\n\t\tauto pen = QPen(cache->outlines[0]);\n\t\tpen.setWidthF(st::historyPollExplanationCloseStroke * 1.);\n\t\tpen.setCapStyle(Qt::RoundCap);\n\t\tp.setPen(pen);\n\t\tp.setRenderHint(QPainter::Antialiasing);\n\t\tp.drawLine(\n\t\t\tcenterX - half, centerY - half,\n\t\t\tcenterX + half, centerY + half);\n\t\tp.drawLine(\n\t\t\tcenterX + half, centerY - half,\n\t\t\tcenterX - half, centerY + half);\n\t\tp.setRenderHint(QPainter::Antialiasing, false);\n\t}\n\n\tyshift += st::semiboldFont->height + st::historyPollExplanationTitleSkip;\n\n\tp.setPen(stm->historyTextFg);\n\t_owner->_parent->prepareCustomEmojiPaint(p, context, _solutionText);\n\t_solutionText.draw(p, {\n\t\t.position = { innerLeft, yshift },\n\t\t.outerWidth = _owner->width(),\n\t\t.availableWidth = textWidth,\n\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t.now = context.now,\n\t\t.pausedEmoji = context.paused,\n\t\t.pausedSpoiler = context.paused,\n\t\t.selection = toSolutionSelection(context.selection),\n\t});\n\n\tif (countSolutionMediaHeight(textWidth)) {\n\t\tyshift += _solutionText.countHeight(textWidth)\n\t\t\t+ st::historyPollExplanationMediaSkip;\n\t\tconst auto isDocument = _solutionMedia\n\t\t\t&& (_solutionMedia->kind == PollThumbnailKind::Document\n\t\t\t\t|| _solutionMedia->kind == PollThumbnailKind::Audio);\n\t\tconst auto isThumbed = isDocument\n\t\t\t&& _owner->_poll->solutionMedia.document\n\t\t\t&& _owner->_poll->solutionMedia.document->hasThumbnail()\n\t\t\t&& !_owner->_poll->solutionMedia.document->isSong();\n\t\tconst auto &fileSt = isThumbed\n\t\t\t? st::msgFileThumbLayout\n\t\t\t: st::msgFileLayout;\n\t\tconst auto shift = isDocument ? fileSt.padding.left() : 0;\n\t\tconst auto attachLeft = rtl()\n\t\t\t? (_owner->width() - innerLeft + shift - _solutionAttach->width())\n\t\t\t: (innerLeft - shift);\n\t\tp.translate(attachLeft, yshift);\n\t\t_solutionAttach->draw(\n\t\t\tp,\n\t\t\tcontext.translated(-attachLeft, -yshift)\n\t\t\t\t.withSelection(TextSelection()));\n\t\tp.translate(-attachLeft, -yshift);\n\t}\n}\n\nint Poll::Options::paintAnswer(\n\t\tPainter &p,\n\t\tconst Answer &answer,\n\t\tconst AnswerAnimation *animation,\n\t\tint left,\n\t\tint top,\n\t\tint width,\n\t\tint outerWidth,\n\t\tconst PaintContext &context) const {\n\tconst auto height = countAnswerHeight(answer, width);\n\tif (!context.highlight.pollOption.isEmpty()\n\t\t&& context.highlight.pollOption == answer.option\n\t\t&& context.highlight.collapsion > 0.) {\n\t\tconst auto hlTextWidth = countAnswerContentWidth(answer, width);\n\t\tconst auto hlTextHeight = answer.text.countHeight(hlTextWidth);\n\t\tconst auto hlMultiline = (hlTextHeight\n\t\t\t> st::historyPollPercentFont->height);\n\t\tconst auto hlVotesExtra = countVotesExtraHeight(\n\t\t\tanswer,\n\t\t\thlTextWidth);\n\t\tconst auto fillingExtra = (_owner->showVotes()\n\t\t\t&& !answer.thumbnail\n\t\t\t&& !hlMultiline\n\t\t\t&& !hlVotesExtra)\n\t\t\t? (st::historyPollChoiceRight.height() / 2)\n\t\t\t: 0;\n\t\tconst auto absoluteTop = top\n\t\t\t+ _owner->_headerPart->countHeight(width);\n\t\tconst auto to = context.highlightInterpolateTo;\n\t\tconst auto toProgress = (1. - context.highlight.collapsion);\n\t\tif (toProgress >= 1.) {\n\t\t\tcontext.highlightPathCache->addRect(to);\n\t\t} else if (toProgress <= 0.) {\n\t\t\tcontext.highlightPathCache->addRect(\n\t\t\t\t0,\n\t\t\t\tabsoluteTop + fillingExtra,\n\t\t\t\t_owner->width(),\n\t\t\t\theight + fillingExtra);\n\t\t} else {\n\t\t\tconst auto lerp = [=](int from, int to) {\n\t\t\t\treturn from + (to - from) * toProgress;\n\t\t\t};\n\t\t\tcontext.highlightPathCache->addRect(\n\t\t\t\tlerp(0, to.x()),\n\t\t\t\tlerp(absoluteTop, to.y()) + fillingExtra,\n\t\t\t\tlerp(_owner->width(), to.width()),\n\t\t\t\tlerp(height + fillingExtra, to.height()));\n\t\t}\n\t}\n\tconst auto stm = context.messageStyle();\n\tconst auto &answerPadding = answer.thumbnail\n\t\t? st::historyPollAnswerPadding\n\t\t: st::historyPollAnswerPaddingNoMedia;\n\tconst auto aleft = left + st::historyPollAnswerPadding.left();\n\tconst auto awidth = width\n\t\t- st::historyPollAnswerPadding.left()\n\t\t- st::historyPollAnswerPadding.right();\n\tconst auto media = answer.thumbnail ? PollAnswerMediaSize() : 0;\n\tconst auto textWidth = countAnswerContentWidth(answer, width);\n\tconst auto textContentHeight = answer.text.countHeight(textWidth);\n\tconst auto multilineAnswer = (textContentHeight\n\t\t> st::historyPollPercentFont->height);\n\tconst auto votesExtraHeight = countVotesExtraHeight(\n\t\tanswer,\n\t\ttextWidth);\n\tconst auto fillingContentHeight = (multilineAnswer || votesExtraHeight)\n\t\t? (textContentHeight + votesExtraHeight)\n\t\t: textContentHeight;\n\tconst auto anyMediaWidth = _anyAnswerHasMedia\n\t\t? (PollAnswerMediaSize() + PollAnswerMediaSkip())\n\t\t: 0;\n\tconst auto barContentWidth = std::max(1, awidth - anyMediaWidth);\n\n\tif (answer.ripple) {\n\t\tp.setOpacity(st::historyPollRippleOpacity);\n\t\tanswer.ripple->paint(\n\t\t\tp,\n\t\t\tleft - st::msgPadding.left(),\n\t\t\ttop,\n\t\t\touterWidth,\n\t\t\t&stm->msgWaveformInactive->c);\n\t\tif (answer.ripple->empty()) {\n\t\t\tanswer.ripple.reset();\n\t\t}\n\t\tp.setOpacity(1.);\n\t}\n\n\tconst auto paintVotesCount = [&](float64 opacity = 1.) {\n\t\tif (answer.votesCountString.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tif (!answer.recentVoters.empty()) {\n\t\t\tif (NeedRegenerateUserpics(\n\t\t\t\t\tanswer.recentVotersImage,\n\t\t\t\t\tanswer.recentVoters)) {\n\t\t\t\tconst auto was = !answer.recentVotersImage.isNull();\n\t\t\t\tGenerateUserpicsInRow(\n\t\t\t\t\tanswer.recentVotersImage,\n\t\t\t\t\tanswer.recentVoters,\n\t\t\t\t\tst::historyPollAnswerUserpics);\n\t\t\t\tif (!was) {\n\t\t\t\t\t_owner->history()->owner().registerHeavyViewPart(\n\t\t\t\t\t\t_owner->_parent);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst auto userpicsWidth = answer.recentVotersImage.isNull()\n\t\t\t? 0\n\t\t\t: (answer.recentVotersImage.width()\n\t\t\t\t/ style::DevicePixelRatio());\n\t\tconst auto userpicsExtra = userpicsWidth\n\t\t\t? (st::historyPollAnswerUserpicsSkip + userpicsWidth)\n\t\t\t: 0;\n\t\tconst auto rightEdge = aleft + awidth\n\t\t\t- anyMediaWidth\n\t\t\t- st::historyPollFillingRight;\n\t\tconst auto countX = rightEdge\n\t\t\t- answer.votesCountWidth\n\t\t\t- userpicsExtra;\n\t\tconst auto atop = top + answerPadding.top()\n\t\t\t+ ((multilineAnswer || votesExtraHeight)\n\t\t\t\t? (textContentHeight\n\t\t\t\t\t- (votesExtraHeight ? 0 : st::normalFont->height))\n\t\t\t\t: 0);\n\t\tp.setOpacity(opacity);\n\t\tp.setFont(st::normalFont);\n\t\tp.setPen(stm->msgDateFg);\n\t\tp.drawTextLeft(\n\t\t\tcountX,\n\t\t\tatop,\n\t\t\touterWidth,\n\t\t\tanswer.votesCountString,\n\t\t\tanswer.votesCountWidth);\n\t\tif (userpicsWidth) {\n\t\t\tconst auto userpicsX = rightEdge - userpicsWidth;\n\t\t\tconst auto userpicsY = atop\n\t\t\t\t+ (st::normalFont->height\n\t\t\t\t\t- st::historyPollAnswerUserpics.size) / 2;\n\t\t\tp.drawImage(userpicsX, userpicsY, answer.recentVotersImage);\n\t\t}\n\t};\n\n\tif (animation) {\n\t\tconst auto opacity = animation->opacity.current();\n\t\tif (opacity < 1.) {\n\t\t\tp.setOpacity(1. - opacity);\n\t\t\tpaintRadio(p, answer, left, top, context);\n\t\t}\n\t\tif (opacity > 0.) {\n\t\t\tconst auto percent = QString::number(\n\t\t\t\tint(base::SafeRound(animation->percent.current()))) + '%';\n\t\t\tconst auto percentWidth = st::historyPollPercentFont->width(\n\t\t\t\tpercent);\n\t\t\tp.setOpacity(opacity);\n\t\t\tpaintPercent(\n\t\t\t\tp,\n\t\t\t\tpercent,\n\t\t\t\tpercentWidth,\n\t\t\t\tleft,\n\t\t\t\ttop,\n\t\t\t\tanswerPadding.top(),\n\t\t\t\touterWidth,\n\t\t\t\tcontext);\n\t\t\tpaintVotesCount(opacity);\n\t\t\tp.setOpacity(sqrt(opacity));\n\t\t\tpaintFilling(\n\t\t\t\tp,\n\t\t\t\tanimation->chosen,\n\t\t\t\tanimation->correct,\n\t\t\t\tanimation->filling.current(),\n\t\t\t\tleft,\n\t\t\t\ttop,\n\t\t\t\tanswerPadding.top(),\n\t\t\t\twidth,\n\t\t\t\tbarContentWidth,\n\t\t\t\tfillingContentHeight,\n\t\t\t\tcontext);\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t} else if (!_owner->showVotes()) {\n\t\tpaintRadio(p, answer, left, top, context);\n\t} else {\n\t\tpaintPercent(\n\t\t\tp,\n\t\t\tanswer.votesPercentString,\n\t\t\tanswer.votesPercentWidth,\n\t\t\tleft,\n\t\t\ttop,\n\t\t\tanswerPadding.top(),\n\t\t\touterWidth,\n\t\t\tcontext);\n\t\tpaintVotesCount();\n\t\tpaintFilling(\n\t\t\tp,\n\t\t\tanswer.chosen,\n\t\t\tanswer.correct,\n\t\t\tanswer.filling,\n\t\t\tleft,\n\t\t\ttop,\n\t\t\tanswerPadding.top(),\n\t\t\twidth,\n\t\t\tbarContentWidth,\n\t\t\tfillingContentHeight,\n\t\t\tcontext);\n\t}\n\n\ttop += answerPadding.top();\n\tif (answer.thumbnail) {\n\t\tconst auto target = QRect(\n\t\t\taleft + awidth - media,\n\t\t\ttop,\n\t\t\tmedia,\n\t\t\tmedia);\n\t\tif (!target.isEmpty()) {\n\t\t\tconst auto image = answer.thumbnail->image(media);\n\t\t\tif (!image.isNull()) {\n\t\t\t\tconst auto source = QRectF(QPointF(), QSizeF(image.size()));\n\t\t\t\tconst auto kx = target.width() / source.width();\n\t\t\t\tconst auto ky = target.height() / source.height();\n\t\t\t\tconst auto scale = std::max(kx, ky);\n\t\t\t\tconst auto size = QSizeF(\n\t\t\t\t\tsource.width() * scale,\n\t\t\t\t\tsource.height() * scale);\n\t\t\t\tconst auto geometry = QRectF(\n\t\t\t\t\ttarget.x() + (target.width() - size.width()) / 2.,\n\t\t\t\t\ttarget.y() + (target.height() - size.height()) / 2.,\n\t\t\t\t\tsize.width(),\n\t\t\t\t\tsize.height());\n\t\t\t\tp.save();\n\t\t\t\tif (answer.thumbnailRounded) {\n\t\t\t\t\tauto path = QPainterPath();\n\t\t\t\t\tpath.addRoundedRect(\n\t\t\t\t\t\ttarget,\n\t\t\t\t\t\tst::roundRadiusSmall,\n\t\t\t\t\t\tst::roundRadiusSmall);\n\t\t\t\t\tp.setClipPath(path);\n\t\t\t\t}\n\t\t\t\tp.drawImage(geometry, image, source);\n\t\t\t\tp.restore();\n\t\t\t\tif (answer.thumbnailIsVideo) {\n\t\t\t\t\tst::dialogsMiniPlay.paintInCenter(p, target);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tp.setPen(stm->historyTextFg);\n\tanswer.text.draw(p, {\n\t\t.position = { aleft, top },\n\t\t.outerWidth = outerWidth,\n\t\t.availableWidth = textWidth,\n\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t.now = context.now,\n\t\t.pausedEmoji = context.paused,\n\t\t.pausedSpoiler = context.paused,\n\t});\n\n\treturn height;\n}\n\nvoid Poll::Options::paintRadio(\n\t\tPainter &p,\n\t\tconst Answer &answer,\n\t\tint left,\n\t\tint top,\n\t\tconst PaintContext &context) const {\n\tconst auto &answerPadding = answer.thumbnail\n\t\t? st::historyPollAnswerPadding\n\t\t: st::historyPollAnswerPaddingNoMedia;\n\ttop += answerPadding.top();\n\n\tconst auto stm = context.messageStyle();\n\n\tPainterHighQualityEnabler hq(p);\n\tconst auto &radio = st::historyPollRadio;\n\tconst auto over = ClickHandler::showAsActive(answer.handler);\n\tconst auto &regular = stm->msgDateFg;\n\n\tconst auto chosen = answer.chosen;\n\tconst auto checkmark = chosen\n\t\t? 1.\n\t\t: answer.selectedAnimation.value(answer.selected ? 1. : 0.);\n\n\tconst auto o = p.opacity();\n\tif (checkmark < 1.) {\n\t\tp.setBrush(Qt::NoBrush);\n\t\tp.setOpacity(o\n\t\t\t* (over\n\t\t\t\t? st::historyPollRadioOpacityOver\n\t\t\t\t: st::historyPollRadioOpacity));\n\t}\n\n\tconst auto multiChoice = (_owner->_flags & PollData::Flag::MultiChoice);\n\tconst auto rect = QRectF(left, top, radio.diameter, radio.diameter)\n\t\t- Margins(radio.thickness / 2.);\n\tconst auto radius = st::historyPollCheckboxRadius;\n\tif (_sendingAnimation && _sendingAnimation->option == answer.option) {\n\t\tconst auto &active = stm->msgServiceFg;\n\t\tif (anim::Disabled()) {\n\t\t\tanim::DrawStaticLoading(p, rect, radio.thickness, active);\n\t\t} else {\n\t\t\tconst auto state = _sendingAnimation->animation.computeState();\n\t\t\tauto pen = anim::pen(regular, active, state.shown);\n\t\t\tpen.setWidth(radio.thickness);\n\t\t\tpen.setCapStyle(Qt::RoundCap);\n\t\t\tp.setPen(pen);\n\t\t\tif (multiChoice) {\n\t\t\t\tp.drawRoundedRect(rect, radius, radius);\n\t\t\t} else {\n\t\t\t\tp.drawArc(\n\t\t\t\t\trect,\n\t\t\t\t\tstate.arcFrom,\n\t\t\t\t\tstate.arcLength);\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (checkmark < 1.) {\n\t\t\tauto pen = regular->p;\n\t\t\tpen.setWidth(radio.thickness);\n\t\t\tp.setPen(pen);\n\t\t\tif (multiChoice) {\n\t\t\t\tp.drawRoundedRect(rect, radius, radius);\n\t\t\t} else {\n\t\t\t\tp.drawEllipse(rect);\n\t\t\t}\n\t\t}\n\t\tif (checkmark > 0.) {\n\t\t\tconst auto removeFull = (radio.diameter / 2 - radio.thickness);\n\t\t\tconst auto removeNow = removeFull * (1. - checkmark);\n\t\t\tconst auto color = stm->msgFileThumbLinkFg;\n\t\t\tauto pen = color->p;\n\t\t\tpen.setWidth(radio.thickness);\n\t\t\tp.setPen(pen);\n\t\t\tp.setBrush(color);\n\t\t\tconst auto inner = rect - Margins(removeNow);\n\t\t\tif (multiChoice) {\n\t\t\t\tp.drawRoundedRect(inner, radius, radius);\n\t\t\t} else {\n\t\t\t\tp.drawEllipse(inner);\n\t\t\t}\n\t\t\tconst auto &icon = stm->historyPollChosen;\n\t\t\ticon.paint(\n\t\t\t\tp,\n\t\t\t\tleft + (radio.diameter - icon.width()) / 2,\n\t\t\t\ttop + (radio.diameter - icon.height()) / 2,\n\t\t\t\t_owner->width());\n\t\t}\n\t}\n\n\tp.setOpacity(o);\n}\n\nvoid Poll::Options::paintPercent(\n\t\tPainter &p,\n\t\tconst QString &percent,\n\t\tint percentWidth,\n\t\tint left,\n\t\tint top,\n\t\tint topPadding,\n\t\tint outerWidth,\n\t\tconst PaintContext &context) const {\n\tconst auto stm = context.messageStyle();\n\tconst auto aleft = left + st::historyPollAnswerPadding.left();\n\n\ttop += topPadding;\n\n\tp.setFont(st::historyPollPercentFont);\n\tp.setPen(stm->historyTextFg);\n\tconst auto pleft = aleft - percentWidth - st::historyPollPercentSkip;\n\tp.drawTextLeft(\n\t\tpleft,\n\t\ttop + st::historyPollPercentTop,\n\t\touterWidth,\n\t\tpercent,\n\t\tpercentWidth);\n}\n\nvoid Poll::Options::paintFilling(\n\t\tPainter &p,\n\t\tbool chosen,\n\t\tbool correct,\n\t\tfloat64 filling,\n\t\tint left,\n\t\tint top,\n\t\tint topPadding,\n\t\tint width,\n\t\tint contentWidth,\n\t\tint contentHeight,\n\t\tconst PaintContext &context) const {\n\tconst auto st = context.st;\n\tconst auto stm = context.messageStyle();\n\tconst auto aleft = left + st::historyPollAnswerPadding.left();\n\n\ttop += topPadding;\n\n\tconst auto thickness = st::historyPollFillingHeight;\n\tconst auto max = contentWidth - st::historyPollFillingRight;\n\tconst auto size\n\t\t= anim::interpolate(st::historyPollFillingMin, max, filling);\n\tconst auto radius = st::historyPollFillingRadius;\n\tconst auto ftop = top\n\t\t+ std::max(st::historyPollPercentFont->height, contentHeight)\n\t\t+ st::historyPollFillingTop;\n\n\tenum class Style {\n\t\tIncorrect,\n\t\tCorrect,\n\t\tDefault,\n\t};\n\tconst auto style = [&] {\n\t\tif (chosen && !correct) {\n\t\t\treturn Style::Incorrect;\n\t\t} else if (chosen\n\t\t\t&& correct\n\t\t\t&& _owner->_poll->quiz()\n\t\t\t&& !context.outbg) {\n\t\t\treturn Style::Correct;\n\t\t} else {\n\t\t\treturn Style::Default;\n\t\t}\n\t}();\n\tauto barleft = aleft;\n\tauto barwidth = size;\n\tconst auto &color = (style == Style::Incorrect)\n\t\t? st->boxTextFgError()\n\t\t: (style == Style::Correct)\n\t\t? st->boxTextFgGood()\n\t\t: stm->msgFileBg;\n\tp.setPen(Qt::NoPen);\n\tPainterHighQualityEnabler hq(p);\n\t{\n\t\tp.setBrush(anim::with_alpha(color->c, st::historyPollFillingBgOpacity));\n\t\tp.drawRoundedRect(barleft, ftop, max, thickness, radius, radius);\n\t}\n\tp.setBrush(color);\n\tif (chosen || correct) {\n\t\tconst auto &icon = (style == Style::Incorrect)\n\t\t\t? st->historyPollChoiceWrong()\n\t\t\t: (style == Style::Correct)\n\t\t\t? st->historyPollChoiceRight()\n\t\t\t: stm->historyPollChoiceRight;\n\t\tconst auto cleft = aleft - st::historyPollPercentSkip - icon.width();\n\t\tconst auto ctop = ftop - (icon.height() - thickness) / 2;\n\t\tif (_owner->_flags & PollData::Flag::MultiChoice) {\n\t\t\tp.drawRoundedRect(\n\t\t\t\tcleft,\n\t\t\t\tctop,\n\t\t\t\ticon.width(),\n\t\t\t\ticon.height(),\n\t\t\t\tst::historyPollCheckboxRadius,\n\t\t\t\tst::historyPollCheckboxRadius);\n\t\t} else {\n\t\t\tp.drawEllipse(cleft, ctop, icon.width(), icon.height());\n\t\t}\n\n\t\tconst auto paintContent = [&](QPainter &p) {\n\t\t\ticon.paint(p, cleft, ctop, width);\n\t\t};\n\t\tif (style == Style::Default && _owner->usesBubblePattern(context)) {\n\t\t\tconst auto add = st::lineWidth * 2;\n\t\t\tconst auto target = QRect(\n\t\t\t\tcleft,\n\t\t\t\tctop,\n\t\t\t\ticon.width(),\n\t\t\t\ticon.height()\n\t\t\t).marginsAdded({ add, add, add, add });\n\t\t\tUi::PaintPatternBubblePart(\n\t\t\t\tp,\n\t\t\t\tcontext.viewport,\n\t\t\t\tcontext.bubblesPattern->pixmap,\n\t\t\t\ttarget,\n\t\t\t\tpaintContent,\n\t\t\t\t_fillingIconCache);\n\t\t} else {\n\t\t\tpaintContent(p);\n\t\t}\n\t\t//barleft += icon.width() - radius;\n\t\t//barwidth -= icon.width() - radius;\n\t}\n\tif (barwidth > 0) {\n\t\tp.drawRoundedRect(barleft, ftop, barwidth, thickness, radius, radius);\n\t}\n}\n\nbool Poll::Options::answerVotesChanged() const {\n\tif (_owner->_poll->answers.size() != _answers.size()\n\t\t|| _owner->_poll->answers.empty()) {\n\t\treturn false;\n\t}\n\tfor (const auto &answer : _answers) {\n\t\tconst auto i = ranges::find(\n\t\t\t_owner->_poll->answers,\n\t\t\tanswer.option,\n\t\t\t&PollAnswer::option);\n\t\tif (i == end(_owner->_poll->answers)) {\n\t\t\treturn false;\n\t\t} else if (answer.votes != i->votes) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Poll::Options::saveStateInAnimation() const {\n\tif (_answersAnimation) {\n\t\treturn;\n\t}\n\tconst auto show = _owner->showVotes();\n\t_answersAnimation = std::make_unique<AnswersAnimation>();\n\t_answersAnimation->data.reserve(_answers.size());\n\tconst auto convert = [&](const Answer &answer) {\n\t\tauto result = AnswerAnimation();\n\t\tresult.percent = show ? float64(answer.votesPercent) : 0.;\n\t\tresult.filling = show ? answer.filling : 0.;\n\t\tresult.opacity = show ? 1. : 0.;\n\t\tresult.chosen = answer.chosen;\n\t\tresult.correct = answer.correct;\n\t\treturn result;\n\t};\n\tranges::transform(\n\t\t_answers,\n\t\tranges::back_inserter(_answersAnimation->data),\n\t\tconvert);\n}\n\nbool Poll::Options::checkAnimationStart() const {\n\tif (_owner->_poll->answers.size() != _answers.size()) {\n\t\t// Skip initial changes.\n\t\treturn false;\n\t}\n\tconst auto result = _owner->showVotes()\n\t\t!= (_owner->_poll->voted() || _owner->_poll->closed())\n\t\t|| answerVotesChanged();\n\tif (result) {\n\t\tsaveStateInAnimation();\n\t}\n\treturn result;\n}\n\nvoid Poll::Options::startAnswersAnimation() const {\n\tif (!_answersAnimation) {\n\t\treturn;\n\t}\n\n\tconst auto show = _owner->showVotes();\n\tauto &&both = ranges::views::zip(_answers, _answersAnimation->data);\n\tfor (auto &&[answer, data] : both) {\n\t\tdata.percent.start(show ? float64(answer.votesPercent) : 0.);\n\t\tdata.filling.start(show ? answer.filling : 0.);\n\t\tdata.opacity.start(show ? 1. : 0.);\n\t\tdata.chosen = data.chosen || answer.chosen;\n\t\tdata.correct = data.correct || answer.correct;\n\t}\n\t_answersAnimation->progress.start(\n\t\t[=] { _owner->repaint(); },\n\t\t0.,\n\t\t1.,\n\t\tst::historyPollDuration);\n}\n\nTextSelection Poll::adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const {\n\tconst auto descLen = _headerPart->_description.length();\n\tconst auto solLen = _headerPart->solutionSelectionLength();\n\tconst auto descSolLen = uint16(descLen + solLen);\n\n\tif (descLen == 0 && solLen == 0) {\n\t\treturn _headerPart->_question.adjustSelection(selection, type);\n\t}\n\tif (selection.to <= descLen && descLen > 0) {\n\t\treturn _headerPart->_description.adjustSelection(selection, type);\n\t}\n\tif (solLen > 0\n\t\t&& selection.from >= descLen\n\t\t&& selection.to <= descSolLen) {\n\t\tconst auto adjusted = _headerPart->_solutionText.adjustSelection(\n\t\t\t_headerPart->toSolutionSelection(selection),\n\t\t\ttype);\n\t\treturn _headerPart->fromSolutionSelection(adjusted);\n\t}\n\tconst auto questionSelection = _headerPart->_question.adjustSelection(\n\t\t_headerPart->toQuestionSelection(selection),\n\t\ttype);\n\tif (selection.from >= descSolLen) {\n\t\treturn _headerPart->fromQuestionSelection(questionSelection);\n\t}\n\tconst auto from = (selection.from < descLen && descLen > 0)\n\t\t? _headerPart->_description.adjustSelection(selection, type).from\n\t\t: (solLen > 0 && selection.from < descSolLen)\n\t\t? _headerPart->fromSolutionSelection(\n\t\t\t_headerPart->_solutionText.adjustSelection(\n\t\t\t\t_headerPart->toSolutionSelection(selection),\n\t\t\t\ttype)).from\n\t\t: _headerPart->fromQuestionSelection(questionSelection).from;\n\tconst auto to = (selection.to <= descSolLen && solLen > 0)\n\t\t? _headerPart->fromSolutionSelection(\n\t\t\t_headerPart->_solutionText.adjustSelection(\n\t\t\t\t_headerPart->toSolutionSelection(selection),\n\t\t\t\ttype)).to\n\t\t: _headerPart->fromQuestionSelection(questionSelection).to;\n\treturn { from, to };\n}\n\nuint16 Poll::fullSelectionLength() const {\n\treturn _headerPart->_description.length()\n\t\t+ _headerPart->solutionSelectionLength()\n\t\t+ _headerPart->_question.length();\n}\n\nTextForMimeData Poll::selectedText(TextSelection selection) const {\n\tauto description = _headerPart->_description.toTextForMimeData(selection);\n\tauto solution = _headerPart->_solutionText.toTextForMimeData(\n\t\t_headerPart->toSolutionSelection(selection));\n\tauto question = _headerPart->_question.toTextForMimeData(\n\t\t_headerPart->toQuestionSelection(selection));\n\tauto result = TextForMimeData();\n\tconst auto append = [&](TextForMimeData &&part) {\n\t\tif (part.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tif (!result.empty()) {\n\t\t\tresult.append('\\n');\n\t\t}\n\t\tresult.append(std::move(part));\n\t};\n\tappend(std::move(description));\n\tappend(std::move(solution));\n\tappend(std::move(question));\n\treturn result;\n}\n\nTextState Poll::textState(QPoint point, StateRequest request) const {\n\tauto result = TextState(_parent);\n\tif (!_poll->sendingVotes.empty()) {\n\t\treturn result;\n\t}\n\n\tconst auto padding = st::msgPadding;\n\tauto paintw = width() - padding.left() - padding.right();\n\n\tconst Part *parts[] = {\n\t\t_headerPart.get(),\n\t\t_optionsPart.get(),\n\t\t_addOptionPart.get(),\n\t\t_footerPart.get(),\n\t};\n\tauto tshift = 0;\n\tauto symbolAdd = uint16(0);\n\tfor (const auto part : parts) {\n\t\tconst auto h = part->countHeight(paintw);\n\t\tif (h > 0) {\n\t\t\tauto partResult = part->textState(\n\t\t\t\tpoint - QPoint(0, tshift),\n\t\t\t\tpadding.left(),\n\t\t\t\tpaintw,\n\t\t\t\twidth(),\n\t\t\t\trequest);\n\t\t\tif (partResult.link) {\n\t\t\t\tpartResult.itemId = result.itemId;\n\t\t\t\tpartResult.symbol += symbolAdd;\n\t\t\t\treturn partResult;\n\t\t\t}\n\t\t\tif (point.y() >= tshift && point.y() < tshift + h) {\n\t\t\t\tpartResult.itemId = result.itemId;\n\t\t\t\tpartResult.symbol += symbolAdd;\n\t\t\t\treturn partResult;\n\t\t\t}\n\t\t}\n\t\tsymbolAdd += part->selectionLength();\n\t\ttshift += h;\n\t}\n\treturn result;\n}\n\nvoid Poll::parentTextUpdated() {\n\t_headerPart->updateDescription();\n\thistory()->owner().requestViewResize(_parent);\n}\n\nauto Poll::bubbleRoll() const -> BubbleRoll {\n\tconst auto value = _wrongAnswerAnimation.value(1.);\n\t_wrongAnswerAnimated = (value < 1.);\n\tif (!_wrongAnswerAnimated) {\n\t\treturn BubbleRoll();\n\t}\n\tconst auto progress = [](float64 full) {\n\t\tconst auto lower = std::floor(full);\n\t\tconst auto shift = (full - lower);\n\t\tswitch (int(lower) % 4) {\n\t\tcase 0: return -shift;\n\t\tcase 1: return (shift - 1.);\n\t\tcase 2: return shift;\n\t\tcase 3: return (1. - shift);\n\t\t}\n\t\tUnexpected(\"Value in Poll::getBubbleRollDegrees.\");\n\t};\n\treturn {\n\t\t.rotate = progress(value * kRotateSegments) * kRotateAmplitude,\n\t\t.scale = 1. + progress(value * kScaleSegments) * kScaleAmplitude\n\t};\n}\n\nQMargins Poll::bubbleRollRepaintMargins() const {\n\tif (!_wrongAnswerAnimated) {\n\t\treturn QMargins();\n\t}\n\tstatic const auto kAdd = int(std::ceil(\n\t\tst::msgMaxWidth * std::sin(kRotateAmplitude * M_PI / 180.)));\n\treturn QMargins(kAdd, kAdd, kAdd, kAdd);\n}\n\nvoid Poll::paintBubbleFireworks(\n\t\tPainter &p,\n\t\tconst QRect &bubble,\n\t\tcrl::time ms) const {\n\tif (!_fireworksAnimation || _fireworksAnimation->paint(p, bubble)) {\n\t\treturn;\n\t}\n\t_fireworksAnimation = nullptr;\n}\n\nvoid Poll::clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool active) {\n\t_headerPart->clickHandlerActiveChanged(handler, active);\n}\n\nvoid Poll::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) {\n\tif (!handler) return;\n\n\t_headerPart->clickHandlerPressedChanged(handler, pressed);\n\t_optionsPart->clickHandlerPressedChanged(handler, pressed);\n\t_addOptionPart->clickHandlerPressedChanged(handler, pressed);\n\t_footerPart->clickHandlerPressedChanged(handler, pressed);\n}\n\nvoid Poll::hideSpoilers() {\n\tif (_headerPart->_description.hasSpoilers()) {\n\t\t_headerPart->_description.setSpoilerRevealed(\n\t\t\tfalse,\n\t\t\tanim::type::instant);\n\t}\n\tif (_headerPart->_solutionText.hasSpoilers()) {\n\t\t_headerPart->_solutionText.setSpoilerRevealed(\n\t\t\tfalse,\n\t\t\tanim::type::instant);\n\t}\n}\n\nvoid Poll::unloadHeavyPart() {\n\t_headerPart->unloadHeavyPart();\n\t_optionsPart->unloadHeavyPart();\n}\n\nbool Poll::hasHeavyPart() const {\n\treturn _headerPart->hasHeavyPart()\n\t\t|| _optionsPart->hasHeavyPart();\n}\n\nvoid Poll::Options::toggleRipple(Answer &answer, bool pressed) {\n\tif (pressed) {\n\t\tconst auto outerWidth = _owner->width();\n\t\tconst auto innerWidth = outerWidth\n\t\t\t- st::msgPadding.left()\n\t\t\t- st::msgPadding.right();\n\t\tif (!answer.ripple) {\n\t\t\tauto mask = Ui::RippleAnimation::RectMask(QSize(\n\t\t\t\touterWidth,\n\t\t\t\tcountAnswerHeight(answer, innerWidth)));\n\t\t\tanswer.ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\tstd::move(mask),\n\t\t\t\t[=] { _owner->repaint(); });\n\t\t}\n\t\t// _owner->_lastLinkPoint is Options-local, compute answer's\n\t\t// position within Options (sum of heights above it).\n\t\tauto answerTop = 0;\n\t\tfor (const auto &a : _answers) {\n\t\t\tif (&a == &answer) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tanswerTop += countAnswerHeight(a, innerWidth);\n\t\t}\n\t\tanswer.ripple->add(_owner->_lastLinkPoint - QPoint(0, answerTop));\n\t} else if (answer.ripple) {\n\t\tanswer.ripple->lastStop();\n\t}\n}\n\nbool Poll::Header::canShowSolution() const {\n\treturn _owner->showVotes() && !_owner->_poll->solution.text.isEmpty();\n}\n\nbool Poll::Header::inShowSolution(\n\t\tQPoint point,\n\t\tint right,\n\t\tint top) const {\n\tif (!canShowSolution() || !_solutionButtonVisible) {\n\t\treturn false;\n\t}\n\tconst auto &icon = st::historyQuizExplainIn;\n\tconst auto x = right - icon.width();\n\tconst auto y = top + (st::normalFont->height - icon.height()) / 2;\n\treturn QRect(x, y, icon.width(), icon.height()).contains(point);\n}\n\nQString Poll::Footer::closeTimerText() const {\n\tif (_owner->_poll->closeDate <= 0\n\t\t|| (_owner->_flags & PollData::Flag::Closed)) {\n\t\t_closeTimer.cancel();\n\t\treturn {};\n\t}\n\tconst auto left = _owner->_poll->closeDate - base::unixtime::now();\n\tif (left <= 0) {\n\t\t_closeTimer.cancel();\n\t\treturn {};\n\t}\n\tif (!_closeTimer.isActive()) {\n\t\t_closeTimer.callEach(1000);\n\t}\n\tconst auto hideResults = (_owner->_flags\n\t\t& PollData::Flag::HideResultsUntilClose);\n\tif (left >= 86400) {\n\t\tconst auto days = (left + 86399) / 86400;\n\t\treturn hideResults\n\t\t\t? tr::lng_polls_results_in_days(tr::now, lt_count, days)\n\t\t\t: tr::lng_polls_ends_in_days(tr::now, lt_count, days);\n\t}\n\tconst auto timer = Ui::FormatDurationText(left);\n\treturn hideResults\n\t\t? tr::lng_polls_results_in_time(tr::now, lt_time, timer)\n\t\t: tr::lng_polls_ends_in_time(tr::now, lt_time, timer);\n}\n\nbool Poll::Footer::timerFooterMultiline(int paintw) const {\n\tconst auto timerText = closeTimerText();\n\tif (timerText.isEmpty()) {\n\t\treturn false;\n\t}\n\tif (_owner->_voted && !_owner->showVotes()) {\n\t\treturn true;\n\t}\n\tconst auto sep = QString::fromUtf8(\" \\xC2\\xB7 \");\n\tconst auto full = _totalVotesLabel.toString()\n\t\t+ sep\n\t\t+ timerText;\n\treturn centeredOverlapsInfo(st::msgDateFont->width(full), paintw);\n}\n\nbool Poll::Footer::centeredOverlapsInfo(\n\t\tint textWidth,\n\t\tint innerWidth) const {\n\tconst auto skipw = _owner->_parent->skipBlockWidth();\n\treturn (innerWidth + textWidth) / 2 > innerWidth - skipw;\n}\n\nint Poll::Footer::bottomLineWidth(int innerWidth) const {\n\tconst auto inline_ = _owner->inlineFooter();\n\tconst auto timerText = closeTimerText();\n\tconst auto timerLine = hasTimerLine(innerWidth);\n\n\tif (inline_ || _owner->showVotersCount()) {\n\t\tif (timerText.isEmpty()) {\n\t\t\treturn _totalVotesLabel.maxWidth();\n\t\t} else if (timerLine) {\n\t\t\treturn st::msgDateFont->width(timerText);\n\t\t}\n\t\t// Single-line timer — timerFooterMultiline already handles.\n\t\treturn 0;\n\t}\n\n\tif (_owner->_addOptionActive) {\n\t\treturn timerLine\n\t\t\t? 0\n\t\t\t: st::semiboldFont->width(\n\t\t\t\ttr::lng_polls_add_option_save(tr::now));\n\t} else if (_owner->isAuthorNotVoted()\n\t\t&& !_owner->_adminShowResults\n\t\t&& !_owner->canSendVotes()) {\n\t\treturn timerLine\n\t\t\t? 0\n\t\t\t: (_owner->_totalVotes > 0)\n\t\t\t? _adminVotesLabel.maxWidth()\n\t\t\t: _totalVotesLabel.maxWidth();\n\t} else if (_owner->_adminShowResults\n\t\t&& _owner->isAuthorNotVoted()) {\n\t\treturn timerLine ? 0 : _adminBackVoteLabel.maxWidth();\n\t}\n\n\tif (timerLine) {\n\t\treturn st::msgDateFont->width(timerText);\n\t}\n\tconst auto votedPublic = _owner->_voted\n\t\t&& (_owner->_flags & PollData::Flag::PublicVotes);\n\tconst auto string = (_owner->showVotes() || votedPublic)\n\t\t? ((_owner->_flags & PollData::Flag::PublicVotes)\n\t\t\t? tr::lng_polls_view_votes(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\t_owner->_totalVotes)\n\t\t\t: tr::lng_polls_view_results(tr::now))\n\t\t: tr::lng_polls_submit_votes(tr::now);\n\treturn st::semiboldFont->width(string);\n}\n\nint Poll::Footer::dateInfoPadding(int innerWidth) const {\n\tconst auto w = bottomLineWidth(innerWidth);\n\treturn (w > 0 && centeredOverlapsInfo(w, innerWidth))\n\t\t? st::msgDateFont->height\n\t\t: 0;\n}\n\nPoll::~Poll() {\n\thistory()->owner().unregisterPollView(_poll, _parent);\n\tif (hasHeavyPart()) {\n\t\tunloadHeavyPart();\n\t\t_parent->checkHeavyPart();\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_poll.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n#include \"ui/effects/animations.h\"\n#include \"data/data_poll.h\"\n#include \"base/weak_ptr.h\"\n#include \"base/timer.h\"\n\nnamespace Ui {\nclass RippleAnimation;\nclass FireworksAnimation;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass Message;\n\nclass Poll final : public Media {\npublic:\n\tPoll(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<PollData*> poll,\n\t\tconst TextWithEntities &consumed);\n\t~Poll();\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\n\tbool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {\n\t\treturn true;\n\t}\n\tbool dragItemByHandler(const ClickHandlerPtr &p) const override {\n\t\treturn true;\n\t}\n\n\tbool needsBubble() const override {\n\t\treturn true;\n\t}\n\tbool customInfoLayout() const override {\n\t\treturn false;\n\t}\n\n\t[[nodiscard]] TextSelection adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const override;\n\tuint16 fullSelectionLength() const override;\n\tTextForMimeData selectedText(TextSelection selection) const override;\n\n\tBubbleRoll bubbleRoll() const override;\n\tQMargins bubbleRollRepaintMargins() const override;\n\tvoid paintBubbleFireworks(\n\t\tPainter &p,\n\t\tconst QRect &bubble,\n\t\tcrl::time ms) const override;\n\n\tvoid clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool active) override;\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) override;\n\n\tvoid hideSpoilers() override;\n\n\tvoid unloadHeavyPart() override;\n\tbool hasHeavyPart() const override;\n\tvoid parentTextUpdated() override;\n\n\t[[nodiscard]] QRect addOptionRect(int innerWidth) const override;\n\tvoid setAddOptionActive(bool active) override;\n\nprivate:\n\tstruct Part;\n\tstruct Header;\n\tstruct Options;\n\tstruct AddOption;\n\tstruct Footer;\n\n\tstruct AnswerAnimation;\n\tstruct AnswersAnimation;\n\tstruct SendingAnimation;\n\tstruct Answer;\n\tstruct AttachedMedia;\n\tstruct SolutionMedia;\n\tstruct RecentVoter;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\n\t[[nodiscard]] bool showVotes() const;\n\t[[nodiscard]] bool canVote() const;\n\t[[nodiscard]] bool canSendVotes() const;\n\t[[nodiscard]] bool isAuthorNotVoted() const;\n\tvoid updateTexts();\n\tvoid updateVotes();\n\tbool showVotersCount() const;\n\tbool inlineFooter() const;\n\n\t[[nodiscard]] bool canAddOption() const;\n\n\tnot_null<PollData*> _poll;\n\tint _pollVersion = 0;\n\tint _totalVotes = 0;\n\tbool _voted = false;\n\tPollData::Flags _flags = PollData::Flags();\n\n\tmutable std::unique_ptr<Ui::FireworksAnimation> _fireworksAnimation;\n\tUi::Animations::Simple _wrongAnswerAnimation;\n\tmutable QPoint _lastLinkPoint;\n\n\tbool _addOptionActive = false;\n\tmutable bool _wrongAnswerAnimated = false;\n\tmutable bool _adminShowResults = false;\n\n\tstd::unique_ptr<Header> _headerPart;\n\tstd::unique_ptr<Options> _optionsPart;\n\tstd::unique_ptr<AddOption> _addOptionPart;\n\tstd::unique_ptr<Footer> _footerPart;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_premium_gift.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_credits.h\" // InputSavedStarGiftId\n#include \"api/api_premium.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/gift_premium_box.h\" // ResolveGiftCode\n#include \"boxes/star_gift_box.h\" // GiftReleasedByHandler\n#include \"chat_helpers/stickers_gift_box_pack.h\"\n#include \"core/click_handler_types.h\" // ClickHandlerContext\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_credits.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/history_view_element.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_credits.h\" // Settings::CreditsId\n#include \"settings/settings_credits_graphics.h\" // GiftedCreditsBox\n#include \"settings/sections/settings_premium.h\" // Settings::ShowGiftPremium\n#include \"ui/chat/chat_style.h\"\n#include \"ui/controls/ton_common.h\" // kNanosInOne\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\n\nPremiumGift::PremiumGift(\n\tnot_null<Element*> parent,\n\tnot_null<Data::MediaGiftBox*> gift)\n: _parent(parent)\n, _gift(gift)\n, _data(*gift->gift()) {\n}\n\nPremiumGift::~PremiumGift() = default;\n\nint PremiumGift::top() {\n\treturn (starGift() || tonGift())\n\t\t? st::msgServiceStarGiftStickerTop\n\t\t: st::msgServiceGiftBoxStickerTop;\n}\n\nint PremiumGift::width() {\n\treturn st::msgServiceStarGiftBoxWidth;\n}\n\nQSize PremiumGift::size() {\n\treturn (starGift() || tonGift())\n\t\t? QSize(\n\t\t\tst::msgServiceStarGiftStickerSize,\n\t\t\tst::msgServiceStarGiftStickerSize)\n\t\t: QSize(\n\t\t\tst::msgServiceGiftBoxStickerSize,\n\t\t\tst::msgServiceGiftBoxStickerSize);\n}\n\nTextWithEntities PremiumGift::title() {\n\tif (tonGift()) {\n\t\treturn tr::lng_gift_ton_amount(\n\t\t\ttr::now,\n\t\t\tlt_count_decimal,\n\t\t\tCreditsAmount(0, _data.count, CreditsType::Ton).value(),\n\t\t\ttr::marked);\n\t} else if (starGift()) {\n\t\tconst auto peer = _parent->history()->peer;\n\t\tconst auto to = _data.auctionTo ? _data.auctionTo : peer.get();\n\t\treturn peer->isSelf()\n\t\t\t? tr::lng_action_gift_self_subtitle(tr::now, tr::marked)\n\t\t\t: (peer->isServiceUser() && _data.channelFrom)\n\t\t\t? tr::lng_action_gift_got_subtitle(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\ttr::marked()\n\t\t\t\t\t.append(Ui::Text::SingleCustomEmoji(\n\t\t\t\t\t\tpeer->owner().customEmojiManager(\n\t\t\t\t\t\t).peerUserpicEmojiData(_data.channelFrom)))\n\t\t\t\t\t.append(' ')\n\t\t\t\t\t.append(_data.channelFrom->shortName()),\n\t\t\t\ttr::marked)\n\t\t\t: (!_data.auctionTo && peer->isServiceUser())\n\t\t\t? tr::lng_gift_link_label_gift(tr::now, tr::marked)\n\t\t\t: (outgoingGift()\n\t\t\t\t? tr::lng_action_gift_sent_subtitle\n\t\t\t\t: tr::lng_action_gift_got_subtitle)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::marked()\n\t\t\t\t\t\t.append(Ui::Text::SingleCustomEmoji(\n\t\t\t\t\t\t\tto->owner().customEmojiManager(\n\t\t\t\t\t\t\t).peerUserpicEmojiData(to)))\n\t\t\t\t\t\t.append(' ')\n\t\t\t\t\t\t.append(to->shortName()),\n\t\t\t\t\ttr::marked);\n\t} else if (creditsPrize()) {\n\t\treturn tr::lng_prize_title(tr::now, tr::marked);\n\t} else if (const auto stars = credits()) {\n\t\treturn tr::lng_gift_stars_title(tr::now, lt_count_decimal, stars, tr::marked);\n\t}\n\treturn gift()\n\t\t? tr::lng_action_gift_premium_months(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tpremiumMonths(),\n\t\t\ttr::marked)\n\t\t: _data.unclaimed\n\t\t? tr::lng_prize_unclaimed_title(tr::now, tr::marked)\n\t\t: tr::lng_prize_title(tr::now, tr::marked);\n}\n\nTextWithEntities PremiumGift::author() {\n\tif (!_data.stargiftReleasedBy) {\n\t\treturn {};\n\t}\n\treturn tr::lng_gift_released_by(\n\t\ttr::now,\n\t\tlt_name,\n\t\ttr::link('@' + _data.stargiftReleasedBy->username()),\n\t\ttr::marked);\n}\n\nTextWithEntities PremiumGift::subtitle() {\n\tif (tonGift()) {\n\t\treturn tr::lng_action_gift_got_ton(tr::now, tr::marked);\n\t} else if (starGift()) {\n\t\tconst auto toChannel = _data.channel\n\t\t\t&& _parent->history()->peer->isServiceUser();\n\t\treturn !_data.message.empty()\n\t\t\t? _data.message\n\t\t\t: _data.refunded\n\t\t\t? tr::lng_action_gift_refunded(tr::now, tr::rich)\n\t\t\t: outgoingGift()\n\t\t\t? (_data.auctionTo\n\t\t\t\t? tr::lng_action_gift_self_auction(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_cost,\n\t\t\t\t\ttr::lng_action_gift_for_stars(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\t_data.starsBid,\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\ttr::rich)\n\t\t\t\t: _data.starsUpgradedBySender\n\t\t\t\t? tr::lng_action_gift_sent_upgradable(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::bold(_parent->history()->peer->shortName()),\n\t\t\t\t\ttr::rich)\n\t\t\t\t: tr::lng_action_gift_sent_text(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t_data.starsConverted,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::bold(_parent->history()->peer->shortName()),\n\t\t\t\t\ttr::rich))\n\t\t\t: _data.starsUpgradedBySender\n\t\t\t? tr::lng_action_gift_got_upgradable_text(tr::now, tr::rich)\n\t\t\t: (_data.starsToUpgrade\n\t\t\t\t&& !_data.converted\n\t\t\t\t&& _parent->history()->peer->isSelf())\n\t\t\t? tr::lng_action_gift_self_about_unique(tr::now, tr::rich)\n\t\t\t: (_data.starsToUpgrade\n\t\t\t\t&& !_data.converted\n\t\t\t\t&& _parent->history()->peer->isServiceUser()\n\t\t\t\t&& _data.channel)\n\t\t\t? tr::lng_action_gift_channel_about_unique(tr::now, tr::rich)\n\t\t\t: (!_data.converted && !_data.starsConverted)\n\t\t\t? (_data.saved\n\t\t\t\t? (toChannel\n\t\t\t\t\t? tr::lng_action_gift_can_remove_channel\n\t\t\t\t\t: tr::lng_action_gift_can_remove_text)\n\t\t\t\t: (toChannel\n\t\t\t\t\t? tr::lng_action_gift_got_gift_channel\n\t\t\t\t\t: tr::lng_action_gift_got_gift_text))(tr::now, tr::rich)\n\t\t\t: (_data.converted\n\t\t\t\t? (toChannel\n\t\t\t\t\t? tr::lng_gift_channel_got\n\t\t\t\t\t: tr::lng_gift_got_stars)\n\t\t\t\t: _parent->history()->peer->isSelf()\n\t\t\t\t? tr::lng_action_gift_self_about\n\t\t\t\t: toChannel\n\t\t\t\t? tr::lng_action_gift_channel_about\n\t\t\t\t: tr::lng_action_gift_got_stars_text)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\t_data.starsConverted,\n\t\t\t\t\ttr::rich);\n\t}\n\tconst auto isCreditsPrize = creditsPrize();\n\tif (const auto count = credits(); count && !isCreditsPrize) {\n\t\treturn outgoingGift()\n\t\t\t? tr::lng_gift_stars_outgoing(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\ttr::bold(_parent->history()->peer->shortName()),\n\t\t\t\ttr::rich)\n\t\t\t: tr::lng_gift_stars_incoming(tr::now, tr::marked);\n\t} else if (gift()) {\n\t\treturn !_data.message.empty()\n\t\t\t? _data.message\n\t\t\t: tr::lng_action_gift_premium_about(tr::now, tr::rich);\n\t}\n\tconst auto name = _data.channel ? _data.channel->name() : \"channel\";\n\tauto result = (_data.unclaimed\n\t\t? tr::lng_prize_unclaimed_about\n\t\t: _data.viaGiveaway\n\t\t? tr::lng_prize_about\n\t\t: tr::lng_prize_gift_about)(\n\t\t\ttr::now,\n\t\t\tlt_channel,\n\t\t\ttr::bold(name),\n\t\t\ttr::rich);\n\tresult.append(\"\\n\\n\");\n\tresult.append(isCreditsPrize\n\t\t? tr::lng_prize_credits(\n\t\t\ttr::now,\n\t\t\tlt_amount,\n\t\t\ttr::lng_prize_credits_amount(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\tcredits(),\n\t\t\t\ttr::marked),\n\t\t\ttr::rich)\n\t\t: (_data.unclaimed\n\t\t\t? tr::lng_prize_unclaimed_duration\n\t\t\t: _data.viaGiveaway\n\t\t\t? tr::lng_prize_duration\n\t\t\t: tr::lng_prize_gift_duration)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_duration,\n\t\t\t\ttr::bold(GiftDuration(premiumDays())),\n\t\t\t\ttr::rich));\n\treturn result;\n}\n\nrpl::producer<QString> PremiumGift::button() {\n\treturn (starGift() && outgoingGift())\n\t\t? tr::lng_sticker_premium_view()\n\t\t: creditsPrize()\n\t\t? tr::lng_view_button_giftcode()\n\t\t: (starGift() && _data.starsUpgradedBySender && !_data.upgraded)\n\t\t? tr::lng_gift_view_unpack()\n\t\t: (gift() && (outgoingGift() || !_data.unclaimed))\n\t\t? tr::lng_sticker_premium_view()\n\t\t: tr::lng_prize_open();\n}\n\nstd::optional<Ui::Premium::MiniStarsType> PremiumGift::buttonMinistars() {\n\treturn tonGift()\n\t\t? Ui::Premium::MiniStarsType::SlowDiamondStars\n\t\t: Ui::Premium::MiniStarsType::SlowStars;\n}\n\nClickHandlerPtr PremiumGift::createViewLink() {\n\tif (tonGift()) {\n\t\tconst auto lifetime = std::make_shared<rpl::lifetime>();\n\t\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\t\tconst auto weak = my.sessionWindow;\n\t\t\tif (const auto window = weak.get()) {\n\t\t\t\twindow->session().credits().tonLoad();\n\t\t\t\t*lifetime = window->session().credits().tonLoadedValue(\n\t\t\t\t) | rpl::filter([=] {\n\t\t\t\t\tif (const auto window = weak.get()) {\n\t\t\t\t\t\treturn window->session().credits().tonLoaded();\n\t\t\t\t\t}\n\t\t\t\t\treturn false;\n\t\t\t\t}) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t\t\tif (const auto window = weak.get()) {\n\t\t\t\t\t\twindow->showSettings(Settings::CurrencyId());\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t}\n\tif (auto link = OpenStarGiftLink(_parent->data())) {\n\t\treturn link;\n\t}\n\tconst auto from = _gift->from();\n\tconst auto peer = _parent->history()->peer;\n\tconst auto date = _parent->data()->date();\n\tconst auto data = *_gift->gift();\n\tconst auto showForWeakWindow = [=](\n\t\t\tbase::weak_ptr<Window::SessionController> weak) {\n\t\tconst auto controller = weak.get();\n\t\tif (!controller) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto selfId = controller->session().userPeerId();\n\t\tconst auto sent = (from->id == selfId);\n\t\tif (creditsPrize()) {\n\t\t\tcontroller->show(Box(\n\t\t\t\tSettings::CreditsPrizeBox,\n\t\t\t\tcontroller,\n\t\t\t\tdata,\n\t\t\t\tdate));\n\t\t} else if (data.type == Data::GiftType::Credits) {\n\t\t\tconst auto to = sent ? peer : peer->session().user();\n\t\t\tcontroller->show(Box(\n\t\t\t\tSettings::GiftedCreditsBox,\n\t\t\t\tcontroller,\n\t\t\t\tfrom,\n\t\t\t\tto,\n\t\t\t\tdata.count,\n\t\t\t\tdate));\n\t\t} else if (data.slug.isEmpty()) {\n\t\t\tconst auto days = data.count;\n\t\t\tSettings::ShowGiftPremium(controller, peer, days, sent);\n\t\t} else {\n\t\t\tconst auto fromId = from->id;\n\t\t\tconst auto toId = sent ? peer->id : selfId;\n\t\t\tResolveGiftCode(controller, data.slug, fromId, toId);\n\t\t}\n\t};\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tshowForWeakWindow(\n\t\t\tcontext.other.value<ClickHandlerContext>().sessionWindow);\n\t});\n}\n\nClickHandlerPtr PremiumGift::authorLink() {\n\tif (const auto by = _data.stargiftReleasedBy) {\n\t\tif (!_authorLink) {\n\t\t\t_authorLink = std::make_shared<LambdaClickHandler>([=] {\n\t\t\t\tUi::GiftReleasedByHandler(by);\n\t\t\t});\n\t\t}\n\t\treturn _authorLink;\n\t}\n\treturn nullptr;\n}\n\nint PremiumGift::buttonSkip() {\n\treturn st::msgServiceGiftBoxButtonMargins.top();\n}\n\nvoid PremiumGift::draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry) {\n\tif (_sticker) {\n\t\t_sticker->draw(p, context, geometry);\n\t} else {\n\t\tensureStickerCreated();\n\t}\n}\n\nQImage PremiumGift::cornerTag(const PaintContext &context) {\n\tauto badge = Info::PeerGifts::GiftBadge();\n\tif (_data.unique) {\n\t\tconst auto burned = _data.unique->burned;\n\t\tconst auto burnedBg = Info::PeerGifts::BurnedBadgeBg();\n\t\tbadge = {\n\t\t\t.text = (burned\n\t\t\t\t? tr::lng_gift_burned_tag(tr::now)\n\t\t\t\t: tr::lng_gift_collectible_tag(tr::now)),\n\t\t\t.bg1 = (burned ? burnedBg : _data.unique->backdrop.edgeColor),\n\t\t\t.bg2 = (burned ? burnedBg : _data.unique->backdrop.patternColor),\n\t\t\t.fg = (burned ? st::white->c : _data.unique->backdrop.textColor),\n\t\t};\n\t} else if (const auto count = _data.limitedCount) {\n\t\tbadge = {\n\t\t\t.text = ((count == 1)\n\t\t\t\t? tr::lng_gift_limited_of_one(tr::now)\n\t\t\t\t: tr::lng_gift_limited_of_count(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_amount,\n\t\t\t\t\t(((count % 1000) && (count < 10'000))\n\t\t\t\t\t\t? Lang::FormatCountDecimal(count)\n\t\t\t\t\t\t: Lang::FormatCountToShort(count).string))),\n\t\t\t.bg1 = context.st->msgServiceBg()->c,\n\t\t\t.fg = context.st->msgServiceFg()->c,\n\t\t};\n\t} else {\n\t\treturn {};\n\t}\n\tif (_badgeCache.isNull() || _badgeKey != badge) {\n\t\t_badgeKey = badge;\n\t\t_badgeCache = ValidateRotatedBadge(\n\t\t\tbadge,\n\t\t\tst::msgServiceGiftBoxBadgePadding);\n\t}\n\treturn _badgeCache;\n}\n\nbool PremiumGift::hideServiceText() {\n\treturn !gift();\n}\n\nvoid PremiumGift::stickerClearLoopPlayed() {\n\tif (_sticker) {\n\t\t_sticker->stickerClearLoopPlayed();\n\t}\n}\n\nstd::unique_ptr<StickerPlayer> PremiumGift::stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) {\n\treturn _sticker\n\t\t? _sticker->stickerTakePlayer(data, replacements)\n\t\t: nullptr;\n}\n\nbool PremiumGift::hasHeavyPart() {\n\treturn (_sticker ? _sticker->hasHeavyPart() : false);\n}\n\nvoid PremiumGift::unloadHeavyPart() {\n\tif (_sticker) {\n\t\t_sticker->unloadHeavyPart();\n\t}\n}\n\nbool PremiumGift::incomingGift() const {\n\tconst auto out = _parent->data()->out();\n\treturn gift() && !_data.auctionTo && (starGiftUpgrade() ? out : !out);\n}\n\nbool PremiumGift::outgoingGift() const {\n\tconst auto out = _parent->data()->out();\n\treturn gift() && (_data.auctionTo || (starGiftUpgrade() ? !out : out));\n}\n\nbool PremiumGift::gift() const {\n\treturn _data.slug.isEmpty() || !_data.channel;\n}\n\nbool PremiumGift::tonGift() const {\n\treturn (_data.type == Data::GiftType::Ton);\n}\n\nbool PremiumGift::starGift() const {\n\treturn (_data.type == Data::GiftType::StarGift);\n}\n\nbool PremiumGift::starGiftUpgrade() const {\n\treturn (_data.type == Data::GiftType::StarGift) && _data.upgrade;\n}\n\nbool PremiumGift::creditsPrize() const {\n\treturn _data.viaGiveaway\n\t\t&& (_data.type == Data::GiftType::Credits)\n\t\t&& !_data.slug.isEmpty();\n}\n\nint PremiumGift::credits() const {\n\treturn (_data.type == Data::GiftType::Credits) ? _data.count : 0;\n}\n\nint PremiumGift::premiumDays() const {\n\treturn (_data.type == Data::GiftType::Premium) ? _data.count : 0;\n}\n\nint PremiumGift::premiumMonths() const {\n\treturn premiumDays() / 30;\n}\n\nvoid PremiumGift::ensureStickerCreated() const {\n\tif (_sticker) {\n\t\treturn;\n\t} else if (tonGift()) {\n\t\tconst auto &session = _parent->history()->session();\n\t\tauto &packs = session.giftBoxStickersPacks();\n\t\tconst auto count = _data.count / Ui::kNanosInOne;\n\t\tif (const auto document = packs.tonLookup(count)) {\n\t\t\tif (document->sticker()) {\n\t\t\t\tconst auto skipPremiumEffect = false;\n\t\t\t\t_sticker.emplace(_parent, document, skipPremiumEffect, _parent);\n\t\t\t\t_sticker->setStopOnLastFrame(true);\n\t\t\t\t_sticker->initSize(st::msgServiceGiftBoxStickerSize);\n\t\t\t}\n\t\t}\n\t\treturn;\n\t} else if (const auto document = _data.document) {\n\t\tconst auto sticker = document->sticker();\n\t\tAssert(sticker != nullptr);\n\t\t_sticker.emplace(_parent, document, false, _parent);\n\t\t_sticker->setPlayingOnce(true);\n\t\t_sticker->initSize(st::msgServiceStarGiftStickerSize);\n\t\t_parent->repaint();\n\t\treturn;\n\t}\n\tconst auto &session = _parent->history()->session();\n\tauto &packs = session.giftBoxStickersPacks();\n\tconst auto count = credits();\n\tconst auto months = count\n\t\t? packs.monthsForStars(count)\n\t\t: premiumMonths();\n\tif (const auto document = packs.lookup(months)) {\n\t\tif (document->sticker()) {\n\t\t\tconst auto skipPremiumEffect = false;\n\t\t\t_sticker.emplace(_parent, document, skipPremiumEffect, _parent);\n\t\t\t_sticker->setStopOnLastFrame(true);\n\t\t\t_sticker->initSize(st::msgServiceGiftBoxStickerSize);\n\t\t}\n\t}\n}\n\nClickHandlerPtr OpenStarGiftLink(not_null<HistoryItem*> item) {\n\tconst auto media = item->media();\n\tconst auto gift = media ? media->gift() : nullptr;\n\tif (!gift || gift->type != Data::GiftType::StarGift) {\n\t\treturn nullptr;\n\t}\n\tconst auto data = *gift;\n\tconst auto itemId = item->fullId();\n\tconst auto upgradedMsgId = data.upgraded\n\t\t? data.realGiftMsgId\n\t\t: MsgId(0);\n\tconst auto openInsteadId = data.realGiftMsgId\n\t\t? Data::SavedStarGiftId::User(data.realGiftMsgId)\n\t\t: (data.channel && data.channelSavedId)\n\t\t? Data::SavedStarGiftId::Chat(data.channel, data.channelSavedId)\n\t\t: Data::SavedStarGiftId();\n\tconst auto requesting = std::make_shared<bool>();\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tconst auto weak = my.sessionWindow;\n\t\tconst auto controller = weak.get();\n\t\tif (!controller) {\n\t\t\treturn;\n\t\t} else if (data.unique && data.unique->burned) {\n\t\t\tcontroller->showToast(tr::lng_gift_burned_message(tr::now));\n\t\t\treturn;\n\t\t}\n\t\tconst auto quick = [=](not_null<Window::SessionController*> window) {\n\t\t\tSettings::ShowStarGiftViewBox(window, data, itemId);\n\t\t};\n\t\tif (!openInsteadId) {\n\t\t\tquick(controller);\n\t\t\treturn;\n\t\t} else if (*requesting) {\n\t\t\treturn;\n\t\t}\n\t\t*requesting = true;\n\t\tconst auto requestSavedGift = [=] {\n\t\t\tcontroller->session().api().request(MTPpayments_GetSavedStarGift(\n\t\t\t\tMTP_vector<MTPInputSavedStarGift>(\n\t\t\t\t\t1,\n\t\t\t\t\tApi::InputSavedStarGiftId(openInsteadId))\n\t\t\t)).done([=](const MTPpayments_SavedStarGifts &result) {\n\t\t\t\t*requesting = false;\n\t\t\t\tif (const auto window = weak.get()) {\n\t\t\t\t\tconst auto &data = result.data();\n\t\t\t\t\twindow->session().data().processUsers(data.vusers());\n\t\t\t\t\twindow->session().data().processChats(data.vchats());\n\t\t\t\t\tconst auto owner = openInsteadId.chat()\n\t\t\t\t\t\t? openInsteadId.chat()\n\t\t\t\t\t\t: window->session().user();\n\t\t\t\t\tconst auto &list = data.vgifts().v;\n\t\t\t\t\tif (list.empty()) {\n\t\t\t\t\t\tquick(window);\n\t\t\t\t\t} else if (auto g = Api::FromTL(owner, list[0])) {\n\t\t\t\t\t\tSettings::ShowSavedStarGiftBox(window, owner, *g);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\t*requesting = false;\n\t\t\t\tif (const auto window = weak.get()) {\n\t\t\t\t\tif (!Ui::ShowGiftErrorToast(window->uiShow(), error)) {\n\t\t\t\t\t\twindow->showToast(error.type());\n\t\t\t\t\t\tquick(window);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}).send();\n\t\t};\n\t\tif (const auto msgId = upgradedMsgId) {\n\t\t\tconst auto session = &controller->session();\n\t\t\tconst auto owner = &controller->session().data();\n\t\t\tconst auto processItem = [=](not_null<HistoryItem*> item) {\n\t\t\t\tconst auto media = item->media();\n\t\t\t\tif (!media || !media->gift() || !media->gift()->unique) {\n\t\t\t\t\t*requesting = false;\n\t\t\t\t\tif (const auto window = weak.get()) {\n\t\t\t\t\t\tquick(window);\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// It is not possible to request a saved star gift\n\t\t\t\t// when it is transferred and does not belong to you.\n\t\t\t\tif (!media->gift()->transferred) {\n\t\t\t\t\treturn requestSavedGift();\n\t\t\t\t}\n\t\t\t\t*requesting = false;\n\t\t\t\tconst auto local = u\"nft/\"_q + media->gift()->unique->slug;\n\t\t\t\tUrlClickHandler::Open(session->createInternalLinkFull(local));\n\t\t\t};\n\t\t\tif (const auto item = owner->nonChannelMessage(msgId)) {\n\t\t\t\tprocessItem(item);\n\t\t\t} else {\n\t\t\t\tsession->api().requestMessageData(nullptr, msgId, [=] {\n\t\t\t\t\tif (const auto item = owner->nonChannelMessage(msgId)) {\n\t\t\t\t\t\tprocessItem(item);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t*requesting = false;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\trequestSavedGift();\n\t\t}\n\t});\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_premium_gift.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_sticker.h\"\n#include \"history/view/media/history_view_service_box.h\"\n#include \"info/peer_gifts/info_peer_gifts_common.h\"\n\nnamespace Data {\nclass MediaGiftBox;\nstruct GiftCode;\n} // namespace Data\n\nnamespace HistoryView {\n\nclass PremiumGift final : public ServiceBoxContent {\npublic:\n\tPremiumGift(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<Data::MediaGiftBox*> gift);\n\t~PremiumGift();\n\n\tint top() override;\n\tint width() override;\n\tQSize size() override;\n\tTextWithEntities title() override;\n\tTextWithEntities author() override;\n\tTextWithEntities subtitle() override;\n\trpl::producer<QString> button() override;\n\tstd::optional<Ui::Premium::MiniStarsType> buttonMinistars() override;\n\tQImage cornerTag(const PaintContext &context) override;\n\tint buttonSkip() override;\n\tvoid draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry) override;\n\tClickHandlerPtr createViewLink() override;\n\tClickHandlerPtr authorLink() override;\n\n\tbool hideServiceText() override;\n\tvoid stickerClearLoopPlayed() override;\n\tstd::unique_ptr<StickerPlayer> stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) override;\n\n\tbool hasHeavyPart() override;\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\t[[nodiscard]] bool incomingGift() const;\n\t[[nodiscard]] bool outgoingGift() const;\n\t[[nodiscard]] bool tonGift() const;\n\t[[nodiscard]] bool starGift() const;\n\t[[nodiscard]] bool starGiftUpgrade() const;\n\t[[nodiscard]] bool gift() const;\n\t[[nodiscard]] bool creditsPrize() const;\n\t[[nodiscard]] int credits() const;\n\t[[nodiscard]] int premiumDays() const;\n\t[[nodiscard]] int premiumMonths() const;\n\tvoid ensureStickerCreated() const;\n\n\tconst not_null<Element*> _parent;\n\tconst not_null<Data::MediaGiftBox*> _gift;\n\tconst Data::GiftCode &_data;\n\tClickHandlerPtr _authorLink;\n\tQImage _badgeCache;\n\tInfo::PeerGifts::GiftBadge _badgeKey;\n\tmutable std::optional<Sticker> _sticker;\n\n};\n\n[[nodiscard]] ClickHandlerPtr OpenStarGiftLink(not_null<HistoryItem*> item);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_save_document_action.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_save_document_action.h\"\n\n#include \"base/call_delayed.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_saved_music.h\"\n#include \"data/data_session.h\"\n#include \"history/view/history_view_context_menu.h\"\n#include \"history/view/history_view_list_widget.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/menu/menu_multiline_action.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace HistoryView {\n\nvoid AddSaveDocumentAction(\n\t\tconst Ui::Menu::MenuCallback &addAction,\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<DocumentData*> document,\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto contextId = item->fullId();\n\tconst auto fromSaved = item->history()->peer->isSelf();\n\tconst auto savedMusic = &document->owner().savedMusic();\n\tconst auto show = controller->uiShow();\n\tconst auto inProfile = savedMusic->has(document);\n\tconst auto &ripple = st::defaultDropdownMenu.menu.ripple;\n\tconst auto duration = ripple.hideDuration;\n\tconst auto saveAs = base::fn_delayed(duration, controller, [=] {\n\t\tDocumentSaveClickHandler::SaveAndTrack(\n\t\t\tcontextId,\n\t\t\tdocument,\n\t\t\tDocumentSaveClickHandler::Mode::ToNewFile);\n\t});\n\tif (!document->isMusicForProfile() || (fromSaved && inProfile)) {\n\t\tconst auto text = document->isVideoFile()\n\t\t\t? tr::lng_context_save_video(tr::now)\n\t\t\t: document->isVoiceMessage()\n\t\t\t? tr::lng_context_save_audio(tr::now)\n\t\t\t: document->isAudioFile()\n\t\t\t? tr::lng_context_save_audio_file(tr::now)\n\t\t\t: document->sticker()\n\t\t\t? tr::lng_context_save_image(tr::now)\n\t\t\t: tr::lng_context_save_file(tr::now);\n\t\taddAction(text, saveAs, &st::menuIconDownload);\n\t\treturn;\n\t}\n\tconst auto fill = [&](not_null<Ui::PopupMenu*> menu) {\n\t\tif (!inProfile) {\n\t\t\tconst auto saved = [=] {\n\t\t\t\tsavedMusic->save(document, contextId);\n\t\t\t\tshow->showToast(tr::lng_saved_music_added(tr::now));\n\t\t\t};\n\t\t\tmenu->addAction(\n\t\t\t\ttr::lng_context_save_music_profile(tr::now),\n\t\t\t\tsaved,\n\t\t\t\t&st::menuIconProfile);\n\t\t}\n\t\tif (!fromSaved) {\n\t\t\tmenu->addAction(\n\t\t\t\ttr::lng_context_save_music_saved(tr::now),\n\t\t\t\t[=] { Window::ForwardToSelf(show, { { contextId } }); },\n\t\t\t\t&st::menuIconSavedMessages);\n\t\t}\n\t\tmenu->addAction(\n\t\t\ttr::lng_context_save_music_folder(tr::now),\n\t\t\tsaveAs,\n\t\t\t&st::menuIconDownload);\n\n\t\tmenu->addSeparator(&st::expandedMenuSeparator);\n\n\t\tauto item = base::make_unique_q<Ui::Menu::MultilineAction>(\n\t\t\tmenu->menu(),\n\t\t\tst::saveMusicInfoMenu,\n\t\t\tst::historyHasCustomEmoji,\n\t\t\tQPoint(\n\t\t\t\tst::saveMusicInfoMenu.itemPadding.left(),\n\t\t\t\tst::saveMusicInfoMenu.itemPadding.top()),\n\t\t\tTextWithEntities{ tr::lng_context_save_music_about(tr::now) });\n\t\titem->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t\titem->setPointerCursor(false);\n\t\tmenu->addAction(std::move(item));\n\t};\n\taddAction(Ui::Menu::MenuCallback::Args{\n\t\t.text = tr::lng_context_save_music_to(tr::now),\n\t\t.handler = nullptr,\n\t\t.icon = &st::menuIconSoundAdd,\n\t\t.fillSubmenu = fill,\n\t\t.submenuSt = &st::popupMenuWithIcons,\n\t});\n}\n\nvoid AddSaveDocumentAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tHistoryItem *item,\n\t\tnot_null<DocumentData*> document,\n\t\tnot_null<ListWidget*> list) {\n\tif (!item || list->hasCopyMediaRestriction(item) || ItemHasTtl(item)) {\n\t\treturn;\n\t}\n\tAddSaveDocumentAction(\n\t\tUi::Menu::CreateAddActionCallback(menu),\n\t\titem,\n\t\tdocument,\n\t\tlist->controller());\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_save_document_action.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass DocumentData;\nclass HistoryItem;\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Ui::Menu {\nstruct MenuCallback;\n} // namespace Ui::Menu\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace HistoryView {\n\nclass ListWidget;\n\nvoid AddSaveDocumentAction(\n\tconst Ui::Menu::MenuCallback &addAction,\n\tnot_null<HistoryItem*> item,\n\tnot_null<DocumentData*> document,\n\tnot_null<Window::SessionController*> controller);\n\nvoid AddSaveDocumentAction(\n\tnot_null<Ui::PopupMenu*> menu,\n\tHistoryItem *item,\n\tnot_null<DocumentData*> document,\n\tnot_null<ListWidget*> list);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_service_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_service_box.h\"\n\n#include \"core/ui_integration.h\"\n#include \"data/data_session.h\"\n#include \"history/view/media/history_view_sticker_player_abstract.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_text_helper.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/effects/premium_stars_colored.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/power_saving.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_polls.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_layers.h\"\n\nnamespace HistoryView {\n\nint ServiceBoxContent::width() {\n\treturn st::msgServiceGiftBoxSize.width();\n}\n\nServiceBox::ServiceBox(\n\tnot_null<Element*> parent,\n\tstd::unique_ptr<ServiceBoxContent> content)\n: Media(parent)\n, _parent(parent)\n, _content(std::move(content))\n, _button({ .link = _content->createViewLink() })\n, _maxWidth(_content->width()\n\t- st::msgPadding.left()\n\t- st::msgPadding.right())\n, _title(\n\tst::defaultSubsectionTitle.style,\n\t_content->title(),\n\tkMarkupTextOptions,\n\t_maxWidth,\n\tCore::TextContext({\n\t\t.session = &parent->history()->session(),\n\t\t.repaint = [parent] { parent->customEmojiRepaint(); },\n\t}))\n, _author(\n\tst::uniqueGiftReleasedBy.style,\n\t_content->author(),\n\tkMarkupTextOptions,\n\t_maxWidth)\n, _subtitle(\n\tst::premiumPreviewAbout.style,\n\tUi::Text::Filtered(\n\t\t_content->subtitle(),\n\t\t{\n\t\t\tEntityType::Bold,\n\t\t\tEntityType::StrikeOut,\n\t\t\tEntityType::Underline,\n\t\t\tEntityType::Italic,\n\t\t\tEntityType::Spoiler,\n\t\t\tEntityType::CustomEmoji,\n\t\t}),\n\tkMarkupTextOptions,\n\t_maxWidth,\n\tCore::TextContext({\n\t\t.session = &parent->history()->session(),\n\t\t.repaint = [parent] { parent->customEmojiRepaint(); },\n\t}))\n, _size(\n\t_content->width(),\n\t(st::msgServiceGiftBoxTopSkip\n\t\t+ _content->top()\n\t\t+ _content->size().height()\n\t\t+ st::msgServiceGiftBoxTitlePadding.top()\n\t\t+ (_title.isEmpty()\n\t\t\t? 0\n\t\t\t: (_title.countHeight(_maxWidth)\n\t\t\t\t+ st::msgServiceGiftBoxTitlePadding.bottom()))\n\t\t+ (_author.isEmpty()\n\t\t\t? 0\n\t\t\t: (st::giftBoxReleasedByMargin.top()\n\t\t\t\t+ st::uniqueGiftReleasedBy.style.font->height\n\t\t\t\t+ st::giftBoxReleasedByMargin.bottom()\n\t\t\t\t+ st::msgServiceGiftBoxTitlePadding.bottom()))\n\t\t+ _subtitle.countHeight(_maxWidth)\n\t\t+ (!_content->button()\n\t\t\t? 0\n\t\t\t: (_content->buttonSkip() + st::msgServiceGiftBoxButtonHeight))\n\t\t+ st::msgServiceGiftBoxButtonMargins.bottom()))\n, _innerSize(_size - QSize(0, st::msgServiceGiftBoxTopSkip)) {\n\tInitElementTextPart(_parent, _subtitle);\n\tif (auto text = _content->button()) {\n\t\t_button.repaint = [=] { repaint(); };\n\t\tstd::move(text) | rpl::on_next([=](QString value) {\n\t\t\t_button.text.setText(st::semiboldTextStyle, value);\n\t\t\tconst auto height = st::msgServiceGiftBoxButtonHeight;\n\t\t\tconst auto &padding = st::msgServiceGiftBoxButtonPadding;\n\t\t\tconst auto empty = _button.size.isEmpty();\n\t\t\t_button.size = QSize(\n\t\t\t\t(_button.text.maxWidth()\n\t\t\t\t\t+ height\n\t\t\t\t\t+ padding.left()\n\t\t\t\t\t+ padding.right()),\n\t\t\t\theight);\n\t\t\tif (!empty) {\n\t\t\t\trepaint();\n\t\t\t}\n\t\t}, _lifetime);\n\t}\n\tif (const auto type = _content->buttonMinistars()) {\n\t\t_button.stars = std::make_unique<Ui::Premium::ColoredMiniStars>(\n\t\t\t[=](const QRect &) { repaint(); },\n\t\t\t*type);\n\t\t_button.lastFg = std::make_unique<QColor>();\n\t}\n\n\tif (auto changes = _content->changes()) {\n\t\tstd::move(changes) | rpl::on_next([=] {\n\t\t\tapplyContentChanges();\n\t\t}, _lifetime);\n\t}\n}\n\nServiceBox::~ServiceBox() = default;\n\nvoid ServiceBox::applyContentChanges() {\n\tconst auto subtitleWas = _subtitle.countHeight(_maxWidth);\n\n\tconst auto parent = _parent;\n\t_subtitle = Ui::Text::String(\n\t\tst::premiumPreviewAbout.style,\n\t\tUi::Text::Filtered(\n\t\t\t_content->subtitle(),\n\t\t\t{\n\t\t\t\tEntityType::Bold,\n\t\t\t\tEntityType::StrikeOut,\n\t\t\t\tEntityType::Underline,\n\t\t\t\tEntityType::Italic,\n\t\t\t\tEntityType::Spoiler,\n\t\t\t\tEntityType::CustomEmoji,\n\t\t\t}),\n\t\t\tkMarkupTextOptions,\n\t\t\t_maxWidth,\n\t\t\tCore::TextContext({\n\t\t\t\t.session = &parent->history()->session(),\n\t\t\t\t.repaint = [parent] { parent->customEmojiRepaint(); },\n\t\t\t}));\n\tInitElementTextPart(parent, _subtitle);\n\tconst auto subtitleNow = _subtitle.countHeight(_maxWidth);\n\tif (subtitleNow != subtitleWas) {\n\t\t_size.setHeight(_size.height() - subtitleWas + subtitleNow);\n\t\t_innerSize = _size - QSize(0, st::msgServiceGiftBoxTopSkip);\n\n\t\tconst auto item = parent->data();\n\t\titem->history()->owner().requestItemResize(item);\n\t} else {\n\t\tparent->repaint();\n\t}\n}\n\nQSize ServiceBox::countOptimalSize() {\n\treturn _size;\n}\n\nQSize ServiceBox::countCurrentSize(int newWidth) {\n\treturn _size;\n}\n\nvoid ServiceBox::draw(Painter &p, const PaintContext &context) const {\n\tp.translate(0, st::msgServiceGiftBoxTopSkip);\n\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(context.st->msgServiceBg());\n\n\tconst auto radius = st::msgServiceGiftBoxRadius;\n\tif (_parent->data()->inlineReplyKeyboard()) {\n\t\tconst auto r = Rect(_innerSize);\n\t\tconst auto half = r.height() / 2;\n\t\tp.setClipRect(r - QMargins(0, 0, 0, half));\n\t\tp.drawRoundedRect(r, radius, radius);\n\t\tp.setClipRect(r - QMargins(0, r.height() - half, 0, 0));\n\t\tconst auto small = Ui::BubbleRadiusSmall();\n\t\tp.drawRoundedRect(r, small, small);\n\t\tp.setClipping(false);\n\t} else {\n\t\tp.drawRoundedRect(Rect(_innerSize), radius, radius);\n\t}\n\n\tif (_button.stars) {\n\t\tconst auto &c = context.st->msgServiceFg()->c;\n\t\tif ((*_button.lastFg) != c) {\n\t\t\t_button.lastFg->setRgb(c.red(), c.green(), c.blue());\n\t\t\tconst auto padding = _button.size.height() / 2;\n\t\t\t_button.stars->setColorOverride(QGradientStops{\n\t\t\t\t{ 0., anim::with_alpha(c, .3) },\n\t\t\t\t{ 1., c },\n\t\t\t});\n\t\t\t_button.stars->setCenter(\n\t\t\t\tRect(_button.size) - QMargins(padding, 0, padding, 0));\n\t\t}\n\t}\n\n\tconst auto content = contentRect();\n\tauto top = content.top() + content.height();\n\t{\n\t\tp.setPen(context.st->msgServiceFg());\n\t\tconst auto &padding = st::msgServiceGiftBoxTitlePadding;\n\t\ttop += padding.top();\n\t\tif (!_title.isEmpty()) {\n\t\t\t_title.draw(p, {\n\t\t\t\t.position = QPoint(st::msgPadding.left(), top),\n\t\t\t\t.availableWidth = _maxWidth,\n\t\t\t\t.align = style::al_top,\n\t\t\t\t.palette = &context.st->serviceTextPalette(),\n\t\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t\t.now = context.now,\n\t\t\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t\t});\n\t\t\ttop += _title.countHeight(_maxWidth) + padding.bottom();\n\t\t}\n\t\tif (!_author.isEmpty()) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(context.st->msgServiceBg());\n\t\t\tconst auto use = std::min(_maxWidth, _author.maxWidth())\n\t\t\t\t+ st::giftBoxReleasedByMargin.left()\n\t\t\t\t+ st::giftBoxReleasedByMargin.right();\n\t\t\tconst auto left = st::msgPadding.left() + (_maxWidth - use) / 2;\n\t\t\tconst auto height = st::giftBoxReleasedByMargin.top()\n\t\t\t\t+ st::uniqueGiftReleasedBy.style.font->height\n\t\t\t\t+ st::giftBoxReleasedByMargin.bottom();\n\t\t\tconst auto radius = height / 2.;\n\t\t\tp.drawRoundedRect(left, top, use, height, radius, radius);\n\n\t\t\tauto fg = context.st->msgServiceFg()->c;\n\t\t\tfg.setAlphaF(0.65 * fg.alphaF());\n\t\t\tp.setPen(fg);\n\t\t\t_author.draw(p, {\n\t\t\t\t.position = QPoint(\n\t\t\t\t\tleft + st::giftBoxReleasedByMargin.left(),\n\t\t\t\t\ttop + st::giftBoxReleasedByMargin.top()),\n\t\t\t\t.availableWidth = (use\n\t\t\t\t\t- st::giftBoxReleasedByMargin.left()\n\t\t\t\t\t- st::giftBoxReleasedByMargin.right()),\n\t\t\t\t.palette = &context.st->serviceTextPalette(),\n\t\t\t\t.elisionLines = 1,\n\t\t\t});\n\t\t\tp.setPen(context.st->msgServiceFg());\n\n\t\t\ttop += height + st::msgServiceGiftBoxTitlePadding.bottom();\n\t\t}\n\t\t_parent->prepareCustomEmojiPaint(p, context, _subtitle);\n\t\t_subtitle.draw(p, {\n\t\t\t.position = QPoint(st::msgPadding.left(), top),\n\t\t\t.availableWidth = _maxWidth,\n\t\t\t.align = style::al_top,\n\t\t\t.palette = &context.st->serviceTextPalette(),\n\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t.now = context.now,\n\t\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t});\n\t\ttop += _subtitle.countHeight(_maxWidth) + padding.bottom();\n\t}\n\n\tif (!_button.empty()) {\n\t\tconst auto position = buttonRect().topLeft();\n\t\tp.translate(position);\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(context.st->msgServiceBg()); // ?\n\t\tif (const auto stars = _button.stars.get()) {\n\t\t\tstars->setPaused(context.paused);\n\t\t}\n\t\t_button.drawBg(p);\n\t\tp.setPen(context.st->msgServiceFg());\n\t\tif (_button.ripple) {\n\t\t\tconst auto opacity = p.opacity();\n\t\t\tp.setOpacity(st::historyPollRippleOpacity);\n\t\t\t_button.ripple->paint(\n\t\t\t\tp,\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\twidth(),\n\t\t\t\t&context.messageStyle()->msgWaveformInactive->c);\n\t\t\tp.setOpacity(opacity);\n\t\t}\n\t\t_button.text.draw(\n\t\t\tp,\n\t\t\t0,\n\t\t\t(_button.size.height() - _button.text.minHeight()) / 2,\n\t\t\t_button.size.width(),\n\t\t\tstyle::al_top);\n\n\t\tp.translate(-position);\n\t}\n\n\t_content->draw(p, context, content);\n\n\tif (const auto tag = _content->cornerTag(context); !tag.isNull()) {\n\t\tconst auto width = tag.width() / tag.devicePixelRatio();\n\t\tp.drawImage(_innerSize.width() - width, 0, tag);\n\t}\n\n\tp.translate(0, -st::msgServiceGiftBoxTopSkip);\n}\n\nTextState ServiceBox::textState(QPoint point, StateRequest request) const {\n\tauto result = TextState(_parent);\n\tpoint.setY(point.y() - st::msgServiceGiftBoxTopSkip);\n\tconst auto content = contentRect();\n\tconst auto lookupSubtitleLink = [&] {\n\t\tauto top = content.top() + content.height();\n\t\tconst auto &padding = st::msgServiceGiftBoxTitlePadding;\n\t\ttop += padding.top();\n\t\tif (!_title.isEmpty()) {\n\t\t\ttop += _title.countHeight(_maxWidth) + padding.bottom();\n\t\t}\n\t\tif (!_author.isEmpty()) {\n\t\t\tconst auto use = std::min(_maxWidth, _author.maxWidth())\n\t\t\t\t+ st::giftBoxReleasedByMargin.left()\n\t\t\t\t+ st::giftBoxReleasedByMargin.right();\n\t\t\tconst auto left = st::msgPadding.left() + (_maxWidth - use) / 2;\n\t\t\tconst auto height = st::giftBoxReleasedByMargin.top()\n\t\t\t\t+ st::defaultTextStyle.font->height\n\t\t\t\t+ st::giftBoxReleasedByMargin.bottom();\n\t\t\tif (point.x() >= left\n\t\t\t\t&& point.y() >= top\n\t\t\t\t&& point.x() < left + use\n\t\t\t\t&& point.y() < top + height) {\n\t\t\t\tresult.link = _content->authorLink();\n\t\t\t}\n\t\t\ttop += height + st::msgServiceGiftBoxTitlePadding.bottom();\n\t\t}\n\n\t\tauto subtitleRequest = request.forText();\n\t\tsubtitleRequest.align = style::al_top;\n\t\tconst auto state = _subtitle.getState(\n\t\t\tpoint - QPoint(st::msgPadding.left(), top),\n\t\t\t_maxWidth,\n\t\t\tsubtitleRequest);\n\t\tif (state.link) {\n\t\t\tresult.link = state.link;\n\t\t}\n\t};\n\tif (_button.empty()) {\n\t\tif (!_button.link) {\n\t\t\tlookupSubtitleLink();\n\t\t} else if (QRect(QPoint(), _innerSize).contains(point)) {\n\t\t\tresult.link = _button.link;\n\t\t}\n\t} else {\n\t\tconst auto rect = buttonRect();\n\t\tif (rect.contains(point)) {\n\t\t\tresult.link = _button.link;\n\t\t\t_button.lastPoint = point - rect.topLeft();\n\t\t} else if (content.contains(point)) {\n\t\t\tif (!_contentLink) {\n\t\t\t\t_contentLink = _content->createViewLink();\n\t\t\t}\n\t\t\tresult.link = _contentLink;\n\t\t} else {\n\t\t\tlookupSubtitleLink();\n\t\t}\n\t}\n\treturn result;\n}\n\nbool ServiceBox::toggleSelectionByHandlerClick(\n\t\tconst ClickHandlerPtr &p) const {\n\treturn false;\n}\n\nbool ServiceBox::dragItemByHandler(const ClickHandlerPtr &p) const {\n\treturn false;\n}\n\nvoid ServiceBox::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) {\n\tif (!handler) {\n\t\treturn;\n\t}\n\n\tif (handler == _button.link) {\n\t\t_button.toggleRipple(pressed);\n\t}\n}\n\nvoid ServiceBox::stickerClearLoopPlayed() {\n\t_content->stickerClearLoopPlayed();\n}\n\nstd::unique_ptr<StickerPlayer> ServiceBox::stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) {\n\treturn _content->stickerTakePlayer(data, replacements);\n}\n\nbool ServiceBox::needsBubble() const {\n\treturn false;\n}\n\nbool ServiceBox::customInfoLayout() const {\n\treturn false;\n}\n\nvoid ServiceBox::hideSpoilers() {\n\t_subtitle.setSpoilerRevealed(false, anim::type::instant);\n}\n\nbool ServiceBox::hasHeavyPart() const {\n\treturn _content->hasHeavyPart();\n}\n\nvoid ServiceBox::unloadHeavyPart() {\n\t_content->unloadHeavyPart();\n}\n\nQRect ServiceBox::buttonRect() const {\n\tconst auto &padding = st::msgServiceGiftBoxButtonMargins;\n\tconst auto position = QPoint(\n\t\t(width() - _button.size.width()) / 2,\n\t\theight() - padding.bottom() - _button.size.height());\n\treturn QRect(position, _button.size);\n}\n\nQRect ServiceBox::contentRect() const {\n\tconst auto size = _content->size();\n\tconst auto top = _content->top();\n\treturn QRect(QPoint((width() - size.width()) / 2, top), size);\n}\n\nvoid ServiceBox::Button::toggleRipple(bool pressed) {\n\tif (empty()) {\n\t\treturn;\n\t} else if (pressed) {\n\t\tconst auto linkWidth = size.width();\n\t\tconst auto linkHeight = size.height();\n\t\tif (!ripple) {\n\t\t\tconst auto drawMask = [&](QPainter &p) { drawBg(p); };\n\t\t\tauto mask = Ui::RippleAnimation::MaskByDrawer(\n\t\t\t\tQSize(linkWidth, linkHeight),\n\t\t\t\tfalse,\n\t\t\t\tdrawMask);\n\t\t\tripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\tstd::move(mask),\n\t\t\t\trepaint);\n\t\t}\n\t\tripple->add(lastPoint);\n\t} else if (ripple) {\n\t\tripple->lastStop();\n\t}\n}\n\nbool ServiceBox::Button::empty() const {\n\treturn text.isEmpty();\n}\n\nvoid ServiceBox::Button::drawBg(QPainter &p) const {\n\tconst auto radius = size.height() / 2.;\n\tconst auto r = Rect(size);\n\tp.drawRoundedRect(r, radius, radius);\n\tif (stars) {\n\t\tauto clipPath = QPainterPath();\n\t\tclipPath.addRoundedRect(r, radius, radius);\n\t\tp.setClipPath(clipPath);\n\t\tstars->paint(p);\n\t\tp.setClipping(false);\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_service_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n\nnamespace Ui {\nclass RippleAnimation;\n} // namespace Ui\n\nnamespace Ui::Premium {\nclass ColoredMiniStars;\nenum class MiniStarsType;\n} // namespace Ui::Premium\n\nnamespace HistoryView {\n\nclass ServiceBoxContent {\npublic:\n\tvirtual ~ServiceBoxContent() = default;\n\n\t[[nodiscard]] virtual int width();\n\t[[nodiscard]] virtual int top() = 0;\n\t[[nodiscard]] virtual QSize size() = 0;\n\t[[nodiscard]] virtual TextWithEntities title() = 0;\n\t[[nodiscard]] virtual TextWithEntities author() {\n\t\treturn {};\n\t}\n\t[[nodiscard]] virtual TextWithEntities subtitle() = 0;\n\t[[nodiscard]] virtual int buttonSkip() {\n\t\treturn top();\n\t}\n\t[[nodiscard]] virtual rpl::producer<QString> button() = 0;\n\n\t// For now only subtitle() changes are observed.\n\t[[nodiscard]] virtual rpl::producer<> changes() {\n\t\treturn nullptr;\n\t}\n\n\t[[nodiscard]] virtual auto buttonMinistars()\n\t-> std::optional<Ui::Premium::MiniStarsType> {\n\t\treturn std::nullopt;\n\t}\n\t[[nodiscard]] virtual QImage cornerTag(const PaintContext &context) {\n\t\treturn {};\n\t}\n\tvirtual void draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry) = 0;\n\t[[nodiscard]] virtual ClickHandlerPtr createViewLink() = 0;\n\t[[nodiscard]] virtual ClickHandlerPtr authorLink() {\n\t\treturn nullptr;\n\t}\n\n\t[[nodiscard]] virtual bool hideServiceText() = 0;\n\n\tvirtual void stickerClearLoopPlayed() = 0;\n\t[[nodiscard]] virtual std::unique_ptr<StickerPlayer> stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) = 0;\n\n\t[[nodiscard]] virtual bool hasHeavyPart() = 0;\n\tvirtual void unloadHeavyPart() = 0;\n};\n\nclass ServiceBox final : public Media {\npublic:\n\tServiceBox(\n\t\tnot_null<Element*> parent,\n\t\tstd::unique_ptr<ServiceBoxContent> content);\n\t~ServiceBox();\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\n\t[[nodiscard]] bool toggleSelectionByHandlerClick(\n\t\tconst ClickHandlerPtr &p) const override;\n\t[[nodiscard]] bool dragItemByHandler(\n\t\tconst ClickHandlerPtr &p) const override;\n\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) override;\n\n\tvoid stickerClearLoopPlayed() override;\n\tstd::unique_ptr<StickerPlayer> stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) override;\n\n\t[[nodiscard]] bool needsBubble() const override;\n\t[[nodiscard]] bool customInfoLayout() const override;\n\n\t[[nodiscard]] bool hideServiceText() const override {\n\t\treturn _content->hideServiceText();\n\t}\n\tvoid hideSpoilers() override;\n\n\tbool hasHeavyPart() const override;\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\t[[nodiscard]] QRect buttonRect() const;\n\t[[nodiscard]] QRect contentRect() const;\n\n\tvoid applyContentChanges();\n\n\tconst not_null<Element*> _parent;\n\tconst std::unique_ptr<ServiceBoxContent> _content;\n\tmutable ClickHandlerPtr _contentLink;\n\n\tstruct Button {\n\t\tvoid drawBg(QPainter &p) const;\n\t\tvoid toggleRipple(bool pressed);\n\t\t[[nodiscard]] bool empty() const;\n\n\t\tFn<void()> repaint;\n\n\t\tUi::Text::String text;\n\t\tQSize size;\n\n\t\tClickHandlerPtr link;\n\t\tstd::unique_ptr<Ui::RippleAnimation> ripple;\n\t\tstd::unique_ptr<Ui::Premium::ColoredMiniStars> stars;\n\t\tstd::unique_ptr<QColor> lastFg;\n\n\t\tmutable QPoint lastPoint;\n\t} _button;\n\n\tconst int _maxWidth = 0;\n\tUi::Text::String _title;\n\tUi::Text::String _author;\n\tUi::Text::String _subtitle;\n\tQSize _size;\n\tQSize _innerSize;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_similar_channels.h\"\n\n#include \"api/api_chat_participants.h\"\n#include \"apiwrap.h\"\n#include \"boxes/peer_lists_box.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_session.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"info/similar_peers/info_similar_peers_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/painter.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_polls.h\"\n\nnamespace HistoryView {\nnamespace {\n\nusing Channels = Api::ChatParticipants::Peers;\n\n//void SimilarChannelsController::prepare() {\n//\tfor (const auto &channel : _channels.list) {\n//\t\tauto row = std::make_unique<PeerListRow>(channel);\n//\t\tif (const auto count = channel->membersCount(); count > 1) {\n//\t\t\trow->setCustomStatus(tr::lng_chat_status_subscribers(\n//\t\t\t\ttr::now,\n//\t\t\t\tlt_count,\n//\t\t\t\tcount));\n//\t\t}\n//\t\tdelegate()->peerListAppendRow(std::move(row));\n//\t}\n//\tdelegate()->peerListRefreshRows();\n//}\n\n[[nodiscard]] ClickHandlerPtr MakeViewAllLink(\n\t\tnot_null<ChannelData*> channel,\n\t\tbool promoForNonPremium) {\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto strong = my.sessionWindow.get()) {\n\t\t\tAssert(channel != nullptr);\n\t\t\tif (promoForNonPremium && !channel->session().premium()) {\n\t\t\t\tconst auto upto = Data::PremiumLimits(\n\t\t\t\t\t&channel->session()).similarChannelsPremium();\n\t\t\t\tSettings::ShowPremiumPromoToast(\n\t\t\t\t\tstrong->uiShow(),\n\t\t\t\t\ttr::lng_similar_channels_premium_all(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tupto,\n\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\ttr::link(\n\t\t\t\t\t\t\ttr::bold(\n\t\t\t\t\t\t\t\ttr::lng_similar_channels_premium_all_link(\n\t\t\t\t\t\t\t\t\ttr::now))),\n\t\t\t\t\t\ttr::rich),\n\t\t\t\t\tu\"similar_channels\"_q);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto api = &channel->session().api();\n\t\t\tconst auto &list = api->chatParticipants().similar(channel);\n\t\t\tif (list.list.empty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstrong->showSection(\n\t\t\t\tstd::make_shared<Info::Memento>(\n\t\t\t\t\tchannel,\n\t\t\t\t\tInfo::Section::Type::SimilarPeers));\n\t\t}\n\t});\n}\n\n} // namespace\n\nSimilarChannels::SimilarChannels(not_null<Element*> parent)\n: Media(parent) {\n}\n\nSimilarChannels::~SimilarChannels() {\n\tif (hasHeavyPart()) {\n\t\tunloadHeavyPart();\n\t\tparent()->checkHeavyPart();\n\t}\n}\n\nvoid SimilarChannels::clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool active) {\n}\n\nvoid SimilarChannels::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed) {\n\tfor (auto &channel : _channels) {\n\t\tif (channel.link != p) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (pressed) {\n\t\t\tif (!channel.ripple) {\n\t\t\t\tchannel.ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\t\tUi::RippleAnimation::RoundRectMask(\n\t\t\t\t\t\tchannel.geometry.size(),\n\t\t\t\t\t\tst::roundRadiusLarge),\n\t\t\t\t\t[=] { repaint(); });\n\t\t\t}\n\t\t\tchannel.ripple->add(_lastPoint);\n\t\t} else if (channel.ripple) {\n\t\t\tchannel.ripple->lastStop();\n\t\t}\n\t\tbreak;\n\t}\n}\n\nvoid SimilarChannels::draw(Painter &p, const PaintContext &context) const {\n\tif (!_toggled) {\n\t\treturn;\n\t}\n\tconst auto large = Ui::BubbleCornerRounding::Large;\n\tconst auto geometry = QRect(0, 0, width(), height());\n\tUi::PaintBubble(\n\t\tp,\n\t\tUi::SimpleBubble{\n\t\t\t.st = context.st,\n\t\t\t.geometry = geometry,\n\t\t\t.pattern = context.bubblesPattern,\n\t\t\t.patternViewport = context.viewport,\n\t\t\t.outerWidth = width(),\n\t\t\t.rounding = { large, large, large, large },\n\t\t});\n\tconst auto stm = context.messageStyle();\n\t{\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tauto path = QPainterPath();\n\t\tconst auto x = geometry.center().x();\n\t\tconst auto y = geometry.y();\n\t\tconst auto size = st::chatSimilarArrowSize;\n\t\tpath.moveTo(x, y - size);\n\t\tpath.lineTo(x + size, y);\n\t\tpath.lineTo(x - size, y);\n\t\tpath.lineTo(x, y - size);\n\t\tp.fillPath(path, stm->msgBg);\n\t}\n\tconst auto padding = st::chatSimilarChannelPadding;\n\tp.setClipRect(geometry);\n\t_hasHeavyPart = 1;\n\tvalidateLastPremiumLock();\n\tconst auto drawOne = [&](const Channel &channel) {\n\t\tconst auto geometry = channel.geometry.translated(-int(_scrollLeft), 0);\n\t\tconst auto right = geometry.x() + geometry.width();\n\t\tif (right <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto subscribing = !channel.subscribed;\n\t\tif (subscribing) {\n\t\t\tchannel.subscribed = 1;\n\t\t\tconst auto raw = channel.thumbnail.get();\n\t\t\tchannel.thumbnail->subscribeToUpdates([=] {\n\t\t\t\tfor (const auto &channel : _channels) {\n\t\t\t\t\tif (channel.thumbnail.get() == raw) {\n\t\t\t\t\t\tchannel.counterBgValid = 0;\n\t\t\t\t\t\trepaint();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\tauto cachedp = std::optional<Painter>();\n\t\tconst auto cached = (geometry.x() < padding.left())\n\t\t\t|| (right > width() - padding.right());\n\t\tif (cached) {\n\t\t\tensureCacheReady(geometry.size());\n\t\t\t_roundedCache.fill(Qt::transparent);\n\t\t\tcachedp.emplace(&_roundedCache);\n\t\t\tcachedp->translate(-geometry.topLeft());\n\t\t}\n\t\tconst auto q = cachedp ? &*cachedp : &p;\n\t\tif (channel.more) {\n\t\t\tchannel.ripple.reset();\n\t\t} else if (channel.ripple) {\n\t\t\tq->setOpacity(st::historyPollRippleOpacity);\n\t\t\tchannel.ripple->paint(\n\t\t\t\t*q,\n\t\t\t\tgeometry.x(),\n\t\t\t\tgeometry.y(),\n\t\t\t\twidth(),\n\t\t\t\t&stm->msgWaveformInactive->c);\n\t\t\tif (channel.ripple->empty()) {\n\t\t\t\tchannel.ripple.reset();\n\t\t\t}\n\t\t\tq->setOpacity(1.);\n\t\t}\n\n\t\tauto pen = stm->msgBg->p;\n\t\tauto left = geometry.x() + 2 * padding.left();\n\t\tconst auto stroke = st::lineWidth * 2.;\n\t\tconst auto add = stroke / 2.;\n\t\tconst auto top = geometry.y() + padding.top();\n\t\tconst auto size = st::chatSimilarChannelPhoto;\n\t\tconst auto paintCircle = [&] {\n\t\t\tauto hq = PainterHighQualityEnabler(*q);\n\t\t\tq->drawEllipse(QRectF(left, top, size, size).marginsAdded(\n\t\t\t\t{ add, add, add, add }));\n\t\t};\n\t\tif (channel.more) {\n\t\t\tpen.setWidthF(stroke);\n\t\t\tp.setPen(pen);\n\t\t\tfor (auto i = 2; i != 0;) {\n\t\t\t\t--i;\n\t\t\t\tif (const auto &thumbnail = _moreThumbnails[i]) {\n\t\t\t\t\tif (subscribing) {\n\t\t\t\t\t\tthumbnail->subscribeToUpdates([=] {\n\t\t\t\t\t\t\trepaint();\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tq->drawImage(left, top, thumbnail->image(size));\n\t\t\t\t\tq->setBrush(Qt::NoBrush);\n\t\t\t\t} else {\n\t\t\t\t\tq->setBrush(st::windowBgRipple->c);\n\t\t\t\t}\n\t\t\t\tif (!i || !_moreThumbnails[i]) {\n\t\t\t\t\tpaintCircle();\n\t\t\t\t}\n\t\t\t\tleft -= padding.left();\n\t\t\t}\n\t\t} else {\n\t\t\tleft -= padding.left();\n\t\t}\n\t\tq->drawImage(\n\t\t\tleft,\n\t\t\ttop,\n\t\t\tchannel.thumbnail->image(size));\n\t\tif (channel.more) {\n\t\t\tq->setBrush(Qt::NoBrush);\n\t\t\tpaintCircle();\n\t\t}\n\t\tif (!channel.counter.isEmpty()) {\n\t\t\tvalidateCounterBg(channel);\n\t\t\tconst auto participants = channel.counterRect.translated(\n\t\t\t\tgeometry.topLeft());\n\t\t\tq->drawImage(participants.topLeft(), channel.counterBg);\n\t\t\tconst auto badge = participants.marginsRemoved(\n\t\t\t\tst::chatSimilarBadgePadding);\n\t\t\tauto textLeft = badge.x();\n\t\t\tconst auto &font = st::chatSimilarBadgeFont;\n\t\t\tconst auto textTop = badge.y() + font->ascent;\n\t\t\tconst auto icon = !channel.more\n\t\t\t\t? &st::chatSimilarBadgeIcon\n\t\t\t\t: channel.moreLocked\n\t\t\t\t? &st::chatSimilarLockedIcon\n\t\t\t\t: nullptr;\n\t\t\tconst auto position = !channel.more\n\t\t\t\t? st::chatSimilarBadgeIconPosition\n\t\t\t\t: st::chatSimilarLockedIconPosition;\n\t\t\tif (icon) {\n\t\t\t\tconst auto skip = channel.more\n\t\t\t\t\t? (badge.width() - icon->width())\n\t\t\t\t\t: 0;\n\t\t\t\ticon->paint(\n\t\t\t\t\t*q,\n\t\t\t\t\tbadge.x() + position.x() + skip,\n\t\t\t\t\tbadge.y() + position.y(),\n\t\t\t\t\twidth());\n\t\t\t\tif (!channel.more) {\n\t\t\t\t\ttextLeft += position.x() + icon->width();\n\t\t\t\t}\n\t\t\t}\n\t\t\tq->setFont(font);\n\t\t\tq->setPen(st::premiumButtonFg);\n\t\t\tq->drawText(textLeft, textTop, channel.counter);\n\t\t}\n\t\tq->setPen(channel.more ? st::windowSubTextFg : stm->historyTextFg);\n\t\tchannel.name.drawLeftElided(\n\t\t\t*q,\n\t\t\tgeometry.x() + st::normalFont->spacew,\n\t\t\tgeometry.y() + st::chatSimilarNameTop,\n\t\t\t(geometry.width() - 2 * st::normalFont->spacew),\n\t\t\twidth(),\n\t\t\t2,\n\t\t\tstyle::al_top);\n\t\tif (cachedp) {\n\t\t\tq->setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\t\tconst auto corners = _roundedCorners.data();\n\t\t\tconst auto side = st::bubbleRadiusLarge;\n\t\t\tq->drawImage(0, 0, corners[Images::kTopLeft]);\n\t\t\tq->drawImage(width() - side, 0, corners[Images::kTopRight]);\n\t\t\tq->drawImage(0, height() - side, corners[Images::kBottomLeft]);\n\t\t\tq->drawImage(\n\t\t\t\tQPoint(width() - side, height() - side),\n\t\t\t\tcorners[Images::kBottomRight]);\n\t\t\tcachedp.reset();\n\t\t\tp.drawImage(geometry.topLeft(), _roundedCache);\n\t\t}\n\t};\n\tfor (const auto &channel : _channels) {\n\t\tif (channel.geometry.x() >= _scrollLeft + width()) {\n\t\t\tbreak;\n\t\t}\n\t\tdrawOne(channel);\n\t}\n\tp.setPen(stm->historyTextFg);\n\tp.setFont(st::chatSimilarTitle);\n\tp.drawTextLeft(\n\t\tst::chatSimilarTitlePosition.x(),\n\t\tst::chatSimilarTitlePosition.y(),\n\t\twidth(),\n\t\t_title);\n\tif (!_hasViewAll) {\n\t\treturn;\n\t}\n\tp.setFont(ClickHandler::showAsActive(_viewAllLink)\n\t\t? st::normalFont->underline()\n\t\t: st::normalFont);\n\tp.setPen(stm->textPalette.linkFg);\n\tconst auto add = st::normalFont->ascent - st::chatSimilarTitle->ascent;\n\tp.drawTextRight(\n\t\tst::chatSimilarTitlePosition.x(),\n\t\tst::chatSimilarTitlePosition.y() + add,\n\t\twidth(),\n\t\t_viewAll);\n\tp.setClipping(false);\n}\n\nvoid SimilarChannels::validateLastPremiumLock() const {\n\tif (_channels.empty()) {\n\t\treturn;\n\t}\n\tif (!_moreThumbnailsValid) {\n\t\t_moreThumbnailsValid = 1;\n\t\tfillMoreThumbnails();\n\t}\n\tconst auto &last = _channels.back();\n\tif (!last.more) {\n\t\treturn;\n\t}\n\tconst auto premium = history()->session().premium();\n\tconst auto locked = !premium && history()->session().premiumPossible();\n\tif (last.moreLocked == locked) {\n\t\treturn;\n\t}\n\tlast.moreLocked = locked ? 1 : 0;\n\tlast.counterBgValid = 0;\n}\n\nvoid SimilarChannels::fillMoreThumbnails() const {\n\tconst auto channel = parent()->history()->peer->asChannel();\n\tAssert(channel != nullptr);\n\n\t_moreThumbnails = {};\n\tconst auto api = &channel->session().api();\n\tconst auto &similar = api->chatParticipants().similar(channel);\n\tfor (auto i = 0, count = int(_moreThumbnails.size()); i != count; ++i) {\n\t\tif (similar.list.size() <= _channels.size() + i) {\n\t\t\tbreak;\n\t\t}\n\t\t_moreThumbnails[i] = Ui::MakeUserpicThumbnail(\n\t\t\tsimilar.list[_channels.size() + i]);\n\t}\n}\n\nvoid SimilarChannels::validateCounterBg(const Channel &channel) const {\n\tif (channel.counterBgValid) {\n\t\treturn;\n\t}\n\tchannel.counterBgValid = 1;\n\n\tconst auto photo = st::chatSimilarChannelPhoto;\n\tconst auto inner = QRect(0, 0, photo, photo);\n\tconst auto outer = inner.marginsAdded(st::chatSimilarChannelPadding);\n\tconst auto length = st::chatSimilarBadgeFont->width(channel.counter);\n\tconst auto contents = length\n\t\t+ (!channel.more\n\t\t\t? st::chatSimilarBadgeIcon.width()\n\t\t\t: channel.moreLocked\n\t\t\t? st::chatSimilarLockedIcon.width()\n\t\t\t: 0);\n\tconst auto delta = (outer.width() - contents) / 2;\n\tconst auto badge = QRect(\n\t\tdelta,\n\t\tst::chatSimilarBadgeTop,\n\t\touter.width() - 2 * delta,\n\t\tst::chatSimilarBadgeFont->height);\n\tchannel.counterRect = badge.marginsAdded(\n\t\tst::chatSimilarBadgePadding);\n\n\tconstexpr auto kMinSaturation = 0;\n\tconstexpr auto kMaxSaturation = 96;\n\tconstexpr auto kMinLightness = 160;\n\tconstexpr auto kMaxLightness = 208;\n\n\tconst auto width = channel.counterRect.width();\n\tconst auto height = channel.counterRect.height();\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto result = QImage(\n\t\tchannel.counterRect.size() * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tauto color = channel.more\n\t\t? QColor(kMinLightness, kMinLightness, kMinLightness)\n\t\t: Ui::CountAverageColor(\n\t\t\tchannel.thumbnail->image(photo).copy(\n\t\t\t\tQRect(photo / 3, photo / 3, photo / 3, photo / 3)));\n\n\tconst auto hsl = color.toHsl();\n\tif (!base::in_range(hsl.saturation(), kMinSaturation, kMaxSaturation)\n\t\t|| !base::in_range(hsl.lightness(), kMinLightness, kMaxLightness)) {\n\t\tcolor = QColor::fromHsl(\n\t\t\thsl.hue(),\n\t\t\tstd::clamp(hsl.saturation(), kMinSaturation, kMaxSaturation),\n\t\t\tstd::clamp(hsl.lightness(), kMinLightness, kMaxLightness)\n\t\t).toRgb();\n\t}\n\n\tresult.fill(color);\n\tresult.setDevicePixelRatio(ratio);\n\tconst auto radius = height / 2;\n\tauto corners = Images::CornersMask(radius);\n\tauto p = QPainter(&result);\n\tp.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\tp.drawImage(0, 0, corners[Images::kTopLeft]);\n\tp.drawImage(width - radius, 0, corners[Images::kTopRight]);\n\tp.drawImage(0, height - radius, corners[Images::kBottomLeft]);\n\tp.drawImage(\n\t\twidth - radius,\n\t\theight - radius,\n\t\tcorners[Images::kBottomRight]);\n\tp.end();\n\tchannel.counterBg = std::move(result);\n}\n\nClickHandlerPtr SimilarChannels::ensureToggleLink() const {\n\tif (_toggleLink) {\n\t\treturn _toggleLink;\n\t}\n\t_toggleLink = std::make_shared<LambdaClickHandler>(crl::guard(this, [=](\n\t\t\tClickContext context) {\n\t\tconst auto channel = history()->peer->asChannel();\n\t\tAssert(channel != nullptr);\n\t\tusing Flag = ChannelDataFlag;\n\t\tconst auto flags = channel->flags();\n\t\tchannel->setFlags((flags & Flag::SimilarExpanded)\n\t\t\t? (flags & ~Flag::SimilarExpanded)\n\t\t\t: (flags | Flag::SimilarExpanded));\n\t}));\n\treturn _toggleLink;\n}\n\nvoid SimilarChannels::ensureCacheReady(QSize size) const {\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (_roundedCache.size() != size * ratio) {\n\t\t_roundedCache = QImage(\n\t\t\tsize * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t_roundedCache.setDevicePixelRatio(ratio);\n\t}\n\tconst auto radius = st::bubbleRadiusLarge;\n\tif (_roundedCorners.front().size() != QSize(radius, radius) * ratio) {\n\t\t_roundedCorners = Images::CornersMask(radius);\n\t}\n}\n\nTextState SimilarChannels::textState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tauto result = TextState();\n\tif (point.y() < 0 && !_empty) {\n\t\tresult.link = ensureToggleLink();\n\t\treturn result;\n\t}\n\tresult.horizontalScroll = (_scrollMax > 0);\n\tconst auto skip = st::chatSimilarTitlePosition;\n\tconst auto viewWidth = _hasViewAll ? (_viewAllWidth + 2 * skip.x()) : 0;\n\tconst auto viewHeight = st::normalFont->height + 2 * skip.y();\n\tconst auto viewLeft = width() - viewWidth;\n\tif (QRect(viewLeft, 0, viewWidth, viewHeight).contains(point)) {\n\t\tif (!_viewAllLink) {\n\t\t\tconst auto channel = parent()->history()->peer->asChannel();\n\t\t\tAssert(channel != nullptr);\n\t\t\t_viewAllLink = MakeViewAllLink(channel, false);\n\t\t}\n\t\tresult.link = _viewAllLink;\n\t\treturn result;\n\t}\n\tfor (const auto &channel : _channels) {\n\t\tif (channel.geometry.translated(-int(_scrollLeft), 0).contains(point)) {\n\t\t\tresult.link = channel.link;\n\t\t\t_lastPoint = point\n\t\t\t\t+ QPoint(_scrollLeft, 0)\n\t\t\t\t- channel.geometry.topLeft();\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn result;\n}\n\nQSize SimilarChannels::countOptimalSize() {\n\tconst auto channel = parent()->history()->peer->asChannel();\n\tAssert(channel != nullptr);\n\n\t_channels.clear();\n\t_moreThumbnails = {};\n\tconst auto api = &channel->session().api();\n\tapi->chatParticipants().loadSimilarPeers(channel);\n\tconst auto premium = channel->session().premium();\n\tconst auto &similar = api->chatParticipants().similar(channel);\n\t_empty = similar.list.empty() ? 1 : 0;\n\t_moreThumbnailsValid = 0;\n\tusing Flag = ChannelDataFlag;\n\t_toggled = (channel->flags() & Flag::SimilarExpanded) ? 1 : 0;\n\tif (_empty || !_toggled) {\n\t\treturn {};\n\t}\n\n\t_channels.reserve(similar.list.size());\n\tauto x = st::chatSimilarPadding.left();\n\tauto y = st::chatSimilarPadding.top();\n\tconst auto skip = st::chatSimilarSkip;\n\tconst auto photo = st::chatSimilarChannelPhoto;\n\tconst auto inner = QRect(0, 0, photo, photo);\n\tconst auto outer = inner.marginsAdded(st::chatSimilarChannelPadding);\n\tconst auto limit = Data::PremiumLimits(\n\t\t&channel->session()).similarChannelsDefault();\n\tconst auto take = (similar.more > 0 || similar.list.size() > 2 * limit)\n\t\t? limit\n\t\t: int(similar.list.size());\n\tconst auto more = similar.more + int(similar.list.size() - take);\n\tauto &&peers = ranges::views::all(similar.list)\n\t\t| ranges::views::take(limit);\n\tfor (const auto &peer : peers) {\n\t\tconst auto channel = peer->asBroadcast();\n\t\tif (!channel) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto moreCounter = (_channels.size() + 1 == take) ? more : 0;\n\t\t_channels.push_back({\n\t\t\t.geometry = QRect(QPoint(x, y), outer.size()),\n\t\t\t.name = Ui::Text::String(\n\t\t\t\tst::chatSimilarName,\n\t\t\t\t(moreCounter\n\t\t\t\t\t? tr::lng_similar_channels_more(tr::now)\n\t\t\t\t\t: channel->name()),\n\t\t\t\tkDefaultTextOptions,\n\t\t\t\tst::chatSimilarChannelPhoto),\n\t\t\t.thumbnail = Ui::MakeUserpicThumbnail(channel),\n\t\t\t.more = uint32(moreCounter),\n\t\t\t.moreLocked = uint32((moreCounter && !premium) ? 1 : 0),\n\t\t});\n\t\tauto &last = _channels.back();\n\t\tlast.link = moreCounter\n\t\t\t? MakeViewAllLink(parent()->history()->peer->asChannel(), true)\n\t\t\t: channel->openLink();\n\n\t\tconst auto counter = moreCounter\n\t\t\t? moreCounter\n\t\t\t: channel->membersCount();\n\t\tif (moreCounter || counter > 1) {\n\t\t\tlast.counter = (moreCounter ? u\"+\"_q : QString())\n\t\t\t\t+ Lang::FormatCountToShort(counter).string;\n\t\t}\n\t\tx += outer.width() + skip;\n\t}\n\t_title = tr::lng_similar_channels_title(tr::now);\n\t_titleWidth = st::chatSimilarTitle->width(_title);\n\t_viewAll = tr::lng_similar_channels_view_all(tr::now);\n\t_viewAllWidth = std::max(st::normalFont->width(_viewAll), 0);\n\tconst auto count = int(_channels.size());\n\tconst auto desired = (count ? (x - skip) : x)\n\t\t- st::chatSimilarPadding.left();\n\tconst auto full = QRect(0, 0, desired, outer.height());\n\tconst auto bubble = full.marginsAdded(st::chatSimilarPadding);\n\t_fullWidth = bubble.width();\n\tconst auto titleSkip = st::chatSimilarTitlePosition.x();\n\tconst auto min = int(_titleWidth) + 2 * titleSkip;\n\tconst auto limited = std::max(\n\t\tstd::min(int(_fullWidth), st::chatSimilarWidthMax),\n\t\tmin);\n\tif (limited > _fullWidth) {\n\t\tconst auto shift = (limited - _fullWidth) / 2;\n\t\tfor (auto &channel : _channels) {\n\t\t\tchannel.geometry.translate(shift, 0);\n\t\t}\n\t}\n\treturn { limited, bubble.height() };\n}\n\nQSize SimilarChannels::countCurrentSize(int newWidth) {\n\tif (!_toggled) {\n\t\treturn {};\n\t}\n\t_scrollMax = std::max(int(_fullWidth) - newWidth, 0);\n\t_scrollLeft = std::clamp(_scrollLeft, uint32(), _scrollMax);\n\t_hasViewAll = (_scrollMax != 0) ? 1 : 0;\n\treturn { newWidth, minHeight() };\n}\n\nbool SimilarChannels::hasHeavyPart() const {\n\treturn _hasHeavyPart != 0;\n}\n\nvoid SimilarChannels::unloadHeavyPart() {\n\t_hasHeavyPart = 0;\n\tfor (const auto &channel : _channels) {\n\t\tchannel.subscribed = 0;\n\t\tchannel.thumbnail->subscribeToUpdates(nullptr);\n\t}\n\tfor (const auto &thumbnail : _moreThumbnails) {\n\t\tif (thumbnail) {\n\t\t\tthumbnail->subscribeToUpdates(nullptr);\n\t\t}\n\t}\n}\n\nbool SimilarChannels::consumeHorizontalScroll(QPoint position, int delta) {\n\tif (_scrollMax == 0) {\n\t\treturn false;\n\t}\n\tconst auto left = _scrollLeft;\n\t_scrollLeft = std::clamp(\n\t\tint(_scrollLeft) - delta,\n\t\t0,\n\t\tint(_scrollMax));\n\tif (_scrollLeft == left) {\n\t\treturn false;\n\t}\n\trepaint();\n\treturn true;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_similar_channels.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n\nnamespace Ui {\nclass DynamicImage;\nclass RippleAnimation;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass SimilarChannels final : public Media {\npublic:\n\texplicit SimilarChannels(not_null<Element*> parent);\n\t~SimilarChannels();\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\n\tvoid clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool active) override;\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed) override;\n\n\tbool toggleSelectionByHandlerClick(\n\t\t\tconst ClickHandlerPtr &p) const override {\n\t\treturn false;\n\t}\n\tbool dragItemByHandler(const ClickHandlerPtr &p) const override {\n\t\treturn false;\n\t}\n\n\tbool needsBubble() const override {\n\t\treturn false;\n\t}\n\tbool customInfoLayout() const override {\n\t\treturn true;\n\t}\n\tbool isDisplayed() const override {\n\t\treturn !_empty && _toggled;\n\t}\n\n\tvoid unloadHeavyPart() override;\n\tbool hasHeavyPart() const override;\n\n\tbool consumeHorizontalScroll(QPoint position, int delta) override;\n\nprivate:\n\tstruct Channel {\n\t\tQRect geometry;\n\t\tUi::Text::String name;\n\t\tstd::shared_ptr<Ui::DynamicImage> thumbnail;\n\t\tClickHandlerPtr link;\n\t\tQString counter;\n\t\tmutable QRect counterRect;\n\t\tmutable QImage counterBg;\n\t\tmutable std::unique_ptr<Ui::RippleAnimation> ripple;\n\t\tuint32 more : 29 = 0;\n\t\tmutable uint32 moreLocked : 1 = 0;\n\t\tmutable uint32 subscribed : 1 = 0;\n\t\tmutable uint32 counterBgValid : 1 = 0;\n\t};\n\n\tvoid ensureCacheReady(QSize size) const;\n\tvoid validateLastPremiumLock() const;\n\tvoid fillMoreThumbnails() const;\n\tvoid validateCounterBg(const Channel &channel) const;\n\t[[nodiscard]] ClickHandlerPtr ensureToggleLink() const;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\n\tQString _title, _viewAll;\n\tmutable QImage _roundedCache;\n\tmutable std::array<QImage, 4> _roundedCorners;\n\tmutable QPoint _lastPoint;\n\tuint32 _titleWidth : 15 = 0;\n\tmutable uint32 _moreThumbnailsValid : 1 = 0;\n\tuint32 _viewAllWidth : 15 = 0;\n\tuint32 _fullWidth : 15 = 0;\n\tuint32 _empty : 1 = 0;\n\tmutable uint32 _toggled : 1 = 0;\n\tuint32 _scrollLeft : 15 = 0;\n\tuint32 _scrollMax : 15 = 0;\n\tuint32 _hasViewAll : 1 = 0;\n\tmutable uint32 _hasHeavyPart : 1 = 0;\n\n\tstd::vector<Channel> _channels;\n\tmutable std::array<std::shared_ptr<Ui::DynamicImage>, 2> _moreThumbnails;\n\tmutable ClickHandlerPtr _viewAllLink;\n\tmutable ClickHandlerPtr _toggleLink;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_slot_machine.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_slot_machine.h\"\n\n#include \"data/data_session.h\"\n#include \"chat_helpers/stickers_dice_pack.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/view/history_view_element.h\"\n#include \"main/main_session.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kStartBackIndex = 0;\nconstexpr auto kWinBackIndex = 1;\nconstexpr auto kPullIndex = 2;\nconstexpr auto kShifts = std::array<int, 3>{ 3, 9, 15 };\nconstexpr auto kSevenWinIndex = 0;\nconstexpr auto kSevenIndex = 1;\nconstexpr auto kBarIndex = 2;\nconstexpr auto kBerriesIndex = 3;\nconstexpr auto kLemonIndex = 4;\nconstexpr auto kStartIndex = 5;\nconstexpr auto kWinValue = 64;\nconstexpr auto kSkipFramesBeforeWinEnding = 90;\n\nconst auto &kEmoji = ::Stickers::DicePacks::kSlotString;\n\n[[nodiscard]] DocumentData *Lookup(\n\t\tnot_null<Element*> view,\n\t\tint value) {\n\tconst auto &session = view->history()->session();\n\treturn session.diceStickersPacks().lookup(kEmoji, value);\n}\n\n[[nodiscard]] int ComplexIndex(int partIndex, int inPartIndex) {\n\tExpects(partIndex >= 0 && partIndex < 3);\n\n\treturn kShifts[partIndex] + inPartIndex;\n}\n\n[[nodiscard]] int ComputePartValue(int value, int partIndex) {\n\treturn ((value - 1) >> (partIndex * 2)) & 0x03; // 0..3\n}\n\n[[nodiscard]] int ComputeComplexIndex(int value, int partIndex) {\n\tExpects(value > 0 && value <= 64);\n\n\tif (value == kWinValue) {\n\t\treturn ComplexIndex(partIndex, kSevenWinIndex);\n\t}\n\treturn ComplexIndex(partIndex, [&] {\n\t\tswitch (ComputePartValue(value, partIndex)) {\n\t\tcase 0: return kBarIndex;\n\t\tcase 1: return kBerriesIndex;\n\t\tcase 2: return kLemonIndex;\n\t\tcase 3: return kSevenIndex;\n\t\t}\n\t\tUnexpected(\"Part value value in ComputeComplexIndex.\");\n\t}());\n}\n\n} // namespace\n\nSlotMachine::SlotMachine(\n\tnot_null<Element*> parent,\n\tnot_null<Data::MediaDice*> dice)\n: _parent(parent)\n, _dice(dice)\n, _link(dice->makeHandler()) {\n\tresolveStarts();\n\t_showLastFrame = _parent->data()->Has<HistoryMessageForwarded>();\n\tif (_showLastFrame) {\n\t\tfor (auto &drawingEnd : _drawingEnd) {\n\t\t\tdrawingEnd = true;\n\t\t}\n\t}\n}\n\nSlotMachine::~SlotMachine() = default;\n\nvoid SlotMachine::resolve(\n\t\tstd::optional<Sticker> &sticker,\n\t\tint singleTimeIndex,\n\t\tint index,\n\t\tbool initSize) const {\n\tif (sticker) {\n\t\treturn;\n\t}\n\tconst auto document = Lookup(_parent, index);\n\tif (!document) {\n\t\treturn;\n\t}\n\tconst auto skipPremiumEffect = false;\n\tsticker.emplace(_parent, document, skipPremiumEffect);\n\tsticker->setDiceIndex(kEmoji, singleTimeIndex);\n\tif (initSize) {\n\t\tsticker->initSize();\n\t}\n}\n\nvoid SlotMachine::resolveStarts(bool initSize) {\n\tresolve(_pull, kPullIndex, kPullIndex, initSize);\n\tresolve(_start[0], 0, kStartBackIndex, initSize);\n\tfor (auto i = 0; i != 3; ++i) {\n\t\tresolve(_start[i + 1], 0, ComplexIndex(i, kStartIndex), initSize);\n\t}\n}\n\nvoid SlotMachine::resolveEnds(int value) {\n\tif (value <= 0 || value > 64) {\n\t\treturn;\n\t}\n\tconst auto firstPartValue = ComputePartValue(value, 0);\n\tif (ComputePartValue(value, 1) == firstPartValue\n\t\t&& ComputePartValue(value, 2) == firstPartValue) { // Three in a row.\n\t\tresolve(_end[0], kWinBackIndex, kWinBackIndex, true);\n\t}\n\tfor (auto i = 0; i != 3; ++i) {\n\t\tconst auto index = ComputeComplexIndex(value, i);\n\t\tresolve(_end[i + 1], index, index, true);\n\t}\n}\n\nbool SlotMachine::isEndResolved() const {\n\tfor (auto i = 0; i != 3; ++i) {\n\t\tif (!_end[i + 1]) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn _end[0].has_value() || (_dice->value() != kWinValue);\n}\n\nQSize SlotMachine::countOptimalSize() {\n\treturn _pull ? _pull->countOptimalSize() : Sticker::EmojiSize();\n}\n\nClickHandlerPtr SlotMachine::link() {\n\treturn _link;\n}\n\nvoid SlotMachine::draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &r) {\n\tresolveStarts(true);\n\tresolveEnds(_dice->value());\n\n\t//const auto endResolved = isEndResolved();\n\t//if (!endResolved) {\n\t//\tfor (auto &drawingEnd : _drawingEnd) {\n\t//\t\tdrawingEnd = false;\n\t//\t}\n\t//}\n\tauto switchedToEnd = _drawingEnd;\n\tconst auto pullReady = _pull && _pull->readyToDrawAnimationFrame();\n\tconst auto paintReady = [&] {\n\t\tauto result = pullReady;\n\t\tauto allPlayedEnough = true;\n\t\tfor (auto i = 1; i != 4; ++i) {\n\t\t\tif (!_end[i] || !_end[i]->readyToDrawAnimationFrame()) {\n\t\t\t\tswitchedToEnd[i] = false;\n\t\t\t}\n\t\t\tif (!switchedToEnd[i]\n\t\t\t\t&& (!_start[i] || !_start[i]->readyToDrawAnimationFrame())) {\n\t\t\t\tresult = false;\n\t\t\t}\n\t\t\tconst auto playedTillFrame = !switchedToEnd[i]\n\t\t\t\t? 0\n\t\t\t\t: _end[i]->frameIndex().value_or(0);\n\t\t\tif (playedTillFrame < kSkipFramesBeforeWinEnding) {\n\t\t\t\tallPlayedEnough = false;\n\t\t\t}\n\t\t}\n\t\tif (!_end[0]\n\t\t\t|| !_end[0]->readyToDrawAnimationFrame()\n\t\t\t|| !allPlayedEnough) {\n\t\t\tswitchedToEnd[0] = false;\n\t\t}\n\t\tif (ranges::contains(switchedToEnd, false)\n\t\t\t&& (!_start[0] || !_start[0]->readyToDrawAnimationFrame())) {\n\t\t\tresult = false;\n\t\t}\n\t\treturn result;\n\t}();\n\n\tif (!paintReady) {\n\t\treturn;\n\t}\n\n\tfor (auto i = 0; i != 4; ++i) {\n\t\tif (switchedToEnd[i]) {\n\t\t\t_end[i]->draw(p, context, r);\n\t\t} else {\n\t\t\t_start[i]->draw(p, context, r);\n\t\t\tif (_end[i]\n\t\t\t\t&& _end[i]->readyToDrawAnimationFrame()\n\t\t\t\t&& _start[i]->atTheEnd()) {\n\t\t\t\t_drawingEnd[i] = true;\n\t\t\t}\n\t\t}\n\t}\n\t_pull->draw(p, context, r);\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_slot_machine.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media_unwrapped.h\"\n#include \"history/view/media/history_view_sticker.h\"\n\nnamespace Data {\nclass MediaDice;\n} // namespace Data\n\nnamespace HistoryView {\n\nclass SlotMachine final : public UnwrappedMedia::Content {\npublic:\n\tSlotMachine(not_null<Element*> parent, not_null<Data::MediaDice*> dice);\n\t~SlotMachine();\n\n\tQSize countOptimalSize() override;\n\tvoid draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &r) override;\n\n\tClickHandlerPtr link() override;\n\n\tbool hasHeavyPart() const override {\n\t\tif (_pull && _pull->hasHeavyPart()) {\n\t\t\treturn true;\n\t\t}\n\t\tfor (auto i = 0; i != 4; ++i) {\n\t\t\tif ((_start[i] && _start[i]->hasHeavyPart())\n\t\t\t\t|| (_end[i] && _end[i]->hasHeavyPart())) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\tvoid unloadHeavyPart() override {\n\t\tif (_pull) {\n\t\t\t_pull->unloadHeavyPart();\n\t\t}\n\t\tfor (auto i = 0; i != 4; ++i) {\n\t\t\tif (_start[i]) {\n\t\t\t\t_start[i]->unloadHeavyPart();\n\t\t\t}\n\t\t\tif (_end[i]) {\n\t\t\t\t_end[i]->unloadHeavyPart();\n\t\t\t}\n\t\t}\n\t}\n\nprivate:\n\tvoid resolveStarts(bool initSize = false);\n\tvoid resolveEnds(int value);\n\t[[nodiscard]] bool isEndResolved() const;\n\tvoid resolve(\n\t\tstd::optional<Sticker> &sticker,\n\t\tint singleTimeIndex,\n\t\tint index,\n\t\tbool initSize) const;\n\n\tconst not_null<Element*> _parent;\n\tconst not_null<Data::MediaDice*> _dice;\n\tClickHandlerPtr _link;\n\tstd::optional<Sticker> _pull;\n\tstd::array<std::optional<Sticker>, 4> _start;\n\tstd::array<std::optional<Sticker>, 4> _end;\n\tmutable bool _showLastFrame = false;\n\tmutable std::array<bool, 4> _drawingEnd = { { false } };\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_sticker.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_sticker.h\"\n\n#include \"base/options.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"history/history.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/media/history_view_media_common.h\"\n#include \"history/view/media/history_view_sticker_player.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/image/image.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/text/custom_emoji_instance.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/click_handler_types.h\"\n#include \"window/window_session_controller.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_file_origin.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kMaxSizeFixed = 512;\nconstexpr auto kMaxEmojiSizeFixed = 256;\nconstexpr auto kPremiumMultiplier = (1 + 0.245 * 2);\nconstexpr auto kEmojiMultiplier = 3;\nconstexpr auto kMessageEffectMultiplier = 2;\n\nbase::options::option<int> OptionStickerSize({\n\t.id = \"sticker-size\",\n\t.name = \"Sticker size\",\n});\n\n[[nodiscard]] QImage CacheDiceImage(\n\t\tconst QString &emoji,\n\t\tint index,\n\t\tconst QImage &image) {\n\tstatic auto Cache = base::flat_map<std::pair<QString, int>, QImage>();\n\tconst auto key = std::make_pair(emoji, index);\n\tconst auto i = Cache.find(key);\n\tif (i != end(Cache) && i->second.size() == image.size()) {\n\t\treturn i->second;\n\t}\n\tCache[key] = image;\n\treturn image;\n}\n\n[[nodiscard]] QColor ComputeEmojiTextColor(const PaintContext &context) {\n\tconst auto st = context.st;\n\tconst auto result = st->messageStyle(false, false).historyTextFg->c;\n\tif (!context.selected()) {\n\t\treturn result;\n\t}\n\tconst auto &add = st->msgStickerOverlay()->c;\n\n\tconst auto ca = add.alpha();\n\tconst auto ra = 0x100 - ca;\n\tconst auto aa = ca + 1;\n\tconst auto red = (result.red() * ra + add.red() * aa) >> 8;\n\tconst auto green = (result.green() * ra + add.green() * aa) >> 8;\n\tconst auto blue = (result.blue() * ra + add.blue() * aa) >> 8;\n\treturn QColor(red, green, blue, result.alpha());\n}\n\n} // namespace\n\nSticker::Sticker(\n\tnot_null<Element*> parent,\n\tnot_null<DocumentData*> data,\n\tbool skipPremiumEffect,\n\tElement *replacing,\n\tconst Lottie::ColorReplacements *replacements)\n: _parent(parent)\n, _data(data)\n, _replacements(replacements)\n, _cachingTag(ChatHelpers::StickerLottieSize::MessageHistory)\n, _skipPremiumEffect(skipPremiumEffect)\n, _sensitiveBlurred(parent->data()->isMediaSensitive()) {\n\tif ((_dataMedia = _data->activeMediaView())) {\n\t\tdataMediaCreated();\n\t} else {\n\t\t_data->loadThumbnail(parent->data()->fullId());\n\t\tif (hasPremiumEffect()) {\n\t\t\t_data->loadVideoThumbnail(parent->data()->fullId());\n\t\t}\n\t}\n\tif (const auto media = replacing ? replacing->media() : nullptr) {\n\t\t_player = media->stickerTakePlayer(_data, _replacements);\n\t\tif (_player) {\n\t\t\tif (hasPremiumEffect() && !_premiumEffectPlayed) {\n\t\t\t\t_premiumEffectPlayed = true;\n\t\t\t\tif (On(PowerSaving::kStickersChat)\n\t\t\t\t\t&& !_premiumEffectSkipped) {\n\t\t\t\t\t_premiumEffectSkipped = true;\n\t\t\t\t} else {\n\t\t\t\t\t_parent->delegate()->elementStartPremium(\n\t\t\t\t\t\t_parent,\n\t\t\t\t\t\treplacing);\n\t\t\t\t}\n\t\t\t}\n\t\t\tplayerCreated();\n\t\t}\n\t}\n}\n\nSticker::~Sticker() {\n\tif (_player || _dataMedia) {\n\t\tif (_player) {\n\t\t\tunloadPlayer();\n\t\t}\n\t\tif (_dataMedia) {\n\t\t\t_data->owner().keepAlive(base::take(_dataMedia));\n\t\t\t_parent->checkHeavyPart();\n\t\t}\n\t}\n}\n\nbool Sticker::hasPremiumEffect() const {\n\treturn !_skipPremiumEffect && _data->isPremiumSticker();\n}\n\nbool Sticker::customEmojiPart() const {\n\treturn _customEmojiPart;\n}\n\nbool Sticker::emojiSticker() const {\n\treturn _emojiSticker;\n}\n\nbool Sticker::webpagePart() const {\n\treturn _webpagePart;\n}\n\nvoid Sticker::initSize(int customSize) {\n\tif (customSize > 0) {\n\t\tconst auto original = Size(_data);\n\t\tconst auto proposed = QSize{ customSize, customSize };\n\t\t_size = original.isEmpty()\n\t\t\t? proposed\n\t\t\t: DownscaledSize(original, proposed);\n\t} else if (emojiSticker() || _diceIndex >= 0) {\n\t\t_size = EmojiSize();\n\t\tif (_diceIndex > 0) {\n\t\t\t[[maybe_unused]] bool result = readyToDrawAnimationFrame();\n\t\t}\n\t} else {\n\t\t_size = Size(_data);\n\t}\n\t_size = DownscaledSize(_size, Size());\n}\n\nQSize Sticker::countOptimalSize() {\n\tif (_size.isEmpty()) {\n\t\tinitSize();\n\t}\n\treturn _size;\n}\n\nbool Sticker::readyToDrawAnimationFrame() {\n\tif (!_lastFrameCached.isNull()) {\n\t\treturn true;\n\t}\n\tconst auto sticker = _data->sticker();\n\tif (!sticker || _sensitiveBlurred) {\n\t\treturn false;\n\t}\n\n\tensureDataMediaCreated();\n\t_dataMedia->checkStickerLarge();\n\tconst auto loaded = _dataMedia->loaded();\n\tconst auto waitingForPremium = hasPremiumEffect()\n\t\t&& _dataMedia->videoThumbnailContent().isEmpty();\n\tif (!_player && loaded && !waitingForPremium && sticker->isAnimated()) {\n\t\tsetupPlayer();\n\t}\n\treturn ready();\n}\n\nQSize Sticker::Size() {\n\tconst auto side = std::min(st::maxStickerSize, kMaxSizeFixed);\n\tif (OptionStickerSize.value() > 0) [[unlikely]] {\n\t\tconst auto scaled = std::clamp(\n\t\t\tOptionStickerSize.value(),\n\t\t\tstyle::ConvertScale(50),\n\t\t\tside);\n\t\treturn { scaled, scaled };\n\t}\n\treturn { side, side };\n}\n\nQSize Sticker::Size(not_null<DocumentData*> document) {\n\treturn DownscaledSize(document->dimensions, Size());\n}\n\nQSize Sticker::PremiumEffectSize(not_null<DocumentData*> document) {\n\treturn Size(document) * kPremiumMultiplier;\n}\n\nQSize Sticker::UsualPremiumEffectSize() {\n\treturn DownscaledSize({ kMaxSizeFixed, kMaxSizeFixed }, Size())\n\t\t* kPremiumMultiplier;\n}\n\nQSize Sticker::EmojiEffectSize() {\n\treturn EmojiSize() * kEmojiMultiplier;\n}\n\nQSize Sticker::MessageEffectSize() {\n\treturn EmojiSize() * kMessageEffectMultiplier;\n}\n\nQSize Sticker::EmojiSize() {\n\tconst auto side = std::min(st::maxAnimatedEmojiSize, kMaxEmojiSizeFixed);\n\treturn { side, side };\n}\n\nvoid Sticker::draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &r) {\n\tif (!customEmojiPart()) {\n\t\t_parent->clearCustomEmojiRepaint();\n\t}\n\n\tensureDataMediaCreated();\n\tif (readyToDrawAnimationFrame()) {\n\t\tpaintAnimationFrame(p, context, r);\n\t} else if (!_data->sticker()\n\t\t|| (_data->sticker()->isLottie() && _replacements)\n\t\t|| !paintPixmap(p, context, r)) {\n\t\tpaintPath(p, context, r);\n\t}\n\tif (_sensitiveBlurred) {\n\t\tpaintSensitiveTag(p, context, r);\n\t}\n}\n\nvoid Sticker::paintSensitiveTag(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &r) {\n\tauto text = Ui::Text::String();\n\tauto iconSkip = 0;\n\ttext.setText(\n\t\tst::semiboldTextStyle,\n\t\ttr::lng_sensitive_tag(tr::now));\n\ticonSkip = st::mediaMenuIconStealth.width() * 1.4;\n\tconst auto width = iconSkip + text.maxWidth();\n\tconst auto inner = QRect(0, 0, width, text.minHeight());\n\tconst auto outer = style::centerrect(\n\t\tr,\n\t\tinner.marginsAdded(st::paidTagPadding));\n\tconst auto size = outer.size();\n\tconst auto real = outer.marginsRemoved(st::paidTagPadding);\n\tconst auto radius = std::min(size.width(), size.height()) / 2;\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(context.st->msgServiceBg());\n\tp.drawRoundedRect(outer, radius, radius);\n\tp.setPen(context.st->msgServiceFg());\n\tif (iconSkip) {\n\t\tst::mediaMenuIconStealth.paint(\n\t\t\tp,\n\t\t\treal.x(),\n\t\t\t(outer.y()\n\t\t\t\t+ (size.height() - st::mediaMenuIconStealth.height()) / 2),\n\t\t\touter.width(),\n\t\t\tcontext.st->msgServiceFg()->c);\n\t}\n\ttext.draw(p, real.x() + iconSkip, real.y(), width);\n}\n\nClickHandlerPtr Sticker::link() {\n\treturn _link;\n}\n\nbool Sticker::ready() const {\n\treturn _player && _player->ready();\n}\n\nDocumentData *Sticker::document() {\n\treturn _data;\n}\n\nbool Sticker::stoppedOnLastFrame() const {\n\treturn _stopOnLastFrame && (!_lastFrameCached.isNull() || atTheEnd());\n}\n\nvoid Sticker::stickerClearLoopPlayed() {\n\tif (!_playingOnce) {\n\t\t_oncePlayed = false;\n\t}\n\t_premiumEffectSkipped = false;\n}\n\nvoid Sticker::paintAnimationFrame(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &r) {\n\tconst auto colored = (customEmojiPart() && _data->emojiUsesTextColor())\n\t\t? ComputeEmojiTextColor(context)\n\t\t: (context.selected() && !_nextLastFrame)\n\t\t? context.st->msgStickerOverlay()->c\n\t\t: QColor(0, 0, 0, 0);\n\tconst auto powerSavingFlag = emojiSticker()\n\t\t? PowerSaving::kEmojiChat\n\t\t: PowerSaving::kStickersChat;\n\tconst auto paused = context.paused\n\t\t|| (_diceIndex < 0 && On(powerSavingFlag));\n\tconst auto frame = _player\n\t\t? _player->frame(\n\t\t\t_size,\n\t\t\tcolored,\n\t\t\tmirrorHorizontal(),\n\t\t\tcontext.now,\n\t\t\tpaused)\n\t\t: StickerPlayer::FrameInfo();\n\tif (_nextLastFrame) {\n\t\t_nextLastFrame = false;\n\t\t_lastFrameCached = (_diceIndex > 0)\n\t\t\t? CacheDiceImage(_diceEmoji, _diceIndex, frame.image)\n\t\t\t: frame.image;\n\t}\n\tconst auto &image = _lastFrameCached.isNull()\n\t\t? frame.image\n\t\t: _lastFrameCached;\n\tconst auto prepared = (!_lastFrameCached.isNull() && context.selected())\n\t\t? Images::Colored(\n\t\t\tbase::duplicate(image),\n\t\t\tcontext.st->msgStickerOverlay()->c)\n\t\t: image;\n\tconst auto size = prepared.size() / style::DevicePixelRatio();\n\tp.drawImage(\n\t\tQRect(\n\t\t\tQPoint(\n\t\t\t\tr.x() + (r.width() - size.width()) / 2,\n\t\t\t\tr.y() + (r.height() - size.height()) / 2),\n\t\t\tsize),\n\t\tprepared);\n\tif (!_lastFrameCached.isNull()) {\n\t\treturn;\n\t}\n\n\tconst auto count = _player->framesCount();\n\t_frameIndex = frame.index;\n\t_framesCount = count;\n\t_nextLastFrame = !paused\n\t\t&& _stopOnLastFrame\n\t\t&& (_frameIndex + 2 == count);\n\tconst auto playOnce = _playingOnce\n\t\t? true\n\t\t: (_diceIndex == 0)\n\t\t? false\n\t\t: ((!customEmojiPart() && emojiSticker())\n\t\t\t|| !Core::App().settings().loopAnimatedStickers());\n\tconst auto lastFrame = _stopOnLastFrame && atTheEnd();\n\tconst auto switchToNext = !playOnce\n\t\t|| (!lastFrame && (_frameIndex != 0 || !_oncePlayed));\n\tif (!paused\n\t\t&& switchToNext\n\t\t&& _player->markFrameShown()\n\t\t&& playOnce\n\t\t&& !_oncePlayed) {\n\t\t_oncePlayed = true;\n\t\t_parent->delegate()->elementStartStickerLoop(_parent);\n\t}\n\tcheckPremiumEffectStart();\n}\n\nbool Sticker::paintPixmap(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &r) {\n\tconst auto pixmap = paintedPixmap(context);\n\tif (pixmap.isNull()) {\n\t\treturn false;\n\t}\n\tconst auto size = pixmap.size() / pixmap.devicePixelRatio();\n\tconst auto position = QPoint(\n\t\tr.x() + (r.width() - size.width()) / 2,\n\t\tr.y() + (r.height() - size.height()) / 2);\n\tconst auto mirror = mirrorHorizontal();\n\tif (mirror) {\n\t\tp.save();\n\t\tconst auto middle = QPointF(\n\t\t\tposition.x() + size.width() / 2.,\n\t\t\tposition.y() + size.height() / 2.);\n\t\tp.translate(middle);\n\t\tp.scale(-1., 1.);\n\t\tp.translate(-middle);\n\t}\n\tp.drawPixmap(position, pixmap);\n\tif (mirror) {\n\t\tp.restore();\n\t}\n\treturn true;\n}\n\nvoid Sticker::paintPath(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &r) {\n\tconst auto pathGradient = _parent->delegate()->elementPathShiftGradient();\n\tauto helper = std::optional<style::owned_color>();\n\tif (customEmojiPart() && _data->emojiUsesTextColor()) {\n\t\thelper.emplace(Ui::CustomEmoji::PreviewColorFromTextColor(\n\t\t\tComputeEmojiTextColor(context)));\n\t\tpathGradient->overrideColors(helper->color(), helper->color());\n\t} else if (webpagePart()) {\n\t\tpathGradient->overrideColors(st::shadowFg, st::shadowFg);\n\t} else if (context.selected()) {\n\t\tpathGradient->overrideColors(\n\t\t\tcontext.st->msgServiceBgSelected(),\n\t\t\tcontext.st->msgServiceBg());\n\t} else {\n\t\tpathGradient->clearOverridenColors();\n\t}\n\tp.setBrush(context.imageStyle()->msgServiceBg);\n\tChatHelpers::PaintStickerThumbnailPath(\n\t\tp,\n\t\t_dataMedia.get(),\n\t\tr,\n\t\tpathGradient,\n\t\tmirrorHorizontal());\n\tif (helper) {\n\t\tpathGradient->clearOverridenColors();\n\t}\n}\n\nQPixmap Sticker::paintedPixmap(const PaintContext &context) const {\n\tauto helper = std::optional<style::owned_color>();\n\tconst auto sticker = _data->sticker();\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto adjust = [&](int side) {\n\t\treturn (((side * ratio) / 8) * 8) / ratio;\n\t};\n\tconst auto useSize = (sticker && sticker->type == StickerType::Tgs)\n\t\t? QSize(adjust(_size.width()), adjust(_size.height()))\n\t\t: _size;\n\tconst auto colored = (customEmojiPart() && _data->emojiUsesTextColor())\n\t\t? &helper.emplace(ComputeEmojiTextColor(context)).color()\n\t\t: context.selected()\n\t\t? &context.st->msgStickerOverlay()\n\t\t: nullptr;\n\tconst auto good = _sensitiveBlurred\n\t\t? nullptr\n\t\t: _dataMedia->goodThumbnail();\n\tconst auto image = _sensitiveBlurred\n\t\t? nullptr\n\t\t: _dataMedia->getStickerLarge();\n\tif (image) {\n\t\treturn image->pix(useSize, { .colored = colored });\n\t//\n\t// Inline thumbnails can't have alpha channel.\n\t//\n\t//} else if (const auto blurred = _data->thumbnailInline()) {\n\t//\treturn blurred->pix(\n\t//\t\tuseSize,\n\t//\t\t{ .colored = colored, .options = Images::Option::Blur });\n\t} else if (good) {\n\t\treturn good->pix(useSize, { .colored = colored });\n\t} else if (const auto thumbnail = _dataMedia->thumbnail()) {\n\t\treturn thumbnail->pix(\n\t\t\tuseSize,\n\t\t\t{ .colored = colored, .options = Images::Option::Blur });\n\t}\n\treturn QPixmap();\n}\n\nbool Sticker::mirrorHorizontal() const {\n\tif (!hasPremiumEffect()) {\n\t\treturn false;\n\t}\n\tconst auto rightAligned = _parent->hasRightLayout();\n\treturn !rightAligned;\n}\n\nClickHandlerPtr Sticker::ShowSetHandler(not_null<DocumentData*> document) {\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto window = my.sessionWindow.get()) {\n\t\t\tStickerSetBox::Show(window->uiShow(), document);\n\t\t}\n\t});\n}\n\nvoid Sticker::refreshLink() {\n\tif (_link) {\n\t\treturn;\n\t}\n\tconst auto sticker = _data->sticker();\n\tif (_sensitiveBlurred) {\n\t\t_link = MakeSensitiveMediaLink(nullptr, _parent->data());\n\t} else if (emojiSticker()) {\n\t\tconst auto weak = base::make_weak(this);\n\t\t_link = std::make_shared<LambdaClickHandler>([weak] {\n\t\t\tif (const auto that = weak.get()) {\n\t\t\t\tthat->emojiStickerClicked();\n\t\t\t}\n\t\t});\n\t} else if (sticker && sticker->set) {\n\t\tif (hasPremiumEffect()) {\n\t\t\tconst auto weak = base::make_weak(this);\n\t\t\t_link = std::make_shared<LambdaClickHandler>([weak] {\n\t\t\t\tif (const auto that = weak.get()) {\n\t\t\t\t\tthat->premiumStickerClicked();\n\t\t\t\t}\n\t\t\t});\n\t\t} else {\n\t\t\t_link = ShowSetHandler(_data);\n\t\t}\n\t} else if (sticker\n\t\t&& (_data->dimensions.width() > kStickerSideSize\n\t\t\t|| _data->dimensions.height() > kStickerSideSize)\n\t\t&& !_parent->data()->isSending()\n\t\t&& !_parent->data()->hasFailed()) {\n\t\t// In case we have a .webp file that is displayed as a sticker, but\n\t\t// that doesn't fit in 512x512, we assume it may be a regular large\n\t\t// .webp image and we allow to open it in media viewer.\n\t\t_link = std::make_shared<DocumentOpenClickHandler>(\n\t\t\t_data,\n\t\t\tcrl::guard(this, [=](FullMsgId id) {\n\t\t\t\t_parent->delegate()->elementOpenDocument(_data, id);\n\t\t\t}),\n\t\t\t_parent->data()->fullId());\n\t}\n}\n\nvoid Sticker::emojiStickerClicked() {\n\tif (_player) {\n\t\t_parent->delegate()->elementStartInteraction(_parent);\n\t}\n\t_oncePlayed = false;\n\t_parent->history()->owner().requestViewRepaint(_parent);\n}\n\nvoid Sticker::premiumStickerClicked() {\n\t_premiumEffectPlayed = false;\n\n\t// Remove when we start playing sticker itself on click.\n\t_premiumEffectSkipped = false;\n\n\t_parent->history()->owner().requestViewRepaint(_parent);\n}\n\nvoid Sticker::ensureDataMediaCreated() const {\n\tif (_dataMedia) {\n\t\treturn;\n\t}\n\t_dataMedia = _data->createMediaView();\n\tdataMediaCreated();\n}\n\nvoid Sticker::dataMediaCreated() const {\n\tExpects(_dataMedia != nullptr);\n\n\t_dataMedia->goodThumbnailWanted();\n\tif (_dataMedia->thumbnailPath().isEmpty()) {\n\t\t_dataMedia->thumbnailWanted(_parent->data()->fullId());\n\t}\n\tif (hasPremiumEffect()) {\n\t\t_data->loadVideoThumbnail(_parent->data()->fullId());\n\t}\n\t_parent->history()->owner().registerHeavyViewPart(_parent);\n}\n\nvoid Sticker::setDiceIndex(const QString &emoji, int index) {\n\t_diceEmoji = emoji;\n\t_diceIndex = index;\n\t_playingOnce = (index > 0);\n\t_stopOnLastFrame = (index > 0);\n}\n\nvoid Sticker::setPlayingOnce(bool once) {\n\t_playingOnce = once;\n}\n\nvoid Sticker::setStopOnLastFrame(bool stop) {\n\t_stopOnLastFrame = stop;\n\t_playingOnce = true;\n}\n\nvoid Sticker::setCustomCachingTag(ChatHelpers::StickerLottieSize tag) {\n\t_cachingTag = tag;\n}\n\nvoid Sticker::setCustomEmojiPart() {\n\t_customEmojiPart = true;\n}\n\nvoid Sticker::setEmojiSticker() {\n\t_emojiSticker = true;\n}\n\nvoid Sticker::setWebpagePart() {\n\t_webpagePart = true;\n}\n\nvoid Sticker::setupPlayer() {\n\tExpects(_dataMedia != nullptr);\n\n\tif (_data->sticker()->isLottie()) {\n\t\t_player = std::make_unique<LottiePlayer>(\n\t\t\tChatHelpers::LottiePlayerFromDocument(\n\t\t\t\t_dataMedia.get(),\n\t\t\t\t_replacements,\n\t\t\t\t_cachingTag,\n\t\t\t\tcountOptimalSize() * style::DevicePixelRatio(),\n\t\t\t\tLottie::Quality::High));\n\t} else if (_data->sticker()->isWebm()) {\n\t\t_player = std::make_unique<WebmPlayer>(\n\t\t\t_dataMedia->owner()->location(),\n\t\t\t_dataMedia->bytes(),\n\t\t\tcountOptimalSize());\n\t}\n\n\tcheckPremiumEffectStart();\n\tplayerCreated();\n}\n\nvoid Sticker::checkPremiumEffectStart() {\n\tif (!_premiumEffectPlayed && hasPremiumEffect()) {\n\t\t_premiumEffectPlayed = true;\n\t\tif (On(PowerSaving::kStickersChat)\n\t\t\t&& !_premiumEffectSkipped) {\n\t\t\t_premiumEffectSkipped = true;\n\t\t} else {\n\t\t\t_parent->delegate()->elementStartPremium(_parent, nullptr);\n\t\t}\n\t}\n}\n\nvoid Sticker::playerCreated() {\n\tExpects(_player != nullptr);\n\n\t_parent->history()->owner().registerHeavyViewPart(_parent);\n\t_player->setRepaintCallback([=] { _parent->customEmojiRepaint(); });\n}\n\nbool Sticker::hasHeavyPart() const {\n\treturn _player || _dataMedia;\n}\n\nvoid Sticker::unloadHeavyPart() {\n\tunloadPlayer();\n\t_dataMedia = nullptr;\n}\n\nvoid Sticker::unloadPlayer() {\n\tif (!_player) {\n\t\treturn;\n\t}\n\tif (_stopOnLastFrame && _lastFrameCached.isNull()) {\n\t\t_nextLastFrame = false;\n\t\t_oncePlayed = false;\n\t}\n\t_player = nullptr;\n\tif (hasPremiumEffect()) {\n\t\t_parent->delegate()->elementCancelPremium(_parent);\n\t}\n\t_parent->checkHeavyPart();\n}\n\nstd::unique_ptr<StickerPlayer> Sticker::stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) {\n\treturn (data == _data && replacements == _replacements)\n\t\t? std::move(_player)\n\t\t: nullptr;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_sticker.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media_unwrapped.h\"\n#include \"history/view/media/history_view_sticker_player_abstract.h\"\n#include \"base/weak_ptr.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\nstruct FileOrigin;\nclass DocumentMedia;\n} // namespace Data\n\nnamespace Lottie {\nstruct ColorReplacements;\n} // namespace Lottie\n\nnamespace ChatHelpers {\nenum class StickerLottieSize : uint8;\n} // namespace ChatHelpers\n\nnamespace HistoryView {\n\nclass Sticker final\n\t: public UnwrappedMedia::Content\n\t, public base::has_weak_ptr {\npublic:\n\tSticker(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<DocumentData*> data,\n\t\tbool skipPremiumEffect,\n\t\tElement *replacing = nullptr,\n\t\tconst Lottie::ColorReplacements *replacements = nullptr);\n\t~Sticker();\n\n\tvoid initSize(int customSize = 0);\n\tQSize countOptimalSize() override;\n\tvoid draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &r) override;\n\tClickHandlerPtr link() override;\n\n\t[[nodiscard]] bool ready() const;\n\t[[nodiscard]] bool stoppedOnLastFrame() const;\n\tDocumentData *document() override;\n\tvoid stickerClearLoopPlayed() override;\n\tstd::unique_ptr<StickerPlayer> stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) override;\n\n\tbool hasHeavyPart() const override;\n\tvoid unloadHeavyPart() override;\n\n\tvoid refreshLink() override;\n\tbool hasTextForCopy() const override {\n\t\treturn emojiSticker();\n\t}\n\n\tvoid setDiceIndex(const QString &emoji, int index);\n\tvoid setPlayingOnce(bool once);\n\tvoid setStopOnLastFrame(bool stop);\n\tvoid setCustomCachingTag(ChatHelpers::StickerLottieSize tag);\n\tvoid setCustomEmojiPart();\n\tvoid setEmojiSticker();\n\tvoid setWebpagePart();\n\t[[nodiscard]] bool atTheEnd() const {\n\t\treturn \t(_frameIndex >= 0) && (_frameIndex + 1 == _framesCount);\n\t}\n\t[[nodiscard]] std::optional<int> frameIndex() const {\n\t\treturn (_frameIndex >= 0)\n\t\t\t? std::make_optional(_frameIndex)\n\t\t\t: std::nullopt;\n\t}\n\t[[nodiscard]] std::optional<int> framesCount() const {\n\t\treturn (_framesCount > 0)\n\t\t\t? std::make_optional(_framesCount)\n\t\t\t: std::nullopt;\n\t}\n\t[[nodiscard]] bool readyToDrawAnimationFrame();\n\n\t[[nodiscard]] static QSize Size();\n\t[[nodiscard]] static QSize Size(not_null<DocumentData*> document);\n\t[[nodiscard]] static QSize PremiumEffectSize(\n\t\tnot_null<DocumentData*> document);\n\t[[nodiscard]] static QSize UsualPremiumEffectSize();\n\t[[nodiscard]] static QSize EmojiEffectSize();\n\t[[nodiscard]] static QSize MessageEffectSize();\n\t[[nodiscard]] static QSize EmojiSize();\n\t[[nodiscard]] static ClickHandlerPtr ShowSetHandler(\n\t\tnot_null<DocumentData*> document);\n\nprivate:\n\t[[nodiscard]] bool hasPremiumEffect() const;\n\t[[nodiscard]] bool customEmojiPart() const;\n\t[[nodiscard]] bool emojiSticker() const;\n\t[[nodiscard]] bool webpagePart() const;\n\tvoid paintAnimationFrame(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &r);\n\tbool paintPixmap(Painter &p, const PaintContext &context, const QRect &r);\n\tvoid paintPath(Painter &p, const PaintContext &context, const QRect &r);\n\t[[nodiscard]] QPixmap paintedPixmap(const PaintContext &context) const;\n\t[[nodiscard]] bool mirrorHorizontal() const;\n\tvoid paintSensitiveTag(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &r);\n\n\tvoid ensureDataMediaCreated() const;\n\tvoid dataMediaCreated() const;\n\n\tvoid setupPlayer();\n\tvoid playerCreated();\n\tvoid unloadPlayer();\n\tvoid emojiStickerClicked();\n\tvoid premiumStickerClicked();\n\tvoid checkPremiumEffectStart();\n\n\tconst not_null<Element*> _parent;\n\tconst not_null<DocumentData*> _data;\n\tconst Lottie::ColorReplacements *_replacements = nullptr;\n\tstd::unique_ptr<StickerPlayer> _player;\n\tmutable std::shared_ptr<Data::DocumentMedia> _dataMedia;\n\tClickHandlerPtr _link;\n\tQSize _size;\n\tQImage _lastFrameCached;\n\tQString _diceEmoji;\n\tint _diceIndex = -1;\n\tmutable int _frameIndex = -1;\n\tmutable int _framesCount = -1;\n\tChatHelpers::StickerLottieSize _cachingTag = {};\n\tmutable bool _oncePlayed : 1 = false;\n\tmutable bool _premiumEffectPlayed : 1 = false;\n\tmutable bool _premiumEffectSkipped : 1 = false;\n\tmutable bool _nextLastFrame : 1 = false;\n\tbool _skipPremiumEffect : 1 = false;\n\tbool _customEmojiPart : 1 = false;\n\tbool _emojiSticker : 1 = false;\n\tbool _webpagePart : 1 = false;\n\tbool _playingOnce : 1 = false;\n\tbool _stopOnLastFrame : 1 = false;\n\tbool _sensitiveBlurred : 1 = false;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_sticker_player.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_sticker_player.h\"\n\n#include \"core/file_location.h\"\n\nnamespace HistoryView {\nnamespace {\n\nusing ClipNotification = ::Media::Clip::Notification;\n\n} // namespace\n\nLottiePlayer::LottiePlayer(std::unique_ptr<Lottie::SinglePlayer> lottie)\n: _lottie(std::move(lottie)) {\n}\n\nvoid LottiePlayer::setRepaintCallback(Fn<void()> callback) {\n\tif (!callback) {\n\t\t_repaintLifetime.destroy();\n\t\treturn;\n\t}\n\t_repaintLifetime = _lottie->updates(\n\t) | rpl::on_next([=](Lottie::Update update) {\n\t\tv::match(update.data, [&](const Lottie::Information &) {\n\t\t\tcallback();\n\t\t\t//markFramesTillExternal();\n\t\t}, [&](const Lottie::DisplayFrameRequest &) {\n\t\t\tcallback();\n\t\t});\n\t});\n}\n\nbool LottiePlayer::ready() {\n\treturn _lottie->ready();\n}\n\nint LottiePlayer::framesCount() {\n\treturn _lottie->information().framesCount;\n}\n\nLottiePlayer::FrameInfo LottiePlayer::frame(\n\t\tQSize size,\n\t\tQColor colored,\n\t\tbool mirrorHorizontal,\n\t\tcrl::time now,\n\t\tbool paused) {\n\tauto request = Lottie::FrameRequest();\n\trequest.box = size * style::DevicePixelRatio();\n\trequest.colored = colored;\n\trequest.mirrorHorizontal = mirrorHorizontal;\n\tconst auto info = _lottie->frameInfo(request);\n\treturn { .image = info.image, .index = info.index };\n}\n\nbool LottiePlayer::markFrameShown() {\n\treturn _lottie->markFrameShown();\n}\n\nWebmPlayer::WebmPlayer(\n\tconst Core::FileLocation &location,\n\tconst QByteArray &data,\n\tQSize size)\n: _reader(\n\t::Media::Clip::MakeReader(location, data, [=](ClipNotification update) {\n\tclipCallback(update);\n}))\n, _size(size) {\n}\n\nvoid WebmPlayer::clipCallback(ClipNotification notification) {\n\tswitch (notification) {\n\tcase ClipNotification::Reinit: {\n\t\tif (_reader->state() == ::Media::Clip::State::Error) {\n\t\t\t_reader.setBad();\n\t\t} else if (_reader->ready() && !_reader->started()) {\n\t\t\t_reader->start({ .frame = _size, .keepAlpha = true });\n\t\t}\n\t} break;\n\n\tcase ClipNotification::Repaint: break;\n\t}\n\n\tif (const auto onstack = _repaintCallback) {\n\t\tonstack();\n\t}\n}\n\nvoid WebmPlayer::setRepaintCallback(Fn<void()> callback) {\n\t_repaintCallback = std::move(callback);\n}\n\nbool WebmPlayer::ready() {\n\treturn _reader && _reader->started();\n}\n\nint WebmPlayer::framesCount() {\n\treturn -1;\n}\n\nWebmPlayer::FrameInfo WebmPlayer::frame(\n\t\tQSize size,\n\t\tQColor colored,\n\t\tbool mirrorHorizontal,\n\t\tcrl::time now,\n\t\tbool paused) {\n\tauto request = ::Media::Clip::FrameRequest();\n\trequest.frame = size;\n\trequest.factor = style::DevicePixelRatio();\n\trequest.keepAlpha = true;\n\trequest.colored = colored;\n\tconst auto info = _reader->frameInfo(request, paused ? 0 : now);\n\treturn { .image = info.image, .index = info.index };\n}\n\nbool WebmPlayer::markFrameShown() {\n\treturn _reader->moveToNextFrame();\n}\n\nStaticStickerPlayer::StaticStickerPlayer(\n\tconst Core::FileLocation &location,\n\tconst QByteArray &data,\n\tQSize size)\n: _frame(Images::Read({\n\t.path = location.name(),\n\t.content = data,\n}).image) {\n\tif (!_frame.isNull()) {\n\t\tsize = _frame.size().scaled(size, Qt::KeepAspectRatio);\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t_frame = Images::Prepare(std::move(_frame), size * ratio, {});\n\t\t_frame.setDevicePixelRatio(ratio);\n\t}\n}\n\nvoid StaticStickerPlayer::setRepaintCallback(Fn<void()> callback) {\n\tif (callback) {\n\t\tcallback();\n\t}\n}\n\nbool StaticStickerPlayer::ready() {\n\treturn true;\n}\n\nint StaticStickerPlayer::framesCount() {\n\treturn 1;\n}\n\nStaticStickerPlayer::FrameInfo StaticStickerPlayer::frame(\n\t\tQSize size,\n\t\tQColor colored,\n\t\tbool mirrorHorizontal,\n\t\tcrl::time now,\n\t\tbool paused) {\n\treturn { _frame };\n}\n\nbool StaticStickerPlayer::markFrameShown() {\n\treturn false;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_sticker_player.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_sticker_player_abstract.h\"\n\n#include \"lottie/lottie_single_player.h\"\n#include \"media/clip/media_clip_reader.h\"\n\nnamespace Core {\nclass FileLocation;\n} // namespace Core\n\nnamespace HistoryView {\n\nclass LottiePlayer final : public StickerPlayer {\npublic:\n\texplicit LottiePlayer(std::unique_ptr<Lottie::SinglePlayer> lottie);\n\n\tvoid setRepaintCallback(Fn<void()> callback) override;\n\tbool ready() override;\n\tint framesCount() override;\n\tFrameInfo frame(\n\t\tQSize size,\n\t\tQColor colored,\n\t\tbool mirrorHorizontal,\n\t\tcrl::time now,\n\t\tbool paused) override;\n\tbool markFrameShown() override;\n\nprivate:\n\tstd::unique_ptr<Lottie::SinglePlayer> _lottie;\n\trpl::lifetime _repaintLifetime;\n\n};\n\nclass WebmPlayer final : public StickerPlayer {\npublic:\n\tWebmPlayer(\n\t\tconst Core::FileLocation &location,\n\t\tconst QByteArray &data,\n\t\tQSize size);\n\n\tvoid setRepaintCallback(Fn<void()> callback) override;\n\tbool ready() override;\n\tint framesCount() override;\n\tFrameInfo frame(\n\t\tQSize size,\n\t\tQColor colored,\n\t\tbool mirrorHorizontal,\n\t\tcrl::time now,\n\t\tbool paused) override;\n\tbool markFrameShown() override;\n\nprivate:\n\tvoid clipCallback(::Media::Clip::Notification notification);\n\n\t::Media::Clip::ReaderPointer _reader;\n\tFn<void()> _repaintCallback;\n\tQSize _size;\n\n};\n\nclass StaticStickerPlayer final : public StickerPlayer {\npublic:\n\tStaticStickerPlayer(\n\t\tconst Core::FileLocation &location,\n\t\tconst QByteArray &data,\n\t\tQSize size);\n\n\tvoid setRepaintCallback(Fn<void()> callback) override;\n\tbool ready() override;\n\tint framesCount() override;\n\tFrameInfo frame(\n\t\tQSize size,\n\t\tQColor colored,\n\t\tbool mirrorHorizontal,\n\t\tcrl::time now,\n\t\tbool paused) override;\n\tbool markFrameShown() override;\n\nprivate:\n\tQImage _frame;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_sticker_player_abstract.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace HistoryView {\n\nclass StickerPlayer {\npublic:\n\tvirtual ~StickerPlayer() = default;\n\n\tstruct FrameInfo {\n\t\tQImage image;\n\t\tint index = 0;\n\t};\n\tvirtual void setRepaintCallback(Fn<void()> callback) = 0;\n\t[[nodiscard]] virtual bool ready() = 0;\n\t[[nodiscard]] virtual int framesCount() = 0;\n\t[[nodiscard]] virtual FrameInfo frame(\n\t\tQSize size,\n\t\tQColor colored,\n\t\tbool mirrorHorizontal,\n\t\tcrl::time now,\n\t\tbool paused) = 0;\n\tvirtual bool markFrameShown() = 0;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_story_mention.h\"\n\n#include \"core/click_handler_types.h\" // ClickHandlerContext\n#include \"data/data_document.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_user.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"editor/photo_editor_common.h\"\n#include \"editor/photo_editor_layer_widget.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/media/history_view_sticker_player_abstract.h\"\n#include \"history/view/history_view_element.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/outline_segments.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/painter.h\"\n#include \"mainwidget.h\"\n#include \"apiwrap.h\"\n#include \"api/api_peer_photo.h\"\n#include \"settings/sections/settings_information.h\" // UpdatePhotoLocally\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kReadOutlineAlpha = 0.5;\n\n} // namespace\n\nStoryMention::StoryMention(\n\tnot_null<Element*> parent,\n\tnot_null<Data::Story*> story)\n: _parent(parent)\n, _story(story)\n, _unread(story->owner().stories().isUnread(story) ? 1 : 0) {\n}\n\nStoryMention::~StoryMention() {\n\tif (_subscribed) {\n\t\tchangeSubscribedTo(0);\n\t\t_parent->checkHeavyPart();\n\t}\n}\n\nint StoryMention::top() {\n\treturn st::msgServiceGiftBoxButtonMargins.top();\n}\n\nQSize StoryMention::size() {\n\treturn { st::msgServicePhotoWidth, st::msgServicePhotoWidth };\n}\n\nTextWithEntities StoryMention::title() {\n\treturn {};\n}\n\nint StoryMention::buttonSkip() {\n\treturn st::storyMentionButtonSkip;\n}\n\nrpl::producer<QString> StoryMention::button() {\n\treturn tr::lng_action_story_mention_button();\n}\n\nTextWithEntities StoryMention::subtitle() {\n\treturn _parent->data()->notificationText();\n}\n\nClickHandlerPtr StoryMention::createViewLink() {\n\tconst auto itemId = _parent->data()->fullId();\n\treturn std::make_shared<LambdaClickHandler>(crl::guard(this, [=](\n\t\t\tClickContext) {\n\t\tif (const auto photo = _story->photo()) {\n\t\t\t_parent->delegate()->elementOpenPhoto(photo, itemId);\n\t\t} else if (const auto video = _story->document()) {\n\t\t\t_parent->delegate()->elementOpenDocument(video, itemId);\n\t\t}\n\t}));\n}\n\nvoid StoryMention::draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry) {\n\tconst auto showStory = _story->forbidsForward() ? 0 : 1;\n\tif (!_thumbnail || _thumbnailFromStory != showStory) {\n\t\tconst auto item = _parent->data();\n\t\tconst auto history = item->history();\n\t\t_thumbnail = showStory\n\t\t\t? Ui::MakeStoryThumbnail(_story)\n\t\t\t: Ui::MakeUserpicThumbnail(item->out()\n\t\t\t\t? history->session().user()\n\t\t\t\t: history->peer);\n\t\t_thumbnailFromStory = showStory;\n\t\tchangeSubscribedTo(0);\n\t}\n\tif (changeSubscribedTo(1)) {\n\t\t_thumbnail->subscribeToUpdates([=] {\n\t\t\t_parent->data()->history()->owner().requestViewRepaint(_parent);\n\t\t});\n\t}\n\n\tconst auto padding = (geometry.width() - st::storyMentionSize) / 2;\n\tconst auto size = geometry.width() - 2 * padding;\n\tp.drawImage(\n\t\tgeometry.topLeft() + QPoint(padding, padding),\n\t\t_thumbnail->image(size));\n\n\tconst auto thumbnail = QRectF(geometry.marginsRemoved(\n\t\tQMargins(padding, padding, padding, padding)));\n\tconst auto added = 0.5 * (_unread\n\t\t? st::storyMentionUnreadSkipTwice\n\t\t: st::storyMentionReadSkipTwice);\n\tconst auto outline = thumbnail.marginsAdded(\n\t\tQMarginsF(added, added, added, added));\n\tif (_unread && _paletteVersion != style::PaletteVersion()) {\n\t\t_paletteVersion = style::PaletteVersion();\n\t\t_unreadBrush = QBrush(Ui::UnreadStoryOutlineGradient(outline));\n\t}\n\tauto readColor = context.st->msgServiceFg()->c;\n\treadColor.setAlphaF(std::min(1. * readColor.alphaF(), kReadOutlineAlpha));\n\tp.setPen(QPen(\n\t\t_unread ? _unreadBrush : QBrush(readColor),\n\t\t0.5 * (_unread\n\t\t\t? st::storyMentionUnreadStrokeTwice\n\t\t\t: st::storyMentionReadStrokeTwice)));\n\tp.setBrush(Qt::NoBrush);\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.drawEllipse(outline);\n}\n\nvoid StoryMention::stickerClearLoopPlayed() {\n}\n\nstd::unique_ptr<StickerPlayer> StoryMention::stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) {\n\treturn nullptr;\n}\n\nbool StoryMention::hasHeavyPart() {\n\treturn _subscribed != 0;\n}\n\nvoid StoryMention::unloadHeavyPart() {\n\tif (changeSubscribedTo(0)) {\n\t\t_thumbnail->subscribeToUpdates(nullptr);\n\t}\n}\n\nbool StoryMention::changeSubscribedTo(uint32 value) {\n\tExpects(value == 0 || value == 1);\n\n\tif (_subscribed == value) {\n\t\treturn false;\n\t}\n\t_subscribed = value;\n\tconst auto stories = &_parent->history()->owner().stories();\n\tif (value) {\n\t\t_parent->history()->owner().registerHeavyViewPart(_parent);\n\t\tstories->registerPolling(_story, Data::Stories::Polling::Chat);\n\t} else {\n\t\tstories->unregisterPolling(_story, Data::Stories::Polling::Chat);\n\t}\n\treturn true;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_story_mention.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n#include \"history/view/media/history_view_media_unwrapped.h\"\n#include \"history/view/media/history_view_service_box.h\"\n\nnamespace Data {\nclass Story;\n} // namespace Data\n\nnamespace Ui {\nclass DynamicImage;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass StoryMention final\n\t: public ServiceBoxContent\n\t, public base::has_weak_ptr {\npublic:\n\tStoryMention(not_null<Element*> parent, not_null<Data::Story*> story);\n\t~StoryMention();\n\n\tint top() override;\n\tQSize size() override;\n\tTextWithEntities title() override;\n\tTextWithEntities subtitle() override;\n\tint buttonSkip() override;\n\trpl::producer<QString> button() override;\n\tvoid draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry) override;\n\tClickHandlerPtr createViewLink() override;\n\n\tbool hideServiceText() override {\n\t\treturn true;\n\t}\n\n\tvoid stickerClearLoopPlayed() override;\n\tstd::unique_ptr<StickerPlayer> stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) override;\n\n\tbool hasHeavyPart() override;\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\tbool changeSubscribedTo(uint32 value);\n\n\tconst not_null<Element*> _parent;\n\tconst not_null<Data::Story*> _story;\n\tstd::shared_ptr<Ui::DynamicImage> _thumbnail;\n\tQBrush _unreadBrush;\n\tuint32 _paletteVersion : 29 = 0;\n\tuint32 _thumbnailFromStory : 1 = 0;\n\tuint32 _subscribed : 1 = 0;\n\tuint32 _unread : 1 = 0;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_suggest_decision.h\"\n\n#include \"base/unixtime.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"history/view/media/history_view_media_generic.h\"\n#include \"history/view/media/history_view_unique_gift.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/text/format_values.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kFadedOpacity = 0.85;\n\nenum EmojiType {\n\tkAgreement,\n\tkCalendar,\n\tkMoney,\n\tkHourglass,\n\tkReload,\n\tkDecline,\n\tkDiscard,\n\tkWarning,\n};\n\n[[nodiscard]] const char *Raw(EmojiType type) {\n\tswitch (type) {\n\tcase EmojiType::kAgreement: return \"\\xf0\\x9f\\xa4\\x9d\";\n\tcase EmojiType::kCalendar: return \"\\xf0\\x9f\\x93\\x86\";\n\tcase EmojiType::kMoney: return \"\\xf0\\x9f\\x92\\xb0\";\n\tcase EmojiType::kHourglass: return \"\\xe2\\x8c\\x9b\\xef\\xb8\\x8f\";\n\tcase EmojiType::kReload: return \"\\xf0\\x9f\\x94\\x84\";\n\tcase EmojiType::kDecline: return \"\\xe2\\x9d\\x8c\";\n\tcase EmojiType::kDiscard: return \"\\xf0\\x9f\\x9a\\xab\";\n\tcase EmojiType::kWarning: return \"\\xe2\\x9a\\xa0\\xef\\xb8\\x8f\";\n\t}\n\tUnexpected(\"EmojiType in Raw.\");\n}\n\n[[nodiscard]] QString Emoji(EmojiType type) {\n\treturn QString::fromUtf8(Raw(type));\n}\n\nstruct Changes {\n\tbool date = false;\n\tbool price = false;\n\tbool message = true;\n};\n[[nodiscard]] std::optional<Changes> ResolveChanges(\n\t\tnot_null<HistoryItem*> changed,\n\t\tHistoryItem *original) {\n\tconst auto wasSuggest = original\n\t\t? original->Get<HistoryMessageSuggestion>()\n\t\t: nullptr;\n\tconst auto nowSuggest = changed->Get<HistoryMessageSuggestion>();\n\tif (!wasSuggest || !nowSuggest) {\n\t\treturn {};\n\t}\n\tauto result = Changes();\n\tif (wasSuggest->date != nowSuggest->date) {\n\t\tresult.date = true;\n\t}\n\tif (wasSuggest->price != nowSuggest->price) {\n\t\tresult.price = true;\n\t}\n\tconst auto wasText = original->originalText();\n\tconst auto nowText = changed->originalText();\n\tconst auto mediaSame = [&] {\n\t\tconst auto wasMedia = original->media();\n\t\tconst auto nowMedia = changed->media();\n\t\tif (!wasMedia && !nowMedia) {\n\t\t\treturn true;\n\t\t} else if (!wasMedia\n\t\t\t|| !nowMedia\n\t\t\t|| !wasMedia->allowsEditCaption()\n\t\t\t|| !nowMedia->allowsEditCaption()) {\n\t\t\treturn false;\n\t\t}\n\t\t// We treat as \"same\" only same photo or same file.\n\t\treturn (wasMedia->photo() == nowMedia->photo())\n\t\t\t&& (wasMedia->document() == nowMedia->document());\n\t};\n\tif (!result.price && !result.date) {\n\t\tresult.message = true;\n\t} else if (wasText == nowText && mediaSame()) {\n\t\tresult.message = false;\n\t}\n\treturn result;\n}\n\n} // namespace\n\nauto GenerateSuggestDecisionMedia(\n\tnot_null<Element*> parent,\n\tnot_null<const HistoryServiceSuggestDecision*> decision)\n\t-> Fn<void(\n\t\tnot_null<MediaGeneric*>,\n\t\tFn<void(std::unique_ptr<MediaGenericPart>)>)> {\n\treturn [=](\n\t\t\tnot_null<MediaGeneric*> media,\n\t\t\tFn<void(std::unique_ptr<MediaGenericPart>)> push) {\n\t\tconst auto peer = parent->history()->peer;\n\t\tconst auto broadcast = peer->monoforumBroadcast();\n\t\tif (!broadcast) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto sublistPeerId = parent->data()->sublistPeerId();\n\t\tconst auto sublistPeer = peer->owner().peer(sublistPeerId);\n\n\t\tauto pushText = [&](\n\t\t\t\tTextWithEntities text,\n\t\t\t\tQMargins margins = {},\n\t\t\t\tstyle::align align = style::al_left,\n\t\t\t\tconst base::flat_map<uint16, ClickHandlerPtr> &links = {}) {\n\t\t\tpush(std::make_unique<MediaGenericTextPart>(\n\t\t\t\tstd::move(text),\n\t\t\t\tmargins,\n\t\t\t\tst::defaultTextStyle,\n\t\t\t\tlinks,\n\t\t\t\tUi::Text::MarkedContext(),\n\t\t\t\talign));\n\t\t};\n\n\t\tif (decision->balanceTooLow) {\n\t\t\tpushText(\n\t\t\t\tTextWithEntities(\n\t\t\t\t).append(Emoji(kWarning)).append(' ').append(\n\t\t\t\t\t(sublistPeer->isSelf()\n\t\t\t\t\t\t? (decision->price.ton()\n\t\t\t\t\t\t\t? tr::lng_suggest_action_your_not_enough_ton\n\t\t\t\t\t\t\t: tr::lng_suggest_action_your_not_enough_stars)\n\t\t\t\t\t\t: (decision->price.ton()\n\t\t\t\t\t\t\t? tr::lng_suggest_action_his_not_enough_ton\n\t\t\t\t\t\t\t: tr::lng_suggest_action_his_not_enough_stars))(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\ttr::rich)),\n\t\t\t\tst::chatSuggestInfoFullMargin,\n\t\t\t\tstyle::al_top);\n\t\t} else if (decision->rejected) {\n\t\t\tconst auto withComment = !decision->rejectComment.isEmpty();\n\t\t\tpushText(\n\t\t\t\tTextWithEntities(\n\t\t\t\t).append(Emoji(kDecline)).append(' ').append(\n\t\t\t\t\t(withComment\n\t\t\t\t\t\t? tr::lng_suggest_action_declined_reason\n\t\t\t\t\t\t: tr::lng_suggest_action_declined)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\t\ttr::bold(broadcast->name()),\n\t\t\t\t\t\t\ttr::marked)),\n\t\t\t\t(withComment\n\t\t\t\t\t? st::chatSuggestInfoTitleMargin\n\t\t\t\t\t: st::chatSuggestInfoFullMargin));\n\t\t\tif (withComment) {\n\t\t\t\tconst auto fadedFg = [](const PaintContext &context) {\n\t\t\t\t\tauto result = context.st->msgServiceFg()->c;\n\t\t\t\t\tresult.setAlphaF(result.alphaF() * kFadedOpacity);\n\t\t\t\t\treturn result;\n\t\t\t\t};\n\t\t\t\tpush(std::make_unique<TextPartColored>(\n\t\t\t\t\tTextWithEntities().append('\"').append(\n\t\t\t\t\t\tdecision->rejectComment\n\t\t\t\t\t).append('\"'),\n\t\t\t\t\tst::chatSuggestInfoLastMargin,\n\t\t\t\t\tfadedFg));\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto price = decision->price;\n\t\t\tpushText(\n\t\t\t\tTextWithEntities(\n\t\t\t\t).append(Emoji(kAgreement)).append(' ').append(\n\t\t\t\t\ttr::bold(tr::lng_suggest_action_agreement(tr::now))\n\t\t\t\t),\n\t\t\t\tst::chatSuggestInfoTitleMargin,\n\t\t\t\tstyle::al_top);\n\t\t\tconst auto date = base::unixtime::parse(decision->date);\n\t\t\tpushText(\n\t\t\t\tTextWithEntities(\n\t\t\t\t).append(Emoji(kCalendar)).append(' ').append(\n\t\t\t\t\ttr::lng_suggest_action_agree_date(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_channel,\n\t\t\t\t\t\ttr::bold(broadcast->name()),\n\t\t\t\t\t\tlt_date,\n\t\t\t\t\t\ttr::bold(tr::lng_mediaview_date_time(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_date,\n\t\t\t\t\t\t\tQLocale().toString(\n\t\t\t\t\t\t\t\tdate.date(),\n\t\t\t\t\t\t\t\tQLocale::ShortFormat),\n\t\t\t\t\t\t\tlt_time,\n\t\t\t\t\t\t\tQLocale().toString(\n\t\t\t\t\t\t\t\tdate.time(),\n\t\t\t\t\t\t\t\tQLocale::ShortFormat))),\n\t\t\t\t\t\ttr::marked)),\n\t\t\t\t(price\n\t\t\t\t\t? st::chatSuggestInfoMiddleMargin\n\t\t\t\t\t: st::chatSuggestInfoLastMargin));\n\t\t\tif (price) {\n\t\t\t\tpushText(\n\t\t\t\t\tTextWithEntities(\n\t\t\t\t\t).append(Emoji(kMoney)).append(' ').append(\n\t\t\t\t\t\t(sublistPeer->isSelf()\n\t\t\t\t\t\t\t? (price.stars()\n\t\t\t\t\t\t\t\t? tr::lng_suggest_action_your_charged_stars\n\t\t\t\t\t\t\t\t: tr::lng_suggest_action_your_charged_ton)(\n\t\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\t\t\t\tprice.value(),\n\t\t\t\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t\t\t: (price.stars()\n\t\t\t\t\t\t\t\t? tr::lng_suggest_action_his_charged_stars\n\t\t\t\t\t\t\t\t: tr::lng_suggest_action_his_charged_ton)(\n\t\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\t\t\t\tprice.value(),\n\t\t\t\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\t\t\t\ttr::bold(sublistPeer->shortName()),\n\t\t\t\t\t\t\t\t\ttr::rich))),\n\t\t\t\t\tst::chatSuggestInfoMiddleMargin);\n\n\t\t\t\tpushText(\n\t\t\t\t\tTextWithEntities(\n\t\t\t\t\t).append(Emoji(kHourglass)).append(' ').append(\n\t\t\t\t\t\t(price.ton()\n\t\t\t\t\t\t\t? tr::lng_suggest_action_agree_receive_ton\n\t\t\t\t\t\t\t: tr::lng_suggest_action_agree_receive_stars)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_channel,\n\t\t\t\t\t\t\ttr::bold(broadcast->name()),\n\t\t\t\t\t\t\ttr::marked)),\n\t\t\t\t\tst::chatSuggestInfoMiddleMargin);\n\n\t\t\t\tpushText(\n\t\t\t\t\tTextWithEntities(\n\t\t\t\t\t).append(Emoji(kReload)).append(' ').append(\n\t\t\t\t\t\t(price.ton()\n\t\t\t\t\t\t\t? tr::lng_suggest_action_agree_removed_ton\n\t\t\t\t\t\t\t: tr::lng_suggest_action_agree_removed_stars)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_channel,\n\t\t\t\t\t\t\ttr::bold(broadcast->name()),\n\t\t\t\t\t\t\ttr::marked)),\n\t\t\t\t\tst::chatSuggestInfoLastMargin);\n\t\t\t}\n\t\t}\n\t};\n}\n\nauto GenerateSuggestRequestMedia(\n\tnot_null<Element*> parent,\n\tnot_null<const HistoryMessageSuggestion*> suggest)\n\t-> Fn<void(\n\t\tnot_null<MediaGeneric*>,\n\t\tFn<void(std::unique_ptr<MediaGenericPart>)>)> {\n\treturn [=](\n\t\t\tnot_null<MediaGeneric*> media,\n\t\t\tFn<void(std::unique_ptr<MediaGenericPart>)> push) {\n\t\tconst auto normalFg = [](const PaintContext &context) {\n\t\t\treturn context.st->msgServiceFg()->c;\n\t\t};\n\t\tconst auto fadedFg = [](const PaintContext &context) {\n\t\t\tauto result = context.st->msgServiceFg()->c;\n\t\t\tresult.setAlphaF(result.alphaF() * kFadedOpacity);\n\t\t\treturn result;\n\t\t};\n\t\tconst auto item = parent->data();\n\t\tconst auto replyData = item->Get<HistoryMessageReply>();\n\t\tconst auto original = replyData\n\t\t\t? replyData->resolvedMessage.get()\n\t\t\t: nullptr;\n\t\tconst auto changes = ResolveChanges(item, original);\n\t\tconst auto from = item->from();\n\n\t\tauto pushText = [&](\n\t\t\t\tTextWithEntities text,\n\t\t\t\tQMargins margins = {},\n\t\t\t\tstyle::align align = style::al_left,\n\t\t\t\tconst base::flat_map<uint16, ClickHandlerPtr> &links = {}) {\n\t\t\tpush(std::make_unique<MediaGenericTextPart>(\n\t\t\t\tstd::move(text),\n\t\t\t\tmargins,\n\t\t\t\tst::defaultTextStyle,\n\t\t\t\tlinks,\n\t\t\t\tUi::Text::MarkedContext(),\n\t\t\t\talign));\n\t\t};\n\n\t\tpushText(\n\t\t\t((!changes && from->isSelf())\n\t\t\t\t? tr::lng_suggest_action_your(\n\t\t\t\t\ttr::now,\n\t\t\t\t\ttr::marked)\n\t\t\t\t: (!changes\n\t\t\t\t\t? tr::lng_suggest_action_his\n\t\t\t\t\t: changes->message\n\t\t\t\t\t? tr::lng_suggest_change_content\n\t\t\t\t\t: (changes->date && changes->price)\n\t\t\t\t\t? tr::lng_suggest_change_price_time\n\t\t\t\t\t: changes->price\n\t\t\t\t\t? tr::lng_suggest_change_price\n\t\t\t\t\t: tr::lng_suggest_change_time)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\ttr::bold(from->shortName()),\n\t\t\t\t\t\ttr::marked)),\n\t\t\tst::chatSuggestInfoTitleMargin,\n\t\t\tstyle::al_top);\n\n\t\tauto entries = std::vector<AttributeTable::Entry>();\n\t\tentries.push_back({\n\t\t\t((changes && changes->price)\n\t\t\t\t? tr::lng_suggest_change_price_label\n\t\t\t\t: tr::lng_suggest_action_price_label)(tr::now),\n\t\t\ttr::bold(!suggest->price\n\t\t\t\t? tr::lng_suggest_action_price_free(tr::now)\n\t\t\t\t: suggest->price.stars()\n\t\t\t\t? tr::lng_suggest_stars_amount(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\tsuggest->price.value())\n\t\t\t\t: tr::lng_suggest_ton_amount(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\tsuggest->price.value())),\n\t\t});\n\t\tentries.push_back({\n\t\t\t((changes && changes->date)\n\t\t\t\t? tr::lng_suggest_change_time_label\n\t\t\t\t: tr::lng_suggest_action_time_label)(tr::now),\n\t\t\ttr::bold(suggest->date\n\t\t\t\t? Ui::FormatDateTime(base::unixtime::parse(suggest->date))\n\t\t\t\t: tr::lng_suggest_action_time_any(tr::now)),\n\t\t});\n\t\tpush(std::make_unique<AttributeTable>(\n\t\t\tstd::move(entries),\n\t\t\t((changes && changes->message)\n\t\t\t\t? st::chatSuggestTableMiddleMargin\n\t\t\t\t: st::chatSuggestTableLastMargin),\n\t\t\tfadedFg,\n\t\t\tnormalFg));\n\t\tif (changes && changes->message) {\n\t\t\tpush(std::make_unique<TextPartColored>(\n\t\t\t\ttr::lng_suggest_change_text_label(\n\t\t\t\t\ttr::now,\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::chatSuggestInfoLastMargin,\n\t\t\t\tfadedFg));\n\t\t}\n\t};\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_suggest_decision.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nstruct HistoryMessageSuggestion;\nstruct HistoryServiceSuggestDecision;\n\nnamespace HistoryView {\n\nclass Element;\nclass MediaGeneric;\nclass MediaGenericPart;\n\nauto GenerateSuggestDecisionMedia(\n\tnot_null<Element*> parent,\n\tnot_null<const HistoryServiceSuggestDecision*> decision\n) -> Fn<void(\n\tnot_null<MediaGeneric*>,\n\tFn<void(std::unique_ptr<MediaGenericPart>)>)>;\n\nauto GenerateSuggestRequestMedia(\n\tnot_null<Element*> parent,\n\tnot_null<const HistoryMessageSuggestion*> suggest\n) -> Fn<void(\n\tnot_null<MediaGeneric*>,\n\tFn<void(std::unique_ptr<MediaGenericPart>)>)>;\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_theme_document.h\"\n\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/background_preview_box.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/media/history_view_sticker_player_abstract.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_wall_paper.h\"\n#include \"base/qthelp_url.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/local_url_handlers.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/painter.h\"\n#include \"ui/top_background_gradient.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/themes/window_theme.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n\nnamespace HistoryView {\nnamespace {\n\n[[nodiscard]] bool WallPaperRevertable(\n\t\tnot_null<PeerData*> peer,\n\t\tconst Data::WallPaper &paper) {\n\tif (!peer->wallPaperOverriden()) {\n\t\treturn false;\n\t}\n\tconst auto now = peer->wallPaper();\n\treturn now && now->equals(paper);\n}\n\n[[nodiscard]] bool WallPaperRevertable(not_null<HistoryItem*> item) {\n\tconst auto media = item->media();\n\tconst auto paper = media ? media->paper() : nullptr;\n\treturn paper\n\t\t&& media->paperForBoth()\n\t\t&& WallPaperRevertable(item->history()->peer, *paper);\n}\n\n[[nodiscard]] rpl::producer<bool> WallPaperRevertableValue(\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto media = item->media();\n\tconst auto paper = media ? media->paper() : nullptr;\n\tif (!paper || !media->paperForBoth()) {\n\t\treturn rpl::single(false);\n\t}\n\tconst auto peer = item->history()->peer;\n\treturn peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::ChatWallPaper\n\t) | rpl::map([peer, paper = *paper] {\n\t\treturn WallPaperRevertable(peer, paper);\n\t});\n}\n\n} // namespace\n\nThemeDocument::ThemeDocument(\n\tnot_null<Element*> parent,\n\tDocumentData *document)\n: ThemeDocument(parent, document, std::nullopt, 0) {\n}\n\nThemeDocument::ThemeDocument(\n\tnot_null<Element*> parent,\n\tDocumentData *document,\n\tconst std::optional<Data::WallPaper> &params,\n\tint serviceWidth)\n: File(parent, parent->data())\n, _data(document)\n, _serviceWidth(serviceWidth) {\n\tExpects(params.has_value()\n\t\t|| (_data && (_data->hasThumbnail() || _data->isTheme())));\n\n\tif (params) {\n\t\t_background = params->backgroundColors();\n\t\t_patternOpacity = params->patternOpacity();\n\t\t_gradientRotation = params->gradientRotation();\n\t\t_blurredWallPaper = params->isBlurred();\n\t\t_dimmingIntensity = (!params->document()\n\t\t\t|| params->isPattern()\n\t\t\t|| !_serviceWidth)\n\t\t\t? 0\n\t\t\t: std::max(params->patternIntensity(), 0);\n\t}\n\tconst auto fullId = _parent->data()->fullId();\n\tif (_data) {\n\t\t_data->loadThumbnail(fullId);\n\t\tsetDocumentLinks(_data, parent->data());\n\t\tsetStatusSize(Ui::FileStatusSizeReady, _data->size, -1, 0);\n\t} else {\n\t\tclass EmptyFileClickHandler final : public FileClickHandler {\n\t\tpublic:\n\t\t\tusing FileClickHandler::FileClickHandler;\n\n\t\tprivate:\n\t\t\tvoid onClickImpl() const override {\n\t\t\t}\n\n\t\t};\n\n\t\t// We could open BackgroundPreviewBox here, but right now\n\t\t// WebPage that created ThemeDocument as its attachment does it.\n\t\t//\n\t\t// So just provide a non-null click handler for this hack to work.\n\t\tsetLinks(\n\t\t\tstd::make_shared<EmptyFileClickHandler>(fullId),\n\t\t\tnullptr,\n\t\t\tnullptr);\n\t}\n}\n\nThemeDocument::~ThemeDocument() {\n\tif (_dataMedia) {\n\t\t_data->owner().keepAlive(base::take(_dataMedia));\n\t\t_parent->checkHeavyPart();\n\t}\n}\n\nstd::optional<Data::WallPaper> ThemeDocument::ParamsFromUrl(\n\t\tconst QString &url) {\n\tconst auto local = Core::TryConvertUrlToLocal(url);\n\tconst auto paramsPosition = local.indexOf('?');\n\tif (paramsPosition < 0) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto paramsString = local.mid(paramsPosition + 1);\n\tconst auto params = qthelp::url_parse_params(\n\t\tparamsString,\n\t\tqthelp::UrlParamNameTransform::ToLower);\n\tauto paper = Data::DefaultWallPaper().withUrlParams(params);\n\treturn paper.backgroundColors().empty()\n\t\t? std::nullopt\n\t\t: std::make_optional(std::move(paper));\n}\n\nQSize ThemeDocument::countOptimalSize() {\n\tif (_serviceWidth > 0) {\n\t\treturn { _serviceWidth, _serviceWidth };\n\t}\n\n\tif (!_data) {\n\t\treturn { st::maxWallPaperWidth, st::maxWallPaperHeight };\n\t} else if (_data->isTheme()) {\n\t\treturn st::historyThemeSize;\n\t}\n\tconst auto &location = _data->thumbnailLocation();\n\tauto tw = style::ConvertScale(location.width());\n\tauto th = style::ConvertScale(location.height());\n\tif (!tw || !th) {\n\t\ttw = th = 1;\n\t}\n\tth = (st::maxWallPaperWidth * th) / tw;\n\ttw = st::maxWallPaperWidth;\n\n\tconst auto maxWidth = tw;\n\tconst auto minHeight = std::clamp(\n\t\tth,\n\t\tst::minPhotoSize,\n\t\tst::maxWallPaperHeight);\n\treturn { maxWidth, minHeight };\n}\n\nQSize ThemeDocument::countCurrentSize(int newWidth) {\n\tif (_serviceWidth) {\n\t\t_pixw = _pixh = _serviceWidth;\n\t\treturn { _serviceWidth, _serviceWidth };\n\t}\n\tif (!_data) {\n\t\t_pixw = st::maxWallPaperWidth;\n\t\t_pixh = st::maxWallPaperHeight;\n\t\treturn { _pixw, _pixh };\n\t} else if (_data->isTheme()) {\n\t\t_pixw = st::historyThemeSize.width();\n\t\t_pixh = st::historyThemeSize.height();\n\t\treturn st::historyThemeSize;\n\t}\n\tconst auto &location = _data->thumbnailLocation();\n\tauto tw = style::ConvertScale(location.width());\n\tauto th = style::ConvertScale(location.height());\n\tif (!tw || !th) {\n\t\ttw = th = 1;\n\t}\n\n\t// We use pix() for image copies, because we rely that backgrounds\n\t// are always displayed with the same dimensions (not pixSingle()).\n\t_pixw = maxWidth();// std::min(newWidth, maxWidth());\n\t_pixh = minHeight();// (_pixw * th / tw);\n\n\tnewWidth = _pixw;\n\tconst auto newHeight = _pixh; /*std::clamp(\n\t\t_pixh,\n\t\tst::minPhotoSize,\n\t\tst::maxWallPaperHeight);*/\n\treturn { newWidth, newHeight };\n}\n\nvoid ThemeDocument::draw(Painter &p, const PaintContext &context) const {\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;\n\n\tensureDataMediaCreated();\n\n\tif (_data) {\n\t\t_dataMedia->automaticLoad(_realParent->fullId(), _parent->data());\n\t}\n\tconst auto st = context.st;\n\tconst auto sti = context.imageStyle();\n\tauto loaded = dataLoaded();\n\tauto displayLoading = _data && _data->displayLoading();\n\n\tauto paintx = 0, painty = 0, paintw = width(), painth = height();\n\n\tif (displayLoading) {\n\t\tensureAnimation();\n\t\tif (!_animation->radial.animating()) {\n\t\t\t_animation->radial.start(dataProgress());\n\t\t}\n\t}\n\tconst auto radial = isRadialAnimation();\n\n\tauto rthumb = style::rtlrect(paintx, painty, paintw, painth, width());\n\tvalidateThumbnail();\n\tp.drawPixmap(rthumb.topLeft(), _thumbnail);\n\tif (context.selected()) {\n\t\tUi::FillComplexOverlayRect(\n\t\t\tp,\n\t\t\trthumb,\n\t\t\tst->msgSelectOverlay(),\n\t\t\tst->msgSelectOverlayCorners(Ui::CachedCornerRadius::Small));\n\t}\n\n\tif (_data) {\n\t\tif (!_serviceWidth) {\n\t\t\tauto statusX = paintx + st::msgDateImgDelta + st::msgDateImgPadding.x();\n\t\t\tauto statusY = painty + st::msgDateImgDelta + st::msgDateImgPadding.y();\n\t\t\tauto statusW = st::normalFont->width(_statusText) + 2 * st::msgDateImgPadding.x();\n\t\t\tauto statusH = st::normalFont->height + 2 * st::msgDateImgPadding.y();\n\t\t\tUi::FillRoundRect(p, style::rtlrect(statusX - st::msgDateImgPadding.x(), statusY - st::msgDateImgPadding.y(), statusW, statusH, width()), sti->msgDateImgBg, sti->msgDateImgBgCorners);\n\t\t\tp.setFont(st::normalFont);\n\t\t\tp.setPen(st->msgDateImgFg());\n\t\t\tp.drawTextLeft(statusX, statusY, width(), _statusText, statusW - 2 * st::msgDateImgPadding.x());\n\t\t}\n\t\tif (radial || (!loaded && !_data->loading())) {\n\t\t\tconst auto radialOpacity = (radial && loaded && !_data->uploading())\n\t\t\t\t? _animation->radial.opacity() :\n\t\t\t\t1.;\n\t\t\tconst auto innerSize = st::msgFileLayout.thumbSize;\n\t\t\tQRect inner(rthumb.x() + (rthumb.width() - innerSize) / 2, rthumb.y() + (rthumb.height() - innerSize) / 2, innerSize, innerSize);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tif (context.selected()) {\n\t\t\t\tp.setBrush(st->msgDateImgBgSelected());\n\t\t\t} else if (isThumbAnimation()) {\n\t\t\t\tauto over = _animation->a_thumbOver.value(1.);\n\t\t\t\tp.setBrush(anim::brush(st->msgDateImgBg(), st->msgDateImgBgOver(), over));\n\t\t\t} else {\n\t\t\t\tauto over = ClickHandler::showAsActive(_data->loading() ? _cancell : _openl);\n\t\t\t\tp.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());\n\t\t\t}\n\n\t\t\tp.setOpacity(radialOpacity * p.opacity());\n\n\t\t\t{\n\t\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\t\tp.drawEllipse(inner);\n\t\t\t}\n\n\t\t\tp.setOpacity(radialOpacity);\n\t\t\tconst auto &icon = (radial || _data->loading())\n\t\t\t\t? sti->historyFileThumbCancel\n\t\t\t\t: sti->historyFileThumbDownload;\n\t\t\ticon.paintInCenter(p, inner);\n\t\t\tp.setOpacity(1);\n\t\t\tif (radial) {\n\t\t\t\tQRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));\n\t\t\t\t_animation->radial.draw(p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ThemeDocument::ensureDataMediaCreated() const {\n\tif (_dataMedia || !_data) {\n\t\treturn;\n\t}\n\t_dataMedia = _data->createMediaView();\n\tif (checkGoodThumbnail()) {\n\t\t_dataMedia->goodThumbnailWanted();\n\t}\n\t_dataMedia->thumbnailWanted(_realParent->fullId());\n\t_parent->history()->owner().registerHeavyViewPart(_parent);\n}\n\nbool ThemeDocument::checkGoodThumbnail() const {\n\treturn _data && (!_data->hasThumbnail() || !_data->isPatternWallPaper());\n}\n\nvoid ThemeDocument::validateThumbnail() const {\n\tconst auto isDark = Window::Theme::IsNightMode();\n\tif (_isDark != isDark) {\n\t\t_isDark = isDark;\n\t\t_thumbnailGood = -1;\n\t}\n\tif (checkGoodThumbnail()) {\n\t\tif (_thumbnailGood > 0) {\n\t\t\treturn;\n\t\t}\n\t\tensureDataMediaCreated();\n\t\tif (const auto good = _dataMedia->goodThumbnail()) {\n\t\t\tprepareThumbnailFrom(good, 1);\n\t\t\treturn;\n\t\t}\n\t}\n\tif (_thumbnailGood >= 0) {\n\t\treturn;\n\t}\n\tif (!_data) {\n\t\tgenerateThumbnail();\n\t\treturn;\n\t}\n\tensureDataMediaCreated();\n\tif (const auto normal = _dataMedia->thumbnail()) {\n\t\tprepareThumbnailFrom(normal, 0);\n\t} else if (_thumbnail.isNull()) {\n\t\tif (const auto blurred = _dataMedia->thumbnailInline()) {\n\t\t\tprepareThumbnailFrom(blurred, -1);\n\t\t}\n\t}\n}\n\nQImage ThemeDocument::finishServiceThumbnail(QImage image) const {\n\tif (!_serviceWidth) {\n\t\treturn image;\n\t} else if (_isDark && _dimmingIntensity > 0) {\n\t\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\t\tauto p = QPainter(&image);\n\t\tconst auto alpha = 255 * _dimmingIntensity / 100;\n\t\tp.fillRect(0, 0, _pixw, _pixh, QColor(0, 0, 0, alpha));\n\t}\n\tif (_blurredWallPaper) {\n\t\tconstexpr auto kRadius = 16;\n\t\timage = Images::BlurLargeImage(std::move(image), kRadius);\n\t}\n\treturn Images::Circle(std::move(image));\n}\n\nvoid ThemeDocument::generateThumbnail() const {\n\tauto image = Ui::GenerateBackgroundImage(\n\t\tQSize(_pixw, _pixh) * style::DevicePixelRatio(),\n\t\t_background,\n\t\t_gradientRotation,\n\t\t_patternOpacity);\n\t_thumbnail = Ui::PixmapFromImage(\n\t\tfinishServiceThumbnail(std::move(image)));\n\t_thumbnail.setDevicePixelRatio(style::DevicePixelRatio());\n\t_thumbnailGood = 1;\n}\n\nvoid ThemeDocument::prepareThumbnailFrom(\n\t\tnot_null<Image*> image,\n\t\tint good) const {\n\tExpects(_data != nullptr);\n\tExpects(_thumbnailGood <= good);\n\n\tconst auto isTheme = _data->isTheme();\n\tconst auto isPattern = _data->isPatternWallPaper();\n\tauto options = (good >= 0 ? Images::Option(0) : Images::Option::Blur)\n\t\t| (isPattern\n\t\t\t? Images::Option::TransparentBackground\n\t\t\t: Images::Option(0));\n\tauto original = image->original();\n\tconst auto &location = _data->thumbnailLocation();\n\tauto tw = isTheme ? _pixw : style::ConvertScale(location.width());\n\tauto th = isTheme ? _pixh : style::ConvertScale(location.height());\n\tif (!tw || !th) {\n\t\ttw = th = 1;\n\t}\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto resizeTo = _serviceWidth\n\t\t? QSize(tw, th).scaled(_pixw, _pixh, Qt::KeepAspectRatioByExpanding)\n\t\t: QSize(_pixw, (_pixw * th) / tw);\n\toriginal = Images::Prepare(\n\t\tstd::move(original),\n\t\tresizeTo * ratio,\n\t\t{ .options = options, .outer = { _pixw, _pixh } });\n\tif (isPattern) {\n\t\toriginal = Ui::PreparePatternImage(\n\t\t\tstd::move(original),\n\t\t\t_background,\n\t\t\t_gradientRotation,\n\t\t\t_patternOpacity);\n\t\toriginal.setDevicePixelRatio(ratio);\n\t}\n\t_thumbnail = Ui::PixmapFromImage(\n\t\tfinishServiceThumbnail(std::move(original)));\n\t_thumbnailGood = good;\n}\n\nTextState ThemeDocument::textState(QPoint point, StateRequest request) const {\n\tauto result = TextState(_parent);\n\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {\n\t\treturn result;\n\t}\n\tauto paintx = 0, painty = 0, paintw = width(), painth = height();\n\tif (QRect(paintx, painty, paintw, painth).contains(point)) {\n\t\tif (!_data) {\n\t\t\tresult.link = _openl;\n\t\t} else if (_data->uploading()) {\n\t\t\tresult.link = _cancell;\n\t\t} else if (dataLoaded()) {\n\t\t\tresult.link = _openl;\n\t\t} else if (_data->loading()) {\n\t\t\tresult.link = _cancell;\n\t\t} else {\n\t\t\tresult.link = _openl;\n\t\t}\n\t}\n\treturn result;\n}\n\nfloat64 ThemeDocument::dataProgress() const {\n\tensureDataMediaCreated();\n\treturn _data ? _dataMedia->progress() : 1.;\n}\n\nbool ThemeDocument::dataFinished() const {\n\treturn !_data\n\t\t|| (!_data->loading()\n\t\t\t&& (!_data->uploading() || _data->waitingForAlbum()));\n}\n\nbool ThemeDocument::dataLoaded() const {\n\tensureDataMediaCreated();\n\treturn !_data || _dataMedia->loaded();\n}\n\nbool ThemeDocument::isReadyForOpen() const {\n\tensureDataMediaCreated();\n\treturn !_data || _dataMedia->loaded();\n}\n\nbool ThemeDocument::hasHeavyPart() const {\n\treturn (_dataMedia != nullptr);\n}\n\nvoid ThemeDocument::unloadHeavyPart() {\n\t_dataMedia = nullptr;\n}\n\nThemeDocumentBox::ThemeDocumentBox(\n\tnot_null<Element*> parent,\n\tconst Data::WallPaper &paper)\n: _parent(parent)\n, _emojiId(paper.emojiId()) {\n\tWindow::WallPaperResolved(\n\t\t&_parent->history()->owner(),\n\t\t&paper\n\t) | rpl::on_next([=](const Data::WallPaper *paper) {\n\t\t_parent->repaint();\n\t\tif (!paper) {\n\t\t\t_preview.reset();\n\t\t} else {\n\t\t\tcreatePreview(*paper);\n\t\t}\n\t}, _lifetime);\n}\n\nvoid ThemeDocumentBox::createPreview(const Data::WallPaper &paper) {\n\t_preview.emplace(\n\t\t_parent,\n\t\tpaper.document(),\n\t\tpaper,\n\t\tst::msgServicePhotoWidth);\n\t_preview->initDimensions();\n\t_preview->resizeGetHeight(_preview->maxWidth());\n}\n\nThemeDocumentBox::~ThemeDocumentBox() = default;\n\nint ThemeDocumentBox::top() {\n\treturn st::msgServiceGiftBoxButtonMargins.top();\n}\n\nQSize ThemeDocumentBox::size() {\n\treturn _preview\n\t\t? QSize(_preview->maxWidth(), _preview->minHeight())\n\t\t: QSize(st::msgServicePhotoWidth, st::msgServicePhotoWidth);\n}\n\nTextWithEntities ThemeDocumentBox::title() {\n\treturn {};\n}\n\nTextWithEntities ThemeDocumentBox::subtitle() {\n\treturn _parent->data()->notificationText();\n}\n\nrpl::producer<QString> ThemeDocumentBox::button() {\n\tif (_parent->data()->out() || _parent->history()->peer->isChannel()) {\n\t\treturn {};\n\t}\n\treturn rpl::conditional(\n\t\tWallPaperRevertableValue(_parent->data()),\n\t\ttr::lng_action_set_wallpaper_remove(),\n\t\ttr::lng_action_set_wallpaper_button());\n}\n\nClickHandlerPtr ThemeDocumentBox::createViewLink() {\n\tconst auto to = _parent->history()->peer;\n\tif (to->isChannel()) {\n\t\treturn nullptr;\n\t}\n\tconst auto out = _parent->data()->out();\n\tconst auto media = _parent->data()->media();\n\tconst auto weak = base::make_weak(_parent);\n\tconst auto paper = media ? media->paper() : nullptr;\n\tconst auto maybe = paper ? *paper : std::optional<Data::WallPaper>();\n\tconst auto itemId = _parent->data()->fullId();\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tconst auto view = weak.get();\n\t\t\tif (view\n\t\t\t\t&& !view->data()->out()\n\t\t\t\t&& WallPaperRevertable(view->data())) {\n\t\t\t\tconst auto reset = crl::guard(weak, [=](Fn<void()> close) {\n\t\t\t\t\tconst auto api = &controller->session().api();\n\t\t\t\t\tapi->request(MTPmessages_SetChatWallPaper(\n\t\t\t\t\t\tMTP_flags(MTPmessages_SetChatWallPaper::Flag::f_revert),\n\t\t\t\t\t\tview->data()->history()->peer->input(),\n\t\t\t\t\t\tMTPInputWallPaper(),\n\t\t\t\t\t\tMTPWallPaperSettings(),\n\t\t\t\t\t\tMTPint()\n\t\t\t\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t\t\t\tapi->applyUpdates(result);\n\t\t\t\t\t}).send();\n\t\t\t\t\tclose();\n\t\t\t\t});\n\t\t\t\tcontroller->show(Ui::MakeConfirmBox({\n\t\t\t\t\t.text = tr::lng_background_sure_reset_default(),\n\t\t\t\t\t.confirmed = reset,\n\t\t\t\t\t.confirmText = tr::lng_background_reset_default(),\n\t\t\t\t}));\n\t\t\t} else if (out) {\n\t\t\t\tcontroller->toggleChooseChatTheme(to);\n\t\t\t} else if (maybe) {\n\t\t\t\tcontroller->show(Box<BackgroundPreviewBox>(\n\t\t\t\t\tcontroller,\n\t\t\t\t\t*maybe,\n\t\t\t\t\tBackgroundPreviewArgs{ to, itemId }));\n\t\t\t}\n\t\t}\n\t});\n}\n\nvoid ThemeDocumentBox::draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry) {\n\tif (_preview) {\n\t\tp.translate(geometry.topLeft());\n\t\t_preview->draw(p, context);\n\t\tp.translate(-geometry.topLeft());\n\t}\n}\n\nvoid ThemeDocumentBox::stickerClearLoopPlayed() {\n}\n\nstd::unique_ptr<StickerPlayer> ThemeDocumentBox::stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) {\n\treturn nullptr;\n}\n\nbool ThemeDocumentBox::hasHeavyPart() {\n\treturn _preview && _preview->hasHeavyPart();\n}\n\nvoid ThemeDocumentBox::unloadHeavyPart() {\n\tif (_preview) {\n\t\t_preview->unloadHeavyPart();\n\t}\n}\n\nGiftServiceBox::GiftServiceBox(\n\tnot_null<Element*> parent,\n\tnot_null<Data::MediaGiftBox*> gift)\n: _parent(parent)\n, _data(*gift->gift()) {\n}\n\nGiftServiceBox::~GiftServiceBox() = default;\n\nint GiftServiceBox::top() {\n\treturn st::msgServiceStarGiftStickerTop;\n}\n\nint GiftServiceBox::width() {\n\treturn st::msgServiceStarGiftBoxWidth;\n}\n\nQSize GiftServiceBox::size() {\n\treturn QSize(\n\t\tst::msgServiceGiftThemeStickerSize,\n\t\tst::msgServiceGiftThemeStickerSize).grownBy(\n\t\t\tst::msgServiceGiftThemeStickerPadding);\n}\n\nTextWithEntities GiftServiceBox::title() {\n\treturn {};\n}\n\nTextWithEntities GiftServiceBox::subtitle() {\n\tconst auto giftName = tr::bold(Data::UniqueGiftName(*_data.unique));\n\tif (_data.type == Data::GiftType::GiftOffer) {\n\t\tconst auto item = _parent->data();\n\t\tif (const auto suggestion = item->Get<HistoryMessageSuggestion>()) {\n\t\t\tconst auto amount = suggestion->price;\n\t\t\tconst auto cost = tr::bold(PrepareCreditsAmountText(amount));\n\t\t\tauto text = tr::marked();\n\t\t\tif (_parent->data()->out()) {\n\t\t\t\ttext.append(tr::lng_action_gift_offer_you(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_cost,\n\t\t\t\t\tcost,\n\t\t\t\t\tlt_name,\n\t\t\t\t\tgiftName,\n\t\t\t\t\ttr::marked));\n\t\t\t} else {\n\t\t\t\ttext.append(tr::lng_action_gift_offer_incoming(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_amount,\n\t\t\t\t\tcost,\n\t\t\t\t\ttr::marked));\n\t\t\t}\n\t\t\ttext.append(u\"\\n\\n\"_q);\n\n\t\t\tconst auto ends = suggestion->date;\n\t\t\tconst auto now = base::unixtime::now();\n\t\t\tconst auto expired = (now >= ends);\n\n\t\t\tcheckKeyboardRemoval(suggestion, expired);\n\n\t\t\tif (suggestion->accepted) {\n\t\t\t\ttext.append(\n\t\t\t\t\ttr::lng_action_gift_offer_state_accepted(tr::now));\n\t\t\t} else if (suggestion->rejected) {\n\t\t\t\ttext.append(\n\t\t\t\t\ttr::lng_action_gift_offer_state_rejected(tr::now));\n\t\t\t} else {\n\t\t\t\tif (expired) {\n\t\t\t\t\ttext.append(\n\t\t\t\t\t\ttr::lng_action_gift_offer_state_expired(tr::now));\n\t\t\t\t} else {\n\t\t\t\t\tauto time = QString();\n\t\t\t\t\tconst auto left = (ends - now) + 59;\n\t\t\t\t\tif (left >= 3600) {\n\t\t\t\t\t\tconst auto hours = left / 3600;\n\t\t\t\t\t\tconst auto minutes = (left % 3600) / 60;\n\t\t\t\t\t\ttime = minutes\n\t\t\t\t\t\t\t? tr::lng_action_gift_offer_time_medium(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_hours,\n\t\t\t\t\t\t\t\tQString::number(hours),\n\t\t\t\t\t\t\t\tlt_minutes,\n\t\t\t\t\t\t\t\tQString::number(minutes))\n\t\t\t\t\t\t\t: tr::lng_action_gift_offer_time_large(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_hours,\n\t\t\t\t\t\t\t\tQString::number(hours));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst auto minutes = left / 60;\n\t\t\t\t\t\ttime = tr::lng_action_gift_offer_time_small(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_minutes,\n\t\t\t\t\t\t\tQString::number(minutes));\n\t\t\t\t\t}\n\t\t\t\t\ttext.append(tr::lng_action_gift_offer_state_expires(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_time,\n\t\t\t\t\t\ttr::bold(time),\n\t\t\t\t\t\ttr::marked));\n\n\t\t\t\t\tconst auto tillNext = left % 60;\n\t\t\t\t\t_changeTimer.setCallback([=] { _changes.fire({}); });\n\t\t\t\t\t_changeTimer.callOnce((tillNext ? tillNext : 60)\n\t\t\t\t\t\t* crl::time(1000));\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn text;\n\t\t}\n\t} else if (_parent->data()->out()) {\n\t\treturn tr::lng_action_you_gift_theme_changed(\n\t\t\ttr::now,\n\t\t\tlt_name,\n\t\t\tgiftName,\n\t\t\ttr::marked);\n\t} else {\n\t\treturn tr::lng_action_gift_theme_changed(\n\t\t\ttr::now,\n\t\t\tlt_from,\n\t\t\ttr::bold(_parent->data()->from()->shortName()),\n\t\t\tlt_name,\n\t\t\tgiftName,\n\t\t\ttr::marked);\n\t}\n\treturn _parent->data()->originalText();\n}\n\nvoid GiftServiceBox::checkKeyboardRemoval(\n\t\tnot_null<const HistoryMessageSuggestion*> suggestion,\n\t\tbool expired) {\n\tExpects(_data.type == Data::GiftType::GiftOffer);\n\n\tconst auto item = _parent->data();\n\tif (const auto markup = item->Get<HistoryMessageReplyMarkup>()) {\n\t\tif (!markup->data.isTrivial()) {\n\t\t\tif (suggestion->accepted || suggestion->rejected || expired) {\n\t\t\t\tcrl::on_main(this, [=] {\n\t\t\t\t\tclearKeyboard();\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid GiftServiceBox::clearKeyboard() {\n\tExpects(_data.type == Data::GiftType::GiftOffer);\n\n\tconst auto item = _parent->data();\n\tif (const auto markup = item->Get<HistoryMessageReplyMarkup>()) {\n\t\tmarkup->updateSuggestControls(SuggestionActions::None);\n\t\titem->history()->owner().requestItemResize(item);\n\t}\n}\n\nrpl::producer<> GiftServiceBox::changes() {\n\tif (_data.type != Data::GiftType::GiftOffer) {\n\t\treturn nullptr;\n\t}\n\treturn _changes.events();\n}\n\nrpl::producer<QString> GiftServiceBox::button() {\n\tif (_data.type == Data::GiftType::GiftOffer) {\n\t\treturn nullptr;\n\t}\n\treturn tr::lng_sticker_premium_view();\n}\n\nClickHandlerPtr GiftServiceBox::createViewLink() {\n\treturn std::make_shared<UrlClickHandler>(\n\t\tu\"tg://nft?slug=\"_q + _data.unique->slug);\n}\n\nint GiftServiceBox::buttonSkip() {\n\treturn st::msgServiceGiftBoxButtonMargins.top();\n}\n\nvoid GiftServiceBox::cacheUniqueBackground(int width, int height) {\n\tif (!_patternEmoji) {\n\t\tconst auto session = &_parent->data()->history()->session();\n\t\t_patternEmoji = session->data().customEmojiManager().create(\n\t\t\t_data.unique->pattern.document,\n\t\t\t[=] { _parent->repaint(); },\n\t\t\tData::CustomEmojiSizeTag::Large);\n\t\t[[maybe_unused]] const auto preload = _patternEmoji->ready();\n\t}\n\tconst auto inner = QRect(0, 0, width, height);\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (_backgroundCache.size() != inner.size() * ratio) {\n\t\t_backgroundCache = QImage(\n\t\t\tinner.size() * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t_backgroundCache.fill(Qt::transparent);\n\t\t_backgroundCache.setDevicePixelRatio(ratio);\n\n\t\tconst auto radius = st::giftBoxGiftRadius;\n\t\tauto p = QPainter(&_backgroundCache);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tauto gradient = QRadialGradient(inner.center(), inner.width() / 2);\n\t\tgradient.setStops({\n\t\t\t{ 0., _data.unique->backdrop.centerColor },\n\t\t\t{ 1., _data.unique->backdrop.edgeColor },\n\t\t});\n\t\tp.setBrush(gradient);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawRoundedRect(inner, radius, radius);\n\t\t_backroundPatterned = false;\n\t}\n\tif (!_backroundPatterned && _patternEmoji->ready()) {\n\t\t_backroundPatterned = true;\n\t\tauto p = QPainter(&_backgroundCache);\n\t\tp.setClipRect(inner);\n\t\tconst auto skip = inner.width() / 3;\n\t\tUi::PaintBgPoints(\n\t\t\tp,\n\t\t\tUi::PatternBgPointsSmall(),\n\t\t\t_patternCache,\n\t\t\t_patternEmoji.get(),\n\t\t\t*_data.unique,\n\t\t\tQRect(-skip, 0, inner.width() + 2 * skip, inner.height()));\n\t}\n}\n\nvoid GiftServiceBox::draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry) {\n\tcacheUniqueBackground(geometry.width(), geometry.height());\n\tp.drawImage(geometry.topLeft(), _backgroundCache);\n\n\tif (_sticker) {\n\t\t_sticker->draw(\n\t\t\tp,\n\t\t\tcontext,\n\t\t\tgeometry.marginsRemoved(st::msgServiceGiftThemeStickerPadding));\n\t} else {\n\t\tensureStickerCreated();\n\t}\n}\n\nbool GiftServiceBox::hideServiceText() {\n\treturn true;\n}\n\nvoid GiftServiceBox::stickerClearLoopPlayed() {\n\tif (_sticker) {\n\t\t_sticker->stickerClearLoopPlayed();\n\t}\n}\n\nstd::unique_ptr<StickerPlayer> GiftServiceBox::stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) {\n\treturn _sticker\n\t\t? _sticker->stickerTakePlayer(data, replacements)\n\t\t: nullptr;\n}\n\nbool GiftServiceBox::hasHeavyPart() {\n\treturn (_sticker ? _sticker->hasHeavyPart() : false);\n}\n\nvoid GiftServiceBox::unloadHeavyPart() {\n\tif (_sticker) {\n\t\t_sticker->unloadHeavyPart();\n\t}\n}\n\nvoid GiftServiceBox::ensureStickerCreated() const {\n\tif (_sticker) {\n\t\treturn;\n\t}\n\tconst auto document = _data.unique->model.document;\n\tconst auto sticker = document->sticker();\n\tAssert(sticker != nullptr);\n\t_sticker.emplace(_parent, document, false, _parent);\n\t_sticker->setPlayingOnce(true);\n\t_sticker->initSize(st::msgServiceGiftThemeStickerSize);\n\t_parent->repaint();\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_theme_document.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_file.h\"\n#include \"history/view/media/history_view_service_box.h\"\n#include \"history/view/media/history_view_sticker.h\"\n\nclass Image;\nstruct HistoryMessageSuggestion;\n\nnamespace Data {\nclass DocumentMedia;\nclass WallPaper;\nclass MediaGiftBox;\nstruct GiftCode;\n} // namespace Data\n\nnamespace HistoryView {\n\nclass ThemeDocument final : public File {\npublic:\n\tThemeDocument(not_null<Element*> parent, DocumentData *document);\n\tThemeDocument(\n\t\tnot_null<Element*> parent,\n\t\tDocumentData *document,\n\t\tconst std::optional<Data::WallPaper> &params,\n\t\tint serviceWidth = 0);\n\t~ThemeDocument();\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\n\tDocumentData *getDocument() const override {\n\t\treturn _data;\n\t}\n\n\tbool needsBubble() const override {\n\t\treturn false;\n\t}\n\tbool customInfoLayout() const override {\n\t\treturn false;\n\t}\n\tbool skipBubbleTail() const override {\n\t\treturn true;\n\t}\n\tbool isReadyForOpen() const override;\n\n\tbool hasHeavyPart() const override;\n\tvoid unloadHeavyPart() override;\n\n\t[[nodiscard]] static std::optional<Data::WallPaper> ParamsFromUrl(\n\t\tconst QString &url);\n\nprotected:\n\tfloat64 dataProgress() const override;\n\tbool dataFinished() const override;\n\tbool dataLoaded() const override;\n\nprivate:\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\n\t[[nodiscard]] bool checkGoodThumbnail() const;\n\tvoid validateThumbnail() const;\n\tvoid prepareThumbnailFrom(not_null<Image*> image, int good) const;\n\tvoid generateThumbnail() const;\n\tvoid ensureDataMediaCreated() const;\n\t[[nodiscard]] QImage finishServiceThumbnail(QImage image) const;\n\n\tDocumentData *_data = nullptr;\n\tint _pixw = 1;\n\tint _pixh = 1;\n\tconst int _serviceWidth = 0;\n\tmutable QPixmap _thumbnail;\n\tmutable int _thumbnailGood = -1; // -1 inline, 0 thumbnail, 1 good\n\tmutable std::shared_ptr<Data::DocumentMedia> _dataMedia;\n\n\t// For wallpaper documents.\n\tstd::vector<QColor> _background;\n\tfloat64 _patternOpacity = 0.;\n\tint _gradientRotation = 0;\n\n\tmutable bool _isDark = false;\n\tint _dimmingIntensity = 0;\n\tbool _blurredWallPaper = false;\n\n};\n\nclass ThemeDocumentBox final : public ServiceBoxContent {\npublic:\n\tThemeDocumentBox(\n\t\tnot_null<Element*> parent,\n\t\tconst Data::WallPaper &paper);\n\t~ThemeDocumentBox();\n\n\tint top() override;\n\tQSize size() override;\n\tTextWithEntities title() override;\n\tTextWithEntities subtitle() override;\n\trpl::producer<QString> button() override;\n\tvoid draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry) override;\n\tClickHandlerPtr createViewLink() override;\n\n\tbool hideServiceText() override {\n\t\treturn true;\n\t}\n\n\tvoid stickerClearLoopPlayed() override;\n\tstd::unique_ptr<StickerPlayer> stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) override;\n\n\tbool hasHeavyPart() override;\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\tvoid createPreview(const Data::WallPaper &paper);\n\n\tconst not_null<Element*> _parent;\n\tQString _emojiId;\n\tstd::optional<ThemeDocument> _preview;\n\trpl::lifetime _lifetime;\n\n};\n\nclass GiftServiceBox final\n\t: public ServiceBoxContent\n\t, public base::has_weak_ptr {\npublic:\n\tGiftServiceBox(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<Data::MediaGiftBox*> gift);\n\t~GiftServiceBox();\n\n\tint top() override;\n\tint width() override;\n\tQSize size() override;\n\tTextWithEntities title() override;\n\tTextWithEntities subtitle() override;\n\trpl::producer<QString> button() override;\n\tint buttonSkip() override;\n\tvoid draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry) override;\n\tClickHandlerPtr createViewLink() override;\n\n\trpl::producer<> changes() override;\n\n\tbool hideServiceText() override;\n\tvoid stickerClearLoopPlayed() override;\n\tstd::unique_ptr<StickerPlayer> stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) override;\n\n\tbool hasHeavyPart() override;\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\tvoid ensureStickerCreated() const;\n\tvoid cacheUniqueBackground(int width, int height);\n\tvoid checkKeyboardRemoval(\n\t\tnot_null<const HistoryMessageSuggestion*> suggestion,\n\t\tbool expired);\n\tvoid clearKeyboard();\n\n\tconst not_null<Element*> _parent;\n\tconst Data::GiftCode &_data;\n\tstd::unique_ptr<Ui::Text::CustomEmoji> _patternEmoji;\n\tQImage _backgroundCache;\n\tbase::flat_map<float64, QImage> _patternCache;\n\tbool _backroundPatterned = false;\n\tmutable std::optional<Sticker> _sticker;\n\trpl::event_stream<> _changes;\n\tbase::Timer _changeTimer;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_todo_list.h\"\n\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\" // TextContext\n#include \"lang/lang_keys.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/view/history_view_message.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_text_helper.h\"\n#include \"calls/calls_instance.h\"\n#include \"ui/chat/message_bubble.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/fireworks_animation.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"base/unixtime.h\"\n#include \"base/timer.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"api/api_todo_lists.h\"\n#include \"window/window_peer_menu.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_polls.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_window.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kShowRecentVotersCount = 3;\nconstexpr auto kRotateSegments = 8;\nconstexpr auto kRotateAmplitude = 3.;\nconstexpr auto kScaleSegments = 2;\nconstexpr auto kScaleAmplitude = 0.03;\nconstexpr auto kLargestRadialDuration = 30 * crl::time(1000);\nconstexpr auto kCriticalCloseDuration = 5 * crl::time(1000);\n\n} // namespace\n\nstruct TodoList::Task {\n\tTask();\n\n\tvoid fillData(\n\t\tnot_null<Element*> view,\n\t\tnot_null<TodoListData*> todolist,\n\t\tconst TodoListItem &original,\n\t\tUi::Text::MarkedContext context);\n\tvoid setCompletedBy(PeerData *by);\n\n\tUi::Text::String text;\n\tUi::Text::String name;\n\tPeerData *completedBy = nullptr;\n\tmutable Ui::PeerUserpicView userpic;\n\tTimeId completionDate = 0;\n\tint id = 0;\n\tClickHandlerPtr handler;\n\tUi::Animations::Simple selectedAnimation;\n\tmutable std::unique_ptr<Ui::RippleAnimation> ripple;\n};\n\nTodoList::Task::Task()\n: text(st::msgMinWidth / 2)\n, name(st::msgMinWidth / 2) {\n}\n\nvoid TodoList::Task::fillData(\n\t\tnot_null<Element*> view,\n\t\tnot_null<TodoListData*> todolist,\n\t\tconst TodoListItem &original,\n\t\tUi::Text::MarkedContext context) {\n\tid = original.id;\n\tsetCompletedBy(original.completedBy);\n\tcompletionDate = original.completionDate;\n\tif (!text.isEmpty() && text.toTextWithEntities() == original.text) {\n\t\treturn;\n\t}\n\ttext.setMarkedText(\n\t\tst::historyPollAnswerStyle,\n\t\toriginal.text,\n\t\tUi::WebpageTextTitleOptions(),\n\t\tcontext);\n\tInitElementTextPart(view, text);\n}\n\nvoid TodoList::Task::setCompletedBy(PeerData *by) {\n\tif (!by || completedBy == by) {\n\t\treturn;\n\t}\n\tcompletedBy = by;\n\tname.setText(st::historyPollAnswerStyle, completedBy->name());\n}\n\nTodoList::TodoList(\n\tnot_null<Element*> parent,\n\tnot_null<TodoListData*> todolist,\n\tElement *replacing)\n: Media(parent)\n, _todolist(todolist)\n, _title(st::msgMinWidth / 2) {\n\thistory()->owner().registerTodoListView(_todolist, _parent);\n\tif (const auto media = replacing ? replacing->media() : nullptr) {\n\t\tconst auto info = media->takeTasksInfo();\n\t\tif (!info.empty()) {\n\t\t\tsetupPreviousState(info);\n\t\t}\n\t}\n}\n\nvoid TodoList::setupPreviousState(const std::vector<TodoTaskInfo> &info) {\n\t// If we restore state from the view we're replacing we'll be able to\n\t// animate the changes properly.\n\tupdateTasks(true);\n\tfor (auto &task : _tasks) {\n\t\tconst auto i = ranges::find(info, task.id, &TodoTaskInfo::id);\n\t\tif (i != end(info)) {\n\t\t\ttask.setCompletedBy(i->completedBy);\n\t\t\ttask.completionDate = i->completionDate;\n\t\t}\n\t}\n}\n\nQSize TodoList::countOptimalSize() {\n \tupdateTexts();\n\n\tconst auto paddings = st::msgPadding.left() + st::msgPadding.right();\n\n\tauto maxWidth = st::msgFileMinWidth;\n\taccumulate_max(maxWidth, paddings + _title.maxWidth());\n\tfor (const auto &task : _tasks) {\n\t\taccumulate_max(\n\t\t\tmaxWidth,\n\t\t\tpaddings\n\t\t\t+ st::historyChecklistTaskPadding.left()\n\t\t\t+ task.text.maxWidth()\n\t\t\t+ st::historyChecklistTaskPadding.right());\n\t}\n\n\tconst auto tasksHeight = ranges::accumulate(ranges::views::all(\n\t\t_tasks\n\t) | ranges::views::transform([](const Task &task) {\n\t\treturn st::historyChecklistTaskPadding.top()\n\t\t\t+ task.text.minHeight()\n\t\t\t+ st::historyChecklistTaskPadding.bottom();\n\t}), 0);\n\n\tconst auto bottomButtonHeight = st::historyPollBottomButtonSkip;\n\tauto minHeight = st::historyPollQuestionTop\n\t\t+ _title.minHeight()\n\t\t+ st::historyPollSubtitleSkip\n\t\t+ st::msgDateFont->height\n\t\t+ st::historyPollAnswersSkip\n\t\t+ tasksHeight\n\t\t+ st::historyPollTotalVotesSkip\n\t\t+ bottomButtonHeight\n\t\t+ st::msgDateFont->height\n\t\t+ st::msgPadding.bottom();\n\tif (!isBubbleTop()) {\n\t\tminHeight -= st::msgFileTopMinus;\n\t}\n\treturn { maxWidth, minHeight };\n}\n\nbool TodoList::canComplete() const {\n\treturn (_parent->data()->out()\n\t\t|| _parent->history()->peer->isSelf()\n\t\t|| _todolist->othersCanComplete())\n\t\t&& _parent->data()->isRegular()\n\t\t&& !_parent->data()->Has<HistoryMessageForwarded>();\n}\n\nint TodoList::countTaskTop(\n\t\tconst Task &task,\n\t\tint innerWidth) const {\n\tauto tshift = st::historyPollQuestionTop;\n\tif (!isBubbleTop()) {\n\t\ttshift -= st::msgFileTopMinus;\n\t}\n\ttshift += _title.countHeight(innerWidth) + st::historyPollSubtitleSkip;\n\ttshift += st::msgDateFont->height + st::historyPollAnswersSkip;\n\tconst auto i = ranges::find(\n\t\t_tasks,\n\t\t&task,\n\t\t[](const Task &task) { return &task; });\n\tconst auto countHeight = [&](const Task &task) {\n\t\treturn countTaskHeight(task, innerWidth);\n\t};\n\ttshift += ranges::accumulate(\n\t\tbegin(_tasks),\n\t\ti,\n\t\t0,\n\t\tranges::plus(),\n\t\tcountHeight);\n\treturn tshift;\n}\n\nint TodoList::countTaskHeight(\n\t\tconst Task &task,\n\t\tint innerWidth) const {\n\tconst auto answerWidth = innerWidth\n\t\t- st::historyChecklistTaskPadding.left()\n\t\t- st::historyChecklistTaskPadding.right();\n\treturn st::historyChecklistTaskPadding.top()\n\t\t+ task.text.countHeight(answerWidth)\n\t\t+ st::historyChecklistTaskPadding.bottom();\n}\n\nQSize TodoList::countCurrentSize(int newWidth) {\n\taccumulate_min(newWidth, maxWidth());\n\tconst auto innerWidth = newWidth\n\t\t- st::msgPadding.left()\n\t\t- st::msgPadding.right();\n\n\tconst auto tasksHeight = ranges::accumulate(ranges::views::all(\n\t\t_tasks\n\t) | ranges::views::transform([&](const Task &task) {\n\t\treturn countTaskHeight(task, innerWidth);\n\t}), 0);\n\n\tconst auto bottomButtonHeight = st::historyPollBottomButtonSkip;\n\tauto newHeight = st::historyPollQuestionTop\n\t\t+ _title.countHeight(innerWidth)\n\t\t+ st::historyPollSubtitleSkip\n\t\t+ st::msgDateFont->height\n\t\t+ st::historyPollAnswersSkip\n\t\t+ tasksHeight\n\t\t+ st::historyPollTotalVotesSkip\n\t\t+ bottomButtonHeight\n\t\t+ st::msgDateFont->height\n\t\t+ st::msgPadding.bottom();\n\tif (!isBubbleTop()) {\n\t\tnewHeight -= st::msgFileTopMinus;\n\t}\n\treturn { newWidth, newHeight };\n}\n\nvoid TodoList::updateTexts() {\n\tif (_todoListVersion == _todolist->version) {\n\t\treturn;\n\t}\n\tconst auto skipAnimations = _tasks.empty();\n\t_todoListVersion = _todolist->version;\n\n\tif (_title.toTextWithEntities() != _todolist->title) {\n\t\tauto options = Ui::WebpageTextTitleOptions();\n\t\toptions.maxw = options.maxh = 0;\n\t\t_title.setMarkedText(\n\t\t\tst::historyPollQuestionStyle,\n\t\t\t_todolist->title,\n\t\t\toptions,\n\t\t\tCore::TextContext({\n\t\t\t\t.session = &_todolist->session(),\n\t\t\t\t.repaint = [=] { repaint(); },\n\t\t\t\t.customEmojiLoopLimit = 2,\n\t\t\t}));\n\t\tInitElementTextPart(_parent, _title);\n\t}\n\tif (_flags != _todolist->flags() || _subtitle.isEmpty()) {\n\t\t_flags = _todolist->flags();\n\t\t_subtitle.setText(\n\t\t\tst::msgDateTextStyle,\n\t\t\t(!_todolist->othersCanComplete()\n\t\t\t\t? tr::lng_todo_title(tr::now)\n\t\t\t\t: _parent->data()->history()->peer->isUser()\n\t\t\t\t? tr::lng_todo_title_user(tr::now)\n\t\t\t\t: tr::lng_todo_title_group(tr::now)));\n\t}\n\tupdateTasks(skipAnimations);\n}\n\nvoid TodoList::updateTasks(bool skipAnimations) {\n\tconst auto context = Core::TextContext({\n\t\t.session = &_todolist->session(),\n\t\t.repaint = [=] { repaint(); },\n\t\t.customEmojiLoopLimit = 2,\n\t});\n\tconst auto changed = !ranges::equal(\n\t\t_tasks,\n\t\t_todolist->items,\n\t\tranges::equal_to(),\n\t\t&Task::id,\n\t\t&TodoListItem::id);\n\tif (!changed) {\n\t\tauto animated = false;\n\t\tauto &&tasks = ranges::views::zip(_tasks, _todolist->items);\n\t\tfor (auto &&[task, original] : tasks) {\n\t\t\tconst auto wasDate = task.completionDate;\n\t\t\ttask.fillData(_parent, _todolist, original, context);\n\t\t\tif (!skipAnimations && (!wasDate != !task.completionDate)) {\n\t\t\t\tstartToggleAnimation(task);\n\t\t\t\tanimated = true;\n\t\t\t}\n\t\t}\n\t\tupdateCompletionStatus();\n\t\tif (animated) {\n\t\t\tmaybeStartFireworks();\n\t\t}\n\t\treturn;\n\t}\n\tconst auto has = hasHeavyPart();\n\t_tasks = ranges::views::all(\n\t\t_todolist->items\n\t) | ranges::views::transform([&](const TodoListItem &item) {\n\t\tauto result = Task();\n\t\tresult.id = item.id;\n\t\tresult.fillData(_parent, _todolist, item, context);\n\t\treturn result;\n\t}) | ranges::to_vector;\n\n\tfor (auto &task : _tasks) {\n\t\ttask.handler = createTaskClickHandler(task);\n\t}\n\n\tupdateCompletionStatus();\n\n\tif (has && !hasHeavyPart()) {\n\t\t_parent->checkHeavyPart();\n\t}\n}\n\nClickHandlerPtr TodoList::createTaskClickHandler(\n\t\tconst Task &task) {\n\tconst auto id = task.id;\n\tauto result = std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {\n\t\ttoggleCompletion(id);\n\t}));\n\tresult->setProperty(kTodoListItemIdProperty, id);\n\treturn result;\n}\n\nvoid TodoList::startToggleAnimation(Task &task) {\n\tconst auto selected = (task.completionDate != 0);\n\ttask.selectedAnimation.start(\n\t\t[=] { repaint(); },\n\t\tselected ? 0. : 1.,\n\t\tselected ? 1. : 0.,\n\t\tst::defaultCheck.duration);\n}\n\nvoid TodoList::toggleCompletion(int id) {\n\tif (_parent->data()->isBusinessShortcut()) {\n\t\treturn;\n\t} else if (_parent->data()->Has<HistoryMessageForwarded>()) {\n\t\t_parent->delegate()->elementShowTooltip(\n\t\t\ttr::lng_todo_mark_forwarded(tr::now, tr::rich),\n\t\t\t[] {});\n\t\treturn;\n\t} else if (!canComplete()) {\n\t\t_parent->delegate()->elementShowTooltip(\n\t\t\ttr::lng_todo_mark_restricted(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\ttr::bold(_parent->data()->from()->shortName()),\n\t\t\t\ttr::rich), [] {});\n\t\treturn;\n\t} else if (!_parent->history()->session().premium()) {\n\t\tWindow::PeerMenuTodoWantsPremium(Window::TodoWantsPremium::Mark);\n\t\treturn;\n\t}\n\tconst auto i = ranges::find(\n\t\t_tasks,\n\t\tid,\n\t\t&Task::id);\n\tif (i == end(_tasks)) {\n\t\treturn;\n\t}\n\n\tconst auto selected = (i->completionDate != 0);\n\ti->completionDate = selected ? TimeId() : base::unixtime::now();\n\tif (!selected) {\n\t\ti->setCompletedBy(_parent->history()->session().user());\n\t}\n\n\tconst auto parentMedia = _parent->data()->media();\n\tconst auto baseList = parentMedia ? parentMedia->todolist() : nullptr;\n\tif (baseList) {\n\t\tconst auto j = ranges::find(baseList->items, id, &TodoListItem::id);\n\t\tif (j != end(baseList->items)) {\n\t\t\tj->completionDate = i->completionDate;\n\t\t\tj->completedBy = i->completedBy;\n\t\t}\n\t\thistory()->owner().updateDependentMessages(_parent->data());\n\t}\n\n\tstartToggleAnimation(*i);\n\trepaint();\n\n\thistory()->session().api().todoLists().toggleCompletion(\n\t\t_parent->data()->fullId(),\n\t\tid,\n\t\t!selected);\n\n\tmaybeStartFireworks();\n}\n\nvoid TodoList::maybeStartFireworks() {\n\tif (!ranges::contains(_tasks, TimeId(), &Task::completionDate)\n\t\t&& !_fireworksAnimation) {\n\t\t_fireworksAnimation = std::make_unique<Ui::FireworksAnimation>(\n\t\t\t[=] { repaint(); });\n\t}\n}\n\nvoid TodoList::updateCompletionStatus() {\n\tconst auto incompleted = int(ranges::count(\n\t\t_todolist->items,\n\t\tnullptr,\n\t\t&TodoListItem::completedBy));\n\tconst auto total = int(_todolist->items.size());\n\tif (_total == total\n\t\t&& _incompleted == incompleted\n\t\t&& !_completionStatusLabel.isEmpty()) {\n\t\treturn;\n\t}\n\t_total = total;\n\t_incompleted = incompleted;\n\tconst auto totalText = QString::number(total);\n\tconst auto string = (incompleted == total)\n\t\t? tr::lng_todo_completed_none(tr::now, lt_total, totalText)\n\t\t: tr::lng_todo_completed(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\ttotal - incompleted,\n\t\t\tlt_total,\n\t\t\ttotalText);\n\t_completionStatusLabel.setText(st::msgDateTextStyle, string);\n}\n\nvoid TodoList::draw(Painter &p, const PaintContext &context) const {\n\tif (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;\n\tauto paintw = width();\n\n\tconst auto stm = context.messageStyle();\n\tconst auto padding = st::msgPadding;\n\tauto tshift = st::historyPollQuestionTop;\n\tif (!isBubbleTop()) {\n\t\ttshift -= st::msgFileTopMinus;\n\t}\n\tpaintw -= padding.left() + padding.right();\n\n\tp.setPen(stm->historyTextFg);\n\t_title.draw(p, {\n\t\t.position = { padding.left(), tshift },\n\t\t.availableWidth = paintw,\n\t\t.palette = &stm->textPalette,\n\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t.now = context.now,\n\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t.selection = context.selection,\n\t});\n\ttshift += _title.countHeight(paintw) + st::historyPollSubtitleSkip;\n\n\tp.setPen(stm->msgDateFg);\n\t_subtitle.drawLeftElided(p, padding.left(), tshift, paintw, width());\n\ttshift += st::msgDateFont->height + st::historyPollAnswersSkip;\n\n\tauto heavy = false;\n\tauto created = false;\n\tauto &&tasks = ranges::views::zip(\n\t\t_tasks,\n\t\tranges::views::ints(0, int(_tasks.size())));\n\tfor (const auto &[task, index] : tasks) {\n\t\tconst auto was = !task.userpic.null();\n\t\tconst auto height = paintTask(\n\t\t\tp,\n\t\t\ttask,\n\t\t\tpadding.left(),\n\t\t\ttshift,\n\t\t\tpaintw,\n\t\t\twidth(),\n\t\t\tcontext);\n\t\tappendTaskHighlight(task.id, tshift, height, context);\n\t\tif (was) {\n\t\t\theavy = true;\n\t\t} else if (!task.userpic.null()) {\n\t\t\tcreated = true;\n\t\t}\n\t\ttshift += height;\n\t}\n\tif (!heavy && created) {\n\t\thistory()->owner().registerHeavyViewPart(_parent);\n\t}\n\tpaintBottom(p, padding.left(), tshift, paintw, context);\n}\n\nvoid TodoList::paintBottom(\n\t\tPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint paintw,\n\t\tconst PaintContext &context) const {\n\tconst auto stringtop = top\n\t\t+ st::msgPadding.bottom()\n\t\t+ st::historyChecklistBottomTop;\n\tconst auto stm = context.messageStyle();\n\n\tp.setPen(stm->msgDateFg);\n\t_completionStatusLabel.draw(p, left, stringtop, paintw, style::al_top);\n}\n\nvoid TodoList::radialAnimationCallback() const {\n\tif (!anim::Disabled()) {\n\t\trepaint();\n\t}\n}\n\nint TodoList::paintTask(\n\t\tPainter &p,\n\t\tconst Task &task,\n\t\tint left,\n\t\tint top,\n\t\tint width,\n\t\tint outerWidth,\n\t\tconst PaintContext &context) const {\n\tconst auto height = countTaskHeight(task, width);\n\tconst auto stm = context.messageStyle();\n\tconst auto aleft = left + st::historyChecklistTaskPadding.left();\n\tconst auto awidth = width\n\t\t- st::historyChecklistTaskPadding.left()\n\t\t- st::historyChecklistTaskPadding.right();\n\n\tif (task.ripple) {\n\t\tp.setOpacity(st::historyPollRippleOpacity);\n\t\ttask.ripple->paint(\n\t\t\tp,\n\t\t\tleft - st::msgPadding.left(),\n\t\t\ttop,\n\t\t\touterWidth,\n\t\t\t&stm->msgWaveformInactive->c);\n\t\tif (task.ripple->empty()) {\n\t\t\ttask.ripple.reset();\n\t\t}\n\t\tp.setOpacity(1.);\n\t}\n\n\tif (canComplete()) {\n\t\tpaintRadio(p, task, left, top, context);\n\t} else {\n\t\tpaintStatus(p, task, left, top, context);\n\t}\n\n\ttop += task.completionDate\n\t\t? st::historyChecklistCheckedTop\n\t\t: st::historyChecklistTaskPadding.top();\n\tp.setPen(stm->historyTextFg);\n\ttask.text.draw(p, {\n\t\t.position = { aleft, top },\n\t\t.availableWidth = awidth,\n\t\t.palette = &stm->textPalette,\n\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t.now = context.now,\n\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t});\n\tif (task.completionDate) {\n\t\tconst auto nameTop = top\n\t\t\t+ height\n\t\t\t- st::historyChecklistTaskPadding.bottom()\n\t\t\t+ st::historyChecklistCheckedTop\n\t\t\t- st::normalFont->height;\n\t\tp.setPen(stm->msgDateFg);\n\t\ttask.name.drawLeft(p, aleft, nameTop, awidth, outerWidth);\n\t}\n\treturn height;\n}\n\nvoid TodoList::appendTaskHighlight(\n\t\tint id,\n\t\tint top,\n\t\tint height,\n\t\tconst PaintContext &context) const {\n\tif (context.highlight.todoItemId != id\n\t\t|| context.highlight.collapsion <= 0.) {\n\t\treturn;\n\t}\n\tconst auto to = context.highlightInterpolateTo;\n\tconst auto toProgress = (1. - context.highlight.collapsion);\n\tif (toProgress >= 1.) {\n\t\tcontext.highlightPathCache->addRect(to);\n\t} else if (toProgress <= 0.) {\n\t\tcontext.highlightPathCache->addRect(0, top, width(), height);\n\t} else {\n\t\tconst auto lerp = [=](int from, int to) {\n\t\t\treturn from + (to - from) * toProgress;\n\t\t};\n\t\tcontext.highlightPathCache->addRect(\n\t\t\tlerp(0, to.x()),\n\t\t\tlerp(top, to.y()),\n\t\t\tlerp(width(), to.width()),\n\t\t\tlerp(height, to.height()));\n\t}\n}\n\nvoid TodoList::paintRadio(\n\t\tPainter &p,\n\t\tconst Task &task,\n\t\tint left,\n\t\tint top,\n\t\tconst PaintContext &context) const {\n\ttop += st::historyChecklistTaskPadding.top();\n\n\tconst auto stm = context.messageStyle();\n\n\tPainterHighQualityEnabler hq(p);\n\tconst auto &radio = st::historyPollRadio;\n\tconst auto over = ClickHandler::showAsActive(task.handler);\n\tconst auto &regular = stm->msgDateFg;\n\n\tconst auto checkmark = task.selectedAnimation.value(\n\t\ttask.completionDate ? 1. : 0.);\n\n\tconst auto o = p.opacity();\n\tif (checkmark < 1.) {\n\t\tp.setBrush(Qt::NoBrush);\n\t\tp.setOpacity(o * (over ? st::historyPollRadioOpacityOver : st::historyPollRadioOpacity));\n\t}\n\n\tconst auto rect = QRectF(left, top, radio.diameter, radio.diameter).marginsRemoved(QMarginsF(radio.thickness / 2., radio.thickness / 2., radio.thickness / 2., radio.thickness / 2.));\n\tif (checkmark > 0. && task.completedBy) {\n\t\tconst auto skip = st::lineWidth;\n\t\tconst auto userpic = QRect(\n\t\t\tleft + (radio.diameter / 2) + skip,\n\t\t\ttop + skip,\n\t\t\tradio.diameter - 2 * skip,\n\t\t\tradio.diameter - 2 * skip);\n\t\tif (checkmark < 1.) {\n\t\t\tp.save();\n\t\t\tp.setOpacity(checkmark);\n\t\t\tp.translate(QRectF(userpic).center());\n\t\t\tconst auto ratio = 0.4 + 0.6 * checkmark;\n\t\t\tp.scale(ratio, ratio);\n\t\t\tp.translate(-QRectF(userpic).center());\n\t\t}\n\t\ttask.completedBy->paintUserpic(\n\t\t\tp,\n\t\t\ttask.userpic,\n\t\t\tuserpic.left(),\n\t\t\tuserpic.top(),\n\t\t\tuserpic.width());\n\t\tif (checkmark < 1.) {\n\t\t\tp.restore();\n\t\t}\n\t}\n\tif (checkmark < 1.) {\n\t\tauto pen = regular->p;\n\t\tpen.setWidth(radio.thickness);\n\t\tp.setPen(pen);\n\t\tp.drawEllipse(rect);\n\t}\n\n\tif (checkmark > 0.) {\n\t\tconst auto removeFull = (radio.diameter / 2 - radio.thickness);\n\t\tconst auto removeNow = removeFull * (1. - checkmark);\n\t\tconst auto color = stm->msgFileThumbLinkFg;\n\t\tauto pen = color->p;\n\t\tpen.setWidth(radio.thickness);\n\t\tp.setPen(pen);\n\t\tp.setBrush(color);\n\t\tp.drawEllipse(rect.marginsRemoved({ removeNow, removeNow, removeNow, removeNow }));\n\t\tconst auto &icon = stm->historyPollChosen;\n\t\ticon.paint(p, left + (radio.diameter - icon.width()) / 2, top + (radio.diameter - icon.height()) / 2, width());\n\n\t\tconst auto stm = context.messageStyle();\n\t\tauto bgpen = stm->msgBg->p;\n\t\tbgpen.setWidth(st::lineWidth);\n\t\tconst auto outline = QRect(left, top, radio.diameter, radio.diameter);\n\t\tconst auto paintContent = [&](QPainter &p) {\n\t\t\tp.setPen(bgpen);\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(outline);\n\t\t};\n\t\tif (usesBubblePattern(context)) {\n\t\t\tconst auto add = st::lineWidth * 3;\n\t\t\tconst auto target = outline.marginsAdded(\n\t\t\t\t{ add, add, add, add });\n\t\t\tUi::PaintPatternBubblePart(\n\t\t\t\tp,\n\t\t\t\tcontext.viewport,\n\t\t\t\tcontext.bubblesPattern->pixmap,\n\t\t\t\ttarget,\n\t\t\t\tpaintContent,\n\t\t\t\t_userpicCircleCache);\n\t\t} else {\n\t\t\tpaintContent(p);\n\t\t}\n\t}\n\n\tp.setOpacity(o);\n}\n\nvoid TodoList::paintStatus(\n\t\tPainter &p,\n\t\tconst Task &task,\n\t\tint left,\n\t\tint top,\n\t\tconst PaintContext &context) const {\n\ttop += st::historyChecklistTaskPadding.top();\n\n\tconst auto stm = context.messageStyle();\n\n\tconst auto &radio = st::historyPollRadio;\n\tconst auto completed = (task.completionDate != 0);\n\n\tconst auto rect = QRect(left, top, radio.diameter, radio.diameter);\n\tif (completed) {\n\t\tconst auto &icon = stm->historyPollChosen;\n\t\ticon.paint(\n\t\t\tp,\n\t\t\tleft + (radio.diameter - icon.width()) / 2,\n\t\t\ttop + (radio.diameter - icon.height()) / 2,\n\t\t\twidth(),\n\t\t\tstm->msgFileBg->c);\n\t} else {\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(stm->msgFileBg);\n\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.drawEllipse(style::centerrect(\n\t\t\trect,\n\t\t\tQRect(0, 0, st::mediaUnreadSize, st::mediaUnreadSize)));\n\t}\n}\n\nTextSelection TodoList::adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const {\n\treturn _title.adjustSelection(selection, type);\n}\n\nuint16 TodoList::fullSelectionLength() const {\n\treturn _title.length();\n}\n\nTextForMimeData TodoList::selectedText(TextSelection selection) const {\n\treturn _title.toTextForMimeData(selection);\n}\n\nTextState TodoList::textState(QPoint point, StateRequest request) const {\n\tauto result = TextState(_parent);\n\tconst auto padding = st::msgPadding;\n\tauto paintw = width();\n\tauto tshift = st::historyPollQuestionTop;\n\tif (!isBubbleTop()) {\n\t\ttshift -= st::msgFileTopMinus;\n\t}\n\tpaintw -= padding.left() + padding.right();\n\n\tconst auto questionH = _title.countHeight(paintw);\n\tif (QRect(padding.left(), tshift, paintw, questionH).contains(point)) {\n\t\tresult = TextState(_parent, _title.getState(\n\t\t\tpoint - QPoint(padding.left(), tshift),\n\t\t\tpaintw,\n\t\t\trequest.forText()));\n\t\treturn result;\n\t}\n\tconst auto aleft = padding.left()\n\t\t+ st::historyChecklistTaskPadding.left();\n\tconst auto awidth = paintw\n\t\t- st::historyChecklistTaskPadding.left()\n\t\t- st::historyChecklistTaskPadding.right();\n\ttshift += questionH + st::historyPollSubtitleSkip;\n\ttshift += st::msgDateFont->height + st::historyPollAnswersSkip;\n\tfor (const auto &task : _tasks) {\n\t\tconst auto height = countTaskHeight(task, paintw);\n\t\tif (point.y() >= tshift && point.y() < tshift + height) {\n\t\t\tconst auto atop = tshift\n\t\t\t\t+ (task.completionDate\n\t\t\t\t\t? st::historyChecklistCheckedTop\n\t\t\t\t\t: st::historyChecklistTaskPadding.top());\n\t\t\tauto taskTextResult = task.text.getState(\n\t\t\t\tpoint - QPoint(aleft, atop),\n\t\t\t\tawidth,\n\t\t\t\trequest.forText());\n\t\t\tif (taskTextResult.link) {\n\t\t\t\tresult.link = taskTextResult.link;\n\t\t\t} else {\n\t\t\t\t_lastLinkPoint = point;\n\t\t\t\tresult.link = task.handler;\n\t\t\t}\n\t\t\tif (task.completionDate) {\n\t\t\t\tresult.customTooltip = true;\n\t\t\t\tusing Flag = Ui::Text::StateRequest::Flag;\n\t\t\t\tif (request.flags & Flag::LookupCustomTooltip) {\n\t\t\t\t\tresult.customTooltipText = langDateTimeFull(\n\t\t\t\t\t\tbase::unixtime::parse(task.completionDate));\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\t\ttshift += height;\n\t}\n\treturn result;\n}\n\nvoid TodoList::paintBubbleFireworks(\n\t\tPainter &p,\n\t\tconst QRect &bubble,\n\t\tcrl::time ms) const {\n\tif (!_fireworksAnimation || _fireworksAnimation->paint(p, bubble)) {\n\t\treturn;\n\t}\n\t_fireworksAnimation = nullptr;\n}\n\nvoid TodoList::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) {\n\tif (!handler) return;\n\n\tconst auto i = ranges::find(\n\t\t_tasks,\n\t\thandler,\n\t\t&Task::handler);\n\tif (i != end(_tasks)) {\n\t\ttoggleRipple(*i, pressed);\n\t}\n}\n\nvoid TodoList::unloadHeavyPart() {\n\tfor (auto &task : _tasks) {\n\t\ttask.userpic = {};\n\t}\n}\n\nbool TodoList::hasHeavyPart() const {\n\tfor (auto &task : _tasks) {\n\t\tif (!task.userpic.null()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid TodoList::hideSpoilers() {\n\tif (_title.hasSpoilers()) {\n\t\t_title.setSpoilerRevealed(false, anim::type::instant);\n\t}\n\tfor (auto &task : _tasks) {\n\t\tif (task.text.hasSpoilers()) {\n\t\t\ttask.text.setSpoilerRevealed(false, anim::type::instant);\n\t\t}\n\t}\n}\n\nstd::vector<Media::TodoTaskInfo> TodoList::takeTasksInfo() {\n\tif (_tasks.empty()) {\n\t\treturn {};\n\t}\n\treturn _tasks | ranges::views::transform([](const Task &task) {\n\t\treturn TodoTaskInfo{\n\t\t\t.id = task.id,\n\t\t\t.completedBy = task.completedBy,\n\t\t\t.completionDate = task.completionDate,\n\t\t};\n\t}) | ranges::to_vector;\n}\n\nvoid TodoList::toggleRipple(Task &task, bool pressed) {\n\tif (pressed) {\n\t\tconst auto outerWidth = width();\n\t\tconst auto innerWidth = outerWidth\n\t\t\t- st::msgPadding.left()\n\t\t\t- st::msgPadding.right();\n\t\tif (!task.ripple) {\n\t\t\tauto mask = Ui::RippleAnimation::RectMask(QSize(\n\t\t\t\touterWidth,\n\t\t\t\tcountTaskHeight(task, innerWidth)));\n\t\t\ttask.ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\tstd::move(mask),\n\t\t\t\t[=] { repaint(); });\n\t\t}\n\t\tconst auto top = countTaskTop(task, innerWidth);\n\t\ttask.ripple->add(_lastLinkPoint - QPoint(0, top));\n\t} else if (task.ripple) {\n\t\ttask.ripple->lastStop();\n\t}\n}\n\nint TodoList::bottomButtonHeight() const {\n\tconst auto skip = st::historyPollChoiceRight.height()\n\t\t- st::historyPollFillingBottom\n\t\t- st::historyPollFillingHeight\n\t\t- (st::historyPollChoiceRight.height() - st::historyPollFillingHeight) / 2;\n\treturn st::historyPollTotalVotesSkip\n\t\t- skip\n\t\t+ st::historyPollBottomButtonSkip\n\t\t+ st::msgDateFont->height\n\t\t+ st::msgPadding.bottom();\n}\n\nTodoList::~TodoList() {\n\thistory()->owner().unregisterTodoListView(_todolist, _parent);\n\tif (hasHeavyPart()) {\n\t\tunloadHeavyPart();\n\t\t_parent->checkHeavyPart();\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_todo_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n#include \"ui/effects/animations.h\"\n#include \"data/data_todo_list.h\"\n#include \"base/weak_ptr.h\"\n\nnamespace Ui {\nclass RippleAnimation;\nclass FireworksAnimation;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass Message;\n\nclass TodoList final : public Media {\npublic:\n\tTodoList(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<TodoListData*> todolist,\n\t\tElement *replacing);\n\t~TodoList();\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\n\tbool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {\n\t\treturn true;\n\t}\n\tbool dragItemByHandler(const ClickHandlerPtr &p) const override {\n\t\treturn true;\n\t}\n\n\tbool needsBubble() const override {\n\t\treturn true;\n\t}\n\tbool customInfoLayout() const override {\n\t\treturn false;\n\t}\n\n\t[[nodiscard]] TextSelection adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const override;\n\tuint16 fullSelectionLength() const override;\n\tTextForMimeData selectedText(TextSelection selection) const override;\n\n\tvoid paintBubbleFireworks(\n\t\tPainter &p,\n\t\tconst QRect &bubble,\n\t\tcrl::time ms) const override;\n\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed) override;\n\n\tvoid unloadHeavyPart() override;\n\tbool hasHeavyPart() const override;\n\n\tvoid hideSpoilers() override;\n\n\tstd::vector<TodoTaskInfo> takeTasksInfo() override;\n\nprivate:\n\tstruct Task;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\n\t[[nodiscard]] bool canComplete() const;\n\n\t[[nodiscard]] int countTaskTop(\n\t\tconst Task &task,\n\t\tint innerWidth) const;\n\t[[nodiscard]] int countTaskHeight(\n\t\tconst Task &task,\n\t\tint innerWidth) const;\n\t[[nodiscard]] ClickHandlerPtr createTaskClickHandler(\n\t\tconst Task &task);\n\tvoid updateTexts();\n\tvoid updateTasks(bool skipAnimations);\n\tvoid startToggleAnimation(Task &task);\n\tvoid updateCompletionStatus();\n\tvoid maybeStartFireworks();\n\tvoid setupPreviousState(const std::vector<TodoTaskInfo> &info);\n\n\tint paintTask(\n\t\tPainter &p,\n\t\tconst Task &task,\n\t\tint left,\n\t\tint top,\n\t\tint width,\n\t\tint outerWidth,\n\t\tconst PaintContext &context) const;\n\tvoid paintRadio(\n\t\tPainter &p,\n\t\tconst Task &task,\n\t\tint left,\n\t\tint top,\n\t\tconst PaintContext &context) const;\n\tvoid paintStatus(\n\t\tPainter &p,\n\t\tconst Task &task,\n\t\tint left,\n\t\tint top,\n\t\tconst PaintContext &context) const;\n\tvoid paintBottom(\n\t\tPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint paintw,\n\t\tconst PaintContext &context) const;\n\tvoid appendTaskHighlight(\n\t\tint id,\n\t\tint top,\n\t\tint height,\n\t\tconst PaintContext &context) const;\n\n\tvoid radialAnimationCallback() const;\n\n\tvoid toggleRipple(Task &task, bool pressed);\n\tvoid toggleCompletion(int id);\n\n\t[[nodiscard]] int bottomButtonHeight() const;\n\n\tconst not_null<TodoListData*> _todolist;\n\tint _todoListVersion = 0;\n\tint _total = 0;\n\tint _incompleted = 0;\n\tTodoListData::Flags _flags = TodoListData::Flags();\n\n\tUi::Text::String _title;\n\tUi::Text::String _subtitle;\n\n\tstd::vector<Task> _tasks;\n\tUi::Text::String _completionStatusLabel;\n\n\tmutable std::unique_ptr<Ui::FireworksAnimation> _fireworksAnimation;\n\tmutable QPoint _lastLinkPoint;\n\tmutable QImage _userpicCircleCache;\n\tmutable QImage _fillingIconCache;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_unique_gift.h\"\n\n#include \"base/unixtime.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_birthday.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_session.h\"\n#include \"data/data_star_gift.h\"\n#include \"data/data_web_page.h\"\n#include \"history/view/media/history_view_media_generic.h\"\n#include \"history/view/media/history_view_premium_gift.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"info/peer_gifts/info_peer_gifts_common.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/ministar_particles.h\"\n#include \"ui/effects/premium_stars_colored.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/rect.h\"\n#include \"ui/top_background_gradient.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_polls.h\"\n\nnamespace HistoryView {\nnamespace {\n\nclass ButtonPart final : public MediaGenericPart {\npublic:\n\tButtonPart(\n\t\tconst QString &text,\n\t\tQMargins margins,\n\t\tFn<void()> repaint,\n\t\tClickHandlerPtr link,\n\t\tQColor bg = QColor(0, 0, 0, 0));\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const override;\n\tTextState textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const override;\n\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed) override;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\nprivate:\n\tUi::Text::String _text;\n\tQMargins _margins;\n\tQColor _bg;\n\tQSize _size;\n\n\tClickHandlerPtr _link;\n\tstd::unique_ptr<Ui::RippleAnimation> _ripple;\n\tmutable Ui::Premium::ColoredMiniStars _stars;\n\tmutable std::optional<QColor> _starsLastColor;\n\tFn<void()> _repaint;\n\n\tmutable QPoint _lastPoint;\n\n};\n\nclass TextBubblePart final : public MediaGenericTextPart {\npublic:\n\tTextBubblePart(\n\t\tTextWithEntities text,\n\t\tQMargins margins,\n\t\tData::UniqueGiftBackdrop backdrop,\n\t\tClickHandlerPtr link);\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const override;\n\tTextState textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const override;\n\nprivate:\n\tvoid setupPen(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context) const override;\n\tint elisionLines() const override;\n\n\tData::UniqueGiftBackdrop _backdrop;\n\tClickHandlerPtr _link;\n\n};\n\nTextBubblePart::TextBubblePart(\n\tTextWithEntities text,\n\tQMargins margins,\n\tData::UniqueGiftBackdrop backdrop,\n\tClickHandlerPtr link)\n: MediaGenericTextPart(\n\tstd::move(text),\n\tmargins,\n\tst::uniqueGiftReleasedBy.style,\n\t{},\n\t{},\n\tstyle::al_top)\n, _backdrop(backdrop)\n, _link(std::move(link)) {\n}\n\nvoid TextBubblePart::draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const {\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setPen(Qt::NoPen);\n\tp.setOpacity(0.5);\n\tp.setBrush(_backdrop.patternColor);\n\tconst auto radius = height() / 2.;\n\tconst auto left = (outerWidth - width()) / 2;\n\tconst auto r = QRect(left, 0, width(), height());\n\tp.drawRoundedRect(r, radius, radius);\n\tp.setOpacity(1.);\n\n\tMediaGenericTextPart::draw(p, owner, context, outerWidth);\n}\n\nTextState TextBubblePart::textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const {\n\tauto result = TextState();\n\tconst auto left = (outerWidth - width()) / 2;\n\tif (point.x() >= left\n\t\t&& point.y() >= 0\n\t\t&& point.x() < left + width()\n\t\t&& point.y() < height()) {\n\t\tresult.link = _link;\n\t}\n\treturn result;\n}\n\nvoid TextBubblePart::setupPen(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context) const {\n\tp.setPen(_backdrop.textColor);\n}\n\nint TextBubblePart::elisionLines() const {\n\treturn 1;\n}\n\nButtonPart::ButtonPart(\n\tconst QString &text,\n\tQMargins margins,\n\tFn<void()> repaint,\n\tClickHandlerPtr link,\n\tQColor bg)\n: _text(st::semiboldTextStyle, text)\n, _margins(margins)\n, _bg(bg)\n, _size(\n\t(_text.maxWidth()\n\t\t+ st::msgServiceGiftBoxButtonHeight\n\t\t+ st::msgServiceGiftBoxButtonPadding.left()\n\t\t+ st::msgServiceGiftBoxButtonPadding.right()),\n\tst::msgServiceGiftBoxButtonHeight)\n, _link(std::move(link))\n, _stars([=](const QRect &) {\n\trepaint();\n}, Ui::Premium::MiniStarsType::SlowStars)\n, _repaint(std::move(repaint)) {\n}\n\nvoid ButtonPart::draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const {\n\tPainterHighQualityEnabler hq(p);\n\n\tconst auto customColors = (_bg.alpha() > 0);\n\n\tconst auto position = QPoint(\n\t\t(outerWidth - width()) / 2 + _margins.left(),\n\t\t_margins.top());\n\tp.translate(position);\n\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(customColors ? QBrush(_bg) : context.st->msgServiceBg());\n\tconst auto radius = _size.height() / 2.;\n\tconst auto r = Rect(_size);\n\tp.drawRoundedRect(r, radius, radius);\n\n\tauto white = QColor(255, 255, 255);\n\tconst auto fg = customColors ? white : context.st->msgServiceFg()->c;\n\tif (!_starsLastColor || *_starsLastColor != fg) {\n\t\t_starsLastColor = fg;\n\t\t_stars.setColorOverride(QGradientStops{\n\t\t\t{ 0., anim::with_alpha(fg, .3) },\n\t\t\t{ 1., fg },\n\t\t});\n\t\tconst auto padding = _size.height() / 2;\n\t\t_stars.setCenter(\n\t\t\tRect(_size) - QMargins(padding, 0, padding, 0));\n\t}\n\n\tauto clipPath = QPainterPath();\n\tclipPath.addRoundedRect(r, radius, radius);\n\tp.setClipPath(clipPath);\n\t_stars.setPaused(context.paused);\n\t_stars.paint(p);\n\tp.setClipping(false);\n\n\tif (_ripple) {\n\t\tconst auto opacity = p.opacity();\n\t\tconst auto ripple = customColors\n\t\t\t? anim::with_alpha(fg, .3)\n\t\t\t: context.messageStyle()->msgWaveformInactive->c;\n\t\tp.setOpacity(st::historyPollRippleOpacity);\n\t\t_ripple->paint(\n\t\t\tp,\n\t\t\t0,\n\t\t\t0,\n\t\t\twidth(),\n\t\t\t&ripple);\n\t\tp.setOpacity(opacity);\n\t}\n\n\tp.setPen(fg);\n\t_text.draw(\n\t\tp,\n\t\t0,\n\t\t(_size.height() - _text.minHeight()) / 2,\n\t\t_size.width(),\n\t\tstyle::al_top);\n\n\tp.translate(-position);\n}\n\nTextState ButtonPart::textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const {\n\tpoint -= QPoint{\n\t\t(outerWidth - width()) / 2 + _margins.left(),\n\t\t_margins.top()\n\t};\n\tif (QRect(QPoint(), _size).contains(point)) {\n\t\tauto result = TextState();\n\t\tresult.link = _link;\n\t\t_lastPoint = point;\n\t\treturn result;\n\t}\n\treturn {};\n}\n\nvoid ButtonPart::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed) {\n\tif (p != _link) {\n\t\treturn;\n\t} else if (pressed) {\n\t\tif (!_ripple) {\n\t\t\tconst auto radius = _size.height() / 2;\n\t\t\t_ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\tUi::RippleAnimation::RoundRectMask(_size, radius),\n\t\t\t\t_repaint);\n\t\t}\n\t\t_ripple->add(_lastPoint);\n\t} else if (_ripple) {\n\t\t_ripple->lastStop();\n\t}\n}\n\nQSize ButtonPart::countOptimalSize() {\n\treturn {\n\t\t_margins.left() + _size.width() + _margins.right(),\n\t\t_margins.top() + _size.height() + _margins.bottom(),\n\t};\n}\n\nQSize ButtonPart::countCurrentSize(int newWidth) {\n\treturn optimalSize();\n}\n\n} // namespace\n\nauto GenerateUniqueGiftMedia(\n\tnot_null<Element*> parent,\n\tElement *replacing,\n\tstd::shared_ptr<Data::UniqueGift> gift)\n-> Fn<void(\n\t\tnot_null<MediaGeneric*>,\n\t\tFn<void(std::unique_ptr<MediaGenericPart>)>)> {\n\treturn [=](\n\t\t\tnot_null<MediaGeneric*> media,\n\t\t\tFn<void(std::unique_ptr<MediaGenericPart>)> push) {\n\t\tauto pushText = [&](\n\t\t\t\tTextWithEntities text,\n\t\t\t\tconst style::TextStyle &st,\n\t\t\t\tQColor color,\n\t\t\t\tQMargins margins) {\n\t\t\tif (text.empty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpush(std::make_unique<TextPartColored>(\n\t\t\t\tstd::move(text),\n\t\t\t\tmargins,\n\t\t\t\t[color](const auto&) { return color; },\n\t\t\t\tst));\n\t\t};\n\n\t\tconst auto item = parent->data();\n\t\tconst auto itemMedia = item->media();\n\t\tconst auto fields = itemMedia ? itemMedia->gift() : nullptr;\n\t\tconst auto upgrade = fields && fields->upgrade;\n\t\tconst auto outgoing = upgrade ? !item->out() : item->out();\n\n\t\tconst auto white = QColor(255, 255, 255);\n\t\tconst auto sticker = [=] {\n\t\t\tusing Tag = ChatHelpers::StickerLottieSize;\n\t\t\treturn StickerInBubblePart::Data{\n\t\t\t\t.sticker = gift->model.document,\n\t\t\t\t.size = st::chatIntroStickerSize,\n\t\t\t\t.cacheTag = Tag::ChatIntroHelloSticker,\n\t\t\t};\n\t\t};\n\t\tpush(std::make_unique<StickerInBubblePart>(\n\t\t\tparent,\n\t\t\treplacing,\n\t\t\tsticker,\n\t\t\tst::chatUniqueStickerPadding));\n\t\tconst auto peer = parent->history()->peer;\n\t\tpushText(\n\t\t\ttr::bold(peer->isSelf()\n\t\t\t\t? (gift->crafted\n\t\t\t\t\t? tr::lng_action_gift_crafted_subtitle(tr::now)\n\t\t\t\t\t: tr::lng_action_gift_self_subtitle(tr::now))\n\t\t\t\t: peer->isServiceUser()\n\t\t\t\t? tr::lng_gift_link_label_gift(tr::now)\n\t\t\t\t: (outgoing\n\t\t\t\t\t? tr::lng_action_gift_sent_subtitle\n\t\t\t\t\t: tr::lng_action_gift_got_subtitle)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\tpeer->shortName())),\n\t\t\tst::chatUniqueTitle,\n\t\t\twhite,\n\t\t\tst::chatUniqueTitlePadding);\n\t\tpushText(\n\t\t\ttr::bold(Data::UniqueGiftName(*gift)),\n\t\t\tst::chatUniqueTextStyle,\n\t\t\tgift->backdrop.textColor,\n\t\t\tst::chatUniqueTextPadding);\n\n\t\tif (const auto by = gift->releasedBy) {\n\t\t\tconst auto handler = std::make_shared<LambdaClickHandler>([=] {\n\t\t\t\tUi::GiftReleasedByHandler(by);\n\t\t\t});\n\t\t\tpush(std::make_unique<TextBubblePart>(\n\t\t\t\ttr::lng_gift_released_by(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_name,\n\t\t\t\t\ttr::link('@' + by->username()),\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::giftBoxReleasedByMargin,\n\t\t\t\tgift->backdrop,\n\t\t\t\thandler));\n\t\t}\n\n\t\tconst auto name = [](const Data::UniqueGiftAttribute &value) {\n\t\t\treturn tr::bold(value.name);\n\t\t};\n\t\tauto attributes = std::vector<AttributeTable::Entry>{\n\t\t\t{ tr::lng_gift_unique_model(tr::now), name(gift->model) },\n\t\t\t{ tr::lng_gift_unique_symbol(tr::now), name(gift->pattern) },\n\t\t\t{ tr::lng_gift_unique_backdrop(tr::now), name(gift->backdrop) },\n\t\t};\n\t\tconst auto tableAddedMargins = gift->releasedBy\n\t\t\t? QMargins(0, st::chatUniqueAuthorSkip, 0, 0)\n\t\t\t: QMargins();\n\t\tpush(std::make_unique<AttributeTable>(\n\t\t\tstd::move(attributes),\n\t\t\tst::chatUniqueTextPadding + tableAddedMargins,\n\t\t\t[c = gift->backdrop.textColor](const auto&) { return c; },\n\t\t\t[](const auto&) { return QColor(255, 255, 255); }));\n\n\t\tauto link = OpenStarGiftLink(parent->data());\n\t\tpush(std::make_unique<ButtonPart>(\n\t\t\ttr::lng_sticker_premium_view(tr::now),\n\t\t\tst::chatUniqueButtonPadding,\n\t\t\t[=] { parent->repaint(); },\n\t\t\tstd::move(link),\n\t\t\tanim::with_alpha(gift->backdrop.patternColor, 0.75)));\n\t};\n}\n\nauto UniqueGiftBg(\n\tnot_null<Element*> view,\n\tstd::shared_ptr<Data::UniqueGift> gift)\n-> Fn<void(\n\t\tPainter&,\n\t\tconst Ui::ChatPaintContext&,\n\t\tnot_null<const MediaGeneric*>)> {\n\tstruct State {\n\t\tQImage bg;\n\t\tbase::flat_map<float64, QImage> cache;\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji> pattern;\n\t\tQImage badgeCache;\n\t\tInfo::PeerGifts::GiftBadge badgeKey;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tstate->pattern = view->history()->owner().customEmojiManager().create(\n\t\tgift->pattern.document,\n\t\t[=] { view->repaint(); },\n\t\tData::CustomEmojiSizeTag::Large);\n\t[[maybe_unused]] const auto preload = state->pattern->ready();\n\n\treturn [=](\n\t\t\tPainter &p,\n\t\t\tconst Ui::ChatPaintContext &context,\n\t\t\tnot_null<const MediaGeneric*> media) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tconst auto webpreview = (media.get() != view->media());\n\t\tconst auto sub = webpreview ? 0 : (st::chatUniqueGiftBorder / 2);\n\t\tconst auto thickness = webpreview ? 0 : st::chatUniqueGiftBorder * 2;\n\t\tconst auto removed = thickness + sub;\n\t\tconst auto radius = webpreview\n\t\t\t? st::roundRadiusLarge\n\t\t\t: (st::msgServiceGiftBoxRadius - thickness + sub);\n\t\tconst auto full = QRect(0, 0, media->width(), media->height());\n\t\tconst auto inner = full.marginsRemoved(\n\t\t\t{ removed, removed, removed, removed });\n\t\tif (!webpreview) {\n\t\t\tauto pen = context.st->msgServiceBg()->p;\n\t\t\tpen.setWidthF(thickness);\n\t\t\tp.setPen(pen);\n\t\t\tp.setBrush(Qt::transparent);\n\t\t\tp.drawRoundedRect(inner, radius, radius);\n\t\t}\n\t\tauto gradient = QRadialGradient(inner.center(), inner.height() / 2);\n\t\tgradient.setStops({\n\t\t\t{ 0., gift->backdrop.centerColor },\n\t\t\t{ 1., gift->backdrop.edgeColor },\n\t\t});\n\t\tp.setBrush(gradient);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawRoundedRect(inner, radius, radius);\n\n\t\tconst auto width = media->width();\n\t\tconst auto shift = width / 12;\n\t\tconst auto doubled = width + 2 * shift;\n\t\tconst auto top = (webpreview ? 2 : 1) * (-shift);\n\t\tconst auto outer = QRect(-shift, top, doubled, doubled);\n\t\tp.setClipRect(inner);\n\t\tUi::PaintBgPoints(\n\t\t\tp,\n\t\t\tUi::PatternBgPoints(),\n\t\t\tstate->cache,\n\t\t\tstate->pattern.get(),\n\t\t\t*gift,\n\t\t\touter);\n\t\tp.setClipping(false);\n\n\t\tconst auto padding = webpreview\n\t\t\t? QMargins()\n\t\t\t: st::chatUniqueGiftBadgePadding;\n\t\tp.setClipRect(inner.marginsAdded(padding));\n\n\t\tconst auto burned = gift->burned;\n\t\tconst auto burnedBg = Info::PeerGifts::BurnedBadgeBg();\n\t\tauto badge = Info::PeerGifts::GiftBadge{\n\t\t\t.text = (burned\n\t\t\t\t? tr::lng_gift_burned_tag(tr::now)\n\t\t\t\t: tr::lng_gift_collectible_tag(tr::now)),\n\t\t\t.bg1 = (burned ? burnedBg : gift->backdrop.edgeColor),\n\t\t\t.bg2 = (burned ? burnedBg : gift->backdrop.patternColor),\n\t\t\t.fg = (burned ? st::white->c : gift->backdrop.textColor),\n\t\t};\n\t\tif (state->badgeCache.isNull() || state->badgeKey != badge) {\n\t\t\tstate->badgeKey = badge;\n\t\t\tstate->badgeCache = ValidateRotatedBadge(badge, padding);\n\t\t}\n\t\tconst auto badgeRatio = state->badgeCache.devicePixelRatio();\n\t\tconst auto badgeWidth = state->badgeCache.width() / badgeRatio;\n\t\tp.drawImage(\n\t\t\tinner.x() + inner.width() - badgeWidth,\n\t\t\tinner.y(),\n\t\t\tstate->badgeCache);\n\t\tp.setClipping(false);\n\t};\n}\n\nauto GenerateUniqueGiftPreview(\n\tnot_null<Element*> parent,\n\tElement *replacing,\n\tstd::shared_ptr<Data::UniqueGift> gift)\n-> Fn<void(\n\t\tnot_null<MediaGeneric*>,\n\t\tFn<void(std::unique_ptr<MediaGenericPart>)>)> {\n\treturn [=](\n\t\t\tnot_null<MediaGeneric*> media,\n\t\t\tFn<void(std::unique_ptr<MediaGenericPart>)> push) {\n\t\tconst auto sticker = [=] {\n\t\t\tusing Tag = ChatHelpers::StickerLottieSize;\n\t\t\treturn StickerInBubblePart::Data{\n\t\t\t\t.sticker = gift->model.document,\n\t\t\t\t.size = st::chatIntroStickerSize,\n\t\t\t\t.cacheTag = Tag::ChatIntroHelloSticker,\n\t\t\t};\n\t\t};\n\t\tpush(std::make_unique<StickerInBubblePart>(\n\t\t\tparent,\n\t\t\treplacing,\n\t\t\tsticker,\n\t\t\tst::chatUniquePreviewPadding));\n\t};\n}\n\nauto GenerateAuctionPreview(\n\tnot_null<Element*> parent,\n\tElement *replacing,\n\tstd::shared_ptr<Data::StarGift> gift,\n\tData::UniqueGiftBackdrop backdrop)\n-> Fn<void(\n\t\tnot_null<MediaGeneric*>,\n\t\tFn<void(std::unique_ptr<MediaGenericPart>)>)> {\n\treturn [=](\n\t\t\tnot_null<MediaGeneric*> media,\n\t\t\tFn<void(std::unique_ptr<MediaGenericPart>)> push) {\n\t\tconst auto sticker = [=] {\n\t\t\tusing Tag = ChatHelpers::StickerLottieSize;\n\t\t\treturn StickerInBubblePart::Data{\n\t\t\t\t.sticker = gift->document,\n\t\t\t\t.size = st::chatIntroStickerSize,\n\t\t\t\t.cacheTag = Tag::ChatIntroHelloSticker,\n\t\t\t};\n\t\t};\n\t\tpush(std::make_unique<StickerInBubblePart>(\n\t\t\tparent,\n\t\t\treplacing,\n\t\t\tsticker,\n\t\t\tst::webPageAuctionPreviewPadding));\n\t\tconst auto name = gift->unique\n\t\t\t? Data::UniqueGiftName(*gift->unique)\n\t\t\t: gift->resellTitle;\n\t\tif (!name.isEmpty()) {\n\t\t\tpush(std::make_unique<TextPartColored>(\n\t\t\t\ttr::bold(name),\n\t\t\t\tQMargins(0, 0, 0, st::defaultVerticalListSkip),\n\t\t\t\t[c = backdrop.textColor](const auto&) { return c; },\n\t\t\t\tst::chatUniqueTitle));\n\t\t}\n\t\tif (const auto all = gift->limitedCount) {\n\t\t\tpush(std::make_unique<TextPartColored>(\n\t\t\t\ttr::lng_boosts_list_tab_gifts(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\tall,\n\t\t\t\t\ttr::marked),\n\t\t\t\tQMargins(0, 0, 0, st::webPageAuctionPreviewPadding.top()),\n\t\t\t\t[c = backdrop.textColor](const auto&) { return c; },\n\t\t\t\tst::chatUniqueTextStyle));\n\t\t}\n\t};\n}\n\nauto AuctionBg(\n\tnot_null<Element*> view,\n\tData::UniqueGiftBackdrop backdrop,\n\tstd::shared_ptr<Data::StarGift> gift,\n\tTimeId startDate,\n\tTimeId endDate)\n-> Fn<void(\n\t\tPainter&,\n\t\tconst Ui::ChatPaintContext&,\n\t\tnot_null<const MediaGeneric*>)> {\n\tstruct State {\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji> pattern;\n\t\tbase::flat_map<float64, QImage> cache;\n\t\tstd::optional<Ui::StarParticles> particles;\n\t\tstd::unique_ptr<base::Timer> timer;\n\t\tcrl::time pausedAt = 0;\n\t\tcrl::time pauseOffset = 0;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tif (gift->unique && gift->unique->pattern.document) {\n\t\tstate->pattern = view->history()->owner().customEmojiManager().create(\n\t\t\tgift->unique->pattern.document,\n\t\t\t[=] { view->repaint(); },\n\t\t\tData::CustomEmojiSizeTag::Large);\n\t}\n\tstate->particles.emplace(\n\t\tUi::StarParticles::Type::RadialInside,\n\t\t25,\n\t\tst::lineWidth * 8);\n\tstate->particles->setSpeed(0.05);\n\tstate->particles->setColor(backdrop.textColor);\n\n\treturn [=](\n\t\t\tPainter &p,\n\t\t\tconst Ui::ChatPaintContext &context,\n\t\t\tnot_null<const MediaGeneric*> media) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tconst auto webpreview = (media.get() != view->media());\n\t\tconst auto radius = webpreview\n\t\t\t? st::roundRadiusLarge\n\t\t\t: st::msgServiceGiftBoxRadius;\n\t\tconst auto full = QRect(0, 0, media->width(), media->height());\n\t\tauto gradient = QRadialGradient(full.center(), full.height() / 2);\n\t\tgradient.setStops({\n\t\t\t{ 0., backdrop.centerColor },\n\t\t\t{ 1., backdrop.edgeColor },\n\t\t});\n\t\tp.setBrush(gradient);\n\t\tp.drawRoundedRect(full, radius, radius);\n\n\t\t/*if (state->pattern) {\n\t\t\tconst auto width = media->width();\n\t\t\tconst auto shift = width / 12;\n\t\t\tconst auto doubled = width + 2 * shift;\n\t\t\tconst auto top = (webpreview ? 2 : 1) * (-shift);\n\t\t\tconst auto outer = QRect(-shift, top, doubled, doubled);\n\t\t\tp.setClipRect(full);\n\t\t\tif (gift->unique) {\n\t\t\t\tUi::PaintBgPoints(\n\t\t\t\t\tp,\n\t\t\t\t\tUi::PatternBgPoints(),\n\t\t\t\t\tstate->cache,\n\t\t\t\t\tstate->pattern.get(),\n\t\t\t\t\t*gift->unique,\n\t\t\t\t\touter);\n\t\t\t}\n\t\t\tp.setClipping(false);\n\t\t}*/\n\n\t\tif (state->particles) {\n\t\t\tp.setClipRect(full);\n\t\t\tstate->particles->paint(p, full, context.now, context.paused);\n\t\t\tp.setClipping(false);\n\t\t}\n\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto startsIn = std::max(startDate - now, 0);\n\t\tconst auto left = std::max(endDate - now, 0);\n\t\tif (startsIn > 0 || left > 0) {\n\t\t\tif (!state->timer) {\n\t\t\t\tstate->timer = std::make_unique<base::Timer>([=] {\n\t\t\t\t\tview->repaint();\n\t\t\t\t});\n\t\t\t}\n\t\t\tstate->timer->callOnce(1000);\n\t\t} else if (state->timer) {\n\t\t\tstate->timer = nullptr;\n\t\t}\n\t\tconst auto still = (startsIn > 0) ? startsIn : left;\n\t\tconst auto time = (still >= 3600)\n\t\t\t? u\"%1:%2:%3\"_q\n\t\t\t.arg(still / 3600)\n\t\t\t.arg((still % 3600) / 60, 2, 10, QChar('0'))\n\t\t\t.arg(still % 60, 2, 10, QChar('0'))\n\t\t\t: u\"%1:%2\"_q\n\t\t\t.arg(still / 60)\n\t\t\t.arg(still % 60, 2, 10, QChar('0'));\n\t\tconst auto text = (startsIn > 0)\n\t\t\t? tr::lng_auction_join_starts_in(tr::now, lt_time, time)\n\t\t\t: (left > 0)\n\t\t\t? time\n\t\t\t: tr::lng_auctino_preview_finished(tr::now);\n\n\t\tconst auto &font = st::webPageAuctionTimeFont;\n\t\tconst auto textWidth = font->width(text);\n\t\tconst auto padding = st::webPageAuctionTimerPadding;\n\t\tconst auto timerWidth = textWidth + rect::m::sum::h(padding);\n\t\tconst auto timerHeight = font->height + rect::m::sum::v(padding);\n\t\tconst auto timerRadius = timerHeight / 2.;\n\t\tconst auto timerRect = QRectF(\n\t\t\tpadding.top(),\n\t\t\tpadding.top(),\n\t\t\ttimerWidth,\n\t\t\ttimerHeight);\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::slideFadeOutBg);\n\t\tp.drawRoundedRect(timerRect, timerRadius, timerRadius);\n\n\t\tp.setPen(backdrop.textColor);\n\t\tp.setFont(font);\n\t\tp.drawText(\n\t\t\ttimerRect.x() + padding.left(),\n\t\t\ttimerRect.y() + padding.top() + font->ascent,\n\t\t\ttext);\n\t};\n}\n\nstd::unique_ptr<MediaGenericPart> MakeGenericButtonPart(\n\t\tconst QString &text,\n\t\tQMargins margins,\n\t\tFn<void()> repaint,\n\t\tClickHandlerPtr link,\n\t\tQColor bg) {\n\treturn std::make_unique<ButtonPart>(text, margins, repaint, link, bg);\n}\n\nTextPartColored::TextPartColored(\n\tTextWithEntities text,\n\tQMargins margins,\n\tFn<QColor(const PaintContext &)> color,\n\tconst style::TextStyle &st,\n\tconst base::flat_map<uint16, ClickHandlerPtr> &links,\n\tconst Ui::Text::MarkedContext &context)\n: MediaGenericTextPart(text, margins, st, links, context)\n, _color(std::move(color)) {\n}\n\nvoid TextPartColored::setupPen(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context) const {\n\tp.setPen(_color(context));\n}\n\nAttributeTable::AttributeTable(\n\tstd::vector<Entry> entries,\n\tQMargins margins,\n\tFn<QColor(const PaintContext &)> labelColor,\n\tFn<QColor(const PaintContext &)> valueColor,\n\tconst Ui::Text::MarkedContext &context)\n: _margins(margins)\n, _labelColor(std::move(labelColor))\n, _valueColor(std::move(valueColor)) {\n\tfor (const auto &entry : entries) {\n\t\t_parts.emplace_back();\n\t\tauto &part = _parts.back();\n\t\tpart.label.setText(st::chatUniqueTextStyle, entry.label);\n\t\tpart.value.setMarkedText(\n\t\t\tst::chatUniqueTextStyle,\n\t\t\tentry.value,\n\t\t\tkMarkupTextOptions,\n\t\t\tcontext);\n\t}\n}\n\nvoid AttributeTable::draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const {\n\tconst auto labelRight = _valueLeft - st::chatUniqueTableSkip;\n\tconst auto palette = &context.st->serviceTextPalette();\n\tauto top = _margins.top();\n\tconst auto paint = [&](\n\t\t\tconst Ui::Text::String &text,\n\t\t\tint left,\n\t\t\tint availableWidth,\n\t\t\tstyle::align align) {\n\t\ttext.draw(p, {\n\t\t\t.position = { left, top },\n\t\t\t.outerWidth = outerWidth,\n\t\t\t.availableWidth = availableWidth,\n\t\t\t.align = align,\n\t\t\t.palette = palette,\n\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t.now = context.now,\n\t\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t\t.elisionLines = 1,\n\t\t});\n\t};\n\tconst auto forLabel = labelRight - _margins.left();\n\tconst auto forValue = width() - _valueLeft - _margins.right();\n\tfor (const auto &part : _parts) {\n\t\tp.setPen(_labelColor(context));\n\t\tpaint(part.label, _margins.left(), forLabel, style::al_topright);\n\t\tp.setPen(_valueColor(context));\n\t\tpaint(part.value, _valueLeft, forValue, style::al_topleft);\n\t\ttop += st::normalFont->height + st::chatUniqueRowSkip;\n\t}\n}\n\nTextState AttributeTable::textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const {\n\tauto top = _margins.top();\n\tfor (const auto &part : _parts) {\n\t\tconst auto height = st::normalFont->height + st::chatUniqueRowSkip;\n\t\tif (point.y() >= top && point.y() < top + height) {\n\t\t\tpoint -= QPoint((outerWidth - width()) / 2 + _valueLeft, top);\n\t\t\tauto result = TextState();\n\t\t\tauto forText = request.forText();\n\t\t\tforText.align = style::al_topleft;\n\t\t\tresult.link = part.value.getState(point, width(), forText).link;\n\t\t\treturn result;\n\t\t}\n\t\ttop += height;\n\t}\n\treturn {};\n}\n\nQSize AttributeTable::countOptimalSize() {\n\tauto maxLabel = 0;\n\tauto maxValue = 0;\n\tfor (const auto &part : _parts) {\n\t\tmaxLabel = std::max(maxLabel, part.label.maxWidth());\n\t\tmaxValue = std::max(maxValue, part.value.maxWidth());\n\t}\n\tconst auto skip = st::chatUniqueTableSkip;\n\tconst auto row = st::normalFont->height + st::chatUniqueRowSkip;\n\tconst auto height = int(_parts.size()) * row - st::chatUniqueRowSkip;\n\treturn {\n\t\t_margins.left() + maxLabel + skip + maxValue + _margins.right(),\n\t\t_margins.top() + height + _margins.bottom(),\n\t};\n}\n\nQSize AttributeTable::countCurrentSize(int newWidth) {\n\tconst auto skip = st::chatUniqueTableSkip;\n\tconst auto width = newWidth - _margins.left() - _margins.right() - skip;\n\tauto maxLabel = 0;\n\tauto maxValue = 0;\n\tfor (const auto &part : _parts) {\n\t\tmaxLabel = std::max(maxLabel, part.label.maxWidth());\n\t\tmaxValue = std::max(maxValue, part.value.maxWidth());\n\t}\n\tif (width <= 0 || !maxLabel) {\n\t\t_valueLeft = _margins.left();\n\t} else if (!maxValue) {\n\t\t_valueLeft = newWidth - _margins.right();\n\t} else {\n\t\t_valueLeft = _margins.left()\n\t\t\t+ int((int64(maxLabel) * width) / (maxLabel + maxValue))\n\t\t\t+ skip;\n\t}\n\treturn { newWidth, minHeight() };\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_unique_gift.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media_generic.h\"\n\nclass Painter;\n\nnamespace Data {\nclass MediaGiftBox;\nstruct UniqueGift;\nstruct UniqueGiftBackdrop;\nstruct StarGift;\nclass Birthday;\n} // namespace Data\n\nnamespace Ui {\nstruct ChatPaintContext;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass Element;\nclass MediaGeneric;\nclass MediaGenericPart;\n\n[[nodiscard]] auto GenerateUniqueGiftMedia(\n\tnot_null<Element*> parent,\n\tElement *replacing,\n\tstd::shared_ptr<Data::UniqueGift> gift)\n-> Fn<void(\n\tnot_null<MediaGeneric*>,\n\tFn<void(std::unique_ptr<MediaGenericPart>)>)>;\n\n[[nodiscard]] auto UniqueGiftBg(\n\tnot_null<Element*> view,\n\tstd::shared_ptr<Data::UniqueGift> gift)\n-> Fn<void(\n\tPainter&,\n\tconst Ui::ChatPaintContext&,\n\tnot_null<const MediaGeneric*>)>;\n\n[[nodiscard]] auto GenerateUniqueGiftPreview(\n\tnot_null<Element*> parent,\n\tElement *replacing,\n\tstd::shared_ptr<Data::UniqueGift> gift)\n-> Fn<void(\n\tnot_null<MediaGeneric*>,\n\tFn<void(std::unique_ptr<MediaGenericPart>)>)>;\n\n[[nodiscard]] auto GenerateAuctionPreview(\n\tnot_null<Element*> parent,\n\tElement *replacing,\n\tstd::shared_ptr<Data::StarGift> gift,\n\tData::UniqueGiftBackdrop backdrop)\n-> Fn<void(\n\tnot_null<MediaGeneric*>,\n\tFn<void(std::unique_ptr<MediaGenericPart>)>)>;\n\n[[nodiscard]] auto AuctionBg(\n\tnot_null<Element*> view,\n\tData::UniqueGiftBackdrop backdrop,\n\tstd::shared_ptr<Data::StarGift> gift,\n\tTimeId startDate,\n\tTimeId endDate)\n-> Fn<void(\n\tPainter&,\n\tconst Ui::ChatPaintContext&,\n\tnot_null<const MediaGeneric*>)>;\n\n[[nodiscard]] std::unique_ptr<MediaGenericPart> MakeGenericButtonPart(\n\tconst QString &text,\n\tQMargins margins,\n\tFn<void()> repaint,\n\tClickHandlerPtr link,\n\tQColor bg = QColor(0, 0, 0, 0));\n\nclass TextPartColored : public MediaGenericTextPart {\npublic:\n\tTextPartColored(\n\t\tTextWithEntities text,\n\t\tQMargins margins,\n\t\tFn<QColor(const PaintContext &)> color,\n\t\tconst style::TextStyle &st = st::defaultTextStyle,\n\t\tconst base::flat_map<uint16, ClickHandlerPtr> &links = {},\n\t\tconst Ui::Text::MarkedContext &context = {});\n\nprivate:\n\tvoid setupPen(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context) const override;\n\n\tFn<QColor(const PaintContext &)> _color;\n\n};\n\nclass AttributeTable final : public MediaGenericPart {\npublic:\n\tstruct Entry {\n\t\tQString label;\n\t\tTextWithEntities value;\n\t};\n\n\tAttributeTable(\n\t\tstd::vector<Entry> entries,\n\t\tQMargins margins,\n\t\tFn<QColor(const PaintContext &)> labelColor,\n\t\tFn<QColor(const PaintContext &)> valueColor,\n\t\tconst Ui::Text::MarkedContext &context = {});\n\n\tvoid draw(\n\t\tPainter &p,\n\t\tnot_null<const MediaGeneric*> owner,\n\t\tconst PaintContext &context,\n\t\tint outerWidth) const override;\n\tTextState textState(\n\t\tQPoint point,\n\t\tStateRequest request,\n\t\tint outerWidth) const override;\n\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\nprivate:\n\tstruct Part {\n\t\tUi::Text::String label;\n\t\tUi::Text::String value;\n\t};\n\n\tstd::vector<Part> _parts;\n\tQMargins _margins;\n\tFn<QColor(const PaintContext &)> _labelColor;\n\tFn<QColor(const PaintContext &)> _valueColor;\n\tint _valueLeft = 0;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_userpic_suggestion.h\"\n\n#include \"core/click_handler_types.h\" // ClickHandlerContext\n#include \"data/data_document.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_user.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_session.h\"\n#include \"editor/photo_editor_common.h\"\n#include \"editor/photo_editor_layer_widget.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/media/history_view_sticker_player_abstract.h\"\n#include \"history/view/history_view_element.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/painter.h\"\n#include \"mainwidget.h\"\n#include \"apiwrap.h\"\n#include \"api/api_peer_photo.h\"\n#include \"settings/sections/settings_information.h\" // UpdatePhotoLocally\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kToastDuration = 5 * crl::time(1000);\n\nvoid ShowUserpicSuggestion(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst std::shared_ptr<Data::PhotoMedia> &media,\n\t\tconst FullMsgId itemId,\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> setDone) {\n\tconst auto photo = media->owner();\n\tconst auto from = peer->asUser();\n\tconst auto name = (from && !from->firstName.isEmpty())\n\t\t? from->firstName\n\t\t: peer->name();\n\tif (photo->hasVideo()) {\n\t\tconst auto done = [=](Fn<void()> close) {\n\t\t\tusing namespace Settings;\n\t\t\tconst auto session = &photo->session();\n\t\t\tauto &peerPhotos = session->api().peerPhoto();\n\t\t\tpeerPhotos.updateSelf(photo, itemId, setDone);\n\t\t\tclose();\n\t\t};\n\t\tcontroller->show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_profile_accept_video_sure(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tname),\n\t\t\t.confirmed = done,\n\t\t\t.confirmText = tr::lng_profile_set_video_button(\n\t\t\t\ttr::now),\n\t\t}));\n\t} else {\n\t\tconst auto original = std::make_shared<QImage>(\n\t\t\tmedia->image(Data::PhotoSize::Large)->original());\n\t\tconst auto callback = [=](QImage &&image) {\n\t\t\tusing namespace Settings;\n\t\t\tconst auto session = &photo->session();\n\t\t\tconst auto user = session->user();\n\t\t\tUpdatePhotoLocally(user, image);\n\t\t\tauto &peerPhotos = session->api().peerPhoto();\n\t\t\tif (original->size() == image.size()\n\t\t\t\t&& original->constBits() == image.constBits()) {\n\t\t\t\tpeerPhotos.updateSelf(photo, itemId, setDone);\n\t\t\t} else {\n\t\t\t\tpeerPhotos.upload(user, { std::move(image) }, setDone);\n\t\t\t}\n\t\t};\n\t\tusing namespace Editor;\n\t\tPrepareProfilePhoto(\n\t\t\tcontroller->content(),\n\t\t\t&controller->window(),\n\t\t\t{\n\t\t\t\t.about = { tr::lng_profile_accept_photo_sure(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\tname) },\n\t\t\t\t.confirm = tr::lng_profile_set_photo_button(tr::now),\n\t\t\t\t.cropType = EditorData::CropType::Ellipse,\n\t\t\t\t.keepAspectRatio = true,\n\t\t\t},\n\t\t\tcallback,\n\t\t\tbase::duplicate(*original));\n\t}\n}\n\n[[nodiscard]] QImage GrabUserpicFrame(base::weak_ptr<Photo> photo) {\n\tconst auto strong = photo.get();\n\tif (!strong || !strong->width() || !strong->height()) {\n\t\treturn {};\n\t}\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto frame = QImage(\n\t\tQSize(strong->width(), strong->height()) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tframe.fill(Qt::transparent);\n\tframe.setDevicePixelRatio(ratio);\n\tauto p = Painter(&frame);\n\tstrong->paintUserpicFrame(p, QPoint(0, 0), false);\n\tp.end();\n\treturn frame;\n}\n\nvoid ShowSetToast(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QImage &frame) {\n\tconst auto text = tr::bold(\n\t\ttr::lng_profile_changed_photo_title(tr::now)\n\t).append('\\n').append(\n\t\ttr::lng_profile_changed_photo_about(\n\t\t\ttr::now,\n\t\t\tlt_link,\n\t\t\ttr::link(\n\t\t\t\ttr::lng_profile_changed_photo_link(tr::now),\n\t\t\t\tu\"tg://settings/edit_profile\"_q),\n\t\t\ttr::marked)\n\t);\n\tauto st = std::make_shared<style::Toast>(st::historyPremiumToast);\n\tconst auto skip = st->padding.top();\n\tconst auto size = st->style.font->height * 2;\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto copy = frame.scaled(\n\t\tQSize(size, size) * ratio,\n\t\tQt::IgnoreAspectRatio,\n\t\tQt::SmoothTransformation);\n\tcopy.setDevicePixelRatio(ratio);\n\tst->padding.setLeft(skip + size + skip);\n\tst->palette.linkFg = st->palette.selectLinkFg = st::mediaviewTextLinkFg;\n\n\tconst auto weak = controller->showToast({\n\t\t.text = text,\n\t\t.st = st.get(),\n\t\t.attach = RectPart::Bottom,\n\t\t.duration = kToastDuration,\n\t});\n\tif (const auto strong = weak.get()) {\n\t\tconst auto widget = strong->widget();\n\t\twidget->lifetime().add([st = std::move(st)] {});\n\n\t\tconst auto preview = Ui::CreateChild<Ui::RpWidget>(widget.get());\n\t\tpreview->moveToLeft(skip, skip);\n\t\tpreview->resize(size, size);\n\t\tpreview->show();\n\t\tpreview->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tpreview->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tQPainter(preview).drawImage(0, 0, copy);\n\t\t}, preview->lifetime());\n\t}\n}\n\n[[nodiscard]] Fn<void()> ShowSetToastCallback(\n\t\tbase::weak_ptr<Window::SessionController> weak,\n\t\tQImage frame) {\n\treturn [weak = std::move(weak), frame = std::move(frame)] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tShowSetToast(strong, frame);\n\t\t}\n\t};\n}\n\n} // namespace\n\nUserpicSuggestion::UserpicSuggestion(\n\tnot_null<Element*> parent,\n\tnot_null<PeerData*> chat,\n\tnot_null<PhotoData*> photo,\n\tint width)\n: _photo(parent, chat, photo, width) {\n\t_photo.initDimensions();\n\t_photo.resizeGetHeight(_photo.maxWidth());\n}\n\nUserpicSuggestion::~UserpicSuggestion() = default;\n\nint UserpicSuggestion::top() {\n\treturn st::msgServiceGiftBoxButtonMargins.top();\n}\n\nQSize UserpicSuggestion::size() {\n\treturn { _photo.maxWidth(), _photo.minHeight() };\n}\n\nTextWithEntities UserpicSuggestion::title() {\n\treturn {};\n}\n\nrpl::producer<QString> UserpicSuggestion::button() {\n\treturn _photo.getPhoto()->hasVideo()\n\t\t? (_photo.parent()->data()->out()\n\t\t\t? tr::lng_action_suggested_video_button()\n\t\t\t: tr::lng_profile_set_video_button())\n\t\t: tr::lng_action_suggested_photo_button();\n}\n\nTextWithEntities UserpicSuggestion::subtitle() {\n\treturn _photo.parent()->data()->notificationText();\n}\n\nClickHandlerPtr UserpicSuggestion::createViewLink() {\n\tconst auto out = _photo.parent()->data()->out();\n\tconst auto photo = _photo.getPhoto();\n\tconst auto itemId = _photo.parent()->data()->fullId();\n\tconst auto peer = _photo.parent()->data()->history()->peer;\n\tconst auto weak = base::make_weak(&_photo);\n\tconst auto show = crl::guard(weak, [=](FullMsgId id) {\n\t\t_photo.showPhoto(id);\n\t});\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tauto frame = GrabUserpicFrame(weak);\n\t\tif (frame.isNull()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tconst auto media = photo->activeMediaView();\n\t\t\tif (media->loaded()) {\n\t\t\t\tif (out) {\n\t\t\t\t\tPhotoOpenClickHandler(photo, show, itemId).onClick(\n\t\t\t\t\t\tcontext);\n\t\t\t\t} else {\n\t\t\t\t\tShowUserpicSuggestion(\n\t\t\t\t\t\tcontroller,\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\titemId,\n\t\t\t\t\t\tpeer,\n\t\t\t\t\t\tShowSetToastCallback(controller, std::move(frame)));\n\t\t\t\t}\n\t\t\t} else if (!photo->loading()) {\n\t\t\t\tPhotoSaveClickHandler(photo, itemId).onClick(context);\n\t\t\t}\n\t\t}\n\t});\n}\n\nvoid UserpicSuggestion::draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry) {\n\tp.translate(geometry.topLeft());\n\t_photo.draw(p, context);\n\tp.translate(-geometry.topLeft());\n}\n\nvoid UserpicSuggestion::stickerClearLoopPlayed() {\n}\n\nstd::unique_ptr<StickerPlayer> UserpicSuggestion::stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) {\n\treturn nullptr;\n}\n\nbool UserpicSuggestion::hasHeavyPart() {\n\treturn _photo.hasHeavyPart();\n}\n\nvoid UserpicSuggestion::unloadHeavyPart() {\n\t_photo.unloadHeavyPart();\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n#include \"history/view/media/history_view_media_unwrapped.h\"\n#include \"history/view/media/history_view_photo.h\"\n#include \"history/view/media/history_view_service_box.h\"\n\nnamespace Data {\nclass MediaGiftBox;\n} // namespace Data\n\nnamespace HistoryView {\n\nclass UserpicSuggestion final : public ServiceBoxContent {\npublic:\n\tUserpicSuggestion(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<PeerData*> chat,\n\t\tnot_null<PhotoData*> photo,\n\t\tint width);\n\t~UserpicSuggestion();\n\n\tint top() override;\n\tQSize size() override;\n\tTextWithEntities title() override;\n\tTextWithEntities subtitle() override;\n\trpl::producer<QString> button() override;\n\tvoid draw(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tconst QRect &geometry) override;\n\tClickHandlerPtr createViewLink() override;\n\n\tbool hideServiceText() override {\n\t\treturn true;\n\t}\n\n\tvoid stickerClearLoopPlayed() override;\n\tstd::unique_ptr<StickerPlayer> stickerTakePlayer(\n\t\tnot_null<DocumentData*> data,\n\t\tconst Lottie::ColorReplacements *replacements) override;\n\n\tbool hasHeavyPart() override;\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\tPhoto _photo;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_web_page.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/history_view_web_page.h\"\n\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"countries/countries_instance.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"window/window_session_controller.h\"\n#include \"iv/iv_instance.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\"\n#include \"data/components/sponsored_messages.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_web_page.h\"\n#include \"history/view/media/history_view_media_common.h\"\n#include \"history/view/media/history_view_media_generic.h\"\n#include \"history/view/media/history_view_unique_gift.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_message.h\"\n#include \"history/view/history_view_reply.h\"\n#include \"history/view/history_view_sponsored_click_handler.h\"\n#include \"history/history.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_sponsored.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView {\nnamespace {\n\nconstexpr auto kMaxOriginalEntryLines = 8192;\nconstexpr auto kFactcheckCollapsedLines = 3;\nconstexpr auto kStickerSetLines = 3;\nconstexpr auto kFactcheckAboutDuration = 5 * crl::time(1000);\nconstexpr auto kSponsoredUserpicLines = 2;\n\n[[nodiscard]] int ArticleThumbWidth(not_null<PhotoData*> thumb, int height) {\n\tconst auto size = thumb->location(Data::PhotoSize::Thumbnail);\n\treturn size.height()\n\t\t? std::max(std::min(height * size.width() / size.height(), height), 1)\n\t\t: 1;\n}\n\n[[nodiscard]] int ArticleThumbHeight(\n\t\tnot_null<Data::PhotoMedia*> thumb,\n\t\tint width) {\n\tconst auto size = thumb->size(Data::PhotoSize::Thumbnail);\n\treturn size.width()\n\t\t? std::max(size.height() * width / size.width(), 1)\n\t\t: 1;\n}\n\n[[nodiscard]] std::vector<std::unique_ptr<Data::Media>> PrepareCollageMedia(\n\t\tnot_null<HistoryItem*> parent,\n\t\tconst WebPageCollage &data) {\n\tauto result = std::vector<std::unique_ptr<Data::Media>>();\n\tresult.reserve(data.items.size());\n\tconst auto spoiler = false;\n\tfor (const auto &item : data.items) {\n\t\tif (const auto document = std::get_if<DocumentData*>(&item)) {\n\t\t\tusing MediaFile = Data::MediaFile;\n\t\t\tusing Args = MediaFile::Args;\n\t\t\tconst auto data = *document;\n\t\t\tresult.push_back(\n\t\t\t\tstd::make_unique<Data::MediaFile>(parent, data, Args{}));\n\t\t} else if (const auto photo = std::get_if<PhotoData*>(&item)) {\n\t\t\tresult.push_back(std::make_unique<Data::MediaPhoto>(\n\t\t\t\tparent,\n\t\t\t\t*photo,\n\t\t\t\tspoiler));\n\t\t} else {\n\t\t\treturn {};\n\t\t}\n\t\tif (!result.back()->canBeGrouped()) {\n\t\t\treturn {};\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] QString ExtractHash(\n\t\tnot_null<WebPageData*> webpage,\n\t\tconst TextWithEntities &text) {\n\tconst auto simplify = [](const QString &url) {\n\t\tauto result = url.split('#')[0].toLower();\n\t\tif (result.endsWith('/')) {\n\t\t\tresult.chop(1);\n\t\t}\n\t\tconst auto prefixes = { u\"http://\"_q, u\"https://\"_q };\n\t\tfor (const auto &prefix : prefixes) {\n\t\t\tif (result.startsWith(prefix)) {\n\t\t\t\tresult = result.mid(prefix.size());\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\tconst auto simplified = simplify(webpage->url);\n\tfor (const auto &entity : text.entities) {\n\t\tconst auto link = (entity.type() == EntityType::Url)\n\t\t\t? text.text.mid(entity.offset(), entity.length())\n\t\t\t: (entity.type() == EntityType::CustomUrl)\n\t\t\t? entity.data()\n\t\t\t: QString();\n\t\tif (simplify(link) == simplified) {\n\t\t\tconst auto i = link.indexOf('#');\n\t\t\treturn (i > 0) ? link.mid(i + 1) : QString();\n\t\t}\n\t}\n\treturn QString();\n}\n\n[[nodiscard]] ClickHandlerPtr IvClickHandler(\n\t\tnot_null<WebPageData*> webpage,\n\t\tconst TextWithEntities &text) {\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tif (const auto controller = my.sessionWindow.get()) {\n\t\t\tif (const auto iv = webpage->iv.get()) {\n\t\t\t\tconst auto hash = ExtractHash(webpage, text);\n\t\t\t\tCore::App().iv().show(controller, iv, hash);\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tHiddenUrlClickHandler::Open(webpage->url, context.other);\n\t\t\t}\n\t\t}\n\t});\n}\n\n[[nodiscard]] QString LookupFactcheckCountryIso2(\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto info = item->Get<HistoryMessageFactcheck>();\n\treturn info ? info->data.country : QString();\n}\n\n[[nodiscard]] QString LookupFactcheckCountryName(const QString &iso2) {\n\tconst auto name = Countries::Instance().countryNameByISO2(iso2);\n\treturn name.isEmpty() ? iso2 : name;\n}\n\n[[nodiscard]] ClickHandlerPtr AboutFactcheckClickHandler(QString iso2) {\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tconst auto my = context.other.value<ClickHandlerContext>();\n\t\tconst auto controller = my.sessionWindow.get();\n\t\tconst auto show = my.show\n\t\t\t? my.show\n\t\t\t: controller\n\t\t\t? controller->uiShow()\n\t\t\t: nullptr;\n\t\tif (show) {\n\t\t\tconst auto country = LookupFactcheckCountryName(iso2);\n\t\t\tshow->showToast({\n\t\t\t\t.text = {\n\t\t\t\t\ttr::lng_factcheck_about(tr::now, lt_country, country)\n\t\t\t\t},\n\t\t\t\t.duration = kFactcheckAboutDuration,\n\t\t\t});\n\t\t}\n\t});\n}\n\n[[nodiscard]] ClickHandlerPtr ToggleFactcheckClickHandler(\n\t\tnot_null<Element*> view) {\n\tconst auto weak = base::make_weak(view);\n\treturn std::make_shared<LambdaClickHandler>([=](ClickContext context) {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tif (const auto factcheck = strong->Get<Factcheck>()) {\n\t\t\t\tfactcheck->expanded = factcheck->expanded ? 0 : 1;\n\t\t\t\tstrong->history()->owner().requestViewResize(strong);\n\t\t\t}\n\t\t}\n\t});\n}\n\n[[nodiscard]] TextWithEntities PageToPhrase(not_null<WebPageData*> page) {\n\tconst auto type = page->type;\n\tconst auto text = tr::upper(page->iv\n\t\t? tr::lng_view_button_iv(tr::now)\n\t\t: page->uniqueGift\n\t\t? tr::lng_view_button_collectible(tr::now)\n\t\t: (type == WebPageType::Theme)\n\t\t? tr::lng_view_button_theme(tr::now)\n\t\t: (type == WebPageType::Story)\n\t\t? tr::lng_view_button_story(tr::now)\n\t\t: (type == WebPageType::Message)\n\t\t? tr::lng_view_button_message(tr::now)\n\t\t: (type == WebPageType::Group)\n\t\t? tr::lng_view_button_group(tr::now)\n\t\t: (type == WebPageType::WallPaper)\n\t\t? tr::lng_view_button_background(tr::now)\n\t\t: (type == WebPageType::Channel)\n\t\t? tr::lng_view_button_channel(tr::now)\n\t\t: (type == WebPageType::GroupWithRequest\n\t\t\t|| type == WebPageType::ChannelWithRequest)\n\t\t? tr::lng_view_button_request_join(tr::now)\n\t\t: (type == WebPageType::GroupBoost\n\t\t\t|| type == WebPageType::ChannelBoost)\n\t\t? tr::lng_view_button_boost(tr::now)\n\t\t: (type == WebPageType::Giftcode)\n\t\t? tr::lng_view_button_giftcode(tr::now)\n\t\t: (type == WebPageType::VoiceChat)\n\t\t? tr::lng_view_button_voice_chat(tr::now)\n\t\t: (type == WebPageType::Livestream)\n\t\t? tr::lng_view_button_voice_chat_channel(tr::now)\n\t\t: (type == WebPageType::ConferenceCall)\n\t\t? tr::lng_view_button_call(tr::now)\n\t\t: (type == WebPageType::Bot)\n\t\t? tr::lng_view_button_bot(tr::now)\n\t\t: (type == WebPageType::User)\n\t\t? tr::lng_view_button_user(tr::now)\n\t\t: (type == WebPageType::BotApp)\n\t\t? tr::lng_view_button_bot_app(tr::now)\n\t\t: (page->stickerSet && page->stickerSet->isEmoji)\n\t\t? tr::lng_view_button_emojipack(tr::now)\n\t\t: (type == WebPageType::StickerSet)\n\t\t? tr::lng_view_button_stickerset(tr::now)\n\t\t: (type == WebPageType::StoryAlbum)\n\t\t? tr::lng_view_button_storyalbum(tr::now)\n\t\t: (type == WebPageType::GiftCollection)\n\t\t? tr::lng_view_button_collection(tr::now)\n\t\t: (type == WebPageType::NewBot)\n\t\t? tr::lng_view_button_newbot(tr::now)\n\t\t: (type == WebPageType::Auction)\n\t\t? ((page->auction\n\t\t\t&& page->auction->endDate\n\t\t\t&& page->auction->endDate <= base::unixtime::now())\n\t\t\t? tr::lng_auction_preview_view_results(tr::now)\n\t\t\t: (page->auction\n\t\t\t\t&& page->auction->auctionGift->auctionStartDate\n\t\t\t\t&& (page->auction->auctionGift->auctionStartDate\n\t\t\t\t> base::unixtime::now()))\n\t\t\t? tr::lng_auction_bar_view(tr::now)\n\t\t\t: tr::lng_auction_preview_join(tr::now))\n\t\t: QString());\n\tif (page->iv) {\n\t\treturn Ui::Text::IconEmoji(&st::historyIvIcon).append(text);\n\t}\n\treturn { text };\n}\n\n[[nodiscard]] bool HasButton(not_null<WebPageData*> webpage) {\n\tconst auto type = webpage->type;\n\treturn webpage->iv\n\t\t|| webpage->uniqueGift\n\t\t|| (type == WebPageType::Message)\n\t\t|| (type == WebPageType::Group)\n\t\t|| (type == WebPageType::GroupWithRequest)\n\t\t|| (type == WebPageType::GroupBoost)\n\t\t|| (type == WebPageType::Channel)\n\t\t|| (type == WebPageType::ChannelBoost)\n\t\t|| (type == WebPageType::ChannelWithRequest)\n\t\t|| (type == WebPageType::Giftcode)\n\t\t// || (type == WebPageType::Bot)\n\t\t|| (type == WebPageType::User)\n\t\t|| (type == WebPageType::VoiceChat)\n\t\t|| (type == WebPageType::Livestream)\n\t\t|| (type == WebPageType::ConferenceCall)\n\t\t|| (type == WebPageType::BotApp)\n\t\t|| ((type == WebPageType::Theme)\n\t\t\t&& webpage->document\n\t\t\t&& webpage->document->isTheme())\n\t\t|| ((type == WebPageType::Story)\n\t\t\t&& (webpage->photo || webpage->document))\n\t\t|| ((type == WebPageType::WallPaper)\n\t\t\t&& webpage->document\n\t\t\t&& webpage->document->isWallPaper())\n\t\t|| (type == WebPageType::StickerSet)\n\t\t|| (type == WebPageType::StoryAlbum)\n\t\t|| (type == WebPageType::GiftCollection)\n\t\t|| (type == WebPageType::Auction)\n\t\t|| (type == WebPageType::NewBot);\n}\n\n} // namespace\n\nWebPage::WebPage(\n\tnot_null<Element*> parent,\n\tnot_null<WebPageData*> data,\n\tMediaWebPageFlags flags)\n: Media(parent)\n, _st(data->type == WebPageType::Factcheck\n\t? st::factcheckPage\n\t: st::historyPagePreview)\n, _data(data)\n, _flags(flags)\n, _siteName(st::minPhotoSize - rect::m::sum::h(_st.padding))\n, _title(st::minPhotoSize - rect::m::sum::h(_st.padding))\n, _description(st::minPhotoSize - rect::m::sum::h(_st.padding)) {\n\thistory()->owner().registerWebPageView(_data, _parent);\n}\n\nvoid WebPage::setupAdditionalData() {\n\tif (_flags & MediaWebPageFlag::Sponsored) {\n\t\t_additionalData = std::make_unique<AdditionalData>(SponsoredData());\n\t\tconst auto raw = sponsoredData();\n\t\tconst auto session = &_data->session();\n\t\tconst auto id = _parent->data()->fullId();\n\t\tconst auto details = session->sponsoredMessages().lookupDetails(id);\n\t\tconst auto link = details.link;\n\t\traw->buttonText = details.buttonText;\n\t\traw->isLinkInternal = details.isLinkInternal ? 1 : 0;\n\t\traw->backgroundEmojiId = details.backgroundEmojiId;\n\t\traw->colorIndex = details.colorIndex;\n\t\traw->canReport = details.canReport ? 1 : 0;\n\t\traw->hasMedia = (details.mediaPhotoId || details.mediaDocumentId)\n\t\t\t? 1\n\t\t\t: 0;\n\t\traw->link = std::make_shared<LambdaClickHandler>([=] {\n\t\t\tsession->sponsoredMessages().clicked(id, false, false);\n\t\t\tUrlClickHandler::Open(link);\n\t\t});\n\t\tif (!_attach) {\n\t\t\tconst auto maybeDocument = details.mediaDocumentId\n\t\t\t\t? session->data().document(\n\t\t\t\t\tdetails.mediaDocumentId).get()\n\t\t\t\t: nullptr;\n\t\t\tconst auto maybePhoto = (!maybeDocument && details.mediaPhotoId)\n\t\t\t\t? session->data().photo(details.mediaPhotoId).get()\n\t\t\t\t: nullptr;\n\t\t\t_attach = CreateAttach(\n\t\t\t\t_parent,\n\t\t\t\tmaybeDocument,\n\t\t\t\tmaybePhoto,\n\t\t\t\t_collage,\n\t\t\t\t_data->url);\n\t\t}\n\t\tif (_attach) {\n\t\t\tif (_attach->getPhoto()) {\n\t\t\t\traw->mediaLink = std::make_shared<LambdaClickHandler>([=] {\n\t\t\t\t\tsession->sponsoredMessages().clicked(id, true, false);\n\t\t\t\t\tUrlClickHandler::Open(link);\n\t\t\t\t});\n\t\t\t} else if (const auto document = _attach->getDocument()) {\n\t\t\t\tconst auto delegate = _parent->delegate();\n\t\t\t\traw->mediaLink = document->isVideoFile()\n\t\t\t\t\t? std::make_shared<LambdaClickHandler>([=] {\n\t\t\t\t\t\tsession->sponsoredMessages().clicked(id, true, false);\n\t\t\t\t\t\tdelegate->elementOpenDocument(document, id, true);\n\t\t\t\t\t})\n\t\t\t\t\t: std::make_shared<LambdaClickHandler>([=] {\n\t\t\t\t\t\tsession->sponsoredMessages().clicked(id, true, false);\n\t\t\t\t\t\tUrlClickHandler::Open(link);\n\t\t\t\t\t});\n\t\t\t}\n\t\t}\n\t} else if (_data->stickerSet) {\n\t\t_additionalData = std::make_unique<AdditionalData>(StickerSetData());\n\t\tconst auto raw = stickerSetData();\n\t\tfor (const auto &sticker : _data->stickerSet->items) {\n\t\t\tif (!sticker->sticker()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\traw->views.push_back(\n\t\t\t\tstd::make_unique<Sticker>(_parent, sticker, true));\n\t\t}\n\t\tconst auto side = std::ceil(std::sqrt(raw->views.size()));\n\t\tconst auto box = UnitedLineHeight() * kStickerSetLines;\n\t\tconst auto single = box / side;\n\t\tfor (const auto &view : raw->views) {\n\t\t\tview->setWebpagePart();\n\t\t\tview->initSize(single);\n\t\t}\n\t} else if (_data->type == WebPageType::Factcheck) {\n\t\t_additionalData = std::make_unique<AdditionalData>(FactcheckData());\n\t}\n}\n\nQSize WebPage::countOptimalSize() {\n\tif (_data->pendingTill || _data->failed) {\n\t\treturn { 0, 0 };\n\t}\n\tsetupAdditionalData();\n\n\tconst auto sponsored = sponsoredData();\n\tconst auto factcheck = factcheckData();\n\tconst auto stickerSet = stickerSetData();\n\tconst auto specialRightPix = (stickerSet\n\t\t|| (sponsored && !sponsored->hasMedia && _data->photo));\n\n\t// Detect _openButtonWidth before counting paddings.\n\t_openButton = Ui::Text::String();\n\tif (HasButton(_data)) {\n\t\tconst auto context = Core::TextContext({\n\t\t\t.session = &_data->session(),\n\t\t\t.customEmojiLoopLimit = 1,\n\t\t});\n\t\t_openButton.setMarkedText(\n\t\t\tst::semiboldTextStyle,\n\t\t\tPageToPhrase(_data),\n\t\t\tkMarkupTextOptions,\n\t\t\tcontext);\n\t} else if (sponsored && !sponsored->buttonText.isEmpty()) {\n\t\t_openButton.setText(\n\t\t\tst::semiboldTextStyle,\n\t\t\ttr::upper(sponsored->buttonText));\n\t}\n\n\tconst auto padding = inBubblePadding() + innerMargin();\n\tconst auto versionChanged = (_dataVersion != _data->version);\n\tif (versionChanged) {\n\t\t_dataVersion = _data->version;\n\t\t_openl = nullptr;\n\t\t_attach = nullptr;\n\t\tconst auto item = _parent->data();\n\t\t_collage = PrepareCollageMedia(item, _data->collage);\n\t\tconst auto min = st::minPhotoSize - rect::m::sum::h(_st.padding);\n\t\t_siteName = Ui::Text::String(min);\n\t\t_title = Ui::Text::String(min);\n\t\t_description = Ui::Text::String(min);\n\t\tif (factcheck) {\n\t\t\tfactcheck->footer = Ui::Text::String(\n\t\t\t\tst::factcheckFooterStyle,\n\t\t\t\ttr::lng_factcheck_bottom(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_country,\n\t\t\t\t\tLookupFactcheckCountryName(\n\t\t\t\t\t\tLookupFactcheckCountryIso2(item))),\n\t\t\t\tkDefaultTextOptions,\n\t\t\t\tmin);\n\t\t}\n\t}\n\tconst auto lineHeight = UnitedLineHeight();\n\n\tif (!_openl && (!_data->url.isEmpty() || sponsored || factcheck)) {\n\t\tconst auto original = _parent->data()->originalText();\n\t\tconst auto previewOfHiddenUrl = [&] {\n\t\t\tif (_data->type == WebPageType::BotApp) {\n\t\t\t\t// Bot Web Apps always show confirmation on hidden urls.\n\t\t\t\t//\n\t\t\t\t// But from the dedicated \"Open App\" button we don't want\n\t\t\t\t// to request users confirmation on non-first app opening.\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto simplify = [](const QString &url) {\n\t\t\t\tauto result = url.toLower();\n\t\t\t\tif (result.endsWith('/')) {\n\t\t\t\t\tresult.chop(1);\n\t\t\t\t}\n\t\t\t\tconst auto prefixes = { u\"http://\"_q, u\"https://\"_q };\n\t\t\t\tfor (const auto &prefix : prefixes) {\n\t\t\t\t\tif (result.startsWith(prefix)) {\n\t\t\t\t\t\tresult = result.mid(prefix.size());\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t};\n\t\t\tconst auto simplified = simplify(_data->url);\n\t\t\tfor (const auto &entity : original.entities) {\n\t\t\t\tif (entity.type() != EntityType::Url) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto link = original.text.mid(\n\t\t\t\t\tentity.offset(),\n\t\t\t\t\tentity.length());\n\t\t\t\tif (simplify(link) == simplified) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}();\n\t\tif (sponsored) {\n\t\t\t_openl = SponsoredLink(_data->url, sponsored->isLinkInternal);\n\t\t\tif (sponsored->canReport) {\n\t\t\t\tsponsored->hint.link = AboutSponsoredClickHandler();\n\t\t\t}\n\t\t} else if (factcheck) {\n\t\t\tconst auto item = _parent->data();\n\t\t\tconst auto iso2 = LookupFactcheckCountryIso2(item);\n\t\t\tif (!iso2.isEmpty()) {\n\t\t\t\tfactcheck->hint.link = AboutFactcheckClickHandler(iso2);\n\t\t\t}\n\t\t} else {\n\t\t\t_openl = _data->iv\n\t\t\t\t? IvClickHandler(_data, original)\n\t\t\t\t: (previewOfHiddenUrl || UrlClickHandler::IsSuspicious(\n\t\t\t\t\t_data->url))\n\t\t\t\t? std::make_shared<HiddenUrlClickHandler>(_data->url)\n\t\t\t\t: std::make_shared<UrlClickHandler>(_data->url, true);\n\t\t\tif (_data->document\n\t\t\t\t&& (_data->document->isWallPaper()\n\t\t\t\t\t|| _data->document->isTheme())) {\n\t\t\t\t_openl = std::make_shared<DocumentWrappedClickHandler>(\n\t\t\t\t\tstd::move(_openl),\n\t\t\t\t\t_data->document,\n\t\t\t\t\t_parent->data()->fullId());\n\t\t\t}\n\t\t}\n\t}\n\n\t// init layout\n\tconst auto title = TextUtilities::SingleLine(_data->title.isEmpty()\n\t\t? _data->author\n\t\t: _data->title);\n\tusing Flag = MediaWebPageFlag;\n\tif (_data->hasLargeMedia && (_flags & Flag::ForceLargeMedia)) {\n\t\t_asArticle = 0;\n\t} else if (_data->hasLargeMedia && (_flags & Flag::ForceSmallMedia)) {\n\t\t_asArticle = 1;\n\t} else {\n\t\t_asArticle = _data->computeDefaultSmallMedia();\n\t}\n\tif (sponsored && sponsored->hasMedia) {\n\t\t_asArticle = 0;\n\t}\n\n\t// init attach\n\tif (!_attach && _data->uniqueGift) {\n\t\t_attach = std::make_unique<MediaGeneric>(\n\t\t\t_parent,\n\t\t\tGenerateUniqueGiftPreview(\n\t\t\t\t_parent,\n\t\t\t\tnullptr,\n\t\t\t\t_data->uniqueGift),\n\t\t\t\tMediaGenericDescriptor{\n\t\t\t\t\t.maxWidth = st::msgServiceGiftPreview,\n\t\t\t\t\t.paintBgFactory = [=] {\n\t\t\t\t\t\treturn UniqueGiftBg(_parent, _data->uniqueGift);\n\t\t\t\t\t},\n\t\t\t\t\t.expandCurrentWidth = true,\n\t\t\t\t});\n\t} else if (!_attach && _data->auction) {\n\t\tconst auto &gift = _data->auction->auctionGift;\n\t\tconst auto backdrop = gift->background\n\t\t\t? gift->background->backdrop()\n\t\t\t: Data::UniqueGiftBackdrop();\n\t\t_attach = std::make_unique<MediaGeneric>(\n\t\t\t_parent,\n\t\t\tGenerateAuctionPreview(\n\t\t\t\t_parent,\n\t\t\t\tnullptr,\n\t\t\t\tgift,\n\t\t\t\tbackdrop),\n\t\t\tMediaGenericDescriptor{\n\t\t\t\t.maxWidth = st::msgServiceGiftPreview,\n\t\t\t\t.paintBgFactory = [=] {\n\t\t\t\t\treturn AuctionBg(\n\t\t\t\t\t\t_parent,\n\t\t\t\t\t\tbackdrop,\n\t\t\t\t\t\tgift,\n\t\t\t\t\t\t_data->auction->auctionGift->auctionStartDate,\n\t\t\t\t\t\t_data->auction->endDate);\n\t\t\t\t},\n\t\t\t\t.expandCurrentWidth = true,\n\t\t\t});\n\t} else if (!_attach && !_asArticle) {\n\t\t_attach = CreateAttach(\n\t\t\t_parent,\n\t\t\t_data->document,\n\t\t\t((!_data->document || _data->photoIsVideoCover)\n\t\t\t\t? _data->photo\n\t\t\t\t: nullptr),\n\t\t\t_collage,\n\t\t\t_data->url);\n\t}\n\n\t// init strings\n\tif (_description.isEmpty()\n\t\t&& !_data->description.text.isEmpty()\n\t\t&& !_data->uniqueGift\n\t\t&& !_data->auction) {\n\t\tconst auto &text = _data->description;\n\t\tusing Type = Core::TextContextDetails::HashtagMentionType;\n\t\tauto context = Core::TextContext({\n\t\t\t.session = &history()->session(),\n\t\t\t.details = {\n\t\t\t\t.type = ((_data->siteName == u\"Twitter\"_q)\n\t\t\t\t\t? Type::Twitter\n\t\t\t\t\t: (_data->siteName == u\"Instagram\"_q)\n\t\t\t\t\t? Type::Instagram\n\t\t\t\t\t: Type::Telegram),\n\t\t\t},\n\t\t\t.repaint = [=] { _parent->customEmojiRepaint(); },\n\t\t});\n\t\t_description.setMarkedText(\n\t\t\tst::webPageDescriptionStyle,\n\t\t\ttext,\n\t\t\tUi::WebpageTextDescriptionOptions(),\n\t\t\tcontext);\n\t}\n\tconst auto siteName = _data->displayedSiteName();\n\tif (!siteName.isEmpty()) {\n\t\t_siteNameLines = 1;\n\t\t_siteName.setMarkedText(\n\t\t\tst::webPageTitleStyle,\n\t\t\ttr::link(siteName, _data->url),\n\t\t\tUi::WebpageTextTitleOptions());\n\t}\n\tif (_title.isEmpty() && !title.isEmpty()) {\n\t\tif (!_siteNameLines && !_data->url.isEmpty()) {\n\t\t\t_title.setMarkedText(\n\t\t\t\tst::webPageTitleStyle,\n\t\t\t\ttr::link(title, _data->url),\n\t\t\t\tUi::WebpageTextTitleOptions());\n\n\t\t} else {\n\t\t\t_title.setText(\n\t\t\t\tst::webPageTitleStyle,\n\t\t\t\ttitle,\n\t\t\t\tUi::WebpageTextTitleOptions());\n\t\t}\n\t}\n\n\t// init dimensions\n\tconst auto skipBlockWidth = (sponsored && sponsored->hasMedia)\n\t\t? 0\n\t\t: _parent->skipBlockWidth();\n\tauto maxWidth = skipBlockWidth;\n\tauto minHeight = 0;\n\n\tconst auto siteNameHeight = _siteName.isEmpty() ? 0 : lineHeight;\n\tconst auto titleMinHeight = _title.isEmpty() ? 0 : lineHeight;\n\tconst auto factcheckMetrics = factcheck\n\t\t? computeFactcheckMetrics(_description.minHeight())\n\t\t: FactcheckMetrics();\n\tconst auto descMaxLines = factcheck\n\t\t? factcheckMetrics.lines\n\t\t: isLogEntryOriginal()\n\t\t? kMaxOriginalEntryLines\n\t\t: (3 + (siteNameHeight ? 0 : 1) + (titleMinHeight ? 0 : 1));\n\tconst auto descriptionMinHeight = _description.isEmpty()\n\t\t? 0\n\t\t: std::min(_description.minHeight(), descMaxLines * lineHeight);\n\tconst auto articleMinHeight = siteNameHeight\n\t\t+ titleMinHeight\n\t\t+ descriptionMinHeight;\n\tconst auto articlePhotoMaxWidth = _asArticle\n\t\t? (st::webPagePhotoDelta\n\t\t\t+ std::max(\n\t\t\t\tArticleThumbWidth(_data->photo, articleMinHeight),\n\t\t\t\tlineHeight))\n\t\t: specialRightPix\n\t\t? (st::webPagePhotoDelta\n\t\t\t+ (lineHeight\n\t\t\t\t* (stickerSet\n\t\t\t\t\t? kStickerSetLines\n\t\t\t\t\t: kSponsoredUserpicLines)))\n\t\t: 0;\n\n\tif (!_siteName.isEmpty()) {\n\t\taccumulate_max(maxWidth, _siteName.maxWidth() + articlePhotoMaxWidth);\n\t\tminHeight += lineHeight;\n\t}\n\tif (!_title.isEmpty()) {\n\t\taccumulate_max(maxWidth, _title.maxWidth() + articlePhotoMaxWidth);\n\t\tminHeight += titleMinHeight;\n\t}\n\tif (!_description.isEmpty()) {\n\t\taccumulate_max(\n\t\t\tmaxWidth,\n\t\t\t_description.maxWidth() + articlePhotoMaxWidth);\n\t\tminHeight += descriptionMinHeight;\n\t}\n\tif (factcheck && factcheck->expanded) {\n\t\taccumulate_max(maxWidth, factcheck->footer.maxWidth());\n\t\tminHeight += st::factcheckFooterSkip + factcheck->footer.minHeight();\n\t}\n\tif (_attach) {\n\t\tconst auto attachAtTop = (_siteName.isEmpty()\n\t\t\t\t&& _title.isEmpty()\n\t\t\t\t&& _description.isEmpty())\n\t\t\t|| (sponsored && sponsored->hasMedia);\n\t\tif (!attachAtTop) {\n\t\t\tminHeight += st::mediaInBubbleSkip;\n\t\t}\n\n\t\t_attach->initDimensions();\n\t\tconst auto bubble = _attach->bubbleMargins();\n\t\tauto maxMediaWidth = _attach->maxWidth() - rect::m::sum::h(bubble);\n\t\tif (isBubbleBottom() && _attach->customInfoLayout()) {\n\t\t\tmaxMediaWidth += skipBlockWidth;\n\t\t}\n\t\taccumulate_max(maxWidth, maxMediaWidth);\n\t\tminHeight += _attach->minHeight() - rect::m::sum::v(bubble);\n\t}\n\tif (_data->type == WebPageType::Video && _data->duration) {\n\t\t_duration = Ui::FormatDurationText(_data->duration);\n\t\t_durationWidth = st::msgDateFont->width(_duration);\n\t}\n\tif (!_openButton.isEmpty()) {\n\t\tconst auto w = rect::m::sum::h(st::historyPageButtonPadding)\n\t\t\t+ _openButton.maxWidth();\n\t\taccumulate_max(maxWidth, w);\n\t}\n\tmaxWidth += rect::m::sum::h(padding);\n\tminHeight += rect::m::sum::v(padding);\n\n\tif (_asArticle) {\n\t\tminHeight = resizeGetHeight(maxWidth);\n\t}\n\tif (const auto hint = hintData()) {\n\t\thint->widthBefore = st::webPageTitleStyle.font->width(siteName);\n\t\tconst auto &font = st::webPageSponsoredHintFont;\n\t\thint->text = sponsored\n\t\t\t? tr::lng_sponsored_message_revenue_button(tr::now)\n\t\t\t: tr::lng_factcheck_whats_this(tr::now);\n\t\thint->size = QSize(\n\t\t\tfont->width(hint->text) + font->height,\n\t\t\tfont->height);\n\t\tmaxWidth += hint->size.width();\n\t}\n\treturn { maxWidth, minHeight };\n}\n\nQSize WebPage::countCurrentSize(int newWidth) {\n\tif (_data->pendingTill || _data->failed) {\n\t\treturn { newWidth, minHeight() };\n\t}\n\n\tconst auto padding = inBubblePadding() + innerMargin();\n\tconst auto innerWidth = newWidth - rect::m::sum::h(padding);\n\tauto newHeight = 0;\n\n\tconst auto stickerSet = stickerSetData();\n\tconst auto factcheck = factcheckData();\n\tconst auto sponsored = sponsoredData();\n\tconst auto specialRightPix = (stickerSet\n\t\t|| (sponsored && !sponsored->hasMedia && _data->photo));\n\tconst auto lineHeight = UnitedLineHeight();\n\tconst auto factcheckMetrics = factcheck\n\t\t? computeFactcheckMetrics(_description.countHeight(innerWidth))\n\t\t: FactcheckMetrics();\n\tif (factcheck) {\n\t\tfactcheck->expandable = factcheckMetrics.expandable ? 1 : 0;\n\t\tfactcheck->expanded = factcheckMetrics.expanded ? 1 : 0;\n\t\t_openl = factcheck->expandable\n\t\t\t? ToggleFactcheckClickHandler(_parent)\n\t\t\t: nullptr;\n\t}\n\tconst auto linesMax = factcheck\n\t\t? (factcheckMetrics.lines + 1)\n\t\t: (sponsored || isLogEntryOriginal())\n\t\t? kMaxOriginalEntryLines\n\t\t: 5;\n\tconst auto siteNameHeight = _siteNameLines ? lineHeight : 0;\n\tconst auto twoTitleLines = 2 * st::webPageTitleFont->height;\n\tconst auto descriptionLineHeight = st::webPageDescriptionFont->height;\n\tif (asArticle() || specialRightPix) {\n\t\t_pixh = lineHeight\n\t\t\t* (stickerSet\n\t\t\t\t? kStickerSetLines\n\t\t\t\t: specialRightPix\n\t\t\t\t? kSponsoredUserpicLines\n\t\t\t\t: linesMax);\n\t\tdo {\n\t\t\t_pixw = specialRightPix\n\t\t\t\t? _pixh\n\t\t\t\t: ArticleThumbWidth(_data->photo, _pixh);\n\t\t\tconst auto wleft = innerWidth\n\t\t\t\t- st::webPagePhotoDelta\n\t\t\t\t- std::max(_pixw, lineHeight);\n\n\t\t\tnewHeight = siteNameHeight;\n\n\t\t\tif (_title.isEmpty()) {\n\t\t\t\t_titleLines = 0;\n\t\t\t} else {\n\t\t\t\t_titleLines = (_title.countHeight(wleft) < twoTitleLines)\n\t\t\t\t\t? 1\n\t\t\t\t\t: 2;\n\t\t\t\tnewHeight += _titleLines * lineHeight;\n\t\t\t}\n\n\t\t\tconst auto descriptionHeight = _description.countHeight(sponsored\n\t\t\t\t? innerWidth\n\t\t\t\t: wleft);\n\t\t\tconst auto restLines = (linesMax - _siteNameLines - _titleLines);\n\t\t\tif (descriptionHeight < restLines * descriptionLineHeight) {\n\t\t\t\t// We have height for all the lines.\n\t\t\t\t_descriptionLines = -1;\n\t\t\t\tnewHeight += descriptionHeight;\n\t\t\t} else {\n\t\t\t\t_descriptionLines = restLines;\n\t\t\t\tnewHeight += _descriptionLines * lineHeight;\n\t\t\t}\n\n\t\t\tif (newHeight >= _pixh) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t_pixh -= lineHeight;\n\t\t} while (_pixh > lineHeight);\n\t} else {\n\t\tnewHeight = siteNameHeight;\n\n\t\tif (_title.isEmpty()) {\n\t\t\t_titleLines = 0;\n\t\t} else {\n\t\t\t_titleLines = (_title.countHeight(innerWidth) < twoTitleLines)\n\t\t\t\t? 1\n\t\t\t\t: 2;\n\t\t\tnewHeight += _titleLines * lineHeight;\n\t\t}\n\n\t\tif (_description.isEmpty()) {\n\t\t\t_descriptionLines = 0;\n\t\t} else {\n\t\t\tconst auto restLines = (linesMax - _siteNameLines - _titleLines);\n\t\t\tconst auto descriptionHeight = _description.countHeight(\n\t\t\t\tinnerWidth);\n\t\t\tif (descriptionHeight < restLines * descriptionLineHeight) {\n\t\t\t\t// We have height for all the lines.\n\t\t\t\t_descriptionLines = -1;\n\t\t\t\tnewHeight += descriptionHeight;\n\t\t\t} else {\n\t\t\t\t_descriptionLines = restLines;\n\t\t\t\tnewHeight += _descriptionLines * lineHeight;\n\t\t\t}\n\t\t}\n\t\tif (factcheck && factcheck->expanded) {\n\t\t\tfactcheck->footerHeight = st::factcheckFooterSkip\n\t\t\t\t+ factcheck->footer.countHeight(innerWidth);\n\t\t\tnewHeight += factcheck->footerHeight;\n\t\t}\n\n\t\tif (_attach) {\n\t\t\tconst auto attachAtTop = (!_siteNameLines\n\t\t\t\t\t&& !_titleLines\n\t\t\t\t\t&& !_descriptionLines)\n\t\t\t\t|| (sponsored && sponsored->hasMedia);\n\t\t\tif (!attachAtTop) {\n\t\t\t\tnewHeight += st::mediaInBubbleSkip;\n\t\t\t}\n\n\t\t\tconst auto bubble = _attach->bubbleMargins();\n\t\t\t_attach->resizeGetHeight(innerWidth + rect::m::sum::h(bubble));\n\t\t\tnewHeight += _attach->height() - rect::m::sum::v(bubble);\n\t\t}\n\t}\n\tnewHeight += rect::m::sum::v(padding);\n\n\treturn { newWidth, newHeight };\n}\n\nTextSelection WebPage::toTitleSelection(TextSelection selection) const {\n\treturn UnshiftItemSelection(selection, _siteName);\n}\n\nTextSelection WebPage::fromTitleSelection(TextSelection selection) const {\n\treturn ShiftItemSelection(selection, _siteName);\n}\n\nTextSelection WebPage::toDescriptionSelection(TextSelection selection) const {\n\treturn UnshiftItemSelection(toTitleSelection(selection), _title);\n}\n\nTextSelection WebPage::fromDescriptionSelection(\n\t\tTextSelection selection) const {\n\treturn ShiftItemSelection(fromTitleSelection(selection), _title);\n}\n\nvoid WebPage::refreshParentId(not_null<HistoryItem*> realParent) {\n\tif (_attach) {\n\t\t_attach->refreshParentId(realParent);\n\t}\n}\n\nvoid WebPage::ensurePhotoMediaCreated() const {\n\tExpects(_data->photo != nullptr);\n\n\tif (_photoMedia) {\n\t\treturn;\n\t}\n\t_photoMedia = _data->photo->createMediaView();\n\tconst auto contextId = _parent->data()->fullId();\n\t_photoMedia->wanted(Data::PhotoSize::Thumbnail, contextId);\n\thistory()->owner().registerHeavyViewPart(_parent);\n}\n\nbool WebPage::hasHeavyPart() const {\n\tif (const auto stickerSet = stickerSetData()) {\n\t\tfor (const auto &part : stickerSet->views) {\n\t\t\tif (part->hasHeavyPart()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\treturn _photoMedia\n\t\t|| (_attach ? _attach->hasHeavyPart() : false);\n}\n\nvoid WebPage::unloadHeavyPart() {\n\tif (_attach) {\n\t\t_attach->unloadHeavyPart();\n\t}\n\t_description.unloadPersistentAnimation();\n\t_photoMedia = nullptr;\n\tif (const auto stickerSet = stickerSetData()) {\n\t\tfor (const auto &part : stickerSet->views) {\n\t\t\tpart->unloadHeavyPart();\n\t\t}\n\t}\n}\n\nvoid WebPage::draw(Painter &p, const PaintContext &context) const {\n\tif (width() < rect::m::sum::h(st::msgPadding) + 1) {\n\t\treturn;\n\t}\n\tconst auto st = context.st;\n\tconst auto sti = context.imageStyle();\n\tconst auto stm = context.messageStyle();\n\n\tconst auto bubble = _attach ? _attach->bubbleMargins() : QMargins();\n\tconst auto full = Rect(currentSize());\n\tconst auto outer = full - inBubblePadding();\n\tconst auto inner = outer - innerMargin();\n\tconst auto attachAdditionalInfoText = _attach\n\t\t? _attach->additionalInfoString()\n\t\t: QString();\n\tauto tshift = inner.top();\n\tauto paintw = inner.width();\n\n\tconst auto sponsored = sponsoredData();\n\tconst auto factcheck = factcheckData();\n\n\tconst auto hasSponsoredMedia = sponsored && sponsored->hasMedia;\n\tif (hasSponsoredMedia && _attach) {\n\t\ttshift += _attach->height() + st::mediaInBubbleSkip;\n\t}\n\n\tconst auto selected = context.selected();\n\tconst auto view = parent();\n\tconst auto colorIndex = factcheck\n\t\t? 0 // red\n\t\t: (sponsored && sponsored->colorIndex)\n\t\t? sponsored->colorIndex\n\t\t: view->contentColorIndex();\n\tconst auto &colorCollectible = factcheck\n\t\t? nullptr\n\t\t: (sponsored && sponsored->colorIndex)\n\t\t? nullptr\n\t\t: view->contentColorCollectible();\n\tconst auto colorPattern = colorCollectible\n\t\t? st->collectiblePatternIndex(colorCollectible)\n\t\t: st->colorPatternIndex(colorIndex);\n\tconst auto useColorCollectible = colorCollectible && !context.outbg;\n\tconst auto useColorIndex = !context.outbg;\n\tconst auto cache = useColorCollectible\n\t\t? st->collectibleReplyCache(selected, colorCollectible).get()\n\t\t: useColorIndex\n\t\t? st->coloredReplyCache(selected, colorIndex).get()\n\t\t: stm->replyCache[colorPattern].get();\n\tconst auto backgroundEmojiId = factcheck\n\t\t? DocumentId()\n\t\t: (sponsored && sponsored->backgroundEmojiId)\n\t\t? sponsored->backgroundEmojiId\n\t\t: view->contentBackgroundEmojiId();\n\tconst auto backgroundEmojiData = backgroundEmojiId\n\t\t? st->backgroundEmojiData(backgroundEmojiId, colorCollectible).get()\n\t\t: nullptr;\n\tconst auto backgroundEmojiCache = !backgroundEmojiData\n\t\t? nullptr\n\t\t: useColorCollectible\n\t\t? &backgroundEmojiData->collectibleCaches[colorCollectible]\n\t\t: &backgroundEmojiData->caches[Ui::BackgroundEmojiData::CacheIndex(\n\t\t\tselected,\n\t\t\tcontext.outbg,\n\t\t\ttrue,\n\t\t\tuseColorIndex ? (colorIndex + 1) : 0)];\n\tUi::Text::ValidateQuotePaintCache(*cache, _st);\n\tUi::Text::FillQuotePaint(p, outer, *cache, _st);\n\tif (backgroundEmojiData) {\n\t\tValidateBackgroundEmoji(\n\t\t\tbackgroundEmojiId,\n\t\t\tcolorCollectible,\n\t\t\tbackgroundEmojiData,\n\t\t\tbackgroundEmojiCache,\n\t\t\tcache,\n\t\t\tview);\n\t\tif (!backgroundEmojiCache->frames[0].isNull()) {\n\t\t\tFillBackgroundEmoji(\n\t\t\t\tp,\n\t\t\t\touter,\n\t\t\t\tfalse,\n\t\t\t\t*backgroundEmojiCache,\n\t\t\t\tbackgroundEmojiData->firstGiftFrame);\n\t\t}\n\t} else if (factcheck && factcheck->expandable) {\n\t\tconst auto &icon = factcheck->expanded ? _st.collapse : _st.expand;\n\t\tconst auto &position = factcheck->expanded\n\t\t\t? _st.collapsePosition\n\t\t\t: _st.expandPosition;\n\t\ticon.paint(\n\t\t\tp,\n\t\t\touter.x() + outer.width() - icon.width() - position.x(),\n\t\t\touter.y() + outer.height() - icon.height() - position.y(),\n\t\t\twidth());\n\t}\n\n\tif (_ripple) {\n\t\t_ripple->paint(p, outer.x(), outer.y(), width(), &cache->bg);\n\t\tif (_ripple->empty()) {\n\t\t\t_ripple = nullptr;\n\t\t}\n\t}\n\n\tauto lineHeight = UnitedLineHeight();\n\tif (const auto stickerSet = stickerSetData()) {\n\t\tconst auto viewsCount = stickerSet->views.size();\n\t\tconst auto box = _pixh;\n\t\tconst auto topLeft = QPoint(inner.left() + paintw - box, tshift);\n\t\tconst auto side = std::ceil(std::sqrt(viewsCount));\n\t\tconst auto single = box / side;\n\t\tfor (auto i = 0; i < side; i++) {\n\t\t\tfor (auto j = 0; j < side; j++) {\n\t\t\t\tconst auto index = i * side + j;\n\t\t\t\tif (viewsCount <= index) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tconst auto &view = stickerSet->views[index];\n\t\t\t\tconst auto size = view->countOptimalSize();\n\t\t\t\tconst auto offsetX = (single - size.width()) / 2.;\n\t\t\t\tconst auto offsetY = (single - size.height()) / 2.;\n\t\t\t\tconst auto x = j * single + offsetX;\n\t\t\t\tconst auto y = i * single + offsetY;\n\t\t\t\tview->draw(p, context, QRect(QPoint(x, y) + topLeft, size));\n\t\t\t}\n\t\t}\n\t\tpaintw -= box;\n\t} else if (asArticle()) {\n\t\tensurePhotoMediaCreated();\n\n\t\tauto pix = QPixmap();\n\t\tconst auto pw = qMax(_pixw, lineHeight);\n\t\tconst auto ph = _pixh;\n\t\tauto pixw = _pixw;\n\t\tauto pixh = ArticleThumbHeight(_photoMedia.get(), _pixw);\n\t\tconst auto maxsize = _photoMedia->size(Data::PhotoSize::Thumbnail);\n\t\tconst auto maxw = style::ConvertScale(maxsize.width());\n\t\tconst auto maxh = style::ConvertScale(maxsize.height());\n\t\tif (pixw * ph != pixh * pw) {\n\t\t\tconst auto coef = (pixw * ph > pixh * pw)\n\t\t\t\t? std::min(ph / float64(pixh), maxh / float64(pixh))\n\t\t\t\t: std::min(pw / float64(pixw), maxw / float64(pixw));\n\t\t\tpixh = std::round(pixh * coef);\n\t\t\tpixw = std::round(pixw * coef);\n\t\t}\n\t\tconst auto size = QSize(pixw, pixh);\n\t\tconst auto args = Images::PrepareArgs{\n\t\t\t.options = Images::Option::RoundSmall,\n\t\t\t.outer = { pw, ph },\n\t\t};\n\t\tusing namespace Data;\n\t\tif (const auto thumbnail = _photoMedia->image(PhotoSize::Thumbnail)) {\n\t\t\tpix = thumbnail->pixSingle(size, args);\n\t\t} else if (const auto small = _photoMedia->image(PhotoSize::Small)) {\n\t\t\tpix = small->pixSingle(size, args.blurred());\n\t\t} else if (const auto blurred = _photoMedia->thumbnailInline()) {\n\t\t\tpix = blurred->pixSingle(size, args.blurred());\n\t\t}\n\t\tp.drawPixmapLeft(inner.left() + paintw - pw, tshift, width(), pix);\n\t\tif (context.selected()) {\n\t\t\tconst auto st = context.st;\n\t\t\tUi::FillRoundRect(\n\t\t\t\tp,\n\t\t\t\tstyle::rtlrect(\n\t\t\t\t\tinner.left() + paintw - pw,\n\t\t\t\t\ttshift,\n\t\t\t\t\tpw,\n\t\t\t\t\t_pixh,\n\t\t\t\t\twidth()),\n\t\t\t\tst->msgSelectOverlay(),\n\t\t\t\tst->msgSelectOverlayCorners(Ui::CachedCornerRadius::Small));\n\t\t}\n\t\tif (!sponsored) {\n\t\t\t// Ignore photo width in sponsored messages,\n\t\t\t// as its width only affects the title.\n\t\t\tpaintw -= pw + st::webPagePhotoDelta;\n\t\t}\n\t}\n\tif (_siteNameLines) {\n\t\tp.setPen(cache->icon);\n\t\tp.setTextPalette(useColorCollectible\n\t\t\t? st->collectibleTextPalette(selected, colorCollectible)\n\t\t\t: useColorIndex\n\t\t\t? st->coloredTextPalette(selected, colorIndex)\n\t\t\t: stm->semiboldPalette);\n\n\t\tconst auto endskip = _siteName.hasSkipBlock()\n\t\t\t? _parent->skipBlockWidth()\n\t\t\t: 0;\n\t\t_siteName.drawLeftElided(\n\t\t\tp,\n\t\t\tinner.left(),\n\t\t\ttshift,\n\t\t\tpaintw,\n\t\t\twidth(),\n\t\t\t_siteNameLines,\n\t\t\tstyle::al_left,\n\t\t\t0,\n\t\t\t-1,\n\t\t\tendskip,\n\t\t\tfalse,\n\t\t\tcontext.selection);\n\t\tconst auto hint = hintData();\n\t\tif (hint && (paintw > hint->widthBefore + hint->size.width())) {\n\t\t\tauto color = cache->icon;\n\t\t\tcolor.setAlphaF(color.alphaF() * 0.15);\n\n\t\t\tconst auto height = st::webPageSponsoredHintFont->height;\n\t\t\tconst auto radius = height / 2;\n\n\t\t\thint->lastPosition = QPointF(\n\t\t\t\tradius + inner.left() + hint->widthBefore,\n\t\t\t\ttshift + (_siteName.style()->font->height - height) / 2.);\n\n\t\t\tif (hint->ripple) {\n\t\t\t\thint->ripple->paint(\n\t\t\t\t\tp,\n\t\t\t\t\thint->lastPosition.x(),\n\t\t\t\t\thint->lastPosition.y(),\n\t\t\t\t\twidth(),\n\t\t\t\t\t&cache->bg);\n\t\t\t\tif (hint->ripple->empty()) {\n\t\t\t\t\thint->ripple = nullptr;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst auto rect = QRectF(hint->lastPosition, hint->size);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(color);\n\t\t\tp.drawRoundedRect(rect, radius, radius);\n\n\t\t\tp.setPen(cache->icon);\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\tp.setFont(st::webPageSponsoredHintFont);\n\t\t\tp.drawText(rect, hint->text, style::al_center);\n\t\t}\n\t\ttshift += lineHeight;\n\n\t\tp.setTextPalette(stm->textPalette);\n\t}\n\tp.setPen(stm->historyTextFg);\n\tif (_titleLines) {\n\t\tconst auto endskip = _title.hasSkipBlock()\n\t\t\t? _parent->skipBlockWidth()\n\t\t\t: 0;\n\t\tconst auto titleWidth = sponsored\n\t\t\t? (paintw - (_pixh ? (_pixh + st::webPagePhotoDelta) : 0))\n\t\t\t: paintw;\n\t\t_title.drawLeftElided(\n\t\t\tp,\n\t\t\tinner.left(),\n\t\t\ttshift,\n\t\t\ttitleWidth,\n\t\t\twidth(),\n\t\t\t_titleLines,\n\t\t\tstyle::al_left,\n\t\t\t0,\n\t\t\t-1,\n\t\t\tendskip,\n\t\t\tfalse,\n\t\t\ttoTitleSelection(context.selection));\n\t\ttshift += _titleLines * lineHeight;\n\t}\n\tif (_descriptionLines) {\n\t\tconst auto endskip = _description.hasSkipBlock()\n\t\t\t? _parent->skipBlockWidth()\n\t\t\t: 0;\n\t\t_parent->prepareCustomEmojiPaint(p, context, _description);\n\t\t_description.draw(p, {\n\t\t\t.position = { inner.left(), tshift },\n\t\t\t.outerWidth = width(),\n\t\t\t.availableWidth = paintw,\n\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t.now = context.now,\n\t\t\t.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),\n\t\t\t.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),\n\t\t\t.selection = toDescriptionSelection(context.selection),\n\t\t\t.elisionHeight = ((_descriptionLines > 0)\n\t\t\t\t? (_descriptionLines * lineHeight)\n\t\t\t\t: 0),\n\t\t\t.elisionRemoveFromEnd = (_descriptionLines > 0) ? endskip : 0,\n\t\t\t.useFullWidth = true,\n\t\t});\n\t\ttshift += (_descriptionLines > 0)\n\t\t\t? (_descriptionLines * lineHeight)\n\t\t\t: _description.countHeight(paintw);\n\t}\n\tif (factcheck && factcheck->expanded) {\n\t\tconst auto skip = st::factcheckFooterSkip;\n\t\tconst auto line = st::lineWidth;\n\t\tconst auto separatorTop = tshift + skip / 2;\n\n\t\tauto color = cache->icon;\n\t\tcolor.setAlphaF(color.alphaF() * 0.3);\n\t\tp.fillRect(inner.left(), separatorTop, paintw, line, color);\n\n\t\tp.setPen(cache->icon);\n\t\tfactcheck->footer.draw(p, {\n\t\t\t.position = { inner.left(), tshift + skip },\n\t\t\t.outerWidth = width(),\n\t\t\t.availableWidth = paintw,\n\t\t});\n\t\ttshift += factcheck->footerHeight;\n\t}\n\tif (_attach) {\n\t\tconst auto attachAtTop = hasSponsoredMedia\n\t\t\t|| (!_siteNameLines && !_titleLines && !_descriptionLines);\n\t\tif (!attachAtTop) {\n\t\t\ttshift += st::mediaInBubbleSkip;\n\t\t}\n\n\t\tconst auto attachLeft = rtl()\n\t\t\t? (width() - (inner.left() - bubble.left()) - _attach->width())\n\t\t\t: (inner.left() - bubble.left());\n\t\tconst auto attachTop = hasSponsoredMedia\n\t\t\t? inner.top()\n\t\t\t: (tshift - bubble.top());\n\n\t\tp.translate(attachLeft, attachTop);\n\n\t\t_attach->draw(p, context.translated(\n\t\t\t-attachLeft,\n\t\t\t-attachTop\n\t\t).withSelection(context.selected()\n\t\t\t? FullSelection\n\t\t\t: TextSelection()));\n\t\tconst auto pixwidth = _attach->width();\n\t\tconst auto pixheight = _attach->height();\n\n\t\tif (_data->type == WebPageType::Video\n\t\t\t&& _collage.empty()\n\t\t\t&& _data->photo\n\t\t\t&& !_data->document) {\n\t\t\tif (_attach->isReadyForOpen()) {\n\t\t\t\tif (_data->siteName == u\"YouTube\"_q) {\n\t\t\t\t\tst->youtubeIcon().paint(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\t(pixwidth - st::youtubeIcon.width()) / 2,\n\t\t\t\t\t\t(pixheight - st::youtubeIcon.height()) / 2,\n\t\t\t\t\t\twidth());\n\t\t\t\t} else {\n\t\t\t\t\tst->videoIcon().paint(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\t(pixwidth - st::videoIcon.width()) / 2,\n\t\t\t\t\t\t(pixheight - st::videoIcon.height()) / 2,\n\t\t\t\t\t\twidth());\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_durationWidth) {\n\t\t\t\tconst auto dateX = pixwidth\n\t\t\t\t\t- _durationWidth\n\t\t\t\t\t- st::msgDateImgDelta\n\t\t\t\t\t- 2 * st::msgDateImgPadding.x();\n\t\t\t\tconst auto dateY = pixheight\n\t\t\t\t\t- st::msgDateFont->height\n\t\t\t\t\t- 2 * st::msgDateImgPadding.y()\n\t\t\t\t\t- st::msgDateImgDelta;\n\t\t\t\tconst auto dateW = pixwidth - dateX - st::msgDateImgDelta;\n\t\t\t\tconst auto dateH = pixheight - dateY - st::msgDateImgDelta;\n\n\t\t\t\tUi::FillRoundRect(\n\t\t\t\t\tp,\n\t\t\t\t\tdateX,\n\t\t\t\t\tdateY,\n\t\t\t\t\tdateW,\n\t\t\t\t\tdateH,\n\t\t\t\t\tsti->msgDateImgBg,\n\t\t\t\t\tsti->msgDateImgBgCorners);\n\n\t\t\t\tp.setFont(st::msgDateFont);\n\t\t\t\tp.setPen(st->msgDateImgFg());\n\t\t\t\tp.drawTextLeft(\n\t\t\t\t\tdateX + st::msgDateImgPadding.x(),\n\t\t\t\t\tdateY + st::msgDateImgPadding.y(),\n\t\t\t\t\tpixwidth,\n\t\t\t\t\t_duration);\n\t\t\t}\n\t\t}\n\n\t\tp.translate(-attachLeft, -attachTop);\n\n\t\tif (!attachAdditionalInfoText.isEmpty()) {\n\t\t\tp.setFont(st::msgDateFont);\n\t\t\tp.setPen(stm->msgDateFg);\n\t\t\tp.drawTextLeft(\n\t\t\t\tst::msgPadding.left(),\n\t\t\t\touter.y() + outer.height() + st::mediaInBubbleSkip,\n\t\t\t\twidth(),\n\t\t\t\tattachAdditionalInfoText);\n\t\t}\n\t}\n\n\tif (!_openButton.isEmpty()) {\n\t\tp.setFont(st::semiboldFont);\n\t\tp.setPen(cache->icon);\n\t\tconst auto end = inner.y() + inner.height() + _st.padding.bottom();\n\t\tconst auto line = st::historyPageButtonLine;\n\t\tauto color = cache->icon;\n\t\tcolor.setAlphaF(color.alphaF() * 0.3);\n\t\tp.fillRect(inner.x(), end, inner.width(), line, color);\n\t\t_openButton.draw(p, {\n\t\t\t.position = QPoint(\n\t\t\t\tinner.x() + (inner.width() - _openButton.maxWidth()) / 2,\n\t\t\t\tend + st::historyPageButtonPadding.top()),\n\t\t\t.availableWidth = inner.width(),\n\t\t\t.now = context.now,\n\t\t\t.elisionLines = 1,\n\t\t});\n\t}\n}\n\nbool WebPage::asArticle() const {\n\treturn _asArticle && (_data->photo != nullptr);\n}\n\nWebPage::StickerSetData *WebPage::stickerSetData() const {\n\treturn std::get_if<StickerSetData>(_additionalData.get());\n}\n\nWebPage::SponsoredData *WebPage::sponsoredData() const {\n\treturn std::get_if<SponsoredData>(_additionalData.get());\n}\n\nWebPage::FactcheckData *WebPage::factcheckData() const {\n\treturn std::get_if<FactcheckData>(_additionalData.get());\n}\n\nWebPage::HintData *WebPage::hintData() const {\n\tif (const auto sponsored = sponsoredData()) {\n\t\treturn sponsored->hint.link ? &sponsored->hint : nullptr;\n\t} else if (const auto factcheck = factcheckData()) {\n\t\treturn factcheck->hint.link ? &factcheck->hint : nullptr;\n\t}\n\treturn nullptr;\n}\n\nTextState WebPage::textState(QPoint point, StateRequest request) const {\n\tauto result = TextState(_parent);\n\n\tif (width() < rect::m::sum::h(st::msgPadding) + 1) {\n\t\treturn result;\n\t}\n\tconst auto sponsored = sponsoredData();\n\tconst auto bubble = _attach ? _attach->bubbleMargins() : QMargins();\n\tconst auto full = Rect(currentSize());\n\tauto outer = full - inBubblePadding();\n\tif (sponsored) {\n\t\touter.translate(0, st::msgDateFont->height);\n\t}\n\tconst auto inner = outer - innerMargin();\n\tauto tshift = inner.top();\n\tauto paintw = inner.width();\n\n\tconst auto hasSponsoredMedia = sponsored && sponsored->hasMedia;\n\tif (hasSponsoredMedia && _attach) {\n\t\ttshift += _attach->height() + st::mediaInBubbleSkip;\n\t}\n\n\tconst auto lineHeight = UnitedLineHeight();\n\tauto inThumb = false;\n\tif (asArticle()) {\n\t\tconst auto pw = std::max(_pixw, lineHeight);\n\t\tinThumb = style::rtlrect(\n\t\t\tinner.left() + paintw - pw,\n\t\t\ttshift,\n\t\t\tpw,\n\t\t\t_pixh,\n\t\t\twidth()).contains(point);\n\t\tpaintw -= pw + st::webPagePhotoDelta;\n\t}\n\tauto symbolAdd = int(0);\n\tif (_siteNameLines) {\n\t\tif (point.y() >= tshift && point.y() < tshift + lineHeight) {\n\t\t\tauto siteNameRequest = Ui::Text::StateRequestElided(\n\t\t\t\trequest.forText());\n\t\t\tsiteNameRequest.lines = _siteNameLines;\n\t\t\tresult = TextState(\n\t\t\t\t_parent,\n\t\t\t\t_siteName.getStateElidedLeft(\n\t\t\t\t\tpoint - QPoint(inner.left(), tshift),\n\t\t\t\t\tpaintw,\n\t\t\t\t\twidth(),\n\t\t\t\t\tsiteNameRequest));\n\t\t} else if (point.y() >= tshift + lineHeight) {\n\t\t\tsymbolAdd += _siteName.length();\n\t\t}\n\t\ttshift += lineHeight;\n\t}\n\tif (_titleLines) {\n\t\tif (point.y() >= tshift\n\t\t\t&& point.y() < tshift + _titleLines * lineHeight) {\n\t\t\tauto titleRequest = Ui::Text::StateRequestElided(\n\t\t\t\trequest.forText());\n\t\t\ttitleRequest.lines = _titleLines;\n\t\t\tresult = TextState(\n\t\t\t\t_parent,\n\t\t\t\t_title.getStateElidedLeft(\n\t\t\t\t\tpoint - QPoint(inner.left(), tshift),\n\t\t\t\t\tpaintw,\n\t\t\t\t\twidth(),\n\t\t\t\t\ttitleRequest));\n\t\t} else if (point.y() >= tshift + _titleLines * lineHeight) {\n\t\t\tsymbolAdd += _title.length();\n\t\t}\n\t\ttshift += _titleLines * lineHeight;\n\t}\n\tif (_descriptionLines) {\n\t\tconst auto descriptionHeight = (_descriptionLines > 0)\n\t\t\t? _descriptionLines * lineHeight\n\t\t\t: _description.countHeight(paintw);\n\t\tif (point.y() >= tshift && point.y() < tshift + descriptionHeight) {\n\t\t\tif (_descriptionLines > 0) {\n\t\t\t\tauto descriptionRequest = Ui::Text::StateRequestElided(\n\t\t\t\t\trequest.forText());\n\t\t\t\tdescriptionRequest.lines = _descriptionLines;\n\t\t\t\tresult = TextState(\n\t\t\t\t\t_parent,\n\t\t\t\t\t_description.getStateElidedLeft(\n\t\t\t\t\t\tpoint - QPoint(inner.left(), tshift),\n\t\t\t\t\t\tpaintw,\n\t\t\t\t\t\twidth(),\n\t\t\t\t\t\tdescriptionRequest));\n\t\t\t} else {\n\t\t\t\tresult = TextState(\n\t\t\t\t\t_parent,\n\t\t\t\t\t_description.getStateLeft(\n\t\t\t\t\t\tpoint - QPoint(inner.left(), tshift),\n\t\t\t\t\t\tpaintw,\n\t\t\t\t\t\twidth(),\n\t\t\t\t\t\trequest.forText()));\n\t\t\t}\n\t\t} else if (point.y() >= tshift + descriptionHeight) {\n\t\t\tsymbolAdd += _description.length();\n\t\t}\n\t\ttshift += descriptionHeight;\n\t}\n\tauto isWithinSponsoredMedia = false;\n\tif (inThumb) {\n\t\tresult.link = _openl;\n\t} else if (_attach) {\n\t\tconst auto attachAtTop = hasSponsoredMedia\n\t\t\t|| (!_siteNameLines && !_titleLines && !_descriptionLines);\n\t\tif (!attachAtTop) {\n\t\t\ttshift += st::mediaInBubbleSkip;\n\t\t}\n\t\tif (hasSponsoredMedia) {\n\t\t\ttshift -= _attach->height();\n\t\t}\n\n\t\tconst auto rect = hasSponsoredMedia\n\t\t\t? QRect(\n\t\t\t\tinner.left(),\n\t\t\t\tinner.top(),\n\t\t\t\t_attach->width(),\n\t\t\t\t_attach->height())\n\t\t\t: QRect(\n\t\t\t\tinner.left(),\n\t\t\t\ttshift,\n\t\t\t\tpaintw,\n\t\t\t\tinner.top() + inner.height() - tshift);\n\t\tif (rect.contains(point)) {\n\t\t\tconst auto attachLeft = rtl()\n\t\t\t\t? width() - (inner.left() - bubble.left()) - _attach->width()\n\t\t\t\t: (inner.left() - bubble.left());\n\t\t\tconst auto attachTop = hasSponsoredMedia\n\t\t\t\t? inner.top()\n\t\t\t\t: (tshift - bubble.top());\n\t\t\tresult = _attach->textState(\n\t\t\t\tpoint - QPoint(attachLeft, attachTop),\n\t\t\t\trequest);\n\t\t\tif (hasSponsoredMedia) {\n\t\t\t\tisWithinSponsoredMedia = true;\n\t\t\t} else if (result.cursor == CursorState::Enlarge) {\n\t\t\t\tresult.cursor = CursorState::None;\n\t\t\t} else {\n\t\t\t\tresult.link = replaceAttachLink(result.link);\n\t\t\t}\n\t\t}\n\t\tif (hasSponsoredMedia) {\n\t\t\ttshift += _attach->height();\n\t\t}\n\t}\n\tif (isWithinSponsoredMedia) {\n\t\tresult.link = sponsored->mediaLink;\n\t} else if (sponsored && outer.contains(point)) {\n\t\tresult.link = sponsored->link;\n\t}\n\tif (!result.link && outer.contains(point)) {\n\t\tresult.link = _openl;\n\t}\n\tif (const auto hint = hintData()) {\n\t\tconst auto check = point\n\t\t\t- QPoint(0, sponsored ? st::msgDateFont->height : 0);\n\t\tconst auto hintRect = QRectF(hint->lastPosition, hint->size);\n\t\tif (hintRect.contains(check)) {\n\t\t\tresult.link = hint->link;\n\t\t}\n\t}\n\t_lastPoint = point - outer.topLeft();\n\n\tresult.symbol += symbolAdd;\n\treturn result;\n}\n\nClickHandlerPtr WebPage::replaceAttachLink(\n\t\tconst ClickHandlerPtr &link) const {\n\tif (!_attach->isReadyForOpen()\n\t\t|| (_siteName.isEmpty()\n\t\t\t&& _title.isEmpty()\n\t\t\t&& _description.isEmpty())\n\t\t|| (_data->document\n\t\t\t&& !_data->document->isWallPaper()\n\t\t\t&& !_data->document->isTheme())\n\t\t|| !_data->collage.items.empty()) {\n\t\treturn link;\n\t}\n\treturn _openl;\n}\n\nTextSelection WebPage::adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const {\n\tif ((!_titleLines && !_descriptionLines)\n\t\t|| selection.to <= _siteName.length()) {\n\t\treturn _siteName.adjustSelection(selection, type);\n\t}\n\n\tconst auto titlesLength = _siteName.length() + _title.length();\n\tconst auto titleSelection = _title.adjustSelection(\n\t\ttoTitleSelection(selection),\n\t\ttype);\n\tif ((!_siteNameLines && !_descriptionLines)\n\t\t|| (selection.from >= _siteName.length()\n\t\t\t&& selection.to <= titlesLength)) {\n\t\treturn fromTitleSelection(titleSelection);\n\t}\n\n\tconst auto descriptionSelection = _description.adjustSelection(\n\t\ttoDescriptionSelection(selection),\n\t\ttype);\n\tif ((!_siteNameLines && !_titleLines) || selection.from >= titlesLength) {\n\t\treturn fromDescriptionSelection(descriptionSelection);\n\t}\n\n\treturn {\n\t\t_siteName.adjustSelection(selection, type).from,\n\t\t(!_descriptionLines || selection.to <= titlesLength)\n\t\t\t? fromTitleSelection(titleSelection).to\n\t\t\t: fromDescriptionSelection(descriptionSelection).to,\n\t};\n}\n\nuint16 WebPage::fullSelectionLength() const {\n\treturn _siteName.length() + _title.length() + _description.length();\n}\n\nvoid WebPage::clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool active) {\n\tif (_attach) {\n\t\t_attach->clickHandlerActiveChanged(p, active);\n\t}\n}\n\nvoid WebPage::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed) {\n\tconst auto hint = hintData();\n\tif (hint && hint->link == p) {\n\t\tif (pressed) {\n\t\t\tif (!hint->ripple) {\n\t\t\t\tconst auto owner = &parent()->history()->owner();\n\t\t\t\thint->ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\t\tUi::RippleAnimation::RoundRectMask(\n\t\t\t\t\t\thint->size,\n\t\t\t\t\t\t_st.radius),\n\t\t\t\t\t[=] { owner->requestViewRepaint(parent()); });\n\t\t\t}\n\t\t\tconst auto full = Rect(currentSize());\n\t\t\tconst auto outer = full - inBubblePadding();\n\t\t\thint->ripple->add(_lastPoint\n\t\t\t\t+ outer.topLeft()\n\t\t\t\t- hint->lastPosition.toPoint());\n\t\t} else if (hint->ripple) {\n\t\t\thint->ripple->lastStop();\n\t\t}\n\t\treturn;\n\t}\n\tif ((p == _openl) || (sponsoredData() && sponsoredData()->link == p)) {\n\t\tif (pressed) {\n\t\t\tif (!_ripple) {\n\t\t\t\tconst auto full = Rect(currentSize());\n\t\t\t\tconst auto outer = full - inBubblePadding();\n\t\t\t\tconst auto owner = &parent()->history()->owner();\n\t\t\t\t_ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\t\tUi::RippleAnimation::RoundRectMask(\n\t\t\t\t\t\touter.size(),\n\t\t\t\t\t\t_st.radius),\n\t\t\t\t\t[=] { owner->requestViewRepaint(parent()); });\n\t\t\t}\n\t\t\t_ripple->add(_lastPoint);\n\t\t} else if (_ripple) {\n\t\t\t_ripple->lastStop();\n\t\t}\n\t}\n\tif (_attach) {\n\t\t_attach->clickHandlerPressedChanged(p, pressed);\n\t}\n}\n\nbool WebPage::enforceBubbleWidth() const {\n\treturn (_attach != nullptr)\n\t\t&& (_data->document != nullptr)\n\t\t&& (_data->document->isWallPaper() || _data->document->isTheme());\n}\n\nbool WebPage::allowsNarrowBubble() const {\n\treturn (_attach != nullptr)\n\t\t&& (_data->uniqueGift != nullptr || _data->auction != nullptr);\n}\n\nint WebPage::minBubbleWidthForNarrowBubble() const {\n\treturn allowsNarrowBubble() ? maxWidth() : 0;\n}\n\nvoid WebPage::playAnimation(bool autoplay) {\n\tif (_attach) {\n\t\tif (autoplay) {\n\t\t\t_attach->autoplayAnimation();\n\t\t} else {\n\t\t\t_attach->playAnimation();\n\t\t}\n\t}\n}\n\nbool WebPage::isDisplayed() const {\n\treturn !_data->pendingTill\n\t\t&& !_data->failed\n\t\t&& !_parent->data()->Has<HistoryMessageLogEntryOriginal>();\n}\n\nQString WebPage::additionalInfoString() const {\n\treturn _attach ? _attach->additionalInfoString() : QString();\n}\n\nbool WebPage::toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const {\n\treturn _attach && _attach->toggleSelectionByHandlerClick(p);\n}\n\nbool WebPage::allowTextSelectionByHandler(const ClickHandlerPtr &p) const {\n\treturn (p == _openl);\n}\n\nbool WebPage::dragItemByHandler(const ClickHandlerPtr &p) const {\n\treturn _attach && _attach->dragItemByHandler(p);\n}\n\nTextForMimeData WebPage::selectedText(TextSelection selection) const {\n\tauto siteNameResult = _siteName.toTextForMimeData(selection);\n\tauto titleResult = _title.toTextForMimeData(toTitleSelection(selection));\n\tauto descriptionResult = _description.toTextForMimeData(\n\t\ttoDescriptionSelection(selection));\n\tif (titleResult.empty() && descriptionResult.empty()) {\n\t\treturn siteNameResult;\n\t} else if (siteNameResult.empty() && descriptionResult.empty()) {\n\t\treturn titleResult;\n\t} else if (siteNameResult.empty() && titleResult.empty()) {\n\t\treturn descriptionResult;\n\t} else if (siteNameResult.empty()) {\n\t\treturn titleResult.append('\\n').append(std::move(descriptionResult));\n\t} else if (titleResult.empty()) {\n\t\treturn siteNameResult\n\t\t\t.append('\\n')\n\t\t\t.append(std::move(descriptionResult));\n\t} else if (descriptionResult.empty()) {\n\t\treturn siteNameResult.append('\\n').append(std::move(titleResult));\n\t}\n\n\treturn siteNameResult\n\t\t.append('\\n')\n\t\t.append(std::move(titleResult))\n\t\t.append('\\n')\n\t\t.append(std::move(descriptionResult));\n}\n\nQMargins WebPage::inBubblePadding() const {\n\treturn {\n\t\tst::msgPadding.left(),\n\t\tisBubbleTop() ? st::msgPadding.left() : 0,\n\t\tst::msgPadding.right(),\n\t\tisBubbleBottom() ? (st::msgPadding.left() + bottomInfoPadding()) : 0\n\t};\n}\n\nQMargins WebPage::innerMargin() const {\n\tconst auto button = _openButton.isEmpty()\n\t\t? 0\n\t\t: st::historyPageButtonHeight;\n\treturn _st.padding + QMargins(0, 0, 0, button);\n}\n\nbool WebPage::isLogEntryOriginal() const {\n\treturn _parent->data()->isAdminLogEntry() && _parent->media() != this;\n}\n\nWebPage::FactcheckMetrics WebPage::computeFactcheckMetrics(\n\t\tint fullHeight) const {\n\tconst auto possible = fullHeight / st::normalFont->height;\n\t//const auto expandable = (possible > kFactcheckCollapsedLines + 1);\n\t// Now always expandable because of the footer.\n\tconst auto expandable = true;\n\tconst auto check = _parent->Get<Factcheck>();\n\tconst auto expanded = check && check->expanded;\n\tconst auto allowExpanding = (expanded || !expandable);\n\treturn {\n\t\t.lines = allowExpanding ? possible : kFactcheckCollapsedLines,\n\t\t.expandable = expandable,\n\t\t.expanded = expanded,\n\t};\n}\n\nint WebPage::bottomInfoPadding() const {\n\tif (!isBubbleBottom()) {\n\t\treturn 0;\n\t}\n\n\tauto result = st::msgDateFont->height;\n\n\t// We use padding greater than st::msgPadding.bottom() in the\n\t// bottom of the bubble so that the left line looks pretty.\n\t// but if we have bottom skip because of the info display\n\t// we don't need that additional padding so we replace it\n\t// back with st::msgPadding.bottom() instead of left().\n\tresult += st::msgPadding.bottom() - st::msgPadding.left();\n\treturn result;\n}\n\nWebPage::~WebPage() {\n\thistory()->owner().unregisterWebPageView(_data, _parent);\n\tif (_photoMedia) {\n\t\thistory()->owner().keepAlive(base::take(_photoMedia));\n\t\t_parent->checkHeavyPart();\n\t}\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/history_view_web_page.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/media/history_view_media.h\"\n#include \"ui/userpic_view.h\"\n\nnamespace Data {\nclass DocumentMedia;\nclass Media;\nclass PhotoMedia;\n} // namespace Data\n\nnamespace Ui {\nclass RippleAnimation;\n} // namespace Ui\n\nnamespace HistoryView {\n\nclass Sticker;\n\nclass WebPage : public Media {\npublic:\n\tWebPage(\n\t\tnot_null<Element*> parent,\n\t\tnot_null<WebPageData*> data,\n\t\tMediaWebPageFlags flags);\n\n\tvoid refreshParentId(not_null<HistoryItem*> realParent) override;\n\n\tvoid draw(Painter &p, const PaintContext &context) const override;\n\tTextState textState(QPoint point, StateRequest request) const override;\n\n\tbool aboveTextByDefault() const override {\n\t\treturn false;\n\t}\n\tbool hideMessageText() const override {\n\t\treturn false;\n\t}\n\n\t[[nodiscard]] TextSelection adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const override;\n\tuint16 fullSelectionLength() const override;\n\tbool hasTextForCopy() const override {\n\t\t// We do not add _title and _description in FullSelection text copy.\n\t\treturn false;\n\t}\n\tQString additionalInfoString() const override;\n\n\tbool toggleSelectionByHandlerClick(\n\t\tconst ClickHandlerPtr &p) const override;\n\tbool allowTextSelectionByHandler(\n\t\tconst ClickHandlerPtr &p) const override;\n\tbool dragItemByHandler(const ClickHandlerPtr &p) const override;\n\n\tTextForMimeData selectedText(TextSelection selection) const override;\n\n\tvoid clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &p, bool active) override;\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p, bool pressed) override;\n\n\tbool isDisplayed() const override;\n\tPhotoData *getPhoto() const override {\n\t\treturn _attach ? _attach->getPhoto() : nullptr;\n\t}\n\tDocumentData *getDocument() const override {\n\t\treturn _attach ? _attach->getDocument() : nullptr;\n\t}\n\tvoid stopAnimation() override {\n\t\tif (_attach) _attach->stopAnimation();\n\t}\n\tvoid checkAnimation() override {\n\t\tif (_attach) _attach->checkAnimation();\n\t}\n\n\tnot_null<WebPageData*> webpage() {\n\t\treturn _data;\n\t}\n\n\tbool needsBubble() const override {\n\t\treturn true;\n\t}\n\tbool customInfoLayout() const override {\n\t\treturn false;\n\t}\n\tbool allowsFastShare() const override {\n\t\treturn true;\n\t}\n\tbool enforceBubbleWidth() const override;\n\tbool allowsNarrowBubble() const override;\n\tint minBubbleWidthForNarrowBubble() const override;\n\n\tMedia *attach() const {\n\t\treturn _attach.get();\n\t}\n\n\tbool hasHeavyPart() const override;\n\tvoid unloadHeavyPart() override;\n\n\t~WebPage();\n\nprivate:\n\tstruct FactcheckMetrics {\n\t\tint lines = 0;\n\t\tbool expandable = false;\n\t\tbool expanded = false;\n\t};\n\tstruct HintData {\n\t\tQSize size;\n\t\tQPointF lastPosition;\n\t\tQString text;\n\t\tint widthBefore = 0;\n\t\tstd::unique_ptr<Ui::RippleAnimation> ripple;\n\t\tClickHandlerPtr link;\n\t};\n\tstruct StickerSetData {\n\t\tstd::vector<std::unique_ptr<Sticker>> views;\n\t};\n\tstruct SponsoredData {\n\t\tClickHandlerPtr link;\n\t\tClickHandlerPtr mediaLink;\n\t\tQString buttonText;\n\n\t\tuint64 backgroundEmojiId = 0;\n\t\tuint8 colorIndex : 6 = 0;\n\t\tuint8 isLinkInternal : 1 = 0;\n\t\tuint8 canReport : 1 = 0;\n\t\tuint8 hasMedia : 1 = 0;\n\n\t\tHintData hint;\n\t};\n\tstruct FactcheckData {\n\t\tHintData hint;\n\t\tUi::Text::String footer;\n\t\tuint32 footerHeight : 30 = 0;\n\t\tuint32 expandable : 1 = 0;\n\t\tuint32 expanded : 1 = 0;\n\t};\n\tusing AdditionalData = std::variant<\n\t\tStickerSetData,\n\t\tSponsoredData,\n\t\tFactcheckData>;\n\n\tvoid playAnimation(bool autoplay) override;\n\tQSize countOptimalSize() override;\n\tQSize countCurrentSize(int newWidth) override;\n\n\tvoid ensurePhotoMediaCreated() const;\n\n\t[[nodiscard]] TextSelection toTitleSelection(\n\t\tTextSelection selection) const;\n\t[[nodiscard]] TextSelection fromTitleSelection(\n\t\tTextSelection selection) const;\n\t[[nodiscard]] TextSelection toDescriptionSelection(\n\t\tTextSelection selection) const;\n\t[[nodiscard]] TextSelection fromDescriptionSelection(\n\t\tTextSelection selection) const;\n\t[[nodiscard]] QMargins inBubblePadding() const;\n\t[[nodiscard]] QMargins innerMargin() const;\n\t[[nodiscard]] int bottomInfoPadding() const;\n\t[[nodiscard]] bool isLogEntryOriginal() const;\n\n\t[[nodiscard]] ClickHandlerPtr replaceAttachLink(\n\t\tconst ClickHandlerPtr &link) const;\n\t[[nodiscard]] bool asArticle() const;\n\n\t[[nodiscard]] StickerSetData *stickerSetData() const;\n\t[[nodiscard]] SponsoredData *sponsoredData() const;\n\t[[nodiscard]] FactcheckData *factcheckData() const;\n\t[[nodiscard]] HintData *hintData() const;\n\n\t[[nodiscard]] FactcheckMetrics computeFactcheckMetrics(\n\t\tint fullHeight) const;\n\n\tvoid setupAdditionalData();\n\n\tconst style::QuoteStyle &_st;\n\tconst not_null<WebPageData*> _data;\n\tconst MediaWebPageFlags _flags;\n\n\tstd::vector<std::unique_ptr<Data::Media>> _collage;\n\tClickHandlerPtr _openl;\n\tstd::unique_ptr<Media> _attach;\n\tmutable std::shared_ptr<Data::PhotoMedia> _photoMedia;\n\tmutable std::unique_ptr<Ui::RippleAnimation> _ripple;\n\n\tint _dataVersion = -1;\n\tint _siteNameLines = 0;\n\tint _descriptionLines = 0;\n\tuint32 _titleLines : 31 = 0;\n\tuint32 _asArticle : 1 = 0;\n\n\tUi::Text::String _siteName;\n\tUi::Text::String _title;\n\tUi::Text::String _description;\n\tUi::Text::String _openButton;\n\n\tQString _duration;\n\tint _durationWidth = 0;\n\n\tmutable QPoint _lastPoint;\n\tint _pixw = 0;\n\tint _pixh = 0;\n\n\tstd::unique_ptr<AdditionalData> _additionalData;\n\n};\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/media/menu/history_view_poll_menu.h\"\n\n#include \"api/api_polls.h\"\n#include \"api/api_toggling_media.h\"\n#include \"apiwrap.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"data/data_cloud_file.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_location.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"editor/editor_layer_widget.h\"\n#include \"editor/photo_editor.h\"\n#include \"history/history_location_manager.h\"\n#include \"history/view/history_view_group_call_bar.h\"\n#include \"history/view/history_view_reaction_preview.h\"\n#include \"history/view/media/history_view_document.h\"\n#include \"lang/lang_keys.h\"\n#include \"layout/layout_document_generic_preview.h\"\n#include \"main/main_session.h\"\n#include \"poll/poll_media_upload.h\"\n#include \"mainwidget.h\"\n#include \"storage/localimageloader.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/format_song_name.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_media_view.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace HistoryView {\nnamespace {\n\nclass VotersItem final : public Ui::Menu::ItemBase {\npublic:\n\tVotersItem(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::Menu &st,\n\t\tint votes,\n\t\tconst std::vector<not_null<PeerData*>> &recentVoters);\n\n\tnot_null<QAction*> action() const override;\n\tbool isEnabled() const override;\n\nprotected:\n\tint contentHeight() const override;\n\nprivate:\n\tvoid paint(Painter &p);\n\n\tconst not_null<QAction*> _dummyAction;\n\tconst style::Menu &_st;\n\tconst int _votes = 0;\n\tconst int _height = 0;\n\tQImage _userpics;\n\tint _userpicsWidth = 0;\n};\n\nVotersItem::VotersItem(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::Menu &st,\n\tint votes,\n\tconst std::vector<not_null<PeerData*>> &recentVoters)\n: ItemBase(parent, st)\n, _dummyAction(Ui::CreateChild<QAction>(parent.get()))\n, _st(st)\n, _votes(votes)\n, _height(st.itemPadding.top()\n\t+ st.itemStyle.font->height\n\t+ st.itemPadding.bottom()) {\n\tauto prepared = PrepareUserpicsInRow(\n\t\trecentVoters,\n\t\tst::historyCommentsUserpics);\n\t_userpics = std::move(prepared.image);\n\t_userpicsWidth = prepared.width;\n\n\tconst auto votesText = tr::lng_polls_votes_count(\n\t\ttr::now,\n\t\tlt_count,\n\t\t_votes);\n\tconst auto spacing = _userpicsWidth > 0\n\t\t? st::normalFont->spacew * 2\n\t\t: 0;\n\tconst auto textWidth = st.itemStyle.font->width(votesText);\n\tconst auto &textPadding = st::defaultMenu.itemPadding;\n\tconst auto minWidth = textPadding.left()\n\t\t+ textWidth\n\t\t+ spacing\n\t\t+ _userpicsWidth\n\t\t+ st.itemPadding.right();\n\tsetMinWidth(minWidth);\n\n\tpaintRequest() | rpl::on_next([=] {\n\t\tPainter p(this);\n\t\tpaint(p);\n\t}, lifetime());\n\n\tfitToMenuWidth();\n}\n\nnot_null<QAction*> VotersItem::action() const {\n\treturn _dummyAction;\n}\n\nbool VotersItem::isEnabled() const {\n\treturn false;\n}\n\nint VotersItem::contentHeight() const {\n\treturn _height;\n}\n\nvoid VotersItem::paint(Painter &p) {\n\tp.fillRect(0, 0, width(), _height, _st.itemBg);\n\n\tconst auto votesText = tr::lng_polls_votes_count(\n\t\ttr::now,\n\t\tlt_count,\n\t\t_votes);\n\n\tp.setPen(_st.itemFg);\n\tp.setFont(_st.itemStyle.font);\n\tconst auto &textPadding = st::defaultMenu.itemPadding;\n\tconst auto textY = _st.itemPadding.top()\n\t\t+ _st.itemStyle.font->ascent;\n\tp.drawText(textPadding.left(), textY, votesText);\n\n\tif (!_userpics.isNull()) {\n\t\tconst auto userpicsHeight\n\t\t\t= _userpics.height() / style::DevicePixelRatio();\n\t\tconst auto x = width() - _st.itemPadding.right() - _userpicsWidth;\n\t\tconst auto y = (_height - userpicsHeight) / 2;\n\t\tp.drawImage(x, y, _userpics);\n\t}\n}\n\n} // namespace\n\nvoid FillPollAnswerMenu(\n\t\tnot_null<Ui::DropdownMenu*> menu,\n\t\tnot_null<PollData*> poll,\n\t\tconst QByteArray &option,\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId itemId,\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto session = &controller->session();\n\tauto addedVotersItem = false;\n\tif (const auto answer = poll->answerByOption(option)) {\n\t\tconst auto canVote = !option.isEmpty()\n\t\t\t&& !poll->closed()\n\t\t\t&& !poll->quiz()\n\t\t\t&& !poll->voted();\n\t\tif (!canVote && answer->votes > 0) {\n\t\t\tmenu->addAction(\n\t\t\t\tbase::make_unique_q<VotersItem>(\n\t\t\t\t\tmenu->menu(),\n\t\t\t\t\tmenu->menu()->st(),\n\t\t\t\t\tanswer->votes,\n\t\t\t\t\tanswer->recentVoters));\n\t\t\tmenu->addSeparator(&st::expandedMenuSeparator);\n\t\t\taddedVotersItem = true;\n\t\t}\n\t}\n\tif (!option.isEmpty()\n\t\t&& !poll->closed()\n\t\t&& !poll->quiz()) {\n\t\tif (poll->voted()\n\t\t\t&& !poll->revotingDisabled()) {\n\t\t\tmenu->addAction(\n\t\t\t\ttr::lng_polls_retract(tr::now),\n\t\t\t\t[=] {\n\t\t\t\t\tsession->api().polls().sendVotes(\n\t\t\t\t\t\titemId,\n\t\t\t\t\t\t{});\n\t\t\t\t},\n\t\t\t\t&st::menuIconRetractVote);\n\t\t} else if (!poll->voted()\n\t\t\t&& poll->sendingVotes.empty()) {\n\t\t\tmenu->addAction(\n\t\t\t\ttr::lng_polls_submit_votes(tr::now),\n\t\t\t\t[=] {\n\t\t\t\t\tsession->api().polls().sendVotes(\n\t\t\t\t\t\titemId,\n\t\t\t\t\t\t{ option });\n\t\t\t\t},\n\t\t\t\t&st::menuIconSelect);\n\t\t\tif (!addedVotersItem) {\n\t\t\t\tmenu->addSeparator(\n\t\t\t\t\t&st::expandedMenuSeparator);\n\t\t\t}\n\t\t}\n\t}\n\tconst auto show = controller->uiShow();\n\tconst auto isFaved\n\t\t= document->owner().stickers().isFaved(document);\n\tmenu->addAction(\n\t\t(isFaved\n\t\t\t? tr::lng_faved_stickers_remove\n\t\t\t: tr::lng_faved_stickers_add)(tr::now),\n\t\t[=] {\n\t\t\tApi::ToggleFavedSticker(\n\t\t\t\tshow,\n\t\t\t\tdocument,\n\t\t\t\tData::FileOriginStickerSet(\n\t\t\t\t\tData::Stickers::FavedSetId,\n\t\t\t\t\t0));\n\t\t},\n\t\tisFaved\n\t\t\t? &st::menuIconUnfave\n\t\t\t: &st::menuIconFave);\n\tif (const auto sticker = document->sticker()) {\n\t\tconst auto setId = sticker->set;\n\t\tif (setId.id) {\n\t\t\tmenu->addAction(\n\t\t\t\ttr::lng_view_button_stickerset(tr::now),\n\t\t\t\t[=] {\n\t\t\t\t\tshow->show(Box<StickerSetBox>(\n\t\t\t\t\t\tshow,\n\t\t\t\t\t\tsetId,\n\t\t\t\t\t\tData::StickersType::Stickers));\n\t\t\t\t},\n\t\t\t\t&st::menuIconStickers);\n\t\t}\n\t}\n}\n\nnamespace {\n\nvoid AddRemoveAction(\n\t\tnot_null<Ui::DropdownMenu*> menu,\n\t\tFn<void()> remove) {\n\tmenu->addAction(\n\t\tbase::make_unique_q<Ui::Menu::Action>(\n\t\t\tmenu->menu(),\n\t\t\tst::menuWithIconsAttention,\n\t\t\tUi::Menu::CreateAction(\n\t\t\t\tmenu->menu().get(),\n\t\t\t\ttr::lng_box_remove(tr::now),\n\t\t\t\tstd::move(remove)),\n\t\t\t&st::menuIconDeleteAttention,\n\t\t\t&st::menuIconDeleteAttention));\n}\n\n} // namespace\n\nvoid ShowPollStickerPreview(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<DocumentData*> document,\n\t\tFn<void()> replace,\n\t\tFn<void()> remove) {\n\tShowStickerPreview(controller, FullMsgId(), document, [=](\n\t\t\tnot_null<Ui::DropdownMenu*> menu) {\n\t\tmenu->addAction(\n\t\t\ttr::lng_attach_replace(tr::now),\n\t\t\treplace,\n\t\t\t&st::menuIconReplace);\n\t\tAddRemoveAction(menu, remove);\n\t});\n}\n\nvoid ShowPollPhotoPreview(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PhotoData*> photo,\n\t\tFn<void()> replace,\n\t\tFn<void()> edit,\n\t\tFn<void()> remove) {\n\tShowPhotoPreview(controller, FullMsgId(), photo, [=](\n\t\t\tnot_null<Ui::DropdownMenu*> menu) {\n\t\tmenu->addAction(\n\t\t\ttr::lng_attach_replace(tr::now),\n\t\t\treplace,\n\t\t\t&st::menuIconReplace);\n\t\tmenu->addAction(\n\t\t\ttr::lng_context_draw(tr::now),\n\t\t\tedit,\n\t\t\t&st::menuIconDraw);\n\t\tAddRemoveAction(menu, remove);\n\t});\n}\n\nvoid ShowPollDocumentPreview(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<DocumentData*> document,\n\t\tFn<void()> replace,\n\t\tFn<void()> remove) {\n\tconst auto isSong = document->isSong();\n\tconst auto songData = isSong ? document->song() : nullptr;\n\tconst auto media = isSong\n\t\t? document->createMediaView()\n\t\t: nullptr;\n\tif (media) {\n\t\tmedia->thumbnailWanted(Data::FileOrigin());\n\t}\n\n\tconst auto docGeneric = Layout::DocumentGenericPreview::Create(\n\t\tdocument);\n\tconst auto docIcon = docGeneric.icon();\n\tconst auto docIconColor = docGeneric.color;\n\tconst auto docExt = [&] {\n\t\tauto ext = docGeneric.ext;\n\t\tconst auto maxW = st::mediaviewFileIconSize\n\t\t\t- st::mediaviewFileExtPadding * 2;\n\t\tif (st::mediaviewFileExtFont->width(ext) > maxW) {\n\t\t\text = st::mediaviewFileExtFont->elided(ext, maxW, Qt::ElideMiddle);\n\t\t}\n\t\treturn ext;\n\t}();\n\n\tconst auto maxTextW = st::mediaviewFileSize.width()\n\t\t- st::mediaviewFileIconSize\n\t\t- st::mediaviewFilePadding * 3;\n\tconst auto docName = [&] {\n\t\tauto name = (isSong && songData)\n\t\t\t? Ui::Text::FormatSongName(\n\t\t\t\tdocument->filename(),\n\t\t\t\tsongData->title,\n\t\t\t\tsongData->performer).string()\n\t\t\t: document->filename().isEmpty()\n\t\t\t? tr::lng_mediaview_doc_image(tr::now)\n\t\t\t: document->filename();\n\t\tif (st::mediaviewFileNameFont->width(name) > maxTextW) {\n\t\t\tname = st::mediaviewFileNameFont->elided(\n\t\t\t\tname,\n\t\t\t\tmaxTextW,\n\t\t\t\tQt::ElideMiddle);\n\t\t}\n\t\treturn name;\n\t}();\n\tconst auto docSize = [&] {\n\t\tauto text = Ui::FormatSizeText(document->size);\n\t\tif (st::mediaviewFont->width(text) > maxTextW) {\n\t\t\ttext = st::mediaviewFont->elided(text, maxTextW);\n\t\t}\n\t\treturn text;\n\t}();\n\n\tShowWidgetPreview(controller, [=](not_null<Ui::RpWidget*> preview) {\n\t\tconst auto shadowExtend = st::boxRoundShadow.extend;\n\t\tconst auto fullW = st::mediaviewFileSize.width()\n\t\t\t+ rect::m::sum::h(shadowExtend);\n\t\tconst auto fullH = st::mediaviewFileSize.height()\n\t\t\t+ rect::m::sum::v(shadowExtend);\n\t\tpreview->resize(fullW, fullH);\n\n\t\tif (media) {\n\t\t\tdocument->session().downloaderTaskFinished(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tpreview->update();\n\t\t\t}, preview->lifetime());\n\t\t}\n\n\t\tpreview->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = Painter(preview);\n\t\t\tconst auto outer = preview->rect() - shadowExtend;\n\n\t\t\tUi::Shadow::paint(\n\t\t\t\tp,\n\t\t\t\touter,\n\t\t\t\tpreview->width(),\n\t\t\t\tst::boxRoundShadow);\n\t\t\t{\n\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(st::windowBg);\n\t\t\t\tp.drawRoundedRect(outer, st::boxRadius, st::boxRadius);\n\t\t\t}\n\n\t\t\tconst auto padding = st::mediaviewFilePadding;\n\t\t\tconst auto iconSize = st::mediaviewFileIconSize;\n\t\t\tconst auto iconRect = QRect(\n\t\t\t\touter.x() + padding,\n\t\t\t\touter.y() + padding,\n\t\t\t\ticonSize,\n\t\t\t\ticonSize);\n\n\t\t\tconst auto coverDrawn = isSong\n\t\t\t\t&& DrawThumbnailAsSongCover(\n\t\t\t\t\tp,\n\t\t\t\t\tst::songCoverOverlayFg,\n\t\t\t\t\tmedia,\n\t\t\t\t\ticonRect);\n\t\t\tif (!coverDrawn) {\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.fillRect(iconRect, docIconColor);\n\t\t\t\tif (docIcon) {\n\t\t\t\t\tdocIcon->paint(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\ticonRect.x()\n\t\t\t\t\t\t\t+ (iconRect.width() - docIcon->width()),\n\t\t\t\t\t\ticonRect.y(),\n\t\t\t\t\t\tpreview->width());\n\t\t\t\t}\n\t\t\t\tif (!docExt.isEmpty()) {\n\t\t\t\t\tp.setPen(st::activeButtonFg);\n\t\t\t\t\tp.setFont(st::mediaviewFileExtFont);\n\t\t\t\t\tconst auto extW\n\t\t\t\t\t\t= st::mediaviewFileExtFont->width(docExt);\n\t\t\t\t\tp.drawText(\n\t\t\t\t\t\ticonRect.x()\n\t\t\t\t\t\t\t+ (iconRect.width() - extW) / 2,\n\t\t\t\t\t\ticonRect.y()\n\t\t\t\t\t\t\t+ st::mediaviewFileExtTop\n\t\t\t\t\t\t\t+ st::mediaviewFileExtFont->ascent,\n\t\t\t\t\t\tdocExt);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (isSong && !coverDrawn) {\n\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(st::msgFileInBg);\n\t\t\t\tp.drawEllipse(iconRect);\n\t\t\t\tst::historyFileInPlay.paintInCenter(p, iconRect);\n\t\t\t}\n\n\t\t\tconst auto textX = outer.x() + 2 * padding + iconSize;\n\t\t\tp.setPen(st::windowFg);\n\t\t\tp.setFont(st::mediaviewFileNameFont);\n\t\t\tp.drawText(\n\t\t\t\ttextX,\n\t\t\t\touter.y() + padding\n\t\t\t\t\t+ st::mediaviewFileNameTop\n\t\t\t\t\t+ st::mediaviewFileNameFont->ascent,\n\t\t\t\tdocName);\n\n\t\t\tp.setPen(st::windowSubTextFg);\n\t\t\tp.setFont(st::mediaviewFont);\n\t\t\tp.drawText(\n\t\t\t\ttextX,\n\t\t\t\touter.y() + padding\n\t\t\t\t\t+ st::mediaviewFileSizeTop\n\t\t\t\t\t+ st::mediaviewFont->ascent,\n\t\t\t\tdocSize);\n\t\t}, preview->lifetime());\n\t}, [=](not_null<Ui::DropdownMenu*> menu) {\n\t\tmenu->addAction(\n\t\t\ttr::lng_attach_replace(tr::now),\n\t\t\treplace,\n\t\t\t&st::menuIconReplace);\n\t\tAddRemoveAction(menu, remove);\n\t});\n}\n\nvoid ShowPollGeoPreview(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tData::LocationPoint point,\n\t\tFn<void()> replace,\n\t\tFn<void()> remove) {\n\tconst auto session = &controller->session();\n\tconst auto cloudImage = session->data().location(point);\n\tconst auto view = cloudImage->createView();\n\tcloudImage->load(session, Data::FileOrigin());\n\n\tShowWidgetPreview(controller, [=](\n\t\t\tnot_null<Ui::RpWidget*> preview) {\n\t\tconst auto body = preview->parentWidget()->size();\n\t\tconst auto skip = st::mediaPreviewPhotoSkip;\n\t\tconst auto maxSide = std::min(\n\t\t\tbody.width() - 2 * skip,\n\t\t\tbody.height() - 2 * skip);\n\t\tconst auto side = std::min(maxSide, st::locationSize.width());\n\t\tconst auto scaled = QSize(\n\t\t\tside,\n\t\t\t(side * st::locationSize.height()\n\t\t\t\t/ st::locationSize.width()));\n\t\tconst auto shadowExtend = st::boxRoundShadow.extend;\n\t\tconst auto fullW = scaled.width()\n\t\t\t+ rect::m::sum::h(shadowExtend);\n\t\tconst auto fullH = scaled.height()\n\t\t\t+ rect::m::sum::v(shadowExtend);\n\t\tpreview->resize(fullW, fullH);\n\n\t\tsession->downloaderTaskFinished(\n\t\t) | rpl::on_next([=] {\n\t\t\tpreview->update();\n\t\t}, preview->lifetime());\n\n\t\tpreview->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = Painter(preview);\n\t\t\tconst auto outer = preview->rect() - shadowExtend;\n\n\t\t\tUi::Shadow::paint(\n\t\t\t\tp,\n\t\t\t\touter,\n\t\t\t\tpreview->width(),\n\t\t\t\tst::boxRoundShadow);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(st::windowBg);\n\t\t\tconst auto radius = st::boxRadius;\n\t\t\tp.drawRoundedRect(outer, radius, radius);\n\n\t\t\tif (!view->isNull()) {\n\t\t\t\tauto path = QPainterPath();\n\t\t\t\tpath.addRoundedRect(outer, radius, radius);\n\t\t\t\tp.setClipPath(path);\n\t\t\t\tp.drawImage(outer, view->scaled(\n\t\t\t\t\touter.size() * style::DevicePixelRatio(),\n\t\t\t\t\tQt::KeepAspectRatioByExpanding,\n\t\t\t\t\tQt::SmoothTransformation));\n\t\t\t\tp.setClipping(false);\n\t\t\t\tconst auto paintMarker = [&](const style::icon &icon) {\n\t\t\t\t\ticon.paint(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\touter.x()\n\t\t\t\t\t\t\t+ (outer.width() - icon.width()) / 2,\n\t\t\t\t\t\touter.y()\n\t\t\t\t\t\t\t+ (outer.height() / 2) - icon.height(),\n\t\t\t\t\t\tpreview->width());\n\t\t\t\t};\n\t\t\t\tpaintMarker(st::historyMapPoint);\n\t\t\t\tpaintMarker(st::historyMapPointInner);\n\t\t\t}\n\t\t}, preview->lifetime());\n\t}, [=](not_null<Ui::DropdownMenu*> menu) {\n\t\tmenu->addAction(\n\t\t\ttr::lng_open_link(tr::now),\n\t\t\t[=] { LocationClickHandler(point).onClick({}); },\n\t\t\t&st::menuIconAddress);\n\t\tif (replace) {\n\t\t\tmenu->addAction(\n\t\t\t\ttr::lng_attach_replace(tr::now),\n\t\t\t\treplace,\n\t\t\t\t&st::menuIconReplace);\n\t\t}\n\t\tif (remove) {\n\t\t\tAddRemoveAction(menu, remove);\n\t\t}\n\t});\n}\n\nvoid EditPollPhoto(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PhotoData*> photo,\n\t\tFn<void(Ui::PreparedList)> done) {\n\tconst auto photoMedia = photo->createMediaView();\n\tconst auto large = photoMedia->image(Data::PhotoSize::Large);\n\tif (!large) {\n\t\treturn;\n\t}\n\tconst auto previewWidth = st::sendMediaPreviewSize;\n\tconst auto fileImage = std::make_shared<Image>(*large);\n\tauto callback = [=](const Editor::PhotoModifications &mods) {\n\t\tif (!mods) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto large = photoMedia->image(Data::PhotoSize::Large);\n\t\tif (!large) {\n\t\t\treturn;\n\t\t}\n\t\tauto copy = large->original();\n\t\tauto list = Storage::PrepareMediaFromImage(\n\t\t\tstd::move(copy),\n\t\t\tQByteArray(),\n\t\t\tpreviewWidth);\n\n\t\tusing ImageInfo = Ui::PreparedFileInformation::Image;\n\t\tauto &file = list.files.front();\n\t\tconst auto image = std::get_if<ImageInfo>(\n\t\t\t&file.information->media);\n\t\timage->modifications = mods;\n\t\tStorage::UpdateImageDetails(\n\t\t\tfile,\n\t\t\tpreviewWidth,\n\t\t\tPhotoSideLimit(true));\n\t\tStorage::ApplyModifications(list);\n\t\tdone(std::move(list));\n\t};\n\tconst auto parent = controller->content().get();\n\tauto editor = base::make_unique_q<Editor::PhotoEditor>(\n\t\tparent,\n\t\t&controller->window(),\n\t\tfileImage,\n\t\tEditor::PhotoModifications());\n\tconst auto raw = editor.get();\n\tauto layer = std::make_unique<Editor::LayerWidget>(\n\t\tparent,\n\t\tstd::move(editor));\n\tEditor::InitEditorLayer(layer.get(), raw, std::move(callback));\n\tcontroller->showLayer(\n\t\tstd::move(layer),\n\t\tUi::LayerOption::KeepOther);\n}\n\nbool ShowPollMediaPreview(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst std::shared_ptr<PollMediaUpload::PollMediaState> &media,\n\t\tPollMediaActions actions) {\n\tif (media->uploading) {\n\t\tactions.remove();\n\t\treturn true;\n\t}\n\tconst auto document = media->media.document;\n\tconst auto photo = media->media.photo;\n\tif (document && document->sticker()) {\n\t\tShowPollStickerPreview(\n\t\t\tcontroller,\n\t\t\tdocument,\n\t\t\tstd::move(actions.chooseSticker),\n\t\t\tstd::move(actions.remove));\n\t\treturn true;\n\t} else if (photo) {\n\t\tShowPollPhotoPreview(\n\t\t\tcontroller,\n\t\t\tphoto,\n\t\t\tstd::move(actions.choosePhotoOrVideo),\n\t\t\t[controller, photo, editPhoto = std::move(actions.editPhoto)] {\n\t\t\t\tEditPollPhoto(\n\t\t\t\t\tcontroller,\n\t\t\t\t\tphoto,\n\t\t\t\t\tstd::move(editPhoto));\n\t\t\t},\n\t\t\tstd::move(actions.remove));\n\t\treturn true;\n\t} else if (document && document->isVideoFile()) {\n\t\tShowPollDocumentPreview(\n\t\t\tcontroller,\n\t\t\tdocument,\n\t\t\tstd::move(actions.choosePhotoOrVideo),\n\t\t\tstd::move(actions.remove));\n\t\treturn true;\n\t} else if (document) {\n\t\tShowPollDocumentPreview(\n\t\t\tcontroller,\n\t\t\tdocument,\n\t\t\tstd::move(actions.chooseDocument),\n\t\t\tstd::move(actions.remove));\n\t\treturn true;\n\t} else if (media->media.geo) {\n\t\tShowPollGeoPreview(\n\t\t\tcontroller,\n\t\t\t*media->media.geo,\n\t\t\tnullptr,\n\t\t\tstd::move(actions.remove));\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbase::unique_qptr<ChatHelpers::TabbedPanel> CreatePollStickerPanel(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller) {\n\tusing Selector = ChatHelpers::TabbedSelector;\n\tusing Descriptor = ChatHelpers::TabbedSelectorDescriptor;\n\n\tauto panel = base::make_unique_q<ChatHelpers::TabbedPanel>(\n\t\tparent,\n\t\tcontroller,\n\t\tobject_ptr<Selector>(\n\t\t\tnullptr,\n\t\t\tDescriptor{\n\t\t\t\t.show = controller->uiShow(),\n\t\t\t\t.st = st::backgroundEmojiPan,\n\t\t\t\t.level = Window::GifPauseReason::Layer,\n\t\t\t\t.mode = Selector::Mode::StickersOnly,\n\t\t\t\t.features = {\n\t\t\t\t\t.megagroupSet = false,\n\t\t\t\t\t.stickersSettings = false,\n\t\t\t\t\t.openStickerSets = false,\n\t\t\t\t},\n\t\t\t}));\n\tpanel->setDesiredHeightValues(\n\t\t0.,\n\t\tst::emojiPanMinHeight,\n\t\tst::emojiPanMinHeight);\n\tpanel->hide();\n\treturn panel;\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n\nnamespace Data {\nclass LocationPoint;\n} // namespace Data\n\nnamespace PollMediaUpload {\nstruct PollMediaState;\n} // namespace PollMediaUpload\n\nstruct PollData;\nclass DocumentData;\nclass PhotoData;\n\nnamespace ChatHelpers {\nclass TabbedPanel;\n} // namespace ChatHelpers\n\nnamespace Ui {\nclass DropdownMenu;\nstruct PreparedList;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace HistoryView {\n\nstruct PollMediaActions {\n\tFn<void()> choosePhotoOrVideo;\n\tFn<void()> chooseDocument;\n\tFn<void()> chooseSticker;\n\tFn<void(Ui::PreparedList)> editPhoto;\n\tFn<void()> remove;\n};\n\nvoid FillPollAnswerMenu(\n\tnot_null<Ui::DropdownMenu*> menu,\n\tnot_null<PollData*> poll,\n\tconst QByteArray &option,\n\tnot_null<DocumentData*> document,\n\tFullMsgId itemId,\n\tnot_null<Window::SessionController*> controller);\n\nvoid ShowPollStickerPreview(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<DocumentData*> document,\n\tFn<void()> replace,\n\tFn<void()> remove);\n\nvoid ShowPollPhotoPreview(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PhotoData*> photo,\n\tFn<void()> replace,\n\tFn<void()> edit,\n\tFn<void()> remove);\n\nvoid ShowPollDocumentPreview(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<DocumentData*> document,\n\tFn<void()> replace,\n\tFn<void()> remove);\n\nvoid ShowPollGeoPreview(\n\tnot_null<Window::SessionController*> controller,\n\tData::LocationPoint point,\n\tFn<void()> replace = nullptr,\n\tFn<void()> remove = nullptr);\n\nvoid EditPollPhoto(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PhotoData*> photo,\n\tFn<void(Ui::PreparedList)> done);\n\n[[nodiscard]] bool ShowPollMediaPreview(\n\tnot_null<Window::SessionController*> controller,\n\tconst std::shared_ptr<PollMediaUpload::PollMediaState> &media,\n\tPollMediaActions actions);\n\n[[nodiscard]] base::unique_qptr<ChatHelpers::TabbedPanel>\nCreatePollStickerPanel(\n\tnot_null<QWidget*> parent,\n\tnot_null<Window::SessionController*> controller);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/reactions/history_view_reactions.h\"\n\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_message.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_group_call_bar.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"lang/lang_tag.h\"\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/reaction_fly_animation.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/power_saving.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace HistoryView::Reactions {\nnamespace {\n\nconstexpr auto kInNonChosenOpacity = 0.12;\nconstexpr auto kOutNonChosenOpacity = 0.18;\nconstexpr auto kMaxRecentUserpics = 3;\nconstexpr auto kMaxNicePerRow = 5;\n\n[[nodiscard]] QColor AdaptChosenServiceFg(QColor serviceBg) {\n\tserviceBg.setAlpha(std::max(serviceBg.alpha(), 192));\n\treturn serviceBg;\n}\n\nvoid PaintTagShape(QPainter &p, QSizeF size, const QColor &color) {\n\tconst auto arrow = st::reactionInlineTagArrow;\n\tconst auto rradius = st::reactionInlineTagRightRadius * 1.;\n\tconst auto radius = st::reactionInlineTagLeftRadius - rradius;\n\tauto pen = QPen(color);\n\tpen.setWidthF(rradius * 2.);\n\tpen.setJoinStyle(Qt::RoundJoin);\n\tconst auto rect = QRectF(QPointF(), size).marginsRemoved(\n\t\t{ rradius, rradius, rradius, rradius });\n\tconst auto right = rect.x() + rect.width();\n\tconst auto bottom = rect.y() + rect.height();\n\tauto path = QPainterPath();\n\tpath.moveTo(rect.x() + radius, rect.y());\n\tpath.lineTo(right - arrow, rect.y());\n\tpath.lineTo(right, rect.y() + rect.height() / 2);\n\tpath.lineTo(right - arrow, bottom);\n\tpath.lineTo(rect.x() + radius, bottom);\n\tpath.arcTo(\n\t\tQRectF(rect.x(), bottom - radius * 2, radius * 2, radius * 2),\n\t\t270,\n\t\t-90);\n\tpath.lineTo(rect.x(), rect.y() + radius);\n\tpath.arcTo(\n\t\tQRectF(rect.x(), rect.y(), radius * 2, radius * 2),\n\t\t180,\n\t\t-90);\n\tpath.closeSubpath();\n\tp.setPen(pen);\n\tp.drawPath(path);\n}\n\n} // namespace\n\nstruct InlineList::Button {\n\tQRect geometry;\n\tmutable std::unique_ptr<Ui::ReactionFlyAnimation> animation;\n\tmutable QImage image;\n\tmutable ClickHandlerPtr link;\n\tmutable std::unique_ptr<Ui::Text::CustomEmoji> custom;\n\tstd::unique_ptr<Userpics> userpics;\n\tReactionId id;\n\tQString text;\n\tint textWidth = 0;\n\tint count = 0;\n\tbool chosen = false;\n\tbool paid = false;\n\tbool tag = false;\n};\n\nstruct InlineList::RippleEffect : Ui::RippleAnimation {\n\tRippleEffect(\n\t\tconst style::RippleAnimation &st,\n\t\tQImage mask,\n\t\tFn<void()> update,\n\t\tReactionId buttonId,\n\t\tint width)\n\t: RippleAnimation(st, std::move(mask), std::move(update))\n\t, buttonId(std::move(buttonId))\n\t, width(width) {\n\t}\n\n\tReactionId buttonId;\n\tint width = 0;\n};\n\nInlineList::InlineList(\n\tnot_null<::Data::Reactions*> owner,\n\tFn<ClickHandlerPtr(ReactionId)> handlerFactory,\n\tFn<void()> customEmojiRepaint,\n\tData &&data)\n: _owner(owner)\n, _handlerFactory(std::move(handlerFactory))\n, _customEmojiRepaint(std::move(customEmojiRepaint))\n, _data(std::move(data)) {\n\tlayout();\n}\n\nInlineList::~InlineList() = default;\n\nvoid InlineList::update(Data &&data, int availableWidth) {\n\t_data = std::move(data);\n\tlayout();\n\tif (width() > 0) {\n\t\tresizeGetHeight(std::min(maxWidth(), availableWidth));\n\t}\n}\n\nvoid InlineList::updateSkipBlock(int width, int height) {\n\t_skipBlock = { width, height };\n}\n\nvoid InlineList::removeSkipBlock() {\n\t_skipBlock = {};\n}\n\nbool InlineList::areTags() const {\n\treturn _data.flags & Data::Flag::Tags;\n}\n\nstd::vector<ReactionId> InlineList::computeTagsList() const {\n\tif (!areTags()) {\n\t\treturn {};\n\t}\n\treturn _buttons | ranges::views::transform(\n\t\t&Button::id\n\t) | ranges::to_vector;\n}\n\nbool InlineList::hasCustomEmoji() const {\n\treturn _hasCustomEmoji;\n}\n\nvoid InlineList::unloadCustomEmoji() {\n\tif (!hasCustomEmoji()) {\n\t\treturn;\n\t}\n\tfor (const auto &button : _buttons) {\n\t\tif (const auto custom = button.custom.get()) {\n\t\t\tcustom->unload();\n\t\t}\n\t}\n\t_customCache = QImage();\n}\n\nvoid InlineList::layout() {\n\tlayoutButtons();\n\tinitDimensions();\n}\n\nvoid InlineList::layoutButtons() {\n\tif (_data.reactions.empty()) {\n\t\t_buttons.clear();\n\t\treturn;\n\t}\n\tauto sorted = ranges::views::all(\n\t\t_data.reactions\n\t) | ranges::views::transform([](const MessageReaction &reaction) {\n\t\treturn not_null{ &reaction };\n\t}) | ranges::to_vector;\n\tconst auto tags = areTags();\n\tif (!tags) {\n\t\tconst auto &list = _owner->list(::Data::Reactions::Type::All);\n\t\tranges::sort(sorted, [&](\n\t\t\t\tnot_null<const MessageReaction*> a,\n\t\t\t\tnot_null<const MessageReaction*> b) {\n\t\t\tconst auto acount = a->count - (a->my ? 1 : 0);\n\t\t\tconst auto bcount = b->count - (b->my ? 1 : 0);\n\t\t\tif (b->id.paid()) {\n\t\t\t\treturn false;\n\t\t\t} else if (a->id.paid()) {\n\t\t\t\treturn true;\n\t\t\t} else if (acount > bcount) {\n\t\t\t\treturn true;\n\t\t\t} else if (acount < bcount) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn ranges::find(list, a->id, &::Data::Reaction::id)\n\t\t\t\t< ranges::find(list, b->id, &::Data::Reaction::id);\n\t\t});\n\t}\n\n\t_hasCustomEmoji = false;\n\tauto buttons = std::vector<Button>();\n\tbuttons.reserve(sorted.size());\n\tfor (const auto &reaction : sorted) {\n\t\tconst auto &id = reaction->id;\n\t\tconst auto i = ranges::find(_buttons, id, &Button::id);\n\t\tbuttons.push_back((i != end(_buttons))\n\t\t\t? std::move(*i)\n\t\t\t: prepareButtonWithId(id));\n\t\tif (tags) {\n\t\t\tsetButtonTag(buttons.back(), _owner->myTagTitle(id));\n\t\t} else if (const auto j = _data.recent.find(id)\n\t\t\t; j != end(_data.recent) && !j->second.empty()) {\n\t\t\tsetButtonUserpics(buttons.back(), j->second);\n\t\t} else {\n\t\t\tsetButtonCount(buttons.back(), reaction->count);\n\t\t}\n\t\tbuttons.back().chosen = reaction->my;\n\t\tif (id.custom()) {\n\t\t\t_hasCustomEmoji = true;\n\t\t}\n\t}\n\t_buttons = std::move(buttons);\n}\n\nInlineList::Dimension InlineList::countDimension(int width) const {\n\tusing Flag = InlineListData::Flag;\n\tconst auto inBubble = (_data.flags & Flag::InBubble);\n\tconst auto centered = (_data.flags & Flag::Centered);\n\tconst auto useWidth = centered\n\t\t? std::min(width, st::chatGiveawayWidth)\n\t\t: width;\n\tconst auto left = inBubble\n\t\t? st::reactionInlineInBubbleLeft\n\t\t: centered\n\t\t? ((width - useWidth) / 2)\n\t\t: 0;\n\treturn { .left = left, .width = useWidth };\n}\n\nInlineList::Button InlineList::prepareButtonWithId(const ReactionId &id) {\n\tauto result = Button{ .id = id, .paid = id.paid()};\n\tif (const auto customId = id.custom()) {\n\t\tresult.custom = _owner->owner().customEmojiManager().create(\n\t\t\tcustomId,\n\t\t\t_customEmojiRepaint);\n\t} else {\n\t\t_owner->preloadReactionImageFor(id);\n\t}\n\treturn result;\n}\n\nvoid InlineList::setButtonTag(Button &button, const QString &title) {\n\tif (button.tag && button.text == title) {\n\t\treturn;\n\t}\n\tbutton.userpics = nullptr;\n\tbutton.count = 0;\n\tbutton.tag = true;\n\tbutton.text = title;\n\tbutton.textWidth = st::reactionInlineTagFont->width(button.text);\n}\n\nvoid InlineList::setButtonCount(Button &button, int count) {\n\tif (!button.tag && button.count == count && !button.userpics) {\n\t\treturn;\n\t}\n\tbutton.userpics = nullptr;\n\tbutton.count = count;\n\tbutton.tag = false;\n\tif (count == 0) {\n\t\tbutton.text = QString();\n\t\tbutton.textWidth = 0;\n\t} else {\n\t\tbutton.text = Lang::FormatCountToShort(count).string;\n\t\tbutton.textWidth = st::semiboldFont->width(button.text);\n\t}\n}\n\nvoid InlineList::setButtonUserpics(\n\t\tButton &button,\n\t\tconst std::vector<not_null<PeerData*>> &peers) {\n\tbutton.tag = false;\n\tif (!button.userpics) {\n\t\tbutton.userpics = std::make_unique<Userpics>();\n\t}\n\tconst auto count = button.count = int(peers.size());\n\tauto &list = button.userpics->list;\n\tconst auto regenerate = [&] {\n\t\tif (list.size() != count) {\n\t\t\treturn true;\n\t\t}\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tif (peers[i] != list[i].peer) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}();\n\tif (!regenerate) {\n\t\treturn;\n\t}\n\tauto generated = std::vector<UserpicInRow>();\n\tgenerated.reserve(count);\n\tfor (auto i = 0; i != count; ++i) {\n\t\tif (i == list.size()) {\n\t\t\tlist.push_back(UserpicInRow{\n\t\t\t\tpeers[i]\n\t\t\t});\n\t\t} else if (list[i].peer != peers[i]) {\n\t\t\tlist[i].peer = peers[i];\n\t\t}\n\t}\n\twhile (list.size() > count) {\n\t\tlist.pop_back();\n\t}\n\tbutton.userpics->image = QImage();\n}\n\nQSize InlineList::countOptimalSize() {\n\tif (_buttons.empty()) {\n\t\treturn _skipBlock;\n\t}\n\tconst auto left = countDimension(width()).left;\n\tauto x = left;\n\tconst auto between = st::reactionInlineBetween;\n\tconst auto padding = st::reactionInlinePadding;\n\tconst auto size = st::reactionInlineSize;\n\tconst auto widthBaseTag = padding.left()\n\t\t+ size\n\t\t+ st::reactionInlineTagSkip\n\t\t+ padding.right();\n\tconst auto widthBaseCount = padding.left()\n\t\t+ size\n\t\t+ st::reactionInlineSkip\n\t\t+ padding.right();\n\tconst auto widthBaseUserpics = padding.left()\n\t\t+ size\n\t\t+ st::reactionInlineUserpicsPadding.left()\n\t\t+ st::reactionInlineUserpicsPadding.right();\n\tconst auto userpicsWidth = [](const Button &button) {\n\t\tconst auto count = int(button.userpics->list.size());\n\t\tconst auto single = st::reactionInlineUserpics.size;\n\t\tconst auto shift = st::reactionInlineUserpics.shift;\n\t\tconst auto width = single + (count - 1) * (single - shift);\n\t\treturn width;\n\t};\n\tconst auto height = padding.top() + size + padding.bottom();\n\tfor (auto &button : _buttons) {\n\t\tconst auto width = button.tag\n\t\t\t? (widthBaseTag\n\t\t\t\t+ button.textWidth\n\t\t\t\t+ (button.textWidth ? st::reactionInlineSkip : 0))\n\t\t\t: button.userpics\n\t\t\t? (widthBaseUserpics + userpicsWidth(button))\n\t\t\t: button.count == 0\n\t\t\t? (rect::m::sum::h(padding) + size - st::reactionInlineEmptySkip)\n\t\t\t: (widthBaseCount + button.textWidth);\n\t\tbutton.geometry.setSize({ width, height });\n\t\tx += width + between;\n\t}\n\treturn QSize(\n\t\tx - between + _skipBlock.width(),\n\t\tstd::max(height, _skipBlock.height()));\n}\n\nQSize InlineList::countCurrentSize(int newWidth) {\n\t_data.flags &= ~Data::Flag::Flipped;\n\tif (_buttons.empty()) {\n\t\treturn optimalSize();\n\t}\n\tusing Flag = InlineListData::Flag;\n\tconst auto between = st::reactionInlineBetween;\n\tconst auto dimension = countDimension(newWidth);\n\tconst auto left = dimension.left;\n\tconst auto width = dimension.width;\n\tconst auto centered = (_data.flags & Flag::Centered);\n\tauto x = left;\n\tauto y = 0;\n\tconst auto recenter = [&](int beforeIndex) {\n\t\tconst auto added = centered ? (left + width + between - x) : 0;\n\t\tif (added <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto shift = added / 2;\n\t\tfor (auto j = beforeIndex; j != 0;) {\n\t\t\tauto &button = _buttons[--j];\n\t\t\tif (button.geometry.y() != y) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tbutton.geometry.translate(shift, 0);\n\t\t}\n\t};\n\tfor (auto i = 0, count = int(_buttons.size()); i != count; ++i) {\n\t\tauto &button = _buttons[i];\n\t\tconst auto size = button.geometry.size();\n\t\tif (x > left && x + size.width() > left + width) {\n\t\t\trecenter(i);\n\t\t\tx = left;\n\t\t\ty += size.height() + between;\n\t\t}\n\t\tbutton.geometry = QRect(QPoint(x, y), size);\n\t\tx += size.width() + between;\n\t}\n\trecenter(_buttons.size());\n\tconst auto &last = _buttons.back().geometry;\n\tconst auto height = y + last.height();\n\tconst auto right = last.x() + last.width() + _skipBlock.width();\n\tconst auto add = (right > width) ? _skipBlock.height() : 0;\n\treturn { newWidth, height + add };\n}\n\nint InlineList::countNiceWidth() const {\n\tconst auto count = _data.reactions.size();\n\tconst auto rows = (count + kMaxNicePerRow - 1) / kMaxNicePerRow;\n\tconst auto columns = (count + rows - 1) / rows;\n\tconst auto between = st::reactionInlineBetween;\n\tauto result = 0;\n\tauto inrow = 0;\n\tauto x = 0;\n\tfor (auto &button : _buttons) {\n\t\tif (inrow++ >= columns) {\n\t\t\tx = 0;\n\t\t\tinrow = 0;\n\t\t}\n\t\tx += button.geometry.width() + between;\n\t\taccumulate_max(result, x - between);\n\t}\n\treturn result;\n}\n\nvoid InlineList::flipToRight() {\n\t_data.flags |= Data::Flag::Flipped;\n\tfor (auto &button : _buttons) {\n\t\tbutton.geometry.moveLeft(\n\t\t\twidth() - button.geometry.x() - button.geometry.width());\n\t}\n}\n\nint InlineList::placeAndResizeGetHeight(QRect available) {\n\tconst auto result = resizeGetHeight(available.width());\n\tfor (auto &button : _buttons) {\n\t\tbutton.geometry.translate(available.x(), 0);\n\t}\n\treturn result;\n}\n\nvoid InlineList::paint(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint outerWidth,\n\t\tconst QRect &clip) const {\n\tstruct SingleAnimation {\n\t\tnot_null<Ui::ReactionFlyAnimation*> animation;\n\t\tQColor textColor;\n\t\tQRect target;\n\t};\n\tstd::vector<SingleAnimation> animations;\n\n\tauto finished = std::vector<std::unique_ptr<Ui::ReactionFlyAnimation>>();\n\tconst auto st = context.st;\n\tconst auto stm = context.messageStyle();\n\tconst auto padding = st::reactionInlinePadding;\n\tconst auto size = st::reactionInlineSize;\n\tconst auto skip = (size - st::reactionInlineImage) / 2;\n\tconst auto tags = areTags();\n\tconst auto inbubble = (_data.flags & Data::Flag::InBubble);\n\tconst auto flipped = (_data.flags & Data::Flag::Flipped);\n\tp.setFont(tags ? st::reactionInlineTagFont : st::semiboldFont);\n\tfor (const auto &button : _buttons) {\n\t\tif (context.reactionInfo\n\t\t\t&& button.animation\n\t\t\t&& button.animation->finished()) {\n\t\t\t// Let the animation (and its custom emoji) live while painting.\n\t\t\tfinished.push_back(std::move(button.animation));\n\t\t}\n\t\tconst auto animating = (button.animation != nullptr);\n\t\tconst auto &geometry = button.geometry;\n\t\tconst auto mine = button.chosen;\n\t\tconst auto withoutMine = button.count - (mine ? 1 : 0);\n\t\tconst auto skipImage = animating\n\t\t\t&& (withoutMine < 1 || !button.animation->flying());\n\t\tconst auto bubbleProgress = skipImage\n\t\t\t? button.animation->flyingProgress()\n\t\t\t: 1.;\n\t\tconst auto bubbleReady = (bubbleProgress == 1.);\n\t\tconst auto bubbleSkip = anim::interpolate(\n\t\t\tgeometry.height() - geometry.width(),\n\t\t\t0,\n\t\t\tbubbleProgress);\n\t\tconst auto inner = geometry.marginsRemoved(padding);\n\t\tconst auto chosen = mine\n\t\t\t&& (!animating || !button.animation->flying() || skipImage);\n\t\tif (bubbleProgress > 0.) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tauto opacity = 1.;\n\t\t\tauto color = QColor();\n\t\t\tif (inbubble) {\n\t\t\t\tif (!chosen) {\n\t\t\t\t\topacity = bubbleProgress * (context.outbg\n\t\t\t\t\t\t? kOutNonChosenOpacity\n\t\t\t\t\t\t: kInNonChosenOpacity);\n\t\t\t\t} else if (!bubbleReady) {\n\t\t\t\t\topacity = bubbleProgress;\n\t\t\t\t}\n\t\t\t\tcolor = button.paid\n\t\t\t\t\t? st->creditsBg3()->c\n\t\t\t\t\t: stm->msgFileBg->c;\n\t\t\t} else {\n\t\t\t\tif (!bubbleReady) {\n\t\t\t\t\topacity = bubbleProgress;\n\t\t\t\t}\n\t\t\t\tcolor = (!chosen\n\t\t\t\t\t? st->msgServiceBg()\n\t\t\t\t\t: button.paid\n\t\t\t\t\t? st->creditsBg2()\n\t\t\t\t\t: st->msgServiceFg())->c;\n\t\t\t}\n\n\t\t\tconst auto fill = geometry.marginsAdded({\n\t\t\t\tflipped ? bubbleSkip : 0,\n\t\t\t\t0,\n\t\t\t\tflipped ? 0 : bubbleSkip,\n\t\t\t\t0,\n\t\t\t});\n\t\t\tpaintSingleBg(p, fill, color, opacity);\n\t\t\tif (inbubble && !chosen) {\n\t\t\t\tp.setOpacity(bubbleProgress);\n\t\t\t}\n\t\t}\n\t\tif (!button.custom && button.image.isNull()) {\n\t\t\tbutton.image = _owner->resolveReactionImageFor(button.id);\n\t\t}\n\n\t\tconst auto textFg = !inbubble\n\t\t\t? (chosen\n\t\t\t\t? QPen(AdaptChosenServiceFg(st->msgServiceBg()->c))\n\t\t\t\t: st->msgServiceFg())\n\t\t\t: !chosen\n\t\t\t? (button.paid ? st->creditsFg() : stm->msgServiceFg)\n\t\t\t: context.outbg\n\t\t\t? (context.selected()\n\t\t\t\t? st->historyFileOutIconFgSelected()\n\t\t\t\t: st->historyFileOutIconFg())\n\t\t\t: (context.selected()\n\t\t\t\t? st->historyFileInIconFgSelected()\n\t\t\t\t: st->historyFileInIconFg());\n\t\tif (_ripple && _ripple->buttonId == button.id) {\n\t\t\tif (!bubbleReady || _ripple->width != geometry.width()) {\n\t\t\t\t_ripple.reset();\n\t\t\t} else {\n\t\t\t\tconst auto savedOpacity = p.opacity();\n\t\t\t\tp.setOpacity(1.);\n\t\t\t\tauto rippleColor = textFg.color();\n\t\t\t\trippleColor.setAlpha(25);\n\t\t\t\t_ripple->paint(\n\t\t\t\t\tp,\n\t\t\t\t\tgeometry.x(),\n\t\t\t\t\tgeometry.y(),\n\t\t\t\t\touterWidth,\n\t\t\t\t\t&rippleColor);\n\t\t\t\tif (_ripple->empty()) {\n\t\t\t\t\t_ripple.reset();\n\t\t\t\t}\n\t\t\t\tp.setOpacity(savedOpacity);\n\t\t\t}\n\t\t}\n\t\tconst auto image = QRect(\n\t\t\tinner.topLeft() + QPoint(skip, skip),\n\t\t\tQSize(st::reactionInlineImage, st::reactionInlineImage));\n\t\tif (!skipImage) {\n\t\t\tif (const auto custom = button.custom.get()) {\n\t\t\t\tpaintCustomFrame(\n\t\t\t\t\tp,\n\t\t\t\t\tcustom,\n\t\t\t\t\tinner.topLeft(),\n\t\t\t\t\tcontext,\n\t\t\t\t\ttextFg.color());\n\t\t\t} else if (!button.image.isNull()) {\n\t\t\t\tp.drawImage(image.topLeft(), button.image);\n\t\t\t}\n\t\t}\n\t\tif (animating) {\n\t\t\tanimations.push_back({\n\t\t\t\t.animation = button.animation.get(),\n\t\t\t\t.textColor = textFg.color(),\n\t\t\t\t.target = image,\n\t\t\t});\n\t\t}\n\t\tif ((tags && !button.textWidth) || bubbleProgress == 0.) {\n\t\t\tp.setOpacity(1.);\n\t\t\tcontinue;\n\t\t}\n\t\tresolveUserpicsImage(button);\n\t\tconst auto left = inner.x() + (flipped ? 0 : bubbleSkip);\n\t\tif (button.userpics) {\n\t\t\tp.drawImage(\n\t\t\t\tleft + size + st::reactionInlineUserpicsPadding.left(),\n\t\t\t\tgeometry.y() + st::reactionInlineUserpicsPadding.top(),\n\t\t\t\tbutton.userpics->image);\n\t\t} else {\n\t\t\tp.setPen(textFg);\n\t\t\tconst auto textLeft = tags\n\t\t\t\t? (left\n\t\t\t\t\t- padding.left()\n\t\t\t\t\t+ st::reactionInlineTagNamePosition.x())\n\t\t\t\t: (left + size + st::reactionInlineSkip);\n\t\t\tconst auto textTop = geometry.y()\n\t\t\t\t+ (tags\n\t\t\t\t\t? st::reactionInlineTagNamePosition.y()\n\t\t\t\t\t: ((geometry.height() - st::semiboldFont->height) / 2));\n\t\t\tconst auto font = tags\n\t\t\t\t? st::reactionInlineTagFont\n\t\t\t\t: st::semiboldFont;\n\t\t\tp.drawText(textLeft, textTop + font->ascent, button.text);\n\t\t}\n\t\tif (!bubbleReady) {\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t}\n\tif (!animations.empty()) {\n\t\tconst auto now = context.now;\n\t\tcontext.reactionInfo->effectPaint = [\n\t\t\tnow,\n\t\t\tlist = std::move(animations)\n\t\t](QPainter &p) {\n\t\t\tauto result = QRect();\n\t\t\tfor (const auto &single : list) {\n\t\t\t\tconst auto area = single.animation->paintGetArea(\n\t\t\t\t\tp,\n\t\t\t\t\tQPoint(),\n\t\t\t\t\tsingle.target,\n\t\t\t\t\tsingle.textColor,\n\t\t\t\t\tQRect(), // Clip, for emoji status.\n\t\t\t\t\tnow);\n\t\t\t\tresult = result.isEmpty() ? area : result.united(area);\n\t\t\t}\n\t\t\treturn result;\n\t\t};\n\t}\n}\n\nfloat64 InlineList::TagDotAlpha() {\n\treturn 0.6;\n}\n\nQImage InlineList::PrepareTagBg(QColor tagBg, QColor dotBg) {\n\tconst auto padding = st::reactionInlinePadding;\n\tconst auto size = st::reactionInlineSize;\n\tconst auto width = padding.left()\n\t\t+ size\n\t\t+ st::reactionInlineTagSkip\n\t\t+ padding.right();\n\tconst auto height = padding.top() + size + padding.bottom();\n\tconst auto ratio = style::DevicePixelRatio();\n\n\tauto result = QImage(\n\t\tQSize(width, height) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\n\tresult.fill(Qt::transparent);\n\tauto p = QPainter(&result);\n\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\tp.setBrush(tagBg);\n\tPaintTagShape(p, QSizeF(width, height), tagBg);\n\n\tif (dotBg.alpha() > 0) {\n\t\tconst auto rradius = st::reactionInlineTagRightRadius * 1.;\n\t\tconst auto rect = QRectF(0, 0, width, height).marginsRemoved(\n\t\t\t{ rradius, rradius, rradius, rradius });\n\t\tconst auto dsize = st::reactionInlineTagDot;\n\t\tconst auto dot = QRectF(\n\t\t\trect.x() + rect.width() - st::reactionInlineTagDotSkip - dsize,\n\t\t\trect.y() + (rect.height() - dsize) / 2.,\n\t\t\tdsize,\n\t\t\tdsize);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(dotBg);\n\t\tp.drawEllipse(dot);\n\t}\n\n\tp.end();\n\n\treturn result;\n}\n\nvoid InlineList::validateTagBg(const QColor &color) const {\n\tif (!_tagBg.isNull() && _tagBgColor == color) {\n\t\treturn;\n\t}\n\t_tagBgColor = color;\n\tauto dot = color;\n\tdot.setAlphaF(dot.alphaF() * TagDotAlpha());\n\t_tagBg = PrepareTagBg(color, anim::with_alpha(color, TagDotAlpha()));\n}\n\nvoid InlineList::paintSingleBg(\n\t\tPainter &p,\n\t\tconst QRect &fill,\n\t\tconst QColor &color,\n\t\tfloat64 opacity) const {\n\tp.setOpacity(opacity);\n\tif (!areTags()) {\n\t\tconst auto radius = fill.height() / 2.;\n\t\tp.setBrush(color);\n\t\tp.drawRoundedRect(fill, radius, radius);\n\t\treturn;\n\t}\n\tvalidateTagBg(color);\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto left = st::reactionInlineTagLeftRadius;\n\tconst auto right = (_tagBg.width() / ratio) - left;\n\tAssert(right > 0);\n\tconst auto useLeft = std::min(fill.width(), left);\n\tp.drawImage(\n\t\tQRect(fill.x(), fill.y(), useLeft, fill.height()),\n\t\t_tagBg,\n\t\tQRect(0, 0, useLeft * ratio, _tagBg.height()));\n\tconst auto middle = fill.width() - left - right;\n\tif (middle > 0) {\n\t\tp.fillRect(fill.x() + left, fill.y(), middle, fill.height(), color);\n\t}\n\tif (const auto useRight = fill.width() - left; useRight > 0) {\n\t\tp.drawImage(\n\t\t\tQRect(\n\t\t\t\tfill.x() + fill.width() - useRight,\n\t\t\t\tfill.y(),\n\t\t\t\tuseRight,\n\t\t\t\tfill.height()),\n\t\t\t_tagBg,\n\t\t\tQRect(_tagBg.width() - useRight * ratio,\n\t\t\t\t0,\n\t\t\t\tuseRight * ratio,\n\t\t\t\t_tagBg.height()));\n\t}\n}\n\nbool InlineList::getState(\n\t\tQPoint point,\n\t\tnot_null<TextState*> outResult) const {\n\tconst auto dimension = countDimension(width());\n\tconst auto left = dimension.left;\n\tif (!QRect(left, 0, dimension.width, height()).contains(point)) {\n\t\treturn false;\n\t}\n\tfor (const auto &button : _buttons) {\n\t\tif (button.geometry.contains(point)) {\n\t\t\tif (!button.link) {\n\t\t\t\tbutton.link = _handlerFactory(button.id);\n\t\t\t\tbutton.link->setProperty(\n\t\t\t\t\tkReactionsCountEmojiProperty,\n\t\t\t\t\tQVariant::fromValue(button.id));\n\t\t\t\t_owner->preloadAnimationsFor(button.id);\n\t\t\t}\n\t\t\t_lastPoint = point - button.geometry.topLeft();\n\t\t\t_lastPointButton = button.id;\n\t\t\toutResult->link = button.link;\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid InlineList::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed,\n\t\tFn<void()> repaint) {\n\tif (pressed) {\n\t\tconst auto id = ReactionIdOfLink(handler);\n\t\tif (id.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto i = ranges::find(_buttons, id, &Button::id);\n\t\tif (i == end(_buttons)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto &geometry = i->geometry;\n\t\tif (!_ripple\n\t\t\t|| _ripple->buttonId != id\n\t\t\t|| _ripple->width != geometry.width()) {\n\t\t\tconst auto mask = [&] {\n\t\t\t\tif (areTags()) {\n\t\t\t\t\tconst auto s = geometry.size();\n\t\t\t\t\treturn Ui::RippleAnimation::MaskByDrawer(\n\t\t\t\t\t\ts,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t[&](QPainter &p) {\n\t\t\t\t\t\t\tPaintTagShape(\n\t\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\t\tQSizeF(s.width(), s.height()),\n\t\t\t\t\t\t\t\tQColor(255, 255, 255));\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn Ui::RippleAnimation::RoundRectMask(\n\t\t\t\t\tgeometry.size(),\n\t\t\t\t\tgeometry.height() / 2);\n\t\t\t}();\n\t\t\t_ripple = std::make_unique<RippleEffect>(\n\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\tstd::move(mask),\n\t\t\t\tstd::move(repaint),\n\t\t\t\tid,\n\t\t\t\tgeometry.width());\n\t\t}\n\t\tconst auto point = (_lastPointButton == id)\n\t\t\t? _lastPoint\n\t\t\t: QPoint(geometry.width() / 2, geometry.height() / 2);\n\t\t_ripple->add(point);\n\t} else if (_ripple) {\n\t\t_ripple->lastStop();\n\t}\n}\n\nvoid InlineList::animate(\n\t\tUi::ReactionFlyAnimationArgs &&args,\n\t\tFn<void()> repaint) {\n\tconst auto i = ranges::find(_buttons, args.id, &Button::id);\n\tif (i == end(_buttons)) {\n\t\treturn;\n\t}\n\ti->animation = std::make_unique<Ui::ReactionFlyAnimation>(\n\t\t_owner,\n\t\tstd::move(args),\n\t\tstd::move(repaint),\n\t\tst::reactionInlineImage);\n}\n\nvoid InlineList::resolveUserpicsImage(const Button &button) const {\n\tconst auto userpics = button.userpics.get();\n\tif (!userpics\n\t\t|| !NeedRegenerateUserpics(userpics->image, userpics->list)) {\n\t\treturn;\n\t}\n\tGenerateUserpicsInRow(\n\t\tuserpics->image,\n\t\tuserpics->list,\n\t\tst::reactionInlineUserpics,\n\t\tkMaxRecentUserpics);\n}\n\nvoid InlineList::paintCustomFrame(\n\t\tPainter &p,\n\t\tnot_null<Ui::Text::CustomEmoji*> emoji,\n\t\tQPoint innerTopLeft,\n\t\tconst PaintContext &context,\n\t\tconst QColor &textColor) const {\n\tif (_customCache.isNull()) {\n\t\tusing namespace Ui::Text;\n\t\tconst auto size = st::emojiSize;\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tconst auto adjusted = AdjustCustomEmojiSize(size);\n\t\t_customCache = QImage(\n\t\t\tQSize(adjusted, adjusted) * factor,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t_customCache.setDevicePixelRatio(factor);\n\t\t_customSkip = (size - adjusted) / 2;\n\t}\n\t_customCache.fill(Qt::transparent);\n\tauto q = QPainter(&_customCache);\n\temoji->paint(q, {\n\t\t.textColor = textColor,\n\t\t.now = context.now,\n\t\t.paused = context.paused || On(PowerSaving::kEmojiChat),\n\t});\n\tq.end();\n\t_customCache = Images::Round(\n\t\tstd::move(_customCache),\n\t\t(Images::Option::RoundLarge\n\t\t\t| Images::Option::RoundSkipTopRight\n\t\t\t| Images::Option::RoundSkipBottomRight));\n\n\tp.drawImage(\n\t\tinnerTopLeft + QPoint(_customSkip, _customSkip),\n\t\t_customCache);\n}\n\nauto InlineList::takeAnimations()\n-> base::flat_map<ReactionId, std::unique_ptr<Ui::ReactionFlyAnimation>> {\n\tauto result = base::flat_map<\n\t\tReactionId,\n\t\tstd::unique_ptr<Ui::ReactionFlyAnimation>>();\n\tfor (auto &button : _buttons) {\n\t\tif (button.animation) {\n\t\t\tresult.emplace(button.id, std::move(button.animation));\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid InlineList::continueAnimations(base::flat_map<\n\t\tReactionId,\n\t\tstd::unique_ptr<Ui::ReactionFlyAnimation>> animations) {\n\tfor (auto &[id, animation] : animations) {\n\t\tconst auto i = ranges::find(_buttons, id, &Button::id);\n\t\tif (i != end(_buttons)) {\n\t\t\ti->animation = std::move(animation);\n\t\t}\n\t}\n}\n\nInlineListData InlineListDataFromMessage(not_null<Element*> view) {\n\tusing Flag = InlineListData::Flag;\n\tconst auto item = view->data();\n\tauto result = InlineListData();\n\tresult.reactions = item->reactionsWithLocal();\n\n\tconst auto shouldAddEmptyPaidButton = [&] {\n\t\tif (view->context() == Context::ChatPreview) {\n\t\t\treturn false;\n\t\t}\n\t\tif (result.reactions.empty()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto hasPaidReaction = ranges::any_of(\n\t\t\tresult.reactions,\n\t\t\t[](const MessageReaction &r) { return r.id.paid(); });\n\t\tif (hasPaidReaction) {\n\t\t\treturn false;\n\t\t}\n\t\tif (const auto channel = item->history()->peer->asChannel()) {\n\t\t\treturn channel->allowedReactions().paidEnabled;\n\t\t} else if (const auto chat = item->history()->peer->asChat()) {\n\t\t\treturn chat->allowedReactions().paidEnabled;\n\t\t}\n\t\treturn false;\n\t}();\n\n\tif (shouldAddEmptyPaidButton) {\n\t\tresult.reactions.insert(\n\t\t\tresult.reactions.begin(),\n\t\t\tMessageReaction{ .id = ReactionId::Paid(), .count = 0 });\n\t}\n\tif (const auto user = item->history()->peer->asUser()) {\n\t\t// Always show userpics, we have all information.\n\t\tresult.recent.reserve(result.reactions.size());\n\t\tconst auto self = user->session().user();\n\t\tfor (const auto &reaction : result.reactions) {\n\t\t\tauto &list = result.recent[reaction.id];\n\t\t\tlist.reserve(reaction.count);\n\t\t\tif (!reaction.my || reaction.count > 1) {\n\t\t\t\tlist.push_back(user);\n\t\t\t}\n\t\t\tif (reaction.my) {\n\t\t\t\tlist.push_back(self);\n\t\t\t}\n\t\t}\n\t} else {\n\t\tconst auto &recent = item->recentReactions();\n\t\tconst auto showUserpics = [&] {\n\t\t\tif (recent.size() != result.reactions.size()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tauto sum = 0;\n\t\t\tfor (const auto &reaction : result.reactions) {\n\t\t\t\tif ((sum += reaction.count) > kMaxRecentUserpics) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst auto i = recent.find(reaction.id);\n\t\t\t\tif (i == end(recent) || reaction.count != i->second.size()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}();\n\t\tif (showUserpics) {\n\t\t\tresult.recent.reserve(recent.size());\n\t\t\tfor (const auto &[id, list] : recent) {\n\t\t\t\tresult.recent.emplace(id).first->second = list\n\t\t\t\t\t| ranges::views::transform(&Data::RecentReaction::peer)\n\t\t\t\t\t| ranges::to_vector;\n\t\t\t}\n\t\t}\n\t}\n\tresult.flags = (view->hasOutLayout() ? Flag::OutLayout : Flag())\n\t\t| (view->embedReactionsInBubble() ? Flag::InBubble : Flag())\n\t\t| (item->reactionsAreTags() ? Flag::Tags : Flag())\n\t\t| (item->isService() ? Flag::Centered : Flag());\n\treturn result;\n}\n\nReactionId ReactionIdOfLink(const ClickHandlerPtr &link) {\n\treturn link\n\t\t? link->property(kReactionsCountEmojiProperty).value<ReactionId>()\n\t\t: ReactionId();\n}\n\nReactionCount ReactionCountOfLink(\n\t\tHistoryItem *item,\n\t\tconst ClickHandlerPtr &link) {\n\tconst auto id = ReactionIdOfLink(link);\n\tif (!item || !id) {\n\t\treturn {};\n\t}\n\tconst auto groups = &item->history()->owner().groups();\n\tif (const auto group = groups->find(item)) {\n\t\titem = group->items.front();\n\t}\n\tconst auto &list = item->reactions();\n\tconst auto i = ranges::find(list, id, &Data::MessageReaction::id);\n\tif (i == end(list) || !i->count) {\n\t\treturn {};\n\t}\n\tconst auto formatted = Lang::FormatCountToShort(i->count);\n\treturn { .count = i->count, .shortened = formatted.shortened };\n}\n\n} // namespace HistoryView::Reactions\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/reactions/history_view_reactions.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/view/history_view_object.h\"\n#include \"data/data_message_reaction_id.h\"\n\nnamespace Data {\nclass Reactions;\n} // namespace Data\n\nnamespace Ui {\nstruct ChatPaintContext;\nstruct ReactionFlyAnimationArgs;\nclass ReactionFlyAnimation;\n} // namespace Ui\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nnamespace HistoryView {\nusing PaintContext = Ui::ChatPaintContext;\nclass Element;\nstruct TextState;\nstruct UserpicInRow;\n} // namespace HistoryView\n\nnamespace HistoryView::Reactions {\n\nusing ::Data::ReactionId;\nusing ::Data::MessageReaction;\n\nstruct InlineListData {\n\tenum class Flag : uchar {\n\t\tInBubble  = 0x01,\n\t\tOutLayout = 0x02,\n\t\tFlipped   = 0x04,\n\t\tTags      = 0x08,\n\t\tCentered  = 0x10,\n\t};\n\tfriend inline constexpr bool is_flag_type(Flag) { return true; };\n\tusing Flags = base::flags<Flag>;\n\n\tstd::vector<MessageReaction> reactions;\n\tbase::flat_map<ReactionId, std::vector<not_null<PeerData*>>> recent;\n\tFlags flags = {};\n};\n\nclass InlineList final : public Object {\npublic:\n\tusing Data = InlineListData;\n\tInlineList(\n\t\tnot_null<::Data::Reactions*> owner,\n\t\tFn<ClickHandlerPtr(ReactionId)> handlerFactory,\n\t\tFn<void()> customEmojiRepaint,\n\t\tData &&data);\n\t~InlineList();\n\n\tvoid update(Data &&data, int availableWidth);\n\tQSize countCurrentSize(int newWidth) override;\n\t[[nodiscard]] int countNiceWidth() const;\n\t[[nodiscard]] int placeAndResizeGetHeight(QRect available);\n\tvoid flipToRight();\n\n\tvoid updateSkipBlock(int width, int height);\n\tvoid removeSkipBlock();\n\n\t[[nodiscard]] bool areTags() const;\n\t[[nodiscard]] std::vector<ReactionId> computeTagsList() const;\n\t[[nodiscard]] bool hasCustomEmoji() const;\n\tvoid unloadCustomEmoji();\n\n\tvoid paint(\n\t\tPainter &p,\n\t\tconst PaintContext &context,\n\t\tint outerWidth,\n\t\tconst QRect &clip) const;\n\t[[nodiscard]] bool getState(\n\t\tQPoint point,\n\t\tnot_null<TextState*> outResult) const;\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &handler,\n\t\tbool pressed,\n\t\tFn<void()> repaint);\n\n\tvoid animate(\n\t\tUi::ReactionFlyAnimationArgs &&args,\n\t\tFn<void()> repaint);\n\t[[nodiscard]] auto takeAnimations()\n\t-> base::flat_map<\n\t\tReactionId,\n\t\tstd::unique_ptr<Ui::ReactionFlyAnimation>>;\n\tvoid continueAnimations(base::flat_map<\n\t\tReactionId,\n\t\tstd::unique_ptr<Ui::ReactionFlyAnimation>> animations);\n\n\t[[nodiscard]] static float64 TagDotAlpha();\n\t[[nodiscard]] static QImage PrepareTagBg(QColor tagBg, QColor dotBg);\n\nprivate:\n\tstruct Dimension {\n\t\tint left = 0;\n\t\tint width = 0;\n\t};\n\tstruct Userpics {\n\t\tQImage image;\n\t\tstd::vector<UserpicInRow> list;\n\t\tbool someNotLoaded = false;\n\t};\n\tstruct Button;\n\tstruct RippleEffect;\n\n\tvoid layout();\n\tvoid layoutButtons();\n\n\tvoid setButtonTag(Button &button, const QString &title);\n\tvoid setButtonCount(Button &button, int count);\n\tvoid setButtonUserpics(\n\t\tButton &button,\n\t\tconst std::vector<not_null<PeerData*>> &peers);\n\t[[nodiscard]] Button prepareButtonWithId(const ReactionId &id);\n\tvoid resolveUserpicsImage(const Button &button) const;\n\tvoid paintCustomFrame(\n\t\tPainter &p,\n\t\tnot_null<Ui::Text::CustomEmoji*> emoji,\n\t\tQPoint innerTopLeft,\n\t\tconst PaintContext &context,\n\t\tconst QColor &textColor) const;\n\tvoid paintSingleBg(\n\t\tPainter &p,\n\t\tconst QRect &fill,\n\t\tconst QColor &color,\n\t\tfloat64 opacity) const;\n\n\tvoid validateTagBg(const QColor &color) const;\n\n\tQSize countOptimalSize() override;\n\t[[nodiscard]] Dimension countDimension(int width) const;\n\n\tconst not_null<::Data::Reactions*> _owner;\n\tconst Fn<ClickHandlerPtr(ReactionId)> _handlerFactory;\n\tconst Fn<void()> _customEmojiRepaint;\n\tData _data;\n\tstd::vector<Button> _buttons;\n\tQSize _skipBlock;\n\tmutable QImage _tagBg;\n\tmutable QColor _tagBgColor;\n\tmutable QImage _customCache;\n\tmutable int _customSkip = 0;\n\tbool _hasCustomEmoji = false;\n\tmutable std::unique_ptr<RippleEffect> _ripple;\n\tmutable QPoint _lastPoint;\n\tmutable ReactionId _lastPointButton;\n\n};\n\n[[nodiscard]] InlineListData InlineListDataFromMessage(\n\tnot_null<Element*> view);\n\n[[nodiscard]] ReactionId ReactionIdOfLink(const ClickHandlerPtr &link);\n\nstruct ReactionCount {\n\tint count = 0;\n\tbool shortened = false;\n};\n[[nodiscard]] ReactionCount ReactionCountOfLink(\n\tHistoryItem *item,\n\tconst ClickHandlerPtr &link);\n\n} // namespace HistoryView::Reactions\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/reactions/history_view_reactions_button.h\"\n\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/ui_utility.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_message_reactions.h\"\n#include \"lang/lang_keys.h\"\n#include \"core/click_handler_types.h\"\n#include \"main/main_session.h\"\n#include \"base/event_filter.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace HistoryView::Reactions {\nnamespace {\n\nconstexpr auto kToggleDuration = crl::time(120);\nconstexpr auto kActivateDuration = crl::time(150);\nconstexpr auto kExpandDuration = crl::time(300);\nconstexpr auto kCollapseDuration = crl::time(250);\nconstexpr auto kButtonShowDelay = crl::time(300);\nconstexpr auto kButtonExpandDelay = crl::time(25);\nconstexpr auto kButtonHideDelay = crl::time(300);\nconstexpr auto kButtonExpandedHideDelay = crl::time(0);\nconstexpr auto kMaxReactionsScrollAtOnce = 2;\nconstexpr auto kRefreshListDelay = crl::time(100);\n\n[[nodiscard]] QPoint LocalPosition(not_null<QWheelEvent*> e) {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)\n\treturn e->position().toPoint();\n#else // Qt >= 6.0\n\treturn e->pos();\n#endif // Qt >= 6.0\n}\n\n[[nodiscard]] QSize CountMaxSizeWithMargins(style::margins margins) {\n\treturn QRect(\n\t\tQPoint(),\n\t\tst::reactionCornerSize\n\t).marginsAdded(margins).size();\n}\n\n[[nodiscard]] QSize CountOuterSize() {\n\treturn CountMaxSizeWithMargins(st::reactionCornerShadow);\n}\n\n} // namespace\n\nButton::Button(\n\tFn<void(QRect)> update,\n\tButtonParameters parameters,\n\tFn<void()> hide)\n: _update(std::move(update))\n, _finalScale(ScaleForState(_state))\n, _collapsed(QPoint(), CountOuterSize())\n, _finalHeight(_collapsed.height())\n, _expandTimer([=] { applyState(State::Inside, _update); })\n, _hideTimer(hide) {\n\tapplyParameters(parameters, nullptr);\n}\n\nButton::~Button() = default;\n\nbool Button::isHidden() const {\n\treturn (_state == State::Hidden) && !_opacityAnimation.animating();\n}\n\nQRect Button::geometry() const {\n\treturn _geometry;\n}\n\nint Button::expandedHeight() const {\n\treturn _expandedHeight;\n}\n\nint Button::scroll() const {\n\treturn _scroll;\n}\n\nint Button::scrollMax() const {\n\treturn _expandedInnerHeight - _expandedHeight;\n}\n\nfloat64 Button::expandAnimationOpacity(float64 expandRatio) const {\n\treturn (_collapseType == CollapseType::Fade)\n\t\t? expandRatio\n\t\t: 1.;\n}\n\nint Button::expandAnimationScroll(float64 expandRatio) const {\n\treturn (_collapseType == CollapseType::Scroll && expandRatio < 1.)\n\t\t? std::clamp(int(base::SafeRound(expandRatio * _scroll)), 0, _scroll)\n\t\t: _scroll;\n}\n\nbool Button::expandUp() const {\n\treturn (_expandDirection == ExpandDirection::Up);\n}\n\nbool Button::consumeWheelEvent(not_null<QWheelEvent*> e) {\n\tconst auto scrollMax = (_expandedInnerHeight - _expandedHeight);\n\tif (_state != State::Inside\n\t\t|| scrollMax <= 0\n\t\t|| !_geometry.contains(LocalPosition(e))) {\n\t\treturn false;\n\t}\n\tconst auto delta = e->angleDelta();\n\tconst auto horizontal = std::abs(delta.x()) > std::abs(delta.y());\n\tif (horizontal) {\n\t\treturn false;\n\t}\n\tconst auto between = st::reactionCornerSkip;\n\tconst auto oneHeight = (st::reactionCornerSize.height() + between);\n\tconst auto max = oneHeight * kMaxReactionsScrollAtOnce;\n\tconst auto shift = std::clamp(\n\t\tdelta.y() * (expandUp() ? 1 : -1),\n\t\t-max,\n\t\tmax);\n\t_scroll = std::clamp(_scroll + shift, 0, scrollMax);\n\t_update(_geometry);\n\te->accept();\n\treturn true;\n}\n\nvoid Button::applyParameters(ButtonParameters parameters) {\n\tapplyParameters(std::move(parameters), _update);\n}\n\nvoid Button::applyParameters(\n\t\tButtonParameters parameters,\n\t\tFn<void(QRect)> update) {\n\tconst auto shift = parameters.center - _collapsed.center();\n\t_collapsed = _collapsed.translated(shift);\n\tupdateGeometry(update);\n\tconst auto inner = _geometry.marginsRemoved(st::reactionCornerShadow);\n\tconst auto active = inner.marginsAdded(\n\t\tst::reactionCornerActiveAreaPadding\n\t).contains(parameters.pointer);\n\tconst auto inside = inner.contains(parameters.pointer)\n\t\t|| (active && (_state == State::Inside));\n\tif (_state != State::Inside && !_heightAnimation.animating()) {\n\t\tupdateExpandDirection(parameters);\n\t}\n\tconst auto delayInside = inside && (_state != State::Inside);\n\tif (!delayInside) {\n\t\t_expandTimer.cancel();\n\t\t_lastGlobalPosition = std::nullopt;\n\t} else {\n\t\tconst auto globalPositionChanged = _lastGlobalPosition\n\t\t\t&& (*_lastGlobalPosition != parameters.globalPointer);\n\t\tif (globalPositionChanged || _state == State::Hidden) {\n\t\t\t_expandTimer.callOnce(kButtonExpandDelay);\n\t\t}\n\t\t_lastGlobalPosition = parameters.globalPointer;\n\t}\n\tconst auto wasInside = (_state == State::Inside);\n\tconst auto state = (inside && !delayInside)\n\t\t? State::Inside\n\t\t: active\n\t\t? State::Active\n\t\t: State::Shown;\n\tapplyState(state, update);\n\tif (parameters.outside && _state == State::Shown) {\n\t\t_hideTimer.callOnce(wasInside\n\t\t\t? kButtonExpandedHideDelay\n\t\t\t: kButtonHideDelay);\n\t} else {\n\t\t_hideTimer.cancel();\n\t}\n}\n\nvoid Button::updateExpandDirection(const ButtonParameters &parameters) {\n\tconst auto maxAddedHeight = (parameters.reactionsCount - 1)\n\t\t* (st::reactionCornerSize.height() + st::reactionCornerSkip)\n\t\t+ (parameters.reactionsCount > 1 ? 2 * st::reactionExpandedSkip : 0);\n\t_expandedInnerHeight = _collapsed.height() + maxAddedHeight;\n\tconst auto addedHeight = std::min(\n\t\tmaxAddedHeight,\n\t\tst::reactionCornerAddedHeightMax);\n\t_expandedHeight = _collapsed.height() + addedHeight;\n\t_scroll = std::clamp(_scroll, 0, scrollMax());\n\tif (parameters.reactionsCount < 2) {\n\t\treturn;\n\t}\n\tconst auto up = (_collapsed.y() - addedHeight >= parameters.visibleTop)\n\t\t|| (_collapsed.y() + _collapsed.height() + addedHeight\n\t\t\t> parameters.visibleBottom);\n\t_expandDirection = up ? ExpandDirection::Up : ExpandDirection::Down;\n}\n\nvoid Button::updateGeometry(Fn<void(QRect)> update) {\n\tconst auto added = int(base::SafeRound(\n\t\t_heightAnimation.value(_finalHeight)\n\t)) - _collapsed.height();\n\tif (!added && _state != State::Inside) {\n\t\t_scroll = 0;\n\t}\n\tconst auto geometry = _collapsed.marginsAdded({\n\t\t0,\n\t\t(_expandDirection == ExpandDirection::Up) ? added : 0,\n\t\t0,\n\t\t(_expandDirection == ExpandDirection::Down) ? added : 0,\n\t});\n\tif (_geometry != geometry) {\n\t\tif (update) {\n\t\t\tupdate(_geometry);\n\t\t}\n\t\t_geometry = geometry;\n\t\tif (update) {\n\t\t\tupdate(_geometry);\n\t\t}\n\t}\n}\n\nvoid Button::applyState(State state) {\n\tapplyState(state, _update);\n}\n\nvoid Button::applyState(State state, Fn<void(QRect)> update) {\n\tif (state == State::Hidden) {\n\t\t_expandTimer.cancel();\n\t\t_hideTimer.cancel();\n\t}\n\tconst auto finalHeight = (state == State::Hidden)\n\t\t? _heightAnimation.value(_finalHeight)\n\t\t: (state == State::Inside)\n\t\t? _expandedHeight\n\t\t: _collapsed.height();\n\tif (_finalHeight != finalHeight) {\n\t\tif (state == State::Hidden) {\n\t\t\t_heightAnimation.stop();\n\t\t} else {\n\t\t\tif (!_heightAnimation.animating()) {\n\t\t\t\t_collapseType = (_scroll < st::reactionCollapseFadeThreshold)\n\t\t\t\t\t? CollapseType::Scroll\n\t\t\t\t\t: CollapseType::Fade;\n\t\t\t}\n\t\t\t_heightAnimation.start(\n\t\t\t\t[=] { updateGeometry(_update); },\n\t\t\t\t_finalHeight,\n\t\t\t\tfinalHeight,\n\t\t\t\t(state == State::Inside\n\t\t\t\t\t? kExpandDuration\n\t\t\t\t\t: kCollapseDuration),\n\t\t\t\tanim::easeOutCirc);\n\t\t}\n\t\t_finalHeight = finalHeight;\n\t}\n\tupdateGeometry(update);\n\tif (_state == state) {\n\t\treturn;\n\t}\n\tconst auto duration = (state == State::Hidden || _state == State::Hidden)\n\t\t? kToggleDuration\n\t\t: kActivateDuration;\n\tconst auto finalScale = ScaleForState(state);\n\t_opacityAnimation.start(\n\t\t[=] { _update(_geometry); },\n\t\tOpacityForScale(ScaleForState(_state)),\n\t\tOpacityForScale(ScaleForState(state)),\n\t\tduration,\n\t\tanim::sineInOut);\n\tif (state != State::Hidden && _finalScale != finalScale) {\n\t\t_scaleAnimation.start(\n\t\t\t[=] { _update(_geometry); },\n\t\t\t_finalScale,\n\t\t\tfinalScale,\n\t\t\tduration,\n\t\t\tanim::sineInOut);\n\t\t_finalScale = finalScale;\n\t}\n\t_state = state;\n}\n\nfloat64 Button::ScaleForState(State state) {\n\tswitch (state) {\n\tcase State::Hidden: return 1. / 3;\n\tcase State::Shown: return 2. / 3;\n\tcase State::Active:\n\tcase State::Inside: return 1.;\n\t}\n\tUnexpected(\"State in ReactionButton::ScaleForState.\");\n}\n\nfloat64 Button::OpacityForScale(float64 scale) {\n\treturn std::min(\n\t\t((scale - ScaleForState(State::Hidden))\n\t\t\t/ (ScaleForState(State::Shown) - ScaleForState(State::Hidden))),\n\t\t1.);\n}\n\nfloat64 Button::currentScale() const {\n\treturn _scaleAnimation.value(_finalScale);\n}\n\nfloat64 Button::currentOpacity() const {\n\treturn _opacityAnimation.value(OpacityForScale(ScaleForState(_state)));\n}\n\nManager::Manager(\n\tQWidget *wheelEventsTarget,\n\tFn<void(QRect)> buttonUpdate,\n\tIconFactory iconFactory)\n: _outer(CountOuterSize())\n, _inner(QRect({}, st::reactionCornerSize))\n, _strip(\n\tst::reactPanelEmojiPan,\n\t_inner,\n\tst::reactionCornerImage,\n\tcrl::guard(this, [=] { updateCurrentButton(); }),\n\tstd::move(iconFactory))\n, _cachedRound(\n\tst::reactionCornerSize,\n\tst::reactionCornerShadow,\n\t_inner.width())\n, _buttonShowTimer([=] { showButtonDelayed(); })\n, _buttonUpdate(std::move(buttonUpdate)) {\n\t_inner.translate(QRect({}, _outer).center() - _inner.center());\n\n\t_expandedBuffer = _cachedRound.PrepareImage(QSize(\n\t\t_outer.width(),\n\t\t_outer.height() + st::reactionCornerAddedHeightMax));\n\tif (wheelEventsTarget) {\n\t\tstealWheelEvents(wheelEventsTarget);\n\t}\n\n\t_createChooseCallback = [=](ReactionId id) {\n\t\treturn [=] {\n\t\t\tif (auto chosen = lookupChosen(id)) {\n\t\t\t\tupdateButton({});\n\t\t\t\t_chosen.fire(std::move(chosen));\n\t\t\t}\n\t\t};\n\t};\n}\n\nManager::~Manager() = default;\n\nChosenReaction Manager::lookupChosen(const ReactionId &id) const {\n\tauto result = ChosenReaction{\n\t\t.context = _buttonContext,\n\t\t.id = id,\n\t};\n\tconst auto button = _button.get();\n\tif (!button) {\n\t\treturn result;\n\t}\n\tconst auto index = _strip.fillChosenIconGetIndex(result);\n\tif (result.icon.isNull()) {\n\t\treturn result;\n\t}\n\tconst auto between = st::reactionCornerSkip;\n\tconst auto oneHeight = (st::reactionCornerSize.height() + between);\n\tconst auto expanded = (_strip.count() > 1);\n\tconst auto skip = (expanded ? st::reactionExpandedSkip : 0);\n\tconst auto scroll = button->scroll();\n\tconst auto local = skip + index * oneHeight - scroll;\n\tconst auto geometry = button->geometry();\n\tconst auto top = button->expandUp()\n\t\t? (geometry.height() - local - _outer.height())\n\t\t: local;\n\tconst auto rect = QRect(geometry.topLeft() + QPoint(0, top), _outer);\n\tconst auto imageSize = _strip.computeOverSize();\n\tresult.localGeometry = QRect(\n\t\trect.x() + (rect.width() - imageSize) / 2,\n\t\trect.y() + (rect.height() - imageSize) / 2,\n\t\timageSize,\n\t\timageSize);\n\treturn result;\n}\n\nvoid Manager::stealWheelEvents(not_null<QWidget*> target) {\n\tbase::install_event_filter(target, [=](not_null<QEvent*> e) {\n\t\tif (e->type() != QEvent::Wheel\n\t\t\t|| !consumeWheelEvent(static_cast<QWheelEvent*>(e.get()))) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\t\tUi::SendSynteticMouseEvent(target, QEvent::MouseMove, Qt::NoButton);\n\t\treturn base::EventFilterResult::Cancel;\n\t});\n}\n\nvoid Manager::updateButton(ButtonParameters parameters) {\n\tif (parameters.cursorLeft && _menu) {\n\t\treturn;\n\t}\n\tconst auto contextChanged = (_buttonContext != parameters.context);\n\tif (contextChanged) {\n\t\t_strip.setSelected(-1);\n\t\tif (_button) {\n\t\t\t_button->applyState(ButtonState::Hidden);\n\t\t\t_buttonHiding.push_back(std::move(_button));\n\t\t}\n\t\t_buttonShowTimer.cancel();\n\t\t_scheduledParameters = std::nullopt;\n\t}\n\t_buttonContext = parameters.context;\n\tparameters.reactionsCount = _strip.count();\n\tif (!_buttonContext || !parameters.reactionsCount) {\n\t\treturn;\n\t} else if (_button) {\n\t\t_button->applyParameters(parameters);\n\t\tif (_button->geometry().height() == _outer.height()) {\n\t\t\tclearAppearAnimations();\n\t\t}\n\t\treturn;\n\t} else if (parameters.outside) {\n\t\t_buttonShowTimer.cancel();\n\t\t_scheduledParameters = std::nullopt;\n\t\treturn;\n\t}\n\tconst auto globalPositionChanged = _scheduledParameters\n\t\t&& (_scheduledParameters->globalPointer != parameters.globalPointer);\n\tconst auto positionChanged = _scheduledParameters\n\t\t&& (_scheduledParameters->pointer != parameters.pointer);\n\t_scheduledParameters = parameters;\n\tif ((_buttonShowTimer.isActive() && positionChanged)\n\t\t|| globalPositionChanged) {\n\t\t_buttonShowTimer.callOnce(kButtonShowDelay);\n\t}\n}\n\nvoid Manager::showButtonDelayed() {\n\tclearAppearAnimations();\n\t_button = std::make_unique<Button>(\n\t\t_buttonUpdate,\n\t\t*_scheduledParameters,\n\t\t[=]{ updateButton({}); });\n}\n\nvoid Manager::applyList(const Data::PossibleItemReactionsRef &reactions) {\n\tusing Button = Strip::AddedButton;\n\t_strip.applyList(\n\t\treactions.recent,\n\t\t(/*reactions.customAllowed\n\t\t\t? Button::Expand\n\t\t\t: */Button::None));\n\t_tagsStrip = reactions.tags;\n}\n\nQMargins Manager::innerMargins() const {\n\treturn {\n\t\t_inner.x(),\n\t\t_inner.y(),\n\t\t_outer.width() - _inner.x() - _inner.width(),\n\t\t_outer.height() - _inner.y() - _inner.height(),\n\t};\n}\n\nQRect Manager::buttonInner() const {\n\treturn buttonInner(_button.get());\n}\n\nQRect Manager::buttonInner(not_null<Button*> button) const {\n\treturn button->geometry().marginsRemoved(innerMargins());\n}\n\nvoid Manager::updateCurrentButton() const {\n\tif (const auto button = _button.get()) {\n\t\t_buttonUpdate(button->geometry());\n\t}\n}\n\nvoid Manager::removeStaleButtons() {\n\t_buttonHiding.erase(\n\t\tranges::remove_if(_buttonHiding, &Button::isHidden),\n\t\tend(_buttonHiding));\n}\n\nvoid Manager::paint(QPainter &p, const PaintContext &context) {\n\tremoveStaleButtons();\n\tfor (const auto &button : _buttonHiding) {\n\t\tpaintButton(p, context, button.get());\n\t}\n\tif (const auto current = _button.get()) {\n\t\tif (context.gestureHorizontal.ratio) {\n\t\t\tcurrent->applyState(ButtonState::Hidden);\n\t\t\t_buttonHiding.push_back(std::move(_button));\n\t\t}\n\t\tpaintButton(p, context, current);\n\t}\n\n\tfor (const auto &[id, effect] : _collectedEffects) {\n\t\tconst auto offset = effect.effectOffset;\n\t\tp.translate(offset);\n\t\t_activeEffectAreas[id] = effect.effectPaint(p).translated(offset);\n\t\tp.translate(-offset);\n\t}\n\t_collectedEffects.clear();\n}\n\nClickHandlerPtr Manager::computeButtonLink(QPoint position) const {\n\tif (_strip.empty()) {\n\t\t_strip.setSelected(-1);\n\t\treturn nullptr;\n\t}\n\tconst auto inner = buttonInner();\n\tconst auto top = _button->expandUp()\n\t\t? (inner.y() + inner.height() - position.y())\n\t\t: (position.y() - inner.y());\n\tconst auto shifted = top + _button->scroll();\n\tconst auto between = st::reactionCornerSkip;\n\tconst auto oneHeight = (st::reactionCornerSize.height() + between);\n\tconst auto index = std::clamp(\n\t\tint(base::SafeRound(shifted + between / 2.)) / oneHeight,\n\t\t0,\n\t\tint(_strip.count() - 1));\n\t_strip.setSelected(index);\n\tconst auto selected = _strip.selected();\n\tif (selected == Strip::AddedButton::Expand) {\n\t\tif (!_expandLink) {\n\t\t\t_expandLink = std::make_shared<LambdaClickHandler>([=] {\n\t\t\t\t_expandChosen.fire_copy(_buttonContext);\n\t\t\t});\n\t\t}\n\t\treturn _expandLink;\n\t}\n\tconst auto id = std::get_if<ReactionId>(&selected);\n\tif (!id || id->empty()) {\n\t\treturn nullptr;\n\t}\n\tauto &result = _links[*id];\n\tif (!result) {\n\t\tresult = resolveButtonLink(*id);\n\t}\n\treturn result;\n}\n\nClickHandlerPtr Manager::resolveButtonLink(const ReactionId &id) const {\n\tconst auto i = _reactionsLinks.find(id);\n\tif (i != end(_reactionsLinks)) {\n\t\treturn i->second;\n\t}\n\tauto handler = std::make_shared<LambdaClickHandler>(\n\t\tcrl::guard(this, _createChooseCallback(id)));\n\thandler->setProperty(\n\t\tkSendReactionEmojiProperty,\n\t\tQVariant::fromValue(id));\n\treturn _reactionsLinks.emplace(id, std::move(handler)).first->second;\n}\n\nTextState Manager::buttonTextState(QPoint position) const {\n\tif (overCurrentButton(position)) {\n\t\tauto result = TextState(nullptr, computeButtonLink(position));\n\t\tresult.itemId = _buttonContext;\n\t\treturn result;\n\t} else {\n\t\t_strip.setSelected(-1);\n\t}\n\treturn {};\n}\n\nbool Manager::overCurrentButton(QPoint position) const {\n\tif (!_button) {\n\t\treturn false;\n\t}\n\treturn _button && buttonInner().contains(position);\n}\n\nvoid Manager::remove(FullMsgId context) {\n\t_activeEffectAreas.remove(context);\n\tif (_buttonContext == context) {\n\t\t_buttonContext = {};\n\t\t_button = nullptr;\n\t}\n}\n\nbool Manager::consumeWheelEvent(not_null<QWheelEvent*> e) {\n\treturn _button && _button->consumeWheelEvent(e);\n}\n\nvoid Manager::paintButton(\n\t\tQPainter &p,\n\t\tconst PaintContext &context,\n\t\tnot_null<Button*> button) {\n\tconst auto geometry = button->geometry();\n\tif (!context.clip.intersects(geometry)) {\n\t\treturn;\n\t}\n\tconstexpr auto kFramesCount = Ui::RoundAreaWithShadow::kFramesCount;\n\tconst auto scale = button->currentScale();\n\tconst auto scaleMin = Button::ScaleForState(ButtonState::Hidden);\n\tconst auto scaleMax = Button::ScaleForState(ButtonState::Active);\n\tconst auto progress = (scale - scaleMin) / (scaleMax - scaleMin);\n\tconst auto frame = int(base::SafeRound(progress * (kFramesCount - 1)));\n\tconst auto useScale = scaleMin\n\t\t+ (frame / float64(kFramesCount - 1)) * (scaleMax - scaleMin);\n\tpaintButton(p, context, button, frame, useScale);\n}\n\nvoid Manager::paintButton(\n\t\tQPainter &p,\n\t\tconst PaintContext &context,\n\t\tnot_null<Button*> button,\n\t\tint frameIndex,\n\t\tfloat64 scale) {\n\tconst auto opacity = button->currentOpacity();\n\tif (opacity == 0.) {\n\t\treturn;\n\t}\n\n\tconst auto geometry = button->geometry();\n\tconst auto position = geometry.topLeft();\n\tconst auto size = geometry.size();\n\tconst auto expanded = (size.height() - _outer.height());\n\tif (opacity != 1.) {\n\t\tp.setOpacity(opacity);\n\t}\n\tauto layeredPainter = std::optional<QPainter>();\n\tif (expanded) {\n\t\t_expandedBuffer.fill(Qt::transparent);\n\t}\n\tconst auto q = expanded ? &layeredPainter.emplace(&_expandedBuffer) : &p;\n\tconst auto shadow = context.st->shadowFg()->c;\n\tconst auto background = context.st->windowBg()->c;\n\t_cachedRound.setShadowColor(shadow);\n\t_cachedRound.setBackgroundColor(background);\n\tif (expanded) {\n\t\tq->fillRect(QRect(QPoint(), size), context.st->windowBg());\n\t} else {\n\t\tconst auto radius = _inner.height() / 2.;\n\t\tconst auto frame = _cachedRound.validateFrame(\n\t\t\tframeIndex,\n\t\t\tscale,\n\t\t\tradius);\n\t\tp.drawImage(position, *frame.image, frame.rect);\n\t}\n\n\tconst auto current = (button == _button.get());\n\tconst auto expandRatio = expanded\n\t\t? std::clamp(\n\t\t\tfloat64(expanded) / (button->expandedHeight() - _outer.height()),\n\t\t\t0.,\n\t\t\t1.)\n\t\t: 0.;\n\tconst auto expandedSkip = int(base::SafeRound(\n\t\texpandRatio * st::reactionExpandedSkip));\n\tconst auto mainEmojiPosition = _inner.topLeft() + (!expanded\n\t\t? position\n\t\t: button->expandUp()\n\t\t? QPoint(0, expanded - expandedSkip)\n\t\t: QPoint(0, expandedSkip));\n\tconst auto mainEmoji = _strip.validateEmoji(frameIndex, scale);\n\tif (expanded\n\t\t|| (current && !_strip.onlyMainEmojiVisible())\n\t\t|| _strip.onlyAddedButton()) {\n\t\tconst auto opacity = button->expandAnimationOpacity(expandRatio);\n\t\tif (opacity != 1.) {\n\t\t\tq->setOpacity(opacity);\n\t\t}\n\t\tconst auto clip = QRect(\n\t\t\texpanded ? QPoint() : position,\n\t\t\tbutton->geometry().size()\n\t\t).marginsRemoved(innerMargins());\n\t\tconst auto between = st::reactionCornerSkip;\n\t\tconst auto oneHeight = st::reactionCornerSize.height() + between;\n\t\tconst auto expandUp = button->expandUp();\n\t\tconst auto shift = QPoint(0, oneHeight * (expandUp ? -1 : 1));\n\t\tconst auto scroll = button->expandAnimationScroll(expandRatio);\n\t\tconst auto startEmojiPosition = mainEmojiPosition\n\t\t\t+ QPoint(0, scroll * (expandUp ? 1 : -1));\n\t\t_strip.paint(*q, startEmojiPosition, shift, clip, scale, !current);\n\t\tif (opacity != 1.) {\n\t\t\tq->setOpacity(1.);\n\t\t}\n\t\tif (current && expanded) {\n\t\t\t_showingAll = true;\n\t\t}\n\t\tif (expanded) {\n\t\t\tpaintInnerGradients(*q, background, button, scroll, expandRatio);\n\t\t}\n\t\tif (opacity != 1.) {\n\t\t\tconst auto appearShift = st::reactionMainAppearShift * opacity;\n\t\t\tconst auto appearPosition = !expanded\n\t\t\t\t? position\n\t\t\t\t: button->expandUp()\n\t\t\t\t? QPoint(0, expanded - appearShift)\n\t\t\t\t: QPoint(0, appearShift);\n\t\t\tq->setOpacity(1. - opacity);\n\t\t\tq->drawImage(\n\t\t\t\tappearPosition + _inner.topLeft(),\n\t\t\t\t*mainEmoji.image,\n\t\t\t\tmainEmoji.rect);\n\t\t\tq->setOpacity(1.);\n\t\t}\n\t} else {\n\t\tp.drawImage(mainEmojiPosition, *mainEmoji.image, mainEmoji.rect);\n\t}\n\tif (current && !expanded) {\n\t\tclearAppearAnimations();\n\t}\n\n\tif (expanded) {\n\t\tconst auto radiusMin = _inner.height() / 2.;\n\t\tconst auto radiusMax = _inner.width() / 2.;\n\t\t_cachedRound.overlayExpandedBorder(\n\t\t\t*q,\n\t\t\tsize,\n\t\t\texpandRatio,\n\t\t\tradiusMin,\n\t\t\tradiusMax,\n\t\t\tscale);\n\t\tlayeredPainter.reset();\n\t\tp.drawImage(\n\t\t\tgeometry,\n\t\t\t_expandedBuffer,\n\t\t\tQRect(QPoint(), size * style::DevicePixelRatio()));\n\t}\n\tif (opacity != 1.) {\n\t\tp.setOpacity(1.);\n\t}\n}\n\nvoid Manager::paintInnerGradients(\n\t\tQPainter &p,\n\t\tconst QColor &background,\n\t\tnot_null<Button*> button,\n\t\tint scroll,\n\t\tfloat64 expandRatio) {\n\tif (_gradientBackground != background) {\n\t\t_gradientBackground = background;\n\t\t_topGradient = _bottomGradient = QImage();\n\t}\n\tconst auto endScroll = button->scrollMax() - scroll;\n\tconst auto size = st::reactionGradientSize;\n\tconst auto ensureGradient = [&](QImage &gradient, bool top) {\n\t\tif (!gradient.isNull()) {\n\t\t\treturn;\n\t\t}\n\t\tgradient = Images::GenerateShadow(\n\t\t\tsize,\n\t\t\ttop ? 255 : 0,\n\t\t\ttop ? 0 : 255,\n\t\t\tbackground);\n\t};\n\tensureGradient(_topGradient, true);\n\tensureGradient(_bottomGradient, false);\n\tconst auto paintGradient = [&](QImage &gradient, int scrolled, int top) {\n\t\tif (scrolled <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto opacity = (expandRatio * scrolled)\n\t\t\t/ st::reactionGradientFadeSize;\n\t\tp.setOpacity(opacity);\n\t\tp.drawImage(\n\t\t\tQRect(0, top, _outer.width(), size),\n\t\t\tgradient,\n\t\t\tQRect(QPoint(), gradient.size()));\n\t};\n\tconst auto up = button->expandUp();\n\tconst auto start = st::reactionGradientStart;\n\tpaintGradient(_topGradient, up ? endScroll : scroll, start);\n\tconst auto bottomStart = button->geometry().height() - start - size;\n\tpaintGradient(_bottomGradient, up ? scroll : endScroll, bottomStart);\n\tp.setOpacity(1.);\n}\n\nvoid Manager::clearAppearAnimations() {\n\tif (!_showingAll) {\n\t\treturn;\n\t}\n\t_showingAll = false;\n\t_strip.clearAppearAnimations();\n}\n\nstd::optional<QRect> Manager::lookupEffectArea(FullMsgId itemId) const {\n\tconst auto i = _activeEffectAreas.find(itemId);\n\treturn (i != end(_activeEffectAreas))\n\t\t? i->second\n\t\t: std::optional<QRect>();\n}\n\nvoid Manager::startEffectsCollection() {\n\t_collectedEffects.clear();\n\t_currentReactionInfo = {};\n}\n\nauto Manager::currentReactionPaintInfo()\n-> not_null<Ui::ReactionPaintInfo*> {\n\treturn &_currentReactionInfo;\n}\n\nvoid Manager::recordCurrentReactionEffect(FullMsgId itemId, QPoint origin) {\n\tif (_currentReactionInfo.effectPaint) {\n\t\t_currentReactionInfo.effectOffset += origin\n\t\t\t+ _currentReactionInfo.position;\n\t\t_collectedEffects[itemId] = base::take(_currentReactionInfo);\n\t} else if (!_collectedEffects.empty()) {\n\t\t_collectedEffects.remove(itemId);\n\t}\n}\n\nbool Manager::showContextMenu(\n\t\tQWidget *parent,\n\t\tQContextMenuEvent *e,\n\t\tconst ReactionId &favorite) {\n\tconst auto selected = _strip.selected();\n\tconst auto id = std::get_if<ReactionId>(&selected);\n\tif (!id || id->empty() || _tagsStrip) {\n\t\treturn false;\n\t} else if (*id == favorite || id->paid()) {\n\t\treturn true;\n\t}\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\t_menu->addAction(\n\t\ttr::lng_context_set_as_quick(tr::now),\n\t\t[=, id = *id] { _faveRequests.fire_copy(id); },\n\t\t&st::menuIconFave);\n\t_menu->popup(e->globalPos());\n\treturn true;\n}\n\nauto Manager::faveRequests() const -> rpl::producer<ReactionId> {\n\treturn _faveRequests.events();\n}\n\nvoid SetupManagerList(\n\t\tnot_null<Manager*> manager,\n\t\trpl::producer<HistoryItem*> items) {\n\tstruct State {\n\t\tPeerData *peer = nullptr;\n\t\tHistoryItem *item = nullptr;\n\t\tMain::Session *session = nullptr;\n\t\trpl::lifetime sessionLifetime;\n\t\trpl::lifetime peerLifetime;\n\t\tbase::Timer timer;\n\t};\n\tconst auto state = manager->lifetime().make_state<State>();\n\n\tstd::move(\n\t\titems\n\t) | rpl::filter([=](HistoryItem *item) {\n\t\treturn (item != state->item);\n\t}) | rpl::on_next([=](HistoryItem *item) {\n\t\tstate->item = item;\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto peer = item->history()->peer;\n\t\tconst auto session = &peer->session();\n\t\tconst auto peerChanged = (state->peer != peer);\n\t\tconst auto sessionChanged = (state->session != session);\n\t\tconst auto push = [=] {\n\t\t\tstate->timer.cancel();\n\t\t\tif (const auto item = state->item) {\n\t\t\t\tmanager->applyList(Data::LookupPossibleReactions(item));\n\t\t\t}\n\t\t};\n\t\tstate->timer.setCallback(push);\n\t\tif (sessionChanged) {\n\t\t\tstate->sessionLifetime.destroy();\n\t\t\tstate->session = session;\n\t\t\tData::AmPremiumValue(\n\t\t\t\tsession\n\t\t\t) | rpl::skip(\n\t\t\t\t1\n\t\t\t) | rpl::on_next(push, state->sessionLifetime);\n\n\t\t\tsession->changes().messageUpdates(\n\t\t\t\tData::MessageUpdate::Flag::Destroyed\n\t\t\t) | rpl::on_next([=](const Data::MessageUpdate &update) {\n\t\t\t\tif (update.item == state->item) {\n\t\t\t\t\tstate->item = nullptr;\n\t\t\t\t\tstate->timer.cancel();\n\t\t\t\t}\n\t\t\t}, state->sessionLifetime);\n\n\t\t\tsession->data().itemDataChanges(\n\t\t\t) | rpl::filter([=](not_null<HistoryItem*> item) {\n\t\t\t\treturn (item == state->item);\n\t\t\t}) | rpl::on_next(push, state->sessionLifetime);\n\n\t\t\tconst auto &reactions = session->data().reactions();\n\t\t\trpl::merge(\n\t\t\t\treactions.topUpdates(),\n\t\t\t\treactions.recentUpdates(),\n\t\t\t\treactions.defaultUpdates(),\n\t\t\t\treactions.favoriteUpdates(),\n\t\t\t\treactions.myTagsUpdates(),\n\t\t\t\treactions.tagsUpdates()\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tif (!state->timer.isActive()) {\n\t\t\t\t\tstate->timer.callOnce(kRefreshListDelay);\n\t\t\t\t}\n\t\t\t}, state->sessionLifetime);\n\t\t}\n\t\tif (peerChanged) {\n\t\t\tstate->peer = peer;\n\t\t\tstate->peerLifetime = rpl::combine(\n\t\t\t\tData::PeerAllowedReactionsValue(peer),\n\t\t\t\tData::UniqueReactionsLimitValue(peer)\n\t\t\t) | rpl::on_next(push);\n\t\t} else {\n\t\t\tpush();\n\t\t}\n\t}, manager->lifetime());\n\n\tmanager->faveRequests(\n\t) | rpl::filter([=] {\n\t\treturn (state->session != nullptr);\n\t}) | rpl::on_next([=](const Data::ReactionId &id) {\n\t\tstate->session->data().reactions().setFavorite(id);\n\t\tmanager->updateButton({});\n\t}, manager->lifetime());\n}\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"base/unique_qptr.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/round_area_with_shadow.h\"\n#include \"history/view/reactions/history_view_reactions_strip.h\"\n#include \"ui/chat/chat_style.h\" // Ui::ReactionPaintInfo\n\nnamespace Ui {\nstruct ChatPaintContext;\nstruct ReactionPaintInfo;\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Data {\nstruct ReactionId;\nstruct Reaction;\nstruct PossibleItemReactionsRef;\nclass DocumentMedia;\n} // namespace Data\n\nnamespace HistoryView {\nusing PaintContext = Ui::ChatPaintContext;\nstruct TextState;\n} // namespace HistoryView\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace HistoryView::Reactions {\n\nenum class ExpandDirection {\n\tUp,\n\tDown,\n};\n\nstruct ButtonParameters {\n\t[[nodiscard]] ButtonParameters translated(QPoint delta) const {\n\t\tauto result = *this;\n\t\tresult.center += delta;\n\t\tresult.pointer += delta;\n\t\treturn result;\n\t}\n\n\tFullMsgId context;\n\tQPoint center;\n\tQPoint pointer;\n\tQPoint globalPointer;\n\tint reactionsHeight = 0;\n\tint reactionsCount = 1;\n\tint visibleTop = 0;\n\tint visibleBottom = 0;\n\tbool outside = false;\n\tbool cursorLeft = false;\n};\n\nenum class ButtonState {\n\tHidden,\n\tShown,\n\tActive,\n\tInside,\n};\n\nclass Button final {\npublic:\n\tButton(\n\t\tFn<void(QRect)> update,\n\t\tButtonParameters parameters,\n\t\tFn<void()> hide);\n\t~Button();\n\n\tvoid applyParameters(ButtonParameters parameters);\n\n\tusing State = ButtonState;\n\tvoid applyState(State state);\n\n\t[[nodiscard]] bool expandUp() const;\n\t[[nodiscard]] bool isHidden() const;\n\t[[nodiscard]] QRect geometry() const;\n\t[[nodiscard]] int expandedHeight() const;\n\t[[nodiscard]] int scroll() const;\n\t[[nodiscard]] int scrollMax() const;\n\t[[nodiscard]] float64 currentScale() const;\n\t[[nodiscard]] float64 currentOpacity() const;\n\t[[nodiscard]] float64 expandAnimationOpacity(float64 expandRatio) const;\n\t[[nodiscard]] int expandAnimationScroll(float64 expandRatio) const;\n\t[[nodiscard]] bool consumeWheelEvent(not_null<QWheelEvent*> e);\n\n\t[[nodiscard]] static float64 ScaleForState(State state);\n\t[[nodiscard]] static float64 OpacityForScale(float64 scale);\n\nprivate:\n\tenum class CollapseType {\n\t\tScroll,\n\t\tFade,\n\t};\n\n\tvoid updateGeometry(Fn<void(QRect)> update);\n\tvoid applyState(State satte, Fn<void(QRect)> update);\n\tvoid applyParameters(\n\t\tButtonParameters parameters,\n\t\tFn<void(QRect)> update);\n\tvoid updateExpandDirection(const ButtonParameters &parameters);\n\n\tconst Fn<void(QRect)> _update;\n\n\tState _state = State::Hidden;\n\tfloat64 _finalScale = 0.;\n\tUi::Animations::Simple _scaleAnimation;\n\tUi::Animations::Simple _opacityAnimation;\n\tUi::Animations::Simple _heightAnimation;\n\n\tQRect _collapsed;\n\tQRect _geometry;\n\tint _expandedInnerHeight = 0;\n\tint _expandedHeight = 0;\n\tint _finalHeight = 0;\n\tint _scroll = 0;\n\tExpandDirection _expandDirection = ExpandDirection::Up;\n\tCollapseType _collapseType = CollapseType::Scroll;\n\n\tbase::Timer _expandTimer;\n\tbase::Timer _hideTimer;\n\tstd::optional<QPoint> _lastGlobalPosition;\n\n};\n\nclass Manager final : public base::has_weak_ptr {\npublic:\n\tManager(\n\t\tQWidget *wheelEventsTarget,\n\t\tFn<void(QRect)> buttonUpdate,\n\t\tIconFactory iconFactory = nullptr);\n\t~Manager();\n\n\tusing ReactionId = ::Data::ReactionId;\n\n\tvoid applyList(const Data::PossibleItemReactionsRef &reactions);\n\n\tvoid updateButton(ButtonParameters parameters);\n\tvoid paint(QPainter &p, const PaintContext &context);\n\t[[nodiscard]] TextState buttonTextState(QPoint position) const;\n\tvoid remove(FullMsgId context);\n\n\t[[nodiscard]] bool consumeWheelEvent(not_null<QWheelEvent*> e);\n\n\t[[nodiscard]] rpl::producer<ChosenReaction> chosen() const {\n\t\treturn _chosen.events();\n\t}\n\t[[nodiscard]] rpl::producer<FullMsgId> expandChosen() const {\n\t\treturn _expandChosen.events();\n\t}\n\n\t[[nodiscard]] std::optional<QRect> lookupEffectArea(\n\t\tFullMsgId itemId) const;\n\tvoid startEffectsCollection();\n\t[[nodiscard]] auto currentReactionPaintInfo()\n\t\t-> not_null<Ui::ReactionPaintInfo*>;\n\tvoid recordCurrentReactionEffect(FullMsgId itemId, QPoint origin);\n\n\tbool showContextMenu(\n\t\tQWidget *parent,\n\t\tQContextMenuEvent *e,\n\t\tconst ReactionId &favorite);\n\t[[nodiscard]] rpl::producer<ReactionId> faveRequests() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tvoid showButtonDelayed();\n\tvoid stealWheelEvents(not_null<QWidget*> target);\n\n\t[[nodiscard]] ChosenReaction lookupChosen(const ReactionId &id) const;\n\t[[nodiscard]] bool overCurrentButton(QPoint position) const;\n\n\tvoid removeStaleButtons();\n\tvoid paintButton(\n\t\tQPainter &p,\n\t\tconst PaintContext &context,\n\t\tnot_null<Button*> button);\n\tvoid paintButton(\n\t\tQPainter &p,\n\t\tconst PaintContext &context,\n\t\tnot_null<Button*> button,\n\t\tint frame,\n\t\tfloat64 scale);\n\tvoid paintInnerGradients(\n\t\tQPainter &p,\n\t\tconst QColor &background,\n\t\tnot_null<Button*> button,\n\t\tint scroll,\n\t\tfloat64 expandRatio);\n\n\tvoid clearAppearAnimations();\n\n\t[[nodiscard]] QMargins innerMargins() const;\n\t[[nodiscard]] QRect buttonInner() const;\n\t[[nodiscard]] QRect buttonInner(not_null<Button*> button) const;\n\n\t[[nodiscard]] ClickHandlerPtr computeButtonLink(QPoint position) const;\n\t[[nodiscard]] ClickHandlerPtr resolveButtonLink(\n\t\tconst ReactionId &id) const;\n\n\tvoid updateCurrentButton() const;\n\n\tQSize _outer;\n\tQRect _inner;\n\tStrip _strip;\n\tUi::RoundAreaWithShadow _cachedRound;\n\tQImage _expandedBuffer;\n\tQColor _gradientBackground;\n\tQImage _topGradient;\n\tQImage _bottomGradient;\n\tQColor _gradient;\n\n\trpl::event_stream<ChosenReaction> _chosen;\n\trpl::event_stream<FullMsgId> _expandChosen;\n\tmutable base::flat_map<ReactionId, ClickHandlerPtr> _links;\n\tmutable ClickHandlerPtr _expandLink;\n\n\trpl::variable<int> _uniqueLimit = 0;\n\tbool _showingAll = false;\n\tbool _tagsStrip = false;\n\n\tstd::optional<ButtonParameters> _scheduledParameters;\n\tbase::Timer _buttonShowTimer;\n\tconst Fn<void(QRect)> _buttonUpdate;\n\tstd::unique_ptr<Button> _button;\n\tstd::vector<std::unique_ptr<Button>> _buttonHiding;\n\tFullMsgId _buttonContext;\n\tmutable base::flat_map<ReactionId, ClickHandlerPtr> _reactionsLinks;\n\tFn<Fn<void()>(ReactionId)> _createChooseCallback;\n\n\tbase::flat_map<FullMsgId, QRect> _activeEffectAreas;\n\n\tUi::ReactionPaintInfo _currentReactionInfo;\n\tbase::flat_map<FullMsgId, Ui::ReactionPaintInfo> _collectedEffects;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\trpl::event_stream<ReactionId> _faveRequests;\n\n\trpl::lifetime _lifetime;\n\n};\n\nvoid SetupManagerList(\n\tnot_null<Manager*> manager,\n\trpl::producer<HistoryItem*> items);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/reactions/history_view_reactions_list.h\"\n\n#include \"history/view/reactions/history_view_reactions_tabs.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"boxes/peers/prepare_short_info_box.h\"\n#include \"window/window_session_controller.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"api/api_who_reacted.h\"\n#include \"ui/controls/who_reacted_context_action.h\"\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/painter.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_message_reaction_id.h\"\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"data/data_peer.h\"\n#include \"lang/lang_keys.h\"\n\nnamespace HistoryView::Reactions {\nnamespace {\n\nconstexpr auto kPerPageFirst = 20;\nconstexpr auto kPerPage = 100;\n\nusing ::Data::ReactionId;\n\nclass Row final : public PeerListRow {\npublic:\n\tRow(\n\t\tuint64 id,\n\t\tnot_null<PeerData*> peer,\n\t\tconst Ui::Text::CustomEmojiFactory &factory,\n\t\tQStringView reactionEntityData,\n\t\tFn<void(Row*)> repaint,\n\t\tFn<bool()> paused);\n\n\tQSize rightActionSize() const override;\n\tQMargins rightActionMargins() const override;\n\tbool rightActionDisabled() const override;\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\nprivate:\n\tstd::unique_ptr<Ui::Text::CustomEmoji> _custom;\n\tFn<bool()> _paused;\n\n};\n\nclass Controller final : public PeerListController {\npublic:\n\tController(\n\t\tnot_null<Window::SessionNavigation*> window,\n\t\tFullMsgId itemId,\n\t\tconst ReactionId &selected,\n\t\trpl::producer<ReactionId> switches,\n\t\tstd::shared_ptr<Api::WhoReadList> whoReadIds);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\n\tstd::unique_ptr<PeerListRow> createRestoredRow(\n\t\tnot_null<PeerData*> peer) override;\n\n\tstd::unique_ptr<PeerListState> saveState() const override;\n\tvoid restoreState(std::unique_ptr<PeerListState> state) override;\n\nprivate:\n\tusing AllEntry = std::pair<not_null<PeerData*>, Data::ReactionId>;\n\n\tstruct SavedState : SavedStateBase {\n\t\tReactionId shownReaction;\n\t\tbase::flat_map<std::pair<PeerId, ReactionId>, uint64> idsMap;\n\t\tuint64 idsCounter = 0;\n\t\tstd::vector<AllEntry> all;\n\t\tQString allOffset;\n\t\tstd::vector<not_null<PeerData*>> filtered;\n\t\tQString filteredOffset;\n\t\tbool wasLoading = false;\n\t};\n\n\tvoid fillWhoRead();\n\tvoid loadMore(const ReactionId &reaction);\n\tbool appendRow(not_null<PeerData*> peer, ReactionId reaction);\n\tstd::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<PeerData*> peer,\n\t\tReactionId reaction) const;\n\tvoid showReaction(const ReactionId &reaction);\n\n\t[[nodiscard]] uint64 id(\n\t\tnot_null<PeerData*> peer,\n\t\tconst ReactionId &reaction) const;\n\n\tconst not_null<Window::SessionNavigation*> _window;\n\tconst not_null<PeerData*> _peer;\n\tconst FullMsgId _itemId;\n\tconst Ui::Text::CustomEmojiFactory _factory;\n\tconst std::shared_ptr<Api::WhoReadList> _whoReadIds;\n\tconst std::vector<not_null<PeerData*>> _whoRead;\n\tMTP::Sender _api;\n\n\tReactionId _shownReaction;\n\n\tmutable base::flat_map<std::pair<PeerId, ReactionId>, uint64> _idsMap;\n\tmutable uint64 _idsCounter = 0;\n\n\tstd::vector<AllEntry> _all;\n\tQString _allOffset;\n\n\tstd::vector<not_null<PeerData*>> _filtered;\n\tQString _filteredOffset;\n\n\tmtpRequestId _loadRequestId = 0;\n\n};\n\n[[nodiscard]] std::vector<not_null<PeerData*>> ResolveWhoRead(\n\t\tnot_null<Window::SessionNavigation*> window,\n\t\tconst std::shared_ptr<Api::WhoReadList> &whoReadIds) {\n\tif (!whoReadIds || whoReadIds->list.empty()) {\n\t\treturn {};\n\t}\n\tauto result = std::vector<not_null<PeerData*>>();\n\tauto &owner = window->session().data();\n\tfor (const auto &peerWithDate : whoReadIds->list) {\n\t\tif (const auto peer = owner.peerLoaded(peerWithDate.peer)) {\n\t\t\tresult.push_back(peer);\n\t\t}\n\t}\n\treturn result;\n}\n\nRow::Row(\n\tuint64 id,\n\tnot_null<PeerData*> peer,\n\tconst Ui::Text::CustomEmojiFactory &factory,\n\tQStringView reactionEntityData,\n\tFn<void(Row*)> repaint,\n\tFn<bool()> paused)\n: PeerListRow(peer, id)\n, _custom(reactionEntityData.isEmpty()\n\t? nullptr\n\t: factory(reactionEntityData, { .repaint = [=] { repaint(this); } }))\n, _paused(std::move(paused)) {\n}\n\nQSize Row::rightActionSize() const {\n\tconst auto size = Ui::Emoji::GetSizeNormal() / style::DevicePixelRatio();\n\treturn _custom ? QSize(size, size) : QSize();\n}\n\nQMargins Row::rightActionMargins() const {\n\tif (!_custom) {\n\t\treturn QMargins();\n\t}\n\tconst auto size = Ui::Emoji::GetSizeNormal() / style::DevicePixelRatio();\n\treturn QMargins(\n\t\tsize / 2,\n\t\t(st::defaultPeerList.item.height - size) / 2,\n\t\t(size * 3) / 2,\n\t\t0);\n}\n\nbool Row::rightActionDisabled() const {\n\treturn true;\n}\n\nvoid Row::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\tif (!_custom) {\n\t\treturn;\n\t}\n\tconst auto size = Ui::Emoji::GetSizeNormal() / style::DevicePixelRatio();\n\tconst auto skip = (size - Ui::Text::AdjustCustomEmojiSize(size)) / 2;\n\t_custom->paint(p, {\n\t\t.textColor = st::windowFg->c,\n\t\t.now = crl::now(),\n\t\t.position = { x + skip, y + skip },\n\t\t.paused = _paused(),\n\t});\n}\n\nController::Controller(\n\tnot_null<Window::SessionNavigation*> window,\n\tFullMsgId itemId,\n\tconst ReactionId &selected,\n\trpl::producer<ReactionId> switches,\n\tstd::shared_ptr<Api::WhoReadList> whoReadIds)\n: _window(window)\n, _peer(window->session().data().peer(itemId.peer))\n, _itemId(itemId)\n, _factory(Data::ReactedMenuFactory(&window->session()))\n, _whoReadIds(whoReadIds)\n, _whoRead(ResolveWhoRead(window, _whoReadIds))\n, _api(&window->session().mtp())\n, _shownReaction(selected) {\n\tstd::move(\n\t\tswitches\n\t) | rpl::filter([=](const ReactionId &reaction) {\n\t\treturn (_shownReaction != reaction);\n\t}) | rpl::on_next([=](const ReactionId &reaction) {\n\t\tshowReaction(reaction);\n\t}, lifetime());\n}\n\nMain::Session &Controller::session() const {\n\treturn _window->session();\n}\n\nvoid Controller::prepare() {\n\tif (_shownReaction.emoji() == u\"read\"_q) {\n\t\tfillWhoRead();\n\t\tsetDescriptionText(QString());\n\t} else {\n\t\tsetDescriptionText(tr::lng_contacts_loading(tr::now));\n\t}\n\tdelegate()->peerListRefreshRows();\n\tloadMore(_shownReaction);\n}\n\nvoid Controller::showReaction(const ReactionId &reaction) {\n\tif (_shownReaction == reaction) {\n\t\treturn;\n\t}\n\n\t_api.request(base::take(_loadRequestId)).cancel();\n\twhile (const auto count = delegate()->peerListFullRowsCount()) {\n\t\tdelegate()->peerListRemoveRow(delegate()->peerListRowAt(count - 1));\n\t}\n\n\t_shownReaction = reaction;\n\tif (_shownReaction.emoji() == u\"read\"_q) {\n\t\tfillWhoRead();\n\t} else if (_shownReaction.empty()) {\n\t\t_filtered.clear();\n\t\tfor (const auto &[peer, reaction] : _all) {\n\t\t\tappendRow(peer, reaction);\n\t\t}\n\t} else {\n\t\t_filtered = _all | ranges::views::filter([&](const AllEntry &entry) {\n\t\t\treturn (entry.second == reaction);\n\t\t}) | ranges::views::transform(\n\t\t\t&AllEntry::first\n\t\t) | ranges::to_vector;\n\t\tfor (const auto &peer : _filtered) {\n\t\t\tappendRow(peer, _shownReaction);\n\t\t}\n\t\t_filteredOffset = QString();\n\t}\n\tloadMore(_shownReaction);\n\tsetDescriptionText(delegate()->peerListFullRowsCount()\n\t\t? QString()\n\t\t: tr::lng_contacts_loading(tr::now));\n\tdelegate()->peerListRefreshRows();\n}\n\nuint64 Controller::id(\n\t\tnot_null<PeerData*> peer,\n\t\tconst ReactionId &reaction) const {\n\tconst auto key = std::pair{ peer->id, reaction };\n\tconst auto i = _idsMap.find(key);\n\treturn (i != end(_idsMap)\n\t\t? i\n\t\t: _idsMap.emplace(key, ++_idsCounter).first)->second;\n}\n\nvoid Controller::fillWhoRead() {\n\tfor (const auto &peer : _whoRead) {\n\t\tappendRow(peer, ReactionId());\n\t}\n}\n\nvoid Controller::loadMoreRows() {\n\tconst auto &offset = _shownReaction.empty()\n\t\t? _allOffset\n\t\t: _filteredOffset;\n\tif (_loadRequestId || offset.isEmpty()) {\n\t\treturn;\n\t}\n\tloadMore(_shownReaction);\n}\n\nstd::unique_ptr<PeerListRow> Controller::createRestoredRow(\n\t\tnot_null<PeerData*> peer) {\n\tif (_shownReaction.emoji() == u\"read\"_q) {\n\t\treturn createRow(peer, Data::ReactionId());\n\t} else if (_shownReaction.empty()) {\n\t\tconst auto i = ranges::find(_all, peer, &AllEntry::first);\n\t\tconst auto reaction = (i != end(_all)) ? i->second : _shownReaction;\n\t\treturn createRow(peer, reaction);\n\t}\n\treturn createRow(peer, _shownReaction);\n}\n\nstd::unique_ptr<PeerListState> Controller::saveState() const {\n\tauto result = PeerListController::saveState();\n\n\tauto my = std::make_unique<SavedState>();\n\tmy->shownReaction = _shownReaction;\n\tmy->idsMap = _idsMap;\n\tmy->idsCounter = _idsCounter;\n\tmy->all = _all;\n\tmy->allOffset = _allOffset;\n\tmy->filtered = _filtered;\n\tmy->filteredOffset = _filteredOffset;\n\tmy->wasLoading = (_loadRequestId != 0);\n\tresult->controllerState = std::move(my);\n\treturn result;\n}\n\nvoid Controller::restoreState(std::unique_ptr<PeerListState> state) {\n\tauto typeErasedState = state\n\t\t? state->controllerState.get()\n\t\t: nullptr;\n\tif (const auto my = dynamic_cast<SavedState*>(typeErasedState)) {\n\t\tif (const auto requestId = base::take(_loadRequestId)) {\n\t\t\t_api.request(requestId).cancel();\n\t\t}\n\t\t_shownReaction = my->shownReaction;\n\t\t_idsMap = std::move(my->idsMap);\n\t\t_idsCounter = my->idsCounter;\n\t\t_all = std::move(my->all);\n\t\t_allOffset = std::move(my->allOffset);\n\t\t_filtered = std::move(my->filtered);\n\t\t_filteredOffset = std::move(my->filteredOffset);\n\t\tif (my->wasLoading) {\n\t\t\tloadMoreRows();\n\t\t}\n\t\tPeerListController::restoreState(std::move(state));\n\t\tif (delegate()->peerListFullRowsCount()) {\n\t\t\tsetDescriptionText(QString());\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t}\n}\n\nvoid Controller::loadMore(const ReactionId &reaction) {\n\tif (reaction.emoji() == u\"read\"_q) {\n\t\tloadMore(ReactionId());\n\t\treturn;\n\t} else if (reaction.empty() && _allOffset.isEmpty() && !_all.empty()) {\n\t\treturn;\n\t}\n\t_api.request(_loadRequestId).cancel();\n\n\tconst auto &offset = reaction.empty()\n\t\t? _allOffset\n\t\t: _filteredOffset;\n\n\tusing Flag = MTPmessages_GetMessageReactionsList::Flag;\n\tconst auto flags = Flag(0)\n\t\t| (offset.isEmpty() ? Flag(0) : Flag::f_offset)\n\t\t| (reaction.empty() ? Flag(0) : Flag::f_reaction);\n\t_loadRequestId = _api.request(MTPmessages_GetMessageReactionsList(\n\t\tMTP_flags(flags),\n\t\t_peer->input(),\n\t\tMTP_int(_itemId.msg),\n\t\tData::ReactionToMTP(reaction),\n\t\tMTP_string(offset),\n\t\tMTP_int(offset.isEmpty() ? kPerPageFirst : kPerPage)\n\t)).done([=](const MTPmessages_MessageReactionsList &result) {\n\t\t_loadRequestId = 0;\n\t\tconst auto filtered = !reaction.empty();\n\t\tconst auto shown = (reaction == _shownReaction);\n\t\tresult.match([&](const MTPDmessages_messageReactionsList &data) {\n\t\t\tconst auto sessionData = &session().data();\n\t\t\tsessionData->processUsers(data.vusers());\n\t\t\tsessionData->processChats(data.vchats());\n\t\t\t(filtered ? _filteredOffset : _allOffset)\n\t\t\t\t= data.vnext_offset().value_or_empty();\n\t\t\tfor (const auto &reaction : data.vreactions().v) {\n\t\t\t\treaction.match([&](const MTPDmessagePeerReaction &data) {\n\t\t\t\t\tconst auto peer = sessionData->peerLoaded(\n\t\t\t\t\t\tpeerFromMTP(data.vpeer_id()));\n\t\t\t\t\tconst auto reaction = Data::ReactionFromMTP(\n\t\t\t\t\t\tdata.vreaction());\n\t\t\t\t\tif (peer && (!shown || appendRow(peer, reaction))) {\n\t\t\t\t\t\tif (filtered) {\n\t\t\t\t\t\t\t_filtered.emplace_back(peer);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t_all.emplace_back(peer, reaction);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t\tif (shown) {\n\t\t\tsetDescriptionText(QString());\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t}).send();\n}\n\nvoid Controller::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto window = _window;\n\tconst auto peer = row->peer();\n\tcrl::on_main(window, [=] {\n\t\twindow->showPeerInfo(peer);\n\t});\n}\n\nbool Controller::appendRow(not_null<PeerData*> peer, ReactionId reaction) {\n\tif (delegate()->peerListFindRow(id(peer, reaction))) {\n\t\treturn false;\n\t}\n\tdelegate()->peerListAppendRow(createRow(peer, reaction));\n\treturn true;\n}\n\nstd::unique_ptr<PeerListRow> Controller::createRow(\n\t\tnot_null<PeerData*> peer,\n\t\tReactionId reaction) const {\n\treturn std::make_unique<Row>(\n\t\tid(peer, reaction),\n\t\tpeer,\n\t\t_factory,\n\t\tData::ReactionEntityData(reaction),\n\t\t[=](Row *row) { delegate()->peerListUpdateRow(row); },\n\t\t[=] { return _window->parentController()->isGifPausedAtLeastFor(\n\t\t\tWindow::GifPauseReason::Layer); });\n}\n\n} // namespace\n\nData::ReactionId DefaultSelectedTab(\n\t\tnot_null<HistoryItem*> item,\n\t\tstd::shared_ptr<Api::WhoReadList> whoReadIds) {\n\treturn DefaultSelectedTab(item, {}, std::move(whoReadIds));\n}\n\nData::ReactionId DefaultSelectedTab(\n\t\tnot_null<HistoryItem*> item,\n\t\tData::ReactionId selected,\n\t\tstd::shared_ptr<Api::WhoReadList> whoReadIds) {\n\tconst auto proj = &Data::MessageReaction::id;\n\tif (!ranges::contains(item->reactions(), selected, proj)) {\n\t\tselected = {};\n\t}\n\treturn (selected.empty() && whoReadIds && !whoReadIds->list.empty())\n\t\t? Data::ReactionId{ u\"read\"_q }\n\t\t: selected;\n}\n\nnot_null<Tabs*> CreateReactionsTabs(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionNavigation*> window,\n\t\tFullMsgId itemId,\n\t\tData::ReactionId selected,\n\t\tstd::shared_ptr<Api::WhoReadList> whoReadIds) {\n\tconst auto item = window->session().data().message(itemId);\n\tauto map = item\n\t\t? item->reactions()\n\t\t: std::vector<Data::MessageReaction>();\n\tif (whoReadIds && !whoReadIds->list.empty()) {\n\t\tmap.push_back({\n\t\t\t.id = Data::ReactionId{ u\"read\"_q },\n\t\t\t.count = int(whoReadIds->list.size()),\n\t\t});\n\t}\n\treturn CreateTabs(\n\t\tparent,\n\t\tData::ReactedMenuFactory(&window->session()),\n\t\t[=] { return window->parentController()->isGifPausedAtLeastFor(\n\t\t\tWindow::GifPauseReason::Layer); },\n\t\tmap,\n\t\tselected,\n\t\twhoReadIds ? whoReadIds->type : Ui::WhoReadType::Reacted);\n}\n\nPreparedFullList FullListController(\n\t\tnot_null<Window::SessionNavigation*> window,\n\t\tFullMsgId itemId,\n\t\tData::ReactionId selected,\n\t\tstd::shared_ptr<Api::WhoReadList> whoReadIds) {\n\tExpects(IsServerMsgId(itemId.msg));\n\n\tconst auto tab = std::make_shared<\n\t\trpl::event_stream<Data::ReactionId>>();\n\treturn {\n\t\t.controller = std::make_unique<Controller>(\n\t\t\twindow,\n\t\t\titemId,\n\t\t\tselected,\n\t\t\ttab->events(),\n\t\t\twhoReadIds),\n\t\t.switchTab = [=](Data::ReactionId id) { tab->fire_copy(id); },\n\t};\n}\n\n} // namespace HistoryView::Reactions\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nclass HistoryItem;\nclass PeerListController;\n\nnamespace Data {\nstruct ReactionId;\n} // namespace Data\n\nnamespace Api {\nstruct WhoReadList;\n} // namespace Api\n\nnamespace Window {\nclass SessionController;\nclass SessionNavigation;\n} // namespace Window\n\nnamespace Ui {\nclass BoxContent;\n} // namespace Ui\n\nnamespace HistoryView::Reactions {\n\n[[nodiscard]] Data::ReactionId DefaultSelectedTab(\n\tnot_null<HistoryItem*> item,\n\tstd::shared_ptr<Api::WhoReadList> whoReadIds);\n\n[[nodiscard]] Data::ReactionId DefaultSelectedTab(\n\tnot_null<HistoryItem*> item,\n\tData::ReactionId selected,\n\tstd::shared_ptr<Api::WhoReadList> whoReadIds = nullptr);\n\nstruct Tabs;\n[[nodiscard]] not_null<Tabs*> CreateReactionsTabs(\n\tnot_null<QWidget*> parent,\n\tnot_null<Window::SessionNavigation*> window,\n\tFullMsgId itemId,\n\tData::ReactionId selected,\n\tstd::shared_ptr<Api::WhoReadList> whoReadIds);\n\nstruct PreparedFullList {\n\tstd::unique_ptr<PeerListController> controller;\n\tFn<void(Data::ReactionId)> switchTab;\n};\n[[nodiscard]] PreparedFullList FullListController(\n\tnot_null<Window::SessionNavigation*> window,\n\tFullMsgId itemId,\n\tData::ReactionId selected,\n\tstd::shared_ptr<Api::WhoReadList> whoReadIds = nullptr);\n\n} // namespace HistoryView::Reactions\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/reactions/history_view_reactions_selector.h\"\n\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/platform/ui_platform_utility.h\"\n#include \"ui/integration.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_send.h\"\n#include \"chat_helpers/emoji_list_widget.h\"\n#include \"chat_helpers/stickers_list_footer.h\"\n#include \"chat_helpers/stickers_list_widget.h\"\n#include \"window/window_session_controller.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"mainwidget.h\"\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_chat.h\"\n\nnamespace HistoryView::Reactions {\nnamespace {\n\nconstexpr auto kExpandDuration = crl::time(300);\nconstexpr auto kScaleDuration = crl::time(120);\nconstexpr auto kFullDuration = kExpandDuration + kScaleDuration;\nconstexpr auto kExpandDelay = crl::time(40);\nconstexpr auto kDefaultColumns = 8;\nconstexpr auto kMinNonTransparentColumns = 7;\n\nclass StripEmoji final : public Ui::Text::CustomEmoji {\npublic:\n\tStripEmoji(\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji> wrapped,\n\t\tnot_null<Strip*> strip,\n\t\tQPoint shift,\n\t\tint index);\n\n\tint width() override;\n\tQString entityData() override;\n\tvoid paint(QPainter &p, const Context &context) override;\n\tvoid unload() override;\n\tbool ready() override;\n\tbool readyInDefaultState() override;\n\nprivate:\n\tconst std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;\n\tconst not_null<Strip*> _strip;\n\tconst QPoint _shift;\n\tconst int _index = 0;\n\tbool _switched = false;\n\n};\n\nStripEmoji::StripEmoji(\n\tstd::unique_ptr<Ui::Text::CustomEmoji> wrapped,\n\tnot_null<Strip*> strip,\n\tQPoint shift,\n\tint index)\n: _wrapped(std::move(wrapped))\n, _strip(strip)\n, _shift(shift)\n, _index(index) {\n}\n\nint StripEmoji::width() {\n\treturn _wrapped->width();\n}\n\nQString StripEmoji::entityData() {\n\treturn _wrapped->entityData();\n}\n\nvoid StripEmoji::paint(QPainter &p, const Context &context) {\n\tif (_switched) {\n\t\t_wrapped->paint(p, context);\n\t} else if (_wrapped->readyInDefaultState()\n\t\t&& _strip->inDefaultState(_index)) {\n\t\t_switched = true;\n\t\t_wrapped->paint(p, context);\n\t} else {\n\t\t_strip->paintOne(p, _index, context.position + _shift, 1.);\n\t}\n}\n\nvoid StripEmoji::unload() {\n\t_wrapped->unload();\n\t_switched = true;\n}\n\nbool StripEmoji::ready() {\n\treturn _wrapped->ready();\n}\n\nbool StripEmoji::readyInDefaultState() {\n\treturn _wrapped->readyInDefaultState();\n}\n\n} // namespace\n\nUnifiedFactoryOwner::UnifiedFactoryOwner(\n\tnot_null<Main::Session*> session,\n\tconst std::vector<Data::Reaction> &reactions,\n\tStrip *strip)\n: _session(session)\n, _strip(strip) {\n\tauto index = 0;\n\tconst auto inStrip = _strip ? _strip->count() : 0;\n\t_unifiedIdsList.reserve(reactions.size());\n\tfor (const auto &reaction : reactions) {\n\t\t_unifiedIdsList.push_back(reaction.selectAnimation->id);\n\n\t\tconst auto unifiedId = _unifiedIdsList.back();\n\t\tif (unifiedId != reaction.id.custom()) {\n\t\t\t_defaultReactionIds.emplace(unifiedId, reaction.id);\n\t\t}\n\t\tif (index + 1 < inStrip) {\n\t\t\t_defaultReactionInStripMap.emplace(unifiedId, index++);\n\t\t}\n\t}\n\n\t_stripPaintOneShift = [&] {\n\t\t// See EmojiListWidget custom emoji position resolving.\n\t\tconst auto size = st::reactStripSize;\n\t\tconst auto area = st::emojiPanArea;\n\t\tconst auto areaPosition = QPoint(\n\t\t\t(size - area.width()) / 2,\n\t\t\t(size - area.height()) / 2);\n\t\tconst auto esize = Ui::Emoji::GetSizeLarge() / style::DevicePixelRatio();\n\t\tconst auto innerPosition = QPoint(\n\t\t\t(area.width() - esize) / 2,\n\t\t\t(area.height() - esize) / 2);\n\t\tconst auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);\n\t\tconst auto customSkip = (esize - customSize) / 2;\n\t\tconst auto customPosition = QPoint(customSkip, customSkip);\n\t\treturn areaPosition + innerPosition + customPosition;\n\t}();\n\n\t_defaultReactionShift = QPoint(\n\t\t(st::reactStripSize - st::reactStripImage) / 2,\n\t\t(st::reactStripSize - st::reactStripImage) / 2\n\t) - _stripPaintOneShift;\n}\n\nData::ReactionId UnifiedFactoryOwner::lookupReactionId(\n\t\tDocumentId unifiedId) const {\n\tconst auto i = _defaultReactionIds.find(unifiedId);\n\treturn (i != end(_defaultReactionIds))\n\t\t? i->second\n\t\t: Data::ReactionId{ unifiedId };\n}\n\nUnifiedFactoryOwner::RecentFactory UnifiedFactoryOwner::factory() {\n\treturn [=](DocumentId id, Fn<void()> repaint)\n\t-> std::unique_ptr<Ui::Text::CustomEmoji> {\n\t\tconst auto tag = Data::CustomEmojiManager::SizeTag::Large;\n\t\tconst auto sizeOverride = st::reactStripImage;\n\t\tconst auto i = _defaultReactionIds.find(id);\n\t\tconst auto isDefaultReaction = (i != end(_defaultReactionIds))\n\t\t\t&& !i->second.custom();\n\t\tconst auto manager = &_session->data().customEmojiManager();\n\t\tauto result = isDefaultReaction\n\t\t\t? std::make_unique<Ui::Text::ShiftedEmoji>(\n\t\t\t\tmanager->create(id, std::move(repaint), tag, sizeOverride),\n\t\t\t\t_defaultReactionShift)\n\t\t\t: manager->create(id, std::move(repaint), tag);\n\t\tconst auto j = _defaultReactionInStripMap.find(id);\n\t\tif (j != end(_defaultReactionInStripMap)) {\n\t\t\tAssert(_strip != nullptr);\n\t\t\treturn std::make_unique<StripEmoji>(\n\t\t\t\tstd::move(result),\n\t\t\t\t_strip,\n\t\t\t\t-_stripPaintOneShift,\n\t\t\t\tj->second);\n\t\t}\n\t\treturn result;\n\t};\n}\n\nSelector::Selector(\n\tnot_null<QWidget*> parent,\n\tconst style::EmojiPan &st,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst Data::PossibleItemReactionsRef &reactions,\n\tTextWithEntities about,\n\tFn<void(bool fast)> close,\n\tIconFactory iconFactory,\n\tFn<bool()> paused,\n\tbool child)\n: Selector(\n\tparent,\n\tst,\n\tstd::move(show),\n\treactions,\n\t(reactions.customAllowed\n\t\t? ChatHelpers::EmojiListMode::FullReactions\n\t\t: reactions.stickers.empty()\n\t\t? ChatHelpers::EmojiListMode::RecentReactions\n\t\t: ChatHelpers::EmojiListMode::MessageEffects),\n\t{},\n\tstd::move(about),\n\tstd::move(iconFactory),\n\tstd::move(paused),\n\tstd::move(close),\n\tchild) {\n}\n\n#if 0 // not ready\nSelector::Selector(\n\tnot_null<QWidget*> parent,\n\tconst style::EmojiPan &st,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tChatHelpers::EmojiListMode mode,\n\tstd::vector<DocumentId> recent,\n\tFn<void(bool fast)> close,\n\tbool child)\n: Selector(\n\tparent,\n\tst,\n\tstd::move(show),\n\t{ .customAllowed = true },\n\tmode,\n\tstd::move(recent),\n\t{},\n\tnullptr,\n\tclose,\n\tchild) {\n}\n#endif\n\nSelector::Selector(\n\tnot_null<QWidget*> parent,\n\tconst style::EmojiPan &st,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst Data::PossibleItemReactionsRef &reactions,\n\tChatHelpers::EmojiListMode mode,\n\tstd::vector<DocumentId> recent,\n\tTextWithEntities about,\n\tIconFactory iconFactory,\n\tFn<bool()> paused,\n\tFn<void(bool fast)> close,\n\tbool child)\n: RpWidget(parent)\n, _st(st)\n, _show(std::move(show))\n, _reactions(reactions)\n, _recent(std::move(recent))\n, _listMode(mode)\n, _paused(std::move(paused))\n, _jumpedToPremium([=] { close(false); })\n, _cachedRound(\n\tQSize(2 * st::reactStripSkip + st::reactStripSize, st::reactStripHeight),\n\tst::reactionCornerShadow,\n\tst::reactStripHeight)\n, _strip(std::make_unique<Strip>(\n\t_st,\n\tQRect(0, 0, st::reactStripSize, st::reactStripSize),\n\tst::reactStripImage,\n\tcrl::guard(this, [=] { update(_inner); }),\n\tstd::move(iconFactory)))\n, _about(about.empty()\n\t? nullptr\n\t: std::make_unique<Ui::FlatLabel>(\n\t\tthis,\n\t\trpl::single(about),\n\t\t_st.about))\n, _size(st::reactStripSize)\n, _skipx(countSkipLeft())\n, _skipy((st::reactStripHeight - st::reactStripSize) / 2) {\n\tsetMouseTracking(true);\n\n\tif (_about) {\n\t\t_about->setClickHandlerFilter([=](const auto &...) {\n\t\t\t_escapes.fire({});\n\t\t\treturn true;\n\t\t});\n\t}\n\n\t_useTransparency = child || Ui::Platform::TranslucentWindowsSupported();\n}\n\nSelector::~Selector() = default;\n\nbool Selector::useTransparency() const {\n\treturn _useTransparency;\n}\n\nint Selector::recentCount() const {\n\treturn int(_strip ? _reactions.recent.size() : _recent.size());\n}\n\nint Selector::countSkipLeft() const {\n\tconst auto addedToMax = _reactions.customAllowed;\n\tconst auto max = recentCount() + (addedToMax ? 1 : 0);\n\treturn std::max(\n\t\t(st::reactStripMinWidth - (max * _size)) / 2,\n\t\tst::reactStripSkip);\n}\n\nint Selector::countWidth(int desiredWidth, int maxWidth) {\n\tconst auto addedToMax = _reactions.customAllowed;\n\tconst auto max = recentCount() + (addedToMax ? 1 : 0);\n\tconst auto desiredColumns = std::max(\n\t\t(desiredWidth - 2 * _skipx + _size - 1) / _size,\n\t\tkMinNonTransparentColumns);\n\tconst auto possibleColumns = std::min(\n\t\tdesiredColumns,\n\t\t(maxWidth - 2 * _skipx) / _size);\n\t_columns = _strip ? std::min(possibleColumns, max) : kDefaultColumns;\n\t_small = (possibleColumns - _columns > 1);\n\t_recentRows = (recentCount() + _columns - 1) / _columns;\n\tconst auto added = (_columns < max || _reactions.customAllowed)\n\t\t? Strip::AddedButton::Expand\n\t\t: Strip::AddedButton::None;\n\tif (_strip) {\n\t\tconst auto &real = _reactions.recent;\n\t\tauto list = std::vector<not_null<const Data::Reaction*>>();\n\t\tlist.reserve(_columns);\n\t\tif (const auto cut = max - _columns) {\n\t\t\tconst auto from = begin(real);\n\t\t\tconst auto till = end(real) - (cut + (addedToMax ? 0 : 1));\n\t\t\tfor (auto i = from; i != till; ++i) {\n\t\t\t\tlist.push_back(&*i);\n\t\t\t}\n\t\t} else {\n\t\t\tfor (const auto &reaction : real) {\n\t\t\t\tlist.push_back(&reaction);\n\t\t\t}\n\t\t}\n\t\t_strip->applyList(list, added);\n\t\t_strip->clearAppearAnimations(false);\n\t}\n\treturn std::max(2 * _skipx + _columns * _size, desiredWidth);\n}\n\nint Selector::effectPreviewHeight() const {\n\tif (_listMode != ChatHelpers::EmojiListMode::MessageEffects) {\n\t\treturn 0;\n\t}\n\tconst auto extend = Ui::BoxShadow::ExtendFor(st::previewMenu.shadow);\n\treturn extend.top()\n\t\t+ HistoryView::Sticker::MessageEffectSize().height()\n\t\t+ st::effectPreviewSend.height\n\t\t+ extend.bottom();\n}\n\nQMargins Selector::marginsForShadow() const {\n\tconst auto line = st::lineWidth;\n\treturn useTransparency()\n\t\t? st::reactionCornerShadow\n\t\t: QMargins(line, line, line, line);\n}\n\nint Selector::extendTopForCategories() const {\n\treturn _reactions.customAllowed ? _st.footer : 0;\n}\n\nint Selector::extendTopForCategoriesAndAbout(int width) const {\n\tif (_about) {\n\t\tconst auto padding = _st.aboutPadding;\n\t\tconst auto available = width - padding.left() - padding.right();\n\t\tconst auto countAboutHeight = [&](int width) {\n\t\t\t_about->resizeToWidth(width);\n\t\t\treturn _about->height();\n\t\t};\n\t\tconst auto desired = Ui::FindNiceTooltipWidth(\n\t\t\tstd::min(available, _st.about.minWidth * 2),\n\t\t\tavailable,\n\t\t\tcountAboutHeight);\n\n\t\t_about->resizeToWidth(desired);\n\t\t_aboutExtend = padding.top() + _about->height() + padding.bottom();\n\t} else {\n\t\t_aboutExtend = 0;\n\t}\n\treturn std::max(extendTopForCategories(), _aboutExtend);\n}\n\nint Selector::opaqueExtendTopAbout(int width) const {\n\tif (_about) {\n\t\tconst auto padding = _st.aboutPadding;\n\t\tconst auto available = width - padding.left() - padding.right();\n\t\tconst auto countAboutHeight = [&](int width) {\n\t\t\t_about->resizeToWidth(width);\n\t\t\treturn _about->height();\n\t\t};\n\t\tconst auto desired = Ui::FindNiceTooltipWidth(\n\t\t\tstd::min(available, _st.about.minWidth * 2),\n\t\t\tavailable,\n\t\t\tcountAboutHeight);\n\n\t\t_about->resizeToWidth(desired);\n\t\t_aboutExtend = padding.top() + _about->height() + padding.bottom();\n\t} else {\n\t\t_aboutExtend = 0;\n\t}\n\treturn _aboutExtend;\n}\n\nvoid Selector::setOpaqueHeightExpand(int expand, Fn<void(int)> apply) {\n\t_opaqueHeightExpand = expand;\n\t_opaqueApplyHeightExpand = std::move(apply);\n}\n\nint Selector::minimalHeight(int fullWidth) const {\n\tauto inner = _recentRows * _size;\n\tif (const auto stickers = int(_reactions.stickers.size())) {\n\t\t// See StickersListWidget.\n\t\tconst auto listWidth = fullWidth\n\t\t\t- marginsForShadow().left()\n\t\t\t- marginsForShadow().right()\n\t\t\t- _st.margin.left()\n\t\t\t- _st.margin.right();\n\t\tconst auto availableWidth = listWidth\n\t\t\t- (st::stickerPanPadding - _st.margin.left());\n\t\tconst auto min = st::stickerEffectWidthMin;\n\t\tif (const auto columns = availableWidth / min) {\n\t\t\tconst auto rows = (stickers + columns - 1) / columns;\n\t\t\tconst auto singleWidth = availableWidth / columns;\n\t\t\tconst auto singleHeight = singleWidth;\n\t\t\tconst auto stickersHeight = rows * singleHeight;\n\t\t\tinner += _st.header + stickersHeight;\n\t\t}\n\t}\n\tif (_listMode == ChatHelpers::EmojiListMode::MessageEffects) {\n\t\tinner += _st.searchMargin.top()\n\t\t\t+ _st.search.height\n\t\t\t+ _st.searchMargin.bottom();\n\t}\n\treturn _skipy\n\t\t+ std::min(inner, st::emojiPanMinHeight)\n\t\t+ st::emojiPanRadius\n\t\t+ _st.padding.bottom();\n}\n\nvoid Selector::setSpecialExpandTopSkip(int skip) {\n\t_specialExpandTopSkip = skip;\n}\n\nvoid Selector::setBubbleUp(bool bubbleUp) {\n\tif (_bubbleUp == bubbleUp) {\n\t\treturn;\n\t}\n\t_bubbleUp = bubbleUp;\n}\n\nvoid Selector::initGeometry(int innerTop) {\n\tconst auto margins = marginsForShadow();\n\tconst auto parent = parentWidget()->rect();\n\tconst auto innerWidth = 2 * _skipx + _columns * _size;\n\tconst auto innerHeight = st::reactStripHeight;\n\tconst auto width = _useTransparency\n\t\t? (innerWidth + margins.left() + margins.right())\n\t\t: parent.width();\n\tconst auto forAbout = width - margins.left() - margins.right();\n\t_collapsedTopSkip = _useTransparency\n\t\t? (extendTopForCategoriesAndAbout(forAbout) + _specialExpandTopSkip)\n\t\t: opaqueExtendTopAbout(forAbout);\n\t_topAddOnExpand = _collapsedTopSkip - _aboutExtend;\n\tconst auto height = margins.top()\n\t\t+ _aboutExtend\n\t\t+ innerHeight\n\t\t+ margins.bottom();\n\tconst auto left = style::RightToLeft() ? 0 : (parent.width() - width);\n\tconst auto top = innerTop\n\t\t- margins.top()\n\t\t- (_useTransparency ? _collapsedTopSkip : 0);\n\tconst auto add = _useTransparency\n\t\t? (_st.icons.stripBubble.height() - margins.bottom())\n\t\t: 0;\n\t_outer = QRect(0, _collapsedTopSkip - _aboutExtend, width, height);\n\t_outerWithBubble = _outer.marginsAdded({ 0, 0, 0, add });\n\tsetGeometry(_outerWithBubble.marginsAdded(\n\t\t{ 0, _outer.y(), 0, 0}\n\t).translated(left, top));\n\t_inner = _outer.marginsRemoved(\n\t\tmargins + QMargins{ 0, _aboutExtend, 0, 0 });\n\tif (_about) {\n\t\t_about->move(\n\t\t\t_inner.x() + (_inner.width() - _about->width()) / 2,\n\t\t\t_outer.y()\n\t\t\t\t+ margins.top()\n\t\t\t\t+ _st.aboutPadding.top()\n\t\t\t\t- skipYBubbleUpShift());\n\t\t_aboutCache = Ui::GrabWidgetToImage(_about.get());\n\t}\n\n\tif (!_strip) {\n\t\texpand();\n\t}\n}\n\nvoid Selector::beforeDestroy() {\n\tif (_list) {\n\t\t_list->beforeHiding();\n\t}\n}\n\nrpl::producer<> Selector::escapes() const {\n\treturn _escapes.events();\n}\n\nvoid Selector::updateShowState(\n\t\tfloat64 progress,\n\t\tfloat64 opacity,\n\t\tbool appearing,\n\t\tbool toggling) {\n\tif (_useTransparency\n\t\t&& _appearing\n\t\t&& !appearing\n\t\t&& !_paintBuffer.isNull()) {\n\t\tpaintBackgroundToBuffer();\n\t\tif (_about && _about->isHidden()) {\n\t\t\t_about->show();\n\t\t}\n\t} else if (_useTransparency\n\t\t&& !_appearing\n\t\t&& appearing\n\t\t&& _about) {\n\t\t_about->hide();\n\t}\n\t_appearing = appearing;\n\t_toggling = toggling;\n\t_appearProgress = progress;\n\t_appearOpacity = opacity;\n\tif (_appearing && isHidden()) {\n\t\tshow();\n\t\traise();\n\t} else if (_toggling && !isHidden()) {\n\t\thide();\n\t}\n\tif (!_appearing && !_low) {\n\t\t_low = true;\n\t\tlower();\n\t}\n\tupdate();\n}\n\nint Selector::countAppearedWidth(float64 progress) const {\n\treturn anim::interpolate(_skipx * 2 + _size, _inner.width(), progress);\n}\n\nvoid Selector::paintAppearing(QPainter &p) {\n\tExpects(_strip != nullptr);\n\n\tp.setOpacity(_appearOpacity);\n\tconst auto factor = style::DevicePixelRatio();\n\tif (_paintBuffer.size() != _outerWithBubble.size() * factor) {\n\t\t_paintBuffer = _cachedRound.PrepareImage(_outerWithBubble.size());\n\t}\n\t_paintBuffer.fill(_st.bg->c);\n\tauto q = QPainter(&_paintBuffer);\n\tconst auto margins = marginsForShadow();\n\tconst auto appearedWidth = countAppearedWidth(_appearProgress);\n\tconst auto fullWidth = _inner.x() + appearedWidth + margins.right();\n\tconst auto size = QSize(fullWidth, _outer.height());\n\n\tq.translate(_inner.topLeft() - QPoint(0, _outer.y()));\n\t_strip->paint(\n\t\tq,\n\t\t{ _skipx, _skipy - skipYBubbleUpShift() },\n\t\t{ _size, 0 },\n\t\t{ 0, 0, appearedWidth, _inner.height() },\n\t\t1.,\n\t\tfalse);\n\n\t_cachedRound.setBackgroundColor(_st.bg->c);\n\t_cachedRound.setShadowColor(st::shadowFg->c);\n\tq.translate(QPoint(0, _outer.y()) - _inner.topLeft());\n\tconst auto radius = st::reactStripHeight / 2;\n\t_cachedRound.overlayExpandedBorder(\n\t\tq,\n\t\tsize,\n\t\t_appearProgress,\n\t\tradius,\n\t\tradius,\n\t\t1.);\n\tq.setCompositionMode(QPainter::CompositionMode_Source);\n\tq.fillRect(\n\t\tQRect{ 0, size.height(), width(), height() - size.height() },\n\t\tQt::transparent);\n\tq.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\tpaintBubble(q, appearedWidth);\n\tq.end();\n\n\tp.drawImage(\n\t\t_outer.topLeft(),\n\t\t_paintBuffer,\n\t\tQRect(QPoint(), QSize(fullWidth, height()) * factor));\n\n\tconst auto aboutRight = _inner.x() + appearedWidth;\n\tif (_about && _about->isHidden() && aboutRight > _about->x()) {\n\t\tconst auto aboutWidth = aboutRight - _about->x();\n\t\tp.drawImage(\n\t\t\t_about->geometry().topLeft(),\n\t\t\t_aboutCache,\n\t\t\tQRect(QPoint(), QSize(aboutWidth, _about->height()) * factor));\n\t}\n}\n\nvoid Selector::paintBackgroundToBuffer() {\n\tif (!_useTransparency) {\n\t\treturn;\n\t}\n\tconst auto factor = style::DevicePixelRatio();\n\tif (_paintBuffer.size() != _outerWithBubble.size() * factor) {\n\t\t_paintBuffer = _cachedRound.PrepareImage(_outerWithBubble.size());\n\t}\n\t_paintBuffer.fill(Qt::transparent);\n\n\t_cachedRound.setBackgroundColor(_st.bg->c);\n\t_cachedRound.setShadowColor(st::shadowFg->c);\n\n\tauto p = QPainter(&_paintBuffer);\n\tif (_bubbleUp) {\n\t\tconst auto centerY = _paintBuffer.height() / (2. * factor);\n\t\tp.translate(0, centerY);\n\t\tp.scale(1., -1.);\n\t\tp.translate(0, -centerY);\n\t}\n\tconst auto radius = _inner.height() / 2.;\n\tconst auto frame = _cachedRound.validateFrame(0, 1., radius);\n\tconst auto outer = _outer.translated(0, -_outer.y());\n\tconst auto fill = _cachedRound.FillWithImage(p, outer, frame);\n\tif (!fill.isEmpty()) {\n\t\tp.fillRect(fill, _st.bg);\n\t}\n\tpaintBubble(p, _inner.width());\n}\n\nint Selector::skipYBubbleUpShift() const {\n\tif (!_bubbleUp) {\n\t\treturn 0;\n\t}\n\treturn -_st.icons.stripBubble.height() + marginsForShadow().bottom();\n}\n\nvoid Selector::paintCollapsed(QPainter &p) {\n\tExpects(_strip != nullptr);\n\n\tif (_useTransparency) {\n\t\tif (_paintBuffer.isNull()) {\n\t\t\tpaintBackgroundToBuffer();\n\t\t}\n\t\tp.drawImage(_outer.topLeft(), _paintBuffer);\n\t} else {\n\t\tp.fillRect(_outer.marginsRemoved(marginsForShadow()), _st.bg);\n\t}\n\t_strip->paint(\n\t\tp,\n\t\t_inner.topLeft() + QPoint(_skipx, _skipy - skipYBubbleUpShift()),\n\t\t{ _size, 0 },\n\t\t_inner,\n\t\t1.,\n\t\tfalse);\n}\n\nvoid Selector::paintExpanding(Painter &p, float64 progress) {\n\tconst auto rects = updateExpandingRects(progress);\n\tpaintExpandingBg(p, rects);\n\tprogress /= kFullDuration;\n\tif (_about && !_aboutCache.isNull()) {\n\t\tp.setClipping(false);\n\t\tp.setOpacity((1. - progress) * (1. - progress));\n\t\tconst auto y = _about->y() - _outer.y() + rects.outer.y();\n\t\tp.drawImage(_about->x(), y, _aboutCache);\n\t\tp.setOpacity(1.);\n\t}\n\tif (_footer) {\n\t\t_footer->paintExpanding(\n\t\t\tp,\n\t\t\trects.categories,\n\t\t\trects.radius,\n\t\t\tRectPart::BottomRight);\n\t}\n\t_list->paintExpanding(\n\t\tp,\n\t\trects.list.marginsRemoved(_st.margin),\n\t\trects.finalBottom,\n\t\trects.expanding,\n\t\tprogress,\n\t\tRectPart::TopRight);\n\tpaintFadingExpandIcon(p, progress);\n}\n\nSelector::ExpandingRects Selector::updateExpandingRects(float64 progress) {\n\tprogress = (progress >= kExpandDuration)\n\t\t? 1.\n\t\t: (progress / kExpandDuration);\n\tconstexpr auto kFramesCount = Ui::RoundAreaWithShadow::kFramesCount;\n\tconst auto frame = int(base::SafeRound(progress * (kFramesCount - 1)));\n\tconst auto radiusStart = st::reactStripHeight / 2.;\n\tconst auto radiusEnd = st::emojiPanRadius;\n\tconst auto radius = _reactions.customAllowed\n\t\t? (radiusStart + progress * (radiusEnd - radiusStart))\n\t\t: radiusStart;\n\tconst auto margins = marginsForShadow();\n\tconst auto expanding = anim::easeOutCirc(1., progress);\n\tconst auto expandUp = anim::interpolate(0, _topAddOnExpand, expanding);\n\tconst auto expandDown = anim::interpolate(\n\t\t0,\n\t\t(height() - _outer.y() - _outer.height()),\n\t\texpanding);\n\tconst auto outer = _outer.marginsAdded({ 0, expandUp, 0, expandDown });\n\tconst auto inner = outer.marginsRemoved(margins\n\t\t+ QMargins{\n\t\t\t0,\n\t\t\tanim::interpolate(_aboutExtend, 0, expanding),\n\t\t\t0,\n\t\t\t0 });\n\tconst auto list = outer.marginsRemoved(margins\n\t\t+ QMargins{\n\t\t\t0,\n\t\t\tanim::interpolate(\n\t\t\t\t_aboutExtend,\n\t\t\t\textendTopForCategories(),\n\t\t\t\texpanding),\n\t\t\t0,\n\t\t\t0 });\n\t_shadowTop = list.y();\n\tconst auto categories = list.y() - inner.y();\n\t_shadowSkip = (_useTransparency && categories < radius)\n\t\t? int(base::SafeRound(\n\t\t\tradius - sqrt(categories * (2 * radius - categories))))\n\t\t: 0;\n\n\tif (!_useTransparency && _opaqueApplyHeightExpand) {\n\t\tUi::PostponeCall(this, [=] {\n\t\t\t_opaqueApplyHeightExpand(y() + outer.y() + outer.height());\n\t\t});\n\t}\n\n\treturn {\n\t\t.categories = QRect(inner.x(), inner.y(), inner.width(), categories),\n\t\t.list = list,\n\t\t.radius = radius,\n\t\t.expanding = expanding,\n\t\t.finalBottom = height() - margins.bottom(),\n\t\t.frame = frame,\n\t\t.outer = outer,\n\t};\n}\n\nvoid Selector::paintExpandingBg(QPainter &p, const ExpandingRects &rects) {\n\tif (_useTransparency) {\n\t\tconst auto pattern = _cachedRound.validateFrame(\n\t\t\trects.frame,\n\t\t\t1.,\n\t\t\trects.radius);\n\t\tconst auto fill = _cachedRound.FillWithImage(p, rects.outer, pattern);\n\t\tif (!fill.isEmpty()) {\n\t\t\tp.fillRect(fill, _st.bg);\n\t\t}\n\t} else {\n\t\tpaintNonTransparentExpandRect(p, rects.outer - marginsForShadow());\n\t}\n}\n\nvoid Selector::paintFadingExpandIcon(QPainter &p, float64 progress) {\n\tif (progress >= 1.) {\n\t\treturn;\n\t}\n\tp.setOpacity(1. - progress);\n\tconst auto sub = anim::interpolate(0, _size / 3, progress);\n\tconst auto expandIconPosition = _inner.topLeft()\n\t\t+ QPoint(_inner.width() - _size - _skipx, _skipy);\n\tconst auto expandIconRect = QRect(\n\t\texpandIconPosition,\n\t\tQSize(_size, _size)\n\t).marginsRemoved(Margins(sub)).translated(0, -skipYBubbleUpShift());\n\tp.drawImage(expandIconRect, _expandIconCache);\n\tp.setOpacity(1.);\n}\n\nvoid Selector::paintNonTransparentExpandRect(\n\t\tQPainter &p,\n\t\tconst QRect &inner) const {\n\tp.fillRect(inner, _st.bg);\n\tp.fillRect(\n\t\tinner.x(),\n\t\tinner.y() + inner.height(),\n\t\tinner.width(),\n\t\tst::lineWidth,\n\t\tst::defaultPopupMenu.shadowFallback);\n}\n\nvoid Selector::paintExpanded(QPainter &p) {\n\tif (!_expandFinished) {\n\t\tfinishExpand();\n\t}\n\tif (_useTransparency) {\n\t\tp.drawImage(0, 0, _paintBuffer);\n\t} else {\n\t\tpaintNonTransparentExpandRect(p, rect() - marginsForShadow());\n\t}\n}\n\nvoid Selector::finishExpand() {\n\tExpects(!_expandFinished);\n\n\t_expandFinished = true;\n\tupdateExpandingRects(kExpandDuration);\n\tif (_useTransparency) {\n\t\tauto q = QPainter(&_paintBuffer);\n\t\tq.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tconst auto pattern = _cachedRound.validateFrame(\n\t\t\tkFramesCount - 1,\n\t\t\t1.,\n\t\t\tst::emojiPanRadius);\n\t\tconst auto fill = _cachedRound.FillWithImage(q, rect(), pattern);\n\t\tif (!fill.isEmpty()) {\n\t\t\tq.fillRect(fill, _st.bg);\n\t\t}\n\t}\n\tif (_footer) {\n\t\t_footer->show();\n\t}\n\t_scroll->show();\n\t_list->afterShown();\n\t_show->session().api().updateCustomEmoji();\n}\n\nvoid Selector::paintBubble(QPainter &p, int innerWidth) {\n\tconst auto &bubble = _st.icons.stripBubble;\n\tconst auto bubbleRight = std::min(\n\t\tst::reactStripBubbleRight,\n\t\t(innerWidth - bubble.width()) / 2);\n\tbubble.paint(\n\t\tp,\n\t\t_inner.x() + innerWidth - bubbleRight - bubble.width(),\n\t\t_inner.y() + _inner.height() - _outer.y(),\n\t\twidth());\n}\n\nvoid Selector::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\tif (_strip && _appearing && _useTransparency) {\n\t\tpaintAppearing(p);\n\t} else if (_strip && !_expanded) {\n\t\tpaintCollapsed(p);\n\t} else if (const auto progress = _expanding.value(kFullDuration)\n\t\t; progress < kFullDuration) {\n\t\tpaintExpanding(p, progress);\n\t} else {\n\t\tpaintExpanded(p);\n\t}\n}\n\nvoid Selector::mouseMoveEvent(QMouseEvent *e) {\n\tif (!_strip) {\n\t\treturn;\n\t}\n\tsetSelected(lookupSelectedIndex(e->pos()));\n}\n\nint Selector::lookupSelectedIndex(QPoint position) const {\n\tconst auto p = position - _inner.topLeft() - QPoint(_skipx, _skipy);\n\tconst auto max = _strip->count();\n\tconst auto index = p.x() / _size;\n\tif (p.x() >= 0 && p.y() >= 0 && p.y() < _inner.height() && index < max) {\n\t\treturn index;\n\t}\n\treturn -1;\n}\n\nvoid Selector::setSelected(int index) {\n\tExpects(_strip != nullptr);\n\n\tif (index >= 0 && _expandScheduled) {\n\t\treturn;\n\t}\n\t_strip->setSelected(index);\n\tconst auto over = (index >= 0);\n\tif (_over != over) {\n\t\t_over = over;\n\t\tsetCursor(over ? style::cur_pointer : style::cur_default);\n\t\tif (over) {\n\t\t\tUi::Integration::Instance().registerLeaveSubscription(this);\n\t\t} else {\n\t\t\tUi::Integration::Instance().unregisterLeaveSubscription(this);\n\t\t}\n\t}\n}\n\nvoid Selector::leaveEventHook(QEvent *e) {\n\tif (!_strip) {\n\t\treturn;\n\t}\n\tsetSelected(-1);\n}\n\nvoid Selector::mousePressEvent(QMouseEvent *e) {\n\tif (!_strip) {\n\t\treturn;\n\t}\n\t_pressed = lookupSelectedIndex(e->pos());\n}\n\nvoid Selector::mouseReleaseEvent(QMouseEvent *e) {\n\tif (!_strip) {\n\t\treturn;\n\t}\n\tif (_pressed != lookupSelectedIndex(e->pos())) {\n#ifdef Q_OS_UNIX\n\t\tif (!_over || e->button() != Qt::RightButton) {\n\t\t\treturn;\n\t\t}\n#else\n\t\treturn;\n#endif // !Q_OS_UNIX\n\t}\n\t_pressed = -1;\n\tconst auto selected = _strip->selected();\n\tif (selected == Strip::AddedButton::Expand) {\n\t\texpand();\n\t} else if (const auto id = std::get_if<Data::ReactionId>(&selected)) {\n\t\tif (!id->empty()) {\n\t\t\t_chosen.fire(lookupChosen(*id));\n\t\t}\n\t}\n}\n\nChosenReaction Selector::lookupChosen(const Data::ReactionId &id) const {\n\tExpects(_strip != nullptr);\n\n\tauto result = ChosenReaction{\n\t\t.id = id,\n\t};\n\tconst auto index = _strip->fillChosenIconGetIndex(result);\n\tif (result.icon.isNull()) {\n\t\treturn result;\n\t}\n\tconst auto rect = QRect(_skipx + index * _size, _skipy, _size, _size);\n\tconst auto imageSize = _strip->computeOverSize();\n\tresult.globalGeometry = mapToGlobal(QRect(\n\t\t_inner.x() + rect.x() + (rect.width() - imageSize) / 2,\n\t\t_inner.y() + rect.y() + (rect.height() - imageSize) / 2,\n\t\timageSize,\n\t\timageSize));\n\treturn result;\n}\n\nvoid Selector::preloadAllRecentsAnimations() {\n\tconst auto preload = [&](DocumentData *document) {\n\t\tconst auto view = document\n\t\t\t? document->activeMediaView()\n\t\t\t: nullptr;\n\t\tif (view) {\n\t\t\tview->checkStickerLarge();\n\t\t}\n\t};\n\tfor (const auto &reaction : _reactions.recent) {\n\t\tif (!reaction.id.custom()) {\n\t\t\tpreload(reaction.centerIcon);\n\t\t}\n\t\tpreload(reaction.aroundAnimation);\n\t}\n}\n\nvoid Selector::expand() {\n\tif (_expandScheduled) {\n\t\treturn;\n\t}\n\t_expandScheduled = true;\n\t_willExpand.fire({});\n\tpreloadAllRecentsAnimations();\n\tconst auto parent = parentWidget()->geometry();\n\tconst auto margins = marginsForShadow();\n\tconst auto heightLimit = _reactions.customAllowed\n\t\t? st::emojiPanMaxHeight\n\t\t: minimalHeight(width());\n\tconst auto opaqueAdded = _useTransparency ? 0 : _opaqueHeightExpand;\n\tconst auto willBeHeight = std::min(\n\t\tparent.height() - y() + opaqueAdded,\n\t\tmargins.top() + heightLimit + margins.bottom());\n\tconst auto additionalBottom = willBeHeight - height();\n\tconst auto additional = _specialExpandTopSkip + additionalBottom;\n\tif (additionalBottom < 0 || additional <= 0) {\n\t\treturn;\n\t} else if (additionalBottom > 0) {\n\t\tresize(width(), height() + additionalBottom);\n\t\traise();\n\t}\n\n\tcreateList();\n\tcacheExpandIcon();\n\n\t[[maybe_unused]] const auto grabbed = Ui::GrabWidget(_scroll); // clazy:exclude=unused-non-trivial-variable\n\t_list->prepareExpanding();\n\tsetSelected(-1);\n\n\tbase::call_delayed(kExpandDelay, this, [this] {\n\t\tconst auto full = kExpandDuration + kScaleDuration;\n\t\tif (_about) {\n\t\t\t_about->hide();\n\t\t}\n\t\t_expanded = true;\n\t\t_paintBuffer = _cachedRound.PrepareImage(size());\n\t\t_expanding.start([=] {\n\t\t\tupdate();\n\t\t}, 0., full, full);\n\t});\n}\n\nvoid Selector::cacheExpandIcon() {\n\tif (!_strip) {\n\t\treturn;\n\t}\n\t_expandIconCache = _cachedRound.PrepareImage({ _size, _size });\n\t_expandIconCache.fill(Qt::transparent);\n\tauto q = QPainter(&_expandIconCache);\n\t_strip->paintOne(q, _strip->count() - 1, { 0, 0 }, 1.);\n}\n\nvoid Selector::createList() {\n\tusing namespace ChatHelpers;\n\t_unifiedFactoryOwner = std::make_unique<UnifiedFactoryOwner>(\n\t\t&_show->session(),\n\t\t_strip ? _reactions.recent : std::vector<Data::Reaction>(),\n\t\t_strip.get());\n\t_scroll = Ui::CreateChild<Ui::ScrollArea>(this, !_useTransparency\n\t\t? st::emojiScroll\n\t\t: _reactions.customAllowed\n\t\t? st::reactPanelScroll\n\t\t: st::reactPanelScrollRounded);\n\t_scroll->hide();\n\n\tconst auto effects = !_reactions.stickers.empty();\n\tconst auto st = lifetime().make_state<style::EmojiPan>(_st);\n\tst->padding.setTop(_skipy);\n\tif (!_reactions.customAllowed) {\n\t\tst->bg = st::transparent;\n\t}\n\tauto lists = _scroll->setOwnedWidget(\n\t\tobject_ptr<Ui::VerticalLayout>(_scroll));\n\tauto recentList = _strip\n\t\t? _unifiedFactoryOwner->unifiedIdsList()\n\t\t: _recent;\n\tauto freeEffects = base::flat_set<DocumentId>();\n\tif (effects) {\n\t\tauto free = base::flat_set<Data::ReactionId>();\n\t\tfree.reserve(_reactions.recent.size());\n\t\tfor (const auto &reaction : _reactions.recent) {\n\t\t\tif (!reaction.premium) {\n\t\t\t\tfree.emplace(reaction.id);\n\t\t\t}\n\t\t}\n\t\tfor (const auto &id : recentList) {\n\t\t\tconst auto reactionId = _strip\n\t\t\t\t? _unifiedFactoryOwner->lookupReactionId(id)\n\t\t\t\t: Data::ReactionId{ id };\n\t\t\tif (free.contains(reactionId)) {\n\t\t\t\tfreeEffects.insert(id);\n\t\t\t}\n\t\t}\n\t}\n\t_list = lists->add(\n\t\tobject_ptr<EmojiListWidget>(lists, EmojiListDescriptor{\n\t\t\t.show = _show,\n\t\t\t.mode = _listMode,\n\t\t\t.paused = _paused ? _paused : [] { return false; },\n\t\t\t.customRecentList = DocumentListToRecent(recentList),\n\t\t\t.customRecentFactory = _unifiedFactoryOwner->factory(),\n\t\t\t.freeEffects = std::move(freeEffects),\n\t\t\t.st = st,\n\t\t\t.mediaPreviewParent = this,\n\t\t\t.mediaPreviewMargins = marginsForShadow(),\n\t\t}));\n\tif (!_reactions.stickers.empty()) {\n\t\tauto descriptors = ranges::views::all(\n\t\t\t_reactions.stickers\n\t\t) | ranges::view::transform([](const Data::Reaction &reaction) {\n\t\t\treturn ChatHelpers::StickerCustomRecentDescriptor{\n\t\t\t\treaction.selectAnimation,\n\t\t\t\treaction.title\n\t\t\t};\n\t\t}) | ranges::to_vector;\n\t\t_stickers = lists->add(\n\t\t\tobject_ptr<StickersListWidget>(\n\t\t\t\tlists,\n\t\t\t\tStickersListDescriptor{\n\t\t\t\t\t.show = _show,\n\t\t\t\t\t.mode = StickersListMode::MessageEffects,\n\t\t\t\t\t.paused = _paused ? _paused : [] { return false; },\n\t\t\t\t\t.customRecentList = std::move(descriptors),\n\t\t\t\t\t.st = st,\n\t\t\t\t}));\n\t}\n\n\t_list->escapes() | rpl::start_to_stream(_escapes, _list->lifetime());\n\n\trpl::merge(\n\t\t_list->customChosen(),\n\t\t(_stickers\n\t\t\t? _stickers->chosen()\n\t\t\t: rpl::never<ChatHelpers::FileChosen>())\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\t_chosen.fire({\n\t\t\t.id = _unifiedFactoryOwner->lookupReactionId(data.document->id),\n\t\t\t.icon = data.messageSendingFrom.frame,\n\t\t\t.globalGeometry = data.messageSendingFrom.globalStartGeometry,\n\t\t});\n\t}, _list->lifetime());\n\n\t_list->jumpedToPremium(\n\t) | rpl::on_next(_jumpedToPremium, _list->lifetime());\n\n\tconst auto inner = rect().marginsRemoved(marginsForShadow());\n\tconst auto footer = _reactions.customAllowed\n\t\t? _list->createFooter().data()\n\t\t: nullptr;\n\tif ((_footer = static_cast<StickersListFooter*>(footer))) {\n\t\t_footer->setParent(this);\n\t\t_footer->hide();\n\t\t_footer->setGeometry(\n\t\t\tinner.x(),\n\t\t\tinner.y(),\n\t\t\tinner.width(),\n\t\t\t_footer->height());\n\t\t_shadowTop = _outer.y();\n\t\t_shadowSkip = _useTransparency ? (st::reactStripHeight / 2) : 0;\n\t\t_shadow = Ui::CreateChild<Ui::PlainShadow>(this);\n\t\trpl::combine(\n\t\t\t_shadowTop.value(),\n\t\t\t_shadowSkip.value()\n\t\t) | rpl::on_next([=](int top, int skip) {\n\t\t\t_shadow->setGeometry(\n\t\t\t\tinner.x() + skip,\n\t\t\t\ttop,\n\t\t\t\tinner.width() - 2 * skip,\n\t\t\t\tst::lineWidth);\n\t\t}, _shadow->lifetime());\n\t\t_shadow->show();\n\t}\n\tconst auto geometry = inner.marginsRemoved(_st.margin);\n\tlists->move(0, 0);\n\tlists->resizeToWidth(geometry.width());\n\t_list->refreshEmoji();\n\tlists->show();\n\n\tconst auto updateVisibleTopBottom = [=] {\n\t\tconst auto scrollTop = _scroll->scrollTop();\n\t\tconst auto scrollBottom = scrollTop + _scroll->height();\n\t\tlists->setVisibleTopBottom(scrollTop, scrollBottom);\n\t};\n\t_scroll->scrollTopChanges(\n\t) | rpl::on_next(updateVisibleTopBottom, lists->lifetime());\n\n\t_list->scrollToRequests(\n\t) | rpl::on_next([=](int y) {\n\t\t_scroll->scrollToY(y);\n\t\tif (_shadow) {\n\t\t\t_shadow->update();\n\t\t}\n\t}, _list->lifetime());\n\n\t_scroll->setGeometry(inner.marginsRemoved({\n\t\t_st.margin.left(),\n\t\t_footer ? _footer->height() : 0,\n\t\t0,\n\t\t0,\n\t}));\n\tif (_stickers) {\n\t\t_list->setMinimalHeight(geometry.width(), 0);\n\t\t_stickers->setMinimalHeight(geometry.width(), 0);\n\n\t\t_list->searchQueries(\n\t\t) | rpl::on_next([=](std::vector<QString> &&query) {\n\t\t\t_stickers->applySearchQuery(std::move(query));\n\t\t}, _stickers->lifetime());\n\n\t\trpl::combine(\n\t\t\t_list->heightValue(),\n\t\t\t_stickers->heightValue()\n\t\t) | rpl::on_next([=] {\n\t\t\tInvokeQueued(lists, updateVisibleTopBottom);\n\t\t}, _stickers->lifetime());\n\n\t\trpl::combine(\n\t\t\t_list->recentShownCount(),\n\t\t\t_stickers->recentShownCount()\n\t\t) | rpl::on_next([=](int emoji, int stickers) {\n\t\t\t_showEmptySearch = !emoji && !stickers;\n\t\t\t_scroll->update();\n\t\t}, _scroll->lifetime());\n\n\t\t_scroll->paintRequest() | rpl::filter([=] {\n\t\t\treturn _showEmptySearch;\n\t\t}) | rpl::on_next([=] {\n\t\t\tauto p = QPainter(_scroll);\n\t\t\tp.setPen(st::windowSubTextFg);\n\t\t\tp.setFont(st::normalFont);\n\t\t\tp.drawText(\n\t\t\t\t_scroll->rect(),\n\t\t\t\ttr::lng_effect_none(tr::now),\n\t\t\t\tstyle::al_center);\n\t\t}, _scroll->lifetime());\n\t} else {\n\t\t_list->setMinimalHeight(geometry.width(), _scroll->height());\n\t}\n\n\tupdateVisibleTopBottom();\n}\n\nbool AdjustMenuGeometryForSelector(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tQPoint desiredPosition,\n\t\tnot_null<Selector*> selector) {\n\tconst auto useTransparency = selector->useTransparency();\n\tconst auto extend = useTransparency\n\t\t? st::reactStripExtend\n\t\t: QMargins(0, st::lineWidth + st::reactStripHeight, 0, 0);\n\tconst auto added = extend.left() + extend.right();\n\tconst auto desiredWidth = menu->menu()->width() + added;\n\tconst auto maxWidth = menu->st().menu.widthMax + added;\n\tconst auto width = selector->countWidth(desiredWidth, maxWidth);\n\tconst auto margins = selector->marginsForShadow();\n\tconst auto categoriesAboutTop = selector->useTransparency()\n\t\t? selector->extendTopForCategoriesAndAbout(width)\n\t\t: selector->opaqueExtendTopAbout(width);\n\tmenu->setForceWidth(width - added);\n\tconst auto height = menu->height();\n\tconst auto fullTop = margins.top() + categoriesAboutTop + extend.top();\n\tconst auto minimalHeight = std::max(\n\t\tmargins.top() + selector->minimalHeight(width) + margins.bottom(),\n\t\tselector->effectPreviewHeight());\n\tconst auto willBeHeightWithoutBottomPadding = fullTop\n\t\t+ height\n\t\t- Ui::BoxShadow::ExtendFor(menu->st().shadow).top();\n\tconst auto additionalPaddingBottom\n\t\t= (willBeHeightWithoutBottomPadding >= minimalHeight\n\t\t\t? 0\n\t\t\t: (minimalHeight - willBeHeightWithoutBottomPadding));\n\tmenu->setAdditionalMenuPadding(QMargins(\n\t\tmargins.left() + extend.left(),\n\t\tfullTop,\n\t\tmargins.right() + extend.right(),\n\t\tadditionalPaddingBottom\n\t), QMargins(\n\t\tmargins.left(),\n\t\tmargins.top(),\n\t\tmargins.right(),\n\t\tstd::min(additionalPaddingBottom, margins.bottom())\n\t));\n\tif (!menu->prepareGeometryFor(desiredPosition)) {\n\t\treturn false;\n\t}\n\tconst auto origin = menu->preparedOrigin();\n\tconst auto expandDown = (origin == Ui::PanelAnimation::Origin::TopLeft)\n\t\t|| (origin == Ui::PanelAnimation::Origin::TopRight);\n\tif (!useTransparency) {\n\t\tconst auto expandBy = additionalPaddingBottom;\n\t\tselector->setOpaqueHeightExpand(expandBy, [=](int bottom) {\n\t\t\tconst auto add = bottom - menu->height();\n\t\t\tif (add > 0) {\n\t\t\t\tconst auto updated = menu->geometry().marginsAdded({\n\t\t\t\t\t0, expandDown ? 0 : add, 0, expandDown ? add : 0 });\n\t\t\t\tmenu->setFixedSize(updated.size());\n\t\t\t\tmenu->setGeometry(updated);\n\t\t\t}\n\t\t});\n\t\tmenu->setAdditionalMenuPadding(QMargins(\n\t\t\tmargins.left() + extend.left(),\n\t\t\tfullTop,\n\t\t\tmargins.right() + extend.right(),\n\t\t\t0\n\t\t), QMargins(\n\t\t\tmargins.left(),\n\t\t\tmargins.top(),\n\t\t\tmargins.right(),\n\t\t\t0\n\t\t));\n\t\treturn menu->prepareGeometryFor(desiredPosition);\n\t} else if (!additionalPaddingBottom || expandDown) {\n\t\treturn true;\n\t}\n\tmenu->setAdditionalMenuPadding(QMargins(\n\t\tmargins.left() + extend.left(),\n\t\tfullTop + additionalPaddingBottom,\n\t\tmargins.right() + extend.right(),\n\t\t0\n\t), QMargins(\n\t\tmargins.left(),\n\t\tmargins.top(),\n\t\tmargins.right(),\n\t\t0\n\t));\n\tselector->setSpecialExpandTopSkip(additionalPaddingBottom);\n\treturn menu->prepareGeometryFor(desiredPosition);\n}\n\n#if 0 // not ready\nAttachSelectorResult MakeJustSelectorMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tQPoint desiredPosition,\n\t\tChatHelpers::EmojiListMode mode,\n\t\tstd::vector<DocumentId> recent,\n\t\tFn<void(ChosenReaction)> chosen) {\n\tconst auto selector = Ui::CreateChild<Selector>(\n\t\tmenu.get(),\n\t\tst::reactPanelEmojiPan,\n\t\tcontroller->uiShow(),\n\t\tmode,\n\t\tstd::move(recent),\n\t\t[=](bool fast) { menu->hideMenu(fast); },\n\t\tfalse); // child\n\tif (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) {\n\t\treturn AttachSelectorResult::Failed;\n\t}\n\tif (mode != ChatHelpers::EmojiListMode::RecentReactions) {\n\t\tUi::Platform::FixPopupMenuNativeEmojiPopup(menu);\n\t}\n\tconst auto selectorInnerTop = menu->preparedPadding().top()\n\t\t- st::reactStripExtend.top();\n\tmenu->animatePhaseValue(\n\t) | rpl::on_next([=](Ui::PopupMenu::AnimatePhase phase) {\n\t\tif (phase == Ui::PopupMenu::AnimatePhase::StartHide) {\n\t\t\tselector->beforeDestroy();\n\t\t}\n\t}, selector->lifetime());\n\tselector->initGeometry(selectorInnerTop);\n\tselector->show();\n\n\tselector->chosen() | rpl::on_next([=](ChosenReaction reaction) {\n\t\tmenu->hideMenu();\n\t\tchosen(std::move(reaction));\n\t}, selector->lifetime());\n\n\tconst auto correctTop = selector->y();\n\tmenu->showStateValue(\n\t) | rpl::on_next([=](Ui::PopupMenu::ShowState state) {\n\t\tconst auto origin = menu->preparedOrigin();\n\t\tusing Origin = Ui::PanelAnimation::Origin;\n\t\tif (origin == Origin::BottomLeft || origin == Origin::BottomRight) {\n\t\t\tconst auto add = state.appearing\n\t\t\t\t? (menu->rect().marginsRemoved(\n\t\t\t\t\tmenu->preparedPadding()\n\t\t\t\t).height() - state.appearingHeight)\n\t\t\t\t: 0;\n\t\t\tselector->move(selector->x(), correctTop + add);\n\t\t}\n\t\tselector->updateShowState(\n\t\t\tstate.widthProgress * state.heightProgress,\n\t\t\tstate.opacity,\n\t\t\tstate.appearing,\n\t\t\tstate.toggling);\n\t}, selector->lifetime());\n\n\tconst auto weak = base::make_weak(controller);\n\tcontroller->enableGifPauseReason(\n\t\tWindow::GifPauseReason::MediaPreview);\n\tQObject::connect(menu.get(), &QObject::destroyed, [weak] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->disableGifPauseReason(\n\t\t\t\tWindow::GifPauseReason::MediaPreview);\n\t\t}\n\t});\n\n\treturn AttachSelectorResult::Attached;\n}\n#endif\n\nAttachSelectorResult AttachSelectorToMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tQPoint desiredPosition,\n\t\tnot_null<HistoryItem*> item,\n\t\tFn<void(ChosenReaction)> chosen,\n\t\tTextWithEntities about,\n\t\tIconFactory iconFactory) {\n\tconst auto result = AttachSelectorToMenu(\n\t\tmenu,\n\t\tdesiredPosition,\n\t\tst::reactPanelEmojiPan,\n\t\tcontroller->uiShow(),\n\t\tData::LookupPossibleReactions(item, true),\n\t\tstd::move(about),\n\t\tstd::move(iconFactory));\n\tif (!result) {\n\t\treturn result.error();\n\t}\n\tconst auto selector = *result;\n\tconst auto itemId = item->fullId();\n\n\tselector->chosen() | rpl::on_next([=](ChosenReaction reaction) {\n\t\tmenu->hideMenu();\n\t\treaction.context = itemId;\n\t\tchosen(std::move(reaction));\n\t}, selector->lifetime());\n\n\tselector->escapes() | rpl::on_next([=] {\n\t\tmenu->hideMenu();\n\t}, selector->lifetime());\n\n\tconst auto weak = base::make_weak(controller);\n\tcontroller->enableGifPauseReason(\n\t\tWindow::GifPauseReason::MediaPreview);\n\tQObject::connect(menu.get(), &QObject::destroyed, [weak] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->disableGifPauseReason(\n\t\t\t\tWindow::GifPauseReason::MediaPreview);\n\t\t}\n\t});\n\n\treturn AttachSelectorResult::Attached;\n}\n\nauto AttachSelectorToMenu(\n\tnot_null<Ui::PopupMenu*> menu,\n\tQPoint desiredPosition,\n\tconst style::EmojiPan &st,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst Data::PossibleItemReactionsRef &reactions,\n\tTextWithEntities about,\n\tIconFactory iconFactory,\n\tFn<bool()> paused)\n-> base::expected<not_null<Selector*>, AttachSelectorResult> {\n\tif (reactions.recent.empty()) {\n\t\treturn base::make_unexpected(AttachSelectorResult::Skipped);\n\t}\n\tconst auto withSearch = reactions.customAllowed;\n\tconst auto selector = Ui::CreateChild<Selector>(\n\t\tmenu.get(),\n\t\tst,\n\t\tstd::move(show),\n\t\tstd::move(reactions),\n\t\tstd::move(about),\n\t\t[=](bool fast) { menu->hideMenu(fast); },\n\t\tstd::move(iconFactory),\n\t\tstd::move(paused),\n\t\tfalse); // child\n\tif (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) {\n\t\treturn base::make_unexpected(AttachSelectorResult::Failed);\n\t}\n\tif (withSearch) {\n\t\tUi::Platform::FixPopupMenuNativeEmojiPopup(menu);\n\t}\n\tconst auto selectorInnerTop = selector->useTransparency()\n\t\t? (menu->preparedPadding().top() - st::reactStripExtend.top())\n\t\t: st::lineWidth;\n\tmenu->animatePhaseValue(\n\t) | rpl::on_next([=](Ui::PopupMenu::AnimatePhase phase) {\n\t\tif (phase == Ui::PopupMenu::AnimatePhase::StartHide) {\n\t\t\tselector->beforeDestroy();\n\t\t}\n\t}, selector->lifetime());\n\tselector->initGeometry(selectorInnerTop);\n\tselector->show();\n\n\tconst auto correctTop = selector->y();\n\tmenu->showStateValue(\n\t) | rpl::on_next([=](Ui::PopupMenu::ShowState state) {\n\t\tconst auto origin = menu->preparedOrigin();\n\t\tusing Origin = Ui::PanelAnimation::Origin;\n\t\tif (origin == Origin::BottomLeft || origin == Origin::BottomRight) {\n\t\t\tconst auto add = state.appearing\n\t\t\t\t? (menu->rect().marginsRemoved(\n\t\t\t\t\tmenu->preparedPadding()\n\t\t\t\t).height() - state.appearingHeight)\n\t\t\t\t: 0;\n\t\t\tselector->move(selector->x(), correctTop + add);\n\t\t}\n\t\tselector->updateShowState(\n\t\t\tstate.widthProgress * state.heightProgress,\n\t\t\tstate.opacity,\n\t\t\tstate.appearing,\n\t\t\tstate.toggling);\n\t}, selector->lifetime());\n\n\treturn selector;\n}\n\nTextWithEntities ItemReactionsAbout(not_null<HistoryItem*> item) {\n\treturn !item->reactionsAreTags()\n\t\t? TextWithEntities()\n\t\t: item->history()->session().premium()\n\t\t? TextWithEntities{ tr::lng_add_tag_about(tr::now) }\n\t\t: tr::lng_subscribe_tag_about(\n\t\t\ttr::now,\n\t\t\tlt_link,\n\t\t\ttr::link(\n\t\t\t\ttr::lng_subscribe_tag_link(tr::now),\n\t\t\t\tu\"internal:about_tags\"_q),\n\t\t\ttr::marked);\n}\n\n} // namespace HistoryView::Reactions\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/expected.h\"\n#include \"base/unique_qptr.h\"\n#include \"data/data_message_reactions.h\"\n#include \"history/view/reactions/history_view_reactions_strip.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/effects/round_area_with_shadow.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Data {\nstruct Reaction;\nstruct ReactionId;\n} // namespace Data\n\nnamespace ChatHelpers {\nclass Show;\nclass TabbedPanel;\nclass EmojiListWidget;\nclass StickersListWidget;\nclass StickersListFooter;\nenum class EmojiListMode;\n} // namespace ChatHelpers\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass PopupMenu;\nclass ScrollArea;\nclass PlainShadow;\nclass FlatLabel;\n} // namespace Ui\n\nnamespace HistoryView::Reactions {\n\nclass UnifiedFactoryOwner final {\npublic:\n\tusing RecentFactory = Fn<std::unique_ptr<Ui::Text::CustomEmoji>(\n\t\tDocumentId,\n\t\tFn<void()>)>;\n\n\tUnifiedFactoryOwner(\n\t\tnot_null<Main::Session*> session,\n\t\tconst std::vector<Data::Reaction> &reactions,\n\t\tStrip *strip = nullptr);\n\n\t[[nodiscard]] const std::vector<DocumentId> &unifiedIdsList() const {\n\t\treturn _unifiedIdsList;\n\t}\n\n\t[[nodiscard]] Data::ReactionId lookupReactionId(\n\t\tDocumentId unifiedId) const;\n\n\t[[nodiscard]] RecentFactory factory();\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\tStrip *_strip = nullptr;\n\n\tstd::vector<DocumentId> _unifiedIdsList;\n\tbase::flat_map<DocumentId, Data::ReactionId> _defaultReactionIds;\n\tbase::flat_map<DocumentId, int> _defaultReactionInStripMap;\n\n\tQPoint _defaultReactionShift;\n\tQPoint _stripPaintOneShift;\n\n};\n\nclass Selector final : public Ui::RpWidget {\npublic:\n\tSelector(\n\t\tnot_null<QWidget*> parent,\n\t\tconst style::EmojiPan &st,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::PossibleItemReactionsRef &reactions,\n\t\tTextWithEntities about,\n\t\tFn<void(bool fast)> close,\n\t\tIconFactory iconFactory = nullptr,\n\t\tFn<bool()> paused = nullptr,\n\t\tbool child = false);\n#if 0 // not ready\n\tSelector(\n\t\tnot_null<QWidget*> parent,\n\t\tconst style::EmojiPan &st,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tChatHelpers::EmojiListMode mode,\n\t\tstd::vector<DocumentId> recent,\n\t\tFn<void(bool fast)> close,\n\t\tbool child = false);\n#endif\n\t~Selector();\n\n\t[[nodiscard]] bool useTransparency() const;\n\n\tint countWidth(int desiredWidth, int maxWidth);\n\t[[nodiscard]] int effectPreviewHeight() const;\n\t[[nodiscard]] QMargins marginsForShadow() const;\n\t[[nodiscard]] int extendTopForCategories() const;\n\t[[nodiscard]] int extendTopForCategoriesAndAbout(int width) const;\n\t[[nodiscard]] int opaqueExtendTopAbout(int width) const;\n\t[[nodiscard]] int minimalHeight(int fullWidth) const;\n\t[[nodiscard]] int countAppearedWidth(float64 progress) const;\n\tvoid setSpecialExpandTopSkip(int skip);\n\tvoid setBubbleUp(bool bubbleUp);\n\tvoid initGeometry(int innerTop);\n\tvoid beforeDestroy();\n\n\tvoid setOpaqueHeightExpand(int expand, Fn<void(int)> apply);\n\n\t[[nodiscard]] rpl::producer<ChosenReaction> chosen() const {\n\t\treturn _chosen.events();\n\t}\n\t[[nodiscard]] rpl::producer<> willExpand() const {\n\t\treturn _willExpand.events();\n\t}\n\t[[nodiscard]] rpl::producer<> escapes() const;\n\n\tvoid updateShowState(\n\t\tfloat64 progress,\n\t\tfloat64 opacity,\n\t\tbool appearing,\n\t\tbool toggling);\n\nprivate:\n\tstatic constexpr int kFramesCount = 32;\n\n\tstruct ExpandingRects {\n\t\tQRect categories;\n\t\tQRect list;\n\t\tfloat64 radius = 0.;\n\t\tfloat64 expanding = 0.;\n\t\tint finalBottom = 0;\n\t\tint frame = 0;\n\t\tQRect outer;\n\t};\n\n\tSelector(\n\t\tnot_null<QWidget*> parent,\n\t\tconst style::EmojiPan &st,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::PossibleItemReactionsRef &reactions,\n\t\tChatHelpers::EmojiListMode mode,\n\t\tstd::vector<DocumentId> recent,\n\t\tTextWithEntities about,\n\t\tIconFactory iconFactory,\n\t\tFn<bool()> paused,\n\t\tFn<void(bool fast)> close,\n\t\tbool child);\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\n\tvoid paintAppearing(QPainter &p);\n\tvoid paintCollapsed(QPainter &p);\n\tvoid paintExpanding(Painter &p, float64 progress);\n\tvoid paintExpandingBg(QPainter &p, const ExpandingRects &rects);\n\tvoid paintFadingExpandIcon(QPainter &p, float64 progress);\n\tvoid paintExpanded(QPainter &p);\n\tvoid paintNonTransparentExpandRect(QPainter &p, const QRect &) const;\n\tvoid paintBubble(QPainter &p, int innerWidth);\n\tvoid paintBackgroundToBuffer();\n\n\tExpandingRects updateExpandingRects(float64 progress);\n\n\t[[nodiscard]] int recentCount() const;\n\t[[nodiscard]] int countSkipLeft() const;\n\t[[nodiscard]] int lookupSelectedIndex(QPoint position) const;\n\tvoid setSelected(int index);\n\n\tvoid expand();\n\tvoid cacheExpandIcon();\n\tvoid createList();\n\tvoid finishExpand();\n\tChosenReaction lookupChosen(const Data::ReactionId &id) const;\n\tvoid preloadAllRecentsAnimations();\n\n\t[[nodiscard]] int skipYBubbleUpShift() const;\n\n\tconst style::EmojiPan &_st;\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst Data::PossibleItemReactions _reactions;\n\tconst std::vector<DocumentId> _recent;\n\tconst ChatHelpers::EmojiListMode _listMode;\n\tconst Fn<bool()> _paused;\n\tFn<void()> _jumpedToPremium;\n\tUi::RoundAreaWithShadow _cachedRound;\n\tstd::unique_ptr<Strip> _strip;\n\tstd::unique_ptr<Ui::FlatLabel> _about;\n\tmutable int _aboutExtend = 0;\n\n\trpl::event_stream<ChosenReaction> _chosen;\n\trpl::event_stream<> _willExpand;\n\trpl::event_stream<> _escapes;\n\n\tUi::ScrollArea *_scroll = nullptr;\n\tChatHelpers::EmojiListWidget *_list = nullptr;\n\tChatHelpers::StickersListWidget *_stickers = nullptr;\n\tChatHelpers::StickersListFooter *_footer = nullptr;\n\tstd::unique_ptr<UnifiedFactoryOwner> _unifiedFactoryOwner;\n\tUi::PlainShadow *_shadow = nullptr;\n\trpl::variable<int> _shadowTop = 0;\n\trpl::variable<int> _shadowSkip = 0;\n\tbool _showEmptySearch = false;\n\n\tQImage _paintBuffer;\n\tUi::Animations::Simple _expanding;\n\tfloat64 _appearProgress = 0.;\n\tfloat64 _appearOpacity = 0.;\n\tQRect _inner;\n\tQRect _outer;\n\tQRect _outerWithBubble;\n\tQImage _expandIconCache;\n\tQImage _aboutCache;\n\tQMargins _padding;\n\tint _specialExpandTopSkip = 0;\n\tint _collapsedTopSkip = 0;\n\tint _topAddOnExpand = 0;\n\n\tint _opaqueHeightExpand = 0;\n\tFn<void(int)> _opaqueApplyHeightExpand;\n\n\tconst int _size = 0;\n\tint _recentRows = 0;\n\tint _columns = 0;\n\tint _skipx = 0;\n\tint _skipy = 0;\n\tint _pressed = -1;\n\tbool _useTransparency = false;\n\tbool _appearing = false;\n\tbool _toggling = false;\n\tbool _expanded = false;\n\tbool _expandScheduled = false;\n\tbool _expandFinished = false;\n\tbool _small = false;\n\tbool _over = false;\n\tbool _low = false;\n\tbool _bubbleUp = false;\n\n};\n\nenum class AttachSelectorResult {\n\tSkipped,\n\tFailed,\n\tAttached,\n};\n\n#if 0 // not ready\nAttachSelectorResult MakeJustSelectorMenu(\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<Window::SessionController*> controller,\n\tQPoint desiredPosition,\n\tChatHelpers::EmojiListMode mode,\n\tstd::vector<DocumentId> recent,\n\tFn<void(ChosenReaction)> chosen);\n#endif\n\nAttachSelectorResult AttachSelectorToMenu(\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<Window::SessionController*> controller,\n\tQPoint desiredPosition,\n\tnot_null<HistoryItem*> item,\n\tFn<void(ChosenReaction)> chosen,\n\tTextWithEntities about,\n\tIconFactory iconFactory = nullptr);\n\n[[nodiscard]] auto AttachSelectorToMenu(\n\tnot_null<Ui::PopupMenu*> menu,\n\tQPoint desiredPosition,\n\tconst style::EmojiPan &st,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst Data::PossibleItemReactionsRef &reactions,\n\tTextWithEntities about,\n\tIconFactory iconFactory = nullptr,\n\tFn<bool()> paused = nullptr\n) -> base::expected<not_null<Selector*>, AttachSelectorResult>;\n\n[[nodiscard]] TextWithEntities ItemReactionsAbout(\n\tnot_null<HistoryItem*> item);\n\n} // namespace HistoryView::Reactions\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/reactions/history_view_reactions_strip.h\"\n\n#include \"data/data_message_reactions.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"main/main_session.h\"\n#include \"ui/effects/frame_generator.h\"\n#include \"ui/animated_icon.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace HistoryView::Reactions {\nnamespace {\n\nconstexpr auto kSizeForDownscale = 96;\nconstexpr auto kEmojiCacheIndex = 0;\nconstexpr auto kHoverScaleDuration = crl::time(200);\nconstexpr auto kHoverScale = 1.24;\n\n[[nodiscard]] int MainReactionSize() {\n\treturn style::ConvertScale(kSizeForDownscale);\n}\n\n[[nodiscard]] std::shared_ptr<Ui::AnimatedIcon> CreateIcon(\n\t\tnot_null<Data::DocumentMedia*> media,\n\t\tint size) {\n\tExpects(media->loaded());\n\n\treturn std::make_shared<Ui::AnimatedIcon>(Ui::AnimatedIconDescriptor{\n\t\t.generator = DocumentIconFrameGenerator(media),\n\t\t.sizeOverride = QSize(size, size),\n\t\t.colorized = media->owner()->emojiUsesTextColor(),\n\t});\n}\n\n} // namespace\n\nStrip::Strip(\n\tconst style::EmojiPan &st,\n\tQRect inner,\n\tint size,\n\tFn<void()> update,\n\tIconFactory iconFactory)\n: _st(st)\n, _iconFactory(iconFactory\n\t? std::move(iconFactory)\n\t: DefaultCachingIconFactory)\n, _inner(inner)\n, _finalSize(size)\n, _update(std::move(update)) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tinvalidateMainReactionImage();\n\t}, _lifetime);\n}\n\nvoid Strip::invalidateMainReactionImage() {\n\tif (_mainReactionImage.isNull()\n\t\t&& !ranges::contains(_validEmoji, true)) {\n\t\treturn;\n\t}\n\tconst auto was = base::take(_mainReactionMedia);\n\t_mainReactionImage = QImage();\n\tranges::fill(_validEmoji, false);\n\tresolveMainReactionIcon();\n}\n\nvoid Strip::applyList(\n\t\tconst std::vector<not_null<const Data::Reaction*>> &list,\n\t\tAddedButton button) {\n\tif (_button == button\n\t\t&& ranges::equal(\n\t\t\tranges::make_subrange(\n\t\t\t\tbegin(_icons),\n\t\t\t\t(begin(_icons)\n\t\t\t\t\t+ _icons.size()\n\t\t\t\t\t- (_button == AddedButton::None ? 0 : 1))),\n\t\t\tlist,\n\t\t\tranges::equal_to(),\n\t\t\t&ReactionIcons::id,\n\t\t\t&Data::Reaction::id)) {\n\t\treturn;\n\t}\n\tconst auto selected = _selectedIcon;\n\tsetSelected(-1);\n\t_icons.clear();\n\tfor (const auto &reaction : list) {\n\t\t_icons.push_back({\n\t\t\t.id = reaction->id,\n\t\t\t.appearAnimation = reaction->appearAnimation,\n\t\t\t.selectAnimation = reaction->selectAnimation,\n\t\t});\n\t}\n\t_button = button;\n\tif (_button != AddedButton::None) {\n\t\t_icons.push_back({ .added = _button });\n\t}\n\tsetSelected((selected < _icons.size()) ? selected : -1);\n\tresolveMainReactionIcon();\n}\n\nvoid Strip::paint(\n\t\tQPainter &p,\n\t\tQPoint position,\n\t\tQPoint shift,\n\t\tQRect clip,\n\t\tfloat64 scale,\n\t\tbool hiding) {\n\tconst auto skip = st::reactionAppearStartSkip;\n\tconst auto animationRect = clip.marginsRemoved({ 0, skip, 0, skip });\n\n\tPainterHighQualityEnabler hq(p);\n\tconst auto countTarget = resolveCountTargetMethod(scale);\n\tfor (auto &icon : _icons) {\n\t\tconst auto target = countTarget(icon).translated(position);\n\t\tposition += shift;\n\t\tif (target.intersects(clip)) {\n\t\t\tpaintOne(\n\t\t\t\tp,\n\t\t\t\ticon,\n\t\t\t\tposition - shift,\n\t\t\t\ttarget,\n\t\t\t\t!hiding && target.intersects(animationRect));\n\t\t} else if (!hiding) {\n\t\t\tclearStateForHidden(icon);\n\t\t}\n\t\tif (!hiding) {\n\t\t\tclearStateForSelectFinished(icon);\n\t\t}\n\t}\n}\n\nauto Strip::resolveCountTargetMethod(float64 scale) const\n-> Fn<QRectF(const ReactionIcons&)> {\n\tconst auto hoveredSize = int(base::SafeRound(_finalSize * kHoverScale));\n\tconst auto basicTargetForScale = [&](int size, float64 scale) {\n\t\tconst auto remove = size * (1. - scale) / 2.;\n\t\treturn QRectF(QRect(\n\t\t\t_inner.x() + (_inner.width() - size) / 2,\n\t\t\t_inner.y() + (_inner.height() - size) / 2,\n\t\t\tsize,\n\t\t\tsize\n\t\t)).marginsRemoved({ remove, remove, remove, remove });\n\t};\n\tconst auto basicTarget = basicTargetForScale(_finalSize, scale);\n\treturn [=](const ReactionIcons &icon) {\n\t\tconst auto selectScale = icon.selectedScale.value(\n\t\t\ticon.selected ? kHoverScale : 1.);\n\t\tif (selectScale == 1.) {\n\t\t\treturn basicTarget;\n\t\t}\n\t\tconst auto finalScale = scale * selectScale;\n\t\treturn (finalScale <= 1.)\n\t\t\t? basicTargetForScale(_finalSize, finalScale)\n\t\t\t: basicTargetForScale(hoveredSize, finalScale / kHoverScale);\n\t};\n}\n\nvoid Strip::paintOne(\n\t\tQPainter &p,\n\t\tReactionIcons &icon,\n\t\tQPoint position,\n\t\tQRectF target,\n\t\tbool allowAppearStart) {\n\tif (icon.added == AddedButton::Expand) {\n\t\tpaintExpandIcon(p, position, target);\n\t} else {\n\t\tconst auto paintFrame = [&](not_null<Ui::AnimatedIcon*> animation) {\n\t\t\tconst auto size = int(std::floor(target.width() + 0.01));\n\t\t\tconst auto &textColor = _st.textFg->c;\n\t\t\tconst auto frame = animation->frame(\n\t\t\t\ttextColor,\n\t\t\t\t{ size, size },\n\t\t\t\t_update);\n\t\t\tp.drawImage(target, frame.image);\n\t\t};\n\n\t\tconst auto appear = icon.appear.get();\n\t\tif (appear && !icon.appearAnimated && allowAppearStart) {\n\t\t\ticon.appearAnimated = true;\n\t\t\tappear->animate(_update);\n\t\t}\n\t\tif (appear && appear->animating()) {\n\t\t\tpaintFrame(appear);\n\t\t} else if (const auto select = icon.select.get()) {\n\t\t\tpaintFrame(select);\n\t\t}\n\t}\n}\n\nvoid Strip::paintOne(\n\t\tQPainter &p,\n\t\tint index,\n\t\tQPoint position,\n\t\tfloat64 scale) {\n\tExpects(index >= 0 && index < _icons.size());\n\n\tauto &icon = _icons[index];\n\tconst auto countTarget = resolveCountTargetMethod(scale);\n\tconst auto target = countTarget(icon).translated(position);\n\tpaintOne(p, icon, position, target, false);\n}\n\nbool Strip::inDefaultState(int index) const {\n\tExpects(index >= 0 && index < _icons.size());\n\n\tconst auto &icon = _icons[index];\n\treturn !icon.selected\n\t\t&& !icon.selectedScale.animating()\n\t\t&& icon.select\n\t\t&& !icon.select->animating()\n\t\t&& (!icon.appear || !icon.appear->animating());\n}\n\nbool Strip::empty() const {\n\treturn _icons.empty();\n}\n\nint Strip::count() const {\n\treturn _icons.size();\n}\n\nbool Strip::onlyAddedButton() const {\n\treturn (_icons.size() == 1)\n\t\t&& (_icons.front().added != AddedButton::None);\n}\n\nint Strip::fillChosenIconGetIndex(ChosenReaction &chosen) const {\n\tconst auto i = ranges::find(_icons, chosen.id, &ReactionIcons::id);\n\tif (i == end(_icons)) {\n\t\treturn -1;\n\t}\n\tconst auto &icon = *i;\n\tif (const auto &appear = icon.appear; appear && appear->animating()) {\n\t\tchosen.icon = appear->frame(_st.textFg->c);\n\t} else if (const auto &select = icon.select; select && select->valid()) {\n\t\tchosen.icon = select->frame(_st.textFg->c);\n\t}\n\treturn (i - begin(_icons));\n}\n\nvoid Strip::paintExpandIcon(\n\t\tQPainter &p,\n\t\tQPoint position,\n\t\tQRectF target) const {\n\tconst auto to = QRect(\n\t\t_inner.x() + (_inner.width() - _finalSize) / 2,\n\t\t_inner.y() + (_inner.height() - _finalSize) / 2,\n\t\t_finalSize,\n\t\t_finalSize\n\t).translated(position);\n\tconst auto scale = target.width() / to.width();\n\tif (scale != 1.) {\n\t\tp.save();\n\t\tp.translate(target.center());\n\t\tp.scale(scale, scale);\n\t\tp.translate(-target.center());\n\t}\n\tauto hq = PainterHighQualityEnabler(p);\n\t((_finalSize == st::reactionCornerImage)\n\t\t? _st.icons.stripExpandDropdown\n\t\t: _st.icons.stripExpandPanel).paintInCenter(p, to);\n\tif (scale != 1.) {\n\t\tp.restore();\n\t}\n}\n\nvoid Strip::setSelected(int index) const {\n\tconst auto set = [&](int index, bool selected) {\n\t\tif (index < 0 || index >= _icons.size()) {\n\t\t\treturn;\n\t\t}\n\t\tauto &icon = _icons[index];\n\t\tif (icon.selected == selected) {\n\t\t\treturn;\n\t\t}\n\t\ticon.selected = selected;\n\t\ticon.selectedScale.start(\n\t\t\t_update,\n\t\t\tselected ? 1. : kHoverScale,\n\t\t\tselected ? kHoverScale : 1.,\n\t\t\tkHoverScaleDuration,\n\t\t\tanim::sineInOut);\n\t\tif (selected) {\n\t\t\tconst auto skipAnimation = icon.selectAnimated\n\t\t\t\t|| !icon.appearAnimated\n\t\t\t\t|| (icon.select && icon.select->animating())\n\t\t\t\t|| (icon.appear && icon.appear->animating());\n\t\t\tconst auto select = skipAnimation ? nullptr : icon.select.get();\n\t\t\tif (select && !icon.selectAnimated) {\n\t\t\t\ticon.selectAnimated = true;\n\t\t\t\tselect->animate(_update);\n\t\t\t}\n\t\t}\n\t};\n\tif (_selectedIcon != index) {\n\t\tset(_selectedIcon, false);\n\t\t_selectedIcon = index;\n\t}\n\tset(index, true);\n}\n\nauto Strip::selected() const -> std::variant<AddedButton, ReactionId> {\n\tif (_selectedIcon < 0 || _selectedIcon >= _icons.size()) {\n\t\treturn {};\n\t}\n\tconst auto &icon = _icons[_selectedIcon];\n\tif (icon.added != AddedButton::None) {\n\t\treturn icon.added;\n\t}\n\treturn icon.id;\n}\n\nint Strip::computeOverSize() const {\n\treturn int(base::SafeRound(_finalSize * kHoverScale));\n}\n\nvoid Strip::clearAppearAnimations(bool mainAppeared) {\n\tauto main = mainAppeared;\n\tfor (auto &icon : _icons) {\n\t\tif (!main) {\n\t\t\tif (icon.selected) {\n\t\t\t\tsetSelected(-1);\n\t\t\t}\n\t\t\ticon.selectedScale.stop();\n\t\t\tif (const auto select = icon.select.get()) {\n\t\t\t\tselect->jumpToStart(nullptr);\n\t\t\t}\n\t\t\ticon.selectAnimated = false;\n\t\t}\n\t\tif (icon.appearAnimated != main) {\n\t\t\tif (const auto appear = icon.appear.get()) {\n\t\t\t\tappear->jumpToStart(nullptr);\n\t\t\t}\n\t\t\ticon.appearAnimated = main;\n\t\t}\n\t\tmain = false;\n\t}\n}\n\nvoid Strip::clearStateForHidden(ReactionIcons &icon) {\n\tif (const auto appear = icon.appear.get()) {\n\t\tappear->jumpToStart(nullptr);\n\t}\n\tif (icon.selected) {\n\t\tsetSelected(-1);\n\t}\n\ticon.appearAnimated = false;\n\ticon.selectAnimated = false;\n\tif (const auto select = icon.select.get()) {\n\t\tselect->jumpToStart(nullptr);\n\t}\n\ticon.selectedScale.stop();\n}\n\nvoid Strip::clearStateForSelectFinished(ReactionIcons &icon) {\n\tif (icon.selectAnimated\n\t\t&& !icon.select->animating()\n\t\t&& !icon.selected) {\n\t\ticon.selectAnimated = false;\n\t}\n}\n\nbool Strip::checkIconLoaded(ReactionDocument &entry) const {\n\tif (!entry.media) {\n\t\treturn true;\n\t} else if (!entry.media->loaded()) {\n\t\treturn false;\n\t}\n\tconst auto size = (entry.media == _mainReactionMedia)\n\t\t? MainReactionSize()\n\t\t: _finalSize;\n\tentry.icon = _iconFactory(entry.media.get(), size);\n\tentry.media = nullptr;\n\treturn true;\n}\n\nvoid Strip::loadIcons() {\n\tconst auto load = [&](not_null<DocumentData*> document) {\n\t\tif (const auto i = _loadCache.find(document); i != end(_loadCache)) {\n\t\t\treturn i->second.icon;\n\t\t}\n\t\tauto &entry = _loadCache.emplace(document).first->second;\n\t\tentry.media = document->createMediaView();\n\t\tentry.media->checkStickerLarge();\n\t\tif (!checkIconLoaded(entry) && !_loadCacheLifetime) {\n\t\t\tdocument->session().downloaderTaskFinished(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tcheckIcons();\n\t\t\t}, _loadCacheLifetime);\n\t\t}\n\t\treturn entry.icon;\n\t};\n\tauto all = true;\n\tfor (auto &icon : _icons) {\n\t\tif (icon.appearAnimation && !icon.appear) {\n\t\t\ticon.appear = load(icon.appearAnimation);\n\t\t\tif (!icon.appear) {\n\t\t\t\tall = false;\n\t\t\t}\n\t\t}\n\t\tif (icon.selectAnimation && !icon.select) {\n\t\t\ticon.select = load(icon.selectAnimation);\n\t\t\tif (!icon.select) {\n\t\t\t\tall = false;\n\t\t\t}\n\t\t}\n\t}\n\tif (all && !_icons.empty() && _icons.front().selectAnimation) {\n\t\tauto &data = _icons.front().selectAnimation->owner().reactions();\n\t\tfor (const auto &icon : _icons) {\n\t\t\tdata.preloadAnimationsFor(icon.id);\n\t\t}\n\t}\n}\n\nvoid Strip::checkIcons() {\n\tauto all = true;\n\tfor (auto &[document, entry] : _loadCache) {\n\t\tif (!checkIconLoaded(entry)) {\n\t\t\tall = false;\n\t\t}\n\t}\n\tif (all) {\n\t\t_loadCacheLifetime.destroy();\n\t\tloadIcons();\n\t}\n}\n\nvoid Strip::resolveMainReactionIcon() {\n\tif (_icons.empty() || onlyAddedButton()) {\n\t\t_mainReactionMedia = nullptr;\n\t\t_mainReactionLifetime.destroy();\n\t\treturn;\n\t}\n\tconst auto main = _icons.front().selectAnimation;\n\tAssert(main != nullptr);\n\t_icons.front().appearAnimated = true;\n\tif (_mainReactionMedia && _mainReactionMedia->owner() == main) {\n\t\tif (!_mainReactionLifetime) {\n\t\t\tloadIcons();\n\t\t}\n\t\treturn;\n\t}\n\t_mainReactionMedia = main->createMediaView();\n\t_mainReactionMedia->checkStickerLarge();\n\tif (_mainReactionMedia->loaded()) {\n\t\tsetMainReactionIcon();\n\t} else if (!_mainReactionLifetime) {\n\t\tmain->session().downloaderTaskFinished(\n\t\t) | rpl::filter([=] {\n\t\t\treturn _mainReactionMedia->loaded();\n\t\t}) | rpl::take(1) | rpl::on_next([=] {\n\t\t\tsetMainReactionIcon();\n\t\t}, _mainReactionLifetime);\n\t}\n}\n\nvoid Strip::setMainReactionIcon() {\n\tExpects(_mainReactionMedia->loaded());\n\n\t_mainReactionLifetime.destroy();\n\tranges::fill(_validEmoji, false);\n\tloadIcons();\n\n\tAssert(_mainReactionMedia->loaded());\n\n\tconst auto i = _loadCache.find(_mainReactionMedia->owner());\n\tif (i != end(_loadCache) && i->second.icon) {\n\t\tconst auto &icon = i->second.icon;\n\t\tif (!icon->frameIndex() && icon->width() == MainReactionSize()) {\n\t\t\t_mainReactionImage = i->second.icon->frame(_st.textFg->c);\n\t\t\treturn;\n\t\t}\n\t}\n\t_mainReactionImage = QImage();\n\n\tAssert(_mainReactionMedia->loaded());\n\t_mainReactionIcon = DefaultIconFactory(\n\t\t_mainReactionMedia.get(),\n\t\tMainReactionSize());\n}\n\nbool Strip::onlyMainEmojiVisible() const {\n\tif (_icons.empty()) {\n\t\treturn true;\n\t}\n\tconst auto &icon = _icons.front();\n\tif (icon.selected\n\t\t|| icon.selectedScale.animating()\n\t\t|| (icon.select && icon.select->animating())) {\n\t\treturn false;\n\t}\n\ticon.selectAnimated = false;\n\treturn true;\n}\n\nUi::ImageSubrect Strip::validateEmoji(int frameIndex, float64 scale) {\n\tconst auto area = _inner.size();\n\tconst auto size = int(base::SafeRound(_finalSize * scale));\n\tconst auto result = Ui::ImageSubrect{\n\t\t&_emojiParts,\n\t\tUi::RoundAreaWithShadow::FrameCacheRect(\n\t\t\tframeIndex,\n\t\t\tkEmojiCacheIndex,\n\t\t\tarea),\n\t};\n\tif (_validEmoji[frameIndex]) {\n\t\treturn result;\n\t} else if (_emojiParts.isNull()) {\n\t\t_emojiParts = Ui::RoundAreaWithShadow::PrepareFramesCache(area);\n\t}\n\n\tauto p = QPainter(result.image);\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto position = result.rect.topLeft() / ratio;\n\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\tp.fillRect(QRect(position, result.rect.size() / ratio), Qt::transparent);\n\tif (_mainReactionImage.isNull()\n\t\t&& _mainReactionIcon) {\n\t\t_mainReactionImage = base::take(_mainReactionIcon)->frame(\n\t\t\t_st.textFg->c);\n\t}\n\tif (!_mainReactionImage.isNull()) {\n\t\tconst auto target = QRect(\n\t\t\t(_inner.width() - size) / 2,\n\t\t\t(_inner.height() - size) / 2,\n\t\t\tsize,\n\t\t\tsize\n\t\t).translated(position);\n\n\t\tp.drawImage(target, _mainReactionImage.scaled(\n\t\t\ttarget.size() * ratio,\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation));\n\t}\n\n\t_validEmoji[frameIndex] = true;\n\treturn result;\n}\n\nIconFactory CachedIconFactory::createMethod() {\n\treturn [=](not_null<Data::DocumentMedia*> media, int size) {\n\t\tconst auto owned = media->owner()->createMediaView();\n\t\tconst auto i = _cache.find(owned);\n\t\treturn (i != end(_cache))\n\t\t\t? i->second\n\t\t\t: _cache.emplace(\n\t\t\t\towned,\n\t\t\t\tDefaultIconFactory(media, size)).first->second;\n\t};\n}\n\nstd::shared_ptr<Ui::AnimatedIcon> DefaultIconFactory(\n\t\tnot_null<Data::DocumentMedia*> media,\n\t\tint size) {\n\treturn CreateIcon(media, size);\n}\n\nstd::shared_ptr<Ui::AnimatedIcon> DefaultCachingIconFactory(\n\t\tnot_null<Data::DocumentMedia*> media,\n\t\tint size) {\n\tauto &factory = media->owner()->session().cachedReactionIconFactory();\n\treturn factory.createMethod()(media, size);\n}\n\n} // namespace HistoryView::Reactions\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/round_area_with_shadow.h\"\n#include \"data/data_message_reaction_id.h\"\n\nnamespace style {\nstruct EmojiPan;\n} // namespace style\n\nclass HistoryItem;\n\nnamespace Data {\nstruct Reaction;\nclass DocumentMedia;\n} // namespace Data\n\nnamespace Ui {\nclass AnimatedIcon;\n} // namespace Ui\n\nnamespace HistoryView::Reactions {\n\nstruct ChosenReaction {\n\tFullMsgId context;\n\tData::ReactionId id;\n\tQImage icon;\n\tQRect localGeometry;\n\tQRect globalGeometry;\n\n\texplicit operator bool() const {\n\t\treturn context && !id.empty();\n\t}\n};\n\nusing IconFactory = Fn<std::shared_ptr<Ui::AnimatedIcon>(\n\tnot_null<Data::DocumentMedia*>,\n\tint)>;\n\nclass Strip final {\npublic:\n\tusing ReactionId = Data::ReactionId;\n\n\tStrip(\n\t\tconst style::EmojiPan &st,\n\t\tQRect inner,\n\t\tint size,\n\t\tFn<void()> update,\n\t\tIconFactory iconFactory = nullptr);\n\n\tenum class AddedButton : uchar {\n\t\tNone,\n\t\tExpand,\n\t};\n\tvoid applyList(\n\t\tconst std::vector<not_null<const Data::Reaction*>> &list,\n\t\tAddedButton button);\n\n\tvoid paint(\n\t\tQPainter &p,\n\t\tQPoint position,\n\t\tQPoint shift,\n\t\tQRect clip,\n\t\tfloat64 scale,\n\t\tbool hiding);\n\tvoid paintOne(QPainter &p, int index, QPoint position, float64 scale);\n\t[[nodiscard]] bool inDefaultState(int index) const;\n\n\t[[nodiscard]] bool empty() const;\n\t[[nodiscard]] int count() const;\n\tvoid setSelected(int index) const;\n\t[[nodiscard]] std::variant<AddedButton, ReactionId> selected() const;\n\t[[nodiscard]] int computeOverSize() const;\n\n\tvoid clearAppearAnimations(bool mainAppeared = true);\n\n\tint fillChosenIconGetIndex(ChosenReaction &chosen) const;\n\n\t[[nodiscard]] bool onlyAddedButton() const;\n\t[[nodiscard]] bool onlyMainEmojiVisible() const;\n\tUi::ImageSubrect validateEmoji(int frameIndex, float64 scale);\n\nprivate:\n\tstatic constexpr auto kFramesCount\n\t\t= Ui::RoundAreaWithShadow::kFramesCount;\n\n\tstruct ReactionDocument {\n\t\tstd::shared_ptr<Data::DocumentMedia> media;\n\t\tstd::shared_ptr<Ui::AnimatedIcon> icon;\n\t};\n\tstruct ReactionIcons {\n\t\tReactionId id;\n\t\tDocumentData *appearAnimation = nullptr;\n\t\tDocumentData *selectAnimation = nullptr;\n\t\tstd::shared_ptr<Ui::AnimatedIcon> appear;\n\t\tstd::shared_ptr<Ui::AnimatedIcon> select;\n\t\tmutable Ui::Animations::Simple selectedScale;\n\t\tAddedButton added = AddedButton::None;\n\t\tbool appearAnimated = false;\n\t\tmutable bool selected = false;\n\t\tmutable bool selectAnimated = false;\n\t};\n\n\tvoid clearStateForHidden(ReactionIcons &icon);\n\tvoid paintExpandIcon(QPainter &p, QPoint position, QRectF target) const;\n\tvoid clearStateForSelectFinished(ReactionIcons &icon);\n\n\t[[nodiscard]] bool checkIconLoaded(ReactionDocument &entry) const;\n\tvoid loadIcons();\n\tvoid checkIcons();\n\tvoid paintOne(\n\t\tQPainter &p,\n\t\tReactionIcons &icon,\n\t\tQPoint position,\n\t\tQRectF target,\n\t\tbool allowAppearStart);\n\t[[nodiscard]] Fn<QRectF(const ReactionIcons&)> resolveCountTargetMethod(\n\t\tfloat64 scale) const;\n\n\tvoid invalidateMainReactionImage();\n\tvoid resolveMainReactionIcon();\n\tvoid setMainReactionIcon();\n\n\tconst style::EmojiPan &_st;\n\tconst IconFactory _iconFactory;\n\tconst QRect _inner;\n\tconst int _finalSize = 0;\n\tFn<void()> _update;\n\n\tstd::vector<ReactionIcons> _icons;\n\tAddedButton _button = AddedButton::None;\n\tbase::flat_map<not_null<DocumentData*>, ReactionDocument> _loadCache;\n\tstd::optional<ReactionIcons> _premiumIcon;\n\trpl::lifetime _loadCacheLifetime;\n\n\tmutable int _selectedIcon = -1;\n\n\tstd::shared_ptr<Data::DocumentMedia> _mainReactionMedia;\n\tstd::shared_ptr<Ui::AnimatedIcon> _mainReactionIcon;\n\tQImage _mainReactionImage;\n\trpl::lifetime _mainReactionLifetime;\n\n\tQImage _emojiParts;\n\tstd::array<bool, kFramesCount> _validEmoji = { { false } };\n\n\trpl::lifetime _lifetime;\n\n};\n\nclass CachedIconFactory final {\npublic:\n\tCachedIconFactory() = default;\n\tCachedIconFactory(const CachedIconFactory &other) = delete;\n\tCachedIconFactory &operator=(const CachedIconFactory &other) = delete;\n\n\t[[nodiscard]] IconFactory createMethod();\n\nprivate:\n\tbase::flat_map<\n\t\tstd::shared_ptr<Data::DocumentMedia>,\n\t\tstd::shared_ptr<Ui::AnimatedIcon>> _cache;\n\n};\n\n[[nodiscard]] std::shared_ptr<Ui::AnimatedIcon> DefaultIconFactory(\n\tnot_null<Data::DocumentMedia*> media,\n\tint size);\n\n[[nodiscard]] std::shared_ptr<Ui::AnimatedIcon> DefaultCachingIconFactory(\n\tnot_null<Data::DocumentMedia*> media,\n\tint size);\n\n} // namespace HistoryView\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"history/view/reactions/history_view_reactions_tabs.h\"\n\n#include \"data/data_message_reaction_id.h\"\n#include \"lang/lang_tag.h\"\n#include \"ui/abstract_button.h\"\n#include \"ui/controls/who_reacted_context_action.h\"\n#include \"ui/painter.h\"\n#include \"ui/rp_widget.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace HistoryView::Reactions {\nnamespace {\n\nusing ::Data::ReactionId;\n\nnot_null<Ui::AbstractButton*> CreateTab(\n\t\tnot_null<QWidget*> parent,\n\t\tconst style::MultiSelect &st,\n\t\tconst Ui::Text::CustomEmojiFactory &factory,\n\t\tFn<bool()> paused,\n\t\tconst ReactionId &reaction,\n\t\tUi::WhoReadType whoReadType,\n\t\tint count,\n\t\trpl::producer<bool> selected) {\n\tstruct State {\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji> custom;\n\t\tQImage cache;\n\t\tbool selected = false;\n\t};\n\tconst auto stm = &st.item;\n\tconst auto text = Lang::FormatCountDecimal(count);\n\tconst auto font = st::semiboldFont;\n\tconst auto textWidth = font->width(text);\n\tconst auto result = Ui::CreateChild<Ui::AbstractButton>(parent.get());\n\tconst auto width = stm->height\n\t\t+ stm->padding.left()\n\t\t+ textWidth\n\t\t+ stm->padding.right();\n\tresult->resize(width, stm->height);\n\tconst auto state = result->lifetime().make_state<State>();\n\tstd::move(\n\t\tselected\n\t) | rpl::on_next([=](bool selected) {\n\t\tstate->selected = selected;\n\t\tstate->cache = QImage();\n\t\tresult->update();\n\t}, result->lifetime());\n\n\tstate->custom = reaction.empty()\n\t\t? nullptr\n\t\t: factory(\n\t\t\tData::ReactionEntityData(reaction),\n\t\t\t{ .repaint = [=] { result->update(); } });\n\n\tresult->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tconst auto height = stm->height;\n\t\tconst auto skip = st::reactionsTabIconSkip;\n\t\tconst auto icon = QRect(skip, 0, height, height);\n\t\tif (state->cache.isNull()) {\n\t\t\tstate->cache = QImage(\n\t\t\t\tresult->size() * factor,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tstate->cache.setDevicePixelRatio(factor);\n\t\t\tstate->cache.fill(Qt::transparent);\n\t\t\tauto p = QPainter(&state->cache);\n\n\t\t\tconst auto height = stm->height;\n\t\t\tconst auto radius = height / 2;\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(state->selected ? stm->textActiveBg : stm->textBg);\n\t\t\t{\n\t\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\t\tp.drawRoundedRect(result->rect(), radius, radius);\n\t\t\t}\n\t\t\tconst auto skip = st::reactionsTabIconSkip;\n\t\t\tconst auto icon = QRect(skip, 0, height, height);\n\t\t\tif (!state->custom) {\n\t\t\t\tusing Type = Ui::WhoReadType;\n\t\t\t\t(reaction.emoji().isEmpty()\n\t\t\t\t\t? (state->selected\n\t\t\t\t\t\t? st::reactionsTabAllSelected\n\t\t\t\t\t\t: st::reactionsTabAll)\n\t\t\t\t\t: (whoReadType == Type::Watched\n\t\t\t\t\t\t|| whoReadType == Type::Listened)\n\t\t\t\t\t? (state->selected\n\t\t\t\t\t\t? st::reactionsTabPlayedSelected\n\t\t\t\t\t\t: st::reactionsTabPlayed)\n\t\t\t\t\t: (state->selected\n\t\t\t\t\t\t? st::reactionsTabChecksSelected\n\t\t\t\t\t\t: st::reactionsTabChecks)).paintInCenter(p, icon);\n\t\t\t}\n\n\t\t\tconst auto textLeft = height + stm->padding.left();\n\t\t\tp.setPen(state->selected ? stm->textActiveFg : stm->textFg);\n\t\t\tp.setFont(font);\n\t\t\tp.drawText(textLeft, stm->padding.top() + font->ascent, text);\n\t\t}\n\t\tauto p = QPainter(result);\n\t\tp.drawImage(0, 0, state->cache);\n\t\tif (const auto custom = state->custom.get()) {\n\t\t\tusing namespace Ui::Text;\n\t\t\tconst auto size = Ui::Emoji::GetSizeNormal() / factor;\n\t\t\tconst auto shift = (height - size) / 2;\n\t\t\tconst auto skip = (size - AdjustCustomEmojiSize(size)) / 2;\n\t\t\tcustom->paint(p, {\n\t\t\t\t.textColor = (state->selected\n\t\t\t\t\t? stm->textActiveFg\n\t\t\t\t\t: stm->textFg)->c,\n\t\t\t\t.now = crl::now(),\n\t\t\t\t.position = { icon.x() + shift + skip, shift + skip },\n\t\t\t});\n\t\t}\n\t}, result->lifetime());\n\treturn result;\n}\n\n} // namespace\n\nnot_null<Tabs*> CreateTabs(\n\t\tnot_null<QWidget*> parent,\n\t\tUi::Text::CustomEmojiFactory factory,\n\t\tFn<bool()> paused,\n\t\tconst std::vector<Data::MessageReaction> &items,\n\t\tconst Data::ReactionId &selected,\n\t\tUi::WhoReadType whoReadType) {\n\tstruct State {\n\t\trpl::variable<ReactionId> selected;\n\t\tstd::vector<not_null<Ui::AbstractButton*>> tabs;\n\t};\n\tconst auto result = Ui::CreateChild<Tabs>(parent.get());\n\tusing Entry = std::pair<int, ReactionId>;\n\tauto tabs = Ui::CreateChild<Ui::RpWidget>(parent.get());\n\tconst auto st = &st::reactionsTabs;\n\tconst auto state = tabs->lifetime().make_state<State>();\n\tstate->selected = selected;\n\tconst auto append = [&](const ReactionId &reaction, int count) {\n\t\tusing namespace rpl::mappers;\n\t\tconst auto tab = CreateTab(\n\t\t\ttabs,\n\t\t\t*st,\n\t\t\tfactory,\n\t\t\tpaused,\n\t\t\treaction,\n\t\t\twhoReadType,\n\t\t\tcount,\n\t\t\tstate->selected.value() | rpl::map(_1 == reaction));\n\t\ttab->setClickedCallback([=] {\n\t\t\tstate->selected = reaction;\n\t\t});\n\t\tstate->tabs.push_back(tab);\n\t};\n\tauto sorted = std::vector<Entry>();\n\tfor (const auto &reaction : items) {\n\t\tif (reaction.id.emoji() == u\"read\"_q) {\n\t\t\tappend(reaction.id, reaction.count);\n\t\t} else {\n\t\t\tsorted.emplace_back(reaction.count, reaction.id);\n\t\t}\n\t}\n\tranges::sort(sorted, std::greater<>(), &Entry::first);\n\tconst auto count = ranges::accumulate(\n\t\tsorted,\n\t\t0,\n\t\tstd::plus<>(),\n\t\t&Entry::first);\n\tappend(ReactionId(), count);\n\tfor (const auto &[count, reaction] : sorted) {\n\t\tappend(reaction, count);\n\t}\n\tresult->move = [=](int x, int y) {\n\t\ttabs->moveToLeft(x, y);\n\t};\n\tresult->resizeToWidth = [=](int width) {\n\t\tconst auto available = width\n\t\t\t- st->padding.left()\n\t\t\t- st->padding.right();\n\t\tif (available <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tauto left = available;\n\t\tauto height = st->padding.top();\n\t\tfor (const auto &tab : state->tabs) {\n\t\t\tif (left > 0 && available - left < tab->width()) {\n\t\t\t\tleft = 0;\n\t\t\t\theight += tab->height() + st->itemSkip;\n\t\t\t}\n\t\t\ttab->move(\n\t\t\t\tst->padding.left() + left,\n\t\t\t\theight - tab->height() - st->itemSkip);\n\t\t\tleft += tab->width() + st->itemSkip;\n\t\t}\n\t\ttabs->resize(width, height - st->itemSkip + st->padding.bottom());\n\t};\n\tresult->heightValue = [=] {\n\t\tusing namespace rpl::mappers;\n\t\treturn tabs->heightValue() | rpl::map(_1 - st::lineWidth);\n\t};\n\tresult->changes = [=] {\n\t\treturn state->selected.changes();\n\t};\n\treturn result;\n}\n\n} // namespace HistoryView::Reactions\n"
  },
  {
    "path": "Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/text/text_custom_emoji.h\" // Ui::Text::CustomEmojiFactory.\n\nnamespace Ui {\nenum class WhoReadType;\n} // namespace Ui\n\nnamespace Data {\nstruct ReactionId;\nstruct MessageReaction;\n} // namespace Data\n\nnamespace HistoryView::Reactions {\n\nstruct Tabs {\n\tFn<void(int, int)> move;\n\tFn<void(int)> resizeToWidth;\n\tFn<rpl::producer<Data::ReactionId>()> changes;\n\tFn<rpl::producer<int>()> heightValue;\n};\n\n[[nodiscard]] not_null<Tabs*> CreateTabs(\n\tnot_null<QWidget*> parent,\n\tUi::Text::CustomEmojiFactory factory,\n\tFn<bool()> paused,\n\tconst std::vector<Data::MessageReaction> &items,\n\tconst Data::ReactionId &selected,\n\tUi::WhoReadType whoReadType);\n\n} // namespace HistoryView::Reactions\n"
  },
  {
    "path": "Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/bot/earn/info_bot_earn_list.h\"\n\n#include \"api/api_credits.h\"\n#include \"api/api_filter_updates.h\"\n#include \"base/unixtime.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_channel_earn.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"info/bot/earn/info_bot_earn_widget.h\"\n#include \"info/bot/starref/info_bot_starref_common.h\"\n#include \"info/bot/starref/info_bot_starref_join_widget.h\"\n#include \"info/channel_statistics/earn/earn_format.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"info/statistics/info_statistics_inner_widget.h\" // FillLoading.\n#include \"info/statistics/info_statistics_list_controllers.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"statistics/chart_widget.h\"\n#include \"statistics/widgets/chart_header_widget.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/rect.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/slider_natural_width.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_channel_earn.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_statistics.h\"\n\nnamespace Info::BotEarn {\nnamespace {\n\nvoid AddHeader(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\ttr::phrase<> text) {\n\tUi::AddSkip(content);\n\tconst auto header = content->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttext(),\n\t\t\tst::channelEarnHeaderLabel),\n\t\tst::boxRowPadding);\n\theader->resizeToWidth(header->width());\n}\n\n} // namespace\n\nInnerWidget::InnerWidget(QWidget *parent, not_null<Controller*> controller)\n: VerticalLayout(parent)\n, _controller(controller)\n, _show(controller->uiShow()) {\n}\n\nvoid InnerWidget::load() {\n\tconst auto apiLifetime = lifetime().make_state<rpl::lifetime>();\n\n\tconst auto request = [=](Fn<void(Data::CreditsEarnStatistics)> done) {\n\t\tconst auto api = apiLifetime->make_state<Api::CreditsEarnStatistics>(\n\t\t\tpeer()->asUser());\n\t\tapi->request(\n\t\t) | rpl::on_error_done([show = _show](const QString &error) {\n\t\t\tshow->showToast(error);\n\t\t}, [=] {\n\t\t\tdone(api->data());\n\t\t\tapiLifetime->destroy();\n\t\t}, *apiLifetime);\n\t};\n\n\tInfo::Statistics::FillLoading(\n\t\tthis,\n\t\tInfo::Statistics::LoadingType::Earn,\n\t\t_loaded.events_starting_with(false) | rpl::map(!rpl::mappers::_1),\n\t\t_showFinished.events());\n\n\t_showFinished.events(\n\t) | rpl::take(1) | rpl::on_next([=, this, peer = peer()] {\n\t\trequest([=](Data::CreditsEarnStatistics state) {\n\t\t\t_state = state;\n\t\t\t_loaded.fire(true);\n\t\t\tfill();\n\n\t\t\tpeer->session().account().mtpUpdates(\n\t\t\t) | rpl::on_next([=](const MTPUpdates &updates) {\n\t\t\t\tusing TL = MTPDupdateStarsRevenueStatus;\n\t\t\t\tApi::PerformForUpdate<TL>(updates, [&](const TL &d) {\n\t\t\t\t\tconst auto peerId = peerFromMTP(d.vpeer());\n\t\t\t\t\tif (peerId == peer->id) {\n\t\t\t\t\t\trequest([=](Data::CreditsEarnStatistics state) {\n\t\t\t\t\t\t\t_state = state;\n\t\t\t\t\t\t\t_stateUpdated.fire({});\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}, lifetime());\n\t\t});\n\t}, lifetime());\n}\n\nvoid InnerWidget::fill() {\n\tusing namespace Info::ChannelEarn;\n\tconst auto container = this;\n\tconst auto &data = _state;\n\tconst auto multiplier = data.usdRate;\n\tconstexpr auto kMinorLength = 3;\n\n\tauto availableBalanceValue = rpl::single(\n\t\tdata.availableBalance\n\t) | rpl::then(\n\t\t_stateUpdated.events() | rpl::map([=] {\n\t\t\treturn _state.availableBalance;\n\t\t})\n\t);\n\tauto overallBalanceValue = rpl::single(\n\t\tdata.overallRevenue\n\t) | rpl::then(\n\t\t_stateUpdated.events() | rpl::map([=] {\n\t\t\treturn _state.overallRevenue;\n\t\t})\n\t);\n\tauto valueToString = [](CreditsAmount v) {\n\t\treturn Lang::FormatCreditsAmountDecimal(v);\n\t};\n\n\tif (data.revenueGraph.chart) {\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\tusing Type = Statistic::ChartViewType;\n\t\tconst auto widget = container->add(\n\t\t\tobject_ptr<Statistic::ChartWidget>(container),\n\t\t\tst::statisticsLayerMargins);\n\n\t\tauto chart = data.revenueGraph.chart;\n\t\tchart.currencyRate = data.usdRate;\n\n\t\twidget->setChartData(chart, Type::StackBar);\n\t\twidget->setTitle(tr::lng_bot_earn_chart_revenue());\n\t\tUi::AddSkip(container);\n\t\tUi::AddDivider(container);\n\t\tUi::AddSkip(container);\n\t\tStatistic::FixCacheForHighDPIChartWidget(container);\n\t}\n\t{\n\t\tAddHeader(container, tr::lng_bot_earn_overview_title);\n\t\tUi::AddSkip(container, st::channelEarnOverviewTitleSkip);\n\n\t\tconst auto addOverview = [&](\n\t\t\t\trpl::producer<CreditsAmount> value,\n\t\t\t\tconst tr::phrase<> &text) {\n\t\t\tconst auto line = container->add(\n\t\t\t\tUi::CreateSkipWidget(container, 0),\n\t\t\t\tst::boxRowPadding);\n\t\t\tconst auto majorLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\tline,\n\t\t\t\trpl::duplicate(value) | rpl::map(valueToString),\n\t\t\t\tst::channelEarnOverviewMajorLabel);\n\t\t\tconst auto icon = Ui::CreateSingleStarWidget(\n\t\t\t\tline,\n\t\t\t\tmajorLabel->height());\n\t\t\tconst auto secondMinorLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\tline,\n\t\t\t\tstd::move(\n\t\t\t\t\tvalue\n\t\t\t\t) | rpl::map([=](CreditsAmount v) {\n\t\t\t\t\treturn v\n\t\t\t\t\t\t? ToUsd(v, multiplier, kMinorLength)\n\t\t\t\t\t\t: QString();\n\t\t\t\t}),\n\t\t\t\tst::channelEarnOverviewSubMinorLabel);\n\t\t\trpl::combine(\n\t\t\t\tline->widthValue(),\n\t\t\t\tmajorLabel->sizeValue()\n\t\t\t) | rpl::on_next([=](int available, const QSize &size) {\n\t\t\t\tline->resize(line->width(), size.height());\n\t\t\t\tmajorLabel->moveToLeft(\n\t\t\t\t\ticon->width() + st::channelEarnOverviewMinorLabelSkip,\n\t\t\t\t\tmajorLabel->y());\n\t\t\t\tsecondMinorLabel->resizeToWidth(available\n\t\t\t\t\t- size.width()\n\t\t\t\t\t- icon->width());\n\t\t\t\tsecondMinorLabel->moveToLeft(\n\t\t\t\t\trect::right(majorLabel)\n\t\t\t\t\t\t+ st::channelEarnOverviewSubMinorLabelPos.x(),\n\t\t\t\t\tst::channelEarnOverviewSubMinorLabelPos.y());\n\t\t\t}, majorLabel->lifetime());\n\t\t\tUi::ToggleChildrenVisibility(line, true);\n\n\t\t\tUi::AddSkip(container);\n\t\t\tcontainer->add(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tcontainer,\n\t\t\t\t\ttext(),\n\t\t\t\t\tst::channelEarnOverviewSubMinorLabel),\n\t\t\t\tst::boxRowPadding);\n\t\t};\n\t\taddOverview(\n\t\t\trpl::duplicate(availableBalanceValue),\n\t\t\ttr::lng_bot_earn_available);\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\taddOverview(\n\t\t\trpl::single(data.currentBalance),\n\t\t\ttr::lng_bot_earn_reward);\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\taddOverview(\n\t\t\trpl::duplicate(overallBalanceValue),\n\t\t\ttr::lng_bot_earn_total);\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\tUi::AddDividerText(container, tr::lng_bot_earn_balance_about());\n\t\tUi::AddSkip(container);\n\t}\n\t{\n\t\tAddHeader(container, tr::lng_bot_earn_balance_title);\n\t\tUi::AddSkip(container);\n\t\tauto dateValue = rpl::single(\n\t\t\tdata.nextWithdrawalAt\n\t\t) | rpl::then(\n\t\t\t_stateUpdated.events() | rpl::map([=] {\n\t\t\t\treturn _state.nextWithdrawalAt;\n\t\t\t})\n\t\t);\n\t\t::Settings::AddWithdrawalWidget(\n\t\t\tcontainer,\n\t\t\t_controller->parentController(),\n\t\t\tpeer(),\n\t\t\trpl::single(\n\t\t\t\tdata.buyAdsUrl\n\t\t\t) | rpl::then(\n\t\t\t\t_stateUpdated.events() | rpl::map([=] {\n\t\t\t\t\treturn _state.buyAdsUrl;\n\t\t\t\t})\n\t\t\t),\n\t\t\trpl::duplicate(availableBalanceValue),\n\t\t\trpl::duplicate(dateValue),\n\t\t\t_state.isWithdrawalEnabled,\n\t\t\trpl::duplicate(\n\t\t\t\tavailableBalanceValue\n\t\t\t) | rpl::map([=](CreditsAmount v) {\n\t\t\t\treturn v ? ToUsd(v, multiplier, kMinorLength) : QString();\n\t\t\t}));\n\t\tcontainer->resizeToWidth(container->width());\n\t}\n\tif (BotStarRef::Join::Allowed(peer()) && !peer()->isSelf()) {\n\t\tconst auto button = BotStarRef::AddViewListButton(\n\t\t\tcontainer,\n\t\t\ttr::lng_credits_summary_earn_title(),\n\t\t\ttr::lng_credits_summary_earn_about(),\n\t\t\ttrue);\n\t\tbutton->setClickedCallback([=] {\n\t\t\t_controller->showSection(BotStarRef::Join::Make(peer()));\n\t\t});\n\t\tUi::AddSkip(container);\n\t\tUi::AddDivider(container);\n\t}\n\tif (!peer()->isSelf()) {\n\t\tfillHistory();\n\t}\n}\n\nvoid InnerWidget::fillHistory() {\n\tconst auto container = this;\n\tUi::AddSkip(container, st::settingsPremiumOptionsPadding.top());\n\tconst auto history = container->add(\n\t\tobject_ptr<Ui::VerticalLayout>(container));\n\n\tconst auto sectionIndex = history->lifetime().make_state<int>(0);\n\n\tconst auto fill = [=, peer = peer()](\n\t\t\tnot_null<PeerData*> premiumBot,\n\t\t\tconst Data::CreditsStatusSlice &fullSlice,\n\t\t\tconst Data::CreditsStatusSlice &inSlice,\n\t\t\tconst Data::CreditsStatusSlice &outSlice) {\n\t\tif (fullSlice.list.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto inner = history->add(\n\t\t\tobject_ptr<Ui::VerticalLayout>(history));\n\t\tconst auto hasOneTab = inSlice.list.empty() && outSlice.list.empty();\n\t\tconst auto hasIn = !inSlice.list.empty();\n\t\tconst auto hasOut = !outSlice.list.empty();\n\t\tconst auto fullTabText = tr::lng_credits_summary_history_tab_full(\n\t\t\ttr::now);\n\t\tconst auto inTabText = tr::lng_credits_summary_history_tab_in(\n\t\t\ttr::now);\n\t\tconst auto outTabText = tr::lng_credits_summary_history_tab_out(\n\t\t\ttr::now);\n\t\tif (hasOneTab) {\n\t\t\tconst auto header = inner->add(\n\t\t\t\tobject_ptr<Statistic::Header>(inner),\n\t\t\t\tst::statisticsLayerMargins\n\t\t\t\t\t+ st::boostsChartHeaderPadding);\n\t\t\theader->resizeToWidth(header->width());\n\t\t\theader->setTitle(fullTabText);\n\t\t\theader->setSubTitle({});\n\t\t}\n\n\t\tconst auto slider = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::CustomWidthSlider>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::CustomWidthSlider>(\n\t\t\t\t\tinner,\n\t\t\t\t\tst::defaultTabsSlider)),\n\t\t\tst::boxRowPadding);\n\t\tslider->toggle(!hasOneTab, anim::type::instant);\n\n\t\tslider->entity()->addSection(fullTabText);\n\t\tif (hasIn) {\n\t\t\tslider->entity()->addSection(inTabText);\n\t\t}\n\t\tif (hasOut) {\n\t\t\tslider->entity()->addSection(outTabText);\n\t\t}\n\n\t\tslider->entity()->setActiveSectionFast(*sectionIndex);\n\n\t\t{\n\t\t\tconst auto &st = st::defaultTabsSlider;\n\t\t\tslider->entity()->setNaturalWidth(0\n\t\t\t\t+ st.labelStyle.font->width(fullTabText)\n\t\t\t\t+ (hasIn ? st.labelStyle.font->width(inTabText) : 0)\n\t\t\t\t+ (hasOut ? st.labelStyle.font->width(outTabText) : 0)\n\t\t\t\t+ rect::m::sum::h(st::boxRowPadding));\n\t\t}\n\n\t\tconst auto fullWrap = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\t\tconst auto inWrap = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\t\tconst auto outWrap = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\n\t\trpl::single(slider->entity()->activeSection()) | rpl::then(\n\t\t\tslider->entity()->sectionActivated()\n\t\t) | rpl::on_next([=](int index) {\n\t\t\tif (index == 0) {\n\t\t\t\tfullWrap->toggle(true, anim::type::instant);\n\t\t\t\tinWrap->toggle(false, anim::type::instant);\n\t\t\t\toutWrap->toggle(false, anim::type::instant);\n\t\t\t} else if (index == 1) {\n\t\t\t\tinWrap->toggle(true, anim::type::instant);\n\t\t\t\tfullWrap->toggle(false, anim::type::instant);\n\t\t\t\toutWrap->toggle(false, anim::type::instant);\n\t\t\t} else {\n\t\t\t\toutWrap->toggle(true, anim::type::instant);\n\t\t\t\tfullWrap->toggle(false, anim::type::instant);\n\t\t\t\tinWrap->toggle(false, anim::type::instant);\n\t\t\t}\n\t\t\t*sectionIndex = index;\n\t\t}, inner->lifetime());\n\n\t\tconst auto controller = _controller->parentController();\n\t\tconst auto entryClicked = [=](\n\t\t\t\tconst Data::CreditsHistoryEntry &e,\n\t\t\t\tconst Data::SubscriptionEntry &s) {\n\t\t\tcontroller->uiShow()->show(Box(\n\t\t\t\t::Settings::ReceiptCreditsBox,\n\t\t\t\tcontroller,\n\t\t\t\te,\n\t\t\t\ts));\n\t\t};\n\n\t\tInfo::Statistics::AddCreditsHistoryList(\n\t\t\tcontroller->uiShow(),\n\t\t\tfullSlice,\n\t\t\tfullWrap->entity(),\n\t\t\tentryClicked,\n\t\t\tpeer,\n\t\t\ttrue,\n\t\t\ttrue);\n\t\tInfo::Statistics::AddCreditsHistoryList(\n\t\t\tcontroller->uiShow(),\n\t\t\tinSlice,\n\t\t\tinWrap->entity(),\n\t\t\tentryClicked,\n\t\t\tpeer,\n\t\t\ttrue,\n\t\t\tfalse);\n\t\tInfo::Statistics::AddCreditsHistoryList(\n\t\t\tcontroller->uiShow(),\n\t\t\toutSlice,\n\t\t\toutWrap->entity(),\n\t\t\tstd::move(entryClicked),\n\t\t\tpeer,\n\t\t\tfalse,\n\t\t\ttrue);\n\n\t\tUi::AddSkip(inner);\n\t\tUi::AddSkip(inner);\n\t};\n\n\tconst auto apiLifetime = history->lifetime().make_state<rpl::lifetime>();\n\trpl::single(rpl::empty) | rpl::then(\n\t\t_stateUpdated.events()\n\t) | rpl::on_next([=, peer = peer()] {\n\t\tusing Api = Api::CreditsHistory;\n\t\tconst auto apiFull = apiLifetime->make_state<Api>(peer, true, true);\n\t\tconst auto apiIn = apiLifetime->make_state<Api>(peer, true, false);\n\t\tconst auto apiOut = apiLifetime->make_state<Api>(peer, false, true);\n\t\tapiFull->request({}, [=](Data::CreditsStatusSlice fullSlice) {\n\t\t\tapiIn->request({}, [=](Data::CreditsStatusSlice inSlice) {\n\t\t\t\tapiOut->request({}, [=](Data::CreditsStatusSlice outSlice) {\n\t\t\t\t\t::Api::PremiumPeerBot(\n\t\t\t\t\t\t&_controller->session()\n\t\t\t\t\t) | rpl::on_next([=](not_null<PeerData*> bot) {\n\t\t\t\t\t\tfill(bot, fullSlice, inSlice, outSlice);\n\t\t\t\t\t\tcontainer->resizeToWidth(container->width());\n\t\t\t\t\t\twhile (history->count() > 1) {\n\t\t\t\t\t\t\tdelete history->widgetAt(0);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tapiLifetime->destroy();\n\t\t\t\t\t}, *apiLifetime);\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\t}, history->lifetime());\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\tmemento->setState(base::take(_state));\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\t_state = memento->state();\n\tif (_state) {\n\t\tfill();\n\t} else {\n\t\tload();\n\t}\n\tUi::RpWidget::resizeToWidth(width());\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nauto InnerWidget::showRequests() const -> rpl::producer<ShowRequest> {\n\treturn _showRequests.events();\n}\n\nvoid InnerWidget::showFinished() {\n\t_showFinished.fire({});\n}\n\nvoid InnerWidget::setInnerFocus() {\n\t_focusRequested.fire({});\n}\n\nnot_null<PeerData*> InnerWidget::peer() const {\n\treturn _controller->statisticsTag().peer;\n}\n\n} // namespace Info::BotEarn\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_credits_earn.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/vertical_layout.h\"\n\nnamespace Ui {\nclass Show;\n} // namespace Ui\n\nnamespace Info {\nclass Controller;\n} // namespace Info\n\nnamespace Info::BotEarn {\n\nclass Memento;\n\n[[nodiscard]] QImage IconCurrency(\n\tconst style::FlatLabel &label,\n\tconst QColor &c);\n\nclass InnerWidget final : public Ui::VerticalLayout {\npublic:\n\tstruct ShowRequest final {\n\t};\n\n\tInnerWidget(QWidget *parent, not_null<Controller*> controller);\n\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\n\t[[nodiscard]] rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\t[[nodiscard]] rpl::producer<ShowRequest> showRequests() const;\n\n\tvoid showFinished();\n\tvoid setInnerFocus();\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\nprivate:\n\tvoid load();\n\tvoid fill();\n\tvoid fillHistory();\n\n\tnot_null<Controller*> _controller;\n\tstd::shared_ptr<Ui::Show> _show;\n\n\tData::CreditsEarnStatistics _state;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\trpl::event_stream<ShowRequest> _showRequests;\n\trpl::event_stream<> _showFinished;\n\trpl::event_stream<> _focusRequested;\n\trpl::event_stream<bool> _loaded;\n\trpl::event_stream<> _stateUpdated;\n\n};\n\n} // namespace Info::BotEarn\n"
  },
  {
    "path": "Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/bot/earn/info_bot_earn_widget.h\"\n\n#include \"info/bot/earn/info_bot_earn_list.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/ui_utility.h\"\n\nnamespace Info::BotEarn {\n\nMemento::Memento(not_null<Controller*> controller)\n: ContentMemento(controller->statisticsTag()) {\n}\n\nMemento::Memento(not_null<PeerData*> peer)\n: ContentMemento(Info::Statistics::Tag{ peer, {}, {} }) {\n}\n\nMemento::~Memento() = default;\n\nSection Memento::section() const {\n\treturn Section(Section::Type::BotEarn);\n}\n\nvoid Memento::setState(SavedState state) {\n\t_state = std::move(state);\n}\n\nMemento::SavedState Memento::state() {\n\treturn base::take(_state);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(parent, controller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: ContentWidget(parent, controller)\n, _inner(setInnerWidget(object_ptr<InnerWidget>(this, controller))) {\n\t_inner->showRequests(\n\t) | rpl::on_next([=](InnerWidget::ShowRequest request) {\n\t}, _inner->lifetime());\n\t_inner->scrollToRequests(\n\t) | rpl::on_next([=](const Ui::ScrollToRequest &request) {\n\t\tscrollTo(request);\n\t}, _inner->lifetime());\n}\n\nnot_null<PeerData*> Widget::peer() const {\n\treturn _inner->peer();\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\treturn (memento->statisticsTag().peer == peer());\n}\n\nrpl::producer<QString> Widget::title() {\n\treturn tr::lng_bot_earn_title();\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nrpl::producer<bool> Widget::desiredShadowVisibility() const {\n\treturn rpl::single<bool>(true);\n}\n\nvoid Widget::showFinished() {\n\t_inner->showFinished();\n}\n\nvoid Widget::setInnerFocus() {\n\t_inner->setInnerFocus();\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(controller());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\nstd::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer) {\n\treturn std::make_shared<Info::Memento>(\n\t\tstd::vector<std::shared_ptr<ContentMemento>>(\n\t\t\t1,\n\t\t\tstd::make_shared<Memento>(peer)));\n}\n\n} // namespace Info::BotEarn\n"
  },
  {
    "path": "Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_credits_earn.h\"\n#include \"info/info_content_widget.h\"\n\nnamespace Info::BotEarn {\n\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\tMemento(not_null<Controller*> controller);\n\tMemento(not_null<PeerData*> peer);\n\t~Memento();\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\tusing SavedState = Data::CreditsEarnStatistics;\n\n\tvoid setState(SavedState states);\n\t[[nodiscard]] SavedState state();\n\nprivate:\n\tSavedState _state;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(QWidget *parent, not_null<Controller*> controller);\n\n\tbool showInternal(not_null<ContentMemento*> memento) override;\n\trpl::producer<QString> title() override;\n\trpl::producer<bool> desiredShadowVisibility() const override;\n\tvoid showFinished() override;\n\tvoid setInnerFocus() override;\n\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tconst not_null<InnerWidget*> _inner;\n\n};\n\n[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer);\n\n} // namespace Info::BotEarn\n"
  },
  {
    "path": "Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/bot/starref/info_bot_starref_common.h\"\n\n#include \"apiwrap.h\"\n#include \"boxes/peers/replace_boost_box.h\" // CreateUserpicsTransfer.\n#include \"boxes/send_credits_box.h\" // Ui::CreditsEmoji.\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"core/ui_integration.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"history/view/media/history_view_sticker_player.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/controls/who_reacted_context_action.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/table_layout.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/new_badges.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace Info::BotStarRef {\nnamespace {\n\nvoid ConnectStarRef(\n\t\tnot_null<UserData*> bot,\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void(ConnectedBot)> done,\n\t\tFn<void(const QString &)> fail) {\n\tbot->session().api().request(MTPpayments_ConnectStarRefBot(\n\t\tpeer->input(),\n\t\tbot->inputUser()\n\t)).done([=](const MTPpayments_ConnectedStarRefBots &result) {\n\t\tconst auto parsed = Parse(&bot->session(), result);\n\t\tif (parsed.empty()) {\n\t\t\tfail(u\"EMPTY\"_q);\n\t\t} else {\n\t\t\tdone(parsed.front());\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tfail(error.type());\n\t}).send();\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> CreateLinkIcon(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Main::Session*> session,\n\t\tint users) {\n\tauto result = object_ptr<Ui::RpWidget>(parent);\n\tconst auto raw = result.data();\n\n\tstruct State {\n\t\tnot_null<DocumentData*> icon;\n\t\tstd::shared_ptr<Data::DocumentMedia> media;\n\t\tstd::shared_ptr<HistoryView::StickerPlayer> player;\n\t\tint counterWidth = 0;\n\t};\n\tconst auto outerSide = st::starrefLinkThumbOuter;\n\tconst auto outerSkip = (outerSide - st::starrefLinkThumbInner) / 2;\n\tconst auto innerSide = (outerSide - 2 * outerSkip);\n\tconst auto add = st::starrefLinkCountAdd;\n\tconst auto outer = QSize(outerSide, outerSide + add);\n\tconst auto inner = QSize(innerSide, innerSide);\n\tconst auto state = raw->lifetime().make_state<State>(State{\n\t\t.icon = ChatHelpers::GenerateLocalTgsSticker(\n\t\t\tsession,\n\t\t\tu\"starref_link\"_q,\n\t\t\ttrue),\n\t});\n\tstate->media = state->icon->createMediaView();\n\tstate->player = std::make_unique<HistoryView::LottiePlayer>(\n\t\tChatHelpers::LottiePlayerFromDocument(\n\t\t\tstate->media.get(),\n\t\t\tChatHelpers::StickerLottieSize::MessageHistory,\n\t\t\tinner,\n\t\t\tLottie::Quality::High));\n\tconst auto player = state->player.get();\n\tplayer->setRepaintCallback([=] { raw->update(); });\n\n\tconst auto text = users\n\t\t? Lang::FormatCountToShort(users).string\n\t\t: QString();\n\tconst auto length = st::starrefLinkCountFont->width(text);\n\tconst auto contents = length + st::starrefLinkCountIcon.width();\n\tconst auto delta = (outer.width() - contents) / 2;\n\tconst auto badge = QRect(\n\t\tdelta,\n\t\touter.height() - st::starrefLinkCountFont->height - st::lineWidth,\n\t\touter.width() - 2 * delta,\n\t\tst::starrefLinkCountFont->height);\n\tconst auto badgeRect = badge.marginsAdded(st::starrefLinkCountPadding);\n\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::windowBgActive);\n\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tconst auto left = (raw->width() - outer.width()) / 2;\n\t\tp.drawEllipse(left, 0, outerSide, outerSide);\n\n\t\tif (!text.isEmpty()) {\n\t\t\tconst auto rect = badgeRect.translated(left, 0);\n\t\t\tconst auto textRect = badge.translated(left, 0);\n\t\t\tconst auto radius = st::starrefLinkCountFont->height / 2.;\n\t\t\tp.setPen(st::historyPeerUserpicFg);\n\t\t\tp.setBrush(st::historyPeer2UserpicBg2);\n\t\t\tp.drawRoundedRect(rect, radius, radius);\n\n\t\t\tp.setFont(st::starrefLinkCountFont);\n\t\t\tconst auto shift = QPoint(\n\t\t\t\tst::starrefLinkCountIcon.width(),\n\t\t\t\tst::starrefLinkCountFont->ascent);\n\t\t\tst::starrefLinkCountIcon.paint(\n\t\t\t\tp,\n\t\t\t\ttextRect.topLeft() + st::starrefLinkCountIconPosition,\n\t\t\t\traw->width());\n\t\t\tp.drawText(textRect.topLeft() + shift, text);\n\t\t}\n\t\tif (player->ready()) {\n\t\t\tconst auto now = crl::now();\n\t\t\tconst auto color = st::windowFgActive->c;\n\t\t\tauto info = player->frame(inner, color, false, now, false);\n\t\t\tp.drawImage(\n\t\t\t\tQRect(QPoint(left + outerSkip, outerSkip), inner),\n\t\t\t\tinfo.image);\n\t\t\tif (info.index + 1 < player->framesCount()) {\n\t\t\t\tplayer->markFrameShown();\n\t\t\t}\n\t\t}\n\t}, raw->lifetime());\n\n\traw->resize(outer);\n\n\treturn result;\n}\n\nvoid ChooseRecipient(\n\t\tnot_null<Ui::RpWidget*> button,\n\t\tconst std::vector<not_null<PeerData*>> &list,\n\t\tnot_null<PeerData*> now,\n\t\tFn<void(not_null<PeerData*>)> done) {\n\tconst auto menu = Ui::CreateChild<Ui::PopupMenu>(\n\t\tbutton,\n\t\tst::starrefPopupMenu);\n\n\tstruct Entry {\n\t\tnot_null<Ui::WhoReactedEntryAction*> action;\n\t\tstd::shared_ptr<Ui::DynamicImage> userpic;\n\t};\n\tauto actions = std::make_shared<std::vector<Entry>>();\n\tactions->reserve(list.size());\n\tfor (const auto &peer : list) {\n\t\tauto view = peer->createUserpicView();\n\t\tauto action = base::make_unique_q<Ui::WhoReactedEntryAction>(\n\t\t\tmenu->menu(),\n\t\t\tData::ReactedMenuFactory(&list.front()->session()),\n\t\t\tmenu->menu()->st(),\n\t\t\tUi::WhoReactedEntryData());\n\t\tconst auto index = int(actions->size());\n\t\tactions->push_back({ action.get(), Ui::MakeUserpicThumbnail(peer) });\n\n\t\tconst auto updateUserpic = [=] {\n\t\t\tconst auto size = st::defaultWhoRead.photoSize;\n\t\t\tactions->at(index).action->setData({\n\t\t\t\t.text = peer->name(),\n\t\t\t\t.date = (peer->isSelf()\n\t\t\t\t\t? tr::lng_group_call_join_as_personal(tr::now)\n\t\t\t\t\t: peer->isUser()\n\t\t\t\t\t? tr::lng_status_bot(tr::now)\n\t\t\t\t\t: peer->isBroadcast()\n\t\t\t\t\t? tr::lng_channel_status(tr::now)\n\t\t\t\t\t: tr::lng_group_status(tr::now)),\n\t\t\t\t.type = (peer == now\n\t\t\t\t\t? Ui::WhoReactedType::RefRecipientNow\n\t\t\t\t\t: Ui::WhoReactedType::RefRecipient),\n\t\t\t\t.userpic = actions->at(index).userpic->image(size),\n\t\t\t\t.callback = [=] { done(peer); },\n\t\t\t});\n\t\t};\n\t\tactions->back().userpic->subscribeToUpdates(updateUserpic);\n\n\t\tmenu->addAction(std::move(action));\n\t\tupdateUserpic();\n\t}\n\n\tmenu->popup(button->mapToGlobal(QPoint(button->width() / 2, 0)));\n}\n\n} // namespace\n\nQString FormatCommission(ushort commission) {\n\treturn QString::number(commission / 10.) + '%';\n}\n\nQString FormatProgramDuration(int durationMonths) {\n\treturn !durationMonths\n\t\t? tr::lng_star_ref_duration_forever(tr::now)\n\t\t: (durationMonths < 12)\n\t\t? tr::lng_months(tr::now, lt_count, durationMonths)\n\t\t: tr::lng_years(tr::now, lt_count, durationMonths / 12);\n}\n\nrpl::producer<TextWithEntities> FormatForProgramDuration(\n\t\tint durationMonths) {\n\treturn !durationMonths\n\t\t? tr::lng_star_ref_one_about_for_forever(tr::rich)\n\t\t: (durationMonths < 12)\n\t\t? tr::lng_star_ref_one_about_for_months(\n\t\t\tlt_count,\n\t\t\trpl::single(durationMonths * 1.),\n\t\t\ttr::rich)\n\t\t: tr::lng_star_ref_one_about_for_years(\n\t\t\tlt_count,\n\t\t\trpl::single((durationMonths / 12) * 1.),\n\t\t\ttr::rich);\n}\n\nnot_null<Ui::AbstractButton*> AddViewListButton(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\trpl::producer<QString> title,\n\t\trpl::producer<QString> subtitle,\n\t\tbool newBadge) {\n\tconst auto &stLabel = st::defaultFlatLabel;\n\tconst auto iconSize = st::settingsPremiumIconDouble.size();\n\tconst auto &titlePadding = st::settingsPremiumRowTitlePadding;\n\tconst auto &descriptionPadding = st::settingsPremiumRowAboutPadding;\n\n\tconst auto button = Ui::CreateChild<Ui::SettingsButton>(\n\t\tparent,\n\t\trpl::single(QString()));\n\tbutton->show();\n\n\tconst auto label = parent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tparent,\n\t\t\tstd::move(title) | rpl::map(tr::bold),\n\t\t\tstLabel),\n\t\ttitlePadding);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tconst auto description = parent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tparent,\n\t\t\tstd::move(subtitle),\n\t\t\tst::boxDividerLabel),\n\t\tdescriptionPadding);\n\tdescription->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tif (newBadge) {\n\t\tUi::NewBadge::AddAfterLabel(parent, label);\n\t}\n\n\tconst auto dummy = Ui::CreateChild<Ui::AbstractButton>(parent);\n\tdummy->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tdummy->show();\n\n\tparent->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tdummy->resize(s.width(), iconSize.height());\n\t}, dummy->lifetime());\n\n\tbutton->geometryValue(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tdummy->moveToLeft(0, r.y() + (r.height() - iconSize.height()) / 2);\n\t}, dummy->lifetime());\n\n\t::Settings::AddButtonIcon(dummy, st::settingsButton, {\n\t\t.icon = &st::settingsStarRefEarnStars,\n\t\t.backgroundBrush = st::premiumIconBg3,\n\t});\n\n\trpl::combine(\n\t\tparent->widthValue(),\n\t\tlabel->heightValue(),\n\t\tdescription->heightValue()\n\t) | rpl::on_next([=,\n\t\ttopPadding = titlePadding,\n\t\tbottomPadding = descriptionPadding](\n\t\t\tint width,\n\t\t\tint topHeight,\n\t\t\tint bottomHeight) {\n\t\tbutton->resize(\n\t\t\twidth,\n\t\t\ttopPadding.top()\n\t\t\t+ topHeight\n\t\t\t+ topPadding.bottom()\n\t\t\t+ bottomPadding.top()\n\t\t\t+ bottomHeight\n\t\t\t+ bottomPadding.bottom());\n\t}, button->lifetime());\n\tlabel->topValue(\n\t) | rpl::on_next([=, padding = titlePadding.top()](int top) {\n\t\tbutton->moveToLeft(0, top - padding);\n\t}, button->lifetime());\n\tconst auto arrow = Ui::CreateChild<Ui::IconButton>(\n\t\tbutton,\n\t\tst::backButton);\n\tarrow->setIconOverride(\n\t\t&st::settingsPremiumArrow,\n\t\t&st::settingsPremiumArrowOver);\n\tarrow->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tbutton->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tconst auto &point = st::settingsPremiumArrowShift;\n\t\tarrow->moveToRight(\n\t\t\t-point.x(),\n\t\t\tpoint.y() + (s.height() - arrow->height()) / 2);\n\t}, arrow->lifetime());\n\n\treturn button;\n}\n\nvoid AddFullWidthButtonFooter(\n\t\tnot_null<Ui::BoxContent*> box,\n\t\tnot_null<Ui::RpWidget*> button,\n\t\trpl::producer<TextWithEntities> text) {\n\tconst auto footer = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton->parentWidget(),\n\t\tstd::move(text),\n\t\tst::starrefJoinFooter);\n\tfooter->setTryMakeSimilarLines(true);\n\tbutton->geometryValue() | rpl::on_next([=](QRect geometry) {\n\t\tfooter->resizeToWidth(geometry.width());\n\t\tconst auto &st = box->getDelegate()->style();\n\t\tconst auto top = geometry.y() + geometry.height();\n\t\tconst auto available = st.buttonPadding.bottom();\n\t\tfooter->moveToLeft(\n\t\t\tgeometry.left(),\n\t\t\ttop + (available - footer->height()) / 2);\n\t}, footer->lifetime());\n}\n\nobject_ptr<Ui::AbstractButton> MakeLinkLabel(\n\t\tnot_null<QWidget*> parent,\n\t\tconst QString &link,\n\t\tconst style::InputField *stOverride) {\n\tconst auto &st = stOverride ? *stOverride : st::dialogsFilter;\n\tconst auto text = Ui::Text::StripUrlProtocol(link);\n\tconst auto margins = st.textMargins;\n\tconst auto height = st.heightMin;\n\tconst auto skip = margins.left();\n\n\tauto result = object_ptr<Ui::AbstractButton>(parent);\n\tconst auto raw = result.data();\n\n\traw->resize(height, height);\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st.textBg);\n\t\tconst auto radius = st::roundRadiusLarge;\n\t\tp.drawRoundedRect(0, 0, raw->width(), height, radius, radius);\n\n\t\tconst auto font = st.style.font;\n\t\tp.setPen(st.textFg);\n\t\tp.setFont(font);\n\t\tconst auto available = raw->width() - skip * 2;\n\t\tp.drawText(\n\t\t\tQRect(skip, margins.top(), available, font->height),\n\t\t\tstyle::al_top,\n\t\t\tfont->elided(text, available));\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\nobject_ptr<Ui::BoxContent> StarRefLinkBox(\n\t\tConnectedBot row,\n\t\tnot_null<PeerData*> peer) {\n\treturn Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto show = box->uiShow();\n\n\t\tconst auto bot = row.bot;\n\t\tconst auto program = row.state.program;\n\n\t\tbox->setStyle(st::starrefFooterBox);\n\t\tbox->setNoContentMargin(true);\n\t\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\n\t\tbox->addRow(\n\t\t\tCreateLinkIcon(box, &bot->session(), row.state.users),\n\t\t\tst::boxRowPadding + st::starrefJoinUserpicsPadding);\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_star_ref_link_title(),\n\t\t\t\tst::boxTitle),\n\t\t\tst::boxRowPadding + st::starrefJoinTitlePadding,\n\t\t\tstyle::al_top);\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\t(peer->isSelf()\n\t\t\t\t\t? tr::lng_star_ref_link_about_user\n\t\t\t\t\t: peer->isUser()\n\t\t\t\t\t? tr::lng_star_ref_link_about_user\n\t\t\t\t\t: tr::lng_star_ref_link_about_channel)(\n\t\t\t\t\t\tlt_amount,\n\t\t\t\t\t\trpl::single(tr::bold(\n\t\t\t\t\t\t\tFormatCommission(program.commission))),\n\t\t\t\t\t\tlt_app,\n\t\t\t\t\t\trpl::single(tr::bold(bot->name())),\n\t\t\t\t\t\tlt_duration,\n\t\t\t\t\t\tFormatForProgramDuration(program.durationMonths),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\tst::starrefCenteredText),\n\t\t\tstyle::al_top);\n\n\t\tUi::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 3);\n\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_star_ref_link_recipient(),\n\t\t\t\tst::starrefCenteredText),\n\t\t\tstyle::al_top);\n\t\tUi::AddSkip(box->verticalLayout());\n\t\tbox->addRow(object_ptr<Ui::AbstractButton>::fromRaw(\n\t\t\tMakePeerBubbleButton(box, peer).release()\n\t\t))->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t\tUi::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 2);\n\t\tconst auto preview = box->addRow(MakeLinkLabel(box, row.state.link));\n\t\tUi::AddSkip(box->verticalLayout());\n\n\t\tconst auto copy = [=](bool close) {\n\t\t\treturn [=] {\n\t\t\t\tQApplication::clipboard()->setText(row.state.link);\n\t\t\t\tbox->uiShow()->showToast(tr::lng_username_copied(tr::now));\n\t\t\t\tif (close) {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t}\n\t\t\t};\n\t\t};\n\t\tpreview->setClickedCallback(copy(false));\n\t\tconst auto button = box->addButton(\n\t\t\ttr::lng_star_ref_link_copy(),\n\t\t\t[=] { copy(true); },\n\t\t\tst::starrefCopyButton);\n\n\t\tconst auto name = TextWithEntities{ bot->name() };\n\t\tAddFullWidthButtonFooter(\n\t\t\tbox,\n\t\t\tbutton,\n\t\t\t(row.state.users > 0\n\t\t\t\t? tr::lng_star_ref_link_copy_users(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(row.state.users * 1.),\n\t\t\t\t\tlt_app,\n\t\t\t\t\trpl::single(name),\n\t\t\t\t\ttr::marked)\n\t\t\t\t: tr::lng_star_ref_link_copy_none(\n\t\t\t\t\tlt_app,\n\t\t\t\t\trpl::single(name),\n\t\t\t\t\ttr::marked)));\n\t});\n}\n\nobject_ptr<Ui::BoxContent> JoinStarRefBox(\n\t\tConnectedBot row,\n\t\tnot_null<PeerData*> initialRecipient,\n\t\tstd::vector<not_null<PeerData*>> recipients,\n\t\tFn<void(ConnectedBotState)> done) {\n\tExpects(row.bot->isUser());\n\n\treturn Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto show = box->uiShow();\n\n\t\tconst auto bot = row.bot;\n\t\tconst auto program = row.state.program;\n\t\tauto list = recipients;\n\t\tif (!list.empty()) {\n\t\t\tlist.erase(ranges::remove(list, bot), end(list));\n\t\t\tif (!ranges::contains(list, initialRecipient)) {\n\t\t\t\tlist.insert(begin(list), initialRecipient);\n\t\t\t}\n\t\t}\n\n\t\tbox->setStyle(st::starrefFooterBox);\n\t\tbox->setNoContentMargin(true);\n\t\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\n\t\tstruct State {\n\t\t\trpl::variable<not_null<PeerData*>> recipient;\n\t\t\tbase::weak_qptr<Ui::GenericBox> weak;\n\t\t\tbool sent = false;\n\t\t};\n\t\tconst auto state = std::make_shared<State>(State{\n\t\t\t.recipient = initialRecipient,\n\t\t\t.weak = box.get(),\n\t\t});\n\t\tconst auto userpicsWrap = box->addRow(\n\t\t\tobject_ptr<Ui::VerticalLayout>(box),\n\t\t\tQMargins());\n\n\t\tstate->recipient.value(\n\t\t) | rpl::on_next([=](not_null<PeerData*> recipient) {\n\t\t\twhile (userpicsWrap->count()) {\n\t\t\t\tdelete userpicsWrap->widgetAt(0);\n\t\t\t}\n\t\t\tuserpicsWrap->add(\n\t\t\t\tCreateUserpicsTransfer(\n\t\t\t\t\tbox,\n\t\t\t\t\trpl::single(std::vector{ not_null<PeerData*>(bot) }),\n\t\t\t\t\trecipient,\n\t\t\t\t\tUserpicsTransferType::StarRefJoin),\n\t\t\t\tst::boxRowPadding + st::starrefJoinUserpicsPadding);\n\t\t\tuserpicsWrap->resizeToWidth(box->width());\n\t\t}, box->lifetime());\n\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_star_ref_title(),\n\t\t\t\tst::boxTitle),\n\t\t\tst::boxRowPadding + st::starrefJoinTitlePadding,\n\t\t\tstyle::al_top);\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_star_ref_one_about(\n\t\t\t\t\tlt_app,\n\t\t\t\t\trpl::single(tr::bold(bot->name())),\n\t\t\t\t\tlt_amount,\n\t\t\t\t\trpl::single(tr::bold(\n\t\t\t\t\t\tFormatCommission(program.commission))),\n\t\t\t\t\tlt_duration,\n\t\t\t\t\tFormatForProgramDuration(program.durationMonths),\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::starrefCenteredText),\n\t\t\tstyle::al_top);\n\n\t\tUi::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 3);\n\t\tif (const auto average = program.revenuePerUser) {\n\t\t\tconst auto layout = box->verticalLayout();\n\t\t\tauto text = Ui::Text::Colorized(Ui::CreditsEmoji());\n\t\t\ttext.append(Lang::FormatCreditsAmountRounded(average));\n\t\t\tlayout->add(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tbox,\n\t\t\t\t\ttr::lng_star_ref_one_daily_revenue(\n\t\t\t\t\t\tlt_amount,\n\t\t\t\t\t\trpl::single(\n\t\t\t\t\t\t\tUi::Text::Wrapped(text, EntityType::Bold)),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\tst::starrefRevenueText,\n\t\t\t\t\tst::defaultPopupMenu),\n\t\t\t\tst::boxRowPadding,\n\t\t\t\tstyle::al_top);\n\t\t\tUi::AddSkip(layout, st::defaultVerticalListSkip);\n\t\t}\n\n\t\tif (!list.empty()) {\n\t\t\tstruct Name {\n\t\t\t\tnot_null<PeerData*> peer;\n\t\t\t\tQString name;\n\t\t\t};\n\t\t\tauto names = ranges::views::transform(list, [](auto peer) {\n\t\t\t\tconst auto name = TextUtilities::NameSortKey(peer->name());\n\t\t\t\treturn Name{ peer, name };\n\t\t\t}) | ranges::to_vector;\n\t\t\tranges::sort(names, ranges::less(), &Name::name);\n\n\t\t\tlist = ranges::views::transform(names, &Name::peer)\n\t\t\t\t| ranges::to_vector;\n\n\t\t\tbox->addRow(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tbox,\n\t\t\t\t\ttr::lng_star_ref_link_recipient(),\n\t\t\t\t\tst::starrefCenteredText),\n\t\t\t\tstyle::al_top);\n\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\tconst auto recipientWrap = box->addRow(\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(box),\n\t\t\t\tQMargins());\n\t\t\tstate->recipient.value(\n\t\t\t) | rpl::on_next([=](not_null<PeerData*> recipient) {\n\t\t\t\twhile (recipientWrap->count()) {\n\t\t\t\t\tdelete recipientWrap->widgetAt(0);\n\t\t\t\t}\n\n\t\t\t\tconst auto selectable = (list.size() > 1);\n\t\t\t\tconst auto bgOverride = selectable\n\t\t\t\t\t? &st::lightButtonBgOver\n\t\t\t\t\t: nullptr;\n\t\t\t\tconst auto right = selectable\n\t\t\t\t\t? Ui::CreateChild<Ui::RpWidget>(recipientWrap)\n\t\t\t\t\t: nullptr;\n\t\t\t\tif (right) {\n\t\t\t\t\tconst auto skip = st::chatGiveawayPeerPadding.right();\n\t\t\t\t\tconst auto icon = &st::starrefRecipientArrow;\n\t\t\t\t\tconst auto height = st::chatGiveawayPeerSize\n\t\t\t\t\t\t- st::chatGiveawayPeerPadding.top() * 2;\n\t\t\t\t\tright->resize(skip + icon->width(), height);\n\t\t\t\t\tright->paintRequest() | rpl::on_next([=] {\n\t\t\t\t\t\tauto p = QPainter(right);\n\t\t\t\t\t\ticon->paint(\n\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\tskip,\n\t\t\t\t\t\t\t(height - icon->height()) / 2,\n\t\t\t\t\t\t\tright->width());\n\t\t\t\t\t}, right->lifetime());\n\t\t\t\t}\n\t\t\t\tconst auto button = recipientWrap->add(\n\t\t\t\t\tobject_ptr<Ui::AbstractButton>::fromRaw(\n\t\t\t\t\t\tMakePeerBubbleButton(\n\t\t\t\t\t\t\tbox,\n\t\t\t\t\t\t\trecipient,\n\t\t\t\t\t\t\tright,\n\t\t\t\t\t\t\tbgOverride).release()),\n\t\t\t\t\tst::boxRowPadding);\n\t\t\t\trecipientWrap->resizeToWidth(box->width());\n\t\t\t\tif (!selectable) {\n\t\t\t\t\tbutton->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\t\tconst auto callback = [=](not_null<PeerData*> peer) {\n\t\t\t\t\t\tstate->recipient = peer;\n\t\t\t\t\t};\n\t\t\t\t\tChooseRecipient(\n\t\t\t\t\t\tbutton,\n\t\t\t\t\t\tlist,\n\t\t\t\t\t\tstate->recipient.current(),\n\t\t\t\t\t\tcrl::guard(button, callback));\n\t\t\t\t});\n\t\t\t}, box->lifetime());\n\t\t}\n\n\t\tconst auto send = [=] {\n\t\t\tif (state->sent) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->sent = true;\n\t\t\tconst auto recipient = state->recipient.current();\n\t\t\tConnectStarRef(bot->asUser(), recipient, [=](ConnectedBot info) {\n\t\t\t\tif (recipient == initialRecipient) {\n\t\t\t\t\tif (const auto onstack = done) {\n\t\t\t\t\t\tonstack(info.state);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tshow->show(StarRefLinkBox(info, recipient));\n\t\t\t\tif (const auto strong = state->weak.get()) {\n\t\t\t\t\tstrong->closeBox();\n\t\t\t\t}\n\t\t\t}, [=](const QString &error) {\n\t\t\t\tstate->sent = false;\n\t\t\t\tshow->showToast(u\"Failed: \"_q + error);\n\t\t\t});\n\t\t};\n\t\tconst auto button = box->addButton(\n\t\t\ttr::lng_star_ref_one_join(),\n\t\t\tsend);\n\t\tAddFullWidthButtonFooter(\n\t\t\tbox,\n\t\t\tbutton,\n\t\t\ttr::lng_star_ref_one_join_text(\n\t\t\t\tlt_terms,\n\t\t\t\ttr::lng_star_ref_button_link(\n\t\t\t\t\ttr::url(tr::lng_star_ref_tos_url(tr::now))),\n\t\t\t\ttr::marked));\n\t});\n}\n\nobject_ptr<Ui::BoxContent> ConfirmEndBox(Fn<void()> finish) {\n\treturn Box([=](not_null<Ui::GenericBox*> box) {\n\t\tbox->setTitle(tr::lng_star_ref_warning_title());\n\t\tconst auto skip = st::defaultVerticalListSkip;\n\t\tconst auto margins = st::boxRowPadding + QMargins(0, 0, 0, skip);\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_star_ref_warning_if_end(tr::rich),\n\t\t\t\tst::boxLabel),\n\t\t\tmargins);\n\t\tconst auto addPoint = [&](tr::phrase<> text) {\n\t\t\tconst auto padded = box->addRow(\n\t\t\t\tobject_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\t\t\t\tbox,\n\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\tbox,\n\t\t\t\t\t\ttext(tr::rich),\n\t\t\t\t\t\tst::blockUserConfirmation),\n\t\t\t\t\tQMargins(st::boxTextFont->height, 0, 0, 0)),\n\t\t\t\tmargins);\n\t\t\tpadded->paintRequest() | rpl::on_next([=] {\n\t\t\t\tauto p = QPainter(padded);\n\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\tconst auto size = st::starrefEndBulletSize;\n\t\t\t\tconst auto top = st::starrefEndBulletTop;\n\t\t\t\tp.setBrush(st::windowFg);\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.drawEllipse(0, top, size, size);\n\t\t\t}, padded->lifetime());\n\t\t};\n\t\taddPoint(tr::lng_star_ref_warning_if_end1);\n\t\taddPoint(tr::lng_star_ref_warning_if_end2);\n\t\taddPoint(tr::lng_star_ref_warning_if_end3);\n\t\tconst auto done = [=] {\n\t\t\tbox->closeBox();\n\t\t\tfinish();\n\t\t};\n\t\tbox->addButton(\n\t\t\ttr::lng_star_ref_warning_end(),\n\t\t\tdone,\n\t\t\tst::attentionBoxButton);\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t});\n}\n\nvoid ResolveRecipients(\n\t\tnot_null<Main::Session*> session,\n\t\tFn<void(std::vector<not_null<PeerData*>>)> done) {\n\tstruct State {\n\t\tnot_null<Main::Session*> session;\n\t\tstd::vector<not_null<PeerData*>> list;\n\t\tFn<void(std::vector<not_null<PeerData*>>)> done;\n\t};\n\tconst auto state = std::make_shared<State>(State{\n\t\t.session = session,\n\t\t.done = std::move(done),\n\t});\n\tconst auto finish1 = [state](const MTPmessages_Chats &result) {\n\t\tconst auto already = int(state->list.size());\n\t\tconst auto session = state->session;\n\t\tresult.match([&](const auto &data) {\n\t\t\tconst auto &list = data.vchats().v;\n\t\t\tstate->list.reserve(list.size() + (already ? already : 1));\n\t\t\tif (!already) {\n\t\t\t\tstate->list.push_back(session->user());\n\t\t\t}\n\t\t\tfor (const auto &chat : list) {\n\t\t\t\tconst auto peer = session->data().processChat(chat);\n\t\t\t\tif (const auto channel = peer->asBroadcast()) {\n\t\t\t\t\tif (channel->canPostMessages()) {\n\t\t\t\t\t\tstate->list.push_back(channel);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (already) {\n\t\t\t\tbase::take(state->done)(base::take(state->list));\n\t\t\t}\n\t\t});\n\t};\n\tconst auto finish2 = [state](const MTPVector<MTPUser> &result) {\n\t\tconst auto already = int(state->list.size());\n\t\tconst auto session = state->session;\n\t\tconst auto &list = result.v;\n\t\tstate->list.reserve(list.size() + (already ? already : 1));\n\t\tif (!already) {\n\t\t\tstate->list.push_back(session->user());\n\t\t}\n\t\tfor (const auto &user : list) {\n\t\t\tstate->list.push_back(session->data().processUser(user));\n\t\t}\n\t\tif (already) {\n\t\t\tbase::take(state->done)(base::take(state->list));\n\t\t}\n\t};\n\n\tsession->api().request(MTPchannels_GetAdminedPublicChannels(\n\t\tMTP_flags(0)\n\t)).done(finish1).fail([=] {\n\t\tfinish1(MTP_messages_chats(MTP_vector<MTPChat>(0)));\n\t}).send();\n\n\tstate->session->api().request(MTPbots_GetAdminedBots(\n\t)).done(finish2).fail([=] {\n\t\tfinish2(MTP_vector<MTPUser>(0));\n\t}).send();\n}\n\nstd::unique_ptr<Ui::AbstractButton> MakePeerBubbleButton(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<PeerData*> peer,\n\t\tUi::RpWidget *right,\n\t\tconst style::color *bgOverride) {\n\tclass Button final : public Ui::AbstractButton {\n\tpublic:\n\t\tButton(QWidget *parent, not_null<int*> innerWidth)\n\t\t: AbstractButton(parent)\n\t\t, _innerWidth(innerWidth) {\n\t\t}\n\n\t\tvoid mouseMoveEvent(QMouseEvent *e) override {\n\t\t\tconst auto inner = *_innerWidth;\n\t\t\tconst auto skip = (width() - inner) / 2;\n\t\t\tconst auto p = e->pos();\n\t\t\tconst auto over = QRect(skip, 0, inner, height()).contains(p);\n\t\t\tsetOver(over, StateChangeSource::ByHover);\n\t\t}\n\n\tprivate:\n\t\tconst not_null<int*> _innerWidth;\n\n\t};\n\n\tauto ownedWidth = std::make_unique<int>();\n\tconst auto width = ownedWidth.get();\n\tauto result = std::make_unique<Button>(parent, width);\n\tresult->lifetime().add([moved = std::move(ownedWidth)] {});\n\n\tconst auto size = st::chatGiveawayPeerSize;\n\tconst auto padding = st::chatGiveawayPeerPadding;\n\n\tconst auto raw = result.get();\n\n\tconst auto name = raw->lifetime().make_state<Ui::FlatLabel>(\n\t\traw,\n\t\trpl::single(peer->name()),\n\t\tst::botEmojiStatusName);\n\tconst auto userpic = raw->lifetime().make_state<Ui::UserpicButton>(\n\t\traw,\n\t\tpeer,\n\t\tst::botEmojiStatusUserpic);\n\tname->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tif (right) {\n\t\tright->setParent(result.get());\n\t\tright->show();\n\t\tright->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t}\n\n\tauto rightWidth = right ? right->widthValue() : rpl::single(0);\n\n\traw->resize(size, size);\n\trpl::combine(\n\t\traw->sizeValue(),\n\t\tstd::move(rightWidth)\n\t) | rpl::on_next([=](QSize outer, int rwidth) {\n\t\tconst auto full = outer.width();\n\t\tconst auto decorations = size\n\t\t\t+ padding.left()\n\t\t\t+ padding.right()\n\t\t\t+ rwidth;\n\t\tconst auto inner = full - decorations;\n\t\tconst auto use = std::min(inner, name->textMaxWidth());\n\t\t*width = use + decorations;\n\t\tconst auto left = (full - *width) / 2;\n\t\tif (inner > 0) {\n\t\t\tuserpic->moveToLeft(left, 0, outer.width());\n\t\t\tif (right) {\n\t\t\t\tright->moveToLeft(\n\t\t\t\t\tleft + *width - padding.right() - rwidth,\n\t\t\t\t\tpadding.top(),\n\t\t\t\t\touter.width());\n\t\t\t}\n\t\t\tname->resizeToWidth(use);\n\t\t\tname->moveToLeft(\n\t\t\t\tleft + size + padding.left(),\n\t\t\t\tpadding.top(),\n\t\t\t\touter.width());\n\t\t}\n\t}, raw->lifetime());\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\tconst auto left = (raw->width() - *width) / 2;\n\t\tconst auto skip = size / 2;\n\t\tp.setClipRect(left + skip, 0, *width - skip, size);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(bgOverride ? *bgOverride : st::windowBgOver);\n\t\tp.drawRoundedRect(left, 0, *width, size, skip, skip);\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\nvoid ConfirmUpdate(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<UserData*> bot,\n\t\tconst StarRefProgram &program,\n\t\tbool exists,\n\t\tFn<void(Fn<void(bool)>)> update) {\n\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto sent = std::make_shared<bool>();\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = (exists\n\t\t\t\t? tr::lng_star_ref_warning_change\n\t\t\t\t: tr::lng_star_ref_warning_text)(tr::rich),\n\t\t\t.confirmed = [=](Fn<void()> close) {\n\t\t\t\tif (*sent) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t*sent = true;\n\t\t\t\tupdate([=](bool success) {\n\t\t\t\t\t*sent = false;\n\t\t\t\t\tif (success) {\n\t\t\t\t\t\tclose();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t},\n\t\t\t.confirmText = (exists\n\t\t\t\t? tr::lng_star_ref_warning_update\n\t\t\t\t: tr::lng_star_ref_warning_start)(),\n\t\t\t.title = tr::lng_star_ref_warning_title(),\n\t\t});\n\n\t\tauto table = box->addRow(\n\t\t\tobject_ptr<Ui::TableLayout>(\n\t\t\t\tbox,\n\t\t\t\tst::giveawayGiftCodeTable),\n\t\t\tst::giveawayGiftCodeTableMargin);\n\t\tconst auto addRow = [&](\n\t\t\t\trpl::producer<QString> label,\n\t\t\t\tconst QString &value) {\n\t\t\ttable->addRow(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\ttable,\n\t\t\t\t\tstd::move(label),\n\t\t\t\t\ttable->st().defaultLabel),\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\ttable,\n\t\t\t\t\tvalue,\n\t\t\t\t\ttable->st().defaultValue,\n\t\t\t\t\tst::defaultPopupMenu),\n\t\t\t\tst::giveawayGiftCodeLabelMargin,\n\t\t\t\tst::giveawayGiftCodeValueMargin);\n\t\t};\n\t\taddRow(\n\t\t\ttr::lng_star_ref_commission_title(),\n\t\t\tFormatCommission(program.commission));\n\t\taddRow(\n\t\t\ttr::lng_star_ref_duration_title(),\n\t\t\tFormatProgramDuration(program.durationMonths));\n\t}));\n}\n\nvoid UpdateProgram(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<UserData*> bot,\n\t\tconst StarRefProgram &program,\n\t\tFn<void(bool)> done) {\n\tusing Flag = MTPbots_UpdateStarRefProgram::Flag;\n\tbot->session().api().request(MTPbots_UpdateStarRefProgram(\n\t\tMTP_flags((program.commission > 0 && program.durationMonths > 0)\n\t\t\t? Flag::f_duration_months\n\t\t\t: Flag()),\n\t\tbot->inputUser(),\n\t\tMTP_int(program.commission),\n\t\tMTP_int(program.durationMonths)\n\t)).done([=](const MTPStarRefProgram &result) {\n\t\tbot->setStarRefProgram(Data::ParseStarRefProgram(&result));\n\t\tdone(true);\n\t}).fail([=](const MTP::Error &error) {\n\t\tshow->showToast(u\"Failed: \"_q + error.type());\n\t\tdone(false);\n\t}).send();\n}\n\nvoid FinishProgram(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<UserData*> bot,\n\t\tFn<void(bool)> done) {\n\tUpdateProgram(std::move(show), bot, {}, std::move(done));\n}\n\nConnectedBots Parse(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPpayments_ConnectedStarRefBots &bots) {\n\tconst auto &data = bots.data();\n\tsession->data().processUsers(data.vusers());\n\tconst auto &list = data.vconnected_bots().v;\n\tauto result = ConnectedBots();\n\tfor (const auto &bot : list) {\n\t\tconst auto &data = bot.data();\n\t\tconst auto botId = UserId(data.vbot_id());\n\t\tconst auto link = qs(data.vurl());\n\t\tconst auto date = data.vdate().v;\n\t\tconst auto commission = data.vcommission_permille().v;\n\t\tconst auto durationMonths\n\t\t\t= data.vduration_months().value_or_empty();\n\t\tconst auto users = int(data.vparticipants().v);\n\t\tconst auto revoked = data.is_revoked();\n\t\tresult.push_back({\n\t\t\t.bot = session->data().user(botId),\n\t\t\t.state = {\n\t\t\t\t.program = {\n\t\t\t\t\t.commission = ushort(commission),\n\t\t\t\t\t.durationMonths = uchar(durationMonths),\n\t\t\t\t},\n\t\t\t\t.link = link,\n\t\t\t\t.date = date,\n\t\t\t\t.users = users,\n\t\t\t\t.revoked = revoked,\n\t\t\t},\n\t\t});\n\t}\n\treturn result;\n}\n\nobject_ptr<Ui::RpWidget> CreateLinkHeaderIcon(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Main::Session*> session,\n\t\tint usersCount) {\n\treturn CreateLinkIcon(parent, session, usersCount);\n}\n\n} // namespace Info::BotStarRef\n"
  },
  {
    "path": "Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"data/data_user.h\"\n\nnamespace Ui {\nclass AbstractButton;\nclass RoundButton;\nclass VerticalLayout;\nclass BoxContent;\nclass RpWidget;\nclass Show;\n} // namespace Ui\n\nnamespace style {\nstruct RoundButton;\nstruct InputField;\n} // namespace style\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Info::BotStarRef {\n\nstruct ConnectedBotState {\n\tStarRefProgram program;\n\tQString link;\n\tTimeId date = 0;\n\tint users = 0;\n\tbool unresolved = false;\n\tbool revoked = false;\n};\nstruct ConnectedBot {\n\tnot_null<UserData*> bot;\n\tConnectedBotState state;\n};\nusing ConnectedBots = std::vector<ConnectedBot>;\n\n[[nodiscard]] QString FormatCommission(ushort commission);\n[[nodiscard]] QString FormatProgramDuration(int durationMonths);\n[[nodiscard]] rpl::producer<TextWithEntities> FormatForProgramDuration(\n\tint durationMonths);\n\n[[nodiscard]] not_null<Ui::AbstractButton*> AddViewListButton(\n\tnot_null<Ui::VerticalLayout*> parent,\n\trpl::producer<QString> title,\n\trpl::producer<QString> subtitle,\n\tbool newBadge = false);\n\nvoid AddFullWidthButtonFooter(\n\tnot_null<Ui::BoxContent*> box,\n\tnot_null<Ui::RpWidget*> button,\n\trpl::producer<TextWithEntities> text);\n\n[[nodiscard]] object_ptr<Ui::BoxContent> StarRefLinkBox(\n\tConnectedBot row,\n\tnot_null<PeerData*> peer);\n[[nodiscard]] object_ptr<Ui::BoxContent> JoinStarRefBox(\n\tConnectedBot row,\n\tnot_null<PeerData*> initialRecipient,\n\tstd::vector<not_null<PeerData*>> recipients,\n\tFn<void(ConnectedBotState)> done = nullptr);\n[[nodiscard]] object_ptr<Ui::BoxContent> ConfirmEndBox(Fn<void()> finish);\n\nvoid ResolveRecipients(\n\tnot_null<Main::Session*> session,\n\tFn<void(std::vector<not_null<PeerData*>>)> done);\n\nstd::unique_ptr<Ui::AbstractButton> MakePeerBubbleButton(\n\tnot_null<QWidget*> parent,\n\tnot_null<PeerData*> peer,\n\tUi::RpWidget *right = nullptr,\n\tconst style::color *bgOverride = nullptr);\n\nvoid ConfirmUpdate(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<UserData*> bot,\n\tconst StarRefProgram &program,\n\tbool exists,\n\tFn<void(Fn<void(bool)> done)> update);\nvoid UpdateProgram(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<UserData*> bot,\n\tconst StarRefProgram &program,\n\tFn<void(bool)> done);\nvoid FinishProgram(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<UserData*> bot,\n\tFn<void(bool)> done);\n\n[[nodiscard]] ConnectedBots Parse(\n\tnot_null<Main::Session*> session,\n\tconst MTPpayments_ConnectedStarRefBots &bots);\n\n[[nodiscard]] object_ptr<Ui::AbstractButton> MakeLinkLabel(\n\tnot_null<QWidget*> parent,\n\tconst QString &link,\n\tconst style::InputField *stOverride = nullptr);\n[[nodiscard]] object_ptr<Ui::RpWidget> CreateLinkHeaderIcon(\n\tnot_null<QWidget*> parent,\n\tnot_null<Main::Session*> session,\n\tint usersCount = 0);\n\n} // namespace Info::BotStarRef\n"
  },
  {
    "path": "Telegram/SourceFiles/info/bot/starref/info_bot_starref_join_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/bot/starref/info_bot_starref_join_widget.h\"\n\n#include \"apiwrap.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/unixtime.h\"\n#include \"base/weak_ptr.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"info/bot/starref/info_bot_starref_common.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/effects/premium_top_bar.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_media_player.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n\n#include <QApplication>\n\nnamespace Info::BotStarRef::Join {\nnamespace {\n\nconstexpr auto kPerPage = 50;\n\nenum class JoinType {\n\tJoined,\n\tSuggested,\n\tExisting,\n};\n\nenum class SuggestedSort {\n\tProfitability,\n\tRevenue,\n\tDate\n};\n\nclass ListController final\n\t: public PeerListController\n\t, public base::has_weak_ptr {\npublic:\n\tListController(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tJoinType type);\n\t~ListController();\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\n\t[[nodiscard]] rpl::producer<int> rowCountValue() const;\n\t[[nodiscard]] rpl::producer<ConnectedBot> connected() const;\n\t[[nodiscard]] rpl::producer<ConnectedBot> revoked() const;\n\t[[nodiscard]] rpl::producer<> addForBotRequests() const;\n\n\tvoid setSort(SuggestedSort sort);\n\n\tvoid process(ConnectedBot row);\n\nprivate:\n\t[[nodiscard]] std::unique_ptr<PeerListRow> createRow(ConnectedBot bot);\n\tvoid open(not_null<UserData*> bot, ConnectedBotState state);\n\tvoid requestRecipients();\n\tvoid setupAddForBot();\n\tvoid setupLinkBadge();\n\tvoid refreshRows();\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<PeerData*> _peer;\n\tconst JoinType _type = {};\n\n\tbase::flat_map<not_null<PeerData*>, ConnectedBotState> _states;\n\tbase::flat_set<not_null<PeerData*>> _resolving;\n\tUserData *_openOnResolve = nullptr;\n\n\tFn<void()> _recipientsReady;\n\tstd::vector<not_null<PeerData*>> _recipients;\n\trpl::event_stream<ConnectedBot> _connected;\n\trpl::event_stream<ConnectedBot> _revoked;\n\trpl::event_stream<> _addForBot;\n\n\tmtpRequestId _requestId = 0;\n\tTimeId _offsetDate = 0;\n\tQString _offsetThing;\n\tbool _allLoaded = false;\n\tbool _recipientsRequested = false;\n\tSuggestedSort _sort = SuggestedSort::Profitability;\n\tQImage _linkBadge;\n\n\trpl::variable<int> _rowCount = 0;\n\n};\n\nclass Row final : public PeerListRow {\npublic:\n\tRow(\n\t\tnot_null<PeerData*> peer,\n\t\tStarRefProgram program,\n\t\tQImage *link = nullptr);\n\n\tvoid paintStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) override;\n\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\nprivate:\n\tvoid refreshStatus() override;\n\n\tStarRefProgram _program;\n\tQImage *_link = nullptr;\n\tQImage _userpic;\n\tQImage _badge;\n\n};\n\nRow::Row(\n\tnot_null<PeerData*> peer,\n\tStarRefProgram program,\n\tQImage *link)\n: PeerListRow(peer)\n, _program(program)\n, _link(link) {\n}\n\nvoid Row::paintStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) {\n\tconst auto top = y\n\t\t+ st::contactsStatusFont->ascent\n\t\t- st::starrefCommissionFont->ascent\n\t\t- st::lineWidth;\n\tp.drawImage(x, top, _badge);\n\n\tconst auto space = st::normalFont->spacew;\n\tauto shift = (_badge.width() / _badge.devicePixelRatio()) + space;\n\tx += shift;\n\tavailableWidth -= shift;\n\n\tPeerListRow::paintStatusText(\n\t\tp,\n\t\tst,\n\t\tx,\n\t\ty,\n\t\tavailableWidth,\n\t\touterWidth,\n\t\tselected);\n}\n\nPaintRoundImageCallback Row::generatePaintUserpicCallback(\n\t\tbool forceRound) {\n\tif (!_link) {\n\t\treturn PeerListRow::generatePaintUserpicCallback(forceRound);\n\t}\n\treturn [=](\n\t\t\tPainter &p,\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tint outerWidth,\n\t\t\tint size) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto dimensions = QSize(size, size);\n\t\tif (_userpic.size() != dimensions * ratio) {\n\t\t\t_userpic = QImage(\n\t\t\t\tdimensions * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_userpic.setDevicePixelRatio(ratio);\n\t\t}\n\t\t_userpic.fill(Qt::transparent);\n\n\t\tauto q = Painter(&_userpic);\n\t\tauto hq = PainterHighQualityEnabler(q);\n\t\tauto paint = PeerListRow::generatePaintUserpicCallback(forceRound);\n\t\tpaint(q, 0, 0, size, size);\n\t\tconst auto corner = _link->size() / _link->devicePixelRatio();\n\t\tauto pen = QPen(Qt::transparent, st::lineWidth * 1.5);\n\t\tq.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tq.setPen(pen);\n\t\tq.setBrush(st::historyPeer2UserpicBg2);\n\t\tconst auto left = size - corner.width();\n\t\tconst auto top = size - corner.height();\n\t\tq.drawEllipse(left, top, corner.width(), corner.height());\n\t\tq.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\tq.drawImage(left, top, *_link);\n\t\tq.end();\n\n\t\tp.drawImage(x, y, _userpic);\n\t};\n}\n\nvoid Row::refreshStatus() {\n\tconst auto text = FormatCommission(_program.commission);\n\tconst auto padding = st::starrefCommissionPadding;\n\tconst auto font = st::starrefCommissionFont;\n\tconst auto width = font->width(text);\n\tconst auto inner = QRect(0, 0, width, font->height);\n\tconst auto outer = inner.marginsAdded(padding);\n\tconst auto ratio = style::DevicePixelRatio();\n\t_badge = QImage(\n\t\touter.size() * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_badge.setDevicePixelRatio(ratio);\n\t_badge.fill(Qt::transparent);\n\n\tauto p = QPainter(&_badge);\n\tp.setBrush(st::historyPeer2UserpicBg2);\n\tp.setPen(Qt::NoPen);\n\tconst auto radius = st::roundRadiusSmall;\n\tp.drawRoundedRect(outer.translated(-outer.topLeft()), radius, radius);\n\tp.setFont(font);\n\tp.setBrush(Qt::NoBrush);\n\tp.setPen(st::historyPeerUserpicFg);\n\tp.drawText(padding.left(), padding.top() + font->ascent, text);\n\tp.end();\n\n\tsetCustomStatus(FormatProgramDuration(_program.durationMonths));\n}\n\nvoid Resolve(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> bot,\n\t\tFn<void(std::optional<ConnectedBotState>)> done) {\n\tpeer->session().api().request(MTPpayments_GetConnectedStarRefBot(\n\t\tpeer->input(),\n\t\tbot->inputUser()\n\t)).done([=](const MTPpayments_ConnectedStarRefBots &result) {\n\t\tconst auto parsed = Parse(&peer->session(), result);\n\t\tif (parsed.empty()) {\n\t\t\tdone(std::nullopt);\n\t\t} else {\n\t\t\tdone(parsed.front().state);\n\t\t}\n\t}).fail([=] {\n\t\tdone(std::nullopt);\n\t}).send();\n}\n\nListController::ListController(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\tJoinType type)\n: PeerListController()\n, _controller(controller)\n, _peer(peer)\n, _type(type) {\n\tsetStyleOverrides(&st::peerListSingleRow);\n\n\tif (_type == JoinType::Joined) {\n\t\tsetupLinkBadge();\n\t\tstyle::PaletteChanged() | rpl::on_next([=] {\n\t\t\tsetupLinkBadge();\n\t\t}, lifetime());\n\t}\n}\n\nListController::~ListController() {\n\tif (_requestId) {\n\t\tsession().api().request(_requestId).cancel();\n\t}\n}\n\nMain::Session &ListController::session() const {\n\treturn _peer->session();\n}\n\nstd::unique_ptr<PeerListRow> ListController::createRow(ConnectedBot bot) {\n\t_states.emplace(bot.bot, bot.state);\n\tconst auto link = _linkBadge.isNull() ? nullptr : &_linkBadge;\n\treturn std::make_unique<Row>(bot.bot, bot.state.program, link);\n}\n\nvoid ListController::setupLinkBadge() {\n\tconst auto side = st::starrefLinkBadge;\n\tconst auto size = QSize(side, side);\n\tconst auto ratio = style::DevicePixelRatio();\n\n\t_linkBadge = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied);\n\t_linkBadge.setDevicePixelRatio(ratio);\n\t_linkBadge.fill(Qt::transparent);\n\n\tconst auto skip = st::starrefLinkBadgeSkip;\n\tconst auto inner = QSize(side - 2 * skip, side - 2 * skip);\n\n\tauto p = QPainter(&_linkBadge);\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tauto owned = Lottie::MakeIcon({\n\t\t.name = u\"starref_link\"_q,\n\t\t.color = &st::historyPeerUserpicFg,\n\t\t.sizeOverride = inner,\n\t});\n\tp.drawImage(QRect(QPoint(skip, skip), inner), owned->frame());\n}\n\nvoid ListController::prepare() {\n\tdelegate()->peerListSetTitle((_type == JoinType::Joined)\n\t\t? tr::lng_star_ref_list_my()\n\t\t: tr::lng_star_ref_list_title());\n\tloadMoreRows();\n}\n\nvoid ListController::loadMoreRows() {\n\tif (_requestId || _allLoaded) {\n\t\treturn;\n\t} else if (_type == JoinType::Joined) {\n\t\tusing Flag = MTPpayments_GetConnectedStarRefBots::Flag;\n\t\t_requestId = session().api().request(\n\t\t\tMTPpayments_GetConnectedStarRefBots(\n\t\t\t\tMTP_flags(Flag()\n\t\t\t\t\t| (_offsetDate ? Flag::f_offset_date : Flag())\n\t\t\t\t\t| (_offsetThing.isEmpty() ? Flag() : Flag::f_offset_link)),\n\t\t\t\t_peer->input(),\n\t\t\t\tMTP_int(_offsetDate),\n\t\t\t\tMTP_string(_offsetThing),\n\t\t\t\tMTP_int(kPerPage))\n\t\t).done([=](const MTPpayments_ConnectedStarRefBots &result) {\n\t\t\tconst auto parsed = Parse(&session(), result);\n\t\t\tif (parsed.empty()) {\n\t\t\t\t_allLoaded = true;\n\t\t\t} else {\n\t\t\t\tfor (const auto &bot : parsed) {\n\t\t\t\t\tif (!delegate()->peerListFindRow(bot.bot->id.value)) {\n\t\t\t\t\t\tdelegate()->peerListAppendRow(createRow(bot));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\trefreshRows();\n\t\t\t}\n\t\t\t_requestId = 0;\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t_requestId = 0;\n\t\t}).send();\n\t} else {\n\t\tif (_type == JoinType::Existing) {\n\t\t\tsetDescriptionText(tr::lng_contacts_loading(tr::now));\n\t\t}\n\t\tusing Flag = MTPpayments_GetSuggestedStarRefBots::Flag;\n\t\t_requestId = session().api().request(\n\t\t\tMTPpayments_GetSuggestedStarRefBots(\n\t\t\t\tMTP_flags((_sort == SuggestedSort::Revenue)\n\t\t\t\t\t? Flag::f_order_by_revenue\n\t\t\t\t\t: (_sort == SuggestedSort::Date)\n\t\t\t\t\t? Flag::f_order_by_date\n\t\t\t\t\t: Flag()),\n\t\t\t\t_peer->input(),\n\t\t\t\tMTP_string(_offsetThing),\n\t\t\t\tMTP_int(kPerPage))\n\t\t).done([=](const MTPpayments_SuggestedStarRefBots &result) {\n\t\t\tsetDescriptionText(QString());\n\t\t\tsetupAddForBot();\n\n\t\t\tif (_offsetThing.isEmpty()) {\n\t\t\t\twhile (delegate()->peerListFullRowsCount() > 0) {\n\t\t\t\t\tdelegate()->peerListRemoveRow(\n\t\t\t\t\t\tdelegate()->peerListRowAt(0));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst auto &data = result.data();\n\t\t\tif (data.vnext_offset()) {\n\t\t\t\t_offsetThing = qs(*data.vnext_offset());\n\t\t\t} else {\n\t\t\t\t_allLoaded = true;\n\t\t\t}\n\t\t\tsession().data().processUsers(data.vusers());\n\t\t\tfor (const auto &program : data.vsuggested_bots().v) {\n\t\t\t\tconst auto botId = UserId(program.data().vbot_id());\n\t\t\t\tconst auto user = session().data().user(botId);\n\t\t\t\tif (!delegate()->peerListFindRow(user->id.value)) {\n\t\t\t\t\tdelegate()->peerListAppendRow(createRow({\n\t\t\t\t\t\t.bot = user,\n\t\t\t\t\t\t.state = {\n\t\t\t\t\t\t\t.program = Data::ParseStarRefProgram(&program),\n\t\t\t\t\t\t\t.unresolved = true,\n\t\t\t\t\t\t},\n\t\t\t\t\t}));\n\t\t\t\t}\n\t\t\t}\n\t\t\trefreshRows();\n\t\t\t_requestId = 0;\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t_allLoaded = true;\n\t\t\t_requestId = 0;\n\t\t}).send();\n\t}\n}\n\nvoid ListController::setupAddForBot() {\n\tconst auto user = _peer->asUser();\n\tif (_type != JoinType::Existing\n\t\t|| !user\n\t\t|| !user->isBot()\n\t\t|| user->botInfo->starRefProgram.commission > 0) {\n\t\treturn;\n\t}\n\tauto button = object_ptr<Ui::PaddingWrap<Ui::SettingsButton>>(\n\t\tnullptr,\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\tnullptr,\n\t\t\ttr::lng_star_ref_add_bot(lt_bot, rpl::single(user->name())),\n\t\t\tst::inviteViaLinkButton),\n\t\tstyle::margins(0, st::membersMarginTop, 0, 0));\n\n\tconst auto icon = Ui::CreateChild<Info::Profile::FloatingIcon>(\n\t\tbutton->entity(),\n\t\tst::starrefAddForBotIcon,\n\t\tQPoint());\n\tbutton->entity()->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\ticon->moveToLeft(\n\t\t\tst::starrefAddForBotIconPosition.x(),\n\t\t\t(height - st::starrefAddForBotIcon.height()) / 2);\n\t}, icon->lifetime());\n\n\tbutton->entity()->setClickedCallback([=] {\n\t\t_addForBot.fire({});\n\t});\n\tbutton->entity()->events(\n\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\treturn (e->type() == QEvent::Enter);\n\t}) | rpl::on_next([=] {\n\t\tdelegate()->peerListMouseLeftGeometry();\n\t}, button->lifetime());\n\tdelegate()->peerListSetAboveWidget(std::move(button));\n}\n\nrpl::producer<int> ListController::rowCountValue() const {\n\treturn _rowCount.value();\n}\n\nrpl::producer<ConnectedBot> ListController::connected() const {\n\treturn _connected.events();\n}\n\nrpl::producer<ConnectedBot> ListController::revoked() const {\n\treturn _revoked.events();\n}\n\nrpl::producer<> ListController::addForBotRequests() const {\n\treturn _addForBot.events();\n}\n\nvoid ListController::setSort(SuggestedSort sort) {\n\tif (_sort == sort) {\n\t\treturn;\n\t}\n\t_sort = sort;\n\tif (const auto requestId = base::take(_requestId)) {\n\t\tsession().api().request(requestId).cancel();\n\t}\n\t_allLoaded = false;\n\t_offsetThing = QString();\n\tloadMoreRows();\n}\n\nvoid ListController::process(ConnectedBot row) {\n\tif (_type != JoinType::Joined) {\n\t\t_states[row.bot] = { .program = row.state.program };\n\t}\n\tif (!delegate()->peerListFindRow(PeerListRowId(row.bot->id.value))) {\n\t\tdelegate()->peerListPrependRow(createRow(row));\n\t\trefreshRows();\n\t}\n}\n\nvoid ListController::refreshRows() {\n\tdelegate()->peerListRefreshRows();\n\t_rowCount = delegate()->peerListFullRowsCount();\n}\n\nvoid ListController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto bot = row->peer()->asUser();\n\tconst auto i = _states.find(bot);\n\tAssert(i != end(_states));\n\tif (i->second.unresolved) {\n\t\tif (!_resolving.emplace(bot).second) {\n\t\t\treturn;\n\t\t}\n\t\t_openOnResolve = bot;\n\t\tconst auto resolved = [=](std::optional<ConnectedBotState> state) {\n\t\t\t_resolving.remove(bot);\n\t\t\tauto &now = _states[bot];\n\t\t\tif (state) {\n\t\t\t\tnow = *state;\n\t\t\t}\n\t\t\tif (_openOnResolve == bot) {\n\t\t\t\topen(bot, now);\n\t\t\t}\n\t\t};\n\t\tResolve(_peer, bot, crl::guard(this, resolved));\n\t} else {\n\t\t_openOnResolve = nullptr;\n\t\topen(bot, i->second);\n\t}\n}\n\nvoid ListController::open(not_null<UserData*> bot, ConnectedBotState state) {\n\tconst auto show = _controller->uiShow();\n\tif (_type == JoinType::Joined\n\t\t|| (!state.link.isEmpty() && !state.revoked)) {\n\t\t_recipientsReady = nullptr;\n\t\tshow->show(StarRefLinkBox({ bot, state }, _peer));\n\t} else {\n\t\tconst auto requireOthers = (_type == JoinType::Existing)\n\t\t\t|| _peer->isSelf();\n\t\tconst auto requestOthers = requireOthers && _recipients.empty();\n\t\tif (requestOthers) {\n\t\t\t_recipientsReady = [=] {\n\t\t\t\tExpects(!_recipients.empty());\n\n\t\t\t\topen(bot, state);\n\t\t\t};\n\t\t\trequestRecipients();\n\t\t\treturn;\n\t\t}\n\t\tconst auto connected = crl::guard(this, [=](ConnectedBotState now) {\n\t\t\t_states[bot] = now;\n\t\t\t_connected.fire({ bot, now });\n\t\t});\n\t\tshow->show(JoinStarRefBox(\n\t\t\t{ bot, state },\n\t\t\t_peer,\n\t\t\trequireOthers ? _recipients : std::vector<not_null<PeerData*>>(),\n\t\t\tconnected));\n\t}\n}\n\nvoid ListController::requestRecipients() {\n\tif (_recipientsRequested) {\n\t\treturn;\n\t}\n\t_recipientsRequested = true;\n\tconst auto session = &this->session();\n\tResolveRecipients(session, crl::guard(this, [=](\n\t\t\tstd::vector<not_null<PeerData*>> list) {\n\t\t_recipients = std::move(list);\n\t\tif (const auto callback = base::take(_recipientsReady)) {\n\t\t\tcallback();\n\t\t}\n\t}));\n}\n\nvoid RevokeLink(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &link,\n\t\tFn<void()> revoked) {\n\tpeer->session().api().request(MTPpayments_EditConnectedStarRefBot(\n\t\tMTP_flags(MTPpayments_EditConnectedStarRefBot::Flag::f_revoked),\n\t\tpeer->input(),\n\t\tMTP_string(link)\n\t)).done([=] {\n\t\tcontroller->showToast({\n\t\t\t.title = tr::lng_star_ref_revoked_title(tr::now),\n\t\t\t.text = { tr::lng_star_ref_revoked_text(tr::now) },\n\t\t});\n\t\trevoked();\n\t}).fail([=](const MTP::Error &error) {\n\t\tcontroller->showToast(u\"Failed: \"_q + error.type());\n\t}).send();\n}\n\nbase::unique_qptr<Ui::PopupMenu> ListController::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tconst auto bot = row->peer()->asUser();\n\tconst auto i = _states.find(bot);\n\tAssert(i != end(_states));\n\tconst auto state = i->second;\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\tconst auto addAction = Ui::Menu::CreateAddActionCallback(result.get());\n\n\tconst auto revoked = crl::guard(this, [=] {\n\t\tif (const auto row = delegate()->peerListFindRow(bot->id.value)) {\n\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t\trefreshRows();\n\t\t}\n\t\t_revoked.fire({ bot, state });\n\t});\n\n\taddAction(tr::lng_star_ref_list_my_open(tr::now), [=] {\n\t\t_controller->showPeerHistory(bot);\n\t}, &st::menuIconBot);\n\tif (!state.link.isEmpty()) {\n\t\taddAction(tr::lng_star_ref_list_my_copy(tr::now), [=] {\n\t\t\tQApplication::clipboard()->setText(state.link);\n\t\t\t_controller->showToast(tr::lng_username_copied(tr::now));\n\t\t}, &st::menuIconLinks);\n\t\tconst auto revoke = [=] {\n\t\t\tconst auto link = state.link;\n\t\t\tconst auto sure = [=](Fn<void()> close) {\n\t\t\t\tRevokeLink(_controller, _peer, link, revoked);\n\t\t\t\tclose();\n\t\t\t};\n\t\t\t_controller->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_star_ref_revoke_text(\n\t\t\t\t\tlt_bot,\n\t\t\t\t\trpl::single(tr::bold(bot->name())),\n\t\t\t\t\ttr::rich),\n\t\t\t\t.confirmed = sure,\n\t\t\t\t.title = tr::lng_star_ref_revoke_title(),\n\t\t\t}));\n\t\t};\n\t\taddAction({\n\t\t\t.text = tr::lng_star_ref_list_my_leave(tr::now),\n\t\t\t.handler = revoke,\n\t\t\t.icon = &st::menuIconLeaveAttention,\n\t\t\t.isAttention = true,\n\t\t});\n\t}\n\treturn result;\n}\n\n} // namespace\n\nclass InnerWidget final : public Ui::RpWidget {\npublic:\n\tInnerWidget(QWidget *parent, not_null<Controller*> controller);\n\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\n\tvoid showFinished();\n\tvoid setInnerFocus();\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\nprivate:\n\tvoid prepare();\n\tvoid setupInfo();\n\tnot_null<ListController*> setupMy();\n\tnot_null<ListController*> setupSuggested();\n\tvoid setupSort(not_null<Ui::RpWidget*> label);\n\n\t[[nodiscard]] object_ptr<Ui::RpWidget> infoRow(\n\t\trpl::producer<QString> title,\n\t\trpl::producer<QString> text,\n\t\tnot_null<const style::icon*> icon);\n\n\tconst not_null<Controller*> _controller;\n\tconst not_null<Ui::VerticalLayout*> _container;\n\trpl::variable<SuggestedSort> _sort = SuggestedSort::Profitability;\n\tListController *_my = nullptr;\n\tListController *_suggested = nullptr;\n\n};\n\nInnerWidget::InnerWidget(QWidget *parent, not_null<Controller*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _container(Ui::CreateChild<Ui::VerticalLayout>(this)) {\n\tprepare();\n}\n\nvoid InnerWidget::prepare() {\n\tUi::ResizeFitChild(this, _container);\n\n\tsetupInfo();\n\tUi::AddSkip(_container);\n\tUi::AddDivider(_container);\n\t_my = setupMy();\n\t_suggested = setupSuggested();\n}\n\nvoid InnerWidget::setupInfo() {\n\tAddSkip(_container, st::defaultVerticalListSkip * 2);\n\n\t_container->add(infoRow(\n\t\ttr::lng_star_ref_reliable_title(),\n\t\ttr::lng_star_ref_reliable_about(),\n\t\t&st::menuIconAntispam));\n\n\t_container->add(infoRow(\n\t\ttr::lng_star_ref_transparent_title(),\n\t\ttr::lng_star_ref_transparent_about(),\n\t\t&st::menuIconTransparent));\n\n\t_container->add(infoRow(\n\t\ttr::lng_star_ref_simple_title(),\n\t\ttr::lng_star_ref_simple_about(),\n\t\t&st::menuIconLike));\n}\n\nnot_null<ListController*> InnerWidget::setupMy() {\n\tconst auto wrap = _container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_container,\n\t\t\tobject_ptr<Ui::VerticalLayout>(_container)));\n\tconst auto inner = wrap->entity();\n\n\tUi::AddSkip(inner);\n\tUi::AddSubsectionTitle(inner, tr::lng_star_ref_list_my());\n\n\tconst auto delegate = lifetime().make_state<\n\t\tPeerListContentDelegateSimple\n\t>();\n\tconst auto controller = lifetime().make_state<ListController>(\n\t\t_controller->parentController(),\n\t\tpeer(),\n\t\tJoinType::Joined);\n\tconst auto content = inner->add(\n\t\tobject_ptr<PeerListContent>(\n\t\t\tinner,\n\t\t\tcontroller));\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\n\tUi::AddSkip(inner);\n\tUi::AddDivider(inner);\n\n\twrap->toggleOn(controller->rowCountValue(\n\t) | rpl::map(rpl::mappers::_1 > 0));\n\n\tcontroller->revoked(\n\t) | rpl::on_next([=](ConnectedBot row) {\n\t\t_suggested->process(row);\n\t}, content->lifetime());\n\n\treturn controller;\n}\n\nvoid InnerWidget::setupSort(not_null<Ui::RpWidget*> label) {\n\tconstexpr auto phrase = [](SuggestedSort sort) {\n\t\treturn ((sort == SuggestedSort::Profitability)\n\t\t\t? tr::lng_star_ref_sort_profitability\n\t\t\t: (sort == SuggestedSort::Revenue)\n\t\t\t? tr::lng_star_ref_sort_revenue\n\t\t\t: tr::lng_star_ref_sort_date)(tr::now, tr::link);\n\t};\n\tconst auto sort = Ui::CreateChild<Ui::FlatLabel>(\n\t\tlabel->parentWidget(),\n\t\ttr::lng_star_ref_sort_text(lt_sort, _sort.value() | rpl::map(phrase),\n\t\ttr::marked),\n\t\tst::defaultFlatLabel);\n\trpl::combine(\n\t\tlabel->geometryValue(),\n\t\twidthValue(),\n\t\tsort->widthValue()\n\t) | rpl::on_next([=](QRect geometry, int outer, int sortWidth) {\n\t\tconst auto skip = st::boxRowPadding.right();\n\t\tconst auto top = geometry.y()\n\t\t\t+ st::defaultSubsectionTitle.style.font->ascent\n\t\t\t- st::defaultFlatLabel.style.font->ascent;\n\t\tsort->moveToLeft(outer - sortWidth - skip, top, outer);\n\t}, sort->lifetime());\n\tsort->setClickHandlerFilter([=](const auto &...) {\n\t\tconst auto menu = Ui::CreateChild<Ui::PopupMenu>(\n\t\t\tsort,\n\t\t\tst::popupMenuWithIcons);\n\t\tconst auto orders = {\n\t\t\tSuggestedSort::Profitability,\n\t\t\tSuggestedSort::Revenue,\n\t\t\tSuggestedSort::Date\n\t\t};\n\t\tfor (const auto order : orders) {\n\t\t\tconst auto chosen = (order == _sort.current());\n\t\t\tmenu->addAction(phrase(order).text, crl::guard(this, [=] {\n\t\t\t\t_sort = order;\n\t\t\t}), chosen ? &st::mediaPlayerMenuCheck : nullptr);\n\t\t}\n\t\tmenu->popup(sort->mapToGlobal(QPoint(0, 0)));\n\t\treturn false;\n\t});\n}\n\nnot_null<ListController*> InnerWidget::setupSuggested() {\n\tconst auto wrap = _container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_container,\n\t\t\tobject_ptr<Ui::VerticalLayout>(_container)));\n\tconst auto inner = wrap->entity();\n\n\tUi::AddSkip(inner);\n\tconst auto subtitle = Ui::AddSubsectionTitle(\n\t\tinner,\n\t\ttr::lng_star_ref_list_subtitle());\n\tsetupSort(subtitle);\n\n\tconst auto delegate = lifetime().make_state<\n\t\tPeerListContentDelegateSimple\n\t>();\n\tauto controller = lifetime().make_state<ListController>(\n\t\t_controller->parentController(),\n\t\tpeer(),\n\t\tJoinType::Suggested);\n\tconst auto content = inner->add(\n\t\tobject_ptr<PeerListContent>(\n\t\t\tinner,\n\t\t\tcontroller));\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\n\twrap->toggleOn(controller->rowCountValue(\n\t) | rpl::map(rpl::mappers::_1 > 0));\n\n\tcontroller->connected(\n\t) | rpl::on_next([=](ConnectedBot row) {\n\t\t_my->process(row);\n\t}, content->lifetime());\n\n\t_sort.value() | rpl::on_next([=](SuggestedSort sort) {\n\t\tcontroller->setSort(sort);\n\t}, content->lifetime());\n\n\treturn controller;\n}\n\nobject_ptr<Ui::RpWidget> InnerWidget::infoRow(\n\t\trpl::producer<QString> title,\n\t\trpl::producer<QString> text,\n\t\tnot_null<const style::icon*> icon) {\n\tauto result = object_ptr<Ui::VerticalLayout>(_container);\n\tconst auto raw = result.data();\n\n\traw->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\traw,\n\t\t\tstd::move(title) | rpl::map(tr::bold),\n\t\t\tst::defaultFlatLabel),\n\t\tst::settingsPremiumRowTitlePadding);\n\traw->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\traw,\n\t\t\tstd::move(text),\n\t\t\tst::boxDividerLabel),\n\t\tst::settingsPremiumRowAboutPadding);\n\tobject_ptr<Info::Profile::FloatingIcon>(\n\t\traw,\n\t\t*icon,\n\t\tst::starrefInfoIconPosition);\n\n\treturn result;\n}\n\nnot_null<PeerData*> InnerWidget::peer() const {\n\treturn _controller->key().starrefPeer();\n}\n\nvoid InnerWidget::showFinished() {\n\n}\n\nvoid InnerWidget::setInnerFocus() {\n\tsetFocus();\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\n}\n\nMemento::Memento(not_null<Controller*> controller)\n: ContentMemento(Tag(controller->starrefPeer(), controller->starrefType())) {\n}\n\nMemento::Memento(not_null<PeerData*> peer)\n: ContentMemento(Tag(peer, Type::Join)) {\n}\n\nMemento::~Memento() = default;\n\nSection Memento::section() const {\n\treturn Section(Section::Type::BotStarRef);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(parent, controller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: ContentWidget(parent, controller)\n, _inner(setInnerWidget(object_ptr<InnerWidget>(this, controller))) {\n\t_top = setupTop();\n}\n\nnot_null<PeerData*> Widget::peer() const {\n\treturn _inner->peer();\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\treturn (memento->starrefPeer() == peer());\n}\n\nrpl::producer<QString> Widget::title() {\n\treturn tr::lng_star_ref_list_title();\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nrpl::producer<bool> Widget::desiredShadowVisibility() const {\n\treturn rpl::single<bool>(true);\n}\n\nvoid Widget::showFinished() {\n\t_inner->showFinished();\n}\n\nvoid Widget::setInnerFocus() {\n\t_inner->setInnerFocus();\n}\n\nvoid Widget::enableBackButton() {\n\t_backEnabled = true;\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(controller());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\nstd::unique_ptr<Ui::Premium::TopBarAbstract> Widget::setupTop() {\n\tauto title = tr::lng_star_ref_list_title();\n\tauto about = tr::lng_star_ref_list_about_channel(tr::marked);\n\n\tconst auto controller = this->controller();\n\tconst auto weak = base::make_weak(controller->parentController());\n\tconst auto clickContextOther = [=] {\n\t\treturn QVariant::fromValue(ClickHandlerContext{\n\t\t\t.sessionWindow = weak,\n\t\t\t.botStartAutoSubmit = true,\n\t\t});\n\t};\n\tauto result = std::make_unique<Ui::Premium::TopBar>(\n\t\tthis,\n\t\tst::starrefCover,\n\t\tUi::Premium::TopBarDescriptor{\n\t\t\t.clickContextOther = clickContextOther,\n\t\t\t.logo = u\"affiliate\"_q,\n\t\t\t.title = std::move(title),\n\t\t\t.about = std::move(about),\n\t\t\t.light = true,\n\t\t});\n\tconst auto raw = result.get();\n\n\tcontroller->wrapValue(\n\t) | rpl::on_next([=](Info::Wrap wrap) {\n\t\traw->setRoundEdges(wrap == Info::Wrap::Layer);\n\t}, raw->lifetime());\n\n\tconst auto baseHeight = st::starrefCoverHeight;\n\traw->resize(width(), baseHeight);\n\n\traw->additionalHeight(\n\t) | rpl::on_next([=](int additionalHeight) {\n\t\traw->setMaximumHeight(baseHeight + additionalHeight);\n\t\traw->setMinimumHeight(baseHeight + additionalHeight);\n\t\tsetPaintPadding({ 0, raw->height(), 0, 0 });\n\t}, raw->lifetime());\n\n\tcontroller->wrapValue(\n\t) | rpl::on_next([=](Info::Wrap wrap) {\n\t\tconst auto isLayer = (wrap == Info::Wrap::Layer);\n\t\t_back = base::make_unique_q<Ui::FadeWrap<Ui::IconButton>>(\n\t\t\traw,\n\t\t\tobject_ptr<Ui::IconButton>(\n\t\t\t\traw,\n\t\t\t\t(isLayer\n\t\t\t\t\t? st::infoLayerTopBar.back\n\t\t\t\t\t: st::infoTopBar.back)),\n\t\t\tst::infoTopBarScale);\n\t\t_back->setDuration(0);\n\t\t_back->toggleOn(isLayer\n\t\t\t? _backEnabled.value() | rpl::type_erased\n\t\t\t: rpl::single(true));\n\t\t_back->entity()->addClickHandler([=] {\n\t\t\tcontroller->showBackFromStack();\n\t\t});\n\t\t_back->entity()->setRippleColorOverride(\n\t\t\t&st::universalRippleAnimation.color);\n\t\t_back->toggledValue(\n\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\tconst auto &st = isLayer ? st::infoLayerTopBar : st::infoTopBar;\n\t\t\traw->setTextPosition(\n\t\t\t\ttoggled ? st.back.width : st.titlePosition.x(),\n\t\t\t\tst.titlePosition.y());\n\t\t}, _back->lifetime());\n\n\t\tif (!isLayer) {\n\t\t\t_close = nullptr;\n\t\t} else {\n\t\t\t_close = base::make_unique_q<Ui::IconButton>(\n\t\t\t\traw,\n\t\t\t\tst::infoTopBarClose);\n\t\t\t_close->addClickHandler([=] {\n\t\t\t\tcontroller->parentController()->hideLayer();\n\t\t\t\tcontroller->parentController()->hideSpecialLayer();\n\t\t\t});\n\t\t\t_close->setRippleColorOverride(\n\t\t\t\t&st::universalRippleAnimation.color);\n\t\t\traw->widthValue(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\t_close->moveToRight(0, 0);\n\t\t\t}, _close->lifetime());\n\t\t}\n\t}, raw->lifetime());\n\n\traw->move(0, 0);\n\twidthValue() | rpl::on_next([=](int width) {\n\t\traw->resizeToWidth(width);\n\t\tsetScrollTopSkip(raw->height());\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\nbool Allowed(not_null<PeerData*> peer) {\n\tif (!peer->session().appConfig().starrefJoinAllowed()) {\n\t\treturn false;\n\t} else if (const auto user = peer->asUser()) {\n\t\treturn user->isSelf()\n\t\t\t|| (user->isBot() && user->botInfo->canEditInformation);\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn channel->isBroadcast() && channel->canPostMessages();\n\t}\n\treturn false;\n}\n\nstd::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer) {\n\treturn std::make_shared<Info::Memento>(\n\t\tstd::vector<std::shared_ptr<ContentMemento>>(\n\t\t\t1,\n\t\t\tstd::make_shared<Memento>(peer)));\n}\n\nobject_ptr<Ui::BoxContent> ProgramsListBox(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerData*> peer) {\n\tconst auto weak = std::make_shared<base::weak_qptr<PeerListBox>>();\n\tconst auto initBox = [=](not_null<PeerListBox*> box) {\n\t\t*weak = box;\n\t\tbox->addButton(tr::lng_close(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t};\n\n\tauto controller = std::make_unique<ListController>(\n\t\twindow,\n\t\tpeer,\n\t\tJoinType::Existing);\n\tcontroller->addForBotRequests() | rpl::on_next([=] {\n\t\tif (const auto strong = weak->get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t}, controller->lifetime());\n\n\treturn Box<PeerListBox>(std::move(controller), initBox);\n}\n\n} // namespace Info::BotStarRef::Join\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/bot/starref/info_bot_starref_join_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n\nnamespace Ui::Premium {\nclass TopBarAbstract;\n} // namespace Ui::Premium\n\nnamespace Ui {\ntemplate <typename Widget>\nclass FadeWrap;\nclass IconButton;\nclass AbstractButton;\nclass VerticalLayout;\nclass BoxContent;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Info::BotStarRef::Join {\n\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\tMemento(not_null<Controller*> controller);\n\tMemento(not_null<PeerData*> peer);\n\t~Memento();\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(QWidget *parent, not_null<Controller*> controller);\n\n\tbool showInternal(not_null<ContentMemento*> memento) override;\n\trpl::producer<QString> title() override;\n\trpl::producer<bool> desiredShadowVisibility() const override;\n\tvoid showFinished() override;\n\tvoid setInnerFocus() override;\n\tvoid enableBackButton() override;\n\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\t[[nodiscard]] std::unique_ptr<Ui::Premium::TopBarAbstract> setupTop();\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tconst not_null<InnerWidget*> _inner;\n\n\tstd::unique_ptr<Ui::Premium::TopBarAbstract> _top;\n\tbase::unique_qptr<Ui::FadeWrap<Ui::IconButton>> _back;\n\tbase::unique_qptr<Ui::IconButton> _close;\n\trpl::variable<bool> _backEnabled;\n\n};\n\n[[nodiscard]] bool Allowed(not_null<PeerData*> peer);\n[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer);\n\n[[nodiscard]] object_ptr<Ui::BoxContent> ProgramsListBox(\n\tnot_null<Window::SessionController*> window,\n\tnot_null<PeerData*> peer);\n\n} // namespace Info::BotStarRef::Join\n"
  },
  {
    "path": "Telegram/SourceFiles/info/bot/starref/info_bot_starref_setup_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/bot/starref/info_bot_starref_setup_widget.h\"\n\n#include \"apiwrap.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/unixtime.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/data_user.h\"\n#include \"info/bot/starref/info_bot_starref_common.h\"\n#include \"info/bot/starref/info_bot_starref_join_widget.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/effects/premium_top_bar.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Info::BotStarRef::Setup {\nnamespace {\n\nconstexpr auto kDurationForeverValue = 999;\nconstexpr auto kCommissionDefault = 200;\nconstexpr auto kDurationDefault = 12;\nconstexpr auto kDisabledFade = 0.3;\n\n} // namespace\n\nstruct State {\n\tnot_null<UserData*> user;\n\tStarRefProgram program;\n\tbool exists = false;\n};\n\nclass InnerWidget final : public Ui::RpWidget {\npublic:\n\tInnerWidget(QWidget *parent, not_null<Controller*> controller);\n\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\t[[nodiscard]] not_null<State*> state();\n\n\tvoid showFinished();\n\tvoid setInnerFocus();\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\nprivate:\n\tvoid prepare();\n\tvoid setupInfo();\n\tvoid setupCommission();\n\tvoid setupDuration();\n\tvoid setupViewExisting();\n\tvoid setupEnd();\n\n\t[[nodiscard]] object_ptr<Ui::RpWidget> infoRow(\n\t\trpl::producer<QString> title,\n\t\trpl::producer<QString> text,\n\t\tnot_null<const style::icon*> icon);\n\n\tconst not_null<Controller*> _controller;\n\tState _state;\n\tconst not_null<Ui::VerticalLayout*> _container;\n\n};\n\n[[nodiscard]] int ValueForCommission(const State &state) {\n\treturn state.program.commission\n\t\t? state.program.commission\n\t\t: kCommissionDefault;\n}\n\n[[nodiscard]] int ValueForDurationMonths(const State &state) {\n\treturn state.program.durationMonths\n\t\t? state.program.durationMonths\n\t\t: state.exists\n\t\t? kDurationForeverValue\n\t\t: kDurationDefault;\n}\n\n[[nodiscard]] State StateForPeer(not_null<PeerData*> peer) {\n\tconst auto user = peer->asUser();\n\tconst auto program = user->botInfo->starRefProgram;\n\treturn State{\n\t\t.user = user,\n\t\t.program = program,\n\t\t.exists = (program.commission > 0) && !program.endDate,\n\t};\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeSliderWithTopTag(\n\t\tQWidget *parent,\n\t\tnot_null<const style::MediaSlider*> sliderStyle,\n\t\tnot_null<const style::FlatLabel*> labelStyle,\n\t\tint valuesCount,\n\t\tFn<int(int)> valueByIndex,\n\t\tint value,\n\t\tFn<void(int)> valueProgress,\n\t\tFn<void(int)> valueFinished,\n\t\tFn<QString(int)> textByValue,\n\t\tbool forbidLessThanValue) {\n\tauto result = object_ptr<Ui::VerticalLayout>(parent);\n\tconst auto raw = result.data();\n\n\tconst auto labels = raw->add(object_ptr<Ui::RpWidget>(raw));\n\tconst auto min = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\ttextByValue(valueByIndex(0)),\n\t\t*labelStyle);\n\tconst auto max = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\ttextByValue(valueByIndex(valuesCount - 1)),\n\t\t*labelStyle);\n\tconst auto current = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\ttextByValue(value),\n\t\t*labelStyle);\n\tconst auto slider = raw->add(object_ptr<Ui::MediaSliderWheelless>(\n\t\traw,\n\t\t*sliderStyle));\n\tlabels->resize(\n\t\tlabels->width(),\n\t\tcurrent->height() + st::defaultVerticalListSkip);\n\tstruct State {\n\t\tint indexMin = 0;\n\t\tint index = 0;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>();\n\tconst auto updatePalette = [=] {\n\t\tconst auto disabled = anim::color(\n\t\t\tst::windowSubTextFg,\n\t\t\tst::windowBg,\n\t\t\tkDisabledFade);\n\t\tmin->setTextColorOverride(!state->index\n\t\t\t? st::windowActiveTextFg->c\n\t\t\t: (state->indexMin > 0)\n\t\t\t? disabled\n\t\t\t: st::windowSubTextFg->c);\n\t\tmax->setTextColorOverride((state->index == valuesCount - 1)\n\t\t\t? st::windowActiveTextFg->c\n\t\t\t: st::windowSubTextFg->c);\n\t\tcurrent->setTextColorOverride(st::windowActiveTextFg->c);\n\t};\n\tconst auto updateByIndex = [=] {\n\t\tupdatePalette();\n\n\t\tcurrent->setVisible(state->index > 0\n\t\t\t&& state->index < valuesCount - 1);\n\t\tconst auto outer = labels->width();\n\t\tconst auto minWidth = min->width();\n\t\tconst auto maxWidth = max->width();\n\t\tconst auto currentWidth = current->width();\n\t\tif (minWidth + maxWidth + currentWidth > outer) {\n\t\t\treturn;\n\t\t}\n\n\t\tmin->moveToLeft(0, 0, outer);\n\t\tmax->moveToRight(0, 0, outer);\n\n\t\tconst auto sliderSkip = sliderStyle->seekSize.width();\n\t\tconst auto availableForCurrent = outer - sliderSkip;\n\t\tconst auto ratio = state->index / float64(valuesCount - 1);\n\t\tconst auto desiredLeft = (sliderSkip / 2)\n\t\t\t+ availableForCurrent * ratio\n\t\t\t- (currentWidth / 2);\n\t\tconst auto minLeft = minWidth;\n\t\tconst auto maxLeft = outer - maxWidth - currentWidth;\n\t\tcurrent->moveToLeft(\n\t\t\tstd::clamp(int(base::SafeRound(desiredLeft)), minLeft, maxLeft),\n\t\t\t0,\n\t\t\touter);\n\t};\n\tconst auto updateByValue = [=](int value) {\n\t\tcurrent->setText(textByValue(value));\n\n\t\tstate->index = 0;\n\t\tauto maxIndex = valuesCount - 1;\n\t\twhile (state->index < maxIndex) {\n\t\t\tconst auto mid = (state->index + maxIndex) / 2;\n\t\t\tconst auto midValue = valueByIndex(mid);\n\t\t\tif (midValue == value) {\n\t\t\t\tstate->index = mid;\n\t\t\t\tbreak;\n\t\t\t} else if (midValue < value) {\n\t\t\t\tstate->index = mid + 1;\n\t\t\t} else {\n\t\t\t\tmaxIndex = mid - 1;\n\t\t\t}\n\t\t}\n\t\tupdateByIndex();\n\t};\n\tconst auto progress = [=](int value) {\n\t\tupdateByValue(value);\n\t\tvalueProgress(value);\n\t};\n\tconst auto finished = [=](int value) {\n\t\tupdateByValue(value);\n\t\tvalueFinished(value);\n\t};\n\tstyle::PaletteChanged() | rpl::on_next([=] {\n\t\tupdatePalette();\n\t}, raw->lifetime());\n\tupdateByValue(value);\n\tstate->indexMin = forbidLessThanValue ? state->index : 0;\n\n\tslider->setPseudoDiscrete(\n\t\tvaluesCount,\n\t\tvalueByIndex,\n\t\tvalue,\n\t\tprogress,\n\t\tfinished,\n\t\tstate->indexMin);\n\tslider->resize(slider->width(), sliderStyle->seekSize.height());\n\n\tif (state->indexMin > 0) {\n\t\tconst auto overlay = Ui::CreateChild<Ui::RpWidget>(slider);\n\t\toverlay->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tslider->sizeValue() | rpl::on_next([=](QSize size) {\n\t\t\toverlay->setGeometry(0, 0, size.width(), size.height());\n\t\t}, slider->lifetime());\n\t\toverlay->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(overlay);\n\t\t\tconst auto sections = valuesCount - 1;\n\t\t\tconst auto shift = sliderStyle->seekSize.width();\n\t\t\tconst auto skip = shift / 2.;\n\t\t\tconst auto available = overlay->width() - shift;\n\t\t\tconst auto till = state->indexMin / float64(sections);\n\t\t\tconst auto now = state->index / float64(sections);\n\t\t\tconst auto edge = available * now;\n\t\t\tconst auto right = int(base::SafeRound(\n\t\t\t\tstd::min(skip + available * till, edge)));\n\t\t\tif (right > 0) {\n\t\t\t\tp.setOpacity(kDisabledFade);\n\t\t\t\tp.fillRect(0, 0, right, overlay->height(), st::windowBg);\n\t\t\t}\n\t\t}, overlay->lifetime());\n\t}\n\n\traw->widthValue() | rpl::on_next([=](int width) {\n\t\tlabels->resizeToWidth(width);\n\t\tupdateByIndex();\n\t}, slider->lifetime());\n\n\treturn result;\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeSliderWithTopLabels(\n\t\tQWidget *parent,\n\t\tnot_null<const style::MediaSlider*> sliderStyle,\n\t\tnot_null<const style::FlatLabel*> labelStyle,\n\t\tint valuesCount,\n\t\tFn<int(int)> valueByIndex,\n\t\tint value,\n\t\tFn<void(int)> valueProgress,\n\t\tFn<void(int)> valueFinished,\n\t\tFn<QString(int)> textByValue,\n\t\tbool forbidLessThanValue) {\n\tauto result = object_ptr<Ui::VerticalLayout>(parent);\n\tconst auto raw = result.data();\n\n\tconst auto labels = raw->add(object_ptr<Ui::RpWidget>(raw));\n\tconst auto slider = raw->add(object_ptr<Ui::MediaSliderWheelless>(\n\t\traw,\n\t\t*sliderStyle));\n\n\tstruct State {\n\t\tstd::vector<not_null<Ui::FlatLabel*>> labels;\n\t\tint indexMin = 0;\n\t\tint index = 0;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>();\n\n\tfor (auto i = 0; i != valuesCount; ++i) {\n\t\tstate->labels.push_back(Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tlabels,\n\t\t\ttextByValue(valueByIndex(i)),\n\t\t\t*labelStyle));\n\t}\n\tlabels->widthValue() | rpl::on_next([=](int outer) {\n\t\tconst auto shift = sliderStyle->seekSize.width() / 2;\n\t\tconst auto available = outer - sliderStyle->seekSize.width();\n\t\tfor (auto i = 0; i != state->labels.size(); ++i) {\n\t\t\tconst auto label = state->labels[i];\n\t\t\tconst auto width = label->width();\n\t\t\tconst auto half = width / 2;\n\t\t\tconst auto progress = (i / float64(valuesCount - 1));\n\t\t\tconst auto left = int(base::SafeRound(progress * available));\n\t\t\tlabel->moveToLeft(\n\t\t\t\tstd::max(std::min(shift + left - half, outer - width), 0),\n\t\t\t\t0,\n\t\t\t\touter);\n\t\t}\n\t}, slider->lifetime());\n\tlabels->resize(\n\t\tlabels->width(),\n\t\tstate->labels.back()->height() + st::defaultVerticalListSkip);\n\n\tconst auto updatePalette = [=] {\n\t\tconst auto disabled = anim::color(\n\t\t\tst::windowSubTextFg,\n\t\t\tst::windowBg,\n\t\t\tkDisabledFade);\n\t\tfor (auto i = 0; i != state->labels.size(); ++i) {\n\t\t\tstate->labels[i]->setTextColorOverride((state->index == i)\n\t\t\t\t? st::windowActiveTextFg->c\n\t\t\t\t: (state->index < state->indexMin)\n\t\t\t\t? disabled\n\t\t\t\t: st::windowSubTextFg->c);\n\t\t}\n\t};\n\tconst auto updateByIndex = [=] {\n\t\tupdatePalette();\n\t};\n\tconst auto updateByValue = [=](int value) {\n\t\tstate->index = 0;\n\t\tauto maxIndex = valuesCount - 1;\n\t\twhile (state->index < maxIndex) {\n\t\t\tconst auto mid = (state->index + maxIndex) / 2;\n\t\t\tconst auto midValue = valueByIndex(mid);\n\t\t\tif (midValue == value) {\n\t\t\t\tstate->index = mid;\n\t\t\t\tbreak;\n\t\t\t} else if (midValue < value) {\n\t\t\t\tstate->index = mid + 1;\n\t\t\t} else {\n\t\t\t\tmaxIndex = mid - 1;\n\t\t\t}\n\t\t}\n\t\tupdateByIndex();\n\t};\n\tconst auto progress = [=](int value) {\n\t\tupdateByValue(value);\n\t\tvalueProgress(value);\n\t};\n\tconst auto finished = [=](int value) {\n\t\tupdateByValue(value);\n\t\tvalueFinished(value);\n\t};\n\tstyle::PaletteChanged() | rpl::on_next([=] {\n\t\tupdatePalette();\n\t}, raw->lifetime());\n\tupdateByValue(value);\n\tstate->indexMin = forbidLessThanValue ? state->index : 0;\n\n\tslider->setPseudoDiscrete(\n\t\tvaluesCount,\n\t\tvalueByIndex,\n\t\tvalue,\n\t\tprogress,\n\t\tfinished,\n\t\tstate->indexMin);\n\tslider->resize(slider->width(), sliderStyle->seekSize.height());\n\n\tif (state->indexMin > 0) {\n\t\tconst auto overlay = Ui::CreateChild<Ui::RpWidget>(slider);\n\t\toverlay->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tslider->sizeValue() | rpl::on_next([=](QSize size) {\n\t\t\toverlay->setGeometry(0, 0, size.width(), size.height());\n\t\t}, slider->lifetime());\n\t\toverlay->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(overlay);\n\n\t\t\tconst auto sections = valuesCount - 1;\n\t\t\tconst auto shift = sliderStyle->seekSize.width();\n\t\t\tconst auto skip = shift / 2.;\n\t\t\tconst auto available = overlay->width() - shift;\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tconst auto stroke = style::ConvertScale(3);\n\t\t\tp.setPen(QPen(st::windowBg, stroke));\n\t\t\tconst auto diameter = shift - stroke;\n\t\t\tconst auto radius = diameter / 2.;\n\t\t\tconst auto top = (sliderStyle->seekSize.height() / 2.) - radius;\n\t\t\tfor (auto i = 0; i != valuesCount; ++i) {\n\t\t\t\tif (i < state->index) {\n\t\t\t\t\tp.setBrush(st::sliderBgActive);\n\t\t\t\t} else if (i > state->index) {\n\t\t\t\t\tp.setBrush(st::sliderBgInactive);\n\t\t\t\t} else {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto progress = i / float64(sections);\n\t\t\t\tconst auto position = skip + available * progress;\n\t\t\t\tp.drawEllipse(position - radius, top, diameter, diameter);\n\t\t\t}\n\n\t\t\tconst auto till = state->indexMin / float64(sections);\n\t\t\tconst auto now = state->index / float64(sections);\n\t\t\tconst auto edge = available * now;\n\t\t\tconst auto right = int(base::SafeRound(\n\t\t\t\tstd::min(skip + available * till + radius, edge)));\n\t\t\tif (right > 0) {\n\t\t\t\tp.setOpacity(kDisabledFade);\n\t\t\t\tp.fillRect(0, 0, right, overlay->height(), st::windowBg);\n\t\t\t}\n\t\t}, overlay->lifetime());\n\t}\n\n\traw->widthValue() | rpl::on_next([=](int width) {\n\t\tlabels->resizeToWidth(width);\n\t\tupdateByIndex();\n\t}, slider->lifetime());\n\n\treturn result;\n}\n\n[[nodiscard]] QString FormatTimeLeft(int seconds) {\n\tconst auto hours = seconds / 3600;\n\tconst auto minutes = (seconds % 3600) / 60;\n\tseconds %= 60;\n\tif (hours > 0) {\n\t\treturn u\"%1:%2:%3\"_q\n\t\t\t.arg(hours)\n\t\t\t.arg(minutes, 2, 10, QChar('0'))\n\t\t\t.arg(seconds, 2, 10, QChar('0'));\n\t}\n\treturn u\"%1:%2\"_q\n\t\t.arg(minutes)\n\t\t.arg(seconds, 2, 10, QChar('0'));\n}\n\n[[nodiscard]] object_ptr<Ui::RoundButton> MakeStartButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tFn<TimeId()> endDate,\n\t\tbool exists) {\n\tauto result = object_ptr<Ui::RoundButton>(\n\t\tparent,\n\t\trpl::single(QString()),\n\t\tst::starrefBottomButton);\n\tconst auto raw = result.data();\n\traw->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\trpl::combine(\n\t\tparent->widthValue(),\n\t\traw->widthValue()\n\t) | rpl::on_next([=](int outer, int inner) {\n\t\tconst auto padding = st::starrefButtonMargin;\n\t\tconst auto added = padding.left() + padding.right();\n\t\tif (outer > added && outer - added != inner) {\n\t\t\traw->resizeToWidth(outer - added);\n\t\t\traw->moveToLeft(padding.left(), padding.top(), outer);\n\t\t}\n\t}, raw->lifetime());\n\tstruct State {\n\t\tUi::FlatLabel *label = nullptr;\n\t\tUi::FlatLabel *sublabel = nullptr;\n\t\tQString labelText;\n\t\tQString sublabelText;\n\t\tFn<void()> update;\n\t\trpl::lifetime lockedLifetime;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>();\n\n\tstate->label = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\tQString(),\n\t\tst::starrefBottomButtonLabel);\n\tstate->label->show();\n\tstate->label->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tstate->sublabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\tQString(),\n\t\tst::starrefBottomButtonSublabel);\n\tstate->sublabel->show();\n\tstate->sublabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\trpl::combine(\n\t\traw->widthValue(),\n\t\tstate->label->widthValue(),\n\t\tstate->sublabel->widthValue()\n\t) | rpl::on_next([=](int outer, int label, int sublabel) {\n\t\tif (sublabel > 0) {\n\t\t\tstate->label->moveToLeft(\n\t\t\t\t(outer - label) / 2,\n\t\t\t\tst::starrefBottomButtonLabelTop);\n\t\t\tstate->sublabel->moveToLeft(\n\t\t\t\t(outer - sublabel) / 2,\n\t\t\t\tst::starrefBottomButtonSublabelTop);\n\t\t} else {\n\t\t\tstate->label->moveToLeft(\n\t\t\t\t(outer - label) / 2,\n\t\t\t\t(raw->height() - state->label->height()) / 2);\n\t\t\tstate->sublabel->move(0, raw->height() * 2);\n\t\t}\n\t}, raw->lifetime());\n\n\tconst auto updatePalette = [=] {\n\t\tauto color = st::windowFgActive->c;\n\t\tif (state->lockedLifetime) {\n\t\t\tcolor.setAlphaF((1. - kDisabledFade) * color.alphaF());\n\t\t}\n\t\tstate->label->setTextColorOverride(color);\n\t\tstate->sublabel->setTextColorOverride(color);\n\t};\n\tupdatePalette();\n\tstyle::PaletteChanged(\n\t) | rpl::on_next(updatePalette, raw->lifetime());\n\n\tstate->update = [=] {\n\t\tconst auto set = [&](\n\t\t\t\tUi::FlatLabel *label,\n\t\t\t\tQString &was,\n\t\t\t\tQString now) {\n\t\t\tif (was != now) {\n\t\t\t\tlabel->setText(now);\n\t\t\t\twas = now;\n\t\t\t}\n\t\t};\n\t\tconst auto till = endDate();\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto left = (till > now) ? (till - now) : 0;\n\t\tif (left) {\n\t\t\tif (!state->lockedLifetime) {\n\t\t\t\tstate->lockedLifetime = base::timer_each(\n\t\t\t\t\t100\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\tstate->update();\n\t\t\t\t});\n\t\t\t\tset(\n\t\t\t\t\tstate->label,\n\t\t\t\t\tstate->labelText,\n\t\t\t\t\ttr::lng_star_ref_start(tr::now));\n\t\t\t\traw->clearState();\n\t\t\t\traw->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\t\tupdatePalette();\n\t\t\t}\n\t\t\tset(\n\t\t\t\tstate->sublabel,\n\t\t\t\tstate->sublabelText,\n\t\t\t\ttr::lng_star_ref_start_disabled(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_time,\n\t\t\t\t\tFormatTimeLeft(left)));\n\t\t} else {\n\t\t\tif (state->lockedLifetime) {\n\t\t\t\tstate->lockedLifetime.destroy();\n\t\t\t\traw->setAttribute(Qt::WA_TransparentForMouseEvents, false);\n\t\t\t\tupdatePalette();\n\t\t\t}\n\t\t\tset(\n\t\t\t\tstate->sublabel,\n\t\t\t\tstate->sublabelText,\n\t\t\t\tQString());\n\t\t\tset(\n\t\t\t\tstate->label,\n\t\t\t\tstate->labelText,\n\t\t\t\t(exists ? tr::lng_star_ref_update : tr::lng_star_ref_start)(\n\t\t\t\t\ttr::now));\n\t\t}\n\t};\n\tstate->update();\n\n\treturn result;\n}\n\nInnerWidget::InnerWidget(QWidget *parent, not_null<Controller*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _state(StateForPeer(_controller->key().starrefPeer()))\n, _container(Ui::CreateChild<Ui::VerticalLayout>(this)) {\n\tprepare();\n}\n\nnot_null<State*> InnerWidget::state() {\n\treturn &_state;\n}\n\nvoid InnerWidget::prepare() {\n\tUi::ResizeFitChild(this, _container);\n\n\tsetupInfo();\n\tUi::AddSkip(_container);\n\tUi::AddDivider(_container);\n\tsetupCommission();\n\tsetupDuration();\n\tUi::AddSkip(_container);\n\tsetupViewExisting();\n\tsetupEnd();\n}\n\nvoid InnerWidget::setupInfo() {\n\tAddSkip(_container, st::defaultVerticalListSkip * 2);\n\n\t_container->add(infoRow(\n\t\ttr::lng_star_ref_share_title(),\n\t\ttr::lng_star_ref_share_about(),\n\t\t&st::menuIconStarRefShare));\n\n\t_container->add(infoRow(\n\t\ttr::lng_star_ref_launch_title(),\n\t\ttr::lng_star_ref_launch_about(),\n\t\t&st::menuIconChannel));\n\n\t_container->add(infoRow(\n\t\ttr::lng_star_ref_let_title(),\n\t\ttr::lng_star_ref_let_about(),\n\t\t&st::menuIconStarRefLink));\n}\n\nvoid InnerWidget::setupCommission() {\n\tUi::AddSkip(_container);\n\tUi::AddSubsectionTitle(_container, tr::lng_star_ref_commission_title());\n\n\tconst auto appConfig = &_controller->session().appConfig();\n\tconst auto commissionMin = std::clamp(\n\t\tappConfig->starrefCommissionMin(),\n\t\t1,\n\t\t998);\n\tconst auto commissionMax = std::clamp(\n\t\tappConfig->starrefCommissionMax(),\n\t\tcommissionMin + 1,\n\t\t999);\n\tconst auto commission = std::clamp(\n\t\tValueForCommission(_state),\n\t\tcommissionMin,\n\t\tcommissionMax);\n\n\tauto valueMin = (commissionMin + 9) / 10;\n\tauto valueMax = commissionMax / 10;\n\n\tauto values = std::vector<int>();\n\tif (commission < valueMin * 10) {\n\t\tvalues.push_back(commission);\n\t}\n\tfor (auto i = valueMin; i != valueMax + 1; ++i) {\n\t\tvalues.push_back(i * 10);\n\t\tif (i * 10 < commission\n\t\t\t&& (i == valueMax || (i + 1) * 10 > commission)) {\n\t\t\tvalues.push_back(commission);\n\t\t}\n\t}\n\tconst auto valuesCount = int(values.size());\n\tconst auto setCommission = [=](int value) {\n\t\t_state.program.commission = value;\n\t};\n\t_container->add(\n\t\tMakeSliderWithTopTag(\n\t\t\t_container,\n\t\t\t&st::settingsScale,\n\t\t\t&st::settingsScaleLabel,\n\t\t\tvaluesCount,\n\t\t\t[=](int index) { return values[index]; },\n\t\t\tcommission,\n\t\t\tsetCommission,\n\t\t\tsetCommission,\n\t\t\t[=](int value) { return FormatCommission(value); },\n\t\t\t_state.exists),\n\t\tst::boxRowPadding);\n\t_state.program.commission = commission;\n\n\tUi::AddSkip(_container, st::defaultVerticalListSkip * 2);\n\tUi::AddDividerText(_container, tr::lng_star_ref_commission_about());\n}\n\nvoid InnerWidget::setupDuration() {\n\tUi::AddSkip(_container);\n\tUi::AddSubsectionTitle(_container, tr::lng_star_ref_duration_title());\n\n\tconst auto durationMonths = ValueForDurationMonths(_state);\n\n\tauto values = std::vector<int>{ 1, 3, 6, 12, 24, 36, 999 };\n\tif (!ranges::contains(values, durationMonths)) {\n\t\tvalues.push_back(durationMonths);\n\t\tranges::sort(values);\n\t}\n\tconst auto valuesCount = int(values.size());\n\tconst auto setDurationMonths = [=](int value) {\n\t\t_state.program.durationMonths = (value == kDurationForeverValue)\n\t\t\t? 0\n\t\t\t: value;\n\t};\n\tconst auto label = [=](int value) {\n\t\treturn (value < 12)\n\t\t\t? tr::lng_months_tiny(tr::now, lt_count, value)\n\t\t\t: (value < 999)\n\t\t\t? tr::lng_years_tiny(tr::now, lt_count, value / 12)\n\t\t\t: QString::fromUtf8(\"\\xE2\\x88\\x9E\"); // utf-8 infinity\n\t};\n\t_container->add(\n\t\tMakeSliderWithTopLabels(\n\t\t\t_container,\n\t\t\t&st::settingsScale,\n\t\t\t&st::settingsScaleLabel,\n\t\t\tvaluesCount,\n\t\t\t[=](int index) { return values[index]; },\n\t\t\tdurationMonths,\n\t\t\tsetDurationMonths,\n\t\t\tsetDurationMonths,\n\t\t\tlabel,\n\t\t\t_state.exists),\n\t\tst::boxRowPadding);\n\t_state.program.durationMonths = durationMonths;\n\n\tUi::AddSkip(_container, st::defaultVerticalListSkip * 2);\n\tUi::AddDividerText(_container, tr::lng_star_ref_duration_about());\n}\n\nvoid InnerWidget::setupViewExisting() {\n\tconst auto button = AddViewListButton(\n\t\t_container,\n\t\ttr::lng_star_ref_existing_title(),\n\t\ttr::lng_star_ref_existing_about());\n\tbutton->setClickedCallback([=] {\n\t\tconst auto window = _controller->parentController();\n\t\twindow->show(Join::ProgramsListBox(window, peer()));\n\t});\n\n\tUi::AddSkip(_container);\n\tUi::AddDivider(_container);\n\tUi::AddSkip(_container);\n}\n\nvoid InnerWidget::setupEnd() {\n\tif (!_state.exists) {\n\t\treturn;\n\t}\n\tconst auto end = _container->add(object_ptr<Ui::SettingsButton>(\n\t\t_container,\n\t\ttr::lng_star_ref_end(),\n\t\tst::settingsAttentionButton));\n\tend->setClickedCallback([=] {\n\t\tconst auto weak = base::make_weak(this);\n\t\tconst auto window = _controller->parentController();\n\t\tconst auto sent = std::make_shared<bool>();\n\t\twindow->show(ConfirmEndBox([=] {\n\t\t\tif (*sent) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t*sent = true;\n\t\t\tconst auto show = _controller->uiShow();\n\t\t\tFinishProgram(show, _state.user, [=](bool success) {\n\t\t\t\t*sent = false;\n\t\t\t\tif (!success) {\n\t\t\t\t\treturn;\n\t\t\t\t} else if ([[maybe_unused]] const auto strong = weak.get()) {\n\t\t\t\t\t_controller->showBackFromStack();\n\t\t\t\t\twindow->showToast({\n\t\t\t\t\t\t.title = tr::lng_star_ref_ended_title(tr::now),\n\t\t\t\t\t\t.text = tr::lng_star_ref_ended_text(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t\t.duration = Ui::Toast::kDefaultDuration * 3,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t}));\n\t});\n\tUi::AddSkip(_container);\n\tUi::AddDivider(_container);\n}\n\nobject_ptr<Ui::RpWidget> InnerWidget::infoRow(\n\t\trpl::producer<QString> title,\n\t\trpl::producer<QString> text,\n\t\tnot_null<const style::icon*> icon) {\n\tauto result = object_ptr<Ui::VerticalLayout>(_container);\n\tconst auto raw = result.data();\n\n\traw->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\traw,\n\t\t\tstd::move(title) | rpl::map(tr::bold),\n\t\t\tst::defaultFlatLabel),\n\t\tst::settingsPremiumRowTitlePadding);\n\traw->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\traw,\n\t\t\tstd::move(text),\n\t\t\tst::boxDividerLabel),\n\t\tst::settingsPremiumRowAboutPadding);\n\tobject_ptr<Info::Profile::FloatingIcon>(\n\t\traw,\n\t\t*icon,\n\t\tst::starrefInfoIconPosition);\n\n\treturn result;\n}\n\nnot_null<PeerData*> InnerWidget::peer() const {\n\treturn _controller->key().starrefPeer();\n}\n\nvoid InnerWidget::showFinished() {\n\n}\n\nvoid InnerWidget::setInnerFocus() {\n\tsetFocus();\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\n}\n\nMemento::Memento(not_null<Controller*> controller)\n: ContentMemento(Tag(controller->starrefPeer(), controller->starrefType())) {\n}\n\nMemento::Memento(not_null<PeerData*> peer)\n: ContentMemento(Tag(peer, Type::Setup)) {\n}\n\nMemento::~Memento() = default;\n\nSection Memento::section() const {\n\treturn Section(Section::Type::BotStarRef);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(parent, controller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: ContentWidget(parent, controller)\n, _inner(setInnerWidget(object_ptr<InnerWidget>(this, controller)))\n, _state(_inner->state()) {\n\t_top = setupTop();\n\t_bottom = setupBottom();\n}\n\nnot_null<PeerData*> Widget::peer() const {\n\treturn _inner->peer();\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\treturn (memento->starrefPeer() == peer());\n}\n\nrpl::producer<QString> Widget::title() {\n\treturn tr::lng_star_ref_title();\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nrpl::producer<bool> Widget::desiredShadowVisibility() const {\n\treturn rpl::single<bool>(true);\n}\n\nvoid Widget::showFinished() {\n\t_inner->showFinished();\n}\n\nvoid Widget::setInnerFocus() {\n\t_inner->setInnerFocus();\n}\n\nvoid Widget::enableBackButton() {\n\t_backEnabled = true;\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(controller());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\nstd::unique_ptr<Ui::Premium::TopBarAbstract> Widget::setupTop() {\n\tauto title = tr::lng_star_ref_title();\n\tauto about = tr::lng_star_ref_about(tr::marked);\n\n\tconst auto controller = this->controller();\n\tconst auto weak = base::make_weak(controller->parentController());\n\tconst auto clickContextOther = [=] {\n\t\treturn QVariant::fromValue(ClickHandlerContext{\n\t\t\t.sessionWindow = weak,\n\t\t\t.botStartAutoSubmit = true,\n\t\t});\n\t};\n\tauto result = std::make_unique<Ui::Premium::TopBar>(\n\t\tthis,\n\t\tst::starrefCover,\n\t\tUi::Premium::TopBarDescriptor{\n\t\t\t.clickContextOther = clickContextOther,\n\t\t\t.logo = u\"affiliate\"_q,\n\t\t\t.title = std::move(title),\n\t\t\t.about = std::move(about),\n\t\t\t.light = true,\n\t\t});\n\tconst auto raw = result.get();\n\n\tcontroller->wrapValue(\n\t) | rpl::on_next([=](Info::Wrap wrap) {\n\t\traw->setRoundEdges(wrap == Info::Wrap::Layer);\n\t}, raw->lifetime());\n\n\tconst auto baseHeight = st::starrefCoverHeight;\n\traw->resize(width(), baseHeight);\n\n\traw->additionalHeight(\n\t) | rpl::on_next([=](int additionalHeight) {\n\t\traw->setMaximumHeight(baseHeight + additionalHeight);\n\t\traw->setMinimumHeight(baseHeight + additionalHeight);\n\t\tsetPaintPadding({ 0, raw->height(), 0, 0 });\n\t}, raw->lifetime());\n\n\tcontroller->wrapValue(\n\t) | rpl::on_next([=](Info::Wrap wrap) {\n\t\tconst auto isLayer = (wrap == Info::Wrap::Layer);\n\t\t_back = base::make_unique_q<Ui::FadeWrap<Ui::IconButton>>(\n\t\t\traw,\n\t\t\tobject_ptr<Ui::IconButton>(\n\t\t\t\traw,\n\t\t\t\t(isLayer\n\t\t\t\t\t? st::infoLayerTopBar.back\n\t\t\t\t\t: st::infoTopBar.back)),\n\t\t\tst::infoTopBarScale);\n\t\t_back->setDuration(0);\n\t\t_back->toggleOn(isLayer\n\t\t\t? _backEnabled.value() | rpl::type_erased\n\t\t\t: rpl::single(true));\n\t\t_back->entity()->addClickHandler([=] {\n\t\t\tcontroller->showBackFromStack();\n\t\t});\n\t\t_back->toggledValue(\n\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\tconst auto &st = isLayer ? st::infoLayerTopBar : st::infoTopBar;\n\t\t\traw->setTextPosition(\n\t\t\t\ttoggled ? st.back.width : st.titlePosition.x(),\n\t\t\t\tst.titlePosition.y());\n\t\t}, _back->lifetime());\n\n\t\tif (!isLayer) {\n\t\t\t_close = nullptr;\n\t\t} else {\n\t\t\t_close = base::make_unique_q<Ui::IconButton>(\n\t\t\t\traw,\n\t\t\t\tst::infoTopBarClose);\n\t\t\t_close->addClickHandler([=] {\n\t\t\t\tcontroller->parentController()->hideLayer();\n\t\t\t\tcontroller->parentController()->hideSpecialLayer();\n\t\t\t});\n\t\t\traw->widthValue(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\t_close->moveToRight(0, 0);\n\t\t\t}, _close->lifetime());\n\t\t}\n\t}, raw->lifetime());\n\n\traw->move(0, 0);\n\twidthValue() | rpl::on_next([=](int width) {\n\t\traw->resizeToWidth(width);\n\t\tsetScrollTopSkip(raw->height());\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\nstd::unique_ptr<Ui::RpWidget> Widget::setupBottom() {\n\tauto result = std::make_unique<Ui::VerticalLayout>(this);\n\tconst auto raw = result.get();\n\n\tconst auto save = raw->add(MakeStartButton(raw, [=] {\n\t\treturn _state->user->botInfo->starRefProgram.endDate;\n\t}, _state->exists), st::starrefButtonMargin);\n\n\tconst auto &margins = st::defaultBoxDividerLabelPadding;\n\traw->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\traw,\n\t\t\t(_state->exists\n\t\t\t\t? tr::lng_star_ref_update_info\n\t\t\t\t: tr::lng_star_ref_start_info)(\n\t\t\t\t\tlt_terms,\n\t\t\t\t\ttr::lng_star_ref_button_link(\n\t\t\t\t\t\ttr::url(tr::lng_star_ref_tos_url(tr::now))),\n\t\t\t\t\ttr::marked),\n\t\t\tst::boxDividerLabel),\n\t\tQMargins(margins.left(), 0, margins.right(), 0));\n\tsave->setClickedCallback([=] {\n\t\tconst auto weak = base::make_weak(this);\n\t\tconst auto user = _state->user;\n\t\tconst auto program = _state->program;\n\t\tconst auto show = controller()->uiShow();\n\t\tconst auto exists = _state->exists;\n\t\tConfirmUpdate(show, user, program, exists, [=](Fn<void(bool)> done) {\n\t\t\tUpdateProgram(show, user, program, [=](bool success) {\n\t\t\t\tdone(success);\n\t\t\t\tif (weak) {\n\t\t\t\t\tcontroller()->showBackFromStack();\n\t\t\t\t}\n\t\t\t\tshow->showToast({\n\t\t\t\t\t.title = (exists\n\t\t\t\t\t\t? tr::lng_star_ref_updated_title\n\t\t\t\t\t\t: tr::lng_star_ref_created_title)(tr::now),\n\t\t\t\t\t.text = (exists\n\t\t\t\t\t\t? tr::lng_star_ref_updated_text\n\t\t\t\t\t\t: tr::lng_star_ref_created_text)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t.duration = Ui::Toast::kDefaultDuration * 3,\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\t});\n\n\twidthValue() | rpl::on_next([=](int width) {\n\t\traw->resizeToWidth(width);\n\t}, raw->lifetime());\n\n\trpl::combine(\n\t\traw->heightValue(),\n\t\theightValue()\n\t) | rpl::on_next([=](int height, int fullHeight) {\n\t\tsetScrollBottomSkip(height);\n\t\traw->move(0, fullHeight - height);\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\nbool Allowed(not_null<PeerData*> peer) {\n\treturn peer->isUser()\n\t\t&& peer->asUser()->isBot()\n\t\t&& peer->session().appConfig().starrefSetupAllowed();\n}\n\nstd::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer) {\n\treturn std::make_shared<Info::Memento>(\n\t\tstd::vector<std::shared_ptr<ContentMemento>>(\n\t\t\t1,\n\t\t\tstd::make_shared<Memento>(peer)));\n}\n\n} // namespace Info::BotStarRef::Setup\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/bot/starref/info_bot_starref_setup_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n#include \"info/bot/starref/info_bot_starref_common.h\"\n\nnamespace Ui::Premium {\nclass TopBarAbstract;\n} // namespace Ui::Premium\n\nnamespace Ui {\ntemplate <typename Widget>\nclass FadeWrap;\nclass IconButton;\nclass AbstractButton;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Info::BotStarRef::Setup {\n\nstruct State;\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\tMemento(not_null<Controller*> controller);\n\tMemento(not_null<PeerData*> peer);\n\t~Memento();\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(QWidget *parent, not_null<Controller*> controller);\n\n\tbool showInternal(not_null<ContentMemento*> memento) override;\n\trpl::producer<QString> title() override;\n\trpl::producer<bool> desiredShadowVisibility() const override;\n\tvoid showFinished() override;\n\tvoid setInnerFocus() override;\n\tvoid enableBackButton() override;\n\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\t[[nodiscard]] std::unique_ptr<Ui::Premium::TopBarAbstract> setupTop();\n\t[[nodiscard]] std::unique_ptr<Ui::RpWidget> setupBottom();\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tconst not_null<InnerWidget*> _inner;\n\tconst not_null<State*> _state;\n\n\tstd::unique_ptr<Ui::Premium::TopBarAbstract> _top;\n\tbase::unique_qptr<Ui::FadeWrap<Ui::IconButton>> _back;\n\tbase::unique_qptr<Ui::IconButton> _close;\n\trpl::variable<bool> _backEnabled;\n\n\tstd::unique_ptr<Ui::RpWidget> _bottom;\n\n};\n\n[[nodiscard]] bool Allowed(not_null<PeerData*> peer);\n[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer);\n\n} // namespace Info::BotStarRef::Setup\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/channel_statistics/boosts/create_giveaway_box.h\"\n\n#include \"api/api_credits.h\"\n#include \"api/api_premium.h\"\n#include \"base/call_delayed.h\"\n#include \"base/unixtime.h\"\n#include \"countries/countries_instance.h\"\n#include \"data/data_peer.h\"\n#include \"info/channel_statistics/boosts/giveaway/boost_badge.h\"\n#include \"info/channel_statistics/boosts/giveaway/giveaway_list_controllers.h\"\n#include \"info/channel_statistics/boosts/giveaway/giveaway_type_row.h\"\n#include \"info/channel_statistics/boosts/giveaway/select_countries_box.h\"\n#include \"info/channel_statistics/boosts/info_boosts_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"info/statistics/info_statistics_list_controllers.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"payments/payments_checkout_process.h\" // Payments::CheckoutProcess\n#include \"payments/payments_form.h\" // Payments::InvoicePremiumGiftCode\n#include \"settings/settings_common.h\"\n#include \"settings/sections/settings_premium.h\" // Settings::ShowPremium\n#include \"ui/boxes/choose_date_time.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/effects/premium_top_bar.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_color_indices.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_statistics.h\"\n\n#include <xxhash.h> // XXH64.\n\nnamespace {\n\nconstexpr auto kDoneTooltipDuration = 5 * crl::time(1000);\nconstexpr auto kAdditionalPrizeLengthMax = 128;\n\n[[nodiscard]] QDateTime ThreeDaysAfterToday() {\n\tauto dateNow = QDateTime::currentDateTime();\n\tdateNow = dateNow.addDays(3);\n\tauto timeNow = dateNow.time();\n\twhile (timeNow.minute() % 5) {\n\t\ttimeNow = timeNow.addSecs(60);\n\t}\n\tdateNow.setTime(timeNow);\n\treturn dateNow;\n}\n\n[[nodiscard]] uint64 UniqueIdFromCreditsOption(\n\t\tconst Data::CreditsGiveawayOption &d,\n\t\tnot_null<PeerData*> peer) {\n\tconst auto string = QString::number(d.credits)\n\t\t+ d.storeProduct\n\t\t+ d.currency\n\t\t+ QString::number(d.amount)\n\t\t+ QString::number(peer->id.value)\n\t\t+ QString::number(peer->session().uniqueId());\n\n\treturn XXH64(string.data(), string.size() * sizeof(ushort), 0);\n}\n\n[[nodiscard]] Fn<bool(int)> CreateErrorCallback(\n\t\tint max,\n\t\ttr::phrase<lngtag_count> phrase) {\n\treturn [=](int count) {\n\t\tconst auto error = (count >= max);\n\t\tif (error) {\n\t\t\tUi::Toast::Show(phrase(tr::now, lt_count, max));\n\t\t}\n\t\treturn error;\n\t};\n}\n\n[[nodiscard]] QWidget *FindFirstShadowInBox(not_null<Ui::BoxContent*> box) {\n\tfor (const auto &child : box->children()) {\n\t\tif (child && child->isWidgetType()) {\n\t\t\tconst auto w = static_cast<QWidget*>(child);\n\t\t\tif (w->height() == st::lineWidth) {\n\t\t\t\treturn w;\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid AddPremiumTopBarWithDefaultTitleBar(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\trpl::producer<> showFinished,\n\t\trpl::producer<QString> titleText,\n\t\trpl::producer<TextWithEntities> subtitleText) {\n\tstruct State final {\n\t\tUi::Animations::Simple animation;\n\t\tUi::Text::String title;\n\n\t\tUi::RpWidget close;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tbox->setNoContentMargin(true);\n\n\tstd::move(\n\t\ttitleText\n\t) | rpl::on_next([=](const QString &s) {\n\t\tstate->title.setText(st::startGiveawayBox.title.style, s);\n\t}, box->lifetime());\n\n\tconst auto hPadding = rect::m::sum::h(st::boxRowPadding);\n\tconst auto titlePaintContext = Ui::Text::PaintContext{\n\t\t.position = st::boxTitlePosition,\n\t\t.outerWidth = (st::boxWideWidth - hPadding),\n\t\t.availableWidth = (st::boxWideWidth - hPadding),\n\t};\n\n\tconst auto isCloseBarShown = [=] { return box->scrollTop() > 0; };\n\n\tconst auto closeTopBar = box->setPinnedToTopContent(\n\t\tobject_ptr<Ui::RpWidget>(box));\n\tcloseTopBar->resize(box->width(), st::boxTitleHeight);\n\tcloseTopBar->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = Painter(closeTopBar);\n\t\tconst auto r = closeTopBar->rect();\n\t\tconst auto radius = st::boxRadius;\n\t\tconst auto progress = state->animation.value(isCloseBarShown()\n\t\t\t? 1.\n\t\t\t: 0.);\n\t\tconst auto resultRect = r + QMargins{ 0, 0, 0, radius };\n\t\t{\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\t\tif (progress < 1.) {\n\t\t\t\tauto path = QPainterPath();\n\t\t\t\tpath.addRect(resultRect);\n\t\t\t\tpath.addRect(\n\t\t\t\t\tst::boxRowPadding.left(),\n\t\t\t\t\t0,\n\t\t\t\t\tresultRect.width() - hPadding,\n\t\t\t\t\tresultRect.height());\n\t\t\t\tp.setClipPath(path);\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(st::boxDividerBg);\n\t\t\t\tp.drawRoundedRect(resultRect, radius, radius);\n\t\t\t}\n\t\t\tif (progress > 0.) {\n\t\t\t\tp.setOpacity(progress);\n\n\t\t\t\tp.setClipping(false);\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(st::boxBg);\n\t\t\t\tp.drawRoundedRect(resultRect, radius, radius);\n\n\t\t\t\tp.setPen(st::startGiveawayBox.title.textFg);\n\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t\tstate->title.draw(p, titlePaintContext);\n\t\t\t}\n\t\t}\n\t}, closeTopBar->lifetime());\n\n\t{\n\t\tconst auto close = Ui::CreateChild<Ui::IconButton>(\n\t\t\tcloseTopBar.get(),\n\t\t\tst::startGiveawayBoxTitleClose);\n\t\tclose->setClickedCallback([=] { box->closeBox(); });\n\t\tcloseTopBar->widthValue(\n\t\t) | rpl::on_next([=](int w) {\n\t\t\tconst auto &pos = st::giveawayGiftCodeCoverClosePosition;\n\t\t\tclose->moveToRight(pos.x(), pos.y());\n\t\t}, box->lifetime());\n\t\tclose->show();\n\t}\n\n\tconst auto bar = Ui::CreateChild<Ui::Premium::TopBar>(\n\t\tbox.get(),\n\t\tst::startGiveawayCover,\n\t\tUi::Premium::TopBarDescriptor{\n\t\t\t.clickContextOther = nullptr,\n\t\t\t.title = tr::lng_giveaway_new_title(),\n\t\t\t.about = std::move(subtitleText),\n\t\t\t.light = true,\n\t\t\t.optimizeMinistars = false,\n\t\t});\n\tbar->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::BoxContentDivider>(\n\t\t\tbox.get(),\n\t\t\tst::giveawayGiftCodeTopHeight\n\t\t\t\t- st::boxTitleHeight\n\t\t\t\t+ st::boxDividerHeight\n\t\t\t\t+ st::defaultVerticalListSkip,\n\t\t\tst::defaultDividerBar,\n\t\t\tRectPart::Bottom),\n\t\tstyle::margins());\n\tbar->setPaused(true);\n\tbar->setRoundEdges(false);\n\tbar->setMaximumHeight(st::giveawayGiftCodeTopHeight);\n\tbar->setMinimumHeight(st::infoLayerTopBarHeight);\n\tbar->resize(bar->width(), bar->maximumHeight());\n\tbox->widthValue(\n\t) | rpl::on_next([=](int w) {\n\t\tbar->resizeToWidth(w - hPadding);\n\t\tbar->moveToLeft(st::boxRowPadding.left(), bar->y());\n\t}, box->lifetime());\n\n\tstd::move(\n\t\tshowFinished\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\tcloseTopBar->raise();\n\t\tif (const auto shadow = FindFirstShadowInBox(box)) {\n\t\t\tbar->stackUnder(shadow);\n\t\t}\n\t\tbar->setPaused(false);\n\t\tbox->scrolls(\n\t\t) | rpl::map(isCloseBarShown) | rpl::distinct_until_changed(\n\t\t) | rpl::on_next([=](bool showBar) {\n\t\t\tstate->animation.stop();\n\t\t\tstate->animation.start(\n\t\t\t\t[=] { closeTopBar->update(); },\n\t\t\t\tshowBar ? 0. : 1.,\n\t\t\t\tshowBar ? 1. : 0.,\n\t\t\t\tst::slideWrapDuration);\n\t\t}, box->lifetime());\n\t\tbox->scrolls(\n\t\t) | rpl::on_next([=] {\n\t\t\tbar->moveToLeft(bar->x(), -box->scrollTop());\n\t\t}, box->lifetime());\n\t}, box->lifetime());\n\n\tbar->show();\n}\n\n} // namespace\n\nvoid CreateGiveawayBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> reloadOnDone,\n\t\tstd::optional<Data::BoostPrepaidGiveaway> prepaid) {\n\tbox->setWidth(st::boxWideWidth);\n\n\tconst auto weakWindow = base::make_weak(navigation->parentController());\n\n\tusing GiveawayType = Giveaway::GiveawayTypeRow::Type;\n\tusing GiveawayGroup = Ui::RadioenumGroup<GiveawayType>;\n\tusing CreditsGroup = Ui::RadioenumGroup<int>;\n\tstruct State final {\n\t\tState(not_null<PeerData*> p) : apiOptions(p), apiCreditsOptions(p) {\n\t\t}\n\n\t\tApi::PremiumGiftCodeOptions apiOptions;\n\t\tApi::CreditsGiveawayOptions apiCreditsOptions;\n\t\trpl::lifetime lifetimeApi;\n\n\t\tstd::vector<not_null<PeerData*>> selectedToAward;\n\t\trpl::event_stream<> toAwardAmountChanged;\n\n\t\tstd::vector<not_null<PeerData*>> selectedToSubscribe;\n\n\t\trpl::variable<GiveawayType> typeValue;\n\t\trpl::variable<int> sliderValue;\n\t\trpl::variable<TimeId> dateValue;\n\t\trpl::variable<std::vector<QString>> countriesValue;\n\n\t\trpl::variable<QString> additionalPrize;\n\t\trpl::variable<int> chosenMonths;\n\t\trpl::variable<bool> showWinners;\n\n\t\trpl::variable<bool> confirmButtonBusy = true;\n\t};\n\tconst auto group = peer->isMegagroup();\n\tconst auto state = box->lifetime().make_state<State>(peer);\n\tconst auto typeGroup = std::make_shared<GiveawayGroup>();\n\tconst auto creditsGroup = std::make_shared<CreditsGroup>();\n\n\tconst auto isPrepaidCredits = (prepaid && prepaid->credits);\n\n\tconst auto isSpecificUsers = [=] {\n\t\treturn !state->selectedToAward.empty();\n\t};\n\tconst auto hideSpecificUsersOn = [=] {\n\t\treturn rpl::combine(\n\t\t\tstate->typeValue.value(),\n\t\t\tstate->toAwardAmountChanged.events_starting_with(\n\t\t\t\trpl::empty_value()) | rpl::type_erased\n\t\t) | rpl::map([=](GiveawayType type, auto) {\n\t\t\treturn (type == GiveawayType::Credits) || !isSpecificUsers();\n\t\t});\n\t};\n\n\tauto showFinished = Ui::BoxShowFinishes(box);\n\tAddPremiumTopBarWithDefaultTitleBar(\n\t\tbox,\n\t\trpl::duplicate(showFinished),\n\t\trpl::conditional(\n\t\t\thideSpecificUsersOn(),\n\t\t\ttr::lng_giveaway_start(),\n\t\t\ttr::lng_giveaway_award()),\n\t\trpl::conditional(\n\t\t\tisPrepaidCredits\n\t\t\t\t? rpl::single(true) | rpl::type_erased\n\t\t\t\t: state->typeValue.value() | rpl::map(\n\t\t\t\t\trpl::mappers::_1 == GiveawayType::Credits),\n\t\t\t(peer->isMegagroup()\n\t\t\t\t? tr::lng_giveaway_credits_new_about_group()\n\t\t\t\t: tr::lng_giveaway_credits_new_about()),\n\t\t\t(peer->isMegagroup()\n\t\t\t\t? tr::lng_giveaway_new_about_group()\n\t\t\t\t: tr::lng_giveaway_new_about())\n\t\t) | rpl::map(tr::rich));\n\t{\n\t\tconst auto &padding = st::giveawayGiftCodeCoverDividerPadding;\n\t\tUi::AddSkip(box->verticalLayout(), padding.bottom());\n\t}\n\n\tconst auto loading = box->addRow(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tbox,\n\t\t\tobject_ptr<Ui::VerticalLayout>(box)));\n\t{\n\t\tloading->toggle(true, anim::type::instant);\n\t\tconst auto container = loading->entity();\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\tcontainer->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_contacts_loading(),\n\t\t\t\tst::giveawayLoadingLabel),\n\t\t\tstyle::al_top);\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t}\n\tconst auto contentWrap = box->verticalLayout()->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tbox,\n\t\t\tobject_ptr<Ui::VerticalLayout>(box)));\n\tcontentWrap->toggle(false, anim::type::instant);\n\n\tif (prepaid) {\n\t\tcontentWrap->entity()->add(\n\t\t\tobject_ptr<Giveaway::GiveawayTypeRow>(\n\t\t\t\tbox,\n\t\t\t\tprepaid->credits\n\t\t\t\t\t? GiveawayType::PrepaidCredits\n\t\t\t\t\t: GiveawayType::Prepaid,\n\t\t\t\tprepaid->credits ? st::colorIndexOrange : prepaid->id,\n\t\t\t\ttr::lng_boosts_prepaid_giveaway_single(),\n\t\t\t\tprepaid->credits\n\t\t\t\t\t? tr::lng_boosts_prepaid_giveaway_credits_status(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single(prepaid->quantity) | tr::to_count(),\n\t\t\t\t\t\tlt_amount,\n\t\t\t\t\t\ttr::lng_prize_credits_amount(\n\t\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\t\trpl::single(prepaid->credits) | tr::to_count()))\n\t\t\t\t\t: tr::lng_boosts_prepaid_giveaway_status(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single(prepaid->quantity) | tr::to_count(),\n\t\t\t\t\t\tlt_duration,\n\t\t\t\t\t\ttr::lng_premium_gift_duration_months(\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\trpl::single(prepaid->months) | tr::to_count())),\n\t\t\t\tQImage())\n\t\t)->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t}\n\tif (!prepaid) {\n\t\tconst auto row = contentWrap->entity()->add(\n\t\t\tobject_ptr<Giveaway::GiveawayTypeRow>(\n\t\t\t\tbox,\n\t\t\t\tGiveawayType::Random,\n\t\t\t\tstate->toAwardAmountChanged.events_starting_with(\n\t\t\t\t\trpl::empty_value()\n\t\t\t\t) | rpl::map([=] {\n\t\t\t\t\tconst auto &selected = state->selectedToAward;\n\t\t\t\t\treturn selected.empty()\n\t\t\t\t\t\t? tr::lng_giveaway_create_subtitle()\n\t\t\t\t\t\t: (selected.size() == 1)\n\t\t\t\t\t\t? rpl::single(selected.front()->name())\n\t\t\t\t\t\t: tr::lng_giveaway_award_chosen(\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\trpl::single(selected.size()) | tr::to_count());\n\t\t\t\t}) | rpl::flatten_latest(),\n\t\t\t\tgroup));\n\t\trow->addRadio(typeGroup);\n\t\trow->setClickedCallback([=] {\n\t\t\tauto initBox = [=](not_null<PeerListBox*> peersBox) {\n\t\t\t\tpeersBox->setTitle(tr::lng_giveaway_award_option());\n\n\t\t\t\tauto aboveOwned = object_ptr<Ui::VerticalLayout>(peersBox);\n\t\t\t\tconst auto above = aboveOwned.data();\n\t\t\t\tpeersBox->peerListSetAboveWidget(std::move(aboveOwned));\n\t\t\t\tUi::AddSkip(above);\n\t\t\t\tconst auto buttonRandom = above->add(\n\t\t\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\t\t\tpeersBox,\n\t\t\t\t\t\ttr::lng_giveaway_random_button(),\n\t\t\t\t\t\tst::settingsButtonLightNoIcon));\n\t\t\t\tbuttonRandom->setClickedCallback([=] {\n\t\t\t\t\tstate->selectedToAward.clear();\n\t\t\t\t\tstate->toAwardAmountChanged.fire({});\n\t\t\t\t\tstate->typeValue.force_assign(GiveawayType::Random);\n\t\t\t\t\tpeersBox->closeBox();\n\t\t\t\t});\n\t\t\t\tUi::AddSkip(above);\n\n\t\t\t\tpeersBox->addButton(tr::lng_settings_save(), [=] {\n\t\t\t\t\tstate->selectedToAward = peersBox->collectSelectedRows();\n\t\t\t\t\tstate->toAwardAmountChanged.fire({});\n\t\t\t\t\tstate->typeValue.force_assign(GiveawayType::Random);\n\t\t\t\t\tpeersBox->closeBox();\n\t\t\t\t});\n\t\t\t\tpeersBox->addButton(tr::lng_cancel(), [=] {\n\t\t\t\t\tpeersBox->closeBox();\n\t\t\t\t});\n\t\t\t};\n\n\t\t\tusing Controller = Giveaway::AwardMembersListController;\n\t\t\tauto listController = std::make_unique<Controller>(\n\t\t\t\tnavigation,\n\t\t\t\tpeer,\n\t\t\t\tstate->selectedToAward);\n\t\t\tlistController->setCheckError(CreateErrorCallback(\n\t\t\t\tstate->apiOptions.giveawayAddPeersMax(),\n\t\t\t\ttr::lng_giveaway_maximum_users_error));\n\t\t\tbox->uiShow()->showBox(\n\t\t\t\tBox<PeerListBox>(\n\t\t\t\t\tstd::move(listController),\n\t\t\t\t\tstd::move(initBox)),\n\t\t\t\tUi::LayerOption::KeepOther);\n\t\t});\n\t}\n\tconst auto creditsOption = [=](int index) {\n\t\tconst auto options = state->apiCreditsOptions.options();\n\t\treturn (index >= 0 && index < options.size())\n\t\t\t? options[index]\n\t\t\t: Data::CreditsGiveawayOption();\n\t};\n\tconst auto creditsOptionWinners = [=](int index) {\n\t\tconst auto winners = creditsOption(index).winners;\n\t\treturn ranges::views::all(\n\t\t\twinners\n\t\t) | ranges::views::transform([](const auto &w) {\n\t\t\treturn w.users;\n\t\t}) | ranges::to_vector;\n\t};\n\tconst auto creditsTypeWrap = contentWrap->entity()->add(\n\t\tobject_ptr<Ui::VerticalLayout>(contentWrap->entity()));\n\tconst auto fillCreditsTypeWrap = [=] {\n\t\tif (state->apiCreditsOptions.options().empty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto row = creditsTypeWrap->add(\n\t\t\tobject_ptr<Giveaway::GiveawayTypeRow>(\n\t\t\t\tbox,\n\t\t\t\tGiveawayType::Credits,\n\t\t\t\tst::colorIndexOrange,\n\t\t\t\ttr::lng_credits_summary_title(),\n\t\t\t\ttr::lng_giveaway_create_subtitle(),\n\t\t\t\tQImage()));\n\t\trow->addRadio(typeGroup);\n\t\trow->setClickedCallback([=] {\n\t\t\tstate->typeValue.force_assign(GiveawayType::Credits);\n\t\t});\n\t};\n\n\t{\n\t\tconst auto &padding = st::giveawayGiftCodeTypeDividerPadding;\n\t\tUi::AddSkip(contentWrap->entity(), padding.top());\n\t\tUi::AddDivider(contentWrap->entity());\n\t\tUi::AddSkip(contentWrap->entity(), padding.bottom());\n\t}\n\n\tconst auto randomWrap = contentWrap->entity()->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontentWrap,\n\t\t\tobject_ptr<Ui::VerticalLayout>(box)));\n\tstate->typeValue.value(\n\t) | rpl::on_next([=](GiveawayType type) {\n\t\trandomWrap->toggle(!isSpecificUsers(), anim::type::instant);\n\t}, randomWrap->lifetime());\n\n\trandomWrap->toggleOn(hideSpecificUsersOn(), anim::type::instant);\n\n\tconst auto randomCreditsWrap = randomWrap->entity()->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontentWrap,\n\t\t\tobject_ptr<Ui::VerticalLayout>(box)));\n\trandomCreditsWrap->toggleOn(\n\t\tstate->typeValue.value(\n\t\t) | rpl::map(rpl::mappers::_1 == GiveawayType::Credits),\n\t\tanim::type::instant);\n\tconst auto fillCreditsOptions = [=] {\n\t\trandomCreditsWrap->entity()->clear();\n\n\t\tconst auto &st = st::giveawayTypeListItem;\n\t\tconst auto &stButton = st::defaultSettingsButton;\n\t\tconst auto &stStatus = st::defaultTextStyle;\n\t\tconst auto buttonInnerSkip = st.height - stButton.height;\n\t\tconst auto options = state->apiCreditsOptions.options();\n\t\tconst auto content = randomCreditsWrap->entity();\n\t\tconst auto title = Ui::AddSubsectionTitle(\n\t\t\tcontent,\n\t\t\ttr::lng_giveaway_credits_options_title());\n\n\t\tconst auto rightLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\tst::giveawayGiftCodeQuantitySubtitle);\n\t\trightLabel->show();\n\n\t\trpl::combine(\n\t\t\ttr::lng_giveaway_quantity(\n\t\t\t\tlt_count,\n\t\t\t\tcreditsGroup->value() | rpl::map([=](int i) -> float64 {\n\t\t\t\t\treturn creditsOption(i).yearlyBoosts;\n\t\t\t\t})),\n\t\t\ttitle->positionValue(),\n\t\t\tcontent->geometryValue()\n\t\t) | rpl::on_next([=](QString s, const QPoint &p, QRect) {\n\t\t\trightLabel->setText(std::move(s));\n\t\t\trightLabel->moveToRight(st::boxRowPadding.right(), p.y());\n\t\t}, rightLabel->lifetime());\n\n\t\tconst auto buttonHeight = st.height;\n\t\tconst auto minCredits = 0;\n\n\t\tstruct State final {\n\t\t\trpl::variable<bool> isExtended = false;\n\t\t};\n\t\tconst auto creditsState = content->lifetime().make_state<State>();\n\n\t\tfor (auto i = 0; i < options.size(); i++) {\n\t\t\tconst auto &option = options[i];\n\t\t\tif (option.credits < minCredits) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tstruct State final {\n\t\t\t\tstd::optional<Ui::Text::String> text;\n\t\t\t\tQString status;\n\t\t\t\tbool hasStatus = false;\n\t\t\t};\n\t\t\tconst auto buttonWrap = content->add(\n\t\t\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\t\t\tcontent,\n\t\t\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\t\t\tcontent,\n\t\t\t\t\t\trpl::never<QString>(),\n\t\t\t\t\t\tstButton)));\n\t\t\tconst auto button = buttonWrap->entity();\n\t\t\tbutton->setPaddingOverride({ 0, buttonInnerSkip, 0, 0 });\n\t\t\tconst auto buttonState = button->lifetime().make_state<State>();\n\t\t\tbuttonState->text.emplace(\n\t\t\t\tst.nameStyle,\n\t\t\t\ttr::lng_credits_summary_options_credits(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\toption.credits));\n\t\t\tbuttonState->status = tr::lng_giveaway_credits_option_status(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\toption.credits);\n\t\t\tconst auto price = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\tbutton,\n\t\t\t\tUi::FillAmountAndCurrency(option.amount, option.currency),\n\t\t\t\tst::creditsTopupPrice);\n\t\t\tconst auto inner = Ui::CreateChild<Ui::RpWidget>(button);\n\t\t\tconst auto stars = Ui::GenerateStars(\n\t\t\t\tst.nameStyle.font->height,\n\t\t\t\t(i + 1));\n\t\t\tconst auto textLeft = st.photoPosition.x()\n\t\t\t\t+ (st.nameStyle.font->spacew * 2)\n\t\t\t\t+ (stars.width() / style::DevicePixelRatio());\n\t\t\tstate->sliderValue.value(\n\t\t\t) | rpl::on_next([=](int users) {\n\t\t\t\tconst auto option = creditsOption(i);\n\t\t\t\tbuttonState->hasStatus = false;\n\t\t\t\tfor (const auto &winner : option.winners) {\n\t\t\t\t\tif (winner.users == users) {\n\t\t\t\t\t\tauto status = tr::lng_giveaway_credits_option_status(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\t\twinner.perUserStars);\n\t\t\t\t\t\tbuttonState->status = std::move(status);\n\t\t\t\t\t\tbuttonState->hasStatus = true;\n\t\t\t\t\t\tinner->update();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinner->update();\n\t\t\t}, button->lifetime());\n\t\t\tinner->paintRequest(\n\t\t\t) | rpl::on_next([=](const QRect &rect) {\n\t\t\t\tauto p = QPainter(inner);\n\t\t\t\tconst auto namey = buttonState->hasStatus\n\t\t\t\t\t? st.namePosition.y()\n\t\t\t\t\t: (buttonHeight - stStatus.font->height) / 2;\n\t\t\t\tp.drawImage(st.photoPosition.x(), namey, stars);\n\t\t\t\tp.setPen(st.nameFg);\n\t\t\t\tbuttonState->text->draw(p, {\n\t\t\t\t\t.position = QPoint(textLeft, namey),\n\t\t\t\t\t.availableWidth = inner->width() - textLeft,\n\t\t\t\t\t.elisionLines = 1,\n\t\t\t\t});\n\t\t\t\tif (buttonState->hasStatus) {\n\t\t\t\t\tp.setFont(stStatus.font);\n\t\t\t\t\tp.setPen(st.statusFg);\n\t\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t\t\tp.drawText(\n\t\t\t\t\t\tst.photoPosition.x(),\n\t\t\t\t\t\tst.statusPosition.y() + stStatus.font->ascent,\n\t\t\t\t\t\tbuttonState->status);\n\t\t\t\t}\n\t\t\t}, inner->lifetime());\n\t\t\tbutton->widthValue(\n\t\t\t) | rpl::on_next([=](int width) {\n\t\t\t\tprice->moveToRight(\n\t\t\t\t\tst::boxRowPadding.right(),\n\t\t\t\t\t(buttonHeight - price->height()) / 2);\n\t\t\t\tinner->moveToLeft(0, 0);\n\t\t\t\tinner->resize(\n\t\t\t\t\twidth\n\t\t\t\t\t\t- price->width()\n\t\t\t\t\t\t- st::boxRowPadding.right()\n\t\t\t\t\t\t- st::boxRowPadding.left() / 2,\n\t\t\t\t\tbuttonHeight);\n\t\t\t}, button->lifetime());\n\n\t\t\t{\n\t\t\t\tconst auto &st = st::defaultCheckbox;\n\t\t\t\tconst auto radio = Ui::CreateChild<Ui::Radioenum<int>>(\n\t\t\t\t\tbutton,\n\t\t\t\t\tcreditsGroup,\n\t\t\t\t\ti,\n\t\t\t\t\tQString(),\n\t\t\t\t\tst);\n\t\t\t\tradio->moveToLeft(\n\t\t\t\t\tst::boxRowPadding.left(),\n\t\t\t\t\t(buttonHeight - radio->checkRect().height()) / 2);\n\t\t\t\tradio->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\t\tradio->show();\n\t\t\t}\n\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\tcreditsGroup->setValue(i);\n\t\t\t});\n\t\t\tif (option.isDefault) {\n\t\t\t\tcreditsGroup->setValue(i);\n\t\t\t}\n\t\t\tbuttonWrap->toggle(\n\t\t\t\t(!option.isExtended) || option.isDefault,\n\t\t\t\tanim::type::instant);\n\t\t\tif (option.isExtended) {\n\t\t\t\tbuttonWrap->toggleOn(creditsState->isExtended.value());\n\t\t\t}\n\t\t\tUi::ToggleChildrenVisibility(button, true);\n\t\t}\n\n\t\t{\n\t\t\tUi::AddSkip(content, st::settingsButton.padding.top());\n\t\t\tconst auto showMoreWrap = Info::Statistics::AddShowMoreButton(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_stories_show_more());\n\t\t\tshowMoreWrap->toggle(true, anim::type::instant);\n\n\t\t\tshowMoreWrap->entity()->setClickedCallback([=] {\n\t\t\t\tshowMoreWrap->toggle(false, anim::type::instant);\n\t\t\t\tcreditsState->isExtended = true;\n\t\t\t});\n\t\t}\n\n\t\tUi::AddSkip(content);\n\t\tUi::AddDividerText(content, tr::lng_giveaway_credits_options_about());\n\t\tUi::AddSkip(content);\n\t};\n\n\tconst auto sliderContainerWrap = randomWrap->entity()->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\trandomWrap,\n\t\t\tobject_ptr<Ui::VerticalLayout>(randomWrap)));\n\tconst auto sliderContainer = sliderContainerWrap->entity();\n\tsliderContainerWrap->toggle(true, anim::type::instant);\n\tconst auto fillSliderContainer = [=] {\n\t\tconst auto availablePresets = state->apiOptions.availablePresets();\n\t\tconst auto creditsOptions = state->apiCreditsOptions.options();\n\t\tif (prepaid) {\n\t\t\tstate->sliderValue = prepaid->quantity;\n\t\t\treturn;\n\t\t}\n\t\tif (availablePresets.empty()\n\t\t\t&& (creditsOptions.empty()\n\t\t\t\t|| creditsOptions.front().winners.empty())) {\n\t\t\treturn;\n\t\t}\n\t\tstate->sliderValue = availablePresets.empty()\n\t\t\t? creditsOptions.front().winners.front().users\n\t\t\t: availablePresets.front();\n\t\tauto creditsValueType = typeGroup->value(\n\t\t\t) | rpl::map(rpl::mappers::_1 == GiveawayType::Credits);\n\t\tconst auto title = Ui::AddSubsectionTitle(\n\t\t\tsliderContainer,\n\t\t\trpl::conditional(\n\t\t\t\trpl::duplicate(creditsValueType),\n\t\t\t\ttr::lng_giveaway_credits_quantity_title(),\n\t\t\t\ttr::lng_giveaway_quantity_title()));\n\t\tconst auto rightLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tsliderContainer,\n\t\t\tst::giveawayGiftCodeQuantitySubtitle);\n\t\trightLabel->show();\n\t\trpl::duplicate(\n\t\t\tcreditsValueType\n\t\t) | rpl::on_next([=](bool isCredits) {\n\t\t\trightLabel->setVisible(!isCredits);\n\t\t}, rightLabel->lifetime());\n\n\t\tconst auto floatLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tsliderContainer,\n\t\t\tst::giveawayGiftCodeQuantityFloat);\n\t\tfloatLabel->show();\n\n\t\trpl::combine(\n\t\t\ttr::lng_giveaway_quantity(\n\t\t\t\tlt_count,\n\t\t\t\tstate->sliderValue.value(\n\t\t\t\t) | rpl::map([=](int v) -> float64 {\n\t\t\t\t\treturn state->apiOptions.giveawayBoostsPerPremium() * v;\n\t\t\t\t})),\n\t\t\ttitle->positionValue(),\n\t\t\tsliderContainer->geometryValue()\n\t\t) | rpl::on_next([=](QString s, const QPoint &p, QRect) {\n\t\t\trightLabel->setText(std::move(s));\n\t\t\trightLabel->moveToRight(st::boxRowPadding.right(), p.y());\n\t\t}, rightLabel->lifetime());\n\n\t\tconst auto &padding = st::giveawayGiftCodeSliderPadding;\n\t\tUi::AddSkip(sliderContainer, padding.top());\n\n\t\tconst auto sliderParent = sliderContainer->add(\n\t\t\tobject_ptr<Ui::VerticalLayout>(sliderContainer),\n\t\t\tst::boxRowPadding);\n\t\tstruct State final {\n\t\t\tUi::MediaSliderWheelless *slider = nullptr;\n\t\t};\n\t\tconst auto sliderState = sliderParent->lifetime().make_state<State>();\n\t\tUi::AddSkip(sliderContainer, padding.bottom());\n\t\trpl::combine(\n\t\t\trpl::duplicate(creditsValueType),\n\t\t\tcreditsGroup->value()\n\t\t) | rpl::on_next([=](bool isCredits, int value) {\n\t\t\twhile (sliderParent->count()) {\n\t\t\t\tdelete sliderParent->widgetAt(0);\n\t\t\t}\n\t\t\tsliderState->slider = sliderParent->add(\n\t\t\t\tobject_ptr<Ui::MediaSliderWheelless>(\n\t\t\t\t\tsliderContainer,\n\t\t\t\t\tst::settingsScale));\n\t\t\tsliderState->slider->resize(\n\t\t\t\tsliderState->slider->width(),\n\t\t\t\tst::settingsScale.seekSize.height());\n\t\t\tconst auto &values = isCredits\n\t\t\t\t? creditsOptionWinners(value)\n\t\t\t\t: availablePresets;\n\t\t\tconst auto resultValue = [&] {\n\t\t\t\tconst auto sliderValue = state->sliderValue.current();\n\t\t\t\treturn ranges::contains(values, sliderValue)\n\t\t\t\t\t? sliderValue\n\t\t\t\t\t: values.front();\n\t\t\t}();\n\t\t\tstate->sliderValue.force_assign(resultValue);\n\t\t\tif (values.size() <= 1) {\n\t\t\t\tsliderContainerWrap->toggle(false, anim::type::instant);\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tsliderContainerWrap->toggle(true, anim::type::instant);\n\t\t\t}\n\t\t\tsliderState->slider->setPseudoDiscrete(\n\t\t\t\tvalues.size(),\n\t\t\t\t[=](int index) { return values[index]; },\n\t\t\t\tresultValue,\n\t\t\t\t[=](int boosts) { state->sliderValue = boosts; },\n\t\t\t\t[](int) {});\n\t\t}, sliderParent->lifetime());\n\n\t\trpl::combine(\n\t\t\trpl::duplicate(creditsValueType),\n\t\t\tcreditsGroup->value(),\n\t\t\tstate->sliderValue.value()\n\t\t) | rpl::on_next([=](\n\t\t\t\tbool isCredits,\n\t\t\t\tint credits,\n\t\t\t\tint boosts) {\n\t\t\tfloatLabel->setText(QString::number(boosts));\n\n\t\t\tif (!sliderState->slider) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto &values = isCredits\n\t\t\t\t? creditsOptionWinners(credits)\n\t\t\t\t: availablePresets;\n\t\t\tconst auto count = values.size();\n\t\t\tif (count <= 1) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto sliderWidth = sliderState->slider->width()\n\t\t\t\t- st::settingsScale.seekSize.width();\n\t\t\tfor (auto i = 0; i < count; i++) {\n\t\t\t\tif ((i + 1 == count || values[i + 1] > boosts)\n\t\t\t\t\t&& values[i] <= boosts) {\n\t\t\t\t\tconst auto x = (sliderWidth * i) / (count - 1);\n\t\t\t\t\tconst auto mapped = sliderState->slider->mapTo(\n\t\t\t\t\t\tsliderContainer,\n\t\t\t\t\t\tsliderState->slider->pos());\n\t\t\t\t\tfloatLabel->moveToLeft(\n\t\t\t\t\t\tmapped.x()\n\t\t\t\t\t\t\t+ x\n\t\t\t\t\t\t\t+ st::settingsScale.seekSize.width() / 2\n\t\t\t\t\t\t\t- floatLabel->width() / 2,\n\t\t\t\t\t\tmapped.y()\n\t\t\t\t\t\t\t- floatLabel->height()\n\t\t\t\t\t\t\t- st::giveawayGiftCodeSliderFloatSkip);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}, floatLabel->lifetime());\n\n\t\tUi::AddSkip(sliderContainer);\n\t\tUi::AddDividerText(\n\t\t\tsliderContainer,\n\t\t\trpl::conditional(\n\t\t\t\trpl::duplicate(creditsValueType),\n\t\t\t\ttr::lng_giveaway_credits_quantity_about(),\n\t\t\t\ttr::lng_giveaway_quantity_about()));\n\t\tUi::AddSkip(sliderContainer);\n\n\t\tsliderContainer->resizeToWidth(box->width());\n\t};\n\n\t{\n\t\tconst auto channelsContainer = randomWrap->entity()->add(\n\t\t\tobject_ptr<Ui::VerticalLayout>(randomWrap));\n\t\tUi::AddSubsectionTitle(\n\t\t\tchannelsContainer,\n\t\t\ttr::lng_giveaway_channels_title(),\n\t\t\tst::giveawayGiftCodeChannelsSubsectionPadding);\n\n\t\tstruct ListState final {\n\t\t\tListState(not_null<PeerData*> p) : controller(p) {\n\t\t\t}\n\t\t\tPeerListContentDelegateSimple delegate;\n\t\t\tGiveaway::SelectedChannelsListController controller;\n\t\t};\n\t\tconst auto listState = box->lifetime().make_state<ListState>(peer);\n\n\t\tlistState->delegate.setContent(channelsContainer->add(\n\t\t\tobject_ptr<PeerListContent>(\n\t\t\t\tchannelsContainer,\n\t\t\t\t&listState->controller)));\n\t\tlistState->controller.setDelegate(&listState->delegate);\n\t\tlistState->controller.channelRemoved(\n\t\t) | rpl::on_next([=](not_null<PeerData*> peer) {\n\t\t\tauto &list = state->selectedToSubscribe;\n\t\t\tlist.erase(ranges::remove(list, peer), end(list));\n\t\t}, box->lifetime());\n\t\tlistState->controller.setTopStatus((peer->isMegagroup()\n\t\t\t? tr::lng_giveaway_channels_this_group\n\t\t\t: tr::lng_giveaway_channels_this)(\n\t\t\t\tlt_count,\n\t\t\t\tstate->sliderValue.value(\n\t\t\t\t) | rpl::map([=](int v) -> float64 {\n\t\t\t\t\treturn (prepaid && prepaid->boosts)\n\t\t\t\t\t\t? prepaid->boosts\n\t\t\t\t\t\t: (state->apiOptions.giveawayBoostsPerPremium() * v);\n\t\t\t\t})));\n\n\t\tusing IconType = Settings::IconType;\n\t\tSettings::AddButtonWithIcon(\n\t\t\tchannelsContainer,\n\t\t\ttr::lng_giveaway_channels_add(),\n\t\t\tst::giveawayGiftCodeChannelsAddButton,\n\t\t\t{ &st::settingsIconAdd, IconType::Round, &st::windowBgActive }\n\t\t)->setClickedCallback([=] {\n\t\t\tauto initBox = [=](not_null<PeerListBox*> peersBox) {\n\t\t\t\tpeersBox->setTitle(tr::lng_giveaway_channels_add());\n\t\t\t\tpeersBox->addButton(tr::lng_settings_save(), [=] {\n\t\t\t\t\tconst auto selected = peersBox->collectSelectedRows();\n\t\t\t\t\tstate->selectedToSubscribe = selected;\n\t\t\t\t\tlistState->controller.rebuild(selected);\n\t\t\t\t\tpeersBox->closeBox();\n\t\t\t\t});\n\t\t\t\tpeersBox->addButton(tr::lng_cancel(), [=] {\n\t\t\t\t\tpeersBox->closeBox();\n\t\t\t\t});\n\t\t\t};\n\n\t\t\tusing Controller = Giveaway::MyChannelsListController;\n\t\t\tauto controller = std::make_unique<Controller>(\n\t\t\t\tpeer,\n\t\t\t\tbox->uiShow(),\n\t\t\t\tstate->selectedToSubscribe);\n\t\t\tcontroller->setCheckError(CreateErrorCallback(\n\t\t\t\tstate->apiOptions.giveawayAddPeersMax(),\n\t\t\t\ttr::lng_giveaway_maximum_channels_error));\n\t\t\tbox->uiShow()->showBox(\n\t\t\t\tBox<PeerListBox>(std::move(controller), std::move(initBox)),\n\t\t\t\tUi::LayerOption::KeepOther);\n\t\t});\n\n\t\tconst auto &padding = st::giveawayGiftCodeChannelsDividerPadding;\n\t\tUi::AddSkip(channelsContainer, padding.top());\n\t\tUi::AddDividerText(\n\t\t\tchannelsContainer,\n\t\t\ttr::lng_giveaway_channels_about());\n\t\tUi::AddSkip(channelsContainer, padding.bottom());\n\t}\n\n\tconst auto membersGroup = std::make_shared<GiveawayGroup>();\n\t{\n\t\tconst auto countriesContainer = randomWrap->entity()->add(\n\t\t\tobject_ptr<Ui::VerticalLayout>(randomWrap));\n\t\tUi::AddSubsectionTitle(\n\t\t\tcountriesContainer,\n\t\t\ttr::lng_giveaway_users_title());\n\n\t\tmembersGroup->setValue(GiveawayType::AllMembers);\n\t\tauto subtitle = state->countriesValue.value(\n\t\t) | rpl::map([=](const std::vector<QString> &list) {\n\t\t\treturn list.empty()\n\t\t\t\t? tr::lng_giveaway_users_from_all_countries()\n\t\t\t\t: (list.size() == 1)\n\t\t\t\t? tr::lng_giveaway_users_from_one_country(\n\t\t\t\t\tlt_country,\n\t\t\t\t\trpl::single(Countries::Instance().countryNameByISO2(\n\t\t\t\t\t\tlist.front())))\n\t\t\t\t: tr::lng_giveaway_users_from_countries(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(list.size()) | tr::to_count());\n\t\t}) | rpl::flatten_latest();\n\n\t\tconst auto showBox = [=] {\n\t\t\tauto done = [=](std::vector<QString> list) {\n\t\t\t\tstate->countriesValue = std::move(list);\n\t\t\t};\n\t\t\tauto error = CreateErrorCallback(\n\t\t\t\tstate->apiOptions.giveawayCountriesMax(),\n\t\t\t\ttr::lng_giveaway_maximum_countries_error);\n\t\t\tbox->uiShow()->showBox(Box(\n\t\t\t\tUi::SelectCountriesBox,\n\t\t\t\tstate->countriesValue.current(),\n\t\t\t\tstd::move(done),\n\t\t\t\tstd::move(error)));\n\t\t};\n\n\t\tconst auto createCallback = [=](GiveawayType type) {\n\t\t\treturn [=] {\n\t\t\t\tconst auto was = membersGroup->current();\n\t\t\t\tmembersGroup->setValue(type);\n\t\t\t\tconst auto now = membersGroup->current();\n\t\t\t\tif (was == now) {\n\t\t\t\t\tbase::call_delayed(\n\t\t\t\t\t\tst::defaultRippleAnimation.hideDuration,\n\t\t\t\t\t\tbox,\n\t\t\t\t\t\tshowBox);\n\t\t\t\t}\n\t\t\t};\n\t\t};\n\n\t\t{\n\t\t\tconst auto row = countriesContainer->add(\n\t\t\t\tobject_ptr<Giveaway::GiveawayTypeRow>(\n\t\t\t\t\tbox,\n\t\t\t\t\tGiveawayType::AllMembers,\n\t\t\t\t\trpl::duplicate(subtitle),\n\t\t\t\t\tgroup));\n\t\t\trow->addRadio(membersGroup);\n\t\t\trow->setClickedCallback(createCallback(GiveawayType::AllMembers));\n\t\t}\n\t\tconst auto row = countriesContainer->add(\n\t\t\tobject_ptr<Giveaway::GiveawayTypeRow>(\n\t\t\t\tbox,\n\t\t\t\tGiveawayType::OnlyNewMembers,\n\t\t\t\tstd::move(subtitle),\n\t\t\t\tgroup));\n\t\trow->addRadio(membersGroup);\n\t\trow->setClickedCallback(createCallback(GiveawayType::OnlyNewMembers));\n\n\t\tUi::AddSkip(countriesContainer);\n\t\tUi::AddDividerText(\n\t\t\tcountriesContainer,\n\t\t\t(group\n\t\t\t\t? tr::lng_giveaway_users_about_group()\n\t\t\t\t: tr::lng_giveaway_users_about()));\n\t\tUi::AddSkip(countriesContainer);\n\t}\n\n\tconst auto addTerms = [=](not_null<Ui::VerticalLayout*> c) {\n\t\tauto terms = object_ptr<Ui::FlatLabel>(\n\t\t\tc,\n\t\t\ttr::lng_premium_gift_terms(\n\t\t\t\tlt_link,\n\t\t\t\ttr::lng_premium_gift_terms_link(\n\t\t\t\t) | rpl::map([](const QString &t) {\n\t\t\t\t\treturn tr::link(t, 1);\n\t\t\t\t}),\n\t\t\t\ttr::marked),\n\t\t\tst::boxDividerLabel);\n\t\tterms->setLink(1, std::make_shared<LambdaClickHandler>([=] {\n\t\t\tbox->closeBox();\n\t\t\tSettings::ShowPremium(&peer->session(), QString());\n\t\t}));\n\t\tc->add(std::move(terms));\n\t};\n\n\tconst auto durationGroup = std::make_shared<Ui::RadiobuttonGroup>(0);\n\tdurationGroup->setChangedCallback([=](int value) {\n\t\tstate->chosenMonths = state->apiOptions.monthsFromPreset(value);\n\t});\n\tconst auto listOptionsRandom = randomWrap->entity()->add(\n\t\tobject_ptr<Ui::VerticalLayout>(box));\n\tconst auto listOptionsSpecific = contentWrap->entity()->add(\n\t\tobject_ptr<Ui::VerticalLayout>(box));\n\tconst auto rebuildListOptions = [=](GiveawayType type, int usersCount) {\n\t\tif (prepaid) {\n\t\t\treturn;\n\t\t}\n\t\twhile (listOptionsRandom->count()) {\n\t\t\tdelete listOptionsRandom->widgetAt(0);\n\t\t}\n\t\twhile (listOptionsSpecific->count()) {\n\t\t\tdelete listOptionsSpecific->widgetAt(0);\n\t\t}\n\t\tconst auto listOptions = isSpecificUsers()\n\t\t\t? listOptionsSpecific\n\t\t\t: listOptionsRandom;\n\t\tif (type != GiveawayType::Credits) {\n\t\t\tUi::AddSubsectionTitle(\n\t\t\t\tlistOptions,\n\t\t\t\ttr::lng_giveaway_duration_title(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(usersCount) | tr::to_count()),\n\t\t\t\tst::giveawayGiftCodeChannelsSubsectionPadding);\n\t\t\tUi::Premium::AddGiftOptions(\n\t\t\t\tlistOptions,\n\t\t\t\tdurationGroup,\n\t\t\t\tstate->apiOptions.optionsForGiveaway(usersCount),\n\t\t\t\tst::giveawayGiftCodeGiftOption,\n\t\t\t\ttrue);\n\n\t\t\tUi::AddSkip(listOptions);\n\n\t\t\tauto termsContainer = object_ptr<Ui::VerticalLayout>(listOptions);\n\t\t\taddTerms(termsContainer.data());\n\t\t\tlistOptions->add(object_ptr<Ui::DividerLabel>(\n\t\t\t\tlistOptions,\n\t\t\t\tstd::move(termsContainer),\n\t\t\t\tst::defaultBoxDividerLabelPadding));\n\n\t\t\tUi::AddSkip(listOptions);\n\t\t}\n\n\t\tbox->verticalLayout()->resizeToWidth(box->width());\n\t};\n\tif (!prepaid) {\n\t\trpl::combine(\n\t\t\tstate->sliderValue.value(),\n\t\t\tstate->typeValue.value()\n\t\t) | rpl::on_next([=](int users, GiveawayType type) {\n\t\t\ttypeGroup->setValue(type);\n\t\t\trebuildListOptions(\n\t\t\t\ttype,\n\t\t\t\tisSpecificUsers() ? state->selectedToAward.size() : users);\n\t\t}, box->lifetime());\n\t} else {\n\t\ttypeGroup->setValue(GiveawayType::Random);\n\t}\n\n\t{\n\t\tconst auto additionalWrap = randomWrap->entity()->add(\n\t\t\tobject_ptr<Ui::VerticalLayout>(randomWrap));\n\t\tconst auto additionalToggle = additionalWrap->add(\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tadditionalWrap,\n\t\t\t\ttr::lng_giveaway_additional_prizes(),\n\t\t\t\tst::defaultSettingsButton));\n\t\tconst auto additionalInner = additionalWrap->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::InputField>>(\n\t\t\t\tadditionalWrap,\n\t\t\t\tobject_ptr<Ui::InputField>(\n\t\t\t\t\tadditionalWrap,\n\t\t\t\t\tst::giveawayGiftCodeAdditionalField,\n\t\t\t\t\tUi::InputField::Mode::SingleLine,\n\t\t\t\t\ttr::lng_giveaway_additional_prizes_ph()),\n\t\t\t\tst::giveawayGiftCodeAdditionalPaddingMin));\n\t\tconst auto additionalPadded = additionalInner->wrapped();\n\t\tconst auto additional = additionalInner->entity();\n\t\tadditionalInner->hide(anim::type::instant);\n\t\tadditional->setMaxLength(kAdditionalPrizeLengthMax);\n\t\tconst auto fillAdditionalPrizeValue = [=] {\n\t\t\tstate->additionalPrize = additional->getLastText().trimmed();\n\t\t};\n\t\tadditionalToggle->toggleOn(rpl::single(false))->toggledChanges(\n\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\tif (!toggled && Ui::InFocusChain(additional)) {\n\t\t\t\tadditionalWrap->setFocus();\n\t\t\t\tstate->additionalPrize = QString();\n\t\t\t}\n\t\t\tadditionalInner->toggle(toggled, anim::type::normal);\n\t\t\tif (toggled) {\n\t\t\t\tadditional->setFocusFast();\n\t\t\t\tfillAdditionalPrizeValue();\n\t\t\t}\n\t\t}, additionalInner->lifetime());\n\t\tadditionalInner->finishAnimating();\n\n\t\tadditional->changes() | rpl::filter([=] {\n\t\t\treturn additionalInner->toggled();\n\t\t}) | rpl::on_next(\n\t\t\tfillAdditionalPrizeValue,\n\t\t\tadditional->lifetime());\n\n\t\tUi::AddSkip(additionalWrap);\n\n\t\tauto monthsValue = prepaid\n\t\t\t? (rpl::single(prepaid->months) | rpl::type_erased)\n\t\t\t: state->chosenMonths.value();\n\t\tconst auto usersCountByType = [=](GiveawayType type) {\n\t\t\tif (!isSpecificUsers()) {\n\t\t\t\treturn state->sliderValue.value() | rpl::type_erased;\n\t\t\t}\n\t\t\treturn state->toAwardAmountChanged.events_starting_with_copy(\n\t\t\t\trpl::empty\n\t\t\t) | rpl::map([=] {\n\t\t\t\treturn int(state->selectedToAward.size());\n\t\t\t}) | rpl::type_erased;\n\t\t};\n\t\tauto usersCountValue = prepaid\n\t\t\t? (rpl::single(prepaid->quantity) | rpl::type_erased)\n\t\t\t: state->typeValue.value(\n\t\t\t) | rpl::map(usersCountByType) | rpl::flatten_latest();\n\n\t\tconst auto additionalLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tadditionalInner,\n\t\t\trpl::duplicate(usersCountValue) | rpl::map([](int count) {\n\t\t\t\treturn QString::number(count);\n\t\t\t}),\n\t\t\tst::giveawayGiftCodeAdditionalLabel);\n\t\tadditionalLabel->widthValue() | rpl::on_next([=](int width) {\n\t\t\tconst auto min = st::giveawayGiftCodeAdditionalPaddingMin;\n\t\t\tconst auto skip = st::giveawayGiftCodeAdditionalLabelSkip;\n\t\t\tconst auto added = std::max(width + skip - min.left(), 0);\n\t\t\tconst auto &field = st::giveawayGiftCodeAdditionalField;\n\t\t\tconst auto top = field.textMargins.top();\n\t\t\tadditionalLabel->moveToLeft(min.right(), min.top() + top);\n\t\t\tadditionalPadded->setPadding(min + QMargins(added, 0, 0, 0));\n\t\t}, additionalLabel->lifetime());\n\n\t\tauto additionalAbout = rpl::combine(\n\t\t\tstate->additionalPrize.value(),\n\t\t\tstd::move(monthsValue),\n\t\t\tstd::move(usersCountValue)\n\t\t) | rpl::map([=](QString prize, int months, int users) {\n\t\t\tconst auto duration = ((months >= 12)\n\t\t\t\t? tr::lng_premium_gift_duration_years\n\t\t\t\t: tr::lng_premium_gift_duration_months)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\t(months >= 12) ? (months / 12) : months);\n\t\t\tif (prize.isEmpty()) {\n\t\t\t\treturn tr::lng_giveaway_prizes_just_premium(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tusers,\n\t\t\t\t\tlt_duration,\n\t\t\t\t\tTextWithEntities{ duration },\n\t\t\t\t\ttr::rich);\n\t\t\t}\n\t\t\treturn tr::lng_giveaway_prizes_additional(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tusers,\n\t\t\t\tlt_prize,\n\t\t\t\tTextWithEntities{ prize },\n\t\t\t\tlt_duration,\n\t\t\t\tTextWithEntities{ duration },\n\t\t\t\ttr::rich);\n\t\t});\n\t\tauto creditsAdditionalAbout = rpl::combine(\n\t\t\tstate->additionalPrize.value(),\n\t\t\tstate->sliderValue.value(),\n\t\t\tcreditsGroup->value()\n\t\t) | rpl::map([=](QString prize, int users, int creditsIndex) {\n\t\t\tconst auto credits = creditsOption(creditsIndex).credits;\n\t\t\treturn prize.isEmpty()\n\t\t\t\t? tr::lng_giveaway_prizes_just_credits(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tcredits,\n\t\t\t\t\ttr::rich)\n\t\t\t\t: tr::lng_giveaway_prizes_additional_credits(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tusers,\n\t\t\t\t\tlt_prize,\n\t\t\t\t\tTextWithEntities{ prize },\n\t\t\t\t\tlt_amount,\n\t\t\t\t\ttr::lng_giveaway_prizes_additional_credits_amount(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tcredits,\n\t\t\t\t\t\ttr::rich),\n\t\t\t\t\ttr::rich);\n\t\t});\n\n\t\tauto creditsValueType = typeGroup->value(\n\t\t) | rpl::map(rpl::mappers::_1 == GiveawayType::Credits);\n\n\t\tUi::AddDividerText(\n\t\t\tadditionalWrap,\n\t\t\trpl::conditional(\n\t\t\t\tadditionalToggle->toggledValue(),\n\t\t\t\trpl::conditional(\n\t\t\t\t\trpl::duplicate(creditsValueType),\n\t\t\t\t\tstd::move(creditsAdditionalAbout),\n\t\t\t\t\tstd::move(additionalAbout)),\n\t\t\t\trpl::conditional(\n\t\t\t\t\trpl::duplicate(creditsValueType),\n\t\t\t\t\ttr::lng_giveaway_additional_credits_about(tr::marked),\n\t\t\t\t\ttr::lng_giveaway_additional_about(tr::marked))));\n\t\tUi::AddSkip(additionalWrap);\n\t}\n\n\t{\n\t\tconst auto dateContainer = randomWrap->entity()->add(\n\t\t\tobject_ptr<Ui::VerticalLayout>(randomWrap));\n\t\tUi::AddSubsectionTitle(\n\t\t\tdateContainer,\n\t\t\ttr::lng_giveaway_date_title(),\n\t\t\tst::giveawayGiftCodeChannelsSubsectionPadding);\n\n\t\tstate->dateValue = ThreeDaysAfterToday().toSecsSinceEpoch();\n\t\tconst auto button = Settings::AddButtonWithLabel(\n\t\t\tdateContainer,\n\t\t\ttr::lng_giveaway_date(),\n\t\t\tstate->dateValue.value() | rpl::map(\n\t\t\t\tbase::unixtime::parse\n\t\t\t) | rpl::map(Ui::FormatDateTime),\n\t\t\tst::defaultSettingsButton);\n\n\t\tbutton->setClickedCallback([=] {\n\t\t\tbox->uiShow()->showBox(Box([=](not_null<Ui::GenericBox*> b) {\n\t\t\t\tUi::ChooseDateTimeBox(b, {\n\t\t\t\t\t.title = tr::lng_giveaway_date_select(),\n\t\t\t\t\t.submit = tr::lng_settings_save(),\n\t\t\t\t\t.done = [=](TimeId time) {\n\t\t\t\t\t\tstate->dateValue = time;\n\t\t\t\t\t\tb->closeBox();\n\t\t\t\t\t},\n\t\t\t\t\t.min = QDateTime::currentSecsSinceEpoch,\n\t\t\t\t\t.time = state->dateValue.current(),\n\t\t\t\t\t.max = [=] {\n\t\t\t\t\t\treturn QDateTime::currentSecsSinceEpoch()\n\t\t\t\t\t\t\t+ state->apiOptions.giveawayPeriodMax();\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}));\n\t\t});\n\n\t\tUi::AddSkip(dateContainer);\n\t\tif (prepaid) {\n\t\t\tauto terms = object_ptr<Ui::VerticalLayout>(dateContainer);\n\t\t\tterms->add(object_ptr<Ui::FlatLabel>(\n\t\t\t\tterms,\n\t\t\t\t(group\n\t\t\t\t\t? tr::lng_giveaway_date_about_group\n\t\t\t\t\t: tr::lng_giveaway_date_about)(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tstate->sliderValue.value() | tr::to_count()),\n\t\t\t\tst::boxDividerLabel));\n\t\t\tUi::AddSkip(terms.data());\n\t\t\tUi::AddSkip(terms.data());\n\t\t\taddTerms(terms.data());\n\t\t\tdateContainer->add(object_ptr<Ui::DividerLabel>(\n\t\t\t\tdateContainer,\n\t\t\t\tstd::move(terms),\n\t\t\t\tst::defaultBoxDividerLabelPadding));\n\t\t\tUi::AddSkip(dateContainer);\n\t\t} else {\n\t\t\tUi::AddDividerText(\n\t\t\t\tdateContainer,\n\t\t\t\t(group\n\t\t\t\t\t? tr::lng_giveaway_date_about_group\n\t\t\t\t\t: tr::lng_giveaway_date_about)(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tstate->sliderValue.value() | tr::to_count()));\n\t\t\tUi::AddSkip(dateContainer);\n\t\t}\n\t}\n\n\t{\n\t\tconst auto winnersWrap = randomWrap->entity()->add(\n\t\t\tobject_ptr<Ui::VerticalLayout>(randomWrap));\n\t\tconst auto winnersToggle = winnersWrap->add(\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\twinnersWrap,\n\t\t\t\ttr::lng_giveaway_show_winners(),\n\t\t\t\tst::defaultSettingsButton));\n\t\tstate->showWinners = winnersToggle->toggleOn(\n\t\t\trpl::single(false)\n\t\t)->toggledValue();\n\t\tUi::AddSkip(winnersWrap);\n\n\t\tUi::AddDividerText(\n\t\t\twinnersWrap,\n\t\t\ttr::lng_giveaway_show_winners_about());\n\t}\n\n\t{\n\t\tusing namespace Info::Statistics;\n\t\tconst auto &stButton = st::startGiveawayBox;\n\t\tbox->setStyle(stButton);\n\t\tauto button = object_ptr<Ui::RoundButton>(\n\t\t\tbox,\n\t\t\trpl::never<QString>(),\n\t\t\tst::giveawayGiftCodeStartButton);\n\n\t\tAddLabelWithBadgeToButton(\n\t\t\tbutton,\n\t\t\trpl::conditional(\n\t\t\t\thideSpecificUsersOn(),\n\t\t\t\ttr::lng_giveaway_start(),\n\t\t\t\ttr::lng_giveaway_award()),\n\t\t\t(prepaid && prepaid->boosts)\n\t\t\t\t? rpl::single(prepaid->boosts) | rpl::type_erased\n\t\t\t\t: rpl::conditional(\n\t\t\t\t\tstate->typeValue.value(\n\t\t\t\t\t) | rpl::map(rpl::mappers::_1 == GiveawayType::Credits),\n\t\t\t\t\tcreditsGroup->value() | rpl::map([=](int v) {\n\t\t\t\t\t\treturn creditsOption(v).yearlyBoosts;\n\t\t\t\t\t}),\n\t\t\t\t\trpl::combine(\n\t\t\t\t\t\tstate->sliderValue.value(),\n\t\t\t\t\t\thideSpecificUsersOn()\n\t\t\t\t\t) | rpl::map([=](int value, bool random) -> int {\n\t\t\t\t\t\treturn state->apiOptions.giveawayBoostsPerPremium()\n\t\t\t\t\t\t\t* (random\n\t\t\t\t\t\t\t\t? value\n\t\t\t\t\t\t\t\t: int(state->selectedToAward.size()));\n\t\t\t\t\t})),\n\t\t\tstate->confirmButtonBusy.value() | rpl::map(!rpl::mappers::_1));\n\n\t\t{\n\t\t\tconst auto loadingAnimation = InfiniteRadialAnimationWidget(\n\t\t\t\tbutton,\n\t\t\t\tst::giveawayGiftCodeStartButton.height / 2);\n\t\t\tAddChildToWidgetCenter(button.data(), loadingAnimation);\n\t\t\tloadingAnimation->showOn(state->confirmButtonBusy.value());\n\t\t}\n\n\t\tstate->typeValue.value(\n\t\t) | rpl::on_next([=, raw = button.data()] {\n\t\t\traw->resizeToWidth(box->width()\n\t\t\t\t- stButton.buttonPadding.left()\n\t\t\t\t- stButton.buttonPadding.right());\n\t\t}, button->lifetime());\n\t\tbutton->setClickedCallback([=] {\n\t\t\tif (state->confirmButtonBusy.current()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto type = typeGroup->current();\n\t\t\tconst auto isSpecific = isSpecificUsers();\n\t\t\tconst auto isRandom = (type == GiveawayType::Random);\n\t\t\tconst auto isCredits = (type == GiveawayType::Credits);\n\t\t\tif (!isSpecific && !isRandom && !isCredits) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto invoice = [&] {\n\t\t\t\tif (isPrepaidCredits) {\n\t\t\t\t\treturn Payments::InvoicePremiumGiftCode{\n\t\t\t\t\t\t.giveawayCredits = prepaid->credits,\n\t\t\t\t\t\t.randomId = prepaid->id,\n\t\t\t\t\t\t.users = prepaid->quantity,\n\t\t\t\t\t};\n\t\t\t\t} else if (isCredits) {\n\t\t\t\t\tconst auto option = creditsOption(\n\t\t\t\t\t\tcreditsGroup->current());\n\t\t\t\t\treturn Payments::InvoicePremiumGiftCode{\n\t\t\t\t\t\t.currency = option.currency,\n\t\t\t\t\t\t.storeProduct = option.storeProduct,\n\t\t\t\t\t\t.giveawayCredits = option.credits,\n\t\t\t\t\t\t.randomId = UniqueIdFromCreditsOption(option, peer),\n\t\t\t\t\t\t.amount = option.amount,\n\t\t\t\t\t\t.users = state->sliderValue.current(),\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn state->apiOptions.invoice(\n\t\t\t\t\tisSpecific\n\t\t\t\t\t\t? state->selectedToAward.size()\n\t\t\t\t\t\t: state->sliderValue.current(),\n\t\t\t\t\tprepaid\n\t\t\t\t\t\t? prepaid->months\n\t\t\t\t\t\t: state->apiOptions.monthsFromPreset(\n\t\t\t\t\t\t\tdurationGroup->current()));\n\t\t\t}();\n\t\t\tif (isSpecific) {\n\t\t\t\tif (state->selectedToAward.empty()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tinvoice.purpose = Payments::InvoicePremiumGiftCodeUsers{\n\t\t\t\t\tranges::views::all(\n\t\t\t\t\t\tstate->selectedToAward\n\t\t\t\t\t) | ranges::views::transform([](\n\t\t\t\t\t\t\tconst not_null<PeerData*> p) {\n\t\t\t\t\t\treturn not_null{ p->asUser() };\n\t\t\t\t\t}) | ranges::to_vector,\n\t\t\t\t\tpeer->asChannel(),\n\t\t\t\t};\n\t\t\t} else if (isRandom || isCredits || isPrepaidCredits) {\n\t\t\t\tinvoice.purpose = Payments::InvoicePremiumGiftCodeGiveaway{\n\t\t\t\t\t.boostPeer = peer->asChannel(),\n\t\t\t\t\t.additionalChannels = ranges::views::all(\n\t\t\t\t\t\tstate->selectedToSubscribe\n\t\t\t\t\t) | ranges::views::transform([](\n\t\t\t\t\t\t\tconst not_null<PeerData*> p) {\n\t\t\t\t\t\treturn not_null{ p->asChannel() };\n\t\t\t\t\t}) | ranges::to_vector,\n\t\t\t\t\t.countries = state->countriesValue.current(),\n\t\t\t\t\t.additionalPrize = state->additionalPrize.current(),\n\t\t\t\t\t.untilDate = state->dateValue.current(),\n\t\t\t\t\t.onlyNewSubscribers = (membersGroup->current()\n\t\t\t\t\t\t== GiveawayType::OnlyNewMembers),\n\t\t\t\t\t.showWinners = state->showWinners.current(),\n\t\t\t\t};\n\t\t\t}\n\t\t\tstate->confirmButtonBusy = true;\n\t\t\tconst auto show = box->uiShow();\n\t\t\tconst auto weak = base::make_weak(box.get());\n\t\t\tconst auto done = [=](Payments::CheckoutResult result) {\n\t\t\t\tconst auto isPaid = result == Payments::CheckoutResult::Paid;\n\t\t\t\tif (result == Payments::CheckoutResult::Pending || isPaid) {\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->window()->setFocus();\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (isPaid) {\n\t\t\t\t\treloadOnDone();\n\t\t\t\t\tconst auto filter = [=](const auto &...) {\n\t\t\t\t\t\tif (const auto window = weakWindow.get()) {\n\t\t\t\t\t\t\twindow->showSection(Info::Boosts::Make(peer));\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t};\n\t\t\t\t\tconst auto group = peer->isMegagroup();\n\t\t\t\t\tconst auto title = isSpecific\n\t\t\t\t\t\t? tr::lng_giveaway_awarded_title\n\t\t\t\t\t\t: tr::lng_giveaway_created_title;\n\t\t\t\t\tconst auto body = isSpecific\n\t\t\t\t\t\t? (group\n\t\t\t\t\t\t\t? tr::lng_giveaway_awarded_body_group\n\t\t\t\t\t\t\t: tr::lng_giveaway_awarded_body)\n\t\t\t\t\t\t: (group\n\t\t\t\t\t\t\t? tr::lng_giveaway_created_body_group\n\t\t\t\t\t\t\t: tr::lng_giveaway_created_body);\n\t\t\t\t\tshow->showToast({\n\t\t\t\t\t\t.text = tr::bold(\n\t\t\t\t\t\t\ttitle(tr::now)).append('\\n').append(\n\t\t\t\t\t\t\t\tbody(\n\t\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\t\t\t\ttr::link(\n\t\t\t\t\t\t\t\t\t\ttr::lng_giveaway_created_link(\n\t\t\t\t\t\t\t\t\t\t\ttr::now)),\n\t\t\t\t\t\t\t\t\ttr::marked)),\n\t\t\t\t\t\t.filter = filter,\n\t\t\t\t\t\t.adaptive = true,\n\t\t\t\t\t\t.duration = kDoneTooltipDuration,\n\t\t\t\t\t});\n\t\t\t\t} else if (weak) {\n\t\t\t\t\tstate->confirmButtonBusy = false;\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto startPrepaid = [=](Fn<void()> close) {\n\t\t\t\tif (!weak) {\n\t\t\t\t\tclose();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tstate->apiOptions.applyPrepaid(\n\t\t\t\t\tinvoice,\n\t\t\t\t\tprepaid->id\n\t\t\t\t) | rpl::on_error_done([=](const QString &error) {\n\t\t\t\t\tif (const auto window = weakWindow.get()) {\n\t\t\t\t\t\twindow->uiShow()->showToast(error);\n\t\t\t\t\t\tclose();\n\t\t\t\t\t\tdone(Payments::CheckoutResult::Cancelled);\n\t\t\t\t\t}\n\t\t\t\t}, [=] {\n\t\t\t\t\tclose();\n\t\t\t\t\tdone(Payments::CheckoutResult::Paid);\n\t\t\t\t}, box->lifetime());\n\t\t\t};\n\t\t\tif (prepaid) {\n\t\t\t\tconst auto cancel = [=](Fn<void()> close) {\n\t\t\t\t\tif (weak) {\n\t\t\t\t\t\tstate->confirmButtonBusy = false;\n\t\t\t\t\t}\n\t\t\t\t\tclose();\n\t\t\t\t};\n\t\t\t\tshow->show(Ui::MakeConfirmBox({\n\t\t\t\t\t.text = tr::lng_giveaway_start_sure(tr::now),\n\t\t\t\t\t.confirmed = startPrepaid,\n\t\t\t\t\t.cancelled = cancel,\n\t\t\t\t}));\n\t\t\t} else {\n\t\t\t\tPayments::CheckoutProcess::Start(std::move(invoice), done);\n\t\t\t}\n\t\t});\n\t\tbox->addButton(std::move(button));\n\t}\n\tstate->typeValue.force_assign(GiveawayType::Random);\n\n\tstd::move(\n\t\tshowFinished\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\tif (!loading->toggled()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto done = [=] {\n\t\t\tstate->lifetimeApi.destroy();\n\t\t\tloading->toggle(false, anim::type::instant);\n\t\t\tstate->confirmButtonBusy = false;\n\t\t\tfillSliderContainer();\n\t\t\tif (!prepaid) {\n\t\t\t\tstate->chosenMonths = state->apiOptions.monthsFromPreset(0);\n\t\t\t}\n\t\t\tfillCreditsTypeWrap();\n\t\t\tfillCreditsOptions();\n\t\t\trebuildListOptions(state->typeValue.current(), 1);\n\t\t\tcontentWrap->toggle(true, anim::type::instant);\n\t\t\tcontentWrap->resizeToWidth(box->width());\n\t\t};\n\t\tconst auto receivedOptions = [=] {\n\t\t\tstate->lifetimeApi.destroy();\n\t\t\tstate->lifetimeApi = state->apiCreditsOptions.request(\n\t\t\t) | rpl::on_error_done([=](const QString &error) {\n\t\t\t\tbox->uiShow()->showToast(error);\n\t\t\t\tbox->closeBox();\n\t\t\t}, done);\n\t\t};\n\t\tif (prepaid) {\n\t\t\treturn done();\n\t\t}\n\t\tstate->lifetimeApi = state->apiOptions.request(\n\t\t) | rpl::on_error_done([=](const QString &error) {\n\t\t\tbox->uiShow()->showToast(error);\n\t\t\tbox->closeBox();\n\t\t}, receivedOptions);\n\t}, box->lifetime());\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\n\nnamespace Data {\nstruct BoostPrepaidGiveaway;\n} // namespace Data\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nvoid CreateGiveawayBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tFn<void()> reloadOnDone,\n\tstd::optional<Data::BoostPrepaidGiveaway> prepaidGiveaway);\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/channel_statistics/boosts/giveaway/boost_badge.h\"\n\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/widgets/labels.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_statistics.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Info::Statistics {\n\nnot_null<Ui::RpWidget*> InfiniteRadialAnimationWidget(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tint size,\n\t\tconst style::InfiniteRadialAnimation *st) {\n\tclass Widget final : public Ui::RpWidget {\n\tpublic:\n\t\tWidget(\n\t\t\tnot_null<Ui::RpWidget*> p,\n\t\t\tint size,\n\t\t\tconst style::InfiniteRadialAnimation *st)\n\t\t: Ui::RpWidget(p)\n\t\t, _st(st ? st : &st::startGiveawayButtonLoading)\n\t\t, _animation([=] { update(); }, *_st) {\n\t\t\tresize(size, size);\n\t\t\tshownValue() | rpl::on_next([=](bool v) {\n\t\t\t\treturn v\n\t\t\t\t\t? _animation.start()\n\t\t\t\t\t: _animation.stop(anim::type::instant);\n\t\t\t}, lifetime());\n\t\t}\n\n\tprotected:\n\t\tvoid paintEvent(QPaintEvent *e) override {\n\t\t\tauto p = QPainter(this);\n\t\t\tp.setPen(st::activeButtonFg);\n\t\t\tp.setBrush(st::activeButtonFg);\n\t\t\tconst auto r = rect() - Margins(_st->thickness);\n\t\t\t_animation.draw(p, r.topLeft(), r.size(), width());\n\t\t}\n\n\tprivate:\n\t\tconst style::InfiniteRadialAnimation *_st;\n\t\tUi::InfiniteRadialAnimation _animation;\n\n\t};\n\n\treturn Ui::CreateChild<Widget>(parent.get(), size, st);\n}\n\nvoid AddChildToWidgetCenter(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Ui::RpWidget*> child) {\n\tparent->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tconst auto size = child->size();\n\t\tchild->moveToLeft(\n\t\t\t(s.width() - size.width()) / 2,\n\t\t\t(s.height() - size.height()) / 2);\n\t}, child->lifetime());\n}\n\nQImage CreateBadge(\n\t\tconst style::TextStyle &textStyle,\n\t\tconst QString &text,\n\t\tint badgeHeight,\n\t\tconst style::margins &textPadding,\n\t\tconst style::color &bg,\n\t\tconst style::color &fg,\n\t\tfloat64 bgOpacity,\n\t\tconst style::margins &iconPadding,\n\t\tconst style::icon &icon) {\n\tauto badgeText = Ui::Text::String(textStyle, text);\n\tconst auto badgeTextWidth = badgeText.maxWidth();\n\tconst auto badgex = 0;\n\tconst auto badgey = 0;\n\tconst auto badgeh = 0 + badgeHeight;\n\tconst auto badgew = badgeTextWidth\n\t\t+ rect::m::sum::h(textPadding);\n\tauto result = QImage(\n\t\tQSize(badgew, badgeh) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::transparent);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\t{\n\t\tauto p = Painter(&result);\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(bg);\n\n\t\tconst auto r = QRect(badgex, badgey, badgew, badgeh);\n\t\t{\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tauto o = ScopedPainterOpacity(p, bgOpacity);\n\t\t\tp.drawRoundedRect(r, badgeh / 2, badgeh / 2);\n\t\t}\n\n\t\tp.setPen(fg);\n\t\tp.setBrush(Qt::NoBrush);\n\t\tbadgeText.drawLeftElided(\n\t\t\tp,\n\t\t\tr.x() + textPadding.left(),\n\t\t\tbadgey + textPadding.top(),\n\t\t\tbadgew,\n\t\t\tbadgew * 2);\n\n\t\ticon.paint(\n\t\t\tp,\n\t\t\tQPoint(r.x() + iconPadding.left(), r.y() + iconPadding.top()),\n\t\t\tbadgew * 2);\n\t}\n\treturn result;\n}\n\nvoid AddLabelWithBadgeToButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<QString> text,\n\t\trpl::producer<int> number,\n\t\trpl::producer<bool> shown) {\n\tstruct State {\n\t\tQImage badge;\n\t};\n\tconst auto state = parent->lifetime().make_state<State>();\n\tconst auto label = Ui::CreateChild<Ui::LabelSimple>(\n\t\tparent.get(),\n\t\tst::startGiveawayButtonLabelSimple);\n\tstd::move(\n\t\ttext\n\t) | rpl::on_next([=](const QString &s) {\n\t\tlabel->setText(s);\n\t}, label->lifetime());\n\tconst auto count = Ui::CreateChild<Ui::RpWidget>(parent.get());\n\tcount->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(count);\n\t\tp.drawImage(0, 0, state->badge);\n\t}, count->lifetime());\n\tstd::move(\n\t\tnumber\n\t) | rpl::on_next([=](int c) {\n\t\tstate->badge = Info::Statistics::CreateBadge(\n\t\t\tst::startGiveawayButtonTextStyle,\n\t\t\tQString::number(c),\n\t\t\tst::boostsListBadgeHeight,\n\t\t\tst::startGiveawayButtonBadgeTextPadding,\n\t\t\tst::activeButtonFg,\n\t\t\tst::activeButtonBg,\n\t\t\t1.,\n\t\t\tst::boostsListMiniIconPadding,\n\t\t\tst::startGiveawayButtonMiniIcon);\n\t\tcount->resize(state->badge.size() / style::DevicePixelRatio());\n\t\tcount->update();\n\t}, count->lifetime());\n\n\tstd::move(\n\t\tshown\n\t) | rpl::on_next([=](bool shown) {\n\t\tcount->setVisible(shown);\n\t\tlabel->setVisible(shown);\n\t}, count->lifetime());\n\n\trpl::combine(\n\t\tparent->sizeValue(),\n\t\tlabel->sizeValue(),\n\t\tcount->sizeValue()\n\t) | rpl::on_next([=](\n\t\t\tconst QSize &s,\n\t\t\tconst QSize &s1,\n\t\t\tconst QSize &s2) {\n\t\tconst auto sum = st::startGiveawayButtonMiniIconSkip\n\t\t\t+ s1.width()\n\t\t\t+ s2.width();\n\t\tconst auto contentLeft = (s.width() - sum) / 2;\n\t\tlabel->moveToLeft(contentLeft, (s.height() - s1.height()) / 2);\n\t\tcount->moveToLeft(\n\t\t\tcontentLeft + sum - s2.width(),\n\t\t\t(s.height() - s2.height()) / 2 + st::boostsListMiniIconSkip);\n\t}, parent->lifetime());\n}\n\n} // namespace Info::Statistics\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace style {\nstruct InfiniteRadialAnimation;\nstruct TextStyle;\n} // namespace style\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Info::Statistics {\n\n[[nodiscard]] QImage CreateBadge(\n\tconst style::TextStyle &textStyle,\n\tconst QString &text,\n\tint badgeHeight,\n\tconst style::margins &textPadding,\n\tconst style::color &bg,\n\tconst style::color &fg,\n\tfloat64 bgOpacity,\n\tconst style::margins &iconPadding,\n\tconst style::icon &icon);\n\n[[nodiscard]] not_null<Ui::RpWidget*> InfiniteRadialAnimationWidget(\n\tnot_null<Ui::RpWidget*> parent,\n\tint size,\n\tconst style::InfiniteRadialAnimation *st = nullptr);\n\nvoid AddChildToWidgetCenter(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Ui::RpWidget*> child);\n\nvoid AddLabelWithBadgeToButton(\n\tnot_null<Ui::RpWidget*> parent,\n\trpl::producer<QString> text,\n\trpl::producer<int> number,\n\trpl::producer<bool> shown);\n\n} // namespace Info::Statistics\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\nusing \"boxes/boxes.style\";\nusing \"ui/effects/premium.style\";\nusing \"statistics/statistics.style\";\n\ngiveawayTypeListItem: PeerListItem(defaultPeerListItem) {\n\theight: 52px;\n\tphotoPosition: point(58px, 6px);\n\tnamePosition: point(110px, 8px);\n\tstatusPosition: point(110px, 28px);\n\tphotoSize: 42px;\n}\ngiveawayUserpic: icon {{ \"boosts/filled_gift\", windowFgActive }};\ngiveawayUserpicSkip: 1px;\ngiveawayUserpicGroup: icon {{ \"limits/groups\", windowFgActive }};\ngiveawayRadioPosition: point(21px, 16px);\n\ngiveawayGiftCodeCountryButton: SettingsButton(reportReasonButton) {\n}\ngiveawayGiftCodeCountrySelect: MultiSelect(defaultMultiSelect) {\n}\n\ngiveawayGiftCodeChannelDeleteIcon: icon {{ \"dialogs/dialogs_cancel_search\", dialogsMenuIconFg }};\ngiveawayGiftCodeChannelDeleteIconOver: icon {{ \"dialogs/dialogs_cancel_search\", dialogsMenuIconFgOver }};\n\ngiveawayLoadingLabel: FlatLabel(membersAbout) {\n}\ngiveawayGiftCodeTopHeight: 195px;\ngiveawayGiftCodeLink: FlatLabel(defaultFlatLabel) {\n\tmargin: margins(10px, 12px, 10px, 8px);\n\ttextFg: menuIconColor;\n\tmaxHeight: 24px;\n}\ngiveawayGiftCodeLinkCopy: icon{{ \"menu/copy\", menuIconColor }};\ngiveawayGiftCodeLinkHeight: 42px;\ngiveawayGiftCodeLinkCopyWidth: 40px;\ngiveawayGiftCodeLinkMargin: margins(24px, 8px, 24px, 12px);\n\ngiveawayGiftCodeGiftOption: PremiumOption(premiumGiftOption) {\n\tbadgeShift: point(5px, 0px);\n}\ngiveawayGiftCodeStartButton: RoundButton(defaultActiveButton) {\n\theight: 42px;\n\ttextTop: 12px;\n\tradius: 6px;\n}\ngiveawayGiftCodeQuantitySubtitle: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(boxFontSize semibold);\n\t}\n\ttextFg: windowActiveTextFg;\n\tminWidth: 240px;\n\talign: align(right);\n}\ngiveawayGiftCodeQuantityFloat: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n\ttextFg: windowActiveTextFg;\n\tminWidth: 50px;\n\talign: align(center);\n}\n\nboostLinkStatsButton: IconButton(defaultIconButton) {\n\twidth: giveawayGiftCodeLinkCopyWidth;\n\theight: giveawayGiftCodeLinkHeight;\n\ticon: icon{{ \"menu/stats\", menuIconColor }};\n\ticonOver: icon{{ \"menu/stats\", menuIconColor }};\n\tripple: emptyRippleAnimation;\n}\n\ngiveawayGiftCodeTable: Table(defaultTable) {\n\tlabelMinWidth: 91px;\n}\ngiveawayGiftCodeTableMargin: margins(24px, 4px, 24px, 4px);\ngiveawayGiftCodeLabelMargin: margins(13px, 10px, 13px, 10px);\ngiveawayGiftCodeValueMultiline: FlatLabel(defaultTableValue) {\n\tminWidth: 128px;\n\tmaxHeight: 100px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(10px);\n\t\tlinkUnderline: kLinkUnderlineNever;\n\t}\n}\ngiveawayGiftMessage: FlatLabel(defaultTableValue) {\n\tminWidth: 128px;\n\tmaxHeight: 0px;\n}\ngiveawayGiftMessageRemove: IconButton(defaultIconButton) {\n\twidth: 32px;\n\theight: 32px;\n\ticon: icon{{ \"menu/delete\", windowActiveTextFg }};\n\ticonOver: icon{{ \"menu/delete\", windowActiveTextFg }};\n\trippleAreaPosition: point(0px, 0px);\n\trippleAreaSize: 32px;\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: lightButtonBgRipple;\n\t}\n}\ngiveawayGiftCodeValueMargin: margins(13px, 9px, 13px, 9px);\ngiveawayGiftCodePeerMargin: margins(11px, 6px, 11px, 4px);\ngiveawayGiftCodeUserpic: UserpicButton(defaultUserpicButton) {\n\tsize: size(24px, 24px);\n\tphotoSize: 24px;\n\tphotoPosition: point(-1px, -1px);\n}\ngiveawayGiftCodeNamePosition: point(32px, 4px);\ngiveawayGiftCodeCover: PremiumCover(userPremiumCover) {\n\tstarSize: size(92px, 90px);\n\tstarTopSkip: 20px;\n\ttitlePadding: margins(0px, 15px, 0px, 17px);\n\ttitleFont: font(15px semibold);\n\tabout: FlatLabel(userPremiumCoverAbout) {\n\t\ttextFg: windowBoldFg;\n\t\tstyle: TextStyle(premiumAboutTextStyle) {\n\t\t\tlineHeight: 17px;\n\t\t}\n\t}\n}\ngiveawayGiftCodeCoverClosePosition: point(5px, 0px);\ngiveawayGiftCodeCoverDividerPadding: margins(0px, 11px, 0px, 5px);\ngiveawayGiftCodeTypeDividerPadding: margins(0px, 7px, 0px, 5px);\ngiveawayGiftCodeSliderPadding: margins(0px, 24px, 0px, 10px);\ngiveawayGiftCodeSliderFloatSkip: 6px;\ngiveawayGiftCodeChannelsSubsectionPadding: margins(0px, -1px, 0px, -4px);\ngiveawayGiftCodeAdditionalPaddingMin: margins(50px, 4px, 22px, 0px);\ngiveawayGiftCodeAdditionalField: InputField(defaultMultiSelectSearchField) {\n}\ngiveawayGiftCodeAdditionalLabel: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n}\ngiveawayGiftCodeAdditionalLabelSkip: 12px;\n\ngiveawayGiftCodeChannelsPeerList: PeerList(boostsListBox) {\n\tpadding: margins(0px, 7px, 0px, 0px);\n}\ngiveawayGiftCodeMembersPeerList: PeerList(defaultPeerList) {\n\titem: PeerListItem(defaultPeerListItem) {\n\t\theight: 50px;\n\t\tnamePosition: point(62px, 7px);\n\t\tstatusPosition: point(62px, 27px);\n\t}\n}\ngiveawayRadioMembersPosition: point(21px, 14px);\n\ngiveawayGiftCodeChannelsAddButton: SettingsButton(defaultSettingsButton) {\n\ttextFg: lightButtonFg;\n\ttextFgOver: lightButtonFgOver;\n\tpadding: margins(70px, 10px, 22px, 8px);\n\ticonLeft: 28px;\n}\ngiveawayGiftCodeChannelsDividerPadding: margins(0px, 5px, 0px, 5px);\n\ngiveawayGiftCodeFooter: FlatLabel(defaultFlatLabel) {\n\talign: align(top);\n\ttextFg: windowBoldFg;\n}\ngiveawayGiftCodeFooterMargin: margins(0px, 9px, 0px, 4px);\ngiveawayGiftCodeBoxButton: RoundButton(defaultActiveButton) {\n\theight: 42px;\n\ttextTop: 12px;\n\tstyle: semiboldTextStyle;\n}\ngiveawayGiftCodeBox: Box(defaultBox) {\n\tbuttonPadding: margins(22px, 11px, 22px, 22px);\n\tbuttonHeight: 42px;\n\tbuttonWide: true;\n\tbutton: giveawayGiftCodeBoxButton;\n\tshadowIgnoreTopSkip: true;\n}\ngiveawayGiftCodeBoxUpgradeNext: RoundButton(defaultLightButton, giveawayGiftCodeBoxButton) {\n}\ngiveawayRefundedLabel: FlatLabel(boxLabel) {\n\talign: align(top);\n\tstyle: semiboldTextStyle;\n\ttextFg: attentionButtonFg;\n}\ngiveawayRefundedPadding: margins(8px, 10px, 8px, 10px);\n\nstartGiveawayBox: Box(premiumGiftBox) {\n\tshadowIgnoreTopSkip: true;\n}\nstartGiveawayScrollArea: ScrollArea(boxScroll) {\n\tdeltax: 3px;\n\tdeltat: 50px;\n}\nstartGiveawayBoxTitleClose: IconButton(boxTitleClose) {\n\tripple: universalRippleAnimation;\n}\nstartGiveawayCover: PremiumCover(giveawayGiftCodeCover) {\n\tbg: boxDividerBg;\n\tadditionalShadowForDarkThemes: false;\n}\n\nstartGiveawayButtonLabelSimple: LabelSimple {\n\tfont: semiboldFont;\n\ttextFg: activeButtonFg;\n}\nstartGiveawayButtonMiniIcon: icon{{ \"boosts/boost_mini2\", activeButtonBg }};\nstartGiveawayButtonMiniIconSkip: 5px;\nstartGiveawayButtonBadgeTextPadding: margins(16px, -1px, 6px, 0px);\nstartGiveawayButtonTextStyle: TextStyle(defaultTextStyle) {\n\tfont: semiboldFont;\n}\n\nstartGiveawayButtonLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {\n\tcolor: activeButtonFg;\n\tthickness: 2px;\n}\n\nstarConvertButtonLoading: InfiniteRadialAnimation(startGiveawayButtonLoading) {\n\tcolor: windowActiveTextFg;\n\tthickness: 2px;\n}\n\nstarGiftSmallButton: defaultTableSmallButton;\ndarkGiftCodeBox: Box(giveawayGiftCodeBox) {\n\tbg: groupCallMembersBg;\n\ttitle: FlatLabel(boxTitle) {\n\t\ttextFg: groupCallMembersFg;\n\t}\n\ttitleAdditionalFg: groupCallMemberNotJoinedStatus;\n}\ndarkGiftLink: icon {{ \"menu/copy\", groupCallMembersFg }};\ndarkGiftShare: icon {{ \"menu/share\", groupCallMembersFg }};\ndarkGiftTheme: icon {{ \"menu/colors\", groupCallMembersFg }};\ndarkGiftTransfer: icon {{ \"chat/input_replace\", groupCallMembersFg }};\ndarkGiftCraft: icon {{ \"menu/craft_start-24x24\", groupCallMembersFg }};\ndarkGiftNftWear: icon {{ \"menu/nft_wear\", groupCallMembersFg }};\ndarkGiftNftTakeOff: icon {{ \"menu/nft_takeoff\", groupCallMembersFg }};\ndarkGiftNftResell: icon {{ \"menu/tag_sell\", groupCallMembersFg }};\ndarkGiftNftUnlist: icon {{ \"menu/tag_remove\", groupCallMembersFg }};\ndarkGiftHide: icon {{ \"menu/stealth\", groupCallMembersFg }};\ndarkGiftShow: icon {{ \"menu/show_in_chat\", groupCallMembersFg }};\ndarkGiftPin: icon {{ \"menu/pin\", groupCallMembersFg }};\ndarkGiftUnpin: icon {{ \"menu/unpin\", groupCallMembersFg }};\ndarkGiftOffer: icon {{ \"menu/earn\", groupCallMembersFg }};\ndarkGiftPalette: TextPalette(defaultTextPalette) {\n\tlinkFg: mediaviewTextLinkFg;\n\tmonoFg: groupCallMembersFg;\n\tspoilerFg: groupCallMembersFg;\n}\ndarkGiftTable: Table(giveawayGiftCodeTable) {\n\theaderBg: groupCallMembersBgOver;\n\tborderFg: mediaviewMenuBgOver;\n\tsmallButton: RoundButton(defaultTableSmallButton) {\n\t\ttextFg: groupCallMembersFg;\n\t\ttextFgOver: groupCallMembersFg;\n\t\ttextBg: groupCallMenuBgRipple;\n\t\ttextBgOver: groupCallMenuBgRipple;\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: mediaviewMenuBgOver;\n\t\t}\n\t}\n\tdefaultLabel: FlatLabel(defaultTableLabel) {\n\t\ttextFg: groupCallMembersFg;\n\t\tpalette: darkGiftPalette;\n\t}\n\tdefaultValue: FlatLabel(defaultTableValue) {\n\t\ttextFg: groupCallMembersFg;\n\t\tpalette: darkGiftPalette;\n\t}\n}\ndarkGiftTableValueMultiline: FlatLabel(giveawayGiftCodeValueMultiline) {\n\ttextFg: groupCallMembersFg;\n\tpalette: darkGiftPalette;\n}\ndarkGiftTableMessage: FlatLabel(giveawayGiftMessage) {\n\ttextFg: groupCallMembersFg;\n\tpalette: darkGiftPalette;\n}\ndarkGiftCodeLink: FlatLabel(giveawayGiftCodeLink) {\n\ttextFg: mediaviewMenuFg;\n}\ndarkGiftBoxClose: IconButton(boxTitleClose) {\n\ticon: icon {{ \"box_button_close\", groupCallMemberInactiveIcon }};\n\ticonOver: icon {{ \"box_button_close\", groupCallMemberInactiveIcon }};\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: groupCallMembersBgOver;\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway_list_controllers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/channel_statistics/boosts/giveaway/giveaway_list_controllers.h\"\n\n#include \"apiwrap.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"dialogs/dialogs_indexed_list.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_giveaway.h\"\n\nnamespace Giveaway {\nnamespace {\n\nclass ChannelRow final : public PeerListRow {\npublic:\n\tusing PeerListRow::PeerListRow;\n\n\tQSize rightActionSize() const override;\n\tQMargins rightActionMargins() const override;\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\n\tvoid rightActionAddRipple(\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) override;\n\tvoid rightActionStopLastRipple() override;\n\nprivate:\n\tstd::unique_ptr<Ui::RippleAnimation> _actionRipple;\n\n};\n\nQSize ChannelRow::rightActionSize() const {\n\treturn QSize(\n\t\tst::giveawayGiftCodeChannelDeleteIcon.width(),\n\t\tst::giveawayGiftCodeChannelDeleteIcon.height()) * 2;\n}\n\nQMargins ChannelRow::rightActionMargins() const {\n\tconst auto itemHeight = st::giveawayGiftCodeChannelsPeerList.item.height;\n\treturn QMargins(\n\t\t0,\n\t\t(itemHeight - rightActionSize().height()) / 2,\n\t\tst::giveawayRadioPosition.x() / 2,\n\t\t0);\n}\n\nvoid ChannelRow::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\tif (_actionRipple) {\n\t\t_actionRipple->paint(\n\t\t\tp,\n\t\t\tx,\n\t\t\ty,\n\t\t\touterWidth);\n\t\tif (_actionRipple->empty()) {\n\t\t\t_actionRipple.reset();\n\t\t}\n\t}\n\tconst auto rect = QRect(QPoint(x, y), ChannelRow::rightActionSize());\n\t(actionSelected\n\t\t? st::giveawayGiftCodeChannelDeleteIconOver\n\t\t: st::giveawayGiftCodeChannelDeleteIcon).paintInCenter(p, rect);\n}\n\nvoid ChannelRow::rightActionAddRipple(\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) {\n\tif (!_actionRipple) {\n\t\tauto mask = Ui::RippleAnimation::EllipseMask(rightActionSize());\n\t\t_actionRipple = std::make_unique<Ui::RippleAnimation>(\n\t\t\tst::defaultRippleAnimation,\n\t\t\tstd::move(mask),\n\t\t\tstd::move(updateCallback));\n\t}\n\t_actionRipple->add(point);\n}\n\nvoid ChannelRow::rightActionStopLastRipple() {\n\tif (_actionRipple) {\n\t\t_actionRipple->lastStop();\n\t}\n}\n\n} // namespace\n\nAwardMembersListController::AwardMembersListController(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tstd::vector<not_null<PeerData*>> selected)\n: ParticipantsBoxController(navigation, peer, ParticipantsRole::Members)\n, _selected(std::move(selected)) {\n}\n\nvoid AwardMembersListController::prepare() {\n\tParticipantsBoxController::prepare();\n\tdelegate()->peerListAddSelectedPeers(base::take(_selected));\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid AwardMembersListController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto checked = !row->checked();\n\tif (checked\n\t\t&& _checkErrorCallback\n\t\t&& _checkErrorCallback(delegate()->peerListSelectedRowsCount())) {\n\t\treturn;\n\t}\n\tdelegate()->peerListSetRowChecked(row, checked);\n}\n\nstd::unique_ptr<PeerListRow> AwardMembersListController::createRow(\n\t\tnot_null<PeerData*> participant) const {\n\tconst auto user = participant->asUser();\n\tif (!user || user->isInaccessible() || user->isBot() || user->isSelf()) {\n\t\treturn nullptr;\n\t}\n\tauto type = Type{\n\t\t.chatStyle = _chatStyle.get(),\n\t\t.circleCache = &_pillCircleCache,\n\t};\n\treturn std::make_unique<Row>(participant, type);\n}\n\nbase::unique_qptr<Ui::PopupMenu> AwardMembersListController::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\treturn nullptr;\n}\n\nvoid AwardMembersListController::setCheckError(Fn<bool(int)> callback) {\n\t_checkErrorCallback = std::move(callback);\n}\n\nMyChannelsListController::MyChannelsListController(\n\tnot_null<PeerData*> peer,\n\tstd::shared_ptr<Ui::Show> show,\n\tstd::vector<not_null<PeerData*>> selected)\n: PeerListController(\n\tstd::make_unique<PeerListGlobalSearchController>(&peer->session()))\n, _peer(peer)\n, _show(show)\n, _selected(std::move(selected))\n, _otherChannels(std::make_unique<std::vector<not_null<ChannelData*>>>()) {\n\t{\n\t\tconst auto addList = [&](not_null<Dialogs::IndexedList*> list) {\n\t\t\tfor (const auto &row : list->all()) {\n\t\t\t\tif (const auto history = row->history()) {\n\t\t\t\t\tconst auto channel = history->peer->asChannel();\n\t\t\t\t\tif (channel && !channel->isMegagroup()) {\n\t\t\t\t\t\t_otherChannels->push_back(channel);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tauto &data = _peer->owner();\n\t\taddList(data.chatsList()->indexed());\n\t\tif (const auto folder = data.folderLoaded(Data::Folder::kId)) {\n\t\t\taddList(folder->chatsList()->indexed());\n\t\t}\n\t\taddList(data.contactsNoChatsList());\n\t}\n}\n\nstd::unique_ptr<PeerListRow> MyChannelsListController::createSearchRow(\n\t\tnot_null<PeerData*> peer) {\n\tif (const auto channel = peer->asChannel()) {\n\t\treturn createRow(channel);\n\t}\n\treturn nullptr;\n}\n\nstd::unique_ptr<PeerListRow> MyChannelsListController::createRestoredRow(\n\t\tnot_null<PeerData*> peer) {\n\tif (const auto channel = peer->asChannel()) {\n\t\treturn createRow(channel);\n\t}\n\treturn nullptr;\n}\n\nvoid MyChannelsListController::loadMoreRows() {\n\tif (_apiLifetime || !_otherChannels) {\n\t\treturn;\n\t} else if (_lastAddedIndex >= _otherChannels->size()) {\n\t\t_otherChannels.release();\n\t\treturn;\n\t}\n\tconstexpr auto kPerPage = int(40);\n\tconst auto till = std::min(\n\t\tint(_otherChannels->size()),\n\t\t_lastAddedIndex + kPerPage);\n\twhile (_lastAddedIndex < till) {\n\t\tdelegate()->peerListAppendRow(\n\t\t\tcreateRow(_otherChannels->at(_lastAddedIndex++)));\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid MyChannelsListController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto channel = row->peer()->asChannel();\n\tconst auto checked = !row->checked();\n\tif (checked\n\t\t&& _checkErrorCallback\n\t\t&& _checkErrorCallback(delegate()->peerListSelectedRowsCount())) {\n\t\treturn;\n\t}\n\tif (checked && channel && channel->username().isEmpty()) {\n\t\t_show->showBox(Box(Ui::ConfirmBox, Ui::ConfirmBoxArgs{\n\t\t\t.text = tr::lng_giveaway_channels_confirm_about(),\n\t\t\t.confirmed = [=](Fn<void()> close) {\n\t\t\t\tdelegate()->peerListSetRowChecked(row, checked);\n\t\t\t\tclose();\n\t\t\t},\n\t\t\t.confirmText = tr::lng_filters_recommended_add(),\n\t\t\t.title = tr::lng_giveaway_channels_confirm_title(),\n\t\t}));\n\t} else {\n\t\tdelegate()->peerListSetRowChecked(row, checked);\n\t}\n}\n\nMain::Session &MyChannelsListController::session() const {\n\treturn _peer->session();\n}\n\nvoid MyChannelsListController::prepare() {\n\tdelegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);\n\tconst auto api = _apiLifetime.make_state<MTP::Sender>(\n\t\t&session().api().instance());\n\tapi->request(\n\t\tMTPstories_GetChatsToSend()\n\t).done([=](const MTPmessages_Chats &result) {\n\t\t_apiLifetime.destroy();\n\t\tconst auto &chats = result.match([](const auto &data) {\n\t\t\treturn data.vchats().v;\n\t\t});\n\t\tauto &owner = session().data();\n\t\tfor (const auto &chat : chats) {\n\t\t\tif (const auto peer = owner.processChat(chat)) {\n\t\t\t\tif (!peer->isChannel() || (peer == _peer)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (!delegate()->peerListFindRow(peer->id.value)) {\n\t\t\t\t\tif (const auto channel = peer->asChannel()) {\n\t\t\t\t\t\tauto row = createRow(channel);\n\t\t\t\t\t\tconst auto raw = row.get();\n\t\t\t\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t\t\t\t\tif (ranges::contains(_selected, peer)) {\n\t\t\t\t\t\t\tdelegate()->peerListSetRowChecked(raw, true);\n\t\t\t\t\t\t\t_selected.erase(\n\t\t\t\t\t\t\t\tranges::remove(_selected, peer),\n\t\t\t\t\t\t\t\tend(_selected));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor (const auto &selected : _selected) {\n\t\t\tif (const auto channel = selected->asChannel()) {\n\t\t\t\tauto row = createRow(channel);\n\t\t\t\tconst auto raw = row.get();\n\t\t\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\t\t\tdelegate()->peerListSetRowChecked(raw, true);\n\t\t\t}\n\t\t}\n\t\tdelegate()->peerListRefreshRows();\n\t\t_selected.clear();\n\t}).send();\n}\n\nvoid MyChannelsListController::setCheckError(Fn<bool(int)> callback) {\n\t_checkErrorCallback = std::move(callback);\n}\n\nstd::unique_ptr<PeerListRow> MyChannelsListController::createRow(\n\t\tnot_null<ChannelData*> channel) const {\n\tauto row = std::make_unique<PeerListRow>(channel);\n\trow->setCustomStatus((channel->isBroadcast()\n\t\t? tr::lng_chat_status_subscribers\n\t\t: tr::lng_chat_status_members)(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tchannel->membersCount()));\n\treturn row;\n}\n\nSelectedChannelsListController::SelectedChannelsListController(\n\tnot_null<PeerData*> peer)\n: _peer(peer) {\n\tPeerListController::setStyleOverrides(\n\t\t&st::giveawayGiftCodeChannelsPeerList);\n}\n\nvoid SelectedChannelsListController::setTopStatus(rpl::producer<QString> s) {\n\t_statusLifetime = std::move(\n\t\ts\n\t) | rpl::on_next([=](const QString &t) {\n\t\tif (delegate()->peerListFullRowsCount() > 0) {\n\t\t\tdelegate()->peerListRowAt(0)->setCustomStatus(t);\n\t\t}\n\t});\n}\n\nvoid SelectedChannelsListController::rebuild(\n\t\tstd::vector<not_null<PeerData*>> selected) {\n\twhile (delegate()->peerListFullRowsCount() > 1) {\n\t\tdelegate()->peerListRemoveRow(delegate()->peerListRowAt(1));\n\t}\n\tfor (const auto &peer : selected) {\n\t\tdelegate()->peerListAppendRow(createRow(peer->asChannel()));\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nauto SelectedChannelsListController::channelRemoved() const\n-> rpl::producer<not_null<PeerData*>> {\n\treturn _channelRemoved.events();\n}\n\nvoid SelectedChannelsListController::rowClicked(not_null<PeerListRow*> row) {\n}\n\nvoid SelectedChannelsListController::rowRightActionClicked(\n\t\tnot_null<PeerListRow*> row) {\n\tconst auto peer = row->peer();\n\tdelegate()->peerListRemoveRow(row);\n\tdelegate()->peerListRefreshRows();\n\t_channelRemoved.fire_copy(peer);\n}\n\nMain::Session &SelectedChannelsListController::session() const {\n\treturn _peer->session();\n}\n\nvoid SelectedChannelsListController::prepare() {\n\tdelegate()->peerListAppendRow(createRow(_peer->asChannel()));\n}\n\nstd::unique_ptr<PeerListRow> SelectedChannelsListController::createRow(\n\t\tnot_null<ChannelData*> channel) const {\n\tconst auto isYourChannel = (_peer->asChannel() == channel);\n\tauto row = isYourChannel\n\t\t? std::make_unique<PeerListRow>(channel)\n\t\t: std::make_unique<ChannelRow>(channel);\n\trow->setCustomStatus(isYourChannel\n\t\t? QString()\n\t\t: (channel->isMegagroup()\n\t\t\t? tr::lng_chat_status_members\n\t\t\t: tr::lng_chat_status_subscribers)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tchannel->membersCount()));\n\treturn row;\n}\n\n} // namespace Giveaway\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway_list_controllers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peers/edit_participants_box.h\"\n\nclass ChannelData;\nclass PeerData;\nclass PeerListRow;\n\nnamespace Ui {\nclass PopupMenu;\nclass Show;\n} // namespace Ui\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nnamespace Giveaway {\n\nclass AwardMembersListController : public ParticipantsBoxController {\npublic:\n\tAwardMembersListController(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tstd::vector<not_null<PeerData*>> selected);\n\n\tvoid prepare() override;\n\n\tvoid setCheckError(Fn<bool(int)> callback);\n\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tstd::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<PeerData*> participant) const override;\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\nprivate:\n\tFn<bool(int)> _checkErrorCallback;\n\n\tstd::vector<not_null<PeerData*>> _selected;\n\n};\n\nclass MyChannelsListController : public PeerListController {\npublic:\n\tMyChannelsListController(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tstd::vector<not_null<PeerData*>> selected);\n\n\tvoid setCheckError(Fn<bool(int)> callback);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\n\tstd::unique_ptr<PeerListRow> createSearchRow(\n\t\tnot_null<PeerData*> peer) override;\n\tstd::unique_ptr<PeerListRow> createRestoredRow(\n\t\tnot_null<PeerData*> peer) override;\n\nprivate:\n\tstd::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<ChannelData*> channel) const;\n\n\tconst not_null<PeerData*> _peer;\n\tconst std::shared_ptr<Ui::Show> _show;\n\n\tFn<bool(int)> _checkErrorCallback;\n\n\tstd::vector<not_null<PeerData*>> _selected;\n\tstd::unique_ptr<std::vector<not_null<ChannelData*>>> _otherChannels;\n\tint _lastAddedIndex = 0;\n\n\trpl::lifetime _apiLifetime;\n\n};\n\nclass SelectedChannelsListController : public PeerListController {\npublic:\n\tSelectedChannelsListController(not_null<PeerData*> peer);\n\n\tvoid setTopStatus(rpl::producer<QString> status);\n\n\tvoid rebuild(std::vector<not_null<PeerData*>> selected);\n\t[[nodiscard]] rpl::producer<not_null<PeerData*>> channelRemoved() const;\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override;\n\nprivate:\n\tstd::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<ChannelData*> channel) const;\n\n\tconst not_null<PeerData*> _peer;\n\n\trpl::event_stream<not_null<PeerData*>> _channelRemoved;\n\trpl::lifetime _statusLifetime;\n\n};\n\n} // namespace Giveaway\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway_type_row.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/channel_statistics/boosts/giveaway/giveaway_type_row.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_color_indices.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_statistics.h\"\n\nnamespace Giveaway {\n\nGiveawayTypeRow::GiveawayTypeRow(\n\tnot_null<Ui::RpWidget*> parent,\n\tType type,\n\trpl::producer<QString> subtitle,\n\tbool group)\n: GiveawayTypeRow(\n\tparent,\n\ttype,\n\t(type == Type::SpecificUsers) ? st::colorIndexBlue : st::colorIndexGreen,\n\t(type == Type::SpecificUsers)\n\t\t? tr::lng_giveaway_award_option()\n\t\t: (type == Type::Random)\n\t\t? tr::lng_premium_summary_title()\n\t\t// ? tr::lng_giveaway_create_option()\n\t\t: (type == Type::AllMembers)\n\t\t? (group\n\t\t\t? tr::lng_giveaway_users_all_group()\n\t\t\t: tr::lng_giveaway_users_all())\n\t\t: (group\n\t\t\t? tr::lng_giveaway_users_new_group()\n\t\t\t: tr::lng_giveaway_users_new()),\n\tstd::move(subtitle),\n\tQImage()) {\n}\n\nGiveawayTypeRow::GiveawayTypeRow(\n\tnot_null<Ui::RpWidget*> parent,\n\tType type,\n\tint colorIndex,\n\trpl::producer<QString> title,\n\trpl::producer<QString> subtitle,\n\tQImage badge)\n: RippleButton(parent, st::defaultRippleAnimation)\n, _type(type)\n, _st((_type == Type::SpecificUsers\n\t\t|| _type == Type::Random\n\t\t|| _type == Type::Credits)\n\t? st::giveawayTypeListItem\n\t: ((_type == Type::Prepaid) || (_type == Type::PrepaidCredits))\n\t? st::boostsListBox.item\n\t: st::giveawayGiftCodeMembersPeerList.item)\n, _userpic(\n\tUi::EmptyUserpic::UserpicColor(Ui::EmptyUserpic::ColorIndex(colorIndex)),\n\tQString())\n, _badge(std::move(badge)) {\n\tif (_type == Type::Credits || _type == Type::PrepaidCredits) {\n\t\t_customUserpic = Ui::CreditsWhiteDoubledIcon(_st.photoSize, 1.);\n\t}\n\tstd::move(\n\t\tsubtitle\n\t) | rpl::on_next([=] (QString s) {\n\t\t_status.setText(\n\t\t\tst::defaultTextStyle,\n\t\t\ts.replace(QChar('>'), QString()),\n\t\t\tUi::NameTextOptions());\n\t}, lifetime());\n\tstd::move(\n\t\ttitle\n\t) | rpl::on_next([=] (const QString &s) {\n\t\t_name.setText(_st.nameStyle, s, Ui::NameTextOptions());\n\t}, lifetime());\n}\n\nint GiveawayTypeRow::resizeGetHeight(int) {\n\treturn _st.height;\n}\n\nvoid GiveawayTypeRow::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\n\tconst auto paintOver = (isOver() || isDown()) && !isDisabled();\n\tconst auto skipRight = _st.photoPosition.x();\n\tconst auto outerWidth = width();\n\tconst auto isRandom = (_type == Type::Random);\n\tconst auto isSpecific = (_type == Type::SpecificUsers);\n\tconst auto isPrepaid = (_type == Type::Prepaid);\n\tconst auto hasUserpic = isRandom\n\t\t|| isSpecific\n\t\t|| isPrepaid\n\t\t|| (!_customUserpic.isNull());\n\n\tif (paintOver) {\n\t\tp.fillRect(e->rect(), _st.button.textBgOver);\n\t}\n\tUi::RippleButton::paintRipple(p, 0, 0);\n\tif (hasUserpic) {\n\t\t_userpic.paintCircle(\n\t\t\tp,\n\t\t\t_st.photoPosition.x(),\n\t\t\t_st.photoPosition.y(),\n\t\t\touterWidth,\n\t\t\t_st.photoSize);\n\n\t\tconst auto userpicRect = QRect(\n\t\t\t_st.photoPosition\n\t\t\t\t- QPoint(\n\t\t\t\t\tisSpecific ? -st::giveawayUserpicSkip : 0,\n\t\t\t\t\tisSpecific ? 0 : st::giveawayUserpicSkip),\n\t\t\tSize(_st.photoSize));\n\t\tif (!_customUserpic.isNull()) {\n\t\t\tp.drawImage(_st.photoPosition, _customUserpic);\n\t\t} else {\n\t\t\tconst auto &userpic = isSpecific\n\t\t\t\t? st::giveawayUserpicGroup\n\t\t\t\t: st::giveawayUserpic;\n\t\t\tuserpic.paintInCenter(p, userpicRect);\n\t\t}\n\t}\n\n\tconst auto namex = _st.namePosition.x();\n\tconst auto namey = _st.namePosition.y();\n\tconst auto namew = outerWidth - namex - skipRight;\n\n\tconst auto badgew = _badge.width() / style::DevicePixelRatio();\n\n\tp.setPen(_st.nameFg);\n\t_name.drawLeftElided(p, namex, namey, namew - badgew, width());\n\n\tif (!_badge.isNull()) {\n\t\tp.drawImage(\n\t\t\tstd::min(\n\t\t\t\tnamex + _name.maxWidth() + st::boostsListBadgePadding.left(),\n\t\t\t\touterWidth - badgew - skipRight),\n\t\t\tnamey + st::boostsListMiniIconSkip,\n\t\t\t_badge);\n\t}\n\n\tconst auto statusIcon = isRandom ? &st::topicButtonArrow : nullptr;\n\tconst auto statusx = _st.statusPosition.x();\n\tconst auto statusy = _st.statusPosition.y();\n\tconst auto statusw = outerWidth\n\t\t- statusx\n\t\t- skipRight\n\t\t- (statusIcon\n\t\t\t? (statusIcon->width() + st::boostsListMiniIconSkip)\n\t\t\t: 0);\n\tp.setFont(st::contactsStatusFont);\n\tp.setPen((isRandom || !hasUserpic) ? st::lightButtonFg : _st.statusFg);\n\t_status.drawLeftElided(p, statusx, statusy, statusw, outerWidth);\n\tif (statusIcon) {\n\t\tstatusIcon->paint(\n\t\t\tp,\n\t\t\tQPoint(\n\t\t\t\tstatusx\n\t\t\t\t\t+ std::min(_status.maxWidth(), statusw)\n\t\t\t\t\t+ st::boostsListMiniIconSkip,\n\t\t\t\tstatusy + st::contactsStatusFont->descent),\n\t\t\touterWidth,\n\t\t\tst::lightButtonFg->c);\n\t}\n}\n\nvoid GiveawayTypeRow::addRadio(\n\t\tstd::shared_ptr<Ui::RadioenumGroup<Type>> typeGroup) {\n\tconst auto &st = st::defaultCheckbox;\n\tconst auto radio = Ui::CreateChild<Ui::Radioenum<Type>>(\n\t\tthis,\n\t\tstd::move(typeGroup),\n\t\t_type,\n\t\tQString(),\n\t\tst);\n\tconst auto pos = (_type == Type::SpecificUsers || _type == Type::Random)\n\t\t? st::giveawayRadioPosition\n\t\t: st::giveawayRadioMembersPosition;\n\tradio->moveToLeft(pos.x(), pos.y());\n\tradio->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tradio->show();\n}\n\n} // namespace Giveaway\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway_type_row.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/empty_userpic.h\"\n#include \"ui/widgets/buttons.h\"\n\nnamespace Ui {\ntemplate <typename Enum>\nclass RadioenumGroup;\n} // namespace Ui\n\nnamespace Giveaway {\n\nclass GiveawayTypeRow final : public Ui::RippleButton {\npublic:\n\tenum class Type {\n\t\tRandom,\n\t\tSpecificUsers,\n\n\t\tAllMembers,\n\t\tOnlyNewMembers,\n\n\t\tPrepaid,\n\t\tPrepaidCredits,\n\n\t\tCredits,\n\t};\n\n\tGiveawayTypeRow(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tType type,\n\t\trpl::producer<QString> subtitle,\n\t\tbool group);\n\n\tGiveawayTypeRow(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tType type,\n\t\tint colorIndex,\n\t\trpl::producer<QString> title,\n\t\trpl::producer<QString> subtitle,\n\t\tQImage badge);\n\n\tvoid addRadio(std::shared_ptr<Ui::RadioenumGroup<Type>> typeGroup);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tint resizeGetHeight(int) override;\n\nprivate:\n\tconst Type _type;\n\tconst style::PeerListItem _st;\n\n\tUi::EmptyUserpic _userpic;\n\tUi::Text::String _status;\n\tUi::Text::String _name;\n\n\tQImage _customUserpic;\n\n\tQImage _badge;\n\n};\n\n} // namespace Giveaway\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/select_countries_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/channel_statistics/boosts/giveaway/select_countries_box.h\"\n\n#include \"countries/countries_instance.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/multi_select.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Ui {\nnamespace {\n\n[[nodiscard]] QImage CacheFlagEmoji(const QString &flag) {\n\tconst auto &st = st::giveawayGiftCodeCountrySelect.item;\n\tauto roundPaintCache = QImage(\n\t\tSize(st.height) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\troundPaintCache.setDevicePixelRatio(style::DevicePixelRatio());\n\troundPaintCache.fill(Qt::transparent);\n\t{\n\t\tconst auto size = st.height;\n\t\tauto p = Painter(&roundPaintCache);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto flagText = Ui::Text::String(st::defaultTextStyle, flag);\n\t\tp.setPen(st.textBg);\n\t\tp.setBrush(st.textBg);\n\t\tp.drawEllipse(0, 0, size, size);\n\t\tflagText.draw(p, {\n\t\t\t.position = QPoint(\n\t\t\t\t0 + (size - flagText.maxWidth()) / 2,\n\t\t\t\t0 + (size - flagText.minHeight()) / 2),\n\t\t\t.outerWidth = size,\n\t\t\t.availableWidth = size,\n\t\t});\n\t}\n\treturn roundPaintCache;\n}\n\n} // namespace\n\nvoid SelectCountriesBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst std::vector<QString> &selected,\n\t\tFn<void(std::vector<QString>)> doneCallback,\n\t\tFn<bool(int)> checkErrorCallback) {\n\tstruct State final {\n\t\tstd::vector<QString> resultList;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\n\tconst auto multiSelect = box->setPinnedToTopContent(\n\t\tobject_ptr<Ui::MultiSelect>(\n\t\t\tbox,\n\t\t\tst::giveawayGiftCodeCountrySelect,\n\t\t\ttr::lng_participant_filter()));\n\tUi::AddSkip(box->verticalLayout());\n\tconst auto &buttonSt = st::giveawayGiftCodeCountryButton;\n\n\tstruct Entry final {\n\t\tUi::SlideWrap<Ui::SettingsButton> *wrap = nullptr;\n\t\tQStringList list;\n\t\tQString iso2;\n\t};\n\n\tauto countries = Countries::Instance().list();\n\tranges::sort(countries, [](\n\t\t\tconst Countries::Info &a,\n\t\t\tconst Countries::Info &b) {\n\t\treturn (a.name.compare(b.name, Qt::CaseInsensitive) < 0);\n\t});\n\tauto buttons = std::vector<Entry>();\n\tbuttons.reserve(countries.size());\n\tfor (const auto &country : countries) {\n\t\tconst auto flag = Countries::Instance().flagEmojiByISO2(country.iso2);\n\t\tif (!Ui::Emoji::Find(flag)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto itemId = buttons.size();\n\t\tauto button = object_ptr<SettingsButton>(\n\t\t\tbox->verticalLayout(),\n\t\t\trpl::single(flag + ' ' + country.name),\n\t\t\tbuttonSt);\n\t\tconst auto radio = Ui::CreateChild<Ui::RpWidget>(button.data());\n\t\tconst auto radioView = std::make_shared<Ui::RadioView>(\n\t\t\tst::defaultRadio,\n\t\t\tfalse,\n\t\t\t[=] { radio->update(); });\n\n\t\t{\n\t\t\tconst auto radioSize = radioView->getSize();\n\t\t\tradio->resize(radioSize);\n\t\t\tradio->paintRequest(\n\t\t\t) | rpl::on_next([=](const QRect &r) {\n\t\t\t\tauto p = QPainter(radio);\n\t\t\t\tradioView->paint(p, 0, 0, radioSize.width());\n\t\t\t}, radio->lifetime());\n\t\t\tconst auto buttonHeight = buttonSt.height\n\t\t\t\t+ rect::m::sum::v(buttonSt.padding);\n\t\t\tradio->moveToLeft(\n\t\t\t\tst::giveawayRadioPosition.x(),\n\t\t\t\t(buttonHeight - radioSize.height()) / 2);\n\t\t}\n\n\t\tconst auto roundPaintCache = CacheFlagEmoji(flag);\n\t\tconst auto paintCallback = [=](Painter &p, int x, int y, int, int) {\n\t\t\tp.drawImage(x, y, roundPaintCache);\n\t\t};\n\t\tconst auto choose = [=](bool clicked) {\n\t\t\tconst auto value = !radioView->checked();\n\t\t\tif (value && checkErrorCallback(state->resultList.size())) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tradioView->setChecked(value, anim::type::normal);\n\n\t\t\tif (value) {\n\t\t\t\tstate->resultList.push_back(country.iso2);\n\t\t\t\tmultiSelect->addItem(\n\t\t\t\t\titemId,\n\t\t\t\t\tcountry.name,\n\t\t\t\t\tst::activeButtonBg,\n\t\t\t\t\tpaintCallback,\n\t\t\t\t\tclicked\n\t\t\t\t\t\t? Ui::MultiSelect::AddItemWay::Default\n\t\t\t\t\t\t: Ui::MultiSelect::AddItemWay::SkipAnimation);\n\t\t\t} else {\n\t\t\t\tauto &list = state->resultList;\n\t\t\t\tlist.erase(ranges::remove(list, country.iso2), end(list));\n\t\t\t\tmultiSelect->removeItem(itemId);\n\t\t\t}\n\t\t};\n\t\tbutton->setClickedCallback([=] {\n\t\t\tchoose(true);\n\t\t});\n\t\tif (ranges::contains(selected, country.iso2)) {\n\t\t\tchoose(false);\n\t\t}\n\n\t\tconst auto wrap = box->verticalLayout()->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\t\tbox,\n\t\t\t\tstd::move(button)));\n\t\twrap->toggle(true, anim::type::instant);\n\n\t\t{\n\t\t\tauto list = QStringList{\n\t\t\t\tflag,\n\t\t\t\tcountry.name,\n\t\t\t\tcountry.alternativeName,\n\t\t\t};\n\t\t\tbuttons.push_back({ wrap, std::move(list), country.iso2 });\n\t\t}\n\t}\n\n\tconst auto noResults = box->addRow(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tbox,\n\t\t\tobject_ptr<Ui::VerticalLayout>(box)));\n\t{\n\t\tnoResults->toggle(false, anim::type::instant);\n\t\tconst auto container = noResults->entity();\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\tcontainer->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_search_messages_none(),\n\t\t\t\tst::membersAbout),\n\t\t\tstyle::al_top);\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t}\n\n\tmultiSelect->setQueryChangedCallback([=](const QString &query) {\n\t\tauto wasAnyFound = false;\n\t\tfor (const auto &entry : buttons) {\n\t\t\tconst auto found = ranges::any_of(entry.list, [&](\n\t\t\t\t\tconst QString &s) {\n\t\t\t\treturn s.startsWith(query, Qt::CaseInsensitive);\n\t\t\t});\n\t\t\tentry.wrap->toggle(found, anim::type::instant);\n\t\t\twasAnyFound |= found;\n\t\t}\n\t\tnoResults->toggle(!wasAnyFound, anim::type::instant);\n\t});\n\tmultiSelect->setItemRemovedCallback([=](uint64 itemId) {\n\t\tauto &list = state->resultList;\n\t\tauto &button = buttons[itemId];\n\t\tconst auto it = ranges::find(list, button.iso2);\n\t\tif (it != end(list)) {\n\t\t\tlist.erase(it);\n\t\t\tbutton.wrap->entity()->clicked({}, Qt::LeftButton);\n\t\t}\n\t});\n\n\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\tdoneCallback(state->resultList);\n\t\tbox->closeBox();\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/select_countries_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nclass GenericBox;\n\nvoid SelectCountriesBox(\n\tnot_null<Ui::GenericBox*> box,\n\tconst std::vector<QString> &selected,\n\tFn<void(std::vector<QString>)> doneCallback,\n\tFn<bool(int)> checkErrorCallback);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/boosts/info_boosts_inner_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/channel_statistics/boosts/info_boosts_inner_widget.h\"\n\n#include \"api/api_premium.h\"\n#include \"api/api_statistics.h\"\n#include \"boxes/gift_premium_box.h\"\n#include \"boxes/peers/edit_peer_invite_link.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"info/channel_statistics/boosts/create_giveaway_box.h\"\n#include \"info/channel_statistics/boosts/giveaway/boost_badge.h\"\n#include \"info/channel_statistics/boosts/giveaway/giveaway_type_row.h\"\n#include \"info/channel_statistics/boosts/info_boosts_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"info/statistics/info_statistics_inner_widget.h\" // FillLoading.\n#include \"info/statistics/info_statistics_list_controllers.h\"\n#include \"lang/lang_keys.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"statistics/widgets/chart_header_widget.h\"\n#include \"ui/boxes/boost_box.h\"\n#include \"ui/controls/invite_link_label.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/slider_natural_width.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_color_indices.h\"\n#include \"styles/style_dialogs.h\" // dialogsSearchTabs\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_statistics.h\"\n\n#include <QtGui/QGuiApplication>\n\nnamespace Info::Boosts {\nnamespace {\n\nvoid AddHeader(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\ttr::phrase<> text) {\n\tconst auto header = content->add(\n\t\tobject_ptr<Statistic::Header>(content),\n\t\tst::statisticsLayerMargins + st::boostsChartHeaderPadding);\n\theader->resizeToWidth(header->width());\n\theader->setTitle(text(tr::now));\n\theader->setSubTitle({});\n}\n\nvoid FillOverview(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tconst Data::BoostStatus &status) {\n\tconst auto &stats = status.overview;\n\n\tUi::AddSkip(content, st::boostsLayerOverviewMargins.top());\n\tAddHeader(content, tr::lng_stats_overview_title);\n\tUi::AddSkip(content);\n\n\tconst auto diffBetweenHeaders = 0\n\t\t+ st::statisticsOverviewValue.style.font->height\n\t\t- st::statisticsHeaderTitleTextStyle.font->height;\n\n\tconst auto container = content->add(\n\t\tobject_ptr<Ui::RpWidget>(content),\n\t\tst::statisticsLayerMargins);\n\n\tconst auto addPrimary = [&](float64 v, bool approximately = false) {\n\t\treturn Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\t(v >= 0)\n\t\t\t\t? (approximately && v ? QChar(0x2248) : QChar())\n\t\t\t\t\t+ Lang::FormatCountToShort(v).string\n\t\t\t\t: QString(),\n\t\t\tst::statisticsOverviewValue);\n\t};\n\tconst auto addSub = [&](\n\t\t\tnot_null<Ui::RpWidget*> primary,\n\t\t\tfloat64 percentage,\n\t\t\ttr::phrase<> text) {\n\t\tconst auto second = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\tpercentage\n\t\t\t\t? u\"%1%\"_q.arg(std::abs(std::round(percentage * 10.) / 10.))\n\t\t\t\t: QString(),\n\t\t\tst::statisticsOverviewSecondValue);\n\t\tsecond->setTextColorOverride(st::windowSubTextFg->c);\n\t\tconst auto sub = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttext(),\n\t\t\tst::statisticsOverviewSubtext);\n\t\tsub->setTextColorOverride(st::windowSubTextFg->c);\n\n\t\tprimary->geometryValue(\n\t\t) | rpl::on_next([=](const QRect &g) {\n\t\t\tconst auto &padding = st::statisticsOverviewSecondValuePadding;\n\t\t\tsecond->moveToLeft(\n\t\t\t\trect::right(g) + padding.left(),\n\t\t\t\tg.y() + padding.top());\n\t\t\tsub->moveToLeft(\n\t\t\t\tg.x(),\n\t\t\t\tst::statisticsChartHeaderHeight\n\t\t\t\t\t- st::statisticsOverviewSubtext.style.font->height\n\t\t\t\t\t+ g.y()\n\t\t\t\t\t+ diffBetweenHeaders);\n\t\t}, primary->lifetime());\n\t};\n\n\n\tconst auto topLeftLabel = addPrimary(stats.level);\n\tconst auto topRightLabel = addPrimary(stats.premiumMemberCount, true);\n\tconst auto bottomLeftLabel = addPrimary(stats.boostCount);\n\tconst auto bottomRightLabel = addPrimary(std::max(\n\t\tstats.nextLevelBoostCount - stats.boostCount,\n\t\t0));\n\n\taddSub(\n\t\ttopLeftLabel,\n\t\t0,\n\t\ttr::lng_boosts_level);\n\taddSub(\n\t\ttopRightLabel,\n\t\tstats.premiumMemberPercentage,\n\t\t(stats.group\n\t\t\t? tr::lng_boosts_premium_members\n\t\t\t: tr::lng_boosts_premium_audience));\n\taddSub(\n\t\tbottomLeftLabel,\n\t\t0,\n\t\ttr::lng_boosts_existing);\n\taddSub(\n\t\tbottomRightLabel,\n\t\t0,\n\t\ttr::lng_boosts_next_level);\n\n\tcontainer->showChildren();\n\tcontainer->resize(container->width(), topLeftLabel->height() * 5);\n\tcontainer->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tconst auto halfWidth = s.width() / 2;\n\t\t{\n\t\t\tconst auto &p = st::boostsOverviewValuePadding;\n\t\t\ttopLeftLabel->moveToLeft(p.left(), p.top());\n\t\t}\n\t\ttopRightLabel->moveToLeft(\n\t\t\ttopLeftLabel->x() + halfWidth + st::statisticsOverviewRightSkip,\n\t\t\ttopLeftLabel->y());\n\t\tbottomLeftLabel->moveToLeft(\n\t\t\ttopLeftLabel->x(),\n\t\t\ttopLeftLabel->y() + st::statisticsOverviewMidSkip);\n\t\tbottomRightLabel->moveToLeft(\n\t\t\ttopRightLabel->x(),\n\t\t\tbottomLeftLabel->y());\n\t}, container->lifetime());\n\tUi::AddSkip(content, st::boostsLayerOverviewMargins.bottom());\n}\n\nvoid FillShareLink(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tconst QString &link,\n\t\tnot_null<PeerData*> peer) {\n\tconst auto weak = base::make_weak(content);\n\tconst auto copyLink = crl::guard(weak, [=] {\n\t\tQGuiApplication::clipboard()->setText(link);\n\t\tshow->showToast(tr::lng_channel_public_link_copied(tr::now));\n\t});\n\tconst auto shareLink = crl::guard(weak, [=] {\n\t\tshow->showBox(ShareInviteLinkBox(peer, link));\n\t});\n\n\tconst auto label = content->lifetime().make_state<Ui::InviteLinkLabel>(\n\t\tcontent,\n\t\trpl::single(base::duplicate(link).replace(u\"https://\"_q, QString())),\n\t\tnullptr);\n\tcontent->add(\n\t\tlabel->take(),\n\t\tst::boostsLinkFieldPadding);\n\n\tlabel->clicks(\n\t) | rpl::on_next(copyLink, label->lifetime());\n\t{\n\t\tconst auto wrap = content->add(\n\t\t\tobject_ptr<Ui::FixedHeightWidget>(\n\t\t\t\tcontent,\n\t\t\t\tst::inviteLinkButton.height),\n\t\t\tst::inviteLinkButtonsPadding);\n\t\tconst auto copy = CreateChild<Ui::RoundButton>(\n\t\t\twrap,\n\t\t\ttr::lng_group_invite_context_copy(),\n\t\t\tst::inviteLinkCopy);\n\t\tcopy->setClickedCallback(copyLink);\n\t\tconst auto share = CreateChild<Ui::RoundButton>(\n\t\t\twrap,\n\t\t\ttr::lng_group_invite_context_share(),\n\t\t\tst::inviteLinkShare);\n\t\tshare->setClickedCallback(shareLink);\n\n\t\twrap->widthValue(\n\t\t) | rpl::on_next([=](int width) {\n\t\t\tconst auto buttonWidth = (width - st::inviteLinkButtonsSkip) / 2;\n\t\t\tcopy->setFullWidth(buttonWidth);\n\t\t\tshare->setFullWidth(buttonWidth);\n\t\t\tcopy->moveToLeft(0, 0, width);\n\t\t\tshare->moveToRight(0, 0, width);\n\t\t}, wrap->lifetime());\n\t\twrap->showChildren();\n\t}\n\tUi::AddSkip(content, st::boostsLinkFieldPadding.bottom());\n}\n\nvoid FillGetBoostsButton(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tnot_null<Controller*> controller,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> reloadOnDone) {\n\tif (!Api::PremiumGiftCodeOptions(peer).giveawayGiftsPurchaseAvailable()) {\n\t\treturn;\n\t}\n\tUi::AddSkip(content);\n\tconst auto &st = st::getBoostsButton;\n\tconst auto &icon = st::getBoostsButtonIcon;\n\tconst auto button = content->add(object_ptr<Ui::SettingsButton>(\n\t\tcontent.get(),\n\t\ttr::lng_boosts_get_boosts(),\n\t\tst));\n\tbutton->setClickedCallback([=] {\n\t\tshow->showBox(Box(\n\t\t\tCreateGiveawayBox,\n\t\t\tcontroller,\n\t\t\tpeer,\n\t\t\treloadOnDone,\n\t\t\tstd::nullopt));\n\t});\n\tUi::CreateChild<Info::Profile::FloatingIcon>(\n\t\tbutton,\n\t\ticon,\n\t\tQPoint{\n\t\t\tst::infoSharedMediaButtonIconPosition.x(),\n\t\t\t(st.height + rect::m::sum::v(st.padding) - icon.height()) / 2,\n\t\t})->show();\n\tUi::AddSkip(content);\n\tUi::AddDividerText(\n\t\tcontent,\n\t\tpeer->isMegagroup()\n\t\t\t? tr::lng_boosts_get_boosts_subtext_group()\n\t\t\t: tr::lng_boosts_get_boosts_subtext());\n}\n\n} // namespace\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller,\n\tnot_null<PeerData*> peer)\n: VerticalLayout(parent)\n, _controller(controller)\n, _peer(peer)\n, _show(controller->uiShow()) {\n}\n\nvoid InnerWidget::load() {\n\tconst auto api = lifetime().make_state<Api::Boosts>(_peer);\n\n\tInfo::Statistics::FillLoading(\n\t\tthis,\n\t\tInfo::Statistics::LoadingType::Boosts,\n\t\t_loaded.events_starting_with(false) | rpl::map(!rpl::mappers::_1),\n\t\t_showFinished.events());\n\n\t_showFinished.events(\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\tapi->request(\n\t\t) | rpl::on_error_done([](const QString &error) {\n\t\t}, [=] {\n\t\t\t_state = api->boostStatus();\n\t\t\t_loaded.fire(true);\n\t\t\tfill();\n\t\t}, lifetime());\n\t}, lifetime());\n}\n\nvoid InnerWidget::fill() {\n\tconst auto fakeShowed = lifetime().make_state<rpl::event_stream<>>();\n\tconst auto &status = _state;\n\tconst auto inner = this;\n\n\tconst auto reloadOnDone = crl::guard(this, [=] {\n\t\twhile (Ui::VerticalLayout::count()) {\n\t\t\tdelete Ui::VerticalLayout::widgetAt(0);\n\t\t}\n\t\tload();\n\t\t_showFinished.fire({});\n\t});\n\n\t{\n\t\tauto dividerContent = object_ptr<Ui::VerticalLayout>(inner);\n\t\tdividerContent->add(object_ptr<Ui::FixedHeightWidget>(\n\t\t\tdividerContent,\n\t\t\tst::boostSkipTop));\n\t\tUi::FillBoostLimit(\n\t\t\tfakeShowed->events(),\n\t\t\tdividerContent.data(),\n\t\t\trpl::single(Ui::BoostCounters{\n\t\t\t\t.level = status.overview.level,\n\t\t\t\t.boosts = status.overview.boostCount,\n\t\t\t\t.thisLevelBoosts\n\t\t\t\t\t= status.overview.currentLevelBoostCount,\n\t\t\t\t.nextLevelBoosts\n\t\t\t\t\t= status.overview.nextLevelBoostCount,\n\t\t\t\t.mine = status.overview.mine,\n\t\t\t}),\n\t\t\tst::statisticsLimitsLinePadding);\n\t\tinner->add(object_ptr<Ui::DividerLabel>(\n\t\t\tinner,\n\t\t\tstd::move(dividerContent),\n\t\t\tst::statisticsLimitsDividerPadding));\n\t}\n\n\tFillOverview(inner, status);\n\n\tUi::AddSkip(inner);\n\tUi::AddDivider(inner);\n\tUi::AddSkip(inner);\n\n\tif (!status.prepaidGiveaway.empty()) {\n\t\tconst auto multiplier = Api::PremiumGiftCodeOptions(_peer)\n\t\t\t.giveawayBoostsPerPremium();\n\t\tUi::AddSkip(inner);\n\t\tAddHeader(inner, tr::lng_boosts_prepaid_giveaway_title);\n\t\tUi::AddSkip(inner);\n\t\tfor (const auto &g : status.prepaidGiveaway) {\n\t\t\tusing namespace Giveaway;\n\t\t\tconst auto button = inner->add(object_ptr<GiveawayTypeRow>(\n\t\t\t\tinner,\n\t\t\t\tg.credits\n\t\t\t\t\t? GiveawayTypeRow::Type::PrepaidCredits\n\t\t\t\t\t: GiveawayTypeRow::Type::Prepaid,\n\t\t\t\tg.credits ? st::colorIndexOrange : g.id,\n\t\t\t\tg.credits\n\t\t\t\t\t? tr::lng_boosts_prepaid_giveaway_single()\n\t\t\t\t\t: tr::lng_boosts_prepaid_giveaway_quantity(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single(g.quantity) | tr::to_count()),\n\t\t\t\tg.credits\n\t\t\t\t\t? tr::lng_boosts_prepaid_giveaway_credits_status(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single(g.quantity) | tr::to_count(),\n\t\t\t\t\t\tlt_amount,\n\t\t\t\t\t\ttr::lng_prize_credits_amount(\n\t\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\t\trpl::single(g.credits) | tr::to_count()))\n\t\t\t\t\t: tr::lng_boosts_prepaid_giveaway_moths(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single(g.months) | tr::to_count()),\n\t\t\t\tInfo::Statistics::CreateBadge(\n\t\t\t\t\tst::statisticsDetailsBottomCaptionStyle,\n\t\t\t\t\tQString::number(\n\t\t\t\t\t\tg.boosts ? g.boosts : (g.quantity * multiplier)),\n\t\t\t\t\tst::boostsListBadgeHeight,\n\t\t\t\t\tst::boostsListBadgeTextPadding,\n\t\t\t\t\tst::premiumButtonBg2,\n\t\t\t\t\tst::premiumButtonFg,\n\t\t\t\t\t1.,\n\t\t\t\t\tst::boostsListMiniIconPadding,\n\t\t\t\t\tst::boostsListMiniIcon)));\n\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\t_controller->uiShow()->showBox(Box(\n\t\t\t\t\tCreateGiveawayBox,\n\t\t\t\t\t_controller,\n\t\t\t\t\t_peer,\n\t\t\t\t\treloadOnDone,\n\t\t\t\t\tg));\n\t\t\t});\n\t\t}\n\n\t\tUi::AddSkip(inner);\n\t\tUi::AddDividerText(\n\t\t\tinner,\n\t\t\ttr::lng_boosts_prepaid_giveaway_title_subtext());\n\t\tUi::AddSkip(inner);\n\t}\n\n\tconst auto hasBoosts = (status.firstSliceBoosts.multipliedTotal > 0);\n\tconst auto hasGifts = (status.firstSliceGifts.multipliedTotal > 0);\n\tif (hasBoosts || hasGifts) {\n\t\tauto boostClicked = [=](const Data::Boost &boost) {\n\t\t\tif (!boost.giftCodeLink.slug.isEmpty()) {\n\t\t\t\tResolveGiftCode(_controller, boost.giftCodeLink.slug);\n\t\t\t} else if (boost.userId) {\n\t\t\t\tconst auto user = _peer->owner().user(boost.userId);\n\t\t\t\tif (boost.isGift || boost.isGiveaway) {\n\t\t\t\t\tconst auto d = Api::GiftCode{\n\t\t\t\t\t\t.from = _peer->id,\n\t\t\t\t\t\t.to = user->id,\n\t\t\t\t\t\t.date = TimeId(boost.date.toSecsSinceEpoch()),\n\t\t\t\t\t\t.days = boost.expiresAfterMonths * 30,\n\t\t\t\t\t};\n\t\t\t\t\t_show->showBox(Box(GiftCodePendingBox, _controller, d));\n\t\t\t\t} else {\n\t\t\t\t\tcrl::on_main(this, [=] {\n\t\t\t\t\t\t_controller->showPeerInfo(user);\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else if (boost.credits) {\n\t\t\t\t_show->showBox(\n\t\t\t\t\tBox(\n\t\t\t\t\t\t::Settings::BoostCreditsBox,\n\t\t\t\t\t\t_controller->parentController(),\n\t\t\t\t\t\tboost));\n\t\t\t} else if (!boost.isUnclaimed) {\n\t\t\t\t_show->showToast(tr::lng_boosts_list_pending_about(tr::now));\n\t\t\t}\n\t\t};\n\n#ifdef _DEBUG\n\t\tconst auto hasOneTab = false;\n#else\n\t\tconst auto hasOneTab = (hasBoosts != hasGifts);\n#endif\n\t\tconst auto boostsTabText = tr::lng_giveaway_quantity(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tstatus.firstSliceBoosts.multipliedTotal);\n\t\tconst auto giftsTabText = tr::lng_boosts_list_tab_gifts(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tstatus.firstSliceGifts.multipliedTotal);\n\t\tif (hasOneTab) {\n\t\t\tUi::AddSkip(inner);\n\t\t\tconst auto header = inner->add(\n\t\t\t\tobject_ptr<Statistic::Header>(inner),\n\t\t\t\tst::statisticsLayerMargins\n\t\t\t\t\t+ st::boostsChartHeaderPadding);\n\t\t\theader->resizeToWidth(header->width());\n\t\t\theader->setTitle(hasBoosts ? boostsTabText : giftsTabText);\n\t\t\theader->setSubTitle({});\n\t\t}\n\n\t\tconst auto slider = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::CustomWidthSlider>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::CustomWidthSlider>(\n\t\t\t\t\tinner,\n\t\t\t\t\tst::dialogsSearchTabs)));\n\t\tif (!hasOneTab) {\n\t\t\tconst auto shadow = Ui::CreateChild<Ui::PlainShadow>(inner);\n\t\t\tshadow->show();\n\t\t\tslider->geometryValue(\n\t\t\t) | rpl::on_next([=](const QRect &r) {\n\t\t\t\tshadow->setGeometry(\n\t\t\t\t\tinner->x(),\n\t\t\t\t\trect::bottom(r) - shadow->height(),\n\t\t\t\t\tinner->width(),\n\t\t\t\t\tshadow->height());\n\t\t\t}, shadow->lifetime());\n\t\t}\n\t\tslider->toggle(!hasOneTab, anim::type::instant);\n\n\t\tslider->entity()->addSection(boostsTabText);\n\t\tslider->entity()->addSection(giftsTabText);\n\n\t\t{\n\t\t\tconst auto &st = st::defaultTabsSlider;\n\t\t\tslider->entity()->setNaturalWidth(0\n\t\t\t\t+ st.labelStyle.font->width(boostsTabText)\n\t\t\t\t+ st.labelStyle.font->width(giftsTabText)\n\t\t\t\t+ rect::m::sum::h(st::boxRowPadding));\n\t\t}\n\n\t\tconst auto boostsWrap = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\t\tconst auto giftsWrap = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\n\t\trpl::single(hasOneTab ? (hasGifts ? 1 : 0) : 0) | rpl::then(\n\t\t\tslider->entity()->sectionActivated()\n\t\t) | rpl::on_next([=](int index) {\n\t\t\tboostsWrap->toggle(!index, anim::type::instant);\n\t\t\tgiftsWrap->toggle(index, anim::type::instant);\n\t\t}, inner->lifetime());\n\n\t\tStatistics::AddBoostsList(\n\t\t\tstatus.firstSliceBoosts,\n\t\t\tboostsWrap->entity(),\n\t\t\tboostClicked,\n\t\t\t_peer,\n\t\t\ttr::lng_boosts_title());\n\t\tStatistics::AddBoostsList(\n\t\t\tstatus.firstSliceGifts,\n\t\t\tgiftsWrap->entity(),\n\t\t\tstd::move(boostClicked),\n\t\t\t_peer,\n\t\t\ttr::lng_boosts_title());\n\n\t\tUi::AddSkip(inner);\n\t\tUi::AddSkip(inner);\n\t\tUi::AddDividerText(inner, status.overview.group\n\t\t\t? tr::lng_boosts_list_subtext_group()\n\t\t\t: tr::lng_boosts_list_subtext());\n\t}\n\n\tUi::AddSkip(inner);\n\tUi::AddSkip(inner);\n\tAddHeader(inner, tr::lng_boosts_link_title);\n\tUi::AddSkip(inner, st::boostsLinkSkip);\n\tFillShareLink(inner, _show, status.link, _peer);\n\tUi::AddSkip(inner);\n\tUi::AddDividerText(inner, status.overview.group\n\t\t? tr::lng_boosts_link_subtext_group()\n\t\t: tr::lng_boosts_link_subtext());\n\n\tFillGetBoostsButton(inner, _controller, _show, _peer, reloadOnDone);\n\n\tresizeToWidth(width());\n\tcrl::on_main(this, [=]{ fakeShowed->fire({}); });\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\tmemento->setState(base::take(_state));\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\t_state = memento->state();\n\tif (!_state.link.isEmpty()) {\n\t\tfill();\n\t} else {\n\t\tload();\n\t}\n\tUi::RpWidget::resizeToWidth(width());\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nauto InnerWidget::showRequests() const -> rpl::producer<ShowRequest> {\n\treturn _showRequests.events();\n}\n\nvoid InnerWidget::showFinished() {\n\t_showFinished.fire({});\n}\n\nnot_null<PeerData*> InnerWidget::peer() const {\n\treturn _peer;\n}\n\n} // namespace Info::Boosts\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/boosts/info_boosts_inner_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_boosts.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/vertical_layout.h\"\n\nnamespace Ui {\nclass Show;\n} // namespace Ui\n\nnamespace Info {\nclass Controller;\n} // namespace Info\n\nnamespace Info::Boosts {\n\nclass Memento;\n\nclass InnerWidget final : public Ui::VerticalLayout {\npublic:\n\tstruct ShowRequest final {\n\t};\n\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<PeerData*> peer);\n\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\n\t[[nodiscard]] rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\t[[nodiscard]] rpl::producer<ShowRequest> showRequests() const;\n\n\tvoid showFinished();\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\nprivate:\n\tvoid load();\n\tvoid fill();\n\n\tnot_null<Controller*> _controller;\n\tnot_null<PeerData*> _peer;\n\tstd::shared_ptr<Ui::Show> _show;\n\n\tData::BoostStatus _state;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\trpl::event_stream<ShowRequest> _showRequests;\n\trpl::event_stream<> _showFinished;\n\trpl::event_stream<bool> _loaded;\n\n};\n\n} // namespace Info::Boosts\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/boosts/info_boosts_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/channel_statistics/boosts/info_boosts_widget.h\"\n\n#include \"info/channel_statistics/boosts/info_boosts_inner_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/ui_utility.h\"\n\nnamespace Info::Boosts {\n\nMemento::Memento(not_null<Controller*> controller)\n: ContentMemento(controller->statisticsTag()) {\n}\n\nMemento::Memento(not_null<PeerData*> peer)\n: ContentMemento(Info::Statistics::Tag{ peer, {}, {} }) {\n}\n\nMemento::~Memento() = default;\n\nSection Memento::section() const {\n\treturn Section(Section::Type::Boosts);\n}\n\nvoid Memento::setState(SavedState state) {\n\t_state = std::move(state);\n}\n\nMemento::SavedState Memento::state() {\n\treturn base::take(_state);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(parent, controller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: ContentWidget(parent, controller)\n, _inner(setInnerWidget(\n\tobject_ptr<InnerWidget>(\n\t\tthis,\n\t\tcontroller,\n\t\tcontroller->statisticsTag().peer))) {\n\t_inner->showRequests(\n\t) | rpl::on_next([=](InnerWidget::ShowRequest request) {\n\t}, _inner->lifetime());\n\t_inner->scrollToRequests(\n\t) | rpl::on_next([=](const Ui::ScrollToRequest &request) {\n\t\tscrollTo(request);\n\t}, _inner->lifetime());\n}\n\nnot_null<PeerData*> Widget::peer() const {\n\treturn _inner->peer();\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\treturn (memento->statisticsTag().peer == peer());\n}\n\nrpl::producer<QString> Widget::title() {\n\treturn tr::lng_boosts_title();\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nrpl::producer<bool> Widget::desiredShadowVisibility() const {\n\treturn rpl::single<bool>(true);\n}\n\nvoid Widget::showFinished() {\n\t_inner->showFinished();\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(controller());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\nstd::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer) {\n\treturn std::make_shared<Info::Memento>(\n\t\tstd::vector<std::shared_ptr<ContentMemento>>(\n\t\t\t1,\n\t\t\tstd::make_shared<Memento>(peer)));\n}\n\n} // namespace Info::Boosts\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/boosts/info_boosts_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_boosts.h\"\n#include \"info/info_content_widget.h\"\n\nnamespace Info::Boosts {\n\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\tMemento(not_null<Controller*> controller);\n\tMemento(not_null<PeerData*> peer);\n\t~Memento();\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\tusing SavedState = Data::BoostStatus;\n\n\tvoid setState(SavedState states);\n\t[[nodiscard]] SavedState state();\n\nprivate:\n\tSavedState _state;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(QWidget *parent, not_null<Controller*> controller);\n\n\tbool showInternal(not_null<ContentMemento*> memento) override;\n\trpl::producer<QString> title() override;\n\trpl::producer<bool> desiredShadowVisibility() const override;\n\tvoid showFinished() override;\n\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tconst not_null<InnerWidget*> _inner;\n\n};\n\n[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer);\n\n} // namespace Info::Boosts\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\nusing \"boxes/boxes.style\";\nusing \"statistics/statistics.style\";\n\nchannelEarnLearnArrowMargins: margins(-2px, 5px, 0px, 0px);\n\nchannelEarnOverviewTitleSkip: 11px;\nchannelEarnOverviewMajorLabel: FlatLabel(defaultFlatLabel) {\n\tmaxHeight: 30px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(15px semibold);\n\t}\n}\nchannelEarnOverviewMinorLabel: FlatLabel(channelEarnOverviewMajorLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px semibold);\n\t}\n}\nchannelEarnOverviewMinorLabelSkip: 3px;\nchannelEarnOverviewSubMinorLabel: FlatLabel(channelEarnOverviewMinorLabel) {\n\ttextFg: windowSubTextFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(13px);\n\t}\n}\nchannelEarnOverviewSubMinorLabelPos: point(4px, 2px);\nchannelEarnSemiboldLabel: FlatLabel(channelEarnOverviewMajorLabel) {\n\tstyle: semiboldTextStyle;\n}\nchannelEarnHeaderLabel: FlatLabel(channelEarnOverviewMajorLabel) {\n\tstyle: TextStyle(statisticsHeaderTitleTextStyle) {\n\t}\n\ttextFg: windowActiveTextFg;\n}\nchannelEarnHistorySubLabel: FlatLabel(channelEarnOverviewSubMinorLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px);\n\t}\n}\nchannelEarnHistoryRecipientLabel: FlatLabel(channelEarnOverviewSubMinorLabel) {\n\tmaxHeight: 0px;\n\tminWidth: 100px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px);\n\t}\n}\nchannelEarnHistoryMajorLabel: FlatLabel(channelEarnOverviewMajorLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px semibold);\n\t}\n}\nchannelEarnHistoryMinorLabel: FlatLabel(channelEarnOverviewMinorLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px semibold);\n\t}\n}\nchannelEarnHistoryDescriptionLabel: FlatLabel(channelEarnHistoryMajorLabel) {\n\t// boxWidth - boxRowPadding = 320 - 24 * 2\n\tminWidth: 272px;\n\tmaxHeight: 0px;\n\talign: align(center);\n}\nchannelEarnHistoryMinorLabelSkip: 2px;\nchannelEarnHistoryOuter: margins(0px, 6px, 0px, 6px);\nchannelEarnHistoryTwoSkip: 5px;\nchannelEarnHistoryThreeSkip: 3px;\n\nchannelEarnHistoryRecipientButton: RoundButton {\n\ttextFg: transparent;\n\ttextFgOver: transparent;\n\tnumbersTextFg: transparent;\n\tnumbersTextFgOver: transparent;\n\ttextBg: windowBgOver;\n\ttextBgOver: windowBgOver;\n\n\tnumbersSkip: 7px;\n\n\twidth: 190px;\n\theight: 34px;\n\tpadding: margins(0px, 0px, 0px, 0px);\n\n\ttextTop: 8px;\n\tradius: 8px;\n\n\ticonPosition: point(0px, 0px);\n\n\tstyle: semiboldTextStyle;\n\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: windowBgRipple;\n\t}\n}\nchannelEarnHistoryRecipientButtonLabel: FlatLabel(channelEarnHistoryRecipientLabel) {\n\talign: align(center);\n}\n\nchannelEarnBalanceMajorLabel: FlatLabel(channelEarnOverviewMajorLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(22px semibold);\n\t}\n}\nchannelEarnBalanceMinorLabel: FlatLabel(channelEarnOverviewMinorLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(16px semibold);\n\t}\n}\nchannelEarnBalanceMinorLabelSkip: 6px;\nchannelEarnFadeDuration: 60;\n\nchannelEarnLearnDescription: FlatLabel(defaultFlatLabel) {\n\tmaxHeight: 0px;\n\tminWidth: 264px;\n\talign: align(top);\n}\n\nchannelEarnCurrencyCommonMargins: margins(0px, 3px, 1px, 0px);\nchannelEarnCurrencyLearnMargins: margins(0px, 2px, 0px, 0px);\n\nsponsoredAboutTitleIcon: icon {{ \"sponsored/large_about\", activeButtonFg }};\nsponsoredAboutPrivacyIcon: icon {{ \"sponsored/privacy_about\", boxTextFg }};\nsponsoredAboutRemoveIcon: icon {{ \"sponsored/remove_about\", boxTextFg }};\nsponsoredAboutSplitIcon: icon {{ \"sponsored/revenue_split\", boxTextFg }};\n\nchannelEarnLearnTitleIcon: icon {{ \"sponsored/large_earn\", activeButtonFg }};\nchannelEarnLearnChannelIcon: icon {{ \"sponsored/channel\", boxTextFg }};\nchannelEarnLearnWithdrawalsIcon: icon {{ \"sponsored/withdrawals\", boxTextFg }};\n\nsponsoredReportLabel: FlatLabel(defaultFlatLabel) {\n\tstyle: boxTextStyle;\n\tminWidth: 150px;\n}\n\nbotEarnInputField: InputField(defaultInputField) {\n\ttextMargins: margins(23px, 28px, 0px, 4px);\n\tplaceholderMargins: margins(-23px, 0px, 0px, 0px);\n\twidth: 100px;\n\theightMax: 55px;\n}\nbotEarnLockedButtonLabel: FlatLabel(channelEarnOverviewMajorLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(10px semibold);\n\t}\n}\nbotEarnButtonLock: IconEmoji {\n\ticon: icon{{ \"chat/mini_lock\", premiumButtonFg }};\n\tpadding: margins(-2px, 4px, 0px, 0px);\n}\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/channel_statistics/earn/earn_format.h\"\n\n#include <QtCore/QLocale>\n\nnamespace Info::ChannelEarn {\nnamespace {\n\nconstexpr auto kMinorPartLength = 9;\nconstexpr auto kMaxChoppedZero = kMinorPartLength - 2;\nconstexpr auto kZero = QChar('0');\nconst auto DecimalPoint = QString() + QLocale().decimalPoint();\n\nusing EarnInt = Data::EarnInt;\n\n} // namespace\n\nQString MajorPart(EarnInt value) {\n\tconst auto string = QString::number(value);\n\tconst auto diff = int(string.size()) - kMinorPartLength;\n\treturn (diff <= 0) ? QString(kZero) : string.mid(0, diff);\n}\n\nQString MajorPart(CreditsAmount value) {\n\treturn QString::number(int64(value.value()));\n}\n\nQString MinorPart(EarnInt value) {\n\tif (!value) {\n\t\treturn DecimalPoint + kZero + kZero;\n\t}\n\tconst auto string = QString::number(value);\n\tconst auto diff = int(string.size()) - kMinorPartLength;\n\tconst auto result = (diff < 0)\n\t\t? DecimalPoint + u\"%1\"_q.arg(0, std::abs(diff), 10, kZero) + string\n\t\t: DecimalPoint + string.mid(diff);\n\tconst auto begin = (result.constData());\n\tconst auto end = (begin + result.size());\n\tauto ch = end - 1;\n\tauto zeroCount = 0;\n\twhile (ch != begin) {\n\t\tif (((*ch) == kZero) && (zeroCount < kMaxChoppedZero)) {\n\t\t\tzeroCount++;\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t\tch--;\n\t}\n\treturn result.chopped(zeroCount);\n}\n\nQString MinorPart(CreditsAmount value) {\n\tstatic const int DecimalPointLength = DecimalPoint.length();\n\n\tconst auto fractional = std::abs(int(value.value() * 100)) % 100;\n\tauto result = QString(DecimalPointLength + 2, Qt::Uninitialized);\n\n\tfor (int i = 0; i < DecimalPointLength; ++i) {\n\t\tresult[i] = DecimalPoint[i];\n\t}\n\n\tresult[DecimalPointLength] = QChar('0' + fractional / 10);\n\tresult[DecimalPointLength + 1] = QChar('0' + fractional % 10);\n\n\treturn result;\n}\n\nQString ToUsd(\n\t\tData::EarnInt value,\n\t\tfloat64 rate,\n\t\tint afterFloat) {\n\treturn ToUsd(CreditsAmount(value), rate, afterFloat);\n}\n\nQString ToUsd(\n\t\tCreditsAmount value,\n\t\tfloat64 rate,\n\t\tint afterFloat) {\n\tconstexpr auto kApproximately = QChar(0x2248);\n\n\treturn QString(kApproximately)\n\t\t+ QChar('$')\n\t\t+ QLocale().toString(\n\t\t\tvalue.value() * rate,\n\t\t\t'f',\n\t\t\tafterFloat ? afterFloat : 2);\n}\n\n} // namespace Info::ChannelEarn\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/earn/earn_format.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_channel_earn.h\"\n\nnamespace Info::ChannelEarn {\n\n[[nodiscard]] QString MajorPart(Data::EarnInt value);\n[[nodiscard]] QString MajorPart(CreditsAmount value);\n[[nodiscard]] QString MinorPart(Data::EarnInt value);\n[[nodiscard]] QString MinorPart(CreditsAmount value);\n[[nodiscard]] QString ToUsd(\n\tData::EarnInt value,\n\tfloat64 rate,\n\tint afterFloat);\n[[nodiscard]] QString ToUsd(\n\tCreditsAmount value,\n\tfloat64 rate,\n\tint afterFloat);\n\n} // namespace Info::ChannelEarn\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/channel_statistics/earn/earn_icons.h\"\n\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/text/custom_emoji_instance.h\"\n#include \"ui/rect.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_info.h\" // infoIconReport.\n\n#include <QFile>\n#include <QtSvg/QSvgRenderer>\n\nnamespace Ui::Earn {\nnamespace {\n\n[[nodiscard]] QByteArray CurrencySvg(const QColor &c) {\n\tconst auto color = u\"rgb(%1,%2,%3)\"_q\n\t\t.arg(c.red())\n\t\t.arg(c.green())\n\t\t.arg(c.blue())\n\t\t.toUtf8();\n\treturn R\"(\n<svg width=\"72px\" height=\"72px\" viewBox=\"0 0 72 72\">\n    <g stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\">\n        <g transform=\"translate(9.000000, 14.000000)\n        \" stroke-width=\"7.2\" stroke=\")\" + color + R\"(\">\n            <path d=\"M2.96014341,0 L50.9898193,0 C51.9732032,-7.06402744e-15\n 52.7703933,0.797190129 52.7703933,1.78057399 C52.7703933,2.08038611\n 52.6946886,2.3753442 52.5502994,2.63809702 L29.699977,44.2200383\n C28.7527832,45.9436969 26.5876295,46.5731461 24.8639708,45.6259523\n C24.2556953,45.2916896 23.7583564,44.7869606 23.4331014,44.1738213\n L1.38718565,2.61498853 C0.926351231,1.74626794 1.25700829,0.668450654\n 2.12572888,0.20761623 C2.38272962,0.0712838007 2.6692209,4.97530809e-16\n 2.96014341,0 Z\"></path>\n            <line x1=\"27\" y1=\"44.4532875\" x2=\"27\" y2=\"0\"></line>\n        </g>\n    </g>\n</svg>)\";\n}\n\n} // namespace\n\nQImage IconCurrencyColored(int size, const QColor &c) {\n\tconst auto s = Size(size);\n\tauto svg = QSvgRenderer(CurrencySvg(c));\n\tauto image = QImage(\n\t\ts * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\timage.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&image);\n\t\tsvg.render(&p, Rect(s));\n\t}\n\treturn image;\n}\n\nQImage IconCurrencyColored(\n\t\tconst style::font &font,\n\t\tconst QColor &c) {\n\treturn IconCurrencyColored(font->ascent, c);\n}\n\nQByteArray CurrencySvgColored(const QColor &c) {\n\treturn CurrencySvg(c);\n}\n\nQImage MenuIconCurrency(const QSize &size) {\n\tauto image = QImage(\n\t\tsize * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\timage.fill(Qt::transparent);\n\tauto p = QPainter(&image);\n\tst::infoIconReport.paintInCenter(\n\t\tp,\n\t\tRect(size),\n\t\tst::infoIconFg->c);\n\tp.setCompositionMode(QPainter::CompositionMode_Clear);\n\tconst auto w = st::lineWidth * 6;\n\tp.fillRect(\n\t\tQRect(\n\t\t\trect::center(Rect(size)).x() - w / 2,\n\t\t\trect::center(Rect(size)).y() - w,\n\t\t\tw,\n\t\t\tw * 2),\n\t\tQt::white);\n\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\n\tconst auto s = Size(st::inviteLinkSubscribeBoxTerms.style.font->ascent);\n\tauto svg = QSvgRenderer(CurrencySvg(st::infoIconFg->c));\n\tsvg.render(\n\t\t&p,\n\t\tQRectF(\n\t\t\t(size.width() - s.width()) / 2.,\n\t\t\t(size.height() - s.height()) / 2.,\n\t\t\ts.width(),\n\t\t\ts.height()));\n\treturn image;\n}\n\nQImage MenuIconCredits() {\n\tconstexpr auto kStrokeWidth = 5;\n\tconst auto sizeShift = st::lineWidth * 1.5;\n\n\tauto colorized = [&] {\n\t\tauto f = QFile(Ui::Premium::Svg());\n\t\tif (!f.open(QIODevice::ReadOnly)) {\n\t\t\treturn QString();\n\t\t}\n\t\treturn QString::fromUtf8(f.readAll()).replace(\n\t\t\tu\"#fff\"_q,\n\t\t\tu\"#ffffff00\"_q);\n\t}();\n\tcolorized.replace(\n\t\tu\"stroke=\\\"none\\\"\"_q,\n\t\tu\"stroke=\\\"%1\\\"\"_q.arg(st::menuIconColor->c.name()));\n\tcolorized.replace(\n\t\tu\"stroke-width=\\\"1\\\"\"_q,\n\t\tu\"stroke-width=\\\"%1\\\"\"_q.arg(kStrokeWidth));\n\tauto svg = QSvgRenderer(colorized.toUtf8());\n\tsvg.setViewBox(svg.viewBox()\n\t\t+ Margins(style::ConvertScale(kStrokeWidth)));\n\n\tauto image = QImage(\n\t\tst::menuIconLinks.size() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\timage.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&image);\n\t\tsvg.render(&p, Rect(st::menuIconLinks.size()) - Margins(sizeShift));\n\t}\n\treturn image;\n}\n\nstd::unique_ptr<Ui::Text::CustomEmoji> MakeCurrencyIconEmoji(\n\t\tconst style::font &font,\n\t\tconst QColor &c) {\n\treturn std::make_unique<Ui::CustomEmoji::Internal>(\n\t\tu\"currency_icon:%1:%2\"_q.arg(font->height).arg(c.name()),\n\t\tIconCurrencyColored(font, c));\n}\n\nUi::Text::PaletteDependentEmoji IconCreditsEmoji(\n\t\tIconDescriptor descriptor) {\n\treturn { .factory = [=] {\n\t\treturn Ui::GenerateStars(\n\t\t\t(descriptor.size\n\t\t\t\t? descriptor.size\n\t\t\t\t: st::defaultTableLabel.style.font->height),\n\t\t\t1);\n\t}, .margin = descriptor.margin.value_or(\n\t\tQMargins{ 0, st::giftBoxByStarsSkip, 0, 0 }) };\n}\n\nUi::Text::PaletteDependentEmoji IconCurrencyEmoji(\n\t\tIconDescriptor descriptor) {\n\treturn { .factory = [=] {\n\t\treturn IconCurrencyColored(\n\t\t\tdescriptor.size ? descriptor.size : st::earnTonIconSize,\n\t\t\tst::currencyFg->c);\n\t}, .margin = descriptor.margin.value_or(st::earnTonIconMargin) };\n}\n\nUi::Text::PaletteDependentEmoji IconCreditsEmojiSmall() {\n\treturn IconCreditsEmoji({\n\t\t.size = st::giftBoxByStarsStyle.font->height,\n\t\t.margin = QMargins{ 0, st::giftBoxByStarsStarTop, 0, 0 },\n\t});\n}\n\nUi::Text::PaletteDependentEmoji IconCurrencyEmojiSmall() {\n\treturn IconCreditsEmoji({});\n}\n\n} // namespace Ui::Earn\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/text/custom_emoji_helper.h\"\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nnamespace Ui::Earn {\n\n[[nodiscard]] QImage IconCurrencyColored(int size, const QColor &c);\n[[nodiscard]] QImage IconCurrencyColored(\n\tconst style::font &font,\n\tconst QColor &c);\n[[nodiscard]] QByteArray CurrencySvgColored(const QColor &c);\n\n[[nodiscard]] QImage MenuIconCurrency(const QSize &size);\n[[nodiscard]] QImage MenuIconCredits();\n\nstd::unique_ptr<Ui::Text::CustomEmoji> MakeCurrencyIconEmoji(\n\tconst style::font &font,\n\tconst QColor &c);\n\nstruct IconDescriptor {\n\tint size = 0;\n\tstd::optional<QMargins> margin;\n};\n[[nodiscard]] Ui::Text::PaletteDependentEmoji IconCreditsEmoji(\n\tIconDescriptor descriptor = {});\n[[nodiscard]] Ui::Text::PaletteDependentEmoji IconCurrencyEmoji(\n\tIconDescriptor descriptor = {});\n\n[[nodiscard]] Ui::Text::PaletteDependentEmoji IconCreditsEmojiSmall();\n[[nodiscard]] Ui::Text::PaletteDependentEmoji IconCurrencyEmojiSmall();\n\n} // namespace Ui::Earn\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/channel_statistics/earn/info_channel_earn_list.h\"\n\n#include \"api/api_credits.h\"\n#include \"api/api_earn.h\"\n#include \"api/api_filter_updates.h\"\n#include \"api/api_statistics.h\"\n#include \"api/api_text_entities.h\"\n#include \"api/api_updates.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/gift_premium_box.h\"\n#include \"boxes/peers/edit_peer_color_box.h\" // AddLevelBadge.\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"core/application.h\"\n#include \"core/ui_integration.h\" // TextContext.\n#include \"data/components/credits.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_session.h\"\n#include \"data/data_web_page.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"dialogs/ui/chat_search_empty.h\"\n#include \"history/view/controls/history_view_webpage_processor.h\"\n#include \"info/bot/starref/info_bot_starref_join_widget.h\"\n#include \"info/bot/starref/info_bot_starref_setup_widget.h\"\n#include \"info/channel_statistics/earn/earn_format.h\"\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"info/channel_statistics/earn/info_channel_earn_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"info/profile/info_profile_values.h\" // Info::Profile::NameValue.\n#include \"info/statistics/info_statistics_inner_widget.h\" // FillLoading.\n#include \"info/statistics/info_statistics_list_controllers.h\"\n#include \"iv/iv_instance.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"statistics/chart_widget.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"ui/boxes/boost_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/animation_value_f.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/effects/toggle_arrow.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/peer_bubble.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/slider_natural_width.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_channel_earn.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_statistics.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_window.h\" // mainMenuToggleFourStrokes.\n\n#include <QtWidgets/QApplication>\n\nnamespace Info::ChannelEarn {\nnamespace {\n\nusing EarnInt = Data::EarnInt;\n\n[[nodiscard]] bool WithdrawalEnabled(not_null<Main::Session*> session) {\n\tconst auto key = u\"channel_revenue_withdrawal_enabled\"_q;\n\treturn session->appConfig().get<bool>(key, false);\n}\n\nvoid ShowMenu(not_null<Ui::GenericBox*> box, const QString &text) {\n\tconst auto menu = Ui::CreateChild<Ui::PopupMenu>(box.get());\n\tmenu->addAction(tr::lng_context_copy_link(tr::now), [=] {\n\t\tTextUtilities::SetClipboardText(TextForMimeData::Simple(text));\n\t\tbox->uiShow()->showToast(tr::lng_background_link_copied(tr::now));\n\t});\n\tmenu->popup(QCursor::pos());\n}\n\n[[nodiscard]] ClickHandlerPtr LearnMoreCurrencyLink(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::GenericBox*> box) {\n\tconst auto url = tr::lng_channel_earn_learn_coin_link(tr::now);\n\n\tusing Resolver = HistoryView::Controls::WebpageResolver;\n\tconst auto resolver = box->lifetime().make_state<Resolver>(\n\t\t&controller->session());\n\tresolver->request(url);\n\treturn std::make_shared<GenericClickHandler>([=](ClickContext context) {\n\t\tif (context.button != Qt::LeftButton) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto data = resolver->lookup(url);\n\t\tconst auto iv = data ? (*data)->iv.get() : nullptr;\n\t\tif (iv) {\n\t\t\tCore::App().iv().show(controller, iv, QString());\n\t\t} else {\n\t\t\tresolver->resolved(\n\t\t\t) | rpl::on_next([=](const QString &s) {\n\t\t\t\tif (s == url) {\n\t\t\t\t\tif (const auto d = resolver->lookup(url)) {\n\t\t\t\t\t\tif (const auto iv = (*d)->iv.get()) {\n\t\t\t\t\t\t\tCore::App().iv().show(controller, iv, QString());\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, box->lifetime());\n\t\t\tresolver->request(url);\n\t\t}\n\t});\n}\n\nvoid AddHeader(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\ttr::phrase<> text) {\n\tUi::AddSkip(content);\n\tconst auto header = content->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttext(),\n\t\t\tst::channelEarnHeaderLabel),\n\t\tst::boxRowPadding);\n\theader->resizeToWidth(header->width());\n}\n\nvoid AddRecipient(not_null<Ui::GenericBox*> box, const TextWithEntities &t) {\n\tconst auto container = box->addRow(\n\t\tobject_ptr<Ui::RoundButton>(\n\t\t\tbox,\n\t\t\trpl::single(QString()),\n\t\t\tst::channelEarnHistoryRecipientButton),\n\t\tstyle::al_top);\n\tcontainer->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tcontainer,\n\t\trpl::single(t),\n\t\tst::channelEarnHistoryRecipientButtonLabel);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tlabel->setBreakEverywhere(true);\n\tlabel->setTryMakeSimilarLines(true);\n\tlabel->resizeToWidth(container->width());\n\tlabel->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tconst auto padding = QMargins(\n\t\t\tst::chatGiveawayPeerPadding.right(),\n\t\t\tst::chatGiveawayPeerPadding.top(),\n\t\t\tst::chatGiveawayPeerPadding.right(),\n\t\t\tst::chatGiveawayPeerPadding.top());\n\t\tcontainer->resize(\n\t\t\tcontainer->width(),\n\t\t\t(Rect(s) + padding).height());\n\t\tlabel->moveToLeft(0, padding.top());\n\t}, container->lifetime());\n\tcontainer->setClickedCallback([=] {\n\t\tQGuiApplication::clipboard()->setText(t.text);\n\t\tbox->showToast(tr::lng_text_copied(tr::now));\n\t});\n}\n\n#if 0\n[[nodiscard]] TextWithEntities EmojiCurrency(\n\t\tnot_null<Main::Session*> session) {\n\tauto emoji = TextWithEntities{\n\t\t.text = (QString(QChar(0xD83D)) + QChar(0xDC8E)),\n\t};\n\tif (const auto e = Ui::Emoji::Find(emoji.text)) {\n\t\tconst auto sticker = session->emojiStickersPack().stickerForEmoji(e);\n\t\tif (sticker.document) {\n\t\t\temoji = Data::SingleCustomEmoji(sticker.document);\n\t\t}\n\t}\n\treturn emoji;\n}\n#endif\n\n[[nodiscard]] QString FormatDate(const QDateTime &date) {\n\treturn tr::lng_group_call_starts_short_date(\n\t\ttr::now,\n\t\tlt_date,\n\t\tlangDayOfMonth(date.date()),\n\t\tlt_time,\n\t\tQLocale().toString(date.time(), QLocale::ShortFormat));\n}\n\nconstexpr auto kMinus = QChar(0x2212);\n\n} // namespace\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller,\n\tnot_null<PeerData*> peer)\n: VerticalLayout(parent)\n, _controller(controller)\n, _peer(peer)\n, _show(controller->uiShow()) {\n}\n\nvoid InnerWidget::load() {\n\tstruct State final {\n\t\tState(not_null<PeerData*> peer)\n\t\t: api(peer)\n\t\t, apiCredits(peer)\n\t\t, apiCreditsHistory(peer, true, true) {\n\t\t}\n\t\tApi::EarnStatistics api;\n\t\tApi::CreditsEarnStatistics apiCredits;\n\t\tApi::CreditsHistory apiCreditsHistory;\n\t\trpl::lifetime apiLifetime;\n\t\trpl::lifetime apiCreditsLifetime;\n\t\trpl::lifetime apiPremiumBotLifetime;\n\t};\n\tconst auto state = lifetime().make_state<State>(_peer);\n\tusing ChannelFlag = ChannelDataFlag;\n\tconst auto canViewCredits = !_peer->isChannel()\n\t\t|| (_peer->asChannel()->flags() & ChannelFlag::CanViewCreditsRevenue);\n\n\tInfo::Statistics::FillLoading(\n\t\tthis,\n\t\tInfo::Statistics::LoadingType::Earn,\n\t\t_loaded.events_starting_with(false) | rpl::map(!rpl::mappers::_1),\n\t\t_showFinished.events());\n\n\tconst auto show = _controller->uiShow();\n\tconst auto fail = [=](const QString &error) { show->showToast(error); };\n\n\tconst auto finish = [=] {\n\t\t_loaded.fire(true);\n\t\tfill();\n\t\tstate->apiLifetime.destroy();\n\t\tstate->apiCreditsLifetime.destroy();\n\n\t\t_peer->session().account().mtpUpdates(\n\t\t) | rpl::on_next([=, peerId = _peer->id](\n\t\t\t\tconst MTPUpdates &updates) {\n\t\t\tusing TLCreditsUpdate = MTPDupdateStarsRevenueStatus;\n\t\t\tusing TLNotificationUpdate = MTPDupdateServiceNotification;\n\t\t\tApi::PerformForUpdate<TLCreditsUpdate>(updates, [&](\n\t\t\t\t\tconst TLCreditsUpdate &d) {\n\t\t\t\tif (peerId != peerFromMTP(d.vpeer())) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto &data = d.vstatus().data();\n\t\t\t\tconst auto isCredits = data.vcurrent_balance().match([](\n\t\t\t\t\t\tconst MTPDstarsAmount &) {\n\t\t\t\t\treturn true;\n\t\t\t\t}, [](const MTPDstarsTonAmount &) {\n\t\t\t\t\treturn false;\n\t\t\t\t});\n\t\t\t\tif (isCredits) {\n\t\t\t\t\tauto &credits = _state.creditsEarn;\n\t\t\t\t\tcredits.currentBalance = CreditsAmountFromTL(\n\t\t\t\t\t\tdata.vcurrent_balance());\n\t\t\t\t\tcredits.availableBalance = CreditsAmountFromTL(\n\t\t\t\t\t\tdata.vavailable_balance());\n\t\t\t\t\tcredits.overallRevenue = CreditsAmountFromTL(\n\t\t\t\t\t\tdata.voverall_revenue());\n\t\t\t\t\tcredits.isWithdrawalEnabled\n\t\t\t\t\t\t= data.is_withdrawal_enabled();\n\t\t\t\t\tcredits.nextWithdrawalAt = data.vnext_withdrawal_at()\n\t\t\t\t\t\t? base::unixtime::parse(\n\t\t\t\t\t\t\tdata.vnext_withdrawal_at()->v)\n\t\t\t\t\t\t: QDateTime();\n\t\t\t\t\tstate->apiCreditsHistory.request({}, [=](\n\t\t\t\t\t\t\tconst Data::CreditsStatusSlice &data) {\n\t\t\t\t\t\t_state.creditsStatusSlice = data;\n\t\t\t\t\t\t_stateUpdated.fire({});\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tauto &currency = _state.currencyEarn;\n\t\t\t\t\tcurrency.currentBalance = CreditsAmountFromTL(\n\t\t\t\t\t\tdata.vcurrent_balance());\n\t\t\t\t\tcurrency.availableBalance = CreditsAmountFromTL(\n\t\t\t\t\t\tdata.vavailable_balance());\n\t\t\t\t\tcurrency.overallRevenue = CreditsAmountFromTL(\n\t\t\t\t\t\tdata.voverall_revenue());\n\t\t\t\t\t_stateUpdated.fire({});\n\t\t\t\t}\n\t\t\t});\n\t\t\tApi::PerformForUpdate<TLNotificationUpdate>(updates, [&](\n\t\t\t\t\tconst TLNotificationUpdate &d) {\n\t\t\t\tif (Api::IsWithdrawalNotification(d) && d.is_popup()) {\n\t\t\t\t\tshow->show(Ui::MakeInformBox(TextWithEntities{\n\t\t\t\t\t\tqs(d.vmessage()),\n\t\t\t\t\t\tApi::EntitiesFromMTP(&\n\t\t\t\t\t\t\t_peer->session(),\n\t\t\t\t\t\t\td.ventities().v),\n\t\t\t\t\t}));\n\t\t\t\t}\n\t\t\t});\n\t\t}, lifetime());\n\t};\n\n\t_showFinished.events(\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\tconst auto nextRequests = [=] {\n\t\t\tstate->apiCreditsHistory.request({}, [=](\n\t\t\t\t\tconst Data::CreditsStatusSlice &data) {\n\t\t\t\t_state.creditsStatusSlice = data;\n\t\t\t\t::Api::PremiumPeerBot(\n\t\t\t\t\t&_peer->session()\n\t\t\t\t) | rpl::on_next([=](not_null<PeerData*> bot) {\n\t\t\t\t\t_state.premiumBotId = bot->id;\n\t\t\t\t\tstate->apiCredits.request(\n\t\t\t\t\t) | rpl::on_error_done([=](const QString &error) {\n\t\t\t\t\t\tif (canViewCredits) {\n\t\t\t\t\t\t\tfail(error);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t_state.creditsEarn = {};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfinish();\n\t\t\t\t\t}, [=] {\n\t\t\t\t\t\t_state.creditsEarn = state->apiCredits.data();\n\t\t\t\t\t\tfinish();\n\t\t\t\t\t}, state->apiCreditsLifetime);\n\t\t\t\t\tstate->apiPremiumBotLifetime.destroy();\n\t\t\t\t}, state->apiPremiumBotLifetime);\n\t\t\t});\n\t\t};\n\t\tconst auto isMegagroup = _peer->isMegagroup();\n\t\tstate->api.request(\n\t\t) | rpl::on_error_done([=](const QString &error) {\n\t\t\tif (isMegagroup) {\n\t\t\t\t_state.currencyEarn = {};\n\t\t\t\tif (error == u\"BROADCAST_REQUIRED\"_q) {\n\t\t\t\t\t_state.canViewCurrencyMegagroupEarn = false;\n\t\t\t\t}\n\t\t\t\tnextRequests();\n\t\t\t} else {\n\t\t\t\tshow->showToast(error);\n\t\t\t}\n\t\t}, [=] {\n\t\t\t_state.currencyEarn = state->api.data();\n\t\t\tnextRequests();\n\t\t}, state->apiLifetime);\n\t}, lifetime());\n}\n\nvoid InnerWidget::fill() {\n\tconst auto container = this;\n\tif (!_state.currencyEarn && !_state.creditsEarn) {\n\t\tconst auto empty = container->add(object_ptr<Dialogs::SearchEmpty>(\n\t\t\tcontainer,\n\t\t\tDialogs::SearchEmptyIcon::NoResults,\n\t\t\ttr::lng_search_tab_no_results(tr::bold)));\n\t\tempty->setMinimalHeight(st::normalBoxLottieSize.height());\n\t\tempty->animate();\n\t\treturn;\n\t}\n\tconst auto bot = (peerIsUser(_peer->id) && _peer->asUser()->botInfo)\n\t\t? _peer->asUser()\n\t\t: nullptr;\n\tconst auto channel = _peer->asChannel();\n\tconst auto canViewCurrencyEarn = [&] {\n\t\tif (!channel) {\n\t\t\treturn true;\n\t\t} else if (!(channel->flags() & ChannelDataFlag::CanViewRevenue)) {\n\t\t\treturn false;\n\t\t} else if (channel->isMegagroup()) {\n\t\t\treturn _state.canViewCurrencyMegagroupEarn;\n\t\t} else {\n\t\t\treturn true;\n\t\t}\n\t}();\n\tconst auto &data = canViewCurrencyEarn\n\t\t? _state.currencyEarn\n\t\t: Data::EarnStatistics();\n\tconst auto &creditsData = bot\n\t\t? Data::CreditsEarnStatistics()\n\t\t: _state.creditsEarn;\n\n\tauto currencyStateValue = rpl::single(\n\t\tdata\n\t) | rpl::then(\n\t\t_stateUpdated.events() | rpl::map([=] {\n\t\t\treturn _state.currencyEarn;\n\t\t})\n\t);\n\n\tauto creditsStateValue = bot\n\t\t? rpl::single(Data::CreditsEarnStatistics()) | rpl::type_erased\n\t\t: rpl::single(creditsData) | rpl::then(\n\t\t\t_stateUpdated.events(\n\t\t\t) | rpl::map([this] { return _state.creditsEarn; })\n\t\t);\n\n\tconstexpr auto kMinorLength = 3;\n\t//constexpr auto kApproximately = QChar(0x2248);\n\tconst auto multiplier = data.usdRate;\n\n\tconst auto creditsToUsdMap = [=](CreditsAmount c) {\n\t\tconst auto creditsMultiplier = _state.creditsEarn.usdRate;\n\t\treturn c ? ToUsd(c, creditsMultiplier, 0) : QString();\n\t};\n\n\tconst auto session = &_peer->session();\n\tconst auto withdrawalEnabled = WithdrawalEnabled(session);\n\n\tconst auto addAboutWithLearn = [&](const tr::phrase<lngtag_link> &text) {\n\t\tauto label = object_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttext(\n\t\t\t\tlt_link,\n\t\t\t\ttr::lng_channel_earn_about_link(\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\trpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\t\t\ttr::rich\n\t\t\t\t) | rpl::map([](TextWithEntities text) {\n\t\t\t\t\treturn tr::link(std::move(text), 1);\n\t\t\t\t}),\n\t\t\t\ttr::rich),\n\t\t\tst::boxDividerLabel);\n\t\tlabel->setLink(1, std::make_shared<LambdaClickHandler>([=] {\n\t\t\t_show->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\tbox->setNoContentMargin(true);\n\n\t\t\t\tauto emojiHelper = Ui::Text::CustomEmojiHelper();\n\t\t\t\tconst auto bigCurrencyIcon = emojiHelper.paletteDependent({\n\t\t\t\t\t.factory = [=] {\n\t\t\t\t\t\treturn Ui::Earn::IconCurrencyColored(\n\t\t\t\t\t\t\tst::boxTitle.style.font,\n\t\t\t\t\t\t\tst::currencyFg->c);\n\t\t\t\t\t}, .margin = st::channelEarnCurrencyLearnMargins });\n\n\t\t\t\tconst auto content = box->verticalLayout().get();\n\n\t\t\t\tUi::AddSkip(content);\n\t\t\t\tUi::AddSkip(content);\n\t\t\t\tUi::AddSkip(content);\n\t\t\t\t{\n\t\t\t\t\tconst auto &icon = st::channelEarnLearnTitleIcon;\n\t\t\t\t\tconst auto rect = Rect(icon.size() * 1.4);\n\t\t\t\t\tauto owned = object_ptr<Ui::RpWidget>(content);\n\t\t\t\t\towned->resize(rect.size());\n\t\t\t\t\towned->setNaturalWidth(rect.width());\n\t\t\t\t\tconst auto widget = box->addRow(\n\t\t\t\t\t\tstd::move(owned),\n\t\t\t\t\t\tstyle::al_top);\n\t\t\t\t\twidget->paintRequest(\n\t\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\t\tauto p = Painter(widget);\n\t\t\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\t\t\tp.setBrush(st::activeButtonBg);\n\t\t\t\t\t\tp.drawEllipse(rect);\n\t\t\t\t\t\ticon.paintInCenter(p, rect);\n\t\t\t\t\t}, widget->lifetime());\n\t\t\t\t}\n\t\t\t\tUi::AddSkip(content);\n\t\t\t\tUi::AddSkip(content);\n\t\t\t\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\t\t\t\tcontent,\n\t\t\t\t\tbot\n\t\t\t\t\t\t? tr::lng_channel_earn_bot_learn_title()\n\t\t\t\t\t\t: tr::lng_channel_earn_learn_title(),\n\t\t\t\t\tst::boxTitle),\n\t\t\t\t\tstyle::al_top);\n\t\t\t\tUi::AddSkip(content);\n\t\t\t\tUi::AddSkip(content);\n\t\t\t\tUi::AddSkip(content);\n\t\t\t\tUi::AddSkip(content);\n\t\t\t\t{\n\t\t\t\t\tconst auto padding = QMargins(\n\t\t\t\t\t\tst::settingsButton.padding.left(),\n\t\t\t\t\t\tst::boxRowPadding.top(),\n\t\t\t\t\t\tst::boxRowPadding.right(),\n\t\t\t\t\t\tst::boxRowPadding.bottom());\n\t\t\t\t\tconst auto addEntry = [&](\n\t\t\t\t\t\t\trpl::producer<QString> title,\n\t\t\t\t\t\t\trpl::producer<QString> about,\n\t\t\t\t\t\t\tconst style::icon &icon) {\n\t\t\t\t\t\tconst auto top = content->add(\n\t\t\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\t\t\tcontent,\n\t\t\t\t\t\t\t\tstd::move(title),\n\t\t\t\t\t\t\t\tst::channelEarnSemiboldLabel),\n\t\t\t\t\t\t\tpadding);\n\t\t\t\t\t\tUi::AddSkip(content, st::channelEarnHistoryThreeSkip);\n\t\t\t\t\t\tcontent->add(\n\t\t\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\t\t\tcontent,\n\t\t\t\t\t\t\t\tstd::move(about),\n\t\t\t\t\t\t\t\tst::channelEarnHistoryRecipientLabel),\n\t\t\t\t\t\t\tpadding);\n\t\t\t\t\t\tconst auto left = Ui::CreateChild<Ui::RpWidget>(\n\t\t\t\t\t\t\tbox->verticalLayout().get());\n\t\t\t\t\t\tleft->paintRequest(\n\t\t\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\t\t\tauto p = Painter(left);\n\t\t\t\t\t\t\ticon.paint(p, 0, 0, left->width());\n\t\t\t\t\t\t}, left->lifetime());\n\t\t\t\t\t\tleft->resize(icon.size());\n\t\t\t\t\t\ttop->geometryValue(\n\t\t\t\t\t\t) | rpl::on_next([=](const QRect &g) {\n\t\t\t\t\t\t\tleft->moveToLeft(\n\t\t\t\t\t\t\t\t(g.left() - left->width()) / 2,\n\t\t\t\t\t\t\t\tg.top() + st::channelEarnHistoryThreeSkip);\n\t\t\t\t\t\t}, left->lifetime());\n\t\t\t\t\t};\n\t\t\t\t\taddEntry(\n\t\t\t\t\t\ttr::lng_channel_earn_learn_in_subtitle(),\n\t\t\t\t\t\tbot\n\t\t\t\t\t\t\t? tr::lng_channel_earn_learn_bot_in_about()\n\t\t\t\t\t\t\t: tr::lng_channel_earn_learn_in_about(),\n\t\t\t\t\t\tst::channelEarnLearnChannelIcon);\n\t\t\t\t\tUi::AddSkip(content);\n\t\t\t\t\tUi::AddSkip(content);\n\t\t\t\t\taddEntry(\n\t\t\t\t\t\ttr::lng_channel_earn_learn_split_subtitle(),\n\t\t\t\t\t\ttr::lng_channel_earn_learn_split_about(),\n\t\t\t\t\t\tst::sponsoredAboutSplitIcon);\n\t\t\t\t\tUi::AddSkip(content);\n\t\t\t\t\tUi::AddSkip(content);\n\t\t\t\t\taddEntry(\n\t\t\t\t\t\ttr::lng_channel_earn_learn_out_subtitle(),\n\t\t\t\t\t\ttr::lng_channel_earn_learn_out_about(),\n\t\t\t\t\t\tst::channelEarnLearnWithdrawalsIcon);\n\t\t\t\t\tUi::AddSkip(content);\n\t\t\t\t\tUi::AddSkip(content);\n\t\t\t\t}\n\t\t\t\tUi::AddSkip(content);\n\t\t\t\tUi::AddSkip(content);\n\t\t\t\t{\n\t\t\t\t\tconst auto l = box->addRow(\n\t\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\t\tcontent,\n\t\t\t\t\t\t\ttr::lng_channel_earn_learn_coin_title(\n\t\t\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\t\t\trpl::single(\n\t\t\t\t\t\t\t\t\ttr::link(bigCurrencyIcon, 1)),\n\t\t\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t\t\tst::boxTitle,\n\t\t\t\t\t\t\tst::defaultPopupMenu,\n\t\t\t\t\t\t\temojiHelper.context()),\n\t\t\t\t\t\tstyle::al_top);\n\t\t\t\t\tconst auto diamonds = l->lifetime().make_state<int>(0);\n\t\t\t\t\tl->setLink(1, std::make_shared<LambdaClickHandler>([=] {\n\t\t\t\t\t\tconst auto count = (*diamonds);\n\t\t\t\t\t\tbox->showToast((count == 100)\n\t\t\t\t\t\t\t? u\"You are rich now!\"_q\n\t\t\t\t\t\t\t: (u\"You have earned \"_q\n\t\t\t\t\t\t\t\t+ QString::number(++(*diamonds))\n\t\t\t\t\t\t\t\t+ (!count\n\t\t\t\t\t\t\t\t\t? u\" diamond!\"_q\n\t\t\t\t\t\t\t\t\t: u\" diamonds!\"_q)));\n\t\t\t\t\t}));\n\t\t\t\t}\n\t\t\t\tUi::AddSkip(content);\n\t\t\t\t{\n\t\t\t\t\tconst auto label = box->addRow(\n\t\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\t\tcontent,\n\t\t\t\t\t\t\ttr::lng_channel_earn_learn_coin_about(\n\t\t\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\t\t\ttr::lng_channel_earn_about_link(\n\t\t\t\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\t\t\t\trpl::single(Ui::Text::IconEmoji(\n\t\t\t\t\t\t\t\t\t\t&st::textMoreIconEmoji)),\n\t\t\t\t\t\t\t\t\ttr::rich\n\t\t\t\t\t\t\t\t) | rpl::map([](TextWithEntities text) {\n\t\t\t\t\t\t\t\t\treturn tr::link(std::move(text), 1);\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t\t\tst::channelEarnLearnDescription));\n\t\t\t\t\tlabel->resizeToWidth(box->width()\n\t\t\t\t\t\t- rect::m::sum::h(st::boxRowPadding));\n\t\t\t\t\tlabel->setLink(\n\t\t\t\t\t\t1,\n\t\t\t\t\t\tLearnMoreCurrencyLink(\n\t\t\t\t\t\t\t_controller->parentController(),\n\t\t\t\t\t\t\tbox));\n\t\t\t\t}\n\t\t\t\tUi::AddSkip(content);\n\t\t\t\tUi::AddSkip(content);\n\t\t\t\t{\n\t\t\t\t\tconst auto &st = st::premiumPreviewDoubledLimitsBox;\n\t\t\t\t\tbox->setStyle(st);\n\t\t\t\t\tauto button = object_ptr<Ui::RoundButton>(\n\t\t\t\t\t\tcontainer,\n\t\t\t\t\t\ttr::lng_channel_earn_learn_close(),\n\t\t\t\t\t\tst::defaultActiveButton);\n\t\t\t\t\tbutton->resizeToWidth(box->width()\n\t\t\t\t\t\t- st.buttonPadding.left()\n\t\t\t\t\t\t- st.buttonPadding.left());\n\t\t\t\t\tbutton->setClickedCallback([=] { box->closeBox(); });\n\t\t\t\t\tbox->addButton(std::move(button));\n\t\t\t\t}\n\t\t\t}));\n\t\t}));\n\t\tcontainer->add(object_ptr<Ui::DividerLabel>(\n\t\t\tcontainer,\n\t\t\tstd::move(label),\n\t\t\tst::defaultBoxDividerLabelPadding));\n\t};\n\tif (canViewCurrencyEarn) {\n\t\taddAboutWithLearn(bot\n\t\t\t? tr::lng_channel_earn_about_bot\n\t\t\t: tr::lng_channel_earn_about);\n\t}\n\t{\n\t\tusing Type = Statistic::ChartViewType;\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\tauto hasPreviousChart = false;\n\t\tif (data.topHoursGraph.chart) {\n\t\t\tconst auto widget = container->add(\n\t\t\t\tobject_ptr<Statistic::ChartWidget>(container),\n\t\t\t\tst::statisticsLayerMargins);\n\n\t\t\twidget->setChartData(data.topHoursGraph.chart, Type::Bar);\n\t\t\twidget->setTitle(tr::lng_channel_earn_chart_top_hours());\n\t\t\thasPreviousChart = true;\n\t\t}\n\t\tif (data.revenueGraph.chart) {\n\t\t\tif (hasPreviousChart) {\n\t\t\t\tUi::AddSkip(container);\n\t\t\t\tUi::AddDivider(container);\n\t\t\t\tUi::AddSkip(container);\n\t\t\t\tUi::AddSkip(container);\n\t\t\t}\n\t\t\tconst auto widget = container->add(\n\t\t\t\tobject_ptr<Statistic::ChartWidget>(container),\n\t\t\t\tst::statisticsLayerMargins);\n\n\t\t\twidget->setChartData([&] {\n\t\t\t\tauto chart = data.revenueGraph.chart;\n\t\t\t\tchart.currencyRate = multiplier;\n\t\t\t\treturn chart;\n\t\t\t}(), Type::StackBar);\n\t\t\twidget->setTitle(tr::lng_channel_earn_chart_revenue());\n\t\t\thasPreviousChart = true;\n\t\t}\n\t\tif (creditsData.revenueGraph.chart) {\n\t\t\tif (hasPreviousChart) {\n\t\t\t\tUi::AddSkip(container);\n\t\t\t\tUi::AddDivider(container);\n\t\t\t\tUi::AddSkip(container);\n\t\t\t\tUi::AddSkip(container);\n\t\t\t}\n\t\t\tconst auto widget = container->add(\n\t\t\t\tobject_ptr<Statistic::ChartWidget>(container),\n\t\t\t\tst::statisticsLayerMargins);\n\n\t\t\twidget->setChartData([&] {\n\t\t\t\tauto chart = creditsData.revenueGraph.chart;\n\t\t\t\tchart.currencyRate = creditsData.usdRate;\n\t\t\t\treturn chart;\n\t\t\t}(), Type::StackBar);\n\t\t\twidget->setTitle(tr::lng_bot_earn_chart_revenue());\n\t\t}\n\t\tStatistic::FixCacheForHighDPIChartWidget(container);\n\t}\n\tif (data.topHoursGraph.chart\n\t\t|| data.revenueGraph.chart\n\t\t|| creditsData.revenueGraph.chart) {\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\tUi::AddDivider(container);\n\t\tUi::AddSkip(container);\n\t}\n\t{\n\t\tAddHeader(container, tr::lng_channel_earn_overview_title);\n\t\tUi::AddSkip(container, st::channelEarnOverviewTitleSkip);\n\n\t\tconst auto addOverview = [&](\n\t\t\t\trpl::producer<CreditsAmount> currencyValue,\n\t\t\t\trpl::producer<CreditsAmount> creditsValue,\n\t\t\t\tconst tr::phrase<> &text,\n\t\t\t\tbool showCurrency,\n\t\t\t\tbool showCredits) {\n\t\t\tconst auto line = container->add(\n\t\t\t\tUi::CreateSkipWidget(container, 0),\n\t\t\t\tst::boxRowPadding);\n\t\t\tconst auto majorLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\tline,\n\t\t\t\tst::channelEarnOverviewMajorLabel);\n\t\t\tAddEmojiToMajor(\n\t\t\t\tmajorLabel,\n\t\t\t\trpl::duplicate(currencyValue),\n\t\t\t\t{},\n\t\t\t\t{});\n\t\t\tconst auto minorLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\tline,\n\t\t\t\trpl::duplicate(\n\t\t\t\t\tcurrencyValue\n\t\t\t\t) | rpl::map([](CreditsAmount v) {\n\t\t\t\t\treturn MinorPart(v);\n\t\t\t\t}),\n\t\t\t\tst::channelEarnOverviewMinorLabel);\n\t\t\tconst auto secondMinorLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\tline,\n\t\t\t\tstd::move(\n\t\t\t\t\tcurrencyValue\n\t\t\t\t) | rpl::map([=](CreditsAmount value) {\n\t\t\t\t\treturn value\n\t\t\t\t\t\t? ToUsd(value, multiplier, kMinorLength)\n\t\t\t\t\t\t: QString();\n\t\t\t\t}),\n\t\t\t\tst::channelEarnOverviewSubMinorLabel);\n\n\t\t\tconst auto creditsLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\tline,\n\t\t\t\trpl::duplicate(\n\t\t\t\t\tcreditsValue\n\t\t\t\t) | rpl::map([](CreditsAmount value) {\n\t\t\t\t\treturn Lang::FormatCreditsAmountDecimal(value);\n\t\t\t\t}),\n\t\t\t\tst::channelEarnOverviewMajorLabel);\n\t\t\tconst auto icon = Ui::CreateSingleStarWidget(\n\t\t\t\tline,\n\t\t\t\tcreditsLabel->height());\n\t\t\tconst auto creditsSecondLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\tline,\n\t\t\t\trpl::duplicate(creditsValue) | rpl::map(creditsToUsdMap),\n\t\t\t\tst::channelEarnOverviewSubMinorLabel);\n\t\t\trpl::combine(\n\t\t\t\tline->widthValue(),\n\t\t\t\tmajorLabel->sizeValue(),\n\t\t\t\tcreditsLabel->sizeValue(),\n\t\t\t\tstd::move(creditsValue)\n\t\t\t) | rpl::on_next([=](\n\t\t\t\t\tint available,\n\t\t\t\t\tconst QSize &size,\n\t\t\t\t\tconst QSize &creditsSize,\n\t\t\t\t\tCreditsAmount credits) {\n\t\t\t\tconst auto skip = st::channelEarnOverviewSubMinorLabelPos.x();\n\t\t\t\tline->resize(line->width(), size.height());\n\t\t\t\tminorLabel->moveToLeft(\n\t\t\t\t\tsize.width(),\n\t\t\t\t\tst::channelEarnOverviewMinorLabelSkip);\n\t\t\t\tsecondMinorLabel->resizeToWidth(\n\t\t\t\t\t(showCredits ? (available / 2) : available)\n\t\t\t\t\t\t- size.width()\n\t\t\t\t\t\t- minorLabel->width());\n\t\t\t\tsecondMinorLabel->moveToLeft(\n\t\t\t\t\trect::right(minorLabel) + skip,\n\t\t\t\t\tst::channelEarnOverviewSubMinorLabelPos.y());\n\n\t\t\t\ticon->moveToLeft(\n\t\t\t\t\tshowCurrency\n\t\t\t\t\t\t? (available / 2 + st::boxRowPadding.left() / 2)\n\t\t\t\t\t\t: 0,\n\t\t\t\t\t0);\n\t\t\t\tcreditsLabel->moveToLeft(rect::right(icon) + skip, 0);\n\t\t\t\tcreditsSecondLabel->moveToLeft(\n\t\t\t\t\trect::right(creditsLabel) + skip,\n\t\t\t\t\tst::channelEarnOverviewSubMinorLabelPos.y());\n\t\t\t\tcreditsSecondLabel->resizeToWidth(\n\t\t\t\t\tavailable - creditsSecondLabel->pos().x());\n\t\t\t\tif (!showCredits) {\n\t\t\t\t\tconst auto x = std::numeric_limits<int>::max() / 2;\n\t\t\t\t\ticon->moveToLeft(x, 0);\n\t\t\t\t\tcreditsLabel->moveToLeft(x, 0);\n\t\t\t\t\tcreditsSecondLabel->moveToLeft(x, 0);\n\t\t\t\t}\n\t\t\t\tif (!showCurrency) {\n\t\t\t\t\tconst auto x = std::numeric_limits<int>::max() / 2;\n\t\t\t\t\tmajorLabel->moveToLeft(x, 0);\n\t\t\t\t\tminorLabel->moveToLeft(x, 0);\n\t\t\t\t\tsecondMinorLabel->moveToLeft(x, 0);\n\t\t\t\t}\n\t\t\t}, minorLabel->lifetime());\n\t\t\tUi::ToggleChildrenVisibility(line, true);\n\n\t\t\tUi::AddSkip(container);\n\t\t\tconst auto sub = container->add(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tcontainer,\n\t\t\t\t\ttext(),\n\t\t\t\t\tst::channelEarnOverviewSubMinorLabel),\n\t\t\t\tst::boxRowPadding);\n\t\t\tsub->setTextColorOverride(st::windowSubTextFg->c);\n\t\t};\n\t\tauto availValueMap = [](const auto &v) { return v.availableBalance; };\n\t\tauto currentValueMap = [](const auto &v) { return v.currentBalance; };\n\t\tauto overallValueMap = [](const auto &v) { return v.overallRevenue; };\n\t\tconst auto hasAnyCredits = creditsData.availableBalance\n\t\t\t|| creditsData.currentBalance\n\t\t\t|| creditsData.overallRevenue;\n\t\taddOverview(\n\t\t\trpl::duplicate(currencyStateValue) | rpl::map(availValueMap),\n\t\t\trpl::duplicate(creditsStateValue) | rpl::map(availValueMap),\n\t\t\ttr::lng_channel_earn_available,\n\t\t\tcanViewCurrencyEarn,\n\t\t\thasAnyCredits);\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\taddOverview(\n\t\t\trpl::duplicate(currencyStateValue) | rpl::map(currentValueMap),\n\t\t\trpl::duplicate(creditsStateValue) | rpl::map(currentValueMap),\n\t\t\ttr::lng_channel_earn_reward,\n\t\t\tcanViewCurrencyEarn,\n\t\t\thasAnyCredits);\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\taddOverview(\n\t\t\trpl::duplicate(currencyStateValue) | rpl::map(overallValueMap),\n\t\t\trpl::duplicate(creditsStateValue) | rpl::map(overallValueMap),\n\t\t\ttr::lng_channel_earn_total,\n\t\t\tcanViewCurrencyEarn,\n\t\t\thasAnyCredits);\n\t\tUi::AddSkip(container);\n\t}\n#ifndef _DEBUG\n\tif (channel && !channel->amCreator()) {\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\treturn;\n\t}\n#endif\n\tUi::AddSkip(container);\n\tUi::AddDivider(container);\n\tUi::AddSkip(container);\n\tif (data.availableBalance) {\n\t\tconst auto value = data.availableBalance;\n\t\tAddHeader(container, tr::lng_channel_earn_balance_title);\n\t\tUi::AddSkip(container);\n\n\t\tconst auto labels = container->add(\n\t\t\tobject_ptr<Ui::RpWidget>(container),\n\t\t\tstyle::al_top);\n\n\t\tconst auto majorLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tlabels,\n\t\t\tst::channelEarnBalanceMajorLabel);\n\t\t{\n\t\t\tconst auto &m = st::channelEarnCurrencyCommonMargins;\n\t\t\tconst auto p = QMargins(m.left(), 0, m.right(), m.bottom());\n\t\t\tAddEmojiToMajor(majorLabel, rpl::single(value), {}, p);\n\t\t}\n\t\tmajorLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tconst auto minorLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tlabels,\n\t\t\tMinorPart(value),\n\t\t\tst::channelEarnBalanceMinorLabel);\n\t\tminorLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\trpl::combine(\n\t\t\tmajorLabel->sizeValue(),\n\t\t\tminorLabel->sizeValue()\n\t\t) | rpl::on_next([=](\n\t\t\t\tconst QSize &majorSize,\n\t\t\t\tconst QSize &minorSize) {\n\t\t\tlabels->resize(\n\t\t\t\tmajorSize.width() + minorSize.width(),\n\t\t\t\tmajorSize.height());\n\t\t\tlabels->setNaturalWidth(\n\t\t\t\tmajorSize.width() + minorSize.width());\n\t\t\tmajorLabel->moveToLeft(0, 0);\n\t\t\tminorLabel->moveToRight(\n\t\t\t\t0,\n\t\t\t\tst::channelEarnBalanceMinorLabelSkip);\n\t\t}, labels->lifetime());\n\t\tUi::ToggleChildrenVisibility(labels, true);\n\n\t\tUi::AddSkip(container);\n\t\tcontainer->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\tToUsd(value, multiplier, 0),\n\t\t\t\tst::channelEarnOverviewSubMinorLabel),\n\t\t\tstyle::al_top);\n\n\t\tUi::AddSkip(container);\n\n\t\tconst auto &stButton = st::defaultActiveButton;\n\t\tconst auto button = container->add(\n\t\t\tobject_ptr<Ui::RoundButton>(\n\t\t\t\tcontainer,\n\t\t\t\trpl::never<QString>(),\n\t\t\t\tstButton),\n\t\t\tst::boxRowPadding,\n\t\t\tstyle::al_justify);\n\t\tbutton->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\n\t\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tbutton,\n\t\t\ttr::lng_channel_earn_balance_button(tr::now),\n\t\t\tst::channelEarnSemiboldLabel);\n\t\tlabel->setTextColorOverride(stButton.textFg->c);\n\t\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\trpl::combine(\n\t\t\tbutton->sizeValue(),\n\t\t\tlabel->sizeValue()\n\t\t) | rpl::on_next([=](const QSize &b, const QSize &l) {\n\t\t\tlabel->moveToLeft(\n\t\t\t\t(b.width() - l.width()) / 2,\n\t\t\t\t(b.height() - l.height()) / 2);\n\t\t}, label->lifetime());\n\n\t\tconst auto colorText = [=](float64 value) {\n\t\t\tlabel->setTextColorOverride(\n\t\t\t\tanim::with_alpha(\n\t\t\t\t\tstButton.textFg->c,\n\t\t\t\t\tanim::interpolateF(.5, 1., value)));\n\t\t};\n\t\tcolorText(withdrawalEnabled ? 1. : 0.);\n\t\tbutton->setAttribute(\n\t\t\tQt::WA_TransparentForMouseEvents,\n\t\t\t!withdrawalEnabled && (value.value() > 0.01));\n\n\t\tApi::HandleWithdrawalButton(\n\t\t\t{ .currencyReceiver = _peer },\n\t\t\tbutton,\n\t\t\t_controller->uiShow());\n\t\tUi::ToggleChildrenVisibility(button, true);\n\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\taddAboutWithLearn(withdrawalEnabled\n\t\t\t? tr::lng_channel_earn_balance_about\n\t\t\t: tr::lng_channel_earn_balance_about_temp);\n\t\tUi::AddSkip(container);\n\t}\n\tif (creditsData.availableBalance.value() > 0) {\n\t\tAddHeader(container, tr::lng_bot_earn_balance_title);\n\t\tauto availableBalanceValue = rpl::single(\n\t\t\tcreditsData.availableBalance\n\t\t) | rpl::then(\n\t\t\t_stateUpdated.events() | rpl::map([=] {\n\t\t\t\treturn _state.creditsEarn.availableBalance;\n\t\t\t})\n\t\t);\n\t\tauto dateValue = rpl::single(\n\t\t\tcreditsData.nextWithdrawalAt\n\t\t) | rpl::then(\n\t\t\t_stateUpdated.events() | rpl::map([=] {\n\t\t\t\treturn _state.creditsEarn.nextWithdrawalAt;\n\t\t\t})\n\t\t);\n\t\t::Settings::AddWithdrawalWidget(\n\t\t\tcontainer,\n\t\t\t_controller->parentController(),\n\t\t\t_peer,\n\t\t\trpl::single(\n\t\t\t\tcreditsData.buyAdsUrl\n\t\t\t) | rpl::then(\n\t\t\t\t_stateUpdated.events() | rpl::map([=] {\n\t\t\t\t\treturn _state.creditsEarn.buyAdsUrl;\n\t\t\t\t})\n\t\t\t),\n\t\t\trpl::duplicate(availableBalanceValue),\n\t\t\trpl::duplicate(dateValue),\n\t\t\t_state.creditsEarn.isWithdrawalEnabled,\n\t\t\trpl::duplicate(\n\t\t\t\tavailableBalanceValue\n\t\t\t) | rpl::map(creditsToUsdMap));\n\t}\n\n\tif (Info::BotStarRef::Join::Allowed(_peer)) {\n\t\tconst auto button = Info::BotStarRef::AddViewListButton(\n\t\t\tcontainer,\n\t\t\ttr::lng_credits_summary_earn_title(),\n\t\t\ttr::lng_credits_summary_earn_about(),\n\t\t\ttrue);\n\t\tbutton->setClickedCallback([=] {\n\t\t\t_controller->showSection(Info::BotStarRef::Join::Make(_peer));\n\t\t});\n\t\tUi::AddSkip(container);\n\t\tUi::AddDivider(container);\n\t}\n\tUi::AddSkip(container);\n\n\tconst auto sectionIndex = container->lifetime().make_state<int>(0);\n\tconst auto rebuildLists = [=](\n\t\t\tconst Memento::SavedState &data,\n\t\t\tnot_null<Ui::VerticalLayout*> listsContainer,\n\t\t\tnot_null<Ui::VerticalLayout*> historyDividerContainer) {\n\t\tconst auto hasCurrencyTab\n\t\t\t= !data.currencyEarn.firstHistorySlice.list.empty();\n\t\t// Credits stats for bots are available in info_bot_earn_list.\n\t\tconst auto hasCreditsTab = !bot\n\t\t\t&& !data.creditsStatusSlice.list.empty();\n\t\tconst auto hasOneTab = (hasCurrencyTab || hasCreditsTab)\n\t\t\t&& (hasCurrencyTab != hasCreditsTab);\n\n\t\tconst auto currencyTabText = tr::lng_channel_earn_currency_history(\n\t\t\ttr::now);\n\t\tconst auto creditsTabText = tr::lng_channel_earn_credits_history(\n\t\t\ttr::now);\n\n\t\tconst auto slider = listsContainer->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::CustomWidthSlider>>(\n\t\t\t\tlistsContainer,\n\t\t\t\tobject_ptr<Ui::CustomWidthSlider>(\n\t\t\t\t\tlistsContainer,\n\t\t\t\t\tst::defaultTabsSlider)),\n\t\t\tst::boxRowPadding);\n\t\tslider->toggle(\n\t\t\t((hasCurrencyTab ? 1 : 0) + (hasCreditsTab ? 1 : 0) > 1),\n\t\t\tanim::type::instant);\n\n\t\tif (hasCurrencyTab) {\n\t\t\tslider->entity()->addSection(currencyTabText);\n\t\t}\n\t\tif (hasCreditsTab) {\n\t\t\tslider->entity()->addSection(creditsTabText);\n\t\t}\n\n\t\t{\n\t\t\tconst auto &st = st::defaultTabsSlider;\n\t\t\tslider->entity()->setNaturalWidth(0\n\t\t\t\t+ (hasCurrencyTab\n\t\t\t\t\t? st.labelStyle.font->width(currencyTabText)\n\t\t\t\t\t: 0)\n\t\t\t\t+ (hasCreditsTab\n\t\t\t\t\t? st.labelStyle.font->width(creditsTabText)\n\t\t\t\t\t: 0)\n\t\t\t\t+ rect::m::sum::h(st::boxRowPadding));\n\t\t}\n\n\t\tif (hasOneTab) {\n\t\t\tif (hasCurrencyTab) {\n\t\t\t\tAddHeader(listsContainer, tr::lng_channel_earn_history_title);\n\t\t\t\tAddSkip(listsContainer);\n\t\t\t} else if (hasCreditsTab) {\n\t\t\t\tAddHeader(\n\t\t\t\t\tlistsContainer,\n\t\t\t\t\ttr::lng_channel_earn_credits_history);\n\t\t\t\tslider->entity()->setActiveSectionFast(1);\n\t\t\t}\n\t\t} else {\n\t\t\tslider->entity()->setActiveSectionFast(*sectionIndex);\n\t\t}\n\n\t\tconst auto tabCurrencyList = listsContainer->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tlistsContainer,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(listsContainer)));\n\t\tconst auto tabCreditsList = listsContainer->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tlistsContainer,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(listsContainer)));\n\n\t\trpl::single(slider->entity()->activeSection()) | rpl::then(\n\t\t\tslider->entity()->sectionActivated()\n\t\t) | rpl::on_next([=](int index) {\n\t\t\tif (index == 0) {\n\t\t\t\ttabCurrencyList->toggle(true, anim::type::instant);\n\t\t\t\ttabCreditsList->toggle(false, anim::type::instant);\n\t\t\t} else if (index == 1) {\n\t\t\t\ttabCurrencyList->toggle(false, anim::type::instant);\n\t\t\t\ttabCreditsList->toggle(true, anim::type::instant);\n\t\t\t}\n\t\t\t*sectionIndex = index;\n\t\t}, listsContainer->lifetime());\n\n\t\tif (hasCurrencyTab) {\n\t\t\tUi::AddSkip(listsContainer);\n\n\t\t\tconst auto historyList = tabCurrencyList->entity()->add(\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(tabCurrencyList->entity()));\n\t\t\tconst auto addHistoryEntry = [=](\n\t\t\t\t\tconst Data::CreditsHistoryEntry &entry,\n\t\t\t\t\tconst tr::phrase<> &text) {\n\t\t\t\tconst auto wrap = historyList->add(\n\t\t\t\t\tobject_ptr<Ui::PaddingWrap<Ui::VerticalLayout>>(\n\t\t\t\t\t\thistoryList,\n\t\t\t\t\t\tobject_ptr<Ui::VerticalLayout>(historyList),\n\t\t\t\t\t\tQMargins()));\n\t\t\t\tconst auto inner = wrap->entity();\n\t\t\t\tinner->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\t\tinner->add(object_ptr<Ui::FlatLabel>(\n\t\t\t\t\tinner,\n\t\t\t\t\ttext(),\n\t\t\t\t\tst::channelEarnSemiboldLabel));\n\n\t\t\t\tconst auto isIn = entry.in;\n\t\t\t\tconst auto recipient = Ui::Text::Wrapped(\n\t\t\t\t\t{ entry.provider },\n\t\t\t\t\tEntityType::Code);\n\t\t\t\tif (!recipient.text.isEmpty()) {\n\t\t\t\t\tUi::AddSkip(inner, st::channelEarnHistoryThreeSkip);\n\t\t\t\t\tconst auto label = inner->add(object_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\tinner,\n\t\t\t\t\t\trpl::single(recipient),\n\t\t\t\t\t\tst::channelEarnHistoryRecipientLabel));\n\t\t\t\t\tlabel->setBreakEverywhere(true);\n\t\t\t\t\tlabel->setTryMakeSimilarLines(true);\n\t\t\t\t\tUi::AddSkip(inner, st::channelEarnHistoryThreeSkip);\n\t\t\t\t} else {\n\t\t\t\t\tUi::AddSkip(inner, st::channelEarnHistoryTwoSkip);\n\t\t\t\t}\n\n\t\t\t\tconst auto isFailed = entry.failed;\n\t\t\t\tconst auto isPending = entry.pending;\n\t\t\t\tconst auto dateText = (!entry.adsProceedsToDate.isNull()\n\t\t\t\t\t\t|| isFailed)\n\t\t\t\t\t? (FormatDate(entry.date)\n\t\t\t\t\t\t+ ' '\n\t\t\t\t\t\t+ QChar(8212)\n\t\t\t\t\t\t+ ' '\n\t\t\t\t\t\t+ (isFailed\n\t\t\t\t\t\t\t? tr::lng_channel_earn_history_out_failed(tr::now)\n\t\t\t\t\t\t\t: FormatDate(entry.adsProceedsToDate)))\n\t\t\t\t\t: isPending\n\t\t\t\t\t? tr::lng_channel_earn_history_pending(tr::now)\n\t\t\t\t\t: FormatDate(entry.date);\n\t\t\t\tinner->add(object_ptr<Ui::FlatLabel>(\n\t\t\t\t\tinner,\n\t\t\t\t\tdateText,\n\t\t\t\t\tst::channelEarnHistorySubLabel)\n\t\t\t\t)->setTextColorOverride(isFailed\n\t\t\t\t\t? std::make_optional<QColor>(\n\t\t\t\t\t\tst::menuIconAttentionColor->c)\n\t\t\t\t\t: std::nullopt);\n\n\t\t\t\tconst auto color = (isIn\n\t\t\t\t\t? st::boxTextFgGood\n\t\t\t\t\t: st::menuIconAttentionColor)->c;\n\t\t\t\tconst auto majorLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\t\twrap,\n\t\t\t\t\tst::channelEarnHistoryMajorLabel);\n\t\t\t\tAddEmojiToMajor(\n\t\t\t\t\tmajorLabel,\n\t\t\t\t\trpl::single(entry.credits),\n\t\t\t\t\tisIn,\n\t\t\t\t\t{});\n\t\t\t\tmajorLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\t\tmajorLabel->setTextColorOverride(color);\n\t\t\t\tconst auto minorText = MinorPart(entry.credits);\n\t\t\t\tconst auto minorLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\t\twrap,\n\t\t\t\t\trpl::single(minorText),\n\t\t\t\t\tst::channelEarnHistoryMinorLabel);\n\t\t\t\tminorLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\t\tminorLabel->setTextColorOverride(color);\n\t\t\t\tconst auto button = Ui::CreateChild<Ui::SettingsButton>(\n\t\t\t\t\twrap,\n\t\t\t\t\trpl::single(QString()));\n\t\t\t\tUi::ToggleChildrenVisibility(wrap, true);\n\n\t\t\t\tconst auto detailsBox = [=, peer = _peer](\n\t\t\t\t\t\tnot_null<Ui::GenericBox*> box) {\n\t\t\t\t\tbox->addTopButton(\n\t\t\t\t\t\tst::boxTitleClose,\n\t\t\t\t\t\t[=] { box->closeBox(); });\n\t\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\t\tconst auto labels = box->addRow(\n\t\t\t\t\t\tobject_ptr<Ui::RpWidget>(box),\n\t\t\t\t\t\tstyle::al_top);\n\n\t\t\t\t\tconst auto majorLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\t\t\tlabels,\n\t\t\t\t\t\tst::channelEarnOverviewMajorLabel);\n\t\t\t\t\tAddEmojiToMajor(\n\t\t\t\t\t\tmajorLabel,\n\t\t\t\t\t\trpl::single(entry.credits),\n\t\t\t\t\t\tisIn,\n\t\t\t\t\t\t{});\n\t\t\t\t\tmajorLabel->setAttribute(\n\t\t\t\t\t\tQt::WA_TransparentForMouseEvents);\n\t\t\t\t\tmajorLabel->setTextColorOverride(color);\n\t\t\t\t\tconst auto minorLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\t\t\tlabels,\n\t\t\t\t\t\tminorText,\n\t\t\t\t\t\tst::channelEarnOverviewMinorLabel);\n\t\t\t\t\tminorLabel->setAttribute(\n\t\t\t\t\t\tQt::WA_TransparentForMouseEvents);\n\t\t\t\t\tminorLabel->setTextColorOverride(color);\n\t\t\t\t\trpl::combine(\n\t\t\t\t\t\tmajorLabel->sizeValue(),\n\t\t\t\t\t\tminorLabel->sizeValue()\n\t\t\t\t\t) | rpl::on_next([=](\n\t\t\t\t\t\t\tconst QSize &majorSize,\n\t\t\t\t\t\t\tconst QSize &minorSize) {\n\t\t\t\t\t\tlabels->resize(\n\t\t\t\t\t\t\tmajorSize.width() + minorSize.width(),\n\t\t\t\t\t\t\tmajorSize.height());\n\t\t\t\t\t\tlabels->setNaturalWidth(\n\t\t\t\t\t\t\tmajorSize.width() + minorSize.width());\n\t\t\t\t\t\tmajorLabel->moveToLeft(0, 0);\n\t\t\t\t\t\tminorLabel->moveToRight(\n\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\tst::channelEarnOverviewMinorLabelSkip);\n\t\t\t\t\t}, box->lifetime());\n\n\t\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\t\tbox->addRow(\n\t\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\t\tbox,\n\t\t\t\t\t\t\tdateText,\n\t\t\t\t\t\t\tst::channelEarnHistorySubLabel),\n\t\t\t\t\t\tstyle::al_top);\n\t\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\t\tAddChannelEarnTable(\n\t\t\t\t\t\tbox->uiShow(),\n\t\t\t\t\t\tbox->verticalLayout(),\n\t\t\t\t\t\tentry);\n\t\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\t\tbox->addRow(\n\t\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\t\tbox,\n\t\t\t\t\t\t\tisIn\n\t\t\t\t\t\t\t\t? tr::lng_channel_earn_history_in_about()\n\t\t\t\t\t\t\t\t: tr::lng_channel_earn_history_out(),\n\t\t\t\t\t\t\tst::channelEarnHistoryDescriptionLabel),\n\t\t\t\t\t\tstyle::al_top);\n\t\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\t\tif (isIn) {\n\t\t\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!recipient.text.isEmpty()) {\n\t\t\t\t\t\tAddRecipient(box, recipient);\n\t\t\t\t\t}\n\t\t\t\t\tif (isIn) {\n\t\t\t\t\t\tbox->addRow(\n\t\t\t\t\t\t\tUi::CreatePeerBubble(box, peer),\n\t\t\t\t\t\t\tstyle::al_top);\n\t\t\t\t\t}\n\t\t\t\t\tconst auto closeBox = [=] { box->closeBox(); };\n\t\t\t\t\t{\n\t\t\t\t\t\tconst auto &st = st::premiumPreviewDoubledLimitsBox;\n\t\t\t\t\t\tbox->setStyle(st);\n\t\t\t\t\t\tauto button = object_ptr<Ui::RoundButton>(\n\t\t\t\t\t\t\tbox,\n\t\t\t\t\t\t\t(!entry.successLink.isEmpty())\n\t\t\t\t\t\t\t\t? tr::lng_channel_earn_history_out_button()\n\t\t\t\t\t\t\t\t: tr::lng_box_ok(),\n\t\t\t\t\t\t\tst::defaultActiveButton);\n\t\t\t\t\t\tbutton->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\t\t\t\t\t\tbutton->resizeToWidth(box->width()\n\t\t\t\t\t\t\t- st.buttonPadding.left()\n\t\t\t\t\t\t\t- st.buttonPadding.left());\n\t\t\t\t\t\tif (!entry.successLink.isEmpty()) {\n\t\t\t\t\t\t\tbutton->setAcceptBoth();\n\t\t\t\t\t\t\tbutton->addClickHandler([=](\n\t\t\t\t\t\t\t\t\tQt::MouseButton button) {\n\t\t\t\t\t\t\t\tif (button == Qt::LeftButton) {\n\t\t\t\t\t\t\t\t\tUrlClickHandler::Open(entry.successLink);\n\t\t\t\t\t\t\t\t} else if (button == Qt::RightButton) {\n\t\t\t\t\t\t\t\t\tShowMenu(box, entry.successLink);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tbutton->setClickedCallback(closeBox);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbox->addButton(std::move(button));\n\t\t\t\t\t}\n\t\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\t\tbox->addButton(tr::lng_box_ok(), closeBox);\n\t\t\t\t};\n\n\t\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\t\t_show->showBox(Box(detailsBox));\n\t\t\t\t});\n\t\t\t\twrap->geometryValue(\n\t\t\t\t) | rpl::on_next([=](const QRect &g) {\n\t\t\t\t\tconst auto &padding = st::boxRowPadding;\n\t\t\t\t\tconst auto majorTop = (g.height() - majorLabel->height())\n\t\t\t\t\t\t/ 2;\n\t\t\t\t\tminorLabel->moveToRight(\n\t\t\t\t\t\tpadding.right(),\n\t\t\t\t\t\tmajorTop + st::channelEarnHistoryMinorLabelSkip);\n\t\t\t\t\tmajorLabel->moveToRight(\n\t\t\t\t\t\tpadding.right() + minorLabel->width(),\n\t\t\t\t\t\tmajorTop);\n\t\t\t\t\tconst auto rightWrapPadding = rect::m::sum::h(padding)\n\t\t\t\t\t\t+ minorLabel->width()\n\t\t\t\t\t\t+ majorLabel->width();\n\t\t\t\t\tconst auto additional = st::channelEarnHistoryOuter\n\t\t\t\t\t\t+ QMargins(padding.left(), 0, rightWrapPadding, 0);\n\t\t\t\t\twrap->setPadding(additional);\n\t\t\t\t\tbutton->resize((g + additional).size());\n\t\t\t\t\tbutton->lower();\n\t\t\t\t}, wrap->lifetime());\n\t\t\t};\n\t\t\tconst auto handleSlice = [=](const Data::EarnHistorySlice &s) {\n\t\t\t\tfor (const auto &entry : s.list) {\n\t\t\t\t\taddHistoryEntry(\n\t\t\t\t\t\tentry,\n\t\t\t\t\t\t(entry.refunded\n\t\t\t\t\t\t\t? tr::lng_channel_earn_history_return\n\t\t\t\t\t\t\t: entry.in\n\t\t\t\t\t\t\t? tr::lng_channel_earn_history_in\n\t\t\t\t\t\t\t: tr::lng_channel_earn_history_out));\n\t\t\t\t}\n\t\t\t\thistoryList->resizeToWidth(listsContainer->width());\n\t\t\t};\n\t\t\tconst auto &firstSlice = data.currencyEarn.firstHistorySlice;\n\t\t\thandleSlice(firstSlice);\n\t\t\tif (!firstSlice.allLoaded) {\n\t\t\t\tstruct ShowMoreState final {\n\t\t\t\t\tShowMoreState(not_null<PeerData*> peer)\n\t\t\t\t\t: api(peer) {\n\t\t\t\t\t}\n\t\t\t\t\tApi::EarnStatistics api;\n\t\t\t\t\tbool loading = false;\n\t\t\t\t\tData::EarnHistorySlice::OffsetToken token;\n\t\t\t\t\trpl::variable<int> showed = 0;\n\t\t\t\t};\n\t\t\t\tconst auto state\n\t\t\t\t\t= lifetime().make_state<ShowMoreState>(_peer);\n\t\t\t\tstate->token = firstSlice.token;\n\t\t\t\tstate->showed = firstSlice.list.size();\n\t\t\t\tconst auto wrap = tabCurrencyList->entity()->add(\n\t\t\t\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\t\t\t\ttabCurrencyList->entity(),\n\t\t\t\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\t\t\t\ttabCurrencyList->entity(),\n\t\t\t\t\t\t\ttr::lng_channels_your_more(),\n\t\t\t\t\t\t\tst::statisticsShowMoreButton)));\n\t\t\t\tconst auto button = wrap->entity();\n\t\t\t\tUi::AddToggleUpDownArrowToMoreButton(button);\n\n\t\t\t\twrap->toggle(!firstSlice.allLoaded, anim::type::instant);\n\t\t\t\tconst auto handleReceived = [=](\n\t\t\t\t\t\tData::EarnHistorySlice slice) {\n\t\t\t\t\tstate->loading = false;\n\t\t\t\t\thandleSlice(slice);\n\t\t\t\t\twrap->toggle(!slice.allLoaded, anim::type::instant);\n\t\t\t\t\tstate->token = slice.token;\n\t\t\t\t\tstate->showed = state->showed.current()\n\t\t\t\t\t\t+ slice.list.size();\n\t\t\t\t};\n\t\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\t\tif (state->loading) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tstate->loading = true;\n\t\t\t\t\tstate->api.requestHistory(state->token, handleReceived);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tif (hasCreditsTab) {\n\t\t\tconst auto controller = _controller->parentController();\n\t\t\tconst auto show = controller->uiShow();\n\t\t\tconst auto entryClicked = [=](\n\t\t\t\t\tconst Data::CreditsHistoryEntry &e,\n\t\t\t\t\tconst Data::SubscriptionEntry &s) {\n\t\t\t\tshow->show(Box(\n\t\t\t\t\t::Settings::ReceiptCreditsBox,\n\t\t\t\t\tcontroller,\n\t\t\t\t\te,\n\t\t\t\t\ts));\n\t\t\t};\n\n\t\t\tInfo::Statistics::AddCreditsHistoryList(\n\t\t\t\tshow,\n\t\t\t\tdata.creditsStatusSlice,\n\t\t\t\ttabCreditsList->entity(),\n\t\t\t\tentryClicked,\n\t\t\t\t_peer,\n\t\t\t\ttrue,\n\t\t\t\ttrue);\n\t\t}\n\t\tif (hasCurrencyTab || hasCreditsTab) {\n\t\t\tUi::AddSkip(historyDividerContainer);\n\t\t\tUi::AddDivider(historyDividerContainer);\n\t\t\tUi::AddSkip(historyDividerContainer);\n\t\t}\n\n\t\tlistsContainer->resizeToWidth(width());\n\t};\n\n\tconst auto historyContainer = container->add(\n\t\tobject_ptr<Ui::VerticalLayout>(container));\n\tconst auto historyDividerContainer = container->add(\n\t\tobject_ptr<Ui::VerticalLayout>(container));\n\trpl::single(rpl::empty) | rpl::then(\n\t\t_stateUpdated.events()\n\t) | rpl::on_next([=] {\n\t\tconst auto listsContainer = historyContainer->add(\n\t\t\tobject_ptr<Ui::VerticalLayout>(container));\n\t\trebuildLists(_state, listsContainer, historyDividerContainer);\n\t\twhile (historyContainer->count() > 1) {\n\t\t\tdelete historyContainer->widgetAt(0);\n\t\t}\n\t}, historyContainer->lifetime());\n\n\tif (channel && !channel->isMegagroup()) {\n\t\t//constexpr auto kMaxCPM = 50; // Debug.\n\t\tconst auto requiredLevel = Data::LevelLimits(session)\n\t\t\t.channelRestrictSponsoredLevelMin();\n\t\tconst auto &phrase = tr::lng_channel_earn_off;\n\t\tconst auto button = container->add(object_ptr<Ui::SettingsButton>(\n\t\t\tcontainer,\n\t\t\tphrase(),\n\t\t\tst::settingsButtonNoIconLocked));\n\t\tconst auto toggled = lifetime().make_state<rpl::event_stream<bool>>();\n\t\tconst auto isLocked = channel->levelHint() < requiredLevel;\n\t\tconst auto reason = Ui::AskBoostReason{\n\t\t\t.data = Ui::AskBoostCpm{ .requiredLevel = requiredLevel },\n\t\t};\n\n\t\tAddLevelBadge(\n\t\t\trequiredLevel,\n\t\t\tbutton,\n\t\t\tnullptr,\n\t\t\tchannel,\n\t\t\tQMargins(st::boxRowPadding.left(), 0, 0, 0),\n\t\t\tphrase());\n\n\t\tbutton->toggleOn(rpl::single(\n\t\t\tdata.switchedOff\n\t\t) | rpl::then(toggled->events()));\n\t\tbutton->setToggleLocked(isLocked);\n\n\t\tbutton->toggledChanges(\n\t\t) | rpl::on_next([=](bool value) {\n\t\t\tif (isLocked && value) {\n\t\t\t\ttoggled->fire(false);\n\t\t\t\tCheckBoostLevel(\n\t\t\t\t\t_controller->uiShow(),\n\t\t\t\t\t_peer,\n\t\t\t\t\t[=](int level) {\n\t\t\t\t\t\treturn (level < requiredLevel)\n\t\t\t\t\t\t\t? std::make_optional(reason)\n\t\t\t\t\t\t\t: std::nullopt;\n\t\t\t\t\t},\n\t\t\t\t\t[] {});\n\t\t\t}\n\t\t\tif (!isLocked) {\n\t\t\t\tconst auto weak = base::make_weak(this);\n\t\t\t\tconst auto show = _controller->uiShow();\n\t\t\t\tconst auto failed = [=](const QString &e) {\n\t\t\t\t\tif (weak.get()) {\n\t\t\t\t\t\ttoggled->fire(false);\n\t\t\t\t\t\tshow->showToast(e);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tApi::RestrictSponsored(channel, value, failed);\n\t\t\t}\n\t\t}, button->lifetime());\n\n\t\tUi::AddSkip(container);\n\t\tUi::AddDividerText(container, tr::lng_channel_earn_off_about());\n\t} else {\n\t\twhile (historyDividerContainer->count() > 1) {\n\t\t\tdelete historyDividerContainer->widgetAt(0);\n\t\t}\n\t}\n\tUi::AddSkip(container);\n\n\tUi::ToggleChildrenVisibility(container, true);\n\tUi::RpWidget::resizeToWidth(width());\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\tmemento->setState(base::take(_state));\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\t_state = memento->state();\n\tif (_state.currencyEarn || _state.creditsEarn) {\n\t\tfill();\n\t} else {\n\t\tload();\n\t}\n\tUi::RpWidget::resizeToWidth(width());\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nauto InnerWidget::showRequests() const -> rpl::producer<ShowRequest> {\n\treturn _showRequests.events();\n}\n\nvoid InnerWidget::showFinished() {\n\t_showFinished.fire({});\n}\n\nvoid InnerWidget::setInnerFocus() {\n\t_focusRequested.fire({});\n}\n\nnot_null<PeerData*> InnerWidget::peer() const {\n\treturn _peer;\n}\n\nvoid AddEmojiToMajor(\n\t\tnot_null<Ui::FlatLabel*> label,\n\t\trpl::producer<CreditsAmount> value,\n\t\tstd::optional<bool> isIn,\n\t\tstd::optional<QMargins> margins) {\n\tconst auto &st = label->st();\n\tconst auto prepended = !isIn\n\t\t? TextWithEntities()\n\t\t: TextWithEntities::Simple((*isIn) ? QChar('+') : kMinus);\n\tstd::move(\n\t\tvalue\n\t) | rpl::on_next([=](CreditsAmount v) {\n\t\tauto helper = Ui::Text::CustomEmojiHelper();\n\t\tauto icon = helper.paletteDependent({\n\t\t\t.factory = [=] {\n\t\t\t\treturn Ui::Earn::IconCurrencyColored(\n\t\t\t\t\tst.style.font,\n\t\t\t\t\t!isIn\n\t\t\t\t\t? st::currencyFg->c\n\t\t\t\t\t: (*isIn)\n\t\t\t\t\t? st::boxTextFgGood->c\n\t\t\t\t\t: st::menuIconAttentionColor->c);\n\t\t\t\t},\n\t\t\t.margin = margins\n\t\t\t\t? *margins\n\t\t\t\t: st::channelEarnCurrencyCommonMargins\n\t\t});\n\t\tauto value = MajorPart(v.abs());\n\t\tlabel->setMarkedText(\n\t\t\tbase::duplicate(prepended).append(icon).append(value),\n\t\t\thelper.context());\n\t}, label->lifetime());\n}\n\n} // namespace Info::ChannelEarn\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/channel_statistics/earn/info_channel_earn_widget.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/vertical_layout.h\"\n\nnamespace Ui {\nclass Show;\nclass FlatLabel;\n} // namespace Ui\n\nnamespace Info {\nclass Controller;\n} // namespace Info\n\nnamespace Info::ChannelEarn {\n\nclass Memento;\n\nclass InnerWidget final : public Ui::VerticalLayout {\npublic:\n\tstruct ShowRequest final {\n\t};\n\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<PeerData*> peer);\n\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\n\t[[nodiscard]] rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\t[[nodiscard]] rpl::producer<ShowRequest> showRequests() const;\n\n\tvoid showFinished();\n\tvoid setInnerFocus();\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\nprivate:\n\tvoid load();\n\tvoid fill();\n\n\tnot_null<Controller*> _controller;\n\tnot_null<PeerData*> _peer;\n\tstd::shared_ptr<Ui::Show> _show;\n\n\tMemento::SavedState _state;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\trpl::event_stream<ShowRequest> _showRequests;\n\trpl::event_stream<> _showFinished;\n\trpl::event_stream<> _focusRequested;\n\trpl::event_stream<bool> _loaded;\n\trpl::event_stream<> _stateUpdated;\n\n};\n\nvoid AddEmojiToMajor(\n\tnot_null<Ui::FlatLabel*> label,\n\trpl::producer<CreditsAmount> value,\n\tstd::optional<bool> isIn,\n\tstd::optional<QMargins> margins);\n\n} // namespace Info::ChannelEarn\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/channel_statistics/earn/info_channel_earn_widget.h\"\n\n#include \"info/channel_statistics/earn/info_channel_earn_list.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/ui_utility.h\"\n\nnamespace Info::ChannelEarn {\n\nMemento::Memento(not_null<Controller*> controller)\n: ContentMemento(controller->statisticsTag()) {\n}\n\nMemento::Memento(not_null<PeerData*> peer)\n: ContentMemento(Info::Statistics::Tag{ peer, {}, {} }) {\n}\n\nMemento::~Memento() = default;\n\nSection Memento::section() const {\n\treturn Section(Section::Type::ChannelEarn);\n}\n\nvoid Memento::setState(SavedState state) {\n\t_state = std::move(state);\n}\n\nMemento::SavedState Memento::state() {\n\treturn base::take(_state);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(parent, controller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: ContentWidget(parent, controller)\n, _inner(setInnerWidget(\n\tobject_ptr<InnerWidget>(\n\t\tthis,\n\t\tcontroller,\n\t\tcontroller->statisticsTag().peer))) {\n\t_inner->showRequests(\n\t) | rpl::on_next([=](InnerWidget::ShowRequest request) {\n\t}, _inner->lifetime());\n\t_inner->scrollToRequests(\n\t) | rpl::on_next([=](const Ui::ScrollToRequest &request) {\n\t\tscrollTo(request);\n\t}, _inner->lifetime());\n}\n\nnot_null<PeerData*> Widget::peer() const {\n\treturn _inner->peer();\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\treturn (memento->statisticsTag().peer == peer());\n}\n\nrpl::producer<QString> Widget::title() {\n\treturn tr::lng_channel_earn_title();\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nrpl::producer<bool> Widget::desiredShadowVisibility() const {\n\treturn rpl::single<bool>(true);\n}\n\nvoid Widget::showFinished() {\n\t_inner->showFinished();\n}\n\nvoid Widget::setInnerFocus() {\n\t_inner->setInnerFocus();\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(controller());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\nstd::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer) {\n\treturn std::make_shared<Info::Memento>(\n\t\tstd::vector<std::shared_ptr<ContentMemento>>(\n\t\t\t1,\n\t\t\tstd::make_shared<Memento>(peer)));\n}\n\n} // namespace Info::ChannelEarn\n"
  },
  {
    "path": "Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_channel_earn.h\"\n#include \"data/data_credits.h\"\n#include \"data/data_credits_earn.h\"\n#include \"info/info_content_widget.h\"\n\nnamespace Info::ChannelEarn {\n\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\tMemento(not_null<Controller*> controller);\n\tMemento(not_null<PeerData*> peer);\n\t~Memento();\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\tstruct SavedState final {\n\t\tData::EarnStatistics currencyEarn;\n\t\tData::CreditsEarnStatistics creditsEarn;\n\t\tData::CreditsStatusSlice creditsStatusSlice;\n\t\tPeerId premiumBotId = PeerId(0);\n\t\tbool canViewCurrencyMegagroupEarn = true;\n\t};\n\n\tvoid setState(SavedState states);\n\t[[nodiscard]] SavedState state();\n\nprivate:\n\tSavedState _state;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(QWidget *parent, not_null<Controller*> controller);\n\n\tbool showInternal(not_null<ContentMemento*> memento) override;\n\trpl::producer<QString> title() override;\n\trpl::producer<bool> desiredShadowVisibility() const override;\n\tvoid showFinished() override;\n\tvoid setInnerFocus() override;\n\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tconst not_null<InnerWidget*> _inner;\n\n};\n\n[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer);\n\n} // namespace Info::ChannelEarn\n"
  },
  {
    "path": "Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/common_groups/info_common_groups_inner_widget.h\"\n\n#include \"base/weak_ptr.h\"\n#include \"info/common_groups/info_common_groups_widget.h\"\n#include \"info/info_controller.h\"\n#include \"lang/lang_keys.h\"\n#include \"mtproto/sender.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/search_field_controller.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Info {\nnamespace CommonGroups {\nnamespace {\n\nconstexpr auto kCommonGroupsPerPage = 40;\nconstexpr auto kCommonGroupsSearchAfter = 20;\n\nclass ListController final\n\t: public PeerListController\n\t, public base::has_weak_ptr {\npublic:\n\tListController(\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<UserData*> user);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\n\tstd::unique_ptr<PeerListRow> createRestoredRow(\n\t\t\tnot_null<PeerData*> peer) override {\n\t\treturn createRow(peer);\n\t}\n\n\tstd::unique_ptr<PeerListState> saveState() const override;\n\tvoid restoreState(std::unique_ptr<PeerListState> state) override;\n\nprivate:\n\tstd::unique_ptr<PeerListRow> createRow(not_null<PeerData*> peer);\n\n\tstruct SavedState : SavedStateBase {\n\t\tPeerId preloadGroupId = 0;\n\t\tbool allLoaded = false;\n\t\tbool wasLoading = false;\n\t};\n\tconst not_null<Controller*> _controller;\n\tMTP::Sender _api;\n\tnot_null<UserData*> _user;\n\tmtpRequestId _preloadRequestId = 0;\n\tbool _allLoaded = false;\n\tPeerId _preloadGroupId = 0;\n\n};\n\nListController::ListController(\n\tnot_null<Controller*> controller,\n\tnot_null<UserData*> user)\n: PeerListController()\n, _controller(controller)\n, _api(&_controller->session().mtp())\n, _user(user) {\n\t_controller->setSearchEnabledByContent(false);\n}\n\nMain::Session &ListController::session() const {\n\treturn _user->session();\n}\n\nstd::unique_ptr<PeerListRow> ListController::createRow(\n\t\tnot_null<PeerData*> peer) {\n\tauto result = std::make_unique<PeerListRow>(peer);\n\tresult->setCustomStatus(QString());\n\treturn result;\n}\n\nvoid ListController::prepare() {\n\tsetSearchNoResultsText(tr::lng_bot_groups_not_found(tr::now));\n\tdelegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);\n\tdelegate()->peerListSetTitle(tr::lng_profile_common_groups_section());\n}\n\nvoid ListController::loadMoreRows() {\n\tif (_preloadRequestId || _allLoaded) {\n\t\treturn;\n\t}\n\t_preloadRequestId = _api.request(MTPmessages_GetCommonChats(\n\t\t_user->inputUser(),\n\t\tMTP_long(peerIsChat(_preloadGroupId)\n\t\t\t? peerToChat(_preloadGroupId).bare\n\t\t\t: peerToChannel(_preloadGroupId).bare),\n\t\tMTP_int(kCommonGroupsPerPage)\n\t)).done([this](const MTPmessages_Chats &result) {\n\t\t_preloadRequestId = 0;\n\t\t_preloadGroupId = 0;\n\t\t_allLoaded = true;\n\t\tconst auto &chats = result.match([](const auto &data) {\n\t\t\treturn data.vchats().v;\n\t\t});\n\t\tif (!chats.empty()) {\n\t\t\tauto add = std::vector<not_null<PeerData*>>();\n\t\t\tauto allLoaded = _allLoaded;\n\t\t\tauto preloadGroupId = _preloadGroupId;\n\t\t\tconst auto owner = &_user->owner();\n\t\t\tconst auto weak = base::make_weak(this);\n\t\t\tfor (const auto &chat : chats) {\n\t\t\t\tif (const auto peer = owner->processChat(chat)) {\n\t\t\t\t\tif (!peer->migrateTo()) {\n\t\t\t\t\t\tadd.push_back(peer);\n\t\t\t\t\t}\n\t\t\t\t\tpreloadGroupId = peer->id;\n\t\t\t\t\tallLoaded = false;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!weak) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfor (const auto &peer : add) {\n\t\t\t\tif (!delegate()->peerListFindRow(peer->id.value)) {\n\t\t\t\t\tdelegate()->peerListAppendRow(createRow(peer));\n\t\t\t\t}\n\t\t\t}\n\t\t\t_preloadGroupId = preloadGroupId;\n\t\t\t_allLoaded = allLoaded;\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t\tauto fullCount = delegate()->peerListFullRowsCount();\n\t\tif (fullCount > kCommonGroupsSearchAfter) {\n\t\t\t_controller->setSearchEnabledByContent(true);\n\t\t}\n\t}).send();\n}\n\nstd::unique_ptr<PeerListState> ListController::saveState() const {\n\tauto result = PeerListController::saveState();\n\tauto my = std::make_unique<SavedState>();\n\tmy->preloadGroupId = _preloadGroupId;\n\tmy->allLoaded = _allLoaded;\n\tmy->wasLoading = (_preloadRequestId != 0);\n\tresult->controllerState = std::move(my);\n\treturn result;\n}\n\nvoid ListController::restoreState(\n\t\tstd::unique_ptr<PeerListState> state) {\n\tauto typeErasedState = state\n\t\t? state->controllerState.get()\n\t\t: nullptr;\n\tif (auto my = dynamic_cast<SavedState*>(typeErasedState)) {\n\t\tif (auto requestId = base::take(_preloadRequestId)) {\n\t\t\t_api.request(requestId).cancel();\n\t\t}\n\t\t_allLoaded = my->allLoaded;\n\t\t_preloadGroupId = my->preloadGroupId;\n\t\tif (my->wasLoading) {\n\t\t\tloadMoreRows();\n\t\t}\n\t\tPeerListController::restoreState(std::move(state));\n\t\tauto fullCount = delegate()->peerListFullRowsCount();\n\t\tif (fullCount > kCommonGroupsSearchAfter) {\n\t\t\t_controller->setSearchEnabledByContent(true);\n\t\t}\n\t}\n}\n\nvoid ListController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto peer = row->peer();\n\tconst auto controller = _controller->parentController();\n\tif (const auto forum = peer->forum()) {\n\t\tcontroller->showForum(forum);\n\t} else {\n\t\tcontroller->showPeerHistory(\n\t\t\tpeer,\n\t\t\tWindow::SectionShow::Way::Forward);\n\t}\n}\n\n} // namespace\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller,\n\tnot_null<UserData*> user)\n: RpWidget(parent)\n, _show(controller->uiShow())\n, _controller(controller)\n, _user(user)\n, _listController(std::make_unique<ListController>(controller, _user))\n, _list(setupList(this, _listController.get())) {\n\tsetContent(_list.data());\n\t_listController->setDelegate(static_cast<PeerListDelegate*>(this));\n\n\t_controller->searchFieldController()->queryValue(\n\t) | rpl::on_next([this](QString &&query) {\n\t\tpeerListScrollToTop();\n\t\tcontent()->searchQueryChanged(std::move(query));\n\t}, lifetime());\n}\n\nvoid InnerWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tsetChildVisibleTopBottom(_list, visibleTop, visibleBottom);\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\tmemento->setListState(_listController->saveState());\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\t_listController->restoreState(memento->listState());\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nint InnerWidget::desiredHeight() const {\n\tauto desired = 0;\n\tauto count = qMax(_user->commonChatsCount(), 1);\n\tdesired += qMax(count, _list->fullRowsCount())\n\t\t* st::infoCommonGroupsList.item.height;\n\treturn qMax(height(), desired);\n}\n\nobject_ptr<InnerWidget::ListWidget> InnerWidget::setupList(\n\t\tRpWidget *parent,\n\t\tnot_null<PeerListController*> controller) const {\n\tcontroller->setStyleOverrides(&st::infoCommonGroupsList);\n\tauto result = object_ptr<ListWidget>(\n\t\tparent,\n\t\tcontroller);\n\tresult->scrollToRequests(\n\t) | rpl::on_next([this](Ui::ScrollToRequest request) {\n\t\tauto addmin = (request.ymin < 0)\n\t\t\t? 0\n\t\t\t: st::infoCommonGroupsMargin.top();\n\t\tauto addmax = (request.ymax < 0)\n\t\t\t? 0\n\t\t\t: st::infoCommonGroupsMargin.top();\n\t\t_scrollToRequests.fire({\n\t\t\trequest.ymin + addmin,\n\t\t\trequest.ymax + addmax });\n\t}, result->lifetime());\n\tresult->moveToLeft(0, st::infoCommonGroupsMargin.top());\n\tparent->widthValue(\n\t) | rpl::on_next([list = result.data()](int newWidth) {\n\t\tlist->resizeToWidth(newWidth);\n\t}, result->lifetime());\n\tresult->heightValue(\n\t) | rpl::on_next([parent](int listHeight) {\n\t\tauto newHeight = st::infoCommonGroupsMargin.top()\n\t\t\t+ listHeight\n\t\t\t+ st::infoCommonGroupsMargin.bottom();\n\t\tparent->resize(parent->width(), newHeight);\n\t}, result->lifetime());\n\treturn result;\n}\n\nvoid InnerWidget::peerListSetTitle(rpl::producer<QString> title) {\n}\n\nvoid InnerWidget::peerListSetAdditionalTitle(rpl::producer<QString> title) {\n}\n\nbool InnerWidget::peerListIsRowChecked(not_null<PeerListRow*> row) {\n\treturn false;\n}\n\nint InnerWidget::peerListSelectedRowsCount() {\n\treturn 0;\n}\n\nvoid InnerWidget::peerListScrollToTop() {\n\t_scrollToRequests.fire({ -1, -1 });\n}\n\nvoid InnerWidget::peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) {\n\tUnexpected(\"Item selection in Info::Profile::Members.\");\n}\n\nvoid InnerWidget::peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) {\n\tUnexpected(\"Item selection in Info::Profile::Members.\");\n}\n\nvoid InnerWidget::peerListFinishSelectedRowsBunch() {\n}\n\nvoid InnerWidget::peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) {\n\tdescription.destroy();\n}\n\nstd::shared_ptr<Main::SessionShow> InnerWidget::peerListUiShow() {\n\treturn _show;\n}\n\n} // namespace CommonGroups\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <rpl/producer.h>\n#include \"ui/rp_widget.h\"\n#include \"boxes/peer_list_box.h\"\n\nnamespace Ui {\nclass Show;\n} // namespace Ui\n\nnamespace Info {\n\nclass Controller;\n\nnamespace CommonGroups {\n\nclass Memento;\n\nclass InnerWidget final\n\t: public Ui::RpWidget\n\t, private PeerListContentDelegate {\npublic:\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<UserData*> user);\n\n\tnot_null<UserData*> user() const {\n\t\treturn _user;\n\t}\n\n\trpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\n\tint desiredHeight() const;\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\nprivate:\n\tusing ListWidget = PeerListContent;\n\n\t// PeerListContentDelegate interface.\n\tvoid peerListSetTitle(rpl::producer<QString> title) override;\n\tvoid peerListSetAdditionalTitle(rpl::producer<QString> title) override;\n\tbool peerListIsRowChecked(not_null<PeerListRow*> row) override;\n\tint peerListSelectedRowsCount() override;\n\tvoid peerListScrollToTop() override;\n\tvoid peerListAddSelectedPeerInBunch(\n\t\tnot_null<PeerData*> peer) override;\n\tvoid peerListAddSelectedRowInBunch(\n\t\tnot_null<PeerListRow*> row) override;\n\tvoid peerListFinishSelectedRowsBunch() override;\n\tvoid peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) override;\n\tstd::shared_ptr<Main::SessionShow> peerListUiShow() override;\n\n\tobject_ptr<ListWidget> setupList(\n\t\tRpWidget *parent,\n\t\tnot_null<PeerListController*> controller) const;\n\n\tstd::shared_ptr<Main::SessionShow> _show;\n\tnot_null<Controller*> _controller;\n\tnot_null<UserData*> _user;\n\tstd::unique_ptr<PeerListController> _listController;\n\tobject_ptr<ListWidget> _list;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\n};\n\n} // namespace CommonGroups\n} // namespace Info\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/common_groups/info_common_groups_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/common_groups/info_common_groups_widget.h\"\n\n#include \"info/common_groups/info_common_groups_inner_widget.h\"\n#include \"info/info_controller.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/ui_utility.h\"\n#include \"lang/lang_keys.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info {\nnamespace CommonGroups {\n\nMemento::Memento(not_null<UserData*> user)\n: ContentMemento(user, nullptr, nullptr, PeerId()) {\n}\n\nSection Memento::section() const {\n\treturn Section(Section::Type::CommonGroups);\n}\n\nnot_null<UserData*> Memento::user() const {\n\treturn peer()->asUser();\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(parent, controller, user());\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nvoid Memento::setListState(std::unique_ptr<PeerListState> state) {\n\t_listState = std::move(state);\n}\n\nstd::unique_ptr<PeerListState> Memento::listState() {\n\treturn std::move(_listState);\n}\n\nMemento::~Memento() = default;\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller,\n\tnot_null<UserData*> user)\n: ContentWidget(parent, controller) {\n\t_inner = setInnerWidget(object_ptr<InnerWidget>(\n\t\tthis,\n\t\tcontroller,\n\t\tuser));\n}\n\nrpl::producer<QString> Widget::title() {\n\treturn tr::lng_profile_common_groups_section();\n}\n\nnot_null<UserData*> Widget::user() const {\n\treturn _inner->user();\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\tif (!controller()->validateMementoPeer(memento)) {\n\t\treturn false;\n\t}\n\tif (auto groupsMemento = dynamic_cast<Memento*>(memento.get())) {\n\t\tif (groupsMemento->user() == user()) {\n\t\t\trestoreState(groupsMemento);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(user());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\n} // namespace CommonGroups\n} // namespace Info\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/common_groups/info_common_groups_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <rpl/producer.h>\n#include \"info/info_content_widget.h\"\n\nstruct PeerListState;\n\nnamespace Ui {\nclass SearchFieldController;\n} // namespace Ui\n\nnamespace Info {\nnamespace CommonGroups {\n\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\texplicit Memento(not_null<UserData*> user);\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\tnot_null<UserData*> user() const;\n\n\tvoid setListState(std::unique_ptr<PeerListState> state);\n\tstd::unique_ptr<PeerListState> listState();\n\n\t~Memento();\n\nprivate:\n\tstd::unique_ptr<PeerListState> _listState;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<UserData*> user);\n\n\tnot_null<UserData*> user() const;\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\n\trpl::producer<QString> title() override;\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tInnerWidget *_inner = nullptr;\n\n};\n\n} // namespace CommonGroups\n} // namespace Info\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/downloads/info_downloads_inner_widget.h\"\n\n#include \"info/downloads/info_downloads_widget.h\"\n#include \"info/media/info_media_list_widget.h\"\n#include \"info/info_controller.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/search_field_controller.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info::Downloads {\n\nclass EmptyWidget : public Ui::RpWidget {\npublic:\n\tEmptyWidget(QWidget *parent);\n\n\tvoid setFullHeight(rpl::producer<int> fullHeightValue);\n\tvoid setSearchQuery(const QString &query);\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tobject_ptr<Ui::FlatLabel> _text;\n\tint _height = 0;\n\n};\n\nEmptyWidget::EmptyWidget(QWidget *parent)\n: RpWidget(parent)\n, _text(this, st::infoEmptyLabel) {\n}\n\nvoid EmptyWidget::setFullHeight(rpl::producer<int> fullHeightValue) {\n\tstd::move(\n\t\tfullHeightValue\n\t) | rpl::on_next([this](int fullHeight) {\n\t\t// Make icon center be on 1/3 height.\n\t\tauto iconCenter = fullHeight / 3;\n\t\tauto iconHeight = st::infoEmptyFile.height();\n\t\tauto iconTop = iconCenter - iconHeight / 2;\n\t\t_height = iconTop + st::infoEmptyIconTop;\n\t\tresizeToWidth(width());\n\t}, lifetime());\n}\n\nvoid EmptyWidget::setSearchQuery(const QString &query) {\n\t_text->setText(query.isEmpty()\n\t\t? tr::lng_media_file_empty(tr::now)\n\t\t: tr::lng_media_file_empty_search(tr::now));\n\tresizeToWidth(width());\n}\n\nint EmptyWidget::resizeGetHeight(int newWidth) {\n\tauto labelTop = _height - st::infoEmptyLabelTop;\n\tauto labelWidth = newWidth - 2 * st::infoEmptyLabelSkip;\n\t_text->resizeToNaturalWidth(labelWidth);\n\n\tauto labelLeft = (newWidth - _text->width()) / 2;\n\t_text->moveToLeft(labelLeft, labelTop, newWidth);\n\n\tupdate();\n\treturn _height;\n}\n\nvoid EmptyWidget::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto iconLeft = (width() - st::infoEmptyFile.width()) / 2;\n\tconst auto iconTop = height() - st::infoEmptyIconTop;\n\tst::infoEmptyFile.paint(p, iconLeft, iconTop, width());\n}\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _empty(this) {\n\t_empty->heightValue(\n\t) | rpl::on_next(\n\t\t[this] { refreshHeight(); },\n\t\t_empty->lifetime());\n\t_list = setupList();\n}\n\nvoid InnerWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tsetChildVisibleTopBottom(_list, visibleTop, visibleBottom);\n}\n\nbool InnerWidget::showInternal(not_null<Memento*> memento) {\n\tif (memento->section().type() == Section::Type::Downloads) {\n\t\trestoreState(memento);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nobject_ptr<Media::ListWidget> InnerWidget::setupList() {\n\tauto result = object_ptr<Media::ListWidget>(this, _controller);\n\tresult->heightValue(\n\t) | rpl::on_next(\n\t\t[this] { refreshHeight(); },\n\t\tresult->lifetime());\n\tusing namespace rpl::mappers;\n\tresult->scrollToRequests(\n\t) | rpl::map([widget = result.data()](int to) {\n\t\treturn Ui::ScrollToRequest {\n\t\t\twidget->y() + to,\n\t\t\t-1\n\t\t};\n\t}) | rpl::start_to_stream(\n\t\t_scrollToRequests,\n\t\tresult->lifetime());\n\t_selectedLists.fire(result->selectedListValue());\n\t_listTops.fire(result->topValue());\n\t_controller->searchQueryValue(\n\t) | rpl::on_next([this](const QString &query) {\n\t\t_empty->setSearchQuery(query);\n\t}, result->lifetime());\n\treturn result;\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\t_list->saveState(&memento->media());\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\t_list->restoreState(&memento->media());\n}\n\nrpl::producer<SelectedItems> InnerWidget::selectedListValue() const {\n\treturn _selectedLists.events_starting_with(\n\t\t_list->selectedListValue()\n\t) | rpl::flatten_latest();\n}\n\nvoid InnerWidget::selectionAction(SelectionAction action) {\n\t_list->selectionAction(action);\n}\n\nInnerWidget::~InnerWidget() = default;\n\nint InnerWidget::resizeGetHeight(int newWidth) {\n\t_inResize = true;\n\tauto guard = gsl::finally([this] { _inResize = false; });\n\n\t_list->resizeToWidth(newWidth);\n\t_empty->resizeToWidth(newWidth);\n\treturn recountHeight();\n}\n\nvoid InnerWidget::refreshHeight() {\n\tif (_inResize) {\n\t\treturn;\n\t}\n\tresize(width(), recountHeight());\n}\n\nint InnerWidget::recountHeight() {\n\tauto top = 0;\n\tauto listHeight = 0;\n\tif (_list) {\n\t\t_list->moveToLeft(0, top);\n\t\tlistHeight = _list->heightNoMargins();\n\t\ttop += listHeight;\n\t}\n\tif (listHeight > 0) {\n\t\t_empty->hide();\n\t} else {\n\t\t_empty->show();\n\t\t_empty->moveToLeft(0, top);\n\t\ttop += _empty->heightNoMargins();\n\t}\n\treturn top;\n}\n\nvoid InnerWidget::setScrollHeightValue(rpl::producer<int> value) {\n\tusing namespace rpl::mappers;\n\t_empty->setFullHeight(rpl::combine(\n\t\tstd::move(value),\n\t\t_listTops.events_starting_with(\n\t\t\t_list->topValue()\n\t\t) | rpl::flatten_latest(),\n\t\t_1 - _2));\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\n} // namespace Info::Downloads\n"
  },
  {
    "path": "Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"base/unique_qptr.h\"\n\nnamespace Ui {\nclass VerticalLayout;\nclass SearchFieldController;\n} // namespace Ui\n\nnamespace Info {\n\nclass Controller;\nstruct SelectedItems;\nenum class SelectionAction;\n\nnamespace Media {\nclass ListWidget;\n} // namespace Media\n\nnamespace Downloads {\n\nclass Memento;\nclass EmptyWidget;\n\nclass InnerWidget final : public Ui::RpWidget {\npublic:\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller);\n\n\tbool showInternal(not_null<Memento*> memento);\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tvoid setScrollHeightValue(rpl::producer<int> value);\n\n\trpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\trpl::producer<SelectedItems> selectedListValue() const;\n\tvoid selectionAction(SelectionAction action);\n\n\t~InnerWidget();\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\nprivate:\n\tint recountHeight();\n\tvoid refreshHeight();\n\n\tobject_ptr<Media::ListWidget> setupList();\n\n\tconst not_null<Controller*> _controller;\n\n\tobject_ptr<Media::ListWidget> _list = { nullptr };\n\tobject_ptr<EmptyWidget> _empty;\n\n\tbool _inResize = false;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\trpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;\n\trpl::event_stream<rpl::producer<int>> _listTops;\n\n};\n\n} // namespace Downloads\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/downloads/info_downloads_provider.h\"\n\n#include \"info/media/info_media_widget.h\"\n#include \"info/media/info_media_list_section.h\"\n#include \"info/info_controller.h\"\n#include \"ui/text/format_song_document_name.h\"\n#include \"ui/ui_utility.h\"\n#include \"data/data_download_manager.h\"\n#include \"data/data_document.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_session.h\"\n#include \"main/main_account.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history.h\"\n#include \"core/application.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"layout/layout_selection.h\"\n#include \"styles/style_overview.h\"\n\nnamespace Info::Downloads {\nnamespace {\n\nusing namespace Media;\n\n} // namespace\n\nProvider::Provider(not_null<AbstractController*> controller)\n: _controller(controller)\n, _storiesAddToAlbumId(_controller->storiesAddToAlbumId()) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tfor (auto &layout : _layouts) {\n\t\t\tlayout.second.item->invalidateCache();\n\t\t}\n\t}, _lifetime);\n}\n\nType Provider::type() {\n\treturn Type::File;\n}\n\nbool Provider::hasSelectRestriction() {\n\treturn false;\n}\n\nrpl::producer<bool> Provider::hasSelectRestrictionChanges() {\n\treturn rpl::never<bool>();\n}\n\nbool Provider::sectionHasFloatingHeader() {\n\treturn false;\n}\n\nQString Provider::sectionTitle(not_null<const BaseLayout*> item) {\n\treturn QString();\n}\n\nbool Provider::sectionItemBelongsHere(\n\t\tnot_null<const BaseLayout*> item,\n\t\tnot_null<const BaseLayout*> previous) {\n\treturn true;\n}\n\nbool Provider::isPossiblyMyItem(not_null<const HistoryItem*> item) {\n\treturn true;\n}\n\nstd::optional<int> Provider::fullCount() {\n\treturn _queryWords.empty()\n\t\t? _fullCount\n\t\t: (_foundCount || _fullCount.has_value())\n\t\t? _foundCount\n\t\t: std::optional<int>();\n}\n\nvoid Provider::restart() {\n}\n\nvoid Provider::checkPreload(\n\tQSize viewport,\n\tnot_null<BaseLayout*> topLayout,\n\tnot_null<BaseLayout*> bottomLayout,\n\tbool preloadTop,\n\tbool preloadBottom) {\n}\n\nvoid Provider::setSearchQuery(QString query) {\n\tif (_query == query) {\n\t\treturn;\n\t}\n\t_query = query;\n\tauto words = TextUtilities::PrepareSearchWords(_query);\n\tif (!_started || _queryWords == words) {\n\t\treturn;\n\t}\n\t_queryWords = std::move(words);\n\tif (searchMode()) {\n\t\t_foundCount = 0;\n\t\tfor (auto &element : _elements) {\n\t\t\tif ((element.found = computeIsFound(element))) {\n\t\t\t\t++_foundCount;\n\t\t\t}\n\t\t}\n\t}\n\t_refreshed.fire({});\n}\n\nvoid Provider::jumpToMessage(MsgId messageId, Fn<void(FullMsgId)>) {\n}\n\nvoid Provider::refreshViewer() {\n\tif (_started) {\n\t\treturn;\n\t}\n\t_started = true;\n\tauto &manager = Core::App().downloadManager();\n\trpl::single(rpl::empty) | rpl::then(\n\t\tmanager.loadingListChanges() | rpl::to_empty\n\t) | rpl::on_next([=, &manager] {\n\t\tauto copy = _downloading;\n\t\tfor (const auto id : manager.loadingList()) {\n\t\t\tif (!id->done) {\n\t\t\t\tconst auto item = id->object.item;\n\t\t\t\tif (!copy.remove(item) && !_downloaded.contains(item)) {\n\t\t\t\t\t_downloading.emplace(item);\n\t\t\t\t\taddElementNow({\n\t\t\t\t\t\t.item = item,\n\t\t\t\t\t\t.started = id->started,\n\t\t\t\t\t\t.path = id->path,\n\t\t\t\t\t});\n\t\t\t\t\ttrackItemSession(item);\n\t\t\t\t\trefreshPostponed(true);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor (const auto &item : copy) {\n\t\t\tAssert(!_downloaded.contains(item));\n\t\t\tremove(item);\n\t\t}\n\t\tif (!_fullCount.has_value()) {\n\t\t\trefreshPostponed(false);\n\t\t}\n\t}, _lifetime);\n\n\tfor (const auto id : manager.loadedList()) {\n\t\taddPostponed(id);\n\t}\n\n\tmanager.loadedAdded(\n\t) | rpl::on_next([=](not_null<const Data::DownloadedId*> entry) {\n\t\taddPostponed(entry);\n\t}, _lifetime);\n\n\tmanager.loadedRemoved(\n\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\tif (!_downloading.contains(item)) {\n\t\t\tremove(item);\n\t\t} else {\n\t\t\t_downloaded.remove(item);\n\t\t\t_addPostponed.erase(\n\t\t\t\tranges::remove(_addPostponed, item, &Element::item),\n\t\t\t\tend(_addPostponed));\n\t\t}\n\t}, _lifetime);\n\n\tmanager.loadedResolveDone(\n\t) | rpl::on_next([=] {\n\t\tif (!_fullCount.has_value()) {\n\t\t\t_fullCount = 0;\n\t\t}\n\t}, _lifetime);\n\n\tperformAdd();\n\tperformRefresh();\n}\n\nvoid Provider::addPostponed(not_null<const Data::DownloadedId*> entry) {\n\tExpects(entry->object != nullptr);\n\n\tconst auto item = entry->object->item;\n\ttrackItemSession(item);\n\tconst auto i = ranges::find(_addPostponed, item, &Element::item);\n\tif (i != end(_addPostponed)) {\n\t\ti->path = entry->path;\n\t\ti->started = entry->started;\n\t} else {\n\t\t_addPostponed.push_back({\n\t\t\t.item = item,\n\t\t\t.started = entry->started,\n\t\t\t.path = entry->path,\n\t\t});\n\t\tif (_addPostponed.size() == 1) {\n\t\t\tUi::PostponeCall(this, [=] {\n\t\t\t\tperformAdd();\n\t\t\t});\n\t\t}\n\t}\n}\n\nvoid Provider::performAdd() {\n\tif (_addPostponed.empty()) {\n\t\treturn;\n\t}\n\tfor (auto &element : base::take(_addPostponed)) {\n\t\t_downloaded.emplace(element.item);\n\t\tif (!_downloading.remove(element.item)) {\n\t\t\taddElementNow(std::move(element));\n\t\t}\n\t}\n\trefreshPostponed(true);\n}\n\nvoid Provider::addElementNow(Element &&element) {\n\t_elements.push_back(std::move(element));\n\tauto &added = _elements.back();\n\tfillSearchIndex(added);\n\tadded.found = searchMode() && computeIsFound(added);\n\tif (added.found) {\n\t\t++_foundCount;\n\t}\n}\n\nvoid Provider::remove(not_null<const HistoryItem*> item) {\n\t_addPostponed.erase(\n\t\tranges::remove(_addPostponed, item, &Element::item),\n\t\tend(_addPostponed));\n\t_downloading.remove(item);\n\t_downloaded.remove(item);\n\tconst auto proj = [&](const Element &element) {\n\t\tif (element.item != item) {\n\t\t\treturn false;\n\t\t} else if (element.found && searchMode()) {\n\t\t\t--_foundCount;\n\t\t}\n\t\treturn true;\n\t};\n\t_elements.erase(ranges::remove_if(_elements, proj), end(_elements));\n\tif (const auto i = _layouts.find(item); i != end(_layouts)) {\n\t\t_layoutRemoved.fire(i->second.item.get());\n\t\t_layouts.erase(i);\n\t}\n\trefreshPostponed(false);\n}\n\nvoid Provider::refreshPostponed(bool added) {\n\tif (added) {\n\t\t_postponedRefreshSort = true;\n\t}\n\tif (!_postponedRefresh) {\n\t\t_postponedRefresh = true;\n\t\tUi::PostponeCall(this, [=] {\n\t\t\tperformRefresh();\n\t\t});\n\t}\n}\n\nvoid Provider::performRefresh() {\n\tif (!_postponedRefresh) {\n\t\treturn;\n\t}\n\t_postponedRefresh = false;\n\tif (!_elements.empty() || _fullCount.has_value()) {\n\t\t_fullCount = _elements.size();\n\t}\n\tif (base::take(_postponedRefreshSort)) {\n\t\tranges::sort(_elements, ranges::less(), &Element::started);\n\t}\n\t_refreshed.fire({});\n}\n\nvoid Provider::trackItemSession(not_null<const HistoryItem*> item) {\n\tconst auto session = &item->history()->session();\n\tif (_trackedSessions.contains(session)) {\n\t\treturn;\n\t}\n\tauto &lifetime = _trackedSessions.emplace(session).first->second;\n\n\tsession->data().itemRemoved(\n\t) | rpl::on_next([this](auto item) {\n\t\titemRemoved(item);\n\t}, lifetime);\n\n\tsession->account().sessionChanges(\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t_trackedSessions.remove(session);\n\t}, lifetime);\n}\n\nrpl::producer<> Provider::refreshed() {\n\treturn _refreshed.events();\n}\n\nstd::vector<ListSection> Provider::fillSections(\n\t\tnot_null<Overview::Layout::Delegate*> delegate) {\n\tconst auto search = searchMode();\n\n\tif (!search) {\n\t\tmarkLayoutsStale();\n\t}\n\tconst auto guard = gsl::finally([&] { clearStaleLayouts(); });\n\n\tif (_elements.empty() || (search && !_foundCount)) {\n\t\treturn {};\n\t}\n\n\tauto result = std::vector<ListSection>();\n\tresult.emplace_back(Type::File, sectionDelegate());\n\tauto &section = result.back();\n\tfor (const auto &element : ranges::views::reverse(_elements)) {\n\t\tif (search && !element.found) {\n\t\t\tcontinue;\n\t\t} else if (auto layout = getLayout(element, delegate)) {\n\t\t\tsection.addItem(layout);\n\t\t}\n\t}\n\tsection.finishSection();\n\treturn result;\n}\n\nvoid Provider::markLayoutsStale() {\n\tfor (auto &layout : _layouts) {\n\t\tlayout.second.stale = true;\n\t}\n}\n\nvoid Provider::clearStaleLayouts() {\n\tfor (auto i = _layouts.begin(); i != _layouts.end();) {\n\t\tif (i->second.stale) {\n\t\t\t_layoutRemoved.fire(i->second.item.get());\n\t\t\ti = _layouts.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n}\n\nrpl::producer<not_null<BaseLayout*>> Provider::layoutRemoved() {\n\treturn _layoutRemoved.events();\n}\n\nBaseLayout *Provider::lookupLayout(const HistoryItem *item) {\n\treturn nullptr;\n}\n\nbool Provider::isMyItem(not_null<const HistoryItem*> item) {\n\treturn _downloading.contains(item) || _downloaded.contains(item);\n}\n\nbool Provider::isAfter(\n\t\tnot_null<const HistoryItem*> a,\n\t\tnot_null<const HistoryItem*> b) {\n\tif (a != b) {\n\t\tfor (const auto &element : _elements) {\n\t\t\tif (element.item == a) {\n\t\t\t\treturn false;\n\t\t\t} else if (element.item == b) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nbool Provider::searchMode() const {\n\treturn !_queryWords.empty();\n}\n\nvoid Provider::fillSearchIndex(Element &element) {\n\tauto strings = QStringList(QFileInfo(element.path).fileName());\n\tif (const auto media = element.item->media()) {\n\t\tif (const auto document = media->document()) {\n\t\t\tstrings.append(document->filename());\n\t\t\tstrings.append(Ui::Text::FormatDownloadsName(document).text);\n\t\t}\n\t}\n\telement.words = TextUtilities::PrepareSearchWords(strings.join(' '));\n\telement.letters.clear();\n\tfor (const auto &word : element.words) {\n\t\telement.letters.emplace(word.front());\n\t}\n}\n\nbool Provider::computeIsFound(const Element &element) const {\n\tExpects(!_queryWords.empty());\n\n\tconst auto has = [&](const QString &queryWord) {\n\t\tif (!element.letters.contains(queryWord.front())) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (const auto &word : element.words) {\n\t\t\tif (word.startsWith(queryWord)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\tfor (const auto &queryWord : _queryWords) {\n\t\tif (!has(queryWord)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid Provider::itemRemoved(not_null<const HistoryItem*> item) {\n\tremove(item);\n}\n\nBaseLayout *Provider::getLayout(\n\t\tElement element,\n\t\tnot_null<Overview::Layout::Delegate*> delegate) {\n\tauto it = _layouts.find(element.item);\n\tif (it == _layouts.end()) {\n\t\tif (auto layout = createLayout(element, delegate)) {\n\t\t\tlayout->initDimensions();\n\t\t\tit = _layouts.emplace(element.item, std::move(layout)).first;\n\t\t} else {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\tit->second.stale = false;\n\treturn it->second.item.get();\n}\n\nstd::unique_ptr<BaseLayout> Provider::createLayout(\n\t\tElement element,\n\t\tnot_null<Overview::Layout::Delegate*> delegate) {\n\tconst auto getFile = [&]() -> DocumentData* {\n\t\tif (auto media = element.item->media()) {\n\t\t\treturn media->document();\n\t\t}\n\t\treturn nullptr;\n\t};\n\n\tusing namespace Overview::Layout;\n\tauto &songSt = st::overviewFileLayout;\n\tif (const auto file = getFile()) {\n\t\treturn std::make_unique<Document>(\n\t\t\tdelegate,\n\t\t\telement.item,\n\t\t\tDocumentFields{\n\t\t\t\t.document = file,\n\t\t\t\t.dateOverride = Data::DateFromDownloadDate(element.started),\n\t\t\t\t.forceFileLayout = true,\n\t\t\t},\n\t\t\tsongSt);\n\t}\n\treturn nullptr;\n}\n\nListItemSelectionData Provider::computeSelectionData(\n\t\tnot_null<const HistoryItem*> item,\n\t\tTextSelection selection) {\n\tauto result = ListItemSelectionData(selection);\n\tresult.canDelete = true;\n\tresult.canForward = item->allowsForward()\n\t\t&& (&item->history()->session() == &_controller->session());\n\treturn result;\n}\n\nvoid Provider::applyDragSelection(\n\t\tListSelectedMap &selected,\n\t\tnot_null<const HistoryItem*> fromItem,\n\t\tbool skipFrom,\n\t\tnot_null<const HistoryItem*> tillItem,\n\t\tbool skipTill) {\n\tauto from = ranges::find(_elements, fromItem, &Element::item);\n\tauto till = ranges::find(_elements, tillItem, &Element::item);\n\tif (from == end(_elements) || till == end(_elements)) {\n\t\treturn;\n\t}\n\tif (skipFrom) {\n\t\t++from;\n\t}\n\tif (!skipTill) {\n\t\t++till;\n\t}\n\tif (from >= till) {\n\t\tselected.clear();\n\t\treturn;\n\t}\n\tconst auto search = !_queryWords.isEmpty();\n\tconst auto selectLimit = _storiesAddToAlbumId\n\t\t? _controller->session().appConfig().storiesAlbumLimit()\n\t\t: MaxSelectedItems;\n\tauto chosen = base::flat_set<not_null<const HistoryItem*>>();\n\tchosen.reserve(till - from);\n\tfor (auto i = from; i != till; ++i) {\n\t\tif (search && !i->found) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto item = i->item;\n\t\tchosen.emplace(item);\n\t\tChangeItemSelection(\n\t\t\tselected,\n\t\t\titem,\n\t\t\tcomputeSelectionData(item, FullSelection),\n\t\t\tselectLimit);\n\t}\n\tif (selected.size() != chosen.size()) {\n\t\tfor (auto i = begin(selected); i != end(selected);) {\n\t\t\tif (selected.contains(i->first)) {\n\t\t\t\t++i;\n\t\t\t} else {\n\t\t\t\ti = selected.erase(i);\n\t\t\t}\n\t\t}\n\t}\n}\n\nbool Provider::allowSaveFileAs(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) {\n\treturn false;\n}\n\nQString Provider::showInFolderPath(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) {\n\tconst auto i = ranges::find(_elements, item, &Element::item);\n\treturn (i != end(_elements)) ? i->path : QString();\n}\n\nint64 Provider::scrollTopStatePosition(not_null<HistoryItem*> item) {\n\tconst auto i = ranges::find(_elements, item, &Element::item);\n\treturn (i != end(_elements)) ? i->started : 0;\n}\n\nHistoryItem *Provider::scrollTopStateItem(ListScrollTopState state) {\n\tif (!state.position) {\n\t\treturn _elements.empty() ? nullptr : _elements.back().item.get();\n\t}\n\tconst auto i = ranges::lower_bound(\n\t\t_elements,\n\t\tstate.position,\n\t\tranges::less(),\n\t\t&Element::started);\n\treturn (i != end(_elements))\n\t\t? i->item.get()\n\t\t: _elements.empty()\n\t\t? nullptr\n\t\t: _elements.back().item.get();\n}\n\nvoid Provider::saveState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tListScrollTopState scrollState) {\n\tif (!_elements.empty() && scrollState.item) {\n\t\tmemento->setAroundId({ PeerId(), 1 });\n\t\tmemento->setScrollTopItem(scrollState.item->globalId());\n\t\tmemento->setScrollTopItemPosition(scrollState.position);\n\t\tmemento->setScrollTopShift(scrollState.shift);\n\t}\n}\n\nvoid Provider::restoreState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tFn<void(ListScrollTopState)> restoreScrollState) {\n\tif (memento->aroundId() == FullMsgId(PeerId(), 1)) {\n\t\trestoreScrollState({\n\t\t\t.position = memento->scrollTopItemPosition(),\n\t\t\t.item = MessageByGlobalId(memento->scrollTopItem()),\n\t\t\t.shift = memento->scrollTopShift(),\n\t\t});\n\t\trefreshViewer();\n\t}\n}\n\n} // namespace Info::Downloads\n"
  },
  {
    "path": "Telegram/SourceFiles/info/downloads/info_downloads_provider.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/media/info_media_common.h\"\n#include \"base/weak_ptr.h\"\n\nnamespace Data {\nstruct DownloadedId;\n} // namespace Data\n\nnamespace Info {\nclass AbstractController;\n} // namespace Info\n\nnamespace Info::Downloads {\n\nclass Provider final\n\t: public Media::ListProvider\n\t, private Media::ListSectionDelegate\n\t, public base::has_weak_ptr {\npublic:\n\texplicit Provider(not_null<AbstractController*> controller);\n\n\tMedia::Type type() override;\n\tbool hasSelectRestriction() override;\n\trpl::producer<bool> hasSelectRestrictionChanges() override;\n\tbool isPossiblyMyItem(not_null<const HistoryItem*> item) override;\n\n\tstd::optional<int> fullCount() override;\n\n\tvoid restart() override;\n\tvoid checkPreload(\n\t\tQSize viewport,\n\t\tnot_null<Media::BaseLayout*> topLayout,\n\t\tnot_null<Media::BaseLayout*> bottomLayout,\n\t\tbool preloadTop,\n\t\tbool preloadBottom) override;\n\tvoid refreshViewer() override;\n\trpl::producer<> refreshed() override;\n\n\tvoid setSearchQuery(QString query) override;\n\tvoid jumpToMessage(MsgId messageId, Fn<void(FullMsgId)> callback) override;\n\n\tstd::vector<Media::ListSection> fillSections(\n\t\tnot_null<Overview::Layout::Delegate*> delegate) override;\n\trpl::producer<not_null<Media::BaseLayout*>> layoutRemoved() override;\n\tMedia::BaseLayout *lookupLayout(const HistoryItem *item) override;\n\tbool isMyItem(not_null<const HistoryItem*> item) override;\n\tbool isAfter(\n\t\tnot_null<const HistoryItem*> a,\n\t\tnot_null<const HistoryItem*> b) override;\n\n\tMedia::ListItemSelectionData computeSelectionData(\n\t\tnot_null<const HistoryItem*> item,\n\t\tTextSelection selection) override;\n\tvoid applyDragSelection(\n\t\tMedia::ListSelectedMap &selected,\n\t\tnot_null<const HistoryItem*> fromItem,\n\t\tbool skipFrom,\n\t\tnot_null<const HistoryItem*> tillItem,\n\t\tbool skipTill) override;\n\n\tbool allowSaveFileAs(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) override;\n\tQString showInFolderPath(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) override;\n\n\n\tint64 scrollTopStatePosition(not_null<HistoryItem*> item) override;\n\tHistoryItem *scrollTopStateItem(\n\t\tMedia::ListScrollTopState state) override;\n\tvoid saveState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tMedia::ListScrollTopState scrollState) override;\n\tvoid restoreState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tFn<void(Media::ListScrollTopState)> restoreScrollState) override;\n\nprivate:\n\tstruct Element {\n\t\tnot_null<HistoryItem*> item;\n\t\tint64 started = 0; // unixtime * 1000\n\t\tQString path;\n\n\t\tQStringList words;\n\t\tbase::flat_set<QChar> letters;\n\t\tbool found = false;\n\t};\n\n\tbool sectionHasFloatingHeader() override;\n\tQString sectionTitle(not_null<const Media::BaseLayout*> item) override;\n\tbool sectionItemBelongsHere(\n\t\tnot_null<const Media::BaseLayout*> item,\n\t\tnot_null<const Media::BaseLayout*> previous) override;\n\n\t[[nodiscard]] bool searchMode() const;\n\tvoid fillSearchIndex(Element &element);\n\t[[nodiscard]] bool computeIsFound(const Element &element) const;\n\n\tvoid itemRemoved(not_null<const HistoryItem*> item);\n\tvoid markLayoutsStale();\n\tvoid clearStaleLayouts();\n\n\tvoid refreshPostponed(bool added);\n\tvoid addPostponed(not_null<const Data::DownloadedId*> entry);\n\tvoid performRefresh();\n\tvoid performAdd();\n\tvoid addElementNow(Element &&element);\n\tvoid remove(not_null<const HistoryItem*> item);\n\tvoid trackItemSession(not_null<const HistoryItem*> item);\n\n\t[[nodiscard]] Media::BaseLayout *getLayout(\n\t\tElement element,\n\t\tnot_null<Overview::Layout::Delegate*> delegate);\n\t[[nodiscard]] std::unique_ptr<Media::BaseLayout> createLayout(\n\t\tElement element,\n\t\tnot_null<Overview::Layout::Delegate*> delegate);\n\n\tconst not_null<AbstractController*> _controller;\n\n\tstd::vector<Element> _elements;\n\tstd::optional<int> _fullCount;\n\tbase::flat_set<not_null<const HistoryItem*>> _downloading;\n\tbase::flat_set<not_null<const HistoryItem*>> _downloaded;\n\tint _storiesAddToAlbumId = 0;\n\n\tstd::vector<Element> _addPostponed;\n\n\tstd::unordered_map<\n\t\tnot_null<const HistoryItem*>,\n\t\tMedia::CachedItem> _layouts;\n\trpl::event_stream<not_null<Media::BaseLayout*>> _layoutRemoved;\n\trpl::event_stream<> _refreshed;\n\n\tQString _query;\n\tQStringList _queryWords;\n\tint _foundCount = 0;\n\n\tbase::flat_map<not_null<Main::Session*>, rpl::lifetime> _trackedSessions;\n\tbool _postponedRefreshSort = false;\n\tbool _postponedRefresh = false;\n\tbool _started = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Info::Downloads\n"
  },
  {
    "path": "Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/downloads/info_downloads_widget.h\"\n\n#include \"info/downloads/info_downloads_inner_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/search_field_controller.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/ui_utility.h\"\n#include \"data/data_download_manager.h\"\n#include \"data/data_user.h\"\n#include \"core/application.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Info::Downloads {\n\nMemento::Memento(not_null<Controller*> controller)\n: ContentMemento(Tag{})\n, _media(controller) {\n}\n\nMemento::Memento(not_null<UserData*> self)\n: ContentMemento(Tag{})\n, _media(self, 0, Media::Type::File) {\n}\n\nMemento::~Memento() = default;\n\nSection Memento::section() const {\n\treturn Section(Section::Type::Downloads);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(parent, controller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: ContentWidget(parent, controller) {\n\t_inner = setInnerWidget(object_ptr<InnerWidget>(\n\t\tthis,\n\t\tcontroller));\n\t_inner->setScrollHeightValue(scrollHeightValue());\n\t_inner->scrollToRequests(\n\t) | rpl::on_next([this](Ui::ScrollToRequest request) {\n\t\tscrollTo(request);\n\t}, _inner->lifetime());\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\tif (auto downloadsMemento = dynamic_cast<Memento*>(memento.get())) {\n\t\trestoreState(downloadsMemento);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(controller());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\nrpl::producer<SelectedItems> Widget::selectedListValue() const {\n\treturn _inner->selectedListValue();\n}\n\nvoid Widget::selectionAction(SelectionAction action) {\n\t_inner->selectionAction(action);\n}\n\nvoid Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {\n\tconst auto window = controller()->parentController();\n\tconst auto deleteAll = [=] {\n\t\tauto &manager = Core::App().downloadManager();\n\t\tconst auto phrase = tr::lng_downloads_delete_sure_all(tr::now);\n\t\tconst auto added = manager.loadedHasNonCloudFile()\n\t\t\t? QString()\n\t\t\t: tr::lng_downloads_delete_in_cloud(tr::now);\n\t\tconst auto deleteSure = [=, &manager](Fn<void()> close) {\n\t\t\tUi::PostponeCall(this, close);\n\t\t\tmanager.deleteAll();\n\t\t};\n\t\twindow->show(Ui::MakeConfirmBox({\n\t\t\t.text = phrase + (added.isEmpty() ? QString() : \"\\n\\n\" + added),\n\t\t\t.confirmed = deleteSure,\n\t\t\t.confirmText = tr::lng_box_delete(tr::now),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t}));\n\t};\n\taddAction(\n\t\ttr::lng_context_delete_all_files(tr::now),\n\t\tdeleteAll,\n\t\t&st::menuIconDelete);\n}\n\nrpl::producer<QString> Widget::title() {\n\treturn tr::lng_downloads_section();\n}\n\nstd::shared_ptr<Info::Memento> Make(not_null<UserData*> self) {\n\treturn std::make_shared<Info::Memento>(\n\t\tstd::vector<std::shared_ptr<ContentMemento>>(\n\t\t\t1,\n\t\t\tstd::make_shared<Memento>(self)));\n}\n\n} // namespace Info::Downloads\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/downloads/info_downloads_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n#include \"info/media/info_media_widget.h\"\n\nnamespace Ui {\nclass SearchFieldController;\n} // namespace Ui\n\nnamespace Info::Downloads {\n\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\tMemento(not_null<Controller*> controller);\n\tMemento(not_null<UserData*> self);\n\t~Memento();\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\t[[nodiscard]] Media::Memento &media() {\n\t\treturn _media;\n\t}\n\t[[nodiscard]] const Media::Memento &media() const {\n\t\treturn _media;\n\t}\n\nprivate:\n\tMedia::Memento _media;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(QWidget *parent, not_null<Controller*> controller);\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\n\trpl::producer<SelectedItems> selectedListValue() const override;\n\tvoid selectionAction(SelectionAction action) override;\n\n\tvoid fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override;\n\n\trpl::producer<QString> title() override;\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tInnerWidget *_inner = nullptr;\n\n};\n\n[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<UserData*> self);\n\n} // namespace Info::Downloads\n"
  },
  {
    "path": "Telegram/SourceFiles/info/global_media/info_global_media_inner_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/global_media/info_global_media_inner_widget.h\"\n\n#include \"info/global_media/info_global_media_provider.h\"\n#include \"info/global_media/info_global_media_widget.h\"\n#include \"info/media/info_media_empty_widget.h\"\n#include \"info/media/info_media_list_widget.h\"\n#include \"info/info_controller.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/search_field_controller.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info::GlobalMedia {\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _empty(this) {\n\t_empty->setType(type());\n\t_empty->heightValue(\n\t) | rpl::on_next(\n\t\t[this] { refreshHeight(); },\n\t\t_empty->lifetime());\n\t_list = setupList();\n}\n\nobject_ptr<Media::ListWidget> InnerWidget::setupList() {\n\tauto result = object_ptr<Media::ListWidget>(this, _controller);\n\n\t// Setup list widget connections\n\tresult->heightValue(\n\t) | rpl::on_next([this] {\n\t\trefreshHeight();\n\t}, result->lifetime());\n\n\tusing namespace rpl::mappers;\n\tresult->scrollToRequests(\n\t) | rpl::map([widget = result.data()](int to) {\n\t\treturn Ui::ScrollToRequest{\n\t\t\twidget->y() + to,\n\t\t\t-1\n\t\t};\n\t}) | rpl::start_to_stream(\n\t\t_scrollToRequests,\n\t\tresult->lifetime());\n\n\t_controller->searchQueryValue(\n\t) | rpl::on_next([this](const QString &query) {\n\t\t_empty->setSearchQuery(query);\n\t}, result->lifetime());\n\n\treturn result;\n}\n\nStorage::SharedMediaType InnerWidget::type() const {\n\treturn _controller->section().mediaType();\n}\n\nvoid InnerWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tsetChildVisibleTopBottom(_list, visibleTop, visibleBottom);\n}\n\nbool InnerWidget::showInternal(not_null<Memento*> memento) {\n\tif (memento->section().type() == Section::Type::GlobalMedia\n\t\t&& memento->section().mediaType() == type()) {\n\t\trestoreState(memento);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\t_list->saveState(&memento->media());\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\t_list->restoreState(&memento->media());\n}\n\nrpl::producer<SelectedItems> InnerWidget::selectedListValue() const {\n\treturn _selectedLists.events_starting_with(\n\t\t_list->selectedListValue()\n\t) | rpl::flatten_latest();\n}\n\nvoid InnerWidget::selectionAction(SelectionAction action) {\n\t_list->selectionAction(action);\n}\n\nInnerWidget::~InnerWidget() = default;\n\nint InnerWidget::resizeGetHeight(int newWidth) {\n\t_inResize = true;\n\tauto guard = gsl::finally([this] { _inResize = false; });\n\n\t_list->resizeToWidth(newWidth);\n\t_empty->resizeToWidth(newWidth);\n\treturn recountHeight();\n}\n\nvoid InnerWidget::refreshHeight() {\n\tif (_inResize) {\n\t\treturn;\n\t}\n\tresize(width(), recountHeight());\n}\n\nint InnerWidget::recountHeight() {\n\tauto top = 0;\n\tauto listHeight = 0;\n\tif (_list) {\n\t\t_list->moveToLeft(0, top);\n\t\tlistHeight = _list->heightNoMargins();\n\t\ttop += listHeight;\n\t}\n\tif (listHeight > 0) {\n\t\t_empty->hide();\n\t} else {\n\t\t_empty->show();\n\t\t_empty->moveToLeft(0, top);\n\t\ttop += _empty->heightNoMargins();\n\t}\n\treturn top;\n}\n\nvoid InnerWidget::setScrollHeightValue(rpl::producer<int> value) {\n\tusing namespace rpl::mappers;\n\t_empty->setFullHeight(rpl::combine(\n\t\tstd::move(value),\n\t\t_listTops.events_starting_with(\n\t\t\t_list->topValue()\n\t\t) | rpl::flatten_latest(),\n\t\t_1 - _2));\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\n} // namespace Info::GlobalMedia"
  },
  {
    "path": "Telegram/SourceFiles/info/global_media/info_global_media_inner_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"base/unique_qptr.h\"\n\nnamespace Ui {\nclass VerticalLayout;\nclass SearchFieldController;\n} // namespace Ui\n\nnamespace Storage {\nenum class SharedMediaType : signed char;\n} // namespace Storage\n\nnamespace Info {\nclass Controller;\nstruct SelectedItems;\nenum class SelectionAction;\n} // namespace Info\n\nnamespace Info::Media {\nclass ListWidget;\nclass EmptyWidget;\n} // namespace Info::Media\n\nnamespace Info::GlobalMedia {\n\nclass Memento;\nclass EmptyWidget;\n\nclass InnerWidget final : public Ui::RpWidget {\npublic:\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller);\n\n\tbool showInternal(not_null<Memento*> memento);\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tvoid setScrollHeightValue(rpl::producer<int> value);\n\n\trpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\trpl::producer<SelectedItems> selectedListValue() const;\n\tvoid selectionAction(SelectionAction action);\n\n\t~InnerWidget();\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\nprivate:\n\tint recountHeight();\n\tvoid refreshHeight();\n\n\tStorage::SharedMediaType type() const;\n\n\tobject_ptr<Media::ListWidget> setupList();\n\n\tconst not_null<Controller*> _controller;\n\n\tobject_ptr<Media::ListWidget> _list = { nullptr };\n\tobject_ptr<Media::EmptyWidget> _empty;\n\n\tbool _inResize = false;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\trpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;\n\trpl::event_stream<rpl::producer<int>> _listTops;\n\n};\n\n} // namespace Info::GlobalMedia\n"
  },
  {
    "path": "Telegram/SourceFiles/info/global_media/info_global_media_provider.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/global_media/info_global_media_provider.h\"\n\n#include \"apiwrap.h\"\n#include \"info/media/info_media_widget.h\"\n#include \"info/media/info_media_list_section.h\"\n#include \"info/info_controller.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/text/format_song_document_name.h\"\n#include \"ui/ui_utility.h\"\n#include \"data/data_document.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"main/main_account.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history.h\"\n#include \"core/application.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"layout/layout_selection.h\"\n#include \"styles/style_overview.h\"\n\nnamespace Info::GlobalMedia {\nnamespace {\n\nconstexpr auto kPerPage = 50;\nconstexpr auto kPreloadedScreensCount = 4;\nconstexpr auto kPreloadedScreensCountFull\n\t= kPreloadedScreensCount + 1 + kPreloadedScreensCount;\n\n} // namespace\n\nGlobalMediaSlice::GlobalMediaSlice(\n\tKey key,\n\tstd::vector<Data::MessagePosition> items,\n\tstd::optional<int> fullCount,\n\tint skippedAfter)\n: _key(key)\n, _items(std::move(items))\n, _fullCount(fullCount)\n, _skippedAfter(skippedAfter) {\n}\n\nstd::optional<int> GlobalMediaSlice::fullCount() const {\n\treturn _fullCount;\n}\n\nstd::optional<int> GlobalMediaSlice::skippedBefore() const {\n\treturn _fullCount\n\t\t? int(*_fullCount - _skippedAfter - _items.size())\n\t\t: std::optional<int>();\n}\n\nstd::optional<int> GlobalMediaSlice::skippedAfter() const {\n\treturn _skippedAfter;\n}\n\nstd::optional<int> GlobalMediaSlice::indexOf(Value position) const {\n\tconst auto it = ranges::find(_items, position);\n\treturn (it != end(_items))\n\t\t? std::make_optional(int(it - begin(_items)))\n\t\t: std::nullopt;\n}\n\nint GlobalMediaSlice::size() const {\n\treturn _items.size();\n}\n\nGlobalMediaSlice::Value GlobalMediaSlice::operator[](int index) const {\n\tExpects(index >= 0 && index < size());\n\n\treturn _items[index];\n}\n\nstd::optional<int> GlobalMediaSlice::distance(\n\t\tconst Key &a,\n\t\tconst Key &b) const {\n\tconst auto i = indexOf(a.aroundId);\n\tconst auto j = indexOf(b.aroundId);\n\treturn (i && j) ? std::make_optional(*j - *i) : std::nullopt;\n}\n\nstd::optional<GlobalMediaSlice::Value> GlobalMediaSlice::nearest(\n\t\tValue position) const {\n\tif (_items.empty()) {\n\t\treturn std::nullopt;\n\t}\n\n\tconst auto it = ranges::lower_bound(\n\t\t_items,\n\t\tposition,\n\t\tstd::greater<>{});\n\n\tif (it == end(_items)) {\n\t\treturn _items.back();\n\t} else if (it == begin(_items)) {\n\t\treturn _items.front();\n\t}\n\treturn *it;\n}\n\nProvider::Provider(not_null<AbstractController*> controller)\n: _controller(controller)\n, _type(_controller->section().mediaType())\n, _slice(sliceKey(_aroundId)) {\n\t_controller->session().data().itemRemoved(\n\t) | rpl::on_next([this](auto item) {\n\t\titemRemoved(item);\n\t}, _lifetime);\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tfor (auto &layout : _layouts) {\n\t\t\tlayout.second.item->invalidateCache();\n\t\t}\n\t}, _lifetime);\n}\n\nProvider::Type Provider::type() {\n\treturn _type;\n}\n\nbool Provider::hasSelectRestriction() {\n\treturn true;\n}\n\nrpl::producer<bool> Provider::hasSelectRestrictionChanges() {\n\treturn rpl::never<bool>();\n}\n\nbool Provider::sectionHasFloatingHeader() {\n\tswitch (_type) {\n\tcase Type::Photo:\n\tcase Type::GIF:\n\tcase Type::Video:\n\tcase Type::RoundFile:\n\tcase Type::RoundVoiceFile:\n\tcase Type::MusicFile:\n\t\treturn false;\n\tcase Type::File:\n\tcase Type::Link:\n\t\treturn true;\n\t}\n\tUnexpected(\"Type in HasFloatingHeader()\");\n}\n\nQString Provider::sectionTitle(not_null<const BaseLayout*> item) {\n\treturn QString();\n}\n\nbool Provider::sectionItemBelongsHere(\n\t\tnot_null<const BaseLayout*> item,\n\t\tnot_null<const BaseLayout*> previous) {\n\treturn true;\n}\n\nbool Provider::isPossiblyMyItem(not_null<const HistoryItem*> item) {\n\treturn item->media() != nullptr;\n}\n\nstd::optional<int> Provider::fullCount() {\n\treturn _slice.fullCount();\n}\n\nvoid Provider::restart() {\n\t_layouts.clear();\n\t_aroundId = Data::MaxMessagePosition;\n\t_idsLimit = kMinimalIdsLimit;\n\t_slice = GlobalMediaSlice(sliceKey(_aroundId));\n\trefreshViewer();\n}\n\nvoid Provider::checkPreload(\n\t\tQSize viewport,\n\t\tnot_null<BaseLayout*> topLayout,\n\t\tnot_null<BaseLayout*> bottomLayout,\n\t\tbool preloadTop,\n\t\tbool preloadBottom) {\n\tconst auto visibleWidth = viewport.width();\n\tconst auto visibleHeight = viewport.height();\n\tconst auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;\n\tconst auto minItemHeight = Media::MinItemHeight(_type, visibleWidth);\n\tconst auto preloadedCount = preloadedHeight / minItemHeight;\n\tconst auto preloadIdsLimitMin = (preloadedCount / 2) + 1;\n\tconst auto preloadIdsLimit = preloadIdsLimitMin\n\t\t+ (visibleHeight / minItemHeight);\n\tconst auto after = _slice.skippedAfter();\n\tconst auto topLoaded = after && (*after == 0);\n\tconst auto before = _slice.skippedBefore();\n\tconst auto bottomLoaded = before && (*before == 0);\n\n\tconst auto minScreenDelta = kPreloadedScreensCount\n\t\t- Media::kPreloadIfLessThanScreens;\n\tconst auto minUniversalIdDelta = (minScreenDelta * visibleHeight)\n\t\t/ minItemHeight;\n\tconst auto preloadAroundItem = [&](not_null<BaseLayout*> layout) {\n\t\tauto preloadRequired = false;\n\t\tauto aroundId = layout->getItem()->position();\n\t\tif (!preloadRequired) {\n\t\t\tpreloadRequired = (_idsLimit < preloadIdsLimitMin);\n\t\t}\n\t\tif (!preloadRequired) {\n\t\t\tauto delta = _slice.distance(\n\t\t\t\tsliceKey(_aroundId),\n\t\t\t\tsliceKey(aroundId));\n\t\t\tAssert(delta != std::nullopt);\n\t\t\tpreloadRequired = (qAbs(*delta) >= minUniversalIdDelta);\n\t\t}\n\t\tif (preloadRequired) {\n\t\t\t_idsLimit = preloadIdsLimit;\n\t\t\t_aroundId = aroundId;\n\t\t\trefreshViewer();\n\t\t}\n\t};\n\n\tif (preloadTop && !topLoaded) {\n\t\tpreloadAroundItem(topLayout);\n\t} else if (preloadBottom && !bottomLoaded) {\n\t\tpreloadAroundItem(bottomLayout);\n\t}\n}\n\nrpl::producer<GlobalMediaSlice> Provider::source(\n\t\tType type,\n\t\tData::MessagePosition aroundId,\n\t\tQString query,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tExpects(_type == type);\n\n\t_totalListQuery = query;\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tconst auto session = &_controller->session();\n\n\t\tstruct State : base::has_weak_ptr {\n\t\t\tState(not_null<Main::Session*> session) : session(session) {\n\t\t\t}\n\t\t\t~State() {\n\t\t\t\tsession->api().request(requestId).cancel();\n\t\t\t}\n\n\t\t\tconst not_null<Main::Session*> session;\n\t\t\tFn<void()> pushAndLoadMore;\n\t\t\tmtpRequestId requestId = 0;\n\t\t};\n\t\tconst auto state = lifetime.make_state<State>(session);\n\t\tconst auto guard = base::make_weak(state);\n\n\t\tstate->pushAndLoadMore = [=] {\n\t\t\tauto result = fillRequest(aroundId, limitBefore, limitAfter);\n\n\t\t\t// May destroy 'state' by calling source() with different args.\n\t\t\tconsumer.put_next(std::move(result.slice));\n\n\t\t\tif (guard && !currentList()->loaded && result.notEnough) {\n\t\t\t\tstate->requestId = requestMore(state->pushAndLoadMore);\n\t\t\t}\n\t\t};\n\t\tstate->pushAndLoadMore();\n\n\t\treturn lifetime;\n\t};\n}\n\nmtpRequestId Provider::requestMore(Fn<void()> loaded) {\n\tconst auto done = [=](const Api::GlobalMediaResult &result) {\n\t\tconst auto list = currentList();\n\t\tif (result.messageIds.empty()) {\n\t\t\tlist->loaded = true;\n\t\t\tlist->fullCount = list->list.size();\n\t\t} else {\n\t\t\tlist->list.reserve(list->list.size() + result.messageIds.size());\n\t\t\tlist->fullCount = result.fullCount;\n\t\t\tfor (const auto &position : result.messageIds) {\n\t\t\t\t_seenIds.emplace(position.fullId);\n\t\t\t\tlist->offsetPosition = position;\n\t\t\t\tlist->list.push_back(position);\n\t\t\t}\n\t\t}\n\t\tif (!result.offsetRate) {\n\t\t\tlist->loaded = true;\n\t\t} else {\n\t\t\tlist->offsetRate = result.offsetRate;\n\t\t}\n\t\tloaded();\n\t};\n\tconst auto list = currentList();\n\treturn _controller->session().api().requestGlobalMedia(\n\t\t_type,\n\t\t_totalListQuery,\n\t\tlist->offsetRate,\n\t\tlist->offsetPosition,\n\t\tdone);\n}\n\nProvider::FillResult Provider::fillRequest(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tconst auto list = currentList();\n\tconst auto i = ranges::lower_bound(\n\t\tlist->list,\n\t\taroundId,\n\t\tstd::greater<>());\n\tconst auto hasAfter = int(i - begin(list->list));\n\tconst auto hasBefore = int(end(list->list) - i);\n\tconst auto takeAfter = std::min(limitAfter, hasAfter);\n\tconst auto takeBefore = std::min(limitBefore, hasBefore);\n\tauto messages = std::vector<Data::MessagePosition>{\n\t\ti - takeAfter,\n\t\ti + takeBefore,\n\t};\n\treturn FillResult{\n\t\t.slice = GlobalMediaSlice(\n\t\t\tGlobalMediaKey{ aroundId },\n\t\t\tstd::move(messages),\n\t\t\t((!list->list.empty() || list->loaded)\n\t\t\t\t? list->fullCount\n\t\t\t\t: std::optional<int>()),\n\t\t\thasAfter - takeAfter),\n\t\t.notEnough = (takeBefore < limitBefore),\n\t};\n}\n\nvoid Provider::refreshViewer() {\n\t_viewerLifetime.destroy();\n\t_controller->searchQueryValue(\n\t) | rpl::map([=](QString query) {\n\t\treturn source(\n\t\t\t_type,\n\t\t\tsliceKey(_aroundId).aroundId,\n\t\t\tquery,\n\t\t\t_idsLimit,\n\t\t\t_idsLimit);\n\t}) | rpl::flatten_latest(\n\t) | rpl::on_next([=](GlobalMediaSlice &&slice) {\n\t\tif (!slice.fullCount()) {\n\t\t\t// Don't display anything while full count is unknown.\n\t\t\treturn;\n\t\t}\n\t\t_slice = std::move(slice);\n\t\tif (auto nearest = _slice.nearest(_aroundId)) {\n\t\t\t_aroundId = *nearest;\n\t\t}\n\t\t_refreshed.fire({});\n\t}, _viewerLifetime);\n}\n\nrpl::producer<> Provider::refreshed() {\n\treturn _refreshed.events();\n}\n\nstd::vector<Media::ListSection> Provider::fillSections(\n\t\tnot_null<Overview::Layout::Delegate*> delegate) {\n\tmarkLayoutsStale();\n\tconst auto guard = gsl::finally([&] { clearStaleLayouts(); });\n\n\tauto result = std::vector<Media::ListSection>();\n\tresult.emplace_back(_type, sectionDelegate());\n\tauto &section = result.back();\n\tfor (auto i = 0, count = int(_slice.size()); i != count; ++i) {\n\t\tauto position = _slice[i];\n\t\tif (auto layout = getLayout(position.fullId, delegate)) {\n\t\t\tsection.addItem(layout);\n\t\t}\n\t}\n\tif (section.empty()) {\n\t\tresult.pop_back();\n\t}\n\treturn result;\n}\n\nvoid Provider::markLayoutsStale() {\n\tfor (auto &layout : _layouts) {\n\t\tlayout.second.stale = true;\n\t}\n}\n\nvoid Provider::clearStaleLayouts() {\n\tfor (auto i = _layouts.begin(); i != _layouts.end();) {\n\t\tif (i->second.stale) {\n\t\t\t_layoutRemoved.fire(i->second.item.get());\n\t\t\ti = _layouts.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n}\n\nProvider::List *Provider::currentList() {\n\treturn &_totalLists[_totalListQuery];\n}\n\nrpl::producer<not_null<Media::BaseLayout*>> Provider::layoutRemoved() {\n\treturn _layoutRemoved.events();\n}\n\nMedia::BaseLayout *Provider::lookupLayout(\n\t\tconst HistoryItem *item) {\n\tconst auto i = _layouts.find(item ? item->fullId() : FullMsgId());\n\treturn (i != _layouts.end()) ? i->second.item.get() : nullptr;\n}\n\nbool Provider::isMyItem(not_null<const HistoryItem*> item) {\n\treturn _seenIds.contains(item->fullId());\n}\n\nbool Provider::isAfter(\n\t\tnot_null<const HistoryItem*> a,\n\t\tnot_null<const HistoryItem*> b) {\n\treturn (a->fullId() < b->fullId());\n}\n\nvoid Provider::setSearchQuery(QString query) {\n\tUnexpected(\"Media::Provider::setSearchQuery.\");\n}\n\nvoid Provider::jumpToMessage(MsgId messageId, Fn<void(FullMsgId)>) {\n\tUnexpected(\"GlobalMedia::Provider::jumpToMessage.\");\n}\n\nGlobalMediaKey Provider::sliceKey(Data::MessagePosition aroundId) const {\n\treturn GlobalMediaKey{ aroundId };\n}\n\nvoid Provider::itemRemoved(not_null<const HistoryItem*> item) {\n\tconst auto id = item->fullId();\n\tif (const auto i = _layouts.find(id); i != end(_layouts)) {\n\t\t_layoutRemoved.fire(i->second.item.get());\n\t\t_layouts.erase(i);\n\t}\n}\n\nMedia::BaseLayout *Provider::getLayout(\n\t\tFullMsgId itemId,\n\t\tnot_null<Overview::Layout::Delegate*> delegate) {\n\tauto it = _layouts.find(itemId);\n\tif (it == _layouts.end()) {\n\t\tif (auto layout = createLayout(itemId, delegate, _type)) {\n\t\t\tlayout->initDimensions();\n\t\t\tit = _layouts.emplace(\n\t\t\t\titemId,\n\t\t\t\tstd::move(layout)).first;\n\t\t} else {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\tit->second.stale = false;\n\treturn it->second.item.get();\n}\n\nstd::unique_ptr<Media::BaseLayout> Provider::createLayout(\n\t\tFullMsgId itemId,\n\t\tnot_null<Overview::Layout::Delegate*> delegate,\n\t\tType type) {\n\tconst auto item = _controller->session().data().message(itemId);\n\tif (!item) {\n\t\treturn nullptr;\n\t}\n\tconst auto getPhoto = [&]() -> PhotoData* {\n\t\tif (const auto media = item->media()) {\n\t\t\treturn media->photo();\n\t\t}\n\t\treturn nullptr;\n\t};\n\tconst auto getFile = [&]() -> DocumentData* {\n\t\tif (const auto media = item->media()) {\n\t\t\treturn media->document();\n\t\t}\n\t\treturn nullptr;\n\t};\n\n\tconst auto &songSt = st::overviewFileLayout;\n\tusing namespace Overview::Layout;\n\tconst auto options = [&] {\n\t\tconst auto media = item->media();\n\t\treturn MediaOptions{ .spoiler = media && media->hasSpoiler() };\n\t};\n\tswitch (type) {\n\tcase Type::Photo:\n\t\tif (const auto photo = getPhoto()) {\n\t\t\treturn std::make_unique<Photo>(\n\t\t\t\tdelegate,\n\t\t\t\titem,\n\t\t\t\tphoto,\n\t\t\t\toptions());\n\t\t}\n\t\treturn nullptr;\n\tcase Type::GIF:\n\t\tif (const auto file = getFile()) {\n\t\t\treturn std::make_unique<Gif>(delegate, item, file);\n\t\t}\n\t\treturn nullptr;\n\tcase Type::Video:\n\t\tif (const auto file = getFile()) {\n\t\t\treturn std::make_unique<Video>(delegate, item, file, options());\n\t\t}\n\t\treturn nullptr;\n\tcase Type::File:\n\t\tif (const auto file = getFile()) {\n\t\t\treturn std::make_unique<Document>(\n\t\t\t\tdelegate,\n\t\t\t\titem,\n\t\t\t\tDocumentFields{ .document = file },\n\t\t\t\tsongSt);\n\t\t}\n\t\treturn nullptr;\n\tcase Type::MusicFile:\n\t\tif (const auto file = getFile()) {\n\t\t\treturn std::make_unique<Document>(\n\t\t\t\tdelegate,\n\t\t\t\titem,\n\t\t\t\tDocumentFields{ .document = file },\n\t\t\t\tsongSt);\n\t\t}\n\t\treturn nullptr;\n\tcase Type::RoundVoiceFile:\n\t\tif (const auto file = getFile()) {\n\t\t\treturn std::make_unique<Voice>(delegate, item, file, songSt);\n\t\t}\n\t\treturn nullptr;\n\tcase Type::Link:\n\t\treturn std::make_unique<Link>(delegate, item, item->media());\n\tcase Type::RoundFile:\n\t\treturn nullptr;\n\t}\n\tUnexpected(\"Type in ListWidget::createLayout()\");\n}\n\nMedia::ListItemSelectionData Provider::computeSelectionData(\n\t\tnot_null<const HistoryItem*> item,\n\t\tTextSelection selection) {\n\tauto result = Media::ListItemSelectionData(selection);\n\tresult.canDelete = item->canDelete();\n\tresult.canForward = item->allowsForward();\n\treturn result;\n}\n\nbool Provider::allowSaveFileAs(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) {\n\treturn item->allowsForward();\n}\n\nQString Provider::showInFolderPath(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) {\n\treturn document->filepath(true);\n}\n\nvoid Provider::applyDragSelection(\n\t\tMedia::ListSelectedMap &selected,\n\t\tnot_null<const HistoryItem*> fromItem,\n\t\tbool skipFrom,\n\t\tnot_null<const HistoryItem*> tillItem,\n\t\tbool skipTill) {\n#if 0 // not used for now\n\tconst auto fromId = GetUniversalId(fromItem) - (skipFrom ? 1 : 0);\n\tconst auto tillId = GetUniversalId(tillItem) - (skipTill ? 0 : 1);\n\tfor (auto i = selected.begin(); i != selected.end();) {\n\t\tconst auto itemId = GetUniversalId(i->first);\n\t\tif (itemId > fromId || itemId <= tillId) {\n\t\t\ti = selected.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tfor (auto &layoutItem : _layouts) {\n\t\tauto &&universalId = layoutItem.first;\n\t\tif (universalId <= fromId && universalId > tillId) {\n\t\t\tconst auto item = layoutItem.second.item->getItem();\n\t\t\tChangeItemSelection(\n\t\t\t\tselected,\n\t\t\t\titem,\n\t\t\t\tcomputeSelectionData(item, FullSelection));\n\t\t}\n\t}\n#endif // todo global media\n}\n\nint64 Provider::scrollTopStatePosition(not_null<HistoryItem*> item) {\n\treturn item->position().date;\n}\n\nHistoryItem *Provider::scrollTopStateItem(Media::ListScrollTopState state) {\n\tconst auto maybe = Data::MessagePosition{\n\t\t.date = TimeId(state.position),\n\t};\n\tif (state.item && _slice.indexOf(state.item->position())) {\n\t\treturn state.item;\n\t} else if (const auto position = _slice.nearest(maybe)) {\n\t\tconst auto id = position->fullId;\n\t\tif (const auto item = _controller->session().data().message(id)) {\n\t\t\treturn item;\n\t\t}\n\t}\n\treturn state.item;\n}\n\nvoid Provider::saveState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tMedia::ListScrollTopState scrollState) {\n\tif (_aroundId != Data::MaxMessagePosition && scrollState.item) {\n\t\tmemento->setAroundId(_aroundId.fullId);\n\t\tmemento->setIdsLimit(_idsLimit);\n\t\tmemento->setScrollTopItem(scrollState.item->globalId());\n\t\tmemento->setScrollTopItemPosition(scrollState.position);\n\t\tmemento->setScrollTopShift(scrollState.shift);\n\t}\n}\n\nvoid Provider::restoreState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tFn<void(Media::ListScrollTopState)> restoreScrollState) {\n\tif (const auto limit = memento->idsLimit()) {\n\t\t_idsLimit = limit;\n\t\t_aroundId = { memento->aroundId() };\n\t\trestoreScrollState({\n\t\t\t.position = memento->scrollTopItemPosition(),\n\t\t\t.item = MessageByGlobalId(memento->scrollTopItem()),\n\t\t\t.shift = memento->scrollTopShift(),\n\t\t});\n\t\trefreshViewer();\n\t}\n}\n\n} // namespace Info::GlobalMedia\n"
  },
  {
    "path": "Telegram/SourceFiles/info/global_media/info_global_media_provider.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_messages.h\"\n#include \"info/media/info_media_common.h\"\n#include \"base/weak_ptr.h\"\n\nnamespace Info {\nclass AbstractController;\n} // namespace Info\n\nnamespace Info::GlobalMedia {\n\nstruct GlobalMediaKey {\n\tData::MessagePosition aroundId;\n\n\tfriend inline constexpr bool operator==(\n\t\tconst GlobalMediaKey &,\n\t\tconst GlobalMediaKey &) = default;\n};\n\nclass GlobalMediaSlice final {\npublic:\n\tusing Key = GlobalMediaKey;\n\tusing Value = Data::MessagePosition;\n\n\texplicit GlobalMediaSlice(\n\t\tKey key,\n\t\tstd::vector<Data::MessagePosition> items = {},\n\t\tstd::optional<int> fullCount = std::nullopt,\n\t\tint skippedAfter = 0);\n\n\t[[nodiscard]] std::optional<int> fullCount() const;\n\t[[nodiscard]] std::optional<int> skippedBefore() const;\n\t[[nodiscard]] std::optional<int> skippedAfter() const;\n\t[[nodiscard]] std::optional<int> indexOf(Value fullId) const;\n\t[[nodiscard]] int size() const;\n\t[[nodiscard]] Value operator[](int index) const;\n\t[[nodiscard]] std::optional<int> distance(\n\t\tconst Key &a,\n\t\tconst Key &b) const;\n\t[[nodiscard]] std::optional<Value> nearest(Value id) const;\n\nprivate:\n\tGlobalMediaKey _key;\n\tstd::vector<Data::MessagePosition> _items;\n\tstd::optional<int> _fullCount;\n\tint _skippedAfter = 0;\n\n};\n\nclass Provider final\n\t: public Media::ListProvider\n\t, private Media::ListSectionDelegate {\npublic:\n\tusing Type = Media::Type;\n\tusing BaseLayout = Media::BaseLayout;\n\n\texplicit Provider(not_null<AbstractController*> controller);\n\n\tType type() override;\n\tbool hasSelectRestriction() override;\n\trpl::producer<bool> hasSelectRestrictionChanges() override;\n\tbool isPossiblyMyItem(not_null<const HistoryItem*> item) override;\n\n\tstd::optional<int> fullCount() override;\n\n\tvoid restart() override;\n\tvoid checkPreload(\n\t\tQSize viewport,\n\t\tnot_null<BaseLayout*> topLayout,\n\t\tnot_null<BaseLayout*> bottomLayout,\n\t\tbool preloadTop,\n\t\tbool preloadBottom) override;\n\tvoid refreshViewer() override;\n\trpl::producer<> refreshed() override;\n\n\tstd::vector<Media::ListSection> fillSections(\n\t\tnot_null<Overview::Layout::Delegate*> delegate) override;\n\trpl::producer<not_null<BaseLayout*>> layoutRemoved() override;\n\tBaseLayout *lookupLayout(const HistoryItem *item) override;\n\tbool isMyItem(not_null<const HistoryItem*> item) override;\n\tbool isAfter(\n\t\tnot_null<const HistoryItem*> a,\n\t\tnot_null<const HistoryItem*> b) override;\n\n\tvoid setSearchQuery(QString query) override;\n\tvoid jumpToMessage(MsgId messageId, Fn<void(FullMsgId)> callback) override;\n\n\tMedia::ListItemSelectionData computeSelectionData(\n\t\tnot_null<const HistoryItem*> item,\n\t\tTextSelection selection) override;\n\tvoid applyDragSelection(\n\t\tMedia::ListSelectedMap &selected,\n\t\tnot_null<const HistoryItem*> fromItem,\n\t\tbool skipFrom,\n\t\tnot_null<const HistoryItem*> tillItem,\n\t\tbool skipTill) override;\n\n\tbool allowSaveFileAs(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) override;\n\tQString showInFolderPath(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) override;\n\n\tint64 scrollTopStatePosition(not_null<HistoryItem*> item) override;\n\tHistoryItem *scrollTopStateItem(\n\t\tMedia::ListScrollTopState state) override;\n\tvoid saveState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tMedia::ListScrollTopState scrollState) override;\n\tvoid restoreState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tFn<void(Media::ListScrollTopState)> restoreScrollState) override;\n\nprivate:\n\tstatic constexpr auto kMinimalIdsLimit = 16;\n\n\tstruct FillResult {\n\t\tGlobalMediaSlice slice;\n\t\tbool notEnough = false;\n\t};\n\tstruct List {\n\t\tstd::vector<Data::MessagePosition> list;\n\t\tData::MessagePosition offsetPosition;\n\t\tint32 offsetRate = 0;\n\t\tint fullCount = 0;\n\t\tbool loaded = false;\n\t};\n\n\tbool sectionHasFloatingHeader() override;\n\tQString sectionTitle(not_null<const BaseLayout*> item) override;\n\tbool sectionItemBelongsHere(\n\t\tnot_null<const BaseLayout*> item,\n\t\tnot_null<const BaseLayout*> previous) override;\n\n\t[[nodiscard]] rpl::producer<GlobalMediaSlice> source(\n\t\tType type,\n\t\tData::MessagePosition aroundId,\n\t\tQString query,\n\t\tint limitBefore,\n\t\tint limitAfter);\n\n\t[[nodiscard]] BaseLayout *getLayout(\n\t\tFullMsgId itemId,\n\t\tnot_null<Overview::Layout::Delegate*> delegate);\n\t[[nodiscard]] std::unique_ptr<BaseLayout> createLayout(\n\t\tFullMsgId itemId,\n\t\tnot_null<Overview::Layout::Delegate*> delegate,\n\t\tType type);\n\n\t[[nodiscard]] GlobalMediaKey sliceKey(\n\t\tData::MessagePosition aroundId) const;\n\n\tvoid itemRemoved(not_null<const HistoryItem*> item);\n\tvoid markLayoutsStale();\n\tvoid clearStaleLayouts();\n\t[[nodiscard]] List *currentList();\n\t[[nodiscard]] FillResult fillRequest(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter);\n\tmtpRequestId requestMore(Fn<void()> loaded);\n\n\tconst not_null<AbstractController*> _controller;\n\tconst Type _type = {};\n\n\tData::MessagePosition _aroundId = Data::MaxMessagePosition;\n\tint _idsLimit = kMinimalIdsLimit;\n\tGlobalMediaSlice _slice;\n\n\tbase::flat_set<FullMsgId> _seenIds;\n\tstd::unordered_map<FullMsgId, Media::CachedItem> _layouts;\n\trpl::event_stream<not_null<BaseLayout*>> _layoutRemoved;\n\trpl::event_stream<> _refreshed;\n\n\tQString _totalListQuery;\n\tbase::flat_map<QString, List> _totalLists;\n\n\trpl::lifetime _lifetime;\n\trpl::lifetime _viewerLifetime;\n\n};\n\n} // namespace Info::GlobalMedia\n"
  },
  {
    "path": "Telegram/SourceFiles/info/global_media/info_global_media_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/global_media/info_global_media_widget.h\"\n\n#include \"info/global_media/info_global_media_inner_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/search_field_controller.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/ui_utility.h\"\n#include \"data/data_download_manager.h\"\n#include \"data/data_user.h\"\n#include \"core/application.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Info::GlobalMedia {\n\nMemento::Memento(not_null<Controller*> controller)\n: ContentMemento(Tag{ controller->session().user() })\n, _media(controller) {\n}\n\nMemento::Memento(not_null<UserData*> self, Storage::SharedMediaType type)\n: ContentMemento(Tag{ self })\n, _media(self, 0, type) {\n}\n\nMemento::~Memento() = default;\n\nSection Memento::section() const {\n\treturn Section(_media.type(), Section::Type::GlobalMedia);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(parent, controller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nWidget::Widget(QWidget *parent, not_null<Controller*> controller)\n: ContentWidget(parent, controller) {\n\t_inner = setInnerWidget(object_ptr<InnerWidget>(\n\t\tthis,\n\t\tcontroller));\n\t_inner->setScrollHeightValue(scrollHeightValue());\n\t_inner->scrollToRequests(\n\t) | rpl::on_next([this](Ui::ScrollToRequest request) {\n\t\tscrollTo(request);\n\t}, _inner->lifetime());\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\tif (auto globalMediaMemento = dynamic_cast<Memento*>(memento.get())) {\n\t\trestoreState(globalMediaMemento);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(controller());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\nrpl::producer<SelectedItems> Widget::selectedListValue() const {\n\treturn _inner->selectedListValue();\n}\n\nvoid Widget::selectionAction(SelectionAction action) {\n\t_inner->selectionAction(action);\n}\n\nvoid Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {\n\tconst auto window = controller()->parentController();\n\tconst auto deleteAll = [=] {\n\t\tauto &manager = Core::App().downloadManager();\n\t\tconst auto phrase = tr::lng_downloads_delete_sure_all(tr::now);\n\t\tconst auto added = manager.loadedHasNonCloudFile()\n\t\t\t? QString()\n\t\t\t: tr::lng_downloads_delete_in_cloud(tr::now);\n\t\tconst auto deleteSure = [=, &manager](Fn<void()> close) {\n\t\t\tUi::PostponeCall(this, close);\n\t\t\tmanager.deleteAll();\n\t\t};\n\t\twindow->show(Ui::MakeConfirmBox({\n\t\t\t.text = phrase + (added.isEmpty() ? QString() : \"\\n\\n\" + added),\n\t\t\t.confirmed = deleteSure,\n\t\t\t.confirmText = tr::lng_box_delete(tr::now),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t}));\n\t};\n\taddAction(\n\t\ttr::lng_context_delete_all_files(tr::now),\n\t\tdeleteAll,\n\t\t&st::menuIconDelete);\n}\n\nrpl::producer<QString> Widget::title() {\n\treturn tr::lng_profile_shared_media();\n}\n\nstd::shared_ptr<Info::Memento> Make(\n\t\tnot_null<UserData*> self,\n\t\tStorage::SharedMediaType type) {\n\treturn std::make_shared<Info::Memento>(\n\t\tstd::vector<std::shared_ptr<ContentMemento>>(\n\t\t\t1,\n\t\t\tstd::make_shared<Memento>(self, type)));\n}\n\n} // namespace Info::GlobalMedia"
  },
  {
    "path": "Telegram/SourceFiles/info/global_media/info_global_media_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n#include \"info/media/info_media_widget.h\"\n\nnamespace Storage {\nenum class SharedMediaType : signed char;\n} // namespace Storage\n\nnamespace Info::GlobalMedia {\n\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\tMemento(not_null<Controller*> controller);\n\tMemento(not_null<UserData*> self, Storage::SharedMediaType type);\n\t~Memento();\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\t[[nodiscard]] Media::Memento &media() {\n\t\treturn _media;\n\t}\n\t[[nodiscard]] const Media::Memento &media() const {\n\t\treturn _media;\n\t}\n\nprivate:\n\tMedia::Memento _media;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(QWidget *parent, not_null<Controller*> controller);\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\n\trpl::producer<SelectedItems> selectedListValue() const override;\n\tvoid selectionAction(SelectionAction action) override;\n\n\tvoid fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override;\n\n\trpl::producer<QString> title() override;\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tInnerWidget *_inner = nullptr;\n\n};\n\n[[nodiscard]] std::shared_ptr<Info::Memento> Make(\n\tnot_null<UserData*> self,\n\tStorage::SharedMediaType type);\n\n} // namespace Info::GlobalMedia"
  },
  {
    "path": "Telegram/SourceFiles/info/info.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\n\nusing \"boxes/boxes.style\";\nusing \"ui/widgets/widgets.style\";\nusing \"ui/chat/chat.style\"; // GroupCallUserpics.\nusing \"dialogs/dialogs.style\"; // dialogsDateFont.\n\nInfoToggle {\n\tcolor: color;\n\tduration: int;\n\tsize: pixels;\n\tskip: pixels;\n\tstroke: pixels;\n\trippleAreaPadding: pixels;\n}\n\nInfoPeerBadge {\n\tverified: icon;\n\tverifiedCheck: icon;\n\tpremium: icon;\n\tpremiumFg: color;\n\tposition: point;\n\tsizeTag: int;\n}\n\nInfoTopBar {\n\theight: pixels;\n\tback: IconButton;\n\ttitle: FlatLabel;\n\ttitlePosition: point;\n\ttitleWithSubtitle: FlatLabel;\n\ttitleWithSubtitlePosition: point;\n\tsubtitle: FlatLabel;\n\tsubtitlePosition: point;\n\tbg: color;\n\tmediaCancel: IconButton;\n\tmediaActionsSkip: pixels;\n\tmediaForward: IconButton;\n\tmediaDelete: IconButton;\n\tstoriesSave: IconButton;\n\tstoriesArchive: IconButton;\n\tstoriesPin: IconButton;\n\tstoriesUnpin: IconButton;\n\tsearch: IconButton;\n\tsearchRow: SearchFieldRow;\n\thighlightBg: color;\n\thighlightDuration: int;\n\tradius: pixels;\n}\n\nSubTabs {\n\tbg: color;\n}\n\ndefaultSubTabs: SubTabs {\n\tbg: windowBg;\n}\n\ninfoMediaHeaderFg: windowFg;\n\ninfoToggle: InfoToggle {\n\tcolor: menuIconFg;\n\tduration: slideWrapDuration;\n\tsize: 24px;\n\tskip: 5px;\n\tstroke: 2px;\n\trippleAreaPadding: 8px;\n}\n\ninfoMediaSearch: SearchFieldRow {\n\theight: 44px;\n\tpadding: margins(8px, 6px, 8px, 6px);\n\tfield: defaultMultiSelectSearchField;\n\tfieldIcon: icon {{\n\t\t\"box_search-flip_horizontal\",\n\t\tmenuIconFg,\n\t\tpoint(6px, 8px)\n\t}};\n\tfieldIconSkip: 36px;\n\tfieldCancel: defaultMultiSelectSearchCancel;\n\tfieldCancelSkip: 40px;\n}\ninfoLayerMediaSearch: SearchFieldRow(infoMediaSearch) {\n\theight: 46px;\n\tfieldIcon: icon {{\n\t\t\"box_search-flip_horizontal\",\n\t\tmenuIconFg,\n\t\tpoint(9px, 9px)\n\t}};\n\tfieldIconSkip: 34px;\n\tfieldCancel: CrossButton(defaultMultiSelectSearchCancel) {\n\t\twidth: 50px;\n\t\tcross: CrossAnimation {\n\t\t\tsize: 38px;\n\t\t\tskip: 12px;\n\t\t\tstroke: 1.5;\n\t\t\tminScale: 0.3;\n\t\t}\n\t\tcrossPosition: point(3px, 4px);\n\t}\n\tfieldCancelSkip: 46px;\n}\ninfoTopBarSearchRow: SearchFieldRow(infoLayerMediaSearch) {\n\theight: 52px;\n\tpadding: margins(0px, 12px, 8px, 10px);\n\tfieldCancel: CrossButton(defaultMultiSelectSearchCancel) {\n\t\twidth: 51px;\n\t\theight: 52px;\n\t\tcross: CrossAnimation {\n\t\t\tsize: 42px;\n\t\t\tskip: 14px;\n\t\t\tstroke: 1.5;\n\t\t\tminScale: 0.3;\n\t\t}\n\t\tcrossPosition: point(1px, 6px);\n\t}\n}\n\ninfoSlideDuration: 0;\n\ninfoTopBarBackIcon: icon {{ \"info/info_back\", boxTitleCloseFg }};\ninfoTopBarBackIconOver: icon {{ \"info/info_back\", boxTitleCloseFgOver }};\ninfoTopBarHeight: 54px;\ninfoTopBarBack: IconButton(defaultIconButton) {\n\twidth: 60px;\n\theight: infoTopBarHeight;\n\n\ticon: infoTopBarBackIcon;\n\ticonOver: infoTopBarBackIconOver;\n\ticonPosition: point(11px, -1px);\n\n\trippleAreaPosition: point(6px, 6px);\n\trippleAreaSize: 42px;\n\tripple: defaultRippleAnimationBgOver;\n}\ninfoTopBarTitle: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowBoldFg;\n\tmaxHeight: 20px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px semibold);\n\t}\n}\ninfoTopBarMediaCancel: IconButton(infoTopBarBack) {\n\twidth: infoTopBarHeight;\n\ticon: icon {{ \"info/info_close\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"info/info_close\", boxTitleCloseFgOver }};\n}\ninfoTopBarClose: IconButton(infoTopBarMediaCancel) {\n\twidth: 48px;\n\ticonPosition: point(5px, -1px);\n\trippleAreaPosition: point(0px, 6px);\n}\ninfoTopBarSearch: IconButton(infoTopBarBack) {\n\twidth: 56px;\n\ticon: icon {{ \"top_bar_search\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"top_bar_search\", boxTitleCloseFgOver }};\n}\ninfoTopBarMenu: IconButton(infoTopBarBack) {\n\twidth: 48px;\n\ticon: icon {{ \"title_menu_dots\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"title_menu_dots\", boxTitleCloseFgOver }};\n\ticonPosition: point(18px, -1px);\n\trippleAreaPosition: point(1px, 6px);\n}\ninfoTopBarMenuActive: icon {{ \"title_menu_dots\", windowActiveTextFg, point(0px, 0px) }};\ninfoTopBarCall: IconButton(infoTopBarMenu) {\n\twidth: 42px;\n\ticon: icon {{ \"top_bar_call\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"top_bar_call\", boxTitleCloseFgOver }};\n\ticonPosition: point(5px, -1px);\n\trippleAreaPosition: point(0px, 6px);\n}\ninfoTopBarQr: IconButton(infoTopBarMenu) {\n\twidth: 48px;\n\ticon: icon {{ \"menu/qr_code\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"menu/qr_code\", boxTitleCloseFgOver }};\n\ticonPosition: point(8px, -1px);\n\trippleAreaPosition: point(0px, 6px);\n}\ninfoTopBarEdit: IconButton(infoTopBarMenu) {\n\twidth: 48px;\n\ticon: icon {{ \"menu/edit\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"menu/edit\", boxTitleCloseFgOver }};\n\ticonPosition: point(8px, -1px);\n\trippleAreaPosition: point(0px, 6px);\n}\ninfoTopBarForward: IconButton(infoTopBarBack) {\n\twidth: 46px;\n\ticon: icon {{ \"info/info_media_forward\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"info/info_media_forward\", boxTitleCloseFgOver }};\n\ticonPosition: point(10px, -1px);\n\trippleAreaPosition: point(1px, 6px);\n}\ninfoTopBarDelete: IconButton(infoTopBarForward) {\n\ticon: icon {{ \"info/info_media_delete\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"info/info_media_delete\", boxTitleCloseFgOver }};\n}\ninfoTopBarSaveStories: IconButton(infoTopBarForward) {\n\ticon: icon {{ \"info/info_stories_to_profile\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"info/info_stories_to_profile\", boxTitleCloseFgOver }};\n}\ninfoTopBarArchiveStories: IconButton(infoTopBarForward) {\n\ticon: icon {{ \"info/info_stories_to_archive\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"info/info_stories_to_archive\", boxTitleCloseFgOver }};\n}\ninfoTopBarPinStories: IconButton(infoTopBarForward) {\n\ticon: icon {{ \"menu/pin\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"menu/pin\", boxTitleCloseFgOver }};\n}\ninfoTopBarUnpinStories: IconButton(infoTopBarForward) {\n\ticon: icon {{ \"menu/unpin\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"menu/unpin\", boxTitleCloseFgOver }};\n}\ninfoTopBar: InfoTopBar {\n\theight: infoTopBarHeight;\n\tback: infoTopBarBack;\n\ttitle: infoTopBarTitle;\n\ttitlePosition: point(24px, 17px);\n\ttitleWithSubtitle: FlatLabel(infoTopBarTitle) {\n\t\tstyle: semiboldTextStyle;\n\t}\n\ttitleWithSubtitlePosition: point(16px, 8px);\n\tsubtitle: FlatLabel(defaultFlatLabel) {\n\t\ttextFg: windowSubTextFg;\n\t}\n\tsubtitlePosition: point(16px, 28px);\n\tbg: windowBg;\n\tmediaCancel: infoTopBarMediaCancel;\n\tmediaActionsSkip: 4px;\n\tmediaForward: infoTopBarForward;\n\tmediaDelete: infoTopBarDelete;\n\tstoriesSave: infoTopBarSaveStories;\n\tstoriesArchive: infoTopBarArchiveStories;\n\tstoriesPin: infoTopBarPinStories;\n\tstoriesUnpin: infoTopBarUnpinStories;\n\tsearch: infoTopBarSearch;\n\tsearchRow: infoTopBarSearchRow;\n\thighlightBg: windowBgOver;\n\thighlightDuration: 240;\n\tradius: 0px;\n}\ninfoTopBarScale: 0.7;\ninfoTopBarDuration: 150;\n\ninfoLayerTopMinimal: 20px;\ninfoLayerTopMaximal: 40px;\ninfoLayerTopBarHeight: 56px;\ninfoLayerTopBarBackIcon: icon {{ \"info/info_back\", boxTitleCloseFg }};\ninfoLayerTopBarBackIconOver: icon {{ \"info/info_back\", boxTitleCloseFgOver }};\ninfoLayerTopBarBack: IconButton(infoTopBarBack) {\n\twidth: 60px;\n\theight: infoLayerTopBarHeight;\n\n\ticonPosition: point(10px, -1px);\n\ticon: infoLayerTopBarBackIcon;\n\ticonOver: infoLayerTopBarBackIconOver;\n\trippleAreaSize: 40px;\n\trippleAreaPosition: point(6px, 8px);\n}\ninfoLayerTopBarMediaCancel: IconButton(infoLayerTopBarBack) {\n\twidth: 48px;\n\ticon: icon {{ \"info/info_close\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"info/info_close\", boxTitleCloseFgOver }};\n}\ninfoLayerTopBarClose: IconButton(infoLayerTopBarMediaCancel) {\n\ticonPosition: point(4px, -1px);\n\trippleAreaPosition: point(0px, 8px);\n}\ninfoLayerTopBarMenu: IconButton(infoLayerTopBarClose) {\n\twidth: 40px;\n\ticon: icon {{ \"title_menu_dots\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"title_menu_dots\", boxTitleCloseFgOver }};\n\ticonPosition: point(16px, -1px);\n}\ninfoLayerTopBarCall: IconButton(infoLayerTopBarMenu) {\n\ticon: icon {{ \"top_bar_call\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"top_bar_call\", boxTitleCloseFgOver }};\n\ticonPosition: point(3px, -1px);\n}\ninfoLayerTopBarQr: IconButton(infoLayerTopBarClose) {\n\twidth: 40px;\n\ticon: icon {{ \"menu/qr_code\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"menu/qr_code\", boxTitleCloseFgOver }};\n\ticonPosition: point(8px, -1px);\n}\ninfoLayerTopBarSearch: IconButton(infoLayerTopBarClose) {\n\twidth: 48px;\n\ticon: icon {{ \"top_bar_search\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"top_bar_search\", boxTitleCloseFgOver }};\n\ticonPosition: point(7px, -1px);\n}\ninfoLayerTopBarEdit: IconButton(infoLayerTopBarClose) {\n\twidth: 44px;\n\theight: infoLayerTopBarHeight - 2px;\n\ticon: icon {{ \"menu/edit\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"menu/edit\", boxTitleCloseFgOver }};\n\ticonPosition: point(10px, -1px);\n\n\trippleAreaSize: 42px;\n\trippleAreaPosition: point(1px, 6px);\n}\ninfoLayerTopBarForward: IconButton(infoLayerTopBarBack) {\n\twidth: 45px;\n\ticon: icon {{ \"info/info_media_forward\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"info/info_media_forward\", boxTitleCloseFgOver }};\n\ticonPosition: point(11px, -1px);\n\trippleAreaPosition: point(3px, 8px);\n}\ninfoLayerTopBarDelete: IconButton(infoLayerTopBarForward) {\n\ticon: icon {{ \"info/info_media_delete\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"info/info_media_delete\", boxTitleCloseFgOver }};\n}\ninfoLayerTopBarSaveStories: IconButton(infoLayerTopBarForward) {\n\ticon: icon {{ \"info/info_stories_to_profile\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"info/info_stories_to_profile\", boxTitleCloseFgOver }};\n}\ninfoLayerTopBarArchiveStories: IconButton(infoLayerTopBarForward) {\n\ticon: icon {{ \"info/info_stories_to_archive\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"info/info_stories_to_archive\", boxTitleCloseFgOver }};\n}\ninfoLayerTopBarPinStories: IconButton(infoLayerTopBarForward) {\n\ticon: icon {{ \"menu/pin\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"menu/pin\", boxTitleCloseFgOver }};\n}\ninfoLayerTopBarUnpinStories: IconButton(infoLayerTopBarForward) {\n\ticon: icon {{ \"menu/unpin\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"menu/unpin\", boxTitleCloseFgOver }};\n}\ninfoLayerTopBar: InfoTopBar(infoTopBar) {\n\theight: infoLayerTopBarHeight;\n\tback: infoLayerTopBarBack;\n\ttitle: boxTitle;\n\ttitlePosition: point(24px, 17px);\n\ttitleWithSubtitlePosition: point(16px, 9px);\n\tsubtitlePosition: point(16px, 30px);\n\tbg: boxBg;\n\tmediaCancel: infoLayerTopBarMediaCancel;\n\tmediaActionsSkip: 6px;\n\tmediaForward: infoLayerTopBarForward;\n\tmediaDelete: infoLayerTopBarDelete;\n\tstoriesSave: infoLayerTopBarSaveStories;\n\tstoriesArchive: infoLayerTopBarArchiveStories;\n\tstoriesPin: infoLayerTopBarPinStories;\n\tstoriesUnpin: infoLayerTopBarUnpinStories;\n\tsearch: infoTopBarSearch;\n\tsearchRow: infoTopBarSearchRow;\n\tradius: boxRadius;\n}\n\ninfoLayerTopBarMenuPosition: point(48px, 37px);\n\ninfoTopBarColoredBack: IconButton(infoTopBarBack) {\n\ticon: icon {{ \"info/info_back\", groupCallMembersFg }};\n\ticonOver: icon {{ \"info/info_back\", groupCallMembersFg }};\n\tripple: universalRippleAnimation;\n}\ninfoLayerTopBarColoredBack: IconButton(infoLayerTopBarBack) {\n\ticon: icon {{ \"info/info_back\", groupCallMembersFg }};\n\ticonOver: icon {{ \"info/info_back\", groupCallMembersFg }};\n\tripple: universalRippleAnimation;\n}\ninfoTopBarColoredClose: IconButton(infoTopBarClose) {\n\ticon: icon {{ \"info/info_close\", groupCallMembersFg }};\n\ticonOver: icon {{ \"info/info_close\", groupCallMembersFg }};\n\tripple: universalRippleAnimation;\n}\ninfoTopBarColoredMenu: IconButton(infoTopBarMenu) {\n\ticon: icon {{ \"title_menu_dots\", groupCallMembersFg }};\n\ticonOver: icon {{ \"title_menu_dots\", groupCallMembersFg }};\n\tripple: universalRippleAnimation;\n}\ninfoLayerTopBarColoredMenu: IconButton(infoLayerTopBarMenu) {\n\ticon: icon {{ \"title_menu_dots\", groupCallMembersFg }};\n\ticonOver: icon {{ \"title_menu_dots\", groupCallMembersFg }};\n\tripple: universalRippleAnimation;\n}\ninfoTopBarColoredEdit: IconButton(infoTopBarEdit) {\n\ticon: icon {{ \"menu/edit\", groupCallMembersFg }};\n\ticonOver: icon {{ \"menu/edit\", groupCallMembersFg }};\n\tripple: universalRippleAnimation;\n}\ninfoLayerTopBarColoredEdit: IconButton(infoLayerTopBarEdit) {\n\ticon: icon {{ \"menu/edit\", groupCallMembersFg }};\n\ticonOver: icon {{ \"menu/edit\", groupCallMembersFg }};\n\tripple: universalRippleAnimation;\n}\ninfoTopBarBlackBack: IconButton(infoTopBarBack) {\n\ticon: icon {{ \"info/info_back\", windowBoldFg }};\n\ticonOver: icon {{ \"info/info_back\", windowBoldFg }};\n\tripple: universalRippleAnimation;\n}\ninfoLayerTopBarBlackBack: IconButton(infoLayerTopBarBack) {\n\ticon: icon {{ \"info/info_back\", windowBoldFg }};\n\ticonOver: icon {{ \"info/info_back\", windowBoldFg }};\n\tripple: universalRippleAnimation;\n}\ninfoTopBarBlackClose: IconButton(infoTopBarClose) {\n\ticon: icon {{ \"info/info_close\", windowBoldFg }};\n\ticonOver: icon {{ \"info/info_close\", windowBoldFg }};\n\tripple: universalRippleAnimation;\n}\ninfoTopBarBlackEdit: IconButton(infoTopBarEdit) {\n\ticon: icon {{ \"menu/edit\", windowBoldFg }};\n\ticonOver: icon {{ \"menu/edit\", windowBoldFg }};\n\tripple: universalRippleAnimation;\n}\ninfoLayerTopBarBlackEdit: IconButton(infoLayerTopBarEdit) {\n\ticon: icon {{ \"menu/edit\", windowBoldFg }};\n\ticonOver: icon {{ \"menu/edit\", windowBoldFg }};\n\tripple: universalRippleAnimation;\n}\n\ninfoProfileTopBarHeightMax: 180px + infoLayerTopBarHeight;\ninfoProfileTopBarTitleTop: 113px;\ninfoProfileTopBarStatusTop: 134px;\ninfoProfileTopBarPhotoTop: 24px;\ninfoProfileTopBarPhotoBgShift: 55px;\ninfoProfileTopBarPhotoBgNoActionsShift: 30px;\ninfoProfileTopBarPhotoSize: 80px;\ninfoProfileTopBarLastSeenSkip: point(8px, 1px);\n\ninfoProfileTopBarGiftSize: 20px;\n\ninfoProfileTopBarGiftLeft: point(15px, 13px);\ninfoProfileTopBarGiftTopLeft: point(4px, 4px);\ninfoProfileTopBarGiftBottomLeft: point(9px, 16px);\ninfoProfileTopBarGiftRight: point(50px, 13px);\ninfoProfileTopBarGiftTopRight: point(30px, 4px);\ninfoProfileTopBarGiftBottomRight: point(40px, 13px);\n\ninfoProfileTopBarActionButtonBgOpacity: 0.16;\ninfoProfileTopBarActionButtonSize: 52px;\ninfoProfileTopBarActionButtonIconSize: 23px;\ninfoProfileTopBarActionButtonLottieSize: infoProfileTopBarActionButtonIconSize - 0px;\ninfoProfileTopBarActionButtonIconTop: 6px;\ninfoProfileTopBarActionButtonTextTop: infoProfileTopBarActionButtonIconSize + infoProfileTopBarActionButtonIconTop + 1px;\ninfoProfileTopBarActionButtonTextSkip: 4px;\ninfoProfileTopBarActionButtonFont: font(11px);\n\ninfoProfileTopBarActionButtonsPadding: margins(18px, 0px, 18px, 16px);\ninfoProfileTopBarActionButtonsHeight: infoProfileTopBarActionButtonSize + infoProfileTopBarActionButtonsPadding.topBottom;\ninfoProfileTopBarActionButtonsSpace: 10px;\n\ninfoProfileTopBarStep2: infoProfileTopBarHeightMax - infoLayerTopBarHeight;\ninfoProfileTopBarStep1: infoProfileTopBarStep2 - infoProfileTopBarActionButtonsHeight;\n\ninfoProfileTopBarNoActionsHeightMax: infoProfileTopBarHeightMax - infoProfileTopBarActionButtonSize;\n\ninfoProfileTopBarActionMenuSkip: 10px;\n\ninfoProfileTopBarActionMessage: icon{{ \"profile/message-23x23\", windowBoldFg }};\ninfoProfileTopBarActionMute: icon{{ \"profile/mute-23x23\", windowBoldFg }};\ninfoProfileTopBarActionUnmute: icon{{ \"profile/unmute-23x23\", windowBoldFg }};\ninfoProfileTopBarActionCall: icon{{ \"profile/call-23x23\", windowBoldFg }};\ninfoProfileTopBarActionGift: icon{{ \"profile/gift-23x23\", windowBoldFg }};\ninfoProfileTopBarActionJoin: icon{{ \"profile/join-23x23\", windowBoldFg }};\ninfoProfileTopBarActionReport: icon{{ \"profile/report-23x23\", windowBoldFg }};\ninfoProfileTopBarActionLeave: icon{{ \"profile/leave-23x23\", windowBoldFg }};\ninfoProfileTopBarActionMore: icon{{ \"profile/profile_more\", windowBoldFg }};\ninfoProfileTopBarActionManage: icon{{ \"profile/profile_manage\", windowBoldFg }};\n\ninfoProfileTopBarTopicStatusButton: RoundButton(defaultTableSmallButton) {\n\tradius: 4px;\n}\n\ninfoProfileTopBarBoostFooter: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(11px);\n\t}\n\tpalette: TextPalette(defaultTextPalette) {\n\t\tlinkFg: windowBoldFg;\n\t\tselectLinkFg: windowBoldFg;\n\t}\n}\ninfoProfileTopBarBoostFooterColored: FlatLabel(infoProfileTopBarBoostFooter) {\n\ttextFg: groupCallVideoSubTextFg;\n\tpalette: TextPalette(defaultTextPalette) {\n\t\tlinkFg: groupCallIconFg;\n\t\tselectLinkFg: groupCallIconFg;\n\t}\n}\n\ninfoProfileTopBarShowLastSeen: RoundButton(defaultActiveButton) {\n\ttextFg: windowBoldFg;\n\ttextFgOver: windowBoldFg;\n\ttextBg: windowBg;\n\ttextBgOver: windowBgOver;\n\twidth: -12px;\n\theight: 18px;\n\ttextTop: 0px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px);\n\t}\n\tripple: universalRippleAnimation;\n}\n\ninfoMinimalWidth: 324px;\ninfoDesiredWidth: 392px;\ninfoMinimalLayerMargin: 48px;\n\nInfoProfileCover {\n\theight: pixels;\n\tphoto: UserpicButton;\n\tphotoLeft: pixels;\n\tphotoTop: pixels;\n\tname: FlatLabel;\n\tnameLeft: pixels;\n\tnameTop: pixels;\n\tstatus: FlatLabel;\n\tstatusLeft: pixels;\n\tstatusTop: pixels;\n\tstarsRatingLeft: pixels;\n\tstarsRatingTop: pixels;\n\tshowLastSeen: RoundButton;\n\tshowLastSeenPosition: point;\n\tshowLastSeenVisible: bool;\n\trightSkip: pixels;\n}\ninfoProfilePhotoInnerSize: 72px;\ninfoProfilePhotoSize: size(\n\tinfoProfilePhotoInnerSize,\n\tinfoProfilePhotoInnerSize);\ninfoProfileStatus: FlatLabel(defaultFlatLabel) {\n\tmaxHeight: 18px;\n\ttextFg: windowSubTextFg;\n}\ninfoProfileCover: InfoProfileCover {\n\theight: 108px;\n\tphoto: UserpicButton(defaultUserpicButton) {\n\t\tsize: infoProfilePhotoSize;\n\t\tphotoSize: infoProfilePhotoInnerSize;\n\t}\n\tphotoLeft: 19px;\n\tphotoTop: 18px;\n\tname: FlatLabel(defaultFlatLabel) {\n\t\tmaxHeight: 24px;\n\t\ttextFg: windowBoldFg;\n\t\tstyle: TextStyle(defaultTextStyle) {\n\t\t\tfont: font(16px semibold);\n\t\t}\n\t}\n\tnameLeft: 109px;\n\tnameTop: 32px;\n\tstatus: infoProfileStatus;\n\tstatusLeft: 109px;\n\tstatusTop: 58px;\n\tstarsRatingLeft: 107px;\n\tstarsRatingTop: 57px;\n\tshowLastSeen: RoundButton(defaultActiveButton) {\n\t\ttextFg: windowSubTextFg;\n\t\ttextFgOver: windowSubTextFg;\n\t\ttextBg: windowBgOver;\n\t\ttextBgOver: windowBgOver;\n\t\twidth: -12px;\n\t\theight: 18px;\n\t\ttextTop: 0px;\n\t\tstyle: TextStyle(defaultTextStyle) {\n\t\t\tfont: font(12px);\n\t\t}\n\t\tripple: defaultRippleAnimation;\n\t}\n\tshowLastSeenPosition: point(3px, 58px);\n\tshowLastSeenVisible: true;\n\trightSkip: 20px;\n}\ninfoProfileMegagroupCover: InfoProfileCover(infoProfileCover) {\n\tstatus: FlatLabel(infoProfileStatus) {\n\t\tpalette: TextPalette(defaultTextPalette) {\n\t\t\tlinkFg: windowSubTextFg;\n\t\t}\n\t}\n}\ninfoTopicCover: InfoProfileCover(infoProfileMegagroupCover) {\n\theight: 77px;\n\tphoto: UserpicButton(defaultUserpicButton) {\n\t\tsize: size(36px, 36px);\n\t}\n\tphotoLeft: 22px;\n\tphotoTop: 18px;\n\tnameLeft: 79px;\n\tnameTop: 14px;\n\tstatusLeft: 79px;\n\tstatusTop: 38px;\n}\ninfoEditContactCover: InfoProfileCover(infoProfileCover) {\n\tnameTop: 33px;\n\tstatusTop: 57px;\n}\ninfoEditContactPersonalLeft: 6px;\n\ninfoRatingDeductedBadge: RoundButton(customEmojiTextBadge) {\n\ttextBg: windowSubTextFg;\n}\n\ninfoProfileInaccessibleUserpic: icon {{ \"info/inaccessible_userpic\", historyPeerUserpicFg }};\n\ninfoVerifiedCheck: icon {{ \"profile_verified_check\", profileVerifiedCheckFg }};\ninfoVerifiedCheckPosition: point(4px, 0px);\ninfoVerifiedStar: icon {{ \"profile_verified_star\", profileVerifiedCheckBg }};\ninfoPremiumStar: icon {{ \"profile_premium\", profileVerifiedCheckBg }};\n\ninfoPeerBadge: InfoPeerBadge {\n\tverified: infoVerifiedStar;\n\tverifiedCheck: infoVerifiedCheck;\n\tpremium: infoPremiumStar;\n\tpremiumFg: profileVerifiedCheckBg;\n\tposition: infoVerifiedCheckPosition;\n\tsizeTag: 0; // Normal\n}\ninfoBotVerifyBadge: InfoPeerBadge(infoPeerBadge) {\n\tposition: point(-2px, 2px);\n}\ninfoColoredPeerBadge: InfoPeerBadge(infoPeerBadge) {\n\tverified: icon {{ \"profile_verified_star\", groupCallMembersFg }}; // Will be colorized.\n\tverifiedCheck: icon {{ \"profile_verified_check\", groupCallMembersFg }};\n\tpremium: icon {{ \"profile_premium\", groupCallMembersFg }};\n\tpremiumFg: groupCallVideoSubTextFg;\n}\ninfoColoredBotVerifyBadge: InfoPeerBadge(infoColoredPeerBadge) {\n\tposition: point(-2px, 2px);\n}\n\ninfoIconFg: windowBoldFg;\n\ninfoProfileSkip: 7px;\n\ninfoProfileLabeledPadding: margins(23px, 9px, 20px, 7px);\ninfoProfileLabeledUsernamePadding: margins(23px, 9px, 20px, 7px);\ninfoProfilePersonalChannelPadding: margins(79px, 9px, 20px, 7px);\ninfoProfileLabeledButtonQr: IconButton(defaultIconButton) {\n\twidth: 34px;\n\theight: 34px;\n\ticon: icon {{ \"menu/qr_code\", windowActiveTextFg }};\n\ticonOver: icon {{ \"menu/qr_code\", windowActiveTextFg }};\n\trippleAreaPosition: point(0px, 0px);\n\trippleAreaSize: 34px;\n\tripple: defaultRippleAnimation;\n}\ninfoProfileLabeledButtonQrRightSkip: 0px;\ninfoProfileLabeledButtonQrInset: 5px;\n\ninfoIconInformation: icon {{ \"info/info_information\", infoIconFg }};\ninfoIconAddMember: icon {{ \"info/info_add_member\", infoIconFg }};\ninfoIconBotBalance: icon {{ \"menu/earn\", infoIconFg, point(5px, 5px) }};\ninfoIconNotifications: icon {{ \"info/info_notifications\", infoIconFg }};\ninfoIconMediaPhoto: icon {{ \"info/info_media_photo\", infoIconFg }};\ninfoIconMediaVideo: icon {{ \"info/info_media_video\", infoIconFg }};\ninfoIconMediaGif: icon {{ \"info/info_media_gif\", infoIconFg }};\ninfoIconMediaFile: icon {{ \"info/info_media_file\", infoIconFg }};\ninfoIconMediaAudio: icon {{ \"info/info_media_audio\", infoIconFg }};\ninfoIconMediaLink: icon {{ \"info/info_media_link\", infoIconFg }};\ninfoIconMediaGroup: icon {{ \"info/info_common_groups\", infoIconFg }};\ninfoIconMediaChannel: icon {{ \"menu/channel\", infoIconFg, point(4px, 4px) }};\ninfoIconMediaBot: icon {{ \"menu/bot\", infoIconFg, point(4px, 4px) }};\ninfoIconMediaVoice: icon {{ \"info/info_media_voice\", infoIconFg }};\ninfoIconMediaPoll: icon {{ \"menu/create_poll\", infoIconFg, point(4px, 4px) }};\ninfoIconMediaStories: icon {{ \"info/info_media_stories\", infoIconFg }};\ninfoIconMediaSaved: icon {{ \"info/info_media_saved\", infoIconFg }};\ninfoIconMediaStoriesArchive: icon {{ \"info/info_stories_archive\", infoIconFg }};\ninfoIconMediaStoriesRecent: icon {{ \"info/info_stories_recent\", infoIconFg }};\ninfoIconMediaGifts: icon {{ \"menu/gift_premium\", infoIconFg, point(4px, 4px) }};\ninfoIconEmojiStatusAccess: icon {{ \"menu/read_reactions\", infoIconFg, point(4px, 4px) }};\n\ninfoIconShare: icon {{ \"info/info_share\", infoIconFg }};\ninfoIconEdit: icon {{ \"info/info_edit\", infoIconFg }};\ninfoIconDelete: icon {{ \"info/info_delete\", infoIconFg }};\ninfoIconDeleteRed: icon {{ \"info/info_delete\", attentionButtonFg }};\ninfoIconReport: icon {{ \"info/info_report\", attentionButtonFg }};\ninfoIconLeave: icon {{ \"info/info_leave\", infoIconFg }};\ninfoIconBlock: icon {{ \"info/info_block\", attentionButtonFg }};\ninfoIconMembers: icon {{ \"info/info_members\", infoIconFg }};\ninfoIconPrivacyPolicy: icon {{ \"menu/2sv_off\", infoIconFg, point(4px, 4px) }};\ninfoIconSettings: icon {{ \"menu/manage\", infoIconFg, point(4px, 4px) }};\ninfoInformationIconPosition: point(25px, 12px);\ninfoNotificationsIconPosition: point(20px, 5px);\ninfoSharedMediaButtonIconPosition: point(20px, 3px);\ninfoGroupMembersIconPosition: point(20px, 10px);\ninfoChannelMembersIconPosition: point(20px, 4px);\ninfoChannelAdminsIconPosition: point(24px, 7px);\ninfoEarnCreditsIconPosition: point(24px, 7px);\ninfoEarnCurrencyIconPosition: point(20px, 3px);\n\ninfoOpenApp: RoundButton(defaultActiveButton) {\n\ttextTop: 11px;\n\theight: 40px;\n}\ninfoOpenAppMargin: margins(16px, 12px, 16px, 12px);\n\ninfoPersonalChannelIconPosition: point(25px, 12px);\ninfoPersonalChannelNameLabel: FlatLabel(infoProfileStatus) {\n\ttextFg: windowBoldFg;\n\tstyle: semiboldTextStyle;\n\tmaxHeight: 20px;\n}\ninfoPersonalChannelDateSkip: 20px;\ninfoPersonalChannelDateLabel: FlatLabel(infoProfileStatus) {\n\ttextFg: dialogsDateFg;\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: dialogsDateFont;\n\t}\n\tmaxHeight: 20px;\n}\ninfoPersonalChannelUserpicSkip: -5px;\ninfoPersonalChannelUserpic: UserpicButton(defaultUserpicButton) {\n\tsize: size(42px, 42px);\n\tphotoSize: 42px;\n}\n\ninfoBlockHeaderLabel: FlatLabel(infoProfileStatus) {\n\ttextFg: windowBoldFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: semiboldFont;\n\t}\n}\ninfoBlockHeaderPosition: point(79px, 17px);\n\ninfoProfileToggle: Toggle(defaultToggle) {\n\tuntoggledFg: menuIconFg;\n}\ninfoProfileToggleOver: Toggle(infoProfileToggle) {\n\tuntoggledFg: menuIconFgOver;\n}\ninfoProfileButton: SettingsButton(defaultSettingsButton) {\n\tstyle: defaultTextStyle;\n\n\tpadding: margins(79px, 10px, 8px, 8px);\n\ticonLeft: 22px;\n\n\ttoggle: infoProfileToggle;\n\ttoggleOver: infoProfileToggleOver;\n\ttoggleSkip: 20px;\n}\ninfoMainButton: SettingsButton(infoProfileButton) {\n\ttextFg: lightButtonFg;\n\ttextFgOver: lightButtonFgOver;\n\tstyle: semiboldTextStyle;\n\tpadding: margins(23px, 10px, 8px, 8px);\n}\ninfoMainButtonAttention: SettingsButton(infoMainButton) {\n\ttextFg: attentionButtonFg;\n\ttextFgOver: attentionButtonFgOver;\n}\ninfoSharedMediaButton: infoProfileButton;\ninfoSharedMediaBottomSkip: 12px;\n\ninfoBlockButton: SettingsButton(infoProfileButton) {\n\ttextFg: attentionButtonFg;\n\ttextFgOver: attentionButtonFgOver;\n}\ninfoCreateDiscussionLinkButton: SettingsButton(infoProfileButton) {\n\tpadding: margins(74px, 10px, 8px, 8px);\n\ttextFg: lightButtonFg;\n\ttextFgOver: lightButtonFgOver;\n}\ninfoUnlinkDiscussionLinkButton: SettingsButton(infoCreateDiscussionLinkButton) {\n\ttextFg: attentionButtonFg;\n\ttextFgOver: attentionButtonFgOver;\n}\ninfoBlockButtonSkip: 8px;\n\ninfoMembersHeader: 56px;\ninfoMembersList: PeerList(defaultPeerList) {\n\titem: PeerListItem(defaultPeerListItem) {\n\t\tphotoPosition: point(18px, 6px);\n\t\tnamePosition: point(79px, 11px);\n\t\tstatusPosition: point(79px, 31px);\n\t\tcheckbox: RoundImageCheckbox(defaultPeerListCheckbox) {\n\t\t\tselectExtendTwice: 1px;\n\t\t\timageRadius: 21px;\n\t\t\timageSmallRadius: 19px;\n\t\t\tcheck: RoundCheckbox(defaultPeerListCheck) {\n\t\t\t\tsize: 0px;\n\t\t\t}\n\t\t}\n\t\tnameFgChecked: contactsNameFg;\n\t}\n}\ninfoMembersButtonPosition: point(12px, 0px);\ninfoMembersButton: IconButton(defaultIconButton) {\n\twidth: 38px;\n\theight: 38px;\n\ticonPosition: point(-1px, -1px);\n\trippleAreaPosition: point(0px, 0px);\n\trippleAreaSize: 38px;\n\tripple: defaultRippleAnimation;\n}\ninfoMembersAddMember: IconButton(infoMembersButton) {\n\ticon: icon {{ \"info/info_add_member\", windowBoldFg }};\n\ticonOver: icon {{ \"info/info_add_member\", windowBoldFg }};\n}\ninfoMembersSearch: IconButton(infoMembersButton) {\n\ticon: icon {{ \"info/info_search\", windowBoldFg }};\n\ticonOver: icon {{ \"info/info_search\", windowBoldFg }};\n}\ninfoMembersSearchField: InputField(defaultMultiSelectSearchField) {\n}\ninfoMembersCancelSearch: CrossButton {\n\twidth: 44px;\n\theight: 44px;\n\n\tcross: CrossAnimation {\n\t\tsize: 44px;\n\t\tskip: 16px;\n\t\tstroke: 1.5;\n\t\tminScale: 0.3;\n\t}\n\tcrossFg: menuIconFg;\n\tcrossFgOver: menuIconFgOver;\n\tcrossPosition: point(0px, 0px);\n\n\tduration: 150;\n\tloadingPeriod: 1000;\n\tripple: defaultRippleAnimationBgOver;\n}\ninfoMembersSearchTop: 15px;\n\ninfoMediaHeaderStyle: semiboldTextStyle;\ninfoMediaHeaderHeight: 28px;\ninfoMediaHeaderPosition: point(14px, 6px);\ninfoMediaSkip: 2px;\ninfoMediaLeft: 3px;\ninfoMediaMargin: margins(0px, 6px, 0px, 2px);\ninfoMediaMinGridSize: 82px;\n\ninfoCommonGroupsMargin: margins(0px, 2px, 0px, 2px);\ninfoCommonGroupsListItem: PeerListItem(defaultPeerListItem) {\n\theight: 52px;\n\tphotoSize: 40px;\n\tphotoPosition: point(16px, 6px);\n\tnamePosition: point(71px, 15px);\n\tnameStyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px semibold);\n\t}\n\tstatusPosition: point(79px, 31px);\n}\ninfoCommonGroupsList: PeerList(infoMembersList) {\n\titem: infoCommonGroupsListItem;\n}\n\nmanagePeerButton: SettingsCountButton {\n\tbutton: SettingsButton(infoProfileButton) {\n\t\tpadding: margins(76px, 12px, 76px, 10px);\n\t}\n\ticonPosition: point(20px, 5px);\n\tlabel: FlatLabel(defaultFlatLabel) {\n\t\ttextFg: windowActiveTextFg;\n\t}\n\tlabelPosition: point(25px, 12px);\n}\n\npeerPermissionsButton: SettingsCountButton(managePeerButton) {\n\tbutton: SettingsButton(infoProfileButton) {\n\t\tpadding: margins(22px, 12px, 24px, 10px);\n\t}\n\ticonPosition: point(24px, 5px);\n}\n\nmanageGroupButtonInner: SettingsButton(infoProfileButton) {\n\tpadding: margins(60px, 10px, 24px, 8px);\n}\nmanageGroupButton: SettingsCountButton(managePeerButton) {\n\tbutton: manageGroupButtonInner;\n\tlabelPosition: point(22px, 10px);\n\ticonPosition: point(20px, 4px);\n}\ninfoSharedMediaCountButton: SettingsCountButton(managePeerButton) {\n\tbutton: infoSharedMediaButton;\n\ticonPosition: point(20px, 4px);\n\tlabel: defaultSettingsRightLabel;\n\tlabelPosition: point(24px, 10px);\n}\n\nmanageGroupTopButtonWithText: SettingsCountButton(manageGroupButton) {\n\ticonPosition: point(0px, 0px);\n}\nmanageGroupTopicsButton: SettingsCountButton(manageGroupTopButtonWithText) {\n\tbutton: SettingsButton(manageGroupButtonInner) {\n\t\ttoggle: Toggle(infoProfileToggle) {\n\t\t\tlockIcon: icon {{ \"info/info_rights_lock\", menuIconFg }};\n\t\t}\n\t\ttoggleOver: Toggle(infoProfileToggleOver) {\n\t\t\tlockIcon: icon {{ \"info/info_rights_lock\", menuIconFgOver }};\n\t\t}\n\t}\n}\nmanagePeerColorsButton: SettingsButton(infoProfileButton) {\n\tpadding: margins(60px, 10px, 48px, 8px);\n}\n\nmanageGroupNoIconButtonInner: SettingsButton(infoProfileButton) {\n\tpadding: margins(25px, 11px, 24px, 8px);\n}\nmanageGroupNoIconButton: SettingsCountButton(manageGroupTopButtonWithText) {\n\tbutton: manageGroupNoIconButtonInner;\n\tlabelPosition: point(22px, 11px);\n\ticonPosition: point(0px, 0px);\n}\n\nmanageDeleteGroupButton: SettingsCountButton(manageGroupNoIconButton) {\n\tbutton: SettingsButton(manageGroupNoIconButtonInner) {\n\t\ttextFg: attentionButtonFg;\n\t\ttextFgOver: attentionButtonFg;\n\t}\n}\n\nmanageGroupReactions: IconButton(defaultIconButton) {\n\twidth: 24px;\n\theight: 36px;\n\ticon: icon{{ \"menu/add\", historyComposeIconFg }};\n\ticonOver: icon{{ \"menu/add\", historyComposeIconFgOver }};\n}\nmanageGroupReactionsField: InputField(defaultInputField) {\n\ttextMargins: margins(1px, 12px, 24px, 8px);\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(1px, 0px, 7px, 0px);\n\tplaceholderAlign: align(topleft);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n\tplaceholderShift: -50px;\n\tstyle: defaultTextStyle;\n\theightMin: 36px;\n\theightMax: 158px;\n}\nmanageGroupReactionsFieldPadding: margins(0px, 0px, 0px, -9px);\nmanageGroupReactionsTextSkip: 16px;\nmanageGroupReactionsMaxSubtitlePadding: margins(0px, 0px, 0px, -3px);\n\ninfoEmptyFg: windowSubTextFg;\ninfoEmptyPhoto: icon {{ \"info/info_media_photo_empty\", infoEmptyFg }};\ninfoEmptyVideo: icon {{ \"info/info_media_video_empty\", infoEmptyFg }};\ninfoEmptyAudio: icon {{ \"info/info_media_audio_empty\", infoEmptyFg }};\ninfoEmptyFile: icon {{ \"info/info_media_file_empty\", infoEmptyFg }};\ninfoEmptyVoice: icon {{ \"info/info_media_voice_empty\", infoEmptyFg }};\ninfoEmptyLink: icon {{ \"info/info_media_link_empty\", infoEmptyFg }};\ninfoEmptyIconTop: 120px;\ninfoEmptyLabelTop: 40px;\ninfoEmptyLabelSkip: 20px;\ninfoEmptyLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 220px;\n\ttextFg: windowSubTextFg;\n}\n\ninfoStoriesAboutArchive: FlatLabel(defaultFlatLabel) {\n\tminWidth: 245px;\n\talign: align(top);\n\ttextFg: windowSubTextFg;\n\tstyle: defaultTextStyle;\n}\ninfoStoriesAboutArchivePadding: margins(22px, 12px, 22px, 12px);\n\neditPeerBottomButtonsLayoutMargins: margins(0px, 7px, 0px, 0px);\n\neditPeerTopButtonsLayoutSkip: 5px;\neditPeerTopButtonsLayoutSkipToBottom: 5px;\n\neditPeerTopButtonsLayoutSkipCustomBottom: 5px;\n\neditPeerHistoryVisibilityTopSkip: 8px;\n\neditPeerPhotoMargins: margins(22px, 8px, 22px, 8px);\neditPeerTitle: defaultInputField;\neditPeerTitleMargins: margins(27px, 13px, 22px, 8px);\neditPeerTitleEmojiPosition: point(0px, 23px);\neditPeerTitleField: InputField(defaultInputField) {\n\ttextMargins: margins(0px, 28px, 30px, 4px);\n}\neditPeerDescription: InputField(defaultInputField) {\n\ttextBg: transparent;\n\ttextMargins: margins(0px, 7px, 0px, 7px);\n\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(2px, 0px, 2px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n\n\tborder: 0px;\n\tborderActive: 0px;\n\n\theightMin: 32px;\n}\neditPeerDescriptionMargins: margins(22px, 3px, 22px, 2px);\neditPeerPrivaciesMargins: margins(15px, 7px, 22px, 0px);\neditPeerPrivacyBottomSkip: 16px;\neditPeerPrivacyLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 220px;\n\ttextFg: windowSubTextFg;\n}\neditPeerPrivacyBoxCheckbox: Checkbox(defaultBoxCheckbox) {\n\tmargin: margins(0px, 8px, 0px, 8px);\n\tstyle: boxTextStyle;\n}\neditPeerPrivacyLabelMargins: margins(42px, 0px, 34px, 0px);\neditPeerPreHistoryLabelMargins: margins(34px, 0px, 34px, 0px);\neditPeerUsernameFieldMargins: margins(22px, 0px, 22px, 0px);\neditPeerUsername: setupChannelLink;\neditPeerUsernameGood: FlatLabel(defaultFlatLabel) {\n\ttextFg: boxTextFgGood;\n\tstyle: boxTextStyle;\n}\n\neditPeerReactionsPreview: 24px;\neditPeerReactionsIconLeft: 21px;\n\nhistoryTopBarBack: IconButton(infoTopBarBack) {\n\twidth: 52px;\n}\ntopBarHeight: 54px;\ntopBarMenuPosition: point(-6px, 45px);\ntopBarMenuGroupCallSkip: 20px;\ntopBarBack: icon {{ \"title_back\", lightButtonFg }};\ntopBarArrowPadding: margins(39px, 8px, 17px, 8px);\ntopBarNameRightPadding: 3px;\ntopBarButton: RoundButton(defaultLightButton) {\n\twidth: -18px;\n\tpadding: margins(0px, 10px, 12px, 10px);\n}\ntopBarClearButton: RoundButton(defaultLightButton) {\n\twidth: -18px;\n}\ntopBarSearch: IconButton {\n\twidth: 40px;\n\theight: topBarHeight;\n\n\ticon: icon {{ \"top_bar_search\", menuIconFg }};\n\ticonOver: icon {{ \"top_bar_search\", menuIconFgOver }};\n\ticonPosition: point(4px, 11px);\n\n\trippleAreaPosition: point(0px, 7px);\n\trippleAreaSize: 40px;\n\tripple: defaultRippleAnimationBgOver;\n}\ntopBarCloseChoose: IconButton(topBarSearch) {\n\twidth: 56px;\n\ticon: icon {{ \"info/info_close\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"info/info_close\", boxTitleCloseFgOver }};\n\ticonPosition: point(10px, -1px);\n\trippleAreaPosition: point(7px, 7px);\n}\ntopBarSkip: -5px;\ntopBarCallSkip: -1px;\ntopBarMenuToggle: IconButton(topBarSearch) {\n\twidth: 44px;\n\n\ticon: menuToggleIcon;\n\ticonOver: menuToggleIconOver;\n\ticonPosition: point(16px, 17px);\n\n\trippleAreaPosition: point(0px, 7px);\n}\ntopBarCall: IconButton(topBarSearch) {\n\ticon: icon {{ \"top_bar_call\", menuIconFg }};\n\ticonOver: icon {{ \"top_bar_call\", menuIconFgOver }};\n}\ntopBarGroupCall: IconButton(topBarSearch) {\n\ticon: icon {{ \"top_bar_group_call\", menuIconFg }};\n\ticonOver: icon {{ \"top_bar_group_call\", menuIconFgOver }};\n\ticonPosition: point(4px, 12px);\n}\ntopBarInfo: IconButton(topBarSearch) {\n\ticon: icon {{ \"top_bar_profile\", menuIconFg }};\n\ticonOver: icon {{ \"top_bar_profile\", menuIconFgOver }};\n}\ntopBarInfoActive: icon {{ \"top_bar_profile\", windowActiveTextFg }};\ntopBarActionSkip: 10px;\ntopBarActionButtonLargeRadius: 8px;\n\ntopBarInfoButtonSize: size(52px, topBarHeight);\ntopBarInfoButtonInnerSize: 42px;\ntopBarInfoButtonInnerPosition: point(2px, -1px);\ntopBarInfoButton: UserpicButton(defaultUserpicButton) {\n\tsize: topBarInfoButtonSize;\n\tphotoSize: topBarInfoButtonInnerSize;\n\tphotoPosition: topBarInfoButtonInnerPosition;\n}\ntopBarConnectingPosition: point(2px, 5px);\ntopBarConnectingSkip: 6px;\ntopBarConnectingAnimation: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {\n\tcolor: windowSubTextFg;\n\tthickness: 1px;\n\tsize: size(8px, 8px);\n}\n\ninviteLinkTitleRight: FlatLabel(defaultSubsectionTitle) {\n\tminWidth: 0px;\n}\n\ninviteLinkFieldRadius: 5px;\ninviteLinkFieldHeight: 42px;\ninviteLinkFieldMargin: margins(14px, 12px, 36px, 9px);\ninviteLinkThreeDotsIcon: icon {{ \"info/edit/dotsmini\", dialogsMenuIconFg }};\ninviteLinkThreeDotsIconOver: icon {{ \"info/edit/dotsmini\", dialogsMenuIconFgOver }};\ninviteLinkThreeDots: IconButton(defaultIconButton) {\n\twidth: 36px;\n\theight: 44px;\n\n\ticon: inviteLinkThreeDotsIcon;\n\ticonOver: inviteLinkThreeDotsIconOver;\n\ticonPosition: point(-1px, -1px);\n\n\trippleAreaSize: 0px;\n}\ninviteLinkFieldPadding: margins(22px, 7px, 22px, 14px);\ninviteLinkFieldLabel: FlatLabel(defaultFlatLabel) {\n\talign: align(center);\n}\n\ninviteLinkButton: RoundButton(defaultActiveButton) {\n\theight: 36px;\n\ttextTop: 9px;\n\tradius: 6px;\n}\ninviteLinkButtonsPadding: margins(22px, 0px, 22px, 0px);\ninviteLinkButtonsSkip: 10px;\ninviteLinkCopy: RoundButton(inviteLinkButton) {\n\ticon: icon {{ \"info/edit/links_copy\", activeButtonFg }};\n\ticonOver: icon {{ \"info/edit/links_copy\", activeButtonFgOver }};\n\ticonPosition: point(-1px, 2px);\n}\ninviteLinkShare: RoundButton(inviteLinkCopy) {\n\ticon: icon {{ \"info/edit/links_share\", activeButtonFg }};\n\ticonOver: icon {{ \"info/edit/links_share\", activeButtonFgOver }};\n}\ninviteLinkReactivate: RoundButton(inviteLinkCopy) {\n\ticon: icon {{ \"info/edit/links_reactivate\", activeButtonFg }};\n\ticonOver: icon {{ \"info/edit/links_reactivate\", activeButtonFgOver }};\n}\ninviteLinkDelete: RoundButton(inviteLinkCopy) {\n\ticon: icon {{ \"info/edit/links_delete\", activeButtonFg }};\n\ticonOver: icon {{ \"info/edit/links_delete\", activeButtonFgOver }};\n}\ninviteLinkUserpics: GroupCallUserpics {\n\tsize: 28px;\n\tshift: 6px;\n\tstroke: 2px;\n\talign: align(left);\n}\ninviteLinkUserpicsSkip: 8px;\ninviteLinkJoinedFont: font(14px);\ninviteLinkJoinedRowPadding: margins(0px, 18px, 0px, 8px);\n\ninviteLinkCreateSkip: 10px;\ninviteLinkCreate: SettingsButton(defaultSettingsButton) {\n\ttextFg: lightButtonFg;\n\ttextFgOver: lightButtonFgOver;\n\ttextBg: windowBg;\n\ttextBgOver: windowBgOver;\n\n\tstyle: semiboldTextStyle;\n\n\theight: 20px;\n\tpadding: margins(60px, 7px, 12px, 5px);\n\n\ttoggle: infoProfileToggle;\n\ttoggleOver: infoProfileToggleOver;\n\ttoggleSkip: 22px;\n\n\tripple: defaultRippleAnimation;\n}\ninviteLinkCreateIcon: icon {{ \"info/edit/roundbtn_plus\", windowFgActive }};\ninviteLinkCreateIconSize: 18px;\ninviteLinkListItem: PeerListItem(defaultPeerListItem) {\n\tbutton: OutlineButton(defaultPeerListButton) {\n\t\tfont: normalFont;\n\t\tpadding: margins(11px, 5px, 11px, 5px);\n\t}\n\theight: 52px;\n\tphotoPosition: point(9px, 4px);\n\tnamePosition: point(60px, 6px);\n\tstatusPosition: point(60px, 26px);\n\tphotoSize: 44px;\n}\ninviteLinkList: PeerList(defaultPeerList) {\n\titem: inviteLinkListItem;\n\tpadding: margins(0px, 4px, 0px, 0px);\n}\ninviteLinkChatList: PeerList(peerListBox) {\n\tpadding: margins(0px, 4px, 0px, 6px);\n}\ninviteLinkAdminsList: PeerList(inviteLinkList) {\n\titem: PeerListItem(inviteLinkListItem) {\n\t\tphotoPosition: point(16px, 9px);\n\t\tnamePosition: point(62px, 6px);\n\t\tstatusPosition: point(62px, 26px);\n\t\tphotoSize: 34px;\n\t}\n\tpadding: margins(0px, 5px, 0px, 6px);\n}\ninviteLinkIconSkip: 7px;\ninviteLinkIconStroke: 2px;\ninviteLinkIcon: icon {{ \"info/edit/links_link\", mediaviewFileExtFg }};\ninviteLinkRevokedIcon: icon {{ \"info/edit/links_revoked\", mediaviewFileExtFg }};\ninviteLinkThreeDotsSkip: 12px;\ninviteLinkRevokedTitlePadding: margins(22px, 16px, 10px, 4px);\ninviteLinkLimitMargin: margins(22px, 8px, 22px, 8px);\ninviteLinkSubscriptionSize: 18px;\n\ninviteLinkQrPixel: 8px;\ninviteLinkQrSkip: 24px;\ninviteLinkQrMargin: margins(0px, 0px, 0px, 13px);\ninviteLinkQrValuePadding: margins(22px, 0px, 22px, 12px);\n\ninviteLinkCreditsField: InputField(defaultInputField) {\n\ttextMargins: margins(23px, 10px, 0px, 0px);\n\ttextAlign: align(left);\n\ttextFg: historyComposeAreaFg;\n\ttextBg: historyComposeAreaBg;\n\theightMin: 36px;\n\theightMax: 36px;\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(0px, 0px, 2px, 0px);\n\tplaceholderAlign: align(left);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n\tplaceholderShift: -50px;\n\tduration: 100;\n}\n\ninviteLinkSubscribeBoxTitle: FlatLabel(defaultFlatLabel) {\n\tminWidth: 300px;\n\ttextFg: windowBoldFg;\n\talign: align(top);\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(17px semibold);\n\t}\n}\ninviteLinkSubscribeBoxAbout: FlatLabel(defaultFlatLabel) {\n\tminWidth: 300px;\n\talign: align(top);\n}\ninviteLinkSubscribeBoxTerms: FlatLabel(defaultFlatLabel) {\n\tminWidth: 300px;\n\talign: align(top);\n\ttextFg: windowSubTextFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(11px);\n\t}\n}\n\nusernamesReorderIcon: icon {{ \"stickers_reorder\", dialogsMenuIconFg }};\n\ninfoAboutGigagroup: FlatLabel(defaultFlatLabel) {\n\tminWidth: 274px;\n}\n\ninfoScrollDateHideTimeout: historyScrollDateHideTimeout;\ninfoDateFadeDuration: historyDateFadeDuration;\n\nshortInfoWidth: 304px;\nshortInfoLabeledPadding: margins(24px, 16px, 24px, 0px);\nshortInfoScroll: ScrollArea(defaultScrollArea) {\n\tdeltat: 3px;\n\tdeltab: 0px;\n\tround: 1px;\n\twidth: 8px;\n\tdeltax: 3px;\n\n\tduration: 150;\n\thiding: 1000;\n}\n\nShortInfoCover {\n\tsize: pixels;\n\tradius: pixels;\n\tname: FlatLabel;\n\tnamePosition: point;\n\tstatus: FlatLabel;\n\tstatusPosition: point;\n\tlinePadding: pixels;\n\tlineSkip: pixels;\n\tline: pixels;\n\tshadowHeight: pixels;\n\tradialAnimation: InfiniteRadialAnimation;\n}\nshortInfoCover: ShortInfoCover {\n\tsize: shortInfoWidth;\n\tradius: boxRadius;\n\tname: FlatLabel(defaultFlatLabel) {\n\t\ttextFg: groupCallVideoTextFg;\n\t\tmaxHeight: 19px;\n\t\tstyle: TextStyle(defaultTextStyle) {\n\t\t\tfont: font(15px semibold);\n\t\t}\n\t}\n\tnamePosition: point(25px, 37px);\n\tstatus: FlatLabel(defaultFlatLabel) {\n\t\ttextFg: groupCallVideoSubTextFg;\n\t\tmaxHeight: 18px;\n\t}\n\tstatusPosition: point(25px, 14px);\n\tlinePadding: 8px;\n\tlineSkip: 4px;\n\tline: 2px;\n\tshadowHeight: 80px;\n\tradialAnimation: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {\n\t\tcolor: radialFg;\n\t\tthickness: 2px;\n\t}\n}\n\npermissionsExpandIcon: icon{{ \"info/edit/expand_arrow_small\", windowBoldFg }};\n\nsimilarChannelsLockOverlap: 58px;\nsimilarChannelsLockFade: 58px;\nsimilarChannelsLock: RoundButton(defaultActiveButton) {\n\theight: 44px;\n\ttextTop: 12px;\n\tstyle: semiboldTextStyle;\n}\nsimilarChannelsLockLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: premiumButtonFg;\n\tstyle: semiboldTextStyle;\n}\nsimilarChannelsLockPadding: margins(12px, 12px, 12px, 12px);\nsimilarChannelsLockAbout: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n\talign: align(top);\n\tminWidth: 128px;\n}\nsimilarChannelsLockAboutPadding: margins(12px, 12px, 12px, 12px);\n\ninfoHoursState: FlatLabel(infoLabeled) {\n\tminWidth: 0px;\n}\ninfoHoursValue: FlatLabel(infoHoursState) {\n\ttextFg: windowSubTextFg;\n\talign: align(topright);\n}\ninfoHoursDayLabel: infoHoursState;\ninfoHoursOuter: RoundButton(defaultActiveButton) {\n\ttextBg: transparent;\n\ttextBgOver: transparent;\n\tripple: defaultRippleAnimationBgOver;\n}\ninfoHoursOuterMargin: margins(8px, 4px, 8px, 4px);\ninfoHoursDaySkip: 6px;\ninfoHoursArrowSize: 4px;\n\ninfoSharedMediaScroll: ScrollArea(defaultScrollArea) {\n\tround: 1px;\n\twidth: 5px;\n\tdeltax: 2px;\n}\n\ninfoGiftTooltip: ImportantTooltip(defaultImportantTooltip) {\n\tmargin: margins(4px, 4px, 4px, 4px);\n\tpadding: margins(8px, 2px, 8px, 3px);\n}\ninfoGiftTooltipFont: font(11px semibold);\n\ntopicsLayoutButtonLabel: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n\tmargin: margins(10px, 4px, 10px, 4px);\n\ttextFg: windowSubTextFg;\n}\ntopicsLayoutButtonIconPadding: margins(4px, 0px, 4px, 0px);\ntopicsLayoutButtonIconSize: 140px;\ntopicsLayoutButtonPadding: margins(4px, 0px, 4px, 12px);\ntopicsLayoutButtonSkip: 0px;\n\ninfoStarsTitle: FlatLabel(defaultFlatLabel) {\n\tminWidth: 40px;\n\ttextFg: windowBoldFg;\n\tmaxHeight: 24px;\n\tstyle: TextStyle(boxTextStyle) {\n\t\tfont: font(17px semibold);\n\t}\n\talign: align(top);\n}\ninfoStarsFeatureTitle: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowBoldFg;\n\tstyle: semiboldTextStyle;\n\tminWidth: 10px;\n\tmaxHeight: 20px;\n}\ninfoStarsFeatureAbout: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n\tminWidth: 20px;\n}\ninfoStarsFeatureIconPosition: point(3px, 7px);\ninfoStarsFeatureMargin: margins(0px, 8px, 0px, 6px);\ninfoStarsFeatureLabelLeft: 46px;\ninfoStarsFeatureSkip: 2px;\ninfoStarsCrown: icon {{ \"limits/filled_rating_crown-24x24\", windowFgActive }};\ninfoStarsUnderstood: IconEmoji{\n\ticon: icon {{ \"limits/filled_understood-20x20\", windowFgActive }};\n\tpadding: margins(0px, -1px, 0px, 0px);\n}\n\ncollectionAbout: FlatLabel(defaultFlatLabel) {\n\tminWidth: 256px;\n}\ncollectionNameField: InputField(defaultInputField) {\n\ttextBg: transparent;\n\ttextMargins: margins(2px, 10px, 32px, 2px);\n\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(2px, 0px, 2px, 0px);\n\tplaceholderScale: 0.;\n\n\theightMin: 36px;\n}\ncollectionEmptyTitle: FlatLabel(boxTitle) {\n}\ncollectionEmptyText: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n}\ncollectionEmptyButton: RoundButton(defaultActiveButton) {\n\twidth: -96px;\n}\ncollectionEmptyTitleMargin: margins(10px, 24px, 10px, 4px);\ncollectionEmptyTextMargin: margins(10px, 4px, 10px, 12px);\ncollectionEmptyAddMargin: margins(10px, 12px, 10px, 24px);\ncollectionAddIcon: IconEmoji {\n\ticon: icon {{ \"settings/plus\", windowFgActive }};\n\tpadding: margins(1px, -2px, 1px, 0px);\n}\ncollectionEditBox: Box(defaultBox) {\n\tbuttonPadding: margins(16px, 12px, 16px, 12px);\n\tbuttonHeight: 42px;\n\tbuttonWide: true;\n\tbutton: RoundButton(defaultActiveButton) {\n\t\theight: 42px;\n\t\ttextTop: 12px;\n\t\tstyle: semiboldTextStyle;\n\t}\n}\ncollectionEditMenuToggle: IconButton(infoTopBarMenu) {\n\twidth: boxTitleHeight;\n\theight: boxTitleHeight;\n\n\ticon: icon {{ \"title_menu_dots\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"title_menu_dots\", boxTitleCloseFgOver }};\n\ticonPosition: point(-1px, -1px);\n\n\trippleAreaPosition: point(4px, 4px);\n\trippleAreaSize: 40px;\n\tripple: defaultRippleAnimationBgOver;\n}\ncollectionSubTabs: SubTabs(defaultSubTabs) {\n\tbg: boxDividerBg;\n}\n\nearnTonIconSize: 16px;\nearnTonIconMargin: margins(0px, 2px, 0px, 0px);\n\ninfoMusicButtonRipple: universalRippleAnimation;\ninfoMusicButtonPadding: margins(16px, 6px, 13px, 6px);\n\nmemberTagPillPadding: margins(5px, -1px, 5px, 0px);\n\ninfoSecurityRiskIconSize: 16px;\ninfoSecurityRiskIconMargin: margins(0px, 2px, 0px, 0px);\n"
  },
  {
    "path": "Telegram/SourceFiles/info/info_content_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/info_content_widget.h\"\n\n#include \"api/api_who_reacted.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_forum.h\"\n#include \"info/profile/info_profile_widget.h\"\n#include \"info/media/info_media_widget.h\"\n#include \"info/common_groups/info_common_groups_widget.h\"\n#include \"info/peer_gifts/info_peer_gifts_common.h\"\n#include \"info/saved/info_saved_music_common.h\"\n#include \"info/stories/info_stories_common.h\"\n#include \"info/info_layer_widget.h\"\n#include \"info/info_section_widget.h\"\n#include \"info/info_controller.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/controls/swipe_handler.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/search_field_controller.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_profile.h\"\n#include \"styles/style_layers.h\"\n\n#include <QtCore/QCoreApplication>\n\nnamespace Info {\nnamespace {\n\nclass FlexibleFiller final : public Ui::RpWidget {\npublic:\n\tusing RpWidget::RpWidget;\n\n\tvoid setTargetWidget(base::unique_qptr<RpWidget> widget);\n\nprivate:\n\tvoid visibleTopBottomUpdated(int visibleTop, int visibleBottom) override;\n\n\tbase::unique_qptr<RpWidget> _target;\n\n};\n\nvoid FlexibleFiller::setTargetWidget(base::unique_qptr<RpWidget> widget) {\n\tExpects(!_target);\n\n\t_target = std::move(widget);\n}\n\nvoid FlexibleFiller::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tif (const auto raw = _target.get()) {\n\t\traw->setVisibleTopBottom(visibleTop, visibleBottom);\n\t}\n}\n\n} // namespace\n\nContentWidget::ContentWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _scroll(\n\tthis,\n\t(_controller->wrap() == Wrap::Search\n\t\t? st::infoSharedMediaScroll\n\t\t: st::defaultScrollArea)) {\n\tusing namespace rpl::mappers;\n\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\t_controller->wrapValue(\n\t) | rpl::on_next([this](Wrap value) {\n\t\tif (value != Wrap::Layer) {\n\t\t\tapplyAdditionalScroll(0);\n\t\t}\n\t\t_bg = (value == Wrap::Layer)\n\t\t\t? st::boxBg\n\t\t\t: st::profileBg;\n\t\tupdate();\n\t}, lifetime());\n\tif (_controller->section().type() != Section::Type::Profile) {\n\t\trpl::combine(\n\t\t\t_controller->wrapValue(),\n\t\t\t_controller->searchEnabledByContent(),\n\t\t\t(_1 == Wrap::Layer) && _2\n\t\t) | rpl::distinct_until_changed(\n\t\t) | rpl::on_next([this](bool shown) {\n\t\t\trefreshSearchField(shown);\n\t\t}, lifetime());\n\t}\n\trpl::merge(\n\t\t_scrollTopSkip.changes(),\n\t\t_scrollBottomSkip.changes()\n\t) | rpl::on_next([this] {\n\t\tupdateControlsGeometry();\n\t}, lifetime());\n}\n\nvoid ContentWidget::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid ContentWidget::updateControlsGeometry() {\n\tif (!_innerWrap) {\n\t\treturn;\n\t}\n\t_innerWrap->resizeToWidth(width());\n\n\tauto newScrollTop = _scroll->scrollTop() + _topDelta;\n\tauto scrollGeometry = rect().marginsRemoved(\n\t\t{ 0, _scrollTopSkip.current(), 0, _scrollBottomSkip.current() });\n\tif (_scroll->geometry() != scrollGeometry) {\n\t\t_scroll->setGeometry(scrollGeometry);\n\t}\n\n\tif (!_scroll->isHidden()) {\n\t\tif (_topDelta) {\n\t\t\t_scroll->scrollToY(newScrollTop);\n\t\t}\n\t\tauto scrollTop = _scroll->scrollTop();\n\t\t_innerWrap->setVisibleTopBottom(\n\t\t\tscrollTop,\n\t\t\tscrollTop + _scroll->height());\n\t}\n}\n\nstd::shared_ptr<ContentMemento> ContentWidget::createMemento() {\n\tauto result = doCreateMemento();\n\t_controller->saveSearchState(result.get());\n\treturn result;\n}\n\nvoid ContentWidget::setIsStackBottom(bool isStackBottom) {\n\t_isStackBottom = isStackBottom;\n}\n\nbool ContentWidget::isStackBottom() const {\n\treturn _isStackBottom;\n}\n\nvoid ContentWidget::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tif (_paintPadding.isNull()) {\n\t\tp.fillRect(e->rect(), _bg);\n\t} else {\n\t\tconst auto &r = e->rect();\n\t\tconst auto padding = QMargins(\n\t\t\t0,\n\t\t\tstd::min(0, (r.top() - _paintPadding.top())),\n\t\t\t0,\n\t\t\tstd::min(0, (r.bottom() - _paintPadding.bottom())));\n\t\tp.fillRect(r + padding, _bg);\n\t}\n}\n\nvoid ContentWidget::setGeometryWithTopMoved(\n\t\tconst QRect &newGeometry,\n\t\tint topDelta) {\n\t_topDelta = topDelta;\n\tauto willBeResized = (size() != newGeometry.size());\n\tif (geometry() != newGeometry) {\n\t\tsetGeometry(newGeometry);\n\t}\n\tif (!willBeResized) {\n\t\tQResizeEvent fake(size(), size());\n\t\tQCoreApplication::sendEvent(this, &fake);\n\t}\n\t_topDelta = 0;\n}\n\nUi::RpWidget *ContentWidget::doSetInnerWidget(\n\t\tobject_ptr<RpWidget> inner) {\n\tusing namespace rpl::mappers;\n\n\t_innerWrap = _scroll->setOwnedWidget(\n\t\tobject_ptr<Ui::PaddingWrap<Ui::RpWidget>>(\n\t\t\tthis,\n\t\t\tstd::move(inner),\n\t\t\t_innerWrap ? _innerWrap->padding() : style::margins()));\n\t_innerWrap->move(0, 0);\n\n\tsetupSwipeHandler(_innerWrap);\n\n\t// MSVC BUG + REGRESSION rpl::mappers::tuple :(\n\trpl::combine(\n\t\t_scroll->scrollTopValue(),\n\t\t_scroll->heightValue(),\n\t\t_innerWrap->entity()->desiredHeightValue()\n\t) | rpl::on_next([this](\n\t\t\tint top,\n\t\t\tint height,\n\t\t\tint desired) {\n\t\tconst auto bottom = top + height;\n\t\t_innerDesiredHeight = desired;\n\t\t_innerWrap->setVisibleTopBottom(top, bottom);\n\t\t_scrollTillBottomChanges.fire_copy(std::max(desired - bottom, 0));\n\t}, _innerWrap->lifetime());\n\n\trpl::combine(\n\t\t_scroll->heightValue(),\n\t\t_innerWrap->entity()->heightValue(),\n\t\t_controller->wrapValue()\n\t) | rpl::on_next([=](\n\t\t\tint scrollHeight,\n\t\t\tint innerHeight,\n\t\t\tWrap wrap) {\n\t\tconst auto added = (wrap == Wrap::Layer)\n\t\t\t? 0\n\t\t\t: std::max(scrollHeight - innerHeight, 0);\n\t\tif (_addedHeight != added) {\n\t\t\t_addedHeight = added;\n\t\t\tupdateInnerPadding();\n\t\t}\n\t}, _innerWrap->lifetime());\n\tupdateInnerPadding();\n\n\treturn _innerWrap->entity();\n}\n\nUi::RpWidget *ContentWidget::doSetupFlexibleInnerWidget(\n\t\tobject_ptr<Ui::RpWidget> inner,\n\t\tFlexibleScrollData &flexibleScroll,\n\t\tFn<void(Ui::RpWidget*)> customSetup) {\n\tconst auto filler = setInnerWidget(object_ptr<FlexibleFiller>(this));\n\tfiller->resize(1, 1);\n\n\tflexibleScroll.contentHeightValue.events(\n\t) | rpl::on_next([=](int h) {\n\t\tfiller->resize(filler->width(), h);\n\t}, filler->lifetime());\n\n\tfiller->widthValue(\n\t) | rpl::start_to_stream(\n\t\tflexibleScroll.fillerWidthValue,\n\t\tfiller->lifetime());\n\n\tif (customSetup) {\n\t\tcustomSetup(filler);\n\t}\n\n\t// ScrollArea -> PaddingWrap -> RpWidget.\n\tconst auto result = inner.release();\n\tresult->setParent(filler->parentWidget()->parentWidget());\n\tresult->raise();\n\tfiller->setTargetWidget(base::unique_qptr<Ui::RpWidget>(result));\n\n\treturn result;\n}\n\nint ContentWidget::scrollTillBottom(int forHeight) const {\n\tconst auto scrollHeight = forHeight\n\t\t- _scrollTopSkip.current()\n\t\t- _scrollBottomSkip.current();\n\tconst auto scrollBottom = _scroll->scrollTop() + scrollHeight;\n\tconst auto desired = _innerDesiredHeight;\n\treturn std::max(desired - scrollBottom, 0);\n}\n\nrpl::producer<int> ContentWidget::scrollTillBottomChanges() const {\n\treturn _scrollTillBottomChanges.events();\n}\n\nvoid ContentWidget::setScrollTopSkip(int scrollTopSkip) {\n\t_scrollTopSkip = scrollTopSkip;\n}\n\nvoid ContentWidget::setScrollBottomSkip(int scrollBottomSkip) {\n\t_scrollBottomSkip = scrollBottomSkip;\n}\n\nrpl::producer<int> ContentWidget::scrollHeightValue() const {\n\treturn _scroll->heightValue();\n}\n\nvoid ContentWidget::applyAdditionalScroll(int additionalScroll) {\n\tif (_additionalScroll != additionalScroll) {\n\t\t_additionalScroll = additionalScroll;\n\t\tif (_innerWrap) {\n\t\t\tupdateInnerPadding();\n\t\t}\n\t}\n}\n\nvoid ContentWidget::updateInnerPadding() {\n\tconst auto addedToBottom = std::max(_additionalScroll, _addedHeight);\n\t_innerWrap->setPadding({ 0, 0, 0, addedToBottom });\n}\n\nvoid ContentWidget::applyMaxVisibleHeight(int maxVisibleHeight) {\n\tif (_maxVisibleHeight != maxVisibleHeight) {\n\t\t_maxVisibleHeight = maxVisibleHeight;\n\t\tupdate();\n\t}\n}\n\nrpl::producer<int> ContentWidget::desiredHeightValue() const {\n\tusing namespace rpl::mappers;\n\treturn rpl::combine(\n\t\t_innerWrap->entity()->desiredHeightValue(),\n\t\t_scrollTopSkip.value(),\n\t\t_scrollBottomSkip.value()\n\t//) | rpl::map(_1 + _2 + _3);\n\t) | rpl::map([=](int desired, int, int) {\n\t\treturn desired\n\t\t\t+ _scrollTopSkip.current()\n\t\t\t+ _scrollBottomSkip.current();\n\t});\n}\n\nrpl::producer<bool> ContentWidget::desiredShadowVisibility() const {\n\tusing namespace rpl::mappers;\n\treturn rpl::combine(\n\t\t_scroll->scrollTopValue(),\n\t\t_scrollTopSkip.value()\n\t) | rpl::map((_1 > 0) || (_2 > 0));\n}\n\nbool ContentWidget::hasTopBarShadow() const {\n\treturn (_scroll->scrollTop() > 0);\n}\n\nvoid ContentWidget::setInnerFocus() {\n\tif (_searchField) {\n\t\t_searchField->setFocus();\n\t} else {\n\t\t_innerWrap->entity()->setFocus();\n\t}\n}\n\nint ContentWidget::scrollTopSave() const {\n\treturn _scroll->scrollTop();\n}\n\nrpl::producer<int> ContentWidget::scrollTopValue() const {\n\treturn _scroll->scrollTopValue();\n}\n\nvoid ContentWidget::scrollTopRestore(int scrollTop) {\n\t_scroll->scrollToY(scrollTop);\n}\n\nvoid ContentWidget::scrollTo(const Ui::ScrollToRequest &request) {\n\t_scroll->scrollTo(request);\n}\n\nbool ContentWidget::floatPlayerHandleWheelEvent(QEvent *e) {\n\treturn _scroll->viewportEvent(e);\n}\n\nQRect ContentWidget::floatPlayerAvailableRect() const {\n\treturn mapToGlobal(_scroll->geometry());\n}\n\nvoid ContentWidget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {\n\tconst auto peer = _controller->key().peer();\n\tconst auto topic = _controller->key().topic();\n\tconst auto sublist = _controller->key().sublist();\n\tif (!peer && !topic) {\n\t\treturn;\n\t}\n\n\tWindow::FillDialogsEntryMenu(\n\t\t_controller->parentController(),\n\t\tDialogs::EntryState{\n\t\t\t.key = (topic\n\t\t\t\t? Dialogs::Key{ topic }\n\t\t\t\t: sublist\n\t\t\t\t? Dialogs::Key{ sublist }\n\t\t\t\t: Dialogs::Key{ peer->owner().history(peer) }),\n\t\t\t.section = Dialogs::EntryState::Section::Profile,\n\t\t},\n\t\taddAction);\n}\n\nvoid ContentWidget::checkBeforeCloseByEscape(Fn<void()> close) {\n\tif (_searchField) {\n\t\tif (!_searchField->empty()) {\n\t\t\t_searchField->setText({});\n\t\t} else {\n\t\t\tclose();\n\t\t}\n\t} else {\n\t\tclose();\n\t}\n}\n\nrpl::producer<SelectedItems> ContentWidget::selectedListValue() const {\n\treturn rpl::single(SelectedItems(Storage::SharedMediaType::Photo));\n}\n\nvoid ContentWidget::setPaintPadding(const style::margins &padding) {\n\t_paintPadding = padding;\n}\n\nvoid ContentWidget::setViewport(\n\t\trpl::producer<not_null<QEvent*>> &&events) const {\n\tstd::move(\n\t\tevents\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\t_scroll->viewportEvent(e);\n\t}, _scroll->lifetime());\n}\n\nauto ContentWidget::titleStories()\n-> rpl::producer<Dialogs::Stories::Content> {\n\treturn nullptr;\n}\n\nvoid ContentWidget::saveChanges(FnMut<void()> done) {\n\tdone();\n}\n\nvoid ContentWidget::refreshSearchField(bool shown) {\n\tauto search = _controller->searchFieldController();\n\tif (search && shown) {\n\t\tauto rowView = search->createRowView(\n\t\t\tthis,\n\t\t\tst::infoLayerMediaSearch);\n\t\t_searchWrap = std::move(rowView.wrap);\n\t\t_searchField = rowView.field;\n\n\t\tconst auto view = _searchWrap.get();\n\t\twidthValue(\n\t\t) | rpl::on_next([=](int newWidth) {\n\t\t\tview->resizeToWidth(newWidth);\n\t\t\tview->moveToLeft(0, 0);\n\t\t}, view->lifetime());\n\t\tview->show();\n\t\t_searchField->setFocus();\n\t\tsetScrollTopSkip(view->heightNoMargins() - st::lineWidth);\n\t} else if (_searchWrap) {\n\t\tif (Ui::InFocusChain(this)) {\n\t\t\tsetFocus();\n\t\t}\n\t\t_searchWrap = nullptr;\n\t\tsetScrollTopSkip(0);\n\t}\n}\n\nint ContentWidget::scrollBottomSkip() const {\n\treturn _scrollBottomSkip.current();\n}\n\nrpl::producer<int> ContentWidget::scrollBottomSkipValue() const {\n\treturn _scrollBottomSkip.value();\n}\n\nrpl::producer<bool> ContentWidget::desiredBottomShadowVisibility() {\n\tusing namespace rpl::mappers;\n\treturn rpl::combine(\n\t\t_scroll->scrollTopValue(),\n\t\t_scrollBottomSkip.value(),\n\t\t_scroll->heightValue()\n\t) | rpl::map([=](int scroll, int skip, int) {\n\t\treturn ((skip > 0) && (scroll < _scroll->scrollTopMax()));\n\t});\n}\n\nnot_null<Ui::ScrollArea*> ContentWidget::scroll() const {\n\treturn _scroll.data();\n}\n\nvoid ContentWidget::replaceSwipeHandler(\n\t\tUi::Controls::SwipeHandlerArgs *incompleteArgs) {\n\t_swipeHandlerLifetime.destroy();\n\tauto args = std::move(*incompleteArgs);\n\targs.widget = _innerWrap;\n\targs.scroll = _scroll.data();\n\targs.onLifetime = &_swipeHandlerLifetime;\n\tUi::Controls::SetupSwipeHandler(std::move(args));\n}\n\nvoid ContentWidget::setupSwipeHandler(not_null<Ui::RpWidget*> widget) {\n\t_swipeHandlerLifetime.destroy();\n\n\tauto update = [=](Ui::Controls::SwipeContextData data) {\n\t\tif (data.translation > 0) {\n\t\t\tif (!_swipeBackData.callback) {\n\t\t\t\t_swipeBackData = Ui::Controls::SetupSwipeBack(\n\t\t\t\t\tthis,\n\t\t\t\t\t[]() -> std::pair<QColor, QColor> {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tst::historyForwardChooseBg->c,\n\t\t\t\t\t\t\tst::historyForwardChooseFg->c,\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t}\n\t\t\t_swipeBackData.callback(data);\n\t\t\treturn;\n\t\t} else if (_swipeBackData.lifetime) {\n\t\t\t_swipeBackData = {};\n\t\t}\n\t};\n\n\tauto init = [=](int, Qt::LayoutDirection direction) {\n\t\treturn (direction == Qt::RightToLeft && _controller->hasBackButton())\n\t\t\t? Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {\n\t\t\t\tcheckBeforeClose(crl::guard(this, [=] {\n\t\t\t\t\t_controller->parentController()->hideLayer();\n\t\t\t\t\t_controller->showBackFromStack();\n\t\t\t\t}));\n\t\t\t})\n\t\t\t: Ui::Controls::SwipeHandlerFinishData();\n\t};\n\n\tUi::Controls::SetupSwipeHandler({\n\t\t.widget = widget,\n\t\t.scroll = _scroll.data(),\n\t\t.update = std::move(update),\n\t\t.init = std::move(init),\n\t\t.onLifetime = &_swipeHandlerLifetime,\n\t});\n}\n\nKey ContentMemento::key() const {\n\tif (const auto topic = this->topic()) {\n\t\treturn Key(topic);\n\t} else if (const auto sublist = this->sublist()) {\n\t\treturn Key(sublist);\n\t} else if (const auto peer = this->peer()) {\n\t\treturn Key(peer);\n\t} else if (const auto poll = this->poll()) {\n\t\treturn Key(poll, pollContextId());\n\t} else if (const auto self = settingsSelf()) {\n\t\treturn Settings::Tag{ self };\n\t} else if (const auto gifts = giftsPeer()) {\n\t\treturn PeerGifts::Tag{\n\t\t\tgifts,\n\t\t\tgiftsCollectionId(),\n\t\t};\n\t} else if (const auto stories = storiesPeer()) {\n\t\treturn Stories::Tag{\n\t\t\tstories,\n\t\t\tstoriesAlbumId(),\n\t\t\tstoriesAddToAlbumId(),\n\t\t};\n\t} else if (const auto music = musicPeer()) {\n\t\treturn Saved::MusicTag{ music };\n\t} else if (statisticsTag().peer) {\n\t\treturn statisticsTag();\n\t} else if (const auto starref = starrefPeer()) {\n\t\treturn BotStarRef::Tag(starref, starrefType());\n\t} else if (const auto who = reactionsWhoReadIds()) {\n\t\treturn Key(who, _reactionsSelected, _pollReactionsContextId);\n\t} else if (const auto another = globalMediaSelf()) {\n\t\treturn GlobalMedia::Tag{ another };\n\t} else {\n\t\treturn Downloads::Tag();\n\t}\n}\n\nContentMemento::ContentMemento(\n\tnot_null<PeerData*> peer,\n\tData::ForumTopic *topic,\n\tData::SavedSublist *sublist,\n\tPeerId migratedPeerId)\n: _peer(peer)\n, _migratedPeerId((!topic && !sublist && peer->migrateFrom())\n\t? peer->migrateFrom()->id\n\t: 0)\n, _topic(topic)\n, _sublist(sublist) {\n\tif (_topic) {\n\t\t_peer->owner().itemIdChanged(\n\t\t) | rpl::on_next([=](const Data::Session::IdChange &change) {\n\t\t\tif (_topic->rootId() == change.oldId) {\n\t\t\t\t_topic = _topic->forum()->topicFor(change.newId.msg);\n\t\t\t}\n\t\t}, _lifetime);\n\t}\n}\n\nContentMemento::ContentMemento(Settings::Tag settings)\n: _settingsSelf(settings.self.get()) {\n}\n\nContentMemento::ContentMemento(Downloads::Tag downloads) {\n}\n\nContentMemento::ContentMemento(Stories::Tag stories)\n: _storiesPeer(stories.peer)\n, _storiesAlbumId(stories.albumId)\n, _storiesAddToAlbumId(stories.addingToAlbumId) {\n}\n\nContentMemento::ContentMemento(Saved::MusicTag music)\n: _musicPeer(music.peer) {\n}\n\nContentMemento::ContentMemento(PeerGifts::Tag gifts)\n: _giftsPeer(gifts.peer)\n, _giftsCollectionId(gifts.collectionId) {\n}\n\nContentMemento::ContentMemento(Statistics::Tag statistics)\n: _statisticsTag(statistics) {\n}\n\nContentMemento::ContentMemento(BotStarRef::Tag starref)\n: _starrefPeer(starref.peer)\n, _starrefType(starref.type) {\n}\n\nContentMemento::ContentMemento(GlobalMedia::Tag global)\n: _globalMediaSelf(global.self) {\n}\n\nContentMemento::ContentMemento(\n\tstd::shared_ptr<Api::WhoReadList> whoReadIds,\n\tFullMsgId contextId,\n\tData::ReactionId selected)\n: _reactionsWhoReadIds(whoReadIds\n\t? whoReadIds\n\t: std::make_shared<Api::WhoReadList>())\n, _reactionsSelected(selected)\n, _pollReactionsContextId(contextId) {\n}\n\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/info_content_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_flexible_scroll.h\"\n#include \"info/info_wrap_widget.h\"\n#include \"info/statistics/info_statistics_tag.h\"\n#include \"ui/controls/swipe_handler_data.h\"\n\nnamespace Api {\nstruct WhoReadList;\n} // namespace Api\n\nnamespace Dialogs::Stories {\nstruct Content;\n} // namespace Dialogs::Stories\n\nnamespace Storage {\nenum class SharedMediaType : signed char;\n} // namespace Storage\n\nnamespace Ui {\nnamespace Controls {\nstruct SwipeHandlerArgs;\n} // namespace Controls\nclass RoundRect;\nclass ScrollArea;\nclass InputField;\nstruct ScrollToRequest;\ntemplate <typename Widget>\nclass PaddingWrap;\n} // namespace Ui\n\nnamespace Ui::Menu {\nstruct MenuCallback;\n} // namespace Ui::Menu\n\nnamespace Info::Settings {\nstruct Tag;\n} // namespace Info::Settings\n\nnamespace Info::Downloads {\nstruct Tag;\n} // namespace Info::Downloads\n\nnamespace Info::Statistics {\nstruct Tag;\n} // namespace Info::Statistics\n\nnamespace Info::BotStarRef {\nenum class Type : uchar;\nstruct Tag;\n} // namespace Info::BotStarRef\n\nnamespace Info::GlobalMedia {\nstruct Tag;\n} // namespace Info::GlobalMedia\n\nnamespace Info::PeerGifts {\nstruct Tag;\n} // namespace Info::PeerGifts\n\nnamespace Info::Stories {\nstruct Tag;\n} // namespace Info::Stories\n\nnamespace Info::Saved {\nstruct MusicTag;\n} // namespace Info::Saved\n\nnamespace Info {\n\nclass ContentMemento;\nclass Controller;\nstruct FlexibleScrollData;\n\nclass ContentWidget : public Ui::RpWidget {\npublic:\n\tContentWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller);\n\n\tvirtual bool showInternal(\n\t\tnot_null<ContentMemento*> memento) = 0;\n\tstd::shared_ptr<ContentMemento> createMemento();\n\n\tvirtual void setIsStackBottom(bool isStackBottom);\n\t[[nodiscard]] bool isStackBottom() const;\n\n\trpl::producer<int> scrollHeightValue() const;\n\trpl::producer<int> desiredHeightValue() const override;\n\tvirtual rpl::producer<bool> desiredShadowVisibility() const;\n\tbool hasTopBarShadow() const;\n\n\tvirtual void setInnerFocus();\n\tvirtual void showFinished() {\n\t}\n\tvirtual void enableBackButton() {\n\t}\n\n\t// When resizing the widget with top edge moved up or down and we\n\t// want to add this top movement to the scroll position, so inner\n\t// content will not move.\n\tvoid setGeometryWithTopMoved(\n\t\tconst QRect &newGeometry,\n\t\tint topDelta);\n\tvoid applyAdditionalScroll(int additionalScroll);\n\tvoid applyMaxVisibleHeight(int maxVisibleHeight);\n\tint scrollTillBottom(int forHeight) const;\n\t[[nodiscard]] rpl::producer<int> scrollTillBottomChanges() const;\n\t[[nodiscard]] virtual const Ui::RoundRect *bottomSkipRounding() const {\n\t\treturn nullptr;\n\t}\n\n\t// Float player interface.\n\tbool floatPlayerHandleWheelEvent(QEvent *e);\n\tQRect floatPlayerAvailableRect() const;\n\n\tvirtual rpl::producer<SelectedItems> selectedListValue() const;\n\tvirtual void selectionAction(SelectionAction action) {\n\t}\n\tvirtual void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction);\n\n\t[[nodiscard]] virtual bool closeByOutsideClick() const {\n\t\treturn true;\n\t}\n\tvirtual void checkBeforeClose(Fn<void()> close) {\n\t\tclose();\n\t}\n\tvirtual void checkBeforeCloseByEscape(Fn<void()> close);\n\t[[nodiscard]] virtual rpl::producer<QString> title() = 0;\n\t[[nodiscard]] virtual rpl::producer<QString> subtitle() {\n\t\treturn nullptr;\n\t}\n\t[[nodiscard]] virtual auto titleStories()\n\t\t-> rpl::producer<Dialogs::Stories::Content>;\n\n\tvirtual void saveChanges(FnMut<void()> done);\n\n\t[[nodiscard]] int scrollBottomSkip() const;\n\t[[nodiscard]] rpl::producer<int> scrollBottomSkipValue() const;\n\t[[nodiscard]] virtual auto desiredBottomShadowVisibility()\n\t\t-> rpl::producer<bool>;\n\n\tvoid replaceSwipeHandler(Ui::Controls::SwipeHandlerArgs *incompleteArgs);\n\nprotected:\n\ttemplate <typename Widget>\n\tWidget *setInnerWidget(object_ptr<Widget> inner) {\n\t\treturn static_cast<Widget*>(\n\t\t\tdoSetInnerWidget(std::move(inner)));\n\t}\n\n\ttemplate <typename Widget>\n\tWidget *setupFlexibleInnerWidget(\n\t\t\tobject_ptr<Widget> inner,\n\t\t\tFlexibleScrollData &flexibleScroll,\n\t\t\tFn<void(Ui::RpWidget*)> customSetup = nullptr) {\n\t\tif (!inner->hasFlexibleTopBar()) {\n\t\t\treturn setInnerWidget(std::move(inner));\n\t\t}\n\t\treturn static_cast<Widget*>(doSetupFlexibleInnerWidget(\n\t\t\tstd::move(inner),\n\t\t\tflexibleScroll,\n\t\t\tstd::move(customSetup)));\n\t}\n\n\t[[nodiscard]] not_null<Controller*> controller() const {\n\t\treturn _controller;\n\t}\n\t[[nodiscard]] not_null<Ui::ScrollArea*> scroll() const;\n\t[[nodiscard]] int maxVisibleHeight() const {\n\t\treturn _maxVisibleHeight;\n\t}\n\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid setScrollTopSkip(int scrollTopSkip);\n\tvoid setScrollBottomSkip(int scrollBottomSkip);\n\tint scrollTopSave() const;\n\tvoid scrollTopRestore(int scrollTop);\n\tvoid scrollTo(const Ui::ScrollToRequest &request);\n\t[[nodiscard]] rpl::producer<int> scrollTopValue() const;\n\n\tvoid setPaintPadding(const style::margins &padding);\n\n\tvoid setViewport(rpl::producer<not_null<QEvent*>> &&events) const;\n\nprivate:\n\tUi::RpWidget *doSetInnerWidget(object_ptr<Ui::RpWidget> inner);\n\tUi::RpWidget *doSetupFlexibleInnerWidget(\n\t\tobject_ptr<Ui::RpWidget> inner,\n\t\tFlexibleScrollData &flexibleScroll,\n\t\tFn<void(Ui::RpWidget*)> customSetup);\n\n\tvoid updateControlsGeometry();\n\tvoid refreshSearchField(bool shown);\n\tvoid setupSwipeHandler(not_null<Ui::RpWidget*> widget);\n\tvoid updateInnerPadding();\n\n\tvirtual std::shared_ptr<ContentMemento> doCreateMemento() = 0;\n\n\tconst not_null<Controller*> _controller;\n\n\tstyle::color _bg;\n\trpl::variable<int> _scrollTopSkip = -1;\n\trpl::variable<int> _scrollBottomSkip = 0;\n\trpl::event_stream<int> _scrollTillBottomChanges;\n\tobject_ptr<Ui::ScrollArea> _scroll;\n\tUi::PaddingWrap<Ui::RpWidget> *_innerWrap = nullptr;\n\tbase::unique_qptr<Ui::RpWidget> _searchWrap = nullptr;\n\tQPointer<Ui::InputField> _searchField;\n\tint _innerDesiredHeight = 0;\n\tint _additionalScroll = 0;\n\tint _addedHeight = 0;\n\tint _maxVisibleHeight = 0;\n\tbool _isStackBottom = false;\n\n\t// Saving here topDelta in setGeometryWithTopMoved() to get it passed to resizeEvent().\n\tint _topDelta = 0;\n\n\t// To paint round edges from content.\n\tstyle::margins _paintPadding;\n\n\tUi::Controls::SwipeBackResult _swipeBackData;\n\trpl::lifetime _swipeHandlerLifetime;\n\n};\n\nclass ContentMemento {\npublic:\n\tContentMemento(\n\t\tnot_null<PeerData*> peer,\n\t\tData::ForumTopic *topic,\n\t\tData::SavedSublist *sublist,\n\t\tPeerId migratedPeerId);\n\texplicit ContentMemento(PeerGifts::Tag gifts);\n\texplicit ContentMemento(Settings::Tag settings);\n\texplicit ContentMemento(Downloads::Tag downloads);\n\texplicit ContentMemento(Stories::Tag stories);\n\texplicit ContentMemento(Saved::MusicTag music);\n\texplicit ContentMemento(Statistics::Tag statistics);\n\texplicit ContentMemento(BotStarRef::Tag starref);\n\texplicit ContentMemento(GlobalMedia::Tag global);\n\tContentMemento(not_null<PollData*> poll, FullMsgId contextId)\n\t: _poll(poll)\n\t, _pollReactionsContextId(contextId) {\n\t}\n\tContentMemento(\n\t\tstd::shared_ptr<Api::WhoReadList> whoReadIds,\n\t\tFullMsgId contextId,\n\t\tData::ReactionId selected);\n\tvirtual ~ContentMemento() = default;\n\n\t[[nodiscard]] virtual object_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) = 0;\n\n\t[[nodiscard]] PeerData *peer() const {\n\t\treturn _peer;\n\t}\n\t[[nodiscard]] PeerId migratedPeerId() const {\n\t\treturn _migratedPeerId;\n\t}\n\t[[nodiscard]] Data::ForumTopic *topic() const {\n\t\treturn _topic;\n\t}\n\t[[nodiscard]] Data::SavedSublist *sublist() const {\n\t\treturn _sublist;\n\t}\n\t[[nodiscard]] UserData *settingsSelf() const {\n\t\treturn _settingsSelf;\n\t}\n\t[[nodiscard]] PeerData *storiesPeer() const {\n\t\treturn _storiesPeer;\n\t}\n\t[[nodiscard]] int storiesAlbumId() const {\n\t\treturn _storiesAlbumId;\n\t}\n\t[[nodiscard]] int storiesAddToAlbumId() const {\n\t\treturn _storiesAddToAlbumId;\n\t}\n\t[[nodiscard]] PeerData *musicPeer() const {\n\t\treturn _musicPeer;\n\t}\n\t[[nodiscard]] PeerData *giftsPeer() const {\n\t\treturn _giftsPeer;\n\t}\n\t[[nodiscard]] int giftsCollectionId() const {\n\t\treturn _giftsCollectionId;\n\t}\n\t[[nodiscard]] Statistics::Tag statisticsTag() const {\n\t\treturn _statisticsTag;\n\t}\n\t[[nodiscard]] PeerData *starrefPeer() const {\n\t\treturn _starrefPeer;\n\t}\n\t[[nodiscard]] BotStarRef::Type starrefType() const {\n\t\treturn _starrefType;\n\t}\n\t[[nodiscard]] PollData *poll() const {\n\t\treturn _poll;\n\t}\n\t[[nodiscard]] FullMsgId pollContextId() const {\n\t\treturn _poll ? _pollReactionsContextId : FullMsgId();\n\t}\n\t[[nodiscard]] auto reactionsWhoReadIds() const\n\t-> std::shared_ptr<Api::WhoReadList> {\n\t\treturn _reactionsWhoReadIds;\n\t}\n\t[[nodiscard]] Data::ReactionId reactionsSelected() const {\n\t\treturn _reactionsSelected;\n\t}\n\t[[nodiscard]] FullMsgId reactionsContextId() const {\n\t\treturn _reactionsWhoReadIds ? _pollReactionsContextId : FullMsgId();\n\t}\n\t[[nodiscard]] UserData *globalMediaSelf() const {\n\t\treturn _globalMediaSelf;\n\t}\n\t[[nodiscard]] Key key() const;\n\n\t[[nodiscard]] virtual Section section() const = 0;\n\n\tvoid setScrollTop(int scrollTop) {\n\t\t_scrollTop = scrollTop;\n\t}\n\tint scrollTop() const {\n\t\treturn _scrollTop;\n\t}\n\tvoid setSearchFieldQuery(const QString &query) {\n\t\t_searchFieldQuery = query;\n\t}\n\t[[nodiscard]] QString searchFieldQuery() const {\n\t\treturn _searchFieldQuery;\n\t}\n\tvoid setSearchEnabledByContent(bool enabled) {\n\t\t_searchEnabledByContent = enabled;\n\t}\n\t[[nodiscard]] bool searchEnabledByContent() const {\n\t\treturn _searchEnabledByContent;\n\t}\n\tvoid setSearchStartsFocused(bool focused) {\n\t\t_searchStartsFocused = focused;\n\t}\n\t[[nodiscard]] bool searchStartsFocused() const {\n\t\treturn _searchStartsFocused;\n\t}\n\nprivate:\n\tPeerData * const _peer = nullptr;\n\tconst PeerId _migratedPeerId = 0;\n\tData::ForumTopic *_topic = nullptr;\n\tData::SavedSublist *_sublist = nullptr;\n\tUserData * const _settingsSelf = nullptr;\n\tPeerData * const _storiesPeer = nullptr;\n\tint _storiesAlbumId = 0;\n\tint _storiesAddToAlbumId = 0;\n\tPeerData * const _musicPeer = nullptr;\n\tPeerData * const _giftsPeer = nullptr;\n\tint _giftsCollectionId = 0;\n\tStatistics::Tag _statisticsTag;\n\tPeerData * const _starrefPeer = nullptr;\n\tBotStarRef::Type _starrefType = {};\n\tPollData * const _poll = nullptr;\n\tstd::shared_ptr<Api::WhoReadList> _reactionsWhoReadIds;\n\tData::ReactionId _reactionsSelected;\n\tconst FullMsgId _pollReactionsContextId;\n\tUserData * const _globalMediaSelf = nullptr;\n\n\tint _scrollTop = 0;\n\tQString _searchFieldQuery;\n\tbool _searchEnabledByContent = false;\n\tbool _searchStartsFocused = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/info_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/info_controller.h\"\n\n#include \"ui/search_field_controller.h\"\n#include \"history/history.h\"\n#include \"info/info_content_widget.h\"\n#include \"info/info_memento.h\"\n#include \"info/global_media/info_global_media_widget.h\"\n#include \"info/media/info_media_widget.h\"\n#include \"info/polls/info_polls_list_widget.h\"\n#include \"core/application.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_shared_media.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_download_manager.h\"\n#include \"history/history_item.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace Info {\n\nKey::Key(not_null<PeerData*> peer) : _value(peer) {\n}\n\nKey::Key(not_null<Data::ForumTopic*> topic) : _value(topic) {\n}\n\nKey::Key(not_null<Data::SavedSublist*> sublist) : _value(sublist) {\n}\n\nKey::Key(Settings::Tag settings) : _value(settings) {\n}\n\nKey::Key(Downloads::Tag downloads) : _value(downloads) {\n}\n\nKey::Key(Stories::Tag stories) : _value(stories) {\n}\n\nKey::Key(Saved::MusicTag music) : _value(music) {\n}\n\nKey::Key(Statistics::Tag statistics) : _value(statistics) {\n}\n\nKey::Key(PeerGifts::Tag gifts) : _value(gifts) {\n}\n\nKey::Key(BotStarRef::Tag starref) : _value(starref) {\n}\n\nKey::Key(GlobalMedia::Tag global) : _value(global) {\n}\n\nKey::Key(not_null<PollData*> poll, FullMsgId contextId)\n: _value(PollKey{ poll, contextId }) {\n}\n\nKey::Key(\n\tstd::shared_ptr<Api::WhoReadList> whoReadIds,\n\tData::ReactionId selected,\n\tFullMsgId contextId)\n: _value(ReactionsKey{ whoReadIds, selected, contextId }) {\n}\n\nPeerData *Key::peer() const {\n\tif (const auto peer = std::get_if<not_null<PeerData*>>(&_value)) {\n\t\treturn *peer;\n\t} else if (const auto topic = this->topic()) {\n\t\treturn topic->peer();\n\t} else if (const auto sublist = this->sublist()) {\n\t\treturn sublist->owningHistory()->peer;\n\t}\n\treturn nullptr;\n}\n\nData::ForumTopic *Key::topic() const {\n\tif (const auto topic = std::get_if<not_null<Data::ForumTopic*>>(\n\t\t\t&_value)) {\n\t\treturn *topic;\n\t}\n\treturn nullptr;\n}\n\nData::SavedSublist *Key::sublist() const {\n\tif (const auto sublist = std::get_if<not_null<Data::SavedSublist*>>(\n\t\t\t&_value)) {\n\t\treturn *sublist;\n\t}\n\treturn nullptr;\n}\n\nUserData *Key::settingsSelf() const {\n\tif (const auto tag = std::get_if<Settings::Tag>(&_value)) {\n\t\treturn tag->self;\n\t}\n\treturn nullptr;\n}\n\nbool Key::isDownloads() const {\n\treturn v::is<Downloads::Tag>(_value);\n}\n\nbool Key::isGlobalMedia() const {\n\treturn v::is<GlobalMedia::Tag>(_value);\n}\n\nPeerData *Key::storiesPeer() const {\n\tif (const auto tag = std::get_if<Stories::Tag>(&_value)) {\n\t\treturn tag->peer;\n\t}\n\treturn nullptr;\n}\n\nint Key::storiesAlbumId() const {\n\tif (const auto tag = std::get_if<Stories::Tag>(&_value)) {\n\t\treturn tag->albumId;\n\t}\n\treturn 0;\n}\n\nint Key::storiesAddToAlbumId() const {\n\tif (const auto tag = std::get_if<Stories::Tag>(&_value)) {\n\t\treturn tag->addingToAlbumId;\n\t}\n\treturn 0;\n}\n\nPeerData *Key::musicPeer() const {\n\tif (const auto tag = std::get_if<Saved::MusicTag>(&_value)) {\n\t\treturn tag->peer;\n\t}\n\treturn nullptr;\n}\n\nPeerData *Key::giftsPeer() const {\n\tif (const auto tag = std::get_if<PeerGifts::Tag>(&_value)) {\n\t\treturn tag->peer;\n\t}\n\treturn nullptr;\n}\n\nint Key::giftsCollectionId() const {\n\tif (const auto tag = std::get_if<PeerGifts::Tag>(&_value)) {\n\t\treturn tag->collectionId;\n\t}\n\treturn 0;\n}\n\nStatistics::Tag Key::statisticsTag() const {\n\tif (const auto tag = std::get_if<Statistics::Tag>(&_value)) {\n\t\treturn *tag;\n\t}\n\treturn Statistics::Tag();\n}\n\nPeerData *Key::starrefPeer() const {\n\tif (const auto tag = std::get_if<BotStarRef::Tag>(&_value)) {\n\t\treturn tag->peer;\n\t}\n\treturn nullptr;\n}\n\nBotStarRef::Type Key::starrefType() const {\n\tif (const auto tag = std::get_if<BotStarRef::Tag>(&_value)) {\n\t\treturn tag->type;\n\t}\n\treturn BotStarRef::Type();\n}\n\nPollData *Key::poll() const {\n\tif (const auto data = std::get_if<PollKey>(&_value)) {\n\t\treturn data->poll;\n\t}\n\treturn nullptr;\n}\n\nFullMsgId Key::pollContextId() const {\n\tif (const auto data = std::get_if<PollKey>(&_value)) {\n\t\treturn data->contextId;\n\t}\n\treturn FullMsgId();\n}\n\nstd::shared_ptr<Api::WhoReadList> Key::reactionsWhoReadIds() const {\n\tif (const auto data = std::get_if<ReactionsKey>(&_value)) {\n\t\treturn data->whoReadIds;\n\t}\n\treturn nullptr;\n}\n\nData::ReactionId Key::reactionsSelected() const {\n\tif (const auto data = std::get_if<ReactionsKey>(&_value)) {\n\t\treturn data->selected;\n\t}\n\treturn Data::ReactionId();\n}\n\nFullMsgId Key::reactionsContextId() const {\n\tif (const auto data = std::get_if<ReactionsKey>(&_value)) {\n\t\treturn data->contextId;\n\t}\n\treturn FullMsgId();\n}\n\nrpl::producer<SparseIdsMergedSlice> AbstractController::mediaSource(\n\t\tSparseIdsMergedSlice::UniversalMsgId aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) const {\n\tExpects(peer() != nullptr);\n\n\tconst auto isScheduled = [&] {\n\t\tconst auto peerId = peer()->id;\n\t\tif (const auto item = session().data().message(peerId, aroundId)) {\n\t\t\treturn item->isScheduled();\n\t\t}\n\t\treturn false;\n\t}();\n\n\tconst auto mediaViewer = isScheduled\n\t\t? SharedScheduledMediaViewer\n\t\t: SharedMediaMergedViewer;\n\tconst auto topicId = isScheduled\n\t\t? SparseIdsMergedSlice::kScheduledTopicId\n\t\t: topic()\n\t\t? topic()->rootId()\n\t\t: MsgId(0);\n\n\treturn mediaViewer(\n\t\t&session(),\n\t\tSharedMediaMergedKey(\n\t\t\tSparseIdsMergedSlice::Key(\n\t\t\t\tpeer()->id,\n\t\t\t\ttopicId,\n\t\t\t\tsublist() ? sublist()->sublistPeer()->id : PeerId(),\n\t\t\t\tmigratedPeerId(),\n\t\t\t\taroundId),\n\t\t\tsection().mediaType()),\n\t\tlimitBefore,\n\t\tlimitAfter);\n}\n\nrpl::producer<QString> AbstractController::mediaSourceQueryValue() const {\n\treturn rpl::single(QString());\n}\n\nrpl::producer<QString> AbstractController::searchQueryValue() const {\n\treturn rpl::single(QString());\n}\n\nAbstractController::AbstractController(\n\tnot_null<Window::SessionController*> parent)\n: SessionNavigation(&parent->session())\n, _parent(parent) {\n}\n\nPeerData *AbstractController::peer() const {\n\treturn key().peer();\n}\n\nPeerId AbstractController::migratedPeerId() const {\n\tif (const auto peer = migrated()) {\n\t\treturn peer->id;\n\t}\n\treturn PeerId(0);\n}\n\nPollData *AbstractController::poll() const {\n\tif (const auto item = session().data().message(pollContextId())) {\n\t\tif (const auto media = item->media()) {\n\t\t\treturn media->poll();\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nauto AbstractController::reactionsWhoReadIds() const\n-> std::shared_ptr<Api::WhoReadList> {\n\treturn key().reactionsWhoReadIds();\n}\n\nData::ReactionId AbstractController::reactionsSelected() const {\n\treturn key().reactionsSelected();\n}\n\nFullMsgId AbstractController::reactionsContextId() const {\n\treturn key().reactionsContextId();\n}\n\nvoid AbstractController::showSection(\n\t\tstd::shared_ptr<Window::SectionMemento> memento,\n\t\tconst Window::SectionShow &params) {\n\treturn parentController()->showSection(std::move(memento), params);\n}\n\nvoid AbstractController::showBackFromStack(\n\t\tconst Window::SectionShow &params) {\n\treturn parentController()->showBackFromStack(params);\n}\n\nvoid AbstractController::showPeerHistory(\n\t\tPeerId peerId,\n\t\tconst Window::SectionShow &params,\n\t\tMsgId msgId) {\n\treturn parentController()->showPeerHistory(peerId, params, msgId);\n}\n\nController::Controller(\n\tnot_null<WrapWidget*> widget,\n\tnot_null<Window::SessionController*> window,\n\tnot_null<ContentMemento*> memento)\n: AbstractController(window)\n, _widget(widget)\n, _key(memento->key())\n, _migrated(memento->migratedPeerId()\n\t? window->session().data().peer(memento->migratedPeerId()).get()\n\t: nullptr)\n, _section(memento->section()) {\n\tupdateSearchControllers(memento);\n\tsetupMigrationViewer();\n\tsetupTopicViewer();\n}\n\nvoid Controller::replaceKey(Key key) {\n\t_key = key;\n}\n\nvoid Controller::setupMigrationViewer() {\n\tconst auto peer = _key.peer();\n\tif (_key.topic()\n\t\t|| !peer\n\t\t|| (!peer->isChat() && !peer->isChannel())\n\t\t|| _migrated) {\n\t\treturn;\n\t}\n\tpeer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::Migration\n\t) | rpl::filter([=] {\n\t\treturn peer->migrateTo() || (peer->migrateFrom() != _migrated);\n\t}) | rpl::on_next([=] {\n\t\treplaceWith(std::make_shared<Memento>(peer, _section));\n\t}, lifetime());\n}\n\nvoid Controller::replaceWith(std::shared_ptr<Memento> memento) {\n\tconst auto window = parentController();\n\tauto params = Window::SectionShow(\n\t\tWindow::SectionShow::Way::Backward,\n\t\tanim::type::instant,\n\t\tanim::activation::background);\n\tif (wrap() == Wrap::Side) {\n\t\tparams.thirdColumn = true;\n\t}\n\tInvokeQueued(_widget, [=, memento = std::move(memento)]() mutable {\n\t\twindow->showSection(std::move(memento), params);\n\t});\n}\n\nvoid Controller::setupTopicViewer() {\n\tsession().data().itemIdChanged(\n\t) | rpl::on_next([=](const Data::Session::IdChange &change) {\n\t\tif (const auto topic = _key.topic()) {\n\t\t\tif (topic->rootId() == change.oldId\n\t\t\t\t|| (topic->peer()->id == change.newId.peer\n\t\t\t\t\t&& topic->rootId() == change.newId.msg)) {\n\t\t\t\tconst auto now = topic->forum()->topicFor(change.newId.msg);\n\t\t\t\t_key = Key(now);\n\t\t\t\treplaceWith(std::make_shared<Memento>(now, _section));\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n}\n\nWrap Controller::wrap() const {\n\treturn _widget->wrap();\n}\n\nrpl::producer<Wrap> Controller::wrapValue() const {\n\treturn _widget->wrapValue();\n}\n\nnot_null<Ui::RpWidget*> Controller::wrapWidget() const {\n\treturn _widget;\n}\n\nbool Controller::validateMementoPeer(\n\t\tnot_null<ContentMemento*> memento) const {\n\treturn memento->peer() == peer()\n\t\t&& memento->migratedPeerId() == migratedPeerId()\n\t\t&& memento->settingsSelf() == settingsSelf()\n\t\t&& memento->storiesPeer() == storiesPeer()\n\t\t&& memento->musicPeer() == musicPeer()\n\t\t&& memento->statisticsTag().peer == statisticsTag().peer\n\t\t&& memento->starrefPeer() == starrefPeer()\n\t\t&& memento->starrefType() == starrefType();\n}\n\nvoid Controller::setSection(not_null<ContentMemento*> memento) {\n\t_section = memento->section();\n\tupdateSearchControllers(memento);\n}\n\nbool Controller::hasBackButton() const {\n\treturn _widget->hasBackButton();\n}\n\nvoid Controller::updateSearchControllers(\n\t\tnot_null<ContentMemento*> memento) {\n\tusing Type = Section::Type;\n\tconst auto type = _section.type();\n\tconst auto isMedia = (type == Type::Media)\n\t\t|| (type == Type::GlobalMedia);\n\tconst auto mediaType = isMedia\n\t\t? _section.mediaType()\n\t\t: Section::MediaType::kCount;\n\tconst auto hasMediaSearch = isMedia\n\t\t&& SharedMediaAllowSearch(mediaType);\n\tconst auto hasRequestsListSearch = (type == Type::RequestsList);\n\tconst auto hasCommonGroupsSearch = (type == Type::CommonGroups);\n\tconst auto hasDownloadsSearch = (type == Type::Downloads);\n\tconst auto hasMembersSearch = (type == Type::Members)\n\t\t|| (type == Type::Profile);\n\tconst auto searchQuery = memento->searchFieldQuery();\n\tif (type == Type::Media) {\n\t\t_searchController\n\t\t\t= std::make_unique<Api::DelayedSearchController>(&session());\n\t\tif (auto mediaMemento = dynamic_cast<Media::Memento*>(\n\t\t\t\tmemento.get())) {\n\t\t\t_searchController->restoreState(mediaMemento->searchState());\n\t\t} else if (dynamic_cast<Polls::ListMemento*>(memento.get())) {\n\t\t\tauto state = Api::SearchController::SavedState();\n\t\t\tstate.query = produceSearchQuery(searchQuery);\n\t\t\t_searchController->restoreState(std::move(state));\n\t\t}\n\t} else {\n\t\t_searchController = nullptr;\n\t}\n\tif (hasMediaSearch\n\t\t|| hasRequestsListSearch\n\t\t|| hasCommonGroupsSearch\n\t\t|| hasDownloadsSearch\n\t\t|| hasMembersSearch) {\n\t\t_searchFieldController\n\t\t\t= std::make_unique<Ui::SearchFieldController>(\n\t\t\t\tsearchQuery);\n\t\tif (_searchController) {\n\t\t\t_searchFieldController->queryValue(\n\t\t\t) | rpl::on_next([=](QString &&query) {\n\t\t\t\t_searchController->setQuery(\n\t\t\t\t\tproduceSearchQuery(std::move(query)));\n\t\t\t}, _searchFieldController->lifetime());\n\t\t}\n\t\t_seachEnabledByContent = memento->searchEnabledByContent();\n\t\t_searchStartsFocused = memento->searchStartsFocused();\n\t} else {\n\t\t_searchFieldController = nullptr;\n\t}\n}\n\nvoid Controller::saveSearchState(not_null<ContentMemento*> memento) {\n\tif (_searchFieldController) {\n\t\tmemento->setSearchFieldQuery(\n\t\t\t_searchFieldController->query());\n\t\tmemento->setSearchEnabledByContent(\n\t\t\t_seachEnabledByContent.current());\n\t}\n\tif (_searchController) {\n\t\tif (auto mediaMemento = dynamic_cast<Media::Memento*>(\n\t\t\t\tmemento.get())) {\n\t\t\tmediaMemento->setSearchState(_searchController->saveState());\n\t\t}\n\t}\n}\n\nvoid Controller::showSection(\n\t\tstd::shared_ptr<Window::SectionMemento> memento,\n\t\tconst Window::SectionShow &params) {\n\tif (!_widget->showInternal(memento.get(), params)) {\n\t\tAbstractController::showSection(std::move(memento), params);\n\t}\n}\n\nvoid Controller::showBackFromStack(const Window::SectionShow &params) {\n\tif (!_widget->showBackFromStackInternal(params)) {\n\t\tAbstractController::showBackFromStack(params);\n\t}\n}\n\nvoid Controller::removeFromStack(const std::vector<Section> &sections) const {\n\t_widget->removeFromStack(sections);\n}\n\nauto Controller::produceSearchQuery(\n\t\tconst QString &query) const -> SearchQuery {\n\tExpects(_key.peer() != nullptr);\n\n\tauto result = SearchQuery();\n\tresult.type = _section.mediaType();\n\tresult.peerId = _key.peer()->id;\n\tresult.topicRootId = _key.topic() ? _key.topic()->rootId() : 0;\n\tresult.query = query;\n\tresult.migratedPeerId = _migrated ? _migrated->id : PeerId(0);\n\treturn result;\n}\n\nrpl::producer<bool> Controller::searchEnabledByContent() const {\n\treturn _seachEnabledByContent.value();\n}\n\nrpl::producer<QString> Controller::mediaSourceQueryValue() const {\n\treturn _searchController->currentQueryValue();\n}\n\nrpl::producer<QString> Controller::searchQueryValue() const {\n\tconst auto controller = searchFieldController();\n\treturn controller ? controller->queryValue() : rpl::single(QString());\n}\n\nrpl::producer<SparseIdsMergedSlice> Controller::mediaSource(\n\t\tSparseIdsMergedSlice::UniversalMsgId aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) const {\n\tauto query = _searchController->currentQuery();\n\tif (!query.query.isEmpty()) {\n\t\treturn _searchController->idsSlice(\n\t\t\taroundId,\n\t\t\tlimitBefore,\n\t\t\tlimitAfter);\n\t}\n\n\treturn SharedMediaMergedViewer(\n\t\t&session(),\n\t\tSharedMediaMergedKey(\n\t\t\tSparseIdsMergedSlice::Key(\n\t\t\t\tquery.peerId,\n\t\t\t\tquery.topicRootId,\n\t\t\t\tquery.monoforumPeerId,\n\t\t\t\tquery.migratedPeerId,\n\t\t\t\taroundId),\n\t\t\tquery.type),\n\t\tlimitBefore,\n\t\tlimitAfter);\n}\n\nstd::any &Controller::stepDataReference() {\n\treturn _stepData;\n}\n\nvoid Controller::takeStepData(not_null<Controller*> another) {\n\t_stepData = base::take(another->_stepData);\n}\n\nController::~Controller() = default;\n\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/info_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_message_reaction_id.h\"\n#include \"data/data_search_controller.h\"\n#include \"info/peer_gifts/info_peer_gifts_common.h\"\n#include \"info/saved/info_saved_music_common.h\"\n#include \"info/statistics/info_statistics_tag.h\"\n#include \"info/stories/info_stories_common.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace Api {\nstruct WhoReadList;\n} // namespace Api\n\nnamespace Data {\nclass ForumTopic;\nclass SavedSublist;\n} // namespace Data\n\nnamespace Ui {\nclass SearchFieldController;\n} // namespace Ui\n\nnamespace Info::Settings {\n\nstruct Tag {\n\texplicit Tag(not_null<UserData*> self) : self(self) {\n\t}\n\n\tnot_null<UserData*> self;\n};\n\n} // namespace Info::Settings\n\nnamespace Info::Downloads {\n\nstruct Tag {\n};\n\n} // namespace Info::Downloads\n\nnamespace Info::GlobalMedia {\n\nstruct Tag {\n\texplicit Tag(not_null<UserData*> self) : self(self) {\n\t}\n\n\tnot_null<UserData*> self;\n};\n\n} // namespace Info::GlobalMedia\n\nnamespace Info::BotStarRef {\n\nenum class Type : uchar {\n\tSetup,\n\tJoin,\n};\nstruct Tag {\n\tTag(not_null<PeerData*> peer, Type type) : peer(peer), type(type) {\n\t}\n\n\tnot_null<PeerData*> peer;\n\tType type = {};\n};\n\n} // namespace Info::BotStarRef\n\nnamespace Info {\n\nclass Key {\npublic:\n\texplicit Key(not_null<PeerData*> peer);\n\texplicit Key(not_null<Data::ForumTopic*> topic);\n\texplicit Key(not_null<Data::SavedSublist*> sublist);\n\tKey(Settings::Tag settings);\n\tKey(Downloads::Tag downloads);\n\tKey(Stories::Tag stories);\n\tKey(Saved::MusicTag music);\n\tKey(Statistics::Tag statistics);\n\tKey(PeerGifts::Tag gifts);\n\tKey(BotStarRef::Tag starref);\n\tKey(GlobalMedia::Tag global);\n\tKey(not_null<PollData*> poll, FullMsgId contextId);\n\tKey(\n\t\tstd::shared_ptr<Api::WhoReadList> whoReadIds,\n\t\tData::ReactionId selected,\n\t\tFullMsgId contextId);\n\n\t[[nodiscard]] PeerData *peer() const;\n\t[[nodiscard]] Data::ForumTopic *topic() const;\n\t[[nodiscard]] Data::SavedSublist *sublist() const;\n\t[[nodiscard]] UserData *settingsSelf() const;\n\t[[nodiscard]] bool isDownloads() const;\n\t[[nodiscard]] bool isGlobalMedia() const;\n\t[[nodiscard]] PeerData *storiesPeer() const;\n\t[[nodiscard]] int storiesAlbumId() const;\n\t[[nodiscard]] int storiesAddToAlbumId() const;\n\t[[nodiscard]] PeerData *musicPeer() const;\n\t[[nodiscard]] PeerData *giftsPeer() const;\n\t[[nodiscard]] int giftsCollectionId() const;\n\t[[nodiscard]] Statistics::Tag statisticsTag() const;\n\t[[nodiscard]] PeerData *starrefPeer() const;\n\t[[nodiscard]] BotStarRef::Type starrefType() const;\n\t[[nodiscard]] PollData *poll() const;\n\t[[nodiscard]] FullMsgId pollContextId() const;\n\t[[nodiscard]] auto reactionsWhoReadIds() const\n\t\t-> std::shared_ptr<Api::WhoReadList>;\n\t[[nodiscard]] Data::ReactionId reactionsSelected() const;\n\t[[nodiscard]] FullMsgId reactionsContextId() const;\n\nprivate:\n\tstruct PollKey {\n\t\tnot_null<PollData*> poll;\n\t\tFullMsgId contextId;\n\t};\n\tstruct ReactionsKey {\n\t\tstd::shared_ptr<Api::WhoReadList> whoReadIds;\n\t\tData::ReactionId selected;\n\t\tFullMsgId contextId;\n\t};\n\tstd::variant<\n\t\tnot_null<PeerData*>,\n\t\tnot_null<Data::ForumTopic*>,\n\t\tnot_null<Data::SavedSublist*>,\n\t\tSettings::Tag,\n\t\tDownloads::Tag,\n\t\tStories::Tag,\n\t\tSaved::MusicTag,\n\t\tStatistics::Tag,\n\t\tPeerGifts::Tag,\n\t\tBotStarRef::Tag,\n\t\tGlobalMedia::Tag,\n\t\tPollKey,\n\t\tReactionsKey> _value;\n\n};\n\nenum class Wrap;\nclass WrapWidget;\nclass Memento;\nclass ContentMemento;\n\nclass Section final {\npublic:\n\tenum class Type {\n\t\tProfile,\n\t\tMedia,\n\t\tGlobalMedia,\n\t\tCommonGroups,\n\t\tSimilarPeers,\n\t\tRequestsList,\n\t\tReactionsList,\n\t\tSavedSublists,\n\t\tPeerGifts,\n\t\tMembers,\n\t\tSettings,\n\t\tDownloads,\n\t\tStories,\n\t\tSavedMusic,\n\t\tPollResults,\n\t\tStatistics,\n\t\tBotStarRef,\n\t\tBoosts,\n\t\tChannelEarn,\n\t\tBotEarn,\n\t};\n\tusing SettingsType = ::Settings::Type;\n\tusing MediaType = Storage::SharedMediaType;\n\n\tSection(Type type) : _type(type) {\n\t\tExpects(type != Type::Media\n\t\t\t&& type != Type::GlobalMedia\n\t\t\t&& type != Type::Settings);\n\t}\n\tSection(MediaType mediaType, Type type = Type::Media)\n\t: _type(type)\n\t, _mediaType(mediaType) {\n\t}\n\tSection(SettingsType settingsType)\n\t: _type(Type::Settings)\n\t, _settingsType(settingsType) {\n\t}\n\n\t[[nodiscard]] Type type() const {\n\t\treturn _type;\n\t}\n\t[[nodiscard]] MediaType mediaType() const {\n\t\tExpects(_type == Type::Media || _type == Type::GlobalMedia);\n\n\t\treturn _mediaType;\n\t}\n\t[[nodiscard]] SettingsType settingsType() const {\n\t\tExpects(_type == Type::Settings);\n\n\t\treturn _settingsType;\n\t}\n\nprivate:\n\tType _type;\n\tMediaType _mediaType = MediaType();\n\tSettingsType _settingsType = SettingsType();\n\n};\n\nclass AbstractController : public Window::SessionNavigation {\npublic:\n\tAbstractController(not_null<Window::SessionController*> parent);\n\n\t[[nodiscard]] virtual Key key() const = 0;\n\t[[nodiscard]] virtual PeerData *migrated() const = 0;\n\t[[nodiscard]] virtual Section section() const = 0;\n\n\t[[nodiscard]] PeerData *peer() const;\n\t[[nodiscard]] PeerId migratedPeerId() const;\n\t[[nodiscard]] Data::ForumTopic *topic() const {\n\t\treturn key().topic();\n\t}\n\t[[nodiscard]] Data::SavedSublist *sublist() const {\n\t\treturn key().sublist();\n\t}\n\t[[nodiscard]] UserData *settingsSelf() const {\n\t\treturn key().settingsSelf();\n\t}\n\t[[nodiscard]] bool isDownloads() const {\n\t\treturn key().isDownloads();\n\t}\n\t[[nodiscard]] bool isGlobalMedia() const {\n\t\treturn key().isGlobalMedia();\n\t}\n\t[[nodiscard]] PeerData *storiesPeer() const {\n\t\treturn key().storiesPeer();\n\t}\n\t[[nodiscard]] int storiesAlbumId() const {\n\t\treturn key().storiesAlbumId();\n\t}\n\t[[nodiscard]] int storiesAddToAlbumId() const {\n\t\treturn key().storiesAddToAlbumId();\n\t}\n\t[[nodiscard]] PeerData *musicPeer() const {\n\t\treturn key().musicPeer();\n\t}\n\t[[nodiscard]] PeerData *giftsPeer() const {\n\t\treturn key().giftsPeer();\n\t}\n\t[[nodiscard]] int giftsCollectionId() const {\n\t\treturn key().giftsCollectionId();\n\t}\n\t[[nodiscard]] Statistics::Tag statisticsTag() const {\n\t\treturn key().statisticsTag();\n\t}\n\t[[nodiscard]] PeerData *starrefPeer() const {\n\t\treturn key().starrefPeer();\n\t}\n\t[[nodiscard]] BotStarRef::Type starrefType() const {\n\t\treturn key().starrefType();\n\t}\n\t[[nodiscard]] PollData *poll() const;\n\t[[nodiscard]] FullMsgId pollContextId() const {\n\t\treturn key().pollContextId();\n\t}\n\t[[nodiscard]] auto reactionsWhoReadIds() const\n\t\t-> std::shared_ptr<Api::WhoReadList>;\n\t[[nodiscard]] Data::ReactionId reactionsSelected() const;\n\t[[nodiscard]] FullMsgId reactionsContextId() const;\n\n\tvirtual void setSearchEnabledByContent(bool enabled) {\n\t}\n\tvirtual rpl::producer<SparseIdsMergedSlice> mediaSource(\n\t\tSparseIdsMergedSlice::UniversalMsgId aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) const;\n\tvirtual rpl::producer<QString> mediaSourceQueryValue() const;\n\tvirtual rpl::producer<QString> searchQueryValue() const;\n\n\tvoid showSection(\n\t\tstd::shared_ptr<Window::SectionMemento> memento,\n\t\tconst Window::SectionShow &params = Window::SectionShow()) override;\n\tvoid showBackFromStack(\n\t\tconst Window::SectionShow &params = Window::SectionShow()) override;\n\n\tvoid showPeerHistory(\n\t\tPeerId peerId,\n\t\tconst Window::SectionShow &params = Window::SectionShow::Way::ClearStack,\n\t\tMsgId msgId = ShowAtUnreadMsgId) override;\n\n\tnot_null<Window::SessionController*> parentController() override {\n\t\treturn _parent;\n\t}\n\nprivate:\n\tnot_null<Window::SessionController*> _parent;\n\n};\n\nclass Controller : public AbstractController {\npublic:\n\tController(\n\t\tnot_null<WrapWidget*> widget,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<ContentMemento*> memento);\n\n\tKey key() const override {\n\t\treturn _key;\n\t}\n\tPeerData *migrated() const override {\n\t\treturn _migrated;\n\t}\n\tSection section() const override {\n\t\treturn _section;\n\t}\n\n\tvoid replaceKey(Key key);\n\t[[nodiscard]] bool validateMementoPeer(\n\t\tnot_null<ContentMemento*> memento) const;\n\n\t[[nodiscard]] Wrap wrap() const;\n\t[[nodiscard]] rpl::producer<Wrap> wrapValue() const;\n\t[[nodiscard]] not_null<Ui::RpWidget*> wrapWidget() const;\n\tvoid setSection(not_null<ContentMemento*> memento);\n\t[[nodiscard]] bool hasBackButton() const;\n\n\tUi::SearchFieldController *searchFieldController() const {\n\t\treturn _searchFieldController.get();\n\t}\n\tvoid setSearchEnabledByContent(bool enabled) override {\n\t\t_seachEnabledByContent = enabled;\n\t}\n\trpl::producer<bool> searchEnabledByContent() const;\n\trpl::producer<SparseIdsMergedSlice> mediaSource(\n\t\tSparseIdsMergedSlice::UniversalMsgId aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) const override;\n\trpl::producer<QString> mediaSourceQueryValue() const override;\n\trpl::producer<QString> searchQueryValue() const override;\n\tbool takeSearchStartsFocused() {\n\t\treturn base::take(_searchStartsFocused);\n\t}\n\n\tvoid saveSearchState(not_null<ContentMemento*> memento);\n\n\tvoid showSection(\n\t\tstd::shared_ptr<Window::SectionMemento> memento,\n\t\tconst Window::SectionShow &params = Window::SectionShow()) override;\n\tvoid showBackFromStack(\n\t\tconst Window::SectionShow &params = Window::SectionShow()) override;\n\n\tvoid removeFromStack(const std::vector<Section> &sections) const;\n\n\tvoid takeStepData(not_null<Controller*> another);\n\tstd::any &stepDataReference();\n\n\trpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\n\t~Controller();\n\nprivate:\n\tusing SearchQuery = Api::DelayedSearchController::Query;\n\n\tvoid updateSearchControllers(not_null<ContentMemento*> memento);\n\tSearchQuery produceSearchQuery(const QString &query) const;\n\tvoid setupMigrationViewer();\n\tvoid setupTopicViewer();\n\n\tvoid replaceWith(std::shared_ptr<Memento> memento);\n\n\tnot_null<WrapWidget*> _widget;\n\tKey _key;\n\tPeerData *_migrated = nullptr;\n\trpl::variable<Wrap> _wrap;\n\tSection _section;\n\n\tstd::unique_ptr<Ui::SearchFieldController> _searchFieldController;\n\tstd::unique_ptr<Api::DelayedSearchController> _searchController;\n\trpl::variable<bool> _seachEnabledByContent = false;\n\tbool _searchStartsFocused = false;\n\n\t// Data between sections based on steps.\n\tstd::any _stepData;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/info_flexible_scroll.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/info_flexible_scroll.h\"\n\n#include \"ui/effects/animation_value.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"base/event_filter.h\"\n#include \"base/options.h\"\n#include \"styles/style_info.h\"\n\n#include <QtWidgets/QApplication>\n#include <QtWidgets/QScrollBar>\n\nnamespace Info {\n\nbase::options::toggle AlternativeScrollProcessing({\n\t.id = kAlternativeScrollProcessing,\n\t.name = \"Use legacy scroll processing in profiles.\",\n});\n\nconst char kAlternativeScrollProcessing[] = \"alternative-scroll-processing\";\n\nFlexibleScrollHelper::FlexibleScrollHelper(\n\tnot_null<Ui::ScrollArea*> scroll,\n\tnot_null<Ui::RpWidget*> inner,\n\tnot_null<Ui::RpWidget*> pinnedToTop,\n\tFn<void(QMargins)> setPaintPadding,\n\tFn<void(rpl::producer<not_null<QEvent*>>&&)> setViewport,\n\tFlexibleScrollData &data)\n: _scroll(scroll)\n, _inner(inner)\n, _pinnedToTop(pinnedToTop)\n, _setPaintPadding(setPaintPadding)\n, _setViewport(setViewport)\n, _data(data) {\n\tsetupScrollAnimation();\n\tif (AlternativeScrollProcessing.value()) {\n\t\tsetupScrollHandling();\n\t} else {\n\t\tsetupScrollHandlingWithFilter();\n\t}\n}\n\nvoid FlexibleScrollHelper::setupScrollAnimation() {\n\tconstexpr auto kScrollStepTime = crl::time(260);\n\n\tconst auto clearScrollState = [=] {\n\t\t_scrollAnimation.stop();\n\t\t_scrollTopFrom = 0;\n\t\t_scrollTopTo = 0;\n\t\t_timeOffset = 0;\n\t\t_lastScrollApplied = 0;\n\t};\n\n\t_scrollAnimation.init([=](crl::time now) {\n\t\tconst auto progress = float64(now\n\t\t\t- _scrollAnimation.started()\n\t\t\t- _timeOffset) / kScrollStepTime;\n\t\tconst auto eased = anim::easeOutQuint(1.0, progress);\n\t\tconst auto scrollCurrent = anim::interpolate(\n\t\t\t_scrollTopFrom,\n\t\t\t_scrollTopTo,\n\t\t\tstd::clamp(eased, 0., 1.));\n\t\tscrollToY(scrollCurrent);\n\t\t_lastScrollApplied = scrollCurrent;\n\t\tif (progress >= 1) {\n\t\t\tclearScrollState();\n\t\t}\n\t});\n}\n\nvoid FlexibleScrollHelper::setupScrollHandling() {\n\tconst auto heightDiff = [=] {\n\t\treturn _pinnedToTop->maximumHeight()\n\t\t\t- _pinnedToTop->minimumHeight();\n\t};\n\n\trpl::combine(\n\t\t_pinnedToTop->heightValue(),\n\t\t_inner->heightValue()\n\t) | rpl::on_next([=](int, int h) {\n\t\t_data.contentHeightValue.fire(h + heightDiff());\n\t}, _pinnedToTop->lifetime());\n\n\tconst auto singleStep = _scroll->verticalScrollBar()->singleStep()\n\t\t* QApplication::wheelScrollLines();\n\tconst auto step1 = (_pinnedToTop->maximumHeight()\n\t\t\t< st::infoProfileTopBarHeightMax)\n\t\t? (st::infoProfileTopBarStep2 + st::lineWidth)\n\t\t: st::infoProfileTopBarStep1;\n\tconst auto step2 = st::infoProfileTopBarStep2;\n\t// const auto stepDepreciation = singleStep\n\t// \t- st::infoProfileTopBarActionButtonsHeight;\n\t_scrollTopPrevious = _scroll->scrollTop();\n\n\t_scroll->scrollTopValue(\n\t) | rpl::on_next([=](int top) {\n\t\tif (_applyingFakeScrollState) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto diff = top - _scrollTopPrevious;\n\t\tif (std::abs(diff) == singleStep) {\n\t\t\tconst auto previousValue = top - diff;\n\t\t\tconst auto nextStep = (diff > 0)\n\t\t\t\t? ((previousValue == 0)\n\t\t\t\t\t? step1\n\t\t\t\t\t: (previousValue == step1)\n\t\t\t\t\t? step2\n\t\t\t\t\t: -1)\n\t\t\t\t// : ((top < step1\n\t\t\t\t// \t&& (top + stepDepreciation != step1\n\t\t\t\t// \t\t|| _scrollAnimation.animating()))\n\t\t\t\t: ((top < step1)\n\t\t\t\t\t? 0\n\t\t\t\t\t: (top < step2)\n\t\t\t\t\t? step1\n\t\t\t\t\t: -1);\n\t\t\t{\n\t\t\t\t_applyingFakeScrollState = true;\n\t\t\t\tscrollToY(previousValue);\n\t\t\t\t_applyingFakeScrollState = false;\n\t\t\t}\n\t\t\tif (_scrollAnimation.animating()\n\t\t\t\t&& ((_scrollTopTo > _scrollTopFrom) != (diff > 0))) {\n\t\t\t\tauto overriddenDirection = true;\n\t\t\t\tif (_scrollTopTo > _scrollTopFrom) {\n\t\t\t\t\t// From going down to going up.\n\t\t\t\t\tif (_scrollTopTo == step1) {\n\t\t\t\t\t\t_scrollTopTo = 0;\n\t\t\t\t\t} else if (_scrollTopTo == step2) {\n\t\t\t\t\t\t_scrollTopTo = step1;\n\t\t\t\t\t} else {\n\t\t\t\t\t\toverriddenDirection = false;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// From going up to going down.\n\t\t\t\t\tif (_scrollTopTo == 0) {\n\t\t\t\t\t\t_scrollTopTo = step1;\n\t\t\t\t\t} else if (_scrollTopTo == step1) {\n\t\t\t\t\t\t_scrollTopTo = step2;\n\t\t\t\t\t} else {\n\t\t\t\t\t\toverriddenDirection = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (overriddenDirection) {\n\t\t\t\t\t_timeOffset = crl::now() - _scrollAnimation.started();\n\t\t\t\t\t_scrollTopFrom = _lastScrollApplied\n\t\t\t\t\t\t? _lastScrollApplied\n\t\t\t\t\t\t: previousValue;\n\t\t\t\t\treturn;\n\t\t\t\t} else {\n\t\t\t\t\t_scrollAnimation.stop();\n\t\t\t\t\t_scrollTopFrom = 0;\n\t\t\t\t\t_scrollTopTo = 0;\n\t\t\t\t\t_timeOffset = 0;\n\t\t\t\t\t_lastScrollApplied = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t\t_scrollTopFrom = _lastScrollApplied\n\t\t\t\t? _lastScrollApplied\n\t\t\t\t: previousValue;\n\t\t\tif (!_scrollAnimation.animating()) {\n\t\t\t\t_scrollTopTo = ((nextStep != -1) ? nextStep : top);\n\t\t\t\t_scrollAnimation.start();\n\t\t\t} else {\n\t\t\t\tif (_scrollTopTo > _scrollTopFrom) {\n\t\t\t\t\t// Down.\n\t\t\t\t\tif (_scrollTopTo == step1) {\n\t\t\t\t\t\t_scrollTopTo = step2;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t_scrollTopTo += diff;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Up.\n\t\t\t\t\tif (_scrollTopTo == step2) {\n\t\t\t\t\t\t_scrollTopTo = step1;\n\t\t\t\t\t} else if (_scrollTopTo == step1) {\n\t\t\t\t\t\t_scrollTopTo = 0;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t_scrollTopTo += diff;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t_timeOffset = (crl::now() - _scrollAnimation.started());\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t_scrollTopPrevious = top;\n\t\tconst auto current = heightDiff() - top;\n\t\t_inner->moveToLeft(0, std::min(0, current));\n\t\t_pinnedToTop->resize(\n\t\t\t_pinnedToTop->width(),\n\t\t\tstd::max(current + _pinnedToTop->minimumHeight(), 0));\n\t}, _inner->lifetime());\n\n\t_data.fillerWidthValue.events(\n\t) | rpl::on_next([=](int w) {\n\t\t_inner->resizeToWidth(w);\n\t}, _inner->lifetime());\n\n\t_setPaintPadding({ 0, _pinnedToTop->minimumHeight(), 0, 0 });\n\t_setViewport(_pinnedToTop->events(\n\t) | rpl::filter([](not_null<QEvent*> e) {\n\t\treturn e->type() == QEvent::Wheel;\n\t}));\n}\n\nvoid FlexibleScrollHelper::setupScrollHandlingWithFilter() {\n\trpl::combine(\n\t\t_pinnedToTop->heightValue(),\n\t\t_inner->heightValue()\n\t) | rpl::on_next([=](int, int h) {\n\t\tconst auto max = _pinnedToTop->maximumHeight();\n\t\tconst auto min = _pinnedToTop->minimumHeight();\n\t\tconst auto diff = max - min;\n\t\tconst auto progress = (diff > 0)\n\t\t\t? std::clamp(\n\t\t\t\t(_pinnedToTop->height() - min) / float64(diff),\n\t\t\t\t0.,\n\t\t\t\t1.)\n\t\t\t: 1.;\n\t\t_data.contentHeightValue.fire(h\n\t\t\t+ anim::interpolate(diff, 0, progress));\n\t}, _pinnedToTop->lifetime());\n\n\tconst auto singleStep = _scroll->verticalScrollBar()->singleStep()\n\t\t* QApplication::wheelScrollLines();\n\tconst auto step1 = (_pinnedToTop->maximumHeight()\n\t\t\t< st::infoProfileTopBarHeightMax)\n\t\t? (st::infoProfileTopBarStep2 + st::lineWidth)\n\t\t: st::infoProfileTopBarStep1;\n\tconst auto step2 = st::infoProfileTopBarStep2;\n\n\tbase::install_event_filter(_scroll->verticalScrollBar(), [=](\n\t\t\tnot_null<QEvent*> e) {\n\t\tif (e->type() != QEvent::Wheel) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\t\tconst auto wheel = static_cast<QWheelEvent*>(e.get());\n\t\tconst auto delta = wheel->angleDelta().y();\n\t\tif (std::abs(delta) != 120 || (wheel->phase() != Qt::NoScrollPhase)) {\n\t\t\tscrollToY(_scroll->scrollTop() - delta);\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t}\n\t\tconst auto actualTop = _scroll->scrollTop();\n\t\tconst auto animationActive = _scrollAnimation.animating()\n\t\t\t&& (_lastScrollApplied != _scrollTopTo);\n\t\tconst auto top = animationActive\n\t\t\t? (_lastScrollApplied ? _lastScrollApplied : actualTop)\n\t\t\t: actualTop;\n\t\tconst auto diff = (delta > 0) ? -singleStep : singleStep;\n\t\tconst auto previousValue = top;\n\t\tconst auto targetTop = top + diff;\n\t\tconst auto nextStep = (diff > 0)\n\t\t\t? ((previousValue == 0)\n\t\t\t\t? step1\n\t\t\t\t: (previousValue == step1)\n\t\t\t\t? step2\n\t\t\t\t: -1)\n\t\t\t: ((targetTop < step1)\n\t\t\t\t? 0\n\t\t\t\t: (targetTop < step2)\n\t\t\t\t? step1\n\t\t\t\t: -1);\n\t\tif (animationActive\n\t\t\t&& ((_scrollTopTo > _scrollTopFrom) != (diff > 0))) {\n\t\t\tauto overriddenDirection = true;\n\t\t\tif (_scrollTopTo > _scrollTopFrom) {\n\t\t\t\tif (_scrollTopTo == step1) {\n\t\t\t\t\t_scrollTopTo = 0;\n\t\t\t\t} else if (_scrollTopTo == step2) {\n\t\t\t\t\t_scrollTopTo = step1;\n\t\t\t\t} else {\n\t\t\t\t\toverriddenDirection = false;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (_scrollTopTo == 0) {\n\t\t\t\t\t_scrollTopTo = step1;\n\t\t\t\t} else if (_scrollTopTo == step1) {\n\t\t\t\t\t_scrollTopTo = step2;\n\t\t\t\t} else {\n\t\t\t\t\toverriddenDirection = false;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (overriddenDirection) {\n\t\t\t\t_timeOffset = crl::now() - _scrollAnimation.started();\n\t\t\t\t_scrollTopFrom = _lastScrollApplied\n\t\t\t\t\t? _lastScrollApplied\n\t\t\t\t\t: top;\n\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t} else {\n\t\t\t\t_scrollAnimation.stop();\n\t\t\t\t_scrollTopFrom = 0;\n\t\t\t\t_scrollTopTo = 0;\n\t\t\t\t_timeOffset = 0;\n\t\t\t\t_lastScrollApplied = 0;\n\t\t\t}\n\t\t}\n\t\t_scrollTopFrom = top;\n\t\tif (!animationActive) {\n\t\t\t_scrollTopTo = (nextStep != -1) ? nextStep : targetTop;\n\t\t\t_scrollAnimation.start();\n\t\t} else {\n\t\t\tif (_scrollTopTo > _scrollTopFrom) {\n\t\t\t\tif (_scrollTopTo == step1) {\n\t\t\t\t\t_scrollTopTo = step2;\n\t\t\t\t} else {\n\t\t\t\t\t_scrollTopTo += diff;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (_scrollTopTo == step2) {\n\t\t\t\t\t_scrollTopTo = step1;\n\t\t\t\t} else if (_scrollTopTo == step1) {\n\t\t\t\t\t_scrollTopTo = 0;\n\t\t\t\t} else {\n\t\t\t\t\t_scrollTopTo += diff;\n\t\t\t\t}\n\t\t\t}\n\t\t\t_timeOffset = crl::now() - _scrollAnimation.started();\n\t\t}\n\t\treturn base::EventFilterResult::Cancel;\n\t}, _filterLifetime);\n\n\t_scroll->scrollTopValue() | rpl::on_next([=](int top) {\n\t\tapplyScrollToPinnedLayout(top);\n\t}, _inner->lifetime());\n\n\t_data.fillerWidthValue.events(\n\t) | rpl::on_next([=](int w) {\n\t\t_inner->resizeToWidth(w);\n\t}, _inner->lifetime());\n\n\t_setPaintPadding({ 0, _pinnedToTop->minimumHeight(), 0, 0 });\n\t_setViewport(_pinnedToTop->events(\n\t) | rpl::filter([](not_null<QEvent*> e) {\n\t\treturn e->type() == QEvent::Wheel;\n\t}));\n}\n\nvoid FlexibleScrollHelper::scrollToY(int scrollCurrent) {\n\tapplyScrollToPinnedLayout(scrollCurrent);\n\t_scroll->scrollToY(scrollCurrent);\n}\n\nvoid FlexibleScrollHelper::applyScrollToPinnedLayout(int scrollCurrent) {\n\tconst auto top = std::min(scrollCurrent, _scroll->scrollTopMax());\n\tconst auto minimumHeight = _pinnedToTop->minimumHeight();\n\tconst auto current = _pinnedToTop->maximumHeight()\n\t\t- minimumHeight\n\t\t- top;\n\t_inner->moveToLeft(0, std::min(0, current));\n\t_pinnedToTop->resize(\n\t\t_pinnedToTop->width(),\n\t\tstd::max(current + minimumHeight, 0));\n}\n\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/info_flexible_scroll.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Info {\n\nextern const char kAlternativeScrollProcessing[];\n\nstruct FlexibleScrollData {\n\trpl::event_stream<int> contentHeightValue;\n\trpl::event_stream<int> fillerWidthValue;\n\trpl::event_stream<> backButtonEnables;\n};\n\nclass FlexibleScrollHelper final {\npublic:\n\tFlexibleScrollHelper(\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\tnot_null<Ui::RpWidget*> inner,\n\t\tnot_null<Ui::RpWidget*> pinnedToTop,\n\t\tFn<void(QMargins)> setPaintPadding,\n\t\tFn<void(rpl::producer<not_null<QEvent*>>&&)> setViewport,\n\t\tFlexibleScrollData &data);\n\nprivate:\n\tvoid setupScrollAnimation();\n\tvoid setupScrollHandling();\n\tvoid setupScrollHandlingWithFilter();\n\tvoid scrollToY(int value);\n\tvoid applyScrollToPinnedLayout(int scrollCurrent);\n\n\tconst not_null<Ui::ScrollArea*> _scroll;\n\tconst not_null<Ui::RpWidget*> _inner;\n\tconst not_null<Ui::RpWidget*> _pinnedToTop;\n\tconst Fn<void(QMargins)> _setPaintPadding;\n\tconst Fn<void(rpl::producer<not_null<QEvent*>>&&)> _setViewport;\n\tFlexibleScrollData &_data;\n\n\tUi::Animations::Basic _scrollAnimation;\n\tint _scrollTopFrom = 0;\n\tint _scrollTopTo = 0;\n\tcrl::time _timeOffset = 0;\n\tint _lastScrollApplied = 0;\n\tint _scrollTopPrevious = 0;\n\tbool _applyingFakeScrollState = false;\n\trpl::lifetime _filterLifetime;\n};\n\n} // namespace Info"
  },
  {
    "path": "Telegram/SourceFiles/info/info_layer_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/info_layer_widget.h\"\n\n#include \"info/info_content_widget.h\"\n#include \"info/info_top_bar.h\"\n#include \"info/info_memento.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/focus_persister.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/main_window.h\"\n#include \"main/main_session.h\"\n#include \"core/application.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Info {\n\nLayerWidget::LayerWidget(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Memento*> memento)\n: _controller(controller)\n, _contentWrap(this, controller, Wrap::Layer, memento) {\n\tsetupHeightConsumers();\n\tcontroller->window().replaceFloatPlayerDelegate(floatPlayerDelegate());\n}\n\nLayerWidget::LayerWidget(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<MoveMemento*> memento)\n: _controller(controller)\n, _contentWrap(memento->takeContent(this, Wrap::Layer)) {\n\tsetupHeightConsumers();\n\tcontroller->window().replaceFloatPlayerDelegate(floatPlayerDelegate());\n}\n\nauto LayerWidget::floatPlayerDelegate()\n-> not_null<::Media::Player::FloatDelegate*> {\n\treturn static_cast<::Media::Player::FloatDelegate*>(this);\n}\n\nnot_null<Ui::RpWidget*> LayerWidget::floatPlayerWidget() {\n\treturn this;\n}\n\nvoid LayerWidget::floatPlayerToggleGifsPaused(bool paused) {\n\tconstexpr auto kReason = Window::GifPauseReason::RoundPlaying;\n\tif (paused) {\n\t\t_controller->enableGifPauseReason(kReason);\n\t} else {\n\t\t_controller->disableGifPauseReason(kReason);\n\t}\n}\n\nauto LayerWidget::floatPlayerGetSection(Window::Column column)\n-> not_null<::Media::Player::FloatSectionDelegate*> {\n\tExpects(_contentWrap != nullptr);\n\n\treturn _contentWrap;\n}\n\nvoid LayerWidget::floatPlayerEnumerateSections(Fn<void(\n\t\tnot_null<::Media::Player::FloatSectionDelegate*> widget,\n\t\tWindow::Column widgetColumn)> callback) {\n\tExpects(_contentWrap != nullptr);\n\n\tcallback(_contentWrap, Window::Column::Second);\n}\n\nbool LayerWidget::floatPlayerIsVisible(not_null<HistoryItem*> item) {\n\treturn false;\n}\n\nvoid LayerWidget::floatPlayerDoubleClickEvent(\n\t\tnot_null<const HistoryItem*> item) {\n\t_controller->showMessage(item);\n}\n\nvoid LayerWidget::setupHeightConsumers() {\n\tExpects(_contentWrap != nullptr);\n\n\t_contentWrap->scrollTillBottomChanges(\n\t) | rpl::filter([this] {\n\t\tif (!_inResize) {\n\t\t\treturn true;\n\t\t}\n\t\t_pendingResize = true;\n\t\treturn false;\n\t}) | rpl::on_next([this] {\n\t\tresizeToWidth(width());\n\t}, lifetime());\n\n\t_contentWrap->grabbingForExpanding(\n\t) | rpl::on_next([=](bool grabbing) {\n\t\tif (grabbing) {\n\t\t\t_savedHeight = _contentWrapHeight;\n\t\t\t_savedHeightAnimation = base::take(_heightAnimation);\n\t\t\tsetContentHeight(_desiredHeight);\n\t\t} else {\n\t\t\t_heightAnimation = base::take(_savedHeightAnimation);\n\t\t\tsetContentHeight(_savedHeight);\n\t\t}\n\t}, lifetime());\n\n\t_contentWrap->desiredHeightValue(\n\t) | rpl::on_next([this](int height) {\n\t\tif (!height) {\n\t\t\t// New content arrived.\n\t\t\t_heightAnimated = _heightAnimation.animating();\n\t\t\treturn;\n\t\t}\n\t\tstd::swap(_desiredHeight, height);\n\t\tif (!height\n\t\t\t|| (_heightAnimated && !_heightAnimation.animating())) {\n\t\t\t_heightAnimated = true;\n\t\t\tsetContentHeight(_desiredHeight);\n\t\t} else {\n\t\t\t_heightAnimated = true;\n\t\t\t_heightAnimation.start([=] {\n\t\t\t\tsetContentHeight(_heightAnimation.value(_desiredHeight));\n\t\t\t}, _contentWrapHeight, _desiredHeight, st::slideDuration);\n\t\t\tresizeToWidth(width());\n\t\t}\n\t}, lifetime());\n}\n\nvoid LayerWidget::setContentHeight(int height) {\n\tif (_contentWrapHeight == height) {\n\t\treturn;\n\t}\n\t_contentWrapHeight = height;\n\tif (_inResize) {\n\t\t_pendingResize = true;\n\t} else if (_contentWrap) {\n\t\tresizeToWidth(width());\n\t}\n}\n\nvoid LayerWidget::showFinished() {\n\tfloatPlayerShowVisible();\n\t_contentWrap->showFast();\n}\n\nvoid LayerWidget::parentResized() {\n\tif (!_contentWrap) {\n\t\treturn;\n\t}\n\n\tauto parentSize = parentWidget()->size();\n\tauto parentWidth = parentSize.width();\n\tif (parentWidth < MinimalSupportedWidth()) {\n\t\tUi::FocusPersister persister(this);\n\t\trestoreFloatPlayerDelegate();\n\n\t\tauto memento = std::make_shared<MoveMemento>(std::move(_contentWrap));\n\n\t\t// We want to call hideSpecialLayer synchronously to avoid glitches,\n\t\t// but we can't destroy LayerStackWidget from its' resizeEvent,\n\t\t// because QWidget has such code for resizing:\n\t\t//\n\t\t// QResizeEvent e(r.size(), olds);\n\t\t// QApplication::sendEvent(q, &e);\n\t\t// if (q->windowHandle())\n\t\t//   q->update();\n\t\t//\n\t\t// So we call it queued. It would be cool to call it 'right after'\n\t\t// the resize event handling was finished.\n\t\tInvokeQueued(this, [=] {\n\t\t\t_controller->hideSpecialLayer(anim::type::instant);\n\t\t});\n\t\t_controller->showSection(\n\t\t\tstd::move(memento),\n\t\t\tWindow::SectionShow(\n\t\t\t\tWindow::SectionShow::Way::Forward,\n\t\t\t\tanim::type::instant,\n\t\t\t\tanim::activation::background));\n\t//\n\t// There was a layout logic which caused layer info to become a\n\t// third column info if the window size allows, but it was decided\n\t// to keep layer info and third column info separated.\n\t//\n\t//} else if (_controller->canShowThirdSectionWithoutResize()) {\n\t//\ttakeToThirdSection();\n\t} else {\n\t\tauto newWidth = qMin(\n\t\t\tparentWidth - 2 * st::infoMinimalLayerMargin,\n\t\t\tst::infoDesiredWidth);\n\t\tresizeToWidth(newWidth);\n\t}\n}\n\nbool LayerWidget::takeToThirdSection() {\n\treturn false;\n\t//\n\t// There was a layout logic which caused layer info to become a\n\t// third column info if the window size allows, but it was decided\n\t// to keep layer info and third column info separated.\n\t//\n\t//Ui::FocusPersister persister(this);\n\t//auto localCopy = _controller;\n\t//auto memento = MoveMemento(std::move(_contentWrap));\n\t//localCopy->hideSpecialLayer(anim::type::instant);\n\n\t//// When creating third section in response to the window\n\t//// size allowing it to fit without window resize we want\n\t//// to save that we didn't extend the window while showing\n\t//// the third section, so that when we close it we won't\n\t//// shrink the window size.\n\t////\n\t//// See https://github.com/telegramdesktop/tdesktop/issues/4091\n\t//localCopy->session()().settings().setThirdSectionExtendedBy(0);\n\n\t//localCopy->session()().settings().setThirdSectionInfoEnabled(true);\n\t//localCopy->session()().saveSettingsDelayed();\n\t//localCopy->showSection(\n\t//\tstd::move(memento),\n\t//\tWindow::SectionShow(\n\t//\t\tWindow::SectionShow::Way::ClearStack,\n\t//\t\tanim::type::instant,\n\t//\t\tanim::activation::background));\n\t//return true;\n}\n\nbool LayerWidget::showSectionInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) {\n\tif (_contentWrap && _contentWrap->showInternal(memento, params)) {\n\t\tif (params.activation != anim::activation::background) {\n\t\t\t_controller->parentController()->hideLayer();\n\t\t}\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool LayerWidget::closeByOutsideClick() const {\n\treturn _contentWrap ? _contentWrap->closeByOutsideClick() : true;\n}\n\nint LayerWidget::MinimalSupportedWidth() {\n\tconst auto minimalMargins = 2 * st::infoMinimalLayerMargin;\n\treturn st::infoMinimalWidth + minimalMargins;\n}\n\nint LayerWidget::resizeGetHeight(int newWidth) {\n\tif (!parentWidget() || !_contentWrap || !newWidth) {\n\t\treturn 0;\n\t}\n\tconstexpr auto kMaxAttempts = 16;\n\tauto attempts = 0;\n\twhile (true) {\n\t\t_inResize = true;\n\t\tconst auto newGeometry = countGeometry(newWidth);\n\t\t_inResize = false;\n\t\tif (!_pendingResize) {\n\t\t\tconst auto oldGeometry = geometry();\n\t\t\tif (newGeometry != oldGeometry) {\n\t\t\t\t_contentWrap->forceContentRepaint();\n\t\t\t}\n\t\t\tif (newGeometry.topLeft() != oldGeometry.topLeft()) {\n\t\t\t\tmove(newGeometry.topLeft());\n\t\t\t}\n\t\t\tfloatPlayerUpdatePositions();\n\t\t\treturn newGeometry.height();\n\t\t}\n\t\t_pendingResize = false;\n\t\tAssert(attempts++ < kMaxAttempts);\n\t}\n}\n\nQRect LayerWidget::countGeometry(int newWidth) {\n\tconst auto &parentSize = parentWidget()->size();\n\tconst auto windowWidth = parentSize.width();\n\tconst auto windowHeight = parentSize.height();\n\tconst auto newLeft = (windowWidth - newWidth) / 2;\n\tconst auto newTop = std::clamp(\n\t\twindowHeight / 24,\n\t\tst::infoLayerTopMinimal,\n\t\tst::infoLayerTopMaximal);\n\tconst auto newBottom = newTop;\n\n\tconst auto bottomRadius = st::boxRadius;\n\tconst auto maxVisibleHeight = windowHeight - newTop;\n\t// Top rounding is included in _contentWrapHeight.\n\tauto desiredHeight = _contentWrapHeight + bottomRadius;\n\taccumulate_min(desiredHeight, maxVisibleHeight - newBottom);\n\n\t// First resize content to new width and get the new desired height.\n\tconst auto contentLeft = 0;\n\tconst auto contentTop = 0;\n\tconst auto contentBottom = bottomRadius;\n\tconst auto contentWidth = newWidth;\n\tauto contentHeight = desiredHeight - contentTop - contentBottom;\n\tconst auto scrollTillBottom = _contentWrap->scrollTillBottom(\n\t\tcontentHeight);\n\tauto additionalScroll = std::min(scrollTillBottom, newBottom);\n\n\tconst auto expanding = (_desiredHeight > _contentWrapHeight);\n\n\tdesiredHeight += additionalScroll;\n\tcontentHeight += additionalScroll;\n\t_tillBottom = (desiredHeight >= maxVisibleHeight);\n\tif (_tillBottom) {\n\t\tadditionalScroll += contentBottom;\n\t}\n\t_contentTillBottom = _tillBottom && !_contentWrap->scrollBottomSkip();\n\tif (_contentTillBottom) {\n\t\tcontentHeight += contentBottom;\n\t}\n\t_contentWrap->updateGeometry({\n\t\tcontentLeft,\n\t\tcontentTop,\n\t\tcontentWidth,\n\t\tcontentHeight,\n\t}, expanding, additionalScroll, maxVisibleHeight);\n\n\treturn QRect(newLeft, newTop, newWidth, desiredHeight);\n}\n\nvoid LayerWidget::doSetInnerFocus() {\n\tif (_contentWrap) {\n\t\t_contentWrap->setInnerFocus();\n\t}\n}\n\nvoid LayerWidget::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto clip = e->rect();\n\tconst auto radius = st::boxRadius;\n\tconst auto &corners = Ui::CachedCornerPixmaps(Ui::BoxCorners);\n\tif (!_tillBottom) {\n\t\tconst auto bottom = QRect{ 0, height() - radius, width(), radius };\n\t\tif (clip.intersects(bottom)) {\n\t\t\tif (const auto rounding = _contentWrap->bottomSkipRounding()) {\n\t\t\t\trounding->paint(p, rect(), RectPart::FullBottom);\n\t\t\t} else {\n\t\t\t\tUi::FillRoundRect(p, bottom, st::boxBg, {\n\t\t\t\t\t.p = { QPixmap(), QPixmap(), corners.p[2], corners.p[3] }\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t} else if (!_contentTillBottom) {\n\t\tconst auto rounding = _contentWrap->bottomSkipRounding();\n\t\tconst auto &color = rounding ? rounding->color() : st::boxBg;\n\t\tp.fillRect(0, height() - radius, width(), radius, color);\n\t}\n\tif (_contentWrap->animatingShow()) {\n\t\tconst auto top = QRect{ 0, 0, width(), radius };\n\t\tif (clip.intersects(top)) {\n\t\t\tUi::FillRoundRect(p, top, st::boxBg, {\n\t\t\t\t.p = { corners.p[0], corners.p[1], QPixmap(), QPixmap() }\n\t\t\t});\n\t\t}\n\t\tp.fillRect(0, radius, width(), height() - 2 * radius, st::boxBg);\n\t}\n}\n\nvoid LayerWidget::restoreFloatPlayerDelegate() {\n\tif (!_floatPlayerDelegateRestored) {\n\t\t_floatPlayerDelegateRestored = true;\n\t\t_controller->window().restoreFloatPlayerDelegate(\n\t\t\tfloatPlayerDelegate());\n\t}\n}\n\nvoid LayerWidget::closeHook() {\n\trestoreFloatPlayerDelegate();\n}\n\nLayerWidget::~LayerWidget() {\n\tif (!Core::Quitting()) {\n\t\trestoreFloatPlayerDelegate();\n\t}\n}\n\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/info_layer_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/layer_widget.h\"\n#include \"media/player/media_player_float.h\"\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Info {\n\nclass Memento;\nclass MoveMemento;\nclass WrapWidget;\nclass TopBar;\n\nclass LayerWidget\n\t: public Ui::LayerWidget\n\t, private ::Media::Player::FloatDelegate {\npublic:\n\tLayerWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Memento*> memento);\n\tLayerWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<MoveMemento*> memento);\n\n\tvoid showFinished() override;\n\tvoid parentResized() override;\n\n\tbool takeToThirdSection() override;\n\tbool showSectionInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) override;\n\n\tbool closeByOutsideClick() const override;\n\n\tstatic int MinimalSupportedWidth();\n\n\t~LayerWidget();\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid doSetInnerFocus() override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid closeHook() override;\n\n\tvoid restoreFloatPlayerDelegate();\n\tnot_null<::Media::Player::FloatDelegate*> floatPlayerDelegate();\n\tnot_null<Ui::RpWidget*> floatPlayerWidget() override;\n\tvoid floatPlayerToggleGifsPaused(bool paused) override;\n\tnot_null<::Media::Player::FloatSectionDelegate*> floatPlayerGetSection(\n\t\tWindow::Column column) override;\n\tvoid floatPlayerEnumerateSections(Fn<void(\n\t\tnot_null<::Media::Player::FloatSectionDelegate*> widget,\n\t\tWindow::Column widgetColumn)> callback) override;\n\tbool floatPlayerIsVisible(not_null<HistoryItem*> item) override;\n\tvoid floatPlayerDoubleClickEvent(\n\t\tnot_null<const HistoryItem*> item) override;\n\n\tvoid setupHeightConsumers();\n\tvoid setContentHeight(int height);\n\t[[nodiscard]] QRect countGeometry(int newWidth);\n\n\tnot_null<Window::SessionController*> _controller;\n\tobject_ptr<WrapWidget> _contentWrap;\n\n\tint _desiredHeight = 0;\n\tint _contentWrapHeight = 0;\n\tint _savedHeight = 0;\n\tUi::Animations::Simple _heightAnimation;\n\tUi::Animations::Simple _savedHeightAnimation;\n\tbool _heightAnimated = false;\n\tbool _inResize = false;\n\tbool _pendingResize = false;\n\tbool _tillBottom = false;\n\tbool _contentTillBottom = false;\n\n\tbool _floatPlayerDelegateRestored = false;\n\n};\n\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/info_memento.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/info_memento.h\"\n\n#include \"info/global_media/info_global_media_widget.h\"\n#include \"info/profile/info_profile_widget.h\"\n#include \"info/media/info_media_widget.h\"\n#include \"info/members/info_members_widget.h\"\n#include \"info/common_groups/info_common_groups_widget.h\"\n#include \"info/saved/info_saved_sublists_widget.h\"\n#include \"info/settings/info_settings_widget.h\"\n#include \"info/similar_peers/info_similar_peers_widget.h\"\n#include \"info/reactions_list/info_reactions_list_widget.h\"\n#include \"info/requests_list/info_requests_list_widget.h\"\n#include \"info/peer_gifts/info_peer_gifts_widget.h\"\n#include \"info/polls/info_polls_list_widget.h\"\n#include \"info/polls/info_polls_results_widget.h\"\n#include \"info/info_section_widget.h\"\n#include \"info/info_layer_widget.h\"\n#include \"info/info_controller.h\"\n#include \"ui/ui_utility.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n\nnamespace Info {\n\nMemento::Memento(not_null<PeerData*> peer)\n: Memento(peer, Section::Type::Profile) {\n}\n\nMemento::Memento(not_null<PeerData*> peer, Section section)\n: Memento(DefaultStack(peer, section)) {\n}\n\nMemento::Memento(not_null<Data::ForumTopic*> topic)\n: Memento(topic, Section::Type::Profile) {\n}\n\nMemento::Memento(not_null<Data::ForumTopic*> topic, Section section)\n: Memento(DefaultStack(topic, section)) {\n}\n\nMemento::Memento(not_null<Data::SavedSublist*> sublist)\n: Memento(sublist, Section::Type::Profile) {\n}\n\nMemento::Memento(not_null<Data::SavedSublist*> sublist, Section section)\n: Memento(DefaultStack(sublist, section)) {\n}\n\nMemento::Memento(Settings::Tag settings, Section section)\n: Memento(DefaultStack(settings, section)) {\n}\n\nMemento::Memento(not_null<PollData*> poll, FullMsgId contextId)\n: Memento(DefaultStack(poll, contextId)) {\n}\n\nMemento::Memento(\n\tstd::shared_ptr<Api::WhoReadList> whoReadIds,\n\tFullMsgId contextId,\n\tData::ReactionId selected)\n: Memento(DefaultStack(std::move(whoReadIds), contextId, selected)) {\n}\n\nMemento::Memento(std::vector<std::shared_ptr<ContentMemento>> stack)\n: _stack(std::move(stack)) {\n\tauto topics = base::flat_set<not_null<Data::ForumTopic*>>();\n\tauto sublists = base::flat_set<not_null<Data::SavedSublist*>>();\n\tfor (auto &entry : _stack) {\n\t\tif (const auto topic = entry->topic()) {\n\t\t\ttopics.emplace(topic);\n\t\t} else if (const auto sublist = entry->sublist()) {\n\t\t\tsublists.emplace(sublist);\n\t\t}\n\t}\n\tfor (const auto &topic : topics) {\n\t\ttopic->destroyed(\n\t\t) | rpl::on_next([=] {\n\t\t\tfor (auto i = begin(_stack); i != end(_stack);) {\n\t\t\t\tif (i->get()->topic() == topic) {\n\t\t\t\t\ti = _stack.erase(i);\n\t\t\t\t} else {\n\t\t\t\t\t++i;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_stack.empty()) {\n\t\t\t\t_removeRequests.fire({});\n\t\t\t}\n\t\t}, _lifetime);\n\t}\n\tfor (const auto &sublist : sublists) {\n\t\tsublist->destroyed(\n\t\t) | rpl::on_next([=] {\n\t\t\tfor (auto i = begin(_stack); i != end(_stack);) {\n\t\t\t\tif (i->get()->sublist() == sublist) {\n\t\t\t\t\ti = _stack.erase(i);\n\t\t\t\t} else {\n\t\t\t\t\t++i;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_stack.empty()) {\n\t\t\t\t_removeRequests.fire({});\n\t\t\t}\n\t\t}, _lifetime);\n\t}\n}\n\nstd::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack(\n\t\tnot_null<PeerData*> peer,\n\t\tSection section) {\n\tauto result = std::vector<std::shared_ptr<ContentMemento>>();\n\tresult.push_back(DefaultContent(peer, section));\n\treturn result;\n}\n\nstd::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack(\n\t\tnot_null<Data::ForumTopic*> topic,\n\t\tSection section) {\n\tauto result = std::vector<std::shared_ptr<ContentMemento>>();\n\tresult.push_back(DefaultContent(topic, section));\n\treturn result;\n}\n\nstd::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack(\n\t\tnot_null<Data::SavedSublist*> sublist,\n\t\tSection section) {\n\tauto result = std::vector<std::shared_ptr<ContentMemento>>();\n\tresult.push_back(DefaultContent(sublist, section));\n\treturn result;\n}\n\nstd::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack(\n\t\tSettings::Tag settings,\n\t\tSection section) {\n\tauto result = std::vector<std::shared_ptr<ContentMemento>>();\n\tresult.push_back(std::make_shared<Settings::Memento>(\n\t\tsettings.self,\n\t\tsection.settingsType()));\n\treturn result;\n}\n\nstd::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId contextId) {\n\tauto result = std::vector<std::shared_ptr<ContentMemento>>();\n\tresult.push_back(std::make_shared<Polls::Memento>(poll, contextId));\n\treturn result;\n}\n\nstd::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack(\n\t\tstd::shared_ptr<Api::WhoReadList> whoReadIds,\n\t\tFullMsgId contextId,\n\t\tData::ReactionId selected) {\n\tauto result = std::vector<std::shared_ptr<ContentMemento>>();\n\tresult.push_back(std::make_shared<ReactionsList::Memento>(\n\t\tstd::move(whoReadIds),\n\t\tcontextId,\n\t\tselected));\n\treturn result;\n}\n\nSection Memento::DefaultSection(not_null<PeerData*> peer) {\n\tif (peer->savedSublistsInfo()) {\n\t\treturn Section(Section::Type::SavedSublists);\n\t} else if (peer->sharedMediaInfo()) {\n\t\treturn Section(Section::MediaType::Photo);\n\t}\n\treturn Section(Section::Type::Profile);\n}\n\nstd::shared_ptr<Memento> Memento::Default(not_null<PeerData*> peer) {\n\treturn std::make_shared<Memento>(peer, DefaultSection(peer));\n}\n\nstd::shared_ptr<ContentMemento> Memento::DefaultContent(\n\t\tnot_null<PeerData*> peer,\n\t\tSection section) {\n\tif (auto to = peer->migrateTo()) {\n\t\tpeer = to;\n\t}\n\tauto migrated = peer->migrateFrom();\n\tauto migratedPeerId = migrated ? migrated->id : PeerId(0);\n\n\tswitch (section.type()) {\n\tcase Section::Type::Profile:\n\t\treturn std::make_shared<Profile::Memento>(\n\t\t\tpeer,\n\t\t\tmigratedPeerId);\n\tcase Section::Type::Media:\n\t\tif (section.mediaType() == Storage::SharedMediaType::Poll) {\n\t\t\treturn std::make_shared<Polls::ListMemento>(\n\t\t\t\tpeer,\n\t\t\t\tmigratedPeerId);\n\t\t}\n\t\treturn std::make_shared<Media::Memento>(\n\t\t\tpeer,\n\t\t\tmigratedPeerId,\n\t\t\tsection.mediaType());\n\tcase Section::Type::GlobalMedia:\n\t\treturn std::make_shared<GlobalMedia::Memento>(\n\t\t\tpeer->asUser(),\n\t\t\tsection.mediaType());\n\tcase Section::Type::CommonGroups:\n\t\treturn std::make_shared<CommonGroups::Memento>(peer->asUser());\n\tcase Section::Type::SimilarPeers:\n\t\treturn std::make_shared<SimilarPeers::Memento>(peer);\n\tcase Section::Type::RequestsList:\n\t\treturn std::make_shared<RequestsList::Memento>(peer);\n\tcase Section::Type::SavedSublists:\n\t\treturn std::make_shared<Saved::SublistsMemento>(&peer->session());\n\tcase Section::Type::Members:\n\t\treturn std::make_shared<Members::Memento>(\n\t\t\tpeer,\n\t\t\tmigratedPeerId);\n\t}\n\tUnexpected(\"Wrong section type in Info::Memento::DefaultContent()\");\n}\n\nstd::shared_ptr<ContentMemento> Memento::DefaultContent(\n\t\tnot_null<Data::ForumTopic*> topic,\n\t\tSection section) {\n\tconst auto peer = topic->peer();\n\tconst auto migrated = peer->migrateFrom();\n\tconst auto migratedPeerId = migrated ? migrated->id : PeerId(0);\n\tswitch (section.type()) {\n\tcase Section::Type::Profile:\n\t\treturn std::make_shared<Profile::Memento>(topic);\n\tcase Section::Type::Media:\n\t\tif (section.mediaType() == Storage::SharedMediaType::Poll) {\n\t\t\treturn std::make_shared<Polls::ListMemento>(topic);\n\t\t}\n\t\treturn std::make_shared<Media::Memento>(topic, section.mediaType());\n\tcase Section::Type::Members:\n\t\treturn std::make_shared<Members::Memento>(peer, migratedPeerId);\n\t}\n\tUnexpected(\"Wrong section type in Info::Memento::DefaultContent()\");\n}\n\nstd::shared_ptr<ContentMemento> Memento::DefaultContent(\n\t\tnot_null<Data::SavedSublist*> sublist,\n\t\tSection section) {\n\tswitch (section.type()) {\n\tcase Section::Type::Profile:\n\t\treturn std::make_shared<Profile::Memento>(sublist);\n\tcase Section::Type::Media:\n\t\tif (section.mediaType() == Storage::SharedMediaType::Poll) {\n\t\t\treturn std::make_shared<Polls::ListMemento>(sublist);\n\t\t}\n\t\treturn std::make_shared<Media::Memento>(\n\t\t\tsublist,\n\t\t\tsection.mediaType());\n\t}\n\tUnexpected(\"Wrong section type in Info::Memento::DefaultContent()\");\n}\n\nobject_ptr<Window::SectionWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tWindow::Column column,\n\t\tconst QRect &geometry) {\n\tauto wrap = (column == Window::Column::Third)\n\t\t? Wrap::Side\n\t\t: Wrap::Narrow;\n\tauto result = object_ptr<SectionWidget>(\n\t\tparent,\n\t\tcontroller,\n\t\twrap,\n\t\tthis);\n\tresult->setGeometry(geometry);\n\treturn result;\n}\n\nobject_ptr<Ui::LayerWidget> Memento::createLayer(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QRect &geometry) {\n\tif (geometry.width() >= LayerWidget::MinimalSupportedWidth()) {\n\t\treturn object_ptr<LayerWidget>(controller, this);\n\t}\n\treturn nullptr;\n}\n\nstd::vector<std::shared_ptr<ContentMemento>> Memento::takeStack() {\n\treturn std::move(_stack);\n}\n\nMemento::~Memento() = default;\n\nMoveMemento::MoveMemento(object_ptr<WrapWidget> content)\n: _content(std::move(content)) {\n\t_content->hide();\n\t_content->setParent(nullptr);\n}\n\nobject_ptr<Window::SectionWidget> MoveMemento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tWindow::Column column,\n\t\tconst QRect &geometry) {\n\tauto wrap = (column == Window::Column::Third)\n\t\t? Wrap::Side\n\t\t: Wrap::Narrow;\n\tauto result = object_ptr<SectionWidget>(\n\t\tparent,\n\t\tcontroller,\n\t\twrap,\n\t\tthis);\n\tresult->setGeometry(geometry);\n\treturn result;\n}\n\nobject_ptr<Ui::LayerWidget> MoveMemento::createLayer(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QRect &geometry) {\n\tif (geometry.width() < LayerWidget::MinimalSupportedWidth()) {\n\t\treturn nullptr;\n\t}\n\treturn object_ptr<LayerWidget>(controller, this);\n}\n\nobject_ptr<WrapWidget> MoveMemento::takeContent(\n\t\tQWidget *parent,\n\t\tWrap wrap) {\n\tUi::AttachParentChild(parent, _content);\n\t_content->setWrap(wrap);\n\treturn std::move(_content);\n}\n\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/info_memento.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"info/info_wrap_widget.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"window/section_memento.h\"\n#include \"base/object_ptr.h\"\n\nnamespace Api {\nstruct WhoReadList;\n} // namespace Api\n\nnamespace Storage {\nenum class SharedMediaType : signed char;\n} // namespace Storage\n\nnamespace Data {\nclass ForumTopic;\nclass SavedSublist;\nstruct ReactionId;\n} // namespace Data\n\nnamespace Ui {\nclass ScrollArea;\nstruct ScrollToRequest;\n} // namespace Ui\n\nnamespace Info {\nnamespace Settings {\nstruct Tag;\n} // namespace Settings\n\nnamespace Downloads {\nstruct Tag;\n} // namespace Downloads\n\nclass ContentMemento;\nclass WrapWidget;\n\nclass Memento final : public Window::SectionMemento {\npublic:\n\texplicit Memento(not_null<PeerData*> peer);\n\tMemento(not_null<PeerData*> peer, Section section);\n\texplicit Memento(not_null<Data::ForumTopic*> topic);\n\tMemento(not_null<Data::ForumTopic*> topic, Section section);\n\texplicit Memento(not_null<Data::SavedSublist*> sublist);\n\tMemento(not_null<Data::SavedSublist*> sublist, Section section);\n\tMemento(Settings::Tag settings, Section section);\n\tMemento(not_null<PollData*> poll, FullMsgId contextId);\n\tMemento(\n\t\tstd::shared_ptr<Api::WhoReadList> whoReadIds,\n\t\tFullMsgId contextId,\n\t\tData::ReactionId selected);\n\texplicit Memento(std::vector<std::shared_ptr<ContentMemento>> stack);\n\n\tobject_ptr<Window::SectionWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tWindow::Column column,\n\t\tconst QRect &geometry) override;\n\n\tobject_ptr<Ui::LayerWidget> createLayer(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QRect &geometry) override;\n\n\trpl::producer<> removeRequests() const override {\n\t\treturn _removeRequests.events();\n\t}\n\n\tint stackSize() const {\n\t\treturn int(_stack.size());\n\t}\n\tstd::vector<std::shared_ptr<ContentMemento>> takeStack();\n\n\tnot_null<ContentMemento*> content() {\n\t\tExpects(!_stack.empty());\n\n\t\treturn _stack.back().get();\n\t}\n\n\tstatic Section DefaultSection(not_null<PeerData*> peer);\n\tstatic std::shared_ptr<Memento> Default(not_null<PeerData*> peer);\n\n\t~Memento();\n\nprivate:\n\tstatic std::vector<std::shared_ptr<ContentMemento>> DefaultStack(\n\t\tnot_null<PeerData*> peer,\n\t\tSection section);\n\tstatic std::vector<std::shared_ptr<ContentMemento>> DefaultStack(\n\t\tnot_null<Data::ForumTopic*> topic,\n\t\tSection section);\n\tstatic std::vector<std::shared_ptr<ContentMemento>> DefaultStack(\n\t\tnot_null<Data::SavedSublist*> sublist,\n\t\tSection section);\n\tstatic std::vector<std::shared_ptr<ContentMemento>> DefaultStack(\n\t\tSettings::Tag settings,\n\t\tSection section);\n\tstatic std::vector<std::shared_ptr<ContentMemento>> DefaultStack(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId contextId);\n\tstatic std::vector<std::shared_ptr<ContentMemento>> DefaultStack(\n\t\tstd::shared_ptr<Api::WhoReadList> whoReadIds,\n\t\tFullMsgId contextId,\n\t\tData::ReactionId selected);\n\n\tstatic std::shared_ptr<ContentMemento> DefaultContent(\n\t\tnot_null<PeerData*> peer,\n\t\tSection section);\n\tstatic std::shared_ptr<ContentMemento> DefaultContent(\n\t\tnot_null<Data::ForumTopic*> topic,\n\t\tSection section);\n\tstatic std::shared_ptr<ContentMemento> DefaultContent(\n\t\tnot_null<Data::SavedSublist*> sublist,\n\t\tSection section);\n\n\tstd::vector<std::shared_ptr<ContentMemento>> _stack;\n\trpl::event_stream<> _removeRequests;\n\trpl::lifetime _lifetime;\n\n};\n\nclass MoveMemento final : public Window::SectionMemento {\npublic:\n\tMoveMemento(object_ptr<WrapWidget> content);\n\n\tobject_ptr<Window::SectionWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tWindow::Column column,\n\t\tconst QRect &geometry) override;\n\n\tobject_ptr<Ui::LayerWidget> createLayer(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QRect &geometry) override;\n\n\tbool instant() const override {\n\t\treturn true;\n\t}\n\n\tobject_ptr<WrapWidget> takeContent(\n\t\tQWidget *parent,\n\t\tWrap wrap);\n\nprivate:\n\tobject_ptr<WrapWidget> _content;\n\n};\n\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/info_section_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/info_section_widget.h\"\n\n#include \"window/window_adaptive.h\"\n#include \"window/window_connecting_widget.h\"\n#include \"window/window_session_controller.h\"\n#include \"main/main_session.h\"\n#include \"info/info_content_widget.h\"\n#include \"info/info_wrap_widget.h\"\n#include \"info/info_layer_widget.h\"\n#include \"info/info_memento.h\"\n#include \"info/info_controller.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Info {\n\nSectionWidget::SectionWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> window,\n\tWrap wrap,\n\tnot_null<Memento*> memento)\n: Window::SectionWidget(parent, window)\n, _content(this, window, wrap, memento) {\n\tinit();\n}\n\nSectionWidget::SectionWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> window,\n\tWrap wrap,\n\tnot_null<MoveMemento*> memento)\n: Window::SectionWidget(parent, window)\n, _content(memento->takeContent(this, wrap)) {\n\tinit();\n}\n\nvoid SectionWidget::init() {\n\tExpects(_connecting == nullptr);\n\n\trpl::combine(\n\t\tsizeValue(),\n\t\t_content->desiredHeightValue()\n\t) | rpl::filter([=] {\n\t\treturn (_content != nullptr);\n\t}) | rpl::on_next([=](QSize size, int) {\n\t\tconst auto expanding = false;\n\t\tconst auto full = !_content->scrollBottomSkip();\n\t\tconst auto additionalScroll = (full ? st::boxRadius : 0);\n\t\tconst auto height = size.height() - (full ? 0 : st::boxRadius);\n\t\tconst auto wrapGeometry = QRect{ 0, 0, size.width(), height };\n\t\t_content->updateGeometry(\n\t\t\twrapGeometry,\n\t\t\texpanding,\n\t\t\tadditionalScroll,\n\t\t\tsize.height());\n\t}, lifetime());\n\n\t_connecting = std::make_unique<Window::ConnectionState>(\n\t\t_content.data(),\n\t\t&controller()->session().account(),\n\t\tcontroller()->adaptive().oneColumnValue());\n\n\t_content->contentChanged(\n\t) | rpl::on_next([=] {\n\t\t_connecting->raise();\n\t}, _connecting->lifetime());\n}\n\nDialogs::RowDescriptor SectionWidget::activeChat() const {\n\treturn _content->activeChat();\n}\n\nbool SectionWidget::hasTopBarShadow() const {\n\treturn _content->hasTopBarShadow();\n}\n\nQPixmap SectionWidget::grabForShowAnimation(\n\t\tconst Window::SectionSlideParams &params) {\n\treturn _content->grabForShowAnimation(params);\n}\n\nvoid SectionWidget::doSetInnerFocus() {\n\t_content->setInnerFocus();\n}\n\nvoid SectionWidget::showFinishedHook() {\n\t_topBarSurrogate.destroy();\n\t_content->showFast();\n}\n\nvoid SectionWidget::showAnimatedHook(\n\t\tconst Window::SectionSlideParams &params) {\n\t_topBarSurrogate = _content->createTopBarSurrogate(this);\n}\n\nvoid SectionWidget::paintEvent(QPaintEvent *e) {\n\tWindow::SectionWidget::paintEvent(e);\n\tif (!animatingShow()) {\n\t\tQPainter(this).fillRect(e->rect(), st::windowBg);\n\t}\n}\n\nbool SectionWidget::showInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) {\n\treturn _content->showInternal(memento, params);\n}\n\nstd::shared_ptr<Window::SectionMemento> SectionWidget::createMemento() {\n\treturn _content->createMemento();\n}\n\nobject_ptr<Ui::LayerWidget> SectionWidget::moveContentToLayer(\n\t\tQRect bodyGeometry) {\n\tif (_content->controller()->wrap() != Wrap::Narrow\n\t\t|| width() < LayerWidget::MinimalSupportedWidth()) {\n\t\treturn nullptr;\n\t}\n\treturn MoveMemento(\n\t\tstd::move(_content)).createLayer(\n\t\t\tcontroller(),\n\t\t\tbodyGeometry);\n}\n\nrpl::producer<> SectionWidget::removeRequests() const {\n\treturn _content->removeRequests();\n}\n\nbool SectionWidget::floatPlayerHandleWheelEvent(QEvent *e) {\n\treturn _content->floatPlayerHandleWheelEvent(e);\n}\n\nQRect SectionWidget::floatPlayerAvailableRect() {\n\treturn _content->floatPlayerAvailableRect();\n}\n\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/info_section_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <rpl/event_stream.h>\n#include \"window/section_widget.h\"\n\nnamespace Window {\nclass ConnectionState;\n} // namespace Window\n\nnamespace Info {\n\nclass Memento;\nclass MoveMemento;\nclass Controller;\nclass WrapWidget;\nenum class Wrap;\n\nclass SectionWidget final : public Window::SectionWidget {\npublic:\n\tSectionWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> window,\n\t\tWrap wrap,\n\t\tnot_null<Memento*> memento);\n\tSectionWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> window,\n\t\tWrap wrap,\n\t\tnot_null<MoveMemento*> memento);\n\n\tDialogs::RowDescriptor activeChat() const override;\n\n\tbool hasTopBarShadow() const override;\n\tQPixmap grabForShowAnimation(\n\t\tconst Window::SectionSlideParams &params) override;\n\n\tbool showInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) override;\n\tstd::shared_ptr<Window::SectionMemento> createMemento() override;\n\n\tobject_ptr<Ui::LayerWidget> moveContentToLayer(\n\t\tQRect bodyGeometry) override;\n\n\trpl::producer<> removeRequests() const override;\n\n\t// Float player interface.\n\tbool floatPlayerHandleWheelEvent(QEvent *e) override;\n\tQRect floatPlayerAvailableRect() override;\n\nprotected:\n\tvoid doSetInnerFocus() override;\n\tvoid showFinishedHook() override;\n\n\tvoid showAnimatedHook(\n\t\tconst Window::SectionSlideParams &params) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid init();\n\n\tobject_ptr<WrapWidget> _content;\n\tobject_ptr<Ui::RpWidget> _topBarSurrogate = { nullptr };\n\tstd::unique_ptr<Window::ConnectionState> _connecting;\n\n};\n\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/info_top_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/info_top_bar.h\"\n\n#include \"dialogs/ui/dialogs_stories_list.h\"\n#include \"lang/lang_keys.h\"\n#include \"info/info_wrap_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"boxes/delete_messages_box.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"main/main_session.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/search_field_controller.h\"\n#include \"data/data_session.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_user.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info {\n\nTopBar::TopBar(\n\tQWidget *parent,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tconst style::InfoTopBar &st,\n\tSelectedItems &&selectedItems)\n: RpWidget(parent)\n, _navigation(navigation)\n, _st(st)\n, _selectedItems(Section::MediaType::kCount) {\n\tif (_st.radius) {\n\t\t_roundRect.emplace(_st.radius, _st.bg);\n\t}\n\tsetAttribute(Qt::WA_OpaquePaintEvent, !_roundRect);\n\tsetSelectedItems(std::move(selectedItems));\n\tupdateControlsVisibility(anim::type::instant);\n}\n\ntemplate <typename Callback>\nvoid TopBar::registerUpdateControlCallback(\n\t\tQObject *guard,\n\t\tCallback &&callback) {\n\t_updateControlCallbacks[guard] =[\n\t\tweak = base::make_weak(guard),\n\t\tcallback = std::forward<Callback>(callback)\n\t](anim::type animated) {\n\t\tif (!weak) {\n\t\t\treturn false;\n\t\t}\n\t\tcallback(animated);\n\t\treturn true;\n\t};\n}\n\ntemplate <typename Widget, typename IsVisible>\nvoid TopBar::registerToggleControlCallback(\n\t\tWidget *widget,\n\t\tIsVisible &&callback) {\n\tregisterUpdateControlCallback(widget, [\n\t\twidget,\n\t\tisVisible = std::forward<IsVisible>(callback)\n\t](anim::type animated) {\n\t\twidget->toggle(isVisible(), animated);\n\t});\n}\n\nvoid TopBar::setTitle(TitleDescriptor descriptor) {\n\tif (_title) {\n\t\tdelete _title;\n\t}\n\tif (_subtitle) {\n\t\tdelete _subtitle;\n\t}\n\tconst auto withSubtitle = !!descriptor.subtitle;\n\tif (withSubtitle) {\n\t\t_subtitle = Ui::CreateChild<Ui::FadeWrap<Ui::FlatLabel>>(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tthis,\n\t\t\t\tstd::move(descriptor.subtitle),\n\t\t\t\t_st.subtitle),\n\t\t\tst::infoTopBarScale);\n\t\t_subtitle->setDuration(st::infoTopBarDuration);\n\t\t_subtitle->toggle(\n\t\t\t!selectionMode() && !storiesTitle(),\n\t\t\tanim::type::instant);\n\t\tregisterToggleControlCallback(_subtitle.data(), [=] {\n\t\t\treturn !selectionMode() && !storiesTitle() && !searchMode();\n\t\t});\n\t}\n\t_title = Ui::CreateChild<Ui::FadeWrap<Ui::FlatLabel>>(\n\t\tthis,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\tstd::move(descriptor.title),\n\t\t\twithSubtitle ? _st.titleWithSubtitle : _st.title),\n\t\tst::infoTopBarScale);\n\t_title->setDuration(st::infoTopBarDuration);\n\t_title->toggle(\n\t\t!selectionMode() && !storiesTitle(),\n\t\tanim::type::instant);\n\tregisterToggleControlCallback(_title.data(), [=] {\n\t\treturn !selectionMode() && !storiesTitle() && !searchMode();\n\t});\n\n\tif (_back) {\n\t\t_title->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tif (_subtitle) {\n\t\t\t_subtitle->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t}\n\t}\n\tupdateControlsGeometry(width());\n}\n\nvoid TopBar::enableBackButton() {\n\tif (_back) {\n\t\treturn;\n\t}\n\t_back = Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(\n\t\tthis,\n\t\tobject_ptr<Ui::IconButton>(this, _st.back),\n\t\tst::infoTopBarScale);\n\t_back->setDuration(st::infoTopBarDuration);\n\t_back->toggle(!selectionMode(), anim::type::instant);\n\t_back->entity()->setAccessibleName(tr::lng_go_back(tr::now));\n\t_back->entity()->clicks(\n\t) | rpl::to_empty\n\t| rpl::start_to_stream(_backClicks, _back->lifetime());\n\tregisterToggleControlCallback(_back.data(), [=] {\n\t\treturn !selectionMode();\n\t});\n\n\tif (_title) {\n\t\t_title->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t}\n\tif (_subtitle) {\n\t\t_subtitle->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t}\n\tif (_storiesWrap) {\n\t\t_storiesWrap->raise();\n\t}\n\tupdateControlsGeometry(width());\n}\n\nvoid TopBar::createSearchView(\n\t\tnot_null<Ui::SearchFieldController*> controller,\n\t\trpl::producer<bool> &&shown,\n\t\tbool startsFocused) {\n\tsetSearchField(\n\t\tcontroller->createField(this, _st.searchRow.field),\n\t\tstd::move(shown),\n\t\tstartsFocused);\n}\n\nbool TopBar::focusSearchField() {\n\tif (_searchField && _searchField->isVisible()) {\n\t\t_searchField->setFocus();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nUi::FadeWrap<Ui::RpWidget> *TopBar::pushButton(\n\t\tbase::unique_qptr<Ui::RpWidget> button) {\n\tauto wrapped = base::make_unique_q<Ui::FadeWrap<Ui::RpWidget>>(\n\t\tthis,\n\t\tobject_ptr<Ui::RpWidget>::fromRaw(button.release()),\n\t\tst::infoTopBarScale);\n\tauto weak = wrapped.get();\n\t_buttons.push_back(std::move(wrapped));\n\tweak->setDuration(st::infoTopBarDuration);\n\tregisterToggleControlCallback(weak, [=] {\n\t\treturn !selectionMode()\n\t\t\t&& !_searchModeEnabled;\n\t});\n\tweak->toggle(\n\t\t!selectionMode() && !_searchModeEnabled,\n\t\tanim::type::instant);\n\tweak->widthValue(\n\t) | rpl::on_next([this] {\n\t\tupdateControlsGeometry(width());\n\t}, lifetime());\n\treturn weak;\n}\n\nvoid TopBar::forceButtonVisibility(\n\t\tUi::FadeWrap<Ui::RpWidget> *button,\n\t\trpl::producer<bool> shown) {\n\t_updateControlCallbacks.erase(button);\n\tbutton->toggleOn(std::move(shown));\n}\n\nvoid TopBar::setSearchField(\n\t\tbase::unique_qptr<Ui::InputField> field,\n\t\trpl::producer<bool> &&shown,\n\t\tbool startsFocused) {\n\tExpects(field != nullptr);\n\n\tcreateSearchView(field.release(), std::move(shown), startsFocused);\n}\n\nvoid TopBar::clearSearchField() {\n\t_searchView = nullptr;\n}\n\nvoid TopBar::checkBeforeCloseByEscape(Fn<void()> close) {\n\tif (_searchModeEnabled) {\n\t\tif (_searchField && !_searchField->empty()) {\n\t\t\t_searchField->setText({});\n\t\t} else {\n\t\t\t_searchModeEnabled = false;\n\t\t\tupdateControlsVisibility(anim::type::normal);\n\t\t}\n\t} else {\n\t\tclose();\n\t}\n}\n\nvoid TopBar::createSearchView(\n\t\tnot_null<Ui::InputField*> field,\n\t\trpl::producer<bool> &&shown,\n\t\tbool startsFocused) {\n\t_searchView = base::make_unique_q<Ui::FixedHeightWidget>(\n\t\tthis,\n\t\t_st.searchRow.height);\n\tauto wrap = _searchView.get();\n\tregisterUpdateControlCallback(wrap, [=](anim::type) {\n\t\twrap->setVisible(!selectionMode() && _searchModeAvailable);\n\t});\n\n\t_searchField = field;\n\tauto fieldWrap = Ui::CreateChild<Ui::FadeWrap<Ui::InputField>>(\n\t\twrap,\n\t\tobject_ptr<Ui::InputField>::fromRaw(field),\n\t\tst::infoTopBarScale);\n\tfieldWrap->setDuration(st::infoTopBarDuration);\n\n\tauto focusLifetime = field->lifetime().make_state<rpl::lifetime>();\n\tregisterUpdateControlCallback(fieldWrap, [=](anim::type animated) {\n\t\tauto fieldShown = !selectionMode() && searchMode();\n\t\tif (!fieldShown && field->hasFocus()) {\n\t\t\tsetFocus();\n\t\t}\n\t\tfieldWrap->toggle(fieldShown, animated);\n\t\tif (fieldShown) {\n\t\t\t*focusLifetime = field->shownValue()\n\t\t\t\t| rpl::filter([](bool shown) { return shown; })\n\t\t\t\t| rpl::take(1)\n\t\t\t\t| rpl::on_next([=] { field->setFocus(); });\n\t\t} else {\n\t\t\tfocusLifetime->destroy();\n\t\t}\n\t});\n\n\tauto button = base::make_unique_q<Ui::IconButton>(this, _st.search);\n\tauto search = button.get();\n\tsearch->setAccessibleName(tr::lng_dlg_filter(tr::now));\n\tsearch->addClickHandler([=] { showSearch(); });\n\tauto searchWrap = pushButton(std::move(button));\n\tregisterToggleControlCallback(searchWrap, [=] {\n\t\treturn !selectionMode()\n\t\t\t&& _searchModeAvailable\n\t\t\t&& !_searchModeEnabled;\n\t});\n\n\tauto cancel = Ui::CreateChild<Ui::CrossButton>(\n\t\twrap,\n\t\t_st.searchRow.fieldCancel);\n\tcancel->setAccessibleName(tr::lng_sr_cancel_search(tr::now));\n\tregisterToggleControlCallback(cancel, [=] {\n\t\treturn !selectionMode() && searchMode();\n\t});\n\n\tconst auto updateCancelName = [=] {\n\t\tconst auto hasText = !field->getLastText().isEmpty();\n\t\tcancel->setAccessibleName(hasText\n\t\t\t? tr::lng_sr_clear_search(tr::now)\n\t\t\t: tr::lng_sr_cancel_search(tr::now));\n\t};\n\tfield->changes(\n\t) | rpl::on_next(updateCancelName, cancel->lifetime());\n\tupdateCancelName();\n\n\tcancel->addClickHandler([=] {\n\t\tif (!field->getLastText().isEmpty()) {\n\t\t\tfield->setText(QString());\n\t\t} else {\n\t\t\t_searchModeEnabled = false;\n\t\t\tupdateControlsVisibility(anim::type::normal);\n\t\t}\n\t});\n\n\twrap->widthValue(\n\t) | rpl::on_next([=](int newWidth) {\n\t\tauto availableWidth = newWidth\n\t\t\t- _st.searchRow.fieldCancelSkip;\n\t\tfieldWrap->resizeToWidth(availableWidth);\n\t\tfieldWrap->moveToLeft(\n\t\t\t_st.searchRow.padding.left(),\n\t\t\t_st.searchRow.padding.top());\n\t\tcancel->moveToRight(0, 0);\n\t}, wrap->lifetime());\n\n\twidthValue(\n\t) | rpl::on_next([=](int newWidth) {\n\t\tauto left = _back\n\t\t\t? _st.back.width\n\t\t\t: _st.titlePosition.x();\n\t\twrap->setGeometryToLeft(\n\t\t\tleft,\n\t\t\t0,\n\t\t\tnewWidth - left,\n\t\t\twrap->height(),\n\t\t\tnewWidth);\n\t}, wrap->lifetime());\n\n\tfield->alive(\n\t) | rpl::on_done([=] {\n\t\tfield->setParent(nullptr);\n\t\tremoveButton(search);\n\t\tclearSearchField();\n\t}, _searchView->lifetime());\n\n\t_searchModeEnabled = !field->getLastText().isEmpty() || startsFocused;\n\tupdateControlsVisibility(anim::type::instant);\n\n\tstd::move(\n\t\tshown\n\t) | rpl::on_next([=](bool visible) {\n\t\tauto alreadyInSearch = !field->getLastText().isEmpty();\n\t\t_searchModeAvailable = visible || alreadyInSearch;\n\t\tupdateControlsVisibility(anim::type::instant);\n\t}, wrap->lifetime());\n}\n\nvoid TopBar::showSearch() {\n\t_searchModeEnabled = true;\n\tupdateControlsVisibility(anim::type::normal);\n}\n\nvoid TopBar::removeButton(not_null<Ui::RpWidget*> button) {\n\t_buttons.erase(\n\t\tstd::remove(_buttons.begin(), _buttons.end(), button),\n\t\t_buttons.end());\n}\n\nint TopBar::resizeGetHeight(int newWidth) {\n\tupdateControlsGeometry(newWidth);\n\treturn _st.height;\n}\n\nvoid TopBar::updateControlsGeometry(int newWidth) {\n\tupdateDefaultControlsGeometry(newWidth);\n\tupdateSelectionControlsGeometry(newWidth);\n\tupdateStoriesGeometry(newWidth);\n}\n\nvoid TopBar::updateDefaultControlsGeometry(int newWidth) {\n\tauto right = 0;\n\tfor (auto &button : _buttons) {\n\t\tif (!button) {\n\t\t\tcontinue;\n\t\t}\n\t\tbutton->moveToRight(right, 0, newWidth);\n\t\tright += button->width();\n\t}\n\tif (_back) {\n\t\t_back->setGeometryToLeft(\n\t\t\t0,\n\t\t\t0,\n\t\t\tnewWidth - right,\n\t\t\t_back->height(),\n\t\t\tnewWidth);\n\t}\n\tif (_title) {\n\t\tconst auto x = _back\n\t\t\t? _st.back.width\n\t\t\t: _subtitle\n\t\t\t? _st.titleWithSubtitlePosition.x()\n\t\t\t: _st.titlePosition.x();\n\t\tconst auto y = _subtitle\n\t\t\t? _st.titleWithSubtitlePosition.y()\n\t\t\t: _st.titlePosition.y();\n\t\t_title->moveToLeft(x, y, newWidth);\n\t\tif (_subtitle) {\n\t\t\t_subtitle->moveToLeft(\n\t\t\t\t_back ? _st.back.width : _st.subtitlePosition.x(),\n\t\t\t\t_st.subtitlePosition.y(),\n\t\t\t\tnewWidth);\n\t\t}\n\t}\n}\n\nvoid TopBar::updateSelectionControlsGeometry(int newWidth) {\n\tif (!_selectionText) {\n\t\treturn;\n\t}\n\n\tauto right = _st.mediaActionsSkip;\n\tif (_canDelete) {\n\t\t_delete->moveToRight(right, 0, newWidth);\n\t\tright += _delete->width();\n\t}\n\tif (_canToggleStoryPin) {\n\t\t_toggleStoryInProfile->moveToRight(right, 0, newWidth);\n\t\tright += _toggleStoryInProfile->width();\n\t\t_toggleStoryPin->moveToRight(right, 0, newWidth);\n\t\tright += _toggleStoryPin->width();\n\t}\n\tif (_canForward) {\n\t\t_forward->moveToRight(right, 0, newWidth);\n\t\tright += _forward->width();\n\t}\n\n\tauto left = 0;\n\t_cancelSelection->moveToLeft(left, 0);\n\tleft += _cancelSelection->width();\n\n\tconst auto top = 0;\n\tconst auto availableWidth = newWidth - left - right;\n\t_selectionText->resizeToNaturalWidth(availableWidth);\n\t_selectionText->moveToLeft(\n\t\tleft,\n\t\ttop,\n\t\tnewWidth);\n}\n\nvoid TopBar::updateStoriesGeometry(int newWidth) {\n\tif (!_stories) {\n\t\treturn;\n\t}\n\n\tauto right = 0;\n\tfor (auto &button : _buttons) {\n\t\tif (!button) {\n\t\t\tcontinue;\n\t\t}\n\t\tbutton->moveToRight(right, 0, newWidth);\n\t\tright += button->width();\n\t}\n\tconst auto &small = st::dialogsStories;\n\tconst auto wrapLeft = (_back ? _st.back.width : 0);\n\tconst auto left = _back\n\t\t? 0\n\t\t: (_st.titlePosition.x() - small.left - small.photoLeft);\n\tconst auto height = small.photo + 2 * small.photoTop;\n\tconst auto top = _st.titlePosition.y()\n\t\t+ (_st.title.style.font->height - height) / 2;\n\t_stories->setLayoutConstraints({ left, top }, style::al_left);\n\t_storiesWrap->move(wrapLeft, 0);\n}\n\nvoid TopBar::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tauto highlight = _a_highlight.value(_highlight ? 1. : 0.);\n\tif (_highlight && !_a_highlight.animating()) {\n\t\t_highlight = false;\n\t\tstartHighlightAnimation();\n\t}\n\tif (!_roundRect) {\n\t\tconst auto brush = anim::brush(_st.bg, _st.highlightBg, highlight);\n\t\tp.fillRect(e->rect(), brush);\n\t} else if (highlight > 0.) {\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(anim::brush(_st.bg, _st.highlightBg, highlight));\n\t\tp.drawRoundedRect(\n\t\t\trect() + style::margins(0, 0, 0, _st.radius * 2),\n\t\t\t_st.radius,\n\t\t\t_st.radius);\n\t} else {\n\t\t_roundRect->paintSomeRounded(\n\t\t\tp,\n\t\t\trect(),\n\t\t\tRectPart::TopLeft | RectPart::TopRight);\n\t}\n}\n\nvoid TopBar::highlight() {\n\t_highlight = true;\n\tstartHighlightAnimation();\n}\n\nvoid TopBar::startHighlightAnimation() {\n\t_a_highlight.start(\n\t\t[this] { update(); },\n\t\t_highlight ? 0. : 1.,\n\t\t_highlight ? 1. : 0.,\n\t\t_st.highlightDuration);\n}\n\nvoid TopBar::updateControlsVisibility(anim::type animated) {\n\tfor (auto i = _updateControlCallbacks.begin(); i != _updateControlCallbacks.end();) {\n\t\tauto &&[widget, callback] = *i;\n\t\tif (!callback(animated)) {\n\t\t\ti = _updateControlCallbacks.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n}\n\nvoid TopBar::setStories(rpl::producer<Dialogs::Stories::Content> content) {\n\t_storiesLifetime.destroy();\n\tdelete _storiesWrap.data();\n\tif (content) {\n\t\tusing namespace Dialogs::Stories;\n\n\t\tauto last = std::move(\n\t\t\tcontent\n\t\t) | rpl::start_spawning(_storiesLifetime);\n\n\t\t_storiesWrap = _storiesLifetime.make_state<\n\t\t\tUi::FadeWrap<Ui::AbstractButton>\n\t\t>(this, object_ptr<Ui::AbstractButton>(this), st::infoTopBarScale);\n\t\tregisterToggleControlCallback(\n\t\t\t_storiesWrap.data(),\n\t\t\t[this] { return _storiesCount > 0; });\n\t\t_storiesWrap->toggle(false, anim::type::instant);\n\t\t_storiesWrap->setDuration(st::infoTopBarDuration);\n\n\t\tconst auto button = _storiesWrap->entity();\n\t\tconst auto stories = Ui::CreateChild<List>(\n\t\t\tbutton,\n\t\t\tst::dialogsStoriesListInfo,\n\t\t\trpl::duplicate(\n\t\t\t\tlast\n\t\t\t) | rpl::filter([](const Content &content) {\n\t\t\t\treturn !content.elements.empty();\n\t\t\t}));\n\t\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tbutton,\n\t\t\tQString(),\n\t\t\t_st.title);\n\t\tstories->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tstories->geometryValue(\n\t\t) | rpl::on_next([=](QRect geometry) {\n\t\t\tconst auto skip = _st.title.style.font->spacew;\n\t\t\tlabel->move(\n\t\t\t\tgeometry.x() + geometry.width() + skip,\n\t\t\t\t_st.titlePosition.y());\n\t\t}, label->lifetime());\n\t\trpl::combine(\n\t\t\t_storiesWrap->positionValue(),\n\t\t\tlabel->geometryValue()\n\t\t) | rpl::on_next([=] {\n\t\t\tbutton->resize(\n\t\t\t\tlabel->x() + label->width() + _st.titlePosition.x(),\n\t\t\t\t_st.height);\n\t\t}, button->lifetime());\n\n\t\t_stories = stories;\n\t\t_stories->clicks(\n\t\t) | rpl::start_to_stream(_storyClicks, _stories->lifetime());\n\n\t\tbutton->setClickedCallback([=] {\n\t\t\t_storyClicks.fire({});\n\t\t});\n\n\t\trpl::duplicate(\n\t\t\tlast\n\t\t) | rpl::on_next([=](const Content &content) {\n\t\t\tconst auto count = content.total;\n\t\t\tif (_storiesCount != count) {\n\t\t\t\tconst auto was = (_storiesCount > 0);\n\t\t\t\t_storiesCount = count;\n\t\t\t\tconst auto now = (_storiesCount > 0);\n\t\t\t\tif (was != now) {\n\t\t\t\t\tupdateControlsVisibility(anim::type::normal);\n\t\t\t\t}\n\t\t\t\tif (now) {\n\t\t\t\t\tlabel->setText(\n\t\t\t\t\t\ttr::lng_contacts_stories_status(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\t_storiesCount));\n\t\t\t\t}\n\t\t\t\tupdateControlsGeometry(width());\n\t\t\t}\n\t\t}, _storiesLifetime);\n\n\t\t_storiesLifetime.add([weak = base::make_weak(label)] {\n\t\t\tdelete weak.get();\n\t\t});\n\t} else {\n\t\t_storiesCount = 0;\n\t}\n\tupdateControlsVisibility(anim::type::instant);\n}\n\nvoid TopBar::setSelectedItems(SelectedItems &&items) {\n\tauto wasSelectionMode = selectionMode();\n\t_selectedItems = std::move(items);\n\tif (selectionMode()) {\n\t\tif (_selectionText) {\n\t\t\tupdateSelectionState();\n\t\t\tif (!wasSelectionMode) {\n\t\t\t\t_selectionText->entity()->finishAnimating();\n\t\t\t}\n\t\t} else {\n\t\t\tcreateSelectionControls();\n\t\t}\n\t}\n\tupdateControlsVisibility(anim::type::normal);\n}\n\nSelectedItems TopBar::takeSelectedItems() {\n\t_canDelete = false;\n\t_canForward = false;\n\treturn std::move(_selectedItems);\n}\n\nrpl::producer<SelectionAction> TopBar::selectionActionRequests() const {\n\treturn _selectionActionRequests.events();\n}\n\nvoid TopBar::updateSelectionState() {\n\tExpects(_selectionText\n\t\t&& _delete\n\t\t&& _forward\n\t\t&& _toggleStoryInProfile\n\t\t&& _toggleStoryPin);\n\n\t_canDelete = computeCanDelete();\n\t_canForward = computeCanForward();\n\t_canUnpinStories = computeCanUnpinStories();\n\t_canToggleStoryPin = computeCanToggleStoryPin();\n\t_allStoriesInProfile = computeAllStoriesInProfile();\n\t_selectionText->entity()->setValue(generateSelectedText());\n\t_delete->toggle(_canDelete, anim::type::instant);\n\t_forward->toggle(_canForward, anim::type::instant);\n\t_toggleStoryInProfile->toggle(_canToggleStoryPin, anim::type::instant);\n\t_toggleStoryInProfile->entity()->setIconOverride(\n\t\t(_allStoriesInProfile\n\t\t\t? &_st.storiesArchive.icon\n\t\t\t: &_st.storiesSave.icon),\n\t\t(_allStoriesInProfile\n\t\t\t? &_st.storiesArchive.iconOver\n\t\t\t: &_st.storiesSave.iconOver));\n\t_toggleStoryInProfile->entity()->setAccessibleName(_allStoriesInProfile\n\t\t? tr::lng_mediaview_archive_story(tr::now)\n\t\t: tr::lng_mediaview_save_to_profile(tr::now));\n\t_toggleStoryPin->entity()->setAccessibleName(_canUnpinStories\n\t\t? tr::lng_context_unpin_from_top(tr::now)\n\t\t: tr::lng_context_pin_to_top(tr::now));\n\t_toggleStoryPin->toggle(_canToggleStoryPin, anim::type::instant);\n\t_toggleStoryPin->entity()->setIconOverride(\n\t\t_canUnpinStories ? &_st.storiesUnpin.icon : nullptr,\n\t\t_canUnpinStories ? &_st.storiesUnpin.iconOver : nullptr);\n\n\tupdateSelectionControlsGeometry(width());\n}\n\nvoid TopBar::createSelectionControls() {\n\tauto wrap = [&](auto created) {\n\t\tregisterToggleControlCallback(\n\t\t\tcreated,\n\t\t\t[this] { return selectionMode(); });\n\t\tcreated->toggle(false, anim::type::instant);\n\t\treturn created;\n\t};\n\t_canDelete = computeCanDelete();\n\t_canForward = computeCanForward();\n\t_canUnpinStories = computeCanUnpinStories();\n\t_canToggleStoryPin = computeCanToggleStoryPin();\n\t_allStoriesInProfile = computeAllStoriesInProfile();\n\t_cancelSelection = wrap(Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(\n\t\tthis,\n\t\tobject_ptr<Ui::IconButton>(this, _st.mediaCancel),\n\t\tst::infoTopBarScale));\n\t_cancelSelection->setDuration(st::infoTopBarDuration);\n\t_cancelSelection->entity()->setAccessibleName(\n\t\ttr::lng_context_clear_selection(tr::now));\n\t_cancelSelection->entity()->clicks(\n\t) | rpl::map_to(\n\t\tSelectionAction::Clear\n\t) | rpl::start_to_stream(\n\t\t_selectionActionRequests,\n\t\t_cancelSelection->lifetime());\n\n\t_selectionText = wrap(Ui::CreateChild<Ui::FadeWrap<Ui::LabelWithNumbers>>(\n\t\tthis,\n\t\tobject_ptr<Ui::LabelWithNumbers>(\n\t\t\tthis,\n\t\t\t_st.title,\n\t\t\t_st.titlePosition.y(),\n\t\t\tgenerateSelectedText()),\n\t\tst::infoTopBarScale));\n\t_selectionText->setDuration(st::infoTopBarDuration);\n\t_selectionText->entity()->resize(0, _st.height);\n\t_selectionText->naturalWidthValue(\n\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\tupdateSelectionControlsGeometry(width());\n\t}, _selectionText->lifetime());\n\n\t_forward = wrap(Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(\n\t\tthis,\n\t\tobject_ptr<Ui::IconButton>(this, _st.mediaForward),\n\t\tst::infoTopBarScale));\n\tregisterToggleControlCallback(\n\t\t_forward.data(),\n\t\t[this] { return selectionMode() && _canForward; });\n\t_forward->setDuration(st::infoTopBarDuration);\n\t_forward->entity()->setAccessibleName(\n\t\ttr::lng_context_forward_selected(tr::now));\n\t_forward->entity()->clicks(\n\t) | rpl::map_to(\n\t\tSelectionAction::Forward\n\t) | rpl::start_to_stream(\n\t\t_selectionActionRequests,\n\t\t_cancelSelection->lifetime());\n\t_forward->entity()->setVisible(_canForward);\n\n\t_delete = wrap(Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(\n\t\tthis,\n\t\tobject_ptr<Ui::IconButton>(this, _st.mediaDelete),\n\t\tst::infoTopBarScale));\n\tregisterToggleControlCallback(\n\t\t_delete.data(),\n\t\t[this] { return selectionMode() && _canDelete; });\n\t_delete->setDuration(st::infoTopBarDuration);\n\t_delete->entity()->setAccessibleName(\n\t\ttr::lng_context_delete_selected(tr::now));\n\t_delete->entity()->clicks(\n\t) | rpl::map_to(\n\t\tSelectionAction::Delete\n\t) | rpl::start_to_stream(\n\t\t_selectionActionRequests,\n\t\t_cancelSelection->lifetime());\n\t_delete->entity()->setVisible(_canDelete);\n\n\t_toggleStoryInProfile = wrap(\n\t\tUi::CreateChild<Ui::FadeWrap<Ui::IconButton>>(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::IconButton>(\n\t\t\t\tthis,\n\t\t\t\t_allStoriesInProfile ? _st.storiesArchive : _st.storiesSave),\n\t\t\tst::infoTopBarScale));\n\tregisterToggleControlCallback(\n\t\t_toggleStoryInProfile.data(),\n\t\t[this] { return selectionMode() && _canToggleStoryPin; });\n\t_toggleStoryInProfile->setDuration(st::infoTopBarDuration);\n\t_toggleStoryInProfile->entity()->setAccessibleName(_allStoriesInProfile\n\t\t? tr::lng_mediaview_archive_story(tr::now)\n\t\t: tr::lng_mediaview_save_to_profile(tr::now));\n\t_toggleStoryInProfile->entity()->clicks(\n\t) | rpl::map([=] {\n\t\treturn _allStoriesInProfile\n\t\t\t? SelectionAction::ToggleStoryToArchive\n\t\t\t: SelectionAction::ToggleStoryToProfile;\n\t}) | rpl::start_to_stream(\n\t\t_selectionActionRequests,\n\t\t_cancelSelection->lifetime());\n\t_toggleStoryInProfile->entity()->setVisible(_canToggleStoryPin);\n\n\t_toggleStoryPin = wrap(\n\t\tUi::CreateChild<Ui::FadeWrap<Ui::IconButton>>(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::IconButton>(\n\t\t\t\tthis,\n\t\t\t\t_st.storiesPin),\n\t\t\tst::infoTopBarScale));\n\tif (_canUnpinStories) {\n\t\t_toggleStoryPin->entity()->setIconOverride(\n\t\t\t_canUnpinStories ? &_st.storiesUnpin.icon : nullptr,\n\t\t\t_canUnpinStories ? &_st.storiesUnpin.iconOver : nullptr);\n\t}\n\tregisterToggleControlCallback(\n\t\t_toggleStoryPin.data(),\n\t\t[this] { return selectionMode() && _canToggleStoryPin; });\n\t_toggleStoryPin->setDuration(st::infoTopBarDuration);\n\t_toggleStoryPin->entity()->setAccessibleName(_canUnpinStories\n\t\t? tr::lng_context_unpin_from_top(tr::now)\n\t\t: tr::lng_context_pin_to_top(tr::now));\n\t_toggleStoryPin->entity()->clicks(\n\t) | rpl::map_to(\n\t\tSelectionAction::ToggleStoryPin\n\t) | rpl::start_to_stream(\n\t\t_selectionActionRequests,\n\t\t_cancelSelection->lifetime());\n\t_toggleStoryPin->entity()->setVisible(_canToggleStoryPin);\n\n\tupdateControlsGeometry(width());\n}\n\nbool TopBar::computeCanDelete() const {\n\treturn ranges::all_of(_selectedItems.list, &SelectedItem::canDelete);\n}\n\nbool TopBar::computeCanForward() const {\n\treturn ranges::all_of(_selectedItems.list, &SelectedItem::canForward);\n}\n\nbool TopBar::computeCanUnpinStories() const {\n\treturn ranges::any_of(_selectedItems.list, &SelectedItem::canUnpinStory);\n}\n\nbool TopBar::computeCanToggleStoryPin() const {\n\treturn ranges::all_of(\n\t\t_selectedItems.list,\n\t\t&SelectedItem::canToggleStoryPin);\n}\n\nbool TopBar::computeAllStoriesInProfile() const {\n\treturn ranges::all_of(\n\t\t_selectedItems.list,\n\t\t&SelectedItem::storyInProfile);\n}\n\nUi::StringWithNumbers TopBar::generateSelectedText() const {\n\treturn _selectedItems.title(_selectedItems.list.size());\n}\n\nbool TopBar::selectionMode() const {\n\treturn !_selectedItems.list.empty();\n}\n\nbool TopBar::storiesTitle() const {\n\treturn _storiesCount > 0;\n}\n\nbool TopBar::searchMode() const {\n\treturn _searchModeAvailable && _searchModeEnabled;\n}\n\nvoid TopBar::performForward() {\n\t_selectionActionRequests.fire(SelectionAction::Forward);\n}\n\nvoid TopBar::performDelete() {\n\t_selectionActionRequests.fire(SelectionAction::Delete);\n}\n\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/info_top_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/round_rect.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/numbers_animation.h\"\n#include \"info/info_wrap_widget.h\"\n\nnamespace style {\nstruct InfoTopBar;\n} // namespace style\n\nnamespace Dialogs::Stories {\nclass List;\nstruct Content;\n} // namespace Dialogs::Stories\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nnamespace Ui {\nclass AbstractButton;\nclass IconButton;\nclass FlatLabel;\nclass InputField;\nclass SearchFieldController;\nclass LabelWithNumbers;\n} // namespace Ui\n\nnamespace Info {\n\nclass Key;\nclass Section;\n\nstruct TitleDescriptor {\n\trpl::producer<QString> title;\n\trpl::producer<QString> subtitle;\n};\n\nclass TopBar : public Ui::RpWidget {\npublic:\n\tTopBar(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tconst style::InfoTopBar &st,\n\t\tSelectedItems &&items);\n\n\t[[nodiscard]] auto backRequest() const {\n\t\treturn _backClicks.events();\n\t}\n\t[[nodiscard]] auto storyClicks() const {\n\t\treturn _storyClicks.events();\n\t}\n\n\tvoid setTitle(TitleDescriptor descriptor);\n\tvoid setStories(rpl::producer<Dialogs::Stories::Content> content);\n\tvoid enableBackButton();\n\tvoid highlight();\n\n\ttemplate <typename ButtonWidget>\n\tButtonWidget *addButton(base::unique_qptr<ButtonWidget> button) {\n\t\tauto result = button.get();\n\t\tpushButton(std::move(button));\n\t\treturn result;\n\t}\n\n\ttemplate <typename ButtonWidget>\n\tButtonWidget *addButtonWithVisibility(\n\t\t\tbase::unique_qptr<ButtonWidget> button,\n\t\t\trpl::producer<bool> shown) {\n\t\tauto result = button.get();\n\t\tforceButtonVisibility(\n\t\t\tpushButton(std::move(button)),\n\t\t\tstd::move(shown));\n\t\treturn result;\n\t}\n\n\tvoid createSearchView(\n\t\tnot_null<Ui::SearchFieldController*> controller,\n\t\trpl::producer<bool> &&shown,\n\t\tbool startsFocused);\n\tbool focusSearchField();\n\n\tvoid setSelectedItems(SelectedItems &&items);\n\tSelectedItems takeSelectedItems();\n\n\t[[nodiscard]] auto selectionActionRequests() const\n\t\t-> rpl::producer<SelectionAction>;\n\n\tvoid finishAnimating() {\n\t\tupdateControlsVisibility(anim::type::instant);\n\t}\n\n\tvoid showSearch();\n\n\tvoid checkBeforeCloseByEscape(Fn<void()> close);\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid updateControlsGeometry(int newWidth);\n\tvoid updateDefaultControlsGeometry(int newWidth);\n\tvoid updateSelectionControlsGeometry(int newWidth);\n\tvoid updateStoriesGeometry(int newWidth);\n\tUi::FadeWrap<Ui::RpWidget> *pushButton(\n\t\tbase::unique_qptr<Ui::RpWidget> button);\n\tvoid forceButtonVisibility(\n\t\tUi::FadeWrap<Ui::RpWidget> *button,\n\t\trpl::producer<bool> shown);\n\tvoid removeButton(not_null<Ui::RpWidget*> button);\n\tvoid startHighlightAnimation();\n\tvoid updateControlsVisibility(anim::type animated);\n\n\t[[nodiscard]] bool selectionMode() const;\n\t[[nodiscard]] bool storiesTitle() const;\n\t[[nodiscard]] bool searchMode() const;\n\t[[nodiscard]] Ui::StringWithNumbers generateSelectedText() const;\n\t[[nodiscard]] bool computeCanDelete() const;\n\t[[nodiscard]] bool computeCanForward() const;\n\t[[nodiscard]] bool computeCanUnpinStories() const;\n\t[[nodiscard]] bool computeCanToggleStoryPin() const;\n\t[[nodiscard]] bool computeAllStoriesInProfile() const;\n\tvoid updateSelectionState();\n\tvoid createSelectionControls();\n\n\tvoid performForward();\n\tvoid performDelete();\n\tvoid performToggleStoryPin();\n\n\tvoid setSearchField(\n\t\tbase::unique_qptr<Ui::InputField> field,\n\t\trpl::producer<bool> &&shown,\n\t\tbool startsFocused);\n\tvoid clearSearchField();\n\tvoid createSearchView(\n\t\tnot_null<Ui::InputField*> field,\n\t\trpl::producer<bool> &&shown,\n\t\tbool startsFocused);\n\n\ttemplate <typename Callback>\n\tvoid registerUpdateControlCallback(QObject *guard, Callback &&callback);\n\n\ttemplate <typename Widget, typename IsVisible>\n\tvoid registerToggleControlCallback(Widget *widget, IsVisible &&callback);\n\n\tconst not_null<Window::SessionNavigation*> _navigation;\n\n\tconst style::InfoTopBar &_st;\n\tstd::optional<Ui::RoundRect> _roundRect;\n\tUi::Animations::Simple _a_highlight;\n\tbool _highlight = false;\n\tQPointer<Ui::FadeWrap<Ui::IconButton>> _back;\n\tstd::vector<base::unique_qptr<Ui::RpWidget>> _buttons;\n\tQPointer<Ui::FadeWrap<Ui::FlatLabel>> _title;\n\tQPointer<Ui::FadeWrap<Ui::FlatLabel>> _subtitle;\n\n\tbool _searchModeEnabled = false;\n\tbool _searchModeAvailable = false;\n\tbase::unique_qptr<Ui::RpWidget> _searchView;\n\tQPointer<Ui::InputField> _searchField;\n\n\trpl::event_stream<> _backClicks;\n\trpl::event_stream<uint64> _storyClicks;\n\n\tSelectedItems _selectedItems;\n\tbool _canDelete = false;\n\tbool _canForward = false;\n\tbool _canToggleStoryPin = false;\n\tbool _canUnpinStories = false;\n\tbool _allStoriesInProfile = false;\n\tQPointer<Ui::FadeWrap<Ui::IconButton>> _cancelSelection;\n\tQPointer<Ui::FadeWrap<Ui::LabelWithNumbers>> _selectionText;\n\tQPointer<Ui::FadeWrap<Ui::IconButton>> _forward;\n\tQPointer<Ui::FadeWrap<Ui::IconButton>> _delete;\n\tQPointer<Ui::FadeWrap<Ui::IconButton>> _toggleStoryInProfile;\n\tQPointer<Ui::FadeWrap<Ui::IconButton>> _toggleStoryPin;\n\trpl::event_stream<SelectionAction> _selectionActionRequests;\n\n\tQPointer<Ui::FadeWrap<Ui::AbstractButton>> _storiesWrap;\n\tQPointer<Dialogs::Stories::List> _stories;\n\trpl::lifetime _storiesLifetime;\n\tint _storiesCount = 0;\n\n\tusing UpdateCallback = Fn<bool(anim::type)>;\n\tstd::map<QObject*, UpdateCallback> _updateControlCallbacks;\n\n};\n\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/info_wrap_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/info_wrap_widget.h\"\n\n#include \"info/profile/info_profile_widget.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"info/media/info_media_widget.h\"\n#include \"info/stories/info_stories_widget.h\"\n#include \"info/info_content_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"info/info_top_bar.h\"\n#include \"settings/cloud_password/settings_cloud_password_email_confirm.h\"\n#include \"settings/sections/settings_chat.h\"\n#include \"settings/sections/settings_information.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"settings/settings_search.h\"\n#include \"ui/effects/ripple_animation.h\" // MaskByDrawer.\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/menu/menu_item_base.h\"\n#include \"ui/widgets/discrete_sliders.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/search_field_controller.h\"\n#include \"ui/ui_utility.h\"\n#include \"core/application.h\"\n#include \"calls/calls_instance.h\"\n#include \"core/shortcuts.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_slide_animation.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/peer_qr_box.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"data/data_download_manager.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_user.h\"\n#include \"data/data_forum_topic.h\"\n#include \"mainwidget.h\"\n#include \"lang/lang_keys.h\"\n#include \"lang/lang_numbers_animation.h\"\n#include \"styles/style_chat.h\" // popupMenuExpandedSeparator\n#include \"styles/style_info.h\"\n#include \"styles/style_profile.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Info {\nnamespace {\n\nconst style::InfoTopBar &TopBarStyle(Wrap wrap) {\n\treturn (wrap == Wrap::Layer)\n\t\t? st::infoLayerTopBar\n\t\t: st::infoTopBar;\n}\n\n[[nodiscard]] bool HasCustomTopBar(not_null<const Controller*> controller) {\n\tconst auto section = controller->section();\n\treturn (section.type() == Section::Type::BotStarRef)\n\t\t|| (section.type() == Section::Type::Profile)\n\t\t|| ((section.type() == Section::Type::Settings)\n\t\t\t&& section.settingsType()->hasCustomTopBar())\n\t\t|| (section.type() == Section::Type::Stories\n\t\t\t&& controller->key().storiesAlbumId() != Stories::ArchiveId()\n\t\t\t&& controller->key().storiesPeer()\n\t\t\t&& controller->key().storiesPeer()->isSelf());\n}\n\n[[nodiscard]] Fn<Ui::StringWithNumbers(int)> SelectedTitleForMedia(\n\t\tSection::MediaType type) {\n\treturn [type](int count) {\n\t\tusing Type = Storage::SharedMediaType;\n\t\treturn [&] {\n\t\t\tswitch (type) {\n\t\t\tcase Type::Photo: return tr::lng_media_selected_photo;\n\t\t\tcase Type::GIF: return tr::lng_media_selected_gif;\n\t\t\tcase Type::Video: return tr::lng_media_selected_video;\n\t\t\tcase Type::File: return tr::lng_media_selected_file;\n\t\t\tcase Type::MusicFile: return tr::lng_media_selected_song;\n\t\t\tcase Type::Link: return tr::lng_media_selected_link;\n\t\t\tcase Type::RoundVoiceFile: return tr::lng_media_selected_audio;\n\t\t\tcase Type::PhotoVideo: return tr::lng_stories_row_count;\n\t\t\tcase Type::Poll: return tr::lng_media_selected_poll;\n\t\t\t}\n\t\t\tUnexpected(\"Type in TopBar::generateSelectedText()\");\n\t\t}()(tr::now, lt_count, count, Ui::StringWithNumbers::FromString);\n\t};\n}\n\n} // namespace\n\nstruct WrapWidget::StackItem {\n\tstd::shared_ptr<ContentMemento> section;\n//\tstd::shared_ptr<ContentMemento> anotherTab;\n};\n\nSelectedItems::SelectedItems(Section::MediaType mediaType)\n: title(SelectedTitleForMedia(mediaType)) {\n}\n\nWrapWidget::WrapWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> window,\n\tWrap wrap,\n\tnot_null<Memento*> memento)\n: SectionWidget(parent, window, rpl::producer<PeerData*>())\n, _isSeparatedWindow(\n\twindow->windowId().type == Window::SeparateType::SharedMedia)\n, _wrap(wrap)\n, _controller(createController(window, memento->content()))\n, _topShadow(this)\n, _bottomShadow(this) {\n\t_topShadow->toggleOn(\n\t\ttopShadowToggledValue(\n\t\t) | rpl::filter([](bool shown) {\n\t\t\treturn true;\n\t\t}));\n\n\t_bottomShadow->toggleOn(\n\t\t_desiredBottomShadowVisibilities.events(\n\t\t) | rpl::flatten_latest() | rpl::distinct_until_changed());\n\n\t_wrap.changes(\n\t) | rpl::on_next([this] {\n\t\tsetupTop();\n\t\tfinishShowContent();\n\t}, lifetime());\n\tselectedListValue(\n\t) | rpl::on_next([this](SelectedItems &&items) {\n\t\tInvokeQueued(this, [this, items = std::move(items)]() mutable {\n\t\t\tif (_topBar) {\n\t\t\t\t_topBar->setSelectedItems(std::move(items));\n\t\t\t}\n\t\t});\n\t}, lifetime());\n\trestoreHistoryStack(memento->takeStack());\n\n\tif (const auto topic = _controller->topic()) {\n\t\ttopic->destroyed(\n\t\t) | rpl::on_next([=] {\n\t\t\tif (_wrap.current() == Wrap::Layer) {\n\t\t\t\t_controller->parentController()->hideSpecialLayer();\n\t\t\t} else if (_wrap.current() == Wrap::Narrow) {\n\t\t\t\t_controller->parentController()->showBackFromStack(\n\t\t\t\t\tWindow::SectionShow(\n\t\t\t\t\t\tanim::type::normal,\n\t\t\t\t\t\tanim::activation::background));\n\t\t\t} else {\n\t\t\t\t_removeRequests.fire({});\n\t\t\t}\n\t\t}, lifetime());\n\t}\n}\n\nvoid WrapWidget::setupShortcuts() {\n\tconst auto isSettings = [=] {\n\t\treturn _controller->section().type() == Section::Type::Settings;\n\t};\n\tconst auto isSearchSettings = [=] {\n\t\treturn isSettings()\n\t\t\t&& (_controller->section().settingsType()\n\t\t\t\t== ::Settings::Search::Id());\n\t};\n\n\tShortcuts::Requests(\n\t) | rpl::filter([=] {\n\t\treturn (Core::App().activeWindow()\n\t\t\t\t== &_controller->parentController()->window())\n\t\t\t&& (requireTopBarSearch() || isSettings());\n\t}) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {\n\t\tusing Command = Shortcuts::Command;\n\t\trequest->check(Command::Search) && request->handle([=] {\n\t\t\tif (requireTopBarSearch()) {\n\t\t\t\t_topBar->showSearch();\n\t\t\t} else if (isSearchSettings()) {\n\t\t\t\t_content->setInnerFocus();\n\t\t\t} else if (isSettings()) {\n\t\t\t\t_controller->showSettings(::Settings::Search::Id());\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\t}, lifetime());\n}\n\nvoid WrapWidget::restoreHistoryStack(\n\t\tstd::vector<std::shared_ptr<ContentMemento>> stack) {\n\tExpects(!stack.empty());\n\tExpects(!hasStackHistory());\n\n\tauto content = std::move(stack.back());\n\tstack.pop_back();\n\tif (!stack.empty()) {\n\t\t_historyStack.reserve(stack.size());\n\t\tfor (auto &stackItem : stack) {\n\t\t\tauto item = StackItem();\n\t\t\titem.section = std::move(stackItem);\n\t\t\t_historyStack.push_back(std::move(item));\n\t\t}\n\t}\n\n\tstartInjectingActivePeerProfiles();\n\n\tshowNewContent(content.get());\n}\n\nvoid WrapWidget::startInjectingActivePeerProfiles() {\n\tusing namespace rpl::mappers;\n\trpl::combine(\n\t\t_wrap.value(),\n\t\t_controller->parentController()->activeChatValue()\n\t) | rpl::filter(\n\t\t(_1 == Wrap::Side) && _2\n\t) | rpl::map(\n\t\t_2\n\t) | rpl::on_next([this](Dialogs::Key key) {\n\t\tinjectActiveProfile(key);\n\t}, lifetime());\n\n}\n\nvoid WrapWidget::injectActiveProfile(Dialogs::Key key) {\n\tif (const auto peer = key.peer()) {\n\t\tinjectActivePeerProfile(peer);\n\t}\n}\n\nvoid WrapWidget::injectActivePeerProfile(not_null<PeerData*> peer) {\n\tconst auto firstPeer = hasStackHistory()\n\t\t? _historyStack.front().section->peer()\n\t\t: _controller->peer();\n\tconst auto firstSectionType = hasStackHistory()\n\t\t? _historyStack.front().section->section().type()\n\t\t: _controller->section().type();\n\tconst auto firstSectionMediaType = [&] {\n\t\tif (firstSectionType == Section::Type::Profile\n\t\t\t|| firstSectionType == Section::Type::SavedSublists\n\t\t\t|| firstSectionType == Section::Type::Downloads) {\n\t\t\treturn Section::MediaType::kCount;\n\t\t}\n\t\treturn hasStackHistory()\n\t\t\t? _historyStack.front().section->section().mediaType()\n\t\t\t: _controller->section().mediaType();\n\t}();\n\tconst auto savedSublistsInfo = peer->savedSublistsInfo();\n\tconst auto sharedMediaInfo = peer->sharedMediaInfo();\n\tconst auto expectedType = savedSublistsInfo\n\t\t? Section::Type::SavedSublists\n\t\t: sharedMediaInfo\n\t\t? Section::Type::Media\n\t\t: Section::Type::Profile;\n\tconst auto expectedMediaType = savedSublistsInfo\n\t\t? Section::MediaType::kCount\n\t\t: sharedMediaInfo\n\t\t? Section::MediaType::Photo\n\t\t: Section::MediaType::kCount;\n\tif (firstSectionType != expectedType\n\t\t|| firstSectionMediaType != expectedMediaType\n\t\t|| firstPeer != peer) {\n\t\tauto section = savedSublistsInfo\n\t\t\t? Section(Section::Type::SavedSublists)\n\t\t\t: sharedMediaInfo\n\t\t\t? Section(Section::MediaType::Photo)\n\t\t\t: Section(Section::Type::Profile);\n\t\tinjectActiveProfileMemento(std::move(\n\t\t\tMemento(peer, section).takeStack().front()));\n\t}\n}\n\nvoid WrapWidget::injectActiveProfileMemento(\n\t\tstd::shared_ptr<ContentMemento> memento) {\n\tauto injected = StackItem();\n\tinjected.section = std::move(memento);\n\t_historyStack.insert(\n\t\t_historyStack.begin(),\n\t\tstd::move(injected));\n\tif (_content) {\n\t\tsetupTop();\n\t\tfinishShowContent();\n\t}\n}\n\nstd::unique_ptr<Controller> WrapWidget::createController(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<ContentMemento*> memento) {\n\tauto result = std::make_unique<Controller>(\n\t\tthis,\n\t\twindow,\n\t\tmemento);\n\treturn result;\n}\n\nKey WrapWidget::key() const {\n\treturn _controller->key();\n}\n\nDialogs::RowDescriptor WrapWidget::activeChat() const {\n\tif (const auto peer = key().peer()) {\n\t\treturn Dialogs::RowDescriptor(\n\t\t\tpeer->owner().history(peer),\n\t\t\tFullMsgId());\n\t} else if (const auto storiesPeer = key().storiesPeer()) {\n\t\treturn (key().storiesAlbumId() == Stories::ArchiveId())\n\t\t\t? Dialogs::RowDescriptor()\n\t\t\t: Dialogs::RowDescriptor(\n\t\t\t\tstoriesPeer->owner().history(storiesPeer),\n\t\t\t\tFullMsgId());\n\t} else if (const auto giftsPeer = key().giftsPeer()) {\n\t\treturn Dialogs::RowDescriptor(\n\t\t\tgiftsPeer->owner().history(giftsPeer),\n\t\t\tFullMsgId());\n\t} else if (const auto musicPeer = key().musicPeer()) {\n\t\treturn Dialogs::RowDescriptor(\n\t\t\tmusicPeer->owner().history(musicPeer),\n\t\t\tFullMsgId());\n\t} else if (key().settingsSelf()\n\t\t\t|| key().isDownloads()\n\t\t\t|| key().reactionsContextId()\n\t\t\t|| key().poll()\n\t\t\t|| key().starrefPeer()\n\t\t\t|| key().statisticsTag().peer) {\n\t\treturn Dialogs::RowDescriptor();\n\t}\n\tUnexpected(\"Owner in WrapWidget::activeChat().\");\n}\n\nvoid WrapWidget::forceContentRepaint() {\n\t// WA_OpaquePaintEvent on TopBar creates render glitches when\n\t// animating the LayerWidget's height :( Fixing by repainting.\n\tif (_topBar) {\n\t\t_topBar->update();\n\t}\n\t_content->update();\n}\n\nvoid WrapWidget::setupTop() {\n\tif (HasCustomTopBar(_controller.get())\n\t\t|| wrap() == Wrap::Search\n\t\t|| wrap() == Wrap::StoryAlbumEdit) {\n\t\t_topBar.destroy();\n\t\treturn;\n\t}\n\tcreateTopBar();\n}\n\nvoid WrapWidget::createTopBar() {\n\tconst auto wrapValue = wrap();\n\tauto selectedItems = _topBar\n\t\t? _topBar->takeSelectedItems()\n\t\t: SelectedItems(Section::MediaType::kCount);\n\t_topBar.create(\n\t\tthis,\n\t\t_controller.get(),\n\t\tTopBarStyle(wrapValue),\n\t\tstd::move(selectedItems));\n\t_topBar->selectionActionRequests(\n\t) | rpl::on_next([=](SelectionAction action) {\n\t\t_content->selectionAction(action);\n\t}, _topBar->lifetime());\n\n\tif (hasBackButton()) {\n\t\t_topBar->enableBackButton();\n\t\t_topBar->backRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tcheckBeforeClose([=] { _controller->showBackFromStack(); });\n\t\t}, _topBar->lifetime());\n\t} else if (wrapValue == Wrap::Side) {\n\t\tauto close = _topBar->addButton(\n\t\t\tbase::make_unique_q<Ui::IconButton>(\n\t\t\t\t_topBar,\n\t\t\t\tst::infoTopBarClose));\n\t\tclose->setAccessibleName(tr::lng_sr_close_panel(tr::now));\n\t\tclose->addClickHandler([this] {\n\t\t\t_controller->parentController()->closeThirdSection();\n\t\t});\n\t}\n\t_topBar->storyClicks() | rpl::on_next([=] {\n\t\tif (const auto peer = _controller->key().peer()) {\n\t\t\t_controller->parentController()->openPeerStories(peer->id);\n\t\t}\n\t}, _topBar->lifetime());\n\tif (wrapValue == Wrap::Layer) {\n\t\tauto close = _topBar->addButton(\n\t\t\tbase::make_unique_q<Ui::IconButton>(\n\t\t\t\t_topBar,\n\t\t\t\tst::infoLayerTopBarClose));\n\t\tclose->setAccessibleName(tr::lng_sr_close_panel(tr::now));\n\t\tclose->addClickHandler([this] {\n\t\t\tcheckBeforeClose([=] {\n\t\t\t\t_controller->parentController()->hideSpecialLayer();\n\t\t\t});\n\t\t});\n\t} else if (requireTopBarSearch()) {\n\t\tauto search = _controller->searchFieldController();\n\t\tAssert(search != nullptr);\n\t\tsetupShortcuts();\n\t\t_topBar->createSearchView(\n\t\t\tsearch,\n\t\t\t_controller->searchEnabledByContent(),\n\t\t\t_controller->takeSearchStartsFocused());\n\t}\n\t_topBar->lower();\n\t_topBar->resizeToWidth(width());\n\t_topBar->finishAnimating();\n\t_topBar->show();\n}\n\nvoid WrapWidget::setupTopBarMenuToggle() {\n\tExpects(_content != nullptr);\n\n\tif (!_topBar) {\n\t\treturn;\n\t}\n\tconst auto key = _controller->key();\n\tconst auto section = _controller->section();\n\tif (section.type() == Section::Type::Profile\n\t\t&& (wrap() != Wrap::Side || hasStackHistory())) {\n\t\taddTopBarMenuButton();\n\t\taddProfileCallsButton();\n\t} else if (section.type() == Section::Type::Settings) {\n\t\taddTopBarMenuButton();\n\t\tif (section.settingsType() == ::Settings::MainId()) {\n\t\t\tconst auto &st = (wrap() == Wrap::Layer)\n\t\t\t\t? st::infoLayerTopBarSearch\n\t\t\t\t: st::infoTopBarSearch;\n\t\t\tconst auto button = _topBar->addButton(\n\t\t\t\tbase::make_unique_q<Ui::IconButton>(_topBar, st));\n\t\t\tbutton->setAccessibleName(tr::lng_dlg_filter(tr::now));\n\t\t\tbutton->addClickHandler([=] {\n\t\t\t\t_controller->showSettings(::Settings::Search::Id());\n\t\t\t});\n\t\t} else if (section.settingsType() == ::Settings::InformationId()) {\n\t\t\tconst auto controller = _controller->parentController();\n\t\t\tconst auto self = controller->session().user();\n\t\t\tif (!self->username().isEmpty()) {\n\t\t\t\tconst auto show = controller->uiShow();\n\t\t\t\tconst auto &st = (wrap() == Wrap::Layer)\n\t\t\t\t\t? st::infoLayerTopBarQr\n\t\t\t\t\t: st::infoTopBarQr;\n\t\t\t\tconst auto button = _topBar->addButton(\n\t\t\t\t\tbase::make_unique_q<Ui::IconButton>(_topBar, st));\n\t\t\t\tbutton->setAccessibleName(tr::lng_group_invite_context_qr(tr::now));\n\t\t\t\tbutton->addClickHandler([show, self] {\n\t\t\t\t\tUi::DefaultShowFillPeerQrBoxCallback(show, self);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tsetupShortcuts();\n\t} else if (key.storiesPeer()\n\t\t&& key.storiesPeer()->isSelf()\n\t\t&& key.storiesAlbumId() != Stories::ArchiveId()) {\n\t\tconst auto &st = (wrap() == Wrap::Layer)\n\t\t\t? st::infoLayerTopBarEdit\n\t\t\t: st::infoTopBarEdit;\n\t\tconst auto button = _topBar->addButton(\n\t\t\tbase::make_unique_q<Ui::IconButton>(_topBar, st));\n\t\tbutton->addClickHandler([=] {\n\t\t\t_controller->showSettings(::Settings::InformationId());\n\t\t});\n\t} else if (section.type() == Section::Type::Media) {\n\t\taddTopBarMenuButton();\n\t} else if (section.type() == Section::Type::Downloads) {\n\t\tauto &manager = Core::App().downloadManager();\n\t\trpl::merge(\n\t\t\trpl::single(false),\n\t\t\tmanager.loadingListChanges() | rpl::map_to(false),\n\t\t\tmanager.loadedAdded() | rpl::map_to(true),\n\t\t\tmanager.loadedRemoved() | rpl::map_to(false)\n\t\t) | rpl::on_next([=, &manager](bool definitelyHas) {\n\t\t\tconst auto has = [&] {\n\t\t\t\tfor ([[maybe_unused]] const auto id : manager.loadingList()) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tfor ([[maybe_unused]] const auto id : manager.loadedList()) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t};\n\t\t\tif (!definitelyHas && !has()) {\n\t\t\t\t_topBarMenuToggle = nullptr;\n\t\t\t} else if (!_topBarMenuToggle) {\n\t\t\t\taddTopBarMenuButton();\n\t\t\t}\n\t\t}, _topBar->lifetime());\n\t} else if (key.giftsPeer()) {\n\t\taddTopBarMenuButton();\n\t}\n}\n\nvoid WrapWidget::checkBeforeClose(Fn<void()> close) {\n\t_content->checkBeforeClose(crl::guard(this, [=] {\n\t\t_controller->parentController()->hideLayer();\n\t\tclose();\n\t}));\n}\n\nvoid WrapWidget::checkBeforeCloseByEscape(Fn<void()> close) {\n\tif (_topBar) {\n\t\t_topBar->checkBeforeCloseByEscape([&] {\n\t\t\t_content->checkBeforeCloseByEscape(crl::guard(this, [=] {\n\t\t\t\tWrapWidget::checkBeforeClose(close);\n\t\t\t}));\n\t\t});\n\t} else {\n\t\t_content->checkBeforeCloseByEscape(crl::guard(this, [=] {\n\t\t\tWrapWidget::checkBeforeClose(close);\n\t\t}));\n\t}\n}\n\nvoid WrapWidget::addTopBarMenuButton() {\n\tExpects(_topBar != nullptr);\n\tExpects(_content != nullptr);\n\n\t{\n\t\tconst auto guard = gsl::finally([&] { _topBarMenu = nullptr; });\n\t\tshowTopBarMenu(true);\n\t\tif (!_topBarMenu) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\t_topBarMenuToggle.reset(_topBar->addButton(\n\t\tbase::make_unique_q<Ui::IconButton>(\n\t\t\t_topBar,\n\t\t\t(wrap() == Wrap::Layer\n\t\t\t\t? st::infoLayerTopBarMenu\n\t\t\t\t: st::infoTopBarMenu))));\n\t_topBarMenuToggle->setAccessibleName(tr::lng_sr_profile_menu(tr::now));\n\t_topBarMenuToggle->addClickHandler([this] {\n\t\tshowTopBarMenu(false);\n\t});\n\n\tShortcuts::Requests(\n\t) | rpl::filter([=] {\n\t\treturn (_controller->section().type() == Section::Type::Profile);\n\t}) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {\n\t\tusing Command = Shortcuts::Command;\n\n\t\trequest->check(Command::ShowChatMenu, 1) && request->handle([=] {\n\t\t\tWindow::ActivateWindow(_controller->parentController());\n\t\t\tshowTopBarMenu(false);\n\t\t\treturn true;\n\t\t});\n\t}, _topBarMenuToggle->lifetime());\n}\n\nbool WrapWidget::closeByOutsideClick() const {\n\treturn _content->closeByOutsideClick();\n}\n\nvoid WrapWidget::addProfileCallsButton() {\n\tExpects(_topBar != nullptr);\n\n\tconst auto peer = key().peer();\n\tconst auto user = peer ? peer->asUser() : nullptr;\n\tif (!user || user->sharedMediaInfo() || user->isInaccessible()) {\n\t\treturn;\n\t}\n\n\tuser->session().changes().peerFlagsValue(\n\t\tuser,\n\t\tData::PeerUpdate::Flag::HasCalls\n\t) | rpl::filter([=] {\n\t\treturn user->hasCalls();\n\t}) | rpl::take(\n\t\t1\n\t) | rpl::on_next([=] {\n\t\t_topBar->addButton(\n\t\t\tbase::make_unique_q<Ui::IconButton>(\n\t\t\t\t_topBar,\n\t\t\t\t(wrap() == Wrap::Layer\n\t\t\t\t\t? st::infoLayerTopBarCall\n\t\t\t\t\t: st::infoTopBarCall))\n\t\t)->addClickHandler([=] {\n\t\t\tCore::App().calls().startOutgoingCall(user, {});\n\t\t});\n\t}, _topBar->lifetime());\n\n\tif (user && user->callsStatus() == UserData::CallsStatus::Unknown) {\n\t\tuser->updateFull();\n\t}\n}\n\nvoid WrapWidget::showTopBarMenu(bool check) {\n\tif (_topBarMenu) {\n\t\t_topBarMenu->hideMenu(true);\n\t\treturn;\n\t}\n\t_topBarMenu = base::make_unique_q<Ui::PopupMenu>(\n\t\tQWidget::window(),\n\t\tst::popupMenuExpandedSeparator);\n\n\t_topBarMenu->setDestroyedCallback([this] {\n\t\tInvokeQueued(this, [this] { _topBarMenu = nullptr; });\n\t\tif (auto toggle = _topBarMenuToggle.get()) {\n\t\t\ttoggle->setForceRippled(false);\n\t\t}\n\t});\n\n\t_content->fillTopBarMenu(Ui::Menu::CreateAddActionCallback(_topBarMenu));\n\tif (_topBarMenu->empty()) {\n\t\t_topBarMenu = nullptr;\n\t\treturn;\n\t} else if (check) {\n\t\treturn;\n\t}\n\t_topBarMenu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);\n\t_topBarMenuToggle->setForceRippled(true);\n\t_topBarMenu->popup(Ui::PopupMenu::ConstrainToParentScreen(\n\t\t_topBarMenu,\n\t\t_topBarMenuToggle->mapToGlobal(st::infoLayerTopBarMenuPosition)));\n}\n\nbool WrapWidget::requireTopBarSearch() const {\n\tif (!_topBar\n\t\t|| !_controller->searchFieldController()\n\t\t|| (_controller->wrap() == Wrap::Layer)\n\t\t|| (_controller->section().type() == Section::Type::Profile)\n\t\t|| key().isDownloads()) {\n\t\treturn false;\n\t} else if (hasStackHistory()\n\t\t|| _controller->section().type() == Section::Type::RequestsList) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool WrapWidget::showBackFromStackInternal(\n\t\tconst Window::SectionShow &params) {\n\tif (hasStackHistory()) {\n\t\tauto last = std::move(_historyStack.back());\n\t\t_historyStack.pop_back();\n\t\tshowNewContent(\n\t\t\tlast.section.get(),\n\t\t\tparams.withWay(Window::SectionShow::Way::Backward));\n\t\treturn true;\n\t}\n\treturn (wrap() == Wrap::Layer);\n}\n\nvoid WrapWidget::removeFromStack(const std::vector<Section> &sections) {\n\tfor (const auto &section : sections) {\n\t\tconst auto it = ranges::find_if(_historyStack, [&](\n\t\t\t\tconst StackItem &item) {\n\t\t\tconst auto &s = item.section->section();\n\t\t\tif (s.type() != section.type()) {\n\t\t\t\treturn false;\n\t\t\t} else if (s.type() == Section::Type::SavedSublists) {\n\t\t\t\treturn true;\n\t\t\t} else if (s.type() == Section::Type::Media) {\n\t\t\t\treturn (s.mediaType() == section.mediaType());\n\t\t\t} else if (s.type() == Section::Type::Settings) {\n\t\t\t\treturn (s.settingsType() == section.settingsType());\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t\tif (it != end(_historyStack)) {\n\t\t\t_historyStack.erase(it);\n\t\t}\n\t}\n}\n\nnot_null<Ui::RpWidget*> WrapWidget::topWidget() const {\n\treturn _topBar;\n}\n\nvoid WrapWidget::showContent(object_ptr<ContentWidget> content) {\n\tif (auto old = std::exchange(_content, std::move(content))) {\n\t\tif (Ui::InFocusChain(old)) {\n\t\t\t// Prevent activating dialogs filter field while animating.\n\t\t\tsetFocus();\n\t\t}\n\t\told->hide();\n\n\t\t// Content destructor may invoke closeBox() that will try to\n\t\t// start layer animation. If we won't detach old content from\n\t\t// its parent WrapWidget layer animation will be started with a\n\t\t// partially destructed grand-child widget and result in a crash.\n\t\told->setParent(nullptr);\n\t\told.destroy();\n\t}\n\t_additionalScroll = 0;\n\t_content->show();\n\tfinishShowContent();\n}\n\nvoid WrapWidget::finishShowContent() {\n\tsetupTopBarMenuToggle();\n\tupdateContentGeometry();\n\t_content->setIsStackBottom(!hasStackHistory());\n\tif (_topBar) {\n\t\t_topBar->setTitle({\n\t\t\t.title = _content->title(),\n\t\t\t.subtitle = _content->subtitle(),\n\t\t});\n\t\t_topBar->setStories(_content->titleStories());\n\t}\n\t_desiredHeights.fire(desiredHeightForContent());\n\t_desiredShadowVisibilities.fire(_content->desiredShadowVisibility());\n\t_desiredBottomShadowVisibilities.fire(\n\t\t_content->desiredBottomShadowVisibility());\n\tif (auto selection = _content->selectedListValue()) {\n\t\t_selectedLists.fire(std::move(selection));\n\t} else {\n\t\t_selectedLists.fire(rpl::single(\n\t\t\tSelectedItems(Storage::SharedMediaType::Photo)));\n\t}\n\t_scrollTillBottomChanges.fire(_content->scrollTillBottomChanges());\n\t_topShadow->raise();\n\t_topShadow->finishAnimating();\n\t_bottomShadow->raise();\n\t_bottomShadow->finishAnimating();\n\t_contentChanges.fire({});\n\n\t_content->scrollBottomSkipValue(\n\t) | rpl::on_next([=] {\n\t\tupdateContentGeometry();\n\t}, _content->lifetime());\n}\n\nrpl::producer<bool> WrapWidget::topShadowToggledValue() const {\n\treturn _desiredShadowVisibilities.events()\n\t\t| rpl::flatten_latest(\n\t\t) | rpl::map([=](bool v) { return v && (_topBar != nullptr); });\n}\n\nrpl::producer<int> WrapWidget::desiredHeightForContent() const {\n\tusing namespace rpl::mappers;\n\treturn rpl::single(0) | rpl::then(rpl::combine(\n\t\t_content->desiredHeightValue(),\n\t\t(_topBar ? _topBar->heightValue() : rpl::single(0)),\n\t\t_1 + _2));\n}\n\nrpl::producer<SelectedItems> WrapWidget::selectedListValue() const {\n\tauto current = _content\n\t\t? _content->selectedListValue()\n\t\t: nullptr;\n\treturn _selectedLists.events_starting_with(current\n\t\t? std::move(current)\n\t\t: rpl::single(SelectedItems(Storage::SharedMediaType::Photo))\n\t) | rpl::flatten_latest();\n}\n\nobject_ptr<ContentWidget> WrapWidget::createContent(\n\t\tnot_null<ContentMemento*> memento,\n\t\tnot_null<Controller*> controller) {\n\treturn memento->createWidget(\n\t\tthis,\n\t\tcontroller,\n\t\tcontentGeometry());\n}\n\nrpl::producer<Wrap> WrapWidget::wrapValue() const {\n\treturn _wrap.value();\n}\n\nvoid WrapWidget::setWrap(Wrap wrap) {\n\t_wrap = wrap;\n}\n\nrpl::producer<> WrapWidget::contentChanged() const {\n\treturn _contentChanges.events();\n}\n\nbool WrapWidget::hasTopBarShadow() const {\n\treturn _topShadow->toggled();\n}\n\nQPixmap WrapWidget::grabForShowAnimation(\n\t\tconst Window::SectionSlideParams &params) {\n\tif (params.withTopBarShadow) {\n\t\t_topShadow->setVisible(false);\n\t} else {\n\t\t_topShadow->setVisible(_topShadow->toggled());\n\t}\n\tconst auto expanding = _expanding;\n\tif (expanding) {\n\t\t_grabbingForExpanding = true;\n\t}\n\tauto result = Ui::GrabWidget(this);\n\tif (expanding) {\n\t\t_grabbingForExpanding = false;\n\t}\n\tif (params.withTopBarShadow) {\n\t\t_topShadow->setVisible(true);\n\t}\n\treturn result;\n}\n\nvoid WrapWidget::showAnimatedHook(\n\t\tconst Window::SectionSlideParams &params) {\n\tif (params.withTopBarShadow) {\n\t\t_topShadow->setVisible(true);\n\t}\n\t_topBarSurrogate = createTopBarSurrogate(this);\n}\n\nvoid WrapWidget::doSetInnerFocus() {\n\tif (!_topBar || !_topBar->focusSearchField()) {\n\t\t_content->setInnerFocus();\n\t}\n}\n\nvoid WrapWidget::showFinishedHook() {\n\t// Restore shadow visibility after showChildren() call.\n\t_topShadow->toggle(_topShadow->toggled(), anim::type::instant);\n\t_bottomShadow->toggle(_bottomShadow->toggled(), anim::type::instant);\n\t_topBarSurrogate.destroy();\n\t_content->showFinished();\n\n\tif (_topBarMenuToggle\n\t\t&& _controller->section().type() == Section::Type::Settings) {\n\t\tconst auto controller = _controller->parentController();\n\t\tconst auto settingsType = _controller->section().settingsType();\n\t\tconst auto highlightId = [&]() -> QString {\n\t\t\tif (settingsType == ::Settings::MainId()) {\n\t\t\t\treturn u\"settings/log-out\"_q;\n\t\t\t} else if (settingsType == ::Settings::ChatId()) {\n\t\t\t\treturn u\"chat/themes-create\"_q;\n\t\t\t}\n\t\t\treturn QString();\n\t\t}();\n\t\tif (!highlightId.isEmpty()\n\t\t\t&& controller->takeHighlightControlId(highlightId)) {\n\t\t\tshowTopBarMenu(false);\n\t\t\tif (_topBarMenu) {\n\t\t\t\tconst auto menu = _topBarMenu->menu();\n\t\t\t\tfor (const auto &action : menu->actions()) {\n\t\t\t\t\tconst auto controlId = \"highlight-control-id\";\n\t\t\t\t\tif (action->property(controlId).toString() == highlightId) {\n\t\t\t\t\t\tif (const auto item = menu->itemForAction(action)) {\n\t\t\t\t\t\t\t::Settings::HighlightWidget(item);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nbool WrapWidget::showInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) {\n\tif (auto infoMemento = dynamic_cast<Memento*>(memento.get())) {\n\t\tif (!_controller || infoMemento->stackSize() > 1) {\n\t\t\treturn false;\n\t\t}\n\t\tauto content = infoMemento->content();\n\t\tauto skipInternal = hasStackHistory()\n\t\t\t&& (params.way == Window::SectionShow::Way::ClearStack);\n\t\tif (_controller->validateMementoPeer(content)) {\n\t\t\tif (!skipInternal && _content->showInternal(content)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\t// If we're in a nested section and we're asked to show\n\t\t// a chat profile that is at the bottom of the stack we'll\n\t\t// just go back in the stack all the way instead of pushing.\n\t\tif (returnToFirstStackFrame(content, params)) {\n\t\t\treturn true;\n\t\t}\n\n\t\tshowNewContent(content, params);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid WrapWidget::highlightTopBar() {\n\tif (_topBar) {\n\t\t_topBar->highlight();\n\t}\n}\n\nstd::shared_ptr<Window::SectionMemento> WrapWidget::createMemento() {\n\tauto stack = std::vector<std::shared_ptr<ContentMemento>>();\n\tstack.reserve(_historyStack.size() + 1);\n\tfor (auto &stackItem : base::take(_historyStack)) {\n\t\tstack.push_back(std::move(stackItem.section));\n\t}\n\tstack.push_back(_content->createMemento());\n\n\t// We're not in valid state anymore and supposed to be destroyed.\n\t_controller = nullptr;\n\n\treturn std::make_shared<Memento>(std::move(stack));\n}\n\nrpl::producer<int> WrapWidget::desiredHeightValue() const {\n\treturn _desiredHeights.events_starting_with(desiredHeightForContent())\n\t\t| rpl::flatten_latest();\n}\n\nQRect WrapWidget::contentGeometry() const {\n\tconst auto top = _topBar ? _topBar->height() : 0;\n\treturn rect().marginsRemoved({ 0, std::min(top, height()), 0, 0});\n}\n\nbool WrapWidget::returnToFirstStackFrame(\n\t\tnot_null<ContentMemento*> memento,\n\t\tconst Window::SectionShow &params) {\n\tif (!hasStackHistory()) {\n\t\treturn false;\n\t}\n\tauto firstPeer = _historyStack.front().section->peer();\n\tauto firstSection = _historyStack.front().section->section();\n\tif (firstPeer == memento->peer()\n\t\t&& firstSection.type() == memento->section().type()\n\t\t&& firstSection.type() == Section::Type::Profile) {\n\t\t_historyStack.resize(1);\n\t\t_controller->showBackFromStack();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid WrapWidget::showNewContent(\n\t\tnot_null<ContentMemento*> memento,\n\t\tconst Window::SectionShow &params) {\n\tconst auto saveToStack = (_content != nullptr)\n\t\t&& (params.way == Window::SectionShow::Way::Forward);\n\tconst auto needAnimation = (_content != nullptr)\n\t\t&& (params.animated != anim::type::instant);\n\tauto animationParams = SectionSlideParams();\n\tauto newController = createController(\n\t\t_controller->parentController(),\n\t\tmemento);\n\tif (_controller && newController) {\n\t\tnewController->takeStepData(_controller.get());\n\t}\n\tauto newContent = object_ptr<ContentWidget>(nullptr);\n\tconst auto withBackButton = willHaveBackButton(params);\n\tconst auto createInAdvance = needAnimation || withBackButton;\n\tif (createInAdvance) {\n\t\tnewContent = createContent(memento, newController.get());\n\t}\n\tif (needAnimation) {\n\t\tanimationParams.withTopBarShadow = hasTopBarShadow()\n\t\t\t&& newContent->hasTopBarShadow();\n\t\tanimationParams.oldContentCache = grabForShowAnimation(\n\t\t\tanimationParams);\n\t\tconst auto layer = (wrap() == Wrap::Layer);\n\t\tanimationParams.withFade = layer;\n\t\tanimationParams.topSkip = layer ? st::boxRadius : 0;\n\n\t\tif (HasCustomTopBar(_controller.get())\n\t\t\t|| HasCustomTopBar(newController.get())) {\n\t\t\tconst auto s = QSize(\n\t\t\t\tnewContent->width(),\n\t\t\t\tanimationParams.topSkip);\n\t\t\tauto image = Ui::RippleAnimation::MaskByDrawer(s, false, [&](\n\t\t\t\t\tQPainter &p) {\n\t\t\t\tconst auto r = QRect(0, 0, s.width(), s.height() * 2);\n\t\t\t\tp.drawRoundedRect(r, st::boxRadius, st::boxRadius);\n\t\t\t});\n\t\t\tanimationParams.topMask = Ui::PixmapFromImage(std::move(image));\n\t\t}\n\t}\n\tif (saveToStack) {\n\t\tauto item = StackItem();\n\t\titem.section = _content->createMemento();\n\t\t_historyStack.push_back(std::move(item));\n\t} else if (params.way == Window::SectionShow::Way::ClearStack) {\n\t\t_historyStack.clear();\n\t}\n\n\tif (withBackButton) {\n\t\tnewContent->enableBackButton();\n\t}\n\n\t{\n\t\t// Let old controller outlive old content widget.\n\t\tconst auto oldController = std::exchange(\n\t\t\t_controller,\n\t\t\tstd::move(newController));\n\t\tif (newContent) {\n\t\t\tsetupTop();\n\t\t\tshowContent(std::move(newContent));\n\t\t} else {\n\t\t\tshowNewContent(memento);\n\t\t}\n\t}\n\n\tif (animationParams) {\n\t\tif (Ui::InFocusChain(this)) {\n\t\t\tsetFocus();\n\t\t}\n\t\tshowAnimated(\n\t\t\tsaveToStack\n\t\t\t\t? SlideDirection::FromRight\n\t\t\t\t: SlideDirection::FromLeft,\n\t\t\tanimationParams);\n\t}\n}\n\nvoid WrapWidget::showNewContent(not_null<ContentMemento*> memento) {\n\t// Validates contentGeometry().\n\tsetupTop();\n\tauto newContent = createContent(memento, _controller.get());\n\tif (!_topBar && hasBackButton()) {\n\t\tnewContent->enableBackButton();\n\t}\n\tshowContent(std::move(newContent));\n}\n\nvoid WrapWidget::resizeEvent(QResizeEvent *e) {\n\tif (_topBar) {\n\t\t_topBar->resizeToWidth(width());\n\t}\n\tupdateContentGeometry();\n}\n\nvoid WrapWidget::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Escape || e->key() == Qt::Key_Back) {\n\t\tcheckBeforeCloseByEscape((hasStackHistory() || wrap() != Wrap::Layer)\n\t\t\t? Fn<void()>([=] { _controller->showBackFromStack(); })\n\t\t\t: Fn<void()>([=] {\n\t\t\t\t_controller->parentController()->hideSpecialLayer();\n\t\t\t}));\n\t\treturn;\n\t}\n\tSectionWidget::keyPressEvent(e);\n}\n\nvoid WrapWidget::updateContentGeometry() {\n\tif (_content) {\n\t\tif (_topBar) {\n\t\t\t_topShadow->resizeToWidth(width());\n\t\t\t_topShadow->moveToLeft(0, _topBar->height());\n\t\t}\n\t\t_content->setGeometry(contentGeometry());\n\t\t_bottomShadow->resizeToWidth(width());\n\t\t_bottomShadow->moveToLeft(\n\t\t\t0,\n\t\t\t_content->y()\n\t\t\t\t+ _content->height()\n\t\t\t\t- _content->scrollBottomSkip());\n\t}\n}\n\nbool WrapWidget::floatPlayerHandleWheelEvent(QEvent *e) {\n\treturn _content->floatPlayerHandleWheelEvent(e);\n}\n\nQRect WrapWidget::floatPlayerAvailableRect() {\n\treturn _content->floatPlayerAvailableRect();\n}\n\nobject_ptr<Ui::RpWidget> WrapWidget::createTopBarSurrogate(\n\t\tQWidget *parent) {\n\tif (_topBar && hasBackButton()) {\n\t\tAssert(_topBar != nullptr);\n\n\t\tauto result = object_ptr<Ui::AbstractButton>(parent);\n\t\tresult->addClickHandler([weak = base::make_weak(this)]{\n\t\t\tif (weak) {\n\t\t\t\tweak->_controller->showBackFromStack();\n\t\t\t}\n\t\t});\n\t\tresult->setGeometry(_topBar->geometry());\n\t\tresult->show();\n\t\treturn result;\n\t}\n\treturn nullptr;\n}\n\nvoid WrapWidget::updateGeometry(\n\t\tQRect newGeometry,\n\t\tbool expanding,\n\t\tint additionalScroll,\n\t\tint maxVisibleHeight) {\n\tauto scrollChanged = (_additionalScroll != additionalScroll);\n\tauto geometryChanged = (geometry() != newGeometry);\n\tauto shrinkingContent = (additionalScroll < _additionalScroll);\n\t_additionalScroll = additionalScroll;\n\t_maxVisibleHeight = maxVisibleHeight;\n\t_expanding = expanding;\n\n\t_content->applyMaxVisibleHeight(maxVisibleHeight);\n\n\tif (geometryChanged) {\n\t\tif (shrinkingContent) {\n\t\t\tsetGeometry(newGeometry);\n\t\t}\n\t\tif (scrollChanged) {\n\t\t\t_content->applyAdditionalScroll(additionalScroll);\n\t\t}\n\t\tif (!shrinkingContent) {\n\t\t\tsetGeometry(newGeometry);\n\t\t}\n\t} else if (scrollChanged) {\n\t\t_content->applyAdditionalScroll(additionalScroll);\n\t}\n}\n\nint WrapWidget::scrollTillBottom(int forHeight) const {\n\treturn _content->scrollTillBottom(forHeight\n\t\t- (_topBar ? _topBar->height() : 0));\n}\n\nint WrapWidget::scrollBottomSkip() const {\n\treturn _content->scrollBottomSkip();\n}\n\nrpl::producer<int> WrapWidget::scrollTillBottomChanges() const {\n\treturn _scrollTillBottomChanges.events_starting_with(\n\t\t_content->scrollTillBottomChanges()\n\t) | rpl::flatten_latest();\n}\n\nrpl::producer<bool> WrapWidget::grabbingForExpanding() const {\n\treturn _grabbingForExpanding.value();\n}\n\nconst Ui::RoundRect *WrapWidget::bottomSkipRounding() const {\n\treturn _content->bottomSkipRounding();\n}\n\nbool WrapWidget::hasBackButton() const {\n\treturn !_isSeparatedWindow\n\t\t&& (wrap() == Wrap::Narrow || hasStackHistory());\n}\n\nbool WrapWidget::willHaveBackButton(\n\t\tconst Window::SectionShow &params) const {\n\tusing Way = Window::SectionShow::Way;\n\tconst auto willSaveToStack = (_content != nullptr)\n\t\t&& (params.way == Way::Forward);\n\tconst auto willClearStack = (params.way == Way::ClearStack);\n\tconst auto willHaveStack = !willClearStack\n\t\t&& (hasStackHistory() || willSaveToStack);\n\treturn (wrap() == Wrap::Narrow) || willHaveStack;\n}\n\nvoid WrapWidget::replaceSwipeHandler(\n\t\tUi::Controls::SwipeHandlerArgs *incompleteArgs) {\n\t_content->replaceSwipeHandler(std::move(incompleteArgs));\n}\n\nWrapWidget::~WrapWidget() = default;\n\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/info_wrap_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"window/section_widget.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace Storage {\nenum class SharedMediaType : signed char;\n} // namespace Storage\n\nnamespace Ui {\nnamespace Controls {\nstruct SwipeHandlerArgs;\n} // namespace Controls\nclass FadeShadow;\nclass PlainShadow;\nclass PopupMenu;\nclass IconButton;\nclass RoundRect;\nstruct StringWithNumbers;\n} // namespace Ui\n\nnamespace Window {\nenum class SlideDirection;\n} // namespace Window\n\nnamespace Info {\nnamespace Profile {\nclass Widget;\n} // namespace Profile\n\nnamespace Media {\nclass Widget;\n} // namespace Media\n\nclass Key;\nclass Controller;\nclass Section;\nclass Memento;\nclass MoveMemento;\nclass ContentMemento;\nclass ContentWidget;\nclass TopBar;\n\nenum class Wrap {\n\tLayer,\n\tNarrow,\n\tSide,\n\tSearch,\n\tStoryAlbumEdit,\n};\n\nstruct SelectedItem {\n\texplicit SelectedItem(GlobalMsgId globalId) : globalId(globalId) {\n\t}\n\n\tGlobalMsgId globalId;\n\tbool canDelete = false;\n\tbool canForward = false;\n\tbool canToggleStoryPin = false;\n\tbool canUnpinStory = false;\n\tbool storyInProfile = false;\n};\n\nstruct SelectedItems {\n\tSelectedItems() = default;\n\texplicit SelectedItems(Storage::SharedMediaType type);\n\n\tFn<Ui::StringWithNumbers(int)> title;\n\tstd::vector<SelectedItem> list;\n};\n\nenum class SelectionAction {\n\tClear,\n\tForward,\n\tDelete,\n\tToggleStoryPin,\n\tToggleStoryToProfile,\n\tToggleStoryToArchive,\n};\n\nclass WrapWidget final : public Window::SectionWidget {\npublic:\n\tWrapWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> window,\n\t\tWrap wrap,\n\t\tnot_null<Memento*> memento);\n\n\tKey key() const;\n\tDialogs::RowDescriptor activeChat() const override;\n\tWrap wrap() const {\n\t\treturn _wrap.current();\n\t}\n\trpl::producer<Wrap> wrapValue() const;\n\tvoid setWrap(Wrap wrap);\n\n\trpl::producer<> contentChanged() const;\n\n\tnot_null<Controller*> controller() {\n\t\treturn _controller.get();\n\t}\n\n\tbool hasTopBarShadow() const override;\n\tQPixmap grabForShowAnimation(\n\t\tconst Window::SectionSlideParams &params) override;\n\n\tvoid forceContentRepaint();\n\n\tbool showInternal(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) override;\n\tbool showBackFromStackInternal(const Window::SectionShow &params);\n\tvoid removeFromStack(const std::vector<Section> &sections);\n\tstd::shared_ptr<Window::SectionMemento> createMemento() override;\n\n\trpl::producer<int> desiredHeightValue() const override;\n\n\t// Float player interface.\n\tbool floatPlayerHandleWheelEvent(QEvent *e) override;\n\tQRect floatPlayerAvailableRect() override;\n\n\tobject_ptr<Ui::RpWidget> createTopBarSurrogate(QWidget *parent);\n\n\t[[nodiscard]] bool hasBackButton() const;\n\t[[nodiscard]] bool closeByOutsideClick() const;\n\n\tvoid updateGeometry(\n\t\tQRect newGeometry,\n\t\tbool expanding,\n\t\tint additionalScroll,\n\t\tint maxVisibleHeight);\n\t[[nodiscard]] int scrollBottomSkip() const;\n\t[[nodiscard]] int scrollTillBottom(int forHeight) const;\n\t[[nodiscard]] rpl::producer<int> scrollTillBottomChanges() const;\n\t[[nodiscard]] rpl::producer<bool> grabbingForExpanding() const;\n\t[[nodiscard]] const Ui::RoundRect *bottomSkipRounding() const;\n\n\t[[nodiscard]] rpl::producer<> removeRequests() const override {\n\t\treturn _removeRequests.events();\n\t}\n\n\t[[nodiscard]] rpl::producer<SelectedItems> selectedListValue() const;\n\n\tvoid replaceSwipeHandler(Ui::Controls::SwipeHandlerArgs *incompleteArgs);\n\n\t~WrapWidget();\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\n\tvoid doSetInnerFocus() override;\n\tvoid showFinishedHook() override;\n\n\tvoid showAnimatedHook(\n\t\tconst Window::SectionSlideParams &params) override;\n\nprivate:\n\tusing SlideDirection = Window::SlideDirection;\n\tusing SectionSlideParams = Window::SectionSlideParams;\n\tstruct StackItem;\n\n\tvoid startInjectingActivePeerProfiles();\n\tvoid injectActiveProfile(Dialogs::Key key);\n\tvoid injectActivePeerProfile(not_null<PeerData*> peer);\n\tvoid injectActiveProfileMemento(\n\t\tstd::shared_ptr<ContentMemento> memento);\n\tvoid checkBeforeClose(Fn<void()> close);\n\tvoid checkBeforeCloseByEscape(Fn<void()> close);\n\tvoid restoreHistoryStack(\n\t\tstd::vector<std::shared_ptr<ContentMemento>> stack);\n\tbool hasStackHistory() const {\n\t\treturn !_historyStack.empty();\n\t}\n\tvoid showNewContent(not_null<ContentMemento*> memento);\n\tvoid showNewContent(\n\t\tnot_null<ContentMemento*> memento,\n\t\tconst Window::SectionShow &params);\n\tbool returnToFirstStackFrame(\n\t\tnot_null<ContentMemento*> memento,\n\t\tconst Window::SectionShow &params);\n\tvoid setupTop();\n\tvoid setupTopBarMenuToggle();\n\tvoid createTopBar();\n\tvoid highlightTopBar();\n\tvoid setupShortcuts();\n\n\t[[nodiscard]] bool willHaveBackButton(\n\t\tconst Window::SectionShow &params) const;\n\n\tnot_null<RpWidget*> topWidget() const;\n\n\tQRect contentGeometry() const;\n\trpl::producer<int> desiredHeightForContent() const;\n\tvoid finishShowContent();\n\trpl::producer<bool> topShadowToggledValue() const;\n\tvoid updateContentGeometry();\n\n\tvoid showContent(object_ptr<ContentWidget> content);\n\tobject_ptr<ContentWidget> createContent(\n\t\tnot_null<ContentMemento*> memento,\n\t\tnot_null<Controller*> controller);\n\tstd::unique_ptr<Controller> createController(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<ContentMemento*> memento);\n\n\tbool requireTopBarSearch() const;\n\n\tvoid addTopBarMenuButton();\n\tvoid addProfileCallsButton();\n\tvoid showTopBarMenu(bool check);\n\n\tconst bool _isSeparatedWindow = false;\n\n\trpl::variable<Wrap> _wrap;\n\tstd::unique_ptr<Controller> _controller;\n\tobject_ptr<ContentWidget> _content = { nullptr };\n\tint _additionalScroll = 0;\n\tint _maxVisibleHeight = 0;\n\tbool _expanding = false;\n\trpl::variable<bool> _grabbingForExpanding = false;\n\tobject_ptr<TopBar> _topBar = { nullptr };\n\tobject_ptr<Ui::RpWidget> _topBarSurrogate = { nullptr };\n\tUi::Animations::Simple _topBarOverrideAnimation;\n\n\tobject_ptr<Ui::FadeShadow> _topShadow;\n\tobject_ptr<Ui::FadeShadow> _bottomShadow;\n\tbase::unique_qptr<Ui::IconButton> _topBarMenuToggle;\n\tbase::unique_qptr<Ui::PopupMenu> _topBarMenu;\n\n\tstd::vector<StackItem> _historyStack;\n\trpl::event_stream<> _removeRequests;\n\n\trpl::event_stream<rpl::producer<int>> _desiredHeights;\n\trpl::event_stream<rpl::producer<bool>> _desiredShadowVisibilities;\n\trpl::event_stream<rpl::producer<bool>> _desiredBottomShadowVisibilities;\n\trpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;\n\trpl::event_stream<rpl::producer<int>> _scrollTillBottomChanges;\n\trpl::event_stream<> _contentChanges;\n\n};\n\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_buttons.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/media/info_media_buttons.h\"\n\n#include \"base/call_delayed.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"core/application.h\"\n#include \"core/ui_integration.h\"\n#include \"data/components/recent_shared_media_gifts.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories_ids.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_chat_section.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"info/peer_gifts/info_peer_gifts_widget.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"info/saved/info_saved_music_widget.h\"\n#include \"info/stories/info_stories_widget.h\"\n#include \"main/main_session.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_separate_id.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Info::Media {\nnamespace {\n\n[[nodiscard]] bool SeparateSupported(Storage::SharedMediaType type) {\n\tusing Type = Storage::SharedMediaType;\n\treturn (type == Type::Photo)\n\t\t|| (type == Type::Video)\n\t\t|| (type == Type::File)\n\t\t|| (type == Type::MusicFile)\n\t\t|| (type == Type::Link)\n\t\t|| (type == Type::RoundVoiceFile)\n\t\t|| (type == Type::GIF)\n\t\t|| (type == Type::Poll);\n}\n\n[[nodiscard]] Window::SeparateId SeparateId(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tStorage::SharedMediaType type) {\n\tif (peer->isSelf() || !SeparateSupported(type)) {\n\t\treturn { nullptr };\n\t}\n\tconst auto topic = topicRootId\n\t\t? peer->forumTopicFor(topicRootId)\n\t\t: nullptr;\n\tif (topicRootId && !topic) {\n\t\treturn { nullptr };\n\t}\n\tconst auto thread = topic\n\t\t\t? (Data::Thread*)topic\n\t\t: peer->owner().history(peer);\n\treturn { thread, type };\n}\n\nvoid AddContextMenuToButton(\n\t\tnot_null<Ui::AbstractButton*> button,\n\t\tFn<void()> openInWindow) {\n\tif (!openInWindow) {\n\t\treturn;\n\t}\n\tbutton->setAcceptBoth();\n\tstruct State final {\n\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t};\n\tconst auto state = button->lifetime().make_state<State>();\n\tbutton->addClickHandler([=](Qt::MouseButton mouse) {\n\t\tif (mouse != Qt::RightButton) {\n\t\t\treturn;\n\t\t}\n\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tbutton.get(),\n\t\t\tst::popupMenuWithIcons);\n\t\tstate->menu->addAction(tr::lng_context_new_window(tr::now), [=] {\n\t\t\tbase::call_delayed(\n\t\t\t\tst::popupMenuWithIcons.showDuration,\n\t\t\t\tcrl::guard(button, openInWindow));\n\t\t\t}, &st::menuIconNewWindow);\n\t\tstate->menu->popup(QCursor::pos());\n\t});\n}\n\n} // namespace\n\ntr::phrase<lngtag_count> MediaTextPhrase(Type type) {\n\tswitch (type) {\n\tcase Type::Photo: return tr::lng_profile_photos;\n\tcase Type::GIF: return tr::lng_profile_gifs;\n\tcase Type::Video: return tr::lng_profile_videos;\n\tcase Type::File: return tr::lng_profile_files;\n\tcase Type::MusicFile: return tr::lng_profile_songs;\n\tcase Type::Link: return tr::lng_profile_shared_links;\n\tcase Type::RoundVoiceFile: return tr::lng_profile_audios;\n\tcase Type::Poll: return tr::lng_profile_polls;\n\t}\n\tUnexpected(\"Type in MediaTextPhrase()\");\n};\n\nFn<QString(int)> MediaText(Type type) {\n\treturn [phrase = MediaTextPhrase(type)](int count) {\n\t\treturn phrase(tr::now, lt_count, count);\n\t};\n}\n\nnot_null<Ui::SlideWrap<Ui::SettingsButton>*> AddCountedButton(\n\t\tUi::VerticalLayout *parent,\n\t\trpl::producer<int> &&count,\n\t\tFn<QString(int)> &&textFromCount,\n\t\tUi::MultiSlideTracker &tracker) {\n\tusing namespace ::Settings;\n\tauto forked = std::move(count)\n\t\t| start_spawning(parent->lifetime());\n\tauto text = rpl::duplicate(\n\t\tforked\n\t) | rpl::map([textFromCount](int count) {\n\t\treturn (count > 0)\n\t\t\t? textFromCount(count)\n\t\t\t: QString();\n\t});\n\tauto button = parent->add(object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\tparent,\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\tparent,\n\t\t\tstd::move(text),\n\t\t\tst::infoSharedMediaButton))\n\t)->setDuration(\n\t\tst::infoSlideDuration\n\t)->toggleOn(\n\t\trpl::duplicate(forked) | rpl::map(rpl::mappers::_1 > 0)\n\t);\n\ttracker.track(button);\n\treturn button;\n};\n\nnot_null<Ui::SettingsButton*> AddButton(\n\t\tUi::VerticalLayout *parent,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tPeerData *migrated,\n\t\tType type,\n\t\tUi::MultiSlideTracker &tracker) {\n\tauto result = AddCountedButton(\n\t\tparent,\n\t\tProfile::SharedMediaCountValue(\n\t\t\tpeer,\n\t\t\ttopicRootId,\n\t\t\tmonoforumPeerId,\n\t\t\tmigrated,\n\t\t\ttype),\n\t\tMediaText(type),\n\t\ttracker)->entity();\n\tconst auto separateId = SeparateId(peer, topicRootId, type);\n\tconst auto openInWindow = separateId\n\t\t? [=] { navigation->parentController()->showInNewWindow(separateId); }\n\t\t: Fn<void()>(nullptr);\n\tUi::InstallTooltip(result, [=] {\n\t\treturn Platform::IsMac()\n\t\t\t? tr::lng_new_window_tooltip_cmd(tr::now)\n\t\t\t: tr::lng_new_window_tooltip_ctrl(tr::now);\n\t});\n\tAddContextMenuToButton(result, openInWindow);\n\tresult->addClickHandler([=](Qt::MouseButton mouse) {\n\t\tif (mouse == Qt::RightButton) {\n\t\t\treturn;\n\t\t}\n\t\tif (openInWindow\n\t\t\t&& (base::IsCtrlPressed() || mouse == Qt::MiddleButton)) {\n\t\t\treturn openInWindow();\n\t\t}\n\t\tconst auto topic = topicRootId\n\t\t\t? peer->forumTopicFor(topicRootId)\n\t\t\t: nullptr;\n\t\tif (topicRootId && !topic) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto separateId = SeparateId(peer, topicRootId, type);\n\t\tif (Core::App().separateWindowFor(separateId) && openInWindow) {\n\t\t\topenInWindow();\n\t\t} else {\n\t\t\tnavigation->showSection(topicRootId\n\t\t\t\t? std::make_shared<Info::Memento>(topic, Section(type))\n\t\t\t\t: std::make_shared<Info::Memento>(peer, Section(type)));\n\t\t}\n\t});\n\treturn result;\n};\n\nnot_null<Ui::SettingsButton*> AddCommonGroupsButton(\n\t\tUi::VerticalLayout *parent,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<UserData*> user,\n\t\tUi::MultiSlideTracker &tracker) {\n\tauto result = AddCountedButton(\n\t\tparent,\n\t\tProfile::CommonGroupsCountValue(user),\n\t\t[](int count) {\n\t\t\treturn tr::lng_profile_common_groups(tr::now, lt_count, count);\n\t\t},\n\t\ttracker)->entity();\n\tresult->addClickHandler([=] {\n\t\tnavigation->showSection(\n\t\t\tstd::make_shared<Info::Memento>(\n\t\t\t\tuser,\n\t\t\t\tSection::Type::CommonGroups));\n\t});\n\treturn result;\n}\n\nnot_null<Ui::SettingsButton*> AddSimilarPeersButton(\n\t\tUi::VerticalLayout *parent,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tUi::MultiSlideTracker &tracker) {\n\tauto result = AddCountedButton(\n\t\tparent,\n\t\tProfile::SimilarPeersCountValue(peer),\n\t\t[=](int count) {\n\t\t\treturn peer->isBroadcast()\n\t\t\t\t? tr::lng_profile_similar_channels(tr::now, lt_count, count)\n\t\t\t\t: tr::lng_profile_similar_bots(tr::now, lt_count, count);\n\t\t},\n\t\ttracker)->entity();\n\tresult->addClickHandler([=] {\n\t\tnavigation->showSection(\n\t\t\tstd::make_shared<Info::Memento>(\n\t\t\t\tpeer,\n\t\t\t\tSection::Type::SimilarPeers));\n\t});\n\treturn result;\n}\n\nnot_null<Ui::SettingsButton*> AddStoriesButton(\n\t\tUi::VerticalLayout *parent,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tUi::MultiSlideTracker &tracker) {\n\tauto count = rpl::single(0) | rpl::then(Data::AlbumStoriesIds(\n\t\tpeer,\n\t\t0, // = Data::kStoriesAlbumIdSaved\n\t\tServerMaxStoryId - 1,\n\t\t0\n\t) | rpl::map([](const Data::StoriesIdsSlice &slice) {\n\t\treturn slice.fullCount().value_or(0);\n\t}));\n\tconst auto phrase = peer->isChannel() ? (+[](int count) {\n\t\treturn tr::lng_profile_posts(tr::now, lt_count, count);\n\t}) : (+[](int count) {\n\t\treturn tr::lng_profile_saved_stories(tr::now, lt_count, count);\n\t});\n\tauto result = AddCountedButton(\n\t\tparent,\n\t\tstd::move(count),\n\t\tphrase,\n\t\ttracker)->entity();\n\tresult->addClickHandler([=] {\n\t\tnavigation->showSection(Info::Stories::Make(peer));\n\t});\n\treturn result;\n}\n\nnot_null<Ui::SettingsButton*> AddSavedSublistButton(\n\t\tUi::VerticalLayout *parent,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tUi::MultiSlideTracker &tracker) {\n\tauto result = AddCountedButton(\n\t\tparent,\n\t\tProfile::SavedSublistCountValue(peer),\n\t\t[](int count) {\n\t\t\treturn tr::lng_profile_saved_messages(tr::now, lt_count, count);\n\t\t},\n\t\ttracker)->entity();\n\tresult->addClickHandler([=] {\n\t\tusing namespace HistoryView;\n\t\tconst auto sublist = peer->owner().savedMessages().sublist(peer);\n\t\tnavigation->showSection(\n\t\t\tstd::make_shared<ChatMemento>(ChatViewId{\n\t\t\t\t.history = sublist->owningHistory(),\n\t\t\t\t.sublist = sublist,\n\t\t\t}));\n\t});\n\treturn result;\n}\n\nnot_null<Ui::SettingsButton*> AddPeerGiftsButton(\n\t\tUi::VerticalLayout *parent,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tUi::MultiSlideTracker &tracker) {\n\n\tauto count = Profile::PeerGiftsCountValue(peer);\n\tauto textFromCount = [](int count) {\n\t\treturn tr::lng_profile_peer_gifts(tr::now, lt_count, count);\n\t};\n\n\tusing namespace ::Settings;\n\tauto forked = std::move(count)\n\t\t| start_spawning(parent->lifetime());\n\tauto text = rpl::duplicate(\n\t\tforked\n\t) | rpl::map([textFromCount](int count) {\n\t\treturn (count > 0)\n\t\t\t? textFromCount(count)\n\t\t\t: QString();\n\t});\n\n\tstruct State final {\n\t\tstd::vector<std::unique_ptr<Ui::Text::CustomEmoji>> emojiList;\n\t\trpl::event_stream<> textRefreshed;\n\t\tQPointer<Ui::SettingsButton> button;\n\t\trpl::lifetime appearedLifetime;\n\t\tbool giftsLoaded = false;\n\t};\n\tconst auto state = parent->lifetime().make_state<State>();\n\n\tconst auto refresh = [=] {\n\t\tif (state->button) {\n\t\t\tstate->button->update();\n\t\t}\n\t};\n\n\tauto customs = state->textRefreshed.events(\n\t) | rpl::map([=]() -> TextWithEntities {\n\t\tauto result = TextWithEntities();\n\t\tfor (const auto &custom : state->emojiList) {\n\t\t\tresult.append(Ui::Text::SingleCustomEmoji(custom->entityData()));\n\t\t}\n\t\treturn result;\n\t});\n\n\tconst auto wrap = parent->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\tparent,\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tparent,\n\t\t\t\trpl::combine(\n\t\t\t\t\tstd::move(text),\n\t\t\t\t\tstd::move(customs)\n\t\t\t\t) | rpl::map([=](QString text, TextWithEntities customs) {\n\t\t\t\t\treturn TextWithEntities()\n\t\t\t\t\t\t.append(std::move(text))\n\t\t\t\t\t\t.append(QChar(' '))\n\t\t\t\t\t\t.append(std::move(customs));\n\t\t\t\t}),\n\t\t\t\tst::infoSharedMediaButton,\n\t\t\t\tCore::TextContext({\n\t\t\t\t\t.session = &navigation->session(),\n\t\t\t\t\t.details = { .session = &navigation->session() },\n\t\t\t\t\t.repaint = refresh,\n\t\t\t\t\t.customEmojiLoopLimit = 1,\n\t\t\t\t}))));\n\twrap->setDuration(st::infoSlideDuration);\n\twrap->toggleOn(\n\t\trpl::combine(\n\t\t\trpl::duplicate(forked),\n\t\t\tstate->textRefreshed.events_starting_with({})\n\t\t) | rpl::map([=](int count, auto) {\n\t\t\treturn count > 0 && state->giftsLoaded;\n\t\t}));\n\ttracker.track(wrap);\n\n\trpl::duplicate(forked) | rpl::filter(\n\t\trpl::mappers::_1 > 0\n\t) | rpl::on_next([=] {\n\t\tstate->appearedLifetime.destroy();\n\t\tconst auto requestDone = crl::guard(wrap, [=](\n\t\t\t\tstd::vector<Data::SavedStarGift> gifts) {\n\t\t\tstate->emojiList.clear();\n\t\t\tfor (const auto &gift : gifts) {\n\t\t\t\tstate->emojiList.push_back(\n\t\t\t\t\tpeer->owner().customEmojiManager().create(\n\t\t\t\t\t\tgift.info.document->id,\n\t\t\t\t\t\trefresh));\n\t\t\t}\n\t\t\tstate->giftsLoaded = true;\n\t\t\tstate->textRefreshed.fire({});\n\t\t});\n\t\tnavigation->session().recentSharedGifts().request(peer, requestDone);\n\t}, state->appearedLifetime);\n\n\tstate->button = wrap->entity();\n\n\twrap->entity()->addClickHandler([=] {\n\t\tif (navigation->showFrozenError()) {\n\t\t\treturn;\n\t\t}\n\t\tnavigation->showSection(Info::PeerGifts::Make(peer));\n\t});\n\treturn wrap->entity();\n}\n\n} // namespace Info::Media\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_buttons.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"lang/lang_keys.h\"\n#include \"storage/storage_shared_media.h\"\n\nnamespace Ui {\nclass AbstractButton;\nclass MultiSlideTracker;\nclass SettingsButton;\nclass VerticalLayout;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nnamespace Info::Media {\n\nusing Type = Storage::SharedMediaType;\n\n[[nodiscard]] tr::phrase<lngtag_count> MediaTextPhrase(Type type);\n\n[[nodiscard]] Fn<QString(int)> MediaText(Type type);\n\n[[nodiscard]] not_null<Ui::SlideWrap<Ui::SettingsButton>*> AddCountedButton(\n\tUi::VerticalLayout *parent,\n\trpl::producer<int> &&count,\n\tFn<QString(int)> &&textFromCount,\n\tUi::MultiSlideTracker &tracker);\n\n[[nodiscard]] not_null<Ui::SettingsButton*> AddButton(\n\tUi::VerticalLayout *parent,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tMsgId topicRootId,\n\tPeerId monoforumPeerId,\n\tPeerData *migrated,\n\tType type,\n\tUi::MultiSlideTracker &tracker);\n\n[[nodiscard]] not_null<Ui::SettingsButton*> AddCommonGroupsButton(\n\tUi::VerticalLayout *parent,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<UserData*> user,\n\tUi::MultiSlideTracker &tracker);\n\n[[nodiscard]] not_null<Ui::SettingsButton*> AddSimilarPeersButton(\n\tUi::VerticalLayout *parent,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tUi::MultiSlideTracker &tracker);\n\n[[nodiscard]] not_null<Ui::SettingsButton*> AddStoriesButton(\n\tUi::VerticalLayout *parent,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tUi::MultiSlideTracker &tracker);\n\n[[nodiscard]] not_null<Ui::SettingsButton*> AddSavedSublistButton(\n\tUi::VerticalLayout *parent,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tUi::MultiSlideTracker &tracker);\n\n[[nodiscard]] not_null<Ui::SettingsButton*> AddPeerGiftsButton(\n\tUi::VerticalLayout *parent,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tUi::MultiSlideTracker &tracker);\n\n} // namespace Info::Media\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/media/info_media_common.h\"\n\n#include \"history/history_item.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_overview.h\"\n\nnamespace Info::Media {\n\nUniversalMsgId GetUniversalId(FullMsgId itemId) {\n\treturn peerIsChannel(itemId.peer)\n\t\t? UniversalMsgId(itemId.msg)\n\t\t: UniversalMsgId(itemId.msg - ServerMaxMsgId);\n}\n\nUniversalMsgId GetUniversalId(not_null<const HistoryItem*> item) {\n\treturn GetUniversalId(item->fullId());\n}\n\nUniversalMsgId GetUniversalId(not_null<const BaseLayout*> layout) {\n\treturn GetUniversalId(layout->getItem()->fullId());\n}\n\nbool ChangeItemSelection(\n\t\tListSelectedMap &selected,\n\t\tnot_null<const HistoryItem*> item,\n\t\tListItemSelectionData selectionData,\n\t\tint limit) {\n\tif (!limit) {\n\t\tlimit = MaxSelectedItems;\n\t}\n\tconst auto changeExisting = [&](auto it) {\n\t\tif (it == selected.cend()) {\n\t\t\treturn false;\n\t\t} else if (it->second != selectionData) {\n\t\t\tit->second = selectionData;\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\tif (selected.size() < limit) {\n\t\tconst auto &[i, ok] = selected.try_emplace(item, selectionData);\n\t\tif (ok) {\n\t\t\treturn true;\n\t\t}\n\t\treturn changeExisting(i);\n\t}\n\treturn changeExisting(selected.find(item));\n}\n\nint MinItemHeight(Type type, int width) {\n\tauto &songSt = st::overviewFileLayout;\n\n\tswitch (type) {\n\tcase Type::Photo:\n\tcase Type::GIF:\n\tcase Type::Video:\n\tcase Type::RoundFile: {\n\t\tauto itemsLeft = st::infoMediaSkip;\n\t\tauto itemsInRow = (width - itemsLeft)\n\t\t\t/ (st::infoMediaMinGridSize + st::infoMediaSkip);\n\t\treturn (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow;\n\t} break;\n\n\tcase Type::RoundVoiceFile:\n\t\treturn songSt.songPadding.top()\n\t\t\t+ songSt.songThumbSize\n\t\t\t+ songSt.songPadding.bottom()\n\t\t\t+ st::lineWidth;\n\tcase Type::File:\n\t\treturn songSt.filePadding.top()\n\t\t\t+ songSt.fileThumbSize\n\t\t\t+ songSt.filePadding.bottom()\n\t\t\t+ st::lineWidth;\n\tcase Type::MusicFile:\n\t\treturn songSt.songPadding.top()\n\t\t\t+ songSt.songThumbSize\n\t\t\t+ songSt.songPadding.bottom();\n\tcase Type::Link:\n\t\treturn st::linksPhotoSize\n\t\t\t+ st::linksMargin.top()\n\t\t\t+ st::linksMargin.bottom()\n\t\t\t+ st::linksBorder;\n\t}\n\tUnexpected(\"Type in MinItemHeight()\");\n}\n} // namespace Info::Media\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"overview/overview_layout.h\"\n\nnamespace Storage {\nenum class SharedMediaType : signed char;\n} // namespace Storage\n\nnamespace Info::Media {\n\nusing Type = Storage::SharedMediaType;\nusing BaseLayout = Overview::Layout::ItemBase;\n\nclass Memento;\nclass ListSection;\n\ninline constexpr auto kPreloadIfLessThanScreens = 2;\n\nstruct ListItemSelectionData {\n\texplicit ListItemSelectionData(TextSelection text) : text(text) {\n\t}\n\n\tTextSelection text;\n\tbool canDelete = false;\n\tbool canForward = false;\n\tbool canToggleStoryPin = false;\n\tbool canUnpinStory = false;\n\tbool storyInProfile = false;\n\n\tfriend inline bool operator==(\n\t\tListItemSelectionData,\n\t\tListItemSelectionData) = default;\n};\n\nusing ListSelectedMap = base::flat_map<\n\tnot_null<const HistoryItem*>,\n\tListItemSelectionData,\n\tstd::less<>>;\n\nenum class ListDragSelectAction {\n\tNone,\n\tSelecting,\n\tDeselecting,\n};\n\nstruct ListContext {\n\tOverview::Layout::PaintContext layoutContext;\n\tnot_null<ListSelectedMap*> selected;\n\tnot_null<ListSelectedMap*> dragSelected;\n\tListDragSelectAction dragSelectAction = ListDragSelectAction::None;\n\tBaseLayout *draggedItem = nullptr;\n};\n\nstruct ListScrollTopState {\n\tint64 position = 0; // ListProvider-specific.\n\tHistoryItem *item = nullptr;\n\tint shift = 0;\n};\n\nstruct ListFoundItem {\n\tnot_null<BaseLayout*> layout;\n\tQRect geometry;\n\tbool exact = false;\n};\n\nstruct ListFoundItemWithSection {\n\tListFoundItem item;\n\tnot_null<const ListSection*> section;\n};\n\nstruct CachedItem {\n\tCachedItem(std::unique_ptr<BaseLayout> item) : item(std::move(item)) {\n\t};\n\tCachedItem(CachedItem &&other) = default;\n\tCachedItem &operator=(CachedItem &&other) = default;\n\t~CachedItem() = default;\n\n\tstd::unique_ptr<BaseLayout> item;\n\tbool stale = false;\n};\n\nusing UniversalMsgId = MsgId;\n\n[[nodiscard]] UniversalMsgId GetUniversalId(FullMsgId itemId);\n[[nodiscard]] UniversalMsgId GetUniversalId(\n\tnot_null<const HistoryItem*> item);\n[[nodiscard]] UniversalMsgId GetUniversalId(\n\tnot_null<const BaseLayout*> layout);\n\nbool ChangeItemSelection(\n\tListSelectedMap &selected,\n\tnot_null<const HistoryItem*> item,\n\tListItemSelectionData selectionData,\n\tint limit = 0);\n\nclass ListSectionDelegate {\npublic:\n\t[[nodiscard]] virtual bool sectionHasFloatingHeader() = 0;\n\t[[nodiscard]] virtual QString sectionTitle(\n\t\tnot_null<const BaseLayout*> item) = 0;\n\t[[nodiscard]] virtual bool sectionItemBelongsHere(\n\t\tnot_null<const BaseLayout*> item,\n\t\tnot_null<const BaseLayout*> previous) = 0;\n\n\t[[nodiscard]] not_null<ListSectionDelegate*> sectionDelegate() {\n\t\treturn this;\n\t}\n};\n\nclass ListProvider {\npublic:\n\t[[nodiscard]] virtual Type type() = 0;\n\t[[nodiscard]] virtual bool hasSelectRestriction() = 0;\n\t[[nodiscard]] virtual auto hasSelectRestrictionChanges()\n\t\t->rpl::producer<bool> = 0;\n\t[[nodiscard]] virtual bool isPossiblyMyItem(\n\t\tnot_null<const HistoryItem*> item) = 0;\n\n\t[[nodiscard]] virtual std::optional<int> fullCount() = 0;\n\n\tvirtual void restart() = 0;\n\tvirtual void checkPreload(\n\t\tQSize viewport,\n\t\tnot_null<BaseLayout*> topLayout,\n\t\tnot_null<BaseLayout*> bottomLayout,\n\t\tbool preloadTop,\n\t\tbool preloadBottom) = 0;\n\tvirtual void refreshViewer() = 0;\n\t[[nodiscard]] virtual rpl::producer<> refreshed() = 0;\n\n\t[[nodiscard]] virtual std::vector<ListSection> fillSections(\n\t\tnot_null<Overview::Layout::Delegate*> delegate) = 0;\n\t[[nodiscard]] virtual auto layoutRemoved()\n\t\t-> rpl::producer<not_null<BaseLayout*>> = 0;\n\t[[nodiscard]] virtual BaseLayout *lookupLayout(\n\t\tconst HistoryItem *item) = 0;\n\t[[nodiscard]] virtual bool isMyItem(\n\t\tnot_null<const HistoryItem*> item) = 0;\n\t[[nodiscard]] virtual bool isAfter(\n\t\tnot_null<const HistoryItem*> a,\n\t\tnot_null<const HistoryItem*> b) = 0;\n\n\t[[nodiscard]] virtual ListItemSelectionData computeSelectionData(\n\t\tnot_null<const HistoryItem*> item,\n\t\tTextSelection selection) = 0;\n\tvirtual void applyDragSelection(\n\t\tListSelectedMap &selected,\n\t\tnot_null<const HistoryItem*> fromItem,\n\t\tbool skipFrom,\n\t\tnot_null<const HistoryItem*> tillItem,\n\t\tbool skipTill) = 0;\n\n\t[[nodiscard]] virtual bool allowSaveFileAs(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) = 0;\n\t[[nodiscard]] virtual QString showInFolderPath(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) = 0;\n\n\tvirtual void setSearchQuery(QString query) = 0;\n\tvirtual void jumpToMessage(MsgId messageId, Fn<void(FullMsgId)>) = 0;\n\n\t[[nodiscard]] virtual int64 scrollTopStatePosition(\n\t\tnot_null<HistoryItem*> item) = 0;\n\t[[nodiscard]] virtual HistoryItem *scrollTopStateItem(\n\t\tListScrollTopState state) = 0;\n\tvirtual void saveState(\n\t\tnot_null<Memento*> memento,\n\t\tListScrollTopState scrollState) = 0;\n\tvirtual void restoreState(\n\t\tnot_null<Memento*> memento,\n\t\tFn<void(ListScrollTopState)> restoreScrollState) = 0;\n\n\tvirtual ~ListProvider() = default;\n};\n\n[[nodiscard]] int MinItemHeight(Type type, int width);\n\n} // namespace Info::Media\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_empty_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/media/info_media_empty_widget.h\"\n\n#include \"ui/widgets/labels.h\"\n#include \"styles/style_info.h\"\n#include \"lang/lang_keys.h\"\n\nnamespace Info {\nnamespace Media {\n\nEmptyWidget::EmptyWidget(QWidget *parent)\n: RpWidget(parent)\n, _text(this, st::infoEmptyLabel) {\n}\n\nvoid EmptyWidget::setFullHeight(rpl::producer<int> fullHeightValue) {\n\tstd::move(\n\t\tfullHeightValue\n\t) | rpl::on_next([this](int fullHeight) {\n\t\t// Make icon center be on 1/3 height.\n\t\tauto iconCenter = fullHeight / 3;\n\t\tauto iconHeight = st::infoEmptyFile.height();\n\t\tauto iconTop = iconCenter - iconHeight / 2;\n\t\t_height = iconTop + st::infoEmptyIconTop;\n\t\tresizeToWidth(width());\n\t}, lifetime());\n}\n\nvoid EmptyWidget::setType(Type type) {\n\t_type = type;\n\t_icon = [&] {\n\t\tswitch (_type) {\n\t\tcase Type::Photo:\n\t\tcase Type::GIF: return &st::infoEmptyPhoto;\n\t\tcase Type::Video: return &st::infoEmptyVideo;\n\t\tcase Type::MusicFile: return &st::infoEmptyAudio;\n\t\tcase Type::File: return &st::infoEmptyFile;\n\t\tcase Type::Link: return &st::infoEmptyLink;\n\t\tcase Type::RoundVoiceFile: return &st::infoEmptyVoice;\n\t\t}\n\t\tUnexpected(\"Bad type in EmptyWidget::setType()\");\n\t}();\n\tupdate();\n}\n\nvoid EmptyWidget::setSearchQuery(const QString &query) {\n\t_text->setText([&] {\n\t\tswitch (_type) {\n\t\tcase Type::Photo:\n\t\t\treturn tr::lng_media_photo_empty(tr::now);\n\t\tcase Type::GIF:\n\t\t\treturn tr::lng_media_gif_empty(tr::now);\n\t\tcase Type::Video:\n\t\t\treturn tr::lng_media_video_empty(tr::now);\n\t\tcase Type::MusicFile:\n\t\t\treturn query.isEmpty()\n\t\t\t\t? tr::lng_media_song_empty(tr::now)\n\t\t\t\t: tr::lng_media_song_empty_search(tr::now);\n\t\tcase Type::File:\n\t\t\treturn query.isEmpty()\n\t\t\t\t? tr::lng_media_file_empty(tr::now)\n\t\t\t\t: tr::lng_media_file_empty_search(tr::now);\n\t\tcase Type::Link:\n\t\t\treturn query.isEmpty()\n\t\t\t\t? tr::lng_media_link_empty(tr::now)\n\t\t\t\t: tr::lng_media_link_empty_search(tr::now);\n\t\tcase Type::RoundVoiceFile:\n\t\t\treturn tr::lng_media_audio_empty(tr::now);\n\t\t}\n\t\tUnexpected(\"Bad type in EmptyWidget::setSearchQuery()\");\n\t}());\n\tresizeToWidth(width());\n}\n\nvoid EmptyWidget::setLoading(bool loading) {\n\tif (_loading == loading) {\n\t\treturn;\n\t}\n\t_loading = loading;\n\tresizeToWidth(width());\n\tupdate();\n}\n\nbool EmptyWidget::loading() const {\n\treturn _loading;\n}\n\nvoid EmptyWidget::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tif (_loading) {\n\t\tp.setFont(st::normalFont);\n\t\tp.setPen(st::windowSubTextFg);\n\t\tp.drawText(rect(), tr::lng_contacts_loading(tr::now),\n\t\t\tQTextOption(Qt::AlignCenter));\n\t\treturn;\n\t}\n\n\tif (!_icon) {\n\t\treturn;\n\t}\n\n\tauto iconLeft = (width() - _icon->width()) / 2;\n\tauto iconTop = height() - st::infoEmptyIconTop;\n\t_icon->paint(p, iconLeft, iconTop, width());\n}\n\nint EmptyWidget::resizeGetHeight(int newWidth) {\n\tif (!_loading) {\n\t\tauto labelTop = _height - st::infoEmptyLabelTop;\n\t\tauto labelWidth = newWidth - 2 * st::infoEmptyLabelSkip;\n\t\t_text->resizeToNaturalWidth(labelWidth);\n\n\t\tauto labelLeft = (newWidth - _text->width()) / 2;\n\t\t_text->moveToLeft(labelLeft, labelTop, newWidth);\n\t}\n\n\tupdate();\n\treturn _height;\n}\n\n} // namespace Media\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_empty_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"info/media/info_media_widget.h\"\n\nnamespace Ui {\nclass FlatLabel;\n} // namespace Ui\n\nnamespace Info {\nnamespace Media {\n\nclass EmptyWidget : public Ui::RpWidget {\npublic:\n\tEmptyWidget(QWidget *parent);\n\n\tvoid setFullHeight(rpl::producer<int> fullHeightValue);\n\tvoid setType(Type type);\n\tvoid setSearchQuery(const QString &query);\n\tvoid setLoading(bool loading);\n\t[[nodiscard]] bool loading() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tobject_ptr<Ui::FlatLabel> _text;\n\tType _type = Type::kCount;\n\tconst style::icon *_icon = nullptr;\n\tint _height = 0;\n\tbool _loading = false;\n\n};\n\n} // namespace Media\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_inner_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/media/info_media_inner_widget.h\"\n\n#include <rpl/flatten_latest.h>\n#include \"boxes/abstract_box.h\"\n#include \"info/media/info_media_list_widget.h\"\n#include \"info/media/info_media_buttons.h\"\n#include \"info/media/info_media_empty_widget.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"info/info_controller.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"ui/widgets/discrete_sliders.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/box_content_divider.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/search_field_controller.h\"\n#include \"styles/style_info.h\"\n#include \"lang/lang_keys.h\"\n\nnamespace Info {\nnamespace Media {\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _empty(this) {\n\t_empty->heightValue(\n\t) | rpl::on_next(\n\t\t[this] { refreshHeight(); },\n\t\t_empty->lifetime());\n\t_list = setupList();\n}\n\n// Allows showing additional shared media links and tabs.\n// Used for shared media in Saved Messages.\nvoid InnerWidget::setupOtherTypes() {\n\tif (_controller->key().peer()->sharedMediaInfo() && _isStackBottom) {\n\t\tcreateOtherTypes();\n\t} else {\n\t\t_otherTypes.destroy();\n\t\trefreshHeight();\n\t}\n}\n\nvoid InnerWidget::createOtherTypes() {\n\t_otherTypes.create(this);\n\t_otherTypes->show();\n\n\tcreateTypeButtons();\n\t_otherTypes->add(object_ptr<Ui::BoxContentDivider>(_otherTypes));\n\n\t_otherTypes->resizeToWidth(width());\n\t_otherTypes->heightValue(\n\t) | rpl::on_next(\n\t\t[this] { refreshHeight(); },\n\t\t_otherTypes->lifetime());\n}\n\nvoid InnerWidget::createTypeButtons() {\n\tauto wrap = _otherTypes->add(object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t_otherTypes,\n\t\tobject_ptr<Ui::VerticalLayout>(_otherTypes)));\n\tauto content = wrap->entity();\n\tcontent->add(object_ptr<Ui::FixedHeightWidget>(\n\t\tcontent,\n\t\tst::infoProfileSkip));\n\n\tauto tracker = Ui::MultiSlideTracker();\n\tconst auto peer = _controller->key().peer();\n\tconst auto topic = _controller->key().topic();\n\tconst auto sublist = _controller->key().sublist();\n\tconst auto topicRootId = topic ? topic->rootId() : MsgId();\n\tconst auto monoforumPeerId = sublist\n\t\t? sublist->sublistPeer()->id\n\t\t: PeerId();\n\tconst auto migrated = _controller->migrated();\n\tconst auto addMediaButton = [&](\n\t\t\tType buttonType,\n\t\t\tconst style::icon &icon) {\n\t\tif (buttonType == type()) {\n\t\t\treturn;\n\t\t}\n\t\tauto result = AddButton(\n\t\t\tcontent,\n\t\t\t_controller,\n\t\t\tpeer,\n\t\t\ttopicRootId,\n\t\t\tmonoforumPeerId,\n\t\t\tmigrated,\n\t\t\tbuttonType,\n\t\t\ttracker);\n\t\tobject_ptr<Profile::FloatingIcon>(\n\t\t\tresult,\n\t\t\ticon,\n\t\t\tst::infoSharedMediaButtonIconPosition)->show();\n\t};\n\n\taddMediaButton(Type::Photo, st::infoIconMediaPhoto);\n\taddMediaButton(Type::Video, st::infoIconMediaVideo);\n\taddMediaButton(Type::File, st::infoIconMediaFile);\n\taddMediaButton(Type::MusicFile, st::infoIconMediaAudio);\n\taddMediaButton(Type::Link, st::infoIconMediaLink);\n\taddMediaButton(Type::RoundVoiceFile, st::infoIconMediaVoice);\n\taddMediaButton(Type::GIF, st::infoIconMediaGif);\n\n\tcontent->add(object_ptr<Ui::FixedHeightWidget>(\n\t\tcontent,\n\t\tst::infoProfileSkip));\n\twrap->toggleOn(tracker.atLeastOneShownValue());\n\twrap->finishAnimating();\n}\n\nType InnerWidget::type() const {\n\treturn _controller->section().mediaType();\n}\n\nvoid InnerWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tsetChildVisibleTopBottom(_list, visibleTop, visibleBottom);\n}\n\nbool InnerWidget::showInternal(not_null<Memento*> memento) {\n\tif (!_controller->validateMementoPeer(memento)) {\n\t\treturn false;\n\t}\n\tauto mementoType = memento->section().mediaType();\n\tif (mementoType == type()) {\n\t\trestoreState(memento);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nobject_ptr<ListWidget> InnerWidget::setupList() {\n\tauto result = object_ptr<ListWidget>(this, _controller);\n\tresult->heightValue(\n\t) | rpl::on_next(\n\t\t[this] { refreshHeight(); },\n\t\tresult->lifetime());\n\tusing namespace rpl::mappers;\n\tresult->scrollToRequests(\n\t) | rpl::map([widget = result.data()](int to) {\n\t\treturn Ui::ScrollToRequest {\n\t\t\twidget->y() + to,\n\t\t\t-1\n\t\t};\n\t}) | rpl::start_to_stream(\n\t\t_scrollToRequests,\n\t\tresult->lifetime());\n\t_selectedLists.fire(result->selectedListValue());\n\t_listTops.fire(result->topValue());\n\t_empty->setType(_controller->section().mediaType());\n\t_controller->mediaSourceQueryValue(\n\t) | rpl::on_next([this](const QString &query) {\n\t\t_empty->setSearchQuery(query);\n\t}, result->lifetime());\n\treturn result;\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\t_list->saveState(memento);\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\t_list->restoreState(memento);\n}\n\nrpl::producer<SelectedItems> InnerWidget::selectedListValue() const {\n\treturn _selectedLists.events_starting_with(\n\t\t_list->selectedListValue()\n\t) | rpl::flatten_latest();\n}\n\nvoid InnerWidget::selectionAction(SelectionAction action) {\n\t_list->selectionAction(action);\n}\n\nInnerWidget::~InnerWidget() = default;\n\nint InnerWidget::resizeGetHeight(int newWidth) {\n\t_inResize = true;\n\tauto guard = gsl::finally([this] { _inResize = false; });\n\n\tif (_otherTypes) {\n\t\t_otherTypes->resizeToWidth(newWidth);\n\t}\n\t_list->resizeToWidth(newWidth);\n\t_empty->resizeToWidth(newWidth);\n\treturn recountHeight();\n}\n\nvoid InnerWidget::refreshHeight() {\n\tif (_inResize) {\n\t\treturn;\n\t}\n\tresize(width(), recountHeight());\n}\n\nint InnerWidget::recountHeight() {\n\tauto top = 0;\n\tif (_otherTypes) {\n\t\t_otherTypes->moveToLeft(0, top);\n\t\ttop += _otherTypes->heightNoMargins() - st::lineWidth;\n\t}\n\tauto listHeight = 0;\n\tif (_list) {\n\t\t_list->moveToLeft(0, top);\n\t\tlistHeight = _list->heightNoMargins();\n\t\ttop += listHeight;\n\t}\n\tif (listHeight > _emptyHeightThreshold && !_empty->loading()) {\n\t\t_empty->hide();\n\t} else {\n\t\t_empty->show();\n\t\t_empty->moveToLeft(0, top);\n\t\ttop += _empty->heightNoMargins();\n\t}\n\treturn top;\n}\n\nvoid InnerWidget::setScrollHeightValue(rpl::producer<int> value) {\n\tusing namespace rpl::mappers;\n\t_empty->setFullHeight(rpl::combine(\n\t\tstd::move(value),\n\t\t_listTops.events_starting_with(\n\t\t\t_list->topValue()\n\t\t) | rpl::flatten_latest(),\n\t\t_1 - _2));\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nvoid InnerWidget::jumpToMessage(MsgId msgId) {\n\t_empty->setLoading(true);\n\t_emptyHeightThreshold = st::semiboldFont->height;\n\t_list->jumpToMessage(msgId);\n\t_emptyLoadingLifetime = _list->heightValue(\n\t) | rpl::skip(1) | rpl::filter(\n\t\trpl::mappers::_1 > _emptyHeightThreshold\n\t) | rpl::take(1) | rpl::on_next([=](int height) {\n\t\t_empty->setLoading(false);\n\t\t_emptyHeightThreshold = 0;\n\t\trecountHeight();\n\t});\n}\n\n} // namespace Media\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_inner_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"base/unique_qptr.h\"\n#include \"info/media/info_media_widget.h\"\n#include \"info/media/info_media_list_widget.h\"\n\nnamespace Ui {\nclass VerticalLayout;\nclass SearchFieldController;\n} // namespace Ui\n\nnamespace Info {\nclass Controller;\n} // namespace Info\n\nnamespace Info::Media {\n\nclass Memento;\nclass ListWidget;\nclass EmptyWidget;\n\nclass InnerWidget final : public Ui::RpWidget {\npublic:\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller);\n\n\tbool showInternal(not_null<Memento*> memento);\n\tvoid setIsStackBottom(bool isStackBottom) {\n\t\t_isStackBottom = isStackBottom;\n\t\tsetupOtherTypes();\n\t}\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tvoid setScrollHeightValue(rpl::producer<int> value);\n\n\trpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\trpl::producer<SelectedItems> selectedListValue() const;\n\tvoid selectionAction(SelectionAction action);\n\n\tvoid jumpToMessage(MsgId msgId);\n\n\t~InnerWidget();\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\nprivate:\n\tint recountHeight();\n\tvoid refreshHeight();\n\t// Allows showing additional shared media links and tabs.\n\t// Used for shared media in Saved Messages.\n\tvoid setupOtherTypes();\n\tvoid createOtherTypes();\n\tvoid createTypeButtons();\n\n\tType type() const;\n\n\tobject_ptr<ListWidget> setupList();\n\n\tconst not_null<Controller*> _controller;\n\n\tobject_ptr<Ui::VerticalLayout> _otherTypes = { nullptr };\n\tobject_ptr<ListWidget> _list = { nullptr };\n\tobject_ptr<EmptyWidget> _empty;\n\n\tbool _inResize = false;\n\tbool _isStackBottom = false;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\trpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;\n\trpl::event_stream<rpl::producer<int>> _listTops;\n\n\tint _emptyHeightThreshold = 0;\n\trpl::lifetime _emptyLoadingLifetime;\n\n};\n\n} // namespace Info::Media\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_list_section.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/media/info_media_list_section.h\"\n\n#include \"storage/storage_shared_media.h\"\n#include \"layout/layout_selection.h\"\n#include \"ui/rect.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info::Media {\nnamespace {\n\nconstexpr auto kFloatingHeaderAlpha = 0.9;\n\n} // namespace\n\nListSection::ListSection(Type type, not_null<ListSectionDelegate*> delegate)\n: _type(type)\n, _delegate(delegate)\n, _hasFloatingHeader(delegate->sectionHasFloatingHeader())\n, _mosaic(st::emojiPanWidth - st::inlineResultsLeft) {\n}\n\nbool ListSection::empty() const {\n\treturn _items.empty();\n}\n\nUniversalMsgId ListSection::minId() const {\n\tExpects(!empty());\n\n\treturn GetUniversalId(_items.back()->getItem());\n}\n\nvoid ListSection::setTop(int top) {\n\t_top = top;\n}\n\nint ListSection::top() const {\n\treturn _top;\n}\n\nvoid ListSection::setCanReorder(bool value) {\n\t_canReorder = value;\n}\n\nint ListSection::height() const {\n\treturn _height;\n}\n\nint ListSection::bottom() const {\n\treturn top() + height();\n}\n\nbool ListSection::isOneColumn() const {\n\treturn _itemsInRow == 1;\n}\n\nbool ListSection::addItem(not_null<BaseLayout*> item) {\n\tif (_items.empty() || belongsHere(item)) {\n\t\tif (_items.empty()) {\n\t\t\tsetHeader(item);\n\t\t}\n\t\tappendItem(item);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid ListSection::finishSection() {\n\tif (_type == Type::GIF) {\n\t\t_mosaic.setPadding({\n\t\t\tst::infoMediaSkip,\n\t\t\theaderHeight(),\n\t\t\tst::infoMediaSkip,\n\t\t\tst::stickerPanPadding,\n\t\t});\n\t\t_mosaic.setRightSkip(st::infoMediaSkip);\n\t\t_mosaic.addItems(_items);\n\t}\n}\n\nvoid ListSection::setHeader(not_null<BaseLayout*> item) {\n\t_header.setText(st::infoMediaHeaderStyle, _delegate->sectionTitle(item));\n}\n\nbool ListSection::belongsHere(\n\t\tnot_null<BaseLayout*> item) const {\n\tExpects(!_items.empty());\n\n\treturn _delegate->sectionItemBelongsHere(item, _items.back());\n}\n\nvoid ListSection::appendItem(not_null<BaseLayout*> item) {\n\t_items.push_back(item);\n\t_byItem.emplace(item->getItem(), item);\n}\n\nbool ListSection::removeItem(not_null<const HistoryItem*> item) {\n\tif (const auto i = _byItem.find(item); i != end(_byItem)) {\n\t\t_items.erase(ranges::remove(_items, i->second), end(_items));\n\t\t_byItem.erase(i);\n\t\trefreshHeight();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid ListSection::reorderItems(int oldPosition, int newPosition) {\n\tbase::reorder(_items, oldPosition, newPosition);\n\trefreshHeight();\n}\n\nQRect ListSection::findItemRect(\n\t\tnot_null<const BaseLayout*> item) const {\n\tconst auto position = item->position();\n\tif (!_mosaic.empty()) {\n\t\treturn _mosaic.findRect(position);\n\t}\n\tconst auto top = position / _itemsInRow;\n\tconst auto indexInRow = position % _itemsInRow;\n\tconst auto left = _itemsLeft\n\t\t+ indexInRow * (_itemWidth + st::infoMediaSkip);\n\treturn QRect(left, top, _itemWidth, item->height());\n}\n\nListFoundItem ListSection::completeResult(\n\t\tnot_null<BaseLayout*> item,\n\t\tbool exact) const {\n\treturn { item, findItemRect(item), exact };\n}\n\nListFoundItem ListSection::findItemByPoint(QPoint point) const {\n\tExpects(!_items.empty());\n\n\tif (!_mosaic.empty()) {\n\t\tconst auto found = _mosaic.findByPoint(point);\n\t\tAssert(found.index != -1);\n\t\tconst auto item = _mosaic.itemAt(found.index);\n\t\tconst auto rect = findItemRect(item);\n\t\treturn { item, rect, found.exact };\n\t}\n\tauto itemIt = findItemAfterTop(point.y());\n\tif (itemIt == end(_items)) {\n\t\t--itemIt;\n\t}\n\tauto item = *itemIt;\n\tauto rect = findItemRect(item);\n\tif (point.y() >= rect.top()) {\n\t\tauto shift = floorclamp(\n\t\t\tpoint.x(),\n\t\t\t(_itemWidth + st::infoMediaSkip),\n\t\t\t0,\n\t\t\t_itemsInRow);\n\t\twhile (shift-- && itemIt != _items.end()) {\n\t\t\t++itemIt;\n\t\t}\n\t\tif (itemIt == _items.end()) {\n\t\t\t--itemIt;\n\t\t}\n\t\titem = *itemIt;\n\t\trect = findItemRect(item);\n\t}\n\treturn { item, rect, rect.contains(point) };\n}\n\nstd::optional<ListFoundItem> ListSection::findItemByItem(\n\t\tnot_null<const HistoryItem*> item) const {\n\tconst auto i = _byItem.find(item);\n\tif (i != end(_byItem)) {\n\t\treturn ListFoundItem{ i->second, findItemRect(i->second), true };\n\t}\n\treturn std::nullopt;\n}\n\nListFoundItem ListSection::findItemDetails(\n\t\tnot_null<BaseLayout*> item) const {\n\treturn { item, findItemRect(item), true };\n}\n\nauto ListSection::findItemAfterTop(\n\t\tint top) -> Items::iterator {\n\tExpects(_mosaic.empty());\n\n\treturn ranges::lower_bound(\n\t\t_items,\n\t\ttop,\n\t\tstd::less_equal<>(),\n\t\t[this](const auto &item) {\n\t\t\tconst auto itemTop = item->position() / _itemsInRow;\n\t\t\treturn itemTop + item->height();\n\t\t});\n}\n\nauto ListSection::findItemAfterTop(\n\t\tint top) const -> Items::const_iterator {\n\tExpects(_mosaic.empty());\n\n\treturn ranges::lower_bound(\n\t\t_items,\n\t\ttop,\n\t\tstd::less_equal<>(),\n\t\t[this](const auto &item) {\n\t\t\tconst auto itemTop = item->position() / _itemsInRow;\n\t\t\treturn itemTop + item->height();\n\t\t});\n}\n\nauto ListSection::findItemAfterBottom(\n\t\tItems::const_iterator from,\n\t\tint bottom) const -> Items::const_iterator {\n\tExpects(_mosaic.empty());\n\treturn ranges::lower_bound(\n\t\tfrom,\n\t\t_items.end(),\n\t\tbottom,\n\t\tstd::less<>(),\n\t\t[this](const auto &item) {\n\t\t\tconst auto itemTop = item->position() / _itemsInRow;\n\t\t\treturn itemTop;\n\t\t});\n}\n\nconst ListSection::Items &ListSection::items() const {\n\treturn _items;\n}\n\nvoid ListSection::paint(\n\t\tPainter &p,\n\t\tconst ListContext &context,\n\t\tQRect clip,\n\t\tint outerWidth) const {\n\tconst auto header = headerHeight();\n\tif (QRect(0, 0, outerWidth, header).intersects(clip)) {\n\t\tp.setPen(st::infoMediaHeaderFg);\n\t\t_header.drawLeftElided(\n\t\t\tp,\n\t\t\tst::infoMediaHeaderPosition.x(),\n\t\t\tst::infoMediaHeaderPosition.y(),\n\t\t\touterWidth - 2 * st::infoMediaHeaderPosition.x(),\n\t\t\touterWidth);\n\t}\n\tauto localContext = context.layoutContext;\n\tif (!_mosaic.empty()) {\n\t\tconst auto paintItem = [&](not_null<BaseLayout*> item, QPoint point) {\n\t\t\tp.translate(point.x(), point.y());\n\t\t\titem->paint(\n\t\t\t\tp,\n\t\t\t\tclip.translated(-point),\n\t\t\t\titemSelection(item, context),\n\t\t\t\t&localContext);\n\t\t\tp.translate(-point.x(), -point.y());\n\t\t};\n\t\t_mosaic.paint(std::move(paintItem), clip);\n\t\treturn;\n\t}\n\n\tconst auto fromIt = findItemAfterTop(clip.y());\n\tconst auto tillIt = findItemAfterBottom(\n\t\tfromIt,\n\t\tclip.y() + clip.height());\n\tfor (auto it = fromIt; it != tillIt; ++it) {\n\t\tconst auto item = *it;\n\t\tif (item == context.draggedItem) {\n\t\t\tcontinue;\n\t\t}\n\t\tauto rect = findItemRect(item);\n\t\trect.translate(item->shift());\n\t\tlocalContext.skipBorder = (rect.y() <= header + _itemsTop);\n\t\tif (rect.intersects(clip)) {\n\t\t\tp.translate(rect.topLeft());\n\t\t\titem->paint(\n\t\t\t\tp,\n\t\t\t\tclip.translated(-rect.topLeft()),\n\t\t\t\titemSelection(item, context),\n\t\t\t\t&localContext);\n\t\t\tp.translate(-rect.topLeft());\n\n\t\t\tif (_canReorder && isOneColumn()) {\n\t\t\t\tst::stickersReorderIcon.paint(\n\t\t\t\t\tp,\n\t\t\t\t\trect::right(rect) - oneColumnRightPadding(),\n\t\t\t\t\t(rect.height() - st::stickersReorderIcon.height()) / 2\n\t\t\t\t\t\t+ rect.y(),\n\t\t\t\t\touterWidth);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ListSection::paintFloatingHeader(\n\t\tPainter &p,\n\t\tint visibleTop,\n\t\tint outerWidth) {\n\tif (!_hasFloatingHeader) {\n\t\treturn;\n\t}\n\tconst auto headerTop = st::infoMediaHeaderPosition.y() / 2;\n\tif (visibleTop <= (_top + headerTop)) {\n\t\treturn;\n\t}\n\tconst auto header = headerHeight();\n\tconst auto headerLeft = st::infoMediaHeaderPosition.x();\n\tconst auto floatingTop = std::min(\n\t\tvisibleTop,\n\t\tbottom() - header + headerTop);\n\tp.save();\n\tp.resetTransform();\n\tp.setOpacity(kFloatingHeaderAlpha);\n\tp.fillRect(QRect(0, floatingTop, outerWidth, header), st::boxBg);\n\tp.setOpacity(1.0);\n\tp.setPen(st::infoMediaHeaderFg);\n\t_header.drawLeftElided(\n\t\tp,\n\t\theaderLeft,\n\t\tfloatingTop + headerTop,\n\t\touterWidth - 2 * headerLeft,\n\t\touterWidth);\n\tp.restore();\n}\n\nTextSelection ListSection::itemSelection(\n\t\tnot_null<const BaseLayout*> item,\n\t\tconst ListContext &context) const {\n\tconst auto parent = item->getItem();\n\tconst auto dragSelectAction = context.dragSelectAction;\n\tif (dragSelectAction != ListDragSelectAction::None) {\n\t\tconst auto i = context.dragSelected->find(parent);\n\t\tif (i != context.dragSelected->end()) {\n\t\t\treturn (dragSelectAction == ListDragSelectAction::Selecting)\n\t\t\t\t? FullSelection\n\t\t\t\t: TextSelection();\n\t\t}\n\t}\n\tconst auto i = context.selected->find(parent);\n\treturn (i == context.selected->cend())\n\t\t? TextSelection()\n\t\t: i->second.text;\n}\n\nint ListSection::headerHeight() const {\n\treturn _header.isEmpty() ? 0 : st::infoMediaHeaderHeight;\n}\n\nint ListSection::oneColumnRightPadding() const {\n\treturn !isOneColumn()\n\t\t? 0\n\t\t: _canReorder\n\t\t? st::stickersReorderIcon.width() + st::infoMediaLeft\n\t\t: 0;\n}\n\nvoid ListSection::resizeToWidth(int newWidth) {\n\tconst auto minWidth = st::infoMediaMinGridSize + st::infoMediaSkip * 2;\n\tif (newWidth < minWidth) {\n\t\treturn;\n\t}\n\n\tconst auto resizeOneColumn = [&](int itemsLeft, int itemWidth) {\n\t\tconst auto rightPadding = oneColumnRightPadding();\n\t\t_itemsLeft = itemsLeft;\n\t\t_itemsTop = 0;\n\t\t_itemsInRow = 1;\n\t\t_itemWidth = itemWidth - rightPadding;\n\t\tfor (auto &item : _items) {\n\t\t\titem->resizeGetHeight(_itemWidth - rightPadding);\n\t\t}\n\t};\n\tswitch (_type) {\n\tcase Type::Photo:\n\tcase Type::Video:\n\tcase Type::PhotoVideo:\n\tcase Type::RoundFile: {\n\t\tconst auto skip = st::infoMediaSkip;\n\t\t_itemsLeft = st::infoMediaLeft;\n\t\t_itemsTop = st::infoMediaSkip;\n\t\t_itemsInRow = (newWidth - _itemsLeft * 2 + skip)\n\t\t\t/ (st::infoMediaMinGridSize + skip);\n\t\t_itemWidth = ((newWidth - _itemsLeft * 2 + skip) / _itemsInRow)\n\t\t\t- st::infoMediaSkip;\n\t\t_itemsLeft = (newWidth - (_itemWidth + skip) * _itemsInRow + skip)\n\t\t\t/ 2;\n\t\tfor (auto &item : _items) {\n\t\t\t_itemHeight = item->resizeGetHeight(_itemWidth);\n\t\t}\n\t} break;\n\n\tcase Type::GIF: {\n\t\t_mosaic.setFullWidth(newWidth - st::infoMediaSkip);\n\t} break;\n\n\tcase Type::RoundVoiceFile:\n\tcase Type::MusicFile:\n\t\tresizeOneColumn(0, newWidth);\n\t\tbreak;\n\tcase Type::File:\n\tcase Type::Link: {\n\t\tconst auto itemsLeft = st::infoMediaHeaderPosition.x();\n\t\tconst auto itemWidth = newWidth - 2 * itemsLeft;\n\t\tresizeOneColumn(itemsLeft, itemWidth);\n\t} break;\n\t}\n\n\trefreshHeight();\n}\n\nint ListSection::recountHeight() {\n\tauto result = headerHeight();\n\n\tswitch (_type) {\n\tcase Type::Photo:\n\tcase Type::Video:\n\tcase Type::PhotoVideo:\n\tcase Type::RoundFile: {\n\t\tconst auto itemHeight = _itemHeight + st::infoMediaSkip;\n\t\tauto index = 0;\n\t\tresult += _itemsTop;\n\t\tfor (auto &item : _items) {\n\t\t\titem->setPosition(_itemsInRow * result + index);\n\t\t\tif (++index == _itemsInRow) {\n\t\t\t\tresult += itemHeight;\n\t\t\t\tindex = 0;\n\t\t\t}\n\t\t}\n\t\tif (_items.size() % _itemsInRow) {\n\t\t\t_rowsCount = int(_items.size()) / _itemsInRow + 1;\n\t\t\tresult += itemHeight;\n\t\t} else {\n\t\t\t_rowsCount = int(_items.size()) / _itemsInRow;\n\t\t}\n\t} break;\n\n\tcase Type::GIF: {\n\t\treturn _mosaic.countDesiredHeight(0);\n\t} break;\n\n\tcase Type::RoundVoiceFile:\n\tcase Type::File:\n\tcase Type::MusicFile:\n\tcase Type::Link:\n\t\tfor (auto &item : _items) {\n\t\t\titem->setPosition(result);\n\t\t\tresult += item->height();\n\t\t}\n\t\t_rowsCount = _items.size();\n\t\tbreak;\n\t}\n\n\treturn result;\n}\n\nvoid ListSection::refreshHeight() {\n\t_height = recountHeight();\n}\n\n} // namespace Info::Media\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_list_section.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/media/info_media_common.h\"\n#include \"layout/layout_mosaic.h\"\n#include \"ui/text/text.h\"\n\nnamespace Info::Media {\n\nclass ListSection {\npublic:\n\tListSection(Type type, not_null<ListSectionDelegate*> delegate);\n\n\tbool addItem(not_null<BaseLayout*> item);\n\tvoid finishSection();\n\n\t[[nodiscard]] bool empty() const;\n\n\t[[nodiscard]] UniversalMsgId minId() const;\n\n\tvoid setTop(int top);\n\t[[nodiscard]] int top() const;\n\tvoid setCanReorder(bool);\n\tvoid resizeToWidth(int newWidth);\n\t[[nodiscard]] int height() const;\n\n\t[[nodiscard]] int bottom() const;\n\t[[nodiscard]] bool isOneColumn() const;\n\t[[nodiscard]] int oneColumnRightPadding() const;\n\n\tbool removeItem(not_null<const HistoryItem*> item);\n\tvoid reorderItems(int oldPosition, int newPosition);\n\t[[nodiscard]] std::optional<ListFoundItem> findItemByItem(\n\t\tnot_null<const HistoryItem*> item) const;\n\t[[nodiscard]] ListFoundItem findItemDetails(\n\t\tnot_null<BaseLayout*> item) const;\n\t[[nodiscard]] ListFoundItem findItemByPoint(QPoint point) const;\n\n\tusing Items = std::vector<not_null<BaseLayout*>>;\n\tconst Items &items() const;\n\n\tvoid paint(\n\t\tPainter &p,\n\t\tconst ListContext &context,\n\t\tQRect clip,\n\t\tint outerWidth) const;\n\n\tvoid paintFloatingHeader(Painter &p, int visibleTop, int outerWidth);\n\nprivate:\n\t[[nodiscard]] int headerHeight() const;\n\tvoid appendItem(not_null<BaseLayout*> item);\n\tvoid setHeader(not_null<BaseLayout*> item);\n\t[[nodiscard]] bool belongsHere(not_null<BaseLayout*> item) const;\n\t[[nodiscard]] Items::iterator findItemAfterTop(int top);\n\t[[nodiscard]] Items::const_iterator findItemAfterTop(int top) const;\n\t[[nodiscard]] Items::const_iterator findItemAfterBottom(\n\t\tItems::const_iterator from,\n\t\tint bottom) const;\n\t[[nodiscard]] QRect findItemRect(not_null<const BaseLayout*> item) const;\n\t[[nodiscard]] ListFoundItem completeResult(\n\t\tnot_null<BaseLayout*> item,\n\t\tbool exact) const;\n\t[[nodiscard]] TextSelection itemSelection(\n\t\tnot_null<const BaseLayout*> item,\n\t\tconst ListContext &context) const;\n\n\tint recountHeight();\n\tvoid refreshHeight();\n\n\tType _type = Type{};\n\tnot_null<ListSectionDelegate*> _delegate;\n\n\tbool _hasFloatingHeader = false;\n\tUi::Text::String _header;\n\tItems _items;\n\tbase::flat_map<\n\t\tnot_null<const HistoryItem*>,\n\t\tnot_null<BaseLayout*>> _byItem;\n\tint _itemsLeft = 0;\n\tint _itemsTop = 0;\n\tint _itemWidth = 0;\n\tint _itemHeight = 0;\n\tint _itemsInRow = 1;\n\tmutable int _rowsCount = 0;\n\tint _top = 0;\n\tint _height = 0;\n\tbool _canReorder = false;\n\n\tMosaic::Layout::MosaicLayout<BaseLayout> _mosaic;\n\n};\n\n} // namespace Info::Media\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_list_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/media/info_media_list_widget.h\"\n\n#include \"forkgram/uri_menu.h\"\n\n#include \"info/global_media/info_global_media_provider.h\"\n#include \"info/media/info_media_common.h\"\n#include \"info/media/info_media_provider.h\"\n#include \"info/media/info_media_list_section.h\"\n#include \"info/downloads/info_downloads_provider.h\"\n#include \"info/saved/info_saved_music_provider.h\"\n#include \"info/stories/info_stories_provider.h\"\n#include \"info/info_controller.h\"\n#include \"layout/layout_mosaic.h\"\n#include \"layout/layout_selection.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_download_manager.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"history/view/media/history_view_save_document_action.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/history_view_service_message.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"media/stories/media_stories_controller.h\" // ...TogglePinnedToast.\n#include \"media/stories/media_stories_share.h\" // PrepareShareBox.\n#include \"media/stories/media_stories_stealth.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_peer_menu.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/delete_message_context_action.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/inactive_press.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/weak_ptr.h\"\n#include \"base/call_delayed.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"boxes/delete_messages_box.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/sticker_set_box.h\" // StickerPremiumMark\n#include \"core/file_utilities.h\"\n#include \"core/application.h\"\n#include \"ui/toast/toast.h\"\n#include \"styles/style_overview.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\" // giftBoxHiddenMark\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_media_stories.h\"\n\n#include \"history/view/history_view_context_menu_fork.h\"\n\n#include <QtWidgets/QApplication>\n#include <QtGui/QClipboard>\n\nnamespace Info {\nnamespace Media {\nnamespace {\n\nconstexpr auto kMediaCountForSearch = 10;\n\n} // namespace\n\nstruct ListWidget::DateBadge {\n\tDateBadge(Type type, Fn<void()> checkCallback, Fn<void()> hideCallback);\n\n\tSingleQueuedInvokation check;\n\tbase::Timer hideTimer;\n\tUi::Animations::Simple opacity;\n\tUi::CornersPixmaps corners;\n\tbool goodType = false;\n\tbool shown = false;\n\tQString text;\n\tint textWidth = 0;\n\tQRect rect;\n};\n\n[[nodiscard]] std::unique_ptr<ListProvider> MakeProvider(\n\t\tnot_null<AbstractController*> controller) {\n\tif (controller->isDownloads()) {\n\t\treturn std::make_unique<Downloads::Provider>(controller);\n\t} else if (controller->musicPeer()) {\n\t\treturn std::make_unique<Saved::MusicProvider>(controller);\n\t} else if (controller->storiesPeer()) {\n\t\treturn std::make_unique<Stories::Provider>(controller);\n\t} else if (controller->section().type() == Section::Type::GlobalMedia) {\n\t\treturn std::make_unique<GlobalMedia::Provider>(controller);\n\t}\n\treturn std::make_unique<Provider>(controller);\n}\n\nbool ListWidget::isAfter(\n\t\tconst MouseState &a,\n\t\tconst MouseState &b) const {\n\tif (a.item != b.item) {\n\t\treturn _provider->isAfter(a.item, b.item);\n\t}\n\tconst auto xAfter = a.cursor.x() - b.cursor.x();\n\tconst auto yAfter = a.cursor.y() - b.cursor.y();\n\treturn (xAfter + yAfter >= 0);\n}\n\nbool ListWidget::SkipSelectFromItem(const MouseState &state) {\n\tif (state.cursor.y() >= state.size.height()\n\t\t|| state.cursor.x() >= state.size.width()) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool ListWidget::SkipSelectTillItem(const MouseState &state) {\n\tif (state.cursor.x() < 0 || state.cursor.y() < 0) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nListWidget::DateBadge::DateBadge(\n\tType type,\n\tFn<void()> checkCallback,\n\tFn<void()> hideCallback)\n: check(std::move(checkCallback))\n, hideTimer(std::move(hideCallback))\n, goodType(type == Type::Photo\n\t|| type == Type::Video\n\t|| type == Type::PhotoVideo\n\t|| type == Type::GIF) {\n}\n\nListWidget::ListWidget(\n\tQWidget *parent,\n\tnot_null<AbstractController*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _provider(MakeProvider(_controller))\n, _dateBadge(std::make_unique<DateBadge>(\n\t_provider->type(),\n\t[=] { scrollDateCheck(); },\n\t[=] { scrollDateHide(); }))\n, _selectedLimit(MaxSelectedItems)\n, _storiesAddToAlbumId(controller->storiesAddToAlbumId())\n, _hiddenMark(std::make_unique<StickerPremiumMark>(\n\t\t&_controller->session(),\n\t\tst::giftBoxHiddenMark,\n\t\tRectPart::Center)) {\n\tstart();\n}\n\nMain::Session &ListWidget::session() const {\n\treturn _controller->session();\n}\n\nvoid ListWidget::start() {\n\tsetMouseTracking(true);\n\n\t_controller->setSearchEnabledByContent(false);\n\n\t_provider->layoutRemoved(\n\t) | rpl::on_next([=](not_null<BaseLayout*> layout) {\n\t\tif (_overLayout == layout) {\n\t\t\t_overLayout = nullptr;\n\t\t}\n\t\t_heavyLayouts.remove(layout);\n\t}, lifetime());\n\n\t_provider->refreshed(\n\t) | rpl::on_next([=] {\n\t\trefreshRows();\n\t}, lifetime());\n\n\tif (_controller->isDownloads()) {\n\t\t_provider->refreshViewer();\n\n\t\t_controller->searchQueryValue(\n\t\t) | rpl::on_next([this](QString &&query) {\n\t\t\t_provider->setSearchQuery(std::move(query));\n\t\t}, lifetime());\n\t} else if (_controller->storiesPeer()) {\n\t\tsetupStoriesTrackIds();\n\t\ttrackSession(&session());\n\t\trestart();\n\t} else if (_controller->musicPeer()) {\n\t\ttrackSession(&session());\n\t\trestart();\n\t} else {\n\t\ttrackSession(&session());\n\n\t\t(_controller->key().isGlobalMedia()\n\t\t\t? _controller->searchQueryValue()\n\t\t\t: _controller->mediaSourceQueryValue()\n\t\t) | rpl::on_next([this] {\n\t\t\trestart();\n\t\t}, lifetime());\n\n\t\tif (_provider->type() == Type::File) {\n\t\t\t// For downloads manager.\n\t\t\tsession().data().itemVisibilityQueries(\n\t\t\t) | rpl::filter([=](\n\t\t\t\t\tconst Data::Session::ItemVisibilityQuery &query) {\n\t\t\t\treturn _provider->isPossiblyMyItem(query.item)\n\t\t\t\t\t&& isVisible();\n\t\t\t}) | rpl::on_next([=](\n\t\t\t\t\tconst Data::Session::ItemVisibilityQuery &query) {\n\t\t\t\tif (const auto found = findItemByItem(query.item)) {\n\t\t\t\t\tif (itemVisible(found->layout)) {\n\t\t\t\t\t\t*query.isVisible = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, lifetime());\n\t\t}\n\t}\n\n\tsetupSelectRestriction();\n}\n\nvoid ListWidget::subscribeToSession(\n\t\tnot_null<Main::Session*> session,\n\t\trpl::lifetime &lifetime) {\n\tsession->downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime);\n\n\tsession->data().itemLayoutChanged(\n\t) | rpl::on_next([this](auto item) {\n\t\titemLayoutChanged(item);\n\t}, lifetime);\n\n\tsession->data().itemRemoved(\n\t) | rpl::on_next([this](auto item) {\n\t\titemRemoved(item);\n\t}, lifetime);\n\n\tsession->data().itemRepaintRequest(\n\t) | rpl::on_next([this](auto item) {\n\t\trepaintItem(item);\n\t}, lifetime);\n\n\tsession->data().itemDataChanges(\n\t) | rpl::on_next([=](not_null<HistoryItem*> item) {\n\t\tif (const auto found = findItemByItem(item)) {\n\t\t\tfound->layout->itemDataChanged();\n\t\t}\n\t}, lifetime);\n}\n\nvoid ListWidget::setupSelectRestriction() {\n\t_provider->hasSelectRestrictionChanges(\n\t) | rpl::filter([=] {\n\t\treturn _provider->hasSelectRestriction() && hasSelectedItems();\n\t}) | rpl::on_next([=] {\n\t\tclearSelected();\n\t\tif (_mouseAction == MouseAction::PrepareSelect) {\n\t\t\tmouseActionCancel();\n\t\t}\n\t}, lifetime());\n}\n\nvoid ListWidget::setupStoriesTrackIds() {\n\tif (!_storiesAddToAlbumId) {\n\t\treturn;\n\t}\n\tconst auto peerId = _controller->storiesPeer()->id;\n\tconst auto stories = &session().data().stories();\n\n\tconstexpr auto kArchive = Data::kStoriesAlbumIdArchive;\n\tconst auto key = Data::StoryAlbumIdsKey{ peerId, kArchive };\n\trpl::single(rpl::empty) | rpl::then(\n\t\tstories->albumIdsChanged() | rpl::filter(\n\t\t\trpl::mappers::_1 == key\n\t\t) | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\tconst auto albumId = _storiesAddToAlbumId;\n\t\tconst auto &ids = stories->albumKnownInArchive(peerId, albumId);\n\t\tif (_storiesInAlbum != ids) {\n\t\t\tfor (const auto id : ids) {\n\t\t\t\tif (_storiesInAlbum.emplace(id).second) {\n\t\t\t\t\t_storyMsgsToMarkSelected.emplace(StoryIdToMsgId(id));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_storiesInAlbum.size() > ids.size()) {\n\t\t\t\tconst auto endIt = end(_storiesInAlbum);\n\t\t\t\tfor (auto i = begin(_storiesInAlbum); i != endIt;) {\n\t\t\t\t\tif (ids.contains(*i)) {\n\t\t\t\t\t\t++i;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t_storyMsgsToMarkSelected.remove(StoryIdToMsgId(*i));\n\t\t\t\t\t\ti = _storiesInAlbum.erase(i);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\tif (!stories->albumIdsCountKnown(peerId, _storiesAddToAlbumId)) {\n\t\tstories->albumIdsLoadMore(peerId, _storiesAddToAlbumId);\n\t}\n\n\tconst auto akey = Data::StoryAlbumIdsKey{ peerId, _storiesAddToAlbumId };\n\trpl::single(rpl::empty) | rpl::then(\n\t\tstories->albumIdsChanged() | rpl::filter(\n\t\t\trpl::mappers::_1 == akey\n\t\t) | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\t_storiesAddToAlbumTotal = stories->albumIdsCount(\n\t\t\tpeerId,\n\t\t\t_storiesAddToAlbumId);\n\n\t\tconst auto albumId = _storiesAddToAlbumId;\n\t\tconst auto &ids = stories->albumKnownInArchive(peerId, albumId);\n\t\tconst auto loadedCount = int(ids.size());\n\t\tconst auto total = std::max(_storiesAddToAlbumTotal, loadedCount);\n\t\tconst auto nonLoadedInAlbum = total - loadedCount;\n\n\t\tconst auto appConfig = &_controller->session().appConfig();\n\t\tconst auto totalLimit = appConfig->storiesAlbumLimit();\n\n\t\t_selectedLimit = std::max(totalLimit - nonLoadedInAlbum, 0);\n\t}, lifetime());\n}\n\nrpl::producer<int> ListWidget::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nrpl::producer<SelectedItems> ListWidget::selectedListValue() const {\n\treturn _selectedListStream.events_starting_with(\n\t\tcollectSelectedItems());\n}\n\nvoid ListWidget::selectionAction(SelectionAction action) {\n\tswitch (action) {\n\tcase SelectionAction::Clear: clearSelected(); return;\n\tcase SelectionAction::Forward: forwardSelected(); return;\n\tcase SelectionAction::Delete: deleteSelected(); return;\n\tcase SelectionAction::ToggleStoryToProfile:\n\t\ttoggleStoryInProfileSelected(true);\n\t\treturn;\n\tcase SelectionAction::ToggleStoryToArchive:\n\t\ttoggleStoryInProfileSelected(false);\n\t\treturn;\n\tcase SelectionAction::ToggleStoryPin: toggleStoryPinSelected(); return;\n\t}\n}\n\nvoid ListWidget::setReorderDescriptor(ReorderDescriptor descriptor) {\n\t_reorderDescriptor = std::move(descriptor);\n\tif (!_reorderDescriptor.save) {\n\t\tcancelReorder();\n\t}\n}\n\nQRect ListWidget::getCurrentSongGeometry() {\n\tconst auto type = AudioMsgId::Type::Song;\n\tconst auto current = ::Media::Player::instance()->current(type);\n\tif (const auto document = current.audio()) {\n\t\tconst auto contextId = current.contextId();\n\t\tif (const auto item = document->owner().message(contextId)) {\n\t\t\tif (const auto found = findItemByItem(item)) {\n\t\t\t\treturn found->geometry;\n\t\t\t}\n\t\t}\n\t}\n\treturn QRect(0, 0, width(), 0);\n}\n\nvoid ListWidget::restart() {\n\tmouseActionCancel();\n\n\t_overLayout = nullptr;\n\t_sections.clear();\n\t_heavyLayouts.clear();\n\n\t_provider->restart();\n\n\t_reorderState = {};\n}\n\nvoid ListWidget::itemRemoved(not_null<const HistoryItem*> item) {\n\tif (!_provider->isMyItem(item)) {\n\t\treturn;\n\t}\n\n\tif (_contextItem == item) {\n\t\t_contextItem = nullptr;\n\t}\n\n\tif (_reorderState.item && _reorderState.item->getItem() == item) {\n\t\t_reorderState = {};\n\t}\n\n\tauto needHeightRefresh = false;\n\tconst auto sectionIt = findSectionByItem(item);\n\tif (sectionIt != _sections.end()) {\n\t\tif (sectionIt->removeItem(item)) {\n\t\t\tif (sectionIt->empty()) {\n\t\t\t\t_sections.erase(sectionIt);\n\t\t\t}\n\t\t\tneedHeightRefresh = true;\n\t\t}\n\t}\n\n\tif (isItemLayout(item, _overLayout)) {\n\t\t_overLayout = nullptr;\n\t}\n\t_dragSelected.remove(item);\n\n\tif (_pressState.item == item) {\n\t\tmouseActionCancel();\n\t}\n\tif (_overState.item == item) {\n\t\t_mouseAction = MouseAction::None;\n\t\t_overState = {};\n\t}\n\n\tif (const auto i = _selected.find(item); i != _selected.cend()) {\n\t\tremoveItemSelection(i);\n\t}\n\n\tif (needHeightRefresh) {\n\t\trefreshHeight();\n\t}\n\tmouseActionUpdate(_mousePosition);\n}\n\nauto ListWidget::collectSelectedItems() const -> SelectedItems {\n\tconst auto convert = [&](\n\t\t\tnot_null<const HistoryItem*> item,\n\t\t\tconst SelectionData &selection) {\n\t\tauto result = SelectedItem(item->globalId());\n\t\tresult.canDelete = selection.canDelete;\n\t\tresult.canForward = selection.canForward;\n\t\tresult.canToggleStoryPin = selection.canToggleStoryPin;\n\t\tresult.canUnpinStory = selection.canUnpinStory;\n\t\tresult.storyInProfile = selection.storyInProfile;\n\t\treturn result;\n\t};\n\tconst auto transformation = [&](const auto &item) {\n\t\treturn convert(item.first, item.second);\n\t};\n\tauto items = SelectedItems(_provider->type());\n\tif (hasSelectedItems()) {\n\t\titems.list.reserve(_selected.size());\n\t\tstd::transform(\n\t\t\t_selected.begin(),\n\t\t\t_selected.end(),\n\t\t\tstd::back_inserter(items.list),\n\t\t\ttransformation);\n\t}\n\tif (_controller->storiesPeer() && items.list.size() > 1) {\n\t\t// Don't allow forwarding more than one story.\n\t\tfor (auto &entry : items.list) {\n\t\t\tentry.canForward = false;\n\t\t}\n\t}\n\treturn items;\n}\n\nMessageIdsList ListWidget::collectSelectedIds() const {\n\treturn collectSelectedIds(collectSelectedItems());\n}\n\nMessageIdsList ListWidget::collectSelectedIds(\n\t\tconst SelectedItems &items) const {\n\tconst auto session = &_controller->session();\n\treturn ranges::views::all(\n\t\titems.list\n\t) | ranges::views::transform([](auto &&item) {\n\t\treturn item.globalId;\n\t}) | ranges::views::filter([&](const GlobalMsgId &globalId) {\n\t\treturn (globalId.sessionUniqueId == session->uniqueId())\n\t\t\t&& (session->data().message(globalId.itemId) != nullptr);\n\t}) | ranges::views::transform([](const GlobalMsgId &globalId) {\n\t\treturn globalId.itemId;\n\t}) | ranges::to_vector;\n}\n\nvoid ListWidget::pushSelectedItems() {\n\t_selectedListStream.fire(collectSelectedItems());\n}\n\nbool ListWidget::hasSelected() const {\n\treturn !_selected.empty();\n}\n\nbool ListWidget::isSelectedItem(\n\t\tconst SelectedMap::const_iterator &i) const {\n\treturn (i != _selected.end())\n\t\t&& (i->second.text == FullSelection);\n}\n\nvoid ListWidget::removeItemSelection(\n\t\tconst SelectedMap::const_iterator &i) {\n\tExpects(i != _selected.cend());\n\t_selected.erase(i);\n\tif (_selected.empty()) {\n\t\tupdate();\n\t}\n\tpushSelectedItems();\n}\n\nbool ListWidget::hasSelectedText() const {\n\treturn hasSelected()\n\t\t&& !hasSelectedItems();\n}\n\nbool ListWidget::hasSelectedItems() const {\n\treturn isSelectedItem(_selected.cbegin());\n}\n\nvoid ListWidget::itemLayoutChanged(\n\t\tnot_null<const HistoryItem*> item) {\n\tif (isItemLayout(item, _overLayout)) {\n\t\tmouseActionUpdate();\n\t}\n}\n\nvoid ListWidget::repaintItem(const HistoryItem *item) {\n\tif (const auto found = findItemByItem(item)) {\n\t\trepaintItem(found->geometry);\n\t}\n}\n\nvoid ListWidget::repaintItem(const BaseLayout *item) {\n\tif (item) {\n\t\trepaintItem(item->getItem());\n\t}\n}\n\nvoid ListWidget::repaintItem(not_null<const BaseLayout*> item) {\n\trepaintItem(item->getItem());\n}\n\nvoid ListWidget::repaintItem(QRect itemGeometry) {\n\trtlupdate(itemGeometry);\n}\n\nbool ListWidget::isItemLayout(\n\t\tnot_null<const HistoryItem*> item,\n\t\tBaseLayout *layout) const {\n\treturn layout && (layout->getItem() == item);\n}\n\nvoid ListWidget::registerHeavyItem(not_null<const BaseLayout*> item) {\n\tif (!_heavyLayouts.contains(item)) {\n\t\t_heavyLayouts.emplace(item);\n\t\t_heavyLayoutsInvalidated = true;\n\t}\n}\n\nvoid ListWidget::unregisterHeavyItem(not_null<const BaseLayout*> item) {\n\tconst auto i = _heavyLayouts.find(item);\n\tif (i != _heavyLayouts.end()) {\n\t\t_heavyLayouts.erase(i);\n\t\t_heavyLayoutsInvalidated = true;\n\t}\n}\n\nbool ListWidget::itemVisible(not_null<const BaseLayout*> item) {\n\tif (const auto &found = findItemByItem(item->getItem())) {\n\t\tconst auto geometry = found->geometry;\n\t\treturn (geometry.top() < _visibleBottom)\n\t\t\t&& (geometry.top() + geometry.height() > _visibleTop);\n\t}\n\treturn true;\n}\n\nnot_null<StickerPremiumMark*> ListWidget::hiddenMark() {\n\treturn _hiddenMark.get();\n}\n\nQString ListWidget::tooltipText() const {\n\tif (const auto link = ClickHandler::getActive()) {\n\t\treturn link->tooltip();\n\t}\n\treturn QString();\n}\n\nQPoint ListWidget::tooltipPos() const {\n\treturn _mousePosition;\n}\n\nbool ListWidget::tooltipWindowActive() const {\n\treturn Ui::AppInFocus() && Ui::InFocusChain(window());\n}\n\nvoid ListWidget::openPhoto(not_null<PhotoData*> photo, FullMsgId id) {\n\tusing namespace Data;\n\n\tconst auto albumId = _controller->storiesAlbumId();\n\tconst auto context = Data::StoriesContext{\n\t\tData::StoriesContextAlbum{ albumId }\n\t};\n\t_controller->parentController()->openPhoto(\n\t\tphoto,\n\t\t{ id, topicRootId(), monoforumPeerId(), showDrawButton() },\n\t\t_controller->storiesPeer() ? &context : nullptr);\n}\n\nvoid ListWidget::openDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId id,\n\t\tbool showInMediaView) {\n\tconst auto albumId = _controller->storiesAlbumId();\n\tconst auto context = Data::StoriesContext{\n\t\tData::StoriesContextAlbum{ albumId }\n\t};\n\t_controller->parentController()->openDocument(\n\t\tdocument,\n\t\tshowInMediaView,\n\t\t{ id, topicRootId(), monoforumPeerId(), showDrawButton() },\n\t\t_controller->storiesPeer() ? &context : nullptr);\n}\n\nbool ListWidget::showDrawButton() const {\n\tif (const auto topic = _controller->key().topic()) {\n\t\treturn Data::CanSendAnyOf(topic, Data::FilesSendRestrictions());\n\t} else if (const auto sublist = _controller->key().sublist()) {\n\t\treturn Data::CanSendAnyOf(sublist, Data::FilesSendRestrictions());\n\t} else if (const auto peer = _controller->key().peer()) {\n\t\treturn Data::CanSendAnyOf(peer, Data::FilesSendRestrictions());\n\t}\n\treturn false;\n}\n\nvoid ListWidget::trackSession(not_null<Main::Session*> session) {\n\tif (_trackedSessions.contains(session)) {\n\t\treturn;\n\t}\n\tauto &lifetime = _trackedSessions.emplace(session).first->second;\n\tsubscribeToSession(session, lifetime);\n\tsession->account().sessionChanges(\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t_trackedSessions.remove(session);\n\t}, lifetime);\n}\n\nvoid ListWidget::markStoryMsgsSelected() {\n\tconst auto now = int(_storyMsgsToMarkSelected.size());\n\tconst auto guard = gsl::finally([&] {\n\t\tif (now != int(_storyMsgsToMarkSelected.size())) {\n\t\t\tpushSelectedItems();\n\t\t}\n\t});\n\tconst auto selection = FullSelection;\n\tfor (const auto &section : _sections) {\n\t\tfor (const auto &entry : section.items()) {\n\t\t\tconst auto item = entry->getItem();\n\t\t\tconst auto id = item->id;\n\t\t\tconst auto i = _storyMsgsToMarkSelected.find(id);\n\t\t\tif (i != end(_storyMsgsToMarkSelected)) {\n\t\t\t\tChangeItemSelection(\n\t\t\t\t\t_selected,\n\t\t\t\t\titem,\n\t\t\t\t\t_provider->computeSelectionData(item, selection),\n\t\t\t\t\t_selectedLimit);\n\t\t\t\trepaintItem(item);\n\t\t\t\t_storyMsgsToMarkSelected.erase(i);\n\t\t\t\tif (_storyMsgsToMarkSelected.empty()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ListWidget::refreshRows() {\n\tsaveScrollState();\n\n\t_reorderState = {};\n\t_sections.clear();\n\t_sections = _provider->fillSections(this);\n\n\tif (_controller->isDownloads() && !_sections.empty()) {\n\t\tfor (const auto &item : _sections.back().items()) {\n\t\t\ttrackSession(&item->getItem()->history()->session());\n\t\t}\n\t} else if (!_storyMsgsToMarkSelected.empty()) {\n\t\tmarkStoryMsgsSelected();\n\t}\n\n\tif (const auto count = _provider->fullCount()) {\n\t\tif (*count > kMediaCountForSearch) {\n\t\t\t_controller->setSearchEnabledByContent(true);\n\t\t}\n\t}\n\n\tresizeToWidth(width());\n\trestoreScrollState();\n\tmouseActionUpdate();\n\tupdate();\n}\n\nbool ListWidget::preventAutoHide() const {\n\treturn (_contextMenu != nullptr) || (_actionBoxWeak != nullptr);\n}\n\nvoid ListWidget::saveState(not_null<Memento*> memento) {\n\t_provider->saveState(memento, countScrollState());\n\t_trackedSessions.clear();\n}\n\nvoid ListWidget::restoreState(not_null<Memento*> memento) {\n\t_provider->restoreState(memento, [&](ListScrollTopState state) {\n\t\t_scrollTopState = state;\n\t});\n}\n\nint ListWidget::resizeGetHeight(int newWidth) {\n\tif (newWidth > 0) {\n\t\tfor (auto &section : _sections) {\n\t\t\tsection.setCanReorder(canReorder());\n\t\t\tsection.resizeToWidth(newWidth);\n\t\t}\n\t}\n\treturn recountHeight();\n}\n\nauto ListWidget::findSectionAndItem(QPoint point) const\n\t\t-> std::pair<std::vector<Section>::const_iterator, FoundItem> {\n\tExpects(!_sections.empty());\n\n\tauto sectionIt = findSectionAfterTop(point.y());\n\tif (sectionIt == _sections.end()) {\n\t\t--sectionIt;\n\t}\n\tconst auto shift = QPoint(0, sectionIt->top());\n\treturn {\n\t\tsectionIt,\n\t\tfoundItemInSection(\n\t\t\tsectionIt->findItemByPoint(point - shift),\n\t\t\t*sectionIt)\n\t};\n}\n\nauto ListWidget::findItemByPoint(QPoint point) const -> FoundItem {\n\treturn findSectionAndItem(point).second;\n}\n\nauto ListWidget::findItemByPointWithSection(QPoint point) const\n\t\t-> ListFoundItemWithSection {\n\tconst auto [sectionIt, item] = findSectionAndItem(point);\n\treturn { item, &(*sectionIt) };\n}\n\nauto ListWidget::findItemByItem(const HistoryItem *item)\n-> std::optional<FoundItem> {\n\tif (!item || !_provider->isPossiblyMyItem(item)) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto sectionIt = findSectionByItem(item);\n\tif (sectionIt != _sections.end()) {\n\t\tif (const auto found = sectionIt->findItemByItem(item)) {\n\t\t\treturn foundItemInSection(*found, *sectionIt);\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nauto ListWidget::findItemDetails(not_null<BaseLayout*> item)\n -> FoundItem {\n\tconst auto sectionIt = findSectionByItem(item->getItem());\n\tAssert(sectionIt != _sections.end());\n\treturn foundItemInSection(sectionIt->findItemDetails(item), *sectionIt);\n}\n\nauto ListWidget::foundItemInSection(\n\tconst FoundItem &item,\n\tconst Section &section) const\n-> FoundItem {\n\treturn {\n\t\titem.layout,\n\t\titem.geometry.translated(0, section.top()),\n\t\titem.exact,\n\t};\n}\n\nvoid ListWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\t_visibleTop = visibleTop;\n\t_visibleBottom = visibleBottom;\n\n\tcheckMoveToOtherViewer();\n\tclearHeavyItems();\n\n\tif (_dateBadge->goodType) {\n\t\tupdateDateBadgeFor(_visibleTop);\n\t\tif (!_visibleTop) {\n\t\t\tif (_dateBadge->shown) {\n\t\t\t\tscrollDateHide();\n\t\t\t} else {\n\t\t\t\tupdate(_dateBadge->rect);\n\t\t\t}\n\t\t} else {\n\t\t\t_dateBadge->check.call();\n\t\t}\n\t}\n\n\tsession().data().itemVisibilitiesUpdated();\n}\n\nvoid ListWidget::updateDateBadgeFor(int top) {\n\tif (_sections.empty()) {\n\t\treturn;\n\t}\n\tconst auto layout = findItemByPoint({ st::infoMediaSkip, top }).layout;\n\tconst auto rectHeight = st::msgServiceMargin.top()\n\t\t+ st::msgServicePadding.top()\n\t\t+ st::msgServiceFont->height\n\t\t+ st::msgServicePadding.bottom();\n\n\t_dateBadge->text = ItemDateText(layout->getItem(), false);\n\t_dateBadge->textWidth = st::msgServiceFont->width(_dateBadge->text);\n\t_dateBadge->rect = QRect(0, top, width(), rectHeight);\n}\n\nvoid ListWidget::scrollDateCheck() {\n\tif (!_dateBadge->shown) {\n\t\ttoggleScrollDateShown();\n\t}\n\t_dateBadge->hideTimer.callOnce(st::infoScrollDateHideTimeout);\n}\n\nvoid ListWidget::scrollDateHide() {\n\tif (_dateBadge->shown) {\n\t\ttoggleScrollDateShown();\n\t}\n}\n\nvoid ListWidget::toggleScrollDateShown() {\n\t_dateBadge->shown = !_dateBadge->shown;\n\t_dateBadge->opacity.start(\n\t\t[=] { update(_dateBadge->rect); },\n\t\t_dateBadge->shown ? 0. : 1.,\n\t\t_dateBadge->shown ? 1. : 0.,\n\t\tst::infoDateFadeDuration);\n}\n\nvoid ListWidget::checkMoveToOtherViewer() {\n\tconst auto visibleHeight = (_visibleBottom - _visibleTop);\n\tif (width() <= 0\n\t\t|| visibleHeight <= 0\n\t\t|| _sections.empty()\n\t\t|| _scrollTopState.item) {\n\t\treturn;\n\t}\n\n\tconst auto topItem = findItemByPoint({ st::infoMediaSkip, _visibleTop });\n\tconst auto bottomItem = findItemByPoint({ st::infoMediaSkip, _visibleBottom });\n\n\tconst auto preloadBefore = kPreloadIfLessThanScreens * visibleHeight;\n\tconst auto preloadTop = (_visibleTop < preloadBefore);\n\tconst auto preloadBottom = (height() - _visibleBottom < preloadBefore);\n\n\t_provider->checkPreload(\n\t\t{ width(), visibleHeight },\n\t\ttopItem.layout,\n\t\tbottomItem.layout,\n\t\tpreloadTop,\n\t\tpreloadBottom);\n}\n\nvoid ListWidget::clearHeavyItems() {\n\tconst auto visibleHeight = _visibleBottom - _visibleTop;\n\tif (!visibleHeight) {\n\t\treturn;\n\t}\n\t_heavyLayoutsInvalidated = false;\n\tconst auto above = _visibleTop - visibleHeight;\n\tconst auto below = _visibleBottom + visibleHeight;\n\tfor (auto i = _heavyLayouts.begin(); i != _heavyLayouts.end();) {\n\t\tconst auto item = const_cast<BaseLayout*>(i->get());\n\t\tconst auto rect = findItemDetails(item).geometry;\n\t\tif (rect.top() + rect.height() <= above || rect.top() >= below) {\n\t\t\ti = _heavyLayouts.erase(i);\n\t\t\titem->clearHeavyPart();\n\t\t\tif (_heavyLayoutsInvalidated) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tif (_heavyLayoutsInvalidated) {\n\t\tclearHeavyItems();\n\t}\n}\n\nListScrollTopState ListWidget::countScrollState() const {\n\tif (_sections.empty() || _visibleTop <= 0) {\n\t\treturn {};\n\t}\n\tconst auto topItem = findItemByPoint({ st::infoMediaSkip, _visibleTop });\n\tconst auto item = topItem.layout->getItem();\n\treturn {\n\t\t.position = _provider->scrollTopStatePosition(item),\n\t\t.item = item,\n\t\t.shift = _visibleTop - topItem.geometry.y(),\n\t};\n}\n\nvoid ListWidget::saveScrollState() {\n\tif (!_scrollTopState.item) {\n\t\t_scrollTopState = countScrollState();\n\t}\n}\n\nvoid ListWidget::restoreScrollState() {\n\tif (_sections.empty() || !_scrollTopState.position) {\n\t\treturn;\n\t}\n\t_scrollTopState.item = _provider->scrollTopStateItem(_scrollTopState);\n\tif (!_scrollTopState.item) {\n\t\treturn;\n\t}\n\tauto sectionIt = findSectionByItem(_scrollTopState.item);\n\tif (sectionIt == _sections.end()) {\n\t\t--sectionIt;\n\t}\n\tconst auto found = sectionIt->findItemByItem(_scrollTopState.item);\n\tif (!found) {\n\t\treturn;\n\t}\n\tconst auto item = foundItemInSection(*found, *sectionIt);\n\tconst auto newVisibleTop = item.geometry.y() + _scrollTopState.shift;\n\tif (_visibleTop != newVisibleTop) {\n\t\t_scrollToRequests.fire_copy(newVisibleTop);\n\t}\n\t_scrollTopState = ListScrollTopState();\n}\n\nMsgId ListWidget::topicRootId() const {\n\tconst auto topic = _controller->key().topic();\n\treturn topic ? topic->rootId() : MsgId(0);\n}\n\nPeerId ListWidget::monoforumPeerId() const {\n\tconst auto sublist = _controller->key().sublist();\n\treturn sublist ? sublist->sublistPeer()->id : PeerId(0);\n}\n\nQMargins ListWidget::padding() const {\n\treturn st::infoMediaMargin;\n}\n\nvoid ListWidget::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tconst auto outerWidth = width();\n\tconst auto clip = e->rect();\n\tconst auto ms = crl::now();\n\tconst auto fromSectionIt = findSectionAfterTop(clip.y());\n\tconst auto tillSectionIt = findSectionAfterBottom(\n\t\tfromSectionIt,\n\t\tclip.y() + clip.height());\n\tconst auto window = _controller->parentController();\n\tconst auto paused = window->isGifPausedAtLeastFor(\n\t\tWindow::GifPauseReason::Layer);\n\tconst auto selecting = hasSelectedItems() || _storiesAddToAlbumId;\n\tconst auto paintContext = Overview::Layout::PaintContext(ms, selecting, paused);\n\tauto context = ListContext{\n\t\tpaintContext,\n\t\t&_selected,\n\t\t&_dragSelected,\n\t\t_dragSelectAction\n\t};\n\tif (_mouseAction == MouseAction::Reordering && _reorderState.item) {\n\t\tcontext.draggedItem = _reorderState.item;\n\t}\n\tfor (auto it = fromSectionIt; it != tillSectionIt; ++it) {\n\t\tconst auto top = it->top();\n\t\tp.translate(0, top);\n\t\tit->paint(p, context, clip.translated(0, -top), outerWidth);\n\t\tp.translate(0, -top);\n\t}\n\tif (fromSectionIt != _sections.end()) {\n\t\tfromSectionIt->paintFloatingHeader(p, _visibleTop, outerWidth);\n\t}\n\n\tif (_mouseAction == MouseAction::Reordering && _reorderState.item) {\n\t\tconst auto o = ScopedPainterOpacity(p, 0.8);\n\t\tp.translate(_reorderState.currentPos);\n\t\tconst auto isOneColumn = _reorderState.section\n\t\t\t&& _reorderState.section->isOneColumn();\n\t\t_reorderState.item->paint(\n\t\t\tp,\n\t\t\tQRect(\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\t_reorderState.item->maxWidth(),\n\t\t\t\t_reorderState.item->minHeight()),\n\t\t\tisOneColumn ? TextSelection{} : FullSelection,\n\t\t\t&context.layoutContext);\n\n\t\tif (isOneColumn) {\n\t\t\tst::stickersReorderIcon.paint(\n\t\t\t\tp,\n\t\t\t\twidth()\n\t\t\t\t\t- _reorderState.section->oneColumnRightPadding() * 2,\n\t\t\t\t(_reorderState.item->minHeight()\n\t\t\t\t\t- st::stickersReorderIcon.height()) / 2,\n\t\t\t\touterWidth);\n\t\t}\n\t\tp.translate(-_reorderState.currentPos);\n\t}\n\n\tif (_dateBadge->goodType && clip.intersects(_dateBadge->rect)) {\n\t\tconst auto scrollDateOpacity\n\t\t\t= _dateBadge->opacity.value(_dateBadge->shown ? 1. : 0.);\n\t\tif (scrollDateOpacity > 0.) {\n\t\t\tp.setOpacity(scrollDateOpacity);\n\t\t\tif (_dateBadge->corners.p[0].isNull()) {\n\t\t\t\t_dateBadge->corners = Ui::PrepareCornerPixmaps(\n\t\t\t\t\tUi::HistoryServiceMsgRadius(),\n\t\t\t\t\tst::roundedBg);\n\t\t\t}\n\t\t\tHistoryView::ServiceMessagePainter::PaintDate(\n\t\t\t\tp,\n\t\t\t\tst::roundedBg,\n\t\t\t\t_dateBadge->corners,\n\t\t\t\tst::roundedFg,\n\t\t\t\t_dateBadge->text,\n\t\t\t\t_dateBadge->textWidth,\n\t\t\t\t_visibleTop,\n\t\t\t\touterWidth,\n\t\t\t\tfalse);\n\t\t}\n\t}\n}\n\nvoid ListWidget::mousePressEvent(QMouseEvent *e) {\n\tif (_contextMenu) {\n\t\te->accept();\n\t\treturn; // ignore mouse press, that was hiding context menu\n\t}\n\tmouseActionStart(e->globalPos(), e->button());\n}\n\nvoid ListWidget::mouseMoveEvent(QMouseEvent *e) {\n\tconst auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton));\n\tif (!buttonsPressed && _mouseAction != MouseAction::None) {\n\t\tmouseReleaseEvent(e);\n\t}\n\tmouseActionUpdate(e->globalPos());\n}\n\nvoid ListWidget::mouseReleaseEvent(QMouseEvent *e) {\n\tmouseActionFinish(e->globalPos(), e->button());\n\tif (!rect().contains(e->pos())) {\n\t\tleaveEvent(e);\n\t}\n}\n\nvoid ListWidget::mouseDoubleClickEvent(QMouseEvent *e) {\n\tmouseActionStart(e->globalPos(), e->button());\n\ttrySwitchToWordSelection();\n}\n\nvoid ListWidget::showContextMenu(\n\t\tQContextMenuEvent *e,\n\t\tContextMenuSource source) {\n\tif (_storiesAddToAlbumId) {\n\t\treturn;\n\t}\n\tif (_contextMenu) {\n\t\t_contextMenu = nullptr;\n\t\trepaintItem(_contextItem);\n\t}\n\tif (e->reason() == QContextMenuEvent::Mouse) {\n\t\tmouseActionUpdate(e->globalPos());\n\t}\n\n\tconst auto item = _overState.item;\n\tif (!item || !_overState.inside) {\n\t\treturn;\n\t}\n\t_contextItem = item;\n\tconst auto globalId = item->globalId();\n\n\tenum class SelectionState {\n\t\tNoSelectedItems,\n\t\tNotOverSelectedItems,\n\t\tOverSelectedItems,\n\t\tNotOverSelectedText,\n\t\tOverSelectedText,\n\t};\n\tauto overSelected = SelectionState::NoSelectedItems;\n\tif (source == ContextMenuSource::Touch) {\n\t\tif (hasSelectedItems()) {\n\t\t\toverSelected = SelectionState::OverSelectedItems;\n\t\t} else if (hasSelectedText()) {\n\t\t\toverSelected = SelectionState::OverSelectedItems;\n\t\t}\n\t} else if (hasSelectedText()) {\n\t\t// #TODO text selection\n\t} else if (hasSelectedItems()) {\n\t\tauto it = _selected.find(_overState.item);\n\t\tif (isSelectedItem(it) && _overState.inside) {\n\t\t\toverSelected = SelectionState::OverSelectedItems;\n\t\t} else {\n\t\t\toverSelected = SelectionState::NotOverSelectedItems;\n\t\t}\n\t}\n\n\tconst auto canDeleteAll = [&] {\n\t\treturn ranges::none_of(_selected, [](auto &&item) {\n\t\t\treturn !item.second.canDelete;\n\t\t});\n\t};\n\tconst auto canForwardAll = [&] {\n\t\treturn ranges::none_of(_selected, [](auto &&item) {\n\t\t\treturn !item.second.canForward;\n\t\t}) && (!_controller->key().storiesPeer() || _selected.size() == 1);\n\t};\n\tconst auto canToggleStoryPinAll = [&] {\n\t\treturn ranges::none_of(_selected, [](auto &&item) {\n\t\t\treturn !item.second.canToggleStoryPin;\n\t\t});\n\t};\n\tconst auto allInProfile = [&] {\n\t\treturn ranges::all_of(_selected, [](auto &&item) {\n\t\t\treturn item.second.storyInProfile;\n\t\t});\n\t};\n\tconst auto canUnpinStoryAll = [&] {\n\t\treturn ranges::any_of(_selected, [](auto &&item) {\n\t\t\treturn item.second.canUnpinStory;\n\t\t});\n\t};\n\n\tconst auto link = ClickHandler::getActive();\n\n\t_contextMenu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\tst::popupMenuWithIcons);\n\tif (item->isHistoryEntry()) {\n\t\t_contextMenu->addAction(\n\t\t\ttr::lng_context_to_msg(tr::now),\n\t\t\t[=] {\n\t\t\t\tif (const auto item = MessageByGlobalId(globalId)) {\n\t\t\t\t\tJumpToMessageClickHandler(item)->onClick({});\n\t\t\t\t}\n\t\t\t},\n\t\t\t&st::menuIconShowInChat);\n\t}\n\n\tconst auto lnkPhoto = link\n\t\t? reinterpret_cast<PhotoData*>(\n\t\t\tlink->property(kPhotoLinkMediaProperty).toULongLong())\n\t\t: nullptr;\n\tconst auto lnkDocument = link\n\t\t? reinterpret_cast<DocumentData*>(\n\t\t\tlink->property(kDocumentLinkMediaProperty).toULongLong())\n\t\t: nullptr;\n\tif (lnkPhoto || lnkDocument) {\n\t\tif (lnkPhoto) {\n\t\t} else {\n\t\t\tif (lnkDocument->loading()) {\n\t\t\t\t_contextMenu->addAction(\n\t\t\t\t\ttr::lng_context_cancel_download(tr::now),\n\t\t\t\t\t[lnkDocument] {\n\t\t\t\t\t\tlnkDocument->cancel();\n\t\t\t\t\t},\n\t\t\t\t\t&st::menuIconCancel);\n\t\t\t} else {\n\t\t\t\tconst auto filepath = _provider->showInFolderPath(\n\t\t\t\t\titem,\n\t\t\t\t\tlnkDocument);\n\t\t\t\tif (!filepath.isEmpty()) {\n\t\t\t\t\tconst auto handler = base::fn_delayed(\n\t\t\t\t\t\tst::defaultDropdownMenu.menu.ripple.hideDuration,\n\t\t\t\t\t\tthis,\n\t\t\t\t\t\t[filepath] {\n\t\t\t\t\t\t\tFile::ShowInFolder(filepath);\n\t\t\t\t\t\t});\n\t\t\t\t\t_contextMenu->addAction(\n\t\t\t\t\t\t(Platform::IsMac()\n\t\t\t\t\t\t\t? tr::lng_context_show_in_finder(tr::now)\n\t\t\t\t\t\t\t: tr::lng_context_show_in_folder(tr::now)),\n\t\t\t\t\t\tstd::move(handler),\n\t\t\t\t\t\t&st::menuIconShowInFolder);\n\t\t\t\t}\n\t\t\t\tconst auto handler = base::fn_delayed(\n\t\t\t\t\tst::defaultDropdownMenu.menu.ripple.hideDuration,\n\t\t\t\t\tthis,\n\t\t\t\t\t[=] {\n\t\t\t\t\t\tDocumentSaveClickHandler::SaveAndTrack(\n\t\t\t\t\t\t\tglobalId.itemId,\n\t\t\t\t\t\t\tlnkDocument,\n\t\t\t\t\t\t\tDocumentSaveClickHandler::Mode::ToNewFile);\n\t\t\t\t\t});\n\t\t\t\tif (_provider->allowSaveFileAs(item, lnkDocument)) {\n\t\t\t\t\tHistoryView::AddSaveDocumentAction(\n\t\t\t\t\t\tUi::Menu::CreateAddActionCallback(_contextMenu),\n\t\t\t\t\t\titem,\n\t\t\t\t\t\tlnkDocument,\n\t\t\t\t\t\t_controller->parentController());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if (link) {\n\t\tconst auto actionText = link->copyToClipboardContextItemText();\n\t\tif (!actionText.isEmpty()) {\n\t\t\t_contextMenu->addAction(\n\t\t\t\tactionText,\n\t\t\t\t[text = link->copyToClipboardText()] {\n\t\t\t\t\tQGuiApplication::clipboard()->setText(text);\n\t\t\t\t},\n\t\t\t\t&st::menuIconCopy);\n\t\t\tForkgram::FillUrlWithCustomUri(\n\t\t\t\t_contextMenu.get(),\n\t\t\t\tlink->copyToClipboardText());\n\t\t}\n\t}\n\tif (overSelected == SelectionState::OverSelectedItems) {\n\t\tif (canToggleStoryPinAll()) {\n\t\t\tconst auto toProfile = !allInProfile();\n\t\t\t_contextMenu->addAction(\n\t\t\t\t(toProfile\n\t\t\t\t\t? tr::lng_mediaview_save_to_profile\n\t\t\t\t\t: tr::lng_archived_add)(tr::now),\n\t\t\t\tcrl::guard(this, [=] {\n\t\t\t\t\ttoggleStoryInProfileSelected(toProfile);\n\t\t\t\t}),\n\t\t\t\t(toProfile\n\t\t\t\t\t? &st::menuIconStoriesSave\n\t\t\t\t\t: &st::menuIconStoriesArchive));\n\t\t\tif (!toProfile) {\n\t\t\t\tconst auto unpin = canUnpinStoryAll();\n\t\t\t\t_contextMenu->addAction(\n\t\t\t\t\t(unpin\n\t\t\t\t\t\t? tr::lng_context_unpin_from_top\n\t\t\t\t\t\t: tr::lng_context_pin_to_top)(tr::now),\n\t\t\t\t\tcrl::guard(\n\t\t\t\t\t\tthis,\n\t\t\t\t\t\t[this] { toggleStoryPinSelected(); }),\n\t\t\t\t\t(unpin ? &st::menuIconUnpin : &st::menuIconPin));\n\t\t\t}\n\t\t}\n\t\tif (canForwardAll()) {\n\t\t\t_contextMenu->addAction(\n\t\t\t\ttr::lng_context_forward_selected(tr::now),\n\t\t\t\tcrl::guard(this, [this] {\n\t\t\t\t\tforwardSelected();\n\t\t\t\t}),\n\t\t\t\t&st::menuIconForward);\n\t\t}\n\t\tif (canDeleteAll()) {\n\t\t\t_contextMenu->addAction(\n\t\t\t\t(_controller->isDownloads()\n\t\t\t\t\t? tr::lng_context_delete_from_disk(tr::now)\n\t\t\t\t\t: tr::lng_context_delete_selected(tr::now)),\n\t\t\t\tcrl::guard(this, [this] {\n\t\t\t\t\tdeleteSelected();\n\t\t\t\t}),\n\t\t\t\t&st::menuIconDelete);\n\t\t}\n\t\t_contextMenu->addAction(\n\t\t\ttr::lng_context_clear_selection(tr::now),\n\t\t\tcrl::guard(this, [this] {\n\t\t\t\tclearSelected();\n\t\t\t}),\n\t\t\t&st::menuIconSelect);\n\t\tFork::AddShowSumDurations(\n\t\t\t_contextMenu,\n\t\t\tcollectSelectedItems(),\n\t\t\t_controller->parentController());\n\t} else {\n\t\tif (overSelected != SelectionState::NotOverSelectedItems) {\n\t\t\tconst auto selectionData = _provider->computeSelectionData(\n\t\t\t\titem,\n\t\t\t\tFullSelection);\n\t\t\tif (selectionData.canToggleStoryPin) {\n\t\t\t\tconst auto toProfile = !selectionData.storyInProfile;\n\t\t\t\t_contextMenu->addAction(\n\t\t\t\t\t(toProfile\n\t\t\t\t\t\t? tr::lng_mediaview_save_to_profile\n\t\t\t\t\t\t: tr::lng_mediaview_archive_story)(tr::now),\n\t\t\t\t\tcrl::guard(this, [=] {\n\t\t\t\t\t\ttoggleStoryInProfile(\n\t\t\t\t\t\t\t{ 1, globalId.itemId },\n\t\t\t\t\t\t\ttoProfile);\n\t\t\t\t\t}),\n\t\t\t\t\t(toProfile\n\t\t\t\t\t\t? &st::menuIconStoriesSave\n\t\t\t\t\t\t: &st::menuIconStoriesArchive));\n\t\t\t\tif (!toProfile) {\n\t\t\t\t\tconst auto unpin = selectionData.canUnpinStory;\n\t\t\t\t\t_contextMenu->addAction(\n\t\t\t\t\t\t(unpin\n\t\t\t\t\t\t\t? tr::lng_context_unpin_from_top\n\t\t\t\t\t\t\t: tr::lng_context_pin_to_top)(tr::now),\n\t\t\t\t\t\tcrl::guard(this, [=] { toggleStoryPin(\n\t\t\t\t\t\t\t{ 1, globalId.itemId },\n\t\t\t\t\t\t\t!unpin); }),\n\t\t\t\t\t\t(unpin ? &st::menuIconUnpin : &st::menuIconPin));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (selectionData.canForward) {\n\t\t\t\t_contextMenu->addAction(\n\t\t\t\t\ttr::lng_context_forward_msg(tr::now),\n\t\t\t\t\tcrl::guard(this, [=] { forwardItem(globalId); }),\n\t\t\t\t\t&st::menuIconForward);\n\t\t\t}\n\t\t\tif (selectionData.canDelete) {\n\t\t\t\tif (_controller->isDownloads()) {\n\t\t\t\t\t_contextMenu->addAction(\n\t\t\t\t\t\ttr::lng_context_delete_from_disk(tr::now),\n\t\t\t\t\t\tcrl::guard(this, [=] { deleteItem(globalId); }),\n\t\t\t\t\t\t&st::menuIconDelete);\n\t\t\t\t} else {\n\t\t\t\t\t_contextMenu->addAction(Ui::DeleteMessageContextAction(\n\t\t\t\t\t\t_contextMenu->menu(),\n\t\t\t\t\t\tcrl::guard(this, [=] { deleteItem(globalId); }),\n\t\t\t\t\t\titem->ttlDestroyAt(),\n\t\t\t\t\t\t[=] { _contextMenu = nullptr; }));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (const auto peer = _controller->key().storiesPeer()) {\n\t\t\tif (!peer->isSelf() && IsStoryMsgId(globalId.itemId.msg)) {\n\t\t\t\t::Media::Stories::AddStealthModeMenu(\n\t\t\t\t\tUi::Menu::CreateAddActionCallback(_contextMenu),\n\t\t\t\t\tpeer,\n\t\t\t\t\t_controller->parentController());\n\t\t\t\tconst auto storyId = FullStoryId{\n\t\t\t\t\tglobalId.itemId.peer,\n\t\t\t\t\tStoryIdFromMsgId(globalId.itemId.msg),\n\t\t\t\t};\n\t\t\t\t_contextMenu->addAction(\n\t\t\t\t\ttr::lng_profile_report(tr::now),\n\t\t\t\t\t[=] { ::Media::Stories::ReportRequested(\n\t\t\t\t\t\t_controller->uiShow(),\n\t\t\t\t\t\tstoryId); },\n\t\t\t\t\t&st::menuIconReport);\n\t\t\t}\n\t\t}\n\t\tif (!_provider->hasSelectRestriction()) {\n\t\t\t_contextMenu->addAction(\n\t\t\t\ttr::lng_context_select_msg(tr::now),\n\t\t\t\tcrl::guard(this, [=] {\n\t\t\t\t\tif (hasSelectedText()) {\n\t\t\t\t\t\tclearSelected();\n\t\t\t\t\t} else if (_selected.size() == _selectedLimit) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t} else if (_selected.empty()) {\n\t\t\t\t\t\tupdate();\n\t\t\t\t\t}\n\t\t\t\t\tapplyItemSelection(\n\t\t\t\t\t\tMessageByGlobalId(globalId),\n\t\t\t\t\t\tFullSelection);\n\t\t\t\t}),\n\t\t\t\t&st::menuIconSelect);\n\t\t}\n\t}\n\n\tif (_contextMenu->empty()) {\n\t\t_contextMenu = nullptr;\n\t\treturn;\n\t}\n\t_contextMenu->setDestroyedCallback(crl::guard(\n\t\tthis,\n\t\t[=] {\n\t\t\tmouseActionUpdate(QCursor::pos());\n\t\t\trepaintItem(MessageByGlobalId(globalId));\n\t\t\t_checkForHide.fire({});\n\t\t}));\n\t_contextMenu->popup(e->globalPos());\n\te->accept();\n}\n\nvoid ListWidget::contextMenuEvent(QContextMenuEvent *e) {\n\tshowContextMenu(\n\t\te,\n\t\t(e->reason() == QContextMenuEvent::Mouse)\n\t\t\t? ContextMenuSource::Mouse\n\t\t\t: ContextMenuSource::Other);\n}\n\nvoid ListWidget::forwardSelected() {\n\tif (auto items = collectSelectedIds(); !items.empty()) {\n\t\tforwardItems(std::move(items));\n\t}\n}\n\nvoid ListWidget::forwardItem(GlobalMsgId globalId) {\n\tconst auto session = &_controller->session();\n\tif (globalId.sessionUniqueId == session->uniqueId()) {\n\t\tif (const auto item = session->data().message(globalId.itemId)) {\n\t\t\tforwardItems({ 1, item->fullId() });\n\t\t}\n\t}\n}\n\nvoid ListWidget::forwardItems(MessageIdsList &&items) {\n\tif (_controller->storiesPeer()) {\n\t\tif (items.size() == 1 && IsStoryMsgId(items.front().msg)) {\n\t\t\tconst auto id = items.front();\n\t\t\t_controller->parentController()->show(\n\t\t\t\t::Media::Stories::PrepareShareBox(\n\t\t\t\t\t_controller->parentController()->uiShow(),\n\t\t\t\t\t{ id.peer, StoryIdFromMsgId(id.msg) }));\n\t\t}\n\t} else {\n\t\tconst auto callback = [weak = base::make_weak(this)] {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->clearSelected();\n\t\t\t}\n\t\t};\n\t\tsetActionBoxWeak(Window::ShowForwardMessagesBox(\n\t\t\t_controller,\n\t\t\tstd::move(items),\n\t\t\tstd::move(callback)));\n\t}\n}\n\nvoid ListWidget::deleteSelected() {\n\tdeleteItems(collectSelectedItems(), crl::guard(this, [=]{\n\t\tclearSelected();\n\t}));\n}\n\nvoid ListWidget::toggleStoryInProfileSelected(bool toProfile) {\n\ttoggleStoryInProfile(\n\t\tcollectSelectedIds(),\n\t\ttoProfile,\n\t\tcrl::guard(this, [=] { clearSelected(); }));\n}\n\nvoid ListWidget::toggleStoryPinSelected() {\n\tconst auto items = collectSelectedItems();\n\tconst auto pin = ranges::none_of(\n\t\titems.list,\n\t\t&SelectedItem::canUnpinStory);\n\ttoggleStoryPin(collectSelectedIds(items), pin, crl::guard(this, [=] {\n\t\tclearSelected();\n\t}));\n}\n\nvoid ListWidget::toggleStoryInProfile(\n\t\tMessageIdsList &&items,\n\t\tbool toProfile,\n\t\tFn<void()> confirmed) {\n\tauto list = std::vector<FullStoryId>();\n\tfor (const auto &id : items) {\n\t\tif (IsStoryMsgId(id.msg)) {\n\t\t\tlist.push_back({ id.peer, StoryIdFromMsgId(id.msg) });\n\t\t}\n\t}\n\tif (list.empty()) {\n\t\treturn;\n\t}\n\tconst auto channel = peerIsChannel(list.front().peer);\n\tconst auto count = int(list.size());\n\tconst auto controller = _controller;\n\tconst auto sure = [=](Fn<void()> close) {\n\t\tusing namespace ::Media::Stories;\n\t\tcontroller->session().data().stories().toggleInProfileList(\n\t\t\tlist,\n\t\t\ttoProfile);\n\t\tcontroller->showToast(\n\t\t\tPrepareToggleInProfileToast(channel, count, toProfile));\n\t\tclose();\n\t\tif (confirmed) {\n\t\t\tconfirmed();\n\t\t}\n\t};\n\tconst auto onePhrase = toProfile\n\t\t? (channel\n\t\t\t? tr::lng_stories_channel_save_sure\n\t\t\t: tr::lng_stories_save_sure)\n\t\t: (channel\n\t\t\t? tr::lng_stories_channel_archive_sure\n\t\t\t: tr::lng_stories_archive_sure);\n\tconst auto manyPhrase = toProfile\n\t\t? (channel\n\t\t\t? tr::lng_stories_channel_save_sure_many\n\t\t\t: tr::lng_stories_save_sure_many)\n\t\t: (channel\n\t\t\t? tr::lng_stories_channel_archive_sure_many\n\t\t\t: tr::lng_stories_archive_sure_many);\n\t_controller->parentController()->show(Ui::MakeConfirmBox({\n\t\t.text = (count == 1\n\t\t\t? onePhrase()\n\t\t\t: manyPhrase(lt_count, rpl::single(count) | tr::to_count())),\n\t\t.confirmed = sure,\n\t\t.confirmText = tr::lng_box_ok(),\n\t}));\n}\n\nvoid ListWidget::toggleStoryPin(\n\t\tMessageIdsList &&items,\n\t\tbool pin,\n\t\tFn<void()> confirmed) {\n\tauto list = std::vector<FullStoryId>();\n\tfor (const auto &id : items) {\n\t\tif (IsStoryMsgId(id.msg)) {\n\t\t\tlist.push_back({ id.peer, StoryIdFromMsgId(id.msg) });\n\t\t}\n\t}\n\tif (list.empty()) {\n\t\treturn;\n\t}\n\tconst auto channel = peerIsChannel(list.front().peer);\n\tconst auto count = int(list.size());\n\tconst auto controller = _controller;\n\tconst auto stories = &controller->session().data().stories();\n\tif (stories->canTogglePinnedList(list, pin)) {\n\t\tusing namespace ::Media::Stories;\n\t\tstories->togglePinnedList(list, pin);\n\t\tcontroller->showToast(PrepareTogglePinToast(channel, count, pin));\n\t\tif (confirmed) {\n\t\t\tconfirmed();\n\t\t}\n\t} else {\n\t\tconst auto limit = stories->maxPinnedCount();\n\t\tcontroller->showToast(\n\t\t\ttr::lng_mediaview_pin_limit(tr::now, lt_count, limit));\n\t}\n}\n\nvoid ListWidget::deleteItem(GlobalMsgId globalId) {\n\tif (const auto item = MessageByGlobalId(globalId)) {\n\t\tauto items = SelectedItems(_provider->type());\n\t\titems.list.push_back(SelectedItem(item->globalId()));\n\t\tconst auto selectionData = _provider->computeSelectionData(\n\t\t\titem,\n\t\t\tFullSelection);\n\t\titems.list.back().canDelete = selectionData.canDelete;\n\t\tdeleteItems(std::move(items));\n\t}\n}\n\nvoid ListWidget::deleteItems(SelectedItems &&items, Fn<void()> confirmed) {\n\tconst auto window = _controller->parentController();\n\tif (items.list.empty()) {\n\t\treturn;\n\t} else if (_controller->isDownloads()) {\n\t\tconst auto count = items.list.size();\n\t\tconst auto allInCloud = ranges::all_of(items.list, [](\n\t\t\t\tconst SelectedItem &entry) {\n\t\t\tconst auto item = MessageByGlobalId(entry.globalId);\n\t\t\treturn item && item->isHistoryEntry();\n\t\t});\n\t\tconst auto phrase = (count == 1)\n\t\t\t? tr::lng_downloads_delete_sure_one(tr::now)\n\t\t\t: tr::lng_downloads_delete_sure(tr::now, lt_count, count);\n\t\tconst auto added = !allInCloud\n\t\t\t? QString()\n\t\t\t: (count == 1\n\t\t\t\t? tr::lng_downloads_delete_in_cloud_one(tr::now)\n\t\t\t\t: tr::lng_downloads_delete_in_cloud(tr::now));\n\t\tconst auto deleteSure = [=] {\n\t\t\tUi::PostponeCall(this, [=] {\n\t\t\t\tif (const auto box = _actionBoxWeak.get()) {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t}\n\t\t\t});\n\t\t\tconst auto ids = ranges::views::all(\n\t\t\t\titems.list\n\t\t\t) | ranges::views::transform([](const SelectedItem &item) {\n\t\t\t\treturn item.globalId;\n\t\t\t}) | ranges::to_vector;\n\t\t\tCore::App().downloadManager().deleteFiles(ids);\n\t\t\tif (confirmed) {\n\t\t\t\tconfirmed();\n\t\t\t}\n\t\t};\n\t\tsetActionBoxWeak(window->show(Ui::MakeConfirmBox({\n\t\t\t.text = phrase + (added.isEmpty() ? QString() : \"\\n\\n\" + added),\n\t\t\t.confirmed = deleteSure,\n\t\t\t.confirmText = tr::lng_box_delete(tr::now),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t})));\n\t} else if (_controller->storiesPeer()) {\n\t\tauto list = std::vector<FullStoryId>();\n\t\tfor (const auto &item : items.list) {\n\t\t\tconst auto id = item.globalId.itemId;\n\t\t\tif (IsStoryMsgId(id.msg)) {\n\t\t\t\tlist.push_back({ id.peer, StoryIdFromMsgId(id.msg) });\n\t\t\t}\n\t\t}\n\t\tconst auto session = &_controller->session();\n\t\tconst auto sure = [=](Fn<void()> close) {\n\t\t\tsession->data().stories().deleteList(list);\n\t\t\tclose();\n\t\t\tif (confirmed) {\n\t\t\t\tconfirmed();\n\t\t\t}\n\t\t};\n\t\tconst auto count = int(list.size());\n\t\twindow->show(Ui::MakeConfirmBox({\n\t\t\t.text = (count == 1\n\t\t\t\t? tr::lng_stories_delete_one_sure()\n\t\t\t\t: tr::lng_stories_delete_sure(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(count) | tr::to_count())),\n\t\t\t.confirmed = sure,\n\t\t\t.confirmText = tr::lng_selected_delete(),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t}));\n\t} else if (auto list = collectSelectedIds(items); !list.empty()) {\n\t\tauto box = Box<DeleteMessagesBox>(\n\t\t\t&_controller->session(),\n\t\t\tstd::move(list));\n\t\tconst auto weak = box.data();\n\t\twindow->show(std::move(box));\n\t\tsetActionBoxWeak(weak);\n\t\tif (confirmed) {\n\t\t\tweak->setDeleteConfirmedCallback(std::move(confirmed));\n\t\t}\n\t}\n}\n\nvoid ListWidget::setActionBoxWeak(base::weak_qptr<Ui::BoxContent> box) {\n\tif ((_actionBoxWeak = box)) {\n\t\t_actionBoxWeakLifetime = _actionBoxWeak->alive(\n\t\t) | rpl::on_done([weak = base::make_weak(this)]{\n\t\t\tif (weak) {\n\t\t\t\tweak->_checkForHide.fire({});\n\t\t\t}\n\t\t});\n\t}\n}\n\nvoid ListWidget::trySwitchToWordSelection() {\n\tconst auto selectingSome = (_mouseAction == MouseAction::Selecting)\n\t\t&& hasSelectedText();\n\tconst auto willSelectSome = (_mouseAction == MouseAction::None)\n\t\t&& !hasSelectedItems();\n\tconst auto checkSwitchToWordSelection = _overLayout\n\t\t&& (_mouseSelectType == TextSelectType::Letters)\n\t\t&& (selectingSome || willSelectSome);\n\tif (checkSwitchToWordSelection) {\n\t\tswitchToWordSelection();\n\t}\n}\n\nvoid ListWidget::switchToWordSelection() {\n\tExpects(_overLayout != nullptr);\n\n\tStateRequest request;\n\trequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;\n\tauto dragState = _overLayout->getState(_pressState.cursor, request);\n\tif (dragState.cursor != CursorState::Text) {\n\t\treturn;\n\t}\n\t_mouseTextSymbol = dragState.symbol;\n\t_mouseSelectType = TextSelectType::Words;\n\tif (_mouseAction == MouseAction::None) {\n\t\t_mouseAction = MouseAction::Selecting;\n\t\tclearSelected();\n\t\tconst auto selStatus = TextSelection {\n\t\t\tdragState.symbol,\n\t\t\tdragState.symbol\n\t\t};\n\t\tapplyItemSelection(_overState.item, selStatus);\n\t}\n\tmouseActionUpdate();\n\n\t_trippleClickPoint = _mousePosition;\n\t_trippleClickStartTime = crl::now();\n}\n\nvoid ListWidget::applyItemSelection(\n\t\tHistoryItem *item,\n\t\tTextSelection selection) {\n\tif (item\n\t\t&& ChangeItemSelection(\n\t\t\t_selected,\n\t\t\titem,\n\t\t\t_provider->computeSelectionData(item, selection),\n\t\t\t_selectedLimit)) {\n\t\trepaintItem(item);\n\t\tpushSelectedItems();\n\t}\n}\n\nvoid ListWidget::toggleItemSelection(not_null<HistoryItem*> item) {\n\tconst auto it = _selected.find(item);\n\tif (it == _selected.cend()) {\n\t\tapplyItemSelection(item, FullSelection);\n\t} else {\n\t\tremoveItemSelection(it);\n\t}\n}\n\nbool ListWidget::isItemUnderPressSelected() const {\n\treturn itemUnderPressSelection() != _selected.end();\n}\n\nauto ListWidget::itemUnderPressSelection() -> SelectedMap::iterator {\n\treturn (_pressState.item && _pressState.inside)\n\t\t? _selected.find(_pressState.item)\n\t\t: _selected.end();\n}\n\nauto ListWidget::itemUnderPressSelection() const\n-> SelectedMap::const_iterator {\n\treturn (_pressState.item && _pressState.inside)\n\t\t? _selected.find(_pressState.item)\n\t\t: _selected.end();\n}\n\nbool ListWidget::requiredToStartDragging(\n\t\tnot_null<BaseLayout*> layout) const {\n\tif (_mouseCursorState == CursorState::Date) {\n\t\treturn true;\n\t}\n//\treturn dynamic_cast<Sticker*>(layout->getMedia());\n\treturn false;\n}\n\nbool ListWidget::isPressInSelectedText(TextState state) const {\n\tif (state.cursor != CursorState::Text) {\n\t\treturn false;\n\t}\n\tif (!hasSelectedText()\n\t\t|| !isItemUnderPressSelected()) {\n\t\treturn false;\n\t}\n\tconst auto pressedSelection = itemUnderPressSelection();\n\tconst auto from = pressedSelection->second.text.from;\n\tconst auto to = pressedSelection->second.text.to;\n\treturn (state.symbol >= from && state.symbol < to);\n}\n\nvoid ListWidget::clearSelected() {\n\tif (_selected.empty()) {\n\t\treturn;\n\t}\n\tif (hasSelectedText()) {\n\t\trepaintItem(_selected.begin()->first);\n\t\t_selected.clear();\n\t} else {\n\t\t_selected.clear();\n\t\tpushSelectedItems();\n\t\tupdate();\n\t}\n}\n\nvoid ListWidget::validateTrippleClickStartTime() {\n\tif (_trippleClickStartTime) {\n\t\tconst auto elapsed = (crl::now() - _trippleClickStartTime);\n\t\tif (elapsed >= QApplication::doubleClickInterval()) {\n\t\t\t_trippleClickStartTime = 0;\n\t\t}\n\t}\n}\n\nvoid ListWidget::enterEventHook(QEnterEvent *e) {\n\tmouseActionUpdate(QCursor::pos());\n\treturn RpWidget::enterEventHook(e);\n}\n\nvoid ListWidget::leaveEventHook(QEvent *e) {\n\tif (const auto item = _overLayout) {\n\t\tif (_overState.inside) {\n\t\t\trepaintItem(item);\n\t\t\t_overState.inside = false;\n\t\t}\n\t}\n\tClickHandler::clearActive();\n\tUi::Tooltip::Hide();\n\tif (!ClickHandler::getPressed() && _cursor != style::cur_default) {\n\t\t_cursor = style::cur_default;\n\t\tsetCursor(_cursor);\n\t}\n\treturn RpWidget::leaveEventHook(e);\n}\n\nQPoint ListWidget::clampMousePosition(QPoint position) const {\n\treturn {\n\t\tstd::clamp(position.x(), 0, qMax(0, width() - 1)),\n\t\tstd::clamp(position.y(), _visibleTop, _visibleBottom - 1)\n\t};\n}\n\nvoid ListWidget::mouseActionUpdate(const QPoint &globalPosition) {\n\tif (_sections.empty()\n\t\t|| _visibleBottom <= _visibleTop\n\t\t|| _returnAnimation.animating()) {\n\t\treturn;\n\t}\n\n\t_mousePosition = globalPosition;\n\n\tconst auto local = mapFromGlobal(_mousePosition);\n\tconst auto point = clampMousePosition(local);\n\tconst auto [foundItem, section] = findItemByPointWithSection(point);\n\tconst auto [layout, geometry, inside] = std::tie(\n\t\tfoundItem.layout,\n\t\tfoundItem.geometry,\n\t\tfoundItem.exact);\n\tconst auto state = MouseState{\n\t\tlayout->getItem(),\n\t\tgeometry.size(),\n\t\tpoint - geometry.topLeft(),\n\t\tinside\n\t};\n\tif (_overLayout != layout) {\n\t\trepaintItem(_overLayout);\n\t\t_overLayout = layout;\n\t\trepaintItem(geometry);\n\t}\n\t_overState = state;\n\n\tconst auto inDragArea = canReorder()\n\t\t&& section\n\t\t&& section->isOneColumn()\n\t\t&& point.y() >= geometry.y()\n\t\t&& point.y() < geometry.bottom()\n\t\t&& ((point.x() - geometry.x())\n\t\t\t>= (geometry.width()\n\t\t\t\t- section->oneColumnRightPadding()\n\t\t\t\t- st::stickersReorderSkip));\n\tif (_inDragArea != inDragArea) {\n\t\t_inDragArea = inDragArea;\n\t}\n\n\tauto dragState = TextState();\n\tauto lnkhost = (ClickHandlerHost*)(nullptr);\n\tauto inTextSelection = _overState.inside\n\t\t&& (_overState.item == _pressState.item)\n\t\t&& hasSelectedText();\n\tif (_overLayout) {\n\t\tconst auto cursorDeltaLength = [&] {\n\t\t\tconst auto cursorDelta = (_overState.cursor - _pressState.cursor);\n\t\t\treturn cursorDelta.manhattanLength();\n\t\t};\n\t\tconst auto dragStartLength = [] {\n\t\t\treturn QApplication::startDragDistance();\n\t\t};\n\t\tif (_overState.item != _pressState.item\n\t\t\t|| cursorDeltaLength() >= dragStartLength()) {\n\t\t\tif (_mouseAction == MouseAction::PrepareDrag) {\n\t\t\t\t_mouseAction = MouseAction::Dragging;\n\t\t\t\tInvokeQueued(this, [this] { performDrag(); });\n\t\t\t} else if (_mouseAction == MouseAction::PrepareSelect) {\n\t\t\t\t_mouseAction = MouseAction::Selecting;\n\t\t\t} else if (_mouseAction == MouseAction::PrepareReorder) {\n\t\t\t\tupdateReorder(_mousePosition);\n\t\t\t}\n\t\t}\n\t\tif (_mouseAction == MouseAction::Reordering) {\n\t\t\tupdateReorder(_mousePosition);\n\t\t}\n\t\tStateRequest request;\n\t\tif (_mouseAction == MouseAction::Selecting) {\n\t\t\trequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t} else {\n\t\t\tinTextSelection = false;\n\t\t}\n\t\tdragState = _overLayout->getState(_overState.cursor, request);\n\t\tlnkhost = _overLayout;\n\t}\n\tconst auto lnkChanged = ClickHandler::setActive(dragState.link, lnkhost);\n\tif (lnkChanged || dragState.cursor != _mouseCursorState) {\n\t\tUi::Tooltip::Hide();\n\t}\n\tif (dragState.link) {\n\t\tUi::Tooltip::Show(1000, this);\n\t}\n\n\tif (_mouseAction == MouseAction::None) {\n\t\t_mouseCursorState = dragState.cursor;\n\t\tconst auto cursor = computeMouseCursor();\n\t\tif (_cursor != cursor) {\n\t\t\tsetCursor(_cursor = cursor);\n\t\t}\n\t} else if (_mouseAction == MouseAction::Selecting) {\n\t\tif (inTextSelection) {\n\t\t\tauto second = dragState.symbol;\n\t\t\tif (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) {\n\t\t\t\t++second;\n\t\t\t}\n\t\t\tauto selState = TextSelection {\n\t\t\t\tqMin(second, _mouseTextSymbol),\n\t\t\t\tqMax(second, _mouseTextSymbol)\n\t\t\t};\n\t\t\tif (_mouseSelectType != TextSelectType::Letters) {\n\t\t\t\tselState = _overLayout->adjustSelection(selState, _mouseSelectType);\n\t\t\t}\n\t\t\tapplyItemSelection(_overState.item, selState);\n\t\t\tconst auto hasSelection = (selState == FullSelection)\n\t\t\t\t|| (selState.from != selState.to);\n\t\t\tif (!_wasSelectedText && hasSelection) {\n\t\t\t\t_wasSelectedText = true;\n\t\t\t\tsetFocus();\n\t\t\t}\n\t\t\tclearDragSelection();\n\t\t} else if (_pressState.item) {\n\t\t\tupdateDragSelection();\n\t\t}\n\t} else if (_mouseAction == MouseAction::Dragging) {\n\t}\n\n\t// #TODO scroll by drag\n\t//if (_mouseAction == MouseAction::Selecting) {\n\t//\t_widget->checkSelectingScroll(mousePos);\n\t//} else {\n\t//\tclearDragSelection();\n\t//\t_widget->noSelectingScroll();\n\t//}\n}\n\nstyle::cursor ListWidget::computeMouseCursor() const {\n\tif (_inDragArea && canReorder()) {\n\t\treturn style::cur_sizeall;\n\t} else if (ClickHandler::getPressed() || ClickHandler::getActive()) {\n\t\treturn style::cur_pointer;\n\t} else if (!hasSelectedItems()\n\t\t&& (_mouseCursorState == CursorState::Text)) {\n\t\treturn style::cur_text;\n\t}\n\treturn style::cur_default;\n}\n\nvoid ListWidget::updateDragSelection() {\n\tauto fromState = _pressState;\n\tauto tillState = _overState;\n\tconst auto swapStates = isAfter(fromState, tillState);\n\tif (swapStates) {\n\t\tstd::swap(fromState, tillState);\n\t}\n\tif (!fromState.item\n\t\t|| !tillState.item\n\t\t|| _provider->hasSelectRestriction()) {\n\t\tclearDragSelection();\n\t\treturn;\n\t}\n\t_provider->applyDragSelection(\n\t\t_dragSelected,\n\t\tfromState.item,\n\t\tSkipSelectFromItem(fromState),\n\t\ttillState.item,\n\t\tSkipSelectTillItem(tillState));\n\t_dragSelectAction = [&] {\n\t\tif (_dragSelected.empty()) {\n\t\t\treturn DragSelectAction::None;\n\t\t}\n\t\tconst auto &[firstDragItem, data] = swapStates\n\t\t\t? _dragSelected.front()\n\t\t\t: _dragSelected.back();\n\t\tif (isSelectedItem(_selected.find(firstDragItem))) {\n\t\t\treturn DragSelectAction::Deselecting;\n\t\t} else {\n\t\t\treturn DragSelectAction::Selecting;\n\t\t}\n\t}();\n\tif (!_wasSelectedText\n\t\t&& !_dragSelected.empty()\n\t\t&& _dragSelectAction == DragSelectAction::Selecting) {\n\t\t_wasSelectedText = true;\n\t\tsetFocus();\n\t}\n\tupdate();\n}\n\nvoid ListWidget::clearDragSelection() {\n\t_dragSelectAction = DragSelectAction::None;\n\tif (!_dragSelected.empty()) {\n\t\t_dragSelected.clear();\n\t\tupdate();\n\t}\n}\n\nvoid ListWidget::mouseActionStart(\n\t\tconst QPoint &globalPosition,\n\t\tQt::MouseButton button) {\n\tmouseActionUpdate(globalPosition);\n\tif (button != Qt::LeftButton) {\n\t\treturn;\n\t}\n\n\tClickHandler::pressed();\n\tif (_pressState != _overState) {\n\t\tif (_pressState.item != _overState.item) {\n\t\t\trepaintItem(_pressState.item);\n\t\t}\n\t\t_pressState = _overState;\n\t\trepaintItem(_overLayout);\n\t}\n\tconst auto pressLayout = _overLayout;\n\n\t_mouseAction = MouseAction::None;\n\t_pressWasInactive = Ui::WasInactivePress(\n\t\t_controller->parentController()->widget());\n\tif (_pressWasInactive) {\n\t\tUi::MarkInactivePress(\n\t\t\t_controller->parentController()->widget(),\n\t\t\tfalse);\n\t}\n\n\tif (_inDragArea && canReorder() && !hasSelected()) {\n\t\tstartReorder(globalPosition);\n\t\tif (_mouseAction == MouseAction::PrepareReorder) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tif (ClickHandler::getPressed() && !hasSelected()) {\n\t\t_mouseAction = MouseAction::PrepareDrag;\n\t\tif (canReorder()) {\n\t\t\tstartReorder(globalPosition);\n\t\t}\n\t} else if (hasSelectedItems()) {\n\t\tif (isItemUnderPressSelected() && ClickHandler::getPressed()) {\n\t\t\t// In shared media overview drag only by click handlers.\n\t\t\t_mouseAction = MouseAction::PrepareDrag; // start items drag\n\t\t} else if (!_pressWasInactive) {\n\t\t\t_mouseAction = MouseAction::PrepareSelect; // start items select\n\t\t}\n\t}\n\tif (_mouseAction == MouseAction::None && pressLayout) {\n\t\tvalidateTrippleClickStartTime();\n\t\tTextState dragState;\n\t\tconst auto startDistance = (globalPosition\n\t\t\t- _trippleClickPoint).manhattanLength();\n\t\tconst auto validStartPoint = startDistance\n\t\t\t< QApplication::startDragDistance();\n\t\tif (_trippleClickStartTime != 0 && validStartPoint) {\n\t\t\tStateRequest request;\n\t\t\trequest.flags = Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t\tdragState = pressLayout->getState(_pressState.cursor, request);\n\t\t\tif (dragState.cursor == CursorState::Text) {\n\t\t\t\tconst auto selStatus = TextSelection{\n\t\t\t\t\tdragState.symbol,\n\t\t\t\t\tdragState.symbol,\n\t\t\t\t};\n\t\t\t\tif (selStatus != FullSelection\n\t\t\t\t\t&& !hasSelectedItems()) {\n\t\t\t\t\tclearSelected();\n\t\t\t\t\tapplyItemSelection(\n\t\t\t\t\t\t_pressState.item,\n\t\t\t\t\t\tselStatus);\n\t\t\t\t\t_mouseTextSymbol = dragState.symbol;\n\t\t\t\t\t_mouseAction = MouseAction::Selecting;\n\t\t\t\t\t_mouseSelectType = TextSelectType::Paragraphs;\n\t\t\t\t\tmouseActionUpdate(_mousePosition);\n\t\t\t\t\t_trippleClickStartTime = crl::now();\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tStateRequest request;\n\t\t\trequest.flags = Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t\tdragState = pressLayout->getState(\n\t\t\t\t_pressState.cursor,\n\t\t\t\trequest);\n\t\t}\n\t\tif (_mouseSelectType != TextSelectType::Paragraphs) {\n\t\t\tif (_pressState.inside) {\n\t\t\t\t_mouseTextSymbol = dragState.symbol;\n\t\t\t\tif (isPressInSelectedText(dragState)) {\n\t\t\t\t\t_mouseAction = MouseAction::PrepareDrag; // start text drag\n\t\t\t\t} else if (!_pressWasInactive) {\n\t\t\t\t\tif (requiredToStartDragging(pressLayout)) {\n\t\t\t\t\t\t_mouseAction = MouseAction::PrepareDrag;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (dragState.afterSymbol) {\n\t\t\t\t\t\t\t++_mouseTextSymbol;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst auto selStatus = TextSelection{\n\t\t\t\t\t\t\t_mouseTextSymbol,\n\t\t\t\t\t\t\t_mouseTextSymbol,\n\t\t\t\t\t\t};\n\t\t\t\t\t\tif (selStatus != FullSelection\n\t\t\t\t\t\t\t&& !hasSelectedItems()) {\n\t\t\t\t\t\t\tclearSelected();\n\t\t\t\t\t\t\tapplyItemSelection(\n\t\t\t\t\t\t\t\t_pressState.item,\n\t\t\t\t\t\t\t\tselStatus);\n\t\t\t\t\t\t\t_mouseAction = MouseAction::Selecting;\n\t\t\t\t\t\t\trepaintItem(pressLayout);\n\t\t\t\t\t\t} else if (!_provider->hasSelectRestriction()) {\n\t\t\t\t\t\t\t_mouseAction = MouseAction::PrepareSelect;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (!_pressWasInactive\n\t\t\t\t&& !_provider->hasSelectRestriction()) {\n\t\t\t\t_mouseAction = MouseAction::PrepareSelect; // start items select\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!pressLayout) {\n\t\t_mouseAction = MouseAction::None;\n\t} else if (_mouseAction == MouseAction::None) {\n\t\tmouseActionCancel();\n\t}\n}\n\nvoid ListWidget::mouseActionCancel() {\n\t_pressState = MouseState();\n\t_mouseAction = MouseAction::None;\n\tclearDragSelection();\n\t_wasSelectedText = false;\n\tcancelReorder();\n//\t_widget->noSelectingScroll(); // #TODO scroll by drag\n}\n\nvoid ListWidget::performDrag() {\n\tif (_mouseAction != MouseAction::Dragging) return;\n\n\tauto uponSelected = false;\n\tif (_pressState.item && _pressState.inside) {\n\t\tif (hasSelectedItems()) {\n\t\t\tuponSelected = isItemUnderPressSelected();\n\t\t} else if (const auto pressLayout = _provider->lookupLayout(\n\t\t\t\t_pressState.item)) {\n\t\t\tStateRequest request;\n\t\t\trequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;\n\t\t\tconst auto dragState = pressLayout->getState(\n\t\t\t\t_pressState.cursor,\n\t\t\t\trequest);\n\t\t\tuponSelected = isPressInSelectedText(dragState);\n\t\t}\n\t}\n\tauto pressedHandler = ClickHandler::getPressed();\n\n\tif (dynamic_cast<VoiceSeekClickHandler*>(pressedHandler.get())) {\n\t\treturn;\n\t}\n\n\tTextWithEntities sel;\n\t//QList<QUrl> urls;\n\tif (uponSelected) {\n//\t\tsel = getSelectedText();\n\t} else if (pressedHandler) {\n\t\tsel = { pressedHandler->dragText(), EntitiesInText() };\n\t\t//if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') {\n\t\t//\turls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o\n\t\t//}\n\t}\n\t//if (auto mimeData = MimeDataFromText(sel)) {\n\t//\tclearDragSelection();\n\t//\t_widget->noSelectingScroll();\n\n\t//\tif (!urls.isEmpty()) mimeData->setUrls(urls);\n\t//\tif (uponSelected && !Adaptive::OneColumn()) {\n\t//\t\tauto selectedState = getSelectionState();\n\t//\t\tif (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {\n\t//\t\t\tsession().data().setMimeForwardIds(collectSelectedIds());\n\t//\t\t\tmimeData->setData(u\"application/x-td-forward\"_q, \"1\");\n\t//\t\t}\n\t//\t}\n\t//\t_controller->parentController()->window()->launchDrag(std::move(mimeData));\n\t//\treturn;\n\t//} else {\n\t//\tauto forwardMimeType = QString();\n\t//\tauto pressedMedia = static_cast<HistoryView::Media*>(nullptr);\n\t//\tif (auto pressedItem = _pressState.layout) {\n\t//\t\tpressedMedia = pressedItem->getMedia();\n\t//\t\tif (_mouseCursorState == CursorState::Date) {\n\t//\t\t\tsession().data().setMimeForwardIds(session().data().itemOrItsGroup(pressedItem));\n\t//\t\t\tforwardMimeType = u\"application/x-td-forward\"_q;\n\t//\t\t}\n\t//\t}\n\t//\tif (auto pressedLnkItem = App::pressedLinkItem()) {\n\t//\t\tif ((pressedMedia = pressedLnkItem->getMedia())) {\n\t//\t\t\tif (forwardMimeType.isEmpty() && pressedMedia->dragItemByHandler(pressedHandler)) {\n\t//\t\t\t\tsession().data().setMimeForwardIds({ 1, pressedLnkItem->fullId() });\n\t//\t\t\t\tforwardMimeType = u\"application/x-td-forward\"_q;\n\t//\t\t\t}\n\t//\t\t}\n\t//\t}\n\t//\tif (!forwardMimeType.isEmpty()) {\n\t//\t\tauto mimeData = std::make_unique<QMimeData>();\n\t//\t\tmimeData->setData(forwardMimeType, \"1\");\n\t//\t\tif (auto document = (pressedMedia ? pressedMedia->getDocument() : nullptr)) {\n\t//\t\t\tauto filepath = document->filepath(true);\n\t//\t\t\tif (!filepath.isEmpty()) {\n\t//\t\t\t\tQList<QUrl> urls;\n\t//\t\t\t\turls.push_back(QUrl::fromLocalFile(filepath));\n\t//\t\t\t\tmimeData->setUrls(urls);\n\t//\t\t\t}\n\t//\t\t}\n\n\t//\t\t// This call enters event loop and can destroy any QObject.\n\t//\t\t_controller->parentController()->window()->launchDrag(std::move(mimeData));\n\t//\t\treturn;\n\t//\t}\n\t//}\n}\n\nvoid ListWidget::mouseActionFinish(\n\t\tconst QPoint &globalPosition,\n\t\tQt::MouseButton button) {\n\tmouseActionUpdate(globalPosition);\n\n\tconst auto pressState = base::take(_pressState);\n\trepaintItem(pressState.item);\n\n\tconst auto selectionMode = hasSelectedItems() || _storiesAddToAlbumId;\n\tconst auto simpleSelectionChange = pressState.item\n\t\t&& pressState.inside\n\t\t&& !_pressWasInactive\n\t\t&& (button != Qt::RightButton)\n\t\t&& (_mouseAction == MouseAction::PrepareDrag\n\t\t\t|| _mouseAction == MouseAction::PrepareSelect);\n\tif (_mouseAction == MouseAction::Reordering\n\t\t|| _mouseAction == MouseAction::PrepareReorder) {\n\t\tfinishReorder();\n\t\treturn;\n\t}\n\tconst auto needSelectionToggle = simpleSelectionChange && selectionMode;\n\tconst auto needSelectionClear = simpleSelectionChange\n\t\t&& hasSelectedText();\n\n\tauto activated = ClickHandler::unpressed();\n\tif (_mouseAction == MouseAction::Dragging\n\t\t|| _mouseAction == MouseAction::Selecting) {\n\t\tactivated = nullptr;\n\t} else if (needSelectionToggle || _storiesAddToAlbumId) {\n\t\tactivated = nullptr;\n\t}\n\n\t_wasSelectedText = false;\n\tif (activated) {\n\t\tmouseActionCancel();\n\t\tconst auto found = findItemByItem(pressState.item);\n\t\tconst auto fullId = found\n\t\t\t? found->layout->getItem()->fullId()\n\t\t\t: FullMsgId();\n\t\tActivateClickHandler(window(), activated, {\n\t\t\tbutton,\n\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.itemId = fullId,\n\t\t\t\t.sessionWindow = base::make_weak(\n\t\t\t\t\t_controller->parentController()),\n\t\t\t})\n\t\t});\n\t\treturn;\n\t}\n\n\tif (needSelectionToggle) {\n\t\ttoggleItemSelection(pressState.item);\n\t} else if (needSelectionClear) {\n\t\tclearSelected();\n\t} else if (_mouseAction == MouseAction::Selecting) {\n\t\tif (!_dragSelected.empty()) {\n\t\t\tapplyDragSelection();\n\t\t} else if (!_selected.empty() && !_pressWasInactive) {\n\t\t\tconst auto selection = _selected.cbegin()->second;\n\t\t\tif (selection.text != FullSelection\n\t\t\t\t&& selection.text.from == selection.text.to) {\n\t\t\t\tclearSelected();\n\t\t\t\t//_controller->parentController()->window()->setInnerFocus(); // #TODO focus\n\t\t\t}\n\t\t}\n\t}\n\t_mouseAction = MouseAction::None;\n\t_mouseSelectType = TextSelectType::Letters;\n\t//_widget->noSelectingScroll(); // #TODO scroll by drag\n\t//_widget->updateTopBarSelection();\n\n\t//if (QGuiApplication::clipboard()->supportsSelection() && hasSelectedText()) { // #TODO linux clipboard\n\t//\tTextUtilities::SetClipboardText(_selected.cbegin()->first->selectedText(_selected.cbegin()->second), QClipboard::Selection);\n\t//}\n}\n\nvoid ListWidget::applyDragSelection() {\n\tif (!_provider->hasSelectRestriction()) {\n\t\tapplyDragSelection(_selected);\n\t}\n\tclearDragSelection();\n\tpushSelectedItems();\n}\n\nvoid ListWidget::applyDragSelection(SelectedMap &applyTo) const {\n\tif (_dragSelectAction == DragSelectAction::Selecting) {\n\t\tfor (auto &[item, data] : _dragSelected) {\n\t\t\tChangeItemSelection(\n\t\t\t\tapplyTo,\n\t\t\t\titem,\n\t\t\t\t_provider->computeSelectionData(item, FullSelection),\n\t\t\t\t_selectedLimit);\n\t\t}\n\t} else if (_dragSelectAction == DragSelectAction::Deselecting) {\n\t\tfor (auto &[item, data] : _dragSelected) {\n\t\t\tapplyTo.remove(item);\n\t\t}\n\t}\n}\n\nvoid ListWidget::refreshHeight() {\n\tresize(width(), recountHeight());\n\tupdate();\n}\n\nint ListWidget::recountHeight() {\n\tif (_sections.empty()) {\n\t\tif (const auto count = _provider->fullCount()) {\n\t\t\tif (*count == 0) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto cachedPadding = padding();\n\tauto result = cachedPadding.top();\n\tfor (auto &section : _sections) {\n\t\tsection.setTop(result);\n\t\tresult += section.height();\n\t}\n\treturn result + cachedPadding.bottom();\n}\n\nvoid ListWidget::mouseActionUpdate() {\n\tmouseActionUpdate(_mousePosition);\n}\n\nstd::vector<ListSection>::iterator ListWidget::findSectionByItem(\n\t\tnot_null<const HistoryItem*> item) {\n\tif (_sections.size() < 2) {\n\t\treturn _sections.begin();\n\t}\n\tAssert(!_controller->isDownloads() && !_controller->isGlobalMedia());\n\treturn ranges::lower_bound(\n\t\t_sections,\n\t\tGetUniversalId(item),\n\t\tstd::greater<>(),\n\t\t[](const Section &section) { return section.minId(); });\n}\n\nauto ListWidget::findSectionAfterTop(\n\t\tint top) -> std::vector<Section>::iterator {\n\treturn ranges::lower_bound(\n\t\t_sections,\n\t\ttop,\n\t\tstd::less_equal<>(),\n\t\t[](const Section &section) { return section.bottom(); });\n}\n\nauto ListWidget::findSectionAfterTop(\n\t\tint top) const -> std::vector<Section>::const_iterator {\n\treturn ranges::lower_bound(\n\t\t_sections,\n\t\ttop,\n\t\tstd::less_equal<>(),\n\t\t[](const Section &section) { return section.bottom(); });\n}\n\nauto ListWidget::findSectionAfterBottom(\n\t\tstd::vector<Section>::const_iterator from,\n\t\tint bottom) const -> std::vector<Section>::const_iterator {\n\treturn ranges::lower_bound(\n\t\tfrom,\n\t\t_sections.end(),\n\t\tbottom,\n\t\tstd::less<>(),\n\t\t[](const Section &section) { return section.top(); });\n}\n\nvoid ListWidget::startReorder(const QPoint &globalPos) {\n\tif (!canReorder() || _mouseAction == MouseAction::Selecting) {\n\t\treturn;\n\t}\n\tconst auto mapped = mapFromGlobal(globalPos);\n\tconst auto foundWithSection = findItemByPointWithSection(mapped);\n\tif (!foundWithSection.section) {\n\t\treturn;\n\t}\n\tif (foundWithSection.section->isOneColumn()\n\t\t? !_inDragArea\n\t\t: !foundWithSection.item.exact) {\n\t\treturn;\n\t}\n\tconst auto index = itemIndexFromPoint(mapped\n\t\t- QPoint(foundWithSection.section->oneColumnRightPadding(), 0));\n\tif (index < 0) {\n\t\treturn;\n\t}\n\n\tif (_reorderDescriptor.filter) {\n\t\tconst auto item = foundWithSection.item.layout->getItem();\n\t\tif (!_reorderDescriptor.filter(item)) {\n\t\t\treturn;\n\t\t}\n\t}\n\t_reorderState.enabled = true;\n\t_reorderState.index = index;\n\t_reorderState.targetIndex = index;\n\t_reorderState.startPos = globalPos;\n\t_reorderState.dragPoint = mapped\n\t\t- foundWithSection.item.geometry.topLeft();\n\t_reorderState.item = foundWithSection.item.layout;\n\t_reorderState.section = foundWithSection.section;\n\t_mouseAction = MouseAction::PrepareReorder;\n}\n\nvoid ListWidget::updateReorder(const QPoint &globalPos) {\n\tif (!_reorderState.enabled || _returnAnimation.animating()) {\n\t\treturn;\n\t}\n\tconst auto distance\n\t\t= (globalPos - _reorderState.startPos).manhattanLength();\n\tif (_mouseAction == MouseAction::PrepareReorder\n\t\t&& distance > QApplication::startDragDistance()) {\n\t\t_mouseAction = MouseAction::Reordering;\n\t}\n\tif (_mouseAction == MouseAction::Reordering) {\n\t\tconst auto mapped = mapFromGlobal(globalPos);\n\t\tauto localPos = mapped - _reorderState.dragPoint;\n\n\t\tauto currentIndex = 0;\n\t\tfor (const auto &section : _sections) {\n\t\t\tconst auto sectionSize = int(section.items().size());\n\t\t\tif (_reorderState.index >= currentIndex\n\t\t\t\t&& _reorderState.index < currentIndex + sectionSize) {\n\t\t\t\tif (section.isOneColumn()) {\n\t\t\t\t\tlocalPos.setX(_reorderState.item\n\t\t\t\t\t\t? itemGeometryByIndex(_reorderState.index).x()\n\t\t\t\t\t\t: localPos.x());\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcurrentIndex += sectionSize;\n\t\t}\n\n\t\t_reorderState.currentPos = localPos;\n\t\tconst auto newIndex = itemIndexFromPoint(mapped);\n\t\tif (newIndex >= 0 && newIndex != _reorderState.targetIndex) {\n\t\t\t_reorderState.targetIndex = newIndex;\n\t\t\tupdateShiftAnimations();\n\t\t}\n\t\tupdate();\n\t}\n}\n\nvoid ListWidget::finishReorder() {\n\tif (_returnAnimation.animating()) {\n\t\treturn;\n\t}\n\tif (!_reorderState.enabled || _mouseAction != MouseAction::Reordering) {\n\t\tcancelReorder();\n\t\treturn;\n\t}\n\tfinishShiftAnimations();\n\tif (_reorderState.index != _reorderState.targetIndex) {\n\t\treorderItemsInSections(\n\t\t\t_reorderState.index,\n\t\t\t_reorderState.targetIndex);\n\t\tif (_reorderDescriptor.save) {\n\t\t\t_reorderDescriptor.save(\n\t\t\t\t_reorderState.index,\n\t\t\t\t_reorderState.targetIndex,\n\t\t\t\t[=] { /* done */ },\n\t\t\t\t[=] { /* fail */ }\n\t\t\t);\n\t\t}\n\t}\n\n\tconst auto targetIndex = _reorderState.targetIndex;\n\tconst auto draggedItem = _reorderState.item;\n\tif (draggedItem) {\n\t\tconst auto targetGeometry = itemGeometryByIndex(targetIndex);\n\t\tif (!targetGeometry.isEmpty()) {\n\t\t\tconst auto startPos = _reorderState.currentPos;\n\t\t\tconst auto endPos = targetGeometry.topLeft()\n\t\t\t\t+ rect::m::pos::tl(padding());\n\t\t\tconst auto callback = [=](float64 progress) {\n\t\t\t\tconst auto currentPos = QPoint(\n\t\t\t\t\tstartPos.x() + (endPos.x() - startPos.x()) * progress,\n\t\t\t\t\tstartPos.y() + (endPos.y() - startPos.y()) * progress);\n\t\t\t\t_reorderState.currentPos = currentPos;\n\t\t\t\tupdate();\n\t\t\t\tif (progress == 1.) {\n\t\t\t\t\tcancelReorder();\n\t\t\t\t}\n\t\t\t};\n\t\t\t_returnAnimation.start(\n\t\t\t\tstd::move(callback),\n\t\t\t\t0.,\n\t\t\t\t1.,\n\t\t\t\tst::slideWrapDuration);\n\t\t\treturn;\n\t\t}\n\t}\n\tcancelReorder();\n}\n\nvoid ListWidget::cancelReorder() {\n\t_reorderState = {};\n\tfinishShiftAnimations();\n\t_mouseAction = MouseAction::None;\n\tupdate();\n}\n\nvoid ListWidget::updateShiftAnimations() {\n\tif (_reorderState.index < 0 || _reorderState.targetIndex < 0) {\n\t\treturn;\n\t}\n\tconst auto fromIndex = _reorderState.index;\n\tconst auto toIndex = _reorderState.targetIndex;\n\tauto itemIndex = 0;\n\tfor (const auto &section : _sections) {\n\t\tfor (const auto &item : section.items()) {\n\t\t\tif (itemIndex == fromIndex) {\n\t\t\t\t++itemIndex;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tauto targetShift = 0;\n\t\t\tif (fromIndex < toIndex\n\t\t\t\t&& itemIndex > fromIndex\n\t\t\t\t&& itemIndex <= toIndex) {\n\t\t\t\ttargetShift = -1;\n\t\t\t} else if (fromIndex > toIndex\n\t\t\t\t&& itemIndex >= toIndex\n\t\t\t\t&& itemIndex < fromIndex) {\n\t\t\t\ttargetShift = 1;\n\t\t\t}\n\t\t\tauto &animation = _shiftAnimations[itemIndex];\n\t\t\tif (animation.targetShift != targetShift) {\n\t\t\t\tanimation.targetShift = targetShift;\n\t\t\t\tconst auto fromGeometry = itemGeometryByIndex(itemIndex);\n\t\t\t\tconst auto toGeometry = itemGeometryByIndex(itemIndex\n\t\t\t\t\t+ targetShift);\n\t\t\t\tif (!fromGeometry.isEmpty() && !toGeometry.isEmpty()) {\n\t\t\t\t\tconst auto deltaX = toGeometry.x() - fromGeometry.x();\n\t\t\t\t\tconst auto deltaY = toGeometry.y() - fromGeometry.y();\n\t\t\t\t\tanimation.xAnimation.start(\n\t\t\t\t\t\t[=](float64 progress) {\n\t\t\t\t\t\t\titem->setShiftX(progress);\n\t\t\t\t\t\t\tupdate();\n\t\t\t\t\t\t},\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tdeltaX,\n\t\t\t\t\t\tst::slideWrapDuration);\n\t\t\t\t\tanimation.yAnimation.start(\n\t\t\t\t\t\t[=](float64 progress) {\n\t\t\t\t\t\t\titem->setShiftY(progress);\n\t\t\t\t\t\t\tupdate();\n\t\t\t\t\t\t},\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tdeltaY,\n\t\t\t\t\t\tst::slideWrapDuration);\n\t\t\t\t}\n\t\t\t\tanimation.shift = targetShift;\n\t\t\t}\n\t\t\t++itemIndex;\n\t\t}\n\t}\n}\n\nint ListWidget::itemIndexFromPoint(QPoint point) const {\n\tif (_sections.empty()) {\n\t\treturn -1;\n\t}\n\tconst auto found = findItemByPoint(point);\n\tif (!found.exact) {\n\t\treturn -1;\n\t}\n\tauto index = 0;\n\tfor (const auto &section : _sections) {\n\t\tfor (const auto &item : section.items()) {\n\t\t\tif (item == found.layout) {\n\t\t\t\treturn index;\n\t\t\t}\n\t\t\t++index;\n\t\t}\n\t}\n\treturn -1;\n}\n\nQRect ListWidget::itemGeometryByIndex(int index) {\n\tif (index < 0) {\n\t\treturn QRect();\n\t}\n\tauto currentIndex = 0;\n\tfor (const auto &section : _sections) {\n\t\tfor (const auto &item : section.items()) {\n\t\t\tif (currentIndex == index) {\n\t\t\t\treturn section.findItemDetails(item).geometry;\n\t\t\t}\n\t\t\t++currentIndex;\n\t\t}\n\t}\n\treturn QRect();\n}\n\nBaseLayout* ListWidget::itemByIndex(int index) {\n\tif (index < 0) {\n\t\treturn nullptr;\n\t}\n\tauto currentIndex = 0;\n\tfor (const auto &section : _sections) {\n\t\tfor (const auto &item : section.items()) {\n\t\t\tif (currentIndex == index) {\n\t\t\t\treturn item;\n\t\t\t}\n\t\t\t++currentIndex;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nbool ListWidget::canReorder() const {\n\treturn !!_reorderDescriptor.save;\n}\n\nvoid ListWidget::reorderItemsInSections(int oldIndex, int newIndex) {\n\tif (oldIndex == newIndex || _sections.empty()) {\n\t\treturn;\n\t}\n\n\tauto currentIndex = 0;\n\tauto oldSection = (Section*)(nullptr);\n\tauto newSection = (Section*)(nullptr);\n\tauto oldSectionIndex = -1;\n\tauto newSectionIndex = -1;\n\n\tfor (auto &section : _sections) {\n\t\tconst auto sectionSize = int(section.items().size());\n\t\tif (oldIndex >= currentIndex\n\t\t\t&& oldIndex < currentIndex + sectionSize) {\n\t\t\toldSection = &section;\n\t\t\toldSectionIndex = oldIndex - currentIndex;\n\t\t}\n\t\tif (newIndex >= currentIndex\n\t\t\t&& newIndex < currentIndex + sectionSize) {\n\t\t\tnewSection = &section;\n\t\t\tnewSectionIndex = newIndex - currentIndex;\n\t\t}\n\t\tcurrentIndex += sectionSize;\n\t}\n\n\tif (!oldSection || !newSection) {\n\t\treturn;\n\t}\n\n\tif (oldSection == newSection) {\n\t\toldSection->reorderItems(oldSectionIndex, newSectionIndex);\n\t\trefreshHeight();\n\t}\n}\n\nvoid ListWidget::resetAllItemShifts() {\n\tfor (auto &section : _sections) {\n\t\tfor (const auto &item : section.items()) {\n\t\t\titem->setShiftX(0);\n\t\t\titem->setShiftY(0);\n\t\t}\n\t}\n}\n\nvoid ListWidget::finishShiftAnimations() {\n\tfor (auto &[index, animation] : _shiftAnimations) {\n\t\tconst auto item = itemByIndex(index);\n\t\tif (!item) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto animating = animation.xAnimation.animating()\n\t\t\t|| animation.yAnimation.animating();\n\t\tconst auto geometry = itemGeometryByIndex(index);\n\t\tanimation.xAnimation.stop();\n\t\tanimation.yAnimation.stop();\n\t\tif (animating) {\n\t\t\t++_activeShiftAnimations;\n\t\t\tanimation.xAnimation.start(\n\t\t\t\t[=](float64 progress) {\n\t\t\t\t\tif (item) item->setShiftX(progress);\n\t\t\t\t\tupdate();\n\t\t\t\t\tif (progress == 1.) {\n\t\t\t\t\t\t--_activeShiftAnimations;\n\t\t\t\t\t\tif (_activeShiftAnimations == 0) {\n\t\t\t\t\t\t\t_shiftAnimations.clear();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tanimation.xAnimation.value(0),\n\t\t\t\t0,\n\t\t\t\tst::slideWrapDuration);\n\t\t\t++_activeShiftAnimations;\n\t\t\tanimation.yAnimation.start(\n\t\t\t\t[=](float64 progress) {\n\t\t\t\t\tif (item) item->setShiftY(progress);\n\t\t\t\t\tupdate();\n\t\t\t\t\tif (progress == 1.) {\n\t\t\t\t\t\t--_activeShiftAnimations;\n\t\t\t\t\t\tif (_activeShiftAnimations == 0) {\n\t\t\t\t\t\t\t_shiftAnimations.clear();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t(animation.targetShift < 0)\n\t\t\t\t\t? (item->shift().y() + geometry.height())\n\t\t\t\t\t: (item->shift().y() - geometry.height()),\n\t\t\t\t0,\n\t\t\t\tst::slideWrapDuration);\n\t\t}\n\t}\n\tif (_activeShiftAnimations == 0) {\n\t\t_shiftAnimations.clear();\n\t}\n\tresetAllItemShifts();\n}\n\nListWidget::~ListWidget() {\n\tif (_contextMenu) {\n\t\t// We don't want it to be called after ListWidget is destroyed.\n\t\t_contextMenu->setDestroyedCallback(nullptr);\n\t}\n}\n\nvoid ListWidget::jumpToMessage(MsgId msgId) {\n\t_provider->jumpToMessage(msgId, [=](FullMsgId fullId) {\n\t\tif (const auto i = session().data().message(fullId)) {\n\t\t\t_scrollTopState.position = _provider->scrollTopStatePosition(i);\n\t\t\t_scrollTopState.item = i;\n\t\t}\n\t});\n}\n\n} // namespace Media\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_list_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"info/media/info_media_widget.h\"\n#include \"info/media/info_media_common.h\"\n#include \"overview/overview_layout_delegate.h\"\n\nclass DeleteMessagesBox;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace HistoryView {\nstruct TextState;\nstruct StateRequest;\nenum class CursorState : char;\nenum class PointState : char;\n} // namespace HistoryView\n\nnamespace Ui {\nclass PopupMenu;\nclass BoxContent;\n} // namespace Ui\n\nnamespace Overview {\nnamespace Layout {\nclass ItemBase;\n} // namespace Layout\n} // namespace Overview\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Info {\n\nclass AbstractController;\n\nnamespace Media {\n\nstruct ListFoundItem;\nstruct ListFoundItemWithSection;\nstruct ListContext;\nclass ListSection;\nclass ListProvider;\n\nclass ListWidget final\n\t: public Ui::RpWidget\n\t, public Overview::Layout::Delegate\n\t, public Ui::AbstractTooltipShower {\npublic:\n\tListWidget(\n\t\tQWidget *parent,\n\t\tnot_null<AbstractController*> controller);\n\t~ListWidget();\n\n\tMain::Session &session() const;\n\n\tvoid restart();\n\n\trpl::producer<int> scrollToRequests() const;\n\trpl::producer<SelectedItems> selectedListValue() const;\n\tvoid selectionAction(SelectionAction action);\n\n\tstruct ReorderDescriptor {\n\t\tFn<void(int old, int pos, Fn<void()> done, Fn<void()> fail)> save;\n\t\tFn<bool(HistoryItem*)> filter;\n\t};\n\n\tvoid setReorderDescriptor(ReorderDescriptor descriptor);\n\n\tQRect getCurrentSongGeometry();\n\trpl::producer<> checkForHide() const {\n\t\treturn _checkForHide.events();\n\t}\n\tbool preventAutoHide() const;\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tvoid jumpToMessage(MsgId msgId);\n\n\t// Overview::Layout::Delegate\n\tvoid registerHeavyItem(not_null<const BaseLayout*> item) override;\n\tvoid unregisterHeavyItem(not_null<const BaseLayout*> item) override;\n\tvoid repaintItem(not_null<const BaseLayout*> item) override;\n\tbool itemVisible(not_null<const BaseLayout*> item) override;\n\tnot_null<StickerPremiumMark*> hiddenMark() override;\n\n\t// AbstractTooltipShower interface\n\tQString tooltipText() const override;\n\tQPoint tooltipPos() const override;\n\tbool tooltipWindowActive() const override;\n\n\tvoid openPhoto(not_null<PhotoData*> photo, FullMsgId id) override;\n\tvoid openDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId id,\n\t\tbool showInMediaView = false) override;\n\nprivate:\n\tstruct DateBadge;\n\tusing Section = ListSection;\n\tusing FoundItem = ListFoundItem;\n\tusing CursorState = HistoryView::CursorState;\n\tusing TextState = HistoryView::TextState;\n\tusing StateRequest = HistoryView::StateRequest;\n\tusing SelectionData = ListItemSelectionData;\n\tusing SelectedMap = ListSelectedMap;\n\tusing DragSelectAction = ListDragSelectAction;\n\tenum class MouseAction {\n\t\tNone,\n\t\tPrepareDrag,\n\t\tDragging,\n\t\tPrepareSelect,\n\t\tSelecting,\n\t\tPrepareReorder,\n\t\tReordering,\n\t};\n\tstruct ReorderState {\n\t\tbool enabled = false;\n\t\tint index = -1;\n\t\tint targetIndex = -1;\n\t\tQPoint startPos;\n\t\tQPoint dragPoint;\n\t\tQPoint currentPos;\n\t\tBaseLayout *item = nullptr;\n\t\tconst Section *section = nullptr;\n\t};\n\tstruct ShiftAnimation {\n\t\tUi::Animations::Simple xAnimation;\n\t\tUi::Animations::Simple yAnimation;\n\t\tint shift = 0;\n\t\tint targetShift = 0;\n\t};\n\tstruct MouseState {\n\t\tHistoryItem *item = nullptr;\n\t\tQSize size;\n\t\tQPoint cursor;\n\t\tbool inside = false;\n\n\t\tinline bool operator==(const MouseState &other) const {\n\t\t\treturn (item == other.item)\n\t\t\t\t&& (cursor == other.cursor);\n\t\t}\n\t\tinline bool operator!=(const MouseState &other) const {\n\t\t\treturn !(*this == other);\n\t\t}\n\n\t};\n\tenum class ContextMenuSource {\n\t\tMouse,\n\t\tTouch,\n\t\tOther,\n\t};\n\n\tint resizeGetHeight(int newWidth) override;\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid mouseDoubleClickEvent(QMouseEvent *e) override;\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\n\tvoid start();\n\tint recountHeight();\n\tvoid refreshHeight();\n\tvoid subscribeToSession(\n\t\tnot_null<Main::Session*> session,\n\t\trpl::lifetime &lifetime);\n\n\tvoid setupSelectRestriction();\n\t[[nodiscard]] bool showDrawButton() const;\n\n\t[[nodiscard]] MsgId topicRootId() const;\n\t[[nodiscard]] PeerId monoforumPeerId() const;\n\n\tQMargins padding() const;\n\tbool isItemLayout(\n\t\tnot_null<const HistoryItem*> item,\n\t\tBaseLayout *layout) const;\n\tvoid repaintItem(const HistoryItem *item);\n\tvoid repaintItem(const BaseLayout *item);\n\tvoid repaintItem(QRect itemGeometry);\n\tvoid itemRemoved(not_null<const HistoryItem*> item);\n\tvoid itemLayoutChanged(not_null<const HistoryItem*> item);\n\n\tvoid refreshRows();\n\tvoid markStoryMsgsSelected();\n\tvoid trackSession(not_null<Main::Session*> session);\n\n\t[[nodiscard]] SelectedItems collectSelectedItems() const;\n\t[[nodiscard]] MessageIdsList collectSelectedIds() const;\n\t[[nodiscard]] MessageIdsList collectSelectedIds(\n\t\tconst SelectedItems &items) const;\n\tvoid pushSelectedItems();\n\t[[nodiscard]] bool hasSelected() const;\n\t[[nodiscard]] bool isSelectedItem(\n\t\tconst SelectedMap::const_iterator &i) const;\n\tvoid removeItemSelection(\n\t\tconst SelectedMap::const_iterator &i);\n\t[[nodiscard]] bool hasSelectedText() const;\n\t[[nodiscard]] bool hasSelectedItems() const;\n\tvoid clearSelected();\n\tvoid forwardSelected();\n\tvoid forwardItem(GlobalMsgId globalId);\n\tvoid forwardItems(MessageIdsList &&items);\n\tvoid deleteSelected();\n\tvoid toggleStoryPinSelected();\n\tvoid toggleStoryInProfileSelected(bool toProfile);\n\tvoid deleteItem(GlobalMsgId globalId);\n\tvoid deleteItems(SelectedItems &&items, Fn<void()> confirmed = nullptr);\n\tvoid toggleStoryInProfile(\n\t\tMessageIdsList &&items,\n\t\tbool toProfile,\n\t\tFn<void()> confirmed = nullptr);\n\tvoid toggleStoryPin(\n\t\tMessageIdsList &&items,\n\t\tbool pin,\n\t\tFn<void()> confirmed = nullptr);\n\tvoid applyItemSelection(\n\t\tHistoryItem *item,\n\t\tTextSelection selection);\n\tvoid toggleItemSelection(not_null<HistoryItem*> item);\n\t[[nodiscard]] SelectedMap::iterator itemUnderPressSelection();\n\t[[nodiscard]] auto itemUnderPressSelection() const\n\t\t-> SelectedMap::const_iterator;\n\tbool isItemUnderPressSelected() const;\n\t[[nodiscard]] bool requiredToStartDragging(\n\t\tnot_null<BaseLayout*> layout) const;\n\t[[nodiscard]] bool isPressInSelectedText(TextState state) const;\n\tvoid applyDragSelection();\n\tvoid applyDragSelection(SelectedMap &applyTo) const;\n\n\t[[nodiscard]] bool isAfter(\n\t\tconst MouseState &a,\n\t\tconst MouseState &b) const;\n\t[[nodiscard]] static bool SkipSelectFromItem(const MouseState &state);\n\t[[nodiscard]] static bool SkipSelectTillItem(const MouseState &state);\n\n\t[[nodiscard]] std::vector<Section>::iterator findSectionByItem(\n\t\tnot_null<const HistoryItem*> item);\n\t[[nodiscard]] std::vector<Section>::iterator findSectionAfterTop(\n\t\tint top);\n\t[[nodiscard]] std::vector<Section>::const_iterator findSectionAfterTop(\n\t\tint top) const;\n\t[[nodiscard]] auto findSectionAfterBottom(\n\t\tstd::vector<Section>::const_iterator from,\n\t\tint bottom) const -> std::vector<Section>::const_iterator;\n\t[[nodiscard]] auto findSectionAndItem(QPoint point) const\n\t\t-> std::pair<std::vector<Section>::const_iterator, FoundItem>;\n\t[[nodiscard]] FoundItem findItemByPoint(QPoint point) const;\n\t[[nodiscard]] ListFoundItemWithSection findItemByPointWithSection(QPoint point) const;\n\t[[nodiscard]] std::optional<FoundItem> findItemByItem(\n\t\tconst HistoryItem *item);\n\t[[nodiscard]] FoundItem findItemDetails(not_null<BaseLayout*> item);\n\t[[nodiscard]] FoundItem foundItemInSection(\n\t\tconst FoundItem &item,\n\t\tconst Section &section) const;\n\n\t[[nodiscard]] ListScrollTopState countScrollState() const;\n\tvoid saveScrollState();\n\tvoid restoreScrollState();\n\n\t[[nodiscard]] QPoint clampMousePosition(QPoint position) const;\n\tvoid mouseActionStart(\n\t\tconst QPoint &globalPosition,\n\t\tQt::MouseButton button);\n\tvoid mouseActionUpdate(const QPoint &globalPosition);\n\tvoid mouseActionUpdate();\n\tvoid mouseActionFinish(\n\t\tconst QPoint &globalPosition,\n\t\tQt::MouseButton button);\n\tvoid mouseActionCancel();\n\tvoid performDrag();\n\t[[nodiscard]] style::cursor computeMouseCursor() const;\n\tvoid showContextMenu(\n\t\tQContextMenuEvent *e,\n\t\tContextMenuSource source);\n\n\tvoid updateDragSelection();\n\tvoid clearDragSelection();\n\n\tvoid updateDateBadgeFor(int top);\n\tvoid scrollDateCheck();\n\tvoid scrollDateHide();\n\tvoid toggleScrollDateShown();\n\n\tvoid trySwitchToWordSelection();\n\tvoid switchToWordSelection();\n\tvoid validateTrippleClickStartTime();\n\tvoid checkMoveToOtherViewer();\n\tvoid clearHeavyItems();\n\n\tvoid setActionBoxWeak(base::weak_qptr<Ui::BoxContent> box);\n\n\tvoid setupStoriesTrackIds();\n\n\tvoid startReorder(const QPoint &globalPos);\n\tvoid updateReorder(const QPoint &globalPos);\n\tvoid finishReorder();\n\tvoid cancelReorder();\n\tvoid updateShiftAnimations();\n\t[[nodiscard]] int itemIndexFromPoint(QPoint point) const;\n\t[[nodiscard]] QRect itemGeometryByIndex(int index);\n\t[[nodiscard]] BaseLayout *itemByIndex(int index);\n\t[[nodiscard]] bool canReorder() const;\n\tvoid reorderItemsInSections(int oldIndex, int newIndex);\n\tvoid resetAllItemShifts();\n\tvoid finishShiftAnimations();\n\n\tconst not_null<AbstractController*> _controller;\n\tconst std::unique_ptr<ListProvider> _provider;\n\n\tbase::flat_set<not_null<const BaseLayout*>> _heavyLayouts;\n\tbool _heavyLayoutsInvalidated = false;\n\tstd::vector<Section> _sections;\n\n\tint _visibleTop = 0;\n\tint _visibleBottom = 0;\n\tListScrollTopState _scrollTopState;\n\trpl::event_stream<int> _scrollToRequests;\n\n\tMouseAction _mouseAction = MouseAction::None;\n\tTextSelectType _mouseSelectType = TextSelectType::Letters;\n\tQPoint _mousePosition;\n\tMouseState _overState;\n\tMouseState _pressState;\n\tBaseLayout *_overLayout = nullptr;\n\tHistoryItem *_contextItem = nullptr;\n\tCursorState _mouseCursorState = CursorState();\n\tuint16 _mouseTextSymbol = 0;\n\tbool _pressWasInactive = false;\n\tSelectedMap _selected;\n\tSelectedMap _dragSelected;\n\trpl::event_stream<SelectedItems> _selectedListStream;\n\tstyle::cursor _cursor = style::cur_default;\n\tDragSelectAction _dragSelectAction = DragSelectAction::None;\n\tbool _wasSelectedText = false; // was some text selected in current drag action\n\n\tconst std::unique_ptr<DateBadge> _dateBadge;\n\n\tint _selectedLimit = 0;\n\tint _storiesAddToAlbumId = 0;\n\tint _storiesAddToAlbumTotal = 0;\n\tbase::flat_set<StoryId> _storiesInAlbum;\n\tbase::flat_set<MsgId> _storyMsgsToMarkSelected;\n\tstd::unique_ptr<StickerPremiumMark> _hiddenMark;\n\n\tbase::unique_qptr<Ui::PopupMenu> _contextMenu;\n\trpl::event_stream<> _checkForHide;\n\tbase::weak_qptr<Ui::BoxContent> _actionBoxWeak;\n\trpl::lifetime _actionBoxWeakLifetime;\n\n\tQPoint _trippleClickPoint;\n\tcrl::time _trippleClickStartTime = 0;\n\n\tbase::flat_map<not_null<Main::Session*>, rpl::lifetime> _trackedSessions;\n\n\tReorderState _reorderState;\n\tbase::flat_map<int, ShiftAnimation> _shiftAnimations;\n\tint _activeShiftAnimations = 0;\n\tUi::Animations::Simple _returnAnimation;\n\tReorderDescriptor _reorderDescriptor;\n\tbool _inDragArea = false;\n\n};\n\n} // namespace Media\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_provider.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/media/info_media_provider.h\"\n\n#include \"apiwrap.h\"\n#include \"info/media/info_media_widget.h\"\n#include \"info/media/info_media_list_section.h\"\n#include \"info/info_controller.h\"\n#include \"layout/layout_selection.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"data/data_session.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_user.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_document.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_overview.h\"\n\nnamespace Info::Media {\nnamespace {\n\nconstexpr auto kPreloadedScreensCount = 4;\nconstexpr auto kPreloadedScreensCountFull\n\t= kPreloadedScreensCount + 1 + kPreloadedScreensCount;\n\n} // namespace\n\nProvider::Provider(not_null<AbstractController*> controller)\n: _controller(controller)\n, _peer(_controller->key().peer())\n, _topicRootId(_controller->key().topic()\n\t? _controller->key().topic()->rootId()\n\t: MsgId())\n, _monoforumPeerId(_controller->key().sublist()\n\t? _controller->key().sublist()->sublistPeer()->id\n\t: PeerId())\n, _migrated(_controller->migrated())\n, _type(_controller->section().mediaType())\n, _slice(sliceKey(_universalAroundId)) {\n\t_controller->session().data().itemRemoved(\n\t) | rpl::on_next([this](auto item) {\n\t\titemRemoved(item);\n\t}, _lifetime);\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tfor (auto &layout : _layouts) {\n\t\t\tlayout.second.item->invalidateCache();\n\t\t}\n\t}, _lifetime);\n\n\t_controller->session().appConfig().ignoredRestrictionReasonsChanges(\n\t) | rpl::on_next([=](std::vector<QString> &&changed) {\n\t\tconst auto sensitive = Data::UnavailableReason::Sensitive();\n\t\tif (ranges::contains(changed, sensitive.reason)) {\n\t\t\tfor (auto &[id, layout] : _layouts) {\n\t\t\t\tlayout.item->maybeClearSensitiveSpoiler();\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n}\n\nType Provider::type() {\n\treturn _type;\n}\n\nbool Provider::hasSelectRestriction() {\n\tif (_peer->session().frozen()) {\n\t\treturn true;\n\t} else if (_peer->allowsForwarding()) {\n\t\treturn false;\n\t} else if (const auto chat = _peer->asChat()) {\n\t\treturn !chat->canDeleteMessages();\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\treturn !channel->canDeleteMessages();\n\t}\n\treturn true;\n}\n\nrpl::producer<bool> Provider::hasSelectRestrictionChanges() {\n\tif (const auto user = _peer->asUser()) {\n\t\treturn rpl::combine(\n\t\t\tData::PeerFlagValue(user, UserDataFlag::NoForwardsMyEnabled),\n\t\t\tData::PeerFlagValue(user, UserDataFlag::NoForwardsPeerEnabled)\n\t\t) | rpl::map([=] {\n\t\t\treturn hasSelectRestriction();\n\t\t}) | rpl::distinct_until_changed() | rpl::skip(1);\n\t}\n\tconst auto chat = _peer->asChat();\n\tconst auto channel = _peer->asChannel();\n\tauto noForwards = chat\n\t\t? Data::PeerFlagValue(chat, ChatDataFlag::NoForwards)\n\t\t: Data::PeerFlagValue(\n\t\t\tchannel,\n\t\t\tChannelDataFlag::NoForwards\n\t\t) | rpl::type_erased;\n\n\tauto rights = chat\n\t\t? chat->adminRightsValue()\n\t\t: channel->adminRightsValue();\n\tauto canDelete = std::move(\n\t\trights\n\t) | rpl::map([=] {\n\t\treturn chat\n\t\t\t? chat->canDeleteMessages()\n\t\t\t: channel->canDeleteMessages();\n\t});\n\treturn rpl::combine(\n\t\tstd::move(noForwards),\n\t\tstd::move(canDelete)\n\t) | rpl::map([=] {\n\t\treturn hasSelectRestriction();\n\t}) | rpl::distinct_until_changed() | rpl::skip(1);\n}\n\nbool Provider::sectionHasFloatingHeader() {\n\tswitch (_type) {\n\tcase Type::Photo:\n\tcase Type::GIF:\n\tcase Type::Video:\n\tcase Type::RoundFile:\n\tcase Type::RoundVoiceFile:\n\tcase Type::MusicFile:\n\t\treturn false;\n\tcase Type::File:\n\tcase Type::Link:\n\t\treturn true;\n\t}\n\tUnexpected(\"Type in HasFloatingHeader()\");\n}\n\nQString Provider::sectionTitle(not_null<const BaseLayout*> item) {\n\tswitch (_type) {\n\tcase Type::Photo:\n\tcase Type::GIF:\n\tcase Type::Video:\n\tcase Type::RoundFile:\n\tcase Type::RoundVoiceFile:\n\tcase Type::File:\n\t\treturn langMonthFull(item->dateTime().date());\n\n\tcase Type::Link:\n\t\treturn langDayOfMonthFull(item->dateTime().date());\n\n\tcase Type::MusicFile:\n\t\treturn QString();\n\t}\n\tUnexpected(\"Type in ListSection::setHeader()\");\n}\n\nbool Provider::sectionItemBelongsHere(\n\t\tnot_null<const BaseLayout*> item,\n\t\tnot_null<const BaseLayout*> previous) {\n\tconst auto date = item->dateTime().date();\n\tconst auto sectionDate = previous->dateTime().date();\n\n\tswitch (_type) {\n\tcase Type::Photo:\n\tcase Type::GIF:\n\tcase Type::Video:\n\tcase Type::RoundFile:\n\tcase Type::RoundVoiceFile:\n\tcase Type::File:\n\t\treturn date.year() == sectionDate.year()\n\t\t\t&& date.month() == sectionDate.month();\n\n\tcase Type::Link:\n\t\treturn date == sectionDate;\n\n\tcase Type::MusicFile:\n\t\treturn true;\n\t}\n\tUnexpected(\"Type in ListSection::belongsHere()\");\n}\n\nbool Provider::isPossiblyMyItem(not_null<const HistoryItem*> item) {\n\treturn isPossiblyMyPeerId(item->history()->peer->id);\n}\n\nbool Provider::isPossiblyMyPeerId(PeerId peerId) const {\n\treturn (peerId == _peer->id) || (_migrated && peerId == _migrated->id);\n}\n\nstd::optional<int> Provider::fullCount() {\n\treturn _slice.fullCount();\n}\n\nvoid Provider::restart() {\n\t_layouts.clear();\n\t_universalAroundId = kDefaultAroundId;\n\t_idsLimit = kMinimalIdsLimit;\n\t_slice = SparseIdsMergedSlice(sliceKey(_universalAroundId));\n\trefreshViewer();\n}\n\nvoid Provider::checkPreload(\n\t\tQSize viewport,\n\t\tnot_null<BaseLayout*> topLayout,\n\t\tnot_null<BaseLayout*> bottomLayout,\n\t\tbool preloadTop,\n\t\tbool preloadBottom) {\n\tconst auto visibleWidth = viewport.width();\n\tconst auto visibleHeight = viewport.height();\n\tconst auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;\n\tconst auto minItemHeight = MinItemHeight(_type, visibleWidth);\n\tconst auto preloadedCount = preloadedHeight / minItemHeight;\n\tconst auto preloadIdsLimitMin = (preloadedCount / 2) + 1;\n\tconst auto preloadIdsLimit = preloadIdsLimitMin\n\t\t+ (visibleHeight / minItemHeight);\n\tconst auto after = _slice.skippedAfter();\n\tconst auto topLoaded = after && (*after == 0);\n\tconst auto before = _slice.skippedBefore();\n\tconst auto bottomLoaded = before && (*before == 0);\n\n\tconst auto minScreenDelta = kPreloadedScreensCount\n\t\t- kPreloadIfLessThanScreens;\n\tconst auto minUniversalIdDelta = (minScreenDelta * visibleHeight)\n\t\t/ minItemHeight;\n\tconst auto preloadAroundItem = [&](not_null<BaseLayout*> layout) {\n\t\tauto preloadRequired = false;\n\t\tauto universalId = GetUniversalId(layout);\n\t\tif (!preloadRequired) {\n\t\t\tpreloadRequired = (_idsLimit < preloadIdsLimitMin);\n\t\t}\n\t\tif (!preloadRequired) {\n\t\t\tauto delta = _slice.distance(\n\t\t\t\tsliceKey(_universalAroundId),\n\t\t\t\tsliceKey(universalId));\n\t\t\tAssert(delta != std::nullopt);\n\t\t\tpreloadRequired = (qAbs(*delta) >= minUniversalIdDelta);\n\t\t}\n\t\tif (preloadRequired) {\n\t\t\t_idsLimit = preloadIdsLimit;\n\t\t\t_universalAroundId = universalId;\n\t\t\trefreshViewer();\n\t\t}\n\t};\n\n\tif (preloadTop && !topLoaded) {\n\t\tpreloadAroundItem(topLayout);\n\t} else if (preloadBottom && !bottomLoaded) {\n\t\tpreloadAroundItem(bottomLayout);\n\t}\n}\n\nvoid Provider::refreshViewer() {\n\t_viewerLifetime.destroy();\n\tconst auto idForViewer = sliceKey(_universalAroundId).universalId;\n\t_controller->mediaSource(\n\t\tidForViewer,\n\t\t_idsLimit,\n\t\t_idsLimit\n\t) | rpl::on_next([=](SparseIdsMergedSlice &&slice) {\n\t\tif (!slice.fullCount()) {\n\t\t\t// Don't display anything while full count is unknown.\n\t\t\treturn;\n\t\t}\n\t\t_slice = std::move(slice);\n\t\tif (auto nearest = _slice.nearest(idForViewer)) {\n\t\t\t_universalAroundId = GetUniversalId(*nearest);\n\t\t}\n\t\t_refreshed.fire({});\n\t}, _viewerLifetime);\n}\n\nrpl::producer<> Provider::refreshed() {\n\treturn _refreshed.events();\n}\n\nstd::vector<ListSection> Provider::fillSections(\n\t\tnot_null<Overview::Layout::Delegate*> delegate) {\n\tmarkLayoutsStale();\n\tconst auto guard = gsl::finally([&] { clearStaleLayouts(); });\n\n\tauto result = std::vector<ListSection>();\n\tauto section = ListSection(_type, sectionDelegate());\n\tauto count = _slice.size();\n\tfor (auto i = count; i != 0;) {\n\t\tauto universalId = GetUniversalId(_slice[--i]);\n\t\tif (auto layout = getLayout(universalId, delegate)) {\n\t\t\tif (!section.addItem(layout)) {\n\t\t\t\tsection.finishSection();\n\t\t\t\tresult.push_back(std::move(section));\n\t\t\t\tsection = ListSection(_type, sectionDelegate());\n\t\t\t\tsection.addItem(layout);\n\t\t\t}\n\t\t}\n\t}\n\tif (!section.empty()) {\n\t\tsection.finishSection();\n\t\tresult.push_back(std::move(section));\n\t}\n\treturn result;\n}\n\nvoid Provider::markLayoutsStale() {\n\tfor (auto &layout : _layouts) {\n\t\tlayout.second.stale = true;\n\t}\n}\n\nvoid Provider::clearStaleLayouts() {\n\tfor (auto i = _layouts.begin(); i != _layouts.end();) {\n\t\tif (i->second.stale) {\n\t\t\t_layoutRemoved.fire(i->second.item.get());\n\t\t\ti = _layouts.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n}\n\nrpl::producer<not_null<BaseLayout*>> Provider::layoutRemoved() {\n\treturn _layoutRemoved.events();\n}\n\nBaseLayout *Provider::lookupLayout(\n\t\tconst HistoryItem *item) {\n\tconst auto i = _layouts.find(GetUniversalId(item));\n\treturn (i != _layouts.end()) ? i->second.item.get() : nullptr;\n}\n\nbool Provider::isMyItem(not_null<const HistoryItem*> item) {\n\tconst auto peer = item->history()->peer;\n\treturn (_peer == peer) || (_migrated == peer);\n}\n\nbool Provider::isAfter(\n\t\tnot_null<const HistoryItem*> a,\n\t\tnot_null<const HistoryItem*> b) {\n\treturn (GetUniversalId(a) < GetUniversalId(b));\n}\n\nvoid Provider::setSearchQuery(QString query) {\n\tUnexpected(\"Media::Provider::setSearchQuery.\");\n}\n\nvoid Provider::jumpToMessage(\n\t\tMsgId messageId,\n\t\tFn<void(FullMsgId)> callback) {\n\t_viewerLifetime.destroy();\n\n\tconst auto peer = _controller->session().data().peer(_peer->id);\n\tconst auto request = Api::PrepareSearchRequest(\n\t\tpeer,\n\t\t_topicRootId,\n\t\t_monoforumPeerId,\n\t\t_type,\n\t\tQString(),\n\t\tmessageId,\n\t\tData::LoadDirection::Around);\n\n\tif (!request) {\n\t\treturn;\n\t}\n\n\t_controller->session().api().request(\n\t\tstd::move(*request)\n\t).done([=](const Api::SearchRequestResult &result) {\n\t\tconst auto parsed = Api::ParseSearchResult(\n\t\t\tpeer,\n\t\t\t_type,\n\t\t\tmessageId,\n\t\t\tData::LoadDirection::Around,\n\t\t\tresult);\n\n\t\tif (!parsed.messageIds.empty()) {\n\t\t\tconst auto fullId = FullMsgId(_peer->id, messageId);\n\t\t\t_universalAroundId = GetUniversalId(fullId);\n\t\t\tif (callback) {\n\t\t\t\tcallback(fullId);\n\t\t\t}\n\t\t\t_idsLimit = kMinimalIdsLimit * 2;\n\t\t\trefreshViewer();\n\t\t}\n\t}).send();\n}\n\nSparseIdsMergedSlice::Key Provider::sliceKey(\n\t\tUniversalMsgId universalId) const {\n\tusing Key = SparseIdsMergedSlice::Key;\n\tif (!_topicRootId && _migrated) {\n\t\treturn Key(\n\t\t\t_peer->id,\n\t\t\t_topicRootId,\n\t\t\t_monoforumPeerId,\n\t\t\t_migrated->id,\n\t\t\tuniversalId);\n\t}\n\tif (universalId < 0) {\n\t\t// Convert back to plain id for non-migrated histories.\n\t\tuniversalId = universalId + ServerMaxMsgId;\n\t}\n\treturn Key(\n\t\t_peer->id,\n\t\t_topicRootId,\n\t\t_monoforumPeerId,\n\t\tPeerId(),\n\t\tuniversalId);\n}\n\nvoid Provider::itemRemoved(not_null<const HistoryItem*> item) {\n\tconst auto id = GetUniversalId(item);\n\tif (const auto i = _layouts.find(id); i != end(_layouts)) {\n\t\t_layoutRemoved.fire(i->second.item.get());\n\t\t_layouts.erase(i);\n\t}\n}\n\nFullMsgId Provider::computeFullId(\n\t\tUniversalMsgId universalId) const {\n\tExpects(universalId != 0);\n\n\treturn (universalId > 0)\n\t\t? FullMsgId(_peer->id, universalId)\n\t\t: FullMsgId(\n\t\t\t(_migrated ? _migrated : _peer.get())->id,\n\t\t\tServerMaxMsgId + universalId);\n}\n\nBaseLayout *Provider::getLayout(\n\t\tUniversalMsgId universalId,\n\t\tnot_null<Overview::Layout::Delegate*> delegate) {\n\tauto it = _layouts.find(universalId);\n\tif (it == _layouts.end()) {\n\t\tif (auto layout = createLayout(universalId, delegate, _type)) {\n\t\t\tlayout->initDimensions();\n\t\t\tit = _layouts.emplace(\n\t\t\t\tuniversalId,\n\t\t\t\tstd::move(layout)).first;\n\t\t} else {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\tit->second.stale = false;\n\treturn it->second.item.get();\n}\n\nstd::unique_ptr<BaseLayout> Provider::createLayout(\n\t\tUniversalMsgId universalId,\n\t\tnot_null<Overview::Layout::Delegate*> delegate,\n\t\tType type) {\n\tconst auto item = _controller->session().data().message(\n\t\tcomputeFullId(universalId));\n\tif (!item) {\n\t\treturn nullptr;\n\t}\n\tconst auto getPhoto = [&]() -> PhotoData* {\n\t\tif (const auto media = item->media()) {\n\t\t\treturn media->photo();\n\t\t}\n\t\treturn nullptr;\n\t};\n\tconst auto getFile = [&]() -> DocumentData* {\n\t\tif (const auto media = item->media()) {\n\t\t\treturn media->document();\n\t\t}\n\t\treturn nullptr;\n\t};\n\n\tconst auto &songSt = st::overviewFileLayout;\n\tusing namespace Overview::Layout;\n\tconst auto options = [&] {\n\t\tconst auto media = item->media();\n\t\treturn MediaOptions{ .spoiler = media && media->hasSpoiler() };\n\t};\n\tswitch (type) {\n\tcase Type::Photo:\n\t\tif (const auto photo = getPhoto()) {\n\t\t\treturn std::make_unique<Photo>(\n\t\t\t\tdelegate,\n\t\t\t\titem,\n\t\t\t\tphoto,\n\t\t\t\toptions());\n\t\t}\n\t\treturn nullptr;\n\tcase Type::GIF:\n\t\tif (const auto file = getFile()) {\n\t\t\treturn std::make_unique<Gif>(delegate, item, file);\n\t\t}\n\t\treturn nullptr;\n\tcase Type::Video:\n\t\tif (const auto file = getFile()) {\n\t\t\treturn std::make_unique<Video>(delegate, item, file, options());\n\t\t}\n\t\treturn nullptr;\n\tcase Type::File:\n\t\tif (const auto file = getFile()) {\n\t\t\treturn std::make_unique<Document>(\n\t\t\t\tdelegate,\n\t\t\t\titem,\n\t\t\t\tDocumentFields{ .document = file },\n\t\t\t\tsongSt);\n\t\t}\n\t\treturn nullptr;\n\tcase Type::MusicFile:\n\t\tif (const auto file = getFile()) {\n\t\t\treturn std::make_unique<Document>(\n\t\t\t\tdelegate,\n\t\t\t\titem,\n\t\t\t\tDocumentFields{ .document = file },\n\t\t\t\tsongSt);\n\t\t}\n\t\treturn nullptr;\n\tcase Type::RoundVoiceFile:\n\t\tif (const auto file = getFile()) {\n\t\t\treturn std::make_unique<Voice>(delegate, item, file, songSt);\n\t\t}\n\t\treturn nullptr;\n\tcase Type::Link:\n\t\treturn std::make_unique<Link>(delegate, item, item->media());\n\tcase Type::RoundFile:\n\t\treturn nullptr;\n\t}\n\tUnexpected(\"Type in ListWidget::createLayout()\");\n}\n\nListItemSelectionData Provider::computeSelectionData(\n\t\tnot_null<const HistoryItem*> item,\n\t\tTextSelection selection) {\n\tauto result = ListItemSelectionData(selection);\n\tresult.canDelete = item->canDelete();\n\tresult.canForward = item->allowsForward();\n\treturn result;\n}\n\nbool Provider::allowSaveFileAs(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) {\n\treturn item->allowsForward();\n}\n\nQString Provider::showInFolderPath(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) {\n\treturn document->filepath(true);\n}\n\nvoid Provider::applyDragSelection(\n\t\tListSelectedMap &selected,\n\t\tnot_null<const HistoryItem*> fromItem,\n\t\tbool skipFrom,\n\t\tnot_null<const HistoryItem*> tillItem,\n\t\tbool skipTill) {\n\tconst auto fromId = GetUniversalId(fromItem) - (skipFrom ? 1 : 0);\n\tconst auto tillId = GetUniversalId(tillItem) - (skipTill ? 0 : 1);\n\tfor (auto i = selected.begin(); i != selected.end();) {\n\t\tconst auto itemId = GetUniversalId(i->first);\n\t\tif (itemId > fromId || itemId <= tillId) {\n\t\t\ti = selected.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tfor (auto &layoutItem : _layouts) {\n\t\tauto &&universalId = layoutItem.first;\n\t\tif (universalId <= fromId && universalId > tillId) {\n\t\t\tconst auto item = layoutItem.second.item->getItem();\n\t\t\tChangeItemSelection(\n\t\t\t\tselected,\n\t\t\t\titem,\n\t\t\t\tcomputeSelectionData(item, FullSelection));\n\t\t}\n\t}\n}\n\nint64 Provider::scrollTopStatePosition(not_null<HistoryItem*> item) {\n\treturn GetUniversalId(item).bare;\n}\n\nHistoryItem *Provider::scrollTopStateItem(ListScrollTopState state) {\n\tif (state.item && _slice.indexOf(state.item->fullId())) {\n\t\treturn state.item;\n\t} else if (const auto id = _slice.nearest(state.position)) {\n\t\tif (const auto item = _controller->session().data().message(*id)) {\n\t\t\treturn item;\n\t\t}\n\t}\n\treturn state.item;\n}\n\nvoid Provider::saveState(\n\t\tnot_null<Memento*> memento,\n\t\tListScrollTopState scrollState) {\n\tif (_universalAroundId != kDefaultAroundId && scrollState.item) {\n\t\tmemento->setAroundId(computeFullId(_universalAroundId));\n\t\tmemento->setIdsLimit(_idsLimit);\n\t\tmemento->setScrollTopItem(scrollState.item->globalId());\n\t\tmemento->setScrollTopItemPosition(scrollState.position);\n\t\tmemento->setScrollTopShift(scrollState.shift);\n\t}\n}\n\nvoid Provider::restoreState(\n\t\tnot_null<Memento*> memento,\n\t\tFn<void(ListScrollTopState)> restoreScrollState) {\n\tif (const auto limit = memento->idsLimit()) {\n\t\tauto wasAroundId = memento->aroundId();\n\t\tif (isPossiblyMyPeerId(wasAroundId.peer)) {\n\t\t\t_idsLimit = limit;\n\t\t\t_universalAroundId = GetUniversalId(wasAroundId);\n\t\t\trestoreScrollState({\n\t\t\t\t.position = memento->scrollTopItemPosition(),\n\t\t\t\t.item = MessageByGlobalId(memento->scrollTopItem()),\n\t\t\t\t.shift = memento->scrollTopShift(),\n\t\t\t});\n\t\t\trefreshViewer();\n\t\t}\n\t}\n}\n\n} // namespace Info::Media\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_provider.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/media/info_media_common.h\"\n#include \"data/data_shared_media.h\"\n\nnamespace Info {\nclass AbstractController;\n} // namespace Info\n\nnamespace Info::Media {\n\nclass Provider final : public ListProvider, private ListSectionDelegate {\npublic:\n\texplicit Provider(not_null<AbstractController*> controller);\n\n\tType type() override;\n\tbool hasSelectRestriction() override;\n\trpl::producer<bool> hasSelectRestrictionChanges() override;\n\tbool isPossiblyMyItem(not_null<const HistoryItem*> item) override;\n\n\tstd::optional<int> fullCount() override;\n\n\tvoid restart() override;\n\tvoid checkPreload(\n\t\tQSize viewport,\n\t\tnot_null<BaseLayout*> topLayout,\n\t\tnot_null<BaseLayout*> bottomLayout,\n\t\tbool preloadTop,\n\t\tbool preloadBottom) override;\n\tvoid refreshViewer() override;\n\trpl::producer<> refreshed() override;\n\n\tstd::vector<ListSection> fillSections(\n\t\tnot_null<Overview::Layout::Delegate*> delegate) override;\n\trpl::producer<not_null<BaseLayout*>> layoutRemoved() override;\n\tBaseLayout *lookupLayout(const HistoryItem *item) override;\n\tbool isMyItem(not_null<const HistoryItem*> item) override;\n\tbool isAfter(\n\t\tnot_null<const HistoryItem*> a,\n\t\tnot_null<const HistoryItem*> b) override;\n\n\tvoid setSearchQuery(QString query) override;\n\n\tvoid jumpToMessage(MsgId, Fn<void(FullMsgId)> done) override;\n\n\tListItemSelectionData computeSelectionData(\n\t\tnot_null<const HistoryItem*> item,\n\t\tTextSelection selection) override;\n\tvoid applyDragSelection(\n\t\tListSelectedMap &selected,\n\t\tnot_null<const HistoryItem*> fromItem,\n\t\tbool skipFrom,\n\t\tnot_null<const HistoryItem*> tillItem,\n\t\tbool skipTill) override;\n\n\tbool allowSaveFileAs(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) override;\n\tQString showInFolderPath(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) override;\n\n\tint64 scrollTopStatePosition(not_null<HistoryItem*> item) override;\n\tHistoryItem *scrollTopStateItem(ListScrollTopState state) override;\n\tvoid saveState(\n\t\tnot_null<Memento*> memento,\n\t\tListScrollTopState scrollState) override;\n\tvoid restoreState(\n\t\tnot_null<Memento*> memento,\n\t\tFn<void(ListScrollTopState)> restoreScrollState) override;\n\nprivate:\n\tstatic constexpr auto kMinimalIdsLimit = 16;\n\tstatic constexpr auto kDefaultAroundId = (ServerMaxMsgId - 1);\n\n\tbool sectionHasFloatingHeader() override;\n\tQString sectionTitle(not_null<const BaseLayout*> item) override;\n\tbool sectionItemBelongsHere(\n\t\tnot_null<const BaseLayout*> item,\n\t\tnot_null<const BaseLayout*> previous) override;\n\n\t[[nodiscard]] bool isPossiblyMyPeerId(PeerId peerId) const;\n\t[[nodiscard]] FullMsgId computeFullId(UniversalMsgId universalId) const;\n\t[[nodiscard]] BaseLayout *getLayout(\n\t\tUniversalMsgId universalId,\n\t\tnot_null<Overview::Layout::Delegate*> delegate);\n\t[[nodiscard]] std::unique_ptr<BaseLayout> createLayout(\n\t\tUniversalMsgId universalId,\n\t\tnot_null<Overview::Layout::Delegate*> delegate,\n\t\tType type);\n\n\t[[nodiscard]] SparseIdsMergedSlice::Key sliceKey(\n\t\tUniversalMsgId universalId) const;\n\n\tvoid itemRemoved(not_null<const HistoryItem*> item);\n\tvoid markLayoutsStale();\n\tvoid clearStaleLayouts();\n\n\tconst not_null<AbstractController*> _controller;\n\n\tconst not_null<PeerData*> _peer;\n\tconst MsgId _topicRootId = 0;\n\tconst PeerId _monoforumPeerId = 0;\n\tPeerData * const _migrated = nullptr;\n\tconst Type _type = Type::Photo;\n\n\tUniversalMsgId _universalAroundId = kDefaultAroundId;\n\tint _idsLimit = kMinimalIdsLimit;\n\tSparseIdsMergedSlice _slice;\n\n\tstd::unordered_map<UniversalMsgId, CachedItem> _layouts;\n\trpl::event_stream<not_null<BaseLayout*>> _layoutRemoved;\n\trpl::event_stream<> _refreshed;\n\n\trpl::lifetime _lifetime;\n\trpl::lifetime _viewerLifetime;\n\n};\n\n} // namespace Info::Media\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/media/info_media_widget.h\"\n\n#include \"history/history.h\"\n#include \"info/media/info_media_inner_widget.h\"\n#include \"info/info_controller.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/search_field_controller.h\"\n#include \"ui/ui_utility.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"lang/lang_keys.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Info::Media {\n\nstd::optional<int> TypeToTabIndex(Type type) {\n\tswitch (type) {\n\tcase Type::Photo: return 0;\n\tcase Type::Video: return 1;\n\tcase Type::File: return 2;\n\t}\n\treturn std::nullopt;\n}\n\nType TabIndexToType(int index) {\n\tswitch (index) {\n\tcase 0: return Type::Photo;\n\tcase 1: return Type::Video;\n\tcase 2: return Type::File;\n\t}\n\tUnexpected(\"Index in Info::Media::TabIndexToType()\");\n}\n\ntr::phrase<> SharedMediaTitle(Type type) {\n\tswitch (type) {\n\tcase Type::Photo:\n\t\treturn tr::lng_media_type_photos;\n\tcase Type::GIF:\n\t\treturn tr::lng_media_type_gifs;\n\tcase Type::Video:\n\t\treturn tr::lng_media_type_videos;\n\tcase Type::MusicFile:\n\t\treturn tr::lng_media_type_songs;\n\tcase Type::File:\n\t\treturn tr::lng_media_type_files;\n\tcase Type::RoundVoiceFile:\n\t\treturn tr::lng_media_type_audios;\n\tcase Type::Link:\n\t\treturn tr::lng_media_type_links;\n\tcase Type::RoundFile:\n\t\treturn tr::lng_media_type_rounds;\n\tcase Type::Poll:\n\t\treturn tr::lng_media_type_polls;\n\t}\n\tUnexpected(\"Bad media type in Info::TitleValue()\");\n}\n\nMemento::Memento(not_null<Controller*> controller)\n: Memento(\n\t(controller->peer()\n\t\t? controller->peer()\n\t\t: controller->storiesPeer()\n\t\t? controller->storiesPeer()\n\t\t: controller->musicPeer()\n\t\t? controller->musicPeer()\n\t\t: controller->parentController()->session().user()),\n\tcontroller->topic(),\n\tcontroller->sublist(),\n\tcontroller->migratedPeerId(),\n\t(controller->section().type() == Section::Type::Downloads\n\t\t? Type::File\n\t\t: controller->section().type() == Section::Type::Stories\n\t\t? Type::PhotoVideo\n\t\t: controller->section().type() == Section::Type::SavedMusic\n\t\t? Type::MusicFile\n\t\t: controller->section().mediaType())) {\n}\n\nMemento::Memento(not_null<PeerData*> peer, PeerId migratedPeerId, Type type)\n: Memento(peer, nullptr, nullptr, migratedPeerId, type) {\n}\n\nMemento::Memento(not_null<Data::ForumTopic*> topic, Type type)\n: Memento(topic->peer(), topic, nullptr, PeerId(), type) {\n}\n\nMemento::Memento(not_null<Data::SavedSublist*> sublist, Type type)\n: Memento(sublist->owningHistory()->peer, nullptr, sublist, PeerId(), type) {\n}\n\nMemento::Memento(\n\tnot_null<PeerData*> peer,\n\tData::ForumTopic *topic,\n\tData::SavedSublist *sublist,\n\tPeerId migratedPeerId,\n\tType type)\n: ContentMemento(peer, topic, sublist, migratedPeerId)\n, _type(type) {\n\t_searchState.query.type = type;\n\t_searchState.query.peerId = peer->id;\n\t_searchState.query.topicRootId = topic ? topic->rootId() : MsgId();\n\t_searchState.query.monoforumPeerId = sublist\n\t\t? sublist->sublistPeer()->id\n\t\t: PeerId();\n\t_searchState.query.migratedPeerId = migratedPeerId;\n\tif (migratedPeerId) {\n\t\t_searchState.migratedList = Storage::SparseIdsList();\n\t}\n}\n\nSection Memento::section() const {\n\treturn Section(_type);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(\n\t\tparent,\n\t\tcontroller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nWidget::Widget(QWidget *parent, not_null<Controller*> controller)\n: ContentWidget(parent, controller) {\n\t_inner = setInnerWidget(object_ptr<InnerWidget>(\n\t\tthis,\n\t\tcontroller));\n\t_inner->setScrollHeightValue(scrollHeightValue());\n\t_inner->scrollToRequests(\n\t) | rpl::on_next([this](Ui::ScrollToRequest request) {\n\t\tscrollTo(request);\n\t}, _inner->lifetime());\n}\n\nrpl::producer<SelectedItems> Widget::selectedListValue() const {\n\treturn _inner->selectedListValue();\n}\n\nvoid Widget::selectionAction(SelectionAction action) {\n\t_inner->selectionAction(action);\n}\n\nvoid Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {\n\tconst auto type = controller()->section().mediaType();\n\tif (type != Type::Photo && type != Type::Video) {\n\t\treturn;\n\t}\n\taddAction(tr::lng_calendar(tr::now), [=] {\n\t\tcontroller()->parentController()->showCalendar({\n\t\t\t.chat = Dialogs::Key(\n\t\t\t\tcontroller()->session().data().history(\n\t\t\t\t\tcontroller()->key().peer())),\n\t\t\t.date = QDate::currentDate(),\n\t\t\t.mediaPhoto = (type == Type::Photo),\n\t\t\t.mediaVideo = (type == Type::Video),\n\t\t\t.customJump = [=](FullMsgId id, Fn<void()> close) {\n\t\t\t\t_inner->jumpToMessage(id.msg);\n\t\t\t\tclose();\n\t\t\t},\n\t\t});\n\t}, &st::menuIconSchedule);\n}\n\nrpl::producer<QString> Widget::title() {\n\tif (controller()->key().peer()->sharedMediaInfo() && isStackBottom()) {\n\t\treturn tr::lng_profile_shared_media();\n\t}\n\treturn SharedMediaTitle(controller()->section().mediaType())();\n}\n\nvoid Widget::setIsStackBottom(bool isStackBottom) {\n\tContentWidget::setIsStackBottom(isStackBottom);\n\t_inner->setIsStackBottom(isStackBottom);\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\tif (!controller()->validateMementoPeer(memento)) {\n\t\treturn false;\n\t}\n\tif (const auto mediaMemento = dynamic_cast<Memento*>(memento.get())) {\n\t\tif (_inner->showInternal(mediaMemento)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(controller());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n}\n\n} // namespace Info::Media\n"
  },
  {
    "path": "Telegram/SourceFiles/info/media/info_media_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"data/data_search_controller.h\"\n\nnamespace tr {\ntemplate <typename ...Tags>\nstruct phrase;\n} // namespace tr\n\nnamespace Data {\nclass ForumTopic;\n} // namespace Data\n\nnamespace Info::Media {\n\nusing Type = Storage::SharedMediaType;\n\n[[nodiscard]] std::optional<int> TypeToTabIndex(Type type);\n[[nodiscard]] Type TabIndexToType(int index);\n[[nodiscard]] tr::phrase<> SharedMediaTitle(Type type);\n\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\texplicit Memento(not_null<Controller*> controller);\n\tMemento(not_null<PeerData*> peer, PeerId migratedPeerId, Type type);\n\tMemento(not_null<Data::ForumTopic*> topic, Type type);\n\tMemento(not_null<Data::SavedSublist*> sublist, Type type);\n\n\tusing SearchState = Api::DelayedSearchController::SavedState;\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\t[[nodiscard]] Section section() const override;\n\n\t[[nodiscard]] Type type() const {\n\t\treturn _type;\n\t}\n\n\t// Only for media, not for downloads.\n\tvoid setAroundId(FullMsgId aroundId) {\n\t\t_aroundId = aroundId;\n\t}\n\t[[nodiscard]] FullMsgId aroundId() const {\n\t\treturn _aroundId;\n\t}\n\tvoid setIdsLimit(int limit) {\n\t\t_idsLimit = limit;\n\t}\n\t[[nodiscard]] int idsLimit() const {\n\t\treturn _idsLimit;\n\t}\n\n\tvoid setScrollTopItem(GlobalMsgId item) {\n\t\t_scrollTopItem = item;\n\t}\n\t[[nodiscard]] GlobalMsgId scrollTopItem() const {\n\t\treturn _scrollTopItem;\n\t}\n\tvoid setScrollTopItemPosition(int64 position) {\n\t\t_scrollTopItemPosition = position;\n\t}\n\t[[nodiscard]] int64 scrollTopItemPosition() const {\n\t\treturn _scrollTopItemPosition;\n\t}\n\tvoid setScrollTopShift(int shift) {\n\t\t_scrollTopShift = shift;\n\t}\n\t[[nodiscard]] int scrollTopShift() const {\n\t\treturn _scrollTopShift;\n\t}\n\tvoid setSearchState(SearchState &&state) {\n\t\t_searchState = std::move(state);\n\t}\n\t[[nodiscard]] SearchState searchState() {\n\t\treturn std::move(_searchState);\n\t}\n\nprivate:\n\tMemento(\n\t\tnot_null<PeerData*> peer,\n\t\tData::ForumTopic *topic,\n\t\tData::SavedSublist *sublist,\n\t\tPeerId migratedPeerId,\n\t\tType type);\n\n\tType _type = Type::Photo;\n\tFullMsgId _aroundId;\n\tint _idsLimit = 0;\n\tint64 _scrollTopItemPosition = 0;\n\tGlobalMsgId _scrollTopItem;\n\tint _scrollTopShift = 0;\n\tSearchState _searchState;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller);\n\n\tvoid setIsStackBottom(bool isStackBottom) override;\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\n\trpl::producer<SelectedItems> selectedListValue() const override;\n\tvoid selectionAction(SelectionAction action) override;\n\n\tvoid fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override;\n\n\trpl::producer<QString> title() override;\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tInnerWidget *_inner = nullptr;\n\n};\n\n} // namespace Info::Media\n"
  },
  {
    "path": "Telegram/SourceFiles/info/members/info_members_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/members/info_members_widget.h\"\n\n#include \"info/profile/info_profile_members.h\"\n#include \"info/info_controller.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/ui_utility.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_channel.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info {\nnamespace Members {\n\nMemento::Memento(not_null<Controller*> controller)\n: Memento(\n\tcontroller->peer(),\n\tcontroller->migratedPeerId()) {\n}\n\nMemento::Memento(not_null<PeerData*> peer, PeerId migratedPeerId)\n: ContentMemento(peer, nullptr, nullptr, migratedPeerId) {\n}\n\nSection Memento::section() const {\n\treturn Section(Section::Type::Members);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(\n\t\tparent,\n\t\tcontroller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nvoid Memento::setState(std::unique_ptr<SavedState> state) {\n\t_state = std::move(state);\n}\n\nstd::unique_ptr<SavedState> Memento::state() {\n\treturn std::move(_state);\n}\n\nMemento::~Memento() = default;\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: ContentWidget(parent, controller) {\n\t_inner = setInnerWidget(object_ptr<Profile::Members>(\n\t\tthis,\n\t\tcontroller));\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\tif (!controller()->validateMementoPeer(memento)) {\n\t\treturn false;\n\t}\n\tif (auto membersMemento = dynamic_cast<Memento*>(memento.get())) {\n\t\trestoreState(membersMemento);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nrpl::producer<QString> Widget::title() {\n\tif (const auto channel = controller()->key().peer()->asChannel()) {\n\t\treturn channel->isMegagroup()\n\t\t\t? tr::lng_profile_participants_section()\n\t\t\t: tr::lng_profile_subscribers_section();\n\t}\n\treturn tr::lng_profile_participants_section();\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(controller());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\tmemento->setState(_inner->saveState());\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento->state());\n\tscrollTopRestore(memento->scrollTop());\n}\n\n} // namespace Members\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/members/info_members_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n\nstruct PeerListState;\n\nnamespace Info {\nnamespace Profile {\nclass Members;\nstruct MembersState;\n} // namespace Profile\n\nnamespace Members {\n\nusing SavedState = Profile::MembersState;\n\nclass Memento final : public ContentMemento {\npublic:\n\tMemento(not_null<Controller*> controller);\n\tMemento(not_null<PeerData*> peer, PeerId migratedPeerId);\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\tvoid setState(std::unique_ptr<SavedState> state);\n\tstd::unique_ptr<SavedState> state();\n\n\t~Memento();\n\nprivate:\n\tstd::unique_ptr<SavedState> _state;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller);\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\n\trpl::producer<QString> title() override;\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tProfile::Members *_inner = nullptr;\n\n};\n\n} // namespace Members\n} // namespace Info\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_collections.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/peer_gifts/info_peer_gifts_collections.h\"\n\n#include \"api/api_credits.h\"\n#include \"apiwrap.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_star_gift.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info::PeerGifts {\nnamespace {\n\nconstexpr auto kCollectionNameLimit = 12;\n\nvoid EditCollectionBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tint id,\n\t\tData::SavedStarGiftId addId,\n\t\tQString currentName,\n\t\tFn<void(MTPStarGiftCollection)> finished) {\n\tbox->setTitle(id\n\t\t? tr::lng_gift_collection_edit()\n\t\t: tr::lng_gift_collection_new_title());\n\n\tif (!id) {\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_gift_collection_new_text(),\n\t\t\t\tst::collectionAbout));\n\t}\n\tconst auto title = box->addRow(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tbox,\n\t\t\tst::collectionNameField,\n\t\t\ttr::lng_gift_collection_new_ph(),\n\t\t\tcurrentName));\n\ttitle->setMaxLength(kCollectionNameLimit * 2);\n\tbox->setFocusCallback([=] {\n\t\ttitle->setFocusFast();\n\t});\n\n\tUi::AddLengthLimitLabel(title, kCollectionNameLimit);\n\n\tconst auto show = navigation->uiShow();\n\tconst auto session = &peer->session();\n\n\tconst auto creating = std::make_shared<bool>(false);\n\tconst auto submit = [=] {\n\t\tif (*creating) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto text = title->getLastText().trimmed();\n\t\tif (text.isEmpty() || text.size() > kCollectionNameLimit) {\n\t\t\ttitle->showError();\n\t\t\treturn;\n\t\t}\n\n\t\t*creating = true;\n\t\tauto ids = QVector<MTPInputSavedStarGift>();\n\t\tif (addId) {\n\t\t\tids.push_back(Api::InputSavedStarGiftId(addId));\n\t\t}\n\t\tconst auto weak = base::make_weak(box);\n\t\tconst auto done = [=](const MTPStarGiftCollection &result) {\n\t\t\t*creating = false;\n\t\t\tif (const auto onstack = finished) {\n\t\t\t\tonstack(result);\n\t\t\t}\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t};\n\t\tconst auto fail = [=](const MTP::Error &error) {\n\t\t\t*creating = false;\n\t\t\tconst auto &type = error.type();\n\t\t\tif (type == u\"COLLECTIONS_TOO_MANY\"_q) {\n\t\t\t\tshow->show(Ui::MakeInformBox({\n\t\t\t\t\t.text = tr::lng_gift_collection_limit_text(),\n\t\t\t\t\t.confirmText = tr::lng_box_ok(),\n\t\t\t\t\t.title = tr::lng_gift_collection_limit_title(),\n\t\t\t\t}));\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->closeBox();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tshow->showToast(error.type());\n\t\t\t}\n\t\t};\n\t\tif (id) {\n\t\t\tusing Flag = MTPpayments_UpdateStarGiftCollection::Flag;\n\t\t\tsession->api().request(MTPpayments_UpdateStarGiftCollection(\n\t\t\t\tMTP_flags(Flag::f_title),\n\t\t\t\tpeer->input(),\n\t\t\t\tMTP_int(id),\n\t\t\t\tMTP_string(text),\n\t\t\t\tMTPVector<MTPInputSavedStarGift>(),\n\t\t\t\tMTPVector<MTPInputSavedStarGift>(),\n\t\t\t\tMTPVector<MTPInputSavedStarGift>()\n\t\t\t)).done(done).fail(fail).send();\n\t\t} else {\n\t\t\tsession->api().request(MTPpayments_CreateStarGiftCollection(\n\t\t\t\tpeer->input(),\n\t\t\t\tMTP_string(text),\n\t\t\t\tMTP_vector<MTPInputSavedStarGift>(ids)\n\t\t\t)).done(done).fail(fail).send();\n\t\t}\n\t};\n\ttitle->submits() | rpl::on_next(submit, title->lifetime());\n\tauto text = id\n\t\t? tr::lng_settings_save()\n\t\t: tr::lng_gift_collection_new_create();\n\tbox->addButton(std::move(text), submit);\n\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\n} // namespace\n\nvoid NewCollectionBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tData::SavedStarGiftId addId,\n\t\tFn<void(MTPStarGiftCollection)> added) {\n\tEditCollectionBox(box, navigation, peer, 0, addId, QString(), added);\n}\n\nvoid EditCollectionNameBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tint id,\n\t\tQString current,\n\t\tFn<void(QString)> done) {\n\tEditCollectionBox(box, navigation, peer, id, {}, current, [=](\n\t\t\tconst MTPStarGiftCollection &result) {\n\t\tdone(qs(result.data().vtitle()));\n\t});\n}\n\n} // namespace Info::PeerGifts\n"
  },
  {
    "path": "Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_collections.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nclass SavedStarGiftId;\n} // namespace Data\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nnamespace Info::PeerGifts {\n\nvoid NewCollectionBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tData::SavedStarGiftId addId,\n\tFn<void(MTPStarGiftCollection)> added);\n\nvoid EditCollectionNameBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tint id,\n\tQString current,\n\tFn<void(QString)> done);\n\n} // namespace Info::PeerGifts\n"
  },
  {
    "path": "Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/peer_gifts/info_peer_gifts_common.h\"\n\n#include \"api/api_global_privacy.h\"\n#include \"api/api_premium.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/send_credits_box.h\" // SetButtonMarkedLabel\n#include \"boxes/sticker_set_box.h\"\n#include \"chat_helpers/stickers_gift_box_pack.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"core/ui_integration.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_credits.h\" // CreditsHistoryEntry\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/view/media/history_view_sticker_player.h\"\n#include \"lang/lang_keys.h\"\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"main/main_session.h\"\n#include \"overview/overview_checkbox.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/painter.h\"\n#include \"ui/top_background_gradient.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_overview.h\"\n#include \"styles/style_premium.h\"\n\nnamespace Info::PeerGifts {\nnamespace {\n\nconstexpr auto kGiftsPerRow = 3;\nconstexpr auto kCraftUnavailableOpacity = 0.5;\n\n[[nodiscard]] bool AllowedToSend(\n\t\tconst GiftTypeStars &gift,\n\t\tnot_null<PeerData*> peer) {\n\tusing Type = Api::DisallowedGiftType;\n\tconst auto user = peer->asUser();\n\tif (!user) {\n\t\treturn gift.resale\n\t\t\t|| (!gift.info.requirePremium\n\t\t\t\t&& !gift.info.auction()\n\t\t\t\t&& !gift.info.limitedCount);\n\t} else if (user->isSelf()) {\n\t\treturn true;\n\t}\n\tconst auto disallowedTypes = user ? user->disallowedGiftTypes() : Type();\n\tconst auto allowLimited = !(disallowedTypes & Type::Limited);\n\tconst auto allowUnlimited = !(disallowedTypes & Type::Unlimited);\n\tconst auto allowUnique = !(disallowedTypes & Type::Unique);\n\tif (gift.resale) {\n\t\treturn allowUnique;\n\t} else if (!gift.info.limitedCount) {\n\t\treturn allowUnlimited;\n\t}\n\treturn allowLimited || (gift.info.starsToUpgrade && allowUnique);\n}\n\n} // namespace\n\nstd::strong_ordering operator<=>(const GiftBadge &a, const GiftBadge &b) {\n\tconst auto result1 = (a.text <=> b.text);\n\tif (result1 != std::strong_ordering::equal) {\n\t\treturn result1;\n\t}\n\tconst auto result2 = (a.bg1.rgb() <=> b.bg1.rgb());\n\tif (result2 != std::strong_ordering::equal) {\n\t\treturn result2;\n\t}\n\tconst auto result3 = (a.bg2.rgb() <=> b.bg2.rgb());\n\tif (result3 != std::strong_ordering::equal) {\n\t\treturn result3;\n\t}\n\tconst auto result4 = (a.border.rgb() <=> b.border.rgb());\n\tif (result4 != std::strong_ordering::equal) {\n\t\treturn result4;\n\t}\n\tconst auto result5 = (a.fg.rgb() <=> b.fg.rgb());\n\tif (result5 != std::strong_ordering::equal) {\n\t\treturn result5;\n\t}\n\treturn a.gradient <=> b.gradient;\n}\n\nrpl::producer<std::vector<GiftTypeStars>> GiftsStars(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<PeerData*> peer) {\n\tstruct Session {\n\t\tstd::vector<GiftTypeStars> last;\n\t};\n\tstatic auto Map = base::flat_map<not_null<Main::Session*>, Session>();\n\n\tconst auto filtered = [=](std::vector<GiftTypeStars> list) {\n\t\tlist.erase(ranges::remove_if(list, [&](const GiftTypeStars &gift) {\n\t\t\treturn !AllowedToSend(gift, peer);\n\t\t}), end(list));\n\t\treturn list;\n\t};\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tauto i = Map.find(session);\n\t\tif (i == end(Map)) {\n\t\t\ti = Map.emplace(session, Session()).first;\n\t\t\tsession->lifetime().add([=] { Map.remove(session); });\n\t\t}\n\t\tif (!i->second.last.empty()) {\n\t\t\tconsumer.put_next(filtered(i->second.last));\n\t\t}\n\n\t\tusing namespace Api;\n\t\tconst auto api = lifetime.make_state<PremiumGiftCodeOptions>(peer);\n\t\tapi->requestStarGifts(\n\t\t) | rpl::on_error_done([=](QString error) {\n\t\t\tconsumer.put_next({});\n\t\t}, [=] {\n\t\t\tauto list = std::vector<GiftTypeStars>();\n\t\t\tconst auto &gifts = api->starGifts();\n\t\t\tlist.reserve(gifts.size());\n\t\t\tfor (auto &gift : gifts) {\n\t\t\t\tlist.push_back({ .info = gift });\n\t\t\t\tif (gift.resellCount > 0) {\n\t\t\t\t\tlist.push_back({ .info = gift, .resale = true });\n\t\t\t\t}\n\t\t\t}\n\t\t\tranges::stable_sort(list, [](const auto &a, const auto &b) {\n\t\t\t\tconst auto soldOut = [](const auto &gift) {\n\t\t\t\t\treturn gift.info.soldOut && !gift.resale;\n\t\t\t\t};\n\t\t\t\treturn soldOut(a) < soldOut(b);\n\t\t\t});\n\n\t\t\tauto &map = Map[session];\n\t\t\tif (map.last != list || list.empty()) {\n\t\t\t\tmap.last = list;\n\t\t\t\tconsumer.put_next(filtered(std::move(list)));\n\t\t\t}\n\t\t}, lifetime);\n\n\t\treturn lifetime;\n\t};\n}\n\nGiftButton::GiftButton(\n\tQWidget *parent,\n\tnot_null<GiftButtonDelegate*> delegate)\n: AbstractButton(parent)\n, _delegate(delegate)\n, _lockedTimer([=] { refreshLocked(); }) {\n\tstyle::PaletteChanged() | rpl::on_next([=] {\n\t\t_delegate->invalidateCache();\n\t\tupdate();\n\t}, lifetime());\n}\n\nGiftButton::~GiftButton() {\n\tunsubscribe();\n}\n\nvoid GiftButton::onStateChanged(State was, StateChangeSource source) {\n\tif (_check) {\n\t\tconst auto diff = state() ^ was;\n\t\tif (diff & State::Enum::Over) {\n\t\t\t_check->setActive(state() & State::Enum::Over);\n\t\t}\n\t\tif (diff & State::Enum::Down) {\n\t\t\t_check->setPressed(state() & State::Enum::Down);\n\t\t}\n\t}\n}\n\nvoid GiftButton::unsubscribe() {\n\tif (_subscribed) {\n\t\t_subscribed = false;\n\t\t_userpic->subscribeToUpdates(nullptr);\n\t}\n}\n\nvoid GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) {\n\t_mode = mode;\n\n\tconst auto unique = v::is<GiftTypeStars>(descriptor)\n\t\t? v::get<GiftTypeStars>(descriptor).info.unique.get()\n\t\t: nullptr;\n\tconst auto resalePrice = (unique && _mode != Mode::CraftPreview)\n\t\t? unique->starsForResale\n\t\t: 0;\n\tif (_descriptor == descriptor && _resalePrice == resalePrice) {\n\t\treturn;\n\t}\n\tconst auto starsType = Ui::Premium::MiniStarsType::SlowStars;\n\tunsubscribe();\n\tupdate();\n\n\tconst auto format = [=](int64 number) {\n\t\tconst auto onlyK = (number < 100'000'000);\n\t\treturn (number >= 1'000'000)\n\t\t\t? Lang::FormatCountToShort(number, onlyK).string\n\t\t\t: Lang::FormatCountDecimal(number);\n\t};\n\n\tconst auto auctionStartDate = v::is<GiftTypeStars>(descriptor)\n\t\t? v::get<GiftTypeStars>(descriptor).info.auctionStartDate\n\t\t: TimeId();\n\tconst auto upcomingAuction = (auctionStartDate > base::unixtime::now());\n\n\t_descriptor = descriptor;\n\t_resalePrice = resalePrice;\n\tconst auto resale = (_resalePrice > 0);\n\tv::match(descriptor, [&](const GiftTypePremium &data) {\n\t\tconst auto months = data.months;\n\t\t_text = Ui::Text::String(st::giftBoxGiftHeight / 4);\n\t\t_text.setMarkedText(\n\t\t\tst::defaultTextStyle,\n\t\t\ttr::bold(\n\t\t\t\ttr::lng_months(tr::now, lt_count, months)\n\t\t\t).append('\\n').append(\n\t\t\t\ttr::lng_gift_premium_label(tr::now)\n\t\t\t));\n\t\t_price.setText(\n\t\t\tst::semiboldTextStyle,\n\t\t\tUi::FillAmountAndCurrency(\n\t\t\t\tdata.cost,\n\t\t\t\tdata.currency,\n\t\t\t\ttrue));\n\t\tif (const auto stars = data.stars) {\n\t\t\tconst auto starsText = Lang::FormatCountDecimal(stars);\n\t\t\t_byStars.setMarkedText(\n\t\t\t\tst::giftBoxByStarsStyle,\n\t\t\t\ttr::lng_gift_premium_by_stars(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_amount,\n\t\t\t\t\t_delegate->ministar().append(' ' + starsText),\n\t\t\t\t\ttr::marked),\n\t\t\t\tkMarkupTextOptions,\n\t\t\t\t_delegate->textContext());\n\t\t}\n\t\t_userpic = nullptr;\n\t\tif (!_stars) {\n\t\t\t_stars.emplace(this, true, starsType);\n\t\t}\n\t\t_stars->setColorOverride(QGradientStops{\n\t\t\t{ 0., anim::with_alpha(st::windowActiveTextFg->c, .3) },\n\t\t\t{ 1., st::windowActiveTextFg->c },\n\t\t});\n\t\t_lockedUntilDate = 0;\n\t}, [&](const GiftTypeStars &data) {\n\t\tconst auto soldOut = data.info.limitedCount\n\t\t\t&& !data.userpic\n\t\t\t&& !data.info.limitedLeft;\n\t\t_userpic = (!data.userpic || (_mode == Mode::Selection))\n\t\t\t? nullptr\n\t\t\t: data.from\n\t\t\t? Ui::MakeUserpicThumbnail(data.from)\n\t\t\t: Ui::MakeHiddenAuthorThumbnail();\n\t\tif ((small() && !resale) || (_mode == Mode::Craft)) {\n\t\t\t_price = {};\n\t\t\t_stars.reset();\n\t\t\treturn;\n\t\t}\n\t\t_price.setMarkedText(\n\t\t\tst::semiboldTextStyle,\n\t\t\t(data.resale\n\t\t\t\t? ((unique && data.forceTon)\n\t\t\t\t\t? Data::FormatGiftResaleTon(*unique)\n\t\t\t\t\t: (unique\n\t\t\t\t\t? _delegate->monostar()\n\t\t\t\t\t\t: _delegate->star()).append(' ').append(\n\t\t\t\t\t\t\tformat(unique\n\t\t\t\t\t\t\t\t? unique->starsForResale\n\t\t\t\t\t\t\t\t: data.info.starsResellMin)\n\t\t\t\t\t\t).append(data.info.resellCount > 1 ? \"+\" : \"\"))\n\t\t\t\t: (small() && unique && unique->starsForResale)\n\t\t\t\t? Data::FormatGiftResaleAsked(*unique)\n\t\t\t\t: unique\n\t\t\t\t? tr::lng_gift_transfer_button(tr::now, tr::marked)\n\t\t\t\t: data.info.auction()\n\t\t\t\t? ((data.info.soldOut || upcomingAuction)\n\t\t\t\t\t? tr::lng_gift_stars_auction_view\n\t\t\t\t\t: tr::lng_gift_stars_auction_join)(tr::now, tr::marked)\n\t\t\t\t: _delegate->star().append(' ' + format(data.info.stars))),\n\t\t\tkMarkupTextOptions,\n\t\t\t_delegate->textContext());\n\t\tif (!_stars) {\n\t\t\t_stars.emplace(this, true, starsType);\n\t\t}\n\t\tif (unique) {\n\t\t\tconst auto white = QColor(255, 255, 255);\n\t\t\t_stars->setColorOverride(QGradientStops{\n\t\t\t\t{ 0., anim::with_alpha(white, .3) },\n\t\t\t\t{ 1., white },\n\t\t\t});\n\t\t} else if (data.resale) {\n\t\t\t_stars->setColorOverride(\n\t\t\t\tUi::Premium::CreditsIconGradientStops());\n\t\t} else if (soldOut) {\n\t\t\t_stars.reset();\n\t\t} else {\n\t\t\t_stars->setColorOverride(\n\t\t\t\tUi::Premium::CreditsIconGradientStops());\n\t\t}\n\t\t_lockedUntilDate = data.resale ? 0 : data.info.lockedUntilDate;\n\t});\n\n\trefreshLocked();\n\n\t_resolvedDocument = nullptr;\n\t_documentLifetime = _delegate->sticker(\n\t\tdescriptor\n\t) | rpl::on_next([=](not_null<DocumentData*> document) {\n\t\t_documentLifetime.destroy();\n\t\tsetDocument(document);\n\t});\n\tif (_resolvedDocument) {\n\t\t_documentLifetime.destroy();\n\t}\n\n\t_patterned = false;\n\t_uniqueBackgroundCache = QImage();\n\t_uniquePatternEmoji = nullptr;\n\t_uniquePatternCache.clear();\n\n\tif (_price.isEmpty()) {\n\t\t_button = QRect();\n\t\treturn;\n\t}\n\tconst auto buttonw = _price.maxWidth();\n\tconst auto buttonh = st::semiboldFont->height;\n\tconst auto inner = QRect(\n\t\tQPoint(),\n\t\tQSize(buttonw, buttonh)\n\t).marginsAdded(st::giftBoxButtonPadding);\n\tconst auto skipy = _delegate->buttonSize().height()\n\t\t- (small()\n\t\t\t? st::giftBoxButtonBottomSmall\n\t\t\t: _byStars.isEmpty()\n\t\t\t? st::giftBoxButtonBottom\n\t\t\t: st::giftBoxButtonBottomByStars)\n\t\t- inner.height();\n\tconst auto skipx = (width() - inner.width()) / 2;\n\tconst auto outer = (width() - 2 * skipx);\n\t_button = QRect(skipx, skipy, outer, inner.height());\n\tif (_stars) {\n\t\tconst auto padding = _button.height() / 2;\n\t\t_stars->setCenter(_button - QMargins(padding, 0, padding, 0));\n\t}\n}\n\nvoid GiftButton::refreshLocked() {\n\t_lockedTimer.cancel();\n\n\tconst auto lockedFor = _lockedUntilDate\n\t\t? std::max(_lockedUntilDate - base::unixtime::now(), TimeId())\n\t\t: TimeId();\n\tconst auto locked = (lockedFor > 0);\n\tif (locked) {\n\t\t_lockedTimer.callOnce(std::min(lockedFor, 86'400) * crl::time(1000));\n\t}\n\tif (_locked != locked) {\n\t\t_locked = locked;\n\t\tupdate();\n\t}\n}\n\nvoid GiftButton::setDocument(not_null<DocumentData*> document) {\n\t_resolvedDocument = document;\n\tif (_playerDocument == document) {\n\t\treturn;\n\t}\n\n\tconst auto media = document->createMediaView();\n\tmedia->checkStickerLarge();\n\tmedia->goodThumbnailWanted();\n\n\tconst auto destroyed = base::take(_player);\n\t_playerDocument = nullptr;\n\t_mediaLifetime = rpl::single() | rpl::then(\n\t\tdocument->session().downloaderTaskFinished()\n\t) | rpl::filter([=] {\n\t\treturn media->loaded();\n\t}) | rpl::on_next([=] {\n\t\t_mediaLifetime.destroy();\n\n\t\tauto result = std::unique_ptr<HistoryView::StickerPlayer>();\n\t\tconst auto sticker = document->sticker();\n\t\tif (sticker->isLottie()) {\n\t\t\tresult = std::make_unique<HistoryView::LottiePlayer>(\n\t\t\t\tChatHelpers::LottiePlayerFromDocument(\n\t\t\t\t\tmedia.get(),\n\t\t\t\t\tChatHelpers::StickerLottieSize::InlineResults,\n\t\t\t\t\tstickerSize(),\n\t\t\t\t\tLottie::Quality::High));\n\t\t} else if (sticker->isWebm()) {\n\t\t\tresult = std::make_unique<HistoryView::WebmPlayer>(\n\t\t\t\tmedia->owner()->location(),\n\t\t\t\tmedia->bytes(),\n\t\t\t\tstickerSize());\n\t\t} else {\n\t\t\tresult = std::make_unique<HistoryView::StaticStickerPlayer>(\n\t\t\t\tmedia->owner()->location(),\n\t\t\t\tmedia->bytes(),\n\t\t\t\tstickerSize());\n\t\t}\n\t\tresult->setRepaintCallback([=] { update(); });\n\t\t_playerDocument = media->owner();\n\t\t_player = std::move(result);\n\t\tupdate();\n\t});\n\tif (_playerDocument) {\n\t\t_mediaLifetime.destroy();\n\t}\n}\n\nvoid GiftButton::setGeometry(QRect inner, QMargins extend) {\n\t_extend = extend;\n\tAbstractButton::setGeometry(inner.marginsAdded(extend));\n}\n\nQMargins GiftButton::currentExtend() const {\n\tconst auto progress = (_selectionMode == GiftSelectionMode::Border)\n\t\t? _selectedAnimation.value(_selected ? 1. : 0.)\n\t\t: 0.;\n\tconst auto added = anim::interpolate(0, st::giftBoxSelectSkip, progress);\n\treturn _extend + QMargins(added, added, added, added);\n}\n\nbool GiftButton::small() const {\n\treturn (_mode != Mode::Full) && (_mode != Mode::CraftResale);\n}\n\nvoid GiftButton::toggleSelected(\n\t\tbool selected,\n\t\tGiftSelectionMode selectionMode,\n\t\tanim::type animated) {\n\t_selectionMode = selectionMode;\n\tif (_selectionMode != GiftSelectionMode::Check) {\n\t\t_check = nullptr;\n\t} else if (!_check) {\n\t\t_check = std::make_unique<Overview::Layout::Checkbox>(\n\t\t\t[=] { update(); },\n\t\t\tst::overviewSmallCheck);\n\t}\n\tif (_selected == selected) {\n\t\tif (animated == anim::type::instant) {\n\t\t\t_selectedAnimation.stop();\n\t\t}\n\t\treturn;\n\t}\n\n\tconst auto duration = st::defaultRoundCheckbox.duration;\n\t_selected = selected;\n\tif (animated == anim::type::instant) {\n\t\tif (_check) {\n\t\t\t_check->finishAnimating();\n\t\t}\n\t\t_selectedAnimation.stop();\n\t\treturn;\n\t} else if (_check) {\n\t\t_check->setChecked(selected, animated);\n\t\treturn;\n\t}\n\t_selectedAnimation.start([=] {\n\t\tupdate();\n\t}, selected ? 0. : 1., selected ? 1. : 0., duration, anim::easeOutCirc);\n}\n\nvoid GiftButton::paintBackground(QPainter &p, const QImage &background) {\n\tconst auto removed = currentExtend() - _extend;\n\tconst auto x = removed.left();\n\tconst auto y = removed.top();\n\tconst auto width = this->width() - x - removed.right();\n\tconst auto height = this->height() - y - removed.bottom();\n\tconst auto dpr = int(background.devicePixelRatio());\n\tconst auto bwidth = background.width() / dpr;\n\tconst auto bheight = background.height() / dpr;\n\tconst auto fillRow = [&](int yfrom, int ytill, int bfrom) {\n\t\tconst auto fill = [&](int xto, int wto, int xfrom, int wfrom = 0) {\n\t\t\tconst auto fheight = ytill - yfrom;\n\t\t\tp.drawImage(\n\t\t\t\tQRect(x + xto, y + yfrom, wto, fheight),\n\t\t\t\tbackground,\n\t\t\t\tQRect(\n\t\t\t\t\tQPoint(xfrom, bfrom) * dpr,\n\t\t\t\t\tQSize((wfrom ? wfrom : wto), fheight) * dpr));\n\t\t};\n\t\tif (width < bwidth) {\n\t\t\tconst auto xhalf = width / 2;\n\t\t\tfill(0, xhalf, 0);\n\t\t\tfill(xhalf, width - xhalf, bwidth - (width - xhalf));\n\t\t} else if (width == bwidth) {\n\t\t\tfill(0, width, 0);\n\t\t} else {\n\t\t\tconst auto half = bwidth / (2 * dpr);\n\t\t\tfill(0, half, 0);\n\t\t\tfill(width - half, half, bwidth - half);\n\t\t\tfill(half, width - 2 * half, half, 1);\n\t\t}\n\t};\n\tif (height < bheight) {\n\t\tfillRow(0, height / 2, 0);\n\t\tfillRow(height / 2, height, bheight - (height - (height / 2)));\n\t} else {\n\t\tfillRow(0, height, 0);\n\t}\n\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto progress = (_selectionMode == GiftSelectionMode::Border)\n\t\t? _selectedAnimation.value(_selected ? 1. : 0.)\n\t\t: 0.;\n\tif (progress < 0.01) {\n\t\treturn;\n\t}\n\tconst auto pwidth = progress * st::defaultRoundCheckbox.width;\n\tp.setPen(QPen(st::defaultRoundCheckbox.bgActive->c, pwidth));\n\tp.setBrush(Qt::NoBrush);\n\tconst auto rounded = rect().marginsRemoved(_extend);\n\tconst auto phalf = pwidth / 2.;\n\tconst auto extended = QRectF(rounded).marginsRemoved(\n\t\t{ phalf, phalf, phalf, phalf });\n\tconst auto xradius = removed.left() + st::giftBoxGiftRadius - phalf;\n\tconst auto yradius = removed.top() + st::giftBoxGiftRadius - phalf;\n\tp.drawRoundedRect(extended, xradius, yradius);\n}\n\nvoid GiftButton::resizeEvent(QResizeEvent *e) {\n\tif (!_button.isEmpty()) {\n\t\t_button.moveLeft((width() - _button.width()) / 2);\n\t\tif (_stars) {\n\t\t\tconst auto padding = _button.height() / 2;\n\t\t\t_stars->setCenter(_button - QMargins(padding, 0, padding, 0));\n\t\t}\n\t}\n}\n\nvoid GiftButton::contextMenuEvent(QContextMenuEvent *e) {\n\t_contextMenuRequests.fire_copy((e->reason() == QContextMenuEvent::Mouse)\n\t\t? e->globalPos()\n\t\t: QCursor::pos());\n}\n\nvoid GiftButton::mousePressEvent(QMouseEvent *e) {\n\tif (_mouseEventsAreListening) {\n\t\tif (e->button() != Qt::LeftButton) {\n\t\t\treturn;\n\t\t}\n\t\t_mouseEvents.fire_copy(e);\n\t} else {\n\t\tAbstractButton::mousePressEvent(e);\n\t}\n}\n\nvoid GiftButton::mouseMoveEvent(QMouseEvent *e) {\n\tif (_mouseEventsAreListening) {\n\t\tif (e->button() != Qt::LeftButton) {\n\t\t\treturn;\n\t\t}\n\t\t_mouseEvents.fire_copy(e);\n\t} else {\n\t\tAbstractButton::mouseMoveEvent(e);\n\t}\n}\n\nvoid GiftButton::mouseReleaseEvent(QMouseEvent *e) {\n\tif (_mouseEventsAreListening) {\n\t\tif (e->button() != Qt::LeftButton) {\n\t\t\treturn;\n\t\t}\n\t\t_mouseEvents.fire_copy(e);\n\t} else {\n\t\tAbstractButton::mouseReleaseEvent(e);\n\t}\n}\n\nrpl::producer<QPoint> GiftButton::contextMenuRequests() const {\n\treturn _contextMenuRequests.events();\n}\n\nrpl::producer<QMouseEvent*> GiftButton::mouseEvents() {\n\t_mouseEventsAreListening = true;\n\treturn _mouseEvents.events();\n}\n\nbool GiftButton::makeCraftFrameIsFinal(\n\t\tQImage &frame,\n\t\tfloat64 progress) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (frame.size() != size() * ratio) {\n\t\tframe = QImage(size() * ratio, QImage::Format_ARGB32_Premultiplied);\n\t\tframe.setDevicePixelRatio(ratio);\n\t}\n\tif (progress < 1.) {\n\t\tframe.fill(Qt::transparent);\n\t}\n\tauto p = QPainter(&frame);\n\tpaint(p, progress);\n\treturn (progress == 1.)\n\t\t&& (!_uniquePatternEmoji || _uniquePatternEmoji->ready())\n\t\t&& (!_player || (_player->ready() && _playerFinished));\n}\n\nvoid GiftButton::cacheUniqueBackground(\n\t\tnot_null<Data::UniqueGift*> unique,\n\t\tint width,\n\t\tint height) {\n\tif (!_uniquePatternEmoji) {\n\t\t_uniquePatternEmoji = _delegate->buttonPatternEmoji(unique, [=] {\n\t\t\tupdate();\n\t\t});\n\t\t[[maybe_unused]] const auto preload = _uniquePatternEmoji->ready();\n\t}\n\tconst auto outer = QRect(0, 0, width, height);\n\tconst auto extend = currentExtend();\n\tconst auto inner = outer.marginsRemoved(\n\t\textend\n\t).translated(-extend.left(), -extend.top());\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (_uniqueBackgroundCache.size() != inner.size() * ratio) {\n\t\t_uniqueBackgroundCache = QImage(\n\t\t\tinner.size() * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t_uniqueBackgroundCache.fill(Qt::transparent);\n\t\t_uniqueBackgroundCache.setDevicePixelRatio(ratio);\n\n\t\tconst auto radius = st::giftBoxGiftRadius;\n\t\tauto p = QPainter(&_uniqueBackgroundCache);\n\t\tpaintUniqueBackgroundGradient(p, unique, inner, radius);\n\t\t_patterned = false;\n\t}\n\tif (!_patterned && _uniquePatternEmoji->ready()) {\n\t\t_patterned = true;\n\t\tauto p = QPainter(&_uniqueBackgroundCache);\n\t\tpaintUniqueBackgroundPattern(p, unique, inner);\n\t}\n}\n\nvoid GiftButton::paintUniqueBackgroundGradient(\n\t\tQPainter &p,\n\t\tnot_null<Data::UniqueGift*> unique,\n\t\tQRect inner,\n\t\tfloat64 radius) {\n\tauto hq = PainterHighQualityEnabler(p);\n\tauto gradient = QRadialGradient(inner.center(), inner.width() / 2);\n\tgradient.setStops({\n\t\t{ 0., unique->backdrop.centerColor },\n\t\t{ 1., unique->backdrop.edgeColor },\n\t});\n\tp.setBrush(gradient);\n\tp.setPen(Qt::NoPen);\n\tp.drawRoundedRect(inner, radius, radius);\n}\n\nvoid GiftButton::paintUniqueBackgroundPattern(\n\t\tQPainter &p,\n\t\tnot_null<Data::UniqueGift*> unique,\n\t\tQRect inner) {\n\tp.setClipRect(inner);\n\tconst auto skip = inner.width() / 3;\n\tUi::PaintBgPoints(\n\t\tp,\n\t\tUi::PatternBgPointsSmall(),\n\t\t_uniquePatternCache,\n\t\t_uniquePatternEmoji.get(),\n\t\t*unique,\n\t\tQRect(\n\t\t\tinner.x() - skip,\n\t\t\tinner.y(),\n\t\t\tinner.width() + 2 * skip,\n\t\t\tinner.height()));\n}\n\nvoid GiftButton::paintEvent(QPaintEvent *e) {\n\tconst auto canCraftAt = [&] {\n\t\tif (_mode != Mode::Craft) {\n\t\t\treturn 0;\n\t\t}\n\t\tconst auto stargift = std::get_if<GiftTypeStars>(&_descriptor);\n\t\tconst auto unique = stargift ? stargift->info.unique.get() : nullptr;\n\t\treturn unique ? unique->canCraftAt : TimeId();\n\t}();\n\n\tauto p = QPainter(this);\n\tif (!canCraftAt || base::unixtime::now() >= canCraftAt) {\n\t\tpaint(p);\n\t\treturn;\n\t}\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto w = width() * ratio;\n\tconst auto h = height() * ratio;\n\tauto cache = _delegate->craftUnavailableFrameCache(this, canCraftAt);\n\tif (cache.width() < w || cache.height() < h) {\n\t\tcache = QImage(\n\t\t\tstd::max(w, cache.width()),\n\t\t\tstd::max(h, cache.height()),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tcache.setDevicePixelRatio(ratio);\n\t}\n\tcache.fill(Qt::transparent);\n\tauto q = QPainter(&cache);\n\tpaint(q);\n\tq.end();\n\n\tp.setOpacity(kCraftUnavailableOpacity);\n\tp.drawImage(rect(), cache, QRect(0, 0, w, h));\n}\n\nvoid GiftButton::paint(QPainter &p, float64 craftProgress) {\n\tconst auto stargift = std::get_if<GiftTypeStars>(&_descriptor);\n\tconst auto unique = stargift ? stargift->info.unique.get() : nullptr;\n\tconst auto onsale = unique && unique->starsForResale && small();\n\tconst auto requirePremium = stargift\n\t\t&& !stargift->userpic\n\t\t&& !stargift->resale\n\t\t&& !stargift->info.unique\n\t\t&& stargift->info.requirePremium;\n\tconst auto auction = stargift\n\t\t&& !stargift->userpic\n\t\t&& !stargift->resale\n\t\t&& !stargift->info.unique\n\t\t&& stargift->info.auction();\n\tconst auto hidden = stargift && stargift->hidden;\n\tconst auto soldOut = stargift\n\t\t&& !(stargift->pinned || stargift->pinnedSelection)\n\t\t&& !unique\n\t\t&& !stargift->userpic\n\t\t&& stargift->info.limitedCount\n\t\t&& !stargift->info.limitedLeft;\n\tconst auto extend = currentExtend();\n\tconst auto position = QPoint(extend.left(), extend.top());\n\tconst auto background = _delegate->background();\n\tconst auto dpr = int(background.devicePixelRatio());\n\tconst auto width = this->width();\n\tconst auto height = background.height() / dpr;\n\tconst auto roundedRatio = (1. - craftProgress);\n\tif (craftProgress == 0.) {\n\t\tpaintBackground(p, background);\n\t} else if (craftProgress < 1.) {\n\t\tp.setOpacity(roundedRatio);\n\t\tpaintBackground(p, background);\n\t\tp.setOpacity(1.);\n\t}\n\n\tif (unique) {\n\t\tif (craftProgress > 0.) {\n\t\t\tconst auto outer = QRect(0, 0, width, height);\n\t\t\tconst auto inner = outer.marginsRemoved(extend * roundedRatio);\n\t\t\tpaintUniqueBackgroundGradient(\n\t\t\t\tp,\n\t\t\t\tunique,\n\t\t\t\tinner,\n\t\t\t\tint(base::SafeRound(st::giftBoxGiftRadius * roundedRatio)));\n\n\t\t\tif (_uniquePatternEmoji->ready()) {\n\t\t\t\tpaintUniqueBackgroundPattern(p, unique, inner);\n\t\t\t\tp.setClipping(false);\n\t\t\t}\n\t\t} else {\n\t\t\tcacheUniqueBackground(unique, width, height);\n\t\t\tp.drawImage(extend.left(), extend.top(), _uniqueBackgroundCache);\n\t\t}\n\t} else if (requirePremium || auction) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tauto pen = st::creditsFg->p;\n\t\tpen.setWidth(style::ConvertScaleExact(2.));\n\t\tp.setPen(pen);\n\t\tp.setBrush(Qt::NoBrush);\n\t\tconst auto outer = QRect(0, 0, width, background.height() / dpr);\n\t\tconst auto extend = currentExtend();\n\t\tconst auto radius = st::giftBoxGiftRadius;\n\t\tp.drawRoundedRect(outer.marginsRemoved(extend), radius, radius);\n\t}\n\tauto inset = 0;\n\tif (_selectionMode == GiftSelectionMode::Inset) {\n\t\tconst auto progress = _selectedAnimation.value(_selected ? 1. : 0.);\n\t\tif (progress > 0) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tauto pen = st::boxBg->p;\n\t\t\tconst auto thickness = style::ConvertScaleExact(2.);\n\t\t\tpen.setWidthF(progress * thickness);\n\t\t\tp.setPen(pen);\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\tconst auto outer = QRectF(0, 0, width, height);\n\t\t\tconst auto shift = progress * thickness * 2;\n\t\t\tconst auto extend = QMarginsF(currentExtend())\n\t\t\t\t+ QMarginsF(shift, shift, shift, shift);\n\t\t\tconst auto radius = st::giftBoxGiftRadius - shift;\n\t\t\tp.drawRoundedRect(outer.marginsRemoved(extend), radius, radius);\n\t\t\tinset = int(std::ceil(\n\t\t\t\tprogress * (thickness * 2 + st::giftBoxUserpicSkip)));\n\t\t}\n\t}\n\tif (_locked && !soldOut) {\n\t\tst::giftBoxLockIcon.paint(\n\t\t\tp,\n\t\t\tposition + st::giftBoxLockIconPosition,\n\t\t\twidth);\n\t}\n\n\tif (_userpic) {\n\t\tif (!_subscribed) {\n\t\t\t_subscribed = true;\n\t\t\t_userpic->subscribeToUpdates([=] { update(); });\n\t\t}\n\t\tconst auto image = _userpic->image(st::giftBoxUserpicSize);\n\t\tconst auto skip = st::giftBoxUserpicSkip;\n\t\tp.drawImage(extend.left() + skip, extend.top() + skip, image);\n\t} else if (_check) {\n\t\tconst auto skip = st::giftBoxUserpicSkip;\n\t\t_check->paint(\n\t\t\tp,\n\t\t\tQPoint(extend.left() + skip, extend.top() + skip),\n\t\t\twidth,\n\t\t\t_selected,\n\t\t\ttrue);\n\t}\n\n\tauto frame = QImage();\n\tif (_player && _player->ready()) {\n\t\tconst auto paused = !isOver() || isHidden();\n\t\tauto info = _player->frame(\n\t\t\tstickerSize(),\n\t\t\tQColor(0, 0, 0, 0),\n\t\t\tfalse,\n\t\t\tcrl::now(),\n\t\t\tpaused);\n\t\tframe = info.image;\n\t\t_playerFinished = (info.index + 1 == _player->framesCount());\n\t\tif (!_playerFinished || !paused) {\n\t\t\t_player->markFrameShown();\n\t\t}\n\t\tconst auto size = frame.size() / style::DevicePixelRatio();\n\t\tp.drawImage(\n\t\t\tQRect(\n\t\t\t\t(width - size.width()) / 2,\n\t\t\t\t((_mode == Mode::CraftPreview\n\t\t\t\t\t|| _mode == Mode::Minimal\n\t\t\t\t\t|| _mode == Mode::Craft)\n\t\t\t\t\t? (extend.top()\n\t\t\t\t\t\t+ ((height\n\t\t\t\t\t\t\t- extend.top()\n\t\t\t\t\t\t\t- extend.bottom()\n\t\t\t\t\t\t\t- size.height()) / 2))\n\t\t\t\t\t: small()\n\t\t\t\t\t? st::giftBoxSmallStickerTop\n\t\t\t\t\t: _text.isEmpty()\n\t\t\t\t\t? (unique\n\t\t\t\t\t\t? st::giftBoxStickerUniqueTop\n\t\t\t\t\t\t: st::giftBoxStickerStarTop)\n\t\t\t\t\t: _byStars.isEmpty()\n\t\t\t\t\t? st::giftBoxStickerTop\n\t\t\t\t\t: st::giftBoxStickerTopByStars),\n\t\t\t\tsize.width(),\n\t\t\t\tsize.height()),\n\t\t\tframe);\n\t}\n\tif (hidden) {\n\t\tconst auto topleft = QPoint(\n\t\t\t(width - stickerSize().width()) / 2,\n\t\t\t(small()\n\t\t\t\t? st::giftBoxSmallStickerTop\n\t\t\t\t: _text.isEmpty()\n\t\t\t\t? (unique\n\t\t\t\t\t? st::giftBoxStickerUniqueTop\n\t\t\t\t\t: st::giftBoxStickerStarTop)\n\t\t\t\t: _byStars.isEmpty()\n\t\t\t\t? st::giftBoxStickerTop\n\t\t\t\t: st::giftBoxStickerTopByStars));\n\t\t_delegate->hiddenMark()->paint(\n\t\t\tp,\n\t\t\tframe,\n\t\t\t_hiddenBgCache,\n\t\t\ttopleft,\n\t\t\tstickerSize(),\n\t\t\twidth);\n\t}\n\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto premium = v::is<GiftTypePremium>(_descriptor);\n\tconst auto singlew = width - extend.left() - extend.right();\n\tconst auto font = st::semiboldFont;\n\tp.setFont(font);\n\n\tconst auto badge = v::match(_descriptor, [&](GiftTypePremium data) {\n\t\tif (data.discountPercent > 0) {\n\t\t\tp.setBrush(st::attentionButtonFg);\n\t\t\tconst auto kMinus = QChar(0x2212);\n\t\t\treturn GiftBadge{\n\t\t\t\t.text = kMinus + QString::number(data.discountPercent) + '%',\n\t\t\t\t.bg1 = st::premiumButtonBg3->c,\n\t\t\t\t.bg2 = st::premiumButtonBg2->c,\n\t\t\t\t.fg = st::windowBg->c,\n\t\t\t\t.gradient = true,\n\t\t\t\t.small = true,\n\t\t\t};\n\t\t}\n\t\treturn GiftBadge();\n\t}, [&](const GiftTypeStars &data) {\n\t\tconst auto count = data.info.limitedCount;\n\t\tconst auto pinned = data.pinned || data.pinnedSelection;\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto upcomingAuction = (data.info.auctionStartDate > 0)\n\t\t\t&& (data.info.auctionStartDate > now);\n\t\tif (count || pinned) {\n\t\t\tconst auto yourLeft = data.info.perUserTotal\n\t\t\t\t? (data.info.perUserRemains\n\t\t\t\t\t? tr::lng_gift_stars_your_left(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tdata.info.perUserRemains)\n\t\t\t\t\t: tr::lng_gift_stars_your_finished(tr::now))\n\t\t\t\t: QString();\n\t\t\treturn GiftBadge{\n\t\t\t\t.text = (onsale\n\t\t\t\t\t? tr::lng_gift_stars_on_sale(tr::now)\n\t\t\t\t\t: (unique && (data.resale || pinned || data.mine))\n\t\t\t\t\t? ('#' + Lang::FormatCountDecimal(unique->number))\n\t\t\t\t\t: data.resale\n\t\t\t\t\t? tr::lng_gift_stars_resale(tr::now)\n\t\t\t\t\t: soldOut\n\t\t\t\t\t? tr::lng_gift_stars_sold_out(tr::now)\n\t\t\t\t\t: (!unique && data.info.auction())\n\t\t\t\t\t? (upcomingAuction\n\t\t\t\t\t\t? tr::lng_gift_stars_auction_soon\n\t\t\t\t\t\t: tr::lng_gift_stars_auction)(tr::now)\n\t\t\t\t\t: (!data.userpic\n\t\t\t\t\t\t&& !data.info.unique\n\t\t\t\t\t\t&& data.info.requirePremium)\n\t\t\t\t\t? ((yourLeft.isEmpty() || !_delegate->amPremium())\n\t\t\t\t\t\t? tr::lng_gift_stars_premium(tr::now)\n\t\t\t\t\t\t: yourLeft)\n\t\t\t\t\t: (!data.userpic && !data.info.unique)\n\t\t\t\t\t? (yourLeft.isEmpty()\n\t\t\t\t\t\t? tr::lng_gift_stars_limited(tr::now)\n\t\t\t\t\t\t: yourLeft)\n\t\t\t\t\t: (count == 1)\n\t\t\t\t\t? tr::lng_gift_limited_of_one(tr::now)\n\t\t\t\t\t: tr::lng_gift_limited_of_count(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_amount,\n\t\t\t\t\t\t(((count % 1000) && (count < 10'000))\n\t\t\t\t\t\t\t? Lang::FormatCountDecimal(count)\n\t\t\t\t\t\t\t: Lang::FormatCountToShort(count).string))),\n\t\t\t\t.bg1 = (onsale\n\t\t\t\t\t? st::boxTextFgGood->c\n\t\t\t\t\t: unique\n\t\t\t\t\t? unique->backdrop.edgeColor\n\t\t\t\t\t: data.resale\n\t\t\t\t\t? st::boxTextFgGood->c\n\t\t\t\t\t: soldOut\n\t\t\t\t\t? st::attentionButtonFg->c\n\t\t\t\t\t: (!data.userpic\n\t\t\t\t\t\t&& (data.info.auction() || data.info.requirePremium))\n\t\t\t\t\t? st::creditsFg->c\n\t\t\t\t\t: st::windowActiveTextFg->c),\n\t\t\t\t.bg2 = (onsale\n\t\t\t\t\t? QColor(0, 0, 0, 0)\n\t\t\t\t\t: unique\n\t\t\t\t\t? unique->backdrop.patternColor\n\t\t\t\t\t: QColor(0, 0, 0, 0)),\n\t\t\t\t.border = (onsale\n\t\t\t\t\t? QColor(255, 255, 255)\n\t\t\t\t\t: QColor(0, 0, 0, 0)),\n\t\t\t\t.fg = (onsale\n\t\t\t\t\t? st::windowBg->c\n\t\t\t\t\t: unique\n\t\t\t\t\t? QColor(255, 255, 255)\n\t\t\t\t\t: st::windowBg->c),\n\t\t\t\t.small = true,\n\t\t\t};\n\t\t}\n\t\treturn GiftBadge();\n\t});\n\n\tif (badge) {\n\t\tconst auto rubberOut = st::lineWidth;\n\t\tconst auto inner = rect().marginsRemoved(extend);\n\t\tp.setClipRect(inner.marginsAdded(\n\t\t\t{ rubberOut, rubberOut, rubberOut, rubberOut }));\n\n\t\tconst auto cached = _delegate->cachedBadge(badge);\n\t\tconst auto width = cached.width() / cached.devicePixelRatio();\n\t\tp.drawImage(\n\t\t\tposition.x() + singlew + rubberOut - width,\n\t\t\tposition.y() - rubberOut,\n\t\t\tcached);\n\t}\n\n\tauto percentSkip = 0;\n\tv::match(_descriptor, [](const GiftTypePremium &) {\n\t}, [&](const GiftTypeStars &data) {\n\t\tif (!unique || _mode == Mode::Craft || _mode == Mode::CraftPreview) {\n\t\t} else if (data.pinned && _mode != Mode::Selection) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tconst auto &icon = st::giftBoxPinIcon;\n\t\t\tconst auto skip = st::giftBoxUserpicSkip;\n\t\t\tconst auto add = (st::giftBoxUserpicSize - icon.width()) / 2;\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(unique->backdrop.patternColor);\n\t\t\tconst auto rect = QRect(\n\t\t\t\tQPoint(extend.left() + skip, extend.top() + skip),\n\t\t\t\tQSize(icon.width() + 2 * add, icon.height() + 2 * add));\n\t\t\tp.drawEllipse(rect);\n\t\t\ticon.paintInCenter(p, rect);\n\t\t} else if (!data.forceTon\n\t\t\t&& unique->nanoTonForResale\n\t\t\t&& unique->onlyAcceptTon) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tif (_tonIcon.isNull()) {\n\t\t\t\t_tonIcon = st::tonIconEmojiLarge.icon.instance(\n\t\t\t\t\tQColor(255, 255, 255));\n\t\t\t}\n\t\t\tconst auto size = _tonIcon.size() / _tonIcon.devicePixelRatio();\n\t\t\tconst auto skip = st::giftBoxUserpicSkip + inset;\n\t\t\tconst auto add = (st::giftBoxUserpicSize - size.width()) / 2;\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(unique->backdrop.patternColor);\n\t\t\tconst auto rect = QRect(\n\t\t\t\tQPoint(extend.left() + skip, extend.top() + skip),\n\t\t\t\tQSize(size.width() + 2 * add, size.height() + 2 * add));\n\t\t\tp.drawEllipse(rect);\n\t\t\tp.drawImage(\n\t\t\t\textend.left() + skip + add,\n\t\t\t\textend.top() + skip + add,\n\t\t\t\t_tonIcon);\n\t\t\tpercentSkip += st::giftBoxUserpicSize;\n\t\t}\n\t});\n\n\tif (unique\n\t\t&& unique->craftChancePermille > 0\n\t\t&& (_mode == Mode::Craft || _mode == Mode::CraftResale)) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(unique->backdrop.patternColor);\n\n\t\tconst auto rounded = (unique->craftChancePermille + 5) / 10;\n\t\tconst auto percent = QString::number(rounded) + '%';\n\t\tconst auto &font = st::giftBoxGiftBadgeFont;\n\t\tconst auto height = font->height + st::lineWidth;\n\t\tconst auto radius = height / 2.;\n\t\tconst auto skip = st::giftBoxUserpicSkip\n\t\t\t+ (_selected ? inset : st::giftBoxUserpicSkip);\n\t\tconst auto space = font->spacew;\n\t\tconst auto x = extend.left() + skip + percentSkip;\n\t\tconst auto y = extend.top() + skip;\n\t\tconst auto width = font->width(percent) + 2 * space;\n\t\tp.drawRoundedRect(x, y, width, height, radius, radius);\n\n\t\tp.setPen(st::white);\n\t\tp.setFont(font);\n\t\tp.drawText(x + space, y + font->ascent, percent);\n\t}\n\n\tif (!_button.isEmpty()) {\n\t\tp.setBrush(onsale\n\t\t\t? QBrush(unique->backdrop.patternColor)\n\t\t\t: unique\n\t\t\t? QBrush(QColor(255, 255, 255, .2 * 255))\n\t\t\t: premium\n\t\t\t? st::lightButtonBgOver\n\t\t\t: st::creditsBg3);\n\t\tp.setPen(Qt::NoPen);\n\t\tif (!unique && !premium) {\n\t\t\tp.setOpacity(0.12);\n\t\t} else if (onsale) {\n\t\t\tp.setOpacity(0.8);\n\t\t}\n\t\tconst auto geometry = _button;\n\t\tconst auto radius = geometry.height() / 2.;\n\t\tp.drawRoundedRect(geometry, radius, radius);\n\t\tif (!premium || onsale) {\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t\tif (_stars) {\n\t\t\tif (unique) {\n\t\t\t\t_stars->paint(p);\n\t\t\t} else {\n\t\t\t\tauto clipPath = QPainterPath();\n\t\t\t\tclipPath.addRoundedRect(geometry, radius, radius);\n\t\t\t\tp.setClipPath(clipPath);\n\t\t\t\t_stars->paint(p);\n\t\t\t\tp.setClipping(false);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!_text.isEmpty()) {\n\t\tp.setPen(st::windowFg);\n\t\t_text.draw(p, {\n\t\t\t.position = (position + QPoint(0, _byStars.isEmpty()\n\t\t\t\t? st::giftBoxPremiumTextTop\n\t\t\t\t: st::giftBoxPremiumTextTopByStars)),\n\t\t\t.availableWidth = singlew,\n\t\t\t.align = style::al_top,\n\t\t});\n\t}\n\n\tif (!_button.isEmpty()) {\n\t\tconst auto padding = st::giftBoxButtonPadding;\n\t\tp.setPen(unique\n\t\t\t? QPen(QColor(255, 255, 255))\n\t\t\t: premium\n\t\t\t? st::windowActiveTextFg\n\t\t\t: st::creditsFg);\n\t\t_price.draw(p, {\n\t\t\t.position = (_button.topLeft()\n\t\t\t\t+ QPoint(padding.left(), padding.top())),\n\t\t\t.availableWidth = _price.maxWidth(),\n\t\t});\n\n\t\tif (!_byStars.isEmpty()) {\n\t\t\tp.setPen(st::creditsFg);\n\t\t\t_byStars.draw(p, {\n\t\t\t\t.position = QPoint(\n\t\t\t\t\tposition.x(),\n\t\t\t\t\t_button.y() + _button.height() + st::giftBoxByStarsSkip),\n\t\t\t\t.availableWidth = singlew,\n\t\t\t\t.align = style::al_top,\n\t\t\t});\n\t\t}\n\t}\n}\n\nQSize GiftButton::stickerSize() const {\n\treturn (_mode == Mode::CraftPreview)\n\t\t? st::giftBoxStickerTiny\n\t\t: st::giftBoxStickerSize;\n}\n\nDelegate::Delegate(not_null<Main::Session*> session, GiftButtonMode mode)\n: _session(session)\n, _hiddenMark(std::make_unique<StickerPremiumMark>(\n\t_session,\n\tst::giftBoxHiddenMark,\n\tRectPart::Center))\n, _mode(mode) {\n\t_ministarEmoji = _emojiHelper.paletteDependent(\n\t\tUi::Earn::IconCreditsEmojiSmall());\n\t_starEmoji = _emojiHelper.paletteDependent(\n\t\tUi::Earn::IconCreditsEmoji());\n}\n\nDelegate::Delegate(Delegate &&other) = default;\n\nDelegate::~Delegate() = default;\n\nTextWithEntities Delegate::star() {\n\treturn _starEmoji;\n}\n\nTextWithEntities Delegate::monostar() {\n\treturn Ui::Text::IconEmoji(&st::starIconEmoji);\n}\n\nTextWithEntities Delegate::monoton() {\n\treturn Ui::Text::IconEmoji(&st::tonIconEmoji);\n}\n\nTextWithEntities Delegate::ministar() {\n\treturn _ministarEmoji;\n}\n\nUi::Text::MarkedContext Delegate::textContext() {\n\treturn _emojiHelper.context();\n}\n\nQSize Delegate::buttonSize() {\n\tif (!_single.isEmpty()) {\n\t\treturn _single;\n\t}\n\tconst auto width = st::boxWideWidth;\n\tconst auto padding = st::giftBoxPadding;\n\tconst auto available = width - padding.left() - padding.right();\n\tconst auto singlew = (available - 2 * st::giftBoxGiftSkip.x())\n\t\t/ kGiftsPerRow;\n\tconst auto minimal = (_mode != GiftButtonMode::Full)\n\t\t&& (_mode != GiftButtonMode::CraftResale);\n\tconst auto tiny = (_mode == GiftButtonMode::CraftPreview);\n\t_single = QSize(\n\t\ttiny ? st::giftBoxGiftTiny : singlew,\n\t\t(tiny\n\t\t\t? st::giftBoxGiftTiny\n\t\t\t: minimal\n\t\t\t? st::giftBoxGiftSmall\n\t\t\t: st::giftBoxGiftHeight));\n\treturn _single;\n}\n\nQMargins Delegate::buttonExtend() const {\n\treturn st::defaultDropdownMenu.wrap.shadow.extend;\n}\n\nauto Delegate::buttonPatternEmoji(\n\tnot_null<Data::UniqueGift*> unique,\n\tFn<void()> repaint)\n-> std::unique_ptr<Ui::Text::CustomEmoji> {\n\treturn _session->data().customEmojiManager().create(\n\t\tunique->pattern.document,\n\t\trepaint,\n\t\tData::CustomEmojiSizeTag::Large);\n}\n\nQImage Delegate::background() {\n\tif (!_bg.isNull()) {\n\t\treturn _bg;\n\t}\n\tconst auto single = buttonSize();\n\tconst auto extend = buttonExtend();\n\tconst auto bgSize = single.grownBy(extend);\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto bg = QImage(\n\t\tbgSize * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tbg.setDevicePixelRatio(ratio);\n\tbg.fill(Qt::transparent);\n\n\tconst auto radius = st::giftBoxGiftRadius;\n\tconst auto rect = QRect(QPoint(), bgSize).marginsRemoved(extend);\n\n\t{\n\t\tauto p = QPainter(&bg);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setOpacity(0.3);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::windowShadowFg);\n\t\tp.drawRoundedRect(\n\t\t\tQRectF(rect).translated(0, radius / 12.),\n\t\t\tradius,\n\t\t\tradius);\n\t}\n\tbg = bg.scaled(\n\t\t(bgSize * ratio) / 2,\n\t\tQt::IgnoreAspectRatio,\n\t\tQt::SmoothTransformation);\n\tbg = Images::Blur(std::move(bg), true);\n\tbg = bg.scaled(\n\t\tbgSize * ratio,\n\t\tQt::IgnoreAspectRatio,\n\t\tQt::SmoothTransformation);\n\t{\n\t\tauto p = QPainter(&bg);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::windowBg);\n\t\tp.drawRoundedRect(rect, radius, radius);\n\t}\n\n\t_bg = std::move(bg);\n\treturn _bg;\n}\n\nrpl::producer<not_null<DocumentData*>> Delegate::sticker(\n\t\tconst GiftDescriptor &descriptor) {\n\treturn GiftStickerValue(_session, descriptor);\n}\n\nnot_null<StickerPremiumMark*> Delegate::hiddenMark() {\n\treturn _hiddenMark.get();\n}\n\nQImage Delegate::cachedBadge(const GiftBadge &badge) {\n\tauto &image = _badges[badge];\n\tif (image.isNull()) {\n\t\timage = ValidateRotatedBadge(badge, QMargins());\n\t}\n\treturn image;\n}\n\nbool Delegate::amPremium() {\n\treturn _session->premium();\n}\n\nvoid Delegate::invalidateCache() {\n\t_bg = QImage();\n\t_badges.clear();\n}\n\nQImage &Delegate::craftUnavailableFrameCache(\n\t\tnot_null<GiftButton*> button,\n\t\tTimeId until) {\n\tconst auto weak = QPointer<QWidget>(button.get());\n\tif (!ranges::contains(_craftUnavailables, weak)) {\n\t\t_craftUnavailables.push_back(weak);\n\t}\n\tif (!_craftUnavailableTimer) {\n\t\t_craftUnavailableTimer = std::make_unique<base::Timer>(\n\t\t\t[=] { updateCraftUnavailables(); });\n\t}\n\tif (!_craftUnavailableTimer->isActive()\n\t\t|| _craftUnavailableUntil > until) {\n\t\t_craftUnavailableUntil = until;\n\t\tconst auto left = until - base::unixtime::now();\n\t\t_craftUnavailableTimer->callOnce(\n\t\t\tstd::clamp(left, 1, 86400) * crl::time(1000));\n\t}\n\treturn _craftUnavailableFrameCache;\n}\n\nvoid Delegate::updateCraftUnavailables() {\n\tExpects(_craftUnavailableTimer != nullptr);\n\n\t_craftUnavailableTimer->cancel();\n\t_craftUnavailableUntil = 0;\n\n\tfor (auto i = begin(_craftUnavailables); i != end(_craftUnavailables);) {\n\t\tif (const auto raw = i->data()) {\n\t\t\traw->update();\n\t\t\t++i;\n\t\t} else {\n\t\t\ti = _craftUnavailables.erase(i);\n\t\t}\n\t}\n}\n\nDocumentData *LookupGiftSticker(\n\t\tnot_null<Main::Session*> session,\n\t\tconst GiftDescriptor &descriptor) {\n\treturn v::match(descriptor, [&](GiftTypePremium data) {\n\t\tauto &packs = session->giftBoxStickersPacks();\n\t\tpacks.load();\n\t\treturn packs.lookup(data.months);\n\t}, [&](GiftTypeStars data) {\n\t\treturn data.info.document.get();\n\t});\n}\n\nrpl::producer<not_null<DocumentData*>> GiftStickerValue(\n\t\tnot_null<Main::Session*> session,\n\t\tconst GiftDescriptor &descriptor) {\n\treturn v::match(descriptor, [&](GiftTypePremium data) {\n\t\tconst auto months = data.months;\n\t\tauto &packs = session->giftBoxStickersPacks();\n\t\tpacks.load();\n\t\tif (const auto result = packs.lookup(months)) {\n\t\t\treturn result->sticker()\n\t\t\t\t? (rpl::single(not_null(result)) | rpl::type_erased)\n\t\t\t\t: rpl::never<not_null<DocumentData*>>();\n\t\t}\n\t\treturn packs.updated(\n\t\t) | rpl::map([=] {\n\t\t\treturn session->giftBoxStickersPacks().lookup(data.months);\n\t\t}) | rpl::filter([](DocumentData *document) {\n\t\t\treturn document && document->sticker();\n\t\t}) | rpl::take(1) | rpl::map([=](DocumentData *document) {\n\t\t\treturn not_null(document);\n\t\t}) | rpl::type_erased;\n\t}, [&](GiftTypeStars data) {\n\t\treturn rpl::single(data.info.document) | rpl::type_erased;\n\t});\n}\n\nQImage ValidateRotatedBadge(\n\t\tconst GiftBadge &badge,\n\t\tQMargins padding,\n\t\tbool left) {\n\tconst auto &font = badge.small\n\t\t? st::giftBoxGiftBadgeFont\n\t\t: st::msgServiceGiftBoxBadgeFont;\n\tconst auto twidth = font->width(badge.text)\n\t\t+ padding.left()\n\t\t+ padding.right();\n\tconst auto skip = int(std::ceil(twidth / M_SQRT2));\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto multiplier = ratio * 3;\n\tconst auto size = (twidth + font->height * 2);\n\tconst auto height = padding.top() + font->height + padding.bottom();\n\tconst auto angle = left ? -45. : 45.;\n\tconst auto textpos = left\n\t\t? QPoint(padding.top(), size - skip)\n\t\t: QPoint(size - skip, padding.top());\n\tauto image = QImage(\n\t\tQSize(size, size) * multiplier,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.fill(Qt::transparent);\n\timage.setDevicePixelRatio(multiplier);\n\t{\n\t\tauto p = QPainter(&image);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.translate(textpos);\n\t\tp.rotate(angle);\n\t\tp.setFont(font);\n\t\tp.setPen(badge.fg);\n\t\tp.drawText(\n\t\t\tQPoint(padding.left(), padding.top() + font->ascent),\n\t\t\tbadge.text);\n\t}\n\n\tauto scaled = image.scaled(\n\t\tQSize(size, size) * ratio,\n\t\tQt::IgnoreAspectRatio,\n\t\tQt::SmoothTransformation);\n\tscaled.setDevicePixelRatio(ratio);\n\n\tauto result = QImage(\n\t\tQSize(size, size) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\tresult.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&result);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tp.save();\n\t\tp.translate(textpos);\n\t\tp.rotate(angle);\n\t\tconst auto rect = QRect(-5 * twidth, 0, twidth * 12, height);\n\t\tif (badge.border.alpha() > 0) {\n\t\t\tp.setPen(badge.border);\n\t\t} else {\n\t\t\tp.setPen(Qt::NoPen);\n\t\t}\n\t\tif (badge.gradient) {\n\t\t\tconst auto skip = font->height / M_SQRT2;\n\t\t\tauto gradient = QLinearGradient(\n\t\t\t\tQPointF(-twidth - skip, 0),\n\t\t\t\tQPointF(twidth + skip, 0));\n\t\t\tgradient.setStops({\n\t\t\t\t{ 0., badge.bg1 },\n\t\t\t\t{ 1., badge.bg2 },\n\t\t\t});\n\t\t\tp.setBrush(gradient);\n\t\t\tp.drawRect(rect);\n\t\t} else {\n\t\t\tp.setBrush(badge.bg1);\n\t\t\tp.drawRect(rect);\n\t\t\tif (badge.bg2.alpha() > 0) {\n\t\t\t\tp.setOpacity(0.5);\n\t\t\t\tp.setBrush(badge.bg2);\n\t\t\t\tp.drawRect(rect);\n\t\t\t\tp.setOpacity(1.);\n\t\t\t}\n\t\t}\n\t\tp.restore();\n\n\t\tp.drawImage(0, 0, scaled);\n\t}\n\treturn result;\n}\n\nvoid SelectGiftToUnpin(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst std::vector<Data::CreditsHistoryEntry> &pinned,\n\t\tFn<void(Data::SavedStarGiftId)> chosen) {\n\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tstruct State {\n\t\t\texplicit State(not_null<Main::Session*> session)\n\t\t\t: delegate(session, GiftButtonMode::Minimal) {\n\t\t\t}\n\n\t\t\tDelegate delegate;\n\t\t\trpl::variable<int> selected = -1;\n\t\t\tstd::vector<not_null<GiftButton*>> buttons;\n\t\t};\n\t\tconst auto session = &show->session();\n\t\tconst auto state = box->lifetime().make_state<State>(session);\n\n\t\tbox->setStyle(st::giftTooManyPinnedBox);\n\t\tbox->setWidth(st::boxWideWidth);\n\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_gift_many_pinned_title(),\n\t\t\t\tst::giftBoxSubtitle),\n\t\t\tst::giftBoxSubtitleMargin,\n\t\t\tstyle::al_top);\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_gift_many_pinned_choose(),\n\t\t\t\tst::giftTooManyPinnedChoose),\n\t\t\tst::giftBoxAboutMargin,\n\t\t\tstyle::al_top);\n\n\t\tconst auto gifts = box->addRow(\n\t\t\tobject_ptr<Ui::RpWidget>(box),\n\t\t\tQMargins(\n\t\t\t\tst::giftBoxPadding.left(),\n\t\t\t\tst::giftTooManyPinnedBox.buttonPadding.top(),\n\t\t\t\tst::giftBoxPadding.right(),\n\t\t\t\t0));\n\t\tfor (const auto &entry : pinned) {\n\t\t\tconst auto index = int(state->buttons.size());\n\t\t\tstate->buttons.push_back(\n\t\t\t\tUi::CreateChild<GiftButton>(gifts, &state->delegate));\n\t\t\tconst auto button = state->buttons.back();\n\t\t\tbutton->setDescriptor(GiftTypeStars{\n\t\t\t\t.info = {\n\t\t\t\t\t.id = entry.stargiftId,\n\t\t\t\t\t.unique = entry.uniqueGift,\n\t\t\t\t\t.document = entry.uniqueGift->model.document,\n\t\t\t\t},\n\t\t\t\t.pinnedSelection = true,\n\t\t\t}, GiftButton::Mode::Minimal);\n\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\tconst auto now = state->selected.current();\n\t\t\t\tstate->selected = (now == index) ? -1 : index;\n\t\t\t});\n\t\t}\n\n\t\tstate->selected.value(\n\t\t) | rpl::combine_previous(\n\t\t) | rpl::on_next([=](int old, int now) {\n\t\t\tif (old >= 0) state->buttons[old]->toggleSelected(false);\n\t\t\tif (now >= 0) state->buttons[now]->toggleSelected(true);\n\t\t}, gifts->lifetime());\n\n\t\tgifts->widthValue() | rpl::on_next([=](int width) {\n\t\t\tconst auto singleMin = state->delegate.buttonSize();\n\t\t\tif (width < singleMin.width()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto count = int(state->buttons.size());\n\t\t\tconst auto skipw = st::giftBoxGiftSkip.x();\n\t\t\tconst auto skiph = st::giftBoxGiftSkip.y();\n\t\t\tconst auto perRow = std::min(\n\t\t\t\t(width + skipw) / (singleMin.width() + skipw),\n\t\t\t\tstd::max(count, 1));\n\t\t\tif (perRow <= 0) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto single = (width - (perRow - 1) * skipw) / perRow;\n\t\t\tconst auto height = singleMin.height();\n\t\t\tconst auto rows = (count + perRow - 1) / perRow;\n\t\t\tfor (auto row = 0; row != rows; ++row) {\n\t\t\t\tconst auto y = row * (height + skiph);\n\t\t\t\tfor (auto column = 0; column != perRow; ++column) {\n\t\t\t\t\tconst auto index = row * perRow + column;\n\t\t\t\t\tif (index >= count) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tconst auto &button = state->buttons[index];\n\t\t\t\t\tconst auto x = column * (single + skipw);\n\t\t\t\t\tbutton->setGeometry(\n\t\t\t\t\t\tQRect(x, y, single, height),\n\t\t\t\t\t\tstate->delegate.buttonExtend());\n\t\t\t\t}\n\t\t\t}\n\t\t\tgifts->resize(width, rows * (height + skiph) - skiph);\n\t\t}, gifts->lifetime());\n\n\t\tconst auto button = box->addButton(rpl::single(QString()), [=] {\n\t\t\tconst auto index = state->selected.current();\n\t\t\tif (index < 0) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tAssert(index < int(pinned.size()));\n\t\t\tconst auto &entry = pinned[index];\n\t\t\tconst auto weak = base::make_weak(box);\n\t\t\tchosen(::Settings::EntryToSavedStarGiftId(session, entry));\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t});\n\t\tconst auto label = Ui::SetButtonMarkedLabel(\n\t\t\tbutton,\n\t\t\ttr::lng_context_unpin_from_top(tr::marked),\n\t\t\t&show->session(),\n\t\t\tst::creditsBoxButtonLabel,\n\t\t\t&st::giftTooManyPinnedBox.button.textFg);\n\n\t\tstate->selected.value() | rpl::on_next([=](int value) {\n\t\t\tconst auto has = (value >= 0);\n\t\t\tlabel->setOpacity(has ? 1. : 0.5);\n\t\t\tbutton->setAttribute(Qt::WA_TransparentForMouseEvents, !has);\n\t\t}, box->lifetime());\n\n\t\tconst auto buttonPadding = st::giftTooManyPinnedBox.buttonPadding;\n\t\tconst auto buttonWidth = st::boxWideWidth\n\t\t\t- buttonPadding.left()\n\t\t\t- buttonPadding.right();\n\t\tbutton->resizeToWidth(buttonWidth);\n\t\tbutton->widthValue() | rpl::on_next([=](int width) {\n\t\t\tif (width != buttonWidth) {\n\t\t\t\tbutton->resizeToWidth(buttonWidth);\n\t\t\t}\n\t\t}, button->lifetime());\n\t}));\n}\n\nQColor BurnedBadgeBg() {\n\treturn QColor(0xd0, 0x3a, 0x3b);\n}\n\n} // namespace Info::PeerGifts\n"
  },
  {
    "path": "Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/qt/qt_compare.h\"\n#include \"base/timer.h\"\n#include \"data/data_star_gift.h\"\n#include \"ui/abstract_button.h\"\n#include \"ui/effects/premium_stars_colored.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/text.h\"\n\nclass StickerPremiumMark;\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nstruct UniqueGift;\nstruct CreditsHistoryEntry;\nclass SavedStarGiftId;\n} // namespace Data\n\nnamespace HistoryView {\nclass StickerPlayer;\n} // namespace HistoryView\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Overview::Layout {\nclass Checkbox;\n} // namespace Overview::Layout\n\nnamespace Ui {\nclass DynamicImage;\n} // namespace Ui\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Info::PeerGifts {\n\nstruct Tag {\n\texplicit Tag(not_null<PeerData*> peer, int collectionId = 0)\n\t: peer(peer)\n\t, collectionId(collectionId) {\n\t}\n\n\tnot_null<PeerData*> peer;\n\tint collectionId = 0;\n};\n\nstruct GiftTypePremium {\n\tint64 cost = 0;\n\tQString currency;\n\tint stars = 0;\n\tint months = 0;\n\tint discountPercent = 0;\n\n\t[[nodiscard]] friend inline bool operator==(\n\t\tconst GiftTypePremium &,\n\t\tconst GiftTypePremium &) = default;\n};\n\nstruct GiftTypeStars {\n\tData::SavedStarGiftId transferId;\n\tData::StarGift info;\n\tPeerData *from = nullptr;\n\tTimeId date = 0;\n\tbool pinnedSelection : 1 = false;\n\tbool forceTon : 1 = false;\n\tbool userpic : 1 = false;\n\tbool pinned : 1 = false;\n\tbool hidden : 1 = false;\n\tbool resale : 1 = false;\n\tbool mine : 1 = false;\n\n\t[[nodiscard]] friend inline bool operator==(\n\t\tconst GiftTypeStars&,\n\t\tconst GiftTypeStars&) = default;\n};\n\n[[nodiscard]] rpl::producer<std::vector<GiftTypeStars>> GiftsStars(\n\tnot_null<Main::Session*> session,\n\tnot_null<PeerData*> peer);\n\nstruct GiftDescriptor : std::variant<GiftTypePremium, GiftTypeStars> {\n\tusing variant::variant;\n\n\t[[nodiscard]] friend inline bool operator==(\n\t\tconst GiftDescriptor&,\n\t\tconst GiftDescriptor&) = default;\n};\n\nstruct GiftSendDetails {\n\tGiftDescriptor descriptor;\n\tTextWithEntities text;\n\tuint64 randomId = 0;\n\tbool anonymous = false;\n\tbool upgraded = false;\n\tbool byStars = false;\n};\n\nstruct GiftBadge {\n\tQString text;\n\tQColor bg1;\n\tQColor bg2 = QColor(0, 0, 0, 0);\n\tQColor border = QColor(0, 0, 0, 0);\n\tQColor fg;\n\tbool gradient = false;\n\tbool small = false;\n\n\texplicit operator bool() const {\n\t\treturn !text.isEmpty();\n\t}\n\n\tfriend std::strong_ordering operator<=>(\n\t\tconst GiftBadge &a,\n\t\tconst GiftBadge &b);\n\n\tfriend inline bool operator==(\n\t\tconst GiftBadge &,\n\t\tconst GiftBadge &) = default;\n};\n\nenum class GiftButtonMode : uint8 {\n\tFull,\n\tCraft,\n\tCraftResale,\n\tMinimal,\n\tSelection,\n\tCraftPreview,\n};\n\nenum class GiftSelectionMode : uint8 {\n\tBorder,\n\tInset,\n\tCheck,\n};\n\nclass GiftButton;\n\nclass GiftButtonDelegate {\npublic:\n\t[[nodiscard]] virtual TextWithEntities star() = 0;\n\t[[nodiscard]] virtual TextWithEntities monostar() = 0;\n\t[[nodiscard]] virtual TextWithEntities monoton() = 0;\n\t[[nodiscard]] virtual TextWithEntities ministar() = 0;\n\t[[nodiscard]] virtual Ui::Text::MarkedContext textContext() = 0;\n\t[[nodiscard]] virtual QSize buttonSize() = 0;\n\t[[nodiscard]] virtual QMargins buttonExtend() const = 0;\n\t[[nodiscard]] virtual auto buttonPatternEmoji(\n\t\tnot_null<Data::UniqueGift*> unique,\n\t\tFn<void()> repaint)\n\t-> std::unique_ptr<Ui::Text::CustomEmoji> = 0;\n\t[[nodiscard]] virtual QImage background() = 0;\n\t[[nodiscard]] virtual rpl::producer<not_null<DocumentData*>> sticker(\n\t\tconst GiftDescriptor &descriptor) = 0;\n\t[[nodiscard]] virtual not_null<StickerPremiumMark*> hiddenMark() = 0;\n\t[[nodiscard]] virtual QImage cachedBadge(const GiftBadge &badge) = 0;\n\t[[nodiscard]] virtual bool amPremium() = 0;\n\tvirtual void invalidateCache() = 0;\n\t[[nodiscard]] virtual QImage &craftUnavailableFrameCache(\n\t\tnot_null<GiftButton*> button,\n\t\tTimeId until) = 0;\n\n};\n\nclass GiftButton final : public Ui::AbstractButton {\npublic:\n\tGiftButton(QWidget *parent, not_null<GiftButtonDelegate*> delegate);\n\t~GiftButton();\n\n\tusing Mode = GiftButtonMode;\n\tvoid setDescriptor(const GiftDescriptor &descriptor, Mode mode);\n\tvoid setGeometry(QRect inner, QMargins extend);\n\n\tvoid toggleSelected(\n\t\tbool selected,\n\t\tGiftSelectionMode selectionMode = GiftSelectionMode::Border,\n\t\tanim::type animated = anim::type::normal);\n\n\t[[nodiscard]] rpl::producer<QPoint> contextMenuRequests() const;\n\t[[nodiscard]] rpl::producer<QMouseEvent*> mouseEvents();\n\n\t[[nodiscard]] bool makeCraftFrameIsFinal(\n\t\tQImage &frame,\n\t\tfloat64 progress);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\n\tvoid paint(QPainter &p, float64 craftProgress = 0.);\n\tvoid paintBackground(QPainter &p, const QImage &background);\n\tvoid cacheUniqueBackground(\n\t\tnot_null<Data::UniqueGift*> unique,\n\t\tint width,\n\t\tint height);\n\tvoid paintUniqueBackgroundGradient(\n\t\tQPainter &p,\n\t\tnot_null<Data::UniqueGift*> unique,\n\t\tQRect inner,\n\t\tfloat64 radius);\n\tvoid paintUniqueBackgroundPattern(\n\t\tQPainter &p,\n\t\tnot_null<Data::UniqueGift*> unique,\n\t\tQRect inner);\n\n\tvoid refreshLocked();\n\tvoid setDocument(not_null<DocumentData*> document);\n\t[[nodiscard]] QSize stickerSize() const;\n\t[[nodiscard]] QMargins currentExtend() const;\n\t[[nodiscard]] bool small() const;\n\n\tvoid onStateChanged(State was, StateChangeSource source) override;\n\tvoid unsubscribe();\n\n\tconst not_null<GiftButtonDelegate*> _delegate;\n\trpl::event_stream<QPoint> _contextMenuRequests;\n\trpl::event_stream<QMouseEvent*> _mouseEvents;\n\tQImage _hiddenBgCache;\n\tGiftDescriptor _descriptor;\n\tUi::Text::String _text;\n\tUi::Text::String _price;\n\tUi::Text::String _byStars;\n\tstd::shared_ptr<Ui::DynamicImage> _userpic;\n\tQImage _uniqueBackgroundCache;\n\tQImage _tonIcon;\n\tstd::unique_ptr<Ui::Text::CustomEmoji> _uniquePatternEmoji;\n\tbase::flat_map<float64, QImage> _uniquePatternCache;\n\tstd::optional<Ui::Premium::ColoredMiniStars> _stars;\n\tUi::Animations::Simple _selectedAnimation;\n\tstd::unique_ptr<Overview::Layout::Checkbox> _check;\n\tint _resalePrice = 0;\n\tGiftButtonMode _mode = GiftButtonMode::Full;\n\tGiftSelectionMode _selectionMode = GiftSelectionMode::Border;\n\tbool _subscribed : 1 = false;\n\tbool _patterned : 1 = false;\n\tbool _selected : 1 = false;\n\tbool _locked : 1 = false;\n\tbool _playerFinished : 1 = false;\n\n\tbool _mouseEventsAreListening = false;\n\n\tbase::Timer _lockedTimer;\n\tTimeId _lockedUntilDate = 0;\n\n\tQRect _button;\n\tQMargins _extend;\n\n\tDocumentData *_resolvedDocument = nullptr;\n\n\tstd::unique_ptr<HistoryView::StickerPlayer> _player;\n\tDocumentData *_playerDocument = nullptr;\n\trpl::lifetime _mediaLifetime;\n\trpl::lifetime _documentLifetime;\n\n};\n\nclass Delegate final : public GiftButtonDelegate {\npublic:\n\tDelegate(not_null<Main::Session*> session, GiftButtonMode mode);\n\tDelegate(Delegate &&other);\n\t~Delegate();\n\n\tTextWithEntities star() override;\n\tTextWithEntities monostar() override;\n\tTextWithEntities monoton() override;\n\tTextWithEntities ministar() override;\n\tUi::Text::MarkedContext textContext() override;\n\tQSize buttonSize() override;\n\tQMargins buttonExtend() const override;\n\tauto buttonPatternEmoji(\n\t\tnot_null<Data::UniqueGift*> unique,\n\t\tFn<void()> repaint)\n\t-> std::unique_ptr<Ui::Text::CustomEmoji> override;\n\tQImage background() override;\n\trpl::producer<not_null<DocumentData*>> sticker(\n\t\tconst GiftDescriptor &descriptor) override;\n\tnot_null<StickerPremiumMark*> hiddenMark() override;\n\tQImage cachedBadge(const GiftBadge &badge) override;\n\tbool amPremium() override;\n\tvoid invalidateCache() override;\n\tQImage &craftUnavailableFrameCache(\n\t\tnot_null<GiftButton*> button,\n\t\tTimeId until) override;\n\nprivate:\n\tvoid updateCraftUnavailables();\n\n\tconst not_null<Main::Session*> _session;\n\tstd::unique_ptr<StickerPremiumMark> _hiddenMark;\n\tbase::flat_map<GiftBadge, QImage> _badges;\n\tQSize _single;\n\tQImage _bg;\n\tGiftButtonMode _mode = GiftButtonMode::Full;\n\tUi::Text::CustomEmojiHelper\t_emojiHelper;\n\tTextWithEntities _ministarEmoji;\n\tTextWithEntities _starEmoji;\n\n\tQImage _craftUnavailableFrameCache;\n\tstd::vector<QPointer<QWidget>> _craftUnavailables;\n\tstd::unique_ptr<base::Timer> _craftUnavailableTimer;\n\tTimeId _craftUnavailableUntil = 0;\n\n};\n\n[[nodiscard]] DocumentData *LookupGiftSticker(\n\tnot_null<Main::Session*> session,\n\tconst GiftDescriptor &descriptor);\n\n[[nodiscard]] rpl::producer<not_null<DocumentData*>> GiftStickerValue(\n\tnot_null<Main::Session*> session,\n\tconst GiftDescriptor &descriptor);\n\n[[nodiscard]] QImage ValidateRotatedBadge(\n\tconst GiftBadge &badge,\n\tQMargins padding,\n\tbool left = false);\n\nvoid SelectGiftToUnpin(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst std::vector<Data::CreditsHistoryEntry> &pinned,\n\tFn<void(Data::SavedStarGiftId)> chosen);\n\n[[nodiscard]] QColor BurnedBadgeBg();\n\n} // namespace Info::PeerGifts\n"
  },
  {
    "path": "Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/peer_gifts/info_peer_gifts_widget.h\"\n\n#include \"api/api_credits.h\"\n#include \"api/api_hash.h\"\n#include \"api/api_premium.h\"\n#include \"apiwrap.h\"\n#include \"boxes/share_box.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"core/ui_integration.h\"\n#include \"data/components/recent_shared_media_gifts.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_credits.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"info/peer_gifts/info_peer_gifts_collections.h\"\n#include \"info/peer_gifts/info_peer_gifts_common.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/sub_tabs.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/box_content_divider.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/effects/animations.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/sender.h\"\n#include \"window/window_session_controller.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\" // boxRadius\n#include \"styles/style_media_player.h\" // mediaPlayerMenuCheck\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_credits.h\" // giftBoxPadding\n\n#include <QtWidgets/QApplication>\n\nnamespace Info::PeerGifts {\nnamespace {\n\nconstexpr auto kPreloadPages = 2;\nconstexpr auto kPerPage = 50;\nconstexpr auto kScrollFactor = 0.05;\n\n[[nodiscard]] GiftDescriptor DescriptorForGift(\n\t\tnot_null<PeerData*> to,\n\t\tconst Data::SavedStarGift &gift) {\n\treturn GiftTypeStars{\n\t\t.info = gift.info,\n\t\t.from = ((gift.anonymous || !gift.fromId)\n\t\t\t? nullptr\n\t\t\t: to->owner().peer(gift.fromId).get()),\n\t\t.date = gift.date,\n\t\t.userpic = !gift.info.unique,\n\t\t.pinned = gift.pinned,\n\t\t.hidden = gift.hidden,\n\t\t.mine = to->isSelf(),\n\t};\n}\n\n[[nodiscard]] Data::GiftCollection FromTL(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPStarGiftCollection &collection) {\n\tconst auto &data = collection.data();\n\treturn {\n\t\t.id = data.vcollection_id().v,\n\t\t.count = data.vgifts_count().v,\n\t\t.title = qs(data.vtitle()),\n\t\t.icon = (data.vicon()\n\t\t\t? session->data().processDocument(*data.vicon()).get()\n\t\t\t: nullptr),\n\t\t.hash = data.vhash().v,\n\t};\n}\n\n[[nodiscard]] std::vector<Data::GiftCollection> FromTL(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDpayments_starGiftCollections &data) {\n\tauto result = std::vector<Data::GiftCollection>();\n\n\tconst auto &list = data.vcollections().v;\n\tresult.reserve(list.size());\n\tfor (const auto &collection : list) {\n\t\tresult.push_back(FromTL(session, collection));\n\t}\n\treturn result;\n}\n\n} // namespace\n\nclass InnerWidget final : public Ui::BoxContentDivider {\npublic:\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<Descriptor> descriptor,\n\t\tUi::ScrollArea *scroll = nullptr);\n\n\t[[nodiscard]] not_null<PeerData*> peer() const {\n\t\treturn _peer;\n\t}\n\t[[nodiscard]] rpl::producer<bool> notifyEnabled() const {\n\t\treturn _notifyEnabled.events();\n\t}\n\t[[nodiscard]] rpl::producer<Descriptor> descriptorChanges() const {\n\t\treturn _descriptorChanges.events();\n\t}\n\t[[nodiscard]] rpl::producer<> scrollToTop() const {\n\t\treturn _scrollToTop.events();\n\t}\n\n\t[[nodiscard]] rpl::producer<Data::GiftsUpdate> changes() const {\n\t\treturn _collectionChanges.value();\n\t}\n\n\t[[nodiscard]] rpl::producer<bool> collectionEmptyValue() const {\n\t\treturn _collectionEmpty.value();\n\t}\n\n\tvoid reloadCollection(int id);\n\tvoid editCollectionGifts(int id);\n\tvoid shareCollectionLink(const QString &username, int id);\n\tvoid editCollectionName(int id);\n\tvoid confirmDeleteCollection(int id);\n\tvoid collectionAdded(MTPStarGiftCollection result);\n\tvoid fillMenu(const Ui::Menu::MenuCallback &addAction);\n\tvoid reorderCollections(const Ui::SubTabs::ReorderUpdate &update);\n\tvoid reorderCollectionsLocally(const Ui::SubTabs::ReorderUpdate &update);\n\tvoid flushCollectionReorder();\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\nprivate:\n\tstruct Entry {\n\t\tData::SavedStarGift gift;\n\t\tGiftDescriptor descriptor;\n\t};\n\tstruct Entries {\n\t\tstd::vector<Entry> list;\n\t\tstd::optional<Filter> filter;\n\t\tint total = 0;\n\t\tbool allLoaded = false;\n\t};\n\tstruct View {\n\t\tstd::unique_ptr<GiftButton> button;\n\t\tData::SavedStarGiftId manageId;\n\t\tuint64 giftId = 0;\n\t\tint index = 0;\n\t};\n\tstruct DragState {\n\t\tbool enabled = false;\n\t\tint index = -1;\n\t\tint lastSelected = -1;\n\t\tQPoint point;\n\t\tQPoint startPos;\n\t};\n\tstruct ShiftAnimation {\n\t\tUi::Animations::Simple xAnimation;\n\t\tUi::Animations::Simple yAnimation;\n\t\tint shift = 0;\n\t};\n\npublic:\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<Descriptor> descriptor,\n\t\tUi::ScrollArea *scroll,\n\t\tint addingToCollectionId,\n\t\tEntries all);\n\nprivate:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\n\tvoid subscribeToUpdates();\n\tvoid applyUpdateTo(Entries &entries, const Data::GiftUpdate &update);\n\tvoid switchTo(int collectionId);\n\tvoid loadCollections();\n\tvoid loadMore();\n\tvoid loaded(const MTPpayments_SavedStarGifts &result);\n\tvoid markInCollection(const Data::SavedStarGift &gift);\n\tvoid refreshButtons();\n\tvoid validateButtons();\n\t[[nodiscard]] std::unique_ptr<GiftButton> createGiftButton();\n\tvoid showGift(int index);\n\tvoid showMenuFor(not_null<GiftButton*> button, QPoint point);\n\tvoid showMenuForCollection(int id);\n\tvoid refreshAbout();\n\tvoid refreshCollectionsTabs();\n\n\tvoid updateSelected();\n\tint giftFromGlobalPos(const QPoint &p) const;\n\t[[nodiscard]] QPoint posFromIndex(int index) const;\n\t[[nodiscard]] bool isDraggedAnimating() const;\n\tvoid requestReorder(int fromIndex, int toIndex);\n\n\tvoid collectionRenamed(int id, QString name);\n\tvoid collectionRemoved(int id);\n\tvoid removeGiftFromCollection(\n\t\tData::SavedStarGiftId giftId,\n\t\tint collectionId);\n\tvoid addGiftToCollection(\n\t\tData::SavedStarGiftId giftId,\n\t\tint collectionId);\n\tvoid fillCollectionsMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst Data::SavedStarGift &gift);\n\n\tvoid markPinned(std::vector<Entry>::iterator i);\n\tvoid markUnpinned(std::vector<Entry>::iterator i);\n\n\tint resizeGetHeight(int width) override;\n\n\t[[nodiscard]] auto pinnedSavedGifts()\n\t\t-> Fn<std::vector<Data::CreditsHistoryEntry>()>;\n\n\tconst not_null<Window::SessionController*> _window;\n\tconst not_null<PeerData*> _peer;\n\tUi::ScrollArea * const _scroll;\n\tconst int _addingToCollectionId = 0;\n\tconst GiftButtonMode _mode;\n\n\trpl::variable<Descriptor> _descriptor;\n\tDelegate _delegate;\n\tstd::unique_ptr<Ui::SubTabs> _collectionsTabs;\n\tstd::unique_ptr<Ui::RpWidget> _about;\n\trpl::event_stream<> _scrollToTop;\n\trpl::variable<bool> _collectionEmpty;\n\tbool _pendingCollectionReorder = false;\n\n\tstd::vector<Data::GiftCollection> _collections;\n\n\tEntries _all;\n\tstd::map<int, Entries> _perCollection;\n\tnot_null<Entries*> _entries;\n\tnot_null<std::vector<Entry>*> _list;\n\trpl::variable<Data::GiftsUpdate> _collectionChanges;\n\tbase::flat_set<Data::SavedStarGiftId> _inCollection;\n\n\tMTP::Sender _api;\n\tmtpRequestId _loadMoreRequestId = 0;\n\tFn<void()> _collectionsLoadedCallback;\n\tQString _offset;\n\tbool _collectionsLoaded = false;\n\n\trpl::event_stream<Descriptor> _descriptorChanges;\n\trpl::event_stream<bool> _notifyEnabled;\n\tstd::vector<View> _views;\n\tstd::unique_ptr<View> _draggedView;\n\tint _viewsForWidth = 0;\n\tint _viewsFromRow = 0;\n\tint _viewsTillRow = 0;\n\n\tQSize _singleMin;\n\tQSize _single;\n\tint _perRow = 0;\n\tint _visibleFrom = 0;\n\tint _visibleTill = 0;\n\n\tDragState _dragging;\n\tbase::flat_map<int, ShiftAnimation> _shiftAnimations;\n\tint _selected = -1;\n\tint _pressedIndex = -1;\n\n\tUi::Animations::Basic _scrollAnimation;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\nprotected:\n\tvoid focusOutEvent(QFocusEvent *e) override;\n\nprivate:\n\tvoid cancelDragging();\n\tvoid updateScrollCallback();\n\tvoid checkForScrollAnimation();\n\t[[nodiscard]] int deltaFromEdge();\n\n};\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> window,\n\tnot_null<PeerData*> peer,\n\trpl::producer<Descriptor> descriptor,\n\tUi::ScrollArea *scroll)\n: InnerWidget(\n\tparent,\n\twindow,\n\tpeer,\n\tstd::move(descriptor),\n\tscroll,\n\t0,\n\t{ .total = peer->peerGiftsCount() }) {\n}\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> window,\n\tnot_null<PeerData*> peer,\n\trpl::producer<Descriptor> descriptor,\n\tUi::ScrollArea *scroll,\n\tint addingToCollectionId,\n\tEntries all)\n: BoxContentDivider(parent)\n, _window(window)\n, _peer(peer)\n, _scroll(scroll)\n, _addingToCollectionId(addingToCollectionId)\n, _mode(_addingToCollectionId\n\t? GiftButtonMode::Selection\n\t: GiftButtonMode::Minimal)\n, _descriptor(std::move(descriptor))\n, _delegate(&_window->session(), _mode)\n, _all(std::move(all))\n, _entries(&_all)\n, _list(&_entries->list)\n, _collectionChanges(Data::GiftsUpdate{\n\t.peer = _peer,\n\t.collectionId = addingToCollectionId,\n})\n, _api(&_peer->session().mtp())\n, _scrollAnimation([=] { updateScrollCallback(); }) {\n\t_singleMin = _delegate.buttonSize();\n\n\tif (peer->canManageGifts()) {\n\t\tsubscribeToUpdates();\n\t}\n\n\tfor (const auto &entry : _all.list) {\n\t\tmarkInCollection(entry.gift);\n\t}\n\n\tloadCollections();\n\n\t_window->session().data().giftsUpdates(\n\t) | rpl::on_next([=](const Data::GiftsUpdate &update) {\n\t\tif (update.peer != _peer) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto added = base::flat_set<Data::SavedStarGiftId>{\n\t\t\tbegin(update.added),\n\t\t\tend(update.added)\n\t\t};\n\t\tconst auto removed = base::flat_set<Data::SavedStarGiftId>{\n\t\t\tbegin(update.removed),\n\t\t\tend(update.removed)\n\t\t};\n\t\tconst auto id = update.collectionId;\n\t\tconst auto process = [&](Entries &entries) {\n\t\t\tfor (auto &entry : entries.list) {\n\t\t\t\tif (added.contains(entry.gift.manageId)) {\n\t\t\t\t\tentry.gift.collectionIds.push_back(id);\n\t\t\t\t} else if (removed.contains(entry.gift.manageId)) {\n\t\t\t\t\tentry.gift.collectionIds.erase(\n\t\t\t\t\t\tranges::remove(entry.gift.collectionIds, id),\n\t\t\t\t\t\tend(entry.gift.collectionIds));\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tfor (auto &[_, entries] : _perCollection) {\n\t\t\tprocess(entries);\n\t\t}\n\t\tprocess(_all);\n\t}, lifetime());\n\n\t_descriptor.value(\n\t) | rpl::on_next([=](Descriptor now) {\n\t\tswitchTo(now.collectionId);\n\t}, lifetime());\n}\n\nvoid InnerWidget::switchTo(int collectionId) {\n\t_collectionsLoadedCallback = nullptr;\n\t_api.request(base::take(_loadMoreRequestId)).cancel();\n\t_entries = collectionId ? &_perCollection[collectionId] : &_all;\n\t_list = &_entries->list;\n\trefreshButtons();\n\trefreshAbout();\n\tloadMore();\n}\n\nvoid InnerWidget::loadCollections() {\n\tif (_addingToCollectionId) {\n\t\treturn;\n\t}\n\t_api.request(MTPpayments_GetStarGiftCollections(\n\t\t_peer->input(),\n\t\tMTP_long(Api::CountHash(_collections\n\t\t\t| ranges::views::transform(&Data::GiftCollection::hash)))\n\t)).done([=](const MTPpayments_StarGiftCollections &result) {\n\t\tresult.match([&](const MTPDpayments_starGiftCollections &data) {\n\t\t\t_collections = FromTL(&_window->session(), data);\n\n\t\t\trefreshCollectionsTabs();\n\t\t}, [&](const MTPDpayments_starGiftCollectionsNotModified &) {\n\t\t});\n\t\t_collectionsLoaded = true;\n\t\tif (const auto onstack = base::take(_collectionsLoadedCallback)) {\n\t\t\tonstack();\n\t\t}\n\t}).fail([=] {\n\t\t_collectionsLoaded = true;\n\t\tif (const auto onstack = base::take(_collectionsLoadedCallback)) {\n\t\t\tonstack();\n\t\t}\n\t}).send();\n}\n\nvoid InnerWidget::subscribeToUpdates() {\n\t_peer->owner().giftUpdates(\n\t) | rpl::on_next([=](const Data::GiftUpdate &update) {\n\t\tapplyUpdateTo(_all, update);\n\t\tusing Action = Data::GiftUpdate::Action;\n\t\tif (update.action == Action::Pin\n\t\t\t|| update.action == Action::Unpin\n\t\t\t|| update.action == Action::Delete) {\n\t\t\tfor (auto &[_, entries] : _perCollection) {\n\t\t\t\tapplyUpdateTo(entries, update);\n\t\t\t}\n\t\t};\n\t}, lifetime());\n}\n\nvoid InnerWidget::applyUpdateTo(\n\t\tEntries &entries,\n\t\tconst Data::GiftUpdate &update) {\n\tusing Action = Data::GiftUpdate::Action;\n\tconst auto savedId = [](const Entry &entry) {\n\t\treturn entry.gift.manageId;\n\t};\n\tconst auto bySlug = [](const Entry &entry) {\n\t\treturn entry.gift.info.unique\n\t\t\t? entry.gift.info.unique->slug\n\t\t\t: QString();\n\t};\n\tconst auto i = update.id\n\t\t? ranges::find(*_list, update.id, savedId)\n\t\t: ranges::find(*_list, update.slug, bySlug);\n\tif (i == end(*_list)) {\n\t\treturn;\n\t}\n\tconst auto index = int(i - begin(*_list));\n\tif (update.action == Action::Convert\n\t\t|| update.action == Action::Transfer\n\t\t|| update.action == Action::Delete) {\n\t\t_list->erase(i);\n\t\tif (entries.total > 0) {\n\t\t\t--entries.total;\n\t\t}\n\t\tfor (auto &view : _views) {\n\t\t\tif (view.index >= index) {\n\t\t\t\t--view.index;\n\t\t\t}\n\t\t}\n\t} else if (update.action == Action::Save\n\t\t|| update.action == Action::Unsave) {\n\t\ti->gift.hidden = (update.action == Action::Unsave);\n\n\t\tconst auto unpin = i->gift.hidden && i->gift.pinned;\n\t\tv::match(i->descriptor, [](GiftTypePremium &) {\n\t\t}, [&](GiftTypeStars &data) {\n\t\t\tdata.hidden = i->gift.hidden;\n\t\t});\n\t\tfor (auto &view : _views) {\n\t\t\tif (view.index == index) {\n\t\t\t\tview.index = -1;\n\t\t\t\tview.manageId = {};\n\t\t\t}\n\t\t}\n\t\tif (unpin) {\n\t\t\tmarkUnpinned(i);\n\t\t}\n\t} else if (update.action == Action::Pin\n\t\t|| update.action == Action::Unpin) {\n\t\tif (update.action == Action::Pin) {\n\t\t\tmarkPinned(i);\n\t\t} else {\n\t\t\tmarkUnpinned(i);\n\t\t}\n\t} else if (update.action == Action::ResaleChange) {\n\t\tfor (auto &view : _views) {\n\t\t\tif (view.index == index) {\n\t\t\t\tview.index = -1;\n\t\t\t\tview.manageId = {};\n\t\t\t}\n\t\t}\n\t} else if (update.action == Action::Upgraded) {\n\t\t_scrollToTop.fire({});\n\t\treloadCollection(_descriptor.current().collectionId);\n\t} else {\n\t\treturn;\n\t}\n\trefreshButtons();\n\tif (update.action == Action::Pin) {\n\t\t_scrollToTop.fire({});\n\t}\n}\n\nvoid InnerWidget::markPinned(std::vector<Entry>::iterator i) {\n\tconst auto index = int(i - begin(*_list));\n\n\ti->gift.pinned = true;\n\tv::match(i->descriptor, [](const GiftTypePremium &) {\n\t}, [&](GiftTypeStars &data) {\n\t\tdata.pinned = true;\n\t});\n\n\tauto lastPinnedIndex = 0;\n\tfor (auto j = begin(*_list); j != end(*_list); ++j) {\n\t\tif (j->gift.pinned) {\n\t\t\tlastPinnedIndex = int(j - begin(*_list));\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tif (index != lastPinnedIndex) {\n\t\tbase::reorder(*_list, index, lastPinnedIndex);\n\t}\n\tauto unpin = end(*_list);\n\tconst auto session = &_window->session();\n\tconst auto limit = session->appConfig().pinnedGiftsLimit();\n\tif (limit < _list->size()) {\n\t\tconst auto j = begin(*_list) + limit;\n\t\tif (j->gift.pinned) {\n\t\t\tunpin = j;\n\t\t}\n\t}\n\tfor (auto &view : _views) {\n\t\tif (view.index <= index) {\n\t\t\tview.index = -1;\n\t\t\tview.manageId = {};\n\t\t}\n\t}\n\tif (unpin != end(_entries->list)) {\n\t\tmarkUnpinned(unpin);\n\t}\n}\n\nvoid InnerWidget::markUnpinned(std::vector<Entry>::iterator i) {\n\tconst auto index = int(i - begin(*_list));\n\n\ti->gift.pinned = false;\n\tv::match(i->descriptor, [](const GiftTypePremium &) {\n\t}, [&](GiftTypeStars &data) {\n\t\tdata.pinned = false;\n\t});\n\tauto after = index + 1;\n\tfor (auto j = i + 1; j != end(*_list); ++j) {\n\t\tif (!j->gift.pinned && j->gift.date <= i->gift.date) {\n\t\t\tbreak;\n\t\t}\n\t\t++after;\n\t}\n\tif (after == _list->size() && !_entries->allLoaded) {\n\t\t// We don't know if the correct position is exactly in the end\n\t\t// of the loaded part or later, so we hide it for now, let it\n\t\t// be loaded later while scrolling.\n\t\t_list->erase(i);\n\t} else if (after > index + 1) {\n\t\tstd::rotate(i, i + 1, begin(*_list) + after);\n\t}\n\tfor (auto &view : _views) {\n\t\tif (view.index >= index) {\n\t\t\tview.index = -1;\n\t\t\tview.manageId = {};\n\t\t}\n\t}\n}\n\nvoid InnerWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tconst auto page = (visibleBottom - visibleTop);\n\tif (visibleBottom + page * kPreloadPages >= height()) {\n\t\tloadMore();\n\t}\n\t_visibleFrom = visibleTop;\n\t_visibleTill = visibleBottom;\n\tvalidateButtons();\n}\n\nvoid InnerWidget::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto aboutSize = _about\n\t\t? _about->size().grownBy(st::giftListAboutMargin)\n\t\t: QSize();\n\tconst auto skips = QMargins(0, 0, 0, aboutSize.height());\n\tp.fillRect(rect().marginsRemoved(skips), st::boxDividerBg->c);\n\tpaintTop(p);\n\tif (const auto bottom = skips.bottom()) {\n\t\tpaintBottom(p, bottom);\n\t}\n}\n\nvoid InnerWidget::collectionAdded(MTPStarGiftCollection result) {\n\t_collections.push_back(FromTL(&_window->session(), result));\n\tconst auto id = _collections.back().id;\n\trefreshCollectionsTabs();\n\n\t_collectionsTabs->setActiveTab(QString::number(id));\n\n\tauto now = _descriptor.current();\n\tnow.collectionId = id;\n\t_descriptorChanges.fire(std::move(now));\n}\n\nvoid InnerWidget::loadMore() {\n\tconst auto descriptor = _descriptor.current();\n\tconst auto filter = descriptor.filter;\n\tconst auto filterChanged = (_entries->filter != filter);\n\tconst auto allLoaded = !filterChanged && _entries->allLoaded;\n\tif (allLoaded || _loadMoreRequestId) {\n\t\treturn;\n\t}\n\tusing Flag = MTPpayments_GetSavedStarGifts::Flag;\n\tconst auto collectionId = descriptor.collectionId;\n\t_loadMoreRequestId = _api.request(MTPpayments_GetSavedStarGifts(\n\t\tMTP_flags((filter.sortByValue ? Flag::f_sort_by_value : Flag())\n\t\t\t| (filter.skipLimited ? Flag::f_exclude_unupgradable : Flag())\n\t\t\t| (filter.skipUpgradable ? Flag::f_exclude_upgradable : Flag())\n\t\t\t| (filter.skipUnlimited ? Flag::f_exclude_unlimited : Flag())\n\t\t\t| (filter.skipUnique ? Flag::f_exclude_unique : Flag())\n\t\t\t| (filter.skipSaved ? Flag::f_exclude_saved : Flag())\n\t\t\t| (filter.skipUnsaved ? Flag::f_exclude_unsaved : Flag())\n\t\t\t| (collectionId ? Flag::f_collection_id : Flag())),\n\t\t_peer->input(),\n\t\tMTP_int(collectionId),\n\t\tMTP_string(filterChanged ? QString() : _offset),\n\t\tMTP_int(kPerPage)\n\t)).done([=](const MTPpayments_SavedStarGifts &result) {\n\t\tconst auto &data = result.data();\n\n\t\tconst auto owner = &_peer->owner();\n\t\towner->processUsers(data.vusers());\n\t\towner->processChats(data.vchats());\n\n\t\tif (_addingToCollectionId || _collectionsLoaded) {\n\t\t\tloaded(result);\n\t\t} else {\n\t\t\t_collectionsLoadedCallback = [=] {\n\t\t\t\tloaded(result);\n\t\t\t};\n\t\t}\n\t}).fail([=] {\n\t\t_loadMoreRequestId = 0;\n\t\t_collectionsLoadedCallback = nullptr;\n\t\t_entries->filter = _descriptor.current().filter;\n\t\t_entries->allLoaded = true;\n\t}).send();\n}\n\nvoid InnerWidget::loaded(const MTPpayments_SavedStarGifts &result) {\n\tconst auto &data = result.data();\n\n\t_loadMoreRequestId = 0;\n\t_collectionsLoadedCallback = nullptr;\n\tif (const auto enabled = data.vchat_notifications_enabled()) {\n\t\t_notifyEnabled.fire(mtpIsTrue(*enabled));\n\t}\n\tif (const auto next = data.vnext_offset()) {\n\t\t_offset = qs(*next);\n\t} else {\n\t\t_entries->allLoaded = true;\n\t}\n\tconst auto descriptor = _descriptor.current();\n\tconst auto filter = descriptor.filter;\n\tif (!filter.skipsSomething()) {\n\t\t_entries->total = data.vcount().v;\n\t}\n\tif (_entries->filter != filter) {\n\t\t_entries->filter = filter;\n\t\t_list->clear();\n\t}\n\t_list->reserve(_list->size() + data.vgifts().v.size());\n\n\tconst auto i = ranges::find(\n\t\t_collections,\n\t\tdescriptor.collectionId,\n\t\t&Data::GiftCollection::id);\n\tconst auto collection = (i != end(_collections)) ? &*i : nullptr;\n\n\tauto hasUnique = false;\n\tfor (const auto &gift : data.vgifts().v) {\n\t\tif (auto parsed = Api::FromTL(_peer, gift)) {\n\t\t\tif (collection && !collection->icon) {\n\t\t\t\tcollection->icon = parsed->info.document;\n\t\t\t\trefreshCollectionsTabs();\n\t\t\t}\n\t\t\tmarkInCollection(*parsed);\n\t\t\tauto descriptor = DescriptorForGift(_peer, *parsed);\n\t\t\t_list->push_back({\n\t\t\t\t.gift = std::move(*parsed),\n\t\t\t\t.descriptor = std::move(descriptor),\n\t\t\t});\n\t\t\thasUnique = (parsed->info.unique != nullptr);\n\t\t}\n\t}\n\tif (_entries->allLoaded) {\n\t\t_entries->total = _entries->list.size();\n\t}\n\trefreshButtons();\n\trefreshAbout();\n\n\tif (hasUnique) {\n\t\tUi::PreloadUniqueGiftResellPrices(&_peer->session());\n\t}\n}\n\nvoid InnerWidget::markInCollection(const Data::SavedStarGift &gift) {\n\tif (const auto collectionId = _addingToCollectionId) {\n\t\tconst auto id = gift.manageId;\n\t\tif (ranges::contains(gift.collectionIds, collectionId)) {\n\t\t\tconst auto &changes = _collectionChanges.current();\n\t\t\tif (!ranges::contains(changes.removed, id)) {\n\t\t\t\t_inCollection.emplace(id);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid InnerWidget::refreshButtons() {\n\t_viewsForWidth = 0;\n\t_viewsFromRow = 0;\n\t_viewsTillRow = 0;\n\tresizeToWidth(width());\n\tvalidateButtons();\n}\n\nstd::unique_ptr<GiftButton> InnerWidget::createGiftButton() {\n\tauto button = std::make_unique<GiftButton>(this, &_delegate);\n\tconst auto raw = button.get();\n\traw->contextMenuRequests(\n\t) | rpl::on_next([=](QPoint point) {\n\t\tshowMenuFor(raw, point);\n\t}, raw->lifetime());\n\n\traw->mouseEvents(\n\t) | rpl::on_next([=](QMouseEvent *e) {\n\t\tswitch (e->type()) {\n\t\tcase QEvent::MouseButtonPress:\n\t\t\traw->raise();\n\t\t\tmousePressEvent(e);\n\t\t\tbreak;\n\t\tcase QEvent::MouseMove:\n\t\t\tmouseMoveEvent(e);\n\t\t\tbreak;\n\t\tcase QEvent::MouseButtonRelease:\n\t\t\tmouseReleaseEvent(e);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tbreak;\n\t\t}\n\t}, raw->lifetime());\n\n\traw->show();\n\treturn button;\n}\n\nvoid InnerWidget::validateButtons() {\n\tif (!_perRow) {\n\t\treturn;\n\t}\n\tconst auto padding = st::giftBoxPadding;\n\tconst auto vskip = (_collectionsTabs && !_collectionsTabs->isHidden())\n\t\t? (padding.top() + _collectionsTabs->height() + padding.top())\n\t\t: padding.bottom();\n\tconst auto row = _single.height() + st::giftBoxGiftSkip.y();\n\tconst auto fromRow = std::max(_visibleFrom - vskip, 0) / row;\n\tconst auto tillRow = (_visibleTill - vskip + row - 1) / row;\n\tAssert(tillRow >= fromRow);\n\tif (_viewsFromRow == fromRow\n\t\t&& _viewsTillRow == tillRow\n\t\t&& _viewsForWidth == width()) {\n\t\treturn;\n\t}\n\t_viewsFromRow = fromRow;\n\t_viewsTillRow = tillRow;\n\t_viewsForWidth = width();\n\n\tconst auto available = _viewsForWidth - padding.left() - padding.right();\n\tconst auto skipw = st::giftBoxGiftSkip.x();\n\tconst auto fullw = _perRow * (_single.width() + skipw) - skipw;\n\tconst auto left = padding.left() + (available - fullw) / 2;\n\tconst auto oneh = _single.height() + st::giftBoxGiftSkip.y();\n\tauto x = left;\n\tauto y = vskip + fromRow * oneh;\n\tauto views = std::vector<View>();\n\tviews.reserve((tillRow - fromRow) * _perRow);\n\tconst auto idUsed = [&](uint64 giftId, int column, int row) {\n\t\tfor (auto j = row; j != tillRow; ++j) {\n\t\t\tfor (auto i = column; i != _perRow; ++i) {\n\t\t\t\tconst auto index = j * _perRow + i;\n\t\t\t\tif (index >= _list->size()) {\n\t\t\t\t\treturn false;\n\t\t\t\t} else if ((*_list)[index].gift.info.id == giftId) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tcolumn = 0;\n\t\t}\n\t\treturn false;\n\t};\n\tconst auto add = [&](int column, int row) {\n\t\tconst auto index = row * _perRow + column;\n\t\tif (index >= _list->size()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto &entry = (*_list)[index];\n\t\tconst auto &gift = entry.gift;\n\t\tconst auto giftId = gift.info.id;\n\t\tconst auto manageId = gift.manageId;\n\t\tconst auto &descriptor = entry.descriptor;\n\t\tconst auto already = ranges::find(_views, giftId, &View::giftId);\n\t\tif (already != end(_views)) {\n\t\t\tviews.push_back(base::take(*already));\n\t\t} else {\n\t\t\tconst auto unused = ranges::find_if(_views, [&](const View &v) {\n\t\t\t\treturn v.button && !idUsed(v.giftId, column, row);\n\t\t\t});\n\t\t\tif (unused != end(_views)) {\n\t\t\t\tviews.push_back(base::take(*unused));\n\t\t\t} else {\n\t\t\t\tviews.push_back({ .button = createGiftButton() });\n\t\t\t}\n\t\t}\n\t\tauto &view = views.back();\n\t\tview.index = index;\n\t\tview.manageId = manageId;\n\t\tview.giftId = giftId;\n\t\tif (_addingToCollectionId) {\n\t\t\tview.button->toggleSelected(\n\t\t\t\t_inCollection.contains(manageId),\n\t\t\t\tGiftSelectionMode::Check,\n\t\t\t\tanim::type::instant);\n\t\t}\n\t\tview.button->setDescriptor(descriptor, _mode);\n\t\treturn true;\n\t};\n\tfor (auto j = fromRow; j != tillRow; ++j) {\n\t\tfor (auto i = 0; i != _perRow; ++i) {\n\t\t\tif (!add(i, j)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst auto &view = views.back();\n\t\t\tconst auto viewIndex = view.index;\n\t\t\tauto pos = QPoint(x, y);\n\n\t\t\tif (_dragging.enabled && viewIndex >= 0) {\n\t\t\t\tif (viewIndex == _dragging.index && !isDraggedAnimating()) {\n\t\t\t\t\tpos = mapFromGlobal(QCursor::pos()) - _dragging.point;\n\t\t\t\t} else if (viewIndex == _dragging.index && isDraggedAnimating()) {\n\t\t\t\t\tconst auto it = _shiftAnimations.find(viewIndex);\n\t\t\t\t\tif (it != _shiftAnimations.end()) {\n\t\t\t\t\t\tpos = QPoint(\n\t\t\t\t\t\t\tit->second.xAnimation.value(pos.x()),\n\t\t\t\t\t\t\tit->second.yAnimation.value(pos.y()));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconst auto it = _shiftAnimations.find(viewIndex);\n\t\t\t\t\tif (it != _shiftAnimations.end()) {\n\t\t\t\t\t\tconst auto &entry = it->second;\n\t\t\t\t\t\tconst auto toPos = posFromIndex(viewIndex + entry.shift);\n\t\t\t\t\t\tpos = QPoint(\n\t\t\t\t\t\t\tentry.xAnimation.value(toPos.x()),\n\t\t\t\t\t\t\tentry.yAnimation.value(toPos.y()));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (_dragging.enabled && viewIndex == _dragging.index) {\n\t\t\t\tpos = mapFromGlobal(QCursor::pos()) - _dragging.point;\n\t\t\t}\n\n\t\t\tview.button->setGeometry(\n\t\t\t\tQRect(pos, _single),\n\t\t\t\t_delegate.buttonExtend());\n\t\t\tx += _single.width() + skipw;\n\t\t}\n\t\tx = left;\n\t\ty += oneh;\n\t}\n\n\tif (_dragging.enabled\n\t\t&& _dragging.index >= 0\n\t\t&& _dragging.index < _list->size()) {\n\t\tconst auto alreadyInViews = ranges::find(\n\t\t\tviews,\n\t\t\t_dragging.index,\n\t\t\t&View::index);\n\t\tif (alreadyInViews == end(views)) {\n\t\t\tif (!_draggedView) {\n\t\t\t\tconst auto &entry = (*_list)[_dragging.index];\n\t\t\t\t_draggedView = std::make_unique<View>();\n\t\t\t\t_draggedView->button = createGiftButton();\n\t\t\t\t_draggedView->index = _dragging.index;\n\t\t\t\t_draggedView->manageId = entry.gift.manageId;\n\t\t\t\t_draggedView->giftId = entry.gift.info.id;\n\t\t\t\t_draggedView->button->setDescriptor(entry.descriptor, _mode);\n\t\t\t\tif (_addingToCollectionId) {\n\t\t\t\t\t_draggedView->button->toggleSelected(\n\t\t\t\t\t\t_inCollection.contains(entry.gift.manageId),\n\t\t\t\t\t\tGiftSelectionMode::Check,\n\t\t\t\t\t\tanim::type::instant);\n\t\t\t\t}\n\t\t\t\t_draggedView->button->show();\n\t\t\t} else {\n\t\t\t\t_draggedView->index = _dragging.index;\n\t\t\t}\n\t\t\tauto pos = mapFromGlobal(QCursor::pos()) - _dragging.point;\n\t\t\t_draggedView->button->setGeometry(\n\t\t\t\tQRect(pos, _single),\n\t\t\t\t_delegate.buttonExtend());\n\t\t\t_draggedView->button->raise();\n\t\t}\n\t} else if (!_dragging.enabled || _dragging.index < 0) {\n\t\t_draggedView = nullptr;\n\t}\n\n\tstd::swap(_views, views);\n}\n\nauto InnerWidget::pinnedSavedGifts()\n-> Fn<std::vector<Data::CreditsHistoryEntry>()> {\n\tstruct Entry {\n\t\tData::SavedStarGiftId id;\n\t\tstd::shared_ptr<Data::UniqueGift> unique;\n\t};\n\tauto entries = std::vector<Entry>();\n\tfor (const auto &entry : *_list) {\n\t\tif (entry.gift.pinned) {\n\t\t\tAssert(entry.gift.info.unique != nullptr);\n\t\t\tentries.push_back({\n\t\t\t\tentry.gift.manageId,\n\t\t\t\tentry.gift.info.unique,\n\t\t\t});\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn [entries] {\n\t\tauto result = std::vector<Data::CreditsHistoryEntry>();\n\t\tresult.reserve(entries.size());\n\t\tfor (const auto &entry : entries) {\n\t\t\tconst auto &id = entry.id;\n\t\t\tresult.push_back({\n\t\t\t\t.bareMsgId = uint64(id.userMessageId().bare),\n\t\t\t\t.bareEntryOwnerId = id.chat() ? id.chat()->id.value : 0,\n\t\t\t\t.giftChannelSavedId = id.chatSavedId(),\n\t\t\t\t.uniqueGift = entry.unique,\n\t\t\t\t.stargift = true,\n\t\t\t});\n\t\t}\n\t\treturn result;\n\t};\n}\n\nvoid InnerWidget::showMenuForCollection(int id) {\n\tif (_menu || _addingToCollectionId) {\n\t\treturn;\n\t}\n\t_menu = base::make_unique_q<Ui::PopupMenu>(this, st::popupMenuWithIcons);\n\tconst auto addAction = Ui::Menu::CreateAddActionCallback(_menu);\n\n\tif (_collectionsTabs && _collectionsTabs->reorderEnabled()) {\n\t\taddAction(\n\t\t\ttr::lng_gift_collection_reorder_exit(tr::now),\n\t\t\t[=] {\n\t\t\t\tflushCollectionReorder();\n\t\t\t\t_collectionsTabs->setReorderEnabled(false);\n\t\t\t},\n\t\t\t&st::menuIconReorder);\n\t\t_menu->popup(QCursor::pos());\n\t\treturn;\n\t}\n\n\taddAction(tr::lng_gift_collection_add_button(tr::now), [=] {\n\t\teditCollectionGifts(id);\n\t}, &st::menuIconGiftPremium);\n\tif (const auto username = _peer->username(); !username.isEmpty()) {\n\t\taddAction(tr::lng_gift_collection_share(tr::now), [=] {\n\t\t\tshareCollectionLink(username, id);\n\t\t}, &st::menuIconShare);\n\t}\n\taddAction(tr::lng_gift_collection_edit(tr::now), [=] {\n\t\teditCollectionName(id);\n\t}, &st::menuIconEdit);\n\tif (_collectionsTabs) {\n\t\taddAction(\n\t\t\ttr::lng_gift_collection_reorder(tr::now),\n\t\t\t[=] { _collectionsTabs->setReorderEnabled(true); },\n\t\t\t&st::menuIconReorder);\n\t}\n\taddAction({\n\t\t.text = tr::lng_gift_collection_delete(tr::now),\n\t\t.handler = [=] { confirmDeleteCollection(id); },\n\t\t.icon = &st::menuIconDeleteAttention,\n\t\t.isAttention = true,\n\t});\n\t_menu->popup(QCursor::pos());\n}\n\nvoid InnerWidget::shareCollectionLink(const QString &username, int id) {\n\tconst auto url = _window->session().createInternalLinkFull(\n\t\tusername + u\"/c/\"_q + QString::number(id));\n\tFastShareLink(_window, url);\n}\n\nvoid InnerWidget::editCollectionName(int id) {\n\tconst auto done = [=](QString name) {\n\t\tcollectionRenamed(id, name);\n\t};\n\tconst auto i = ranges::find(_collections, id, &Data::GiftCollection::id);\n\tif (i == end(_collections)) {\n\t\treturn;\n\t}\n\t_window->uiShow()->show(Box(\n\t\tEditCollectionNameBox,\n\t\t_window,\n\t\tpeer(),\n\t\tid,\n\t\ti->title,\n\t\tdone));\n}\n\nvoid InnerWidget::confirmDeleteCollection(int id) {\n\tconst auto done = [=](Fn<void()> close) {\n\t\t_window->session().api().request(\n\t\t\tMTPpayments_DeleteStarGiftCollection(_peer->input(), MTP_int(id))\n\t\t).send();\n\t\tcollectionRemoved(id);\n\t\tclose();\n\t};\n\t_window->uiShow()->show(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_gift_collection_delete_sure(),\n\t\t.confirmed = crl::guard(this, done),\n\t\t.confirmText = tr::lng_gift_collection_delete_button(),\n\t\t.confirmStyle = &st::attentionBoxButton,\n\t}));\n}\n\nvoid InnerWidget::fillCollectionsMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst Data::SavedStarGift &gift) {\n\tif (!_peer->canManageGifts() || _collections.empty()) {\n\t\treturn;\n\t}\n\n\tconst auto addAction = Ui::Menu::CreateAddActionCallback(menu);\n\tfor (const auto &collection : _collections) {\n\t\tconst auto id = collection.id;\n\t\tconst auto contains = ranges::contains(gift.collectionIds, id);\n\t\tconst auto title = collection.title;\n\n\t\tauto callback = [=] {\n\t\t\tif (contains) {\n\t\t\t\tremoveGiftFromCollection(gift.manageId, id);\n\t\t\t} else {\n\t\t\t\taddGiftToCollection(gift.manageId, id);\n\t\t\t}\n\t\t};\n\n\t\taddAction(\n\t\t\ttitle,\n\t\t\tstd::move(callback),\n\t\t\tcontains ? &st::mediaPlayerMenuCheck : nullptr);\n\t}\n}\n\nvoid InnerWidget::addGiftToCollection(\n\t\tData::SavedStarGiftId giftId,\n\t\tint collectionId) {\n\tauto changes = Data::GiftsUpdate{\n\t\t.peer = _peer,\n\t\t.collectionId = collectionId,\n\t\t.added = { giftId },\n\t};\n\tusing Flag = MTPpayments_UpdateStarGiftCollection::Flag;\n\t_window->session().api().request(\n\t\tMTPpayments_UpdateStarGiftCollection(\n\t\t\tMTP_flags(Flag::f_add_stargift),\n\t\t\t_peer->input(),\n\t\t\tMTP_int(collectionId),\n\t\t\tMTPstring(),\n\t\t\tMTPVector<MTPInputSavedStarGift>(),\n\t\t\tMTP_vector<MTPInputSavedStarGift>({\n\t\t\t\tApi::InputSavedStarGiftId(giftId)\n\t\t\t}),\n\t\t\tMTPVector<MTPInputSavedStarGift>())\n\t).done([=](const MTPStarGiftCollection &result) {\n\t\t_window->session().data().notifyGiftsUpdate(base::duplicate(changes));\n\n\t\tconst auto i = ranges::find(\n\t\t\t_collections,\n\t\t\tcollectionId,\n\t\t\t&Data::GiftCollection::id);\n\t\tif (i != end(_collections)) {\n\t\t\tconst auto updated = FromTL(&_window->session(), result);\n\t\t\t*i = updated;\n\n\t\t\tauto &per = _perCollection[collectionId];\n\t\t\tper.total = updated.count;\n\n\t\t\tconst auto giftIt = ranges::find(\n\t\t\t\t_all.list,\n\t\t\t\tgiftId,\n\t\t\t\t[](const Entry &entry) { return entry.gift.manageId; });\n\t\t\tif (giftIt != end(_all.list)) {\n\t\t\t\tper.list.insert(per.list.begin(), *giftIt);\n\t\t\t}\n\n\t\t\tif (_addingToCollectionId == collectionId) {\n\t\t\t\tauto currentChanges = _collectionChanges.current();\n\t\t\t\tcurrentChanges.added.push_back(giftId);\n\t\t\t\t_collectionChanges = std::move(currentChanges);\n\t\t\t}\n\n\t\t\trefreshCollectionsTabs();\n\t\t}\n\t}).fail([=, show = _window->uiShow()](const MTP::Error &error) {\n\t\tif (!Ui::ShowGiftErrorToast(show, error)) {\n\t\t\tshow->showToast(error.type());\n\t\t}\n\t}).send();\n}\n\nvoid InnerWidget::showMenuFor(not_null<GiftButton*> button, QPoint point) {\n\tif (_menu || _addingToCollectionId) {\n\t\treturn;\n\t}\n\tconst auto index = [&] {\n\t\tfor (const auto &view : _views) {\n\t\t\tif (view.button.get() == button) {\n\t\t\t\treturn view.index;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t}();\n\tif (index < 0) {\n\t\treturn;\n\t}\n\n\tauto entry = ::Settings::SavedStarGiftEntry(\n\t\t_peer,\n\t\t(*_list)[index].gift);\n\tconst auto collectionId = _descriptor.current().collectionId;\n\tentry.pinnedSavedGifts = collectionId > 0 ? nullptr : pinnedSavedGifts();\n\t_menu = base::make_unique_q<Ui::PopupMenu>(this, st::popupMenuWithIcons);\n\tif (_peer->canManageGifts() && !_collections.empty()) {\n\t\tconst auto &gift = (*_list)[index].gift;\n\t\tconst auto addAction = Ui::Menu::CreateAddActionCallback(_menu);\n\t\taddAction(Ui::Menu::MenuCallback::Args{\n\t\t\t.text = tr::lng_gift_collection_add_to(tr::now),\n\t\t\t.handler = nullptr,\n\t\t\t.icon = &st::menuIconAddToFolder,\n\t\t\t.fillSubmenu = [&](not_null<Ui::PopupMenu*> menu) {\n\t\t\t\tfillCollectionsMenu(menu, gift);\n\t\t\t},\n\t\t});\n\t}\n\t::Settings::FillSavedStarGiftMenu(\n\t\t_window->uiShow(),\n\t\t_menu.get(),\n\t\tentry,\n\t\t::Settings::SavedStarGiftMenuType::List);\n\n\tif (collectionId > 0 && _peer->canManageGifts()) {\n\t\tconst auto &gift = (*_list)[index].gift;\n\t\tif (ranges::contains(gift.collectionIds, collectionId)) {\n\t\t\tconst auto addAction = Ui::Menu::CreateAddActionCallback(_menu);\n\t\t\taddAction({\n\t\t\t\t.text = tr::lng_gift_collection_remove_from(tr::now),\n\t\t\t\t.handler = [=] {\n\t\t\t\t\tremoveGiftFromCollection(gift.manageId, collectionId);\n\t\t\t\t},\n\t\t\t\t.icon = &st::menuIconDeleteAttention,\n\t\t\t\t.isAttention = true,\n\t\t\t});\n\t\t}\n\t}\n\n\tif (_menu->empty()) {\n\t\treturn;\n\t}\n\t_menu->popup(point);\n}\n\nvoid InnerWidget::showGift(int index) {\n\tExpects(index >= 0 && index < _list->size());\n\n\tif (const auto id = _addingToCollectionId) {\n\t\tauto &gift = (*_list)[index].gift;\n\t\tauto changes = _collectionChanges.current();\n\t\tconst auto selected = _inCollection.contains(gift.manageId);\n\t\tif (selected) {\n\t\t\t_inCollection.remove(gift.manageId);\n\t\t\tif (ranges::contains(gift.collectionIds, id)) {\n\t\t\t\tchanges.removed.push_back(gift.manageId);\n\t\t\t} else {\n\t\t\t\tchanges.added.erase(\n\t\t\t\t\tranges::remove(changes.added, gift.manageId),\n\t\t\t\t\tend(changes.added));\n\t\t\t}\n\t\t} else {\n\t\t\t_inCollection.emplace(gift.manageId);\n\t\t\tif (ranges::contains(gift.collectionIds, id)) {\n\t\t\t\tchanges.removed.erase(\n\t\t\t\t\tranges::remove(changes.removed, gift.manageId),\n\t\t\t\t\tend(changes.removed));\n\t\t\t} else {\n\t\t\t\tchanges.added.push_back(gift.manageId);\n\t\t\t}\n\t\t}\n\t\t_collectionChanges = std::move(changes);\n\n\t\tconst auto view = ranges::find(_views, index, &View::index);\n\t\tif (view != end(_views)) {\n\t\t\tview->button->toggleSelected(\n\t\t\t\t!selected,\n\t\t\t\tGiftSelectionMode::Check);\n\t\t}\n\t\treturn;\n\t}\n\t::Settings::ShowSavedStarGiftBox(\n\t\t_window,\n\t\t_peer,\n\t\t(*_list)[index].gift,\n\t\tpinnedSavedGifts());\n}\n\nvoid InnerWidget::refreshAbout() {\n\tconst auto descriptor = _descriptor.current();\n\tconst auto filter = descriptor.filter;\n\tconst auto collectionId = descriptor.collectionId;\n\tconst auto maybeEmpty = _list->empty();\n\tconst auto knownEmpty = maybeEmpty\n\t\t&& (_entries->allLoaded || !_entries->total);\n\tconst auto filteredEmpty = knownEmpty && filter.skipsSomething();\n\tconst auto collectionCanAdd = knownEmpty\n\t\t&& descriptor.collectionId != 0\n\t\t&& _peer->canManageGifts();\n\t_collectionEmpty = !filteredEmpty && collectionCanAdd;\n\tif (filteredEmpty) {\n\t\tauto text = tr::lng_peer_gifts_empty_search(\n\t\t\ttr::now,\n\t\t\ttr::rich);\n\t\ttext.append(\"\\n\\n\").append(tr::link(\n\t\t\ttr::lng_peer_gifts_view_all(tr::now)));\n\t\tauto about = std::make_unique<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\trpl::single(text),\n\t\t\tst::giftListAbout);\n\t\tabout->setClickHandlerFilter([=](const auto &...) {\n\t\t\tauto now = _descriptor.current();\n\t\t\tnow.filter = Filter();\n\t\t\t_descriptorChanges.fire(std::move(now));\n\t\t\treturn false;\n\t\t});\n\t\tabout->show();\n\t\t_about = std::move(about);\n\t\tresizeToWidth(width());\n\t} else if (collectionCanAdd) {\n\t\tauto about = std::make_unique<Ui::VerticalLayout>(this);\n\t\tabout->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tabout.get(),\n\t\t\t\ttr::lng_gift_collection_empty_title(),\n\t\t\t\tst::collectionEmptyTitle),\n\t\t\tst::collectionEmptyTitleMargin,\n\t\t\tstyle::al_top);\n\t\tabout->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tabout.get(),\n\t\t\t\ttr::lng_gift_collection_empty_text(),\n\t\t\t\tst::collectionEmptyText),\n\t\t\tst::collectionEmptyTextMargin,\n\t\t\tstyle::al_top);\n\n\t\tconst auto button = about->add(\n\t\t\tobject_ptr<Ui::RoundButton>(\n\t\t\t\tabout.get(),\n\t\t\t\trpl::single(QString()),\n\t\t\t\tst::collectionEmptyButton),\n\t\t\tst::collectionEmptyAddMargin,\n\t\t\tstyle::al_top);\n\t\tbutton->setText(tr::lng_gift_collection_add_button(\n\t\t) | rpl::map([](const QString &text) {\n\t\t\treturn Ui::Text::IconEmoji(&st::collectionAddIcon).append(text);\n\t\t}));\n\t\tbutton->setClickedCallback([=] {\n\t\t\teditCollectionGifts(collectionId);\n\t\t});\n\n\t\tabout->show();\n\t\t_about = std::move(about);\n\t\tresizeToWidth(width());\n\t} else if ((!collectionId && _peer->isSelf())\n\t\t|| (!collectionId && !_peer->canManageGifts())\n\t\t|| maybeEmpty) {\n\t\t_about = std::make_unique<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\t((maybeEmpty && !knownEmpty)\n\t\t\t\t? tr::lng_contacts_loading(tr::marked)\n\t\t\t\t: _peer->isSelf()\n\t\t\t\t? tr::lng_peer_gifts_about_mine(tr::rich)\n\t\t\t\t: tr::lng_peer_gifts_about(\n\t\t\t\t\tlt_user,\n\t\t\t\t\trpl::single(tr::bold(_peer->shortName())),\n\t\t\t\t\ttr::rich)),\n\t\t\tst::giftListAbout);\n\t\t_about->show();\n\t\tresizeToWidth(width());\n\t} else if (_about) {\n\t\t_about = nullptr;\n\t\tresizeToWidth(width());\n\t}\n}\n\nvoid InnerWidget::reloadCollection(int id) {\n\t_perCollection[id].filter = std::optional<Filter>();\n\t_perCollection[id].allLoaded = false;\n\n\tauto now = _descriptor.current();\n\tnow.filter = Filter();\n\tnow.collectionId = id;\n\t_descriptorChanges.fire(std::move(now));\n\n\t_api.request(base::take(_loadMoreRequestId)).cancel();\n\t_collectionsLoadedCallback = nullptr;\n\trefreshButtons();\n\trefreshAbout();\n\tloadMore();\n}\n\nvoid InnerWidget::editCollectionGifts(int id) {\n\tconst auto weak = base::make_weak(this);\n\t_window->uiShow()->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tbox->setTitle(tr::lng_gift_collection_add_title());\n\t\tbox->setWidth(st::boxWideWidth);\n\t\tbox->setStyle(st::collectionEditBox);\n\n\t\tstruct State {\n\t\t\trpl::variable<Descriptor> descriptor;\n\t\t\trpl::variable<Data::GiftsUpdate> changes;\n\t\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t\t\tbool saving = false;\n\t\t};\n\t\tconst auto state = box->lifetime().make_state<State>(State{\n\t\t\t.changes = Data::GiftsUpdate{\n\t\t\t\t.peer = _peer,\n\t\t\t\t.collectionId = id,\n\t\t\t},\n\t\t});\n\t\tconst auto content = box->addRow(\n\t\t\tobject_ptr<InnerWidget>(\n\t\t\t\tbox,\n\t\t\t\t_window,\n\t\t\t\t_peer,\n\t\t\t\tstate->descriptor.value(),\n\t\t\t\tnullptr,\n\t\t\t\tid,\n\t\t\t\t(_all.filter == Filter()) ? _all : Entries()),\n\t\t\tstyle::margins());\n\t\tstate->changes = content->changes();\n\n\t\tcontent->descriptorChanges(\n\t\t) | rpl::on_next([=](Descriptor now) {\n\t\t\tstate->descriptor = now;\n\t\t}, content->lifetime());\n\n\t\tcontent->scrollToTop() | rpl::on_next([=] {\n\t\t\tbox->scrollToY(0);\n\t\t}, content->lifetime());\n\n\t\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t\tbox->addTopButton(st::collectionEditMenuToggle, [=] {\n\t\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\tbox,\n\t\t\t\tst::popupMenuWithIcons);\n\t\t\tcontent->fillMenu(\n\t\t\t\tUi::Menu::CreateAddActionCallback(state->menu));\n\t\t\tstate->menu->popup(QCursor::pos());\n\t\t});\n\t\tconst auto weakBox = base::make_weak(box);\n\t\tauto text = state->changes.value(\n\t\t) | rpl::map([=](const Data::GiftsUpdate &update) {\n\t\t\treturn (!update.added.empty() && update.removed.empty())\n\t\t\t\t? tr::lng_gift_collection_add_button()\n\t\t\t\t: tr::lng_settings_save();\n\t\t}) | rpl::flatten_latest();\n\t\tbox->addButton(std::move(text), [=] {\n\t\t\tif (state->saving) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto add = QVector<MTPInputSavedStarGift>();\n\t\t\tauto remove = QVector<MTPInputSavedStarGift>();\n\t\t\tconst auto &changes = state->changes.current();\n\t\t\tfor (const auto &id : changes.added) {\n\t\t\t\tadd.push_back(Api::InputSavedStarGiftId(id));\n\t\t\t}\n\t\t\tfor (const auto &id : changes.removed) {\n\t\t\t\tremove.push_back(Api::InputSavedStarGiftId(id));\n\t\t\t}\n\t\t\tif (add.empty() && remove.empty()) {\n\t\t\t\tbox->closeBox();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->saving = true;\n\t\t\tconst auto session = &_window->session();\n\t\t\tusing Flag = MTPpayments_UpdateStarGiftCollection::Flag;\n\t\t\tsession->api().request(\n\t\t\t\tMTPpayments_UpdateStarGiftCollection(\n\t\t\t\t\tMTP_flags(Flag()\n\t\t\t\t\t\t| (add.isEmpty() ? Flag() : Flag::f_add_stargift)\n\t\t\t\t\t\t| (remove.isEmpty()\n\t\t\t\t\t\t\t? Flag()\n\t\t\t\t\t\t\t: Flag::f_delete_stargift)),\n\t\t\t\t\t_peer->input(),\n\t\t\t\t\tMTP_int(id),\n\t\t\t\t\tMTPstring(),\n\t\t\t\t\tMTP_vector<MTPInputSavedStarGift>(remove),\n\t\t\t\t\tMTP_vector<MTPInputSavedStarGift>(add),\n\t\t\t\t\tMTPVector<MTPInputSavedStarGift>())\n\t\t\t).done([=] {\n\t\t\t\tif (const auto strong = weakBox.get()) {\n\t\t\t\t\tstate->saving = false;\n\t\t\t\t\tstrong->closeBox();\n\t\t\t\t}\n\t\t\t\tsession->data().notifyGiftsUpdate(base::duplicate(changes));\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->reloadCollection(id);\n\t\t\t\t}\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tif (const auto strong = weakBox.get()) {\n\t\t\t\t\tstate->saving = false;\n\t\t\t\t\tif (!Ui::ShowGiftErrorToast(strong->uiShow(), error)) {\n\t\t\t\t\t\tstrong->uiShow()->showToast(error.type());\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}).send();\n\t\t});\n\t}));\n}\n\nvoid InnerWidget::refreshCollectionsTabs() {\n\tif (_collections.empty() || _addingToCollectionId) {\n\t\tif (base::take(_collectionsTabs)) {\n\t\t\tresizeToWidth(width());\n\t\t}\n\t\treturn;\n\t}\n\n\tauto tabs = std::vector<Ui::SubTabs::Tab>();\n\ttabs.push_back({\n\t\t.id = u\"all\"_q,\n\t\t.text = tr::lng_gift_collection_all(tr::now, tr::marked),\n\t});\n\tfor (const auto &collection : _collections) {\n\t\tauto &per = _perCollection[collection.id];\n\t\tif (!per.allLoaded) {\n\t\t\tper.total = collection.count;\n\t\t}\n\n\t\tauto title = TextWithEntities();\n\t\tif (collection.icon) {\n\t\t\ttitle.append(\n\t\t\t\tData::SingleCustomEmoji(collection.icon)\n\t\t\t).append(' ');\n\t\t}\n\t\ttitle.append(collection.title);\n\t\ttabs.push_back({\n\t\t\t.id = QString::number(collection.id),\n\t\t\t.text = std::move(title),\n\t\t});\n\t}\n\tif (_peer->canManageGifts()) {\n\t\ttabs.push_back({\n\t\t\t.id = u\"add\"_q,\n\t\t\t.text = { '+' + tr::lng_gift_collection_add(tr::now) },\n\t\t});\n\t}\n\tconst auto context = Core::TextContext({\n\t\t.session = &_window->session(),\n\t});\n\tif (!_collectionsTabs) {\n\t\tconst auto selectedId = _descriptor.current().collectionId;\n\t\tconst auto selected = (selectedId > 0\n\t\t\t&& ranges::contains(\n\t\t\t\t_collections,\n\t\t\t\tselectedId,\n\t\t\t\t&Data::GiftCollection::id))\n\t\t\t? QString::number(selectedId)\n\t\t\t: u\"all\"_q;\n\t\tconst auto tabsCount = tabs.size();\n\t\t_collectionsTabs = std::make_unique<Ui::SubTabs>(\n\t\t\tthis,\n\t\t\tst::collectionSubTabs,\n\t\t\tUi::SubTabs::Options{ .selected = selected, .centered = true },\n\t\t\tstd::move(tabs),\n\t\t\tcontext);\n\t\t_collectionsTabs->setPinnedInterval(0, 1);\n\t\t_collectionsTabs->setPinnedInterval(tabsCount - 1, tabsCount);\n\t\t_collectionsTabs->show();\n\n\t\t_collectionsTabs->activated(\n\t\t) | rpl::on_next([=](const QString &id) {\n\t\t\tif (id == u\"add\"_q) {\n\t\t\t\tconst auto added = [=](MTPStarGiftCollection result) {\n\t\t\t\t\tcollectionAdded(result);\n\t\t\t\t};\n\t\t\t\t_window->uiShow()->show(Box(\n\t\t\t\t\tNewCollectionBox,\n\t\t\t\t\t_window,\n\t\t\t\t\tpeer(),\n\t\t\t\t\tData::SavedStarGiftId(),\n\t\t\t\t\tadded));\n\t\t\t} else {\n\t\t\t\t_collectionsTabs->setActiveTab(id);\n\n\t\t\t\tauto now = _descriptor.current();\n\t\t\t\tnow.collectionId = (id == u\"all\"_q) ? 0 : id.toInt();\n\t\t\t\t_descriptorChanges.fire(std::move(now));\n\t\t\t}\n\t\t}, _collectionsTabs->lifetime());\n\n\t\t_collectionsTabs->contextMenuRequests(\n\t\t) | rpl::on_next([=](const QString &id) {\n\t\t\tif (id == u\"add\"_q\n\t\t\t\t|| id == u\"all\"_q\n\t\t\t\t|| !_peer->canManageGifts()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tshowMenuForCollection(id.toInt());\n\t\t}, _collectionsTabs->lifetime());\n\n\t\tusing ReorderUpdate = Ui::SubTabs::ReorderUpdate;\n\t\t_collectionsTabs->reorderUpdates(\n\t\t) | rpl::on_next([=](const ReorderUpdate &update) {\n\t\t\tif (update.state == ReorderUpdate::State::Applied) {\n\t\t\t\treorderCollectionsLocally(update);\n\t\t\t}\n\t\t}, _collectionsTabs->lifetime());\n\t} else {\n\t\tconst auto tabsCount = tabs.size();\n\t\t_collectionsTabs->setTabs(std::move(tabs), context);\n\t\t_collectionsTabs->clearPinnedIntervals();\n\t\t_collectionsTabs->setPinnedInterval(0, 1);\n\t\tif (_peer->canManageGifts()) {\n\t\t\t_collectionsTabs->setPinnedInterval(tabsCount - 1, tabsCount);\n\t\t}\n\t}\n\tresizeToWidth(width());\n}\n\nvoid InnerWidget::collectionRenamed(int id, QString name) {\n\tconst auto i = ranges::find(_collections, id, &Data::GiftCollection::id);\n\tif (i != end(_collections)) {\n\t\ti->title = name;\n\t\trefreshCollectionsTabs();\n\t}\n}\n\nvoid InnerWidget::removeGiftFromCollection(\n\t\tData::SavedStarGiftId giftId,\n\t\tint collectionId) {\n\tauto changes = Data::GiftsUpdate{\n\t\t.peer = _peer,\n\t\t.collectionId = collectionId,\n\t\t.removed = { giftId },\n\t};\n\tusing Flag = MTPpayments_UpdateStarGiftCollection::Flag;\n\t_window->session().api().request(\n\t\tMTPpayments_UpdateStarGiftCollection(\n\t\t\tMTP_flags(Flag::f_delete_stargift),\n\t\t\t_peer->input(),\n\t\t\tMTP_int(collectionId),\n\t\t\tMTPstring(),\n\t\t\tMTP_vector<MTPInputSavedStarGift>({\n\t\t\t\tApi::InputSavedStarGiftId(giftId)\n\t\t\t}),\n\t\t\tMTPVector<MTPInputSavedStarGift>(),\n\t\t\tMTPVector<MTPInputSavedStarGift>())\n\t).done([=](const MTPStarGiftCollection &result) {\n\t\t_window->session().data().notifyGiftsUpdate(base::duplicate(changes));\n\n\t\tconst auto i = ranges::find(\n\t\t\t_collections,\n\t\t\tcollectionId,\n\t\t\t&Data::GiftCollection::id);\n\t\tif (i != end(_collections)) {\n\t\t\tconst auto updated = FromTL(&_window->session(), result);\n\t\t\t*i = updated;\n\n\t\t\tauto &per = _perCollection[collectionId];\n\t\t\tper.total = updated.count;\n\n\t\t\tif (_descriptor.current().collectionId == collectionId) {\n\t\t\t\tconst auto it = ranges::find(\n\t\t\t\t\t*_list,\n\t\t\t\t\tgiftId,\n\t\t\t\t\t[](const Entry &entry) { return entry.gift.manageId; });\n\t\t\t\tif (it != end(*_list)) {\n\t\t\t\t\t_list->erase(it);\n\t\t\t\t\tif (_entries->total > 0) {\n\t\t\t\t\t\t--_entries->total;\n\t\t\t\t\t}\n\t\t\t\t\trefreshButtons();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst auto giftIt = ranges::find(\n\t\t\t\tper.list,\n\t\t\t\tgiftId,\n\t\t\t\t[](const Entry &entry) { return entry.gift.manageId; });\n\t\t\tif (giftIt != end(per.list)) {\n\t\t\t\tper.list.erase(giftIt);\n\t\t\t}\n\n\t\t\tif (_addingToCollectionId == collectionId) {\n\t\t\t\tauto currentChanges = _collectionChanges.current();\n\t\t\t\tcurrentChanges.removed.push_back(giftId);\n\t\t\t\t_collectionChanges = std::move(currentChanges);\n\t\t\t}\n\n\t\t\trefreshCollectionsTabs();\n\t\t}\n\t}).fail([=, show = _window->uiShow()](const MTP::Error &error) {\n\t\tif (!Ui::ShowGiftErrorToast(show, error)) {\n\t\t\tshow->showToast(error.type());\n\t\t}\n\t}).send();\n}\n\nvoid InnerWidget::collectionRemoved(int id) {\n\tauto now = _descriptor.current();\n\tif (now.collectionId == id) {\n\t\tnow.collectionId = 0;\n\t\t_descriptorChanges.fire(std::move(now));\n\t}\n\tAssert(_entries != &_perCollection[id]);\n\t_perCollection.erase(id);\n\tconst auto removeFrom = [&](Entries &entries) {\n\t\tfor (auto &entry : entries.list) {\n\t\t\tentry.gift.collectionIds.erase(\n\t\t\t\tranges::remove(entry.gift.collectionIds, id),\n\t\t\t\tend(entry.gift.collectionIds));\n\t\t}\n\t};\n\tremoveFrom(_all);\n\tfor (auto &[_, entries] : _perCollection) {\n\t\tremoveFrom(entries);\n\t}\n\n\tconst auto i = ranges::find(_collections, id, &Data::GiftCollection::id);\n\tif (i != end(_collections)) {\n\t\t_collections.erase(i);\n\t\trefreshCollectionsTabs();\n\t}\n}\n\nint InnerWidget::resizeGetHeight(int width) {\n\tconst auto padding = st::giftBoxPadding;\n\tconst auto count = int(_list->size());\n\tconst auto available = width - padding.left() - padding.right();\n\tconst auto skipw = st::giftBoxGiftSkip.x();\n\t_perRow = std::min(\n\t\t(available + skipw) / (_singleMin.width() + skipw),\n\t\tstd::max(count, 1));\n\tif (!_perRow) {\n\t\treturn 0;\n\t}\n\tauto result = 0;\n\tif (_collectionsTabs && !_collectionsTabs->isHidden()) {\n\t\tresult += padding.top();\n\t\t_collectionsTabs->resizeToWidth(width);\n\t\t_collectionsTabs->move(0, result);\n\t\tresult += _collectionsTabs->height();\n\t} else {\n\t\tresult += padding.bottom();\n\t}\n\n\tconst auto singlew = std::min(\n\t\t((available + skipw) / _perRow) - skipw,\n\t\t2 * _singleMin.width());\n\tAssert(singlew >= _singleMin.width());\n\tconst auto singleh = _singleMin.height();\n\n\t_single = QSize(singlew, singleh);\n\tconst auto rows = (count + _perRow - 1) / _perRow;\n\tconst auto rowsPerCount = rows\n\t\t? rows\n\t\t: ((std::min(_entries->total, kPerPage) + _perRow - 1) / _perRow);\n\tconst auto skiph = st::giftBoxGiftSkip.y();\n\n\tconst auto resultPerCount = result\n\t\t+ (rowsPerCount\n\t\t\t? (padding.bottom() + rowsPerCount * (singleh + skiph) - skiph)\n\t\t\t: 0);\n\tresult += rows\n\t\t? (padding.bottom() + rows * (singleh + skiph) - skiph)\n\t\t: 0;\n\n\tif (const auto about = _about.get()) {\n\t\tconst auto margin = st::giftListAboutMargin;\n\t\tabout->resizeToWidth(width - margin.left() - margin.right());\n\t\tabout->moveToLeft(margin.left(), result + margin.top());\n\t\tresult += margin.top() + about->height() + margin.bottom();\n\t}\n\n\treturn std::max(result, resultPerCount);\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\tauto state = std::make_unique<ListState>();\n\tmemento->setListState(std::move(state));\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\tif (const auto state = memento->listState()) {\n\n\t}\n}\n\nvoid InnerWidget::fillMenu(const Ui::Menu::MenuCallback &addAction) {\n\tconst auto canManage = _peer->canManageGifts();\n\tconst auto descriptor = _descriptor.current();\n\tconst auto filter = descriptor.filter;\n\tconst auto change = [=](Fn<void(Filter&)> update) {\n\t\tauto now = _descriptor.current();\n\t\tupdate(now.filter);\n\t\t_descriptorChanges.fire(std::move(now));\n\t};\n\n\tconst auto collectionId = descriptor.collectionId;\n\tif (!collectionId) {\n\t\tif (filter.sortByValue) {\n\t\t\taddAction(tr::lng_peer_gifts_filter_by_date(tr::now), [=] {\n\t\t\t\tchange([](Filter &filter) { filter.sortByValue = false; });\n\t\t\t}, &st::menuIconSchedule);\n\t\t} else {\n\t\t\taddAction(tr::lng_peer_gifts_filter_by_value(tr::now), [=] {\n\t\t\t\tchange([](Filter &filter) { filter.sortByValue = true; });\n\t\t\t}, &st::menuIconEarn);\n\t\t}\n\t\tif (canManage && !_addingToCollectionId) {\n\t\t\tconst auto peer = _peer;\n\t\t\tconst auto weak = base::make_weak(_window);\n\t\t\taddAction(tr::lng_gift_collection_add(tr::now), [=] {\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tconst auto added = [=](MTPStarGiftCollection result) {\n\t\t\t\t\t\tcollectionAdded(result);\n\t\t\t\t\t};\n\t\t\t\t\tstrong->uiShow()->show(Box(\n\t\t\t\t\t\tNewCollectionBox,\n\t\t\t\t\t\tstrong,\n\t\t\t\t\t\tpeer,\n\t\t\t\t\t\tData::SavedStarGiftId(),\n\t\t\t\t\t\tcrl::guard(this, added)));\n\t\t\t\t}\n\t\t\t}, &st::menuIconAddToFolder);\n\t\t}\n\t} else if (canManage) {\n\t\taddAction(tr::lng_gift_collection_add_button(tr::now), [=] {\n\t\t\teditCollectionGifts(collectionId);\n\t\t}, &st::menuIconGiftPremium);\n\n\t\taddAction({\n\t\t\t.text = tr::lng_gift_collection_delete(tr::now),\n\t\t\t.handler = [=] { confirmDeleteCollection(collectionId); },\n\t\t\t.icon = &st::menuIconDeleteAttention,\n\t\t\t.isAttention = true,\n\t\t});\n\t}\n\n\tif (canManage || !collectionId) {\n\t\taddAction({ .isSeparator = true });\n\t}\n\n\taddAction(tr::lng_peer_gifts_filter_unlimited(tr::now), [=] {\n\t\tchange([](Filter &filter) {\n\t\t\tfilter.skipUnlimited = !filter.skipUnlimited;\n\t\t\tif (filter.skipUnlimited\n\t\t\t\t&& filter.skipLimited\n\t\t\t\t&& filter.skipUnique) {\n\t\t\t\tfilter.skipLimited = false;\n\t\t\t}\n\t\t});\n\t}, filter.skipUnlimited ? nullptr : &st::mediaPlayerMenuCheck);\n\taddAction(tr::lng_peer_gifts_filter_limited(tr::now), [=] {\n\t\tchange([](Filter &filter) {\n\t\t\tfilter.skipLimited = !filter.skipLimited;\n\t\t\tif (filter.skipUpgradable\n\t\t\t\t&& filter.skipUnlimited\n\t\t\t\t&& filter.skipLimited\n\t\t\t\t&& filter.skipUnique) {\n\t\t\t\tfilter.skipUnlimited = false;\n\t\t\t}\n\t\t});\n\t}, filter.skipLimited ? nullptr : &st::mediaPlayerMenuCheck);\n\taddAction(tr::lng_peer_gifts_filter_upgradable(tr::now), [=] {\n\t\tchange([](Filter &filter) {\n\t\t\tfilter.skipUpgradable = !filter.skipUpgradable;\n\t\t\tif (filter.skipUpgradable\n\t\t\t\t&& filter.skipUnlimited\n\t\t\t\t&& filter.skipLimited\n\t\t\t\t&& filter.skipUnique) {\n\t\t\t\tfilter.skipUnlimited = false;\n\t\t\t}\n\t\t});\n\t}, filter.skipUpgradable ? nullptr: &st::mediaPlayerMenuCheck);\n\taddAction(tr::lng_peer_gifts_filter_unique(tr::now), [=] {\n\t\tchange([](Filter &filter) {\n\t\t\tfilter.skipUnique = !filter.skipUnique;\n\t\t\tif (filter.skipUpgradable\n\t\t\t\t&& filter.skipUnlimited\n\t\t\t\t&& filter.skipLimited\n\t\t\t\t&& filter.skipUnique) {\n\t\t\t\tfilter.skipUnlimited = false;\n\t\t\t}\n\t\t});\n\t}, filter.skipUnique ? nullptr : &st::mediaPlayerMenuCheck);\n\n\tif (canManage) {\n\t\taddAction({ .isSeparator = true });\n\n\t\taddAction(tr::lng_peer_gifts_filter_saved(tr::now), [=] {\n\t\t\tchange([](Filter &filter) {\n\t\t\t\tfilter.skipSaved = !filter.skipSaved;\n\t\t\t\tif (filter.skipSaved && filter.skipUnsaved) {\n\t\t\t\t\tfilter.skipUnsaved = false;\n\t\t\t\t}\n\t\t\t});\n\t\t}, filter.skipSaved ? nullptr : &st::mediaPlayerMenuCheck);\n\t\taddAction(tr::lng_peer_gifts_filter_unsaved(tr::now), [=] {\n\t\t\tchange([](Filter &filter) {\n\t\t\t\tfilter.skipUnsaved = !filter.skipUnsaved;\n\t\t\t\tif (filter.skipSaved && filter.skipUnsaved) {\n\t\t\t\t\tfilter.skipSaved = false;\n\t\t\t\t}\n\t\t\t});\n\t\t}, filter.skipUnsaved ? nullptr : &st::mediaPlayerMenuCheck);\n\t}\n}\n\nvoid InnerWidget::reorderCollectionsLocally(\n\t\tconst Ui::SubTabs::ReorderUpdate &update) {\n\tif (!_collectionsTabs || !_peer->canManageGifts()) {\n\t\treturn;\n\t}\n\n\tconst auto collectionId = update.id.toInt();\n\tif (collectionId <= 0) {\n\t\treturn;\n\t}\n\n\tconst auto it = ranges::find(\n\t\t_collections,\n\t\tcollectionId,\n\t\t&Data::GiftCollection::id);\n\tif (it == _collections.end()) {\n\t\treturn;\n\t}\n\n\tconst auto collection = *it;\n\t_collections.erase(it);\n\n\tconst auto newPos = std::max(\n\t\t0,\n\t\tstd::min(update.newPosition - 1, int(_collections.size())));\n\t_collections.insert(_collections.begin() + newPos, collection);\n\n\t_pendingCollectionReorder = true;\n}\n\nvoid InnerWidget::flushCollectionReorder() {\n\tif (!_pendingCollectionReorder || !_peer->canManageGifts()) {\n\t\treturn;\n\t}\n\n\tauto order = QVector<MTPint>();\n\tfor (const auto &c : _collections) {\n\t\torder.push_back(MTP_int(c.id));\n\t}\n\n\t_api.request(MTPpayments_ReorderStarGiftCollections(\n\t\t_peer->input(),\n\t\tMTP_vector<MTPint>(order)\n\t)).fail([show = _window->uiShow()](const MTP::Error &error) {\n\t\tshow->showToast(error.type());\n\t}).send();\n\n\t_pendingCollectionReorder = false;\n}\n\nvoid InnerWidget::mousePressEvent(QMouseEvent *e) {\n\tif (e->button() != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tconst auto index = giftFromGlobalPos(e->globalPos());\n\tif (index < 0 || index >= _list->size()) {\n\t\treturn;\n\t}\n\t_pressedIndex = index;\n\tconst auto collectionId = _descriptor.current().collectionId;\n\tconst auto canDrag = !_addingToCollectionId\n\t\t&& _peer->canManageGifts()\n\t\t&& _list->size() > 1\n\t\t&& (collectionId\n\t\t\t|| (!collectionId\n\t\t\t\t&& index < _list->size()\n\t\t\t\t&& (*_list)[index].gift.pinned));\n\tif (canDrag) {\n\t\tif (isDraggedAnimating()) {\n\t\t\treturn;\n\t\t}\n\t\t_dragging.enabled = false;\n\t\t_dragging.index = index;\n\t\t_dragging.point = mapFromGlobal(e->globalPos()) - posFromIndex(index);\n\t\t_dragging.startPos = e->globalPos();\n\t\tgrabMouse();\n\t\te->accept();\n\t\treturn;\n\t}\n}\n\nvoid InnerWidget::mouseMoveEvent(QMouseEvent *e) {\n\tif (_dragging.index < 0) {\n\t\tupdateSelected();\n\t\treturn;\n\t}\n\n\tconst auto draggedAnimating = isDraggedAnimating();\n\n\tif (!_dragging.enabled) {\n\t\tconst auto distance\n\t\t\t= (e->globalPos() - _dragging.startPos).manhattanLength();\n\t\tif (distance > QApplication::startDragDistance()) {\n\t\t\t_dragging.enabled = true;\n\t\t}\n\t}\n\n\tif (!_dragging.enabled) {\n\t\treturn;\n\t}\n\n\te->accept();\n\n\tconst auto currentPos = e->globalPos();\n\tauto selected = giftFromGlobalPos(currentPos);\n\tconst auto collectionId = _descriptor.current().collectionId;\n\tif (!collectionId && selected >= 0) {\n\t\tauto pinnedCount = 0;\n\t\tfor (const auto &entry : *_list) {\n\t\t\tif (entry.gift.pinned) {\n\t\t\t\t++pinnedCount;\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (selected >= pinnedCount) {\n\t\t\tselected = pinnedCount - 1;\n\t\t}\n\t}\n\tif (selected >= 0 && !draggedAnimating) {\n\t\t_dragging.lastSelected = selected;\n\t}\n\n\tif (_dragging.index >= 0\n\t\t&& _dragging.index < _list->size()\n\t\t&& _dragging.lastSelected >= 0\n\t\t&& !draggedAnimating) {\n\t\tfor (auto i = 0; i < _list->size(); i++) {\n\t\t\tif (i == _dragging.index) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tauto &entry = _shiftAnimations[i];\n\t\t\tconst auto wasShift = entry.shift;\n\t\t\tconst auto dragIndex = _dragging.index;\n\t\t\tconst auto targetIndex = _dragging.lastSelected;\n\n\t\t\tif (dragIndex < targetIndex) {\n\t\t\t\tif (i > dragIndex && i <= targetIndex) {\n\t\t\t\t\tentry.shift = -1;\n\t\t\t\t} else {\n\t\t\t\t\tentry.shift = 0;\n\t\t\t\t}\n\t\t\t} else if (dragIndex > targetIndex) {\n\t\t\t\tif (i >= targetIndex && i < dragIndex) {\n\t\t\t\t\tentry.shift = 1;\n\t\t\t\t} else {\n\t\t\t\t\tentry.shift = 0;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tentry.shift = 0;\n\t\t\t}\n\t\t\tif (wasShift != entry.shift) {\n\t\t\t\tconst auto fromPoint = posFromIndex(i + wasShift);\n\t\t\t\tconst auto toPoint = posFromIndex(i + entry.shift);\n\t\t\t\tconst auto toX = float64(toPoint.x());\n\t\t\t\tconst auto toY = float64(toPoint.y());\n\t\t\t\tconst auto ratio = [&] {\n\t\t\t\t\tconst auto fromX = entry.xAnimation.value(toX);\n\t\t\t\t\tconst auto ratioX = std::min(toX, fromX)\n\t\t\t\t\t\t/ std::max(toX, fromX);\n\t\t\t\t\tconst auto fromY = entry.yAnimation.value(toY);\n\t\t\t\t\tconst auto ratioY = std::min(toY, fromY)\n\t\t\t\t\t\t/ std::max(toY, fromY);\n\t\t\t\t\treturn (ratioX == 1.)\n\t\t\t\t\t\t? ratioY\n\t\t\t\t\t\t: (ratioY == 1.)\n\t\t\t\t\t\t? ratioX\n\t\t\t\t\t\t: std::max(ratioX, ratioY);\n\t\t\t\t}();\n\t\t\t\tif (!entry.xAnimation.animating()) {\n\t\t\t\t\tentry.xAnimation.stop();\n\t\t\t\t\tentry.xAnimation.start(\n\t\t\t\t\t\t[this, i](float64 value) {\n\t\t\t\t\t\t\tfor (auto &view : _views) {\n\t\t\t\t\t\t\t\tif (view.index == i && view.button) {\n\t\t\t\t\t\t\t\t\tview.button->moveToLeft(\n\t\t\t\t\t\t\t\t\t\tvalue,\n\t\t\t\t\t\t\t\t\t\tview.button->y());\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\tfromPoint.x(),\n\t\t\t\t\t\ttoX,\n\t\t\t\t\t\tst::fadeWrapDuration);\n\t\t\t\t} else {\n\t\t\t\t\tentry.xAnimation.change(\n\t\t\t\t\t\ttoX,\n\t\t\t\t\t\tst::fadeWrapDuration * (1. - ratio),\n\t\t\t\t\t\tanim::linear);\n\t\t\t\t}\n\t\t\t\tif (!entry.yAnimation.animating()) {\n\t\t\t\t\tentry.yAnimation.stop();\n\t\t\t\t\tentry.yAnimation.start(\n\t\t\t\t\t\t[this, i](float64 value) {\n\t\t\t\t\t\t\tfor (auto &view : _views) {\n\t\t\t\t\t\t\t\tif (view.index == i && view.button) {\n\t\t\t\t\t\t\t\t\tview.button->moveToLeft(\n\t\t\t\t\t\t\t\t\t\tview.button->x(),\n\t\t\t\t\t\t\t\t\t\tvalue);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\tfromPoint.y(),\n\t\t\t\t\t\ttoY,\n\t\t\t\t\t\tst::fadeWrapDuration);\n\t\t\t\t} else {\n\t\t\t\t\tentry.yAnimation.change(\n\t\t\t\t\t\ttoY,\n\t\t\t\t\t\tst::fadeWrapDuration * (1. - ratio),\n\t\t\t\t\t\tanim::linear);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor (auto &view : _views) {\n\t\t\tif (view.index >= 0 && view.button) {\n\t\t\t\tauto pos = posFromIndex(view.index);\n\n\t\t\t\tif (view.index == _dragging.index) {\n\t\t\t\t\tpos = mapFromGlobal(currentPos) - _dragging.point;\n\t\t\t\t} else {\n\t\t\t\t\tconst auto it = _shiftAnimations.find(view.index);\n\t\t\t\t\tif (it != _shiftAnimations.end()) {\n\t\t\t\t\t\tconst auto &entry = it->second;\n\t\t\t\t\t\tconst auto toPos = posFromIndex(view.index + entry.shift);\n\t\t\t\t\t\tpos = QPoint(\n\t\t\t\t\t\t\tentry.xAnimation.value(toPos.x()),\n\t\t\t\t\t\t\tentry.yAnimation.value(toPos.y()));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tview.button->moveToLeft(pos.x(), pos.y());\n\t\t\t}\n\t\t}\n\n\t\tif (_draggedView && _draggedView->button) {\n\t\t\tauto pos = mapFromGlobal(currentPos) - _dragging.point;\n\t\t\t_draggedView->button->moveToLeft(pos.x(), pos.y());\n\t\t\t_draggedView->button->raise();\n\t\t}\n\n\t\tupdate();\n\t}\n\n\tcheckForScrollAnimation();\n}\n\nvoid InnerWidget::mouseReleaseEvent(QMouseEvent *e) {\n\tconst auto pressedIndex = base::take(_pressedIndex);\n\tif (mouseGrabber() == this) {\n\t\treleaseMouse();\n\t}\n\n\tif (_dragging.enabled && _dragging.index >= 0 && !isDraggedAnimating()) {\n\t\tconst auto fromPos = mapFromGlobal(e->globalPos()) - _dragging.point;\n\t\tconst auto toPos = posFromIndex(_dragging.lastSelected);\n\t\tconst auto wasPosition = _dragging.index;\n\t\tconst auto nowPosition = _dragging.lastSelected;\n\t\tconst auto finish = [=, this] {\n\t\t\tbase::reorder(*_list, wasPosition, nowPosition);\n\t\t\tfor (auto &view : _views) {\n\t\t\t\tview.index = base::reorder_index(\n\t\t\t\t\tview.index,\n\t\t\t\t\twasPosition,\n\t\t\t\t\tnowPosition);\n\t\t\t}\n\t\t\tif (_draggedView) {\n\t\t\t\t_draggedView->index = nowPosition;\n\t\t\t}\n\t\t\trequestReorder(wasPosition, nowPosition);\n\t\t\t_dragging = {};\n\t\t\t_shiftAnimations.clear();\n\t\t\trefreshButtons();\n\t\t};\n\t\tauto &entry = _shiftAnimations[_dragging.index];\n\t\tentry.xAnimation.stop();\n\t\tentry.yAnimation.stop();\n\t\tentry.xAnimation.start(\n\t\t\t[finish, toPos, this](float64 value) {\n\t\t\t\tconst auto index = _dragging.index;\n\t\t\t\tif (std::abs(value - toPos.x()) < 1.0\n\t\t\t\t\t&& index >= 0\n\t\t\t\t\t&& !_shiftAnimations[index].yAnimation.animating()) {\n\t\t\t\t\tfinish();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfor (auto &view : _views) {\n\t\t\t\t\tif (view.index == index && view.button) {\n\t\t\t\t\t\tview.button->moveToLeft(value, view.button->y());\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (_draggedView\n\t\t\t\t\t&& _draggedView->index == index\n\t\t\t\t\t&& _draggedView->button) {\n\t\t\t\t\t_draggedView->button->moveToLeft(\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t\t_draggedView->button->y());\n\t\t\t\t}\n\t\t\t},\n\t\t\tfromPos.x(),\n\t\t\ttoPos.x(),\n\t\t\tst::fadeWrapDuration);\n\t\tentry.yAnimation.start(\n\t\t\t[finish, toPos, this](float64 value) {\n\t\t\t\tconst auto index = _dragging.index;\n\t\t\t\tif (std::abs(value - toPos.y()) < 1.0\n\t\t\t\t\t&& index >= 0\n\t\t\t\t\t&& !_shiftAnimations[index].xAnimation.animating()) {\n\t\t\t\t\tfinish();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfor (auto &view : _views) {\n\t\t\t\t\tif (view.index == index && view.button) {\n\t\t\t\t\t\tview.button->moveToLeft(view.button->x(), value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (_draggedView\n\t\t\t\t\t&& _draggedView->index == index\n\t\t\t\t\t&& _draggedView->button) {\n\t\t\t\t\t_draggedView->button->moveToLeft(\n\t\t\t\t\t\t_draggedView->button->x(),\n\t\t\t\t\t\tvalue);\n\t\t\t\t}\n\t\t\t},\n\t\t\tfromPos.y(),\n\t\t\ttoPos.y(),\n\t\t\tst::fadeWrapDuration);\n\t} else {\n\t\tconst auto index = giftFromGlobalPos(e->globalPos());\n\t\t_dragging = {};\n\t\t_shiftAnimations.clear();\n\t\t_draggedView = nullptr;\n\t\tif (pressedIndex >= 0 && index == pressedIndex) {\n\t\t\tshowGift(index);\n\t\t}\n\t\trefreshButtons();\n\t}\n}\n\nvoid InnerWidget::updateSelected() {\n\tif (_dragging.enabled) {\n\t\treturn;\n\t}\n\tconst auto selected = giftFromGlobalPos(QCursor::pos());\n\tif (_selected != selected) {\n\t\t_selected = selected;\n\t}\n}\n\nint InnerWidget::giftFromGlobalPos(const QPoint &p) const {\n\tconst auto l = mapFromGlobal(p);\n\tif (!_perRow) {\n\t\treturn -1;\n\t}\n\tconst auto padding = st::giftBoxPadding;\n\tconst auto vskip = (_collectionsTabs && !_collectionsTabs->isHidden())\n\t\t? (padding.top() + _collectionsTabs->height() + padding.top())\n\t\t: padding.bottom();\n\tconst auto row = (l.y() >= vskip)\n\t\t? ((l.y() - vskip) / (_single.height() + st::giftBoxGiftSkip.y()))\n\t\t: -1;\n\tconst auto available = width() - padding.left() - padding.right();\n\tconst auto skipw = st::giftBoxGiftSkip.x();\n\tconst auto fullw = _perRow * (_single.width() + skipw) - skipw;\n\tconst auto left = padding.left() + (available - fullw) / 2;\n\tconst auto col = (l.x() >= left)\n\t\t? ((l.x() - left) / (_single.width() + skipw))\n\t\t: -1;\n\tif (row >= 0 && col >= 0 && col < _perRow) {\n\t\tconst auto result = row * _perRow + col;\n\t\treturn (result < _list->size()) ? result : -1;\n\t}\n\treturn -1;\n}\n\nQPoint InnerWidget::posFromIndex(int index) const {\n\tif (!_perRow) {\n\t\treturn {};\n\t}\n\tconst auto padding = st::giftBoxPadding;\n\tconst auto vskip = (_collectionsTabs && !_collectionsTabs->isHidden())\n\t\t? (padding.top() + _collectionsTabs->height() + padding.top())\n\t\t: padding.bottom();\n\tconst auto available = width() - padding.left() - padding.right();\n\tconst auto skipw = st::giftBoxGiftSkip.x();\n\tconst auto skiph = st::giftBoxGiftSkip.y();\n\tconst auto fullw = _perRow * (_single.width() + skipw) - skipw;\n\tconst auto left = padding.left() + (available - fullw) / 2;\n\tconst auto extend = _delegate.buttonExtend();\n\treturn {\n\t\tleft + (index % _perRow) * (_single.width() + skipw) - extend.left(),\n\t\tvskip + (index / _perRow) * (_single.height() + skiph) - extend.top(),\n\t};\n}\n\nbool InnerWidget::isDraggedAnimating() const {\n\tif (_dragging.index < 0) {\n\t\treturn false;\n\t}\n\tconst auto it = _shiftAnimations.find(_dragging.index);\n\treturn (it == _shiftAnimations.end())\n\t\t? false\n\t\t: (it->second.xAnimation.animating()\n\t\t\t|| it->second.yAnimation.animating());\n}\n\nvoid InnerWidget::requestReorder(int fromIndex, int toIndex) {\n\tif (fromIndex == toIndex || !_peer->canManageGifts()) {\n\t\treturn;\n\t}\n\tconst auto collectionId = _descriptor.current().collectionId;\n\tif (collectionId) {\n\t\tauto order = QVector<MTPInputSavedStarGift>();\n\t\torder.reserve(_list->size());\n\t\tfor (const auto &entry : *_list) {\n\t\t\torder.push_back(Api::InputSavedStarGiftId(entry.gift.manageId));\n\t\t}\n\n\t\t_api.request(\n\t\t\tMTPpayments_UpdateStarGiftCollection(\n\t\t\t\tMTP_flags(MTPpayments_UpdateStarGiftCollection::Flag::f_order),\n\t\t\t\t_peer->input(),\n\t\t\t\tMTP_int(collectionId),\n\t\t\t\tMTPstring(),\n\t\t\t\tMTPVector<MTPInputSavedStarGift>(),\n\t\t\t\tMTPVector<MTPInputSavedStarGift>(),\n\t\t\t\tMTP_vector<MTPInputSavedStarGift>(order))\n\t\t).done([=] {\n\t\t\tconst auto i = ranges::find(\n\t\t\t\t_collections,\n\t\t\t\tcollectionId,\n\t\t\t\t&Data::GiftCollection::id);\n\t\t\tif (i != end(_collections) && !_list->empty()) {\n\t\t\t\ti->icon = (*_list)[0].gift.info.document;\n\t\t\t\trefreshCollectionsTabs();\n\t\t\t}\n\t\t}).fail([show = _window->uiShow()](const MTP::Error &error) {\n\t\t\tif (!Ui::ShowGiftErrorToast(show, error)) {\n\t\t\t\tshow->showToast(error.type());\n\t\t\t}\n\t\t}).send();\n\t} else {\n\t\t_window->session().recentSharedGifts().reorderPinned(\n\t\t\t_window->uiShow(),\n\t\t\t_peer,\n\t\t\tfromIndex,\n\t\t\ttoIndex);\n\t}\n}\n\nvoid InnerWidget::reorderCollections(\n\t\tconst Ui::SubTabs::ReorderUpdate &update) {\n\treorderCollectionsLocally(update);\n\tflushCollectionReorder();\n}\n\nvoid InnerWidget::focusOutEvent(QFocusEvent *e) {\n\tif (_dragging.enabled) {\n\t\tcancelDragging();\n\t}\n\tBoxContentDivider::focusOutEvent(e);\n}\n\nvoid InnerWidget::cancelDragging() {\n\tif (mouseGrabber() == this) {\n\t\treleaseMouse();\n\t}\n\t_scrollAnimation.stop();\n\t_dragging = {};\n\t_pressedIndex = -1;\n\t_shiftAnimations.clear();\n\tif (_draggedView) {\n\t\t_draggedView = nullptr;\n\t}\n\trefreshButtons();\n}\n\nvoid InnerWidget::updateScrollCallback() {\n\tif (!_scroll) {\n\t\treturn;\n\t}\n\tconst auto delta = deltaFromEdge();\n\tconst auto oldTop = _scroll->scrollTop();\n\t_scroll->scrollToY(oldTop + delta);\n\tconst auto newTop = _scroll->scrollTop();\n\n\tif (newTop == 0 || newTop == _scroll->scrollTopMax()) {\n\t\t_scrollAnimation.stop();\n\t}\n}\n\nvoid InnerWidget::checkForScrollAnimation() {\n\tconst auto delta = deltaFromEdge();\n\tif (!_scroll || !delta || _scrollAnimation.animating()) {\n\t\treturn;\n\t}\n\t_scrollAnimation.start();\n}\n\nint InnerWidget::deltaFromEdge() {\n\tif (!_dragging.enabled || _dragging.index < 0 || !_scroll) {\n\t\treturn 0;\n\t}\n\n\tconst auto mousePos = QCursor::pos();\n\tconst auto scrollGlobalRect = QRect(\n\t\t_scroll->mapToGlobal(QPoint(0, 0)),\n\t\t_scroll->size());\n\n\tconst auto scrollTop = scrollGlobalRect.top();\n\tconst auto scrollBottom = scrollGlobalRect.bottom();\n\n\tconst auto scrollZone = 50;\n\tconst auto topDistance = mousePos.y() - scrollTop;\n\tconst auto bottomDistance = scrollBottom - mousePos.y();\n\n\tif (topDistance < scrollZone) {\n\t\tconst auto effectiveDistance = std::max(1, scrollZone - topDistance);\n\t\treturn -int(effectiveDistance * kScrollFactor);\n\t} else if (bottomDistance < scrollZone) {\n\t\tconst auto effectiveDistance\n\t\t\t= std::max(1, scrollZone - bottomDistance);\n\t\treturn int(effectiveDistance * kScrollFactor);\n\t}\n\treturn 0;\n}\n\nMemento::Memento(not_null<Controller*> controller)\n: ContentMemento(Tag{\n\tcontroller->giftsPeer(),\n\tcontroller->giftsCollectionId() }) {\n}\n\nMemento::Memento(not_null<PeerData*> peer, int collectionId)\n: ContentMemento(Tag{ peer, collectionId }) {\n}\n\nSection Memento::section() const {\n\treturn Section(Section::Type::PeerGifts);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(parent, controller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nvoid Memento::setListState(std::unique_ptr<ListState> state) {\n\t_listState = std::move(state);\n}\n\nstd::unique_ptr<ListState> Memento::listState() {\n\treturn std::move(_listState);\n}\n\nMemento::~Memento() = default;\n\nWidget::Widget(QWidget *parent, not_null<Controller*> controller)\n: ContentWidget(parent, controller)\n, _descriptor(Descriptor{\n\t.collectionId = controller->giftsCollectionId(),\n}) {\n\t_inner = setInnerWidget(\n\t\tobject_ptr<InnerWidget>(\n\t\t\tthis,\n\t\t\tcontroller->parentController(),\n\t\t\tcontroller->giftsPeer(),\n\t\t\t_descriptor.value(),\n\t\t\tscroll()));\n\t_emptyCollectionShown = _inner->collectionEmptyValue();\n\t_inner->notifyEnabled(\n\t) | rpl::take(1) | rpl::on_next([=](bool enabled) {\n\t\t_notifyEnabled = enabled;\n\t\trefreshBottom();\n\t}, _inner->lifetime());\n\t_inner->descriptorChanges(\n\t) | rpl::on_next([=](Descriptor descriptor) {\n\t\t_descriptor = descriptor;\n\t}, _inner->lifetime());\n\t_inner->scrollToTop() | rpl::on_next([=] {\n\t\tscrollTo({ 0, 0 });\n\t}, _inner->lifetime());\n\n\trpl::combine(\n\t\t_descriptor.value(),\n\t\t_emptyCollectionShown.value()\n\t) | rpl::on_next([=] {\n\t\trefreshBottom();\n\t}, _inner->lifetime());\n}\n\nvoid Widget::refreshBottom() {\n\tconst auto notify = _notifyEnabled.has_value();\n\tconst auto descriptor = _descriptor.current();\n\tconst auto shownId = descriptor.collectionId;\n\tconst auto withButton = shownId\n\t\t&& peer()->canManageGifts()\n\t\t&& !_emptyCollectionShown.current();\n\tconst auto wasBottom = _pinnedToBottom ? _pinnedToBottom->height() : 0;\n\tdelete _pinnedToBottom.data();\n\tif (!notify && !withButton) {\n\t\tsetScrollBottomSkip(0);\n\t\t_hasPinnedToBottom = false;\n\t} else if (withButton) {\n\t\tsetupBottomButton(wasBottom);\n\t} else {\n\t\tsetupNotifyCheckbox(wasBottom, *_notifyEnabled);\n\t}\n}\n\nvoid Widget::setupBottomButton(int wasBottomHeight) {\n\t_pinnedToBottom = Ui::CreateChild<Ui::SlideWrap<Ui::RpWidget>>(\n\t\tthis,\n\t\tobject_ptr<Ui::RpWidget>(this));\n\tconst auto wrap = _pinnedToBottom.data();\n\twrap->toggle(false, anim::type::instant);\n\n\tconst auto bottom = wrap->entity();\n\tbottom->show();\n\n\tconst auto button = Ui::CreateChild<Ui::RoundButton>(\n\t\tbottom,\n\t\trpl::single(QString()),\n\t\tst::collectionEditBox.button);\n\tbutton->setText(tr::lng_gift_collection_add_button(\n\t) | rpl::map([](const QString &text) {\n\t\treturn Ui::Text::IconEmoji(&st::collectionAddIcon).append(text);\n\t}));\n\tbutton->show();\n\t_hasPinnedToBottom = true;\n\n\tbutton->setClickedCallback([=] {\n\t\tif (const auto id = _descriptor.current().collectionId) {\n\t\t\t_inner->editCollectionGifts(id);\n\t\t} else {\n\t\t\trefreshBottom();\n\t\t}\n\t});\n\n\tconst auto buttonTop = st::boxRadius;\n\tbottom->widthValue() | rpl::on_next([=](int width) {\n\t\tconst auto normal = width - 2 * buttonTop;\n\t\tbutton->resizeToWidth(normal);\n\t\tconst auto buttonLeft = (width - normal) / 2;\n\t\tbutton->moveToLeft(buttonLeft, buttonTop);\n\t}, button->lifetime());\n\n\tbutton->heightValue() | rpl::on_next([=](int height) {\n\t\tbottom->resize(bottom->width(), st::boxRadius + height);\n\t}, button->lifetime());\n\n\tconst auto processHeight = [=] {\n\t\tsetScrollBottomSkip(wrap->height());\n\t\twrap->moveToLeft(wrap->x(), height() - wrap->height());\n\t};\n\n\t_inner->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\twrap->resizeToWidth(s.width());\n\t\tcrl::on_main(wrap, processHeight);\n\t}, wrap->lifetime());\n\n\trpl::combine(\n\t\twrap->heightValue(),\n\t\theightValue()\n\t) | rpl::on_next(processHeight, wrap->lifetime());\n\n\tif (_shown) {\n\t\twrap->toggle(\n\t\t\ttrue,\n\t\t\twasBottomHeight ? anim::type::instant : anim::type::normal);\n\t}\n}\n\nvoid Widget::showFinished() {\n\t_shown = true;\n\tif (const auto bottom = _pinnedToBottom.data()) {\n\t\tbottom->toggle(true, anim::type::normal);\n\t}\n}\n\nvoid Widget::setupNotifyCheckbox(int wasBottomHeight, bool enabled) {\n\t_pinnedToBottom = Ui::CreateChild<Ui::SlideWrap<Ui::RpWidget>>(\n\t\tthis,\n\t\tobject_ptr<Ui::RpWidget>(this));\n\tconst auto wrap = _pinnedToBottom.data();\n\twrap->toggle(false, anim::type::instant);\n\n\tconst auto bottom = wrap->entity();\n\tbottom->show();\n\n\tconst auto notify = Ui::CreateChild<Ui::Checkbox>(\n\t\tbottom,\n\t\ttr::lng_peer_gifts_notify(),\n\t\tenabled);\n\tnotify->show();\n\n\tnotify->checkedChanges() | rpl::on_next([=](bool checked) {\n\t\tconst auto api = &controller()->session().api();\n\t\tconst auto show = controller()->uiShow();\n\t\tusing Flag = MTPpayments_ToggleChatStarGiftNotifications::Flag;\n\t\tapi->request(MTPpayments_ToggleChatStarGiftNotifications(\n\t\t\tMTP_flags(checked ? Flag::f_enabled : Flag()),\n\t\t\t_inner->peer()->input()\n\t\t)).send();\n\t\tif (checked) {\n\t\t\tshow->showToast(tr::lng_peer_gifts_notify_enabled(tr::now));\n\t\t}\n\t}, notify->lifetime());\n\n\tconst auto &checkSt = st::defaultCheckbox;\n\tconst auto checkTop = st::boxRadius + checkSt.margin.top();\n\tbottom->widthValue() | rpl::on_next([=](int width) {\n\t\tconst auto normal = notify->naturalWidth()\n\t\t\t- checkSt.margin.left()\n\t\t\t- checkSt.margin.right();\n\t\tnotify->resizeToWidth(normal);\n\t\tconst auto checkLeft = (width - normal) / 2;\n\t\tnotify->moveToLeft(checkLeft, checkTop);\n\t}, notify->lifetime());\n\n\tnotify->heightValue() | rpl::on_next([=](int height) {\n\t\tbottom->resize(bottom->width(), st::boxRadius + height);\n\t}, notify->lifetime());\n\n\tconst auto processHeight = [=] {\n\t\tsetScrollBottomSkip(wrap->height());\n\t\twrap->moveToLeft(wrap->x(), height() - wrap->height());\n\t};\n\n\t_inner->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\twrap->resizeToWidth(s.width());\n\t\tcrl::on_main(wrap, processHeight);\n\t}, wrap->lifetime());\n\n\trpl::combine(\n\t\twrap->heightValue(),\n\t\theightValue()\n\t) | rpl::on_next(processHeight, wrap->lifetime());\n\n\tif (_shown) {\n\t\twrap->toggle(\n\t\t\ttrue,\n\t\t\twasBottomHeight ? anim::type::instant : anim::type::normal);\n\t}\n\t_hasPinnedToBottom = true;\n}\n\nvoid Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {\n\t_inner->fillMenu(addAction);\n}\n\nrpl::producer<QString> Widget::title() {\n\treturn tr::lng_peer_gifts_title();\n}\n\nrpl::producer<bool> Widget::desiredBottomShadowVisibility() {\n\treturn _hasPinnedToBottom.value();\n}\n\nnot_null<PeerData*> Widget::peer() const {\n\treturn _inner->peer();\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\tif (!controller()->validateMementoPeer(memento)) {\n\t\treturn false;\n\t}\n\tif (auto similarMemento = dynamic_cast<Memento*>(memento.get())) {\n\t\tif (similarMemento->peer() == peer()) {\n\t\t\trestoreState(similarMemento);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(controller());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\nstd::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer, int albumId) {\n\treturn std::make_shared<Info::Memento>(\n\t\tstd::vector<std::shared_ptr<ContentMemento>>(\n\t\t\t1,\n\t\t\tstd::make_shared<Memento>(peer, albumId)));\n}\n\n} // namespace Info::PeerGifts\n"
  },
  {
    "path": "Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_star_gift.h\"\n#include \"info/info_content_widget.h\"\n\nclass UserData;\nstruct PeerListState;\n\nnamespace Ui {\nclass RpWidget;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Info::PeerGifts {\n\nstruct ListState {\n\tstd::vector<Data::SavedStarGift> list;\n\tQString offset;\n};\n\nstruct Filter {\n\tbool sortByValue : 1 = false;\n\tbool skipUnlimited : 1 = false;\n\tbool skipLimited : 1 = false;\n\tbool skipUpgradable : 1 = false;\n\tbool skipUnique : 1 = false;\n\tbool skipSaved : 1 = false;\n\tbool skipUnsaved : 1 = false;\n\n\t[[nodiscard]] bool skipsSomething() const {\n\t\treturn skipLimited\n\t\t\t|| skipUnlimited\n\t\t\t|| skipSaved\n\t\t\t|| skipUnsaved\n\t\t\t|| skipUpgradable\n\t\t\t|| skipUnique;\n\t}\n\n\tfriend inline bool operator==(Filter, Filter) = default;\n};\n\nstruct Descriptor {\n\tFilter filter;\n\tint collectionId = 0;\n\n\tfriend inline bool operator==(\n\t\tconst Descriptor &,\n\t\tconst Descriptor &) = default;\n};\n\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\tMemento(not_null<Controller*> controller);\n\tMemento(not_null<PeerData*> peer, int collectionId);\n\t~Memento();\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\tvoid setListState(std::unique_ptr<ListState> state);\n\tstd::unique_ptr<ListState> listState();\n\nprivate:\n\tstd::unique_ptr<ListState> _listState;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(QWidget *parent, not_null<Controller*> controller);\n\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\n\tvoid fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override;\n\n\trpl::producer<QString> title() override;\n\n\trpl::producer<bool> desiredBottomShadowVisibility() override;\n\n\tvoid showFinished() override;\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tvoid setupNotifyCheckbox(int wasBottomHeight, bool enabled);\n\tvoid setupBottomButton(int wasBottomHeight);\n\tvoid refreshBottom();\n\n\tInnerWidget *_inner = nullptr;\n\tQPointer<Ui::SlideWrap<Ui::RpWidget>> _pinnedToBottom;\n\trpl::variable<bool> _hasPinnedToBottom;\n\trpl::variable<bool> _emptyCollectionShown;\n\trpl::variable<Descriptor> _descriptor;\n\tstd::optional<bool> _notifyEnabled;\n\tbool _shown = false;\n\n};\n\n[[nodiscard]] std::shared_ptr<Info::Memento> Make(\n\tnot_null<PeerData*> peer,\n\tint collectionId = 0);\n\n} // namespace Info::PeerGifts\n"
  },
  {
    "path": "Telegram/SourceFiles/info/polls/info_polls_list_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/polls/info_polls_list_widget.h\"\n\n#include \"data/data_poll_messages.h\"\n#include \"data/data_shared_media.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/history_view_list_widget.h\"\n#include \"history/view/history_view_corner_buttons.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_service_message.h\"\n#include \"history/view/reactions/history_view_reactions_button.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"ui/painter.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/widgets/elastic_scroll.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/section_widget.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/main_window.h\"\n#include \"window/window_session_controller.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"info/info_wrap_widget.h\"\n#include \"window/window_peer_menu.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_polls.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Info::Polls {\n\nclass ListWidget::Inner final\n\t: private HistoryView::ListDelegate\n\t, private HistoryView::CornerButtonsDelegate {\npublic:\n\tInner(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Controller*> controller);\n\n\t[[nodiscard]] not_null<Ui::ElasticScroll*> scroll() const {\n\t\treturn _scroll.get();\n\t}\n\t[[nodiscard]] HistoryView::ListWidget *list() const {\n\t\treturn _list;\n\t}\n\n\tvoid updateGeometry(QRect rect);\n\n\t[[nodiscard]] int scrollTop() const;\n\tvoid setScrollTop(int top);\n\tvoid setSearchQuery(const QString &query);\n\n\t[[nodiscard]] rpl::producer<SelectedItems> selectedItems() const;\n\tvoid selectionAction(SelectionAction action);\n\n\tvoid paintBackground(QPainter &p, QRect clip);\n\nprivate:\n\t[[nodiscard]] SparseIdsMergedSlice::UniversalMsgId computeUniversalId(\n\t\tData::MessagePosition aroundId) const;\n\t[[nodiscard]] Data::MessagesSlice sliceFromSparseIds(\n\t\tSparseIdsMergedSlice &&slice,\n\t\tSparseIdsMergedSlice::UniversalMsgId aroundId) const;\n\tvoid setupHistory();\n\tvoid updateInnerVisibleArea();\n\tvoid updateNewPollButtonPosition();\n\tvoid updateNewPollButtonVisibility();\n\n\tHistoryView::Context listContext() override;\n\tbool listScrollTo(int top, bool syntetic = true) override;\n\tvoid listCancelRequest() override;\n\tvoid listDeleteRequest() override;\n\tvoid listTryProcessKeyInput(not_null<QKeyEvent*> e) override;\n\trpl::producer<Data::MessagesSlice> listSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) override;\n\tbool listAllowsMultiSelect() override;\n\tbool listIsItemGoodForSelection(\n\t\tnot_null<HistoryItem*> item) override;\n\tbool listIsLessInOrder(\n\t\tnot_null<HistoryItem*> first,\n\t\tnot_null<HistoryItem*> second) override;\n\tvoid listSelectionChanged(\n\t\tHistoryView::SelectedItems &&items) override;\n\tvoid listMarkReadTill(not_null<HistoryItem*> item) override;\n\tvoid listMarkContentsRead(\n\t\tconst base::flat_set<not_null<HistoryItem*>> &items) override;\n\tHistoryView::MessagesBarData listMessagesBar(\n\t\tconst std::vector<not_null<HistoryView::Element*>> &elements,\n\t\tbool markLastAsRead) override;\n\tvoid listContentRefreshed() override;\n\tvoid listUpdateDateLink(\n\t\tClickHandlerPtr &link,\n\t\tnot_null<HistoryView::Element*> view) override;\n\tbool listElementHideReply(\n\t\tnot_null<const HistoryView::Element*> view) override;\n\tbool listElementShownUnread(\n\t\tnot_null<const HistoryView::Element*> view) override;\n\tbool listIsGoodForAroundPosition(\n\t\tnot_null<const HistoryView::Element*> view) override;\n\tvoid listSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) override;\n\tvoid listSearch(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) override;\n\tvoid listHandleViaClick(not_null<UserData*> bot) override;\n\tnot_null<Ui::ChatTheme*> listChatTheme() override;\n\tHistoryView::CopyRestrictionType listCopyRestrictionType(\n\t\tHistoryItem *item) override;\n\tHistoryView::CopyRestrictionType listCopyMediaRestrictionType(\n\t\tnot_null<HistoryItem*> item) override;\n\tHistoryView::CopyRestrictionType listSelectRestrictionType() override;\n\tauto listAllowedReactionsValue()\n\t\t-> rpl::producer<Data::AllowedReactions> override;\n\tvoid listShowPremiumToast(\n\t\tnot_null<DocumentData*> document) override;\n\tvoid listOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) override;\n\tvoid listOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView) override;\n\tvoid listPaintEmpty(\n\t\tPainter &p,\n\t\tconst Ui::ChatPaintContext &context) override;\n\tQString listElementAuthorRank(\n\t\tnot_null<const HistoryView::Element*> view) override;\n\tbool listElementHideTopicButton(\n\t\tnot_null<const HistoryView::Element*> view) override;\n\tHistory *listTranslateHistory() override;\n\tvoid listAddTranslatedItems(\n\t\tnot_null<HistoryView::TranslateTracker*> tracker) override;\n\tnot_null<Window::SessionController*> listWindow() override;\n\tnot_null<QWidget*> listEmojiInteractionsParent() override;\n\tnot_null<const Ui::ChatStyle*> listChatStyle() override;\n\trpl::producer<bool> listChatWideValue() override;\n\tstd::unique_ptr<HistoryView::Reactions::Manager>\n\t\tlistMakeReactionsManager(\n\t\t\tQWidget *wheelEventsTarget,\n\t\t\tFn<void(QRect)> update) override;\n\tvoid listVisibleAreaUpdated() override;\n\tstd::shared_ptr<Ui::Show> listUiShow() override;\n\tvoid listShowPollResults(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context) override;\n\tvoid listCancelUploadLayer(not_null<HistoryItem*> item) override;\n\tbool listAnimationsPaused() override;\n\tauto listSendingAnimation()\n\t\t-> Ui::MessageSendingAnimationController* override;\n\tUi::ChatPaintContext listPreparePaintContext(\n\t\tUi::ChatPaintContextArgs &&args) override;\n\tbool listMarkingContentRead() override;\n\tbool listIgnorePaintEvent(QWidget *w, QPaintEvent *e) override;\n\tbool listShowReactPremiumError(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst Data::ReactionId &id) override;\n\tbase::unique_qptr<Ui::PopupMenu> listFillSenderUserpicMenu(\n\t\tPeerId userpicPeerId) override;\n\tvoid listWindowSetInnerFocus() override;\n\tbool listAllowsDragForward() override;\n\tvoid listLaunchDrag(\n\t\tstd::unique_ptr<QMimeData> data,\n\t\tFn<void()> finished) override;\n\n\tvoid cornerButtonsShowAtPosition(\n\t\tData::MessagePosition position) override;\n\tData::Thread *cornerButtonsThread() override;\n\tFullMsgId cornerButtonsCurrentId() override;\n\tbool cornerButtonsIgnoreVisibility() override;\n\tstd::optional<bool> cornerButtonsDownShown() override;\n\tbool cornerButtonsUnreadMayBeShown() override;\n\tbool cornerButtonsHas(HistoryView::CornerButtonType type) override;\n\n\tconst not_null<Controller*> _controller;\n\tconst not_null<Main::Session*> _session;\n\tconst not_null<History*> _history;\n\tconst MsgId _topicRootId = 0;\n\tconst PeerId _monoforumPeerId = 0;\n\tconst std::shared_ptr<Ui::ChatTheme> _theme;\n\tconst std::unique_ptr<Ui::ChatStyle> _chatStyle;\n\tconst std::unique_ptr<Ui::ElasticScroll> _scroll;\n\n\tQPointer<HistoryView::ListWidget> _list;\n\tstd::unique_ptr<HistoryView::CornerButtons> _cornerButtons;\n\tbool _viewerRefreshed = false;\n\tQString _searchQuery;\n\n\tobject_ptr<Ui::RoundButton> _newPollButton = { nullptr };\n\tUi::Animations::Simple _newPollButtonAnimation;\n\tbool _newPollButtonShown = true;\n\n\tstd::unique_ptr<Lottie::Icon> _emptyIcon;\n\tUi::Text::String _emptyText;\n\tbool _emptyAnimated = false;\n\n\tQImage _bg;\n\n\trpl::event_stream<SelectedItems> _selectedItems;\n\n};\n\nListWidget::Inner::Inner(\n\tnot_null<QWidget*> parent,\n\tnot_null<Controller*> controller)\n: _controller(controller)\n, _session(&controller->session())\n, _history(_session->data().history(controller->key().peer()))\n, _topicRootId(controller->topic()\n\t? controller->topic()->rootId()\n\t: MsgId())\n, _monoforumPeerId(controller->sublist()\n\t? controller->sublist()->sublistPeer()->id\n\t: PeerId())\n, _theme(Window::Theme::DefaultChatThemeOn(\n\t_controller->parentController()->lifetime()))\n, _chatStyle(\n\tstd::make_unique<Ui::ChatStyle>(_session->colorIndicesValue()))\n, _scroll(std::make_unique<Ui::ElasticScroll>(parent)) {\n\t_chatStyle->apply(_theme.get());\n\tsetupHistory();\n}\n\nvoid ListWidget::Inner::setupHistory() {\n\t_list = _scroll->setOwnedWidget(\n\t\tobject_ptr<HistoryView::ListWidget>(\n\t\t\t_scroll.get(),\n\t\t\t_session,\n\t\t\tstatic_cast<HistoryView::ListDelegate*>(this)));\n\t_cornerButtons = std::make_unique<HistoryView::CornerButtons>(\n\t\t_scroll.get(),\n\t\t_chatStyle.get(),\n\t\tstatic_cast<HistoryView::CornerButtonsDelegate*>(this));\n\n\t_scroll->scrolls(\n\t) | rpl::on_next([=] {\n\t\tupdateInnerVisibleArea();\n\t}, _scroll->lifetime());\n\t_scroll->setOverscrollBg(QColor(0, 0, 0, 0));\n\tusing Type = Ui::ElasticScroll::OverscrollType;\n\t_scroll->setOverscrollTypes(Type::Real, Type::Real);\n\n\t_list->scrollKeyEvents(\n\t) | rpl::on_next([=](not_null<QKeyEvent*> e) {\n\t\t_scroll->keyPressEvent(e);\n\t}, _scroll->lifetime());\n\n\tconst auto topic = _controller->topic();\n\tconst auto canCreate = topic\n\t\t? Data::CanSend(topic, ChatRestriction::SendPolls)\n\t\t: _history->peer->canCreatePolls();\n\tif (!canCreate) {\n\t\treturn;\n\t}\n\t_newPollButton.create(\n\t\t_scroll.get(),\n\t\ttr::lng_polls_create_title(),\n\t\tst::defaultActiveButton);\n\t_newPollButton->setFullRadius(true);\n\t_newPollButton->setClickedCallback([=] {\n\t\tWindow::PeerMenuCreatePoll(\n\t\t\t_controller->parentController(),\n\t\t\t_history->peer);\n\t});\n\t_newPollButton->show();\n}\n\nvoid ListWidget::Inner::updateNewPollButtonPosition() {\n\tif (!_newPollButton) {\n\t\treturn;\n\t}\n\tconst auto progress = _newPollButtonAnimation.value(\n\t\t_newPollButtonShown ? 1. : 0.);\n\tconst auto buttonWidth = _newPollButton->width();\n\tconst auto buttonHeight = _newPollButton->height();\n\tconst auto x = (_scroll->width() - buttonWidth) / 2;\n\tconst auto bottom = st::infoPollsNewButtonBottom;\n\tconst auto top = anim::interpolate(\n\t\t_scroll->height(),\n\t\t_scroll->height() - buttonHeight - bottom,\n\t\tprogress);\n\t_newPollButton->moveToLeft(x, top);\n\tconst auto shouldBeHidden = !_newPollButtonShown\n\t\t&& !_newPollButtonAnimation.animating();\n\tif (shouldBeHidden != _newPollButton->isHidden()) {\n\t\t_newPollButton->setVisible(!shouldBeHidden);\n\t}\n}\n\nvoid ListWidget::Inner::updateNewPollButtonVisibility() {\n\tconst auto scrollTop = _scroll->scrollTop();\n\tconst auto scrollTopMax = _scroll->scrollTopMax();\n\tconst auto nearBottom = (scrollTop + st::historyToDownShownAfter / 2)\n\t\t>= scrollTopMax;\n\tconst auto shown = !nearBottom;\n\tif (_newPollButtonShown != shown) {\n\t\t_newPollButtonShown = shown;\n\t\t_newPollButtonAnimation.start(\n\t\t\t[=] { updateNewPollButtonPosition(); },\n\t\t\tshown ? 0. : 1.,\n\t\t\tshown ? 1. : 0.,\n\t\t\tst::historyToDownDuration);\n\t}\n}\n\nvoid ListWidget::Inner::updateGeometry(QRect rect) {\n\t_scroll->setGeometry(rect);\n\t_cornerButtons->updatePositions();\n\tupdateNewPollButtonPosition();\n\n\tif (rect.isEmpty()) {\n\t\treturn;\n\t}\n\t_list->resizeToWidth(_scroll->width(), _scroll->height());\n\tif (!_viewerRefreshed) {\n\t\t_viewerRefreshed = true;\n\t\t_list->refreshViewer();\n\t}\n\tconst auto ratio = style::DevicePixelRatio();\n\t_bg = QImage(\n\t\trect.size() * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tauto p = QPainter(&_bg);\n\tWindow::SectionWidget::PaintBackground(\n\t\tp,\n\t\t_theme.get(),\n\t\tQSize(rect.width(), rect.height() * 2),\n\t\tQRect(QPoint(), rect.size()));\n}\n\nvoid ListWidget::Inner::paintBackground(QPainter &p, QRect clip) {\n\tif (!_bg.isNull()) {\n\t\tconst auto scrollGeometry = _scroll->geometry();\n\t\tp.drawImage(scrollGeometry.topLeft(), _bg);\n\t}\n}\n\nvoid ListWidget::Inner::updateInnerVisibleArea() {\n\tconst auto scrollTop = _scroll->scrollTop();\n\t_list->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());\n\t_cornerButtons->updateJumpDownVisibility();\n\tupdateNewPollButtonVisibility();\n}\n\nint ListWidget::Inner::scrollTop() const {\n\treturn _scroll->scrollTop();\n}\n\nvoid ListWidget::Inner::setScrollTop(int top) {\n\t_scroll->scrollToY(top);\n}\n\nvoid ListWidget::Inner::setSearchQuery(const QString &query) {\n\tif (_searchQuery == query) {\n\t\treturn;\n\t}\n\t_emptyAnimated = false;\n\tif (_emptyIcon) {\n\t\t_emptyIcon->jumpTo(0, nullptr);\n\t}\n\t_searchQuery = query;\n\tif (_viewerRefreshed) {\n\t\t_list->refreshViewer();\n\t}\n}\n\nrpl::producer<SelectedItems> ListWidget::Inner::selectedItems() const {\n\treturn _selectedItems.events();\n}\n\nvoid ListWidget::Inner::selectionAction(SelectionAction action) {\n\tswitch (action) {\n\tcase SelectionAction::Clear: _list->cancelSelection(); return;\n\tcase SelectionAction::Forward:\n\t\tHistoryView::ConfirmForwardSelectedItems(_list);\n\t\treturn;\n\tcase SelectionAction::Delete:\n\t\tHistoryView::ConfirmDeleteSelectedItems(_list);\n\t\treturn;\n\tdefault: return;\n\t}\n}\n\nSparseIdsMergedSlice::UniversalMsgId\nListWidget::Inner::computeUniversalId(\n\t\tData::MessagePosition aroundId) const {\n\tconst auto peerId = _history->peer->id;\n\treturn ((aroundId.fullId.msg == ShowAtTheEndMsgId)\n\t\t\t|| (aroundId == Data::MaxMessagePosition))\n\t\t? (ServerMaxMsgId - 1)\n\t\t: (aroundId.fullId.peer == peerId)\n\t\t? aroundId.fullId.msg\n\t\t: (aroundId.fullId.msg\n\t\t\t? (aroundId.fullId.msg - ServerMaxMsgId)\n\t\t\t: (ServerMaxMsgId - 1));\n}\n\nData::MessagesSlice ListWidget::Inner::sliceFromSparseIds(\n\t\tSparseIdsMergedSlice &&slice,\n\t\tSparseIdsMergedSlice::UniversalMsgId aroundId) const {\n\tauto result = Data::MessagesSlice();\n\tresult.fullCount = slice.fullCount();\n\tresult.skippedAfter = slice.skippedAfter();\n\tresult.skippedBefore = slice.skippedBefore();\n\tconst auto count = slice.size();\n\tresult.ids.reserve(count);\n\tif (const auto msgId = slice.nearest(aroundId)) {\n\t\tresult.nearestToAround = *msgId;\n\t}\n\tfor (auto i = 0; i != count; ++i) {\n\t\tresult.ids.push_back(slice[i]);\n\t}\n\treturn result;\n}\n\nHistoryView::Context ListWidget::Inner::listContext() {\n\treturn HistoryView::Context::ChatPreview;\n}\n\nbool ListWidget::Inner::listScrollTo(int top, bool syntetic) {\n\ttop = std::clamp(top, 0, _scroll->scrollTopMax());\n\tif (_scroll->scrollTop() == top) {\n\t\tupdateInnerVisibleArea();\n\t\treturn false;\n\t}\n\t_scroll->scrollToY(top);\n\treturn true;\n}\n\nvoid ListWidget::Inner::listCancelRequest() {\n\t_list->cancelSelection();\n}\n\nvoid ListWidget::Inner::listDeleteRequest() {\n\tHistoryView::ConfirmDeleteSelectedItems(_list);\n}\n\nvoid ListWidget::Inner::listTryProcessKeyInput(not_null<QKeyEvent*> e) {\n}\n\nrpl::producer<Data::MessagesSlice> ListWidget::Inner::listSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tif (!_searchQuery.isEmpty()) {\n\t\tconst auto universalId = computeUniversalId(aroundId);\n\t\treturn _controller->mediaSource(\n\t\t\tuniversalId,\n\t\t\tlimitBefore,\n\t\t\tlimitAfter\n\t\t) | rpl::map([=](SparseIdsMergedSlice &&slice) {\n\t\t\treturn sliceFromSparseIds(\n\t\t\t\tstd::move(slice),\n\t\t\t\tuniversalId);\n\t\t});\n\t}\n\treturn Data::PollMessagesViewer(\n\t\t_session,\n\t\t_history,\n\t\t_topicRootId,\n\t\t_monoforumPeerId,\n\t\taroundId,\n\t\tlimitBefore,\n\t\tlimitAfter);\n}\n\nbool ListWidget::Inner::listAllowsMultiSelect() {\n\treturn true;\n}\n\nbool ListWidget::Inner::listIsItemGoodForSelection(\n\t\tnot_null<HistoryItem*> item) {\n\treturn item->isRegular() && !item->isService();\n}\n\nbool ListWidget::Inner::listIsLessInOrder(\n\t\tnot_null<HistoryItem*> first,\n\t\tnot_null<HistoryItem*> second) {\n\tif (first->isRegular() && second->isRegular()) {\n\t\treturn first->id < second->id;\n\t} else if (first->isRegular()) {\n\t\treturn true;\n\t} else if (second->isRegular()) {\n\t\treturn false;\n\t}\n\treturn first->id < second->id;\n}\n\nvoid ListWidget::Inner::listSelectionChanged(\n\t\tHistoryView::SelectedItems &&items) {\n\tauto result = SelectedItems(Storage::SharedMediaType::Poll);\n\tresult.list.reserve(items.size());\n\tconst auto sessionId = _session->uniqueId();\n\tfor (const auto &item : items) {\n\t\tauto entry = SelectedItem(GlobalMsgId{ item.msgId, sessionId });\n\t\tentry.canDelete = item.canDelete;\n\t\tentry.canForward = item.canForward;\n\t\tresult.list.push_back(std::move(entry));\n\t}\n\t_selectedItems.fire(std::move(result));\n}\n\nvoid ListWidget::Inner::listMarkReadTill(not_null<HistoryItem*> item) {\n}\n\nvoid ListWidget::Inner::listMarkContentsRead(\n\t\tconst base::flat_set<not_null<HistoryItem*>> &items) {\n}\n\nHistoryView::MessagesBarData ListWidget::Inner::listMessagesBar(\n\t\tconst std::vector<not_null<HistoryView::Element*>> &elements,\n\t\tbool markLastAsRead) {\n\treturn {};\n}\n\nvoid ListWidget::Inner::listContentRefreshed() {\n}\n\nvoid ListWidget::Inner::listUpdateDateLink(\n\t\tClickHandlerPtr &link,\n\t\tnot_null<HistoryView::Element*> view) {\n}\n\nbool ListWidget::Inner::listElementHideReply(\n\t\tnot_null<const HistoryView::Element*> view) {\n\treturn false;\n}\n\nbool ListWidget::Inner::listElementShownUnread(\n\t\tnot_null<const HistoryView::Element*> view) {\n\treturn false;\n}\n\nbool ListWidget::Inner::listIsGoodForAroundPosition(\n\t\tnot_null<const HistoryView::Element*> view) {\n\treturn view->data()->isRegular();\n}\n\nvoid ListWidget::Inner::listSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) {\n}\n\nvoid ListWidget::Inner::listSearch(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) {\n}\n\nvoid ListWidget::Inner::listHandleViaClick(not_null<UserData*> bot) {\n}\n\nnot_null<Ui::ChatTheme*> ListWidget::Inner::listChatTheme() {\n\treturn _theme.get();\n}\n\nHistoryView::CopyRestrictionType\nListWidget::Inner::listCopyRestrictionType(HistoryItem *item) {\n\treturn HistoryView::CopyRestrictionType::None;\n}\n\nHistoryView::CopyRestrictionType\nListWidget::Inner::listCopyMediaRestrictionType(\n\t\tnot_null<HistoryItem*> item) {\n\treturn HistoryView::CopyRestrictionType::None;\n}\n\nHistoryView::CopyRestrictionType\nListWidget::Inner::listSelectRestrictionType() {\n\treturn HistoryView::CopyRestrictionType::None;\n}\n\nauto ListWidget::Inner::listAllowedReactionsValue()\n-> rpl::producer<Data::AllowedReactions> {\n\treturn Data::PeerAllowedReactionsValue(_history->peer);\n}\n\nvoid ListWidget::Inner::listShowPremiumToast(\n\t\tnot_null<DocumentData*> document) {\n}\n\nvoid ListWidget::Inner::listOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) {\n}\n\nvoid ListWidget::Inner::listOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView) {\n}\n\nvoid ListWidget::Inner::listPaintEmpty(\n\t\tPainter &p,\n\t\tconst Ui::ChatPaintContext &context) {\n\tif (!_emptyIcon) {\n\t\tconst auto size = st::recentPeersEmptySize;\n\t\t_emptyIcon = Lottie::MakeIcon({\n\t\t\t.name = u\"noresults\"_q,\n\t\t\t.sizeOverride = { size, size },\n\t\t});\n\t\t_emptyText.setText(\n\t\t\tst::serviceTextStyle,\n\t\t\ttr::lng_polls_search_none(tr::now));\n\t}\n\tif (!_emptyAnimated) {\n\t\t_emptyAnimated = true;\n\t\t_emptyIcon->animate(\n\t\t\t[=] { _scroll->update(); },\n\t\t\t0,\n\t\t\t_emptyIcon->framesCount() - 1);\n\t}\n\tconst auto iconSize = _emptyIcon->size();\n\tconst auto width = st::repliesEmptyWidth;\n\tconst auto padding = st::repliesEmptyPadding;\n\tconst auto textWidth = width - padding.left() - padding.right();\n\tconst auto textHeight = _emptyText.countHeight(textWidth);\n\tconst auto height = padding.top()\n\t\t+ iconSize.height()\n\t\t+ st::repliesEmptySkip\n\t\t+ textHeight\n\t\t+ padding.bottom();\n\tconst auto r = QRect(\n\t\t(_scroll->width() - width) / 2,\n\t\t(_scroll->height() - height) / 3,\n\t\twidth,\n\t\theight);\n\tHistoryView::ServiceMessagePainter::PaintBubble(\n\t\tp,\n\t\tcontext.st,\n\t\tr);\n\n\t_emptyIcon->paintInCenter(\n\t\tp,\n\t\tQRect(r.x(), r.y() + padding.top(), r.width(), iconSize.height()),\n\t\tst::msgServiceFg->c);\n\tp.setPen(st::msgServiceFg);\n\t_emptyText.draw(\n\t\tp,\n\t\tr.x() + (r.width() - textWidth) / 2,\n\t\tr.y() + padding.top() + iconSize.height() + st::repliesEmptySkip,\n\t\ttextWidth,\n\t\tstyle::al_top);\n}\n\nQString ListWidget::Inner::listElementAuthorRank(\n\t\tnot_null<const HistoryView::Element*> view) {\n\treturn {};\n}\n\nbool ListWidget::Inner::listElementHideTopicButton(\n\t\tnot_null<const HistoryView::Element*> view) {\n\treturn true;\n}\n\nHistory *ListWidget::Inner::listTranslateHistory() {\n\treturn nullptr;\n}\n\nvoid ListWidget::Inner::listAddTranslatedItems(\n\t\tnot_null<HistoryView::TranslateTracker*> tracker) {\n}\n\nnot_null<Window::SessionController*> ListWidget::Inner::listWindow() {\n\treturn _controller->parentController();\n}\n\nnot_null<QWidget*> ListWidget::Inner::listEmojiInteractionsParent() {\n\treturn _scroll.get();\n}\n\nnot_null<const Ui::ChatStyle*> ListWidget::Inner::listChatStyle() {\n\treturn _chatStyle.get();\n}\n\nrpl::producer<bool> ListWidget::Inner::listChatWideValue() {\n\treturn rpl::single(false);\n}\n\nstd::unique_ptr<HistoryView::Reactions::Manager>\nListWidget::Inner::listMakeReactionsManager(\n\t\tQWidget *wheelEventsTarget,\n\t\tFn<void(QRect)> update) {\n\treturn std::make_unique<HistoryView::Reactions::Manager>(\n\t\twheelEventsTarget,\n\t\tstd::move(update));\n}\n\nvoid ListWidget::Inner::listVisibleAreaUpdated() {\n}\n\nstd::shared_ptr<Ui::Show> ListWidget::Inner::listUiShow() {\n\treturn _controller->parentController()->uiShow();\n}\n\nvoid ListWidget::Inner::listShowPollResults(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context) {\n\t_controller->parentController()->showSection(\n\t\tstd::make_shared<Info::Memento>(poll, context));\n}\n\nvoid ListWidget::Inner::listCancelUploadLayer(\n\t\tnot_null<HistoryItem*> item) {\n}\n\nbool ListWidget::Inner::listAnimationsPaused() {\n\treturn false;\n}\n\nauto ListWidget::Inner::listSendingAnimation()\n-> Ui::MessageSendingAnimationController* {\n\treturn nullptr;\n}\n\nUi::ChatPaintContext ListWidget::Inner::listPreparePaintContext(\n\t\tUi::ChatPaintContextArgs &&args) {\n\tconst auto visibleAreaTopLocal = _scroll->mapFromGlobal(\n\t\targs.visibleAreaPositionGlobal).y();\n\tconst auto area = QRect(\n\t\t0,\n\t\targs.visibleAreaTop,\n\t\targs.visibleAreaWidth,\n\t\targs.visibleAreaHeight);\n\tconst auto viewport = QRect(\n\t\t0,\n\t\targs.visibleAreaTop - visibleAreaTopLocal,\n\t\targs.visibleAreaWidth,\n\t\t_scroll->height());\n\treturn args.theme->preparePaintContext(\n\t\t_chatStyle.get(),\n\t\tviewport,\n\t\tarea,\n\t\targs.clip,\n\t\tfalse);\n}\n\nbool ListWidget::Inner::listMarkingContentRead() {\n\treturn false;\n}\n\nbool ListWidget::Inner::listIgnorePaintEvent(QWidget *w, QPaintEvent *e) {\n\treturn false;\n}\n\nbool ListWidget::Inner::listShowReactPremiumError(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst Data::ReactionId &id) {\n\treturn Window::ShowReactPremiumError(\n\t\t_controller->parentController(),\n\t\titem,\n\t\tid);\n}\n\nbase::unique_qptr<Ui::PopupMenu>\nListWidget::Inner::listFillSenderUserpicMenu(PeerId userpicPeerId) {\n\treturn nullptr;\n}\n\nvoid ListWidget::Inner::listWindowSetInnerFocus() {\n}\n\nbool ListWidget::Inner::listAllowsDragForward() {\n\treturn _controller->parentController()->adaptive().isOneColumn();\n}\n\nvoid ListWidget::Inner::listLaunchDrag(\n\t\tstd::unique_ptr<QMimeData> data,\n\t\tFn<void()> finished) {\n}\n\nvoid ListWidget::Inner::cornerButtonsShowAtPosition(\n\t\tData::MessagePosition position) {\n\tif (position == Data::UnreadMessagePosition) {\n\t\tposition = Data::MaxMessagePosition;\n\t}\n\t_list->showAtPosition(\n\t\tposition,\n\t\t{},\n\t\t_cornerButtons->doneJumpFrom(position.fullId, {}, true));\n}\n\nData::Thread *ListWidget::Inner::cornerButtonsThread() {\n\treturn _history;\n}\n\nFullMsgId ListWidget::Inner::cornerButtonsCurrentId() {\n\treturn {};\n}\n\nbool ListWidget::Inner::cornerButtonsIgnoreVisibility() {\n\treturn false;\n}\n\nstd::optional<bool> ListWidget::Inner::cornerButtonsDownShown() {\n\tconst auto top = _scroll->scrollTop() + st::historyToDownShownAfter;\n\tif (top < _scroll->scrollTopMax()) {\n\t\treturn true;\n\t} else if (_list->loadedAtBottomKnown()) {\n\t\treturn !_list->loadedAtBottom();\n\t}\n\treturn std::nullopt;\n}\n\nbool ListWidget::Inner::cornerButtonsUnreadMayBeShown() {\n\treturn false;\n}\n\nbool ListWidget::Inner::cornerButtonsHas(\n\t\tHistoryView::CornerButtonType type) {\n\treturn (type == HistoryView::CornerButtonType::Down)\n\t\t|| (type == HistoryView::CornerButtonType::PollVotes);\n}\n\n// --- ListMemento ---\n\nListMemento::ListMemento(\n\tnot_null<PeerData*> peer,\n\tPeerId migratedPeerId)\n: ContentMemento(peer, nullptr, nullptr, migratedPeerId) {\n}\n\nListMemento::ListMemento(not_null<Data::ForumTopic*> topic)\n: ContentMemento(topic->peer(), topic, nullptr, PeerId()) {\n}\n\nListMemento::ListMemento(not_null<Data::SavedSublist*> sublist)\n: ContentMemento(\n\tsublist->owningHistory()->peer,\n\tnullptr,\n\tsublist,\n\tPeerId()) {\n}\n\nSection ListMemento::section() const {\n\treturn Section(Storage::SharedMediaType::Poll);\n}\n\nobject_ptr<ContentWidget> ListMemento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<ListWidget>(parent, controller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\n// --- ListWidget ---\n\nListWidget::ListWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: ContentWidget(parent, controller)\n, _inner(std::make_unique<Inner>(this, controller)) {\n\tsetInnerWidget(object_ptr<Ui::RpWidget>(this));\n\tscroll()->hide();\n\n\tscroll()->geometryValue(\n\t) | rpl::on_next([=](QRect rect) {\n\t\t_inner->updateGeometry(rect);\n\t}, lifetime());\n\n\tsetupSearch();\n}\n\nListWidget::~ListWidget() = default;\n\nvoid ListWidget::fillTopBarMenu(\n\t\tconst Ui::Menu::MenuCallback &addAction) {\n}\n\nvoid ListWidget::setupSearch() {\n\tauto search = controller()->searchFieldController();\n\tif (!search) {\n\t\treturn;\n\t}\n\tcontroller()->setSearchEnabledByContent(true);\n\n\tcontroller()->mediaSourceQueryValue(\n\t) | rpl::on_next([=](const QString &query) {\n\t\t_inner->setSearchQuery(query);\n\t}, lifetime());\n}\n\nrpl::producer<SelectedItems> ListWidget::selectedListValue() const {\n\treturn _inner->selectedItems();\n}\n\nvoid ListWidget::selectionAction(SelectionAction action) {\n\t_inner->selectionAction(action);\n}\n\nrpl::producer<QString> ListWidget::title() {\n\treturn tr::lng_media_type_polls();\n}\n\nrpl::producer<int> ListWidget::desiredHeightValue() const {\n\treturn sizeValue(\n\t) | rpl::map([=](QSize) {\n\t\treturn maxVisibleHeight();\n\t}) | rpl::distinct_until_changed();\n}\n\nbool ListWidget::showInternal(not_null<ContentMemento*> memento) {\n\tif (!controller()->validateMementoPeer(memento)) {\n\t\treturn false;\n\t}\n\tif (auto my = dynamic_cast<ListMemento*>(memento.get())) {\n\t\trestoreState(my);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid ListWidget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<ListMemento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nstd::shared_ptr<ContentMemento> ListWidget::doCreateMemento() {\n\tauto result = std::make_shared<ListMemento>(\n\t\tcontroller()->key().peer(),\n\t\tcontroller()->migratedPeerId());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid ListWidget::saveState(not_null<ListMemento*> memento) {\n\tmemento->setScrollTop(_inner->scrollTop());\n}\n\nvoid ListWidget::restoreState(not_null<ListMemento*> memento) {\n\t_inner->setScrollTop(memento->scrollTop());\n}\n\nvoid ListWidget::resizeEvent(QResizeEvent *e) {\n\tContentWidget::resizeEvent(e);\n\t_inner->updateGeometry(scroll()->geometry());\n}\n\nvoid ListWidget::paintEvent(QPaintEvent *e) {\n\tContentWidget::paintEvent(e);\n\tauto p = QPainter(this);\n\t_inner->paintBackground(p, e->rect());\n}\n\n} // namespace Info::Polls\n"
  },
  {
    "path": "Telegram/SourceFiles/info/polls/info_polls_list_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n#include \"storage/storage_shared_media.h\"\n\nnamespace Ui {\nclass ElasticScroll;\nclass ChatStyle;\nclass ChatTheme;\n} // namespace Ui\n\nnamespace HistoryView {\nclass ListWidget;\n} // namespace HistoryView\n\nnamespace Data {\nclass ForumTopic;\nclass SavedSublist;\n} // namespace Data\n\nnamespace Info::Polls {\n\nclass ListMemento final : public ContentMemento {\npublic:\n\tListMemento(\n\t\tnot_null<PeerData*> peer,\n\t\tPeerId migratedPeerId);\n\tListMemento(not_null<Data::ForumTopic*> topic);\n\tListMemento(not_null<Data::SavedSublist*> sublist);\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\t[[nodiscard]] Section section() const override;\n\n\tvoid setScrollTop(int scrollTop) {\n\t\t_scrollTop = scrollTop;\n\t}\n\t[[nodiscard]] int scrollTop() const {\n\t\treturn _scrollTop;\n\t}\n\nprivate:\n\tint _scrollTop = 0;\n\n};\n\nclass ListWidget final : public ContentWidget {\npublic:\n\tListWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller);\n\t~ListWidget();\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<ListMemento*> memento);\n\n\tvoid fillTopBarMenu(\n\t\tconst Ui::Menu::MenuCallback &addAction) override;\n\n\trpl::producer<QString> title() override;\n\trpl::producer<int> desiredHeightValue() const override;\n\n\trpl::producer<SelectedItems> selectedListValue() const override;\n\tvoid selectionAction(SelectionAction action) override;\n\nprivate:\n\tvoid setupSearch();\n\tvoid saveState(not_null<ListMemento*> memento);\n\tvoid restoreState(not_null<ListMemento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tclass Inner;\n\tconst std::unique_ptr<Inner> _inner;\n\n};\n\n} // namespace Info::Polls\n"
  },
  {
    "path": "Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/polls/info_polls_results_inner_widget.h\"\n\n#include \"info/polls/info_polls_results_widget.h\"\n#include \"lang/lang_keys.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_session.h\"\n#include \"ui/controls/peer_list_dummy.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/painter.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"main/main_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_polls.h\"\n\nnamespace Info::Polls {\nnamespace {\n\nconstexpr auto kFirstPage = 15;\nconstexpr auto kPerPage = 50;\nconstexpr auto kLeavePreloaded = 5;\n\nclass ListDelegate final : public PeerListContentDelegate {\npublic:\n\tvoid peerListSetTitle(rpl::producer<QString> title) override;\n\tvoid peerListSetAdditionalTitle(rpl::producer<QString> title) override;\n\tbool peerListIsRowChecked(not_null<PeerListRow*> row) override;\n\tint peerListSelectedRowsCount() override;\n\tvoid peerListScrollToTop() override;\n\tvoid peerListAddSelectedPeerInBunch(\n\t\tnot_null<PeerData*> peer) override;\n\tvoid peerListAddSelectedRowInBunch(\n\t\tnot_null<PeerListRow*> row) override;\n\tvoid peerListFinishSelectedRowsBunch() override;\n\tvoid peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) override;\n\tstd::shared_ptr<Main::SessionShow> peerListUiShow() override;\n\n};\n\nvoid ListDelegate::peerListSetTitle(rpl::producer<QString> title) {\n}\n\nvoid ListDelegate::peerListSetAdditionalTitle(rpl::producer<QString> title) {\n}\n\nbool ListDelegate::peerListIsRowChecked(not_null<PeerListRow*> row) {\n\treturn false;\n}\n\nint ListDelegate::peerListSelectedRowsCount() {\n\treturn 0;\n}\n\nvoid ListDelegate::peerListScrollToTop() {\n}\n\nvoid ListDelegate::peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) {\n\tUnexpected(\"Item selection in Info::Profile::Members.\");\n}\n\nvoid ListDelegate::peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) {\n\tUnexpected(\"Item selection in Info::Profile::Members.\");\n}\n\nvoid ListDelegate::peerListFinishSelectedRowsBunch() {\n}\n\nvoid ListDelegate::peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) {\n\tdescription.destroy();\n}\n\nstd::shared_ptr<Main::SessionShow> ListDelegate::peerListUiShow() {\n\tUnexpected(\"...ListDelegate::peerListUiShow\");\n}\n\nclass VoterRow final : public PeerListRow {\npublic:\n\tVoterRow(not_null<PeerData*> peer, TimeId date);\n\n\tQSize rightActionSize() const override;\n\tQMargins rightActionMargins() const override;\n\tbool rightActionDisabled() const override;\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\nprivate:\n\tvoid computeTexts();\n\n\tTimeId _date = 0;\n\tQString _timeText;\n\tQString _dateText;\n\tint _timeWidth = 0;\n\tint _dateWidth = 0;\n\n};\n\nVoterRow::VoterRow(not_null<PeerData*> peer, TimeId date)\n: PeerListRow(peer)\n, _date(date) {\n\tcomputeTexts();\n}\n\nvoid VoterRow::computeTexts() {\n\tif (!_date) {\n\t\treturn;\n\t}\n\tconst auto parsed = base::unixtime::parse(_date);\n\tconst auto nowDate = base::unixtime::parse(\n\t\tbase::unixtime::now()).date();\n\tconst auto voteDate = parsed.date();\n\t_timeText = QLocale().toString(\n\t\tparsed.time(),\n\t\tQLocale::ShortFormat);\n\t_timeWidth = st::pollResultsVoteTimeFont->width(_timeText);\n\tif (voteDate == nowDate) {\n\t\treturn;\n\t} else if (voteDate.addDays(1) == nowDate) {\n\t\t_dateText = tr::lng_polls_vote_yesterday(tr::now);\n\t} else {\n\t\t_dateText = langDayOfMonthShort(voteDate);\n\t}\n\t_dateWidth = st::pollResultsVoteTimeDateFont->width(_dateText);\n}\n\nQSize VoterRow::rightActionSize() const {\n\tif (!_date) {\n\t\treturn QSize();\n\t}\n\tconst auto &timeFont = st::pollResultsVoteTimeFont;\n\tif (_dateText.isEmpty()) {\n\t\treturn QSize(_timeWidth, timeFont->height);\n\t}\n\tconst auto &dateFont = st::pollResultsVoteTimeDateFont;\n\treturn QSize(\n\t\tstd::max(_timeWidth, _dateWidth),\n\t\tdateFont->height + st::pollResultsVoteTimeGap + timeFont->height);\n}\n\nQMargins VoterRow::rightActionMargins() const {\n\tif (!_date) {\n\t\treturn QMargins();\n\t}\n\tconst auto size = rightActionSize();\n\treturn QMargins(\n\t\tst::pollResultsVoteTimeLeftSkip,\n\t\t(st::infoCommonGroupsListItem.height - size.height()) / 2,\n\t\tst::pollResultsVoteTimeRightSkip,\n\t\t0);\n}\n\nbool VoterRow::rightActionDisabled() const {\n\treturn true;\n}\n\nvoid VoterRow::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\tif (!_date) {\n\t\treturn;\n\t}\n\tconst auto &timeFont = st::pollResultsVoteTimeFont;\n\tconst auto size = rightActionSize();\n\tif (_dateText.isEmpty()) {\n\t\tp.setFont(timeFont);\n\t\tp.setPen(st::windowFg);\n\t\tp.drawText(\n\t\t\tx + size.width() - _timeWidth,\n\t\t\ty + timeFont->ascent,\n\t\t\t_timeText);\n\t} else {\n\t\tconst auto &dateFont = st::pollResultsVoteTimeDateFont;\n\t\tp.setFont(dateFont);\n\t\tp.setPen(st::windowSubTextFg);\n\t\tp.drawText(\n\t\t\tx + size.width() - _dateWidth,\n\t\t\ty + dateFont->ascent,\n\t\t\t_dateText);\n\t\tp.setFont(timeFont);\n\t\tp.setPen(st::windowFg);\n\t\tp.drawText(\n\t\t\tx + size.width() - _timeWidth,\n\t\t\ty + dateFont->height\n\t\t\t\t+ st::pollResultsVoteTimeGap\n\t\t\t\t+ timeFont->ascent,\n\t\t\t_timeText);\n\t}\n}\n\n} // namespace\n\nclass ListController final : public PeerListController {\npublic:\n\tListController(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context,\n\t\tQByteArray option);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\n\tvoid allowLoadMore();\n\tvoid collapse();\n\n\t[[nodiscard]] auto showPeerInfoRequests() const\n\t\t-> rpl::producer<not_null<PeerData*>>;\n\t[[nodiscard]] rpl::producer<int> scrollToRequests() const;\n\t[[nodiscard]] rpl::producer<int> count() const;\n\t[[nodiscard]] rpl::producer<int> fullCount() const;\n\t[[nodiscard]] rpl::producer<int> loadMoreCount() const;\n\n\tstd::unique_ptr<PeerListState> saveState() const override;\n\tvoid restoreState(std::unique_ptr<PeerListState> state) override;\n\n\tstd::unique_ptr<PeerListRow> createRestoredRow(\n\t\tnot_null<PeerData*> peer) override;\n\n\tvoid scrollTo(int y);\n\nprivate:\n\tstruct SavedState : SavedStateBase {\n\t\tQString offset;\n\t\tQString loadForOffset;\n\t\tint leftToLoad = 0;\n\t\tint fullCount = 0;\n\t\tstd::vector<std::pair<not_null<PeerData*>, TimeId>> preloaded;\n\t\tbool wasLoading = false;\n\t\tbase::flat_map<PeerId, TimeId> dates;\n\t};\n\n\tbool appendRow(not_null<PeerData*> peer, TimeId date);\n\tstd::unique_ptr<PeerListRow> createRow(\n\t\tnot_null<PeerData*> peer,\n\t\tTimeId date) const;\n\tvoid addPreloaded();\n\tbool addPreloadedPage();\n\tvoid preloadedAdded();\n\n\tconst not_null<Main::Session*> _session;\n\tconst not_null<PollData*> _poll;\n\tconst FullMsgId _context;\n\tconst QByteArray _option;\n\n\tMTP::Sender _api;\n\n\tQString _offset;\n\tmtpRequestId _loadRequestId = 0;\n\tQString _loadForOffset;\n\tstd::vector<std::pair<not_null<PeerData*>, TimeId>> _preloaded;\n\tbase::flat_map<PeerId, TimeId> _dates;\n\trpl::variable<int> _count = 0;\n\trpl::variable<int> _fullCount;\n\trpl::variable<int> _leftToLoad;\n\n\trpl::event_stream<not_null<PeerData*>> _showPeerInfoRequests;\n\trpl::event_stream<int> _scrollToRequests;\n\n};\n\nListController::ListController(\n\tnot_null<Main::Session*> session,\n\tnot_null<PollData*> poll,\n\tFullMsgId context,\n\tQByteArray option)\n: _session(session)\n, _poll(poll)\n, _context(context)\n, _option(option)\n, _api(&_session->mtp()) {\n\tconst auto i = ranges::find(poll->answers, option, &PollAnswer::option);\n\tAssert(i != poll->answers.end());\n\t_fullCount = i->votes;\n\t_leftToLoad = i->votes;\n}\n\nMain::Session &ListController::session() const {\n\treturn *_session;\n}\n\nvoid ListController::prepare() {\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid ListController::loadMoreRows() {\n\tif (_loadRequestId\n\t\t|| !_leftToLoad.current()\n\t\t|| (!_offset.isEmpty() && _loadForOffset != _offset)\n\t\t|| !_preloaded.empty()) {\n\t\treturn;\n\t}\n\tconst auto item = session().data().message(_context);\n\tif (!item || !item->isRegular()) {\n\t\t_leftToLoad = 0;\n\t\treturn;\n\t}\n\n\tusing Flag = MTPmessages_GetPollVotes::Flag;\n\tconst auto flags = Flag::f_option\n\t\t| (_offset.isEmpty() ? Flag(0) : Flag::f_offset);\n\tconst auto limit = _offset.isEmpty() ? kFirstPage : kPerPage;\n\t_loadRequestId = _api.request(MTPmessages_GetPollVotes(\n\t\tMTP_flags(flags),\n\t\titem->history()->peer->input(),\n\t\tMTP_int(item->id),\n\t\tMTP_bytes(_option),\n\t\tMTP_string(_offset),\n\t\tMTP_int(limit)\n\t)).done([=](const MTPmessages_VotesList &result) {\n\t\tconst auto count = result.match([&](\n\t\t\t\tconst MTPDmessages_votesList &data) {\n\t\t\t_offset = data.vnext_offset().value_or_empty();\n\t\t\tauto &owner = session().data();\n\t\t\towner.processUsers(data.vusers());\n\t\t\towner.processChats(data.vchats());\n\t\t\tauto add = limit - kLeavePreloaded;\n\t\t\tfor (const auto &vote : data.vvotes().v) {\n\t\t\t\tvote.match([&](const auto &data) {\n\t\t\t\t\tconst auto peer = owner.peer(peerFromMTP(data.vpeer()));\n\t\t\t\t\tconst auto date = data.vdate().v;\n\t\t\t\t\tif (peer->isMinimalLoaded()) {\n\t\t\t\t\t\t_dates[peer->id] = date;\n\t\t\t\t\t\tif (add) {\n\t\t\t\t\t\t\tappendRow(peer, date);\n\t\t\t\t\t\t\t--add;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t_preloaded.emplace_back(peer, date);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn data.vcount().v;\n\t\t});\n\t\tif (_offset.isEmpty()) {\n\t\t\taddPreloaded();\n\t\t\t_fullCount = delegate()->peerListFullRowsCount();\n\t\t\t_leftToLoad = 0;\n\t\t} else {\n\t\t\t_count = delegate()->peerListFullRowsCount();\n\t\t\t_fullCount = count;\n\t\t\t_leftToLoad = count - delegate()->peerListFullRowsCount();\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t}\n\t\t_loadRequestId = 0;\n\t}).fail([=] {\n\t\t_loadRequestId = 0;\n\t}).send();\n}\n\nvoid ListController::allowLoadMore() {\n\tif (!addPreloadedPage()) {\n\t\t_loadForOffset = _offset;\n\t\taddPreloaded();\n\t\tloadMoreRows();\n\t}\n}\n\nvoid ListController::collapse() {\n\tconst auto count = delegate()->peerListFullRowsCount();\n\tif (count <= kFirstPage) {\n\t\treturn;\n\t}\n\tconst auto remove = count - (kFirstPage - kLeavePreloaded);\n\tranges::actions::reverse(_preloaded);\n\t_preloaded.reserve(_preloaded.size() + remove);\n\tfor (auto i = 0; i != remove; ++i) {\n\t\tconst auto row = delegate()->peerListRowAt(count - i - 1);\n\t\tconst auto peerId = row->peer()->id;\n\t\tconst auto it = _dates.find(peerId);\n\t\tconst auto date = (it != end(_dates)) ? it->second : TimeId(0);\n\t\t_preloaded.emplace_back(row->peer(), date);\n\t\tdelegate()->peerListRemoveRow(row);\n\t}\n\tranges::actions::reverse(_preloaded);\n\n\tdelegate()->peerListRefreshRows();\n\tconst auto now = count - remove;\n\t_count = now;\n\t_leftToLoad = _fullCount.current() - now;\n}\n\nvoid ListController::addPreloaded() {\n\tfor (const auto &[peer, date] : base::take(_preloaded)) {\n\t\tappendRow(peer, date);\n\t}\n\tpreloadedAdded();\n}\n\nbool ListController::addPreloadedPage() {\n\tif (_preloaded.size() < kPerPage + kLeavePreloaded) {\n\t\treturn false;\n\t}\n\tconst auto from = begin(_preloaded);\n\tconst auto till = from + kPerPage;\n\tfor (auto i = from; i != till; ++i) {\n\t\tappendRow(i->first, i->second);\n\t}\n\t_preloaded.erase(from, till);\n\tpreloadedAdded();\n\treturn true;\n}\n\nvoid ListController::preloadedAdded() {\n\t_count = delegate()->peerListFullRowsCount();\n\t_leftToLoad = _fullCount.current() - _count.current();\n\tdelegate()->peerListRefreshRows();\n}\n\nauto ListController::showPeerInfoRequests() const\n-> rpl::producer<not_null<PeerData*>> {\n\treturn _showPeerInfoRequests.events();\n}\n\nrpl::producer<int> ListController::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nrpl::producer<int> ListController::count() const {\n\treturn _count.value();\n}\n\nrpl::producer<int> ListController::fullCount() const {\n\treturn _fullCount.value();\n}\n\nrpl::producer<int> ListController::loadMoreCount() const {\n\tconst auto initial = (_fullCount.current() <= kFirstPage)\n\t\t? _fullCount.current()\n\t\t: (kFirstPage - kLeavePreloaded);\n\treturn rpl::combine(\n\t\t_count.value(),\n\t\t_leftToLoad.value()\n\t) | rpl::map([=](int count, int leftToLoad) {\n\t\treturn (count > 0) ? leftToLoad : (leftToLoad - initial);\n\t});\n}\n\nauto ListController::saveState() const -> std::unique_ptr<PeerListState> {\n\tauto result = PeerListController::saveState();\n\n\tauto my = std::make_unique<SavedState>();\n\tmy->offset = _offset;\n\tmy->fullCount = _fullCount.current();\n\tmy->leftToLoad = _leftToLoad.current();\n\tmy->preloaded = _preloaded;\n\tmy->wasLoading = (_loadRequestId != 0);\n\tmy->loadForOffset = _loadForOffset;\n\tmy->dates = _dates;\n\tresult->controllerState = std::move(my);\n\n\treturn result;\n}\n\nvoid ListController::restoreState(std::unique_ptr<PeerListState> state) {\n\tauto typeErasedState = state\n\t\t? state->controllerState.get()\n\t\t: nullptr;\n\tif (const auto my = dynamic_cast<SavedState*>(typeErasedState)) {\n\t\tif (const auto requestId = base::take(_loadRequestId)) {\n\t\t\t_api.request(requestId).cancel();\n\t\t}\n\n\t\t_offset = my->offset;\n\t\t_loadForOffset = my->loadForOffset;\n\t\t_preloaded = std::move(my->preloaded);\n\t\t_count = int(state->list.size());\n\t\t_fullCount = my->fullCount;\n\t\t_leftToLoad = my->leftToLoad;\n\t\t_dates = std::move(my->dates);\n\t\tif (my->wasLoading) {\n\t\t\tloadMoreRows();\n\t\t}\n\t\tPeerListController::restoreState(std::move(state));\n\t}\n}\n\nstd::unique_ptr<PeerListRow> ListController::createRestoredRow(\n\t\tnot_null<PeerData*> peer) {\n\tconst auto it = _dates.find(peer->id);\n\tconst auto date = (it != end(_dates)) ? it->second : TimeId(0);\n\treturn createRow(peer, date);\n}\n\nvoid ListController::rowClicked(not_null<PeerListRow*> row) {\n\t_showPeerInfoRequests.fire(row->peer());\n}\n\nbool ListController::appendRow(not_null<PeerData*> peer, TimeId date) {\n\tif (delegate()->peerListFindRow(peer->id.value)) {\n\t\treturn false;\n\t}\n\tdelegate()->peerListAppendRow(createRow(peer, date));\n\treturn true;\n}\n\nstd::unique_ptr<PeerListRow> ListController::createRow(\n\t\tnot_null<PeerData*> peer,\n\t\tTimeId date) const {\n\tauto row = std::make_unique<VoterRow>(peer, date);\n\trow->setCustomStatus(QString());\n\treturn row;\n}\n\nvoid ListController::scrollTo(int y) {\n\t_scrollToRequests.fire_copy(y);\n}\n\nListController *CreateAnswerRows(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<int> visibleTop,\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId context,\n\t\tconst PollAnswer &answer) {\n\tusing namespace rpl::mappers;\n\n\tif (!answer.votes) {\n\t\treturn nullptr;\n\t}\n\n\tconst auto delegate = container->lifetime().make_state<ListDelegate>();\n\tconst auto controller = container->lifetime().make_state<ListController>(\n\t\tsession,\n\t\tpoll,\n\t\tcontext,\n\t\tanswer.option);\n\n\tconst auto percent = answer.votes * 100 / poll->totalVoters;\n\tconst auto phrase = poll->quiz()\n\t\t? tr::lng_polls_answers_count\n\t\t: tr::lng_polls_votes_count;\n\tconst auto sampleText = phrase(\n\t\t\ttr::now,\n\t\t\tlt_count_decimal,\n\t\t\tanswer.votes);\n\tconst auto &font = st::boxDividerLabel.style.font;\n\tconst auto sampleWidth = font->width(sampleText);\n\tconst auto rightSkip = sampleWidth + font->spacew * 4;\n\tconst auto headerWrap = container->add(\n\t\tobject_ptr<Ui::RpWidget>(\n\t\t\tcontainer));\n\n\tcontainer->add(object_ptr<Ui::FixedHeightWidget>(\n\t\tcontainer,\n\t\tst::boxLittleSkip));\n\n\tcontroller->setStyleOverrides(&st::infoCommonGroupsList);\n\tconst auto content = container->add(object_ptr<PeerListContent>(\n\t\tcontainer,\n\t\tcontroller));\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\n\tconst auto count = (answer.votes <= kFirstPage)\n\t\t? answer.votes\n\t\t: (kFirstPage - kLeavePreloaded);\n\tconst auto placeholder = container->add(object_ptr<PeerListDummy>(\n\t\tcontainer,\n\t\tcount,\n\t\tst::infoCommonGroupsList));\n\n\tcontroller->count(\n\t) | rpl::filter(_1 > 0) | rpl::on_next([=] {\n\t\tdelete placeholder;\n\t}, placeholder->lifetime());\n\n\tconst auto header = Ui::CreateChild<Ui::DividerLabel>(\n\t\tcontainer.get(),\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\trpl::single(\n\t\t\t\tTextWithEntities(answer.text)\n\t\t\t\t\t.append(QString::fromUtf8(\" \\xe2\\x80\\x94 \"))\n\t\t\t\t\t.append(QString::number(percent))\n\t\t\t\t\t.append('%')),\n\t\t\tst::boxDividerLabel,\n\t\t\tst::defaultPopupMenu,\n\t\t\tCore::TextContext({ .session = session })),\n\t\tstyle::margins(\n\t\t\tst::pollResultsHeaderPadding.left(),\n\t\t\tst::pollResultsHeaderPadding.top(),\n\t\t\tst::pollResultsHeaderPadding.right() + rightSkip,\n\t\t\tst::pollResultsHeaderPadding.bottom()));\n\n\tconst auto votes = Ui::CreateChild<Ui::FlatLabel>(\n\t\theader,\n\t\tphrase(\n\t\t\tlt_count_decimal,\n\t\t\tcontroller->fullCount() | rpl::map(_1 + 0.)),\n\t\tst::pollResultsVotesCount);\n\tconst auto collapse = Ui::CreateChild<Ui::LinkButton>(\n\t\theader,\n\t\ttr::lng_polls_votes_collapse(tr::now),\n\t\tst::defaultLinkButton);\n\tcollapse->setClickedCallback([=] {\n\t\tcontroller->scrollTo(headerWrap->y());\n\t\tcontroller->collapse();\n\t});\n\trpl::combine(\n\t\tcontroller->fullCount(),\n\t\tcontroller->count()\n\t) | rpl::on_next([=](int fullCount, int count) {\n\t\tconst auto many = (fullCount > kFirstPage)\n\t\t\t&& (count > kFirstPage - kLeavePreloaded);\n\t\tcollapse->setVisible(many);\n\t\tvotes->setVisible(!many);\n\t}, collapse->lifetime());\n\n\theaderWrap->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\theader->resizeToWidth(width);\n\t\tvotes->moveToRight(\n\t\t\tst::pollResultsHeaderPadding.right(),\n\t\t\tst::pollResultsHeaderPadding.top(),\n\t\t\twidth);\n\t\tcollapse->moveToRight(\n\t\t\tst::pollResultsHeaderPadding.right(),\n\t\t\tst::pollResultsHeaderPadding.top(),\n\t\t\twidth);\n\t}, header->lifetime());\n\n\theader->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\theaderWrap->resize(headerWrap->width(), height);\n\t}, header->lifetime());\n\n\tauto moreTopWidget = object_ptr<Ui::RpWidget>(container);\n\tmoreTopWidget->resize(0, 0);\n\tconst auto moreTop = container->add(std::move(moreTopWidget));\n\tconst auto more = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_polls_show_more(\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\tcontroller->loadMoreCount() | rpl::map(_1 + 0.),\n\t\t\t\t\ttr::upper),\n\t\t\t\tst::pollResultsShowMore)));\n\tmore->entity()->setClickedCallback([=] {\n\t\tcontroller->allowLoadMore();\n\t});\n\tcontroller->loadMoreCount(\n\t) | rpl::map(_1 > 0) | rpl::on_next([=](bool visible) {\n\t\tmore->toggle(visible, anim::type::instant);\n\t}, more->lifetime());\n\n\tcontainer->add(object_ptr<Ui::FixedHeightWidget>(\n\t\tcontainer,\n\t\tst::boxLittleSkip));\n\n\trpl::combine(\n\t\tstd::move(visibleTop),\n\t\theaderWrap->geometryValue(),\n\t\tmoreTop->topValue()\n\t) | rpl::filter([=](int, QRect headerRect, int moreTop) {\n\t\treturn moreTop >= headerRect.y() + headerRect.height();\n\t}) | rpl::on_next([=](\n\t\t\tint visibleTop,\n\t\t\tQRect headerRect,\n\t\t\tint moreTop) {\n\t\tconst auto skip = st::pollResultsHeaderPadding.top()\n\t\t\t- st::pollResultsHeaderPadding.bottom();\n\t\tconst auto top = std::clamp(\n\t\t\tvisibleTop - skip,\n\t\t\theaderRect.y(),\n\t\t\tmoreTop - headerRect.height());\n\t\theader->move(0, top);\n\t}, header->lifetime());\n\n\treturn controller;\n}\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller,\n\tnot_null<PollData*> poll,\n\tFullMsgId contextId)\n: RpWidget(parent)\n, _controller(controller)\n, _poll(poll)\n, _contextId(contextId)\n, _content(this) {\n\tsetupContent();\n}\n\nvoid InnerWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tsetChildVisibleTopBottom(_content, visibleTop, visibleBottom);\n\t_visibleTop = visibleTop;\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\tauto states = base::flat_map<\n\t\tQByteArray,\n\t\tstd::unique_ptr<PeerListState>>();\n\tfor (const auto &[option, controller] : _sections) {\n\t\tstates[option] = controller->saveState();\n\t}\n\tmemento->setListStates(std::move(states));\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\tauto states = memento->listStates();\n\tfor (const auto &[option, controller] : _sections) {\n\t\tconst auto i = states.find(option);\n\t\tif (i != end(states)) {\n\t\t\tcontroller->restoreState(std::move(i->second));\n\t\t}\n\t}\n}\n\nint InnerWidget::desiredHeight() const {\n\tauto desired = 0;\n\t//auto count = qMax(_user->commonChatsCount(), 1);\n\t//desired += qMax(count, _list->fullRowsCount())\n\t//\t* st::infoCommonGroupsList.item.height;\n\treturn qMax(height(), desired);\n}\n\nvoid InnerWidget::setupContent() {\n\t_content->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_content,\n\t\t\trpl::single(_poll->question),\n\t\t\tst::pollResultsQuestion,\n\t\t\tst::defaultPopupMenu,\n\t\t\tCore::TextContext({ .session = &_controller->session() })),\n\t\tst::boxRowPadding);\n\tUi::AddSkip(_content, st::boxLittleSkip / 2);\n\t_content->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_content,\n\t\t\ttr::lng_polls_votes_count(\n\t\t\t\tlt_count_decimal,\n\t\t\t\trpl::single(float64(_poll->totalVoters))),\n\t\t\tst::boxDividerLabel),\n\t\tst::boxRowPadding);\n\tUi::AddSkip(_content, st::boxLittleSkip);\n\tfor (const auto &answer : _poll->answers) {\n\t\tconst auto session = &_controller->session();\n\t\tconst auto controller = CreateAnswerRows(\n\t\t\t_content,\n\t\t\t_visibleTop.value(),\n\t\t\tsession,\n\t\t\t_poll,\n\t\t\t_contextId,\n\t\t\tanswer);\n\t\tif (!controller) {\n\t\t\tcontinue;\n\t\t}\n\t\tcontroller->showPeerInfoRequests(\n\t\t) | rpl::start_to_stream(\n\t\t\t_showPeerInfoRequests,\n\t\t\tlifetime());\n\t\tcontroller->scrollToRequests(\n\t\t) | rpl::on_next([=](int y) {\n\t\t\t_scrollToRequests.fire({ y, -1 });\n\t\t}, lifetime());\n\t\t_sections.emplace(answer.option, controller);\n\t}\n\n\twidthValue(\n\t) | rpl::on_next([=](int newWidth) {\n\t\t_content->resizeToWidth(newWidth);\n\t}, _content->lifetime());\n\n\t_content->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tresize(width(), height);\n\t}, _content->lifetime());\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nauto InnerWidget::showPeerInfoRequests() const\n-> rpl::producer<not_null<PeerData*>> {\n\treturn _showPeerInfoRequests.events();\n}\n\n} // namespace Info::Polls\n"
  },
  {
    "path": "Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Info {\nclass Controller;\n} // namespace Info\n\nnamespace Info::Polls {\n\nclass Memento;\nclass ListController;\n\nclass InnerWidget final : public Ui::RpWidget {\npublic:\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId contextId);\n\n\t[[nodiscard]] not_null<PollData*> poll() const {\n\t\treturn _poll;\n\t}\n\t[[nodiscard]] FullMsgId contextId() const {\n\t\treturn _contextId;\n\t}\n\n\t[[nodiscard]] auto scrollToRequests() const\n\t\t-> rpl::producer<Ui::ScrollToRequest>;\n\n\t[[nodiscard]] auto showPeerInfoRequests() const\n\t\t-> rpl::producer<not_null<PeerData*>>;\n\n\t[[nodiscard]] int desiredHeight() const;\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\nprivate:\n\tvoid setupContent();\n\n\tnot_null<Controller*> _controller;\n\tnot_null<PollData*> _poll;\n\tFullMsgId _contextId;\n\tobject_ptr<Ui::VerticalLayout> _content;\n\tbase::flat_map<QByteArray, not_null<ListController*>> _sections;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\trpl::event_stream<not_null<PeerData*>> _showPeerInfoRequests;\n\trpl::variable<int> _visibleTop = 0;\n\n};\n\n} // namespace Info::Polls\n"
  },
  {
    "path": "Telegram/SourceFiles/info/polls/info_polls_results_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/polls/info_polls_results_widget.h\"\n\n#include \"info/polls/info_polls_results_inner_widget.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"data/data_poll.h\"\n#include \"ui/ui_utility.h\"\n\nnamespace Info::Polls {\n\nMemento::Memento(not_null<PollData*> poll, FullMsgId contextId)\n: ContentMemento(poll, contextId) {\n}\n\nSection Memento::section() const {\n\treturn Section(Section::Type::PollResults);\n}\n\nvoid Memento::setListStates(base::flat_map<\n\t\tQByteArray,\n\t\tstd::unique_ptr<PeerListState>> states) {\n\t_listStates = std::move(states);\n}\n\nauto Memento::listStates()\n-> base::flat_map<QByteArray, std::unique_ptr<PeerListState>> {\n\treturn std::move(_listStates);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(parent, controller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nMemento::~Memento() = default;\n\nWidget::Widget(QWidget *parent, not_null<Controller*> controller)\n: ContentWidget(parent, controller)\n, _inner(setInnerWidget(\n\tobject_ptr<InnerWidget>(\n\t\tthis,\n\t\tcontroller,\n\t\tcontroller->poll(),\n\t\tcontroller->pollContextId()))) {\n\t_inner->showPeerInfoRequests(\n\t) | rpl::on_next([=](not_null<PeerData*> peer) {\n\t\tcontroller->showPeerInfo(peer);\n\t}, _inner->lifetime());\n\t_inner->scrollToRequests(\n\t) | rpl::on_next([=](const Ui::ScrollToRequest &request) {\n\t\tscrollTo(request);\n\t}, _inner->lifetime());\n}\n\nnot_null<PollData*> Widget::poll() const {\n\treturn _inner->poll();\n}\n\nFullMsgId Widget::contextId() const {\n\treturn _inner->contextId();\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\t//if (const auto myMemento = dynamic_cast<Memento*>(memento.get())) {\n\t//\tAssert(myMemento->self() == self());\n\n\t//\tif (_inner->showInternal(myMemento)) {\n\t//\t\treturn true;\n\t//\t}\n\t//}\n\treturn false;\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nrpl::producer<QString> Widget::title() {\n\treturn poll()->quiz()\n\t\t? tr::lng_polls_quiz_results_title()\n\t\t: tr::lng_polls_poll_results_title();\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(poll(), contextId());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\n} // namespace Info::Polls\n"
  },
  {
    "path": "Telegram/SourceFiles/info/polls/info_polls_results_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n#include \"info/info_controller.h\"\n\nstruct PeerListState;\n\nnamespace Info::Polls {\n\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\tMemento(not_null<PollData*> poll, FullMsgId contextId);\n\t~Memento();\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\tvoid setListStates(base::flat_map<\n\t\tQByteArray,\n\t\tstd::unique_ptr<PeerListState>> states);\n\tauto listStates()\n\t-> base::flat_map<QByteArray, std::unique_ptr<PeerListState>>;\n\nprivate:\n\tbase::flat_map<\n\t\tQByteArray,\n\t\tstd::unique_ptr<PeerListState>> _listStates;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(QWidget *parent, not_null<Controller*> controller);\n\n\t[[nodiscard]] not_null<PollData*> poll() const;\n\t[[nodiscard]] FullMsgId contextId() const;\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\n\trpl::producer<QString> title() override;\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tnot_null<InnerWidget*> _inner;\n\n};\n\n} // namespace Info::Polls\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_levels.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\n\nLevelShape {\n\ticon: icon;\n\tposition: point;\n}\nlevelStyle: TextStyle(defaultTextStyle) {\n\tfont: font(9px semibold);\n}\nlevelTextFg: windowFgActive;\nlevelMargin: margins(4px, 3px, 2px, 2px);\n\nlevelBase: LevelShape {\n\tposition: point(0px, 6px);\n}\nlevel1: LevelShape(levelBase) {\n\tposition: point(-1px, 6px);\n\ticon: icon {{ \"levels/level1_inner-26x26\", windowBgActive }};\n}\nlevel2: LevelShape(level1) {\n\ticon: icon {{ \"levels/level2_inner-26x26\", windowBgActive }};\n}\nlevel3: LevelShape(level1) {\n\ticon: icon {{ \"levels/level3_inner-26x26\", windowBgActive }};\n}\nlevel4: LevelShape(level1) {\n\ticon: icon {{ \"levels/level4_inner-26x26\", windowBgActive }};\n}\nlevel5: LevelShape(level1) {\n\ticon: icon {{ \"levels/level5_inner-26x26\", windowBgActive }};\n}\nlevel6: LevelShape(level1) {\n\ticon: icon {{ \"levels/level6_inner-26x26\", windowBgActive }};\n}\nlevel7: LevelShape(level1) {\n\ticon: icon {{ \"levels/level7_inner-26x26\", windowBgActive }};\n}\nlevel8: LevelShape(level1) {\n\ticon: icon {{ \"levels/level8_inner-26x26\", windowBgActive }};\n}\nlevel9: LevelShape(level1) {\n\ticon: icon {{ \"levels/level9_inner-26x26\", windowBgActive }};\n}\nlevel10: LevelShape(levelBase) {\n\ticon: icon {{ \"levels/level10_inner-26x26\", windowBgActive }};\n}\nlevel20: LevelShape(levelBase) {\n\ticon: icon {{ \"levels/level20_inner-26x26\", windowBgActive }};\n}\nlevel30: LevelShape(levelBase) {\n\ticon: icon {{ \"levels/level30_inner-26x26\", windowBgActive }};\n}\nlevel40: LevelShape(levelBase) {\n\ticon: icon {{ \"levels/level40_inner-26x26\", windowBgActive }};\n}\nlevel50: LevelShape(levelBase) {\n\ticon: icon {{ \"levels/level50_inner-26x26\", windowBgActive }};\n}\nlevel60: LevelShape(levelBase) {\n\ticon: icon {{ \"levels/level60_inner-26x26\", windowBgActive }};\n}\nlevel70: LevelShape(levelBase) {\n\ticon: icon {{ \"levels/level70_inner-26x26\", windowBgActive }};\n}\nlevel80: LevelShape(levelBase) {\n\ticon: icon {{ \"levels/level80_inner-26x26\", windowBgActive }};\n}\nlevel90: LevelShape(levelBase) {\n\ticon: icon {{ \"levels/level90_inner-26x26\", windowBgActive }};\n}\nlevelNegative: LevelShape(levelBase) {\n\ticon: icon {{ \"levels/level_warning-18x18\", attentionButtonFg, point(6px, 5px) }};\n}\nlevelNegativeBubble: icon {{ \"levels/level_warning-28x28\", windowFgActive }};\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_actions.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_actions.h\"\n\n#include \"api/api_blocked_peers.h\"\n#include \"api/api_chat_participants.h\"\n#include \"api/api_credits.h\"\n#include \"api/api_statistics.h\"\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"base/event_filter.h\"\n#include \"base/options.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/peers/add_bot_to_chat_box.h\"\n#include \"boxes/peers/edit_contact_box.h\"\n#include \"boxes/peers/edit_participants_box.h\"\n#include \"boxes/peers/edit_peer_info_box.h\"\n#include \"boxes/peers/verify_peers_box.h\"\n#include \"boxes/report_messages_box.h\"\n#include \"boxes/share_box.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"boxes/translate_box.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\"\n#include \"data/business/data_business_common.h\"\n#include \"data/business/data_business_info.h\"\n#include \"data/components/credits.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"dialogs/ui/dialogs_message_view.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/view/history_view_item_preview.h\"\n#include \"info/bot/earn/info_bot_earn_widget.h\"\n#include \"info/bot/starref/info_bot_starref_common.h\"\n#include \"info/channel_statistics/earn/earn_format.h\"\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"info/channel_statistics/earn/info_channel_earn_list.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"info/profile/info_profile_phone_menu.h\"\n#include \"info/profile/info_profile_text.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"info/profile/info_profile_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n#include \"iv/iv_instance.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_mute.h\"\n#include \"settings/settings_common.h\"\n#include \"support/support_helper.h\"\n#include \"ui/boxes/peer_qr_box.h\"\n#include \"ui/boxes/report_box_graphics.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/effects/toggle_arrow.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/text/text_variant.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_controller.h\" // Window::Controller::show.\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_channel_earn.h\" // st::channelEarnCurrencyCommonMargins\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\" // settingsButtonRightSkip.\n#include \"styles/style_window.h\" // mainMenuToggleFourStrokes.\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QClipboard>\n\n#include \"ui/text/format_values.h\"\n#include \"base/unixtime.h\"\n\nnamespace Info {\nnamespace Profile {\nnamespace {\n\nconstexpr auto kDay = Data::WorkingInterval::kDay;\n\nbase::options::toggle ShowPeerIdBelowAbout({\n\t.id = kOptionShowPeerIdBelowAbout,\n\t.name = \"Show Peer IDs in Profile\",\n\t.description = \"Show peer IDs from API below their Bio / Description.\"\n\t\t\" Add contact IDs to exported data.\",\n});\n\nbase::options::toggle ShowChannelJoinedBelowAbout({\n\t.id = kOptionShowChannelJoinedBelowAbout,\n\t.name = \"Show Channel Joined Date in Profile\",\n\t.description = \"Show when you join Channel under its Description.\",\n});\n\nrpl::producer<TextWithEntities> IDValue(not_null<PeerData*> peer) {\n\tconst auto peerId = peer->id;\n\tconst auto id = peer->isUser()\n\t\t? peerToUser(peerId).bare\n\t\t: peer->isChat()\n\t\t? peerToChat(peerId).bare\n\t\t: peer->isChannel()\n\t\t? peerToChannel(peerId).bare\n\t\t: 0;\n\treturn rpl::single(Ui::Text::WithEntities(QString::number(id)));\n}\n\n[[nodiscard]] rpl::producer<TextWithEntities> UsernamesSubtext(\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<QString> fallback) {\n\treturn rpl::combine(\n\t\tUsernamesValue(peer),\n\t\tstd::move(fallback)\n\t) | rpl::map([](std::vector<TextWithEntities> usernames, QString text) {\n\t\tif (usernames.size() < 2) {\n\t\t\treturn TextWithEntities{ .text = text };\n\t\t} else {\n\t\t\tauto result = TextWithEntities();\n\t\t\tresult.append(tr::lng_info_usernames_label(tr::now));\n\t\t\tresult.append(' ');\n\t\t\tauto &&subrange = ranges::make_subrange(\n\t\t\t\tbegin(usernames) + 1,\n\t\t\t\tend(usernames));\n\t\t\tfor (auto &username : std::move(subrange)) {\n\t\t\t\tconst auto isLast = (usernames.back() == username);\n\t\t\t\tresult.append(tr::link(\n\t\t\t\t\t'@' + base::take(username.text),\n\t\t\t\t\tusername.entities.front().data()));\n\t\t\t\tif (!isLast) {\n\t\t\t\t\tresult.append(u\", \"_q);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\t});\n}\n\n[[nodiscard]] rpl::producer<TextWithEntities> TopicSubtext(\n\t\tnot_null<PeerData*> peer) {\n\treturn rpl::conditional(\n\t\tUsernamesValue(peer) | rpl::map([](std::vector<TextWithEntities> v) {\n\t\t\treturn !v.empty();\n\t\t}),\n\t\ttr::lng_filters_link_subtitle(tr::marked),\n\t\ttr::lng_info_link_topic_label(tr::marked));\n}\n\n[[nodiscard]] Fn<void(QString)> UsernamesLinkCallback(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &addToLink) {\n\tconst auto weak = base::make_weak(controller);\n\treturn [=](QString link) {\n\t\tif (link.startsWith(u\"internal:\"_q)) {\n\t\t\tCore::App().openInternalUrl(link,\n\t\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t\t.sessionWindow = weak,\n\t\t\t\t}));\n\t\t\treturn;\n\t\t} else if (!link.startsWith(u\"https://\"_q)) {\n\t\t\tlink = peer->session().createInternalLinkFull(peer->username())\n\t\t\t\t+ addToLink;\n\t\t}\n\t\tif (!link.isEmpty()) {\n\t\t\tTextUtilities::SetClipboardText({ link });\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->showToast(\n\t\t\t\t\ttr::lng_channel_public_link_copied(tr::now));\n\t\t\t}\n\t\t}\n\t};\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> CreateSkipWidget(\n\t\tnot_null<Ui::RpWidget*> parent) {\n\treturn Ui::CreateSkipWidget(parent, st::infoProfileSkip);\n}\n\n[[nodiscard]] object_ptr<Ui::SlideWrap<>> CreateSlideSkipWidget(\n\t\tnot_null<Ui::RpWidget*> parent) {\n\tauto result = Ui::CreateSlideSkipWidget(\n\t\tparent,\n\t\tst::infoProfileSkip);\n\tresult->setDuration(st::infoSlideDuration);\n\treturn result;\n}\n\n[[nodiscard]] rpl::producer<TextWithEntities> AboutWithAdvancedValue(\n\t\tnot_null<PeerData*> peer) {\n\n\treturn AboutValue(\n\t\tpeer\n\t) | rpl::map([=](TextWithEntities &&value) {\n\t\tif (ShowPeerIdBelowAbout.value()) {\n\t\t\tusing namespace Ui::Text;\n\t\t\tif (!value.empty()) {\n\t\t\t\tvalue.append(\"\\n\\n\");\n\t\t\t}\n\t\t\tvalue.append(Italic(u\"id: \"_q));\n\t\t\tconst auto raw = peer->id.value & PeerId::kChatTypeMask;\n\t\t\tvalue.append(Link(\n\t\t\t\tItalic(Lang::FormatCountDecimal(raw)),\n\t\t\t\t\"internal:~peer_id~:copy:\" + QString::number(raw)));\n\t\t}\n\t\tif (ShowChannelJoinedBelowAbout.value()) {\n\t\t\tif (const auto channel = peer->asChannel()) {\n\t\t\t\tif (!channel->amCreator() && channel->inviteDate) {\n\t\t\t\t\tif (!value.empty()) {\n\t\t\t\t\t\tif (ShowPeerIdBelowAbout.value()) {\n\t\t\t\t\t\t\tvalue.append(\"\\n\");\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tvalue.append(\"\\n\\n\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tusing namespace Ui::Text;\n\t\t\t\t\tvalue.append((channel->isMegagroup()\n\t\t\t\t\t\t? tr::lng_you_joined_group\n\t\t\t\t\t\t: tr::lng_action_you_joined)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\ttr::italic));\n\t\t\t\t\tvalue.append(Italic(\": \"));\n\t\t\t\t\tconst auto raw = channel->inviteDate;\n\t\t\t\t\tvalue.append(Link(\n\t\t\t\t\t\tItalic(langDateTimeFull(base::unixtime::parse(raw))),\n\t\t\t\t\t\t\"internal:~join_date~:show:\" + QString::number(raw)));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn std::move(value);\n\t});\n}\n\n[[nodiscard]] bool AreNonTrivialHours(const Data::WorkingHours &hours) {\n\tif (!hours) {\n\t\treturn false;\n\t}\n\tconst auto &intervals = hours.intervals.list;\n\tfor (auto i = 0; i != 7; ++i) {\n\t\tconst auto day = Data::WorkingInterval{ i * kDay, (i + 1) * kDay };\n\t\tfor (const auto &interval : intervals) {\n\t\t\tconst auto intersection = interval.intersected(day);\n\t\t\tif (intersection && intersection != day) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\n[[nodiscard]] TimeId OpensIn(\n\t\tconst Data::WorkingIntervals &intervals,\n\t\tTimeId now) {\n\tusing namespace Data;\n\n\twhile (now < 0) {\n\t\tnow += WorkingInterval::kWeek;\n\t}\n\twhile (now > WorkingInterval::kWeek) {\n\t\tnow -= WorkingInterval::kWeek;\n\t}\n\tauto closest = WorkingInterval::kWeek;\n\tfor (const auto &interval : intervals.list) {\n\t\tif (interval.start <= now && interval.end > now) {\n\t\t\treturn TimeId(0);\n\t\t} else if (interval.start > now && interval.start - now < closest) {\n\t\t\tclosest = interval.start - now;\n\t\t} else if (interval.start < now) {\n\t\t\tconst auto next = interval.start + WorkingInterval::kWeek - now;\n\t\t\tif (next < closest) {\n\t\t\t\tclosest = next;\n\t\t\t}\n\t\t}\n\t}\n\treturn closest;\n}\n\n[[nodiscard]] rpl::producer<QString> OpensInText(\n\t\trpl::producer<TimeId> in,\n\t\trpl::producer<bool> hoursExpanded,\n\t\trpl::producer<QString> fallback) {\n\treturn rpl::combine(\n\t\tstd::move(in),\n\t\tstd::move(hoursExpanded),\n\t\tstd::move(fallback)\n\t) | rpl::map([](TimeId in, bool hoursExpanded, QString fallback) {\n\t\treturn (!in || hoursExpanded)\n\t\t\t? std::move(fallback)\n\t\t\t: (in >= 86400)\n\t\t\t? tr::lng_info_hours_opens_in_days(tr::now, lt_count, in / 86400)\n\t\t\t: (in >= 3600)\n\t\t\t? tr::lng_info_hours_opens_in_hours(tr::now, lt_count, in / 3600)\n\t\t\t: tr::lng_info_hours_opens_in_minutes(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tstd::max(in / 60, 1));\n\t});\n}\n\n[[nodiscard]] QString FormatDayTime(TimeId time) {\n\tconst auto wrap = [](TimeId value) {\n\t\tconst auto hours = value / 3600;\n\t\tconst auto minutes = (value % 3600) / 60;\n\t\treturn QString::number(hours).rightJustified(2, u'0')\n\t\t\t+ ':'\n\t\t\t+ QString::number(minutes).rightJustified(2, u'0');\n\t};\n\treturn (time > kDay)\n\t\t? tr::lng_info_hours_next_day(tr::now, lt_time, wrap(time - kDay))\n\t\t: wrap(time == kDay ? 0 : time);\n}\n\n[[nodiscard]] QString JoinIntervals(const Data::WorkingIntervals &data) {\n\tauto result = QStringList();\n\tresult.reserve(data.list.size());\n\tfor (const auto &interval : data.list) {\n\t\tconst auto start = FormatDayTime(interval.start);\n\t\tconst auto end = FormatDayTime(interval.end);\n\t\tresult.push_back(start + u\" - \"_q + end);\n\t}\n\treturn result.join('\\n');\n}\n\n[[nodiscard]] QString FormatDayHours(\n\t\tconst Data::WorkingHours &hours,\n\t\tconst Data::WorkingIntervals &mine,\n\t\tbool my,\n\t\tint day) {\n\tusing namespace Data;\n\n\tconst auto local = ExtractDayIntervals(hours.intervals, day);\n\tif (IsFullOpen(local)) {\n\t\treturn tr::lng_info_hours_open_full(tr::now);\n\t}\n\tconst auto use = my ? ExtractDayIntervals(mine, day) : local;\n\tif (!use) {\n\t\treturn tr::lng_info_hours_closed(tr::now);\n\t}\n\treturn JoinIntervals(use);\n}\n\n[[nodiscard]] Data::WorkingIntervals ShiftedIntervals(\n\t\tData::WorkingIntervals intervals,\n\t\tint delta) {\n\tauto &list = intervals.list;\n\tif (!delta || list.empty()) {\n\t\treturn { std::move(list) };\n\t}\n\tfor (auto &interval : list) {\n\t\tinterval.start += delta;\n\t\tinterval.end += delta;\n\t}\n\twhile (list.front().start < 0) {\n\t\tconstexpr auto kWeek = Data::WorkingInterval::kWeek;\n\t\tconst auto first = list.front();\n\t\tif (first.end > 0) {\n\t\t\tlist.push_back({ first.start + kWeek, kWeek });\n\t\t\tlist.front().start = 0;\n\t\t} else {\n\t\t\tlist.push_back(first.shifted(kWeek));\n\t\t\tlist.erase(list.begin());\n\t\t}\n\t}\n\treturn intervals.normalized();\n}\n\n[[nodiscard]] object_ptr<Ui::SlideWrap<>> CreateWorkingHours(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<UserData*> user) {\n\tusing namespace Data;\n\n\tauto result = object_ptr<Ui::SlideWrap<Ui::RoundButton>>(\n\t\tparent,\n\t\tobject_ptr<Ui::RoundButton>(\n\t\t\tparent,\n\t\t\trpl::single(QString()),\n\t\t\tst::infoHoursOuter),\n\t\tst::infoProfileLabeledPadding - st::infoHoursOuterMargin);\n\tconst auto button = result->entity();\n\tbutton->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\tconst auto inner = Ui::CreateChild<Ui::VerticalLayout>(button);\n\tbutton->widthValue() | rpl::on_next([=](int width) {\n\t\tconst auto margin = st::infoHoursOuterMargin;\n\t\tinner->resizeToWidth(width - margin.left() - margin.right());\n\t\tinner->move(margin.left(), margin.top());\n\t}, inner->lifetime());\n\tinner->heightValue() | rpl::on_next([=](int height) {\n\t\tconst auto margin = st::infoHoursOuterMargin;\n\t\theight += margin.top() + margin.bottom();\n\t\tbutton->resize(button->width(), height);\n\t}, inner->lifetime());\n\n\tconst auto info = &user->owner().businessInfo();\n\n\tstruct State {\n\t\trpl::variable<WorkingHours> hours;\n\t\trpl::variable<TimeId> time;\n\t\trpl::variable<int> day;\n\t\trpl::variable<int> timezoneDelta;\n\n\t\trpl::variable<WorkingIntervals> mine;\n\t\trpl::variable<WorkingIntervals> mineByDays;\n\t\trpl::variable<TimeId> opensIn;\n\t\trpl::variable<bool> opened;\n\t\trpl::variable<bool> expanded;\n\t\trpl::variable<bool> nonTrivial;\n\t\trpl::variable<bool> myTimezone;\n\n\t\trpl::event_stream<> recounts;\n\t};\n\tconst auto state = inner->lifetime().make_state<State>();\n\n\tauto recounts = state->recounts.events_starting_with_copy(rpl::empty);\n\tconst auto recount = [=] {\n\t\tstate->recounts.fire({});\n\t};\n\n\tstate->hours = user->session().changes().peerFlagsValue(\n\t\tuser,\n\t\tPeerUpdate::Flag::BusinessDetails\n\t) | rpl::map([=] {\n\t\treturn user->businessDetails().hours;\n\t});\n\tstate->nonTrivial = state->hours.value() | rpl::map(AreNonTrivialHours);\n\n\tconst auto seconds = QTime::currentTime().msecsSinceStartOfDay() / 1000;\n\tconst auto inMinute = seconds % 60;\n\tconst auto firstTick = inMinute ? (61 - inMinute) : 1;\n\tstate->time = rpl::single(rpl::empty) | rpl::then(\n\t\tbase::timer_once(firstTick * crl::time(1000))\n\t) | rpl::then(\n\t\tbase::timer_each(60 * crl::time(1000))\n\t) | rpl::map([] {\n\t\tconst auto local = QDateTime::currentDateTime();\n\t\tconst auto day = local.date().dayOfWeek() - 1;\n\t\tconst auto seconds = local.time().msecsSinceStartOfDay() / 1000;\n\t\treturn day * kDay + seconds;\n\t});\n\n\tstate->day = state->time.value() | rpl::map([](TimeId time) {\n\t\treturn time / kDay;\n\t});\n\tstate->timezoneDelta = rpl::combine(\n\t\tstate->hours.value(),\n\t\tinfo->timezonesValue()\n\t) | rpl::filter([](\n\t\t\tconst WorkingHours &hours,\n\t\t\tconst Timezones &timezones) {\n\t\treturn ranges::contains(\n\t\t\ttimezones.list,\n\t\t\thours.timezoneId,\n\t\t\t&Timezone::id);\n\t}) | rpl::map([](WorkingHours &&hours, const Timezones &timezones) {\n\t\tconst auto &list = timezones.list;\n\t\tconst auto closest = FindClosestTimezoneId(list);\n\t\tconst auto i = ranges::find(list, closest, &Timezone::id);\n\t\tconst auto j = ranges::find(list, hours.timezoneId, &Timezone::id);\n\t\tAssert(i != end(list));\n\t\tAssert(j != end(list));\n\t\treturn i->utcOffset - j->utcOffset;\n\t});\n\n\tstate->mine = rpl::combine(\n\t\tstate->hours.value(),\n\t\tstate->timezoneDelta.value()\n\t) | rpl::map([](WorkingHours &&hours, int delta) {\n\t\treturn ShiftedIntervals(hours.intervals, delta);\n\t});\n\n\tstate->opensIn = rpl::combine(\n\t\tstate->mine.value(),\n\t\tstate->time.value()\n\t) | rpl::map([](const WorkingIntervals &mine, TimeId time) {\n\t\treturn OpensIn(mine, time);\n\t});\n\tstate->opened = state->opensIn.value() | rpl::map(rpl::mappers::_1 == 0);\n\n\tstate->mineByDays = rpl::combine(\n\t\tstate->hours.value(),\n\t\tstate->timezoneDelta.value()\n\t) | rpl::map([](WorkingHours &&hours, int delta) {\n\t\tauto full = std::array<bool, 7>();\n\t\tauto withoutFullDays = hours.intervals;\n\t\tfor (auto i = 0; i != 7; ++i) {\n\t\t\tif (IsFullOpen(ExtractDayIntervals(hours.intervals, i))) {\n\t\t\t\tfull[i] = true;\n\t\t\t\twithoutFullDays = ReplaceDayIntervals(\n\t\t\t\t\twithoutFullDays,\n\t\t\t\t\ti,\n\t\t\t\t\tData::WorkingIntervals());\n\t\t\t}\n\t\t}\n\t\tauto result = ShiftedIntervals(withoutFullDays, delta);\n\t\tfor (auto i = 0; i != 7; ++i) {\n\t\t\tif (full[i]) {\n\t\t\t\tresult = ReplaceDayIntervals(\n\t\t\t\t\tresult,\n\t\t\t\t\ti,\n\t\t\t\t\tData::WorkingIntervals{ { { 0, kDay } } });\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t});\n\n\tconst auto dayHoursText = [=](int day) {\n\t\treturn rpl::combine(\n\t\t\tstate->hours.value(),\n\t\t\tstate->mineByDays.value(),\n\t\t\tstate->myTimezone.value()\n\t\t) | rpl::map([=](\n\t\t\t\tconst WorkingHours &hours,\n\t\t\t\tconst WorkingIntervals &mine,\n\t\t\t\tbool my) {\n\t\t\treturn FormatDayHours(hours, mine, my, day);\n\t\t});\n\t};\n\tconst auto dayHoursTextValue = [=](rpl::producer<int> day) {\n\t\treturn std::move(day)\n\t\t\t| rpl::map(dayHoursText)\n\t\t\t| rpl::flatten_latest();\n\t};\n\n\tconst auto openedWrap = inner->add(object_ptr<Ui::RpWidget>(inner));\n\tconst auto opened = Ui::CreateChild<Ui::FlatLabel>(\n\t\topenedWrap,\n\t\trpl::conditional(\n\t\t\tstate->opened.value(),\n\t\t\ttr::lng_info_work_open(),\n\t\t\ttr::lng_info_work_closed()\n\t\t) | rpl::after_next(recount),\n\t\tst::infoHoursState);\n\topened->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tconst auto timing = Ui::CreateChild<Ui::FlatLabel>(\n\t\topenedWrap,\n\t\tOpensInText(\n\t\t\tstate->opensIn.value(),\n\t\t\tstate->expanded.value(),\n\t\t\tdayHoursTextValue(state->day.value())\n\t\t) | rpl::after_next(recount),\n\t\tst::infoHoursValue);\n\tconst auto timingArrow = Ui::CreateChild<Ui::RpWidget>(openedWrap);\n\ttimingArrow->resize(Size(timing->st().style.font->height));\n\ttiming->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tstate->opened.value() | rpl::on_next([=](bool value) {\n\t\topened->setTextColorOverride(value\n\t\t\t? st::boxTextFgGood->c\n\t\t\t: st::boxTextFgError->c);\n\t}, opened->lifetime());\n\n\trpl::combine(\n\t\topenedWrap->widthValue(),\n\t\topened->heightValue(),\n\t\ttiming->sizeValue()\n\t) | rpl::on_next([=](int width, int h1, QSize size) {\n\t\topened->moveToLeft(0, 0, width);\n\t\ttimingArrow->moveToRight(0, 0, width);\n\t\ttiming->moveToRight(timingArrow->width(), 0, width);\n\n\t\tconst auto margins = opened->getMargins();\n\t\tconst auto added = margins.top() + margins.bottom();\n\t\topenedWrap->resize(width, std::max(h1, size.height()) - added);\n\t}, openedWrap->lifetime());\n\n\trpl::combine(\n\t\tstate->opened.value(),\n\t\tstate->opensIn.value(),\n\t\tstate->expanded.value(),\n\t\tdayHoursTextValue(state->day.value())\n\t) | rpl::on_next([=](\n\t\t\tbool opened,\n\t\t\tTimeId opensIn,\n\t\t\tbool expanded,\n\t\t\tconst QString &timing) {\n\t\tconst auto status = (opened\n\t\t\t? tr::lng_info_work_open\n\t\t\t: tr::lng_info_work_closed)(tr::now);\n\t\tconst auto when = (!opensIn || expanded)\n\t\t\t? timing\n\t\t\t: (opensIn >= 86400)\n\t\t\t? tr::lng_info_hours_opens_in_days(tr::now, lt_count, opensIn / 86400)\n\t\t\t: (opensIn >= 3600)\n\t\t\t? tr::lng_info_hours_opens_in_hours(tr::now, lt_count, opensIn / 3600)\n\t\t\t: tr::lng_info_hours_opens_in_minutes(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tstd::max(opensIn / 60, 1));\n\t\tbutton->setAccessibleName(\n\t\t\ttr::lng_info_hours_label(tr::now) + \": \" + status + \", \" + when);\n\t}, inner->lifetime());\n\n\tconst auto labelWrap = inner->add(object_ptr<Ui::RpWidget>(inner));\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tlabelWrap,\n\t\ttr::lng_info_hours_label(),\n\t\tst::infoLabel);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tauto linkText = rpl::combine(\n\t\tstate->nonTrivial.value(),\n\t\tstate->hours.value(),\n\t\tstate->mine.value(),\n\t\tstate->myTimezone.value()\n\t) | rpl::map([=](\n\t\t\tbool complex,\n\t\t\tconst WorkingHours &hours,\n\t\t\tconst WorkingIntervals &mine,\n\t\t\tbool my) {\n\t\treturn (!complex || hours.intervals == mine)\n\t\t\t? rpl::single(QString())\n\t\t\t: my\n\t\t\t? tr::lng_info_hours_my_time()\n\t\t\t: tr::lng_info_hours_local_time();\n\t}) | rpl::flatten_latest();\n\tconst auto link = Ui::CreateChild<Ui::RoundButton>(\n\t\tlabelWrap,\n\t\tstd::move(linkText),\n\t\tst::defaultTableSmallButton);\n\tlink->setClickedCallback([=] {\n\t\tstate->myTimezone = !state->myTimezone.current();\n\t\tstate->expanded = true;\n\t});\n\n\trpl::combine(\n\t\tlabelWrap->widthValue(),\n\t\tlabel->heightValue(),\n\t\tlink->sizeValue()\n\t) | rpl::on_next([=](int width, int h1, QSize size) {\n\t\tlabel->moveToLeft(0, 0, width);\n\t\tlink->moveToRight(0, 0, width);\n\n\t\tconst auto margins = label->getMargins();\n\t\tconst auto added = margins.top() + margins.bottom();\n\t\tlabelWrap->resize(width, std::max(h1, size.height()) - added);\n\t}, labelWrap->lifetime());\n\n\tconst auto other = inner->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tinner,\n\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\tother->toggleOn(state->expanded.value(), anim::type::normal);\n\tconstexpr auto kSlideDuration = float64(st::slideWrapDuration);\n\tother->setDuration(kSlideDuration);\n\t{\n\t\tconst auto arrowAnimation\n\t\t\t= other->lifetime().make_state<Ui::Animations::Basic>();\n\t\tarrowAnimation->init([=] {\n\t\t\ttimingArrow->update();\n\t\t\tif (!other->animating()) {\n\t\t\t\tarrowAnimation->stop();\n\t\t\t}\n\t\t});\n\t\ttimingArrow->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(timingArrow);\n\t\t\tconst auto progress = other->animating()\n\t\t\t\t? (crl::now() - arrowAnimation->started()) / kSlideDuration\n\t\t\t\t: 1.;\n\n\t\t\tconst auto path = Ui::ToggleUpDownArrowPath(\n\t\t\t\ttimingArrow->width() / 2,\n\t\t\t\ttimingArrow->height() / 2,\n\t\t\t\tst::infoHoursArrowSize,\n\t\t\t\tst::mainMenuToggleFourStrokes,\n\t\t\t\tother->toggled() ? progress : 1 - progress);\n\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.fillPath(path, timing->st().textFg);\n\t\t}, timingArrow->lifetime());\n\t\tstate->expanded.value() | rpl::on_next([=] {\n\t\t\tarrowAnimation->start();\n\t\t}, other->lifetime());\n\t}\n\n\tother->finishAnimating();\n\tconst auto days = other->entity();\n\n\tfor (auto i = 1; i != 7; ++i) {\n\t\tconst auto dayWrap = days->add(\n\t\t\tobject_ptr<Ui::RpWidget>(other),\n\t\t\tQMargins(0, st::infoHoursDaySkip, 0, 0));\n\t\tauto label = state->day.value() | rpl::map([=](int day) {\n\t\t\tswitch ((day + i) % 7) {\n\t\t\tcase 0: return tr::lng_hours_monday();\n\t\t\tcase 1: return tr::lng_hours_tuesday();\n\t\t\tcase 2: return tr::lng_hours_wednesday();\n\t\t\tcase 3: return tr::lng_hours_thursday();\n\t\t\tcase 4: return tr::lng_hours_friday();\n\t\t\tcase 5: return tr::lng_hours_saturday();\n\t\t\tcase 6: return tr::lng_hours_sunday();\n\t\t\t}\n\t\t\tUnexpected(\"Index in working hours.\");\n\t\t}) | rpl::flatten_latest();\n\t\tconst auto dayLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tdayWrap,\n\t\t\tstd::move(label),\n\t\t\tst::infoHoursDayLabel);\n\t\tdayLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tconst auto dayHours = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tdayWrap,\n\t\t\tdayHoursTextValue(state->day.value()\n\t\t\t\t| rpl::map((rpl::mappers::_1 + i) % 7)),\n\t\t\tst::infoHoursValue);\n\t\tdayHours->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\trpl::combine(\n\t\t\tdayWrap->widthValue(),\n\t\t\tdayLabel->heightValue(),\n\t\t\tdayHours->sizeValue()\n\t\t) | rpl::on_next([=](int width, int h1, QSize size) {\n\t\t\tdayLabel->moveToLeft(0, 0, width);\n\t\t\tdayHours->moveToRight(0, 0, width);\n\n\t\t\tconst auto margins = dayLabel->getMargins();\n\t\t\tconst auto added = margins.top() + margins.bottom();\n\t\t\tdayWrap->resize(width, std::max(h1, size.height()) - added);\n\t\t}, dayWrap->lifetime());\n\t}\n\n\tbutton->setClickedCallback([=] {\n\t\tstate->expanded = !state->expanded.current();\n\t});\n\n\tresult->toggleOn(state->hours.value(\n\t) | rpl::map([](const WorkingHours &data) {\n\t\treturn bool(data);\n\t}));\n\n\treturn result;\n}\n\nvoid DeleteContactNote(\n\t\tnot_null<UserData*> user,\n\t\tFn<void(const QString &)> showError = nullptr) {\n\tuser->session().api().request(MTPcontacts_UpdateContactNote(\n\t\tuser->inputUser(),\n\t\tMTP_textWithEntities(MTP_string(), MTP_vector<MTPMessageEntity>())\n\t)).done([=] {\n\t\tuser->setNote(TextWithEntities());\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (showError) {\n\t\t\tshowError(error.description());\n\t\t}\n\t}).send();\n}\n\n[[nodiscard]] object_ptr<Ui::SlideWrap<>> CreateNotes(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> user) {\n\tauto allNotesText = user->session().changes().peerFlagsValue(\n\t\tuser,\n\t\tData::PeerUpdate::Flag::FullInfo\n\t) | rpl::map([=] {\n\t\treturn user->note();\n\t});\n\n\tauto notesText = rpl::duplicate(\n\t\tallNotesText\n\t) | rpl::filter([](const TextWithEntities &note) {\n\t\treturn !note.text.isEmpty();\n\t});\n\n\tauto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\tparent,\n\t\tobject_ptr<Ui::VerticalLayout>(parent));\n\tresult->toggleOn(rpl::duplicate(\n\t\tallNotesText\n\t) | rpl::map([](const TextWithEntities &note) {\n\t\treturn !note.text.isEmpty();\n\t}));\n\tresult->finishAnimating();\n\n\tconst auto notesContainer = result->entity();\n\n\tconst auto context = Ui::Text::MarkedContext{\n\t\t.customEmojiFactory = user->owner().customEmojiManager().factory(\n\t\t\tData::CustomEmojiManager::SizeTag::Normal)\n\t};\n\n\tauto notesLine = CreateTextWithLabel(\n\t\tnotesContainer,\n\t\ttr::lng_info_notes_label(TextWithEntities::Simple),\n\t\trpl::duplicate(notesText),\n\t\tst::infoLabel,\n\t\tst::infoLabeled,\n\t\tst::infoProfileLabeledPadding);\n\n\tstd::move(\n\t\tnotesText\n\t) | rpl::on_next([=, raw = notesLine.text](\n\t\t\tTextWithEntities note) {\n\t\tTextUtilities::ParseEntities(note, TextParseLinks);\n\t\traw->setMarkedText(note, context);\n\t}, notesLine.text->lifetime());\n\n\tnotesLine.text->setContextMenuHook([=, raw = notesLine.text](\n\t\t\tUi::FlatLabel::ContextMenuRequest request) {\n\t\traw->fillContextMenu(request);\n\t\tconst auto addAction = Ui::Menu::CreateAddActionCallback(\n\t\t\trequest.menu);\n\t\taddAction({\n\t\t\t.text = tr::lng_edit_note(tr::now),\n\t\t\t.handler = [=] {\n\t\t\t\tcontroller->window().show(\n\t\t\t\t\tBox(EditContactNoteBox, controller, user));\n\t\t\t},\n\t\t});\n\t\taddAction({\n\t\t\t.text = tr::lng_delete_note(tr::now),\n\t\t\t.handler = [=] {\n\t\t\t\tDeleteContactNote(user, [=](const QString &error) {\n\t\t\t\t\tcontroller->showToast(error);\n\t\t\t\t});\n\t\t\t},\n\t\t\t.isAttention = true,\n\t\t});\n\t});\n\n\trpl::merge(\n\t\tnotesLine.wrap->events(),\n\t\tnotesLine.subtext->events()\n\t) | rpl::on_next([=, raw = notesLine.text](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::ContextMenu) {\n\t\t\tconst auto ce = static_cast<QContextMenuEvent*>(e.get());\n\t\t\tQCoreApplication::postEvent(\n\t\t\t\traw,\n\t\t\t\tnew QContextMenuEvent(\n\t\t\t\t\tce->reason(),\n\t\t\t\t\tce->pos(),\n\t\t\t\t\tce->globalPos()));\n\t\t}\n\t}, notesLine.wrap->lifetime());\n\n\tconst auto subtextLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\tnotesLine.wrap->entity(),\n\t\ttr::lng_info_notes_private(tr::now),\n\t\tst::infoLabel);\n\tsubtextLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\trpl::combine(\n\t\tnotesLine.wrap->entity()->widthValue(),\n\t\tnotesLine.subtext->geometryValue()\n\t) | rpl::on_next([=, skip = st::lineWidth * 5](\n\t\t\tint width,\n\t\t\tconst QRect &subtextGeometry) {\n\t\tsubtextLabel->moveToRight(\n\t\t\t0,\n\t\t\tsubtextGeometry.y() + skip,\n\t\t\twidth);\n\t}, subtextLabel->lifetime());\n\n\tnotesContainer->add(std::move(notesLine.wrap));\n\n\treturn result;\n}\n\n[[nodiscard]] object_ptr<Ui::SlideWrap<>> CreateBirthday(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> user) {\n\tusing namespace Data;\n\n\tauto result = object_ptr<Ui::SlideWrap<Ui::RoundButton>>(\n\t\tparent,\n\t\tobject_ptr<Ui::RoundButton>(\n\t\t\tparent,\n\t\t\trpl::single(QString()),\n\t\t\tst::infoHoursOuter),\n\t\tst::infoProfileLabeledPadding - st::infoHoursOuterMargin);\n\tresult->setDuration(st::infoSlideDuration);\n\tconst auto button = result->entity();\n\tbutton->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\n\tauto outer = Ui::CreateChild<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\tbutton,\n\t\tobject_ptr<Ui::VerticalLayout>(button),\n\t\tst::infoHoursOuterMargin);\n\tconst auto layout = outer->entity();\n\tlayout->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tauto birthday = BirthdayValue(\n\t\tuser\n\t) | rpl::start_spawning(result->lifetime());\n\n\tauto label = BirthdayLabelText(rpl::duplicate(birthday));\n\tauto text = BirthdayValueText(\n\t\trpl::duplicate(birthday)\n\t) | rpl::map(tr::marked);\n\n\tconst auto giftIcon = Ui::CreateChild<Ui::RpWidget>(layout);\n\tgiftIcon->resize(st::birthdayTodayIcon.size());\n\tlayout->sizeValue() | rpl::on_next([=](QSize size) {\n\t\tgiftIcon->moveToRight(\n\t\t\t0,\n\t\t\t(size.height() - giftIcon->height()) / 2,\n\t\t\tsize.width());\n\t}, giftIcon->lifetime());\n\tgiftIcon->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(giftIcon);\n\t\tst::birthdayTodayIcon.paint(p, 0, 0, giftIcon->width());\n\t}, giftIcon->lifetime());\n\n\trpl::duplicate(\n\t\tbirthday\n\t) | rpl::map([](Data::Birthday value) {\n\t\treturn Data::IsBirthdayTodayValue(value);\n\t}) | rpl::flatten_latest(\n\t) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](bool today) {\n\t\tconst auto disable = !today && user->session().premiumCanBuy();\n\t\tbutton->setDisabled(disable);\n\t\tbutton->setAttribute(Qt::WA_TransparentForMouseEvents, disable);\n\t\tbutton->clearState();\n\t\tgiftIcon->setVisible(!disable);\n\t}, result->lifetime());\n\n\tBirthdayValueText(\n\t\trpl::duplicate(birthday),\n\t\ttrue\n\t) | rpl::on_next([=](const QString &accessibleText) {\n\t\tbutton->setAccessibleName(\n\t\t\ttr::lng_info_birthday_label(tr::now) + \": \" + accessibleText);\n\t}, button->lifetime());\n\n\tauto nonEmptyText = std::move(\n\t\ttext\n\t) | rpl::before_next([slide = result.data()](\n\t\t\tconst TextWithEntities &value) {\n\t\tif (value.text.isEmpty()) {\n\t\t\tslide->hide(anim::type::normal);\n\t\t}\n\t}) | rpl::filter([](const TextWithEntities &value) {\n\t\treturn !value.text.isEmpty();\n\t}) | rpl::after_next([slide = result.data()](\n\t\t\tconst TextWithEntities &value) {\n\t\tslide->show(anim::type::normal);\n\t});\n\tlayout->add(object_ptr<Ui::FlatLabel>(\n\t\tlayout,\n\t\tstd::move(nonEmptyText),\n\t\tst::birthdayLabeled));\n\tlayout->add(Ui::CreateSkipWidget(layout, st::infoLabelSkip));\n\tlayout->add(object_ptr<Ui::FlatLabel>(\n\t\tlayout,\n\t\tstd::move(\n\t\t\tlabel\n\t\t) | rpl::after_next([=] {\n\t\t\tlayout->resizeToWidth(layout->widthNoMargins());\n\t\t}),\n\t\tst::birthdayLabel));\n\tresult->finishAnimating();\n\n\tUi::ResizeFitChild(button, outer);\n\n\tbutton->setClickedCallback([=] {\n\t\tif (!button->isDisabled()) {\n\t\t\tUi::ShowStarGiftBox(controller, user);\n\t\t}\n\t});\n\n\treturn result;\n}\n\ntemplate <typename Text, typename ToggleOn, typename Callback>\nauto AddActionButton(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tText &&text,\n\t\tToggleOn &&toggleOn,\n\t\tCallback &&callback,\n\t\tconst style::icon *icon,\n\t\tconst style::SettingsButton &st = st::infoSharedMediaButton) {\n\tauto result = parent->add(object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\tparent,\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\tparent,\n\t\t\tstd::move(text),\n\t\t\tst))\n\t);\n\tresult->setDuration(\n\t\tst::infoSlideDuration\n\t)->toggleOn(\n\t\tstd::move(toggleOn)\n\t)->entity()->addClickHandler(std::move(callback));\n\tresult->finishAnimating();\n\tif (icon) {\n\t\tobject_ptr<Profile::FloatingIcon>(\n\t\t\tresult,\n\t\t\t*icon,\n\t\t\tst::infoSharedMediaButtonIconPosition);\n\t}\n\treturn result;\n};\n\ntemplate <typename Text, typename ToggleOn, typename Callback>\n[[nodiscard]] auto AddMainButton(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tText &&text,\n\t\tToggleOn &&toggleOn,\n\t\tCallback &&callback,\n\t\tUi::MultiSlideTracker &tracker,\n\t\tUi::MultiSlideTracker *buttonTracker,\n\t\tconst style::SettingsButton &st = st::infoMainButton) {\n\tconst auto button = AddActionButton(\n\t\tparent,\n\t\tstd::move(text) | rpl::map(tr::upper),\n\t\tstd::move(toggleOn),\n\t\tstd::move(callback),\n\t\tnullptr,\n\t\tst);\n\ttracker.track(button);\n\tif (buttonTracker) {\n\t\tbuttonTracker->track(button);\n\t}\n}\n\nrpl::producer<CreditsAmount> AddCurrencyAction(\n\t\tnot_null<UserData*> user,\n\t\tnot_null<Ui::VerticalLayout*> wrap,\n\t\tnot_null<Controller*> controller) {\n\tstruct State final {\n\t\trpl::variable<CreditsAmount> balance;\n\t\tUi::Text::CustomEmojiHelper helper;\n\t};\n\tconst auto state = wrap->lifetime().make_state<State>();\n\tconst auto parentController = controller->parentController();\n\tconst auto wrapButton = AddActionButton(\n\t\twrap,\n\t\ttr::lng_manage_peer_bot_balance_currency(),\n\t\tstate->balance.value(\n\t\t) | rpl::map(rpl::mappers::_1 > CreditsAmount(0)),\n\t\t[=] { parentController->showSection(Info::ChannelEarn::Make(user)); },\n\t\tnullptr);\n\t{\n\t\tconst auto button = wrapButton->entity();\n\t\tconst auto icon = Ui::CreateChild<Ui::RpWidget>(button);\n\t\ticon->resize(st::infoIconReport.size());\n\t\tconst auto image = Ui::Earn::MenuIconCurrency(icon->size());\n\t\ticon->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(icon);\n\t\t\tp.drawImage(0, 0, image);\n\t\t}, icon->lifetime());\n\n\t\tbutton->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &size) {\n\t\t\ticon->move(st::infoEarnCurrencyIconPosition);\n\t\t}, icon->lifetime());\n\t}\n\tconst auto balance = user->session().credits().balanceCurrency(user->id);\n\tif (balance) {\n\t\tstate->balance = balance;\n\t}\n\t{\n\t\tconst auto weak = base::make_weak(wrap);\n\t\tconst auto currencyLoadLifetime\n\t\t\t= std::make_shared<rpl::lifetime>();\n\t\tconst auto currencyLoad\n\t\t\t= currencyLoadLifetime->make_state<Api::EarnStatistics>(user);\n\t\tconst auto done = [=](CreditsAmount balance) {\n\t\t\tif ([[maybe_unused]] const auto strong = weak.get()) {\n\t\t\t\tstate->balance = balance;\n\t\t\t\tcurrencyLoadLifetime->destroy();\n\t\t\t}\n\t\t};\n\t\tcurrencyLoad->request() | rpl::on_error_done(\n\t\t\t[=](const QString &error) {\n\t\t\t\tdone(CreditsAmount(0, CreditsType::Ton));\n\t\t\t},\n\t\t\t[=] { done(currencyLoad->data().currentBalance); },\n\t\t\t*currencyLoadLifetime);\n\t}\n\tconst auto &st = st::infoSharedMediaButton;\n\tconst auto button = wrapButton->entity();\n\tconst auto name = Ui::CreateChild<Ui::FlatLabel>(button, st.rightLabel);\n\tconst auto icon = state->helper.paletteDependent({ .factory = [=] {\n\t\treturn Ui::Earn::IconCurrencyColored(\n\t\t\tst.rightLabel.style.font,\n\t\t\tst.rightLabel.textFg->c);\n\t}, .margin = st::channelEarnCurrencyCommonMargins });\n\tname->show();\n\trpl::combine(\n\t\tbutton->widthValue(),\n\t\ttr::lng_manage_peer_bot_balance_currency(),\n\t\tstate->balance.value()\n\t) | rpl::on_next([=, &st](\n\t\t\tint width,\n\t\t\tconst QString &button,\n\t\t\tCreditsAmount balance) {\n\t\tconst auto available = width\n\t\t\t- rect::m::sum::h(st.padding)\n\t\t\t- st.style.font->width(button)\n\t\t\t- st::settingsButtonRightSkip;\n\t\tname->setMarkedText(\n\t\t\tbase::duplicate(icon)\n\t\t\t\t.append(QChar(' '))\n\t\t\t\t.append(Info::ChannelEarn::MajorPart(balance))\n\t\t\t\t.append(Info::ChannelEarn::MinorPart(balance)),\n\t\t\tstate->helper.context());\n\t\tname->resizeToNaturalWidth(available);\n\t\tname->moveToRight(st::settingsButtonRightSkip, st.padding.top());\n\t}, name->lifetime());\n\tname->setAttribute(Qt::WA_TransparentForMouseEvents);\n\twrapButton->finishAnimating();\n\treturn state->balance.value();\n}\n\nrpl::producer<CreditsAmount> AddCreditsAction(\n\t\tnot_null<UserData*> user,\n\t\tnot_null<Ui::VerticalLayout*> wrap,\n\t\tnot_null<Controller*> controller) {\n\tstruct State final {\n\t\trpl::variable<CreditsAmount> balance;\n\t};\n\tconst auto state = wrap->lifetime().make_state<State>();\n\tconst auto parentController = controller->parentController();\n\tconst auto wrapButton = AddActionButton(\n\t\twrap,\n\t\ttr::lng_manage_peer_bot_balance_credits(),\n\t\tstate->balance.value(\n\t\t) | rpl::map(rpl::mappers::_1 > CreditsAmount(0)),\n\t\t[=] { parentController->showSection(Info::BotEarn::Make(user)); },\n\t\tnullptr);\n\t{\n\t\tconst auto button = wrapButton->entity();\n\t\tconst auto icon = Ui::CreateChild<Ui::RpWidget>(button);\n\t\tconst auto image = Ui::Earn::MenuIconCredits();\n\t\ticon->resize(image.size() / style::DevicePixelRatio());\n\t\ticon->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(icon);\n\t\t\tp.drawImage(0, 0, image);\n\t\t}, icon->lifetime());\n\n\t\tbutton->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &size) {\n\t\t\ticon->move(st::infoEarnCreditsIconPosition);\n\t\t}, icon->lifetime());\n\t}\n\tif (const auto balance = user->session().credits().balance(user->id)) {\n\t\tstate->balance = balance;\n\t}\n\t{\n\t\tconst auto api = wrap->lifetime().make_state<Api::CreditsStatus>(\n\t\t\tuser);\n\t\tapi->request({}, [=](Data::CreditsStatusSlice data) {\n\t\t\tstate->balance = data.balance;\n\t\t});\n\t}\n\tconst auto &st = st::infoSharedMediaButton;\n\tconst auto button = wrapButton->entity();\n\tconst auto name = Ui::CreateChild<Ui::FlatLabel>(button, st.rightLabel);\n\n\tauto helper = Ui::Text::CustomEmojiHelper();\n\tconst auto icon = helper.paletteDependent(Ui::Earn::IconCreditsEmoji());\n\tconst auto context = helper.context([=] { name->update(); });\n\tname->show();\n\trpl::combine(\n\t\tbutton->widthValue(),\n\t\ttr::lng_manage_peer_bot_balance_credits(),\n\t\tstate->balance.value()\n\t) | rpl::on_next([=, &st](\n\t\t\tint width,\n\t\t\tconst QString &button,\n\t\t\tCreditsAmount balance) {\n\t\tconst auto available = width\n\t\t\t- rect::m::sum::h(st.padding)\n\t\t\t- st.style.font->width(button)\n\t\t\t- st::settingsButtonRightSkip;\n\t\tname->setMarkedText(\n\t\t\tbase::duplicate(icon)\n\t\t\t\t.append(QChar(' '))\n\t\t\t\t.append(Lang::FormatCreditsAmountDecimal(balance)),\n\t\t\tcontext);\n\t\tname->resizeToNaturalWidth(available);\n\t\tname->moveToRight(st::settingsButtonRightSkip, st.padding.top());\n\t}, name->lifetime());\n\tname->setAttribute(Qt::WA_TransparentForMouseEvents);\n\twrapButton->finishAnimating();\n\treturn state->balance.value();\n}\n\nclass DetailsFiller {\npublic:\n\tDetailsFiller(\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<PeerData*> peer,\n\t\tOrigin origin,\n\t\tUi::MultiSlideTracker &mainTracker,\n\t\trpl::variable<bool> &dividerOverridden);\n\tDetailsFiller(\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Data::SavedSublist*> sublist,\n\t\tUi::MultiSlideTracker &mainTracker,\n\t\trpl::variable<bool> &dividerOverridden);\n\tDetailsFiller(\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Data::ForumTopic*> topic,\n\t\tUi::MultiSlideTracker &mainTracker,\n\t\trpl::variable<bool> &dividerOverridden);\n\n\tobject_ptr<Ui::RpWidget> fill();\n\nprivate:\n\tobject_ptr<Ui::RpWidget> setupPersonalChannel(not_null<UserData*> user);\n\tobject_ptr<Ui::RpWidget> setupInfo();\n\tvoid setupMainApp(bool suppressBottom = false);\n\tvoid setupBotPermissions();\n\tvoid addShowTopicsListButton(\n\t\tUi::MultiSlideTracker &tracker,\n\t\tnot_null<Data::Forum*> forum,\n\t\tUi::MultiSlideTracker *buttonTracker);\n\tvoid addViewChannelButton(\n\t\tUi::MultiSlideTracker &tracker,\n\t\tnot_null<ChannelData*> channel,\n\t\tUi::MultiSlideTracker *buttonTracker);\n\n\tvoid addReportReaction(\n\t\tUi::MultiSlideTracker &tracker,\n\t\tUi::MultiSlideTracker *buttonTracker);\n\tvoid addReportReaction(\n\t\tGroupReactionOrigin data,\n\t\tbool ban,\n\t\tUi::MultiSlideTracker &tracker,\n\t\tUi::MultiSlideTracker *buttonTracker);\n\n\ttemplate <\n\t\ttypename Widget,\n\t\ttypename = std::enable_if_t<\n\t\tstd::is_base_of_v<Ui::RpWidget, Widget>>>\n\tWidget *add(\n\t\t\tobject_ptr<Widget> &&child,\n\t\t\tconst style::margins &margin = style::margins()) {\n\t\treturn _wrap->add(\n\t\t\tstd::move(child),\n\t\t\tmargin);\n\t}\n\n\tnot_null<Controller*> _controller;\n\tnot_null<Ui::RpWidget*> _parent;\n\tnot_null<PeerData*> _peer;\n\tData::ForumTopic *_topic = nullptr;\n\tData::SavedSublist *_sublist = nullptr;\n\tOrigin _origin;\n\tUi::MultiSlideTracker &_mainTracker;\n\trpl::variable<bool> &_dividerOverridden;\n\tobject_ptr<Ui::VerticalLayout> _wrap;\n\n};\n\nclass ActionsFiller {\npublic:\n\tActionsFiller(\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<PeerData*> peer);\n\n\tobject_ptr<Ui::RpWidget> fill();\n\nprivate:\n\tvoid addAffiliateProgram(not_null<UserData*> user);\n\tvoid addBalanceActions(not_null<UserData*> user);\n\tvoid addInviteToGroupAction(not_null<UserData*> user);\n\tvoid addShareContactAction(not_null<UserData*> user);\n\tvoid addEditContactAction(not_null<UserData*> user);\n\tvoid addDeleteContactAction(not_null<UserData*> user);\n\tvoid addBotCommandActions(not_null<UserData*> user);\n\tvoid addFastButtonsMode(not_null<UserData*> user);\n\tvoid addReportAction();\n\tvoid addBlockAction(not_null<UserData*> user);\n\tvoid addLeaveChannelAction(not_null<ChannelData*> channel);\n\tvoid addJoinChannelAction(not_null<ChannelData*> channel);\n\tvoid fillUserActions(not_null<UserData*> user);\n\tvoid fillChannelActions(not_null<ChannelData*> channel);\n\n\tnot_null<Controller*> _controller;\n\tnot_null<Ui::RpWidget*> _parent;\n\tnot_null<PeerData*> _peer;\n\tobject_ptr<Ui::VerticalLayout> _wrap = { nullptr };\n\n};\n\nvoid ReportReactionBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> participant,\n\t\tGroupReactionOrigin data,\n\t\tbool ban,\n\t\tFn<void()> sent) {\n\tbox->setTitle(tr::lng_report_reaction_title());\n\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\tbox,\n\t\ttr::lng_report_reaction_about(),\n\t\tst::boxLabel));\n\tconst auto check = ban\n\t\t? box->addRow(\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_report_and_ban_button(tr::now),\n\t\t\t\ttrue),\n\t\t\tst::boxRowPadding + QMargins{ 0, st::boxLittleSkip, 0, 0 })\n\t\t: nullptr;\n\tbox->addButton(tr::lng_report_button(), [=] {\n\t\tconst auto chat = data.group->asChat();\n\t\tconst auto channel = data.group->asMegagroup();\n\t\tif (check && check->checked()) {\n\t\t\tif (chat) {\n\t\t\t\tchat->session().api().chatParticipants().kick(\n\t\t\t\t\tchat,\n\t\t\t\t\tparticipant);\n\t\t\t} else if (channel) {\n\t\t\t\tchannel->session().api().chatParticipants().kick(\n\t\t\t\t\tchannel,\n\t\t\t\t\tparticipant,\n\t\t\t\t\tChatRestrictionsInfo());\n\t\t\t}\n\t\t}\n\t\tdata.group->session().api().request(MTPmessages_ReportReaction(\n\t\t\tdata.group->input(),\n\t\t\tMTP_int(data.messageId.bare),\n\t\t\tparticipant->input()\n\t\t)).done(crl::guard(controller, [=] {\n\t\t\tcontroller->showToast(tr::lng_report_thanks(tr::now));\n\t\t})).send();\n\t\tsent();\n\t\tbox->closeBox();\n\t}, st::attentionBoxButton);\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nDetailsFiller::DetailsFiller(\n\tnot_null<Controller*> controller,\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<PeerData*> peer,\n\tOrigin origin,\n\tUi::MultiSlideTracker &mainTracker,\n\trpl::variable<bool> &dividerOverridden)\n: _controller(controller)\n, _parent(parent)\n, _peer(peer)\n, _origin(origin)\n, _mainTracker(mainTracker)\n, _dividerOverridden(dividerOverridden)\n, _wrap(_parent) {\n}\n\nDetailsFiller::DetailsFiller(\n\tnot_null<Controller*> controller,\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Data::SavedSublist*> sublist,\n\tUi::MultiSlideTracker &mainTracker,\n\trpl::variable<bool> &dividerOverridden)\n: _controller(controller)\n, _parent(parent)\n, _peer(sublist->sublistPeer())\n, _sublist(sublist)\n, _mainTracker(mainTracker)\n, _dividerOverridden(dividerOverridden)\n, _wrap(_parent) {\n}\n\nDetailsFiller::DetailsFiller(\n\tnot_null<Controller*> controller,\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Data::ForumTopic*> topic,\n\tUi::MultiSlideTracker &mainTracker,\n\trpl::variable<bool> &dividerOverridden)\n: _controller(controller)\n, _parent(parent)\n, _peer(topic->peer())\n, _topic(topic)\n, _mainTracker(mainTracker)\n, _dividerOverridden(dividerOverridden)\n, _wrap(_parent) {\n}\n\ntemplate <typename T>\nbool SetClickContext(\n\t\tconst ClickHandlerPtr &handler,\n\t\tconst ClickContext &context) {\n\tif (const auto casted = std::dynamic_pointer_cast<T>(handler)) {\n\t\tcasted->T::onClick(context);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nobject_ptr<Ui::RpWidget> DetailsFiller::setupInfo() {\n\tauto wrap = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t_wrap,\n\t\tobject_ptr<Ui::VerticalLayout>(_wrap));\n\t_mainTracker.track(wrap.data());\n\tconst auto result = wrap->entity();\n\tauto tracker = Ui::MultiSlideTracker();\n\tadd(CreateSlideSkipWidget(wrap))->toggleOn(\n\t\ttracker.atLeastOneShownValueLater());\n\n\t// Fill context for a mention / hashtag / bot command link.\n\tconst auto infoClickFilter = [=,\n\t\tpeer = _peer.get(),\n\t\twindow = _controller->parentController()](\n\t\t\tconst ClickHandlerPtr &handler,\n\t\t\tQt::MouseButton button) {\n\t\tconst auto context = ClickContext{\n\t\t\tbutton,\n\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.sessionWindow = base::make_weak(window),\n\t\t\t\t.peer = peer,\n\t\t\t})\n\t\t};\n\t\tif (SetClickContext<BotCommandClickHandler>(handler, context)) {\n\t\t\treturn false;\n\t\t} else if (SetClickContext<MentionClickHandler>(handler, context)) {\n\t\t\treturn false;\n\t\t} else if (SetClickContext<HashtagClickHandler>(handler, context)) {\n\t\t\treturn false;\n\t\t} else if (SetClickContext<CashtagClickHandler>(handler, context)) {\n\t\t\treturn false;\n\t\t} else if (handler->url().startsWith(u\"internal:~join_date~:\"_q)) {\n\t\t\tconst auto joinDate = handler->url().split(\n\t\t\t\tu\"show:\"_q,\n\t\t\t\tQt::SkipEmptyParts).last();\n\t\t\tif (!joinDate.isEmpty()) {\n\t\t\t\tconst auto weak = base::make_weak(window);\n\t\t\t\twindow->session().api().resolveJumpToDate(\n\t\t\t\t\tDialogs::Key(peer->owner().history(peer)),\n\t\t\t\t\tbase::unixtime::parse(joinDate.toULongLong()).date(),\n\t\t\t\t\t[=](not_null<PeerData*> p, MsgId m) {\n\t\t\t\t\t\tconst auto f = Window::SectionShow::Way::Forward;\n\t\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\t\tstrong->showPeerHistory(p, f, m);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else if (SetClickContext<UrlClickHandler>(handler, context)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t};\n\n\tconst auto addTranslateToMenu = [&,\n\t\t\tpeer = _peer.get(),\n\t\t\tcontroller = _controller->parentController()](\n\t\t\tnot_null<Ui::FlatLabel*> label,\n\t\t\trpl::producer<TextWithEntities> &&text) {\n\t\tstruct State {\n\t\t\trpl::variable<TextWithEntities> labelText;\n\t\t};\n\t\tconst auto state = label->lifetime().make_state<State>();\n\t\tstate->labelText = std::move(text);\n\t\tlabel->setContextMenuHook([=](\n\t\t\t\tUi::FlatLabel::ContextMenuRequest request) {\n\t\t\tif (request.link) {\n\t\t\t\tconst auto &url = request.link->url();\n\t\t\t\tif (url.startsWith(u\"internal:~peer_id~:\"_q)) {\n\t\t\t\t\tconst auto weak = base::make_weak(controller);\n\t\t\t\t\trequest.menu->addAction(u\"Copy ID\"_q, [=] {\n\t\t\t\t\t\tCore::App().openInternalUrl(\n\t\t\t\t\t\t\turl,\n\t\t\t\t\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t\t\t\t\t.sessionWindow = weak,\n\t\t\t\t\t\t\t}));\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tlabel->fillContextMenu(request);\n\t\t\tif (Ui::SkipTranslate(state->labelText.current())) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto item = (request.selection.empty()\n\t\t\t\t? tr::lng_context_translate\n\t\t\t\t: tr::lng_context_translate_selected)(tr::now);\n\t\t\trequest.menu->addAction(std::move(item), [=] {\n\t\t\t\tcontroller->window().show(Box(\n\t\t\t\t\tUi::TranslateBox,\n\t\t\t\t\tpeer,\n\t\t\t\t\tMsgId(),\n\t\t\t\t\trequest.selection.empty()\n\t\t\t\t\t\t? state->labelText.current()\n\t\t\t\t\t\t: Ui::Text::Mid(\n\t\t\t\t\t\t\tstate->labelText.current(),\n\t\t\t\t\t\t\trequest.selection.from,\n\t\t\t\t\t\t\trequest.selection.to - request.selection.from),\n\t\t\t\t\tfalse));\n\t\t\t});\n\t\t});\n\t};\n\n\tconst auto addInfoLineGeneric = [&](\n\t\t\tv::text::data &&label,\n\t\t\trpl::producer<TextWithEntities> &&text,\n\t\t\tconst style::FlatLabel &textSt = st::infoLabeled,\n\t\t\tconst style::margins &padding = st::infoProfileLabeledPadding) {\n\t\tauto line = CreateTextWithLabel(\n\t\t\tresult,\n\t\t\tv::text::take_marked(std::move(label)),\n\t\t\tstd::move(text),\n\t\t\tst::infoLabel,\n\t\t\ttextSt,\n\t\t\tpadding);\n\t\ttracker.track(result->add(std::move(line.wrap)));\n\n\t\tline.text->setClickHandlerFilter(infoClickFilter);\n\t\treturn line;\n\t};\n\tconst auto addInfoLine = [&](\n\t\t\tv::text::data &&label,\n\t\t\trpl::producer<TextWithEntities> &&text,\n\t\t\tconst style::FlatLabel &textSt = st::infoLabeled,\n\t\t\tconst style::margins &padding = st::infoProfileLabeledPadding) {\n\t\treturn addInfoLineGeneric(\n\t\t\tstd::move(label),\n\t\t\tstd::move(text),\n\t\t\ttextSt,\n\t\t\tpadding);\n\t};\n\tconst auto addInfoOneLine = [&](\n\t\t\tv::text::data &&label,\n\t\t\trpl::producer<TextWithEntities> &&text,\n\t\t\tconst QString &contextCopyText,\n\t\t\tconst style::margins &padding = st::infoProfileLabeledPadding) {\n\t\tauto result = addInfoLine(\n\t\t\tstd::move(label),\n\t\t\tstd::move(text),\n\t\t\tst::infoLabeledOneLine,\n\t\t\tpadding);\n\t\tresult.text->setDoubleClickSelectsParagraph(true);\n\t\tresult.text->setContextCopyText(contextCopyText);\n\t\treturn result;\n\t};\n\tconst auto fitLabelToButton = [&](\n\t\t\tnot_null<Ui::RpWidget*> button,\n\t\t\tnot_null<Ui::FlatLabel*> label,\n\t\t\tint rightSkip) {\n\t\tconst auto parent = label->parentWidget();\n\t\tconst auto container = result;\n\t\trpl::combine(\n\t\t\tcontainer->widthValue(),\n\t\t\tlabel->geometryValue(),\n\t\t\tbutton->sizeValue(),\n\t\t\tbutton->shownValue()\n\t\t) | rpl::on_next([=](\n\t\t\t\tint width,\n\t\t\t\tQRect,\n\t\t\t\tQSize buttonSize,\n\t\t\t\tbool buttonShown) {\n\t\t\tbutton->moveToRight(\n\t\t\t\trightSkip,\n\t\t\t\t(parent->height() - buttonSize.height()) / 2);\n\t\t\tconst auto x = Ui::MapFrom(container, label, QPoint(0, 0)).x();\n\t\t\tconst auto s = buttonShown\n\t\t\t\t? Ui::MapFrom(container, button, QPoint(0, 0)).x()\n\t\t\t\t: width;\n\t\t\tlabel->resizeToWidth(s - x);\n\t\t}, button->lifetime());\n\t};\n\tconst auto controller = _controller->parentController();\n\tconst auto weak = base::make_weak(controller);\n\tconst auto peerIdRaw = QString::number(_peer->id.value);\n\tconst auto lnkHook = [=](Ui::FlatLabel::ContextMenuRequest request) {\n\t\tconst auto strong = weak.get();\n\t\tif (!strong || !request.link) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto url = request.link->url();\n\t\tif (url.startsWith(u\"https://\")) {\n\t\t\trequest.menu->addAction(\n\t\t\t\ttr::lng_context_copy_link(tr::now),\n\t\t\t\t[=] {\n\t\t\t\t\tTextUtilities::SetClipboardText({ url });\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->showToast(\n\t\t\t\t\t\t\ttr::lng_channel_public_link_copied(tr::now));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\trequest.menu->addAction(\n\t\t\t\ttr::lng_group_invite_share(tr::now),\n\t\t\t\t[=] {\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tFastShareLink(strong, url);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tstatic const auto kPrefix = QRegularExpression(u\"^internal:\"\n\t\t\t\"(collectible_username|username_link|username_regular)/\"\n\t\t\t\"([a-zA-Z0-9\\\\-\\\\_\\\\.]+)@\"_q);\n\t\tconst auto match = kPrefix.match(url);\n\t\tif (!match.hasMatch()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto username = match.captured(2);\n\t\tconst auto fullname = username + '@' + peerIdRaw;\n\t\tconst auto mentionLink = \"internal:username_regular/\" + fullname;\n\t\tconst auto linkLink = \"internal:username_link/\" + fullname;\n\t\tconst auto context = QVariant::fromValue(ClickHandlerContext{\n\t\t\t.sessionWindow = weak,\n\t\t});\n\t\tconst auto session = &strong->session();\n\t\tconst auto link = session->createInternalLinkFull(username);\n\t\trequest.menu->addAction(\n\t\t\ttr::lng_context_copy_mention(tr::now),\n\t\t\t[=] { Core::App().openInternalUrl(mentionLink, context); });\n\t\trequest.menu->addAction(\n\t\t\ttr::lng_context_copy_link(tr::now),\n\t\t\t[=] { Core::App().openInternalUrl(linkLink, context); });\n\t\trequest.menu->addAction(\n\t\t\ttr::lng_group_invite_share(tr::now),\n\t\t\t[=] {\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tFastShareLink(strong, link);\n\t\t\t\t}\n\t\t\t});\n\t};\n\tif (const auto user = _peer->asUser()) {\n\t\tif (user->session().supportMode()) {\n\t\t\taddInfoLineGeneric(\n\t\t\t\tuser->session().supportHelper().infoLabelValue(user),\n\t\t\t\tuser->session().supportHelper().infoTextValue(user));\n\t\t}\n\n\t\t{\n\t\t\tconst auto phoneLabel = addInfoOneLine(\n\t\t\t\ttr::lng_info_mobile_label(),\n\t\t\t\tPhoneOrHiddenValue(user),\n\t\t\t\ttr::lng_profile_copy_phone(tr::now)).text;\n\t\t\tconst auto hook = [=](Ui::FlatLabel::ContextMenuRequest request) {\n\t\t\t\tif (request.selection.empty()) {\n\t\t\t\t\tconst auto callback = [=] {\n\t\t\t\t\t\tauto phone = rpl::variable<TextWithEntities>(\n\t\t\t\t\t\t\tPhoneOrHiddenValue(user)).current().text;\n\t\t\t\t\t\tphone.replace(' ', QString()).replace('-', QString());\n\t\t\t\t\t\tTextUtilities::SetClipboardText({ phone });\n\t\t\t\t\t};\n\t\t\t\t\trequest.menu->addAction(\n\t\t\t\t\t\ttr::lng_profile_copy_phone(tr::now),\n\t\t\t\t\t\tcallback);\n\t\t\t\t} else {\n\t\t\t\t\tphoneLabel->fillContextMenu(request);\n\t\t\t\t}\n\t\t\t\tAddPhoneMenu(request.menu, user);\n\t\t\t};\n\t\t\tphoneLabel->setContextMenuHook(hook);\n\t\t}\n\t\tauto label = user->isBot()\n\t\t\t? tr::lng_info_about_label()\n\t\t\t: tr::lng_info_bio_label();\n\t\taddTranslateToMenu(\n\t\t\taddInfoLine(std::move(label), AboutWithAdvancedValue(user)).text,\n\t\t\tAboutWithAdvancedValue(user));\n\n\t\tconst auto usernameLine = addInfoOneLine(\n\t\t\tUsernamesSubtext(_peer, tr::lng_info_username_label()),\n\t\t\tUsernameValue(user, true) | rpl::map([=](TextWithEntities u) {\n\t\t\t\treturn u.text.isEmpty()\n\t\t\t\t\t? TextWithEntities()\n\t\t\t\t\t: tr::link(u, UsernameUrl(user, u.text.mid(1)));\n\t\t\t}),\n\t\t\tQString(),\n\t\t\tst::infoProfileLabeledUsernamePadding);\n\t\tconst auto callback = UsernamesLinkCallback(\n\t\t\t_peer,\n\t\t\tcontroller,\n\t\t\tQString());\n\t\tusernameLine.text->overrideLinkClickHandler(callback);\n\t\tusernameLine.subtext->overrideLinkClickHandler(callback);\n\t\tusernameLine.text->setContextMenuHook(lnkHook);\n\t\tusernameLine.subtext->setContextMenuHook(lnkHook);\n\n\t\tconst auto qrButton = Ui::CreateChild<Ui::IconButton>(\n\t\t\tusernameLine.text->parentWidget(),\n\t\t\tst::infoProfileLabeledButtonQr);\n\t\tqrButton->setAccessibleName(tr::lng_group_invite_context_qr(tr::now));\n\t\tUsernamesValue(_peer) | rpl::on_next([=](const auto &u) {\n\t\t\tqrButton->setVisible(!u.empty());\n\t\t}, qrButton->lifetime());\n\t\tconst auto rightSkip = st::infoProfileLabeledButtonQrRightSkip;\n\t\tfitLabelToButton(qrButton, usernameLine.text, rightSkip);\n\t\tfitLabelToButton(qrButton, usernameLine.subtext, rightSkip);\n\t\tqrButton->setClickedCallback([=, show = controller->uiShow()] {\n\t\t\tUi::DefaultShowFillPeerQrBoxCallback(show, user);\n\t\t\treturn false;\n\t\t});\n\n\t\tif (!user->isBot()) {\n\t\t\ttracker.track(result->add(\n\t\t\t\tCreateBirthday(result, controller, user),\n\t\t\t\t{},\n\t\t\t\tstyle::al_justify));\n\t\t\ttracker.track(result->add(\n\t\t\t\tCreateWorkingHours(result, user), {}, style::al_justify));\n\n\t\t\ttracker.track(result->add(\n\t\t\t\tCreateNotes(result, controller, user), {}, style::al_justify));\n\n\t\t\tauto locationText = user->session().changes().peerFlagsValue(\n\t\t\t\tuser,\n\t\t\t\tData::PeerUpdate::Flag::BusinessDetails\n\t\t\t) | rpl::map([=] {\n\t\t\t\tconst auto &details = user->businessDetails();\n\t\t\t\tif (!details.location) {\n\t\t\t\t\treturn TextWithEntities();\n\t\t\t\t} else if (!details.location.point) {\n\t\t\t\t\treturn TextWithEntities{ details.location.address };\n\t\t\t\t}\n\t\t\t\treturn tr::link(\n\t\t\t\t\tTextUtilities::SingleLine(details.location.address),\n\t\t\t\t\tLocationClickHandler::Url(*details.location.point));\n\t\t\t});\n\t\t\taddInfoOneLine(\n\t\t\t\ttr::lng_info_location_label(),\n\t\t\t\tstd::move(locationText),\n\t\t\t\tQString()\n\t\t\t).text->setLinksTrusted();\n\t\t}\n\t} else {\n\t\tconst auto topicRootId = _topic ? _topic->rootId() : 0;\n\t\tconst auto addToLink = topicRootId\n\t\t\t? ('/' + QString::number(topicRootId.bare))\n\t\t\t: QString();\n\t\tauto linkText = LinkValue(\n\t\t\t_peer,\n\t\t\ttrue,\n\t\t\ttopicRootId\n\t\t) | rpl::map([=](const LinkWithUrl &link) {\n\t\t\tconst auto text = link.text;\n\t\t\treturn text.isEmpty()\n\t\t\t\t? TextWithEntities()\n\t\t\t\t: tr::link(\n\t\t\t\t\t(text.startsWith(u\"https://\"_q)\n\t\t\t\t\t\t? text.mid(u\"https://\"_q.size())\n\t\t\t\t\t\t: text) + addToLink,\n\t\t\t\t\t(addToLink.isEmpty() ? link.url : (text + addToLink)));\n\t\t});\n\t\tconst auto linkLine = addInfoOneLine(\n\t\t\t(topicRootId\n\t\t\t\t? TopicSubtext(_peer)\n\t\t\t\t: UsernamesSubtext(_peer, tr::lng_info_link_label())),\n\t\t\tstd::move(linkText),\n\t\t\tQString());\n\t\tconst auto controller = _controller->parentController();\n\t\tconst auto linkCallback = UsernamesLinkCallback(\n\t\t\t_peer,\n\t\t\tcontroller,\n\t\t\taddToLink);\n\t\tlinkLine.text->overrideLinkClickHandler(linkCallback);\n\t\tlinkLine.subtext->overrideLinkClickHandler(linkCallback);\n\t\tlinkLine.text->setContextMenuHook(lnkHook);\n\t\tlinkLine.subtext->setContextMenuHook(lnkHook);\n\t\tif (!topicRootId || !_peer->username().isEmpty()) {\n\t\t\tconst auto qr = Ui::CreateChild<Ui::IconButton>(\n\t\t\t\tlinkLine.text->parentWidget(),\n\t\t\t\tst::infoProfileLabeledButtonQr);\n\t\t\tqr->setAccessibleName(tr::lng_group_invite_context_qr(tr::now));\n\t\t\tUsernamesValue(_peer) | rpl::on_next([=](const auto &u) {\n\t\t\t\tqr->setVisible(!u.empty());\n\t\t\t}, qr->lifetime());\n\t\t\tconst auto rightSkip = st::infoProfileLabeledButtonQrRightSkip;\n\t\t\tfitLabelToButton(qr, linkLine.text, rightSkip);\n\t\t\tfitLabelToButton(qr, linkLine.subtext, rightSkip);\n\t\t\tconst auto peer = _peer;\n\t\t\tqr->setClickedCallback([=, show = controller->uiShow()] {\n\t\t\t\tUi::DefaultShowFillPeerQrBoxCallback(show, peer);\n\t\t\t\treturn false;\n\t\t\t});\n\t\t}\n\n\t\tif (const auto channel = _topic ? nullptr : _peer->asChannel()) {\n\t\t\tauto locationText = LocationValue(\n\t\t\t\tchannel\n\t\t\t) | rpl::map([](const ChannelLocation *location) {\n\t\t\t\treturn location\n\t\t\t\t\t? tr::link(\n\t\t\t\t\t\tTextUtilities::SingleLine(location->address),\n\t\t\t\t\t\tLocationClickHandler::Url(location->point))\n\t\t\t\t\t: TextWithEntities();\n\t\t\t});\n\t\t\taddInfoOneLine(\n\t\t\t\ttr::lng_info_location_label(),\n\t\t\t\tstd::move(locationText),\n\t\t\t\tQString()\n\t\t\t).text->setLinksTrusted();\n\t\t}\n\n\t\tconst auto about = addInfoLine(tr::lng_info_about_label(), _topic\n\t\t\t? rpl::single(TextWithEntities())\n\t\t\t: AboutWithAdvancedValue(_peer));\n\t\tif (!_topic) {\n\t\t\taddTranslateToMenu(about.text, AboutWithAdvancedValue(_peer));\n\t\t}\n\t}\n\t// ID\n\taddInfoOneLine(\n\t\ttr::lng_info_id_label(),\n\t\tIDValue(_peer),\n\t\ttr::lng_profile_copy_id(tr::now));\n\t//\n\t// Topic ID\n\tif (_topic) {\n\t\tconst auto date = Ui::FormatDateTime(\n\t\t\tbase::unixtime::parse(_topic->creationDate()));\n\t\taddInfoOneLine(\n\t\t\ttr::lng_info_id_topic_label(),\n\t\t\ttr::lng_info_id_topic_value_label(\n\t\t\t\tlt_id,\n\t\t\t\trpl::single(QString::number(_topic->rootId().bare)),\n\t\t\t\tlt_date,\n\t\t\t\trpl::single(date)) | rpl::map(TextWithEntities::Simple),\n\t\t\ttr::lng_profile_copy_id(tr::now));\n\t}\n\t//\n\twrap->toggleOn(tracker.atLeastOneShownValue());\n\twrap->finishAnimating();\n\n\treturn wrap;\n}\n\nobject_ptr<Ui::RpWidget> DetailsFiller::setupPersonalChannel(\n\t\tnot_null<UserData*> user) {\n\tauto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t_wrap,\n\t\tobject_ptr<Ui::VerticalLayout>(_wrap));\n\tconst auto container = result->entity();\n\tconst auto window = _controller->parentController();\n\tconst auto duration = st::slideWrapDuration;\n\n\tresult->toggleOn(PersonalChannelValue(\n\t\tuser\n\t) | rpl::map(rpl::mappers::_1 != nullptr));\n\tresult->finishAnimating();\n\n\tauto channel = PersonalChannelValue(\n\t\tuser\n\t) | rpl::start_spawning(result->lifetime());\n\n\tconst auto channelLabelFactory = [=](rpl::producer<ChannelData*> c) {\n\t\treturn rpl::combine(\n\t\t\ttr::lng_info_personal_channel_label(tr::marked),\n\t\t\tstd::move(c)\n\t\t) | rpl::map([](TextWithEntities &&text, ChannelData *channel) {\n\t\t\tconst auto count = channel ? channel->membersCount() : 0;\n\t\t\tif (count > 1) {\n\t\t\t\ttext.append(' ')\n\t\t\t\t.append(Ui::kQBullet)\n\t\t\t\t.append(' ')\n\t\t\t\t.append(\n\t\t\t\t\ttr::lng_chat_status_subscribers(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\tcount));\n\t\t\t}\n\t\t\treturn text;\n\t\t});\n\t};\n\tif (user->isSelf()) {\n\t\tstruct State {\n\t\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t\t};\n\t\tconst auto state = container->lifetime().make_state<State>();\n\t\tbase::install_event_filter(container, [=](\n\t\t\t\tnot_null<QEvent*> e) {\n\t\t\tif (e->type() == QEvent::ContextMenu) {\n\t\t\t\tconst auto ce = static_cast<QContextMenuEvent*>(e.get());\n\t\t\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\t\tcontainer,\n\t\t\t\t\tst::defaultPopupMenu);\n\t\t\t\tstate->menu->addAction(\n\t\t\t\t\ttr::lng_settings_channel_menu_remove(tr::now),\n\t\t\t\t\t[] {\n\t\t\t\t\t\tUrlClickHandler::Open(\n\t\t\t\t\t\t\tu\"internal:edit_personal_channel:remove\"_q);\n\t\t\t\t\t});\n\t\t\t\tstate->menu->popup(ce->globalPos());\n\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t}\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}, container->lifetime());\n\t}\n\n\t{\n\t\tconst auto onlyChannelWrap = container->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\t\tonlyChannelWrap->toggleOn(rpl::duplicate(channel) | rpl::map([=] {\n\t\t\treturn user->personalChannelId()\n\t\t\t\t&& !user->personalChannelMessageId();\n\t\t}));\n\t\tonlyChannelWrap->finishAnimating();\n\n\t\tauto text = rpl::duplicate(\n\t\t\tchannel\n\t\t) | rpl::map([=](ChannelData *channel) {\n\t\t\treturn channel ? NameValue(channel) : rpl::single(QString());\n\t\t}) | rpl::flatten_latest() | rpl::map([](const QString &name) {\n\t\t\treturn name.isEmpty() ? TextWithEntities() : tr::link(name);\n\t\t});\n\t\tauto line = CreateTextWithLabel(\n\t\t\tresult,\n\t\t\tchannelLabelFactory(rpl::duplicate(channel)),\n\t\t\tstd::move(text),\n\t\t\tst::infoLabel,\n\t\t\tst::infoLabeled,\n\t\t\tst::infoProfilePersonalChannelPadding);\n\t\tonlyChannelWrap->entity()->add(std::move(line.wrap));\n\n\t\tline.text->setClickHandlerFilter([=](\n\t\t\t\tconst ClickHandlerPtr &handler,\n\t\t\t\tQt::MouseButton button) {\n\t\t\tif (const auto channelId = user->personalChannelId()) {\n\t\t\t\twindow->showPeerInfo(peerFromChannel(channelId));\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\n\t\tobject_ptr<FloatingIcon>(\n\t\t\tonlyChannelWrap,\n\t\t\tst::infoIconMediaChannel,\n\t\t\tst::infoPersonalChannelIconPosition);\n\n\t\tUi::AddDivider(onlyChannelWrap->entity());\n\t}\n\n\t{\n\t\tconst auto messageChannelWrap = container->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\t\tmessageChannelWrap->setDuration(duration);\n\t\tmessageChannelWrap->toggleOn(rpl::duplicate(channel) | rpl::map([=] {\n\t\t\treturn user->personalChannelId()\n\t\t\t\t&& user->personalChannelMessageId();\n\t\t}));\n\t\tmessageChannelWrap->finishAnimating();\n\t\tmessageChannelWrap->toggledValue(\n\t\t) | rpl::filter(rpl::mappers::_1) | rpl::on_next([=] {\n\t\t\tmessageChannelWrap->resizeToWidth(messageChannelWrap->width());\n\t\t}, messageChannelWrap->lifetime());\n\n\t\tconst auto clear = [=] {\n\t\t\twhile (messageChannelWrap->entity()->count()) {\n\t\t\t\tdelete messageChannelWrap->entity()->widgetAt(0);\n\t\t\t}\n\t\t};\n\n\t\tconst auto rebuild = [=](\n\t\t\t\tnot_null<HistoryItem*> item,\n\t\t\t\tanim::type animated) {\n\t\t\tconst auto &stUserpic = st::infoPersonalChannelUserpic;\n\t\t\tconst auto &stLabeled = st::infoProfilePersonalChannelPadding;\n\n\t\t\tmessageChannelWrap->toggle(false, anim::type::instant);\n\t\t\tclear();\n\n\t\t\tUi::AddSkip(messageChannelWrap->entity());\n\n\t\t\tconst auto inner = messageChannelWrap->entity()->add(\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(messageChannelWrap->entity()));\n\n\t\t\tconst auto line = inner->add(\n\t\t\t\tobject_ptr<Ui::FixedHeightWidget>(\n\t\t\t\t\tinner,\n\t\t\t\t\tstUserpic.photoSize + rect::m::sum::v(stLabeled)));\n\t\t\tconst auto userpic = Ui::CreateChild<Ui::UserpicButton>(\n\t\t\t\tline,\n\t\t\t\titem->history()->peer,\n\t\t\t\tst::infoPersonalChannelUserpic);\n\n\t\t\tuserpic->moveToLeft(\n\t\t\t\t-st::infoPersonalChannelUserpicSkip\n\t\t\t\t\t+ (stLabeled.left() - stUserpic.photoSize) / 2,\n\t\t\t\tstLabeled.top());\n\t\t\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t\t\tconst auto date = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\tline,\n\t\t\t\tUi::FormatDialogsDate(ItemDateTime(item)),\n\t\t\t\tst::infoPersonalChannelDateLabel);\n\n\t\t\tconst auto name = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\tline,\n\t\t\t\tNameValue(item->history()->peer),\n\t\t\t\tst::infoPersonalChannelNameLabel);\n\n\t\t\tconst auto preview = Ui::CreateChild<Ui::RpWidget>(line);\n\t\t\tauto &lifetime = preview->lifetime();\n\t\t\tusing namespace Dialogs::Ui;\n\t\t\tstruct State {\n\t\t\t\tMessageView view;\n\t\t\t\tHistoryItem *item = nullptr;\n\t\t\t\trpl::lifetime lifetime;\n\t\t\t};\n\t\t\tconst auto state = lifetime.make_state<State>();\n\t\t\tstate->item = item;\n\t\t\titem->history()->session().changes().realtimeMessageUpdates(\n\t\t\t\tData::MessageUpdate::Flag::Destroyed\n\t\t\t) | rpl::on_next([=](const Data::MessageUpdate &update) {\n\t\t\t\tif (update.item == state->item) {\n\t\t\t\t\tstate->lifetime.destroy();\n\t\t\t\t\tstate->item = nullptr;\n\t\t\t\t\tpreview->update();\n\t\t\t\t}\n\t\t\t}, state->lifetime);\n\n\t\t\tpreview->resize(0, st::infoLabeled.style.font->height);\n\t\t\tpreview->paintRequest(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tauto p = Painter(preview);\n\t\t\t\tconst auto item = state->item;\n\t\t\t\tif (!item) {\n\t\t\t\t\tp.setPen(st::infoPersonalChannelDateLabel.textFg);\n\t\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t\t\tp.setFont(st::infoPersonalChannelDateLabel.style.font);\n\t\t\t\t\tp.drawText(\n\t\t\t\t\t\tpreview->rect(),\n\t\t\t\t\t\ttr::lng_deleted_message(tr::now),\n\t\t\t\t\t\tstyle::al_left);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!state->view.prepared(item, nullptr, nullptr)) {\n\t\t\t\t\tconst auto repaint = [=] { preview->update(); };\n\t\t\t\t\tstate->view.prepare(\n\t\t\t\t\t\titem,\n\t\t\t\t\t\tnullptr,\n\t\t\t\t\t\tnullptr,\n\t\t\t\t\t\trepaint,\n\t\t\t\t\t\t{});\n\t\t\t\t}\n\t\t\t\tstate->view.paint(p, preview->rect(), {\n\t\t\t\t\t.st = &st::defaultDialogRow,\n\t\t\t\t\t.currentBg = st::boxBg->b,\n\t\t\t\t});\n\t\t\t}, preview->lifetime());\n\n\t\t\tline->sizeValue() | rpl::filter_size(\n\t\t\t) | rpl::on_next([=](const QSize &size) {\n\t\t\t\tconst auto left = stLabeled.left();\n\t\t\t\tconst auto right = st::infoPersonalChannelDateSkip;\n\t\t\t\tconst auto top = stLabeled.top();\n\t\t\t\tdate->moveToRight(right, top, size.width());\n\n\t\t\t\tname->resizeToWidth(size.width()\n\t\t\t\t\t- left\n\t\t\t\t\t- date->width()\n\t\t\t\t\t- st::defaultVerticalListSkip\n\t\t\t\t\t- right);\n\t\t\t\tname->moveToLeft(left, top);\n\n\t\t\t\tpreview->resize(\n\t\t\t\t\tsize.width() - left - right,\n\t\t\t\t\tst::infoLabeled.style.font->height);\n\t\t\t\tpreview->moveToLeft(\n\t\t\t\t\tleft,\n\t\t\t\t\tsize.height() - stLabeled.bottom() - preview->height());\n\t\t\t}, preview->lifetime());\n\n\t\t\t{\n\t\t\t\tinner->add(\n\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\tinner,\n\t\t\t\t\t\tchannelLabelFactory(\n\t\t\t\t\t\t\trpl::single(item->history()->peer->asChannel())),\n\t\t\t\t\t\tst::infoLabel),\n\t\t\t\t\tQMargins(\n\t\t\t\t\t\tst::infoProfilePersonalChannelPadding.left(),\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tst::infoProfilePersonalChannelPadding.right(),\n\t\t\t\t\t\tst::infoProfilePersonalChannelPadding.bottom()));\n\t\t\t}\n\t\t\t{\n\t\t\t\tconst auto button = Ui::CreateSimpleRectButton(\n\t\t\t\t\tmessageChannelWrap->entity(),\n\t\t\t\t\tst::defaultRippleAnimation);\n\t\t\t\tinner->geometryValue(\n\t\t\t\t) | rpl::on_next([=](const QRect &rect) {\n\t\t\t\t\tbutton->setGeometry(rect);\n\t\t\t\t}, button->lifetime());\n\t\t\t\tbutton->setClickedCallback([=, msg = item->fullId().msg] {\n\t\t\t\t\twindow->showPeerHistory(\n\t\t\t\t\t\titem->history()->peer,\n\t\t\t\t\t\tWindow::SectionShow::Way::Forward,\n\t\t\t\t\t\tmsg);\n\t\t\t\t});\n\t\t\t\tbutton->lower();\n\t\t\t\tinner->lifetime().make_state<base::unique_qptr<Ui::RpWidget>>(\n\t\t\t\t\tbutton);\n\t\t\t\tbutton->setAccessibleName(tr::lng_profile_view_channel(tr::now));\n\t\t\t}\n\t\t\tinner->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\tUi::AddSkip(messageChannelWrap->entity());\n\t\t\tUi::AddDivider(messageChannelWrap->entity());\n\n\t\t\tUi::ToggleChildrenVisibility(messageChannelWrap->entity(), true);\n\t\t\tUi::ToggleChildrenVisibility(line, true);\n\t\t\tmessageChannelWrap->toggle(true, animated);\n\t\t};\n\n\t\trpl::duplicate(\n\t\t\tchannel\n\t\t) | rpl::on_next([=](ChannelData *channel) {\n\t\t\tif (!channel && messageChannelWrap->animating()) {\n\t\t\t\tbase::call_delayed(duration, messageChannelWrap, clear);\n\t\t\t} else {\n\t\t\t\tclear();\n\t\t\t}\n\t\t\tif (!channel) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto id = FullMsgId(\n\t\t\t\tchannel->id,\n\t\t\t\tuser->personalChannelMessageId());\n\t\t\tif (const auto item = user->session().data().message(id)) {\n\t\t\t\treturn rebuild(item, anim::type::instant);\n\t\t\t}\n\t\t\tuser->session().api().requestMessageData(\n\t\t\t\tchannel,\n\t\t\t\tuser->personalChannelMessageId(),\n\t\t\t\tcrl::guard(container, [=] {\n\t\t\t\t\tif (const auto i = user->session().data().message(id)) {\n\t\t\t\t\t\trebuild(i, anim::type::normal);\n\t\t\t\t\t}\n\t\t\t\t}));\n\t\t}, messageChannelWrap->lifetime());\n\t}\n\n\treturn result;\n}\n\nvoid DetailsFiller::setupMainApp(bool suppressBottom) {\n\tconst auto button = _wrap->add(\n\t\tobject_ptr<Ui::RoundButton>(\n\t\t\t_wrap,\n\t\t\ttr::lng_profile_open_app(),\n\t\t\tst::infoOpenApp),\n\t\tst::infoOpenAppMargin,\n\t\tstyle::al_justify);\n\n\tconst auto user = _peer->asUser();\n\tconst auto controller = _controller->parentController();\n\tbutton->setClickedCallback([=] {\n\t\tuser->session().attachWebView().open({\n\t\t\t.bot = user,\n\t\t\t.context = {\n\t\t\t\t.controller = controller,\n\t\t\t\t.maySkipConfirmation = true,\n\t\t\t},\n\t\t\t.source = InlineBots::WebViewSourceBotProfile(),\n\t\t});\n\t});\n\n\tconst auto url = tr::lng_mini_apps_tos_url(tr::now);\n\tconst auto parts = suppressBottom\n\t\t? RectPart::Top\n\t\t: (RectPart::Top | RectPart::Bottom);\n\tconst auto divider = Ui::AddDividerText(\n\t\t_wrap,\n\t\trpl::combine(\n\t\t\ttr::lng_profile_open_app_about(\n\t\t\t\tlt_terms,\n\t\t\t\ttr::lng_profile_open_app_terms(tr::url(url)),\n\t\t\t\ttr::marked),\n\t\t\tuser->session().changes().peerFlagsValue(\n\t\t\t\tuser,\n\t\t\t\tData::PeerUpdate::Flag::VerifyInfo)\n\t\t) | rpl::map([=](TextWithEntities text, auto) {\n\t\t\tif (const auto verify = user->botVerifyDetails()) {\n\t\t\t\ttext = text.append(u\"\\n\\n\"_q).append(verify->description);\n\t\t\t}\n\t\t\treturn text;\n\t\t}),\n\t\tst::defaultBoxDividerLabelPadding,\n\t\tst::defaultDividerLabel,\n\t\tparts);\n\tdivider->setClickHandlerFilter([=](const auto &...) {\n\t\tUrlClickHandler::Open(url);\n\t\treturn false;\n\t});\n}\n\nvoid DetailsFiller::setupBotPermissions() {\n\tAddSkip(_wrap);\n\tAddSubsectionTitle(_wrap, tr::lng_profile_bot_permissions_title());\n\tconst auto emoji = _wrap->add(\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t_wrap,\n\t\t\ttr::lng_profile_bot_emoji_status_access(),\n\t\t\tst::infoSharedMediaButton));\n\tobject_ptr<Profile::FloatingIcon>(\n\t\temoji,\n\t\tst::infoIconEmojiStatusAccess,\n\t\tst::infoSharedMediaButtonIconPosition);\n\n\tconst auto user = _peer->asUser();\n\temoji->toggleOn(\n\t\trpl::single(bool(user->botInfo->canManageEmojiStatus))\n\t)->toggledValue() | rpl::filter([=](bool allowed) {\n\t\treturn allowed != user->botInfo->canManageEmojiStatus;\n\t}) | rpl::on_next([=](bool allowed) {\n\t\tuser->botInfo->canManageEmojiStatus = allowed;\n\t\tconst auto session = &user->session();\n\t\tsession->api().request(MTPbots_ToggleUserEmojiStatusPermission(\n\t\t\tuser->inputUser(),\n\t\t\tMTP_bool(allowed)\n\t\t)).send();\n\t}, emoji->lifetime());\n\tAddSkip(_wrap);\n}\n\nvoid DetailsFiller::addReportReaction(\n\t\tUi::MultiSlideTracker &tracker,\n\t\tUi::MultiSlideTracker *buttonTracker) {\n\tv::match(_origin.data, [&](GroupReactionOrigin data) {\n\t\tconst auto user = _peer->asUser();\n\t\tif (_peer->isSelf()) {\n\t\t\treturn;\n#if 0 // Only public groups allow reaction reports for now.\n\t\t} else if (const auto chat = data.group->asChat()) {\n\t\t\tconst auto ban = chat->canBanMembers()\n\t\t\t\t&& (!user || !chat->admins.contains(_peer))\n\t\t\t\t&& (!user || chat->creator != user->id);\n\t\t\taddReportReaction(data, ban, tracker);\n#endif\n\t\t} else if (const auto channel = data.group->asMegagroup()) {\n\t\t\tif (channel->isPublic()) {\n\t\t\t\tconst auto ban = channel->canBanMembers()\n\t\t\t\t\t&& (!user || !channel->mgInfo->admins.contains(user->id))\n\t\t\t\t\t&& (!user || channel->mgInfo->creator != user);\n\t\t\t\taddReportReaction(data, ban, tracker, buttonTracker);\n\t\t\t}\n\t\t}\n\t}, [](const auto &) {});\n}\n\nvoid DetailsFiller::addReportReaction(\n\t\tGroupReactionOrigin data,\n\t\tbool ban,\n\t\tUi::MultiSlideTracker &tracker,\n\t\tUi::MultiSlideTracker *buttonTracker) {\n\tconst auto peer = _peer;\n\tif (!peer) {\n\t\treturn;\n\t}\n\tconst auto controller = _controller->parentController();\n\tconst auto forceHidden = std::make_shared<rpl::variable<bool>>(false);\n\tconst auto user = peer->asUser();\n\tconst auto wrap = _wrap->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_wrap.data(),\n\t\t\tobject_ptr<Ui::VerticalLayout>(_wrap.data())));\n\tUi::AddSkip(wrap->entity());\n\tauto shown = user\n\t\t? rpl::combine(\n\t\t\tInfo::Profile::IsContactValue(user),\n\t\t\tforceHidden->value(),\n\t\t\t!rpl::mappers::_1 && !rpl::mappers::_2\n\t\t) | rpl::type_erased\n\t\t: (forceHidden->value() | rpl::map(!rpl::mappers::_1));\n\tconst auto sent = [=] {\n\t\t*forceHidden = true;\n\t};\n\twrap->toggleOn(rpl::duplicate(shown));\n\trpl::duplicate(shown) | rpl::on_next([=](bool shown) {\n\t\tif (shown) {\n\t\t\t_dividerOverridden.force_assign(false);\n\t\t}\n\t}, wrap->lifetime());\n\tAddMainButton(\n\t\t_wrap,\n\t\t(ban\n\t\t\t? tr::lng_report_and_ban()\n\t\t\t: tr::lng_report_reaction()),\n\t\tstd::move(shown),\n\t\t[=] { controller->show(\n\t\t\tBox(ReportReactionBox, controller, peer, data, ban, sent)); },\n\t\ttracker,\n\t\tbuttonTracker,\n\t\tst::infoMainButtonAttention);\n}\n\nvoid DetailsFiller::addViewChannelButton(\n\t\tUi::MultiSlideTracker &tracker,\n\t\tnot_null<ChannelData*> channel,\n\t\tUi::MultiSlideTracker *buttonTracker) {\n\tusing namespace rpl::mappers;\n\n\tconst auto wrap = _wrap->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_wrap.data(),\n\t\t\tobject_ptr<Ui::VerticalLayout>(_wrap.data())));\n\tUi::AddSkip(wrap->entity());\n\n\tconst auto window = _controller->parentController();\n\tauto activePeerValue = window->activeChatValue(\n\t) | rpl::map([](Dialogs::Key key) {\n\t\treturn key.peer();\n\t});\n\tauto viewChannelVisible = rpl::combine(\n\t\t_controller->wrapValue(),\n\t\tstd::move(activePeerValue),\n\t\t(_1 != Wrap::Side) || (_2 != channel));\n\tauto viewChannel = [=] {\n\t\twindow->showPeerHistory(\n\t\t\tchannel,\n\t\t\tWindow::SectionShow::Way::Forward);\n\t};\n\twrap->toggleOn(rpl::duplicate(viewChannelVisible));\n\tAddMainButton(\n\t\twrap->entity(),\n\t\ttr::lng_profile_view_channel(),\n\t\tstd::move(viewChannelVisible),\n\t\tstd::move(viewChannel),\n\t\ttracker,\n\t\tbuttonTracker);\n}\n\nvoid DetailsFiller::addShowTopicsListButton(\n\t\tUi::MultiSlideTracker &tracker,\n\t\tnot_null<Data::Forum*> forum,\n\t\tUi::MultiSlideTracker *buttonTracker) {\n\tusing namespace rpl::mappers;\n\n\tconst auto window = _controller->parentController();\n\tconst auto peer = forum->peer();\n\tauto showTopicsVisible = rpl::combine(\n\t\twindow->adaptive().oneColumnValue(),\n\t\twindow->shownForum().value(),\n\t\t_1 || (_2 != forum));\n\tconst auto callback = [=] {\n\t\tif (const auto forum = peer->forum()) {\n\t\t\tif (peer->useSubsectionTabs()) {\n\t\t\t\twindow->searchInChat(forum->history());\n\t\t\t} else {\n\t\t\t\twindow->showForum(forum);\n\t\t\t}\n\t\t}\n\t};\n\tAddMainButton(\n\t\t_wrap,\n\t\t(forum->peer()->isBot()\n\t\t\t? tr::lng_bot_show_threads_list()\n\t\t\t: tr::lng_forum_show_topics_list()),\n\t\tstd::move(showTopicsVisible),\n\t\tcallback,\n\t\ttracker,\n\t\tbuttonTracker);\n}\n\nobject_ptr<Ui::RpWidget> DetailsFiller::fill() {\n\tExpects(!_topic || !_topic->creating());\n\n\tif (const auto user = _sublist ? nullptr : _peer->asUser()) {\n\t\tadd(setupPersonalChannel(user));\n\t}\n\t// add(CreateSlideSkipWidget(_wrap))->toggleOn(\n\t// \t_mainTracker.atLeastOneShownValueLater());\n\tadd(setupInfo());\n\tauto lastButtonTracker = Ui::MultiSlideTracker();\n\tif (const auto user = _peer->asUser()) {\n\t\t{\n\t\t\tconst auto wrap = _wrap->add(\n\t\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\t\t_wrap.data(),\n\t\t\t\t\tobject_ptr<Ui::VerticalLayout>(_wrap.data())));\n\t\t\tUi::AddSkip(wrap->entity());\n\t\t\tAddMainButton(\n\t\t\t\twrap->entity(),\n\t\t\t\ttr::lng_info_add_as_contact(),\n\t\t\t\tCanAddContactValue(user),\n\t\t\t\t[=, controller = _controller->parentController()] {\n\t\t\t\t\tcontroller->uiShow()->show(\n\t\t\t\t\t\tBox(EditContactBox, controller, user));\n\t\t\t\t},\n\t\t\t\t_mainTracker,\n\t\t\t\t&lastButtonTracker);\n\t\t\twrap->toggleOn(CanAddContactValue(user));\n\t\t}\n\t\tif (const auto info = user->botInfo.get()) {\n\t\t\tif (info->hasMainApp) {\n\t\t\t\t_dividerOverridden.force_assign(true);\n\t\t\t\tconst auto managedBotFollows = user->botManagerId()\n\t\t\t\t\t&& !info->canManageEmojiStatus\n\t\t\t\t\t&& user->owner().userLoaded(user->botManagerId());\n\t\t\t\tsetupMainApp(managedBotFollows);\n\t\t\t}\n\t\t\tif (info->canManageEmojiStatus) {\n\t\t\t\t_dividerOverridden.force_assign(false);\n\t\t\t\tsetupBotPermissions();\n\t\t\t}\n\t\t\tif (user->botManagerId()) {\n\t\t\t\tif (const auto managerUser = user->owner().userLoaded(\n\t\t\t\t\t\tuser->botManagerId())) {\n\t\t\t\t\tif (!info->hasMainApp) {\n\t\t\t\t\t\t_dividerOverridden.force_assign(true);\n\t\t\t\t\t}\n\t\t\t\t\tconst auto botUsername = managerUser->username();\n\t\t\t\t\tconst auto linkText = botUsername.isEmpty()\n\t\t\t\t\t\t? managerUser->name()\n\t\t\t\t\t\t: (u\"@\"_q + botUsername);\n\t\t\t\t\tconst auto parts = (info->hasMainApp && !info->canManageEmojiStatus)\n\t\t\t\t\t\t? RectPart::Bottom\n\t\t\t\t\t\t: (RectPart::Top | RectPart::Bottom);\n\t\t\t\t\tUi::AddSkip(_wrap);\n\t\t\t\t\tconst auto divider = Ui::AddDividerText(\n\t\t\t\t\t\t_wrap,\n\t\t\t\t\t\ttr::lng_managed_bot_label(\n\t\t\t\t\t\t\tlt_icon,\n\t\t\t\t\t\t\trpl::single(Ui::Text::IconEmoji(&st::managedBotIconEmoji)),\n\t\t\t\t\t\t\tlt_bot,\n\t\t\t\t\t\t\trpl::single(tr::link(linkText)),\n\t\t\t\t\t\t\ttr::marked),\n\t\t\t\t\t\tst::defaultBoxDividerLabelPadding,\n\t\t\t\t\t\tst::defaultDividerLabel,\n\t\t\t\t\t\tparts);\n\t\t\t\t\tconst auto weak = base::make_weak(_controller);\n\t\t\t\t\tdivider->setClickHandlerFilter([=](const auto &...) {\n\t\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\t\tstrong->showPeerInfo(managerUser);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!user->isSelf() && !_sublist) {\n\t\t\taddReportReaction(_mainTracker, &lastButtonTracker);\n\t\t}\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\tif (!channel->isMegagroup()) {\n\t\t\t_dividerOverridden.force_assign(false);\n\t\t\taddViewChannelButton(_mainTracker, channel, &lastButtonTracker);\n\t\t}\n\t\tif (const auto forum = channel->forum()) {\n\t\t\t_dividerOverridden.force_assign(false);\n\t\t\taddShowTopicsListButton(\n\t\t\t\t_mainTracker,\n\t\t\t\tforum,\n\t\t\t\t&lastButtonTracker);\n\t\t}\n\t}\n\tadd(CreateSlideSkipWidget(_wrap))->toggleOn(\n\t\tlastButtonTracker.atLeastOneShownValueLater());\n\n\treturn std::move(_wrap);\n}\n\nActionsFiller::ActionsFiller(\n\tnot_null<Controller*> controller,\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<PeerData*> peer)\n: _controller(controller)\n, _parent(parent)\n, _peer(peer) {\n}\n\nvoid ActionsFiller::addAffiliateProgram(not_null<UserData*> user) {\n\tif (!user->isBot()) {\n\t\treturn;\n\t}\n\n\tconst auto wrap = _wrap->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_wrap.data(),\n\t\t\tobject_ptr<Ui::VerticalLayout>(_wrap.data())));\n\tconst auto inner = wrap->entity();\n\tauto program = user->session().changes().peerFlagsValue(\n\t\tuser,\n\t\tData::PeerUpdate::Flag::StarRefProgram\n\t) | rpl::map([=] {\n\t\treturn user->botInfo->starRefProgram;\n\t}) | rpl::start_spawning(inner->lifetime());\n\tauto commission = rpl::duplicate(\n\t\tprogram\n\t) | rpl::filter([=](StarRefProgram program) {\n\t\treturn program.commission > 0;\n\t}) | rpl::map([=](StarRefProgram program) {\n\t\treturn Info::BotStarRef::FormatCommission(program.commission);\n\t});\n\tconst auto show = _controller->uiShow();\n\n\tstruct StarRefRecipients {\n\t\tstd::vector<not_null<PeerData*>> list;\n\t\tbool requested = false;\n\t\tFn<void()> open;\n\t};\n\tconst auto recipients = std::make_shared<StarRefRecipients>();\n\trecipients->open = [=] {\n\t\tif (!recipients->list.empty()) {\n\t\t\tconst auto program = user->botInfo->starRefProgram;\n\t\t\tshow->show(Info::BotStarRef::JoinStarRefBox(\n\t\t\t\t{ user, { program } },\n\t\t\t\tuser->session().user(),\n\t\t\t\trecipients->list));\n\t\t} else if (!recipients->requested) {\n\t\t\trecipients->requested = true;\n\t\t\tconst auto done = [=](std::vector<not_null<PeerData*>> list) {\n\t\t\t\trecipients->list = std::move(list);\n\t\t\t\trecipients->open();\n\t\t\t};\n\t\t\tInfo::BotStarRef::ResolveRecipients(&user->session(), done);\n\t\t}\n\t};\n\n\tinner->add(EditPeerInfoBox::CreateButton(\n\t\tinner,\n\t\ttr::lng_manage_peer_bot_star_ref(),\n\t\trpl::duplicate(commission),\n\t\trecipients->open,\n\t\tst::infoSharedMediaCountButton,\n\t\t{ .icon = &st::menuIconSharing, .newBadge = true }));\n\tUi::AddSkip(inner);\n\tUi::AddDividerText(\n\t\tinner,\n\t\ttr::lng_manage_peer_bot_star_ref_about(\n\t\t\tlt_bot,\n\t\t\trpl::single(TextWithEntities{ user->name() }),\n\t\t\tlt_amount,\n\t\t\trpl::duplicate(commission) | rpl::map(tr::marked),\n\t\t\ttr::rich));\n\tUi::AddSkip(inner);\n\n\twrap->toggleOn(std::move(\n\t\tprogram\n\t) | rpl::map([](StarRefProgram program) {\n\t\treturn program.commission > 0;\n\t}));\n\twrap->finishAnimating();\n}\n\nvoid ActionsFiller::addBalanceActions(not_null<UserData*> user) {\n\tconst auto wrap = _wrap->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_wrap.data(),\n\t\t\tobject_ptr<Ui::VerticalLayout>(_wrap.data())));\n\tconst auto inner = wrap->entity();\n\tUi::AddSubsectionTitle(inner, tr::lng_manage_peer_bot_balance());\n\tauto currencyBalance = AddCurrencyAction(user, inner, _controller);\n\tauto creditsBalance = AddCreditsAction(user, inner, _controller);\n\tUi::AddSkip(inner);\n\tUi::AddDivider(inner);\n\tUi::AddSkip(inner);\n\twrap->toggleOn(\n\t\trpl::combine(\n\t\t\tstd::move(currencyBalance),\n\t\t\tstd::move(creditsBalance)\n\t\t) | rpl::map((rpl::mappers::_1 > CreditsAmount(0))\n\t\t\t|| (rpl::mappers::_2 > CreditsAmount(0))));\n}\n\nvoid ActionsFiller::addInviteToGroupAction(not_null<UserData*> user) {\n\tconst auto notEmpty = [](const QString &value) {\n\t\treturn !value.isEmpty();\n\t};\n\tconst auto controller = _controller->parentController();\n\tAddActionButton(\n\t\t_wrap,\n\t\tInviteToChatButton(user) | rpl::filter(notEmpty),\n\t\tInviteToChatButton(user) | rpl::map(notEmpty),\n\t\t[=] { AddBotToGroupBoxController::Start(controller, user); },\n\t\t&st::infoIconAddMember);\n\tconst auto about = _wrap->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_wrap.data(),\n\t\t\tobject_ptr<Ui::VerticalLayout>(_wrap.data())));\n\tabout->toggleOn(InviteToChatAbout(user) | rpl::map(notEmpty));\n\tUi::AddSkip(about->entity());\n\tUi::AddDividerText(\n\t\tabout->entity(),\n\t\tInviteToChatAbout(user) | rpl::filter(notEmpty));\n\tUi::AddSkip(about->entity());\n\tabout->finishAnimating();\n}\n\nvoid ActionsFiller::addShareContactAction(not_null<UserData*> user) {\n\tconst auto controller = _controller->parentController();\n\tAddActionButton(\n\t\t_wrap,\n\t\ttr::lng_info_share_contact(),\n\t\tCanShareContactValue(user),\n\t\t[=] { Window::PeerMenuShareContactBox(controller, user); },\n\t\t&st::infoIconShare);\n}\n\nvoid ActionsFiller::addEditContactAction(not_null<UserData*> user) {\n\tconst auto controller = _controller->parentController();\n\tconst auto edit = [=] {\n\t\tif (controller->showFrozenError()) {\n\t\t\treturn;\n\t\t}\n\t\tcontroller->window().show(Box(EditContactBox, controller, user));\n\t};\n\tAddActionButton(\n\t\t_wrap,\n\t\ttr::lng_info_edit_contact(),\n\t\tIsContactValue(user),\n\t\tedit,\n\t\t&st::infoIconEdit);\n}\n\nvoid ActionsFiller::addDeleteContactAction(not_null<UserData*> user) {\n\tconst auto controller = _controller->parentController();\n\tAddActionButton(\n\t\t_wrap,\n\t\ttr::lng_info_delete_contact(),\n\t\tIsContactValue(user),\n\t\t[=] { Window::PeerMenuDeleteContact(controller, user); },\n\t\t&st::infoIconDelete);\n}\n\nvoid ActionsFiller::addFastButtonsMode(not_null<UserData*> user) {\n\tExpects(user->isBot());\n\n\tconst auto bots = &user->session().fastButtonsBots();\n\tconst auto button = _wrap->add(object_ptr<Ui::SettingsButton>(\n\t\t_wrap,\n\t\trpl::single(u\"Fast buttons mode\"_q),\n\t\tst::infoSharedMediaButton));\n\tobject_ptr<Info::Profile::FloatingIcon>(\n\t\tbutton,\n\t\tst::infoIconMediaBot,\n\t\tst::infoSharedMediaButtonIconPosition);\n\n\tAddSkip(_wrap);\n\tAddDivider(_wrap);\n\tAddSkip(_wrap);\n\n\tbutton->toggleOn(bots->enabledValue(user));\n\tbutton->toggledValue(\n\t) | rpl::filter([=](bool value) {\n\t\treturn value != bots->enabled(user);\n\t}) | rpl::on_next([=](bool value) {\n\t\tbots->setEnabled(user, value);\n\t}, button->lifetime());\n}\n\nvoid ActionsFiller::addBotCommandActions(not_null<UserData*> user) {\n\tif (FastButtonsMode()) {\n\t\taddFastButtonsMode(user);\n\t}\n\tconst auto window = _controller->parentController();\n\tconst auto findBotCommand = [user](const QString &command) {\n\t\tif (!user->isBot()) {\n\t\t\treturn QString();\n\t\t}\n\t\tfor (const auto &data : user->botInfo->commands) {\n\t\t\tconst auto isSame = !data.command.compare(\n\t\t\t\tcommand,\n\t\t\t\tQt::CaseInsensitive);\n\t\t\tif (isSame) {\n\t\t\t\treturn data.command;\n\t\t\t}\n\t\t}\n\t\treturn QString();\n\t};\n\tconst auto hasBotCommandValue = [=](const QString &command) {\n\t\treturn user->session().changes().peerFlagsValue(\n\t\t\tuser,\n\t\t\tData::PeerUpdate::Flag::BotCommands\n\t\t) | rpl::map([=] {\n\t\t\treturn !findBotCommand(command).isEmpty();\n\t\t});\n\t};\n\tconst auto makeOtherContext = [=] {\n\t\treturn QVariant::fromValue(ClickHandlerContext{\n\t\t\t.sessionWindow = base::make_weak(window),\n\t\t\t.peer = user,\n\t\t});\n\t};\n\tconst auto sendBotCommand = [=](const QString &command) {\n\t\tconst auto original = findBotCommand(command);\n\t\tif (original.isEmpty()) {\n\t\t\treturn false;\n\t\t}\n\t\tBotCommandClickHandler('/' + original).onClick(ClickContext{\n\t\t\tQt::LeftButton,\n\t\t\tmakeOtherContext()\n\t\t});\n\t\treturn true;\n\t};\n\tconst auto addBotCommand = [=](\n\t\t\trpl::producer<QString> text,\n\t\t\tconst QString &command,\n\t\t\tconst style::icon *icon = nullptr) {\n\t\tAddActionButton(\n\t\t\t_wrap,\n\t\t\tstd::move(text),\n\t\t\thasBotCommandValue(command),\n\t\t\t[=] { sendBotCommand(command); },\n\t\t\ticon);\n\t};\n\taddBotCommand(\n\t\ttr::lng_profile_bot_help(),\n\t\tu\"help\"_q,\n\t\t&st::infoIconInformation);\n\taddBotCommand(\n\t\ttr::lng_profile_bot_settings(),\n\t\tu\"settings\"_q,\n\t\t&st::infoIconSettings);\n\t//addBotCommand(tr::lng_profile_bot_privacy(), u\"privacy\"_q);\n\tconst auto openUrl = [=](const QString &url) {\n\t\tCore::App().iv().openWithIvPreferred(\n\t\t\t&user->session(),\n\t\t\turl,\n\t\t\tmakeOtherContext());\n\t};\n\tconst auto openPrivacyPolicy = [=] {\n\t\tif (const auto info = user->botInfo.get()) {\n\t\t\tif (!info->privacyPolicyUrl.isEmpty()) {\n\t\t\t\topenUrl(info->privacyPolicyUrl);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (!sendBotCommand(u\"privacy\"_q)) {\n\t\t\topenUrl(tr::lng_profile_bot_privacy_url(tr::now));\n\t\t}\n\t};\n\tAddActionButton(\n\t\t_wrap,\n\t\ttr::lng_profile_bot_privacy(),\n\t\trpl::single(true),\n\t\topenPrivacyPolicy,\n\t\t&st::infoIconPrivacyPolicy);\n}\n\nvoid ActionsFiller::addReportAction() {\n\tconst auto peer = _peer;\n\tconst auto controller = _controller->parentController();\n\tconst auto report = [=] {\n\t\tShowReportMessageBox(controller->uiShow(), peer, {}, {});\n\t};\n\tAddActionButton(\n\t\t_wrap,\n\t\ttr::lng_profile_report(),\n\t\trpl::single(true),\n\t\treport,\n\t\t&st::infoIconReport,\n\t\tst::infoBlockButton);\n}\n\nvoid ActionsFiller::addBlockAction(not_null<UserData*> user) {\n\tconst auto controller = _controller->parentController();\n\tconst auto window = &controller->window();\n\n\tauto text = user->session().changes().peerFlagsValue(\n\t\tuser,\n\t\tData::PeerUpdate::Flag::IsBlocked\n\t) | rpl::map([=] {\n\t\tswitch (user->blockStatus()) {\n\t\tcase UserData::BlockStatus::Blocked:\n\t\t\treturn ((user->isBot() && !user->isSupport())\n\t\t\t\t? tr::lng_profile_restart_bot\n\t\t\t\t: tr::lng_profile_unblock_user)();\n\t\tcase UserData::BlockStatus::NotBlocked:\n\t\tdefault:\n\t\t\treturn ((user->isBot() && !user->isSupport())\n\t\t\t\t? tr::lng_profile_block_bot\n\t\t\t\t: tr::lng_profile_block_user)();\n\t\t}\n\t}) | rpl::flatten_latest(\n\t) | rpl::start_spawning(_wrap->lifetime());\n\n\tauto toggleOn = rpl::duplicate(\n\t\ttext\n\t) | rpl::map([](const QString &text) {\n\t\treturn !text.isEmpty();\n\t});\n\tauto callback = [=] {\n\t\tif (user->isBlocked()) {\n\t\t\tconst auto show = controller->uiShow();\n\t\t\tWindow::PeerMenuUnblockUserWithBotRestart(show, user);\n\t\t\tif (user->isBot()) {\n\t\t\t\tcontroller->showPeerHistory(user);\n\t\t\t}\n\t\t} else if (user->isBot()) {\n\t\t\tuser->session().api().blockedPeers().block(user);\n\t\t} else {\n\t\t\twindow->show(Box(\n\t\t\t\tWindow::PeerMenuBlockUserBox,\n\t\t\t\twindow,\n\t\t\t\tuser,\n\t\t\t\tv::null,\n\t\t\t\tv::null));\n\t\t}\n\t};\n\tAddActionButton(\n\t\t_wrap,\n\t\trpl::duplicate(text),\n\t\tstd::move(toggleOn),\n\t\tstd::move(callback),\n\t\t&st::infoIconBlock,\n\t\tst::infoBlockButton);\n}\n\nvoid ActionsFiller::addLeaveChannelAction(not_null<ChannelData*> channel) {\n\tExpects(_controller->parentController());\n\n\tAddActionButton(\n\t\t_wrap,\n\t\ttr::lng_profile_leave_channel(),\n\t\tAmInChannelValue(channel),\n\t\tWindow::DeleteAndLeaveHandler(\n\t\t\t_controller->parentController(),\n\t\t\tchannel),\n\t\t&st::infoIconLeave);\n}\n\nvoid ActionsFiller::addJoinChannelAction(\n\t\tnot_null<ChannelData*> channel) {\n\tusing namespace rpl::mappers;\n\tauto joinVisible = AmInChannelValue(channel)\n\t\t| rpl::map(!_1)\n\t\t| rpl::start_spawning(_wrap->lifetime());\n\tAddActionButton(\n\t\t_wrap,\n\t\ttr::lng_profile_join_channel(),\n\t\trpl::duplicate(joinVisible),\n\t\t[=] { channel->session().api().joinChannel(channel); },\n\t\t&st::infoIconAddMember);\n\t_wrap->add(object_ptr<Ui::SlideWrap<Ui::FixedHeightWidget>>(\n\t\t_wrap,\n\t\tCreateSkipWidget(\n\t\t\t_wrap,\n\t\t\tst::infoBlockButtonSkip))\n\t)->setDuration(\n\t\tst::infoSlideDuration\n\t)->toggleOn(\n\t\trpl::duplicate(joinVisible)\n\t);\n}\n\nvoid ActionsFiller::fillUserActions(not_null<UserData*> user) {\n\tif (user->isBot()) {\n\t\taddAffiliateProgram(user);\n\t\taddBalanceActions(user);\n\t\taddInviteToGroupAction(user);\n\t}\n\taddShareContactAction(user);\n\tif (!user->isSelf()) {\n\t\taddEditContactAction(user);\n\t\taddDeleteContactAction(user);\n\t}\n\tif (!user->isSelf() && !user->isSupport() && !user->isVerifyCodes()) {\n\t\tif (user->isBot()) {\n\t\t\taddBotCommandActions(user);\n\t\t}\n\t\t_wrap->add(CreateSkipWidget(\n\t\t\t_wrap,\n\t\t\tst::infoBlockButtonSkip));\n\t\tif (user->isBot()) {\n\t\t\taddReportAction();\n\t\t}\n\t\taddBlockAction(user);\n\t}\n}\n\nvoid ActionsFiller::fillChannelActions(\n\t\tnot_null<ChannelData*> channel) {\n\tusing namespace rpl::mappers;\n\n\taddJoinChannelAction(channel);\n\taddLeaveChannelAction(channel);\n\tif (!channel->amCreator()) {\n\t\taddReportAction();\n\t}\n}\n\nobject_ptr<Ui::RpWidget> ActionsFiller::fill() {\n\tauto wrapResult = [=](auto &&callback) {\n\t\t_wrap = object_ptr<Ui::VerticalLayout>(_parent);\n\t\t_wrap->add(CreateSkipWidget(_wrap));\n\t\tcallback();\n\t\t_wrap->add(CreateSkipWidget(_wrap));\n\t\treturn std::move(_wrap);\n\t};\n\tif (auto user = _peer->asUser()) {\n\t\treturn wrapResult([=] {\n\t\t\tfillUserActions(user);\n\t\t});\n\t} else if (auto channel = _peer->asChannel()) {\n\t\tif (channel->isMegagroup()) {\n\t\t\treturn { nullptr };\n\t\t}\n\t\treturn wrapResult([=] {\n\t\t\tfillChannelActions(channel);\n\t\t});\n\t}\n\treturn { nullptr };\n}\n\n} // namespace\n\nconst char kOptionShowPeerIdBelowAbout[] = \"show-peer-id-below-about\";\nconst char kOptionShowChannelJoinedBelowAbout[] = \"show-channel-joined-below-about\";\n\nobject_ptr<Ui::RpWidget> SetupDetails(\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<PeerData*> peer,\n\t\tOrigin origin,\n\t\tUi::MultiSlideTracker &mainTracker,\n\t\trpl::variable<bool> &dividerOverridden) {\n\tDetailsFiller filler(\n\t\tcontroller,\n\t\tparent,\n\t\tpeer,\n\t\torigin,\n\t\tmainTracker,\n\t\tdividerOverridden);\n\treturn filler.fill();\n}\n\nobject_ptr<Ui::RpWidget> SetupDetails(\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Data::SavedSublist*> sublist,\n\t\tUi::MultiSlideTracker &mainTracker,\n\t\trpl::variable<bool> &dividerOverridden) {\n\tDetailsFiller filler(\n\t\tcontroller,\n\t\tparent,\n\t\tsublist,\n\t\tmainTracker,\n\t\tdividerOverridden);\n\treturn filler.fill();\n}\n\nobject_ptr<Ui::RpWidget> SetupDetails(\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Data::ForumTopic*> topic,\n\t\tUi::MultiSlideTracker &mainTracker,\n\t\trpl::variable<bool> &dividerOverridden) {\n\tDetailsFiller filler(\n\t\tcontroller,\n\t\tparent,\n\t\ttopic,\n\t\tmainTracker,\n\t\tdividerOverridden);\n\treturn filler.fill();\n}\n\nobject_ptr<Ui::RpWidget> SetupActions(\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<PeerData*> peer) {\n\tActionsFiller filler(controller, parent, peer);\n\treturn filler.fill();\n}\n\nvoid SetupAddChannelMember(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<ChannelData*> channel) {\n\tauto add = Ui::CreateChild<Ui::IconButton>(\n\t\tparent.get(),\n\t\tst::infoMembersAddMember);\n\tadd->setAccessibleName(tr::lng_channel_add_members(tr::now));\n\tadd->showOn(CanAddMemberValue(channel));\n\tadd->addClickHandler([=] {\n\t\tWindow::PeerMenuAddChannelMembers(navigation, channel);\n\t});\n\tparent->widthValue(\n\t) | rpl::on_next([add](int newWidth) {\n\t\tauto availableWidth = newWidth\n\t\t\t- st::infoMembersButtonPosition.x();\n\t\tadd->moveToLeft(\n\t\t\tavailableWidth - add->width(),\n\t\t\tst::infoMembersButtonPosition.y(),\n\t\t\tnewWidth);\n\t}, add->lifetime());\n}\n\nobject_ptr<Ui::RpWidget> SetupChannelMembersAndManage(\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<PeerData*> peer) {\n\tusing namespace rpl::mappers;\n\n\tauto channel = peer->asChannel();\n\tif (!channel || channel->isMegagroup()) {\n\t\treturn { nullptr };\n\t}\n\n\tauto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\tparent,\n\t\tobject_ptr<Ui::VerticalLayout>(parent));\n\tresult->entity()->add(object_ptr<Ui::BoxContentDivider>(result));\n\tresult->entity()->add(CreateSkipWidget(result));\n\n\tauto membersShown = rpl::combine(\n\t\tMembersCountValue(channel),\n\t\tData::PeerFlagValue(\n\t\t\tchannel,\n\t\t\tChannelDataFlag::CanViewParticipants),\n\t\t\t(_1 > 0) && _2);\n\tauto membersText = tr::lng_chat_status_subscribers(\n\t\tlt_count_decimal,\n\t\tMembersCountValue(channel) | tr::to_count());\n\tauto membersCallback = [=] {\n\t\tcontroller->showSection(std::make_shared<Info::Memento>(\n\t\t\tchannel,\n\t\t\tSection::Type::Members));\n\t};\n\n\tconst auto membersWrap = result->entity()->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tresult->entity(),\n\t\t\tobject_ptr<Ui::VerticalLayout>(result->entity())));\n\tmembersWrap->setDuration(\n\t\tst::infoSlideDuration\n\t)->toggleOn(rpl::duplicate(membersShown));\n\n\tconst auto members = membersWrap->entity();\n\t{\n\t\tauto button = AddActionButton(\n\t\t\tmembers,\n\t\t\tstd::move(membersText),\n\t\t\trpl::single(true),\n\t\t\tstd::move(membersCallback),\n\t\t\tnullptr)->entity();\n\n\t\tSetupAddChannelMember(controller, button, channel);\n\t}\n\n\tobject_ptr<FloatingIcon>(\n\t\tmembers,\n\t\tst::infoIconMembers,\n\t\tst::infoChannelMembersIconPosition);\n\n\tauto adminsShown = peer->session().changes().peerFlagsValue(\n\t\tchannel,\n\t\tData::PeerUpdate::Flag::Rights\n\t) | rpl::map([=] { return channel->canViewAdmins(); });\n\tauto adminsText = tr::lng_profile_administrators(\n\t\tlt_count_decimal,\n\t\tInfo::Profile::MigratedOrMeValue(\n\t\t\tchannel\n\t\t) | rpl::map(\n\t\t\tInfo::Profile::AdminsCountValue\n\t\t) | rpl::flatten_latest() | tr::to_count());\n\tauto adminsCallback = [=] {\n\t\tParticipantsBoxController::Start(\n\t\t\tcontroller,\n\t\t\tchannel,\n\t\t\tParticipantsBoxController::Role::Admins);\n\t};\n\n\tconst auto adminsWrap = result->entity()->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tresult->entity(),\n\t\t\tobject_ptr<Ui::VerticalLayout>(result->entity())));\n\tadminsWrap->setDuration(\n\t\tst::infoSlideDuration\n\t)->toggleOn(rpl::duplicate(adminsShown));\n\n\tconst auto admins = adminsWrap->entity();\n\tAddActionButton(\n\t\tadmins,\n\t\tstd::move(adminsText),\n\t\trpl::single(true),\n\t\tstd::move(adminsCallback),\n\t\tnullptr);\n\n\tobject_ptr<FloatingIcon>(\n\t\tadmins,\n\t\tst::menuIconAdmin,\n\t\tst::infoChannelAdminsIconPosition);\n\n\tconst auto canViewBalance = false\n\t\t|| (channel->flags() & ChannelDataFlag::CanViewRevenue)\n\t\t|| (channel->flags() & ChannelDataFlag::CanViewCreditsRevenue)\n\t\t|| (channel->loadedStatus() != ChannelData::LoadedStatus::Full);\n\tif (canViewBalance) {\n\t\tconst auto balanceWrap = result->entity()->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tresult->entity(),\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(result->entity())));\n\t\tauto refreshed = channel->session().credits().refreshedByPeerId(\n\t\t\tchannel->id);\n\t\tauto creditsValue = rpl::single(\n\t\t\trpl::empty_value()\n\t\t) | rpl::then(rpl::duplicate(refreshed)) | rpl::map([=] {\n\t\t\treturn channel->session().credits().balance(channel->id);\n\t\t});\n\t\tauto currencyValue = rpl::single(\n\t\t\trpl::empty_value()\n\t\t) | rpl::then(rpl::duplicate(refreshed)) | rpl::map([=] {\n\t\t\treturn channel->session().credits().balanceCurrency(channel->id);\n\t\t});\n\t\tconst auto emptyAmount = CreditsAmount(0);\n\t\tbalanceWrap->toggleOn(\n\t\t\trpl::combine(\n\t\t\t\trpl::duplicate(creditsValue),\n\t\t\t\trpl::duplicate(currencyValue)\n\t\t\t) | rpl::map(rpl::mappers::_1 > emptyAmount\n\t\t\t\t|| rpl::mappers::_2 > emptyAmount),\n\t\t\tanim::type::normal);\n\t\tbalanceWrap->finishAnimating();\n\n\t\tconst auto &st = st::infoSharedMediaButton;\n\n\t\tauto customEmojiFactory = [height = st.style.font->height,\n\t\t\t\tfont = st.rightLabel.style.font,\n\t\t\t\tcolor = st.rightLabel.textFg->c](\n\t\t\tQStringView data,\n\t\t\tconst Ui::Text::MarkedContext &context\n\t\t) -> std::unique_ptr<Ui::Text::CustomEmoji> {\n\t\t\treturn (data == Ui::kCreditsCurrency)\n\t\t\t\t? Ui::MakeCreditsIconEmoji(height, 1)\n\t\t\t\t: std::make_unique<Ui::Text::ShiftedEmoji>(\n\t\t\t\t\tUi::Earn::MakeCurrencyIconEmoji(font, color),\n\t\t\t\t\tQPoint(0, st::channelEarnCurrencyCommonMargins.top()));\n\t\t};\n\t\tconst auto context = Ui::Text::MarkedContext{\n\t\t\t.customEmojiFactory = std::move(customEmojiFactory),\n\t\t};\n\n\t\tconst auto balance = balanceWrap->entity();\n\t\tconst auto button = AddActionButton(\n\t\t\tbalance,\n\t\t\ttr::lng_manage_peer_bot_balance(),\n\t\t\trpl::single(true),\n\t\t\t[=] { controller->showSection(Info::ChannelEarn::Make(peer)); },\n\t\t\tnullptr);\n\n\t\t::Settings::CreateRightLabel(\n\t\t\tbutton->entity(),\n\t\t\trpl::combine(\n\t\t\t\tstd::move(creditsValue),\n\t\t\t\tstd::move(currencyValue)\n\t\t\t) | rpl::map([](CreditsAmount credits, CreditsAmount currency) {\n\t\t\t\tauto creditsText = (credits > CreditsAmount(0))\n\t\t\t\t\t? Ui::MakeCreditsIconEntity()\n\t\t\t\t\t\t.append(QChar(' '))\n\t\t\t\t\t\t.append(Info::ChannelEarn::MajorPart(credits))\n\t\t\t\t\t\t.append(credits.nano()\n\t\t\t\t\t\t\t? Info::ChannelEarn::MinorPart(credits)\n\t\t\t\t\t\t\t: QString())\n\t\t\t\t\t: TextWithEntities();\n\t\t\t\tauto currencyText = (currency > CreditsAmount(0))\n\t\t\t\t\t? Ui::Text::SingleCustomEmoji(\"_\")\n\t\t\t\t\t\t.append(QChar(' '))\n\t\t\t\t\t\t.append(Info::ChannelEarn::MajorPart(currency))\n\t\t\t\t\t\t.append(Info::ChannelEarn::MinorPart(currency))\n\t\t\t\t\t: TextWithEntities();\n\t\t\t\treturn currencyText\n\t\t\t\t\t.append(QChar(' '))\n\t\t\t\t\t.append(std::move(creditsText));\n\t\t\t}),\n\t\t\tst,\n\t\t\ttr::lng_manage_peer_bot_balance(),\n\t\t\tcontext);\n\n\t\tobject_ptr<FloatingIcon>(\n\t\t\tbalance,\n\t\t\tst::menuIconEarn,\n\t\t\tst::infoChannelAdminsIconPosition);\n\t}\n\n\tresult->setDuration(st::infoSlideDuration)->toggleOn(\n\t\trpl::combine(\n\t\t\tstd::move(membersShown),\n\t\t\tstd::move(adminsShown)\n\t\t) | rpl::map(rpl::mappers::_1 || rpl::mappers::_2));\n\n\tresult->entity()->add(CreateSkipWidget(result));\n\n\treturn result;\n}\n\nvoid AddDetails(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tData::ForumTopic *topic,\n\t\tData::SavedSublist *sublist,\n\t\tOrigin origin,\n\t\tUi::MultiSlideTracker &mainTracker,\n\t\trpl::variable<bool> &dividerOverridden) {\n\tif (topic) {\n\t\tcontainer->add(\n\t\t\tSetupDetails(\n\t\t\t\tcontroller,\n\t\t\t\tcontainer,\n\t\t\t\ttopic,\n\t\t\t\tmainTracker,\n\t\t\t\tdividerOverridden));\n\t} else if (sublist) {\n\t\tcontainer->add(\n\t\t\tSetupDetails(\n\t\t\t\tcontroller,\n\t\t\t\tcontainer,\n\t\t\t\tsublist,\n\t\t\t\tmainTracker,\n\t\t\t\tdividerOverridden));\n\t} else {\n\t\tcontainer->add(\n\t\t\tSetupDetails(\n\t\t\t\tcontroller,\n\t\t\t\tcontainer,\n\t\t\t\tpeer,\n\t\t\t\torigin,\n\t\t\t\tmainTracker,\n\t\t\t\tdividerOverridden));\n\t}\n}\n\n} // namespace Profile\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_actions.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\nclass RpWidget;\nclass VerticalLayout;\nclass MultiSlideTracker;\n} // namespace Ui\n\nnamespace Data {\nclass ForumTopic;\nclass SavedSublist;\n} // namespace Data\n\nnamespace Info {\nclass Controller;\n} // namespace Info\n\nnamespace Info::Profile {\n\nextern const char kOptionShowPeerIdBelowAbout[];\nextern const char kOptionShowChannelJoinedBelowAbout[];\n\nstruct Origin;\n\nobject_ptr<Ui::RpWidget> SetupDetails(\n\tnot_null<Controller*> controller,\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<PeerData*> peer,\n\tOrigin origin,\n\tUi::MultiSlideTracker &mainTracker);\n\nobject_ptr<Ui::RpWidget> SetupDetails(\n\tnot_null<Controller*> controller,\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Data::ForumTopic*> topic,\n\tUi::MultiSlideTracker &mainTracker);\n\nobject_ptr<Ui::RpWidget> SetupDetails(\n\tnot_null<Controller*> controller,\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Data::SavedSublist*> sublist,\n\tUi::MultiSlideTracker &mainTracker);\n\nobject_ptr<Ui::RpWidget> SetupActions(\n\tnot_null<Controller*> controller,\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<PeerData*> peer);\n\nobject_ptr<Ui::RpWidget> SetupChannelMembersAndManage(\n\tnot_null<Controller*> controller,\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<PeerData*> peer);\n\nvoid AddDetails(\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<Controller*> controller,\n\tnot_null<PeerData*> peer,\n\tData::ForumTopic *topic,\n\tData::SavedSublist *sublist,\n\tOrigin origin,\n\tUi::MultiSlideTracker &mainTracker,\n\trpl::variable<bool> &dividerOverridden);\n\n} // namespace Info::Profile\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_badge.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_badge.h\"\n\n#include \"data/data_changes.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"info/profile/info_profile_emoji_status_panel.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"main/main_session.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info::Profile {\nnamespace {\n\n[[nodiscard]] bool HasPremiumClick(const Badge::Content &content) {\n\treturn content.badge == BadgeType::Premium\n\t\t|| (content.badge == BadgeType::Verified && content.emojiStatusId);\n}\n\n} // namespace\n\nBadge::Badge(\n\tnot_null<QWidget*> parent,\n\tconst style::InfoPeerBadge &st,\n\tnot_null<Main::Session*> session,\n\trpl::producer<Content> content,\n\tEmojiStatusPanel *emojiStatusPanel,\n\tFn<bool()> animationPaused,\n\tint customStatusLoopsLimit,\n\tbase::flags<BadgeType> allowed)\n: _parent(parent)\n, _st(st)\n, _session(session)\n, _emojiStatusPanel(emojiStatusPanel)\n, _customStatusLoopsLimit(customStatusLoopsLimit)\n, _allowed(allowed)\n, _animationPaused(std::move(animationPaused)) {\n\tstd::move(\n\t\tcontent\n\t) | rpl::on_next([=](Content content) {\n\t\tsetContent(content);\n\t}, _lifetime);\n}\n\nBadge::~Badge() = default;\n\nUi::RpWidget *Badge::widget() const {\n\treturn _view.data();\n}\n\nvoid Badge::setContent(Content content) {\n\tif (!(_allowed & content.badge)\n\t\t|| (!_session->premiumBadgesShown()\n\t\t\t&& content.badge == BadgeType::Premium)) {\n\t\tcontent.badge = BadgeType::None;\n\t}\n\tif (!(_allowed & content.badge)) {\n\t\tcontent.badge = BadgeType::None;\n\t}\n\tif (_content == content) {\n\t\treturn;\n\t}\n\t_content = content;\n\t_emojiStatus = nullptr;\n\t_view.destroy();\n\tif (_content.badge == BadgeType::None) {\n\t\t_updated.fire({});\n\t\treturn;\n\t}\n\t_view.create(_parent);\n\t_view->setAccessibleName([&] {\n\t\tswitch (_content.badge) {\n\t\tcase BadgeType::Verified:\n\t\t\treturn tr::lng_sr_verified_badge(tr::now);\n\t\tcase BadgeType::BotVerified:\n\t\t\treturn tr::lng_sr_bot_verified_badge(tr::now);\n\t\tcase BadgeType::Premium:\n\t\t\tif (_content.emojiStatusId) {\n\t\t\t\treturn tr::lng_profile_bot_emoji_status_access(tr::now);\n\t\t\t}\n\t\t\treturn tr::lng_premium_summary_title(tr::now);\n\t\tcase BadgeType::Scam:\n\t\t\treturn tr::lng_scam_badge(tr::now);\n\t\tcase BadgeType::Fake:\n\t\t\treturn tr::lng_fake_badge(tr::now);\n\t\tcase BadgeType::Direct:\n\t\t\treturn tr::lng_direct_badge(tr::now);\n\t\t}\n\t\tUnexpected(\"badge type\");\n\t}());\n\t_view->show();\n\tswitch (_content.badge) {\n\tcase BadgeType::Verified:\n\tcase BadgeType::BotVerified:\n\tcase BadgeType::Premium: {\n\t\tconst auto id = _content.emojiStatusId;\n\t\tconst auto emoji = id\n\t\t\t? (Data::FrameSizeFromTag(sizeTag())\n\t\t\t\t/ style::DevicePixelRatio())\n\t\t\t: 0;\n\t\tconst auto &style = st();\n\t\tconst auto icon = (_content.badge == BadgeType::Verified)\n\t\t\t? &style.verified\n\t\t\t: id\n\t\t\t? nullptr\n\t\t\t: &style.premium;\n\t\tconst auto iconForeground = (_content.badge == BadgeType::Verified)\n\t\t\t? &style.verifiedCheck\n\t\t\t: nullptr;\n\t\tif (id) {\n\t\t\t_emojiStatus = _session->data().customEmojiManager().create(\n\t\t\t\tData::EmojiStatusCustomId(id),\n\t\t\t\t[raw = _view.data()] { raw->update(); },\n\t\t\t\tsizeTag());\n\t\t\tif (_content.badge == BadgeType::BotVerified) {\n\t\t\t\t_emojiStatus = std::make_unique<Ui::Text::FirstFrameEmoji>(\n\t\t\t\t\tstd::move(_emojiStatus));\n\t\t\t} else if (_customStatusLoopsLimit > 0) {\n\t\t\t\t_emojiStatus = std::make_unique<Ui::Text::LimitedLoopsEmoji>(\n\t\t\t\t\tstd::move(_emojiStatus),\n\t\t\t\t\t_customStatusLoopsLimit);\n\t\t\t}\n\t\t}\n\t\tconst auto width = emoji + (icon ? icon->width() : 0);\n\t\tconst auto height = std::max(emoji, icon ? icon->height() : 0);\n\t\t_view->resize(width, height);\n\t\t_view->paintRequest(\n\t\t) | rpl::on_next([=, check = _view.data()]{\n\t\t\tif (_emojiStatus) {\n\t\t\t\tauto args = Ui::Text::CustomEmoji::Context{\n\t\t\t\t\t.textColor = style.premiumFg->c,\n\t\t\t\t\t.now = crl::now(),\n\t\t\t\t\t.paused = ((_animationPaused && _animationPaused())\n\t\t\t\t\t\t|| On(PowerSaving::kEmojiStatus)),\n\t\t\t\t};\n\t\t\t\tif (!_emojiStatusPanel\n\t\t\t\t\t|| !_emojiStatusPanel->paintBadgeFrame(check)) {\n\t\t\t\t\tPainter p(check);\n\t\t\t\t\t_emojiStatus->paint(p, args);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (icon) {\n\t\t\t\tauto p = Painter(check);\n\t\t\t\tif (_overrideSt && !iconForeground) {\n\t\t\t\t\ticon->paint(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\temoji,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tcheck->width(),\n\t\t\t\t\t\t_overrideSt->premiumFg->c);\n\t\t\t\t} else {\n\t\t\t\t\ticon->paint(p, emoji, 0, check->width());\n\t\t\t\t}\n\t\t\t\tif (iconForeground) {\n\t\t\t\t\tif (_overrideSt) {\n\t\t\t\t\t\ticonForeground->paint(\n\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\temoji,\n\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\tcheck->width(),\n\t\t\t\t\t\t\t_overrideSt->premiumFg->c);\n\t\t\t\t\t} else {\n\t\t\t\t\t\ticonForeground->paint(p, emoji, 0, check->width());\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}, _view->lifetime());\n\t} break;\n\tcase BadgeType::Scam:\n\tcase BadgeType::Fake:\n\tcase BadgeType::Direct: {\n\t\tconst auto type = (_content.badge == BadgeType::Direct)\n\t\t\t? Ui::TextBadgeType::Direct\n\t\t\t: (_content.badge == BadgeType::Fake)\n\t\t\t? Ui::TextBadgeType::Fake\n\t\t\t: Ui::TextBadgeType::Scam;\n\t\tconst auto size = Ui::TextBadgeSize(type);\n\t\tconst auto skip = st::infoVerifiedCheckPosition.x();\n\t\t_view->resize(\n\t\t\tsize.width() + 2 * skip,\n\t\t\tsize.height() + 2 * skip);\n\t\t_view->paintRequest(\n\t\t) | rpl::on_next([=, badge = _view.data()]{\n\t\t\tPainter p(badge);\n\t\t\tUi::DrawTextBadge(\n\t\t\t\ttype,\n\t\t\t\tp,\n\t\t\t\tbadge->rect().marginsRemoved({ skip, skip, skip, skip }),\n\t\t\t\tbadge->width(),\n\t\t\t\t_overrideSt\n\t\t\t\t\t? _overrideSt->premiumFg\n\t\t\t\t\t: (type == Ui::TextBadgeType::Direct\n\t\t\t\t\t\t? st::windowSubTextFg\n\t\t\t\t\t\t: st::attentionButtonFg));\n\t\t\t}, _view->lifetime());\n\t} break;\n\t}\n\n\tif (!HasPremiumClick(_content) || !_premiumClickCallback) {\n\t\t_view->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t} else {\n\t\t_view->setClickedCallback(_premiumClickCallback);\n\t}\n\n\t_updated.fire({});\n}\n\nvoid Badge::setPremiumClickCallback(Fn<void()> callback) {\n\t_premiumClickCallback = std::move(callback);\n\tif (_view && HasPremiumClick(_content)) {\n\t\tif (!_premiumClickCallback) {\n\t\t\t_view->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t} else {\n\t\t\t_view->setAttribute(Qt::WA_TransparentForMouseEvents, false);\n\t\t\t_view->setClickedCallback(_premiumClickCallback);\n\t\t}\n\t}\n}\n\nvoid Badge::setOverrideStyle(const style::InfoPeerBadge *st) {\n\tconst auto was = _content;\n\t_overrideSt = st;\n\t_content = {};\n\tsetContent(was);\n}\n\nrpl::producer<> Badge::updated() const {\n\treturn _updated.events();\n}\n\nvoid Badge::move(int left, int top, int bottom) {\n\tif (!_view) {\n\t\treturn;\n\t}\n\tconst auto &style = st();\n\tconst auto star = !_emojiStatus\n\t\t&& (_content.badge == BadgeType::Premium\n\t\t\t|| _content.badge == BadgeType::Verified);\n\tconst auto fake = !_emojiStatus && !star;\n\tconst auto skip = fake ? 0 : style.position.x();\n\tconst auto badgeLeft = left + skip;\n\tconst auto badgeTop = top\n\t\t+ (star\n\t\t\t? style.position.y()\n\t\t\t: (bottom - top - _view->height()) / 2);\n\t_view->moveToLeft(badgeLeft, badgeTop);\n}\n\nconst style::InfoPeerBadge &Badge::st() const {\n\treturn _overrideSt ? *_overrideSt : _st;\n}\n\nData::CustomEmojiSizeTag Badge::sizeTag() const {\n\tusing SizeTag = Data::CustomEmojiSizeTag;\n\tconst auto &style = st();\n\treturn (style.sizeTag == 2)\n\t\t? SizeTag::Isolated\n\t\t: (style.sizeTag == 1)\n\t\t? SizeTag::Large\n\t\t: SizeTag::Normal;\n}\n\nrpl::producer<Badge::Content> BadgeContentForPeer(not_null<PeerData*> peer) {\n\tconst auto statusOnlyForPremium = peer->isUser();\n\treturn rpl::combine(\n\t\tBadgeValue(peer),\n\t\tEmojiStatusIdValue(peer)\n\t) | rpl::map([=](BadgeType badge, EmojiStatusId emojiStatusId) {\n\t\tif (emojiStatusId.collectible && (badge == BadgeType::Verified)) {\n\t\t\treturn Badge::Content{ BadgeType::Premium, emojiStatusId };\n\t\t}\n\t\tif (badge == BadgeType::Verified) {\n\t\t\tbadge = BadgeType::None;\n\t\t}\n\t\tif (statusOnlyForPremium && badge != BadgeType::Premium) {\n\t\t\temojiStatusId = EmojiStatusId();\n\t\t} else if (emojiStatusId && badge == BadgeType::None) {\n\t\t\tbadge = BadgeType::Premium;\n\t\t}\n\t\treturn Badge::Content{ badge, emojiStatusId };\n\t});\n}\n\nrpl::producer<Badge::Content> VerifiedContentForPeer(\n\t\tnot_null<PeerData*> peer) {\n\treturn BadgeValue(peer) | rpl::map([=](BadgeType badge) {\n\t\tif (badge != BadgeType::Verified) {\n\t\t\tbadge = BadgeType::None;\n\t\t}\n\t\treturn Badge::Content{ badge };\n\t});\n}\n\nrpl::producer<Badge::Content> BotVerifyBadgeForPeer(\n\t\tnot_null<PeerData*> peer) {\n\treturn peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::VerifyInfo\n\t) | rpl::map([=] {\n\t\tconst auto info = peer->botVerifyDetails();\n\t\treturn Badge::Content{\n\t\t\t.badge = info ? BadgeType::BotVerified : BadgeType::None,\n\t\t\t.emojiStatusId = { info ? info->iconId : DocumentId() },\n\t\t};\n\t});\n}\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_badge.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n#include \"base/object_ptr.h\"\n\nnamespace style {\nstruct InfoPeerBadge;\n} // namespace style\n\nnamespace Data {\nenum class CustomEmojiSizeTag : uchar;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass RpWidget;\nclass AbstractButton;\n} // namespace Ui\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nnamespace Info::Profile {\n\nclass EmojiStatusPanel;\n\nenum class BadgeType : uchar {\n\tNone = 0x00,\n\tVerified = 0x01,\n\tBotVerified = 0x02,\n\tPremium = 0x04,\n\tScam = 0x08,\n\tFake = 0x10,\n\tDirect = 0x20,\n};\ninline constexpr bool is_flag_type(BadgeType) { return true; }\n\nclass Badge final {\npublic:\n\tstruct Content {\n\t\tBadgeType badge = BadgeType::None;\n\t\tEmojiStatusId emojiStatusId;\n\n\t\tfriend inline bool operator==(Content, Content) = default;\n\t};\n\tBadge(\n\t\tnot_null<QWidget*> parent,\n\t\tconst style::InfoPeerBadge &st,\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<Content> content,\n\t\tEmojiStatusPanel *emojiStatusPanel,\n\t\tFn<bool()> animationPaused,\n\t\tint customStatusLoopsLimit = 0,\n\t\tbase::flags<BadgeType> allowed\n\t\t\t= base::flags<BadgeType>::from_raw(-1));\n\n\t~Badge();\n\n\t[[nodiscard]] Ui::RpWidget *widget() const;\n\n\tvoid setPremiumClickCallback(Fn<void()> callback);\n\tvoid setOverrideStyle(const style::InfoPeerBadge *st);\n\t[[nodiscard]] rpl::producer<> updated() const;\n\tvoid move(int left, int top, int bottom);\n\n\t[[nodiscard]] Data::CustomEmojiSizeTag sizeTag() const;\n\nprivate:\n\tvoid setContent(Content content);\n\t[[nodiscard]] const style::InfoPeerBadge &st() const;\n\n\tconst not_null<QWidget*> _parent;\n\tconst style::InfoPeerBadge &_st;\n\tconst style::InfoPeerBadge *_overrideSt = nullptr;\n\tconst not_null<Main::Session*> _session;\n\tEmojiStatusPanel *_emojiStatusPanel = nullptr;\n\tconst int _customStatusLoopsLimit = 0;\n\tstd::unique_ptr<Ui::Text::CustomEmoji> _emojiStatus;\n\tbase::flags<BadgeType> _allowed;\n\tContent _content;\n\tFn<void()> _premiumClickCallback;\n\tFn<bool()> _animationPaused;\n\tobject_ptr<Ui::AbstractButton> _view = { nullptr };\n\trpl::event_stream<> _updated;\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] rpl::producer<Badge::Content> BadgeContentForPeer(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<Badge::Content> VerifiedContentForPeer(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<Badge::Content> BotVerifyBadgeForPeer(\n\tnot_null<PeerData*> peer);\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_badge_tooltip.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_badge_tooltip.h\"\n\n#include \"data/data_emoji_statuses.h\"\n#include \"base/event_filter.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info::Profile {\nnamespace {\n\nconstexpr auto kGlareDurationStep = crl::time(320);\nconstexpr auto kGlareTimeout = crl::time(1000);\n\n} // namespace\n\nBadgeTooltip::BadgeTooltip(\n\tnot_null<QWidget*> parent,\n\tstd::shared_ptr<Data::EmojiStatusCollectible> collectible,\n\tnot_null<QWidget*> pointTo)\n: Ui::RpWidget(parent)\n, _st(st::infoGiftTooltip)\n, _collectible(std::move(collectible))\n, _text(_collectible->title)\n, _font(st::infoGiftTooltipFont)\n, _inner(_font->width(_text), _font->height)\n, _outer(_inner.grownBy(_st.padding))\n, _stroke(st::lineWidth)\n, _skip(2 * _stroke)\n, _full(_outer + QSize(2 * _skip, _st.arrow + 2 * _skip))\n, _glareSize(_outer.height() * 3)\n, _glareRange(_outer.width() + _glareSize)\n, _glareDuration(_glareRange * kGlareDurationStep / _glareSize)\n, _glareTimer([=] { showGlare(); }) {\n\tresize(_full + QSize(0, _st.shift));\n\tsetupGeometry(pointTo);\n}\n\nvoid BadgeTooltip::fade(bool shown) {\n\tif (_shown == shown) {\n\t\treturn;\n\t}\n\tshow();\n\t_shown = shown;\n\t_showAnimation.start([=] {\n\t\tupdate();\n\t\tif (!_showAnimation.animating()) {\n\t\t\tif (!_shown) {\n\t\t\t\thide();\n\t\t\t} else {\n\t\t\t\tshowGlare();\n\t\t\t}\n\t\t}\n\t}, _shown ? 0. : 1., _shown ? 1. : 0., _st.duration, anim::easeInCirc);\n}\n\nvoid BadgeTooltip::showGlare() {\n\t_glareAnimation.start([=] {\n\t\tupdate();\n\t\tif (!_glareAnimation.animating()) {\n\t\t\t_glareTimer.callOnce(kGlareTimeout);\n\t\t}\n\t}, 0., 1., _glareDuration);\n}\n\nvoid BadgeTooltip::finishAnimating() {\n\t_showAnimation.stop();\n\tif (!_shown) {\n\t\thide();\n\t}\n}\n\nvoid BadgeTooltip::setOpacity(float64 opacity) {\n\t_opacity = opacity;\n\tupdate();\n}\n\ncrl::time BadgeTooltip::glarePeriod() const {\n\treturn _glareDuration + kGlareTimeout;\n}\n\nvoid BadgeTooltip::paintEvent(QPaintEvent *e) {\n\tconst auto glare = _glareAnimation.value(0.);\n\t_glareRight = anim::interpolate(0, _glareRange, glare);\n\tprepareImage();\n\n\tauto p = QPainter(this);\n\tconst auto shown = _showAnimation.value(_shown ? 1. : 0.);\n\tp.setOpacity(shown * _opacity);\n\tconst auto imageHeight = _image.height() / _image.devicePixelRatio();\n\tconst auto top = anim::interpolate(0, height() - imageHeight, shown);\n\tp.drawImage(0, top, _image);\n}\n\nvoid BadgeTooltip::setupGeometry(not_null<QWidget*> pointTo) {\n\tauto widget = pointTo.get();\n\tconst auto parent = parentWidget();\n\n\tconst auto refresh = [=, weak = base::make_weak(pointTo)] {\n\t\tconst auto strong = weak.get();\n\t\tif (!strong) {\n\t\t\thide();\n\t\t\treturn setGeometry({});\n\t\t}\n\t\tconst auto rect = Ui::MapFrom(parent, pointTo, pointTo->rect());\n\t\tconst auto point = QPoint(rect.center().x(), rect.y());\n\t\tconst auto left = point.x() - (width() / 2);\n\t\tconst auto skip = _st.padding.left();\n\t\tsetGeometry(\n\t\t\tstd::min(std::max(left, skip), parent->width() - width() - skip),\n\t\t\tstd::max(point.y() - height() - _st.margin.bottom(), skip),\n\t\t\twidth(),\n\t\t\theight());\n\t\tconst auto arrowMiddle = point.x() - x();\n\t\tif (_arrowMiddle != arrowMiddle) {\n\t\t\t_arrowMiddle = arrowMiddle;\n\t\t\tupdate();\n\t\t}\n\t};\n\trefresh();\n\twhile (widget && widget != parent) {\n\t\tbase::install_event_filter(this, widget, [=](not_null<QEvent*> e) {\n\t\t\tif (e->type() == QEvent::Resize\n\t\t\t\t|| e->type() == QEvent::Move\n\t\t\t\t|| e->type() == QEvent::ZOrderChange) {\n\t\t\t\trefresh();\n\t\t\t\traise();\n\t\t\t}\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t});\n\t\twidget = widget->parentWidget();\n\t}\n}\n\nvoid BadgeTooltip::prepareImage() {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto arrow = _st.arrow;\n\tconst auto size = _full * ratio;\n\tif (_image.size() != size) {\n\t\t_image = QImage(size, QImage::Format_ARGB32_Premultiplied);\n\t\t_image.setDevicePixelRatio(ratio);\n\t} else if (_imageGlareRight == _glareRight\n\t\t&& _imageArrowMiddle == _arrowMiddle) {\n\t\treturn;\n\t}\n\t_imageGlareRight = _glareRight;\n\t_imageArrowMiddle = _arrowMiddle;\n\t_image.fill(Qt::transparent);\n\n\tconst auto gfrom = _imageGlareRight - _glareSize;\n\tconst auto gtill = _imageGlareRight;\n\n\tauto path = QPainterPath();\n\tconst auto width = _outer.width();\n\tconst auto height = _outer.height();\n\tconst auto radius = (height + 1) / 2;\n\tconst auto diameter = height;\n\tpath.moveTo(radius, 0);\n\tpath.lineTo(width - radius, 0);\n\tpath.arcTo(\n\t\tQRect(QPoint(width - diameter, 0), QSize(diameter, diameter)),\n\t\t90,\n\t\t-180);\n\tconst auto xarrow = _arrowMiddle - _skip;\n\tif (xarrow - arrow <= radius || xarrow + arrow >= width - radius) {\n\t\tpath.lineTo(radius, height);\n\t} else {\n\t\tpath.lineTo(xarrow + arrow, height);\n\t\tpath.lineTo(xarrow, height + arrow);\n\t\tpath.lineTo(xarrow - arrow, height);\n\t\tpath.lineTo(radius, height);\n\t}\n\tpath.arcTo(\n\t\tQRect(QPoint(0, 0), QSize(diameter, diameter)),\n\t\t-90,\n\t\t-180);\n\tpath.closeSubpath();\n\n\tauto p = QPainter(&_image);\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setPen(Qt::NoPen);\n\tif (gtill > 0) {\n\t\tauto gradient = QLinearGradient(gfrom, 0, gtill, 0);\n\t\tgradient.setStops({\n\t\t\t{ 0., _collectible->edgeColor },\n\t\t\t{ 0.5, _collectible->centerColor },\n\t\t\t{ 1., _collectible->edgeColor },\n\t\t});\n\t\tp.setBrush(gradient);\n\t} else {\n\t\tp.setBrush(_collectible->edgeColor);\n\t}\n\tp.translate(_skip, _skip);\n\tp.drawPath(path);\n\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\tp.setBrush(Qt::NoBrush);\n\tauto copy = _collectible->textColor;\n\tcopy.setAlpha(0);\n\tif (gtill > 0) {\n\t\tauto gradient = QLinearGradient(gfrom, 0, gtill, 0);\n\t\tgradient.setStops({\n\t\t\t{ 0., copy },\n\t\t\t{ 0.5, _collectible->textColor },\n\t\t\t{ 1., copy },\n\t\t});\n\t\tp.setPen(QPen(gradient, _stroke));\n\t} else {\n\t\tp.setPen(QPen(copy, _stroke));\n\t}\n\tp.drawPath(path);\n\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\tp.setFont(_font);\n\tp.setPen(QColor(255, 255, 255));\n\tp.drawText(_st.padding.left(), _st.padding.top() + _font->ascent, _text);\n}\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_badge_tooltip.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n#include \"base/timer.h\"\n\nnamespace style {\nstruct ImportantTooltip;\n} // namespace style\n\nnamespace Data {\nstruct EmojiStatusCollectible;\n} // namespace Data\n\nnamespace Info::Profile {\n\nclass BadgeTooltip final : public Ui::RpWidget {\npublic:\n\tBadgeTooltip(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::shared_ptr<Data::EmojiStatusCollectible> collectible,\n\t\tnot_null<QWidget*> pointTo);\n\n\tvoid fade(bool shown);\n\tvoid finishAnimating();\n\tvoid setOpacity(float64 opacity);\n\n\t[[nodiscard]] crl::time glarePeriod() const;\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid setupGeometry(not_null<QWidget*> pointTo);\n\tvoid prepareImage();\n\tvoid showGlare();\n\n\tconst style::ImportantTooltip &_st;\n\tstd::shared_ptr<Data::EmojiStatusCollectible> _collectible;\n\tQString _text;\n\tconst style::font &_font;\n\tQSize _inner;\n\tQSize _outer;\n\tint _stroke = 0;\n\tint _skip = 0;\n\tQSize _full;\n\tint _glareSize = 0;\n\tint _glareRange = 0;\n\tcrl::time _glareDuration = 0;\n\tbase::Timer _glareTimer;\n\n\tUi::Animations::Simple _showAnimation;\n\tUi::Animations::Simple _glareAnimation;\n\n\tQImage _image;\n\tint _glareRight = 0;\n\tint _imageGlareRight = 0;\n\tint _arrowMiddle = 0;\n\tint _imageArrowMiddle = 0;\n\n\tbool _shown = false;\n\tfloat64 _opacity = 1.;\n};\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_cover.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_cover.h\"\n\n#include \"main/main_session.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"history/view/media/history_view_sticker_player.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"window/window_session_controller.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Info::Profile {\n\nQMargins LargeCustomEmojiMargins() {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto emoji = Ui::Emoji::GetSizeLarge() / ratio;\n\tconst auto size = Data::FrameSizeFromTag(Data::CustomEmojiSizeTag::Large)\n\t\t/ ratio;\n\tconst auto left = (size - emoji) / 2;\n\tconst auto right = size - emoji - left;\n\treturn { left, left, right, right };\n}\n\nTopicIconView::TopicIconView(\n\tnot_null<Data::ForumTopic*> topic,\n\tFn<bool()> paused,\n\tFn<void()> update)\n: TopicIconView(\n\ttopic,\n\tstd::move(paused),\n\tstd::move(update),\n\tst::windowSubTextFg) {\n}\n\nTopicIconView::TopicIconView(\n\tnot_null<Data::ForumTopic*> topic,\n\tFn<bool()> paused,\n\tFn<void()> update,\n\tconst style::color &generalIconFg)\n: _topic(topic)\n, _generalIconFg(generalIconFg)\n, _paused(std::move(paused))\n, _update(std::move(update)) {\n\tsetup(topic);\n}\n\nvoid TopicIconView::paintInRect(QPainter &p, QRect rect, QColor textColor) {\n\tconst auto paint = [&](const QImage &image) {\n\t\tconst auto size = image.size() / style::DevicePixelRatio();\n\t\tp.drawImage(\n\t\t\tQRect(\n\t\t\t\trect.x() + (rect.width() - size.width()) / 2,\n\t\t\t\trect.y() + (rect.height() - size.height()) / 2,\n\t\t\t\tsize.width(),\n\t\t\t\tsize.height()),\n\t\t\timage);\n\t};\n\tif (_player && _player->ready()) {\n\t\tconst auto colored = (textColor.alpha() > 0)\n\t\t\t? textColor\n\t\t\t: _playerUsesTextColor\n\t\t\t? st::windowFg->c\n\t\t\t: QColor(0, 0, 0, 0);\n\t\tpaint(_player->frame(\n\t\t\tst::infoTopicCover.photo.size,\n\t\t\tcolored,\n\t\t\tfalse,\n\t\t\tcrl::now(),\n\t\t\t_paused()).image);\n\t\t_player->markFrameShown();\n\t} else if (!_topic->iconId() && !_image.isNull()) {\n\t\tpaint(_image);\n\t}\n}\n\nvoid TopicIconView::setup(not_null<Data::ForumTopic*> topic) {\n\tsetupPlayer(topic);\n\tsetupImage(topic);\n}\n\nvoid TopicIconView::setupPlayer(not_null<Data::ForumTopic*> topic) {\n\tIconIdValue(\n\t\ttopic\n\t) | rpl::map([=](DocumentId id) -> rpl::producer<DocumentData*> {\n\t\tif (!id) {\n\t\t\treturn rpl::single((DocumentData*)nullptr);\n\t\t}\n\t\treturn topic->owner().customEmojiManager().resolve(\n\t\t\tid\n\t\t) | rpl::map([=](not_null<DocumentData*> document) {\n\t\t\treturn document.get();\n\t\t}) | rpl::map_error_to_done();\n\t}) | rpl::flatten_latest(\n\t) | rpl::map([=](DocumentData *document)\n\t-> rpl::producer<std::shared_ptr<StickerPlayer>> {\n\t\tif (!document) {\n\t\t\treturn rpl::single(std::shared_ptr<StickerPlayer>());\n\t\t}\n\t\tconst auto media = document->createMediaView();\n\t\tmedia->checkStickerLarge();\n\t\tmedia->goodThumbnailWanted();\n\n\t\treturn rpl::single() | rpl::then(\n\t\t\tdocument->session().downloaderTaskFinished()\n\t\t) | rpl::filter([=] {\n\t\t\treturn media->loaded();\n\t\t}) | rpl::take(1) | rpl::map([=] {\n\t\t\tauto result = std::shared_ptr<StickerPlayer>();\n\t\t\tconst auto sticker = document->sticker();\n\t\t\tif (sticker->isLottie()) {\n\t\t\t\tresult = std::make_shared<HistoryView::LottiePlayer>(\n\t\t\t\t\tChatHelpers::LottiePlayerFromDocument(\n\t\t\t\t\t\tmedia.get(),\n\t\t\t\t\t\tChatHelpers::StickerLottieSize::StickerSet,\n\t\t\t\t\t\tst::infoTopicCover.photo.size,\n\t\t\t\t\t\tLottie::Quality::High));\n\t\t\t} else if (sticker->isWebm()) {\n\t\t\t\tresult = std::make_shared<HistoryView::WebmPlayer>(\n\t\t\t\t\tmedia->owner()->location(),\n\t\t\t\t\tmedia->bytes(),\n\t\t\t\t\tst::infoTopicCover.photo.size);\n\t\t\t} else {\n\t\t\t\tresult = std::make_shared<HistoryView::StaticStickerPlayer>(\n\t\t\t\t\tmedia->owner()->location(),\n\t\t\t\t\tmedia->bytes(),\n\t\t\t\t\tst::infoTopicCover.photo.size);\n\t\t\t}\n\t\t\tresult->setRepaintCallback(_update);\n\t\t\t_playerUsesTextColor = media->owner()->emojiUsesTextColor();\n\t\t\treturn result;\n\t\t});\n\t}) | rpl::flatten_latest(\n\t) | rpl::on_next([=](std::shared_ptr<StickerPlayer> player) {\n\t\t_player = std::move(player);\n\t\tif (!_player) {\n\t\t\t_update();\n\t\t}\n\t}, _lifetime);\n}\n\nvoid TopicIconView::setupImage(not_null<Data::ForumTopic*> topic) {\n\tusing namespace Data;\n\tif (topic->isGeneral()) {\n\t\trpl::single(rpl::empty) | rpl::then(\n\t\t\tstyle::PaletteChanged()\n\t\t) | rpl::on_next([=] {\n\t\t\t_image = ForumTopicGeneralIconFrame(\n\t\t\t\tst::infoForumTopicIcon.size,\n\t\t\t\t_generalIconFg->c);\n\t\t\t_update();\n\t\t}, _lifetime);\n\t\treturn;\n\t}\n\trpl::combine(\n\t\tTitleValue(topic),\n\t\tColorIdValue(topic)\n\t) | rpl::map([=](const QString &title, int32 colorId) {\n\t\treturn ForumTopicIconFrame(colorId, title, st::infoForumTopicIcon);\n\t}) | rpl::on_next([=](QImage &&image) {\n\t\t_image = std::move(image);\n\t\t_update();\n\t}, _lifetime);\n}\n\nTopicIconButton::TopicIconButton(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Data::ForumTopic*> topic)\n: TopicIconButton(parent, topic, [=] {\n\treturn controller->isGifPausedAtLeastFor(Window::GifPauseReason::Layer);\n}) {\n}\n\nTopicIconButton::TopicIconButton(\n\tQWidget *parent,\n\tnot_null<Data::ForumTopic*> topic,\n\tFn<bool()> paused)\n: AbstractButton(parent)\n, _view(topic, paused, [=] { update(); }) {\n\tresize(st::infoTopicCover.photo.size);\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(this);\n\t\t_view.paintInRect(p, rect());\n\t}, lifetime());\n}\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_cover.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/abstract_button.h\"\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace HistoryView {\nclass StickerPlayer;\n} // namespace HistoryView\n\nnamespace Data {\nclass ForumTopic;\n} // namespace Data\n\nnamespace Info::Profile {\n\n[[nodiscard]] QMargins LargeCustomEmojiMargins();\n\nclass TopicIconView final {\npublic:\n\tTopicIconView(\n\t\tnot_null<Data::ForumTopic*> topic,\n\t\tFn<bool()> paused,\n\t\tFn<void()> update);\n\tTopicIconView(\n\t\tnot_null<Data::ForumTopic*> topic,\n\t\tFn<bool()> paused,\n\t\tFn<void()> update,\n\t\tconst style::color &generalIconFg);\n\n\tvoid paintInRect(\n\t\tQPainter &p,\n\t\tQRect rect,\n\t\tQColor textColor = QColor(0, 0, 0, 0));\n\nprivate:\n\tusing StickerPlayer = HistoryView::StickerPlayer;\n\n\tvoid setup(not_null<Data::ForumTopic*> topic);\n\tvoid setupPlayer(not_null<Data::ForumTopic*> topic);\n\tvoid setupImage(not_null<Data::ForumTopic*> topic);\n\n\tconst not_null<Data::ForumTopic*> _topic;\n\tconst style::color &_generalIconFg;\n\tFn<bool()> _paused;\n\tFn<void()> _update;\n\tstd::shared_ptr<StickerPlayer> _player;\n\tbool _playerUsesTextColor = false;\n\tQImage _image;\n\trpl::lifetime _lifetime;\n\n};\n\nclass TopicIconButton final : public Ui::AbstractButton {\npublic:\n\tTopicIconButton(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Data::ForumTopic*> topic);\n\tTopicIconButton(\n\t\tQWidget *parent,\n\t\tnot_null<Data::ForumTopic*> topic,\n\t\tFn<bool()> paused);\n\nprivate:\n\tTopicIconView _view;\n\n};\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_emoji_status_panel.h\"\n\n#include \"api/api_peer_photo.h\"\n#include \"apiwrap.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"lang/lang_keys.h\"\n#include \"menu/menu_send.h\" // SendMenu::Type.\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/time_picker_box.h\"\n#include \"ui/effects/emoji_fly_animation.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/ui_utility.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"main/main_session.h\"\n#include \"mainwindow.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Info::Profile {\nnamespace {\n\nconstexpr auto kLimitFirstRow = 8;\n\nvoid PickUntilBox(not_null<Ui::GenericBox*> box, Fn<void(TimeId)> callback) {\n\tbox->setTitle(tr::lng_emoji_status_for_title());\n\n\tconst auto seconds = Ui::DefaultTimePickerValues();\n\tconst auto phrases = ranges::views::all(\n\t\tseconds\n\t) | ranges::views::transform(Ui::FormatMuteFor) | ranges::to_vector;\n\n\tconst auto pickerCallback = Ui::TimePickerBox(box, seconds, phrases, 0);\n\n\tUi::ConfirmBox(box, {\n\t\t.confirmed = [=] {\n\t\t\tcallback(pickerCallback());\n\t\t\tbox->closeBox();\n\t\t},\n\t\t.confirmText = tr::lng_emoji_status_for_submit(),\n\t\t.cancelText = tr::lng_cancel(),\n\t});\n}\n\n} // namespace\n\nEmojiStatusPanel::EmojiStatusPanel() = default;\n\nEmojiStatusPanel::~EmojiStatusPanel() {\n\tif (hasFocus()) {\n\t\t// Panel will try to return focus to the layer widget, the problem is\n\t\t// we are destroying the layer widget probably right now and focusing\n\t\t// it will lead to a crash, because it destroys its children (how we\n\t\t// got here) after it clears focus out of itself. So if you return\n\t\t// the focus inside a child destructor, it won't be cleared at all.\n\t\t_panel->window()->setFocus();\n\t}\n}\n\nvoid EmojiStatusPanel::setChooseFilter(Fn<bool(EmojiStatusId)> filter) {\n\t_chooseFilter = std::move(filter);\n}\n\nvoid EmojiStatusPanel::show(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> button,\n\t\tData::CustomEmojiSizeTag animationSizeTag) {\n\tshow({\n\t\t.controller = controller,\n\t\t.button = button,\n\t\t.animationSizeTag = animationSizeTag,\n\t\t.ensureAddedEmojiId = controller->session().user()->emojiStatusId(),\n\t\t.withCollectibles = true,\n\t});\n}\n\nvoid EmojiStatusPanel::show(Descriptor &&descriptor) {\n\tconst auto controller = descriptor.controller;\n\tif (!_panel) {\n\t\tcreate(descriptor);\n\n\t\t_panel->shownValue(\n\t\t) | rpl::filter([=] {\n\t\t\treturn (_panelButton != nullptr);\n\t\t}) | rpl::on_next([=](bool shown) {\n\t\t\tif (shown) {\n\t\t\t\t_panelButton->installEventFilter(_panel.get());\n\t\t\t} else {\n\t\t\t\t_panelButton->removeEventFilter(_panel.get());\n\t\t\t}\n\t\t}, _panel->lifetime());\n\t}\n\tconst auto button = descriptor.button;\n\tif (const auto previous = _panelButton.data()) {\n\t\tif (previous != button) {\n\t\t\tprevious->removeEventFilter(_panel.get());\n\t\t}\n\t}\n\t_panelButton = button;\n\t_animationSizeTag = descriptor.animationSizeTag;\n\tconst auto feed = [=, now = descriptor.ensureAddedEmojiId](\n\t\t\tstd::vector<EmojiStatusId> list) {\n\t\tlist.insert(begin(list), EmojiStatusId());\n\t\tif (now && !ranges::contains(list, now)) {\n\t\t\tlist.push_back(now);\n\t\t}\n\t\t_panel->selector()->provideRecentEmoji(list);\n\t};\n\tif (descriptor.backgroundEmojiMode) {\n\t\tcontroller->session().api().peerPhoto().emojiListValue(\n\t\t\tApi::PeerPhoto::EmojiListType::Background\n\t\t) | rpl::on_next([=](std::vector<DocumentId> &&list) {\n\t\t\tauto tmp = std::vector<EmojiStatusId>();\n\t\t\tfor (const auto &id : list) {\n\t\t\t\ttmp.push_back(EmojiStatusId{ .documentId = id });\n\t\t\t}\n\t\t\tfeed(std::move(tmp));\n\t\t}, _panel->lifetime());\n\t} else if (descriptor.channelStatusMode) {\n\t\tconst auto &statuses = controller->session().data().emojiStatuses();\n\t\tconst auto &other = statuses.list(Data::EmojiStatuses::Type::ChannelDefault);\n\t\tauto list = statuses.list(Data::EmojiStatuses::Type::ChannelColored);\n\t\tif (list.size() > kLimitFirstRow - 1) {\n\t\t\tlist.erase(begin(list) + kLimitFirstRow - 1, end(list));\n\t\t}\n\t\tlist.reserve(list.size() + other.size() + 1);\n\t\tfor (const auto &id : other) {\n\t\t\tif (!ranges::contains(list, id)) {\n\t\t\t\tlist.push_back(id);\n\t\t\t}\n\t\t}\n\t\tfeed(std::move(list));\n\t} else {\n\t\tconst auto &statuses = controller->session().data().emojiStatuses();\n\t\tconst auto &recent = statuses.list(Data::EmojiStatuses::Type::Recent);\n\t\tconst auto &other = statuses.list(Data::EmojiStatuses::Type::Default);\n\t\tauto list = statuses.list(Data::EmojiStatuses::Type::Colored);\n\t\tif (list.size() > kLimitFirstRow - 1) {\n\t\t\tlist.erase(begin(list) + kLimitFirstRow - 1, end(list));\n\t\t}\n\t\tlist.reserve(list.size() + recent.size() + other.size() + 1);\n\t\tfor (const auto &id : ranges::views::concat(recent, other)) {\n\t\t\tif (!ranges::contains(list, id)) {\n\t\t\t\tlist.push_back(id);\n\t\t\t}\n\t\t}\n\t\tfeed(std::move(list));\n\t}\n\tconst auto parent = _panel->parentWidget();\n\tconst auto global = button->mapToGlobal(QPoint());\n\tconst auto local = parent->mapFromGlobal(global);\n\tif (descriptor.backgroundEmojiMode || descriptor.channelStatusMode) {\n\t\t_panel->moveBottomRight(\n\t\t\tlocal.y() + (st::normalFont->height / 2),\n\t\t\tlocal.x() + button->width() * 3);\n\t} else {\n\t\t_panel->moveTopRight(\n\t\t\tlocal.y() + button->height() - (st::normalFont->height / 2),\n\t\t\tlocal.x() + button->width() * 3);\n\t}\n\t_panel->toggleAnimated();\n}\n\nbool EmojiStatusPanel::hasFocus() const {\n\treturn _panel && Ui::InFocusChain(_panel.get());\n}\n\nvoid EmojiStatusPanel::repaint() {\n\t_panel->selector()->update();\n}\n\nbool EmojiStatusPanel::paintBadgeFrame(not_null<Ui::RpWidget*> widget) {\n\tif (!_animation) {\n\t\treturn false;\n\t} else if (_animation->paintBadgeFrame(widget)) {\n\t\treturn true;\n\t}\n\tInvokeQueued(_animation->layer(), [=] { _animation = nullptr; });\n\treturn false;\n}\n\nvoid EmojiStatusPanel::create(const Descriptor &descriptor) {\n\tusing Selector = ChatHelpers::TabbedSelector;\n\tusing Descriptor = ChatHelpers::TabbedSelectorDescriptor;\n\tusing Mode = ChatHelpers::TabbedSelector::Mode;\n\tconst auto controller = descriptor.controller;\n\tconst auto body = controller->window().widget()->bodyWidget();\n\tauto features = ChatHelpers::ComposeFeatures();\n\tfeatures.collectibleStatus = descriptor.withCollectibles;\n\t_panel = base::make_unique_q<ChatHelpers::TabbedPanel>(\n\t\tbody,\n\t\tcontroller,\n\t\tobject_ptr<Selector>(\n\t\t\tnullptr,\n\t\t\tDescriptor{\n\t\t\t\t.show = controller->uiShow(),\n\t\t\t\t.st = ((descriptor.backgroundEmojiMode\n\t\t\t\t\t|| descriptor.channelStatusMode)\n\t\t\t\t\t? st::backgroundEmojiPan\n\t\t\t\t\t: st::statusEmojiPan),\n\t\t\t\t.level = Window::GifPauseReason::Layer,\n\t\t\t\t.mode = (descriptor.backgroundEmojiMode\n\t\t\t\t\t? Mode::BackgroundEmoji\n\t\t\t\t\t: descriptor.channelStatusMode\n\t\t\t\t\t? Mode::ChannelStatus\n\t\t\t\t\t: Mode::EmojiStatus),\n\t\t\t\t.customTextColor = descriptor.customTextColor,\n\t\t\t\t.features = features,\n\t\t\t}));\n\t_customTextColor = descriptor.customTextColor;\n\t_backgroundEmojiMode = descriptor.backgroundEmojiMode;\n\t_channelStatusMode = descriptor.channelStatusMode;\n\t_panel->setDropDown(!_backgroundEmojiMode && !_channelStatusMode);\n\t_panel->setDesiredHeightValues(\n\t\t1.,\n\t\tst::emojiPanMinHeight / 2,\n\t\tst::emojiPanMinHeight);\n\t_panel->hide();\n\n\tstruct Chosen {\n\t\tEmojiStatusId id;\n\t\tTimeId until = 0;\n\t\tUi::MessageSendingAnimationFrom animation;\n\t};\n\n\t_panel->selector()->contextMenuRequested(\n\t) | rpl::on_next([=] {\n\t\t_panel->selector()->showMenuWithDetails({});\n\t}, _panel->lifetime());\n\n\tauto statusChosen = _panel->selector()->customEmojiChosen(\n\t) | rpl::map([=](ChatHelpers::FileChosen data) {\n\t\treturn Chosen{\n\t\t\t.id = {\n\t\t\t\tdata.collectible ? DocumentId() : data.document->id,\n\t\t\t\tdata.collectible,\n\t\t\t},\n\t\t\t.until = data.options.scheduled,\n\t\t\t.animation = data.messageSendingFrom,\n\t\t};\n\t});\n\n\tauto emojiChosen = _panel->selector()->emojiChosen(\n\t) | rpl::map([=](ChatHelpers::EmojiChosen data) {\n\t\treturn Chosen{ .animation = data.messageSendingFrom };\n\t});\n\n\tif (descriptor.backgroundEmojiMode || descriptor.channelStatusMode) {\n\t\trpl::merge(\n\t\t\tstd::move(statusChosen),\n\t\t\tstd::move(emojiChosen)\n\t\t) | rpl::on_next([=](const Chosen &chosen) {\n\t\t\tconst auto owner = &controller->session().data();\n\t\t\tstartAnimation(owner, body, chosen.id, chosen.animation);\n\t\t\t_someCustomChosen.fire({ chosen.id, chosen.until });\n\t\t\t_panel->hideAnimated();\n\t\t}, _panel->lifetime());\n\t} else {\n\t\tconst auto weak = base::make_weak(_panel.get());\n\t\tconst auto accept = [=](Chosen chosen) {\n\t\t\tExpects(chosen.until != Selector::kPickCustomTimeId);\n\n\t\t\t// PickUntilBox calls this after EmojiStatusPanel is destroyed!\n\t\t\tconst auto owner = &controller->session().data();\n\t\t\tif (weak) {\n\t\t\t\tstartAnimation(owner, body, chosen.id, chosen.animation);\n\t\t\t}\n\t\t\towner->emojiStatuses().set(chosen.id, chosen.until);\n\t\t};\n\n\t\trpl::merge(\n\t\t\tstd::move(statusChosen),\n\t\t\tstd::move(emojiChosen)\n\t\t) | rpl::filter([=](const Chosen &chosen) {\n\t\t\treturn filter(controller, chosen.id);\n\t\t}) | rpl::on_next([=](const Chosen &chosen) {\n\t\t\tif (chosen.until == Selector::kPickCustomTimeId) {\n\t\t\t\t_panel->hideAnimated();\n\t\t\t\tcontroller->show(Box(PickUntilBox, [=](TimeId seconds) {\n\t\t\t\t\taccept({ chosen.id, base::unixtime::now() + seconds });\n\t\t\t\t}));\n\t\t\t} else {\n\t\t\t\taccept(chosen);\n\t\t\t\t_panel->hideAnimated();\n\t\t\t}\n\t\t}, _panel->lifetime());\n\t}\n}\n\nbool EmojiStatusPanel::filter(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tEmojiStatusId chosenId) const {\n\tif (_chooseFilter) {\n\t\treturn _chooseFilter(chosenId);\n\t} else if (chosenId && !controller->session().premium()) {\n\t\tShowPremiumPreviewBox(controller, PremiumFeature::EmojiStatus);\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nvoid EmojiStatusPanel::startAnimation(\n\t\tnot_null<Data::Session*> owner,\n\t\tnot_null<Ui::RpWidget*> body,\n\t\tEmojiStatusId statusId,\n\t\tUi::MessageSendingAnimationFrom from) {\n\tif (!_panelButton || !statusId) {\n\t\treturn;\n\t}\n\tconst auto documentId = statusId.collectible\n\t\t? statusId.collectible->documentId\n\t\t: statusId.documentId;\n\tauto args = Ui::ReactionFlyAnimationArgs{\n\t\t.id = { { documentId } },\n\t\t.flyIcon = from.frame,\n\t\t.flyFrom = body->mapFromGlobal(from.globalStartGeometry),\n\t\t.forceFirstFrame = _backgroundEmojiMode,\n\t};\n\tconst auto color = _customTextColor\n\t\t? _customTextColor\n\t\t: [] { return st::profileVerifiedCheckBg->c; };\n\t_animation = std::make_unique<Ui::EmojiFlyAnimation>(\n\t\tbody,\n\t\t&owner->reactions(),\n\t\tstd::move(args),\n\t\t[=] { _animation->repaint(); },\n\t\t_customTextColor,\n\t\t_animationSizeTag);\n}\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n\nnamespace Data {\nclass Session;\nenum class CustomEmojiSizeTag : uchar;\n} // namespace Data\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nstruct MessageSendingAnimationFrom;\nclass EmojiFlyAnimation;\nclass RpWidget;\n} // namespace Ui\n\nnamespace Ui::Text {\nclass CustomEmoji;\nstruct CustomEmojiPaintContext;\n} // namespace Ui::Text\n\nnamespace ChatHelpers {\nclass TabbedPanel;\n} // namespace ChatHelpers\n\nnamespace Info::Profile {\n\nclass EmojiStatusPanel final {\npublic:\n\tEmojiStatusPanel();\n\t~EmojiStatusPanel();\n\n\tvoid setChooseFilter(Fn<bool(EmojiStatusId)> filter);\n\n\tvoid show(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> button,\n\t\tData::CustomEmojiSizeTag animationSizeTag = {});\n\t[[nodiscard]] bool hasFocus() const;\n\n\tstruct Descriptor {\n\t\tnot_null<Window::SessionController*> controller;\n\t\tnot_null<QWidget*> button;\n\t\tData::CustomEmojiSizeTag animationSizeTag = {};\n\t\tEmojiStatusId ensureAddedEmojiId;\n\t\tFn<QColor()> customTextColor;\n\t\tbool backgroundEmojiMode = false;\n\t\tbool channelStatusMode = false;\n\t\tbool withCollectibles = false;\n\t};\n\tvoid show(Descriptor &&descriptor);\n\tvoid repaint();\n\n\tstruct CustomChosen {\n\t\tEmojiStatusId id;\n\t\tTimeId until = 0;\n\t};\n\t[[nodiscard]] rpl::producer<CustomChosen> someCustomChosen() const {\n\t\treturn _someCustomChosen.events();\n\t}\n\n\tbool paintBadgeFrame(not_null<Ui::RpWidget*> widget);\n\nprivate:\n\tvoid create(const Descriptor &descriptor);\n\t[[nodiscard]] bool filter(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tEmojiStatusId chosenId) const;\n\n\tvoid startAnimation(\n\t\tnot_null<Data::Session*> owner,\n\t\tnot_null<Ui::RpWidget*> body,\n\t\tEmojiStatusId statusId,\n\t\tUi::MessageSendingAnimationFrom from);\n\n\tbase::unique_qptr<ChatHelpers::TabbedPanel> _panel;\n\tFn<QColor()> _customTextColor;\n\tFn<bool(EmojiStatusId)> _chooseFilter;\n\tQPointer<QWidget> _panelButton;\n\tstd::unique_ptr<Ui::EmojiFlyAnimation> _animation;\n\trpl::event_stream<CustomChosen> _someCustomChosen;\n\tData::CustomEmojiSizeTag _animationSizeTag = {};\n\tbool _backgroundEmojiMode = false;\n\tbool _channelStatusMode = false;\n\n};\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_icon.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_icon.h\"\n\nnamespace Info {\nnamespace Profile {\n\nFloatingIcon::FloatingIcon(\n\tRpWidget *parent,\n\tconst style::icon &icon,\n\tQPoint position)\n: FloatingIcon(parent, icon, position, Tag{}) {\n}\n\nFloatingIcon::FloatingIcon(\n\tRpWidget *parent,\n\tconst style::icon &icon,\n\tQPoint position,\n\tconst Tag &)\n: RpWidget(parent)\n, _icon(&icon)\n, _point(position) {\n\tsetGeometry(QRect(\n\t\tQPoint(0, 0),\n\t\tQSize(_point.x() + _icon->width(), _point.y() + _icon->height())));\n\tsetAttribute(Qt::WA_TransparentForMouseEvents);\n}\n\nvoid FloatingIcon::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\t_icon->paint(p, _point, width());\n}\n\n} // namespace Profile\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_icon.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace Info {\nnamespace Profile {\n\nclass FloatingIcon : public Ui::RpWidget {\npublic:\n\tFloatingIcon(\n\t\tRpWidget *parent,\n\t\tconst style::icon &icon,\n\t\tQPoint position);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tstruct Tag {\n\t};\n\tFloatingIcon(\n\t\tRpWidget *parent,\n\t\tconst style::icon &icon,\n\t\tQPoint position,\n\t\tconst Tag &);\n\n\tnot_null<const style::icon*> _icon;\n\tQPoint _point;\n\n};\n\n} // namespace Profile\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_inner_widget.h\"\n\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"info/profile/info_profile_widget.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"info/profile/info_profile_members.h\"\n#include \"info/profile/info_profile_music_button.h\"\n#include \"info/profile/info_profile_top_bar.h\"\n#include \"info/profile/info_profile_actions.h\"\n#include \"info/media/info_media_buttons.h\"\n#include \"info/saved/info_saved_music_widget.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_user.h\"\n#include \"data/data_saved_music.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"info/saved/info_saved_music_common.h\"\n#include \"info_profile_actions.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"api/api_peer_photo.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/format_song_document_name.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info {\nnamespace Profile {\n\nnamespace {\n\nvoid AddAboutVerification(\n\t\tnot_null<Ui::VerticalLayout*> layout,\n\t\tnot_null<PeerData*> peer) {\n\tconst auto inner = layout->add(object_ptr<Ui::VerticalLayout>(layout));\n\tpeer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::VerifyInfo\n\t) | rpl::on_next([=] {\n\t\tconst auto info = peer->botVerifyDetails();\n\t\twhile (inner->count()) {\n\t\t\tdelete inner->widgetAt(0);\n\t\t}\n\t\tif (!info) {\n\t\t\tUi::AddDivider(inner);\n\t\t} else {\n\t\t\tauto hasMainApp = false;\n\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\tif (user->botInfo) {\n\t\t\t\t\thasMainApp = user->botInfo->hasMainApp;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!hasMainApp && !info->description.empty()) {\n\t\t\t\tUi::AddDividerText(inner, rpl::single(info->description));\n\t\t\t}\n\t\t}\n\t\tinner->resizeToWidth(inner->width());\n\t}, inner->lifetime());\n}\n\nvoid AddUnofficialSecurityRiskWarning(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<UserData*> user) {\n\tconst auto content = container->add(\n\t\tobject_ptr<Ui::VerticalLayout>(container));\n\tuser->session().changes().peerFlagsValue(\n\t\tuser,\n\t\tData::PeerUpdate::Flag::FullInfo\n\t) | rpl::on_next([=] {\n\t\twhile (content->count()) {\n\t\t\tdelete content->widgetAt(0);\n\t\t}\n\t\tif (user->unofficialSecurityRisk()) {\n\t\t\tauto helper = Ui::Text::CustomEmojiHelper();\n\t\t\tauto icon = helper.paletteDependent({\n\t\t\t\t.factory = [] {\n\t\t\t\t\tconst auto s = st::infoSecurityRiskIconSize;\n\t\t\t\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t\t\t\tconst auto rect = QRect(0, 0, s, s);\n\t\t\t\t\tauto result = QImage(\n\t\t\t\t\t\trect.size() * ratio,\n\t\t\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t\t\tresult.setDevicePixelRatio(ratio);\n\t\t\t\t\tresult.fill(Qt::transparent);\n\n\t\t\t\t\tauto p = QPainter(&result);\n\t\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\t\tp.setBrush(st::attentionButtonFg);\n\t\t\t\t\tp.drawEllipse(rect);\n\n\t\t\t\t\tp.setPen(st::windowFgActive);\n\t\t\t\t\tp.setFont(st::semiboldFont);\n\t\t\t\t\tp.drawText(rect, u\"!\"_q, style::al_center);\n\n\t\t\t\t\tp.end();\n\t\t\t\t\treturn result;\n\t\t\t\t},\n\t\t\t\t.margin = st::infoSecurityRiskIconMargin,\n\t\t\t});\n\t\t\tauto label = object_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_profile_unofficial_warning(\n\t\t\t\t\tlt_icon,\n\t\t\t\t\trpl::single(std::move(icon)),\n\t\t\t\t\tlt_name,\n\t\t\t\t\trpl::single(TextWithEntities{ user->firstName }),\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::defaultDividerLabel.label,\n\t\t\t\tst::defaultPopupMenu,\n\t\t\t\thelper.context([=] { content->update(); }));\n\t\t\tcontent->add(object_ptr<Ui::DividerLabel>(\n\t\t\t\tcontent,\n\t\t\t\tstd::move(label),\n\t\t\t\tst::defaultBoxDividerLabelPadding,\n\t\t\t\tst::defaultDividerLabel.bar,\n\t\t\t\tRectPart::Top | RectPart::Bottom));\n\t\t}\n\t\tcontent->resizeToWidth(content->width());\n\t}, content->lifetime());\n}\n\n} // namespace\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller,\n\tOrigin origin)\n: RpWidget(parent)\n, _controller(controller)\n, _peer(_controller->key().peer())\n, _migrated(_controller->migrated())\n, _topic(_controller->key().topic())\n, _sublist(_controller->key().sublist())\n, _content(setupContent(this, origin)) {\n\t_content->heightValue(\n\t) | rpl::on_next([this](int height) {\n\t\tif (!_inResize) {\n\t\t\tresizeToWidth(width());\n\t\t\tupdateDesiredHeight();\n\t\t}\n\t}, lifetime());\n}\n\nrpl::producer<> InnerWidget::backRequest() const {\n\treturn _backClicks.events();\n}\n\nobject_ptr<Ui::RpWidget> InnerWidget::setupContent(\n\t\tnot_null<RpWidget*> parent,\n\t\tOrigin origin) {\n\tif (const auto user = _peer->asUser()) {\n\t\tuser->session().changes().peerFlagsValue(\n\t\t\tuser,\n\t\t\tData::PeerUpdate::Flag::FullInfo\n\t\t) | rpl::on_next([=] {\n\t\t\tauto &photos = user->session().api().peerPhoto();\n\t\t\tif (const auto original = photos.nonPersonalPhoto(user)) {\n\t\t\t\t// Preload it for the edit contact box.\n\t\t\t\t_nonPersonalView = original->createMediaView();\n\t\t\t\tconst auto id = peerToUser(user->id);\n\t\t\t\toriginal->load(Data::FileOriginFullUser{ id });\n\t\t\t}\n\t\t}, lifetime());\n\t}\n\n\tauto result = object_ptr<Ui::VerticalLayout>(parent);\n\tsetupSavedMusic(result);\n\tif (const auto user = _peer->asUser()) {\n\t\tAddUnofficialSecurityRiskWarning(result.data(), user);\n\t}\n\tif (_topic && _topic->creating()) {\n\t\treturn result;\n\t}\n\n\tauto mainTracker = Ui::MultiSlideTracker();\n\tauto sharedTracker = Ui::MultiSlideTracker();\n\tauto dividerOverridden = rpl::variable<bool>(false);\n\tAddDetails(\n\t\tresult,\n\t\t_controller,\n\t\t_peer,\n\t\t_topic,\n\t\t_sublist,\n\t\torigin,\n\t\tmainTracker,\n\t\tdividerOverridden);\n\tauto showDivider = rpl::combine(\n\t\tmainTracker.atLeastOneShownValue(),\n\t\tdividerOverridden.value()\n\t) | rpl::map([](bool main, bool dividerOverridden) {\n\t\treturn dividerOverridden ? false : main;\n\t}) | rpl::distinct_until_changed();\n\tresult->add(\n\t\tsetupSharedMedia(\n\t\t\tresult.data(),\n\t\t\trpl::duplicate(showDivider),\n\t\t\tsharedTracker));\n\tif (_topic || _sublist) {\n\t\treturn result;\n\t}\n\t{\n\t\tauto buttons = SetupChannelMembersAndManage(\n\t\t\t_controller,\n\t\t\tresult.data(),\n\t\t\t_peer);\n\t\tif (buttons) {\n\t\t\tresult->add(std::move(buttons));\n\t\t}\n\t}\n\tauto showNext = rpl::combine(\n\t\tstd::move(showDivider),\n\t\tsharedTracker.atLeastOneShownValue()\n\t) | rpl::map([](bool show, bool shared) {\n\t\treturn show || shared;\n\t}) | rpl::distinct_until_changed();\n\tif (auto actions = SetupActions(_controller, result.data(), _peer)) {\n\t\taddAboutVerificationOrDivider(result, rpl::duplicate(showNext));\n\t\tresult->add(std::move(actions));\n\t}\n\tif (!_aboutVerificationAdded) {\n\t\tAddAboutVerification(result, _peer);\n\t}\n\tif (_peer->isChat() || _peer->isMegagroup()) {\n\t\tif (!_peer->isMonoforum()) {\n\t\t\tsetupMembers(result.data(), rpl::duplicate(showNext));\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid InnerWidget::setupMembers(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<bool> showDivider) {\n\tauto wrap = container->add(object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto inner = wrap->entity();\n\taddAboutVerificationOrDivider(inner, std::move(showDivider));\n\t_members = inner->add(object_ptr<Members>(inner, _controller));\n\t_members->scrollToRequests(\n\t) | rpl::on_next([this](Ui::ScrollToRequest request) {\n\t\tauto min = (request.ymin < 0)\n\t\t\t? request.ymin\n\t\t\t: MapFrom(this, _members, QPoint(0, request.ymin)).y();\n\t\tauto max = (request.ymin < 0)\n\t\t\t? MapFrom(this, _members, QPoint()).y()\n\t\t\t: (request.ymax < 0)\n\t\t\t? request.ymax\n\t\t\t: MapFrom(this, _members, QPoint(0, request.ymax)).y();\n\t\t_scrollToRequests.fire({ min, max });\n\t}, _members->lifetime());\n\t_members->onlineCountValue(\n\t) | rpl::on_next([=](int count) {\n\t\t_onlineCount.fire_copy(count);\n\t}, _members->lifetime());\n\n\tusing namespace rpl::mappers;\n\twrap->toggleOn(\n\t\t_members->fullCountValue() | rpl::map(_1 > 0),\n\t\tanim::type::instant);\n}\n\nvoid InnerWidget::setupSavedMusic(not_null<Ui::VerticalLayout*> container) {\n\tInfo::Saved::SetupSavedMusic(\n\t\tcontainer,\n\t\t_controller,\n\t\t_sublist ? _sublist->sublistPeer() : _peer,\n\t\t_topBarColor.value());\n}\n\nvoid InnerWidget::addAboutVerificationOrDivider(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\trpl::producer<bool> showDivider) {\n\tif (rpl::variable<bool>(rpl::duplicate(showDivider)).current()) {\n\t\tif (_aboutVerificationAdded) {\n\t\t\tUi::AddDivider(content);\n\t\t} else {\n\t\t\tAddAboutVerification(content, _peer);\n\t\t\t_aboutVerificationAdded = true;\n\t\t}\n\t} else {\n\t\tconst auto wrap = content->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontent,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(content)));\n\t\tUi::AddDivider(wrap->entity());\n\t\twrap->setDuration(\n\t\t\tst::infoSlideDuration\n\t\t)->toggleOn(rpl::duplicate(showDivider));\n\t}\n}\n\nobject_ptr<Ui::RpWidget> InnerWidget::setupSharedMedia(\n\t\tnot_null<RpWidget*> parent,\n\t\trpl::producer<bool> showDivider,\n\t\tUi::MultiSlideTracker &sharedTracker) {\n\tusing namespace rpl::mappers;\n\tusing MediaType = Media::Type;\n\n\tconst auto peer = _sublist ? _sublist->sublistPeer() : _peer;\n\tauto content = object_ptr<Ui::VerticalLayout>(parent);\n\tauto &tracker = sharedTracker;\n\tauto addMediaButton = [&](\n\t\t\tMediaType type,\n\t\t\tconst style::icon &icon) {\n\t\tauto result = Media::AddButton(\n\t\t\tcontent,\n\t\t\t_controller,\n\t\t\tpeer,\n\t\t\t_topic ? _topic->rootId() : MsgId(),\n\t\t\t_sublist ? _sublist->sublistPeer()->id : PeerId(),\n\t\t\t_migrated,\n\t\t\ttype,\n\t\t\ttracker);\n\t\tobject_ptr<Profile::FloatingIcon>(\n\t\t\tresult,\n\t\t\ticon,\n\t\t\tst::infoSharedMediaButtonIconPosition);\n\t};\n\tauto addCommonGroupsButton = [&](\n\t\t\tnot_null<UserData*> user,\n\t\t\tconst style::icon &icon) {\n\t\tauto result = Media::AddCommonGroupsButton(\n\t\t\tcontent,\n\t\t\t_controller,\n\t\t\tuser,\n\t\t\ttracker);\n\t\tobject_ptr<Profile::FloatingIcon>(\n\t\t\tresult,\n\t\t\ticon,\n\t\t\tst::infoSharedMediaButtonIconPosition);\n\t};\n\tconst auto addSimilarPeersButton = [&](\n\t\t\tnot_null<PeerData*> peer,\n\t\t\tconst style::icon &icon) {\n\t\tauto result = Media::AddSimilarPeersButton(\n\t\t\tcontent,\n\t\t\t_controller,\n\t\t\tpeer,\n\t\t\ttracker);\n\t\tobject_ptr<Profile::FloatingIcon>(\n\t\t\tresult,\n\t\t\ticon,\n\t\t\tst::infoSharedMediaButtonIconPosition);\n\t};\n\tauto addStoriesButton = [&](\n\t\t\tnot_null<PeerData*> peer,\n\t\t\tconst style::icon &icon) {\n\t\tif (peer->isChat()) {\n\t\t\treturn;\n\t\t}\n\t\tauto result = Media::AddStoriesButton(\n\t\t\tcontent,\n\t\t\t_controller,\n\t\t\tpeer,\n\t\t\ttracker);\n\t\tobject_ptr<Profile::FloatingIcon>(\n\t\t\tresult,\n\t\t\ticon,\n\t\t\tst::infoSharedMediaButtonIconPosition);\n\t};\n\tauto addSavedSublistButton = [&](\n\t\t\tnot_null<PeerData*> peer,\n\t\t\tconst style::icon &icon) {\n\t\tauto result = Media::AddSavedSublistButton(\n\t\t\tcontent,\n\t\t\t_controller,\n\t\t\tpeer,\n\t\t\ttracker);\n\t\tobject_ptr<Profile::FloatingIcon>(\n\t\t\tresult,\n\t\t\ticon,\n\t\t\tst::infoSharedMediaButtonIconPosition);\n\t};\n\tauto addPeerGiftsButton = [&](\n\t\t\tnot_null<PeerData*> peer,\n\t\t\tconst style::icon &icon) {\n\t\tauto result = Media::AddPeerGiftsButton(\n\t\t\tcontent,\n\t\t\t_controller,\n\t\t\tpeer,\n\t\t\ttracker);\n\t\tobject_ptr<Profile::FloatingIcon>(\n\t\t\tresult,\n\t\t\ticon,\n\t\t\tst::infoSharedMediaButtonIconPosition);\n\t};\n\n\tif (!_topic) {\n\t\taddStoriesButton(peer, st::infoIconMediaStories);\n\t\taddPeerGiftsButton(peer, st::infoIconMediaGifts);\n\t\taddSavedSublistButton(peer, st::infoIconMediaSaved);\n\t}\n\taddMediaButton(MediaType::Photo, st::infoIconMediaPhoto);\n\taddMediaButton(MediaType::Video, st::infoIconMediaVideo);\n\taddMediaButton(MediaType::File, st::infoIconMediaFile);\n\taddMediaButton(MediaType::MusicFile, st::infoIconMediaAudio);\n\taddMediaButton(MediaType::Link, st::infoIconMediaLink);\n\taddMediaButton(MediaType::Poll, st::infoIconMediaPoll);\n\taddMediaButton(MediaType::RoundVoiceFile, st::infoIconMediaVoice);\n\taddMediaButton(MediaType::GIF, st::infoIconMediaGif);\n\tif (const auto bot = peer->asBot()) {\n\t\taddCommonGroupsButton(bot, st::infoIconMediaGroup);\n\t\taddSimilarPeersButton(bot, st::infoIconMediaBot);\n\t} else if (const auto channel = peer->asBroadcast()) {\n\t\taddSimilarPeersButton(channel, st::infoIconMediaChannel);\n\t} else if (const auto user = peer->asUser()) {\n\t\taddCommonGroupsButton(user, st::infoIconMediaGroup);\n\t}\n\n\tauto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\tparent,\n\t\tobject_ptr<Ui::VerticalLayout>(parent)\n\t);\n\n\tresult->setDuration(\n\t\tst::infoSlideDuration\n\t)->toggleOn(\n\t\ttracker.atLeastOneShownValue()\n\t);\n\n\tauto layout = result->entity();\n\n\taddAboutVerificationOrDivider(layout, std::move(showDivider));\n\tUi::AddSkip(layout, st::infoSharedMediaBottomSkip);\n\tlayout->add(std::move(content));\n\tUi::AddSkip(layout, st::infoSharedMediaBottomSkip);\n\n\t_sharedMediaWrap = result;\n\treturn result;\n}\n\nint InnerWidget::countDesiredHeight() const {\n\treturn _content->height() + (_members\n\t\t? (_members->desiredHeight() - _members->height())\n\t\t: 0);\n}\n\nvoid InnerWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tsetChildVisibleTopBottom(_content, visibleTop, visibleBottom);\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\tif (_members) {\n\t\tmemento->setMembersState(_members->saveState());\n\t}\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\tif (_members) {\n\t\t_members->restoreState(memento->membersState());\n\t}\n\tif (_sharedMediaWrap) {\n\t\t_sharedMediaWrap->finishAnimating();\n\t}\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nrpl::producer<int> InnerWidget::desiredHeightValue() const {\n\treturn _desiredHeight.events_starting_with(countDesiredHeight());\n}\n\nint InnerWidget::resizeGetHeight(int newWidth) {\n\t_inResize = true;\n\tauto guard = gsl::finally([&] { _inResize = false; });\n\n\t_content->resizeToWidth(newWidth);\n\t_content->moveToLeft(0, 0);\n\tupdateDesiredHeight();\n\treturn _content->heightNoMargins();\n}\n\nvoid InnerWidget::enableBackButton() {\n\t_backToggles.force_assign(true);\n}\n\nvoid InnerWidget::showFinished() {\n\t_showFinished.fire({});\n}\n\nbool InnerWidget::hasFlexibleTopBar() const {\n\treturn true;\n}\n\nbase::weak_qptr<Ui::RpWidget> InnerWidget::createPinnedToTop(\n\t\tnot_null<Ui::RpWidget*> parent) {\n\tconst auto content = Ui::CreateChild<TopBar>(\n\t\tparent,\n\t\tTopBar::Descriptor{\n\t\t\t.controller = _controller->parentController(),\n\t\t\t.key = _controller->key(),\n\t\t\t.wrap = _controller->wrapValue(),\n\t\t\t.peer = _sublist ? _sublist->sublistPeer().get() : nullptr,\n\t\t\t.backToggles = _backToggles.value(),\n\t\t\t.showFinished = _showFinished.events(),\n\t\t});\n\tcontent->backRequest(\n\t) | rpl::start_to_stream(_backClicks, content->lifetime());\n\tcontent->setOnlineCount(_onlineCount.events());\n\t_topBarColor = content->edgeColor();\n\treturn base::make_weak(not_null<Ui::RpWidget*>{ content });\n}\n\nbase::weak_qptr<Ui::RpWidget> InnerWidget::createPinnedToBottom(\n\t\tnot_null<Ui::RpWidget*> parent) {\n\treturn nullptr;\n}\n\n} // namespace Profile\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_inner_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"base/object_ptr.h\"\n\nnamespace Data {\nclass ForumTopic;\nclass SavedSublist;\nclass PhotoMedia;\n} // namespace Data\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass VerticalLayout;\ntemplate <typename Widget>\nclass SlideWrap;\nstruct ScrollToRequest;\nclass MultiSlideTracker;\n} // namespace Ui\n\nnamespace Info {\n\nenum class Wrap;\nclass Controller;\n\nnamespace Profile {\n\nclass Memento;\nclass Members;\nstruct Origin;\n\nclass InnerWidget final : public Ui::RpWidget {\npublic:\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tOrigin origin);\n\n\t[[nodiscard]] rpl::producer<> backRequest() const;\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\trpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\trpl::producer<int> desiredHeightValue() const override;\n\n\tbool hasFlexibleTopBar() const;\n\tbase::weak_qptr<Ui::RpWidget> createPinnedToTop(\n\t\tnot_null<Ui::RpWidget*> parent);\n\tbase::weak_qptr<Ui::RpWidget> createPinnedToBottom(\n\t\tnot_null<Ui::RpWidget*> parent);\n\n\tvoid enableBackButton();\n\tvoid showFinished();\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\nprivate:\n\tobject_ptr<RpWidget> setupContent(\n\t\tnot_null<RpWidget*> parent,\n\t\tOrigin origin);\n\tobject_ptr<RpWidget> setupSharedMedia(\n\t\tnot_null<RpWidget*> parent,\n\t\trpl::producer<bool> showDivider,\n\t\tUi::MultiSlideTracker &sharedTracker);\n\tvoid setupMembers(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<bool> showDivider);\n\tvoid setupSavedMusic(not_null<Ui::VerticalLayout*> container);\n\n\tint countDesiredHeight() const;\n\tvoid updateDesiredHeight() {\n\t\t_desiredHeight.fire(countDesiredHeight());\n\t}\n\n\tvoid addAboutVerificationOrDivider(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\trpl::producer<bool> showDivider);\n\n\tconst not_null<Controller*> _controller;\n\tconst not_null<PeerData*> _peer;\n\tPeerData * const _migrated = nullptr;\n\tData::ForumTopic * const _topic = nullptr;\n\tData::SavedSublist * const _sublist = nullptr;\n\n\tbool _inResize = false;\n\tbool _aboutVerificationAdded = false;\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\trpl::event_stream<int> _desiredHeight;\n\n\trpl::variable<bool> _backToggles;\n\trpl::event_stream<> _backClicks;\n\trpl::event_stream<int> _onlineCount;\n\trpl::event_stream<> _showFinished;\n\n\tstd::shared_ptr<Data::PhotoMedia> _nonPersonalView;\n\n\trpl::variable<std::optional<QColor>> _topBarColor;\n\n\tMembers *_members = nullptr;\n\tUi::SlideWrap<RpWidget> *_sharedMediaWrap = nullptr;\n\tobject_ptr<RpWidget> _content;\n\n};\n\n} // namespace Profile\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_members.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_members.h\"\n\n#include <rpl/combine.h>\n#include \"info/profile/info_profile_widget.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"info/profile/info_profile_members_controllers.h\"\n#include \"info/members/info_members_widget.h\"\n#include \"info/info_content_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/search_field_controller.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/peers/add_participants_box.h\"\n#include \"window/window_session_controller.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info {\nnamespace Profile {\nnamespace {\n\nconstexpr auto kEnableSearchMembersAfterCount = 20;\n\n} // namespace\n\nMembers::Members(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: RpWidget(parent)\n, _show(controller->uiShow())\n, _controller(controller)\n, _peer(_controller->key().peer())\n, _listController(CreateMembersController(controller, _peer)) {\n\t_listController->setStoriesShown(true);\n\tsetupHeader();\n\tsetupList();\n\tsetContent(_list.data());\n\t_listController->setDelegate(static_cast<PeerListDelegate*>(this));\n\n\t_controller->searchFieldController()->queryValue(\n\t) | rpl::on_next([this](QString &&query) {\n\t\tpeerListScrollToTop();\n\t\tcontent()->searchQueryChanged(std::move(query));\n\t}, lifetime());\n\tMembersCountValue(\n\t\t_peer\n\t) | rpl::on_next([this](int count) {\n\t\tconst auto enabled = (count >= kEnableSearchMembersAfterCount);\n\t\t_controller->setSearchEnabledByContent(enabled);\n\t}, lifetime());\n}\n\nint Members::desiredHeight() const {\n\tauto desired = _header ? _header->height() : 0;\n\tauto count = [this] {\n\t\tif (auto chat = _peer->asChat()) {\n\t\t\treturn chat->count;\n\t\t} else if (auto channel = _peer->asChannel()) {\n\t\t\treturn channel->membersCount();\n\t\t}\n\t\treturn 0;\n\t}();\n\tdesired += qMax(count, _list->fullRowsCount())\n\t\t* st::infoMembersList.item.height;\n\treturn qMax(height(), desired);\n}\n\nrpl::producer<int> Members::onlineCountValue() const {\n\treturn _listController->onlineCountValue();\n}\n\nrpl::producer<int> Members::fullCountValue() const {\n\treturn _listController->fullCountValue();\n}\n\nrpl::producer<Ui::ScrollToRequest> Members::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nstd::unique_ptr<MembersState> Members::saveState() {\n\tauto result = std::make_unique<MembersState>();\n\tresult->list = _listController->saveState();\n\t//if (_searchShown) {\n\t//\tresult->search = _searchField->getLastText();\n\t//}\n\treturn result;\n}\n\nvoid Members::restoreState(std::unique_ptr<MembersState> state) {\n\tif (!state) {\n\t\treturn;\n\t}\n\t_listController->restoreState(std::move(state->list));\n\t//updateSearchEnabledByContent();\n\t//if (!_controller->searchFieldController()->query().isEmpty()) {\n\t//\tif (!_searchShown) {\n\t//\t\ttoggleSearch(anim::type::instant);\n\t//\t}\n\t//} else if (_searchShown) {\n\t//\ttoggleSearch(anim::type::instant);\n\t//}\n}\n\nvoid Members::setupHeader() {\n\tif (_controller->section().type() == Section::Type::Members) {\n\t\treturn;\n\t}\n\t_header = object_ptr<Ui::FixedHeightWidget>(\n\t\tthis,\n\t\tst::infoMembersHeader);\n\tauto parent = _header.data();\n\n\t_openMembers = Ui::CreateChild<Ui::SettingsButton>(\n\t\tparent,\n\t\trpl::single(QString()));\n\t// _openMembers->setAccessibleName(tr::lng_manage_peer_members(tr::now));\n\n\tobject_ptr<FloatingIcon>(\n\t\tparent,\n\t\tst::infoIconMembers,\n\t\tst::infoGroupMembersIconPosition);\n\n\t_titleWrap = Ui::CreateChild<Ui::RpWidget>(parent);\n\t_title = setupTitle();\n\t_addMember = Ui::CreateChild<Ui::IconButton>(\n\t\t_openMembers,\n\t\tst::infoMembersAddMember);\n\t_addMember->setAccessibleName(tr::lng_channel_add_members(tr::now));\n\t//_searchField = _controller->searchFieldController()->createField(\n\t//\tparent,\n\t//\tst::infoMembersSearchField);\n\t_search = Ui::CreateChild<Ui::IconButton>(\n\t\t_openMembers,\n\t\tst::infoMembersSearch);\n\t_search->setAccessibleName(tr::lng_participant_filter(tr::now));\n\t//_cancelSearch = Ui::CreateChild<Ui::CrossButton>(\n\t//\tparent,\n\t//\tst::infoMembersCancelSearch);\n\n\tsetupButtons();\n\n\t//_controller->wrapValue(\n\t//) | rpl::on_next([this](Wrap wrap) {\n\t//\t_wrap = wrap;\n\t//\tupdateSearchOverrides();\n\t//}, lifetime());\n\twidthValue(\n\t) | rpl::on_next([this](int width) {\n\t\t_header->resizeToWidth(width);\n\t}, _header->lifetime());\n}\n\nobject_ptr<Ui::FlatLabel> Members::setupTitle() {\n\tauto visible = _peer->isMegagroup()\n\t\t? CanViewParticipantsValue(_peer->asMegagroup())\n\t\t: rpl::single(true);\n\tauto text = rpl::conditional(\n\t\tstd::move(visible),\n\t\ttr::lng_chat_status_members(\n\t\t\tlt_count_decimal,\n\t\t\tMembersCountValue(_peer) | tr::to_count(),\n\t\t\ttr::upper),\n\t\ttr::lng_channel_admins(tr::upper));\n\trpl::duplicate(text) | rpl::on_next([=](const QString &v) {\n\t\t_openMembers->setAccessibleName(v);\n\t}, _openMembers->lifetime());\n\tauto result = object_ptr<Ui::FlatLabel>(\n\t\t_titleWrap,\n\t\tstd::move(text),\n\t\tst::infoBlockHeaderLabel);\n\tresult->setAttribute(Qt::WA_TransparentForMouseEvents);\n\treturn result;\n}\n\nvoid Members::setupButtons() {\n\tusing namespace rpl::mappers;\n\n\t_openMembers->addClickHandler([this] {\n\t\tshowMembersWithSearch(false);\n\t});\n\n\t//_searchField->hide();\n\t//_cancelSearch->setVisible(false);\n\n\tauto visible = _peer->isMegagroup()\n\t\t? CanViewParticipantsValue(_peer->asMegagroup())\n\t\t: rpl::single(true);\n\trpl::duplicate(visible) | rpl::on_next([=](bool visible) {\n\t\t_openMembers->setVisible(visible);\n\t}, lifetime());\n\n\tauto addMemberShown = CanAddMemberValue(\n\t\t_peer\n\t) | rpl::start_spawning(lifetime());\n\t_addMember->showOn(rpl::duplicate(addMemberShown));\n\t_addMember->addClickHandler([this] { // TODO throttle(ripple duration)\n\t\tthis->addMember();\n\t});\n\n\tauto searchShown = MembersCountValue(_peer)\n\t\t| rpl::map(_1 >= kEnableSearchMembersAfterCount)\n\t\t| rpl::distinct_until_changed()\n\t\t| rpl::start_spawning(lifetime());\n\t_search->showOn(rpl::duplicate(searchShown));\n\t_search->addClickHandler([this] { // TODO throttle(ripple duration)\n\t\tthis->showMembersWithSearch(true);\n\t});\n\t//_cancelSearch->addClickHandler([this] {\n\t//\tthis->cancelSearch();\n\t//});\n\n\trpl::combine(\n\t\tstd::move(addMemberShown),\n\t\tstd::move(searchShown),\n\t\tstd::move(visible)\n\t) | rpl::on_next([this] {\n\t\tupdateHeaderControlsGeometry(width());\n\t}, lifetime());\n}\n\nvoid Members::setupList() {\n\tauto topSkip = _header ? _header->height() : 0;\n\t_listController->setStyleOverrides(&st::infoMembersList);\n\t_listController->setStoriesShown(true);\n\t_list = object_ptr<ListWidget>(\n\t\tthis,\n\t\t_listController.get());\n\t_list->scrollToRequests(\n\t) | rpl::on_next([this](Ui::ScrollToRequest request) {\n\t\tauto addmin = (request.ymin < 0 || !_header)\n\t\t\t? 0\n\t\t\t: _header->height();\n\t\tauto addmax = (request.ymax < 0 || !_header)\n\t\t\t? 0\n\t\t\t: _header->height();\n\t\t_scrollToRequests.fire({\n\t\t\trequest.ymin + addmin,\n\t\t\trequest.ymax + addmax });\n\t}, _list->lifetime());\n\twidthValue(\n\t) | rpl::on_next([this](int newWidth) {\n\t\t_list->resizeToWidth(newWidth);\n\t}, _list->lifetime());\n\t_list->heightValue(\n\t) | rpl::on_next([=](int listHeight) {\n\t\tauto newHeight = (listHeight > st::membersMarginBottom)\n\t\t\t? (topSkip\n\t\t\t\t+ listHeight\n\t\t\t\t+ st::membersMarginBottom)\n\t\t\t: 0;\n\t\tresize(width(), newHeight);\n\t}, _list->lifetime());\n\t_list->moveToLeft(0, topSkip);\n}\n\nint Members::resizeGetHeight(int newWidth) {\n\tif (_header) {\n\t\tupdateHeaderControlsGeometry(newWidth);\n\t}\n\treturn heightNoMargins();\n}\n\n//void Members::updateSearchEnabledByContent() {\n//\t_controller->setSearchEnabledByContent(\n//\t\tpeerListFullRowsCount() >= kEnableSearchMembersAfterCount);\n//}\n\nvoid Members::updateHeaderControlsGeometry(int newWidth) {\n\t_openMembers->setGeometry(0, st::infoProfileSkip, newWidth, st::infoMembersButton.height);\n\n\tauto availableWidth = newWidth\n\t\t- st::infoMembersButtonPosition.x();\n\n\t//auto cancelLeft = availableWidth - _cancelSearch->width();\n\t//_cancelSearch->moveToLeft(\n\t//\tcancelLeft,\n\t//\tst::infoMembersButtonPosition.y());\n\n\t//auto searchShownLeft = st::infoIconPosition.x()\n\t//\t- st::infoMembersSearch.iconPosition.x();\n\t//auto searchHiddenLeft = availableWidth - _search->width();\n\t//auto searchShown = _searchShownAnimation.value(_searchShown ? 1. : 0.);\n\t//auto searchCurrentLeft = anim::interpolate(\n\t//\tsearchHiddenLeft,\n\t//\tsearchShownLeft,\n\t//\tsearchShown);\n\t//_search->moveToLeft(\n\t//\tsearchCurrentLeft,\n\t//\tst::infoMembersButtonPosition.y());\n\n\t//if (!_search->isHidden()) {\n\t//\tavailableWidth -= st::infoMembersSearch.width;\n\t//}\n\t_addMember->moveToLeft(\n\t\tavailableWidth - _addMember->width(),\n\t\tst::infoMembersButtonPosition.y(),\n\t\tnewWidth);\n\tif (!_addMember->isHidden()) {\n\t\tavailableWidth -= st::infoMembersSearch.width;\n\t}\n\t_search->moveToLeft(\n\t\tavailableWidth - _search->width(),\n\t\tst::infoMembersButtonPosition.y(),\n\t\tnewWidth);\n\n\t//auto fieldLeft = anim::interpolate(\n\t//\tcancelLeft,\n\t//\tst::infoBlockHeaderPosition.x(),\n\t//\tsearchShown);\n\t//_searchField->setGeometryToLeft(\n\t//\tfieldLeft,\n\t//\tst::infoMembersSearchTop,\n\t//\tcancelLeft - fieldLeft,\n\t//\t_searchField->height());\n\n\t//_titleWrap->resize(\n\t//\tsearchCurrentLeft - st::infoBlockHeaderPosition.x(),\n\t//\t_title->height());\n\t_titleWrap->resize(\n\t\tavailableWidth - _addMember->width() - st::infoBlockHeaderPosition.x(),\n\t\t_title->height());\n\t_titleWrap->moveToLeft(\n\t\tst::infoBlockHeaderPosition.x(),\n\t\tst::infoBlockHeaderPosition.y(),\n\t\tnewWidth);\n\t_titleWrap->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t//_title->resizeToWidth(searchHiddenLeft);\n\t_title->resizeToWidth(_titleWrap->width());\n\t_title->moveToLeft(0, 0);\n}\n\nvoid Members::addMember() {\n\tif (const auto chat = _peer->asChat()) {\n\t\tAddParticipantsBoxController::Start(_controller, chat);\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\tconst auto state = _listController->saveState();\n\t\tconst auto users = ranges::views::all(\n\t\t\tstate->list\n\t\t) | ranges::views::transform([](not_null<PeerData*> peer) {\n\t\t\treturn peer->asUser();\n\t\t}) | ranges::to_vector;\n\t\tAddParticipantsBoxController::Start(\n\t\t\t_controller,\n\t\t\tchannel,\n\t\t\t{ users.begin(), users.end() });\n\t}\n}\n\nvoid Members::showMembersWithSearch(bool withSearch) {\n\t//if (!_searchShown) {\n\t//\ttoggleSearch();\n\t//}\n\tauto contentMemento = std::make_shared<Info::Members::Memento>(\n\t\t_controller);\n\tcontentMemento->setState(saveState());\n\tcontentMemento->setSearchStartsFocused(withSearch);\n\tauto mementoStack = std::vector<std::shared_ptr<ContentMemento>>();\n\tmementoStack.push_back(std::move(contentMemento));\n\t_controller->showSection(\n\t\tstd::make_shared<Info::Memento>(std::move(mementoStack)));\n}\n\n//void Members::toggleSearch(anim::type animated) {\n//\t_searchShown = !_searchShown;\n//\t_cancelSearch->toggle(_searchShown, animated);\n//\tif (animated == anim::type::normal) {\n//\t\t_searchShownAnimation.start(\n//\t\t\t[this] { searchAnimationCallback(); },\n//\t\t\t_searchShown ? 0. : 1.,\n//\t\t\t_searchShown ? 1. : 0.,\n//\t\t\tst::slideWrapDuration);\n//\t} else {\n//\t\t_searchShownAnimation.finish();\n//\t\tsearchAnimationCallback();\n//\t}\n//\t_search->setDisabled(_searchShown);\n//\tif (_searchShown) {\n//\t\t_searchField->show();\n//\t\t_searchField->setFocus();\n//\t} else {\n//\t\tsetFocus();\n//\t}\n//}\n//\n//void Members::searchAnimationCallback() {\n//\tif (!_searchShownAnimation.animating()) {\n//\t\t_searchField->setVisible(_searchShown);\n//\t\tupdateSearchOverrides();\n//\t\t_search->setPointerCursor(!_searchShown);\n//\t}\n//\tupdateHeaderControlsGeometry(width());\n//}\n//\n//void Members::updateSearchOverrides() {\n//\tauto iconOverride = !_searchShown\n//\t\t? nullptr\n//\t\t: (_wrap == Wrap::Layer)\n//\t\t? &st::infoMembersSearchActiveLayer\n//\t\t: &st::infoMembersSearchActive;\n//\t_search->setIconOverride(iconOverride, iconOverride);\n//}\n//\n//void Members::cancelSearch() {\n//\tif (_searchShown) {\n//\t\tif (!_searchField->getLastText().isEmpty()) {\n//\t\t\t_searchField->setText(QString());\n//\t\t\t_searchField->setFocus();\n//\t\t} else {\n//\t\t\ttoggleSearch();\n//\t\t}\n//\t}\n//}\n\nvoid Members::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tsetChildVisibleTopBottom(_list, visibleTop, visibleBottom);\n}\n\nvoid Members::peerListSetTitle(rpl::producer<QString> title) {\n}\n\nvoid Members::peerListSetAdditionalTitle(rpl::producer<QString> title) {\n}\n\nbool Members::peerListIsRowChecked(not_null<PeerListRow*> row) {\n\treturn false;\n}\n\nint Members::peerListSelectedRowsCount() {\n\treturn 0;\n}\n\nvoid Members::peerListScrollToTop() {\n\t_scrollToRequests.fire({ -1, -1 });\n}\n\nvoid Members::peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) {\n\tUnexpected(\"Item selection in Info::Profile::Members.\");\n}\n\nvoid Members::peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) {\n\tUnexpected(\"Item selection in Info::Profile::Members.\");\n}\n\nvoid Members::peerListFinishSelectedRowsBunch() {\n}\n\nstd::shared_ptr<Main::SessionShow> Members::peerListUiShow() {\n\treturn _show;\n}\n\nvoid Members::peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) {\n\tdescription.destroy();\n}\n\n} // namespace Profile\n} // namespace Info\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_members.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"boxes/peer_list_box.h\"\n\nclass ParticipantsBoxController;\n\nnamespace Ui {\nclass InputField;\nclass CrossButton;\nclass IconButton;\nclass FlatLabel;\nstruct ScrollToRequest;\nclass AbstractButton;\nclass SettingsButton;\n} // namespace Ui\n\nnamespace Window {\nclass Show;\n} // namespace Window\n\nnamespace Info {\n\nclass Controller;\nenum class Wrap;\n\nnamespace Profile {\n\nclass Memento;\nstruct MembersState {\n\tstd::unique_ptr<PeerListState> list;\n\tstd::optional<QString> search;\n};\n\nclass Members\n\t: public Ui::RpWidget\n\t, private PeerListContentDelegate {\npublic:\n\tMembers(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller);\n\n\trpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\n\tstd::unique_ptr<MembersState> saveState();\n\tvoid restoreState(std::unique_ptr<MembersState> state);\n\n\t[[nodiscard]] int desiredHeight() const;\n\t[[nodiscard]] rpl::producer<int> onlineCountValue() const;\n\t[[nodiscard]] rpl::producer<int> fullCountValue() const;\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tusing ListWidget = PeerListContent;\n\n\t// PeerListContentDelegate interface.\n\tvoid peerListSetTitle(rpl::producer<QString> title) override;\n\tvoid peerListSetAdditionalTitle(rpl::producer<QString> title) override;\n\tbool peerListIsRowChecked(not_null<PeerListRow*> row) override;\n\tint peerListSelectedRowsCount() override;\n\tvoid peerListScrollToTop() override;\n\tvoid peerListAddSelectedPeerInBunch(\n\t\tnot_null<PeerData*> peer) override;\n\tvoid peerListAddSelectedRowInBunch(\n\t\tnot_null<PeerListRow*> row) override;\n\tvoid peerListFinishSelectedRowsBunch() override;\n\tvoid peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) override;\n\tstd::shared_ptr<Main::SessionShow> peerListUiShow() override;\n\n\t//void peerListAppendRow(\n\t//\tstd::unique_ptr<PeerListRow> row) override {\n\t//\tPeerListContentDelegate::peerListAppendRow(std::move(row));\n\t//\tupdateSearchEnabledByContent();\n\t//}\n\t//void peerListPrependRow(\n\t//\tstd::unique_ptr<PeerListRow> row) override {\n\t//\tPeerListContentDelegate::peerListPrependRow(std::move(row));\n\t//\tupdateSearchEnabledByContent();\n\t//}\n\t//void peerListRemoveRow(not_null<PeerListRow*> row) override {\n\t//\tPeerListContentDelegate::peerListRemoveRow(row);\n\t//\tupdateSearchEnabledByContent();\n\t//}\n\n\tvoid setupHeader();\n\tobject_ptr<Ui::FlatLabel> setupTitle();\n\tvoid setupList();\n\n\tvoid setupButtons();\n\t//void updateSearchOverrides();\n\n\tvoid addMember();\n\tvoid showMembersWithSearch(bool withSearch);\n\t//void toggleSearch(anim::type animated = anim::type::normal);\n\t//void cancelSearch();\n\t//void searchAnimationCallback();\n\tvoid updateHeaderControlsGeometry(int newWidth);\n\t//void updateSearchEnabledByContent();\n\n\tstd::shared_ptr<Main::SessionShow> _show;\n\n\t//Wrap _wrap;\n\tnot_null<Controller*> _controller;\n\tnot_null<PeerData*> _peer;\n\tstd::unique_ptr<ParticipantsBoxController> _listController;\n\tobject_ptr<Ui::RpWidget> _header = { nullptr };\n\tobject_ptr<ListWidget> _list = { nullptr };\n\n\tUi::SettingsButton *_openMembers = nullptr;\n\tUi::RpWidget *_titleWrap = nullptr;\n\tUi::FlatLabel *_title = nullptr;\n\tUi::IconButton *_addMember = nullptr;\n\t//base::unique_qptr<Ui::InputField> _searchField;\n\tUi::IconButton *_search = nullptr;\n\t//Ui::CrossButton *_cancelSearch = nullptr;\n\n\t//Ui::Animations::Simple _searchShownAnimation;\n\t//bool _searchShown = false;\n\t//base::Timer _searchTimer;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\n};\n\n} // namespace Profile\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_members_controllers.h\"\n\n#include \"boxes/peers/edit_participants_box.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"ui/unread_badge.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Info {\nnamespace Profile {\n\nMemberListRow::MemberListRow(\n\tnot_null<PeerData*> peer,\n\tType type)\n: PeerListRow(peer)\n, _type(type) {\n\tsetType(type);\n}\n\nMemberListRow::~MemberListRow() = default;\n\nvoid MemberListRow::setType(Type type) {\n\t_type = type;\n\t_tagRipple = nullptr;\n\t_removeRipple = nullptr;\n\tif (_type.canAddTag) {\n\t\t_tagMode = TagMode::AddTag;\n\t\t_tagText = tr::lng_context_add_my_tag(tr::now);\n\t} else if (!_type.rank.isEmpty()) {\n\t\t_tagMode = (_type.rights == Rights::Admin\n\t\t\t|| _type.rights == Rights::Creator)\n\t\t\t? TagMode::AdminPill\n\t\t\t: TagMode::NormalText;\n\t\t_tagText = _type.rank;\n\t} else if (_type.rights == Rights::Creator) {\n\t\t_tagMode = TagMode::AdminPill;\n\t\t_tagText = tr::lng_owner_badge(tr::now);\n\t} else if (_type.rights == Rights::Admin) {\n\t\t_tagMode = TagMode::AdminPill;\n\t\t_tagText = tr::lng_admin_badge(tr::now);\n\t} else {\n\t\t_tagMode = TagMode::None;\n\t\t_tagText = QString();\n\t}\n\t_tagTextWidth = _tagText.isEmpty()\n\t\t? 0\n\t\t: st::normalFont->width(_tagText);\n\tif (_type.canRemove) {\n\t\t_removeText = _type.removeText.isEmpty()\n\t\t\t? tr::lng_profile_kick(tr::now)\n\t\t\t: _type.removeText;\n\t\t_removeTextWidth = st::normalFont->width(_removeText);\n\t} else {\n\t\t_removeText = QString();\n\t\t_removeTextWidth = 0;\n\t}\n}\n\nMemberListRow::Type MemberListRow::type() const {\n\treturn _type;\n}\n\nUserData *MemberListRow::user() const {\n\treturn peer()->asUser();\n}\n\nvoid MemberListRow::setRefreshCallback(Fn<void()> callback) {\n\t_refreshCallback = std::move(callback);\n}\n\nbool MemberListRow::tagInteractive() const {\n\treturn _type.canEditTag || _type.canAddTag;\n}\n\nQSize MemberListRow::tagSize() const {\n\tif (_tagTextWidth == 0) {\n\t\treturn QSize();\n\t}\n\tconst auto usePill = (_tagMode == TagMode::AdminPill)\n\t\t|| (_tagMode == TagMode::AddTag)\n\t\t|| ((_tagMode == TagMode::NormalText) && tagInteractive());\n\tif (usePill) {\n\t\tconst auto &p = st::memberTagPillPadding;\n\t\tconst auto h = p.top() + st::normalFont->height + p.bottom();\n\t\tconst auto w = p.left() + _tagTextWidth + p.right();\n\t\treturn QSize(std::max(w, h), h);\n\t}\n\treturn QSize(_tagTextWidth, st::normalFont->height);\n}\n\nQSize MemberListRow::rightActionSize() const {\n\tconst auto tag = tagSize();\n\tconst auto remove = removeSize();\n\tconst auto w = std::max(tag.width(), remove.width());\n\treturn (w > 0)\n\t\t? QSize(w, st::defaultPeerListItem.height)\n\t\t: QSize();\n}\n\nQMargins MemberListRow::rightActionMargins() const {\n\tconst auto skip = st::contactsCheckPosition.x();\n\treturn QMargins(\n\t\tskip,\n\t\t0,\n\t\tst::defaultPeerListItem.photoPosition.x() + skip,\n\t\t0);\n}\n\nQSize MemberListRow::removeSize() const {\n\tif (_removeTextWidth == 0) {\n\t\treturn QSize();\n\t}\n\tconst auto &p = st::memberTagPillPadding;\n\tconst auto h = p.top() + st::normalFont->height + p.bottom();\n\tconst auto w = p.left() + _removeTextWidth + p.right();\n\treturn QSize(std::max(w, h), h);\n}\n\nint MemberListRow::elementsCount() const {\n\treturn _type.canRemove ? 2 : 1;\n}\n\nQRect MemberListRow::elementGeometry(int element, int outerWidth) const {\n\tconst auto skip = st::contactsCheckPosition.x();\n\tconst auto &st = st::defaultPeerListItem;\n\tconst auto right = st.photoPosition.x() + skip;\n\n\tif (element == kTagElement) {\n\t\tconst auto size = tagSize();\n\t\tif (size.isEmpty()) {\n\t\t\treturn QRect();\n\t\t}\n\t\tconst auto left = outerWidth - right - size.width();\n\t\tif (!_type.canRemove) {\n\t\t\tconst auto top = (st.height - size.height()) / 2;\n\t\t\treturn QRect(QPoint(left, top), size);\n\t\t}\n\t\tconst auto progress = _hoverAnimation.value(\n\t\t\t_wasHovered ? 1. : 0.);\n\t\tconst auto centeredTop = (st.height - size.height()) / 2;\n\t\tconst auto statusAlignedTop = st.statusPosition.y()\n\t\t\t+ (st::contactsStatusFont->height - size.height()) / 2;\n\t\tconst auto top = anim::interpolate(\n\t\t\tcenteredTop, statusAlignedTop, progress);\n\t\treturn QRect(QPoint(left, top), size);\n\t} else if (element == kRemoveElement) {\n\t\tconst auto size = removeSize();\n\t\tif (size.isEmpty()) {\n\t\t\treturn QRect();\n\t\t}\n\t\tconst auto left = outerWidth - right - size.width();\n\t\tconst auto nameAlignedTop = st.namePosition.y()\n\t\t\t+ (st.nameStyle.font->height - size.height()) / 2;\n\t\treturn QRect(QPoint(left, nameAlignedTop), size);\n\t}\n\treturn QRect();\n}\n\nbool MemberListRow::elementDisabled(int element) const {\n\tif (element == kTagElement) {\n\t\treturn !tagInteractive();\n\t}\n\tif (element == kRemoveElement) {\n\t\tconst auto progress = _hoverAnimation.value(\n\t\t\t_wasHovered ? 1. : 0.);\n\t\treturn (progress == 0.);\n\t}\n\treturn false;\n}\n\nbool MemberListRow::elementOnlySelect(int element) const {\n\treturn false;\n}\n\nvoid MemberListRow::elementAddRipple(\n\t\tint element,\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) {\n\tif (!_refreshCallback) {\n\t\t_refreshCallback = updateCallback;\n\t}\n\tif (element == kTagElement) {\n\t\tif (!tagInteractive()) {\n\t\t\treturn;\n\t\t}\n\t\tif (!_tagRipple) {\n\t\t\tconst auto size = tagSize();\n\t\t\tconst auto radius = size.height() / 2;\n\t\t\tauto mask = Ui::RippleAnimation::RoundRectMask(\n\t\t\t\tsize, radius);\n\t\t\t_tagRipple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\tst::defaultLightButton.ripple,\n\t\t\t\tstd::move(mask),\n\t\t\t\tupdateCallback);\n\t\t}\n\t\t_tagRipple->add(point);\n\t} else if (element == kRemoveElement) {\n\t\tif (!_removeRipple) {\n\t\t\tconst auto size = removeSize();\n\t\t\tconst auto radius = size.height() / 2;\n\t\t\tauto mask = Ui::RippleAnimation::RoundRectMask(\n\t\t\t\tsize, radius);\n\t\t\t_removeRipple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\tst::defaultLightButton.ripple,\n\t\t\t\tstd::move(mask),\n\t\t\t\tupdateCallback);\n\t\t}\n\t\t_removeRipple->add(point);\n\t}\n}\n\nvoid MemberListRow::elementsStopLastRipple() {\n\tif (_tagRipple) {\n\t\t_tagRipple->lastStop();\n\t}\n\tif (_removeRipple) {\n\t\t_removeRipple->lastStop();\n\t}\n}\n\nvoid MemberListRow::refreshStatus() {\n\tconst auto u = user();\n\tif (u && u->isBot()) {\n\t\tconst auto seesAllMessages = (u->botInfo->readsAllHistory\n\t\t\t|| _type.rights != Rights::Normal);\n\t\tsetCustomStatus(seesAllMessages\n\t\t\t? tr::lng_status_bot_reads_all(tr::now)\n\t\t\t: tr::lng_status_bot_not_reads_all(tr::now));\n\t} else {\n\t\tPeerListRow::refreshStatus();\n\t}\n}\n\nvoid MemberListRow::checkHoverChanged(bool hovered) {\n\tif (!_type.canRemove || _wasHovered == hovered) {\n\t\treturn;\n\t}\n\t_wasHovered = hovered;\n\t_hoverAnimation.start(\n\t\t_refreshCallback ? _refreshCallback : Fn<void()>([] {}),\n\t\thovered ? 0. : 1.,\n\t\thovered ? 1. : 0.,\n\t\tst::universalDuration);\n}\n\nint MemberListRow::pillHeight() const {\n\tconst auto &p = st::memberTagPillPadding;\n\treturn p.top() + st::normalFont->height + p.bottom();\n}\n\nconst QImage &MemberListRow::ensurePillCircle(const QColor &color) const {\n\tauto &cache = *_type.circleCache;\n\tconst auto rgba = color.rgba();\n\tconst auto it = cache.find(rgba);\n\tif (it != end(cache)) {\n\t\treturn it->second;\n\t}\n\tconst auto h = pillHeight();\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto image = QImage(\n\t\tQSize(h, h) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(ratio);\n\timage.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&image);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(color);\n\t\tp.drawEllipse(0, 0, h, h);\n\t}\n\treturn cache.emplace(rgba, std::move(image)).first->second;\n}\n\nvoid MemberListRow::paintPill(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint width,\n\t\tconst QColor &bgColor) const {\n\tconst auto h = pillHeight();\n\tconst auto &circle = ensurePillCircle(bgColor);\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto half = h / 2;\n\tconst auto otherHalf = h - half;\n\tp.drawImage(\n\t\tQRect(x, y, half, h),\n\t\tcircle,\n\t\tQRect(0, 0, half * ratio, h * ratio));\n\tif (width > h) {\n\t\tp.fillRect(\n\t\t\tx + half,\n\t\t\ty,\n\t\t\twidth - h,\n\t\t\th,\n\t\t\tbgColor);\n\t}\n\tp.drawImage(\n\t\tQRect(x + width - otherHalf, y, otherHalf, h),\n\t\tcircle,\n\t\tQRect(half * ratio, 0, otherHalf * ratio, h * ratio));\n}\n\nvoid MemberListRow::paintColoredPill(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint w,\n\t\tint textWidth,\n\t\tconst QString &text,\n\t\tconst QColor &color,\n\t\tbool over,\n\t\tstd::unique_ptr<Ui::RippleAnimation> &ripple,\n\t\tint outerWidth) {\n\tconst auto &pad = st::memberTagPillPadding;\n\tauto bgColor = color;\n\tbgColor.setAlphaF(over ? 0.16 : 0.12);\n\tpaintPill(p, x, y, w, bgColor);\n\tif (ripple) {\n\t\tauto rippleColor = color;\n\t\trippleColor.setAlphaF(0.12);\n\t\tripple->paint(p, x, y, outerWidth, &rippleColor);\n\t\tif (ripple->empty()) {\n\t\t\tripple.reset();\n\t\t}\n\t}\n\tp.setFont(st::normalFont);\n\tp.setPen(color);\n\tp.drawTextLeft(\n\t\tx + (w - textWidth) / 2,\n\t\ty + pad.top(),\n\t\touterWidth,\n\t\ttext,\n\t\ttextWidth);\n}\n\nvoid MemberListRow::paintTag(\n\t\tPainter &p,\n\t\tQRect geometry,\n\t\tint outerWidth,\n\t\tbool over) {\n\tif (_tagTextWidth == 0) {\n\t\treturn;\n\t}\n\tconst auto x = geometry.x();\n\tconst auto y = geometry.y();\n\tconst auto &pad = st::memberTagPillPadding;\n\tswitch (_tagMode) {\n\tcase TagMode::AdminPill: {\n\t\tconst auto nameColor = (_type.rights == Rights::Creator)\n\t\t\t? st::rankOwnerFg->c\n\t\t\t: st::rankAdminFg->c;\n\t\tconst auto h = pillHeight();\n\t\tconst auto cw = pad.left() + _tagTextWidth + pad.right();\n\t\tconst auto w = std::max(cw, h);\n\t\tpaintColoredPill(\n\t\t\tp,\n\t\t\tx,\n\t\t\ty,\n\t\t\tw,\n\t\t\t_tagTextWidth,\n\t\t\t_tagText,\n\t\t\tnameColor,\n\t\t\tover,\n\t\t\t_tagRipple,\n\t\t\touterWidth);\n\t} break;\n\tcase TagMode::NormalText: {\n\t\tif (tagInteractive()) {\n\t\t\tconst auto h = pillHeight();\n\t\t\tconst auto cw = pad.left() + _tagTextWidth + pad.right();\n\t\t\tconst auto w = std::max(cw, h);\n\t\t\tif (over) {\n\t\t\t\tauto bgColor = st::rankUserFg->c;\n\t\t\t\tbgColor.setAlphaF(0.12);\n\t\t\t\tpaintPill(p, x, y, w, bgColor);\n\t\t\t}\n\t\t\tif (_tagRipple) {\n\t\t\t\tauto rippleColor = st::rankUserFg->c;\n\t\t\t\trippleColor.setAlphaF(0.12);\n\t\t\t\t_tagRipple->paint(\n\t\t\t\t\tp, x, y, outerWidth, &rippleColor);\n\t\t\t\tif (_tagRipple->empty()) {\n\t\t\t\t\t_tagRipple.reset();\n\t\t\t\t}\n\t\t\t}\n\t\t\tp.setFont(st::normalFont);\n\t\t\tp.setPen(st::rankUserFg);\n\t\t\tp.drawTextLeft(\n\t\t\t\tx + (w - _tagTextWidth) / 2,\n\t\t\t\ty + pad.top(),\n\t\t\t\touterWidth,\n\t\t\t\t_tagText,\n\t\t\t\t_tagTextWidth);\n\t\t} else {\n\t\t\tp.setFont(st::normalFont);\n\t\t\tp.setPen(st::rankUserFg);\n\t\t\tp.drawTextLeft(\n\t\t\t\tx, y, outerWidth, _tagText, _tagTextWidth);\n\t\t}\n\t} break;\n\tcase TagMode::AddTag: {\n\t\tconst auto h = pillHeight();\n\t\tconst auto cw = pad.left() + _tagTextWidth + pad.right();\n\t\tconst auto w = std::max(cw, h);\n\t\tif (over) {\n\t\t\tpaintPill(p, x, y, w, st::lightButtonBgOver->c);\n\t\t}\n\t\tif (_tagRipple) {\n\t\t\tconst auto color = st::lightButtonBgRipple->c;\n\t\t\t_tagRipple->paint(p, x, y, outerWidth, &color);\n\t\t\tif (_tagRipple->empty()) {\n\t\t\t\t_tagRipple.reset();\n\t\t\t}\n\t\t}\n\t\tp.setFont(st::normalFont);\n\t\tp.setPen(over\n\t\t\t? st::lightButtonFgOver\n\t\t\t: st::lightButtonFg);\n\t\tp.drawTextLeft(\n\t\t\tx + (w - _tagTextWidth) / 2,\n\t\t\ty + pad.top(),\n\t\t\touterWidth,\n\t\t\t_tagText,\n\t\t\t_tagTextWidth);\n\t} break;\n\tcase TagMode::None:\n\t\tbreak;\n\t}\n}\n\nvoid MemberListRow::paintRemove(\n\t\tPainter &p,\n\t\tQRect geometry,\n\t\tint outerWidth,\n\t\tbool over) {\n\tif (_removeTextWidth == 0) {\n\t\treturn;\n\t}\n\tconst auto progress = _hoverAnimation.value(\n\t\t_wasHovered ? 1. : 0.);\n\tif (progress == 0.) {\n\t\treturn;\n\t}\n\tconst auto x = geometry.x();\n\tconst auto y = geometry.y();\n\tconst auto &pad = st::memberTagPillPadding;\n\tconst auto h = pillHeight();\n\tconst auto cw = pad.left() + _removeTextWidth + pad.right();\n\tconst auto w = std::max(cw, h);\n\n\tauto o = p.opacity();\n\tp.setOpacity(o * progress);\n\n\tif (over) {\n\t\tpaintPill(p, x, y, w, st::lightButtonBgOver->c);\n\t}\n\tif (_removeRipple) {\n\t\tconst auto color = st::lightButtonBgRipple->c;\n\t\t_removeRipple->paint(p, x, y, outerWidth, &color);\n\t\tif (_removeRipple->empty()) {\n\t\t\t_removeRipple.reset();\n\t\t}\n\t}\n\tp.setFont(st::normalFont);\n\tp.setPen(over ? st::lightButtonFgOver : st::lightButtonFg);\n\tp.drawTextLeft(\n\t\tx + (w - _removeTextWidth) / 2,\n\t\ty + pad.top(),\n\t\touterWidth,\n\t\t_removeText,\n\t\t_removeTextWidth);\n\n\tp.setOpacity(o);\n}\n\nvoid MemberListRow::elementsPaint(\n\t\tPainter &p,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tint selectedElement) {\n\tcheckHoverChanged(selected || (selectedElement > 0));\n\tif (_type.canRemove) {\n\t\tconst auto removeGeometry = elementGeometry(\n\t\t\tkRemoveElement, outerWidth);\n\t\tif (!removeGeometry.isEmpty()) {\n\t\t\tpaintRemove(\n\t\t\t\tp,\n\t\t\t\tremoveGeometry,\n\t\t\t\touterWidth,\n\t\t\t\t(selectedElement == kRemoveElement));\n\t\t}\n\t}\n\tconst auto tagGeometry = elementGeometry(\n\t\tkTagElement, outerWidth);\n\tif (!tagGeometry.isEmpty()) {\n\t\tpaintTag(\n\t\t\tp,\n\t\t\ttagGeometry,\n\t\t\touterWidth,\n\t\t\t(selectedElement == kTagElement));\n\t}\n}\n\nstd::unique_ptr<ParticipantsBoxController> CreateMembersController(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer) {\n\treturn std::make_unique<ParticipantsBoxController>(\n\t\tnavigation,\n\t\tpeer,\n\t\tParticipantsBoxController::Role::Profile);\n}\n\n} // namespace Profile\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_members_controllers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peer_list_box.h\"\n#include \"base/flat_map.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/unread_badge.h\"\n\nnamespace Ui {\nclass ChatStyle;\n} // namespace Ui\n\nclass ParticipantsBoxController;\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nnamespace Info {\nnamespace Profile {\n\nclass MemberListRow final : public PeerListRow {\npublic:\n\tenum class Rights {\n\t\tNormal,\n\t\tAdmin,\n\t\tCreator,\n\t};\n\tstruct Type {\n\t\tRights rights;\n\t\tQString rank;\n\t\tQString removeText;\n\t\tnot_null<const Ui::ChatStyle*> chatStyle;\n\t\tnot_null<base::flat_map<QRgb, QImage>*> circleCache;\n\t\tbool canAddTag = false;\n\t\tbool canEditTag = false;\n\t\tbool canRemove = false;\n\t};\n\n\tstatic constexpr auto kTagElement = 1;\n\tstatic constexpr auto kRemoveElement = 2;\n\n\tMemberListRow(not_null<PeerData*> peer, Type type);\n\t~MemberListRow();\n\n\tvoid setType(Type type);\n\t[[nodiscard]] Type type() const;\n\tvoid setRefreshCallback(Fn<void()> callback);\n\tvoid refreshStatus() override;\n\n\t[[nodiscard]] UserData *user() const;\n\n\tint elementsCount() const override;\n\tQRect elementGeometry(int element, int outerWidth) const override;\n\tbool elementDisabled(int element) const override;\n\tbool elementOnlySelect(int element) const override;\n\tvoid elementAddRipple(\n\t\tint element,\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) override;\n\tvoid elementsStopLastRipple() override;\n\tvoid elementsPaint(\n\t\tPainter &p,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tint selectedElement) override;\n\n\tQSize rightActionSize() const override;\n\tQMargins rightActionMargins() const override;\n\nprivate:\n\tenum class TagMode {\n\t\tNone,\n\t\tAdminPill,\n\t\tNormalText,\n\t\tAddTag,\n\t};\n\n\t[[nodiscard]] bool tagInteractive() const;\n\t[[nodiscard]] int pillHeight() const;\n\t[[nodiscard]] const QImage &ensurePillCircle(const QColor &color) const;\n\tvoid paintPill(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint width,\n\t\tconst QColor &bgColor) const;\n\tvoid paintColoredPill(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint w,\n\t\tint textWidth,\n\t\tconst QString &text,\n\t\tconst QColor &color,\n\t\tbool over,\n\t\tstd::unique_ptr<Ui::RippleAnimation> &ripple,\n\t\tint outerWidth);\n\tvoid paintTag(\n\t\tPainter &p,\n\t\tQRect geometry,\n\t\tint outerWidth,\n\t\tbool over);\n\tvoid paintRemove(\n\t\tPainter &p,\n\t\tQRect geometry,\n\t\tint outerWidth,\n\t\tbool over);\n\t[[nodiscard]] QSize tagSize() const;\n\t[[nodiscard]] QSize removeSize() const;\n\tvoid checkHoverChanged(bool hovered);\n\n\tType _type;\n\tTagMode _tagMode = TagMode::None;\n\tQString _tagText;\n\tint _tagTextWidth = 0;\n\tQString _removeText;\n\tint _removeTextWidth = 0;\n\tUi::Animations::Simple _hoverAnimation;\n\tstd::unique_ptr<Ui::RippleAnimation> _tagRipple;\n\tstd::unique_ptr<Ui::RippleAnimation> _removeRipple;\n\tFn<void()> _refreshCallback;\n\tbool _wasHovered = false;\n\n};\n\nstd::unique_ptr<ParticipantsBoxController> CreateMembersController(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer);\n\n} // namespace Profile\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_music_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_music_button.h\"\n\n#include \"ui/effects/animation_value.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info::Profile {\n\nMusicButton::MusicButton(\n\tQWidget *parent,\n\tMusicButtonData data,\n\tFn<void()> handler)\n: RippleButton(parent, st::infoMusicButtonRipple)\n, _noteSymbol(u\"\\u266B\"_q + QChar(' '))\n, _noteWidth(st::normalFont->width(_noteSymbol)) {\n\tupdateData(std::move(data));\n\tsetClickedCallback(std::move(handler));\n}\n\nMusicButton::~MusicButton() = default;\n\nvoid MusicButton::updateData(MusicButtonData data) {\n\tconst auto result = data.name.textWithEntities();\n\tconst auto performerLength = result.entities.empty()\n\t\t? 0\n\t\t: int(result.entities.front().length());\n\t_performer.setText(\n\t\tst::semiboldTextStyle,\n\t\tresult.text.mid(0, performerLength));\n\t_title.setText(\n\t\tst::defaultTextStyle,\n\t\tresult.text.mid(performerLength, result.text.size()));\n\tsetAccessibleName(result.text);\n\tupdate();\n}\n\nvoid MusicButton::setOverrideBg(std::optional<QColor> color) {\n\t_overrideBg = color;\n\tupdate();\n}\n\nvoid MusicButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tif (_overrideBg) {\n\t\tp.fillRect(e->rect(), Ui::BlendColors(\n\t\t\t*_overrideBg,\n\t\t\tQt::black,\n\t\t\tst::infoProfileTopBarActionButtonBgOpacity));\n\t} else {\n\t\tp.fillRect(e->rect(), st::shadowFg);\n\t}\n\tpaintRipple(p, QPoint());\n\n\tconst auto &icon = st::topicButtonArrow;\n\tconst auto iconWidth = icon.width();\n\tconst auto iconHeight = icon.height();\n\n\tconst auto padding = st::infoMusicButtonPadding;\n\tconst auto skip = st::normalFont->spacew;\n\n\tconst auto titleWidth = _title.maxWidth();\n\tconst auto performerWidth = _performer.maxWidth();\n\tconst auto totalNeeded = titleWidth + performerWidth + skip;\n\tconst auto availableWidth = width()\n\t\t- rect::m::sum::h(padding)\n\t\t- iconWidth\n\t\t- skip\n\t\t- _noteWidth;\n\n\tauto actualTitleWidth = 0;\n\tauto actualPerformerWidth = 0;\n\tif (totalNeeded <= availableWidth) {\n\t\tactualTitleWidth = titleWidth;\n\t\tactualPerformerWidth = performerWidth;\n\t} else {\n\t\tconst auto ratio = float64(titleWidth) / totalNeeded;\n\t\tactualPerformerWidth = int(availableWidth * (1.0 - ratio));\n\t\tactualTitleWidth = availableWidth - actualPerformerWidth;\n\t}\n\n\tconst auto totalContentWidth = _noteWidth\n\t\t+ actualPerformerWidth\n\t\t+ skip\n\t\t+ actualTitleWidth\n\t\t+ skip\n\t\t+ iconWidth;\n\tconst auto centerX = width() / 2;\n\tconst auto contentStartX = centerX - totalContentWidth / 2;\n\tconst auto textTop = (height() - st::normalFont->height) / 2;\n\n\tp.setPen(_overrideBg ? st::groupCallMembersFg : st::windowBoldFg);\n\tp.setFont(st::normalFont);\n\tp.drawText(contentStartX, textTop + st::normalFont->ascent, _noteSymbol);\n\n\t_performer.draw(p, {\n\t\t.position = { contentStartX + _noteWidth, textTop },\n\t\t.availableWidth = actualPerformerWidth,\n\t\t.now = crl::now(),\n\t\t.elisionLines = 1,\n\t\t.elisionMiddle = true,\n\t});\n\n\tp.setPen(_overrideBg ? st::groupCallVideoSubTextFg : st::windowSubTextFg);\n\t_title.draw(p, {\n\t\t.position = QPoint(\n\t\t\tcontentStartX + _noteWidth + actualPerformerWidth + skip,\n\t\t\ttextTop),\n\t\t.availableWidth = actualTitleWidth,\n\t\t.now = crl::now(),\n\t\t.elisionLines = 1,\n\t\t.elisionMiddle = true,\n\t});\n\n\tconst auto iconLeft = contentStartX\n\t\t+ _noteWidth\n\t\t+ actualPerformerWidth\n\t\t+ actualTitleWidth\n\t\t+ skip\n\t\t+ skip;\n\tconst auto iconTop = (height() - iconHeight) / 2;\n\ticon.paint(p, iconLeft, iconTop, iconWidth, p.pen().color());\n}\n\nint MusicButton::resizeGetHeight(int newWidth) {\n\tconst auto padding = st::infoMusicButtonPadding;\n\tconst auto &font = st::defaultTextStyle.font;\n\n\treturn padding.top() + font->height + padding.bottom();\n}\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_music_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/buttons.h\"\n#include \"ui/text/format_song_name.h\"\n#include \"ui/text/text.h\"\n\nnamespace Info::Profile {\n\nstruct MusicButtonData {\n\tUi::Text::FormatSongName name;\n};\n\nclass MusicButton final : public Ui::RippleButton {\npublic:\n\tMusicButton(QWidget *parent, MusicButtonData data, Fn<void()> handler);\n\t~MusicButton();\n\n\tvoid updateData(MusicButtonData data);\n\tvoid setOverrideBg(std::optional<QColor> color);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tint resizeGetHeight(int newWidth) override;\n\n\tUi::Text::String _performer;\n\tUi::Text::String _title;\n\tstd::optional<QColor> _overrideBg;\n\n\tconst QString _noteSymbol;\n\tconst int _noteWidth;\n\n};\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_phone_menu.h\"\n\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_app_config_values.h\"\n#include \"main/main_session.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"styles/style_chat.h\" // expandedMenuSeparator.\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Info {\nnamespace Profile {\nnamespace {\n\nclass TextItem final : public Ui::Menu::ItemBase {\npublic:\n\tTextItem(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::Menu &st,\n\t\trpl::producer<TextWithEntities> &&text);\n\n\tnot_null<QAction*> action() const override;\n\tbool isEnabled() const override;\n\nprotected:\n\tint contentHeight() const override;\n\nprivate:\n\tconst base::unique_qptr<Ui::FlatLabel> _label;\n\tconst not_null<QAction*> _dummyAction;\n\n};\n\n[[nodiscard]] int CountMinWidthForHeight(\n\t\tnot_null<Ui::FlatLabel*> label,\n\t\tint basicWidth,\n\t\tint heightLimit) {\n\tconst auto height = [&](int width) {\n\t\tlabel->resizeToWidth(width);\n\t\treturn label->height();\n\t};\n\tauto widthMin = basicWidth;\n\tauto widthMax = label->textMaxWidth();\n\tif (height(widthMin) <= heightLimit || height(widthMax) > heightLimit) {\n\t\treturn basicWidth;\n\t}\n\twhile (widthMin + 1 < widthMax) {\n\t\tconst auto middle = (widthMin + widthMax) / 2;\n\t\tif (height(middle) > heightLimit) {\n\t\t\twidthMin = middle;\n\t\t} else {\n\t\t\twidthMax = middle;\n\t\t}\n\t}\n\treturn widthMax;\n}\n\nTextItem::TextItem(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::Menu &st,\n\trpl::producer<TextWithEntities> &&text)\n: ItemBase(parent, st)\n, _label(base::make_unique_q<Ui::FlatLabel>(\n\tthis,\n\tstd::move(text),\n\tst::historyMessagesTTLLabel))\n, _dummyAction(Ui::CreateChild<QAction>(parent.get())) {\n\t// Try to fit the phrase in two lines.\n\tconst auto limit = st::historyMessagesTTLLabel.style.font->height * 2;\n\tconst auto min1 = st::historyMessagesTTLLabel.minWidth;\n\tconst auto min2 = CountMinWidthForHeight(_label.get(), min1, limit);\n\tconst auto added = st.itemPadding.left() + st.itemPadding.right();\n\tsetMinWidth(std::max(min1, min2) + added);\n\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tif (s.width() <= added) {\n\t\t\treturn;\n\t\t}\n\t\t_label->resizeToWidth(s.width() - added);\n\t\t_label->moveToLeft(\n\t\t\tst.itemPadding.left(),\n\t\t\t(s.height() - _label->height()) / 2);\n\t}, lifetime());\n\n\t_label->resizeToWidth(parent->width() - added);\n\tfitToMenuWidth();\n}\n\nnot_null<QAction*> TextItem::action() const {\n\treturn _dummyAction;\n}\n\nbool TextItem::isEnabled() const {\n\treturn false;\n}\n\nint TextItem::contentHeight() const {\n\treturn _label->height();\n}\n\n} // namespace\n\nbool IsCollectiblePhone(not_null<UserData*> user) {\n\tusing Strings = std::vector<QString>;\n\tconst auto prefixes = user->session().appConfig().get<Strings>(\n\t\tu\"fragment_prefixes\"_q,\n\t\tStrings{ u\"888\"_q });\n\tconst auto phone = user->phone();\n\tconst auto proj = [&](const QString &p) {\n\t\treturn phone.startsWith(p);\n\t};\n\treturn ranges::any_of(prefixes, proj);\n}\n\nvoid AddPhoneMenu(not_null<Ui::PopupMenu*> menu, not_null<UserData*> user) {\n\tif (user->isSelf() || !IsCollectiblePhone(user)) {\n\t\treturn;\n\t} else if (const auto url = AppConfig::FragmentLink(&user->session())) {\n\t\tmenu->addSeparator(&st::expandedMenuSeparator);\n\t\tconst auto link = tr::link(\n\t\t\ttr::lng_info_mobile_context_menu_fragment_about_link(tr::now),\n\t\t\t*url);\n\t\tmenu->addAction(base::make_unique_q<TextItem>(\n\t\t\tmenu->menu(),\n\t\t\tst::reactionMenu.menu,\n\t\t\ttr::lng_info_mobile_context_menu_fragment_about(\n\t\t\t\tlt_link,\n\t\t\t\trpl::single(link),\n\t\t\t\ttr::rich)));\n\t}\n}\n\n} // namespace Profile\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_phone_menu.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass UserData;\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Info {\nnamespace Profile {\n\n[[nodiscard]] bool IsCollectiblePhone(not_null<UserData*> user);\n\nvoid AddPhoneMenu(not_null<Ui::PopupMenu*> menu, not_null<UserData*> user);\n\n} // namespace Profile\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_status_label.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_status_label.h\"\n\n#include \"data/data_peer_values.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"base/unixtime.h\"\n\nnamespace Info::Profile {\nnamespace {\n\n[[nodiscard]] auto MembersStatusText(int count) {\n\treturn tr::lng_chat_status_members(tr::now, lt_count_decimal, count);\n};\n\n[[nodiscard]] auto OnlineStatusText(int count) {\n\treturn tr::lng_chat_status_online(tr::now, lt_count_decimal, count);\n};\n\n[[nodiscard]] auto ChatStatusText(\n\t\tint fullCount,\n\t\tint onlineCount,\n\t\tbool isGroup) {\n\tif (onlineCount > 1 && onlineCount <= fullCount) {\n\t\treturn tr::lng_chat_status_members_online(\n\t\t\ttr::now,\n\t\t\tlt_members_count,\n\t\t\tMembersStatusText(fullCount),\n\t\t\tlt_online_count,\n\t\t\tOnlineStatusText(onlineCount));\n\t} else if (fullCount > 0) {\n\t\treturn isGroup\n\t\t\t? tr::lng_chat_status_members(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\tfullCount)\n\t\t\t: tr::lng_chat_status_subscribers(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\tfullCount);\n\t}\n\treturn isGroup\n\t\t? tr::lng_group_status(tr::now)\n\t\t: tr::lng_channel_status(tr::now);\n};\n\n[[nodiscard]] auto ChannelTypeText(not_null<ChannelData*> channel) {\n\tconst auto isPublic = channel->isPublic();\n\tconst auto isMegagroup = channel->isMegagroup();\n\treturn (isPublic\n\t\t? (isMegagroup\n\t\t\t? tr::lng_create_public_group_title(tr::now)\n\t\t\t: tr::lng_create_public_channel_title(tr::now))\n\t\t: (isMegagroup\n\t\t\t? tr::lng_create_private_group_title(tr::now)\n\t\t\t: tr::lng_create_private_channel_title(tr::now))).toLower();\n};\n\n} // namespace\n\nStatusLabel::StatusLabel(\n\tnot_null<Ui::FlatLabel*> label,\n\tnot_null<PeerData*> peer)\n: _label(label)\n, _peer(peer)\n, _refreshTimer([=] { refresh(); }) {\n}\n\nvoid StatusLabel::setOnlineCount(int count) {\n\t_onlineCount = count;\n\trefresh();\n}\n\nvoid StatusLabel::refresh() {\n\tauto hasMembersLink = [&] {\n\t\tif (auto megagroup = _peer->asMegagroup()) {\n\t\t\treturn megagroup->canViewMembers();\n\t\t}\n\t\treturn false;\n\t}();\n\tauto statusText = [&]() -> TextWithEntities {\n\t\tusing namespace Ui::Text;\n\t\tauto currentTime = base::unixtime::now();\n\t\tif (auto user = _peer->asUser()) {\n\t\t\tconst auto result = Data::OnlineTextFull(user, currentTime);\n\t\t\tconst auto showOnline = Data::OnlineTextActive(\n\t\t\t\tuser,\n\t\t\t\tcurrentTime);\n\t\t\tconst auto updateIn = Data::OnlineChangeTimeout(\n\t\t\t\tuser,\n\t\t\t\tcurrentTime);\n\t\t\tif (showOnline) {\n\t\t\t\t_refreshTimer.callOnce(updateIn);\n\t\t\t}\n\t\t\treturn (showOnline && _colorized)\n\t\t\t\t? Ui::Text::Colorized(result)\n\t\t\t\t: TextWithEntities{ .text = result };\n\t\t} else if (auto chat = _peer->asChat()) {\n\t\t\tif (!chat->amIn()) {\n\t\t\t\treturn tr::lng_chat_status_unaccessible(\n\t\t\t\t\t{},\n\t\t\t\t\tWithEntities);\n\t\t\t}\n\t\t\tconst auto onlineCount = _onlineCount;\n\t\t\tconst auto fullCount = std::max(\n\t\t\t\tchat->count,\n\t\t\t\tint(chat->participants.size()));\n\t\t\treturn { .text = ChatStatusText(\n\t\t\t\tfullCount,\n\t\t\t\tonlineCount,\n\t\t\t\ttrue) };\n\t\t} else if (auto broadcast = _peer->monoforumBroadcast()) {\n\t\t\tif (!broadcast->membersCountKnown()) {\n\t\t\t\treturn TextWithEntities{ .text = ChannelTypeText(broadcast) };\n\t\t\t}\n\t\t\tauto result = ChatStatusText(\n\t\t\t\tbroadcast->membersCount(),\n\t\t\t\t0,\n\t\t\t\tfalse);\n\t\t\treturn TextWithEntities{ .text = result };\n\t\t} else if (auto channel = _peer->asChannel()) {\n\t\t\tif (!channel->membersCountKnown()) {\n\t\t\t\tauto result = ChannelTypeText(channel);\n\t\t\t\treturn hasMembersLink\n\t\t\t\t\t? tr::link(result)\n\t\t\t\t\t: TextWithEntities{ .text = result };\n\t\t\t}\n\t\t\tconst auto onlineCount = _onlineCount;\n\t\t\tconst auto fullCount = channel->membersCount();\n\t\t\tauto result = ChatStatusText(\n\t\t\t\tfullCount,\n\t\t\t\tonlineCount,\n\t\t\t\tchannel->isMegagroup());\n\t\t\treturn hasMembersLink\n\t\t\t\t? tr::link(result)\n\t\t\t\t: TextWithEntities{ .text = result };\n\t\t}\n\t\treturn tr::lng_chat_status_unaccessible(tr::now, WithEntities);\n\t}();\n\t_label->setMarkedText(statusText);\n\tif (hasMembersLink && _membersLinkCallback) {\n\t\t_label->setLink(\n\t\t\t1,\n\t\t\tstd::make_shared<LambdaClickHandler>(_membersLinkCallback));\n\t}\n}\n\nvoid StatusLabel::setMembersLinkCallback(Fn<void()> callback) {\n\t_membersLinkCallback = std::move(callback);\n}\n\nFn<void()> StatusLabel::membersLinkCallback() const {\n\treturn _membersLinkCallback;\n}\n\nvoid StatusLabel::setColorized(bool enabled) {\n\t_colorized = enabled;\n\trefresh();\n}\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_status_label.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n\nnamespace Ui {\nclass FlatLabel;\n} // namespace Ui\n\nnamespace Info::Profile {\n\nclass StatusLabel final {\npublic:\n\tStatusLabel(\n\t\tnot_null<Ui::FlatLabel*> label,\n\t\tnot_null<PeerData*> peer);\n\n\tvoid refresh();\n\tvoid setMembersLinkCallback(Fn<void()> callback);\n\t[[nodiscard]] Fn<void()> membersLinkCallback() const;\n\tvoid setOnlineCount(int count);\n\tvoid setColorized(bool enabled);\n\nprivate:\n\tconst not_null<Ui::FlatLabel*> _label;\n\tconst not_null<PeerData*> _peer;\n\tint _onlineCount = 0;\n\tbool _colorized = true;\n\tFn<void()> _membersLinkCallback;\n\tbase::Timer _refreshTimer;\n\n};\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_text.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_text.h\"\n\n#include <rpl/before_next.h>\n#include <rpl/filter.h>\n#include <rpl/after_next.h>\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info {\nnamespace Profile {\n\nTextWithLabel CreateTextWithLabel(\n\t\tQWidget *parent,\n\t\trpl::producer<TextWithEntities> &&label,\n\t\trpl::producer<TextWithEntities> &&text,\n\t\tconst style::FlatLabel &labelSt,\n\t\tconst style::FlatLabel &textSt,\n\t\tconst style::margins &padding) {\n\tauto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\tparent,\n\t\tobject_ptr<Ui::VerticalLayout>(parent),\n\t\tpadding);\n\tresult->setDuration(st::infoSlideDuration);\n\tauto layout = result->entity();\n\tauto nonEmptyText = rpl::duplicate(\n\t\ttext\n\t) | rpl::before_next([slide = result.data()](\n\t\t\tconst TextWithEntities &value) {\n\t\tif (value.text.isEmpty()) {\n\t\t\tslide->hide(anim::type::normal);\n\t\t}\n\t}) | rpl::filter([](const TextWithEntities &value) {\n\t\treturn !value.text.isEmpty();\n\t}) | rpl::after_next([slide = result.data()](\n\t\t\tconst TextWithEntities &value) {\n\t\tslide->show(anim::type::normal);\n\t});\n\tconst auto labeled = layout->add(object_ptr<Ui::FlatLabel>(\n\t\tlayout,\n\t\tstd::move(nonEmptyText),\n\t\ttextSt));\n\tstd::move(text) | rpl::on_next([=] {\n\t\tlabeled->resizeToWidth(layout->width());\n\t}, labeled->lifetime());\n\tlabeled->setSelectable(true);\n\tlayout->add(Ui::CreateSkipWidget(layout, st::infoLabelSkip));\n\tconst auto subtext = layout->add(object_ptr<Ui::FlatLabel>(\n\t\tlayout,\n\t\tstd::move(\n\t\t\tlabel\n\t\t) | rpl::after_next([=] {\n\t\t\tlayout->resizeToWidth(layout->widthNoMargins());\n\t\t}),\n\t\tlabelSt));\n\tresult->finishAnimating();\n\treturn { std::move(result), labeled, subtext };\n}\n\n} // namespace Profile\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_text.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\n#include <rpl/producer.h>\n\nnamespace style {\nstruct FlatLabel;\n} // namespace style\n\nnamespace Ui {\nclass VerticalLayout;\nclass FlatLabel;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Info {\nnamespace Profile {\n\nstruct TextWithLabel {\n\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>> wrap;\n\tnot_null<Ui::FlatLabel*> text;\n\tnot_null<Ui::FlatLabel*> subtext;\n};\n\nTextWithLabel CreateTextWithLabel(\n\tQWidget *parent,\n\trpl::producer<TextWithEntities> &&label,\n\trpl::producer<TextWithEntities> &&text,\n\tconst style::FlatLabel &labelSt,\n\tconst style::FlatLabel &textSt,\n\tconst style::margins &padding);\n\n} // namespace Profile\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_top_bar.h\"\n\n#include \"api/api_peer_colors.h\"\n#include \"api/api_peer_photo.h\"\n#include \"api/api_user_privacy.h\"\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/timer.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/peers/edit_peer_info_box.h\" // EditPeerInfoBox::Available.\n#include \"boxes/peers/edit_forum_topic_box.h\"\n#include \"boxes/moderate_messages_box.h\"\n#include \"boxes/report_messages_box.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"calls/calls_instance.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"core/application.h\"\n#include \"core/shortcuts.h\"\n#include \"data/components/recent_shared_media_gifts.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_document.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_star_gift.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_user.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/notify/data_peer_notify_settings.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"editor/photo_editor_common.h\"\n#include \"editor/photo_editor_layer_widget.h\"\n#include \"history/history.h\"\n#include \"info/info_memento.h\"\n#include \"info/profile/info_profile_badge_tooltip.h\"\n#include \"info/profile/info_profile_badge.h\"\n#include \"info/profile/info_profile_cover.h\" // LargeCustomEmojiMargins\n#include \"info/profile/info_profile_status_label.h\"\n#include \"info/profile/info_profile_top_bar_action_button.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"info/userpic/info_userpic_emoji_builder_common.h\"\n#include \"info/userpic/info_userpic_emoji_builder_common.h\"\n#include \"info/userpic/info_userpic_emoji_builder_menu_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_animation.h\"\n#include \"lottie/lottie_multi_player.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_mute.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"settings/sections/settings_information.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/boxes/show_or_premium_box.h\"\n#include \"ui/color_contrast.h\"\n#include \"ui/controls/stars_rating.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/upload_progress_overlay.h\"\n#include \"ui/effects/outline_segments.h\"\n#include \"ui/effects/round_checkbox.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/peer/video_userpic_player.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/top_background_gradient.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/horizontal_fit_container.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\n#include <QtGui/QClipboard>\n#include <QtGui/QGuiApplication>\n\nnamespace Info::Profile {\nnamespace {\n\nclass Userpic final\n\t: public Ui::AbstractButton\n\t, public Ui::AbstractTooltipShower {\npublic:\n\tUserpic(QWidget *parent, Fn<bool()> hasStories)\n\t: Ui::AbstractButton(parent)\n\t, _hasStories(std::move(hasStories)) {\n\t\tinstallEventFilter(this);\n\t\tsetAccessibleName(tr::lng_mediaview_profile_photo(tr::now));\n\t}\n\n\tQString tooltipText() const override {\n\t\treturn _hasStories() ? tr::lng_view_button_story(tr::now) : QString();\n\t}\n\n\tQPoint tooltipPos() const override {\n\t\treturn QCursor::pos();\n\t}\n\n\tbool tooltipWindowActive() const override {\n\t\treturn Ui::AppInFocus() && Ui::InFocusChain(window());\n\t}\n\nprotected:\n\tbool eventFilter(QObject *obj, QEvent *e) override {\n\t\tif (obj == this && e->type() == QEvent::Enter && _hasStories()) {\n\t\t\tUi::Tooltip::Show(1000, this);\n\t\t}\n\t\treturn Ui::AbstractButton::eventFilter(obj, e);\n\t}\n\nprivate:\n\tFn<bool()> _hasStories;\n\n};\n\nconstexpr auto kWaitBeforeGiftBadge = crl::time(1000);\nconstexpr auto kGiftBadgeGlares = 3;\nconstexpr auto kMinPatternRadius = 8;\nconstexpr auto kMinContrast = 5.5;\nconstexpr auto kStoryOutlineFadeEnd = 0.4;\nconstexpr auto kStoryOutlineFadeRange = 1. - kStoryOutlineFadeEnd;\n\nusing AnimatedPatternPoint = TopBar::AnimatedPatternPoint;\n\nstruct PatternColors {\n\tQColor patternColor;\n\tbool useOverlayBlend = false;\n};\n\n[[nodiscard]] PatternColors CalculatePatternColors(\n\t\tconst std::optional<Data::ColorProfileSet> &colorProfile,\n\t\tconst std::shared_ptr<Data::EmojiStatusCollectible> &collectible,\n\t\tconst std::optional<QColor> &edgeColor,\n\t\tbool isDark) {\n\tif (collectible && collectible->patternColor.isValid()) {\n\t\tauto blended = Ui::BlendColors(\n\t\t\tcollectible->patternColor,\n\t\t\tQt::black,\n\t\t\tisDark ? (140. / 255) : (160. / 255));\n\t\tauto result = !edgeColor\n\t\t\t? std::move(blended)\n\t\t\t: (Ui::CountContrast(blended, *edgeColor)\n\t\t\t\t> Ui::CountContrast(collectible->patternColor, *edgeColor))\n\t\t\t? std::move(blended)\n\t\t\t: collectible->patternColor;\n\t\treturn {\n\t\t\t.patternColor = std::move(result),\n\t\t\t// .patternColor = collectible->patternColor.lighter(isDark\n\t\t\t// \t? 140\n\t\t\t// \t: 160),\n\t\t\t.useOverlayBlend = false\n\t\t};\n\t}\n\tif (colorProfile && !colorProfile->bg.empty()) {\n\t\treturn {\n\t\t\t.patternColor = QColor(0, 0, 0, int(0.6 * 255)),\n\t\t\t.useOverlayBlend = true\n\t\t};\n\t}\n\tconst auto baseWhite = isDark ? 0.5 : 0.3;\n\treturn {\n\t\t.patternColor = QColor::fromRgbF(\n\t\t\tbaseWhite,\n\t\t\tbaseWhite,\n\t\t\tbaseWhite,\n\t\t\t0.6),\n\t\t.useOverlayBlend = false\n\t};\n}\n\n[[nodiscard]] std::vector<AnimatedPatternPoint> GenerateAnimatedPattern(\n\t\tconst QRect &userpicRect) {\n\tauto points = std::vector<TopBar::AnimatedPatternPoint>();\n\tpoints.reserve(18); // 6 + 6 + 4 + 2.\n\tconst auto ax = float64(userpicRect.x());\n\tconst auto ay = float64(userpicRect.y());\n\tconst auto aw = float64(userpicRect.width());\n\tconst auto ah = float64(userpicRect.height());\n\tconst auto acx = ax + aw / 2.;\n\tconst auto acy = ay + ah / 2.;\n\n\tconstexpr auto kPaddingScale = 0.8;\n\n\tconst auto padding24 = style::ConvertFloatScale(24. * kPaddingScale);\n\tconst auto padding16 = style::ConvertFloatScale(16. * kPaddingScale);\n\tconst auto padding8 = style::ConvertFloatScale(8. * kPaddingScale);\n\tconst auto padding12 = style::ConvertFloatScale(12. * kPaddingScale);\n\tconst auto padding48 = style::ConvertFloatScale(48. * kPaddingScale);\n\tconst auto padding96 = style::ConvertFloatScale(96. * kPaddingScale);\n\tstatic const auto kCos120 = std::cos(M_PI * 120. / 180.);\n\tstatic const auto kCos160 = std::cos(M_PI * 160. / 180.);\n\tconst auto r48Cos120 = (padding48 + aw / 2.) * kCos120;\n\tconst auto r16Cos160 = (padding16 + ah / 2.) * kCos160;\n\n\t// First ring.\n\tpoints.push_back({ { acx, ay - padding24 }, 20, 0.02, 0.42 });\n\tpoints.push_back({ { acx, ay + ah + padding24 }, 20, 0.00, 0.32 });\n\tpoints.push_back({ { ax - padding16, acy - ah / 4 - padding8 }, 23, 0.00, 0.40 });\n\tpoints.push_back({ { ax + aw + padding16, acy - ah / 4 - padding8 }, 18, 0.00, 0.40 });\n\tpoints.push_back({ { ax - padding16, acy + ah / 4 + padding8 }, 24, 0.00, 0.40 });\n\tpoints.push_back({ { ax + aw + padding16 - 4, acy + ah / 4 + padding8 }, 24, 0.00, 0.40 });\n\n\t// Second ring.\n\tpoints.push_back({ { ax - padding48, acy }, 19, 0.14, 0.60 });\n\tpoints.push_back({ { ax + aw + padding48, acy }, 19, 0.16, 0.64 });\n\tpoints.push_back({ { acx + r48Cos120, ay - padding48 + padding12 }, 17, 0.14, 0.70 });\n\tpoints.push_back({ { acx - r48Cos120, ay - padding48 + padding12 }, 17, 0.14, 0.90 });\n\tpoints.push_back({ { acx + r48Cos120, ay + ah + padding48 - padding12 }, 20, 0.20, 0.75 });\n\tpoints.push_back({ { acx - r48Cos120, ay + ah + padding48 - padding12 }, 20, 0.20, 0.85 });\n\n\t// Third ring.\n\tpoints.push_back({ { ax - padding48 - padding8, acy + r16Cos160 }, 20, 0.09, 0.45 });\n\tpoints.push_back({ { ax + aw + padding48 + padding8, acy + r16Cos160 }, 19, 0.09, 0.45 });\n\tpoints.push_back({ { ax - padding48 - padding8, acy - r16Cos160 }, 21, 0.09, 0.45 });\n\tpoints.push_back({ { ax + aw + padding48 + padding8, acy - r16Cos160 }, 18, 0.11, 0.45 });\n\n\t// Fourth ring.\n\tpoints.push_back({ { ax - padding96, acy }, 19, 0.14, 0.75 });\n\tpoints.push_back({ { ax + aw + padding96, acy }, 19, 0.20, 0.80 });\n\n\treturn points;\n}\n\n} // namespace\n\nTopBar::TopBar(\n\tnot_null<Ui::RpWidget*> parent,\n\tDescriptor descriptor)\n: RpWidget(parent)\n, _peer(descriptor.peer\n\t? descriptor.peer\n\t: descriptor.key.peer())\n, _topic(descriptor.key.topic())\n, _key(descriptor.key)\n, _wrap(std::move(descriptor.wrap))\n, _st(st::infoTopBar)\n, _source(descriptor.source)\n, _badgeTooltipHide(\n\tstd::make_unique<base::Timer>([=] { hideBadgeTooltip(); }))\n, _botVerify(std::make_unique<Badge>(\n\tthis,\n\tst::infoBotVerifyBadge,\n\t&_peer->session(),\n\tBotVerifyBadgeForPeer(_peer),\n\tnullptr,\n\tFn<bool()>([=, controller = descriptor.controller] {\n\t\treturn controller->isGifPausedAtLeastFor(\n\t\t\tWindow::GifPauseReason::Layer);\n\t})))\n, _badgeContent(BadgeContentForPeer(_peer))\n, _gifPausedChecker([=, controller = descriptor.controller] {\n\treturn controller->isGifPausedAtLeastFor(Window::GifPauseReason::Layer);\n})\n, _badge(std::make_unique<Badge>(\n\tthis,\n\tst::infoPeerBadge,\n\t&_peer->session(),\n\t_badgeContent.value(),\n\tnullptr,\n\t_gifPausedChecker))\n, _verified(std::make_unique<Badge>(\n\tthis,\n\tst::infoPeerBadge,\n\t&_peer->session(),\n\tVerifiedContentForPeer(_peer),\n\tnullptr,\n\t_gifPausedChecker))\n, _hasActions(descriptor.source != Source::Stories\n\t&& descriptor.source != Source::Preview\n\t&& (_wrap.current() != Wrap::Side || !_peer->isNotificationsUser()))\n, _minForProgress([&] {\n\tQWidget::setMinimumHeight(st::infoLayerTopBarHeight);\n\tQWidget::setMaximumHeight(_hasActions\n\t\t? st::infoProfileTopBarHeightMax\n\t\t: st::infoProfileTopBarNoActionsHeightMax);\n\treturn QWidget::minimumHeight()\n\t\t+ (!_hasActions\n\t\t\t? 0\n\t\t\t: st::infoProfileTopBarActionButtonsHeight);\n}())\n, _title(this, nameValue(), _st.title)\n, _starsRating(_peer->isUser()\n\t? std::make_unique<Ui::StarsRating>(\n\t\tthis,\n\t\tdescriptor.controller->uiShow(),\n\t\t_peer->isSelf() ? QString() : _peer->shortName(),\n\t\tData::StarsRatingValue(_peer),\n\t\t(_peer->isSelf()\n\t\t\t? [=] { return _peer->owner().pendingStarsRating(); }\n\t\t\t: Fn<Data::StarsRatingPending()>()))\n\t: nullptr)\n, _status(this, QString(), statusStyle())\n, _statusLabel(std::make_unique<StatusLabel>(_status.data(), _peer))\n, _showLastSeen(\n\tthis,\n\tobject_ptr<Ui::RoundButton>(\n\t\tthis,\n\t\ttr::lng_status_lastseen_when(),\n\t\tst::infoProfileTopBarShowLastSeen))\n, _forumButton([&, controller = descriptor.controller] {\n\tconst auto topic = _key.topic();\n\tif (!topic) {\n\t\treturn object_ptr<Ui::RoundButton>{ nullptr };\n\t}\n\tauto owned = object_ptr<Ui::RoundButton>(\n\t\tthis,\n\t\trpl::single(QString()),\n\t\tst::infoProfileTopBarTopicStatusButton);\n\towned->setText(Info::Profile::NameValue(\n\t\t_peer\n\t) | rpl::map([=](const QString &name) {\n\t\treturn TextWithEntities(name)\n\t\t\t.append(' ')\n\t\t\t.append(Ui::Text::IconEmoji(&st::textMoreIconEmoji, QString()));\n\t}));\n\towned->setClickedCallback([=, peer = _peer] {\n\t\tif (const auto forum = peer->forum()) {\n\t\t\tif (peer->useSubsectionTabs()) {\n\t\t\t\tcontroller->searchInChat(forum->history());\n\t\t\t} else if (controller->adaptive().isOneColumn()) {\n\t\t\t\tcontroller->showForum(forum);\n\t\t\t} else {\n\t\t\t\tcontroller->showPeerHistory(peer->id);\n\t\t\t}\n\t\t} else {\n\t\t\tcontroller->showPeerHistory(peer->id);\n\t\t}\n\t});\n\treturn owned;\n}())\n, _backToggles(std::move(descriptor.backToggles)) {\n\t_peer->updateFull();\n\tif (const auto broadcast = _peer->monoforumBroadcast()) {\n\t\tbroadcast->updateFull();\n\t}\n\tconst auto controller = descriptor.controller;\n\n\tif (_peer->isMegagroup() || _peer->isChat()) {\n\t\t_statusLabel->setMembersLinkCallback([=, peer = _peer] {\n\t\t\tconst auto topic = _key.topic();\n\t\t\tconst auto sublist = _key.sublist();\n\t\t\tconst auto shown = sublist\n\t\t\t\t? sublist->sublistPeer().get()\n\t\t\t\t: peer.get();\n\t\t\tconst auto section = Section::Type::Members;\n\t\t\tcontroller->showSection(topic\n\t\t\t\t? std::make_shared<Info::Memento>(topic, section)\n\t\t\t\t: std::make_shared<Info::Memento>(shown, section));\n\t\t});\n\t}\n\tif (!_peer->isMegagroup() && !_topic) {\n\t\tsetupStatusWithRating();\n\t}\n\n\tif (!_topic) {\n\t\tsetupShowLastSeen(controller);\n\t}\n\n\t_peer->session().changes().peerFlagsValue(\n\t\t_peer,\n\t\tData::PeerUpdate::Flag::OnlineStatus | Data::PeerUpdate::Flag::Members\n\t) | rpl::on_next([=] {\n\t\t_statusLabel->refresh();\n\t}, lifetime());\n\n\t_title->setSelectable(true);\n\t_title->setContextCopyText(tr::lng_profile_copy_fullname(tr::now));\n\n\tauto badgeUpdates = rpl::producer<rpl::empty_value>();\n\tif (_badge) {\n\t\tbadgeUpdates = rpl::merge(\n\t\t\tstd::move(badgeUpdates),\n\t\t\t_badge->updated());\n\n\t\t_badge->setPremiumClickCallback([controller, peer = _peer] {\n\t\t\t::Settings::ShowEmojiStatusPremium(controller, peer);\n\t\t});\n\t}\n\tif (_verified) {\n\t\tbadgeUpdates = rpl::merge(\n\t\t\tstd::move(badgeUpdates),\n\t\t\t_verified->updated());\n\t}\n\tif (_botVerify) {\n\t\tbadgeUpdates = rpl::merge(\n\t\t\tstd::move(badgeUpdates),\n\t\t\t_botVerify->updated());\n\t}\n\t_title->naturalWidthValue() | rpl::on_next([=](int w) {\n\t\t_title->resizeToWidth(w);\n\t}, _title->lifetime());\n\tbadgeUpdates = rpl::merge(\n\t\tstd::move(badgeUpdates),\n\t\tnameValue() | rpl::to_empty,\n\t\t_backToggles.value() | rpl::to_empty);\n\tstd::move(badgeUpdates) | rpl::on_next([=] {\n\t\tupdateLabelsPosition();\n\t}, _title->lifetime());\n\n\tsetupUniqueBadgeTooltip();\n\tsetupButtons(controller, descriptor.source);\n\tsetupUserpicButton(controller);\n\tif (_hasActions) {\n\t\t_peer->session().changes().peerFlagsValue(\n\t\t\t_peer,\n\t\t\tData::PeerUpdate::Flag::FullInfo\n\t\t\t\t| Data::PeerUpdate::Flag::ChannelAmIn\n\t\t) | rpl::on_next([=] {\n\t\t\tsetupActions(controller);\n\t\t}, lifetime());\n\t}\n\tsetupStoryOutline();\n\tif (_topic) {\n\t\t_topicIconView = std::make_unique<TopicIconView>(\n\t\t\t_topic,\n\t\t\t_gifPausedChecker,\n\t\t\t[=] { update(); });\n\t} else {\n\t\tupdateVideoUserpic();\n\t}\n\n\trpl::merge(\n\t\tstyle::PaletteChanged(),\n\t\t_peer->session().data().giftUpdates() | rpl::filter([=](\n\t\t\t\tconst Data::GiftUpdate &update) {\n\t\t\tif (update.action == Data::GiftUpdate::Action::Pin) {\n\t\t\t\tif (_peer->isSelf() && update.id.isUser()) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tif (_peer == update.id.chat()) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (update.action == Data::GiftUpdate::Action::Unpin) {\n\t\t\t\tfor (const auto &gift : _pinnedToTopGifts) {\n\t\t\t\t\tif (gift.manageId == update.id) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t}) | rpl::to_empty,\n\t\t_peer->session().changes().peerFlagsValue(\n\t\t\t_peer,\n\t\t\tData::PeerUpdate::Flag::EmojiStatus\n\t\t\t\t| Data::PeerUpdate::Flag::ColorProfile) | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\tif (_pinnedToTopGiftsFirstTimeShowed) {\n\t\t\t_peer->session().recentSharedGifts().clearLastRequestTime(_peer);\n\t\t\tsetupPinnedToTopGifts(controller);\n\t\t} else {\n\t\t\tupdateCollectibleStatus();\n\t\t}\n\t}, lifetime());\n\n\tstd::move(\n\t\tdescriptor.showFinished\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\tsetupPinnedToTopGifts(controller);\n\t}, lifetime());\n\n\tif (_forumButton) {\n\t\t_forumButton->show();\n\t}\n}\n\nvoid TopBar::adjustColors(const std::optional<QColor> &edgeColor) {\n\tconstexpr auto kMinContrast = 5.5;\n\tconst auto shouldOverride = [&](const style::color &color) {\n\t\treturn edgeColor\n\t\t\t&& (kMinContrast > Ui::CountContrast(color->c, *edgeColor));\n\t};\n\tconst auto collectible = effectiveCollectible();\n\tconst auto shouldOverrideTitle = shouldOverride(_title->st().textFg);\n\tconst auto shouldOverrideStatus = shouldOverrideTitle\n\t\t|| shouldOverride(_status->st().textFg);\n\t_title->setTextColorOverride(collectible\n\t\t? collectible->textColor\n\t\t: shouldOverrideTitle\n\t\t? std::optional<QColor>(st::groupCallMembersFg->c)\n\t\t: std::nullopt);\n\tif (_showLastSeen->toggled()) {\n\t\tif (shouldOverrideTitle) {\n\t\t\tconst auto st = mapActionStyle(edgeColor);\n\t\t\t_showLastSeen->entity()->setBrushOverride(st.bgColor);\n\t\t\t_showLastSeen->entity()->setTextFgOverride(st.fgColor);\n\t\t} else {\n\t\t\t_showLastSeen->entity()->setBrushOverride(std::nullopt);\n\t\t\t_showLastSeen->entity()->setTextFgOverride(std::nullopt);\n\t\t}\n\t}\n\t{\n\t\tconst auto membersLinkCallback = _statusLabel->membersLinkCallback();\n\t\t{\n\t\t\t_statusLabel = nullptr;\n\t\t\tdelete _status.release();\n\t\t}\n\t\tif (shouldOverrideStatus) {\n\t\t\tconst auto copySt = [&](const style::FlatLabel &st) {\n\t\t\t\tauto result = std::make_unique<style::FlatLabel>(\n\t\t\t\t\tbase::duplicate(st));\n\t\t\t\tresult->palette.linkFg = st::groupCallVideoSubTextFg;\n\t\t\t\treturn result;\n\t\t\t};\n\t\t\t_statusSt = copySt(statusStyle());\n\t\t\t_status.create(this, QString(), *(_statusSt.get()));\n\t\t} else {\n\t\t\t_status.create(this, QString(), statusStyle());\n\t\t}\n\t\t_status->show();\n\t\tif (!_peer->isMegagroup() && !_topic) {\n\t\t\tsetupStatusWithRating();\n\t\t}\n\t\t_status->widthValue() | rpl::on_next([=] {\n\t\t\tupdateStatusPosition(_progress.current());\n\t\t}, _status->lifetime());\n\t\t_statusLabel = std::make_unique<StatusLabel>(_status.data(), _peer);\n\t\t_statusLabel->setMembersLinkCallback(membersLinkCallback);\n\t\t_status->setTextColorOverride(collectible\n\t\t\t? collectible->textColor\n\t\t\t: shouldOverrideStatus\n\t\t\t? std::optional<QColor>(st::groupCallVideoSubTextFg->c)\n\t\t\t: std::nullopt);\n\t\t_statusLabel->setColorized(!shouldOverrideStatus);\n\t}\n\n\tconst auto shouldOverrideBadges = shouldOverride(\n\t\tst::infoBotVerifyBadge.premiumFg);\n\t_botVerify->setOverrideStyle(shouldOverrideBadges\n\t\t? _botVerifySt\n\t\t? _botVerifySt.get()\n\t\t: &st::infoColoredBotVerifyBadge\n\t\t: nullptr);\n\t_badge->setOverrideStyle(shouldOverrideBadges\n\t\t? _badgeSt\n\t\t? _badgeSt.get()\n\t\t: &st::infoColoredPeerBadge\n\t\t: nullptr);\n\t_verified->setOverrideStyle(shouldOverrideBadges\n\t\t? _verifiedSt\n\t\t? _verifiedSt.get()\n\t\t: &st::infoColoredPeerBadge\n\t\t: nullptr);\n\n\tif (_starsRating) {\n\t\tconst auto shouldOverrideRating = shouldOverride(st::windowBgActive);\n\t\t_starsRating->setCustomColors(\n\t\t\tshouldOverrideRating\n\t\t\t\t? edgeColor\n\t\t\t\t: std::nullopt,\n\t\t\tshouldOverrideRating\n\t\t\t\t? std::make_optional<QColor>(st::windowFgActive->c)\n\t\t\t\t: std::nullopt);\n\t}\n\n\t_edgeColor = edgeColor;\n}\n\nvoid TopBar::updateCollectibleStatus() {\n\tconst auto collectible = effectiveCollectible();\n\tconst auto colorProfile = effectiveColorProfile();\n\t_hasGradientBg = (collectible != nullptr)\n\t\t|| (colorProfile && colorProfile->bg.size() > 1);\n\t_solidBg = (colorProfile && colorProfile->bg.size() == 1)\n\t\t? std::make_optional(colorProfile->bg.front())\n\t\t: std::nullopt;\n\t_cachedClipPath = QPainterPath();\n\t_cachedGradient = QImage();\n\t_basePatternImage = QImage();\n\t_lastUserpicRect = QRect();\n\tconst auto patternEmojiId = _localPatternEmojiId\n\t\t? *_localPatternEmojiId\n\t\t: collectible && collectible->patternDocumentId\n\t\t? collectible->patternDocumentId\n\t\t: _peer->profileBackgroundEmojiId();\n\tif (patternEmojiId) {\n\t\tconst auto document = _peer->owner().document(patternEmojiId);\n\t\tif (!_patternEmoji\n\t\t\t|| _patternEmoji->entityData()\n\t\t\t\t!= Data::SerializeCustomEmojiId(document)) {\n\t\t\t_patternEmoji = document->owner().customEmojiManager().create(\n\t\t\t\tdocument,\n\t\t\t\t[=] { update(); },\n\t\t\t\tData::CustomEmojiSizeTag::Normal);\n\t\t}\n\t} else {\n\t\t_patternEmoji = nullptr;\n\t}\n\tif (collectible || _localPatternEmojiId) {\n\t\tsetupAnimatedPattern();\n\t} else {\n\t\t_animatedPoints.clear();\n\t\t_pinnedToTopGifts.clear();\n\t}\n\tconst auto verifiedFg = [&]() -> std::optional<QColor> {\n\t\tif (collectible) {\n\t\t\treturn Ui::BlendColors(\n\t\t\t\tcollectible->edgeColor,\n\t\t\t\tcollectible->centerColor,\n\t\t\t\t0.2);\n\t\t}\n\t\tif (colorProfile && !colorProfile->palette.empty()) {\n\t\t\treturn Ui::BlendColors(\n\t\t\t\tcolorProfile->palette.back(),\n\t\t\t\tcolorProfile->palette.size() == 1 ? Qt::white : Qt::black,\n\t\t\t\t0.2);\n\t\t}\n\t\treturn std::nullopt;\n\t}();\n\tif (verifiedFg) {\n\t\tconst auto copyStVerified = [&](const style::InfoPeerBadge &st) {\n\t\t\tauto result = std::make_unique<style::InfoPeerBadge>(\n\t\t\t\tbase::duplicate(st));\n\t\t\tauto fg = std::make_shared<style::owned_color>(*verifiedFg);\n\t\t\tresult->premiumFg = fg->color();\n\t\t\treturn std::shared_ptr<style::InfoPeerBadge>(\n\t\t\t\tresult.release(),\n\t\t\t\t[fg](style::InfoPeerBadge *ptr) { delete ptr; });\n\t\t\treturn std::shared_ptr<style::InfoPeerBadge>(result.release());\n\t\t};\n\t\tconst auto copySt = [&](const style::InfoPeerBadge &st) {\n\t\t\tauto result = std::make_unique<style::InfoPeerBadge>(\n\t\t\t\tbase::duplicate(st));\n\t\t\tresult->premiumFg = st::groupCallVideoSubTextFg;\n\t\t\treturn std::shared_ptr<style::InfoPeerBadge>(result.release());\n\t\t};\n\t\t_botVerifySt = copySt(st::infoColoredBotVerifyBadge);\n\t\t_badgeSt = copySt(st::infoColoredPeerBadge);\n\t\t_verifiedSt = copyStVerified(st::infoColoredPeerBadge);\n\t} else {\n\t\t_botVerifySt = nullptr;\n\t\t_badgeSt = nullptr;\n\t\t_verifiedSt = nullptr;\n\t}\n\tupdate();\n\tadjustColors(collectible\n\t\t? std::optional<QColor>(collectible->edgeColor)\n\t\t: (colorProfile && !colorProfile->bg.empty())\n\t\t? std::optional<QColor>(colorProfile->bg.front())\n\t\t: std::nullopt);\n}\n\nvoid TopBar::setupActions(not_null<Window::SessionController*> controller) {\n\tconst auto peer = _peer;\n\tconst auto user = peer->asUser();\n\tconst auto channel = peer->asChannel();\n\tconst auto chat = peer->asChat();\n\tconst auto topic = _key.topic();\n\tconst auto sublist = _key.sublist();\n\tconst auto isSide = (_wrap.current() == Wrap::Side);\n\n\tauto buttons = std::vector<not_null<TopBarActionButton*>>();\n\t_actions = base::make_unique_q<Ui::HorizontalFitContainer>(\n\t\tthis,\n\t\tst::infoProfileTopBarActionButtonsSpace);\n\tconst auto chechMax = [&, max = 3] {\n\t\treturn buttons.size() >= max;\n\t};\n\tconst auto addMore = [&] {\n\t\tif ([&]() -> bool {\n\t\t\tif (isSide) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto guard = gsl::finally([&] { _peerMenu = nullptr; });\n\t\t\tshowTopBarMenu(controller, true);\n\t\t\treturn _peerMenu;\n\t\t}()) {\n\t\t\tconst auto moreButton = Ui::CreateChild<TopBarActionButton>(\n\t\t\t\tthis,\n\t\t\t\ttr::lng_profile_action_short_more(tr::now),\n\t\t\t\tst::infoProfileTopBarActionMore);\n\t\t\tmoreButton->setClickedCallback([=] {\n\t\t\t\tshowTopBarMenu(controller, false);\n\t\t\t});\n\t\t\tmoreButton->setAccessibleName(tr::lng_profile_action_short_more(tr::now));\n\t\t\t_actionMore = moreButton;\n\t\t\t_actions->add(moreButton);\n\t\t\tbuttons.push_back(moreButton);\n\t\t}\n\t};\n\tconst auto guard = gsl::finally([&] {\n\t\taddMore();\n\t\tstyle::PaletteChanged(\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto current = _edgeColor.current();\n\t\t\t_edgeColor.force_assign(current);\n\t\t}, _actions->lifetime());\n\t\t_edgeColor.value() | rpl::map([=](std::optional<QColor> c) {\n\t\t\treturn mapActionStyle(c);\n\t\t}) | rpl::on_next([=](\n\t\t\t\tTopBarActionButtonStyle st) {\n\t\t\tfor (const auto &button : buttons) {\n\t\t\t\tbutton->setStyle(st);\n\t\t\t}\n\t\t}, _actions->lifetime());\n\t\tconst auto padding = st::infoProfileTopBarActionButtonsPadding;\n\t\tsizeValue() | rpl::on_next([=](const QSize &size) {\n\t\t\tconst auto ratio = float64(size.height())\n\t\t\t\t/ (st::infoProfileTopBarActionButtonsHeight\n\t\t\t\t\t+ st::infoLayerTopBarHeight);\n\t\t\tconst auto h = st::infoProfileTopBarActionButtonSize;\n\t\t\tconst auto resultHeight = (ratio >= 1.)\n\t\t\t\t? h\n\t\t\t\t: (ratio <= 0.5)\n\t\t\t\t? 0\n\t\t\t\t: int(h * (ratio - 0.5) / 0.5);\n\t\t\t_actions->setGeometry(\n\t\t\t\tpadding.left(),\n\t\t\t\tsize.height() - resultHeight - padding.bottom(),\n\t\t\t\tsize.width() - rect::m::sum::h(padding),\n\t\t\t\tresultHeight);\n\t\t}, _actions->lifetime());\n\t\t_actions->show();\n\t\t_actions->raise();\n\t});\n\tif (user) {\n\t\tconst auto message = Ui::CreateChild<TopBarActionButton>(\n\t\t\tthis,\n\t\t\ttr::lng_profile_action_short_message(tr::now),\n\t\t\tst::infoProfileTopBarActionMessage);\n\t\tmessage->setClickedCallback([=, window = controller] {\n\t\t\twindow->showPeerHistory(\n\t\t\t\tpeer->id,\n\t\t\t\tWindow::SectionShow::Way::Forward);\n\t\t});\n\t\tmessage->setAccessibleName(tr::lng_profile_action_short_message(tr::now));\n\t\tbuttons.push_back(message);\n\t\t_actions->add(message);\n\t}\n\tconst auto canJoin = (!sublist && !topic && channel && !channel->amIn());\n\tif (canJoin) {\n\t\tconst auto join = Ui::CreateChild<TopBarActionButton>(\n\t\t\tthis,\n\t\t\ttr::lng_profile_action_short_join(tr::now),\n\t\t\tst::infoProfileTopBarActionJoin);\n\t\tjoin->setClickedCallback([=] {\n\t\t\tchannel->session().api().joinChannel(channel);\n\t\t});\n\t\tjoin->setAccessibleName(tr::lng_profile_action_short_join(tr::now));\n\t\tbuttons.push_back(join);\n\t\t_actions->add(join);\n\t} else if (const auto channel = peer->monoforumBroadcast()) {\n\t\tconst auto message = Ui::CreateChild<TopBarActionButton>(\n\t\t\tthis,\n\t\t\ttr::lng_profile_action_short_channel(tr::now),\n\t\t\tst::infoProfileTopBarActionMessage);\n\t\tmessage->setClickedCallback([=, window = controller] {\n\t\t\twindow->showPeerHistory(\n\t\t\t\tchannel,\n\t\t\t\tWindow::SectionShow::Way::Forward);\n\t\t});\n\t\tmessage->setAccessibleName(tr::lng_profile_action_short_channel(tr::now));\n\t\tbuttons.push_back(message);\n\t\t_actions->add(message);\n\t}\n\tif (!peer->isSelf()) {\n\t\tconst auto notifications = Ui::CreateChild<TopBarActionButton>(\n\t\t\tthis,\n\t\t\ttr::lng_profile_action_short_mute(tr::now),\n\t\t\tst::infoProfileTopBarActionMessage);\n\t\tnotifications->setAccessibleName(tr::lng_profile_action_short_mute(tr::now));\n\t\tnotifications->convertToToggle(\n\t\t\tst::infoProfileTopBarActionUnmute,\n\t\t\tst::infoProfileTopBarActionMute,\n\t\t\tu\"profile_muting\"_q,\n\t\t\tu\"profile_unmuting\"_q);\n\n\t\tconst auto topicRootId = topic ? topic->rootId() : MsgId();\n\t\tconst auto makeThread = [=] {\n\t\t\treturn topicRootId\n\t\t\t\t? static_cast<Data::Thread*>(peer->forumTopicFor(topicRootId))\n\t\t\t\t: peer->owner().history(peer).get();\n\t\t};\n\t\t(topic\n\t\t\t? NotificationsEnabledValue(topic)\n\t\t\t: NotificationsEnabledValue(peer)\n\t\t) | rpl::on_next([=](bool enabled) {\n\t\t\tnotifications->toggle(enabled);\n\t\t\tconst auto text = enabled\n\t\t\t\t? tr::lng_profile_action_short_mute(tr::now)\n\t\t\t\t: tr::lng_profile_action_short_unmute(tr::now);\n\t\t\tnotifications->setText(text);\n\t\t\tnotifications->setAccessibleName(text);\n\t\t}, notifications->lifetime());\n\t\tnotifications->finishAnimating();\n\n\t\tnotifications->setAcceptBoth();\n\t\tconst auto notifySettings = &peer->owner().notifySettings();\n\t\t\tMuteMenu::SetupMuteMenu(\n\t\t\t\tnotifications,\n\t\t\t\tnotifications->clicks(\n\t\t\t\t) | rpl::filter([=](Qt::MouseButton button) {\n\t\t\t\t\tif (button == Qt::RightButton) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\tconst auto topic = topicRootId\n\t\t\t\t\t\t? peer->forumTopicFor(topicRootId)\n\t\t\t\t\t\t: nullptr;\n\t\t\t\t\tAssert(!topicRootId || topic != nullptr);\n\t\t\t\t\tconst auto is = topic\n\t\t\t\t\t\t? notifySettings->isMuted(topic)\n\t\t\t\t\t\t: notifySettings->isMuted(peer);\n\t\t\t\t\tif (is) {\n\t\t\t\t\t\tif (topic) {\n\t\t\t\t\t\t\tnotifySettings->update(topic, { .unmute = true });\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tnotifySettings->update(peer, { .unmute = true });\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}) | rpl::to_empty,\n\t\t\t\tmakeThread,\n\t\t\t\tcontroller->uiShow(),\n\t\t\t\t[=, skip = st::infoProfileTopBarActionMenuSkip] {\n\t\t\t\t\treturn notifications->mapToGlobal(\n\t\t\t\t\t\tQPoint(0, notifications->height() + skip));\n\t\t\t\t});\n\t\tbuttons.push_back(notifications);\n\t\t_actions->add(notifications);\n\t\t_edgeColor.value() | rpl::on_next([=](\n\t\t\t\tstd::optional<QColor> c) {\n\t\t\tnotifications->setLottieColor(c\n\t\t\t\t? (const style::color*)(nullptr)\n\t\t\t\t: &st::windowBoldFg);\n\t\t}, notifications->lifetime());\n\t}\n\tif (chechMax()) {\n\t\treturn;\n\t}\n\tif (!isSide\n\t\t&& user\n\t\t&& !user->sharedMediaInfo()\n\t\t&& !user->isInaccessible()\n\t\t&& user->callsStatus() != UserData::CallsStatus::Disabled) {\n\t\tconst auto call = Ui::CreateChild<TopBarActionButton>(\n\t\t\tthis,\n\t\t\ttr::lng_profile_action_short_call(tr::now),\n\t\t\tst::infoProfileTopBarActionCall);\n\t\tcall->setClickedCallback([=] {\n\t\t\tCore::App().calls().startOutgoingCall(user, {});\n\t\t});\n\t\tcall->setAccessibleName(tr::lng_profile_action_short_call(tr::now));\n\t\tbuttons.push_back(call);\n\t\t_actions->add(call);\n\t}\n\tif (chechMax()) {\n\t\treturn;\n\t}\n\tif (const auto chat = channel ? channel->discussionLink() : nullptr;\n\t\t\tchat && chat->isMegagroup()) {\n\t\tconst auto discuss = Ui::CreateChild<TopBarActionButton>(\n\t\t\tthis,\n\t\t\ttr::lng_profile_action_short_discuss(tr::now),\n\t\t\tst::infoProfileTopBarActionMessage);\n\t\tdiscuss->setClickedCallback([=] {\n\t\t\tif (channel->invitePeekExpires()) {\n\t\t\t\tcontroller->showToast(\n\t\t\t\t\ttr::lng_channel_invite_private(tr::now));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcontroller->showPeerHistory(\n\t\t\t\tchat,\n\t\t\t\tWindow::SectionShow::Way::Forward);\n\t\t});\n\t\tdiscuss->setAccessibleName(tr::lng_profile_action_short_discuss(tr::now));\n\t\t_actions->add(discuss);\n\t\tbuttons.push_back(discuss);\n\t}\n\tif (chechMax()) {\n\t\treturn;\n\t}\n\tif (topic ? topic->canEdit() : EditPeerInfoBox::Available(peer)) {\n\t\tconst auto manage = Ui::CreateChild<TopBarActionButton>(\n\t\t\tthis,\n\t\t\ttr::lng_profile_action_short_manage(tr::now),\n\t\t\tst::infoProfileTopBarActionManage);\n\t\tmanage->setClickedCallback([=, window = controller] {\n\t\t\tif (topic) {\n\t\t\t\twindow->show(Box(\n\t\t\t\t\tEditForumTopicBox,\n\t\t\t\t\twindow,\n\t\t\t\t\tpeer->owner().history(peer),\n\t\t\t\t\ttopic->rootId()));\n\t\t\t} else {\n\t\t\t\twindow->showEditPeerBox(peer);\n\t\t\t}\n\t\t});\n\t\tmanage->setAccessibleName(tr::lng_profile_action_short_manage(tr::now));\n\t\tbuttons.push_back(manage);\n\t\t_actions->add(manage);\n\t}\n\tif (chechMax()) {\n\t\treturn;\n\t}\n\t{\n\t\tconst auto channel = peer->asBroadcast();\n\t\tif (!user && !channel) {\n\t\t} else if (user\n\t\t\t&& (user->isInaccessible()\n\t\t\t\t|| user->isSelf()\n\t\t\t\t|| user->isBot()\n\t\t\t\t|| user->isServiceUser()\n\t\t\t\t|| user->isNotificationsUser()\n\t\t\t\t|| user->isRepliesChat()\n\t\t\t\t|| user->isVerifyCodes()\n\t\t\t\t|| !user->session().premiumCanBuy())) {\n\t\t} else if (channel\n\t\t\t&& (channel->isForbidden() || !channel->stargiftsAvailable())) {\n\t\t} else {\n\t\t\tconst auto giftButton = Ui::CreateChild<TopBarActionButton>(\n\t\t\t\tthis,\n\t\t\t\ttr::lng_profile_action_short_gift(tr::now),\n\t\t\t\tst::infoProfileTopBarActionGift);\n\t\t\tgiftButton->setClickedCallback([=] {\n\t\t\t\tUi::ShowStarGiftBox(controller, peer);\n\t\t\t});\n\t\t\tgiftButton->setAccessibleName(tr::lng_profile_action_short_gift(tr::now));\n\t\t\t_actions->add(giftButton);\n\t\t\tbuttons.push_back(giftButton);\n\t\t}\n\t}\n\tif (chechMax()) {\n\t\treturn;\n\t}\n\tif (!topic\n\t\t&& canJoin\n\t\t&& ((chat && !chat->amCreator() && !chat->hasAdminRights())\n\t\t\t|| (channel\n\t\t\t\t&& !channel->amCreator()\n\t\t\t\t&& !channel->hasAdminRights()))) {\n\t\tconst auto show = controller->uiShow();\n\t\tconst auto reportButton = Ui::CreateChild<TopBarActionButton>(\n\t\t\tthis,\n\t\t\ttr::lng_profile_action_short_report(tr::now),\n\t\t\tst::infoProfileTopBarActionReport);\n\t\treportButton->setClickedCallback([=] {\n\t\t\tShowReportMessageBox(show, peer, {}, {});\n\t\t});\n\t\treportButton->setAccessibleName(tr::lng_profile_action_short_report(tr::now));\n\t\t_actions->add(reportButton);\n\t\tbuttons.push_back(reportButton);\n\t}\n\tif (chechMax()) {\n\t\treturn;\n\t}\n\tif (!canJoin && !topic && !sublist && (chat || channel)) {\n\t\tconst auto leaveButton = Ui::CreateChild<TopBarActionButton>(\n\t\t\tthis,\n\t\t\ttr::lng_profile_action_short_leave(tr::now),\n\t\t\tst::infoProfileTopBarActionLeave);\n\t\tleaveButton->setClickedCallback(\n\t\t\tWindow::DeleteAndLeaveHandler(controller, peer));\n\t\tleaveButton->setAccessibleName(tr::lng_profile_action_short_leave(tr::now));\n\t\t_actions->add(leaveButton);\n\t\tbuttons.push_back(leaveButton);\n\t}\n}\n\nvoid TopBar::setupUserpicButton(\n\t\tnot_null<Window::SessionController*> controller) {\n\t_userpicButton = base::make_unique_q<Userpic>(\n\t\tthis,\n\t\t[=] { return _hasStories; });\n\n\tconst auto openPhoto = [=, peer = _peer] {\n\t\tif (const auto id = peer->userpicPhotoId()) {\n\t\t\tif (const auto photo = peer->owner().photo(id); photo->date()) {\n\t\t\t\tcontroller->openPhoto(photo, peer);\n\t\t\t}\n\t\t}\n\t};\n\n\t_userpicButton->setAcceptBoth(true);\n\n\tconst auto menu = _userpicButton->lifetime().make_state<\n\t\tbase::unique_qptr<Ui::PopupMenu>\n\t>();\n\tconst auto canReport = [=, peer = _peer] {\n\t\tif (!peer->hasUserpic()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto user = peer->asUser();\n\t\tif (!user) {\n\t\t\treturn false;\n\t\t} else if (user->hasPersonalPhoto()\n\t\t\t\t|| user->isSelf()\n\t\t\t\t|| user->isInaccessible()\n\t\t\t\t|| user->isRepliesChat()\n\t\t\t\t|| user->isVerifyCodes()\n\t\t\t\t|| (user->botInfo && user->botInfo->canEditInformation)\n\t\t\t\t|| user->isServiceUser()) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t};\n\n\tconst auto canChangePhoto = [=, peer = _peer] {\n\t\tif (_topicIconView) {\n\t\t\treturn false;\n\t\t}\n\t\tif (const auto user = peer->asUser()) {\n\t\t\treturn user->isContact()\n\t\t\t\t&& !user->isSelf()\n\t\t\t\t&& !user->isInaccessible()\n\t\t\t\t&& !user->isServiceUser();\n\t\t}\n\t\tif (const auto chat = peer->asChat()) {\n\t\t\treturn chat->canEditInformation();\n\t\t}\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\treturn channel->canEditInformation();\n\t\t}\n\t\treturn false;\n\t};\n\n\tconst auto canSuggestPhoto = [=, peer = _peer] {\n\t\tif (const auto user = peer->asUser()) {\n\t\t\treturn !user->isSelf()\n\t\t\t\t&& !user->isBot()\n\t\t\t\t&& !user->starsPerMessageChecked()\n\t\t\t\t&& user->owner().history(user)->lastServerMessage();\n\t\t}\n\t\treturn false;\n\t};\n\n\tconst auto hasMenu = [=] {\n\t\tif (canChangePhoto()) {\n\t\t\treturn true;\n\t\t}\n\t\tif (canSuggestPhoto()) {\n\t\t\treturn true;\n\t\t}\n\t\tif (_hasStories || canReport()) {\n\t\t\treturn !!_peer->userpicPhotoId();\n\t\t}\n\t\treturn false;\n\t};\n\n\tconst auto invalidate = [=] {\n\t\t_userpicUniqueKey = InMemoryKey();\n\t\tconst auto hasLeftButton = _peer->userpicPhotoId() || _hasStories;\n\t\t_userpicButton->setAttribute(\n\t\t\tQt::WA_TransparentForMouseEvents,\n\t\t\t!hasLeftButton && !hasMenu());\n\t\t_userpicButton->setPointerCursor(hasLeftButton);\n\t\tupdateVideoUserpic();\n\t\t_peer->session().downloaderTaskFinished(\n\t\t) | rpl::filter([=] {\n\t\t\treturn !Ui::PeerUserpicLoading(_userpicView);\n\t\t}) | rpl::on_next([=] {\n\t\t\tupdate();\n\t\t\t_userpicLoadingLifetime.destroy();\n\t\t}, _userpicLoadingLifetime);\n\t\tUi::PostponeCall(this, [=] {\n\t\t\tupdate();\n\t\t});\n\t};\n\n\trpl::single(\n\t\trpl::empty_value()\n\t) | rpl::then(\n\t\t_peer->session().changes().peerFlagsValue(\n\t\t\t_peer,\n\t\t\tData::PeerUpdate::Flag::Photo\n\t\t\t\t| Data::PeerUpdate::Flag::FullInfo) | rpl::to_empty\n\t) | rpl::on_next(invalidate, lifetime());\n\n\tif (const auto broadcast = _peer->monoforumBroadcast()) {\n\t\t_peer->session().changes().peerFlagsValue(\n\t\t\tbroadcast,\n\t\t\tData::PeerUpdate::Flag::Photo\n\t\t\t\t| Data::PeerUpdate::Flag::FullInfo\n\t\t) | rpl::to_empty | rpl::on_next(invalidate, lifetime());\n\t}\n\n\tusing ChosenType = Ui::UserpicButton::ChosenType;\n\n\tconst auto choosePhotoCallback = [=](ChosenType type) {\n\t\treturn [=](QImage &&image) {\n\t\t\tauto result = Api::PeerPhoto::UserPhoto{\n\t\t\t\tstd::move(image),\n\t\t\t\t0,\n\t\t\t\tstd::vector<QColor>(),\n\t\t\t};\n\t\t\tswitch (type) {\n\t\t\tcase ChosenType::Set:\n\t\t\t\t_peer->session().api().peerPhoto().upload(\n\t\t\t\t\t_peer,\n\t\t\t\t\tstd::move(result));\n\t\t\t\tstartUploadOverlay();\n\t\t\t\tbreak;\n\t\t\tcase ChosenType::Suggest:\n\t\t\t\t_peer->session().api().peerPhoto().suggest(\n\t\t\t\t\t_peer,\n\t\t\t\t\tstd::move(result));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t};\n\t};\n\n\tconst auto editorData = [=](ChosenType type) {\n\t\tconst auto user = _peer->asUser();\n\t\tconst auto name = (user && !user->firstName.isEmpty())\n\t\t\t? user->firstName\n\t\t\t: _peer->name();\n\t\tconst auto phrase = (type == ChosenType::Suggest)\n\t\t\t? &tr::lng_profile_suggest_sure\n\t\t\t: (user && !user->isSelf() && !_peer->isBot())\n\t\t\t? &tr::lng_profile_set_personal_sure\n\t\t\t: nullptr;\n\t\treturn Editor::EditorData{\n\t\t\t.about = (phrase\n\t\t\t\t? (*phrase)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::bold(name),\n\t\t\t\t\ttr::marked)\n\t\t\t\t: TextWithEntities()),\n\t\t\t.confirm = ((type == ChosenType::Suggest)\n\t\t\t\t? tr::lng_profile_suggest_button(tr::now)\n\t\t\t\t: tr::lng_profile_set_photo_button(tr::now)),\n\t\t\t.cropType = Editor::EditorData::CropType::Ellipse,\n\t\t\t.keepAspectRatio = true,\n\t\t};\n\t};\n\n\tconst auto chooseFile = [=](ChosenType type) {\n\t\tbase::call_delayed(\n\t\t\tst::defaultRippleAnimation.hideDuration,\n\t\t\tcrl::guard(this, [=] {\n\t\t\t\tEditor::PrepareProfilePhotoFromFile(\n\t\t\t\t\tthis,\n\t\t\t\t\t&controller->window(),\n\t\t\t\t\teditorData(type),\n\t\t\t\t\tchoosePhotoCallback(type));\n\t\t\t}));\n\t};\n\n\tconst auto addFromClipboard = [=](\n\t\t\tUi::PopupMenu *menu,\n\t\t\tChosenType type,\n\t\t\ttr::phrase<> text) {\n\t\tif (const auto data = QGuiApplication::clipboard()->mimeData()) {\n\t\t\tif (data->hasImage()) {\n\t\t\t\tauto openEditor = crl::guard(this, [=] {\n\t\t\t\t\tEditor::PrepareProfilePhoto(\n\t\t\t\t\t\tthis,\n\t\t\t\t\t\t&controller->window(),\n\t\t\t\t\t\teditorData(type),\n\t\t\t\t\t\tchoosePhotoCallback(type),\n\t\t\t\t\t\tqvariant_cast<QImage>(data->imageData()));\n\t\t\t\t});\n\t\t\t\tmenu->addAction(\n\t\t\t\t\tstd::move(text)(tr::now),\n\t\t\t\t\tstd::move(openEditor),\n\t\t\t\t\t&st::menuIconPhoto);\n\t\t\t}\n\t\t}\n\t};\n\n\t_userpicButton->clicks() | rpl::on_next([=](\n\t\t\tQt::MouseButton button) {\n\t\tif (button == Qt::RightButton && hasMenu()) {\n\t\t\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\tthis,\n\t\t\t\tst::popupMenuWithIcons);\n\n\t\t\t(*menu)->addAction(\n\t\t\t\ttr::lng_profile_open_photo(tr::now),\n\t\t\t\topenPhoto,\n\t\t\t\t&st::menuIconPhoto);\n\n\t\t\tif (canReport()) {\n\t\t\t\t(*menu)->addAction(\n\t\t\t\t\ttr::lng_profile_report(tr::now),\n\t\t\t\t\t[=] {\n\t\t\t\t\t\tcontroller->show(\n\t\t\t\t\t\t\tReportProfilePhotoBox(\n\t\t\t\t\t\t\t\t_peer,\n\t\t\t\t\t\t\t\t_peer->owner().photo(\n\t\t\t\t\t\t\t\t\t_peer->userpicPhotoId())));\n\t\t\t\t\t},\n\t\t\t\t\t&st::menuIconReport);\n\t\t\t}\n\n\t\t\tif (canChangePhoto()) {\n\t\t\t\tif (!(*menu)->empty()) {\n\t\t\t\t\t(*menu)->addSeparator(&st::expandedMenuSeparator);\n\t\t\t\t}\n\t\t\t\tconst auto isUser = _peer->asUser();\n\t\t\t\tif (isUser) {\n\t\t\t\t\t(*menu)->addAction(\n\t\t\t\t\t\ttr::lng_profile_set_photo_for(tr::now),\n\t\t\t\t\t\t[=] { chooseFile(ChosenType::Set); },\n\t\t\t\t\t\t&st::menuIconPhotoSet);\n\t\t\t\t\taddFromClipboard(\n\t\t\t\t\t\tmenu->get(),\n\t\t\t\t\t\tChosenType::Set,\n\t\t\t\t\t\ttr::lng_profile_set_photo_for_from_clipboard);\n\t\t\t\t\tif (canSuggestPhoto()) {\n\t\t\t\t\t\t(*menu)->addAction(\n\t\t\t\t\t\t\ttr::lng_profile_suggest_photo(tr::now),\n\t\t\t\t\t\t\t[=] {\n\t\t\t\t\t\t\t\tchooseFile(\n\t\t\t\t\t\t\t\t\tChosenType::Suggest);\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t&st::menuIconPhotoSuggest);\n\t\t\t\t\t\taddFromClipboard(\n\t\t\t\t\t\t\t\tmenu->get(),\n\t\t\t\t\t\t\t\tChosenType::Suggest,\n\t\t\t\t\t\t\t\ttr::lng_profile_suggest_photo_from_clipboard);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconst auto channel = _peer->asChannel();\n\t\t\t\t\tconst auto isChannel = channel && !channel->isMegagroup();\n\t\t\t\t\t(*menu)->addAction(\n\t\t\t\t\t\tisChannel\n\t\t\t\t\t\t\t? tr::lng_profile_set_photo_for_channel(tr::now)\n\t\t\t\t\t\t\t: tr::lng_profile_set_photo_for_group(tr::now),\n\t\t\t\t\t\t[=] { chooseFile(ChosenType::Set); },\n\t\t\t\t\t\t&st::menuIconPhotoSet);\n\t\t\t\t\taddFromClipboard(\n\t\t\t\t\t\tmenu->get(),\n\t\t\t\t\t\tChosenType::Set,\n\t\t\t\t\t\ttr::lng_profile_set_photo_for_from_clipboard);\n\t\t\t\t}\n\t\t\t\tif (controller && isUser) {\n\t\t\t\t\tconst auto done = [=](UserpicBuilder::Result data) {\n\t\t\t\t\t\tauto result = Api::PeerPhoto::UserPhoto{\n\t\t\t\t\t\t\tbase::take(data.image),\n\t\t\t\t\t\t\tdata.id,\n\t\t\t\t\t\t\tstd::move(data.colors),\n\t\t\t\t\t\t};\n\t\t\t\t\t\t_peer->session().api().peerPhoto().upload(\n\t\t\t\t\t\t\t_peer,\n\t\t\t\t\t\t\tstd::move(result));\n\t\t\t\t\t};\n\t\t\t\t\tUserpicBuilder::AddEmojiBuilderAction(\n\t\t\t\t\t\tcontroller,\n\t\t\t\t\t\tmenu->get(),\n\t\t\t\t\t\t_peer->session().api().peerPhoto().emojiListValue(\n\t\t\t\t\t\t\tApi::PeerPhoto::EmojiListType::Profile),\n\t\t\t\t\t\tdone,\n\t\t\t\t\t\tfalse);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!(*menu)->empty()) {\n\t\t\t\t(*menu)->popup(QCursor::pos());\n\t\t\t}\n\t\t} else if (button == Qt::LeftButton) {\n\t\t\tif (_uploadOverlay && _uploadOverlay->uploading()) {\n\t\t\t\t_peer->session().api().peerPhoto().cancelUpload(_peer);\n\t\t\t} else if (_topicIconView && _topic && _topic->iconId()) {\n\t\t\t\tconst auto document = _peer->owner().document(\n\t\t\t\t\t_topic->iconId());\n\t\t\t\tif (const auto sticker = document->sticker()) {\n\t\t\t\t\tconst auto packName\n\t\t\t\t\t\t= _peer->owner().customEmojiManager().lookupSetName(\n\t\t\t\t\t\t\tsticker->set.id);\n\t\t\t\t\tif (!packName.isEmpty()) {\n\t\t\t\t\t\tconst auto text = tr::lng_profile_topic_toast(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\t\ttr::link(packName, u\"internal:\"_q),\n\t\t\t\t\t\t\ttr::marked);\n\t\t\t\t\t\tconst auto weak = base::make_weak(controller);\n\t\t\t\t\t\tcontroller->showToast(Ui::Toast::Config{\n\t\t\t\t\t\t\t.text = text,\n\t\t\t\t\t\t\t.filter = [=, set = sticker->set](\n\t\t\t\t\t\t\t\t\tconst ClickHandlerPtr &handler,\n\t\t\t\t\t\t\t\t\tQt::MouseButton) {\n\t\t\t\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\t\t\t\tstrong->show(\n\t\t\t\t\t\t\t\t\t\tBox<StickerSetBox>(\n\t\t\t\t\t\t\t\t\t\t\tstrong->uiShow(),\n\t\t\t\t\t\t\t\t\t\t\tset,\n\t\t\t\t\t\t\t\t\t\t\tData::StickersType::Emoji));\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t.duration = crl::time(3000),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (_hasStories) {\n\t\t\t\tcontroller->openPeerStories(_peer->id);\n\t\t\t} else {\n\t\t\t\topenPhoto();\n\t\t\t}\n\t\t}\n\t}, _userpicButton->lifetime());\n}\n\nvoid TopBar::startUploadOverlay() {\n\tif (_uploadOverlay && _uploadOverlay->uploading()) {\n\t\treturn;\n\t}\n\t_waitingUserpicCloudLoad = true;\n\t_uploadOverlay = std::make_unique<Ui::UploadProgressOverlay>(\n\t\tthis,\n\t\t[=] { update(); });\n\t_uploadOverlay->start();\n\n\t_userpicButton->events(\n\t) | rpl::filter([](not_null<QEvent*> e) {\n\t\treturn e->type() == QEvent::Enter\n\t\t\t|| e->type() == QEvent::Leave;\n\t}) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (_uploadOverlay) {\n\t\t\t_uploadOverlay->setOver(e->type() == QEvent::Enter);\n\t\t}\n\t}, _uploadLifetime);\n\n\tconst auto cleanup = [=] {\n\t\t_uploadLifetime.destroy();\n\t\t_uploadOverlay = nullptr;\n\t};\n\t_peer->session().api().peerPhoto().subscribeToUpload(\n\t\t_peer,\n\t\t_uploadLifetime,\n\t\t{\n\t\t\t.progress = [=](float64 value) {\n\t\t\t\t_uploadOverlay->setProgress(value);\n\t\t\t},\n\t\t\t.done = [=] {\n\t\t\t\t_uploadOverlay->stop(cleanup);\n\t\t\t},\n\t\t\t.failed = [=] {\n\t\t\t\t_uploadOverlay->fail(cleanup);\n\t\t\t},\n\t\t});\n}\n\nvoid TopBar::setupUniqueBadgeTooltip() {\n\tif (!_badge || _source == Source::Preview) {\n\t\treturn;\n\t}\n\tbase::timer_once(kWaitBeforeGiftBadge) | rpl::then(\n\t\t_badge->updated()\n\t) | rpl::on_next([=] {\n\t\tconst auto widget = _badge->widget();\n\t\tconst auto &content = _badgeContent.current();\n\t\tconst auto &collectible = content.emojiStatusId.collectible;\n\t\tconst auto premium = (content.badge == BadgeType::Premium);\n\t\tconst auto id = (collectible && widget && premium)\n\t\t\t? collectible->id\n\t\t\t: uint64();\n\t\tif (_badgeCollectibleId == id) {\n\t\t\treturn;\n\t\t}\n\t\thideBadgeTooltip();\n\t\tif (!collectible || _localCollectible) {\n\t\t\treturn;\n\t\t}\n\t\t_badgeTooltip = std::make_unique<BadgeTooltip>(\n\t\t\tthis,\n\t\t\tcollectible,\n\t\t\twidget);\n\t\tconst auto raw = _badgeTooltip.get();\n\t\traw->fade(true);\n\t\t_badgeTooltipHide->callOnce(kGiftBadgeGlares * raw->glarePeriod()\n\t\t\t- st::infoGiftTooltip.duration * 1.5);\n\t\traw->setOpacity(_progress.current());\n\t}, lifetime());\n\n\tif (const auto raw = _badgeTooltip.get()) {\n\t\traw->finishAnimating();\n\t}\n}\n\nvoid TopBar::hideBadgeTooltip() {\n\t_badgeTooltipHide->cancel();\n\tif (auto old = base::take(_badgeTooltip)) {\n\t\tconst auto raw = old.get();\n\t\t_badgeOldTooltips.push_back(std::move(old));\n\n\t\traw->fade(false);\n\t\traw->shownValue(\n\t\t) | rpl::filter(\n\t\t\t!rpl::mappers::_1\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto i = ranges::find(\n\t\t\t\t_badgeOldTooltips,\n\t\t\t\traw,\n\t\t\t\t&std::unique_ptr<BadgeTooltip>::get);\n\t\t\tif (i != end(_badgeOldTooltips)) {\n\t\t\t\t_badgeOldTooltips.erase(i);\n\t\t\t}\n\t\t}, raw->lifetime());\n\t}\n}\n\nTopBar::~TopBar() {\n\tbase::take(_badgeTooltip);\n\tbase::take(_badgeOldTooltips);\n}\n\nrpl::producer<> TopBar::backRequest() const {\n\treturn _backClicks.events();\n}\n\nvoid TopBar::setOnlineCount(rpl::producer<int> &&count) {\n\tstd::move(count) | rpl::on_next([=](int v) {\n\t\tif (_statusLabel) {\n\t\t\t_statusLabel->setOnlineCount(v);\n\t\t}\n\t}, lifetime());\n}\n\nvoid TopBar::setRoundEdges(bool value) {\n\t_roundEdges = value;\n\tupdate();\n}\n\nvoid TopBar::setLottieSingleLoop(bool value) {\n\t_lottieSingleLoop = value;\n}\n\nvoid TopBar::setColorProfileIndex(std::optional<uint8> index) {\n\t_localColorProfileIndex = index;\n\tupdateCollectibleStatus();\n}\n\nvoid TopBar::setPatternEmojiId(std::optional<DocumentId> patternEmojiId) {\n\t_localPatternEmojiId = patternEmojiId;\n\tupdateCollectibleStatus();\n}\n\nvoid TopBar::setLocalEmojiStatusId(EmojiStatusId emojiStatusId) {\n\t_localCollectible = emojiStatusId.collectible;\n\t_badgeContent = Badge::Content{ BadgeType::Premium, emojiStatusId };\n\tupdateCollectibleStatus();\n}\n\nstd::optional<Data::ColorProfileSet> TopBar::effectiveColorProfile() const {\n\treturn _localColorProfileIndex\n\t\t? _peer->session().api().peerColors().colorProfileFor(\n\t\t\t*_localColorProfileIndex)\n\t\t: _source == Source::Preview\n\t\t? std::nullopt\n\t\t: _peer->session().api().peerColors().colorProfileFor(_peer);\n}\n\nauto TopBar::effectiveCollectible() const\n-> std::shared_ptr<Data::EmojiStatusCollectible> {\n\treturn _localCollectible\n\t\t? _localCollectible\n\t\t: _localColorProfileIndex\n\t\t? nullptr\n\t\t: _peer->emojiStatusId().collectible;\n}\n\nvoid TopBar::paintEdges(QPainter &p, const QBrush &brush) const {\n\tconst auto r = rect();\n\tif (_roundEdges) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto radius = st::boxRadius;\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(brush);\n\t\tp.drawRoundedRect(\n\t\t\tr + QMargins{ 0, 0, 0, radius + 1 },\n\t\t\tradius,\n\t\t\tradius);\n\t} else {\n\t\tp.fillRect(r, brush);\n\t}\n}\n\nvoid TopBar::paintEdges(QPainter &p) const {\n\tif (!_solidBg) {\n\t\tpaintEdges(p, st::boxDividerBg);\n\t} else {\n\t\tpaintEdges(p, *_solidBg);\n\t}\n}\n\nint TopBar::titleMostLeft() const {\n\treturn (_back && _back->toggled())\n\t\t? _back->width()\n\t\t: _st.titleWithSubtitlePosition.x();\n}\n\nint TopBar::statusMostLeft() const {\n\treturn (_back && _back->toggled())\n\t\t? _back->width()\n\t\t: _st.subtitlePosition.x();\n}\n\nint TopBar::calculateRightButtonsWidth() const {\n\tauto width = 0;\n\tif (_close) {\n\t\twidth += _close->width();\n\t}\n\tif (_topBarButton) {\n\t\twidth += _topBarButton->width();\n\t}\n\treturn width;\n}\n\nvoid TopBar::updateLabelsPosition() {\n\tif (width() <= 0) {\n\t\treturn;\n\t}\n\t_progress = [&] {\n\t\tconst auto max = QWidget::maximumHeight();\n\t\tconst auto min = _minForProgress;\n\t\tconst auto p = (max > min)\n\t\t\t? ((height() - min) / float64(max - min))\n\t\t\t: 1.;\n\t\treturn std::clamp(p, 0., 1.);\n\t}();\n\tconst auto progressCurrent = _progress.current();\n\n\tconst auto rightButtonsWidth = calculateRightButtonsWidth();\n\n\tconst auto reservedRight = anim::interpolate(\n\t\t0,\n\t\trightButtonsWidth,\n\t\t1. - progressCurrent);\n\tconst auto titleMostLeft = TopBar::titleMostLeft();\n\tconst auto interpolatedPadding = anim::interpolate(\n\t\ttitleMostLeft,\n\t\trect::m::sum::h(st::boxRowPadding),\n\t\tprogressCurrent);\n\tconst auto verifiedWidget = _verified ? _verified->widget() : nullptr;\n\tconst auto badgeWidget = _badge ? _badge->widget() : nullptr;\n\tconst auto botVerifyWidget = _botVerify ? _botVerify->widget() : nullptr;\n\tauto badgesWidth = 0;\n\tif (verifiedWidget) {\n\t\tbadgesWidth += verifiedWidget->width();\n\t}\n\tif (badgeWidget) {\n\t\tbadgesWidth += badgeWidget->width();\n\t}\n\tif (botVerifyWidget) {\n\t\tbadgesWidth += botVerifyWidget->width();\n\t}\n\tif (verifiedWidget || badgeWidget) {\n\t\tbadgesWidth += st::infoVerifiedCheckPosition.x();\n\t}\n\tconst auto titleWidth = width()\n\t\t- interpolatedPadding\n\t\t- reservedRight\n\t\t- badgesWidth;\n\n\tif (titleWidth > 0 && _title->textMaxWidth() > titleWidth) {\n\t\t_title->resizeToWidth(titleWidth);\n\t}\n\n\tconst auto titleTop = anim::interpolate(\n\t\t_st.titleWithSubtitlePosition.y(),\n\t\tst::infoProfileTopBarTitleTop,\n\t\tprogressCurrent);\n\n\tconst auto badgeTop = titleTop;\n\tconst auto badgeBottom = titleTop + _title->height();\n\tconst auto margins = LargeCustomEmojiMargins();\n\n\tauto totalElementsWidth = _title->width();\n\tconst auto botVerifySkip = botVerifyWidget\n\t\t? botVerifyWidget->width() + st::infoVerifiedCheckPosition.x()\n\t\t: 0;\n\tif (verifiedWidget) {\n\t\ttotalElementsWidth += verifiedWidget->width();\n\t}\n\tif (badgeWidget) {\n\t\ttotalElementsWidth += badgeWidget->width();\n\t}\n\tif (verifiedWidget || badgeWidget) {\n\t\ttotalElementsWidth += st::infoVerifiedCheckPosition.x();\n\t}\n\ttotalElementsWidth += botVerifySkip;\n\n\tauto titleLeft = anim::interpolate(\n\t\ttitleMostLeft,\n\t\t(width() - totalElementsWidth) / 2,\n\t\tprogressCurrent);\n\n\tif (_botVerify) {\n\t\t_botVerify->move(\n\t\t\ttitleLeft,\n\t\t\tbadgeTop,\n\t\t\tbadgeBottom);\n\t\ttitleLeft += margins.left() + botVerifySkip;\n\t}\n\n\t_title->moveToLeft(titleLeft, titleTop);\n\tconst auto badgeLeft = titleLeft + _title->width();\n\tif (_badge) {\n\t\t_badge->move(badgeLeft, badgeTop, badgeBottom);\n\t}\n\tif (_verified) {\n\t\t_verified->move(\n\t\t\tbadgeLeft + (badgeWidget ? badgeWidget->width() : 0),\n\t\t\tbadgeTop,\n\t\t\tbadgeBottom);\n\t}\n\n\tupdateStatusPosition(progressCurrent);\n\n\tif (_badgeTooltip) {\n\t\t_badgeTooltip->setOpacity(progressCurrent);\n\t}\n\n\t{\n\t\tconst auto userpicRect = userpicGeometry();\n\t\tif (_userpicButton) {\n\t\t\t_userpicButton->setGeometry(userpicGeometry());\n\t\t}\n\n\t\tupdateGiftButtonsGeometry(progressCurrent, userpicRect);\n\t}\n}\n\nvoid TopBar::updateStatusPosition(float64 progressCurrent) {\n\tif (width() <= 0) {\n\t\treturn;\n\t}\n\tif (_forumButton) {\n\t\tconst auto buttonTop = anim::interpolate(\n\t\t\t_st.subtitlePosition.y(),\n\t\t\tst::infoProfileTopBarStatusTop,\n\t\t\tprogressCurrent);\n\t\tconst auto mostLeft = statusMostLeft();\n\t\tconst auto buttonMostLeft = anim::interpolate(\n\t\t\tmostLeft,\n\t\t\tst::infoProfileTopBarActionButtonsPadding.left(),\n\t\t\tprogressCurrent);\n\t\tconst auto buttonMostRight = anim::interpolate(\n\t\t\tcalculateRightButtonsWidth(),\n\t\t\tst::infoProfileTopBarActionButtonsPadding.right(),\n\t\t\tprogressCurrent);\n\t\tconst auto maxWidth = width() - buttonMostLeft - buttonMostRight;\n\t\tif (_forumButton->contentWidth() > maxWidth) {\n\t\t\t_forumButton->setFullWidth(maxWidth);\n\t\t}\n\t\tconst auto buttonLeft = anim::interpolate(\n\t\t\tmostLeft,\n\t\t\t(width() - _forumButton->width()) / 2,\n\t\t\tprogressCurrent);\n\t\t_forumButton->moveToLeft(buttonLeft, buttonTop);\n\t\t_forumButton->setVisible(true);\n\n\t\t_status->hide();\n\t\t// _starsRating->hide();\n\t\t_showLastSeen->hide(anim::type::instant);\n\t\treturn;\n\t}\n\n\tconst auto statusTop = anim::interpolate(\n\t\t_st.subtitlePosition.y(),\n\t\tst::infoProfileTopBarStatusTop,\n\t\tprogressCurrent);\n\tconst auto totalElementsWidth = _status->width()\n\t\t+ (_starsRating ? _starsRating->width() : 0)\n\t\t+ (_showLastSeen->toggled() ? _showLastSeen->width() : 0);\n\tconst auto statusLeft = anim::interpolate(\n\t\tstatusMostLeft(),\n\t\t(width() - totalElementsWidth) / 2,\n\t\tprogressCurrent);\n\n\tif (const auto rating = _starsRating.get()) {\n\t\trating->moveTo(statusLeft, statusTop - st::lineWidth);\n\t\trating->setOpacity(progressCurrent);\n\t}\n\tconst auto statusShift = _statusShift.current()\n\t\t* std::clamp((progressCurrent) / 0.15, 0., 1.);\n\n\t_status->moveToLeft(statusLeft + statusShift, statusTop);\n\n\tif (_showLastSeen->toggled()) {\n\t\t_showLastSeen->moveToLeft(\n\t\t\tstatusLeft\n\t\t\t\t+ statusShift\n\t\t\t\t+ _status->textMaxWidth()\n\t\t\t\t+ st::infoProfileTopBarLastSeenSkip.x(),\n\t\t\tstatusTop + st::infoProfileTopBarLastSeenSkip.y());\n\t\t_showLastSeen->setOpacity(progressCurrent);\n\t\t_showLastSeen->entity()->setAttribute(\n\t\t\tQt::WA_TransparentForMouseEvents,\n\t\t\t!progressCurrent);\n\t}\n}\n\nvoid TopBar::resizeEvent(QResizeEvent *e) {\n\t_cachedClipPath = QPainterPath();\n\tconst auto collectible = effectiveCollectible();\n\tif (collectible && !_animatedPoints.empty()) {\n\t\tsetupAnimatedPattern();\n\t}\n\tif (_hasGradientBg && e->oldSize().width() != e->size().width()) {\n\t\t_cachedClipPath = QPainterPath();\n\t\t_cachedGradient = QImage();\n\t}\n\tupdateLabelsPosition();\n\tRpWidget::resizeEvent(e);\n}\n\nQRect TopBar::userpicGeometry() const {\n\tconstexpr auto kMinScale = 0.25;\n\tconst auto progressCurrent = _progress.current();\n\tconst auto fullSize = st::infoProfileTopBarPhotoSize;\n\tconst auto minSize = fullSize * kMinScale;\n\tconst auto size = anim::interpolate(minSize, fullSize, progressCurrent);\n\tconst auto x = (width() - size) / 2;\n\tconst auto minY = -minSize;\n\tconst auto maxY = st::infoProfileTopBarPhotoTop;\n\tconst auto y = anim::interpolate(minY, maxY, progressCurrent);\n\treturn QRect(x, y, size, size);\n}\n\nvoid TopBar::updateGiftButtonsGeometry(\n\t\tfloat64 progressCurrent,\n\t\tconst QRect &userpicRect) {\n\tif (width() <= 0) {\n\t\treturn;\n\t}\n\tconst auto sz = st::infoProfileTopBarGiftSize;\n\tconst auto halfSz = sz / 2.;\n\tfor (const auto &gift : _pinnedToTopGifts) {\n\t\tif (gift.button) {\n\t\t\tconst auto giftPos = calculateGiftPosition(\n\t\t\t\tgift.position,\n\t\t\t\tprogressCurrent,\n\t\t\t\tuserpicRect);\n\t\t\tconst auto buttonRect = QRect(\n\t\t\t\tQPoint(giftPos.x() - halfSz, giftPos.y() - halfSz),\n\t\t\t\tSize(sz));\n\t\t\tgift.button->setGeometry(buttonRect);\n\t\t}\n\t}\n}\n\nvoid TopBar::paintUserpic(QPainter &p, const QRect &geometry) {\n\tif (_topicIconView) {\n\t\t_topicIconView->paintInRect(p, geometry);\n\t\treturn;\n\t}\n\tif (_videoUserpicPlayer && _videoUserpicPlayer->ready()) {\n\t\tconst auto size = st::infoProfileTopBarPhotoSize;\n\t\tconst auto frame = _videoUserpicPlayer->frame(Size(size), _peer);\n\t\tif (!frame.isNull()) {\n\t\t\tp.drawImage(geometry, frame);\n\t\t\tupdate();\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto key = _peer->userpicUniqueKey(_userpicView);\n\tconst auto overlayActive = _uploadOverlay && _uploadOverlay->shown();\n\tconst auto awaitingCloud = _waitingUserpicCloudLoad\n\t\t&& !_peer->userpicCloudImage(_userpicView);\n\tif (!overlayActive && !awaitingCloud && _userpicUniqueKey != key) {\n\t\t_waitingUserpicCloudLoad = false;\n\t\t_userpicUniqueKey = key;\n\t\tconst auto fullSize = st::infoProfileTopBarPhotoSize;\n\t\tconst auto scaled = fullSize * style::DevicePixelRatio();\n\t\tauto image = QImage();\n\t\tif (const auto broadcast = _peer->monoforumBroadcast()) {\n\t\t\timage = PeerData::GenerateUserpicImage(\n\t\t\t\tbroadcast,\n\t\t\t\t_userpicView,\n\t\t\t\tscaled,\n\t\t\t\t0);\n\t\t\tif (_monoforumMask.isNull()) {\n\t\t\t\t_monoforumMask = Ui::MonoforumShapeMask(Size(scaled));\n\t\t\t}\n\t\t\tconstexpr auto kFormat = QImage::Format_ARGB32_Premultiplied;\n\t\t\tif (image.format() != kFormat) {\n\t\t\t\timage = std::move(image).convertToFormat(kFormat);\n\t\t\t}\n\t\t\tauto q = QPainter(&image);\n\t\t\tq.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\t\tq.drawImage(\n\t\t\t\tRect(image.size() / image.devicePixelRatio()),\n\t\t\t\t_monoforumMask);\n\t\t\tq.end();\n\t\t} else {\n\t\t\timage = PeerData::GenerateUserpicImage(\n\t\t\t\t_peer,\n\t\t\t\t_userpicView,\n\t\t\t\tscaled,\n\t\t\t\tstd::nullopt);\n\t\t}\n\t\t_cachedUserpic = std::move(image);\n\t\t_cachedUserpic.setDevicePixelRatio(style::DevicePixelRatio());\n\t}\n\tp.drawImage(geometry, _cachedUserpic);\n\tif (_uploadOverlay && _uploadOverlay->shown()) {\n\t\t_uploadOverlay->paint(p, geometry, {\n\t\t\t.lineWidth = st::defaultUserpicButton.uploadProgressLine,\n\t\t\t.margin = st::defaultUserpicButton.uploadProgressMargin,\n\t\t\t.progressFg = st::historyFileThumbRadialFg,\n\t\t\t.overlayFg = st::songCoverOverlayFg,\n\t\t\t.cancelIcon = &st::userpicUploadCancel,\n\t\t});\n\t}\n}\n\nvoid TopBar::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tconst auto geometry = userpicGeometry();\n\n\tif (_hasGradientBg && _cachedGradient.isNull()) {\n\t\tconst auto collectible = effectiveCollectible();\n\t\tconst auto colorProfile = effectiveColorProfile();\n\t\tconst auto offset = QPoint(\n\t\t\t0,\n\t\t\t_hasActions\n\t\t\t\t? -st::infoProfileTopBarPhotoBgShift\n\t\t\t\t: -st::infoProfileTopBarPhotoBgNoActionsShift);\n\t\tif (collectible) {\n\t\t\t_cachedGradient = Ui::CreateTopBgGradient(\n\t\t\t\tQSize(width(), maximumHeight()),\n\t\t\t\tcollectible->centerColor,\n\t\t\t\tcollectible->edgeColor,\n\t\t\t\tfalse,\n\t\t\t\toffset);\n\t\t} else if (colorProfile && colorProfile->bg.size() > 1) {\n\t\t\t_cachedGradient = Ui::CreateTopBgGradient(\n\t\t\t\tQSize(width(), maximumHeight()),\n\t\t\t\tcolorProfile->bg[1],\n\t\t\t\tcolorProfile->bg[0],\n\t\t\t\tfalse,\n\t\t\t\toffset);\n\t\t}\n\t}\n\tif (!_hasGradientBg) {\n\t\tpaintEdges(p);\n\t} else {\n\t\tconst auto x = (width()\n\t\t\t- _cachedGradient.width() / style::DevicePixelRatio())\n\t\t\t\t/ 2;\n\t\tconst auto y = (height()\n\t\t\t- _cachedGradient.height() / style::DevicePixelRatio())\n\t\t\t\t/ 2;\n\t\tif (_roundEdges) {\n\t\t\tif (_cachedClipPath.isEmpty()) {\n\t\t\t\tconst auto radius = st::boxRadius;\n\t\t\t\t_cachedClipPath.addRoundedRect(\n\t\t\t\t\trect() + QMargins{ 0, 0, 0, radius + 1 },\n\t\t\t\t\tradius,\n\t\t\t\t\tradius);\n\t\t\t}\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setClipPath(_cachedClipPath);\n\t\t\tp.drawImage(x, y, _cachedGradient);\n\t\t} else {\n\t\t\tp.drawImage(x, y, _cachedGradient);\n\t\t}\n\t}\n\tif (_patternEmoji && _patternEmoji->ready()) {\n\t\tpaintAnimatedPattern(p, rect(), geometry);\n\t}\n\n\tconst auto clipBounds = e->region().boundingRect();\n\tif (clipBounds.bottom() >= geometry.top()\n\t\t&& clipBounds.top() <= geometry.bottom()) {\n\t\tpaintPinnedToTopGifts(p, rect(), geometry);\n\t}\n\n\tif (clipBounds.intersects(geometry)) {\n\t\tpaintUserpic(p, geometry);\n\t\tpaintStoryOutline(p, geometry);\n\t}\n}\n\nvoid TopBar::setupButtons(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tSource source) {\n\tif (source == Source::Preview) {\n\t\tsetRoundEdges(false);\n\t\treturn;\n\t}\n\trpl::combine(\n\t\t_wrap.value(),\n\t\t_edgeColor.value()\n\t) | rpl::on_next([=](\n\t\t\tWrap wrap,\n\t\t\tstd::optional<QColor> edgeColor) mutable {\n\t\tconst auto isLayer = (wrap == Wrap::Layer);\n\t\tconst auto isSide = (wrap == Wrap::Side);\n\t\tsetRoundEdges(isLayer);\n\t\tsetLottieSingleLoop(wrap == Wrap::Side);\n\n\t\tconst auto shouldUseColored = edgeColor\n\t\t\t&& (kMinContrast > Ui::CountContrast(\n\t\t\t\tst::boxTitleCloseFg->c,\n\t\t\t\t*edgeColor));\n\t\t_back = base::make_unique_q<Ui::FadeWrap<Ui::IconButton>>(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::IconButton>(\n\t\t\t\tthis,\n\t\t\t\t(isLayer\n\t\t\t\t\t? (shouldUseColored\n\t\t\t\t\t\t? st::infoTopBarColoredBack\n\t\t\t\t\t\t: st::infoTopBarBlackBack)\n\t\t\t\t\t: (shouldUseColored\n\t\t\t\t\t\t? st::infoLayerTopBarColoredBack\n\t\t\t\t\t\t: st::infoLayerTopBarBlackBack))),\n\t\t\tst::infoTopBarScale);\n\t\t_back->QWidget::show();\n\t\t_back->setDuration(0);\n\t\t_back->entity()->setAccessibleName(tr::lng_go_back(tr::now));\n\t\t_back->toggleOn(isLayer || isSide\n\t\t\t? (_backToggles.value() | rpl::type_erased)\n\t\t\t: rpl::single(wrap == Wrap::Narrow));\n\t\t_back->entity()->clicks() | rpl::to_empty | rpl::start_to_stream(\n\t\t\t_backClicks,\n\t\t\t_back->lifetime());\n\n\t\tif (!isLayer && !isSide) {\n\t\t\t_close = nullptr;\n\t\t} else {\n\t\t\t_close = base::make_unique_q<Ui::IconButton>(\n\t\t\t\tthis,\n\t\t\t\tshouldUseColored\n\t\t\t\t\t? st::infoTopBarColoredClose\n\t\t\t\t\t: st::infoTopBarBlackClose);\n\t\t\t_close->setAccessibleName(tr::lng_sr_close_panel(tr::now));\n\t\t\t_close->show();\n\t\t\t_close->addClickHandler(isSide\n\t\t\t\t? Fn<void()> ([=] { controller->closeThirdSection(); })\n\t\t\t\t: Fn<void()> ([=] {\n\t\t\t\t\tcontroller->hideLayer();\n\t\t\t\t\tcontroller->hideSpecialLayer();\n\t\t\t\t}));\n\t\t\twidthValue() | rpl::on_next([=] {\n\t\t\t\t_close->moveToRight(0, 0);\n\t\t\t}, _close->lifetime());\n\t\t}\n\n\t\tif (wrap != Wrap::Side) {\n\t\t\tif (source == Source::Stories) {\n\t\t\t\taddTopBarEditButton(controller, wrap, shouldUseColored);\n\t\t\t}\n\t\t}\n\t}, lifetime());\n}\n\nvoid TopBar::addTopBarEditButton(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tWrap wrap,\n\t\tbool shouldUseColored) {\n\t_topBarButton = base::make_unique_q<Ui::IconButton>(\n\t\tthis,\n\t\t((wrap == Wrap::Layer)\n\t\t\t? (shouldUseColored\n\t\t\t\t? st::infoLayerTopBarColoredEdit\n\t\t\t\t: st::infoLayerTopBarBlackEdit)\n\t\t\t: (shouldUseColored\n\t\t\t\t? st::infoTopBarColoredEdit\n\t\t\t\t: st::infoTopBarBlackEdit)));\n\t_topBarButton->show();\n\t_topBarButton->addClickHandler([=] {\n\t\tcontroller->showSettings(::Settings::InformationId());\n\t});\n\t_topBarButton->setAccessibleName(tr::lng_settings_information(tr::now));\n\n\twidthValue() | rpl::on_next([=] {\n\t\tif (_close) {\n\t\t\t_topBarButton->moveToRight(_close->width(), 0);\n\t\t} else {\n\t\t\t_topBarButton->moveToRight(0, 0);\n\t\t}\n\t}, _topBarButton->lifetime());\n}\n\nvoid TopBar::showTopBarMenu(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tbool check) {\n\tif (_peerMenu) {\n\t\t_peerMenu->hideMenu(true);\n\t\treturn;\n\t}\n\t_peerMenu = base::make_unique_q<Ui::PopupMenu>(\n\t\tQWidget::window(),\n\t\tst::popupMenuExpandedSeparator);\n\n\t_peerMenu->setDestroyedCallback([this] {\n\t\tInvokeQueued(this, [this] { _peerMenu = nullptr; });\n\t\t// if (auto toggle = _topBarMenuToggle.get()) {\n\t\t// \ttoggle->setForceRippled(false);\n\t\t// }\n\t});\n\n\tfillTopBarMenu(\n\t\tcontroller,\n\t\tUi::Menu::CreateAddActionCallback(_peerMenu));\n\tif (_peerMenu->empty()) {\n\t\t_peerMenu = nullptr;\n\t\treturn;\n\t} else if (check) {\n\t\treturn;\n\t}\n\t_peerMenu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);\n\t_peerMenu->popup(_actionMore\n\t\t? Ui::PopupMenu::ConstrainToParentScreen(\n\t\t\t_peerMenu,\n\t\t\t_actionMore->mapToGlobal(QPoint(\n\t\t\t\t_actionMore->width()\n\t\t\t\t\t+ Ui::BoxShadow::ExtendFor(\n\t\t\t\t\t\t_peerMenu->st().shadow).right(),\n\t\t\t\t_actionMore->height() + st::infoProfileTopBarActionMenuSkip)))\n\t\t: QCursor::pos());\n}\n\nvoid TopBar::fillTopBarMenu(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst Ui::Menu::MenuCallback &addAction) {\n\tconst auto peer = _peer;\n\tconst auto topic = _key.topic();\n\tconst auto sublist = _key.sublist();\n\tif (!peer && !topic) {\n\t\treturn;\n\t}\n\n\tWindow::FillDialogsEntryMenu(\n\t\tcontroller,\n\t\tDialogs::EntryState{\n\t\t\t.key = (topic\n\t\t\t\t? Dialogs::Key{ topic }\n\t\t\t\t: sublist\n\t\t\t\t? Dialogs::Key{ sublist }\n\t\t\t\t: Dialogs::Key{ peer->owner().history(peer) }),\n\t\t\t.section = Dialogs::EntryState::Section::Profile,\n\t\t},\n\t\taddAction);\n}\n\nvoid TopBar::updateVideoUserpic() {\n\tif (width() <= 0) {\n\t\treturn;\n\t}\n\tconst auto id = _peer->userpicPhotoId();\n\tif (!id) {\n\t\t_videoUserpicPlayer = nullptr;\n\t\treturn;\n\t}\n\tconst auto photo = _peer->owner().photo(id);\n\tif (!photo->date() || !photo->videoCanBePlayed()) {\n\t\t_videoUserpicPlayer = nullptr;\n\t\treturn;\n\t}\n\tif (!_videoUserpicPlayer) {\n\t\t_videoUserpicPlayer = std::make_unique<Ui::VideoUserpicPlayer>();\n\t}\n\t_videoUserpicPlayer->setup(_peer, photo);\n}\n\nvoid TopBar::setupShowLastSeen(\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto user = _peer->asUser();\n\tif (!user\n\t\t|| user->isSelf()\n\t\t|| user->isBot()\n\t\t|| user->isServiceUser()\n\t\t|| !user->session().premiumPossible()) {\n\t\t_showLastSeen->hide(anim::type::instant);\n\t\treturn;\n\t}\n\n\tif (user->session().premium()) {\n\t\tif (user->lastseen().isHiddenByMe()) {\n\t\t\tuser->updateFullForced();\n\t\t}\n\t\t_showLastSeen->hide(anim::type::instant);\n\t\treturn;\n\t}\n\n\trpl::combine(\n\t\tuser->session().changes().peerFlagsValue(\n\t\t\tuser,\n\t\t\tData::PeerUpdate::Flag::OnlineStatus),\n\t\tData::AmPremiumValue(&user->session())\n\t) | rpl::on_next([=](auto, bool premium) {\n\t\tconst auto wasShown = _showLastSeen->toggled();\n\t\tconst auto hiddenByMe = user->lastseen().isHiddenByMe();\n\t\tconst auto shown = hiddenByMe\n\t\t\t&& !user->lastseen().isOnline(base::unixtime::now())\n\t\t\t&& !premium\n\t\t\t&& user->session().premiumPossible();\n\t\t_showLastSeen->toggle(shown, anim::type::instant);\n\t\tif (wasShown && premium && hiddenByMe) {\n\t\t\tuser->updateFullForced();\n\t\t}\n\t}, _showLastSeen->lifetime());\n\n\tcontroller->session().api().userPrivacy().value(\n\t\tApi::UserPrivacy::Key::LastSeen\n\t) | rpl::filter([=](Api::UserPrivacy::Rule rule) {\n\t\treturn (rule.option == Api::UserPrivacy::Option::Everyone);\n\t}) | rpl::on_next([=] {\n\t\tif (user->lastseen().isHiddenByMe()) {\n\t\t\tuser->updateFullForced();\n\t\t}\n\t}, _showLastSeen->lifetime());\n\n\t_showLastSeen->setOpacity(0.);\n\n\t_showLastSeen->entity()->setFullRadius(true);\n\n\t_showLastSeen->entity()->setClickedCallback([=] {\n\t\tconst auto type = Ui::ShowOrPremium::LastSeen;\n\t\tcontroller->show(Box(\n\t\t\tUi::ShowOrPremiumBox,\n\t\t\ttype,\n\t\t\tuser->shortName(),\n\t\t\t[=] {\n\t\t\t\tcontroller->session().api().userPrivacy().save(\n\t\t\t\t\t::Api::UserPrivacy::Key::LastSeen,\n\t\t\t\t\t{});\n\t\t\t},\n\t\t\t[=] {\n\t\t\t\t::Settings::ShowPremium(controller, u\"lastseen_hidden\"_q);\n\t\t\t}));\n\t});\n}\n\nvoid TopBar::setupAnimatedPattern(const QRect &userpicGeometry) {\n\t_animatedPoints = GenerateAnimatedPattern(userpicGeometry.isNull()\n\t\t? TopBar::userpicGeometry()\n\t\t: userpicGeometry);\n}\n\nvoid TopBar::paintAnimatedPattern(\n\t\tQPainter &p,\n\t\tconst QRect &rect,\n\t\tconst QRect &userpicGeometry) {\n\tif (!_patternEmoji || !_patternEmoji->ready()) {\n\t\treturn;\n\t}\n\n\t{\n\t\t// TODO make it better.\n\t\tif (_lastUserpicRect != userpicGeometry) {\n\t\t\t_lastUserpicRect = userpicGeometry;\n\t\t\tsetupAnimatedPattern(userpicGeometry);\n\t\t}\n\t}\n\n\tif (_basePatternImage.isNull()) {\n\t\tauto patternColors = CalculatePatternColors(\n\t\t\teffectiveColorProfile(),\n\t\t\teffectiveCollectible(),\n\t\t\t_edgeColor.current(),\n\t\t\tWindow::Theme::IsNightMode());\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto scale = 0.910;\n\t\tconst auto size = st::emojiSize;\n\t\t_basePatternImage = QImage(\n\t\t\tQSize(size, size) * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t_basePatternImage.setDevicePixelRatio(ratio);\n\t\t_basePatternImage.fill(Qt::transparent);\n\t\tauto painter = QPainter(&_basePatternImage);\n\t\tauto hq = PainterHighQualityEnabler(painter);\n\t\t// const auto contentSize = size * scale;\n\t\t// const auto offset = (size - contentSize) / 2.;\n\t\t// painter.translate(offset, offset);\n\t\tpainter.scale(scale, scale);\n\t\t_patternEmoji->paint(painter, { .textColor = Qt::white });\n\t\tpainter.resetTransform();\n\n\t\tif (patternColors.useOverlayBlend) {\n\t\t\tpainter.setCompositionMode(QPainter::CompositionMode_SourceIn);\n\t\t\tpainter.fillRect(\n\t\t\t\tRect(Size(size)),\n\t\t\t\tQColor(0, 0, 0, int(0.8 * 255)));\n\t\t} else {\n\t\t\tpainter.setCompositionMode(QPainter::CompositionMode_SourceIn);\n\t\t\tpainter.fillRect(Rect(Size(size)), patternColors.patternColor);\n\t\t}\n\t}\n\n\tconst auto progress = _progress.current();\n\t// const auto collapseDiff = progress >= 0.85 ? 1. : (progress / 0.85);\n\t// const auto collapse = std::clamp((collapseDiff - 0.2) / 0.8, 0., 1.);\n\tconst auto collapse = progress;\n\n\tconst auto userpicCenter = rect::center(userpicGeometry);\n\tconst auto yOffset = 12 * (1. - progress);\n\tconst auto imageSize = _basePatternImage.size()\n\t\t/ style::DevicePixelRatio();\n\tconst auto halfImageWidth = imageSize.width() * 0.5;\n\tconst auto halfImageHeight = imageSize.height() * 0.5;\n\n\tfor (const auto &point : _animatedPoints) {\n\t\tconst auto timeRange = point.endTime - point.startTime;\n\t\tconst auto collapseProgress = (1. - collapse <= point.startTime)\n\t\t\t? 1.\n\t\t\t: 1.\n\t\t\t\t- std::clamp(\n\t\t\t\t\t(1. - collapse - point.startTime) / timeRange,\n\t\t\t\t\t0.,\n\t\t\t\t\t1.);\n\n\t\tif (collapseProgress <= 0.) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto x = point.basePosition.x();\n\t\tauto y = point.basePosition.y() - yOffset;\n\t\tauto r = point.size * 0.5;\n\n\t\tif (collapseProgress < 1.) {\n\t\t\tconst auto dx = x - userpicCenter.x();\n\t\t\tconst auto dy = y - userpicCenter.y();\n\t\t\tx = userpicCenter.x() + dx * collapseProgress;\n\t\t\ty = userpicCenter.y() + dy * collapseProgress;\n\t\t\tr = kMinPatternRadius\n\t\t\t\t+ (r - kMinPatternRadius) * collapseProgress;\n\t\t}\n\n\t\tconst auto scale = r / (point.size * 0.5);\n\t\tconst auto scaledHalfWidth = halfImageWidth * scale;\n\t\tconst auto scaledHalfHeight = halfImageHeight * scale;\n\n\t\t// Distance-based alpha calculation.\n\t\tconst auto userpicRect = _lastUserpicRect;\n\t\tconst auto acx = userpicRect.x() + userpicRect.width() / 2.;\n\t\tconst auto acy = userpicRect.y() + userpicRect.height() / 2.;\n\t\tconst auto aw = userpicRect.width();\n\t\tconst auto ah = userpicRect.height();\n\t\tconst auto distance = std::sqrt((x - acx) * (x - acx)\n\t\t\t+ (y - acy) * (y - acy));\n\t\tconst auto normalizedDistance = std::clamp(\n\t\t\tdistance / (aw * 2.),\n\t\t\t0.,\n\t\t\t1.);\n\t\tconst auto distanceAlpha = 1. - normalizedDistance;\n\n\t\t// Bottom alpha calculation.\n\t\tconst auto bottomThreshold = userpicRect.y() + ah + kMinPatternRadius;\n\t\tconst auto bottomAlpha = (y > bottomThreshold)\n\t\t\t? 1. - std::clamp((y - bottomThreshold) / 56., 0., 1.)\n\t\t\t: 1.;\n\n\t\tauto alpha = progress * distanceAlpha * 0.5 * bottomAlpha;\n\t\tif (collapseProgress < 1.) {\n\t\t\talpha = alpha * collapseProgress;\n\t\t}\n\n\t\tp.setOpacity(alpha);\n\t\tp.drawImage(\n\t\t\tQRectF(\n\t\t\t\tx - scaledHalfWidth,\n\t\t\t\ty - scaledHalfHeight,\n\t\t\t\tscaledHalfWidth * 2,\n\t\t\t\tscaledHalfHeight * 2),\n\t\t\t_basePatternImage);\n\t}\n\tp.setOpacity(1.);\n}\n\nvoid TopBar::setupPinnedToTopGifts(\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto requestDone = crl::guard(this, [=](\n\t\t\tstd::vector<Data::SavedStarGift> gifts) {\n\t\tconst auto shouldHideFirst = _pinnedToTopGiftsFirstTimeShowed\n\t\t\t&& !_pinnedToTopGifts.empty();\n\n\t\tif (shouldHideFirst) {\n\t\t\t_giftsHiding = std::make_unique<Ui::Animations::Simple>();\n\t\t\t_giftsHiding->start([=](float64 value) {\n\t\t\t\tupdate();\n\t\t\t\tif (value <= 0.) {\n\t\t\t\t\t_giftsHiding = nullptr;\n\t\t\t\t\t_pinnedToTopGifts.clear();\n\t\t\t\t\t_giftsLoadingLifetime.destroy();\n\t\t\t\t\tupdateCollectibleStatus();\n\t\t\t\t\tsetupNewGifts(controller, gifts);\n\t\t\t\t}\n\t\t\t}, 1., 0., 300, anim::linear);\n\t\t\treturn;\n\t\t}\n\n\t\t_pinnedToTopGifts.clear();\n\t\t_giftsLoadingLifetime.destroy();\n\n\t\tupdateCollectibleStatus();\n\t\tsetupNewGifts(controller, gifts);\n\t});\n\t_peer->session().recentSharedGifts().request(_peer, requestDone, true);\n}\n\nvoid TopBar::setupNewGifts(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst std::vector<Data::SavedStarGift> &gifts) {\n\tconst auto emojiStatusId = _peer->emojiStatusId().collectible\n\t\t? _peer->emojiStatusId().collectible->id\n\t\t: CollectibleId(0);\n\tauto filteredGifts = std::vector<Data::SavedStarGift>();\n\tconst auto subtract = emojiStatusId ? 1 : 0;\n\tfilteredGifts.reserve((gifts.size() > subtract)\n\t\t? (gifts.size() - subtract)\n\t\t: 0);\n\tfor (const auto &gift : gifts) {\n\t\tif (const auto &unique = gift.info.unique) {\n\t\t\tif (unique->id != emojiStatusId) {\n\t\t\t\tfilteredGifts.push_back(gift);\n\t\t\t}\n\t\t}\n\t}\n\n\t_pinnedToTopGifts.reserve(filteredGifts.size());\n\tif (filteredGifts.empty()) {\n\t\t_giftsAppearing = nullptr;\n\t\t_lottiePlayer = nullptr;\n\t\t_pinnedToTopGiftsFirstTimeShowed = true;\n\t} else if (!_lottiePlayer) {\n\t\t_lottiePlayer = std::make_unique<Lottie::MultiPlayer>(\n\t\t\tLottie::Quality::Default);\n\t\t_lottiePlayer->updates() | rpl::on_next([=] {\n\t\t\tupdate();\n\t\t}, lifetime());\n\t}\n\n\t_giftsAppearing = std::make_unique<Ui::Animations::Simple>();\n\n\tconstexpr auto kMaxPinnedToTopGifts = 6;\n\n\tauto positions = ranges::views::iota(\n\t\t0,\n\t\tkMaxPinnedToTopGifts) | ranges::to_vector;\n\tranges::shuffle(positions);\n\n\tfor (auto i = 0;\n\t\ti < filteredGifts.size() && i < kMaxPinnedToTopGifts;\n\t\t++i) {\n\t\tconst auto &gift = filteredGifts[i];\n\t\tconst auto document = _peer->owner().document(\n\t\t\tgift.info.document->id);\n\t\tauto entry = PinnedToTopGiftEntry();\n\t\tentry.manageId = gift.manageId;\n\t\tentry.media = document->createMediaView();\n\t\tentry.media->checkStickerSmall();\n\t\tif (const auto &unique = gift.info.unique) {\n\t\t\tif (unique->backdrop.centerColor.isValid()\n\t\t\t\t&& unique->backdrop.edgeColor.isValid()) {\n\t\t\t\tentry.bg = Ui::CreateTopBgGradient(\n\t\t\t\t\tSize(st::infoProfileTopBarGiftSize * 2),\n\t\t\t\t\tunique->backdrop.centerColor,\n\t\t\t\t\tanim::with_alpha(unique->backdrop.edgeColor, 0.0),\n\t\t\t\t\tfalse);\n\t\t\t}\n\t\t}\n\t\tentry.position = positions[i];\n\t\tentry.button = base::make_unique_q<Ui::AbstractButton>(this);\n\t\tentry.button->setAccessibleName([&] {\n\t\t\tconst auto base = tr::lng_profile_action_short_gift(tr::now);\n\n\t\t\tif (const auto &unique = gift.info.unique) {\n\t\t\t\tconst auto name = Data::UniqueGiftName(*unique);\n\t\t\t\tif (!name.isEmpty()) {\n\t\t\t\t\treturn base + \": \" + name;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn base;\n\t\t}());\n\t\tentry.button->show();\n\n\t\tentry.button->setClickedCallback([=, giftData = gift, peer = _peer] {\n\t\t\t::Settings::ShowSavedStarGiftBox(controller, peer, giftData);\n\t\t});\n\n\t\t_pinnedToTopGifts.push_back(std::move(entry));\n\t}\n\tupdateGiftButtonsGeometry(_progress.current(), userpicGeometry());\n\n\tusing namespace ChatHelpers;\n\n\trpl::single(\n\t\trpl::empty_value()\n\t) | rpl::then(\n\t\t_peer->session().downloaderTaskFinished()\n\t) | rpl::on_next([=] {\n\t\tauto allLoaded = true;\n\t\tfor (auto &entry : _pinnedToTopGifts) {\n\t\t\tif (!entry.animation && !entry.lastFrame.isNull()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!entry.animation && entry.media->loaded()) {\n\t\t\t\tentry.animation = LottieAnimationFromDocument(\n\t\t\t\t\t_lottiePlayer.get(),\n\t\t\t\t\tentry.media.get(),\n\t\t\t\t\tStickerLottieSize::PinnedProfileUniqueGiftSize,\n\t\t\t\t\tSize(st::infoProfileTopBarGiftSize)\n\t\t\t\t\t\t* style::DevicePixelRatio());\n\t\t\t} else if (!entry.media->loaded()) {\n\t\t\t\tallLoaded = false;\n\t\t\t}\n\t\t}\n\t\tif (allLoaded) {\n\t\t\t_giftsLoadingLifetime.destroy();\n\t\t\t_giftsAppearing->stop();\n\t\t\t_giftsAppearing->start([=](float64 value) {\n\t\t\t\tupdate();\n\t\t\t\tif (value >= 1.) {\n\t\t\t\t\t_giftsAppearing = nullptr;\n\t\t\t\t\t_pinnedToTopGiftsFirstTimeShowed = true;\n\t\t\t\t\tif (_lottieSingleLoop) {\n\t\t\t\t\t\tauto allFramesCaptured = true;\n\t\t\t\t\t\tfor (const auto &entry : _pinnedToTopGifts) {\n\t\t\t\t\t\t\tif (entry.animation || entry.lastFrame.isNull()) {\n\t\t\t\t\t\t\t\tallFramesCaptured = false;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (allFramesCaptured) {\n\t\t\t\t\t\t\t_lottiePlayer = nullptr;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, 0., 1., 400, anim::easeOutQuint);\n\t\t}\n\t}, _giftsLoadingLifetime);\n}\n\nQPointF TopBar::calculateGiftPosition(\n\t\tint position,\n\t\tfloat64 progress,\n\t\tconst QRect &userpicRect) const {\n\tconst auto acx = userpicRect.x() + userpicRect.width() / 2.;\n\tconst auto acy = userpicRect.y() + userpicRect.height() / 2.;\n\tconst auto aw = userpicRect.width();\n\tconst auto ah = userpicRect.height();\n\n\tauto giftPos = QPointF();\n\tauto delayValue = 0.;\n\tswitch (position) {\n\tcase 0: // Left.\n\t\tgiftPos = QPointF(\n\t\t\tacx / 2. - st::infoProfileTopBarGiftLeft.x(),\n\t\t\tacy - st::infoProfileTopBarGiftLeft.y());\n\t\tdelayValue = 1.6;\n\t\tbreak;\n\tcase 1: // Top left.\n\t\tgiftPos = QPointF(\n\t\t\tacx * 2. / 3. - st::infoProfileTopBarGiftTopLeft.x(),\n\t\t\tuserpicRect.y() - st::infoProfileTopBarGiftTopLeft.y());\n\t\tdelayValue = 0.;\n\t\tbreak;\n\tcase 2: // Bottom left.\n\t\tgiftPos = QPointF(\n\t\t\tacx * 2. / 3. - st::infoProfileTopBarGiftBottomLeft.x(),\n\t\t\tuserpicRect.y() + ah - st::infoProfileTopBarGiftBottomLeft.y());\n\t\tdelayValue = 0.9;\n\t\tbreak;\n\tcase 3: // Right.\n\t\tgiftPos = QPointF(\n\t\t\tacx + aw / 2. + st::infoProfileTopBarGiftRight.x(),\n\t\t\tacy - st::infoProfileTopBarGiftRight.y());\n\t\tdelayValue = 1.6;\n\t\tbreak;\n\tcase 4: // Top right.\n\t\tgiftPos = QPointF(\n\t\t\tacx + aw / 3. + st::infoProfileTopBarGiftTopRight.x(),\n\t\t\tuserpicRect.y() - st::infoProfileTopBarGiftTopRight.y());\n\t\tdelayValue = 0.9;\n\t\tbreak;\n\tdefault: // Bottom right.\n\t\tgiftPos = QPointF(\n\t\t\tacx + aw / 3. + st::infoProfileTopBarGiftBottomRight.x(),\n\t\t\tuserpicRect.y() + ah - st::infoProfileTopBarGiftBottomRight.y());\n\t\tdelayValue = 0.;\n\t\tbreak;\n\t}\n\n\tconst auto delayFraction = 0.2;\n\tconst auto maxDelayFraction = 1.6 * delayFraction;\n\tconst auto intervalFraction = 1. - maxDelayFraction;\n\tconst auto delay = delayValue * delayFraction;\n\tconst auto collapse = (progress >= 1. - delay)\n\t\t? 1.\n\t\t: std::clamp((progress - maxDelayFraction + delay)\n\t\t\t/ intervalFraction, 0., 1.);\n\n\tif (collapse < 1.) {\n\t\tconst auto collapseX = 1. - std::pow(1. - collapse, 2.);\n\t\tgiftPos = QPointF(\n\t\t\tacx + (giftPos.x() - acx) * collapseX,\n\t\t\tacy + (giftPos.y() - acy) * collapse);\n\t}\n\n\treturn giftPos;\n}\n\nvoid TopBar::paintPinnedToTopGifts(\n\t\tQPainter &p,\n\t\tconst QRect &rect,\n\t\tconst QRect &userpicRect) {\n\tif (_pinnedToTopGifts.empty() || _source == Source::Preview) {\n\t\treturn;\n\t}\n\n\tconst auto progress = _giftsHiding\n\t\t? _progress.current() * _giftsHiding->value(1.)\n\t\t: (_giftsAppearing\n\t\t\t? _progress.current() * _giftsAppearing->value(0.)\n\t\t\t: _progress.current());\n\n\tfor (auto &gift : _pinnedToTopGifts) {\n\t\tif (!gift.animation\n\t\t\t&& (_lottieSingleLoop ? gift.lastFrame.isNull() : true)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto giftPos = calculateGiftPosition(\n\t\t\tgift.position,\n\t\t\tprogress,\n\t\t\tuserpicRect);\n\n\t\tconst auto alpha = progress;\n\t\tif (alpha <= 0.) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tp.setOpacity(alpha);\n\t\tauto frameToRender = QImage();\n\t\tif (_lottieSingleLoop && !gift.lastFrame.isNull()) {\n\t\t\tframeToRender = gift.lastFrame;\n\t\t} else if (gift.animation && gift.animation->ready()) {\n\t\t\tframeToRender = gift.animation->frame();\n\t\t\tframeToRender.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\tif (_lottiePlayer) {\n\t\t\t\t_lottiePlayer->markFrameShown();\n\t\t\t}\n\t\t\tif (_lottieSingleLoop && gift.animation->framesCount() > 0) {\n\t\t\t\tconst auto currentFrame = gift.animation->frameIndex();\n\t\t\t\tconst auto totalFrames = gift.animation->framesCount();\n\t\t\t\tif (currentFrame >= totalFrames - 1) {\n\t\t\t\t\tgift.lastFrame = frameToRender;\n\t\t\t\t\tgift.animation = nullptr;\n\n\t\t\t\t\tauto allDone = true;\n\t\t\t\t\tfor (const auto &entry : _pinnedToTopGifts) {\n\t\t\t\t\t\tif (entry.animation) {\n\t\t\t\t\t\t\tallDone = false;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (allDone) {\n\t\t\t\t\t\t_lottiePlayer = nullptr;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!frameToRender.isNull()) {\n\t\t\tconst auto frameSize = frameToRender.width()\n\t\t\t\t/ style::DevicePixelRatio();\n\t\t\tconst auto halfFrameSize = frameSize / 2.;\n\t\t\tconst auto resultPos = QPointF(\n\t\t\t\tgiftPos.x() - halfFrameSize,\n\t\t\t\tgiftPos.y() - halfFrameSize);\n\t\t\tif (!gift.bg.isNull()) {\n\t\t\t\tconst auto bgSize = gift.bg.width()\n\t\t\t\t\t/ style::DevicePixelRatio();\n\t\t\t\tconst auto bgPos = QPointF(\n\t\t\t\t\tresultPos.x() + (frameSize - bgSize) / 2.,\n\t\t\t\t\tresultPos.y() + (frameSize - bgSize) / 2.);\n\t\t\t\tp.drawImage(bgPos, gift.bg);\n\t\t\t}\n\t\t\tp.drawImage(resultPos, frameToRender);\n\t\t}\n\t}\n\tp.setOpacity(1.);\n}\n\nvoid TopBar::setupStoryOutline(const QRect &geometry) {\n\tconst auto user = _peer->asUser();\n\tconst auto channel = _peer->asChannel();\n\tif (!user && !channel) {\n\t\treturn;\n\t}\n\n\trpl::combine(\n\t\t_edgeColor.value(),\n\t\trpl::merge(\n\t\t\trpl::single(rpl::empty_value()),\n\t\t\tstyle::PaletteChanged(),\n\t\t\t_peer->session().changes().peerUpdates(\n\t\t\t\tData::PeerUpdate::Flag::StoriesState\n\t\t\t\t\t| Data::PeerUpdate::Flag::ColorProfile\n\t\t\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\t\t\treturn update.peer == _peer;\n\t\t\t}) | rpl::to_empty)\n\t) | rpl::on_next([=](\n\t\t\tstd::optional<QColor> edgeColor,\n\t\t\trpl::empty_value) {\n\t\tconst auto geometry = QRectF(userpicGeometry());\n\t\tconst auto colorProfile\n\t\t\t= _peer->session().api().peerColors().colorProfileFor(_peer);\n\t\tconst auto hasProfileColor = colorProfile\n\t\t\t&& colorProfile->story.size() > 1;\n\t\tif (hasProfileColor) {\n\t\t\tedgeColor = std::nullopt;\n\t\t}\n\t\t_storyOutlineBrush = hasProfileColor\n\t\t\t? Ui::UnreadStoryOutlineGradient(\n\t\t\t\tgeometry,\n\t\t\t\tcolorProfile->story[0],\n\t\t\t\tcolorProfile->story[1])\n\t\t\t: Ui::UnreadStoryOutlineGradient(geometry);\n\t\tupdateStoryOutline(edgeColor);\n\t}, lifetime());\n}\n\nvoid TopBar::updateStoryOutline(std::optional<QColor> edgeColor) {\n\tconst auto user = _peer->asUser();\n\tconst auto channel = _peer->asChannel();\n\tif (!user && !channel) {\n\t\treturn;\n\t}\n\n\tconst auto hasActiveStories = (_source == Source::Preview)\n\t\t? true\n\t\t: (user ? user->hasActiveStories() : channel->hasActiveStories());\n\tconst auto hasLiveStories = (_source == Source::Preview)\n\t\t? false\n\t\t: (user ? user->hasActiveVideoStream() : false);\n\n\tif (_hasStories != hasActiveStories\n\t\t|| _hasLiveStories != hasLiveStories) {\n\t\t_hasStories = hasActiveStories;\n\t\t_hasLiveStories = hasLiveStories;\n\t\tupdate();\n\t}\n\n\tif (!hasActiveStories) {\n\t\t_storySegments.clear();\n\t\treturn;\n\t}\n\tconst auto widthBig = style::ConvertFloatScale(3.0);\n\n\t_storySegments.clear();\n\n\tif (_source == Source::Preview) {\n\t\tconst auto colorProfile = effectiveColorProfile();\n\t\tconst auto hasProfileColor = colorProfile\n\t\t\t&& colorProfile->story.size() > 1;\n\t\tconst auto previewBrush = hasProfileColor\n\t\t\t? Ui::UnreadStoryOutlineGradient(\n\t\t\t\tQRectF(userpicGeometry()),\n\t\t\t\tcolorProfile->story[0],\n\t\t\t\tcolorProfile->story[1])\n\t\t\t: _localCollectible\n\t\t\t? Ui::UnreadStoryOutlineGradient(\n\t\t\t\tQRectF(userpicGeometry()),\n\t\t\t\tUi::BlendColors(_localCollectible->edgeColor, Qt::white, .5),\n\t\t\t\tUi::BlendColors(_localCollectible->edgeColor, Qt::white, .5))\n\t\t\t: Ui::UnreadStoryOutlineGradient(QRectF(userpicGeometry()));\n\t\t_storySegments.push_back({\n\t\t\t.brush = QBrush(previewBrush),\n\t\t\t.width = widthBig,\n\t\t});\n\t\treturn;\n\t}\n\n\tconst auto &stories = _peer->owner().stories();\n\tconst auto source = stories.source(_peer->id);\n\tif (!source) {\n\t\treturn;\n\t}\n\n\tconst auto baseColor = edgeColor\n\t\t? Ui::BlendColors(*edgeColor, Qt::white, .5)\n\t\t: _storyOutlineBrush.color();\n\tconst auto unreadBrush = _hasLiveStories\n\t\t? st::attentionButtonFg->b\n\t\t: edgeColor\n\t\t? QBrush(baseColor)\n\t\t: _storyOutlineBrush;\n\tconst auto readBrush = edgeColor\n\t\t? QBrush(anim::with_alpha(baseColor, 0.5))\n\t\t: QBrush(st::dialogsUnreadBgMuted->b);\n\n\tif (_hasLiveStories) {\n\t\t_storySegments.push_back({\n\t\t\t.brush = unreadBrush,\n\t\t\t.width = widthBig,\n\t\t});\n\t} else {\n\t\tconst auto readTill = source->readTill;\n\t\tconst auto widthSmall = widthBig / 2.;\n\t\tfor (const auto &storyIdDates : source->ids) {\n\t\t\tconst auto isUnread = (storyIdDates.id > readTill);\n\t\t\t_storySegments.push_back({\n\t\t\t\t.brush = isUnread ? unreadBrush : readBrush,\n\t\t\t\t.width = !isUnread ? widthSmall : widthBig,\n\t\t\t});\n\t\t}\n\t}\n}\n\nvoid TopBar::paintStoryOutline(QPainter &p, const QRect &geometry) {\n\tif (!_hasStories || _storySegments.empty()) {\n\t\treturn;\n\t}\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tconst auto progress = _progress.current();\n\tconst auto alpha = std::clamp(\n\t\t(progress - kStoryOutlineFadeEnd) / kStoryOutlineFadeRange,\n\t\t0.,\n\t\t1.);\n\tif (alpha <= 0.) {\n\t\treturn;\n\t}\n\n\tp.setOpacity(alpha);\n\tconst auto outlineWidth = style::ConvertFloatScale(4.0);\n\tconst auto padding = style::ConvertFloatScale(3.0);\n\tconst auto outlineRect = QRectF(geometry).adjusted(\n\t\t-padding - outlineWidth / 2,\n\t\t-padding - outlineWidth / 2,\n\t\tpadding + outlineWidth / 2,\n\t\tpadding + outlineWidth / 2);\n\n\tUi::PaintOutlineSegments(p, outlineRect, _storySegments);\n\n\tif (_hasLiveStories) {\n\t\tconst auto outline = _edgeColor.current().value_or(\n\t\t\t_solidBg.value_or(st::boxDividerBg->c));\n\t\tUi::PaintLiveBadge(\n\t\t\tp,\n\t\t\tgeometry.x(),\n\t\t\tgeometry.y() + outlineWidth + padding,\n\t\t\tgeometry.width(),\n\t\t\toutline);\n\t}\n}\n\nvoid TopBar::setupStatusWithRating() {\n\t_status->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tif (const auto rating = _starsRating.get()) {\n\t\t_statusShift = rating->widthValue();\n\t\t_statusShift.changes() | rpl::on_next([=] {\n\t\t\tupdateLabelsPosition();\n\t\t}, _status->lifetime());\n\t\trating->raise();\n\t}\n}\n\nrpl::producer<std::optional<QColor>> TopBar::edgeColor() const {\n\treturn _edgeColor.value();\n}\n\nconst style::FlatLabel &TopBar::statusStyle() const {\n\treturn _peer->isMegagroup()\n\t\t? st::infoProfileMegagroupCover.status\n\t\t: st::infoProfileCover.status;\n}\n\nrpl::producer<QString> TopBar::nameValue() const {\n\tif (const auto topic = _key.topic()) {\n\t\treturn Info::Profile::TitleValue(topic);\n\t}\n\treturn Info::Profile::NameValue(_peer);\n}\n\nTopBarActionButtonStyle TopBar::mapActionStyle(\n\t\tstd::optional<QColor> c) const {\n\tif (c) {\n\t\treturn TopBarActionButtonStyle{\n\t\t\t.bgColor = Ui::BlendColors(\n\t\t\t\t*c,\n\t\t\t\tQt::black,\n\t\t\t\tst::infoProfileTopBarActionButtonBgOpacity),\n\t\t\t.fgColor = std::make_optional(st::premiumButtonFg->c),\n\t\t\t.shadowColor = std::nullopt,\n\t\t};\n\t} else {\n\t\treturn TopBarActionButtonStyle{\n\t\t\t.bgColor = anim::with_alpha(\n\t\t\t\tst::boxBg->c,\n\t\t\t\t1. - st::infoProfileTopBarActionButtonBgOpacity),\n\t\t\t.fgColor = std::nullopt,\n\t\t\t.shadowColor = std::make_optional(st::windowShadowFgFallback->c),\n\t\t};\n\t}\n}\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_top_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"info/info_controller.h\" // Key\n#include \"info/profile/info_profile_badge.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/userpic_view.h\"\n\nnamespace Data {\nclass ForumTopic;\nclass DocumentMedia;\nstruct SavedStarGift;\nstruct ColorProfileSet;\nclass SavedStarGiftId;\n} // namespace Data\n\nnamespace Info::Profile {\nclass BadgeTooltip;\nclass TopicIconView;\n} // namespace Info::Profile\n\nnamespace Lottie {\nclass Animation;\nclass MultiPlayer;\n} // namespace Lottie\n\nnamespace Ui {\nclass UploadProgressOverlay;\nclass VideoUserpicPlayer;\nstruct OutlineSegment;\nnamespace Text {\nclass CustomEmoji;\n} // namespace Text\n} // namespace Ui\n\nclass PeerData;\n\nnamespace base {\nclass Timer;\n} // namespace base\n\nnamespace style {\nstruct InfoTopBar;\nstruct InfoPeerBadge;\nstruct FlatLabel;\n} //namespace style\n\nnamespace Ui {\nclass FlatLabel;\nclass IconButton;\nclass PopupMenu;\nclass RoundButton;\nclass StarsRating;\ntemplate <typename Widget>\nclass FadeWrap;\nclass HorizontalFitContainer;\nnamespace Animations {\nclass Simple;\n} // namespace Animations\n} //namespace Ui\n\nnamespace Info {\nclass Controller;\nclass Key;\nenum class Wrap;\n} //namespace Info\n\nnamespace Ui::Menu {\nstruct MenuCallback;\n} //namespace Ui::Menu\n\nnamespace Info::Profile {\n\nclass Badge;\nclass StatusLabel;\n\nstruct TopBarActionButtonStyle;\n\nclass TopBar final : public Ui::RpWidget {\npublic:\n\tenum class Source {\n\t\tProfile,\n\t\tStories,\n\t\tPreview,\n\t};\n\n\tstruct Descriptor {\n\t\tnot_null<Window::SessionController*> controller;\n\t\tKey key;\n\t\trpl::producer<Wrap> wrap;\n\t\tSource source = Source::Profile;\n\t\tPeerData *peer = nullptr;\n\t\trpl::producer<bool> backToggles;\n\t\trpl::producer<> showFinished;\n\t};\n\n\tstruct AnimatedPatternPoint {\n\t\tQPointF basePosition;\n\t\tfloat64 size;\n\t\tfloat64 startTime;\n\t\tfloat64 endTime;\n\t};\n\n\tTopBar(not_null<Ui::RpWidget*> parent, Descriptor descriptor);\n\t~TopBar();\n\n\t[[nodiscard]] rpl::producer<> backRequest() const;\n\n\tvoid setOnlineCount(rpl::producer<int> &&count);\n\n\tvoid setRoundEdges(bool value);\n\tvoid setLottieSingleLoop(bool value);\n\tvoid setColorProfileIndex(std::optional<uint8> index);\n\tvoid setPatternEmojiId(std::optional<DocumentId> patternEmojiId);\n\tvoid setLocalEmojiStatusId(EmojiStatusId emojiStatusId);\n\tvoid addTopBarEditButton(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tWrap wrap,\n\t\tbool shouldUseColored);\n\n\trpl::producer<std::optional<QColor>> edgeColor() const;\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid paintEdges(QPainter &p, const QBrush &brush) const;\n\tvoid paintEdges(QPainter &p) const;\n\tvoid updateLabelsPosition();\n\t[[nodiscard]] int titleMostLeft() const;\n\t[[nodiscard]] int statusMostLeft() const;\n\t[[nodiscard]] QRect userpicGeometry() const;\n\tvoid updateGiftButtonsGeometry(\n\t\tfloat64 progressCurrent,\n\t\tconst QRect &userpicRect);\n\tvoid paintUserpic(QPainter &p, const QRect &geometry);\n\tvoid updateVideoUserpic();\n\tvoid showTopBarMenu(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tbool check);\n\tvoid fillTopBarMenu(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst Ui::Menu::MenuCallback &addAction);\n\tvoid setupUserpicButton(not_null<Window::SessionController*> controller);\n\tvoid startUploadOverlay();\n\tvoid setupActions(not_null<Window::SessionController*> controller);\n\tvoid setupButtons(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tSource source);\n\tvoid setupShowLastSeen(not_null<Window::SessionController*> controller);\n\tvoid setupUniqueBadgeTooltip();\n\tvoid hideBadgeTooltip();\n\tvoid setupAnimatedPattern(const QRect &userpicGeometry = QRect());\n\tvoid paintAnimatedPattern(\n\t\tQPainter &p,\n\t\tconst QRect &rect,\n\t\tconst QRect &userpicGeometry);\n\tvoid setupPinnedToTopGifts(\n\t\tnot_null<Window::SessionController*> controller);\n\tvoid setupNewGifts(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst std::vector<Data::SavedStarGift> &gifts);\n\tvoid paintPinnedToTopGifts(\n\t\tQPainter &p,\n\t\tconst QRect &rect,\n\t\tconst QRect &userpicGeometry);\n\t[[nodiscard]] QPointF calculateGiftPosition(\n\t\tint position,\n\t\tfloat64 progress,\n\t\tconst QRect &userpicRect) const;\n\tvoid adjustColors(const std::optional<QColor> &edgeColor);\n\tvoid updateCollectibleStatus();\n\tvoid setupStoryOutline(const QRect &geometry = QRect());\n\tvoid updateStoryOutline(std::optional<QColor> edgeColor);\n\tvoid paintStoryOutline(QPainter &p, const QRect &geometry);\n\tvoid updateStatusPosition(float64 progressCurrent);\n\t[[nodiscard]] int calculateRightButtonsWidth() const;\n\t[[nodiscard]] const style::FlatLabel &statusStyle() const;\n\tvoid setupStatusWithRating();\n\t[[nodiscard]] TopBarActionButtonStyle mapActionStyle(\n\t\tstd::optional<QColor> c) const;\n\n\t[[nodiscard]] rpl::producer<QString> nameValue() const;\n\n\t[[nodiscard]] auto effectiveColorProfile()\n\tconst -> std::optional<Data::ColorProfileSet>;\n\t[[nodiscard]] auto effectiveCollectible()\n\tconst -> std::shared_ptr<Data::EmojiStatusCollectible>;\n\n\tconst not_null<PeerData*> _peer;\n\tData::ForumTopic *_topic = nullptr;\n\tconst Key _key;\n\trpl::variable<Wrap> _wrap;\n\tconst style::InfoTopBar &_st;\n\tconst Source _source;\n\n\tstd::unique_ptr<base::Timer> _badgeTooltipHide;\n\tconst std::unique_ptr<Badge> _botVerify;\n\trpl::variable<Badge::Content> _badgeContent;\n\tconst Fn<bool()> _gifPausedChecker;\n\tconst std::unique_ptr<Badge> _badge;\n\tconst std::unique_ptr<Badge> _verified;\n\n\tconst bool _hasActions;\n\tconst int _minForProgress;\n\n\tstd::unique_ptr<BadgeTooltip> _badgeTooltip;\n\tstd::vector<std::unique_ptr<BadgeTooltip>> _badgeOldTooltips;\n\tuint64 _badgeCollectibleId = 0;\n\n\tobject_ptr<Ui::FlatLabel> _title;\n\tstd::unique_ptr<Ui::StarsRating> _starsRating;\n\tobject_ptr<Ui::FlatLabel> _status;\n\tstd::unique_ptr<StatusLabel> _statusLabel;\n\trpl::variable<int> _statusShift = 0;\n\tobject_ptr<Ui::FadeWrap<Ui::RoundButton>> _showLastSeen = { nullptr };\n\tobject_ptr<Ui::RoundButton> _forumButton = { nullptr };\n\n\tstd::shared_ptr<style::FlatLabel> _statusSt;\n\tstd::shared_ptr<style::InfoPeerBadge> _botVerifySt;\n\tstd::shared_ptr<style::InfoPeerBadge> _badgeSt;\n\tstd::shared_ptr<style::InfoPeerBadge> _verifiedSt;\n\n\trpl::variable<float64> _progress = 0.;\n\tbool _roundEdges = true;\n\n\trpl::variable<std::optional<QColor>> _edgeColor;\n\tbool _hasGradientBg = false;\n\tstd::optional<QColor> _solidBg;\n\tQImage _cachedGradient;\n\tQPainterPath _cachedClipPath;\n\tstd::unique_ptr<Ui::Text::CustomEmoji> _patternEmoji;\n\tQImage _basePatternImage;\n\n\tstd::vector<AnimatedPatternPoint> _animatedPoints;\n\tQRect _lastUserpicRect;\n\n\tbase::unique_qptr<Ui::AbstractButton> _userpicButton;\n\n\tUi::PeerUserpicView _userpicView;\n\tInMemoryKey _userpicUniqueKey;\n\tQImage _cachedUserpic;\n\tQImage _monoforumMask;\n\tstd::unique_ptr<Ui::VideoUserpicPlayer> _videoUserpicPlayer;\n\tstd::unique_ptr<TopicIconView> _topicIconView;\n\tstd::unique_ptr<Ui::UploadProgressOverlay> _uploadOverlay;\n\trpl::lifetime _uploadLifetime;\n\tbool _waitingUserpicCloudLoad = false;\n\trpl::lifetime _userpicLoadingLifetime;\n\n\tbase::unique_qptr<Ui::IconButton> _close;\n\tbase::unique_qptr<Ui::FadeWrap<Ui::IconButton>> _back;\n\trpl::variable<bool> _backToggles;\n\n\trpl::event_stream<> _backClicks;\n\n\tbase::unique_qptr<Ui::IconButton> _topBarButton;\n\tbase::unique_qptr<Ui::PopupMenu> _peerMenu;\n\n\tUi::RpWidget *_actionMore = nullptr;\n\n\tbase::unique_qptr<Ui::HorizontalFitContainer> _actions;\n\n\tstd::unique_ptr<Lottie::MultiPlayer> _lottiePlayer;\n\tbool _lottieSingleLoop = false;\n\tstruct PinnedToTopGiftEntry {\n\t\tData::SavedStarGiftId manageId;\n\t\t// QString slug;\n\t\tLottie::Animation *animation = nullptr;\n\t\tstd::shared_ptr<Data::DocumentMedia> media;\n\t\tQImage bg;\n\t\tQImage lastFrame;\n\t\tint position = 0;\n\t\tbase::unique_qptr<Ui::AbstractButton> button;\n\t};\n\tbool _pinnedToTopGiftsFirstTimeShowed = false;\n\tstd::vector<PinnedToTopGiftEntry> _pinnedToTopGifts;\n\tstd::unique_ptr<Ui::Animations::Simple> _giftsAppearing;\n\tstd::unique_ptr<Ui::Animations::Simple> _giftsHiding;\n\trpl::lifetime _giftsLoadingLifetime;\n\n\tQBrush _storyOutlineBrush;\n\tstd::vector<Ui::OutlineSegment> _storySegments;\n\tbool _hasStories = false;\n\tbool _hasLiveStories = false;\n\n\tstd::optional<uint8> _localColorProfileIndex;\n\tstd::optional<DocumentId> _localPatternEmojiId;\n\tstd::shared_ptr<Data::EmojiStatusCollectible> _localCollectible;\n\n};\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_top_bar_action_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_top_bar_action_button.h\"\n\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info::Profile {\nnamespace {\n\nconstexpr auto kIconFadeStart = 0.4;\nconstexpr auto kIconFadeRange = 1.0 - kIconFadeStart;\n\n} // namespace\n\nTopBarActionButton::TopBarActionButton(\n\tnot_null<QWidget*> parent,\n\tconst QString &text,\n\tconst QString &lottieName)\n: RippleButton(parent, st::universalRippleAnimation)\n, _text(text) {\n\tsetupLottie(lottieName);\n}\n\nTopBarActionButton::TopBarActionButton(\n\tnot_null<QWidget*> parent,\n\tconst QString &text,\n\tconst style::icon &icon)\n: RippleButton(parent, st::universalRippleAnimation)\n, _text(text)\n, _icon(&icon) {\n}\n\nTopBarActionButton::~TopBarActionButton() = default;\n\nvoid TopBarActionButton::setupLottie(const QString &lottieName) {\n\t_lottie = std::make_unique<Lottie::Icon>(Lottie::IconDescriptor{\n\t\t.name = lottieName,\n\t\t.color = _lottieColor,\n\t\t.sizeOverride = Size(st::infoProfileTopBarActionButtonLottieSize),\n\t});\n\t_lottie->animate([=] { update(); }, 0, _lottie->framesCount() - 1);\n}\n\nvoid TopBarActionButton::convertToToggle(\n\t\tconst style::icon &offIcon,\n\t\tconst style::icon &onIcon,\n\t\tconst QString &offLottie,\n\t\tconst QString &onLottie) {\n\t_isToggle = true;\n\t_offIcon = &offIcon;\n\t_onIcon = &onIcon;\n\t_offLottie = offLottie;\n\t_onLottie = onLottie;\n\t_icon = _offIcon;\n\t_lottie.reset();\n}\n\nvoid TopBarActionButton::toggle(bool state) {\n\tif (!_isToggle || _toggleState == state) {\n\t\treturn;\n\t}\n\t_toggleState = state;\n\tconst auto &lottie = _toggleState ? _onLottie : _offLottie;\n\tsetupLottie(lottie);\n\t_lottie->animate([=] {\n\t\tupdate();\n\t\tif (_lottie->frameIndex() == _lottie->framesCount() - 1) {\n\t\t\t_icon = _toggleState ? _onIcon : _offIcon;\n\t\t\t_lottie.reset();\n\t\t}\n\t}, 0, _lottie->framesCount() - 1);\n}\n\nvoid TopBarActionButton::finishAnimating() {\n\tif (_lottie) {\n\t\t_icon = _toggleState ? _onIcon : _offIcon;\n\t\t_lottie.reset();\n\t\tupdate();\n\t}\n}\n\nvoid TopBarActionButton::setText(const QString &text) {\n\t_text = text;\n\tupdate();\n}\n\nvoid TopBarActionButton::setLottieColor(const style::color *color) {\n\t_lottieColor = color;\n\t_lottie.reset();\n\tupdate();\n}\n\nvoid TopBarActionButton::setStyle(const TopBarActionButtonStyle &style) {\n\t_bgColor = style.bgColor;\n\t_fgColor = style.fgColor;\n\t_shadowColor = style.shadowColor;\n\tupdate();\n}\n\nvoid TopBarActionButton::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\n\tconst auto progress = float64(height())\n\t\t/ st::infoProfileTopBarActionButtonSize;\n\tp.setOpacity(progress);\n\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(_bgColor);\n\t{\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t// Todo shadows.\n\t\tp.drawRoundedRect(rect(), st::boxRadius, st::boxRadius);\n\t}\n\n\tpaintRipple(p, 0, 0);\n\n\tconst auto iconSize = st::infoProfileTopBarActionButtonIconSize;\n\tconst auto iconTop = st::infoProfileTopBarActionButtonIconTop;\n\n\tif (_lottie || _icon) {\n\t\tconst auto iconScale = (progress > kIconFadeStart)\n\t\t\t? (progress - kIconFadeStart) / kIconFadeRange\n\t\t\t: 0.0;\n\t\tp.setOpacity(iconScale);\n\t\tp.save();\n\t\tconst auto iconLeft = (width() - iconSize) / 2.;\n\t\tconst auto half = iconSize / 2.;\n\t\tconst auto iconCenter = QPointF(iconLeft + half, iconTop + half);\n\t\tp.translate(iconCenter);\n\t\tp.scale(iconScale, iconScale);\n\t\tp.translate(-iconCenter);\n\t\tp.translate(iconLeft, iconTop);\n\t\tif (_lottie) {\n\t\t\t_lottie->paint(p, 0, 0, _fgColor);\n\t\t} else if (_icon) {\n\t\t\tif (_fgColor) {\n\t\t\t\t_icon->paint(p, 0, 0, width(), *_fgColor);\n\t\t\t} else {\n\t\t\t\t_icon->paint(p, 0, 0, width());\n\t\t\t}\n\t\t}\n\t\tp.restore();\n\t\tp.setOpacity(progress);\n\t}\n\n\tconst auto skip = st::infoProfileTopBarActionButtonTextSkip;\n\n\tp.setClipRect(0, 0, width(), height() - skip);\n\n\tif (_fgColor.has_value()) {\n\t\tp.setPen(*_fgColor);\n\t} else {\n\t\tp.setPen(st::windowBoldFg);\n\t}\n\n\tp.setFont(st::infoProfileTopBarActionButtonFont);\n\n\tconst auto textScale = std::max(kIconFadeStart, progress);\n\tconst auto textRect = rect()\n\t\t- QMargins(0, st::infoProfileTopBarActionButtonTextTop, 0, 0);\n\tconst auto textCenter = rect::center(textRect);\n\tp.translate(textCenter);\n\tp.scale(textScale, textScale);\n\tp.translate(-textCenter);\n\n\tconst auto elidedText = st::infoProfileTopBarActionButtonFont->elided(\n\t\t_text,\n\t\ttextRect.width(),\n\t\tQt::ElideMiddle);\n\tp.drawText(textRect, elidedText, style::al_top);\n}\n\nQImage TopBarActionButton::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::RoundRectMask(size(), st::boxRadius);\n}\n\nQPoint TopBarActionButton::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos());\n}\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_top_bar_action_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/buttons.h\"\n\nnamespace Lottie {\nclass Icon;\n} // namespace Lottie\n\nnamespace Info::Profile {\n\nstruct TopBarActionButtonStyle {\n\tQColor bgColor;\n\tstd::optional<QColor> fgColor;\n\tstd::optional<QColor> shadowColor;\n};\n\nclass TopBarActionButton final : public Ui::RippleButton {\npublic:\n\tTopBarActionButton(\n\t\tnot_null<QWidget*> parent,\n\t\tconst QString &text,\n\t\tconst QString &lottieName);\n\tTopBarActionButton(\n\t\tnot_null<QWidget*> parent,\n\t\tconst QString &text,\n\t\tconst style::icon &icon);\n\n\tvoid convertToToggle(\n\t\tconst style::icon &offIcon,\n\t\tconst style::icon &onIcon,\n\t\tconst QString &offLottie,\n\t\tconst QString &onLottie);\n\tvoid setLottieColor(const style::color *color);\n\tvoid toggle(bool state);\n\tvoid finishAnimating();\n\tvoid setText(const QString &text);\n\tvoid setStyle(const TopBarActionButtonStyle &style);\n\n\t~TopBarActionButton();\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\nprivate:\n\tvoid setupLottie(const QString &lottieName);\n\n\tQString _text;\n\tstd::unique_ptr<Lottie::Icon> _lottie;\n\tconst style::icon *_icon = nullptr;\n\n\tbool _isToggle = false;\n\tbool _toggleState = false;\n\tconst style::icon *_offIcon = nullptr;\n\tconst style::icon *_onIcon = nullptr;\n\tQString _offLottie;\n\tQString _onLottie;\n\tconst style::color *_lottieColor = nullptr;\n\n\tQColor _bgColor;\n\tstd::optional<QColor> _fgColor;\n\tstd::optional<QColor> _shadowColor;\n\n};\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_values.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_values.h\"\n\n#include \"api/api_chat_participants.h\"\n#include \"apiwrap.h\"\n#include \"info/profile/info_profile_phone_menu.h\"\n#include \"info/profile/info_profile_badge.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"countries/countries_instance.h\"\n#include \"main/main_session.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/text/format_values.h\" // Ui::FormatPhone\n#include \"ui/text/text_utilities.h\"\n#include \"lang/lang_keys.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_shared_media.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_session.h\"\n#include \"data/data_premium_limits.h\"\n#include \"boxes/peers/edit_peer_permissions_box.h\"\n#include \"base/unixtime.h\"\n\nnamespace Info {\nnamespace Profile {\nnamespace {\n\nusing UpdateFlag = Data::PeerUpdate::Flag;\n\nauto PlainAboutValue(not_null<PeerData*> peer) {\n\treturn peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tUpdateFlag::About\n\t) | rpl::map([=] {\n\t\treturn peer->about();\n\t});\n}\n\nauto PlainUsernameValue(not_null<PeerData*> peer) {\n\treturn rpl::merge(\n\t\tpeer->session().changes().peerFlagsValue(peer, UpdateFlag::Username),\n\t\tpeer->session().changes().peerFlagsValue(peer, UpdateFlag::Usernames)\n\t) | rpl::map([=] {\n\t\treturn peer->username();\n\t});\n}\n\nauto PlainPrimaryUsernameValue(not_null<PeerData*> peer) {\n\treturn UsernamesValue(\n\t\tpeer\n\t) | rpl::map([=](std::vector<TextWithEntities> usernames) {\n\t\tif (!usernames.empty()) {\n\t\t\treturn rpl::single(usernames.front().text) | rpl::type_erased;\n\t\t} else {\n\t\t\treturn PlainUsernameValue(peer) | rpl::type_erased;\n\t\t}\n\t}) | rpl::flatten_latest();\n}\n\nvoid StripExternalLinks(TextWithEntities &text) {\n\tconst auto local = [](const QString &url) {\n\t\treturn !UrlRequiresConfirmation(QUrl::fromUserInput(url));\n\t};\n\tconst auto notLocal = [&](const EntityInText &entity) {\n\t\tif (entity.type() == EntityType::CustomUrl) {\n\t\t\treturn !local(entity.data());\n\t\t} else if (entity.type() == EntityType::Url) {\n\t\t\treturn !local(text.text.mid(entity.offset(), entity.length()));\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t};\n\ttext.entities.erase(\n\t\tranges::remove_if(text.entities, notLocal),\n\t\ttext.entities.end());\n}\n\n} // namespace\n\nrpl::producer<QString> NameValue(not_null<PeerData*> peer) {\n\tif (const auto broadcast = peer->monoforumBroadcast()) {\n\t\treturn NameValue(broadcast);\n\t}\n\treturn peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tUpdateFlag::Name\n\t) | rpl::map([=] { return peer->name(); });\n}\n\nrpl::producer<QString> TitleValue(not_null<Data::ForumTopic*> topic) {\n\treturn topic->session().changes().topicFlagsValue(\n\t\ttopic,\n\t\tData::TopicUpdate::Flag::Title\n\t) | rpl::map([=] { return topic->title(); });\n}\n\nrpl::producer<DocumentId> IconIdValue(not_null<Data::ForumTopic*> topic) {\n\treturn topic->session().changes().topicFlagsValue(\n\t\ttopic,\n\t\tData::TopicUpdate::Flag::IconId\n\t) | rpl::map([=] { return topic->iconId(); });\n}\n\nrpl::producer<int32> ColorIdValue(not_null<Data::ForumTopic*> topic) {\n\treturn topic->session().changes().topicFlagsValue(\n\t\ttopic,\n\t\tData::TopicUpdate::Flag::ColorId\n\t) | rpl::map([=] { return topic->colorId(); });\n}\n\nrpl::producer<TextWithEntities> PhoneValue(not_null<UserData*> user) {\n\treturn rpl::merge(\n\t\tCountries::Instance().updated(),\n\t\tuser->session().changes().peerFlagsValue(\n\t\t\tuser,\n\t\t\tUpdateFlag::PhoneNumber) | rpl::to_empty\n\t) | rpl::map([=] {\n\t\treturn tr::marked(Ui::FormatPhone(user->phone()));\n\t});\n}\n\nrpl::producer<TextWithEntities> PhoneOrHiddenValue(not_null<UserData*> user) {\n\treturn rpl::combine(\n\t\tPhoneValue(user),\n\t\tPlainUsernameValue(user),\n\t\tPlainAboutValue(user),\n\t\ttr::lng_info_mobile_hidden()\n\t) | rpl::map([user](\n\t\t\tconst TextWithEntities &phone,\n\t\t\tconst QString &username,\n\t\t\tconst QString &about,\n\t\t\tconst QString &hidden) {\n\t\tif (phone.text.isEmpty() && username.isEmpty() && about.isEmpty()) {\n\t\t\treturn tr::marked(hidden);\n\t\t} else if (IsCollectiblePhone(user)) {\n\t\t\treturn tr::link(phone, u\"internal:collectible_phone/\"_q\n\t\t\t\t+ user->phone() + '@' + QString::number(user->id.value));\n\t\t} else {\n\t\t\treturn phone;\n\t\t}\n\t});\n}\n\nrpl::producer<TextWithEntities> UsernameValue(\n\t\tnot_null<PeerData*> peer,\n\t\tbool primary) {\n\treturn (primary\n\t\t? PlainPrimaryUsernameValue(peer)\n\t\t: (PlainUsernameValue(peer) | rpl::type_erased)\n\t) | rpl::map([](QString &&username) {\n\t\treturn username.isEmpty()\n\t\t\t? tr::marked()\n\t\t\t: tr::marked('@' + username);\n\t});\n}\n\nQString UsernameUrl(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &username,\n\t\tbool link) {\n\tconst auto type = !peer->isUsernameEditable(username)\n\t\t? u\"collectible_username\"_q\n\t\t: link\n\t\t? u\"username_link\"_q\n\t\t: u\"username_regular\"_q;\n\treturn u\"internal:\"_q\n\t\t+ type\n\t\t+ u\"/\"_q\n\t\t+ username\n\t\t+ \"@\"\n\t\t+ QString::number(peer->id.value);\n}\n\nrpl::producer<std::vector<TextWithEntities>> UsernamesValue(\n\t\tnot_null<PeerData*> peer) {\n\tconst auto map = [=](const std::vector<QString> &usernames) {\n\t\treturn ranges::views::all(\n\t\t\tusernames\n\t\t) | ranges::views::transform([&](const QString &u) {\n\t\t\treturn tr::link(u, UsernameUrl(peer, u));\n\t\t}) | ranges::to_vector;\n\t};\n\tauto value = rpl::merge(\n\t\tpeer->session().changes().peerFlagsValue(peer, UpdateFlag::Username),\n\t\tpeer->session().changes().peerFlagsValue(peer, UpdateFlag::Usernames)\n\t);\n\tif (const auto user = peer->asUser()) {\n\t\treturn std::move(value) | rpl::map([=] {\n\t\t\treturn map(user->usernames());\n\t\t});\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn std::move(value) | rpl::map([=] {\n\t\t\treturn map(channel->usernames());\n\t\t});\n\t} else {\n\t\treturn rpl::single(std::vector<TextWithEntities>());\n\t}\n}\n\nTextWithEntities AboutWithEntities(\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &value) {\n\tauto flags = TextParseLinks | TextParseMentions;\n\tconst auto user = peer->asUser();\n\tconst auto isBot = user && user->isBot();\n\tconst auto isPremium = user && user->isPremium();\n\tif (!user) {\n\t\tflags |= TextParseHashtags;\n\t} else if (isBot) {\n\t\tflags |= TextParseHashtags | TextParseBotCommands;\n\t}\n\tconst auto stripExternal = peer->isChat()\n\t\t|| peer->isMegagroup()\n\t\t|| (user && !isBot && !isPremium);\n\tauto result = TextWithEntities{ value };\n\tTextUtilities::ParseEntities(result, flags);\n\tif (stripExternal) {\n\t\tStripExternalLinks(result);\n\t}\n\treturn result;\n}\n\nrpl::producer<TextWithEntities> AboutValue(not_null<PeerData*> peer) {\n\treturn PlainAboutValue(\n\t\tpeer\n\t) | rpl::map([peer](const QString &value) {\n\t\treturn AboutWithEntities(peer, value);\n\t});\n}\n\nQString TopicLink(not_null<Data::ForumTopic*> topic, bool full) {\n\tconst auto channel = topic->channel();\n\tconst auto id = topic->rootId();\n\tconst auto base = channel->hasUsername()\n\t\t? channel->username()\n\t\t: \"c/\" + QString::number(peerToChannel(channel->id).bare);\n\treturn channel->session().createInternalLinkFull(full\n\t\t? base + '/' + QString::number(id.bare)\n\t\t: base);\n}\n\nrpl::producer<LinkWithUrl> LinkValue(\n\t\tnot_null<PeerData*> peer,\n\t\tbool primary,\n\t\tMsgId rootId) {\n\treturn (primary\n\t\t? PlainPrimaryUsernameValue(peer)\n\t\t: PlainUsernameValue(peer) | rpl::type_erased\n\t) | rpl::map([=](QString &&username) {\n\t\tif (username.isEmpty()) {\n\t\t\tif (const auto topic\n\t\t\t\t= rootId ? peer->forumTopicFor(rootId) : nullptr) {\n\t\t\t\tconst auto link = TopicLink(topic, false);\n\t\t\t\treturn LinkWithUrl{\n\t\t\t\t\t.text = link,\n\t\t\t\t\t.url = link,\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\treturn LinkWithUrl{};\n\t\t\t}\n\t\t} else {\n\t\t\treturn LinkWithUrl{\n\t\t\t\t.text = peer->session().createInternalLinkFull(username),\n\t\t\t\t.url = UsernameUrl(peer, username, true),\n\t\t\t};\n\t\t}\n\t\treturn LinkWithUrl{\n\t\t\t.text = (username.isEmpty()\n\t\t\t\t? QString()\n\t\t\t\t: peer->session().createInternalLinkFull(username)),\n\t\t\t.url = (username.isEmpty()\n\t\t\t\t? QString()\n\t\t\t\t: UsernameUrl(peer, username, true)),\n\t\t};\n\t});\n}\n\nrpl::producer<const ChannelLocation*> LocationValue(\n\t\tnot_null<ChannelData*> channel) {\n\treturn channel->session().changes().peerFlagsValue(\n\t\tchannel,\n\t\tUpdateFlag::ChannelLocation\n\t) | rpl::map([=] {\n\t\treturn channel->getLocation();\n\t});\n}\n\nrpl::producer<bool> NotificationsEnabledValue(\n\t\tnot_null<Data::Thread*> thread) {\n\tconst auto topic = thread->asTopic();\n\tif (!topic) {\n\t\treturn NotificationsEnabledValue(thread->peer());\n\t}\n\treturn rpl::merge(\n\t\ttopic->session().changes().topicFlagsValue(\n\t\t\ttopic,\n\t\t\tData::TopicUpdate::Flag::Notifications\n\t\t) | rpl::to_empty,\n\t\ttopic->session().changes().peerUpdates(\n\t\t\ttopic->peer(),\n\t\t\tUpdateFlag::Notifications\n\t\t) | rpl::to_empty,\n\t\ttopic->owner().notifySettings().defaultUpdates(topic->peer())\n\t) | rpl::map([=] {\n\t\treturn !topic->owner().notifySettings().isMuted(topic);\n\t}) | rpl::distinct_until_changed();\n}\n\nrpl::producer<bool> NotificationsEnabledValue(not_null<PeerData*> peer) {\n\treturn rpl::merge(\n\t\tpeer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tUpdateFlag::Notifications\n\t\t) | rpl::to_empty,\n\t\tpeer->owner().notifySettings().defaultUpdates(peer)\n\t) | rpl::map([=] {\n\t\treturn !peer->owner().notifySettings().isMuted(peer);\n\t}) | rpl::distinct_until_changed();\n}\n\nrpl::producer<bool> IsContactValue(not_null<UserData*> user) {\n\treturn user->session().changes().peerFlagsValue(\n\t\tuser,\n\t\tUpdateFlag::IsContact\n\t) | rpl::map([=] {\n\t\treturn user->isContact();\n\t});\n}\n\n[[nodiscard]] rpl::producer<QString> InviteToChatButton(\n\t\tnot_null<UserData*> user) {\n\tif (!user->isBot()\n\t\t|| user->isRepliesChat()\n\t\t|| user->isVerifyCodes()\n\t\t|| user->isSupport()) {\n\t\treturn rpl::single(QString());\n\t}\n\tusing Flag = Data::PeerUpdate::Flag;\n\treturn user->session().changes().peerFlagsValue(\n\t\tuser,\n\t\tFlag::BotCanBeInvited | Flag::Rights\n\t) | rpl::map([=] {\n\t\tconst auto info = user->botInfo.get();\n\t\treturn info->cantJoinGroups\n\t\t\t? (info->channelAdminRights\n\t\t\t\t? tr::lng_profile_invite_to_channel(tr::now)\n\t\t\t\t: QString())\n\t\t\t: (info->channelAdminRights\n\t\t\t\t? tr::lng_profile_add_bot_as_admin(tr::now)\n\t\t\t\t: tr::lng_profile_invite_to_group(tr::now));\n\t});\n}\n\n[[nodiscard]] rpl::producer<QString> InviteToChatAbout(\n\t\tnot_null<UserData*> user) {\n\tif (!user->isBot()\n\t\t|| user->isRepliesChat()\n\t\t|| user->isVerifyCodes()\n\t\t|| user->isSupport()) {\n\t\treturn rpl::single(QString());\n\t}\n\tusing Flag = Data::PeerUpdate::Flag;\n\treturn user->session().changes().peerFlagsValue(\n\t\tuser,\n\t\tFlag::BotCanBeInvited | Flag::Rights\n\t) | rpl::map([=] {\n\t\tconst auto info = user->botInfo.get();\n\t\treturn (info->cantJoinGroups || !info->groupAdminRights)\n\t\t\t? (info->channelAdminRights\n\t\t\t\t? tr::lng_profile_invite_to_channel_about(tr::now)\n\t\t\t\t: QString())\n\t\t\t: (info->channelAdminRights\n\t\t\t\t? tr::lng_profile_add_bot_as_admin_about(tr::now)\n\t\t\t\t: tr::lng_profile_invite_to_group_about(tr::now));\n\t});\n}\n\nrpl::producer<bool> CanShareContactValue(not_null<UserData*> user) {\n\treturn user->session().changes().peerFlagsValue(\n\t\tuser,\n\t\tUpdateFlag::CanShareContact\n\t) | rpl::map([=] {\n\t\treturn user->canShareThisContact();\n\t});\n}\n\nrpl::producer<bool> CanAddContactValue(not_null<UserData*> user) {\n\tusing namespace rpl::mappers;\n\tif (user->isBot() || user->isSelf() || user->isInaccessible()) {\n\t\treturn rpl::single(false);\n\t}\n\treturn IsContactValue(\n\t\tuser\n\t) | rpl::map(!_1);\n}\n\nrpl::producer<Data::Birthday> BirthdayValue(not_null<UserData*> user) {\n\treturn user->session().changes().peerFlagsValue(\n\t\tuser,\n\t\tUpdateFlag::Birthday\n\t) | rpl::map([=] {\n\t\treturn user->birthday();\n\t});\n}\n\nrpl::producer<ChannelData*> PersonalChannelValue(not_null<UserData*> user) {\n\treturn user->session().changes().peerFlagsValue(\n\t\tuser,\n\t\tUpdateFlag::PersonalChannel\n\t) | rpl::map([=] {\n\t\tconst auto channelId = user->personalChannelId();\n\t\treturn channelId ? user->owner().channel(channelId).get() : nullptr;\n\t});\n}\n\nrpl::producer<bool> AmInChannelValue(not_null<ChannelData*> channel) {\n\treturn channel->session().changes().peerFlagsValue(\n\t\tchannel,\n\t\tUpdateFlag::ChannelAmIn\n\t) | rpl::map([=] {\n\t\treturn channel->amIn();\n\t});\n}\n\nrpl::producer<int> MembersCountValue(not_null<PeerData*> peer) {\n\tif (const auto chat = peer->asChat()) {\n\t\treturn peer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tUpdateFlag::Members\n\t\t) | rpl::map([=] {\n\t\t\treturn chat->amIn()\n\t\t\t\t? std::max(chat->count, int(chat->participants.size()))\n\t\t\t\t: 0;\n\t\t});\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn peer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tUpdateFlag::Members\n\t\t) | rpl::map([=] {\n\t\t\treturn channel->membersCount();\n\t\t});\n\t}\n\tUnexpected(\"User in MembersCountViewer().\");\n}\n\nrpl::producer<int> PendingRequestsCountValue(not_null<PeerData*> peer) {\n\tif (const auto chat = peer->asChat()) {\n\t\treturn peer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tUpdateFlag::PendingRequests\n\t\t) | rpl::map([=] {\n\t\t\treturn chat->pendingRequestsCount();\n\t\t});\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn peer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tUpdateFlag::PendingRequests\n\t\t) | rpl::map([=] {\n\t\t\treturn channel->pendingRequestsCount();\n\t\t});\n\t}\n\tUnexpected(\"User in MembersCountViewer().\");\n}\n\nrpl::producer<int> AdminsCountValue(not_null<PeerData*> peer) {\n\tif (const auto chat = peer->asChat()) {\n\t\treturn peer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tUpdateFlag::Admins | UpdateFlag::Rights\n\t\t) | rpl::map([=] {\n\t\t\treturn chat->participants.empty()\n\t\t\t\t? 0\n\t\t\t\t: int(chat->admins.size() + (chat->creator ? 1 : 0));\n\t\t});\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn peer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tUpdateFlag::Admins | UpdateFlag::Rights\n\t\t) | rpl::map([=] {\n\t\t\treturn channel->canViewAdmins()\n\t\t\t\t? channel->adminsCount()\n\t\t\t\t: 0;\n\t\t});\n\t}\n\tUnexpected(\"User in AdminsCountValue().\");\n}\n\n\nrpl::producer<int> RestrictionsCountValue(not_null<PeerData*> peer) {\n\tconst auto countOfRestrictions = [](\n\t\t\tData::RestrictionsSetOptions options,\n\t\t\tChatRestrictions restrictions) {\n\t\tauto count = 0;\n\t\tconst auto list = Data::ListOfRestrictions(options);\n\t\tfor (const auto &f : list) {\n\t\t\tif (restrictions & f) count++;\n\t\t}\n\t\treturn int(list.size()) - count;\n\t};\n\n\tif (const auto chat = peer->asChat()) {\n\t\treturn peer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tUpdateFlag::Rights\n\t\t) | rpl::map([=] {\n\t\t\treturn countOfRestrictions({}, chat->defaultRestrictions());\n\t\t});\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn rpl::combine(\n\t\t\tData::PeerFlagValue(channel, ChannelData::Flag::Forum),\n\t\t\tchannel->session().changes().peerFlagsValue(\n\t\t\t\tchannel,\n\t\t\t\tUpdateFlag::Rights)\n\t\t) | rpl::map([=] {\n\t\t\treturn countOfRestrictions(\n\t\t\t\t{ .isForum = channel->isForum() },\n\t\t\t\tchannel->defaultRestrictions());\n\t\t});\n\t}\n\tUnexpected(\"User in RestrictionsCountValue().\");\n}\n\nrpl::producer<not_null<PeerData*>> MigratedOrMeValue(\n\t\tnot_null<PeerData*> peer) {\n\tif (const auto chat = peer->asChat()) {\n\t\treturn peer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tUpdateFlag::Migration\n\t\t) | rpl::map([=] {\n\t\t\treturn chat->migrateToOrMe();\n\t\t});\n\t} else {\n\t\treturn rpl::single(peer);\n\t}\n}\n\nrpl::producer<int> RestrictedCountValue(not_null<ChannelData*> channel) {\n\treturn channel->session().changes().peerFlagsValue(\n\t\tchannel,\n\t\tUpdateFlag::BannedUsers | UpdateFlag::Rights\n\t) | rpl::map([=] {\n\t\treturn channel->canViewBanned()\n\t\t\t? channel->restrictedCount()\n\t\t\t: 0;\n\t});\n}\n\nrpl::producer<int> KickedCountValue(not_null<ChannelData*> channel) {\n\treturn channel->session().changes().peerFlagsValue(\n\t\tchannel,\n\t\tUpdateFlag::BannedUsers | UpdateFlag::Rights\n\t) | rpl::map([=] {\n\t\treturn channel->canViewBanned()\n\t\t\t? channel->kickedCount()\n\t\t\t: 0;\n\t});\n}\n\nrpl::producer<int> SharedMediaCountValue(\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tPeerData *migrated,\n\t\tStorage::SharedMediaType type) {\n\tauto aroundId = 0;\n\tauto limit = 0;\n\tauto updated = SharedMediaMergedViewer(\n\t\t&peer->session(),\n\t\tSharedMediaMergedKey(\n\t\t\tSparseIdsMergedSlice::Key(\n\t\t\t\tpeer->id,\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tmigrated ? migrated->id : 0,\n\t\t\t\taroundId),\n\t\t\ttype),\n\t\tlimit,\n\t\tlimit\n\t) | rpl::map([](const SparseIdsMergedSlice &slice) {\n\t\treturn slice.fullCount();\n\t}) | rpl::filter_optional();\n\treturn rpl::single(0) | rpl::then(std::move(updated));\n}\n\nrpl::producer<int> CommonGroupsCountValue(not_null<UserData*> user) {\n\treturn user->session().changes().peerFlagsValue(\n\t\tuser,\n\t\tUpdateFlag::CommonChats\n\t) | rpl::map([=] {\n\t\treturn user->commonChatsCount();\n\t});\n}\n\nrpl::producer<int> SimilarPeersCountValue(\n\t\tnot_null<PeerData*> peer) {\n\tconst auto participants = &peer->session().api().chatParticipants();\n\tparticipants->loadSimilarPeers(peer);\n\treturn rpl::single(peer) | rpl::then(\n\t\tparticipants->similarLoaded()\n\t) | rpl::filter(\n\t\trpl::mappers::_1 == peer\n\t) | rpl::map([=] {\n\t\tconst auto &similar = participants->similar(peer);\n\t\treturn int(similar.list.size()) + similar.more;\n\t});\n}\n\nrpl::producer<int> SavedSublistCountValue(\n\t\tnot_null<PeerData*> peer) {\n\tconst auto saved = &peer->owner().savedMessages();\n\tconst auto sublist = saved->sublist(peer);\n\tif (!sublist->fullCount().has_value()) {\n\t\tsublist->loadFullCount();\n\t\treturn rpl::single(0) | rpl::then(sublist->fullCountValue());\n\t}\n\treturn sublist->fullCountValue();\n}\n\nrpl::producer<int> PeerGiftsCountValue(not_null<PeerData*> peer) {\n\treturn peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tUpdateFlag::PeerGifts\n\t) | rpl::map([=] {\n\t\treturn peer->peerGiftsCount();\n\t});\n}\n\nrpl::producer<bool> CanAddMemberValue(not_null<PeerData*> peer) {\n\tif (const auto chat = peer->asChat()) {\n\t\treturn peer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tUpdateFlag::Rights\n\t\t) | rpl::map([=] {\n\t\t\treturn chat->canAddMembers();\n\t\t});\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn peer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tUpdateFlag::Rights\n\t\t) | rpl::map([=] {\n\t\t\treturn channel->canAddMembers();\n\t\t});\n\t}\n\treturn rpl::single(false);\n}\n\nrpl::producer<int> FullReactionsCountValue(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto reactions = &session->data().reactions();\n\treturn rpl::single(rpl::empty) | rpl::then(\n\t\treactions->defaultUpdates()\n\t) | rpl::map([=] {\n\t\treturn int(reactions->list(Data::Reactions::Type::Active).size());\n\t}) | rpl::distinct_until_changed();\n}\n\nrpl::producer<bool> CanViewParticipantsValue(\n\t\tnot_null<ChannelData*> megagroup) {\n\tif (megagroup->amCreator()) {\n\t\treturn rpl::single(true);\n\t}\n\treturn rpl::combine(\n\t\tmegagroup->session().changes().peerFlagsValue(\n\t\t\tmegagroup,\n\t\t\tUpdateFlag::Rights),\n\t\tmegagroup->flagsValue(),\n\t\t[=] { return megagroup->canViewMembers(); }\n\t) | rpl::distinct_until_changed();\n}\n\ntemplate <typename Flag, typename Peer>\nrpl::producer<BadgeType> BadgeValueFromFlags(Peer peer) {\n\treturn rpl::combine(\n\t\tData::PeerFlagsValue(\n\t\t\tpeer,\n\t\t\tFlag::Verified | Flag::Scam | Flag::Fake),\n\t\tData::PeerPremiumValue(peer)\n\t) | rpl::map([=](base::flags<Flag> value, bool premium) {\n\t\treturn (value & Flag::Scam)\n\t\t\t? BadgeType::Scam\n\t\t\t: (value & Flag::Fake)\n\t\t\t? BadgeType::Fake\n\t\t\t: peer->isMonoforum()\n\t\t\t? BadgeType::Direct\n\t\t\t: (value & Flag::Verified)\n\t\t\t? BadgeType::Verified\n\t\t\t: premium\n\t\t\t? BadgeType::Premium\n\t\t\t: BadgeType::None;\n\t});\n}\n\nrpl::producer<BadgeType> BadgeValue(not_null<PeerData*> peer) {\n\tif (const auto user = peer->asUser()) {\n\t\treturn BadgeValueFromFlags<UserDataFlag>(user);\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn BadgeValueFromFlags<ChannelDataFlag>(channel);\n\t}\n\treturn rpl::single(BadgeType::None);\n}\n\nrpl::producer<EmojiStatusId> EmojiStatusIdValue(not_null<PeerData*> peer) {\n\tif (peer->isChat()) {\n\t\treturn rpl::single(EmojiStatusId());\n\t}\n\treturn peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::EmojiStatus\n\t) | rpl::map([=] { return peer->emojiStatusId(); });\n}\n\nrpl::producer<QString> BirthdayLabelText(\n\t\trpl::producer<Data::Birthday> birthday) {\n\treturn std::move(birthday) | rpl::map([](Data::Birthday value) {\n\t\treturn rpl::conditional(\n\t\t\tData::IsBirthdayTodayValue(value),\n\t\t\ttr::lng_info_birthday_today_label(),\n\t\t\ttr::lng_info_birthday_label());\n\t}) | rpl::flatten_latest();\n}\n\nrpl::producer<QString> BirthdayValueText(\n\t\trpl::producer<Data::Birthday> birthday,\n\t\tbool fullMonth) {\n\treturn std::move(\n\t\tbirthday\n\t) | rpl::map([=](Data::Birthday value) -> rpl::producer<QString> {\n\t\tif (!value) {\n\t\t\treturn rpl::single(QString());\n\t\t}\n\t\treturn Data::IsBirthdayTodayValue(\n\t\t\tvalue\n\t\t) | rpl::map([=](bool today) {\n\t\t\tauto text = Data::BirthdayText(value, fullMonth);\n\t\t\tif (const auto age = Data::BirthdayAge(value)) {\n\t\t\t\ttext = (today\n\t\t\t\t\t? tr::lng_info_birthday_today_years\n\t\t\t\t\t: tr::lng_info_birthday_years)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tage,\n\t\t\t\t\t\tlt_date,\n\t\t\t\t\t\ttext);\n\t\t\t}\n\t\t\tif (today) {\n\t\t\t\ttext = tr::lng_info_birthday_today(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\tData::BirthdayCake(),\n\t\t\t\t\tlt_date,\n\t\t\t\t\ttext);\n\t\t\t}\n\t\t\treturn text;\n\t\t});\n\t}) | rpl::flatten_latest();\n}\n\n} // namespace Profile\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_values.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n\n#include <rpl/producer.h>\n#include <rpl/map.h>\n\nstruct ChannelLocation;\n\nnamespace Data {\nclass ForumTopic;\nclass Thread;\nclass Birthday;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass RpWidget;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Storage {\nenum class SharedMediaType : signed char;\n} // namespace Storage\n\nnamespace Info::Profile {\n\ninline auto ToSingleLine() {\n\treturn rpl::map([](const QString &text) {\n\t\treturn TextUtilities::SingleLine(text);\n\t});\n}\n\nrpl::producer<not_null<PeerData*>> MigratedOrMeValue(\n\tnot_null<PeerData*> peer);\n\n[[nodiscard]] rpl::producer<QString> NameValue(not_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<QString> TitleValue(\n\tnot_null<Data::ForumTopic*> topic);\n[[nodiscard]] rpl::producer<DocumentId> IconIdValue(\n\tnot_null<Data::ForumTopic*> topic);\n[[nodiscard]] rpl::producer<int32> ColorIdValue(\n\tnot_null<Data::ForumTopic*> topic);\n[[nodiscard]] rpl::producer<TextWithEntities> PhoneValue(\n\tnot_null<UserData*> user);\n[[nodiscard]] rpl::producer<TextWithEntities> PhoneOrHiddenValue(\n\tnot_null<UserData*> user);\n[[nodiscard]] rpl::producer<TextWithEntities> UsernameValue(\n\tnot_null<PeerData*> peer,\n\tbool primary = false);\n[[nodiscard]] rpl::producer<std::vector<TextWithEntities>> UsernamesValue(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] QString UsernameUrl(\n\tnot_null<PeerData*> peer,\n\tconst QString &username,\n\tbool link = false);\n[[nodiscard]] TextWithEntities AboutWithEntities(\n\tnot_null<PeerData*> peer,\n\tconst QString &value);\n[[nodiscard]] rpl::producer<TextWithEntities> AboutValue(\n\tnot_null<PeerData*> peer);\n\nstruct LinkWithUrl {\n\tQString text;\n\tQString url;\n};\n[[nodiscard]] QString TopicLink(\n\tnot_null<Data::ForumTopic*> topic,\n\tbool full);\n[[nodiscard]] rpl::producer<LinkWithUrl> LinkValue(\n\tnot_null<PeerData*> peer,\n\tbool primary = false,\n\tMsgId topicRootId = 0);\n\n[[nodiscard]] rpl::producer<const ChannelLocation*> LocationValue(\n\tnot_null<ChannelData*> channel);\n[[nodiscard]] rpl::producer<bool> NotificationsEnabledValue(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<bool> NotificationsEnabledValue(\n\tnot_null<Data::Thread*> thread);\n[[nodiscard]] rpl::producer<bool> IsContactValue(not_null<UserData*> user);\n[[nodiscard]] rpl::producer<QString> InviteToChatButton(\n\tnot_null<UserData*> user);\n[[nodiscard]] rpl::producer<QString> InviteToChatAbout(\n\tnot_null<UserData*> user);\n[[nodiscard]] rpl::producer<bool> CanShareContactValue(\n\tnot_null<UserData*> user);\n[[nodiscard]] rpl::producer<bool> CanAddContactValue(\n\tnot_null<UserData*> user);\n[[nodiscard]] rpl::producer<Data::Birthday> BirthdayValue(\n\tnot_null<UserData*> user);\n[[nodiscard]] rpl::producer<ChannelData*> PersonalChannelValue(\n\tnot_null<UserData*> user);\n[[nodiscard]] rpl::producer<bool> AmInChannelValue(\n\tnot_null<ChannelData*> channel);\n[[nodiscard]] rpl::producer<int> MembersCountValue(not_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<int> PendingRequestsCountValue(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<int> AdminsCountValue(not_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<int> RestrictionsCountValue(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<int> RestrictedCountValue(\n\tnot_null<ChannelData*> channel);\n[[nodiscard]] rpl::producer<int> KickedCountValue(\n\tnot_null<ChannelData*> channel);\n[[nodiscard]] rpl::producer<int> SharedMediaCountValue(\n\tnot_null<PeerData*> peer,\n\tMsgId topicRootId,\n\tPeerId monoforumPeerId,\n\tPeerData *migrated,\n\tStorage::SharedMediaType type);\n[[nodiscard]] rpl::producer<int> CommonGroupsCountValue(\n\tnot_null<UserData*> user);\n[[nodiscard]] rpl::producer<int> SimilarPeersCountValue(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<int> SavedSublistCountValue(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<int> PeerGiftsCountValue(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<bool> CanAddMemberValue(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<int> FullReactionsCountValue(\n\tnot_null<Main::Session*> peer);\n[[nodiscard]] rpl::producer<bool> CanViewParticipantsValue(\n\tnot_null<ChannelData*> megagroup);\n\nenum class BadgeType : uchar;\n[[nodiscard]] rpl::producer<BadgeType> BadgeValue(not_null<PeerData*> peer);\n[[nodiscard]] rpl::producer<EmojiStatusId> EmojiStatusIdValue(\n\tnot_null<PeerData*> peer);\n\n[[nodiscard]] rpl::producer<QString> BirthdayLabelText(\n\trpl::producer<Data::Birthday> birthday);\n[[nodiscard]] rpl::producer<QString> BirthdayValueText(\n\trpl::producer<Data::Birthday> birthday,\n\tbool fullMonth = false);\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/profile/info_profile_widget.h\"\n\n#include \"dialogs/ui/dialogs_stories_content.h\"\n#include \"history/history.h\"\n#include \"info/profile/info_profile_inner_widget.h\"\n#include \"info/profile/info_profile_members.h\"\n#include \"info/settings/info_settings_widget.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/ui_utility.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"info/info_controller.h\"\n#include \"styles/style_info.h\"\n\n#include <QtWidgets/QApplication>\n#include <QtWidgets/QScrollBar>\n\nnamespace Info::Settings {\nstruct SectionCustomTopBarData;\n} // namespace Info::Settings\n\nnamespace Info::Profile {\n\nusing Info::Settings::SectionCustomTopBarData;\n\nMemento::Memento(not_null<Controller*> controller)\n: Memento(\n\tcontroller->peer(),\n\tcontroller->topic(),\n\tcontroller->sublist(),\n\tcontroller->migratedPeerId(),\n\t{ v::null }) {\n}\n\nMemento::Memento(\n\tnot_null<PeerData*> peer,\n\tPeerId migratedPeerId,\n\tOrigin origin)\n: Memento(peer, nullptr, nullptr, migratedPeerId, origin) {\n}\n\nMemento::Memento(\n\tnot_null<PeerData*> peer,\n\tData::ForumTopic *topic,\n\tData::SavedSublist *sublist,\n\tPeerId migratedPeerId,\n\tOrigin origin)\n: ContentMemento(peer, topic, sublist, migratedPeerId)\n, _origin(origin) {\n}\n\nMemento::Memento(not_null<Data::ForumTopic*> topic)\n: ContentMemento(topic->peer(), topic, nullptr, 0) {\n}\n\nMemento::Memento(not_null<Data::SavedSublist*> sublist)\n: ContentMemento(sublist->owningHistory()->peer, nullptr, sublist, 0) {\n}\n\nSection Memento::section() const {\n\treturn Section(Section::Type::Profile);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(parent, controller, _origin);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nvoid Memento::setMembersState(std::unique_ptr<MembersState> state) {\n\t_membersState = std::move(state);\n}\n\nstd::unique_ptr<MembersState> Memento::membersState() {\n\treturn std::move(_membersState);\n}\n\nMemento::~Memento() = default;\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller,\n\tOrigin origin)\n: ContentWidget(parent, controller)\n, _inner(\n\tsetupFlexibleInnerWidget(\n\t\tobject_ptr<InnerWidget>(this, controller, origin),\n\t\t_flexibleScroll))\n, _pinnedToTop(_inner->createPinnedToTop(this))\n, _pinnedToBottom(_inner->createPinnedToBottom(this)) {\n\tcontroller->setSearchEnabledByContent(false);\n\n\t_inner->move(0, 0);\n\t_inner->scrollToRequests(\n\t) | rpl::on_next([this](Ui::ScrollToRequest request) {\n\t\tif (request.ymin < 0) {\n\t\t\tscrollTopRestore(\n\t\t\t\tqMin(scrollTopSave(), request.ymax));\n\t\t} else {\n\t\t\tscrollTo(request);\n\t\t}\n\t}, lifetime());\n\n\t_inner->backRequest() | rpl::on_next([=] {\n\t\tcheckBeforeClose([=] { controller->showBackFromStack(); });\n\t}, _inner->lifetime());\n\n\tif (_pinnedToTop) {\n\t\t_inner->widthValue(\n\t\t) | rpl::on_next([=](int w) {\n\t\t\t_pinnedToTop->resizeToWidth(w);\n\t\t\tsetScrollTopSkip(_pinnedToTop->height());\n\t\t}, _pinnedToTop->lifetime());\n\n\t\t_pinnedToTop->heightValue(\n\t\t) | rpl::on_next([=](int h) {\n\t\t\tsetScrollTopSkip(h);\n\t\t}, _pinnedToTop->lifetime());\n\t}\n\n\tif (_pinnedToBottom) {\n\t\tconst auto processHeight = [=] {\n\t\t\tsetScrollBottomSkip(_pinnedToBottom->height());\n\t\t\t_pinnedToBottom->moveToLeft(\n\t\t\t\t_pinnedToBottom->x(),\n\t\t\t\theight() - _pinnedToBottom->height());\n\t\t};\n\n\t\t_inner->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\t_pinnedToBottom->resizeToWidth(s.width());\n\t\t}, _pinnedToBottom->lifetime());\n\n\t\trpl::combine(\n\t\t\t_pinnedToBottom->heightValue(),\n\t\t\theightValue()\n\t\t) | rpl::on_next(processHeight, _pinnedToBottom->lifetime());\n\t}\n\n\tif (_pinnedToTop\n\t\t&& _pinnedToTop->minimumHeight()\n\t\t&& _inner->hasFlexibleTopBar()) {\n\t\t_flexibleScrollHelper = std::make_unique<FlexibleScrollHelper>(\n\t\t\tscroll(),\n\t\t\t_inner,\n\t\t\t_pinnedToTop.get(),\n\t\t\t[=](QMargins margins) {\n\t\t\t\tContentWidget::setPaintPadding(std::move(margins));\n\t\t\t},\n\t\t\t[=](rpl::producer<not_null<QEvent*>> &&events) {\n\t\t\t\tContentWidget::setViewport(std::move(events));\n\t\t\t},\n\t\t\t_flexibleScroll);\n\t}\n}\n\nvoid Widget::setInnerFocus() {\n\t_inner->setFocus();\n}\n\nvoid Widget::enableBackButton() {\n\t_inner->enableBackButton();\n}\n\nvoid Widget::showFinished() {\n\t_inner->showFinished();\n}\n\nrpl::producer<QString> Widget::title() {\n\tif (const auto topic = controller()->key().topic()) {\n\t\treturn topic->peer()->isBot()\n\t\t\t? tr::lng_info_thread_title()\n\t\t\t: tr::lng_info_topic_title();\n\t} else if (controller()->key().sublist()\n\t\t&& controller()->key().sublist()->parentChat()) {\n\t\treturn tr::lng_profile_direct_messages();\n\t}\n\tconst auto peer = controller()->key().peer();\n\tif (const auto user = peer->asUser()) {\n\t\treturn (user->isBot() && !user->isSupport())\n\t\t\t? tr::lng_info_bot_title()\n\t\t\t: tr::lng_info_user_title();\n\t} else if (const auto channel = peer->asChannel()) {\n\t\treturn channel->isMonoforum()\n\t\t\t? tr::lng_profile_direct_messages()\n\t\t\t: channel->isMegagroup()\n\t\t\t? tr::lng_info_group_title()\n\t\t\t: tr::lng_info_channel_title();\n\t} else if (peer->isChat()) {\n\t\treturn tr::lng_info_group_title();\n\t}\n\tUnexpected(\"Bad peer type in Info::TitleValue()\");\n}\n\nrpl::producer<Dialogs::Stories::Content> Widget::titleStories() {\n\tconst auto peer = controller()->key().peer();\n\tif (peer && !peer->isChat()) {\n\t\treturn Dialogs::Stories::LastForPeer(peer);\n\t}\n\treturn nullptr;\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\tif (!controller()->validateMementoPeer(memento)) {\n\t\treturn false;\n\t}\n\tif (auto profileMemento = dynamic_cast<Memento*>(memento.get())) {\n\t\trestoreState(profileMemento);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(controller());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/profile/info_profile_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace Data {\nclass ForumTopic;\n} // namespace Data\n\nnamespace Info::Profile {\n\nclass InnerWidget;\nstruct MembersState;\n\nstruct GroupReactionOrigin {\n\tnot_null<PeerData*> group;\n\tMsgId messageId = 0;\n};\n\nstruct Origin {\n\tstd::variant<v::null_t, GroupReactionOrigin> data;\n};\n\nclass Memento final : public ContentMemento {\npublic:\n\texplicit Memento(not_null<Controller*> controller);\n\tMemento(\n\t\tnot_null<PeerData*> peer,\n\t\tPeerId migratedPeerId,\n\t\tOrigin origin = { v::null });\n\texplicit Memento(not_null<Data::ForumTopic*> topic);\n\texplicit Memento(not_null<Data::SavedSublist*> sublist);\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\t[[nodiscard]] Origin origin() const {\n\t\treturn _origin;\n\t}\n\n\tvoid setMembersState(std::unique_ptr<MembersState> state);\n\tstd::unique_ptr<MembersState> membersState();\n\n\t~Memento();\n\nprivate:\n\tMemento(\n\t\tnot_null<PeerData*> peer,\n\t\tData::ForumTopic *topic,\n\t\tData::SavedSublist *sublist,\n\t\tPeerId migratedPeerId,\n\t\tOrigin origin);\n\n\tstd::unique_ptr<MembersState> _membersState;\n\tOrigin _origin;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(QWidget *parent, not_null<Controller*> controller, Origin origin);\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\n\tvoid setInnerFocus() override;\n\tvoid enableBackButton() override;\n\tvoid showFinished() override;\n\n\trpl::producer<QString> title() override;\n\trpl::producer<Dialogs::Stories::Content> titleStories() override;\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tFlexibleScrollData _flexibleScroll;\n\tInnerWidget *_inner = nullptr;\n\tbase::weak_qptr<Ui::RpWidget> _pinnedToTop;\n\tbase::weak_qptr<Ui::RpWidget> _pinnedToBottom;\n\tstd::unique_ptr<FlexibleScrollHelper> _flexibleScrollHelper;\n\n};\n\n} // namespace Info::Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/info/reactions_list/info_reactions_list_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/reactions_list/info_reactions_list_widget.h\"\n\n#include \"api/api_who_reacted.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"data/data_channel.h\"\n#include \"history/view/reactions/history_view_reactions_list.h\"\n#include \"history/view/reactions/history_view_reactions_tabs.h\"\n#include \"info/info_controller.h\"\n#include \"ui/controls/who_reacted_context_action.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/ui_utility.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info::ReactionsList {\nnamespace {\n\n} // namespace\n\nclass InnerWidget final\n\t: public Ui::RpWidget\n\t, private PeerListContentDelegate {\npublic:\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tstd::shared_ptr<Api::WhoReadList> whoReadIds,\n\t\tFullMsgId contextId,\n\t\tData::ReactionId selected);\n\n\t[[nodiscard]] std::shared_ptr<Api::WhoReadList> whoReadIds() const;\n\t[[nodiscard]] FullMsgId contextId() const;\n\t[[nodiscard]] Data::ReactionId selected() const;\n\n\trpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\n\tint desiredHeight() const;\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\nprivate:\n\tusing ListWidget = PeerListContent;\n\n\t// PeerListContentDelegate interface\n\tvoid peerListSetTitle(rpl::producer<QString> title) override;\n\tvoid peerListSetAdditionalTitle(rpl::producer<QString> title) override;\n\tbool peerListIsRowChecked(not_null<PeerListRow*> row) override;\n\tint peerListSelectedRowsCount() override;\n\tvoid peerListScrollToTop() override;\n\tvoid peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) override;\n\tvoid peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) override;\n\tvoid peerListFinishSelectedRowsBunch() override;\n\tvoid peerListSetDescription(object_ptr<Ui::FlatLabel> description) override;\n\tstd::shared_ptr<Main::SessionShow> peerListUiShow() override;\n\n\tobject_ptr<ListWidget> setupList(\n\t\tRpWidget *parent,\n\t\tnot_null<PeerListController*> controller);\n\n\tconst std::shared_ptr<Main::SessionShow> _show;\n\tnot_null<Controller*> _controller;\n\tData::ReactionId _selected;\n\tnot_null<HistoryView::Reactions::Tabs*> _tabs;\n\trpl::variable<int> _tabsHeight;\n\tHistoryView::Reactions::PreparedFullList _full;\n\tobject_ptr<ListWidget> _list;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n};\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller,\n\tstd::shared_ptr<Api::WhoReadList> whoReadIds,\n\tFullMsgId contextId,\n\tData::ReactionId selected)\n: RpWidget(parent)\n, _show(controller->uiShow())\n, _controller(controller)\n, _selected(selected)\n, _tabs(HistoryView::Reactions::CreateReactionsTabs(\n\tthis,\n\tcontroller,\n\tcontroller->reactionsContextId(),\n\t_selected,\n\tcontroller->reactionsWhoReadIds()))\n, _tabsHeight(_tabs->heightValue())\n, _full(HistoryView::Reactions::FullListController(\n\tcontroller,\n\tcontroller->reactionsContextId(),\n\t_selected,\n\tcontroller->reactionsWhoReadIds()))\n, _list(setupList(this, _full.controller.get())) {\n\tsetContent(_list.data());\n\t_full.controller->setDelegate(static_cast<PeerListDelegate*>(this));\n\t_tabs->changes(\n\t) | rpl::on_next([=](Data::ReactionId reaction) {\n\t\t_selected = reaction;\n\t\t_full.switchTab(reaction);\n\t}, _list->lifetime());\n}\n\nstd::shared_ptr<Api::WhoReadList> InnerWidget::whoReadIds() const {\n\treturn _controller->reactionsWhoReadIds();\n}\n\nFullMsgId InnerWidget::contextId() const {\n\treturn _controller->reactionsContextId();\n}\n\nData::ReactionId InnerWidget::selected() const {\n\treturn _selected;\n}\n\nvoid InnerWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tsetChildVisibleTopBottom(_list, visibleTop, visibleBottom);\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\tmemento->setListState(_full.controller->saveState());\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\t_full.controller->restoreState(memento->listState());\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nint InnerWidget::desiredHeight() const {\n\tauto desired = 0;\n\tdesired += _list->fullRowsCount() * st::infoMembersList.item.height;\n\treturn qMax(height(), desired);\n}\n\nobject_ptr<InnerWidget::ListWidget> InnerWidget::setupList(\n\t\tRpWidget *parent,\n\t\tnot_null<PeerListController*> controller) {\n\tauto result = object_ptr<ListWidget>(parent, controller);\n\tconst auto raw = result.data();\n\n\traw->scrollToRequests(\n\t) | rpl::on_next([this](Ui::ScrollToRequest request) {\n\t\tconst auto skip = _tabsHeight.current()\n\t\t\t+ st::infoCommonGroupsMargin.top();\n\t\tauto addmin = (request.ymin < 0) ? 0 : skip;\n\t\tauto addmax = (request.ymax < 0) ? 0 : skip;\n\t\t_scrollToRequests.fire({\n\t\t\trequest.ymin + addmin,\n\t\t\trequest.ymax + addmax });\n\t}, raw->lifetime());\n\n\t_tabs->move(0, 0);\n\t_tabsHeight.value() | rpl::on_next([=](int tabs) {\n\t\traw->moveToLeft(0, tabs + st::infoCommonGroupsMargin.top());\n\t}, raw->lifetime());\n\n\tparent->widthValue(\n\t) | rpl::on_next([=](int newWidth) {\n\t\t_tabs->resizeToWidth(newWidth);\n\t\traw->resizeToWidth(newWidth);\n\t}, raw->lifetime());\n\n\trpl::combine(\n\t\t_tabsHeight.value(),\n\t\traw->heightValue()\n\t) | rpl::on_next([parent](int tabsHeight, int listHeight) {\n\t\tconst auto newHeight = tabsHeight\n\t\t\t+ st::infoCommonGroupsMargin.top()\n\t\t\t+ listHeight\n\t\t\t+ st::infoCommonGroupsMargin.bottom();\n\t\tparent->resize(parent->width(), newHeight);\n\t}, result->lifetime());\n\n\treturn result;\n}\n\nvoid InnerWidget::peerListSetTitle(rpl::producer<QString> title) {\n}\n\nvoid InnerWidget::peerListSetAdditionalTitle(rpl::producer<QString> title) {\n}\n\nbool InnerWidget::peerListIsRowChecked(not_null<PeerListRow*> row) {\n\treturn false;\n}\n\nint InnerWidget::peerListSelectedRowsCount() {\n\treturn 0;\n}\n\nvoid InnerWidget::peerListScrollToTop() {\n\t_scrollToRequests.fire({ -1, -1 });\n}\n\nvoid InnerWidget::peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) {\n\tUnexpected(\"Item selection in Info::Profile::Members.\");\n}\n\nvoid InnerWidget::peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) {\n\tUnexpected(\"Item selection in Info::Profile::Members.\");\n}\n\nvoid InnerWidget::peerListFinishSelectedRowsBunch() {\n}\n\nvoid InnerWidget::peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) {\n\tdescription.destroy();\n}\n\nstd::shared_ptr<Main::SessionShow> InnerWidget::peerListUiShow() {\n\treturn _show;\n}\n\nMemento::Memento(\n\tstd::shared_ptr<Api::WhoReadList> whoReadIds,\n\tFullMsgId contextId,\n\tData::ReactionId selected)\n: ContentMemento(std::move(whoReadIds), contextId, selected) {\n}\n\nSection Memento::section() const {\n\treturn Section(Section::Type::ReactionsList);\n}\n\nstd::shared_ptr<Api::WhoReadList> Memento::whoReadIds() const {\n\treturn reactionsWhoReadIds();\n}\n\nFullMsgId Memento::contextId() const {\n\treturn reactionsContextId();\n}\n\nData::ReactionId Memento::selected() const {\n\treturn reactionsSelected();\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(\n\t\tparent,\n\t\tcontroller,\n\t\twhoReadIds(),\n\t\tcontextId(),\n\t\tselected());\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nvoid Memento::setListState(std::unique_ptr<PeerListState> state) {\n\t_listState = std::move(state);\n}\n\nstd::unique_ptr<PeerListState> Memento::listState() {\n\treturn std::move(_listState);\n}\n\nMemento::~Memento() = default;\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller,\n\tstd::shared_ptr<Api::WhoReadList> whoReadIds,\n\tFullMsgId contextId,\n\tData::ReactionId selected)\n: ContentWidget(parent, controller) {\n\t_inner = setInnerWidget(object_ptr<InnerWidget>(\n\t\tthis,\n\t\tcontroller,\n\t\tstd::move(whoReadIds),\n\t\tcontextId,\n\t\tselected));\n}\n\nrpl::producer<QString> Widget::title() {\n\tconst auto ids = whoReadIds();\n\tconst auto count = ids ? int(ids->list.size()) : 0;\n\treturn !count\n\t\t? tr::lng_manage_peer_reactions()\n\t\t: (ids->type == Ui::WhoReadType::Seen)\n\t\t? tr::lng_context_seen_text(lt_count, rpl::single(1. * count))\n\t\t: (ids->type == Ui::WhoReadType::Listened)\n\t\t? tr::lng_context_seen_listened(lt_count, rpl::single(1. * count))\n\t\t: (ids->type == Ui::WhoReadType::Watched)\n\t\t? tr::lng_context_seen_watched(lt_count, rpl::single(1. * count))\n\t\t: tr::lng_manage_peer_reactions();\n}\n\nstd::shared_ptr<Api::WhoReadList> Widget::whoReadIds() const {\n\treturn _inner->whoReadIds();\n}\n\nFullMsgId Widget::contextId() const {\n\treturn _inner->contextId();\n}\n\nData::ReactionId Widget::selected() const {\n\treturn _inner->selected();\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\treturn false;\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(\n\t\twhoReadIds(),\n\t\tcontextId(),\n\t\tselected());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\n} // namespace Info::ReactionsList\n"
  },
  {
    "path": "Telegram/SourceFiles/info/reactions_list/info_reactions_list_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n\nclass ChannelData;\nstruct PeerListState;\n\nnamespace Api {\nstruct WhoReadList;\n} // namespace Api\n\nnamespace Info::ReactionsList {\n\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\tMemento(\n\t\tstd::shared_ptr<Api::WhoReadList> whoReadIds,\n\t\tFullMsgId contextId,\n\t\tData::ReactionId selected);\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\t[[nodiscard]] std::shared_ptr<Api::WhoReadList> whoReadIds() const;\n\t[[nodiscard]] FullMsgId contextId() const;\n\t[[nodiscard]] Data::ReactionId selected() const;\n\n\tvoid setListState(std::unique_ptr<PeerListState> state);\n\tstd::unique_ptr<PeerListState> listState();\n\n\t~Memento();\n\nprivate:\n\tstd::unique_ptr<PeerListState> _listState;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tstd::shared_ptr<Api::WhoReadList> whoReadIds,\n\t\tFullMsgId contextId,\n\t\tData::ReactionId selected);\n\n\t[[nodiscard]] std::shared_ptr<Api::WhoReadList> whoReadIds() const;\n\t[[nodiscard]] FullMsgId contextId() const;\n\t[[nodiscard]] Data::ReactionId selected() const;\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\n\trpl::producer<QString> title() override;\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tInnerWidget *_inner = nullptr;\n};\n\n} // namespace Info::ReactionsList\n"
  },
  {
    "path": "Telegram/SourceFiles/info/requests_list/info_requests_list_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/requests_list/info_requests_list_widget.h\"\n\n#include \"boxes/peers/edit_peer_requests_box.h\"\n#include \"info/info_controller.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/search_field_controller.h\"\n#include \"ui/ui_utility.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info::RequestsList {\nnamespace {\n\n} // namespace\n\nclass InnerWidget final\n\t: public Ui::RpWidget\n\t, private PeerListContentDelegate {\npublic:\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<PeerData*> peer);\n\n\t[[nodiscard]] not_null<PeerData*> peer() const {\n\t\treturn _peer;\n\t}\n\n\trpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\n\tint desiredHeight() const;\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\nprivate:\n\tusing ListWidget = PeerListContent;\n\n\t// PeerListContentDelegate interface\n\tvoid peerListSetTitle(rpl::producer<QString> title) override;\n\tvoid peerListSetAdditionalTitle(rpl::producer<QString> title) override;\n\tbool peerListIsRowChecked(not_null<PeerListRow*> row) override;\n\tint peerListSelectedRowsCount() override;\n\tvoid peerListScrollToTop() override;\n\tvoid peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) override;\n\tvoid peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) override;\n\tvoid peerListFinishSelectedRowsBunch() override;\n\tvoid peerListSetDescription(object_ptr<Ui::FlatLabel> description) override;\n\tstd::shared_ptr<Main::SessionShow> peerListUiShow() override;\n\n\tobject_ptr<ListWidget> setupList(\n\t\tRpWidget *parent,\n\t\tnot_null<RequestsBoxController*> controller);\n\n\tconst std::shared_ptr<Main::SessionShow> _show;\n\tnot_null<Controller*> _controller;\n\tconst not_null<PeerData*> _peer;\n\tstd::unique_ptr<RequestsBoxController> _listController;\n\tobject_ptr<ListWidget> _list;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n};\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller,\n\tnot_null<PeerData*> peer)\n: RpWidget(parent)\n, _show(controller->uiShow())\n, _controller(controller)\n, _peer(peer)\n, _listController(std::make_unique<RequestsBoxController>(\n\tcontroller,\n\t_peer))\n, _list(setupList(this, _listController.get())) {\n\tsetContent(_list.data());\n\t_listController->setDelegate(static_cast<PeerListDelegate*>(this));\n\n\tcontroller->searchFieldController()->queryValue(\n\t) | rpl::on_next([this](QString &&query) {\n\t\tpeerListScrollToTop();\n\t\tcontent()->searchQueryChanged(std::move(query));\n\t}, lifetime());\n}\n\nvoid InnerWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tsetChildVisibleTopBottom(_list, visibleTop, visibleBottom);\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\tmemento->setListState(_listController->saveState());\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\t_listController->restoreState(memento->listState());\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nint InnerWidget::desiredHeight() const {\n\tauto desired = 0;\n\tdesired += _list->fullRowsCount() * st::infoMembersList.item.height;\n\treturn qMax(height(), desired);\n}\n\nobject_ptr<InnerWidget::ListWidget> InnerWidget::setupList(\n\t\tRpWidget *parent,\n\t\tnot_null<RequestsBoxController*> controller) {\n\tauto result = object_ptr<ListWidget>(parent, controller);\n\tresult->scrollToRequests(\n\t) | rpl::on_next([this](Ui::ScrollToRequest request) {\n\t\tauto addmin = (request.ymin < 0) ? 0 : st::infoCommonGroupsMargin.top();\n\t\tauto addmax = (request.ymax < 0) ? 0 : st::infoCommonGroupsMargin.top();\n\t\t_scrollToRequests.fire({\n\t\t\trequest.ymin + addmin,\n\t\t\trequest.ymax + addmax });\n\t}, result->lifetime());\n\tresult->moveToLeft(0, st::infoCommonGroupsMargin.top());\n\n\tparent->widthValue(\n\t) | rpl::on_next([list = result.data()](int newWidth) {\n\t\tlist->resizeToWidth(newWidth);\n\t}, result->lifetime());\n\n\tresult->heightValue(\n\t) | rpl::on_next([parent](int listHeight) {\n\t\tauto newHeight = st::infoCommonGroupsMargin.top()\n\t\t\t+ listHeight\n\t\t\t+ st::infoCommonGroupsMargin.bottom();\n\t\tparent->resize(parent->width(), newHeight);\n\t}, result->lifetime());\n\n\treturn result;\n}\n\nvoid InnerWidget::peerListSetTitle(rpl::producer<QString> title) {\n}\n\nvoid InnerWidget::peerListSetAdditionalTitle(rpl::producer<QString> title) {\n}\n\nbool InnerWidget::peerListIsRowChecked(not_null<PeerListRow*> row) {\n\treturn false;\n}\n\nint InnerWidget::peerListSelectedRowsCount() {\n\treturn 0;\n}\n\nvoid InnerWidget::peerListScrollToTop() {\n\t_scrollToRequests.fire({ -1, -1 });\n}\n\nvoid InnerWidget::peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) {\n\tUnexpected(\"Item selection in Info::Profile::Members.\");\n}\n\nvoid InnerWidget::peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) {\n\tUnexpected(\"Item selection in Info::Profile::Members.\");\n}\n\nvoid InnerWidget::peerListFinishSelectedRowsBunch() {\n}\n\nvoid InnerWidget::peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) {\n\tdescription.destroy();\n}\n\nstd::shared_ptr<Main::SessionShow> InnerWidget::peerListUiShow() {\n\treturn _show;\n}\n\nMemento::Memento(not_null<PeerData*> peer)\n: ContentMemento(peer, nullptr, nullptr, PeerId()) {\n}\n\nSection Memento::section() const {\n\treturn Section(Section::Type::RequestsList);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(parent, controller, peer());\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nvoid Memento::setListState(std::unique_ptr<PeerListState> state) {\n\t_listState = std::move(state);\n}\n\nstd::unique_ptr<PeerListState> Memento::listState() {\n\treturn std::move(_listState);\n}\n\nMemento::~Memento() = default;\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller,\n\tnot_null<PeerData*> peer)\n: ContentWidget(parent, controller) {\n\tcontroller->setSearchEnabledByContent(true);\n\t_inner = setInnerWidget(object_ptr<InnerWidget>(this, controller, peer));\n}\n\nrpl::producer<QString> Widget::title() {\n\treturn tr::lng_manage_peer_requests();\n}\n\nnot_null<PeerData*> Widget::peer() const {\n\treturn _inner->peer();\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\tif (!controller()->validateMementoPeer(memento)) {\n\t\treturn false;\n\t}\n\tif (auto requestsMemento = dynamic_cast<Memento*>(memento.get())) {\n\t\tif (requestsMemento->peer() == peer()) {\n\t\t\trestoreState(requestsMemento);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(peer());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\n} // namespace Info::RequestsList\n"
  },
  {
    "path": "Telegram/SourceFiles/info/requests_list/info_requests_list_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n\nclass PeerData;\nstruct PeerListState;\n\nnamespace Info::RequestsList {\n\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\texplicit Memento(not_null<PeerData*> peer);\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\tvoid setListState(std::unique_ptr<PeerListState> state);\n\tstd::unique_ptr<PeerListState> listState();\n\n\t~Memento();\n\nprivate:\n\tstd::unique_ptr<PeerListState> _listState;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<PeerData*> peer);\n\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\n\trpl::producer<QString> title() override;\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tInnerWidget *_inner = nullptr;\n};\n\n} // namespace Info::RequestsList\n"
  },
  {
    "path": "Telegram/SourceFiles/info/saved/info_saved_music_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/saved/info_saved_music_common.h\"\n\n#include \"data/data_peer.h\"\n#include \"data/data_saved_music.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"history/history_item.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"info/profile/info_profile_music_button.h\"\n#include \"info/saved/info_saved_music_widget.h\"\n#include \"ui/text/format_song_document_name.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/vertical_list.h\"\n\nnamespace Info::Saved {\n\nnamespace {\n\n[[nodiscard]] Profile::MusicButtonData DocumentMusicButtonData(\n\t\tnot_null<DocumentData*> document) {\n\treturn { Ui::Text::FormatSongNameFor(document) };\n}\n\n} // namespace\n\nvoid SetupSavedMusic(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Info::Controller*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<std::optional<QColor>> topBarColor) {\n\tauto musicValue = Data::SavedMusic::Supported(peer->id)\n\t\t? Data::SavedMusicList(\n\t\t\tpeer,\n\t\t\tnullptr,\n\t\t\t1\n\t\t) | rpl::map([=](const Data::SavedMusicSlice &data) {\n\t\t\treturn data.size() ? data[0].get() : nullptr;\n\t\t}) | rpl::type_erased\n\t\t: rpl::single<HistoryItem*>((HistoryItem*)(nullptr));\n\n\tconst auto divider = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\n\trpl::combine(\n\t\tstd::move(musicValue),\n\t\tstd::move(topBarColor)\n\t) | rpl::on_next([=](\n\t\t\tHistoryItem *item,\n\t\t\tstd::optional<QColor> color) {\n\t\twhile (divider->entity()->count()) {\n\t\t\tdelete divider->entity()->widgetAt(0);\n\t\t}\n\t\tif (item) {\n\t\t\tif (const auto document = item->media()\n\t\t\t\t\t? item->media()->document()\n\t\t\t\t\t: nullptr) {\n\t\t\t\tconst auto music = divider->entity()->add(\n\t\t\t\t\tobject_ptr<Profile::MusicButton>(\n\t\t\t\t\t\tdivider->entity(),\n\t\t\t\t\t\tDocumentMusicButtonData(document),\n\t\t\t\t\t\t[window = controller, peer] {\n\t\t\t\t\t\t\twindow->showSection(Info::Saved::MakeMusic(peer));\n\t\t\t\t\t\t}));\n\t\t\t\tmusic->setOverrideBg(color);\n\t\t\t}\n\t\t\tdivider->toggle(true, anim::type::normal);\n\t\t}\n\t}, container->lifetime());\n\tdivider->finishAnimating();\n}\n\n} // namespace Info::Saved"
  },
  {
    "path": "Telegram/SourceFiles/info/saved/info_saved_music_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Info {\nclass Controller;\n} // namespace Info\n\nnamespace Info::Saved {\n\nstruct MusicTag {\n\texplicit MusicTag(not_null<PeerData*> peer)\n\t: peer(peer) {\n\t}\n\n\tnot_null<PeerData*> peer;\n};\n\nvoid SetupSavedMusic(\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<Info::Controller*> controller,\n\tnot_null<PeerData*> peer,\n\trpl::producer<std::optional<QColor>> topBarColor);\n\n} // namespace Info::Saved\n"
  },
  {
    "path": "Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/saved/info_saved_music_provider.h\"\n\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_saved_music.h\"\n#include \"data/data_session.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history.h\"\n#include \"info/media/info_media_widget.h\"\n#include \"info/media/info_media_list_section.h\"\n#include \"info/info_controller.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"layout/layout_selection.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_overview.h\"\n\nnamespace Info::Saved {\nnamespace {\n\nusing namespace Media;\n\nconstexpr auto kPreloadedScreensCount = 4;\nconstexpr auto kPreloadedScreensCountFull\n\t= kPreloadedScreensCount + 1 + kPreloadedScreensCount;\n\n} // namespace\n\nMusicProvider::MusicProvider(not_null<AbstractController*> controller)\n: _controller(controller)\n, _peer(controller->key().musicPeer())\n, _history(_peer->owner().history(_peer)) {\n\t_controller->session().data().itemRemoved(\n\t) | rpl::on_next([this](auto item) {\n\t\titemRemoved(item);\n\t}, _lifetime);\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tfor (auto &layout : _layouts) {\n\t\t\tlayout.second.item->invalidateCache();\n\t\t}\n\t}, _lifetime);\n}\n\nMusicProvider::~MusicProvider() {\n\tclear();\n}\n\nType MusicProvider::type() {\n\treturn Type::MusicFile;\n}\n\nbool MusicProvider::hasSelectRestriction() {\n\t//if (_peer->session().frozen()) {\n\t//\treturn true;\n\t//}\n\treturn true;\n}\n\nrpl::producer<bool> MusicProvider::hasSelectRestrictionChanges() {\n\treturn rpl::never<bool>();\n}\n\nbool MusicProvider::sectionHasFloatingHeader() {\n\treturn false;\n}\n\nQString MusicProvider::sectionTitle(not_null<const BaseLayout*> item) {\n\treturn QString();\n}\n\nbool MusicProvider::sectionItemBelongsHere(\n\t\tnot_null<const BaseLayout*> item,\n\t\tnot_null<const BaseLayout*> previous) {\n\treturn true;\n}\n\nbool MusicProvider::isPossiblyMyItem(not_null<const HistoryItem*> item) {\n\treturn true;\n}\n\nstd::optional<int> MusicProvider::fullCount() {\n\treturn _slice.fullCount();\n}\n\nvoid MusicProvider::clear() {\n\t_layouts.clear();\n\t_aroundId = nullptr;\n\t_idsLimit = kMinimalIdsLimit;\n\t_slice = Data::SavedMusicSlice();\n}\n\nvoid MusicProvider::restart() {\n\tclear();\n\trefreshViewer();\n}\n\nvoid MusicProvider::checkPreload(\n\t\tQSize viewport,\n\t\tnot_null<BaseLayout*> topLayout,\n\t\tnot_null<BaseLayout*> bottomLayout,\n\t\tbool preloadTop,\n\t\tbool preloadBottom) {\n\tconst auto visibleWidth = viewport.width();\n\tconst auto visibleHeight = viewport.height();\n\tconst auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;\n\tconst auto minItemHeight = MinItemHeight(\n\t\tType::MusicFile,\n\t\tvisibleWidth);\n\tconst auto preloadedCount = preloadedHeight / minItemHeight;\n\tconst auto preloadIdsLimitMin = (preloadedCount / 2) + 1;\n\tconst auto preloadIdsLimit = preloadIdsLimitMin\n\t\t+ (visibleHeight / minItemHeight);\n\tconst auto after = _slice.skippedAfter();\n\tconst auto topLoaded = after && (*after == 0);\n\tconst auto before = _slice.skippedBefore();\n\tconst auto bottomLoaded = before && (*before == 0);\n\n\tconst auto minScreenDelta = kPreloadedScreensCount\n\t\t- kPreloadIfLessThanScreens;\n\tconst auto minIdDelta = (minScreenDelta * visibleHeight)\n\t\t/ minItemHeight;\n\tconst auto preloadAroundItem = [&](not_null<BaseLayout*> layout) {\n\t\tauto preloadRequired = false;\n\t\tconst auto item = layout->getItem();\n\t\tif (!preloadRequired) {\n\t\t\tpreloadRequired = (_idsLimit < preloadIdsLimitMin);\n\t\t}\n\t\tif (!preloadRequired) {\n\t\t\tauto delta = _slice.distance(_aroundId, item);\n\t\t\tAssert(delta != std::nullopt);\n\t\t\tpreloadRequired = (qAbs(*delta) >= minIdDelta);\n\t\t}\n\t\tif (preloadRequired) {\n\t\t\t_idsLimit = preloadIdsLimit;\n\t\t\t_aroundId = item;\n\t\t\trefreshViewer();\n\t\t}\n\t};\n\n\tif (preloadTop && !topLoaded) {\n\t\tpreloadAroundItem(topLayout);\n\t} else if (preloadBottom && !bottomLoaded) {\n\t\tpreloadAroundItem(bottomLayout);\n\t}\n}\n\nvoid MusicProvider::setSearchQuery(QString query) {\n}\n\nvoid MusicProvider::jumpToMessage(MsgId messageId, Fn<void(FullMsgId)>) {\n}\n\nvoid MusicProvider::refreshViewer() {\n\t_viewerLifetime.destroy();\n\tconst auto aroundId = _aroundId;\n \tauto ids = Data::SavedMusicList(_peer, aroundId, _idsLimit);\n\tstd::move(\n\t\tids\n\t) | rpl::on_next([=](Data::SavedMusicSlice &&slice) {\n\t\tif (!slice.fullCount()) {\n\t\t\t// Don't display anything while full count is unknown.\n\t\t\treturn;\n\t\t}\n\t\t_slice = std::move(slice);\n\n\t\tauto nearestId = (HistoryItem*)nullptr;\n\t\tfor (auto i = 0; i != _slice.size(); ++i) {\n\t\t\tif (_slice[i] == aroundId) {\n\t\t\t\tnearestId = aroundId;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (!nearestId && _slice.size() > 0) {\n\t\t\t_aroundId = _slice[_slice.size() / 2];\n\t\t}\n\t\t_refreshed.fire({});\n\t}, _viewerLifetime);\n}\n\nrpl::producer<> MusicProvider::refreshed() {\n\treturn _refreshed.events();\n}\n\nstd::vector<ListSection> MusicProvider::fillSections(\n\t\tnot_null<Overview::Layout::Delegate*> delegate) {\n\tmarkLayoutsStale();\n\tconst auto guard = gsl::finally([&] { clearStaleLayouts(); });\n\n\tauto result = std::vector<ListSection>();\n\tauto section = ListSection(Type::MusicFile, sectionDelegate());\n\tauto count = _slice.size();\n\tfor (auto i = 0; i != count; ++i) {\n\t\tconst auto item = _slice[i];\n\t\tif (const auto layout = getLayout(item, delegate)) {\n\t\t\tif (!section.addItem(layout)) {\n\t\t\t\tsection.finishSection();\n\t\t\t\tresult.push_back(std::move(section));\n\t\t\t\tsection = ListSection(Type::MusicFile, sectionDelegate());\n\t\t\t\tsection.addItem(layout);\n\t\t\t}\n\t\t}\n\t}\n\tif (!section.empty()) {\n\t\tsection.finishSection();\n\t\tresult.push_back(std::move(section));\n\t}\n\treturn result;\n}\n\nvoid MusicProvider::itemRemoved(not_null<const HistoryItem*> item) {\n\tif (const auto i = _layouts.find(item); i != end(_layouts)) {\n\t\t_layoutRemoved.fire(i->second.item.get());\n\t\t_layouts.erase(i);\n\t}\n}\n\nvoid MusicProvider::markLayoutsStale() {\n\tfor (auto &layout : _layouts) {\n\t\tlayout.second.stale = true;\n\t}\n}\n\nvoid MusicProvider::clearStaleLayouts() {\n\tfor (auto i = _layouts.begin(); i != _layouts.end();) {\n\t\tif (i->second.stale) {\n\t\t\t_layoutRemoved.fire(i->second.item.get());\n\t\t\ti = _layouts.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n}\n\nrpl::producer<not_null<BaseLayout*>> MusicProvider::layoutRemoved() {\n\treturn _layoutRemoved.events();\n}\n\nBaseLayout *MusicProvider::lookupLayout(const HistoryItem *item) {\n\treturn nullptr;\n}\n\nbool MusicProvider::isMyItem(not_null<const HistoryItem*> item) {\n\treturn item->isSavedMusicItem() && (item->history()->peer == _peer);\n}\n\nbool MusicProvider::isAfter(\n\t\tnot_null<const HistoryItem*> a,\n\t\tnot_null<const HistoryItem*> b) {\n\treturn (a->id < b->id);\n}\n\nBaseLayout *MusicProvider::getLayout(\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<Overview::Layout::Delegate*> delegate) {\n\tauto it = _layouts.find(item);\n\tif (it == _layouts.end()) {\n\t\tif (auto layout = createLayout(item, delegate)) {\n\t\t\tlayout->initDimensions();\n\t\t\tit = _layouts.emplace(item, std::move(layout)).first;\n\t\t} else {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\tit->second.stale = false;\n\treturn it->second.item.get();\n}\n\nstd::unique_ptr<BaseLayout> MusicProvider::createLayout(\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<Overview::Layout::Delegate*> delegate) {\n\tusing namespace Overview::Layout;\n\tif (const auto media = item->media()) {\n\t\tif (const auto file = media->document()) {\n\t\t\treturn std::make_unique<Document>(\n\t\t\t\tdelegate,\n\t\t\t\titem,\n\t\t\t\tDocumentFields{ file },\n\t\t\t\tst::overviewFileLayout);\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nListItemSelectionData MusicProvider::computeSelectionData(\n\t\tnot_null<const HistoryItem*> item,\n\t\tTextSelection selection) {\n\tauto result = ListItemSelectionData(selection);\n\tresult.canDelete = item->history()->peer->isSelf();\n\tresult.canForward = true;// item->allowsForward();\n\treturn result;\n}\n\nvoid MusicProvider::applyDragSelection(\n\t\tListSelectedMap &selected,\n\t\tnot_null<const HistoryItem*> fromItem,\n\t\tbool skipFrom,\n\t\tnot_null<const HistoryItem*> tillItem,\n\t\tbool skipTill) {\n\tconst auto fromId = fromItem->id - (skipFrom ? 1 : 0);\n\tconst auto tillId = tillItem->id - (skipTill ? 0 : 1);\n\tfor (auto i = selected.begin(); i != selected.end();) {\n\t\tconst auto itemId = i->first->id;\n\t\tif (itemId > fromId || itemId <= tillId) {\n\t\t\ti = selected.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tfor (auto &layoutItem : _layouts) {\n\t\tconst auto item = layoutItem.first;\n\t\tif (item->id <= fromId && item->id > tillId) {\n\t\t\tChangeItemSelection(\n\t\t\t\tselected,\n\t\t\t\titem,\n\t\t\t\tcomputeSelectionData(item, FullSelection));\n\t\t}\n\t}\n}\n\nbool MusicProvider::allowSaveFileAs(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) {\n\treturn true;\n}\n\nQString MusicProvider::showInFolderPath(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) {\n\treturn document->filepath(true);\n}\n\nint64 MusicProvider::scrollTopStatePosition(not_null<HistoryItem*> item) {\n\treturn item->id.bare;\n}\n\nHistoryItem *MusicProvider::scrollTopStateItem(ListScrollTopState state) {\n\tif (state.item && _slice.indexOf(state.item)) {\n\t\treturn state.item;\n\t//} else if (const auto id = _slice.nearest(state.position)) {\n\t//\tconst auto full = FullMsgId(_peer->id, StoryIdToMsgId(*id));\n\t//\tif (const auto item = _controller->session().data().message(full)) {\n\t//\t\treturn item;\n\t//\t}\n\t}\n\n\tauto nearestId = (HistoryItem*)nullptr; //AssertIsDebug();\n\t//for (auto i = 0; i != _slice.size(); ++i) {\n\t//\tif (!nearestId\n\t//\t\t|| std::abs(*nearestId - state.position)\n\t//\t\t\t> std::abs(_slice[i] - state.position)) {\n\t//\t\tnearestId = _slice[i];\n\t//\t}\n\t//}\n\tif (nearestId) {\n\t\tconst auto full = nearestId->fullId();\n\t\tif (const auto item = _controller->session().data().message(full)) {\n\t\t\treturn item;\n\t\t}\n\t}\n\n\treturn state.item;\n}\n\nvoid MusicProvider::saveState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tListScrollTopState scrollState) {\n\tif (_aroundId != nullptr && scrollState.item) {\n\t\t//AssertIsDebug();\n\t\t//memento->setAroundId({ _peer->id, StoryIdToMsgId(_aroundId) });\n\t\t//memento->setIdsLimit(_idsLimit);\n\t\t//memento->setScrollTopItem(scrollState.item->globalId());\n\t\t//memento->setScrollTopItemPosition(scrollState.position);\n\t\t//memento->setScrollTopShift(scrollState.shift);\n\t}\n}\n\nvoid MusicProvider::restoreState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tFn<void(ListScrollTopState)> restoreScrollState) {\n\tif (const auto limit = memento->idsLimit()) {\n\t\tconst auto wasAroundId = memento->aroundId();\n\t\tif (wasAroundId.peer == _peer->id) {\n\t\t\t_idsLimit = limit;\n\t\t\t//AssertIsDebug();\n\t\t\t//_aroundId = StoryIdFromMsgId(wasAroundId.msg);\n\t\t\trestoreScrollState({\n\t\t\t\t.position = memento->scrollTopItemPosition(),\n\t\t\t\t.item = MessageByGlobalId(memento->scrollTopItem()),\n\t\t\t\t.shift = memento->scrollTopShift(),\n\t\t\t});\n\t\t\trefreshViewer();\n\t\t}\n\t}\n}\n\n} // namespace Info::Saved\n"
  },
  {
    "path": "Telegram/SourceFiles/info/saved/info_saved_music_provider.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"data/data_saved_music.h\"\n#include \"info/media/info_media_common.h\"\n#include \"info/saved/info_saved_music_common.h\"\n#include \"history/history_item.h\"\n\nclass DocumentData;\nclass HistoryItem;\nclass PeerData;\nclass History;\n\nnamespace Info {\nclass AbstractController;\n} // namespace Info\n\nnamespace Info::Saved {\n\nclass MusicProvider final\n\t: public Media::ListProvider\n\t, private Media::ListSectionDelegate\n\t, public base::has_weak_ptr {\npublic:\n\texplicit MusicProvider(not_null<AbstractController*> controller);\n\t~MusicProvider();\n\n\tMedia::Type type() override;\n\tbool hasSelectRestriction() override;\n\trpl::producer<bool> hasSelectRestrictionChanges() override;\n\tbool isPossiblyMyItem(not_null<const HistoryItem*> item) override;\n\n\tstd::optional<int> fullCount() override;\n\n\tvoid restart() override;\n\tvoid checkPreload(\n\t\tQSize viewport,\n\t\tnot_null<Media::BaseLayout*> topLayout,\n\t\tnot_null<Media::BaseLayout*> bottomLayout,\n\t\tbool preloadTop,\n\t\tbool preloadBottom) override;\n\tvoid refreshViewer() override;\n\trpl::producer<> refreshed() override;\n\n\tvoid setSearchQuery(QString query) override;\n\tvoid jumpToMessage(MsgId, Fn<void(FullMsgId)> done) override;\n\n\tstd::vector<Media::ListSection> fillSections(\n\t\tnot_null<Overview::Layout::Delegate*> delegate) override;\n\trpl::producer<not_null<Media::BaseLayout*>> layoutRemoved() override;\n\tMedia::BaseLayout *lookupLayout(const HistoryItem *item) override;\n\tbool isMyItem(not_null<const HistoryItem*> item) override;\n\tbool isAfter(\n\t\tnot_null<const HistoryItem*> a,\n\t\tnot_null<const HistoryItem*> b) override;\n\n\tMedia::ListItemSelectionData computeSelectionData(\n\t\tnot_null<const HistoryItem*> item,\n\t\tTextSelection selection) override;\n\tvoid applyDragSelection(\n\t\tMedia::ListSelectedMap &selected,\n\t\tnot_null<const HistoryItem*> fromItem,\n\t\tbool skipFrom,\n\t\tnot_null<const HistoryItem*> tillItem,\n\t\tbool skipTill) override;\n\n\tbool allowSaveFileAs(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) override;\n\tQString showInFolderPath(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) override;\n\n\tint64 scrollTopStatePosition(not_null<HistoryItem*> item) override;\n\tHistoryItem *scrollTopStateItem(\n\t\tMedia::ListScrollTopState state) override;\n\tvoid saveState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tMedia::ListScrollTopState scrollState) override;\n\tvoid restoreState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tFn<void(Media::ListScrollTopState)> restoreScrollState) override;\n\nprivate:\n\tstatic constexpr auto kMinimalIdsLimit = 16;\n\n\tbool sectionHasFloatingHeader() override;\n\tQString sectionTitle(not_null<const Media::BaseLayout*> item) override;\n\tbool sectionItemBelongsHere(\n\t\tnot_null<const Media::BaseLayout*> item,\n\t\tnot_null<const Media::BaseLayout*> previous) override;\n\n\tvoid itemRemoved(not_null<const HistoryItem*> item);\n\tvoid markLayoutsStale();\n\tvoid clearStaleLayouts();\n\tvoid clear();\n\n\t[[nodiscard]] Media::BaseLayout *getLayout(\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<Overview::Layout::Delegate*> delegate);\n\t[[nodiscard]] std::unique_ptr<Media::BaseLayout> createLayout(\n\t\tnot_null<HistoryItem*> item,\n\t\tnot_null<Overview::Layout::Delegate*> delegate);\n\n\tconst not_null<AbstractController*> _controller;\n\tconst not_null<PeerData*> _peer;\n\tconst not_null<History*> _history;\n\n\tHistoryItem *_aroundId = nullptr;\n\tint _idsLimit = kMinimalIdsLimit;\n\tData::SavedMusicSlice _slice;\n\n\tstd::unordered_map<\n\t\tnot_null<const HistoryItem*>,\n\t\tMedia::CachedItem> _layouts;\n\trpl::event_stream<not_null<Media::BaseLayout*>> _layoutRemoved;\n\trpl::event_stream<> _refreshed;\n\n\trpl::lifetime _lifetime;\n\trpl::lifetime _viewerLifetime;\n\n};\n\n} // namespace Info::Saved\n"
  },
  {
    "path": "Telegram/SourceFiles/info/saved/info_saved_music_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/saved/info_saved_music_widget.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_saved_music.h\"\n#include \"data/data_session.h\"\n#include \"info/media/info_media_list_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"main/main_session.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_credits.h\" // giftListAbout\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Info::Saved {\n\nclass MusicInner final : public Ui::RpWidget {\npublic:\n\tMusicInner(QWidget *parent, not_null<Controller*> controller);\n\t~MusicInner();\n\n\tbool showInternal(not_null<MusicMemento*> memento);\n\tvoid setIsStackBottom(bool isStackBottom) {\n\t\t_isStackBottom = isStackBottom;\n\t}\n\n\tvoid saveState(not_null<MusicMemento*> memento);\n\tvoid restoreState(not_null<MusicMemento*> memento);\n\n\tvoid setScrollHeightValue(rpl::producer<int> value);\n\n\trpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\trpl::producer<SelectedItems> selectedListValue() const;\n\tvoid selectionAction(SelectionAction action);\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\nprivate:\n\tint recountHeight();\n\tvoid refreshHeight();\n\tvoid refreshEmpty();\n\n\tvoid setupList();\n\tvoid setupEmpty();\n\n\tconst not_null<Controller*> _controller;\n\tconst not_null<PeerData*> _peer;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tobject_ptr<Media::ListWidget> _list = { nullptr };\n\tobject_ptr<Ui::RpWidget> _empty = { nullptr };\n\tint _lastNonLoadingHeight = 0;\n\tbool _emptyLoading = false;\n\n\tbool _inResize = false;\n\tbool _isStackBottom = false;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\trpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;\n\trpl::event_stream<rpl::producer<int>> _listTops;\n\trpl::variable<int> _topHeight;\n\trpl::variable<bool> _albumEmpty;\n\n};\n\nMusicInner::MusicInner(QWidget *parent, not_null<Controller*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _peer(controller->key().musicPeer()) {\n\tsetupList();\n\tsetupEmpty();\n}\n\nMusicInner::~MusicInner() = default;\n\nvoid MusicInner::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tsetChildVisibleTopBottom(_list, visibleTop, visibleBottom);\n}\n\nbool MusicInner::showInternal(not_null<MusicMemento*> memento) {\n\tif (memento->section().type() == Section::Type::SavedMusic) {\n\t\trestoreState(memento);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid MusicInner::setupList() {\n\tExpects(!_list);\n\n\t_list = object_ptr<Media::ListWidget>(this, _controller);\n\tif (_peer->isSelf()) {\n\t\t_list->setReorderDescriptor({\n\t\t\t.save = [=](\n\t\t\t\t\tint oldPosition,\n\t\t\t\t\tint newPosition,\n\t\t\t\t\tFn<void()> done,\n\t\t\t\t\tFn<void()> fail) {\n\t\t\t\t_controller->session().data().savedMusic().reorder(\n\t\t\t\t\toldPosition,\n\t\t\t\t\tnewPosition);\n\t\t\t\tdone();\n\t\t\t}\n\t\t});\n\t}\n\tconst auto raw = _list.data();\n\n\tusing namespace rpl::mappers;\n\traw->scrollToRequests(\n\t) | rpl::map([=](int to) {\n\t\treturn Ui::ScrollToRequest {\n\t\t\traw->y() + to,\n\t\t\t-1\n\t\t};\n\t}) | rpl::start_to_stream(_scrollToRequests, raw->lifetime());\n\t_selectedLists.fire(raw->selectedListValue());\n\t_listTops.fire(raw->topValue());\n\n\traw->show();\n}\n\nvoid MusicInner::setupEmpty() {\n\t_list->resizeToWidth(width());\n\n\tconst auto savedMusic = &_controller->session().data().savedMusic();\n\trpl::combine(\n\t\trpl::single(\n\t\t\trpl::empty\n\t\t) | rpl::then(\n\t\t\tsavedMusic->changed() | rpl::filter(\n\t\t\t\trpl::mappers::_1 == _peer->id\n\t\t\t) | rpl::to_empty\n\t\t),\n\t\t_list->heightValue()\n\t) | rpl::on_next([=](auto, int listHeight) {\n\t\tconst auto padding = st::infoMediaMargin;\n\t\tif (const auto raw = _empty.release()) {\n\t\t\traw->hide();\n\t\t\traw->deleteLater();\n\t\t}\n\t\t_emptyLoading = false;\n\t\tif (listHeight <= padding.bottom() + padding.top()) {\n\t\t\trefreshEmpty();\n\t\t} else {\n\t\t\t_albumEmpty = false;\n\t\t}\n\t\trefreshHeight();\n\t}, _list->lifetime());\n}\n\nvoid MusicInner::refreshEmpty() {\n\tconst auto savedMusic = &_controller->session().data().savedMusic();\n\tconst auto knownEmpty = savedMusic->countKnown(_peer->id);\n\t_empty = object_ptr<Ui::FlatLabel>(\n\t\tthis,\n\t\t(!knownEmpty\n\t\t\t? tr::lng_contacts_loading(tr::marked)\n\t\t\t: rpl::single(\n\t\t\t\ttr::lng_media_song_empty(tr::now, tr::marked))),\n\t\tst::giftListAbout);\n\t_empty->show();\n\t_emptyLoading = !knownEmpty;\n\tresizeToWidth(width());\n}\n\nvoid MusicInner::saveState(not_null<MusicMemento*> memento) {\n\t_list->saveState(&memento->media());\n}\n\nvoid MusicInner::restoreState(not_null<MusicMemento*> memento) {\n\t_list->restoreState(&memento->media());\n}\n\nrpl::producer<SelectedItems> MusicInner::selectedListValue() const {\n\treturn _selectedLists.events_starting_with(\n\t\t_list->selectedListValue()\n\t) | rpl::flatten_latest();\n}\n\nvoid MusicInner::selectionAction(SelectionAction action) {\n\t_list->selectionAction(action);\n}\n\nint MusicInner::resizeGetHeight(int newWidth) {\n\tif (!newWidth) {\n\t\treturn 0;\n\t}\n\t_inResize = true;\n\tauto guard = gsl::finally([this] { _inResize = false; });\n\n\tif (_list) {\n\t\t_list->resizeToWidth(newWidth);\n\t}\n\tif (const auto empty = _empty.get()) {\n\t\tconst auto margin = st::giftListAboutMargin;\n\t\tempty->resizeToWidth(newWidth - margin.left() - margin.right());\n\t}\n\n\treturn recountHeight();\n}\n\nvoid MusicInner::refreshHeight() {\n\tif (_inResize) {\n\t\treturn;\n\t}\n\tresize(width(), recountHeight());\n}\n\nint MusicInner::recountHeight() {\n\tauto top = 0;\n\tauto listHeight = 0;\n\tif (_list) {\n\t\t_list->moveToLeft(0, top);\n\t\tlistHeight = _list->heightNoMargins();\n\t\ttop += listHeight;\n\t}\n\tif (const auto empty = _empty.get()) {\n\t\tconst auto margin = st::giftListAboutMargin;\n\t\tempty->moveToLeft(margin.left(), top + margin.top());\n\t\ttop += margin.top() + empty->height() + margin.bottom();\n\t}\n\tif (_emptyLoading) {\n\t\ttop = std::max(top, _lastNonLoadingHeight);\n\t} else {\n\t\t_lastNonLoadingHeight = top;\n\t}\n\treturn top;\n}\n\nvoid MusicInner::setScrollHeightValue(rpl::producer<int> value) {\n}\n\nrpl::producer<Ui::ScrollToRequest> MusicInner::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nMusicMemento::MusicMemento(not_null<Controller*> controller)\n: ContentMemento(MusicTag{ controller->musicPeer() })\n, _media(controller) {\n}\n\nMusicMemento::MusicMemento(not_null<PeerData*> peer)\n: ContentMemento(MusicTag{ peer })\n, _media(peer, 0, Media::Type::MusicFile) {\n}\n\nMusicMemento::~MusicMemento() = default;\n\nSection MusicMemento::section() const {\n\treturn Section(Section::Type::SavedMusic);\n}\n\nobject_ptr<ContentWidget> MusicMemento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<MusicWidget>(parent, controller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nMusicWidget::MusicWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: ContentWidget(parent, controller) {\n\t_inner = setInnerWidget(object_ptr<MusicInner>(this, controller));\n\t_inner->setScrollHeightValue(scrollHeightValue());\n\t_inner->scrollToRequests(\n\t) | rpl::on_next([this](Ui::ScrollToRequest request) {\n\t\tscrollTo(request);\n\t}, _inner->lifetime());\n}\n\nvoid MusicWidget::setIsStackBottom(bool isStackBottom) {\n\tContentWidget::setIsStackBottom(isStackBottom);\n\t_inner->setIsStackBottom(isStackBottom);\n}\n\nbool MusicWidget::showInternal(not_null<ContentMemento*> memento) {\n\tif (!controller()->validateMementoPeer(memento)) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nvoid MusicWidget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<MusicMemento*> memento) {\n\t\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nstd::shared_ptr<ContentMemento> MusicWidget::doCreateMemento() {\n\tauto result = std::make_shared<MusicMemento>(controller());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid MusicWidget::saveState(not_null<MusicMemento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid MusicWidget::restoreState(not_null<MusicMemento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\nrpl::producer<SelectedItems> MusicWidget::selectedListValue() const {\n\treturn _inner->selectedListValue();\n}\n\nvoid MusicWidget::selectionAction(SelectionAction action) {\n\t_inner->selectionAction(action);\n}\n\nrpl::producer<QString> MusicWidget::title() {\n\treturn controller()->key().musicPeer()->isSelf()\n\t\t? tr::lng_media_saved_music_your()\n\t\t: tr::lng_media_saved_music_title();\n}\n\nstd::shared_ptr<Info::Memento> MakeMusic(not_null<PeerData*> peer) {\n\treturn std::make_shared<Info::Memento>(\n\t\tstd::vector<std::shared_ptr<ContentMemento>>(\n\t\t\t1,\n\t\t\tstd::make_shared<MusicMemento>(peer)));\n}\n\n} // namespace Info::Saved\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/saved/info_saved_music_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n#include \"info/media/info_media_widget.h\"\n#include \"info/stories/info_stories_common.h\"\n\nnamespace Ui {\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Info::Saved {\n\nclass MusicInner;\n\nclass MusicMemento final : public ContentMemento {\npublic:\n\tMusicMemento(not_null<Controller*> controller);\n\tMusicMemento(not_null<PeerData*> peer);\n\t~MusicMemento();\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\t[[nodiscard]] Media::Memento &media() {\n\t\treturn _media;\n\t}\n\t[[nodiscard]] const Media::Memento &media() const {\n\t\treturn _media;\n\t}\n\nprivate:\n\tMedia::Memento _media;\n\n};\n\nclass MusicWidget final : public ContentWidget {\npublic:\n\tMusicWidget(QWidget *parent, not_null<Controller*> controller);\n\n\tvoid setIsStackBottom(bool isStackBottom) override;\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<MusicMemento*> memento);\n\n\trpl::producer<SelectedItems> selectedListValue() const override;\n\tvoid selectionAction(SelectionAction action) override;\n\n\trpl::producer<QString> title() override;\n\nprivate:\n\tvoid saveState(not_null<MusicMemento*> memento);\n\tvoid restoreState(not_null<MusicMemento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tMusicInner *_inner = nullptr;\n\n};\n\n[[nodiscard]] std::shared_ptr<Info::Memento> MakeMusic(\n\tnot_null<PeerData*> peer);\n\n} // namespace Info::Saved\n"
  },
  {
    "path": "Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/saved/info_saved_sublists_widget.h\"\n//\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"dialogs/dialogs_inner_widget.h\"\n#include \"history/view/history_view_chat_section.h\"\n#include \"info/media/info_media_buttons.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"main/main_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/box_content_divider.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info::Saved {\n\nSublistsMemento::SublistsMemento(not_null<Main::Session*> session)\n: ContentMemento(session->user(), nullptr, nullptr, PeerId()) {\n}\n\nSection SublistsMemento::section() const {\n\treturn Section(Section::Type::SavedSublists);\n}\n\nobject_ptr<ContentWidget> SublistsMemento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<SublistsWidget>(parent, controller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nSublistsMemento::~SublistsMemento() = default;\n\nSublistsWidget::SublistsWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: ContentWidget(parent, controller)\n, _layout(setInnerWidget(object_ptr<Ui::VerticalLayout>(this))) {\n\tsetupOtherTypes();\n\n\t_list = _layout->add(object_ptr<Dialogs::InnerWidget>(\n\t\tthis,\n\t\tcontroller->parentController(),\n\t\trpl::single(Dialogs::InnerWidget::ChildListShown())));\n\t_list->showSavedSublists();\n\t_list->setNarrowRatio(0.);\n\n\t_list->chosenRow() | rpl::on_next([=](Dialogs::ChosenRow row) {\n\t\tif (const auto sublist = row.key.sublist()) {\n\t\t\tusing namespace Window;\n\t\t\tusing namespace HistoryView;\n\t\t\tauto params = SectionShow(SectionShow::Way::Forward);\n\t\t\tparams.dropSameFromStack = true;\n\t\t\tcontroller->showSection(\n\t\t\t\tstd::make_shared<ChatMemento>(ChatViewId{\n\t\t\t\t\t.history = sublist->owningHistory(),\n\t\t\t\t\t.sublist = sublist,\n\t\t\t\t}),\n\t\t\t\tparams);\n\t\t}\n\t}, _list->lifetime());\n\n\tconst auto saved = &controller->session().data().savedMessages();\n\t_list->heightValue() | rpl::on_next([=] {\n\t\tif (!saved->supported()) {\n\t\t\tcrl::on_main(controller, [=] {\n\t\t\t\tcontroller->showSection(\n\t\t\t\t\tMemento::Default(controller->session().user()),\n\t\t\t\t\tWindow::SectionShow::Way::Backward);\n\t\t\t});\n\t\t}\n\t}, lifetime());\n\n\t_list->setLoadMoreCallback([=] {\n\t\tsaved->loadMore();\n\t});\n}\n\nvoid SublistsWidget::setupOtherTypes() {\n\tauto wrap = _layout->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_layout,\n\t\t\tobject_ptr<Ui::VerticalLayout>(_layout)));\n\tauto content = wrap->entity();\n\tcontent->add(object_ptr<Ui::FixedHeightWidget>(\n\t\tcontent,\n\t\tst::infoProfileSkip));\n\n\tusing Type = Media::Type;\n\tauto tracker = Ui::MultiSlideTracker();\n\tconst auto peer = controller()->session().user();\n\tconst auto addMediaButton = [&](\n\t\t\tType buttonType,\n\t\t\tconst style::icon &icon) {\n\t\tauto result = Media::AddButton(\n\t\t\tcontent,\n\t\t\tcontroller(),\n\t\t\tpeer,\n\t\t\tMsgId(), // topicRootId\n\t\t\tPeerId(), // monoforumPeerId\n\t\t\tnullptr, // migrated\n\t\t\tbuttonType,\n\t\t\ttracker);\n\t\tobject_ptr<Profile::FloatingIcon>(\n\t\t\tresult,\n\t\t\ticon,\n\t\t\tst::infoSharedMediaButtonIconPosition)->show();\n\t};\n\n\taddMediaButton(Type::Photo, st::infoIconMediaPhoto);\n\taddMediaButton(Type::Video, st::infoIconMediaVideo);\n\taddMediaButton(Type::File, st::infoIconMediaFile);\n\taddMediaButton(Type::MusicFile, st::infoIconMediaAudio);\n\taddMediaButton(Type::Link, st::infoIconMediaLink);\n\taddMediaButton(Type::Poll, st::infoIconMediaPoll);\n\taddMediaButton(Type::RoundVoiceFile, st::infoIconMediaVoice);\n\taddMediaButton(Type::GIF, st::infoIconMediaGif);\n\n\tcontent->add(object_ptr<Ui::FixedHeightWidget>(\n\t\tcontent,\n\t\tst::infoProfileSkip));\n\twrap->toggleOn(tracker.atLeastOneShownValue());\n\twrap->finishAnimating();\n\n\t_layout->add(object_ptr<Ui::BoxContentDivider>(_layout));\n\t_layout->add(object_ptr<Ui::FixedHeightWidget>(\n\t\tcontent,\n\t\tst::infoProfileSkip));\n}\n\nrpl::producer<QString> SublistsWidget::title() {\n\treturn tr::lng_saved_messages();\n}\n\nrpl::producer<QString> SublistsWidget::subtitle() {\n\tconst auto saved = &controller()->session().data().savedMessages();\n\treturn saved->chatsList()->fullSize().value(\n\t) | rpl::map([=](int value) {\n\t\treturn (value || saved->chatsList()->loaded())\n\t\t\t? tr::lng_filters_chats_count(tr::now, lt_count, value)\n\t\t\t: tr::lng_contacts_loading(tr::now);\n\t});\n}\n\nbool SublistsWidget::showInternal(not_null<ContentMemento*> memento) {\n\tif (!controller()->validateMementoPeer(memento)) {\n\t\treturn false;\n\t}\n\tif (auto my = dynamic_cast<SublistsMemento*>(memento.get())) {\n\t\trestoreState(my);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid SublistsWidget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<SublistsMemento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nstd::shared_ptr<ContentMemento> SublistsWidget::doCreateMemento() {\n\tauto result = std::make_shared<SublistsMemento>(\n\t\t&controller()->session());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid SublistsWidget::saveState(not_null<SublistsMemento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n}\n\nvoid SublistsWidget::restoreState(not_null<SublistsMemento*> memento) {\n\tscrollTopRestore(memento->scrollTop());\n}\n\n} // namespace Info::Saved\n"
  },
  {
    "path": "Telegram/SourceFiles/info/saved/info_saved_sublists_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n\nnamespace Dialogs {\nclass InnerWidget;\n} // namespace Dialogs\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Info::Saved {\n\nclass SublistsMemento final : public ContentMemento {\npublic:\n\texplicit SublistsMemento(not_null<Main::Session*> session);\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\t~SublistsMemento();\n\nprivate:\n\n};\n\nclass SublistsWidget final : public ContentWidget {\npublic:\n\tSublistsWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller);\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<SublistsMemento*> memento);\n\n\trpl::producer<QString> title() override;\n\trpl::producer<QString> subtitle() override;\n\nprivate:\n\tvoid saveState(not_null<SublistsMemento*> memento);\n\tvoid restoreState(not_null<SublistsMemento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tvoid setupOtherTypes();\n\n\tconst not_null<Ui::VerticalLayout*> _layout;\n\tDialogs::InnerWidget *_list = nullptr;\n\n};\n\n} // namespace Info::Saved\n"
  },
  {
    "path": "Telegram/SourceFiles/info/settings/info_settings_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/settings/info_settings_widget.h\"\n\n#include \"info/info_memento.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"settings/sections/settings_information.h\"\n#include \"settings/settings_common_session.h\"\n#include \"ui/ui_utility.h\"\n\nnamespace Info {\nnamespace Settings {\n\nMemento::Memento(not_null<UserData*> self, Type type)\n: ContentMemento(Tag{ self })\n, _type(type) {\n}\n\nSection Memento::section() const {\n\treturn Section(_type);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(\n\t\tparent,\n\t\tcontroller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nMemento::~Memento() = default;\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: ContentWidget(parent, controller)\n, _self(controller->key().settingsSelf())\n, _type(controller->section().settingsType())\n, _inner(setupFlexibleInnerWidget(\n\t\t_type->create(\n\t\t\tthis,\n\t\t\tcontroller->parentController(),\n\t\t\tscroll(),\n\t\t\tcontroller->wrapValue(\n\t\t\t) | rpl::map([](Wrap wrap) { return (wrap == Wrap::Layer)\n\t\t\t\t? ::Settings::Container::Layer\n\t\t\t\t: ::Settings::Container::Section; })),\n\t\t_flexibleScroll,\n\t\t[=](Ui::RpWidget*) {\n\t\t\tcontroller->stepDataReference() = SectionCustomTopBarData{\n\t\t\t\t.backButtonEnables = _flexibleScroll.backButtonEnables.events(),\n\t\t\t\t.wrapValue = controller->wrapValue(),\n\t\t\t};\n\t\t}))\n, _pinnedToTop(_inner->createPinnedToTop(this))\n, _pinnedToBottom(_inner->createPinnedToBottom(this)) {\n\t_inner->sectionShowOther(\n\t) | rpl::on_next([=](Type type) {\n\t\tcontroller->showSettings(type);\n\t}, _inner->lifetime());\n\n\t_inner->sectionShowBack(\n\t) | rpl::on_next([=] {\n\t\tcontroller->showBackFromStack();\n\t}, _inner->lifetime());\n\n\t_inner->setStepDataReference(controller->stepDataReference());\n\n\t_removesFromStack.events(\n\t) | rpl::on_next([=](const std::vector<Type> &types) {\n\t\tconst auto sections = ranges::views::all(\n\t\t\ttypes\n\t\t) | ranges::views::transform([](Type type) {\n\t\t\treturn Section(type);\n\t\t}) | ranges::to_vector;\n\t\tcontroller->removeFromStack(sections);\n\t}, _inner->lifetime());\n\n\tif (_pinnedToTop) {\n\t\t_inner->widthValue(\n\t\t) | rpl::on_next([=](int w) {\n\t\t\t_pinnedToTop->resizeToWidth(w);\n\t\t\tsetScrollTopSkip(_pinnedToTop->height());\n\t\t}, _pinnedToTop->lifetime());\n\n\t\t_pinnedToTop->heightValue(\n\t\t) | rpl::on_next([=](int h) {\n\t\t\tsetScrollTopSkip(h);\n\t\t}, _pinnedToTop->lifetime());\n\t}\n\n\tif (_pinnedToBottom) {\n\t\tconst auto processHeight = [=] {\n\t\t\tsetScrollBottomSkip(_pinnedToBottom->height());\n\t\t\t_pinnedToBottom->moveToLeft(\n\t\t\t\t_pinnedToBottom->x(),\n\t\t\t\theight() - _pinnedToBottom->height());\n\t\t};\n\n\t\t_inner->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\t_pinnedToBottom->resizeToWidth(s.width());\n\t\t\t//processHeight();\n\t\t}, _pinnedToBottom->lifetime());\n\n\t\trpl::combine(\n\t\t\t_pinnedToBottom->heightValue(),\n\t\t\theightValue()\n\t\t) | rpl::on_next(processHeight, _pinnedToBottom->lifetime());\n\t}\n\n\tif (_pinnedToTop\n\t\t&& _pinnedToTop->minimumHeight()\n\t\t&& _inner->hasFlexibleTopBar()) {\n\t\tconst auto heightDiff = [=] {\n\t\t\treturn _pinnedToTop->maximumHeight()\n\t\t\t\t- _pinnedToTop->minimumHeight();\n\t\t};\n\n\t\trpl::combine(\n\t\t\t_pinnedToTop->heightValue(),\n\t\t\t_inner->heightValue()\n\t\t) | rpl::on_next([=](int, int h) {\n\t\t\t_flexibleScroll.contentHeightValue.fire(h + heightDiff());\n\t\t}, _pinnedToTop->lifetime());\n\n\t\tscrollTopValue(\n\t\t) | rpl::on_next([=](int top) {\n\t\t\tif (!_pinnedToTop) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto current = heightDiff() - top;\n\t\t\t_inner->moveToLeft(0, std::min(0, current));\n\t\t\t_pinnedToTop->resize(\n\t\t\t\t_pinnedToTop->width(),\n\t\t\t\tstd::max(current + _pinnedToTop->minimumHeight(), 0));\n\t\t}, _inner->lifetime());\n\n\t\t_flexibleScroll.fillerWidthValue.events(\n\t\t) | rpl::on_next([=](int w) {\n\t\t\t_inner->resizeToWidth(w);\n\t\t}, _inner->lifetime());\n\n\t\tsetPaintPadding({ 0, _pinnedToTop->minimumHeight(), 0, 0 });\n\n\t\tsetViewport(_pinnedToTop->events(\n\t\t) | rpl::filter([](not_null<QEvent*> e) {\n\t\t\treturn e->type() == QEvent::Wheel;\n\t\t}));\n\t}\n}\n\nWidget::~Widget() = default;\n\nnot_null<UserData*> Widget::self() const {\n\treturn _self;\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\t//if (const auto myMemento = dynamic_cast<Memento*>(memento.get())) {\n\t//\tAssert(myMemento->self() == self());\n\n\t//\tif (_inner->showInternal(myMemento)) {\n\t//\t\treturn true;\n\t//\t}\n\t//}\n\treturn false;\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nvoid Widget::saveChanges(FnMut<void()> done) {\n\t_inner->sectionSaveChanges(std::move(done));\n}\n\nvoid Widget::showFinished() {\n\t_inner->showFinished();\n\n\t_inner->removeFromStack(\n\t) | rpl::start_to_stream(_removesFromStack, lifetime());\n}\n\nvoid Widget::setInnerFocus() {\n\t_inner->setInnerFocus();\n}\n\nconst Ui::RoundRect *Widget::bottomSkipRounding() const {\n\treturn _inner->bottomSkipRounding();\n}\n\nrpl::producer<bool> Widget::desiredShadowVisibility() const {\n\treturn (_type == ::Settings::MainId()\n\t\t|| _type == ::Settings::InformationId())\n\t\t? ContentWidget::desiredShadowVisibility()\n\t\t: rpl::single(true);\n}\n\nbool Widget::closeByOutsideClick() const {\n\treturn _inner->closeByOutsideClick();;\n}\n\nvoid Widget::checkBeforeClose(Fn<void()> close) {\n\t_inner->checkBeforeClose(std::move(close));\n}\n\nvoid Widget::checkBeforeCloseByEscape(Fn<void()> close) {\n\tContentWidget::checkBeforeCloseByEscape([&] {\n\t\t_inner->checkBeforeClose(std::move(close));\n\t});\n}\n\nrpl::producer<QString> Widget::title() {\n\treturn _inner->title();\n}\n\nvoid Widget::paintEvent(QPaintEvent *e) {\n\tif (!_inner->paintOuter(this, maxVisibleHeight(), e->rect())) {\n\t\tContentWidget::paintEvent(e);\n\t}\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(self(), _type);\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::enableBackButton() {\n\t_flexibleScroll.backButtonEnables.fire({});\n}\n\nrpl::producer<SelectedItems> Widget::selectedListValue() const {\n\treturn _inner->selectedListValue();\n}\n\nvoid Widget::selectionAction(SelectionAction action) {\n\t_inner->selectionAction(action);\n}\n\nvoid Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {\n\t_inner->fillTopBarMenu(addAction);\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\tauto sectionState = std::any();\n\t_inner->sectionSaveState(sectionState);\n\tmemento->setSectionState(std::move(sectionState));\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->sectionRestoreState(memento->sectionState());\n\tscrollTopRestore(memento->scrollTop());\n}\n\n} // namespace Settings\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/settings/info_settings_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_flexible_scroll.h\"\n\n#include <any>\n\nnamespace Settings {\nclass AbstractSection;\n} // namespace Settings\n\nnamespace Info {\nnamespace Settings {\n\nusing Type = Section::SettingsType;\n\nstruct Tag;\n\nstruct SectionCustomTopBarData {\n\trpl::producer<> backButtonEnables;\n\trpl::producer<Info::Wrap> wrapValue;\n};\n\nclass Memento final : public ContentMemento {\npublic:\n\tMemento(not_null<UserData*> self, Type type);\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\tType type() const {\n\t\treturn _type;\n\t}\n\n\tnot_null<UserData*> self() const {\n\t\treturn settingsSelf();\n\t}\n\n\t~Memento();\n\n\tvoid setSectionState(std::any state) {\n\t\t_sectionState = std::move(state);\n\t}\n\t[[nodiscard]] const std::any &sectionState() const {\n\t\treturn _sectionState;\n\t}\n\nprivate:\n\tType _type = Type();\n\tstd::any _sectionState;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller);\n\t~Widget();\n\n\tnot_null<UserData*> self() const;\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\n\tvoid saveChanges(FnMut<void()> done) override;\n\n\tvoid showFinished() override;\n\tvoid setInnerFocus() override;\n\tconst Ui::RoundRect *bottomSkipRounding() const override;\n\n\trpl::producer<bool> desiredShadowVisibility() const override;\n\n\tbool closeByOutsideClick() const override;\n\tvoid checkBeforeClose(Fn<void()> close) override;\n\tvoid checkBeforeCloseByEscape(Fn<void()> close) override;\n\trpl::producer<QString> title() override;\n\n\tvoid enableBackButton() override;\n\n\trpl::producer<SelectedItems> selectedListValue() const override;\n\tvoid selectionAction(SelectionAction action) override;\n\tvoid fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override;\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tnot_null<UserData*> _self;\n\tType _type = Type();\n\n\tFlexibleScrollData _flexibleScroll;\n\tnot_null<::Settings::AbstractSection*> _inner;\n\tbase::weak_qptr<Ui::RpWidget> _pinnedToTop;\n\tbase::weak_qptr<Ui::RpWidget> _pinnedToBottom;\n\n\trpl::event_stream<std::vector<Type>> _removesFromStack;\n\n};\n\n} // namespace Settings\n} // namespace Info\n"
  },
  {
    "path": "Telegram/SourceFiles/info/similar_peers/info_similar_peers_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/similar_peers/info_similar_peers_widget.h\"\n\n#include \"api/api_chat_participants.h\"\n#include \"apiwrap.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"info/info_controller.h\"\n#include \"main/main_session.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/ui_utility.h\"\n#include \"lang/lang_keys.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Info::SimilarPeers {\nnamespace {\n\nclass ListController final : public PeerListController {\npublic:\n\tListController(\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<PeerData*> peer);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\n\tstd::unique_ptr<PeerListRow> createRestoredRow(\n\t\t\tnot_null<PeerData*> peer) override {\n\t\treturn createRow(peer);\n\t}\n\n\tstd::unique_ptr<PeerListState> saveState() const override;\n\tvoid restoreState(std::unique_ptr<PeerListState> state) override;\n\n\tvoid setContentWidget(not_null<Ui::RpWidget*> widget);\n\t[[nodiscard]] rpl::producer<int> unlockHeightValue() const;\n\nprivate:\n\tstd::unique_ptr<PeerListRow> createRow(not_null<PeerData*> peer);\n\tvoid setupUnlock();\n\tvoid rebuild();\n\n\tstruct SavedState : SavedStateBase {\n\t};\n\tconst not_null<Controller*> _controller;\n\tconst not_null<PeerData*> _peer;\n\tUi::RpWidget *_content = nullptr;\n\tUi::RpWidget *_unlock = nullptr;\n\trpl::variable<int> _unlockHeight;\n\n};\n\nListController::ListController(\n\tnot_null<Controller*> controller,\n\tnot_null<PeerData*> peer)\n: PeerListController()\n, _controller(controller)\n, _peer(peer) {\n}\n\nMain::Session &ListController::session() const {\n\treturn _peer->session();\n}\n\nstd::unique_ptr<PeerListRow> ListController::createRow(\n\t\tnot_null<PeerData*> peer) {\n\tauto result = std::make_unique<PeerListRow>(peer);\n\tif (const auto channel = peer->asChannel()) {\n\t\tif (const auto count = channel->membersCount(); count > 1) {\n\t\t\tresult->setCustomStatus(\n\t\t\t\ttr::lng_chat_status_subscribers(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\tcount));\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid ListController::prepare() {\n\tdelegate()->peerListSetTitle(_peer->isBroadcast()\n\t\t? tr::lng_similar_channels_title()\n\t\t: tr::lng_similar_bots_title());\n\n\tconst auto participants = &_peer->session().api().chatParticipants();\n\n\tData::AmPremiumValue(\n\t\t&_peer->session()\n\t) | rpl::on_next([=] {\n\t\tparticipants->loadSimilarPeers(_peer);\n\t\trebuild();\n\t}, lifetime());\n\n\tparticipants->similarLoaded(\n\t) | rpl::filter(\n\t\trpl::mappers::_1 == _peer\n\t) | rpl::on_next([=] {\n\t\trebuild();\n\t}, lifetime());\n}\n\nvoid ListController::setContentWidget(not_null<Ui::RpWidget*> widget) {\n\t_content = widget;\n}\n\nrpl::producer<int> ListController::unlockHeightValue() const {\n\treturn _unlockHeight.value();\n}\n\nvoid ListController::rebuild() {\n\tconst auto participants = &_peer->session().api().chatParticipants();\n\tconst auto &list = participants->similar(_peer);\n\tfor (const auto &peer : list.list) {\n\t\tif (!delegate()->peerListFindRow(peer->id.value)) {\n\t\t\tdelegate()->peerListAppendRow(createRow(peer));\n\t\t}\n\t}\n\tif (!list.more\n\t\t|| _peer->session().premium()\n\t\t|| !_peer->session().premiumPossible()) {\n\t\tdelete base::take(_unlock);\n\t\t_unlockHeight = 0;\n\t} else if (!_unlock) {\n\t\tsetupUnlock();\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid ListController::setupUnlock() {\n\tExpects(_content != nullptr);\n\n\t_unlock = Ui::CreateChild<Ui::RpWidget>(_content);\n\t_unlock->show();\n\n\tconst auto button = ::Settings::CreateLockedButton(\n\t\t_unlock,\n\t\t(_peer->isBroadcast()\n\t\t\t? tr::lng_similar_channels_show_more()\n\t\t\t: tr::lng_similar_bots_show_more()),\n\t\tst::similarChannelsLock,\n\t\trpl::single(true));\n\tbutton->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\tbutton->setClickedCallback([=] {\n\t\tconst auto window = _controller->parentController();\n\t\t::Settings::ShowPremium(window, u\"similar_channels\"_q);\n\t});\n\n\tconst auto upto = Data::PremiumLimits(\n\t\t&_peer->session()).similarChannelsPremium();\n\tconst auto about = Ui::CreateChild<Ui::FlatLabel>(\n\t\t_unlock,\n\t\t(_peer->isBroadcast()\n\t\t\t? tr::lng_similar_channels_premium_all\n\t\t\t: tr::lng_similar_bots_premium_all)(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(upto * 1.),\n\t\t\t\tlt_link,\n\t\t\t\ttr::lng_similar_channels_premium_all_link(\n\t\t\t\t\ttr::bold\n\t\t\t\t) | rpl::map(tr::link),\n\t\t\t\ttr::rich),\n\t\tst::similarChannelsLockAbout);\n\tabout->setClickHandlerFilter([=](const auto &...) {\n\t\tconst auto window = _controller->parentController();\n\t\t::Settings::ShowPremium(window, u\"similar_channels\"_q);\n\t\treturn false;\n\t});\n\n\trpl::combine(\n\t\t_content->sizeValue(),\n\t\t(_peer->isBroadcast()\n\t\t\t? tr::lng_similar_channels_show_more()\n\t\t\t: tr::lng_similar_bots_show_more())\n\t) | rpl::on_next([=](QSize size, const auto &) {\n\t\tauto top = st::similarChannelsLockFade\n\t\t\t+ st::similarChannelsLockPadding.top();\n\t\tbutton->setGeometry(\n\t\t\tst::similarChannelsLockPadding.left(),\n\t\t\ttop,\n\t\t\t(size.width()\n\t\t\t\t- st::similarChannelsLockPadding.left()\n\t\t\t\t- st::similarChannelsLockPadding.right()),\n\t\t\tbutton->height());\n\t\ttop += button->height() + st::similarChannelsLockPadding.bottom();\n\n\t\tconst auto minWidth = st::similarChannelsLockAbout.minWidth;\n\t\tconst auto maxWidth = std::max(\n\t\t\tminWidth + 1,\n\t\t\t(size.width()\n\t\t\t\t- st::similarChannelsLockAboutPadding.left()\n\t\t\t\t- st::similarChannelsLockAboutPadding.right()));\n\t\tconst auto countAboutHeight = [&](int width) {\n\t\t\tabout->resizeToWidth(width);\n\t\t\treturn about->height();\n\t\t};\n\t\tconst auto desired = Ui::FindNiceTooltipWidth(\n\t\t\tminWidth,\n\t\t\tmaxWidth,\n\t\t\tcountAboutHeight);\n\t\tabout->resizeToWidth(desired);\n\t\tabout->move((size.width() - about->width()) / 2, top);\n\t\ttop += about->height()\n\t\t\t+ st::similarChannelsLockAboutPadding.bottom();\n\t\t_unlock->setGeometry(0, size.height() - top, size.width(), top);\n\t}, _unlock->lifetime());\n\n\t_unlockHeight = _unlock->heightValue();\n\n\t_unlock->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(_unlock);\n\t\tconst auto width = _unlock->width();\n\t\tconst auto fade = st::similarChannelsLockFade;\n\t\tauto gradient = QLinearGradient(0, 0, 0, fade);\n\t\tgradient.setStops({\n\t\t\t{ 0., QColor(255, 255, 255, 0) },\n\t\t\t{ 1., st::windowBg->c },\n\t\t});\n\t\tp.fillRect(0, 0, width, fade, gradient);\n\t\tp.fillRect(0, fade, width, _unlock->height() - fade, st::windowBg);\n\t}, _unlock->lifetime());\n}\n\nvoid ListController::loadMoreRows() {\n}\n\nstd::unique_ptr<PeerListState> ListController::saveState() const {\n\tauto result = PeerListController::saveState();\n\tauto my = std::make_unique<SavedState>();\n\tresult->controllerState = std::move(my);\n\treturn result;\n}\n\nvoid ListController::restoreState(\n\t\tstd::unique_ptr<PeerListState> state) {\n\tauto typeErasedState = state\n\t\t? state->controllerState.get()\n\t\t: nullptr;\n\tif (dynamic_cast<SavedState*>(typeErasedState)) {\n\t\tPeerListController::restoreState(std::move(state));\n\t}\n}\n\nvoid ListController::rowClicked(not_null<PeerListRow*> row) {\n\t_controller->parentController()->showPeerHistory(\n\t\trow->peer(),\n\t\tWindow::SectionShow::Way::Forward);\n}\n\n} // namespace\n\nclass InnerWidget final\n\t: public Ui::RpWidget\n\t, private PeerListContentDelegate {\npublic:\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<PeerData*> peer);\n\n\t[[nodiscard]] not_null<PeerData*> peer() const {\n\t\treturn _peer;\n\t}\n\n\trpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\n\tint desiredHeight() const;\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\nprivate:\n\tusing ListWidget = PeerListContent;\n\n\t// PeerListContentDelegate interface.\n\tvoid peerListSetTitle(rpl::producer<QString> title) override;\n\tvoid peerListSetAdditionalTitle(rpl::producer<QString> title) override;\n\tbool peerListIsRowChecked(not_null<PeerListRow*> row) override;\n\tint peerListSelectedRowsCount() override;\n\tvoid peerListScrollToTop() override;\n\tvoid peerListAddSelectedPeerInBunch(\n\t\tnot_null<PeerData*> peer) override;\n\tvoid peerListAddSelectedRowInBunch(\n\t\tnot_null<PeerListRow*> row) override;\n\tvoid peerListFinishSelectedRowsBunch() override;\n\tvoid peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) override;\n\tstd::shared_ptr<Main::SessionShow> peerListUiShow() override;\n\n\tobject_ptr<ListWidget> setupList(\n\t\tRpWidget *parent,\n\t\tnot_null<ListController*> controller);\n\n\tconst std::shared_ptr<Main::SessionShow> _show;\n\tnot_null<Controller*> _controller;\n\tconst not_null<PeerData*> _peer;\n\tstd::unique_ptr<ListController> _listController;\n\tobject_ptr<ListWidget> _list;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\n};\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller,\n\tnot_null<PeerData*> peer)\n: RpWidget(parent)\n, _show(controller->uiShow())\n, _controller(controller)\n, _peer(peer)\n, _listController(std::make_unique<ListController>(controller, _peer))\n, _list(setupList(this, _listController.get())) {\n\tsetContent(_list.data());\n\t_listController->setDelegate(static_cast<PeerListDelegate*>(this));\n}\n\nvoid InnerWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\tsetChildVisibleTopBottom(_list, visibleTop, visibleBottom);\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\tmemento->setListState(_listController->saveState());\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\t_listController->restoreState(memento->listState());\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nint InnerWidget::desiredHeight() const {\n\tauto desired = 0;\n\tdesired += _list->fullRowsCount() * st::infoMembersList.item.height;\n\treturn qMax(height(), desired);\n}\n\nobject_ptr<InnerWidget::ListWidget> InnerWidget::setupList(\n\t\tRpWidget *parent,\n\t\tnot_null<ListController*> controller) {\n\tcontroller->setStyleOverrides(&st::infoMembersList);\n\tauto result = object_ptr<ListWidget>(\n\t\tparent,\n\t\tcontroller);\n\tcontroller->setContentWidget(this);\n\tresult->scrollToRequests(\n\t) | rpl::on_next([this](Ui::ScrollToRequest request) {\n\t\tauto addmin = (request.ymin < 0)\n\t\t\t? 0\n\t\t\t: st::infoCommonGroupsMargin.top();\n\t\tauto addmax = (request.ymax < 0)\n\t\t\t? 0\n\t\t\t: st::infoCommonGroupsMargin.top();\n\t\t_scrollToRequests.fire({\n\t\t\trequest.ymin + addmin,\n\t\t\trequest.ymax + addmax });\n\t}, result->lifetime());\n\tresult->moveToLeft(0, st::infoCommonGroupsMargin.top());\n\tparent->widthValue(\n\t) | rpl::on_next([list = result.data()](int newWidth) {\n\t\tlist->resizeToWidth(newWidth);\n\t}, result->lifetime());\n\trpl::combine(\n\t\tresult->heightValue(),\n\t\tcontroller->unlockHeightValue()\n\t) | rpl::on_next([=](int listHeight, int unlockHeight) {\n\t\tauto newHeight = st::infoCommonGroupsMargin.top()\n\t\t\t+ listHeight\n\t\t\t+ (unlockHeight\n\t\t\t\t? (unlockHeight - st::similarChannelsLockOverlap)\n\t\t\t\t: st::infoCommonGroupsMargin.bottom());\n\t\tparent->resize(parent->width(), std::max(newHeight, 0));\n\t}, result->lifetime());\n\treturn result;\n}\n\nvoid InnerWidget::peerListSetTitle(rpl::producer<QString> title) {\n}\n\nvoid InnerWidget::peerListSetAdditionalTitle(rpl::producer<QString> title) {\n}\n\nbool InnerWidget::peerListIsRowChecked(not_null<PeerListRow*> row) {\n\treturn false;\n}\n\nint InnerWidget::peerListSelectedRowsCount() {\n\treturn 0;\n}\n\nvoid InnerWidget::peerListScrollToTop() {\n\t_scrollToRequests.fire({ -1, -1 });\n}\n\nvoid InnerWidget::peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) {\n\tUnexpected(\"Item selection in Info::Profile::Members.\");\n}\n\nvoid InnerWidget::peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) {\n\tUnexpected(\"Item selection in Info::Profile::Members.\");\n}\n\nvoid InnerWidget::peerListFinishSelectedRowsBunch() {\n}\n\nvoid InnerWidget::peerListSetDescription(\n\t\tobject_ptr<Ui::FlatLabel> description) {\n\tdescription.destroy();\n}\n\nstd::shared_ptr<Main::SessionShow> InnerWidget::peerListUiShow() {\n\treturn _show;\n}\n\nMemento::Memento(not_null<PeerData*> peer)\n: ContentMemento(peer, nullptr, nullptr, PeerId()) {\n}\n\nSection Memento::section() const {\n\treturn Section(Section::Type::SimilarPeers);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(parent, controller, peer());\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nvoid Memento::setListState(std::unique_ptr<PeerListState> state) {\n\t_listState = std::move(state);\n}\n\nstd::unique_ptr<PeerListState> Memento::listState() {\n\treturn std::move(_listState);\n}\n\nMemento::~Memento() = default;\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller,\n\tnot_null<PeerData*> peer)\n: ContentWidget(parent, controller) {\n\t_inner = setInnerWidget(object_ptr<InnerWidget>(\n\t\tthis,\n\t\tcontroller,\n\t\tpeer));\n}\n\nrpl::producer<QString> Widget::title() {\n\treturn peer()->isBroadcast()\n\t\t? tr::lng_similar_channels_title()\n\t\t: tr::lng_similar_bots_title();\n}\n\nnot_null<PeerData*> Widget::peer() const {\n\treturn _inner->peer();\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\tif (!controller()->validateMementoPeer(memento)) {\n\t\treturn false;\n\t}\n\tif (auto similarMemento = dynamic_cast<Memento*>(memento.get())) {\n\t\tif (similarMemento->peer() == peer()) {\n\t\t\trestoreState(similarMemento);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(peer());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\n} // namespace Info::SimilarPeers\n"
  },
  {
    "path": "Telegram/SourceFiles/info/similar_peers/info_similar_peers_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n\nclass PeerData;\nstruct PeerListState;\n\nnamespace Info::SimilarPeers {\n\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\texplicit Memento(not_null<PeerData*> peer);\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\tvoid setListState(std::unique_ptr<PeerListState> state);\n\tstd::unique_ptr<PeerListState> listState();\n\n\t~Memento();\n\nprivate:\n\tstd::unique_ptr<PeerListState> _listState;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<PeerData*> peer);\n\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\n\trpl::producer<QString> title() override;\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tInnerWidget *_inner = nullptr;\n\n};\n\n} // namespace Info::SimilarPeers\n"
  },
  {
    "path": "Telegram/SourceFiles/info/statistics/info_statistics_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_statistics.h\"\n\nnamespace Info::Statistics {\n\nstruct SavedState final {\n\tData::AnyStatistics stats;\n\tbase::flat_map<Data::RecentPostId, QImage> recentPostPreviews;\n\tData::PublicForwardsSlice publicForwardsFirstSlice;\n\tint recentPostsExpanded = 0;\n};\n\n} // namespace Info::Statistics\n"
  },
  {
    "path": "Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/statistics/info_statistics_inner_widget.h\"\n\n#include \"api/api_statistics.h\"\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"base/event_filter.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_story.h\"\n#include \"history/history_item.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"info/statistics/info_statistics_list_controllers.h\"\n#include \"info/statistics/info_statistics_recent_message.h\"\n#include \"info/statistics/info_statistics_widget.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\" // CreateLottieIcon.\n#include \"statistics/chart_widget.h\"\n#include \"statistics/statistics_common.h\"\n#include \"statistics/statistics_format_values.h\"\n#include \"statistics/widgets/chart_header_widget.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/rect.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_statistics.h\"\n\nnamespace Info::Statistics {\nnamespace {\n\nstruct Descriptor final {\n\tnot_null<PeerData*> peer;\n\tnot_null<Api::Statistics*> api;\n\tnot_null<QWidget*> toastParent;\n};\n\nvoid AddContextMenu(\n\t\tnot_null<Ui::RpWidget*> button,\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto fullId = item->fullId();\n\tconst auto contextMenu = button->lifetime()\n\t\t.make_state<base::unique_qptr<Ui::PopupMenu>>();\n\tconst auto showMenu = [=] {\n\t\t*contextMenu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tbutton,\n\t\t\tst::popupMenuWithIcons);\n\t\tconst auto go = [=] {\n\t\t\tconst auto &session = controller->parentController();\n\t\t\tif (const auto item = session->session().data().message(fullId)) {\n\t\t\t\tsession->showMessage(item);\n\t\t\t}\n\t\t};\n\t\tcontextMenu->get()->addAction(\n\t\t\ttr::lng_context_to_msg(tr::now),\n\t\t\tcrl::guard(controller, go),\n\t\t\t&st::menuIconShowInChat);\n\t\tcontextMenu->get()->popup(QCursor::pos());\n\t};\n\n\tbase::install_event_filter(button, [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::ContextMenu) {\n\t\t\tshowMenu();\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n}\n\nvoid ProcessZoom(\n\t\tconst Descriptor &d,\n\t\tnot_null<Statistic::ChartWidget*> widget,\n\t\tconst QString &zoomToken,\n\t\tStatistic::ChartViewType type) {\n\tif (zoomToken.isEmpty()) {\n\t\treturn;\n\t}\n\twidget->zoomRequests(\n\t) | rpl::on_next([=](float64 x) {\n\t\td.api->requestZoom(\n\t\t\tzoomToken,\n\t\t\tx\n\t\t) | rpl::on_next_error_done([=](\n\t\t\t\tconst Data::StatisticalGraph &graph) {\n\t\t\tif (graph.chart) {\n\t\t\t\twidget->setZoomedChartData(graph.chart, x, type);\n\t\t\t} else if (!graph.error.isEmpty()) {\n\t\t\t\tUi::Toast::Show(d.toastParent, graph.error);\n\t\t\t}\n\t\t}, [=](const QString &error) {\n\t\t}, [=] {\n\t\t}, widget->lifetime());\n\t}, widget->lifetime());\n}\n\nvoid FillStatistic(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tconst Descriptor &descriptor,\n\t\tData::AnyStatistics &stats,\n\t\tFn<void()> done) {\n\tusing Type = Statistic::ChartViewType;\n\tconst auto &padding = st::statisticsChartEntryPadding;\n\tconst auto &m = st::statisticsLayerMargins;\n\tconst auto addSkip = [&](not_null<Ui::VerticalLayout*> c) {\n\t\tUi::AddSkip(c, padding.bottom());\n\t\tUi::AddDivider(c);\n\t\tUi::AddSkip(c, padding.top());\n\t};\n\tstruct State final {\n\t\tFn<void()> done;\n\t\tint pendingCount = 0;\n\t};\n\tconst auto state = content->lifetime().make_state<State>(\n\t\tState{ std::move(done) });\n\n\tconst auto singlePendingDone = [=] {\n\t\tstate->pendingCount--;\n\t\tif (!state->pendingCount && state->done) {\n\t\t\tbase::take(state->done)();\n\t\t}\n\t};\n\n\tconst auto addChart = [&](\n\t\t\tData::StatisticalGraph &graphData,\n\t\t\trpl::producer<QString> &&title,\n\t\t\tStatistic::ChartViewType type) {\n\t\tif (graphData.chart) {\n\t\t\tconst auto widget = content->add(\n\t\t\t\tobject_ptr<Statistic::ChartWidget>(content),\n\t\t\t\tm);\n\n\t\t\twidget->setChartData(graphData.chart, type);\n\t\t\tProcessZoom(descriptor, widget, graphData.zoomToken, type);\n\t\t\twidget->setTitle(std::move(title));\n\n\t\t\taddSkip(content);\n\t\t} else if (!graphData.zoomToken.isEmpty()) {\n\t\t\tstate->pendingCount++;\n\t\t\tconst auto wrap = content->add(\n\t\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\t\tcontent,\n\t\t\t\t\tobject_ptr<Ui::VerticalLayout>(content)));\n\t\t\twrap->toggle(false, anim::type::instant);\n\t\t\tconst auto widget = wrap->entity()->add(\n\t\t\t\tobject_ptr<Statistic::ChartWidget>(content),\n\t\t\t\tm);\n\n\t\t\tdescriptor.api->requestZoom(\n\t\t\t\tgraphData.zoomToken,\n\t\t\t\t0\n\t\t\t) | rpl::on_next_error_done([=, graphPtr = &graphData](\n\t\t\t\t\tconst Data::StatisticalGraph &graph) mutable {\n\t\t\t\t{\n\t\t\t\t\t// Save the loaded async data to cache.\n\t\t\t\t\t// Guarded by content->lifetime().\n\t\t\t\t\t*graphPtr = graph;\n\t\t\t\t}\n\n\t\t\t\tif (graph.chart) {\n\t\t\t\t\twidget->setChartData(graph.chart, type);\n\t\t\t\t\twrap->toggle(true, anim::type::instant);\n\t\t\t\t\tProcessZoom(descriptor, widget, graph.zoomToken, type);\n\t\t\t\t\twidget->setTitle(rpl::duplicate(title));\n\t\t\t\t} else if (!graph.error.isEmpty()) {\n\t\t\t\t}\n\t\t\t}, [=](const QString &error) {\n\t\t\t\tsinglePendingDone();\n\t\t\t}, [=] {\n\t\t\t\tsinglePendingDone();\n\t\t\t}, content->lifetime());\n\n\t\t\taddSkip(wrap->entity());\n\t\t}\n\t};\n\taddSkip(content);\n\tif (stats.channel) {\n\t\taddChart(\n\t\t\tstats.channel.memberCountGraph,\n\t\t\ttr::lng_chart_title_member_count(),\n\t\t\tType::Linear);\n\t\taddChart(\n\t\t\tstats.channel.joinGraph,\n\t\t\ttr::lng_chart_title_join(),\n\t\t\tType::Linear);\n\t\taddChart(\n\t\t\tstats.channel.muteGraph,\n\t\t\ttr::lng_chart_title_mute(),\n\t\t\tType::Linear);\n\t\taddChart(\n\t\t\tstats.channel.viewCountByHourGraph,\n\t\t\ttr::lng_chart_title_view_count_by_hour(),\n\t\t\tType::Linear);\n\t\taddChart(\n\t\t\tstats.channel.viewCountBySourceGraph,\n\t\t\ttr::lng_chart_title_view_count_by_source(),\n\t\t\tType::StackBar);\n\t\taddChart(\n\t\t\tstats.channel.joinBySourceGraph,\n\t\t\ttr::lng_chart_title_join_by_source(),\n\t\t\tType::StackBar);\n\t\taddChart(\n\t\t\tstats.channel.languageGraph,\n\t\t\ttr::lng_chart_title_language(),\n\t\t\tType::StackLinear);\n\t\taddChart(\n\t\t\tstats.channel.messageInteractionGraph,\n\t\t\ttr::lng_chart_title_message_interaction(),\n\t\t\tType::DoubleLinear);\n\t\taddChart(\n\t\t\tstats.channel.instantViewInteractionGraph,\n\t\t\ttr::lng_chart_title_instant_view_interaction(),\n\t\t\tType::DoubleLinear);\n\t\taddChart(\n\t\t\tstats.channel.reactionsByEmotionGraph,\n\t\t\ttr::lng_chart_title_reactions_by_emotion(),\n\t\t\tType::Bar);\n\t\taddChart(\n\t\t\tstats.channel.storyInteractionsGraph,\n\t\t\ttr::lng_chart_title_story_interactions(),\n\t\t\tType::DoubleLinear);\n\t\taddChart(\n\t\t\tstats.channel.storyReactionsByEmotionGraph,\n\t\t\ttr::lng_chart_title_story_reactions_by_emotion(),\n\t\t\tType::Bar);\n\t} else if (stats.supergroup) {\n\t\taddChart(\n\t\t\tstats.supergroup.memberCountGraph,\n\t\t\ttr::lng_chart_title_member_count(),\n\t\t\tType::Linear);\n\t\taddChart(\n\t\t\tstats.supergroup.joinGraph,\n\t\t\ttr::lng_chart_title_group_join(),\n\t\t\tType::Linear);\n\t\taddChart(\n\t\t\tstats.supergroup.joinBySourceGraph,\n\t\t\ttr::lng_chart_title_group_join_by_source(),\n\t\t\tType::StackBar);\n\t\taddChart(\n\t\t\tstats.supergroup.languageGraph,\n\t\t\ttr::lng_chart_title_group_language(),\n\t\t\tType::StackLinear);\n\t\taddChart(\n\t\t\tstats.supergroup.messageContentGraph,\n\t\t\ttr::lng_chart_title_group_message_content(),\n\t\t\tType::StackBar);\n\t\taddChart(\n\t\t\tstats.supergroup.actionGraph,\n\t\t\ttr::lng_chart_title_group_action(),\n\t\t\tType::DoubleLinear);\n\t\taddChart(\n\t\t\tstats.supergroup.dayGraph,\n\t\t\ttr::lng_chart_title_group_day(),\n\t\t\tType::Linear);\n\t\taddChart(\n\t\t\tstats.supergroup.weekGraph,\n\t\t\ttr::lng_chart_title_group_week(),\n\t\t\tType::StackLinear);\n\t} else {\n\t\tauto &messageOrStory = stats.message\n\t\t\t? stats.message\n\t\t\t: stats.story;\n\t\tif (messageOrStory) {\n\t\t\taddChart(\n\t\t\t\tmessageOrStory.messageInteractionGraph,\n\t\t\t\ttr::lng_chart_title_message_interaction(),\n\t\t\t\tType::DoubleLinear);\n\t\t\taddChart(\n\t\t\t\tmessageOrStory.reactionsByEmotionGraph,\n\t\t\t\ttr::lng_chart_title_reactions_by_emotion(),\n\t\t\t\tType::Bar);\n\t\t}\n\t}\n\tStatistic::FixCacheForHighDPIChartWidget(content);\n\tif (!state->pendingCount) {\n\t\t++state->pendingCount;\n\t\tsinglePendingDone();\n\t}\n}\n\nvoid AddHeader(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\ttr::phrase<> text,\n\t\tconst Data::AnyStatistics &stats) {\n\tconst auto startDate = stats.channel\n\t\t? stats.channel.startDate\n\t\t: stats.supergroup.startDate;\n\tconst auto endDate = stats.channel\n\t\t? stats.channel.endDate\n\t\t: stats.supergroup.endDate;\n\tconst auto header = content->add(\n\t\tobject_ptr<Statistic::Header>(content),\n\t\tst::statisticsLayerMargins + st::statisticsChartHeaderPadding);\n\theader->resizeToWidth(header->width());\n\theader->setTitle(text(tr::now));\n\tif (!endDate || !startDate) {\n\t\theader->setSubTitle({});\n\t\treturn;\n\t}\n\theader->setSubTitle(Statistic::LangDayMonthYear(startDate)\n\t\t+ ' '\n\t\t+ QChar(8212)\n\t\t+ ' '\n\t\t+ Statistic::LangDayMonthYear(endDate));\n}\n\nvoid FillOverview(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tconst Data::AnyStatistics &stats,\n\t\tbool isChannelStoryStats) {\n\tusing Value = Data::StatisticalValue;\n\n\tconst auto &channel = stats.channel;\n\tconst auto &supergroup = stats.supergroup;\n\n\tif (!isChannelStoryStats) {\n\t\tUi::AddSkip(content, st::statisticsLayerOverviewMargins.top());\n\t\tAddHeader(content, tr::lng_stats_overview_title, stats);\n\t\tUi::AddSkip(content);\n\t}\n\n\tstruct Second final {\n\t\tQColor color;\n\t\tQString text;\n\t};\n\n\tconst auto parseSecond = [&](const Value &v) -> Second {\n\t\tconst auto diff = v.value - v.previousValue;\n\t\tif (!diff || !v.previousValue) {\n\t\t\treturn {};\n\t\t}\n\t\tconstexpr auto kTooMuchDiff = int(1'000'000);\n\t\tconst auto diffAbs = std::abs(diff);\n\t\tconst auto diffText = diffAbs > kTooMuchDiff\n\t\t\t? Lang::FormatCountToShort(std::abs(diff)).string\n\t\t\t: QString::number(diffAbs);\n\t\tconst auto percentage = std::abs(v.growthRatePercentage);\n\t\tconst auto precision = (percentage == int(percentage)) ? 0 : 1;\n\t\treturn {\n\t\t\t(diff < 0 ? st::menuIconAttentionColor : st::settingsIconBg2)->c,\n\t\t\tQString(\"%1%2 (%3%)\")\n\t\t\t\t.arg((diff < 0) ? QChar(0x2212) : QChar(0x002B))\n\t\t\t\t.arg(diffText)\n\t\t\t\t.arg(QString::number(percentage, 'f', precision))\n\t\t};\n\t};\n\n\tconst auto diffBetweenHeaders = 0\n\t\t+ st::statisticsOverviewValue.style.font->height\n\t\t- st::statisticsHeaderTitleTextStyle.font->height;\n\n\tconst auto container = content->add(\n\t\tobject_ptr<Ui::RpWidget>(content),\n\t\tst::statisticsLayerMargins);\n\n\tconst auto addPrimary = [&](const Value &v) {\n\t\treturn Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\t(v.value >= 0)\n\t\t\t\t? Lang::FormatCountToShort(v.value).string\n\t\t\t\t: QString(),\n\t\t\tst::statisticsOverviewValue);\n\t};\n\tconst auto addSub = [&](\n\t\t\tnot_null<Ui::RpWidget*> primary,\n\t\t\tconst Value &v,\n\t\t\ttr::phrase<> text) {\n\t\tconst auto data = parseSecond(v);\n\t\tconst auto second = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\tdata.text,\n\t\t\tst::statisticsOverviewSecondValue);\n\t\tsecond->setTextColorOverride(data.color);\n\t\tconst auto sub = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttext(),\n\t\t\tst::statisticsOverviewSubtext);\n\t\tsub->setTextColorOverride(st::windowSubTextFg->c);\n\n\t\tprimary->geometryValue(\n\t\t) | rpl::on_next([=](const QRect &g) {\n\t\t\tconst auto &padding = st::statisticsOverviewSecondValuePadding;\n\t\t\tsecond->moveToLeft(\n\t\t\t\trect::right(g) + padding.left(),\n\t\t\t\tg.y() + padding.top());\n\t\t\tsub->moveToLeft(\n\t\t\t\tg.x(),\n\t\t\t\tst::statisticsChartHeaderHeight\n\t\t\t\t\t- st::statisticsOverviewSubtext.style.font->height\n\t\t\t\t\t+ g.y()\n\t\t\t\t\t+ diffBetweenHeaders);\n\t\t\tif (container->height() < rect::bottom(sub)) {\n\t\t\t\tcontainer->resize(container->width(), rect::bottom(sub));\n\t\t\t}\n\t\t}, primary->lifetime());\n\t};\n\n\tconst auto isChannel = (!!channel);\n\tconst auto &messageOrStory = stats.message ? stats.message : stats.story;\n\tconst auto isMessage = (!!messageOrStory);\n\n\tconst auto hasPostReactions = isChannel\n\t\t&& (channel.meanReactionCount.value\n\t\t\t|| channel.meanReactionCount.previousValue);\n\n\tconst auto topLeftLabel = (isChannelStoryStats && isChannel)\n\t\t? addPrimary(channel.meanShareCount)\n\t\t: isChannel\n\t\t? addPrimary(channel.memberCount)\n\t\t: isMessage\n\t\t? addPrimary({ .value = float64(messageOrStory.views) })\n\t\t: addPrimary(supergroup.memberCount);\n\tconst auto topRightLabel = (isChannelStoryStats && isChannel)\n\t\t? addPrimary(channel.meanStoryShareCount)\n\t\t: isChannel\n\t\t? Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\tQString(\"%1%\").arg(0.01\n\t\t\t\t* std::round(channel.enabledNotificationsPercentage * 100.)),\n\t\t\tst::statisticsOverviewValue)\n\t\t: isMessage\n\t\t? addPrimary({ .value = float64(messageOrStory.publicForwards) })\n\t\t: addPrimary(supergroup.messageCount);\n\tconst auto bottomLeftLabel = (isChannelStoryStats && isChannel)\n\t\t? addPrimary(hasPostReactions\n\t\t\t? channel.meanReactionCount\n\t\t\t: channel.meanStoryReactionCount)\n\t\t: isChannel\n\t\t? addPrimary(channel.meanViewCount)\n\t\t: isMessage\n\t\t? addPrimary({ .value = float64(messageOrStory.reactions) })\n\t\t: addPrimary(supergroup.viewerCount);\n\tconst auto bottomRightLabel = (isChannelStoryStats && isChannel)\n\t\t? addPrimary(!hasPostReactions\n\t\t\t? Value{ .value = -1 }\n\t\t\t: channel.meanStoryReactionCount)\n\t\t: isChannel\n\t\t? addPrimary(channel.meanStoryViewCount)\n\t\t: isMessage\n\t\t? addPrimary({ .value = float64(messageOrStory.privateForwards) })\n\t\t: addPrimary(supergroup.senderCount);\n\tif (isChannelStoryStats && isChannel) {\n\t\taddSub(\n\t\t\ttopLeftLabel,\n\t\t\tchannel.meanShareCount,\n\t\t\ttr::lng_stats_overview_mean_share_count);\n\t\taddSub(\n\t\t\ttopRightLabel,\n\t\t\tchannel.meanStoryShareCount,\n\t\t\ttr::lng_stats_overview_mean_story_share_count);\n\t\taddSub(\n\t\t\tbottomLeftLabel,\n\t\t\thasPostReactions\n\t\t\t\t? channel.meanReactionCount\n\t\t\t\t: channel.meanStoryReactionCount,\n\t\t\thasPostReactions\n\t\t\t\t? tr::lng_stats_overview_mean_reactions_count\n\t\t\t\t: tr::lng_stats_overview_mean_story_reactions_count);\n\t\tif (hasPostReactions) {\n\t\t\taddSub(\n\t\t\t\tbottomRightLabel,\n\t\t\t\tchannel.meanStoryReactionCount,\n\t\t\t\ttr::lng_stats_overview_mean_story_reactions_count);\n\t\t}\n\t} else if (const auto &s = channel) {\n\t\taddSub(\n\t\t\ttopLeftLabel,\n\t\t\ts.memberCount,\n\t\t\ttr::lng_stats_overview_member_count);\n\t\taddSub(\n\t\t\ttopRightLabel,\n\t\t\t{},\n\t\t\ttr::lng_stats_overview_enabled_notifications);\n\t\taddSub(\n\t\t\tbottomLeftLabel,\n\t\t\ts.meanViewCount,\n\t\t\ttr::lng_stats_overview_mean_view_count);\n\t\taddSub(\n\t\t\tbottomRightLabel,\n\t\t\ts.meanStoryViewCount,\n\t\t\ttr::lng_stats_overview_mean_story_view_count);\n\t} else if (const auto &s = supergroup) {\n\t\taddSub(\n\t\t\ttopLeftLabel,\n\t\t\ts.memberCount,\n\t\t\ttr::lng_manage_peer_members);\n\t\taddSub(\n\t\t\ttopRightLabel,\n\t\t\ts.messageCount,\n\t\t\ttr::lng_stats_overview_messages);\n\t\taddSub(\n\t\t\tbottomLeftLabel,\n\t\t\ts.viewerCount,\n\t\t\ttr::lng_stats_overview_group_mean_view_count);\n\t\taddSub(\n\t\t\tbottomRightLabel,\n\t\t\ts.senderCount,\n\t\t\ttr::lng_stats_overview_group_mean_post_count);\n\t} else if (const auto &s = messageOrStory) {\n\t\tif (s.views >= 0) {\n\t\t\taddSub(\n\t\t\t\ttopLeftLabel,\n\t\t\t\t{},\n\t\t\t\ttr::lng_stats_overview_message_views);\n\t\t}\n\t\tif (s.publicForwards >= 0) {\n\t\t\taddSub(\n\t\t\t\ttopRightLabel,\n\t\t\t\t{},\n\t\t\t\ttr::lng_stats_overview_message_public_shares);\n\t\t}\n\t\tif (s.reactions >= 0) {\n\t\t\taddSub(\n\t\t\t\tbottomLeftLabel,\n\t\t\t\t{},\n\t\t\t\ttr::lng_manage_peer_reactions);\n\t\t}\n\t\tif (s.privateForwards >= 0) {\n\t\t\taddSub(\n\t\t\t\tbottomRightLabel,\n\t\t\t\t{},\n\t\t\t\ttr::lng_stats_overview_message_private_shares);\n\t\t}\n\t}\n\tcontainer->showChildren();\n\tcontainer->sizeValue() | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tconst auto halfWidth = s.width() / 2;\n\t\t{\n\t\t\tconst auto &p = st::statisticsOverviewValuePadding;\n\t\t\ttopLeftLabel->moveToLeft(p.left(), p.top());\n\t\t}\n\t\ttopRightLabel->moveToLeft(\n\t\t\ttopLeftLabel->x() + halfWidth + st::statisticsOverviewRightSkip,\n\t\t\ttopLeftLabel->y());\n\t\tbottomLeftLabel->moveToLeft(\n\t\t\ttopLeftLabel->x(),\n\t\t\ttopLeftLabel->y() + st::statisticsOverviewMidSkip);\n\t\tbottomRightLabel->moveToLeft(\n\t\t\ttopRightLabel->x(),\n\t\t\tbottomLeftLabel->y());\n\t}, container->lifetime());\n\tUi::AddSkip(content, st::statisticsLayerOverviewMargins.bottom());\n}\n\n} // namespace\n\nvoid FillLoading(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tLoadingType type,\n\t\trpl::producer<bool> toggleOn,\n\t\trpl::producer<> showFinished) {\n\tconst auto emptyWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\temptyWrap->toggleOn(std::move(toggleOn), anim::type::instant);\n\n\tconst auto content = emptyWrap->entity();\n\tconst auto iconName = (type == LoadingType::Boosts)\n\t\t? u\"stats_boosts\"_q\n\t\t: (type == LoadingType::Earn)\n\t\t? u\"stats_earn\"_q\n\t\t: u\"stats\"_q;\n\tauto icon = ::Settings::CreateLottieIcon(\n\t\tcontent,\n\t\t{ .name = iconName, .sizeOverride = st::normalBoxLottieSize },\n\t\tst::settingsBlockedListIconPadding);\n\n\t(\n\t\tstd::move(showFinished) | rpl::take(1)\n\t) | rpl::on_next([animate = std::move(icon.animate)] {\n\t\tanimate(anim::repeat::loop);\n\t}, icon.widget->lifetime());\n\tcontent->add(std::move(icon.widget));\n\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\t(type == LoadingType::Boosts)\n\t\t\t\t? tr::lng_stats_boosts_loading()\n\t\t\t\t: (type == LoadingType::Earn)\n\t\t\t\t? tr::lng_stats_earn_loading()\n\t\t\t\t: tr::lng_stats_loading(),\n\t\t\tst::changePhoneTitle),\n\t\tst::changePhoneTitlePadding + st::boxRowPadding,\n\t\tstyle::al_top);\n\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\t(type == LoadingType::Boosts)\n\t\t\t\t? tr::lng_stats_boosts_loading_subtext()\n\t\t\t\t: (type == LoadingType::Earn)\n\t\t\t\t? tr::lng_stats_earn_loading_subtext()\n\t\t\t\t: tr::lng_stats_loading_subtext(),\n\t\t\tst::statisticsLoadingSubtext),\n\t\tst::changePhoneDescriptionPadding + st::boxRowPadding,\n\t\tstyle::al_top\n\t)->setTryMakeSimilarLines(true);\n\n\tUi::AddSkip(content, st::settingsBlockedListIconPadding.top());\n}\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller,\n\tnot_null<PeerData*> peer,\n\tFullMsgId contextId,\n\tFullStoryId storyId)\n: VerticalLayout(parent)\n, _controller(controller)\n, _peer(peer)\n, _contextId(contextId)\n, _storyId(storyId) {\n}\n\nvoid InnerWidget::load() {\n\tconst auto inner = this;\n\n\tconst auto descriptor = Descriptor{\n\t\t_peer,\n\t\tlifetime().make_state<Api::Statistics>(_peer->asChannel()),\n\t\t_controller->uiShow()->toastParent(),\n\t};\n\n\tFillLoading(\n\t\tinner,\n\t\tInfo::Statistics::LoadingType::Statistic,\n\t\t_loaded.events_starting_with(false) | rpl::map(!rpl::mappers::_1),\n\t\t_showFinished.events());\n\n\t_showFinished.events(\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\tif (!_contextId && !_storyId) {\n\t\t\tdescriptor.api->request(\n\t\t\t) | rpl::on_done([=] {\n\t\t\t\t_state.stats = Data::AnyStatistics{\n\t\t\t\t\tdescriptor.api->channelStats(),\n\t\t\t\t\tdescriptor.api->supergroupStats(),\n\t\t\t\t};\n\t\t\t\tfill();\n\n\t\t\t}, lifetime());\n\t\t} else {\n\t\t\tconst auto lifetimeApi = lifetime().make_state<rpl::lifetime>();\n\t\t\tconst auto api = _storyId\n\t\t\t\t? lifetimeApi->make_state<Api::MessageStatistics>(\n\t\t\t\t\tdescriptor.peer->asChannel(),\n\t\t\t\t\t_storyId)\n\t\t\t\t: lifetimeApi->make_state<Api::MessageStatistics>(\n\t\t\t\t\tdescriptor.peer->asChannel(),\n\t\t\t\t\t_contextId);\n\n\t\t\tapi->request([=](const Data::StoryStatistics &data) {\n\t\t\t\t_state.stats = Data::AnyStatistics{\n\t\t\t\t\t.message = _contextId ? data : Data::StoryStatistics(),\n\t\t\t\t\t.story = _storyId ? data : Data::StoryStatistics(),\n\t\t\t\t};\n\t\t\t\tif (_contextId || _storyId) {\n\t\t\t\t\t_state.publicForwardsFirstSlice = api->firstSlice();\n\t\t\t\t}\n\t\t\t\tfill();\n\n\t\t\t\tlifetimeApi->destroy();\n\t\t\t});\n\t\t}\n\t}, lifetime());\n}\n\nvoid InnerWidget::fill() {\n\tconst auto wrap = this->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::VerticalLayout>(this)));\n\twrap->toggle(false, anim::type::instant);\n\tconst auto inner = wrap->entity();\n\tconst auto descriptor = Descriptor{\n\t\t_peer,\n\t\tlifetime().make_state<Api::Statistics>(_peer->asChannel()),\n\t\t_controller->uiShow()->toastParent(),\n\t};\n\tconst auto finishLoading = [=] {\n\t\t_loaded.fire(true);\n\t\twrap->toggle(true, anim::type::instant);\n\t\tthis->resizeToWidth(width());\n\t\tthis->showChildren();\n\t};\n\tif (_state.stats.message) {\n\t\tif (const auto i = _peer->owner().message(_contextId)) {\n\t\t\tUi::AddSkip(inner);\n\t\t\tconst auto preview = inner->add(\n\t\t\t\tobject_ptr<MessagePreview>(inner, i, QImage()));\n\t\t\tAddContextMenu(preview, _controller, i);\n\t\t\tUi::AddSkip(inner);\n\t\t\tUi::AddDivider(inner);\n\t\t}\n\t} else if (_state.stats.story) {\n\t\tif (const auto story = _peer->owner().stories().lookup(_storyId)) {\n\t\t\tUi::AddSkip(inner);\n\t\t\tconst auto preview = inner->add(\n\t\t\t\tobject_ptr<MessagePreview>(inner, *story, QImage()));\n\t\t\tpreview->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\tUi::AddSkip(inner);\n\t\t\tUi::AddDivider(inner);\n\t\t}\n\t}\n\tFillOverview(inner, _state.stats, false);\n\tif (_state.stats.channel) {\n\t\tFillOverview(inner, _state.stats, true);\n\t}\n\tFillStatistic(inner, descriptor, _state.stats, finishLoading);\n\tconst auto &channel = _state.stats.channel;\n\tconst auto &supergroup = _state.stats.supergroup;\n\tif (channel) {\n\t\tfillRecentPosts(inner);\n\t} else if (supergroup) {\n\t\tconst auto showPeerInfo = [=](not_null<PeerData*> peer) {\n\t\t\t_showRequests.fire({ .info = peer->id });\n\t\t};\n\t\tconst auto addSkip = [&](not_null<Ui::VerticalLayout*> c) {\n\t\t\tUi::AddSkip(c);\n\t\t\tUi::AddDivider(c);\n\t\t\tUi::AddSkip(c);\n\t\t\tUi::AddSkip(c);\n\t\t};\n\t\tif (!supergroup.topSenders.empty()) {\n\t\t\tAddMembersList(\n\t\t\t\t{ .topSenders = supergroup.topSenders },\n\t\t\t\tinner,\n\t\t\t\tshowPeerInfo,\n\t\t\t\tdescriptor.peer,\n\t\t\t\ttr::lng_stats_members_title());\n\t\t}\n\t\tif (!supergroup.topAdministrators.empty()) {\n\t\t\taddSkip(inner);\n\t\t\tAddMembersList(\n\t\t\t\t{ .topAdministrators\n\t\t\t\t\t= supergroup.topAdministrators },\n\t\t\t\tinner,\n\t\t\t\tshowPeerInfo,\n\t\t\t\tdescriptor.peer,\n\t\t\t\ttr::lng_stats_admins_title());\n\t\t}\n\t\tif (!supergroup.topInviters.empty()) {\n\t\t\taddSkip(inner);\n\t\t\tAddMembersList(\n\t\t\t\t{ .topInviters = supergroup.topInviters },\n\t\t\t\tinner,\n\t\t\t\tshowPeerInfo,\n\t\t\t\tdescriptor.peer,\n\t\t\t\ttr::lng_stats_inviters_title());\n\t\t}\n\t} else if (_state.stats.message || _state.stats.story) {\n\t\tusing namespace Data;\n\t\tAddPublicForwards(\n\t\t\t_state.publicForwardsFirstSlice,\n\t\t\tinner,\n\t\t\t[=](RecentPostId id) {\n\t\t\t\t_showRequests.fire({\n\t\t\t\t\t.info = (!id.messageId && !id.storyId)\n\t\t\t\t\t\t? id.messageId.peer\n\t\t\t\t\t\t: PeerId(0),\n\t\t\t\t\t.history = id.messageId,\n\t\t\t\t\t.story = id.storyId,\n\t\t\t\t});\n\t\t\t},\n\t\t\tdescriptor.peer,\n\t\t\tRecentPostId{ .messageId = _contextId, .storyId = _storyId });\n\t}\n}\n\nvoid InnerWidget::fillRecentPosts(not_null<Ui::VerticalLayout*> container) {\n\tconst auto &stats = _state.stats.channel;\n\tif (!stats || stats.recentMessageInteractions.empty()) {\n\t\treturn;\n\t}\n\t_messagePreviews.reserve(stats.recentMessageInteractions.size());\n\n\tconst auto wrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto content = wrap->entity();\n\tAddHeader(content, tr::lng_stats_recent_messages_title, { stats, {} });\n\tUi::AddSkip(content);\n\n\tconst auto addMessage = [=](\n\t\t\tnot_null<Ui::VerticalLayout*> messageWrap,\n\t\t\tHistoryItem *maybeItem,\n\t\t\tData::Story *maybeStory,\n\t\t\tconst Data::StatisticsMessageInteractionInfo &info) {\n\t\tconst auto button = messageWrap->add(\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tmessageWrap,\n\t\t\t\trpl::never<QString>(),\n\t\t\t\tst::statisticsRecentPostButton));\n\t\tconst auto fullRecentId = Data::RecentPostId{\n\t\t\t.messageId = maybeItem ? maybeItem->fullId() : FullMsgId(),\n\t\t\t.storyId = maybeStory ? maybeStory->fullId() : FullStoryId(),\n\t\t};\n\t\tauto it = _state.recentPostPreviews.find(fullRecentId);\n\t\tauto cachedPreview = (it != end(_state.recentPostPreviews))\n\t\t\t? base::take(it->second)\n\t\t\t: QImage();\n\t\tconst auto raw = maybeItem\n\t\t\t? Ui::CreateChild<MessagePreview>(\n\t\t\t\tbutton,\n\t\t\t\tmaybeItem,\n\t\t\t\tstd::move(cachedPreview))\n\t\t\t: Ui::CreateChild<MessagePreview>(\n\t\t\t\tbutton,\n\t\t\t\tmaybeStory,\n\t\t\t\tstd::move(cachedPreview));\n\t\traw->setInfo(\n\t\t\tinfo.viewsCount,\n\t\t\tinfo.forwardsCount,\n\t\t\tinfo.reactionsCount);\n\n\t\tif (maybeItem) {\n\t\t\tAddContextMenu(button, _controller, maybeItem);\n\t\t}\n\n\t\t_messagePreviews.push_back(raw);\n\t\traw->show();\n\t\tbutton->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\tif (!s.isNull()) {\n\t\t\t\traw->setGeometry(Rect(s)\n\t\t\t\t\t- st::statisticsRecentPostButton.padding);\n\t\t\t}\n\t\t}, raw->lifetime());\n\t\tbutton->setClickedCallback([=] {\n\t\t\t_showRequests.fire({\n\t\t\t\t.messageStatistic = fullRecentId.messageId,\n\t\t\t\t.storyStatistic = fullRecentId.storyId,\n\t\t\t});\n\t\t});\n\t\tUi::AddSkip(messageWrap);\n\t\tif (!wrap->toggled()) {\n\t\t\twrap->toggle(true, anim::type::normal);\n\t\t}\n\t};\n\n\tconst auto buttonWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_stories_show_more())));\n\n\tconstexpr auto kFirstPage = int(10);\n\tconstexpr auto kPerPage = int(30);\n\tconst auto max = int(stats.recentMessageInteractions.size());\n\tif (_state.recentPostsExpanded) {\n\t\t_state.recentPostsExpanded = std::max(\n\t\t\t_state.recentPostsExpanded - kPerPage,\n\t\t\t0);\n\t}\n\tconst auto showMore = [=] {\n\t\tconst auto from = _state.recentPostsExpanded;\n\t\t_state.recentPostsExpanded = std::min(\n\t\t\tmax,\n\t\t\t_state.recentPostsExpanded\n\t\t\t\t? (_state.recentPostsExpanded + kPerPage)\n\t\t\t\t: kFirstPage);\n\t\tif (_state.recentPostsExpanded == max) {\n\t\t\tbuttonWrap->toggle(false, anim::type::instant);\n\t\t}\n\t\tfor (auto i = from; i < _state.recentPostsExpanded; i++) {\n\t\t\tconst auto &recent = stats.recentMessageInteractions[i];\n\t\t\tconst auto messageWrap = content->add(\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(content));\n\t\t\tauto &data = _peer->owner();\n\t\t\tif (recent.messageId) {\n\t\t\t\tconst auto fullId = FullMsgId(_peer->id, recent.messageId);\n\t\t\t\tif (const auto item = data.message(fullId)) {\n\t\t\t\t\taddMessage(messageWrap, item, nullptr, recent);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto callback = crl::guard(content, [=] {\n\t\t\t\t\tif (const auto item = _peer->owner().message(fullId)) {\n\t\t\t\t\t\taddMessage(messageWrap, item, nullptr, recent);\n\t\t\t\t\t\tcontent->resizeToWidth(content->width());\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\t_peer->session().api().requestMessageData(\n\t\t\t\t\t_peer,\n\t\t\t\t\tfullId.msg,\n\t\t\t\t\tcallback);\n\t\t\t} else if (recent.storyId) {\n\t\t\t\tconst auto fullId = FullStoryId{ _peer->id, recent.storyId };\n\t\t\t\tif (const auto story = data.stories().lookup(fullId)) {\n\t\t\t\t\taddMessage(messageWrap, nullptr, *story, recent);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcontainer->resizeToWidth(container->width());\n\t};\n\tconst auto delay = st::defaultRippleAnimation.hideDuration;\n\tbuttonWrap->entity()->setClickedCallback([=] {\n\t\tbase::call_delayed(delay, crl::guard(container, showMore));\n\t});\n\tshowMore();\n\tif (_messagePreviews.empty()) {\n\t\twrap->toggle(false, anim::type::instant);\n\t}\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\tfor (const auto &message : _messagePreviews) {\n\t\tmessage->saveState(_state);\n\t}\n\tmemento->setState(base::take(_state));\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\t_state = memento->state();\n\tif (_state.stats.channel\n\t\t|| _state.stats.supergroup\n\t\t|| _state.stats.message\n\t\t|| _state.stats.story) {\n\t\tfill();\n\t} else {\n\t\tload();\n\t}\n\tUi::RpWidget::resizeToWidth(width());\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nauto InnerWidget::showRequests() const -> rpl::producer<ShowRequest> {\n\treturn _showRequests.events();\n}\n\nvoid InnerWidget::showFinished() {\n\t_showFinished.fire({});\n}\n\n} // namespace Info::Statistics\n"
  },
  {
    "path": "Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"info/statistics/info_statistics_common.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/vertical_layout.h\"\n\nnamespace Info {\nclass Controller;\n} // namespace Info\n\nnamespace Info::Statistics {\n\nclass Memento;\nclass MessagePreview;\n\nenum class LoadingType {\n\tStatistic,\n\tBoosts,\n\tEarn,\n};\n\nvoid FillLoading(\n\tnot_null<Ui::VerticalLayout*> container,\n\tLoadingType type,\n\trpl::producer<bool> toggleOn,\n\trpl::producer<> showFinished);\n\nclass InnerWidget final : public Ui::VerticalLayout {\npublic:\n\tstruct ShowRequest final {\n\t\tPeerId info = PeerId(0);\n\t\tFullMsgId history;\n\t\tFullMsgId messageStatistic;\n\t\tFullStoryId storyStatistic;\n\t\tFullStoryId story;\n\t};\n\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tFullMsgId contextId,\n\t\tFullStoryId storyId);\n\n\t[[nodiscard]] rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\t[[nodiscard]] rpl::producer<ShowRequest> showRequests() const;\n\n\tvoid showFinished();\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\nprivate:\n\tvoid load();\n\tvoid fill();\n\tvoid fillRecentPosts(not_null<Ui::VerticalLayout*> container);\n\n\tnot_null<Controller*> _controller;\n\tnot_null<PeerData*> _peer;\n\tFullMsgId _contextId;\n\tFullStoryId _storyId;\n\n\tstd::vector<not_null<MessagePreview*>> _messagePreviews;\n\n\tSavedState _state;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\trpl::event_stream<ShowRequest> _showRequests;\n\trpl::event_stream<> _showFinished;\n\trpl::event_stream<bool> _loaded;\n\n};\n\n} // namespace Info::Statistics\n"
  },
  {
    "path": "Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/statistics/info_statistics_list_controllers.h\"\n\n#include \"api/api_credits.h\"\n#include \"api/api_statistics.h\"\n#include \"boxes/peers/replace_boost_box.h\" // GenerateGiftUniqueUserpicCallback\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/peer_list_widgets.h\"\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"info/channel_statistics/earn/earn_format.h\"\n#include \"chat_helpers/stickers_gift_box_pack.h\"\n#include \"core/ui_integration.h\" // TextContext\n#include \"data/data_channel.h\"\n#include \"data/data_credits.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"history/history_item.h\"\n#include \"info/channel_statistics/boosts/giveaway/boost_badge.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"main/session/session_show.h\"\n#include \"settings/settings_credits_graphics.h\" // PaintSubscriptionRightLabelCallback\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/effects/outline_segments.h\" // Ui::UnreadStoryOutlineGradient.\n#include \"ui/effects/toggle_arrow.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_color_indices.h\"\n#include \"styles/style_channel_earn.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_dialogs.h\" // dialogsStoriesFull.\n#include \"styles/style_layers.h\" // boxRowPadding.\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_statistics.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_chat.h\"\n\nnamespace Info::Statistics {\nnamespace {\n\nusing BoostCallback = Fn<void(const Data::Boost &)>;\n\n[[nodiscard]] PeerListRowId UniqueRowIdFromEntry(\n\t\tconst Data::CreditsHistoryEntry &entry) {\n\treturn UniqueRowIdFromString(entry.id\n\t\t+ (entry.refunded ? '1' : '0')\n\t\t+ (entry.pending ? '1' : '0')\n\t\t+ (entry.failed ? '1' : '0')\n\t\t+ (entry.in ? '1' : '0'));\n}\n\nvoid AddSubtitle(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> title) {\n\tconst auto &subtitlePadding = st::settingsButton.padding;\n\tUi::AddSubsectionTitle(\n\t\tcontainer,\n\t\tstd::move(title),\n\t\t{ 0, -subtitlePadding.top(), 0, -subtitlePadding.bottom() });\n}\n\n[[nodiscard]] object_ptr<Ui::SettingsButton> CreateShowMoreButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<QString> title) {\n\tauto owned = object_ptr<Ui::SettingsButton>(\n\t\tparent,\n\t\tstd::move(title),\n\t\tst::statisticsShowMoreButton);\n\tUi::AddToggleUpDownArrowToMoreButton(owned.data());\n\treturn owned;\n}\n\n[[nodiscard]] QString FormatText(\n\t\tint value1, tr::phrase<lngtag_count> phrase1,\n\t\tint value2, tr::phrase<lngtag_count> phrase2,\n\t\tint value3, tr::phrase<lngtag_count> phrase3) {\n\tconst auto separator = u\", \"_q;\n\tauto resultText = QString();\n\tif (value1 > 0) {\n\t\tresultText += phrase1(tr::now, lt_count, value1);\n\t}\n\tif (value2 > 0) {\n\t\tif (!resultText.isEmpty()) {\n\t\t\tresultText += separator;\n\t\t}\n\t\tresultText += phrase2(tr::now, lt_count, value2);\n\t}\n\tif (value3 > 0) {\n\t\tif (!resultText.isEmpty()) {\n\t\t\tresultText += separator;\n\t\t}\n\t\tresultText += phrase3(tr::now, lt_count, value3);\n\t}\n\treturn resultText;\n}\n\nstruct PublicForwardsDescriptor final {\n\tData::PublicForwardsSlice firstSlice;\n\tFn<void(Data::RecentPostId)> requestShow;\n\tnot_null<PeerData*> peer;\n\tData::RecentPostId contextId;\n};\n\nstruct MembersDescriptor final {\n\tnot_null<Main::Session*> session;\n\tFn<void(not_null<PeerData*>)> showPeerInfo;\n\tData::SupergroupStatistics data;\n};\n\nstruct BoostsDescriptor final {\n\tData::BoostsListSlice firstSlice;\n\tBoostCallback boostClickedCallback;\n\tnot_null<PeerData*> peer;\n};\n\nstruct CreditsDescriptor final {\n\tData::CreditsStatusSlice firstSlice;\n\tClicked entryClickedCallback;\n\tnot_null<PeerData*> peer;\n\tbool in = false;\n\tbool out = false;\n\tbool subscription = false;\n};\n\nclass PeerListRowWithFullId : public PeerListRow {\npublic:\n\tPeerListRowWithFullId(\n\t\tnot_null<PeerData*> peer,\n\t\tData::RecentPostId contextId);\n\n\t[[nodiscard]] PaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool) override;\n\n\t[[nodiscard]] Data::RecentPostId contextId() const;\n\nprivate:\n\tconst Data::RecentPostId _contextId;\n\n};\n\nPeerListRowWithFullId::PeerListRowWithFullId(\n\tnot_null<PeerData*> peer,\n\tData::RecentPostId contextId)\n: PeerListRow(peer)\n, _contextId(contextId) {\n}\n\nPaintRoundImageCallback PeerListRowWithFullId::generatePaintUserpicCallback(\n\t\tbool forceRound) {\n\tif (!_contextId.storyId) {\n\t\treturn PeerListRow::generatePaintUserpicCallback(forceRound);\n\t}\n\tconst auto peer = PeerListRow::peer();\n\tauto userpic = PeerListRow::ensureUserpicView();\n\n\tconst auto line = st::dialogsStoriesFull.lineTwice;\n\tconst auto penWidth = line / 2.;\n\tconst auto offset = 1.5 * penWidth * 2;\n\treturn [=](Painter &p, int x, int y, int outerWidth, int size) mutable {\n\t\tconst auto rect = QRect(QPoint(x, y), Size(size));\n\t\tpeer->paintUserpicLeft(\n\t\t\tp,\n\t\t\tuserpic,\n\t\t\tx + offset,\n\t\t\ty + offset,\n\t\t\touterWidth,\n\t\t\tsize - offset * 2);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tauto gradient = Ui::UnreadStoryOutlineGradient();\n\t\tgradient.setStart(rect.topRight());\n\t\tgradient.setFinalStop(rect.bottomLeft());\n\n\t\tp.setPen(QPen(gradient, penWidth));\n\t\tp.setBrush(Qt::NoBrush);\n\t\tp.drawEllipse(rect - Margins(penWidth));\n\t};\n}\n\nData::RecentPostId PeerListRowWithFullId::contextId() const {\n\treturn _contextId;\n}\n\nclass MembersController final : public PeerListController {\npublic:\n\tMembersController(MembersDescriptor d);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\n\tvoid setLimit(int limit);\n\nprivate:\n\tvoid addRows(int from, int to);\n\n\tconst not_null<Main::Session*> _session;\n\tFn<void(not_null<PeerData*>)> _showPeerInfo;\n\tData::SupergroupStatistics _data;\n\tint _limit = 0;\n\n};\n\nMembersController::MembersController(MembersDescriptor d)\n: _session(std::move(d.session))\n, _showPeerInfo(std::move(d.showPeerInfo))\n, _data(std::move(d.data)) {\n}\n\nMain::Session &MembersController::session() const {\n\treturn *_session;\n}\n\nvoid MembersController::setLimit(int limit) {\n\taddRows(_limit, limit);\n\t_limit = limit;\n}\n\nvoid MembersController::addRows(int from, int to) {\n\tconst auto addRow = [&](UserId userId, QString text) {\n\t\tconst auto user = _session->data().user(userId);\n\t\tauto row = std::make_unique<PeerListRow>(user);\n\t\trow->setCustomStatus(std::move(text));\n\t\tdelegate()->peerListAppendRow(std::move(row));\n\t};\n\tif (!_data.topSenders.empty()) {\n\t\tfor (auto i = from; i < to; i++) {\n\t\t\tconst auto &member = _data.topSenders[i];\n\t\t\taddRow(\n\t\t\t\tmember.userId,\n\t\t\t\tFormatText(\n\t\t\t\t\tmember.sentMessageCount,\n\t\t\t\t\ttr::lng_stats_member_messages,\n\t\t\t\t\tmember.averageCharacterCount,\n\t\t\t\t\ttr::lng_stats_member_characters,\n\t\t\t\t\t0,\n\t\t\t\t\t{}));\n\t\t}\n\t} else if (!_data.topAdministrators.empty()) {\n\t\tfor (auto i = from; i < to; i++) {\n\t\t\tconst auto &admin = _data.topAdministrators[i];\n\t\t\taddRow(\n\t\t\t\tadmin.userId,\n\t\t\t\tFormatText(\n\t\t\t\t\tadmin.deletedMessageCount,\n\t\t\t\t\ttr::lng_stats_member_deletions,\n\t\t\t\t\tadmin.bannedUserCount,\n\t\t\t\t\ttr::lng_stats_member_bans,\n\t\t\t\t\tadmin.restrictedUserCount,\n\t\t\t\t\ttr::lng_stats_member_restrictions));\n\t\t}\n\t} else if (!_data.topInviters.empty()) {\n\t\tfor (auto i = from; i < to; i++) {\n\t\t\tconst auto &inviter = _data.topInviters[i];\n\t\t\taddRow(\n\t\t\t\tinviter.userId,\n\t\t\t\tFormatText(\n\t\t\t\t\tinviter.addedMemberCount,\n\t\t\t\t\ttr::lng_stats_member_invitations,\n\t\t\t\t\t0,\n\t\t\t\t\t{},\n\t\t\t\t\t0,\n\t\t\t\t\t{}));\n\t\t}\n\t}\n}\n\nvoid MembersController::prepare() {\n}\n\nvoid MembersController::loadMoreRows() {\n}\n\nvoid MembersController::rowClicked(not_null<PeerListRow*> row) {\n\tcrl::on_main([=, peer = row->peer()] {\n\t\t_showPeerInfo(peer);\n\t});\n}\n\nclass PublicForwardsController final : public PeerListController {\npublic:\n\texplicit PublicForwardsController(PublicForwardsDescriptor d);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\nprivate:\n\tvoid appendRow(not_null<PeerData*> peer, Data::RecentPostId contextId);\n\tvoid applySlice(const Data::PublicForwardsSlice &slice);\n\n\tconst not_null<Main::Session*> _session;\n\tFn<void(Data::RecentPostId)> _requestShow;\n\n\tApi::PublicForwards _api;\n\tData::PublicForwardsSlice _firstSlice;\n\tData::PublicForwardsSlice::OffsetToken _apiToken;\n\n\tbool _allLoaded = false;\n\n};\n\nPublicForwardsController::PublicForwardsController(PublicForwardsDescriptor d)\n: _session(&d.peer->session())\n, _requestShow(std::move(d.requestShow))\n, _api(d.peer->asChannel(), d.contextId)\n, _firstSlice(std::move(d.firstSlice)) {\n}\n\nMain::Session &PublicForwardsController::session() const {\n\treturn *_session;\n}\n\nvoid PublicForwardsController::prepare() {\n\tapplySlice(base::take(_firstSlice));\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid PublicForwardsController::loadMoreRows() {\n\tif (_allLoaded) {\n\t\treturn;\n\t}\n\t_api.request(_apiToken, [=](const Data::PublicForwardsSlice &slice) {\n\t\tapplySlice(slice);\n\t});\n}\n\nvoid PublicForwardsController::applySlice(\n\t\tconst Data::PublicForwardsSlice &slice) {\n\t_allLoaded = slice.allLoaded;\n\t_apiToken = slice.token;\n\n\tfor (const auto &item : slice.list) {\n\t\tif (const auto &full = item.messageId) {\n\t\t\tif (const auto peer = session().data().peerLoaded(full.peer)) {\n\t\t\t\tappendRow(peer, item);\n\t\t\t}\n\t\t} else if (const auto &full = item.storyId) {\n\t\t\tif (const auto story = session().data().stories().lookup(full)) {\n\t\t\t\tappendRow((*story)->peer(), item);\n\t\t\t}\n\t\t}\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid PublicForwardsController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto rowWithId = static_cast<PeerListRowWithFullId*>(row.get());\n\tcrl::on_main([=, id = rowWithId->contextId()] { _requestShow(id); });\n}\n\nbase::unique_qptr<Ui::PopupMenu> PublicForwardsController::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tauto menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\tconst auto peer = row->peer();\n\tconst auto text = (peer->isChat() || peer->isMegagroup())\n\t\t? tr::lng_context_view_group(tr::now)\n\t\t: peer->isUser()\n\t\t? tr::lng_context_view_profile(tr::now)\n\t\t: peer->isChannel()\n\t\t? tr::lng_context_view_channel(tr::now)\n\t\t: QString();\n\tif (text.isEmpty()) {\n\t\treturn nullptr;\n\t}\n\tmenu->addAction(text, crl::guard(parent, [=, peerId = peer->id] {\n\t\t_requestShow({ .messageId = { peerId, MsgId() } });\n\t}), peer->isUser() ? &st::menuIconProfile : &st::menuIconInfo);\n\treturn menu;\n}\n\nvoid PublicForwardsController::appendRow(\n\t\tnot_null<PeerData*> peer,\n\t\tData::RecentPostId contextId) {\n\tif (delegate()->peerListFindRow(peer->id.value)) {\n\t\treturn;\n\t}\n\n\tauto row = std::make_unique<PeerListRowWithFullId>(peer, contextId);\n\n\tconst auto members = peer->isChannel()\n\t\t? peer->asChannel()->membersCount()\n\t\t: 0;\n\tconst auto views = [&] {\n\t\tif (contextId.messageId) {\n\t\t\tconst auto message = peer->owner().message(contextId.messageId);\n\t\t\treturn message ? std::max(message->viewsCount(), 0) : 0;\n\t\t} else if (const auto &id = contextId.storyId) {\n\t\t\tconst auto story = peer->owner().stories().lookup(id);\n\t\t\treturn story ? (*story)->views() : 0;\n\t\t}\n\t\treturn 0;\n\t}();\n\n\tconst auto membersText = !members\n\t\t? QString()\n\t\t: peer->isMegagroup()\n\t\t? tr::lng_chat_status_members(tr::now, lt_count_decimal, members)\n\t\t: tr::lng_chat_status_subscribers(tr::now, lt_count_decimal, members);\n\tconst auto viewsText = views\n\t\t? tr::lng_stats_recent_messages_views({}, lt_count_decimal, views)\n\t\t: QString();\n\tconst auto resultText = (membersText.isEmpty() && viewsText.isEmpty())\n\t\t? tr::lng_stories_no_views(tr::now)\n\t\t: (membersText.isEmpty() || viewsText.isEmpty())\n\t\t? membersText + viewsText\n\t\t: QString(\"%1, %2\").arg(membersText, viewsText);\n\trow->setCustomStatus(resultText);\n\n\tdelegate()->peerListAppendRow(std::move(row));\n\treturn;\n}\n\nclass BoostRow final : public PeerListRow {\npublic:\n\tBoostRow(not_null<PeerData*> peer, const Data::Boost &boost);\n\tBoostRow(const Data::Boost &boost);\n\n\t[[nodiscard]] const Data::Boost &boost() const;\n\t[[nodiscard]] QString generateName() override;\n\n\t[[nodiscard]] PaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\n\tint paintNameIconGetWidth(\n\t\tPainter &p,\n\t\tFn<void()> repaint,\n\t\tcrl::time now,\n\t\tint nameLeft,\n\t\tint nameTop,\n\t\tint nameWidth,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) override;\n\n\tQSize rightActionSize() const override;\n\tQMargins rightActionMargins() const override;\n\tbool rightActionDisabled() const override;\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\nprivate:\n\tvoid init();\n\tvoid invalidateBadges();\n\n\tconst Data::Boost _boost;\n\tUi::EmptyUserpic _userpic;\n\tQImage _badge;\n\tQImage _rightBadge;\n\tPaintRoundImageCallback _paintUserpicCallback;\n\n};\n\nBoostRow::BoostRow(not_null<PeerData*> peer, const Data::Boost &boost)\n: PeerListRow(peer, UniqueRowIdFromString(boost.id))\n, _boost(boost)\n, _userpic(Ui::EmptyUserpic::UserpicColor(0), QString()) {\n\tinit();\n}\n\nBoostRow::BoostRow(const Data::Boost &boost)\n: PeerListRow(UniqueRowIdFromString(boost.id))\n, _boost(boost)\n, _userpic(\n\tUi::EmptyUserpic::UserpicColor(boost.credits\n\t\t? st::colorIndexOrange\n\t\t: boost.isUnclaimed\n\t\t? st::colorIndexSea\n\t\t: st::colorIndexBlue),\n\tQString()) {\n\tinit();\n}\n\nvoid BoostRow::init() {\n\tif (!PeerListRow::special()) {\n\t\t_paintUserpicCallback = PeerListRow::generatePaintUserpicCallback(\n\t\t\tfalse);\n\t} else if (_boost.credits) {\n\t\tconst auto creditsIcon = std::make_shared<QImage>();\n\t\t_paintUserpicCallback = [=](\n\t\t\t\tPainter &p,\n\t\t\t\tint x,\n\t\t\t\tint y,\n\t\t\t\tint outerWidth,\n\t\t\t\tint size) mutable {\n\t\t\t_userpic.paintCircle(p, x, y, outerWidth, size);\n\t\t\tif (creditsIcon->isNull()) {\n\t\t\t\t*creditsIcon = Ui::CreditsWhiteDoubledIcon(size, 1.);\n\t\t\t}\n\t\t\tp.drawImage(x, y, *creditsIcon);\n\t\t};\n\t} else {\n\t\t_paintUserpicCallback = [=](\n\t\t\t\tPainter &p,\n\t\t\t\tint x,\n\t\t\t\tint y,\n\t\t\t\tint outerWidth,\n\t\t\t\tint size) mutable {\n\t\t\t_userpic.paintCircle(p, x, y, outerWidth, size);\n\t\t\t(_boost.isUnclaimed\n\t\t\t\t? st::boostsListUnclaimedIcon\n\t\t\t\t: st::boostsListUnknownIcon).paintInCenter(\n\t\t\t\t\tp,\n\t\t\t\t\tRect(x, y, Size(size)));\n\t\t};\n\t}\n\n\tinvalidateBadges();\n\tauto status = (!PeerListRow::special() || _boost.credits)\n\t\t? tr::lng_boosts_list_status(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\tlangDayOfMonth(_boost.expiresAt.date()))\n\t\t: tr::lng_months_tiny(tr::now, lt_count, _boost.expiresAfterMonths)\n\t\t\t+ ' '\n\t\t\t+ QChar(0x2022)\n\t\t\t+ ' '\n\t\t\t+ langDayOfMonth(_boost.date.date());\n\tPeerListRow::setCustomStatus(std::move(status));\n}\n\nconst Data::Boost &BoostRow::boost() const {\n\treturn _boost;\n}\n\nQString BoostRow::generateName() {\n\treturn !PeerListRow::special()\n\t\t? PeerListRow::generateName()\n\t\t: _boost.credits\n\t\t? tr::lng_giveaway_prizes_additional_credits_amount(\n\t\t\ttr::now,\n\t\t\tlt_count_decimal,\n\t\t\t_boost.credits)\n\t\t: _boost.isUnclaimed\n\t\t? tr::lng_boosts_list_unclaimed(tr::now)\n\t\t: tr::lng_boosts_list_pending(tr::now);\n}\n\nPaintRoundImageCallback BoostRow::generatePaintUserpicCallback(bool force) {\n\treturn _paintUserpicCallback;\n}\n\nvoid BoostRow::invalidateBadges() {\n\t_badge = _boost.multiplier\n\t\t? CreateBadge(\n\t\t\tst::statisticsDetailsBottomCaptionStyle,\n\t\t\tQString::number(_boost.multiplier),\n\t\t\tst::boostsListBadgeHeight,\n\t\t\tst::boostsListBadgeTextPadding,\n\t\t\tst::premiumButtonBg2,\n\t\t\tst::premiumButtonFg,\n\t\t\t1.,\n\t\t\tst::boostsListMiniIconPadding,\n\t\t\tst::boostsListMiniIcon)\n\t\t: QImage();\n\n\tconstexpr auto kBadgeBgOpacity = 0.2;\n\tconst auto &rightColor = _boost.isGiveaway\n\t\t? st::historyPeer4UserpicBg2\n\t\t: st::historyPeer8UserpicBg2;\n\tconst auto &rightIcon = _boost.isGiveaway\n\t\t? st::boostsListGiveawayMiniIcon\n\t\t: st::boostsListGiftMiniIcon;\n\t_rightBadge = ((_boost.isGift || _boost.isGiveaway) && !_boost.credits)\n\t\t? CreateBadge(\n\t\t\tst::boostsListRightBadgeTextStyle,\n\t\t\t_boost.isGiveaway\n\t\t\t\t? tr::lng_gift_link_reason_giveaway(tr::now)\n\t\t\t\t: tr::lng_gift_link_label_gift(tr::now),\n\t\t\tst::boostsListRightBadgeHeight,\n\t\t\tst::boostsListRightBadgeTextPadding,\n\t\t\trightColor,\n\t\t\trightColor,\n\t\t\tkBadgeBgOpacity,\n\t\t\tst::boostsListGiftMiniIconPadding,\n\t\t\trightIcon)\n\t\t: QImage();\n}\n\n\nQSize BoostRow::rightActionSize() const {\n\treturn _rightBadge.size() / style::DevicePixelRatio();\n}\n\nQMargins BoostRow::rightActionMargins() const {\n\treturn st::boostsListRightBadgePadding;\n}\n\nbool BoostRow::rightActionDisabled() const {\n\treturn true;\n}\n\nvoid BoostRow::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\tif (!_rightBadge.isNull()) {\n\t\tp.drawImage(x, y, _rightBadge);\n\t}\n}\n\nint BoostRow::paintNameIconGetWidth(\n\t\tPainter &p,\n\t\tFn<void()> repaint,\n\t\tcrl::time now,\n\t\tint nameLeft,\n\t\tint nameTop,\n\t\tint nameWidth,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) {\n\tif (_badge.isNull()) {\n\t\treturn 0;\n\t}\n\tconst auto badgew = _badge.width() / style::DevicePixelRatio();\n\tconst auto nameTooLarge = (nameWidth > availableWidth);\n\tconst auto &padding = st::boostsListBadgePadding;\n\tconst auto left = nameTooLarge\n\t\t? ((nameLeft + availableWidth) - badgew - padding.left())\n\t\t: (nameLeft + nameWidth + padding.right());\n\tp.drawImage(left, nameTop + padding.top(), _badge);\n\treturn badgew + (nameTooLarge ? padding.left() : 0);\n}\n\nclass BoostsController final : public PeerListController {\npublic:\n\texplicit BoostsController(BoostsDescriptor d);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\n\t[[nodiscard]] bool skipRequest() const;\n\tvoid requestNext();\n\n\t[[nodiscard]] rpl::producer<int> totalBoostsValue() const;\n\nprivate:\n\tvoid applySlice(const Data::BoostsListSlice &slice);\n\n\tconst not_null<Main::Session*> _session;\n\tBoostCallback _boostClickedCallback;\n\n\tApi::Boosts _api;\n\tData::BoostsListSlice _firstSlice;\n\tData::BoostsListSlice::OffsetToken _apiToken;\n\n\tbool _allLoaded = false;\n\tbool _requesting = false;\n\n\trpl::variable<int> _totalBoosts;\n\n};\n\nBoostsController::BoostsController(BoostsDescriptor d)\n: _session(&d.peer->session())\n, _boostClickedCallback(std::move(d.boostClickedCallback))\n, _api(d.peer)\n, _firstSlice(std::move(d.firstSlice)) {\n\tPeerListController::setStyleOverrides(&st::boostsListBox);\n}\n\nMain::Session &BoostsController::session() const {\n\treturn *_session;\n}\n\nbool BoostsController::skipRequest() const {\n\treturn _requesting || _allLoaded;\n}\n\nvoid BoostsController::requestNext() {\n\t_requesting = true;\n\t_api.requestBoosts(_apiToken, [=](const Data::BoostsListSlice &slice) {\n\t\t_requesting = false;\n\t\tapplySlice(slice);\n\t});\n}\n\nvoid BoostsController::prepare() {\n\tapplySlice(base::take(_firstSlice));\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid BoostsController::loadMoreRows() {\n}\n\nvoid BoostsController::applySlice(const Data::BoostsListSlice &slice) {\n\t_allLoaded = slice.allLoaded;\n\t_apiToken = slice.token;\n\n\tauto sumFromSlice = 0;\n\tfor (const auto &item : slice.list) {\n\t\tsumFromSlice += item.multiplier ? item.multiplier : 1;\n\t\tauto row = [&] {\n\t\t\tif (item.userId && !item.isUnclaimed) {\n\t\t\t\tconst auto user = session().data().user(item.userId);\n\t\t\t\treturn std::make_unique<BoostRow>(user, item);\n\t\t\t} else {\n\t\t\t\treturn std::make_unique<BoostRow>(item);\n\t\t\t}\n\t\t}();\n\t\tdelegate()->peerListAppendRow(std::move(row));\n\t}\n\tdelegate()->peerListRefreshRows();\n\t_totalBoosts = _totalBoosts.current() + sumFromSlice;\n}\n\nvoid BoostsController::rowClicked(not_null<PeerListRow*> row) {\n\tif (_boostClickedCallback) {\n\t\t_boostClickedCallback(\n\t\t\tstatic_cast<const BoostRow*>(row.get())->boost());\n\t}\n}\n\nrpl::producer<int> BoostsController::totalBoostsValue() const {\n\treturn _totalBoosts.value();\n}\n\nstruct CreditsRowDescriptionData {\n\tuint64 rowId = 0;\n\tuint64 bareGiftStickerId = 0;\n};\n\n[[nodiscard]] QString SerializeCreditsRowDescriptionData(\n\t\tconst CreditsRowDescriptionData &data) {\n\treturn QString(\"%1;%2\").arg(data.rowId).arg(data.bareGiftStickerId);\n}\n\n[[nodiscard]] CreditsRowDescriptionData DeserializeCreditsRowDescriptionData(\n\t\tconst QString &str) {\n\tauto data = CreditsRowDescriptionData();\n\tconst auto parts = str.split(';');\n\tif (parts.size() >= 1) {\n\t\tdata.rowId = parts[0].toULongLong();\n\t}\n\tif (parts.size() >= 2) {\n\t\tdata.bareGiftStickerId = parts[1].toULongLong();\n\t}\n\treturn data;\n}\n\nclass CreditsRow final : public PeerListRow {\npublic:\n\tstruct Descriptor final {\n\t\tnot_null<Main::Session*> session;\n\t\tData::CreditsHistoryEntry entry;\n\t\tData::SubscriptionEntry subscription;\n\t\tUi::Text::MarkedContext context;\n\t\tint rowHeight = 0;\n\t\tFn<void(not_null<PeerListRow*>)> updateCallback;\n\t};\n\n\tCreditsRow(not_null<PeerData*> peer, const Descriptor &descriptor);\n\tCreditsRow(const Descriptor &descriptor);\n\n\tvoid init();\n\n\t[[nodiscard]] const Data::CreditsHistoryEntry &entry() const;\n\t[[nodiscard]] const Data::SubscriptionEntry &subscription() const;\n\t[[nodiscard]] QString generateName() override;\n\n\t[[nodiscard]] PaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\n\tQSize rightActionSize() const override;\n\tQMargins rightActionMargins() const override;\n\tbool rightActionDisabled() const override;\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\n\tvoid paintStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint available,\n\t\tint outer,\n\t\tbool selected) override;\n\n\tconst style::PeerListItem &computeSt(\n\t\tconst style::PeerListItem &st) const override;\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\tconst Data::CreditsHistoryEntry _entry;\n\tconst Data::SubscriptionEntry _subscription;\n\tconst Ui::Text::MarkedContext _context;\n\n\tconst int _rowHeight;\n\n\tPaintRoundImageCallback _paintUserpicCallback;\n\tstd::optional<Settings::SubscriptionRightLabel> _rightLabel;\n\tQString _name;\n\n\tUi::Text::String _description;\n\tUi::Text::String _rightText;\n\tUi::Text::String _rightMinorText;\n\n\tstd::shared_ptr<Ui::DynamicImage> _descriptionThumbnail;\n\tQImage _descriptionThumbnailCache;\n\n\tbase::has_weak_ptr _guard;\n};\n\nCreditsRow::CreditsRow(\n\tnot_null<PeerData*> peer,\n\tconst Descriptor &descriptor)\n: PeerListRow(peer, UniqueRowIdFromEntry(descriptor.entry))\n, _session(descriptor.session)\n, _entry(descriptor.entry)\n, _subscription(descriptor.subscription)\n, _context(descriptor.context)\n, _rowHeight(descriptor.rowHeight) {\n\tconst auto callback = Ui::PaintPreviewCallback(\n\t\t&peer->session(),\n\t\t_entry);\n\tif (callback) {\n\t\t_paintUserpicCallback = callback(crl::guard(\n\t\t\t&_guard,\n\t\t\t[this, update = descriptor.updateCallback] { update(this); }));\n\t}\n\tif (!_subscription.cancelled\n\t\t&& !_subscription.expired\n\t\t&& _subscription.subscription) {\n\t\t_rightLabel = Settings::PaintSubscriptionRightLabelCallback(\n\t\t\t&peer->session(),\n\t\t\tst::boostsListBox.item,\n\t\t\t_subscription.subscription.credits);\n\t}\n}\n\nCreditsRow::CreditsRow(const Descriptor &descriptor)\n: PeerListRow(UniqueRowIdFromEntry(descriptor.entry))\n, _session(descriptor.session)\n, _entry(descriptor.entry)\n, _subscription(descriptor.subscription)\n, _context(descriptor.context)\n, _rowHeight(descriptor.rowHeight) {\n}\n\nvoid CreditsRow::init() {\n\tconst auto isSpecial = PeerListRow::special();\n\tconst auto name = !isSpecial\n\t\t? PeerListRow::generateName()\n\t\t: Ui::GenerateEntryName(_entry).text;\n\t_name = (_entry.isLiveStoryReaction() || _entry.paidMessagesCount)\n\t\t? name\n\t\t: _entry.postsSearch\n\t\t? tr::lng_credits_box_history_entry_posts_search(tr::now)\n\t\t: (_entry.giftUpgraded && _entry.uniqueGift && !isSpecial)\n\t\t? u\"%1 #%2\"_q.arg(_entry.uniqueGift->title).arg(Lang::FormatCountDecimal(_entry.uniqueGift->number))\n\t\t: ((!_entry.subscriptionUntil.isNull() && !isSpecial)\n\t\t\t|| (_entry.giftResale && !isSpecial)\n\t\t\t|| _entry.title.isEmpty())\n\t\t? name\n\t\t: _entry.title;\n\tsetSkipPeerBadge(true);\n\tconst auto description = _entry.floodSkip\n\t\t? tr::lng_credits_box_history_entry_floodskip_about(\n\t\t\ttr::now,\n\t\t\tlt_count_decimal,\n\t\t\t_entry.floodSkip)\n\t\t: _entry.isLiveStoryReaction()\n\t\t? tr::lng_credits_paid_messages_fee_live_reaction(tr::now)\n\t\t: _entry.paidMessagesCount\n\t\t? tr::lng_credits_paid_messages_fee(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\t_entry.paidMessagesCount)\n\t\t: (!_entry.subscriptionUntil.isNull() && !_entry.title.isEmpty())\n\t\t? _entry.title\n\t\t: _entry.refunded\n\t\t? tr::lng_channel_earn_history_return(tr::now)\n\t\t: _entry.pending\n\t\t? tr::lng_channel_earn_history_pending(tr::now)\n\t\t: _entry.failed\n\t\t? tr::lng_channel_earn_history_failed(tr::now)\n\t\t: !_entry.subscriptionUntil.isNull()\n\t\t? tr::lng_credits_box_history_entry_subscription(tr::now)\n\t\t: (_entry.peerType\n\t\t\t== Data::CreditsHistoryEntry::PeerType::PremiumBot)\n\t\t? tr::lng_credits_box_history_entry_via_premium_bot(tr::now)\n\t\t: (_entry.peerType == Data::CreditsHistoryEntry::PeerType::Fragment)\n\t\t? tr::lng_credits_box_history_entry_fragment(tr::now)\n\t\t: (_entry.gift && isSpecial)\n\t\t? tr::lng_credits_box_history_entry_anonymous(tr::now)\n\t\t: _entry.giftUpgraded\n\t\t? tr::lng_credits_box_history_entry_gift_upgrade(tr::now)\n\t\t: (_name == name)\n\t\t? Ui::GenerateEntryName(_entry).text\n\t\t: name;\n\t_description.setText(st::defaultTextStyle, description);\n\tPeerListRow::setCustomStatus(langDateTime(_entry.date));\n\tif (_subscription) {\n\t\tPeerListRow::setCustomStatus((_subscription.expired\n\t\t\t? tr::lng_credits_subscription_status_none\n\t\t\t: _subscription.cancelled\n\t\t\t? tr::lng_credits_subscription_status_off\n\t\t\t: tr::lng_credits_subscription_status_on)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_date,\n\t\t\t\tlangDayOfMonthFull(_subscription.until.date())));\n\t\t_description.setText(st::defaultTextStyle, _subscription.title);\n\t}\n\tif (_entry.bareGiftStickerId && !_entry.giftUpgraded) {\n\t\t_description.setMarkedText(\n\t\t\tst::defaultTextStyle,\n\t\t\tUi::Text::SingleCustomEmoji(\n\t\t\t\tSerializeCreditsRowDescriptionData({\n\t\t\t\t\tPeerListRow::id(),\n\t\t\t\t\t_entry.bareGiftStickerId,\n\t\t\t\t}))\n\t\t\t.append(' ')\n\t\t\t.append(description),\n\t\t\tkMarkupTextOptions,\n\t\t\t_context);\n\t}\n\tconst auto descriptionPhotoId = (!_entry.subscriptionUntil.isNull())\n\t\t? _entry.photoId\n\t\t: _subscription.photoId;\n\tif (descriptionPhotoId) {\n\t\t_descriptionThumbnail = Ui::MakePhotoThumbnail(\n\t\t\t_session->data().photo(descriptionPhotoId),\n\t\t\t{});\n\t\t_descriptionThumbnail->subscribeToUpdates([this] {\n\t\t\tconst auto thumbnailSide = st::defaultTextStyle.font->height;\n\t\t\t_descriptionThumbnailCache = Images::Round(\n\t\t\t\t_descriptionThumbnail->image(thumbnailSide),\n\t\t\t\tImageRoundRadius::Large);\n\t\t\tif (_context.repaint) {\n\t\t\t\t_context.repaint();\n\t\t\t}\n\t\t});\n\t}\n\tif (_entry) {\n\t\tconstexpr auto kMinus = QChar(0x2212);\n\t\tconst auto isCurrency = _entry.credits.ton();\n\t\t_rightText.setMarkedText(\n\t\t\tisCurrency\n\t\t\t\t? st::channelEarnHistoryMajorLabel.style\n\t\t\t\t: st::creditsHistoryRowRightStyle,\n\t\t\tTextWithEntities()\n\t\t\t\t.append(_entry.in ? QChar('+') : kMinus)\n\t\t\t\t.append(isCurrency\n\t\t\t\t\t? Info::ChannelEarn::MajorPart(_entry.credits.abs())\n\t\t\t\t\t: Lang::FormatCreditsAmountDecimal(_entry.credits.abs()))\n\t\t\t\t.append(QChar(' '))\n\t\t\t\t.append(isCurrency\n\t\t\t\t\t? TextWithEntities()\n\t\t\t\t\t: Ui::MakeCreditsIconEntity()),\n\t\t\tkMarkupTextOptions,\n\t\t\t_context);\n\t\tif (isCurrency) {\n\t\t\t_rightMinorText.setMarkedText(\n\t\t\t\tst::channelEarnHistoryMinorLabel.style,\n\t\t\t\tTextWithEntities()\n\t\t\t\t\t.append(Info::ChannelEarn::MinorPart(_entry.credits))\n\t\t\t\t\t.append(QChar(' '))\n\t\t\t\t\t.append(\n\t\t\t\t\t\tUi::Text::SingleCustomEmoji(_entry.in\n\t\t\t\t\t\t\t? u\"ton:in\"_q\n\t\t\t\t\t\t\t: u\"ton:out\"_q)),\n\t\t\t\tkMarkupTextOptions,\n\t\t\t\t_context);\n\t\t}\n\t}\n\tif (!_paintUserpicCallback) {\n\t\t_paintUserpicCallback = _entry.giftUpgraded\n\t\t\t? GenerateGiftUniqueUserpicCallback(\n\t\t\t\t_session,\n\t\t\t\t_entry.uniqueGift,\n\t\t\t\t_context.repaint)\n\t\t\t: (isSpecial || _entry.postsSearch)\n\t\t\t? Ui::GenerateCreditsPaintUserpicCallback(_entry)\n\t\t\t: PeerListRow::generatePaintUserpicCallback(false);\n\t}\n}\n\nconst Data::CreditsHistoryEntry &CreditsRow::entry() const {\n\treturn _entry;\n}\n\nconst Data::SubscriptionEntry &CreditsRow::subscription() const {\n\treturn _subscription;\n}\n\nQString CreditsRow::generateName() {\n\treturn _name;\n}\n\nPaintRoundImageCallback CreditsRow::generatePaintUserpicCallback(bool force) {\n\treturn _paintUserpicCallback;\n}\n\n[[nodiscard]] QString RightActionText(const Data::SubscriptionEntry &s) {\n\treturn s.cancelledByBot\n\t\t? tr::lng_credits_subscription_status_off_by_bot_right(tr::now)\n\t\t: s.cancelled\n\t\t? tr::lng_credits_subscription_status_off_right(tr::now)\n\t\t: s.expired\n\t\t? tr::lng_credits_subscription_status_none_right(tr::now)\n\t\t: QString();\n}\n\nQSize CreditsRow::rightActionSize() const {\n\tif (_rightLabel) {\n\t\treturn _rightLabel->size;\n\t} else if (const auto t = RightActionText(_subscription); !t.isEmpty()) {\n\t\tconst auto lines = t.split('\\n');\n\t\tauto maxWidth = 0;\n\t\tfor (const auto &line : lines) {\n\t\t\tconst auto width = st::contactsStatusFont->width(line);\n\t\t\tif (width > maxWidth) {\n\t\t\t\tmaxWidth = width;\n\t\t\t}\n\t\t}\n\t\treturn QSize(maxWidth + st::boxRowPadding.right(), _rowHeight);\n\t} else if (_subscription || _entry) {\n\t\treturn QSize(\n\t\t\t_rightText.maxWidth()\n\t\t\t\t+ _rightMinorText.maxWidth()\n\t\t\t\t+ st::boxRowPadding.right() / 2,\n\t\t\t_rowHeight);\n\t} else if (!_entry && !_subscription) {\n\t\treturn QSize();\n\t}\n\treturn QSize();\n}\n\nQMargins CreditsRow::rightActionMargins() const {\n\treturn QMargins(0, 0, st::boxRowPadding.right(), 0);\n}\n\nbool CreditsRow::rightActionDisabled() const {\n\treturn true;\n}\n\nvoid CreditsRow::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\tconst auto rightSkip = st::boxRowPadding.right();\n\tif (_rightLabel) {\n\t\treturn _rightLabel->draw(p, x, y, _rowHeight);\n\t} else if (const auto t = RightActionText(_subscription); !t.isEmpty()) {\n\t\tconst auto &statusFont = st::contactsStatusFont;\n\t\ty += _rowHeight / 2;\n\t\tp.setFont(statusFont);\n\t\tp.setPen(st::attentionButtonFg);\n\n\t\tconst auto lines = t.split('\\n');\n\t\tif (lines.size() > 1) {\n\t\t\tconst auto rect = QRect(x, 0, outerWidth - x, _rowHeight);\n\t\t\tconst auto lineHeight = statusFont->height;\n\t\t\tconst auto totalHeight = lines.size() * lineHeight;\n\t\t\tauto startY = rect.top()\n\t\t\t\t+ (rect.height() - totalHeight) / 2\n\t\t\t\t+ statusFont->ascent;\n\n\t\t\tfor (const auto &line : lines) {\n\t\t\t\tconst auto lineWidth = statusFont->width(line);\n\t\t\t\tconst auto startX = rect.left()\n\t\t\t\t\t+ (rect.width() - lineWidth) / 2;\n\t\t\t\tp.drawText(startX, startY, line);\n\t\t\t\tstartY += lineHeight;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tp.drawTextRight(rightSkip, y - statusFont->height / 2, outerWidth, t);\n\t\treturn;\n\t}\n\tp.setPen(_entry.pending\n\t\t? st::creditsStroke\n\t\t: _entry.in\n\t\t? st::boxTextFgGood\n\t\t: st::menuIconAttentionColor);\n\tconst auto xMinor = outerWidth - _rightMinorText.maxWidth() - rightSkip;\n\t_rightMinorText.draw(p, Ui::Text::PaintContext{\n\t\t.position = QPoint(xMinor, y + st::creditsHistoryRowRightMinorTop),\n\t\t.outerWidth = outerWidth,\n\t\t.availableWidth = outerWidth,\n\t});\n\t_rightText.draw(p, Ui::Text::PaintContext{\n\t\t.position = QPoint(\n\t\t\txMinor - _rightText.maxWidth(),\n\t\t\ty + st::creditsHistoryRowRightTop),\n\t\t.outerWidth = outerWidth,\n\t\t.availableWidth = outerWidth,\n\t});\n}\n\nvoid CreditsRow::paintStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint available,\n\t\tint outer,\n\t\tbool selected) {\n\tPeerListRow::paintStatusText(p, st, x, y, available, outer, selected);\n\tp.setPen(st.nameFg);\n\tif (!_descriptionThumbnailCache.isNull()) {\n\t\tconst auto thumbnailSide = _descriptionThumbnailCache.width()\n\t\t\t/ style::DevicePixelRatio();\n\t\tconst auto thumbnailSpace = st::lineWidth * 4 + thumbnailSide;\n\t\tp.drawImage(\n\t\t\tx,\n\t\t\ty - thumbnailSide,\n\t\t\t_descriptionThumbnailCache);\n\t\tx += thumbnailSpace;\n\t\touter -= thumbnailSpace;\n\t\tavailable -= thumbnailSpace;\n\t}\n\t_description.draw(p, {\n\t\t.position = QPoint(x, y - st::creditsHistoryRowDescriptionSkip),\n\t\t.outerWidth = outer,\n\t\t.availableWidth = available,\n\t\t.elisionLines = 1,\n\t});\n}\n\nconst style::PeerListItem &CreditsRow::computeSt(\n\t\tconst style::PeerListItem &st) const {\n\treturn (!_subscription || !_subscription.title.isEmpty())\n\t\t? st\n\t\t: st::boostsListBox.item;\n}\n\nclass CreditsController final : public PeerListController {\npublic:\n\texplicit CreditsController(CreditsDescriptor d);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\n\t[[nodiscard]] bool skipRequest() const;\n\tvoid requestNext();\n\n\t[[nodiscard]] rpl::producer<bool> allLoadedValue() const;\n\nprivate:\n\tvoid applySlice(const Data::CreditsStatusSlice &slice);\n\n\tconst not_null<Main::Session*> _session;\n\tconst bool _subscription;\n\tClicked _entryClickedCallback;\n\n\tApi::CreditsHistory _api;\n\tData::CreditsStatusSlice _firstSlice;\n\tData::CreditsStatusSlice::OffsetToken _apiToken;\n\tUi::Text::MarkedContext _context;\n\n\tbase::flat_map<PeerListRowId, not_null<PeerListRow*>> _rowsById;\n\n\trpl::variable<bool> _allLoaded = false;\n\tbool _requesting = false;\n\n};\n\nCreditsController::CreditsController(CreditsDescriptor d)\n: _session(&d.peer->session())\n, _subscription(d.subscription)\n, _entryClickedCallback(std::move(d.entryClickedCallback))\n, _api(d.peer, d.in, d.out)\n, _firstSlice(std::move(d.firstSlice))\n, _context([&]() -> Ui::Text::MarkedContext {\n\tconst auto height = st::creditsHistoryRowRightStyle.font->height\n\t\t- st::lineWidth;\n\tauto customEmojiFactory = [=](\n\t\t\tQStringView data,\n\t\t\tconst Ui::Text::MarkedContext &context\n\t\t) -> std::unique_ptr<Ui::Text::CustomEmoji> {\n\t\tif (data == Ui::kCreditsCurrency) {\n\t\t\treturn std::make_unique<Ui::Text::ShiftedEmoji>(\n\t\t\t\tUi::MakeCreditsIconEmoji(height, 1),\n\t\t\t\tQPoint(-st::lineWidth, st::lineWidth));\n\t\t}\n\t\tif (data.startsWith(u\"ton\"_q)) {\n\t\t\tconst auto in = data.split(u\":\"_q)[1].startsWith(u\"in\"_q);\n\t\t\treturn std::make_unique<Ui::Text::ShiftedEmoji>(\n\t\t\t\tstd::make_unique<Ui::CustomEmoji::Internal>(\n\t\t\t\t\tdata.toString(),\n\t\t\t\t\tUi::Earn::IconCurrencyColored(\n\t\t\t\t\t\tst::tonFieldIconSize,\n\t\t\t\t\t\tin\n\t\t\t\t\t\t\t? st::boxTextFgGood->c\n\t\t\t\t\t\t\t: st::menuIconAttentionColor->c)),\n\t\t\t\tQPoint(0, st::lineWidth));\n\t\t}\n\t\tconst auto desc = DeserializeCreditsRowDescriptionData(\n\t\t\tdata.toString());\n\t\tif (!desc.rowId || !desc.bareGiftStickerId) {\n\t\t\treturn nullptr;\n\t\t}\n\t\tconst auto it = _rowsById.find(desc.rowId);\n\t\tif (it != _rowsById.end()) {\n\t\t\tconst auto row = it->second;\n\t\t\treturn _session->data().customEmojiManager().create(\n\t\t\t\tdesc.bareGiftStickerId,\n\t\t\t\t[=]{ delegate()->peerListUpdateRow(row); });\n\t\t}\n\t\treturn nullptr;\n\t};\n\treturn { .customEmojiFactory = std::move(customEmojiFactory) };\n}()) {\n\tPeerListController::setStyleOverrides(&st::creditsHistoryEntriesList);\n}\n\nMain::Session &CreditsController::session() const {\n\treturn *_session;\n}\n\nbool CreditsController::skipRequest() const {\n\treturn _requesting || _allLoaded.current();\n}\n\nvoid CreditsController::requestNext() {\n\t_requesting = true;\n\tconst auto done = [=](const Data::CreditsStatusSlice &s) {\n\t\t_requesting = false;\n\t\tapplySlice(s);\n\t};\n\tif (_subscription) {\n\t\treturn _api.requestSubscriptions(_apiToken, done);\n\t}\n\t_api.request(_apiToken, done);\n}\n\nvoid CreditsController::prepare() {\n\tapplySlice(base::take(_firstSlice));\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid CreditsController::loadMoreRows() {\n}\n\nvoid CreditsController::applySlice(const Data::CreditsStatusSlice &slice) {\n\t_allLoaded = slice.allLoaded;\n\t_apiToken = _subscription ? slice.tokenSubscriptions : slice.token;\n\n\tauto create = [&](\n\t\t\tconst Data::CreditsHistoryEntry &i,\n\t\t\tconst Data::SubscriptionEntry &s) {\n\t\tconst auto descriptor = CreditsRow::Descriptor{\n\t\t\t.session = &session(),\n\t\t\t.entry = i,\n\t\t\t.subscription = s,\n\t\t\t.context = _context,\n\t\t\t.rowHeight = ((!s || !s.title.isEmpty())\n\t\t\t\t? computeListSt().item\n\t\t\t\t: st::boostsListBox.item).height,\n\t\t\t.updateCallback = [=](not_null<PeerListRow*> row) {\n\t\t\t\tdelegate()->peerListUpdateRow(row);\n\t\t\t},\n\t\t};\n\t\tauto owned = std::unique_ptr<CreditsRow>(nullptr);\n\t\tif (i.bareActorId) {\n\t\t\tconst auto peer = session().data().peer(PeerId(i.bareActorId));\n\t\t\towned = std::make_unique<CreditsRow>(peer, descriptor);\n\t\t} else if (const auto peerId = PeerId(i.barePeerId + s.barePeerId)) {\n\t\t\tconst auto peer = session().data().peer(peerId);\n\t\t\towned = std::make_unique<CreditsRow>(peer, descriptor);\n\t\t} else {\n\t\t\towned = std::make_unique<CreditsRow>(descriptor);\n\t\t}\n\t\t_rowsById.emplace(owned->id(), owned.get());\n\t\towned->init();\n\t\treturn owned;\n\t};\n\n\tauto giftPacksRequested = false;\n\tfor (const auto &item : slice.list) {\n\t\tif (item.bareGiveawayMsgId && !giftPacksRequested) {\n\t\t\tgiftPacksRequested = true;\n\t\t\tsession().giftBoxStickersPacks().load();\n\t\t}\n\t\tdelegate()->peerListAppendRow(create(item, {}));\n\t}\n\tfor (const auto &item : slice.subscriptions) {\n\t\tdelegate()->peerListAppendRow(create({}, item));\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid CreditsController::rowClicked(not_null<PeerListRow*> row) {\n\tif (_entryClickedCallback) {\n\t\tconst auto r = static_cast<const CreditsRow*>(row.get());\n\t\t_entryClickedCallback(r->entry(), r->subscription());\n\t}\n}\n\nrpl::producer<bool> CreditsController::allLoadedValue() const {\n\treturn _allLoaded.value();\n}\n\n} // namespace\n\nvoid AddPublicForwards(\n\t\tconst Data::PublicForwardsSlice &firstSlice,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tFn<void(Data::RecentPostId)> requestShow,\n\t\tnot_null<PeerData*> peer,\n\t\tData::RecentPostId contextId) {\n\tif (!peer->isChannel()) {\n\t\treturn;\n\t}\n\n\tstruct State final {\n\t\tState(PublicForwardsDescriptor d) : controller(std::move(d)) {\n\t\t}\n\t\tPeerListContentDelegateSimple delegate;\n\t\tPublicForwardsController controller;\n\t};\n\tauto d = PublicForwardsDescriptor{\n\t\tfirstSlice,\n\t\tstd::move(requestShow),\n\t\tpeer,\n\t\tcontextId,\n\t};\n\tconst auto state = container->lifetime().make_state<State>(std::move(d));\n\n\tif (const auto total = firstSlice.total; total > 0) {\n\t\tAddSubtitle(\n\t\t\tcontainer,\n\t\t\ttr::lng_stats_overview_message_public_share(\n\t\t\t\tlt_count_decimal,\n\t\t\t\trpl::single<float64>(total)));\n\t}\n\n\tstate->delegate.setContent(container->add(\n\t\tobject_ptr<PeerListContent>(container, &state->controller)));\n\tstate->controller.setDelegate(&state->delegate);\n}\n\nvoid AddMembersList(\n\t\tData::SupergroupStatistics data,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tFn<void(not_null<PeerData*>)> showPeerInfo,\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<QString> title) {\n\tif (!peer->isMegagroup()) {\n\t\treturn;\n\t}\n\tconst auto max = !data.topSenders.empty()\n\t\t? data.topSenders.size()\n\t\t: !data.topAdministrators.empty()\n\t\t? data.topAdministrators.size()\n\t\t: !data.topInviters.empty()\n\t\t? data.topInviters.size()\n\t\t: 0;\n\tif (!max) {\n\t\treturn;\n\t}\n\n\tconstexpr auto kPerPage = 40;\n\tstruct State final {\n\t\tState(MembersDescriptor d) : controller(std::move(d)) {\n\t\t}\n\t\tPeerListContentDelegateSimple delegate;\n\t\tMembersController controller;\n\t\tint limit = 0;\n\t};\n\tauto d = MembersDescriptor{\n\t\t&peer->session(),\n\t\tstd::move(showPeerInfo),\n\t\tstd::move(data),\n\t};\n\tconst auto state = container->lifetime().make_state<State>(std::move(d));\n\n\tAddSubtitle(container, std::move(title));\n\n\tstate->delegate.setContent(container->add(\n\t\tobject_ptr<PeerListContent>(container, &state->controller)));\n\tstate->controller.setDelegate(&state->delegate);\n\n\tconst auto wrap = AddShowMoreButton(\n\t\tcontainer,\n\t\ttr::lng_stories_show_more());\n\n\tconst auto showMore = [=] {\n\t\tstate->limit = std::min(int(max), state->limit + kPerPage);\n\t\tstate->controller.setLimit(state->limit);\n\t\tif (state->limit == max) {\n\t\t\twrap->toggle(false, anim::type::instant);\n\t\t}\n\t\tcontainer->resizeToWidth(container->width());\n\t};\n\twrap->entity()->setClickedCallback(showMore);\n\tshowMore();\n}\n\nvoid AddBoostsList(\n\t\tconst Data::BoostsListSlice &firstSlice,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tBoostCallback boostClickedCallback,\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<QString> title) {\n\tconst auto max = firstSlice.multipliedTotal;\n\tstruct State final {\n\t\tState(BoostsDescriptor d) : controller(std::move(d)) {\n\t\t}\n\t\tPeerListContentDelegateSimple delegate;\n\t\tBoostsController controller;\n\t};\n\tauto d = BoostsDescriptor{ firstSlice, boostClickedCallback, peer };\n\tconst auto state = container->lifetime().make_state<State>(std::move(d));\n\n\tstate->delegate.setContent(container->add(\n\t\tobject_ptr<PeerListContent>(container, &state->controller)));\n\tstate->controller.setDelegate(&state->delegate);\n\n\tconst auto wrap = AddShowMoreButton(\n\t\tcontainer,\n\t\t(firstSlice.token.gifts\n\t\t\t? tr::lng_boosts_show_more_gifts\n\t\t\t: tr::lng_boosts_show_more_boosts)(\n\t\t\t\tlt_count,\n\t\t\t\tstate->controller.totalBoostsValue(\n\t\t\t\t) | rpl::map(max - rpl::mappers::_1) | tr::to_count()));\n\n\tconst auto showMore = [=] {\n\t\tif (!state->controller.skipRequest()) {\n\t\t\tstate->controller.requestNext();\n\t\t\tcontainer->resizeToWidth(container->width());\n\t\t}\n\t};\n\twrap->toggleOn(\n\t\tstate->controller.totalBoostsValue(\n\t\t) | rpl::map(rpl::mappers::_1 > 0 && rpl::mappers::_1 < max),\n\t\tanim::type::instant);\n\twrap->entity()->setClickedCallback(showMore);\n}\n\nvoid AddCreditsHistoryList(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tconst Data::CreditsStatusSlice &firstSlice,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tClicked callback,\n\t\tnot_null<PeerData*> bot,\n\t\tbool in,\n\t\tbool out,\n\t\tbool subs) {\n\tstruct State final {\n\t\tState(CreditsDescriptor d) : controller(std::move(d)) {\n\t\t}\n\t\tstd::optional<PeerListContentDelegateShow> creditsDelegate;\n\t\tstd::optional<PeerListWidgetsDelegate> subscriptionDelegate;\n\t\tCreditsController controller;\n\t};\n\tconst auto state = container->lifetime().make_state<State>(\n\t\tCreditsDescriptor{ firstSlice, callback, bot, in, out, subs });\n\tif (subs) {\n\t\tstate->subscriptionDelegate.emplace();\n\t\tstate->subscriptionDelegate->setUiShow(show);\n\t\tstate->subscriptionDelegate->setContent(container->add(\n\t\t\tobject_ptr<PeerListWidgets>(container, &state->controller)));\n\t\tstate->controller.setDelegate(&(*state->subscriptionDelegate));\n\t} else {\n\t\tstate->creditsDelegate.emplace(show);\n\t\tstate->creditsDelegate->setContent(container->add(\n\t\t\tobject_ptr<PeerListContent>(container, &state->controller)));\n\t\tstate->controller.setDelegate(&(*state->creditsDelegate));\n\t}\n\n\tconst auto wrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\tcontainer,\n\t\t\tCreateShowMoreButton(container, tr::lng_stories_show_more())),\n\t\tsubs\n\t\t\t? QMargins()\n\t\t\t: QMargins(0, -st::settingsButton.padding.top(), 0, 0));\n\n\tconst auto showMore = [=] {\n\t\tif (!state->controller.skipRequest()) {\n\t\t\tstate->controller.requestNext();\n\t\t\tcontainer->resizeToWidth(container->width());\n\t\t}\n\t};\n\twrap->toggleOn(\n\t\tstate->controller.allLoadedValue() | rpl::map(!rpl::mappers::_1),\n\t\tanim::type::instant);\n\twrap->entity()->setClickedCallback(showMore);\n}\n\nnot_null<Ui::SlideWrap<Ui::SettingsButton>*> AddShowMoreButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> title) {\n\treturn container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\tcontainer,\n\t\t\tCreateShowMoreButton(container, std::move(title))),\n\t\t{ 0, -st::settingsButton.padding.top(), 0, 0 });\n}\n\n} // namespace Info::Statistics\n"
  },
  {
    "path": "Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\n\nnamespace Ui {\nclass SettingsButton;\ntemplate <typename Widget>\nclass SlideWrap;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Data {\nstruct Boost;\nstruct BoostsListSlice;\nstruct CreditsHistoryEntry;\nstruct CreditsStatusSlice;\nstruct PublicForwardsSlice;\nstruct RecentPostId;\nstruct SubscriptionEntry;\nstruct SupergroupStatistics;\n} // namespace Data\n\nnamespace Main {\nclass SessionShow;\n} // namespace Main\n\nnamespace Info::Statistics {\n\nvoid AddPublicForwards(\n\tconst Data::PublicForwardsSlice &firstSlice,\n\tnot_null<Ui::VerticalLayout*> container,\n\tFn<void(Data::RecentPostId)> requestShow,\n\tnot_null<PeerData*> peer,\n\tData::RecentPostId contextId);\n\nvoid AddMembersList(\n\tData::SupergroupStatistics data,\n\tnot_null<Ui::VerticalLayout*> container,\n\tFn<void(not_null<PeerData*>)> showPeerInfo,\n\tnot_null<PeerData*> peer,\n\trpl::producer<QString> title);\n\nvoid AddBoostsList(\n\tconst Data::BoostsListSlice &firstSlice,\n\tnot_null<Ui::VerticalLayout*> container,\n\tFn<void(const Data::Boost &)> boostClickedCallback,\n\tnot_null<PeerData*> peer,\n\trpl::producer<QString> title);\n\nusing Clicked = Fn<void(\n\tconst Data::CreditsHistoryEntry &,\n\tconst Data::SubscriptionEntry &)>;\nvoid AddCreditsHistoryList(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tconst Data::CreditsStatusSlice &firstSlice,\n\tnot_null<Ui::VerticalLayout*> container,\n\tClicked entryClickedCallback,\n\tnot_null<PeerData*> peer,\n\tbool in,\n\tbool out,\n\tbool subscription = false);\n\n[[nodiscard]] not_null<Ui::SlideWrap<Ui::SettingsButton>*> AddShowMoreButton(\n\tnot_null<Ui::VerticalLayout*> container,\n\trpl::producer<QString> title);\n\n} // namespace Info::Statistics\n"
  },
  {
    "path": "Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/statistics/info_statistics_recent_message.h\"\n\n#include \"base/unixtime.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_story.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/view/history_view_item_preview.h\"\n#include \"info/statistics/info_statistics_common.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/outline_segments.h\" // UnreadStoryOutlineGradient\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_options.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_statistics.h\"\n\nnamespace Info::Statistics {\nnamespace {\n\n[[nodiscard]] QImage PreparePreviewImage(\n\t\tQImage original,\n\t\tImageRoundRadius radius,\n\t\tint size,\n\t\tbool spoiler) {\n\tif (original.width() * 10 < original.height()\n\t\t|| original.height() * 10 < original.width()) {\n\t\treturn QImage();\n\t}\n\tconst auto factor = style::DevicePixelRatio();\n\tsize *= factor;\n\tconst auto scaled = original.scaled(\n\t\tQSize(size, size),\n\t\tQt::KeepAspectRatioByExpanding,\n\t\tQt::FastTransformation);\n\tauto square = scaled.copy(\n\t\t(scaled.width() - size) / 2,\n\t\t(scaled.height() - size) / 2,\n\t\tsize,\n\t\tsize\n\t).convertToFormat(QImage::Format_ARGB32_Premultiplied);\n\tif (spoiler) {\n\t\tsquare = Images::BlurLargeImage(\n\t\t\tstd::move(square),\n\t\t\tstyle::ConvertScale(3) * factor);\n\t}\n\tsquare = Images::Round(std::move(square), radius);\n\tsquare.setDevicePixelRatio(factor);\n\treturn square;\n}\n\n} // namespace\n\nMessagePreview::MessagePreview(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<HistoryItem*> item,\n\tQImage cachedPreview)\n: Ui::RpWidget(parent)\n, _messageId(item->fullId())\n, _date(\n\tst::statisticsHeaderTitleTextStyle,\n\tUi::FormatDateTime(ItemDateTime(item)))\n, _preview(std::move(cachedPreview)) {\n\t_text.setMarkedText(\n\t\tst::defaultPeerListItem.nameStyle,\n\t\titem->toPreview({ .generateImages = false }).text,\n\t\tUi::DialogTextOptions(),\n\t\tCore::TextContext({\n\t\t\t.session = &item->history()->session(),\n\t\t\t.repaint = [=] { update(); },\n\t\t}));\n\tif (item->media() && item->media()->hasSpoiler()) {\n\t\t_spoiler = std::make_unique<Ui::SpoilerAnimation>([=] { update(); });\n\t}\n\tif (_preview.isNull()) {\n\t\tif (const auto media = item->media()) {\n\t\t\tif (const auto photo = media->photo()) {\n\t\t\t\t_photoMedia = photo->createMediaView();\n\t\t\t\t_photoMedia->wanted(Data::PhotoSize::Large, item->fullId());\n\t\t\t} else if (const auto document = media->document()) {\n\t\t\t\t_documentMedia = document->createMediaView();\n\t\t\t\t_documentMedia->thumbnailWanted(item->fullId());\n\t\t\t}\n\t\t\tprocessPreview();\n\t\t}\n\t\tif ((!_documentMedia || _documentMedia->thumbnailSize().isNull())\n\t\t\t&& !_photoMedia) {\n\t\t\tconst auto userpic = Ui::CreateChild<Ui::UserpicButton>(\n\t\t\t\tthis,\n\t\t\t\titem->history()->peer,\n\t\t\t\tst::statisticsRecentPostUserpic);\n\t\t\tuserpic->move(st::peerListBoxItem.photoPosition);\n\t\t\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t}\n\t}\n}\n\nMessagePreview::MessagePreview(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Data::Story*> story,\n\tQImage cachedPreview)\n: Ui::RpWidget(parent)\n, _storyId(story->fullId())\n, _date(\n\tst::statisticsHeaderTitleTextStyle,\n\tUi::FormatDateTime(base::unixtime::parse(story->date())))\n, _preview(std::move(cachedPreview)) {\n\t_text.setMarkedText(\n\t\tst::defaultPeerListItem.nameStyle,\n\t\t{ tr::lng_in_dlg_story(tr::now) },\n\t\tUi::DialogTextOptions(),\n\t\tCore::TextContext({\n\t\t\t.session = &story->peer()->session(),\n\t\t\t.repaint = [=] { update(); },\n\t\t}));\n\tif (_preview.isNull()) {\n\t\tif (const auto photo = story->photo()) {\n\t\t\t_photoMedia = photo->createMediaView();\n\t\t\t_photoMedia->wanted(Data::PhotoSize::Large, story->fullId());\n\t\t} else if (const auto document = story->document()) {\n\t\t\t_documentMedia = document->createMediaView();\n\t\t\t_documentMedia->thumbnailWanted(story->fullId());\n\t\t}\n\t\tprocessPreview();\n\t}\n}\n\nvoid MessagePreview::setInfo(int views, int shares, int reactions) {\n\t_views = Ui::Text::String(\n\t\tst::defaultPeerListItem.nameStyle,\n\t\t(views >= 0)\n\t\t\t? tr::lng_stats_recent_messages_views(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\tviews)\n\t\t\t: QString());\n\t_shares = Ui::Text::String(\n\t\tst::statisticsHeaderTitleTextStyle,\n\t\t(shares > 0) ? Lang::FormatCountDecimal(shares) : QString());\n\t_reactions = Ui::Text::String(\n\t\tst::statisticsHeaderTitleTextStyle,\n\t\t(reactions > 0) ? Lang::FormatCountDecimal(reactions) : QString());\n\t_viewsWidth = (_views.maxWidth());\n\t_sharesWidth = (_shares.maxWidth());\n\t_reactionsWidth = (_reactions.maxWidth());\n}\n\nvoid MessagePreview::processPreview() {\n\tconst auto session = _photoMedia\n\t\t? &_photoMedia->owner()->session()\n\t\t: _documentMedia\n\t\t? &_documentMedia->owner()->session()\n\t\t: nullptr;\n\tif (!session) {\n\t\treturn;\n\t}\n\n\tstruct ThumbInfo final {\n\t\tbool loaded = false;\n\t\tImage *image = nullptr;\n\t};\n\n\tconst auto computeThumbInfo = [=]() -> ThumbInfo {\n\t\tusing Size = Data::PhotoSize;\n\t\tif (_documentMedia) {\n\t\t\treturn { true, _documentMedia->thumbnail() };\n\t\t} else if (const auto large = _photoMedia->image(Size::Large)) {\n\t\t\treturn { true, large };\n\t\t} else if (const auto thumb = _photoMedia->image(Size::Thumbnail)) {\n\t\t\treturn { false, thumb };\n\t\t} else if (const auto small = _photoMedia->image(Size::Small)) {\n\t\t\treturn { false, small };\n\t\t} else {\n\t\t\treturn { false, _photoMedia->thumbnailInline() };\n\t\t}\n\t};\n\n\trpl::single(rpl::empty) | rpl::then(\n\t\tsession->downloaderTaskFinished()\n\t) | rpl::on_next([=] {\n\t\tconst auto computed = computeThumbInfo();\n\t\tconst auto guard = gsl::finally([&] { update(); });\n\t\tif (!computed.image) {\n\t\t\tif (_documentMedia && !_documentMedia->owner()->hasThumbnail()) {\n\t\t\t\t_preview = QImage();\n\t\t\t\t_lifetimeDownload.destroy();\n\t\t\t}\n\t\t\treturn;\n\t\t} else if (computed.loaded) {\n\t\t\t_lifetimeDownload.destroy();\n\t\t}\n\t\tif (_storyId) {\n\t\t\tconst auto line = st::dialogsStoriesFull.lineTwice;\n\t\t\tconst auto rect = Rect(Size(st::peerListBoxItem.photoSize));\n\t\t\tconst auto penWidth = line / 2.;\n\t\t\tconst auto offset = 1.5 * penWidth * 2;\n\t\t\tconst auto preview = PreparePreviewImage(\n\t\t\t\tcomputed.image->original(),\n\t\t\t\tImageRoundRadius::Ellipse,\n\t\t\t\tst::peerListBoxItem.photoSize - offset * 2,\n\t\t\t\t!!_spoiler);\n\t\t\tauto image = QImage(\n\t\t\t\trect.size() * style::DevicePixelRatio(),\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\timage.fill(Qt::transparent);\n\t\t\t{\n\t\t\t\tauto p = QPainter(&image);\n\t\t\t\tp.drawImage(offset, offset, preview);\n\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\tauto gradient = Ui::UnreadStoryOutlineGradient();\n\t\t\t\tgradient.setStart(rect.topRight());\n\t\t\t\tgradient.setFinalStop(rect.bottomLeft());\n\n\t\t\t\tp.setPen(QPen(gradient, penWidth));\n\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t\tp.drawEllipse(rect - Margins(penWidth));\n\t\t\t}\n\t\t\t_preview = std::move(image);\n\t\t} else {\n\t\t\t_preview = PreparePreviewImage(\n\t\t\t\tcomputed.image->original(),\n\t\t\t\tImageRoundRadius::Large,\n\t\t\t\tst::peerListBoxItem.photoSize,\n\t\t\t\t!!_spoiler);\n\t\t}\n\t}, _lifetimeDownload);\n}\n\nint MessagePreview::resizeGetHeight(int newWidth) {\n\treturn st::peerListBoxItem.height;\n}\n\nvoid MessagePreview::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto padding = st::boxRowPadding.left() / 2;\n\tconst auto rightSubTextWidth = 0\n\t\t+ (_sharesWidth\n\t\t\t? _sharesWidth\n\t\t\t\t+ st::statisticsRecentPostShareIcon.width()\n\t\t\t\t+ st::statisticsRecentPostIconSkip\n\t\t\t: 0)\n\t\t+ (_reactionsWidth\n\t\t\t? _reactionsWidth\n\t\t\t\t+ st::statisticsRecentPostReactionIcon.width()\n\t\t\t\t+ st::statisticsChartRulerCaptionSkip\n\t\t\t\t+ st::statisticsRecentPostIconSkip\n\t\t\t: 0);\n\tconst auto rightWidth = std::max(_viewsWidth, rightSubTextWidth)\n\t\t+ padding;\n\tconst auto left = (false && _preview.isNull())\n\t\t? st::peerListBoxItem.photoPosition.x()\n\t\t: st::peerListBoxItem.namePosition.x();\n\tif (left) {\n\t\tconst auto rect = QRect(\n\t\t\tst::peerListBoxItem.photoPosition,\n\t\t\tSize(st::peerListBoxItem.photoSize));\n\t\tp.drawImage(rect.topLeft(), _preview);\n\t\tif (_spoiler) {\n\t\t\tconst auto paused = On(PowerSaving::kChatSpoiler);\n\t\t\tFillSpoilerRect(\n\t\t\t\tp,\n\t\t\t\trect,\n\t\t\t\tImages::CornersMaskRef(\n\t\t\t\t\tImages::CornersMask(st::roundRadiusLarge)),\n\t\t\t\tUi::DefaultImageSpoiler().frame(\n\t\t\t\t\t_spoiler->index(crl::now(), paused)),\n\t\t\t\t_cornerCache);\n\t\t}\n\t}\n\tconst auto topTextTop = st::peerListBoxItem.namePosition.y();\n\tconst auto bottomTextTop = st::peerListBoxItem.statusPosition.y();\n\n\tp.setBrush(Qt::NoBrush);\n\tp.setPen(st::boxTextFg);\n\t_text.draw(p, {\n\t\t.position = { left, topTextTop },\n\t\t.outerWidth = width() - left,\n\t\t.availableWidth = width() - rightWidth - left,\n\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t.now = crl::now(),\n\t\t.elisionHeight = st::statisticsDetailsPopupHeaderStyle.font->height,\n\t\t.elisionLines = 1,\n\t});\n\t_views.draw(p, {\n\t\t.position = { width() - _viewsWidth, topTextTop },\n\t\t.outerWidth = _viewsWidth,\n\t\t.availableWidth = _viewsWidth,\n\t});\n\n\tp.setPen(st::windowSubTextFg);\n\t_date.draw(p, {\n\t\t.position = { left, bottomTextTop },\n\t\t.outerWidth = width() - left,\n\t\t.availableWidth = width() - rightWidth - left,\n\t});\n\t{\n\t\tauto right = width() - _sharesWidth;\n\t\t_shares.draw(p, {\n\t\t\t.position = { right, bottomTextTop },\n\t\t\t.outerWidth = _sharesWidth,\n\t\t\t.availableWidth = _sharesWidth,\n\t\t});\n\t\tconst auto bottomTextBottom = bottomTextTop\n\t\t\t+ st::statisticsHeaderTitleTextStyle.font->height\n\t\t\t- st::statisticsRecentPostIconSkip;\n\t\tif (_sharesWidth) {\n\t\t\tconst auto &icon = st::statisticsRecentPostShareIcon;\n\t\t\tconst auto iconTop = bottomTextBottom - icon.height();\n\t\t\tright -= st::statisticsRecentPostIconSkip + icon.width();\n\t\t\ticon.paint(p, { right, iconTop }, width());\n\t\t}\n\t\tright -= _reactionsWidth + st::statisticsChartRulerCaptionSkip;\n\t\t_reactions.draw(p, {\n\t\t\t.position = { right, bottomTextTop },\n\t\t\t.outerWidth = _reactionsWidth,\n\t\t\t.availableWidth = _reactionsWidth,\n\t\t});\n\t\tif (_reactionsWidth) {\n\t\t\tconst auto &icon = st::statisticsRecentPostReactionIcon;\n\t\t\tconst auto iconTop = bottomTextBottom - icon.height();\n\t\t\tright -= st::statisticsRecentPostIconSkip + icon.width();\n\t\t\ticon.paint(p, { right, iconTop }, width());\n\t\t}\n\t}\n}\n\nvoid MessagePreview::saveState(SavedState &state) const {\n\tif (!_lifetimeDownload) {\n\t\tconst auto fullId = Data::RecentPostId{ _messageId, _storyId };\n\t\tstate.recentPostPreviews[fullId] = _preview;\n\t}\n}\n\n} // namespace Info::Statistics\n"
  },
  {
    "path": "Telegram/SourceFiles/info/statistics/info_statistics_recent_message.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nclass HistoryItem;\n\nnamespace Data {\nclass DocumentMedia;\nclass PhotoMedia;\nclass Story;\n} // namespace Data\n\nnamespace Ui {\nclass SpoilerAnimation;\n} // namespace Ui\n\nnamespace Info::Statistics {\n\nstruct SavedState;\n\nclass MessagePreview final : public Ui::RpWidget {\npublic:\n\tMessagePreview(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<HistoryItem*> item,\n\t\tQImage cachedPreview);\n\tMessagePreview(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Data::Story*> story,\n\t\tQImage cachedPreview);\n\n\tvoid setInfo(int views, int shares, int reactions);\n\tvoid saveState(SavedState &state) const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tvoid processPreview();\n\n\tFullMsgId _messageId;\n\tFullStoryId _storyId;\n\tUi::Text::String _text;\n\tUi::Text::String _date;\n\tUi::Text::String _views;\n\tUi::Text::String _shares;\n\tUi::Text::String _reactions;\n\n\tint _viewsWidth = 0;\n\tint _sharesWidth = 0;\n\tint _reactionsWidth = 0;\n\n\tQImage _cornerCache;\n\tQImage _preview;\n\n\tstd::shared_ptr<Data::PhotoMedia> _photoMedia;\n\tstd::shared_ptr<Data::DocumentMedia> _documentMedia;\n\tstd::unique_ptr<Ui::SpoilerAnimation> _spoiler;\n\n\trpl::lifetime _lifetimeDownload;\n\n};\n\n} // namespace Info::Statistics\n"
  },
  {
    "path": "Telegram/SourceFiles/info/statistics/info_statistics_tag.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\n\nnamespace Info::Statistics {\n\nstruct Tag final {\n\texplicit Tag() = default;\n\texplicit Tag(\n\t\tPeerData *peer,\n\t\tFullMsgId contextId,\n\t\tFullStoryId storyId)\n\t: peer(peer)\n\t, contextId(contextId)\n\t, storyId(storyId) {\n\t}\n\n\tPeerData *peer = nullptr;\n\tFullMsgId contextId;\n\tFullStoryId storyId;\n\n};\n\n} // namespace Info::Statistics\n"
  },
  {
    "path": "Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/statistics/info_statistics_widget.h\"\n\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"info/statistics/info_statistics_inner_widget.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/ui_utility.h\"\n\nnamespace Info::Statistics {\n\nMemento::Memento(not_null<Controller*> controller)\n: ContentMemento(controller->statisticsTag()) {\n}\n\nMemento::Memento(not_null<PeerData*> peer, FullMsgId contextId)\n: ContentMemento(Tag{ peer, contextId, {} }) {\n}\n\nMemento::Memento(not_null<PeerData*> peer, FullStoryId storyId)\n: ContentMemento(Tag{ peer, {}, storyId }) {\n}\n\nMemento::~Memento() = default;\n\nSection Memento::section() const {\n\treturn Section(Section::Type::Statistics);\n}\n\nvoid Memento::setState(SavedState state) {\n\t_state = std::move(state);\n}\n\nSavedState Memento::state() {\n\treturn base::take(_state);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(parent, controller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: ContentWidget(parent, controller)\n, _inner(setInnerWidget(\n\tobject_ptr<InnerWidget>(\n\t\tthis,\n\t\tcontroller,\n\t\tcontroller->statisticsTag().peer,\n\t\tcontroller->statisticsTag().contextId,\n\t\tcontroller->statisticsTag().storyId))) {\n\t_inner->showRequests(\n\t) | rpl::on_next([=](InnerWidget::ShowRequest request) {\n\t\tif (request.history) {\n\t\t\tcontroller->showPeerHistory(\n\t\t\t\trequest.history.peer,\n\t\t\t\tWindow::SectionShow::Way::Forward,\n\t\t\t\trequest.history.msg);\n\t\t} else if (request.info) {\n\t\t\tcontroller->showPeerInfo(request.info);\n\t\t} else if (request.messageStatistic || request.storyStatistic) {\n\t\t\tcontroller->showSection(Make(\n\t\t\t\tcontroller->statisticsTag().peer,\n\t\t\t\trequest.messageStatistic,\n\t\t\t\trequest.storyStatistic));\n\t\t} else if (const auto &s = request.story) {\n\t\t\tif (const auto peer = controller->session().data().peer(s.peer)) {\n\t\t\t\tcontroller->parentController()->openPeerStory(\n\t\t\t\t\tpeer,\n\t\t\t\t\ts.story,\n\t\t\t\t\t{ Data::StoriesContextSingle() });\n\t\t\t}\n\t\t}\n\t}, _inner->lifetime());\n\t_inner->scrollToRequests(\n\t) | rpl::on_next([this](const Ui::ScrollToRequest &request) {\n\t\tscrollTo(request);\n\t}, _inner->lifetime());\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\treturn false;\n}\n\nrpl::producer<QString> Widget::title() {\n\treturn controller()->statisticsTag().contextId\n\t\t? tr::lng_stats_message_title()\n\t\t: controller()->statisticsTag().storyId\n\t\t? tr::lng_stats_story_title()\n\t\t: tr::lng_stats_title();\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nrpl::producer<bool> Widget::desiredShadowVisibility() const {\n\treturn rpl::single<bool>(true);\n}\n\nvoid Widget::showFinished() {\n\t_inner->showFinished();\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(controller());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\nstd::shared_ptr<Info::Memento> Make(\n\t\tnot_null<PeerData*> peer,\n\t\tFullMsgId contextId,\n\t\tFullStoryId storyId) {\n\tconst auto memento = storyId\n\t\t? std::make_shared<Memento>(peer, storyId)\n\t\t: std::make_shared<Memento>(peer, contextId);\n\treturn std::make_shared<Info::Memento>(\n\t\tstd::vector<std::shared_ptr<ContentMemento>>(1, std::move(memento)));\n}\n\n} // namespace Info::Statistics\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/statistics/info_statistics_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n#include \"info/statistics/info_statistics_common.h\"\n\nnamespace Info::Statistics {\n\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\tMemento(not_null<Controller*> controller);\n\tMemento(not_null<PeerData*> peer, FullMsgId contextId);\n\tMemento(not_null<PeerData*> peer, FullStoryId storyId);\n\t~Memento();\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\tvoid setState(SavedState states);\n\t[[nodiscard]] SavedState state();\n\nprivate:\n\tSavedState _state;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(QWidget *parent, not_null<Controller*> controller);\n\n\tbool showInternal(not_null<ContentMemento*> memento) override;\n\trpl::producer<QString> title() override;\n\trpl::producer<bool> desiredShadowVisibility() const override;\n\tvoid showFinished() override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tconst not_null<InnerWidget*> _inner;\n\n};\n\n[[nodiscard]] std::shared_ptr<Info::Memento> Make(\n\tnot_null<PeerData*> peer,\n\tFullMsgId contextId,\n\tFullStoryId storyId);\n\n} // namespace Info::Statistics\n"
  },
  {
    "path": "Telegram/SourceFiles/info/stories/info_stories_albums.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/stories/info_stories_albums.h\"\n#include \"apiwrap.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_story.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info::Stories {\nnamespace {\n\nconstexpr auto kAlbumNameLimit = 12;\n\nvoid EditAlbumBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tint id,\n\t\tStoryId addId,\n\t\tQString currentName,\n\t\tFn<void(Data::StoryAlbum)> finished) {\n\tbox->setTitle(id\n\t\t? tr::lng_stories_album_edit()\n\t\t: tr::lng_stories_album_new_title());\n\n\tif (!id) {\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_stories_album_new_text(),\n\t\t\t\tst::collectionAbout));\n\t}\n\tconst auto title = box->addRow(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tbox,\n\t\t\tst::collectionNameField,\n\t\t\ttr::lng_stories_album_new_ph(),\n\t\t\tcurrentName));\n\ttitle->setMaxLength(kAlbumNameLimit * 2);\n\tbox->setFocusCallback([=] {\n\t\ttitle->setFocusFast();\n\t});\n\n\tUi::AddLengthLimitLabel(title, kAlbumNameLimit);\n\n\tconst auto show = navigation->uiShow();\n\tconst auto session = &peer->session();\n\n\tconst auto creating = std::make_shared<bool>(false);\n\tconst auto submit = [=] {\n\t\tif (*creating) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto text = title->getLastText().trimmed();\n\t\tif (text.isEmpty() || text.size() > kAlbumNameLimit) {\n\t\t\ttitle->showError();\n\t\t\treturn;\n\t\t}\n\n\t\t*creating = true;\n\t\tconst auto weak = base::make_weak(box);\n\t\tconst auto done = [=](Data::StoryAlbum result) {\n\t\t\t*creating = false;\n\t\t\tif (const auto onstack = finished) {\n\t\t\t\tonstack(result);\n\t\t\t}\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t};\n\t\tconst auto fail = [=](QString type) {\n\t\t\t*creating = false;\n\t\t\tif (type == u\"ALBUMS_TOO_MANY\"_q) {\n\t\t\t\tshow->show(Ui::MakeInformBox({\n\t\t\t\t\t.text = tr::lng_stories_album_limit_text(),\n\t\t\t\t\t.confirmText = tr::lng_box_ok(),\n\t\t\t\t\t.title = tr::lng_stories_album_limit_title(),\n\t\t\t\t}));\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->closeBox();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tshow->showToast(type);\n\t\t\t}\n\t\t};\n\t\tif (id) {\n\t\t\tsession->data().stories().albumRename(\n\t\t\t\tpeer,\n\t\t\t\tid,\n\t\t\t\ttext,\n\t\t\t\tdone,\n\t\t\t\tfail);\n\t\t} else {\n\t\t\tsession->data().stories().albumCreate(\n\t\t\t\tpeer,\n\t\t\t\ttext,\n\t\t\t\taddId,\n\t\t\t\tdone,\n\t\t\t\tfail);\n\t\t}\n\t};\n\ttitle->submits() | rpl::on_next(submit, title->lifetime());\n\tauto text = id\n\t\t? tr::lng_settings_save()\n\t\t: tr::lng_stories_album_new_create();\n\tbox->addButton(std::move(text), submit);\n\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\n} // namespace\n\nvoid NewAlbumBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tStoryId addId,\n\t\tFn<void(Data::StoryAlbum)> added) {\n\tEditAlbumBox(box, navigation, peer, 0, addId, QString(), added);\n}\n\nvoid EditAlbumNameBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tint id,\n\t\tQString current,\n\t\tFn<void(QString)> done) {\n\tEditAlbumBox(box, navigation, peer, id, {}, current, [=](\n\t\t\tData::StoryAlbum result) {\n\t\tdone(result.title);\n\t});\n}\n\n} // namespace Info::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/info/stories/info_stories_albums.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nstruct StoryAlbum;\n} // namespace Data\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nnamespace Info::Stories {\n\nvoid NewAlbumBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tStoryId addId,\n\tFn<void(Data::StoryAlbum)> added);\n\nvoid EditAlbumNameBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tint id,\n\tQString current,\n\tFn<void(QString)> done);\n\n} // namespace Info::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/info/stories/info_stories_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Info::Stories {\n\n[[nodiscard]] int ArchiveId();\n\nstruct Tag {\n\texplicit Tag(\n\t\tnot_null<PeerData*> peer,\n\t\tint albumId = 0,\n\t\tint addingToAlbumId = 0)\n\t: peer(peer)\n\t, albumId(albumId)\n\t, addingToAlbumId(addingToAlbumId) {\n\t}\n\n\tnot_null<PeerData*> peer;\n\tint albumId = 0;\n\tint addingToAlbumId = 0;\n};\n\n} // namespace Info::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/stories/info_stories_inner_widget.h\"\n\n#include \"apiwrap.h\"\n#include \"boxes/share_box.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_saved_music.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_user.h\"\n#include \"dialogs/ui/dialogs_stories_content.h\"\n#include \"dialogs/ui/dialogs_stories_list.h\"\n#include \"info/media/info_media_buttons.h\"\n#include \"info/media/info_media_list_widget.h\"\n#include \"info/peer_gifts/info_peer_gifts_widget.h\"\n#include \"info/profile/info_profile_actions.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"info/profile/info_profile_top_bar.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"info/profile/info_profile_widget.h\"\n#include \"info/saved/info_saved_music_common.h\"\n#include \"info/stories/info_stories_albums.h\"\n#include \"info/stories/info_stories_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/sender.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/sub_tabs.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Info::Stories {\nnamespace {\n\nclass EditAlbumBox final : public Ui::BoxContent {\npublic:\n\tEditAlbumBox(\n\t\tQWidget*,\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void()> reload,\n\t\tint albumId);\n\nprivate:\n\tvoid prepare() override;\n\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\tconst not_null<Window::SessionController*> _window;\n\tconst not_null<WrapWidget*> _content;\n\trpl::variable<Data::StoryAlbumUpdate> _changes;\n\tFn<void()> _reload;\n\tbool _saving = false;\n\n};\n\nEditAlbumBox::EditAlbumBox(\n\tQWidget*,\n\tnot_null<Controller*> controller,\n\tnot_null<PeerData*> peer,\n\tFn<void()> reload,\n\tint albumId)\n: _window(controller->parentController())\n, _content(Ui::CreateChild<WrapWidget>(\n\tthis,\n\t_window,\n\tWrap::StoryAlbumEdit,\n\tstd::make_shared<Info::Memento>(\n\t\tstd::vector<std::shared_ptr<ContentMemento>>(\n\t\t\t1,\n\t\t\tstd::make_shared<Memento>(\n\t\t\t\tpeer,\n\t\t\t\tData::kStoriesAlbumIdArchive,\n\t\t\t\talbumId))).get()))\n, _changes(Data::StoryAlbumUpdate{ .peer = peer, .albumId = albumId })\n, _reload(std::move(reload)) {\n\t_content->selectedListValue(\n\t) | rpl::on_next([=](const SelectedItems &selection) {\n\t\tconst auto stories = &_window->session().data().stories();\n\t\tauto ids = stories->albumKnownInArchive(peer->id, albumId);\n\t\tauto now = _changes.current();\n\t\tnow.added.clear();\n\t\tnow.added.reserve(selection.list.size());\n\t\tnow.removed.clear();\n\t\tnow.removed.reserve(ids.size());\n\t\tfor (const auto &entry : selection.list) {\n\t\t\tconst auto id = StoryIdFromMsgId(entry.globalId.itemId.msg);\n\t\t\tif (!ids.remove(id)) {\n\t\t\t\tnow.added.push_back(id);\n\t\t\t}\n\t\t}\n\t\tfor (const auto id : ids) {\n\t\t\tnow.removed.push_back(id);\n\t\t}\n\t\t_changes = std::move(now);\n\t}, lifetime());\n}\n\nvoid EditAlbumBox::prepare() {\n\tsetTitle(tr::lng_stories_album_add_title());\n\tsetStyle(st::collectionEditBox);\n\n\t_content->desiredHeightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tsetDimensions(st::boxWideWidth, height);\n\t}, _content->lifetime());\n\n\taddTopButton(st::boxTitleClose, [=] {\n\t\tcloseBox();\n\t});\n\n\tconst auto weakBox = base::make_weak(this);\n\tauto text = _changes.value(\n\t) | rpl::map([=](const Data::StoryAlbumUpdate &update) {\n\t\treturn (!update.added.empty() && update.removed.empty())\n\t\t\t? tr::lng_stories_album_add_button()\n\t\t\t: tr::lng_settings_save();\n\t}) | rpl::flatten_latest();\n\taddButton(std::move(text), [=] {\n\t\tif (_saving) {\n\t\t\treturn;\n\t\t}\n\t\tauto add = QVector<MTPint>();\n\t\tauto remove = QVector<MTPint>();\n\t\tconst auto &changes = _changes.current();\n\t\tfor (const auto &id : changes.added) {\n\t\t\tadd.push_back(MTP_int(id));\n\t\t}\n\t\tfor (const auto &id : changes.removed) {\n\t\t\tremove.push_back(MTP_int(id));\n\t\t}\n\t\tif (add.empty() && remove.empty()) {\n\t\t\tcloseBox();\n\t\t\treturn;\n\t\t}\n\t\t_saving = true;\n\t\tconst auto session = &_window->session();\n\t\tconst auto reload = _reload;\n\t\tusing Flag = MTPstories_UpdateAlbum::Flag;\n\t\tsession->api().request(\n\t\t\tMTPstories_UpdateAlbum(\n\t\t\t\tMTP_flags(Flag()\n\t\t\t\t\t| (add.isEmpty() ? Flag() : Flag::f_add_stories)\n\t\t\t\t\t| (remove.isEmpty()\n\t\t\t\t\t\t? Flag()\n\t\t\t\t\t\t: Flag::f_delete_stories)),\n\t\t\t\tchanges.peer->input(),\n\t\t\t\tMTP_int(changes.albumId),\n\t\t\t\tMTPstring(),\n\t\t\t\tMTP_vector<MTPint>(remove),\n\t\t\t\tMTP_vector<MTPint>(add),\n\t\t\t\tMTPVector<MTPint>())\n\t\t).done([=] {\n\t\t\tif (const auto strong = weakBox.get()) {\n\t\t\t\tstrong->_saving = false;\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t\tsession->data().stories().notifyAlbumUpdate(\n\t\t\t\tbase::duplicate(changes));\n\t\t\tif (const auto onstack = reload) {\n\t\t\t\tonstack();\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tif (const auto strong = weakBox.get()) {\n\t\t\t\tstrong->_saving = false;\n\t\t\t\tstrong->uiShow()->showToast(error.type());\n\t\t\t}\n\t\t}).send();\n\t});\n}\n\nvoid EditAlbumBox::resizeEvent(QResizeEvent *e) {\n\t_content->setGeometry(rect());\n}\n\n} // namespace\n\nInnerWidget::InnerWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller,\n\trpl::producer<int> albumId,\n\tint addingToAlbumId)\n: RpWidget(parent)\n, _controller(controller)\n, _peer(controller->key().storiesPeer())\n, _addingToAlbumId(addingToAlbumId)\n, _albumId(std::move(albumId))\n, _albumChanges(Data::StoryAlbumUpdate{\n\t.peer = _peer,\n\t.albumId = _addingToAlbumId,\n})\n, _api(std::make_unique<MTP::Sender>(&_peer->session().mtp())) {\n\tpreloadArchiveCount();\n\n\t_albumId.value(\n\t) | rpl::on_next([=](int albumId) {\n\t\tif (_albumsTabs\n\t\t\t&& (albumId == Data::kStoriesAlbumIdSaved\n\t\t\t\t|| ranges::contains(\n\t\t\t\t\t_albums,\n\t\t\t\t\talbumId,\n\t\t\t\t\t&Data::StoryAlbum::id))) {\n\t\t\t_albumsTabs->setActiveTab((albumId == Data::kStoriesAlbumIdSaved)\n\t\t\t\t? u\"all\"_q\n\t\t\t\t: QString::number(albumId));\n\t\t}\n\t\t_controller->replaceKey(Key(Tag(_peer, albumId, _addingToAlbumId)));\n\t\treload();\n\t}, lifetime());\n}\n\nvoid InnerWidget::preloadArchiveCount() {\n\tconstexpr auto kArchive = Data::kStoriesAlbumIdArchive;\n\tconst auto stories = &_peer->owner().stories();\n\tif (!_peer->canEditStories()\n\t\t|| stories->albumIdsCountKnown(_peer->id, kArchive)) {\n\t\treturn;\n\t}\n\tconst auto key = Data::StoryAlbumIdsKey{ _peer->id, kArchive };\n\tstories->albumIdsLoadMore(_peer->id, kArchive);\n\tstories->albumIdsChanged() | rpl::filter(\n\t\trpl::mappers::_1 == key\n\t) | rpl::take_while([=] {\n\t\treturn !stories->albumIdsCountKnown(_peer->id, kArchive);\n\t}) | rpl::on_next([=] {\n\t\trefreshAlbumsTabs();\n\t}, lifetime());\n}\n\nvoid InnerWidget::setupAlbums() {\n\tUi::AddSkip(_top);\n\t_albumsWrap = _top->add(object_ptr<Ui::BoxContentDivider>(_top));\n\n\t_peer->owner().stories().albumsListValue(\n\t\t_peer->id\n\t) | rpl::on_next([=](std::vector<Data::StoryAlbum> &&albums) {\n\t\t_albums = std::move(albums);\n\t\trefreshAlbumsTabs();\n\t}, lifetime());\n}\n\nInnerWidget::~InnerWidget() = default;\n\nrpl::producer<> InnerWidget::backRequest() const {\n\treturn _backClicks.events();\n}\n\nvoid InnerWidget::setupTop() {\n\tconst auto albumId = _albumId.current();\n\tif (_addingToAlbumId) {\n\t\treturn;\n\t} else if (albumId == Data::kStoriesAlbumIdArchive) {\n\t\tcreateAboutArchive();\n\t} else if (_isStackBottom) {\n\t\tif (_peer->isSelf()) {\n\t\t\tcreateProfileTop();\n\t\t} else if (_peer->owner().stories().hasArchive(_peer)) {\n\t\t\tcreateButtons();\n\t\t} else {\n\t\t\tstartTop();\n\t\t\tfinalizeTop();\n\t\t}\n\t} else {\n\t\tstartTop();\n\t\tfinalizeTop();\n\t}\n}\n\nvoid InnerWidget::startTop() {\n\t_albumsTabs = nullptr;\n\t_albumsWrap = nullptr;\n\t_top.create(this);\n\t_top->show();\n\t_topHeight = _top->heightValue();\n}\n\nvoid InnerWidget::createProfileTop() {\n\tstartTop();\n\n\tInfo::Saved::SetupSavedMusic(\n\t\t_top,\n\t\t_controller,\n\t\t_peer,\n\t\t_topBarColor.value());\n\n\tusing namespace Profile;\n\tauto mainTracker = Ui::MultiSlideTracker();\n\tauto dividerOverridden = rpl::variable<bool>(false);\n\tAddDetails(\n\t\t_top,\n\t\t_controller,\n\t\t_peer,\n\t\tnullptr,\n\t\tnullptr,\n\t\t{ v::null },\n\t\tmainTracker,\n\t\tdividerOverridden);\n\n\tauto tracker = Ui::MultiSlideTracker();\n\tconst auto dividerWrap = _top->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_top,\n\t\t\tobject_ptr<Ui::VerticalLayout>(_top)));\n\tconst auto divider = dividerWrap->entity();\n\tUi::AddDivider(divider);\n\tUi::AddSkip(divider);\n\n\taddGiftsButton(tracker);\n\taddArchiveButton(tracker);\n\taddRecentButton(tracker);\n\n\tdividerWrap->toggleOn(tracker.atLeastOneShownValue());\n\n\tfinalizeTop();\n}\n\nvoid InnerWidget::createButtons() {\n\tstartTop();\n\tauto tracker = Ui::MultiSlideTracker();\n\taddArchiveButton(tracker);\n\taddRecentButton(tracker);\n\tfinalizeTop();\n}\n\nvoid InnerWidget::addArchiveButton(Ui::MultiSlideTracker &tracker) {\n\tExpects(_top != nullptr);\n\n\tconst auto stories = &_peer->owner().stories();\n\n\tconstexpr auto kArchive = Data::kStoriesAlbumIdArchive;\n\tif (!stories->albumIdsCountKnown(_peer->id, kArchive)) {\n\t\tstories->albumIdsLoadMore(_peer->id, kArchive);\n\t}\n\n\tauto count = rpl::single(\n\t\trpl::empty\n\t) | rpl::then(\n\t\tstories->albumIdsChanged(\n\t\t) | rpl::filter(\n\t\t\trpl::mappers::_1 == Data::StoryAlbumIdsKey{ _peer->id, kArchive }\n\t\t) | rpl::to_empty\n\t) | rpl::map([=] {\n\t\treturn stories->albumIdsCount(_peer->id, kArchive);\n\t}) | rpl::start_spawning(_top->lifetime());\n\n\tconst auto archiveWrap = _top->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\t_top,\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\t_top,\n\t\t\t\ttr::lng_stories_archive_button(),\n\t\t\t\tst::infoSharedMediaButton))\n\t)->setDuration(\n\t\tst::infoSlideDuration\n\t)->toggleOn(rpl::duplicate(count) | rpl::map(rpl::mappers::_1 > 0));\n\n\tconst auto archive = archiveWrap->entity();\n\tarchive->addClickHandler([=] {\n\t\t_controller->showSection(Make(_peer, kArchive));\n\t});\n\tauto label = rpl::duplicate(\n\t\tcount\n\t) | rpl::filter(\n\t\trpl::mappers::_1 > 0\n\t) | rpl::map([=](int count) {\n\t\treturn (count > 0) ? QString::number(count) : QString();\n\t});\n\t::Settings::CreateRightLabel(\n\t\tarchive,\n\t\tstd::move(label),\n\t\tst::infoSharedMediaButton,\n\t\ttr::lng_stories_archive_button());\n\tobject_ptr<Profile::FloatingIcon>(\n\t\tarchive,\n\t\tst::infoIconMediaStoriesArchive,\n\t\tst::infoSharedMediaButtonIconPosition)->show();\n\ttracker.track(archiveWrap);\n}\n\nvoid InnerWidget::addRecentButton(Ui::MultiSlideTracker &tracker) {\n\tExpects(_top != nullptr);\n\n\tconst auto recentWrap = _top->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\t_top,\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\t_top,\n\t\t\t\ttr::lng_stories_recent_button(),\n\t\t\t\tst::infoSharedMediaButton)));\n\n\tusing namespace Dialogs::Stories;\n\tauto last = LastForPeer(\n\t\t_peer\n\t) | rpl::map([=](Content &&content) {\n\t\tfor (auto &element : content.elements) {\n\t\t\telement.unreadCount = 0;\n\t\t}\n\t\treturn std::move(content);\n\t}) | rpl::start_spawning(recentWrap->lifetime());\n\tconst auto recent = recentWrap->entity();\n\tconst auto thumbs = Ui::CreateChild<List>(\n\t\trecent,\n\t\tst::dialogsStoriesListMine,\n\t\trpl::duplicate(last) | rpl::filter([](const Content &content) {\n\t\t\treturn !content.elements.empty();\n\t\t}));\n\tthumbs->show();\n\trpl::combine(\n\t\trecent->sizeValue(),\n\t\trpl::duplicate(last)\n\t) | rpl::on_next([=](QSize size, const Content &content) {\n\t\tif (content.elements.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto &small = st::dialogsStories;\n\t\tconst auto height = small.photo + 2 * small.photoTop;\n\t\tconst auto top = (size.height() - height) / 2;\n\t\tconst auto right = st::settingsButtonRightSkip\n\t\t\t- small.left\n\t\t\t- small.photoLeft;\n\t\tconst auto left = size.width() - right;\n\t\tthumbs->setLayoutConstraints({ left, top }, style::al_right);\n\t}, thumbs->lifetime());\n\tthumbs->setAttribute(Qt::WA_TransparentForMouseEvents);\n\trecent->addClickHandler([=] {\n\t\t_controller->parentController()->openPeerStories(_peer->id);\n\t});\n\tobject_ptr<Profile::FloatingIcon>(\n\t\trecent,\n\t\tst::infoIconMediaStoriesRecent,\n\t\tst::infoSharedMediaButtonIconPosition)->show();\n\trecentWrap->toggleOn(rpl::duplicate(\n\t\tlast\n\t) | rpl::map([](const Content &content) {\n\t\treturn !content.elements.empty();\n\t}));\n\ttracker.track(recentWrap);\n}\n\nvoid InnerWidget::addGiftsButton(Ui::MultiSlideTracker &tracker) {\n\tExpects(_top != nullptr);\n\n\tconst auto user = _peer->asUser();\n\tAssert(user != nullptr);\n\n\tauto count = Profile::PeerGiftsCountValue(\n\t\tuser\n\t) | rpl::start_spawning(_top->lifetime());\n\n\tconst auto giftsWrap = _top->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\t_top,\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\t_top,\n\t\t\t\ttr::lng_peer_gifts_title(),\n\t\t\t\tst::infoSharedMediaButton))\n\t)->setDuration(\n\t\tst::infoSlideDuration\n\t)->toggleOn(rpl::duplicate(count) | rpl::map(rpl::mappers::_1 > 0));\n\n\tconst auto gifts = giftsWrap->entity();\n\tgifts->addClickHandler([=] {\n\t\t_controller->showSection(PeerGifts::Make(_peer));\n\t});\n\tauto label = rpl::duplicate(\n\t\tcount\n\t) | rpl::filter(\n\t\trpl::mappers::_1 > 0\n\t) | rpl::map([=](int count) {\n\t\treturn (count > 0) ? QString::number(count) : QString();\n\t});\n\t::Settings::CreateRightLabel(\n\t\tgifts,\n\t\tstd::move(label),\n\t\tst::infoSharedMediaButton,\n\t\ttr::lng_stories_archive_button());\n\tobject_ptr<Profile::FloatingIcon>(\n\t\tgifts,\n\t\tst::infoIconMediaGifts,\n\t\tst::infoSharedMediaButtonIconPosition)->show();\n\ttracker.track(giftsWrap);\n}\n\nbool InnerWidget::hasFlexibleTopBar() const {\n\treturn (_controller->key().storiesAlbumId() != Stories::ArchiveId()\n\t\t&& _controller->key().storiesPeer()\n\t\t&& _controller->key().storiesPeer()->isSelf());\n}\n\nbase::weak_qptr<Ui::RpWidget> InnerWidget::createPinnedToTop(\n\t\tnot_null<Ui::RpWidget*> parent) {\n\tif (!hasFlexibleTopBar()) {\n\t\treturn nullptr;\n\t}\n\n\tconst auto content = Ui::CreateChild<Profile::TopBar>(\n\t\tparent,\n\t\tProfile::TopBar::Descriptor{\n\t\t\t.controller = _controller->parentController(),\n\t\t\t.key = _controller->key(),\n\t\t\t.wrap = _controller->wrapValue(),\n\t\t\t.source = Profile::TopBar::Source::Stories,\n\t\t\t.peer = _peer,\n\t\t\t.backToggles = _backToggles.value(),\n\t\t\t.showFinished = _showFinished.events(),\n\t\t});\n\tcontent->backRequest(\n\t) | rpl::start_to_stream(_backClicks, content->lifetime());\n\t_topBarColor = content->edgeColor();\n\treturn base::make_weak(not_null<Ui::RpWidget*>{ content });\n}\n\nbase::weak_qptr<Ui::RpWidget> InnerWidget::createPinnedToBottom(\n\t\tnot_null<Ui::RpWidget*> parent) {\n\treturn nullptr;\n}\n\nvoid InnerWidget::enableBackButton() {\n\t_backToggles.force_assign(true);\n}\n\nvoid InnerWidget::showFinished() {\n\t_showFinished.fire({});\n\n\tconst auto window = _controller->parentController();\n\twindow->checkHighlightControl(u\"my-profile/posts\"_q, _albumsWrap);\n\tif (window->highlightControlId() == u\"my-profile/posts/add-album\"_q) {\n\t\twindow->setHighlightControlId(QString());\n\t\tif (_albumsWrap) {\n\t\t\t::Settings::HighlightWidget(_albumsWrap);\n\t\t}\n\t\t_controller->uiShow()->show(Box(\n\t\t\tNewAlbumBox,\n\t\t\t_controller,\n\t\t\t_peer,\n\t\t\tStoryId(),\n\t\t\t[=](Data::StoryAlbum album) { albumAdded(album); }));\n\t}\n}\n\nvoid InnerWidget::finalizeTop() {\n\tconst auto addPossibleAlbums = !_addingToAlbumId\n\t\t&& (_albumId.current() != Data::kStoriesAlbumIdArchive);\n\tif (addPossibleAlbums) {\n\t\tsetupAlbums();\n\t}\n\t_top->resizeToWidth(width());\n\n\t_top->heightValue(\n\t) | rpl::on_next([=] {\n\t\trefreshHeight();\n\t}, _top->lifetime());\n}\n\nvoid InnerWidget::createAboutArchive() {\n\tstartTop();\n\n\t_top->add(object_ptr<Ui::DividerLabel>(\n\t\t_top,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_top,\n\t\t\t(_peer->isChannel()\n\t\t\t\t? tr::lng_stories_channel_archive_about\n\t\t\t\t: tr::lng_stories_archive_about)(),\n\t\t\tst::infoStoriesAboutArchive),\n\t\tst::infoStoriesAboutArchivePadding));\n\n\tfinalizeTop();\n}\n\nvoid InnerWidget::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\t_visibleRange = { visibleTop, visibleBottom };\n\tsetChildVisibleTopBottom(_list, visibleTop, visibleBottom);\n}\n\nbool InnerWidget::showInternal(not_null<Memento*> memento) {\n\tif (memento->section().type() == Section::Type::Stories) {\n\t\trestoreState(memento);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid InnerWidget::setupList() {\n\tExpects(!_list);\n\n\t_list = object_ptr<Media::ListWidget>(\n\t\tthis,\n\t\t_controller);\n\tconst auto raw = _list.data();\n\tconst auto albumId = _albumId.current();\n\tif (albumId && albumId != Data::kStoriesAlbumIdArchive) {\n\t\traw->setReorderDescriptor({\n\t\t\t// .filter = [=](HistoryItem *item) {\n\t\t\t// \tconst auto stories = &_peer->owner().stories();\n\t\t\t// \tconst auto &albumIds = stories->albumIds(_peer->id, albumId);\n\t\t\t// \tconst auto storyId = StoryIdFromMsgId(item->id);\n\t\t\t// \treturn !ranges::contains(albumIds.pinnedToTop, storyId);\n\t\t\t// },\n\t\t\t.save = [=](\n\t\t\t\t\tint oldPosition,\n\t\t\t\t\tint newPosition,\n\t\t\t\t\tFn<void()> done,\n\t\t\t\t\tFn<void()> fail) {\n\t\t\t\t_peer->owner().stories().albumReorderStories(\n\t\t\t\t\t_peer,\n\t\t\t\t\talbumId,\n\t\t\t\t\toldPosition,\n\t\t\t\t\tnewPosition,\n\t\t\t\t\tdone,\n\t\t\t\t\tfail);\n\t\t\t}\n\t\t});\n\t}\n\n\tusing namespace rpl::mappers;\n\traw->scrollToRequests(\n\t) | rpl::map([=](int to) {\n\t\treturn Ui::ScrollToRequest {\n\t\t\traw->y() + to,\n\t\t\t-1\n\t\t};\n\t}) | rpl::start_to_stream(_scrollToRequests, raw->lifetime());\n\t_selectedLists.fire(raw->selectedListValue());\n\t_listTops.fire(raw->topValue());\n\n\traw->show();\n\n\tUi::PostponeCall(crl::guard(this, [=] {\n\t\tvisibleTopBottomUpdated(_visibleRange.top, _visibleRange.bottom);\n\t}));\n}\n\nvoid InnerWidget::setupEmpty() {\n\t_list->resizeToWidth(width());\n\n\tconst auto stories = &_controller->session().data().stories();\n\tconst auto key = Data::StoryAlbumIdsKey{ _peer->id, _albumId.current() };\n\trpl::combine(\n\t\trpl::single(\n\t\t\trpl::empty\n\t\t) | rpl::then(\n\t\t\tstories->albumIdsChanged() | rpl::filter(\n\t\t\t\trpl::mappers::_1 == key\n\t\t\t) | rpl::to_empty\n\t\t),\n\t\t_list->heightValue()\n\t) | rpl::on_next([=](auto, int listHeight) {\n\t\tconst auto padding = st::infoMediaMargin;\n\t\tif (const auto raw = _empty.release()) {\n\t\t\traw->hide();\n\t\t\traw->deleteLater();\n\t\t}\n\t\t_emptyLoading = false;\n\t\tif (listHeight <= padding.bottom() + padding.top()) {\n\t\t\trefreshEmpty();\n\t\t} else {\n\t\t\t_albumEmpty = false;\n\t\t}\n\t\trefreshHeight();\n\t}, _list->lifetime());\n}\n\nvoid InnerWidget::refreshEmpty() {\n\tconst auto albumId = _albumId.current();\n\tconst auto stories = &_controller->session().data().stories();\n\tconst auto knownEmpty = stories->albumIdsCountKnown(_peer->id, albumId);\n\tconst auto albumCanAdd = knownEmpty\n\t\t&& albumId\n\t\t&& (albumId != Data::kStoriesAlbumIdArchive)\n\t\t&& _peer->canEditStories();\n\t_albumEmpty = albumCanAdd;\n\tif (albumCanAdd) {\n\t\tauto empty = object_ptr<Ui::VerticalLayout>(this);\n\t\tempty->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tempty.get(),\n\t\t\t\ttr::lng_stories_album_empty_title(),\n\t\t\t\tst::collectionEmptyTitle),\n\t\t\tst::collectionEmptyTitleMargin,\n\t\t\tstyle::al_top);\n\t\tempty->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tempty.get(),\n\t\t\t\ttr::lng_stories_album_empty_text(),\n\t\t\t\tst::collectionEmptyText),\n\t\t\tst::collectionEmptyTextMargin,\n\t\t\tstyle::al_top);\n\n\t\tconst auto button = empty->add(\n\t\t\tobject_ptr<Ui::RoundButton>(\n\t\t\t\tempty.get(),\n\t\t\t\trpl::single(QString()),\n\t\t\t\tst::collectionEmptyButton),\n\t\t\tst::collectionEmptyAddMargin,\n\t\t\tstyle::al_top);\n\t\tbutton->setText(tr::lng_stories_album_add_button(\n\t\t) | rpl::map([](const QString &text) {\n\t\t\treturn Ui::Text::IconEmoji(&st::collectionAddIcon).append(text);\n\t\t}));\n\t\tbutton->setClickedCallback([=] {\n\t\t\teditAlbumStories(albumId);\n\t\t});\n\t\tempty->show();\n\t\t_empty = std::move(empty);\n\t} else {\n\t\t_empty = object_ptr<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\t(!knownEmpty\n\t\t\t\t? tr::lng_contacts_loading(tr::marked)\n\t\t\t\t: _peer->isSelf()\n\t\t\t\t? tr::lng_stories_empty(tr::rich)\n\t\t\t\t: tr::lng_stories_empty_channel(tr::rich)),\n\t\t\tst::giftListAbout);\n\t\t_empty->show();\n\t}\n\t_emptyLoading = !albumCanAdd && !knownEmpty;\n\tresizeToWidth(width());\n}\n\nvoid InnerWidget::refreshAlbumsTabs() {\n\tExpects(!_addingToAlbumId);\n\tExpects(_albumsWrap != nullptr);\n\n\tconst auto has = _peer->canEditStories()\n\t\t&& _peer->owner().stories().albumIdsCount(\n\t\t\t_peer->id,\n\t\t\tData::kStoriesAlbumIdArchive);\n\tif (_albums.empty() && !has) {\n\t\tif (base::take(_albumsTabs)) {\n\t\t\tresizeToWidth(width());\n\t\t}\n\t\treturn;\n\t}\n\tauto tabs = std::vector<Ui::SubTabs::Tab>();\n\tauto selected = QString();\n\tif (!_albums.empty()) {\n\t\ttabs.push_back({\n\t\t\t.id = u\"all\"_q,\n\t\t\t.text = tr::lng_stories_album_all(\n\t\t\t\ttr::now,\n\t\t\t\ttr::marked),\n\t\t});\n\t\tfor (const auto &album : _albums) {\n\t\t\tauto title = TextWithEntities();\n\t\t\ttitle.append(album.title);\n\t\t\ttabs.push_back({\n\t\t\t\t.id = QString::number(album.id),\n\t\t\t\t.text = std::move(title),\n\t\t\t});\n\t\t\tif (_albumId.current() == album.id) {\n\t\t\t\tselected = tabs.back().id;\n\t\t\t}\n\t\t}\n\t\tif (selected.isEmpty()) {\n\t\t\tselected = tabs.front().id;\n\t\t}\n\t}\n\tif (has) {\n\t\ttabs.push_back({\n\t\t\t.id = u\"add\"_q,\n\t\t\t.text = { '+' + tr::lng_stories_album_add(tr::now) },\n\t\t});\n\t}\n\tif (!_albumsTabs) {\n\t\tconst auto tabsCount = tabs.size();\n\t\t_albumsTabs = std::make_unique<Ui::SubTabs>(\n\t\t\t_albumsWrap,\n\t\t\tst::collectionSubTabs,\n\t\t\tUi::SubTabs::Options{\n\t\t\t\t.selected = selected,\n\t\t\t\t.centered = true,\n\t\t\t},\n\t\t\tstd::move(tabs));\n\t\t_albumsTabs->setPinnedInterval(0, 1);\n\t\tif (has) {\n\t\t\t_albumsTabs->setPinnedInterval(tabsCount - 1, tabsCount);\n\t\t}\n\t\t_albumsTabs->show();\n\n\t\tconst auto padding = st::giftBoxPadding;\n\t\t_albumsWrap->resize(\n\t\t\t_albumsWrap->width(),\n\t\t\tpadding.top() + _albumsTabs->height() + padding.top());\n\t\t_albumsWrap->widthValue() | rpl::on_next([=](int width) {\n\t\t\t_albumsTabs->resizeToWidth(width);\n\t\t}, _albumsTabs->lifetime());\n\t\t_albumsTabs->move(0, padding.top());\n\n\t\t_albumsTabs->activated(\n\t\t) | rpl::on_next([=](const QString &id) {\n\t\t\tif (id == u\"add\"_q) {\n\t\t\t\tconst auto added = [=](Data::StoryAlbum album) {\n\t\t\t\t\talbumAdded(album);\n\t\t\t\t};\n\t\t\t\t_controller->uiShow()->show(Box(\n\t\t\t\t\tNewAlbumBox,\n\t\t\t\t\t_controller,\n\t\t\t\t\t_peer,\n\t\t\t\t\tStoryId(),\n\t\t\t\t\tadded));\n\t\t\t} else {\n\t\t\t\t_albumIdChanges.fire((id == u\"all\"_q) ? 0 : id.toInt());\n\t\t\t}\n\t\t}, _albumsTabs->lifetime());\n\n\t\t_albumsTabs->contextMenuRequests(\n\t\t) | rpl::on_next([=](const QString &id) {\n\t\t\tif (id == u\"add\"_q || id == u\"all\"_q) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tshowMenuForAlbum(id.toInt());\n\t\t}, _albumsTabs->lifetime());\n\n\t\tusing ReorderUpdate = Ui::SubTabsReorderUpdate;\n\t\t_albumsTabs->reorderUpdates(\n\t\t) | rpl::on_next([=](const ReorderUpdate &update) {\n\t\t\tif (update.state == ReorderUpdate::State::Applied) {\n\t\t\t\treorderAlbumsLocally(update);\n\t\t\t}\n\t\t}, _albumsTabs->lifetime());\n\t} else {\n\t\tconst auto tabsCount = tabs.size();\n\t\t_albumsTabs->setTabs(std::move(tabs));\n\t\t_albumsTabs->clearPinnedIntervals();\n\t\t_albumsTabs->setPinnedInterval(0, 1);\n\t\tif (has) {\n\t\t\t_albumsTabs->setPinnedInterval(tabsCount - 1, tabsCount);\n\t\t}\n\t\tif (!selected.isEmpty()) {\n\t\t\t_albumsTabs->setActiveTab(selected);\n\t\t}\n\t}\n\tresizeToWidth(width());\n}\n\nvoid InnerWidget::showMenuForAlbum(int id) {\n\tExpects(id > 0);\n\n\tif (_menu || _addingToAlbumId) {\n\t\treturn;\n\t}\n\t_menu = base::make_unique_q<Ui::PopupMenu>(this, st::popupMenuWithIcons);\n\tconst auto addAction = Ui::Menu::CreateAddActionCallback(_menu);\n\n\tif (_albumsTabs && _albumsTabs->reorderEnabled()) {\n\t\taddAction(\n\t\t\ttr::lng_gift_collection_reorder_exit(tr::now),\n\t\t\t[=] {\n\t\t\t\tflushAlbumReorder();\n\t\t\t\t_albumsTabs->setReorderEnabled(false);\n\t\t\t},\n\t\t\t&st::menuIconReorder);\n\t\t_menu->popup(QCursor::pos());\n\t\treturn;\n\t}\n\n\tif (_peer->canEditStories()) {\n\t\taddAction(tr::lng_stories_album_add_button(tr::now), [=] {\n\t\t\teditAlbumStories(id);\n\t\t}, &st::menuIconStoriesSave);\n\t}\n\tif (const auto username = _peer->username(); !username.isEmpty()) {\n\t\taddAction(tr::lng_stories_album_share(tr::now), [=] {\n\t\t\tshareAlbumLink(username, id);\n\t\t}, &st::menuIconShare);\n\t}\n\tif (_peer->canEditStories()) {\n\t\taddAction(tr::lng_stories_album_edit(tr::now), [=] {\n\t\t\teditAlbumName(id);\n\t\t}, &st::menuIconEdit);\n\t\tif (_albumsTabs) {\n\t\t\taddAction(\n\t\t\t\ttr::lng_gift_collection_reorder(tr::now),\n\t\t\t\t[=] { _albumsTabs->setReorderEnabled(true); },\n\t\t\t\t&st::menuIconReorder);\n\t\t}\n\t\taddAction({\n\t\t\t.text = tr::lng_stories_album_delete(tr::now),\n\t\t\t.handler = [=] { confirmDeleteAlbum(id); },\n\t\t\t.icon = &st::menuIconDeleteAttention,\n\t\t\t.isAttention = true,\n\t\t});\n\t}\n\tif (_menu->empty()) {\n\t\t_menu = nullptr;\n\t} else {\n\t\t_menu->popup(QCursor::pos());\n\t}\n}\n\nrpl::producer<int> InnerWidget::albumIdChanges() const {\n\treturn _albumIdChanges.events();\n}\n\nrpl::producer<Data::StoryAlbumUpdate> InnerWidget::changes() const {\n\treturn _albumChanges.value();\n}\n\nvoid InnerWidget::reload() {\n\tauto old = std::exchange(_list, object_ptr<Media::ListWidget>(nullptr));\n\tsetupList();\n\tsetupEmpty();\n\told.destroy();\n\n\tresizeToWidth(width());\n}\n\nvoid InnerWidget::editAlbumStories(int id) {\n\tconst auto weak = base::make_weak(this);\n\tauto box = Box<EditAlbumBox>(_controller, _peer, crl::guard(this, [=] {\n\t\tif (_albumId.current() == id) {\n\t\t\treload();\n\t\t}\n\t}), id);\n\n\t_controller->uiShow()->show(std::move(box));\n}\n\nvoid InnerWidget::shareAlbumLink(const QString &username, int id) {\n\tconst auto url = _controller->session().createInternalLinkFull(\n\t\tusername + u\"/a/\"_q + QString::number(id));\n\tFastShareLink(_controller->parentController(), url);\n}\n\nvoid InnerWidget::editAlbumName(int id) {\n\tconst auto done = [=](QString name) {\n\t\talbumRenamed(id, name);\n\t};\n\tconst auto i = ranges::find(_albums, id, &Data::StoryAlbum::id);\n\tif (i == end(_albums)) {\n\t\treturn;\n\t}\n\t_controller->uiShow()->show(Box(\n\t\tEditAlbumNameBox,\n\t\t_controller->parentController(),\n\t\t_peer,\n\t\tid,\n\t\ti->title,\n\t\tdone));\n}\n\nvoid InnerWidget::confirmDeleteAlbum(int id) {\n\tconst auto done = [=](Fn<void()> close) {\n\t\talbumRemoved(id);\n\n\t\tconst auto stories = &_controller->session().data().stories();\n\t\tstories->albumDelete(_peer, id);\n\n\t\tclose();\n\t};\n\t_controller->uiShow()->show(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_stories_album_delete_sure(),\n\t\t.confirmed = crl::guard(this, done),\n\t\t.confirmText = tr::lng_stories_album_delete_button(),\n\t\t.confirmStyle = &st::attentionBoxButton,\n\t}));\n}\n\nvoid InnerWidget::albumAdded(Data::StoryAlbum result) {\n\tExpects(ranges::contains(_albums, result.id, &Data::StoryAlbum::id));\n\n\t_albumIdChanges.fire_copy(result.id);\n}\n\nvoid InnerWidget::albumRenamed(int id, QString name) {\n\tconst auto i = ranges::find(_albums, id, &Data::StoryAlbum::id);\n\tif (i != end(_albums)) {\n\t\ti->title = name;\n\t\trefreshAlbumsTabs();\n\t}\n}\n\nvoid InnerWidget::albumRemoved(int id) {\n\tauto now = _albumId.current();\n\tif (now == id) {\n\t\t_albumIdChanges.fire_copy(0);\n\t}\n\tconst auto i = ranges::find(_albums, id, &Data::StoryAlbum::id);\n\tif (i != end(_albums)) {\n\t\t_albums.erase(i);\n\t\trefreshAlbumsTabs();\n\t}\n}\n\nvoid InnerWidget::saveState(not_null<Memento*> memento) {\n\t_list->saveState(&memento->media());\n}\n\nvoid InnerWidget::restoreState(not_null<Memento*> memento) {\n\t_list->restoreState(&memento->media());\n}\n\nrpl::producer<SelectedItems> InnerWidget::selectedListValue() const {\n\treturn _selectedLists.events_starting_with(\n\t\t_list->selectedListValue()\n\t) | rpl::flatten_latest();\n}\n\nvoid InnerWidget::selectionAction(SelectionAction action) {\n\t_list->selectionAction(action);\n}\n\nint InnerWidget::resizeGetHeight(int newWidth) {\n\tif (!newWidth) {\n\t\treturn 0;\n\t}\n\t_inResize = true;\n\tauto guard = gsl::finally([this] { _inResize = false; });\n\n\tif (_top) {\n\t\t_top->resizeToWidth(newWidth);\n\t}\n\tif (_list) {\n\t\t_list->resizeToWidth(newWidth);\n\t}\n\tif (const auto empty = _empty.get()) {\n\t\tconst auto margin = st::giftListAboutMargin;\n\t\tempty->resizeToWidth(newWidth - margin.left() - margin.right());\n\t}\n\n\treturn recountHeight();\n}\n\nvoid InnerWidget::refreshHeight() {\n\tif (_inResize) {\n\t\treturn;\n\t}\n\tresize(width(), recountHeight());\n}\n\nint InnerWidget::recountHeight() {\n\tauto top = 0;\n\tif (_top) {\n\t\t_top->moveToLeft(0, top);\n\t\ttop += _top->heightNoMargins() - st::lineWidth;\n\t}\n\tauto listHeight = 0;\n\tif (_list) {\n\t\t_list->moveToLeft(0, top);\n\t\tlistHeight = _list->heightNoMargins();\n\t\ttop += listHeight;\n\t}\n\tif (const auto empty = _empty.get()) {\n\t\tconst auto margin = st::giftListAboutMargin;\n\t\tempty->moveToLeft(margin.left(), top + margin.top());\n\t\ttop += margin.top() + empty->height() + margin.bottom();\n\t}\n\tif (_emptyLoading) {\n\t\ttop = std::max(top, _lastNonLoadingHeight);\n\t} else {\n\t\t_lastNonLoadingHeight = top;\n\t}\n\treturn top;\n}\n\nvoid InnerWidget::setScrollHeightValue(rpl::producer<int> value) {\n}\n\nrpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {\n\treturn _scrollToRequests.events();\n}\n\nvoid InnerWidget::reorderAlbumsLocally(\n\t\tconst Ui::SubTabsReorderUpdate &update) {\n\tif (!_albumsTabs || !_peer->canEditStories()) {\n\t\treturn;\n\t}\n\n\tconst auto albumId = update.id.toInt();\n\tif (albumId <= 0) {\n\t\treturn;\n\t}\n\n\tconst auto it = ranges::find(\n\t\t_albums,\n\t\talbumId,\n\t\t&Data::StoryAlbum::id);\n\tif (it == _albums.end()) {\n\t\treturn;\n\t}\n\n\tconst auto album = *it;\n\t_albums.erase(it);\n\n\tconst auto newPos = std::max(\n\t\t0,\n\t\tstd::min(update.newPosition - 1, int(_albums.size())));\n\t_albums.insert(_albums.begin() + newPos, album);\n\n\t_pendingAlbumReorder = true;\n}\n\nvoid InnerWidget::flushAlbumReorder() {\n\tif (!_pendingAlbumReorder || !_peer->canEditStories()) {\n\t\treturn;\n\t}\n\n\tif (_reorderRequestId) {\n\t\t_api->request(_reorderRequestId).cancel();\n\t\t_reorderRequestId = 0;\n\t}\n\n\tauto order = QVector<MTPint>();\n\tfor (const auto &album : _albums) {\n\t\torder.push_back(MTP_int(album.id));\n\t}\n\n\t_reorderRequestId = _api->request(MTPstories_ReorderAlbums(\n\t\t_peer->input(),\n\t\tMTP_vector<MTPint>(order)\n\t)).done([=] {\n\t\t_reorderRequestId = 0;\n\t}).fail([=, show = _controller->uiShow()](const MTP::Error &error) {\n\t\t_reorderRequestId = 0;\n\t}).send();\n\n\t_pendingAlbumReorder = false;\n}\n\n} // namespace Info::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/info/stories/info_stories_inner_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n#include \"data/data_stories.h\"\n#include \"info/stories/info_stories_common.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/widgets/scroll_area.h\"\n\nnamespace Data {\nstruct StoryAlbum;\n} // namespace Data\n\nnamespace Ui {\nclass SubTabs;\nclass PopupMenu;\nclass VerticalLayout;\nclass MultiSlideTracker;\nstruct SubTabsReorderUpdate;\n} // namespace Ui\n\nnamespace Info {\nclass Controller;\nstruct SelectedItems;\nenum class SelectionAction;\n} // namespace Info\n\nnamespace Info::Media {\nclass ListWidget;\n} // namespace Info::Media\n\nnamespace Window {\nclass SessionNavigation;\n} // namespace Window\n\nnamespace MTP {\nclass Sender;\n} // namespace MTP\n\nnamespace Info::Stories {\n\nclass Memento;\nclass EmptyWidget;\n\nclass InnerWidget final : public Ui::RpWidget {\npublic:\n\tInnerWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\trpl::producer<int> albumId,\n\t\tint addingToAlbumId = 0);\n\t~InnerWidget();\n\n\t[[nodiscard]] rpl::producer<> backRequest() const;\n\n\tbool showInternal(not_null<Memento*> memento);\n\tvoid setIsStackBottom(bool isStackBottom) {\n\t\t_isStackBottom = isStackBottom;\n\t\tsetupTop();\n\t}\n\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tvoid setScrollHeightValue(rpl::producer<int> value);\n\n\trpl::producer<Ui::ScrollToRequest> scrollToRequests() const;\n\trpl::producer<SelectedItems> selectedListValue() const;\n\tvoid selectionAction(SelectionAction action);\n\n\tvoid reload();\n\tvoid editAlbumStories(int id);\n\tvoid shareAlbumLink(const QString &username, int id);\n\tvoid editAlbumName(int id);\n\tvoid confirmDeleteAlbum(int id);\n\tvoid albumAdded(Data::StoryAlbum result);\n\n\t[[nodiscard]] rpl::producer<bool> albumEmptyValue() const {\n\t\treturn _albumEmpty.value();\n\t}\n\n\t[[nodiscard]] rpl::producer<int> albumIdChanges() const;\n\t[[nodiscard]] rpl::producer<Data::StoryAlbumUpdate> changes() const;\n\n\tbool hasFlexibleTopBar() const;\n\tbase::weak_qptr<Ui::RpWidget> createPinnedToTop(\n\t\tnot_null<Ui::RpWidget*> parent);\n\tbase::weak_qptr<Ui::RpWidget> createPinnedToBottom(\n\t\tnot_null<Ui::RpWidget*> parent);\n\n\tvoid enableBackButton();\n\tvoid showFinished();\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\nprivate:\n\tint recountHeight();\n\tvoid refreshHeight();\n\tvoid refreshEmpty();\n\tvoid preloadArchiveCount();\n\n\tvoid setupTop();\n\tvoid setupList();\n\tvoid setupEmpty();\n\tvoid setupAlbums();\n\tvoid createButtons();\n\tvoid createProfileTop();\n\tvoid createAboutArchive();\n\n\tvoid startTop();\n\tvoid addArchiveButton(Ui::MultiSlideTracker &tracker);\n\tvoid addRecentButton(Ui::MultiSlideTracker &tracker);\n\tvoid addGiftsButton(Ui::MultiSlideTracker &tracker);\n\tvoid finalizeTop();\n\n\tvoid refreshAlbumsTabs();\n\tvoid showMenuForAlbum(int id);\n\n\tvoid albumRenamed(int id, QString name);\n\tvoid albumRemoved(int id);\n\n\tvoid reorderAlbumsLocally(const Ui::SubTabsReorderUpdate &update);\n\tvoid flushAlbumReorder();\n\n\tconst not_null<Controller*> _controller;\n\tconst not_null<PeerData*> _peer;\n\tconst int _addingToAlbumId = 0;\n\n\tstd::vector<Data::StoryAlbum> _albums;\n\trpl::variable<int> _albumId;\n\trpl::event_stream<int> _albumIdChanges;\n\tUi::RpWidget *_albumsWrap = nullptr;\n\tstd::unique_ptr<Ui::SubTabs> _albumsTabs;\n\trpl::variable<Data::StoryAlbumUpdate> _albumChanges;\n\tbool _pendingAlbumReorder = false;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\tstd::unique_ptr<MTP::Sender> _api;\n\tmtpRequestId _reorderRequestId = 0;\n\n\tobject_ptr<Ui::VerticalLayout> _top = { nullptr };\n\tobject_ptr<Media::ListWidget> _list = { nullptr };\n\tobject_ptr<Ui::RpWidget> _empty = { nullptr };\n\tint _lastNonLoadingHeight = 0;\n\tbool _emptyLoading = false;\n\n\tbool _inResize = false;\n\tbool _isStackBottom = false;\n\n\trpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;\n\trpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;\n\trpl::event_stream<rpl::producer<int>> _listTops;\n\trpl::variable<int> _topHeight;\n\trpl::variable<bool> _albumEmpty;\n\n\trpl::variable<bool> _backToggles;\n\trpl::event_stream<> _backClicks;\n\trpl::event_stream<> _showFinished;\n\trpl::variable<std::optional<QColor>> _topBarColor;\n\n\tUi::VisibleRange _visibleRange;\n\n};\n\n} // namespace Info::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/info/stories/info_stories_provider.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/stories/info_stories_provider.h\"\n\n#include \"info/media/info_media_widget.h\"\n#include \"info/media/info_media_list_section.h\"\n#include \"info/info_controller.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_stories_ids.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history.h\"\n#include \"core/application.h\"\n#include \"layout/layout_selection.h\"\n#include \"styles/style_info.h\"\n\nnamespace Info::Stories {\nnamespace {\n\nusing namespace Media;\n\nconstexpr auto kPreloadedScreensCount = 4;\nconstexpr auto kPreloadedScreensCountFull\n\t= kPreloadedScreensCount + 1 + kPreloadedScreensCount;\n\n[[nodiscard]] int MinStoryHeight(int width) {\n\tauto itemsLeft = st::infoMediaSkip;\n\tauto itemsInRow = (width - itemsLeft)\n\t\t/ (st::infoMediaMinGridSize + st::infoMediaSkip);\n\treturn (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow;\n}\n\n} // namespace\n\nProvider::Provider(not_null<AbstractController*> controller)\n: _controller(controller)\n, _peer(controller->key().storiesPeer())\n, _history(_peer->owner().history(_peer))\n, _albumId(controller->key().storiesAlbumId())\n, _addingToAlbumId(controller->key().storiesAddToAlbumId()) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tfor (auto &layout : _layouts) {\n\t\t\tlayout.second.item->invalidateCache();\n\t\t}\n\t}, _lifetime);\n\n\t_peer->session().changes().storyUpdates(\n\t\tData::StoryUpdate::Flag::Destroyed\n\t) | rpl::filter([=](const Data::StoryUpdate &update) {\n\t\treturn update.story->peer() == _peer;\n\t}) | rpl::on_next([=](const Data::StoryUpdate &update) {\n\t\tstoryRemoved(update.story);\n\t}, _lifetime);\n}\n\nProvider::~Provider() {\n\tclear();\n}\n\nType Provider::type() {\n\treturn Type::PhotoVideo;\n}\n\nbool Provider::hasSelectRestriction() {\n\tif (_peer->session().frozen()) {\n\t\treturn true;\n\t} else if (const auto channel = _peer->asChannel()) {\n\t\treturn !channel->canEditStories() && !channel->canDeleteStories();\n\t}\n\treturn !_peer->isSelf();\n}\n\nrpl::producer<bool> Provider::hasSelectRestrictionChanges() {\n\treturn rpl::never<bool>();\n}\n\nbool Provider::sectionHasFloatingHeader() {\n\treturn false;\n}\n\nQString Provider::sectionTitle(not_null<const BaseLayout*> item) {\n\treturn QString();\n}\n\nbool Provider::sectionItemBelongsHere(\n\t\tnot_null<const BaseLayout*> item,\n\t\tnot_null<const BaseLayout*> previous) {\n\treturn true;\n}\n\nbool Provider::isPossiblyMyItem(not_null<const HistoryItem*> item) {\n\treturn true;\n}\n\nstd::optional<int> Provider::fullCount() {\n\treturn _slice.fullCount();\n}\n\nvoid Provider::clear() {\n\tfor (const auto &[storyId, _] : _layouts) {\n\t\t_peer->owner().stories().unregisterPolling(\n\t\t\t{ _peer->id, storyId },\n\t\t\tData::Stories::Polling::Chat);\n\t}\n\t_layouts.clear();\n\t_aroundId = kDefaultAroundId;\n\t_idsLimit = kMinimalIdsLimit;\n\t_slice = Data::StoriesIdsSlice();\n}\n\nvoid Provider::restart() {\n\tclear();\n\trefreshViewer();\n}\n\nvoid Provider::checkPreload(\n\t\tQSize viewport,\n\t\tnot_null<BaseLayout*> topLayout,\n\t\tnot_null<BaseLayout*> bottomLayout,\n\t\tbool preloadTop,\n\t\tbool preloadBottom) {\n\tconst auto visibleWidth = viewport.width();\n\tconst auto visibleHeight = viewport.height();\n\tconst auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;\n\tconst auto minItemHeight = MinStoryHeight(visibleWidth);\n\tconst auto preloadedCount = preloadedHeight / minItemHeight;\n\tconst auto preloadIdsLimitMin = (preloadedCount / 2) + 1;\n\tconst auto preloadIdsLimit = preloadIdsLimitMin\n\t\t+ (visibleHeight / minItemHeight);\n\tconst auto after = _slice.skippedAfter();\n\tconst auto topLoaded = after && (*after == 0);\n\tconst auto before = _slice.skippedBefore();\n\tconst auto bottomLoaded = before && (*before == 0);\n\n\tconst auto minScreenDelta = kPreloadedScreensCount\n\t\t- kPreloadIfLessThanScreens;\n\tconst auto minIdDelta = (minScreenDelta * visibleHeight)\n\t\t/ minItemHeight;\n\tconst auto preloadAroundItem = [&](not_null<BaseLayout*> layout) {\n\t\tauto preloadRequired = false;\n\t\tconst auto id = StoryIdFromMsgId(layout->getItem()->id);\n\t\tif (!preloadRequired) {\n\t\t\tpreloadRequired = (_idsLimit < preloadIdsLimitMin);\n\t\t}\n\t\tif (!preloadRequired) {\n\t\t\tauto delta = _slice.distance(_aroundId, id);\n\t\t\tAssert(delta != std::nullopt);\n\t\t\tpreloadRequired = (qAbs(*delta) >= minIdDelta);\n\t\t}\n\t\tif (preloadRequired) {\n\t\t\t_idsLimit = preloadIdsLimit;\n\t\t\t_aroundId = id;\n\t\t\trefreshViewer();\n\t\t}\n\t};\n\n\tif (preloadTop && !topLoaded) {\n\t\tpreloadAroundItem(topLayout);\n\t} else if (preloadBottom && !bottomLoaded) {\n\t\tpreloadAroundItem(bottomLayout);\n\t}\n}\n\nvoid Provider::setSearchQuery(QString query) {\n}\n\nvoid Provider::jumpToMessage(MsgId messageId, Fn<void(FullMsgId)>) {\n}\n\nvoid Provider::refreshViewer() {\n\t_viewerLifetime.destroy();\n\tconst auto aroundId = _aroundId;\n\tauto ids = Data::AlbumStoriesIds(_peer, _albumId, aroundId, _idsLimit);\n\tstd::move(\n\t\tids\n\t) | rpl::on_next([=](Data::StoriesIdsSlice &&slice) {\n\t\tif (!slice.fullCount()) {\n\t\t\t// Don't display anything while full count is unknown.\n\t\t\treturn;\n\t\t}\n\t\t_slice = std::move(slice);\n\n\t\tauto nearestId = std::optional<StoryId>();\n\t\tfor (auto i = 0; i != _slice.size(); ++i) {\n\t\t\tif (!nearestId\n\t\t\t\t|| std::abs(*nearestId - aroundId)\n\t\t\t\t\t> std::abs(_slice[i] - aroundId)) {\n\t\t\t\tnearestId = _slice[i];\n\t\t\t}\n\t\t}\n\t\tif (nearestId) {\n\t\t\t_aroundId = *nearestId;\n\t\t}\n\n\t\t//if (const auto nearest = _slice.nearest(idForViewer)) {\n\t\t//\t_aroundId = *nearest;\n\t\t//}\n\t\t_refreshed.fire({});\n\t}, _viewerLifetime);\n}\n\nrpl::producer<> Provider::refreshed() {\n\treturn _refreshed.events();\n}\n\nstd::vector<ListSection> Provider::fillSections(\n\t\tnot_null<Overview::Layout::Delegate*> delegate) {\n\tmarkLayoutsStale();\n\tconst auto guard = gsl::finally([&] { clearStaleLayouts(); });\n\n\tauto result = std::vector<ListSection>();\n\tauto section = ListSection(Type::PhotoVideo, sectionDelegate());\n\tauto count = _slice.size();\n\tfor (auto i = 0; i != count; ++i) {\n\t\tconst auto storyId = _slice[i];\n\t\tif (const auto layout = getLayout(storyId, delegate)) {\n\t\t\tif (!section.addItem(layout)) {\n\t\t\t\tsection.finishSection();\n\t\t\t\tresult.push_back(std::move(section));\n\t\t\t\tsection = ListSection(Type::PhotoVideo, sectionDelegate());\n\t\t\t\tsection.addItem(layout);\n\t\t\t}\n\t\t}\n\t}\n\tif (!section.empty()) {\n\t\tsection.finishSection();\n\t\tresult.push_back(std::move(section));\n\t}\n\treturn result;\n}\n\nvoid Provider::markLayoutsStale() {\n\tfor (auto &layout : _layouts) {\n\t\tlayout.second.stale = true;\n\t}\n}\n\nvoid Provider::clearStaleLayouts() {\n\tfor (auto i = _layouts.begin(); i != _layouts.end();) {\n\t\tif (i->second.stale) {\n\t\t\t_peer->owner().stories().unregisterPolling(\n\t\t\t\t{ _peer->id, i->first },\n\t\t\t\tData::Stories::Polling::Chat);\n\t\t\t_layoutRemoved.fire(i->second.item.get());\n\t\t\tconst auto taken = _items.take(i->first);\n\t\t\ti = _layouts.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n}\n\nrpl::producer<not_null<BaseLayout*>> Provider::layoutRemoved() {\n\treturn _layoutRemoved.events();\n}\n\nBaseLayout *Provider::lookupLayout(const HistoryItem *item) {\n\treturn nullptr;\n}\n\nbool Provider::isMyItem(not_null<const HistoryItem*> item) {\n\treturn IsStoryMsgId(item->id) && (item->history()->peer == _peer);\n}\n\nbool Provider::isAfter(\n\t\tnot_null<const HistoryItem*> a,\n\t\tnot_null<const HistoryItem*> b) {\n\treturn (a->id < b->id);\n}\n\nvoid Provider::storyRemoved(not_null<Data::Story*> story) {\n\tExpects(story->peer() == _peer);\n\n\tif (const auto i = _layouts.find(story->id()); i != end(_layouts)) {\n\t\t_peer->owner().stories().unregisterPolling(\n\t\t\tstory,\n\t\t\tData::Stories::Polling::Chat);\n\t\t_layoutRemoved.fire(i->second.item.get());\n\t\t_layouts.erase(i);\n\t}\n\t_items.remove(story->id());\n}\n\nBaseLayout *Provider::getLayout(\n\t\tStoryId id,\n\t\tnot_null<Overview::Layout::Delegate*> delegate) {\n\tauto it = _layouts.find(id);\n\tif (it == _layouts.end()) {\n\t\tif (auto layout = createLayout(id, delegate)) {\n\t\t\tlayout->initDimensions();\n\t\t\tit = _layouts.emplace(id, std::move(layout)).first;\n\t\t\tconst auto ok = _peer->owner().stories().registerPolling(\n\t\t\t\t{ _peer->id, id },\n\t\t\t\tData::Stories::Polling::Chat);\n\t\t\tAssert(ok);\n\t\t} else {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\tit->second.stale = false;\n\treturn it->second.item.get();\n}\n\nHistoryItem *Provider::ensureItem(StoryId id) {\n\tconst auto i = _items.find(id);\n\tif (i != end(_items)) {\n\t\treturn i->second.get();\n\t}\n\tauto item = _peer->owner().stories().resolveItem({ _peer->id, id });\n\tif (!item) {\n\t\treturn nullptr;\n\t}\n\treturn _items.emplace(id, std::move(item)).first->second.get();\n}\n\nstd::unique_ptr<BaseLayout> Provider::createLayout(\n\t\tStoryId id,\n\t\tnot_null<Overview::Layout::Delegate*> delegate) {\n\tconst auto item = ensureItem(id);\n\tif (!item) {\n\t\treturn nullptr;\n\t}\n\tconst auto getPhoto = [&]() -> PhotoData* {\n\t\tif (const auto media = item->media()) {\n\t\t\treturn media->photo();\n\t\t}\n\t\treturn nullptr;\n\t};\n\tconst auto getFile = [&]() -> DocumentData* {\n\t\tif (const auto media = item->media()) {\n\t\t\treturn media->document();\n\t\t}\n\t\treturn nullptr;\n\t};\n\n\tconst auto peer = item->history()->peer;\n\tconst auto channel = peer->asChannel();\n\tconst auto showPinned = (_albumId == Data::kStoriesAlbumIdSaved);\n\tconst auto showHidden = peer->isSelf()\n\t\t|| (channel && channel->canEditStories());\n\n\tusing namespace Overview::Layout;\n\tconst auto options = MediaOptions{\n\t\t.story = true,\n\t\t.storyPinned = showPinned && item->isPinned(),\n\t\t.storyShowPinned = showPinned,\n\t\t.storyHidden = showHidden && !item->storyInProfile(),\n\t\t.storyShowHidden = showHidden,\n\t};\n\tif (const auto photo = getPhoto()) {\n\t\treturn std::make_unique<Photo>(delegate, item, photo, options);\n\t} else if (const auto file = getFile()) {\n\t\treturn std::make_unique<Video>(delegate, item, file, options);\n\t} else {\n\t\treturn std::make_unique<Photo>(\n\t\t\tdelegate,\n\t\t\titem,\n\t\t\tData::MediaStory::LoadingStoryPhoto(&item->history()->owner()),\n\t\t\toptions);\n\t}\n\treturn nullptr;\n}\n\nListItemSelectionData Provider::computeSelectionData(\n\t\tnot_null<const HistoryItem*> item,\n\t\tTextSelection selection) {\n\tauto result = ListItemSelectionData(selection);\n\tconst auto id = item->id;\n\tif (_addingToAlbumId || !IsStoryMsgId(id)) {\n\t\treturn result;\n\t}\n\tconst auto peer = item->history()->peer;\n\tconst auto channel = peer->asChannel();\n\tconst auto maybeStory = peer->owner().stories().lookup(\n\t\t{ peer->id, StoryIdFromMsgId(id) });\n\tif (maybeStory) {\n\t\tconst auto story = *maybeStory;\n\t\tresult.canForward = peer->isSelf() && story->canShare();\n\t\tresult.canDelete = story->canDelete();\n\t\tresult.canUnpinStory = story->pinnedToTop();\n\t\tresult.storyInProfile = story->inProfile();\n\t}\n\tresult.canToggleStoryPin = peer->isSelf()\n\t\t|| (channel && channel->canEditStories());\n\treturn result;\n}\n\nvoid Provider::applyDragSelection(\n\t\tListSelectedMap &selected,\n\t\tnot_null<const HistoryItem*> fromItem,\n\t\tbool skipFrom,\n\t\tnot_null<const HistoryItem*> tillItem,\n\t\tbool skipTill) {\n\tconst auto fromId = fromItem->id - (skipFrom ? 1 : 0);\n\tconst auto tillId = tillItem->id - (skipTill ? 0 : 1);\n\tfor (auto i = selected.begin(); i != selected.end();) {\n\t\tconst auto itemId = i->first->id;\n\t\tif (itemId > fromId || itemId <= tillId) {\n\t\t\ti = selected.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tfor (auto &layoutItem : _layouts) {\n\t\tconst auto storyId = layoutItem.first;\n\t\tconst auto id = StoryIdToMsgId(storyId);\n\t\tif (id <= fromId && id > tillId) {\n\t\t\tconst auto i = _items.find(storyId);\n\t\t\tAssert(i != end(_items));\n\t\t\tconst auto item = i->second.get();\n\t\t\tChangeItemSelection(\n\t\t\t\tselected,\n\t\t\t\titem,\n\t\t\t\tcomputeSelectionData(item, FullSelection));\n\t\t}\n\t}\n}\n\nbool Provider::allowSaveFileAs(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) {\n\treturn false;\n}\n\nQString Provider::showInFolderPath(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) {\n\treturn QString();\n}\n\nint64 Provider::scrollTopStatePosition(not_null<HistoryItem*> item) {\n\treturn StoryIdFromMsgId(item->id);\n}\n\nHistoryItem *Provider::scrollTopStateItem(ListScrollTopState state) {\n\tif (state.item && _slice.indexOf(StoryIdFromMsgId(state.item->id))) {\n\t\treturn state.item;\n\t//} else if (const auto id = _slice.nearest(state.position)) {\n\t//\tconst auto full = FullMsgId(_peer->id, StoryIdToMsgId(*id));\n\t//\tif (const auto item = _controller->session().data().message(full)) {\n\t//\t\treturn item;\n\t//\t}\n\t}\n\n\tauto nearestId = std::optional<StoryId>();\n\tfor (auto i = 0; i != _slice.size(); ++i) {\n\t\tif (!nearestId\n\t\t\t|| std::abs(*nearestId - state.position)\n\t\t\t\t> std::abs(_slice[i] - state.position)) {\n\t\t\tnearestId = _slice[i];\n\t\t}\n\t}\n\tif (nearestId) {\n\t\tconst auto full = FullMsgId(_peer->id, StoryIdToMsgId(*nearestId));\n\t\tif (const auto item = _controller->session().data().message(full)) {\n\t\t\treturn item;\n\t\t}\n\t}\n\n\treturn state.item;\n}\n\nvoid Provider::saveState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tListScrollTopState scrollState) {\n\tif (_aroundId != kDefaultAroundId && scrollState.item) {\n\t\tmemento->setAroundId({ _peer->id, StoryIdToMsgId(_aroundId) });\n\t\tmemento->setIdsLimit(_idsLimit);\n\t\tmemento->setScrollTopItem(scrollState.item->globalId());\n\t\tmemento->setScrollTopItemPosition(scrollState.position);\n\t\tmemento->setScrollTopShift(scrollState.shift);\n\t}\n}\n\nvoid Provider::restoreState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tFn<void(ListScrollTopState)> restoreScrollState) {\n\tif (const auto limit = memento->idsLimit()) {\n\t\tconst auto wasAroundId = memento->aroundId();\n\t\tif (wasAroundId.peer == _peer->id) {\n\t\t\t_idsLimit = limit;\n\t\t\t_aroundId = StoryIdFromMsgId(wasAroundId.msg);\n\t\t\trestoreScrollState({\n\t\t\t\t.position = memento->scrollTopItemPosition(),\n\t\t\t\t.item = MessageByGlobalId(memento->scrollTopItem()),\n\t\t\t\t.shift = memento->scrollTopShift(),\n\t\t\t});\n\t\t\trefreshViewer();\n\t\t}\n\t}\n}\n\n} // namespace Info::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/info/stories/info_stories_provider.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"data/data_stories_ids.h\"\n#include \"info/media/info_media_common.h\"\n#include \"info/stories/info_stories_common.h\"\n\nclass DocumentData;\nclass HistoryItem;\nclass PeerData;\nclass History;\n\nnamespace Data {\nclass Story;\n} // namespace Data\n\nnamespace Info {\nclass AbstractController;\n} // namespace Info\n\nnamespace Info::Stories {\n\nclass Provider final\n\t: public Media::ListProvider\n\t, private Media::ListSectionDelegate\n\t, public base::has_weak_ptr {\npublic:\n\texplicit Provider(not_null<AbstractController*> controller);\n\t~Provider();\n\n\tMedia::Type type() override;\n\tbool hasSelectRestriction() override;\n\trpl::producer<bool> hasSelectRestrictionChanges() override;\n\tbool isPossiblyMyItem(not_null<const HistoryItem*> item) override;\n\n\tstd::optional<int> fullCount() override;\n\n\tvoid restart() override;\n\tvoid checkPreload(\n\t\tQSize viewport,\n\t\tnot_null<Media::BaseLayout*> topLayout,\n\t\tnot_null<Media::BaseLayout*> bottomLayout,\n\t\tbool preloadTop,\n\t\tbool preloadBottom) override;\n\tvoid refreshViewer() override;\n\trpl::producer<> refreshed() override;\n\n\tvoid setSearchQuery(QString query) override;\n\tvoid jumpToMessage(MsgId, Fn<void(FullMsgId)> done) override;\n\n\tstd::vector<Media::ListSection> fillSections(\n\t\tnot_null<Overview::Layout::Delegate*> delegate) override;\n\trpl::producer<not_null<Media::BaseLayout*>> layoutRemoved() override;\n\tMedia::BaseLayout *lookupLayout(const HistoryItem *item) override;\n\tbool isMyItem(not_null<const HistoryItem*> item) override;\n\tbool isAfter(\n\t\tnot_null<const HistoryItem*> a,\n\t\tnot_null<const HistoryItem*> b) override;\n\n\tMedia::ListItemSelectionData computeSelectionData(\n\t\tnot_null<const HistoryItem*> item,\n\t\tTextSelection selection) override;\n\tvoid applyDragSelection(\n\t\tMedia::ListSelectedMap &selected,\n\t\tnot_null<const HistoryItem*> fromItem,\n\t\tbool skipFrom,\n\t\tnot_null<const HistoryItem*> tillItem,\n\t\tbool skipTill) override;\n\n\tbool allowSaveFileAs(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) override;\n\tQString showInFolderPath(\n\t\tnot_null<const HistoryItem*> item,\n\t\tnot_null<DocumentData*> document) override;\n\n\tint64 scrollTopStatePosition(not_null<HistoryItem*> item) override;\n\tHistoryItem *scrollTopStateItem(\n\t\tMedia::ListScrollTopState state) override;\n\tvoid saveState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tMedia::ListScrollTopState scrollState) override;\n\tvoid restoreState(\n\t\tnot_null<Media::Memento*> memento,\n\t\tFn<void(Media::ListScrollTopState)> restoreScrollState) override;\n\nprivate:\n\tstatic constexpr auto kMinimalIdsLimit = 16;\n\tstatic constexpr auto kDefaultAroundId = ServerMaxStoryId - 1;\n\n\tbool sectionHasFloatingHeader() override;\n\tQString sectionTitle(not_null<const Media::BaseLayout*> item) override;\n\tbool sectionItemBelongsHere(\n\t\tnot_null<const Media::BaseLayout*> item,\n\t\tnot_null<const Media::BaseLayout*> previous) override;\n\n\tvoid storyRemoved(not_null<Data::Story*> story);\n\tvoid markLayoutsStale();\n\tvoid clearStaleLayouts();\n\tvoid clear();\n\n\t[[nodiscard]] HistoryItem *ensureItem(StoryId id);\n\t[[nodiscard]] Media::BaseLayout *getLayout(\n\t\tStoryId id,\n\t\tnot_null<Overview::Layout::Delegate*> delegate);\n\t[[nodiscard]] std::unique_ptr<Media::BaseLayout> createLayout(\n\t\tStoryId id,\n\t\tnot_null<Overview::Layout::Delegate*> delegate);\n\n\tconst not_null<AbstractController*> _controller;\n\tconst not_null<PeerData*> _peer;\n\tconst not_null<History*> _history;\n\tconst int _albumId = 0;\n\tconst int _addingToAlbumId = 0;\n\n\tStoryId _aroundId = kDefaultAroundId;\n\tint _idsLimit = kMinimalIdsLimit;\n\tData::StoriesIdsSlice _slice;\n\n\tbase::flat_map<StoryId, std::shared_ptr<HistoryItem>> _items;\n\tstd::unordered_map<StoryId, Media::CachedItem> _layouts;\n\trpl::event_stream<not_null<Media::BaseLayout*>> _layoutRemoved;\n\trpl::event_stream<> _refreshed;\n\n\trpl::lifetime _lifetime;\n\trpl::lifetime _viewerLifetime;\n\n};\n\n} // namespace Info::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/info/stories/info_stories_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/stories/info_stories_widget.h\"\n\n#include \"data/data_peer.h\"\n#include \"data/data_stories.h\"\n#include \"info/stories/info_stories_inner_widget.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n\n#include <QtWidgets/QApplication>\n#include <QtWidgets/QScrollBar>\n\nnamespace Info::Stories {\n\nint ArchiveId() {\n\treturn Data::kStoriesAlbumIdArchive;\n}\n\nMemento::Memento(not_null<Controller*> controller)\n: ContentMemento(Tag{\n\tcontroller->storiesPeer(),\n\tcontroller->storiesAlbumId(),\n\tcontroller->storiesAddToAlbumId() })\n, _media(controller) {\n}\n\nMemento::Memento(not_null<PeerData*> peer, int albumId, int addingToAlbumId)\n: ContentMemento(Tag{ peer, albumId, addingToAlbumId })\n, _media(peer, 0, Media::Type::PhotoVideo) {\n}\n\nMemento::~Memento() = default;\n\nSection Memento::section() const {\n\treturn Section(Section::Type::Stories);\n}\n\nobject_ptr<ContentWidget> Memento::createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) {\n\tauto result = object_ptr<Widget>(parent, controller);\n\tresult->setInternalState(geometry, this);\n\treturn result;\n}\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Controller*> controller)\n: ContentWidget(parent, controller)\n, _albumId(controller->key().storiesAlbumId())\n, _inner(\n\tsetupFlexibleInnerWidget(\n\t\tobject_ptr<InnerWidget>(\n\t\t\tthis,\n\t\t\tcontroller,\n\t\t\t_albumId.value(),\n\t\t\tcontroller->key().storiesAddToAlbumId()),\n\t\t_flexibleScroll))\n, _pinnedToTop(_inner->createPinnedToTop(this)) {\n\t_emptyAlbumShown = _inner->albumEmptyValue();\n\t_inner->albumIdChanges() | rpl::on_next([=](int id) {\n\t\tcontroller->showSection(\n\t\t\tMake(controller->storiesPeer(), id),\n\t\t\tWindow::SectionShow::Way::Backward);\n\t}, _inner->lifetime());\n\t_inner->setScrollHeightValue(scrollHeightValue());\n\t_inner->scrollToRequests(\n\t) | rpl::on_next([this](Ui::ScrollToRequest request) {\n\t\tif (request.ymin < 0) {\n\t\t\tscrollTopRestore(\n\t\t\t\tqMin(scrollTopSave(), request.ymax));\n\t\t} else {\n\t\t\tscrollTo(request);\n\t\t}\n\t}, lifetime());\n\n\tif (_pinnedToTop) {\n\t\t_inner->widthValue(\n\t\t) | rpl::on_next([=](int w) {\n\t\t\t_pinnedToTop->resizeToWidth(w);\n\t\t\tsetScrollTopSkip(_pinnedToTop->height());\n\t\t}, _pinnedToTop->lifetime());\n\n\t\t_pinnedToTop->heightValue(\n\t\t) | rpl::on_next([=](int h) {\n\t\t\tsetScrollTopSkip(h);\n\t\t}, _pinnedToTop->lifetime());\n\t}\n\n\tif (_pinnedToTop\n\t\t&& _pinnedToTop->minimumHeight()\n\t\t&& _inner->hasFlexibleTopBar()) {\n\t\t_flexibleScrollHelper = std::make_unique<FlexibleScrollHelper>(\n\t\t\tscroll(),\n\t\t\t_inner,\n\t\t\t_pinnedToTop.get(),\n\t\t\t[=](QMargins margins) {\n\t\t\t\tContentWidget::setPaintPadding(std::move(margins));\n\t\t\t},\n\t\t\t[=](rpl::producer<not_null<QEvent*>> &&events) {\n\t\t\t\tContentWidget::setViewport(std::move(events));\n\t\t\t},\n\t\t\t_flexibleScroll);\n\t}\n\n\trpl::combine(\n\t\t_albumId.value(),\n\t\t_emptyAlbumShown.value()\n\t) | rpl::on_next([=] {\n\t\trefreshBottom();\n\t}, _inner->lifetime());\n\n\t_inner->backRequest() | rpl::on_next([=] {\n\t\tcheckBeforeClose([=] { controller->showBackFromStack(); });\n\t}, _inner->lifetime());\n}\n\nvoid Widget::setInnerFocus() {\n\t_inner->setFocus();\n}\n\nvoid Widget::setIsStackBottom(bool isStackBottom) {\n\tContentWidget::setIsStackBottom(isStackBottom);\n\t_inner->setIsStackBottom(isStackBottom);\n}\n\nbool Widget::showInternal(not_null<ContentMemento*> memento) {\n\tif (!controller()->validateMementoPeer(memento)) {\n\t\treturn false;\n\t}\n\tif (auto storiesMemento = dynamic_cast<Memento*>(memento.get())) {\n\t\tconst auto myId = controller()->key().storiesAlbumId();\n\t\tconst auto hisId = storiesMemento->storiesAlbumId();\n\t\tconstexpr auto kArchive = Data::kStoriesAlbumIdArchive;\n\t\tif (myId == hisId) {\n\t\t\trestoreState(storiesMemento);\n\t\t\treturn true;\n\t\t} else if (myId != kArchive && hisId != kArchive) {\n\t\t\t_albumId = hisId;\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Widget::setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento) {\n\t\tsetGeometry(geometry);\n\tUi::SendPendingMoveResizeEvents(this);\n\trestoreState(memento);\n}\n\nstd::shared_ptr<ContentMemento> Widget::doCreateMemento() {\n\tauto result = std::make_shared<Memento>(controller());\n\tsaveState(result.get());\n\treturn result;\n}\n\nvoid Widget::saveState(not_null<Memento*> memento) {\n\tmemento->setScrollTop(scrollTopSave());\n\t_inner->saveState(memento);\n}\n\nvoid Widget::restoreState(not_null<Memento*> memento) {\n\t_inner->restoreState(memento);\n\tscrollTopRestore(memento->scrollTop());\n}\n\nvoid Widget::refreshBottom() {\n\tconst auto albumId = _albumId.current();\n\tconst auto withButton = (albumId != Data::kStoriesAlbumIdSaved)\n\t\t&& (albumId != Data::kStoriesAlbumIdArchive)\n\t\t&& controller()->storiesPeer()->canEditStories()\n\t\t&& !_emptyAlbumShown.current();\n\tconst auto wasBottom = _pinnedToBottom ? _pinnedToBottom->height() : 0;\n\tdelete _pinnedToBottom.data();\n\tif (!withButton) {\n\t\tsetScrollBottomSkip(0);\n\t\t_hasPinnedToBottom = false;\n\t} else {\n\t\tsetupBottomButton(wasBottom);\n\t}\n\n\tif (_pinnedToBottom) {\n\t\tconst auto processHeight = [=] {\n\t\t\tsetScrollBottomSkip(_pinnedToBottom->height());\n\t\t\t_pinnedToBottom->moveToLeft(\n\t\t\t\t_pinnedToBottom->x(),\n\t\t\t\theight() - _pinnedToBottom->height());\n\t\t};\n\n\t\t_inner->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\t_pinnedToBottom->resizeToWidth(s.width());\n\t\t}, _pinnedToBottom->lifetime());\n\n\t\trpl::combine(\n\t\t\t_pinnedToBottom->heightValue(),\n\t\t\theightValue()\n\t\t) | rpl::on_next(processHeight, _pinnedToBottom->lifetime());\n\t}\n}\n\nvoid Widget::setupBottomButton(int wasBottomHeight) {\n\t_pinnedToBottom = Ui::CreateChild<Ui::SlideWrap<Ui::RpWidget>>(\n\t\tthis,\n\t\tobject_ptr<Ui::RpWidget>(this));\n\tconst auto wrap = _pinnedToBottom.data();\n\twrap->toggle(false, anim::type::instant);\n\n\tconst auto bottom = wrap->entity();\n\tbottom->show();\n\n\tconst auto button = Ui::CreateChild<Ui::RoundButton>(\n\t\tbottom,\n\t\trpl::single(QString()),\n\t\tst::collectionEditBox.button);\n\tbutton->setText(tr::lng_stories_album_add_button(\n\t) | rpl::map([](const QString &text) {\n\t\treturn Ui::Text::IconEmoji(&st::collectionAddIcon).append(text);\n\t}));\n\tbutton->show();\n\t_hasPinnedToBottom = true;\n\n\tbutton->setClickedCallback([=] {\n\t\tif (const auto id = _albumId.current()) {\n\t\t\t_inner->editAlbumStories(id);\n\t\t} else {\n\t\t\trefreshBottom();\n\t\t}\n\t});\n\n\tconst auto buttonTop = st::boxRadius;\n\tbottom->widthValue() | rpl::on_next([=](int width) {\n\t\tconst auto normal = width - 2 * buttonTop;\n\t\tbutton->resizeToWidth(normal);\n\t\tconst auto buttonLeft = (width - normal) / 2;\n\t\tbutton->moveToLeft(buttonLeft, buttonTop);\n\t}, button->lifetime());\n\n\tbutton->heightValue() | rpl::on_next([=](int height) {\n\t\tbottom->resize(bottom->width(), st::boxRadius + height);\n\t}, button->lifetime());\n\n\tif (_shown) {\n\t\twrap->toggle(\n\t\t\ttrue,\n\t\t\twasBottomHeight ? anim::type::instant : anim::type::normal);\n\t}\n}\n\nvoid Widget::showFinished() {\n\t_shown = true;\n\tif (const auto bottom = _pinnedToBottom.data()) {\n\t\tbottom->toggle(true, anim::type::normal);\n\t}\n\t_inner->showFinished();\n}\n\nrpl::producer<SelectedItems> Widget::selectedListValue() const {\n\treturn _inner->selectedListValue();\n}\n\nvoid Widget::selectionAction(SelectionAction action) {\n\t_inner->selectionAction(action);\n}\n\nrpl::producer<QString> Widget::title() {\n\tconst auto peer = controller()->key().storiesPeer();\n\treturn (controller()->key().storiesAlbumId() == ArchiveId())\n\t\t? tr::lng_stories_archive_title()\n\t\t: (peer && peer->isSelf())\n\t\t? tr::lng_menu_my_profile()\n\t\t: tr::lng_stories_my_title();\n}\n\nrpl::producer<bool> Widget::desiredBottomShadowVisibility() {\n\treturn _hasPinnedToBottom.value();\n}\n\nstd::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer, int albumId) {\n\treturn std::make_shared<Info::Memento>(\n\t\tstd::vector<std::shared_ptr<ContentMemento>>(\n\t\t\t1,\n\t\t\tstd::make_shared<Memento>(peer, albumId, 0)));\n}\n\n} // namespace Info::Stories\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/stories/info_stories_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"info/info_content_widget.h\"\n#include \"info/media/info_media_widget.h\"\n#include \"info/stories/info_stories_common.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace Ui {\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Info::Stories {\n\nclass InnerWidget;\n\nclass Memento final : public ContentMemento {\npublic:\n\tMemento(not_null<Controller*> controller);\n\tMemento(not_null<PeerData*> peer, int albumId, int addingToAlbumId);\n\t~Memento();\n\n\tobject_ptr<ContentWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> controller,\n\t\tconst QRect &geometry) override;\n\n\tSection section() const override;\n\n\t[[nodiscard]] Media::Memento &media() {\n\t\treturn _media;\n\t}\n\t[[nodiscard]] const Media::Memento &media() const {\n\t\treturn _media;\n\t}\n\nprivate:\n\tMedia::Memento _media;\n\n};\n\nclass Widget final : public ContentWidget {\npublic:\n\tWidget(QWidget *parent, not_null<Controller*> controller);\n\n\tvoid setInnerFocus() override;\n\tvoid setIsStackBottom(bool isStackBottom) override;\n\n\tbool showInternal(\n\t\tnot_null<ContentMemento*> memento) override;\n\n\tvoid setInternalState(\n\t\tconst QRect &geometry,\n\t\tnot_null<Memento*> memento);\n\n\trpl::producer<SelectedItems> selectedListValue() const override;\n\tvoid selectionAction(SelectionAction action) override;\n\n\trpl::producer<QString> title() override;\n\n\trpl::producer<bool> desiredBottomShadowVisibility() override;\n\n\tvoid showFinished() override;\n\nprivate:\n\tvoid saveState(not_null<Memento*> memento);\n\tvoid restoreState(not_null<Memento*> memento);\n\n\tvoid setupBottomButton(int wasBottomHeight);\n\tvoid refreshBottom();\n\n\tstd::shared_ptr<ContentMemento> doCreateMemento() override;\n\n\tFlexibleScrollData _flexibleScroll;\n\trpl::variable<int> _albumId;\n\tInnerWidget *_inner = nullptr;\n\tbase::weak_qptr<Ui::RpWidget> _pinnedToTop;\n\tQPointer<Ui::SlideWrap<Ui::RpWidget>> _pinnedToBottom;\n\trpl::variable<bool> _hasPinnedToBottom;\n\trpl::variable<bool> _emptyAlbumShown;\n\tbool _shown = false;\n\tstd::unique_ptr<FlexibleScrollHelper> _flexibleScrollHelper;\n\n};\n\n[[nodiscard]] std::shared_ptr<Info::Memento> Make(\n\tnot_null<PeerData*> peer,\n\tint albumId = 0);\n\n} // namespace Info::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_bubble_wrap.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/userpic/info_userpic_bubble_wrap.h\"\n\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_info_userpic_builder.h\"\n\nnamespace Ui {\nnamespace {\n\nvoid PaintExcludeTopShadow(QPainter &p, int radius, const QRect &r) {\n\tconstexpr auto kHorizontalOffset = 1.;\n\tconstexpr auto kVerticalOffset = 2.;\n\tconstexpr auto kOpacityStep1 = 0.2;\n\tconstexpr auto kOpacityStep2 = 0.4;\n\tconst auto opacity = p.opacity();\n\tconst auto hOffset = style::ConvertScale(kHorizontalOffset);\n\tconst auto vOffset = style::ConvertScale(kVerticalOffset);\n\tp.setOpacity(opacity * kOpacityStep1);\n\tp.drawRoundedRect(\n\t\tr + QMarginsF(hOffset, -radius, hOffset, 0),\n\t\tradius,\n\t\tradius);\n\tp.setOpacity(opacity * kOpacityStep1);\n\tp.drawRoundedRect(\n\t\tr + QMarginsF(0, 0, 0, vOffset),\n\t\tradius,\n\t\tradius);\n\tp.setOpacity(opacity * kOpacityStep2);\n\tp.drawRoundedRect(\n\t\tr + QMarginsF(0, 0, 0, vOffset / 2.),\n\t\tradius,\n\t\tradius);\n\tp.setOpacity(opacity);\n}\n\n} // namespace\n\nQRect BubbleWrapInnerRect(const QRect &r) {\n\treturn r - st::userpicBuilderEmojiBubblePadding;\n}\n\nnot_null<Ui::RpWidget*> AddBubbleWrap(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst QSize &size) {\n\tconst auto bubble = container->add(\n\t\tobject_ptr<Ui::RpWidget>(container),\n\t\tstyle::al_top);\n\tbubble->resize(size);\n\tbubble->setNaturalWidth(size.width());\n\n\tauto cached = QImage(\n\t\tsize * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tcached.setDevicePixelRatio(style::DevicePixelRatio());\n\tcached.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&cached);\n\t\tconst auto innerRect = BubbleWrapInnerRect(bubble->rect());\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto radius = st::bubbleRadiusSmall;\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::shadowFg);\n\t\tPaintExcludeTopShadow(p, radius, innerRect);\n\t\tp.setBrush(st::boxBg);\n\t\tp.drawRoundedRect(innerRect, radius, radius);\n\t}\n\n\tbubble->paintRequest(\n\t) | rpl::on_next([bubble, cached = std::move(cached)] {\n\t\tauto p = QPainter(bubble);\n\t\tp.drawImage(0, 0, cached);\n\t}, bubble->lifetime());\n\n\treturn bubble;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_bubble_wrap.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\n\nclass VerticalLayout;\n\n[[nodiscard]] QRect BubbleWrapInnerRect(const QRect &r);\n\nnot_null<Ui::RpWidget*> AddBubbleWrap(\n\tnot_null<Ui::VerticalLayout*> container,\n\tconst QSize &size);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_builder.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\n\nusing \"boxes/boxes.style\";\nusing \"ui/widgets/widgets.style\";\n\nuserpicBuilderEmojiPreviewPadding: margins(0px, 36px, 0px, 8px);\nuserpicBuilderEmojiSubtitle: FlatLabel(defaultFlatLabel) {\n\talign: align(center);\n\tminWidth: 356px;\n\tmaxHeight: 24px;\n\ttextFg: windowSubTextFg;\n}\nuserpicBuilderEmojiSubtitlePadding: margins(0px, 9px, 0px, 2px);\nuserpicBuilderEmojiBubblePaletteWidth: 356px;\nuserpicBuilderEmojiBubblePalettePadding: margins(12px, 8px, 12px, 8px);\n\nuserpicBuilderEmojiSelectorLeft: 5px;\n\nuserpicBuilderEmojiButton: RoundButton(defaultBoxButton) {\n\ttextFg: boxTextFg;\n\ttextFgOver: boxTextFg;\n\ttextBg: boxDividerBg;\n\ttextBgOver: boxDividerBg;\n\n\tripple: universalRippleAnimation;\n}\nuserpicBuilderEmojiBackButton: IconButton(backButton) {\n\ticon: icon {{ \"box_button_back\", boxTextFg }};\n\ticonOver: icon {{ \"box_button_back\", boxTextFg }};\n\n\tripple: universalRippleAnimation;\n}\n\nuserpicBuilderEmojiBackPosiiton: point(8px, 8px);\nuserpicBuilderEmojiSavePosiiton: point(7px, 12px);\n\nuserpicBuilderEmojiAccentColorSize: 30px;\nuserpicBuilderEmojiBubblePadding: margins(5px, 5px, 5px, 5px);\n\nuserpicBuilderEmojiLayerMinHeight: 496px;\nuserpicBuilderEmojiSelectorMinHeight: 216px;\nuserpicBuilderEmojiSelectorTogglePosition: point(6px, 6px);\n\nuserpicBuilderEmojiColorMinus: IconButton(defaultIconButton) {\n\twidth: userpicBuilderEmojiAccentColorSize;\n\theight: userpicBuilderEmojiAccentColorSize;\n\n\ticon: icon {{ \"info/info_colors_remove\", menuIconFg }};\n\ticonOver: icon {{ \"info/info_colors_remove\", menuIconFg }};\n\n\trippleAreaSize: userpicBuilderEmojiAccentColorSize;\n\trippleAreaPosition: point(0px, 0px);\n\tripple: universalRippleAnimation;\n}\n\nuserpicBuilderEmojiColorPlus: IconButton(userpicBuilderEmojiColorMinus) {\n\ticon: icon {{ \"info/info_colors_add\", menuIconFg }};\n\ticonOver: icon {{ \"info/info_colors_add\", menuIconFg }};\n}\n\nuserpicBuilderEmojiToggleStickersIcon: icon {{ \"menu/stickers\", emojiIconFg }};\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_color_circle_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/userpic/info_userpic_color_circle_button.h\"\n\n#include \"settings/sections/settings_chat.h\" // Settings::PaintRoundColorButton.\n#include \"ui/painter.h\"\n\nnamespace UserpicBuilder {\n\nvoid CircleButton::setIndex(int index) {\n\t_index = index;\n}\n\nint CircleButton::index() const {\n\treturn _index;\n}\n\nvoid CircleButton::setBrush(QBrush brush) {\n\t_brush = brush;\n\tupdate();\n}\n\nvoid CircleButton::setSelectedProgress(float64 progress) {\n\tif (_selectedProgress != progress) {\n\t\t_selectedProgress = progress;\n\t\tupdate();\n\t}\n}\n\nvoid CircleButton::paintEvent(QPaintEvent *event) {\n\tauto p = QPainter(this);\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto h = height();\n\tSettings::PaintRoundColorButton(p, h, _brush, _selectedProgress);\n}\n\n} // namespace UserpicBuilder\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_color_circle_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/abstract_button.h\"\n\nnamespace UserpicBuilder {\n\nclass CircleButton final : public Ui::AbstractButton {\npublic:\n\tusing Ui::AbstractButton::AbstractButton;\n\n\tvoid setIndex(int index);\n\t[[nodiscard]] int index() const;\n\tvoid setBrush(QBrush brush);\n\tvoid setSelectedProgress(float64 progress);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *event) override;\n\n\tint _index = 0;\n\tfloat64 _selectedProgress = 0.;\n\tQBrush _brush;\n\n};\n\n} // namespace UserpicBuilder\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_colors_editor.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/userpic/info_userpic_colors_editor.h\"\n\n#include \"base/random.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"info/userpic/info_userpic_emoji_builder_preview.h\"\n#include \"info/userpic/info_userpic_color_circle_button.h\"\n#include \"info/userpic/info_userpic_emoji_builder_common.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/widgets/color_editor.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/rect.h\"\n#include \"styles/style_info_userpic_builder.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace UserpicBuilder {\nnamespace {\n\nconstexpr auto kMaxColors = int(4);\n\n[[nodiscard]] QColor RandomColor(const QColor &c) {\n\tauto random = bytes::vector(2);\n\tbase::RandomFill(random.data(), random.size());\n\tauto result = QColor();\n\tresult.setHslF(\n\t\t(uchar(random[0]) % 100) / 100.,\n\t\t(uchar(random[1]) % 50) / 100. + 0.5,\n\t\tc.lightnessF());\n\treturn result;\n}\n\nclass ColorsLine final : public Ui::RpWidget {\npublic:\n\tusing Chosen = CircleButton;\n\tusing Success = bool;\n\tColorsLine(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<std::vector<QColor>*> colors);\n\n\tvoid init();\n\tvoid fillButtons();\n\n\t[[nodiscard]] Chosen *chosen() const;\n\t[[nodiscard]] rpl::producer<Chosen*> chosenChanges() const;\n\nprivate:\n\tstruct ButtonState {\n\t\tbool shown = false;\n\t\tint left = 0;\n\t};\n\t[[nodiscard]] std::vector<ButtonState> calculatePositionFor(int count);\n\tvoid processChange(\n\t\tconst std::vector<QColor> wasColors,\n\t\tconst std::vector<QColor> nowColors);\n\tvoid setLastChosen() const;\n\n\tconst not_null<std::vector<QColor>*> _colors;\n\n\tbase::unique_qptr<Ui::RpWidget> _container;\n\n\tstd::vector<not_null<CircleButton*>> _colorButtons;\n\tstd::vector<not_null<Ui::FadeWrap<Ui::RpWidget>*>> _wraps;\n\n\tUi::Animations::Simple _chooseAnimation;\n\tUi::Animations::Simple _positionAnimation;\n\tChosen *_chosen = nullptr;\n\n\trpl::event_stream<Chosen*> _chosenChanges;\n\n};\n\nColorsLine::ColorsLine(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<std::vector<QColor>*> colors)\n: Ui::RpWidget(parent)\n, _colors(colors) {\n}\n\nvoid ColorsLine::init() {\n\tfillButtons();\n\tprocessChange(*_colors, *_colors);\n\tsetLastChosen();\n}\n\nvoid ColorsLine::fillButtons() {\n\t_container = base::make_unique_q<Ui::RpWidget>(this);\n\tconst auto container = _container.get();\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tcontainer->setGeometry(Rect(s));\n\t}, container->lifetime());\n\n\tconst auto minus = Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::IconButton>(\n\t\t\tcontainer,\n\t\t\tst::userpicBuilderEmojiColorMinus));\n\t_wraps.push_back(minus);\n\tminus->toggle(_colors->size() > 1, anim::type::instant);\n\tminus->entity()->setClickedCallback([=] {\n\t\tif (_colors->size() < 2) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto wasColors = *_colors;\n\t\t_colors->erase(_colors->end() - 1);\n\t\tconst auto nowColors = *_colors;\n\t\tprocessChange(wasColors, nowColors);\n\t\tsetLastChosen();\n\t});\n\n\tfor (auto i = 0; i < kMaxColors; i++) {\n\t\tconst auto wrap = Ui::CreateChild<Ui::FadeWrap<CircleButton>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<CircleButton>(container));\n\t\tconst auto button = wrap->entity();\n\t\tbutton->resize(height(), height());\n\t\tbutton->setIndex(i);\n\t\t_wraps.push_back(wrap);\n\t\t_colorButtons.push_back(button);\n\t\tbutton->setClickedCallback([=] {\n\t\t\tconst auto wasChosen = _chosen;\n\t\t\t_chosen = button;\n\t\t\tconst auto nowChosen = _chosen;\n\t\t\t_chosenChanges.fire_copy(_chosen);\n\n\t\t\t_chooseAnimation.stop();\n\t\t\t_chooseAnimation.start([=](float64 progress) {\n\t\t\t\tif (wasChosen) {\n\t\t\t\t\twasChosen->setSelectedProgress(1. - progress);\n\t\t\t\t}\n\t\t\t\tnowChosen->setSelectedProgress(progress);\n\t\t\t}, 0., 1., st::universalDuration);\n\t\t});\n\t\tif (i < _colors->size()) {\n\t\t\tbutton->setBrush((*_colors)[i]);\n\t\t} else {\n\t\t\twrap->hide(anim::type::instant);\n\t\t}\n\t}\n\n\tconst auto plus = Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::IconButton>(\n\t\t\tcontainer,\n\t\t\tst::userpicBuilderEmojiColorPlus));\n\t_wraps.push_back(plus);\n\tplus->toggle(_colors->size() < kMaxColors, anim::type::instant);\n\tplus->entity()->setClickedCallback([=] {\n\t\tif (_colors->size() >= kMaxColors) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto wasColors = *_colors;\n\t\t_colors->push_back(RandomColor(_colors->back()));\n\t\tconst auto nowColors = *_colors;\n\t\tprocessChange(wasColors, nowColors);\n\t\tsetLastChosen();\n\t});\n\tfor (const auto &wrap : _wraps) {\n\t\twrap->setDuration(st::universalDuration);\n\t}\n}\n\nstd::vector<ColorsLine::ButtonState> ColorsLine::calculatePositionFor(\n\t\tint count) {\n\t// Minus - Color - Color - Color - Color - Plus.\n\tauto result = std::vector<ButtonState>(6);\n\tconst auto fullWidth = _container->width();\n\tconst auto width = _container->height();\n\tconst auto colorsWidth = width * count + width * (count - 1);\n\tconst auto left = (fullWidth - colorsWidth) / 2;\n\tfor (auto i = 0; i < _colorButtons.size(); i++) {\n\t\tresult[i + 1] = {\n\t\t\t.shown = (i < count),\n\t\t\t.left = left + (i * width * 2),\n\t\t};\n\t}\n\tresult[0] = {\n\t\t.shown = (count > 1),\n\t\t.left = (left - width * 2),\n\t};\n\tresult[result.size() - 1] = {\n\t\t.shown = (count < kMaxColors),\n\t\t.left = (left + colorsWidth + width),\n\t};\n\treturn result;\n}\n\nvoid ColorsLine::processChange(\n\t\tconst std::vector<QColor> wasColors,\n\t\tconst std::vector<QColor> nowColors) {\n\tconst auto wasPosition = calculatePositionFor(wasColors.size());\n\tconst auto nowPosition = calculatePositionFor(nowColors.size());\n\tfor (auto i = 0; i < nowPosition.size(); i++) {\n\t\tconst auto colorIndex = i - 1;\n\t\tif ((colorIndex > 0) && (colorIndex < _colors->size())) {\n\t\t\t_colorButtons[colorIndex]->setBrush((*_colors)[colorIndex]);\n\t\t}\n\t\t_wraps[i]->toggle(nowPosition[i].shown, anim::type::normal);\n\t}\n\t_positionAnimation.stop();\n\t_positionAnimation.start([=](float64 value) {\n\t\tfor (auto i = 0; i < nowPosition.size(); i++) {\n\t\t\tconst auto wasLeft = wasPosition[i].left;\n\t\t\tconst auto nowLeft = nowPosition[i].left;\n\t\t\tconst auto left = anim::interpolate(wasLeft, nowLeft, value);\n\t\t\t_wraps[i]->moveToLeft(left, 0);\n\t\t}\n\t}, 0., 1., st::universalDuration);\n}\n\nvoid ColorsLine::setLastChosen() const {\n\tfor (auto i = 0; i < _colorButtons.size(); i++) {\n\t\tif (i == (_colors->size() - 1)) {\n\t\t\t_colorButtons[i]->clicked({}, Qt::LeftButton);\n\t\t}\n\t}\n}\n\nColorsLine::Chosen *ColorsLine::chosen() const {\n\treturn _chosen;\n}\n\nrpl::producer<ColorsLine::Chosen*> ColorsLine::chosenChanges() const {\n\treturn _chosenChanges.events();\n}\n\n} // namespace\n\nobject_ptr<Ui::RpWidget> CreateGradientEditor(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tDocumentData *document,\n\t\tstd::vector<QColor> startColors,\n\t\tBothWayCommunication<std::vector<QColor>> communication) {\n\tauto container = object_ptr<Ui::VerticalLayout>(parent.get());\n\n\tstruct State {\n\t\tstd::vector<QColor> colors;\n\t};\n\tconst auto preview = container->add(\n\t\tobject_ptr<EmojiUserpic>(\n\t\t\tcontainer,\n\t\t\tSize(st::defaultUserpicButton.photoSize),\n\t\t\tfalse),\n\t\tstyle::al_top);\n\tpreview->setDuration(0);\n\tif (document) {\n\t\tpreview->setDocument(document);\n\t}\n\n\tUi::AddSkip(container);\n\tUi::AddDivider(container);\n\tUi::AddSkip(container);\n\n\tconst auto state = container->lifetime().make_state<State>();\n\tstate->colors = std::move(startColors);\n\tconst auto buttonsContainer = container->add(object_ptr<ColorsLine>(\n\t\tcontainer,\n\t\t&state->colors));\n\tbuttonsContainer->resize(0, st::userpicBuilderEmojiAccentColorSize);\n\n\tUi::AddSkip(container);\n\tUi::AddDivider(container);\n\tUi::AddSkip(container);\n\n\tauto ownedEditor = object_ptr<ColorEditor>(\n\t\tcontainer,\n\t\tColorEditor::Mode::HSL,\n\t\tstate->colors.back());\n\tcontainer->resizeToWidth(ownedEditor->width());\n\tconst auto editor = container->add(std::move(ownedEditor));\n\n\tbuttonsContainer->chosenChanges(\n\t) | rpl::on_next([=](ColorsLine::Chosen *chosen) {\n\t\tif (chosen) {\n\t\t\tconst auto color = state->colors[chosen->index()];\n\t\t\teditor->showColor(color);\n\t\t\teditor->setCurrent(color);\n\t\t}\n\t}, editor->lifetime());\n\n\tconst auto save = crl::guard(container.data(), [=] {\n\t\tcommunication.result(state->colors);\n\t});\n\t// editor->submitRequests(\n\t// ) | rpl::on_next([=] {\n\t// }, editor->lifetime());\n\teditor->colorValue(\n\t) | rpl::on_next([=](QColor c) {\n\t\tif (const auto chosen = buttonsContainer->chosen()) {\n\t\t\tchosen->setBrush(c);\n\t\t\tstate->colors[chosen->index()] = c;\n\t\t}\n\t\tpreview->setGradientColors(state->colors);\n\t}, preview->lifetime());\n\n\tbase::take(\n\t\tcommunication.triggers\n\t) | rpl::on_next([=] {\n\t\tsave();\n\t}, container->lifetime());\n\n\tbuttonsContainer->init();\n\n\treturn container;\n}\n\n} // namespace UserpicBuilder\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_colors_editor.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\ntemplate <typename Object>\nclass object_ptr;\n\nclass DocumentData;\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace UserpicBuilder {\n\ntemplate <typename Result>\nstruct BothWayCommunication;\n\n[[nodiscard]] object_ptr<Ui::RpWidget> CreateGradientEditor(\n\tnot_null<Ui::RpWidget*> parent,\n\tDocumentData *document,\n\tstd::vector<QColor> startColors,\n\tBothWayCommunication<std::vector<QColor>> communication);\n\n} // namespace UserpicBuilder\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/userpic/info_userpic_emoji_builder.h\"\n\n#include \"info/userpic/info_userpic_emoji_builder_common.h\"\n#include \"info/userpic/info_userpic_emoji_builder_layer.h\"\n#include \"info/userpic/info_userpic_emoji_builder_widget.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_info_userpic_builder.h\"\n\nnamespace UserpicBuilder {\n\nvoid ShowLayer(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tStartData data,\n\t\tFn<void(UserpicBuilder::Result)> &&doneCallback) {\n\tauto layer = std::make_unique<LayerWidget>();\n\tconst auto layerRaw = layer.get();\n\t{\n\t\tstruct State {\n\t\t\trpl::event_stream<> clicks;\n\t\t};\n\t\tconst auto state = layer->lifetime().make_state<State>();\n\n\t\tconst auto content = CreateUserpicBuilder(\n\t\t\tlayerRaw,\n\t\t\tcontroller,\n\t\t\tdata,\n\t\t\tBothWayCommunication<UserpicBuilder::Result>{\n\t\t\t\t.triggers = state->clicks.events(),\n\t\t\t\t.result = [=, done = std::move(doneCallback)](Result r) {\n\t\t\t\t\tdone(std::move(r));\n\t\t\t\t\tlayerRaw->closeLayer();\n\t\t\t\t},\n\t\t\t});\n\t\tconst auto save = Ui::CreateChild<Ui::RoundButton>(\n\t\t\tcontent.get(),\n\t\t\ttr::lng_connection_save(),\n\t\t\tst::userpicBuilderEmojiButton);\n\t\tcontent->sizeValue(\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto &p = st::userpicBuilderEmojiSavePosiiton;\n\t\t\tsave->moveToRight(p.x(), p.y());\n\t\t}, save->lifetime());\n\n\t\tsave->clicks() | rpl::to_empty | rpl::start_to_stream(\n\t\t\tstate->clicks,\n\t\t\tsave->lifetime());\n\n\t\tconst auto back = Ui::CreateChild<Ui::IconButton>(\n\t\t\tcontent.get(),\n\t\t\tst::userpicBuilderEmojiBackButton);\n\t\tback->setClickedCallback([=] {\n\t\t\tlayerRaw->closeLayer();\n\t\t});\n\t\tcontent->sizeValue(\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto &p = st::userpicBuilderEmojiBackPosiiton;\n\t\t\tback->moveToLeft(p.x(), p.y());\n\t\t}, back->lifetime());\n\n\t\tlayer->setContent(content);\n\t}\n\n\tcontroller->showLayer(std::move(layer), Ui::LayerOption::KeepOther);\n}\n\n} // namespace UserpicBuilder\n\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace UserpicBuilder {\n\nstruct Result;\nstruct StartData;\n\nvoid ShowLayer(\n\tnot_null<Window::SessionController*> controller,\n\tStartData data,\n\tFn<void(UserpicBuilder::Result)> &&doneCallback);\n\n} // namespace UserpicBuilder\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/userpic/info_userpic_emoji_builder_common.h\"\n\n#include \"ui/image/image_prepare.h\"\n#include \"ui/userpic_view.h\" // ForumUserpicRadiusMultiplier.\n\nnamespace UserpicBuilder {\n\n[[nodiscard]] QImage GenerateGradient(\n\t\tconst QSize &size,\n\t\tconst std::vector<QColor> &colors,\n\t\tbool circle,\n\t\tbool roundForumRect) {\n\tconstexpr auto kRotation = int(45);\n\tauto gradient = Images::GenerateGradient(size, colors, kRotation);\n\tif (!circle && !roundForumRect) {\n\t\treturn gradient;\n\t}\n\tconst auto processModifier = [&](QImage &&i) {\n\t\tif (circle) {\n\t\t\treturn Images::Circle(std::move(i));\n\t\t} else if (roundForumRect) {\n\t\t\tconst auto radius = std::min(i.height(), i.width())\n\t\t\t\t* Ui::ForumUserpicRadiusMultiplier();\n\t\t\treturn Images::Round(\n\t\t\t\tstd::move(i),\n\t\t\t\tImages::CornersMask(radius / style::DevicePixelRatio()));\n\t\t} else {\n\t\t\treturn std::move(i);\n\t\t}\n\t};\n\tif (style::DevicePixelRatio() == 1) {\n\t\treturn processModifier(std::move(gradient));\n\t}\n\tauto image = QImage(\n\t\tsize * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\timage.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&image);\n\t\tp.drawImage(QRect(QPoint(), size), gradient);\n\t}\n\treturn processModifier(std::move(image));\n}\n\n} // namespace UserpicBuilder\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace UserpicBuilder {\n\nstruct Result {\n\tQImage&& image;\n\tDocumentId id = 0;\n\tstd::vector<QColor> colors;\n};\n\n[[nodiscard]] QImage GenerateGradient(\n\tconst QSize &size,\n\tconst std::vector<QColor> &colors,\n\tbool circle = true,\n\tbool roundForumRect = false);\n\nstruct StartData {\n\tDocumentId documentId = DocumentId(0);\n\tint builderColorIndex = 0;\n\trpl::producer<std::vector<DocumentId>> documents;\n\tstd::vector<QColor> gradientEditorColors;\n\tbool isForum = false;\n};\n\ntemplate <typename Result>\nstruct BothWayCommunication {\n\trpl::producer<> triggers;\n\tFn<void(Result)> result;\n};\n\n} // namespace UserpicBuilder\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_layer.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/userpic/info_userpic_emoji_builder_layer.h\"\n\n#include \"styles/style_info.h\"\n#include \"styles/style_info_userpic_builder.h\"\n#include \"styles/style_layers.h\"\n\nnamespace UserpicBuilder {\n\nLayerWidget::LayerWidget()\n: _corners(Ui::PrepareCornerPixmaps(st::boxRadius, st::boxDividerBg)) {\n}\n\nvoid LayerWidget::setContent(not_null<Ui::RpWidget*> content) {\n\t_content = content;\n}\n\nvoid LayerWidget::parentResized() {\n\tExpects(_content != nullptr);\n\tconst auto parentSize = parentWidget()->size();\n\tconst auto currentHeight = resizeGetHeight(0);\n\tconst auto currentWidth = _content->width();\n\tresizeToWidth(currentWidth);\n\tmoveToLeft(\n\t\t(parentSize.width() - currentWidth) / 2,\n\t\t(parentSize.height() - currentHeight) / 2);\n}\n\nbool LayerWidget::closeByOutsideClick() const {\n\treturn false;\n}\n\nint LayerWidget::resizeGetHeight(int newWidth) {\n\tExpects(_content != nullptr);\n\t_content->resizeToWidth(st::infoDesiredWidth);\n\treturn st::userpicBuilderEmojiLayerMinHeight;\n}\n\nvoid LayerWidget::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tUi::FillRoundRect(p, rect(), st::boxDividerBg, _corners);\n}\n\n} // namespace UserpicBuilder\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_layer.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/layer_widget.h\"\n\n#include \"ui/cached_round_corners.h\"\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace UserpicBuilder {\n\nclass LayerWidget : public Ui::LayerWidget {\npublic:\n\tLayerWidget();\n\n\tvoid setContent(not_null<Ui::RpWidget*> content);\n\n\tvoid parentResized() override;\n\tbool closeByOutsideClick() const override;\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tconst Ui::CornersPixmaps _corners;\n\tUi::RpWidget *_content;\n\n};\n\n} // namespace UserpicBuilder\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_menu_item.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/userpic/info_userpic_emoji_builder_menu_item.h\"\n\n#include \"base/random.h\"\n#include \"base/timer.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"info/userpic/info_userpic_emoji_builder.h\"\n#include \"info/userpic/info_userpic_emoji_builder_common.h\"\n#include \"info/userpic/info_userpic_emoji_builder_widget.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/menu/menu_common.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <random>\n\nnamespace UserpicBuilder {\nnamespace {\n\nconstexpr auto kTimeout = crl::time(1500);\n\nclass StickerProvider final {\npublic:\n\tStickerProvider(not_null<Data::Session*> owner);\n\n\tvoid setDocuments(std::vector<DocumentId> documents);\n\t[[nodiscard]] DocumentId documentId() const;\n\t[[nodiscard]] auto documentChanged() const\n\t-> rpl::producer<not_null<DocumentData*>>;\n\nprivate:\n\tvoid processDocumentIndex(int documentIndex);\n\t[[nodiscard]] DocumentData *lookupAndRememberSticker(int documentIndex);\n\t[[nodiscard]] std::pair<DocumentData*, int> lookupSticker(\n\t\tint documentIndex) const;\n\n\tconst not_null<Data::Session*> _owner;\n\n\tint _documentIndex = 0;\n\tstd::vector<DocumentId> _shuffledDocuments;\n\n\tbase::Timer _timer;\n\n\trpl::event_stream<not_null<DocumentData*>> _documentChanged;\n\trpl::lifetime _resolvingLifetime;\n\trpl::lifetime _downloadFinishedLifetime;\n\n};\n\nStickerProvider::StickerProvider(not_null<Data::Session*> owner)\n: _owner(owner) {\n\t_timer.setCallback([=] {\n\t\t_documentIndex++;\n\t\tif (_documentIndex >= _shuffledDocuments.size()) {\n\t\t\t_documentIndex = 0;\n\t\t}\n\t\tprocessDocumentIndex(_documentIndex);\n\t});\n}\n\nDocumentId StickerProvider::documentId() const {\n\tconst auto &[document, index] = lookupSticker(_documentIndex);\n\treturn document ? document->id : DocumentId(0);\n}\n\nvoid StickerProvider::setDocuments(std::vector<DocumentId> documents) {\n\tif (documents.empty()) {\n\t\treturn;\n\t}\n\tauto rd = std::random_device();\n\tranges::shuffle(documents, std::mt19937(rd()));\n\t_shuffledDocuments = std::move(documents);\n\t_documentIndex = 0;\n\tprocessDocumentIndex(_documentIndex);\n}\n\nauto StickerProvider::documentChanged() const\n-> rpl::producer<not_null<DocumentData*>> {\n\treturn _documentChanged.events();\n}\n\nvoid StickerProvider::processDocumentIndex(int documentIndex) {\n\tif (const auto document = lookupAndRememberSticker(documentIndex)) {\n\t\t_resolvingLifetime.destroy();\n\t\t_owner->customEmojiManager().resolve(\n\t\t\tdocument->id\n\t\t) | rpl::on_next([=](not_null<DocumentData*> d) {\n\t\t\t_resolvingLifetime.destroy();\n\t\t\t_downloadFinishedLifetime.destroy();\n\n\t\t\tconst auto mediaView = d->createMediaView();\n\t\t\t_downloadFinishedLifetime.add([=] {\n\t\t\t\t[[maybe_unused]] const auto copy = mediaView;\n\t\t\t});\n\t\t\tmediaView->checkStickerLarge();\n\t\t\tmediaView->goodThumbnailWanted();\n\t\t\trpl::single(\n\t\t\t\trpl::empty_value()\n\t\t\t) | rpl::then(\n\t\t\t\t_owner->session().downloaderTaskFinished()\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tif (mediaView->loaded()) {\n\t\t\t\t\t_timer.callOnce(kTimeout);\n\t\t\t\t\t_documentChanged.fire_copy(mediaView->owner());\n\t\t\t\t\t_downloadFinishedLifetime.destroy();\n\t\t\t\t}\n\t\t\t}, _downloadFinishedLifetime);\n\t\t}, _resolvingLifetime);\n\t} else if (!_resolvingLifetime) {\n\t\t_timer.callOnce(kTimeout);\n\t}\n}\n\nDocumentData *StickerProvider::lookupAndRememberSticker(int documentIndex) {\n\tconst auto &[document, index] = lookupSticker(documentIndex);\n\tif (document) {\n\t\t_documentIndex = index;\n\t}\n\treturn document;\n}\n\nstd::pair<DocumentData*, int> StickerProvider::lookupSticker(\n\t\tint documentIndex) const {\n\tconst auto size = _shuffledDocuments.size();\n\tfor (auto i = 0; i < size; i++) {\n\t\tconst auto unrestrictedIndex = documentIndex + i;\n\t\tconst auto index = (unrestrictedIndex >= size)\n\t\t\t? (unrestrictedIndex - size)\n\t\t\t: unrestrictedIndex;\n\t\tconst auto id = _shuffledDocuments[index];\n\t\tconst auto document = _owner->document(id);\n\t\tif (document->sticker()) {\n\t\t\treturn { document, index };\n\t\t}\n\t}\n\treturn { nullptr, 0 };\n}\n\n} // namespace\n\nvoid AddEmojiBuilderAction(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\trpl::producer<std::vector<DocumentId>> documents,\n\t\tFn<void(UserpicBuilder::Result)> &&done,\n\t\tbool isForum) {\n\n\tstruct State final {\n\t\tState(not_null<Window::SessionController*> controller)\n\t\t: manager(&controller->session().data())\n\t\t, colorIndex(rpl::single(\n\t\t\trpl::empty_value()\n\t\t) | rpl::then(\n\t\t\tmanager.documentChanged() | rpl::skip(1) | rpl::to_empty\n\t\t) | rpl::map([] {\n\t\t\treturn base::RandomIndex(std::numeric_limits<int>::max());\n\t\t})) {\n\t\t}\n\n\t\tStickerProvider manager;\n\t\trpl::variable<int> colorIndex;\n\t};\n\tconst auto state = menu->lifetime().make_state<State>(controller);\n\tauto item = base::make_unique_q<Ui::Menu::Action>(\n\t\tmenu->menu(),\n\t\tmenu->st().menu,\n\t\tUi::Menu::CreateAction(\n\t\t\tmenu.get(),\n\t\t\ttr::lng_attach_profile_emoji(tr::now),\n\t\t\t[=, done = std::move(done), docs = rpl::duplicate(documents)] {\n\t\t\t\tconst auto id = state->manager.documentId();\n\t\t\t\tUserpicBuilder::ShowLayer(\n\t\t\t\t\tcontroller,\n\t\t\t\t\t{ id, state->colorIndex.current(), docs, {}, isForum },\n\t\t\t\t\tbase::duplicate(done));\n\t\t\t}),\n\t\tnullptr,\n\t\tnullptr);\n\tconst auto icon = UserpicBuilder::CreateEmojiUserpic(\n\t\titem.get(),\n\t\tst::restoreUserpicIcon.size,\n\t\tstate->manager.documentChanged(),\n\t\tstate->colorIndex.value(),\n\t\tisForum);\n\ticon->setAttribute(Qt::WA_TransparentForMouseEvents);\n\ticon->move(menu->st().menu.itemIconPosition\n\t\t+ QPoint(\n\t\t\t(st::menuIconRemove.width() - icon->width()) / 2,\n\t\t\t(st::menuIconRemove.height() - icon->height()) / 2));\n\n\trpl::duplicate(\n\t\tdocuments\n\t) | rpl::on_next([=](std::vector<DocumentId> documents) {\n\t\tstate->manager.setDocuments(std::move(documents));\n\t}, item->lifetime());\n\n\tconst auto action = menu->addAction(std::move(item));\n\taction->setProperty(\n\t\t\"highlight-control-id\",\n\t\tu\"profile-photo/use-emoji\"_q);\n}\n\n} // namespace UserpicBuilder\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_menu_item.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nnamespace UserpicBuilder {\n\nstruct Result;\n\nvoid AddEmojiBuilderAction(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::PopupMenu*> menu,\n\trpl::producer<std::vector<DocumentId>> documents,\n\tFn<void(UserpicBuilder::Result)> &&done,\n\tbool isForum);\n\n} // namespace UserpicBuilder\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/userpic/info_userpic_emoji_builder_preview.h\"\n\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_session.h\"\n#include \"history/view/media/history_view_sticker_player.h\"\n#include \"info/userpic/info_userpic_emoji_builder_common.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n\nnamespace UserpicBuilder {\n\nPreviewPainter::PreviewPainter(int size)\n: _size(size)\n, _emojiSize(base::SafeRound(_size / M_SQRT2))\n, _frameGeometry(Rect(Size(_size)) - Margins((_size - _emojiSize) / 2))\n, _frameRect(Rect(_frameGeometry.size()))\n, _mask(\n\t_frameRect.size() * style::DevicePixelRatio(),\n\tQImage::Format_ARGB32_Premultiplied)\n, _frame(_mask.size(), QImage::Format_ARGB32_Premultiplied) {\n\t_frame.setDevicePixelRatio(style::DevicePixelRatio());\n\t_mask.setDevicePixelRatio(style::DevicePixelRatio());\n\t_mask.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&_mask);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::windowBg);\n\t\tconstexpr auto kFrameRadiusPercent = 25;\n\t\tp.drawRoundedRect(\n\t\t\t_frameRect,\n\t\t\tkFrameRadiusPercent,\n\t\t\tkFrameRadiusPercent,\n\t\t\tQt::RelativeSize);\n\t}\n}\n\nDocumentData *PreviewPainter::document() const {\n\treturn _media ? _media->owner().get() : nullptr;\n}\n\nvoid PreviewPainter::setPlayOnce(bool value) {\n\t_playOnce = value;\n}\n\nvoid PreviewPainter::setDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFn<void()> updateCallback) {\n\tif (_media && (document == _media->owner())) {\n\t\treturn;\n\t}\n\t_lifetime.destroy();\n\n\tconst auto sticker = document->sticker();\n\tAssert(sticker != nullptr);\n\t_media = document->createMediaView();\n\t_media->checkStickerLarge();\n\t_media->goodThumbnailWanted();\n\n\tif (_playOnce) {\n\t\t_firstFrameShown = false;\n\t\t_paused = false;\n\t} else {\n\t\t_paused = true;\n\t}\n\n\trpl::single() | rpl::then(\n\t\tdocument->session().downloaderTaskFinished()\n\t) | rpl::on_next([=] {\n\t\tif (!_media->loaded()) {\n\t\t\treturn;\n\t\t}\n\t\t_lifetime.destroy();\n\t\tconst auto emojiSize = Size(_size * style::DevicePixelRatio());\n\t\tif (sticker->isLottie()) {\n\t\t\t_player = std::make_unique<HistoryView::LottiePlayer>(\n\t\t\t\tChatHelpers::LottiePlayerFromDocument(\n\t\t\t\t\t_media.get(),\n\t\t\t\t\t//\n\t\t\t\t\tChatHelpers::StickerLottieSize::EmojiInteractionReserved7,\n\t\t\t\t\temojiSize,\n\t\t\t\t\tLottie::Quality::High));\n\t\t} else if (sticker->isWebm()) {\n\t\t\t_player = std::make_unique<HistoryView::WebmPlayer>(\n\t\t\t\t_media->owner()->location(),\n\t\t\t\t_media->bytes(),\n\t\t\t\temojiSize);\n\t\t} else if (sticker) {\n\t\t\t_player = std::make_unique<HistoryView::StaticStickerPlayer>(\n\t\t\t\t_media->owner()->location(),\n\t\t\t\t_media->bytes(),\n\t\t\t\temojiSize);\n\t\t}\n\t\tif (_player) {\n\t\t\t_player->setRepaintCallback(updateCallback);\n\t\t} else if (updateCallback) {\n\t\t\tupdateCallback();\n\t\t}\n\t}, _lifetime);\n}\n\nvoid PreviewPainter::paintBackground(QPainter &p, const QImage &image) {\n\tp.drawImage(0, 0, image);\n}\n\nbool PreviewPainter::paintForeground(QPainter &p) {\n\tif (_player && _player->ready()) {\n\t\tconst auto c = _media->owner()->emojiUsesTextColor() ? 255 : 0;\n\t\tauto frame = _player->frame(\n\t\t\tSize(_emojiSize),\n\t\t\tQColor(c, c, c, c),\n\t\t\tfalse,\n\t\t\tcrl::now(),\n\t\t\t_paused);\n\n\t\tif (_playOnce) {\n\t\t\tif (!_firstFrameShown && (frame.index == 1)) {\n\t\t\t\t_firstFrameShown = true;\n\t\t\t} else if (_firstFrameShown && !frame.index) {\n\t\t\t\t_paused = true;\n\t\t\t}\n\t\t}\n\n\t\t_frame.fill(Qt::transparent);\n\t\t{\n\t\t\tQPainter q(&_frame);\n\t\t\tif (frame.image.width() == frame.image.height()) {\n\t\t\t\tq.drawImage(_frameRect, frame.image);\n\t\t\t} else {\n\t\t\t\tauto frameRect = Rect(frame.image.size().scaled(\n\t\t\t\t\t_frameRect.size(),\n\t\t\t\t\tQt::KeepAspectRatio));\n\t\t\t\tframeRect.moveCenter(_frameRect.center());\n\t\t\t\tq.drawImage(frameRect, frame.image);\n\t\t\t}\n\t\t\tq.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\t\tq.drawImage(0, 0, _mask);\n\t\t}\n\n\t\tp.drawImage(_frameGeometry.topLeft(), _frame);\n\t\tif (!_paused) {\n\t\t\t_player->markFrameShown();\n\t\t}\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nEmojiUserpic::EmojiUserpic(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst QSize &size,\n\tbool isForum)\n: Ui::RpWidget(parent)\n, _forum(isForum)\n, _painter(size.width())\n, _duration(st::slideWrapDuration) {\n\tresize(size);\n\tsetNaturalWidth(size.width());\n}\n\nvoid EmojiUserpic::setDocument(not_null<DocumentData*> document) {\n\tif (!_playOnce.has_value()) {\n\t\tconst auto &c = document->session().appConfig();\n\t\t_playOnce = !c.get<bool>(u\"upload_markup_video\"_q, false);\n\t}\n\t_painter.setDocument(document, [=] { update(); });\n\t_painter.setPlayOnce(*_playOnce);\n}\n\nvoid EmojiUserpic::result(int size, Fn<void(UserpicBuilder::Result)> done) {\n\tconst auto painter = lifetime().make_state<PreviewPainter>(size);\n\t// Reset to the first frame.\n\tconst auto document = _painter.document();\n\tconst auto callback = [=] {\n\t\tauto background = GenerateGradient(Size(size), _colors, false);\n\n\t\t{\n\t\t\tconstexpr auto kAttemptsToDrawFirstFrame = 3000;\n\t\t\tauto attempts = 0;\n\t\t\tauto p = QPainter(&background);\n\t\t\twhile (attempts < kAttemptsToDrawFirstFrame) {\n\t\t\t\tif (painter->paintForeground(p)) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tattempts++;\n\t\t\t}\n\t\t}\n\t\tif (*_playOnce && document) {\n\t\t\tdone({ std::move(background), document->id, _colors });\n\t\t} else {\n\t\t\tdone({ std::move(background) });\n\t\t}\n\t};\n\tif (document) {\n\t\tpainter->setDocument(document, callback);\n\t} else {\n\t\tcallback();\n\t}\n}\n\nvoid EmojiUserpic::setGradientColors(std::vector<QColor> colors) {\n\tif (_colors == colors) {\n\t\treturn;\n\t}\n\tif (const auto colors = base::take(_colors); !colors.empty()) {\n\t\t_previousImage = GenerateGradient(size(), colors, !_forum, _forum);\n\t}\n\t_colors = std::move(colors);\n\t{\n\t\t_image = GenerateGradient(size(), _colors, !_forum, _forum);\n\t}\n\tif (_duration) {\n\t\t_animation.stop();\n\t\t_animation.start([=] { update(); }, 0., 1., _duration);\n\t} else {\n\t\tupdate();\n\t}\n}\n\nvoid EmojiUserpic::paintEvent(QPaintEvent *event) {\n\tauto p = QPainter(this);\n\n\tif (_animation.animating() && !_previousImage.isNull()) {\n\t\t_painter.paintBackground(p, _previousImage);\n\n\t\tp.setOpacity(_animation.value(1.));\n\t}\n\n\t_painter.paintBackground(p, _image);\n\n\tp.setOpacity(1.);\n\t_painter.paintForeground(p);\n}\n\nvoid EmojiUserpic::setDuration(crl::time duration) {\n\t_duration = duration;\n}\n\n} // namespace UserpicBuilder\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\n#include \"ui/effects/animations.h\"\n\nnamespace Data {\nclass DocumentMedia;\n} // namespace Data\n\nnamespace HistoryView {\nclass StickerPlayer;\n} // namespace HistoryView\n\nclass DocumentData;\n\nnamespace UserpicBuilder {\n\nstruct Result;\n\nclass PreviewPainter final {\npublic:\n\tPreviewPainter(int size);\n\n\t[[nodiscard]] DocumentData *document() const;\n\n\tvoid setPlayOnce(bool value);\n\tvoid setDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFn<void()> updateCallback);\n\n\tvoid paintBackground(QPainter &p, const QImage &image);\n\tbool paintForeground(QPainter &p);\n\nprivate:\n\tconst int _size;\n\tconst int _emojiSize;\n\tconst QRect _frameGeometry;\n\tconst QRect _frameRect;\n\n\tQImage _mask;\n\tQImage _frame;\n\n\tstd::shared_ptr<Data::DocumentMedia> _media;\n\tstd::unique_ptr<HistoryView::StickerPlayer> _player;\n\tbool _playOnce = false;\n\tbool _paused = false;\n\tbool _firstFrameShown = false;\n\trpl::lifetime _lifetime;\n\n};\n\nclass EmojiUserpic final : public Ui::RpWidget {\npublic:\n\tEmojiUserpic(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst QSize &size,\n\t\tbool isForum);\n\n\tvoid result(int size, Fn<void(UserpicBuilder::Result)> done);\n\tvoid setGradientColors(std::vector<QColor> colors);\n\tvoid setDocument(not_null<DocumentData*> document);\n\tvoid setDuration(crl::time duration);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *event) override;\n\nprivate:\n\tconst bool _forum;\n\tPreviewPainter _painter;\n\n\tstd::optional<bool> _playOnce;\n\n\tQImage _previousImage;\n\tQImage _image;\n\tstd::vector<QColor> _colors;\n\n\tcrl::time _duration;\n\tUi::Animations::Simple _animation;\n\n};\n\n} // namespace UserpicBuilder\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"info/userpic/info_userpic_emoji_builder_widget.h\"\n\n#include \"api/api_peer_photo.h\"\n#include \"apiwrap.h\"\n#include \"chat_helpers/emoji_list_widget.h\"\n#include \"chat_helpers/stickers_list_widget.h\"\n#include \"data/data_document.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"editor/photo_editor_layer_widget.h\" // Editor::kProfilePhotoSize.\n#include \"info/userpic/info_userpic_bubble_wrap.h\"\n#include \"info/userpic/info_userpic_color_circle_button.h\"\n#include \"info/userpic/info_userpic_colors_editor.h\"\n#include \"info/userpic/info_userpic_emoji_builder_common.h\"\n#include \"info/userpic/info_userpic_emoji_builder_preview.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_info_userpic_builder.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace UserpicBuilder {\nnamespace {\n\nvoid AlignChildren(not_null<Ui::RpWidget*> widget, int fullWidth) {\n\tconst auto children = widget->children();\n\tconst auto widgets = ranges::views::all(\n\t\tchildren\n\t) | ranges::views::filter([](not_null<const QObject*> object) {\n\t\treturn object->isWidgetType();\n\t}) | ranges::views::transform([](not_null<QObject*> object) {\n\t\treturn static_cast<QWidget*>(object.get());\n\t}) | ranges::to_vector;\n\n\tconst auto widgetWidth = widgets.front()->width();\n\tconst auto widgetsCount = widgets.size();\n\tconst auto widgetsWidth = widgetWidth * widgetsCount;\n\tconst auto step = (fullWidth - widgetsWidth) / (widgetsCount - 1);\n\tfor (auto i = 0; i < widgetsCount; i++) {\n\t\twidgets[i]->move(i * (widgetWidth + step), widgets[i]->y());\n\t}\n}\n\n[[nodiscard]] QImage GenerateSpecial(\n\t\tint size,\n\t\tconst std::vector<QColor> colors) {\n\tif (colors.empty()) {\n\t\tauto image = QImage(\n\t\t\tSize(size * style::DevicePixelRatio()),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\t\timage.fill(Qt::transparent);\n\t\t{\n\t\t\tauto p = QPainter(&image);\n\t\t\tst::userpicBuilderEmojiColorPlus.icon.paintInCenter(\n\t\t\t\tp,\n\t\t\t\tRect(Size(size)));\n\t\t}\n\t\treturn image;\n\t} else {\n\t\tauto image = GenerateGradient(Size(size), colors);\n\t\t{\n\t\t\tauto p = QPainter(&image);\n\t\t\tconstexpr auto kEllipseSize = 1;\n\t\t\tconst auto center = QPointF(size / 2., size / 2.);\n\t\t\tconst auto shift = QPointF(kEllipseSize * 4, 0);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(st::boxBg);\n\t\t\tp.drawEllipse(center, kEllipseSize, kEllipseSize);\n\t\t\tp.drawEllipse(center + shift, kEllipseSize, kEllipseSize);\n\t\t\tp.drawEllipse(center - shift, kEllipseSize, kEllipseSize);\n\t\t}\n\t\treturn image;\n\t}\n}\n\n[[nodiscard]] std::vector<std::vector<QColor>> PaletteGradients() {\n\tauto v = std::vector<std::vector<QColor>>{\n\t\t{\n\t\t\tQColor(32, 226, 205),\n\t\t\tQColor(14, 225, 241),\n\t\t\tQColor(77, 141, 255),\n\t\t\tQColor(43, 191, 255),\n\t\t},\n\t\t{\n\t\t\tQColor(69, 247, 183),\n\t\t\tQColor(31, 241, 217),\n\t\t\tQColor(94, 182, 251),\n\t\t\tQColor(31, 206, 235),\n\t\t},\n\t\t{\n\t\t\tQColor(193, 229, 38),\n\t\t\tQColor(128, 223, 43),\n\t\t\tQColor(9, 210, 96),\n\t\t\tQColor(94, 220, 64),\n\t\t},\n\t\t{\n\t\t\tQColor(255, 212, 18),\n\t\t\tQColor(255, 167, 67),\n\t\t\tQColor(245, 105, 78),\n\t\t\tQColor(245, 119, 44),\n\t\t},\n\t\t{\n\t\t\tQColor(246, 167, 48),\n\t\t\tQColor(255, 119, 66),\n\t\t\tQColor(246, 72, 132),\n\t\t\tQColor(239, 91, 65),\n\t\t},\n\t\t{\n\t\t\tQColor(255, 178, 58),\n\t\t\tQColor(254, 126, 98),\n\t\t\tQColor(249, 75, 160),\n\t\t\tQColor(251, 92, 128),\n\t\t},\n\t\t{\n\t\t\tQColor(255, 114, 169),\n\t\t\tQColor(226, 105, 255),\n\t\t\tQColor(131, 124, 255),\n\t\t\tQColor(176, 99, 255),\n\t\t},\n\t};\n\tfor (auto &g : v) {\n\t\t// Rotate 180 degrees.\n\t\tstd::swap(g[0], g[2]);\n\t\tstd::swap(g[1], g[3]);\n\t}\n\treturn v;\n}\n\nvoid ShowGradientEditor(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tStartData data,\n\t\tFn<void(std::vector<QColor>)> &&doneCallback) {\n\tcontroller->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tstruct State {\n\t\t\trpl::event_stream<> saveRequests;\n\t\t};\n\t\tconst auto state = box->lifetime().make_state<State>();\n\t\tbox->setTitle(tr::lng_chat_theme_change());\n\t\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\t\tstate->saveRequests.fire({});\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\n\t\tauto content = CreateGradientEditor(\n\t\t\tbox,\n\t\t\t(data.documentId\n\t\t\t\t? controller->session().data().document(\n\t\t\t\t\tdata.documentId).get()\n\t\t\t\t: nullptr),\n\t\t\tdata.gradientEditorColors,\n\t\t\tBothWayCommunication<std::vector<QColor>>{\n\t\t\t\tstate->saveRequests.events(),\n\t\t\t\t[=](std::vector<QColor> colors) {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t\tdoneCallback(std::move(colors));\n\t\t\t\t},\n\t\t\t});\n\t\tbox->setWidth(content->width());\n\t\tbox->addRow(std::move(content), style::margins());\n\t}));\n}\n\nclass EmojiSelector final : public Ui::RpWidget {\npublic:\n\tEmojiSelector(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\trpl::producer<std::vector<DocumentId>> recent);\n\n\t[[nodiscard]] rpl::producer<not_null<DocumentData*>> chosen() const;\n\nprivate:\n\tusing Footer = ChatHelpers::TabbedSelector::InnerFooter;\n\tusing List = ChatHelpers::TabbedSelector::Inner;\n\tusing Type = ChatHelpers::SelectorTab;\n\tvoid createSelector(Type type);\n\n\tstruct Selector {\n\t\tnot_null<List*> list;\n\t\tnot_null<Footer*> footer;\n\t};\n\t[[nodiscard]] Selector createEmojiList(\n\t\tnot_null<Ui::ScrollArea*> scroll);\n\t[[nodiscard]] Selector createStickersList(\n\t\tnot_null<Ui::ScrollArea*> scroll) const;\n\n\tconst not_null<Window::SessionController*> _controller;\n\tbase::unique_qptr<Ui::RpWidget> _container;\n\n\trpl::event_stream<> _recentChanges;\n\tstd::vector<DocumentId> _lastRecent;\n\trpl::event_stream<not_null<DocumentData*>> _chosen;\n\n};\n\nEmojiSelector::EmojiSelector(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Window::SessionController*> controller,\n\trpl::producer<std::vector<DocumentId>> recent)\n: RpWidget(parent)\n, _controller(controller) {\n\tstd::move(\n\t\trecent\n\t) | rpl::on_next([=](std::vector<DocumentId> ids) {\n\t\t_lastRecent = std::move(ids);\n\t\t_recentChanges.fire({});\n\t}, lifetime());\n\tcreateSelector(Type::Emoji);\n}\n\nrpl::producer<not_null<DocumentData*>> EmojiSelector::chosen() const {\n\treturn _chosen.events();\n}\n\nEmojiSelector::Selector EmojiSelector::createEmojiList(\n\t\tnot_null<Ui::ScrollArea*> scroll) {\n\tconst auto session = &_controller->session();\n\tconst auto manager = &session->data().customEmojiManager();\n\tconst auto tag = Data::CustomEmojiManager::SizeTag::Large;\n\tauto args = ChatHelpers::EmojiListDescriptor{\n\t\t.show = _controller->uiShow(),\n\t\t.mode = ChatHelpers::EmojiListMode::UserpicBuilder,\n\t\t.paused = [=] { return true; },\n\t\t.customRecentList = ChatHelpers::DocumentListToRecent(_lastRecent),\n\t\t.customRecentFactory = [=](DocumentId id, Fn<void()> repaint) {\n\t\t\treturn manager->create(id, std::move(repaint), tag);\n\t\t},\n\t\t.st = &st::userpicBuilderEmojiPan,\n\t};\n\tconst auto list = scroll->setOwnedWidget(\n\t\tobject_ptr<ChatHelpers::EmojiListWidget>(scroll, std::move(args)));\n\tconst auto footer = list->createFooter().data();\n\tlist->refreshEmoji();\n\tlist->customChosen(\n\t) | rpl::on_next([=](const ChatHelpers::FileChosen &chosen) {\n\t\t_chosen.fire_copy(chosen.document);\n\t}, list->lifetime());\n\t_recentChanges.events(\n\t) | rpl::on_next([=] {\n\t\tcreateSelector(Type::Emoji);\n\t}, list->lifetime());\n\tlist->setAllowWithoutPremium(true);\n\treturn { list, footer };\n}\n\nEmojiSelector::Selector EmojiSelector::createStickersList(\n\t\tnot_null<Ui::ScrollArea*> scroll) const {\n\tconst auto list = scroll->setOwnedWidget(\n\t\tobject_ptr<ChatHelpers::StickersListWidget>(\n\t\t\tscroll,\n\t\t\t_controller,\n\t\t\tWindow::GifPauseReason::Any,\n\t\t\tChatHelpers::StickersListMode::UserpicBuilder));\n\tconst auto footer = list->createFooter().data();\n\tlist->refreshRecent();\n\tlist->chosen(\n\t) | rpl::on_next([=](const ChatHelpers::FileChosen &chosen) {\n\t\t_chosen.fire_copy(chosen.document);\n\t}, list->lifetime());\n\treturn { list, footer };\n}\n\nvoid EmojiSelector::createSelector(Type type) {\n\tExpects((type == Type::Emoji) || (type == Type::Stickers));\n\n\tconst auto isEmoji = (type == Type::Emoji);\n\tconst auto &stScroll = st::reactPanelScroll;\n\n\t_container = base::make_unique_q<Ui::RpWidget>(this);\n\tconst auto container = _container.get();\n\tcontainer->show();\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tcontainer->setGeometry(Rect(s));\n\t}, container->lifetime());\n\n\tconst auto scroll = Ui::CreateChild<Ui::ScrollArea>(container, stScroll);\n\n\tconst auto selector = isEmoji\n\t\t? createEmojiList(scroll)\n\t\t: createStickersList(scroll);\n\tselector.footer->setParent(container);\n\n\tconst auto toggleButton = Ui::CreateChild<Ui::AbstractButton>(container);\n\tconst auto &togglePos = st::userpicBuilderEmojiSelectorTogglePosition;\n\t{\n\t\tconst auto &pos = togglePos;\n\t\ttoggleButton->resize(st::menuIconStickers.size()\n\t\t\t// Trying to overlap the settings button under.\n\t\t\t+ QSize(pos.x() * 2, pos.y() * 2));\n\t\ttoggleButton->show();\n\t\ttoggleButton->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = QPainter(toggleButton);\n\t\t\tconst auto r = toggleButton->rect()\n\t\t\t\t- QMargins(pos.x(), pos.y(), pos.x(), pos.y());\n\t\t\tp.fillRect(r, st::boxBg);\n\t\t\tif (isEmoji) {\n\t\t\t\tst::userpicBuilderEmojiToggleStickersIcon.paintInCenter(p, r);\n\t\t\t} else {\n\t\t\t\tst::defaultEmojiPan.icons.people.paintInCenter(p, r);\n\t\t\t}\n\t\t}, toggleButton->lifetime());\n\t}\n\ttoggleButton->show();\n\ttoggleButton->setClickedCallback([=] {\n\t\tcreateSelector(isEmoji ? Type::Stickers : Type::Emoji);\n\t});\n\n\trpl::combine(\n\t\tscroll->scrollTopValue(),\n\t\tscroll->heightValue()\n\t) | rpl::on_next([=](int scrollTop, int scrollHeight) {\n\t\tconst auto scrollBottom = scrollTop + scrollHeight;\n\t\tselector.list->setVisibleTopBottom(scrollTop, scrollBottom);\n\t}, selector.list->lifetime());\n\n\tselector.list->scrollToRequests(\n\t) | rpl::on_next([=](int y) {\n\t\tscroll->scrollToY(y);\n\t\t// _shadow->update();\n\t}, selector.list->lifetime());\n\n\tconst auto separator = Ui::CreateChild<Ui::RpWidget>(container);\n\tseparator->paintRequest(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tauto p = QPainter(separator);\n\t\tp.fillRect(r, st::shadowFg);\n\t}, separator->lifetime());\n\n\tselector.footer->show();\n\tseparator->show();\n\tscroll->show();\n\n\tconst auto scrollWidth = stScroll.width;\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tconst auto left = st::userpicBuilderEmojiSelectorLeft;\n\t\tconst auto mostTop = st::userpicBuilderEmojiSelectorLeft;\n\n\t\ttoggleButton->move(QPoint(left, mostTop));\n\n\t\tselector.footer->setGeometry(\n\t\t\t(isEmoji ? (rect::right(toggleButton) - togglePos.x()) : left),\n\t\t\tmostTop,\n\t\t\ts.width() - left,\n\t\t\tselector.footer->height());\n\n\t\tseparator->setGeometry(\n\t\t\t0,\n\t\t\trect::bottom(selector.footer),\n\t\t\ts.width(),\n\t\t\tst::lineWidth);\n\n\t\tconst auto listWidth = s.width() - st::boxRadius * 2;\n\t\tselector.list->resizeToWidth(listWidth);\n\t\tscroll->setGeometry(\n\t\t\tst::boxRadius,\n\t\t\trect::bottom(separator),\n\t\t\tselector.list->width() + scrollWidth,\n\t\t\ts.height() - rect::bottom(separator));\n\t\tselector.list->setMinimalHeight(listWidth, scroll->height());\n\t}, lifetime());\n\n\t// Reset all animations.\n\tselector.list->hideFinished();\n}\n\n} // namespace\n\nnot_null<Ui::VerticalLayout*> CreateUserpicBuilder(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tStartData data,\n\t\tBothWayCommunication<UserpicBuilder::Result> communication) {\n\tconst auto container = Ui::CreateChild<Ui::VerticalLayout>(parent.get());\n\n\tstruct State {\n\t\tstd::vector<not_null<CircleButton*>> circleButtons;\n\t\tUi::Animations::Simple chosenColorAnimation;\n\t\tint colorIndex = -1;\n\n\t\tstd::vector<QColor> editorColors;\n\t\tStartData gradientEditorStartData;\n\t};\n\tconst auto state = container->lifetime().make_state<State>();\n\n\tconst auto preview = container->add(\n\t\tobject_ptr<EmojiUserpic>(\n\t\t\tcontainer,\n\t\t\tSize(st::settingsInfoPhotoSize),\n\t\t\tdata.isForum),\n\t\tst::userpicBuilderEmojiPreviewPadding,\n\t\tstyle::al_top);\n\tif (const auto id = data.documentId) {\n\t\tconst auto document = controller->session().data().document(id);\n\t\tif (document && document->sticker()) {\n\t\t\tpreview->setDocument(document);\n\t\t}\n\t}\n\n\tcontainer->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_userpic_builder_color_subtitle(),\n\t\t\tst::userpicBuilderEmojiSubtitle),\n\t\tst::userpicBuilderEmojiSubtitlePadding,\n\t\tstyle::al_top);\n\n\tconst auto paletteBg = Ui::AddBubbleWrap(\n\t\tcontainer,\n\t\tQSize(\n\t\t\tst::userpicBuilderEmojiBubblePaletteWidth,\n\t\t\tstd::abs(Ui::BubbleWrapInnerRect(QRect(0, 0, 0, 0)).height())\n\t\t\t\t+ st::userpicBuilderEmojiAccentColorSize\n\t\t\t\t+ rect::m::sum::v(\n\t\t\t\t\tst::userpicBuilderEmojiBubblePalettePadding)));\n\tconst auto palette = Ui::CreateChild<Ui::VerticalLayout>(paletteBg.get());\n\t{\n\t\tconstexpr auto kColorsCount = int(7);\n\t\tconst auto checkIsSpecial = [=](int i) {\n\t\t\treturn (i == kColorsCount);\n\t\t};\n\t\tconst auto size = st::userpicBuilderEmojiAccentColorSize;\n\t\tconst auto paletteGradients = PaletteGradients();\n\t\tfor (auto i = 0; i < kColorsCount + 1; i++) {\n\t\t\tconst auto isSpecial = checkIsSpecial(i);\n\t\t\tconst auto colors = paletteGradients[i % kColorsCount];\n\t\t\tconst auto button = Ui::CreateChild<CircleButton>(palette);\n\t\t\tstate->circleButtons.push_back(button);\n\t\t\tbutton->resize(size, size);\n\t\t\tbutton->setBrush(isSpecial\n\t\t\t\t? GenerateSpecial(size, state->editorColors)\n\t\t\t\t: GenerateGradient(Size(size), colors));\n\n\t\t\tconst auto openEditor = isSpecial\n\t\t\t\t? Fn<void()>([=] {\n\t\t\t\t\tif (checkIsSpecial(state->colorIndex)) {\n\t\t\t\t\t\tstate->colorIndex = -1;\n\t\t\t\t\t}\n\t\t\t\t\tShowGradientEditor(\n\t\t\t\t\t\tcontroller,\n\t\t\t\t\t\tstate->gradientEditorStartData,\n\t\t\t\t\t\t[=](std::vector<QColor> colors) {\n\t\t\t\t\t\t\tstate->editorColors = std::move(colors);\n\t\t\t\t\t\t\tbutton->setBrush(\n\t\t\t\t\t\t\t\tGenerateSpecial(size, state->editorColors));\n\t\t\t\t\t\t\tbutton->clicked({}, Qt::LeftButton);\n\t\t\t\t\t\t});\n\t\t\t\t})\n\t\t\t\t: nullptr;\n\n\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\tif (openEditor && state->editorColors.empty()) {\n\t\t\t\t\treturn openEditor();\n\t\t\t\t}\n\t\t\t\tconst auto was = state->colorIndex;\n\t\t\t\tconst auto now = i;\n\t\t\t\tif (was == now) {\n\t\t\t\t\tif (openEditor) {\n\t\t\t\t\t\topenEditor();\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tstate->chosenColorAnimation.stop();\n\t\t\t\tstate->chosenColorAnimation.start([=](float64 progress) {\n\t\t\t\t\tif (was >= 0) {\n\t\t\t\t\t\tstate->circleButtons[was]->setSelectedProgress(\n\t\t\t\t\t\t\t1. - progress);\n\t\t\t\t\t}\n\t\t\t\t\tstate->circleButtons[now]->setSelectedProgress(progress);\n\t\t\t\t}, 0., 1., st::universalDuration);\n\t\t\t\tstate->colorIndex = now;\n\n\t\t\t\tconst auto result = isSpecial\n\t\t\t\t\t? state->editorColors\n\t\t\t\t\t: colors;\n\t\t\t\tstate->gradientEditorStartData.gradientEditorColors = result;\n\t\t\t\tpreview->setGradientColors(result);\n\t\t\t});\n\t\t}\n\t\tconst auto current = data.builderColorIndex % kColorsCount;\n\t\tstate->circleButtons[current]->setSelectedProgress(1.);\n\t\tstate->circleButtons[current]->clicked({}, Qt::LeftButton);\n\t}\n\tpaletteBg->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tpalette->setGeometry(Ui::BubbleWrapInnerRect(Rect(s))\n\t\t\t- st::userpicBuilderEmojiBubblePalettePadding);\n\t\tAlignChildren(palette, palette->width());\n\t}, palette->lifetime());\n\n\tcontainer->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_userpic_builder_emoji_subtitle(),\n\t\t\tst::userpicBuilderEmojiSubtitle),\n\t\tst::userpicBuilderEmojiSubtitlePadding,\n\t\tstyle::al_top);\n\n\tconst auto selectorBg = Ui::AddBubbleWrap(\n\t\tcontainer,\n\t\tQSize(\n\t\t\tst::userpicBuilderEmojiBubblePaletteWidth,\n\t\t\tst::userpicBuilderEmojiSelectorMinHeight));\n\tconst auto selector = Ui::CreateChild<EmojiSelector>(\n\t\tselectorBg.get(),\n\t\tcontroller,\n\t\tbase::take(data.documents));\n\tselector->chosen(\n\t) | rpl::on_next([=](not_null<DocumentData*> document) {\n\t\tstate->gradientEditorStartData.documentId = document->id;\n\t\tpreview->setDocument(document);\n\t}, preview->lifetime());\n\tselectorBg->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tselector->setGeometry(Ui::BubbleWrapInnerRect(Rect(s)));\n\t}, selector->lifetime());\n\n\tbase::take(\n\t\tcommunication.triggers\n\t) | rpl::on_next([=, done = base::take(communication.result)] {\n\t\tpreview->result(Editor::kProfilePhotoSize, [=](Result result) {\n\t\t\tdone(std::move(result));\n\t\t});\n\t}, preview->lifetime());\n\n\treturn container;\n}\n\nnot_null<Ui::RpWidget*> CreateEmojiUserpic(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst QSize &size,\n\t\trpl::producer<not_null<DocumentData*>> document,\n\t\trpl::producer<int> colorIndex,\n\t\tbool isForum) {\n\tconst auto paletteGradients = PaletteGradients();\n\tconst auto widget = Ui::CreateChild<EmojiUserpic>(\n\t\tparent.get(),\n\t\tsize,\n\t\tisForum);\n\tstd::move(\n\t\tdocument\n\t) | rpl::on_next([=](not_null<DocumentData*> d) {\n\t\twidget->setDocument(d);\n\t}, widget->lifetime());\n\tstd::move(\n\t\tcolorIndex\n\t) | rpl::on_next([=](int index) {\n\t\twidget->setGradientColors(\n\t\t\tpaletteGradients[index % paletteGradients.size()]);\n\t}, widget->lifetime());\n\treturn widget;\n}\n\n} // namespace UserpicBuilder\n"
  },
  {
    "path": "Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass RpWidget;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace UserpicBuilder {\n\nstruct StartData;\nstruct Result;\n\ntemplate <typename Result>\nstruct BothWayCommunication;\n\nnot_null<Ui::VerticalLayout*> CreateUserpicBuilder(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Window::SessionController*> controller,\n\tStartData data,\n\tBothWayCommunication<UserpicBuilder::Result> communication);\n\n[[nodiscard]] not_null<Ui::RpWidget*> CreateEmojiUserpic(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst QSize &size,\n\trpl::producer<not_null<DocumentData*>> document,\n\trpl::producer<int> colorIndex,\n\tbool isForum);\n\n} // namespace UserpicBuilder\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"inline_bots/bot_attach_web_view.h\"\n\n#include \"api/api_blocked_peers.h\"\n#include \"api/api_common.h\"\n#include \"api/api_sending.h\"\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"base/base_file_utilities.h\"\n#include \"base/qthelp_url.h\"\n#include \"base/random.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/peers/choose_peer_box.h\"\n#include \"boxes/peers/create_managed_bot_box.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"boxes/share_box.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/local_url_handlers.h\"\n#include \"core/shortcuts.h\"\n#include \"core/ui_integration.h\" // TextContext\n#include \"core/core_settings.h\"\n#include \"data/components/location_pickers.h\"\n#include \"data/data_bot_app.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer_bot_command.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_web_page.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history_item_reply_markup.h\"\n#include \"info/bot/starref/info_bot_starref_common.h\" // MakePeerBubbleButton\n#include \"info/profile/info_profile_values.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"inline_bots/inline_bot_confirm_prepared.h\"\n#include \"inline_bots/inline_bot_downloads.h\"\n#include \"inline_bots/inline_bot_storage.h\"\n#include \"iv/iv_instance.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"payments/payments_checkout_process.h\"\n#include \"payments/payments_non_panel_process.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/storage_domain.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/attach/attach_bot_webview.h\"\n#include \"ui/controls/location_picker.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/widgets/menu/menu_item_base.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"webview/webview_interface.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_channel_earn.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_info.h\" // infoVerifiedStar.\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_window.h\"\n\n#include <QSvgRenderer>\n#include <QtGui/QWindow>\n\nnamespace InlineBots {\nnamespace {\n\nbase::flat_map<PeerId, QString> PlatformForBotData;\n\n[[nodiscard]] QString PlatformForBot(PeerId botId) {\n\tconst auto it = PlatformForBotData.find(botId);\n\tif (it != PlatformForBotData.end()) {\n\t\treturn it->second;\n\t}\n\treturn u\"tdesktop\"_q;\n}\n\nvoid SetPlatformForBot(PeerId botId, QString platform) {\n\tPlatformForBotData[botId] = platform;\n}\n\nQString SerializePlatformForBots() {\n\tauto serializedList = QStringList();\n\tfor (const auto &pair : PlatformForBotData) {\n\t\tserializedList.append(QString::number(pair.first.value)\n\t\t\t+ \":\"\n\t\t\t+ pair.second);\n\t}\n\treturn serializedList.join(\"|\");\n}\n\nvoid DeserializePlatformForBots(const QString &data) {\n\tPlatformForBotData.clear();\n\tauto botEntries = data.split(\"|\");\n\tfor (const auto &entry : botEntries) {\n\t\tconst auto keyValue = entry.split(\":\");\n\t\tif (keyValue.size() == 2) {\n\t\t\tconst auto botId = PeerId(keyValue[0].toULongLong());\n\t\t\tQString platform = keyValue[1];\n\t\t\tSetPlatformForBot(botId, platform);\n\t\t}\n\t}\n}\n\nconstexpr auto kProlongTimeout = 60 * crl::time(1000);\nconstexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000);\nconstexpr auto kPopularAppBotsLimit = 100;\n\n[[nodiscard]] DocumentData *ResolveIcon(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDattachMenuBot &data) {\n\tfor (const auto &icon : data.vicons().v) {\n\t\tconst auto document = icon.match([&](\n\t\t\tconst MTPDattachMenuBotIcon &data\n\t\t) -> DocumentData* {\n\t\t\tif (data.vname().v == \"default_static\") {\n\t\t\t\treturn session->data().processDocument(data.vicon()).get();\n\t\t\t}\n\t\t\treturn nullptr;\n\t\t});\n\t\tif (document) {\n\t\t\treturn document;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\n[[nodiscard]] PeerTypes ResolvePeerTypes(\n\t\tconst QVector<MTPAttachMenuPeerType> &types) {\n\tauto result = PeerTypes();\n\tfor (const auto &type : types) {\n\t\tresult |= type.match([&](const MTPDattachMenuPeerTypeSameBotPM &) {\n\t\t\treturn PeerType::SameBot;\n\t\t}, [&](const MTPDattachMenuPeerTypeBotPM &) {\n\t\t\treturn PeerType::Bot;\n\t\t}, [&](const MTPDattachMenuPeerTypePM &) {\n\t\t\treturn PeerType::User;\n\t\t}, [&](const MTPDattachMenuPeerTypeChat &) {\n\t\t\treturn PeerType::Group;\n\t\t}, [&](const MTPDattachMenuPeerTypeBroadcast &) {\n\t\t\treturn PeerType::Broadcast;\n\t\t});\n\t}\n\treturn result;\n}\n\n[[nodiscard]] std::optional<AttachWebViewBot> ParseAttachBot(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPAttachMenuBot &bot) {\n\tauto result = bot.match([&](const MTPDattachMenuBot &data) {\n\t\tconst auto user = session->data().userLoaded(UserId(data.vbot_id()));\n\t\tconst auto good = user\n\t\t\t&& user->isBot()\n\t\t\t&& user->botInfo->supportsAttachMenu;\n\t\treturn good\n\t\t\t? AttachWebViewBot{\n\t\t\t\t.user = user,\n\t\t\t\t.icon = ResolveIcon(session, data),\n\t\t\t\t.name = qs(data.vshort_name()),\n\t\t\t\t.types = (data.vpeer_types()\n\t\t\t\t\t? ResolvePeerTypes(data.vpeer_types()->v)\n\t\t\t\t\t: PeerTypes()),\n\t\t\t\t.inactive = data.is_inactive(),\n\t\t\t\t.inMainMenu = data.is_show_in_side_menu(),\n\t\t\t\t.inAttachMenu = data.is_show_in_attach_menu(),\n\t\t\t\t.disclaimerRequired = data.is_side_menu_disclaimer_needed(),\n\t\t\t\t.requestWriteAccess = data.is_request_write_access(),\n\t\t\t} : std::optional<AttachWebViewBot>();\n\t});\n\tif (const auto icon = result ? result->icon : nullptr) {\n\t\ticon->forceToCache(true);\n\n\t\tresult->media = icon->createMediaView();\n\t\ticon->save(Data::FileOrigin(), {});\n\t}\n\treturn result;\n}\n\n[[nodiscard]] PeerTypes PeerTypesFromNames(\n\t\tconst std::vector<QString> &names) {\n\tauto result = PeerTypes();\n\tfor (const auto &name : names) {\n\t\t//, bots, groups, channels\n\t\tresult |= (name == u\"users\"_q)\n\t\t\t? PeerType::User\n\t\t\t: name == u\"bots\"_q\n\t\t\t? PeerType::Bot\n\t\t\t: name == u\"groups\"_q\n\t\t\t? PeerType::Group\n\t\t\t: name == u\"channels\"_q\n\t\t\t? PeerType::Broadcast\n\t\t\t: PeerType(0);\n\t}\n\treturn result;\n}\n\n[[nodiscard]] Ui::LocationPickerConfig ResolveMapsConfig(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto &appConfig = session->appConfig();\n\tauto map = appConfig.get<base::flat_map<QString, QString>>(\n\t\tu\"tdesktop_config_map\"_q,\n\t\tbase::flat_map<QString, QString>());\n\treturn {\n\t\t.mapsToken = map[u\"maps\"_q],\n\t\t.geoToken = map[u\"geo\"_q],\n\t};\n}\n\n[[nodiscard]] Window::SessionController *WindowForThread(\n\t\tbase::weak_ptr<Window::SessionController> weak,\n\t\tnot_null<Data::Thread*> thread) {\n\tif (const auto separate = Core::App().separateWindowFor(thread)) {\n\t\treturn separate->sessionController();\n\t}\n\tconst auto strong = weak.get();\n\tif (strong && strong->windowId().hasChatsList()) {\n\t\tstrong->showThread(thread);\n\t\treturn strong;\n\t}\n\tconst auto window = Core::App().ensureSeparateWindowFor(thread);\n\treturn window ? window->sessionController() : nullptr;\n}\n\nvoid ShowChooseBox(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<Main::Session*> session,\n\t\tPeerTypes types,\n\t\tFn<void(not_null<Data::Thread*>)> callback,\n\t\trpl::producer<QString> titleOverride = nullptr) {\n\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tauto done = [=](not_null<Data::Thread*> thread) mutable {\n\t\tif (const auto strong = *weak) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t\tcallback(thread);\n\t};\n\tauto filter = [=](not_null<Data::Thread*> thread) -> bool {\n\t\tconst auto peer = thread->peer();\n\t\tif (!Data::CanSend(thread, ChatRestriction::SendInline, false)) {\n\t\t\treturn false;\n\t\t} else if (const auto user = peer->asUser()) {\n\t\t\tif (user->isBot()) {\n\t\t\t\treturn (types & PeerType::Bot);\n\t\t\t} else {\n\t\t\t\treturn (types & PeerType::User);\n\t\t\t}\n\t\t} else if (peer->isBroadcast()) {\n\t\t\treturn (types & PeerType::Broadcast);\n\t\t} else {\n\t\t\treturn (types & PeerType::Group);\n\t\t}\n\t};\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tif (titleOverride) {\n\t\t\tbox->setTitle(std::move(titleOverride));\n\t\t}\n\t\tbox->addButton(tr::lng_cancel(), [box] {\n\t\t\tbox->closeBox();\n\t\t});\n\t};\n\t*weak = show->show(Box<PeerListBox>(\n\t\tstd::make_unique<ChooseRecipientBoxController>(\n\t\t\tsession,\n\t\t\tstd::move(done),\n\t\t\tstd::move(filter)),\n\t\tstd::move(initBox)));\n}\n\nvoid ShowChooseBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tPeerTypes types,\n\t\tFn<void(not_null<Data::Thread*>)> callback,\n\t\trpl::producer<QString> titleOverride = nullptr) {\n\tShowChooseBox(\n\t\tcontroller->uiShow(),\n\t\t&controller->session(),\n\t\ttypes,\n\t\tstd::move(callback),\n\t\tstd::move(titleOverride));\n}\n\nvoid FillDisclaimerBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tFn<void(bool accepted)> done) {\n\tconst auto updateCheck = std::make_shared<Fn<void()>>();\n\tconst auto validateCheck = std::make_shared<Fn<bool()>>();\n\n\tconst auto callback = [=](Fn<void()> close) {\n\t\tif (validateCheck && (*validateCheck)()) {\n\t\t\tdone(true);\n\t\t\tclose();\n\t\t}\n\t};\n\n\tconst auto padding = st::boxRowPadding;\n\tUi::ConfirmBox(box, {\n\t\t.text = tr::lng_mini_apps_disclaimer_text(\n\t\t\ttr::now,\n\t\t\ttr::rich),\n\t\t.confirmed = callback,\n\t\t.cancelled = [=](Fn<void()> close) { done(false); close(); },\n\t\t.confirmText = tr::lng_box_ok(),\n\t\t.labelPadding = QMargins(padding.left(), 0, padding.right(), 0),\n\t\t.title = tr::lng_mini_apps_disclaimer_title(),\n\t});\n\n\tauto checkView = std::make_unique<Ui::CheckView>(\n\t\tst::defaultCheck,\n\t\tfalse,\n\t\t[=] { if (*updateCheck) { (*updateCheck)(); } });\n\tconst auto check = checkView.get();\n\tconst auto row = box->addRow(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tbox.get(),\n\t\t\ttr::lng_mini_apps_disclaimer_button(\n\t\t\t\tlt_link,\n\t\t\t\trpl::single(tr::link(\n\t\t\t\t\ttr::lng_mini_apps_disclaimer_link(tr::now),\n\t\t\t\t\ttr::lng_mini_apps_tos_url(tr::now))),\n\t\t\t\ttr::marked),\n\t\t\tst::urlAuthCheckbox,\n\t\t\tstd::move(checkView)),\n\t\t{\n\t\t\tst::boxRowPadding.left(),\n\t\t\tst::boxRowPadding.left(),\n\t\t\tst::boxRowPadding.right(),\n\t\t\t0,\n\t\t});\n\trow->setAllowTextLines();\n\trow->setClickHandlerFilter([=](\n\t\t\tconst ClickHandlerPtr &link,\n\t\t\tQt::MouseButton button) {\n\t\tActivateClickHandler(row, link, ClickContext{\n\t\t\t.button = button,\n\t\t\t.other = QVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.show = box->uiShow(),\n\t\t\t})\n\t\t});\n\t\treturn false;\n\t});\n\n\t(*updateCheck) = [=] { row->update(); };\n\n\tconst auto showError = Ui::CheckView::PrepareNonToggledError(\n\t\tcheck,\n\t\tbox->lifetime());\n\n\t(*validateCheck) = [=] {\n\t\tif (check->checked()) {\n\t\t\treturn true;\n\t\t}\n\t\tshowError();\n\t\treturn false;\n\t};\n}\n\nWebViewContext ResolveContext(\n\t\tnot_null<UserData*> bot,\n\t\tWebViewContext context) {\n\tif (!context.dialogsEntryState.key) {\n\t\tif (const auto strong = context.controller.get()) {\n\t\t\tcontext.dialogsEntryState = strong->dialogsEntryStateCurrent();\n\t\t}\n\t}\n\tif (!context.action) {\n\t\tconst auto &state = context.dialogsEntryState;\n\t\tif (const auto thread = state.key.thread()) {\n\t\t\tcontext.action = Api::SendAction(thread);\n\t\t\tcontext.action->replyTo = state.currentReplyTo;\n\t\t\tcontext.action->options.suggest = state.currentSuggest;\n\t\t} else {\n\t\t\tcontext.action = Api::SendAction(bot->owner().history(bot));\n\t\t}\n\t}\n\tif (!context.dialogsEntryState.key) {\n\t\tusing namespace Dialogs;\n\t\tusing Section = EntryState::Section;\n\t\tconst auto history = context.action->history;\n\t\tconst auto topicId = context.action->replyTo.topicRootId;\n\t\tconst auto topic = history->peer->forumTopicFor(topicId);\n\t\tcontext.dialogsEntryState = EntryState{\n\t\t\t.key = (topic ? Key{ topic } : Key{ history }),\n\t\t\t.section = (topic ? Section::Replies : Section::History),\n\t\t\t.currentReplyTo = context.action->replyTo,\n\t\t\t.currentSuggest = context.action->options.suggest,\n\t\t};\n\t}\n\treturn context;\n}\n\nvoid FillBotUsepic(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<PeerData*> bot,\n\t\tbase::weak_ptr<Window::SessionController> weak) {\n\tauto aboutLabel = object_ptr<Ui::FlatLabel>(\n\t\tbox->verticalLayout(),\n\t\ttr::lng_allow_bot_webview_details(\n\t\t\tlt_emoji,\n\t\t\trpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\ttr::rich\n\t\t) | rpl::map([](TextWithEntities text) {\n\t\t\treturn tr::link(std::move(text), u\"internal:\"_q);\n\t\t}),\n\t\tst::defaultFlatLabel);\n\tconst auto userpic = Ui::CreateChild<Ui::UserpicButton>(\n\t\tbox->verticalLayout(),\n\t\tbot,\n\t\tst::infoPersonalChannelUserpic);\n\tUi::AddSkip(box->verticalLayout());\n\taboutLabel->setClickHandlerFilter([=](auto &&...) {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->showPeerHistory(\n\t\t\t\tbot->id,\n\t\t\t\tWindow::SectionShow::Way::Forward);\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t});\n\tconst auto title = Ui::CreateChild<Ui::RpWidget>(box->verticalLayout());\n\tconst auto titleLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\ttitle,\n\t\trpl::single(bot->name()),\n\t\tbox->getDelegate()->style().title);\n\tconst auto icon = bot->isVerified() ? &st::infoVerifiedStar : nullptr;\n\tconst auto iconCheck = icon\n\t\t? &st::infoPeerBadge.verifiedCheck\n\t\t: nullptr;\n\ttitle->resize(\n\t\ttitleLabel->width() + (icon ? icon->width() : 0),\n\t\ttitleLabel->height());\n\ttitle->widthValue(\n\t) | rpl::distinct_until_changed() | rpl::on_next([=](int w) {\n\t\ttitleLabel->resizeToWidth(w\n\t\t\t- (icon ? icon->width() + st::lineWidth : 0));\n\t}, title->lifetime());\n\tif (icon) {\n\t\ttitle->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = Painter(title);\n\t\t\tp.fillRect(title->rect(), Qt::transparent);\n\t\t\tconst auto x = std::min(\n\t\t\t\ttitleLabel->textMaxWidth() + st::lineWidth,\n\t\t\t\ttitle->width() - st::lineWidth - icon->width());\n\t\t\tconst auto y = (title->height() - icon->height()) / 2;\n\t\t\tconst auto w = title->width();\n\t\t\ticon->paint(p, x, y, w);\n\t\t\ticonCheck->paint(p, x, y, w);\n\t\t}, title->lifetime());\n\t}\n\n\tUi::IconWithTitle(box->verticalLayout(), userpic, title, aboutLabel);\n}\n\nstd::unique_ptr<Ui::RpWidget> MakeEmojiSetStatusPreview(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<DocumentData*> document) {\n\tconst auto emoji = Ui::CreateChild<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\tparent,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tparent,\n\t\t\trpl::single(\n\t\t\t\tUi::Text::SingleCustomEmoji(\n\t\t\t\t\tData::SerializeCustomEmojiId(document->id),\n\t\t\t\t\t(document->sticker()\n\t\t\t\t\t\t? document->sticker()->alt\n\t\t\t\t\t\t: QString()))),\n\t\t\tst::botEmojiStatusEmoji,\n\t\t\tst::defaultPopupMenu,\n\t\t\tCore::TextContext({ .session = &peer->session() })),\n\t\tstyle::margins(st::normalFont->spacew, 0, 0, 0));\n\temoji->entity()->resizeToWidth(emoji->entity()->textMaxWidth());\n\n\tauto result = Info::BotStarRef::MakePeerBubbleButton(\n\t\tparent,\n\t\tpeer,\n\t\temoji);\n\tresult->setAttribute(Qt::WA_TransparentForMouseEvents);\n\treturn result;\n}\n\nbool CheckEmojiStatusPremium(not_null<UserData*> bot) {\n\tif (bot->session().premium()) {\n\t\treturn true;\n\t}\n\tconst auto window = ChatHelpers::ResolveWindowDefault()(\n\t\t&bot->session());\n\tif (window) {\n\t\tShowPremiumPreviewBox(window, PremiumFeature::EmojiStatus);\n\t\twindow->window().activate();\n\t}\n\treturn false;\n}\n\nvoid ConfirmEmojiStatusAccessBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<UserData*> bot,\n\t\tFn<void(bool)> done) {\n\tbox->setNoContentMargin(true);\n\n\tconst auto set = box->lifetime().make_state<bool>();\n\n\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\tbox->closeBox();\n\t});\n\n\tAddSkip(box->verticalLayout(), 4 * st::defaultVerticalListSkip);\n\n\tconst auto statusIcon = ChatHelpers::GenerateLocalTgsSticker(\n\t\t&bot->session(),\n\t\tu\"hello_status\"_q,\n\t\ttrue);\n\n\tauto ownedSet = MakeEmojiSetStatusPreview(\n\t\tbox,\n\t\tbot->session().user(),\n\t\tstatusIcon);\n\tbox->addRow(\n\t\tobject_ptr<Ui::RpWidget>::fromRaw(ownedSet.release()));\n\n\tAddSkip(box->verticalLayout(), 2 * st::defaultVerticalListSkip);\n\n\tauto name = tr::bold(bot->name());\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_bot_emoji_status_access_text(\n\t\t\t\tlt_bot,\n\t\t\t\trpl::single(name),\n\t\t\t\tlt_name,\n\t\t\t\trpl::single(name),\n\t\t\t\ttr::rich),\n\t\t\tst::botEmojiStatusText),\n\t\tstyle::al_top);\n\n\tbox->addButton(tr::lng_bot_emoji_status_access_allow(), [=] {\n\t\tif (!CheckEmojiStatusPremium(bot)) {\n\t\t\treturn;\n\t\t}\n\t\t*set = true;\n\t\tbox->closeBox();\n\t\tdone(true);\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tconst auto was = *set;\n\t\tbox->closeBox();\n\t\tif (!was) {\n\t\t\tdone(false);\n\t\t}\n\t});\n}\n\nvoid ConfirmEmojiStatusBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<UserData*> bot,\n\t\tnot_null<DocumentData*> document,\n\t\tTimeId duration,\n\t\tFn<void(bool)> done) {\n\tbox->setNoContentMargin(true);\n\n\tauto owned = Settings::MakeEmojiStatusPreview(box, document);\n\tconst auto preview = box->addRow(\n\t\tobject_ptr<Ui::RpWidget>::fromRaw(owned.release()));\n\tpreview->resize(preview->width(), st::botEmojiStatusPreviewHeight);\n\n\tconst auto set = box->lifetime().make_state<bool>();\n\n\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\tbox->closeBox();\n\t});\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_bot_emoji_status_title(),\n\t\t\tst::botEmojiStatusTitle),\n\t\tstyle::al_top);\n\tAddSkip(box->verticalLayout());\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_bot_emoji_status_text(\n\t\t\t\tlt_bot,\n\t\t\t\trpl::single(tr::bold(bot->name())),\n\t\t\t\ttr::rich),\n\t\t\tst::botEmojiStatusText),\n\t\tstyle::al_top);\n\n\tAddSkip(box->verticalLayout(), 2 * st::defaultVerticalListSkip);\n\n\tauto ownedSet = MakeEmojiSetStatusPreview(\n\t\tbox,\n\t\tdocument->session().user(),\n\t\tdocument);\n\tbox->addRow(\n\t\tobject_ptr<Ui::RpWidget>::fromRaw(ownedSet.release()));\n\n\tbox->addButton(tr::lng_bot_emoji_status_confirm(), [=] {\n\t\tif (!CheckEmojiStatusPremium(bot)) {\n\t\t\treturn;\n\t\t}\n\t\tdocument->owner().emojiStatuses().set(\n\t\t\t{ document->id },\n\t\t\tduration ? (base::unixtime::now() + duration) : 0);\n\t\t*set = true;\n\t\tbox->closeBox();\n\t\tdone(true);\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n\tbox->boxClosing() | rpl::on_next([=] {\n\t\tif (!*set) {\n\t\t\tdone(false);\n\t\t}\n\t}, box->lifetime());\n}\n\nclass BotAction final : public Ui::Menu::ItemBase {\npublic:\n\tBotAction(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tconst style::Menu &st,\n\t\tconst AttachWebViewBot &bot,\n\t\tFn<void()> callback);\n\n\tbool isEnabled() const override;\n\tnot_null<QAction*> action() const override;\n\n\t[[nodiscard]] rpl::producer<bool> forceShown() const;\n\n\tvoid handleKeyPress(not_null<QKeyEvent*> e) override;\n\nprivate:\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\n\tQPoint prepareRippleStartPosition() const override;\n\tQImage prepareRippleMask() const override;\n\n\tint contentHeight() const override;\n\n\tvoid prepare();\n\tvoid paint(Painter &p);\n\n\tconst std::shared_ptr<Ui::Show> _show;\n\tconst not_null<QAction*> _dummyAction;\n\tconst style::Menu &_st;\n\tconst AttachWebViewBot _bot;\n\n\tMenuBotIcon _icon;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\trpl::event_stream<bool> _forceShown;\n\n\tUi::Text::String _text;\n\tint _textWidth = 0;\n\tconst int _height;\n\n};\n\nBotAction::BotAction(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tstd::shared_ptr<Ui::Show> show,\n\tconst style::Menu &st,\n\tconst AttachWebViewBot &bot,\n\tFn<void()> callback)\n: ItemBase(parent, st)\n, _show(std::move(show))\n, _dummyAction(Ui::CreateChild<QAction>(parent))\n, _st(st)\n, _bot(bot)\n, _icon(this, _bot.media)\n, _height(_st.itemPadding.top()\n\t\t+ _st.itemStyle.font->height\n\t\t+ _st.itemPadding.bottom()) {\n\tsetAcceptBoth(false);\n\tfitToMenuWidth();\n\tsetActionTriggered(std::move(callback));\n\n\t_icon.move(_st.itemIconPosition);\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(this);\n\t\tpaint(p);\n\t}, lifetime());\n\n\tenableMouseSelecting();\n\tprepare();\n}\n\nvoid BotAction::paint(Painter &p) {\n\tconst auto selected = isSelected();\n\tif (selected && _st.itemBgOver->c.alpha() < 255) {\n\t\tp.fillRect(0, 0, width(), _height, _st.itemBg);\n\t}\n\tp.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg);\n\tif (isEnabled()) {\n\t\tpaintRipple(p, 0, 0);\n\t}\n\n\tp.setPen(selected ? _st.itemFgOver : _st.itemFg);\n\t_text.drawLeftElided(\n\t\tp,\n\t\t_st.itemPadding.left(),\n\t\t_st.itemPadding.top(),\n\t\t_textWidth,\n\t\twidth());\n}\n\nvoid BotAction::prepare() {\n\t_text.setMarkedText(_st.itemStyle, { _bot.name });\n\tconst auto textWidth = _text.maxWidth();\n\tconst auto &padding = _st.itemPadding;\n\n\tconst auto goodWidth = padding.left()\n\t\t+ textWidth\n\t\t+ padding.right();\n\n\tconst auto w = std::clamp(goodWidth, _st.widthMin, _st.widthMax);\n\t_textWidth = w - (goodWidth - textWidth);\n\tsetMinWidth(w);\n\tupdate();\n}\n\nbool BotAction::isEnabled() const {\n\treturn true;\n}\n\nnot_null<QAction*> BotAction::action() const {\n\treturn _dummyAction;\n}\n\nvoid BotAction::contextMenuEvent(QContextMenuEvent *e) {\n\t_menu = nullptr;\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\tst::popupMenuWithIcons);\n\t_menu->addAction(tr::lng_bot_remove_from_menu(tr::now), [=] {\n\t\tconst auto bot = _bot.user;\n\t\tbot->session().attachWebView().removeFromMenu(_show, bot);\n\t}, &st::menuIconDelete);\n\n\tQObject::connect(_menu, &QObject::destroyed, [=] {\n\t\t_forceShown.fire(false);\n\t});\n\n\t_forceShown.fire(true);\n\t_menu->popup(e->globalPos());\n\te->accept();\n}\n\nQPoint BotAction::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos());\n}\n\nQImage BotAction::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::RectMask(size());\n}\n\nint BotAction::contentHeight() const {\n\treturn _height;\n}\n\nrpl::producer<bool> BotAction::forceShown() const {\n\treturn _forceShown.events();\n}\n\nvoid BotAction::handleKeyPress(not_null<QKeyEvent*> e) {\n\tif (!isSelected()) {\n\t\treturn;\n\t}\n\tconst auto key = e->key();\n\tif (key == Qt::Key_Enter || key == Qt::Key_Return) {\n\t\tsetClicked(Ui::Menu::TriggeredSource::Keyboard);\n\t}\n}\n\n} // namespace\n\nbase::weak_ptr<WebViewInstance> WebViewInstance::PendingActivation;\n\nMenuBotIcon::MenuBotIcon(\n\tQWidget *parent,\n\tstd::shared_ptr<Data::DocumentMedia> media)\n: RpWidget(parent)\n, _media(std::move(media)) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_image = QImage();\n\t\tupdate();\n\t}, lifetime());\n\n\tsetAttribute(Qt::WA_TransparentForMouseEvents);\n\tresize(st::menuIconAdmin.size());\n\tshow();\n}\n\nvoid MenuBotIcon::paintEvent(QPaintEvent *e) {\n\tvalidate();\n\tif (!_image.isNull()) {\n\t\tQPainter(this).drawImage(0, 0, _image);\n\t}\n}\n\nvoid MenuBotIcon::validate() {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto wanted = size() * ratio;\n\tif (_mask.size() != wanted) {\n\t\tif (!_media || !_media->loaded()) {\n\t\t\treturn;\n\t\t}\n\t\tauto icon = QSvgRenderer(_media->bytes());\n\t\t_mask = QImage(wanted, QImage::Format_ARGB32_Premultiplied);\n\t\t_mask.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t_mask.fill(Qt::transparent);\n\t\tif (icon.isValid()) {\n\t\t\tauto p = QPainter(&_mask);\n\t\t\ticon.render(&p, rect());\n\t\t\tp.end();\n\n\t\t\t_mask = Images::Colored(std::move(_mask), Qt::white);\n\t\t}\n\t}\n\tif (_image.isNull()) {\n\t\t_image = style::colorizeImage(_mask, st::menuIconColor);\n\t}\n}\n\nbool PeerMatchesTypes(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> bot,\n\t\tPeerTypes types) {\n\tif (const auto user = peer->asUser()) {\n\t\treturn (user == bot)\n\t\t\t? (types & PeerType::SameBot)\n\t\t\t: user->isBot()\n\t\t\t? (types & PeerType::Bot)\n\t\t\t: (types & PeerType::User);\n\t} else if (peer->isBroadcast()) {\n\t\treturn (types & PeerType::Broadcast);\n\t}\n\treturn (types & PeerType::Group);\n}\n\nPeerTypes ParseChooseTypes(QStringView choose) {\n\tauto result = PeerTypes();\n\tfor (const auto &entry : choose.split(QChar(' '))) {\n\t\tif (entry == u\"users\"_q) {\n\t\t\tresult |= PeerType::User;\n\t\t} else if (entry == u\"bots\"_q) {\n\t\t\tresult |= PeerType::Bot;\n\t\t} else if (entry == u\"groups\"_q) {\n\t\t\tresult |= PeerType::Group;\n\t\t} else if (entry == u\"channels\"_q) {\n\t\t\tresult |= PeerType::Broadcast;\n\t\t}\n\t}\n\treturn result;\n}\n\nWebViewInstance::WebViewInstance(WebViewDescriptor &&descriptor)\n: _parentShow(descriptor.parentShow\n\t? std::move(descriptor.parentShow)\n\t: descriptor.context.controller\n\t? descriptor.context.controller.get()->uiShow()\n\t: nullptr)\n, _session(&descriptor.bot->session())\n, _bot(descriptor.bot)\n, _context(ResolveContext(_bot, std::move(descriptor.context)))\n, _button(std::move(descriptor.button))\n, _source(std::move(descriptor.source)) {\n\tDeserializePlatformForBots(Core::App().settings().fork().botsPlatforms());\n\tresolve();\n}\n\nWebViewInstance::~WebViewInstance() {\n\t_session->api().request(base::take(_requestId)).cancel();\n\t_session->api().request(base::take(_prolongId)).cancel();\n\tbase::take(_panel);\n}\n\nMain::Session &WebViewInstance::session() const {\n\treturn *_session;\n}\n\nnot_null<UserData*> WebViewInstance::bot() const {\n\treturn _bot;\n}\n\nWebViewSource WebViewInstance::source() const {\n\treturn _source;\n}\n\nvoid WebViewInstance::activate() {\n\tif (_panel) {\n\t\t_panel->requestActivate();\n\t} else {\n\t\tPendingActivation = this;\n\t}\n}\n\nvoid WebViewInstance::requestFullBot() {\n\tif (_bot->isFullLoaded()) {\n\t\treturn;\n\t}\n\t_bot->updateFull();\n\t_bot->session().changes().peerUpdates(\n\t\t_bot,\n\t\tData::PeerUpdate::Flag::FullInfo\n\t) | rpl::on_next([=] {\n\t\tif (_botFullWaitingArgs.has_value()) {\n\t\t\tauto args = *base::take(_botFullWaitingArgs);\n\t\t\tif (args.url.isEmpty()) {\n\t\t\t\tshowGame();\n\t\t\t} else {\n\t\t\t\tshow(std::move(args));\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n}\n\nvoid WebViewInstance::resolve() {\n\trequestFullBot();\n\tv::match(_source, [&](WebViewSourceButton data) {\n\t\tconfirmOpen([=] {\n\t\t\tif (data.simple) {\n\t\t\t\trequestSimple();\n\t\t\t} else {\n\t\t\t\trequestButton();\n\t\t\t}\n\t\t});\n\t}, [&](WebViewSourceSwitch) {\n\t\tconfirmOpen([=] {\n\t\t\trequestSimple();\n\t\t});\n\t}, [&](WebViewSourceLinkApp data) {\n\t\tresolveApp(\n\t\t\tdata.appname,\n\t\t\tdata.token,\n\t\t\t(_context.maySkipConfirmation\n\t\t\t\t? ConfirmType::None\n\t\t\t\t: ConfirmType::Always));\n\t}, [&](WebViewSourceLinkBotProfile) {\n\t\tconfirmOpen([=] {\n\t\t\trequestMain();\n\t\t});\n\t}, [&](WebViewSourceLinkAttachMenu data) {\n\t\trequestWithMenuAdd();\n\t}, [&](WebViewSourceMainMenu) {\n\t\trequestWithMainMenuDisclaimer();\n\t}, [&](WebViewSourceAttachMenu) {\n\t\trequestWithMenuAdd();\n\t}, [&](WebViewSourceBotMenu) {\n\t\tif (!openAppFromBotMenuLink()) {\n\t\t\tconfirmOpen([=] {\n\t\t\t\trequestButton();\n\t\t\t});\n\t\t}\n\t}, [&](WebViewSourceGame game) {\n\t\tshowGame();\n\t}, [&](WebViewSourceBotProfile) {\n\t\tif (_context.maySkipConfirmation) {\n\t\t\trequestMain();\n\t\t} else {\n\t\t\tconfirmOpen([=] {\n\t\t\t\trequestMain();\n\t\t\t});\n\t\t}\n\t}, [&](WebViewSourceAgeVerification) {\n\t\trequestMain();\n\t});\n}\n\nbool WebViewInstance::openAppFromBotMenuLink() {\n\tconst auto url = QString::fromUtf8(_button.url);\n\tconst auto local = Core::TryConvertUrlToLocal(url);\n\tconst auto prefix = u\"tg://resolve?\"_q;\n\tif (!local.startsWith(prefix)) {\n\t\treturn false;\n\t}\n\tconst auto params = qthelp::url_parse_params(\n\t\tlocal.mid(prefix.size()),\n\t\tqthelp::UrlParamNameTransform::ToLower);\n\tconst auto domainParam = params.value(u\"domain\"_q);\n\tconst auto appnameParam = params.value(u\"appname\"_q);\n\tconst auto webChannelPreviewLink = (domainParam == u\"s\"_q)\n\t\t&& !appnameParam.isEmpty();\n\tconst auto appname = webChannelPreviewLink ? QString() : appnameParam;\n\tif (appname.isEmpty()) {\n\t\treturn false;\n\t}\n\tresolveApp(appname, params.value(u\"startapp\"_q), ConfirmType::Once);\n\treturn true;\n}\n\nvoid WebViewInstance::resolveApp(\n\t\tconst QString &appname,\n\t\tconst QString &startparam,\n\t\tConfirmType confirmType) {\n\tconst auto already = _session->data().findBotApp(_bot->id, appname);\n\t_requestId = _session->api().request(MTPmessages_GetBotApp(\n\t\tMTP_inputBotAppShortName(\n\t\t\t_bot->inputUser(),\n\t\t\tMTP_string(appname)),\n\t\tMTP_long(already ? already->hash : 0)\n\t)).done([=](const MTPmessages_BotApp &result) {\n\t\t_requestId = 0;\n\t\tconst auto &data = result.data();\n\t\tconst auto received = _session->data().processBotApp(\n\t\t\t_bot->id,\n\t\t\tdata.vapp());\n\t\t_app = received ? received : already;\n\t\t_appStartParam = startparam;\n\t\tif (!_app) {\n\t\t\t_parentShow->showToast(tr::lng_username_app_not_found(tr::now));\n\t\t\tclose();\n\t\t\treturn;\n\t\t}\n\t\tconst auto confirm = data.is_inactive()\n\t\t\t|| (confirmType != ConfirmType::None);\n\t\tconst auto writeAccess = result.data().is_request_write_access();\n\t\tconst auto forceConfirmation = data.is_inactive()\n\t\t\t|| (confirmType == ConfirmType::Always);\n\n\t\t// Check if this app can be added to main menu.\n\t\t// On fail it'll still be opened.\n\t\tusing Result = AttachWebView::AddToMenuResult;\n\t\tconst auto done = crl::guard(this, [=](Result value, auto) {\n\t\t\tif (value == Result::Cancelled) {\n\t\t\t\tclose();\n\t\t\t} else if (value != Result::Unsupported) {\n\t\t\t\trequestApp(true);\n\t\t\t} else if (confirm) {\n\t\t\t\tconfirmAppOpen(writeAccess, [=](bool allowWrite) {\n\t\t\t\t\trequestApp(allowWrite);\n\t\t\t\t}, forceConfirmation);\n\t\t\t} else {\n\t\t\t\trequestApp(false);\n\t\t\t}\n\t\t});\n\t\t_session->attachWebView().requestAddToMenu(_bot, done);\n\t}).fail([=] {\n\t\t_parentShow->showToast(tr::lng_username_app_not_found(tr::now));\n\t\tclose();\n\t}).send();\n}\n\nvoid WebViewInstance::confirmOpen(Fn<void()> done) {\n\tif (_bot->isVerified()\n\t\t|| _session->local().isPeerTrustedOpenWebView(_bot->id)) {\n\t\tdone();\n\t\treturn;\n\t}\n\tconst auto callback = [=](Fn<void()> close) {\n\t\t_session->local().markPeerTrustedOpenWebView(_bot->id);\n\t\tclose();\n\t\tdone();\n\t};\n\tconst auto cancel = [=](Fn<void()> close) {\n\t\tbotClose();\n\t\tclose();\n\t};\n\n\t_parentShow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tFillBotUsepic(box, _bot, _context.controller);\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = tr::lng_profile_open_app_about(\n\t\t\t\ttr::now,\n\t\t\t\tlt_terms,\n\t\t\t\ttr::link(\n\t\t\t\t\ttr::lng_profile_open_app_terms(tr::now),\n\t\t\t\t\ttr::lng_mini_apps_tos_url(tr::now)),\n\t\t\t\ttr::rich),\n\t\t\t.confirmed = crl::guard(this, callback),\n\t\t\t.cancelled = crl::guard(this, cancel),\n\t\t\t.confirmText = tr::lng_view_button_bot_app(),\n\t\t});\n\t}));\n}\n\nvoid WebViewInstance::confirmAppOpen(\n\t\tbool writeAccess,\n\t\tFn<void(bool allowWrite)> done,\n\t\tbool forceConfirmation) {\n\tif (!forceConfirmation\n\t\t&& (_bot->isVerified()\n\t\t\t|| _session->local().isPeerTrustedOpenWebView(_bot->id))) {\n\t\tdone(writeAccess);\n\t\treturn;\n\t}\n\t_parentShow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto allowed = std::make_shared<Ui::Checkbox*>();\n\t\tconst auto callback = [=](Fn<void()> close) {\n\t\t\t_session->local().markPeerTrustedOpenWebView(_bot->id);\n\t\t\tdone((*allowed) && (*allowed)->checked());\n\t\t\tclose();\n\t\t};\n\t\tconst auto cancelled = [=](Fn<void()> close) {\n\t\t\tbotClose();\n\t\t\tclose();\n\t\t};\n\t\tFillBotUsepic(box, _bot, _context.controller);\n\t\tUi::ConfirmBox(box, {\n\t\t\ttr::lng_profile_open_app_about(\n\t\t\t\ttr::now,\n\t\t\t\tlt_terms,\n\t\t\t\ttr::link(\n\t\t\t\t\ttr::lng_profile_open_app_terms(tr::now),\n\t\t\t\t\ttr::lng_mini_apps_tos_url(tr::now)),\n\t\t\t\ttr::rich),\n\t\t\tcrl::guard(this, callback),\n\t\t\tcrl::guard(this, cancelled),\n\t\t\ttr::lng_view_button_bot_app(),\n\t\t});\n\t\tif (writeAccess) {\n\t\t\t(*allowed) = box->addRow(\n\t\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\t\tbox,\n\t\t\t\t\ttr::lng_url_auth_allow_messages(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_bot,\n\t\t\t\t\t\ttr::bold(_bot->name()),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\ttrue,\n\t\t\t\t\tst::urlAuthCheckbox),\n\t\t\t\tstyle::margins(\n\t\t\t\t\tst::boxRowPadding.left(),\n\t\t\t\t\tst::boxPhotoCaptionSkip,\n\t\t\t\t\tst::boxRowPadding.right(),\n\t\t\t\t\tst::boxPhotoCaptionSkip));\n\t\t\t(*allowed)->setAllowTextLines();\n\t\t}\n\t}));\n}\n\nvoid WebViewInstance::requestButton() {\n\tExpects(_context.action.has_value());\n\n\tconst auto &action = *_context.action;\n\tusing Flag = MTPmessages_RequestWebView::Flag;\n\t_requestId = _session->api().request(MTPmessages_RequestWebView(\n\t\tMTP_flags(Flag::f_theme_params\n\t\t\t| (_context.fullscreen ? Flag::f_fullscreen : Flag(0))\n\t\t\t| (_button.url.isEmpty() ? Flag(0) : Flag::f_url)\n\t\t\t| (_button.startCommand.isEmpty()\n\t\t\t\t? Flag(0)\n\t\t\t\t: Flag::f_start_param)\n\t\t\t| (v::is<WebViewSourceBotMenu>(_source)\n\t\t\t\t? Flag::f_from_bot_menu\n\t\t\t\t: Flag(0))\n\t\t\t| (action.replyTo ? Flag::f_reply_to : Flag(0))\n\t\t\t| (action.options.sendAs ? Flag::f_send_as : Flag(0))\n\t\t\t| (action.options.silent ? Flag::f_silent : Flag(0))),\n\t\taction.history->peer->input(),\n\t\t_bot->inputUser(),\n\t\tMTP_bytes(_button.url),\n\t\tMTP_string(_button.startCommand),\n\t\tMTP_dataJSON(MTP_bytes(botThemeParams().json)),\n\t\tMTP_string(PlatformForBot(_bot->id)),\n\t\taction.mtpReplyTo(),\n\t\t(action.options.sendAs\n\t\t\t? action.options.sendAs->input()\n\t\t\t: MTP_inputPeerEmpty())\n\t)).done([=](const MTPWebViewResult &result) {\n\t\tconst auto &data = result.data();\n\t\tshow({\n\t\t\t.url = qs(data.vurl()),\n\t\t\t.queryId = data.vquery_id().value_or_empty(),\n\t\t\t.fullscreen = data.is_fullscreen(),\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\t_parentShow->showToast(error.type());\n\t\tif (error.type() == u\"BOT_INVALID\"_q) {\n\t\t\t_session->attachWebView().requestBots();\n\t\t}\n\t\tclose();\n\t}).send();\n}\n\nvoid WebViewInstance::requestSimple() {\n\tusing Flag = MTPmessages_RequestSimpleWebView::Flag;\n\t_requestId = _session->api().request(MTPmessages_RequestSimpleWebView(\n\t\tMTP_flags(Flag::f_theme_params\n\t\t\t| (_context.fullscreen ? Flag::f_fullscreen : Flag(0))\n\t\t\t| (v::is<WebViewSourceSwitch>(_source)\n\t\t\t\t? (Flag::f_url | Flag::f_from_switch_webview)\n\t\t\t\t: v::is<WebViewSourceMainMenu>(_source)\n\t\t\t\t? (Flag::f_from_side_menu\n\t\t\t\t\t| (_button.startCommand.isEmpty() // from LinkMainMenu\n\t\t\t\t\t\t? Flag()\n\t\t\t\t\t\t: Flag::f_start_param))\n\t\t\t\t: Flag::f_url)),\n\t\t_bot->inputUser(),\n\t\tMTP_bytes(_button.url),\n\t\tMTP_string(_button.startCommand),\n\t\tMTP_dataJSON(MTP_bytes(botThemeParams().json)),\n\t\tMTP_string(PlatformForBot(_bot->id))\n\t)).done([=](const MTPWebViewResult &result) {\n\t\tconst auto &data = result.data();\n\t\tshow({\n\t\t\t.url = qs(data.vurl()),\n\t\t\t.fullscreen = data.is_fullscreen(),\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\t_parentShow->showToast(error.type());\n\t\tclose();\n\t}).send();\n}\n\nvoid WebViewInstance::requestMain() {\n\tusing Flag = MTPmessages_RequestMainWebView::Flag;\n\t_requestId = _session->api().request(MTPmessages_RequestMainWebView(\n\t\tMTP_flags(Flag::f_theme_params\n\t\t\t| (_context.fullscreen ? Flag::f_fullscreen : Flag(0))\n\t\t\t| (_button.startCommand.isEmpty()\n\t\t\t\t\t\t? Flag()\n\t\t\t\t\t\t: Flag::f_start_param)\n\t\t\t| (v::is<WebViewSourceLinkBotProfile>(_source)\n\t\t\t\t? (v::get<WebViewSourceLinkBotProfile>(_source).compact\n\t\t\t\t\t? Flag::f_compact\n\t\t\t\t\t: Flag(0))\n\t\t\t\t: Flag(0))),\n\t\t_context.action->history->peer->input(),\n\t\t_bot->inputUser(),\n\t\tMTP_string(_button.startCommand),\n\t\tMTP_dataJSON(MTP_bytes(botThemeParams().json)),\n\t\tMTP_string(PlatformForBot(_bot->id))\n\t)).done([=](const MTPWebViewResult &result) {\n\t\tconst auto &data = result.data();\n\t\tshow({\n\t\t\t.url = qs(data.vurl()),\n\t\t\t.fullscreen = data.is_fullscreen(),\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\t_parentShow->showToast(error.type());\n\t\tclose();\n\t}).send();\n}\n\nvoid WebViewInstance::requestApp(bool allowWrite) {\n\tExpects(_app != nullptr);\n\tExpects(_context.action.has_value());\n\n\tusing Flag = MTPmessages_RequestAppWebView::Flag;\n\tconst auto app = _app;\n\tconst auto title = app->title;\n\tconst auto flags = Flag::f_theme_params\n\t\t| (_context.fullscreen ? Flag::f_fullscreen : Flag(0))\n\t\t| (_appStartParam.isEmpty() ? Flag(0) : Flag::f_start_param)\n\t\t| (allowWrite ? Flag::f_write_allowed : Flag(0));\n\t_requestId = _session->api().request(MTPmessages_RequestAppWebView(\n\t\tMTP_flags(flags),\n\t\t_context.action->history->peer->input(),\n\t\tMTP_inputBotAppID(MTP_long(app->id), MTP_long(app->accessHash)),\n\t\tMTP_string(_appStartParam),\n\t\tMTP_dataJSON(MTP_bytes(botThemeParams().json)),\n\t\tMTP_string(PlatformForBot(_bot->id))\n\t)).done([=](const MTPWebViewResult &result) {\n\t\t_requestId = 0;\n\t\tconst auto &data = result.data();\n\t\tshow({\n\t\t\t.url = qs(data.vurl()),\n\t\t\t.title = title,\n\t\t\t.fullscreen = data.is_fullscreen(),\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\t_requestId = 0;\n\t\tif (error.type() == u\"BOT_INVALID\"_q) {\n\t\t\t_session->attachWebView().requestBots();\n\t\t}\n\t\tclose();\n\t}).send();\n}\n\nvoid WebViewInstance::requestWithMainMenuDisclaimer() {\n\tusing Result = AttachWebView::AddToMenuResult;\n\tconst auto done = crl::guard(this, [=](Result value, auto) {\n\t\tif (value == Result::Cancelled) {\n\t\t\tclose();\n\t\t} else if (value == Result::Unsupported) {\n\t\t\t_parentShow->showToast(tr::lng_bot_menu_not_supported(tr::now));\n\t\t\tclose();\n\t\t} else {\n\t\t\trequestSimple();\n\t\t}\n\t});\n\t_session->attachWebView().acceptMainMenuDisclaimer(\n\t\t_parentShow,\n\t\t_bot,\n\t\tdone);\n}\n\nvoid WebViewInstance::requestWithMenuAdd() {\n\tusing Result = AttachWebView::AddToMenuResult;\n\tconst auto done = crl::guard(this, [=](Result value, PeerTypes types) {\n\t\tif (value == Result::Cancelled) {\n\t\t\tclose();\n\t\t} else if (value == Result::Unsupported) {\n\t\t\t_parentShow->showToast(tr::lng_bot_menu_not_supported(tr::now));\n\t\t\tclose();\n\t\t} else if (v::is<WebViewSourceLinkAttachMenu>(_source)) {\n\t\t\tmaybeChooseAndRequestButton(types);\n\t\t} else if (v::is<WebViewSourceAttachMenu>(_source)) {\n\t\t\trequestButton();\n\t\t} else {\n\t\t\trequestSimple();\n\t\t}\n\t});\n\t_session->attachWebView().requestAddToMenu(_bot, done);\n}\n\nvoid WebViewInstance::maybeChooseAndRequestButton(PeerTypes supported) {\n\tExpects(v::is<WebViewSourceLinkAttachMenu>(_source));\n\n\tconst auto link = v::get<WebViewSourceLinkAttachMenu>(_source);\n\tconst auto chooseFrom = (link.choose & supported);\n\tif (!chooseFrom) {\n\t\trequestButton();\n\t\treturn;\n\t}\n\tconst auto bot = _bot;\n\tconst auto button = _button;\n\tconst auto weak = _context.controller;\n\tconst auto done = [=](not_null<Data::Thread*> thread) {\n\t\tif (const auto controller = WindowForThread(weak, thread)) {\n\t\t\tthread->session().attachWebView().open({\n\t\t\t\t.bot = bot,\n\t\t\t\t.context = {\n\t\t\t\t\t.controller = controller,\n\t\t\t\t\t.action = Api::SendAction(thread),\n\t\t\t\t},\n\t\t\t\t.button = button,\n\t\t\t\t.source = InlineBots::WebViewSourceLinkAttachMenu{\n\t\t\t\t\t.thread = thread,\n\t\t\t\t\t.token = button.startCommand,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t};\n\tShowChooseBox(_parentShow, _session, chooseFrom, done);\n\tclose();\n}\n\nvoid WebViewInstance::show(ShowArgs &&args) {\n\tif (!_bot->isFullLoaded()) {\n\t\t_botFullWaitingArgs.emplace(std::move(args));\n\t\treturn;\n\t}\n\tauto title = args.title.isEmpty()\n\t\t? Info::Profile::NameValue(_bot)\n\t\t: rpl::single(args.title);\n\tauto titleBadge = _bot->isVerified()\n\t\t? object_ptr<Ui::RpWidget>(_parentShow->toastParent())\n\t\t: nullptr;\n\tif (titleBadge) {\n\t\tconst auto raw = titleBadge.data();\n\t\traw->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = Painter(raw);\n\t\t\tconst auto w = raw->width();\n\t\t\tst::infoVerifiedStar.paint(p, st::lineWidth, 0, w);\n\t\t\tst::infoPeerBadge.verifiedCheck.paint(p, st::lineWidth, 0, w);\n\t\t}, raw->lifetime());\n\t\traw->resize(st::infoVerifiedStar.size() + QSize(0, st::lineWidth));\n\t}\n\n\tconst auto &bots = _session->attachWebView().attachBots();\n\n\tusing Button = Ui::BotWebView::MenuButton;\n\tconst auto attached = ranges::find(\n\t\tbots,\n\t\tnot_null{ _bot },\n\t\t&AttachWebViewBot::user);\n\tconst auto hasOpenBot = v::is<WebViewSourceMainMenu>(_source)\n\t\t|| (_context.action->history->peer != _bot);\n\tconst auto hasRemoveFromMenu = (attached != end(bots))\n\t\t&& (!attached->inactive || attached->inMainMenu)\n\t\t&& (v::is<WebViewSourceMainMenu>(_source)\n\t\t\t|| v::is<WebViewSourceAttachMenu>(_source)\n\t\t\t|| v::is<WebViewSourceLinkAttachMenu>(_source));\n\tconst auto buttons = (hasOpenBot ? Button::OpenBot : Button::None)\n\t\t| (!hasRemoveFromMenu\n\t\t\t? Button::None\n\t\t\t: attached->inMainMenu\n\t\t\t? Button::RemoveFromMainMenu\n\t\t\t: Button::RemoveFromMenu);\n\tconst auto allowClipboardRead = v::is<WebViewSourceMainMenu>(_source)\n\t\t|| v::is<WebViewSourceAttachMenu>(_source)\n\t\t|| (attached != end(bots)\n\t\t\t&& (attached->inAttachMenu || attached->inMainMenu));\n\tconst auto downloads = &_session->attachWebView().downloads();\n\t_panelUrl = args.url;\n\t_panel = Ui::BotWebView::Show({\n\t\t.url = args.url,\n\t\t.storageId = _session->local().resolveStorageIdBots(),\n\t\t.title = std::move(title),\n\t\t.titleBadge = std::move(titleBadge),\n\t\t.bottom = rpl::single('@' + _bot->username()),\n\t\t.delegate = static_cast<Ui::BotWebView::Delegate*>(this),\n\t\t.menuButtons = buttons,\n\t\t.fullscreen = args.fullscreen,\n\t\t.allowClipboardRead = allowClipboardRead,\n\t\t.forkAdditionalButtons = Core::App().settings().fork().additionalButtonsWebBot(),\n\t\t.downloadsProgress = downloads->progress(_bot),\n\t});\n\tif (Core::App().settings().fork().additionalButtonsWebBot()) {\n\t\tDeserializePlatformForBots(Core::App().settings().fork().botsPlatforms());\n\t\tconst auto platforms = std::vector<QString>{\n\t\t\tu\"tdesktop\"_q,\n\t\t\tu\"android\"_q,\n\t\t\tu\"ios\"_q,\n\t\t\tu\"macos\"_q,\n\t\t};\n\t\tconst auto platformButton = Ui::CreateChild<Ui::LinkButton>(\n\t\t\t_panel->toastParent(),\n\t\t\tu\"\"_q);\n\t\tconst auto index = platformButton->lifetime().make_state<int>(0);\n\t\tconst auto platformUpdate = [=] {\n\t\t\tconst auto p = platforms[*index];\n\t\t\tSetPlatformForBot(_bot->id, p);\n\t\t\tplatformButton->setText(p);\n\t\t\tCore::App().settings().fork().setBotsPlatforms(\n\t\t\t\tSerializePlatformForBots());\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t};\n\t\tplatformButton->setText(PlatformForBot(_bot->id));\n\t\tplatformButton->setClickedCallback([=] {\n\t\t\t(*index)++;\n\t\t\tif (*index >= platforms.size()) {\n\t\t\t\t*index = 0;\n\t\t\t}\n\t\t\tplatformUpdate();\n\t\t});\n\n\t\tconst auto copyBotUrls = Ui::CreateChild<Ui::LinkButton>(\n\t\t\t_panel->toastParent(),\n\t\t\tu\"\"_q);\n\t\tconst auto copy = platformButton->lifetime().make_state<bool>(false);\n\t\tconst auto copyBotUrlsUpdate = [=] {\n\t\t\t_copyBotUrls = *copy;\n\t\t\tcopyBotUrls->setText(_copyBotUrls ? u\"Copy url\"_q : u\"Open url\"_q);\n\t\t};\n\t\tcopyBotUrlsUpdate();\n\t\tcopyBotUrls->setClickedCallback([=] {\n\t\t\t*copy = !(*copy);\n\t\t\tcopyBotUrlsUpdate();\n\t\t});\n\n\t\tconst auto p = dynamic_cast<Ui::RpWidget*>(\n\t\t\t_panel->toastParent().get());\n\t\tp->geometryValue() | rpl::on_next([=](QRect r) {\n\t\t\tplatformButton->moveToLeft(15, r.height() - 40);\n\t\t\tplatformButton->show();\n\t\t\tplatformButton->raise();\n\n\t\t\tcopyBotUrls->moveToLeft(\n\t\t\t\tplatformButton->x() + platformButton->width() + 10,\n\t\t\t\tr.height() - 40);\n\t\t\tcopyBotUrls->show();\n\t\t\tcopyBotUrls->raise();\n\t\t}, platformButton->lifetime());\n\t}\n\tstarted(args.queryId);\n\t_panel->toastParent()->windowHandle()->setTitle(u\"Bot_%1_%2_%3\"_q\n\t\t.arg(_bot->id.value)\n\t\t.arg(_bot->session().user()->id.value)\n\t\t.arg(args.title));\n\n\tif (const auto strong = PendingActivation.get()) {\n\t\tif (strong == this) {\n\t\t\tPendingActivation = nullptr;\n\t\t\t_panel->requestActivate();\n\t\t}\n\t}\n}\n\nvoid WebViewInstance::showGame() {\n\tExpects(v::is<WebViewSourceGame>(_source));\n\n\tif (!_bot->isFullLoaded()) {\n\t\t_botFullWaitingArgs = ShowArgs{};\n\t\treturn;\n\t}\n\tconst auto game = v::get<WebViewSourceGame>(_source);\n\t_panelUrl = QString::fromUtf8(_button.url);\n\t_panel = Ui::BotWebView::Show({\n\t\t.url = _panelUrl,\n\t\t.storageId = _session->local().resolveStorageIdBots(),\n\t\t.title = rpl::single(game.title),\n\t\t.bottom = rpl::single('@' + _bot->username()),\n\t\t.delegate = static_cast<Ui::BotWebView::Delegate*>(this),\n\t\t.menuButtons = Ui::BotWebView::MenuButton::ShareGame,\n\t});\n}\n\nvoid WebViewInstance::close() {\n\t_session->attachWebView().close(this);\n}\n\nvoid WebViewInstance::started(uint64 queryId) {\n\tExpects(_context.action.has_value());\n\n\tif (!queryId) {\n\t\treturn;\n\t}\n\n\t_session->data().webViewResultSent(\n\t) | rpl::filter([=](const Data::Session::WebViewResultSent &sent) {\n\t\treturn (sent.queryId == queryId);\n\t}) | rpl::on_next([=] {\n\t\tclose();\n\t}, _panel->lifetime());\n\n\tconst auto action = *_context.action;\n\tbase::timer_each(\n\t\tkProlongTimeout\n\t) | rpl::on_next([=] {\n\t\tusing Flag = MTPmessages_ProlongWebView::Flag;\n\t\t_session->api().request(base::take(_prolongId)).cancel();\n\t\t_prolongId = _session->api().request(MTPmessages_ProlongWebView(\n\t\t\tMTP_flags(Flag(0)\n\t\t\t\t| (action.replyTo ? Flag::f_reply_to : Flag(0))\n\t\t\t\t| (action.options.sendAs ? Flag::f_send_as : Flag(0))\n\t\t\t\t| (action.options.silent ? Flag::f_silent : Flag(0))),\n\t\t\taction.history->peer->input(),\n\t\t\t_bot->inputUser(),\n\t\t\tMTP_long(queryId),\n\t\t\taction.mtpReplyTo(),\n\t\t\t(action.options.sendAs\n\t\t\t\t? action.options.sendAs->input()\n\t\t\t\t: MTP_inputPeerEmpty())\n\t\t)).done([=] {\n\t\t\t_prolongId = 0;\n\t\t}).send();\n\t}, _panel->lifetime());\n}\n\nWebview::ThemeParams WebViewInstance::botThemeParams() {\n\tauto result = Window::Theme::WebViewParams();\n\tif (const auto info = _bot->botInfo.get()) {\n\t\tconst auto night = Window::Theme::IsNightMode();\n\t\tconst auto &title = night\n\t\t\t? info->botAppColorTitleNight\n\t\t\t: info->botAppColorTitleDay;\n\t\tconst auto &body = night\n\t\t\t? info->botAppColorBodyNight\n\t\t\t: info->botAppColorBodyDay;\n\t\tif (title.alpha() == 255) {\n\t\t\tresult.titleBg = title;\n\t\t}\n\t\tif (body.alpha() == 255) {\n\t\t\tresult.bodyBg = body;\n\t\t}\n\t}\n\treturn result;\n}\n\nUi::Text::MarkedContext WebViewInstance::botTextContext() {\n\treturn Core::TextContext({ .session = _session });\n}\n\nauto WebViewInstance::botDownloads(bool forceCheck)\n-> const std::vector<Ui::BotWebView::DownloadsEntry> & {\n\treturn _session->attachWebView().downloads().list(_bot, forceCheck);\n}\n\nvoid WebViewInstance::botDownloadsAction(\n\t\tuint32 id,\n\t\tUi::BotWebView::DownloadsAction type) {\n\t_session->attachWebView().downloads().action(_bot, id, type);\n}\n\nbool WebViewInstance::botHandleLocalUri(QString uri, bool keepOpen) {\n\tconst auto local = Core::TryConvertUrlToLocal(uri);\n\tif (Core::InternalPassportOrOAuthLink(local)) {\n\t\treturn true;\n\t} else if (!local.startsWith(u\"tg://\"_q, Qt::CaseInsensitive)\n\t\t&& !local.startsWith(u\"tonsite://\"_q, Qt::CaseInsensitive)\n\t\t&& !local.startsWith(u\"ton://\"_q, Qt::CaseInsensitive)) {\n\t\treturn false;\n\t}\n\tif (_copyBotUrls) {\n\t\tTextUtilities::SetClipboardText({ local });\n\t\t_panel->showToast({\n\t\t\ttr::lng_username_copied(tr::now)\n\t\t\t\t+ u\"\\n(\"_q + local + u\")\"_q\n\t\t});\n\t\treturn true;\n\t}\n\tconst auto bot = _bot;\n\tconst auto context = std::make_shared<WebViewContext>(_context);\n\tif (!keepOpen) {\n\t\tbotClose();\n\t}\n\tcrl::on_main([=] {\n\t\tif (bot->session().windows().empty()) {\n\t\t\tCore::App().domain().activate(&bot->session().account());\n\t\t}\n\t\tconst auto window = !bot->session().windows().empty()\n\t\t\t? bot->session().windows().front().get()\n\t\t\t: nullptr;\n\t\tcontext->controller = window;\n\t\tconst auto variant = QVariant::fromValue(ClickHandlerContext{\n\t\t\t.sessionWindow = window,\n\t\t\t.botWebviewContext = context,\n\t\t});\n\t\tUrlClickHandler::Open(local, variant);\n\t});\n\treturn true;\n}\n\nvoid WebViewInstance::botHandleInvoice(QString slug) {\n\tExpects(_panel != nullptr);\n\n\tusing Result = Payments::CheckoutResult;\n\tconst auto weak = base::make_weak(_panel.get());\n\tconst auto reactivate = [=](Result result) {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->invoiceClosed(slug, [&] {\n\t\t\t\tswitch (result) {\n\t\t\t\tcase Result::Paid: return \"paid\";\n\t\t\t\tcase Result::Failed: return \"failed\";\n\t\t\t\tcase Result::Pending: return \"pending\";\n\t\t\t\tcase Result::Cancelled: return \"cancelled\";\n\t\t\t\t}\n\t\t\t\tUnexpected(\"Payments::CheckoutResult value.\");\n\t\t\t}());\n\t\t}\n\t};\n\tPayments::CheckoutProcess::Start(\n\t\t_session,\n\t\tslug,\n\t\treactivate,\n\t\tnonPanelPaymentFormFactory(reactivate));\n}\n\nauto WebViewInstance::nonPanelPaymentFormFactory(\n\tFn<void(Payments::CheckoutResult)> reactivate)\n-> Fn<void(Payments::NonPanelPaymentForm)> {\n\tusing namespace Payments;\n\tconst auto panel = base::make_weak(_panel.get());\n\tconst auto weak = _context.controller;\n\treturn [=](Payments::NonPanelPaymentForm form) {\n\t\tusing CreditsFormDataPtr = std::shared_ptr<CreditsFormData>;\n\t\tusing CreditsReceiptPtr = std::shared_ptr<CreditsReceiptData>;\n\t\tv::match(form, [&](const CreditsFormDataPtr &form) {\n\t\t\tif (const auto strong = panel.get()) {\n\t\t\t\tProcessCreditsPayment(\n\t\t\t\t\tuiShow(),\n\t\t\t\t\tstrong->toastParent().get(),\n\t\t\t\t\tform,\n\t\t\t\t\treactivate);\n\t\t\t}\n\t\t}, [&](const CreditsReceiptPtr &receipt) {\n\t\t\tif (const auto controller = weak.get()) {\n\t\t\t\tProcessCreditsReceipt(controller, receipt, reactivate);\n\t\t\t}\n\t\t}, [&](RealFormPresentedNotification) {\n\t\t\t_panel->hideForPayment();\n\t\t});\n\t};\n}\n\nvoid WebViewInstance::botHandleMenuButton(\n\t\tUi::BotWebView::MenuButton button) {\n\tExpects(_panel != nullptr);\n\n\tusing Button = Ui::BotWebView::MenuButton;\n\tconst auto bot = _bot;\n\tswitch (button) {\n\tcase Button::OpenBot:\n\t\tbotClose();\n\t\tif (bot->session().windows().empty()) {\n\t\t\tCore::App().domain().activate(&bot->session().account());\n\t\t}\n\t\tif (!bot->session().windows().empty()) {\n\t\t\tconst auto window = bot->session().windows().front();\n\t\t\twindow->showPeerHistory(bot);\n\t\t\twindow->window().activate();\n\t\t}\n\t\tbreak;\n\tcase Button::RemoveFromMenu:\n\tcase Button::RemoveFromMainMenu: {\n\t\tconst auto &bots = _session->attachWebView().attachBots();\n\t\tconst auto attached = ranges::find(\n\t\t\tbots,\n\t\t\t_bot,\n\t\t\t&AttachWebViewBot::user);\n\t\tconst auto name = (attached != end(bots))\n\t\t\t? attached->name\n\t\t\t: _bot->name();\n\t\tconst auto done = crl::guard(this, [=] {\n\t\t\tconst auto session = _session;\n\t\t\tconst auto was = _parentShow;\n\t\t\tbotClose();\n\n\t\t\tconst auto active = Core::App().activeWindow();\n\t\t\tconst auto show = active ? active->uiShow() : was;\n\t\t\tsession->attachWebView().removeFromMenu(show, bot);\n\t\t\tif (active) {\n\t\t\t\tactive->activate();\n\t\t\t}\n\t\t});\n\t\tconst auto main = (button == Button::RemoveFromMainMenu);\n\t\t_panel->showBox(Ui::MakeConfirmBox({\n\t\t\t(main\n\t\t\t\t? tr::lng_bot_remove_from_side_menu_sure\n\t\t\t\t: tr::lng_bot_remove_from_menu_sure)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_bot,\n\t\t\t\t\ttr::bold(name),\n\t\t\t\t\ttr::marked),\n\t\t\tdone,\n\t\t}));\n\t} break;\n\tcase Button::ShareGame: {\n\t\tconst auto itemId = v::is<WebViewSourceGame>(_source)\n\t\t\t? v::get<WebViewSourceGame>(_source).messageId\n\t\t\t: FullMsgId();\n\t\tif (!_panel || !itemId) {\n\t\t\treturn;\n\t\t} else if (const auto item = _session->data().message(itemId)) {\n\t\t\tFastShareMessage(uiShow(), item);\n\t\t} else {\n\t\t\t_panel->showToast({ tr::lng_message_not_found(tr::now) });\n\t\t}\n\t} break;\n\t}\n}\n\nbool WebViewInstance::botValidateExternalLink(QString uri) {\n\tconst auto lower = uri.toLower();\n\tif (_copyBotUrls) {\n\t\tTextUtilities::SetClipboardText({ lower });\n\t\t_panel->showToast({\n\t\t\ttr::lng_username_copied(tr::now)\n\t\t\t\t+ u\"\\n(\"_q + lower + u\")\"_q\n\t\t});\n\t\treturn false;\n\t}\n\tconst auto allowed = _session->appConfig().get<std::vector<QString>>(\n\t\t\"web_app_allowed_protocols\",\n\t\tstd::vector{ u\"http\"_q, u\"https\"_q });\n\tfor (const auto &protocol : allowed) {\n\t\tif (lower.startsWith(protocol + u\"://\"_q)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid WebViewInstance::botOpenIvLink(QString uri) {\n\tconst auto window = _context.controller.get();\n\tif (window) {\n\t\tCore::App().iv().openWithIvPreferred(window, uri);\n\t} else {\n\t\tCore::App().iv().openWithIvPreferred(_session, uri);\n\t}\n}\n\nvoid WebViewInstance::botSendData(QByteArray data) {\n\tExpects(_context.action.has_value());\n\n\tconst auto button = std::get_if<WebViewSourceButton>(&_source);\n\tif (!button\n\t\t|| !button->simple\n\t\t|| _context.action->history->peer != _bot\n\t\t|| _dataSent) {\n\t\treturn;\n\t}\n\t_dataSent = true;\n\t_session->api().request(MTPmessages_SendWebViewData(\n\t\t_bot->inputUser(),\n\t\tMTP_long(base::RandomValue<uint64>()),\n\t\tMTP_string(_button.text),\n\t\tMTP_bytes(data)\n\t)).done([session = _session](const MTPUpdates &result) {\n\t\tsession->api().applyUpdates(result);\n\t}).send();\n\tbotClose();\n}\n\nvoid WebViewInstance::botSwitchInlineQuery(\n\t\tstd::vector<QString> chatTypes,\n\t\tQString query) {\n\tconst auto controller = _context.controller.get();\n\tconst auto types = PeerTypesFromNames(chatTypes);\n\tif (!_bot\n\t\t|| !_bot->isBot()\n\t\t|| _bot->botInfo->inlinePlaceholder.isEmpty()\n\t\t|| !controller) {\n\t\treturn;\n\t} else if (!types) {\n\t\tif (_context.dialogsEntryState.key.owningHistory()) {\n\t\t\tcontroller->switchInlineQuery(\n\t\t\t\t_context.dialogsEntryState,\n\t\t\t\t_bot,\n\t\t\t\tquery);\n\t\t}\n\t} else {\n\t\tconst auto bot = _bot;\n\t\tconst auto done = [=](not_null<Data::Thread*> thread) {\n\t\t\tcontroller->switchInlineQuery(thread, bot, query);\n\t\t};\n\t\tShowChooseBox(\n\t\t\tcontroller,\n\t\t\ttypes,\n\t\t\tdone,\n\t\t\ttr::lng_inline_switch_choose());\n\t}\n\tbotClose();\n}\n\nvoid WebViewInstance::botCheckWriteAccess(Fn<void(bool allowed)> callback) {\n\t_session->api().request(MTPbots_CanSendMessage(\n\t\t_bot->inputUser()\n\t)).done([=](const MTPBool &result) {\n\t\tcallback(mtpIsTrue(result));\n\t}).fail([=] {\n\t\tcallback(false);\n\t}).send();\n}\n\nvoid WebViewInstance::botAllowWriteAccess(Fn<void(bool allowed)> callback) {\n\t_session->api().request(MTPbots_AllowSendMessage(\n\t\t_bot->inputUser()\n\t)).done([session = _session, callback](const MTPUpdates &result) {\n\t\tsession->api().applyUpdates(result);\n\t\tcallback(true);\n\t}).fail([=] {\n\t\tcallback(false);\n\t}).send();\n}\n\nbool WebViewInstance::botStorageWrite(\n\t\tQString key,\n\t\tstd::optional<QString> value) {\n\treturn _session->attachWebView().storage().write(_bot->id, key, value);\n}\n\nstd::optional<QString> WebViewInstance::botStorageRead(QString key) {\n\treturn _session->attachWebView().storage().read(_bot->id, key);\n}\n\nvoid WebViewInstance::botStorageClear() {\n\t_session->attachWebView().storage().clear(_bot->id);\n}\n\nvoid WebViewInstance::botRequestEmojiStatusAccess(\n\t\tFn<void(bool allowed)> callback) {\n\tif (_bot->botInfo->canManageEmojiStatus) {\n\t\tcallback(true);\n\t} else if (const auto panel = _panel.get()) {\n\t\tconst auto bot = _bot;\n\t\tpanel->showBox(Box(ConfirmEmojiStatusAccessBox, bot, [=](bool ok) {\n\t\t\tif (!ok) {\n\t\t\t\tcallback(false);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto session = &bot->session();\n\t\t\tbot->botInfo->canManageEmojiStatus = true;\n\t\t\tsession->api().request(MTPbots_ToggleUserEmojiStatusPermission(\n\t\t\t\tbot->inputUser(),\n\t\t\t\tMTP_bool(true)\n\t\t\t)).done([=] {\n\t\t\t\tcallback(true);\n\t\t\t}).fail([=] {\n\t\t\t\tcallback(false);\n\t\t\t}).send();\n\t\t}));\n\t} else {\n\t\tcallback(false);\n\t}\n}\n\nvoid WebViewInstance::botSharePhone(Fn<void(bool shared)> callback) {\n\tconst auto history = _bot->owner().history(_bot);\n\tif (_bot->isBlocked()) {\n\t\tconst auto done = crl::guard(this, [=](bool success) {\n\t\t\tif (success) {\n\t\t\t\tbotSharePhone(callback);\n\t\t\t} else {\n\t\t\t\tcallback(false);\n\t\t\t}\n\t\t});\n\t\t_session->api().blockedPeers().unblock(_bot, done);\n\t\treturn;\n\t}\n\tauto action = Api::SendAction(history);\n\taction.clearDraft = false;\n\t_session->api().shareContact(\n\t\t_session->user(),\n\t\taction,\n\t\tstd::move(callback));\n}\n\nvoid WebViewInstance::botInvokeCustomMethod(\n\t\tUi::BotWebView::CustomMethodRequest request) {\n\tconst auto callback = request.callback;\n\t_session->api().request(MTPbots_InvokeWebViewCustomMethod(\n\t\t_bot->inputUser(),\n\t\tMTP_string(request.method),\n\t\tMTP_dataJSON(MTP_bytes(request.params))\n\t)).done([=](const MTPDataJSON &result) {\n\t\tcallback(result.data().vdata().v);\n\t}).fail([=](const MTP::Error &error) {\n\t\tcallback(base::make_unexpected(error.type()));\n\t}).send();\n}\n\nvoid WebViewInstance::botSendPreparedMessage(\n\t\tUi::BotWebView::SendPreparedMessageRequest request) {\n\tconst auto bot = _bot;\n\tconst auto id = request.id;\n\tconst auto panel = _panel.get();\n\tconst auto weak = base::make_weak(panel);\n\tconst auto callback = request.callback;\n\tif (!panel) {\n\t\tcallback(u\"UNKNOWN_ERROR\"_q);\n\t\treturn;\n\t}\n\t_session->api().request(MTPmessages_GetPreparedInlineMessage(\n\t\tbot->inputUser(),\n\t\tMTP_string(request.id)\n\t)).done([=](const MTPmessages_PreparedInlineMessage &result) {\n\t\tif (Core::App().settings().fork().skipShareFromBot()) {\n\t\t\tcallback(QString());\n\t\t\treturn;\n\t\t}\n\t\tconst auto panel = weak.get();\n\t\tconst auto &data = result.data();\n\t\tbot->owner().processUsers(data.vusers());\n\t\tconst auto parsed = std::shared_ptr<Result>(Result::Create(\n\t\t\t&bot->session(),\n\t\t\tdata.vquery_id().v,\n\t\t\tdata.vresult()));\n\t\tif (!parsed || !panel) {\n\t\t\tcallback(u\"UNKNOWN_ERROR\"_q);\n\t\t\treturn;\n\t\t}\n\t\tconst auto types = PeerTypesFromMTP(data.vpeer_types());\n\t\tconst auto history = bot->owner().history(bot->session().user());\n\t\tconst auto item = parsed->makeMessage(history, {\n\t\t\t.id = bot->owner().nextNonHistoryEntryId(),\n\t\t\t.flags = MessageFlag::FakeHistoryItem,\n\t\t\t.from = bot->session().userPeerId(),\n\t\t\t.date = base::unixtime::now(),\n\t\t\t.viaBotId = peerToUser(bot->id),\n\t\t});\n\t\tstruct State {\n\t\t\tbase::weak_qptr<Ui::BoxContent> preview;\n\t\t\tbase::weak_qptr<Ui::BoxContent> choose;\n\t\t\trpl::event_stream<not_null<Data::Thread*>> recipient;\n\t\t\tFn<void(Api::SendOptions)> send;\n\t\t\tSendPaymentHelper sendPayment;\n\t\t\tbool sent = false;\n\t\t};\n\t\tconst auto state = std::make_shared<State>();\n\t\tauto recipient = state->recipient.events();\n\t\tconst auto send = [=](\n\t\t\t\tstd::vector<not_null<Data::Thread*>> list,\n\t\t\t\tApi::SendOptions options) {\n\t\t\tif (state->sent) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->sent = true;\n\t\t\tconst auto failed = std::make_shared<int>();\n\t\t\tconst auto count = int(list.size());\n\t\t\tconst auto weak1 = state->preview;\n\t\t\tconst auto weak2 = state->choose;\n\t\t\tconst auto close = [=] {\n\t\t\t\tif (const auto strong = weak1.get()) {\n\t\t\t\t\tstrong->closeBox();\n\t\t\t\t}\n\t\t\t\tif (const auto strong = weak2.get()) {\n\t\t\t\t\tstrong->closeBox();\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto done = [=](bool success) {\n\t\t\t\tif (*failed < 0) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (success) {\n\t\t\t\t\t*failed = -1;\n\t\t\t\t\tif (const auto strong2 = weak2.get()) {\n\t\t\t\t\t\tstrong2->showToast({ tr::lng_share_done(tr::now) });\n\t\t\t\t\t} else if (const auto strong1 = weak1.get()) {\n\t\t\t\t\t\tstrong1->showToast({ tr::lng_share_done(tr::now) });\n\t\t\t\t\t}\n\t\t\t\t\tbase::call_delayed(Ui::Toast::kDefaultDuration, close);\n\t\t\t\t\tcallback(QString());\n\t\t\t\t} else if (++*failed == count) {\n\t\t\t\t\tclose();\n\t\t\t\t\tcallback(u\"MESSAGE_SEND_FAILED\"_q);\n\t\t\t\t}\n\t\t\t};\n\t\t\tfor (const auto &thread : list) {\n\t\t\t\tbot->session().api().sendInlineResult(\n\t\t\t\t\tbot,\n\t\t\t\t\tparsed.get(),\n\t\t\t\t\tApi::SendAction(thread, options),\n\t\t\t\t\tstd::nullopt,\n\t\t\t\t\tdone);\n\t\t\t}\n\t\t};\n\t\tauto box = Box(PreparedPreviewBox, item, std::move(recipient), [=] {\n\t\t\tif (state->sent) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto chosen = [=](not_null<Data::Thread*> thread) {\n\t\t\t\tif (!Data::CanSend(thread, ChatRestriction::SendInline)) {\n\t\t\t\t\tpanel->showToast({\n\t\t\t\t\t\ttr::lng_restricted_send_inline_all(tr::now),\n\t\t\t\t\t});\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tstate->recipient.fire_copy(thread);\n\t\t\t\treturn true;\n\t\t\t};\n\t\t\tauto box = Window::PrepareChooseRecipientBox(\n\t\t\t\t&bot->session(),\n\t\t\t\tchosen,\n\t\t\t\ttr::lng_inline_switch_choose(),\n\t\t\t\tnullptr,\n\t\t\t\ttypes,\n\t\t\t\tsend);\n\t\t\tstate->choose = box.data();\n\t\t\tpanel->showBox(std::move(box));\n\t\t}, [=](not_null<Data::Thread*> thread) {\n\t\t\tconst auto weak = base::make_weak(thread);\n\t\t\tstate->send = [=](Api::SendOptions options) {\n\t\t\t\tconst auto strong = weak.get();\n\t\t\t\tif (!strong) {\n\t\t\t\t\tstate->send = nullptr;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto withPaymentApproved = [=](int stars) {\n\t\t\t\t\tif (const auto onstack = state->send) {\n\t\t\t\t\t\tauto copy = options;\n\t\t\t\t\t\tcopy.starsApproved = stars;\n\t\t\t\t\t\tonstack(copy);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tconst auto checked = state->sendPayment.check(\n\t\t\t\t\tuiShow(),\n\t\t\t\t\tstrong->peer(),\n\t\t\t\t\toptions,\n\t\t\t\t\t1,\n\t\t\t\t\twithPaymentApproved);\n\t\t\t\tif (!checked) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t[[maybe_unused]] const auto ongoing = base::take(state->send);\n\t\t\t\tsend({ strong }, options);\n\t\t\t};\n\t\t\tstate->send({});\n\t\t}, [=] {\n\t\t\tstate->sent = true;\n\t\t\tcallback(QString());\n\t\t\tif (const auto strong = state->preview.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t});\n\t\tbox->boxClosing() | rpl::on_next([=] {\n\t\t\tif (!state->sent) {\n\t\t\t\tcallback(\"USER_DECLINED\");\n\t\t\t}\n\t\t}, box->lifetime());\n\t\tstate->preview = box.data();\n\t\tpanel->showBox(std::move(box));\n\t}).fail([=] {\n\t\tcallback(u\"MESSAGE_EXPIRED\"_q);\n\t}).send();\n}\n\nvoid WebViewInstance::botRequestChat(\n\t\tUi::BotWebView::RequestChatRequest request) {\n\tconst auto bot = _bot;\n\tconst auto callback = request.callback;\n\tconst auto requestId = request.requestId;\n\tif (!_panel) {\n\t\tcallback(u\"UNKNOWN_ERROR\"_q);\n\t\treturn;\n\t}\n\tconst auto show = uiShow();\n\tbot->session().api().request(MTPbots_GetRequestedWebViewButton(\n\t\tbot->inputUser(),\n\t\tMTP_string(requestId)\n\t)).done([show, bot, callback, requestId](\n\t\t\tconst MTPKeyboardButton &result) {\n\t\tresult.match([&](const MTPDkeyboardButtonRequestPeer &data) {\n\t\t\tif (!*show) {\n\t\t\t\tcallback(u\"UNKNOWN_ERROR\"_q);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto buttonId = data.vbutton_id();\n\t\t\tconst auto sendPeers = [=](\n\t\t\t\t\tstd::vector<not_null<PeerData*>> peers) {\n\t\t\t\tusing Flag = MTPmessages_SendBotRequestedPeer::Flag;\n\t\t\t\tbot->session().api().request(\n\t\t\t\t\tMTPmessages_SendBotRequestedPeer(\n\t\t\t\t\t\tMTP_flags(Flag::f_webapp_req_id),\n\t\t\t\t\t\tbot->input(),\n\t\t\t\t\t\tMTPint(),\n\t\t\t\t\t\tMTP_string(requestId),\n\t\t\t\t\t\tbuttonId,\n\t\t\t\t\t\tMTP_vector_from_range(\n\t\t\t\t\t\t\tpeers | ranges::views::transform([](\n\t\t\t\t\t\t\t\t\tnot_null<PeerData*> peer) {\n\t\t\t\t\t\t\t\treturn MTPInputPeer(peer->input());\n\t\t\t\t\t\t\t})))\n\t\t\t\t).done([=](const MTPUpdates &result) {\n\t\t\t\t\tbot->session().api().applyUpdates(result);\n\t\t\t\t\tcallback(QString());\n\t\t\t\t}).fail([callback](const MTP::Error &error) {\n\t\t\t\t\tcallback(error.type());\n\t\t\t\t}).send();\n\t\t\t};\n\t\t\tdata.vpeer_type().match([&](\n\t\t\t\t\tconst MTPDrequestPeerTypeCreateBot &createData) {\n\t\t\t\tShowCreateManagedBotBox({\n\t\t\t\t\t.show = show,\n\t\t\t\t\t.manager = bot,\n\t\t\t\t\t.suggestedName = qs(\n\t\t\t\t\t\tcreateData.vsuggested_name().value_or_empty()),\n\t\t\t\t\t.suggestedUsername = qs(\n\t\t\t\t\t\tcreateData.vsuggested_username()\n\t\t\t\t\t\t\t.value_or_empty()),\n\t\t\t\t\t.done = [=](not_null<UserData*> createdBot) {\n\t\t\t\t\t\tsendPeers({ createdBot });\n\t\t\t\t\t\tshow->showBox(Ui::MakeInformBox({\n\t\t\t\t\t\t\t.text = tr::lng_managed_bot_created_text(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_parent_name,\n\t\t\t\t\t\t\t\tbot->name()),\n\t\t\t\t\t\t\t.title = tr::lng_managed_bot_created_title(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\t\t\tcreatedBot->name()),\n\t\t\t\t\t\t}));\n\t\t\t\t\t},\n\t\t\t\t\t.cancelled = [=] {\n\t\t\t\t\t\tcallback(u\"USER_DECLINED\"_q);\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}, [&](const auto &) {\n\t\t\t\tconst auto query = RequestPeerQueryFromTL(data);\n\t\t\t\tShowChoosePeerBox(show, bot, query, sendPeers, [=] {\n\t\t\t\t\tcallback(u\"USER_DECLINED\"_q);\n\t\t\t\t});\n\t\t\t});\n\t\t}, [&](const auto &) {\n\t\t\tcallback(u\"UNSUPPORTED_BUTTON_TYPE\"_q);\n\t\t});\n\t}).fail([callback](const MTP::Error &error) {\n\t\tcallback(error.type());\n\t}).send();\n}\n\nvoid WebViewInstance::botSetEmojiStatus(\n\t\tUi::BotWebView::SetEmojiStatusRequest request) {\n\tconst auto bot = _bot;\n\tconst auto panel = _panel.get();\n\tconst auto callback = request.callback;\n\tconst auto duration = request.duration;\n\tif (!panel) {\n\t\tcallback(u\"UNKNOWN_ERROR\"_q);\n\t\treturn;\n\t}\n\t_session->data().customEmojiManager().resolve(\n\t\trequest.customEmojiId\n\t) | rpl::on_next_error([=](not_null<DocumentData*> document) {\n\t\tconst auto sticker = document->sticker();\n\t\tif (!sticker || sticker->setType != Data::StickersType::Emoji) {\n\t\t\tcallback(u\"SUGGESTED_EMOJI_INVALID\"_q);\n\t\t\treturn;\n\t\t}\n\t\tconst auto done = [=](bool success) {\n\t\t\tcallback(success ? QString() : u\"USER_DECLINED\"_q);\n\t\t};\n\t\tpanel->showBox(\n\t\t\tBox(ConfirmEmojiStatusBox, bot, document, duration, done));\n\t}, [=] { callback(u\"SUGGESTED_EMOJI_INVALID\"_q); }, panel->lifetime());\n}\n\nvoid WebViewInstance::botDownloadFile(\n\t\tUi::BotWebView::DownloadFileRequest request) {\n\tconst auto callback = request.callback;\n\tif (_confirmingDownload || !_panel) {\n\t\tcallback(false);\n\t\treturn;\n\t}\n\t_confirmingDownload = true;\n\tconst auto done = [=](QString path) {\n\t\t_confirmingDownload = false;\n\t\tif (path.isEmpty()) {\n\t\t\tcallback(false);\n\t\t\treturn;\n\t\t}\n\t\t_session->attachWebView().downloads().start({\n\t\t\t.bot = _bot,\n\t\t\t.url = request.url,\n\t\t\t.path = path,\n\t\t});\n\t\tcallback(true);\n\t};\n\t_session->api().request(MTPbots_CheckDownloadFileParams(\n\t\t_bot->inputUser(),\n\t\tMTP_string(request.name),\n\t\tMTP_string(request.url)\n\t)).done([=] {\n\t\t_panel->showBox(Box(DownloadFileBox, DownloadBoxArgs{\n\t\t\t.session = _session,\n\t\t\t.bot = _bot->name(),\n\t\t\t.name = base::FileNameFromUserString(request.name),\n\t\t\t.url = request.url,\n\t\t\t.done = done,\n\t\t}));\n\t}).fail([=] {\n\t\tdone(QString());\n\t}).send();\n}\n\nvoid WebViewInstance::botVerifyAge(int age) {\n\tif (v::is<WebViewSourceAgeVerification>(_source)) {\n\t\tv::get<WebViewSourceAgeVerification>(_source).done(age);\n\t}\n}\n\nvoid WebViewInstance::botOpenPrivacyPolicy() {\n\tconst auto bot = _bot;\n\tconst auto weak = _context.controller;\n\tconst auto command = u\"privacy\"_q;\n\tconst auto findCommand = [=] {\n\t\tif (!bot->isBot()) {\n\t\t\treturn QString();\n\t\t}\n\t\tfor (const auto &data : bot->botInfo->commands) {\n\t\t\tconst auto isSame = !data.command.compare(\n\t\t\t\tcommand,\n\t\t\t\tQt::CaseInsensitive);\n\t\t\tif (isSame) {\n\t\t\t\treturn data.command;\n\t\t\t}\n\t\t}\n\t\treturn QString();\n\t};\n\tconst auto makeOtherContext = [=](bool forceWindow) {\n\t\treturn QVariant::fromValue(ClickHandlerContext{\n\t\t\t.sessionWindow = (forceWindow\n\t\t\t\t? WindowForThread(weak, bot->owner().history(bot))\n\t\t\t\t: weak),\n\t\t\t.peer = bot,\n\t\t});\n\t};\n\tconst auto sendCommand = [=] {\n\t\tconst auto original = findCommand();\n\t\tif (original.isEmpty()) {\n\t\t\treturn false;\n\t\t}\n\t\tBotCommandClickHandler('/' + original).onClick(ClickContext{\n\t\t\tQt::LeftButton,\n\t\t\tmakeOtherContext(true)\n\t\t});\n\t\treturn true;\n\t};\n\tconst auto openUrl = [=](const QString &url) {\n\t\tCore::App().iv().openWithIvPreferred(\n\t\t\t_session,\n\t\t\turl,\n\t\t\tmakeOtherContext(false));\n\t};\n\tif (const auto info = _bot->botInfo.get()) {\n\t\tif (!info->privacyPolicyUrl.isEmpty()) {\n\t\t\topenUrl(info->privacyPolicyUrl);\n\t\t\treturn;\n\t\t}\n\t}\n\tif (!sendCommand()) {\n\t\topenUrl(tr::lng_profile_bot_privacy_url(tr::now));\n\t}\n}\n\nvoid WebViewInstance::botClose() {\n\tcrl::on_main(this, [=] { close(); });\n}\n\nstd::shared_ptr<Main::SessionShow> WebViewInstance::uiShow() {\n\tclass Show final : public Main::SessionShow {\n\tpublic:\n\t\texplicit Show(not_null<WebViewInstance*> that) : _that(that) {\n\t\t}\n\n\t\tvoid showOrHideBoxOrLayer(\n\t\t\t\tstd::variant<\n\t\t\t\tv::null_t,\n\t\t\t\tobject_ptr<Ui::BoxContent>,\n\t\t\t\tstd::unique_ptr<Ui::LayerWidget>> &&layer,\n\t\t\t\tUi::LayerOptions options,\n\t\t\t\tanim::type animated) const override {\n\t\t\tusing UniqueLayer = std::unique_ptr<Ui::LayerWidget>;\n\t\t\tusing ObjectBox = object_ptr<Ui::BoxContent>;\n\t\t\tconst auto panel = _that ? _that->_panel.get() : nullptr;\n\t\t\tif (v::is<UniqueLayer>(layer)) {\n\t\t\t\tUnexpected(\"Layers in WebView are not implemented.\");\n\t\t\t} else if (auto box = std::get_if<ObjectBox>(&layer)) {\n\t\t\t\tif (panel) {\n\t\t\t\t\tpanel->showBox(std::move(*box), options, animated);\n\t\t\t\t}\n\t\t\t} else if (panel) {\n\t\t\t\tpanel->hideLayer(animated);\n\t\t\t}\n\t\t}\n\t\t[[nodiscard]] not_null<QWidget*> toastParent() const override {\n\t\t\tconst auto panel = _that ? _that->_panel.get() : nullptr;\n\n\t\t\tEnsures(panel != nullptr);\n\t\t\treturn panel->toastParent();\n\t\t}\n\t\t[[nodiscard]] bool valid() const override {\n\t\t\treturn _that && (_that->_panel != nullptr);\n\t\t}\n\t\toperator bool() const override {\n\t\t\treturn valid();\n\t\t}\n\n\t\t[[nodiscard]] Main::Session &session() const override {\n\t\t\tExpects(_that.get() != nullptr);\n\n\t\t\treturn *_that->_session;\n\t\t}\n\n\tprivate:\n\t\tconst base::weak_ptr<WebViewInstance> _that;\n\n\t};\n\treturn std::make_shared<Show>(this);\n}\n\nAttachWebView::AttachWebView(not_null<Main::Session*> session)\n: _session(session)\n, _downloads(std::make_unique<Downloads>(session))\n, _storage(std::make_unique<Storage>(session))\n, _refreshTimer([=] { requestBots(); }) {\n\t_refreshTimer.callEach(kRefreshBotsTimeout);\n}\n\nAttachWebView::~AttachWebView() {\n\tcloseAll();\n\t_session->api().request(_popularAppBotsRequestId).cancel();\n}\n\nvoid AttachWebView::openByUsername(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst Api::SendAction &action,\n\t\tconst QString &botUsername,\n\t\tconst QString &startCommand,\n\t\tbool fullscreen) {\n\tif (botUsername.isEmpty()\n\t\t|| (_botUsername == botUsername\n\t\t\t&& _startCommand == startCommand\n\t\t\t&& _fullScreenRequested == fullscreen)) {\n\t\treturn;\n\t}\n\tcancel();\n\n\t_botUsername = botUsername;\n\t_startCommand = startCommand;\n\t_fullScreenRequested = fullscreen;\n\tconst auto weak = base::make_weak(controller);\n\tconst auto show = controller->uiShow();\n\tresolveUsername(show, crl::guard(weak, [=](not_null<PeerData*> peer) {\n\t\t_botUsername = QString();\n\t\tconst auto token = base::take(_startCommand);\n\t\tconst auto fullscreen = base::take(_fullScreenRequested);\n\n\t\tconst auto bot = peer->asUser();\n\t\tif (!bot || !bot->isBot()) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->showToast(tr::lng_bot_menu_not_supported(tr::now));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\topen({\n\t\t\t.bot = bot,\n\t\t\t.context = {\n\t\t\t\t.controller = controller,\n\t\t\t\t.action = action,\n\t\t\t\t.fullscreen = fullscreen,\n\t\t\t},\n\t\t\t.button = { .startCommand = token },\n\t\t\t.source = InlineBots::WebViewSourceLinkAttachMenu{},\n\t\t});\n\t}));\n}\n\nvoid AttachWebView::close(not_null<WebViewInstance*> instance) {\n\tconst auto i = ranges::find(\n\t\t_instances,\n\t\tinstance.get(),\n\t\t&std::unique_ptr<WebViewInstance>::get);\n\tif (i != end(_instances)) {\n\t\tconst auto taken = base::take(*i);\n\t\t_instances.erase(i);\n\t}\n}\n\nvoid AttachWebView::closeAll() {\n\tcancel();\n\tbase::take(_instances);\n}\n\nvoid AttachWebView::loadPopularAppBots() {\n\tif (_popularAppBotsLoaded.current() || _popularAppBotsRequestId) {\n\t\treturn;\n\t}\n\t_popularAppBotsRequestId = _session->api().request(\n\t\tMTPbots_GetPopularAppBots(\n\t\t\tMTP_string(),\n\t\t\tMTP_int(kPopularAppBotsLimit))\n\t).done([=](const MTPbots_PopularAppBots &result) {\n\t\t_popularAppBotsRequestId = 0;\n\n\t\tconst auto &list = result.data().vusers().v;\n\t\tauto parsed = std::vector<not_null<UserData*>>();\n\t\tparsed.reserve(list.size());\n\t\tfor (const auto &user : list) {\n\t\t\tconst auto bot = _session->data().processUser(user);\n\t\t\tif (bot->isBot()) {\n\t\t\t\tparsed.push_back(bot);\n\t\t\t}\n\t\t}\n\t\t_popularAppBots = std::move(parsed);\n\t\t_popularAppBotsLoaded = true;\n\t}).send();\n}\n\nauto AttachWebView::popularAppBots() const\n-> const std::vector<not_null<UserData*>> & {\n\treturn _popularAppBots;\n}\n\nrpl::producer<> AttachWebView::popularAppBotsLoaded() const {\n\treturn _popularAppBotsLoaded.changes() | rpl::to_empty;\n}\n\nvoid AttachWebView::cancel() {\n\t_session->api().request(base::take(_requestId)).cancel();\n\t_botUsername = QString();\n\t_startCommand = QString();\n}\n\nvoid AttachWebView::requestBots(Fn<void()> callback) {\n\tif (callback) {\n\t\t_botsRequestCallbacks.push_back(std::move(callback));\n\t}\n\tif (_botsRequestId) {\n\t\treturn;\n\t}\n\t_botsRequestId = _session->api().request(MTPmessages_GetAttachMenuBots(\n\t\tMTP_long(_botsHash)\n\t)).done([=](const MTPAttachMenuBots &result) {\n\t\t_botsRequestId = 0;\n\t\tresult.match([&](const MTPDattachMenuBotsNotModified &) {\n\t\t}, [&](const MTPDattachMenuBots &data) {\n\t\t\t_session->data().processUsers(data.vusers());\n\t\t\t_botsHash = data.vhash().v;\n\t\t\t_attachBots.clear();\n\t\t\t_attachBots.reserve(data.vbots().v.size());\n\t\t\tfor (const auto &bot : data.vbots().v) {\n\t\t\t\tif (auto parsed = ParseAttachBot(_session, bot)) {\n\t\t\t\t\t_attachBots.push_back(std::move(*parsed));\n\t\t\t\t}\n\t\t\t}\n\t\t\t_attachBotsUpdates.fire({});\n\t\t});\n\t\tfor (const auto &callback : base::take(_botsRequestCallbacks)) {\n\t\t\tcallback();\n\t\t}\n\t}).fail([=] {\n\t\t_botsRequestId = 0;\n\t\tfor (const auto &callback : base::take(_botsRequestCallbacks)) {\n\t\t\tcallback();\n\t\t}\n\t}).send();\n}\n\nbool AttachWebView::disclaimerAccepted(const AttachWebViewBot &bot) const {\n\treturn _disclaimerAccepted.contains(bot.user);\n}\n\nbool AttachWebView::showMainMenuNewBadge(\n\t\tconst AttachWebViewBot &bot) const {\n\treturn bot.inMainMenu\n\t\t&& bot.disclaimerRequired\n\t\t&& !disclaimerAccepted(bot);\n}\n\nvoid AttachWebView::requestAddToMenu(\n\t\tnot_null<UserData*> bot,\n\t\tFn<void(AddToMenuResult, PeerTypes supported)> done) {\n\tauto &process = _addToMenu[bot];\n\tif (done) {\n\t\tprocess.done.push_back(std::move(done));\n\t}\n\tif (process.requestId) {\n\t\treturn;\n\t}\n\n\tconst auto finish = [=](AddToMenuResult result, PeerTypes supported) {\n\t\tif (auto process = _addToMenu.take(bot)) {\n\t\t\tfor (const auto &done : process->done) {\n\t\t\t\tdone(result, supported);\n\t\t\t}\n\t\t}\n\t};\n\tif (!bot->isBot() || !bot->botInfo->supportsAttachMenu) {\n\t\tfinish(AddToMenuResult::Unsupported, {});\n\t\treturn;\n\t}\n\n\tprocess.requestId = _session->api().request(\n\t\tMTPmessages_GetAttachMenuBot(bot->inputUser())\n\t).done([=](const MTPAttachMenuBotsBot &result) {\n\t\t_addToMenu[bot].requestId = 0;\n\t\tconst auto &data = result.data();\n\t\t_session->data().processUsers(data.vusers());\n\t\tconst auto parsed = ParseAttachBot(_session, data.vbot());\n\t\tif (!parsed || bot != parsed->user) {\n\t\t\tfinish(AddToMenuResult::Unsupported, {});\n\t\t\treturn;\n\t\t}\n\t\tconst auto i = ranges::find(\n\t\t\t_attachBots,\n\t\t\tnot_null(bot),\n\t\t\t&AttachWebViewBot::user);\n\t\tif (i != end(_attachBots)) {\n\t\t\t// Save flags in our list, like 'inactive'.\n\t\t\t*i = *parsed;\n\t\t}\n\t\tconst auto types = parsed->types;\n\t\tif (parsed->inactive) {\n\t\t\tconfirmAddToMenu(*parsed, [=](bool added) {\n\t\t\t\tconst auto result = added\n\t\t\t\t\t? AddToMenuResult::Added\n\t\t\t\t\t: AddToMenuResult::Cancelled;\n\t\t\t\tfinish(result, types);\n\t\t\t});\n\t\t} else {\n\t\t\trequestBots();\n\t\t\tfinish(AddToMenuResult::AlreadyInMenu, types);\n\t\t}\n\t}).fail([=] {\n\t\tfinish(AddToMenuResult::Unsupported, {});\n\t}).send();\n}\n\nvoid AttachWebView::removeFromMenu(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<UserData*> bot) {\n\ttoggleInMenu(bot, ToggledState::Removed, [=](bool success) {\n\t\tif (success) {\n\t\t\tshow->showToast(tr::lng_bot_remove_from_menu_done(tr::now));\n\t\t}\n\t});\n}\n\nvoid AttachWebView::resolveUsername(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tFn<void(not_null<PeerData*>)> done) {\n\tif (const auto peer = _session->data().peerByUsername(_botUsername)) {\n\t\tdone(peer);\n\t\treturn;\n\t}\n\t_session->api().request(base::take(_requestId)).cancel();\n\t_requestId = _session->api().request(MTPcontacts_ResolveUsername(\n\t\tMTP_flags(0),\n\t\tMTP_string(_botUsername),\n\t\tMTP_string()\n\t)).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\t_requestId = 0;\n\t\tresult.match([&](const MTPDcontacts_resolvedPeer &data) {\n\t\t\t_session->data().processUsers(data.vusers());\n\t\t\t_session->data().processChats(data.vchats());\n\t\t\tif (const auto peerId = peerFromMTP(data.vpeer())) {\n\t\t\t\tdone(_session->data().peer(peerId));\n\t\t\t}\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\t_requestId = 0;\n\t\tif (error.code() == 400) {\n\t\t\tshow->showToast(\n\t\t\t\ttr::lng_username_not_found(tr::now, lt_user, _botUsername));\n\t\t}\n\t}).send();\n}\n\nvoid AttachWebView::open(WebViewDescriptor &&descriptor) {\n\tfor (const auto &instance : _instances) {\n\t\tif (instance->bot() == descriptor.bot\n\t\t\t&& instance->source() == descriptor.source) {\n\t\t\tinstance->activate();\n\t\t\treturn;\n\t\t}\n\t}\n\t_instances.push_back(\n\t\tstd::make_unique<WebViewInstance>(std::move(descriptor)));\n\t_instances.back()->activate();\n}\n\nvoid AttachWebView::acceptMainMenuDisclaimer(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<UserData*> bot,\n\t\tFn<void(AddToMenuResult, PeerTypes supported)> done) {\n\tconst auto i = ranges::find(_attachBots, bot, &AttachWebViewBot::user);\n\tif (i == end(_attachBots)) {\n\t\t_attachBotsUpdates.fire({});\n\t\treturn;\n\t} else if (i->inactive) {\n\t\trequestAddToMenu(bot, std::move(done));\n\t\treturn;\n\t} else if (!i->disclaimerRequired || disclaimerAccepted(*i)) {\n\t\tdone(AddToMenuResult::AlreadyInMenu, i->types);\n\t\treturn;\n\t}\n\tconst auto types = i->types;\n\tshow->show(Box(FillDisclaimerBox, crl::guard(this, [=](bool accepted) {\n\t\tif (accepted) {\n\t\t\t_disclaimerAccepted.emplace(bot);\n\t\t\t_attachBotsUpdates.fire({});\n\t\t\tdone(AddToMenuResult::AlreadyInMenu, types);\n\t\t} else {\n\t\t\tdone(AddToMenuResult::Cancelled, {});\n\t\t}\n\t})));\n}\n\nvoid AttachWebView::confirmAddToMenu(\n\t\tAttachWebViewBot bot,\n\t\tFn<void(bool added)> callback) {\n\tconst auto active = Core::App().activeWindow();\n\tif (!active) {\n\t\tif (callback) {\n\t\t\tcallback(false);\n\t\t}\n\t\treturn;\n\t}\n\tconst auto weak = base::make_weak(active);\n\tactive->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto allowed = std::make_shared<Ui::Checkbox*>();\n\t\tconst auto disclaimer = !disclaimerAccepted(bot);\n\t\tconst auto done = [=](Fn<void()> close) {\n\t\t\tconst auto state = (disclaimer\n\t\t\t\t|| ((*allowed) && (*allowed)->checked()))\n\t\t\t\t? ToggledState::AllowedToWrite\n\t\t\t\t: ToggledState::Added;\n\t\t\ttoggleInMenu(bot.user, state, [=](bool success) {\n\t\t\t\tif (callback) {\n\t\t\t\t\tcallback(success);\n\t\t\t\t}\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->showToast((bot.inMainMenu\n\t\t\t\t\t\t? tr::lng_bot_add_to_side_menu_done\n\t\t\t\t\t\t: tr::lng_bot_add_to_menu_done)(tr::now));\n\t\t\t\t}\n\t\t\t});\n\t\t\tclose();\n\t\t};\n\t\tif (disclaimer) {\n\t\t\tFillDisclaimerBox(box, [=](bool accepted) {\n\t\t\t\tif (accepted) {\n\t\t\t\t\t_disclaimerAccepted.emplace(bot.user);\n\t\t\t\t\t_attachBotsUpdates.fire({});\n\t\t\t\t\tdone([] {});\n\t\t\t\t} else if (callback) {\n\t\t\t\t\tcallback(false);\n\t\t\t\t}\n\t\t\t});\n\t\t\tbox->addRow(object_ptr<Ui::FixedHeightWidget>(\n\t\t\t\tbox,\n\t\t\t\tst::boxRowPadding.left()));\n\t\t\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_bot_will_be_added(\n\t\t\t\t\tlt_bot,\n\t\t\t\t\trpl::single(tr::bold(bot.name)),\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::boxLabel));\n\t\t} else {\n\t\t\tUi::ConfirmBox(box, {\n\t\t\t\t(bot.inMainMenu\n\t\t\t\t\t? tr::lng_bot_add_to_side_menu\n\t\t\t\t\t: tr::lng_bot_add_to_menu)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_bot,\n\t\t\t\t\t\ttr::bold(bot.name),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\tdone,\n\t\t\t\t(callback\n\t\t\t\t\t? [=](Fn<void()> close) { callback(false); close(); }\n\t\t\t\t\t: Fn<void(Fn<void()>)>()),\n\t\t\t});\n\t\t\tif (bot.requestWriteAccess) {\n\t\t\t\t(*allowed) = box->addRow(\n\t\t\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\t\t\tbox,\n\t\t\t\t\t\ttr::lng_url_auth_allow_messages(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_bot,\n\t\t\t\t\t\t\ttr::bold(bot.name),\n\t\t\t\t\t\t\ttr::marked),\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t\tst::urlAuthCheckbox),\n\t\t\t\t\tstyle::margins(\n\t\t\t\t\t\tst::boxRowPadding.left(),\n\t\t\t\t\t\t(disclaimer\n\t\t\t\t\t\t\t? st::boxPhotoCaptionSkip\n\t\t\t\t\t\t\t: st::boxRowPadding.left()),\n\t\t\t\t\t\tst::boxRowPadding.right(),\n\t\t\t\t\t\tst::boxRowPadding.left()));\n\t\t\t\t(*allowed)->setAllowTextLines();\n\t\t\t}\n\t\t}\n\t}));\n}\n\nvoid AttachWebView::toggleInMenu(\n\t\tnot_null<UserData*> bot,\n\t\tToggledState state,\n\t\tFn<void(bool success)> callback) {\n\tusing Flag = MTPmessages_ToggleBotInAttachMenu::Flag;\n\t_session->api().request(MTPmessages_ToggleBotInAttachMenu(\n\t\tMTP_flags((state == ToggledState::AllowedToWrite)\n\t\t\t? Flag::f_write_allowed\n\t\t\t: Flag()),\n\t\tbot->inputUser(),\n\t\tMTP_bool(state != ToggledState::Removed)\n\t)).done([=] {\n\t\t_requestId = 0;\n\t\t_session->api().request(base::take(_botsRequestId)).cancel();\n\t\trequestBots(callback ? [=] { callback(true); } : Fn<void()>());\n\t}).fail([=] {\n\t\tcancel();\n\t\tif (callback) {\n\t\t\tcallback(false);\n\t\t}\n\t}).send();\n}\n\nvoid ChooseAndSendLocation(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst Ui::LocationPickerConfig &config,\n\t\tApi::SendAction action) {\n\tconst auto weak = base::make_weak(controller);\n\tconst auto session = &controller->session();\n\tif (const auto picker = session->locationPickers().lookup(action)) {\n\t\tpicker->activate();\n\t\treturn;\n\t}\n\tstruct State {\n\t\tSendPaymentHelper sendPayment;\n\t\tFn<void(Data::InputVenue, Api::SendAction)> send;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tstate->send = [=](Data::InputVenue venue, Api::SendAction action) {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tconst auto withPaymentApproved = [=](int stars) {\n\t\t\t\tif (const auto onstack = state->send) {\n\t\t\t\t\tauto copy = action;\n\t\t\t\t\tcopy.options.starsApproved = stars;\n\t\t\t\t\tonstack(venue, copy);\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto checked = state->sendPayment.check(\n\t\t\t\tstrong,\n\t\t\t\taction.history->peer,\n\t\t\t\taction.options,\n\t\t\t\t1,\n\t\t\t\twithPaymentApproved);\n\t\t\tif (!checked) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tstate->send = nullptr;\n\t\tif (venue.justLocation()) {\n\t\t\tApi::SendLocation(action, venue.lat, venue.lon);\n\t\t} else {\n\t\t\tApi::SendVenue(action, venue);\n\t\t}\n\t};\n\tconst auto callback = [=](Data::InputVenue venue) {\n\t\tstate->send(venue, action);\n\t};\n\tconst auto picker = Ui::LocationPicker::Show({\n\t\t.parent = controller->widget(),\n\t\t.config = config,\n\t\t.chooseLabel = tr::lng_maps_point_send(),\n\t\t.recipient = action.history->peer,\n\t\t.session = session,\n\t\t.callback = crl::guard(session, callback),\n\t\t.quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); },\n\t\t.storageId = session->local().resolveStorageIdBots(),\n\t\t.closeRequests = controller->content()->death(),\n\t});\n\tsession->locationPickers().emplace(action, picker);\n}\n\nstd::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tFn<Api::SendAction()> actionFactory,\n\t\tFn<void(bool)> attach) {\n\tauto result = std::make_unique<Ui::DropdownMenu>(\n\t\tparent,\n\t\tst::dropdownMenuWithIcons);\n\tconst auto bots = &peer->session().attachWebView();\n\tconst auto raw = result.get();\n\tauto minimal = 0;\n\tif (Data::CanSend(peer, ChatRestriction::SendPhotos, false)) {\n\t\t++minimal;\n\t\traw->addAction(tr::lng_attach_photo_or_video(tr::now), [=] {\n\t\t\tattach(true);\n\t\t}, &st::menuIconPhoto);\n\t}\n\tconst auto fileTypes = ChatRestriction::SendVideos\n\t\t| ChatRestriction::SendGifs\n\t\t| ChatRestriction::SendStickers\n\t\t| ChatRestriction::SendMusic\n\t\t| ChatRestriction::SendFiles;\n\tif (Data::CanSendAnyOf(peer, fileTypes)) {\n\t\t++minimal;\n\t\traw->addAction(tr::lng_attach_document(tr::now), [=] {\n\t\t\tattach(false);\n\t\t}, &st::menuIconFile);\n\t}\n\tif (peer->canCreatePolls()) {\n\t\t++minimal;\n\t\traw->addAction(tr::lng_polls_menu_item(tr::now), [=] {\n\t\t\tconst auto action = actionFactory();\n\t\t\tconst auto source = action.options.scheduled\n\t\t\t\t? Api::SendType::Scheduled\n\t\t\t\t: Api::SendType::Normal;\n\t\t\tconst auto sendMenuType = (action.replyTo.topicRootId\n\t\t\t\t|| action.history->peer->starsPerMessageChecked())\n\t\t\t\t? SendMenu::Type::SilentOnly\n\t\t\t\t: SendMenu::Type::Scheduled;\n\t\t\tconst auto chosen = kDefaultPollCreateFlags;\n\t\t\tWindow::PeerMenuCreatePoll(\n\t\t\t\tcontroller,\n\t\t\t\tpeer,\n\t\t\t\taction.replyTo,\n\t\t\t\taction.options.suggest,\n\t\t\t\tchosen,\n\t\t\t\tPollData::Flags(),\n\t\t\t\tsource,\n\t\t\t\t{ sendMenuType });\n\t\t}, &st::menuIconCreatePoll);\n\t}\n\tif (peer->canCreateTodoLists()) {\n\t\t++minimal;\n\t\traw->addAction(tr::lng_todo_menu_item(tr::now), [=] {\n\t\t\tconst auto action = actionFactory();\n\t\t\tconst auto source = action.options.scheduled\n\t\t\t\t? Api::SendType::Scheduled\n\t\t\t\t: Api::SendType::Normal;\n\t\t\tconst auto sendMenuType = (action.replyTo.topicRootId\n\t\t\t\t|| action.history->peer->starsPerMessageChecked())\n\t\t\t\t? SendMenu::Type::SilentOnly\n\t\t\t\t: SendMenu::Type::Scheduled;\n\t\t\tWindow::PeerMenuCreateTodoList(\n\t\t\t\tcontroller,\n\t\t\t\tpeer,\n\t\t\t\taction.replyTo,\n\t\t\t\taction.options.suggest,\n\t\t\t\tsource,\n\t\t\t\t{ sendMenuType });\n\t\t}, &st::menuIconCreateTodoList);\n\t}\n\tconst auto session = &controller->session();\n\tconst auto locationType = ChatRestriction::SendOther;\n\tconst auto config = ResolveMapsConfig(session);\n\tif (Data::CanSendAnyOf(peer, locationType)\n\t\t&& Ui::LocationPicker::Available(config)) {\n\t\traw->addAction(tr::lng_maps_point(tr::now), [=] {\n\t\t\tChooseAndSendLocation(controller, config, actionFactory());\n\t\t}, &st::menuIconAddress);\n\t}\n\tconst auto addBots = Data::CanSend(peer, ChatRestriction::SendInline)\n\t\t&& !peer->starsPerMessageChecked();\n\tfor (const auto &bot : bots->attachBots()) {\n\t\tif (!addBots\n\t\t\t|| !bot.inAttachMenu\n\t\t\t|| !PeerMatchesTypes(peer, bot.user, bot.types)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto callback = [=] {\n\t\t\tbots->open({\n\t\t\t\t.bot = bot.user,\n\t\t\t\t.context = {\n\t\t\t\t\t.controller = controller,\n\t\t\t\t\t.action = actionFactory(),\n\t\t\t\t},\n\t\t\t\t.source = InlineBots::WebViewSourceAttachMenu(),\n\t\t\t});\n\t\t};\n\t\tauto action = base::make_unique_q<BotAction>(\n\t\t\traw->menu(),\n\t\t\tcontroller->uiShow(),\n\t\t\traw->menu()->st(),\n\t\t\tbot,\n\t\t\tcallback);\n\t\taction->forceShown(\n\t\t) | rpl::on_next([=](bool shown) {\n\t\t\tif (shown) {\n\t\t\t\traw->setAutoHiding(false);\n\t\t\t} else {\n\t\t\t\traw->hideAnimated();\n\t\t\t\traw->setAutoHiding(true);\n\t\t\t}\n\t\t}, action->lifetime());\n\t\traw->addAction(std::move(action));\n\t}\n\tconst auto actions = raw->actions().size();\n\tconst auto onclick = ChatHelpers::ShowPanelOnClick();\n\tif (!actions) {\n\t\treturn nullptr;\n\t} else if (actions <= minimal && !onclick) {\n\t\treturn nullptr;\n\t}\n\treturn result;\n}\n\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/bot_attach_web_view.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_common.h\"\n#include \"base/flags.h\"\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"mtproto/sender.h\"\n#include \"ui/chat/attach/attach_bot_webview.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Data {\nclass Thread;\n} // namespace Data\n\nnamespace Ui {\nclass Show;\nclass GenericBox;\nclass DropdownMenu;\n} // namespace Ui\n\nnamespace Ui::BotWebView {\nclass Panel;\nstruct DownloadsEntry;\n} // namespace Ui::BotWebView\n\nnamespace Main {\nclass Session;\nclass SessionShow;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Data {\nclass DocumentMedia;\n} // namespace Data\n\nnamespace Payments {\nstruct NonPanelPaymentForm;\nenum class CheckoutResult;\n} // namespace Payments\n\nnamespace InlineBots {\n\nclass WebViewInstance;\nclass Downloads;\nclass Storage;\n\nenum class PeerType : uint8 {\n\tSameBot   = 0x01,\n\tBot       = 0x02,\n\tUser      = 0x04,\n\tGroup     = 0x08,\n\tBroadcast = 0x10,\n};\nusing PeerTypes = base::flags<PeerType>;\n\n[[nodiscard]] bool PeerMatchesTypes(\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> bot,\n\tPeerTypes types);\n[[nodiscard]] PeerTypes ParseChooseTypes(QStringView choose);\n\nstruct AttachWebViewBot {\n\tnot_null<UserData*> user;\n\tDocumentData *icon = nullptr;\n\tstd::shared_ptr<Data::DocumentMedia> media;\n\tQString name;\n\tPeerTypes types = 0;\n\tbool inactive : 1 = false;\n\tbool inMainMenu : 1 = false;\n\tbool inAttachMenu : 1 = false;\n\tbool disclaimerRequired : 1 = false;\n\tbool requestWriteAccess : 1 = false;\n};\n\nstruct WebViewSourceButton {\n\tbool simple = false;\n\n\tfriend inline bool operator==(\n\t\tWebViewSourceButton,\n\t\tWebViewSourceButton) = default;\n};\n\nstruct WebViewSourceSwitch {\n\tfriend inline bool operator==(\n\t\tconst WebViewSourceSwitch &,\n\t\tconst WebViewSourceSwitch &) = default;\n};\n\nstruct WebViewSourceLinkApp { // t.me/botusername/appname\n\tbase::weak_ptr<WebViewInstance> from;\n\tQString appname;\n\tQString token;\n\n\tfriend inline bool operator==(\n\t\tconst WebViewSourceLinkApp &,\n\t\tconst WebViewSourceLinkApp &) = default;\n};\n\nstruct WebViewSourceLinkAttachMenu { // ?startattach\n\tbase::weak_ptr<WebViewInstance> from;\n\tbase::weak_ptr<Data::Thread> thread;\n\tPeerTypes choose;\n\tQString token;\n\n\tfriend inline bool operator==(\n\t\tconst WebViewSourceLinkAttachMenu &,\n\t\tconst WebViewSourceLinkAttachMenu &) = default;\n};\n\nstruct WebViewSourceLinkBotProfile { // t.me/botusername?startapp\n\tbase::weak_ptr<WebViewInstance> from;\n\tQString token;\n\tbool compact = false;\n\n\tfriend inline bool operator==(\n\t\tconst WebViewSourceLinkBotProfile &,\n\t\tconst WebViewSourceLinkBotProfile &) = default;\n};\n\nstruct WebViewSourceMainMenu {\n\tfriend inline bool operator==(\n\t\tWebViewSourceMainMenu,\n\t\tWebViewSourceMainMenu) = default;\n};\n\nstruct WebViewSourceAttachMenu {\n\tbase::weak_ptr<Data::Thread> thread;\n\n\tfriend inline bool operator==(\n\t\tconst WebViewSourceAttachMenu &,\n\t\tconst WebViewSourceAttachMenu &) = default;\n};\n\nstruct WebViewSourceBotMenu {\n\tfriend inline bool operator==(\n\t\tWebViewSourceBotMenu,\n\t\tWebViewSourceBotMenu) = default;\n};\n\nstruct WebViewSourceGame {\n\tFullMsgId messageId;\n\tQString title;\n\n\tfriend inline bool operator==(\n\t\tWebViewSourceGame,\n\t\tWebViewSourceGame) = default;\n};\n\nstruct WebViewSourceBotProfile {\n\tfriend inline bool operator==(\n\t\tWebViewSourceBotProfile,\n\t\tWebViewSourceBotProfile) = default;\n};\n\nstruct WebViewSourceAgeVerification {\n\tFn<void(int)> done;\n\n\tfriend inline bool operator==(\n\t\t\tWebViewSourceAgeVerification,\n\t\t\tWebViewSourceAgeVerification) {\n\t\treturn true;\n\t}\n};\n\nstruct WebViewSource : std::variant<\n\tWebViewSourceButton,\n\tWebViewSourceSwitch,\n\tWebViewSourceLinkApp,\n\tWebViewSourceLinkAttachMenu,\n\tWebViewSourceLinkBotProfile,\n\tWebViewSourceMainMenu,\n\tWebViewSourceAttachMenu,\n\tWebViewSourceBotMenu,\n\tWebViewSourceGame,\n\tWebViewSourceBotProfile,\n\tWebViewSourceAgeVerification> {\n\tusing variant::variant;\n};\n\nstruct WebViewButton {\n\tQString text;\n\tQString startCommand;\n\tQByteArray url;\n\tbool fromAttachMenu = false;\n\tbool fromMainMenu = false;\n\tbool fromSwitch = false;\n};\n\nstruct WebViewContext {\n\tbase::weak_ptr<Window::SessionController> controller;\n\tDialogs::EntryState dialogsEntryState;\n\tstd::optional<Api::SendAction> action;\n\tbool fullscreen = false;\n\tbool maySkipConfirmation = false;\n};\n\nstruct WebViewDescriptor {\n\tnot_null<UserData*> bot;\n\tstd::shared_ptr<Ui::Show> parentShow;\n\tWebViewContext context;\n\tWebViewButton button;\n\tWebViewSource source;\n};\n\nclass WebViewInstance final\n\t: public base::has_weak_ptr\n\t, public Ui::BotWebView::Delegate {\npublic:\n\texplicit WebViewInstance(WebViewDescriptor &&descriptor);\n\t~WebViewInstance();\n\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] not_null<UserData*> bot() const;\n\t[[nodiscard]] WebViewSource source() const;\n\n\tvoid activate();\n\tvoid close();\n\n\t[[nodiscard]] std::shared_ptr<Main::SessionShow> uiShow();\n\nprivate:\n\tvoid resolve();\n\tvoid requestFullBot();\n\n\tbool openAppFromBotMenuLink();\n\n\tvoid requestButton();\n\tvoid requestSimple();\n\tvoid requestMain();\n\tvoid requestApp(bool allowWrite);\n\tvoid requestWithMainMenuDisclaimer();\n\tvoid requestWithMenuAdd();\n\tvoid maybeChooseAndRequestButton(PeerTypes supported);\n\n\tenum class ConfirmType : uchar {\n\t\tAlways,\n\t\tOnce,\n\t\tNone,\n\t};\n\tvoid resolveApp(\n\t\tconst QString &appname,\n\t\tconst QString &startparam,\n\t\tConfirmType confirmType);\n\tvoid confirmOpen(Fn<void()> done);\n\tvoid confirmAppOpen(\n\t\tbool writeAccess,\n\t\tFn<void(bool allowWrite)> done,\n\t\tbool forceConfirmation);\n\n\tstruct ShowArgs {\n\t\tQString url;\n\t\tQString title;\n\t\tuint64 queryId = 0;\n\t\tbool fullscreen = false;\n\t};\n\tvoid show(ShowArgs &&args);\n\tvoid showGame();\n\tvoid started(uint64 queryId);\n\n\tauto nonPanelPaymentFormFactory(\n\t\tFn<void(Payments::CheckoutResult)> reactivate)\n\t-> Fn<void(Payments::NonPanelPaymentForm)>;\n\n\tWebview::ThemeParams botThemeParams() override;\n\tUi::Text::MarkedContext botTextContext() override;\n\tauto botDownloads(bool forceCheck = false)\n\t\t-> const std::vector<Ui::BotWebView::DownloadsEntry> & override;\n\tvoid botDownloadsAction(\n\t\tuint32 id,\n\t\tUi::BotWebView::DownloadsAction type) override;\n\tbool botHandleLocalUri(QString uri, bool keepOpen) override;\n\tvoid botHandleInvoice(QString slug) override;\n\tvoid botHandleMenuButton(Ui::BotWebView::MenuButton button) override;\n\tbool botValidateExternalLink(QString uri) override;\n\tvoid botOpenIvLink(QString uri) override;\n\tvoid botSendData(QByteArray data) override;\n\tvoid botSwitchInlineQuery(\n\t\tstd::vector<QString> chatTypes,\n\t\tQString query) override;\n\tvoid botCheckWriteAccess(Fn<void(bool allowed)> callback) override;\n\tvoid botAllowWriteAccess(Fn<void(bool allowed)> callback) override;\n\tbool botStorageWrite(QString key, std::optional<QString> value) override;\n\tstd::optional<QString> botStorageRead(QString key) override;\n\tvoid botStorageClear() override;\n\tvoid botRequestEmojiStatusAccess(\n\t\tFn<void(bool allowed)> callback) override;\n\tvoid botSharePhone(Fn<void(bool shared)> callback) override;\n\tvoid botInvokeCustomMethod(\n\t\tUi::BotWebView::CustomMethodRequest request) override;\n\tvoid botSendPreparedMessage(\n\t\tUi::BotWebView::SendPreparedMessageRequest request) override;\n\tvoid botRequestChat(\n\t\tUi::BotWebView::RequestChatRequest request) override;\n\tvoid botSetEmojiStatus(\n\t\tUi::BotWebView::SetEmojiStatusRequest request) override;\n\tvoid botDownloadFile(\n\t\tUi::BotWebView::DownloadFileRequest request) override;\n\tvoid botVerifyAge(int age) override;\n\tvoid botOpenPrivacyPolicy() override;\n\tvoid botClose() override;\n\n\tconst std::shared_ptr<Ui::Show> _parentShow;\n\tconst not_null<Main::Session*> _session;\n\tconst not_null<UserData*> _bot;\n\tconst WebViewContext _context;\n\tconst WebViewButton _button;\n\tconst WebViewSource _source;\n\n\tstd::optional<ShowArgs> _botFullWaitingArgs;\n\n\tBotAppData *_app = nullptr;\n\tQString _appStartParam;\n\tbool _dataSent = false;\n\tbool _confirmingDownload = false;\n\n\tmtpRequestId _requestId = 0;\n\tmtpRequestId _prolongId = 0;\n\n\tQString _panelUrl;\n\tstd::unique_ptr<Ui::BotWebView::Panel> _panel;\n\n\trpl::lifetime _lifetime;\n\n\tstatic base::weak_ptr<WebViewInstance> PendingActivation;\n\n\tbool _copyBotUrls = false;\n\n};\n\nclass AttachWebView final : public base::has_weak_ptr {\npublic:\n\texplicit AttachWebView(not_null<Main::Session*> session);\n\t~AttachWebView();\n\n\t[[nodiscard]] Downloads &downloads() const {\n\t\treturn *_downloads;\n\t}\n\t[[nodiscard]] Storage &storage() const {\n\t\treturn *_storage;\n\t}\n\n\tvoid open(WebViewDescriptor &&descriptor);\n\tvoid openByUsername(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst Api::SendAction &action,\n\t\tconst QString &botUsername,\n\t\tconst QString &startCommand,\n\t\tbool fullscreen);\n\n\tvoid cancel();\n\n\tvoid requestBots(Fn<void()> callback = nullptr);\n\t[[nodiscard]] const std::vector<AttachWebViewBot> &attachBots() const {\n\t\treturn _attachBots;\n\t}\n\t[[nodiscard]] rpl::producer<> attachBotsUpdates() const {\n\t\treturn _attachBotsUpdates.events();\n\t}\n\tvoid notifyBotIconLoaded() {\n\t\t_attachBotsUpdates.fire({});\n\t}\n\t[[nodiscard]] bool disclaimerAccepted(\n\t\tconst AttachWebViewBot &bot) const;\n\t[[nodiscard]] bool showMainMenuNewBadge(\n\t\tconst AttachWebViewBot &bot) const;\n\n\tvoid removeFromMenu(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<UserData*> bot);\n\n\tenum class AddToMenuResult {\n\t\tAlreadyInMenu,\n\t\tAdded,\n\t\tUnsupported,\n\t\tCancelled,\n\t};\n\tvoid requestAddToMenu(\n\t\tnot_null<UserData*> bot,\n\t\tFn<void(AddToMenuResult, PeerTypes supported)> done);\n\tvoid acceptMainMenuDisclaimer(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<UserData*> bot,\n\t\tFn<void(AddToMenuResult, PeerTypes supported)> done);\n\n\tvoid close(not_null<WebViewInstance*> instance);\n\tvoid closeAll();\n\n\tvoid loadPopularAppBots();\n\t[[nodiscard]] auto popularAppBots() const\n\t\t-> const std::vector<not_null<UserData*>> &;\n\t[[nodiscard]] rpl::producer<> popularAppBotsLoaded() const;\n\nprivate:\n\tvoid resolveUsername(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tFn<void(not_null<PeerData*>)> done);\n\n\tenum class ToggledState {\n\t\tRemoved,\n\t\tAdded,\n\t\tAllowedToWrite,\n\t};\n\tvoid toggleInMenu(\n\t\tnot_null<UserData*> bot,\n\t\tToggledState state,\n\t\tFn<void(bool success)> callback = nullptr);\n\tvoid confirmAddToMenu(\n\t\tAttachWebViewBot bot,\n\t\tFn<void(bool added)> callback = nullptr);\n\n\tconst not_null<Main::Session*> _session;\n\tconst std::unique_ptr<Downloads> _downloads;\n\tconst std::unique_ptr<Storage> _storage;\n\n\tbase::Timer _refreshTimer;\n\n\tQString _botUsername;\n\tQString _startCommand;\n\tbool _fullScreenRequested = false;\n\n\tmtpRequestId _requestId = 0;\n\n\tuint64 _botsHash = 0;\n\tmtpRequestId _botsRequestId = 0;\n\tstd::vector<Fn<void()>> _botsRequestCallbacks;\n\n\tstruct AddToMenuProcess {\n\t\tmtpRequestId requestId = 0;\n\t\tstd::vector<Fn<void(AddToMenuResult, PeerTypes supported)>> done;\n\t};\n\tbase::flat_map<not_null<UserData*>, AddToMenuProcess> _addToMenu;\n\n\tstd::vector<AttachWebViewBot> _attachBots;\n\trpl::event_stream<> _attachBotsUpdates;\n\tbase::flat_set<not_null<UserData*>> _disclaimerAccepted;\n\n\tstd::vector<std::unique_ptr<WebViewInstance>> _instances;\n\n\tstd::vector<not_null<UserData*>> _popularAppBots;\n\tmtpRequestId _popularAppBotsRequestId = 0;\n\trpl::variable<bool> _popularAppBotsLoaded = false;\n\n};\n\n[[nodiscard]] std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(\n\tnot_null<QWidget*> parent,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\tFn<Api::SendAction()> actionFactory,\n\tFn<void(bool)> attach);\n\nclass MenuBotIcon final : public Ui::RpWidget {\npublic:\n\tMenuBotIcon(\n\t\tQWidget *parent,\n\t\tstd::shared_ptr<Data::DocumentMedia> media);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid validate();\n\n\tstd::shared_ptr<Data::DocumentMedia> _media;\n\tQImage _image;\n\tQImage _mask;\n\n};\n\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_bot_confirm_prepared.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"inline_bots/inline_bot_confirm_prepared.h\"\n\n#include \"boxes/peers/edit_peer_invite_link.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/admin_log/history_admin_log_item.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/section_widget.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n\nnamespace InlineBots {\nnamespace {\n\nusing namespace HistoryView;\n\nclass PreviewDelegate final : public DefaultElementDelegate {\npublic:\n\tPreviewDelegate(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Ui::ChatStyle*> st,\n\t\tFn<void()> update);\n\n\tbool elementAnimationsPaused() override;\n\tnot_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;\n\tContext elementContext() override;\n\nprivate:\n\tconst not_null<QWidget*> _parent;\n\tconst std::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\n};\n\nclass PreviewWrap final : public Ui::RpWidget {\npublic:\n\tPreviewWrap(not_null<QWidget*> parent, not_null<HistoryItem*> item);\n\t~PreviewWrap();\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid resizeTo(int width);\n\tvoid prepare(not_null<HistoryItem*> item);\n\n\tconst not_null<History*> _history;\n\tconst std::unique_ptr<Ui::ChatTheme> _theme;\n\tconst std::unique_ptr<Ui::ChatStyle> _style;\n\tconst std::unique_ptr<PreviewDelegate> _delegate;\n\tAdminLog::OwnedItem _item;\n\tQPoint _position;\n\n};\n\nPreviewDelegate::PreviewDelegate(\n\tnot_null<QWidget*> parent,\n\tnot_null<Ui::ChatStyle*> st,\n\tFn<void()> update)\n: _parent(parent)\n, _pathGradient(MakePathShiftGradient(st, update)) {\n}\n\nbool PreviewDelegate::elementAnimationsPaused() {\n\treturn _parent->window()->isActiveWindow();\n}\n\nauto PreviewDelegate::elementPathShiftGradient()\n-> not_null<Ui::PathShiftGradient*> {\n\treturn _pathGradient.get();\n}\n\nContext PreviewDelegate::elementContext() {\n\treturn Context::History;\n}\n\nPreviewWrap::PreviewWrap(\n\tnot_null<QWidget*> parent,\n\tnot_null<HistoryItem*> item)\n: RpWidget(parent)\n, _history(item->history())\n, _theme(Window::Theme::DefaultChatThemeOn(lifetime()))\n, _style(std::make_unique<Ui::ChatStyle>(\n\t_history->session().colorIndicesValue()))\n, _delegate(std::make_unique<PreviewDelegate>(\n\tparent,\n\t_style.get(),\n\t[=] { update(); }))\n, _position(0, st::msgMargin.bottom()) {\n\t_style->apply(_theme.get());\n\n\tusing namespace HistoryView;\n\t_history->owner().viewRepaintRequest(\n\t) | rpl::on_next([=](Data::RequestViewRepaint data) {\n\t\tif (data.view == _item.get()) {\n\t\t\tupdate();\n\t\t}\n\t}, lifetime());\n\n\t_history->session().downloaderTaskFinished() | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\tprepare(item);\n}\n\nPreviewWrap::~PreviewWrap() {\n\t_item = {};\n}\n\nvoid PreviewWrap::prepare(not_null<HistoryItem*> item) {\n\t_item = AdminLog::OwnedItem(_delegate.get(), item);\n\tif (width() >= st::msgMinWidth) {\n\t\tresizeTo(width());\n\t}\n\n\twidthValue(\n\t) | rpl::filter([=](int width) {\n\t\treturn width >= st::msgMinWidth;\n\t}) | rpl::on_next([=](int width) {\n\t\tresizeTo(width);\n\t}, lifetime());\n}\n\nvoid PreviewWrap::resizeTo(int width) {\n\tconst auto height = _position.y()\n\t\t+ _item->resizeGetHeight(width)\n\t\t+ _position.y()\n\t\t+ st::msgServiceMargin.top()\n\t\t+ st::msgServiceGiftBoxTopSkip\n\t\t- st::msgServiceMargin.bottom();\n\tresize(width, height);\n}\n\nvoid PreviewWrap::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\n\tconst auto clip = e->rect();\n\tif (!clip.isEmpty()) {\n\t\tp.setClipRect(clip);\n\t\tWindow::SectionWidget::PaintBackground(\n\t\t\tp,\n\t\t\t_theme.get(),\n\t\t\tQSize(width(), window()->height()),\n\t\t\tclip);\n\t}\n\n\tauto context = _theme->preparePaintContext(\n\t\t_style.get(),\n\t\trect(),\n\t\trect(),\n\t\te->rect(),\n\t\t!window()->isActiveWindow());\n\tp.translate(_position);\n\t_item->draw(p, context);\n}\n\n} // namespace\n\nvoid PreparedPreviewBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<HistoryItem*> item,\n\t\trpl::producer<not_null<Data::Thread*>> recipient,\n\t\tFn<void()> choose,\n\t\tFn<void(not_null<Data::Thread*>)> send,\n\t\tFn<void()> sentTop) {\n\tbox->addTopButton(st::historySend.inner, sentTop);\n\tbox->setTitle(tr::lng_bot_share_prepared_title());\n\tconst auto container = box->verticalLayout();\n\tcontainer->add(object_ptr<PreviewWrap>(container, item));\n\tconst auto bot = item->viaBot();\n\tconst auto name = bot ? bot->name() : u\"Bot\"_q;\n\tconst auto info = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::DividerLabel>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tcontainer,\n\t\t\t\t\ttr::lng_bot_share_prepared_about(lt_bot, rpl::single(name)),\n\t\t\t\t\tst::boxDividerLabel),\n\t\t\t\tst::defaultBoxDividerLabelPadding)));\n\tconst auto row = container->add(object_ptr<Ui::VerticalLayout>(\n\t\tcontainer));\n\n\tconst auto reset = [=] {\n\t\tinfo->show(anim::type::instant);\n\t\twhile (row->count()) {\n\t\t\tdelete row->widgetAt(0);\n\t\t}\n\t\tbox->addButton(tr::lng_bot_share_prepared_button(), choose);\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t};\n\treset();\n\n\tconst auto lifetime = box->lifetime().make_state<rpl::lifetime>();\n\tstd::move(\n\t\trecipient\n\t) | rpl::on_next([=](not_null<Data::Thread*> thread) {\n\t\tinfo->hide(anim::type::instant);\n\t\twhile (row->count()) {\n\t\t\tdelete row->widgetAt(0);\n\t\t}\n\t\tAddSkip(row);\n\t\tAddSinglePeerRow(row, thread, nullptr, choose);\n\t\tif (const auto topic = thread->asTopic()) {\n\t\t\t*lifetime = topic->destroyed() | rpl::on_next(reset);\n\t\t} else {\n\t\t\t*lifetime = rpl::lifetime();\n\t\t}\n\t\trow->resizeToWidth(container->width());\n\t\tbox->clearButtons();\n\t\tbox->addButton(tr::lng_send_button(), [=] { send(thread); });\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t}, info->lifetime());\n\n\titem->history()->owner().itemRemoved(\n\t) | rpl::on_next([=](not_null<const HistoryItem*> removed) {\n\t\tif (removed == item) {\n\t\t\tbox->closeBox();\n\t\t}\n\t}, box->lifetime());\n}\n\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_bot_confirm_prepared.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nclass Thread;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nnamespace InlineBots {\n\nvoid PreparedPreviewBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<HistoryItem*> item,\n\trpl::producer<not_null<Data::Thread*>> recipient,\n\tFn<void()> choose,\n\tFn<void(not_null<Data::Thread*>)> sent,\n\tFn<void()> sentTop);\n\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_bot_downloads.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"inline_bots/inline_bot_downloads.h\"\n\n#include \"core/file_utilities.h\"\n#include \"data/data_document.h\"\n#include \"data/data_peer_id.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"storage/file_download_web.h\"\n#include \"storage/serialize_common.h\"\n#include \"storage/storage_account.h\"\n#include \"ui/chat/attach/attach_bot_downloads.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/labels.h\"\n#include \"styles/style_chat.h\"\n\n#include <QtCore/QBuffer>\n#include <QtCore/QDataStream>\n\n#include \"base/call_delayed.h\"\n\nnamespace InlineBots {\nnamespace {\n\nconstexpr auto kDownloadsVersion = 1;\nconstexpr auto kMaxDownloadsBots = 4096;\nconstexpr auto kMaxDownloadsPerBot = 16384;\n\n} // namespace\n\nDownloads::Downloads(not_null<Main::Session*> session)\n: _session(session) {\n}\n\nDownloads::~Downloads() {\n\tbase::take(_loaders);\n\tbase::take(_lists);\n}\n\nDownloadId Downloads::start(StartArgs &&args) {\n\tread();\n\n\tconst auto botId = args.bot->id;\n\tconst auto id = ++_autoIncrementId;\n\tauto &list = _lists[botId].list;\n\tlist.push_back({\n\t\t.id = id,\n\t\t.url = std::move(args.url),\n\t\t.path = std::move(args.path),\n\t});\n\tload(botId, id, list.back());\n\treturn id;\n}\n\nvoid Downloads::load(\n\t\tPeerId botId,\n\t\tDownloadId id,\n\t\tDownloadsEntry &entry) {\n\tentry.loading = 1;\n\tentry.failed = 0;\n\n\tauto &loader = _loaders[id];\n\tAssert(!loader.loader);\n\tloader.botId = botId;\n\tloader.loader = std::make_unique<webFileLoader>(\n\t\t_session,\n\t\tentry.url,\n\t\tentry.path,\n\t\tWebRequestType::FullLoad);\n\n\tapplyProgress(botId, id, 0, 0);\n\n\tloader.loader->updates(\n\t) | rpl::on_next_error_done([=] {\n\t\tprogress(botId, id);\n\t}, [=](FileLoader::Error) {\n\t\tfail(botId, id);\n\t}, [=] {\n\t\tdone(botId, id);\n\t}, loader.loader->lifetime());\n\n\tloader.loader->start();\n}\n\nvoid Downloads::progress(PeerId botId, DownloadId id) {\n\tconst auto i = _loaders.find(id);\n\tif (i == end(_loaders)) {\n\t\treturn;\n\t}\n\tconst auto &loader = i->second.loader;\n\tconst auto total = loader->fullSize();\n\tconst auto ready = loader->currentOffset();\n\n\tauto &list = _lists[botId].list;\n\tconst auto j = ranges::find(\n\t\tlist,\n\t\tid,\n\t\t&DownloadsEntry::id);\n\tAssert(j != end(list));\n\n\tif (total < 0 || ready > total) {\n\t\tfail(botId, id);\n\t\treturn;\n\t} else if (ready == total) {\n\t\t// Wait for 'done' signal.\n\t\treturn;\n\t}\n\n\tapplyProgress(botId, id, total, ready);\n}\n\nvoid Downloads::fail(PeerId botId, DownloadId id, bool cancel) {\n\tconst auto i = _loaders.find(id);\n\tif (i == end(_loaders)) {\n\t\treturn;\n\t}\n\tauto loader = std::move(i->second.loader);\n\t_loaders.erase(i);\n\tloader = nullptr;\n\n\tauto &list = _lists[botId].list;\n\tconst auto k = ranges::find(\n\t\tlist,\n\t\tid,\n\t\t&DownloadsEntry::id);\n\tAssert(k != end(list));\n\tk->loading = 0;\n\tk->failed = 1;\n\n\tif (cancel) {\n\t\tauto copy = *k;\n\t\tlist.erase(k);\n\t\tapplyProgress(botId, copy, 0, 0);\n\t} else {\n\t\tapplyProgress(botId, *k, 0, 0);\n\t}\n}\n\nvoid Downloads::done(PeerId botId, DownloadId id) {\n\tconst auto i = _loaders.find(id);\n\tif (i == end(_loaders)) {\n\t\treturn;\n\t}\n\tconst auto total = i->second.loader->fullSize();\n\tif (total <= 0) {\n\t\tfail(botId, id);\n\t\treturn;\n\t}\n\t_loaders.erase(i);\n\n\tauto &list = _lists[botId].list;\n\tconst auto j = ranges::find(\n\t\tlist,\n\t\tid,\n\t\t&DownloadsEntry::id);\n\tAssert(j != end(list));\n\tj->loading = 0;\n\n\tapplyProgress(botId, id, total, total);\n}\n\nvoid Downloads::applyProgress(\n\t\tPeerId botId,\n\t\tDownloadId id,\n\t\tint64 total,\n\t\tint64 ready) {\n\tExpects(total >= 0);\n\tExpects(ready >= 0 && ready <= total);\n\n\tauto &list = _lists[botId].list;\n\tconst auto j = ranges::find(\n\t\tlist,\n\t\tid,\n\t\t&DownloadsEntry::id);\n\tAssert(j != end(list));\n\n\tapplyProgress(botId, *j, total, ready);\n}\n\nvoid Downloads::applyProgress(\n\t\tPeerId botId,\n\t\tDownloadsEntry &entry,\n\t\tint64 total,\n\t\tint64 ready) {\n\tauto &progress = _progressView[botId];\n\tauto current = progress.current();\n\tauto subtract = int64(0);\n\tif (current.ready == current.total) {\n\t\tsubtract = current.ready;\n\t}\n\tif (entry.total != total) {\n\t\tconst auto delta = total - entry.total;\n\t\tentry.total = total;\n\t\tcurrent.total += delta;\n\t}\n\tif (entry.ready != ready) {\n\t\tconst auto delta = ready - entry.ready;\n\t\tentry.ready = ready;\n\t\tcurrent.ready += delta;\n\t}\n\tif (subtract > 0\n\t\t&& current.ready >= subtract\n\t\t&& current.total >= subtract) {\n\t\tcurrent.ready -= subtract;\n\t\tcurrent.total -= subtract;\n\t}\n\tif (entry.loading || current.ready < current.total) {\n\t\tcurrent.loading = 1;\n\t} else {\n\t\tcurrent.loading = 0;\n\t}\n\n\tif (total > 0 && total == ready) {\n\t\twrite();\n\t}\n\n\tprogress = current;\n}\n\nvoid Downloads::action(\n\t\tnot_null<UserData*> bot,\n\t\tDownloadId id,\n\t\tDownloadsAction type) {\n\tswitch (type) {\n\tcase DownloadsAction::Open: {\n\t\tconst auto i = ranges::find(\n\t\t\t_lists[bot->id].list,\n\t\t\tid,\n\t\t\t&DownloadsEntry::id);\n\t\tif (i == end(_lists[bot->id].list)) {\n\t\t\treturn;\n\t\t}\n\t\tFile::ShowInFolder(i->path);\n\t} break;\n\tcase DownloadsAction::Cancel: {\n\t\tconst auto i = _loaders.find(id);\n\t\tif (i == end(_loaders)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto botId = i->second.botId;\n\t\tfail(botId, id, true);\n\t} break;\n\tcase DownloadsAction::Retry: {\n\t\tconst auto i = ranges::find(\n\t\t\t_lists[bot->id].list,\n\t\t\tid,\n\t\t\t&DownloadsEntry::id);\n\t\tif (i == end(_lists[bot->id].list)) {\n\t\t\treturn;\n\t\t}\n\t\tload(bot->id, id, *i);\n\t} break;\n\t}\n}\n\n[[nodiscard]] auto Downloads::progress(not_null<UserData*> bot)\n->rpl::producer<DownloadsProgress> {\n\tread();\n\n\treturn _progressView[bot->id].value();\n}\n\nconst std::vector<DownloadsEntry> &Downloads::list(\n\t\tnot_null<UserData*> bot,\n\t\tbool forceCheck) {\n\tread();\n\n\tauto &entry = _lists[bot->id];\n\tif (forceCheck) {\n\t\tconst auto was = int(entry.list.size());\n\t\tfor (auto i = begin(entry.list); i != end(entry.list);) {\n\t\t\tif (i->loading || i->failed) {\n\t\t\t\t++i;\n\t\t\t} else if (auto info = QFileInfo(i->path)\n\t\t\t\t; !info.exists() || info.size() != i->total) {\n\t\t\t\ti = entry.list.erase(i);\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t\tif (int(entry.list.size()) != was) {\n\t\t\twrite();\n\t\t}\n\t}\n\treturn entry.list;\n}\n\nvoid Downloads::read() {\n\tauto bytes = _session->local().readInlineBotsDownloads();\n\tif (bytes.isEmpty()) {\n\t\treturn;\n\t}\n\n\tAssert(_lists.empty());\n\n\tauto stream = QDataStream(&bytes, QIODevice::ReadOnly);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\n\tquint32 version = 0, count = 0;\n\tstream >> version;\n\tif (version != kDownloadsVersion) {\n\t\treturn;\n\t}\n\tstream >> count;\n\tif (!count || count > kMaxDownloadsBots) {\n\t\treturn;\n\t}\n\tauto lists = base::flat_map<PeerId, List>();\n\tfor (auto i = 0; i != count; ++i) {\n\t\tquint64 rawBotId = 0;\n\t\tquint32 count = 0;\n\t\tstream >> rawBotId >> count;\n\t\tconst auto botId = DeserializePeerId(rawBotId);\n\t\tif (!botId\n\t\t\t|| !peerIsUser(botId)\n\t\t\t|| count > kMaxDownloadsPerBot\n\t\t\t|| lists.contains(botId)) {\n\t\t\treturn;\n\t\t}\n\t\tauto &list = lists[botId];\n\t\tlist.list.reserve(count);\n\t\tfor (auto j = 0; j != count; ++j) {\n\t\t\tauto entry = DownloadsEntry();\n\t\t\tauto size = int64();\n\t\t\tstream >> entry.url >> entry.path >> size;\n\t\t\tentry.total = entry.ready = size;\n\t\t\tentry.id = ++_autoIncrementId;\n\t\t\tlist.list.push_back(std::move(entry));\n\t\t}\n\t}\n\t_lists = std::move(lists);\n}\n\nvoid Downloads::write() {\n\tauto size = sizeof(quint32) // version\n\t\t+ sizeof(quint32); // lists count\n\n\tfor (const auto &[botId, list] : _lists) {\n\t\tsize += sizeof(quint64) // botId\n\t\t\t+ sizeof(quint32); // list count\n\t\tfor (const auto &entry : list.list) {\n\t\t\tif (entry.total > 0 && entry.ready == entry.total) {\n\t\t\t\tsize += Serialize::stringSize(entry.url)\n\t\t\t\t\t+ Serialize::stringSize(entry.path)\n\t\t\t\t\t+ sizeof(quint64); // size\n\t\t\t}\n\t\t}\n\t}\n\n\tauto bytes = QByteArray();\n\tbytes.reserve(size);\n\tauto buffer = QBuffer(&bytes);\n\tbuffer.open(QIODevice::WriteOnly);\n\tauto stream = QDataStream(&buffer);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\n\tstream << quint32(kDownloadsVersion) << quint32(_lists.size());\n\n\tfor (const auto &[botId, list] : _lists) {\n\t\tstream << SerializePeerId(botId) << quint32(list.list.size());\n\t\tfor (const auto &entry : list.list) {\n\t\t\tif (entry.total > 0 && entry.ready == entry.total) {\n\t\t\t\tstream << entry.url << entry.path << entry.total;\n\t\t\t}\n\t\t}\n\t}\n\tbuffer.close();\n\n\t_session->local().writeInlineBotsDownloads(bytes);\n}\n\nvoid DownloadFileBox(not_null<Ui::GenericBox*> box, DownloadBoxArgs args) {\n\tExpects(!args.name.isEmpty());\n\n\tbox->setTitle(tr::lng_bot_download_file());\n\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\tbox,\n\t\ttr::lng_bot_download_file_sure(\n\t\t\tlt_bot,\n\t\t\trpl::single(tr::bold(args.bot)),\n\t\t\ttr::rich),\n\t\tst::botDownloadLabel));\n\t//box->addRow(MakeFilePreview(box, args));\n\tconst auto done = std::move(args.done);\n\tconst auto name = args.name;\n\tconst auto session = args.session;\n\tconst auto chosen = std::make_shared<bool>();\n\tbox->addButton(tr::lng_bot_download_file_button(), [=] {\n\t\tconst auto path = FileNameForSave(\n\t\t\tsession,\n\t\t\ttr::lng_save_file(tr::now),\n\t\t\tQString(),\n\t\t\tu\"file\"_q,\n\t\t\tname,\n\t\t\tfalse,\n\t\t\tQDir());\n\t\tif (!path.isEmpty()) {\n\t\t\t*chosen = true;\n\t\t\tbox->closeBox();\n\t\t\tdone(path);\n\t\t}\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n\tbox->boxClosing() | rpl::on_next([=] {\n\t\tif (!*chosen) {\n\t\t\tdone(QString());\n\t\t}\n\t}, box->lifetime());\n}\n\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_bot_downloads.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/chat/attach/attach_bot_webview.h\"\n\nclass webFileLoader;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nnamespace InlineBots {\n\nusing DownloadId = uint32;\n\nusing ::Ui::BotWebView::DownloadsProgress;\nusing ::Ui::BotWebView::DownloadsEntry;\nusing ::Ui::BotWebView::DownloadsAction;\n\nclass Downloads final {\npublic:\n\texplicit Downloads(not_null<Main::Session*> session);\n\t~Downloads();\n\n\tstruct StartArgs {\n\t\tnot_null<UserData*> bot;\n\t\tQString url;\n\t\tQString path;\n\t};\n\tuint32 start(StartArgs &&args); // Returns download id.\n\n\tvoid action(\n\t\tnot_null<UserData*> bot,\n\t\tDownloadId id,\n\t\tDownloadsAction type);\n\n\t[[nodiscard]] rpl::producer<DownloadsProgress> progress(\n\t\tnot_null<UserData*> bot);\n\t[[nodiscard]] const std::vector<DownloadsEntry> &list(\n\t\tnot_null<UserData*> bot,\n\t\tbool check = false);\n\nprivate:\n\tstruct List {\n\t\tstd::vector<DownloadsEntry> list;\n\t};\n\tstruct Loader {\n\t\tstd::unique_ptr<webFileLoader> loader;\n\t\tPeerId botId = 0;\n\t};\n\n\tvoid read();\n\tvoid write();\n\n\tvoid load(\n\t\tPeerId botId,\n\t\tDownloadId id,\n\t\tDownloadsEntry &entry);\n\tvoid progress(PeerId botId, DownloadId id);\n\tvoid fail(PeerId botId, DownloadId id, bool cancel = false);\n\tvoid done(PeerId botId, DownloadId id);\n\tvoid applyProgress(\n\t\tPeerId botId,\n\t\tDownloadId id,\n\t\tint64 total,\n\t\tint64 ready);\n\tvoid applyProgress(\n\t\tPeerId botId,\n\t\tDownloadsEntry &entry,\n\t\tint64 total,\n\t\tint64 ready);\n\n\tconst not_null<Main::Session*> _session;\n\n\tbase::flat_map<PeerId, List> _lists;\n\tbase::flat_map<DownloadId, Loader> _loaders;\n\n\tbase::flat_map<\n\t\tPeerId,\n\t\trpl::variable<DownloadsProgress>> _progressView;\n\n\tDownloadId _autoIncrementId = 0;\n\n};\n\nstruct DownloadBoxArgs {\n\tnot_null<Main::Session*> session;\n\tQString bot;\n\tQString name;\n\tQString url;\n\tFn<void(QString)> done;\n};\nvoid DownloadFileBox(not_null<Ui::GenericBox*> box, DownloadBoxArgs args);\n\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"inline_bots/inline_bot_layout_internal.h\"\n\n#include \"data/data_photo.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_document_media.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"chat_helpers/gifs_list_widget.h\" // ChatHelpers::AddGifAction.\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/clip/media_clip_reader.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"history/history_location_manager.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/media/history_view_document.h\" // DrawThumbnailAsSongCover\n#include \"history/view/media/history_view_media_common.h\"\n#include \"ui/image/image.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/painter.h\"\n#include \"main/main_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_overview.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace InlineBots {\nnamespace Layout {\nnamespace internal {\n\nusing TextState = HistoryView::TextState;\n\nconstexpr auto kMaxInlineArea = 1280 * 720;\n\n[[nodiscard]] QSize ScaleDown(int w, int h, int maxW, int maxH) {\n\tif (w * maxH > h * maxW) {\n\t\tif (maxH < h) {\n\t\t\tw = w * maxH / h;\n\t\t\th = maxH;\n\t\t}\n\t} else {\n\t\tif (maxW < w) {\n\t\t\th = h * maxW / w;\n\t\t\tw = maxW;\n\t\t}\n\t}\n\treturn { w, h };\n}\n\n[[nodiscard]] bool CanPlayInline(not_null<DocumentData*> document) {\n\tconst auto dimensions = document->dimensions;\n\treturn dimensions.width() * dimensions.height() <= kMaxInlineArea;\n}\n\nFileBase::FileBase(not_null<Context*> context, std::shared_ptr<Result> result)\n: ItemBase(context, std::move(result)) {\n}\n\nFileBase::FileBase(\n\tnot_null<Context*> context,\n\tnot_null<DocumentData*> document)\n: ItemBase(context, document) {\n}\n\nDocumentData *FileBase::getShownDocument() const {\n\tif (const auto result = getDocument()) {\n\t\treturn result;\n\t}\n\treturn getResultDocument();\n}\n\nint FileBase::content_width() const {\n\tif (const auto document = getShownDocument()) {\n\t\tif (document->dimensions.width() > 0) {\n\t\t\treturn document->dimensions.width();\n\t\t}\n\t\treturn style::ConvertScale(document->thumbnailLocation().width());\n\t}\n\treturn 0;\n}\n\nint FileBase::content_height() const {\n\tif (const auto document = getShownDocument()) {\n\t\tif (document->dimensions.height() > 0) {\n\t\t\treturn document->dimensions.height();\n\t\t}\n\t\treturn style::ConvertScale(document->thumbnailLocation().height());\n\t}\n\treturn 0;\n}\n\nint FileBase::content_duration() const {\n\tif (const auto document = getShownDocument()) {\n\t\tif (document->hasDuration()) {\n\t\t\treturn document->duration() / 1000;\n\t\t}\n\t}\n\treturn getResultDuration();\n}\n\nGif::Gif(not_null<Context*> context, std::shared_ptr<Result> result)\n: FileBase(context, std::move(result)) {\n\tExpects(getResultDocument() != nullptr);\n}\n\nGif::Gif(\n\tnot_null<Context*> context,\n\tnot_null<DocumentData*> document,\n\tbool hasDeleteButton)\n: FileBase(context, document) {\n\tif (hasDeleteButton) {\n\t\t_delete = std::make_shared<DeleteSavedGifClickHandler>(document);\n\t}\n}\n\nvoid Gif::initDimensions() {\n\tint32 w = content_width(), h = content_height();\n\tif (w <= 0 || h <= 0) {\n\t\t_maxw = 0;\n\t} else {\n\t\tw = w * st::inlineMediaHeight / h;\n\t\t_maxw = qMax(w, int32(st::inlineResultsMinWidth));\n\t}\n\t_minh = st::inlineMediaHeight + st::inlineResultsSkip;\n}\n\nvoid Gif::setPosition(int32 position) {\n\tAbstractLayoutItem::setPosition(position);\n\tif (_position < 0) {\n\t\t_gif.reset();\n\t}\n}\n\nvoid DeleteSavedGifClickHandler::onClickImpl() const {\n\tChatHelpers::AddGifAction(\n\t\t[](QString, Fn<void()> &&done, const style::icon*) { done(); },\n\t\tnullptr,\n\t\t_data);\n}\n\nint Gif::resizeGetHeight(int width) {\n\t_width = width;\n\t_height = _minh;\n\treturn _height;\n}\n\nQRect Gif::innerContentRect() const {\n\tensureDataMediaCreated(getShownDocument());\n\n\tconst auto size = (!_thumb.isNull())\n\t\t? (_thumb.size() / style::DevicePixelRatio())\n\t\t: countFrameSize();\n\treturn QRect(QPoint(), size);\n}\n\nvoid Gif::paint(Painter &p, const QRect &clip, const PaintContext *context) const {\n\tconst auto document = getShownDocument();\n\tensureDataMediaCreated(document);\n\tconst auto preview = Data::VideoPreviewState(_dataMedia.get());\n\tpreview.automaticLoad(fileOrigin());\n\n\tconst auto displayLoading = !preview.usingThumbnail()\n\t\t&& document->displayLoading();\n\tconst auto loaded = preview.loaded();\n\tconst auto loading = preview.loading();\n\tif (loaded\n\t\t&& !_gif\n\t\t&& !_gif.isBad()\n\t\t&& CanPlayInline(document)) {\n\t\tauto that = const_cast<Gif*>(this);\n\t\tthat->_gif = preview.makeAnimation([=](\n\t\t\t\tMedia::Clip::Notification notification) {\n\t\t\tthat->clipCallback(notification);\n\t\t});\n\t}\n\n\tconst auto animating = (_gif && _gif->started());\n\tif (displayLoading) {\n\t\tensureAnimation();\n\t\tif (!_animation->radial.animating()) {\n\t\t\t_animation->radial.start(_dataMedia->progress());\n\t\t}\n\t}\n\tconst auto radial = isRadialAnimation();\n\n\tconst auto frame = countFrameSize();\n\tconst auto r = QRect(0, 0, _width, st::inlineMediaHeight);\n\tif (animating) {\n\t\tconst auto pixmap = _gif->current({\n\t\t\t.frame = frame,\n\t\t\t.outer = r.size(),\n\t\t}, context->paused ? 0 : context->ms);\n\t\tif (_thumb.isNull()) {\n\t\t\t_thumb = pixmap;\n\t\t\t_thumbGood = true;\n\t\t}\n\t\tp.drawImage(r.topLeft(), pixmap);\n\t} else {\n\t\tprepareThumbnail(r.size(), frame);\n\t\tif (_thumb.isNull()) {\n\t\t\tp.fillRect(r, st::overviewPhotoBg);\n\t\t} else {\n\t\t\tp.drawImage(r.topLeft(), _thumb);\n\t\t}\n\t}\n\n\tif (radial\n\t\t|| _gif.isBad()\n\t\t|| (!_gif && !loaded && !loading && !preview.usingThumbnail())) {\n\t\tauto radialOpacity = (radial && loaded) ? _animation->radial.opacity() : 1.;\n\t\tif (_animation && _animation->_a_over.animating()) {\n\t\t\tauto over = _animation->_a_over.value(1.);\n\t\t\tp.fillRect(r, anim::brush(st::msgDateImgBg, st::msgDateImgBgOver, over));\n\t\t} else {\n\t\t\tauto over = (_state & StateFlag::Over);\n\t\t\tp.fillRect(r, over ? st::msgDateImgBgOver : st::msgDateImgBg);\n\t\t}\n\t\tp.setOpacity(radialOpacity * p.opacity());\n\n\t\tp.setOpacity(radialOpacity);\n\t\tauto icon = [&] {\n\t\t\tif (radial || loading) {\n\t\t\t\treturn &st::historyFileInCancel;\n\t\t\t} else if (loaded) {\n\t\t\t\treturn &st::historyFileInPlay;\n\t\t\t}\n\t\t\treturn &st::historyFileInDownload;\n\t\t}();\n\t\tconst auto size = st::inlineRadialSize;\n\t\tQRect inner(\n\t\t\t(r.width() - size) / 2,\n\t\t\t(r.height() - size) / 2,\n\t\t\tsize,\n\t\t\tsize);\n\t\ticon->paintInCenter(p, inner);\n\t\tif (radial) {\n\t\t\tp.setOpacity(1);\n\t\t\tQRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));\n\t\t\t_animation->radial.draw(p, rinner, st::msgFileRadialLine, st::historyFileThumbRadialFg);\n\t\t}\n\t}\n\n\tif (_delete && (_state & StateFlag::Over)) {\n\t\tauto deleteSelected = (_state & StateFlag::DeleteOver);\n\t\tauto deletePos = QPoint(_width - st::stickerPanDeleteIconBg.width(), 0);\n\t\tp.setOpacity(deleteSelected ? st::stickerPanDeleteOpacityBgOver : st::stickerPanDeleteOpacityBg);\n\t\tst::stickerPanDeleteIconBg.paint(p, deletePos, width());\n\t\tp.setOpacity(deleteSelected ? st::stickerPanDeleteOpacityFgOver : st::stickerPanDeleteOpacityFg);\n\t\tst::stickerPanDeleteIconFg.paint(p, deletePos, width());\n\t\tp.setOpacity(1.);\n\t}\n}\n\nTextState Gif::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tif (QRect(0, 0, _width, st::inlineMediaHeight).contains(point)) {\n\t\tif (_delete && style::rtlpoint(point, _width).x() >= _width - st::stickerPanDeleteIconBg.width() && point.y() < st::stickerPanDeleteIconBg.height()) {\n\t\t\treturn { nullptr, _delete };\n\t\t} else {\n\t\t\treturn { nullptr, _send };\n\t\t}\n\t}\n\treturn {};\n}\n\nvoid Gif::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {\n\tif (!p) return;\n\n\tif (_delete && p == _delete) {\n\t\tbool wasactive = (_state & StateFlag::DeleteOver);\n\t\tif (active != wasactive) {\n\t\t\tauto from = active ? 0. : 1., to = active ? 1. : 0.;\n\t\t\t_a_deleteOver.start([this] { update(); }, from, to, st::stickersRowDuration);\n\t\t\tif (active) {\n\t\t\t\t_state |= StateFlag::DeleteOver;\n\t\t\t} else {\n\t\t\t\t_state &= ~StateFlag::DeleteOver;\n\t\t\t}\n\t\t}\n\t}\n\tif (p == _delete || p == _send) {\n\t\tbool wasactive = (_state & StateFlag::Over);\n\t\tif (active != wasactive) {\n\t\t\tensureDataMediaCreated(getShownDocument());\n\t\t\tconst auto preview = Data::VideoPreviewState(_dataMedia.get());\n\t\t\tif (!preview.usingThumbnail() && !preview.loaded()) {\n\t\t\t\tensureAnimation();\n\t\t\t\tauto from = active ? 0. : 1., to = active ? 1. : 0.;\n\t\t\t\t_animation->_a_over.start([=] { update(); }, from, to, st::stickersRowDuration);\n\t\t\t}\n\t\t\tif (active) {\n\t\t\t\t_state |= StateFlag::Over;\n\t\t\t} else {\n\t\t\t\t_state &= ~StateFlag::Over;\n\t\t\t}\n\t\t}\n\t}\n\tItemBase::clickHandlerActiveChanged(p, active);\n}\n\nQSize Gif::countFrameSize() const {\n\tbool animating = (_gif && _gif->ready());\n\tint32 framew = animating ? _gif->width() : content_width(), frameh = animating ? _gif->height() : content_height(), height = st::inlineMediaHeight;\n\tif (framew * height > frameh * _width) {\n\t\tif (framew < st::maxStickerSize || frameh > height) {\n\t\t\tif (frameh > height || (framew * height / frameh) <= st::maxStickerSize) {\n\t\t\t\tframew = framew * height / frameh;\n\t\t\t\tframeh = height;\n\t\t\t} else {\n\t\t\t\tframeh = int32(frameh * st::maxStickerSize) / framew;\n\t\t\t\tframew = st::maxStickerSize;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (frameh < st::maxStickerSize || framew > _width) {\n\t\t\tif (framew > _width || (frameh * _width / framew) <= st::maxStickerSize) {\n\t\t\t\tframeh = frameh * _width / framew;\n\t\t\t\tframew = _width;\n\t\t\t} else {\n\t\t\t\tframew = int32(framew * st::maxStickerSize) / frameh;\n\t\t\t\tframeh = st::maxStickerSize;\n\t\t\t}\n\t\t}\n\t}\n\treturn QSize(framew, frameh);\n}\n\nvoid Gif::validateThumbnail(\n\t\tImage *image,\n\t\tQSize size,\n\t\tQSize frame,\n\t\tbool good) const {\n\tif (!image || (_thumbGood && !good)) {\n\t\treturn;\n\t} else if ((_thumb.size() == size * style::DevicePixelRatio())\n\t\t&& (_thumbGood || !good)) {\n\t\treturn;\n\t}\n\t_thumbGood = good;\n\t_thumb = image->pixNoCache(\n\t\tframe * style::DevicePixelRatio(),\n\t\t{\n\t\t\t.options = (Images::Option::TransparentBackground\n\t\t\t\t| (good ? Images::Option() : Images::Option::Blur)),\n\t\t\t.outer = size,\n\t\t}).toImage();\n}\n\nvoid Gif::prepareThumbnail(QSize size, QSize frame) const {\n\tconst auto document = getShownDocument();\n\tAssert(document != nullptr);\n\n\tensureDataMediaCreated(document);\n\tvalidateThumbnail(_dataMedia->thumbnail(), size, frame, true);\n\tvalidateThumbnail(_dataMedia->thumbnailInline(), size, frame, false);\n}\n\nvoid Gif::ensureDataMediaCreated(not_null<DocumentData*> document) const {\n\tif (_dataMedia) {\n\t\treturn;\n\t}\n\t_dataMedia = document->createMediaView();\n\t_dataMedia->thumbnailWanted(fileOrigin());\n\t_dataMedia->videoThumbnailWanted(fileOrigin());\n}\n\nvoid Gif::ensureAnimation() const {\n\tif (!_animation) {\n\t\t_animation = std::make_unique<AnimationData>([=](crl::time now) {\n\t\t\tradialAnimationCallback(now);\n\t\t});\n\t}\n}\n\nbool Gif::isRadialAnimation() const {\n\tif (_animation) {\n\t\tif (_animation->radial.animating()) {\n\t\t\treturn true;\n\t\t} else {\n\t\t\tensureDataMediaCreated(getShownDocument());\n\t\t\tconst auto preview = Data::VideoPreviewState(_dataMedia.get());\n\t\t\tif (preview.usingThumbnail() || preview.loaded()) {\n\t\t\t\t_animation = nullptr;\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Gif::radialAnimationCallback(crl::time now) const {\n\tconst auto document = getShownDocument();\n\tensureDataMediaCreated(document);\n\tconst auto updated = [&] {\n\t\treturn _animation->radial.update(\n\t\t\t_dataMedia->progress(),\n\t\t\t!document->loading() || _dataMedia->loaded(),\n\t\t\tnow);\n\t}();\n\tif (!anim::Disabled() || updated) {\n\t\tupdate();\n\t}\n\tif (!_animation->radial.animating() && _dataMedia->loaded()) {\n\t\t_animation = nullptr;\n\t}\n}\n\nvoid Gif::unloadHeavyPart() {\n\t_gif.reset();\n\t_dataMedia = nullptr;\n}\n\nvoid Gif::clipCallback(Media::Clip::Notification notification) {\n\tusing namespace Media::Clip;\n\tswitch (notification) {\n\tcase Notification::Reinit: {\n\t\tif (_gif) {\n\t\t\tif (_gif->state() == State::Error) {\n\t\t\t\t_gif.setBad();\n\t\t\t} else if (_gif->ready() && !_gif->started()) {\n\t\t\t\tif (_gif->width() * _gif->height() > kMaxInlineArea) {\n\t\t\t\t\tgetShownDocument()->dimensions = QSize(\n\t\t\t\t\t\t_gif->width(),\n\t\t\t\t\t\t_gif->height());\n\t\t\t\t\t_gif.reset();\n\t\t\t\t} else {\n\t\t\t\t\t_gif->start({\n\t\t\t\t\t\t.frame = countFrameSize(),\n\t\t\t\t\t\t.outer = { _width, st::inlineMediaHeight },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else if (_gif->autoPausedGif() && !context()->inlineItemVisible(this)) {\n\t\t\t\tunloadHeavyPart();\n\t\t\t}\n\t\t}\n\n\t\tupdate();\n\t} break;\n\n\tcase Notification::Repaint: {\n\t\tif (_gif && !_gif->currentDisplayed()) {\n\t\t\tupdate();\n\t\t}\n\t} break;\n\t}\n}\n\nSticker::Sticker(not_null<Context*> context, std::shared_ptr<Result> result)\n: FileBase(context, std::move(result)) {\n\tExpects(getResultDocument() != nullptr);\n}\n\nSticker::~Sticker() = default;\n\nvoid Sticker::initDimensions() {\n\t_maxw = st::stickerPanSize.width();\n\t_minh = st::stickerPanSize.height();\n}\n\nvoid Sticker::preload() const {\n\tconst auto document = getShownDocument();\n\tAssert(document != nullptr);\n\n\tensureDataMediaCreated(document);\n\t_dataMedia->checkStickerSmall();\n}\n\nvoid Sticker::ensureDataMediaCreated(not_null<DocumentData*> document) const {\n\tif (_dataMedia) {\n\t\treturn;\n\t}\n\t_dataMedia = document->createMediaView();\n}\n\nvoid Sticker::unloadHeavyPart() {\n\t_dataMedia = nullptr;\n\t_lifetime.destroy();\n\t_lottie = nullptr;\n\t_webm = nullptr;\n}\n\nQRect Sticker::innerContentRect() const {\n\tensureDataMediaCreated(getShownDocument());\n\n\tconst auto size = (_lottie && _lottie->ready())\n\t\t? (_lottie->frame().size() / style::DevicePixelRatio())\n\t\t: (!_thumb.isNull())\n\t\t? (_thumb.size() / style::DevicePixelRatio())\n\t\t: getThumbSize();\n\tconst auto pos = QPoint(\n\t\t(st::stickerPanSize.width() - size.width()) / 2,\n\t\t(st::stickerPanSize.height() - size.height()) / 2);\n\treturn QRect(pos, size);\n}\n\nvoid Sticker::paint(Painter &p, const QRect &clip, const PaintContext *context) const {\n\tensureDataMediaCreated(getShownDocument());\n\n\tauto over = _a_over.value(_active ? 1. : 0.);\n\tif (over > 0) {\n\t\tp.setOpacity(over);\n\t\tUi::FillRoundRect(p, QRect(QPoint(0, 0), st::stickerPanSize), st::emojiPanHover, Ui::StickerHoverCorners);\n\t\tp.setOpacity(1);\n\t}\n\n\tprepareThumbnail();\n\tif (_lottie && _lottie->ready()) {\n\t\tconst auto frame = _lottie->frame();\n\t\tconst auto size = frame.size() / style::DevicePixelRatio();\n\t\tconst auto pos = QPoint(\n\t\t\t(st::stickerPanSize.width() - size.width()) / 2,\n\t\t\t(st::stickerPanSize.height() - size.height()) / 2);\n\t\tp.drawImage(\n\t\t\tQRect(pos, size),\n\t\t\tframe);\n\t\tif (!context->paused) {\n\t\t\t_lottie->markFrameShown();\n\t\t}\n\t} else if (_webm && _webm->started()) {\n\t\tconst auto size = getThumbSize();\n\t\tconst auto frame = _webm->current({\n\t\t\t.frame = size,\n\t\t\t.keepAlpha = true,\n\t\t}, context->paused ? 0 : context->ms);\n\t\tp.drawImage(\n\t\t\t(st::stickerPanSize.width() - size.width()) / 2,\n\t\t\t(st::stickerPanSize.height() - size.width()) / 2,\n\t\t\tframe);\n\t} else if (!_thumb.isNull()) {\n\t\tconst auto w = _thumb.width() / style::DevicePixelRatio();\n\t\tconst auto h = _thumb.height() / style::DevicePixelRatio();\n\t\tconst auto pos = QPoint(\n\t\t\t(st::stickerPanSize.width() - w) / 2,\n\t\t\t(st::stickerPanSize.height() - h) / 2);\n\t\tp.drawPixmap(pos, _thumb);\n\t} else if (context->pathGradient) {\n\t\tconst auto thumbSize = getThumbSize();\n\t\tconst auto w = thumbSize.width();\n\t\tconst auto h = thumbSize.height();\n\t\tChatHelpers::PaintStickerThumbnailPath(\n\t\t\tp,\n\t\t\t_dataMedia.get(),\n\t\t\tQRect(\n\t\t\t\t(st::stickerPanSize.width() - w) / 2,\n\t\t\t\t(st::stickerPanSize.height() - h) / 2,\n\t\t\t\tw,\n\t\t\t\th),\n\t\t\tcontext->pathGradient);\n\t}\n}\n\nTextState Sticker::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tif (QRect(0, 0, _width, st::inlineMediaHeight).contains(point)) {\n\t\treturn { nullptr, _send };\n\t}\n\treturn {};\n}\n\nvoid Sticker::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {\n\tif (!p) return;\n\n\tif (p == _send) {\n\t\tif (active != _active) {\n\t\t\t_active = active;\n\n\t\t\tauto from = active ? 0. : 1., to = active ? 1. : 0.;\n\t\t\t_a_over.start([this] { update(); }, from, to, st::stickersRowDuration);\n\t\t}\n\t}\n\tItemBase::clickHandlerActiveChanged(p, active);\n}\n\nQSize Sticker::boundingBox() const {\n\tconst auto size = st::stickerPanSize.width() - st::roundRadiusSmall * 2;\n\treturn { size, size };\n}\n\nQSize Sticker::getThumbSize() const {\n\tconst auto width = qMax(content_width(), 1);\n\tconst auto height = qMax(content_height(), 1);\n\treturn HistoryView::DownscaledSize({ width, height }, boundingBox());\n}\n\nvoid Sticker::setupLottie() const {\n\tExpects(_dataMedia != nullptr);\n\n\t_lottie = ChatHelpers::LottiePlayerFromDocument(\n\t\t_dataMedia.get(),\n\t\tChatHelpers::StickerLottieSize::InlineResults,\n\t\tboundingBox() * style::DevicePixelRatio());\n\n\t_lottie->updates(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, _lifetime);\n}\n\nvoid Sticker::setupWebm() const {\n\tExpects(_dataMedia != nullptr);\n\n\tconst auto that = const_cast<Sticker*>(this);\n\tauto callback = [=](Media::Clip::Notification notification) {\n\t\tthat->clipCallback(notification);\n\t};\n\tthat->_webm = Media::Clip::MakeReader(\n\t\t_dataMedia->owner()->location(),\n\t\t_dataMedia->bytes(),\n\t\tstd::move(callback));\n}\n\nvoid Sticker::prepareThumbnail() const {\n\tconst auto document = getShownDocument();\n\tAssert(document != nullptr);\n\n\tensureDataMediaCreated(document);\n\tconst auto sticker = document->sticker();\n\tif (sticker && _dataMedia->loaded()) {\n\t\tif (!_lottie && sticker->isLottie()) {\n\t\t\tsetupLottie();\n\t\t} else if (!_webm && sticker->isWebm()) {\n\t\t\tsetupWebm();\n\t\t}\n\t}\n\t_dataMedia->checkStickerSmall();\n\tif (const auto image = _dataMedia->getStickerSmall()) {\n\t\tif (!_lottie && !_thumbLoaded) {\n\t\t\tconst auto thumbSize = getThumbSize();\n\t\t\t_thumb = image->pix(thumbSize);\n\t\t\t_thumbLoaded = true;\n\t\t}\n\t}\n}\n\nvoid Sticker::clipCallback(Media::Clip::Notification notification) {\n\tusing namespace Media::Clip;\n\tswitch (notification) {\n\tcase Notification::Reinit: {\n\t\tif (!_webm) {\n\t\t\tbreak;\n\t\t} else if (_webm->state() == State::Error) {\n\t\t\t_webm.setBad();\n\t\t} else if (_webm->ready() && !_webm->started()) {\n\t\t\t_webm->start({\n\t\t\t\t.frame = getThumbSize(),\n\t\t\t\t.keepAlpha = true,\n\t\t\t});\n\t\t} else if (_webm->autoPausedGif()\n\t\t\t&& !context()->inlineItemVisible(this)) {\n\t\t\tunloadHeavyPart();\n\t\t}\n\t} break;\n\n\tcase Notification::Repaint: break;\n\t}\n\tupdate();\n}\n\nPhoto::Photo(not_null<Context*> context, std::shared_ptr<Result> result)\n: ItemBase(context, std::move(result)) {\n\tExpects(getShownPhoto() != nullptr);\n}\n\nvoid Photo::initDimensions() {\n\tconst auto photo = getShownPhoto();\n\tint32 w = photo->width(), h = photo->height();\n\tif (w <= 0 || h <= 0) {\n\t\tw = h = 1;\n\t}\n\tw = w * st::inlineMediaHeight / h;\n\t_maxw = qMax(w, int32(st::inlineResultsMinWidth));\n\t_minh = st::inlineMediaHeight + st::inlineResultsSkip;\n}\n\nvoid Photo::paint(Painter &p, const QRect &clip, const PaintContext *context) const {\n\tint32 height = st::inlineMediaHeight;\n\tQSize frame = countFrameSize();\n\n\tQRect r(0, 0, _width, height);\n\n\tprepareThumbnail({ _width, height }, frame);\n\tif (_thumb.isNull()) {\n\t\tp.fillRect(r, st::overviewPhotoBg);\n\t} else {\n\t\tp.drawPixmap(r.topLeft(), _thumb);\n\t}\n}\n\nTextState Photo::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tif (QRect(0, 0, _width, st::inlineMediaHeight).contains(point)) {\n\t\treturn { nullptr, _send };\n\t}\n\treturn {};\n}\n\nvoid Photo::unloadHeavyPart() {\n\t_photoMedia = nullptr;\n}\n\nPhotoData *Photo::getShownPhoto() const {\n\tif (const auto result = getPhoto()) {\n\t\treturn result;\n\t}\n\treturn getResultPhoto();\n}\n\nQSize Photo::countFrameSize() const {\n\tconst auto photo = getShownPhoto();\n\tint32 framew = photo->width(), frameh = photo->height(), height = st::inlineMediaHeight;\n\tif ((framew <= 0 || frameh <= 0) && _photoMedia) {\n\t\tusing PhotoSize = Data::PhotoSize;\n\t\tif (const auto image = _photoMedia->image(PhotoSize::Thumbnail)) {\n\t\t\tframew = image->width();\n\t\t\tframeh = image->height();\n\t\t} else if (const auto image = _photoMedia->image(PhotoSize::Small)) {\n\t\t\tframew = image->width();\n\t\t\tframeh = image->height();\n\t\t} else if (const auto image = _photoMedia->thumbnailInline()) {\n\t\t\tframew = image->width();\n\t\t\tframeh = image->height();\n\t\t}\n\t}\n\tif (framew <= 0 || frameh <= 0) {\n\t\treturn { _width, height };\n\t}\n\tif (framew * height > frameh * _width) {\n\t\tif (framew < st::maxStickerSize || frameh > height) {\n\t\t\tif (frameh > height || (framew * height / frameh) <= st::maxStickerSize) {\n\t\t\t\tframew = framew * height / frameh;\n\t\t\t\tframeh = height;\n\t\t\t} else {\n\t\t\t\tframeh = int32(frameh * st::maxStickerSize) / framew;\n\t\t\t\tframew = st::maxStickerSize;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (frameh < st::maxStickerSize || framew > _width) {\n\t\t\tif (framew > _width || (frameh * _width / framew) <= st::maxStickerSize) {\n\t\t\t\tframeh = frameh * _width / framew;\n\t\t\t\tframew = _width;\n\t\t\t} else {\n\t\t\t\tframew = int32(framew * st::maxStickerSize) / frameh;\n\t\t\t\tframeh = st::maxStickerSize;\n\t\t\t}\n\t\t}\n\t}\n\treturn QSize(framew, frameh);\n}\n\nvoid Photo::validateThumbnail(\n\t\tImage *image,\n\t\tQSize size,\n\t\tQSize frame,\n\t\tbool good) const {\n\tif (!image || (_thumbGood && !good)) {\n\t\treturn;\n\t} else if ((_thumb.size() == size * style::DevicePixelRatio())\n\t\t&& (_thumbGood || !good)) {\n\t\treturn;\n\t}\n\tconst auto origin = fileOrigin();\n\t_thumb = image->pixNoCache(\n\t\tframe * style::DevicePixelRatio(),\n\t\t{\n\t\t\t.options = (Images::Option::TransparentBackground\n\t\t\t\t| (good ? Images::Option() : Images::Option::Blur)),\n\t\t\t.outer = size,\n\t\t});\n\t_thumbGood = good;\n}\n\nvoid Photo::prepareThumbnail(QSize size, QSize frame) const {\n\tusing PhotoSize = Data::PhotoSize;\n\n\tconst auto photo = getShownPhoto();\n\tAssert(photo != nullptr);\n\n\tif (!_photoMedia) {\n\t\t_photoMedia = photo->createMediaView();\n\t\t_photoMedia->wanted(PhotoSize::Thumbnail, fileOrigin());\n\t}\n\tvalidateThumbnail(_photoMedia->image(PhotoSize::Thumbnail), size, frame, true);\n\tvalidateThumbnail(_photoMedia->image(PhotoSize::Small), size, frame, false);\n\tvalidateThumbnail(_photoMedia->thumbnailInline(), size, frame, false);\n}\n\nVideo::Video(not_null<Context*> context, std::shared_ptr<Result> result)\n: FileBase(context, std::move(result))\n, _link(getResultPreviewHandler())\n, _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip)\n, _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) {\n\tif (int duration = content_duration()) {\n\t\t_duration = Ui::FormatDurationText(duration);\n\t\t_durationWidth = st::normalFont->width(_duration);\n\t}\n}\n\nbool Video::withThumbnail() const {\n\tif (const auto document = getShownDocument()) {\n\t\tif (document->hasThumbnail()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn hasResultThumb();\n}\n\nvoid Video::initDimensions() {\n\tconst auto withThumb = withThumbnail();\n\n\t_maxw = st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft;\n\tconst auto textWidth = _maxw - (st::inlineThumbSize + st::inlineThumbSkip);\n\tTextParseOptions titleOpts = { 0, textWidth, 2 * st::semiboldFont->height, Qt::LayoutDirectionAuto };\n\tauto title = TextUtilities::SingleLine(_result->getLayoutTitle());\n\tif (title.isEmpty()) {\n\t\ttitle = tr::lng_media_video(tr::now);\n\t}\n\t_title.setText(st::semiboldTextStyle, title, titleOpts);\n\tint32 titleHeight = qMin(_title.countHeight(textWidth), 2 * st::semiboldFont->height);\n\n\tint32 descriptionLines = withThumb ? (titleHeight > st::semiboldFont->height ? 1 : 2) : 3;\n\n\tTextParseOptions descriptionOpts = { TextParseMultiline, textWidth, descriptionLines * st::normalFont->height, Qt::LayoutDirectionAuto };\n\tQString description = _result->getLayoutDescription();\n\tif (description.isEmpty()) {\n\t\tdescription = _duration;\n\t}\n\t_description.setText(st::defaultTextStyle, description, descriptionOpts);\n\n\t_minh = st::inlineThumbSize;\n\t_minh += st::inlineRowMargin * 2 + st::inlineRowBorder;\n}\n\nvoid Video::paint(Painter &p, const QRect &clip, const PaintContext *context) const {\n\tint left = st::inlineThumbSize + st::inlineThumbSkip;\n\n\tconst auto withThumb = withThumbnail();\n\tif (withThumb) {\n\t\tprepareThumbnail({ st::inlineThumbSize, st::inlineThumbSize });\n\t\tif (_thumb.isNull()) {\n\t\t\tp.fillRect(style::rtlrect(0, st::inlineRowMargin, st::inlineThumbSize, st::inlineThumbSize, _width), st::overviewPhotoBg);\n\t\t} else {\n\t\t\tp.drawPixmapLeft(0, st::inlineRowMargin, _width, _thumb);\n\t\t}\n\t} else {\n\t\tp.fillRect(style::rtlrect(0, st::inlineRowMargin, st::inlineThumbSize, st::inlineThumbSize, _width), st::overviewVideoBg);\n\t}\n\n\tif (!_duration.isEmpty()) {\n\t\tint durationTop = st::inlineRowMargin + st::inlineThumbSize - st::normalFont->height - st::inlineDurationMargin;\n\t\tint durationW = _durationWidth + 2 * st::msgDateImgPadding.x(), durationH = st::normalFont->height + 2 * st::msgDateImgPadding.y();\n\t\tint durationX = (st::inlineThumbSize - durationW) / 2, durationY = st::inlineRowMargin + st::inlineThumbSize - durationH;\n\t\tUi::FillRoundRect(p, durationX, durationY - st::msgDateImgPadding.y(), durationW, durationH, st::msgDateImgBg, Ui::DateCorners);\n\t\tp.setPen(st::msgDateImgFg);\n\t\tp.setFont(st::normalFont);\n\t\tp.drawText(durationX + st::msgDateImgPadding.x(), durationTop + st::normalFont->ascent, _duration);\n\t}\n\n\tp.setPen(st::inlineTitleFg);\n\t_title.drawLeftElided(p, left, st::inlineRowMargin, _width - left, _width, 2);\n\tint32 titleHeight = qMin(_title.countHeight(_width - left), st::semiboldFont->height * 2);\n\n\tp.setPen(st::inlineDescriptionFg);\n\tint32 descriptionLines = withThumb ? (titleHeight > st::semiboldFont->height ? 1 : 2) : 3;\n\t_description.drawLeftElided(p, left, st::inlineRowMargin + titleHeight, _width - left, _width, descriptionLines);\n\n\tif (!context->lastRow) {\n\t\tp.fillRect(style::rtlrect(left, _height - st::inlineRowBorder, _width - left, st::inlineRowBorder, _width), st::inlineRowBorderFg);\n\t}\n}\n\nvoid Video::unloadHeavyPart() {\n\t_documentMedia = nullptr;\n\tItemBase::unloadHeavyPart();\n}\n\nTextState Video::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tif (QRect(0, st::inlineRowMargin, st::inlineThumbSize, st::inlineThumbSize).contains(point)) {\n\t\treturn { nullptr, _link };\n\t}\n\tif (QRect(st::inlineThumbSize + st::inlineThumbSkip, 0, _width - st::inlineThumbSize - st::inlineThumbSkip, _height).contains(point)) {\n\t\treturn { nullptr, _send };\n\t}\n\treturn {};\n}\n\nvoid Video::prepareThumbnail(QSize size) const {\n\tif (const auto document = getShownDocument()) {\n\t\tif (document->hasThumbnail()) {\n\t\t\tif (!_documentMedia) {\n\t\t\t\t_documentMedia = document->createMediaView();\n\t\t\t\t_documentMedia->thumbnailWanted(fileOrigin());\n\t\t\t}\n\t\t\tif (!_documentMedia->thumbnail()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\tauto resultThumbnailImage = _documentMedia\n\t\t? nullptr\n\t\t: getResultThumb(fileOrigin());\n\tconst auto resultThumbnail = Image(resultThumbnailImage\n\t\t? base::duplicate(*resultThumbnailImage)\n\t\t: QImage());\n\tconst auto thumb = _documentMedia\n\t\t? _documentMedia->thumbnail()\n\t\t: resultThumbnailImage\n\t\t? &resultThumbnail\n\t\t: nullptr;\n\tif (!thumb) {\n\t\treturn;\n\t}\n\tif (_thumb.size() != size * style::DevicePixelRatio()) {\n\t\tconst auto width = size.width();\n\t\tconst auto height = size.height();\n\t\tauto w = qMax(style::ConvertScale(thumb->width()), 1);\n\t\tauto h = qMax(style::ConvertScale(thumb->height()), 1);\n\t\tif (w * height > h * width) {\n\t\t\tif (height < h) {\n\t\t\t\tw = w * height / h;\n\t\t\t\th = height;\n\t\t\t}\n\t\t} else {\n\t\t\tif (width < w) {\n\t\t\t\th = h * width / w;\n\t\t\t\tw = width;\n\t\t\t}\n\t\t}\n\t\t_thumb = thumb->pixNoCache(\n\t\t\tQSize(w, h) * style::DevicePixelRatio(),\n\t\t\t{\n\t\t\t\t.options = Images::Option::TransparentBackground,\n\t\t\t\t.outer = size,\n\t\t\t});\n\t}\n}\n\nvoid CancelFileClickHandler::onClickImpl() const {\n\t_result->cancelFile();\n}\n\nFile::File(not_null<Context*> context, std::shared_ptr<Result> result)\n: FileBase(context, std::move(result))\n, _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineFileSize - st::inlineThumbSkip)\n, _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineFileSize - st::inlineThumbSkip)\n, _cancel(std::make_shared<CancelFileClickHandler>(_result.get()))\n, _document(getShownDocument()) {\n\tExpects(getResultDocument() != nullptr);\n\n\tupdateStatusText();\n\n\t// We have to save document, not read it from Result every time.\n\t// Because we first delete the Result and then delete this File.\n\t// So in destructor we have to remember _document, we can't read it.\n\tregDocumentItem(_document, this);\n}\n\nvoid File::initDimensions() {\n\t_maxw = st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft;\n\n\tTextParseOptions titleOpts = { 0, _maxw, st::semiboldFont->height, Qt::LayoutDirectionAuto };\n\t_title.setText(st::semiboldTextStyle, TextUtilities::SingleLine(_result->getLayoutTitle()), titleOpts);\n\n\tTextParseOptions descriptionOpts = { TextParseMultiline, _maxw, st::normalFont->height, Qt::LayoutDirectionAuto };\n\t_description.setText(st::defaultTextStyle, _result->getLayoutDescription(), descriptionOpts);\n\n\t_minh = st::inlineFileSize;\n\t_minh += st::inlineRowMargin * 2 + st::inlineRowBorder;\n}\n\nvoid File::paint(Painter &p, const QRect &clip, const PaintContext *context) const {\n\tconst auto left = st::inlineFileSize + st::inlineThumbSkip;\n\n\tensureDataMediaCreated();\n\tconst auto displayLoading = _document->displayLoading();\n\tif (displayLoading) {\n\t\tensureAnimation();\n\t\tif (!_animation->radial.animating()) {\n\t\t\t_animation->radial.start(_documentMedia->progress());\n\t\t}\n\t}\n\tconst auto showPause = updateStatusText();\n\tconst auto radial = isRadialAnimation();\n\n\tauto inner = style::rtlrect(0, st::inlineRowMargin, st::inlineFileSize, st::inlineFileSize, _width);\n\tp.setPen(Qt::NoPen);\n\n\tconst auto coverDrawn = _document->isSongWithCover()\n\t\t&& HistoryView::DrawThumbnailAsSongCover(\n\t\t\tp,\n\t\t\tst::songCoverOverlayFg,\n\t\t\t_documentMedia,\n\t\t\tinner);\n\tif (!coverDrawn) {\n\t\tPainterHighQualityEnabler hq(p);\n\t\tif (isThumbAnimation()) {\n\t\t\tconst auto over = _animation->a_thumbOver.value(1.);\n\t\t\tp.setBrush(\n\t\t\t\tanim::brush(st::msgFileInBg, st::msgFileInBgOver, over));\n\t\t} else {\n\t\t\tconst auto over = ClickHandler::showAsActive(_document->loading()\n\t\t\t\t? _cancel\n\t\t\t\t: _open);\n\t\t\tp.setBrush(over ? st::msgFileInBgOver : st::msgFileInBg);\n\t\t}\n\t\tp.drawEllipse(inner);\n\t}\n\n\tif (radial) {\n\t\tauto radialCircle = inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine));\n\t\t_animation->radial.draw(p, radialCircle, st::msgFileRadialLine, st::historyFileInRadialFg);\n\t}\n\n\tconst auto icon = [&] {\n\t\tif (radial || _document->loading()) {\n\t\t\treturn &st::historyFileInCancel;\n\t\t} else if (showPause) {\n\t\t\treturn &st::historyFileInPause;\n\t\t} else if (_document->isImage()) {\n\t\t\treturn &st::historyFileInImage;\n\t\t} else if (_document->isSongWithCover()) {\n\t\t\treturn &st::historyFileThumbPlay;\n\t\t} else if (_document->isVoiceMessage()\n\t\t\t|| _document->isAudioFile()) {\n\t\t\treturn &st::historyFileInPlay;\n\t\t}\n\t\treturn &st::historyFileInDocument;\n\t}();\n\ticon->paintInCenter(p, inner);\n\n\tint titleTop = st::inlineRowMargin + st::inlineRowFileNameTop;\n\tint descriptionTop = st::inlineRowMargin + st::inlineRowFileDescriptionTop;\n\n\tp.setPen(st::inlineTitleFg);\n\t_title.drawLeftElided(p, left, titleTop, _width - left, _width);\n\n\tp.setPen(st::inlineDescriptionFg);\n\tbool drawStatusSize = true;\n\tif (_statusSize == Ui::FileStatusSizeReady\n\t\t|| _statusSize == Ui::FileStatusSizeLoaded\n\t\t|| _statusSize == Ui::FileStatusSizeFailed) {\n\t\tif (!_description.isEmpty()) {\n\t\t\t_description.drawLeftElided(p, left, descriptionTop, _width - left, _width);\n\t\t\tdrawStatusSize = false;\n\t\t}\n\t}\n\tif (drawStatusSize) {\n\t\tp.setFont(st::normalFont);\n\t\tp.drawTextLeft(left, descriptionTop, _width, _statusText);\n\t}\n\n\tif (!context->lastRow) {\n\t\tp.fillRect(style::rtlrect(left, _height - st::inlineRowBorder, _width - left, st::inlineRowBorder, _width), st::inlineRowBorderFg);\n\t}\n}\n\nTextState File::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tif (QRect(0, st::inlineRowMargin, st::inlineFileSize, st::inlineFileSize).contains(point)) {\n\t\treturn { nullptr, _document->loading() ? _cancel : _open };\n\t} else {\n\t\tauto left = st::inlineFileSize + st::inlineThumbSkip;\n\t\tif (QRect(left, 0, _width - left, _height).contains(point)) {\n\t\t\treturn { nullptr, _send };\n\t\t}\n\t}\n\treturn {};\n}\n\nvoid File::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {\n\tif (p == _open || p == _cancel) {\n\t\tensureAnimation();\n\t\t_animation->a_thumbOver.start([this] { thumbAnimationCallback(); }, active ? 0. : 1., active ? 1. : 0., st::msgFileOverDuration);\n\t}\n}\n\nvoid File::unloadHeavyPart() {\n\t_documentMedia = nullptr;\n}\n\nFile::~File() {\n\tunregDocumentItem(_document, this);\n}\n\nvoid File::thumbAnimationCallback() {\n\tupdate();\n}\n\nvoid File::radialAnimationCallback(crl::time now) const {\n\tensureDataMediaCreated();\n\tconst auto updated = [&] {\n\t\treturn _animation->radial.update(\n\t\t\t_documentMedia->progress(),\n\t\t\t!_document->loading() || _documentMedia->loaded(),\n\t\t\tnow);\n\t}();\n\tif (!anim::Disabled() || updated) {\n\t\tupdate();\n\t}\n\tif (!_animation->radial.animating()) {\n\t\tcheckAnimationFinished();\n\t}\n}\n\nvoid File::ensureAnimation() const {\n\tif (!_animation) {\n\t\t_animation = std::make_unique<AnimationData>([=](crl::time now) {\n\t\t\treturn radialAnimationCallback(now);\n\t\t});\n\t}\n}\n\nvoid File::ensureDataMediaCreated() const {\n\tif (_documentMedia) {\n\t\treturn;\n\t}\n\t_documentMedia = _document->createMediaView();\n}\n\nvoid File::checkAnimationFinished() const {\n\tif (_animation\n\t\t&& !_animation->a_thumbOver.animating()\n\t\t&& !_animation->radial.animating()) {\n\t\tensureDataMediaCreated();\n\t\tif (_documentMedia->loaded()) {\n\t\t\t_animation.reset();\n\t\t}\n\t}\n}\n\nbool File::updateStatusText() const {\n\tensureDataMediaCreated();\n\tauto showPause = false;\n\tauto statusSize = int64();\n\tauto realDuration = TimeId();\n\tif (_document->status == FileDownloadFailed || _document->status == FileUploadFailed) {\n\t\tstatusSize = Ui::FileStatusSizeFailed;\n\t} else if (_document->uploading()) {\n\t\tstatusSize = _document->uploadingData->offset;\n\t} else if (_document->loading()) {\n\t\tstatusSize = _document->loadOffset();\n\t} else if (_documentMedia->loaded()) {\n\t\tstatusSize = Ui::FileStatusSizeLoaded;\n\t} else {\n\t\tstatusSize = Ui::FileStatusSizeReady;\n\t}\n\n\tif (_document->isVoiceMessage() || _document->isAudioFile()) {\n\t\tconst auto type = _document->isVoiceMessage() ? AudioMsgId::Type::Voice : AudioMsgId::Type::Song;\n\t\tconst auto state = Media::Player::instance()->getState(type);\n\t\tif (state.id == AudioMsgId(_document, FullMsgId(), state.id.externalPlayId()) && !Media::Player::IsStoppedOrStopping(state.state)) {\n\t\t\tstatusSize = -1 - (state.position / state.frequency);\n\t\t\trealDuration = (state.length / state.frequency);\n\t\t\tshowPause = Media::Player::ShowPauseIcon(state.state);\n\t\t}\n\t\tif (!showPause && (state.id == AudioMsgId(_document, FullMsgId(), state.id.externalPlayId())) && Media::Player::instance()->isSeeking(AudioMsgId::Type::Song)) {\n\t\t\tshowPause = true;\n\t\t}\n\t}\n\n\tif (statusSize != _statusSize) {\n\t\tconst auto duration = _document->isSong()\n\t\t\t? _document->duration()\n\t\t\t: (_document->isVoiceMessage() ? _document->duration() : -1);\n\t\tsetStatusSize(statusSize, _document->size, (duration >= 0) ? (duration / 1000) : -1, realDuration);\n\t}\n\treturn showPause;\n}\n\nvoid File::setStatusSize(\n\t\tint64 newSize,\n\t\tint64 fullSize,\n\t\tTimeId duration,\n\t\tTimeId realDuration) const {\n\t_statusSize = newSize;\n\tif (_statusSize == Ui::FileStatusSizeReady) {\n\t\t_statusText = (duration >= 0) ? Ui::FormatDurationAndSizeText(duration, fullSize) : (duration < -1 ? Ui::FormatGifAndSizeText(fullSize) : Ui::FormatSizeText(fullSize));\n\t} else if (_statusSize == Ui::FileStatusSizeLoaded) {\n\t\t_statusText = (duration >= 0) ? Ui::FormatDurationText(duration) : (duration < -1 ? u\"GIF\"_q : Ui::FormatSizeText(fullSize));\n\t} else if (_statusSize == Ui::FileStatusSizeFailed) {\n\t\t_statusText = tr::lng_attach_failed(tr::now);\n\t} else if (_statusSize >= 0) {\n\t\t_statusText = Ui::FormatDownloadText(_statusSize, fullSize);\n\t} else {\n\t\t_statusText = Ui::FormatPlayedText(-_statusSize - 1, realDuration);\n\t}\n}\n\nContact::Contact(not_null<Context*> context, std::shared_ptr<Result> result)\n: ItemBase(context, std::move(result))\n, _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip)\n, _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) {\n}\n\nvoid Contact::initDimensions() {\n\t_maxw = st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft;\n\tint32 textWidth = _maxw - (st::inlineThumbSize + st::inlineThumbSkip);\n\tTextParseOptions titleOpts = { 0, textWidth, st::semiboldFont->height, Qt::LayoutDirectionAuto };\n\t_title.setText(st::semiboldTextStyle, TextUtilities::SingleLine(_result->getLayoutTitle()), titleOpts);\n\n\tTextParseOptions descriptionOpts = { TextParseMultiline, textWidth, st::normalFont->height, Qt::LayoutDirectionAuto };\n\t_description.setText(st::defaultTextStyle, _result->getLayoutDescription(), descriptionOpts);\n\n\t_minh = st::inlineFileSize;\n\t_minh += st::inlineRowMargin * 2 + st::inlineRowBorder;\n}\n\nvoid Contact::paint(Painter &p, const QRect &clip, const PaintContext *context) const {\n\tint32 left = st::defaultEmojiPan.headerLeft - st::inlineResultsLeft;\n\n\tleft = st::inlineFileSize + st::inlineThumbSkip;\n\tprepareThumbnail(st::inlineFileSize, st::inlineFileSize);\n\tQRect rthumb(style::rtlrect(0, st::inlineRowMargin, st::inlineFileSize, st::inlineFileSize, _width));\n\tp.drawPixmapLeft(rthumb.topLeft(), _width, _thumb);\n\n\tint titleTop = st::inlineRowMargin + st::inlineRowFileNameTop;\n\tint descriptionTop = st::inlineRowMargin + st::inlineRowFileDescriptionTop;\n\n\tp.setPen(st::inlineTitleFg);\n\t_title.drawLeftElided(p, left, titleTop, _width - left, _width);\n\n\tp.setPen(st::inlineDescriptionFg);\n\t_description.drawLeftElided(p, left, descriptionTop, _width - left, _width);\n\n\tif (!context->lastRow) {\n\t\tp.fillRect(style::rtlrect(left, _height - st::inlineRowBorder, _width - left, st::inlineRowBorder, _width), st::inlineRowBorderFg);\n\t}\n}\n\nTextState Contact::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tif (!QRect(0, st::inlineRowMargin, st::inlineFileSize, st::inlineThumbSize).contains(point)) {\n\t\tauto left = (st::inlineFileSize + st::inlineThumbSkip);\n\t\tif (QRect(left, 0, _width - left, _height).contains(point)) {\n\t\t\treturn { nullptr, _send };\n\t\t}\n\t}\n\treturn {};\n}\n\nvoid Contact::prepareThumbnail(int width, int height) const {\n\tif (!hasResultThumb()) {\n\t\tif ((_thumb.width() != width * style::DevicePixelRatio())\n\t\t\t|| (_thumb.height() != height * style::DevicePixelRatio())) {\n\t\t\t_thumb = getResultContactAvatar(width, height);\n\t\t}\n\t\treturn;\n\t}\n\n\tconst auto origin = fileOrigin();\n\tconst auto thumb = getResultThumb(origin);\n\tif (!thumb\n\t\t|| ((_thumb.width() == width * style::DevicePixelRatio())\n\t\t\t&& (_thumb.height() == height * style::DevicePixelRatio()))) {\n\t\treturn;\n\t}\n\tconst auto scaled = ScaleDown(\n\t\tqMax(style::ConvertScale(thumb->width()), 1),\n\t\tqMax(style::ConvertScale(thumb->height()), 1),\n\t\twidth,\n\t\theight);\n\t_thumb = Image(base::duplicate(*thumb)).pixNoCache(\n\t\tscaled * style::DevicePixelRatio(),\n\t\t{\n\t\t\t.options = Images::Option::TransparentBackground,\n\t\t\t.outer = { width, height },\n\t\t});\n}\n\nThumbnail::Thumbnail(\n\tnot_null<Context*> context,\n\tstd::shared_ptr<Result> result)\n: ItemBase(context, std::move(result)) {\n}\n\nvoid Thumbnail::initDimensions() {\n\tint w = 0, h = 0;\n\tif (const auto photo = getResultPhoto()) {\n\t\tw = photo->width();\n\t\th = photo->height();\n\t} else if (const auto document = getResultDocument()) {\n\t\tw = document->dimensions.width();\n\t\th = document->dimensions.height();\n\t}\n\tif (w <= 0 || h <= 0) {\n\t\tw = h = 1;\n\t}\n\tw = w * st::inlineMediaHeight / h;\n\t_maxw = qMax(w, int32(st::inlineResultsMinWidth));\n\t_minh = st::inlineMediaHeight + st::inlineResultsSkip;\n}\n\nQSize Thumbnail::countFrameSize() const {\n\tint w = 0, h = 0;\n\tif (const auto photo = getResultPhoto()) {\n\t\tw = photo->width();\n\t\th = photo->height();\n\t} else if (const auto document = getResultDocument()) {\n\t\tw = document->dimensions.width();\n\t\th = document->dimensions.height();\n\t}\n\tif (w <= 0 || h <= 0) {\n\t\treturn { _width, st::inlineMediaHeight };\n\t}\n\t// Aspect-fill: scale so the smaller dimension covers the cell.\n\tconst auto targetHeight = st::inlineMediaHeight;\n\tif (w * targetHeight > h * _width) {\n\t\tw = w * targetHeight / h;\n\t\th = targetHeight;\n\t} else {\n\t\th = h * _width / w;\n\t\tw = _width;\n\t}\n\treturn { qMax(w, 1), qMax(h, 1) };\n}\n\nvoid Thumbnail::validateThumbnail(\n\t\tImage *image,\n\t\tQSize size,\n\t\tQSize frame,\n\t\tbool good) const {\n\tif (!image || (_thumbGood && !good)) {\n\t\treturn;\n\t} else if ((_thumb.size() == size * style::DevicePixelRatio())\n\t\t&& (_thumbGood || !good)) {\n\t\treturn;\n\t}\n\t_thumb = image->pixNoCache(\n\t\tframe * style::DevicePixelRatio(),\n\t\t{\n\t\t\t.options = (Images::Option::TransparentBackground\n\t\t\t\t| (good ? Images::Option() : Images::Option::Blur)),\n\t\t\t.outer = size,\n\t\t});\n\t_thumbGood = good;\n}\n\nvoid Thumbnail::prepareThumbnail(QSize size, QSize frame) const {\n\tif (const auto photo = getResultPhoto()) {\n\t\tif (!_photoMedia) {\n\t\t\t_photoMedia = photo->createMediaView();\n\t\t\t_photoMedia->wanted(Data::PhotoSize::Thumbnail, fileOrigin());\n\t\t}\n\t\tusing PhotoSize = Data::PhotoSize;\n\t\tvalidateThumbnail(\n\t\t\t_photoMedia->image(PhotoSize::Thumbnail),\n\t\t\tsize,\n\t\t\tframe,\n\t\t\ttrue);\n\t\tvalidateThumbnail(\n\t\t\t_photoMedia->image(PhotoSize::Small),\n\t\t\tsize,\n\t\t\tframe,\n\t\t\tfalse);\n\t\tvalidateThumbnail(\n\t\t\t_photoMedia->thumbnailInline(),\n\t\t\tsize,\n\t\t\tframe,\n\t\t\tfalse);\n\t} else if (const auto document = getResultDocument()) {\n\t\tif (!_documentMedia) {\n\t\t\t_documentMedia = document->createMediaView();\n\t\t\tdocument->loadThumbnail(fileOrigin());\n\t\t}\n\t\tvalidateThumbnail(\n\t\t\t_documentMedia->thumbnail(),\n\t\t\tsize,\n\t\t\tframe,\n\t\t\ttrue);\n\t\tvalidateThumbnail(\n\t\t\t_documentMedia->thumbnailInline(),\n\t\t\tsize,\n\t\t\tframe,\n\t\t\tfalse);\n\t} else if (const auto thumb = getResultThumb(fileOrigin())) {\n\t\tif (_thumb.isNull()) {\n\t\t\tconst auto scaled = ScaleDown(\n\t\t\t\tqMax(style::ConvertScale(thumb->width()), 1),\n\t\t\t\tqMax(style::ConvertScale(thumb->height()), 1),\n\t\t\t\tframe.width(),\n\t\t\t\tframe.height());\n\t\t\t_thumb = Image(base::duplicate(*thumb)).pixNoCache(\n\t\t\t\tscaled * style::DevicePixelRatio(),\n\t\t\t\t{\n\t\t\t\t\t.options = Images::Option::TransparentBackground,\n\t\t\t\t\t.outer = size,\n\t\t\t\t});\n\t\t}\n\t}\n}\n\nvoid Thumbnail::paint(\n\t\tPainter &p,\n\t\tconst QRect &clip,\n\t\tconst PaintContext *context) const {\n\tconst auto height = st::inlineMediaHeight;\n\tconst auto frame = countFrameSize();\n\n\tQRect r(0, 0, _width, height);\n\tprepareThumbnail({ _width, height }, frame);\n\tif (_thumb.isNull()) {\n\t\tp.fillRect(r, st::overviewPhotoBg);\n\t} else {\n\t\tp.drawPixmap(r.topLeft(), _thumb);\n\t}\n}\n\nTextState Thumbnail::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tif (QRect(0, 0, _width, st::inlineMediaHeight).contains(point)) {\n\t\treturn { nullptr, _send };\n\t}\n\treturn {};\n}\n\nvoid Thumbnail::unloadHeavyPart() {\n\t_photoMedia = nullptr;\n\t_documentMedia = nullptr;\n\tItemBase::unloadHeavyPart();\n}\n\nArticle::Article(\n\tnot_null<Context*> context,\n\tstd::shared_ptr<Result> result,\n\tbool withThumb)\n: ItemBase(context, std::move(result))\n, _url(getResultUrlHandler())\n, _link(getResultPreviewHandler())\n, _withThumb(withThumb)\n, _title(st::emojiPanWidth / 2)\n, _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) {\n\tif (!_link) {\n\t\tif (const auto point = _result->getLocationPoint()) {\n\t\t\t_link = std::make_shared<LocationClickHandler>(*point);\n\t\t}\n\t}\n\t_thumbLetter = getResultThumbLetter();\n}\n\nvoid Article::initDimensions() {\n\t_maxw = st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft;\n\tint32 textWidth = _maxw - (_withThumb ? (st::inlineThumbSize + st::inlineThumbSkip) : (st::defaultEmojiPan.headerLeft - st::inlineResultsLeft));\n\tTextParseOptions titleOpts = { 0, textWidth, 2 * st::semiboldFont->height, Qt::LayoutDirectionAuto };\n\t_title.setText(st::semiboldTextStyle, TextUtilities::SingleLine(_result->getLayoutTitle()), titleOpts);\n\tint32 titleHeight = qMin(_title.countHeight(textWidth), 2 * st::semiboldFont->height);\n\n\tint32 descriptionLines = (_withThumb || _url) ? 2 : 3;\n\tQString description = _result->getLayoutDescription();\n\tTextParseOptions descriptionOpts = { TextParseMultiline, textWidth, descriptionLines * st::normalFont->height, Qt::LayoutDirectionAuto };\n\t_description.setText(st::defaultTextStyle, description, descriptionOpts);\n\tint32 descriptionHeight = qMin(_description.countHeight(textWidth), descriptionLines * st::normalFont->height);\n\n\t_minh = titleHeight + descriptionHeight;\n\tif (_url) _minh += st::normalFont->height;\n\tif (_withThumb) _minh = qMax(_minh, int32(st::inlineThumbSize));\n\t_minh += st::inlineRowMargin * 2 + st::inlineRowBorder;\n}\n\nint Article::resizeGetHeight(int width) {\n\t_width = qMin(width, _maxw);\n\tif (_url) {\n\t\t_urlText = getResultUrl();\n\t\t_urlWidth = st::normalFont->width(_urlText);\n\t\tint32 textWidth = _width - (_withThumb ? (st::inlineThumbSize + st::inlineThumbSkip) : (st::defaultEmojiPan.headerLeft - st::inlineResultsLeft));\n\t\tif (_urlWidth > textWidth) {\n\t\t\t_urlText = st::normalFont->elided(_urlText, textWidth);\n\t\t\t_urlWidth = st::normalFont->width(_urlText);\n\t\t}\n\t}\n\t_height = _minh;\n\treturn _height;\n}\n\nvoid Article::paint(Painter &p, const QRect &clip, const PaintContext *context) const {\n\tint32 left = st::defaultEmojiPan.headerLeft - st::inlineResultsLeft;\n\tif (_withThumb) {\n\t\tleft = st::inlineThumbSize + st::inlineThumbSkip;\n\t\tprepareThumbnail(st::inlineThumbSize, st::inlineThumbSize);\n\t\tQRect rthumb(style::rtlrect(0, st::inlineRowMargin, st::inlineThumbSize, st::inlineThumbSize, _width));\n\t\tif (_thumb.isNull()) {\n\t\t\tif (!hasResultThumb() && !_thumbLetter.isEmpty()) {\n\t\t\t\tint32 index = (_thumbLetter.at(0).unicode() % 4);\n\t\t\t\tstyle::color colors[] = {\n\t\t\t\t\tst::msgFile3Bg,\n\t\t\t\t\tst::msgFile4Bg,\n\t\t\t\t\tst::msgFile2Bg,\n\t\t\t\t\tst::msgFile1Bg\n\t\t\t\t};\n\n\t\t\t\tp.fillRect(rthumb, colors[index]);\n\t\t\t\tif (!_thumbLetter.isEmpty()) {\n\t\t\t\t\tp.setFont(st::linksLetterFont);\n\t\t\t\t\tp.setPen(st::linksLetterFg);\n\t\t\t\t\tp.drawText(rthumb, _thumbLetter, style::al_center);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tp.fillRect(rthumb, st::overviewPhotoBg);\n\t\t\t}\n\t\t} else {\n\t\t\tp.drawPixmapLeft(rthumb.topLeft(), _width, _thumb);\n\t\t}\n\t}\n\n\tp.setPen(st::inlineTitleFg);\n\t_title.drawLeftElided(p, left, st::inlineRowMargin, _width - left, _width, 2);\n\tint32 titleHeight = qMin(_title.countHeight(_width - left), st::semiboldFont->height * 2);\n\n\tp.setPen(st::inlineDescriptionFg);\n\tint32 descriptionLines = (_withThumb || _url) ? 2 : 3;\n\t_description.drawLeftElided(p, left, st::inlineRowMargin + titleHeight, _width - left, _width, descriptionLines);\n\n\tif (_url) {\n\t\tint32 descriptionHeight = qMin(_description.countHeight(_width - left), st::normalFont->height * descriptionLines);\n\t\tp.drawTextLeft(left, st::inlineRowMargin + titleHeight + descriptionHeight, _width, _urlText, _urlWidth);\n\t}\n\n\tif (!context->lastRow) {\n\t\tp.fillRect(style::rtlrect(left, _height - st::inlineRowBorder, _width - left, st::inlineRowBorder, _width), st::inlineRowBorderFg);\n\t}\n}\n\nTextState Article::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tif (_withThumb && QRect(0, st::inlineRowMargin, st::inlineThumbSize, st::inlineThumbSize).contains(point)) {\n\t\treturn { nullptr, _link };\n\t}\n\tauto left = _withThumb ? (st::inlineThumbSize + st::inlineThumbSkip) : 0;\n\tif (QRect(left, 0, _width - left, _height).contains(point)) {\n\t\tif (_url) {\n\t\t\tauto left = st::inlineThumbSize + st::inlineThumbSkip;\n\t\t\tauto titleHeight = qMin(_title.countHeight(_width - left), st::semiboldFont->height * 2);\n\t\t\tauto descriptionLines = 2;\n\t\t\tauto descriptionHeight = qMin(_description.countHeight(_width - left), st::normalFont->height * descriptionLines);\n\t\t\tif (style::rtlrect(left, st::inlineRowMargin + titleHeight + descriptionHeight, _urlWidth, st::normalFont->height, _width).contains(point)) {\n\t\t\t\treturn { nullptr, _url };\n\t\t\t}\n\t\t}\n\t\treturn { nullptr, _send };\n\t}\n\treturn {};\n}\n\nvoid Article::prepareThumbnail(int width, int height) const {\n\tif (!hasResultThumb()) {\n\t\tprepareMediaThumbnail(width, height);\n\t\tif (!_thumb.isNull()) {\n\t\t\treturn;\n\t\t}\n\t\tif ((_thumb.width() != width * style::DevicePixelRatio())\n\t\t\t|| (_thumb.height() != height * style::DevicePixelRatio())) {\n\t\t\t_thumb = getResultContactAvatar(width, height);\n\t\t}\n\t\treturn;\n\t}\n\n\tconst auto origin = fileOrigin();\n\tconst auto thumb = getResultThumb(origin);\n\tif (!thumb\n\t\t|| ((_thumb.width() == width * style::DevicePixelRatio())\n\t\t\t&& (_thumb.height() == height * style::DevicePixelRatio()))) {\n\t\treturn;\n\t}\n\tconst auto scaled = ScaleDown(\n\t\tqMax(style::ConvertScale(thumb->width()), 1),\n\t\tqMax(style::ConvertScale(thumb->height()), 1),\n\t\twidth,\n\t\theight);\n\t_thumb = Image(base::duplicate(*thumb)).pixNoCache(\n\t\tscaled * style::DevicePixelRatio(),\n\t\t{\n\t\t\t.options = Images::Option::TransparentBackground,\n\t\t\t.outer = { width, height },\n\t\t});\n}\n\nvoid Article::prepareMediaThumbnail(int width, int height) const {\n\tauto thumbGood = false;\n\tconst auto make = [&](Image *image, bool good) {\n\t\tif (!image || (thumbGood && !good)) {\n\t\t\treturn;\n\t\t}\n\t\tif (!_thumb.isNull() && !good) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto scaled = ScaleDown(\n\t\t\tqMax(style::ConvertScale(image->width()), 1),\n\t\t\tqMax(style::ConvertScale(image->height()), 1),\n\t\t\twidth,\n\t\t\theight);\n\t\t_thumb = image->pixNoCache(\n\t\t\tscaled * style::DevicePixelRatio(),\n\t\t\t{\n\t\t\t\t.options = (Images::Option::TransparentBackground\n\t\t\t\t\t| (good\n\t\t\t\t\t\t? Images::Option()\n\t\t\t\t\t\t: Images::Option::Blur)),\n\t\t\t\t.outer = { width, height },\n\t\t\t});\n\t\tif (good) {\n\t\t\tthumbGood = true;\n\t\t}\n\t};\n\tif (const auto photo = getResultPhoto()) {\n\t\tif (!_photoMedia) {\n\t\t\t_photoMedia = photo->createMediaView();\n\t\t\t_photoMedia->wanted(Data::PhotoSize::Thumbnail, fileOrigin());\n\t\t}\n\t\tusing PhotoSize = Data::PhotoSize;\n\t\tmake(_photoMedia->image(PhotoSize::Thumbnail), true);\n\t\tmake(_photoMedia->image(PhotoSize::Small), false);\n\t\tmake(_photoMedia->thumbnailInline(), false);\n\t} else if (const auto document = getResultDocument()) {\n\t\tif (document->hasThumbnail()) {\n\t\t\tif (!_documentMedia) {\n\t\t\t\t_documentMedia = document->createMediaView();\n\t\t\t\tdocument->loadThumbnail(fileOrigin());\n\t\t\t}\n\t\t\tmake(_documentMedia->thumbnail(), true);\n\t\t\tmake(_documentMedia->thumbnailInline(), false);\n\t\t}\n\t}\n}\n\nvoid Article::unloadHeavyPart() {\n\t_photoMedia = nullptr;\n\t_documentMedia = nullptr;\n\tItemBase::unloadHeavyPart();\n}\n\nGame::Game(not_null<Context*> context, std::shared_ptr<Result> result)\n: ItemBase(context, std::move(result))\n, _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip)\n, _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) {\n\tcountFrameSize();\n}\n\nvoid Game::countFrameSize() {\n\tif (auto document = getResultDocument()) {\n\t\tif (document->isAnimation()) {\n\t\t\tauto documentSize = document->dimensions;\n\t\t\tif (documentSize.isEmpty()) {\n\t\t\t\tdocumentSize = QSize(st::inlineThumbSize, st::inlineThumbSize);\n\t\t\t}\n\t\t\tauto resizeByHeight1 = (documentSize.width() > documentSize.height()) && (documentSize.height() >= st::inlineThumbSize);\n\t\t\tauto resizeByHeight2 = (documentSize.height() >= documentSize.width()) && (documentSize.width() < st::inlineThumbSize);\n\t\t\tif (resizeByHeight1 || resizeByHeight2) {\n\t\t\t\tif (documentSize.height() > st::inlineThumbSize) {\n\t\t\t\t\t_frameSize = QSize((documentSize.width() * st::inlineThumbSize) / documentSize.height(), st::inlineThumbSize);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (documentSize.width() > st::inlineThumbSize) {\n\t\t\t\t\t_frameSize = QSize(st::inlineThumbSize, (documentSize.height() * st::inlineThumbSize) / documentSize.width());\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!_frameSize.width()) {\n\t\t\t\t_frameSize.setWidth(1);\n\t\t\t}\n\t\t\tif (!_frameSize.height()) {\n\t\t\t\t_frameSize.setHeight(1);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Game::initDimensions() {\n\t_maxw = st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft;\n\tTextParseOptions titleOpts = { 0, _maxw, 2 * st::semiboldFont->height, Qt::LayoutDirectionAuto };\n\t_title.setText(st::semiboldTextStyle, TextUtilities::SingleLine(_result->getLayoutTitle()), titleOpts);\n\tint32 titleHeight = qMin(_title.countHeight(_maxw), 2 * st::semiboldFont->height);\n\n\tint32 descriptionLines = 2;\n\tQString description = _result->getLayoutDescription();\n\tTextParseOptions descriptionOpts = { TextParseMultiline, _maxw, descriptionLines * st::normalFont->height, Qt::LayoutDirectionAuto };\n\t_description.setText(st::defaultTextStyle, description, descriptionOpts);\n\tint32 descriptionHeight = qMin(_description.countHeight(_maxw), descriptionLines * st::normalFont->height);\n\n\t_minh = titleHeight + descriptionHeight;\n\taccumulate_max(_minh, st::inlineThumbSize);\n\t_minh += st::inlineRowMargin * 2 + st::inlineRowBorder;\n}\n\nvoid Game::setPosition(int32 position) {\n\tAbstractLayoutItem::setPosition(position);\n\tif (_position < 0) {\n\t\t_gif.reset();\n\t}\n}\n\nvoid Game::paint(Painter &p, const QRect &clip, const PaintContext *context) const {\n\tint32 left = st::defaultEmojiPan.headerLeft - st::inlineResultsLeft;\n\n\tleft = st::inlineThumbSize + st::inlineThumbSkip;\n\tauto rthumb = style::rtlrect(0, st::inlineRowMargin, st::inlineThumbSize, st::inlineThumbSize, _width);\n\n\t// Gif thumb\n\tauto thumbDisplayed = false, radial = false;\n\tconst auto photo = getResultPhoto();\n\tconst auto document = getResultDocument();\n\tif (document) {\n\t\tensureDataMediaCreated(document);\n\t} else if (photo) {\n\t\tensureDataMediaCreated(photo);\n\t}\n\tauto animatedThumb = document && document->isAnimation();\n\tif (animatedThumb) {\n\t\t_documentMedia->automaticLoad(fileOrigin(), nullptr);\n\n\t\tbool loaded = _documentMedia->loaded(), displayLoading = document->displayLoading();\n\t\tif (loaded && !_gif && !_gif.isBad()) {\n\t\t\tauto that = const_cast<Game*>(this);\n\t\t\tthat->_gif = Media::Clip::MakeReader(\n\t\t\t\t_documentMedia->owner()->location(),\n\t\t\t\t_documentMedia->bytes(),\n\t\t\t\t[=](Media::Clip::Notification notification) { that->clipCallback(notification); });\n\t\t}\n\n\t\tbool animating = (_gif && _gif->started());\n\t\tif (displayLoading) {\n\t\t\tif (!_radial) {\n\t\t\t\t_radial = std::make_unique<Ui::RadialAnimation>([=](crl::time now) {\n\t\t\t\t\treturn radialAnimationCallback(now);\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (!_radial->animating()) {\n\t\t\t\t_radial->start(_documentMedia->progress());\n\t\t\t}\n\t\t}\n\t\tradial = isRadialAnimation();\n\n\t\tif (animating) {\n\t\t\tconst auto pixmap = _gif->current({\n\t\t\t\t.frame = _frameSize,\n\t\t\t\t.outer = { st::inlineThumbSize, st::inlineThumbSize },\n\t\t\t}, context->paused ? 0 : context->ms);\n\t\t\tif (_thumb.isNull()) {\n\t\t\t\t_thumb = pixmap;\n\t\t\t\t_thumbGood = true;\n\t\t\t}\n\t\t\tp.drawImage(rthumb.topLeft(), pixmap);\n\t\t\tthumbDisplayed = true;\n\t\t}\n\t}\n\n\tif (!thumbDisplayed) {\n\t\tprepareThumbnail({ st::inlineThumbSize, st::inlineThumbSize });\n\t\tif (_thumb.isNull()) {\n\t\t\tp.fillRect(rthumb, st::overviewPhotoBg);\n\t\t} else {\n\t\t\tp.drawImage(rthumb.topLeft(), _thumb);\n\t\t}\n\t}\n\n\tif (radial) {\n\t\tp.fillRect(rthumb, st::msgDateImgBg);\n\t\tQRect inner((st::inlineThumbSize - st::inlineRadialSize) / 2, (st::inlineThumbSize - st::inlineRadialSize) / 2, st::inlineRadialSize, st::inlineRadialSize);\n\t\tif (radial) {\n\t\t\tp.setOpacity(1);\n\t\t\tQRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));\n\t\t\t_radial->draw(p, rinner, st::msgFileRadialLine, st::historyFileThumbRadialFg);\n\t\t}\n\t}\n\n\tp.setPen(st::inlineTitleFg);\n\t_title.drawLeftElided(p, left, st::inlineRowMargin, _width - left, _width, 2);\n\tint32 titleHeight = qMin(_title.countHeight(_width - left), st::semiboldFont->height * 2);\n\n\tp.setPen(st::inlineDescriptionFg);\n\tint32 descriptionLines = 2;\n\t_description.drawLeftElided(p, left, st::inlineRowMargin + titleHeight, _width - left, _width, descriptionLines);\n\n\tif (!context->lastRow) {\n\t\tp.fillRect(style::rtlrect(left, _height - st::inlineRowBorder, _width - left, st::inlineRowBorder, _width), st::inlineRowBorderFg);\n\t}\n}\n\nTextState Game::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tint left = st::inlineThumbSize + st::inlineThumbSkip;\n\tif (QRect(0, st::inlineRowMargin, st::inlineThumbSize, st::inlineThumbSize).contains(point)) {\n\t\treturn { nullptr, _send };\n\t}\n\tif (QRect(left, 0, _width - left, _height).contains(point)) {\n\t\treturn { nullptr, _send };\n\t}\n\treturn {};\n}\n\nvoid Game::prepareThumbnail(QSize size) const {\n\tif ([[maybe_unused]] const auto document = getResultDocument()) {\n\t\tAssert(_documentMedia != nullptr);\n\t\tvalidateThumbnail(_documentMedia->thumbnail(), size, true);\n\t\tvalidateThumbnail(_documentMedia->thumbnailInline(), size, false);\n\t} else if ([[maybe_unused]] const auto photo = getResultPhoto()) {\n\t\tusing Data::PhotoSize;\n\t\tAssert(_photoMedia != nullptr);\n\t\tvalidateThumbnail(_photoMedia->image(PhotoSize::Thumbnail), size, true);\n\t\tvalidateThumbnail(_photoMedia->image(PhotoSize::Small), size, false);\n\t\tvalidateThumbnail(_photoMedia->thumbnailInline(), size, false);\n\t}\n}\n\nvoid Game::ensureDataMediaCreated(not_null<DocumentData*> document) const {\n\tif (_documentMedia) {\n\t\treturn;\n\t}\n\t_documentMedia = document->createMediaView();\n\t_documentMedia->thumbnailWanted(fileOrigin());\n}\n\nvoid Game::ensureDataMediaCreated(not_null<PhotoData*> photo) const {\n\tif (_photoMedia) {\n\t\treturn;\n\t}\n\t_photoMedia = photo->createMediaView();\n\t_photoMedia->wanted(Data::PhotoSize::Thumbnail, fileOrigin());\n}\n\nvoid Game::validateThumbnail(Image *image, QSize size, bool good) const {\n\tif (!image || (_thumbGood && !good)) {\n\t\treturn;\n\t} else if ((_thumb.size() == size * style::DevicePixelRatio())\n\t\t&& (_thumbGood || !good)) {\n\t\treturn;\n\t}\n\tconst auto width = size.width();\n\tconst auto height = size.height();\n\tauto w = qMax(style::ConvertScale(image->width()), 1);\n\tauto h = qMax(style::ConvertScale(image->height()), 1);\n\tauto resizeByHeight1 = (w * height > h * width) && (h >= height);\n\tauto resizeByHeight2 = (h * width >= w * height) && (w < width);\n\tif (resizeByHeight1 || resizeByHeight2) {\n\t\tif (h > height) {\n\t\t\tw = w * height / h;\n\t\t\th = height;\n\t\t}\n\t} else {\n\t\tif (w > width) {\n\t\t\th = h * width / w;\n\t\t\tw = width;\n\t\t}\n\t}\n\t_thumbGood = good;\n\t_thumb = image->pixNoCache(\n\t\tQSize(w, h) * style::DevicePixelRatio(),\n\t\t{\n\t\t\t.options = (Images::Option::TransparentBackground\n\t\t\t\t| (good ? Images::Option() : Images::Option::Blur)),\n\t\t\t.outer = size,\n\t\t}).toImage();\n}\n\nbool Game::isRadialAnimation() const {\n\tif (_radial) {\n\t\tif (_radial->animating()) {\n\t\t\treturn true;\n\t\t} else {\n\t\t\tensureDataMediaCreated(getResultDocument());\n\t\t\tif (_documentMedia->loaded()) {\n\t\t\t\t_radial = nullptr;\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Game::radialAnimationCallback(crl::time now) const {\n\tconst auto document = getResultDocument();\n\tensureDataMediaCreated(document);\n\tconst auto updated = [&] {\n\t\treturn _radial->update(\n\t\t\t_documentMedia->progress(),\n\t\t\t!document->loading() || _documentMedia->loaded(),\n\t\t\tnow);\n\t}();\n\tif (!anim::Disabled() || updated) {\n\t\tupdate();\n\t}\n\tif (!_radial->animating() && _documentMedia->loaded()) {\n\t\t_radial = nullptr;\n\t}\n}\n\nvoid Game::unloadHeavyPart() {\n\t_gif.reset();\n\t_documentMedia = nullptr;\n\t_photoMedia = nullptr;\n}\n\nvoid Game::clipCallback(Media::Clip::Notification notification) {\n\tusing namespace Media::Clip;\n\tswitch (notification) {\n\tcase Notification::Reinit: {\n\t\tif (_gif) {\n\t\t\tif (_gif->state() == State::Error) {\n\t\t\t\t_gif.setBad();\n\t\t\t} else if (_gif->ready() && !_gif->started()) {\n\t\t\t\tif (_gif->width() * _gif->height() > kMaxInlineArea) {\n\t\t\t\t\tgetResultDocument()->dimensions = QSize(\n\t\t\t\t\t\t_gif->width(),\n\t\t\t\t\t\t_gif->height());\n\t\t\t\t\t_gif.reset();\n\t\t\t\t} else {\n\t\t\t\t\t_gif->start({\n\t\t\t\t\t\t.frame = _frameSize,\n\t\t\t\t\t\t.outer = { st::inlineThumbSize, st::inlineThumbSize },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else if (_gif->autoPausedGif() && !context()->inlineItemVisible(this)) {\n\t\t\t\tunloadHeavyPart();\n\t\t\t}\n\t\t}\n\n\t\tupdate();\n\t} break;\n\n\tcase Notification::Repaint: {\n\t\tif (_gif && !_gif->currentDisplayed()) {\n\t\t\tupdate();\n\t\t}\n\t} break;\n\t}\n}\n\n} // namespace internal\n} // namespace Layout\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n#include \"inline_bots/inline_bot_layout_item.h\"\n#include \"media/clip/media_clip_reader.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/text/text.h\"\n\nnamespace Lottie {\nclass SinglePlayer;\n} // namespace Lottie\n\nnamespace Data {\nclass PhotoMedia;\nclass DocumentMedia;\n} // namespace Data\n\nnamespace InlineBots {\nnamespace Layout {\nnamespace internal {\n\nclass FileBase : public ItemBase {\npublic:\n\tFileBase(not_null<Context*> context, std::shared_ptr<Result> result);\n\n\t// For saved gif layouts.\n\tFileBase(not_null<Context*> context, not_null<DocumentData*> document);\n\nprotected:\n\tDocumentData *getShownDocument() const;\n\n\tint content_width() const;\n\tint content_height() const;\n\tint content_duration() const;\n\n};\n\nclass DeleteSavedGifClickHandler : public LeftButtonClickHandler {\npublic:\n\tDeleteSavedGifClickHandler(not_null<DocumentData*> data) : _data(data) {\n\t}\n\nprotected:\n\tvoid onClickImpl() const override;\n\nprivate:\n\tconst not_null<DocumentData*> _data;\n\n};\n\nclass Gif final : public FileBase {\npublic:\n\tGif(not_null<Context*> context, std::shared_ptr<Result> result);\n\tGif(\n\t\tnot_null<Context*> context,\n\t\tnot_null<DocumentData*> document,\n\t\tbool hasDeleteButton);\n\n\tvoid setPosition(int32 position) override;\n\tvoid initDimensions() override;\n\n\tbool isFullLine() const override {\n\t\treturn false;\n\t}\n\tbool hasRightSkip() const override {\n\t\treturn true;\n\t}\n\n\tvoid paint(Painter &p, const QRect &clip, const PaintContext *context) const override;\n\tTextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\t// ClickHandlerHost interface\n\tvoid clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;\n\n\tint resizeGetHeight(int width) override;\n\n\tvoid unloadHeavyPart() override;\n\n\tQRect innerContentRect() const override;\n\nprivate:\n\tenum class StateFlag {\n\t\tOver = (1 << 0),\n\t\tDeleteOver = (1 << 1),\n\t};\n\tusing StateFlags = base::flags<StateFlag>;\n\tfriend inline constexpr auto is_flag_type(StateFlag) { return true; };\n\n\tstruct AnimationData {\n\t\ttemplate <typename Callback>\n\t\tAnimationData(Callback &&callback)\n\t\t\t: radial(std::forward<Callback>(callback)) {\n\t\t}\n\t\tbool over = false;\n\t\tUi::Animations::Simple _a_over;\n\t\tUi::RadialAnimation radial;\n\t};\n\n\tvoid ensureDataMediaCreated(not_null<DocumentData*> document) const;\n\tQSize countFrameSize() const;\n\n\tvoid validateThumbnail(\n\t\tImage *image,\n\t\tQSize size,\n\t\tQSize frame,\n\t\tbool good) const;\n\tvoid prepareThumbnail(QSize size, QSize frame) const;\n\n\tvoid ensureAnimation() const;\n\tbool isRadialAnimation() const;\n\tvoid radialAnimationCallback(crl::time now) const;\n\n\tvoid clipCallback(Media::Clip::Notification notification);\n\n\tStateFlags _state;\n\n\tMedia::Clip::ReaderPointer _gif;\n\tClickHandlerPtr _delete;\n\tmutable QImage _thumb;\n\tmutable bool _thumbGood = false;\n\n\tmutable std::shared_ptr<Data::DocumentMedia> _dataMedia;\n\n\tmutable std::unique_ptr<AnimationData> _animation;\n\tmutable Ui::Animations::Simple _a_deleteOver;\n\n};\n\nclass Photo : public ItemBase {\npublic:\n\tPhoto(not_null<Context*> context, std::shared_ptr<Result> result);\n\t// Not used anywhere currently.\n\t//Photo(not_null<Context*> context, not_null<PhotoData*> photo);\n\n\tvoid initDimensions() override;\n\n\tbool isFullLine() const override {\n\t\treturn false;\n\t}\n\tbool hasRightSkip() const override {\n\t\treturn true;\n\t}\n\n\tvoid paint(Painter &p, const QRect &clip, const PaintContext *context) const override;\n\tTextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\tPhotoData *getShownPhoto() const;\n\n\tQSize countFrameSize() const;\n\n\tmutable QPixmap _thumb;\n\tmutable bool _thumbGood = false;\n\tvoid prepareThumbnail(QSize size, QSize frame) const;\n\tvoid validateThumbnail(\n\t\tImage *image,\n\t\tQSize size,\n\t\tQSize frame,\n\t\tbool good) const;\n\n\tmutable std::shared_ptr<Data::PhotoMedia> _photoMedia;\n\n};\n\nclass Sticker : public FileBase {\npublic:\n\tSticker(not_null<Context*> context, std::shared_ptr<Result> result);\n\t~Sticker();\n\t// Not used anywhere currently.\n\t//Sticker(not_null<Context*> context, not_null<DocumentData*> document);\n\n\tvoid initDimensions() override;\n\n\tbool isFullLine() const override {\n\t\treturn false;\n\t}\n\tbool hasRightSkip() const override {\n\t\treturn false;\n\t}\n\tvoid preload() const override;\n\n\tvoid paint(Painter &p, const QRect &clip, const PaintContext *context) const override;\n\tTextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\t// ClickHandlerHost interface\n\tvoid clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;\n\n\tvoid unloadHeavyPart() override;\n\n\tQRect innerContentRect() const override;\n\nprivate:\n\tvoid ensureDataMediaCreated(not_null<DocumentData*> document) const;\n\tvoid setupLottie() const;\n\tvoid setupWebm() const;\n\tQSize getThumbSize() const;\n\tQSize boundingBox() const;\n\tvoid prepareThumbnail() const;\n\tvoid clipCallback(Media::Clip::Notification notification);\n\n\tmutable Ui::Animations::Simple _a_over;\n\tmutable bool _active = false;\n\n\tmutable QPixmap _thumb;\n\tmutable bool _thumbLoaded = false;\n\n\tmutable std::unique_ptr<Lottie::SinglePlayer> _lottie;\n\tMedia::Clip::ReaderPointer _webm;\n\tmutable std::shared_ptr<Data::DocumentMedia> _dataMedia;\n\tmutable rpl::lifetime _lifetime;\n\n};\n\nclass Video : public FileBase {\npublic:\n\tVideo(not_null<Context*> context, std::shared_ptr<Result> result);\n\n\tvoid initDimensions() override;\n\n\tvoid paint(Painter &p, const QRect &clip, const PaintContext *context) const override;\n\tTextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\tClickHandlerPtr _link;\n\n\tmutable QPixmap _thumb;\n\tmutable std::shared_ptr<Data::DocumentMedia> _documentMedia;\n\tUi::Text::String _title, _description;\n\tQString _duration;\n\tint _durationWidth = 0;\n\n\t[[nodiscard]] bool withThumbnail() const;\n\tvoid prepareThumbnail(QSize size) const;\n\n};\n\nclass CancelFileClickHandler : public LeftButtonClickHandler {\npublic:\n\tCancelFileClickHandler(not_null<Result*> result) : _result(result) {\n\t}\n\nprotected:\n\tvoid onClickImpl() const override;\n\nprivate:\n\tnot_null<Result*> _result;\n\n};\n\nclass File : public FileBase {\npublic:\n\tFile(not_null<Context*> context, std::shared_ptr<Result> result);\n\t~File();\n\n\tvoid initDimensions() override;\n\n\tvoid paint(Painter &p, const QRect &clip, const PaintContext *context) const override;\n\tTextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\t// ClickHandlerHost interface\n\tvoid clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;\n\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\tvoid thumbAnimationCallback();\n\tvoid radialAnimationCallback(crl::time now) const;\n\n\tvoid ensureAnimation() const;\n\tvoid ensureDataMediaCreated() const;\n\tvoid checkAnimationFinished() const;\n\tbool updateStatusText() const;\n\n\tbool isRadialAnimation() const {\n\t\tif (_animation) {\n\t\t\tif (_animation->radial.animating()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tcheckAnimationFinished();\n\t\t}\n\t\treturn false;\n\t}\n\tbool isThumbAnimation() const {\n\t\tif (_animation) {\n\t\t\tif (_animation->a_thumbOver.animating()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tcheckAnimationFinished();\n\t\t}\n\t\treturn false;\n\t}\n\n\tstruct AnimationData {\n\t\ttemplate <typename Callback>\n\t\tAnimationData(Callback &&radialCallback)\n\t\t: radial(std::forward<Callback>(radialCallback)) {\n\t\t}\n\t\tUi::Animations::Simple a_thumbOver;\n\t\tUi::RadialAnimation radial;\n\t};\n\tmutable std::unique_ptr<AnimationData> _animation;\n\n\tUi::Text::String _title, _description;\n\tClickHandlerPtr _cancel;\n\n\t// >= 0 will contain download / upload string, _statusSize = loaded bytes\n\t// < 0 will contain played string, _statusSize = -(seconds + 1) played\n\t// 0xFFFFFFF0LL will contain status for not yet downloaded file\n\t// 0xFFFFFFF1LL will contain status for already downloaded file\n\t// 0xFFFFFFF2LL will contain status for failed to download / upload file\n\tmutable int64 _statusSize = 0;\n\tmutable QString _statusText;\n\n\t// duration = -1 - no duration, duration = -2 - \"GIF\" duration\n\tvoid setStatusSize(\n\t\tint64 newSize,\n\t\tint64 fullSize,\n\t\tTimeId duration,\n\t\tTimeId realDuration) const;\n\n\tnot_null<DocumentData*> _document;\n\tmutable std::shared_ptr<Data::DocumentMedia> _documentMedia;\n\n};\n\nclass Contact : public ItemBase {\npublic:\n\tContact(not_null<Context*> context, std::shared_ptr<Result> result);\n\n\tvoid initDimensions() override;\n\n\tvoid paint(Painter &p, const QRect &clip, const PaintContext *context) const override;\n\tTextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\nprivate:\n\tmutable QPixmap _thumb;\n\tUi::Text::String _title, _description;\n\n\tvoid prepareThumbnail(int width, int height) const;\n\n};\n\nclass Thumbnail : public ItemBase {\npublic:\n\tThumbnail(not_null<Context*> context, std::shared_ptr<Result> result);\n\n\tvoid initDimensions() override;\n\n\tbool isFullLine() const override {\n\t\treturn false;\n\t}\n\tbool hasRightSkip() const override {\n\t\treturn true;\n\t}\n\n\tvoid paint(\n\t\tPainter &p,\n\t\tconst QRect &clip,\n\t\tconst PaintContext *context) const override;\n\tTextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\tQSize countFrameSize() const;\n\tvoid prepareThumbnail(QSize size, QSize frame) const;\n\tvoid validateThumbnail(\n\t\tImage *image,\n\t\tQSize size,\n\t\tQSize frame,\n\t\tbool good) const;\n\n\tmutable QPixmap _thumb;\n\tmutable bool _thumbGood = false;\n\tmutable std::shared_ptr<Data::PhotoMedia> _photoMedia;\n\tmutable std::shared_ptr<Data::DocumentMedia> _documentMedia;\n\n};\n\nclass Article : public ItemBase {\npublic:\n\tArticle(\n\t\tnot_null<Context*> context,\n\t\tstd::shared_ptr<Result> result,\n\t\tbool withThumb);\n\n\tvoid initDimensions() override;\n\tint resizeGetHeight(int width) override;\n\n\tvoid paint(Painter &p, const QRect &clip, const PaintContext *context) const override;\n\tTextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\tClickHandlerPtr _url, _link;\n\n\tbool _withThumb;\n\tmutable QPixmap _thumb;\n\tUi::Text::String _title, _description;\n\tQString _thumbLetter, _urlText;\n\tint32 _urlWidth;\n\n\tvoid prepareThumbnail(int width, int height) const;\n\tvoid prepareMediaThumbnail(int width, int height) const;\n\n\tmutable std::shared_ptr<Data::PhotoMedia> _photoMedia;\n\tmutable std::shared_ptr<Data::DocumentMedia> _documentMedia;\n\n};\n\nclass Game : public ItemBase {\npublic:\n\tGame(not_null<Context*> context, std::shared_ptr<Result> result);\n\n\tvoid setPosition(int32 position) override;\n\tvoid initDimensions() override;\n\n\tvoid paint(Painter &p, const QRect &clip, const PaintContext *context) const override;\n\tTextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\tvoid unloadHeavyPart() override;\n\nprivate:\n\tvoid ensureDataMediaCreated(not_null<PhotoData*> photo) const;\n\tvoid ensureDataMediaCreated(not_null<DocumentData*> document) const;\n\tvoid countFrameSize();\n\n\tvoid prepareThumbnail(QSize size) const;\n\tvoid validateThumbnail(Image *image, QSize size, bool good) const;\n\n\tbool isRadialAnimation() const;\n\tvoid radialAnimationCallback(crl::time now) const;\n\n\tvoid clipCallback(Media::Clip::Notification notification);\n\n\tMedia::Clip::ReaderPointer _gif;\n\tmutable std::shared_ptr<Data::PhotoMedia> _photoMedia;\n\tmutable std::shared_ptr<Data::DocumentMedia> _documentMedia;\n\tmutable QImage _thumb;\n\tmutable bool _thumbGood = false;\n\tmutable std::unique_ptr<Ui::RadialAnimation> _radial;\n\tUi::Text::String _title, _description;\n\n\tQSize _frameSize;\n\n};\n\n} // namespace internal\n} // namespace Layout\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"inline_bots/inline_bot_layout_item.h\"\n\n#include \"base/never_freed_pointer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_document.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_document_media.h\"\n#include \"core/click_handler_types.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"inline_bots/inline_bot_layout_internal.h\"\n#include \"storage/localstorage.h\"\n#include \"mainwidget.h\"\n#include \"ui/image/image.h\"\n#include \"ui/empty_userpic.h\"\n\nnamespace InlineBots {\nnamespace Layout {\nnamespace {\n\nbase::NeverFreedPointer<DocumentItems> documentItemsMap;\n\n} // namespace\n\nstd::shared_ptr<Result> ItemBase::getResult() const {\n\treturn _result;\n}\n\nDocumentData *ItemBase::getDocument() const {\n\treturn _document;\n}\n\nPhotoData *ItemBase::getPhoto() const {\n\treturn _photo;\n}\n\nDocumentData *ItemBase::getPreviewDocument() const {\n\tif (_document) {\n\t\treturn _document;\n\t} else if (_result) {\n\t\treturn _result->_document;\n\t}\n\treturn nullptr;\n}\n\nPhotoData *ItemBase::getPreviewPhoto() const {\n\tif (_photo) {\n\t\treturn _photo;\n\t} else if (_result) {\n\t\treturn _result->_photo;\n\t}\n\treturn nullptr;\n}\n\nvoid ItemBase::preload() const {\n\tconst auto origin = fileOrigin();\n\tif (_result) {\n\t\tif (const auto photo = _result->_photo) {\n\t\t\tif (photo->hasExact(Data::PhotoSize::Thumbnail)) {\n\t\t\t\tphoto->load(Data::PhotoSize::Thumbnail, origin);\n\t\t\t}\n\t\t} else if (const auto document = _result->_document) {\n\t\t\tdocument->loadThumbnail(origin);\n\t\t} else if (auto &thumb = _result->_thumbnail; !thumb.empty()) {\n\t\t\tthumb.load(_result->_session, origin);\n\t\t}\n\t} else if (_document) {\n\t\t_document->loadThumbnail(origin);\n\t} else if (_photo && _photo->hasExact(Data::PhotoSize::Thumbnail)) {\n\t\t_photo->load(Data::PhotoSize::Thumbnail, origin);\n\t}\n}\n\nvoid ItemBase::update() const {\n\tif (_position >= 0) {\n\t\tcontext()->inlineItemRepaint(this);\n\t}\n}\n\nvoid ItemBase::layoutChanged() {\n\tif (_position >= 0) {\n\t\tcontext()->inlineItemLayoutChanged(this);\n\t}\n}\n\nstd::unique_ptr<ItemBase> ItemBase::createLayout(\n\t\tnot_null<Context*> context,\n\t\tstd::shared_ptr<Result> result,\n\t\tbool forceThumb,\n\t\tstd::optional<bool> gallery) {\n\tusing Type = Result::Type;\n\n\tif (gallery.has_value()) {\n\t\tif (!*gallery) {\n\t\t\t// Force list mode: render gallery types as Article.\n\t\t\tswitch (result->_type) {\n\t\t\tcase Type::Photo:\n\t\t\tcase Type::Sticker:\n\t\t\tcase Type::Gif:\n\t\t\t\treturn std::make_unique<internal::Article>(\n\t\t\t\t\tcontext,\n\t\t\t\t\tstd::move(result),\n\t\t\t\t\tforceThumb);\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t\t}\n\t\t} else {\n\t\t\t// Force gallery mode: render list types as Thumbnail.\n\t\t\tswitch (result->_type) {\n\t\t\tcase Type::Article:\n\t\t\tcase Type::Geo:\n\t\t\tcase Type::Venue:\n\t\t\tcase Type::Video:\n\t\t\tcase Type::Audio:\n\t\t\tcase Type::File:\n\t\t\tcase Type::Contact:\n\t\t\tcase Type::Game:\n\t\t\t\treturn std::make_unique<internal::Thumbnail>(\n\t\t\t\t\tcontext,\n\t\t\t\t\tstd::move(result));\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tswitch (result->_type) {\n\tcase Type::Photo:\n\t\treturn std::make_unique<internal::Photo>(context, std::move(result));\n\tcase Type::Audio:\n\tcase Type::File:\n\t\treturn std::make_unique<internal::File>(context, std::move(result));\n\tcase Type::Video:\n\t\treturn std::make_unique<internal::Video>(context, std::move(result));\n\tcase Type::Sticker:\n\t\treturn std::make_unique<internal::Sticker>(\n\t\t\tcontext,\n\t\t\tstd::move(result));\n\tcase Type::Gif:\n\t\treturn std::make_unique<internal::Gif>(context, std::move(result));\n\tcase Type::Article:\n\tcase Type::Geo:\n\tcase Type::Venue:\n\t\treturn std::make_unique<internal::Article>(\n\t\t\tcontext,\n\t\t\tstd::move(result),\n\t\t\tforceThumb);\n\tcase Type::Game:\n\t\treturn std::make_unique<internal::Game>(context, std::move(result));\n\tcase Type::Contact:\n\t\treturn std::make_unique<internal::Contact>(\n\t\t\tcontext,\n\t\t\tstd::move(result));\n\t}\n\treturn nullptr;\n}\n\nstd::unique_ptr<ItemBase> ItemBase::createLayoutGif(\n\t\tnot_null<Context*> context,\n\t\tnot_null<DocumentData*> document) {\n\treturn std::make_unique<internal::Gif>(context, document, true);\n}\n\nDocumentData *ItemBase::getResultDocument() const {\n\treturn _result ? _result->_document : nullptr;\n}\n\nPhotoData *ItemBase::getResultPhoto() const {\n\treturn _result ? _result->_photo : nullptr;\n}\n\nbool ItemBase::hasResultThumb() const {\n\treturn _result\n\t\t&& (!_result->_thumbnail.empty()\n\t\t\t|| !_result->_locationThumbnail.empty());\n}\n\nQImage *ItemBase::getResultThumb(Data::FileOrigin origin) const {\n\tif (_result && !_thumbnail) {\n\t\tif (!_result->_thumbnail.empty()) {\n\t\t\t_thumbnail = _result->_thumbnail.createView();\n\t\t\t_result->_thumbnail.load(_result->_session, origin);\n\t\t} else if (!_result->_locationThumbnail.empty()) {\n\t\t\t_thumbnail = _result->_locationThumbnail.createView();\n\t\t\t_result->_locationThumbnail.load(_result->_session, origin);\n\t\t}\n\t}\n\treturn (_thumbnail && !_thumbnail->isNull())\n\t\t? _thumbnail.get()\n\t\t: nullptr;\n}\n\nQPixmap ItemBase::getResultContactAvatar(int width, int height) const {\n\tif (_result->_type == Result::Type::Contact) {\n\t\tauto result = Ui::EmptyUserpic(\n\t\t\tUi::EmptyUserpic::UserpicColor(Ui::EmptyUserpic::ColorIndex(\n\t\t\t\tBareId(qHash(_result->_id)))),\n\t\t\t_result->getLayoutTitle()\n\t\t).generate(width);\n\t\tif (result.height() != height * style::DevicePixelRatio()) {\n\t\t\tresult = result.scaled(\n\t\t\t\tQSize(width, height) * style::DevicePixelRatio(),\n\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\tQt::SmoothTransformation);\n\t\t}\n\t\treturn result;\n\t}\n\treturn QPixmap();\n}\n\nint ItemBase::getResultDuration() const {\n\treturn 0;\n}\n\nQString ItemBase::getResultUrl() const {\n\treturn _result->_url;\n}\n\nClickHandlerPtr ItemBase::getResultUrlHandler() const {\n\tif (!_result->_url.isEmpty()) {\n\t\treturn std::make_shared<UrlClickHandler>(_result->_url);\n\t}\n\treturn ClickHandlerPtr();\n}\n\nClickHandlerPtr ItemBase::getResultPreviewHandler() const {\n\tif (!_result->_content_url.isEmpty()) {\n\t\treturn std::make_shared<UrlClickHandler>(\n\t\t\t_result->_content_url,\n\t\t\tfalse);\n\t} else if (const auto document = _result->_document\n\t\t; document && document->createMediaView()->canBePlayed()) {\n\t\treturn std::make_shared<OpenFileClickHandler>();\n\t} else if (_result->_photo) {\n\t\treturn std::make_shared<OpenFileClickHandler>();\n\t}\n\treturn ClickHandlerPtr();\n}\n\nQString ItemBase::getResultThumbLetter() const {\n\tauto parts = QStringView(_result->_url).split('/');\n\tif (!parts.isEmpty()) {\n\t\tauto domain = parts.at(0);\n\t\tif (parts.size() > 2 && domain.endsWith(':') && parts.at(1).isEmpty()) { // http:// and others\n\t\t\tdomain = parts.at(2);\n\t\t}\n\n\t\tparts = domain.split('@').constLast().split('.');\n\t\tif (parts.size() > 1) {\n\t\t\treturn parts.at(parts.size() - 2).at(0).toUpper();\n\t\t}\n\t}\n\tif (!_result->_title.isEmpty()) {\n\t\treturn _result->_title.at(0).toUpper();\n\t}\n\treturn QString();\n}\n\nData::FileOrigin ItemBase::fileOrigin() const {\n\treturn _context->inlineItemFileOrigin();\n}\n\nconst DocumentItems *documentItems() {\n\treturn documentItemsMap.data();\n}\n\nnamespace internal {\n\nvoid regDocumentItem(\n\t\tnot_null<const DocumentData*> document,\n\t\tnot_null<ItemBase*> item) {\n\tdocumentItemsMap.createIfNull();\n\t(*documentItemsMap)[document].insert(item);\n}\n\nvoid unregDocumentItem(\n\t\tnot_null<const DocumentData*> document,\n\t\tnot_null<ItemBase*> item) {\n\tif (documentItemsMap) {\n\t\tauto i = documentItemsMap->find(document);\n\t\tif (i != documentItemsMap->cend()) {\n\t\t\tif (i->second.remove(item) && i->second.empty()) {\n\t\t\t\tdocumentItemsMap->erase(i);\n\t\t\t}\n\t\t}\n\t\tif (documentItemsMap->empty()) {\n\t\t\tdocumentItemsMap.clear();\n\t\t}\n\t}\n}\n\n} // namespace internal\n\n} // namespace Layout\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"layout/layout_item_base.h\"\n#include \"ui/text/text.h\"\n\nclass Image;\n\nnamespace Ui {\nclass PathShiftGradient;\n} // namespace Ui\n\nnamespace InlineBots {\n\nclass Result;\n\nnamespace Layout {\n\nclass ItemBase;\n\nclass PaintContext : public PaintContextBase {\npublic:\n\tPaintContext(crl::time ms, bool selecting, bool paused, bool lastRow)\n\t: PaintContextBase(ms, selecting)\n\t, paused(paused)\n\t, lastRow(lastRow) {\n\t}\n\tbool paused, lastRow;\n\tUi::PathShiftGradient *pathGradient = nullptr;\n\n};\n\n// this type used as a flag, we dynamic_cast<> to it\nclass SendClickHandler : public ClickHandler {\npublic:\n\tvoid onClick(ClickContext context) const override {\n\t}\n};\n\nclass OpenFileClickHandler : public ClickHandler {\npublic:\n\tvoid onClick(ClickContext context) const override {\n\t}\n};\n\nclass Context {\npublic:\n\tvirtual void inlineItemLayoutChanged(const ItemBase *layout) = 0;\n\tvirtual bool inlineItemVisible(const ItemBase *item) = 0;\n\tvirtual void inlineItemRepaint(const ItemBase *item) = 0;\n\tvirtual Data::FileOrigin inlineItemFileOrigin() = 0;\n};\n\nclass ItemBase : public LayoutItemBase {\npublic:\n\tItemBase(not_null<Context*> context, std::shared_ptr<Result> result)\n\t: _result(result)\n\t, _context(context) {\n\t}\n\tItemBase(not_null<Context*> context, not_null<DocumentData*> document)\n\t: _document(document)\n\t, _context(context) {\n\t}\n\t// Not used anywhere currently.\n\t//ItemBase(not_null<Context*> context, PhotoData *photo) : _photo(photo), _context(context) {\n\t//}\n\n\tvirtual void paint(Painter &p, const QRect &clip, const PaintContext *context) const = 0;\n\n\tvirtual bool isFullLine() const {\n\t\treturn true;\n\t}\n\tvirtual bool hasRightSkip() const {\n\t\treturn false;\n\t}\n\n\tstd::shared_ptr<Result> getResult() const;\n\tDocumentData *getDocument() const;\n\tPhotoData *getPhoto() const;\n\n\t// Get document or photo (possibly from InlineBots::Result) for\n\t// showing sticker / GIF / photo preview by long mouse press.\n\tDocumentData *getPreviewDocument() const;\n\tPhotoData *getPreviewPhoto() const;\n\n\tvirtual void preload() const;\n\tvirtual void unloadHeavyPart() {\n\t\t_thumbnail = nullptr;\n\t}\n\n\tvoid update() const;\n\tvoid layoutChanged();\n\n\t// ClickHandlerHost interface\n\tvoid clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override {\n\t\tupdate();\n\t}\n\tvoid clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override {\n\t\tupdate();\n\t}\n\n\tvirtual QRect innerContentRect() const {\n\t\t// Only stickers are supported for now.\n\t\tUnexpected(\"Unsupported type to get a rect of inner content.\");\n\t}\n\n\tstatic std::unique_ptr<ItemBase> createLayout(\n\t\tnot_null<Context*> context,\n\t\tstd::shared_ptr<Result> result,\n\t\tbool forceThumb,\n\t\tstd::optional<bool> gallery = std::nullopt);\n\tstatic std::unique_ptr<ItemBase> createLayoutGif(\n\t\tnot_null<Context*> context,\n\t\tnot_null<DocumentData*> document);\n\nprotected:\n\tDocumentData *getResultDocument() const;\n\tPhotoData *getResultPhoto() const;\n\tbool hasResultThumb() const;\n\tQImage *getResultThumb(Data::FileOrigin origin) const;\n\tQPixmap getResultContactAvatar(int width, int height) const;\n\tint getResultDuration() const;\n\tQString getResultUrl() const;\n\tClickHandlerPtr getResultUrlHandler() const;\n\tClickHandlerPtr getResultPreviewHandler() const;\n\tQString getResultThumbLetter() const;\n\n\tnot_null<Context*> context() const {\n\t\treturn _context;\n\t}\n\tData::FileOrigin fileOrigin() const;\n\n\tstd::shared_ptr<Result> _result;\n\tDocumentData *_document = nullptr;\n\tPhotoData *_photo = nullptr;\n\n\tClickHandlerPtr _send = ClickHandlerPtr{ new SendClickHandler() };\n\tClickHandlerPtr _open = ClickHandlerPtr{ new OpenFileClickHandler() };\n\nprivate:\n\tnot_null<Context*> _context;\n\tmutable std::shared_ptr<QImage> _thumbnail;\n\n};\n\nusing DocumentItems = std::map<\n\tnot_null<const DocumentData*>,\n\tbase::flat_set<not_null<ItemBase*>>>;\nconst DocumentItems *documentItems();\n\nnamespace internal {\n\nvoid regDocumentItem(\n\tnot_null<const DocumentData*> document,\n\tnot_null<ItemBase*> item);\nvoid unregDocumentItem(\n\tnot_null<const DocumentData*> document,\n\tnot_null<ItemBase*> item);\n\n} // namespace internal\n} // namespace Layout\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_bot_result.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"inline_bots/inline_bot_result.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"base/random.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_document_media.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_reply_markup.h\"\n#include \"inline_bots/inline_bot_layout_item.h\"\n#include \"inline_bots/inline_bot_send_data.h\"\n#include \"storage/file_download.h\"\n#include \"core/file_utilities.h\"\n#include \"core/mime_type.h\"\n#include \"ui/image/image.h\"\n#include \"ui/image/image_location_factory.h\"\n#include \"mainwidget.h\"\n#include \"main/main_session.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace InlineBots {\nnamespace {\n\nconst auto kVideoThumbMime = \"video/mp4\"_q;\n\nQString GetContentUrl(const MTPWebDocument &document) {\n\tswitch (document.type()) {\n\tcase mtpc_webDocument:\n\t\treturn qs(document.c_webDocument().vurl());\n\tcase mtpc_webDocumentNoProxy:\n\t\treturn qs(document.c_webDocumentNoProxy().vurl());\n\t}\n\tUnexpected(\"Type in GetContentUrl.\");\n}\n\n} // namespace\n\nResult::Result(not_null<Main::Session*> session, const Creator &creator)\n: _session(session)\n, _queryId(creator.queryId)\n, _type(creator.type) {\n}\n\nstd::shared_ptr<Result> Result::Create(\n\t\tnot_null<Main::Session*> session,\n\t\tuint64 queryId,\n\t\tconst MTPBotInlineResult &data) {\n\tusing Type = Result::Type;\n\n\tconst auto type = [&] {\n\t\tstatic const auto kStringToTypeMap = base::flat_map<QString, Type>{\n\t\t\t{ u\"photo\"_q, Type::Photo },\n\t\t\t{ u\"video\"_q, Type::Video },\n\t\t\t{ u\"audio\"_q, Type::Audio },\n\t\t\t{ u\"voice\"_q, Type::Audio },\n\t\t\t{ u\"sticker\"_q, Type::Sticker },\n\t\t\t{ u\"file\"_q, Type::File },\n\t\t\t{ u\"gif\"_q, Type::Gif },\n\t\t\t{ u\"article\"_q, Type::Article },\n\t\t\t{ u\"contact\"_q, Type::Contact },\n\t\t\t{ u\"venue\"_q, Type::Venue },\n\t\t\t{ u\"geo\"_q, Type::Geo },\n\t\t\t{ u\"game\"_q, Type::Game },\n\t\t};\n\t\tconst auto type = data.match([](const auto &data) {\n\t\t\treturn qs(data.vtype());\n\t\t});\n\t\tconst auto i = kStringToTypeMap.find(type);\n\t\treturn (i != kStringToTypeMap.end()) ? i->second : Type::Unknown;\n\t}();\n\tif (type == Type::Unknown) {\n\t\treturn nullptr;\n\t}\n\n\tauto result = std::make_shared<Result>(\n\t\tsession,\n\t\tCreator{ queryId, type });\n\tconst auto message = data.match([&](const MTPDbotInlineResult &data) {\n\t\tresult->_id = qs(data.vid());\n\t\tresult->_title = qs(data.vtitle().value_or_empty());\n\t\tresult->_description = qs(data.vdescription().value_or_empty());\n\t\tresult->_url = qs(data.vurl().value_or_empty());\n\t\tconst auto thumbMime = [&] {\n\t\t\tif (const auto thumb = data.vthumb()) {\n\t\t\t\treturn thumb->match([&](const auto &data) {\n\t\t\t\t\treturn data.vmime_type().v;\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn QByteArray();\n\t\t}();\n\t\tconst auto contentMime = [&] {\n\t\t\tif (const auto content = data.vcontent()) {\n\t\t\t\treturn content->match([&](const auto &data) {\n\t\t\t\t\treturn data.vmime_type().v;\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn QByteArray();\n\t\t}();\n\t\tconst auto imageThumb = !thumbMime.isEmpty()\n\t\t\t&& (thumbMime != kVideoThumbMime);\n\t\tconst auto videoThumb = !thumbMime.isEmpty() && !imageThumb;\n\t\tif (const auto content = data.vcontent()) {\n\t\t\tresult->_content_url = GetContentUrl(*content);\n\t\t\tif (result->_type == Type::Photo) {\n\t\t\t\tresult->_photo = session->data().photoFromWeb(\n\t\t\t\t\t*content,\n\t\t\t\t\t(imageThumb\n\t\t\t\t\t\t? Images::FromWebDocument(*data.vthumb())\n\t\t\t\t\t\t: ImageLocation()));\n\t\t\t} else if (contentMime != \"text/html\"_q) {\n\t\t\t\tresult->_document = session->data().documentFromWeb(\n\t\t\t\t\tresult->adjustAttributes(*content),\n\t\t\t\t\t(imageThumb\n\t\t\t\t\t\t? Images::FromWebDocument(*data.vthumb())\n\t\t\t\t\t\t: ImageLocation()),\n\t\t\t\t\t(videoThumb\n\t\t\t\t\t\t? Images::FromWebDocument(*data.vthumb())\n\t\t\t\t\t\t: ImageLocation()));\n\t\t\t}\n\t\t}\n\t\tif (!result->_photo && !result->_document && imageThumb) {\n\t\t\tresult->_thumbnail.update(result->_session, ImageWithLocation{\n\t\t\t\t.location = Images::FromWebDocument(*data.vthumb())\n\t\t\t});\n\t\t}\n\t\treturn &data.vsend_message();\n\t}, [&](const MTPDbotInlineMediaResult &data) {\n\t\tresult->_id = qs(data.vid());\n\t\tresult->_title = qs(data.vtitle().value_or_empty());\n\t\tresult->_description = qs(data.vdescription().value_or_empty());\n\t\tif (const auto photo = data.vphoto()) {\n\t\t\tresult->_photo = session->data().processPhoto(*photo);\n\t\t}\n\t\tif (const auto document = data.vdocument()) {\n\t\t\tresult->_document = session->data().processDocument(*document);\n\t\t}\n\t\treturn &data.vsend_message();\n\t});\n\tif ((result->_photo && result->_photo->isNull())\n\t\t|| (result->_document && result->_document->isNull())) {\n\t\treturn nullptr;\n\t}\n\n\t// Ensure required media fields for layouts.\n\tif (result->_type == Type::Photo) {\n\t\tif (!result->_photo) {\n\t\t\treturn nullptr;\n\t\t}\n\t} else if (result->_type == Type::Audio\n\t\t|| result->_type == Type::File\n\t\t|| result->_type == Type::Sticker\n\t\t|| result->_type == Type::Gif) {\n\t\tif (!result->_document) {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\n\tmessage->match([&](const MTPDbotInlineMessageMediaAuto &data) {\n\t\tconst auto message = qs(data.vmessage());\n\t\tconst auto entities = Api::EntitiesFromMTP(\n\t\t\tsession,\n\t\t\tdata.ventities().value_or_empty());\n\t\tif (result->_type == Type::Photo) {\n\t\t\tif (result->_photo) {\n\t\t\t\tresult->sendData = std::make_unique<internal::SendPhoto>(\n\t\t\t\t\tsession,\n\t\t\t\t\tresult->_photo,\n\t\t\t\t\tmessage,\n\t\t\t\t\tentities);\n\t\t\t} else {\n\t\t\t\tLOG((\"Inline Error: No 'photo' in media-auto, type=photo.\"));\n\t\t\t}\n\t\t} else if (result->_type == Type::Game) {\n\t\t\tresult->createGame(session);\n\t\t\tresult->sendData = std::make_unique<internal::SendGame>(\n\t\t\t\tsession,\n\t\t\t\tresult->_game);\n\t\t} else {\n\t\t\tif (result->_document) {\n\t\t\t\tresult->sendData = std::make_unique<internal::SendFile>(\n\t\t\t\t\tsession,\n\t\t\t\t\tresult->_document,\n\t\t\t\t\tmessage,\n\t\t\t\t\tentities);\n\t\t\t} else {\n\t\t\t\tLOG((\"Inline Error: No 'document' in media-auto, type=%1.\"\n\t\t\t\t\t).arg(int(result->_type)));\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDbotInlineMessageText &data) {\n\t\tresult->sendData = std::make_unique<internal::SendText>(\n\t\t\tsession,\n\t\t\tqs(data.vmessage()),\n\t\t\tApi::EntitiesFromMTP(session, data.ventities().value_or_empty()),\n\t\t\tdata.is_no_webpage());\n\t}, [&](const MTPDbotInlineMessageMediaGeo &data) {\n\t\tdata.vgeo().match([&](const MTPDgeoPoint &geo) {\n\t\t\tif (const auto period = data.vperiod()) {\n\t\t\t\tresult->sendData = std::make_unique<internal::SendGeo>(\n\t\t\t\t\tsession,\n\t\t\t\t\tgeo,\n\t\t\t\t\tperiod->v,\n\t\t\t\t\t(data.vheading()\n\t\t\t\t\t\t? std::make_optional(data.vheading()->v)\n\t\t\t\t\t\t: std::nullopt),\n\t\t\t\t\t(data.vproximity_notification_radius()\n\t\t\t\t\t\t? std::make_optional(\n\t\t\t\t\t\t\tdata.vproximity_notification_radius()->v)\n\t\t\t\t\t\t: std::nullopt));\n\t\t\t} else {\n\t\t\t\tresult->sendData = std::make_unique<internal::SendGeo>(\n\t\t\t\t\tsession,\n\t\t\t\t\tgeo);\n\t\t\t}\n\t\t}, [&](const MTPDgeoPointEmpty &) {\n\t\t\tLOG((\"Inline Error: Empty 'geo' in media-geo.\"));\n\t\t});\n\t}, [&](const MTPDbotInlineMessageMediaVenue &data) {\n\t\tdata.vgeo().match([&](const MTPDgeoPoint &geo) {\n\t\t\tresult->sendData = std::make_unique<internal::SendVenue>(\n\t\t\t\tsession,\n\t\t\t\tgeo,\n\t\t\t\tqs(data.vvenue_id()),\n\t\t\t\tqs(data.vprovider()),\n\t\t\t\tqs(data.vtitle()),\n\t\t\t\tqs(data.vaddress()));\n\t\t}, [&](const MTPDgeoPointEmpty &) {\n\t\t\tLOG((\"Inline Error: Empty 'geo' in media-venue.\"));\n\t\t});\n\t}, [&](const MTPDbotInlineMessageMediaContact &data) {\n\t\tresult->sendData = std::make_unique<internal::SendContact>(\n\t\t\tsession,\n\t\t\tqs(data.vfirst_name()),\n\t\t\tqs(data.vlast_name()),\n\t\t\tqs(data.vphone_number()));\n\t}, [&](const MTPDbotInlineMessageMediaInvoice &data) {\n\t\tusing Flag = MTPDmessageMediaInvoice::Flag;\n\t\tconst auto media = MTP_messageMediaInvoice(\n\t\t\tMTP_flags((data.is_shipping_address_requested()\n\t\t\t\t? Flag::f_shipping_address_requested\n\t\t\t\t: Flag(0))\n\t\t\t\t| (data.is_test() ? Flag::f_test : Flag(0))\n\t\t\t\t| (data.vphoto() ? Flag::f_photo : Flag(0))),\n\t\t\tdata.vtitle(),\n\t\t\tdata.vdescription(),\n\t\t\tdata.vphoto() ? (*data.vphoto()) : MTPWebDocument(),\n\t\t\tMTPint(), // receipt_msg_id\n\t\t\tdata.vcurrency(),\n\t\t\tdata.vtotal_amount(),\n\t\t\tMTP_string(QString()), // start_param\n\t\t\tMTPMessageExtendedMedia());\n\t\tresult->sendData = std::make_unique<internal::SendInvoice>(\n\t\t\tsession,\n\t\t\tmedia);\n\t}, [&](const MTPDbotInlineMessageMediaWebPage &data) {\n\t\tresult->sendData = std::make_unique<internal::SendText>(\n\t\t\tsession,\n\t\t\tqs(data.vmessage()),\n\t\t\tApi::EntitiesFromMTP(session, data.ventities().value_or_empty()),\n\t\t\tfalse);\n\t});\n\n\tif (!result->sendData || !result->sendData->isValid()) {\n\t\treturn nullptr;\n\t}\n\n\tmessage->match([&](const auto &data) {\n\t\tif (const auto markup = data.vreply_markup()) {\n\t\t\tresult->_replyMarkup\n\t\t\t\t= std::make_unique<HistoryMessageMarkupData>(markup);\n\t\t}\n\t});\n\n\tif (const auto point = result->getLocationPoint()) {\n\t\tconst auto scale = 1 + (cScale() * style::DevicePixelRatio()) / 200;\n\t\tconst auto zoom = 15 + (scale - 1);\n\t\tconst auto w = st::inlineThumbSize / scale;\n\t\tconst auto h = st::inlineThumbSize / scale;\n\n\t\tauto location = GeoPointLocation();\n\t\tlocation.lat = point->lat();\n\t\tlocation.lon = point->lon();\n\t\tlocation.access = point->accessHash();\n\t\tlocation.width = w;\n\t\tlocation.height = h;\n\t\tlocation.zoom = zoom;\n\t\tlocation.scale = scale;\n\t\tresult->_locationThumbnail.update(result->_session, ImageWithLocation{\n\t\t\t.location = ImageLocation({ location }, w, h)\n\t\t});\n\t}\n\n\treturn result;\n}\n\nbool Result::onChoose(Layout::ItemBase *layout) {\n\tif (_photo && _type == Type::Photo) {\n\t\tconst auto media = _photo->activeMediaView();\n\t\tif (!media || media->image(Data::PhotoSize::Thumbnail)) {\n\t\t\treturn true;\n\t\t} else if (!_photo->loading(Data::PhotoSize::Thumbnail)) {\n\t\t\t_photo->load(\n\t\t\t\tData::PhotoSize::Thumbnail,\n\t\t\t\tData::FileOrigin());\n\t\t}\n\t\treturn false;\n\t}\n\tif (_document && (\n\t\t_type == Type::Video ||\n\t\t_type == Type::Audio ||\n\t\t_type == Type::Sticker ||\n\t\t_type == Type::File ||\n\t\t_type == Type::Gif)) {\n\t\tif (_type == Type::Gif) {\n\t\t\tconst auto media = _document->activeMediaView();\n\t\t\tconst auto preview = Data::VideoPreviewState(media.get());\n\t\t\tif (!media || preview.loaded()) {\n\t\t\t\treturn true;\n\t\t\t} else if (!preview.usingThumbnail()) {\n\t\t\t\tif (preview.loading()) {\n\t\t\t\t\t_document->cancel();\n\t\t\t\t} else {\n\t\t\t\t\tDocumentSaveClickHandler::Save(\n\t\t\t\t\t\tData::FileOriginSavedGifs(),\n\t\t\t\t\t\t_document);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\treturn true;\n}\n\nMedia::View::OpenRequest Result::openRequest() {\n\tusing namespace Media::View;\n\tif (_document) {\n\t\treturn OpenRequest(nullptr, _document, nullptr, MsgId(), PeerId());\n\t} else if (_photo) {\n\t\treturn OpenRequest(nullptr, _photo, nullptr, MsgId(), PeerId());\n\t}\n\treturn {};\n}\n\nvoid Result::cancelFile() {\n\tif (_document) {\n\t\tDocumentCancelClickHandler(_document, nullptr).onClick({});\n\t} else if (_photo) {\n\t\tPhotoCancelClickHandler(_photo, nullptr).onClick({});\n\t}\n}\n\nbool Result::hasThumbDisplay() const {\n\tif (!_thumbnail.empty()\n\t\t|| _photo\n\t\t|| (_document && _document->hasThumbnail())) {\n\t\treturn true;\n\t} else if (_type == Type::Contact) {\n\t\treturn true;\n\t} else if (sendData->hasLocationCoords()) {\n\t\treturn true;\n\t}\n\treturn false;\n};\n\nvoid Result::addToHistory(\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields) const {\n\thistory->addNewLocalMessage(makeMessage(history, std::move(fields)));\n}\n\nnot_null<HistoryItem*> Result::makeMessage(\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields) const {\n\tfields.flags |= MessageFlag::FromInlineBot | MessageFlag::Local;\n\tif (_replyMarkup) {\n\t\tfields.markup = *_replyMarkup;\n\t\tif (!fields.markup.isNull()) {\n\t\t\tfields.flags |= MessageFlag::HasReplyMarkup;\n\t\t}\n\t}\n\treturn sendData->makeMessage(this, history, std::move(fields));\n}\n\nData::SendError Result::getErrorOnSend(not_null<History*> history) const {\n\treturn sendData->getErrorOnSend(this, history).value_or(\n\t\tData::RestrictionError(history->peer, ChatRestriction::SendInline));\n}\n\nstd::optional<Data::LocationPoint> Result::getLocationPoint() const {\n\treturn sendData->getLocationPoint();\n}\n\nQString Result::getLayoutTitle() const {\n\treturn sendData->getLayoutTitle(this);\n}\n\nQString Result::getLayoutDescription() const {\n\treturn sendData->getLayoutDescription(this);\n}\n\n// just to make unique_ptr see the destructors.\nResult::~Result() {\n}\n\nvoid Result::createGame(not_null<Main::Session*> session) {\n\tif (_game) {\n\t\treturn;\n\t}\n\n\tconst auto gameId = base::RandomValue<GameId>();\n\t_game = session->data().game(\n\t\tgameId,\n\t\t0,\n\t\tQString(),\n\t\t_title,\n\t\t_description,\n\t\t_photo,\n\t\t_document);\n}\n\nQSize Result::thumbBox() const {\n\treturn (_type == Type::Photo) ? QSize(100, 100) : QSize(90, 90);\n}\n\nMTPWebDocument Result::adjustAttributes(const MTPWebDocument &document) {\n\tswitch (document.type()) {\n\tcase mtpc_webDocument: {\n\t\tconst auto &data = document.c_webDocument();\n\t\treturn MTP_webDocument(\n\t\t\tdata.vurl(),\n\t\t\tdata.vaccess_hash(),\n\t\t\tdata.vsize(),\n\t\t\tdata.vmime_type(),\n\t\t\tadjustAttributes(data.vattributes(), data.vmime_type()));\n\t} break;\n\n\tcase mtpc_webDocumentNoProxy: {\n\t\tconst auto &data = document.c_webDocumentNoProxy();\n\t\treturn MTP_webDocumentNoProxy(\n\t\t\tdata.vurl(),\n\t\t\tdata.vsize(),\n\t\t\tdata.vmime_type(),\n\t\t\tadjustAttributes(data.vattributes(), data.vmime_type()));\n\t} break;\n\t}\n\tUnexpected(\"Type in InlineBots::Result::adjustAttributes.\");\n}\n\nMTPVector<MTPDocumentAttribute> Result::adjustAttributes(\n\t\tconst MTPVector<MTPDocumentAttribute> &existing,\n\t\tconst MTPstring &mimeType) {\n\tauto result = existing.v;\n\tconst auto find = [&](mtpTypeId attributeType) {\n\t\treturn ranges::find(\n\t\t\tresult,\n\t\t\tattributeType,\n\t\t\t[](const MTPDocumentAttribute &value) { return value.type(); });\n\t};\n\tconst auto exists = [&](mtpTypeId attributeType) {\n\t\treturn find(attributeType) != result.cend();\n\t};\n\tconst auto mime = qs(mimeType);\n\tif (_type == Type::Gif) {\n\t\tif (!exists(mtpc_documentAttributeFilename)) {\n\t\t\tauto filename = (mime == u\"video/mp4\"_q\n\t\t\t\t? \"animation.gif.mp4\"\n\t\t\t\t: \"animation.gif\");\n\t\t\tresult.push_back(MTP_documentAttributeFilename(\n\t\t\t\tMTP_string(filename)));\n\t\t}\n\t\tif (!exists(mtpc_documentAttributeAnimated)) {\n\t\t\tresult.push_back(MTP_documentAttributeAnimated());\n\t\t}\n\t} else if (_type == Type::Audio) {\n\t\tconst auto audio = find(mtpc_documentAttributeAudio);\n\t\tif (audio != result.cend()) {\n\t\t\tusing Flag = MTPDdocumentAttributeAudio::Flag;\n\t\t\tif (mime == u\"audio/ogg\"_q) {\n\t\t\t\t// We always treat audio/ogg as a voice message.\n\t\t\t\t// It was that way before we started to get attributes here.\n\t\t\t\tconst auto &fields = audio->c_documentAttributeAudio();\n\t\t\t\tif (!(fields.vflags().v & Flag::f_voice)) {\n\t\t\t\t\t*audio = MTP_documentAttributeAudio(\n\t\t\t\t\t\tMTP_flags(fields.vflags().v | Flag::f_voice),\n\t\t\t\t\t\tfields.vduration(),\n\t\t\t\t\t\tMTP_bytes(fields.vtitle().value_or_empty()),\n\t\t\t\t\t\tMTP_bytes(fields.vperformer().value_or_empty()),\n\t\t\t\t\t\tMTP_bytes(fields.vwaveform().value_or_empty()));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst auto &fields = audio->c_documentAttributeAudio();\n\t\t\tif (!exists(mtpc_documentAttributeFilename)\n\t\t\t\t&& !(fields.vflags().v & Flag::f_voice)) {\n\t\t\t\tconst auto p = Core::MimeTypeForName(mime).globPatterns();\n\t\t\t\tauto pattern = p.isEmpty() ? QString() : p.front();\n\t\t\t\tconst auto extension = pattern.isEmpty()\n\t\t\t\t\t? u\".unknown\"_q\n\t\t\t\t\t: pattern.replace('*', QString());\n\t\t\t\tconst auto filename = filedialogDefaultName(\n\t\t\t\t\tu\"inline\"_q,\n\t\t\t\t\textension,\n\t\t\t\t\tQString(),\n\t\t\t\t\ttrue);\n\t\t\t\tresult.push_back(\n\t\t\t\t\tMTP_documentAttributeFilename(MTP_string(filename)));\n\t\t\t}\n\t\t}\n\t}\n\treturn MTP_vector<MTPDocumentAttribute>(std::move(result));\n}\n\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_bot_result.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_cloud_file.h\"\n#include \"api/api_common.h\"\n#include \"media/view/media_view_open_common.h\"\n#include \"ui/effects/message_sending_animation_common.h\"\n\nclass FileLoader;\nclass History;\nclass UserData;\nstruct HistoryMessageMarkupData;\nstruct HistoryItemCommonFields;\n\nnamespace Data {\nclass LocationPoint;\nstruct SendError;\n} // namespace Data\n\nnamespace InlineBots {\n\nnamespace Layout {\nclass ItemBase;\n} // namespace Layout\n\nnamespace internal {\nclass SendData;\n} // namespace internal\n\nclass Result {\nprivate:\n\t// See http://stackoverflow.com/a/8147326\n\tstruct Creator;\n\npublic:\n\t// Constructor is public only for std::make_unique<>() to work.\n\t// You should use create() static method instead.\n\tResult(not_null<Main::Session*> session, const Creator &creator);\n\n\tstatic std::shared_ptr<Result> Create(\n\t\tnot_null<Main::Session*> session,\n\t\tuint64 queryId,\n\t\tconst MTPBotInlineResult &mtpData);\n\n\tuint64 getQueryId() const {\n\t\treturn _queryId;\n\t}\n\tQString getId() const {\n\t\treturn _id;\n\t}\n\n\t// This is real SendClickHandler::onClick implementation for the specified\n\t// inline bot result. If it returns true you need to send this result.\n\tbool onChoose(Layout::ItemBase *layout);\n\n\tMedia::View::OpenRequest openRequest();\n\tvoid cancelFile();\n\n\tbool hasThumbDisplay() const;\n\n\tvoid addToHistory(\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields) const;\n\t[[nodiscard]] not_null<HistoryItem*> makeMessage(\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields) const;\n\t[[nodiscard]] Data::SendError getErrorOnSend(\n\t\tnot_null<History*> history) const;\n\n\t// interface for Layout:: usage\n\tstd::optional<Data::LocationPoint> getLocationPoint() const;\n\tQString getLayoutTitle() const;\n\tQString getLayoutDescription() const;\n\n\t~Result();\n\nprivate:\n\tvoid createGame(not_null<Main::Session*> session);\n\tQSize thumbBox() const;\n\tMTPWebDocument adjustAttributes(const MTPWebDocument &document);\n\tMTPVector<MTPDocumentAttribute> adjustAttributes(\n\t\tconst MTPVector<MTPDocumentAttribute> &document,\n\t\tconst MTPstring &mimeType);\n\n\tenum class Type {\n\t\tUnknown,\n\t\tPhoto,\n\t\tVideo,\n\t\tAudio,\n\t\tSticker,\n\t\tFile,\n\t\tGif,\n\t\tArticle,\n\t\tContact,\n\t\tGeo,\n\t\tVenue,\n\t\tGame,\n\t};\n\n\tfriend class internal::SendData;\n\tfriend class Layout::ItemBase;\n\tstruct Creator {\n\t\tuint64 queryId = 0;\n\t\tType type = Type::Unknown;\n\t};\n\n\tnot_null<Main::Session*> _session;\n\tuint64 _queryId = 0;\n\tQString _id;\n\tType _type = Type::Unknown;\n\tQString _title, _description, _url;\n\tQString _content_url;\n\tDocumentData *_document = nullptr;\n\tPhotoData *_photo = nullptr;\n\tGameData *_game = nullptr;\n\n\tstd::unique_ptr<HistoryMessageMarkupData> _replyMarkup;\n\n\tData::CloudImage _thumbnail;\n\tData::CloudImage _locationThumbnail;\n\n\tstd::unique_ptr<internal::SendData> sendData;\n\n};\n\nstruct ResultSelected {\n\tstd::shared_ptr<Result> result;\n\tnot_null<UserData*> bot;\n\tPeerData *recipientOverride = nullptr;\n\tApi::SendOptions options;\n\tUi::MessageSendingAnimationFrom messageSendingFrom;\n\t// Open in OverlayWidget;\n\tbool open = false;\n};\n\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"inline_bots/inline_bot_send_data.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"data/data_document.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"storage/localstorage.h\"\n#include \"lang/lang_keys.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"data/data_channel.h\"\n#include \"ui/text/format_values.h\" // Ui::FormatPhone\n\nnamespace InlineBots {\nnamespace internal {\n\nQString SendData::getLayoutTitle(const Result *owner) const {\n\treturn owner->_title;\n}\n\nQString SendData::getLayoutDescription(const Result *owner) const {\n\treturn owner->_description;\n}\n\nnot_null<HistoryItem*> SendDataCommon::makeMessage(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields) const {\n\tauto distinct = getSentMessageFields();\n\tif (fields.replyTo) {\n\t\tfields.flags |= MessageFlag::HasReplyInfo;\n\t}\n\treturn history->makeMessage(\n\t\tstd::move(fields),\n\t\tstd::move(distinct.text),\n\t\tstd::move(distinct.media));\n}\n\nData::SendError SendDataCommon::getErrorOnSend(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history) const {\n\tconst auto type = ChatRestriction::SendOther;\n\treturn Data::RestrictionError(history->peer, type);\n}\n\nSendDataCommon::SentMessageFields SendText::getSentMessageFields() const {\n\treturn { .text = { _message, _entities } };\n}\n\nSendDataCommon::SentMessageFields SendGeo::getSentMessageFields() const {\n\tif (_period) {\n\t\tusing Flag = MTPDmessageMediaGeoLive::Flag;\n\t\treturn { .media = MTP_messageMediaGeoLive(\n\t\t\tMTP_flags((_heading ? Flag::f_heading : Flag(0))\n\t\t\t\t| (_proximityNotificationRadius\n\t\t\t\t\t? Flag::f_proximity_notification_radius\n\t\t\t\t\t: Flag(0))),\n\t\t\t_location.toMTP(),\n\t\t\tMTP_int(_heading.value_or(0)),\n\t\t\tMTP_int(*_period),\n\t\t\tMTP_int(_proximityNotificationRadius.value_or(0))) };\n\t}\n\treturn { .media = MTP_messageMediaGeo(_location.toMTP()) };\n}\n\nSendDataCommon::SentMessageFields SendVenue::getSentMessageFields() const {\n\treturn { .media = MTP_messageMediaVenue(\n\t\t_location.toMTP(),\n\t\tMTP_string(_title),\n\t\tMTP_string(_address),\n\t\tMTP_string(_provider),\n\t\tMTP_string(_venueId),\n\t\tMTP_string(QString())) }; // venue_type\n}\n\nSendDataCommon::SentMessageFields SendContact::getSentMessageFields() const {\n\treturn { .media = MTP_messageMediaContact(\n\t\tMTP_string(_phoneNumber),\n\t\tMTP_string(_firstName),\n\t\tMTP_string(_lastName),\n\t\tMTP_string(), // vcard\n\t\tMTP_long(0)) }; // user_id\n}\n\nQString SendContact::getLayoutDescription(const Result *owner) const {\n\tauto result = SendData::getLayoutDescription(owner);\n\tif (result.isEmpty()) {\n\t\treturn Ui::FormatPhone(_phoneNumber);\n\t}\n\treturn result;\n}\n\nnot_null<HistoryItem*> SendPhoto::makeMessage(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields) const {\n\treturn history->makeMessage(\n\t\tstd::move(fields),\n\t\t_photo,\n\t\tTextWithEntities{ _message, _entities });\n}\n\nData::SendError SendPhoto::getErrorOnSend(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history) const {\n\tconst auto type = ChatRestriction::SendPhotos;\n\treturn Data::RestrictionError(history->peer, type);\n}\n\nnot_null<HistoryItem*> SendFile::makeMessage(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields) const {\n\treturn history->makeMessage(\n\t\tstd::move(fields),\n\t\t_document,\n\t\tTextWithEntities{ _message, _entities });\n}\n\nData::SendError SendFile::getErrorOnSend(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history) const {\n\tconst auto type = _document->requiredSendRight();\n\treturn Data::RestrictionError(history->peer, type);\n}\n\nnot_null<HistoryItem*> SendGame::makeMessage(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields) const {\n\treturn history->makeMessage(std::move(fields), _game);\n}\n\nData::SendError SendGame::getErrorOnSend(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history) const {\n\tconst auto type = ChatRestriction::SendGames;\n\treturn Data::RestrictionError(history->peer, type);\n}\n\nSendDataCommon::SentMessageFields SendInvoice::getSentMessageFields() const {\n\treturn { .media = _media };\n}\n\nQString SendInvoice::getLayoutDescription(const Result *owner) const {\n\treturn qs(_media.c_messageMediaInvoice().vdescription());\n}\n\n} // namespace internal\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_bot_send_data.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/history_location_manager.h\"\n\nstruct HistoryItemCommonFields;\n\nnamespace Data {\nstruct SendError;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nclass History;\n\nnamespace InlineBots {\n\nclass Result;\n\nnamespace internal {\n\n// Abstract class describing the message that will be\n// sent if the user chooses this inline bot result.\n// For each type of message that can be sent there will be a subclass.\nclass SendData {\npublic:\n\texplicit SendData(not_null<Main::Session*> session) : _session(session) {\n\t}\n\tSendData(const SendData &other) = delete;\n\tSendData &operator=(const SendData &other) = delete;\n\tvirtual ~SendData() = default;\n\n\t[[nodiscard]] Main::Session &session() const {\n\t\treturn *_session;\n\t}\n\n\tvirtual bool isValid() const = 0;\n\n\tvirtual not_null<HistoryItem*> makeMessage(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields) const = 0;\n\tvirtual Data::SendError getErrorOnSend(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history) const = 0;\n\n\tvirtual bool hasLocationCoords() const {\n\t\treturn false;\n\t}\n\tvirtual std::optional<Data::LocationPoint> getLocationPoint() const {\n\t\treturn std::nullopt;\n\t}\n\tvirtual QString getLayoutTitle(const Result *owner) const;\n\tvirtual QString getLayoutDescription(const Result *owner) const;\n\nprivate:\n\tnot_null<Main::Session*> _session;\n\n};\n\n// This class implements addHistory() for most of the types hiding\n// the differences in getSentMessageFields() method.\n// Only SendFile and SendPhoto work by their own.\nclass SendDataCommon : public SendData {\npublic:\n\tusing SendData::SendData;\n\n\tstruct SentMessageFields {\n\t\tTextWithEntities text;\n\t\tMTPMessageMedia media = MTP_messageMediaEmpty();\n\t};\n\tvirtual SentMessageFields getSentMessageFields() const = 0;\n\n\tnot_null<HistoryItem*> makeMessage(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields) const override;\n\n\tData::SendError getErrorOnSend(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history) const override;\n\n};\n\n// Plain text message.\nclass SendText : public SendDataCommon {\npublic:\n\tSendText(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &message,\n\t\tconst EntitiesInText &entities,\n\t\tbool/* noWebPage*/)\n\t: SendDataCommon(session)\n\t, _message(message)\n\t, _entities(entities) {\n\t}\n\n\tbool isValid() const override {\n\t\treturn !_message.isEmpty();\n\t}\n\n\tSentMessageFields getSentMessageFields() const override;\n\nprivate:\n\tQString _message;\n\tEntitiesInText _entities;\n\n};\n\n// Message with geo location point media.\nclass SendGeo : public SendDataCommon {\npublic:\n\tSendGeo(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDgeoPoint &point)\n\t: SendDataCommon(session)\n\t, _location(point) {\n\t}\n\tSendGeo(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDgeoPoint &point,\n\t\tint period,\n\t\tstd::optional<int> heading,\n\t\tstd::optional<int> proximityNotificationRadius)\n\t: SendDataCommon(session)\n\t, _location(point)\n\t, _period(period)\n\t, _heading(heading)\n\t, _proximityNotificationRadius(proximityNotificationRadius){\n\t}\n\n\tbool isValid() const override {\n\t\treturn true;\n\t}\n\n\tSentMessageFields getSentMessageFields() const override;\n\n\tbool hasLocationCoords() const override {\n\t\treturn true;\n\t}\n\tstd::optional<Data::LocationPoint> getLocationPoint() const override {\n\t\treturn _location;\n\t}\n\nprivate:\n\tData::LocationPoint _location;\n\tstd::optional<int> _period;\n\tstd::optional<int> _heading;\n\tstd::optional<int> _proximityNotificationRadius;\n\n};\n\n// Message with venue media.\nclass SendVenue : public SendDataCommon {\npublic:\n\tSendVenue(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDgeoPoint &point,\n\t\tconst QString &venueId,\n\t\tconst QString &provider,\n\t\tconst QString &title,\n\t\tconst QString &address)\n\t: SendDataCommon(session)\n\t, _location(point)\n\t, _venueId(venueId)\n\t, _provider(provider)\n\t, _title(title)\n\t, _address(address) {\n\t}\n\n\tbool isValid() const override {\n\t\treturn true;\n\t}\n\n\tSentMessageFields getSentMessageFields() const override;\n\n\tbool hasLocationCoords() const override {\n\t\treturn true;\n\t}\n\tstd::optional<Data::LocationPoint> getLocationPoint() const override {\n\t\treturn _location;\n\t}\n\nprivate:\n\tData::LocationPoint _location;\n\tQString _venueId, _provider, _title, _address;\n\n};\n\n// Message with shared contact media.\nclass SendContact : public SendDataCommon {\npublic:\n\tSendContact(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &firstName,\n\t\tconst QString &lastName,\n\t\tconst QString &phoneNumber)\n\t: SendDataCommon(session)\n\t, _firstName(firstName)\n\t, _lastName(lastName)\n\t, _phoneNumber(phoneNumber) {\n\t}\n\n\tbool isValid() const override {\n\t\treturn (!_firstName.isEmpty() || !_lastName.isEmpty()) && !_phoneNumber.isEmpty();\n\t}\n\n\tSentMessageFields getSentMessageFields() const override;\n\n\tQString getLayoutDescription(const Result *owner) const override;\n\nprivate:\n\tQString _firstName, _lastName, _phoneNumber;\n\n};\n\n// Message with photo.\nclass SendPhoto : public SendData {\npublic:\n\tSendPhoto(\n\t\tnot_null<Main::Session*> session,\n\t\tPhotoData *photo,\n\t\tconst QString &message,\n\t\tconst EntitiesInText &entities)\n\t: SendData(session)\n\t, _photo(photo)\n\t, _message(message)\n\t, _entities(entities) {\n\t}\n\n\tbool isValid() const override {\n\t\treturn _photo != nullptr;\n\t}\n\n\tnot_null<HistoryItem*> makeMessage(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields) const override;\n\n\tData::SendError getErrorOnSend(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history) const override;\n\nprivate:\n\tPhotoData *_photo;\n\tQString _message;\n\tEntitiesInText _entities;\n\n};\n\n// Message with file.\nclass SendFile : public SendData {\npublic:\n\tSendFile(\n\t\tnot_null<Main::Session*> session,\n\t\tDocumentData *document,\n\t\tconst QString &message,\n\t\tconst EntitiesInText &entities)\n\t: SendData(session)\n\t, _document(document)\n\t, _message(message)\n\t, _entities(entities) {\n\t}\n\n\tbool isValid() const override {\n\t\treturn _document != nullptr;\n\t}\n\n\tnot_null<HistoryItem*> makeMessage(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields) const override;\n\n\tData::SendError getErrorOnSend(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history) const override;\n\nprivate:\n\tDocumentData *_document;\n\tQString _message;\n\tEntitiesInText _entities;\n\n};\n\n// Message with game.\nclass SendGame : public SendData {\npublic:\n\tSendGame(not_null<Main::Session*> session, GameData *game)\n\t: SendData(session)\n\t, _game(game) {\n\t}\n\n\tbool isValid() const override {\n\t\treturn _game != nullptr;\n\t}\n\n\tnot_null<HistoryItem*> makeMessage(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history,\n\t\tHistoryItemCommonFields &&fields) const override;\n\n\tData::SendError getErrorOnSend(\n\t\tconst Result *owner,\n\t\tnot_null<History*> history) const override;\n\nprivate:\n\tGameData *_game;\n\n};\n\nclass SendInvoice : public SendDataCommon {\npublic:\n\tSendInvoice(\n\t\tnot_null<Main::Session*> session,\n\t\tMTPMessageMedia media)\n\t: SendDataCommon(session)\n\t, _media(media) {\n\t}\n\n\tbool isValid() const override {\n\t\treturn true;\n\t}\n\n\tSentMessageFields getSentMessageFields() const override;\n\n\tQString getLayoutDescription(const Result *owner) const override;\n\nprivate:\n\tMTPMessageMedia _media;\n\n};\n\n} // namespace internal\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_bot_storage.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"inline_bots/inline_bot_storage.h\"\n\n#include \"main/main_session.h\"\n#include \"storage/storage_account.h\"\n\n#include <xxhash.h>\n\nnamespace InlineBots {\nnamespace {\n\nconstexpr auto kMaxStorageSize = (5 << 20);\n\n[[nodiscard]] uint64 KeyHash(const QString &key) {\n\treturn XXH64(key.data(), key.size(), 0);\n}\n\n} // namespace\n\nStorage::Storage(not_null<Main::Session*> session)\n: _session(session) {\n}\n\nbool Storage::write(\n\t\tPeerId botId,\n\t\tconst QString &key,\n\t\tconst std::optional<QString> &value) {\n\tif (value && value->size() > kMaxStorageSize) {\n\t\treturn false;\n\t}\n\treadFromDisk(botId);\n\tauto i = _lists.find(botId);\n\tif (i == end(_lists)) {\n\t\tif (!value) {\n\t\t\treturn true;\n\t\t}\n\t\ti = _lists.emplace(botId).first;\n\t}\n\tauto &list = i->second;\n\tconst auto hash = KeyHash(key);\n\tauto j = list.data.find(hash);\n\tif (j == end(list.data)) {\n\t\tif (!value) {\n\t\t\treturn true;\n\t\t}\n\t\tj = list.data.emplace(hash).first;\n\t}\n\tauto &bykey = j->second;\n\tconst auto k = ranges::find(bykey, key, &Entry::key);\n\tif (k == end(bykey) && !value) {\n\t\treturn true;\n\t}\n\tconst auto size = list.totalSize\n\t\t- (k != end(bykey) ? (key.size() + k->value.size()) : 0)\n\t\t+ (value ? (key.size() + value->size()) : 0);\n\tif (size > kMaxStorageSize) {\n\t\treturn false;\n\t}\n\tif (k == end(bykey)) {\n\t\tbykey.emplace_back(Entry{ key, *value });\n\t\t++list.keysCount;\n\t} else if (value) {\n\t\tk->value = *value;\n\t} else {\n\t\tbykey.erase(k);\n\t\t--list.keysCount;\n\t}\n\tif (bykey.empty()) {\n\t\tlist.data.erase(j);\n\t\tif (list.data.empty()) {\n\t\t\tAssert(size == 0);\n\t\t\t_lists.erase(i);\n\t\t} else {\n\t\t\tlist.totalSize = size;\n\t\t}\n\t} else {\n\t\tlist.totalSize = size;\n\t}\n\tsaveToDisk(botId);\n\treturn true;\n}\n\nstd::optional<QString> Storage::read(PeerId botId, const QString &key) {\n\treadFromDisk(botId);\n\tconst auto i = _lists.find(botId);\n\tif (i == end(_lists)) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto &list = i->second;\n\tconst auto j = list.data.find(KeyHash(key));\n\tif (j == end(list.data)) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto &bykey = j->second;\n\tconst auto k = ranges::find(bykey, key, &Entry::key);\n\tif (k == end(bykey)) {\n\t\treturn std::nullopt;\n\t}\n\treturn k->value;\n}\n\nvoid Storage::clear(PeerId botId) {\n\tif (_lists.remove(botId)) {\n\t\tsaveToDisk(botId);\n\t}\n}\n\nvoid Storage::saveToDisk(PeerId botId) {\n\tconst auto i = _lists.find(botId);\n\tif (i != end(_lists)) {\n\t\t_session->local().writeBotStorage(botId, Serialize(i->second));\n\t} else {\n\t\t_session->local().writeBotStorage(botId, QByteArray());\n\t}\n}\n\nvoid Storage::readFromDisk(PeerId botId) {\n\tconst auto serialized = _session->local().readBotStorage(botId);\n\tif (!serialized.isEmpty()) {\n\t\t_lists[botId] = Deserialize(serialized);\n\t}\n}\n\nQByteArray Storage::Serialize(const List &list) {\n\tauto result = QByteArray();\n\tconst auto size = sizeof(quint32)\n\t\t+ (list.keysCount * sizeof(quint32))\n\t\t+ (list.totalSize * sizeof(ushort));\n\tresult.reserve(size);\n\t{\n\t\tQDataStream stream(&result, QIODevice::WriteOnly);\n\t\tauto count = 0;\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\t\tstream << quint32(list.keysCount);\n\t\tfor (const auto &[hash, bykey] : list.data) {\n\t\t\tfor (const auto &entry : bykey) {\n\t\t\t\tstream << entry.key << entry.value;\n\t\t\t\t++count;\n\t\t\t}\n\t\t}\n\t\tAssert(count == list.keysCount);\n\t}\n\treturn result;\n}\n\nStorage::List Storage::Deserialize(const QByteArray &serialized) {\n\tQDataStream stream(serialized);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\n\tauto count = quint32();\n\tauto result = List();\n\tstream >> count;\n\tif (count > kMaxStorageSize) {\n\t\treturn {};\n\t}\n\tfor (auto i = 0; i != count; ++i) {\n\t\tauto entry = Entry();\n\t\tstream >> entry.key >> entry.value;\n\t\tconst auto hash = KeyHash(entry.key);\n\t\tauto j = result.data.find(hash);\n\t\tif (j == end(result.data)) {\n\t\t\tj = result.data.emplace(hash).first;\n\t\t}\n\t\tauto &bykey = j->second;\n\t\tconst auto k = ranges::find(bykey, entry.key, &Entry::key);\n\t\tif (k == end(bykey)) {\n\t\t\tbykey.push_back(entry);\n\t\t\tresult.totalSize += entry.key.size() + entry.value.size();\n\t\t\t++result.keysCount;\n\t\t}\n\t}\n\treturn result;\n}\n\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_bot_storage.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/chat/attach/attach_bot_webview.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace InlineBots {\n\nclass Storage final {\npublic:\n\texplicit Storage(not_null<Main::Session*> session);\n\n\tbool write(\n\t\tPeerId botId,\n\t\tconst QString &key,\n\t\tconst std::optional<QString> &value);\n\tstd::optional<QString> read(PeerId botId, const QString &key);\n\tvoid clear(PeerId botId);\n\nprivate:\n\tstruct Entry {\n\t\tQString key;\n\t\tQString value;\n\t};\n\tstruct List {\n\t\tbase::flat_map<uint64, std::vector<Entry>> data;\n\t\tint keysCount = 0;\n\t\tint totalSize = 0;\n\t};\n\n\tvoid saveToDisk(PeerId botId);\n\tvoid readFromDisk(PeerId botId);\n\n\t[[nodiscard]] static QByteArray Serialize(const List &list);\n\t[[nodiscard]] static List Deserialize(const QByteArray &serialized);\n\n\tconst not_null<Main::Session*> _session;\n\n\tbase::flat_map<PeerId, List> _lists;\n\n};\n\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_results_inner.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"inline_bots/inline_results_inner.h\"\n\n#include \"api/api_common.h\"\n#include \"chat_helpers/gifs_list_widget.h\" // ChatHelpers::AddGifAction\n#include \"menu/menu_send.h\" // SendMenu::FillSendMenu\n#include \"core/click_handler_types.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_user.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"data/data_session.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"inline_bots/inline_bot_layout_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"layout/layout_position.h\"\n#include \"mainwindow.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/history.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace InlineBots {\nnamespace Layout {\nnamespace {\n\nconstexpr auto kMinRepaintDelay = crl::time(33);\nconstexpr auto kMinAfterScrollDelay = crl::time(33);\n\n} // namespace\n\nInner::Inner(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _pathGradient(std::make_unique<Ui::PathShiftGradient>(\n\tst::windowBgRipple,\n\tst::windowBgOver,\n\t[=] { repaintItems(); }))\n, _updateInlineItems([=] { updateInlineItems(); })\n, _mosaic(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft)\n, _previewTimer([=] { showPreview(); }) {\n\tresize(st::emojiPanWidth - st::emojiScroll.width - st::roundRadiusSmall, st::inlineResultsMinHeight);\n\n\tsetMouseTracking(true);\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\n\t_controller->session().downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdateInlineItems();\n\t}, lifetime());\n\n\tcontroller->gifPauseLevelChanged(\n\t) | rpl::on_next([=] {\n\t\tif (!_controller->isGifPausedAtLeastFor(\n\t\t\t\tWindow::GifPauseReason::InlineResults)) {\n\t\t\tupdateInlineItems();\n\t\t}\n\t}, lifetime());\n\n\t_controller->session().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::Rights\n\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\treturn (update.peer.get() == _inlineQueryPeer);\n\t}) | rpl::on_next([=] {\n\t\tauto isRestricted = (_restrictedLabel != nullptr);\n\t\tif (isRestricted != isRestrictedView()) {\n\t\t\tauto h = countHeight();\n\t\t\tif (h != height()) resize(width(), h);\n\t\t}\n\t}, lifetime());\n\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\t_mosaic.setFullWidth(s.width());\n\t}, lifetime());\n\n\t_mosaic.setRightSkip(st::inlineResultsSkip);\n}\n\nvoid Inner::visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) {\n\t_visibleBottom = visibleBottom;\n\tif (_visibleTop != visibleTop) {\n\t\t_visibleTop = visibleTop;\n\t\t_lastScrolledAt = crl::now();\n\t\tupdate();\n\t}\n}\n\nvoid Inner::checkRestrictedPeer() {\n\tif (_inlineQueryPeer) {\n\t\tconst auto error = Data::RestrictionError(\n\t\t\t_inlineQueryPeer,\n\t\t\tChatRestriction::SendInline);\n\t\tconst auto changed = (_restrictedLabelKey != error.text);\n\t\tif (!changed) {\n\t\t\treturn;\n\t\t}\n\t\t_restrictedLabelKey = error.text;\n\t\tif (error) {\n\t\t\tconst auto window = _controller;\n\t\t\tconst auto peer = _inlineQueryPeer;\n\t\t\t_restrictedLabel.create(\n\t\t\t\tthis,\n\t\t\t\trpl::single(error.boostsToLift\n\t\t\t\t\t? tr::link(error.text)\n\t\t\t\t\t: TextWithEntities{ error.text }),\n\t\t\t\tst::stickersRestrictedLabel);\n\t\t\tconst auto lifting = error.boostsToLift;\n\t\t\t_restrictedLabel->setClickHandlerFilter([=](auto...) {\n\t\t\t\twindow->resolveBoostState(peer->asChannel(), lifting);\n\t\t\t\treturn false;\n\t\t\t});\n\t\t\t_restrictedLabel->show();\n\t\t\tupdateRestrictedLabelGeometry();\n\t\t\tif (_switchPmButton) {\n\t\t\t\t_switchPmButton->hide();\n\t\t\t}\n\t\t\trepaintItems();\n\t\t\treturn;\n\t\t}\n\t} else {\n\t\t_restrictedLabelKey = QString();\n\t}\n\tif (_restrictedLabel) {\n\t\t_restrictedLabel.destroy();\n\t\tif (_switchPmButton) {\n\t\t\t_switchPmButton->show();\n\t\t}\n\t\trepaintItems();\n\t}\n}\n\nvoid Inner::updateRestrictedLabelGeometry() {\n\tif (!_restrictedLabel) {\n\t\treturn;\n\t}\n\n\tauto labelWidth = width() - st::stickerPanPadding * 2;\n\t_restrictedLabel->resizeToWidth(labelWidth);\n\t_restrictedLabel->moveToLeft(\n\t\t(width() - _restrictedLabel->width()) / 2,\n\t\tst::stickerPanPadding);\n}\n\nbool Inner::isRestrictedView() {\n\tcheckRestrictedPeer();\n\treturn (_restrictedLabel != nullptr);\n}\n\nint Inner::countHeight() {\n\tif (isRestrictedView()) {\n\t\treturn st::stickerPanPadding + _restrictedLabel->height() + st::stickerPanPadding;\n\t} else if (_mosaic.empty() && !_switchPmButton) {\n\t\treturn st::stickerPanPadding + st::normalFont->height + st::stickerPanPadding;\n\t}\n\tauto result = st::stickerPanPadding;\n\tif (_switchPmButton) {\n\t\tresult += _switchPmButton->height() + st::inlineResultsSkip;\n\t}\n\tfor (auto i = 0, l = _mosaic.rowsCount(); i < l; ++i) {\n\t\tresult += _mosaic.rowHeightAt(i);\n\t}\n\treturn result + st::stickerPanPadding;\n}\n\nQString Inner::tooltipText() const {\n\tif (const auto lnk = ClickHandler::getActive()) {\n\t\treturn lnk->tooltip();\n\t}\n\treturn QString();\n}\n\nQPoint Inner::tooltipPos() const {\n\treturn _lastMousePos;\n}\n\nbool Inner::tooltipWindowActive() const {\n\treturn Ui::AppInFocus() && Ui::InFocusChain(window());\n}\n\nrpl::producer<> Inner::inlineRowsCleared() const {\n\treturn _inlineRowsCleared.events();\n}\n\nInner::~Inner() = default;\n\nvoid Inner::resizeEvent(QResizeEvent *e) {\n\tupdateRestrictedLabelGeometry();\n}\n\nvoid Inner::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\tQRect r = e ? e->rect() : rect();\n\tif (r != rect()) {\n\t\tp.setClipRect(r);\n\t}\n\tp.fillRect(r, st::emojiPanBg);\n\n\tpaintInlineItems(p, r);\n}\n\nvoid Inner::paintInlineItems(Painter &p, const QRect &r) {\n\tif (_restrictedLabel) {\n\t\treturn;\n\t}\n\tif (_mosaic.empty() && !_switchPmButton) {\n\t\tp.setFont(st::normalFont);\n\t\tp.setPen(st::noContactsColor);\n\t\tp.drawText(QRect(0, 0, width(), (height() / 3) * 2 + st::normalFont->height), tr::lng_inline_bot_no_results(tr::now), style::al_center);\n\t\treturn;\n\t}\n\tconst auto gifPaused = _controller->isGifPausedAtLeastFor(\n\t\tWindow::GifPauseReason::InlineResults);\n\tusing namespace InlineBots::Layout;\n\tPaintContext context(crl::now(), false, gifPaused, false);\n\tcontext.pathGradient = _pathGradient.get();\n\tcontext.pathGradient->startFrame(0, width(), width() / 2);\n\n\tauto paintItem = [&](not_null<const ItemBase*> item, QPoint point) {\n\t\tp.translate(point.x(), point.y());\n\t\titem->paint(\n\t\t\tp,\n\t\t\tr.translated(-point),\n\t\t\t&context);\n\t\tp.translate(-point.x(), -point.y());\n\t};\n\t_mosaic.paint(std::move(paintItem), r);\n}\n\nvoid Inner::mousePressEvent(QMouseEvent *e) {\n\tif (e->button() != Qt::LeftButton) {\n\t\treturn;\n\t}\n\t_lastMousePos = e->globalPos();\n\tupdateSelected();\n\n\t_pressed = _selected;\n\tClickHandler::pressed();\n\t_previewTimer.callOnce(QApplication::startDragTime());\n}\n\nvoid Inner::mouseReleaseEvent(QMouseEvent *e) {\n\t_previewTimer.cancel();\n\n\tauto pressed = std::exchange(_pressed, -1);\n\tauto activated = ClickHandler::unpressed();\n\n\tif (_previewShown) {\n\t\t_previewShown = false;\n\t\treturn;\n\t}\n\n\t_lastMousePos = e->globalPos();\n\tupdateSelected();\n\n\tif (_selected < 0 || _selected != pressed || !activated) {\n\t\treturn;\n\t}\n\n\tusing namespace InlineBots::Layout;\n\tconst auto open = dynamic_cast<OpenFileClickHandler*>(activated.get());\n\tif (dynamic_cast<SendClickHandler*>(activated.get()) || open) {\n\t\tselectInlineResult(_selected, {}, !!open);\n\t} else {\n\t\tActivateClickHandler(window(), activated, {\n\t\t\te->button(),\n\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.sessionWindow = base::make_weak(_controller),\n\t\t\t})\n\t\t});\n\t}\n}\n\nvoid Inner::selectInlineResult(\n\t\tint index,\n\t\tApi::SendOptions options,\n\t\tbool open) {\n\tconst auto item = _mosaic.maybeItemAt(index);\n\tif (!item) {\n\t\treturn;\n\t}\n\tconst auto messageSendingFrom = [&]() -> Ui::MessageSendingAnimationFrom {\n\t\tconst auto document = item->getDocument()\n\t\t\t? item->getDocument()\n\t\t\t: item->getPreviewDocument();\n\t\tif (options.scheduled\n\t\t\t|| item->isFullLine()\n\t\t\t|| !document\n\t\t\t|| (!document->sticker() && !document->isGifv())) {\n\t\t\treturn {};\n\t\t}\n\t\tusing Type = Ui::MessageSendingAnimationFrom::Type;\n\t\tconst auto type = document->sticker()\n\t\t\t? Type::Sticker\n\t\t\t: document->isGifv()\n\t\t\t? Type::Gif\n\t\t\t: Type::None;\n\t\tconst auto rect = item->innerContentRect().translated(\n\t\t\t_mosaic.findRect(index).topLeft());\n\t\treturn {\n\t\t\t.type = type,\n\t\t\t.localId = _controller->session().data().nextLocalMessageId(),\n\t\t\t.globalStartGeometry = mapToGlobal(rect),\n\t\t\t.crop = document->isGifv(),\n\t\t};\n\t};\n\n\tif (const auto inlineResult = item->getResult()) {\n\t\tif (inlineResult->onChoose(item)) {\n\t\t\t_resultSelectedCallback({\n\t\t\t\t.result = std::move(inlineResult),\n\t\t\t\t.bot = _inlineBot,\n\t\t\t\t.options = std::move(options),\n\t\t\t\t.messageSendingFrom = messageSendingFrom(),\n\t\t\t\t.open = open,\n\t\t\t});\n\t\t}\n\t}\n}\n\nvoid Inner::mouseMoveEvent(QMouseEvent *e) {\n\t_lastMousePos = e->globalPos();\n\tupdateSelected();\n}\n\nvoid Inner::leaveEventHook(QEvent *e) {\n\tclearSelection();\n\tUi::Tooltip::Hide();\n}\n\nvoid Inner::leaveToChildEvent(QEvent *e, QWidget *child) {\n\tclearSelection();\n}\n\nvoid Inner::enterFromChildEvent(QEvent *e, QWidget *child) {\n\t_lastMousePos = QCursor::pos();\n\tupdateSelected();\n}\n\nvoid Inner::contextMenuEvent(QContextMenuEvent *e) {\n\tif (_selected < 0 || _pressed >= 0) {\n\t\treturn;\n\t}\n\tauto details = _sendMenuDetails\n\t\t? _sendMenuDetails()\n\t\t: SendMenu::Details();\n\n\t// inline results don't have effects\n\tdetails.effectAllowed = false;\n\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\tst::popupMenuWithIcons);\n\n\tconst auto selected = _selected;\n\tconst auto send = crl::guard(this, [=](Api::SendOptions options) {\n\t\tselectInlineResult(selected, options, false);\n\t});\n\tconst auto show = _controller->uiShow();\n\n\t// In case we're adding items after FillSendMenu we have\n\t// to pass nullptr for showForEffect and attach selector later.\n\t// Otherwise added items widths won't be respected in menu geometry.\n\tSendMenu::FillSendMenu(\n\t\t_menu,\n\t\tnullptr, // showForEffect\n\t\tdetails,\n\t\tSendMenu::DefaultCallback(show, send));\n\n\tconst auto item = _mosaic.itemAt(_selected);\n\tif (const auto previewDocument = item->getPreviewDocument()) {\n\t\tauto callback = [&](\n\t\t\t\tconst QString &text,\n\t\t\t\tFn<void()> &&done,\n\t\t\t\tconst style::icon *icon) {\n\t\t\t_menu->addAction(text, std::move(done), icon);\n\t\t};\n\t\tChatHelpers::AddGifAction(\n\t\t\tstd::move(callback),\n\t\t\t_controller->uiShow(),\n\t\t\tpreviewDocument);\n\t}\n\n\tSendMenu::AttachSendMenuEffect(\n\t\t_menu,\n\t\tshow,\n\t\tdetails,\n\t\tSendMenu::DefaultCallback(show, send));\n\n\tif (!_menu->empty()) {\n\t\t_menu->popup(QCursor::pos());\n\t}\n}\n\nvoid Inner::clearSelection() {\n\tif (_selected >= 0) {\n\t\tClickHandler::clearActive(_mosaic.itemAt(_selected));\n\t\tsetCursor(style::cur_default);\n\t}\n\t_selected = _pressed = -1;\n\tupdateInlineItems();\n}\n\nvoid Inner::hideFinished() {\n\tclearHeavyData();\n}\n\nvoid Inner::clearHeavyData() {\n\tclearInlineRows(false);\n\tfor (const auto &[result, layout] : _inlineLayouts) {\n\t\tlayout->unloadHeavyPart();\n\t}\n}\n\nvoid Inner::inlineBotChanged() {\n\trefreshInlineRows(nullptr, nullptr, nullptr, true);\n}\n\nvoid Inner::clearInlineRows(bool resultsDeleted) {\n\tif (resultsDeleted) {\n\t\t_selected = _pressed = -1;\n\t} else {\n\t\tclearSelection();\n\t}\n\t_mosaic.clearRows(resultsDeleted);\n}\n\nItemBase *Inner::layoutPrepareInlineResult(std::shared_ptr<Result> result) {\n\tconst auto raw = result.get();\n\tauto it = _inlineLayouts.find(raw);\n\tif (it == _inlineLayouts.cend()) {\n\t\tif (auto layout = ItemBase::createLayout(\n\t\t\t\tthis,\n\t\t\t\tstd::move(result),\n\t\t\t\t_inlineWithThumb,\n\t\t\t\t_gallery)) {\n\t\t\tit = _inlineLayouts.emplace(raw, std::move(layout)).first;\n\t\t\tit->second->initDimensions();\n\t\t} else {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\tif (!it->second->maxWidth()) {\n\t\treturn nullptr;\n\t}\n\n\treturn it->second.get();\n}\n\nvoid Inner::deleteUnusedInlineLayouts() {\n\tif (_mosaic.empty()) { // delete all\n\t\t_inlineLayouts.clear();\n\t} else {\n\t\tfor (auto i = _inlineLayouts.begin(); i != _inlineLayouts.cend();) {\n\t\t\tif (i->second->position() < 0) {\n\t\t\t\ti = _inlineLayouts.erase(i);\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Inner::preloadImages() {\n\t_mosaic.forEach([](not_null<const ItemBase*> item) {\n\t\titem->preload();\n\t});\n}\n\nvoid Inner::hideInlineRowsPanel() {\n\tclearInlineRows(false);\n}\n\nvoid Inner::clearInlineRowsPanel() {\n\tclearInlineRows(false);\n}\n\nvoid Inner::refreshMosaicOffset() {\n\tconst auto top = _switchPmButton\n\t\t? (_switchPmButton->height() + st::inlineResultsSkip)\n\t\t: 0;\n\t_mosaic.setPadding(st::emojiPanMargins + QMargins(0, top, 0, 0));\n}\n\nvoid Inner::refreshSwitchPmButton(const CacheEntry *entry) {\n\tif (!entry || entry->switchPmText.isEmpty()) {\n\t\t_switchPmButton.destroy();\n\t\t_switchPmStartToken.clear();\n\t\t_switchPmUrl = QByteArray();\n\t} else {\n\t\tif (!_switchPmButton) {\n\t\t\t_switchPmButton.create(this, nullptr, st::switchPmButton);\n\t\t\t_switchPmButton->show();\n\t\t\t_switchPmButton->addClickHandler([=] { switchPm(); });\n\t\t}\n\t\t_switchPmButton->setText(rpl::single(entry->switchPmText));\n\t\t_switchPmStartToken = entry->switchPmStartToken;\n\t\t_switchPmUrl = entry->switchPmUrl;\n\t\tconst auto buttonTop = st::stickerPanPadding;\n\t\t_switchPmButton->move(st::inlineResultsLeft - st::roundRadiusSmall, buttonTop);\n\t\tif (isRestrictedView()) {\n\t\t\t_switchPmButton->hide();\n\t\t}\n\t}\n\trepaintItems();\n}\n\nint Inner::refreshInlineRows(PeerData *queryPeer, UserData *bot, const CacheEntry *entry, bool resultsDeleted) {\n\t_inlineBot = bot;\n\t_inlineQueryPeer = queryPeer;\n\trefreshSwitchPmButton(entry);\n\trefreshMosaicOffset();\n\tauto clearResults = [&] {\n\t\tif (!entry) {\n\t\t\treturn true;\n\t\t}\n\t\tif (entry->results.empty() && entry->switchPmText.isEmpty()) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\tauto clearResultsResult = clearResults(); // Clang workaround.\n\tif (clearResultsResult) {\n\t\tif (resultsDeleted) {\n\t\t\tclearInlineRows(true);\n\t\t\tdeleteUnusedInlineLayouts();\n\t\t}\n\t\t_inlineRowsCleared.fire({});\n\t\treturn 0;\n\t}\n\n\tclearSelection();\n\n\tAssert(_inlineBot != 0);\n\n\t_gallery = entry->gallery;\n\n\tconst auto count = int(entry->results.size());\n\tconst auto from = validateExistingInlineRows(entry->results);\n\tauto added = 0;\n\n\tif (count) {\n\t\tconst auto resultItems = entry->results | ranges::views::slice(\n\t\t\tfrom,\n\t\t\tcount\n\t\t) | ranges::views::transform([&](const std::shared_ptr<Result> &r) {\n\t\t\treturn layoutPrepareInlineResult(r);\n\t\t}) | ranges::views::filter([](const ItemBase *item) {\n\t\t\treturn item != nullptr;\n\t\t}) | ranges::to<std::vector<not_null<ItemBase*>>>;\n\n\t\t_mosaic.addItems(resultItems);\n\t\tadded = resultItems.size();\n\t\tpreloadImages();\n\t}\n\n\tauto h = countHeight();\n\tif (h != height()) resize(width(), h);\n\trepaintItems();\n\n\t_lastMousePos = QCursor::pos();\n\tupdateSelected();\n\n\treturn added;\n}\n\nint Inner::validateExistingInlineRows(const Results &results) {\n\tconst auto until = _mosaic.validateExistingRows([&](\n\t\t\tnot_null<const ItemBase*> item,\n\t\t\tint untilIndex) {\n\t\treturn item->getResult().get() != results[untilIndex].get();\n\t}, results.size());\n\n\tif (_mosaic.empty()) {\n\t\t_inlineWithThumb = false;\n\t\tfor (int i = until; i < results.size(); ++i) {\n\t\t\tif (results.at(i)->hasThumbDisplay()) {\n\t\t\t\t_inlineWithThumb = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\treturn until;\n}\n\nvoid Inner::inlineItemLayoutChanged(const ItemBase *layout) {\n\tif (_selected < 0 || !isVisible()) {\n\t\treturn;\n\t}\n\n\tif (const auto item = _mosaic.maybeItemAt(_selected)) {\n\t\tif (layout == item) {\n\t\t\tupdateSelected();\n\t\t}\n\t}\n}\n\nvoid Inner::inlineItemRepaint(const ItemBase *layout) {\n\tupdateInlineItems();\n}\n\nbool Inner::inlineItemVisible(const ItemBase *layout) {\n\tint32 position = layout->position();\n\tif (position < 0 || !isVisible()) {\n\t\treturn false;\n\t}\n\n\tconst auto &[row, column] = ::Layout::IndexToPosition(position);\n\n\tauto top = st::stickerPanPadding;\n\tfor (auto i = 0; i != row; ++i) {\n\t\ttop += _mosaic.rowHeightAt(i);\n\t}\n\n\treturn (top < _visibleBottom)\n\t\t&& (top + _mosaic.itemAt(row, column)->height() > _visibleTop);\n}\n\nData::FileOrigin Inner::inlineItemFileOrigin() {\n\treturn Data::FileOrigin();\n}\n\nvoid Inner::updateSelected() {\n\tif (_pressed >= 0 && !_previewShown) {\n\t\treturn;\n\t}\n\n\tconst auto p = mapFromGlobal(_lastMousePos);\n\tconst auto sx = rtl() ? (width() - p.x()) : p.x();\n\tconst auto sy = p.y();\n\tconst auto &[index, exact, relative] = _mosaic.findByPoint({ sx, sy });\n\tconst auto selected = exact ? index : -1;\n\tconst auto item = exact ? _mosaic.itemAt(selected).get() : nullptr;\n\tconst auto link = exact ? item->getState(relative, {}).link : nullptr;\n\n\tif (_selected != selected) {\n\t\tif (const auto s = _mosaic.maybeItemAt(_selected)) {\n\t\t\ts->update();\n\t\t}\n\t\t_selected = selected;\n\t\tif (item) {\n\t\t\titem->update();\n\t\t}\n\t\tif (_previewShown && _selected >= 0 && _pressed != _selected) {\n\t\t\t_pressed = _selected;\n\t\t\tif (item) {\n\t\t\t\tif (const auto preview = item->getPreviewDocument()) {\n\t\t\t\t\t_controller->widget()->showMediaPreview(\n\t\t\t\t\t\tData::FileOrigin(),\n\t\t\t\t\t\tpreview);\n\t\t\t\t} else if (const auto preview = item->getPreviewPhoto()) {\n\t\t\t\t\t_controller->widget()->showMediaPreview(\n\t\t\t\t\t\tData::FileOrigin(),\n\t\t\t\t\t\tpreview);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (ClickHandler::setActive(link, item)) {\n\t\tsetCursor(link ? style::cur_pointer : style::cur_default);\n\t\tUi::Tooltip::Hide();\n\t}\n\tif (link) {\n\t\tUi::Tooltip::Show(1000, this);\n\t}\n}\n\nvoid Inner::showPreview() {\n\tif (_pressed < 0) {\n\t\treturn;\n\t}\n\n\tif (const auto layout = _mosaic.maybeItemAt(_pressed)) {\n\t\tif (const auto previewDocument = layout->getPreviewDocument()) {\n\t\t\t_previewShown = _controller->widget()->showMediaPreview(\n\t\t\t\tData::FileOrigin(),\n\t\t\t\tpreviewDocument);\n\t\t} else if (const auto previewPhoto = layout->getPreviewPhoto()) {\n\t\t\t_previewShown = _controller->widget()->showMediaPreview(\n\t\t\t\tData::FileOrigin(),\n\t\t\t\tpreviewPhoto);\n\t\t}\n\t}\n}\n\nvoid Inner::updateInlineItems() {\n\tconst auto now = crl::now();\n\n\tconst auto delay = std::max(\n\t\t_lastScrolledAt + kMinAfterScrollDelay - now,\n\t\t_lastUpdatedAt + kMinRepaintDelay - now);\n\tif (delay <= 0) {\n\t\trepaintItems();\n\t} else if (!_updateInlineItems.isActive()\n\t\t|| _updateInlineItems.remainingTime() > kMinRepaintDelay) {\n\t\t_updateInlineItems.callOnce(std::max(delay, kMinRepaintDelay));\n\t}\n}\n\nvoid Inner::repaintItems(crl::time now) {\n\t_lastUpdatedAt = now ? now : crl::now();\n\tupdate();\n}\n\nvoid Inner::switchPm() {\n\tif (!_inlineBot || !_inlineBot->isBot()) {\n\t\treturn;\n\t} else if (!_switchPmUrl.isEmpty()) {\n\t\tconst auto bot = _inlineBot;\n\t\t_inlineBot->session().attachWebView().open({\n\t\t\t.bot = bot,\n\t\t\t.context = { .controller = _controller },\n\t\t\t.button = { .url = _switchPmUrl },\n\t\t\t.source = InlineBots::WebViewSourceSwitch(),\n\t\t});\n\t} else {\n\t\t_inlineBot->botInfo->startToken = _switchPmStartToken;\n\t\t_inlineBot->botInfo->inlineReturnTo\n\t\t\t= _controller->dialogsEntryStateCurrent();\n\t\t_controller->showPeerHistory(\n\t\t\t_inlineBot,\n\t\t\tWindow::SectionShow::Way::ClearStack,\n\t\t\tShowAndStartBotMsgId);\n\t}\n}\n\nvoid Inner::setSendMenuDetails(Fn<SendMenu::Details()> &&callback) {\n\t_sendMenuDetails = std::move(callback);\n}\n\n} // namespace Layout\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_results_inner.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/abstract_button.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/panel_animation.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"base/timer.h\"\n#include \"mtproto/sender.h\"\n#include \"inline_bots/inline_bot_layout_item.h\"\n#include \"layout/layout_mosaic.h\"\n\nnamespace Api {\nstruct SendOptions;\n} // namespace Api\n\nnamespace Ui {\nclass ScrollArea;\nclass IconButton;\nclass LinkButton;\nclass RoundButton;\nclass FlatLabel;\nclass RippleAnimation;\nclass PopupMenu;\nclass PathShiftGradient;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace InlineBots {\nclass Result;\nstruct ResultSelected;\n} // namespace InlineBots\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nnamespace InlineBots {\nnamespace Layout {\n\nclass ItemBase;\nusing Results = std::vector<std::shared_ptr<Result>>;\n\nstruct CacheEntry {\n\tQString nextOffset;\n\tQString switchPmText;\n\tQString switchPmStartToken;\n\tQByteArray switchPmUrl;\n\tResults results;\n\tbool gallery = false;\n};\n\nclass Inner\n\t: public Ui::RpWidget\n\t, public Ui::AbstractTooltipShower\n\t, public Context {\n\npublic:\n\tInner(QWidget *parent, not_null<Window::SessionController*> controller);\n\n\tvoid hideFinished();\n\n\tvoid clearSelection();\n\n\tint refreshInlineRows(PeerData *queryPeer, UserData *bot, const CacheEntry *results, bool resultsDeleted);\n\tvoid inlineBotChanged();\n\tvoid hideInlineRowsPanel();\n\tvoid clearInlineRowsPanel();\n\n\tvoid preloadImages();\n\n\tvoid inlineItemLayoutChanged(const ItemBase *layout) override;\n\tvoid inlineItemRepaint(const ItemBase *layout) override;\n\tbool inlineItemVisible(const ItemBase *layout) override;\n\tData::FileOrigin inlineItemFileOrigin() override;\n\n\tint countHeight();\n\n\tvoid setResultSelectedCallback(Fn<void(ResultSelected)> callback) {\n\t\t_resultSelectedCallback = std::move(callback);\n\t}\n\tvoid setSendMenuDetails(Fn<SendMenu::Details()> &&callback);\n\n\t// Ui::AbstractTooltipShower interface.\n\tQString tooltipText() const override;\n\tQPoint tooltipPos() const override;\n\tbool tooltipWindowActive() const override;\n\n\trpl::producer<> inlineRowsCleared() const;\n\n\t~Inner();\n\nprotected:\n\tvoid visibleTopBottomUpdated(\n\t\tint visibleTop,\n\t\tint visibleBottom) override;\n\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid leaveToChildEvent(QEvent *e, QWidget *child) override;\n\tvoid enterFromChildEvent(QEvent *e, QWidget *child) override;\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\nprivate:\n\tstatic constexpr bool kRefreshIconsScrollAnimation = true;\n\tstatic constexpr bool kRefreshIconsNoAnimation = false;\n\n\tvoid switchPm();\n\n\tvoid updateSelected();\n\tvoid checkRestrictedPeer();\n\tbool isRestrictedView();\n\tvoid clearHeavyData();\n\n\tvoid paintInlineItems(Painter &p, const QRect &r);\n\n\tvoid refreshSwitchPmButton(const CacheEntry *entry);\n\tvoid refreshMosaicOffset();\n\n\tvoid showPreview();\n\tvoid updateInlineItems();\n\tvoid repaintItems(crl::time now = 0);\n\tvoid clearInlineRows(bool resultsDeleted);\n\tItemBase *layoutPrepareInlineResult(std::shared_ptr<Result> result);\n\n\tvoid updateRestrictedLabelGeometry();\n\tvoid deleteUnusedInlineLayouts();\n\n\tint validateExistingInlineRows(const Results &results);\n\tvoid selectInlineResult(\n\t\tint index,\n\t\tApi::SendOptions options,\n\t\tbool open);\n\n\tnot_null<Window::SessionController*> _controller;\n\tconst std::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\n\tint _visibleTop = 0;\n\tint _visibleBottom = 0;\n\n\tUserData *_inlineBot = nullptr;\n\tPeerData *_inlineQueryPeer = nullptr;\n\tcrl::time _lastScrolledAt = 0;\n\tcrl::time _lastUpdatedAt = 0;\n\tbase::Timer _updateInlineItems;\n\tbool _inlineWithThumb = false;\n\tbool _gallery = false;\n\n\tobject_ptr<Ui::RoundButton> _switchPmButton = { nullptr };\n\tQString _switchPmStartToken;\n\tQByteArray _switchPmUrl;\n\n\tobject_ptr<Ui::FlatLabel> _restrictedLabel = { nullptr };\n\tQString _restrictedLabelKey;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tMosaic::Layout::MosaicLayout<InlineBots::Layout::ItemBase> _mosaic;\n\n\tstd::map<Result*, std::unique_ptr<ItemBase>> _inlineLayouts;\n\n\trpl::event_stream<> _inlineRowsCleared;\n\n\tint _selected = -1;\n\tint _pressed = -1;\n\tQPoint _lastMousePos;\n\n\tbase::Timer _previewTimer;\n\tbool _previewShown = false;\n\n\tFn<void(ResultSelected)> _resultSelectedCallback;\n\tFn<SendMenu::Details()> _sendMenuDetails;\n\n};\n\n} // namespace Layout\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_results_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"inline_bots/inline_results_widget.h\"\n\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"inline_bots/inline_results_inner.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace InlineBots {\nnamespace Layout {\nnamespace {\n\nconstexpr auto kInlineBotRequestDelay = 400;\n\n} // namespace\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _api(&_controller->session().mtp())\n, _contentMaxHeight(st::emojiPanMaxHeight)\n, _contentHeight(_contentMaxHeight)\n, _shadow(st::emojiPanAnimation.shadow)\n, _scroll(this, st::inlineBotsScroll)\n, _innerRounding(Ui::PrepareCornerPixmaps(\n\tImageRoundRadius::Small,\n\tst::emojiPanBg))\n, _inlineRequestTimer([=] { onInlineRequest(); }) {\n\tresize(QRect(0, 0, st::emojiPanWidth, _contentHeight).marginsAdded(innerPadding()).size());\n\t_width = width();\n\t_height = height();\n\n\t_scroll->resize(st::emojiPanWidth - st::roundRadiusSmall, _contentHeight);\n\n\t_scroll->move(verticalRect().topLeft());\n\t_inner = _scroll->setOwnedWidget(object_ptr<Inner>(this, controller));\n\n\t_inner->moveToLeft(0, 0, _scroll->width());\n\n\t_scroll->scrolls(\n\t) | rpl::on_next([=] {\n\t\tonScroll();\n\t}, lifetime());\n\n\t_inner->inlineRowsCleared(\n\t) | rpl::on_next([=] {\n\t\thideAnimated();\n\t\t_inner->clearInlineRowsPanel();\n\t}, lifetime());\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_innerRounding = Ui::PrepareCornerPixmaps(\n\t\t\tImageRoundRadius::Small,\n\t\t\tst::emojiPanBg);\n\t}, lifetime());\n\n\tmacWindowDeactivateEvents(\n\t) | rpl::filter([=] {\n\t\treturn !isHidden();\n\t}) | rpl::on_next([=] {\n\t\tleaveEvent(nullptr);\n\t}, lifetime());\n\n\t// Inner widget has OpaquePaintEvent attribute so it doesn't repaint on scroll.\n\t// But we should force it to repaint so that GIFs will continue to animate without update() calls.\n\t// We do that by creating a transparent widget above our _inner.\n\tauto forceRepaintOnScroll = object_ptr<RpWidget>(this);\n\tforceRepaintOnScroll->setGeometry(innerRect().x() + st::roundRadiusSmall, innerRect().y() + st::roundRadiusSmall, st::roundRadiusSmall, st::roundRadiusSmall);\n\tforceRepaintOnScroll->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tforceRepaintOnScroll->show();\n\n\tsetMouseTracking(true);\n\tsetAttribute(Qt::WA_OpaquePaintEvent, false);\n}\n\nvoid Widget::moveBottom(int bottom) {\n\t_bottom = bottom;\n\tupdateContentHeight();\n}\n\nvoid Widget::updateContentHeight() {\n\tauto addedHeight = innerPadding().top() + innerPadding().bottom();\n\tauto wantedContentHeight = qRound(st::emojiPanHeightRatio * _bottom) - addedHeight;\n\tauto contentHeight = std::clamp(\n\t\twantedContentHeight,\n\t\tst::inlineResultsMinHeight,\n\t\tst::inlineResultsMaxHeight);\n\taccumulate_min(contentHeight, _bottom - addedHeight);\n\taccumulate_min(contentHeight, _contentMaxHeight);\n\tauto resultTop = _bottom - addedHeight - contentHeight;\n\tif (contentHeight == _contentHeight) {\n\t\tmove(x(), resultTop);\n\t\treturn;\n\t}\n\n\tauto was = _contentHeight;\n\t_contentHeight = contentHeight;\n\n\tresize(QRect(0, 0, innerRect().width(), _contentHeight).marginsAdded(innerPadding()).size());\n\t_height = height();\n\tmoveToLeft(0, resultTop);\n\n\tif (was > _contentHeight) {\n\t\t_scroll->resize(_scroll->width(), _contentHeight);\n\t\tauto scrollTop = _scroll->scrollTop();\n\t\t_inner->setVisibleTopBottom(scrollTop, scrollTop + _contentHeight);\n\t} else {\n\t\tauto scrollTop = _scroll->scrollTop();\n\t\t_inner->setVisibleTopBottom(scrollTop, scrollTop + _contentHeight);\n\t\t_scroll->resize(_scroll->width(), _contentHeight);\n\t}\n\n\tupdate();\n}\n\nvoid Widget::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tauto opacityAnimating = _a_opacity.animating();\n\n\tauto showAnimating = _a_show.animating();\n\tif (_showAnimation && !showAnimating) {\n\t\t_showAnimation.reset();\n\t\tif (!opacityAnimating) {\n\t\t\tshowChildren();\n\t\t}\n\t}\n\n\tif (showAnimating) {\n\t\tAssert(_showAnimation != nullptr);\n\t\tif (auto opacity = _a_opacity.value(_hiding ? 0. : 1.)) {\n\t\t\t_showAnimation->paintFrame(p, 0, 0, width(), _a_show.value(1.), opacity);\n\t\t}\n\t} else if (opacityAnimating) {\n\t\tp.setOpacity(_a_opacity.value(_hiding ? 0. : 1.));\n\t\tp.drawPixmap(0, 0, _cache);\n\t} else if (_hiding || isHidden()) {\n\t\thideFinished();\n\t} else {\n\t\tif (!_cache.isNull()) _cache = QPixmap();\n\t\tif (!_inPanelGrab) _shadow.paint(p, innerRect(), st::roundRadiusSmall);\n\t\tpaintContent(p);\n\t}\n}\n\nvoid Widget::paintContent(QPainter &p) {\n\tauto inner = innerRect();\n\tconst auto radius = st::roundRadiusSmall;\n\n\tconst auto top = Ui::CornersPixmaps{\n\t\t.p = { _innerRounding.p[0], _innerRounding.p[1], QPixmap(), QPixmap() },\n\t};\n\tUi::FillRoundRect(p, inner.x(), inner.y(), inner.width(), radius, st::emojiPanBg, top);\n\n\tconst auto bottom = Ui::CornersPixmaps{\n\t\t.p = { QPixmap(), QPixmap(), _innerRounding.p[2], _innerRounding.p[3] },\n\t};\n\tUi::FillRoundRect(p, inner.x(), inner.y() + inner.height() - radius, inner.width(), radius, st::emojiPanBg, bottom);\n\n\tauto horizontal = horizontalRect();\n\tauto sidesTop = horizontal.y();\n\tauto sidesHeight = horizontal.height();\n\tp.fillRect(myrtlrect(inner.x() + inner.width() - st::emojiScroll.width, sidesTop, st::emojiScroll.width, sidesHeight), st::emojiPanBg);\n\tp.fillRect(myrtlrect(inner.x(), sidesTop, st::roundRadiusSmall, sidesHeight), st::emojiPanBg);\n}\n\nvoid Widget::moveByBottom() {\n\tupdateContentHeight();\n}\n\nvoid Widget::hideFast() {\n\tif (isHidden()) return;\n\n\t_hiding = false;\n\t_a_opacity.stop();\n\thideFinished();\n}\n\nvoid Widget::opacityAnimationCallback() {\n\tupdate();\n\tif (!_a_opacity.animating()) {\n\t\tif (_hiding) {\n\t\t\t_hiding = false;\n\t\t\thideFinished();\n\t\t} else if (!_a_show.animating()) {\n\t\t\tshowChildren();\n\t\t}\n\t}\n}\n\nvoid Widget::prepareCache() {\n\tif (_a_opacity.animating()) return;\n\n\tauto showAnimation = base::take(_a_show);\n\tauto showAnimationData = base::take(_showAnimation);\n\tshowChildren();\n\t_cache = Ui::GrabWidget(this);\n\t_showAnimation = base::take(showAnimationData);\n\t_a_show = base::take(showAnimation);\n\tif (_a_show.animating()) {\n\t\thideChildren();\n\t}\n}\n\nvoid Widget::startOpacityAnimation(bool hiding) {\n\t_hiding = false;\n\tprepareCache();\n\t_hiding = hiding;\n\thideChildren();\n\t_a_opacity.start([this] { opacityAnimationCallback(); }, _hiding ? 1. : 0., _hiding ? 0. : 1., st::emojiPanDuration);\n}\n\nvoid Widget::startShowAnimation() {\n\tif (!_a_show.animating()) {\n\t\tauto cache = base::take(_cache);\n\t\tauto opacityAnimation = base::take(_a_opacity);\n\t\tshowChildren();\n\t\tauto image = grabForPanelAnimation();\n\t\t_a_opacity = base::take(opacityAnimation);\n\t\t_cache = base::take(cache);\n\n\t\t_showAnimation = std::make_unique<Ui::PanelAnimation>(st::emojiPanAnimation, Ui::PanelAnimation::Origin::BottomLeft);\n\t\tauto inner = rect().marginsRemoved(st::emojiPanMargins);\n\t\t_showAnimation->setFinalImage(\n\t\t\tstd::move(image),\n\t\t\tQRect(\n\t\t\t\tinner.topLeft() * style::DevicePixelRatio(),\n\t\t\t\tinner.size() * style::DevicePixelRatio()),\n\t\t\tst::emojiPanRadius);\n\t\t_showAnimation->setCornerMasks(Images::CornersMask(ImageRoundRadius::Small));\n\t\t_showAnimation->start();\n\t}\n\thideChildren();\n\t_a_show.start([this] { update(); }, 0., 1., st::emojiPanShowDuration);\n}\n\nQImage Widget::grabForPanelAnimation() {\n\tUi::SendPendingMoveResizeEvents(this);\n\tauto result = QImage(\n\t\tsize() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult.fill(Qt::transparent);\n\t_inPanelGrab = true;\n\trender(&result);\n\t_inPanelGrab = false;\n\treturn result;\n}\n\nvoid Widget::setResultSelectedCallback(Fn<void(ResultSelected)> callback) {\n\t_inner->setResultSelectedCallback(std::move(callback));\n}\n\nvoid Widget::setSendMenuDetails(Fn<SendMenu::Details()> &&callback) {\n\t_inner->setSendMenuDetails(std::move(callback));\n}\n\nvoid Widget::hideAnimated() {\n\tif (isHidden()) return;\n\tif (_hiding) return;\n\n\tstartOpacityAnimation(true);\n}\n\nWidget::~Widget() = default;\n\nvoid Widget::hideFinished() {\n\thide();\n\t_controller->disableGifPauseReason(\n\t\tWindow::GifPauseReason::InlineResults);\n\n\t_inner->hideFinished();\n\t_a_show.stop();\n\t_showAnimation.reset();\n\t_cache = QPixmap();\n\t_hiding = false;\n\n\t_scroll->scrollToY(0);\n}\n\nvoid Widget::showAnimated() {\n\tshowStarted();\n}\n\nvoid Widget::showStarted() {\n\tif (isHidden()) {\n\t\trecountContentMaxHeight();\n\t\t_inner->preloadImages();\n\t\tshow();\n\t\t_controller->enableGifPauseReason(\n\t\t\tWindow::GifPauseReason::InlineResults);\n\t\tstartShowAnimation();\n\t} else if (_hiding) {\n\t\tstartOpacityAnimation(false);\n\t}\n}\n\nvoid Widget::onScroll() {\n\tauto st = _scroll->scrollTop();\n\tif (st + _scroll->height() > _scroll->scrollTopMax()) {\n\t\tonInlineRequest();\n\t}\n\t_inner->setVisibleTopBottom(st, st + _scroll->height());\n}\n\nstyle::margins Widget::innerPadding() const {\n\treturn st::emojiPanMargins;\n}\n\nQRect Widget::innerRect() const {\n\treturn rect().marginsRemoved(innerPadding());\n}\n\nQRect Widget::horizontalRect() const {\n\treturn innerRect().marginsRemoved(style::margins(0, st::roundRadiusSmall, 0, st::roundRadiusSmall));\n}\n\nQRect Widget::verticalRect() const {\n\treturn innerRect().marginsRemoved(style::margins(st::roundRadiusSmall, 0, st::roundRadiusSmall, 0));\n}\n\nvoid Widget::clearInlineBot() {\n\tinlineBotChanged();\n}\n\nbool Widget::overlaps(const QRect &globalRect) const {\n\tif (isHidden() || !_cache.isNull()) return false;\n\n\tauto testRect = QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size());\n\tauto inner = rect().marginsRemoved(st::emojiPanMargins);\n\treturn inner.marginsRemoved(QMargins(st::roundRadiusSmall, 0, st::roundRadiusSmall, 0)).contains(testRect)\n\t\t|| inner.marginsRemoved(QMargins(0, st::roundRadiusSmall, 0, st::roundRadiusSmall)).contains(testRect);\n}\n\nvoid Widget::inlineBotChanged() {\n\tif (!_inlineBot) {\n\t\treturn;\n\t}\n\n\tif (!isHidden() && !_hiding) {\n\t\thideAnimated();\n\t}\n\n\t_api.request(base::take(_inlineRequestId)).cancel();\n\t_inlineQuery = _inlineNextQuery = _inlineNextOffset = QString();\n\t_inlineBot = nullptr;\n\t_inlineCache.clear();\n\t_inner->inlineBotChanged();\n\t_inner->hideInlineRowsPanel();\n\n\t_requesting.fire(false);\n}\n\nvoid Widget::inlineResultsDone(const MTPmessages_BotResults &result) {\n\t_inlineRequestId = 0;\n\t_requesting.fire(false);\n\n\tauto it = _inlineCache.find(_inlineQuery);\n\tauto adding = (it != _inlineCache.cend());\n\tif (result.type() == mtpc_messages_botResults) {\n\t\tauto &d = result.c_messages_botResults();\n\t\t_controller->session().data().processUsers(d.vusers());\n\n\t\tauto &v = d.vresults().v;\n\t\tauto queryId = d.vquery_id().v;\n\n\t\tif (it == _inlineCache.cend()) {\n\t\t\tit = _inlineCache.emplace(\n\t\t\t\t_inlineQuery,\n\t\t\t\tstd::make_unique<CacheEntry>()).first;\n\t\t}\n\t\tauto entry = it->second.get();\n\t\tentry->nextOffset = qs(d.vnext_offset().value_or_empty());\n\t\tif (const auto switchPm = d.vswitch_pm()) {\n\t\t\tentry->switchPmText = qs(switchPm->data().vtext());\n\t\t\tentry->switchPmStartToken = qs(switchPm->data().vstart_param());\n\t\t\tentry->switchPmUrl = QByteArray();\n\t\t} else if (const auto switchWebView = d.vswitch_webview()) {\n\t\t\tentry->switchPmText = qs(switchWebView->data().vtext());\n\t\t\tentry->switchPmStartToken = QString();\n\t\t\tentry->switchPmUrl = switchWebView->data().vurl().v;\n\t\t}\n\t\tentry->gallery = d.is_gallery();\n\n\t\tif (const auto count = v.size()) {\n\t\t\tentry->results.reserve(entry->results.size() + count);\n\t\t}\n\t\tauto added = 0;\n\t\tfor (const auto &res : v) {\n\t\t\tauto result = InlineBots::Result::Create(\n\t\t\t\t&_controller->session(),\n\t\t\t\tqueryId,\n\t\t\t\tres);\n\t\t\tif (result) {\n\t\t\t\t++added;\n\t\t\t\tentry->results.push_back(std::move(result));\n\t\t\t}\n\t\t}\n\n\t\tif (!added) {\n\t\t\tentry->nextOffset = QString();\n\t\t}\n\t} else if (adding) {\n\t\tit->second->nextOffset = QString();\n\t}\n\n\tif (!showInlineRows(!adding)) {\n\t\tit->second->nextOffset = QString();\n\t}\n\tonScroll();\n}\n\nvoid Widget::queryInlineBot(UserData *bot, PeerData *peer, QString query) {\n\tbool force = false;\n\t_inlineQueryPeer = peer;\n\tif (bot != _inlineBot) {\n\t\tinlineBotChanged();\n\t\t_inlineBot = bot;\n\t\tforce = true;\n\t}\n\n\tif (_inlineQuery != query || force) {\n\t\tif (_inlineRequestId) {\n\t\t\t_api.request(_inlineRequestId).cancel();\n\t\t\t_inlineRequestId = 0;\n\t\t\t_requesting.fire(false);\n\t\t}\n\t\tif (_inlineCache.find(query) != _inlineCache.cend()) {\n\t\t\t_inlineRequestTimer.cancel();\n\t\t\t_inlineQuery = _inlineNextQuery = query;\n\t\t\tshowInlineRows(true);\n\t\t} else {\n\t\t\t_inlineNextQuery = query;\n\t\t\t_inlineRequestTimer.callOnce(kInlineBotRequestDelay);\n\t\t}\n\t}\n}\n\nvoid Widget::onInlineRequest() {\n\tif (_inlineRequestId || !_inlineBot || !_inlineQueryPeer) return;\n\t_inlineQuery = _inlineNextQuery;\n\n\tQString nextOffset;\n\tauto it = _inlineCache.find(_inlineQuery);\n\tif (it != _inlineCache.cend()) {\n\t\tnextOffset = it->second->nextOffset;\n\t\tif (nextOffset.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t}\n\t_requesting.fire(true);\n\t_inlineRequestId = _api.request(MTPmessages_GetInlineBotResults(\n\t\tMTP_flags(0),\n\t\t_inlineBot->inputUser(),\n\t\t_inlineQueryPeer->input(),\n\t\tMTPInputGeoPoint(),\n\t\tMTP_string(_inlineQuery),\n\t\tMTP_string(nextOffset)\n\t)).done([=](const MTPmessages_BotResults &result) {\n\t\tinlineResultsDone(result);\n\t}).fail([=] {\n\t\t// show error?\n\t\t_requesting.fire(false);\n\t\t_inlineRequestId = 0;\n\t}).handleAllErrors().send();\n}\n\nbool Widget::refreshInlineRows(int *added) {\n\tauto it = _inlineCache.find(_inlineQuery);\n\tconst CacheEntry *entry = nullptr;\n\tif (it != _inlineCache.cend()) {\n\t\tif (!it->second->results.empty() || !it->second->switchPmText.isEmpty()) {\n\t\t\tentry = it->second.get();\n\t\t}\n\t\t_inlineNextOffset = it->second->nextOffset;\n\t}\n\tif (!entry) prepareCache();\n\tauto result = _inner->refreshInlineRows(_inlineQueryPeer, _inlineBot, entry, false);\n\tif (added) *added = result;\n\treturn (entry != nullptr);\n}\n\nint Widget::showInlineRows(bool newResults) {\n\tauto added = 0;\n\tauto clear = !refreshInlineRows(&added);\n\tif (newResults) {\n\t\t_scroll->scrollToY(0);\n\t}\n\n\tauto hidden = isHidden();\n\tif (!hidden && !clear) {\n\t\trecountContentMaxHeight();\n\t}\n\tif (clear) {\n\t\tif (!hidden) {\n\t\t\thideAnimated();\n\t\t} else if (!_hiding) {\n\t\t\t_cache = QPixmap(); // clear after refreshInlineRows()\n\t\t}\n\t} else {\n\t\tif (hidden || _hiding) {\n\t\t\tshowAnimated();\n\t\t}\n\t}\n\n\treturn added;\n}\n\nvoid Widget::recountContentMaxHeight() {\n\t_contentMaxHeight = _inner->countHeight();\n\tupdateContentHeight();\n}\n\n} // namespace Layout\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/inline_bots/inline_results_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/abstract_button.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/panel_animation.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"base/timer.h\"\n#include \"mtproto/sender.h\"\n#include \"inline_bots/inline_bot_layout_item.h\"\n\nnamespace Api {\nstruct SendOptions;\n} // namespace Api\n\nnamespace Ui {\nclass ScrollArea;\nclass IconButton;\nclass LinkButton;\nclass RoundButton;\nclass FlatLabel;\nclass RippleAnimation;\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Dialogs {\nstruct EntryState;\n} // namespace Dialogs\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace InlineBots {\nclass Result;\nstruct ResultSelected;\n} // namespace InlineBots\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nnamespace InlineBots {\nnamespace Layout {\n\nstruct CacheEntry;\nclass Inner;\n\nclass Widget : public Ui::RpWidget {\npublic:\n\tWidget(QWidget *parent, not_null<Window::SessionController*> controller);\n\t~Widget();\n\n\tvoid moveBottom(int bottom);\n\n\tvoid hideFast();\n\tbool hiding() const {\n\t\treturn _hiding;\n\t}\n\n\tvoid queryInlineBot(UserData *bot, PeerData *peer, QString query);\n\tvoid clearInlineBot();\n\n\tbool overlaps(const QRect &globalRect) const;\n\n\tvoid showAnimated();\n\tvoid hideAnimated();\n\n\tvoid setResultSelectedCallback(Fn<void(ResultSelected)> callback);\n\tvoid setSendMenuDetails(Fn<SendMenu::Details()> &&callback);\n\n\t[[nodiscard]] rpl::producer<bool> requesting() const {\n\t\treturn _requesting.events();\n\t}\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid moveByBottom();\n\tvoid paintContent(QPainter &p);\n\n\tstyle::margins innerPadding() const;\n\n\tvoid onScroll();\n\tvoid onInlineRequest();\n\n\t// Rounded rect which has shadow around it.\n\tQRect innerRect() const;\n\n\t// Inner rect with removed st::roundRadiusSmall from top and bottom.\n\t// This one is allowed to be not rounded.\n\tQRect horizontalRect() const;\n\n\t// Inner rect with removed st::roundRadiusSmall from left and right.\n\t// This one is allowed to be not rounded.\n\tQRect verticalRect() const;\n\n\tQImage grabForPanelAnimation();\n\tvoid startShowAnimation();\n\tvoid startOpacityAnimation(bool hiding);\n\tvoid prepareCache();\n\n\tclass Container;\n\tvoid opacityAnimationCallback();\n\n\tvoid hideFinished();\n\tvoid showStarted();\n\n\tvoid updateContentHeight();\n\n\tvoid inlineBotChanged();\n\tint showInlineRows(bool newResults);\n\tvoid recountContentMaxHeight();\n\tbool refreshInlineRows(int *added = nullptr);\n\tvoid inlineResultsDone(const MTPmessages_BotResults &result);\n\n\tconst not_null<Window::SessionController*> _controller;\n\tMTP::Sender _api;\n\n\tint _contentMaxHeight = 0;\n\tint _contentHeight = 0;\n\n\tint _width = 0;\n\tint _height = 0;\n\tint _bottom = 0;\n\n\tstd::unique_ptr<Ui::PanelAnimation> _showAnimation;\n\tUi::Animations::Simple _a_show;\n\n\tbool _hiding = false;\n\tQPixmap _cache;\n\tUi::Animations::Simple _a_opacity;\n\tbool _inPanelGrab = false;\n\tUi::BoxShadow _shadow;\n\n\tobject_ptr<Ui::ScrollArea> _scroll;\n\tQPointer<Inner> _inner;\n\tUi::CornersPixmaps _innerRounding;\n\n\tstd::map<QString, std::unique_ptr<CacheEntry>> _inlineCache;\n\tbase::Timer _inlineRequestTimer;\n\n\tUserData *_inlineBot = nullptr;\n\tPeerData *_inlineQueryPeer = nullptr;\n\tQString _inlineQuery, _inlineNextQuery, _inlineNextOffset;\n\tmtpRequestId _inlineRequestId = 0;\n\n\trpl::event_stream<bool> _requesting;\n\n};\n\n} // namespace Layout\n} // namespace InlineBots\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\nusing \"ui/widgets/widgets.style\";\n\ncountryRipple: defaultRippleAnimation;\n\nintroCoverHeight: 208px;\nintroCoverMaxWidth: 880px;\nintroCoverIconsMinSkip: 120px;\nintroCoverLeft: icon {{ \"intro_left\", introCoverIconsFg }};\nintroCoverRight: icon {{ \"intro_right\", introCoverIconsFg }};\nintroCoverIcon: icon {\n\t{ \"intro_plane_trace\", introCoverPlaneTrace },\n\t{ \"intro_plane_inner\", introCoverPlaneInner },\n\t{ \"intro_plane_outer\", introCoverPlaneOuter },\n\t{ \"intro_plane_top\", introCoverPlaneTop },\n};\nintroCoverIconLeft: 50px;\nintroCoverIconTop: 46px;\n\nintroSettingsSkip: 10px;\n\nintroPhotoTop: 10px;\n\nintroCoverTitle: FlatLabel(defaultFlatLabel) {\n\ttextFg: introTitleFg;\n\talign: align(center);\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(22px semibold);\n\t}\n}\nintroCoverTitleTop: 136px;\nintroCoverDescription: FlatLabel(defaultFlatLabel) {\n\ttextFg: introDescriptionFg;\n\talign: align(center);\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(15px);\n\t\tlineHeight: 24px;\n\t}\n}\nintroCoverDescriptionTop: 174px;\nintroTitle: FlatLabel(defaultFlatLabel) {\n\ttextFg: introTitleFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(17px semibold);\n\t}\n}\nintroTitleTop: 1px;\nintroDescription: FlatLabel(defaultFlatLabel) {\n\tminWidth: 300px;\n\ttextFg: introDescriptionFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tlineHeight: 20px;\n\t}\n}\nintroDescriptionTop: 34px;\n\nintroLink: defaultLinkButton;\n\nintroPlaneWidth: 48px;\nintroPlaneHeight: 38px;\nintroHeight: 406px;\nintroStepTopMin: 76px;\nintroStepWidth: 380px;\nintroNextTop: 266px;\nintroNextSlide: 200px;\nintroStepHeight: 384px;\nintroContentTopAdd: 30px;\nintroStepHeightFull: 590px;\nintroSlideDuration: 200;\nintroCoverDuration: 200;\n\nintroNextButton: RoundButton(defaultActiveButton) {\n\twidth: 300px;\n\theight: 42px;\n\tradius: 6px;\n\ttextTop: 11px;\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(boxFontSize semibold);\n\t}\n}\nintroFragmentIcon: icon{{ \"fragment\", activeButtonFg }};\nintroFragmentIconOver: icon{{ \"fragment\", activeButtonFgOver }};\nintroFragmentButton: RoundButton(introNextButton) {\n\ticon: introFragmentIcon;\n\ticonOver: introFragmentIconOver;\n\ticonPosition: point(-10px, 9px);\n}\n\nintroStepFieldTop: 96px;\nintroPhoneTop: 6px;\nintroLinkTop: 24px;\nintroCountry: InputField(defaultInputField) {\n\ttextMargins: margins(3px, 27px, 3px, 6px);\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(16px);\n\t}\n\twidth: 300px;\n\theightMin: 61px;\n}\nintroCountryCode: InputField(introCountry) {\n\twidth: 64px;\n\ttextAlign: align(top);\n}\nintroPhone: InputField(introCountry) {\n\ttextMargins: margins(12px, 27px, 12px, 6px);\n\twidth: 225px;\n}\nintroQrLoginLinkTop: 368px;\nintroCode: introCountry;\nintroName: introCountry;\nintroPassword: introCountry;\nintroPasswordTop: 74px;\nintroPasswordHintTop: 151px;\n\nintroCodeDigitFont: font(20px);\nintroCodeDigitHeight: 50px;\nintroCodeDigitBorderWidth: 4px;\nintroCodeDigitSkip: 10px;\n\nintroPasswordHint: FlatLabel(introDescription) {\n\ttextFg: windowFg;\n}\n\nintroResetButton: RoundButton(defaultLightButton) {\n\ttextFg: attentionButtonFg;\n\ttextFgOver: attentionButtonFgOver;\n\ttextBgOver: attentionButtonBgOver;\n\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: attentionButtonBgRipple;\n\t}\n}\nintroResetBottom: 20px;\n\nintroTermsLabel: FlatLabel(defaultFlatLabel) {\n\talign: align(top);\n}\nintroTermsBottom: 20px;\n\nintroCountryIcon: icon {{ \"intro_country_dropdown\", menuIconFg }};\nintroCountryIconPosition: point(8px, 37px);\n\nintroErrorTop: 235px;\nintroErrorBelowLinkTop: 220px;\n\nintroError: FlatLabel(introDescription) {\n}\nintroErrorCentered: FlatLabel(introError) {\n\talign: align(center);\n}\n\nintroBackButton: IconButton(defaultIconButton) {\n\twidth: 56px;\n\theight: 56px;\n\n\ticon: backButtonIcon;\n\ticonOver: backButtonIconOver;\n\n\trippleAreaPosition: point(8px, 8px);\n\trippleAreaSize: 40px;\n\tripple: defaultRippleAnimationBgOver;\n}\n\nintroQrTop: -18px;\nintroQrPixel: 50px; // large enough\nintroQrMaxSize: 180px;\nintroQrBackgroundSkip: 12px;\nintroQrBackgroundRadius: 8px;\nintroQrLabelsWidth: 292px;\nintroQrTitleWidth: 320px;\nintroQrTitle: FlatLabel(defaultFlatLabel) {\n\ttextFg: introTitleFg;\n\talign: align(top);\n\tminWidth: introQrTitleWidth;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(20px semibold);\n\t}\n}\nintroQrErrorTop: 336px;\nintroQrTitleTop: 196px;\nintroQrStep: FlatLabel(defaultFlatLabel) {\n\tminWidth: 200px;\n}\nintroQrStepsTop: 232px;\nintroQrStepMargins: margins(20px, 8px, 0px, 0px);\nintroQrSkipTop: 360px;\nintroQrCenterSize: 44px;\nintroQrPlane: icon {{ \"intro_qr_plane\", activeButtonFg }};\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_code.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"intro/intro_code.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"intro/intro_code_input.h\"\n#include \"intro/intro_signup.h\"\n#include \"intro/intro_password_check.h\"\n#include \"boxes/abstract_box.h\"\n#include \"core/file_utilities.h\"\n#include \"core/update_checker.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/fields/masked_input_field.h\"\n#include \"ui/text/format_values.h\" // Ui::FormatPhone\n#include \"ui/text/text_utilities.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"main/main_account.h\"\n#include \"mtproto/mtp_instance.h\"\n#include \"styles/style_intro.h\"\n\nnamespace Intro {\nnamespace details {\n\nCodeWidget::CodeWidget(\n\tQWidget *parent,\n\tnot_null<Main::Account*> account,\n\tnot_null<Data*> data)\n: Step(parent, account, data)\n, _noTelegramCode(this, tr::lng_code_no_telegram(tr::now), st::introLink)\n, _code(this)\n, _callTimer([=] { sendCall(); })\n, _callStatus(getData()->callStatus)\n, _callTimeout(getData()->callTimeout)\n, _callLabel(this, st::introDescription)\n, _checkRequestTimer([=] { checkRequest(); }) {\n\tLang::Updated(\n\t) | rpl::on_next([=] {\n\t\trefreshLang();\n\t}, lifetime());\n\n\t_noTelegramCode->addClickHandler([=] { noTelegramCode(); });\n\n\t_code->setDigitsCountMax(getData()->codeLength);\n\n\tupdateDescText();\n\tsetTitleText(_isFragment.value(\n\t) | rpl::map([=](bool isFragment) {\n\t\treturn !isFragment\n\t\t\t? rpl::single(Ui::FormatPhone(getData()->phone))\n\t\t\t: tr::lng_intro_fragment_title();\n\t}) | rpl::flatten_latest());\n\n\taccount->setHandleLoginCode([=](const QString &code) {\n\t\t_code->setCode(code);\n\t\t_code->requestCode();\n\t});\n\n\t_code->codeCollected(\n\t) | rpl::on_next([=](const QString &code) {\n\t\thideError();\n\t\tsubmitCode(code);\n\t}, lifetime());\n}\n\nvoid CodeWidget::refreshLang() {\n\tif (_noTelegramCode) {\n\t\t_noTelegramCode->setText(tr::lng_code_no_telegram(tr::now));\n\t}\n\tupdateDescText();\n\tupdateControlsGeometry();\n}\n\nint CodeWidget::errorTop() const {\n\treturn contentTop() + st::introErrorBelowLinkTop;\n}\n\nvoid CodeWidget::updateDescText() {\n\tconst auto byTelegram = getData()->codeByTelegram;\n\tconst auto isFragment = !getData()->codeByFragmentUrl.isEmpty();\n\t_isFragment = isFragment;\n\tconst auto emailPattern = !getData()->emailPatternSetup.isEmpty()\n\t\t? getData()->emailPatternSetup\n\t\t: getData()->emailPatternLogin;\n\tsetDescriptionText(!emailPattern.isEmpty()\n\t\t? tr::lng_intro_email_confirm_subtitle(\n\t\t\tlt_email,\n\t\t\trpl::single(Ui::Text::WrapEmailPattern(emailPattern)),\n\t\t\ttr::marked)\n\t\t: isFragment\n\t\t? tr::lng_intro_fragment_about(\n\t\t\tlt_phone_number,\n\t\t\trpl::single(\n\t\t\t\tTextWithEntities::Simple(Ui::FormatPhone(getData()->phone))),\n\t\t\ttr::rich)\n\t\t: (byTelegram ? tr::lng_code_from_telegram : tr::lng_code_desc)(\n\t\t\ttr::rich));\n\tif (getData()->codeByTelegram) {\n\t\t_noTelegramCode->show();\n\t\t_callTimer.cancel();\n\t} else {\n\t\t_noTelegramCode->hide();\n\t\t_callStatus = getData()->callStatus;\n\t\t_callTimeout = getData()->callTimeout;\n\t\tif (_callStatus == CallStatus::Waiting && !_callTimer.isActive()) {\n\t\t\t_callTimer.callEach(1000);\n\t\t}\n\t}\n\tupdateCallText();\n}\n\nvoid CodeWidget::updateCallText() {\n\tauto text = ([this]() -> QString {\n\t\tif (getData()->codeByTelegram) {\n\t\t\treturn QString();\n\t\t}\n\t\tswitch (_callStatus) {\n\t\tcase CallStatus::Waiting: {\n\t\t\tif (_callTimeout >= 3600) {\n\t\t\t\treturn tr::lng_code_call(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_minutes,\n\t\t\t\t\t(u\"%1:%2\"_q\n\t\t\t\t\t).arg(_callTimeout / 3600\n\t\t\t\t\t).arg((_callTimeout / 60) % 60, 2, 10, QChar('0')),\n\t\t\t\t\tlt_seconds,\n\t\t\t\t\tu\"%1\"_q.arg(_callTimeout % 60, 2, 10, QChar('0')));\n\t\t\t} else {\n\t\t\t\treturn tr::lng_code_call(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_minutes,\n\t\t\t\t\tQString::number(_callTimeout / 60),\n\t\t\t\t\tlt_seconds,\n\t\t\t\t\tu\"%1\"_q.arg(_callTimeout % 60, 2, 10, QChar('0')));\n\t\t\t}\n\t\t} break;\n\t\tcase CallStatus::Calling:\n\t\t\treturn tr::lng_code_calling(tr::now);\n\t\tcase CallStatus::Called:\n\t\t\treturn tr::lng_code_called(tr::now);\n\t\t}\n\t\treturn QString();\n\t})();\n\t_callLabel->setText(text);\n\t_callLabel->setVisible(!text.isEmpty() && !animating());\n}\n\nvoid CodeWidget::resizeEvent(QResizeEvent *e) {\n\tStep::resizeEvent(e);\n\tupdateControlsGeometry();\n}\n\nvoid CodeWidget::updateControlsGeometry() {\n\t_code->moveToLeft(\n\t\tcontentLeft() - st::shakeShift - st::lineWidth,\n\t\tcontentTop() + st::introStepFieldTop + st::introPhoneTop * 3);\n\tauto linkTop = _code->y() + _code->height() + st::introLinkTop;\n\t_noTelegramCode->moveToLeft(contentLeft() + st::buttonRadius, linkTop);\n\t_callLabel->moveToLeft(contentLeft() + st::buttonRadius, linkTop);\n}\n\nvoid CodeWidget::showCodeError(rpl::producer<QString> text) {\n\t_code->showError();\n\tshowError(std::move(text));\n}\n\nvoid CodeWidget::setInnerFocus() {\n\t_code->setFocus();\n}\n\nvoid CodeWidget::activate() {\n\tStep::activate();\n\t_code->show();\n\tif (getData()->codeByTelegram) {\n\t\t_noTelegramCode->show();\n\t} else {\n\t\t_callLabel->show();\n\t}\n\tsetInnerFocus();\n}\n\nvoid CodeWidget::finished() {\n\tStep::finished();\n\taccount().setHandleLoginCode(nullptr);\n\t_checkRequestTimer.cancel();\n\t_callTimer.cancel();\n\tapiClear();\n\n\tcancelled();\n\t_sentCode.clear();\n\t_code->clear();\n}\n\nvoid CodeWidget::cancelled() {\n\tapi().request(base::take(_sentRequest)).cancel();\n\tapi().request(base::take(_callRequestId)).cancel();\n\tapi().request(MTPauth_CancelCode(\n\t\tMTP_string(getData()->phone),\n\t\tMTP_bytes(getData()->phoneHash)\n\t)).send();\n}\n\nvoid CodeWidget::stopCheck() {\n\t_checkRequestTimer.cancel();\n}\n\nvoid CodeWidget::checkRequest() {\n\tauto status = api().instance().state(_sentRequest);\n\tif (status < 0) {\n\t\tauto leftms = -status;\n\t\tif (leftms >= 1000) {\n\t\t\tif (_sentRequest) {\n\t\t\t\tapi().request(base::take(_sentRequest)).cancel();\n\t\t\t\t_sentCode.clear();\n\t\t\t}\n\t\t}\n\t}\n\tif (!_sentRequest && status == MTP::RequestSent) {\n\t\tstopCheck();\n\t}\n}\n\nvoid CodeWidget::codeSubmitDone(const MTPauth_Authorization &result) {\n\tstopCheck();\n\t_code->setEnabled(true);\n\t_sentRequest = 0;\n\tfinish(result);\n}\n\nvoid CodeWidget::emailVerifyDone(const MTPaccount_EmailVerified &result) {\n\tstopCheck();\n\t_sentRequest = 0;\n\n\tresult.match([&](const MTPDaccount_emailVerified &data) {\n\t\t_code->setEnabled(true);\n\t\tshowCodeError(rpl::single(\n\t\t\tu\"Unexpected type of response: emailVerifiedLogin\"_q));\n\t}, [&](const MTPDaccount_emailVerifiedLogin &data) {\n\t\tgetData()->emailPatternSetup.clear();\n\t\tdata.vsent_code().match([&](const MTPDauth_sentCode &sentData) {\n\t\t\tfillSentCodeData(sentData);\n\t\t\tgetData()->phoneHash = qba(sentData.vphone_code_hash());\n\t\t\tconst auto next = sentData.vnext_type();\n\t\t\tif (next && next->type() == mtpc_auth_codeTypeCall) {\n\t\t\t\tgetData()->callStatus = CallStatus::Waiting;\n\t\t\t\tgetData()->callTimeout = sentData.vtimeout().value_or(60);\n\t\t\t} else {\n\t\t\t\tgetData()->callStatus = CallStatus::Disabled;\n\t\t\t\tgetData()->callTimeout = 0;\n\t\t\t}\n\t\t\tgoReplace<CodeWidget>(Animate::Forward);\n\t\t}, [&](const MTPDauth_sentCodeSuccess &sentData) {\n\t\t\tfinish(sentData.vauthorization());\n\t\t}, [](const MTPDauth_sentCodePaymentRequired &) {\n\t\t\tLOG((\"API Error: Unexpected auth.sentCodePaymentRequired \"\n\t\t\t\t\"(CodeWidget::emailVerifyDone).\"));\n\t\t});\n\t});\n}\n\nvoid CodeWidget::codeSubmitFail(const MTP::Error &error) {\n\tif (MTP::IsFloodError(error)) {\n\t\tstopCheck();\n\t\t_code->setEnabled(true);\n\t\t_code->setFocus();\n\t\t_sentRequest = 0;\n\t\tshowCodeError(tr::lng_flood_error());\n\t\treturn;\n\t}\n\n\tstopCheck();\n\t_code->setEnabled(true);\n\t_code->setFocus();\n\t_sentRequest = 0;\n\tauto &err = error.type();\n\tif (err == u\"PHONE_NUMBER_INVALID\"_q\n\t\t|| err == u\"PHONE_CODE_EXPIRED\"_q\n\t\t|| err == u\"PHONE_NUMBER_BANNED\"_q) { // show error\n\t\tgoBack();\n\t} else if (err == u\"PHONE_CODE_EMPTY\"_q || err == u\"PHONE_CODE_INVALID\"_q) {\n\t\tshowCodeError(tr::lng_bad_code());\n\t} else if (err == u\"SESSION_PASSWORD_NEEDED\"_q) {\n\t\t_checkRequestTimer.callEach(1000);\n\t\t_sentRequest = api().request(MTPaccount_GetPassword(\n\t\t)).done([=](const MTPaccount_Password &result) {\n\t\t\tgotPassword(result);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tcodeSubmitFail(error);\n\t\t}).handleFloodErrors().send();\n\t} else if (Logs::DebugEnabled()) { // internal server error\n\t\tshowCodeError(rpl::single(err + \": \" + error.description()));\n\t} else {\n\t\tshowCodeError(rpl::single(Lang::Hard::ServerError()));\n\t}\n}\n\nvoid CodeWidget::sendCall() {\n\tif (_callStatus == CallStatus::Waiting) {\n\t\tif (--_callTimeout <= 0) {\n\t\t\t_callStatus = CallStatus::Calling;\n\t\t\t_callTimer.cancel();\n\t\t\t_callRequestId = api().request(MTPauth_ResendCode(\n\t\t\t\tMTP_flags(0),\n\t\t\t\tMTP_string(getData()->phone),\n\t\t\t\tMTP_bytes(getData()->phoneHash),\n\t\t\t\tMTPstring() // reason\n\t\t\t)).done([=](const MTPauth_SentCode &result) {\n\t\t\t\tcallDone(result);\n\t\t\t}).send();\n\t\t} else {\n\t\t\tgetData()->callStatus = _callStatus;\n\t\t\tgetData()->callTimeout = _callTimeout;\n\t\t}\n\t\tupdateCallText();\n\t}\n}\n\nvoid CodeWidget::callDone(const MTPauth_SentCode &result) {\n\tresult.match([&](const MTPDauth_sentCode &data) {\n\t\tfillSentCodeData(data);\n\t\t_code->setDigitsCountMax(getData()->codeLength);\n\t\tif (_callStatus == CallStatus::Calling) {\n\t\t\t_callStatus = CallStatus::Called;\n\t\t\tgetData()->callStatus = _callStatus;\n\t\t\tgetData()->callTimeout = _callTimeout;\n\t\t\tupdateCallText();\n\t\t}\n\t}, [&](const MTPDauth_sentCodeSuccess &data) {\n\t\tfinish(data.vauthorization());\n\t}, [](const MTPDauth_sentCodePaymentRequired &) {\n\t\tLOG((\"API Error: Unexpected auth.sentCodePaymentRequired \"\n\t\t\t\"(CodeWidget::callDone).\"));\n\t});\n}\n\nvoid CodeWidget::gotPassword(const MTPaccount_Password &result) {\n\tExpects(result.type() == mtpc_account_password);\n\n\tstopCheck();\n\t_sentRequest = 0;\n\tconst auto &d = result.c_account_password();\n\tgetData()->pwdState = Core::ParseCloudPasswordState(d);\n\tif (!d.vcurrent_algo() || !d.vsrp_id() || !d.vsrp_B()) {\n\t\tLOG((\"API Error: No current password received on login.\"));\n\t\t_code->setFocus();\n\t\treturn;\n\t} else if (!getData()->pwdState.hasPassword) {\n\t\tconst auto callback = [=](Fn<void()> &&close) {\n\t\t\tCore::UpdateApplication();\n\t\t\tclose();\n\t\t};\n\t\tUi::show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_passport_app_out_of_date(),\n\t\t\t.confirmed = callback,\n\t\t\t.confirmText = tr::lng_menu_update(),\n\t\t}));\n\t\treturn;\n\t}\n\tgoReplace<PasswordCheckWidget>(Animate::Forward);\n}\n\nvoid CodeWidget::submit() {\n\tif (getData()->codeByFragmentUrl.isEmpty()) {\n\t\t_code->requestCode();\n\t} else {\n\t\tFile::OpenUrl(getData()->codeByFragmentUrl);\n\t}\n}\n\nvoid CodeWidget::submitCode(const QString &text) {\n\tif (_sentRequest\n\t\t|| _sentCode == text\n\t\t|| text.size() != getData()->codeLength) {\n\t\treturn;\n\t}\n\n\thideError();\n\n\t_checkRequestTimer.callEach(1000);\n\n\t_sentCode = text;\n\t_code->setEnabled(false);\n\tgetData()->pwdState = Core::CloudPasswordState();\n\n\tif (isEmailVerification()) {\n\t\t_sentRequest = api().request(MTPaccount_VerifyEmail(\n\t\t\tMTP_emailVerifyPurposeLoginSetup(\n\t\t\t\tMTP_string(getData()->phone),\n\t\t\t\tMTP_bytes(getData()->phoneHash)),\n\t\t\tMTP_emailVerificationCode(MTP_string(_sentCode))\n\t\t)).done([=](const MTPaccount_EmailVerified &result) {\n\t\t\temailVerifyDone(result);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tcodeSubmitFail(error);\n\t\t}).handleFloodErrors().send();\n\t} else {\n\t\tconst auto isEmailLogin = !getData()->emailPatternLogin.isEmpty();\n\t\t_sentRequest = api().request(MTPauth_SignIn(\n\t\t\tMTP_flags(isEmailLogin\n\t\t\t\t? MTPauth_SignIn::Flag::f_email_verification\n\t\t\t\t: MTPauth_SignIn::Flag::f_phone_code),\n\t\t\tMTP_string(getData()->phone),\n\t\t\tMTP_bytes(getData()->phoneHash),\n\t\t\tMTP_string(_sentCode),\n\t\t\tMTP_emailVerificationCode(\n\t\t\t\tMTP_string(isEmailLogin ? _sentCode : QString()))\n\t\t)).done([=](const MTPauth_Authorization &result) {\n\t\t\tcodeSubmitDone(result);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tcodeSubmitFail(error);\n\t\t}).handleFloodErrors().send();\n\t}\n}\n\nbool CodeWidget::isEmailVerification() const {\n\treturn !getData()->emailPatternSetup.isEmpty();\n}\n\nrpl::producer<QString> CodeWidget::nextButtonText() const {\n\treturn _isFragment.value(\n\t) | rpl::map([=](bool isFragment) {\n\t\treturn isFragment\n\t\t\t? tr::lng_intro_fragment_button()\n\t\t\t: Step::nextButtonText();\n\t}) | rpl::flatten_latest();\n}\n\nrpl::producer<const style::RoundButton*> CodeWidget::nextButtonStyle() const {\n\treturn _isFragment.value(\n\t) | rpl::map([](bool isFragment) {\n\t\treturn isFragment ? &st::introFragmentButton : nullptr;\n\t});\n}\n\nvoid CodeWidget::noTelegramCode() {\n\tif (_noTelegramCodeRequestId) {\n\t\treturn;\n\t}\n\t_noTelegramCodeRequestId = api().request(MTPauth_ResendCode(\n\t\tMTP_flags(0),\n\t\tMTP_string(getData()->phone),\n\t\tMTP_bytes(getData()->phoneHash),\n\t\tMTPstring() // reason\n\t)).done([=](const MTPauth_SentCode &result) {\n\t\tnoTelegramCodeDone(result);\n\t}).fail([=](const MTP::Error &error) {\n\t\tnoTelegramCodeFail(error);\n\t}).handleFloodErrors().send();\n}\n\nvoid CodeWidget::noTelegramCodeDone(const MTPauth_SentCode &result) {\n\t_noTelegramCodeRequestId = 0;\n\n\tresult.match([&](const MTPDauth_sentCode &data) {\n\t\tconst auto &d = result.c_auth_sentCode();\n\t\tfillSentCodeData(data);\n\t\t_code->setDigitsCountMax(getData()->codeLength);\n\t\tconst auto next = data.vnext_type();\n\t\tif (next && next->type() == mtpc_auth_codeTypeCall) {\n\t\t\tgetData()->callStatus = CallStatus::Waiting;\n\t\t\tgetData()->callTimeout = d.vtimeout().value_or(60);\n\t\t} else {\n\t\t\tgetData()->callStatus = CallStatus::Disabled;\n\t\t\tgetData()->callTimeout = 0;\n\t\t}\n\t\tgetData()->codeByTelegram = false;\n\t\tupdateDescText();\n\t}, [&](const MTPDauth_sentCodeSuccess &data) {\n\t\tfinish(data.vauthorization());\n\t}, [](const MTPDauth_sentCodePaymentRequired &) {\n\t\tLOG((\"API Error: Unexpected auth.sentCodePaymentRequired \"\n\t\t\t\"(CodeWidget::noTelegramCodeDone).\"));\n\t});\n}\n\nvoid CodeWidget::noTelegramCodeFail(const MTP::Error &error) {\n\tif (MTP::IsFloodError(error)) {\n\t\t_noTelegramCodeRequestId = 0;\n\t\tshowCodeError(tr::lng_flood_error());\n\t\treturn;\n\t} else if (error.type() == u\"SEND_CODE_UNAVAILABLE\"_q) {\n\t\t_noTelegramCodeRequestId = 0;\n\t\treturn;\n\t}\n\n\t_noTelegramCodeRequestId = 0;\n\tif (Logs::DebugEnabled()) { // internal server error\n\t\tshowCodeError(rpl::single(error.type() + \": \" + error.description()));\n\t} else {\n\t\tshowCodeError(rpl::single(Lang::Hard::ServerError()));\n\t}\n}\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_code.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"intro/intro_step.h\"\n#include \"intro/intro_widget.h\"\n#include \"ui/widgets/fields/masked_input_field.h\"\n#include \"base/timer.h\"\n\nnamespace Ui {\nclass RoundButton;\nclass LinkButton;\nclass FlatLabel;\nclass CodeInput;\n} // namespace Ui\n\nnamespace Intro {\nnamespace details {\n\nenum class CallStatus;\n\nclass CodeWidget final : public Step {\npublic:\n\tCodeWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Account*> account,\n\t\tnot_null<Data*> data);\n\n\tbool hasBack() const override {\n\t\treturn true;\n\t}\n\tvoid setInnerFocus() override;\n\tvoid activate() override;\n\tvoid finished() override;\n\tvoid cancelled() override;\n\tvoid submit() override;\n\trpl::producer<QString> nextButtonText() const override;\n\trpl::producer<const style::RoundButton*> nextButtonStyle() const override;\n\n\tvoid updateDescText();\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid noTelegramCode();\n\tvoid sendCall();\n\tvoid checkRequest();\n\n\tint errorTop() const override;\n\n\t[[nodiscard]] bool isEmailVerification() const;\n\n\tvoid updateCallText();\n\tvoid refreshLang();\n\tvoid updateControlsGeometry();\n\n\tvoid codeSubmitDone(const MTPauth_Authorization &result);\n\tvoid codeSubmitFail(const MTP::Error &error);\n\tvoid emailVerifyDone(const MTPaccount_EmailVerified &result);\n\n\tvoid showCodeError(rpl::producer<QString> text);\n\tvoid callDone(const MTPauth_SentCode &result);\n\tvoid gotPassword(const MTPaccount_Password &result);\n\n\tvoid noTelegramCodeDone(const MTPauth_SentCode &result);\n\tvoid noTelegramCodeFail(const MTP::Error &result);\n\n\tvoid submitCode(const QString &text);\n\n\tvoid stopCheck();\n\n\tobject_ptr<Ui::LinkButton> _noTelegramCode;\n\tmtpRequestId _noTelegramCodeRequestId = 0;\n\n\tobject_ptr<Ui::CodeInput> _code;\n\tQString _sentCode;\n\tmtpRequestId _sentRequest = 0;\n\n\trpl::variable<bool> _isFragment = false;\n\n\tbase::Timer _callTimer;\n\tCallStatus _callStatus = CallStatus();\n\tint _callTimeout;\n\tmtpRequestId _callRequestId = 0;\n\tobject_ptr<Ui::FlatLabel> _callLabel;\n\n\tbase::Timer _checkRequestTimer;\n\n};\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_code_input.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"intro/intro_code_input.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/abstract_button.h\"\n#include \"ui/effects/shake_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"styles/style_basic.h\"\n#include \"styles/style_intro.h\"\n#include \"styles/style_layers.h\" // boxRadius\n\n#include <QtCore/QRegularExpression>\n#include <QtGui/QClipboard>\n#include <QtGui/QGuiApplication>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kDigitNone = int(-1);\n\n[[nodiscard]] int Circular(int left, int right) {\n\treturn ((left % right) + right) % right;\n}\n\nclass Shaker final {\npublic:\n\texplicit Shaker(not_null<Ui::RpWidget*> widget);\n\n\tvoid shake();\n\nprivate:\n\tconst not_null<Ui::RpWidget*> _widget;\n\tUi::Animations::Simple _animation;\n\n};\n\nShaker::Shaker(not_null<Ui::RpWidget*> widget)\n: _widget(widget) {\n}\n\nvoid Shaker::shake() {\n\tif (_animation.animating()) {\n\t\treturn;\n\t}\n\t_animation.start(DefaultShakeCallback([=, x = _widget->x()](int shift) {\n\t\t_widget->moveToLeft(x + shift, _widget->y());\n\t}), 0., 1., st::shakeDuration);\n}\n\n} // namespace\n\nclass CodeDigit final : public Ui::AbstractButton {\npublic:\n\texplicit CodeDigit(not_null<Ui::RpWidget*> widget);\n\n\tvoid setDigit(int digit);\n\t[[nodiscard]] int digit() const;\n\n\tvoid setBorderColor(const QBrush &brush);\n\tvoid shake();\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\nprivate:\n\tShaker _shaker;\n\tUi::Animations::Simple _animation;\n\tint _dataDigit = kDigitNone;\n\tint _viewDigit = kDigitNone;\n\n\tQPen _borderPen;\n\n};\n\nCodeDigit::CodeDigit(not_null<Ui::RpWidget*> widget)\n: Ui::AbstractButton(widget)\n, _shaker(this) {\n\tsetBorderColor(st::windowBgRipple);\n}\n\nvoid CodeDigit::setDigit(int digit) {\n\tif ((_dataDigit == digit) && _animation.animating()) {\n\t\treturn;\n\t}\n\t_dataDigit = digit;\n\tif (_viewDigit != digit) {\n\t\t_animation.stop();\n\t\tif (digit == kDigitNone) {\n\t\t\t_animation.start([=](float64 value) {\n\t\t\t\tupdate();\n\t\t\t\tif (!value) {\n\t\t\t\t\t_viewDigit = digit;\n\t\t\t\t}\n\t\t\t}, 1., 0., st::universalDuration);\n\t\t} else {\n\t\t\t_viewDigit = digit;\n\t\t\t_animation.start([=] { update(); }, 0, 1., st::universalDuration);\n\t\t}\n\t}\n}\n\nint CodeDigit::digit() const {\n\treturn _dataDigit;\n}\n\nvoid CodeDigit::setBorderColor(const QBrush &brush) {\n\t_borderPen = QPen(brush, st::introCodeDigitBorderWidth);\n\tupdate();\n}\n\nvoid CodeDigit::shake() {\n\t_shaker.shake();\n}\n\nvoid CodeDigit::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tauto clipPath = QPainterPath();\n\tclipPath.addRoundedRect(rect(), st::boxRadius, st::boxRadius);\n\tp.setClipPath(clipPath);\n\n\tp.fillRect(rect(), st::windowBgOver);\n\t{\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.strokePath(clipPath, _borderPen);\n\t}\n\n\tif (_viewDigit == kDigitNone) {\n\t\treturn;\n\t}\n\tconst auto hiding = (_dataDigit == kDigitNone);\n\tconst auto progress = _animation.value(1.);\n\n\tif (hiding) {\n\t\tp.setOpacity(progress * progress);\n\t\tconst auto center = rect().center();\n\t\tp.setTransform(QTransform()\n\t\t\t.translate(center.x(), center.y())\n\t\t\t.scale(progress, progress)\n\t\t\t.translate(-center.x(), -center.y()));\n\t} else {\n\t\tp.setOpacity(progress);\n\t\tconstexpr auto kSlideDistanceRatio = 0.2;\n\t\tconst auto distance = rect().height() * kSlideDistanceRatio;\n\t\tp.translate(0, (distance * (1. - progress)));\n\t}\n\tp.setFont(st::introCodeDigitFont);\n\tp.setPen(st::windowFg);\n\tp.drawText(rect(), QString::number(_viewDigit), style::al_center);\n}\n\nvoid CodeDigit::mouseReleaseEvent(QMouseEvent *e) {\n\tQGuiApplication::inputMethod()->show();\n\tUi::AbstractButton::mouseReleaseEvent(e);\n}\n\nCodeInput::CodeInput(QWidget *parent)\n: Ui::RpWidget(parent) {\n\tsetFocusPolicy(Qt::StrongFocus);\n\tsetAttribute(Qt::WA_InputMethodEnabled);\n}\n\nQString CodeInput::accessibilityName() {\n\treturn tr::lng_code_ph(tr::now);\n}\n\nQString CodeInput::accessibilityValue() const {\n\treturn collectDigits();\n}\n\nvoid CodeInput::setDigitsCountMax(int digitsCount) {\n\t_digitsCountMax = digitsCount;\n\n\t_digits.clear();\n\t_currentIndex = 0;\n\n\tconstexpr auto kWidthRatio = 0.8;\n\tconst auto digitWidth = st::introCodeDigitHeight * kWidthRatio;\n\tconst auto padding = Margins(st::introCodeDigitSkip);\n\tresize(\n\t\tpadding.left()\n\t\t\t+ digitWidth * digitsCount\n\t\t\t+ st::introCodeDigitSkip * (digitsCount - 1)\n\t\t\t+ padding.right(),\n\t\tst::introCodeDigitHeight);\n\tsetNaturalWidth(width());\n\n\tfor (auto i = 0; i < digitsCount; i++) {\n\t\tconst auto widget = Ui::CreateChild<CodeDigit>(this);\n\t\twidget->setPointerCursor(false);\n\t\twidget->setClickedCallback([=] { unfocusAll(_currentIndex = i); });\n\t\twidget->resize(digitWidth, st::introCodeDigitHeight);\n\t\twidget->moveToLeft(\n\t\t\tpadding.left() + (digitWidth + st::introCodeDigitSkip) * i,\n\t\t\t0);\n\t\t_digits.emplace_back(widget);\n\t}\n}\n\nvoid CodeInput::setCode(QString code) {\n\tusing namespace TextUtilities;\n\tcode = code.remove(RegExpDigitsExclude()).mid(0, _digitsCountMax);\n\tfor (int i = 0; i < _digits.size(); i++) {\n\t\tif (i >= code.size()) {\n\t\t\treturn;\n\t\t}\n\t\t_digits[i]->setDigit(code.at(i).digitValue());\n\t}\n}\n\nvoid CodeInput::requestCode() {\n\tconst auto result = collectDigits();\n\tif (result.size() == _digitsCountMax) {\n\t\t_codeCollected.fire_copy(result);\n\t} else {\n\t\tfindEmptyAndPerform([&](int i) { _digits[i]->shake(); });\n\t}\n}\n\nrpl::producer<QString> CodeInput::codeCollected() const {\n\treturn _codeCollected.events();\n}\n\nvoid CodeInput::clear() {\n\tfor (const auto &digit : _digits) {\n\t\tdigit->setDigit(kDigitNone);\n\t}\n\tunfocusAll(_currentIndex = 0);\n}\n\nvoid CodeInput::showError() {\n\tclear();\n\tfor (const auto &digit : _digits) {\n\t\tdigit->shake();\n\t\tdigit->setBorderColor(st::activeLineFgError);\n\t}\n}\n\nvoid CodeInput::focusInEvent(QFocusEvent *e) {\n\tunfocusAll(_currentIndex);\n}\n\nvoid CodeInput::focusOutEvent(QFocusEvent *e) {\n\tunfocusAll(kDigitNone);\n}\n\nvoid CodeInput::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tp.fillRect(rect(), st::windowBg);\n}\n\nvoid CodeInput::keyPressEvent(QKeyEvent *e) {\n\tconst auto key = e->key();\n\tif (key == Qt::Key_Down || key == Qt::Key_Right || key == Qt::Key_Space) {\n\t\t_currentIndex = Circular(_currentIndex + 1, _digits.size());\n\t\tunfocusAll(_currentIndex);\n\t} else if (key == Qt::Key_Up || key == Qt::Key_Left) {\n\t\t_currentIndex = Circular(_currentIndex - 1, _digits.size());\n\t\tunfocusAll(_currentIndex);\n\t} else if (key >= Qt::Key_0 && key <= Qt::Key_9) {\n\t\tprocessDigit(int(key - Qt::Key_0));\n\t} else if (key == Qt::Key_Delete) {\n\t\t_digits[_currentIndex]->setDigit(kDigitNone);\n\t} else if (key == Qt::Key_Backspace) {\n\t\tconst auto wasDigit = _digits[_currentIndex]->digit();\n\t\t_digits[_currentIndex]->setDigit(kDigitNone);\n\t\t_currentIndex = std::clamp(_currentIndex - 1, 0, int(_digits.size()));\n\t\tif (wasDigit == kDigitNone) {\n\t\t\t_digits[_currentIndex]->setDigit(kDigitNone);\n\t\t}\n\t\tunfocusAll(_currentIndex);\n\t} else if (key == Qt::Key_Enter || key == Qt::Key_Return) {\n\t\trequestCode();\n\t} else if (e == QKeySequence::Paste) {\n\t\tinsertCodeAndSubmit(QGuiApplication::clipboard()->text());\n\t} else if (key >= Qt::Key_A && key <= Qt::Key_Z) {\n\t\t_digits[_currentIndex]->shake();\n\t} else if (key == Qt::Key_Home || key == Qt::Key_PageUp) {\n\t\tunfocusAll(_currentIndex = 0);\n\t} else if (key == Qt::Key_End || key == Qt::Key_PageDown) {\n\t\tunfocusAll(_currentIndex = (_digits.size() - 1));\n\t}\n}\n\nvoid CodeInput::keyReleaseEvent(QKeyEvent *e) {\n\tQGuiApplication::inputMethod()->show();\n}\n\nvoid CodeInput::contextMenuEvent(QContextMenuEvent *e) {\n\tif (_menu) {\n\t\treturn;\n\t}\n\t_menu = base::make_unique_q<Ui::PopupMenu>(this, st::defaultPopupMenu);\n\t_menu->addAction(tr::lng_mac_menu_paste(tr::now), [=] {\n\t\tinsertCodeAndSubmit(QGuiApplication::clipboard()->text());\n\t})->setEnabled(!QGuiApplication::clipboard()->text().isEmpty());\n\t_menu->popup(QCursor::pos());\n}\n\nvoid CodeInput::insertCodeAndSubmit(const QString &code) {\n\tif (code.isEmpty()) {\n\t\treturn;\n\t}\n\tsetCode(code);\n\t_currentIndex = _digits.size() - 1;\n\tfindEmptyAndPerform([&](int i) { _currentIndex = i; });\n\tunfocusAll(_currentIndex);\n\tif ((_currentIndex == _digits.size() - 1)\n\t\t&& _digits[_currentIndex]->digit() != kDigitNone) {\n\t\trequestCode();\n\t}\n\taccessibilityValueChanged();\n}\n\nQString CodeInput::collectDigits() const {\n\tauto result = QString();\n\tfor (const auto &digit : _digits) {\n\t\tif (digit->digit() != kDigitNone) {\n\t\t\tresult += QString::number(digit->digit());\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid CodeInput::unfocusAll(int except) {\n\tfor (auto i = 0; i < _digits.size(); i++) {\n\t\tconst auto focused = (i == except);\n\t\t_digits[i]->setBorderColor(focused\n\t\t\t? st::windowActiveTextFg\n\t\t\t: st::windowBgRipple);\n\t}\n}\n\nvoid CodeInput::findEmptyAndPerform(const Fn<void(int)> &callback) {\n\tfor (auto i = 0; i < _digits.size(); i++) {\n\t\tif (_digits[i]->digit() == kDigitNone) {\n\t\t\tcallback(i);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nQVariant CodeInput::inputMethodQuery(Qt::InputMethodQuery query) const {\n\tswitch (query) {\n\tcase Qt::ImEnabled:\n\t\treturn true;\n\tcase Qt::ImHints:\n\t\treturn int(Qt::ImhDigitsOnly | Qt::ImhNoPredictiveText);\n\tdefault:\n\t\treturn RpWidget::inputMethodQuery(query);\n\t}\n}\n\nvoid CodeInput::inputMethodEvent(QInputMethodEvent *e) {\n\tconst auto text = e->commitString();\n\tfor (const auto &ch : text) {\n\t\tif (ch.isDigit()) {\n\t\t\tprocessDigit(ch.digitValue());\n\t\t}\n\t}\n\te->accept();\n}\n\nvoid CodeInput::processDigit(int digit) {\n\t_digits[_currentIndex]->setDigit(digit);\n\t_currentIndex = Circular(_currentIndex + 1, _digits.size());\n\tif (!_currentIndex) {\n\t\tconst auto result = collectDigits();\n\t\tif (result.size() == _digitsCountMax) {\n\t\t\t_codeCollected.fire_copy(result);\n\t\t\t_currentIndex = _digits.size() - 1;\n\t\t} else {\n\t\t\tfindEmptyAndPerform([&](int i) { _currentIndex = i; });\n\t\t}\n\t}\n\tunfocusAll(_currentIndex);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_code_input.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\n\nclass CodeDigit;\nclass PopupMenu;\n\nclass CodeInput final : public Ui::RpWidget {\npublic:\n\tCodeInput(QWidget *parent);\n\n\tQAccessible::Role accessibilityRole() override {\n\t\treturn QAccessible::EditableText;\n\t}\n\tQString accessibilityName() override;\n\tQString accessibilityValue() const override;\n\n\tvoid setDigitsCountMax(int digitsCount);\n\n\tvoid setCode(QString code);\n\n\tvoid requestCode();\n\t[[nodiscard]] rpl::producer<QString> codeCollected() const;\n\n\tvoid clear();\n\tvoid showError();\n\nprotected:\n\tvoid focusInEvent(QFocusEvent *e) override;\n\tvoid focusOutEvent(QFocusEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid keyReleaseEvent(QKeyEvent *e) override;\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\tQVariant inputMethodQuery(Qt::InputMethodQuery query) const override;\n\tvoid inputMethodEvent(QInputMethodEvent *e) override;\n\nprivate:\n\t[[nodiscard]] QString collectDigits() const;\n\n\tvoid insertCodeAndSubmit(const QString &code);\n\tvoid unfocusAll(int except);\n\tvoid findEmptyAndPerform(const Fn<void(int)> &callback);\n\tvoid processDigit(int digit);\n\n\tint _digitsCountMax = 0;\n\tstd::vector<not_null<CodeDigit*>> _digits;\n\tint _currentIndex = 0;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\trpl::event_stream<QString> _codeCollected;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_email.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"intro/intro_email.h\"\n\n#include \"intro/intro_code.h\"\n#include \"intro/intro_code_input.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"settings/cloud_password/settings_cloud_password_common.h\"\n#include \"settings/settings_common.h\" // CreateLottieIcon.\n#include \"ui/rect.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_intro.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Intro {\nnamespace details {\n\nEmailWidget::EmailWidget(\n\tQWidget *parent,\n\tnot_null<Main::Account*> account,\n\tnot_null<Data*> data)\n: Step(parent, account, data)\n, _inner(this) {\n\tconst auto content = _inner.get();\n\twidthValue() | rpl::on_next([=](int w) {\n\t\tcontent->resizeToWidth(st::introNextButton.width);\n\t\tcontent->moveToLeft((w - content->width()) / 2, contentTop());\n\t}, content->lifetime());\n\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_intro_email_setup_title(),\n\t\t\tst::introTitle),\n\t\tstyle::margins(),\n\t\tstyle::al_left);\n\tUi::AddSkip(content, st::lineWidth * 2);\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_settings_cloud_login_email_about(),\n\t\t\tst::introDescription),\n\t\tstyle::margins(),\n\t\tstyle::al_left);\n\n\t{\n\t\tconst auto lottie = u\"cloud_password/email\"_q;\n\t\tconst auto size = st::settingsCloudPasswordIconSize / 3 * 2;\n\t\tauto icon = Settings::CreateLottieIcon(\n\t\t\tcontent,\n\t\t\t{ .name = lottie, .sizeOverride = Size(size) },\n\t\t\tstyle::margins());\n\t\tcontent->add(std::move(icon.widget));\n\t\t_showFinished.events(\n\t\t) | rpl::on_next([animate = std::move(icon.animate)] {\n\t\t\tanimate(anim::repeat::once);\n\t\t}, lifetime());\n\t}\n\n\tconst auto newInput = Settings::CloudPassword::AddWrappedField(\n\t\tcontent,\n\t\ttr::lng_settings_cloud_login_email_placeholder(),\n\t\tQString());\n\tUi::AddSkip(content);\n\tconst auto error = Settings::CloudPassword::AddError(content, nullptr);\n\tnewInput->changes() | rpl::on_next([=] {\n\t\terror->hide();\n\t}, newInput->lifetime());\n\tnewInput->submits() | rpl::on_next([=] {\n\t\tsubmit();\n\t}, newInput->lifetime());\n\tnewInput->setText(getData()->email);\n\tif (newInput->hasText()) {\n\t\tnewInput->selectAll();\n\t}\n\t_setFocus.events() | rpl::on_next([=] {\n\t\tnewInput->setFocus();\n\t}, newInput->lifetime());\n\n\t_submitCallback = [=] {\n\t\tconst auto send = [=](const QString &email) {\n\t\t\tgetData()->email = email;\n\n\t\t\tconst auto done = [=](int length, const QString &pattern) {\n\t\t\t\t_sentRequest = 0;\n\t\t\t\tgetData()->codeLength = length;\n\t\t\t\tgetData()->emailPatternSetup = pattern;\n\t\t\t\tgoNext<CodeWidget>();\n\t\t\t};\n\t\t\tconst auto fail = [=](const QString &type) {\n\t\t\t\t_sentRequest = 0;\n\n\t\t\t\tnewInput->setFocus();\n\t\t\t\tnewInput->showError();\n\t\t\t\tnewInput->selectAll();\n\t\t\t\terror->show();\n\n\t\t\t\tif (MTP::IsFloodError(type)) {\n\t\t\t\t\terror->setText(tr::lng_flood_error(tr::now));\n\t\t\t\t} else if (type == u\"EMAIL_NOT_ALLOWED\"_q) {\n\t\t\t\t\terror->setText(\n\t\t\t\t\t\ttr::lng_settings_error_email_not_alowed(tr::now));\n\t\t\t\t} else if (type == u\"EMAIL_INVALID\"_q) {\n\t\t\t\t\terror->setText(tr::lng_cloud_password_bad_email(tr::now));\n\t\t\t\t} else if (type == u\"EMAIL_HASH_EXPIRED\"_q) {\n\t\t\t\t\t// Show box?\n\t\t\t\t\terror->setText(Lang::Hard::EmailConfirmationExpired());\n\t\t\t\t} else {\n\t\t\t\t\terror->setText(Lang::Hard::ServerError());\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t_sentRequest = api().request(MTPaccount_SendVerifyEmailCode(\n\t\t\t\tMTP_emailVerifyPurposeLoginSetup(\n\t\t\t\t\tMTP_string(getData()->phone),\n\t\t\t\t\tMTP_bytes(getData()->phoneHash)),\n\t\t\t\tMTP_string(email)\n\t\t\t)).done([=](const MTPaccount_SentEmailCode &result) {\n\t\t\t\tdone(\n\t\t\t\t\tresult.data().vlength().v,\n\t\t\t\t\tqs(result.data().vemail_pattern()));\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tfail(error.type());\n\t\t\t}).send();\n\t\t};\n\t\tconst auto newText = newInput->getLastText();\n\t\tif (newText.isEmpty()) {\n\t\t\tnewInput->setFocus();\n\t\t\tnewInput->showError();\n\t\t} else {\n\t\t\tsend(newText);\n\t\t}\n\t};\n}\n\nvoid EmailWidget::submit() {\n\tif (_submitCallback) {\n\t\t_submitCallback();\n\t}\n}\n\nvoid EmailWidget::setInnerFocus() {\n\t_setFocus.fire({});\n}\n\nvoid EmailWidget::activate() {\n\tStep::activate();\n\tshowChildren();\n\tsetInnerFocus();\n\t_showFinished.fire({});\n}\n\nvoid EmailWidget::finished() {\n\tStep::finished();\n\tcancelled();\n}\n\nvoid EmailWidget::cancelled() {\n\tapi().request(base::take(_sentRequest)).cancel();\n}\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_email.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"intro/intro_step.h\"\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace MTP {\nclass Sender;\n} // namespace MTP\n\nnamespace Intro {\nnamespace details {\n\nclass EmailWidget final : public Step {\npublic:\n\tEmailWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Account*> account,\n\t\tnot_null<Data*> data);\n\n\tvoid setInnerFocus() override;\n\tvoid activate() override;\n\tvoid finished() override;\n\tvoid cancelled() override;\n\tvoid submit() override;\n\n\tbool hasBack() const override {\n\t\treturn true;\n\t}\n\nprivate:\n\tobject_ptr<Ui::VerticalLayout> _inner;\n\tFn<void()> _submitCallback = nullptr;\n\trpl::event_stream<> _showFinished;\n\trpl::event_stream<> _setFocus;\n\tmtpRequestId _sentRequest = 0;\n\n};\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_password_check.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"intro/intro_password_check.h\"\n\n#include \"intro/intro_widget.h\"\n#include \"core/core_cloud_password.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/abstract_box.h\"\n#include \"boxes/passcode_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"intro/intro_signup.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/fields/password_input.h\"\n#include \"main/main_account.h\"\n#include \"base/random.h\"\n#include \"styles/style_intro.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace Intro {\nnamespace details {\n\nPasswordCheckWidget::PasswordCheckWidget(\n\tQWidget *parent,\n\tnot_null<Main::Account*> account,\n\tnot_null<Data*> data)\n: Step(parent, account, data)\n, _passwordState(getData()->pwdState)\n, _pwdField(this, st::introPassword, tr::lng_signin_password())\n, _pwdHint(this, st::introPasswordHint)\n, _codeField(this, st::introPassword, tr::lng_signin_code())\n, _toRecover(this, tr::lng_signin_recover(tr::now))\n, _toPassword(this, tr::lng_signin_try_password(tr::now)) {\n\tExpects(_passwordState.hasPassword);\n\n\tLang::Updated(\n\t) | rpl::on_next([=] {\n\t\trefreshLang();\n\t}, lifetime());\n\n\t_toRecover->addClickHandler([=] { toRecover(); });\n\t_toPassword->addClickHandler([=] { toPassword(); });\n\tconnect(_pwdField, &Ui::PasswordInput::changed, [=] { hideError(); });\n\t_codeField->changes(\n\t) | rpl::on_next([=] {\n\t\thideError();\n\t}, _codeField->lifetime());\n\n\tsetTitleText(tr::lng_signin_title());\n\tupdateDescriptionText();\n\n\tif (_passwordState.hint.isEmpty()) {\n\t\t_pwdHint->hide();\n\t} else {\n\t\t_pwdHint->setText(tr::lng_signin_hint(\n\t\t\ttr::now,\n\t\t\tlt_password_hint,\n\t\t\t_passwordState.hint));\n\t}\n\t_codeField->hide();\n\t_toPassword->hide();\n\n\tsetMouseTracking(true);\n}\n\nvoid PasswordCheckWidget::refreshLang() {\n\tif (_toRecover) {\n\t\t_toRecover->setText(tr::lng_signin_recover(tr::now));\n\t}\n\tif (_toPassword) {\n\t\t_toPassword->setText(\n\t\t\ttr::lng_signin_try_password(tr::now));\n\t}\n\tif (!_passwordState.hint.isEmpty()) {\n\t\t_pwdHint->setText(tr::lng_signin_hint(\n\t\t\ttr::now,\n\t\t\tlt_password_hint,\n\t\t\t_passwordState.hint));\n\t}\n\tupdateControlsGeometry();\n}\n\nint PasswordCheckWidget::errorTop() const {\n\treturn contentTop() + st::introErrorBelowLinkTop;\n}\n\nvoid PasswordCheckWidget::resizeEvent(QResizeEvent *e) {\n\tStep::resizeEvent(e);\n\tupdateControlsGeometry();\n}\n\nvoid PasswordCheckWidget::updateControlsGeometry() {\n\t_pwdField->moveToLeft(contentLeft(), contentTop() + st::introPasswordTop);\n\t_pwdHint->moveToLeft(contentLeft() + st::buttonRadius, contentTop() + st::introPasswordHintTop);\n\t_codeField->moveToLeft(contentLeft(), contentTop() + st::introStepFieldTop);\n\tauto linkTop = _codeField->y() + _codeField->height() + st::introLinkTop;\n\t_toRecover->moveToLeft(contentLeft() + st::buttonRadius, linkTop);\n\t_toPassword->moveToLeft(contentLeft() + st::buttonRadius, linkTop);\n}\n\nvoid PasswordCheckWidget::setInnerFocus() {\n\tif (_pwdField->isHidden()) {\n\t\t_codeField->setFocusFast();\n\t} else {\n\t\t_pwdField->setFocusFast();\n\t}\n}\n\nvoid PasswordCheckWidget::activate() {\n\tif (_pwdField->isHidden() && _codeField->isHidden()) {\n\t\tStep::activate();\n\t\t_pwdField->show();\n\t\t_pwdHint->show();\n\t\t_toRecover->show();\n\t}\n\tsetInnerFocus();\n}\n\nvoid PasswordCheckWidget::cancelled() {\n\tapi().request(base::take(_sentRequest)).cancel();\n}\n\nvoid PasswordCheckWidget::pwdSubmitDone(\n\t\tbool recover,\n\t\tconst MTPauth_Authorization &result) {\n\t_sentRequest = 0;\n\tif (recover) {\n\t\tcSetPasswordRecovered(true);\n\t}\n\tfinish(result);\n}\n\nvoid PasswordCheckWidget::pwdSubmitFail(const MTP::Error &error) {\n\tif (MTP::IsFloodError(error)) {\n\t\t_sentRequest = 0;\n\t\tshowError(tr::lng_flood_error());\n\t\t_pwdField->showError();\n\t\treturn;\n\t}\n\n\t_sentRequest = 0;\n\tconst auto &type = error.type();\n\tif (type == u\"PASSWORD_HASH_INVALID\"_q\n\t\t|| type == u\"SRP_PASSWORD_CHANGED\"_q) {\n\t\tshowError(tr::lng_signin_bad_password());\n\t\t_pwdField->selectAll();\n\t\t_pwdField->showError();\n\t} else if (type == u\"PASSWORD_EMPTY\"_q\n\t\t|| type == u\"AUTH_KEY_UNREGISTERED\"_q) {\n\t\tgoBack();\n\t} else if (type == u\"SRP_ID_INVALID\"_q) {\n\t\thandleSrpIdInvalid();\n\t} else {\n\t\tif (Logs::DebugEnabled()) { // internal server error\n\t\t\tshowError(rpl::single(type + \": \" + error.description()));\n\t\t} else {\n\t\t\tshowError(rpl::single(Lang::Hard::ServerError()));\n\t\t}\n\t\t_pwdField->setFocus();\n\t}\n}\n\nvoid PasswordCheckWidget::handleSrpIdInvalid() {\n\tconst auto now = crl::now();\n\tif (_lastSrpIdInvalidTime > 0\n\t\t&& now - _lastSrpIdInvalidTime < Core::kHandleSrpIdInvalidTimeout) {\n\t\t_passwordState.mtp.request.id = 0;\n\t\tshowError(rpl::single(Lang::Hard::ServerError()));\n\t} else {\n\t\t_lastSrpIdInvalidTime = now;\n\t\trequestPasswordData();\n\t}\n}\n\nvoid PasswordCheckWidget::checkPasswordHash() {\n\tif (_passwordState.mtp.request.id) {\n\t\tpasswordChecked();\n\t} else {\n\t\trequestPasswordData();\n\t}\n}\n\nvoid PasswordCheckWidget::requestPasswordData() {\n\tapi().request(base::take(_sentRequest)).cancel();\n\t_sentRequest = api().request(\n\t\tMTPaccount_GetPassword()\n\t).done([=](const MTPaccount_Password &result) {\n\t\t_sentRequest = 0;\n\t\tresult.match([&](const MTPDaccount_password &data) {\n\t\t\tbase::RandomAddSeed(bytes::make_span(data.vsecure_random().v));\n\t\t\t_passwordState = Core::ParseCloudPasswordState(data);\n\t\t\tpasswordChecked();\n\t\t});\n\t}).send();\n}\n\nvoid PasswordCheckWidget::passwordChecked() {\n\tconst auto check = Core::ComputeCloudPasswordCheck(\n\t\t_passwordState.mtp.request,\n\t\t_passwordHash);\n\tif (!check) {\n\t\treturn serverError();\n\t}\n\t_passwordState.mtp.request.id = 0;\n\t_sentRequest = api().request(\n\t\tMTPauth_CheckPassword(check.result)\n\t).done([=](const MTPauth_Authorization &result) {\n\t\tpwdSubmitDone(false, result);\n\t}).fail([=](const MTP::Error &error) {\n\t\tpwdSubmitFail(error);\n\t}).handleFloodErrors().send();\n}\n\nvoid PasswordCheckWidget::serverError() {\n\tshowError(rpl::single(Lang::Hard::ServerError()));\n}\n\nvoid PasswordCheckWidget::codeSubmitDone(\n\t\tconst QString &code,\n\t\tconst MTPBool &result) {\n\tauto fields = PasscodeBox::CloudFields::From(_passwordState);\n\tfields.fromRecoveryCode = code;\n\tfields.hasRecovery = false;\n\tfields.mtp.curRequest = {};\n\tfields.hasPassword = false;\n\tauto box = Box<PasscodeBox>(&api().instance(), nullptr, fields);\n\tconst auto boxShared = std::make_shared<base::weak_qptr<PasscodeBox>>();\n\n\tbox->newAuthorization(\n\t) | rpl::on_next([=](const MTPauth_Authorization &result) {\n\t\tif (boxShared) {\n\t\t\t(*boxShared)->closeBox();\n\t\t}\n\t\tpwdSubmitDone(true, result);\n\t}, lifetime());\n\n\t*boxShared = Ui::show(std::move(box));\n}\n\nvoid PasswordCheckWidget::codeSubmitFail(const MTP::Error &error) {\n\tif (MTP::IsFloodError(error)) {\n\t\tshowError(tr::lng_flood_error());\n\t\t_codeField->showError();\n\t\treturn;\n\t}\n\n\t_sentRequest = 0;\n\tconst auto &type = error.type();\n\tif (type == u\"PASSWORD_EMPTY\"_q\n\t\t|| type == u\"AUTH_KEY_UNREGISTERED\"_q) {\n\t\tgoBack();\n\t} else if (type == u\"PASSWORD_RECOVERY_NA\"_q) {\n\t\trecoverStartFail(error);\n\t} else if (type == u\"PASSWORD_RECOVERY_EXPIRED\"_q) {\n\t\t_emailPattern = QString();\n\t\ttoPassword();\n\t} else if (type == u\"CODE_INVALID\"_q) {\n\t\tshowError(tr::lng_signin_wrong_code());\n\t\t_codeField->selectAll();\n\t\t_codeField->showError();\n\t} else {\n\t\tif (Logs::DebugEnabled()) { // internal server error\n\t\t\tshowError(rpl::single(type + \": \" + error.description()));\n\t\t} else {\n\t\t\tshowError(rpl::single(Lang::Hard::ServerError()));\n\t\t}\n\t\t_codeField->setFocus();\n\t}\n}\n\nvoid PasswordCheckWidget::recoverStarted(const MTPauth_PasswordRecovery &result) {\n\t_emailPattern = qs(result.c_auth_passwordRecovery().vemail_pattern());\n\tupdateDescriptionText();\n}\n\nvoid PasswordCheckWidget::recoverStartFail(const MTP::Error &error) {\n\t_pwdField->show();\n\t_pwdHint->show();\n\t_codeField->hide();\n\t_pwdField->setFocus();\n\tupdateDescriptionText();\n\tupdate();\n\thideError();\n}\n\nvoid PasswordCheckWidget::toRecover() {\n\tif (_passwordState.hasRecovery) {\n\t\tif (_sentRequest) {\n\t\t\tapi().request(base::take(_sentRequest)).cancel();\n\t\t}\n\t\thideError();\n\t\t_toRecover->hide();\n\t\t_toPassword->show();\n\t\t_pwdField->hide();\n\t\t_pwdHint->hide();\n\t\t_pwdField->setText(QString());\n\t\t_codeField->show();\n\t\t_codeField->setFocus();\n\t\tupdateDescriptionText();\n\t\tif (_emailPattern.isEmpty()) {\n\t\t\tapi().request(\n\t\t\t\tMTPauth_RequestPasswordRecovery()\n\t\t\t).done([=](const MTPauth_PasswordRecovery &result) {\n\t\t\t\trecoverStarted(result);\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\trecoverStartFail(error);\n\t\t\t}).send();\n\t\t}\n\t} else {\n\t\tconst auto box = Ui::show(\n\t\t\tUi::MakeInformBox(tr::lng_signin_no_email_forgot()));\n\t\tbox->boxClosing(\n\t\t) | rpl::on_next([=] {\n\t\t\tshowReset();\n\t\t}, box->lifetime());\n\t}\n}\n\nvoid PasswordCheckWidget::toPassword() {\n\tconst auto box = Ui::show(\n\t\tUi::MakeInformBox(tr::lng_signin_cant_email_forgot()));\n\tbox->boxClosing(\n\t) | rpl::on_next([=] {\n\t\tshowReset();\n\t}, box->lifetime());\n}\n\nvoid PasswordCheckWidget::showReset() {\n\tif (_sentRequest) {\n\t\tapi().request(base::take(_sentRequest)).cancel();\n\t}\n\t_toRecover->show();\n\t_toPassword->hide();\n\t_pwdField->show();\n\t_pwdHint->show();\n\t_codeField->hide();\n\t_codeField->setText(QString());\n\t_pwdField->setFocus();\n\tshowResetButton();\n\tupdateDescriptionText();\n\tupdate();\n}\n\nvoid PasswordCheckWidget::updateDescriptionText() {\n\tauto pwdHidden = _pwdField->isHidden();\n\tauto emailPattern = _emailPattern;\n\tsetDescriptionText(pwdHidden\n\t\t? tr::lng_signin_recover_desc(\n\t\t\tlt_email,\n\t\t\trpl::single(Ui::Text::WrapEmailPattern(emailPattern)),\n\t\t\ttr::marked)\n\t\t: tr::lng_signin_desc(tr::marked));\n}\n\nvoid PasswordCheckWidget::submit() {\n\tif (_sentRequest) {\n\t\treturn;\n\t}\n\tif (_pwdField->isHidden()) {\n\t\tauto code = _codeField->getLastText().trimmed();\n\t\tif (code.isEmpty()) {\n\t\t\t_codeField->showError();\n\t\t\treturn;\n\t\t}\n\t\tconst auto send = crl::guard(this, [=] {\n\t\t\t_sentRequest = api().request(MTPauth_CheckRecoveryPassword(\n\t\t\t\tMTP_string(code)\n\t\t\t)).done([=](const MTPBool &result) {\n\t\t\t\tcodeSubmitDone(code, result);\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tcodeSubmitFail(error);\n\t\t\t}).handleFloodErrors().send();\n\t\t});\n\n\t\tif (_passwordState.notEmptyPassport) {\n\t\t\tconst auto confirmed = [=](Fn<void()> &&close) {\n\t\t\t\tsend();\n\t\t\t\tclose();\n\t\t\t};\n\t\t\tUi::show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_cloud_password_passport_losing(),\n\t\t\t\t.confirmed = confirmed,\n\t\t\t\t.confirmText = tr::lng_continue(),\n\t\t\t}));\n\t\t} else {\n\t\t\tsend();\n\t\t}\n\t} else {\n\t\thideError();\n\n\t\tconst auto password = _pwdField->getLastText().toUtf8();\n\t\t_passwordHash = Core::ComputeCloudPasswordHash(\n\t\t\t_passwordState.mtp.request.algo,\n\t\t\tbytes::make_span(password));\n\t\tcheckPasswordHash();\n\t}\n}\n\nrpl::producer<QString> PasswordCheckWidget::nextButtonText() const {\n\treturn tr::lng_intro_submit();\n}\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_password_check.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"intro/intro_step.h\"\n#include \"core/core_cloud_password.h\"\n#include \"mtproto/sender.h\"\n#include \"base/timer.h\"\n\nnamespace Ui {\nclass InputField;\nclass PasswordInput;\nclass RoundButton;\nclass LinkButton;\n} // namespace Ui\n\nnamespace Intro {\nnamespace details {\n\nclass PasswordCheckWidget final : public Step {\npublic:\n\tPasswordCheckWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Account*> account,\n\t\tnot_null<Data*> data);\n\n\tvoid setInnerFocus() override;\n\tvoid activate() override;\n\tvoid cancelled() override;\n\tvoid submit() override;\n\trpl::producer<QString> nextButtonText() const override;\n\n\tbool hasBack() const override {\n\t\treturn true;\n\t}\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid toRecover();\n\tvoid toPassword();\n\n\tint errorTop() const override;\n\n\tvoid showReset();\n\tvoid refreshLang();\n\tvoid updateControlsGeometry();\n\n\tvoid pwdSubmitDone(bool recover, const MTPauth_Authorization &result);\n\tvoid pwdSubmitFail(const MTP::Error &error);\n\tvoid codeSubmitDone(const QString &code, const MTPBool &result);\n\tvoid codeSubmitFail(const MTP::Error &error);\n\tvoid recoverStartFail(const MTP::Error &error);\n\n\tvoid recoverStarted(const MTPauth_PasswordRecovery &result);\n\n\tvoid updateDescriptionText();\n\tvoid handleSrpIdInvalid();\n\tvoid requestPasswordData();\n\tvoid checkPasswordHash();\n\tvoid passwordChecked();\n\tvoid serverError();\n\n\tCore::CloudPasswordState _passwordState;\n\tcrl::time _lastSrpIdInvalidTime = 0;\n\tbytes::vector _passwordHash;\n\tQString _emailPattern;\n\n\tobject_ptr<Ui::PasswordInput> _pwdField;\n\tobject_ptr<Ui::FlatLabel> _pwdHint;\n\tobject_ptr<Ui::InputField> _codeField;\n\tobject_ptr<Ui::LinkButton> _toRecover;\n\tobject_ptr<Ui::LinkButton> _toPassword;\n\tmtpRequestId _sentRequest = 0;\n\n};\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_phone.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"intro/intro_phone.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"intro/intro_code.h\"\n#include \"intro/intro_email.h\"\n#include \"intro/intro_qr.h\"\n#include \"styles/style_intro.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/widgets/fields/special_fields.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"data/data_user.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/abstract_box.h\"\n#include \"boxes/phone_banned_box.h\"\n#include \"core/application.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"countries/countries_instance.h\" // Countries::Groups\n\nnamespace Intro {\nnamespace details {\nnamespace {\n\n[[nodiscard]] bool AllowPhoneAttempt(const QString &phone) {\n\tconst auto digits = ranges::count_if(\n\t\tphone,\n\t\t[](QChar ch) { return ch.isNumber(); });\n\treturn (digits > 1);\n}\n\n[[nodiscard]] QString DigitsOnly(QString value) {\n\tstatic const auto RegExp = QRegularExpression(\"[^0-9]\");\n\treturn value.replace(RegExp, QString());\n}\n\n} // namespace\n\nPhoneWidget::PhoneWidget(\n\tQWidget *parent,\n\tnot_null<Main::Account*> account,\n\tnot_null<Data*> data)\n: Step(parent, account, data)\n, _country(\n\tthis,\n\tgetData()->controller->uiShow(),\n\tst::introCountry)\n, _code(this, st::introCountryCode)\n, _phone(\n\tthis,\n\tst::introPhone,\n\t[](const QString &s) { return Countries::Groups(s); })\n, _checkRequestTimer([=] { checkRequest(); }) {\n\t_code->setAccessibleName(tr::lng_country_code(tr::now));\n\t_phone->setAccessibleName(tr::lng_phone_number(tr::now));\n\t_phone->frontBackspaceEvent(\n\t) | rpl::on_next([=](not_null<QKeyEvent*> e) {\n\t\t_code->startErasing(e);\n\t}, _code->lifetime());\n\n\t_country->codeChanged(\n\t) | rpl::on_next([=](const QString &code) {\n\t\t_code->codeSelected(code);\n\t\t_phone->chooseCode(code);\n\t}, _country->lifetime());\n\t_code->codeChanged(\n\t) | rpl::on_next([=](const QString &code) {\n\t\t_country->onChooseCode(code);\n\t\t_phone->chooseCode(code);\n\t}, _code->lifetime());\n\t_code->addedToNumber(\n\t) | rpl::on_next([=](const QString &added) {\n\t\t_phone->addedToNumber(added);\n\t}, _phone->lifetime());\n\t_code->spacePressed() | rpl::on_next([=] {\n\t\tsubmit();\n\t}, _code->lifetime());\n\tconnect(_phone, &Ui::PhonePartInput::changed, [=] { phoneChanged(); });\n\tconnect(_code, &Ui::CountryCodeInput::changed, [=] { phoneChanged(); });\n\n\tsetTitleText(tr::lng_phone_title());\n\tsetDescriptionText(tr::lng_phone_desc());\n\tgetData()->updated.events(\n\t) | rpl::on_next([=] {\n\t\tcountryChanged();\n\t}, lifetime());\n\tsetErrorCentered(true);\n\tsetupQrLogin();\n\n\tif (!_country->chooseCountry(getData()->country)) {\n\t\t_country->chooseCountry(u\"US\"_q);\n\t}\n\t_changed = false;\n}\n\nQString PhoneWidget::accessibilityName() {\n\treturn tr::lng_phone_title(tr::now);\n}\n\nvoid PhoneWidget::setupQrLogin() {\n\tconst auto qrLogin = Ui::CreateChild<Ui::LinkButton>(\n\t\tthis,\n\t\ttr::lng_phone_to_qr(tr::now));\n\tqrLogin->show();\n\n\tDEBUG_LOG((\"PhoneWidget.qrLogin link created and shown.\"));\n\n\trpl::combine(\n\t\tsizeValue(),\n\t\tqrLogin->widthValue()\n\t) | rpl::on_next([=](QSize size, int qrLoginWidth) {\n\t\tqrLogin->moveToLeft(\n\t\t\t(size.width() - qrLoginWidth) / 2,\n\t\t\tcontentTop() + st::introQrLoginLinkTop);\n\t}, qrLogin->lifetime());\n\n\tqrLogin->setClickedCallback([=] {\n\t\tgoReplace<QrWidget>(Animate::Forward);\n\t});\n}\n\nvoid PhoneWidget::resizeEvent(QResizeEvent *e) {\n\tStep::resizeEvent(e);\n\t_country->moveToLeft(contentLeft(), contentTop() + st::introStepFieldTop);\n\tauto phoneTop = _country->y() + _country->height() + st::introPhoneTop;\n\t_code->moveToLeft(contentLeft(), phoneTop);\n\t_phone->moveToLeft(contentLeft() + _country->width() - st::introPhone.width, phoneTop);\n}\n\nvoid PhoneWidget::showPhoneError(rpl::producer<QString> text) {\n\t_phone->showError();\n\tshowError(std::move(text));\n}\n\nvoid PhoneWidget::hidePhoneError() {\n\thideError();\n}\n\nvoid PhoneWidget::countryChanged() {\n\tif (!_changed) {\n\t\tselectCountry(getData()->country);\n\t}\n}\n\nvoid PhoneWidget::phoneChanged() {\n\t_changed = true;\n\thidePhoneError();\n}\n\nvoid PhoneWidget::submit() {\n\tif (_sentRequest || isHidden()) {\n\t\treturn;\n\t}\n\n\t{\n\t\tconst auto hasCodeButWaitingPhone = _code->hasFocus()\n\t\t\t&& (_code->getLastText().size() > 1)\n\t\t\t&& _phone->getLastText().isEmpty();\n\t\tif (hasCodeButWaitingPhone) {\n\t\t\t_phone->hideError();\n\t\t\t_phone->setFocus();\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto phone = fullNumber();\n\tif (!AllowPhoneAttempt(phone)) {\n\t\tshowPhoneError(tr::lng_bad_phone());\n\t\t_phone->setFocus();\n\t\treturn;\n\t}\n\n\tcancelNearestDcRequest();\n\n\t// Check if such account is authorized already.\n\tconst auto phoneDigits = DigitsOnly(phone);\n\tfor (const auto &[index, existing] : Core::App().domain().accounts()) {\n\t\tconst auto raw = existing.get();\n\t\tif (const auto session = raw->maybeSession()) {\n\t\t\tif (raw->mtp().environment() == account().mtp().environment()\n\t\t\t\t&& DigitsOnly(session->user()->phone()) == phoneDigits) {\n\t\t\t\tcrl::on_main(raw, [=] {\n\t\t\t\t\tCore::App().domain().activate(raw);\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\thidePhoneError();\n\n\t_checkRequestTimer.callEach(1000);\n\n\t_sentPhone = phone;\n\tapi().instance().setUserPhone(_sentPhone);\n\t_sentRequest = api().request(MTPauth_SendCode(\n\t\tMTP_string(_sentPhone),\n\t\tMTP_int(ApiId),\n\t\tMTP_string(ApiHash),\n\t\tMTP_codeSettings(\n\t\t\tMTP_flags(0),\n\t\t\tMTPVector<MTPbytes>(),\n\t\t\tMTPstring(),\n\t\t\tMTPBool())\n\t)).done([=](const MTPauth_SentCode &result) {\n\t\tphoneSubmitDone(result);\n\t}).fail([=](const MTP::Error &error) {\n\t\tphoneSubmitFail(error);\n\t}).handleFloodErrors().send();\n}\n\nvoid PhoneWidget::stopCheck() {\n\t_checkRequestTimer.cancel();\n}\n\nvoid PhoneWidget::checkRequest() {\n\tauto status = api().instance().state(_sentRequest);\n\tif (status < 0) {\n\t\tauto leftms = -status;\n\t\tif (leftms >= 1000) {\n\t\t\tapi().request(base::take(_sentRequest)).cancel();\n\t\t}\n\t}\n\tif (!_sentRequest && status == MTP::RequestSent) {\n\t\tstopCheck();\n\t}\n}\n\nvoid PhoneWidget::phoneSubmitDone(const MTPauth_SentCode &result) {\n\tstopCheck();\n\t_sentRequest = 0;\n\n\tresult.match([&](const MTPDauth_sentCode &data) {\n\t\tfillSentCodeData(data);\n\t\tgetData()->phone = DigitsOnly(_sentPhone);\n\t\tgetData()->phoneHash = qba(data.vphone_code_hash());\n\t\tif (getData()->emailStatus == EmailStatus::SetupRequired) {\n\t\t\treturn goNext<EmailWidget>();\n\t\t}\n\t\tconst auto next = data.vnext_type();\n\t\tif (next && next->type() == mtpc_auth_codeTypeCall) {\n\t\t\tgetData()->callStatus = CallStatus::Waiting;\n\t\t\tgetData()->callTimeout = data.vtimeout().value_or(60);\n\t\t} else {\n\t\t\tgetData()->callStatus = CallStatus::Disabled;\n\t\t\tgetData()->callTimeout = 0;\n\t\t}\n\t\tgoNext<CodeWidget>();\n\t}, [&](const MTPDauth_sentCodeSuccess &data) {\n\t\tfinish(data.vauthorization());\n\t}, [](const MTPDauth_sentCodePaymentRequired &) {\n\t\tLOG((\"API Error: Unexpected auth.sentCodePaymentRequired \"\n\t\t\t\"(PhoneWidget::phoneSubmitDone).\"));\n\t});\n}\n\nvoid PhoneWidget::phoneSubmitFail(const MTP::Error &error) {\n\tif (MTP::IsFloodError(error)) {\n\t\tstopCheck();\n\t\t_sentRequest = 0;\n\t\tshowPhoneError(tr::lng_flood_error());\n\t\treturn;\n\t}\n\n\tstopCheck();\n\t_sentRequest = 0;\n\tauto &err = error.type();\n\tif (err == u\"PHONE_NUMBER_FLOOD\"_q) {\n\t\tUi::show(Ui::MakeInformBox(tr::lng_error_phone_flood()));\n\t} else if (err == u\"PHONE_NUMBER_INVALID\"_q) { // show error\n\t\tshowPhoneError(tr::lng_bad_phone());\n\t} else if (err == u\"PHONE_NUMBER_BANNED\"_q) {\n\t\tUi::ShowPhoneBannedError(getData()->controller, _sentPhone);\n\t} else if (Logs::DebugEnabled()) { // internal server error\n\t\tshowPhoneError(rpl::single(err + \": \" + error.description()));\n\t} else {\n\t\tshowPhoneError(rpl::single(Lang::Hard::ServerError()));\n\t}\n}\n\nQString PhoneWidget::fullNumber() const {\n\treturn _code->getLastText() + _phone->getLastText();\n}\n\nvoid PhoneWidget::selectCountry(const QString &country) {\n\t_country->chooseCountry(country);\n}\n\nvoid PhoneWidget::setInnerFocus() {\n\t_phone->setFocusFast();\n}\n\nvoid PhoneWidget::activate() {\n\tStep::activate();\n\tshowChildren();\n\tsetInnerFocus();\n}\n\nvoid PhoneWidget::finished() {\n\tStep::finished();\n\t_checkRequestTimer.cancel();\n\tapiClear();\n\n\tcancelled();\n}\n\nvoid PhoneWidget::cancelled() {\n\tapi().request(base::take(_sentRequest)).cancel();\n}\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_phone.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/countryinput.h\"\n#include \"intro/intro_step.h\"\n#include \"base/timer.h\"\n\nnamespace Ui {\nclass PhonePartInput;\nclass CountryCodeInput;\nclass RoundButton;\nclass FlatLabel;\n} // namespace Ui\n\nnamespace Intro {\nnamespace details {\n\nclass PhoneWidget final : public Step {\npublic:\n\tPhoneWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Account*> account,\n\t\tnot_null<Data*> data);\n\n\tQString accessibilityName() override;\n\n\tvoid selectCountry(const QString &country);\n\n\tvoid setInnerFocus() override;\n\tvoid activate() override;\n\tvoid finished() override;\n\tvoid cancelled() override;\n\tvoid submit() override;\n\n\tbool hasBack() const override {\n\t\treturn true;\n\t}\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid setupQrLogin();\n\tvoid phoneChanged();\n\tvoid checkRequest();\n\tvoid countryChanged();\n\n\tvoid phoneSubmitDone(const MTPauth_SentCode &result);\n\tvoid phoneSubmitFail(const MTP::Error &error);\n\n\tQString fullNumber() const;\n\tvoid stopCheck();\n\n\tvoid showPhoneError(rpl::producer<QString> text);\n\tvoid hidePhoneError();\n\n\tbool _changed = false;\n\n\tobject_ptr<CountryInput> _country;\n\tobject_ptr<Ui::CountryCodeInput> _code;\n\tobject_ptr<Ui::PhonePartInput> _phone;\n\n\tQString _sentPhone;\n\tmtpRequestId _sentRequest = 0;\n\n\tbase::Timer _checkRequestTimer;\n\n};\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_qr.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"intro/intro_qr.h\"\n\n#include \"boxes/abstract_box.h\"\n#include \"data/components/passkeys.h\"\n#include \"intro/intro_phone.h\"\n#include \"intro/intro_widget.h\"\n#include \"intro/intro_password_check.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_account.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"core/application.h\"\n#include \"core/core_cloud_password.h\"\n#include \"core/update_checker.h\"\n#include \"base/unixtime.h\"\n#include \"qr/qr_generate.h\"\n#include \"platform/platform_webauthn.h\"\n#include \"styles/style_intro.h\"\n\nnamespace Intro {\nnamespace details {\nnamespace {\n\n[[nodiscard]] QImage TelegramQrExact(const Qr::Data &data, int pixel) {\n\treturn Qr::Generate(data, pixel, Qt::black);\n}\n\n[[nodiscard]] QImage TelegramQr(const Qr::Data &data, int pixel, int max = 0) {\n\tExpects(data.size > 0);\n\n\tif (max > 0 && data.size * pixel > max) {\n\t\tpixel = std::max(max / data.size, 1);\n\t}\n\tconst auto qr = TelegramQrExact(data, pixel * style::DevicePixelRatio());\n\tauto result = QImage(qr.size(), QImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::white);\n\t{\n\t\tauto p = QPainter(&result);\n\t\tp.drawImage(QRect(QPoint(), qr.size()), qr);\n\t}\n\treturn result;\n}\n\n[[nodiscard]] QColor QrActiveColor() {\n\treturn QColor(0x40, 0xA7, 0xE3); // Default windowBgActive.\n}\n\n[[nodiscard]] not_null<Ui::RpWidget*> PrepareQrWidget(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<QByteArray> codes) {\n\tstruct State {\n\t\texplicit State(Fn<void()> callback)\n\t\t: waiting(callback, st::defaultInfiniteRadialAnimation) {\n\t\t}\n\n\t\tQImage previous;\n\t\tQImage qr;\n\t\tQImage center;\n\t\tUi::Animations::Simple shown;\n\t\tUi::InfiniteRadialAnimation waiting;\n\t};\n\tauto qrs = std::move(\n\t\tcodes\n\t) | rpl::map([](const QByteArray &code) {\n\t\treturn Qr::Encode(code, Qr::Redundancy::Quartile);\n\t});\n\tauto palettes = rpl::single(rpl::empty) | rpl::then(\n\t\tstyle::PaletteChanged()\n\t);\n\tclass QrWidget final : public Ui::RpWidget {\n\tpublic:\n\t\tusing RpWidget::RpWidget;\n\n\t\tQAccessible::Role accessibilityRole() override {\n\t\t\treturn QAccessible::Role::Graphic;\n\t\t}\n\t\tQString accessibilityName() override {\n\t\t\treturn tr::lng_intro_qr_title(tr::now);\n\t\t}\n\n\t};\n\tauto result = Ui::CreateChild<QrWidget>(parent.get());\n\tconst auto state = result->lifetime().make_state<State>(\n\t\t[=] { result->update(); });\n\tstate->waiting.start();\n\tconst auto size = st::introQrMaxSize + 2 * st::introQrBackgroundSkip;\n\tresult->resize(size, size);\n\trpl::combine(\n\t\tstd::move(qrs),\n\t\trpl::duplicate(palettes)\n\t) | rpl::map([](const Qr::Data &code, const auto &) {\n\t\treturn TelegramQr(code, st::introQrPixel, st::introQrMaxSize);\n\t}) | rpl::on_next([=](QImage &&image) {\n\t\tstate->previous = std::move(state->qr);\n\t\tstate->qr = std::move(image);\n\t\tstate->waiting.stop();\n\t\tstate->shown.stop();\n\t\tstate->shown.start(\n\t\t\t[=] { result->update(); },\n\t\t\t0.,\n\t\t\t1.,\n\t\t\tst::fadeWrapDuration);\n\t}, result->lifetime());\n\tstd::move(\n\t\tpalettes\n\t) | rpl::map([] {\n\t\treturn TelegramLogoImage();\n\t}) | rpl::on_next([=](QImage &&image) {\n\t\tstate->center = std::move(image);\n\t}, result->lifetime());\n\tresult->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(result);\n\t\tconst auto has = !state->qr.isNull();\n\t\tconst auto shown = has ? state->shown.value(1.) : 0.;\n\t\tconst auto usualSize = 41;\n\t\tconst auto pixel = std::clamp(\n\t\t\tst::introQrMaxSize / usualSize,\n\t\t\t1,\n\t\t\tst::introQrPixel);\n\t\tconst auto size = has\n\t\t\t? (state->qr.size() / style::DevicePixelRatio())\n\t\t\t: QSize(usualSize * pixel, usualSize * pixel);\n\t\tconst auto qr = QRect(\n\t\t\t(result->width() - size.width()) / 2,\n\t\t\t(result->height() - size.height()) / 2,\n\t\t\tsize.width(),\n\t\t\tsize.height());\n\t\tconst auto radius = st::introQrBackgroundRadius;\n\t\tconst auto skip = st::introQrBackgroundSkip;\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(Qt::white);\n\t\tp.drawRoundedRect(\n\t\t\tqr.marginsAdded({ skip, skip, skip, skip }),\n\t\t\tradius,\n\t\t\tradius);\n\t\tif (!state->qr.isNull()) {\n\t\t\tif (shown == 1.) {\n\t\t\t\tstate->previous = QImage();\n\t\t\t} else if (!state->previous.isNull()) {\n\t\t\t\tp.drawImage(qr, state->previous);\n\t\t\t}\n\t\t\tp.setOpacity(shown);\n\t\t\tp.drawImage(qr, state->qr);\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t\tconst auto rect = QRect(\n\t\t\t(result->width() - st::introQrCenterSize) / 2,\n\t\t\t(result->height() - st::introQrCenterSize) / 2,\n\t\t\tst::introQrCenterSize,\n\t\t\tst::introQrCenterSize);\n\t\tp.drawImage(rect, state->center);\n\t\tif (!anim::Disabled() && state->waiting.animating()) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tconst auto line = st::radialLine;\n\t\t\tconst auto radial = state->waiting.computeState();\n\t\t\tauto pen = QPen(QrActiveColor());\n\t\t\tpen.setWidth(line);\n\t\t\tpen.setCapStyle(Qt::RoundCap);\n\t\t\tp.setOpacity(radial.shown * (1. - shown));\n\t\t\tp.setPen(pen);\n\t\t\tp.drawArc(\n\t\t\t\trect.marginsAdded({ line, line, line, line }),\n\t\t\t\tradial.arcFrom,\n\t\t\t\tradial.arcLength);\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t}, result->lifetime());\n\treturn result;\n}\n\n} // namespace\n\nQrWidget::QrWidget(\n\tQWidget *parent,\n\tnot_null<Main::Account*> account,\n\tnot_null<Data*> data)\n: Step(parent, account, data)\n, _refreshTimer([=] { refreshCode(); }) {\n\tsetTitleText(rpl::single(QString()));\n\tsetDescriptionText(rpl::single(QString()));\n\tsetErrorCentered(true);\n\n\tcancelNearestDcRequest();\n\n\taccount->mtpUpdates(\n\t) | rpl::on_next([=](const MTPUpdates &updates) {\n\t\tcheckForTokenUpdate(updates);\n\t}, lifetime());\n\n\tsetupControls();\n\taccount->mtp().mainDcIdValue(\n\t) | rpl::on_next([=] {\n\t\tapi().request(base::take(_requestId)).cancel();\n\t\trefreshCode();\n\t}, lifetime());\n\n\taccount->appConfig().value(\n\t) | rpl::filter([=] {\n\t\treturn !_passkey;\n\t}) | rpl::on_next([=] {\n\t\tsetupPasskeyLink();\n\t}, lifetime());\n}\n\nQString QrWidget::accessibilityName() {\n\treturn tr::lng_intro_qr_title(tr::now);\n}\n\nQString QrWidget::accessibilityDescription() {\n\tconst auto phrases = {\n\t\ttr::lng_intro_qr_step1,\n\t\ttr::lng_intro_qr_step2,\n\t\ttr::lng_intro_qr_step3,\n\t};\n\tauto result = QString();\n\tauto index = 0;\n\tfor (const auto &phrase : phrases) {\n\t\tresult.append(QString::number(++index)).append(\". \").append(phrase(tr::now)).append('\\n');\n\t}\n\treturn result;\n}\n\nint QrWidget::errorTop() const {\n\treturn contentTop() + st::introQrErrorTop;\n}\n\nvoid QrWidget::checkForTokenUpdate(const MTPUpdates &updates) {\n\tupdates.match([&](const MTPDupdateShort &data) {\n\t\tcheckForTokenUpdate(data.vupdate());\n\t}, [&](const MTPDupdates &data) {\n\t\tfor (const auto &update : data.vupdates().v) {\n\t\t\tcheckForTokenUpdate(update);\n\t\t}\n\t}, [&](const MTPDupdatesCombined &data) {\n\t\tfor (const auto &update : data.vupdates().v) {\n\t\t\tcheckForTokenUpdate(update);\n\t\t}\n\t}, [](const auto &) {});\n}\n\nvoid QrWidget::checkForTokenUpdate(const MTPUpdate &update) {\n\tupdate.match([&](const MTPDupdateLoginToken &data) {\n\t\tif (_requestId) {\n\t\t\t_forceRefresh = true;\n\t\t} else {\n\t\t\t_refreshTimer.cancel();\n\t\t\trefreshCode();\n\t\t}\n\t}, [](const auto &) {});\n}\n\nvoid QrWidget::submit() {\n\tgoReplace<PhoneWidget>(Animate::Forward);\n}\n\nrpl::producer<QString> QrWidget::nextButtonText() const {\n\treturn rpl::single(QString());\n}\n\nvoid QrWidget::setupControls() {\n\tconst auto code = PrepareQrWidget(this, _qrCodes.events());\n\trpl::combine(\n\t\tsizeValue(),\n\t\tcode->widthValue()\n\t) | rpl::on_next([=](QSize size, int codeWidth) {\n\t\tcode->moveToLeft(\n\t\t\t(size.width() - codeWidth) / 2,\n\t\t\tcontentTop() + st::introQrTop);\n\t}, code->lifetime());\n\n\tconst auto title = Ui::CreateChild<Ui::FlatLabel>(\n\t\tthis,\n\t\ttr::lng_intro_qr_title(),\n\t\tst::introQrTitle);\n\trpl::combine(\n\t\tsizeValue(),\n\t\ttitle->widthValue()\n\t) | rpl::on_next([=](QSize size, int titleWidth) {\n\t\ttitle->resizeToWidth(st::introQrTitleWidth);\n\t\tconst auto oneLine = st::introQrTitle.style.font->height;\n\t\tconst auto topDelta = (title->height() - oneLine);\n\t\ttitle->moveToLeft(\n\t\t\t(size.width() - title->width()) / 2,\n\t\t\tcontentTop() + st::introQrTitleTop - topDelta);\n\t}, title->lifetime());\n\n\tconst auto steps = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tconst auto texts = {\n\t\ttr::lng_intro_qr_step1,\n\t\ttr::lng_intro_qr_step2,\n\t\ttr::lng_intro_qr_step3,\n\t};\n\tauto index = 0;\n\tfor (const auto &text : texts) {\n\t\tconst auto label = steps->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tsteps,\n\t\t\t\ttext(tr::rich),\n\t\t\t\tst::introQrStep),\n\t\t\tst::introQrStepMargins);\n\t\tconst auto number = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tsteps,\n\t\t\trpl::single(tr::semibold(QString::number(++index) + \".\")),\n\t\t\tst::defaultFlatLabel);\n\t\trpl::combine(\n\t\t\tnumber->widthValue(),\n\t\t\tlabel->positionValue()\n\t\t) | rpl::on_next([=](int width, QPoint position) {\n\t\t\tnumber->moveToLeft(\n\t\t\t\tposition.x() - width - st::normalFont->spacew,\n\t\t\t\tposition.y());\n\t\t}, number->lifetime());\n\t}\n\tsteps->resizeToWidth(st::introQrLabelsWidth);\n\trpl::combine(\n\t\tsizeValue(),\n\t\tsteps->widthValue()\n\t) | rpl::on_next([=](QSize size, int stepsWidth) {\n\t\tsteps->moveToLeft(\n\t\t\t(size.width() - stepsWidth) / 2,\n\t\t\tcontentTop() + st::introQrStepsTop);\n\t}, steps->lifetime());\n\n\t_skip = Ui::CreateChild<Ui::LinkButton>(\n\t\tthis,\n\t\ttr::lng_intro_qr_phone(tr::now));\n\trpl::combine(\n\t\tsizeValue(),\n\t\t_skip->widthValue()\n\t) | rpl::on_next([=](QSize size, int skipWidth) {\n\t\t_skip->moveToLeft(\n\t\t\t(size.width() - skipWidth) / 2,\n\t\t\tcontentTop() + st::introQrSkipTop);\n\t}, _skip->lifetime());\n\n\t_skip->setClickedCallback([=] { submit(); });\n}\n\nvoid QrWidget::setupPasskeyLink() {\n\tExpects(!_passkey);\n\n\tif (!account().appConfig().settingsDisplayPasskeys()\n\t\t|| !Platform::WebAuthn::IsSupported()) {\n\t\treturn;\n\t}\n\t_passkey = Ui::CreateChild<Ui::LinkButton>(\n\t\tthis,\n\t\ttr::lng_intro_qr_passkey(tr::now));\n\t_passkey->show();\n\trpl::combine(\n\t\tsizeValue(),\n\t\t_passkey->widthValue()\n\t) | rpl::on_next([=](QSize size, int passkeyWidth) {\n\t\t_passkey->moveToLeft(\n\t\t\t(size.width() - passkeyWidth) / 2,\n\t\t\t(contentTop()\n\t\t\t\t+ st::introQrSkipTop\n\t\t\t\t+ 1.5 * st::normalFont->height));\n\t}, _passkey->lifetime());\n\n\t_passkey->setClickedCallback([=] {\n\t\tconst auto initialDc = api().instance().mainDcId();\n\t\t::Data::InitPasskeyLogin(api(), [=](\n\t\t\tconst ::Data::Passkey::LoginData &loginData) {\n\t\t\tPlatform::WebAuthn::Login(loginData, [=](\n\t\t\t\t\tPlatform::WebAuthn::LoginResult result) {\n\t\t\t\tif (result.userHandle.isEmpty()) {\n\t\t\t\t\tusing Error = Platform::WebAuthn::Error;\n\t\t\t\t\tif (result.error == Error::UnsignedBuild) {\n\t\t\t\t\t\tshowError(\n\t\t\t\t\t\t\ttr::lng_settings_passkeys_unsigned_error());\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t::Data::FinishPasskeyLogin(\n\t\t\t\t\tapi(),\n\t\t\t\t\tinitialDc,\n\t\t\t\t\tresult,\n\t\t\t\t\t[=](const MTPauth_Authorization &auth) { done(auth); },\n\t\t\t\t\t[=](QString error) {\n\t\t\t\t\t\tif (error == u\"SESSION_PASSWORD_NEEDED\"_q) {\n\t\t\t\t\t\t\tsendCheckPasswordRequest();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tshowError(rpl::single(error));\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t});\n\t\t});\n\t});\n}\n\nvoid QrWidget::refreshCode() {\n\tif (_requestId) {\n\t\treturn;\n\t}\n\t_requestId = api().request(MTPauth_ExportLoginToken(\n\t\tMTP_int(ApiId),\n\t\tMTP_string(ApiHash),\n\t\tMTP_vector<MTPlong>(0)\n\t)).done([=](const MTPauth_LoginToken &result) {\n\t\thandleTokenResult(result);\n\t}).fail([=](const MTP::Error &error) {\n\t\tshowTokenError(error);\n\t}).send();\n}\n\nvoid QrWidget::handleTokenResult(const MTPauth_LoginToken &result) {\n\tresult.match([&](const MTPDauth_loginToken &data) {\n\t\t_requestId = 0;\n\t\tshowToken(data.vtoken().v);\n\n\t\tif (base::take(_forceRefresh)) {\n\t\t\trefreshCode();\n\t\t} else {\n\t\t\tconst auto left = data.vexpires().v - base::unixtime::now();\n\t\t\t_refreshTimer.callOnce(std::max(left, 1) * crl::time(1000));\n\t\t}\n\t}, [&](const MTPDauth_loginTokenMigrateTo &data) {\n\t\timportTo(data.vdc_id().v, data.vtoken().v);\n\t}, [&](const MTPDauth_loginTokenSuccess &data) {\n\t\tdone(data.vauthorization());\n\t});\n}\n\nvoid QrWidget::showTokenError(const MTP::Error &error) {\n\t_requestId = 0;\n\tif (error.type() == u\"SESSION_PASSWORD_NEEDED\"_q) {\n\t\tsendCheckPasswordRequest();\n\t} else if (base::take(_forceRefresh)) {\n\t\trefreshCode();\n\t} else {\n\t\tshowError(rpl::single(error.type()));\n\t}\n}\n\nvoid QrWidget::showToken(const QByteArray &token) {\n\tconst auto encoded = token.toBase64(QByteArray::Base64UrlEncoding);\n\t_qrCodes.fire_copy(\"tg://login?token=\" + encoded);\n}\n\nvoid QrWidget::importTo(MTP::DcId dcId, const QByteArray &token) {\n\tExpects(_requestId != 0);\n\n\tapi().instance().setMainDcId(dcId);\n\t_requestId = api().request(MTPauth_ImportLoginToken(\n\t\tMTP_bytes(token)\n\t)).done([=](const MTPauth_LoginToken &result) {\n\t\thandleTokenResult(result);\n\t}).fail([=](const MTP::Error &error) {\n\t\tshowTokenError(error);\n\t}).toDC(dcId).send();\n}\n\nvoid QrWidget::done(const MTPauth_Authorization &authorization) {\n\tfinish(authorization);\n}\n\nvoid QrWidget::sendCheckPasswordRequest() {\n\t_requestId = api().request(MTPaccount_GetPassword(\n\t)).done([=](const MTPaccount_Password &result) {\n\t\tresult.match([&](const MTPDaccount_password &data) {\n\t\t\tgetData()->pwdState = Core::ParseCloudPasswordState(data);\n\t\t\tif (!data.vcurrent_algo() || !data.vsrp_id() || !data.vsrp_B()) {\n\t\t\t\tLOG((\"API Error: No current password received on login.\"));\n\t\t\t\tgoReplace<QrWidget>(Animate::Forward);\n\t\t\t\treturn;\n\t\t\t} else if (!getData()->pwdState.hasPassword) {\n\t\t\t\tconst auto callback = [=](Fn<void()> &&close) {\n\t\t\t\t\tCore::UpdateApplication();\n\t\t\t\t\tclose();\n\t\t\t\t};\n\t\t\t\tUi::show(Ui::MakeConfirmBox({\n\t\t\t\t\t.text = tr::lng_passport_app_out_of_date(),\n\t\t\t\t\t.confirmed = callback,\n\t\t\t\t\t.confirmText = tr::lng_menu_update(),\n\t\t\t\t}));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tgoReplace<PasswordCheckWidget>(Animate::Forward);\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\tshowTokenError(error);\n\t}).send();\n}\n\nvoid QrWidget::activate() {\n\tStep::activate();\n\tshowChildren();\n\n\tif (_skip) {\n\t\t_skip->setFocus(Qt::OtherFocusReason);\n\t}\n}\n\nvoid QrWidget::finished() {\n\tStep::finished();\n\t_refreshTimer.cancel();\n\tapiClear();\n\tcancelled();\n}\n\nvoid QrWidget::cancelled() {\n\tapi().request(base::take(_requestId)).cancel();\n}\n\nQImage TelegramLogoImage() {\n\tconst auto size = QSize(st::introQrCenterSize, st::introQrCenterSize);\n\tauto result = QImage(\n\t\tsize * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::transparent);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\t{\n\t\tauto p = QPainter(&result);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setBrush(QrActiveColor());\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawEllipse(QRect(QPoint(), size));\n\t\tst::introQrPlane.paintInCenter(p, QRect(QPoint(), size));\n\t}\n\treturn result;\n}\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_qr.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/countryinput.h\"\n#include \"intro/intro_step.h\"\n#include \"base/timer.h\"\n\nnamespace Ui {\nclass LinkButton;\n} // namespace Ui\n\nnamespace Intro {\nnamespace details {\n\nclass QrWidget final : public Step {\npublic:\n\tQrWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Account*> account,\n\t\tnot_null<Data*> data);\n\n\tQString accessibilityName() override;\n\tQString accessibilityDescription() override;\n\n\tvoid activate() override;\n\tvoid finished() override;\n\tvoid cancelled() override;\n\tvoid submit() override;\n\trpl::producer<QString> nextButtonText() const override;\n\n\tbool hasBack() const override {\n\t\treturn true;\n\t}\n\nprivate:\n\tint errorTop() const override;\n\n\tvoid sendCheckPasswordRequest();\n\tvoid setupControls();\n\tvoid setupPasskeyLink();\n\tvoid refreshCode();\n\tvoid checkForTokenUpdate(const MTPUpdates &updates);\n\tvoid checkForTokenUpdate(const MTPUpdate &update);\n\tvoid handleTokenResult(const MTPauth_LoginToken &result);\n\tvoid showTokenError(const MTP::Error &error);\n\tvoid importTo(MTP::DcId dcId, const QByteArray &token);\n\tvoid showToken(const QByteArray &token);\n\tvoid done(const MTPauth_Authorization &authorization);\n\n\trpl::event_stream<QByteArray> _qrCodes;\n\tUi::LinkButton *_skip = nullptr;\n\tUi::LinkButton *_passkey = nullptr;\n\tbase::Timer _refreshTimer;\n\tmtpRequestId _requestId = 0;\n\tbool _forceRefresh = false;\n\n};\n\n[[nodiscard]] QImage TelegramLogoImage();\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_signup.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"intro/intro_signup.h\"\n\n#include \"boxes/abstract_box.h\"\n#include \"intro/intro_widget.h\"\n#include \"core/file_utilities.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"styles/style_intro.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace Intro {\nnamespace details {\n\nSignupWidget::SignupWidget(\n\tQWidget *parent,\n\tnot_null<Main::Account*> account,\n\tnot_null<Data*> data)\n: Step(parent, account, data)\n, _photo(\n\tthis,\n\tdata->controller,\n\tUi::UserpicButton::Role::ChoosePhoto,\n\tst::defaultUserpicButton)\n, _first(this, st::introName, tr::lng_signup_firstname())\n, _last(this, st::introName, tr::lng_signup_lastname())\n, _invertOrder(langFirstNameGoesSecond()) {\n\t_photo->showCustomOnChosen();\n\n\tLang::Updated(\n\t) | rpl::on_next([=] {\n\t\trefreshLang();\n\t}, lifetime());\n\n\tif (_invertOrder) {\n\t\tsetTabOrder(_last, _first);\n\t} else {\n\t\tsetTabOrder(_first, _last);\n\t}\n\n\tsetErrorCentered(true);\n\n\tsetTitleText(tr::lng_signup_title());\n\tsetDescriptionText(tr::lng_signup_desc());\n\tsetMouseTracking(true);\n}\n\nvoid SignupWidget::finishInit() {\n\tshowTerms();\n}\n\nvoid SignupWidget::refreshLang() {\n\t_invertOrder = langFirstNameGoesSecond();\n\tif (_invertOrder) {\n\t\tsetTabOrder(_last, _first);\n\t} else {\n\t\tsetTabOrder(_first, _last);\n\t}\n\tupdateControlsGeometry();\n}\n\nvoid SignupWidget::resizeEvent(QResizeEvent *e) {\n\tStep::resizeEvent(e);\n\tupdateControlsGeometry();\n}\n\nvoid SignupWidget::updateControlsGeometry() {\n\tauto photoRight = contentLeft() + st::introNextButton.width;\n\tauto photoTop = contentTop() + st::introPhotoTop;\n\t_photo->moveToLeft(photoRight - _photo->width(), photoTop);\n\n\tauto firstTop = contentTop() + st::introStepFieldTop;\n\tauto secondTop = firstTop + st::introName.heightMin + st::introPhoneTop;\n\tif (_invertOrder) {\n\t\t_last->moveToLeft(contentLeft(), firstTop);\n\t\t_first->moveToLeft(contentLeft(), secondTop);\n\t} else {\n\t\t_first->moveToLeft(contentLeft(), firstTop);\n\t\t_last->moveToLeft(contentLeft(), secondTop);\n\t}\n}\n\nvoid SignupWidget::setInnerFocus() {\n\tif (_invertOrder || _last->hasFocus()) {\n\t\t_last->setFocusFast();\n\t} else {\n\t\t_first->setFocusFast();\n\t}\n}\n\nvoid SignupWidget::activate() {\n\tStep::activate();\n\t_first->show();\n\t_last->show();\n\t_photo->show();\n\tsetInnerFocus();\n}\n\nvoid SignupWidget::cancelled() {\n\tapi().request(base::take(_sentRequest)).cancel();\n}\n\nvoid SignupWidget::nameSubmitDone(const MTPauth_Authorization &result) {\n\tfinish(result);\n}\n\nvoid SignupWidget::nameSubmitFail(const MTP::Error &error) {\n\tif (MTP::IsFloodError(error)) {\n\t\tshowError(tr::lng_flood_error());\n\t\tif (_invertOrder) {\n\t\t\t_first->setFocus();\n\t\t} else {\n\t\t\t_last->setFocus();\n\t\t}\n\t\treturn;\n\t}\n\n\tauto &err = error.type();\n\tif (err == u\"PHONE_NUMBER_FLOOD\"_q) {\n\t\tUi::show(Ui::MakeInformBox(tr::lng_error_phone_flood()));\n\t} else if (err == u\"PHONE_NUMBER_INVALID\"_q\n\t\t|| err == u\"PHONE_NUMBER_BANNED\"_q\n\t\t|| err == u\"PHONE_CODE_EXPIRED\"_q\n\t\t|| err == u\"PHONE_CODE_EMPTY\"_q\n\t\t|| err == u\"PHONE_CODE_INVALID\"_q\n\t\t|| err == u\"PHONE_NUMBER_OCCUPIED\"_q) {\n\t\tgoBack();\n\t} else if (err == \"FIRSTNAME_INVALID\") {\n\t\tshowError(tr::lng_bad_name());\n\t\t_first->setFocus();\n\t} else if (err == \"LASTNAME_INVALID\") {\n\t\tshowError(tr::lng_bad_name());\n\t\t_last->setFocus();\n\t} else {\n\t\tif (Logs::DebugEnabled()) { // internal server error\n\t\t\tshowError(rpl::single(err + \": \" + error.description()));\n\t\t} else {\n\t\t\tshowError(rpl::single(Lang::Hard::ServerError()));\n\t\t}\n\t\tif (_invertOrder) {\n\t\t\t_last->setFocus();\n\t\t} else {\n\t\t\t_first->setFocus();\n\t\t}\n\t}\n}\n\nvoid SignupWidget::submit() {\n\tif (_sentRequest) {\n\t\treturn;\n\t}\n\tif (_invertOrder) {\n\t\tif ((_last->hasFocus() || _last->getLastText().trimmed().length()) && !_first->getLastText().trimmed().length()) {\n\t\t\t_first->setFocus();\n\t\t\treturn;\n\t\t} else if (!_last->getLastText().trimmed().length()) {\n\t\t\t_last->setFocus();\n\t\t\treturn;\n\t\t}\n\t} else {\n\t\tif ((_first->hasFocus() || _first->getLastText().trimmed().length()) && !_last->getLastText().trimmed().length()) {\n\t\t\t_last->setFocus();\n\t\t\treturn;\n\t\t} else if (!_first->getLastText().trimmed().length()) {\n\t\t\t_first->setFocus();\n\t\t\treturn;\n\t\t}\n\t}\n\n\tconst auto send = [&] {\n\t\thideError();\n\n\t\t_firstName = _first->getLastText().trimmed();\n\t\t_lastName = _last->getLastText().trimmed();\n\t\t_sentRequest = api().request(MTPauth_SignUp(\n\t\t\tMTP_flags(0),\n\t\t\tMTP_string(getData()->phone),\n\t\t\tMTP_bytes(getData()->phoneHash),\n\t\t\tMTP_string(_firstName),\n\t\t\tMTP_string(_lastName)\n\t\t)).done([=](const MTPauth_Authorization &result) {\n\t\t\tnameSubmitDone(result);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tnameSubmitFail(error);\n\t\t}).handleFloodErrors().send();\n\t};\n\tif (_termsAccepted\n\t\t|| getData()->termsLock.text.text.isEmpty()\n\t\t|| !getData()->termsLock.popup) {\n\t\tsend();\n\t} else {\n\t\tacceptTerms(crl::guard(this, [=] {\n\t\t\t_termsAccepted = true;\n\t\t\tsend();\n\t\t}));\n\t}\n}\n\nrpl::producer<QString> SignupWidget::nextButtonText() const {\n\treturn tr::lng_intro_finish();\n}\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_signup.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"intro/intro_step.h\"\n\nnamespace Ui {\nclass RoundButton;\nclass InputField;\nclass UserpicButton;\n} // namespace Ui\n\nnamespace Intro {\nnamespace details {\n\nclass SignupWidget final : public Step {\npublic:\n\tSignupWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Account*> account,\n\t\tnot_null<Data*> data);\n\n\tvoid finishInit() override;\n\tvoid setInnerFocus() override;\n\tvoid activate() override;\n\tvoid cancelled() override;\n\tvoid submit() override;\n\trpl::producer<QString> nextButtonText() const override;\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid refreshLang();\n\tvoid updateControlsGeometry();\n\n\tvoid nameSubmitDone(const MTPauth_Authorization &result);\n\tvoid nameSubmitFail(const MTP::Error &error);\n\n\tobject_ptr<Ui::UserpicButton> _photo;\n\tobject_ptr<Ui::InputField> _first;\n\tobject_ptr<Ui::InputField> _last;\n\tQString _firstName, _lastName;\n\tmtpRequestId _sentRequest = 0;\n\n\tbool _invertOrder = false;\n\n\tbool _termsAccepted = false;\n\n};\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_start.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"intro/intro_start.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"intro/intro_qr.h\"\n#include \"intro/intro_phone.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"main/main_account.h\"\n#include \"main/main_app_config.h\"\n\nnamespace Intro {\nnamespace details {\n\nStartWidget::StartWidget(\n\tQWidget *parent,\n\tnot_null<Main::Account*> account,\n\tnot_null<Data*> data)\n: Step(parent, account, data, true) {\n\tsetMouseTracking(true);\n\tsetTitleText(rpl::single(u\"Telegram Desktop\"_q));\n\tsetDescriptionText(tr::lng_intro_about());\n\tsetTitleText(rpl::single(qsl(\"Forkgram Desktop\")));\n\tsetDescriptionText(rpl::single(qsl(\"Unofficial application.\")));\n\tshow();\n}\n\nvoid StartWidget::submit() {\n\taccount().destroyStaleAuthorizationKeys();\n\tgoNext<QrWidget>();\n}\n\nrpl::producer<QString> StartWidget::nextButtonText() const {\n\treturn tr::lng_start_msgs();\n}\n\nrpl::producer<> StartWidget::nextButtonFocusRequests() const {\n\treturn _nextButtonFocusRequests.events();\n}\n\nvoid StartWidget::activate() {\n\tStep::activate();\n\tsetInnerFocus();\n}\n\nvoid StartWidget::setInnerFocus() {\n\t_nextButtonFocusRequests.fire({});\n}\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_start.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"intro/intro_step.h\"\n\nnamespace Ui {\nclass FlatLabel;\nclass LinkButton;\nclass RoundButton;\n} // namespace Ui\n\nnamespace Intro {\nnamespace details {\n\nclass StartWidget : public Step {\npublic:\n\tStartWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Account*> account,\n\t\tnot_null<Data*> data);\n\n\tvoid submit() override;\n\trpl::producer<QString> nextButtonText() const override;\n\trpl::producer<> nextButtonFocusRequests() const override;\n\tvoid activate() override;\n\tvoid setInnerFocus() override;\n\nprivate:\n\trpl::event_stream<> _nextButtonFocusRequests;\n\n};\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_step.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"intro/intro_step.h\"\n\n#include \"intro/intro_widget.h\"\n#include \"intro/intro_signup.h\"\n#include \"storage/localstorage.h\"\n#include \"storage/storage_account.h\"\n#include \"lang/lang_keys.h\"\n#include \"lang/lang_instance.h\"\n#include \"lang/lang_cloud_manager.h\"\n#include \"main/main_account.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"boxes/abstract_box.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"apiwrap.h\"\n#include \"api/api_peer_photo.h\"\n#include \"mainwindow.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/effects/slide_animation.h\"\n#include \"ui/ui_utility.h\"\n#include \"data/data_user.h\"\n#include \"data/data_auto_download.h\"\n#include \"data/data_session.h\"\n#include \"data/data_chat_filters.h\"\n#include \"window/window_controller.h\"\n#include \"styles/style_intro.h\"\n#include \"styles/style_window.h\"\n\nnamespace Intro {\nnamespace details {\nnamespace {\n\nvoid PrepareSupportMode(not_null<Main::Session*> session) {\n\tusing ::Data::AutoDownload::Full;\n\n\tanim::SetDisabled(true);\n\tCore::App().settings().setDesktopNotify(false);\n\tCore::App().settings().setSoundNotify(false);\n\tCore::App().settings().setFlashBounceNotify(false);\n\tCore::App().saveSettings();\n\n\tsession->settings().autoDownload() = Full::FullDisabled();\n\tsession->saveSettings();\n}\n\n} // namespace\n\nStep::CoverAnimation::~CoverAnimation() = default;\n\nStep::Step(\n\tQWidget *parent,\n\tnot_null<Main::Account*> account,\n\tnot_null<Data*> data,\n\tbool hasCover)\n: RpWidget(parent)\n, _account(account)\n, _data(data)\n, _hasCover(hasCover)\n, _title(this, _hasCover ? st::introCoverTitle : st::introTitle)\n, _description(\n\tthis,\n\tobject_ptr<Ui::FlatLabel>(\n\t\tthis,\n\t\t_hasCover\n\t\t\t? st::introCoverDescription\n\t\t\t: st::introDescription)) {\n\thide();\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tif (!_coverMask.isNull()) {\n\t\t\t_coverMask = QPixmap();\n\t\t\tprepareCoverMask();\n\t\t}\n\t}, lifetime());\n\n\t_errorText.value(\n\t) | rpl::on_next([=](const QString &text) {\n\t\trefreshError(text);\n\t}, lifetime());\n\n\t_titleText.value(\n\t) | rpl::on_next([=](const QString &text) {\n\t\t_title->setText(text);\n\t\taccessibilityNameChanged();\n\t\tupdateLabelsPosition();\n\t}, lifetime());\n\n\t_descriptionText.value(\n\t) | rpl::on_next([=](const TextWithEntities &text) {\n\t\tconst auto label = _description->entity();\n\t\tconst auto hasSpoiler = ranges::contains(\n\t\t\ttext.entities,\n\t\t\tEntityType::Spoiler,\n\t\t\t&EntityInText::type);\n\t\tlabel->setMarkedText(text);\n\t\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents, hasSpoiler);\n\t\taccessibilityDescriptionChanged();\n\t\tupdateLabelsPosition();\n\t}, lifetime());\n}\n\nStep::~Step() = default;\n\nMTP::Sender &Step::api() const {\n\tif (!_api) {\n\t\t_api.emplace(&_account->mtp());\n\t}\n\treturn *_api;\n}\n\nvoid Step::apiClear() {\n\t_api.reset();\n}\n\nrpl::producer<QString> Step::nextButtonText() const {\n\treturn tr::lng_intro_next();\n}\n\nrpl::producer<const style::RoundButton*> Step::nextButtonStyle() const {\n\treturn rpl::single((const style::RoundButton*)(nullptr));\n}\n\nrpl::producer<> Step::nextButtonFocusRequests() const {\n\treturn rpl::never();\n}\n\nvoid Step::goBack() {\n\tif (_goCallback) {\n\t\t_goCallback(nullptr, StackAction::Back, Animate::Back);\n\t}\n}\n\nvoid Step::goNext(Step *step) {\n\tif (_goCallback) {\n\t\t_goCallback(step, StackAction::Forward, Animate::Forward);\n\t}\n}\n\nvoid Step::goReplace(Step *step, Animate animate) {\n\tif (_goCallback) {\n\t\t_goCallback(step, StackAction::Replace, animate);\n\t}\n}\n\nvoid Step::finish(const MTPauth_Authorization &auth, QImage &&photo) {\n\tauth.match([&](const MTPDauth_authorization &data) {\n\t\tif (data.vuser().type() != mtpc_user\n\t\t\t|| !data.vuser().c_user().is_self()) {\n\t\t\tshowError(rpl::single(Lang::Hard::ServerError())); // wtf?\n\t\t\treturn;\n\t\t}\n\t\tfinish(data.vuser(), std::move(photo));\n\t}, [&](const MTPDauth_authorizationSignUpRequired &data) {\n\t\tif (const auto terms = data.vterms_of_service()) {\n\t\t\tterms->match([&](const MTPDhelp_termsOfService &data) {\n\t\t\t\tgetData()->termsLock = Window::TermsLock::FromMTP(\n\t\t\t\t\tnullptr,\n\t\t\t\t\tdata);\n\t\t\t});\n\t\t} else {\n\t\t\tgetData()->termsLock = Window::TermsLock();\n\t\t}\n\t\tgoReplace<SignupWidget>(Animate::Forward);\n\t});\n}\n\nvoid Step::finish(const MTPUser &user, QImage &&photo) {\n\tif (user.type() != mtpc_user\n\t\t|| !user.c_user().is_self()\n\t\t|| !user.c_user().vid().v) {\n\t\t// No idea what to do here.\n\t\t// We could've reset intro and MTP, but this really should not happen.\n\t\tUi::show(Ui::MakeInformBox(\n\t\t\t\"Internal error: bad user.is_self() after sign in.\"));\n\t\treturn;\n\t}\n\n\t// Check if such account is authorized already.\n\tfor (const auto &[index, existing] : Core::App().domain().accounts()) {\n\t\tconst auto raw = existing.get();\n\t\tif (const auto session = raw->maybeSession()) {\n\t\t\tif (raw->mtp().environment() == _account->mtp().environment()\n\t\t\t\t&& UserId(user.c_user().vid()) == session->userId()) {\n\t\t\t\t_account->logOut();\n\t\t\t\tcrl::on_main(raw, [=] {\n\t\t\t\t\tCore::App().domain().activate(raw);\n\t\t\t\t\tLocal::sync();\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tapi().request(MTPmessages_GetDialogFilters(\n\t)).done([=](const MTPmessages_DialogFilters &result) {\n\t\tconst auto &d = result.data();\n\t\tcreateSession(user, photo, d.vfilters().v, d.is_tags_enabled());\n\t}).fail([=] {\n\t\tcreateSession(user, photo, QVector<MTPDialogFilter>(), false);\n\t}).send();\n}\n\nvoid Step::createSession(\n\t\tconst MTPUser &user,\n\t\tQImage photo,\n\t\tconst QVector<MTPDialogFilter> &filters,\n\t\tbool tagsEnabled) {\n\t// Save the default language if we've suggested some other and user ignored it.\n\tconst auto currentId = Lang::Id();\n\tconst auto defaultId = Lang::DefaultLanguageId();\n\tconst auto suggested = Lang::CurrentCloudManager().suggestedLanguage();\n\tif (currentId.isEmpty() && !suggested.isEmpty() && suggested != defaultId) {\n\t\tLang::GetInstance().switchToId(Lang::DefaultLanguage());\n\t\tLocal::writeLangPack();\n\t}\n\n\tauto settings = std::make_unique<Main::SessionSettings>();\n\tconst auto hasFilters = ranges::contains(\n\t\tfilters,\n\t\tmtpc_dialogFilter,\n\t\t&MTPDialogFilter::type);\n\tsettings->setDialogsFiltersEnabled(hasFilters);\n\n\tconst auto account = _account;\n\taccount->createSession(user, std::move(settings));\n\n\t// \"this\" is already deleted here by creating the main widget.\n\taccount->local().enforceModernStorageIdBots();\n\taccount->local().writeMtpData();\n\tauto &session = account->session();\n\tsession.data().chatsFilters().setPreloaded(filters, tagsEnabled);\n\tif (hasFilters) {\n\t\tsession.saveSettingsDelayed();\n\t}\n\tif (!photo.isNull()) {\n\t\tsession.api().peerPhoto().upload(\n\t\t\tsession.user(),\n\t\t\t{ std::move(photo) });\n\t}\n\taccount->appConfig().refresh();\n\tif (session.supportMode()) {\n\t\tPrepareSupportMode(&session);\n\t}\n\tLocal::sync();\n}\n\nvoid Step::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tpaintAnimated(p, e->rect());\n}\n\nvoid Step::resizeEvent(QResizeEvent *e) {\n\tupdateLabelsPosition();\n}\n\nvoid Step::updateLabelsPosition() {\n\tUi::SendPendingMoveResizeEvents(_description->entity());\n\tif (hasCover()) {\n\t\t_title->moveToLeft((width() - _title->width()) / 2, contentTop() + st::introCoverTitleTop);\n\t\t_description->moveToLeft((width() - _description->width()) / 2, contentTop() + st::introCoverDescriptionTop);\n\t} else {\n\t\t_title->moveToLeft(contentLeft() + st::buttonRadius, contentTop() + st::introTitleTop);\n\t\t_description->resizeToWidth(st::introDescription.minWidth);\n\t\t_description->moveToLeft(contentLeft() + st::buttonRadius, contentTop() + st::introDescriptionTop);\n\t}\n\tif (_error) {\n\t\tif (_errorCentered) {\n\t\t\t_error->entity()->resizeToWidth(width());\n\t\t}\n\t\tUi::SendPendingMoveResizeEvents(_error->entity());\n\t\tauto errorLeft = _errorCentered ? 0 : (contentLeft() + st::buttonRadius);\n\t\t_error->moveToLeft(errorLeft, errorTop());\n\t}\n}\n\nint Step::errorTop() const {\n\treturn contentTop() + st::introErrorTop;\n}\n\nvoid Step::setTitleText(rpl::producer<QString> titleText) {\n\t_titleText = std::move(titleText);\n}\n\nvoid Step::setDescriptionText(v::text::data &&descriptionText) {\n\t_descriptionText = v::text::take_marked(std::move(descriptionText));\n}\n\nvoid Step::showFinished() {\n\t_a_show.stop();\n\t_coverAnimation = CoverAnimation();\n\t_slideAnimation.reset();\n\tprepareCoverMask();\n\tactivate();\n}\n\nbool Step::paintAnimated(QPainter &p, QRect clip) {\n\tif (_slideAnimation) {\n\t\t_slideAnimation->paintFrame(p, (width() - st::introStepWidth) / 2, contentTop(), width());\n\t\tif (!_slideAnimation->animating()) {\n\t\t\tshowFinished();\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\tauto dt = _a_show.value(1.);\n\tif (!_a_show.animating()) {\n\t\tif (hasCover()) {\n\t\t\tpaintCover(p, 0);\n\t\t}\n\t\tif (_coverAnimation.title) {\n\t\t\tshowFinished();\n\t\t}\n\t\tif (!QRect(0, contentTop(), width(), st::introStepHeight).intersects(clip)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\tif (!_coverAnimation.clipping.isEmpty()) {\n\t\tp.setClipRect(_coverAnimation.clipping);\n\t}\n\n\tauto progress = (hasCover() ? anim::easeOutCirc(1., dt) : anim::linear(1., dt));\n\tauto arrivingAlpha = progress;\n\tauto departingAlpha = 1. - progress;\n\tauto showCoverMethod = progress;\n\tauto hideCoverMethod = progress;\n\tauto coverTop = (hasCover() ? anim::interpolate(-st::introCoverHeight, 0, showCoverMethod) : anim::interpolate(0, -st::introCoverHeight, hideCoverMethod));\n\n\tpaintCover(p, coverTop);\n\n\tauto positionReady = hasCover() ? showCoverMethod : hideCoverMethod;\n\t_coverAnimation.title->paintFrame(p, positionReady, departingAlpha, arrivingAlpha);\n\t_coverAnimation.description->paintFrame(p, positionReady, departingAlpha, arrivingAlpha);\n\n\tpaintContentSnapshot(p, _coverAnimation.contentSnapshotWas, departingAlpha, showCoverMethod);\n\tpaintContentSnapshot(p, _coverAnimation.contentSnapshotNow, arrivingAlpha, 1. - hideCoverMethod);\n\n\treturn true;\n}\n\nvoid Step::fillSentCodeData(const MTPDauth_sentCode &data) {\n\tconst auto bad = [](const char *type) {\n\t\tLOG((\"API Error: Should not be '%1'.\").arg(type));\n\t};\n\tgetData()->codeByTelegram = false;\n\tgetData()->codeByFragmentUrl = QString();\n\tdata.vtype().match([&](const MTPDauth_sentCodeTypeApp &data) {\n\t\tgetData()->codeByTelegram = true;\n\t\tgetData()->codeLength = data.vlength().v;\n\t}, [&](const MTPDauth_sentCodeTypeSms &data) {\n\t\tgetData()->codeLength = data.vlength().v;\n\t}, [&](const MTPDauth_sentCodeTypeFragmentSms &data) {\n\t\tgetData()->codeByFragmentUrl = qs(data.vurl());\n\t\tgetData()->codeLength = data.vlength().v;\n\t}, [&](const MTPDauth_sentCodeTypeCall &data) {\n\t\tgetData()->codeLength = data.vlength().v;\n\t}, [&](const MTPDauth_sentCodeTypeFlashCall &) {\n\t\tbad(\"FlashCall\");\n\t}, [&](const MTPDauth_sentCodeTypeMissedCall &) {\n\t\tbad(\"MissedCall\");\n\t}, [&](const MTPDauth_sentCodeTypeFirebaseSms &) {\n\t\tbad(\"FirebaseSms\");\n\t}, [&](const MTPDauth_sentCodeTypeEmailCode &data) {\n\t\tgetData()->emailPatternLogin = qs(data.vemail_pattern());\n\t\tgetData()->codeLength = data.vlength().v;\n\t}, [&](const MTPDauth_sentCodeTypeSmsWord &) {\n\t\tbad(\"SmsWord\");\n\t}, [&](const MTPDauth_sentCodeTypeSmsPhrase &) {\n\t\tbad(\"SmsPhrase\");\n\t}, [&](const MTPDauth_sentCodeTypeSetUpEmailRequired &) {\n\t\tgetData()->emailStatus = EmailStatus::SetupRequired;\n\t});\n}\n\nvoid Step::showDescription() {\n\t_description->show(anim::type::normal);\n}\n\nvoid Step::hideDescription() {\n\t_description->hide(anim::type::normal);\n}\n\nvoid Step::paintContentSnapshot(QPainter &p, const QPixmap &snapshot, float64 alpha, float64 howMuchHidden) {\n\tif (!snapshot.isNull()) {\n\t\tconst auto contentTop = anim::interpolate(\n\t\t\theight() - (snapshot.height() / style::DevicePixelRatio()),\n\t\t\theight(),\n\t\t\thowMuchHidden);\n\t\tif (contentTop < height()) {\n\t\t\tp.setOpacity(alpha);\n\t\t\tp.drawPixmap(\n\t\t\t\tQPoint(contentLeft(), contentTop),\n\t\t\t\tsnapshot,\n\t\t\t\tQRect(\n\t\t\t\t\t0,\n\t\t\t\t\t0,\n\t\t\t\t\tsnapshot.width(),\n\t\t\t\t\t(height() - contentTop) * style::DevicePixelRatio()));\n\t\t}\n\t}\n}\n\nvoid Step::prepareCoverMask() {\n\tif (!_coverMask.isNull()) return;\n\n\tauto maskWidth = style::DevicePixelRatio();\n\tauto maskHeight = st::introCoverHeight * style::DevicePixelRatio();\n\tauto mask = QImage(maskWidth, maskHeight, QImage::Format_ARGB32_Premultiplied);\n\tauto maskInts = reinterpret_cast<uint32*>(mask.bits());\n\tAssert(mask.depth() == (sizeof(uint32) << 3));\n\tauto maskIntsPerLineAdded = (mask.bytesPerLine() >> 2) - maskWidth;\n\tAssert(maskIntsPerLineAdded >= 0);\n\tauto realHeight = static_cast<float64>(maskHeight - 1);\n\tfor (auto y = 0; y != maskHeight; ++y) {\n\t\tauto color = anim::color(QColor(83, 61, 133), QColor(61, 44, 95), y / realHeight);\n\t\tauto colorInt = anim::getPremultiplied(color);\n\t\tfor (auto x = 0; x != maskWidth; ++x) {\n\t\t\t*maskInts++ = colorInt;\n\t\t}\n\t\tmaskInts += maskIntsPerLineAdded;\n\t}\n\t_coverMask = Ui::PixmapFromImage(std::move(mask));\n}\n\nvoid Step::paintCover(QPainter &p, int top) {\n\tauto coverHeight = top + st::introCoverHeight;\n\tif (coverHeight > 0) {\n\t\tp.drawPixmap(\n\t\t\tQRect(0, 0, width(), coverHeight),\n\t\t\t_coverMask,\n\t\t\tQRect(\n\t\t\t\t0,\n\t\t\t\t-top * style::DevicePixelRatio(),\n\t\t\t\t_coverMask.width(),\n\t\t\t\tcoverHeight * style::DevicePixelRatio()));\n\t}\n\n\tauto left = 0;\n\tauto right = 0;\n\tif (width() < st::introCoverMaxWidth) {\n\t\tauto iconsMaxSkip = st::introCoverMaxWidth - st::introCoverLeft.width() - st::introCoverRight.width();\n\t\tauto iconsSkip = st::introCoverIconsMinSkip + (iconsMaxSkip - st::introCoverIconsMinSkip) * (width() - st::introStepWidth) / (st::introCoverMaxWidth - st::introStepWidth);\n\t\tauto outside = iconsSkip + st::introCoverLeft.width() + st::introCoverRight.width() - width();\n\t\tleft = -outside / 2;\n\t\tright = -outside - left;\n\t}\n\tif (top < 0) {\n\t\tauto shown = float64(coverHeight) / st::introCoverHeight;\n\t\tauto leftShown = qRound(shown * (left + st::introCoverLeft.width()));\n\t\tleft = leftShown - st::introCoverLeft.width();\n\t\tauto rightShown = qRound(shown * (right + st::introCoverRight.width()));\n\t\tright = rightShown - st::introCoverRight.width();\n\t}\n\tconst auto cOver = QColor(255, 255, 255, 40);\n\tst::introCoverLeft.paint(p, left, coverHeight - st::introCoverLeft.height(), width(), cOver);\n\tst::introCoverRight.paint(p, width() - right - st::introCoverRight.width(), coverHeight - st::introCoverRight.height(), width(), cOver);\n\n\tauto planeLeft = (width() - st::introCoverIcon.width()) / 2 - st::introCoverIconLeft;\n\tauto planeTop = top + st::introCoverIconTop;\n\tif (top < 0 && !_hasCover) {\n\t\tauto deltaLeft = -qRound(float64(st::introPlaneWidth / st::introPlaneHeight) * top);\n//\t\tauto deltaTop = top;\n\t\tplaneLeft += deltaLeft;\n\t//\tplaneTop += top;\n\t}\n\tst::introCoverIcon.paint(p, planeLeft, planeTop, width());\n}\n\nint Step::contentLeft() const {\n\treturn (width() - st::introNextButton.width) / 2;\n}\n\nint Step::contentTop() const {\n\tauto result = (height() - st::introHeight) / 2;\n\taccumulate_max(result, st::introStepTopMin);\n\tif (_hasCover) {\n\t\tconst auto currentHeightFull = result + st::introNextTop + st::introContentTopAdd;\n\t\tauto added = 1. - std::clamp(\n\t\t\tfloat64(currentHeightFull - st::windowMinHeight)\n\t\t\t\t/ (st::introStepHeightFull - st::windowMinHeight),\n\t\t\t0.,\n\t\t\t1.);\n\t\tresult += qRound(added * st::introContentTopAdd);\n\t}\n\treturn result;\n}\n\nvoid Step::setErrorCentered(bool centered) {\n\t_errorCentered = centered;\n\t_error.destroy();\n}\n\nvoid Step::showError(rpl::producer<QString> text) {\n\t_errorText = std::move(text);\n}\n\nvoid Step::refreshError(const QString &text) {\n\tif (text.isEmpty()) {\n\t\tif (_error) _error->hide(anim::type::normal);\n\t} else {\n\t\tif (!_error) {\n\t\t\t_error.create(\n\t\t\t\tthis,\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tthis,\n\t\t\t\t\t_errorCentered\n\t\t\t\t\t\t? st::introErrorCentered\n\t\t\t\t\t\t: st::introError));\n\t\t\t_error->hide(anim::type::instant);\n\t\t}\n\t\t_error->entity()->setText(text);\n\t\tupdateLabelsPosition();\n\t\t_error->show(anim::type::normal);\n\t}\n}\n\nvoid Step::prepareShowAnimated(Step *after) {\n\tsetInnerFocus();\n\tif (hasCover() || after->hasCover()) {\n\t\t_coverAnimation = prepareCoverAnimation(after);\n\t\tprepareCoverMask();\n\t} else {\n\t\tauto leftSnapshot = after->prepareSlideAnimation();\n\t\tauto rightSnapshot = prepareSlideAnimation();\n\t\t_slideAnimation = std::make_unique<Ui::SlideAnimation>();\n\t\t_slideAnimation->setSnapshots(std::move(leftSnapshot), std::move(rightSnapshot));\n\t\t_slideAnimation->setOverflowHidden(false);\n\t}\n}\n\nStep::CoverAnimation Step::prepareCoverAnimation(Step *after) {\n\tUi::SendPendingMoveResizeEvents(this);\n\n\tauto result = CoverAnimation();\n\tresult.title = Ui::FlatLabel::CrossFade(\n\t\tafter->_title,\n\t\t_title,\n\t\tst::introBg);\n\tresult.description = Ui::FlatLabel::CrossFade(\n\t\tafter->_description->entity(),\n\t\t_description->entity(),\n\t\tst::introBg,\n\t\tafter->_description->pos(),\n\t\t_description->pos());\n\tresult.contentSnapshotWas = after->prepareContentSnapshot();\n\tresult.contentSnapshotNow = prepareContentSnapshot();\n\treturn result;\n}\n\nQPixmap Step::prepareContentSnapshot() {\n\tauto otherTop = _description->y() + _description->height();\n\tauto otherRect = myrtlrect(contentLeft(), otherTop, st::introStepWidth, height() - otherTop);\n\treturn Ui::GrabWidget(this, otherRect);\n}\n\nQPixmap Step::prepareSlideAnimation() {\n\tauto grabLeft = (width() - st::introStepWidth) / 2;\n\tauto grabTop = contentTop();\n\treturn Ui::GrabWidget(\n\t\tthis,\n\t\tQRect(grabLeft, grabTop, st::introStepWidth, st::introStepHeight));\n}\n\nvoid Step::showAnimated(Animate animate) {\n\tsetFocus();\n\tshow();\n\thideChildren();\n\tif (_slideAnimation) {\n\t\tauto slideLeft = (animate == Animate::Back);\n\t\t_slideAnimation->start(\n\t\t\tslideLeft,\n\t\t\t[=] { update(0, contentTop(), width(), st::introStepHeight); },\n\t\t\tst::introSlideDuration);\n\t} else {\n\t\t_a_show.start([this] { update(); }, 0., 1., st::introCoverDuration);\n\t}\n}\n\nvoid Step::setShowAnimationClipping(QRect clipping) {\n\t_coverAnimation.clipping = clipping;\n}\n\nvoid Step::setGoCallback(\n\t\tFn<void(Step *step, StackAction action, Animate animate)> callback) {\n\t_goCallback = std::move(callback);\n}\n\nvoid Step::setShowResetCallback(Fn<void()> callback) {\n\t_showResetCallback = std::move(callback);\n}\n\nvoid Step::setShowTermsCallback(Fn<void()> callback) {\n\t_showTermsCallback = std::move(callback);\n}\n\nvoid Step::setCancelNearestDcCallback(Fn<void()> callback) {\n\t_cancelNearestDcCallback = std::move(callback);\n}\n\nvoid Step::setAcceptTermsCallback(\n\t\tFn<void(Fn<void()> callback)> callback) {\n\t_acceptTermsCallback = std::move(callback);\n}\n\nvoid Step::showFast() {\n\tshow();\n\tshowFinished();\n}\n\nbool Step::animating() const {\n\treturn (_slideAnimation && _slideAnimation->animating())\n\t\t|| _a_show.animating();\n}\n\nbool Step::hasCover() const {\n\treturn _hasCover;\n}\n\nbool Step::hasBack() const {\n\treturn false;\n}\n\nvoid Step::activate() {\n\t_title->show();\n\t_description->show(anim::type::instant);\n\tif (!_errorText.current().isEmpty()) {\n\t\t_error->show(anim::type::instant);\n\t}\n}\n\nvoid Step::cancelled() {\n}\n\nvoid Step::finished() {\n\thide();\n}\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_step.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"mtproto/sender.h\"\n#include \"ui/text/text_variant.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace style {\nstruct RoundButton;\n} // namespace style;\n\nnamespace Main {\nclass Account;\n} // namespace Main;\n\nnamespace Ui {\nclass SlideAnimation;\nclass CrossFadeAnimation;\nclass FlatLabel;\ntemplate <typename Widget>\nclass FadeWrap;\n} // namespace Ui\n\nnamespace Intro {\nnamespace details {\n\nstruct Data;\nenum class StackAction;\nenum class Animate;\n\nclass Step : public Ui::RpWidget {\npublic:\n\tStep(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Account*> account,\n\t\tnot_null<Data*> data,\n\t\tbool hasCover = false);\n\t~Step();\n\n\tQAccessible::Role accessibilityRole() override {\n\t\treturn QAccessible::Role::Dialog;\n\t}\n\tQString accessibilityName() override {\n\t\treturn _titleText.current();\n\t}\n\tQString accessibilityDescription() override {\n\t\treturn _descriptionText.current().text;\n\t}\n\n\t[[nodiscard]] Main::Account &account() const {\n\t\treturn *_account;\n\t}\n\n\t// It should not be called in StartWidget, in other steps it should be\n\t// present and not changing.\n\t[[nodiscard]] MTP::Sender &api() const;\n\tvoid apiClear();\n\n\tvirtual void finishInit() {\n\t}\n\tvirtual void setInnerFocus() {\n\t\tsetFocus();\n\t}\n\n\tvoid setGoCallback(\n\t\tFn<void(Step *step, StackAction action, Animate animate)> callback);\n\tvoid setShowResetCallback(Fn<void()> callback);\n\tvoid setShowTermsCallback(Fn<void()> callback);\n\tvoid setCancelNearestDcCallback(Fn<void()> callback);\n\tvoid setAcceptTermsCallback(\n\t\tFn<void(Fn<void()> callback)> callback);\n\n\tvoid prepareShowAnimated(Step *after);\n\tvoid showAnimated(Animate animate);\n\tvoid showFast();\n\t[[nodiscard]] bool animating() const;\n\tvoid setShowAnimationClipping(QRect clipping);\n\n\t[[nodiscard]] bool hasCover() const;\n\t[[nodiscard]] virtual bool hasBack() const;\n\tvirtual void activate();\n\tvirtual void cancelled();\n\tvirtual void finished();\n\n\tvirtual void submit() = 0;\n\t[[nodiscard]] virtual rpl::producer<QString> nextButtonText() const;\n\t[[nodiscard]] virtual auto nextButtonStyle() const\n\t\t-> rpl::producer<const style::RoundButton*>;\n\t[[nodiscard]] virtual rpl::producer<> nextButtonFocusRequests() const;\n\n\t[[nodiscard]] int contentLeft() const;\n\t[[nodiscard]] int contentTop() const;\n\n\tvoid setErrorCentered(bool centered);\n\tvoid showError(rpl::producer<QString> text);\n\tvoid hideError() {\n\t\tshowError(rpl::single(QString()));\n\t}\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\tvoid setTitleText(rpl::producer<QString> titleText);\n\tvoid setDescriptionText(v::text::data &&descriptionText);\n\tbool paintAnimated(QPainter &p, QRect clip);\n\n\tvoid fillSentCodeData(const MTPDauth_sentCode &type);\n\n\tvoid showDescription();\n\tvoid hideDescription();\n\n\t[[nodiscard]] not_null<Data*> getData() const {\n\t\treturn _data;\n\t}\n\tvoid finish(const MTPauth_Authorization &auth, QImage &&photo = {});\n\tvoid finish(const MTPUser &user, QImage &&photo = {});\n\tvoid createSession(\n\t\tconst MTPUser &user,\n\t\tQImage photo,\n\t\tconst QVector<MTPDialogFilter> &filters,\n\t\tbool tagsEnabled);\n\n\tvoid goBack();\n\n\ttemplate <typename StepType>\n\tvoid goNext() {\n\t\tgoNext(new StepType(parentWidget(), _account, _data));\n\t}\n\n\ttemplate <typename StepType>\n\tvoid goReplace(Animate animate) {\n\t\tgoReplace(new StepType(parentWidget(), _account, _data), animate);\n\t}\n\n\tvoid showResetButton() {\n\t\tif (_showResetCallback) _showResetCallback();\n\t}\n\tvoid showTerms() {\n\t\tif (_showTermsCallback) _showTermsCallback();\n\t}\n\tvoid acceptTerms(Fn<void()> callback) {\n\t\tif (_acceptTermsCallback) {\n\t\t\t_acceptTermsCallback(callback);\n\t\t}\n\t}\n\tvoid cancelNearestDcRequest() {\n\t\tif (_cancelNearestDcCallback) _cancelNearestDcCallback();\n\t}\n\n\tvirtual int errorTop() const;\n\nprivate:\n\tstruct CoverAnimation {\n\t\tCoverAnimation() = default;\n\t\tCoverAnimation(CoverAnimation &&other) = default;\n\t\tCoverAnimation &operator=(CoverAnimation &&other) = default;\n\t\t~CoverAnimation();\n\n\t\tstd::unique_ptr<Ui::CrossFadeAnimation> title;\n\t\tstd::unique_ptr<Ui::CrossFadeAnimation> description;\n\n\t\t// From content top till the next button top.\n\t\tQPixmap contentSnapshotWas;\n\t\tQPixmap contentSnapshotNow;\n\n\t\tQRect clipping;\n\t};\n\tvoid updateLabelsPosition();\n\tvoid paintContentSnapshot(\n\t\tQPainter &p,\n\t\tconst QPixmap &snapshot,\n\t\tfloat64 alpha,\n\t\tfloat64 howMuchHidden);\n\tvoid refreshError(const QString &text);\n\n\tvoid goNext(Step *step);\n\tvoid goReplace(Step *step, Animate animate);\n\n\t[[nodiscard]] CoverAnimation prepareCoverAnimation(Step *step);\n\t[[nodiscard]] QPixmap prepareContentSnapshot();\n\t[[nodiscard]] QPixmap prepareSlideAnimation();\n\tvoid showFinished();\n\n\tvoid prepareCoverMask();\n\tvoid paintCover(QPainter &p, int top);\n\n\tconst not_null<Main::Account*> _account;\n\tconst not_null<Data*> _data;\n\tmutable std::optional<MTP::Sender> _api;\n\n\tbool _hasCover = false;\n\tFn<void(Step *step, StackAction action, Animate animate)> _goCallback;\n\tFn<void()> _showResetCallback;\n\tFn<void()> _showTermsCallback;\n\tFn<void()> _cancelNearestDcCallback;\n\tFn<void(Fn<void()> callback)> _acceptTermsCallback;\n\n\trpl::variable<QString> _titleText;\n\tobject_ptr<Ui::FlatLabel> _title;\n\trpl::variable<TextWithEntities> _descriptionText;\n\tobject_ptr<Ui::FadeWrap<Ui::FlatLabel>> _description;\n\n\tbool _errorCentered = false;\n\trpl::variable<QString> _errorText;\n\tobject_ptr<Ui::FadeWrap<Ui::FlatLabel>> _error = { nullptr };\n\n\tUi::Animations::Simple _a_show;\n\tCoverAnimation _coverAnimation;\n\tstd::unique_ptr<Ui::SlideAnimation> _slideAnimation;\n\tQPixmap _coverMask;\n\n};\n\n\n} // namespace details\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"intro/intro_widget.h\"\n\n#include \"intro/intro_start.h\"\n#include \"intro/intro_phone.h\"\n#include \"intro/intro_qr.h\"\n#include \"intro/intro_code.h\"\n#include \"intro/intro_signup.h\"\n#include \"intro/intro_password_check.h\"\n#include \"lang/lang_keys.h\"\n#include \"lang/lang_instance.h\"\n#include \"lang/lang_cloud_manager.h\"\n#include \"storage/localstorage.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"mainwindow.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"data/data_user.h\"\n#include \"data/components/promo_suggestions.h\"\n#include \"countries/countries_instance.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/format_values.h\" // Ui::FormatPhone\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/ui_utility.h\"\n#include \"boxes/abstract_box.h\"\n#include \"core/update_checker.h\"\n#include \"core/application.h\"\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"window/window_slide_animation.h\"\n#include \"window/window_connecting_widget.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/section_widget.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"api/api_text_entities.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_intro.h\"\n#include \"base/qt/qt_common_adapters.h\"\n\nnamespace Intro {\nnamespace {\n\nusing namespace ::Intro::details;\n\n[[nodiscard]] QString ComputeNewAccountCountry() {\n\tif (const auto parent\n\t\t= Core::App().domain().maybeLastOrSomeAuthedAccount()) {\n\t\tif (const auto session = parent->maybeSession()) {\n\t\t\tconst auto iso = Countries::Instance().countryISO2ByPhone(\n\t\t\t\tsession->user()->phone());\n\t\t\tif (!iso.isEmpty()) {\n\t\t\t\treturn iso;\n\t\t\t}\n\t\t}\n\t}\n\treturn Platform::SystemCountry();\n}\n\n} // namespace\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Window::Controller*> controller,\n\tnot_null<Main::Account*> account,\n\tEnterPoint point)\n: RpWidget(parent)\n, _account(account)\n, _data(details::Data{ .controller = controller })\n, _nextStyle(&st::introNextButton)\n, _back(this, object_ptr<Ui::IconButton>(this, st::introBackButton))\n, _settings(\n\tthis,\n\tobject_ptr<Ui::RoundButton>(\n\t\tthis,\n\t\ttr::lng_menu_settings(),\n\t\tst::defaultBoxButton))\n, _next(\n\tthis,\n\tobject_ptr<Ui::RoundButton>(this, nullptr, *_nextStyle))\n, _connecting(std::make_unique<Window::ConnectionState>(\n\t\tthis,\n\t\taccount,\n\t\trpl::single(true))) {\n\t_settings->entity()->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\tcontroller->setDefaultFloatPlayerDelegate(floatPlayerDelegate());\n\n\tgetData()->country = ComputeNewAccountCountry();\n\n\t_account->mtpValue(\n\t) | rpl::on_next([=](not_null<MTP::Instance*> instance) {\n\t\t_api.emplace(instance);\n\t\tcrl::on_main(this, [=] { createLanguageLink(); });\n\t}, lifetime());\n\n\tswitch (point) {\n\tcase EnterPoint::Start:\n\t\tgetNearestDC();\n\t\tappendStep(new StartWidget(this, _account, getData()));\n\t\tbreak;\n\tcase EnterPoint::Phone:\n\t\tappendStep(new PhoneWidget(this, _account, getData()));\n\t\tbreak;\n\tcase EnterPoint::Qr:\n\t\tappendStep(new QrWidget(this, _account, getData()));\n\t\tbreak;\n\tdefault: Unexpected(\"Enter point in Intro::Widget::Widget.\");\n\t}\n\n\tsetupStep();\n\tfixOrder();\n\n\tif (_account->mtp().isTestMode()) {\n\t\t_testModeLabel.create(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tthis,\n\t\t\t\tu\"Test Mode\"_q,\n\t\t\t\tst::defaultFlatLabel));\n\t\t_testModeLabel->entity()->setTextColorOverride(\n\t\t\tst::windowSubTextFg->c);\n\t\t_testModeLabel->show(anim::type::instant);\n\t}\n\n\tLang::CurrentCloudManager().firstLanguageSuggestion(\n\t) | rpl::on_next([=] {\n\t\tcreateLanguageLink();\n\t}, lifetime());\n\n\t_account->mtpUpdates(\n\t) | rpl::on_next([=](const MTPUpdates &updates) {\n\t\thandleUpdates(updates);\n\t}, lifetime());\n\n\t_back->entity()->setClickedCallback([=] { backRequested(); });\n\t_back->entity()->setAccessibleName(tr::lng_go_back(tr::now));\n\t_back->hide(anim::type::instant);\n\n\tif (_changeLanguage) {\n\t\t_changeLanguage->finishAnimating();\n\t}\n\n\tLang::Updated(\n\t) | rpl::on_next([=] {\n\t\trefreshLang();\n\t}, lifetime());\n\n\tshow();\n\tshowControls();\n\tgetStep()->showFast();\n\tsetInnerFocus();\n\n\tcSetPasswordRecovered(false);\n\n\tif (!Core::UpdaterDisabled()) {\n\t\tCore::UpdateChecker checker;\n\t\tchecker.start();\n\t\trpl::merge(\n\t\t\trpl::single(rpl::empty),\n\t\t\tchecker.isLatest(),\n\t\t\tchecker.failed(),\n\t\t\tchecker.ready()\n\t\t) | rpl::on_next([=] {\n\t\t\tcheckUpdateStatus();\n\t\t}, lifetime());\n\t}\n}\n\nrpl::producer<> Widget::showSettingsRequested() const {\n\treturn _settings->entity()->clicks() | rpl::to_empty;\n}\n\nnot_null<Media::Player::FloatDelegate*> Widget::floatPlayerDelegate() {\n\treturn static_cast<Media::Player::FloatDelegate*>(this);\n}\n\nauto Widget::floatPlayerSectionDelegate()\n-> not_null<Media::Player::FloatSectionDelegate*> {\n\treturn static_cast<Media::Player::FloatSectionDelegate*>(this);\n}\n\nnot_null<Ui::RpWidget*> Widget::floatPlayerWidget() {\n\treturn this;\n}\n\nvoid Widget::floatPlayerToggleGifsPaused(bool paused) {\n}\n\nauto Widget::floatPlayerGetSection(Window::Column column)\n-> not_null<Media::Player::FloatSectionDelegate*> {\n\treturn this;\n}\n\nvoid Widget::floatPlayerEnumerateSections(Fn<void(\n\t\tnot_null<Media::Player::FloatSectionDelegate*> widget,\n\t\tWindow::Column widgetColumn)> callback) {\n\tcallback(this, Window::Column::Second);\n}\n\nbool Widget::floatPlayerIsVisible(not_null<HistoryItem*> item) {\n\treturn false;\n}\n\nvoid Widget::floatPlayerDoubleClickEvent(not_null<const HistoryItem*> item) {\n\tgetData()->controller->invokeForSessionController(\n\t\t&item->history()->peer->session().account(),\n\t\titem->history()->peer,\n\t\t[&](not_null<Window::SessionController*> controller) {\n\t\t\tcontroller->showMessage(item);\n\t\t});\n}\n\nQRect Widget::floatPlayerAvailableRect() {\n\treturn mapToGlobal(rect());\n}\n\nbool Widget::floatPlayerHandleWheelEvent(QEvent *e) {\n\treturn false;\n}\n\nvoid Widget::refreshLang() {\n\t_changeLanguage.destroy();\n\tcreateLanguageLink();\n\tInvokeQueued(this, [this] { updateControlsGeometry(); });\n}\n\nvoid Widget::handleUpdates(const MTPUpdates &updates) {\n\tupdates.match([&](const MTPDupdateShort &data) {\n\t\thandleUpdate(data.vupdate());\n\t}, [&](const MTPDupdates &data) {\n\t\tfor (const auto &update : data.vupdates().v) {\n\t\t\thandleUpdate(update);\n\t\t}\n\t}, [&](const MTPDupdatesCombined &data) {\n\t\tfor (const auto &update : data.vupdates().v) {\n\t\t\thandleUpdate(update);\n\t\t}\n\t}, [](const auto &) {});\n}\n\nvoid Widget::handleUpdate(const MTPUpdate &update) {\n\tupdate.match([&](const MTPDupdateDcOptions &data) {\n\t\t_account->mtp().dcOptions().addFromList(data.vdc_options());\n\t}, [&](const MTPDupdateConfig &data) {\n\t\t_account->mtp().requestConfig();\n\t\tif (_account->sessionExists()) {\n\t\t\t_account->session().promoSuggestions().invalidate();\n\t\t}\n\t}, [&](const MTPDupdateServiceNotification &data) {\n\t\tconst auto text = TextWithEntities{\n\t\t\tqs(data.vmessage()),\n\t\t\tApi::EntitiesFromMTP(nullptr, data.ventities().v)\n\t\t};\n\t\tUi::show(Ui::MakeInformBox(text));\n\t}, [](const auto &) {});\n}\n\nvoid Widget::createLanguageLink() {\n\tif (_changeLanguage\n\t\t|| Core::App().domain().maybeLastOrSomeAuthedAccount()) {\n\t\treturn;\n\t}\n\n\tconst auto createLink = [=](\n\t\t\tconst QString &text,\n\t\t\tconst QString &languageId) {\n\t\t_changeLanguage.create(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::LinkButton>(this, text));\n\t\t_changeLanguage->hide(anim::type::instant);\n\t\t_changeLanguage->entity()->setClickedCallback([=] {\n\t\t\tLang::CurrentCloudManager().switchToLanguage(languageId);\n\t\t});\n\t\t_changeLanguage->toggle(\n\t\t\t!_resetAccount && !_terms && _nextShown,\n\t\t\tanim::type::normal);\n\t\tupdateControlsGeometry();\n\t};\n\n\tconst auto currentId = Lang::LanguageIdOrDefault(Lang::Id());\n\tconst auto defaultId = Lang::DefaultLanguageId();\n\tconst auto suggested = Lang::CurrentCloudManager().suggestedLanguage();\n\tif (currentId != defaultId) {\n\t\tcreateLink(\n\t\t\tLang::GetOriginalValue(tr::lng_switch_to_this.base),\n\t\t\tdefaultId);\n\t} else if (!suggested.isEmpty() && suggested != currentId && _api) {\n\t\t_api->request(MTPlangpack_GetStrings(\n\t\t\tMTP_string(Lang::CloudLangPackName()),\n\t\t\tMTP_string(suggested),\n\t\t\tMTP_vector<MTPstring>(1, MTP_string(\"lng_switch_to_this\"))\n\t\t)).done([=](const MTPVector<MTPLangPackString> &result) {\n\t\t\tconst auto strings = Lang::Instance::ParseStrings(result);\n\t\t\tconst auto i = strings.find(tr::lng_switch_to_this.base);\n\t\t\tif (i != strings.end()) {\n\t\t\t\tcreateLink(i->second, suggested);\n\t\t\t}\n\t\t}).send();\n\t}\n}\n\nvoid Widget::checkUpdateStatus() {\n\tExpects(!Core::UpdaterDisabled());\n\n\tif (Core::UpdateChecker().state() == Core::UpdateChecker::State::Ready) {\n\t\tif (_update) return;\n\t\t_update.create(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::RoundButton>(\n\t\t\t\tthis,\n\t\t\t\ttr::lng_menu_update(),\n\t\t\t\tst::defaultBoxButton));\n\t\t_update->entity()->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\t\tif (!_showAnimation) {\n\t\t\t_update->setVisible(true);\n\t\t}\n\t\tconst auto stepHasCover = getStep()->hasCover();\n\t\t_update->toggle(!stepHasCover, anim::type::instant);\n\t\t_update->entity()->setClickedCallback([] {\n\t\t\tCore::checkReadyUpdate();\n\t\t\tCore::Restart();\n\t\t});\n\t} else {\n\t\tif (!_update) return;\n\t\t_update.destroy();\n\t}\n\tupdateControlsGeometry();\n}\n\nvoid Widget::setInnerFocus() {\n\tif (getStep()->animating()) {\n\t\tsetFocus();\n\t} else {\n\t\tgetStep()->setInnerFocus();\n\t}\n}\n\nvoid Widget::setupStep() {\n\tgetStep()->nextButtonStyle(\n\t) | rpl::on_next([=](const style::RoundButton *st) {\n\t\tconst auto nextStyle = st ? st : &st::introNextButton;\n\t\tif (_nextStyle != nextStyle) {\n\t\t\t_nextStyle = nextStyle;\n\t\t\tconst auto wasShown = _next->toggled();\n\t\t\t_next.destroy();\n\t\t\t_next.create(\n\t\t\t\tthis,\n\t\t\t\tobject_ptr<Ui::RoundButton>(this, nullptr, *nextStyle));\n\t\t\tshowControls();\n\t\t\tupdateControlsGeometry();\n\t\t\t_next->toggle(wasShown, anim::type::instant);\n\t\t}\n\t}, getStep()->lifetime());\n\n\tgetStep()->nextButtonFocusRequests() | rpl::on_next([=] {\n\t\tif (_next && !_next->isHidden()) {\n\t\t\t_next->entity()->setFocus(Qt::OtherFocusReason);\n\t\t}\n\t}, getStep()->lifetime());\n\n\tgetStep()->finishInit();\n}\n\nvoid Widget::historyMove(StackAction action, Animate animate) {\n\tExpects(_stepHistory.size() > 1);\n\n\tif (getStep()->animating()) {\n\t\treturn;\n\t}\n\n\tauto wasStep = getStep((action == StackAction::Back) ? 0 : 1);\n\tif (action == StackAction::Back) {\n\t\t_stepHistory.pop_back();\n\t\twasStep->cancelled();\n\t} else if (action == StackAction::Replace) {\n\t\t_stepHistory.erase(_stepHistory.end() - 2);\n\t}\n\n\tif (_resetAccount) {\n\t\thideAndDestroy(std::exchange(_resetAccount, { nullptr }));\n\t}\n\tif (_terms) {\n\t\thideAndDestroy(std::exchange(_terms, { nullptr }));\n\t}\n\tsetupStep();\n\n\tgetStep()->prepareShowAnimated(wasStep);\n\tif (wasStep->hasCover() != getStep()->hasCover()) {\n\t\t_nextTopFrom = wasStep->contentTop() + st::introNextTop;\n\t\t_controlsTopFrom = wasStep->hasCover() ? st::introCoverHeight : 0;\n\t\t_coverShownAnimation.start(\n\t\t\t[this] { updateControlsGeometry(); },\n\t\t\t0.,\n\t\t\t1.,\n\t\t\tst::introCoverDuration,\n\t\t\twasStep->hasCover() ? anim::linear : anim::easeOutCirc);\n\t}\n\n\t_stepLifetime.destroy();\n\tif (action == StackAction::Forward || action == StackAction::Replace) {\n\t\twasStep->finished();\n\t}\n\tif (action == StackAction::Back || action == StackAction::Replace) {\n\t\tdelete base::take(wasStep);\n\t}\n\t_back->toggle(getStep()->hasBack(), anim::type::normal);\n\n\tauto stepHasCover = getStep()->hasCover();\n\t_settings->toggle(!stepHasCover, anim::type::normal);\n\tif (_testModeLabel) {\n\t\t_testModeLabel->toggle(!stepHasCover, anim::type::normal);\n\t}\n\tif (_update) {\n\t\t_update->toggle(!stepHasCover, anim::type::normal);\n\t}\n\tsetupNextButton();\n\tif (_resetAccount) _resetAccount->show(anim::type::normal);\n\tif (_terms) _terms->show(anim::type::normal);\n\tgetStep()->showAnimated(animate);\n\tfixOrder();\n}\n\nvoid Widget::hideAndDestroy(object_ptr<Ui::FadeWrap<Ui::RpWidget>> widget) {\n\tconst auto weak = base::make_weak(widget.data());\n\twidget->hide(anim::type::normal);\n\twidget->shownValue(\n\t) | rpl::on_next([=](bool shown) {\n\t\tif (!shown && weak) {\n\t\t\tweak->deleteLater();\n\t\t}\n\t}, widget->lifetime());\n}\n\nvoid Widget::fixOrder() {\n\t_next->raise();\n\tif (_update) _update->raise();\n\tif (_changeLanguage) _changeLanguage->raise();\n\t_settings->raise();\n\t_back->raise();\n\tfloatPlayerRaiseAll();\n\t_connecting->raise();\n}\n\nvoid Widget::moveToStep(Step *step, StackAction action, Animate animate) {\n\tappendStep(step);\n\t_back->raise();\n\t_settings->raise();\n\tif (_update) {\n\t\t_update->raise();\n\t}\n\t_connecting->raise();\n\n\thistoryMove(action, animate);\n}\n\nvoid Widget::appendStep(Step *step) {\n\t_stepHistory.push_back(step);\n\tstep->setGeometry(rect());\n\tstep->setGoCallback([=](Step *step, StackAction action, Animate animate) {\n\t\tif (action == StackAction::Back) {\n\t\t\tbackRequested();\n\t\t} else {\n\t\t\tmoveToStep(step, action, animate);\n\t\t}\n\t});\n\tstep->setShowResetCallback([=] {\n\t\tshowResetButton();\n\t});\n\tstep->setShowTermsCallback([=] {\n\t\tshowTerms();\n\t});\n\tstep->setCancelNearestDcCallback([=] {\n\t\tif (_api) {\n\t\t\t_api->request(base::take(_nearestDcRequestId)).cancel();\n\t\t}\n\t});\n\tstep->setAcceptTermsCallback([=](Fn<void()> callback) {\n\t\tacceptTerms(callback);\n\t});\n}\n\nvoid Widget::showResetButton() {\n\tif (!_resetAccount) {\n\t\tauto entity = object_ptr<Ui::RoundButton>(\n\t\t\tthis,\n\t\t\ttr::lng_signin_reset_account(),\n\t\t\tst::introResetButton);\n\t\tentity->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\t\t_resetAccount.create(this, std::move(entity));\n\t\t_resetAccount->hide(anim::type::instant);\n\t\t_resetAccount->entity()->setClickedCallback([this] { resetAccount(); });\n\t\tupdateControlsGeometry();\n\t}\n\t_resetAccount->show(anim::type::normal);\n\tif (_changeLanguage) {\n\t\t_changeLanguage->hide(anim::type::normal);\n\t}\n}\n\nvoid Widget::showTerms() {\n\tif (getData()->termsLock.text.text.isEmpty()) {\n\t\t_terms.destroy();\n\t} else if (!_terms) {\n\t\tauto entity = object_ptr<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\ttr::lng_terms_signup(\n\t\t\t\tlt_link,\n\t\t\t\ttr::lng_terms_signup_link(tr::link),\n\t\t\t\ttr::marked),\n\t\t\tst::introTermsLabel);\n\t\t_terms.create(this, std::move(entity));\n\t\t_terms->entity()->overrideLinkClickHandler([=] {\n\t\t\tshowTerms(nullptr);\n\t\t});\n\t\tupdateControlsGeometry();\n\t\t_terms->hide(anim::type::instant);\n\t}\n\tif (_changeLanguage) {\n\t\t_changeLanguage->toggle(\n\t\t\t!_terms && !_resetAccount && _nextShown,\n\t\t\tanim::type::normal);\n\t}\n}\n\nvoid Widget::acceptTerms(Fn<void()> callback) {\n\tshowTerms(callback);\n}\n\nvoid Widget::resetAccount() {\n\tif (_resetRequest || !_api) {\n\t\treturn;\n\t}\n\n\tconst auto callback = crl::guard(this, [this] {\n\t\tif (_resetRequest) {\n\t\t\treturn;\n\t\t}\n\t\t_resetRequest = _api->request(MTPaccount_DeleteAccount(\n\t\t\tMTP_flags(0),\n\t\t\tMTP_string(\"Forgot password\"),\n\t\t\tMTPInputCheckPasswordSRP()\n\t\t)).done([=] {\n\t\t\t_resetRequest = 0;\n\n\t\t\tgetData()->controller->hideLayer();\n\t\t\tif (getData()->phone.isEmpty()) {\n\t\t\t\tmoveToStep(\n\t\t\t\t\tnew QrWidget(this, _account, getData()),\n\t\t\t\t\tStackAction::Replace,\n\t\t\t\t\tAnimate::Back);\n\t\t\t} else {\n\t\t\t\tmoveToStep(\n\t\t\t\t\tnew SignupWidget(this, _account, getData()),\n\t\t\t\t\tStackAction::Replace,\n\t\t\t\t\tAnimate::Forward);\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t_resetRequest = 0;\n\n\t\t\tconst auto &type = error.type();\n\t\t\tif (type.startsWith(u\"2FA_CONFIRM_WAIT_\"_q)) {\n\t\t\t\tconst auto seconds = base::StringViewMid(\n\t\t\t\t\ttype,\n\t\t\t\t\tu\"2FA_CONFIRM_WAIT_\"_q.size()).toInt();\n\t\t\t\tconst auto days = (seconds + 59) / 86400;\n\t\t\t\tconst auto hours = ((seconds + 59) % 86400) / 3600;\n\t\t\t\tconst auto minutes = ((seconds + 59) % 3600) / 60;\n\t\t\t\tauto when = tr::lng_minutes(tr::now, lt_count, minutes);\n\t\t\t\tif (days > 0) {\n\t\t\t\t\tconst auto daysCount = tr::lng_days(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tdays);\n\t\t\t\t\tconst auto hoursCount = tr::lng_hours(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\thours);\n\t\t\t\t\twhen = tr::lng_signin_reset_in_days(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_days_count,\n\t\t\t\t\t\tdaysCount,\n\t\t\t\t\t\tlt_hours_count,\n\t\t\t\t\t\thoursCount,\n\t\t\t\t\t\tlt_minutes_count,\n\t\t\t\t\t\twhen);\n\t\t\t\t} else if (hours > 0) {\n\t\t\t\t\tconst auto hoursCount = tr::lng_hours(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\thours);\n\t\t\t\t\twhen = tr::lng_signin_reset_in_hours(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_hours_count,\n\t\t\t\t\t\thoursCount,\n\t\t\t\t\t\tlt_minutes_count,\n\t\t\t\t\t\twhen);\n\t\t\t\t}\n\t\t\t\tUi::show(Ui::MakeInformBox(tr::lng_signin_reset_wait(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_phone_number,\n\t\t\t\t\tUi::FormatPhone(getData()->phone),\n\t\t\t\t\tlt_when,\n\t\t\t\t\twhen)));\n\t\t\t} else if (type == u\"2FA_RECENT_CONFIRM\"_q) {\n\t\t\t\tUi::show(Ui::MakeInformBox(\n\t\t\t\t\ttr::lng_signin_reset_cancelled()));\n\t\t\t} else {\n\t\t\t\tgetData()->controller->hideLayer();\n\t\t\t\tgetStep()->showError(rpl::single(Lang::Hard::ServerError()));\n\t\t\t}\n\t\t}).send();\n\t});\n\n\tUi::show(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_signin_sure_reset(),\n\t\t.confirmed = callback,\n\t\t.confirmText = tr::lng_signin_reset(),\n\t\t.confirmStyle = &st::attentionBoxButton,\n\t}));\n}\n\nvoid Widget::getNearestDC() {\n\tif (!_api) {\n\t\treturn;\n\t}\n\t_nearestDcRequestId = _api->request(MTPhelp_GetNearestDc(\n\t)).done([=](const MTPNearestDc &result) {\n\t\t_nearestDcRequestId = 0;\n\t\tconst auto &nearest = result.c_nearestDc();\n\t\tDEBUG_LOG((\"Got nearest dc, country: %1, nearest: %2, this: %3\"\n\t\t\t).arg(qs(nearest.vcountry())\n\t\t\t).arg(nearest.vnearest_dc().v\n\t\t\t).arg(nearest.vthis_dc().v));\n\t\t_account->suggestMainDcId(nearest.vnearest_dc().v);\n\t\tconst auto nearestCountry = qs(nearest.vcountry());\n\t\tif (getData()->country != nearestCountry) {\n\t\t\tgetData()->country = nearestCountry;\n\t\t\tgetData()->updated.fire({});\n\t\t}\n\t}).send();\n}\n\nvoid Widget::showTerms(Fn<void()> callback) {\n\tif (getData()->termsLock.text.text.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto weak = base::make_weak(this);\n\tconst auto box = Ui::show(callback\n\t\t? Box<Window::TermsBox>(\n\t\t\tgetData()->termsLock,\n\t\t\ttr::lng_terms_agree(),\n\t\t\ttr::lng_terms_decline())\n\t\t: Box<Window::TermsBox>(\n\t\t\tgetData()->termsLock.text,\n\t\t\ttr::lng_box_ok(),\n\t\t\tnullptr));\n\n\tbox->setCloseByEscape(false);\n\tbox->setCloseByOutsideClick(false);\n\n\tbox->agreeClicks(\n\t) | rpl::on_next([=] {\n\t\tif (callback) {\n\t\t\tcallback();\n\t\t}\n\t\tif (box) {\n\t\t\tbox->closeBox();\n\t\t}\n\t}, box->lifetime());\n\n\tbox->cancelClicks(\n\t) | rpl::on_next([=] {\n\t\tconst auto box = Ui::show(Box<Window::TermsBox>(\n\t\t\tTextWithEntities{ tr::lng_terms_signup_sorry(tr::now) },\n\t\t\ttr::lng_intro_finish(),\n\t\t\ttr::lng_terms_decline()));\n\t\tbox->agreeClicks(\n\t\t) | rpl::on_next([=] {\n\t\t\tif (weak) {\n\t\t\t\tshowTerms(callback);\n\t\t\t}\n\t\t}, box->lifetime());\n\t\tbox->cancelClicks(\n\t\t) | rpl::on_next([=] {\n\t\t\tif (box) {\n\t\t\t\tbox->closeBox();\n\t\t\t}\n\t\t}, box->lifetime());\n\t}, box->lifetime());\n}\n\nvoid Widget::showControls() {\n\tgetStep()->show();\n\tsetupNextButton();\n\t_next->toggle(_nextShown, anim::type::instant);\n\t_nextShownAnimation.stop();\n\t_connecting->setForceHidden(false);\n\tauto hasCover = getStep()->hasCover();\n\t_settings->toggle(!hasCover, anim::type::instant);\n\tif (_testModeLabel) {\n\t\t_testModeLabel->toggle(!hasCover, anim::type::instant);\n\t}\n\tif (_update) {\n\t\t_update->toggle(!hasCover, anim::type::instant);\n\t}\n\tif (_changeLanguage) {\n\t\t_changeLanguage->toggle(\n\t\t\t!_resetAccount && !_terms && _nextShown,\n\t\t\tanim::type::instant);\n\t}\n\tif (_terms) {\n\t\t_terms->show(anim::type::instant);\n\t}\n\t_back->toggle(getStep()->hasBack(), anim::type::instant);\n}\n\nvoid Widget::setupNextButton() {\n\t_next->entity()->setClickedCallback([=] { getStep()->submit(); });\n\n\t_next->entity()->setText(getStep()->nextButtonText(\n\t) | rpl::filter([](const QString &text) {\n\t\treturn !text.isEmpty();\n\t}));\n\tgetStep()->nextButtonText(\n\t) | rpl::map([](const QString &text) {\n\t\treturn !text.isEmpty();\n\t}) | rpl::filter([=](bool visible) {\n\t\treturn visible != _nextShown;\n\t}) | rpl::on_next([=](bool visible) {\n\t\t_next->toggle(visible, anim::type::normal);\n\t\t_nextShown = visible;\n\t\tif (_changeLanguage) {\n\t\t\t_changeLanguage->toggle(\n\t\t\t\t!_resetAccount && !_terms && _nextShown,\n\t\t\t\tanim::type::normal);\n\t\t}\n\t\t_nextShownAnimation.start(\n\t\t\t[=] { updateControlsGeometry(); },\n\t\t\t_nextShown ? 0. : 1.,\n\t\t\t_nextShown ? 1. : 0.,\n\t\t\tst::slideDuration);\n\t}, _stepLifetime);\n}\n\nvoid Widget::hideControls() {\n\tgetStep()->hide();\n\t_next->hide(anim::type::instant);\n\t_connecting->setForceHidden(true);\n\t_settings->hide(anim::type::instant);\n\tif (_testModeLabel) _testModeLabel->hide(anim::type::instant);\n\tif (_update) _update->hide(anim::type::instant);\n\tif (_changeLanguage) _changeLanguage->hide(anim::type::instant);\n\tif (_terms) _terms->hide(anim::type::instant);\n\t_back->hide(anim::type::instant);\n}\n\nvoid Widget::showAnimated(QPixmap oldContentCache, bool back) {\n\t_showAnimation = nullptr;\n\n\tshowControls();\n\tfloatPlayerHideAll();\n\tauto newContentCache = Ui::GrabWidget(this);\n\thideControls();\n\tfloatPlayerShowVisible();\n\n\t_showAnimation = std::make_unique<Window::SlideAnimation>();\n\t_showAnimation->setDirection(back\n\t\t? Window::SlideDirection::FromLeft\n\t\t: Window::SlideDirection::FromRight);\n\t_showAnimation->setRepaintCallback([=] { update(); });\n\t_showAnimation->setFinishedCallback([=] { showFinished(); });\n\t_showAnimation->setPixmaps(oldContentCache, newContentCache);\n\t_showAnimation->start();\n\n\tshow();\n}\n\nvoid Widget::showFinished() {\n\t_showAnimation = nullptr;\n\n\tshowControls();\n\tgetStep()->activate();\n}\n\nvoid Widget::paintEvent(QPaintEvent *e) {\n\tconst auto trivial = (rect() == e->rect());\n\tsetMouseTracking(true);\n\n\tQPainter p(this);\n\tif (!trivial) {\n\t\tp.setClipRect(e->rect());\n\t}\n\tif (_showAnimation) {\n\t\t_showAnimation->paintContents(p);\n\t\treturn;\n\t}\n\tp.fillRect(e->rect(), st::windowBg);\n}\n\nvoid Widget::resizeEvent(QResizeEvent *e) {\n\tif (_stepHistory.empty()) {\n\t\treturn;\n\t}\n\tfor (const auto step : _stepHistory) {\n\t\tstep->setGeometry(rect());\n\t}\n\n\tupdateControlsGeometry();\n\tfloatPlayerAreaUpdated();\n}\n\nvoid Widget::updateControlsGeometry() {\n\tconst auto skip = st::introSettingsSkip;\n\tconst auto shown = _coverShownAnimation.value(1.);\n\n\tconst auto controlsTop = anim::interpolate(\n\t\t_controlsTopFrom,\n\t\tgetStep()->hasCover() ? st::introCoverHeight : 0,\n\t\tshown);\n\t_settings->moveToRight(skip, controlsTop + skip);\n\tif (_testModeLabel) {\n\t\t_testModeLabel->moveToRight(\n\t\t\tskip + _settings->width() + skip,\n\t\t\t_settings->y()\n\t\t\t\t+ (_settings->height()\n\t\t\t\t- _testModeLabel->height()) / 2);\n\t}\n\tif (_update) {\n\t\t_update->moveToRight(\n\t\t\tskip + _settings->width() + skip,\n\t\t\t_settings->y());\n\t}\n\t_back->moveToLeft(0, controlsTop);\n\n\tauto nextTopTo = getStep()->contentTop() + st::introNextTop;\n\tauto nextTop = anim::interpolate(_nextTopFrom, nextTopTo, shown);\n\tconst auto shownAmount = _nextShownAnimation.value(_nextShown ? 1. : 0.);\n\tconst auto realNextTop = anim::interpolate(\n\t\tnextTop + st::introNextSlide,\n\t\tnextTop,\n\t\tshownAmount);\n\t_next->moveToLeft((width() - _next->width()) / 2, realNextTop);\n\tgetStep()->setShowAnimationClipping(shownAmount > 0\n\t\t? QRect(0, 0, width(), realNextTop)\n\t\t: QRect());\n\tif (_changeLanguage) {\n\t\t_changeLanguage->moveToLeft(\n\t\t\t(width() - _changeLanguage->width()) / 2,\n\t\t\t_next->y() + _next->height() + _changeLanguage->height());\n\t}\n\tif (_resetAccount) {\n\t\t_resetAccount->moveToLeft(\n\t\t\t(width() - _resetAccount->width()) / 2,\n\t\t\theight() - st::introResetBottom - _resetAccount->height());\n\t}\n\tif (_terms) {\n\t\t_terms->moveToLeft(\n\t\t\t(width() - _terms->width()) / 2,\n\t\t\theight() - st::introTermsBottom - _terms->height());\n\t}\n}\n\nvoid Widget::keyPressEvent(QKeyEvent *e) {\n\tif (_showAnimation || getStep()->animating()) return;\n\n\tif (e->key() == Qt::Key_Escape || e->key() == Qt::Key_Back) {\n\t\tif (getStep()->hasBack()) {\n\t\t\tbackRequested();\n\t\t}\n\t} else if (e->key() == Qt::Key_Enter\n\t\t|| e->key() == Qt::Key_Return\n\t\t|| e->key() == Qt::Key_Space) {\n\t\tgetStep()->submit();\n\t}\n}\n\nvoid Widget::backRequested() {\n\tif (_stepHistory.size() > 1) {\n\t\thistoryMove(StackAction::Back, Animate::Back);\n\t} else if (const auto parent\n\t\t= Core::App().domain().maybeLastOrSomeAuthedAccount()) {\n\t\tCore::App().domain().activate(parent);\n\t} else {\n\t\tmoveToStep(\n\t\t\tUi::CreateChild<StartWidget>(this, _account, getData()),\n\t\t\tStackAction::Replace,\n\t\t\tAnimate::Back);\n\t}\n}\n\nWidget::~Widget() {\n\tfor (auto step : base::take(_stepHistory)) {\n\t\tdelete step;\n\t}\n}\n\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/intro/intro_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n#include \"window/window_lock_widgets.h\"\n#include \"core/core_cloud_password.h\"\n#include \"media/player/media_player_float.h\"\n\nnamespace Main {\nclass Account;\n} // namespace Main\n\nnamespace Ui {\nclass IconButton;\nclass RoundButton;\nclass LinkButton;\nclass FlatLabel;\ntemplate <typename Widget>\nclass FadeWrap;\n} // namespace Ui\n\nnamespace Window {\nclass ConnectionState;\nclass Controller;\nclass SlideAnimation;\n} // namespace Window\n\nnamespace Intro {\nnamespace details {\n\nenum class CallStatus {\n\tWaiting,\n\tCalling,\n\tCalled,\n\tDisabled,\n};\n\nenum class EmailStatus {\n\tNone,\n\tSetupRequired,\n};\n\nstruct Data {\n\t// Required for the UserpicButton.\n\tconst not_null<Window::Controller*> controller;\n\n\tQString country;\n\tQString phone;\n\tQByteArray phoneHash;\n\n\tCallStatus callStatus = CallStatus::Disabled;\n\tint callTimeout = 0;\n\n\tint codeLength = 5;\n\tbool codeByTelegram = false;\n\tQString codeByFragmentUrl;\n\n\tEmailStatus emailStatus = EmailStatus::None;\n\tQString email;\n\tQString emailPatternSetup;\n\tQString emailPatternLogin;\n\n\tCore::CloudPasswordState pwdState;\n\n\tWindow::TermsLock termsLock;\n\n\trpl::event_stream<> updated;\n\n};\n\nenum class StackAction {\n\tBack,\n\tForward,\n\tReplace,\n};\n\nenum class Animate {\n\tBack,\n\tForward,\n};\n\nclass Step;\n\n} // namespace details\n\nenum class EnterPoint : uchar {\n\tStart,\n\tPhone,\n\tQr,\n};\n\nclass Widget\n\t: public Ui::RpWidget\n\t, private Media::Player::FloatDelegate\n\t, private Media::Player::FloatSectionDelegate {\npublic:\n\tWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::Controller*> controller,\n\t\tnot_null<Main::Account*> account,\n\t\tEnterPoint point);\n\t~Widget();\n\n\tvoid showAnimated(QPixmap oldContentCache, bool back = false);\n\n\tvoid setInnerFocus();\n\n\t[[nodiscard]] rpl::producer<> showSettingsRequested() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tvoid setupStep();\n\tvoid refreshLang();\n\tvoid showFinished();\n\tvoid createLanguageLink();\n\tvoid checkUpdateStatus();\n\tvoid setupNextButton();\n\tvoid handleUpdates(const MTPUpdates &updates);\n\tvoid handleUpdate(const MTPUpdate &update);\n\tvoid backRequested();\n\n\tvoid updateControlsGeometry();\n\t[[nodiscard]] not_null<details::Data*> getData() {\n\t\treturn &_data;\n\t}\n\n\tvoid fixOrder();\n\tvoid showControls();\n\tvoid hideControls();\n\n\tvoid showResetButton();\n\tvoid resetAccount();\n\n\tvoid showTerms();\n\tvoid acceptTerms(Fn<void()> callback);\n\tvoid hideAndDestroy(object_ptr<Ui::FadeWrap<Ui::RpWidget>> widget);\n\n\t[[nodiscard]] details::Step *getStep(int skip = 0) const {\n\t\tExpects(skip >= 0);\n\t\tExpects(skip < _stepHistory.size());\n\n\t\treturn _stepHistory[_stepHistory.size() - skip - 1];\n\t}\n\tvoid historyMove(details::StackAction action, details::Animate animate);\n\tvoid moveToStep(\n\t\tdetails::Step *step,\n\t\tdetails::StackAction action,\n\t\tdetails::Animate animate);\n\tvoid appendStep(details::Step *step);\n\n\tvoid getNearestDC();\n\tvoid showTerms(Fn<void()> callback);\n\n\t// FloatDelegate\n\t[[nodiscard]] auto floatPlayerDelegate()\n\t\t-> not_null<Media::Player::FloatDelegate*>;\n\t[[nodiscard]] auto floatPlayerSectionDelegate()\n\t\t-> not_null<Media::Player::FloatSectionDelegate*>;\n\tnot_null<Ui::RpWidget*> floatPlayerWidget() override;\n\tvoid floatPlayerToggleGifsPaused(bool paused) override;\n\tnot_null<Media::Player::FloatSectionDelegate*> floatPlayerGetSection(\n\t\tWindow::Column column) override;\n\tvoid floatPlayerEnumerateSections(Fn<void(\n\t\tnot_null<Media::Player::FloatSectionDelegate*> widget,\n\t\tWindow::Column widgetColumn)> callback) override;\n\tbool floatPlayerIsVisible(not_null<HistoryItem*> item) override;\n\tvoid floatPlayerDoubleClickEvent(\n\t\tnot_null<const HistoryItem*> item) override;\n\n\t// FloatSectionDelegate\n\tQRect floatPlayerAvailableRect() override;\n\tbool floatPlayerHandleWheelEvent(QEvent *e) override;\n\n\tconst not_null<Main::Account*> _account;\n\tstd::optional<MTP::Sender> _api;\n\tmtpRequestId _nearestDcRequestId = 0;\n\n\tstd::unique_ptr<Window::SlideAnimation> _showAnimation;\n\n\tstd::vector<details::Step*> _stepHistory;\n\trpl::lifetime _stepLifetime;\n\n\tdetails::Data _data;\n\n\tUi::Animations::Simple _coverShownAnimation;\n\tint _nextTopFrom = 0;\n\tint _controlsTopFrom = 0;\n\n\tconst style::RoundButton *_nextStyle = nullptr;\n\n\tobject_ptr<Ui::FadeWrap<Ui::IconButton>> _back;\n\tobject_ptr<Ui::FadeWrap<Ui::RoundButton>> _update = { nullptr };\n\tobject_ptr<Ui::FadeWrap<Ui::RoundButton>> _settings;\n\tobject_ptr<Ui::FadeWrap<Ui::FlatLabel>> _testModeLabel = { nullptr };\n\n\tobject_ptr<Ui::FadeWrap<Ui::RoundButton>> _next;\n\tobject_ptr<Ui::FadeWrap<Ui::LinkButton>> _changeLanguage = { nullptr };\n\tobject_ptr<Ui::FadeWrap<Ui::RoundButton>> _resetAccount = { nullptr };\n\tobject_ptr<Ui::FadeWrap<Ui::FlatLabel>> _terms = { nullptr };\n\n\tstd::unique_ptr<Window::ConnectionState> _connecting;\n\n\tbool _nextShown = true;\n\tUi::Animations::Simple _nextShownAnimation;\n\n\tmtpRequestId _resetRequest = 0;\n\n};\n\n} // namespace Intro\n"
  },
  {
    "path": "Telegram/SourceFiles/iv/iv.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\nusing \"ui/widgets/widgets.style\";\n\nivMenuToggle: IconButton(defaultIconButton) {\n\twidth: 48px;\n\theight: 48px;\n\n\ticon: icon {{ \"title_menu_dots\", menuIconColor }};\n\ticonOver: icon {{ \"title_menu_dots\", menuIconColor }};\n\n\trippleAreaPosition: point(6px, 6px);\n\trippleAreaSize: 36px;\n\tripple: defaultRippleAnimationBgOver;\n}\nivMenuPosition: point(-2px, 40px);\nivBackIcon: icon {{ \"box_button_back\", menuIconColor }};\nivBack: IconButton(ivMenuToggle) {\n\twidth: 60px;\n\ticon: ivBackIcon;\n\ticonOver: ivBackIcon;\n\trippleAreaPosition: point(12px, 6px);\n}\nivZoomButtonsSize: 26px;\nivPlusMinusZoom: IconButton(ivMenuToggle) {\n\twidth: ivZoomButtonsSize;\n\theight: ivZoomButtonsSize;\n\n\trippleAreaPosition: point(0px, 0px);\n\trippleAreaSize: ivZoomButtonsSize;\n\tripple: defaultRippleAnimationBgOver;\n}\nivResetZoomStyle: TextStyle(defaultTextStyle) {\n\tfont: font(12px);\n}\nivResetZoom: RoundButton(defaultActiveButton) {\n\ttextFg: windowFg;\n\ttextFgOver: windowFgOver;\n\ttextBg: windowBg;\n\ttextBgOver: windowBgOver;\n\n\theight: ivZoomButtonsSize;\n\tpadding: margins(0px, 0px, 0px, 0px);\n\n\tstyle: ivResetZoomStyle;\n\n\tripple: defaultRippleAnimation;\n}\nivResetZoomLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowFg;\n\tstyle: ivResetZoomStyle;\n}\nivResetZoomInnerPadding: 20px;\nivBackIconDisabled: icon {{ \"box_button_back\", menuIconFg }};\nivForwardIcon: icon {{ \"box_button_back-flip_horizontal\", menuIconColor }};\nivForward: IconButton(ivBack) {\n\twidth: 48px;\n\ticon: ivForwardIcon;\n\ticonOver: ivForwardIcon;\n\trippleAreaPosition: point(0px, 6px);\n}\nivSubtitleFont: font(16px semibold);\nivSubtitle: FlatLabel(defaultFlatLabel) {\n\ttextFg: boxTitleFg;\n\tmaxHeight: 26px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: ivSubtitleFont;\n\t}\n}\nivSubtitleHeight: 48px;\nivSubtitleTop: 12px;\nivSubtitleLeft: 22px;\nivSubtitleSkip: 0px;\n"
  },
  {
    "path": "Telegram/SourceFiles/iv/iv_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"iv/iv_controller.h\"\n\n#include \"base/event_filter.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"base/invoke_queued.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"base/qthelp_url.h\"\n#include \"core/file_utilities.h\"\n#include \"iv/iv_data.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/chat/attach/attach_bot_webview.h\"\n#include \"ui/platform/ui_platform_window_title.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/rp_window.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/webview_helpers.h\"\n#include \"ui/ui_utility.h\"\n#include \"webview/webview_data_stream_memory.h\"\n#include \"webview/webview_embed.h\"\n#include \"webview/webview_interface.h\"\n#include \"styles/palette.h\"\n#include \"styles/style_iv.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_payments.h\" // paymentsCriticalError\n#include \"styles/style_widgets.h\"\n#include \"styles/style_window.h\"\n\n#include <QtCore/QRegularExpression>\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonObject>\n#include <QtCore/QJsonValue>\n#include <QtCore/QFile>\n#include <QtGui/QGuiApplication>\n#include <QtGui/QPainter>\n#include <QtGui/QWindow>\n#include <charconv>\n\n#include <ada.h>\n\nnamespace Iv {\nnamespace {\n\nconstexpr auto kZoomStep = int(10);\nconstexpr auto kZoomSmallStep = int(5);\nconstexpr auto kZoomTinyStep = int(1);\nconstexpr auto kDefaultZoom = int(100);\n\nclass ItemZoom final\n\t: public Ui::Menu::Action\n\t, public Ui::AbstractTooltipShower {\npublic:\n\tItemZoom(\n\t\tnot_null<Ui::PopupMenu*> parent,\n\t\tconst not_null<Delegate*> delegate,\n\t\tconst style::Menu &st)\n\t: Ui::Menu::Action(\n\t\tparent->menu(),\n\t\tst,\n\t\tUi::CreateChild<QAction>(parent),\n\t\tnullptr,\n\t\tnullptr)\n\t, _delegate(delegate)\n\t, _st(st) {\n\t\tinit();\n\t}\n\n\tvoid init() {\n\t\tenableMouseSelecting();\n\n\t\tAbstractButton::setDisabled(true);\n\n\t\tconst auto processTooltip = [=](not_null<Ui::RpWidget*> w) {\n\t\t\tw->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\t\t\tif (e->type() == QEvent::Enter) {\n\t\t\t\t\tUi::Tooltip::Show(1000, this);\n\t\t\t\t} else if (e->type() == QEvent::Leave) {\n\t\t\t\t\tUi::Tooltip::Hide();\n\t\t\t\t}\n\t\t\t}, w->lifetime());\n\t\t};\n\n\t\tconst auto reset = Ui::CreateChild<Ui::RoundButton>(\n\t\t\tthis,\n\t\t\trpl::single<QString>(QString()),\n\t\t\tst::ivResetZoom);\n\t\tprocessTooltip(reset);\n\t\tconst auto resetLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\treset,\n\t\t\ttr::lng_background_reset_default(),\n\t\t\tst::ivResetZoomLabel);\n\t\tresetLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\treset->setClickedCallback([this] {\n\t\t\t_delegate->ivSetZoom(0);\n\t\t});\n\t\treset->show();\n\t\tconst auto plus = Ui::CreateSimpleCircleButton(\n\t\t\tthis,\n\t\t\tst::defaultRippleAnimationBgOver);\n\t\tplus->resize(Size(st::ivZoomButtonsSize));\n\t\tplus->paintRequest() | rpl::on_next([=, fg = _st.itemFg] {\n\t\t\tauto p = QPainter(plus);\n\t\t\tp.setPen(fg);\n\t\t\tp.setFont(st::normalFont);\n\t\t\tp.drawText(plus->rect(), QChar('+'), style::al_center);\n\t\t}, plus->lifetime());\n\t\tprocessTooltip(plus);\n\t\tconst auto step = [] {\n\t\t\treturn base::IsAltPressed()\n\t\t\t\t? kZoomTinyStep\n\t\t\t\t: base::IsCtrlPressed()\n\t\t\t\t? kZoomSmallStep\n\t\t\t\t: kZoomStep;\n\t\t};\n\t\tplus->setClickedCallback([this, step] {\n\t\t\t_delegate->ivSetZoom(_delegate->ivZoom() + step());\n\t\t});\n\t\tplus->show();\n\t\tconst auto minus = Ui::CreateSimpleCircleButton(\n\t\t\tthis,\n\t\t\tst::defaultRippleAnimationBgOver);\n\t\tminus->resize(Size(st::ivZoomButtonsSize));\n\t\tminus->paintRequest() | rpl::on_next([=, fg = _st.itemFg] {\n\t\t\tauto p = QPainter(minus);\n\t\t\tconst auto r = minus->rect();\n\t\t\tp.setPen(fg);\n\t\t\tp.setFont(st::normalFont);\n\t\t\tp.drawText(\n\t\t\t\tQRectF(r).translated(0, style::ConvertFloatScale(-1)),\n\t\t\t\tQChar(0x2013),\n\t\t\t\tstyle::al_center);\n\t\t}, minus->lifetime());\n\t\tprocessTooltip(minus);\n\t\tminus->setClickedCallback([this, step] {\n\t\t\t_delegate->ivSetZoom(_delegate->ivZoom() - step());\n\t\t});\n\t\tminus->show();\n\n\t\t{\n\t\t\tconst auto maxWidthText = u\"000%\"_q;\n\t\t\t_text.setText(_st.itemStyle, maxWidthText);\n\t\t\tUi::Menu::ItemBase::setMinWidth(\n\t\t\t\t_text.maxWidth()\n\t\t\t\t\t+ st::ivResetZoomInnerPadding\n\t\t\t\t\t+ resetLabel->width()\n\t\t\t\t\t+ plus->width()\n\t\t\t\t\t+ minus->width()\n\t\t\t\t\t+ _st.itemPadding.right() * 2);\n\t\t}\n\n\t\t_delegate->ivZoomValue(\n\t\t) | rpl::on_next([this](int value) {\n\t\t\t_text.setText(_st.itemStyle, QString::number(value) + '%');\n\t\t\tupdate();\n\t\t}, lifetime());\n\n\t\trpl::combine(\n\t\t\tsizeValue(),\n\t\t\treset->sizeValue()\n\t\t) | rpl::on_next([=](const QSize &size, const QSize &) {\n\t\t\treset->setFullWidth(0\n\t\t\t\t+ resetLabel->width()\n\t\t\t\t+ st::ivResetZoomInnerPadding);\n\t\t\tresetLabel->moveToLeft(\n\t\t\t\t(reset->width() - resetLabel->width()) / 2,\n\t\t\t\t(reset->height() - resetLabel->height()) / 2);\n\t\t\treset->moveToRight(\n\t\t\t\t_st.itemPadding.right(),\n\t\t\t\t(size.height() - reset->height()) / 2);\n\t\t\tplus->moveToRight(\n\t\t\t\t_st.itemPadding.right() + reset->width(),\n\t\t\t\t(size.height() - plus->height()) / 2);\n\t\t\tminus->moveToRight(\n\t\t\t\t_st.itemPadding.right() + plus->width() + reset->width(),\n\t\t\t\t(size.height() - minus->height()) / 2);\n\t\t}, lifetime());\n\t}\n\n\tvoid paintEvent(QPaintEvent *event) override {\n\t\tauto p = QPainter(this);\n\t\tp.setPen(_st.itemFg);\n\t\t_text.draw(p, {\n\t\t\t.position = QPoint(\n\t\t\t\t_st.itemIconPosition.x(),\n\t\t\t\t(height() - _text.minHeight()) / 2),\n\t\t\t.outerWidth = width(),\n\t\t\t.availableWidth = width(),\n\t\t});\n\t}\n\n\tQString tooltipText() const override {\n#ifdef Q_OS_MAC\n\t\treturn tr::lng_iv_zoom_tooltip_cmd(tr::now);\n#else\n\t\treturn tr::lng_iv_zoom_tooltip_ctrl(tr::now);\n#endif\n\t}\n\n\tQPoint tooltipPos() const override {\n\t\treturn QCursor::pos();\n\t}\n\n\tbool tooltipWindowActive() const override {\n\t\treturn true;\n\t}\n\nprivate:\n\tconst not_null<Delegate*> _delegate;\n\tconst style::Menu &_st;\n\tUi::Text::String _text;\n\n};\n\n[[nodiscard]] QByteArray ComputeStyles(int zoom) {\n\tstatic const auto map = base::flat_map<QByteArray, const style::color*>{\n\t\t{ \"shadow-fg\", &st::shadowFg },\n\t\t{ \"scroll-bg\", &st::scrollBg },\n\t\t{ \"scroll-bg-over\", &st::scrollBgOver },\n\t\t{ \"scroll-bar-bg\", &st::scrollBarBg },\n\t\t{ \"scroll-bar-bg-over\", &st::scrollBarBgOver },\n\t\t{ \"window-bg\", &st::windowBg },\n\t\t{ \"window-bg-over\", &st::windowBgOver },\n\t\t{ \"window-bg-ripple\", &st::windowBgRipple },\n\t\t{ \"window-bg-active\", &st::windowBgActive },\n\t\t{ \"window-fg\", &st::windowFg },\n\t\t{ \"window-sub-text-fg\", &st::windowSubTextFg },\n\t\t{ \"window-active-text-fg\", &st::windowActiveTextFg },\n\t\t{ \"window-shadow-fg\", &st::windowShadowFg },\n\t\t{ \"box-divider-bg\", &st::boxDividerBg },\n\t\t{ \"box-divider-fg\", &st::boxDividerFg },\n\t\t{ \"light-button-fg\", &st::lightButtonFg },\n\t\t//{ \"light-button-bg-over\", &st::lightButtonBgOver },\n\t\t{ \"menu-icon-fg\", &st::menuIconFg },\n\t\t{ \"menu-icon-fg-over\", &st::menuIconFgOver },\n\t\t{ \"menu-bg\", &st::menuBg },\n\t\t{ \"menu-bg-over\", &st::menuBgOver },\n\t\t{ \"history-to-down-fg\", &st::historyToDownFg },\n\t\t{ \"history-to-down-fg-over\", &st::historyToDownFgOver },\n\t\t{ \"history-to-down-bg\", &st::historyToDownBg },\n\t\t{ \"history-to-down-bg-over\", &st::historyToDownBgOver },\n\t\t{ \"history-to-down-bg-ripple\", &st::historyToDownBgRipple },\n\t\t{ \"history-to-down-shadow\", &st::historyToDownShadow },\n\t\t{ \"toast-bg\", &st::toastBg },\n\t\t{ \"toast-fg\", &st::toastFg },\n\t};\n\tstatic const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{\n\t\t{ \"iv-join-channel\", tr::lng_iv_join_channel },\n\t};\n\treturn Ui::ComputeStyles(map, phrases, zoom)\n\t\t+ ';'\n\t\t+ Ui::ComputeSemiTransparentOverStyle(\n\t\t\t\"light-button-bg-over\",\n\t\t\tst::lightButtonBgOver,\n\t\t\tst::windowBg);\n}\n\n[[nodiscard]] QByteArray WrapPage(const Prepared &page, int zoom) {\n#ifdef Q_OS_MAC\n\tconst auto classAttribute = \"\"_q;\n#else // Q_OS_MAC\n\tconst auto classAttribute = \" class=\\\"custom_scroll\\\"\"_q;\n#endif // Q_OS_MAC\n\n\tconst auto js = QByteArray()\n\t\t+ (page.hasCode ? \"IV.initPreBlocks();\" : \"\")\n\t\t+ (page.hasEmbeds ? \"IV.initEmbedBlocks();\" : \"\")\n\t\t+ \"IV.init();\"\n\t\t+ page.script;\n\n\treturn R\"(<!DOCTYPE html>\n<html)\"_q\n\t+ classAttribute\n\t+ R\"( style=\")\"\n\t+ Ui::EscapeForAttribute(ComputeStyles(zoom))\n\t+ R\"(\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"robots\" content=\"noindex, nofollow\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t\t<script src=\"/iv/page.js\"></script>\n\t\t<link rel=\"stylesheet\" href=\"/iv/page.css\" />\n\t</head>\n\t<body>\n\t\t<div id=\"top_shadow\"></div>\n\t\t<button class=\"fixed_button hidden\" id=\"bottom_up\" onclick=\"IV.scrollTo(0);\">\n\t\t\t<svg viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n\t\t\t\t<path d=\"M14.9972363,18 L9.13865768,12.1414214 C9.06055283,12.0633165 9.06055283,11.9366835 9.13865768,11.8585786 L14.9972363,6 L14.9972363,6\" transform=\"translate(11.997236, 12.000000) scale(-1, -1) rotate(-90.000000) translate(-11.997236, -12.000000) \"></path>\n\t\t\t</svg>\n\t\t</button>\n\t\t<div class=\"page-scroll\" tabindex=\"-1\">)\"_q + page.content.trimmed() + R\"(</div>\n\t\t<script>)\"_q + js + R\"(</script>\n\t</body>\n</html>\n)\"_q;\n}\n\n[[nodiscard]] QByteArray ReadResource(const QString &name) {\n\tauto file = QFile(u\":/iv/\"_q + name);\n\treturn file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray();\n}\n\n[[nodiscard]] QString TonsiteToHttps(QString value) {\n\tconst auto ChangeHost = [](QString tonsite) {\n\t\tconst auto fake = \"http://\" + tonsite.toStdString();\n\t\tconst auto parsed = ada::parse<ada::url>(fake);\n\t\tif (!parsed) {\n\t\t\treturn QString();\n\t\t}\n\t\ttonsite = QString::fromStdString(parsed->get_hostname());\n\t\ttonsite = tonsite.replace('-', \"-h\");\n\t\ttonsite = tonsite.replace('.', \"-d\");\n\t\treturn tonsite + \".magic.org\";\n\t};\n\tconst auto prefix = u\"tonsite://\"_q;\n\tif (!value.toLower().startsWith(prefix)) {\n\t\treturn QString();\n\t}\n\tconst auto part = value.mid(prefix.size());\n\tconst auto split = part.indexOf('/');\n\tconst auto host = ChangeHost((split < 0) ? part : part.left(split));\n\tif (host.isEmpty()) {\n\t\treturn QString();\n\t}\n\treturn \"https://\" + host + ((split < 0) ? u\"/\"_q : part.mid(split));\n}\n\n[[nodiscard]] QString HttpsToTonsite(QString value) {\n\tconst auto ChangeHost = [](QString https) {\n\t\tconst auto dot = https.indexOf('.');\n\t\tif (dot < 0 || https.mid(dot).toLower() != u\".magic.org\"_q) {\n\t\t\treturn QString();\n\t\t}\n\t\thttps = https.mid(0, dot);\n\t\thttps = https.replace(\"-d\", \".\");\n\t\thttps = https.replace(\"-h\", \"-\");\n\t\tauto parts = https.split('.');\n\t\tfor (auto &part : parts) {\n\t\t\tif (part.startsWith(u\"xn--\"_q)) {\n\t\t\t\tconst auto utf8 = part.mid(4).toStdString();\n\t\t\t\tauto out = std::u32string();\n\t\t\t\tif (ada::idna::punycode_to_utf32(utf8, out)) {\n\t\t\t\t\tpart = QString::fromUcs4(out.data(), out.size());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn parts.join('.');\n\t};\n\tconst auto prefix = u\"https://\"_q;\n\tif (!value.toLower().startsWith(prefix)) {\n\t\treturn value;\n\t}\n\tconst auto part = value.mid(prefix.size());\n\tconst auto split = part.indexOf('/');\n\tconst auto host = ChangeHost((split < 0) ? part : part.left(split));\n\tif (host.isEmpty()) {\n\t\treturn value;\n\t}\n\treturn \"tonsite://\"\n\t\t+ host\n\t\t+ ((split < 0) ? u\"/\"_q : part.mid(split));\n}\n\n} // namespace\n\nController::Controller(\n\tnot_null<Delegate*> delegate,\n\tFn<ShareBoxResult(ShareBoxDescriptor)> showShareBox)\n: _delegate(delegate)\n, _updateStyles([=] {\n\tif (_webview) {\n\t\tconst auto webviewZoomController = _webview->zoomController();\n\t\tconst auto styleZoom = webviewZoomController\n\t\t\t? kDefaultZoom\n\t\t\t: _delegate->ivZoom();\n\t\tconst auto str = Ui::EscapeForScriptString(ComputeStyles(styleZoom));\n\t\t_webview->eval(\"IV.updateStyles('\" + str + \"');\");\n\t\tif (webviewZoomController) {\n\t\t\twebviewZoomController->setZoom(_delegate->ivZoom());\n\t\t}\n\t}\n})\n, _showShareBox(std::move(showShareBox)) {\n\tcreateWindow();\n}\n\nController::~Controller() {\n\tdestroyShareMenu();\n\tif (_window) {\n\t\t_window->hide();\n\t}\n\t_ready = false;\n\tbase::take(_webview);\n\t_back.destroy();\n\t_forward.destroy();\n\t_menu = nullptr;\n\t_menuToggle.destroy();\n\t_subtitle = nullptr;\n\t_subtitleWrap = nullptr;\n\t_window = nullptr;\n}\n\nvoid Controller::updateTitleGeometry(int newWidth) const {\n\t_subtitleWrap->setGeometry(\n\t\t0,\n\t\t0,\n\t\tnewWidth,\n\t\tst::ivSubtitleHeight);\n\t_subtitleWrap->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tQPainter(_subtitleWrap.get()).fillRect(clip, st::windowBg);\n\t}, _subtitleWrap->lifetime());\n\n\tconst auto progressBack = _subtitleBackShift.value(\n\t\t_back->toggled() ? 1. : 0.);\n\tconst auto progressForward = _subtitleForwardShift.value(\n\t\t_forward->toggled() ? 1. : 0.);\n\tconst auto backAdded = _back->width()\n\t\t+ st::ivSubtitleSkip\n\t\t- st::ivSubtitleLeft;\n\tconst auto forwardAdded = _forward->width();\n\tconst auto left = st::ivSubtitleLeft\n\t\t+ anim::interpolate(0, backAdded, progressBack)\n\t\t+ anim::interpolate(0, forwardAdded, progressForward);\n\t_subtitle->resizeToWidth(newWidth - left - _menuToggle->width());\n\t_subtitle->moveToLeft(left, st::ivSubtitleTop);\n\n\t_back->moveToLeft(0, 0);\n\t_forward->moveToLeft(_back->width() * progressBack, 0);\n\t_menuToggle->moveToRight(0, 0);\n}\n\nvoid Controller::initControls() {\n\t_subtitleWrap = std::make_unique<Ui::RpWidget>(_window->body().get());\n\t_subtitleText = _index.value() | rpl::filter(\n\t\trpl::mappers::_1 >= 0\n\t) | rpl::map([=](int index) {\n\t\treturn _pages[index].name;\n\t});\n\t_subtitle = std::make_unique<Ui::FlatLabel>(\n\t\t_subtitleWrap.get(),\n\t\t_subtitleText.value(),\n\t\tst::ivSubtitle);\n\t_subtitle->setSelectable(true);\n\n\t_windowTitleText = _subtitleText.value(\n\t) | rpl::map([=](const QString &subtitle) {\n\t\tconst auto prefix = tr::lng_iv_window_title(tr::now);\n\t\treturn prefix + ' ' + QChar(0x2014) + ' ' + subtitle;\n\t});\n\t_windowTitleText.value(\n\t) | rpl::on_next([=](const QString &title) {\n\t\t_window->setWindowTitle(title);\n\t}, _subtitle->lifetime());\n\n\t_menuToggle.create(_subtitleWrap.get(), st::ivMenuToggle);\n\t_menuToggle->setClickedCallback([=] { showMenu(); });\n\n\t_back.create(\n\t\t_subtitleWrap.get(),\n\t\tobject_ptr<Ui::IconButton>(_subtitleWrap.get(), st::ivBack));\n\t_back->entity()->setClickedCallback([=] {\n\t\tif (_webview) {\n\t\t\t_webview->eval(\"window.history.back();\");\n\t\t} else {\n\t\t\t_back->hide(anim::type::normal);\n\t\t}\n\t});\n\t_forward.create(\n\t\t_subtitleWrap.get(),\n\t\tobject_ptr<Ui::IconButton>(_subtitleWrap.get(), st::ivForward));\n\t_forward->entity()->setClickedCallback([=] {\n\t\tif (_webview) {\n\t\t\t_webview->eval(\"window.history.forward();\");\n\t\t} else {\n\t\t\t_forward->hide(anim::type::normal);\n\t\t}\n\t});\n\n\t_back->toggledValue(\n\t) | rpl::on_next([=](bool toggled) {\n\t\t_subtitleBackShift.start(\n\t\t\t[=] { updateTitleGeometry(_window->body()->width()); },\n\t\t\ttoggled ? 0. : 1.,\n\t\t\ttoggled ? 1. : 0.,\n\t\t\tst::fadeWrapDuration);\n\t}, _back->lifetime());\n\t_back->hide(anim::type::instant);\n\n\t_forward->toggledValue(\n\t) | rpl::on_next([=](bool toggled) {\n\t\t_subtitleForwardShift.start(\n\t\t\t[=] { updateTitleGeometry(_window->body()->width()); },\n\t\t\ttoggled ? 0. : 1.,\n\t\t\ttoggled ? 1. : 0.,\n\t\t\tst::fadeWrapDuration);\n\t}, _forward->lifetime());\n\t_forward->hide(anim::type::instant);\n\n\t_subtitleBackShift.stop();\n\t_subtitleForwardShift.stop();\n}\n\nvoid Controller::show(\n\t\tconst Webview::StorageId &storageId,\n\t\tPrepared page,\n\t\tbase::flat_map<QByteArray, rpl::producer<bool>> inChannelValues) {\n\tpage.script = fillInChannelValuesScript(std::move(inChannelValues));\n\tInvokeQueued(_container, [=, page = std::move(page)]() mutable {\n\t\tshowInWindow(storageId, std::move(page));\n\t});\n}\n\nvoid Controller::update(Prepared page) {\n\tconst auto url = page.url;\n\tauto i = _indices.find(url);\n\tif (i == end(_indices)) {\n\t\treturn;\n\t}\n\tconst auto index = i->second;\n\t_pages[index] = std::move(page);\n\n\tif (_ready) {\n\t\t_webview->eval(reloadScript(index));\n\t} else if (!index) {\n\t\t_reloadInitialWhenReady = true;\n\t}\n}\n\nbool Controller::IsGoodTonSiteUrl(const QString &uri) {\n\treturn !TonsiteToHttps(uri).isEmpty();\n}\n\nvoid Controller::showTonSite(\n\t\tconst Webview::StorageId &storageId,\n\t\tQString uri) {\n\tconst auto url = TonsiteToHttps(uri);\n\tAssert(!url.isEmpty());\n\n\tif (!_webview) {\n\t\tcreateWebview(storageId);\n\t}\n\tif (_webview && _webview->widget()) {\n\t\t_webview->navigate(url);\n\t\tactivate();\n\t}\n\t_url = url;\n\t_subtitleText = _url.value(\n\t) | rpl::filter([=](const QString &url) {\n\t\treturn !url.isEmpty() && url != u\"about:blank\"_q;\n\t}) | rpl::map([=](QString value) {\n\t\treturn HttpsToTonsite(value);\n\t});\n\t_windowTitleText = _subtitleText.value();\n\t_menuToggle->hide();\n}\n\nQByteArray Controller::fillInChannelValuesScript(\n\t\tbase::flat_map<QByteArray, rpl::producer<bool>> inChannelValues) {\n\tauto result = QByteArray();\n\tfor (auto &[id, in] : inChannelValues) {\n\t\tif (_inChannelSubscribed.emplace(id).second) {\n\t\t\tstd::move(in) | rpl::on_next([=](bool in) {\n\t\t\t\tif (_ready) {\n\t\t\t\t\t_webview->eval(toggleInChannelScript(id, in));\n\t\t\t\t} else {\n\t\t\t\t\t_inChannelChanged[id] = in;\n\t\t\t\t}\n\t\t\t}, _lifetime);\n\t\t}\n\t}\n\tfor (const auto &[id, in] : base::take(_inChannelChanged)) {\n\t\tresult += toggleInChannelScript(id, in);\n\t}\n\treturn result;\n}\n\nQByteArray Controller::toggleInChannelScript(\n\t\tconst QByteArray &id,\n\t\tbool in) const {\n\tconst auto value = in ? \"true\" : \"false\";\n\treturn \"IV.toggleChannelJoined('\" + id + \"', \" + value + \");\";\n}\n\nvoid Controller::createWindow() {\n\t_window = std::make_unique<Ui::RpWindow>();\n\tconst auto window = _window.get();\n\n\tbase::qt_signal_producer(\n\t\tqApp,\n\t\t&QGuiApplication::focusWindowChanged\n\t) | rpl::filter([=](QWindow *focused) {\n\t\tconst auto handle = window->window()->windowHandle();\n\t\treturn _webview && handle && (focused == handle);\n\t}) | rpl::on_next([=] {\n\t\tsetInnerFocus();\n\t}, window->lifetime());\n\n\tinitControls();\n\n\twindow->body()->widthValue() | rpl::on_next([=](int width) {\n\t\tupdateTitleGeometry(width);\n\t}, _subtitle->lifetime());\n\n\twindow->setGeometry(_delegate->ivGeometry(window));\n\twindow->setMinimumSize({ st::windowMinWidth, st::windowMinHeight });\n\n\twindow->geometryValue(\n\t) | rpl::distinct_until_changed(\n\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\t_delegate->ivSaveGeometry(window);\n\t}, window->lifetime());\n\n\t_container = Ui::CreateChild<Ui::RpWidget>(window->body().get());\n\trpl::combine(\n\t\twindow->body()->sizeValue(),\n\t\t_subtitleWrap->heightValue()\n\t) | rpl::on_next([=](QSize size, int title) {\n\t\t_container->setGeometry(QRect(QPoint(), size).marginsRemoved(\n\t\t\t{ 0, title, 0, 0 }));\n\t}, _container->lifetime());\n\n\t_container->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tQPainter(_container).fillRect(clip, st::windowBg);\n\t}, _container->lifetime());\n\n\t_container->show();\n\twindow->show();\n}\n\nvoid Controller::createWebview(const Webview::StorageId &storageId) {\n\tExpects(!_webview);\n\n\tconst auto window = _window.get();\n\t_webview = std::make_unique<Webview::Window>(\n\t\t_container,\n\t\tWebview::WindowConfig{\n\t\t\t.opaqueBg = st::windowBg->c,\n\t\t\t.storageId = storageId,\n\t\t\t.safe = true,\n\t\t});\n\tconst auto raw = _webview.get();\n\n\tif (const auto webviewZoomController = raw->zoomController()) {\n\t\twebviewZoomController->zoomValue(\n\t\t) | rpl::on_next([this](int value) {\n\t\t\tif (value > 0) {\n\t\t\t\t_delegate->ivSetZoom(value);\n\t\t\t}\n\t\t}, lifetime());\n\t\t_delegate->ivZoomValue(\n\t\t) | rpl::on_next([=](int value) {\n\t\t\twebviewZoomController->setZoom(value);\n\t\t}, lifetime());\n\t}\n\n\twindow->lifetime().add([=] {\n\t\t_ready = false;\n\t\tbase::take(_webview);\n\t});\n\n\twindow->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::Close) {\n\t\t\tclose();\n\t\t} else if (e->type() == QEvent::KeyPress) {\n\t\t\tconst auto event = static_cast<QKeyEvent*>(e.get());\n\t\t\tif (event->key() == Qt::Key_Escape) {\n\t\t\t\tescape();\n\t\t\t}\n\t\t}\n\t}, window->lifetime());\n\n\tbase::install_event_filter(window, qApp, [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::ShortcutOverride) {\n\t\t\tif (!window->isActiveWindow()) {\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t}\n\t\t\tconst auto event = static_cast<QKeyEvent*>(e.get());\n\t\t\tif (event->modifiers() & Qt::ControlModifier) {\n\t\t\t\tif (event->key() == Qt::Key_Plus\n\t\t\t\t\t|| event->key() == Qt::Key_Equal) {\n\t\t\t\t\t_delegate->ivSetZoom(_delegate->ivZoom() + kZoomStep);\n\t\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t\t} else if (event->key() == Qt::Key_Minus) {\n\t\t\t\t\t_delegate->ivSetZoom(_delegate->ivZoom() - kZoomStep);\n\t\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t\t} else if (event->key() == Qt::Key_0) {\n\t\t\t\t\t_delegate->ivSetZoom(0);\n\t\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\tconst auto widget = raw->widget();\n\tif (!widget) {\n\t\tbase::take(_webview);\n\t\tshowWebviewError();\n\t\treturn;\n\t}\n\twidget->show();\n\n\tQObject::connect(widget, &QObject::destroyed, [=] {\n\t\tif (!_webview) {\n\t\t\t// If we destroyed _webview ourselves,\n\t\t\t// we don't show any message, nothing crashed.\n\t\t\treturn;\n\t\t}\n\t\tcrl::on_main(window, [=] {\n\t\t\tshowWebviewError({ \"Error: WebView has crashed.\" });\n\t\t});\n\t\tbase::take(_webview);\n\t});\n\n\t_container->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tif (const auto widget = raw->widget()) {\n\t\t\twidget->setGeometry(QRect(QPoint(), size));\n\t\t}\n\t}, _container->lifetime());\n\n\traw->setNavigationStartHandler([=](const QString &uri, bool newWindow) {\n\t\tif (uri.startsWith(u\"http://desktop-app-resource/\"_q)\n\t\t\t|| QUrl(uri).host().toLower().endsWith(u\".magic.org\"_q)) {\n\t\t\treturn true;\n\t\t}\n\t\t_events.fire({ .type = Event::Type::OpenLink, .url = uri });\n\t\treturn false;\n\t});\n\traw->setNavigationDoneHandler([=](bool success) {\n\t});\n\traw->setMessageHandler([=](const QJsonDocument &message) {\n\t\tcrl::on_main(_window.get(), [=] {\n\t\t\tconst auto object = message.object();\n\t\t\tconst auto event = object.value(\"event\").toString();\n\t\t\tif (event == u\"keydown\"_q) {\n\t\t\t\tconst auto key = object.value(\"key\").toString();\n\t\t\t\tconst auto modifier = object.value(\"modifier\").toString();\n\t\t\t\tprocessKey(key, modifier);\n\t\t\t} else if (event == u\"mouseenter\"_q) {\n\t\t\t\twindow->overrideSystemButtonOver({});\n\t\t\t} else if (event == u\"mouseup\"_q) {\n\t\t\t\twindow->overrideSystemButtonDown({});\n\t\t\t} else if (event == u\"link_click\"_q) {\n\t\t\t\tconst auto url = object.value(\"url\").toString();\n\t\t\t\tconst auto context = object.value(\"context\").toString();\n\t\t\t\tprocessLink(url, context);\n\t\t\t} else if (event == \"menu_page_blocker_click\") {\n\t\t\t\tif (_menu) {\n\t\t\t\t\t_menu->hideMenu();\n\t\t\t\t}\n\t\t\t} else if (event == u\"ready\"_q) {\n\t\t\t\t_ready = true;\n\t\t\t\tauto script = QByteArray();\n\t\t\t\tfor (const auto &[id, in] : base::take(_inChannelChanged)) {\n\t\t\t\t\tscript += toggleInChannelScript(id, in);\n\t\t\t\t}\n\t\t\t\tif (_navigateToIndexWhenReady >= 0) {\n\t\t\t\t\tscript += navigateScript(\n\t\t\t\t\t\tstd::exchange(_navigateToIndexWhenReady, -1),\n\t\t\t\t\t\tbase::take(_navigateToHashWhenReady));\n\t\t\t\t}\n\t\t\t\tif (base::take(_reloadInitialWhenReady)) {\n\t\t\t\t\tscript += reloadScript(0);\n\t\t\t\t}\n\t\t\t\tif (_menu) {\n\t\t\t\t\tscript += \"IV.menuShown(true);\";\n\t\t\t\t}\n\t\t\t\tif (!script.isEmpty()) {\n\t\t\t\t\t_webview->eval(script);\n\t\t\t\t}\n\t\t\t} else if (event == u\"location_change\"_q) {\n\t\t\t\t_index = object.value(\"index\").toInt();\n\t\t\t\t_hash = object.value(\"hash\").toString();\n\t\t\t\t_webview->refreshNavigationHistoryState();\n\t\t\t}\n\t\t});\n\t});\n\traw->setDataRequestHandler([=](Webview::DataRequest request) {\n\t\tconst auto pos = request.id.find('#');\n\t\tif (pos != request.id.npos) {\n\t\t\trequest.id = request.id.substr(0, pos);\n\t\t}\n\t\tif (!request.id.starts_with(\"iv/\")) {\n\t\t\t_dataRequests.fire(std::move(request));\n\t\t\treturn Webview::DataResult::Pending;\n\t\t}\n\t\tconst auto finishWith = [&](QByteArray data, std::string mime) {\n\t\t\trequest.done({\n\t\t\t\t.stream = std::make_unique<Webview::DataStreamFromMemory>(\n\t\t\t\t\tstd::move(data),\n\t\t\t\t\tstd::move(mime)),\n\t\t\t\t});\n\t\t\treturn Webview::DataResult::Done;\n\t\t};\n\t\tconst auto id = std::string_view(request.id).substr(3);\n\t\tif (id.starts_with(\"page\") && id.ends_with(\".html\")) {\n\t\t\tif (!_subscribedToColors) {\n\t\t\t\t_subscribedToColors = true;\n\n\t\t\t\trpl::merge(\n\t\t\t\t\tLang::Updated(),\n\t\t\t\t\tstyle::PaletteChanged(),\n\t\t\t\t\t_delegate->ivZoomValue() | rpl::to_empty\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\t_updateStyles.call();\n\t\t\t\t}, _webview->lifetime());\n\t\t\t}\n\t\t\tauto index = 0;\n\t\t\tconst auto result = std::from_chars(\n\t\t\t\tid.data() + 4,\n\t\t\t\tid.data() + id.size() - 5,\n\t\t\t\tindex);\n\t\t\tif (result.ec != std::errc()\n\t\t\t\t|| index < 0\n\t\t\t\t|| index >= _pages.size()) {\n\t\t\t\treturn Webview::DataResult::Failed;\n\t\t\t}\n\t\t\tconst auto webviewZoomController = _webview->zoomController();\n\t\t\tconst auto styleZoom = webviewZoomController\n\t\t\t\t? kDefaultZoom\n\t\t\t\t: _delegate->ivZoom();\n\t\t\treturn finishWith(\n\t\t\t\tWrapPage(_pages[index], styleZoom),\n\t\t\t\t\"text/html; charset=utf-8\");\n\t\t} else if (id.starts_with(\"page\") && id.ends_with(\".json\")) {\n\t\t\tauto index = 0;\n\t\t\tconst auto result = std::from_chars(\n\t\t\t\tid.data() + 4,\n\t\t\t\tid.data() + id.size() - 5,\n\t\t\t\tindex);\n\t\t\tif (result.ec != std::errc()\n\t\t\t\t|| index < 0\n\t\t\t\t|| index >= _pages.size()) {\n\t\t\t\treturn Webview::DataResult::Failed;\n\t\t\t}\n\t\t\tauto &page = _pages[index];\n\t\t\treturn finishWith(QJsonDocument(QJsonObject{\n\t\t\t\t{ \"html\", QJsonValue(QString::fromUtf8(page.content)) },\n\t\t\t\t{ \"js\", QJsonValue(QString::fromUtf8(page.script)) },\n\t\t\t}).toJson(QJsonDocument::Compact), \"application/json\");\n\t\t}\n\t\tconst auto css = id.ends_with(\".css\");\n\t\tconst auto js = !css && id.ends_with(\".js\");\n\t\tif (!css && !js) {\n\t\t\treturn Webview::DataResult::Failed;\n\t\t}\n\t\tconst auto qstring = QString::fromUtf8(id.data(), id.size());\n\t\tconst auto pattern = u\"^[a-zA-Z\\\\.\\\\-_0-9]+$\"_q;\n\t\tif (QRegularExpression(pattern).match(qstring).hasMatch()) {\n\t\t\tconst auto bytes = ReadResource(qstring);\n\t\t\tif (!bytes.isEmpty()) {\n\t\t\t\tconst auto mime = css ? \"text/css\" : \"text/javascript\";\n\t\t\t\tconst auto full = (qstring == u\"page.js\"_q)\n\t\t\t\t\t? (ReadResource(\"morphdom.js\") + bytes)\n\t\t\t\t\t: bytes;\n\t\t\t\treturn finishWith(full, mime);\n\t\t\t}\n\t\t}\n\t\treturn Webview::DataResult::Failed;\n\t});\n\n\traw->navigationHistoryState(\n\t) | rpl::on_next([=](Webview::NavigationHistoryState state) {\n\t\t_back->toggle(\n\t\t\tstate.canGoBack || state.canGoForward,\n\t\t\tanim::type::normal);\n\t\t_forward->toggle(state.canGoForward, anim::type::normal);\n\t\t_back->entity()->setDisabled(!state.canGoBack);\n\t\t_back->entity()->setIconOverride(\n\t\t\tstate.canGoBack ? nullptr : &st::ivBackIconDisabled,\n\t\t\tstate.canGoBack ? nullptr : &st::ivBackIconDisabled);\n\t\t_back->setAttribute(\n\t\t\tQt::WA_TransparentForMouseEvents,\n\t\t\t!state.canGoBack);\n\t\t_url = QString::fromStdString(state.url);\n\t}, _webview->lifetime());\n\n\traw->init(R\"()\");\n}\n\nvoid Controller::showWebviewError() {\n\tconst auto available = Webview::Availability();\n\tif (available.error != Webview::Available::Error::None) {\n\t\tshowWebviewError(Ui::BotWebView::ErrorText(available));\n\t} else {\n\t\tshowWebviewError({ \"Error: Could not initialize WebView.\" });\n\t}\n}\n\nvoid Controller::showWebviewError(TextWithEntities text) {\n\tconst auto wrap = Ui::CreateChild<Ui::RpWidget>(_container);\n\n\tconst auto error = Ui::CreateChild<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\twrap,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\twrap,\n\t\t\trpl::single(text),\n\t\t\tst::paymentsCriticalError),\n\t\tst::paymentsCriticalErrorPadding);\n\terror->entity()->setClickHandlerFilter([=](\n\t\t\tconst ClickHandlerPtr &handler,\n\t\t\tQt::MouseButton) {\n\t\tconst auto entity = handler->getTextEntity();\n\t\tif (entity.type != EntityType::CustomUrl) {\n\t\t\treturn true;\n\t\t}\n\t\tFile::OpenUrl(entity.data);\n\t\treturn false;\n\t});\n\twrap->show();\n\n\twrap->widthValue() | rpl::on_next([=](int width) {\n\t\terror->resizeToWidth(width);\n\t\twrap->resize(width, error->height());\n\t}, wrap->lifetime());\n\n\t_container->sizeValue() | rpl::on_next([=](QSize size) {\n\t\twrap->setGeometry(0, 0, size.width(), size.height() * 2 / 3);\n\t}, wrap->lifetime());\n}\n\nvoid Controller::showInWindow(\n\t\tconst Webview::StorageId &storageId,\n\t\tPrepared page) {\n\tExpects(_container != nullptr);\n\n\tconst auto url = page.url;\n\t_hash = page.hash;\n\tauto i = _indices.find(url);\n\tif (i == end(_indices)) {\n\t\t_pages.push_back(std::move(page));\n\t\ti = _indices.emplace(url, int(_pages.size() - 1)).first;\n\t}\n\tconst auto index = i->second;\n\t_index = index;\n\tif (!_webview) {\n\t\tcreateWebview(storageId);\n\t\tif (_webview && _webview->widget()) {\n\t\t\tauto id = u\"iv/page%1.html\"_q.arg(index);\n\t\t\tif (!_hash.isEmpty()) {\n\t\t\t\tid += '#' + _hash;\n\t\t\t}\n\t\t\t_webview->navigateToData(id);\n\t\t\tactivate();\n\t\t} else {\n\t\t\t_events.fire({ Event::Type::Close });\n\t\t}\n\t} else if (_ready) {\n\t\t_webview->eval(navigateScript(index, _hash));\n\t\tactivate();\n\t} else {\n\t\t_navigateToIndexWhenReady = index;\n\t\t_navigateToHashWhenReady = _hash;\n\t\tactivate();\n\t}\n}\n\nvoid Controller::activate() {\n\tif (_window->isMinimized()) {\n\t\t_window->showNormal();\n\t} else if (_window->isHidden()) {\n\t\t_window->show();\n\t}\n\t_window->raise();\n\t_window->activateWindow();\n\t_window->setFocus();\n\tsetInnerFocus();\n}\n\nvoid Controller::setInnerFocus() {\n\tif (const auto onstack = _shareFocus) {\n\t\tonstack();\n\t} else if (_webview) {\n\t\t_webview->focus();\n\t}\n}\n\nQByteArray Controller::navigateScript(int index, const QString &hash) {\n\treturn \"IV.navigateTo(\"\n\t\t+ QByteArray::number(index)\n\t\t+ \", '\"\n\t\t+ Ui::EscapeForScriptString(qthelp::url_decode(hash).toUtf8())\n\t\t+ \"');\";\n}\n\nQByteArray Controller::reloadScript(int index) {\n\treturn \"IV.reloadPage(\"\n\t\t+ QByteArray::number(index)\n\t\t+ \");\";\n}\n\nvoid Controller::processKey(const QString &key, const QString &modifier) {\n\tconst auto ctrl = Platform::IsMac() ? u\"cmd\"_q : u\"ctrl\"_q;\n\tif (key == u\"escape\"_q) {\n\t\tescape();\n\t} else if (key == u\"w\"_q && modifier == ctrl) {\n\t\tclose();\n\t} else if (key == u\"m\"_q && modifier == ctrl) {\n\t\tminimize();\n\t} else if (key == u\"q\"_q && modifier == ctrl) {\n\t\tquit();\n\t} else if (key == u\"0\"_q && modifier == ctrl) {\n\t\t_delegate->ivSetZoom(0);\n\t}\n}\n\nvoid Controller::processLink(const QString &url, const QString &context) {\n\tconst auto channelPrefix = u\"channel\"_q;\n\tconst auto joinPrefix = u\"join_link\"_q;\n\tconst auto webpagePrefix = u\"webpage\"_q;\n\tconst auto viewerPrefix = u\"viewer\"_q;\n\tif (context == u\"report-iv\") {\n\t\t_events.fire({\n\t\t\t.type = Event::Type::Report,\n\t\t\t.context = QString::number(compuseCurrentPageId()),\n\t\t});\n\t} else if (context.startsWith(channelPrefix)) {\n\t\t_events.fire({\n\t\t\t.type = Event::Type::OpenChannel,\n\t\t\t.context = context.mid(channelPrefix.size()),\n\t\t});\n\t} else if (context.startsWith(joinPrefix)) {\n\t\t_events.fire({\n\t\t\t.type = Event::Type::JoinChannel,\n\t\t\t.context = context.mid(joinPrefix.size()),\n\t\t});\n\t} else if (context.startsWith(webpagePrefix)) {\n\t\t_events.fire({\n\t\t\t.type = Event::Type::OpenPage,\n\t\t\t.url = url,\n\t\t\t.context = context.mid(webpagePrefix.size()),\n\t\t});\n\t} else if (context.startsWith(viewerPrefix)) {\n\t\t_events.fire({\n\t\t\t.type = Event::Type::OpenMedia,\n\t\t\t.url = url,\n\t\t\t.context = context.mid(viewerPrefix.size()),\n\t\t});\n\t} else if (context.isEmpty()) {\n\t\t_events.fire({ .type = Event::Type::OpenLink, .url = url });\n\t}\n}\n\nbool Controller::active() const {\n\treturn _window && _window->isActiveWindow();\n}\n\nvoid Controller::showJoinedTooltip() {\n\tif (_webview && _ready) {\n\t\t_webview->eval(\"IV.showTooltip('\"\n\t\t\t+ Ui::EscapeForScriptString(\n\t\t\t\ttr::lng_action_you_joined(tr::now).toUtf8())\n\t\t\t+ \"');\");\n\t}\n}\n\nvoid Controller::minimize() {\n\tif (_window) {\n\t\t_window->setWindowState(_window->windowState()\n\t\t\t| Qt::WindowMinimized);\n\t}\n}\n\nQString Controller::composeCurrentUrl() const {\n\tconst auto index = _index.current();\n\tAssert(index >= 0 && index < _pages.size());\n\n\treturn _pages[index].url\n\t\t+ (_hash.isEmpty() ? u\"\"_q : ('#' + _hash));\n}\n\nuint64 Controller::compuseCurrentPageId() const {\n\tconst auto index = _index.current();\n\tAssert(index >= 0 && index < _pages.size());\n\n\treturn _pages[index].pageId;\n}\n\nvoid Controller::showMenu() {\n\tconst auto index = _index.current();\n\tif (_menu || index < 0 || index > _pages.size()) {\n\t\treturn;\n\t}\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t_window.get(),\n\t\tst::popupMenuWithIcons);\n\tif (_webview && _ready) {\n\t\t_webview->eval(\"IV.menuShown(true);\");\n\t}\n\t_menu->setDestroyedCallback(crl::guard(_window.get(), [\n\t\t\tthis,\n\t\t\tweakButton = base::make_weak(_menuToggle.data()),\n\t\t\tmenu = _menu.get()] {\n\t\tif (_menu == menu && weakButton) {\n\t\t\tweakButton->setForceRippled(false);\n\t\t}\n\t\tif (const auto widget = _webview ? _webview->widget() : nullptr) {\n\t\t\tInvokeQueued(widget, crl::guard(_window.get(), [=] {\n\t\t\t\tif (_webview && _ready) {\n\t\t\t\t\t_webview->eval(\"IV.menuShown(false);\");\n\t\t\t\t}\n\t\t\t}));\n\t\t}\n\t}));\n\t_menuToggle->setForceRippled(true);\n\n\tconst auto url = composeCurrentUrl();\n\tconst auto openInBrowser = crl::guard(_window.get(), [=] {\n\t\t_events.fire({ .type = Event::Type::OpenLinkExternal, .url = url });\n\t});\n\t_menu->addAction(\n\t\ttr::lng_iv_open_in_browser(tr::now),\n\t\topenInBrowser,\n\t\t&st::menuIconIpAddress);\n\n\t_menu->addAction(tr::lng_iv_share(tr::now), [=] {\n\t\tshowShareMenu();\n\t}, &st::menuIconShare);\n\n\t_menu->addSeparator();\n\t_menu->addAction(\n\t\tbase::make_unique_q<ItemZoom>(_menu, _delegate, _menu->menu()->st()));\n\n\t_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);\n\t_menu->popup(_window->body()->mapToGlobal(\n\t\tQPoint(_window->body()->width(), 0) + st::ivMenuPosition));\n}\n\nvoid Controller::escape() {\n\tif (const auto onstack = _shareHide) {\n\t\tonstack();\n\t} else {\n\t\tclose();\n\t}\n}\n\nvoid Controller::close() {\n\t_events.fire({ Event::Type::Close });\n}\n\nvoid Controller::quit() {\n\t_events.fire({ Event::Type::Quit });\n}\n\nrpl::lifetime &Controller::lifetime() {\n\treturn _lifetime;\n}\n\nvoid Controller::destroyShareMenu() {\n\t_shareHide = nullptr;\n\tif (_shareFocus) {\n\t\t_shareFocus = nullptr;\n\t\tsetInnerFocus();\n\t}\n\tif (_shareWrap) {\n\t\tif (_shareContainer) {\n\t\t\t_shareWrap->windowHandle()->setParent(nullptr);\n\t\t}\n\t\t_shareWrap = nullptr;\n\t\t_shareContainer = nullptr;\n\t}\n\tif (_shareHidesContent) {\n\t\t_shareHidesContent = false;\n\t\tif (const auto content = _webview ? _webview->widget() : nullptr) {\n\t\t\tcontent->show();\n\t\t}\n\t}\n}\n\nvoid Controller::showShareMenu() {\n\tconst auto index = _index.current();\n\tif (_shareWrap || index < 0 || index > _pages.size()) {\n\t\treturn;\n\t}\n\t_shareHidesContent = Platform::IsMac();\n\tif (_shareHidesContent) {\n\t\tif (const auto content = _webview ? _webview->widget() : nullptr) {\n\t\t\tcontent->hide();\n\t\t}\n\t}\n\n\t_shareWrap = std::make_unique<Ui::RpWidget>(_shareHidesContent\n\t\t? _window->body().get()\n\t\t: nullptr);\n\tif (!_shareHidesContent) {\n\t\t_shareWrap->setGeometry(_window->body()->rect());\n\t\t_shareWrap->setWindowFlag(Qt::FramelessWindowHint);\n\t\t_shareWrap->setAttribute(Qt::WA_TranslucentBackground);\n\t\t_shareWrap->setAttribute(Qt::WA_NoSystemBackground);\n\t\t_shareWrap->createWinId();\n\n\t\t_shareContainer.reset(QWidget::createWindowContainer(\n\t\t\t_shareWrap->windowHandle(),\n\t\t\t_window->body().get(),\n\t\t\tQt::FramelessWindowHint | Qt::WindowStaysOnTopHint));\n\t}\n\t_window->body()->sizeValue() | rpl::on_next([=](QSize size) {\n\t\tconst auto widget = _shareHidesContent\n\t\t\t? _shareWrap.get()\n\t\t\t: _shareContainer.get();\n\t\twidget->setGeometry(QRect(QPoint(), size));\n\t}, _shareWrap->lifetime());\n\n\tauto result = _showShareBox({\n\t\t.parent = _shareWrap.get(),\n\t\t.url = composeCurrentUrl(),\n\t});\n\t_shareFocus = result.focus;\n\t_shareHide = result.hide;\n\n\tstd::move(result.destroyRequests) | rpl::on_next([=] {\n\t\tdestroyShareMenu();\n\t}, _shareWrap->lifetime());\n\n\tUi::ForceFullRepaintSync(_shareWrap.get());\n\n\tif (_shareHidesContent) {\n\t\t_shareWrap->show();\n\t} else {\n\t\t_shareContainer->show();\n\t}\n\tactivate();\n}\n\n} // namespace Iv\n"
  },
  {
    "path": "Telegram/SourceFiles/iv/iv_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/invoke_queued.h\"\n#include \"base/object_ptr.h\"\n#include \"base/unique_qptr.h\"\n#include \"iv/iv_delegate.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/text/text.h\"\n#include \"webview/webview_common.h\"\n\nclass Painter;\n\nnamespace Webview {\nstruct DataRequest;\nclass Window;\n} // namespace Webview\n\nnamespace Ui {\nclass RpWidget;\nclass RpWindow;\nclass PopupMenu;\nclass FlatLabel;\nclass IconButton;\ntemplate <typename Widget>\nclass FadeWrapScaled;\n} // namespace Ui\n\nnamespace Iv {\n\nstruct Prepared;\n\nstruct ShareBoxResult {\n\tFn<void()> focus;\n\tFn<void()> hide;\n\trpl::producer<> destroyRequests;\n};\nstruct ShareBoxDescriptor {\n\tnot_null<Ui::RpWidget*> parent;\n\tQString url;\n};\n\nclass Controller final {\npublic:\n\tController(\n\t\tnot_null<Delegate*> delegate,\n\t\tFn<ShareBoxResult(ShareBoxDescriptor)> showShareBox);\n\t~Controller();\n\n\tstruct Event {\n\t\tenum class Type {\n\t\t\tClose,\n\t\t\tQuit,\n\t\t\tOpenChannel,\n\t\t\tJoinChannel,\n\t\t\tOpenPage,\n\t\t\tOpenLink,\n\t\t\tOpenLinkExternal,\n\t\t\tOpenMedia,\n\t\t\tReport,\n\t\t};\n\t\tType type = Type::Close;\n\t\tQString url;\n\t\tQString context;\n\t};\n\n\tvoid show(\n\t\tconst Webview::StorageId &storageId,\n\t\tPrepared page,\n\t\tbase::flat_map<QByteArray, rpl::producer<bool>> inChannelValues);\n\tvoid update(Prepared page);\n\n\t[[nodiscard]] static bool IsGoodTonSiteUrl(const QString &uri);\n\tvoid showTonSite(const Webview::StorageId &storageId, QString uri);\n\n\t[[nodiscard]] bool active() const;\n\tvoid showJoinedTooltip();\n\tvoid minimize();\n\n\t[[nodiscard]] rpl::producer<Webview::DataRequest> dataRequests() const {\n\t\treturn _dataRequests.events();\n\t}\n\n\t[[nodiscard]] rpl::producer<Event> events() const {\n\t\treturn _events.events();\n\t}\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tvoid createWindow();\n\tvoid createWebview(const Webview::StorageId &storageId);\n\t[[nodiscard]] QByteArray navigateScript(int index, const QString &hash);\n\t[[nodiscard]] QByteArray reloadScript(int index);\n\n\tvoid showInWindow(const Webview::StorageId &storageId, Prepared page);\n\t[[nodiscard]] QByteArray fillInChannelValuesScript(\n\t\tbase::flat_map<QByteArray, rpl::producer<bool>> inChannelValues);\n\t[[nodiscard]] QByteArray toggleInChannelScript(\n\t\tconst QByteArray &id,\n\t\tbool in) const;\n\n\tvoid processKey(const QString &key, const QString &modifier);\n\tvoid processLink(const QString &url, const QString &context);\n\n\tvoid initControls();\n\tvoid updateTitleGeometry(int newWidth) const;\n\n\tvoid activate();\n\tvoid setInnerFocus();\n\tvoid showMenu();\n\tvoid escape();\n\tvoid close();\n\tvoid quit();\n\n\t[[nodiscard]] QString composeCurrentUrl() const;\n\t[[nodiscard]] uint64 compuseCurrentPageId() const;\n\tvoid showShareMenu();\n\tvoid destroyShareMenu();\n\n\tvoid showWebviewError();\n\tvoid showWebviewError(TextWithEntities text);\n\n\tconst not_null<Delegate*> _delegate;\n\n\tstd::unique_ptr<Ui::RpWindow> _window;\n\tstd::unique_ptr<Ui::RpWidget> _subtitleWrap;\n\trpl::variable<QString> _url;\n\trpl::variable<QString> _subtitleText;\n\trpl::variable<QString> _windowTitleText;\n\tstd::unique_ptr<Ui::FlatLabel> _subtitle;\n\tUi::Animations::Simple _subtitleBackShift;\n\tUi::Animations::Simple _subtitleForwardShift;\n\tobject_ptr<Ui::IconButton> _menuToggle = { nullptr };\n\tobject_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _back = { nullptr };\n\tobject_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _forward = { nullptr };\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\tUi::RpWidget *_container = nullptr;\n\tstd::unique_ptr<Webview::Window> _webview;\n\trpl::event_stream<Webview::DataRequest> _dataRequests;\n\trpl::event_stream<Event> _events;\n\tbase::flat_map<QByteArray, bool> _inChannelChanged;\n\tbase::flat_set<QByteArray> _inChannelSubscribed;\n\tSingleQueuedInvokation _updateStyles;\n\tbool _reloadInitialWhenReady = false;\n\tbool _subscribedToColors = false;\n\tbool _ready = false;\n\n\trpl::variable<int> _index = -1;\n\tQString _hash;\n\n\tFn<ShareBoxResult(ShareBoxDescriptor)> _showShareBox;\n\tstd::unique_ptr<Ui::RpWidget> _shareWrap;\n\tstd::unique_ptr<QWidget> _shareContainer;\n\tFn<void()> _shareFocus;\n\tFn<void()> _shareHide;\n\tbool _shareHidesContent = false;\n\n\tstd::vector<Prepared> _pages;\n\tbase::flat_map<QString, int> _indices;\n\tQString _navigateToHashWhenReady;\n\tint _navigateToIndexWhenReady = -1;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Iv\n"
  },
  {
    "path": "Telegram/SourceFiles/iv/iv_data.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"iv/iv_data.h\"\n\n#include \"iv/iv_prepare.h\"\n#include \"core/cached_webview_availability.h\"\n\n#include <QtCore/QRegularExpression>\n#include <QtCore/QUrl>\n\nnamespace Iv {\nnamespace {\n\nbool FailureRecorded/* = false*/;\n\n} // namespace\n\nQByteArray GeoPointId(Geo point) {\n\tconst auto lat = int(point.lat * 1000000);\n\tconst auto lon = int(point.lon * 1000000);\n\tconst auto combined = (std::uint64_t(std::uint32_t(lat)) << 32)\n\t\t| std::uint64_t(std::uint32_t(lon));\n\treturn QByteArray::number(quint64(combined))\n\t\t+ ','\n\t\t+ QByteArray::number(point.access);\n}\n\nGeo GeoPointFromId(QByteArray data) {\n\tconst auto parts = data.split(',');\n\tif (parts.size() != 2) {\n\t\treturn {};\n\t}\n\tconst auto combined = parts[0].toULongLong();\n\tconst auto lat = int(std::uint32_t(combined >> 32));\n\tconst auto lon = int(std::uint32_t(combined & 0xFFFFFFFFULL));\n\treturn {\n\t\t.lat = lat / 1000000.,\n\t\t.lon = lon / 1000000.,\n\t\t.access = parts[1].toULongLong(),\n\t};\n}\n\nData::Data(const MTPDwebPage &webpage, const MTPPage &page)\n: _source(std::make_unique<Source>(Source{\n\t.pageId = webpage.vid().v,\n\t.page = page,\n\t.webpagePhoto = (webpage.vphoto()\n\t\t? *webpage.vphoto()\n\t\t: std::optional<MTPPhoto>()),\n\t.webpageDocument = (webpage.vdocument()\n\t\t? *webpage.vdocument()\n\t\t: std::optional<MTPDocument>()),\n\t.name = (webpage.vsite_name()\n\t\t? qs(*webpage.vsite_name())\n\t\t: SiteNameFromUrl(qs(webpage.vurl())))\n})) {\n}\n\nQString Data::id() const {\n\treturn qs(_source->page.data().vurl());\n}\n\nbool Data::partial() const {\n\treturn _source->page.data().is_part();\n}\n\nData::~Data() = default;\n\nvoid Data::updateCachedViews(int cachedViews) {\n\t_source->updatedCachedViews = std::max(\n\t\t_source->updatedCachedViews,\n\t\tcachedViews);\n}\n\nvoid Data::prepare(const Options &options, Fn<void(Prepared)> done) const {\n\tcrl::async([source = *_source, options, done = std::move(done)] {\n\t\tdone(Prepare(source, options));\n\t});\n}\n\nQString SiteNameFromUrl(const QString &url) {\n\tconst auto u = QUrl(url);\n\tQString pretty = u.isValid() ? u.toDisplayString() : url;\n\tconst auto m = QRegularExpression(u\"^[a-zA-Z0-9]+://\"_q).match(pretty);\n\tif (m.hasMatch()) pretty = pretty.mid(m.capturedLength());\n\tint32 slash = pretty.indexOf('/');\n\tif (slash > 0) pretty = pretty.mid(0, slash);\n\tQStringList components = pretty.split('.', Qt::SkipEmptyParts);\n\tif (components.size() >= 2) {\n\t\tcomponents = components.mid(components.size() - 2);\n\t\treturn components.at(0).at(0).toUpper()\n\t\t\t+ components.at(0).mid(1)\n\t\t\t+ '.'\n\t\t\t+ components.at(1);\n\t}\n\treturn QString();\n}\n\nbool ShowButton() {\n\tconst auto &availability = Core::CachedWebviewAvailability();\n\treturn availability.customSchemeRequests\n\t\t&& availability.customRangeRequests;\n}\n\nvoid RecordShowFailure() {\n\tFailureRecorded = true;\n}\n\nbool FailedToShow() {\n\treturn FailureRecorded;\n}\n\n} // namespace Iv\n"
  },
  {
    "path": "Telegram/SourceFiles/iv/iv_data.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Iv {\n\nstruct Source;\n\nstruct Options {\n};\n\nstruct Prepared {\n\tuint64 pageId = 0;\n\tQString name;\n\tQByteArray content;\n\tQByteArray script;\n\tQString url;\n\tQString hash;\n\tbase::flat_map<QByteArray, QByteArray> embeds;\n\tbase::flat_set<QByteArray> channelIds;\n\tbool rtl = false;\n\tbool hasCode = false;\n\tbool hasEmbeds = false;\n};\n\nstruct Geo {\n\tfloat64 lat = 0.;\n\tfloat64 lon = 0.;\n\tuint64 access = 0;\n};\n\n[[nodiscard]] QByteArray GeoPointId(Geo point);\n[[nodiscard]] Geo GeoPointFromId(QByteArray data);\n\nclass Data final {\npublic:\n\tData(const MTPDwebPage &webpage, const MTPPage &page);\n\t~Data();\n\n\t[[nodiscard]] QString id() const;\n\t[[nodiscard]] bool partial() const;\n\n\tvoid updateCachedViews(int cachedViews);\n\n\tvoid prepare(const Options &options, Fn<void(Prepared)> done) const;\n\nprivate:\n\tconst std::unique_ptr<Source> _source;\n\n};\n\n[[nodiscard]] QString SiteNameFromUrl(const QString &url);\n\n[[nodiscard]] bool ShowButton();\n\nvoid RecordShowFailure();\n[[nodiscard]] bool FailedToShow();\n\n} // namespace Iv\n"
  },
  {
    "path": "Telegram/SourceFiles/iv/iv_delegate.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass RpWindow;\n} // namespace Ui\n\nnamespace Iv {\n\nclass Delegate {\npublic:\n\t[[nodiscard]] virtual QRect ivGeometry(not_null<Ui::RpWindow*> window) const = 0;\n\tvirtual void ivSaveGeometry(not_null<Ui::RpWindow*> window) = 0;\n\n\t[[nodiscard]] virtual int ivZoom() const = 0;\n\t[[nodiscard]] virtual rpl::producer<int> ivZoomValue() const = 0;\n\tvirtual void ivSetZoom(int value) = 0;\n};\n\n} // namespace Iv\n"
  },
  {
    "path": "Telegram/SourceFiles/iv/iv_delegate_impl.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"iv/iv_delegate_impl.h\"\n\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"mainwindow.h\"\n#include \"window/main_window.h\"\n#include \"window/window_controller.h\"\n#include \"styles/style_window.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QScreen>\n#include <QtGui/QWindow>\n\nnamespace Iv {\nnamespace {\n\n[[nodiscard]] Core::WindowPosition DefaultPosition() {\n\tauto center = qApp->primaryScreen()->geometry().center();\n\tconst auto moncrc = [&] {\n\t\tif (const auto active = Core::App().activeWindow()) {\n\t\t\tconst auto widget = active->widget();\n\t\t\tcenter = widget->geometry().center();\n\t\t\tif (const auto screen = widget->screen()) {\n\t\t\t\treturn Platform::ScreenNameChecksum(screen->name());\n\t\t\t}\n\t\t}\n\t\treturn Core::App().settings().windowPosition().moncrc;\n\t}();\n\treturn {\n\t\t.moncrc = moncrc,\n\t\t.scale = cScale(),\n\t\t.x = (center.x() - st::ivWidthDefault / 2),\n\t\t.y = (center.y() - st::ivHeightDefault / 2),\n\t\t.w = st::ivWidthDefault,\n\t\t.h = st::ivHeightDefault,\n\t};\n}\n\n} // namespace\n\nQRect DelegateImpl::ivGeometry(not_null<Ui::RpWindow*> window) const {\n\tconst auto saved = Core::App().settings().ivPosition();\n\tconst auto adjusted = Core::AdjustToScale(saved, u\"IV\"_q);\n\tconst auto initial = DefaultPosition();\n\treturn Window::CountInitialGeometry(\n\t\twindow,\n\t\tadjusted,\n\t\tinitial,\n\t\t{ st::ivWidthMin, st::ivHeightMin },\n\t\tu\"IV\"_q);\n}\n\nvoid DelegateImpl::ivSaveGeometry(not_null<Ui::RpWindow*> window) {\n\tif (!window->windowHandle()) {\n\t\treturn;\n\t}\n\tconst auto state = window->windowHandle()->windowState();\n\tif (state == Qt::WindowMinimized) {\n\t\treturn;\n\t}\n\tconst auto &savedPosition = Core::App().settings().ivPosition();\n\tauto realPosition = savedPosition;\n\tif (state == Qt::WindowMaximized) {\n\t\trealPosition.maximized = 1;\n\t\trealPosition.moncrc = 0;\n\t\tDEBUG_LOG((\"IV Pos: Saving maximized position.\"));\n\t} else {\n\t\tauto r = window->body()->mapToGlobal(window->body()->rect());\n\t\trealPosition.x = r.x();\n\t\trealPosition.y = r.y();\n\t\trealPosition.w = r.width();\n\t\trealPosition.h = r.height();\n\t\trealPosition.scale = cScale();\n\t\trealPosition.maximized = 0;\n\t\trealPosition.moncrc = 0;\n\t\tDEBUG_LOG((\"IV Pos: \"\n\t\t\t\"Saving non-maximized position: %1, %2, %3, %4\"\n\t\t\t).arg(realPosition.x\n\t\t\t).arg(realPosition.y\n\t\t\t).arg(realPosition.w\n\t\t\t).arg(realPosition.h));\n\t}\n\trealPosition = Window::PositionWithScreen(\n\t\trealPosition,\n\t\twindow,\n\t\t{ st::ivWidthMin, st::ivHeightMin },\n\t\tu\"IV\"_q);\n\tif (realPosition.w >= st::ivWidthMin\n\t\t&& realPosition.h >= st::ivHeightMin\n\t\t&& realPosition != savedPosition) {\n\t\tDEBUG_LOG((\"IV Pos: \"\n\t\t\t\"Writing: %1, %2, %3, %4 (scale %5%, maximized %6)\")\n\t\t\t.arg(realPosition.x)\n\t\t\t.arg(realPosition.y)\n\t\t\t.arg(realPosition.w)\n\t\t\t.arg(realPosition.h)\n\t\t\t.arg(realPosition.scale)\n\t\t\t.arg(Logs::b(realPosition.maximized)));\n\t\tCore::App().settings().setIvPosition(realPosition);\n\t\tCore::App().saveSettingsDelayed();\n\t}\n}\n\nint DelegateImpl::ivZoom() const {\n\treturn Core::App().settings().ivZoom();\n}\nrpl::producer<int> DelegateImpl::ivZoomValue() const {\n\treturn Core::App().settings().ivZoomValue();\n}\nvoid DelegateImpl::ivSetZoom(int value) {\n\tCore::App().settings().setIvZoom(value);\n\tCore::App().saveSettingsDelayed();\n}\n\n} // namespace Iv\n"
  },
  {
    "path": "Telegram/SourceFiles/iv/iv_delegate_impl.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"iv/iv_delegate.h\"\n\nnamespace Iv {\n\nclass DelegateImpl final : public Delegate {\npublic:\n\tDelegateImpl() = default;\n\n\t[[nodiscard]] QRect ivGeometry(not_null<Ui::RpWindow*> window) const override;\n\tvoid ivSaveGeometry(not_null<Ui::RpWindow*> window) override;\n\n\t[[nodiscard]] int ivZoom() const override;\n\t[[nodiscard]] rpl::producer<int> ivZoomValue() const override;\n\tvoid ivSetZoom(int value) override;\n\n};\n\n} // namespace Iv\n"
  },
  {
    "path": "Telegram/SourceFiles/iv/iv_instance.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"iv/iv_instance.h\"\n\n#include \"apiwrap.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"boxes/share_box.h\"\n#include \"core/application.h\"\n#include \"core/file_utilities.h\"\n#include \"core/shortcuts.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_cloud_file.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_thread.h\"\n#include \"data/data_web_page.h\"\n#include \"data/data_user.h\"\n#include \"history/history_item_helpers.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"iv/iv_controller.h\"\n#include \"iv/iv_data.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_common.h\" // Lottie::ReadContent.\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"main/session/session_show.h\"\n#include \"media/streaming/media_streaming_loader.h\"\n#include \"media/view/media_view_open_common.h\"\n#include \"storage/file_download.h\"\n#include \"storage/storage_account.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/layers/layer_widget.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"webview/webview_data_stream_memory.h\"\n#include \"webview/webview_interface.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_session_controller_link_info.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QWindow>\n\nnamespace Iv {\nnamespace {\n\nconstexpr auto kGeoPointScale = 1;\nconstexpr auto kGeoPointZoomMin = 13;\nconstexpr auto kMaxLoadParts = 5;\nconstexpr auto kKeepLoadingParts = 8;\nconstexpr auto kAllowPageReloadAfter = 3 * crl::time(1000);\n\n} // namespace\n\nclass Shown final : public base::has_weak_ptr {\npublic:\n\tShown(\n\t\tnot_null<Delegate*> delegate,\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<Data*> data,\n\t\tQString hash);\n\n\t[[nodiscard]] bool showing(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<Data*> data) const;\n\t[[nodiscard]] bool showingFrom(not_null<Main::Session*> session) const;\n\t[[nodiscard]] bool activeFor(not_null<Main::Session*> session) const;\n\t[[nodiscard]] bool active() const;\n\n\tvoid moveTo(not_null<Data*> data, QString hash);\n\tvoid update(not_null<Data*> data);\n\n\tvoid showJoinedTooltip();\n\tvoid minimize();\n\n\t[[nodiscard]] rpl::producer<Controller::Event> events() const {\n\t\treturn _events.events();\n\t}\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tstruct MapPreview {\n\t\tstd::unique_ptr<::Data::CloudFile> file;\n\t\tQByteArray bytes;\n\t};\n\tstruct PartRequest {\n\t\tWebview::DataRequest request;\n\t\tQByteArray data;\n\t\tstd::vector<bool> loaded;\n\t\tint64 offset = 0;\n\t};\n\tstruct FileStream {\n\t\tnot_null<DocumentData*> document;\n\t\tstd::unique_ptr<Media::Streaming::Loader> loader;\n\t\tstd::vector<PartRequest> requests;\n\t\tstd::string mime;\n\t\trpl::lifetime lifetime;\n\t};\n\tstruct FileLoad {\n\t\tstd::shared_ptr<::Data::DocumentMedia> media;\n\t\tstd::vector<Webview::DataRequest> requests;\n\t};\n\n\tvoid prepare(not_null<Data*> data, const QString &hash);\n\tvoid createController();\n\n\tvoid showWindowed(Prepared result);\n\t[[nodiscard]] ShareBoxResult shareBox(ShareBoxDescriptor &&descriptor);\n\n\t[[nodiscard]] ::Data::FileOrigin fileOrigin(\n\t\tnot_null<WebPageData*> page) const;\n\tvoid streamPhoto(QStringView idWithPageId, Webview::DataRequest request);\n\tvoid streamFile(QStringView idWithPageId, Webview::DataRequest request);\n\tvoid streamFile(FileStream &file, Webview::DataRequest request);\n\tvoid processPartInFile(\n\t\tFileStream &file,\n\t\tMedia::Streaming::LoadedPart &&part);\n\tbool finishRequestWithPart(\n\t\tPartRequest &request,\n\t\tconst Media::Streaming::LoadedPart &part);\n\tvoid streamMap(QString params, Webview::DataRequest request);\n\tvoid sendEmbed(QByteArray hash, Webview::DataRequest request);\n\n\tvoid fillChannelJoinedValues(const Prepared &result);\n\tvoid fillEmbeds(base::flat_map<QByteArray, QByteArray> added);\n\tvoid subscribeToDocuments();\n\t[[nodiscard]] QByteArray readFile(\n\t\tconst std::shared_ptr<::Data::DocumentMedia> &media);\n\tvoid requestDone(\n\t\tWebview::DataRequest request,\n\t\tQByteArray bytes,\n\t\tstd::string mime,\n\t\tint64 offset = 0,\n\t\tint64 total = 0);\n\tvoid requestFail(Webview::DataRequest request);\n\n\tconst not_null<Delegate*> _delegate;\n\tconst not_null<Main::Session*> _session;\n\tstd::shared_ptr<Main::SessionShow> _show;\n\tQString _id;\n\tstd::unique_ptr<Controller> _controller;\n\tbase::flat_map<DocumentId, FileStream> _streams;\n\tbase::flat_map<DocumentId, FileLoad> _files;\n\tbase::flat_map<QByteArray, rpl::producer<bool>> _inChannelValues;\n\n\tbool _preparing = false;\n\n\tbase::flat_map<QByteArray, QByteArray> _embeds;\n\tbase::flat_map<QString, MapPreview> _maps;\n\tstd::vector<QByteArray> _resources;\n\n\trpl::event_stream<Controller::Event> _events;\n\n\trpl::lifetime _documentLifetime;\n\trpl::lifetime _lifetime;\n\n};\n\nclass TonSite final : public base::has_weak_ptr {\npublic:\n\tTonSite(not_null<Delegate*> delegate, QString uri);\n\n\t[[nodiscard]] bool active() const;\n\n\tvoid moveTo(QString uri);\n\n\tvoid minimize();\n\n\t[[nodiscard]] rpl::producer<Controller::Event> events() const {\n\t\treturn _events.events();\n\t}\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tvoid createController();\n\n\tvoid showWindowed();\n\n\tconst not_null<Delegate*> _delegate;\n\tQString _uri;\n\tstd::unique_ptr<Controller> _controller;\n\n\trpl::event_stream<Controller::Event> _events;\n\n\trpl::lifetime _lifetime;\n\n};\n\nShown::Shown(\n\tnot_null<Delegate*> delegate,\n\tnot_null<Main::Session*> session,\n\tnot_null<Data*> data,\n\tQString hash)\n: _delegate(delegate)\n, _session(session) {\n\tprepare(data, hash);\n}\n\nvoid Shown::prepare(not_null<Data*> data, const QString &hash) {\n\tconst auto weak = base::make_weak(this);\n\n\t_preparing = true;\n\tconst auto id = _id = data->id();\n\tdata->prepare({}, [=](Prepared result) {\n\t\tresult.hash = hash;\n\t\tcrl::on_main(weak, [=, result = std::move(result)]() mutable {\n\t\t\tresult.url = id;\n\t\t\tif (_id != id || !_preparing) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_preparing = false;\n\t\t\tfillChannelJoinedValues(result);\n\t\t\tfillEmbeds(std::move(result.embeds));\n\t\t\tshowWindowed(std::move(result));\n\t\t});\n\t});\n}\n\nvoid Shown::fillChannelJoinedValues(const Prepared &result) {\n\tfor (const auto &id : result.channelIds) {\n\t\tconst auto channelId = ChannelId(id.toLongLong());\n\t\tconst auto channel = _session->data().channel(channelId);\n\t\tif (!channel->isLoaded() && !channel->username().isEmpty()) {\n\t\t\tchannel->session().api().request(MTPcontacts_ResolveUsername(\n\t\t\t\tMTP_flags(0),\n\t\t\t\tMTP_string(channel->username()),\n\t\t\t\tMTP_string()\n\t\t\t)).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\t\t\tchannel->owner().processUsers(result.data().vusers());\n\t\t\t\tchannel->owner().processChats(result.data().vchats());\n\t\t\t}).send();\n\t\t}\n\t\t_inChannelValues[id] = Info::Profile::AmInChannelValue(channel);\n\t}\n}\n\nvoid Shown::fillEmbeds(base::flat_map<QByteArray, QByteArray> added) {\n\tif (_embeds.empty()) {\n\t\t_embeds = std::move(added);\n\t} else {\n\t\tfor (auto &[k, v] : added) {\n\t\t\t_embeds[k] = std::move(v);\n\t\t}\n\t}\n}\n\nShareBoxResult Shown::shareBox(ShareBoxDescriptor &&descriptor) {\n\tclass Show final : public Ui::Show {\n\tpublic:\n\t\tShow(QPointer<QWidget> parent, Fn<Ui::LayerStackWidget*()> lookup)\n\t\t: _parent(parent)\n\t\t, _lookup(lookup) {\n\t\t}\n\t\tvoid showOrHideBoxOrLayer(\n\t\t\t\tstd::variant<\n\t\t\t\tv::null_t,\n\t\t\t\tobject_ptr<Ui::BoxContent>,\n\t\t\t\tstd::unique_ptr<Ui::LayerWidget>> &&layer,\n\t\t\t\tUi::LayerOptions options,\n\t\t\t\tanim::type animated) const override {\n\t\t\tusing UniqueLayer = std::unique_ptr<Ui::LayerWidget>;\n\t\t\tusing ObjectBox = object_ptr<Ui::BoxContent>;\n\t\t\tconst auto stack = _lookup();\n\t\t\tif (!stack) {\n\t\t\t\treturn;\n\t\t\t} else if (auto layerWidget = std::get_if<UniqueLayer>(&layer)) {\n\t\t\t\tstack->showLayer(std::move(*layerWidget), options, animated);\n\t\t\t} else if (auto box = std::get_if<ObjectBox>(&layer)) {\n\t\t\t\tstack->showBox(std::move(*box), options, animated);\n\t\t\t} else {\n\t\t\t\tstack->hideAll(animated);\n\t\t\t}\n\t\t}\n\t\tnot_null<QWidget*> toastParent() const override {\n\t\t\treturn _parent.data();\n\t\t}\n\t\tbool valid() const override {\n\t\t\treturn _lookup() != nullptr;\n\t\t}\n\t\toperator bool() const override {\n\t\t\treturn valid();\n\t\t}\n\n\tprivate:\n\t\tconst QPointer<QWidget> _parent;\n\t\tconst Fn<Ui::LayerStackWidget*()> _lookup;\n\n\t};\n\n\tconst auto url = descriptor.url;\n\tconst auto wrap = descriptor.parent;\n\n\tstruct State {\n\t\tUi::LayerStackWidget *stack = nullptr;\n\t\trpl::event_stream<> destroyRequests;\n\t};\n\tconst auto state = wrap->lifetime().make_state<State>();\n\n\tconst auto weak = base::make_weak(wrap);\n\tconst auto lookup = crl::guard(weak, [state] { return state->stack; });\n\tconst auto layer = Ui::CreateChild<Ui::LayerStackWidget>(\n\t\twrap.get(),\n\t\t[=] { return std::make_shared<Show>(weak.get(), lookup); });\n\tstate->stack = layer;\n\tconst auto show = layer->showFactory()();\n\n\tlayer->setHideByBackgroundClick(false);\n\tlayer->move(0, 0);\n\twrap->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tlayer->resize(size);\n\t}, layer->lifetime());\n\tlayer->hideFinishEvents(\n\t) | rpl::filter([=] {\n\t\treturn !!lookup(); // Last hide finish is sent from destructor.\n\t}) | rpl::on_next([=] {\n\t\tstate->destroyRequests.fire({});\n\t}, wrap->lifetime());\n\n\tconst auto waiting = layer->lifetime().make_state<rpl::lifetime>();\n\tconst auto focus = crl::guard(layer, [=] {\n\t\tconst auto set = [=] {\n\t\t\tlayer->window()->setFocus();\n\t\t\tlayer->setInnerFocus();\n\t\t};\n\n\t\tconst auto handle = layer->window()->windowHandle();\n\t\tif (!handle) {\n\t\t\twaiting->destroy();\n\t\t\treturn;\n\t\t} else if (QGuiApplication::focusWindow() == handle) {\n\t\t\twaiting->destroy();\n\t\t\tset();\n\t\t} else {\n\t\t\t*waiting = base::qt_signal_producer(\n\t\t\t\tqApp,\n\t\t\t\t&QGuiApplication::focusWindowChanged\n\t\t\t) | rpl::filter([=](QWindow *focused) {\n\t\t\t\tconst auto handle = layer->window()->windowHandle();\n\t\t\t\treturn handle && (focused == handle);\n\t\t\t}) | rpl::on_next([=] {\n\t\t\t\twaiting->destroy();\n\t\t\t\tset();\n\t\t\t});\n\t\t\tlayer->window()->activateWindow();\n\t\t}\n\t});\n\tauto result = ShareBoxResult{\n\t\t.focus = focus,\n\t\t.hide = [=] { show->hideLayer(); },\n\t\t.destroyRequests = state->destroyRequests.events(),\n\t};\n\n\tFastShareLink(Main::MakeSessionShow(show, _session), url);\n\treturn result;\n}\n\nvoid Shown::createController() {\n\tExpects(!_controller);\n\n\tconst auto showShareBox = [=](ShareBoxDescriptor &&descriptor) {\n\t\treturn shareBox(std::move(descriptor));\n\t};\n\t_controller = std::make_unique<Controller>(\n\t\t_delegate,\n\t\tstd::move(showShareBox));\n\n\t_controller->events(\n\t) | rpl::start_to_stream(_events, _controller->lifetime());\n\n\t_controller->dataRequests(\n\t) | rpl::on_next([=](Webview::DataRequest request) {\n\t\tconst auto requested = QString::fromStdString(request.id);\n\t\tconst auto id = QStringView(requested);\n\t\tif (id.startsWith(u\"photo/\")) {\n\t\t\tstreamPhoto(id.mid(6), std::move(request));\n\t\t} else if (id.startsWith(u\"document/\"_q)) {\n\t\t\tstreamFile(id.mid(9), std::move(request));\n\t\t} else if (id.startsWith(u\"map/\"_q)) {\n\t\t\tstreamMap(id.mid(4).toUtf8(), std::move(request));\n\t\t} else if (id.startsWith(u\"html/\"_q)) {\n\t\t\tsendEmbed(id.mid(5).toUtf8(), std::move(request));\n\t\t}\n\t}, _controller->lifetime());\n}\n\nvoid Shown::showWindowed(Prepared result) {\n\tif (!_controller) {\n\t\tcreateController();\n\t}\n\n\t_controller->show(\n\t\t_session->local().resolveStorageIdOther(),\n\t\tstd::move(result),\n\t\tbase::duplicate(_inChannelValues));\n}\n\n::Data::FileOrigin Shown::fileOrigin(not_null<WebPageData*> page) const {\n\treturn ::Data::FileOriginWebPage{ page->url };\n}\n\nvoid Shown::streamPhoto(\n\t\tQStringView idWithPageId,\n\t\tWebview::DataRequest request) {\n\tusing namespace Data;\n\n\tconst auto parts = idWithPageId.split('/');\n\tif (parts.size() != 2) {\n\t\trequestFail(std::move(request));\n\t\treturn;\n\t}\n\tconst auto photo = _session->data().photo(parts[0].toULongLong());\n\tconst auto page = _session->data().webpage(parts[1].toULongLong());\n\tif (photo->isNull() || page->url.isEmpty()) {\n\t\trequestFail(std::move(request));\n\t\treturn;\n\t}\n\tconst auto media = photo->createMediaView();\n\tmedia->wanted(PhotoSize::Large, fileOrigin(page));\n\tconst auto check = [=] {\n\t\tif (!media->loaded() && !media->owner()->failed(PhotoSize::Large)) {\n\t\t\treturn false;\n\t\t}\n\t\trequestDone(\n\t\t\trequest,\n\t\t\tmedia->imageBytes(PhotoSize::Large),\n\t\t\t\"image/jpeg\");\n\t\treturn true;\n\t};\n\tif (!check()) {\n\t\tphoto->session().downloaderTaskFinished(\n\t\t) | rpl::filter(\n\t\t\tcheck\n\t\t) | rpl::take(1) | rpl::start(_controller->lifetime());\n\t}\n}\n\nvoid Shown::streamFile(\n\t\tQStringView idWithPageId,\n\t\tWebview::DataRequest request) {\n\tusing namespace Data;\n\n\tconst auto parts = idWithPageId.split('/');\n\tif (parts.size() != 2) {\n\t\trequestFail(std::move(request));\n\t\treturn;\n\t}\n\tconst auto documentId = DocumentId(parts[0].toULongLong());\n\tconst auto i = _streams.find(documentId);\n\tif (i != end(_streams)) {\n\t\tstreamFile(i->second, std::move(request));\n\t\treturn;\n\t}\n\tconst auto document = _session->data().document(documentId);\n\tconst auto page = _session->data().webpage(parts[1].toULongLong());\n\tif (page->url.isEmpty()) {\n\t\trequestFail(std::move(request));\n\t\treturn;\n\t}\n\tauto loader = document->createStreamingLoader(fileOrigin(page), false);\n\tif (!loader) {\n\t\tif (document->size >= Storage::kMaxFileInMemory) {\n\t\t\trequestFail(std::move(request));\n\t\t} else {\n\t\t\tauto media = document->createMediaView();\n\t\t\tif (const auto content = readFile(media); !content.isEmpty()) {\n\t\t\t\trequestDone(\n\t\t\t\t\tstd::move(request),\n\t\t\t\t\tcontent,\n\t\t\t\t\tdocument->mimeString().toStdString());\n\t\t\t} else {\n\t\t\t\tsubscribeToDocuments();\n\t\t\t\tauto &file = _files[documentId];\n\t\t\t\tfile.media = std::move(media);\n\t\t\t\tfile.requests.push_back(std::move(request));\n\t\t\t\tdocument->forceToCache(true);\n\t\t\t\tdocument->save(fileOrigin(page), QString());\n\t\t\t}\n\t\t}\n\t\treturn;\n\t}\n\tauto &file = _streams.emplace(\n\t\tdocumentId,\n\t\tFileStream{\n\t\t\t.document = document,\n\t\t\t.loader = std::move(loader),\n\t\t\t.mime = document->mimeString().toStdString(),\n\t\t}).first->second;\n\n\tfile.loader->parts(\n\t) | rpl::on_next([=](Media::Streaming::LoadedPart &&part) {\n\t\tconst auto i = _streams.find(documentId);\n\t\tAssert(i != end(_streams));\n\t\tprocessPartInFile(i->second, std::move(part));\n\t}, file.lifetime);\n\n\tstreamFile(file, std::move(request));\n}\n\nvoid Shown::streamFile(FileStream &file, Webview::DataRequest request) {\n\tconstexpr auto kPart = Media::Streaming::Loader::kPartSize;\n\tconst auto size = file.document->size;\n\tconst auto last = int((size + kPart - 1) / kPart);\n\tconst auto from = int(std::min(int64(request.offset), size) / kPart);\n\tconst auto till = (request.limit > 0)\n\t\t? std::min(int64(request.offset + request.limit), size)\n\t\t: size;\n\tconst auto parts = std::min(\n\t\tint((till + kPart - 1) / kPart) - from,\n\t\tkMaxLoadParts);\n\t//auto base = IvBaseCacheKey(document);\n\n\tconst auto length = std::min((from + parts) * kPart, size)\n\t\t- from * kPart;\n\tfile.requests.push_back(PartRequest{\n\t\t.request = std::move(request),\n\t\t.data = QByteArray(length, 0),\n\t\t.loaded = std::vector<bool>(parts, false),\n\t\t.offset = from * kPart,\n\t});\n\n\tfile.loader->resetPriorities();\n\tconst auto load = std::min(from + kKeepLoadingParts, last) - from;\n\tfor (auto i = 0; i != load; ++i) {\n\t\tfile.loader->load((from + i) * kPart);\n\t}\n}\n\nvoid Shown::subscribeToDocuments() {\n\tif (_documentLifetime) {\n\t\treturn;\n\t}\n\t_documentLifetime = _session->data().documentLoadProgress(\n\t) | rpl::filter([=](not_null<DocumentData*> document) {\n\t\treturn !document->loading();\n\t}) | rpl::on_next([=](not_null<DocumentData*> document) {\n\t\tconst auto i = _files.find(document->id);\n\t\tif (i == end(_files)) {\n\t\t\treturn;\n\t\t}\n\t\tauto requests = base::take(i->second.requests);\n\t\tconst auto content = readFile(i->second.media);\n\t\t_files.erase(i);\n\n\t\tif (!content.isEmpty()) {\n\t\t\tfor (auto &request : requests) {\n\t\t\t\trequestDone(\n\t\t\t\t\tstd::move(request),\n\t\t\t\t\tcontent,\n\t\t\t\t\tdocument->mimeString().toStdString());\n\t\t\t}\n\t\t} else {\n\t\t\tfor (auto &request : requests) {\n\t\t\t\trequestFail(std::move(request));\n\t\t\t}\n\t\t}\n\t});\n}\n\nQByteArray Shown::readFile(\n\t\tconst std::shared_ptr<::Data::DocumentMedia> &media) {\n\treturn Lottie::ReadContent(media->bytes(), media->owner()->filepath());\n}\n\nvoid Shown::processPartInFile(\n\t\tFileStream &file,\n\t\tMedia::Streaming::LoadedPart &&part) {\n\tfor (auto i = begin(file.requests); i != end(file.requests);) {\n\t\tif (finishRequestWithPart(*i, part)) {\n\t\t\tauto done = base::take(*i);\n\t\t\ti = file.requests.erase(i);\n\t\t\trequestDone(\n\t\t\t\tstd::move(done.request),\n\t\t\t\tdone.data,\n\t\t\t\tfile.mime,\n\t\t\t\tdone.offset,\n\t\t\t\tfile.document->size);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n}\n\nbool Shown::finishRequestWithPart(\n\t\tPartRequest &request,\n\t\tconst Media::Streaming::LoadedPart &part) {\n\tconst auto offset = part.offset;\n\tif (offset == Media::Streaming::LoadedPart::kFailedOffset) {\n\t\trequest.data = QByteArray();\n\t\treturn true;\n\t} else if (offset < request.offset\n\t\t|| offset >= request.offset + request.data.size()) {\n\t\treturn false;\n\t}\n\tconstexpr auto kPart = Media::Streaming::Loader::kPartSize;\n\tconst auto copy = std::min(\n\t\tint(part.bytes.size()),\n\t\tint(request.data.size() - (offset - request.offset)));\n\tconst auto index = (offset - request.offset) / kPart;\n\tAssert(index < request.loaded.size());\n\tif (request.loaded[index]) {\n\t\treturn false;\n\t}\n\trequest.loaded[index] = true;\n\tmemcpy(\n\t\trequest.data.data() + index * kPart,\n\t\tpart.bytes.constData(),\n\t\tcopy);\n\treturn !ranges::contains(request.loaded, false);\n}\n\nvoid Shown::streamMap(QString params, Webview::DataRequest request) {\n\tusing namespace ::Data;\n\n\tconst auto parts = params.split(u'&');\n\tif (parts.size() != 3) {\n\t\trequestFail(std::move(request));\n\t\treturn;\n\t}\n\tconst auto point = GeoPointFromId(parts[0].toUtf8());\n\tconst auto size = parts[1].split(',');\n\tconst auto zoom = parts[2].toInt();\n\tif (size.size() != 2) {\n\t\trequestFail(std::move(request));\n\t\treturn;\n\t}\n\tconst auto location = GeoPointLocation{\n\t\t.lat = point.lat,\n\t\t.lon = point.lon,\n\t\t.access = point.access,\n\t\t.width = size[0].toInt(),\n\t\t.height = size[1].toInt(),\n\t\t.zoom = std::max(zoom, kGeoPointZoomMin),\n\t\t.scale = kGeoPointScale,\n\t};\n\tconst auto prepared = ImageWithLocation{\n\t\t.location = ImageLocation(\n\t\t\t{ location },\n\t\t\tlocation.width,\n\t\t\tlocation.height)\n\t};\n\tauto &preview = _maps.emplace(params, MapPreview()).first->second;\n\tpreview.file = std::make_unique<CloudFile>();\n\n\tUpdateCloudFile(\n\t\t*preview.file,\n\t\tprepared,\n\t\t_session->data().cache(),\n\t\tkImageCacheTag,\n\t\t[=](FileOrigin origin) { /* restartLoader not used here */ });\n\tconst auto autoLoading = false;\n\tconst auto finalCheck = [=] { return true; };\n\tconst auto done = [=](QByteArray bytes) {\n\t\tconst auto i = _maps.find(params);\n\t\tAssert(i != end(_maps));\n\t\ti->second.bytes = std::move(bytes);\n\t\trequestDone(request, i->second.bytes, \"image/png\");\n\t};\n\tLoadCloudFile(\n\t\t_session,\n\t\t*preview.file,\n\t\tFileOrigin(),\n\t\tLoadFromCloudOrLocal,\n\t\tautoLoading,\n\t\tkImageCacheTag,\n\t\tfinalCheck,\n\t\tdone,\n\t\t[=](bool) { done(\"failed...\"); });\n}\n\nvoid Shown::sendEmbed(QByteArray hash, Webview::DataRequest request) {\n\tconst auto i = _embeds.find(hash);\n\tif (i != end(_embeds)) {\n\t\trequestDone(std::move(request), i->second, \"text/html; charset=utf-8\");\n\t} else {\n\t\trequestFail(std::move(request));\n\t}\n}\n\nvoid Shown::requestDone(\n\t\tWebview::DataRequest request,\n\t\tQByteArray bytes,\n\t\tstd::string mime,\n\t\tint64 offset,\n\t\tint64 total) {\n\tif (bytes.isEmpty() && mime.empty()) {\n\t\trequestFail(std::move(request));\n\t\treturn;\n\t}\n\tcrl::on_main([\n\t\tdone = std::move(request.done),\n\t\tdata = std::move(bytes),\n\t\tmime = std::move(mime),\n\t\toffset,\n\t\ttotal\n\t] {\n\t\tusing namespace Webview;\n\t\tdone({\n\t\t\t.stream = std::make_unique<DataStreamFromMemory>(data, mime),\n\t\t\t.streamOffset = offset,\n\t\t\t.totalSize = total,\n\t\t});\n\t});\n}\n\nvoid Shown::requestFail(Webview::DataRequest request) {\n\tcrl::on_main([done = std::move(request.done)] {\n\t\tdone({});\n\t});\n}\n\nbool Shown::showing(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<Data*> data) const {\n\treturn showingFrom(session) && (_id == data->id());\n}\n\nbool Shown::showingFrom(not_null<Main::Session*> session) const {\n\treturn (_session == session);\n}\n\nbool Shown::activeFor(not_null<Main::Session*> session) const {\n\treturn showingFrom(session) && _controller;\n}\n\nbool Shown::active() const {\n\treturn _controller && _controller->active();\n}\n\nvoid Shown::moveTo(not_null<Data*> data, QString hash) {\n\tprepare(data, hash);\n}\n\nvoid Shown::update(not_null<Data*> data) {\n\tconst auto weak = base::make_weak(this);\n\n\tconst auto id = data->id();\n\tdata->prepare({}, [=](Prepared result) {\n\t\tcrl::on_main(weak, [=, result = std::move(result)]() mutable {\n\t\t\tresult.url = id;\n\t\t\tfillChannelJoinedValues(result);\n\t\t\tfillEmbeds(std::move(result.embeds));\n\t\t\tif (_controller) {\n\t\t\t\t_controller->update(std::move(result));\n\t\t\t}\n\t\t});\n\t});\n}\n\nvoid Shown::showJoinedTooltip() {\n\tif (_controller) {\n\t\t_controller->showJoinedTooltip();\n\t}\n}\n\nvoid Shown::minimize() {\n\tif (_controller) {\n\t\t_controller->minimize();\n\t}\n}\n\nTonSite::TonSite(not_null<Delegate*> delegate, QString uri)\n: _delegate(delegate)\n, _uri(uri) {\n\tshowWindowed();\n}\n\nvoid TonSite::createController() {\n\tExpects(!_controller);\n\n\tconst auto showShareBox = [=](ShareBoxDescriptor &&descriptor) {\n\t\treturn ShareBoxResult();\n\t};\n\t_controller = std::make_unique<Controller>(\n\t\t_delegate,\n\t\tstd::move(showShareBox));\n\n\t_controller->events(\n\t) | rpl::start_to_stream(_events, _controller->lifetime());\n}\n\nvoid TonSite::showWindowed() {\n\tif (!_controller) {\n\t\tcreateController();\n\t}\n\n\t_controller->showTonSite(Storage::TonSiteStorageId(), _uri);\n}\n\nbool TonSite::active() const {\n\treturn _controller && _controller->active();\n}\n\nvoid TonSite::moveTo(QString uri) {\n\t_controller->showTonSite({}, uri);\n}\n\nvoid TonSite::minimize() {\n\tif (_controller) {\n\t\t_controller->minimize();\n\t}\n}\n\nInstance::Instance(not_null<Delegate*> delegate) : _delegate(delegate) {\n}\n\nInstance::~Instance() = default;\n\nvoid Instance::show(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Data*> data,\n\t\tQString hash) {\n\tshow(controller->uiShow(), data, hash);\n}\n\nvoid Instance::show(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<Data*> data,\n\t\tQString hash) {\n\tthis->show(&show->session(), data, hash);\n}\n\nvoid Instance::show(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<Data*> data,\n\t\tQString hash) {\n\tif (Platform::IsMac()) {\n\t\t// Otherwise IV is not visible under the media viewer.\n\t\tCore::App().hideMediaView();\n\t}\n\n\tif (Core::App().settings().normalizeIvZoom()) {\n\t\tCore::App().saveSettingsDelayed();\n\t}\n\n\tconst auto guard = gsl::finally([&] {\n\t\trequestFull(session, data->id());\n\t});\n\tif (_shown && _shownSession == session) {\n\t\t_shown->moveTo(data, hash);\n\t\treturn;\n\t}\n\t_shown = std::make_unique<Shown>(_delegate, session, data, hash);\n\t_shownSession = session;\n\t_shown->events() | rpl::on_next([=](Controller::Event event) {\n\t\tusing Type = Controller::Event::Type;\n\t\tconst auto lower = event.url.toLower();\n\t\tconst auto urlChecked = lower.startsWith(\"http://\")\n\t\t\t|| lower.startsWith(\"https://\");\n\t\tconst auto tonsite = lower.startsWith(\"tonsite://\");\n\t\tswitch (event.type) {\n\t\tcase Type::Close:\n\t\t\t_shown = nullptr;\n\t\t\tbreak;\n\t\tcase Type::Quit:\n\t\t\tShortcuts::Launch(Shortcuts::Command::Quit);\n\t\t\tbreak;\n\t\tcase Type::OpenChannel:\n\t\t\tprocessOpenChannel(event.context);\n\t\t\tbreak;\n\t\tcase Type::JoinChannel:\n\t\t\tprocessJoinChannel(event.context);\n\t\t\tbreak;\n\t\tcase Type::OpenLinkExternal:\n\t\t\tif (urlChecked) {\n\t\t\t\tFile::OpenUrl(event.url);\n\t\t\t\tcloseAll();\n\t\t\t} else if (tonsite) {\n\t\t\t\tshowTonSite(event.url);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase Type::OpenMedia:\n\t\t\tif (const auto window = Core::App().activeWindow()) {\n\t\t\t\tconst auto current = window->sessionController();\n\t\t\t\tconst auto controller = (current\n\t\t\t\t\t&& &current->session() == _shownSession)\n\t\t\t\t\t? current\n\t\t\t\t\t: nullptr;\n\t\t\t\tconst auto item = (HistoryItem*)nullptr;\n\t\t\t\tconst auto topicRootId = MsgId(0);\n\t\t\t\tconst auto monoforumPeerId = PeerId(0);\n\t\t\t\tif (event.context.startsWith(\"-photo\")) {\n\t\t\t\t\tconst auto id = event.context.mid(6).toULongLong();\n\t\t\t\t\tconst auto photo = _shownSession->data().photo(id);\n\t\t\t\t\tif (!photo->isNull()) {\n\t\t\t\t\t\twindow->openInMediaView({\n\t\t\t\t\t\t\tcontroller,\n\t\t\t\t\t\t\tphoto,\n\t\t\t\t\t\t\titem,\n\t\t\t\t\t\t\ttopicRootId,\n\t\t\t\t\t\t\tmonoforumPeerId\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} else if (event.context.startsWith(\"-video\")) {\n\t\t\t\t\tconst auto id = event.context.mid(6).toULongLong();\n\t\t\t\t\tconst auto video = _shownSession->data().document(id);\n\t\t\t\t\tif (!video->isNull()) {\n\t\t\t\t\t\twindow->openInMediaView({\n\t\t\t\t\t\t\tcontroller,\n\t\t\t\t\t\t\tvideo,\n\t\t\t\t\t\t\titem,\n\t\t\t\t\t\t\ttopicRootId,\n\t\t\t\t\t\t\tmonoforumPeerId\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\tcase Type::OpenPage:\n\t\tcase Type::OpenLink: {\n\t\t\tif (tonsite) {\n\t\t\t\tshowTonSite(event.url);\n\t\t\t\tbreak;\n\t\t\t} else if (!urlChecked) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst auto session = _shownSession;\n\t\t\tconst auto url = event.url;\n\t\t\tauto &requested = _fullRequested[session][url];\n\t\t\trequested.lastRequestedAt = crl::now();\n\t\t\tsession->api().request(MTPmessages_GetWebPage(\n\t\t\t\tMTP_string(url),\n\t\t\t\tMTP_int(requested.hash)\n\t\t\t)).done([=](const MTPmessages_WebPage &result) {\n\t\t\t\tconst auto page = processReceivedPage(session, url, result);\n\t\t\t\tif (page && page->iv) {\n\t\t\t\t\tconst auto parts = event.url.split('#');\n\t\t\t\t\tconst auto hash = (parts.size() > 1) ? parts[1] : u\"\"_q;\n\t\t\t\t\tthis->show(_shownSession, page->iv.get(), hash);\n\t\t\t\t} else {\n\t\t\t\t\tUrlClickHandler::Open(event.url);\n\t\t\t\t}\n\t\t\t}).fail([=] {\n\t\t\t\tUrlClickHandler::Open(event.url);\n\t\t\t}).send();\n\t\t} break;\n\t\tcase Type::Report:\n\t\t\tif (const auto controller = _shownSession->tryResolveWindow()) {\n\t\t\t\tcontroller->window().activate();\n\t\t\t\tcontroller->showPeerByLink(Window::PeerByLinkInfo{\n\t\t\t\t\t.usernameOrId = \"previews\",\n\t\t\t\t\t.resolveType = Window::ResolveType::BotStart,\n\t\t\t\t\t.startToken = (\"webpage\"\n\t\t\t\t\t\t+ QString::number(event.context.toULongLong())),\n\t\t\t\t});\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}, _shown->lifetime());\n\n\tsession->changes().peerUpdates(\n\t\t::Data::PeerUpdate::Flag::ChannelAmIn\n\t) | rpl::on_next([=](const ::Data::PeerUpdate &update) {\n\t\tif (const auto channel = update.peer->asChannel()) {\n\t\t\tif (channel->amIn()) {\n\t\t\t\tconst auto i = _joining.find(session);\n\t\t\t\tconst auto value = not_null{ channel };\n\t\t\t\tif (i != end(_joining) && i->second.remove(value)) {\n\t\t\t\t\t_shown->showJoinedTooltip();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, _shown->lifetime());\n\n\ttrackSession(session);\n}\n\nvoid Instance::trackSession(not_null<Main::Session*> session) {\n\tif (!_tracking.emplace(session).second) {\n\t\treturn;\n\t}\n\tsession->lifetime().add([=] {\n\t\t_tracking.remove(session);\n\t\t_joining.remove(session);\n\t\t_fullRequested.remove(session);\n\t\t_ivCache.remove(session);\n\t\tif (_ivRequestSession == session) {\n\t\t\tsession->api().request(_ivRequestId).cancel();\n\t\t\t_ivRequestSession = nullptr;\n\t\t\t_ivRequestUri = QString();\n\t\t\t_ivRequestId = 0;\n\t\t}\n\t\tif (_shownSession == session) {\n\t\t\t_shownSession = nullptr;\n\t\t}\n\t\tif (_shown && _shown->showingFrom(session)) {\n\t\t\t_shown = nullptr;\n\t\t}\n\t});\n}\n\nvoid Instance::openWithIvPreferred(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tQString uri,\n\t\tQVariant context) {\n\tauto my = context.value<ClickHandlerContext>();\n\tmy.sessionWindow = controller;\n\topenWithIvPreferred(\n\t\t&controller->session(),\n\t\turi,\n\t\tQVariant::fromValue(my));\n}\n\nvoid Instance::openWithIvPreferred(\n\t\tnot_null<Main::Session*> session,\n\t\tQString uri,\n\t\tQVariant context) {\n\tconst auto openExternal = [=] {\n\t\tauto my = context.value<ClickHandlerContext>();\n\t\tmy.ignoreIv = true;\n\t\tUrlClickHandler::Open(uri, QVariant::fromValue(my));\n\t};\n\tconst auto parts = uri.split('#');\n\tif (parts.isEmpty() || parts[0].isEmpty()) {\n\t\treturn;\n\t} else if (!ShowButton()) {\n\t\treturn openExternal();\n\t}\n\ttrackSession(session);\n\tconst auto hash = (parts.size() > 1) ? parts[1] : u\"\"_q;\n\tconst auto url = parts[0];\n\tauto &cache = _ivCache[session];\n\tif (const auto i = cache.find(url); i != end(cache)) {\n\t\tconst auto page = i->second;\n\t\tif (page && page->iv) {\n\t\t\tauto my = context.value<ClickHandlerContext>();\n\t\t\tif (const auto window = my.sessionWindow.get()) {\n\t\t\t\tshow(window, page->iv.get(), hash);\n\t\t\t} else {\n\t\t\t\tshow(session, page->iv.get(), hash);\n\t\t\t}\n\t\t} else {\n\t\t\topenExternal();\n\t\t}\n\t\treturn;\n\t} else if (_ivRequestSession == session.get() && _ivRequestUri == uri) {\n\t\treturn;\n\t} else if (_ivRequestId) {\n\t\t_ivRequestSession->api().request(_ivRequestId).cancel();\n\t}\n\tconst auto finish = [=](WebPageData *page) {\n\t\tExpects(_ivRequestSession == session);\n\n\t\t_ivRequestId = 0;\n\t\t_ivRequestUri = QString();\n\t\t_ivRequestSession = nullptr;\n\t\t_ivCache[session][url] = page;\n\t\topenWithIvPreferred(session, uri, context);\n\t};\n\t_ivRequestSession = session;\n\t_ivRequestUri = uri;\n\tauto &requested = _fullRequested[session][url];\n\trequested.lastRequestedAt = crl::now();\n\t_ivRequestId = session->api().request(MTPmessages_GetWebPage(\n\t\tMTP_string(url),\n\t\tMTP_int(requested.hash)\n\t)).done([=](const MTPmessages_WebPage &result) {\n\t\tfinish(processReceivedPage(session, url, result));\n\t}).fail([=] {\n\t\tfinish(nullptr);\n\t}).send();\n}\n\nvoid Instance::showTonSite(\n\t\tconst QString &uri,\n\t\tQVariant context) {\n\tif (!Controller::IsGoodTonSiteUrl(uri)) {\n\t\tUi::Toast::Show(tr::lng_iv_not_supported(tr::now));\n\t\treturn;\n\t} else if (Platform::IsMac()) {\n\t\t// Otherwise IV is not visible under the media viewer.\n\t\tCore::App().hideMediaView();\n\t}\n\tif (_tonSite) {\n\t\t_tonSite->moveTo(uri);\n\t\treturn;\n\t}\n\t_tonSite = std::make_unique<TonSite>(_delegate, uri);\n\t_tonSite->events() | rpl::on_next([=](Controller::Event event) {\n\t\tusing Type = Controller::Event::Type;\n\t\tconst auto lower = event.url.toLower();\n\t\tconst auto urlChecked = lower.startsWith(\"http://\")\n\t\t\t|| lower.startsWith(\"https://\");\n\t\tconst auto tonsite = lower.startsWith(\"tonsite://\");\n\t\tswitch (event.type) {\n\t\tcase Type::Close:\n\t\t\t_tonSite = nullptr;\n\t\t\tbreak;\n\t\tcase Type::Quit:\n\t\t\tShortcuts::Launch(Shortcuts::Command::Quit);\n\t\t\tbreak;\n\t\tcase Type::OpenLinkExternal:\n\t\t\tif (urlChecked) {\n\t\t\t\tFile::OpenUrl(event.url);\n\t\t\t\tcloseAll();\n\t\t\t} else if (tonsite) {\n\t\t\t\tshowTonSite(event.url);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase Type::OpenPage:\n\t\tcase Type::OpenLink:\n\t\t\tif (urlChecked) {\n\t\t\t\tUrlClickHandler::Open(event.url);\n\t\t\t} else if (tonsite) {\n\t\t\t\tshowTonSite(event.url);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}, _tonSite->lifetime());\n}\n\nvoid Instance::requestFull(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &id) {\n\tif (!_tracking.contains(session)) {\n\t\treturn;\n\t}\n\tauto &requested = _fullRequested[session][id];\n\tconst auto last = requested.lastRequestedAt;\n\tconst auto now = crl::now();\n\tif (last && (now - last) < kAllowPageReloadAfter) {\n\t\treturn;\n\t}\n\trequested.lastRequestedAt = now;\n\tsession->api().request(MTPmessages_GetWebPage(\n\t\tMTP_string(id),\n\t\tMTP_int(requested.hash)\n\t)).done([=](const MTPmessages_WebPage &result) {\n\t\tconst auto page = processReceivedPage(session, id, result);\n\t\tif (page && page->iv && _shown && _shownSession == session) {\n\t\t\t_shown->update(page->iv.get());\n\t\t}\n\t}).send();\n}\n\nWebPageData *Instance::processReceivedPage(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &url,\n\t\tconst MTPmessages_WebPage &result) {\n\tconst auto &data = result.data();\n\tconst auto owner = &session->data();\n\towner->processUsers(data.vusers());\n\towner->processChats(data.vchats());\n\tauto &requested = _fullRequested[session][url];\n\tconst auto &mtp = data.vwebpage();\n\tmtp.match([&](const MTPDwebPageNotModified &data) {\n\t\tconst auto page = requested.page;\n\t\tif (const auto views = data.vcached_page_views()) {\n\t\t\tif (page && page->iv) {\n\t\t\t\tpage->iv->updateCachedViews(views->v);\n\t\t\t}\n\t\t}\n\t}, [&](const MTPDwebPage &data) {\n\t\trequested.hash = data.vhash().v;\n\t\trequested.page = owner->processWebpage(data).get();\n\t}, [&](const auto &) {\n\t\trequested.page = owner->processWebpage(mtp).get();\n\t});\n\treturn requested.page;\n}\n\nvoid Instance::processOpenChannel(const QString &context) {\n\tif (!_shownSession) {\n\t\treturn;\n\t} else if (const auto channelId = ChannelId(context.toLongLong())) {\n\t\tconst auto channel = _shownSession->data().channel(channelId);\n\t\tif (channel->isLoaded()) {\n\t\t\tif (const auto controller = _shownSession->tryResolveWindow(channel)) {\n\t\t\t\tcontroller->showPeerHistory(channel);\n\t\t\t\t_shown = nullptr;\n\t\t\t}\n\t\t} else if (!channel->username().isEmpty()) {\n\t\t\tif (const auto controller = _shownSession->tryResolveWindow(channel)) {\n\t\t\t\tcontroller->showPeerByLink({\n\t\t\t\t\t.usernameOrId = channel->username(),\n\t\t\t\t});\n\t\t\t\t_shown = nullptr;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Instance::processJoinChannel(const QString &context) {\n\tif (!_shownSession) {\n\t\treturn;\n\t} else if (const auto channelId = ChannelId(context.toLongLong())) {\n\t\tconst auto channel = _shownSession->data().channel(channelId);\n\t\t_joining[_shownSession].emplace(channel);\n\t\tif (channel->isLoaded()) {\n\t\t\t_shownSession->api().joinChannel(channel);\n\t\t} else if (!channel->username().isEmpty()) {\n\t\t\tif (const auto controller = _shownSession->tryResolveWindow(channel)) {\n\t\t\t\tcontroller->showPeerByLink({\n\t\t\t\t\t.usernameOrId = channel->username(),\n\t\t\t\t\t.joinChannel = true,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n}\n\nbool Instance::hasActiveWindow(not_null<Main::Session*> session) const {\n\treturn _shown && _shown->activeFor(session);\n}\n\nbool Instance::closeActive() {\n\tif (_shown && _shown->active()) {\n\t\t_shown = nullptr;\n\t\treturn true;\n\t} else if (_tonSite && _tonSite->active()) {\n\t\t_tonSite = nullptr;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool Instance::minimizeActive() {\n\tif (_shown && _shown->active()) {\n\t\t_shown->minimize();\n\t\treturn true;\n\t} else if (_tonSite && _tonSite->active()) {\n\t\t_tonSite->minimize();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Instance::closeAll() {\n\t_shown = nullptr;\n\t_tonSite = nullptr;\n}\n\nbool PreferForUri(const QString &uri) {\n\tconst auto url = QUrl(uri);\n\tconst auto host = url.host().toLower();\n\tconst auto path = url.path().toLower();\n\treturn (host == u\"telegra.ph\"_q)\n\t\t|| (host == u\"te.legra.ph\"_q)\n\t\t|| (host == u\"graph.org\"_q)\n\t\t|| (host == u\"telegram.org\"_q\n\t\t\t&& (path.startsWith(u\"/faq\"_q)\n\t\t\t\t|| path.startsWith(u\"/privacy\"_q)\n\t\t\t\t|| path.startsWith(u\"/blog\"_q)));\n}\n\n} // namespace Iv\n"
  },
  {
    "path": "Telegram/SourceFiles/iv/iv_instance.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"iv/iv_delegate.h\"\n\nnamespace Main {\nclass Session;\nclass SessionShow;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Iv {\n\nclass Data;\nclass Shown;\nclass TonSite;\n\nclass Instance final {\npublic:\n\texplicit Instance(not_null<Delegate*> delegate);\n\t~Instance();\n\n\tvoid show(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Data*> data,\n\t\tQString hash);\n\tvoid show(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<Data*> data,\n\t\tQString hash);\n\tvoid show(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<Data*> data,\n\t\tQString hash);\n\n\tvoid openWithIvPreferred(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tQString uri,\n\t\tQVariant context = {});\n\tvoid openWithIvPreferred(\n\t\tnot_null<Main::Session*> session,\n\t\tQString uri,\n\t\tQVariant context = {});\n\n\tvoid showTonSite(\n\t\tconst QString &uri,\n\t\tQVariant context = {});\n\n\t[[nodiscard]] bool hasActiveWindow(\n\t\tnot_null<Main::Session*> session) const;\n\n\tbool closeActive();\n\tbool minimizeActive();\n\n\tvoid closeAll();\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tstruct FullResult {\n\t\tcrl::time lastRequestedAt = 0;\n\t\tWebPageData *page = nullptr;\n\t\tint32 hash = 0;\n\t};\n\n\tvoid processOpenChannel(const QString &context);\n\tvoid processJoinChannel(const QString &context);\n\tvoid requestFull(not_null<Main::Session*> session, const QString &id);\n\n\tvoid trackSession(not_null<Main::Session*> session);\n\n\tWebPageData *processReceivedPage(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &url,\n\t\tconst MTPmessages_WebPage &result);\n\n\tconst not_null<Delegate*> _delegate;\n\n\tstd::unique_ptr<Shown> _shown;\n\tMain::Session *_shownSession = nullptr;\n\tbase::flat_set<not_null<Main::Session*>> _tracking;\n\tbase::flat_map<\n\t\tnot_null<Main::Session*>,\n\t\tbase::flat_set<not_null<ChannelData*>>> _joining;\n\tbase::flat_map<\n\t\tnot_null<Main::Session*>,\n\t\tbase::flat_map<QString, FullResult>> _fullRequested;\n\n\tbase::flat_map<\n\t\tnot_null<Main::Session*>,\n\t\tbase::flat_map<QString, WebPageData*>> _ivCache;\n\tMain::Session *_ivRequestSession = nullptr;\n\tQString _ivRequestUri;\n\tmtpRequestId _ivRequestId = 0;\n\n\tstd::unique_ptr<TonSite> _tonSite;\n\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] bool PreferForUri(const QString &uri);\n\n} // namespace Iv\n"
  },
  {
    "path": "Telegram/SourceFiles/iv/iv_pch.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\n#include <QtCore/QString>\n#include <QtCore/QByteArray>\n\n#include <crl/crl.h>\n#include <rpl/rpl.h>\n\n#include <vector>\n#include <map>\n#include <set>\n#include <deque>\n#include <atomic>\n\n#include <range/v3/all.hpp>\n\n#include \"base/flat_map.h\"\n#include \"base/flat_set.h\"\n#include \"base/weak_qptr.h\"\n\n#include \"ui/qt_object_factory.h\"\n\n#include \"scheme.h\"\n#include \"logs.h\"\n"
  },
  {
    "path": "Telegram/SourceFiles/iv/iv_prepare.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"iv/iv_prepare.h\"\n\n#include \"base/openssl_help.h\"\n#include \"base/unixtime.h\"\n#include \"iv/iv_data.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/grouped_layout.h\"\n#include \"styles/palette.h\"\n#include \"styles/style_chat.h\"\n\n#include <QtCore/QSize>\n\nnamespace Iv {\nnamespace {\n\nstruct Attribute {\n\tQByteArray name;\n\tstd::optional<QByteArray> value;\n};\nusing Attributes = std::vector<Attribute>;\n\nstruct Photo {\n\tuint64 id = 0;\n\tint width = 0;\n\tint height = 0;\n\tQByteArray minithumbnail;\n};\n\nstruct Document {\n\tuint64 id = 0;\n\tint width = 0;\n\tint height = 0;\n\tQByteArray minithumbnail;\n};\n\ntemplate <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>\n[[nodiscard]] QByteArray Number(T value) {\n\treturn QByteArray::number(value);\n}\n\ntemplate <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>\n[[nodiscard]] QByteArray Percent(T value) {\n\treturn Number(base::SafeRound(value * 10000.) / 100.);\n};\n\n[[nodiscard]] QByteArray Escape(QByteArray value) {\n\tauto result = QByteArray();\n\tresult.reserve(value.size());\n\tfor (const auto &ch : value) {\n\t\tswitch (ch) {\n\t\tcase '&': result.append(\"&amp;\"); break;\n\t\tcase '<': result.append(\"&lt;\"); break;\n\t\tcase '>': result.append(\"&gt;\"); break;\n\t\tcase '\"': result.append(\"&quot;\"); break;\n\t\tcase '\\'': result.append(\"&apos;\"); break;\n\t\tdefault: result.append(ch); break;\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] QByteArray Date(TimeId date) {\n\treturn Escape(langDateTimeFull(base::unixtime::parse(date)).toUtf8());\n}\n\nclass Parser final {\npublic:\n\tParser(const Source &source, const Options &options);\n\n\t[[nodiscard]] Prepared result();\n\nprivate:\n\tvoid process(const Source &source);\n\tvoid process(const MTPPhoto &photo);\n\tvoid process(const MTPDocument &document);\n\n\ttemplate <typename Inner>\n\t[[nodiscard]] QByteArray list(const MTPVector<Inner> &data);\n\n\t[[nodiscard]] QByteArray collage(\n\t\tconst QVector<MTPPageBlock> &list,\n\t\tconst std::vector<QSize> &dimensions,\n\t\tint offset = 0);\n\t[[nodiscard]] QByteArray slideshow(\n\t\tconst QVector<MTPPageBlock> &list,\n\t\tQSize dimensions);\n\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockUnsupported &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockTitle &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockSubtitle &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockAuthorDate &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockHeader &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockSubheader &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockParagraph &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockPreformatted &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockFooter &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockDivider &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockAnchor &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockList &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockBlockquote &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockPullquote &data);\n\t[[nodiscard]] QByteArray block(\n\t\tconst MTPDpageBlockPhoto &data,\n\t\tconst Ui::GroupMediaLayout &layout = {},\n\t\tQSize outer = {});\n\t[[nodiscard]] QByteArray block(\n\t\tconst MTPDpageBlockVideo &data,\n\t\tconst Ui::GroupMediaLayout &layout = {},\n\t\tQSize outer = {});\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockCover &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockEmbed &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockEmbedPost &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockCollage &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockSlideshow &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockChannel &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockAudio &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockKicker &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockTable &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockOrderedList &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockDetails &data);\n\t[[nodiscard]] QByteArray block(\n\t\tconst MTPDpageBlockRelatedArticles &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageBlockMap &data);\n\n\t[[nodiscard]] QByteArray block(const MTPDpageRelatedArticle &data);\n\n\t[[nodiscard]] QByteArray block(const MTPDpageTableRow &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageTableCell &data);\n\n\t[[nodiscard]] QByteArray block(const MTPDpageListItemText &data);\n\t[[nodiscard]] QByteArray block(const MTPDpageListItemBlocks &data);\n\n\t[[nodiscard]] QByteArray block(const MTPDpageListOrderedItemText &data);\n\t[[nodiscard]] QByteArray block(\n\t\tconst MTPDpageListOrderedItemBlocks &data);\n\n\t[[nodiscard]] QByteArray wrap(const QByteArray &content, int views);\n\n\t[[nodiscard]] QByteArray tag(\n\t\tconst QByteArray &name,\n\t\tconst QByteArray &body = {});\n\t[[nodiscard]] QByteArray tag(\n\t\tconst QByteArray &name,\n\t\tconst Attributes &attributes,\n\t\tconst QByteArray &body = {});\n\t[[nodiscard]] QByteArray utf(const MTPstring &text);\n\t[[nodiscard]] QByteArray utf(const tl::conditional<MTPstring> &text);\n\t[[nodiscard]] QByteArray rich(const MTPRichText &text);\n\t[[nodiscard]] QByteArray caption(const MTPPageCaption &caption);\n\n\t[[nodiscard]] Photo parse(const MTPPhoto &photo);\n\t[[nodiscard]] Document parse(const MTPDocument &document);\n\t[[nodiscard]] Geo parse(const MTPGeoPoint &geo);\n\n\t[[nodiscard]] Photo photoById(uint64 id);\n\t[[nodiscard]] Document documentById(uint64 id);\n\n\t[[nodiscard]] QByteArray photoFullUrl(const Photo &photo);\n\t[[nodiscard]] QByteArray documentFullUrl(const Document &document);\n\t[[nodiscard]] QByteArray embedUrl(const QByteArray &html);\n\t[[nodiscard]] QByteArray mapUrl(\n\t\tconst Geo &geo,\n\t\tint width,\n\t\tint height,\n\t\tint zoom);\n\t[[nodiscard]] QByteArray resource(QByteArray id);\n\n\t[[nodiscard]] std::vector<QSize> computeCollageDimensions(\n\t\tconst QVector<MTPPageBlock> &items);\n\t[[nodiscard]] QSize computeSlideshowDimensions(\n\t\tconst QVector<MTPPageBlock> &items);\n\n\t//const Options _options;\n\tconst QByteArray _fileOriginPostfix;\n\n\tbase::flat_set<QByteArray> _resources;\n\n\tPrepared _result;\n\n\tbase::flat_map<uint64, Photo> _photosById;\n\tbase::flat_map<uint64, Document> _documentsById;\n\n};\n\n[[nodiscard]] bool IsVoidElement(const QByteArray &name) {\n\t// Thanks https://developer.mozilla.org/en-US/docs/Glossary/Void_element\n\tstatic const auto voids = base::flat_set<QByteArray>{\n\t\t\"area\"_q,\n\t\t\"base\"_q,\n\t\t\"br\"_q,\n\t\t\"col\"_q,\n\t\t\"embed\"_q,\n\t\t\"hr\"_q,\n\t\t\"img\"_q,\n\t\t\"input\"_q,\n\t\t\"link\"_q,\n\t\t\"meta\"_q,\n\t\t\"source\"_q,\n\t\t\"track\"_q,\n\t\t\"wbr\"_q,\n\t};\n\treturn voids.contains(name);\n}\n\n[[nodiscard]] QByteArray ArrowSvg(bool left) {\n\tconst auto rotate = QByteArray(left ? \"180\" : \"0\");\n\treturn R\"(\n<svg viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n\t<path\n\t\td=\"M14.9972363,18 L9.13865768,12.1414214 C9.06055283,12.0633165 9.06055283,11.9366835 9.13865768,11.8585786 L14.9972363,6 L14.9972363,6\"\n\t\ttransform=\"translate(11.997236, 12) scale(-1, -1) rotate()\" + rotate + \") translate(-11.997236, -12)\" + R\"(\">\n\t</path>\n</svg>)\";\n}\n\nParser::Parser(const Source &source, const Options &options)\n: /*_options(options)\n, */_fileOriginPostfix('/' + Number(source.pageId)) {\n\tprocess(source);\n\t_result.pageId = source.pageId;\n\t_result.name = source.name;\n\t_result.rtl = source.page.data().is_rtl();\n\n\tconst auto views = std::max(\n\t\tsource.page.data().vviews().value_or_empty(),\n\t\tsource.updatedCachedViews);\n\tconst auto content = list(source.page.data().vblocks());\n\t_result.content = wrap(content, views);\n}\n\nPrepared Parser::result() {\n\treturn _result;\n}\n\nvoid Parser::process(const Source &source) {\n\tconst auto &data = source.page.data();\n\tfor (const auto &photo : data.vphotos().v) {\n\t\tprocess(photo);\n\t}\n\tfor (const auto &document : data.vdocuments().v) {\n\t\tprocess(document);\n\t}\n\tif (source.webpagePhoto) {\n\t\tprocess(*source.webpagePhoto);\n\t}\n\tif (source.webpageDocument) {\n\t\tprocess(*source.webpageDocument);\n\t}\n}\n\nvoid Parser::process(const MTPPhoto &photo) {\n\t_photosById.emplace(\n\t\tphoto.match([](const auto &data) { return data.vid().v; }),\n\t\tparse(photo));\n}\n\nvoid Parser::process(const MTPDocument &document) {\n\t_documentsById.emplace(\n\t\tdocument.match([](const auto &data) { return data.vid().v; }),\n\t\tparse(document));\n}\n\ntemplate <typename Inner>\nQByteArray Parser::list(const MTPVector<Inner> &data) {\n\tauto result = QByteArrayList();\n\tresult.reserve(data.v.size());\n\tfor (const auto &item : data.v) {\n\t\tresult.append(item.match([&](const auto &data) {\n\t\t\treturn block(data);\n\t\t}));\n\t}\n\treturn result.join(QByteArray());\n}\n\nQByteArray Parser::collage(\n\t\tconst QVector<MTPPageBlock> &list,\n\t\tconst std::vector<QSize> &dimensions,\n\t\tint offset) {\n\tExpects(list.size() == dimensions.size());\n\n\tconstexpr auto kPerCollage = 10;\n\tconst auto last = (offset + kPerCollage >= int(dimensions.size()));\n\n\tauto result = QByteArray();\n\tauto slice = ((offset > 0) || (dimensions.size() > kPerCollage))\n\t\t? (dimensions\n\t\t\t| ranges::views::drop(offset)\n\t\t\t| ranges::views::take(kPerCollage)\n\t\t\t| ranges::to_vector)\n\t\t: dimensions;\n\tconst auto layout = Ui::LayoutMediaGroup(\n\t\tslice,\n\t\tst::historyGroupWidthMax,\n\t\tst::historyGroupWidthMin,\n\t\tst::historyGroupSkip);\n\tauto size = QSize();\n\tfor (const auto &part : layout) {\n\t\tconst auto &rect = part.geometry;\n\t\tsize = QSize(\n\t\t\tstd::max(size.width(), rect.x() + rect.width()),\n\t\t\tstd::max(size.height(), rect.y() + rect.height()));\n\t}\n\tfor (auto i = 0, count = int(layout.size()); i != count; ++i) {\n\t\tconst auto &part = layout[i];\n\t\tlist[offset + i].match([&](const MTPDpageBlockPhoto &data) {\n\t\t\tresult += block(data, part, size);\n\t\t}, [&](const MTPDpageBlockVideo &data) {\n\t\t\tresult += block(data, part, size);\n\t\t}, [](const auto &) {\n\t\t\tUnexpected(\"Block type in collage layout.\");\n\t\t});\n\t}\n\tconst auto aspectHeight = size.height() / float64(size.width());\n\tconst auto aspectSkip = st::historyGroupSkip / float64(size.width());\n\tauto wrapped = tag(\"figure\", {\n\t\t{ \"class\", \"collage\" },\n\t\t{\n\t\t\t\"style\",\n\t\t\t(\"padding-top: \" + Percent(aspectHeight) + \"%; \"\n\t\t\t\t+ \"margin-bottom: \" + Percent(last ? 0 : aspectSkip) + \"%;\")\n\t\t},\n\t}, result);\n\tif (offset + kPerCollage < int(dimensions.size())) {\n\t\twrapped += collage(list, dimensions, offset + kPerCollage);\n\t}\n\treturn wrapped;\n}\n\nQByteArray Parser::slideshow(\n\t\tconst QVector<MTPPageBlock> &list,\n\t\tQSize dimensions) {\n\tauto result = QByteArray();\n\tfor (auto i = 0, count = int(list.size()); i != count; ++i) {\n\t\tlist[i].match([&](const MTPDpageBlockPhoto &data) {\n\t\t\tresult += block(data, {}, dimensions);\n\t\t}, [&](const MTPDpageBlockVideo &data) {\n\t\t\tresult += block(data, {}, dimensions);\n\t\t}, [](const auto &) {\n\t\t\tUnexpected(\"Block type in collage layout.\");\n\t\t});\n\t}\n\n\tauto inputs = QByteArrayList();\n\tfor (auto i = 0; i != int(list.size()); ++i) {\n\t\tauto attributes = Attributes{\n\t\t\t{ \"type\", \"radio\" },\n\t\t\t{ \"name\", \"s\" },\n\t\t\t{ \"value\", Number(i) },\n\t\t\t{ \"onchange\", \"return IV.slideshowSlide(this);\" },\n\t\t};\n\t\tif (!i) {\n\t\t\tattributes.push_back({ \"checked\", std::nullopt });\n\t\t}\n\t\tinputs.append(tag(\"label\", tag(\"input\", attributes, tag(\"i\"))));\n\t}\n\tconst auto form = tag(\n\t\t\"form\",\n\t\t{ { \"class\", \"slideshow-buttons\" } },\n\t\ttag(\"fieldset\", inputs.join(QByteArray())));\n\tconst auto navigation = tag(\"a\", {\n\t\t{ \"class\", \"slideshow-prev\" },\n\t\t{ \"onclick\", \"IV.slideshowSlide(this, -1);\" },\n\t}, ArrowSvg(true)) + tag(\"a\", {\n\t\t{ \"class\", \"slideshow-next\" },\n\t\t{ \"onclick\", \"IV.slideshowSlide(this, 1);\" },\n\t}, ArrowSvg(false));\n\tauto wrapStyle = \"padding-top: calc(min(\"\n\t\t+ Percent(dimensions.height() / float64(dimensions.width()))\n\t\t+ \"%, 480px));\";\n\tresult = form + tag(\"figure\", {\n\t\t{ \"class\", \"slideshow\" },\n\t}, result) + navigation;\n\treturn tag(\"figure\", {\n\t\t{ \"class\", \"slideshow-wrap\" },\n\t\t{ \"style\", wrapStyle },\n\t}, result);\n}\n\nQByteArray Parser::block(const MTPDpageBlockUnsupported &data) {\n\treturn QByteArray();\n}\n\nQByteArray Parser::block(const MTPDpageBlockTitle &data) {\n\treturn tag(\"h1\", {\n\t\t{ \"class\", \"title\" },\n\t\t{ \"dir\", \"auto\" },\n\t}, rich(data.vtext()));\n}\n\nQByteArray Parser::block(const MTPDpageBlockSubtitle &data) {\n\treturn tag(\"h2\", {\n\t\t{ \"class\", \"subtitle\" },\n\t\t{ \"dir\", \"auto\" },\n\t}, rich(data.vtext()));\n}\n\nQByteArray Parser::block(const MTPDpageBlockAuthorDate &data) {\n\tauto inner = rich(data.vauthor());\n\tif (const auto date = data.vpublished_date().v) {\n\t\tinner += \" \\xE2\\x80\\xA2 \" + tag(\"time\", Date(date));\n\t}\n\treturn tag(\"address\", { { \"dir\", \"auto\" } }, inner);\n}\n\nQByteArray Parser::block(const MTPDpageBlockHeader &data) {\n\treturn tag(\"h3\", {\n\t\t{ \"class\", \"header\" },\n\t\t{ \"dir\", \"auto\" },\n\t}, rich(data.vtext()));\n}\n\nQByteArray Parser::block(const MTPDpageBlockSubheader &data) {\n\treturn tag(\"h4\", {\n\t\t{ \"class\", \"subheader\" },\n\t\t{ \"dir\", \"auto\" },\n\t}, rich(data.vtext()));\n}\n\nQByteArray Parser::block(const MTPDpageBlockParagraph &data) {\n\treturn tag(\"p\", { { \"dir\", \"auto\" } }, rich(data.vtext()));\n}\n\nQByteArray Parser::block(const MTPDpageBlockPreformatted &data) {\n\tauto list = Attributes{ { \"dir\", \"auto\" } };\n\tconst auto language = utf(data.vlanguage());\n\tif (!language.isEmpty()) {\n\t\tlist.push_back({ \"data-language\", language });\n\t\tlist.push_back({ \"class\", \"lang-\" + language });\n\t\t_result.hasCode = true;\n\t}\n\treturn tag(\"pre\", list, rich(data.vtext()));\n}\n\nQByteArray Parser::block(const MTPDpageBlockFooter &data) {\n\treturn tag(\"footer\", {\n\t\t{ \"class\", \"footer\" },\n\t\t{ \"dir\", \"auto\" },\n\t}, rich(data.vtext()));\n}\n\nQByteArray Parser::block(const MTPDpageBlockDivider &data) {\n\treturn tag(\"hr\", Attributes{ { \"class\", \"divider\" } });\n}\n\nQByteArray Parser::block(const MTPDpageBlockAnchor &data) {\n\treturn tag(\"a\", { { \"name\", utf(data.vname()) } });\n}\n\nQByteArray Parser::block(const MTPDpageBlockList &data) {\n\treturn tag(\"ul\", list(data.vitems()));\n}\n\nQByteArray Parser::block(const MTPDpageBlockBlockquote &data) {\n\tconst auto caption = rich(data.vcaption());\n\tconst auto cite = [&] {\n\t\tif (caption.isEmpty()) {\n\t\t\treturn QByteArray();\n\t\t} else {\n\t\t\treturn tag(\"cite\", { { \"dir\", \"auto\" } }, caption);\n\t\t}\n\t}();\n\treturn tag(\"blockquote\", {\n\t\t{ \"dir\", \"auto\" }\n\t}, rich(data.vtext()) + cite);\n}\n\nQByteArray Parser::block(const MTPDpageBlockPullquote &data) {\n\tconst auto caption = rich(data.vcaption());\n\tconst auto cite = [&] {\n\t\tif (caption.isEmpty()) {\n\t\t\treturn QByteArray();\n\t\t} else {\n\t\t\treturn tag(\"cite\", { { \"dir\", \"auto\" } }, caption);\n\t\t}\n\t}();\n\treturn tag(\"div\", {\n\t\t{ \"class\", \"pullquote\" },\n\t\t{ \"dir\", \"auto\" },\n\t}, rich(data.vtext()) + cite);\n}\n\nQByteArray Parser::block(\n\t\tconst MTPDpageBlockPhoto &data,\n\t\tconst Ui::GroupMediaLayout &layout,\n\t\tQSize outer) {\n\tconst auto collage = !layout.geometry.isEmpty();\n\tconst auto slideshow = !collage && !outer.isEmpty();\n\tconst auto photo = photoById(data.vphoto_id().v);\n\tif (!photo.id) {\n\t\treturn \"Photo not found.\";\n\t}\n\tconst auto src = photoFullUrl(photo);\n\tauto wrapStyle = QByteArray();\n\tif (collage) {\n\t\tconst auto wcoef = 1. / outer.width();\n\t\tconst auto hcoef = 1. / outer.height();\n\t\twrapStyle += \"left: \" + Percent(layout.geometry.x() * wcoef) + \"%; \"\n\t\t\t+ \"top: \" + Percent(layout.geometry.y() * hcoef) + \"%; \"\n\t\t\t+ \"width: \" + Percent(layout.geometry.width() * wcoef) + \"%; \"\n\t\t\t+ \"height: \" + Percent(layout.geometry.height() * hcoef) + \"%\";\n\t} else if (!slideshow && photo.width) {\n\t\twrapStyle += \"max-width:\" + Number(photo.width) + \"px\";\n\t}\n\tconst auto dimension = collage\n\t\t? (layout.geometry.height() / float64(layout.geometry.width()))\n\t\t: (photo.width && photo.height)\n\t\t? (photo.height / float64(photo.width))\n\t\t: (3 / 4.);\n\tconst auto paddingTop = collage\n\t\t? Percent(dimension) + \"%\"\n\t\t: \"calc(min(480px, \" + Percent(dimension) + \"%))\";\n\tconst auto style = \"background-image:url('\" + src + \"');\"\n\t\t\"padding-top: \" + paddingTop + \";\";\n\tauto inner = tag(\"div\", {\n\t\t{ \"class\", \"photo\" },\n\t\t{ \"style\", style } });\n\tconst auto minithumb = Images::ExpandInlineBytes(photo.minithumbnail);\n\tif (!minithumb.isEmpty()) {\n\t\tconst auto image = Images::Read({ .content = minithumb });\n\t\tinner = tag(\"div\", {\n\t\t\t{ \"class\", \"photo-bg\" },\n\t\t\t{ \"style\", \"background-image:url('data:image/jpeg;base64,\"\n\t\t\t\t+ minithumb.toBase64()\n\t\t\t\t+ \"');\" },\n\t\t}) + inner;\n\t}\n\tauto attributes = Attributes{\n\t\t{ \"class\", \"photo-wrap\" },\n\t\t{ \"style\", wrapStyle }\n\t};\n\tauto result = tag(\"div\", attributes, inner);\n\n\tconst auto href = data.vurl() ? utf(*data.vurl()) : photoFullUrl(photo);\n\tconst auto id = Number(photo.id);\n\tresult = tag(\"a\", {\n\t\t{ \"href\", href },\n\t\t{ \"oncontextmenu\", data.vurl() ? QByteArray() : \"return false;\" },\n\t\t{ \"data-context\", data.vurl() ? QByteArray() : \"viewer-photo\" + id },\n\t}, result);\n\tif (!slideshow) {\n\t\tresult += caption(data.vcaption());\n\t\tif (!collage) {\n\t\t\tresult = tag(\"div\", { { \"class\", \"media-outer\" } }, result);\n\t\t}\n\t}\n\treturn result;\n}\n\nQByteArray Parser::block(\n\t\tconst MTPDpageBlockVideo &data,\n\t\tconst Ui::GroupMediaLayout &layout,\n\t\tQSize outer) {\n\tconst auto collage = !layout.geometry.isEmpty();\n\tconst auto slideshow = !collage && !outer.isEmpty();\n\tconst auto collageSmall = collage\n\t\t&& (layout.geometry.width() < outer.width());\n\tconst auto video = documentById(data.vvideo_id().v);\n\tif (!video.id) {\n\t\treturn \"Video not found.\";\n\t}\n\tauto inner = tag(\"div\", {\n\t\t{ \"class\", \"video\" },\n\t\t{ \"data-src\", documentFullUrl(video) },\n\t\t{ \"data-autoplay\", data.is_autoplay() ? \"1\" : \"0\" },\n\t\t{ \"data-loop\", data.is_loop() ? \"1\" : \"0\" },\n\t\t{ \"data-small\", collageSmall ? \"1\" : \"0\" },\n\t});\n\tconst auto minithumb = Images::ExpandInlineBytes(video.minithumbnail);\n\tif (!minithumb.isEmpty()) {\n\t\tconst auto image = Images::Read({ .content = minithumb });\n\t\tinner = tag(\"div\", {\n\t\t\t{ \"class\", \"video-bg\" },\n\t\t\t{ \"style\", \"background-image:url('data:image/jpeg;base64,\"\n\t\t\t\t+ minithumb.toBase64()\n\t\t\t\t+ \"');\" },\n\t\t}) + inner;\n\t}\n\tauto wrapStyle = QByteArray();\n\tif (collage) {\n\t\tconst auto wcoef = 1. / outer.width();\n\t\tconst auto hcoef = 1. / outer.height();\n\t\twrapStyle += \"left: \" + Percent(layout.geometry.x() * wcoef) + \"%; \"\n\t\t\t+ \"top: \" + Percent(layout.geometry.y() * hcoef) + \"%; \"\n\t\t\t+ \"width: \" + Percent(layout.geometry.width() * wcoef) + \"%; \"\n\t\t\t+ \"height: \" + Percent(layout.geometry.height() * hcoef) + \"%; \";\n\t} else {\n\t\tconst auto dimension = (video.width && video.height)\n\t\t\t? (video.height / float64(video.width))\n\t\t\t: (3 / 4.);\n\t\twrapStyle += \"padding-top: calc(min(480px, \"\n\t\t\t+ Percent(dimension)\n\t\t\t+ \"%));\";\n\t}\n\tauto attributes = Attributes{\n\t\t{ \"class\", \"video-wrap\" },\n\t\t{ \"style\", wrapStyle },\n\t};\n\tauto result = tag(\"div\", attributes, inner);\n\tif (data.is_autoplay() || collageSmall) {\n\t\tconst auto id = Number(video.id);\n\t\tconst auto href = resource(\"video\" + id);\n\t\tresult = tag(\"a\", {\n\t\t\t{ \"href\", href },\n\t\t\t{ \"oncontextmenu\", \"return false;\" },\n\t\t\t{ \"data-context\", \"viewer-video\" + id },\n\t\t}, result);\n\t}\n\tif (!slideshow) {\n\t\tresult += caption(data.vcaption());\n\t\tif (!collage) {\n\t\t\tresult = tag(\"div\", { { \"class\", \"media-outer\" } }, result);\n\t\t}\n\t}\n\treturn result;\n}\n\nQByteArray Parser::block(const MTPDpageBlockCover &data) {\n\treturn tag(\"figure\", data.vcover().match([&](const auto &data) {\n\t\treturn block(data);\n\t}));\n}\n\nQByteArray Parser::block(const MTPDpageBlockEmbed &data) {\n\t_result.hasEmbeds = true;\n\tauto eclass = data.is_full_width() ? QByteArray() : \"nowide\";\n\tauto width = QByteArray();\n\tauto height = QByteArray();\n\tauto iframeWidth = QByteArray();\n\tauto iframeHeight = QByteArray();\n\tconst auto autosize = !data.vw();\n\tif (autosize) {\n\t\tiframeWidth = \"100%\";\n\t\teclass = \"nowide\";\n\t} else if (data.is_full_width() || !data.vw()->v) {\n\t\twidth = \"100%\";\n\t\theight = Number(data.vh()->v) + \"px\";\n\t\tiframeWidth = \"100%\";\n\t\tiframeHeight = height;\n\t} else {\n\t\twidth = Number(data.vw()->v) + \"px\";\n\t\theight = Percent(data.vh()->v / float64(data.vw()->v)) + \"%\";\n\t}\n\tauto attributes = Attributes();\n\tif (autosize) {\n\t\tattributes.push_back({ \"class\", \"autosize\" });\n\t}\n\tattributes.push_back({ \"width\", iframeWidth });\n\tattributes.push_back({ \"height\", iframeHeight });\n\tif (const auto url = data.vurl()) {\n\t\tif (!autosize) {\n\t\t\tattributes.push_back({ \"src\", utf(url) });\n\t\t} else {\n\t\t\tattributes.push_back({ \"srcdoc\", utf(url) });\n\t\t}\n\t} else if (const auto html = data.vhtml()) {\n\t\tattributes.push_back({ \"src\", embedUrl(html->v) });\n\t}\n\tif (!data.is_allow_scrolling()) {\n\t\tattributes.push_back({ \"scrolling\", \"no\" });\n\t}\n\tattributes.push_back({ \"frameborder\", \"0\" });\n\tattributes.push_back({ \"allowtransparency\", \"true\" });\n\tattributes.push_back({ \"allowfullscreen\", \"true\" });\n\tauto result = tag(\"iframe\", attributes);\n\tif (!autosize) {\n\t\tresult = tag(\"div\", {\n\t\t\t{ \"class\", \"iframe-wrap\" },\n\t\t\t{ \"style\", \"width:\" + width },\n\t\t}, tag(\"div\", {\n\t\t\t{ \"style\", \"padding-bottom: \" + height },\n\t\t}, result));\n\t}\n\tresult += caption(data.vcaption());\n\treturn tag(\"figure\", { { \"class\", eclass } }, result);\n}\n\nQByteArray Parser::block(const MTPDpageBlockEmbedPost &data) {\n\tauto result = QByteArray();\n\tif (!data.vblocks().v.isEmpty()) {\n\t\tauto address = QByteArray();\n\t\tconst auto photo = photoById(data.vauthor_photo_id().v);\n\t\tif (photo.id) {\n\t\t\tconst auto src = photoFullUrl(photo);\n\t\t\taddress += tag(\n\t\t\t\t\"figure\",\n\t\t\t\t{ { \"style\", \"background-image:url('\" + src + \"')\" } });\n\t\t}\n\t\taddress += tag(\n\t\t\t\"a\",\n\t\t\t{ { \"rel\", \"author\" }, { \"onclick\", \"return false;\" } },\n\t\t\tutf(data.vauthor()));\n\t\tif (const auto date = data.vdate().v) {\n\t\t\tconst auto parsed = base::unixtime::parse(date);\n\t\t\taddress += tag(\"time\", Date(date));\n\t\t}\n\t\tconst auto inner = tag(\"address\", address) + list(data.vblocks());\n\t\tresult = tag(\"blockquote\", { { \"class\", \"embed-post\" } }, inner);\n\t} else {\n\t\tconst auto url = utf(data.vurl());\n\t\tconst auto inner = tag(\"strong\", utf(data.vauthor()))\n\t\t\t+ tag(\n\t\t\t\t\"small\",\n\t\t\t\ttag(\"a\", { { \"href\", url } }, url));\n\t\tresult = tag(\"section\", { { \"class\", \"embed-post\" } }, inner);\n\t}\n\tresult += caption(data.vcaption());\n\treturn tag(\"figure\", result);\n}\n\nQByteArray Parser::block(const MTPDpageBlockCollage &data) {\n\tconst auto &items = data.vitems().v;\n\tconst auto dimensions = computeCollageDimensions(items);\n\tif (dimensions.empty()) {\n\t\treturn tag(\n\t\t\t\"figure\",\n\t\t\ttag(\"figure\", list(data.vitems())) + caption(data.vcaption()));\n\t}\n\n\treturn tag(\n\t\t\"figure\",\n\t\t{ { \"class\", \"collage-wrap\" } },\n\t\tcollage(items, dimensions) + caption(data.vcaption()));\n}\n\nQByteArray Parser::block(const MTPDpageBlockSlideshow &data) {\n\tconst auto &items = data.vitems().v;\n\tconst auto dimensions = computeSlideshowDimensions(items);\n\tif (dimensions.isEmpty()) {\n\t\treturn list(data.vitems());\n\t}\n\tconst auto result = slideshow(items, dimensions);\n\treturn tag(\"figure\", result + caption(data.vcaption()));\n}\n\nQByteArray Parser::block(const MTPDpageBlockChannel &data) {\n\tauto name = QByteArray();\n\tauto username = QByteArray();\n\tauto id = data.vchannel().match([](const auto &data) {\n\t\treturn Number(data.vid().v);\n\t});\n\tdata.vchannel().match([&](const MTPDchannel &data) {\n\t\tif (const auto has = data.vusername()) {\n\t\t\tusername = utf(*has);\n\t\t}\n\t\tname = utf(data.vtitle());\n\t}, [&](const MTPDchat &data) {\n\t\tname = utf(data.vtitle());\n\t}, [](const auto &) {\n\t});\n\tauto result = tag(\n\t\t\"div\",\n\t\t{ { \"class\", \"join\" }, { \"data-context\", \"join_link\" + id } },\n\t\ttag(\"span\")\n\t) + tag(\"h4\", name);\n\tconst auto link = username.isEmpty()\n\t\t? \"javascript:alert('Channel Link');\"\n\t\t: \"https://t.me/\" + username;\n\tresult = tag(\n\t\t\"a\",\n\t\t{ { \"href\", link }, { \"data-context\", \"channel\" + id } },\n\t\tresult);\n\t_result.channelIds.emplace(id);\n\treturn tag(\"section\", {\n\t\t{ \"class\", \"channel joined\" },\n\t\t{ \"data-context\", \"channel\" + id },\n\t}, result);\n}\n\nQByteArray Parser::block(const MTPDpageBlockAudio &data) {\n\tconst auto audio = documentById(data.vaudio_id().v);\n\tif (!audio.id) {\n\t\treturn \"Audio not found.\";\n\t}\n\tconst auto src = documentFullUrl(audio);\n\treturn tag(\"figure\", tag(\"audio\", {\n\t\t{ \"src\", src },\n\t\t{ \"oncontextmenu\", \"return false;\" },\n\t\t{ \"controls\", std::nullopt },\n\t}) + caption(data.vcaption()));\n}\n\nQByteArray Parser::block(const MTPDpageBlockKicker &data) {\n\treturn tag(\"h5\", {\n\t\t{ \"class\", \"kicker\" },\n\t\t{ \"dir\", \"auto\" },\n\t}, rich(data.vtext()));\n}\n\nQByteArray Parser::block(const MTPDpageBlockTable &data) {\n\tauto classes = QByteArrayList();\n\tif (data.is_bordered()) {\n\t\tclasses.push_back(\"bordered\");\n\t}\n\tif (data.is_striped()) {\n\t\tclasses.push_back(\"striped\");\n\t}\n\tauto attibutes = Attributes();\n\tif (!classes.isEmpty()) {\n\t\tattibutes.push_back({ \"class\", classes.join(\" \") });\n\t}\n\tauto title = rich(data.vtitle());\n\tif (!title.isEmpty()) {\n\t\ttitle = tag(\"caption\", { { \"dir\", \"auto\" } }, title);\n\t}\n\tauto result = tag(\"table\", attibutes, title + list(data.vrows()));\n\tresult = tag(\"figure\", { { \"class\", \"table\" } }, result);\n\tresult = tag(\"figure\", { { \"class\", \"table-wrap\" } }, result);\n\treturn tag(\"figure\", result);\n}\n\nQByteArray Parser::block(const MTPDpageBlockOrderedList &data) {\n\treturn tag(\"ol\", list(data.vitems()));\n}\n\nQByteArray Parser::block(const MTPDpageBlockDetails &data) {\n\tauto attributes = Attributes();\n\tif (data.is_open()) {\n\t\tattributes.push_back({ \"open\", std::nullopt });\n\t}\n\treturn tag(\n\t\t\"details\",\n\t\tattributes,\n\t\t(tag(\"summary\", { { \"dir\", \"auto\" } }, rich(data.vtitle()))\n\t\t\t+ list(data.vblocks())));\n}\n\nQByteArray Parser::block(const MTPDpageBlockRelatedArticles &data) {\n\tconst auto result = list(data.varticles());\n\tif (result.isEmpty()) {\n\t\treturn QByteArray();\n\t}\n\tauto title = rich(data.vtitle());\n\tif (!title.isEmpty()) {\n\t\ttitle = tag(\"h4\", {\n\t\t\t{ \"class\", \"related-title\" },\n\t\t\t{ \"dir\", \"auto\" },\n\t\t}, title);\n\t}\n\treturn tag(\"section\", { { \"class\", \"related\" } }, title + result);\n}\n\nQByteArray Parser::block(const MTPDpageBlockMap &data) {\n\tconst auto geo = parse(data.vgeo());\n\tif (!geo.access) {\n\t\treturn \"Map not found.\";\n\t}\n\tconst auto width = 650;\n\tconst auto height = std::min(450, (data.vh().v * width / data.vw().v));\n\treturn tag(\"figure\", tag(\"img\", {\n\t\t{ \"src\", mapUrl(geo, width, height, data.vzoom().v) },\n\t}) + caption(data.vcaption()));\n}\n\nQByteArray Parser::block(const MTPDpageRelatedArticle &data) {\n\tauto result = QByteArray();\n\tconst auto photo = photoById(data.vphoto_id().value_or_empty());\n\tif (photo.id) {\n\t\tconst auto src = photoFullUrl(photo);\n\t\tresult += tag(\"i\", {\n\t\t\t{ \"class\", \"related-link-thumb\" },\n\t\t\t{ \"style\", \"background-image:url('\" + src + \"')\" },\n\t\t});\n\t}\n\tconst auto title = data.vtitle();\n\tconst auto description = data.vdescription();\n\tconst auto author = data.vauthor();\n\tconst auto published = data.vpublished_date();\n\tif (title || description || author || published) {\n\t\tauto inner = QByteArray();\n\t\tif (title) {\n\t\t\tinner += tag(\n\t\t\t\t\"span\",\n\t\t\t\t{ { \"class\", \"related-link-title\" } },\n\t\t\t\tutf(*title));\n\t\t}\n\t\tif (description) {\n\t\t\tinner += tag(\n\t\t\t\t\"span\",\n\t\t\t\t{ { \"class\", \"related-link-desc\" } },\n\t\t\t\tutf(*description));\n\t\t}\n\t\tif (author || published) {\n\t\t\tinner += tag(\n\t\t\t\t\"span\",\n\t\t\t\t{ { \"class\", \"related-link-source\" } },\n\t\t\t\t((author ? utf(*author) : QByteArray())\n\t\t\t\t\t+ ((author && published) ? \", \" : QByteArray())\n\t\t\t\t\t+ (published ? Date(published->v) : QByteArray())));\n\t\t}\n\t\tresult += tag(\"span\", {\n\t\t\t{ \"class\", \"related-link-content\" },\n\t\t}, inner);\n\t}\n\tconst auto webpageId = data.vwebpage_id().v;\n\tconst auto context = webpageId\n\t\t? (\"webpage\" + Number(webpageId))\n\t\t: QByteArray();\n\treturn tag(\"a\", {\n\t\t{ \"class\", \"related-link\" },\n\t\t{ \"href\", utf(data.vurl()) },\n\t\t{ \"data-context\", context },\n\t}, result);\n}\n\nQByteArray Parser::block(const MTPDpageTableRow &data) {\n\treturn tag(\"tr\", list(data.vcells()));\n}\n\nQByteArray Parser::block(const MTPDpageTableCell &data) {\n\tconst auto text = data.vtext() ? rich(*data.vtext()) : QByteArray();\n\tauto style = QByteArray();\n\tif (data.is_align_right()) {\n\t\tstyle += \"text-align:right;\";\n\t} else if (data.is_align_center()) {\n\t\tstyle += \"text-align:center;\";\n\t} else {\n\t\tstyle += \"text-align:left;\";\n\t}\n\tif (data.is_valign_bottom()) {\n\t\tstyle += \"vertical-align:bottom;\";\n\t} else if (data.is_valign_middle()) {\n\t\tstyle += \"vertical-align:middle;\";\n\t} else {\n\t\tstyle += \"vertical-align:top;\";\n\t}\n\tauto attributes = Attributes{ { \"style\", style }, { \"dir\", \"auto\" } };\n\tif (const auto cs = data.vcolspan()) {\n\t\tattributes.push_back({ \"colspan\", Number(cs->v) });\n\t}\n\tif (const auto rs = data.vrowspan()) {\n\t\tattributes.push_back({ \"rowspan\", Number(rs->v) });\n\t}\n\treturn tag(data.is_header() ? \"th\" : \"td\", attributes, text);\n}\n\nQByteArray Parser::block(const MTPDpageListItemText &data) {\n\treturn tag(\"li\", { { \"dir\", \"auto\" } }, rich(data.vtext()));\n}\n\nQByteArray Parser::block(const MTPDpageListItemBlocks &data) {\n\treturn tag(\"li\", list(data.vblocks()));\n}\n\nQByteArray Parser::block(const MTPDpageListOrderedItemText &data) {\n\treturn tag(\n\t\t\"li\",\n\t\t{ { \"value\", utf(data.vnum()) }, { \"dir\", \"auto\" } },\n\t\trich(data.vtext()));\n}\n\nQByteArray Parser::block(const MTPDpageListOrderedItemBlocks &data) {\n\treturn tag(\n\t\t\"li\",\n\t\t{ { \"value\", utf(data.vnum()) } },\n\t\tlist(data.vblocks()));\n}\n\nQByteArray Parser::utf(const MTPstring &text) {\n\treturn Escape(text.v);\n}\n\nQByteArray Parser::utf(const tl::conditional<MTPstring> &text) {\n\treturn text ? utf(*text) : QByteArray();\n}\n\nQByteArray Parser::wrap(const QByteArray &content, int views) {\n\tconst auto sep = \" \\xE2\\x80\\xA2 \";\n\tconst auto viewsText = views\n\t\t? (tr::lng_stories_views(tr::now, lt_count_decimal, views) + sep)\n\t\t: QString();\n\treturn R\"(\n<div class=\"page-slide\">\n\t<article>)\"_q + content + R\"(</article>\n</div>\n<div class=\"page-footer\">\n\t<div class=\"content\">\n\t\t)\"_q\n\t\t+ viewsText.toUtf8()\n\t\t+ R\"(<a class=\"wrong\" data-context=\"report-iv\">)\"_q\n\t\t+ tr::lng_iv_wrong_layout(tr::now).toUtf8()\n\t\t+ R\"(</a>\n\t</div>\n</div>)\"_q;\n}\n\nQByteArray Parser::tag(\n\t\tconst QByteArray &name,\n\t\tconst QByteArray &body) {\n\treturn tag(name, {}, body);\n}\n\nQByteArray Parser::tag(\n\t\tconst QByteArray &name,\n\t\tconst Attributes &attributes,\n\t\tconst QByteArray &body) {\n\tauto list = QByteArrayList();\n\tlist.reserve(attributes.size());\n\tfor (auto &[name, value] : attributes) {\n\t\tlist.push_back(' ' + name + (value ? \"=\\\"\" + *value + \"\\\"\" : \"\"));\n\t}\n\tconst auto serialized = list.join(QByteArray());\n\treturn (IsVoidElement(name) && body.isEmpty())\n\t\t? ('<' + name + serialized + \" />\")\n\t\t: ('<' + name + serialized + '>' + body + \"</\" + name + '>');\n}\n\nQByteArray Parser::rich(const MTPRichText &text) {\n\treturn text.match([&](const MTPDtextEmpty &data) {\n\t\treturn QByteArray();\n\t}, [&](const MTPDtextPlain &data) {\n\t\tstruct Replacement {\n\t\t\tQByteArray from;\n\t\t\tQByteArray to;\n\t\t};\n\t\tconst auto replacements = std::vector<Replacement>{\n\t\t\t{ \"\\xE2\\x81\\xA6\", \"<span dir=\\\"ltr\\\">\" },\n\t\t\t{ \"\\xE2\\x81\\xA7\", \"<span dir=\\\"rtl\\\">\" },\n\t\t\t{ \"\\xE2\\x81\\xA8\", \"<span dir=\\\"auto\\\">\" },\n\t\t\t{ \"\\xE2\\x81\\xA9\", \"</span>\" },\n\t\t};\n\t\tauto text = utf(data.vtext());\n\t\tfor (const auto &[from, to] : replacements) {\n\t\t\ttext.replace(from, to);\n\t\t}\n\t\treturn text;\n\t}, [&](const MTPDtextConcat &data) {\n\t\tconst auto &list = data.vtexts().v;\n\t\tauto result = QByteArrayList();\n\t\tresult.reserve(list.size());\n\t\tfor (const auto &item : list) {\n\t\t\tresult.append(rich(item));\n\t\t}\n\t\treturn result.join(QByteArray());\n\t}, [&](const MTPDtextImage &data) {\n\t\tconst auto image = documentById(data.vdocument_id().v);\n\t\tif (!image.id) {\n\t\t\treturn \"Image not found.\"_q;\n\t\t}\n\t\tauto attributes = Attributes{\n\t\t\t{ \"class\", \"pic\" },\n\t\t\t{ \"src\", documentFullUrl(image) },\n\t\t};\n\t\tif (const auto width = data.vw().v) {\n\t\t\tattributes.push_back({ \"width\", Number(width) });\n\t\t}\n\t\tif (const auto height = data.vh().v) {\n\t\t\tattributes.push_back({ \"height\", Number(height) });\n\t\t}\n\t\treturn tag(\"img\", attributes);\n\t}, [&](const MTPDtextBold &data) {\n\t\treturn tag(\"b\", rich(data.vtext()));\n\t}, [&](const MTPDtextItalic &data) {\n\t\treturn tag(\"i\", rich(data.vtext()));\n\t}, [&](const MTPDtextUnderline &data) {\n\t\treturn tag(\"u\", rich(data.vtext()));\n\t}, [&](const MTPDtextStrike &data) {\n\t\treturn tag(\"s\", rich(data.vtext()));\n\t}, [&](const MTPDtextFixed &data) {\n\t\treturn tag(\"code\", rich(data.vtext()));\n\t}, [&](const MTPDtextUrl &data) {\n\t\tconst auto webpageId = data.vwebpage_id().v;\n\t\tconst auto context = webpageId\n\t\t\t? (\"webpage\" + Number(webpageId))\n\t\t\t: QByteArray();\n\t\treturn tag(\"a\", {\n\t\t\t{ \"href\", utf(data.vurl()) },\n\t\t\t{ \"class\", webpageId ? \"internal-iv-link\" : \"\" },\n\t\t\t{ \"data-context\", context },\n\t\t}, rich(data.vtext()));\n\t}, [&](const MTPDtextEmail &data) {\n\t\treturn tag(\"a\", {\n\t\t\t{ \"href\", \"mailto:\" + utf(data.vemail()) },\n\t\t}, rich(data.vtext()));\n\t}, [&](const MTPDtextSubscript &data) {\n\t\treturn tag(\"sub\", rich(data.vtext()));\n\t}, [&](const MTPDtextSuperscript &data) {\n\t\treturn tag(\"sup\", rich(data.vtext()));\n\t}, [&](const MTPDtextMarked &data) {\n\t\treturn tag(\"mark\", rich(data.vtext()));\n\t}, [&](const MTPDtextPhone &data) {\n\t\treturn tag(\"a\", {\n\t\t\t{ \"href\", \"tel:\" + utf(data.vphone()) },\n\t\t}, rich(data.vtext()));\n\t}, [&](const MTPDtextAnchor &data) {\n\t\tconst auto inner = rich(data.vtext());\n\t\tconst auto name = utf(data.vname());\n\t\tif (inner.isEmpty()) {\n\t\t\treturn tag(\"a\", { { \"name\", name } });\n\t\t} else {\n\t\t\treturn tag(\n\t\t\t\t\"span\",\n\t\t\t\t{ { \"class\", \"reference\" } },\n\t\t\t\ttag(\"a\", { { \"name\", name } }) + inner);\n\t\t}\n\t});\n}\n\nQByteArray Parser::caption(const MTPPageCaption &caption) {\n\tauto text = rich(caption.data().vtext());\n\tconst auto credit = rich(caption.data().vcredit());\n\tif (!credit.isEmpty()) {\n\t\ttext += tag(\"cite\", { { \"dir\", \"auto\" } }, credit);\n\t} else if (text.isEmpty()) {\n\t\treturn QByteArray();\n\t}\n\treturn tag(\"figcaption\", { { \"dir\", \"auto\" } }, text);\n}\n\nPhoto Parser::parse(const MTPPhoto &photo) {\n\tauto result = Photo{\n\t\t.id = photo.match([&](const auto &d) { return d.vid().v; }),\n\t};\n\tauto sizes = base::flat_map<QByteArray, QSize>();\n\tphoto.match([](const MTPDphotoEmpty &) {\n\t}, [&](const MTPDphoto &data) {\n\t\tfor (const auto &size : data.vsizes().v) {\n\t\t\tsize.match([&](const MTPDphotoSizeEmpty &data) {\n\t\t\t}, [&](const MTPDphotoSize &data) {\n\t\t\t\tsizes.emplace(\n\t\t\t\t\tdata.vtype().v,\n\t\t\t\t\tQSize(data.vw().v, data.vh().v));\n\t\t\t}, [&](const MTPDphotoCachedSize &data) {\n\t\t\t\tsizes.emplace(\n\t\t\t\t\tdata.vtype().v,\n\t\t\t\t\tQSize(data.vw().v, data.vh().v));\n\t\t\t}, [&](const MTPDphotoStrippedSize &data) {\n\t\t\t\tresult.minithumbnail = data.vbytes().v;\n\t\t\t}, [&](const MTPDphotoSizeProgressive &data) {\n\t\t\t\tsizes.emplace(\n\t\t\t\t\tdata.vtype().v,\n\t\t\t\t\tQSize(data.vw().v, data.vh().v));\n\t\t\t}, [&](const MTPDphotoPathSize &data) {\n\t\t\t});\n\t\t}\n\t});\n\tfor (const auto attempt : { \"y\", \"x\", \"w\" }) {\n\t\tconst auto i = sizes.find(QByteArray(attempt));\n\t\tif (i != end(sizes)) {\n\t\t\tresult.width = i->second.width();\n\t\t\tresult.height = i->second.height();\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn result;\n}\n\nDocument Parser::parse(const MTPDocument &document) {\n\tauto result = Document{\n\t\t.id = document.match([&](const auto &d) { return d.vid().v; }),\n\t};\n\tdocument.match([](const MTPDdocumentEmpty &) {\n\t}, [&](const MTPDdocument &data) {\n\t\tfor (const auto &attribute : data.vattributes().v) {\n\t\t\tattribute.match([&](const MTPDdocumentAttributeImageSize &data) {\n\t\t\t\tresult.width = data.vw().v;\n\t\t\t\tresult.height = data.vh().v;\n\t\t\t}, [&](const MTPDdocumentAttributeVideo &data) {\n\t\t\t\tresult.width = data.vw().v;\n\t\t\t\tresult.height = data.vh().v;\n\t\t\t}, [](const auto &) {});\n\t\t}\n\t\tif (const auto sizes = data.vthumbs()) {\n\t\t\tfor (const auto &size : sizes->v) {\n\t\t\t\tsize.match([&](const MTPDphotoStrippedSize &data) {\n\t\t\t\t\tresult.minithumbnail = data.vbytes().v;\n\t\t\t\t}, [&](const auto &data) {\n\t\t\t\t});\n\t\t\t}\n\n\t\t}\n\t});\n\treturn result;\n}\n\nGeo Parser::parse(const MTPGeoPoint &geo) {\n\treturn geo.match([](const MTPDgeoPointEmpty &) {\n\t\treturn Geo();\n\t}, [&](const MTPDgeoPoint &data) {\n\t\treturn Geo{\n\t\t\t.lat = data.vlat().v,\n\t\t\t.lon = data.vlong().v,\n\t\t\t.access = data.vaccess_hash().v,\n\t\t};\n\t});\n}\n\nPhoto Parser::photoById(uint64 id) {\n\tconst auto i = _photosById.find(id);\n\treturn (i != end(_photosById)) ? i->second : Photo();\n}\n\nDocument Parser::documentById(uint64 id) {\n\tconst auto i = _documentsById.find(id);\n\treturn (i != end(_documentsById)) ? i->second : Document();\n}\n\nQByteArray Parser::photoFullUrl(const Photo &photo) {\n\treturn resource(\"photo/\" + Number(photo.id) + _fileOriginPostfix);\n}\n\nQByteArray Parser::documentFullUrl(const Document &document) {\n\treturn resource(\"document/\" + Number(document.id) + _fileOriginPostfix);\n}\n\nQByteArray Parser::embedUrl(const QByteArray &html) {\n\tauto binary = std::array<uchar, SHA256_DIGEST_LENGTH>{};\n\tSHA256(\n\t\treinterpret_cast<const unsigned char*>(html.data()),\n\t\thtml.size(),\n\t\tbinary.data());\n\tconst auto hex = [](uchar value) -> char {\n\t\treturn (value >= 10) ? ('a' + (value - 10)) : ('0' + value);\n\t};\n\tauto result = QByteArray();\n\tresult.reserve(binary.size() * 2);\n\tfor (const auto byte : binary) {\n\t\tresult.push_back(hex(byte / 16));\n\t\tresult.push_back(hex(byte % 16));\n\t}\n\tresult += \".html\";\n\t_result.embeds.emplace(result, html);\n\treturn resource(\"html/\" + result);\n}\n\nQByteArray Parser::mapUrl(const Geo &geo, int width, int height, int zoom) {\n\treturn resource(\"map/\"\n\t\t+ GeoPointId(geo) + \"&\"\n\t\t+ Number(width) + \",\"\n\t\t+ Number(height) + \"&\"\n\t\t+ Number(zoom));\n}\n\nQByteArray Parser::resource(QByteArray id) {\n\treturn '/' + id;\n}\n\nstd::vector<QSize> Parser::computeCollageDimensions(\n\t\tconst QVector<MTPPageBlock> &items) {\n\tif (items.size() < 2) {\n\t\treturn {};\n\t}\n\tauto result = std::vector<QSize>(items.size());\n\tfor (auto i = 0, count = int(items.size()); i != count; ++i) {\n\t\titems[i].match([&](const MTPDpageBlockPhoto &data) {\n\t\t\tconst auto photo = photoById(data.vphoto_id().v);\n\t\t\tif (photo.id && photo.width > 0 && photo.height > 0) {\n\t\t\t\tresult[i] = QSize(photo.width, photo.height);\n\t\t\t}\n\t\t}, [&](const MTPDpageBlockVideo &data) {\n\t\t\tconst auto document = documentById(data.vvideo_id().v);\n\t\t\tif (document.id && document.width > 0 && document.height > 0) {\n\t\t\t\tresult[i] = QSize(document.width, document.height);\n\t\t\t}\n\t\t}, [](const auto &) {});\n\t\tif (result[i].isEmpty()) {\n\t\t\treturn {};\n\t\t}\n\t}\n\treturn result;\n}\n\nQSize Parser::computeSlideshowDimensions(\n\t\tconst QVector<MTPPageBlock> &items) {\n\tif (items.size() < 2) {\n\t\treturn {};\n\t}\n\tauto result = QSize();\n\tfor (const auto &item : items) {\n\t\tauto size = QSize();\n\t\titem.match([&](const MTPDpageBlockPhoto &data) {\n\t\t\tconst auto photo = photoById(data.vphoto_id().v);\n\t\t\tif (photo.id && photo.width > 0 && photo.height > 0) {\n\t\t\t\tsize = QSize(photo.width, photo.height);\n\t\t\t}\n\t\t}, [&](const MTPDpageBlockVideo &data) {\n\t\t\tconst auto document = documentById(data.vvideo_id().v);\n\t\t\tif (document.id && document.width > 0 && document.height > 0) {\n\t\t\t\tsize = QSize(document.width, document.height);\n\t\t\t}\n\t\t}, [](const auto &) {});\n\t\tif (size.isEmpty()) {\n\t\t\treturn {};\n\t\t} else if (result.height() * size.width()\n\t\t\t< result.width() * size.height()) {\n\t\t\tresult = size;\n\t\t}\n\t}\n\treturn result;\n}\n\n} // namespace\n\nPrepared Prepare(const Source &source, const Options &options) {\n\tauto parser = Parser(source, options);\n\treturn parser.result();\n}\n\n} // namespace Iv\n"
  },
  {
    "path": "Telegram/SourceFiles/iv/iv_prepare.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Iv {\n\nstruct Options;\nstruct Prepared;\n\nstruct Source {\n\tuint64 pageId = 0;\n\tMTPPage page;\n\tstd::optional<MTPPhoto> webpagePhoto;\n\tstd::optional<MTPDocument> webpageDocument;\n\tQString name;\n\tint updatedCachedViews = 0;\n};\n\n[[nodiscard]] Prepared Prepare(const Source &source, const Options &options);\n\n} // namespace Iv\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_cloud_manager.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"lang/lang_cloud_manager.h\"\n\n#include \"lang/lang_instance.h\"\n#include \"lang/lang_file_parser.h\"\n#include \"lang/lang_text_entity.h\"\n#include \"mtproto/mtp_instance.h\"\n#include \"storage/localstorage.h\"\n#include \"core/application.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"core/file_utilities.h\"\n#include \"core/click_handler_types.h\"\n#include \"boxes/abstract_box.h\" // Ui::hideLayer().\n#include \"styles/style_layers.h\"\n\nnamespace Lang {\nnamespace {\n\nclass ConfirmSwitchBox : public Ui::BoxContent {\npublic:\n\tConfirmSwitchBox(\n\t\tQWidget*,\n\t\tconst MTPDlangPackLanguage &data,\n\t\tFn<void()> apply);\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\tQString _name;\n\tint _percent = 0;\n\tbool _official = false;\n\tQString _editLink;\n\tFn<void()> _apply;\n\n};\n\nclass NotReadyBox : public Ui::BoxContent {\npublic:\n\tNotReadyBox(\n\t\tQWidget*,\n\t\tconst MTPDlangPackLanguage &data);\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\tQString _name;\n\tQString _editLink;\n\n};\n\nConfirmSwitchBox::ConfirmSwitchBox(\n\tQWidget*,\n\tconst MTPDlangPackLanguage &data,\n\tFn<void()> apply)\n: _name(qs(data.vnative_name()))\n, _percent(data.vtranslated_count().v * 100 / data.vstrings_count().v)\n, _official(data.is_official())\n, _editLink(qs(data.vtranslations_url()))\n, _apply(std::move(apply)) {\n}\n\nvoid ConfirmSwitchBox::prepare() {\n\tsetTitle(tr::lng_language_switch_title());\n\n\tauto text = (_official\n\t\t? tr::lng_language_switch_about_official\n\t\t: tr::lng_language_switch_about_unofficial)(\n\t\t\tlt_lang_name,\n\t\t\trpl::single(tr::bold(_name)),\n\t\t\tlt_percent,\n\t\t\trpl::single(tr::bold(QString::number(_percent))),\n\t\t\tlt_link,\n\t\t\ttr::lng_language_switch_link(tr::url(_editLink)),\n\t\t\ttr::marked);\n\tconst auto content = Ui::CreateChild<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\tthis,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\tstd::move(text),\n\t\t\tst::boxLabel),\n\t\tQMargins{ st::boxPadding.left(), 0, st::boxPadding.right(), 0 });\n\tcontent->entity()->setLinksTrusted();\n\n\taddButton(tr::lng_language_switch_apply(), [=] {\n\t\tconst auto apply = _apply;\n\t\tcloseBox();\n\t\tapply();\n\t});\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\n\tcontent->resizeToWidth(st::boxWideWidth);\n\tcontent->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tsetDimensions(st::boxWideWidth, height);\n\t}, lifetime());\n}\n\nNotReadyBox::NotReadyBox(\n\tQWidget*,\n\tconst MTPDlangPackLanguage &data)\n: _name(qs(data.vnative_name()))\n, _editLink(qs(data.vtranslations_url())) {\n}\n\nvoid NotReadyBox::prepare() {\n\tsetTitle(tr::lng_language_not_ready_title());\n\n\tauto text = tr::lng_language_not_ready_about(\n\t\tlt_lang_name,\n\t\trpl::single(tr::marked(_name)),\n\t\tlt_link,\n\t\ttr::lng_language_not_ready_link(tr::url(_editLink)),\n\t\ttr::marked);\n\tconst auto content = Ui::CreateChild<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\tthis,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\tstd::move(text),\n\t\t\tst::boxLabel),\n\t\tQMargins{ st::boxPadding.left(), 0, st::boxPadding.right(), 0 });\n\tcontent->entity()->setLinksTrusted();\n\n\taddButton(tr::lng_box_ok(), [=] { closeBox(); });\n\n\tcontent->resizeToWidth(st::boxWidth);\n\tcontent->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tsetDimensions(st::boxWidth, height);\n\t}, lifetime());\n}\n\n} // namespace\n\nLanguage ParseLanguage(const MTPLangPackLanguage &data) {\n\treturn data.match([](const MTPDlangPackLanguage &data) {\n\t\treturn Language{\n\t\t\tqs(data.vlang_code()),\n\t\t\tqs(data.vplural_code()),\n\t\t\tqs(data.vbase_lang_code().value_or_empty()),\n\t\t\tqs(data.vname()),\n\t\t\tqs(data.vnative_name())\n\t\t};\n\t});\n}\n\nCloudManager::CloudManager(Instance &langpack)\n: _langpack(langpack) {\n\tconst auto mtpLifetime = _lifetime.make_state<rpl::lifetime>();\n\tCore::App().domain().activeValue(\n\t) | rpl::filter([=](Main::Account *account) {\n\t\treturn (account != nullptr);\n\t}) | rpl::on_next_done([=](Main::Account *account) {\n\t\t*mtpLifetime = account->mtpMainSessionValue(\n\t\t) | rpl::on_next([=](not_null<MTP::Instance*> instance) {\n\t\t\t_api.emplace(instance);\n\t\t\tresendRequests();\n\t\t});\n\t}, [=] {\n\t\t_api.reset();\n\t}, _lifetime);\n}\n\nPack CloudManager::packTypeFromId(const QString &id) const {\n\tif (id == LanguageIdOrDefault(_langpack.id())) {\n\t\treturn Pack::Current;\n\t} else if (id == _langpack.baseId()) {\n\t\treturn Pack::Base;\n\t}\n\treturn Pack::None;\n}\n\nrpl::producer<> CloudManager::languageListChanged() const {\n\treturn _languageListChanged.events();\n}\n\nrpl::producer<> CloudManager::firstLanguageSuggestion() const {\n\treturn _firstLanguageSuggestion.events();\n}\n\nvoid CloudManager::requestLangPackDifference(const QString &langId) {\n\tExpects(!langId.isEmpty());\n\n\tif (langId == LanguageIdOrDefault(_langpack.id())) {\n\t\trequestLangPackDifference(Pack::Current);\n\t} else {\n\t\trequestLangPackDifference(Pack::Base);\n\t}\n}\n\nmtpRequestId &CloudManager::packRequestId(Pack pack) {\n\treturn (pack != Pack::Base)\n\t\t? _langPackRequestId\n\t\t: _langPackBaseRequestId;\n}\n\nmtpRequestId CloudManager::packRequestId(Pack pack) const {\n\treturn (pack != Pack::Base)\n\t\t? _langPackRequestId\n\t\t: _langPackBaseRequestId;\n}\n\nvoid CloudManager::requestLangPackDifference(Pack pack) {\n\tif (!_api) {\n\t\treturn;\n\t}\n\t_api->request(base::take(packRequestId(pack))).cancel();\n\tif (_langpack.isCustom()) {\n\t\treturn;\n\t}\n\n\tconst auto version = _langpack.version(pack);\n\tconst auto code = _langpack.cloudLangCode(pack);\n\tif (code.isEmpty()) {\n\t\treturn;\n\t}\n\tif (version > 0) {\n\t\tpackRequestId(pack) = _api->request(MTPlangpack_GetDifference(\n\t\t\tMTP_string(CloudLangPackName()),\n\t\t\tMTP_string(code),\n\t\t\tMTP_int(version)\n\t\t)).done([=](const MTPLangPackDifference &result) {\n\t\t\tpackRequestId(pack) = 0;\n\t\t\tapplyLangPackDifference(result);\n\t\t}).fail([=] {\n\t\t\tpackRequestId(pack) = 0;\n\t\t}).send();\n\t} else {\n\t\tpackRequestId(pack) = _api->request(MTPlangpack_GetLangPack(\n\t\t\tMTP_string(CloudLangPackName()),\n\t\t\tMTP_string(code)\n\t\t)).done([=](const MTPLangPackDifference &result) {\n\t\t\tpackRequestId(pack) = 0;\n\t\t\tapplyLangPackDifference(result);\n\t\t}).fail([=] {\n\t\t\tpackRequestId(pack) = 0;\n\t\t}).send();\n\t}\n}\n\nvoid CloudManager::setSuggestedLanguage(const QString &langCode) {\n\tif (Lang::LanguageIdOrDefault(langCode) != Lang::DefaultLanguageId()) {\n\t\t_suggestedLanguage = langCode;\n\t} else {\n\t\t_suggestedLanguage = QString();\n\t}\n\n\tif (!_languageWasSuggested) {\n\t\t_languageWasSuggested = true;\n\t\t_firstLanguageSuggestion.fire({});\n\n\t\tif (Core::App().offerLegacyLangPackSwitch()\n\t\t\t&& _langpack.id().isEmpty()\n\t\t\t&& !_suggestedLanguage.isEmpty()) {\n\t\t\t_offerSwitchToId = _suggestedLanguage;\n\t\t\tofferSwitchLangPack();\n\t\t}\n\t}\n}\n\nvoid CloudManager::setCurrentVersions(int version, int baseVersion) {\n\tconst auto check = [&](Pack pack, int version) {\n\t\tif (version > _langpack.version(pack) && !packRequestId(pack)) {\n\t\t\trequestLangPackDifference(pack);\n\t\t}\n\t};\n\tcheck(Pack::Current, version);\n\tcheck(Pack::Base, baseVersion);\n}\n\nvoid CloudManager::applyLangPackDifference(\n\t\tconst MTPLangPackDifference &difference) {\n\tExpects(difference.type() == mtpc_langPackDifference);\n\n\tif (_langpack.isCustom()) {\n\t\treturn;\n\t}\n\n\tconst auto &langpack = difference.c_langPackDifference();\n\tconst auto langpackId = qs(langpack.vlang_code());\n\tconst auto pack = packTypeFromId(langpackId);\n\tif (pack != Pack::None) {\n\t\tapplyLangPackData(pack, langpack);\n\t\tif (_restartAfterSwitch) {\n\t\t\trestartAfterSwitch();\n\t\t}\n\t} else {\n\t\tLOG((\"Lang Warning: \"\n\t\t\t\"Ignoring update for '%1' because our language is '%2'\").arg(\n\t\t\tlangpackId,\n\t\t\t_langpack.id()));\n\t}\n}\n\nvoid CloudManager::requestLanguageList() {\n\tif (!_api) {\n\t\t_languagesRequestId = -1;\n\t\treturn;\n\t}\n\t_api->request(base::take(_languagesRequestId)).cancel();\n\t_languagesRequestId = _api->request(MTPlangpack_GetLanguages(\n\t\tMTP_string(CloudLangPackName())\n\t)).done([=](const MTPVector<MTPLangPackLanguage> &result) {\n\t\tauto languages = Languages();\n\t\tfor (const auto &language : result.v) {\n\t\t\tlanguages.push_back(ParseLanguage(language));\n\t\t}\n\t\tif (_languages != languages) {\n\t\t\t_languages = languages;\n\t\t\t_languageListChanged.fire({});\n\t\t}\n\t\t_languagesRequestId = 0;\n\t}).fail([=] {\n\t\t_languagesRequestId = 0;\n\t}).send();\n}\n\nvoid CloudManager::offerSwitchLangPack() {\n\tExpects(!_offerSwitchToId.isEmpty());\n\tExpects(_offerSwitchToId != DefaultLanguageId());\n\n\tif (!showOfferSwitchBox()) {\n\t\tlanguageListChanged(\n\t\t) | rpl::on_next([=] {\n\t\t\tshowOfferSwitchBox();\n\t\t}, _lifetime);\n\t\trequestLanguageList();\n\t}\n}\n\nLanguage CloudManager::findOfferedLanguage() const {\n\tfor (const auto &language : _languages) {\n\t\tif (language.id == _offerSwitchToId) {\n\t\t\treturn language;\n\t\t}\n\t}\n\treturn {};\n}\n\nbool CloudManager::showOfferSwitchBox() {\n\tconst auto language = findOfferedLanguage();\n\tif (language.id.isEmpty()) {\n\t\treturn false;\n\t}\n\n\tconst auto confirm = [=] {\n\t\tUi::hideLayer();\n\t\tif (_offerSwitchToId.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tperformSwitchAndRestart(language);\n\t};\n\tconst auto cancel = [=] {\n\t\tUi::hideLayer();\n\t\tchangeIdAndReInitConnection(DefaultLanguage());\n\t\tLocal::writeLangPack();\n\t};\n\tUi::show(\n\t\tUi::MakeConfirmBox({\n\t\t\t.text = QString(\"Do you want to switch your language to \")\n\t\t\t+ language.nativeName\n\t\t\t+ QString(\"? You can always change your language in Settings.\"),\n\t\t\t.confirmed = confirm,\n\t\t\t.cancelled = cancel,\n\t\t\t.confirmText = QString(\"Change\"),\n\t\t}),\n\t\tUi::LayerOption::KeepOther);\n\treturn true;\n}\n\nvoid CloudManager::applyLangPackData(\n\t\tPack pack,\n\t\tconst MTPDlangPackDifference &data) {\n\tif (_langpack.version(pack) < data.vfrom_version().v) {\n\t\trequestLangPackDifference(pack);\n\t} else if (!data.vstrings().v.isEmpty()) {\n\t\t_langpack.applyDifference(pack, data);\n\t\tLocal::writeLangPack();\n\t} else if (_restartAfterSwitch) {\n\t\tLocal::writeLangPack();\n\t} else {\n\t\tLOG((\"Lang Info: Up to date.\"));\n\t}\n}\n\nbool CloudManager::canApplyWithoutRestart(const QString &id) const {\n\tif (id == u\"#TEST_X\"_q || id == u\"#TEST_0\"_q) {\n\t\treturn true;\n\t}\n\treturn Core::App().canApplyLangPackWithoutRestart();\n}\n\nvoid CloudManager::resetToDefault() {\n\tperformSwitch(DefaultLanguage());\n}\n\nvoid CloudManager::switchToLanguage(const QString &id) {\n\trequestLanguageAndSwitch(id, false);\n}\n\nvoid CloudManager::switchWithWarning(const QString &id) {\n\trequestLanguageAndSwitch(id, true);\n}\n\nvoid CloudManager::requestLanguageAndSwitch(\n\t\tconst QString &id,\n\t\tbool warning) {\n\tExpects(!id.isEmpty());\n\n\tif (LanguageIdOrDefault(_langpack.id()) == id) {\n\t\tUi::show(Ui::MakeInformBox(tr::lng_language_already()));\n\t\treturn;\n\t} else if (id == u\"#custom\"_q) {\n\t\tperformSwitchToCustom();\n\t\treturn;\n\t}\n\n\t_switchingToLanguageId = id;\n\t_switchingToLanguageWarning = warning;\n\tsendSwitchingToLanguageRequest();\n}\n\nvoid CloudManager::sendSwitchingToLanguageRequest() {\n\tif (!_api) {\n\t\t_switchingToLanguageRequest = -1;\n\t\treturn;\n\t}\n\t_api->request(_switchingToLanguageRequest).cancel();\n\t_switchingToLanguageRequest = _api->request(MTPlangpack_GetLanguage(\n\t\tMTP_string(Lang::CloudLangPackName()),\n\t\tMTP_string(_switchingToLanguageId)\n\t)).done([=](const MTPLangPackLanguage &result) {\n\t\t_switchingToLanguageRequest = 0;\n\t\tconst auto language = Lang::ParseLanguage(result);\n\t\tconst auto finalize = [=] {\n\t\t\tif (canApplyWithoutRestart(language.id)) {\n\t\t\t\tperformSwitchAndAddToRecent(language);\n\t\t\t} else {\n\t\t\t\tperformSwitchAndRestart(language);\n\t\t\t}\n\t\t};\n\t\tif (!_switchingToLanguageWarning) {\n\t\t\tfinalize();\n\t\t\treturn;\n\t\t}\n\t\tresult.match([=](const MTPDlangPackLanguage &data) {\n\t\t\tif (data.vstrings_count().v > 0) {\n\t\t\t\tUi::show(Box<ConfirmSwitchBox>(data, finalize));\n\t\t\t} else {\n\t\t\t\tUi::show(Box<NotReadyBox>(data));\n\t\t\t}\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\t_switchingToLanguageRequest = 0;\n\t\tif (error.type() == \"LANG_CODE_NOT_SUPPORTED\") {\n\t\t\tUi::show(Ui::MakeInformBox(tr::lng_language_not_found()));\n\t\t}\n\t}).send();\n}\n\nvoid CloudManager::switchToLanguage(const Language &data) {\n\tif (_langpack.id() == data.id && data.id != u\"#custom\"_q) {\n\t\treturn;\n\t} else if (!_api) {\n\t\treturn;\n\t}\n\n\t_api->request(base::take(_getKeysForSwitchRequestId)).cancel();\n\tif (data.id == u\"#custom\"_q) {\n\t\tperformSwitchToCustom();\n\t} else if (canApplyWithoutRestart(data.id)) {\n\t\tperformSwitchAndAddToRecent(data);\n\t} else {\n\t\tQVector<MTPstring> keys;\n\t\tkeys.reserve(3);\n\t\tkeys.push_back(MTP_string(\"lng_sure_save_language\"));\n\t\t_getKeysForSwitchRequestId = _api->request(MTPlangpack_GetStrings(\n\t\t\tMTP_string(Lang::CloudLangPackName()),\n\t\t\tMTP_string(data.id),\n\t\t\tMTP_vector<MTPstring>(std::move(keys))\n\t\t)).done([=](const MTPVector<MTPLangPackString> &result) {\n\t\t\t_getKeysForSwitchRequestId = 0;\n\t\t\tconst auto values = Instance::ParseStrings(result);\n\t\t\tconst auto getValue = [&](ushort key) {\n\t\t\t\tauto it = values.find(key);\n\t\t\t\treturn (it == values.cend())\n\t\t\t\t\t? GetOriginalValue(key)\n\t\t\t\t\t: it->second;\n\t\t\t};\n\t\t\tconst auto text = tr::lng_sure_save_language(tr::now)\n\t\t\t\t+ \"\\n\\n\"\n\t\t\t\t+ getValue(tr::lng_sure_save_language.base);\n\t\t\tUi::show(\n\t\t\t\tUi::MakeConfirmBox({\n\t\t\t\t\t.text = text,\n\t\t\t\t\t.confirmed = [=] { performSwitchAndRestart(data); },\n\t\t\t\t\t.confirmText = tr::lng_box_ok(),\n\t\t\t\t}),\n\t\t\t\tUi::LayerOption::KeepOther);\n\t\t}).fail([=] {\n\t\t\t_getKeysForSwitchRequestId = 0;\n\t\t}).send();\n\t}\n}\n\nvoid CloudManager::performSwitchToCustom() {\n\tauto filter = u\"Language files (*.strings)\"_q;\n\tauto title = u\"Choose language .strings file\"_q;\n\tFileDialog::GetOpenPath(Core::App().getFileDialogParent(), title, filter, [=, weak = base::make_weak(this)](const FileDialog::OpenResult &result) {\n\t\tif (!weak || result.paths.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto filePath = result.paths.front();\n\t\tauto loader = Lang::FileParser(\n\t\t\tfilePath,\n\t\t\t{ tr::lng_sure_save_language.base });\n\t\tif (loader.errors().isEmpty()) {\n\t\t\tif (_api) {\n\t\t\t\t_api->request(\n\t\t\t\t\tbase::take(_switchingToLanguageRequest)\n\t\t\t\t).cancel();\n\t\t\t}\n\t\t\tif (canApplyWithoutRestart(u\"#custom\"_q)) {\n\t\t\t\t_langpack.switchToCustomFile(filePath);\n\t\t\t} else {\n\t\t\t\tconst auto values = loader.found();\n\t\t\t\tconst auto getValue = [&](ushort key) {\n\t\t\t\t\tconst auto it = values.find(key);\n\t\t\t\t\treturn (it == values.cend())\n\t\t\t\t\t\t? GetOriginalValue(key)\n\t\t\t\t\t\t: it.value();\n\t\t\t\t};\n\t\t\t\tconst auto text = tr::lng_sure_save_language(tr::now)\n\t\t\t\t\t+ \"\\n\\n\"\n\t\t\t\t\t+ getValue(tr::lng_sure_save_language.base);\n\t\t\t\tconst auto change = [=] {\n\t\t\t\t\t_langpack.switchToCustomFile(filePath);\n\t\t\t\t\tCore::Restart();\n\t\t\t\t};\n\t\t\t\tUi::show(\n\t\t\t\t\tUi::MakeConfirmBox({\n\t\t\t\t\t\t.text = text,\n\t\t\t\t\t\t.confirmed = change,\n\t\t\t\t\t\t.confirmText = tr::lng_box_ok(),\n\t\t\t\t\t}),\n\t\t\t\t\tUi::LayerOption::KeepOther);\n\t\t\t}\n\t\t} else {\n\t\t\tUi::show(\n\t\t\t\tUi::MakeInformBox(\n\t\t\t\t\t\"Custom lang failed :(\\n\\nError: \" + loader.errors()),\n\t\t\t\tUi::LayerOption::KeepOther);\n\t\t}\n\t});\n}\n\nvoid CloudManager::switchToTestLanguage() {\n\tconst auto testLanguageId = (_langpack.id() == u\"#TEST_X\"_q)\n\t\t? u\"#TEST_0\"_q\n\t\t: u\"#TEST_X\"_q;\n\tperformSwitch({ testLanguageId });\n}\n\nvoid CloudManager::performSwitch(const Language &data) {\n\t_restartAfterSwitch = false;\n\tswitchLangPackId(data);\n\trequestLangPackDifference(Pack::Current);\n\trequestLangPackDifference(Pack::Base);\n}\n\nvoid CloudManager::performSwitchAndAddToRecent(const Language &data) {\n\tLocal::pushRecentLanguage(data);\n\tperformSwitch(data);\n}\n\nvoid CloudManager::performSwitchAndRestart(const Language &data) {\n\tperformSwitchAndAddToRecent(data);\n\trestartAfterSwitch();\n}\n\nvoid CloudManager::restartAfterSwitch() {\n\tif (_langPackRequestId || _langPackBaseRequestId) {\n\t\t_restartAfterSwitch = true;\n\t} else {\n\t\tCore::Restart();\n\t}\n}\n\nvoid CloudManager::switchLangPackId(const Language &data) {\n\tconst auto currentId = _langpack.id();\n\tconst auto currentBaseId = _langpack.baseId();\n\tconst auto notChanged = (currentId == data.id\n\t\t&& currentBaseId == data.baseId)\n\t\t|| (currentId.isEmpty()\n\t\t\t&& currentBaseId.isEmpty()\n\t\t\t&& data.id == DefaultLanguageId());\n\tif (!notChanged) {\n\t\tchangeIdAndReInitConnection(data);\n\t}\n}\n\nvoid CloudManager::changeIdAndReInitConnection(const Language &data) {\n\t_langpack.switchToId(data);\n\tif (_api) {\n\t\tconst auto mtproto = &_api->instance();\n\t\tmtproto->reInitConnection(mtproto->mainDcId());\n\t}\n}\n\nvoid CloudManager::getValueForLang(\n\t\tconst QString &key,\n\t\tconst QString &langId,\n\t\tFn<void(const QString &)> callback) {\n\tconst auto requestKey = langId + ':' + key;\n\tauto &request = _getValueForLangRequests[requestKey];\n\trequest.callback = std::move(callback);\n\tif (!_api) {\n\t\trequest.requestId = -1;\n\t\treturn;\n\t}\n\t_api->request(base::take(request.requestId)).cancel();\n\trequest.requestId = _api->request(\n\t\tMTPlangpack_GetStrings(\n\t\t\tMTP_string(Lang::CloudLangPackName()),\n\t\t\tMTP_string(langId),\n\t\t\tMTP_vector<MTPstring>(1, MTP_string(key))\n\t)).done([=](const MTPVector<MTPLangPackString> &result) {\n\t\tconst auto it = _getValueForLangRequests.find(requestKey);\n\t\tif (it != _getValueForLangRequests.end()) {\n\t\t\tconst auto onstack = it->second.callback;\n\t\t\t_getValueForLangRequests.erase(it);\n\t\t\tconst auto values = Instance::ParseStrings(result);\n\t\t\tfor (const auto &[k, v] : values) {\n\t\t\t\tonstack(v);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tonstack(QString());\n\t\t}\n\t}).fail([=] {\n\t\tconst auto it = _getValueForLangRequests.find(requestKey);\n\t\tif (it != _getValueForLangRequests.end()) {\n\t\t\tconst auto onstack = it->second.callback;\n\t\t\t_getValueForLangRequests.erase(it);\n\t\t\tonstack(QString());\n\t\t}\n\t}).send();\n}\n\nvoid CloudManager::resendPendingValueRequests() {\n\tif (!_api) {\n\t\treturn;\n\t}\n\tfor (const auto &[requestKey, request] : _getValueForLangRequests) {\n\t\tif (request.requestId == -1) {\n\t\t\tconst auto colonPos = requestKey.indexOf(':');\n\t\t\tif (colonPos > 0) {\n\t\t\t\tgetValueForLang(\n\t\t\t\t\trequestKey.mid(colonPos + 1),\n\t\t\t\t\trequestKey.left(colonPos),\n\t\t\t\t\trequest.callback);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid CloudManager::resendRequests() {\n\tif (packRequestId(Pack::Base)) {\n\t\trequestLangPackDifference(Pack::Base);\n\t}\n\tif (packRequestId(Pack::Current)) {\n\t\trequestLangPackDifference(Pack::Current);\n\t}\n\tif (_languagesRequestId) {\n\t\trequestLanguageList();\n\t}\n\tif (_switchingToLanguageRequest) {\n\t\tsendSwitchingToLanguageRequest();\n\t}\n\tresendPendingValueRequests();\n}\n\nCloudManager &CurrentCloudManager() {\n\tauto result = Core::App().langCloudManager();\n\tAssert(result != nullptr);\n\treturn *result;\n}\n\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_cloud_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n#include \"base/weak_ptr.h\"\n\nnamespace MTP {\nclass Instance;\n} // namespace MTP\n\nnamespace Lang {\n\nclass Instance;\nenum class Pack;\nstruct Language;\n\nLanguage ParseLanguage(const MTPLangPackLanguage &data);\n\nclass CloudManager : public base::has_weak_ptr {\npublic:\n\texplicit CloudManager(Instance &langpack);\n\n\tusing Languages = std::vector<Language>;\n\n\tvoid requestLanguageList();\n\tconst Languages &languageList() const {\n\t\treturn _languages;\n\t}\n\t[[nodiscard]] rpl::producer<> languageListChanged() const;\n\t[[nodiscard]] rpl::producer<> firstLanguageSuggestion() const;\n\tvoid requestLangPackDifference(const QString &langId);\n\tvoid applyLangPackDifference(const MTPLangPackDifference &difference);\n\tvoid setCurrentVersions(int version, int baseVersion);\n\n\tvoid resetToDefault();\n\tvoid switchWithWarning(const QString &id);\n\tvoid switchToLanguage(const QString &id);\n\tvoid switchToLanguage(const Language &data);\n\tvoid switchToTestLanguage();\n\tvoid setSuggestedLanguage(const QString &langCode);\n\tQString suggestedLanguage() const {\n\t\treturn _suggestedLanguage;\n\t}\n\n\tvoid getValueForLang(\n\t\tconst QString &key,\n\t\tconst QString &langId,\n\t\tFn<void(const QString &)> callback);\n\nprivate:\n\tmtpRequestId &packRequestId(Pack pack);\n\tmtpRequestId packRequestId(Pack pack) const;\n\tPack packTypeFromId(const QString &id) const;\n\tvoid requestLangPackDifference(Pack pack);\n\tbool canApplyWithoutRestart(const QString &id) const;\n\tvoid performSwitchToCustom();\n\tvoid performSwitch(const Language &data);\n\tvoid performSwitchAndAddToRecent(const Language &data);\n\tvoid performSwitchAndRestart(const Language &data);\n\tvoid restartAfterSwitch();\n\tvoid offerSwitchLangPack();\n\tbool showOfferSwitchBox();\n\tLanguage findOfferedLanguage() const;\n\n\tvoid requestLanguageAndSwitch(const QString &id, bool warning);\n\tvoid applyLangPackData(Pack pack, const MTPDlangPackDifference &data);\n\tvoid switchLangPackId(const Language &data);\n\tvoid changeIdAndReInitConnection(const Language &data);\n\n\tvoid sendSwitchingToLanguageRequest();\n\tvoid resendPendingValueRequests();\n\tvoid resendRequests();\n\n\tstd::optional<MTP::Sender> _api;\n\tInstance &_langpack;\n\tLanguages _languages;\n\tmtpRequestId _langPackRequestId = 0;\n\tmtpRequestId _langPackBaseRequestId = 0;\n\tmtpRequestId _languagesRequestId = 0;\n\n\tQString _offerSwitchToId;\n\tbool _restartAfterSwitch = false;\n\n\tQString _suggestedLanguage;\n\tbool _languageWasSuggested = false;\n\n\tmtpRequestId _switchingToLanguageRequest = 0;\n\tQString _switchingToLanguageId;\n\tbool _switchingToLanguageWarning = false;\n\n\tmtpRequestId _getKeysForSwitchRequestId = 0;\n\n\tstruct ValueRequest {\n\t\tmtpRequestId requestId = 0;\n\t\tFn<void(const QString &)> callback;\n\t};\n\tbase::flat_map<QString, ValueRequest> _getValueForLangRequests;\n\n\trpl::event_stream<> _languageListChanged;\n\trpl::event_stream<> _firstLanguageSuggestion;\n\n\trpl::lifetime _lifetime;\n\n};\n\nCloudManager &CurrentCloudManager();\n\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_file_parser.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"lang/lang_file_parser.h\"\n\n#include \"base/parse_helper.h\"\n#include \"base/debug_log.h\"\n\n#include <QtCore/QTextStream>\n#include <QtCore/QFile>\n#include <QtCore/QFileInfo>\n\nnamespace Lang {\nnamespace {\n\nconstexpr auto kLangFileLimit = 1024 * 1024;\n\n} // namespace\n\nFileParser::FileParser(const QString &file, const std::set<ushort> &request)\n: _content(base::parse::stripComments(ReadFile(file, file)))\n, _request(request) {\n\tparse();\n}\n\nFileParser::FileParser(const QByteArray &content, Fn<void(QLatin1String key, const QByteArray &value)> callback)\n: _content(base::parse::stripComments(content))\n, _callback(std::move(callback)) {\n\tparse();\n}\n\nvoid FileParser::parse() {\n\tif (_content.isEmpty()) {\n\t\terror(u\"Got empty lang file content\"_q);\n\t\treturn;\n\t}\n\n\tauto text = _content.constData(), end = text + _content.size();\n\twhile (text != end) {\n\t\tif (!readKeyValue(text, end)) {\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nconst QString &FileParser::errors() const {\n\tif (_errors.isEmpty() && !_errorsList.isEmpty()) {\n\t\t_errors = _errorsList.join('\\n');\n\t}\n\treturn _errors;\n}\n\nconst QString &FileParser::warnings() const {\n\tif (_warnings.isEmpty() && !_warningsList.isEmpty()) {\n\t\t_warnings = _warningsList.join('\\n');\n\t}\n\treturn _warnings;\n}\n\nbool FileParser::readKeyValue(const char *&from, const char *end) {\n\tusing base::parse::skipWhitespaces;\n\tif (!skipWhitespaces(from, end)) return false;\n\n\tif (*from != '\"') {\n\t\treturn error(\"Expected quote before key name!\");\n\t}\n\t++from;\n\tconst char *nameStart = from;\n\twhile (from < end && ((*from >= 'a' && *from <= 'z') || (*from >= 'A' && *from <= 'Z') || *from == '_' || (*from >= '0' && *from <= '9') || *from == '#')) {\n\t\t++from;\n\t}\n\n\tauto key = QLatin1String(nameStart, from - nameStart);\n\n\tif (from == end || *from != '\"') {\n\t\treturn error(u\"Expected quote after key name '%1'!\"_q.arg(key));\n\t}\n\t++from;\n\n\tif (!skipWhitespaces(from, end)) {\n\t\treturn error(u\"Unexpected end of file in key '%1'!\"_q.arg(key));\n\t}\n\tif (*from != '=') {\n\t\treturn error(u\"'=' expected in key '%1'!\"_q.arg(key));\n\t}\n\tif (!skipWhitespaces(++from, end)) {\n\t\treturn error(u\"Unexpected end of file in key '%1'!\"_q.arg(key));\n\t}\n\tif (*from != '\"') {\n\t\treturn error(u\"Expected string after '=' in key '%1'!\"_q.arg(key));\n\t}\n\n\tauto skipping = false;\n\tauto keyIndex = kKeysCount;\n\tif (!_callback) {\n\t\tkeyIndex = GetKeyIndex(key);\n\t\tskipping = (_request.find(keyIndex) == _request.end());\n\t}\n\n\tauto value = QByteArray();\n\tauto appendValue = [&value, skipping](auto&&... args) {\n\t\tif (!skipping) {\n\t\t\tvalue.append(std::forward<decltype(args)>(args)...);\n\t\t}\n\t};\n\tconst char *start = ++from;\n\twhile (from < end && *from != '\"') {\n\t\tif (*from == '\\n') {\n\t\t\treturn error(u\"Unexpected end of string in key '%1'!\"_q.arg(key));\n\t\t}\n\t\tif (*from == '\\\\') {\n\t\t\tif (from + 1 >= end) {\n\t\t\t\treturn error(u\"Unexpected end of file in key '%1'!\"_q.arg(key));\n\t\t\t}\n\t\t\tif (*(from + 1) == '\"' || *(from + 1) == '\\\\') {\n\t\t\t\tif (from > start) appendValue(start, from - start);\n\t\t\t\tstart = ++from;\n\t\t\t} else if (*(from + 1) == 'n') {\n\t\t\t\tif (from > start) appendValue(start, from - start);\n\t\t\t\tappendValue('\\n');\n\t\t\t\tstart = (++from) + 1;\n\t\t\t}\n\t\t}\n\t\t++from;\n\t}\n\tif (from >= end) {\n\t\treturn error(u\"Unexpected end of file in key '%1'!\"_q.arg(key));\n\t}\n\tif (from > start) {\n\t\tappendValue(start, from - start);\n\t}\n\n\tif (!skipWhitespaces(++from, end)) {\n\t\treturn error(u\"Unexpected end of file in key '%1'!\"_q.arg(key));\n\t}\n\tif (*from != ';') {\n\t\treturn error(u\"';' expected after \\\"value\\\" in key '%1'!\"_q.arg(key));\n\t}\n\n\tskipWhitespaces(++from, end);\n\n\tif (_callback) {\n\t\t_callback(key, value);\n\t} else if (!skipping) {\n\t\t_result.insert(keyIndex, QString::fromUtf8(value));\n\t}\n\n\treturn true;\n}\n\nQByteArray FileParser::ReadFile(const QString &absolutePath, const QString &relativePath) {\n\tQFile file(QFileInfo::exists(relativePath) ? relativePath : absolutePath);\n\tif (!file.open(QIODevice::ReadOnly)) {\n\t\tLOG((\"Lang Error: Could not open file at '%1' ('%2')\")\n\t\t\t.arg(relativePath, absolutePath));\n\t\treturn QByteArray();\n\t}\n\tif (file.size() > kLangFileLimit) {\n\t\tLOG((\"Lang Error: File is too big: %1\").arg(file.size()));\n\t\treturn QByteArray();\n\t}\n\n\tconstexpr auto kCodecMagicSize = 3;\n\tauto codecMagic = file.read(kCodecMagicSize);\n\tif (codecMagic.size() < kCodecMagicSize) {\n\t\tLOG((\"Lang Error: Found bad file at '%1' ('%2')\").arg(relativePath, absolutePath));\n\t\treturn QByteArray();\n\t}\n\tfile.seek(0);\n\n\tQByteArray data;\n\tauto readUtf16Stream = [relativePath, absolutePath](auto &&stream) {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)\n\t\tstream.setEncoding(QStringConverter::Utf16);\n#else // Qt >= 6.0.0\n\t\tstream.setCodec(\"UTF-16\");\n#endif // Qt < 6.0.0\n\t\tauto string = stream.readAll();\n\t\tif (stream.status() != QTextStream::Ok) {\n\t\t\tLOG((\"Lang Error: Could not read UTF-16 data from '%1' ('%2')\").arg(relativePath, absolutePath));\n\t\t\treturn QByteArray();\n\t\t}\n\t\tif (string.isEmpty()) {\n\t\t\tLOG((\"Lang Error: Empty UTF-16 content in '%1' ('%2')\").arg(relativePath, absolutePath));\n\t\t\treturn QByteArray();\n\t\t}\n\t\treturn string.toUtf8();\n\t};\n\tif ((codecMagic.at(0) == '\\xFF' && codecMagic.at(1) == '\\xFE') || (codecMagic.at(0) == '\\xFE' && codecMagic.at(1) == '\\xFF') || (codecMagic.at(1) == 0)) {\n\t\treturn readUtf16Stream(QTextStream(&file));\n\t} else if (codecMagic.at(0) == 0) {\n\t\tauto utf16WithBOM = \"\\xFE\\xFF\" + file.readAll();\n\t\treturn readUtf16Stream(QTextStream(utf16WithBOM));\n\t}\n\tdata = file.readAll();\n\tif (codecMagic.at(0) == '\\xEF' && codecMagic.at(1) == '\\xBB' && codecMagic.at(2) == '\\xBF') {\n\t\tdata = data.mid(3); // skip UTF-8 BOM\n\t}\n\tif (data.isEmpty()) {\n\t\tLOG((\"Lang Error: Empty UTF-8 content in '%1' ('%2')\").arg(relativePath, absolutePath));\n\t\treturn QByteArray();\n\t}\n\treturn data;\n}\n\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_file_parser.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"lang/lang_keys.h\"\n\n#include <set>\n#include <QtCore/QMap>\n\nnamespace Lang {\n\nclass FileParser {\npublic:\n\tusing Result = QMap<ushort, QString>;\n\n\tFileParser(const QString &file, const std::set<ushort> &request);\n\tFileParser(const QByteArray &content, Fn<void(QLatin1String key, const QByteArray &value)> callback);\n\n\tstatic QByteArray ReadFile(const QString &absolutePath, const QString &relativePath);\n\n\tconst QString &errors() const;\n\tconst QString &warnings() const;\n\n\tResult found() const {\n\t\treturn _result;\n\t}\n\nprivate:\n\tvoid parse();\n\n\tbool error(const QString &text) {\n\t\t_errorsList.push_back(text);\n\t\treturn false;\n\t}\n\tvoid warning(const QString &text) {\n\t\t_warningsList.push_back(text);\n\t}\n\tbool readKeyValue(const char *&from, const char *end);\n\n\tmutable QStringList _errorsList, _warningsList;\n\tmutable QString _errors, _warnings;\n\n\tconst QByteArray _content;\n\tconst std::set<ushort> _request;\n\tconst Fn<void(QLatin1String key, const QByteArray &value)> _callback;\n\n\tResult _result;\n\n};\n\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_hardcoded.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Lang {\nnamespace Hard {\n\ninline QString FavedSetTitle() {\n\treturn u\"Favorite stickers\"_q;\n}\n\ninline QString CallErrorIncompatible() {\n\treturn u\"{user}'s app is using an incompatible protocol. They need to update their app before you can call them.\"_q;\n}\n\ninline QString ServerError() {\n\treturn u\"Internal server error.\"_q;\n}\n\ninline QString ClearPathFailed() {\n\treturn u\"Clear failed :(\"_q;\n}\n\ninline QString ProxyConfigError() {\n\treturn u\"The proxy you are using is not configured correctly and will be disabled. Please find another one.\"_q;\n}\n\ninline QString NoAuthorizationBot() {\n\treturn u\"Could not get authorization bot.\"_q;\n}\n\ninline QString SecureSaveError() {\n\treturn u\"Error saving value.\"_q;\n}\n\ninline QString SecureAcceptError() {\n\treturn u\"Error accepting form.\"_q;\n}\n\ninline QString PassportCorrupted() {\n\treturn u\"It seems your Telegram Passport data was corrupted.\\n\\nYou can reset your Telegram Passport and restart this authorization.\"_q;\n}\n\ninline QString PassportCorruptedChange() {\n\treturn u\"It seems your Telegram Passport data was corrupted.\\n\\nYou can reset your Telegram Passport and change your cloud password.\"_q;\n}\n\ninline QString PassportCorruptedReset() {\n\treturn u\"Reset\"_q;\n}\n\ninline QString PassportCorruptedResetSure() {\n\treturn u\"Are you sure you want to reset your Telegram Passport data?\"_q;\n}\n\ninline QString UnknownSecureScanError() {\n\treturn u\"Unknown scan read error.\"_q;\n}\n\ninline QString EmailConfirmationExpired() {\n\treturn u\"This email confirmation has expired. Please setup two-step verification once again.\"_q;\n}\n\ninline QString AutostartEnableError() {\n\treturn u\"Could not register for autostart.\"_q;\n}\n\n} // namespace Hard\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_instance.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"lang/lang_instance.h\"\n\n#include \"core/application.h\"\n#include \"storage/serialize_common.h\"\n#include \"storage/localstorage.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"lang/lang_file_parser.h\"\n#include \"lang/lang_tag.h\" // kTextCommandLangTag.\n#include \"base/platform/base_platform_info.h\"\n#include \"base/qthelp_regex.h\"\n\n#include <QRegularExpression>\n\nnamespace Lang {\nnamespace {\n\nconst auto kSerializeVersionTag = u\"#new\"_q;\nconstexpr auto kSerializeVersion = 1;\nconstexpr auto kCloudLangPackName = \"tdesktop\"_cs;\nconstexpr auto kCustomLanguage = \"#custom\"_cs;\nconstexpr auto kLangValuesLimit = 20000;\nconstexpr auto kCustomBrand = \"Forkgram\"_cs;\n\nstd::vector<QString> PrepareDefaultValues() {\n\tauto result = std::vector<QString>();\n\tresult.reserve(kKeysCount);\n\tfor (auto i = 0; i != kKeysCount; ++i) {\n\t\tresult.emplace_back(GetOriginalValue(ushort(i)));\n\t}\n\treturn result;\n}\n\nclass ValueParser {\npublic:\n\tValueParser(\n\t\tconst QByteArray &key,\n\t\tushort keyIndex,\n\t\tconst QByteArray &value);\n\n\tQString takeResult() {\n\t\tExpects(!_failed);\n\n\t\treturn std::move(_result);\n\t}\n\n\tbool parse();\n\nprivate:\n\tvoid appendToResult(const char *nextBegin);\n\tbool logError(const QString &text);\n\tbool readTag();\n\n\tconst QByteArray &_key;\n\tushort _keyIndex = kKeysCount;\n\n\tQLatin1String _currentTag;\n\tushort _currentTagIndex = 0;\n\tQString _currentTagReplacer;\n\n\tbool _failed = true;\n\n\tconst char *_begin = nullptr;\n\tconst char *_ch = nullptr;\n\tconst char *_end = nullptr;\n\n\tQString _result;\n\tOrderedSet<ushort> _tagsUsed;\n\n};\n\nValueParser::ValueParser(\n\tconst QByteArray &key,\n\tushort keyIndex,\n\tconst QByteArray &value)\n: _key(key)\n, _keyIndex(keyIndex)\n, _currentTag(\"\")\n, _begin(value.constData())\n, _ch(_begin)\n, _end(_begin + value.size()) {\n}\n\nvoid ValueParser::appendToResult(const char *nextBegin) {\n\tif (_ch > _begin) _result.append(QString::fromUtf8(_begin, _ch - _begin));\n\t_begin = nextBegin;\n}\n\nbool ValueParser::logError(const QString &text) {\n\t_failed = true;\n\tauto loggedKey = (_currentTag.size() > 0) ? (_key + QString(':') + _currentTag) : QString(_key);\n\tLOG((\"Lang Error: %1 (key '%2')\").arg(text, loggedKey));\n\treturn false;\n}\n\nbool ValueParser::readTag() {\n\tauto tagStart = _ch;\n\tauto isTagChar = [](QChar ch) {\n\t\tif (ch >= 'a' && ch <= 'z') {\n\t\t\treturn true;\n\t\t} else if (ch >= 'A' && ch <= 'Z') {\n\t\t\treturn true;\n\t\t} else if (ch >= '0' && ch <= '9') {\n\t\t\treturn true;\n\t\t}\n\t\treturn (ch == '_');\n\t};\n\twhile (_ch != _end && isTagChar(*_ch)) {\n\t\t++_ch;\n\t}\n\tif (_ch == tagStart) {\n\t\treturn logError(\"Expected tag name\");\n\t}\n\n\t_currentTag = QLatin1String(tagStart, _ch - tagStart);\n\tif (_ch == _end || *_ch != '}') {\n\t\treturn logError(\"Expected '}' after tag name\");\n\t}\n\n\t_currentTagIndex = GetTagIndex(_currentTag);\n\tif (_currentTagIndex == kTagsCount) {\n\t\treturn logError(\"Unknown tag\");\n\t}\n\tif (!IsTagReplaced(_keyIndex, _currentTagIndex)) {\n\t\treturn logError(\"Unexpected tag\");\n\t}\n\tif (_tagsUsed.contains(_currentTagIndex)) {\n\t\treturn logError(\"Repeated tag\");\n\t}\n\t_tagsUsed.insert(_currentTagIndex);\n\n\tif (_currentTagReplacer.isEmpty()) {\n\t\t_currentTagReplacer = QString(4, QChar(kTextCommand));\n\t\t_currentTagReplacer[1] = QChar(kTextCommandLangTag);\n\t}\n\t_currentTagReplacer[2] = QChar(0x0020 + _currentTagIndex);\n\n\treturn true;\n}\n\nbool ValueParser::parse() {\n\t_failed = false;\n\t_result.reserve(_end - _begin);\n\tfor (; _ch != _end; ++_ch) {\n\t\tif (*_ch == '{') {\n\t\t\tappendToResult(_ch);\n\n\t\t\t++_ch;\n\t\t\tif (!readTag()) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t_result.append(_currentTagReplacer);\n\n\t\t\t_begin = _ch + 1;\n\t\t\t_currentTag = QLatin1String(\"\");\n\t\t}\n\t}\n\tappendToResult(_end);\n\treturn true;\n}\n\nQString PrepareTestValue(const QString &current, QChar filler) {\n\tauto size = current.size();\n\tauto result = QString(size + 1, filler);\n\tauto inCommand = false;\n\tfor (auto i = 0; i != size; ++i) {\n\t\tconst auto ch = current[i];\n\t\tconst auto newInCommand = (ch.unicode() == kTextCommand)\n\t\t\t? (!inCommand)\n\t\t\t: inCommand;\n\t\tif (inCommand || newInCommand || ch.isSpace()) {\n\t\t\tresult[i + 1] = ch;\n\t\t}\n\t\tinCommand = newInCommand;\n\t}\n\treturn result;\n}\n\nQString PluralCodeForCustom(\n\tconst QString &absolutePath,\n\tconst QString &relativePath) {\n\tconst auto path = !absolutePath.isEmpty()\n\t\t? absolutePath\n\t\t: relativePath;\n\tconst auto name = QFileInfo(path).fileName();\n\tif (const auto match = qthelp::regex_match(\n\t\t\t\"_([a-z]{2,3}_[A-Z]{2,3}|\\\\-[a-z]{2,3})?\\\\.\",\n\t\t\tname)) {\n\t\treturn match->captured(1);\n\t}\n\treturn DefaultLanguageId();\n}\n\ntemplate <typename Save>\nvoid ParseKeyValue(\n\t\tconst QByteArray &key,\n\t\tconst QByteArray &value,\n\t\tSave &&save) {\n\tconst auto index = GetKeyIndex(QLatin1String(key));\n\tif (index != kKeysCount) {\n\t\tValueParser parser(key, index, value);\n\t\tif (parser.parse()) {\n\t\t\tsave(index, parser.takeResult());\n\t\t}\n\t} else if (!key.startsWith(\"cloud_\")) {\n\t\tDEBUG_LOG((\"Lang Warning: Unknown key '%1'\"\n\t\t\t).arg(QString::fromLatin1(key)));\n\t}\n}\n\n} // namespace\n\nQString CloudLangPackName() {\n\treturn kCloudLangPackName.utf16();\n}\n\nQString CustomLanguageId() {\n\treturn kCustomLanguage.utf16();\n}\n\nLanguage DefaultLanguage() {\n\treturn Language{\n\t\tu\"en\"_q,\n\t\tQString(),\n\t\tQString(),\n\t\tu\"English\"_q,\n\t\tu\"English\"_q,\n\t};\n}\n\nstruct Instance::PrivateTag {\n};\n\nInstance::Instance()\n: _values(PrepareDefaultValues())\n, _nonDefaultSet(kKeysCount, 0) {\n}\n\nInstance::Instance(not_null<Instance*> derived, const PrivateTag &)\n: _derived(derived)\n, _nonDefaultSet(kKeysCount, 0) {\n}\n\nvoid Instance::switchToId(const Language &data) {\n\treset(data);\n\tif (_id == u\"#TEST_X\"_q || _id == u\"#TEST_0\"_q) {\n\t\tfor (auto &value : _values) {\n\t\t\tvalue = PrepareTestValue(value, _id[5]);\n\t\t}\n\t\tif (!_derived) {\n\t\t\t_updated.fire({});\n\t\t}\n\t}\n\tupdatePluralRules();\n}\n\nvoid Instance::setBaseId(const QString &baseId, const QString &pluralId) {\n\tif (baseId.isEmpty()) {\n\t\t_base = nullptr;\n\t} else {\n\t\tif (!_base) {\n\t\t\t_base = std::make_unique<Instance>(this, PrivateTag{});\n\t\t}\n\t\t_base->switchToId({ baseId, pluralId });\n\t}\n}\n\nvoid Instance::switchToCustomFile(const QString &filePath) {\n\tif (loadFromCustomFile(filePath)) {\n\t\tLocal::writeLangPack();\n\t\t_updated.fire({});\n\t}\n}\n\nvoid Instance::reset(const Language &data) {\n\tconst auto computedPluralId = !data.pluralId.isEmpty()\n\t\t? data.pluralId\n\t\t: !data.baseId.isEmpty()\n\t\t? data.baseId\n\t\t: data.id;\n\tsetBaseId(data.baseId, computedPluralId);\n\t_id = LanguageIdOrDefault(data.id);\n\t_pluralId = computedPluralId;\n\t_name = data.name;\n\t_nativeName = data.nativeName;\n\n\t_customFilePathAbsolute = QString();\n\t_customFilePathRelative = QString();\n\t_customFileContent = QByteArray();\n\t_version = 0;\n\t_nonDefaultValues.clear();\n\tfor (auto i = 0, count = int(_values.size()); i != count; ++i) {\n\t\t_values[i] = GetOriginalValue(ushort(i));\n\t}\n\tranges::fill(_nonDefaultSet, 0);\n\tupdateChoosingStickerReplacement();\n\n\t_idChanges.fire_copy(_id);\n}\n\nQString Instance::systemLangCode() const {\n\tif (_systemLanguage.isEmpty()) {\n\t\t_systemLanguage = Platform::SystemLanguage();\n\t\tif (_systemLanguage.isEmpty()) {\n\t\t\tauto uiLanguages = QLocale::system().uiLanguages();\n\t\t\tif (!uiLanguages.isEmpty()) {\n\t\t\t\t_systemLanguage = uiLanguages.front();\n\t\t\t}\n\t\t\tif (_systemLanguage.isEmpty()) {\n\t\t\t\t_systemLanguage = DefaultLanguageId();\n\t\t\t}\n\t\t}\n\t}\n\treturn _systemLanguage;\n}\n\nQString Instance::cloudLangCode(Pack pack) const {\n\treturn (isCustom() || id().isEmpty())\n\t\t? DefaultLanguageId()\n\t\t: id(pack);\n}\n\nQString Instance::id() const {\n\treturn id(Pack::Current);\n}\n\nrpl::producer<QString> Instance::idChanges() const {\n\treturn _idChanges.events();\n}\n\nQString Instance::baseId() const {\n\treturn id(Pack::Base);\n}\n\nQString Instance::name() const {\n\treturn _name.isEmpty()\n\t\t? getValue(tr::lng_language_name.base)\n\t\t: _name;\n}\n\nQString Instance::nativeName() const {\n\treturn _nativeName.isEmpty()\n\t\t? getValue(tr::lng_language_name.base)\n\t\t: _nativeName;\n}\n\nQString Instance::id(Pack pack) const {\n\treturn (pack != Pack::Base)\n\t\t? _id\n\t\t: _base\n\t\t? _base->id(Pack::Current)\n\t\t: QString();\n}\n\nbool Instance::isCustom() const {\n\treturn (_id == CustomLanguageId())\n\t\t|| (_id == u\"#TEST_X\"_q)\n\t\t|| (_id == u\"#TEST_0\"_q);\n}\n\nint Instance::version(Pack pack) const {\n\treturn (pack != Pack::Base)\n\t\t? _version\n\t\t: _base\n\t\t? _base->version(Pack::Current)\n\t\t: 0;\n}\n\nQString Instance::langPackName() const {\n\treturn isCustom() ? QString() : CloudLangPackName();\n}\n\nQByteArray Instance::serialize() const {\n\tauto size = Serialize::stringSize(kSerializeVersionTag)\n\t\t+ sizeof(qint32) // serializeVersion\n\t\t+ Serialize::stringSize(_id)\n\t\t+ Serialize::stringSize(_pluralId)\n\t\t+ Serialize::stringSize(_name)\n\t\t+ Serialize::stringSize(_nativeName)\n\t\t+ sizeof(qint32) // version\n\t\t+ Serialize::stringSize(_customFilePathAbsolute)\n\t\t+ Serialize::stringSize(_customFilePathRelative)\n\t\t+ Serialize::bytearraySize(_customFileContent)\n\t\t+ sizeof(qint32); // _nonDefaultValues.size()\n\tfor (auto &nonDefault : _nonDefaultValues) {\n\t\tsize += Serialize::bytearraySize(nonDefault.first)\n\t\t\t+ Serialize::bytearraySize(nonDefault.second);\n\t}\n\tconst auto base = _base ? _base->serialize() : QByteArray();\n\tsize += Serialize::bytearraySize(base);\n\n\tauto result = QByteArray();\n\tresult.reserve(size);\n\t{\n\t\tQDataStream stream(&result, QIODevice::WriteOnly);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\t\tstream\n\t\t\t<< kSerializeVersionTag\n\t\t\t<< qint32(kSerializeVersion)\n\t\t\t<< _id\n\t\t\t<< _pluralId\n\t\t\t<< _name\n\t\t\t<< _nativeName\n\t\t\t<< qint32(_version)\n\t\t\t<< _customFilePathAbsolute\n\t\t\t<< _customFilePathRelative\n\t\t\t<< _customFileContent\n\t\t\t<< qint32(_nonDefaultValues.size());\n\t\tfor (const auto &nonDefault : _nonDefaultValues) {\n\t\t\tstream << nonDefault.first << nonDefault.second;\n\t\t}\n\t\tstream << base;\n\t}\n\treturn result;\n}\n\nvoid Instance::fillFromSerialized(\n\t\tconst QByteArray &data,\n\t\tint dataAppVersion) {\n\tQDataStream stream(data);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\tqint32 serializeVersion = 0;\n\tQString serializeVersionTag;\n\tQString id, pluralId, name, nativeName;\n\tqint32 version = 0;\n\tQString customFilePathAbsolute, customFilePathRelative;\n\tQByteArray customFileContent;\n\tqint32 nonDefaultValuesCount = 0;\n\tstream >> serializeVersionTag;\n\tconst auto legacyFormat = (serializeVersionTag != kSerializeVersionTag);\n\tif (legacyFormat) {\n\t\tid = serializeVersionTag;\n\t\tstream\n\t\t\t>> version\n\t\t\t>> customFilePathAbsolute\n\t\t\t>> customFilePathRelative\n\t\t\t>> customFileContent\n\t\t\t>> nonDefaultValuesCount;\n\t} else {\n\t\tstream >> serializeVersion;\n\t\tif (serializeVersion == kSerializeVersion) {\n\t\t\tstream\n\t\t\t\t>> id\n\t\t\t\t>> pluralId\n\t\t\t\t>> name\n\t\t\t\t>> nativeName\n\t\t\t\t>> version\n\t\t\t\t>> customFilePathAbsolute\n\t\t\t\t>> customFilePathRelative\n\t\t\t\t>> customFileContent\n\t\t\t\t>> nonDefaultValuesCount;\n\t\t} else {\n\t\t\tLOG((\"Lang Error: Unsupported serialize version.\"));\n\t\t\treturn;\n\t\t}\n\t}\n\tif (stream.status() != QDataStream::Ok) {\n\t\tLOG((\"Lang Error: Could not read data from serialized langpack.\"));\n\t\treturn;\n\t}\n\tif (nonDefaultValuesCount > kLangValuesLimit) {\n\t\tLOG((\"Lang Error: Values count limit exceeded: %1\"\n\t\t\t).arg(nonDefaultValuesCount));\n\t\treturn;\n\t}\n\n\tif (!customFilePathAbsolute.isEmpty()) {\n\t\tid = CustomLanguageId();\n\t\tauto currentCustomFileContent = Lang::FileParser::ReadFile(\n\t\t\tcustomFilePathAbsolute,\n\t\t\tcustomFilePathRelative);\n\t\tif (!currentCustomFileContent.isEmpty()\n\t\t\t&& currentCustomFileContent != customFileContent) {\n\t\t\tfillFromCustomContent(\n\t\t\t\tcustomFilePathAbsolute,\n\t\t\t\tcustomFilePathRelative,\n\t\t\t\tcurrentCustomFileContent);\n\t\t\tLocal::writeLangPack();\n\t\t\treturn;\n\t\t}\n\t}\n\n\tstd::vector<QByteArray> nonDefaultStrings;\n\tnonDefaultStrings.reserve(2 * nonDefaultValuesCount);\n\tfor (auto i = 0; i != nonDefaultValuesCount; ++i) {\n\t\tQByteArray key, value;\n\t\tstream >> key >> value;\n\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\tLOG((\"Lang Error: \"\n\t\t\t\t\"Could not read data from serialized langpack.\"));\n\t\t\treturn;\n\t\t}\n\n\t\tnonDefaultStrings.push_back(key);\n\t\tnonDefaultStrings.push_back(value);\n\t}\n\n\t_base = nullptr;\n\tQByteArray base;\n\tif (legacyFormat) {\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> pluralId;\n\t\t} else {\n\t\t\tpluralId = id;\n\t\t}\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> base;\n\t\t\tif (base.isEmpty()) {\n\t\t\t\tstream.setStatus(QDataStream::ReadCorruptData);\n\t\t\t}\n\t\t}\n\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\tLOG((\"Lang Error: \"\n\t\t\t\t\"Could not read data from serialized langpack.\"));\n\t\t\treturn;\n\t\t}\n\t} else {\n\t\tstream >> base;\n\t}\n\tif (!base.isEmpty()) {\n\t\t_base = std::make_unique<Instance>(this, PrivateTag{});\n\t\t_base->fillFromSerialized(base, dataAppVersion);\n\t}\n\n\t_id = id;\n\t_pluralId = (id == CustomLanguageId())\n\t\t? PluralCodeForCustom(\n\t\t\tcustomFilePathAbsolute,\n\t\t\tcustomFilePathRelative)\n\t\t: pluralId;\n\t_name = name;\n\t_nativeName = nativeName;\n\t_version = version;\n\t_customFilePathAbsolute = customFilePathAbsolute;\n\t_customFilePathRelative = customFilePathRelative;\n\t_customFileContent = customFileContent;\n\tLOG((\"Lang Info: Loaded cached, keys: %1\").arg(nonDefaultValuesCount));\n\tfor (auto i = 0, count = nonDefaultValuesCount * 2; i != count; i += 2) {\n\t\tapplyValue(nonDefaultStrings[i], nonDefaultStrings[i + 1]);\n\t}\n\tupdatePluralRules();\n\tupdateChoosingStickerReplacement();\n\n\t_idChanges.fire_copy(_id);\n}\n\nvoid Instance::loadFromContent(const QByteArray &content) {\n\tLang::FileParser loader(content, [this](QLatin1String key, const QByteArray &value) {\n\t\tapplyValue(QByteArray(key.data(), key.size()), value);\n\t});\n\tif (!loader.errors().isEmpty()) {\n\t\tLOG((\"Lang load errors: %1\").arg(loader.errors()));\n\t} else if (!loader.warnings().isEmpty()) {\n\t\tLOG((\"Lang load warnings: %1\").arg(loader.warnings()));\n\t}\n}\n\nvoid Instance::fillFromCustomContent(\n\t\tconst QString &absolutePath,\n\t\tconst QString &relativePath,\n\t\tconst QByteArray &content) {\n\tsetBaseId(QString(), QString());\n\t_id = CustomLanguageId();\n\t_pluralId = PluralCodeForCustom(absolutePath, relativePath);\n\t_name = _nativeName = QString();\n\tloadFromCustomContent(absolutePath, relativePath, content);\n\tupdateChoosingStickerReplacement();\n\n\t_idChanges.fire_copy(_id);\n}\n\nvoid Instance::loadFromCustomContent(\n\t\tconst QString &absolutePath,\n\t\tconst QString &relativePath,\n\t\tconst QByteArray &content) {\n\t_version = 0;\n\t_customFilePathAbsolute = absolutePath;\n\t_customFilePathRelative = relativePath;\n\t_customFileContent = content;\n\tloadFromContent(_customFileContent);\n}\n\nbool Instance::loadFromCustomFile(const QString &filePath) {\n\tauto absolutePath = QFileInfo(filePath).absoluteFilePath();\n\tauto relativePath = QDir().relativeFilePath(filePath);\n\tauto content = Lang::FileParser::ReadFile(absolutePath, relativePath);\n\tif (!content.isEmpty()) {\n\t\treset({\n\t\t\tCustomLanguageId(),\n\t\t\tPluralCodeForCustom(absolutePath, relativePath) });\n\t\tloadFromCustomContent(absolutePath, relativePath, content);\n\t\tupdatePluralRules();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Instance::updateChoosingStickerReplacement() {\n\t// A language changing in the runtime is not supported.\n\tconst auto replacement = kChoosingStickerReplacement.utf8();\n\tconst auto phrase = tr::lng_send_action_choose_sticker(tr::now);\n\tconst auto first = phrase.indexOf(replacement);\n\tconst auto support = (first != -1);\n\tconst auto phraseNamed = tr::lng_user_action_choose_sticker(\n\t\ttr::now,\n\t\tlt_user,\n\t\tQString());\n\tconst auto firstNamed = phraseNamed.indexOf(replacement);\n\tconst auto supportNamed = (firstNamed != -1);\n\n\t_choosingStickerReplacement.support = (supportNamed && support);\n\t_choosingStickerReplacement.rightIndex = phrase.size() - first;\n\t_choosingStickerReplacement.rightIndexNamed = phraseNamed.size()\n\t\t- firstNamed;\n}\n\nbool Instance::supportChoosingStickerReplacement() const {\n\treturn _choosingStickerReplacement.support;\n}\n\nint Instance::rightIndexChoosingStickerReplacement(bool named) const {\n\treturn named\n\t\t? _choosingStickerReplacement.rightIndexNamed\n\t\t: _choosingStickerReplacement.rightIndex;\n}\n\n// SetCallback takes two QByteArrays: key, value.\n// It is called for all key-value pairs in string.\n// ResetCallback takes one QByteArray: key.\ntemplate <typename SetCallback, typename ResetCallback>\nvoid HandleString(\n\t\tconst MTPLangPackString &string,\n\t\tSetCallback setCallback,\n\t\tResetCallback resetCallback) {\n\tstring.match([&](const MTPDlangPackString &data) {\n\t\tsetCallback(qba(data.vkey()), qba(data.vvalue()));\n\t}, [&](const MTPDlangPackStringPluralized &data) {\n\t\tconst auto key = qba(data.vkey());\n\t\tsetCallback(key + \"#zero\", data.vzero_value().value_or_empty());\n\t\tsetCallback(key + \"#one\", data.vone_value().value_or_empty());\n\t\tsetCallback(key + \"#two\", data.vtwo_value().value_or_empty());\n\t\tsetCallback(key + \"#few\", data.vfew_value().value_or_empty());\n\t\tsetCallback(key + \"#many\", data.vmany_value().value_or_empty());\n\t\tsetCallback(key + \"#other\", qba(data.vother_value()));\n\t}, [&](const MTPDlangPackStringDeleted &data) {\n\t\tauto key = qba(data.vkey());\n\t\tresetCallback(key);\n\t\tconst auto postfixes = {\n\t\t\t\"#zero\",\n\t\t\t\"#one\",\n\t\t\t\"#two\",\n\t\t\t\"#few\",\n\t\t\t\"#many\",\n\t\t\t\"#other\"\n\t\t};\n\t\tfor (const auto plural : postfixes) {\n\t\t\tresetCallback(key + plural);\n\t\t}\n\t});\n}\n\nvoid Instance::applyDifference(\n\t\tPack pack,\n\t\tconst MTPDlangPackDifference &difference) {\n\tswitch (pack) {\n\tcase Pack::Current:\n\t\tapplyDifferenceToMe(difference);\n\t\tbreak;\n\tcase Pack::Base:\n\t\tAssert(_base != nullptr);\n\t\t_base->applyDifference(Pack::Current, difference);\n\t\tbreak;\n\tdefault:\n\t\tUnexpected(\"Pack in Instance::applyDifference.\");\n\t}\n}\n\nvoid Instance::applyDifferenceToMe(\n\t\tconst MTPDlangPackDifference &difference) {\n\tExpects(LanguageIdOrDefault(_id) == qs(difference.vlang_code()));\n\tExpects(difference.vfrom_version().v <= _version);\n\n\t_version = difference.vversion().v;\n\tfor (const auto &string : difference.vstrings().v) {\n\t\tHandleString(string, [&](auto &&key, auto &&value) {\n\t\t\tapplyValue(key, value);\n\t\t}, [&](auto &&key) {\n\t\t\tresetValue(key);\n\t\t});\n\t}\n\tif (!_derived) {\n\t\t_updated.fire({});\n\t} else {\n\t\t_derived->_updated.fire({});\n\t}\n}\n\nstd::map<ushort, QString> Instance::ParseStrings(\n\t\tconst MTPVector<MTPLangPackString> &strings) {\n\tauto result = std::map<ushort, QString>();\n\tfor (const auto &string : strings.v) {\n\t\tHandleString(string, [&](auto &&key, auto &&value) {\n\t\t\tParseKeyValue(key, value, [&](ushort key, QString &&value) {\n\t\t\t\tresult[key] = std::move(value);\n\t\t\t});\n\t\t}, [&](auto &&key) {\n\t\t\tauto keyIndex = GetKeyIndex(QLatin1String(key));\n\t\t\tif (keyIndex != kKeysCount) {\n\t\t\t\tresult.erase(keyIndex);\n\t\t\t}\n\t\t});\n\t}\n\treturn result;\n}\n\nQString Instance::getNonDefaultValue(const QByteArray &key) const {\n\tconst auto i = _nonDefaultValues.find(key);\n\treturn (i != end(_nonDefaultValues))\n\t\t? QString::fromUtf8(i->second)\n\t\t: _base\n\t\t? _base->getNonDefaultValue(key)\n\t\t: QString();\n}\n\nvoid Instance::applyValue(const QByteArray &key, const QByteArray &value) {\n\t_nonDefaultValues[key] = value;\n\tParseKeyValue(key, value, [&](ushort key, QString &&value) {\n\t\t_nonDefaultSet[key] = 1;\n\t\tif (!_derived) {\n\t\t\tif (ranges::contains(tr::hasTelegram, key)) {\n\t\t\t\tauto v = std::move(value);\n\t\t\t\t_values[key] = v.replace(\n\t\t\t\t\tQRegularExpression(\"Telegram\"),\n\t\t\t\t\tkCustomBrand.utf16());\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_values[key] = std::move(value);\n\t\t} else if (!_derived->_nonDefaultSet[key]) {\n\t\t\t_derived->_values[key] = std::move(value);\n\t\t}\n\t\tif (key == tr::lng_send_action_choose_sticker.base\n\t\t\t|| key == tr::lng_user_action_choose_sticker.base) {\n\t\t\tif (!_derived) {\n\t\t\t\tupdateChoosingStickerReplacement();\n\t\t\t} else {\n\t\t\t\t_derived->updateChoosingStickerReplacement();\n\t\t\t}\n\t\t}\n\t});\n}\n\nvoid Instance::updatePluralRules() {\n\tif (_pluralId.isEmpty()) {\n\t\t_pluralId = isCustom()\n\t\t\t? PluralCodeForCustom(\n\t\t\t\t_customFilePathAbsolute,\n\t\t\t\t_customFilePathRelative)\n\t\t\t: LanguageIdOrDefault(_id);\n\t}\n\tUpdatePluralRules(_pluralId);\n}\n\nvoid Instance::resetValue(const QByteArray &key) {\n\t_nonDefaultValues.erase(key);\n\n\tconst auto keyIndex = GetKeyIndex(QLatin1String(key));\n\tif (keyIndex != kKeysCount) {\n\t\t_nonDefaultSet[keyIndex] = 0;\n\t\tif (!_derived) {\n\t\t\tconst auto base = _base\n\t\t\t\t? _base->getNonDefaultValue(key)\n\t\t\t\t: QString();\n\t\t\t_values[keyIndex] = !base.isEmpty()\n\t\t\t\t? base\n\t\t\t\t: GetOriginalValue(keyIndex);\n\t\t} else if (!_derived->_nonDefaultSet[keyIndex]) {\n\t\t\t_derived->_values[keyIndex] = GetOriginalValue(keyIndex);\n\t\t}\n\t\tif (keyIndex == tr::lng_send_action_choose_sticker.base\n\t\t\t|| keyIndex == tr::lng_user_action_choose_sticker.base) {\n\t\t\tif (!_derived) {\n\t\t\t\tupdateChoosingStickerReplacement();\n\t\t\t} else {\n\t\t\t\t_derived->updateChoosingStickerReplacement();\n\t\t\t}\n\t\t}\n\t}\n}\n\nInstance &GetInstance() {\n\treturn Core::App().langpack();\n}\n\nQString Id() {\n\treturn GetInstance().id();\n}\n\nrpl::producer<> Updated() {\n\treturn GetInstance().updated();\n}\n\nQString GetNonDefaultValue(const QByteArray &key) {\n\treturn GetInstance().getNonDefaultValue(key);\n}\n\nnamespace details {\n\nQString Current(ushort key) {\n\treturn GetInstance().getValue(key);\n}\n\nrpl::producer<QString> Value(ushort key) {\n\treturn rpl::single(\n\t\tCurrent(key)\n\t) | then(\n\t\tUpdated() | rpl::map([=] { return Current(key); })\n\t);\n}\n\nbool IsNonDefaultPlural(ushort keyBase) {\n\treturn GetInstance().isNonDefaultPlural(keyBase);\n}\n\n} // namespace details\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_instance.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"lang_auto.h\"\n#include \"base/const_string.h\"\n#include \"base/weak_ptr.h\"\n\nnamespace Lang {\n\ninline constexpr auto kChoosingStickerReplacement = \"oo\"_cs;\n\nstruct Language {\n\tQString id;\n\tQString pluralId;\n\tQString baseId;\n\tQString name;\n\tQString nativeName;\n};\n\ninline bool operator==(const Language &a, const Language &b) {\n\treturn (a.id == b.id) && (a.name == b.name);\n}\n\ninline bool operator!=(const Language &a, const Language &b) {\n\treturn !(a == b);\n}\n\nQString CloudLangPackName();\nQString CustomLanguageId();\nLanguage DefaultLanguage();\n\nclass Instance;\nInstance &GetInstance();\n\nenum class Pack {\n\tNone,\n\tCurrent,\n\tBase,\n};\n\nclass Instance {\n\tstruct PrivateTag;\n\npublic:\n\tInstance();\n\tInstance(not_null<Instance*> derived, const PrivateTag &);\n\n\tvoid switchToId(const Language &language);\n\tvoid switchToCustomFile(const QString &filePath);\n\n\tInstance(const Instance &other) = delete;\n\tInstance &operator=(const Instance &other) = delete;\n\tInstance(Instance &&other) = default;\n\tInstance &operator=(Instance &&other) = default;\n\n\tQString systemLangCode() const;\n\tQString langPackName() const;\n\tQString cloudLangCode(Pack pack) const;\n\tQString id() const;\n\trpl::producer<QString> idChanges() const;\n\tQString baseId() const;\n\tQString name() const;\n\tQString nativeName() const;\n\tQString id(Pack pack) const;\n\tbool isCustom() const;\n\tint version(Pack pack) const;\n\n\tQByteArray serialize() const;\n\tvoid fillFromSerialized(const QByteArray &data, int dataAppVersion);\n\n\tbool supportChoosingStickerReplacement() const;\n\tint rightIndexChoosingStickerReplacement(bool named) const;\n\n\tvoid applyDifference(\n\t\tPack pack,\n\t\tconst MTPDlangPackDifference &difference);\n\tstatic std::map<ushort, QString> ParseStrings(\n\t\tconst MTPVector<MTPLangPackString> &strings);\n\n\t[[nodiscard]] rpl::producer<> updated() const {\n\t\treturn _updated.events();\n\t}\n\n\tQString getValue(ushort key) const {\n\t\tExpects(key < _values.size());\n\n\t\treturn _values[key];\n\t}\n\tQString getNonDefaultValue(const QByteArray &key) const;\n\tbool isNonDefaultPlural(ushort key) const {\n\t\tExpects(key + 5 < _nonDefaultSet.size());\n\n\t\treturn _nonDefaultSet[key]\n\t\t\t|| _nonDefaultSet[key + 1]\n\t\t\t|| _nonDefaultSet[key + 2]\n\t\t\t|| _nonDefaultSet[key + 3]\n\t\t\t|| _nonDefaultSet[key + 4]\n\t\t\t|| _nonDefaultSet[key + 5]\n\t\t\t|| (_base && _base->isNonDefaultPlural(key));\n\t}\n\nprivate:\n\tvoid setBaseId(const QString &baseId, const QString &pluralId);\n\n\tvoid applyDifferenceToMe(const MTPDlangPackDifference &difference);\n\tvoid applyValue(const QByteArray &key, const QByteArray &value);\n\tvoid resetValue(const QByteArray &key);\n\tvoid reset(const Language &language);\n\tvoid fillFromCustomContent(\n\t\tconst QString &absolutePath,\n\t\tconst QString &relativePath,\n\t\tconst QByteArray &content);\n\tbool loadFromCustomFile(const QString &filePath);\n\tvoid loadFromContent(const QByteArray &content);\n\tvoid loadFromCustomContent(\n\t\tconst QString &absolutePath,\n\t\tconst QString &relativePath,\n\t\tconst QByteArray &content);\n\tvoid updatePluralRules();\n\tvoid updateChoosingStickerReplacement();\n\n\tInstance *_derived = nullptr;\n\n\tQString _id, _pluralId;\n\trpl::event_stream<QString> _idChanges;\n\tQString _name, _nativeName;\n\tQString _customFilePathAbsolute;\n\tQString _customFilePathRelative;\n\tQByteArray _customFileContent;\n\tint _version = 0;\n\trpl::event_stream<> _updated;\n\n\tstruct {\n\t\tbool support = false;\n\t\tint rightIndex = 0;\n\t\tint rightIndexNamed = 0;\n\t} _choosingStickerReplacement;\n\n\tmutable QString _systemLanguage;\n\n\tstd::vector<QString> _values;\n\tstd::vector<uchar> _nonDefaultSet;\n\tstd::map<QByteArray, QByteArray> _nonDefaultValues;\n\n\tstd::unique_ptr<Instance> _base;\n\n};\n\nnamespace details {\n\nQString Current(ushort key);\nrpl::producer<QString> Value(ushort key);\n\n} // namespace details\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_keys.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"lang/lang_keys.h\"\n\n#include \"base/const_string.h\"\n#include \"lang/lang_file_parser.h\"\n#include \"ui/integration.h\"\n\n#include <QtCore/QLocale>\n\nnamespace {\n\nconstexpr auto kDefaultLanguage = \"en\"_cs;\n\ntemplate <typename WithYear, typename WithoutYear>\ninline QString langDateMaybeWithYear(\n\t\tQDate date,\n\t\tWithYear withYear,\n\t\tWithoutYear withoutYear) {\n\tconst auto month = date.month();\n\tif (month <= 0 || month > 12) {\n\t\treturn u\"MONTH_ERR\"_q;\n\t};\n\tconst auto year = date.year();\n\tconst auto current = QDate::currentDate();\n\tconst auto currentYear = current.year();\n\tconst auto currentMonth = current.month();\n\tif (year != currentYear) {\n\t\tconst auto yearIsMuchGreater = [](int year, int otherYear) {\n\t\t\treturn (year > otherYear + 1);\n\t\t};\n\t\tconst auto monthIsMuchGreater = [](\n\t\t\t\tint year,\n\t\t\t\tint month,\n\t\t\t\tint otherYear,\n\t\t\t\tint otherMonth) {\n\t\t\treturn (year == otherYear + 1) && (month + 12 > otherMonth + 3);\n\t\t};\n\t\tif (false\n\t\t\t|| yearIsMuchGreater(year, currentYear)\n\t\t\t|| yearIsMuchGreater(currentYear, year)\n\t\t\t|| monthIsMuchGreater(year, month, currentYear, currentMonth)\n\t\t\t|| monthIsMuchGreater(currentYear, currentMonth, year, month)) {\n\t\t\treturn withYear(month, year);\n\t\t}\n\t}\n\treturn withoutYear(month, year);\n}\n\nusing namespace Lang;\n\n} // namespace\n\nbool langFirstNameGoesSecond() {\n\tconst auto kFirstName = QChar(0x0001);\n\tconst auto kLastName = QChar(0x0002);\n\tconst auto fullname = tr::lng_full_name(\n\t\ttr::now,\n\t\tlt_first_name,\n\t\tQString(1, kFirstName),\n\t\tlt_last_name,\n\t\tQString(1, kLastName));\n\treturn fullname.indexOf(kLastName) < fullname.indexOf(kFirstName);\n}\n\nQString langDayOfMonth(const QDate &date) {\n\tauto day = date.day();\n\treturn langDateMaybeWithYear(date, [&](int month, int year) {\n\t\treturn tr::lng_month_day_year(\n\t\t\ttr::now,\n\t\t\tlt_month,\n\t\t\tMonthSmall(month)(tr::now),\n\t\t\tlt_day,\n\t\t\tQString::number(day),\n\t\t\tlt_year,\n\t\t\tQString::number(year));\n\t}, [day](int month, int year) {\n\t\treturn tr::lng_month_day(\n\t\t\ttr::now,\n\t\t\tlt_month,\n\t\t\tMonthSmall(month)(tr::now),\n\t\t\tlt_day,\n\t\t\tQString::number(day));\n\t});\n}\n\nQString langDayOfMonthFull(const QDate &date) {\n\tauto day = date.day();\n\treturn langDateMaybeWithYear(date, [day](int month, int year) {\n\t\treturn tr::lng_month_day_year(\n\t\t\ttr::now,\n\t\t\tlt_month,\n\t\t\tMonthDay(month)(tr::now),\n\t\t\tlt_day,\n\t\t\tQString::number(day),\n\t\t\tlt_year,\n\t\t\tQString::number(year));\n\t}, [day](int month, int year) {\n\t\treturn tr::lng_month_day(\n\t\t\ttr::now,\n\t\t\tlt_month,\n\t\t\tMonthDay(month)(tr::now),\n\t\t\tlt_day,\n\t\t\tQString::number(day));\n\t});\n}\n\nQString langDayOfMonthShort(const QDate &date) {\n\tauto day = date.day();\n\treturn langDateMaybeWithYear(date, [&](int month, int year) {\n\t\treturn QLocale().toString(date, QLocale::ShortFormat);\n\t}, [day](int month, int year) {\n\t\treturn tr::lng_month_day(\n\t\t\ttr::now,\n\t\t\tlt_month,\n\t\t\tMonthSmall(month)(tr::now),\n\t\t\tlt_day,\n\t\t\tQString::number(day));\n\t});\n}\n\nQString langMonthOfYear(int month, int year) {\n\treturn (month > 0 && month <= 12)\n\t\t? tr::lng_month_year(\n\t\t\ttr::now,\n\t\t\tlt_month,\n\t\t\tMonthSmall(month)(tr::now),\n\t\t\tlt_year,\n\t\t\tQString::number(year))\n\t\t: u\"MONTH_ERR\"_q;\n}\n\nQString langMonth(const QDate &date) {\n\treturn langDateMaybeWithYear(date, [](int month, int year) {\n\t\treturn langMonthOfYear(month, year);\n\t}, [](int month, int year) {\n\t\treturn MonthSmall(month)(tr::now);\n\t});\n}\n\nQString langMonthOfYearFull(int month, int year) {\n\treturn (month > 0 && month <= 12)\n\t\t? tr::lng_month_year(\n\t\t\ttr::now,\n\t\t\tlt_month,\n\t\t\tMonth(month)(tr::now),\n\t\t\tlt_year,\n\t\t\tQString::number(year))\n\t\t: u\"MONTH_ERR\"_q;\n}\n\nQString langMonthFull(const QDate &date) {\n\treturn langDateMaybeWithYear(date, [](int month, int year) {\n\t\treturn langMonthOfYearFull(month, year);\n\t}, [](int month, int year) {\n\t\treturn Month(month)(tr::now);\n\t});\n}\n\nQString langDayOfWeek(int index) {\n\treturn (index > 0 && index <= 7)\n\t\t? Weekday(index)(tr::now)\n\t\t: u\"DAY_ERR\"_q;\n}\n\nQString langDayOfWeekFull(int index) {\n\treturn (index > 0 && index <= 7)\n\t\t? WeekdayFull(index)(tr::now)\n\t\t: u\"DAY_ERR\"_q;\n}\n\nQString langDateTime(const QDateTime &date) {\n\treturn tr::lng_mediaview_date_time(\n\t\ttr::now,\n\t\tlt_date,\n\t\tlangDayOfMonth(date.date()),\n\t\tlt_time,\n\t\tQLocale().toString(date.time(), QLocale::ShortFormat));\n}\n\nQString langDateTimeFull(const QDateTime &date) {\n\treturn tr::lng_mediaview_date_time(\n\t\ttr::now,\n\t\tlt_date,\n\t\tlangDayOfMonthFull(date.date()),\n\t\tlt_time,\n\t\tQLocale().toString(date.time(), QLocale::ShortFormat));\n}\n\nnamespace Lang {\n\nQString DefaultLanguageId() {\n\treturn kDefaultLanguage.utf16();\n}\n\nQString LanguageIdOrDefault(const QString &id) {\n\treturn !id.isEmpty() ? id : DefaultLanguageId();\n}\n\ntr::phrase<> Month(int index) {\n\tswitch (index) {\n\tcase 1: return tr::lng_month1;\n\tcase 2: return tr::lng_month2;\n\tcase 3: return tr::lng_month3;\n\tcase 4: return tr::lng_month4;\n\tcase 5: return tr::lng_month5;\n\tcase 6: return tr::lng_month6;\n\tcase 7: return tr::lng_month7;\n\tcase 8: return tr::lng_month8;\n\tcase 9: return tr::lng_month9;\n\tcase 10: return tr::lng_month10;\n\tcase 11: return tr::lng_month11;\n\tcase 12: return tr::lng_month12;\n\t}\n\tUnexpected(\"Index in MonthSmall.\");\n}\n\ntr::phrase<> MonthSmall(int index) {\n\tswitch (index) {\n\tcase 1: return tr::lng_month1_small;\n\tcase 2: return tr::lng_month2_small;\n\tcase 3: return tr::lng_month3_small;\n\tcase 4: return tr::lng_month4_small;\n\tcase 5: return tr::lng_month5_small;\n\tcase 6: return tr::lng_month6_small;\n\tcase 7: return tr::lng_month7_small;\n\tcase 8: return tr::lng_month8_small;\n\tcase 9: return tr::lng_month9_small;\n\tcase 10: return tr::lng_month10_small;\n\tcase 11: return tr::lng_month11_small;\n\tcase 12: return tr::lng_month12_small;\n\t}\n\tUnexpected(\"Index in MonthSmall.\");\n}\n\ntr::phrase<> MonthDay(int index) {\n\tswitch (index) {\n\tcase 1: return tr::lng_month_day1;\n\tcase 2: return tr::lng_month_day2;\n\tcase 3: return tr::lng_month_day3;\n\tcase 4: return tr::lng_month_day4;\n\tcase 5: return tr::lng_month_day5;\n\tcase 6: return tr::lng_month_day6;\n\tcase 7: return tr::lng_month_day7;\n\tcase 8: return tr::lng_month_day8;\n\tcase 9: return tr::lng_month_day9;\n\tcase 10: return tr::lng_month_day10;\n\tcase 11: return tr::lng_month_day11;\n\tcase 12: return tr::lng_month_day12;\n\t}\n\tUnexpected(\"Index in MonthDay.\");\n}\n\ntr::phrase<> Weekday(int index) {\n\tswitch (index) {\n\tcase 1: return tr::lng_weekday1;\n\tcase 2: return tr::lng_weekday2;\n\tcase 3: return tr::lng_weekday3;\n\tcase 4: return tr::lng_weekday4;\n\tcase 5: return tr::lng_weekday5;\n\tcase 6: return tr::lng_weekday6;\n\tcase 7: return tr::lng_weekday7;\n\t}\n\tUnexpected(\"Index in Weekday.\");\n}\n\ntr::phrase<> WeekdayFull(int index) {\n\tswitch (index) {\n\tcase 1: return tr::lng_hours_monday;\n\tcase 2: return tr::lng_hours_tuesday;\n\tcase 3: return tr::lng_hours_wednesday;\n\tcase 4: return tr::lng_hours_thursday;\n\tcase 5: return tr::lng_hours_friday;\n\tcase 6: return tr::lng_hours_saturday;\n\tcase 7: return tr::lng_hours_sunday;\n\t}\n\tUnexpected(\"Index in WeekdayFull.\");\n}\n\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_keys.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"lang_auto.h\"\n#include \"lang/lang_hardcoded.h\"\n#include \"lang/lang_text_entity.h\"\n\n#include <QDateTime>\n\n[[nodiscard]] QString langDayOfMonth(const QDate &date);\n[[nodiscard]] QString langDayOfMonthFull(const QDate &date);\n[[nodiscard]] QString langDayOfMonthShort(const QDate &date);\n[[nodiscard]] QString langMonthOfYear(int month, int year);\n[[nodiscard]] QString langMonth(const QDate &date);\n[[nodiscard]] QString langMonthOfYearFull(int month, int year);\n[[nodiscard]] QString langMonthFull(const QDate &date);\n[[nodiscard]] QString langDayOfWeek(int index);\n[[nodiscard]] QString langDayOfWeekFull(int index);\n\n[[nodiscard]] inline QString langDayOfWeek(const QDate &date) {\n\treturn langDayOfWeek(date.dayOfWeek());\n}\n\n[[nodiscard]] inline QString langDayOfWeekFull(const QDate &date) {\n\treturn langDayOfWeekFull(date.dayOfWeek());\n}\n\n[[nodiscard]] QString langDateTime(const QDateTime &date);\n[[nodiscard]] QString langDateTimeFull(const QDateTime &date);\n[[nodiscard]] bool langFirstNameGoesSecond();\n\nnamespace Lang {\n\n[[nodiscard]] QString Id();\n[[nodiscard]] rpl::producer<> Updated();\n[[nodiscard]] QString GetNonDefaultValue(const QByteArray &key);\n[[nodiscard]] QString DefaultLanguageId();\n[[nodiscard]] QString LanguageIdOrDefault(const QString &id);\n\n[[nodiscard]] tr::phrase<> Month(int index);\n[[nodiscard]] tr::phrase<> MonthSmall(int index);\n[[nodiscard]] tr::phrase<> MonthDay(int index);\n[[nodiscard]] tr::phrase<> Weekday(int index);\n[[nodiscard]] tr::phrase<> WeekdayFull(int index);\n\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_numbers_animation.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"lang/lang_numbers_animation.h\"\n\n#include \"lang/lang_tag.h\"\n\nnamespace Lang {\n\nUi::StringWithNumbers ReplaceTag<Ui::StringWithNumbers>::Call(\n\t\tUi::StringWithNumbers &&original,\n\t\tushort tag,\n\t\tconst Ui::StringWithNumbers &replacement) {\n\toriginal.offset = FindTagReplacementPosition(original.text, tag);\n\tif (original.offset < 0) {\n\t\treturn std::move(original);\n\t}\n\toriginal.text = ReplaceTag<QString>::Call(\n\t\tstd::move(original.text),\n\t\ttag,\n\t\treplacement.text);\n\toriginal.length = replacement.text.size();\n\treturn std::move(original);\n}\n\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_numbers_animation.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/numbers_animation.h\"\n\nnamespace Lang {\n\ntemplate <typename ResultString>\nstruct StartReplacements;\n\ntemplate <>\nstruct StartReplacements<Ui::StringWithNumbers> {\n\tstatic inline Ui::StringWithNumbers Call(QString &&langString) {\n\t\treturn { std::move(langString) };\n\t}\n};\n\ntemplate <typename ResultString>\nstruct ReplaceTag;\n\ntemplate <>\nstruct ReplaceTag<Ui::StringWithNumbers> {\n\tstatic Ui::StringWithNumbers Call(\n\t\tUi::StringWithNumbers &&original,\n\t\tushort tag,\n\t\tconst Ui::StringWithNumbers &replacement);\n};\n\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_pch.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\n#include <QtCore/QString>\n#include <QtCore/QDateTime>\n\n#include <rpl/rpl.h>\n\n#include \"base/basic_types.h\"\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_tag.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"lang/lang_tag.h\"\n\n#include \"core/credits_amount.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/text/text.h\"\n#include \"base/qt/qt_common_adapters.h\"\n#include \"base/qt/qt_string_view.h\"\n\n#include <QtCore/QLocale>\n\nnamespace Lang {\nnamespace {\n\n//\n// http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html\n//\n\nconstexpr auto kShiftZero  = ushort(0);\nconstexpr auto kShiftOne   = ushort(1);\nconstexpr auto kShiftTwo   = ushort(2);\nconstexpr auto kShiftFew   = ushort(3);\nconstexpr auto kShiftMany  = ushort(4);\nconstexpr auto kShiftOther = ushort(5);\n\n//\n// n absolute value of the source number (integer and decimals).\n// i integer digits of n.\n// v number of visible fraction digits in n, with trailing zeros.\n// w number of visible fraction digits in n, without trailing zeros.\n// f visible fractional digits in n, with trailing zeros.\n// t visible fractional digits in n, without trailing zeros.\n//\n// Let n be int, being -1 for non-integer numbers and n == i for integer numbers.\n// It is fine while in the rules we compare n only to integers.\n//\n// -123.450: n = -1, i = 123, v = 3, w = 2, f = 450, t = 45\n//\n\nusing ChoosePluralMethod = ushort (*)(int n, int i, int v, int w, int f, int t);\n\nushort ChoosePlural1(int n, int i, int v, int w, int f, int t) {\n\treturn kShiftOther;\n}\n\nushort ChoosePlural2fil(int n, int i, int v, int w, int f, int t) {\n\tif (v == 0) {\n\t\tconst auto mod10 = (i % 10);\n\t\tif (i == 1 || i == 2 || i == 3) {\n\t\t\treturn kShiftOne;\n\t\t} else if (mod10 != 4 && mod10 != 6 && mod10 != 9) {\n\t\t\treturn kShiftOne;\n\t\t}\n\t\treturn kShiftOther;\n\t}\n\tconst auto mod10 = (f % 10);\n\tif (mod10 != 4 && mod10 != 6 && mod10 != 9) {\n\t\treturn kShiftOne;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural2tzm(int n, int i, int v, int w, int f, int t) {\n\tif (n == 0 || n == 1) {\n\t\treturn kShiftOne;\n\t} else if (n >= 11 && n <= 99) {\n\t\treturn kShiftOne;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural2is(int n, int i, int v, int w, int f, int t) {\n\tif (t == 0) {\n\t\tconst auto mod10 = (i % 10);\n\t\tconst auto mod100 = (i % 100);\n\t\tif (mod10 == 1 && mod100 != 11) {\n\t\t\treturn kShiftOne;\n\t\t}\n\t\treturn kShiftOther;\n\t}\n\treturn kShiftOne;\n}\n\nushort ChoosePlural2mk(int n, int i, int v, int w, int f, int t) {\n\tif (v == 0) {\n\t\tconst auto mod10 = (i % 10);\n\t\tconst auto mod100 = (i % 100);\n\t\tif (mod10 == 1 && mod100 != 11) {\n\t\t\treturn kShiftOne;\n\t\t}\n\t}\n\tconst auto mod10 = (f % 10);\n\tconst auto mod100 = (f % 100);\n\tif ((mod10 == 1) && (mod100 != 11)) {\n\t\treturn kShiftOne;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural2ak(int n, int i, int v, int w, int f, int t) {\n\tif (n == 0 || n == 1) {\n\t\treturn kShiftOne;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural2am(int n, int i, int v, int w, int f, int t) {\n\tif (i == 0 || n == 1) {\n\t\treturn kShiftOne;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural2hy(int n, int i, int v, int w, int f, int t) {\n\tif (i == 0 || i == 1) {\n\t\treturn kShiftOne;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural2si(int n, int i, int v, int w, int f, int t) {\n\tif (n == 0 || n == 1) {\n\t\treturn kShiftOne;\n\t} else if (i == 0 && f == 1) {\n\t\treturn kShiftOne;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural2bh(int n, int i, int v, int w, int f, int t) {\n\t// not documented\n\tif (n == 0 || n == 1) {\n\t\treturn kShiftOne;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural2af(int n, int i, int v, int w, int f, int t) {\n\tif (n == 1) {\n\t\treturn kShiftOne;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural2ast(int n, int i, int v, int w, int f, int t) {\n\tif (i == 1 && v == 0) {\n\t\treturn kShiftOne;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural2da(int n, int i, int v, int w, int f, int t) {\n\tif (n == 1) {\n\t\treturn kShiftOne;\n\t} else if (t != 0 && ((i == 0) || (i == 1))) {\n\t\treturn kShiftOne;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural3lv(int n, int i, int v, int w, int f, int t) {\n\tconst auto nmod10 = (n % 10);\n\tconst auto nmod100 = (n % 100);\n\tconst auto fmod10 = (f % 10);\n\tconst auto fmod100 = (f % 100);\n\tif (nmod10 == 0) {\n\t\treturn kShiftZero;\n\t} else if ((nmod100 >= 11) && (nmod100 <= 19)) {\n\t\treturn kShiftZero;\n\t} else if ((v == 2) && (fmod100 >= 11) && (fmod100 <= 19)) {\n\t\treturn kShiftZero;\n\t} else if ((nmod10 == 1) && (nmod100 != 11)) {\n\t\treturn kShiftOne;\n\t} else if ((v == 2) && (fmod10 == 1) && (fmod100 != 11)) {\n\t\treturn kShiftOne;\n\t} else if ((v != 2) && (fmod10 == 1)) {\n\t\treturn kShiftOne;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural3ksh(int n, int i, int v, int w, int f, int t) {\n\tif (n == 0) {\n\t\treturn kShiftZero;\n\t} else if (n == 1) {\n\t\treturn kShiftOne;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural3lag(int n, int i, int v, int w, int f, int t) {\n\tif (n == 0) {\n\t\treturn kShiftZero;\n\t} else if ((n != 0) && ((i == 0) || (i == 1))) {\n\t\treturn kShiftOne;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural3kw(int n, int i, int v, int w, int f, int t) {\n\tif (n == 1) {\n\t\treturn kShiftOne;\n\t} else if (n == 2) {\n\t\treturn kShiftTwo;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural3bs(int n, int i, int v, int w, int f, int t) {\n\tif (v == 0) {\n\t\tconst auto mod10 = (i % 10);\n\t\tconst auto mod100 = (i % 100);\n\t\tif ((mod10 >= 2) && (mod10 <= 4) && (mod100 < 12 || mod100 > 14)) {\n\t\t\treturn kShiftFew;\n\t\t} else if ((mod10 == 1) && (mod100 != 11)) {\n\t\t\treturn kShiftOne;\n\t\t}\n\t\treturn kShiftOther;\n\t}\n\tconst auto mod10 = (f % 10);\n\tconst auto mod100 = (f % 100);\n\tif ((mod10 >= 2) && (mod10 <= 4) && (mod100 < 12 || mod100 > 14)) {\n\t\treturn kShiftFew;\n\t} else if ((mod10 == 1) && (mod100 != 11)) {\n\t\treturn kShiftOne;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural3shi(int n, int i, int v, int w, int f, int t) {\n\tif (i == 0 || n == 1) {\n\t\treturn kShiftOne;\n\t} else if (n >= 2 && n <= 10) {\n\t\treturn kShiftFew;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural3mo(int n, int i, int v, int w, int f, int t) {\n\tif (v == 0) {\n\t\tconst auto mod100 = (n % 100);\n\t\tif (i == 1) {\n\t\t\treturn kShiftOne;\n\t\t} else if (n == 0) {\n\t\t\treturn kShiftFew;\n\t\t} else if ((n != 1) && (mod100 >= 1) && (mod100 <= 19)) {\n\t\t\treturn kShiftFew;\n\t\t}\n\t\treturn kShiftOther;\n\t}\n\treturn kShiftFew;\n}\n\nushort ChoosePlural4be(int n, int i, int v, int w, int f, int t) {\n\tconst auto mod10 = (n % 10);\n\tconst auto mod100 = (n % 100);\n\tif ((mod10 >= 2) && (mod10 <= 4) && (mod100 < 12 || mod100 > 14)) {\n\t\treturn kShiftFew;\n\t} else if ((mod10 == 1) && (mod100 != 11)) {\n\t\treturn kShiftOne;\n\t} else if (mod10 == 0) {\n\t\treturn kShiftMany;\n\t} else if ((mod10 >= 5) && (mod10 <= 9)) {\n\t\treturn kShiftMany;\n\t} else if ((mod100 >= 11) && (mod100 <= 14)) {\n\t\treturn kShiftMany;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural4ru(int n, int i, int v, int w, int f, int t) {\n\tif (v == 0) {\n\t\tconst auto mod10 = (i % 10);\n\t\tconst auto mod100 = (i % 100);\n\t\tif ((mod10 >= 2) && (mod10 <= 4) && (mod100 < 12 || mod100 > 14)) {\n\t\t\treturn kShiftFew;\n\t\t} else if ((mod10 == 1) && (mod100 != 11)) {\n\t\t\treturn kShiftOne;\n\t\t}\n\t\treturn kShiftMany;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural4pl(int n, int i, int v, int w, int f, int t) {\n\tif (v == 0) {\n\t\tif (i == 1) {\n\t\t\treturn kShiftOne;\n\t\t}\n\t\tconst auto mod10 = (i % 10);\n\t\tconst auto mod100 = (i % 100);\n\t\tif ((mod10 >= 2) && (mod10 <= 4) && (mod100 < 12 || mod100 > 14)) {\n\t\t\treturn kShiftFew;\n\t\t} else {\n\t\t\treturn kShiftMany;\n\t\t}\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural4lt(int n, int i, int v, int w, int f, int t) {\n\tconst auto mod10 = (n % 10);\n\tconst auto mod100 = (n % 100);\n\tif ((mod10 >= 2) && (mod10 <= 9) && (mod100 < 11 || mod100 > 19)) {\n\t\treturn kShiftFew;\n\t} else if ((mod10 == 1) && (mod100 != 11)) {\n\t\treturn kShiftOne;\n\t} else if (f != 0) {\n\t\treturn kShiftMany;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural4cs(int n, int i, int v, int w, int f, int t) {\n\tif (v == 0) {\n\t\tif (i == 1) {\n\t\t\treturn kShiftOne;\n\t\t} else if (i >= 2 && i <= 4) {\n\t\t\treturn kShiftFew;\n\t\t}\n\t\treturn kShiftOther;\n\t}\n\treturn kShiftMany;\n}\n\nushort ChoosePlural4gd(int n, int i, int v, int w, int f, int t) {\n\tif (n == 1 || n == 11) {\n\t\treturn kShiftOne;\n\t} else if (n == 2 || n == 12) {\n\t\treturn kShiftTwo;\n\t} else if (n >= 3 && n <= 10) {\n\t\treturn kShiftFew;\n\t} else if (n >= 13 && n <= 19) {\n\t\treturn kShiftFew;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural4dsb(int n, int i, int v, int w, int f, int t) {\n\tif (v == 0) {\n\t\tconst auto imod100 = (i % 100);\n\t\tif (imod100 == 1) {\n\t\t\treturn kShiftOne;\n\t\t} else if (imod100 == 2) {\n\t\t\treturn kShiftTwo;\n\t\t} else if (imod100 == 3 || imod100 == 4) {\n\t\t\treturn kShiftFew;\n\t\t}\n\t}\n\tconst auto fmod100 = (f % 100);\n\tif (fmod100 == 1) {\n\t\treturn kShiftOne;\n\t} else if (fmod100 == 2) {\n\t\treturn kShiftTwo;\n\t} else if (fmod100 == 3 || fmod100 == 4) {\n\t\treturn kShiftFew;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural4sl(int n, int i, int v, int w, int f, int t) {\n\tif (v == 0) {\n\t\tconst auto imod100 = (i % 100);\n\t\tif (imod100 == 3 || imod100 == 4) {\n\t\t\treturn kShiftFew;\n\t\t} else if (imod100 == 1) {\n\t\t\treturn kShiftOne;\n\t\t} else if (imod100 == 2) {\n\t\t\treturn kShiftTwo;\n\t\t}\n\t\treturn kShiftOther;\n\t}\n\treturn kShiftFew;\n}\n\nushort ChoosePlural4he(int n, int i, int v, int w, int f, int t) {\n\tif (v == 0) {\n\t\tif (i == 1) {\n\t\t\treturn kShiftOne;\n\t\t} else if (i == 2) {\n\t\t\treturn kShiftTwo;\n\t\t} else if ((n != 0) && (n != 10) && ((n % 10) == 0)) {\n\t\t\treturn kShiftMany;\n\t\t}\n\t\treturn kShiftOther;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural4mt(int n, int i, int v, int w, int f, int t) {\n\tconst auto mod100 = (n % 100);\n\tif (n == 1) {\n\t\treturn kShiftOne;\n\t} else if (n == 0) {\n\t\treturn kShiftFew;\n\t} else if (mod100 >= 2 && mod100 <= 10) {\n\t\treturn kShiftFew;\n\t} else if (mod100 >= 11 && mod100 <= 19) {\n\t\treturn kShiftMany;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural5gv(int n, int i, int v, int w, int f, int t) {\n\tif (v == 0) {\n\t\tconst auto mod10 = (i % 10);\n\t\tconst auto mod20 = (i % 20);\n\t\tif (mod10 == 1) {\n\t\t\treturn kShiftOne;\n\t\t} else if (mod10 == 2) {\n\t\t\treturn kShiftTwo;\n\t\t} else if (mod20 == 0) {\n\t\t\treturn kShiftFew;\n\t\t}\n\t\treturn kShiftOther;\n\t}\n\treturn kShiftMany;\n}\n\nushort ChoosePlural5br(int n, int i, int v, int w, int f, int t) {\n\tconst auto mod10 = (n % 10);\n\tconst auto mod100 = (n % 100);\n\tif ((mod10 == 1)\n\t\t&& (mod100 != 11)\n\t\t&& (mod100 != 71)\n\t\t&& (mod100 != 91)) {\n\t\treturn kShiftOne;\n\t} else if ((mod10 == 2)\n\t\t&& (mod100 != 12)\n\t\t&& (mod100 != 72)\n\t\t&& (mod100 != 92)) {\n\t\treturn kShiftTwo;\n\t} else if (((mod10 == 3) || (mod10 == 4) || (mod10 == 9))\n\t\t&& ((mod100 < 10) || (mod100 > 19))\n\t\t&& ((mod100 < 70) || (mod100 > 79))\n\t\t&& ((mod100 < 90) || (mod100 > 99))) {\n\t\treturn kShiftFew;\n\t} else if ((n != 0) && (n % 1000000 == 0)) {\n\t\treturn kShiftMany;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural5ga(int n, int i, int v, int w, int f, int t) {\n\tif (n == 1) {\n\t\treturn kShiftOne;\n\t} else if (n == 2) {\n\t\treturn kShiftTwo;\n\t} else if (n >= 3 && n <= 6) {\n\t\treturn kShiftFew;\n\t} else if (n >= 7 && n <= 10) {\n\t\treturn kShiftMany;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural6ar(int n, int i, int v, int w, int f, int t) {\n\tif (n == 0) {\n\t\treturn kShiftZero;\n\t} else if (n == 1) {\n\t\treturn kShiftOne;\n\t} else if (n == 2) {\n\t\treturn kShiftTwo;\n\t} else if (n < 0) {\n\t\treturn kShiftOther;\n\t}\n\tconst auto mod100 = (n % 100);\n\tif (mod100 >= 3 && mod100 <= 10) {\n\t\treturn kShiftFew;\n\t} else if (mod100 >= 11 && mod100 <= 99) {\n\t\treturn kShiftMany;\n\t}\n\treturn kShiftOther;\n}\n\nushort ChoosePlural6cy(int n, int i, int v, int w, int f, int t) {\n\tif (n == 0) {\n\t\treturn kShiftZero;\n\t} else if (n == 1) {\n\t\treturn kShiftOne;\n\t} else if (n == 2) {\n\t\treturn kShiftTwo;\n\t} else if (n == 3) {\n\t\treturn kShiftFew;\n\t} else if (n == 6) {\n\t\treturn kShiftMany;\n\t}\n\treturn kShiftOther;\n}\n\nstruct PluralsKey {\n\tPluralsKey(uint64 key);\n\tPluralsKey(const char *value);\n\n\tinline operator uint64() const {\n\t\treturn data;\n\t}\n\n\tuint64 data = 0;\n};\n\nchar ConvertKeyChar(char ch) {\n\treturn (ch == '_') ? '-' : QChar::toLower(ch);\n}\n\nPluralsKey::PluralsKey(uint64 key) : data(key) {\n}\n\nPluralsKey::PluralsKey(const char *value) {\n\tfor (auto ch = *value; ch; ch = *++value) {\n\t\tdata = (data << 8) | uint64(ConvertKeyChar(ch));\n\t}\n}\n\nstd::map<PluralsKey, ChoosePluralMethod> GeneratePluralRulesMap() {\n\treturn {\n\t\t//{ \"bm\", ChoosePlural1 },\n\t\t//{ \"my\", ChoosePlural1 },\n\t\t//{ \"yue\", ChoosePlural1 },\n\t\t//{ \"zh\", ChoosePlural1 },\n\t\t//{ \"dz\", ChoosePlural1 },\n\t\t//{ \"ig\", ChoosePlural1 },\n\t\t//{ \"id\", ChoosePlural1 },\n\t\t//{ \"in\", ChoosePlural1 }, // same as \"id\"\n\t\t//{ \"ja\", ChoosePlural1 },\n\t\t//{ \"jv\", ChoosePlural1 },\n\t\t//{ \"jw\", ChoosePlural1 }, // same as \"jv\"\n\t\t//{ \"kea\", ChoosePlural1 },\n\t\t//{ \"km\", ChoosePlural1 },\n\t\t//{ \"ko\", ChoosePlural1 },\n\t\t//{ \"ses\", ChoosePlural1 },\n\t\t//{ \"lkt\", ChoosePlural1 },\n\t\t//{ \"lo\", ChoosePlural1 },\n\t\t//{ \"jbo\", ChoosePlural1 },\n\t\t//{ \"kde\", ChoosePlural1 },\n\t\t//{ \"ms\", ChoosePlural1 },\n\t\t//{ \"nqo\", ChoosePlural1 },\n\t\t//{ \"sah\", ChoosePlural1 },\n\t\t//{ \"sg\", ChoosePlural1 },\n\t\t//{ \"ii\", ChoosePlural1 },\n\t\t//{ \"th\", ChoosePlural1 },\n\t\t//{ \"bo\", ChoosePlural1 },\n\t\t//{ \"to\", ChoosePlural1 },\n\t\t//{ \"vi\", ChoosePlural1 },\n\t\t//{ \"wo\", ChoosePlural1 },\n\t\t//{ \"yo\", ChoosePlural1 },\n\t\t//{ default, ChoosePlural1 },\n\t\t{ \"fil\", ChoosePlural2fil },\n\t\t{ \"tl\", ChoosePlural2fil },\n\t\t{ \"tzm\", ChoosePlural2tzm },\n\t\t{ \"is\", ChoosePlural2is },\n\t\t{ \"mk\", ChoosePlural2mk },\n\t\t{ \"ak\", ChoosePlural2ak },\n\t\t{ \"guw\", ChoosePlural2ak },\n\t\t{ \"ln\", ChoosePlural2ak },\n\t\t{ \"mg\", ChoosePlural2ak },\n\t\t{ \"nso\", ChoosePlural2ak },\n\t\t{ \"pa\", ChoosePlural2ak },\n\t\t{ \"ti\", ChoosePlural2ak },\n\t\t{ \"wa\", ChoosePlural2ak },\n\t\t{ \"am\", ChoosePlural2am },\n\t\t{ \"as\", ChoosePlural2am },\n\t\t{ \"bn\", ChoosePlural2am },\n\t\t{ \"gu\", ChoosePlural2am },\n\t\t{ \"hi\", ChoosePlural2am },\n\t\t{ \"kn\", ChoosePlural2am },\n\t\t{ \"mr\", ChoosePlural2am },\n\t\t{ \"fa\", ChoosePlural2am },\n\t\t{ \"zu\", ChoosePlural2am },\n\t\t{ \"hy\", ChoosePlural2hy },\n\t\t{ \"fr\", ChoosePlural2hy },\n\t\t{ \"ff\", ChoosePlural2hy },\n\t\t{ \"kab\", ChoosePlural2hy },\n\t\t{ \"pt\", ChoosePlural2hy },\n\t\t{ \"si\", ChoosePlural2si },\n\t\t{ \"bh\", ChoosePlural2bh },\n\t\t{ \"bho\", ChoosePlural2bh },\n\t\t{ \"af\", ChoosePlural2af },\n\t\t{ \"sq\", ChoosePlural2af },\n\t\t{ \"asa\", ChoosePlural2af },\n\t\t{ \"az\", ChoosePlural2af },\n\t\t{ \"eu\", ChoosePlural2af },\n\t\t{ \"bem\", ChoosePlural2af },\n\t\t{ \"bez\", ChoosePlural2af },\n\t\t{ \"brx\", ChoosePlural2af },\n\t\t{ \"bg\", ChoosePlural2af },\n\t\t{ \"ckb\", ChoosePlural2af },\n\t\t{ \"ce\", ChoosePlural2af },\n\t\t{ \"chr\", ChoosePlural2af },\n\t\t{ \"cgg\", ChoosePlural2af },\n\t\t{ \"dv\", ChoosePlural2af },\n\t\t{ \"eo\", ChoosePlural2af },\n\t\t{ \"ee\", ChoosePlural2af },\n\t\t{ \"fo\", ChoosePlural2af },\n\t\t{ \"fur\", ChoosePlural2af },\n\t\t{ \"ka\", ChoosePlural2af },\n\t\t{ \"el\", ChoosePlural2af },\n\t\t{ \"ha\", ChoosePlural2af },\n\t\t{ \"haw\", ChoosePlural2af },\n\t\t{ \"hu\", ChoosePlural2af },\n\t\t{ \"kaj\", ChoosePlural2af },\n\t\t{ \"kkj\", ChoosePlural2af },\n\t\t{ \"kl\", ChoosePlural2af },\n\t\t{ \"ks\", ChoosePlural2af },\n\t\t{ \"kk\", ChoosePlural2af },\n\t\t{ \"ku\", ChoosePlural2af },\n\t\t{ \"ky\", ChoosePlural2af },\n\t\t{ \"lb\", ChoosePlural2af },\n\t\t{ \"jmc\", ChoosePlural2af },\n\t\t{ \"ml\", ChoosePlural2af },\n\t\t{ \"mas\", ChoosePlural2af },\n\t\t{ \"mgo\", ChoosePlural2af },\n\t\t{ \"mn\", ChoosePlural2af },\n\t\t{ \"nah\", ChoosePlural2af },\n\t\t{ \"ne\", ChoosePlural2af },\n\t\t{ \"nnh\", ChoosePlural2af },\n\t\t{ \"jgo\", ChoosePlural2af },\n\t\t{ \"nd\", ChoosePlural2af },\n\t\t{ \"no\", ChoosePlural2af },\n\t\t{ \"nb\", ChoosePlural2af },\n\t\t{ \"nn\", ChoosePlural2af },\n\t\t{ \"ny\", ChoosePlural2af },\n\t\t{ \"nyn\", ChoosePlural2af },\n\t\t{ \"or\", ChoosePlural2af },\n\t\t{ \"om\", ChoosePlural2af },\n\t\t{ \"os\", ChoosePlural2af },\n\t\t{ \"pap\", ChoosePlural2af },\n\t\t{ \"ps\", ChoosePlural2af },\n\t\t{ \"rm\", ChoosePlural2af },\n\t\t{ \"rof\", ChoosePlural2af },\n\t\t{ \"rwk\", ChoosePlural2af },\n\t\t{ \"ssy\", ChoosePlural2af },\n\t\t{ \"saq\", ChoosePlural2af },\n\t\t{ \"seh\", ChoosePlural2af },\n\t\t{ \"ksb\", ChoosePlural2af },\n\t\t{ \"sn\", ChoosePlural2af },\n\t\t{ \"sd\", ChoosePlural2af },\n\t\t{ \"xog\", ChoosePlural2af },\n\t\t{ \"so\", ChoosePlural2af },\n\t\t{ \"nr\", ChoosePlural2af },\n\t\t{ \"sdh\", ChoosePlural2af },\n\t\t{ \"st\", ChoosePlural2af },\n\t\t{ \"es\", ChoosePlural2af },\n\t\t{ \"ss\", ChoosePlural2af },\n\t\t{ \"gsw\", ChoosePlural2af },\n\t\t{ \"syr\", ChoosePlural2af },\n\t\t{ \"ta\", ChoosePlural2af },\n\t\t{ \"te\", ChoosePlural2af },\n\t\t{ \"teo\", ChoosePlural2af },\n\t\t{ \"tig\", ChoosePlural2af },\n\t\t{ \"ts\", ChoosePlural2af },\n\t\t{ \"tn\", ChoosePlural2af },\n\t\t{ \"tr\", ChoosePlural2af },\n\t\t{ \"tk\", ChoosePlural2af },\n\t\t{ \"kcg\", ChoosePlural2af },\n\t\t{ \"ug\", ChoosePlural2af },\n\t\t{ \"uz\", ChoosePlural2af },\n\t\t{ \"ve\", ChoosePlural2af },\n\t\t{ \"vo\", ChoosePlural2af },\n\t\t{ \"vun\", ChoosePlural2af },\n\t\t{ \"wae\", ChoosePlural2af },\n\t\t{ \"xh\", ChoosePlural2af },\n\t\t{ \"\", ChoosePlural2ast },\n\t\t{ \"ast\", ChoosePlural2ast },\n\t\t{ \"ca\", ChoosePlural2ast },\n\t\t{ \"nl\", ChoosePlural2ast },\n\t\t{ \"en\", ChoosePlural2ast },\n\t\t{ \"et\", ChoosePlural2ast },\n\t\t{ \"pt_PT\", ChoosePlural2ast },\n\t\t{ \"fi\", ChoosePlural2ast },\n\t\t{ \"gl\", ChoosePlural2ast },\n\t\t{ \"lg\", ChoosePlural2ast },\n\t\t{ \"de\", ChoosePlural2ast },\n\t\t{ \"io\", ChoosePlural2ast },\n\t\t{ \"ia\", ChoosePlural2ast },\n\t\t{ \"it\", ChoosePlural2ast },\n\t\t{ \"sc\", ChoosePlural2ast },\n\t\t{ \"scn\", ChoosePlural2ast },\n\t\t{ \"sw\", ChoosePlural2ast },\n\t\t{ \"sv\", ChoosePlural2ast },\n\t\t{ \"ur\", ChoosePlural2ast },\n\t\t{ \"fy\", ChoosePlural2ast },\n\t\t{ \"ji\", ChoosePlural2ast },\n\t\t{ \"yi\", ChoosePlural2ast }, // same as \"ji\"\n\t\t{ \"da\", ChoosePlural2da },\n\t\t{ \"lv\", ChoosePlural3lv },\n\t\t{ \"prg\", ChoosePlural3lv },\n\t\t{ \"ksh\", ChoosePlural3ksh },\n\t\t{ \"lag\", ChoosePlural3lag },\n\t\t{ \"kw\", ChoosePlural3kw },\n\t\t{ \"smn\", ChoosePlural3kw },\n\t\t{ \"iu\", ChoosePlural3kw },\n\t\t{ \"smj\", ChoosePlural3kw },\n\t\t{ \"naq\", ChoosePlural3kw },\n\t\t{ \"se\", ChoosePlural3kw },\n\t\t{ \"smi\", ChoosePlural3kw },\n\t\t{ \"sms\", ChoosePlural3kw },\n\t\t{ \"sma\", ChoosePlural3kw },\n\t\t{ \"bs\", ChoosePlural3bs },\n\t\t{ \"hr\", ChoosePlural3bs },\n\t\t{ \"sr\", ChoosePlural3bs },\n\t\t{ \"sh\", ChoosePlural3bs },\n\t\t{ \"sr_Latn\", ChoosePlural3bs }, // same as \"sh\"\n\t\t{ \"shi\", ChoosePlural3shi },\n\t\t{ \"mo\", ChoosePlural3mo },\n\t\t{ \"ro_MD\", ChoosePlural3mo }, // same as \"mo\"\n\t\t{ \"ro\", ChoosePlural3mo },\n\t\t{ \"be\", ChoosePlural4be },\n\t\t{ \"ru\", ChoosePlural4ru },\n\t\t{ \"uk\", ChoosePlural4ru },\n\t\t{ \"pl\", ChoosePlural4pl },\n\t\t{ \"lt\", ChoosePlural4lt },\n\t\t{ \"cs\", ChoosePlural4cs },\n\t\t{ \"sk\", ChoosePlural4cs },\n\t\t{ \"gd\", ChoosePlural4gd },\n\t\t{ \"dsb\", ChoosePlural4dsb },\n\t\t{ \"hsb\", ChoosePlural4dsb },\n\t\t{ \"sl\", ChoosePlural4sl },\n\t\t{ \"he\", ChoosePlural4he },\n\t\t{ \"iw\", ChoosePlural4he }, // same as \"he\"\n\t\t{ \"mt\", ChoosePlural4mt },\n\t\t{ \"gv\", ChoosePlural5gv },\n\t\t{ \"br\", ChoosePlural5br },\n\t\t{ \"ga\", ChoosePlural5ga },\n\t\t{ \"ar\", ChoosePlural6ar },\n\t\t{ \"ars\", ChoosePlural6ar },\n\t\t{ \"cy\", ChoosePlural6cy },\n\t};\n\t//return {\n\t//\t//{ \"af\", ChoosePluralEn },\n\t//\t{ \"ak\", ChoosePluralPt },\n\t//\t{ \"am\", ChoosePluralPt },\n\t//\t{ \"ar\", ChoosePluralAr },\n\t//\t{ \"as\", ChoosePluralPt },\n\t//\t{ \"az\", ChoosePluralEs },\n\t//\t{ \"be\", ChoosePluralRu }, // should be different\n\t//\t{ \"bg\", ChoosePluralEs },\n\t//\t{ \"bh\", ChoosePluralPt },\n\t//\t{ \"bm\", ChoosePluralKo },\n\t//\t{ \"bn\", ChoosePluralPt },\n\t//\t{ \"bo\", ChoosePluralKo },\n\t//\t{ \"bs\", ChoosePluralSr },\n\t//\t//{ \"ca\", ChoosePluralEn },\n\t//\t//{ \"ce\", ChoosePluralEn },\n\t//\t{ \"cs\", ChoosePluralSk },\n\t//\t{ \"da\", ChoosePluralDa },\n\t//\t//{ \"de\", ChoosePluralEn },\n\t//\t//{ \"dv\", ChoosePluralEn },\n\t//\t{ \"dz\", ChoosePluralKo },\n\t//\t//{ \"ee\", ChoosePluralEn },\n\t//\t{ \"el\", ChoosePluralEs },\n\t//\t//{ \"en\", ChoosePluralEn },\n\t//\t{ \"es\", ChoosePluralEs },\n\t//\t{ \"eo\", ChoosePluralEs },\n\t//\t//{ \"et\", ChoosePluralEn },\n\t//\t{ \"eu\", ChoosePluralEs },\n\t//\t{ \"fa\", ChoosePluralPt },\n\t//\t{ \"ff\", ChoosePluralPt },\n\t//\t//{ \"fi\", ChoosePluralEn },\n\t//\t//{ \"fo\", ChoosePluralEn },\n\t//\t{ \"fr\", ChoosePluralPt },\n\t//\t//{ \"fy\", ChoosePluralEn },\n\t//\t//{ \"gl\", ChoosePluralEn },\n\t//\t{ \"gu\", ChoosePluralPt },\n\t//\t//{ \"ha\", ChoosePluralEn },\n\t//\t{ \"he\", ChoosePluralHe },\n\t//\t{ \"hi\", ChoosePluralPt },\n\t//\t{ \"hr\", ChoosePluralSr },\n\t//\t{ \"hu\", ChoosePluralEs },\n\t//\t{ \"hy\", ChoosePluralPt },\n\t//\t//{ \"ia\", ChoosePluralEn },\n\t//\t{ \"ig\", ChoosePluralKo },\n\t//\t{ \"ii\", ChoosePluralKo },\n\t//\t{ \"in\", ChoosePluralKo },\n\t//\t//{ \"io\", ChoosePluralEn },\n\t//\t{ \"is\", ChoosePluralIs },\n\t//\t//{ \"it\", ChoosePluralEn },\n\t//\t{ \"ja\", ChoosePluralKo },\n\t//\t{ \"jw\", ChoosePluralKo },\n\t//\t//{ \"ka\", ChoosePluralEn },\n\t//\t//{ \"kk\", ChoosePluralEn },\n\t//\t//{ \"kl\", ChoosePluralEn },\n\t//\t{ \"km\", ChoosePluralKo },\n\t//\t{ \"kn\", ChoosePluralPt },\n\t//\t{ \"ko\", ChoosePluralKo },\n\t//\t//{ \"ks\", ChoosePluralEn },\n\t//\t//{ \"ku\", ChoosePluralEn },\n\t//\t//{ \"ky\", ChoosePluralEn },\n\t//\t//{ \"lb\", ChoosePluralEn },\n\t//\t//{ \"lg\", ChoosePluralEn },\n\t//\t{ \"ln\", ChoosePluralPt },\n\t//\t{ \"lo\", ChoosePluralKo },\n\t//\t{ \"mg\", ChoosePluralPt },\n\t//\t{ \"mk\", ChoosePluralIs },\n\t//\t//{ \"ml\", ChoosePluralEn },\n\t//\t//{ \"mn\", ChoosePluralEn },\n\t//\t{ \"mo\", ChoosePluralRo },\n\t//\t{ \"mr\", ChoosePluralPt },\n\t//\t{ \"ms\", ChoosePluralKo },\n\t//\t{ \"mt\", ChoosePluralMt },\n\t//\t{ \"my\", ChoosePluralKo },\n\t//\t{ \"nb\", ChoosePluralEs },\n\t//\t//{ \"nd\", ChoosePluralEn },\n\t//\t//{ \"ne\", ChoosePluralEn },\n\t//\t//{ \"nl\", ChoosePluralEn },\n\t//\t//{ \"nn\", ChoosePluralEn },\n\t//\t//{ \"no\", ChoosePluralEn },\n\t//\t//{ \"nr\", ChoosePluralEn },\n\t//\t//{ \"ny\", ChoosePluralEn },\n\t//\t//{ \"om\", ChoosePluralEn },\n\t//\t//{ \"or\", ChoosePluralEn },\n\t//\t//{ \"os\", ChoosePluralEn },\n\t//\t{ \"pa\", ChoosePluralPt },\n\t//\t//{ \"ps\", ChoosePluralEn },\n\t//\t{ \"pt\", ChoosePluralPt },\n\t//\t//{ \"pt_PT\", ChoosePluralEn }, // not supported\n\t//\t//{ \"rm\", ChoosePluralEn },\n\t//\t{ \"ro\", ChoosePluralRo },\n\t//\t{ \"ru\", ChoosePluralRu },\n\t//\t//{ \"sc\", ChoosePluralEn },\n\t//\t//{ \"sd\", ChoosePluralEn },\n\t//\t{ \"sg\", ChoosePluralKo },\n\t//\t{ \"sk\", ChoosePluralSk },\n\t//\t//{ \"sn\", ChoosePluralEn },\n\t//\t//{ \"so\", ChoosePluralEn },\n\t//\t{ \"sh\", ChoosePluralSr },\n\t//\t{ \"si\", ChoosePluralPt },\n\t//\t{ \"sr\", ChoosePluralSr },\n\t//\t//{ \"ss\", ChoosePluralEn },\n\t//\t//{ \"st\", ChoosePluralEn },\n\t//\t//{ \"sv\", ChoosePluralEn },\n\t//\t//{ \"sw\", ChoosePluralEn },\n\t//\t{ \"ta\", ChoosePluralEs },\n\t//\t//{ \"te\", ChoosePluralEn },\n\t//\t{ \"th\", ChoosePluralKo },\n\t//\t{ \"ti\", ChoosePluralPt },\n\t//\t{ \"tk\", ChoosePluralEs },\n\t//\t{ \"tl\", ChoosePluralTl },\n\t//\t//{ \"tn\", ChoosePluralEn },\n\t//\t{ \"to\", ChoosePluralKo },\n\t//\t{ \"tr\", ChoosePluralEs },\n\t//\t//{ \"ts\", ChoosePluralEn },\n\t//\t//{ \"ug\", ChoosePluralEn },\n\t//\t{ \"uk\", ChoosePluralRu },\n\t//\t//{ \"ur\", ChoosePluralEn },\n\t//\t{ \"uz\", ChoosePluralEs },\n\t//\t{ \"pl\", ChoosePluralPl },\n\t//\t{ \"sq\", ChoosePluralEs },\n\t//\t//{ \"ve\", ChoosePluralEn },\n\t//\t{ \"vi\", ChoosePluralKo },\n\t//\t//{ \"vo\", ChoosePluralEn },\n\t//\t{ \"wa\", ChoosePluralPt },\n\t//\t{ \"wo\", ChoosePluralKo },\n\t//\t//{ \"xh\", ChoosePluralEn },\n\t//\t//{ \"yi\", ChoosePluralEn },\n\t//\t{ \"yo\", ChoosePluralKo },\n\t//\t{ \"zh\", ChoosePluralKo },\n\t//\t{ \"zu\", ChoosePluralPt },\n\t//};\n}\n\nconst auto ChoosePluralDefault = &ChoosePlural2ast;\nauto ChoosePlural = ChoosePluralDefault;\n\n} // namespace\n\nint FindTagReplacementPosition(const QString &original, ushort tag) {\n\tfor (auto s = original.constData(), ch = s, e = ch + original.size(); ch != e;) {\n\t\tif (ch->unicode() == kTextCommand) {\n\t\t\tif (ch + kTagReplacementSize <= e\n\t\t\t\t&& (ch + 1)->unicode() == kTextCommandLangTag\n\t\t\t\t&& (ch + 3)->unicode() == kTextCommand) {\n\t\t\t\tif ((ch + 2)->unicode() == 0x0020 + tag) {\n\t\t\t\t\treturn ch - s;\n\t\t\t\t} else {\n\t\t\t\t\tch += kTagReplacementSize;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t++ch;\n\t\t\t}\n\t\t} else {\n\t\t\t++ch;\n\t\t}\n\t}\n\treturn -1;\n\n}\n\nQString FormatDouble(float64 value) {\n\tauto result = QString::number(value, 'f', 6);\n\twhile (result.endsWith('0')) {\n\t\tresult.chop(1);\n\t}\n\tif (result.endsWith('.')) {\n\t\tresult.chop(1);\n\t}\n\treturn result;\n}\n\nint NonZeroPartToInt(QString value) {\n\tauto zeros = 0;\n\tfor (const auto &ch : value) {\n\t\tif (ch == '0') {\n\t\t\t++zeros;\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn (zeros > 0)\n\t\t? (zeros < value.size() ? base::StringViewMid(value, zeros).toInt() : 0)\n\t\t: (value.isEmpty() ? 0 : value.toInt());\n}\n\nShortenedCount FormatCountToShort(int64 number, bool onlyK) {\n\tauto result = ShortenedCount{ number };\n\tconst auto abs = std::abs(number);\n\tconst auto shorten = [&](int64 divider, char multiplier) {\n\t\tconst auto sign = (number > 0) ? 1 : -1;\n\t\tconst auto rounded = abs / (divider / 10);\n\t\tresult.string = QString::number(sign * rounded / 10);\n\t\tif (rounded % 10) {\n\t\t\tresult.string += '.' + QString::number(rounded % 10) + multiplier;\n\t\t} else {\n\t\t\tresult.string += multiplier;\n\t\t}\n\t\t// Update given number.\n\t\t// E.g. 12345 will be 12000.\n\t\tresult.number = rounded * divider;\n\t\tresult.shortened = true;\n\t};\n\tif (!onlyK && abs >= 1'000'000) {\n\t\tshorten(1'000'000, 'M');\n\t} else if (abs >= 10'000) {\n\t\tshorten(1'000, 'K');\n\t} else {\n\t\tresult.string = QString::number(number);\n\t}\n\treturn result;\n}\n\nQString FormatCountDecimal(int64 number) {\n\treturn QLocale().toString(number);\n}\n\nQString FormatExactCountDecimal(float64 number) {\n\tconst auto locale = QLocale();\n\tif (qFuzzyCompare(number, base::SafeRound(number))) {\n\t\treturn locale.toString(int64(base::SafeRound(number)));\n\t}\n\n\t// Somehow using QLocale::FloatingPointShortest sometimes produces\n\t// \"0.8500000000000001\" on some systems / locales,\n\t// so I want to stick to 6 digits max (default third argument value).\n\tauto result = locale.toString(number, 'f');\n\tconst auto zero = locale.zeroDigit();\n\twhile (result.endsWith(zero)) {\n\t\tresult.chop(1);\n\t}\n\treturn result;\n}\n\nShortenedCount FormatCreditsAmountToShort(CreditsAmount amount) {\n\tconst auto attempt = FormatCountToShort(amount.whole());\n\treturn attempt.shortened ? attempt : ShortenedCount{\n\t\t.string = FormatCreditsAmountDecimal(amount),\n\t};\n}\n\nQString FormatCreditsAmountDecimal(CreditsAmount amount) {\n\treturn FormatExactCountDecimal(amount.value());\n}\n\nQString FormatCreditsAmountRounded(CreditsAmount amount) {\n\tconst auto value = amount.value();\n\treturn FormatExactCountDecimal(base::SafeRound(value * 100.) / 100.);\n}\n\nPluralResult Plural(\n\t\tushort keyBase,\n\t\tfloat64 value,\n\t\tlngtag_count type) {\n\t// To correctly select a shift for PluralType::Short\n\t// we must first round the number.\n\tconst auto shortened = (type == lt_count_short)\n\t\t? FormatCountToShort(qRound(value))\n\t\t: ShortenedCount();\n\n\t// Simplified.\n\tconst auto n = std::abs(shortened.number ? float64(shortened.number) : value);\n\tconst auto i = int(std::floor(n));\n\tconst auto integer = (int(std::ceil(n)) == i);\n\tconst auto formatted = integer ? QString() : FormatDouble(n);\n\tconst auto dot = formatted.indexOf('.');\n\tconst auto fraction = (dot >= 0) ? formatted.mid(dot + 1) : QString();\n\tconst auto v = fraction.size();\n\tconst auto w = v;\n\tconst auto f = NonZeroPartToInt(fraction);\n\tconst auto t = f;\n\n\tconst auto useNonDefaultPlural = (ChoosePlural != ChoosePluralDefault)\n\t\t&& (keyBase == kPluralKeyBaseForCloudValue\n\t\t\t|| Lang::details::IsNonDefaultPlural(keyBase));\n\tconst auto shift = (useNonDefaultPlural\n\t\t? ChoosePlural\n\t\t: ChoosePluralDefault)((integer ? i : -1), i, v, w, f, t);\n\tif (integer) {\n\t\tconst auto round = qRound(value);\n\t\tif (type == lt_count_short) {\n\t\t\treturn { shift, shortened.string };\n\t\t} else if (type == lt_count_decimal) {\n\t\t\treturn { shift, FormatCountDecimal(round) };\n\t\t}\n\t\treturn { shift, QString::number(round) };\n\t}\n\treturn { shift, FormatDouble(value) };\n}\n\nvoid UpdatePluralRules(const QString &languageId) {\n\tstatic auto kMap = GeneratePluralRulesMap();\n\tauto parent = uint64(0);\n\tauto key = uint64(0);\n\tfor (const auto &ch : languageId) {\n\t\tconst auto converted = ConvertKeyChar(ch.unicode());\n\t\tif (converted == '-' && !parent) {\n\t\t\tparent = key;\n\t\t}\n\t\tkey = (key << 8) | uint64(converted);\n\t}\n\tconst auto i = [&] {\n\t\tconst auto result = kMap.find(key);\n\t\treturn (result != end(kMap) || !parent)\n\t\t\t? result\n\t\t\t: kMap.find(parent);\n\t}();\n\tChoosePlural = (i == end(kMap)) ? ChoosePlural1 : i->second;\n}\n\nQString ReplaceTag<QString>::Replace(QString &&original, const QString &replacement, int replacementPosition) {\n\tauto result = QString();\n\tresult.reserve(original.size() + replacement.size() - kTagReplacementSize);\n\tif (replacementPosition > 0) {\n\t\tresult.append(base::StringViewMid(original, 0, replacementPosition));\n\t}\n\tresult.append(replacement);\n\tif (replacementPosition + kTagReplacementSize < original.size()) {\n\t\tresult.append(base::StringViewMid(original, replacementPosition + kTagReplacementSize));\n\t}\n\treturn result;\n}\n\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_tag.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass CreditsAmount;\n\nenum lngtag_count : int;\n\nnamespace Lang {\n\ninline constexpr auto kTextCommand = 0x10;\ninline constexpr auto kTextCommandLangTag = 0x20;\nconstexpr auto kTagReplacementSize = 4;\n\n[[nodiscard]] int FindTagReplacementPosition(\n\tconst QString &original,\n\tushort tag);\n\nstruct ShortenedCount {\n\tint64 number = 0;\n\tQString string;\n\tbool shortened = false;\n};\n[[nodiscard]] ShortenedCount FormatCountToShort(\n\tint64 number,\n\tbool onlyK = false);\n[[nodiscard]] QString FormatCountDecimal(int64 number);\n[[nodiscard]] QString FormatExactCountDecimal(float64 number);\n[[nodiscard]] ShortenedCount FormatCreditsAmountToShort(\n\tCreditsAmount amount);\n[[nodiscard]] QString FormatCreditsAmountDecimal(CreditsAmount amount);\n[[nodiscard]] QString FormatCreditsAmountRounded(CreditsAmount amount);\n\nstruct PluralResult {\n\tint keyShift = 0;\n\tQString replacement;\n};\ninline constexpr auto kPluralKeyBaseForCloudValue = ushort(-1);\nPluralResult Plural(\n\tushort keyBase,\n\tfloat64 value,\n\tlngtag_count type);\nvoid UpdatePluralRules(const QString &languageId);\n\ntemplate <typename ResultString>\nstruct StartReplacements;\n\ntemplate <>\nstruct StartReplacements<QString> {\n\tstatic inline QString Call(QString &&langString) {\n\t\treturn std::move(langString);\n\t}\n};\n\ntemplate <typename ResultString>\nstruct ReplaceTag;\n\ntemplate <>\nstruct ReplaceTag<QString> {\n\tstatic inline QString Call(QString &&original, ushort tag, const QString &replacement) {\n\t\tauto replacementPosition = FindTagReplacementPosition(original, tag);\n\t\tif (replacementPosition < 0) {\n\t\t\treturn std::move(original);\n\t\t}\n\t\treturn Replace(std::move(original), replacement, replacementPosition);\n\t}\n\tstatic QString Replace(QString &&original, const QString &replacement, int start);\n\n};\n\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_text_entity.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"lang/lang_text_entity.h\"\n\n#include \"lang/lang_tag.h\"\n\nnamespace Lang {\n\nTextWithEntities ReplaceTag<TextWithEntities>::Call(TextWithEntities &&original, ushort tag, const TextWithEntities &replacement) {\n\tauto replacementPosition = FindTagReplacementPosition(original.text, tag);\n\tif (replacementPosition < 0) {\n\t\treturn std::move(original);\n\t}\n\treturn Replace(std::move(original), replacement, replacementPosition);\n}\n\nTextWithEntities ReplaceTag<TextWithEntities>::Replace(TextWithEntities &&original, const TextWithEntities &replacement, int start) {\n\tauto result = TextWithEntities();\n\tresult.text = ReplaceTag<QString>::Replace(std::move(original.text), replacement.text, start);\n\tauto originalEntitiesCount = original.entities.size();\n\tauto replacementEntitiesCount = replacement.entities.size();\n\tif (originalEntitiesCount != 0 || replacementEntitiesCount != 0) {\n\t\tresult.entities.reserve(originalEntitiesCount + replacementEntitiesCount);\n\n\t\tauto replacementEnd = start + int(replacement.text.size());\n\t\tauto replacementEntity = replacement.entities.cbegin();\n\t\tauto addReplacementEntitiesUntil = [&](int untilPosition) {\n\t\t\twhile (replacementEntity != replacement.entities.cend()) {\n\t\t\t\tauto newOffset = start + replacementEntity->offset();\n\t\t\t\tif (newOffset >= untilPosition) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tauto newEnd = newOffset + replacementEntity->length();\n\t\t\t\tnewOffset = std::clamp(newOffset, start, replacementEnd);\n\t\t\t\tnewEnd = std::clamp(newEnd, start, replacementEnd);\n\t\t\t\tif (auto newLength = newEnd - newOffset) {\n\t\t\t\t\tresult.entities.push_back({ replacementEntity->type(), newOffset, newLength, replacementEntity->data() });\n\t\t\t\t}\n\t\t\t\t++replacementEntity;\n\t\t\t}\n\t\t};\n\n\t\tfor (const auto &entity : std::as_const(original.entities)) {\n\t\t\t// Transform the entity by the replacement.\n\t\t\tauto offset = entity.offset();\n\t\t\tauto end = offset + entity.length();\n\t\t\tif (offset > start) {\n\t\t\t\toffset = offset + replacement.text.size() - kTagReplacementSize;\n\t\t\t}\n\t\t\tif (end > start) {\n\t\t\t\tend = end + replacement.text.size() - kTagReplacementSize;\n\t\t\t}\n\t\t\toffset = std::clamp(offset, 0, int(result.text.size()));\n\t\t\tend = std::clamp(end, 0, int(result.text.size()));\n\n\t\t\t// Add all replacement entities that start before the current original entity.\n\t\t\taddReplacementEntitiesUntil(offset);\n\n\t\t\t// Add a modified original entity.\n\t\t\tif (auto length = end - offset) {\n\t\t\t\tresult.entities.push_back({ entity.type(), offset, length, entity.data() });\n\t\t\t}\n\t\t}\n\t\t// Add the remaining replacement entities.\n\t\taddReplacementEntitiesUntil(result.text.size());\n\t}\n\treturn result;\n}\n\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_text_entity.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/text/text_entity.h\"\n#include \"ui/text/text_utilities.h\"\n\nnamespace Lang {\n\ntemplate <typename ResultString>\nstruct StartReplacements;\n\ntemplate <>\nstruct StartReplacements<TextWithEntities> {\n\tstatic inline TextWithEntities Call(QString &&langString) {\n\t\treturn { std::move(langString), EntitiesInText() };\n\t}\n};\n\ntemplate <typename ResultString>\nstruct ReplaceTag;\n\ntemplate <>\nstruct ReplaceTag<TextWithEntities> {\n\tstatic TextWithEntities Call(TextWithEntities &&original, ushort tag, const TextWithEntities &replacement);\n\tstatic TextWithEntities Replace(TextWithEntities &&original, const TextWithEntities &replacement, int start);\n\n};\n\n} // namespace Lang\n\nnamespace tr {\nnamespace details {\n\nstruct UpperProjection {\n\t[[nodiscard]] QString operator()(const QString &value) const {\n\t\treturn value.toUpper();\n\t}\n\t[[nodiscard]] QString operator()(QString &&value) const {\n\t\treturn std::move(value).toUpper();\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tconst TextWithEntities &value) const {\n\t\treturn { value.text.toUpper(), value.entities };\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tTextWithEntities &&value) const {\n\t\treturn {\n\t\t\tstd::move(value.text).toUpper(),\n\t\t\tstd::move(value.entities) };\n\t}\n};\n\nstruct MarkedProjection {\n\t[[nodiscard]] TextWithEntities operator()() const {\n\t\treturn {};\n\t}\n\t[[nodiscard]] TextWithEntities operator()(const QString &value) const {\n\t\treturn TextWithEntities{ value };\n\t}\n\t[[nodiscard]] TextWithEntities operator()(QString &&value) const {\n\t\treturn TextWithEntities{ std::move(value) };\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tconst TextWithEntities &value) const {\n\t\treturn value;\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tTextWithEntities &&value) const {\n\t\treturn std::move(value);\n\t}\n};\n\nstruct RichProjection {\n\t[[nodiscard]] TextWithEntities operator()(const QString &value) const {\n\t\treturn Ui::Text::RichLangValue(value);\n\t}\n};\n\nstruct BoldProjection {\n\t[[nodiscard]] TextWithEntities operator()(const QString &value) const {\n\t\treturn Ui::Text::Bold(value);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tconst TextWithEntities &value) const {\n\t\treturn Ui::Text::Wrapped(value, EntityType::Bold);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tTextWithEntities &&value) const {\n\t\treturn Ui::Text::Wrapped(std::move(value), EntityType::Bold);\n\t}\n};\n\nstruct SemiboldProjection {\n\t[[nodiscard]] TextWithEntities operator()(const QString &value) const {\n\t\treturn Ui::Text::Semibold(value);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tconst TextWithEntities &value) const {\n\t\treturn Ui::Text::Wrapped(value, EntityType::Semibold);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tTextWithEntities &&value) const {\n\t\treturn Ui::Text::Wrapped(std::move(value), EntityType::Semibold);\n\t}\n};\n\nstruct ItalicProjection {\n\t[[nodiscard]] TextWithEntities operator()(const QString &value) const {\n\t\treturn Ui::Text::Italic(value);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tconst TextWithEntities &value) const {\n\t\treturn Ui::Text::Wrapped(value, EntityType::Italic);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tTextWithEntities &&value) const {\n\t\treturn Ui::Text::Wrapped(std::move(value), EntityType::Italic);\n\t}\n};\n\nstruct UnderlineProjection {\n\t[[nodiscard]] TextWithEntities operator()(const QString &value) const {\n\t\treturn Ui::Text::Underline(value);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tconst TextWithEntities &value) const {\n\t\treturn Ui::Text::Wrapped(value, EntityType::Underline);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tTextWithEntities &&value) const {\n\t\treturn Ui::Text::Wrapped(std::move(value), EntityType::Underline);\n\t}\n};\n\nstruct StrikeOutProjection {\n\t[[nodiscard]] TextWithEntities operator()(const QString &value) const {\n\t\treturn Ui::Text::StrikeOut(value);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tconst TextWithEntities &value) const {\n\t\treturn Ui::Text::Wrapped(value, EntityType::StrikeOut);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tTextWithEntities &&value) const {\n\t\treturn Ui::Text::Wrapped(std::move(value), EntityType::StrikeOut);\n\t}\n};\n\nstruct LinkProjection {\n\t[[nodiscard]] TextWithEntities operator()(const QString &value) const {\n\t\treturn Ui::Text::Link(value);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tconst QString &value,\n\t\t\tconst QString &url) const {\n\t\treturn Ui::Text::Link(value, url);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tconst QString &value,\n\t\t\tint index) const {\n\t\treturn Ui::Text::Link(value, index);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tconst TextWithEntities &value) const {\n\t\treturn Ui::Text::Link(value);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tconst TextWithEntities &value,\n\t\t\tconst QString &url) const {\n\t\treturn Ui::Text::Link(value, url);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tconst TextWithEntities &value,\n\t\t\tint index) const {\n\t\treturn Ui::Text::Link(value, index);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tTextWithEntities &&value) const {\n\t\treturn Ui::Text::Link(std::move(value));\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tTextWithEntities &&value,\n\t\t\tconst QString &url) const {\n\t\treturn Ui::Text::Link(std::move(value), url);\n\t}\n\t[[nodiscard]] TextWithEntities operator()(\n\t\t\tTextWithEntities &&value,\n\t\t\tint index) const {\n\t\treturn Ui::Text::Link(std::move(value), index);\n\t}\n};\n\nstruct UrlProjection {\n\t[[nodiscard]] auto operator()(const QString &url) const {\n\t\treturn [url](auto &&text) {\n\t\t\treturn Ui::Text::Link(std::forward<decltype(text)>(text), url);\n\t\t};\n\t}\n\t[[nodiscard]] auto operator()(int index) const {\n\t\treturn [index](auto &&text) {\n\t\t\treturn Ui::Text::Link(std::forward<decltype(text)>(text), index);\n\t\t};\n\t}\n};\n\n} // namespace details\n\ninline constexpr details::UpperProjection upper{};\ninline constexpr details::MarkedProjection marked{};\ninline constexpr details::RichProjection rich{};\ninline constexpr details::BoldProjection bold{};\ninline constexpr details::SemiboldProjection semibold{};\ninline constexpr details::ItalicProjection italic{};\ninline constexpr details::UnderlineProjection underline{};\ninline constexpr details::StrikeOutProjection strikeout{};\ninline constexpr details::LinkProjection link{};\ninline constexpr details::UrlProjection url{};\n\n} // namespace tr\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_translator.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"lang/lang_translator.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"base/platform/base_platform_info.h\"\n\nnamespace Lang {\n\nQString Translator::translate(const char *context, const char *sourceText, const char *disambiguation, int n) const {\n\tif (u\"QMenuBar\"_q == context) {\n\t\tif (u\"Services\"_q == sourceText) return tr::lng_mac_menu_services(tr::now);\n\t\tif (u\"Hide %1\"_q == sourceText) return tr::lng_mac_menu_hide_telegram(tr::now, lt_telegram, u\"%1\"_q);\n\t\tif (u\"Hide Others\"_q == sourceText) return tr::lng_mac_menu_hide_others(tr::now);\n\t\tif (u\"Show All\"_q == sourceText) return tr::lng_mac_menu_show_all(tr::now);\n\t\tif (u\"Preferences...\"_q == sourceText) return tr::lng_mac_menu_preferences(tr::now);\n\t\tif (u\"Quit %1\"_q == sourceText) return tr::lng_mac_menu_quit_telegram(tr::now, lt_telegram, u\"%1\"_q);\n\t\tif (u\"About %1\"_q == sourceText) return tr::lng_mac_menu_about_telegram(tr::now, lt_telegram, u\"%1\"_q);\n\t\treturn QString();\n\t}\n\tif (u\"QWidgetTextControl\"_q == context || u\"QLineEdit\"_q == context) {\n\t\tif (u\"&Undo\"_q == sourceText) return Platform::IsWindows() ? tr::lng_wnd_menu_undo(tr::now) : Platform::IsMac() ? tr::lng_mac_menu_undo(tr::now) : tr::lng_linux_menu_undo(tr::now);\n\t\tif (u\"&Redo\"_q == sourceText) return Platform::IsWindows() ? tr::lng_wnd_menu_redo(tr::now) : Platform::IsMac() ? tr::lng_mac_menu_redo(tr::now) : tr::lng_linux_menu_redo(tr::now);\n\t\tif (u\"Cu&t\"_q == sourceText) return tr::lng_mac_menu_cut(tr::now);\n\t\tif (u\"&Copy\"_q == sourceText) return tr::lng_mac_menu_copy(tr::now);\n\t\tif (u\"&Paste\"_q == sourceText) return tr::lng_mac_menu_paste(tr::now);\n\t\tif (u\"Delete\"_q == sourceText) return tr::lng_mac_menu_delete(tr::now);\n\t\tif (u\"Select All\"_q == sourceText) return tr::lng_mac_menu_select_all(tr::now);\n\t\treturn QString();\n\t}\n\tif (u\"QUnicodeControlCharacterMenu\"_q == context) {\n\t\tif (u\"Insert Unicode control character\"_q == sourceText) return tr::lng_menu_insert_unicode(tr::now);\n\t\treturn QString();\n\t}\n\treturn QString();\n}\n\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_translator.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QTranslator>\n\nnamespace Lang {\n\nclass Translator : public QTranslator {\npublic:\n\tQString translate(const char *context, const char *sourceText, const char *disambiguation = 0, int n = -1) const override;\n\n};\n\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/lang_values.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"lang/lang_tag.h\"\n\nenum lngtag_count : int;\n\nnamespace Lang {\nnamespace details {\n\ninline constexpr auto kPluralCount = 6;\n\ntemplate <typename Tag>\ninline constexpr ushort TagValue();\n\ntemplate <typename P>\nusing S = std::decay_t<decltype(std::declval<P>()(QString()))>;\n\n[[nodiscard]] QString Current(ushort key);\n[[nodiscard]] rpl::producer<QString> Value(ushort key);\n[[nodiscard]] bool IsNonDefaultPlural(ushort keyBase);\n\ntemplate <int Index, typename Type, typename Tuple>\n[[nodiscard]] Type ReplaceUnwrapTuple(Type accumulated, const Tuple &tuple) {\n\treturn accumulated;\n}\n\ntemplate <\n\tint Index,\n\ttypename Type,\n\ttypename Tuple,\n\ttypename Tag,\n\ttypename ...Tags>\n[[nodiscard]] Type ReplaceUnwrapTuple(\n\t\tType accumulated,\n\t\tconst Tuple &tuple,\n\t\tTag tag,\n\t\tTags ...tags) {\n\treturn ReplaceUnwrapTuple<Index + 1>(\n\t\tReplaceTag<Type>::Call(\n\t\t\tstd::move(accumulated),\n\t\t\ttag,\n\t\t\tstd::get<Index>(tuple)),\n\t\ttuple,\n\t\ttags...);\n}\n\ntemplate <typename ...Tags>\nstruct ReplaceUnwrap;\n\ntemplate <>\nstruct ReplaceUnwrap<> {\n\ttemplate <typename Type>\n\t[[nodiscard]] static Type Call(Type accumulated) {\n\t\treturn accumulated;\n\t}\n};\n\ntemplate <typename Tag, typename ...Tags>\nstruct ReplaceUnwrap<Tag, Tags...> {\n\ttemplate <typename Type, typename Value, typename ...Values>\n\t[[nodiscard]] static Type Call(\n\t\t\tType accumulated,\n\t\t\tconst Value &value,\n\t\t\tconst Values &...values) {\n\t\treturn ReplaceUnwrap<Tags...>::Call(\n\t\t\tReplaceTag<Type>::Call(\n\t\t\t\tstd::move(accumulated),\n\t\t\t\tTagValue<Tag>(),\n\t\t\t\tvalue),\n\t\t\tvalues...);\n\t}\n};\n\ntemplate <typename ...Tags>\nstruct Producer {\n\ttemplate <typename P, typename ...Values>\n\t[[nodiscard]] static rpl::producer<S<P>> Combine(ushort base, P p, Values &...values) {\n\t\treturn rpl::combine(\n\t\t\tValue(base),\n\t\t\tstd::move(values)...\n\t\t) | rpl::map([p = std::move(p)](auto tuple) {\n\t\t\treturn ReplaceUnwrapTuple<1>(p(std::get<0>(tuple)), tuple, TagValue<Tags>()...);\n\t\t});\n\t}\n\n\ttemplate <typename P, typename ...Values>\n\t[[nodiscard]] static S<P> Current(ushort base, P p, const Values &...values) {\n\t\treturn ReplaceUnwrap<Tags...>::Call(\n\t\t\tp(Lang::details::Current(base)),\n\t\t\tvalues...);\n\t}\n};\n\ntemplate <>\nstruct Producer<> {\n\ttemplate <typename P>\n\t[[nodiscard]] static rpl::producer<S<P>> Combine(ushort base, P p) {\n\t\treturn Value(base) | rpl::map(std::move(p));\n\t}\n\n\ttemplate <typename P>\n\t[[nodiscard]] static S<P> Current(ushort base, P p) {\n\t\treturn p(Lang::details::Current(base));\n\t}\n};\n\ntemplate <typename ...Tags>\nstruct Producer<lngtag_count, Tags...> {\n\ttemplate <typename P, typename ...Values>\n\t[[nodiscard]] static rpl::producer<S<P>> Combine(\n\t\t\tushort base,\n\t\t\tP p,\n\t\t\tlngtag_count type,\n\t\t\trpl::producer<float64> &count,\n\t\t\tValues &...values) {\n\t\treturn rpl::combine(\n\t\t\tValue(base),\n\t\t\tValue(base + 1),\n\t\t\tValue(base + 2),\n\t\t\tValue(base + 3),\n\t\t\tValue(base + 4),\n\t\t\tValue(base + 5),\n\t\t\tstd::move(count),\n\t\t\tstd::move(values)...\n\t\t) | rpl::map([base, type, p = std::move(p)](auto tuple) {\n\t\t\tauto plural = Plural(base, std::get<6>(tuple), type);\n\t\t\tconst auto select = [&] {\n\t\t\t\tswitch (plural.keyShift) {\n\t\t\t\tcase 0: return std::get<0>(tuple);\n\t\t\t\tcase 1: return std::get<1>(tuple);\n\t\t\t\tcase 2: return std::get<2>(tuple);\n\t\t\t\tcase 3: return std::get<3>(tuple);\n\t\t\t\tcase 4: return std::get<4>(tuple);\n\t\t\t\tcase 5: return std::get<5>(tuple);\n\t\t\t\t}\n\t\t\t\tUnexpected(\"Lang shift value in Plural result.\");\n\t\t\t};\n\t\t\treturn ReplaceUnwrapTuple<7>(\n\t\t\t\tReplaceTag<S<P>>::Call(\n\t\t\t\t\tp(select()),\n\t\t\t\t\tTagValue<lngtag_count>(),\n\t\t\t\t\tStartReplacements<S<P>>::Call(\n\t\t\t\t\t\tstd::move(plural.replacement))),\n\t\t\t\ttuple,\n\t\t\t\tTagValue<Tags>()...);\n\t\t});\n\t}\n\n\ttemplate <typename P, typename ...Values>\n\t[[nodiscard]] static S<P> Current(\n\t\t\tushort base,\n\t\t\tP p,\n\t\t\tlngtag_count type,\n\t\t\tfloat64 count,\n\t\t\tconst Values &...values) {\n\t\tauto plural = Plural(base, count, type);\n\t\treturn ReplaceUnwrap<Tags...>::Call(\n\t\t\tReplaceTag<S<P>>::Call(\n\t\t\t\tp(Lang::details::Current(base + plural.keyShift)),\n\t\t\t\tTagValue<lngtag_count>(),\n\t\t\t\tStartReplacements<S<P>>::Call(\n\t\t\t\t\tstd::move(plural.replacement))),\n\t\t\tvalues...);\n\t}\n};\n\n} // namespace details\n} // namespace Lang\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/translate_mtproto_provider.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"lang/translate_mtproto_provider.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/sender.h\"\n#include \"spellcheck/platform/platform_language.h\"\n\nnamespace Ui {\nnamespace {\n\nclass MTProtoTranslateProvider final : public TranslateProvider {\npublic:\n\texplicit MTProtoTranslateProvider(not_null<Main::Session*> session)\n\t: _session(session)\n\t, _api(&session->mtp()) {\n\t}\n\n\t[[nodiscard]] bool supportsMessageId() const override {\n\t\treturn true;\n\t}\n\n\tvoid request(\n\t\t\tTranslateProviderRequest request,\n\t\t\tLanguageId to,\n\t\t\tFn<void(TranslateProviderResult)> done) override {\n\t\trequestBatch(\n\t\t\t{ std::move(request) },\n\t\t\tto,\n\t\t\t[done = std::move(done)](\n\t\t\t\t\tint,\n\t\t\t\t\tTranslateProviderResult result) {\n\t\t\t\tdone(std::move(result));\n\t\t\t},\n\t\t\t[] {});\n\t}\n\n\tvoid requestBatch(\n\t\t\tstd::vector<TranslateProviderRequest> requests,\n\t\t\tconst LanguageId &to,\n\t\t\tFn<void(int, TranslateProviderResult)> doneOne,\n\t\t\tFn<void()> doneAll) override {\n\t\tusing Flag = MTPmessages_TranslateText::Flag;\n\t\tif (requests.empty()) {\n\t\t\tdoneAll();\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto failAll = [=] {\n\t\t\tfor (auto i = 0; i != requests.size(); ++i) {\n\t\t\t\tdoneOne(i, TranslateProviderResult{\n\t\t\t\t\t.error = TranslateProviderError::Unknown,\n\t\t\t\t});\n\t\t\t}\n\t\t\tdoneAll();\n\t\t};\n\t\tconst auto doneFromList = [=, session = _session](\n\t\t\t\tconst QVector<MTPTextWithEntities> &list) {\n\t\t\tfor (auto i = 0; i != requests.size(); ++i) {\n\t\t\t\tdoneOne(\n\t\t\t\t\ti,\n\t\t\t\t\t(i < list.size())\n\t\t\t\t\t\t? TranslateProviderResult{\n\t\t\t\t\t\t\t.text = Api::ParseTextWithEntities(\n\t\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\t\tlist[i]),\n\t\t\t\t\t\t}\n\t\t\t\t\t\t: TranslateProviderResult{\n\t\t\t\t\t\t\t.error = TranslateProviderError::Unknown,\n\t\t\t\t\t\t});\n\t\t\t}\n\t\t\tdoneAll();\n\t\t};\n\n\t\tconst auto firstPeer = PeerId(requests.front().peerId);\n\t\tconst auto allWithIds = ranges::all_of(\n\t\t\trequests,\n\t\t\t[&](const TranslateProviderRequest &request) {\n\t\t\t\treturn (PeerId(request.peerId) == firstPeer)\n\t\t\t\t\t&& (request.msgId != 0);\n\t\t\t});\n\t\tif (allWithIds) {\n\t\t\tconst auto peer = _session->data().peerLoaded(firstPeer);\n\t\t\tif (!peer) {\n\t\t\t\tfailAll();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto ids = QVector<MTPint>();\n\t\t\tids.reserve(requests.size());\n\t\t\tfor (const auto &request : requests) {\n\t\t\t\tids.push_back(MTP_int(MsgId(request.msgId)));\n\t\t\t}\n\t\t\t_api.request(MTPmessages_TranslateText(\n\t\t\t\tMTP_flags(Flag::f_peer | Flag::f_id),\n\t\t\t\tpeer->input(),\n\t\t\t\tMTP_vector<MTPint>(ids),\n\t\t\t\tMTPVector<MTPTextWithEntities>(),\n\t\t\t\tMTP_string(to.twoLetterCode()),\n\t\t\t\tMTPstring() // tone\n\t\t\t)).done([=](const MTPmessages_TranslatedText &result) {\n\t\t\t\tdoneFromList(result.data().vresult().v);\n\t\t\t}).fail([=](const MTP::Error &) {\n\t\t\t\tfailAll();\n\t\t\t}).send();\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto allWithText = ranges::all_of(\n\t\t\trequests,\n\t\t\t[](const TranslateProviderRequest &request) {\n\t\t\t\treturn !request.text.text.isEmpty();\n\t\t\t});\n\t\tif (!allWithText) {\n\t\t\tTranslateProvider::requestBatch(\n\t\t\t\tstd::move(requests),\n\t\t\t\tto,\n\t\t\t\tstd::move(doneOne),\n\t\t\t\tstd::move(doneAll));\n\t\t\treturn;\n\t\t}\n\n\t\tauto text = QVector<MTPTextWithEntities>();\n\t\ttext.reserve(requests.size());\n\t\tfor (const auto &request : requests) {\n\t\t\ttext.push_back(MTP_textWithEntities(\n\t\t\t\tMTP_string(request.text.text),\n\t\t\t\tApi::EntitiesToMTP(\n\t\t\t\t\t_session,\n\t\t\t\t\trequest.text.entities,\n\t\t\t\t\tApi::ConvertOption::SkipLocal)));\n\t\t}\n\t\t_api.request(MTPmessages_TranslateText(\n\t\t\tMTP_flags(Flag::f_text),\n\t\t\tMTP_inputPeerEmpty(),\n\t\t\tMTPVector<MTPint>(),\n\t\t\tMTP_vector<MTPTextWithEntities>(text),\n\t\t\tMTP_string(to.twoLetterCode()),\n\t\t\tMTPstring() // tone\n\t\t)).done([=](const MTPmessages_TranslatedText &result) {\n\t\t\tdoneFromList(result.data().vresult().v);\n\t\t}).fail([=](const MTP::Error &) {\n\t\t\tfailAll();\n\t\t}).send();\n\t}\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\n};\n\n} // namespace\n\nstd::unique_ptr<TranslateProvider> CreateMTProtoTranslateProvider(\n\t\tnot_null<Main::Session*> session) {\n\treturn std::make_unique<MTProtoTranslateProvider>(session);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/translate_mtproto_provider.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"translate_provider.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\n\n[[nodiscard]] std::unique_ptr<TranslateProvider> CreateMTProtoTranslateProvider(\n\tnot_null<Main::Session*> session);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/translate_provider.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"lang/translate_provider.h\"\n\n#include \"base/options.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/data_msg_id.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"history/history_item.h\"\n#include \"lang/translate_mtproto_provider.h\"\n#include \"lang/translate_url_provider.h\"\n#include \"platform/platform_translate_provider.h\"\n\nnamespace {\n\nbase::options::option<QString> OptionTranslateUrlTemplate({\n\t.id = \"translate-url-template\",\n\t.name = \"Translate URL template\",\n\t.description = \"Template URL for custom translation provider.\"\n\t\t\" Supports %q text, %f source language and %t target language.\",\n});\n\n} // namespace\n\nnamespace Ui {\n\nstd::unique_ptr<TranslateProvider> CreateTranslateProvider(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto urlTemplate = OptionTranslateUrlTemplate.value();\n\tif (!urlTemplate.isEmpty()\n\t\t&& urlTemplate.contains(u\"%q\"_q)) {\n\t\treturn CreateUrlTranslateProvider(urlTemplate);\n\t}\n\tif (Core::App().settings().usePlatformTranslation()\n\t\t&& Platform::IsTranslateProviderAvailable()) {\n\t\treturn Platform::CreateTranslateProvider();\n\t}\n\treturn CreateMTProtoTranslateProvider(session);\n}\n\nTranslateProviderRequest PrepareTranslateProviderRequest(\n\t\tnot_null<TranslateProvider*> provider,\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId msgId,\n\t\tTextWithEntities text) {\n\tauto result = TranslateProviderRequest{\n\t\t.peerId = uint64(peer->id.value),\n\t\t.msgId = IsServerMsgId(msgId) ? msgId.bare : 0,\n\t\t.text = std::move(text),\n\t};\n\tif (provider->supportsMessageId()) {\n\t\treturn result;\n\t}\n\tif (result.msgId) {\n\t\tif (const auto i = peer->owner().message(peer, MsgId(result.msgId))) {\n\t\t\tresult.text = i->originalText();\n\t\t}\n\t\tresult.msgId = 0;\n\t}\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/translate_provider.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <translate_provider.h>\n\nclass PeerData;\nstruct MsgId;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\n\n[[nodiscard]] std::unique_ptr<TranslateProvider> CreateTranslateProvider(\n\tnot_null<Main::Session*> session);\n\n[[nodiscard]] TranslateProviderRequest PrepareTranslateProviderRequest(\n\tnot_null<TranslateProvider*> provider,\n\tnot_null<PeerData*> peer,\n\tMsgId msgId,\n\tTextWithEntities text);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/translate_url_provider.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"lang/translate_url_provider.h\"\n\n#include \"spellcheck/platform/platform_language.h\"\n\n#include <QtCore/QJsonArray>\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonObject>\n#include <QtCore/QJsonParseError>\n#include <QtCore/QUrl>\n#include <QtNetwork/QNetworkAccessManager>\n#include <QtNetwork/QNetworkReply>\n#include <QtNetwork/QNetworkRequest>\n\nnamespace Ui {\nnamespace {\n\n[[nodiscard]] bool SkipJsonKey(const QString &key) {\n\treturn (key.compare(u\"code\"_q, Qt::CaseInsensitive) == 0);\n}\n\n[[nodiscard]] QString DetectFromLanguage(const QString &text) {\n#ifndef TDESKTOP_DISABLE_SPELLCHECK\n\tconst auto result = Platform::Language::Recognize(text);\n\treturn result.known() ? result.twoLetterCode() : u\"auto\"_q;\n#else // TDESKTOP_DISABLE_SPELLCHECK\n\treturn u\"auto\"_q;\n#endif // !TDESKTOP_DISABLE_SPELLCHECK\n}\n\n[[nodiscard]] QString JsonValueToText(const QJsonValue &v) {\n\tswitch (v.type()) {\n\tcase QJsonValue::Null: return u\"null\"_q;\n\tcase QJsonValue::Bool: return v.toBool()\n\t\t? u\"true\"_q\n\t\t: u\"false\"_q;\n\tcase QJsonValue::Double: return QString::number(v.toDouble(), 'g', 15);\n\tcase QJsonValue::String: return v.toString().trimmed();\n\tcase QJsonValue::Array: return QString::fromUtf8(\n\t\tQJsonDocument(v.toArray()).toJson(QJsonDocument::Compact));\n\tcase QJsonValue::Object: return QString::fromUtf8(\n\t\tQJsonDocument(v.toObject()).toJson(QJsonDocument::Compact));\n\tcase QJsonValue::Undefined: return QString();\n\t}\n\treturn QString();\n}\n\n[[nodiscard]] std::optional<QString> ParseSegmentedArrayResponse(\n\t\tconst QJsonDocument &parsed) {\n\tif (!parsed.isArray()) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto root = parsed.array();\n\tif (root.isEmpty() || !root[0].isArray()) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto segments = root[0].toArray();\n\tauto translated = QString();\n\tfor (const auto &segmentValue : segments) {\n\t\tif (!segmentValue.isArray()) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\tconst auto segment = segmentValue.toArray();\n\t\tif (segment.isEmpty()) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\tif (!segment[0].isString()) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\ttranslated += segment[0].toString();\n\t}\n\tif (translated.trimmed().isEmpty()) {\n\t\treturn std::nullopt;\n\t}\n\treturn translated;\n}\n\nstruct JsonLine {\n\tQString name;\n\tQString value;\n\tint length = 0;\n};\n\nvoid CollectJsonLines(\n\t\tconst QString &name,\n\t\tconst QJsonValue &value,\n\t\tstd::vector<JsonLine> &lines) {\n\tif (value.isObject()) {\n\t\tconst auto object = value.toObject();\n\t\tfor (auto i = object.constBegin(); i != object.constEnd(); ++i) {\n\t\t\tif (SkipJsonKey(i.key())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tCollectJsonLines(\n\t\t\t\tname.isEmpty() ? i.key() : (name + '.' + i.key()),\n\t\t\t\ti.value(),\n\t\t\t\tlines);\n\t\t}\n\t\treturn;\n\t}\n\tif (value.isArray()) {\n\t\tconst auto array = value.toArray();\n\t\tfor (auto i = 0; i != array.size(); ++i) {\n\t\t\tCollectJsonLines(\n\t\t\t\tu\"%1[%2]\"_q.arg(name).arg(i),\n\t\t\t\tarray.at(i),\n\t\t\t\tlines);\n\t\t}\n\t\treturn;\n\t}\n\tconst auto text = JsonValueToText(value);\n\tif (text.isEmpty()) {\n\t\treturn;\n\t}\n\tlines.push_back(JsonLine{\n\t\t.name = name,\n\t\t.value = text,\n\t\t.length = int(text.size()),\n\t});\n}\n\n[[nodiscard]] std::optional<QString> FormatJsonResponse(\n\t\tconst QByteArray &body) {\n\tauto error = QJsonParseError();\n\tconst auto parsed = QJsonDocument::fromJson(body, &error);\n\tif (error.error != QJsonParseError::NoError) {\n\t\treturn std::nullopt;\n\t}\n\tif (const auto parsedArray = ParseSegmentedArrayResponse(parsed)) {\n\t\treturn parsedArray;\n\t}\n\tauto lines = std::vector<JsonLine>();\n\tif (parsed.isObject()) {\n\t\tconst auto object = parsed.object();\n\t\tfor (auto i = object.constBegin(); i != object.constEnd(); ++i) {\n\t\t\tif (SkipJsonKey(i.key())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tCollectJsonLines(i.key(), i.value(), lines);\n\t\t}\n\t} else if (parsed.isArray()) {\n\t\tconst auto array = parsed.array();\n\t\tfor (auto i = 0; i != array.size(); ++i) {\n\t\t\tCollectJsonLines(u\"[%1]\"_q.arg(i), array.at(i), lines);\n\t\t}\n\t}\n\tif (lines.empty()) {\n\t\treturn QString::fromUtf8(body);\n\t}\n\tranges::sort(lines, [](const JsonLine &a, const JsonLine &b) {\n\t\treturn (a.length != b.length)\n\t\t\t? (a.length > b.length)\n\t\t\t: (a.name < b.name);\n\t});\n\tauto result = QString();\n\tresult.reserve(lines.size() * 16);\n\tfor (auto i = 0; i != int(lines.size()); ++i) {\n\t\tconst auto &line = lines[i];\n\t\tif (!line.name.isEmpty()) {\n\t\t\tresult += line.name;\n\t\t\tresult += '\\n';\n\t\t}\n\t\tresult += line.value;\n\t\tif (i + 1 != int(lines.size())) {\n\t\t\tresult += \"\\n\\n\";\n\t\t}\n\t}\n\treturn result;\n}\n\nclass UrlTranslateProvider final : public TranslateProvider {\npublic:\n\texplicit UrlTranslateProvider(QString urlTemplate)\n\t: _urlTemplate(std::move(urlTemplate)) {\n\t}\n\n\t[[nodiscard]] bool supportsMessageId() const override {\n\t\treturn false;\n\t}\n\n\tvoid request(\n\t\t\tTranslateProviderRequest request,\n\t\t\tLanguageId to,\n\t\t\tFn<void(TranslateProviderResult)> done) override {\n\t\tif (request.text.text.isEmpty()) {\n\t\t\tdone(TranslateProviderResult{\n\t\t\t\t.error = TranslateProviderError::Unknown,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tauto url = _urlTemplate;\n\t\tconst auto from = DetectFromLanguage(request.text.text);\n\t\tconst auto toCode = to.twoLetterCode();\n\t\turl.replace(\n\t\t\tu\"%q\"_q,\n\t\t\tQString::fromLatin1(\n\t\t\t\tQUrl::toPercentEncoding(request.text.text.toHtmlEscaped())));\n\t\turl.replace(\n\t\t\tu\"%f\"_q,\n\t\t\tQString::fromLatin1(QUrl::toPercentEncoding(from)));\n\t\turl.replace(\n\t\t\tu\"%t\"_q,\n\t\t\tQString::fromLatin1(QUrl::toPercentEncoding(toCode)));\n\t\tconst auto requestUrl = QUrl(url);\n\t\tif (!requestUrl.isValid()) {\n\t\t\tdone(TranslateProviderResult{\n\t\t\t\t.error = TranslateProviderError::Unknown,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tauto networkRequest = QNetworkRequest(requestUrl);\n\t\tconst auto reply = _network.get(networkRequest);\n\t\tQObject::connect(reply, &QNetworkReply::finished, [=] {\n\t\t\tauto result = TranslateProviderResult();\n\t\t\tif (reply->error() != QNetworkReply::NoError) {\n\t\t\t\tresult.error = TranslateProviderError::Unknown;\n\t\t\t} else {\n\t\t\t\tconst auto body = reply->readAll();\n\t\t\t\tconst auto formatted = FormatJsonResponse(body).value_or(\n\t\t\t\t\tQString::fromUtf8(body));\n\t\t\t\tresult.text = TextWithEntities{ formatted };\n\t\t\t}\n\t\t\tdone(std::move(result));\n\t\t\treply->deleteLater();\n\t\t});\n\t}\n\nprivate:\n\tconst QString _urlTemplate;\n\tQNetworkAccessManager _network;\n\n};\n\n} // namespace\n\nstd::unique_ptr<TranslateProvider> CreateUrlTranslateProvider(\n\t\tQString urlTemplate) {\n\treturn std::make_unique<UrlTranslateProvider>(std::move(urlTemplate));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/lang/translate_url_provider.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"translate_provider.h\"\n\nnamespace Ui {\n\n[[nodiscard]] std::unique_ptr<TranslateProvider> CreateUrlTranslateProvider(\n\tQString urlTemplate);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/languages.h",
    "content": "/*\n\tAuthor: github.com/23rd.\n*/\n\n#pragma once\n\nstatic const QMap<QLocale::Language, QString> languages = {\n\n\t{QLocale::English,    \"`1234567890-=qwertyuiop[]asdfghjkl;'zxcvbnm,./~QWERTYUIOP{}ASDFGHJKL:\\\" ZXCVBNM<>?\"},\n    {QLocale::Arabic,     \"ذ1234567890-=ضصثقفغعهخحجدسيبلاتنمكط ؤر ىةوزظ  # إ`÷×؛<>|ٍِ]أـ،/:\\\"   ~ْ}',.؟ \"},\n\t{QLocale::Chinese,    \"`1234567890-=qwertyuiop[]asdfghjkl;'zxcvbnm,./~QWERTYUIOP{}ASDFGHJKL:\\\" ZXCVBNM<>?\"},\n\t{QLocale::Croatian,   \"¸1234567890'+qwertzuiopšđasdfghjklčćyxcvbnm,.-¨QWERTZUIOPŠĐASDFGHJKLČĆŽYXCVBNM;:_\"},\n\t{QLocale::Danish,     \"½1234567890+´qwertyuiopå¨asdfghjklæøzxcvbnm,.-½QWERTYUIOPÅ^ASDFGHJKLÆØ*ZXCVBNM;:_\"},\n\t{QLocale::Esperanto,  \"[1234567890-=',.pbfvdgh/yriaokjentlĝ;ŝscĉmuĵŭz{@<>PBFVDGH?YRIAOKJENTLĜW:ŜSCĈMUĴŬZ\"},\n\t{QLocale::Finnish,    \"§1234567890+´qwertyuiopå¨asdfghjklöäzxcvbnm,.-½QWERTYUIOPÅ^ASDFGHJKLÖÄ*ZXCVBNM;:_\"},\n\t{QLocale::French,     \" &é\\\"'(-è_çà)=azertyuiop^$qsdfghjklmùwxcvbn,;:! AZERTYUIOP\\\"£QSDFGHJKLM%µWXCVBN?./§\"},\n\t{QLocale::Galician,   \"º1234567890'¡qwertyuiop`+asdfghjklñ'zxcvbnm,.-ªQWERTYUIOP^*ASDFGHJKLÑ\\\"ÇZXCVBNM;:_\"},\n\t{QLocale::German,     \"°1234567890ß'qwertzuiopü+asdfghjklÖÄyxcvbnm,.-°QWERTZUIOPÜ*ASDFGHJKLÖÄ'YXCVBNM;:_\"},\n\t{QLocale::Greek,      \"`1234567890-=;ςερτυθιοπ[]ασδφγηξκλά'ζχψωβνμ,./~:ΣΕΡΤΥΘΙΟΠ{}ΑΣΔΦΓΗΞΚΛ¨\\\"|ΖΧΨΩΒΝΜ<>?\"},\n\t{QLocale::Italian,    \"\\\\1234567890'ìqwertyuiopè+asdfghjklòàzxcvbnm,.-|QWERTYUIOPé*ASDFGHJKLç°§ZXCVBNM;:_\"},\n\t{QLocale::Korean,     \"`1234567890-=ㅂㅈㄷㄱㅅㅛㅕㅑㅐㅔ[]ㅁㄴㅇㄹㅎㅗㅓㅏㅣ;'ㅋㅌㅊㅍㅠㅜㅡ,./~ㅃㅉㄸㄲㅆㅛㅕㅑㅒㅖ{}ㅁㄴㅇㄹㅎㅗㅓㅏㅣ:\\\" ㅋㅌㅊㅍㅠㅜㅡ<>?\"},\n\t{QLocale::Norwegian,  \"|1234567890+\\\\qwertyuiopå¨asdfghjkløæzxcvbnm,.-`QWERTYUIOPÅ^ASDFGHJKLØÆ*ZXCVBNM;:_\"},\n\t{QLocale::Polish,     \"`1234567890-=qwertyuiop[]asdfghjkl;'zxcvbnm,./~QWERTYUIOP{}ASDFGHJKL:\\\" ZXCVBNM<>?\"},\n\t{QLocale::Portuguese, \"º1234567890'¡qwertyuiop`+asdfghjklñ'zxcvbnm,.-ªQWERTYUIOP^*ASDFGHJKLÑ\\\"ÇZXCVBNM;:_\"},\n\t{QLocale::Russian,    \"ё1234567890-=йцукенгшщзхъфывапролджэячсмитьбю.ЁЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭ/ЯЧСМИТЬБЮ,\"},\n\t{QLocale::Serbian,    \"`1234567890'+љњертзуиопшђасдфгхјклчћsџцвбнм,.-~ЉЊЕРТЗУИОПШЂАСДФГХЈКЛЧЋ SЏЦВБНМ;:_\"},\n\t{QLocale::Slovenian,  \"¸1234567890'+qwertzuiopšđasdfghjklčćyxcvbnm,.-¨QWERTZUIOPŠĐASDFGHJKLČĆŽYXCVBNM;:_\"},\n\t{QLocale::Spanish,    \"º1234567890'¡qwertyuiop`+asdfghjklñ'zxcvbnm,.-ªQWERTYUIOP^*ASDFGHJKLÑ\\\"ÇZXCVBNM;:_\"},\n\t{QLocale::Ukrainian,  \"'1234567890-=йцукенгшщзхїфівапролджєячсмитьбю.'ЙЦУКЕНГШЩЗХЇФІВАПРОЛДЖЄ ЯЧСМИТЬБЮ,\"},\n};\n\nconst QString swapFromWrongKeyboardLayout(const QString &text, const bool &toEng) {\n    const auto systemLang = QLocale::system().language();\n    if (!languages.contains(systemLang)\n    \t|| systemLang == QLocale::English) {\n    \treturn text;\n    }\n\n    QString sys = toEng ? languages[systemLang] : languages[QLocale::English];\n    QString eng = toEng ? languages[QLocale::English] : languages[systemLang];\n    QString newString = \"\";\n\n    QString symbol = \"\";\n    for (int i = 0; i < text.length(); i++) {\n    \tsymbol = QString(text[i]);\n        if (symbol == \" \") {\n            newString += symbol;\n            continue;\n        }\n        int index = sys.indexOf(symbol);\n        if (index >= 0) {\n        \tnewString += QString(eng[index]);\n        } else {\n    \t\tnewString += symbol;\n        }\n    }\n\n    return newString;\n}"
  },
  {
    "path": "Telegram/SourceFiles/layout/abstract_layout_item.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"layout/abstract_layout_item.h\"\n\nAbstractLayoutItem::AbstractLayoutItem() {\n}\n\nint AbstractLayoutItem::maxWidth() const {\n\treturn _maxw;\n}\nint AbstractLayoutItem::minHeight() const {\n\treturn _minh;\n}\n\nint AbstractLayoutItem::resizeGetHeight(int width) {\n\t_width = qMin(width, _maxw);\n\t_height = _minh;\n\treturn _height;\n}\n\nint AbstractLayoutItem::width() const {\n\treturn _width;\n}\nint AbstractLayoutItem::height() const {\n\treturn _height;\n}\n\nvoid AbstractLayoutItem::setPosition(int position) {\n\t_position = position;\n}\nint AbstractLayoutItem::position() const {\n\treturn _position;\n}\n\nvoid AbstractLayoutItem::setShiftX(int x) {\n\t_shiftX = x;\n}\n\nvoid AbstractLayoutItem::setShiftY(int y) {\n\t_shiftY = y;\n}\n\nQPoint AbstractLayoutItem::shift() const {\n\treturn { _shiftX, _shiftY };\n}\n\nbool AbstractLayoutItem::hasPoint(QPoint point) const {\n\treturn QRect(0, 0, width(), height()).contains(point);\n}\n\nAbstractLayoutItem::~AbstractLayoutItem() {\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/layout/abstract_layout_item.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/runtime_composer.h\"\n#include \"ui/click_handler.h\"\n\nclass PaintContextBase {\npublic:\n\tPaintContextBase(crl::time ms, bool selecting)\n\t: ms(ms)\n\t, selecting(selecting) {\n\t}\n\tcrl::time ms = 0;\n\tbool selecting = false;\n\n};\n\nclass AbstractLayoutItem\n\t: public RuntimeComposer<AbstractLayoutItem>\n\t, public ClickHandlerHost {\npublic:\n\tAbstractLayoutItem();\n\n\tAbstractLayoutItem(const AbstractLayoutItem &other) = delete;\n\tAbstractLayoutItem &operator=(\n\t\tconst AbstractLayoutItem &other) = delete;\n\n\t[[nodiscard]] int maxWidth() const;\n\t[[nodiscard]] int minHeight() const;\n\tvirtual int resizeGetHeight(int width);\n\n\t[[nodiscard]] int width() const;\n\t[[nodiscard]] int height() const;\n\n\tvirtual void setPosition(int position);\n\t[[nodiscard]] int position() const;\n\n\tvoid setShiftX(int x);\n\tvoid setShiftY(int y);\n\t[[nodiscard]] QPoint shift() const;\n\n\t[[nodiscard]] bool hasPoint(QPoint point) const;\n\n\tvirtual ~AbstractLayoutItem();\n\nprotected:\n\tint _width = 0;\n\tint _height = 0;\n\tint _maxw = 0;\n\tint _minh = 0;\n\tint _position = 0; // < 0 means removed from layout\n\tint _shiftX = 0;\n\tint _shiftY = 0;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/layout/layout_document_generic_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"layout/layout_document_generic_preview.h\"\n\n#include \"data/data_document.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Layout {\nnamespace {\n\n[[nodiscard]] int ColorIndexForName(\n\t\tconst QString &name,\n\t\tconst QString &mime = QString()) {\n\tconst auto lastDot = name.lastIndexOf('.');\n\tif (name.endsWith(u\".doc\"_q) ||\n\t\tname.endsWith(u\".docx\"_q) ||\n\t\tname.endsWith(u\".txt\"_q) ||\n\t\tname.endsWith(u\".psd\"_q) ||\n\t\tmime.startsWith(u\"text/\"_q)) {\n\t\treturn 0;\n\t} else if (\n\t\tname.endsWith(u\".xls\"_q) ||\n\t\tname.endsWith(u\".xlsx\"_q) ||\n\t\tname.endsWith(u\".csv\"_q)) {\n\t\treturn 1;\n\t} else if (\n\t\tname.endsWith(u\".pdf\"_q) ||\n\t\tname.endsWith(u\".ppt\"_q) ||\n\t\tname.endsWith(u\".pptx\"_q) ||\n\t\tname.endsWith(u\".key\"_q)) {\n\t\treturn 2;\n\t} else if (\n\t\tname.endsWith(u\".zip\"_q) ||\n\t\tname.endsWith(u\".rar\"_q) ||\n\t\tname.endsWith(u\".ai\"_q) ||\n\t\tname.endsWith(u\".mp3\"_q) ||\n\t\tname.endsWith(u\".mov\"_q) ||\n\t\tname.endsWith(u\".avi\"_q)) {\n\t\treturn 3;\n\t}\n\tconst auto ch = (lastDot >= 0 && lastDot + 1 < name.size())\n\t\t? name.at(lastDot + 1)\n\t\t: (name.isEmpty()\n\t\t\t? (mime.isEmpty() ? QChar('0') : mime.at(0))\n\t\t\t: name.at(0));\n\treturn (ch.unicode() % 4) & 3;\n}\n\n[[nodiscard]] DocumentGenericPreview BuildFromColorIndex(\n\t\tint colorIndex,\n\t\tconst QString &ext) {\n\tswitch (colorIndex) {\n\tcase 0: return {\n\t\t.index = colorIndex,\n\t\t.color = st::msgFile1Bg,\n\t\t.dark = st::msgFile1BgDark,\n\t\t.over = st::msgFile1BgOver,\n\t\t.selected = st::msgFile1BgSelected,\n\t\t.ext = ext,\n\t};\n\tcase 1: return {\n\t\t.index = colorIndex,\n\t\t.color = st::msgFile2Bg,\n\t\t.dark = st::msgFile2BgDark,\n\t\t.over = st::msgFile2BgOver,\n\t\t.selected = st::msgFile2BgSelected,\n\t\t.ext = ext,\n\t};\n\tcase 2: return {\n\t\t.index = colorIndex,\n\t\t.color = st::msgFile3Bg,\n\t\t.dark = st::msgFile3BgDark,\n\t\t.over = st::msgFile3BgOver,\n\t\t.selected = st::msgFile3BgSelected,\n\t\t.ext = ext,\n\t};\n\tcase 3: return {\n\t\t.index = colorIndex,\n\t\t.color = st::msgFile4Bg,\n\t\t.dark = st::msgFile4BgDark,\n\t\t.over = st::msgFile4BgOver,\n\t\t.selected = st::msgFile4BgSelected,\n\t\t.ext = ext,\n\t};\n\t}\n\tUnexpected(\"Color index in BuildFromColorIndex.\");\n}\n\n} // namespace\n\nconst style::icon *DocumentGenericPreview::icon() const {\n\tswitch (index) {\n\tcase 0: return &st::mediaviewFileBlue;\n\tcase 1: return &st::mediaviewFileGreen;\n\tcase 2: return &st::mediaviewFileRed;\n\tcase 3: return &st::mediaviewFileYellow;\n\t}\n\tUnexpected(\"Color index in DocumentGenericPreview::icon.\");\n}\n\nDocumentGenericPreview DocumentGenericPreview::Create(\n\t\tDocumentData *document) {\n\tconst auto name = (document\n\t\t? (document->filename().isEmpty()\n\t\t\t? (document->sticker()\n\t\t\t\t? tr::lng_in_dlg_sticker(tr::now)\n\t\t\t\t: u\"Unknown File\"_q)\n\t\t\t: document->filename())\n\t\t: tr::lng_message_empty(tr::now)).toLower();\n\tconst auto mime = document ? document->mimeString() : QString();\n\tconst auto colorIndex = ColorIndexForName(name, mime);\n\tconst auto lastDot = name.lastIndexOf('.');\n\tconst auto ext = document\n\t\t? ((lastDot < 0 || lastDot + 2 > name.size())\n\t\t\t? name\n\t\t\t: name.mid(lastDot + 1))\n\t\t: QString();\n\n\treturn BuildFromColorIndex(colorIndex, ext);\n}\n\nDocumentGenericPreview DocumentGenericPreview::Create(\n\t\tconst QString &filename) {\n\tconst auto name = filename.toLower();\n\tconst auto colorIndex = ColorIndexForName(name);\n\tconst auto lastDot = name.lastIndexOf('.');\n\tconst auto ext = (lastDot < 0 || lastDot + 2 > name.size())\n\t\t? name\n\t\t: name.mid(lastDot + 1);\n\n\treturn BuildFromColorIndex(colorIndex, ext);\n}\n\n// Ui::CachedRoundCorners DocumentCorners(int32 colorIndex) {\n// \treturn Ui::CachedRoundCorners(Ui::Doc1Corners + (colorIndex & 3));\n// }\n\n} // namespace Layout\n"
  },
  {
    "path": "Telegram/SourceFiles/layout/layout_document_generic_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Layout {\n\nstruct DocumentGenericPreview final {\n\tstatic DocumentGenericPreview Create(DocumentData *document);\n\tstatic DocumentGenericPreview Create(const QString &filename);\n\tconst style::icon *icon() const;\n\tconst int index;\n\tconst style::color &color;\n\tconst style::color &dark;\n\tconst style::color &over;\n\tconst style::color &selected;\n\tconst QString ext;\n};\n\n// Ui::CachedRoundCorners DocumentCorners(int colorIndex);\n\n} // namespace Layout\n"
  },
  {
    "path": "Telegram/SourceFiles/layout/layout_item_base.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"layout/layout_item_base.h\"\n\n#include \"history/view/history_view_cursor_state.h\"\n\n[[nodiscard]] HistoryView::TextState LayoutItemBase::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\treturn {};\n}\n\n[[nodiscard]] TextSelection LayoutItemBase::adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const {\n\treturn selection;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/layout/layout_item_base.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"layout/abstract_layout_item.h\"\n\nnamespace HistoryView {\nstruct TextState;\nstruct StateRequest;\n} // namespace HistoryView\n\nclass LayoutItemBase : public AbstractLayoutItem {\npublic:\n\tusing TextState = HistoryView::TextState;\n\tusing StateRequest = HistoryView::StateRequest;\n\n\tusing AbstractLayoutItem::AbstractLayoutItem;\n\n\tvirtual void initDimensions() = 0;\n\n\t[[nodiscard]] virtual TextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const;\n\t[[nodiscard]] virtual TextSelection adjustSelection(\n\t\tTextSelection selection,\n\t\tTextSelectType type) const;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/layout/layout_mosaic.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"layout/layout_mosaic.h\"\n\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Mosaic::Layout {\n\nAbstractMosaicLayout::AbstractMosaicLayout(int bigWidth)\n: _bigWidth(bigWidth) {\n}\n\nint AbstractMosaicLayout::rowHeightAt(int row) const {\n\tExpects(row >= 0 && row < _rows.size());\n\n\treturn _rows[row].height;\n}\n\nint AbstractMosaicLayout::countDesiredHeight(int newWidth) {\n\tauto result = 0;\n\tfor (auto &row : _rows) {\n\t\tlayoutRow(row, newWidth ? newWidth : _width);\n\t\tresult += row.height;\n\t}\n\treturn _padding.top() + result + _padding.bottom();\n}\n\nFoundItem AbstractMosaicLayout::findByPoint(const QPoint &globalPoint) const {\n\tauto sx = globalPoint.x() - _padding.left();\n\tauto sy = globalPoint.y() - _padding.top();\n\tauto row = -1;\n\tauto col = -1;\n\tauto sel = -1;\n\tbool exact = true;\n\tif (sy >= 0) {\n\t\trow = 0;\n\t\tfor (auto rows = rowsCount(); row < rows; ++row) {\n\t\t\tconst auto rowHeight = _rows[row].height;\n\t\t\tif (sy < rowHeight) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tsy -= rowHeight;\n\t\t}\n\t} else {\n\t\trow = 0;\n\t\texact = false;\n\t}\n\tif (row >= rowsCount()) {\n\t\trow = rowsCount() - 1;\n\t\texact = false;\n\t}\n\tif (sx < 0) {\n\t\tsx = 0;\n\t\texact = false;\n\t}\n\tif (sx >= 0 && row >= 0 && row < rowsCount()) {\n\t\tconst auto columnsCount = _rows[row].items.size();\n\t\tcol = 0;\n\t\tfor (int cols = columnsCount; col < cols; ++col) {\n\t\t\tconst auto item = itemAt(row, col);\n\t\t\tconst auto width = item->width();\n\t\t\tif (sx < width) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tsx -= width;\n\t\t\tsx -= _rightSkip;\n\t\t}\n\t\tif (col >= columnsCount) {\n\t\t\tcol = columnsCount - 1;\n\t\t\texact = false;\n\t\t}\n\n\t\tsel = ::Layout::PositionToIndex(row, + col);\n\t} else {\n\t\trow = col = -1;\n\t}\n\treturn { sel, exact, QPoint(sx, sy) };\n}\n\nQRect AbstractMosaicLayout::findRect(int index) const {\n\tconst auto clip = QRect(0, 0, _width, 100);\n\tconst auto fromX = style::RightToLeft()\n\t\t? (_width - clip.x() - clip.width())\n\t\t: clip.x();\n\tconst auto toX = style::RightToLeft()\n\t\t? (_width - clip.x())\n\t\t: (clip.x() + clip.width());\n\tconst auto rows = _rows.size();\n\tauto top = 0;\n\tfor (auto row = 0; row != rows; ++row) {\n\t\tauto &inlineRow = _rows[row];\n\t\t// if ((top + inlineRow.height) > clip.top()) {\n\t\t\tauto left = 0;\n\t\t\tif (row == (rows - 1)) {\n//\t\t\t\tcontext.lastRow = true;\n\t\t\t}\n\t\t\tfor (const auto &item : inlineRow.items) {\n\t\t\t\tif (left >= toX) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tconst auto w = item->width();\n\t\t\t\tif ((left + w) > fromX) {\n\t\t\t\t\tif (item->position() == index) {\n\t\t\t\t\t\treturn QRect(\n\t\t\t\t\t\t\tleft + _padding.left(),\n\t\t\t\t\t\t\ttop + _padding.top(),\n\t\t\t\t\t\t\titem->width(),\n\t\t\t\t\t\t\titem->height());\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tleft += w;\n\t\t\t\tleft += _rightSkip;\n\t\t\t}\n\t\t// }\n\t\ttop += inlineRow.height;\n\t}\n\treturn QRect();\n}\n\nvoid AbstractMosaicLayout::addItems(\n\t\tgsl::span<const not_null<AbstractLayoutItem*>> items) {\n\t_rows.reserve(items.size());\n\tauto row = Row();\n\trow.items.reserve(kInlineItemsMaxPerRow);\n\tauto sumWidth = 0;\n\tfor (const auto &item : items) {\n\t\taddItem(item, row, sumWidth);\n\t}\n\trowFinalize(row, sumWidth, true);\n}\n\nvoid AbstractMosaicLayout::setRightSkip(int rightSkip) {\n\t_rightSkip = rightSkip;\n}\n\nvoid AbstractMosaicLayout::setPadding(QMargins padding) {\n\t_padding = padding;\n}\n\nvoid AbstractMosaicLayout::setFullWidth(int w) {\n\t_width = w;\n}\n\nbool AbstractMosaicLayout::empty() const {\n\treturn _rows.empty();\n}\n\nint AbstractMosaicLayout::rowsCount() const {\n\treturn _rows.size();\n}\n\nnot_null<AbstractLayoutItem*> AbstractMosaicLayout::itemAt(\n\t\tint row,\n\t\tint column) const {\n\tExpects((row >= 0)\n\t\t&& (row < _rows.size())\n\t\t&& (column >= 0)\n\t\t&& (column < _rows[row].items.size()));\n\n\treturn _rows[row].items[column];\n}\n\nnot_null<AbstractLayoutItem*> AbstractMosaicLayout::itemAt(int index) const {\n\tconst auto &[row, column] = ::Layout::IndexToPosition(index);\n\treturn itemAt(row, column);\n}\n\nAbstractLayoutItem *AbstractMosaicLayout::maybeItemAt(\n\t\tint row,\n\t\tint column) const {\n\tif ((row >= 0)\n\t\t&& (row < _rows.size())\n\t\t&& (column >= 0)\n\t\t&& (column < _rows[row].items.size())) {\n\t\treturn _rows[row].items[column];\n\t}\n\treturn nullptr;\n}\n\nAbstractLayoutItem *AbstractMosaicLayout::maybeItemAt(int index) const {\n\tconst auto &[row, column] = ::Layout::IndexToPosition(index);\n\treturn maybeItemAt(row, column);\n}\n\nvoid AbstractMosaicLayout::clearRows(bool resultsDeleted) {\n\tif (!resultsDeleted) {\n\t\tfor (const auto &row : _rows) {\n\t\t\tfor (const auto &item : row.items) {\n\t\t\t\titem->setPosition(-1);\n\t\t\t}\n\t\t}\n\t}\n\t_rows.clear();\n}\n\nvoid AbstractMosaicLayout::forEach(\n\t\tFn<void(not_null<const AbstractLayoutItem*>)> callback) {\n\tfor (const auto &row : _rows) {\n\t\tfor (const auto &item : row.items) {\n\t\t\tcallback(item);\n\t\t}\n\t}\n}\n\nvoid AbstractMosaicLayout::paint(\n\t\tFn<void(not_null<AbstractLayoutItem*>, QPoint)> paintItem,\n\t\tconst QRect &clip) const {\n\tauto top = _padding.top();\n\tconst auto fromX = style::RightToLeft()\n\t\t? (_width - clip.x() - clip.width())\n\t\t: clip.x();\n\tconst auto toX = style::RightToLeft()\n\t\t? (_width - clip.x())\n\t\t: (clip.x() + clip.width());\n\tconst auto rows = _rows.size();\n\tfor (auto row = 0; row != rows; ++row) {\n\t\tif (top >= clip.top() + clip.height()) {\n\t\t\tbreak;\n\t\t}\n\t\tauto &inlineRow = _rows[row];\n\t\tif ((top + inlineRow.height) > clip.top()) {\n\t\t\tauto left = _padding.left();\n\t\t\tif (row == (rows - 1)) {\n//\t\t\t\tcontext.lastRow = true;\n\t\t\t}\n\t\t\tfor (const auto &item : inlineRow.items) {\n\t\t\t\tif (left >= toX) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tconst auto w = item->width();\n\t\t\t\tif ((left + w) > fromX) {\n\t\t\t\t\tpaintItem(item, QPoint(left, top));\n\t\t\t\t}\n\t\t\t\tleft += w;\n\t\t\t\tleft += _rightSkip;\n\t\t\t}\n\t\t}\n\t\ttop += inlineRow.height;\n\t}\n}\n\nint AbstractMosaicLayout::validateExistingRows(\n\t\tFn<bool(not_null<const AbstractLayoutItem*>, int)> checkItem,\n\t\tint count) {\n\tauto until = 0;\n\tauto untilRow = 0;\n\tauto untilCol = 0;\n\twhile (until < count) {\n\t\tif ((untilRow >= _rows.size())\n\t\t\t|| checkItem(_rows[untilRow].items[untilCol], until)) {\n\t\t\tbreak;\n\t\t}\n\t\t++until;\n\t\tif (++untilCol == _rows[untilRow].items.size()) {\n\t\t\t++untilRow;\n\t\t\tuntilCol = 0;\n\t\t}\n\t}\n\tif (until == count) { // All items are layed out.\n\t\tif (untilRow == _rows.size()) { // Nothing changed.\n\t\t\treturn until;\n\t\t}\n\n\t\t{\n\t\t\tconst auto rows = _rows.size();\n\t\t\tauto skip = untilCol;\n\t\t\tfor (auto i = untilRow; i < rows; ++i) {\n\t\t\t\tfor (const auto &item : _rows[i].items) {\n\t\t\t\t\tif (skip) {\n\t\t\t\t\t\t--skip;\n\t\t\t\t\t} else {\n\t\t\t\t\t\titem->setPosition(-1);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!untilCol) { // All good rows are filled.\n\t\t\t_rows.resize(untilRow);\n\t\t\treturn until;\n\t\t}\n\t\t_rows.resize(untilRow + 1);\n\t\t_rows[untilRow].items.resize(untilCol);\n\t\t_rows[untilRow].maxWidth = ranges::accumulate(\n\t\t\t_rows[untilRow].items,\n\t\t\t0,\n\t\t\t[](int w, auto &row) { return w + row->maxWidth(); });\n\t\tlayoutRow(_rows[untilRow], _width);\n\t\treturn until;\n\t}\n\tif (untilRow && !untilCol) { // Remove last row, maybe it is not full.\n\t\t--untilRow;\n\t\tuntilCol = _rows[untilRow].items.size();\n\t}\n\tuntil -= untilCol;\n\n\tfor (auto i = untilRow; i < _rows.size(); ++i) {\n\t\tfor (const auto &item : _rows[i].items) {\n\t\t\titem->setPosition(-1);\n\t\t}\n\t}\n\t_rows.resize(untilRow);\n\n\treturn until;\n}\n\nvoid AbstractMosaicLayout::addItem(\n\t\tnot_null<AbstractLayoutItem*> item,\n\t\tRow &row,\n\t\tint &sumWidth) {\n\t// item->preload();\n\n\tusing namespace ::Layout;\n\titem->setPosition(PositionToIndex(_rows.size(), row.items.size()));\n\tif (rowFinalize(row, sumWidth, false)) {\n\t\titem->setPosition(PositionToIndex(_rows.size(), 0));\n\t}\n\n\tsumWidth += item->maxWidth();\n\tif (!row.items.empty() && _rightSkip) {\n\t\tsumWidth += _rightSkip;\n\t}\n\n\trow.items.push_back(item);\n}\n\nbool AbstractMosaicLayout::rowFinalize(Row &row, int &sumWidth, bool force) {\n\tif (row.items.empty()) {\n\t\treturn false;\n\t}\n\n\tconst auto full = (row.items.size() >= kInlineItemsMaxPerRow);\n\t// Currently use the same GIFs layout for all widget sizes.\n\tconst auto big = (sumWidth >= _bigWidth);\n\tif (full || big || force) {\n\t\trow.maxWidth = (full || big) ? sumWidth : 0;\n\t\tlayoutRow(row, _width);\n\t\t_rows.push_back(std::move(row));\n\t\trow = Row();\n\t\trow.items.reserve(kInlineItemsMaxPerRow);\n\t\tsumWidth = 0;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid AbstractMosaicLayout::layoutRow(Row &row, int fullWidth) {\n\tconst auto count = int(row.items.size());\n\tAssert(count <= kInlineItemsMaxPerRow);\n\n\t// Enumerate items in the order of growing maxWidth()\n\t// for that sort item indices by maxWidth().\n\tint indices[kInlineItemsMaxPerRow];\n\tfor (auto i = 0; i != count; ++i) {\n\t\tindices[i] = i;\n\t}\n\tstd::sort(indices, indices + count, [&](int a, int b) {\n\t\treturn row.items[a]->maxWidth() < row.items[b]->maxWidth();\n\t});\n\n\tauto desiredWidth = row.maxWidth;\n\trow.height = 0;\n\tauto availableWidth = fullWidth - _padding.left() - _padding.right();\n\tfor (auto i = 0; i < count; ++i) {\n\t\tconst auto index = indices[i];\n\t\tconst auto &item = row.items[index];\n\t\tconst auto w = desiredWidth\n\t\t\t? (item->maxWidth() * availableWidth / desiredWidth)\n\t\t\t: item->maxWidth();\n\t\tconst auto actualWidth = std::max(w, st::inlineResultsMinWidth);\n\t\trow.height = std::max(\n\t\t\trow.height,\n\t\t\titem->resizeGetHeight(actualWidth));\n\t\tif (desiredWidth) {\n\t\t\tavailableWidth -= actualWidth;\n\t\t\tdesiredWidth -= row.items[index]->maxWidth();\n\t\t\tif (index > 0 && _rightSkip) {\n\t\t\t\tavailableWidth -= _rightSkip;\n\t\t\t\tdesiredWidth -= _rightSkip;\n\t\t\t}\n\t\t}\n\t}\n}\n\n} // namespace Mosaic::Layout\n"
  },
  {
    "path": "Telegram/SourceFiles/layout/layout_mosaic.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"layout/abstract_layout_item.h\"\n#include \"layout/layout_position.h\"\n\nnamespace Mosaic::Layout {\n\nstruct FoundItem {\n\tint index = -1;\n\tbool exact = false;\n\tQPoint relative;\n};\n\nclass AbstractMosaicLayout {\npublic:\n\tAbstractMosaicLayout(int bigWidth);\n\n\t[[nodiscard]] int rowHeightAt(int row) const;\n\t[[nodiscard]] int countDesiredHeight(int newWidth);\n\n\t[[nodiscard]] FoundItem findByPoint(const QPoint &globalPoint) const;\n\n\t[[nodiscard]] QRect findRect(int index) const;\n\n\tvoid setRightSkip(int rightSkip);\n\tvoid setPadding(QMargins padding);\n\tvoid setFullWidth(int w);\n\n\t[[nodiscard]] bool empty() const;\n\t[[nodiscard]] int rowsCount() const;\n\n\tvoid clearRows(bool resultsDeleted);\n\nprotected:\n\tvoid addItems(gsl::span<const not_null<AbstractLayoutItem*>> items);\n\n\t[[nodiscard]] not_null<AbstractLayoutItem*> itemAt(int row, int column) const;\n\t[[nodiscard]] not_null<AbstractLayoutItem*> itemAt(int index) const;\n\n\t[[nodiscard]] AbstractLayoutItem *maybeItemAt(int row, int column) const;\n\t[[nodiscard]] AbstractLayoutItem *maybeItemAt(int index) const;\n\n\tvoid forEach(Fn<void(not_null<const AbstractLayoutItem*>)> callback);\n\n\tvoid paint(\n\t\tFn<void(not_null<AbstractLayoutItem*>, QPoint)> paintItem,\n\t\tconst QRect &clip) const;\n\tint validateExistingRows(\n\t\tFn<bool(not_null<const AbstractLayoutItem*>, int)> checkItem,\n\t\tint count);\n\nprivate:\n\tstatic constexpr auto kInlineItemsMaxPerRow = 5;\n\tstruct Row {\n\t\tint maxWidth = 0;\n\t\tint height = 0;\n\t\tstd::vector<AbstractLayoutItem*> items;\n\t};\n\n\tvoid addItem(not_null<AbstractLayoutItem*> item, Row &row, int &sumWidth);\n\tbool rowFinalize(Row &row, int &sumWidth, bool force);\n\tvoid layoutRow(Row &row, int fullWidth);\n\n\tint _bigWidth;\n\tint _width = 0;\n\tint _rightSkip = 0;\n\tQMargins _padding;\n\tstd::vector<Row> _rows;\n\n};\n\ntemplate <\n\ttypename ItemBase,\n\ttypename = std::enable_if_t<\n\t\tstd::is_base_of_v<AbstractLayoutItem, ItemBase>>>\nclass MosaicLayout final : public AbstractMosaicLayout {\n\tusing Parent = AbstractMosaicLayout;\n\npublic:\n\tusing Parent::Parent;\n\n\tvoid addItems(const std::vector<not_null<ItemBase*>> &items) {\n\t\tParent::addItems({\n\t\t\treinterpret_cast<const not_null<AbstractLayoutItem*>*>(\n\t\t\t\titems.data()),\n\t\t\titems.size() });\n\t}\n\n\t[[nodiscard]] not_null<ItemBase*> itemAt(int row, int column) const {\n\t\treturn Downcast(Parent::itemAt(row, column));\n\t}\n\t[[nodiscard]] not_null<ItemBase*> itemAt(int index) const {\n\t\treturn Downcast(Parent::itemAt(index));\n\t}\n\t[[nodiscard]] ItemBase *maybeItemAt(int row, int column) const {\n\t\treturn Downcast(Parent::maybeItemAt(row, column));\n\t}\n\t[[nodiscard]] ItemBase *maybeItemAt(int index) const {\n\t\treturn Downcast(Parent::maybeItemAt(index));\n\t}\n\n\tvoid forEach(Fn<void(not_null<const ItemBase*>)> callback) {\n\t\tParent::forEach([&](\n\t\t\t\tnot_null<const AbstractLayoutItem*> item) {\n\t\t\tcallback(Downcast(item));\n\t\t});\n\t}\n\n\tvoid paint(\n\t\t\tFn<void(not_null<ItemBase*>, QPoint)> paintItem,\n\t\t\tconst QRect &clip) const {\n\t\tParent::paint([&](\n\t\t\t\tnot_null<AbstractLayoutItem*> item,\n\t\t\t\tQPoint point) {\n\t\t\tpaintItem(Downcast(item), point);\n\t\t}, clip);\n\t}\n\n\tint validateExistingRows(\n\t\t\tFn<bool(not_null<const ItemBase*>, int)> checkItem,\n\t\t\tint count) {\n\t\treturn Parent::validateExistingRows([&](\n\t\t\t\tnot_null<const AbstractLayoutItem*> item,\n\t\t\t\tint until) {\n\t\t\treturn checkItem(Downcast(item), until);\n\t\t}, count);\n\t}\n\nprivate:\n\t[[nodiscard]] static not_null<ItemBase*> Downcast(\n\t\t\tnot_null<AbstractLayoutItem*> item) {\n\t\treturn static_cast<ItemBase*>(item.get());\n\t}\n\t[[nodiscard]] static ItemBase *Downcast(\n\t\t\tAbstractLayoutItem *item) {\n\t\treturn static_cast<ItemBase*>(item);\n\t}\n\t[[nodiscard]] static not_null<const ItemBase*> Downcast(\n\t\t\tnot_null<const AbstractLayoutItem*> item) {\n\t\treturn static_cast<const ItemBase*>(item.get());\n\t}\n\t[[nodiscard]] static const ItemBase *Downcast(\n\t\t\tconst AbstractLayoutItem *item) {\n\t\treturn static_cast<const ItemBase*>(item);\n\t}\n\n};\n\n} // namespace Mosaic::Layout\n"
  },
  {
    "path": "Telegram/SourceFiles/layout/layout_position.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"layout/layout_position.h\"\n\nnamespace Layout {\nnamespace {\n\nconstexpr auto kMatrixRowShift = 40000;\n\n} // namespace\n\nLayout::Position IndexToPosition(int index) {\n\treturn {\n\t\t(index >= 0) ? (index / kMatrixRowShift) : -1,\n\t\t(index >= 0) ? (index % kMatrixRowShift) : -1 };\n}\n\nint PositionToIndex(int row, int column) {\n\treturn row * kMatrixRowShift + column;\n}\n\nint PositionToIndex(const Layout::Position &position) {\n\treturn PositionToIndex(position.row, position.column);\n}\n\n} // namespace Layout\n"
  },
  {
    "path": "Telegram/SourceFiles/layout/layout_position.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Layout {\n\nstruct Position {\n\tint row = -1;\n\tint column = -1;\n};\n\n[[nodiscard]] Position IndexToPosition(int index);\n[[nodiscard]] int PositionToIndex(int row, int column);\n[[nodiscard]] int PositionToIndex(const Position &position);\n\n} // namespace Layout\n"
  },
  {
    "path": "Telegram/SourceFiles/layout/layout_selection.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"layout/layout_selection.h\"\n\nbool IsSubGroupSelection(TextSelection selection) {\n\treturn (selection.from == 0xFFFF) && (selection.to != 0xFFFF);\n}\n\nbool IsGroupItemSelection(\n\t\tTextSelection selection,\n\t\tint index) {\n\tExpects(index >= 0 && index < kMaxGroupSelectionItems);\n\n\treturn IsSubGroupSelection(selection) && (selection.to & (1 << index));\n}\n\nint FirstGroupItemIndex(TextSelection selection) {\n\tif (!IsSubGroupSelection(selection)) {\n\t\treturn -1;\n\t}\n\tfor (auto i = 0; i != kMaxGroupSelectionItems; ++i) {\n\t\tif (selection.to & (1 << i)) {\n\t\t\treturn i;\n\t\t}\n\t}\n\treturn -1;\n}\n\nTextSelection AddGroupItemSelection(\n\t\tTextSelection selection,\n\t\tint index) {\n\tExpects(index >= 0 && index < kMaxGroupSelectionItems);\n\n\tconst auto bit = uint16(1U << index);\n\treturn TextSelection(\n\t\t0xFFFF,\n\t\tIsSubGroupSelection(selection) ? (selection.to | bit) : bit);\n}\n\nTextSelection RemoveGroupItemSelection(\n\t\tTextSelection selection,\n\t\tint index) {\n\tExpects(index >= 0 && index < kMaxGroupSelectionItems);\n\n\tconst auto bit = uint16(1U << index);\n\treturn IsSubGroupSelection(selection)\n\t\t? TextSelection(0xFFFF, selection.to & ~bit)\n\t\t: selection;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/layout/layout_selection.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/text/text.h\"\n\nconstexpr auto FullSelection = TextSelection { 0xFFFF, 0xFFFF };\nconstexpr auto kMaxGroupSelectionItems = 0x0F;\n\n[[nodiscard]] bool IsSubGroupSelection(TextSelection selection);\n\n[[nodiscard]] bool IsGroupItemSelection(\n\t\tTextSelection selection,\n\t\tint index);\n\n[[nodiscard]] int FirstGroupItemIndex(TextSelection selection);\n\n[[nodiscard]] TextSelection AddGroupItemSelection(\n\t\tTextSelection selection,\n\t\tint index);\n\n[[nodiscard]] TextSelection RemoveGroupItemSelection(\n\t\tTextSelection selection,\n\t\tint index);\n"
  },
  {
    "path": "Telegram/SourceFiles/logs.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"logs.h\"\n\n#include \"platform/platform_specific.h\"\n#include \"core/crash_reports.h\"\n#include \"core/launcher.h\"\n#include \"mtproto/facade.h\"\n\nnamespace {\n\nstd::atomic<int> ThreadCounter/* = 0*/;\nthread_local bool WritingEntryFlag/* = false*/;\n\nclass WritingEntryScope final {\npublic:\n\tWritingEntryScope() {\n\t\tWritingEntryFlag = true;\n\t}\n\t~WritingEntryScope() {\n\t\tWritingEntryFlag = false;\n\t}\n};\n\n} // namespace\n\nenum LogDataType {\n\tLogDataMain,\n\tLogDataDebug,\n\tLogDataMtp,\n\n\tLogDataCount\n};\n\nQMutex *_logsMutex(LogDataType type, bool clear = false) {\n\tstatic QMutex *LogsMutexes = 0;\n\tif (clear) {\n\t\tdelete[] LogsMutexes;\n\t\tLogsMutexes = 0;\n\t} else if (!LogsMutexes) {\n\t\tLogsMutexes = new QMutex[LogDataCount];\n\t}\n\treturn &LogsMutexes[type];\n}\n\nQString _logsFilePath(LogDataType type, const QString &postfix = QString()) {\n\tQString path(cWorkingDir());\n\tswitch (type) {\n\tcase LogDataMain: path += u\"log\"_q + postfix + u\".txt\"_q; break;\n\tcase LogDataDebug: path += u\"DebugLogs/log\"_q + postfix + u\".txt\"_q; break;\n\tcase LogDataMtp: path += u\"DebugLogs/mtp\"_q + postfix + u\".txt\"_q; break;\n\t}\n\treturn path;\n}\n\nint32 LogsStartIndexChosen = -1;\nQString _logsEntryStart() {\n\tstatic thread_local auto threadId = ThreadCounter++;\n\tstatic auto index = 0;\n\n\tconst auto tm = QDateTime::currentDateTime();\n\n\treturn QString(\"[%1 %2-%3]\").arg(tm.toString(\"hh:mm:ss.zzz\"), QString(\"%1\").arg(threadId, 2, 10, QChar('0'))).arg(++index, 7, 10, QChar('0'));\n}\n\nclass LogsDataFields {\npublic:\n\n\tLogsDataFields() {\n\t\tfor (int32 i = 0; i < LogDataCount; ++i) {\n\t\t\tfiles[i].reset(new QFile());\n\t\t}\n\t}\n\n\tbool openMain() {\n\t\treturn reopen(LogDataMain, 0, u\"start\"_q);\n\t}\n\n\tvoid closeMain() {\n\t\tQMutexLocker lock(_logsMutex(LogDataMain));\n\t\tWritingEntryScope scope;\n\n\t\tconst auto file = files[LogDataMain].get();\n\t\tif (file && file->isOpen()) {\n\t\t\tfile->close();\n\t\t}\n\t}\n\n\tbool instanceChecked() {\n\t\treturn reopen(LogDataMain, 0, QString());\n\t}\n\n\tQString full() {\n\t\tconst auto file = files[LogDataMain].get();\n\t\tif (!file || !file->isOpen()) {\n\t\t\treturn QString();\n\t\t}\n\n\t\tQFile out(file->fileName());\n\t\tif (out.open(QIODevice::ReadOnly)) {\n\t\t\treturn QString::fromUtf8(out.readAll());\n\t\t}\n\t\treturn QString();\n\t}\n\n\tvoid write(LogDataType type, const QString &msg) {\n\t\tQMutexLocker lock(_logsMutex(type));\n\t\tWritingEntryScope scope;\n\n\t\tif (type != LogDataMain) {\n\t\t\treopenDebug();\n\t\t}\n\t\tconst auto file = files[type].get();\n\t\tif (!file || !file->isOpen()) {\n\t\t\treturn;\n\t\t}\n\t\tfile->write(msg.toUtf8());\n\t\tfile->flush();\n\t}\n\nprivate:\n\tstd::unique_ptr<QFile> files[LogDataCount];\n\n\tint32 part = -1;\n\n\tbool reopen(LogDataType type, int32 dayIndex, const QString &postfix) {\n\t\tif (files[type] && files[type]->isOpen()) {\n\t\t\tif (type == LogDataMain) {\n\t\t\t\tif (!postfix.isEmpty()) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfiles[type]->close();\n\t\t\t}\n\t\t}\n\n\t\tauto mode = QIODevice::WriteOnly | QIODevice::Text;\n\t\tif (type == LogDataMain) { // we can call LOG() in LogDataMain reopen - mutex not locked\n\t\t\tif (postfix.isEmpty()) { // instance checked, need to move to log.txt\n\t\t\t\tAssert(!files[type]->fileName().isEmpty()); // one of log_startXX.txt should've been opened already\n\n\t\t\t\tconst auto startName = files[type]->fileName();\n\t\t\t\tconst auto targetName = _logsFilePath(type, postfix);\n\n\t\t\t\tauto target = QFile(targetName);\n\t\t\t\tif (target.exists() && !target.remove()) {\n\t\t\t\t\tLOG((\"Could not delete '%1' file to start new logging: %2\").arg(targetName, target.errorString()));\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tfiles[type]->close();\n\n\t\t\t\tconst auto reopenStart = [&](const QString &name) {\n\t\t\t\t\tfiles[type]->setFileName(name);\n\t\t\t\t\treturn files[type]->open(mode | QIODevice::Append);\n\t\t\t\t};\n\n\t\t\t\tauto source = QFile(startName);\n\t\t\t\tif (!source.rename(targetName)) {\n\t\t\t\t\tif (reopenStart(startName)) {\n\t\t\t\t\t\tLOG((\"Could not rename '%1' to '%2' to start new logging: %3\").arg(startName, targetName, source.errorString()));\n\t\t\t\t\t}\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif (!reopenStart(targetName)) {\n\t\t\t\t\tLOG((\"Could not open '%1' file to start new logging: %2\").arg(targetName, files[type]->errorString()));\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tLOG((\"Moved logging from '%1' to '%2'!\").arg(startName, files[type]->fileName()));\n\n\t\t\t\tLogsStartIndexChosen = -1;\n\n\t\t\t\tQDir working(cWorkingDir()); // delete all other log_startXX.txt that we can\n\t\t\t\tQStringList oldlogs = working.entryList(QStringList(\"log_start*.txt\"), QDir::Files);\n\t\t\t\tfor (QStringList::const_iterator i = oldlogs.cbegin(), e = oldlogs.cend(); i != e; ++i) {\n\t\t\t\t\tQString oldlog = cWorkingDir() + *i, oldlogend = i->mid(u\"log_start\"_q.size());\n\t\t\t\t\tif (oldlogend.size() == 1 + u\".txt\"_q.size() && oldlogend.at(0).isDigit() && base::StringViewMid(oldlogend, 1) == u\".txt\"_q) {\n\t\t\t\t\t\tbool removed = QFile(oldlog).remove();\n\t\t\t\t\t\tLOG((\"Old start log '%1' found, deleted: %2\").arg(*i, Logs::b(removed)));\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\t\t\t} else {\n\t\t\t\tbool found = false;\n\t\t\t\tint32 oldest = -1; // find not existing log_startX.txt or pick the oldest one (by lastModified)\n\t\t\t\tQDateTime oldestLastModified;\n\t\t\t\tfor (int32 i = 0; i < 10; ++i) {\n\t\t\t\t\tQString trying = _logsFilePath(type, u\"_start%1\"_q.arg(i));\n\t\t\t\t\tfiles[type]->setFileName(trying);\n\t\t\t\t\tif (!files[type]->exists()) {\n\t\t\t\t\t\tLogsStartIndexChosen = i;\n\t\t\t\t\t\tfound = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tQDateTime lastModified = QFileInfo(trying).lastModified();\n\t\t\t\t\tif (oldest < 0 || lastModified < oldestLastModified) {\n\t\t\t\t\t\toldestLastModified = lastModified;\n\t\t\t\t\t\toldest = i;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!found) {\n\t\t\t\t\tfiles[type]->setFileName(_logsFilePath(type, u\"_start%1\"_q.arg(oldest)));\n\t\t\t\t\tLogsStartIndexChosen = oldest;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfiles[type]->setFileName(_logsFilePath(type, postfix));\n\t\t\tif (files[type]->exists()) {\n\t\t\t\tif (files[type]->open(QIODevice::ReadOnly | QIODevice::Text)) {\n\t\t\t\t\tif (QString::fromUtf8(files[type]->readLine()).toInt() == dayIndex) {\n\t\t\t\t\t\tmode |= QIODevice::Append;\n\t\t\t\t\t}\n\t\t\t\t\tfiles[type]->close();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tQDir().mkdir(cWorkingDir() + u\"DebugLogs\"_q);\n\t\t\t}\n\t\t}\n\t\tif (files[type]->open(mode)) {\n\t\t\tif (type != LogDataMain) {\n\t\t\t\tfiles[type]->write(((mode & QIODevice::Append)\n\t\t\t\t\t? qsl(\"\\\n----------------------------------------------------------------\\n\\\nNEW LOGGING INSTANCE STARTED!!!\\n\\\n----------------------------------------------------------------\\n\")\n\t\t\t\t\t: qsl(\"%1\\n\").arg(dayIndex)).toUtf8());\n\t\t\t\tfiles[type]->flush();\n\t\t\t}\n\n\t\t\treturn true;\n\t\t} else if (type != LogDataMain) {\n\t\t\tLOG((\"Could not open debug log '%1': %2\").arg(files[type]->fileName(), files[type]->errorString()));\n\t\t}\n\t\treturn false;\n\t}\n\n\tvoid reopenDebug() {\n\t\ttime_t t = time(NULL);\n\t\tstruct tm tm;\n\t\tmylocaltime(&tm, &t);\n\n\t\tstatic const int switchEach = 15; // minutes\n\t\tint32 newPart = (tm.tm_min + tm.tm_hour * 60) / switchEach;\n\t\tif (newPart == part) return;\n\n\t\tpart = newPart;\n\n\t\tint32 dayIndex = (tm.tm_year + 1900) * 10000 + (tm.tm_mon + 1) * 100 + tm.tm_mday;\n\t\tQString postfix = QString(\"_%4_%5\").arg((part * switchEach) / 60, 2, 10, QChar('0')).arg((part * switchEach) % 60, 2, 10, QChar('0'));\n\n\t\treopen(LogDataDebug, dayIndex, postfix);\n\t\treopen(LogDataMtp, dayIndex, postfix);\n\t}\n\n};\n\nLogsDataFields *LogsData = 0;\n\nusing LogsInMemoryList = QList<QPair<LogDataType, QString>>;\nLogsInMemoryList *LogsInMemory = 0;\nLogsInMemoryList *DeletedLogsInMemory = SharedMemoryLocation<LogsInMemoryList, 0>();\n\nQString LogsBeforeSingleInstanceChecked; // LogsInMemory already dumped in LogsData, but LogsData is about to be deleted\n\nvoid _logsWrite(LogDataType type, const QString &msg) {\n\tif (LogsData && (type == LogDataMain || LogsStartIndexChosen < 0)) {\n\t\tif (type == LogDataMain || Logs::DebugEnabled()) {\n\t\t\tLogsData->write(type, msg);\n\t\t}\n\t} else if (LogsInMemory != DeletedLogsInMemory) {\n\t\tif (!LogsInMemory) {\n\t\t\tLogsInMemory = new LogsInMemoryList;\n\t\t}\n\t\tLogsInMemory->push_back(qMakePair(type, msg));\n\t} else if (!LogsBeforeSingleInstanceChecked.isEmpty() && type == LogDataMain) {\n\t\tLogsBeforeSingleInstanceChecked += msg;\n\t}\n}\n\nnamespace Logs {\nnamespace {\n\nbool DebugModeEnabled = false;\n\n[[maybe_unused]] void MoveOldDataFiles(const QString &wasDir) {\n\tif (wasDir.isEmpty()) {\n\t\treturn;\n\t}\n\tQFile data(wasDir + \"data\"), dataConfig(wasDir + \"data_config\"), tdataConfig(wasDir + \"tdata/config\");\n\tif (data.exists() && dataConfig.exists() && !QFileInfo::exists(cWorkingDir() + \"data\") && !QFileInfo::exists(cWorkingDir() + \"data_config\")) { // move to home dir\n\t\tLOG((\"Copying data to home dir '%1' from '%2'\").arg(cWorkingDir(), wasDir));\n\t\tif (data.copy(cWorkingDir() + \"data\")) {\n\t\t\tLOG((\"Copied 'data' to home dir\"));\n\t\t\tif (dataConfig.copy(cWorkingDir() + \"data_config\")) {\n\t\t\t\tLOG((\"Copied 'data_config' to home dir\"));\n\t\t\t\tbool tdataGood = true;\n\t\t\t\tif (tdataConfig.exists()) {\n\t\t\t\t\ttdataGood = false;\n\t\t\t\t\tQDir().mkpath(cWorkingDir() + \"tdata\");\n\t\t\t\t\tif (tdataConfig.copy(cWorkingDir() + \"tdata/config\")) {\n\t\t\t\t\t\tLOG((\"Copied 'tdata/config' to home dir\"));\n\t\t\t\t\t\ttdataGood = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tLOG((\"Copied 'data' and 'data_config', but could not copy 'tdata/config'!\"));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (tdataGood) {\n\t\t\t\t\tif (data.remove()) {\n\t\t\t\t\t\tLOG((\"Removed 'data'\"));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tLOG((\"Could not remove 'data'\"));\n\t\t\t\t\t}\n\t\t\t\t\tif (dataConfig.remove()) {\n\t\t\t\t\t\tLOG((\"Removed 'data_config'\"));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tLOG((\"Could not remove 'data_config'\"));\n\t\t\t\t\t}\n\t\t\t\t\tif (!tdataConfig.exists() || tdataConfig.remove()) {\n\t\t\t\t\t\tLOG((\"Removed 'tdata/config'\"));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tLOG((\"Could not remove 'tdata/config'\"));\n\t\t\t\t\t}\n\t\t\t\t\tQDir().rmdir(wasDir + \"tdata\");\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tLOG((\"Copied 'data', but could not copy 'data_config'!!\"));\n\t\t\t}\n\t\t} else {\n\t\t\tLOG((\"Could not copy 'data'!\"));\n\t\t}\n\t}\n}\n\n} // namespace\n\nvoid SetDebugEnabled(bool enabled) {\n\tDebugModeEnabled = enabled;\n}\n\nbool DebugEnabled() {\n\treturn DebugModeEnabled;\n}\n\nbool WritingEntry() {\n\treturn WritingEntryFlag;\n}\n\nvoid start() {\n\tAssert(LogsData == nullptr);\n\n\tauto &launcher = Core::Launcher::Instance();\n\tif (!launcher.checkPortableVersionFolder()) {\n\t\treturn;\n\t}\n\n\tLogsData = new LogsDataFields();\n\tif (cWorkingDir().isEmpty()) {\n#if (!defined Q_OS_WIN && !defined _DEBUG) || defined Q_OS_WINRT || defined OS_WIN_STORE || defined OS_MAC_STORE\n\t\tcForceWorkingDir(psAppDataPath());\n#else // (!Q_OS_WIN && !_DEBUG) || Q_OS_WINRT || OS_WIN_STORE || OS_MAC_STORE\n\t\tcForceWorkingDir(cExeDir());\n\t\tif (!LogsData->openMain()) {\n\t\t\tcForceWorkingDir(psAppDataPath());\n\t\t}\n#endif // (!Q_OS_WIN && !_DEBUG) || Q_OS_WINRT || OS_WIN_STORE || OS_MAC_STORE\n\t}\n\n\tif (launcher.validateCustomWorkingDir()) {\n\t\tdelete LogsData;\n\t\tLogsData = new LogsDataFields();\n\t}\n\n// WinRT build requires the working dir to stay the same for plugin loading.\n#ifndef Q_OS_WINRT\n\tQDir::setCurrent(cWorkingDir());\n#endif // !Q_OS_WINRT\n\n\tQDir().mkpath(cWorkingDir() + u\"tdata\"_q);\n\n\tlauncher.workingFolderReady();\n\tCrashReports::StartCatching();\n\n\tif (!LogsData->openMain()) {\n\t\tdelete LogsData;\n\t\tLogsData = nullptr;\n\t}\n\n\tLOG((\"Launched version: %1, install beta: %2, alpha: %3, debug mode: %4\"\n\t\t).arg(AppVersion\n\t\t).arg(Logs::b(cInstallBetaVersion())\n\t\t).arg(cAlphaVersion()\n\t\t).arg(Logs::b(DebugEnabled())));\n\tLOG((\"Executable dir: %1, name: %2\").arg(cExeDir(), cExeName()));\n\tLOG((\"Initial working dir: %1\").arg(launcher.initialWorkingDir()));\n\tLOG((\"Working dir: %1\").arg(cWorkingDir()));\n\tLOG((\"Command line: %1\").arg(launcher.arguments().join(' ')));\n\n\tif (!LogsData) {\n\t\tLOG((\"FATAL: Could not open '%1' for writing log!\"\n\t\t\t).arg(_logsFilePath(LogDataMain, u\"_startXX\"_q)));\n\t\treturn;\n\t}\n\n#ifdef Q_OS_WIN\n\tif (cWorkingDir() == psAppDataPath()) { // fix old \"Telegram Win (Unofficial)\" version\n\t\tMoveOldDataFiles(psAppDataPathOld());\n\t}\n#elif !defined Q_OS_MAC && !defined _DEBUG // fix first version\n\tMoveOldDataFiles(launcher.initialWorkingDir());\n#endif\n\n\tif (LogsInMemory) {\n\t\tAssert(LogsInMemory != DeletedLogsInMemory);\n\t\tLogsInMemoryList list = *LogsInMemory;\n\t\tfor (LogsInMemoryList::const_iterator i = list.cbegin(), e = list.cend(); i != e; ++i) {\n\t\t\tif (i->first == LogDataMain) {\n\t\t\t\t_logsWrite(i->first, i->second);\n\t\t\t}\n\t\t}\n\t}\n\n\tLOG((\"Logs started\"));\n}\n\nvoid finish() {\n\tdelete LogsData;\n\tLogsData = 0;\n\n\tif (LogsInMemory && LogsInMemory != DeletedLogsInMemory) {\n\t\tdelete LogsInMemory;\n\t}\n\tLogsInMemory = DeletedLogsInMemory;\n\n\t_logsMutex(LogDataMain, true);\n\n\tCrashReports::FinishCatching();\n}\n\nbool started() {\n\treturn LogsData != 0;\n}\n\nbool instanceChecked() {\n\tif (!LogsData) return false;\n\n\tif (!LogsData->instanceChecked()) {\n\t\tLogsBeforeSingleInstanceChecked = Logs::full();\n\n\t\tdelete LogsData;\n\t\tLogsData = 0;\n\t\tLOG((\"FATAL: Could not move logging to '%1'!\").arg(_logsFilePath(LogDataMain)));\n\t\treturn false;\n\t}\n\n\tif (LogsInMemory) {\n\t\tAssert(LogsInMemory != DeletedLogsInMemory);\n\t\tLogsInMemoryList list = *LogsInMemory;\n\t\tfor (LogsInMemoryList::const_iterator i = list.cbegin(), e = list.cend(); i != e; ++i) {\n\t\t\tif (i->first != LogDataMain) {\n\t\t\t\t_logsWrite(i->first, i->second);\n\t\t\t}\n\t\t}\n\t}\n\tif (LogsInMemory) {\n\t\tAssert(LogsInMemory != DeletedLogsInMemory);\n\t\tdelete LogsInMemory;\n\t}\n\tLogsInMemory = DeletedLogsInMemory;\n\n\tDEBUG_LOG((\"Debug logs started.\"));\n\tLogsBeforeSingleInstanceChecked.clear();\n\treturn true;\n}\n\nvoid multipleInstances() {\n\tif (LogsInMemory) {\n\t\tAssert(LogsInMemory != DeletedLogsInMemory);\n\t\tdelete LogsInMemory;\n\t}\n\tLogsInMemory = DeletedLogsInMemory;\n\n\tif (Logs::DebugEnabled()) {\n\t\tLOG((\"WARNING: debug logs are not written in multiple instances mode!\"));\n\t}\n\tLogsBeforeSingleInstanceChecked.clear();\n}\n\nvoid closeMain() {\n\tLOG((\"Explicitly closing main log and finishing crash handlers.\"));\n\tif (LogsData) {\n\t\tLogsData->closeMain();\n\t}\n}\n\nvoid writeMain(const QString &v) {\n\ttime_t t = time(NULL);\n\tstruct tm tm;\n\tmylocaltime(&tm, &t);\n\n\tconst auto msg = QString(\"[%1.%2.%3 %4:%5:%6] %7\\n\"\n\t).arg(tm.tm_year + 1900\n\t).arg(tm.tm_mon + 1, 2, 10, QChar('0')\n\t).arg(tm.tm_mday, 2, 10, QChar('0')\n\t).arg(tm.tm_hour, 2, 10, QChar('0')\n\t).arg(tm.tm_min, 2, 10, QChar('0')\n\t).arg(tm.tm_sec, 2, 10, QChar('0')\n\t).arg(v);\n\t_logsWrite(LogDataMain, msg);\n\n\twriteDebug(v);\n}\n\nvoid writeDebug(const QString &v) {\n\tconst auto msg = QString(\"%1 %2\\n\").arg(_logsEntryStart(), v);\n\t_logsWrite(LogDataDebug, msg);\n\n#ifdef Q_OS_WIN\n\t//OutputDebugString(reinterpret_cast<const wchar_t *>(msg.utf16()));\n#elif defined Q_OS_MAC\n\t//objc_outputDebugString(msg);\n#elif defined _DEBUG\n\t//std::cout << msg.toUtf8().constData();\n#endif\n}\n\nvoid writeMtp(int32 dc, const QString &v) {\n\tconst auto expanded = [&] {\n\t\tconst auto bare = MTP::isTemporaryDcId(dc)\n\t\t\t? MTP::getRealIdFromTemporaryDcId(dc)\n\t\t\t: MTP::BareDcId(dc);\n\t\tconst auto base = (MTP::isTemporaryDcId(dc) ? \"temporary_\" : \"\")\n\t\t\t+ QString::number(bare);\n\t\tconst auto shift = MTP::GetDcIdShift(dc);\n\t\tif (shift == 0) {\n\t\t\treturn base + \"_main\";\n\t\t} else if (shift == MTP::kExportDcShift) {\n\t\t\treturn base + \"_export\";\n\t\t} else if (shift == MTP::kExportMediaDcShift) {\n\t\t\treturn base + \"_export_download\";\n\t\t} else if (shift == MTP::kConfigDcShift) {\n\t\t\treturn base + \"_config_enumeration\";\n\t\t} else if (shift == MTP::kLogoutDcShift) {\n\t\t\treturn base + \"_logout_guest\";\n\t\t} else if (shift == MTP::kUpdaterDcShift) {\n\t\t\treturn base + \"_download_update\";\n\t\t} else if (shift == MTP::kGroupCallStreamDcShift) {\n\t\t\treturn base + \"_stream\";\n\t\t} else if (MTP::isDownloadDcId(dc)) {\n\t\t\tconst auto index = shift - MTP::kBaseDownloadDcShift;\n\t\t\treturn base + \"_download\" + QString::number(index);\n\t\t} else if (MTP::isUploadDcId(dc)) {\n\t\t\tconst auto index = shift - MTP::kBaseUploadDcShift;\n\t\t\treturn base + \"_upload\" + QString::number(index);\n\t\t} else if (shift >= MTP::kDestroyKeyStartDcShift) {\n\t\t\tconst auto index = shift - MTP::kDestroyKeyStartDcShift;\n\t\t\treturn base + \"_key_destroyer\" + QString::number(index);\n\t\t}\n\t\treturn base + \"_unknown\" + QString::number(shift);\n\t}();\n\tconst auto msg = _logsEntryStart()\n\t\t+ u\" (dc:%1) \"_q.arg(expanded)\n\t\t+ v\n\t\t+ '\\n';\n\t_logsWrite(LogDataMtp, msg);\n}\n\nQString full() {\n\tif (LogsData) {\n\t\treturn LogsData->full();\n\t}\n\tif (!LogsInMemory || LogsInMemory == DeletedLogsInMemory) {\n\t\treturn LogsBeforeSingleInstanceChecked;\n\t}\n\n\tint32 size = LogsBeforeSingleInstanceChecked.size();\n\tfor (LogsInMemoryList::const_iterator i = LogsInMemory->cbegin(), e = LogsInMemory->cend(); i != e; ++i) {\n\t\tif (i->first == LogDataMain) {\n\t\t\tsize += i->second.size();\n\t\t}\n\t}\n\tQString result;\n\tresult.reserve(size);\n\tif (!LogsBeforeSingleInstanceChecked.isEmpty()) {\n\t\tresult.append(LogsBeforeSingleInstanceChecked);\n\t}\n\tfor (LogsInMemoryList::const_iterator i = LogsInMemory->cbegin(), e = LogsInMemory->cend(); i != e; ++i) {\n\t\tif (i->first == LogDataMain) {\n\t\t\tresult += i->second;\n\t\t}\n\t}\n\treturn result;\n}\n\n} // namespace Logs\n"
  },
  {
    "path": "Telegram/SourceFiles/logs.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/basic_types.h\"\n#include \"base/assertion.h\"\n#include \"base/debug_log.h\"\n\nnamespace Logs {\n\nvoid SetDebugEnabled(bool enabled);\nbool DebugEnabled();\n[[nodiscard]] bool WritingEntry();\n\nvoid start();\nbool started();\nvoid finish();\n\nbool instanceChecked();\nvoid multipleInstances();\n\nvoid closeMain();\n\nvoid writeMain(const QString &v);\nvoid writeDebug(const QString &v);\nvoid writeMtp(int32 dc, const QString &v);\n\nQString full();\n\ninline const char *b(bool v) {\n\treturn v ? \"[TRUE]\" : \"[FALSE]\";\n}\n\nstruct MemoryBuffer {\n\tMemoryBuffer(const void *ptr, uint32 size) : p(ptr), s(size) {\n\t}\n\tQString str() const {\n\t\tQString result;\n\t\tconst uchar *buf((const uchar*)p);\n\t\tconst char *hex = \"0123456789ABCDEF\";\n\t\tresult.reserve(s * 3);\n\t\tfor (uint32 i = 0; i < s; ++i) {\n\t\t\tresult += hex[(buf[i] >> 4)];\n\t\t\tresult += hex[buf[i] & 0x0F];\n\t\t\tresult += ' ';\n\t\t}\n\t\tresult.chop(1);\n\t\treturn result;\n\t}\n\n\tconst void *p;\n\tuint32 s;\n\n};\n\ninline MemoryBuffer mb(const void *ptr, uint32 size) {\n\treturn MemoryBuffer(ptr, size);\n}\n\n} // namespace Logs\n\n#define MTP_LOG(dc, msg) {\\\n\tif (Logs::DebugEnabled() || !Logs::started()) {\\\n\t\tLogs::writeMtp(dc, QString msg);\\\n\t}\\\n}\n//usage MTP_LOG(dc, (\"log: %1 %2\").arg(1).arg(2))\n"
  },
  {
    "path": "Telegram/SourceFiles/main/main_account.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"main/main_account.h\"\n\n#include \"base/platform/base_platform_info.h\"\n#include \"core/application.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/storage_domain.h\" // Storage::StartResult.\n#include \"storage/serialize_common.h\"\n#include \"storage/serialize_peer.h\"\n#include \"storage/localstorage.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_changes.h\"\n#include \"window/window_controller.h\"\n#include \"media/audio/media_audio.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"mainwidget.h\"\n#include \"api/api_updates.h\"\n#include \"ui/ui_utility.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session_settings.h\"\n\nnamespace Main {\nnamespace {\n\nconstexpr auto kWideIdsTag = ~uint64(0);\n\n[[nodiscard]] QString ComposeDataString(const QString &dataName, int index) {\n\tauto result = dataName;\n\tresult.replace('#', QString());\n\tif (index > 0) {\n\t\tresult += '#' + QString::number(index + 1);\n\t}\n\treturn result;\n}\n\n} // namespace\n\nAccount::Account(not_null<Domain*> domain, const QString &dataName, int index)\n: _domain(domain)\n, _local(std::make_unique<Storage::Account>(\n\tthis,\n\tComposeDataString(dataName, index))) {\n}\n\nAccount::~Account() {\n\tif (const auto session = maybeSession()) {\n\t\tsession->saveSettingsNowIfNeeded();\n\t\t_local->writeSearchSuggestionsIfNeeded();\n\t}\n\tdestroySession(DestroyReason::Quitting);\n}\n\nStorage::Domain &Account::domainLocal() const {\n\treturn _domain->local();\n}\n\n[[nodiscard]] Storage::StartResult Account::legacyStart(\n\t\tconst QByteArray &passcode) {\n\tExpects(!_appConfig);\n\n\treturn _local->legacyStart(passcode);\n}\n\nstd::unique_ptr<MTP::Config> Account::prepareToStart(\n\t\tstd::shared_ptr<MTP::AuthKey> localKey) {\n\treturn _local->start(std::move(localKey));\n}\n\nvoid Account::start(std::unique_ptr<MTP::Config> config) {\n\t_appConfig = std::make_unique<AppConfig>(this);\n\tstartMtp(config\n\t\t? std::move(config)\n\t\t: std::make_unique<MTP::Config>(\n\t\t\tCore::App().fallbackProductionConfig()));\n\t_appConfig->start();\n\twatchProxyChanges();\n\twatchSessionChanges();\n}\n\nvoid Account::prepareToStartAdded(\n\t\tstd::shared_ptr<MTP::AuthKey> localKey) {\n\t_local->startAdded(std::move(localKey));\n}\n\nvoid Account::watchProxyChanges() {\n\tusing ProxyChange = Core::Application::ProxyChange;\n\n\tCore::App().proxyChanges(\n\t) | rpl::on_next([=](const ProxyChange &change) {\n\t\tconst auto key = [&](const MTP::ProxyData &proxy) {\n\t\t\treturn (proxy.type == MTP::ProxyData::Type::Mtproto)\n\t\t\t\t? std::make_pair(proxy.host, proxy.port)\n\t\t\t\t: std::make_pair(QString(), uint32(0));\n\t\t};\n\t\tif (_mtp) {\n\t\t\t_mtp->restart();\n\t\t\tif (key(change.was) != key(change.now)) {\n\t\t\t\t_mtp->reInitConnection(_mtp->mainDcId());\n\t\t\t}\n\t\t}\n\t\tif (_mtpForKeysDestroy) {\n\t\t\t_mtpForKeysDestroy->restart();\n\t\t}\n\t}, _lifetime);\n}\n\nvoid Account::watchSessionChanges() {\n\tsessionChanges(\n\t) | rpl::on_next([=](Session *session) {\n\t\tif (!session && _mtp) {\n\t\t\t_mtp->setUserPhone(QString());\n\t\t}\n\t}, _lifetime);\n}\n\nuint64 Account::willHaveSessionUniqueId(MTP::Config *config) const {\n\t// See also Session::uniqueId.\n\tif (!_sessionUserId) {\n\t\treturn 0;\n\t}\n\treturn _sessionUserId.bare\n\t\t| (config && config->isTestMode() ? 0x0100'0000'0000'0000ULL : 0ULL);\n}\n\nvoid Account::createSession(\n\t\tconst MTPUser &user,\n\t\tstd::unique_ptr<SessionSettings> settings) {\n\tcreateSession(\n\t\tuser,\n\t\tQByteArray(),\n\t\t0,\n\t\tsettings ? std::move(settings) : std::make_unique<SessionSettings>());\n}\n\nvoid Account::createSession(\n\t\tUserId id,\n\t\tQByteArray serialized,\n\t\tint streamVersion,\n\t\tstd::unique_ptr<SessionSettings> settings) {\n\tDEBUG_LOG((\"sessionUserSerialized.size: %1\").arg(serialized.size()));\n\tQDataStream peekStream(serialized);\n\tconst auto phone = Serialize::peekUserPhone(streamVersion, peekStream);\n\tconst auto flags = MTPDuser::Flag::f_self | (phone.isEmpty()\n\t\t? MTPDuser::Flag()\n\t\t: MTPDuser::Flag::f_phone);\n\n\tcreateSession(\n\t\tMTP_user(\n\t\t\tMTP_flags(flags),\n\t\t\tMTP_long(base::take(_sessionUserId).bare),\n\t\t\tMTPlong(), // access_hash\n\t\t\tMTPstring(), // first_name\n\t\t\tMTPstring(), // last_name\n\t\t\tMTPstring(), // username\n\t\t\tMTP_string(phone),\n\t\t\tMTPUserProfilePhoto(),\n\t\t\tMTPUserStatus(),\n\t\t\tMTPint(), // bot_info_version\n\t\t\tMTPVector<MTPRestrictionReason>(),\n\t\t\tMTPstring(), // bot_inline_placeholder\n\t\t\tMTPstring(), // lang_code\n\t\t\tMTPEmojiStatus(),\n\t\t\tMTPVector<MTPUsername>(),\n\t\t\tMTPRecentStory(),\n\t\t\tMTPPeerColor(), // color\n\t\t\tMTPPeerColor(), // profile_color\n\t\t\tMTPint(), // bot_active_users\n\t\t\tMTPlong(), // bot_verification_icon\n\t\t\tMTPlong()), // send_paid_messages_stars\n\t\tserialized,\n\t\tstreamVersion,\n\t\tstd::move(settings));\n}\n\nvoid Account::createSession(\n\t\tconst MTPUser &user,\n\t\tQByteArray serialized,\n\t\tint streamVersion,\n\t\tstd::unique_ptr<SessionSettings> settings) {\n\tExpects(_mtp != nullptr);\n\tExpects(_session == nullptr);\n\tExpects(_sessionValue.current() == nullptr);\n\n\t_session = std::make_unique<Session>(this, user, std::move(settings));\n\tif (!serialized.isEmpty()) {\n\t\tlocal().readSelf(_session.get(), serialized, streamVersion);\n\t}\n\t_sessionValue = _session.get();\n\n\tEnsures(_session != nullptr);\n}\n\nvoid Account::destroySession(DestroyReason reason) {\n\t_storedSessionSettings.reset();\n\t_sessionUserId = 0;\n\t_sessionUserSerialized = {};\n\tif (!sessionExists()) {\n\t\treturn;\n\t}\n\n\t_sessionValue = nullptr;\n\n\tif (reason == DestroyReason::LoggedOut) {\n\t\t_session->finishLogout();\n\t}\n\t_session = nullptr;\n}\n\nbool Account::sessionExists() const {\n\treturn (_sessionValue.current() != nullptr);\n}\n\nSession &Account::session() const {\n\tExpects(sessionExists());\n\n\treturn *_sessionValue.current();\n}\n\nSession *Account::maybeSession() const {\n\treturn _sessionValue.current();\n}\n\nrpl::producer<Session*> Account::sessionValue() const {\n\treturn _sessionValue.value();\n}\n\nrpl::producer<Session*> Account::sessionChanges() const {\n\treturn _sessionValue.changes();\n}\n\nrpl::producer<not_null<MTP::Instance*>> Account::mtpValue() const {\n\treturn _mtpValue.value() | rpl::map([](MTP::Instance *instance) {\n\t\treturn not_null{ instance };\n\t});\n}\n\nrpl::producer<not_null<MTP::Instance*>> Account::mtpMainSessionValue() const {\n\treturn mtpValue() | rpl::map([=](not_null<MTP::Instance*> instance) {\n\t\treturn instance->mainDcIdValue() | rpl::map_to(instance);\n\t}) | rpl::flatten_latest();\n}\n\nrpl::producer<MTPUpdates> Account::mtpUpdates() const {\n\treturn _mtpUpdates.events();\n}\n\nrpl::producer<> Account::mtpNewSessionCreated() const {\n\treturn _mtpNewSessionCreated.events();\n}\n\nvoid Account::setMtpMainDcId(MTP::DcId mainDcId) {\n\tExpects(!_mtp);\n\n\t_mtpFields.mainDcId = mainDcId;\n}\n\nvoid Account::setLegacyMtpKey(std::shared_ptr<MTP::AuthKey> key) {\n\tExpects(!_mtp);\n\tExpects(key != nullptr);\n\n\t_mtpFields.keys.push_back(std::move(key));\n}\n\nQByteArray Account::serializeMtpAuthorization() const {\n\tconst auto serialize = [&](\n\t\t\tMTP::DcId mainDcId,\n\t\t\tconst MTP::AuthKeysList &keys,\n\t\t\tconst MTP::AuthKeysList &keysToDestroy) {\n\t\tconst auto keysSize = [](auto &list) {\n\t\t\tconst auto keyDataSize = MTP::AuthKey::Data().size();\n\t\t\treturn sizeof(qint32)\n\t\t\t\t+ list.size() * (sizeof(qint32) + keyDataSize);\n\t\t};\n\t\tconst auto writeKeys = [](\n\t\t\t\tQDataStream &stream,\n\t\t\t\tconst MTP::AuthKeysList &keys) {\n\t\t\tstream << qint32(keys.size());\n\t\t\tfor (const auto &key : keys) {\n\t\t\t\tstream << qint32(key->dcId());\n\t\t\t\tkey->write(stream);\n\t\t\t}\n\t\t};\n\n\t\tauto result = QByteArray();\n\t\t// wide tag + userId + mainDcId\n\t\tauto size = 2 * sizeof(quint64) + sizeof(qint32);\n\t\tsize += keysSize(keys) + keysSize(keysToDestroy);\n\t\tresult.reserve(size);\n\t\t{\n\t\t\tQDataStream stream(&result, QIODevice::WriteOnly);\n\t\t\tstream.setVersion(QDataStream::Qt_5_1);\n\n\t\t\tconst auto currentUserId = sessionExists()\n\t\t\t\t? session().userId()\n\t\t\t\t: UserId();\n\t\t\tstream\n\t\t\t\t<< quint64(kWideIdsTag)\n\t\t\t\t<< quint64(currentUserId.bare)\n\t\t\t\t<< qint32(mainDcId);\n\t\t\twriteKeys(stream, keys);\n\t\t\twriteKeys(stream, keysToDestroy);\n\n\t\t\tDEBUG_LOG((\"MTP Info: Keys written, userId: %1, dcId: %2\"\n\t\t\t\t).arg(currentUserId.bare\n\t\t\t\t).arg(mainDcId));\n\t\t}\n\t\treturn result;\n\t};\n\tif (_mtp) {\n\t\tconst auto keys = _mtp->getKeysForWrite();\n\t\tconst auto keysToDestroy = _mtpForKeysDestroy\n\t\t\t? _mtpForKeysDestroy->getKeysForWrite()\n\t\t\t: MTP::AuthKeysList();\n\t\treturn serialize(_mtp->mainDcId(), keys, keysToDestroy);\n\t}\n\tconst auto &keys = _mtpFields.keys;\n\tconst auto &keysToDestroy = _mtpKeysToDestroy;\n\treturn serialize(_mtpFields.mainDcId, keys, keysToDestroy);\n}\n\nvoid Account::setSessionUserId(UserId userId) {\n\tExpects(!sessionExists());\n\n\t_sessionUserId = userId;\n}\n\nvoid Account::setSessionFromStorage(\n\t\tstd::unique_ptr<SessionSettings> data,\n\t\tQByteArray &&selfSerialized,\n\t\tint32 selfStreamVersion) {\n\tExpects(!sessionExists());\n\n\tDEBUG_LOG((\"sessionUserSerialized set: %1\"\n\t\t).arg(selfSerialized.size()));\n\n\t_storedSessionSettings = std::move(data);\n\t_sessionUserSerialized = std::move(selfSerialized);\n\t_sessionUserStreamVersion = selfStreamVersion;\n}\n\nSessionSettings *Account::getSessionSettings() {\n\tif (_sessionUserId) {\n\t\treturn _storedSessionSettings\n\t\t\t? _storedSessionSettings.get()\n\t\t\t: nullptr;\n\t} else if (const auto session = maybeSession()) {\n\t\treturn &session->settings();\n\t}\n\treturn nullptr;\n}\n\nvoid Account::setMtpAuthorization(const QByteArray &serialized) {\n\tExpects(!_mtp);\n\n\tQDataStream stream(serialized);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\n\tauto legacyUserId = Serialize::read<qint32>(stream);\n\tauto legacyMainDcId = Serialize::read<qint32>(stream);\n\tauto userId = quint64();\n\tauto mainDcId = qint32();\n\tif (((uint64(legacyUserId) << 32) | uint64(legacyMainDcId))\n\t\t== kWideIdsTag) {\n\t\tuserId = Serialize::read<quint64>(stream);\n\t\tmainDcId = Serialize::read<qint32>(stream);\n\t} else {\n\t\tuserId = legacyUserId;\n\t\tmainDcId = legacyMainDcId;\n\t}\n\tif (stream.status() != QDataStream::Ok) {\n\t\tLOG((\"MTP Error: \"\n\t\t\t\"Could not read main fields from mtp authorization.\"));\n\t\treturn;\n\t}\n\n\tsetSessionUserId(userId);\n\t_mtpFields.mainDcId = mainDcId;\n\n\tconst auto readKeys = [&](auto &keys) {\n\t\tconst auto count = Serialize::read<qint32>(stream);\n\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\tLOG((\"MTP Error: \"\n\t\t\t\t\"Could not read keys count from mtp authorization.\"));\n\t\t\treturn;\n\t\t}\n\t\tkeys.reserve(count);\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tconst auto dcId = Serialize::read<qint32>(stream);\n\t\t\tconst auto keyData = Serialize::read<MTP::AuthKey::Data>(stream);\n\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\tLOG((\"MTP Error: \"\n\t\t\t\t\t\"Could not read key from mtp authorization.\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tkeys.push_back(std::make_shared<MTP::AuthKey>(MTP::AuthKey::Type::ReadFromFile, dcId, keyData));\n\t\t}\n\t};\n\treadKeys(_mtpFields.keys);\n\treadKeys(_mtpKeysToDestroy);\n\tLOG((\"MTP Info: \"\n\t\t\"read keys, current: %1, to destroy: %2\"\n\t\t).arg(_mtpFields.keys.size()\n\t\t).arg(_mtpKeysToDestroy.size()));\n}\n\nvoid Account::startMtp(std::unique_ptr<MTP::Config> config) {\n\tExpects(!_mtp);\n\n\tauto fields = base::take(_mtpFields);\n\tfields.config = std::move(config);\n\tfields.deviceModel = Platform::DeviceModelPretty();\n\tfields.systemVersion = Platform::SystemVersionPretty();\n\t_mtp = std::make_unique<MTP::Instance>(\n\t\tMTP::Instance::Mode::Normal,\n\t\tstd::move(fields));\n\n\tconst auto writingKeys = _mtp->lifetime().make_state<bool>(false);\n\t_mtp->writeKeysRequests(\n\t) | rpl::filter([=] {\n\t\treturn !*writingKeys;\n\t}) | rpl::on_next([=] {\n\t\t*writingKeys = true;\n\t\tUi::PostponeCall(_mtp.get(), [=] {\n\t\t\tlocal().writeMtpData();\n\t\t\t*writingKeys = false;\n\t\t});\n\t}, _mtp->lifetime());\n\n\tconst auto writingConfig = _lifetime.make_state<bool>(false);\n\trpl::merge(\n\t\t_mtp->config().updates(),\n\t\t_mtp->dcOptions().changed() | rpl::to_empty\n\t) | rpl::filter([=] {\n\t\treturn !*writingConfig;\n\t}) | rpl::on_next([=] {\n\t\t*writingConfig = true;\n\t\tUi::PostponeCall(_mtp.get(), [=] {\n\t\t\tlocal().writeMtpConfig();\n\t\t\t*writingConfig = false;\n\t\t});\n\t}, _lifetime);\n\n\t_mtpFields.mainDcId = _mtp->mainDcId();\n\n\t_mtp->setUpdatesHandler([=](const MTP::Response &message) {\n\t\tcheckForUpdates(message) || checkForNewSession(message);\n\t});\n\t_mtp->setGlobalFailHandler([=](const MTP::Error &, const MTP::Response &) {\n\t\tif (const auto session = maybeSession()) {\n\t\t\tcrl::on_main(session, [=] { logOut(); });\n\t\t}\n\t});\n\t_mtp->setStateChangedHandler([=](MTP::ShiftedDcId dc, int32 state) {\n\t\tif (dc == _mtp->mainDcId()) {\n\t\t\tCore::App().settings().proxy().connectionTypeChangesNotify();\n\t\t}\n\t});\n\t_mtp->setSessionResetHandler([=](MTP::ShiftedDcId shiftedDcId) {\n\t\tif (const auto session = maybeSession()) {\n\t\t\tif (shiftedDcId == _mtp->mainDcId()) {\n\t\t\t\tsession->updates().getDifference();\n\t\t\t}\n\t\t}\n\t});\n\n\tif (!_mtpKeysToDestroy.empty()) {\n\t\tdestroyMtpKeys(base::take(_mtpKeysToDestroy));\n\t}\n\n\tif (_sessionUserId) {\n\t\tcreateSession(\n\t\t\t_sessionUserId,\n\t\t\tbase::take(_sessionUserSerialized),\n\t\t\tbase::take(_sessionUserStreamVersion),\n\t\t\t(_storedSessionSettings\n\t\t\t\t? std::move(_storedSessionSettings)\n\t\t\t\t: std::make_unique<SessionSettings>()));\n\t}\n\t_storedSessionSettings = nullptr;\n\n\tif (const auto session = maybeSession()) {\n\t\t// Skip all pending self updates so that we won't local().writeSelf.\n\t\tsession->changes().sendNotifications();\n\t}\n\n\t_mtpValue = _mtp.get();\n}\n\nbool Account::checkForUpdates(const MTP::Response &message) {\n\tauto updates = MTPUpdates();\n\tauto from = message.reply.constData();\n\tif (!updates.read(from, from + message.reply.size())) {\n\t\treturn false;\n\t}\n\t_mtpUpdates.fire(std::move(updates));\n\treturn true;\n}\n\nbool Account::checkForNewSession(const MTP::Response &message) {\n\tauto newSession = MTPNewSession();\n\tauto from = message.reply.constData();\n\tif (!newSession.read(from, from + message.reply.size())) {\n\t\treturn false;\n\t}\n\t_mtpNewSessionCreated.fire({});\n\treturn true;\n}\n\nvoid Account::logOut() {\n\tif (_loggingOut) {\n\t\treturn;\n\t}\n\t_loggingOut = true;\n\tif (_mtp) {\n\t\t_mtp->logout([=] { loggedOut(); });\n\t} else {\n\t\t// We log out because we've forgotten passcode.\n\t\tloggedOut();\n\t}\n}\n\nbool Account::loggingOut() const {\n\treturn _loggingOut;\n}\n\nvoid Account::forcedLogOut() {\n\tif (sessionExists()) {\n\t\tresetAuthorizationKeys();\n\t\tloggedOut();\n\t}\n}\n\nvoid Account::loggedOut() {\n\t_loggingOut = false;\n\tMedia::Player::mixer()->stopAndClear();\n\tdestroySession(DestroyReason::LoggedOut);\n\tlocal().reset();\n\tcSetOtherOnline(0);\n}\n\nvoid Account::destroyMtpKeys(MTP::AuthKeysList &&keys) {\n\tExpects(_mtp != nullptr);\n\n\tif (keys.empty()) {\n\t\treturn;\n\t}\n\tif (_mtpForKeysDestroy) {\n\t\t_mtpForKeysDestroy->addKeysForDestroy(std::move(keys));\n\t\tlocal().writeMtpData();\n\t\treturn;\n\t}\n\tauto destroyFields = MTP::Instance::Fields();\n\n\tdestroyFields.mainDcId = MTP::Instance::Fields::kNoneMainDc;\n\tdestroyFields.config = std::make_unique<MTP::Config>(_mtp->config());\n\tdestroyFields.keys = std::move(keys);\n\tdestroyFields.deviceModel = Platform::DeviceModelPretty();\n\tdestroyFields.systemVersion = Platform::SystemVersionPretty();\n\t_mtpForKeysDestroy = std::make_unique<MTP::Instance>(\n\t\tMTP::Instance::Mode::KeysDestroyer,\n\t\tstd::move(destroyFields));\n\t_mtpForKeysDestroy->writeKeysRequests(\n\t) | rpl::on_next([=] {\n\t\tlocal().writeMtpData();\n\t}, _mtpForKeysDestroy->lifetime());\n\t_mtpForKeysDestroy->allKeysDestroyed(\n\t) | rpl::on_next([=] {\n\t\tLOG((\"MTP Info: all keys scheduled for destroy are destroyed.\"));\n\t\tcrl::on_main(this, [=] {\n\t\t\t_mtpForKeysDestroy = nullptr;\n\t\t\tlocal().writeMtpData();\n\t\t});\n\t}, _mtpForKeysDestroy->lifetime());\n}\n\nvoid Account::suggestMainDcId(MTP::DcId mainDcId) {\n\tExpects(_mtp != nullptr);\n\n\t_mtp->suggestMainDcId(mainDcId);\n\tif (_mtpFields.mainDcId != MTP::Instance::Fields::kNotSetMainDc) {\n\t\t_mtpFields.mainDcId = mainDcId;\n\t}\n}\n\nvoid Account::destroyStaleAuthorizationKeys() {\n\tExpects(_mtp != nullptr);\n\n\tfor (const auto &key : _mtp->getKeysForWrite()) {\n\t\t// Disable this for now.\n\t\tif (key->type() == MTP::AuthKey::Type::ReadFromFile) {\n\t\t\t_mtpKeysToDestroy = _mtp->getKeysForWrite();\n\t\t\tLOG((\"MTP Info: destroying stale keys, count: %1\"\n\t\t\t\t).arg(_mtpKeysToDestroy.size()));\n\t\t\tresetAuthorizationKeys();\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nvoid Account::setHandleLoginCode(Fn<void(QString)> callback) {\n\t_handleLoginCode = std::move(callback);\n}\n\nvoid Account::handleLoginCode(const QString &code) const {\n\tif (_handleLoginCode) {\n\t\t_handleLoginCode(code);\n\t}\n}\n\nvoid Account::resetAuthorizationKeys() {\n\tExpects(_mtp != nullptr);\n\n\t{\n\t\tconst auto old = base::take(_mtp);\n\t\tauto config = std::make_unique<MTP::Config>(old->config());\n\t\tstartMtp(std::move(config));\n\t}\n\tlocal().writeMtpData();\n}\n\n} // namespace Main\n"
  },
  {
    "path": "Telegram/SourceFiles/main/main_account.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/mtproto_auth_key.h\"\n#include \"mtproto/mtp_instance.h\"\n#include \"base/weak_ptr.h\"\n\nnamespace Storage {\nclass Account;\nclass Domain;\nenum class StartResult : uchar;\n} // namespace Storage\n\nnamespace MTP {\nclass AuthKey;\nclass Config;\n} // namespace MTP\n\nnamespace Main {\n\nclass Domain;\nclass Session;\nclass SessionSettings;\nclass AppConfig;\n\nclass Account final : public base::has_weak_ptr {\npublic:\n\tAccount(not_null<Domain*> domain, const QString &dataName, int index);\n\t~Account();\n\n\t[[nodiscard]] Domain &domain() const {\n\t\treturn *_domain;\n\t}\n\n\t[[nodiscard]] Storage::Domain &domainLocal() const;\n\n\t[[nodiscard]] Storage::StartResult legacyStart(\n\t\tconst QByteArray &passcode);\n\t[[nodiscard]] std::unique_ptr<MTP::Config> prepareToStart(\n\t\tstd::shared_ptr<MTP::AuthKey> localKey);\n\tvoid prepareToStartAdded(\n\t\tstd::shared_ptr<MTP::AuthKey> localKey);\n\tvoid start(std::unique_ptr<MTP::Config> config);\n\n\t[[nodiscard]] uint64 willHaveSessionUniqueId(MTP::Config *config) const;\n\tvoid createSession(\n\t\tconst MTPUser &user,\n\t\tstd::unique_ptr<SessionSettings> settings = nullptr);\n\tvoid createSession(\n\t\tUserId id,\n\t\tQByteArray serialized,\n\t\tint streamVersion,\n\t\tstd::unique_ptr<SessionSettings> settings);\n\n\tvoid logOut();\n\tvoid forcedLogOut();\n\t[[nodiscard]] bool loggingOut() const;\n\n\t[[nodiscard]] AppConfig &appConfig() const {\n\t\tExpects(_appConfig != nullptr);\n\n\t\treturn *_appConfig;\n\t}\n\n\t[[nodiscard]] Storage::Account &local() const {\n\t\treturn *_local;\n\t}\n\n\t[[nodiscard]] bool sessionExists() const;\n\t[[nodiscard]] Session &session() const;\n\t[[nodiscard]] Session *maybeSession() const;\n\t[[nodiscard]] rpl::producer<Session*> sessionValue() const;\n\t[[nodiscard]] rpl::producer<Session*> sessionChanges() const;\n\n\t[[nodiscard]] MTP::Instance &mtp() const {\n\t\treturn *_mtp;\n\t}\n\t[[nodiscard]] rpl::producer<not_null<MTP::Instance*>> mtpValue() const;\n\n\t// Each time the main session changes a new copy of the pointer is fired.\n\t// This allows to resend the requests that were not requiring auth, and\n\t// which could be forgotten without calling .done() or .fail() because\n\t// of the main dc changing.\n\t[[nodiscard]] auto mtpMainSessionValue() const\n\t\t-> rpl::producer<not_null<MTP::Instance*>>;\n\n\t// Set from legacy storage.\n\tvoid setLegacyMtpKey(std::shared_ptr<MTP::AuthKey> key);\n\n\tvoid setMtpMainDcId(MTP::DcId mainDcId);\n\tvoid setSessionUserId(UserId userId);\n\tvoid setSessionFromStorage(\n\t\tstd::unique_ptr<SessionSettings> data,\n\t\tQByteArray &&selfSerialized,\n\t\tint32 selfStreamVersion);\n\t[[nodiscard]] SessionSettings *getSessionSettings();\n\t[[nodiscard]] rpl::producer<> mtpNewSessionCreated() const;\n\t[[nodiscard]] rpl::producer<MTPUpdates> mtpUpdates() const;\n\n\t// Serialization.\n\t[[nodiscard]] QByteArray serializeMtpAuthorization() const;\n\tvoid setMtpAuthorization(const QByteArray &serialized);\n\n\tvoid suggestMainDcId(MTP::DcId mainDcId);\n\tvoid destroyStaleAuthorizationKeys();\n\n\tvoid setHandleLoginCode(Fn<void(QString)> callback);\n\tvoid handleLoginCode(const QString &code) const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tstatic constexpr auto kDefaultSaveDelay = crl::time(1000);\n\tenum class DestroyReason {\n\t\tQuitting,\n\t\tLoggedOut,\n\t};\n\n\tvoid startMtp(std::unique_ptr<MTP::Config> config);\n\tvoid createSession(\n\t\tconst MTPUser &user,\n\t\tQByteArray serialized,\n\t\tint streamVersion,\n\t\tstd::unique_ptr<SessionSettings> settings);\n\tvoid watchProxyChanges();\n\tvoid watchSessionChanges();\n\tbool checkForUpdates(const MTP::Response &message);\n\tbool checkForNewSession(const MTP::Response &message);\n\n\tvoid destroyMtpKeys(MTP::AuthKeysList &&keys);\n\tvoid resetAuthorizationKeys();\n\n\tvoid loggedOut();\n\tvoid destroySession(DestroyReason reason);\n\n\tconst not_null<Domain*> _domain;\n\tconst std::unique_ptr<Storage::Account> _local;\n\n\tstd::unique_ptr<MTP::Instance> _mtp;\n\trpl::variable<MTP::Instance*> _mtpValue;\n\tstd::unique_ptr<MTP::Instance> _mtpForKeysDestroy;\n\trpl::event_stream<MTPUpdates> _mtpUpdates;\n\trpl::event_stream<> _mtpNewSessionCreated;\n\n\tstd::unique_ptr<AppConfig> _appConfig;\n\n\tstd::unique_ptr<Session> _session;\n\trpl::variable<Session*> _sessionValue;\n\n\tFn<void(QString)> _handleLoginCode = nullptr;\n\n\tUserId _sessionUserId = 0;\n\tQByteArray _sessionUserSerialized;\n\tint32 _sessionUserStreamVersion = 0;\n\tstd::unique_ptr<SessionSettings> _storedSessionSettings;\n\tMTP::Instance::Fields _mtpFields;\n\tMTP::AuthKeysList _mtpKeysToDestroy;\n\tbool _loggingOut = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Main\n"
  },
  {
    "path": "Telegram/SourceFiles/main/main_app_config.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"main/main_app_config.h\"\n\n#include \"api/api_authorizations.h\"\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"calls/group/ui/calls_group_stars_coloring.h\"\n#include \"data/data_session.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/chat_style.h\"\n\nnamespace Main {\nnamespace {\n\nconstexpr auto kRefreshTimeout = 3600 * crl::time(1000);\n\n} // namespace\n\nAppConfig::AppConfig(not_null<Account*> account) : _account(account) {\n\taccount->sessionChanges(\n\t) | rpl::filter([=](Session *session) {\n\t\treturn (session != nullptr);\n\t}) | rpl::on_next([=] {\n\t\t_lastFrozenRefresh = 0;\n\t\trefresh();\n\t}, _lifetime);\n}\n\nAppConfig::~AppConfig() = default;\n\nvoid AppConfig::start() {\n\t_account->mtpMainSessionValue(\n\t) | rpl::on_next([=](not_null<MTP::Instance*> instance) {\n\t\t_api.emplace(instance);\n\t\t_requestId = 0;\n\t\trefresh();\n\n\t\t_frozenTrackLifetime = instance->frozenErrorReceived(\n\t\t) | rpl::on_next([=] {\n\t\t\tif (!get<int>(u\"freeze_since_date\"_q, 0)) {\n\t\t\t\tconst auto now = crl::now();\n\t\t\t\tif (!_lastFrozenRefresh\n\t\t\t\t\t|| now > _lastFrozenRefresh + kRefreshTimeout) {\n\t\t\t\t\t_lastFrozenRefresh = now;\n\t\t\t\t\trefresh();\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}, _lifetime);\n}\n\nint AppConfig::quoteLengthMax() const {\n\treturn get<int>(u\"quote_length_max\"_q, 1024);\n}\n\nint AppConfig::stargiftConvertPeriodMax() const {\n\treturn get<int>(\n\t\tu\"stargifts_convert_period_max\"_q,\n\t\t_account->mtp().isTestMode() ? 300 : (90 * 86400));\n}\n\nint AppConfig::noForwardsRequestExpirePeriod() const {\n\treturn get<int>(\n\t\tu\"no_forwards_request_expire_period\"_q,\n\t\t_account->mtp().isTestMode() ? 300 : 86400);\n}\n\nconst std::vector<QString> &AppConfig::startRefPrefixes() {\n\tif (_startRefPrefixes.empty()) {\n\t\t_startRefPrefixes = get<std::vector<QString>>(\n\t\t\tu\"starref_start_param_prefixes\"_q,\n\t\t\tstd::vector<QString>());\n\t}\n\treturn _startRefPrefixes;\n}\n\nbool AppConfig::starrefSetupAllowed() const {\n\treturn get<bool>(u\"starref_program_allowed\"_q, false);\n}\n\nbool AppConfig::starrefJoinAllowed() const {\n\treturn get<bool>(u\"starref_connect_allowed\"_q, false);\n}\n\nint AppConfig::starrefCommissionMin() const {\n\treturn get<int>(u\"starref_min_commission_permille\"_q, 1);\n}\n\nint AppConfig::starrefCommissionMax() const {\n\treturn get<int>(u\"starref_max_commission_permille\"_q, 900);\n}\n\nint AppConfig::starsWithdrawMax() const {\n\treturn get<int>(u\"stars_revenue_withdrawal_max\"_q, 100);\n}\n\nfloat64 AppConfig::starsWithdrawRate() const {\n\treturn get<float64>(u\"stars_usd_withdraw_rate_x1000\"_q, 1300) / 1000.;\n}\n\nfloat64 AppConfig::currencyWithdrawRate() const {\n\treturn get<float64>(u\"ton_usd_rate\"_q, 1);\n}\n\nfloat64 AppConfig::starsSellRate() const {\n\treturn get<float64>(u\"stars_usd_sell_rate_x1000\"_q, 1410) / 1000.;\n}\n\nfloat64 AppConfig::currencySellRate() const {\n\treturn get<float64>(u\"ton_usd_rate\"_q, 1);\n}\n\nbool AppConfig::paidMessagesAvailable() const {\n\treturn get<bool>(u\"stars_paid_messages_available\"_q, false);\n}\n\nint AppConfig::paidMessageStarsMax() const {\n\treturn get<int>(u\"stars_paid_message_amount_max\"_q, 10'000);\n}\n\nint AppConfig::paidMessageCommission() const {\n\treturn get<int>(u\"stars_paid_message_commission_permille\"_q, 850);\n}\n\nint AppConfig::paidMessageChannelStarsDefault() const {\n\treturn get<int>(u\"stars_paid_messages_channel_amount_default\"_q, 10);\n}\n\nint AppConfig::pinnedGiftsLimit() const {\n\treturn get<int>(u\"stargifts_pinned_to_top_limit\"_q, 6);\n}\n\nint AppConfig::giftCollectionsLimit() const {\n\treturn get<int>(u\"stargifts_collections_limit\"_q, 10);\n}\n\nint AppConfig::giftCollectionGiftsLimit() const {\n\treturn get<int>(u\"stargifts_collection_gifts_limit\"_q, 500);\n}\n\nbool AppConfig::callsDisabledForSession() const {\n\tconst auto authorizations = _account->sessionExists()\n\t\t? &_account->session().api().authorizations()\n\t\t: nullptr;\n\treturn get<bool>(\n\t\tu\"call_requests_disabled\"_q,\n\t\tauthorizations->callsDisabledHere());\n}\n\nint AppConfig::confcallSizeLimit() const {\n\treturn get<int>(\n\t\tu\"conference_call_size_limit\"_q,\n\t\t_account->mtp().isTestMode() ? 5 : 100);\n}\n\nbool AppConfig::confcallPrioritizeVP8() const {\n\treturn get<bool>(u\"confcall_use_vp8\"_q, false);\n}\n\nint AppConfig::giftResaleStarsMin() const {\n\treturn get<int>(u\"stars_stargift_resale_amount_min\"_q, 125);\n}\n\nint AppConfig::giftResaleStarsMax() const {\n\treturn get<int>(u\"stars_stargift_resale_amount_max\"_q, 35000);\n}\n\nint AppConfig::giftResaleStarsThousandths() const {\n\treturn get<int>(u\"stars_stargift_resale_commission_permille\"_q, 800);\n}\n\nint64 AppConfig::giftResaleNanoTonMin() const {\n\treturn get<int64>(u\"ton_stargift_resale_amount_min\"_q, 250'000'000LL);\n}\n\nint64 AppConfig::giftResaleNanoTonMax() const {\n\treturn get<int64>(\n\t\tu\"ton_stargift_resale_amount_max\"_q,\n\t\t1'000'000'000'000'000LL);\n}\n\nint AppConfig::giftResaleNanoTonThousandths() const {\n\treturn get<int>(u\"ton_stargift_resale_commission_permille\"_q, 800);\n}\n\nint AppConfig::pollOptionsLimit() const {\n\treturn get<int>(u\"poll_answers_max\"_q, 12);\n}\n\nint AppConfig::pollAnswerDeletePeriod() const {\n\treturn get<int>(u\"poll_answer_delete_period\"_q, 300);\n}\n\nint AppConfig::todoListItemsLimit() const {\n\treturn get<int>(\n\t\tu\"todo_items_max\"_q,\n\t\t_account->mtp().isTestMode() ? 10 : 30);\n}\n\nint AppConfig::todoListTitleLimit() const {\n\treturn get<int>(u\"todo_title_length_max\"_q, 32);\n}\n\nint AppConfig::todoListItemTextLimit() const {\n\treturn get<int>(u\"todo_item_length_max\"_q, 64);\n}\n\nint AppConfig::suggestedPostCommissionStars() const {\n\treturn get<int>(u\"stars_suggested_post_commission_permille\"_q, 850);\n}\n\nint AppConfig::suggestedPostCommissionTon() const {\n\treturn get<int>(u\"ton_suggested_post_commission_permille\"_q, 850);\n}\n\nint AppConfig::suggestedPostStarsMin() const {\n\treturn get<int>(u\"stars_suggested_post_amount_min\"_q, 5);\n}\n\nint AppConfig::suggestedPostStarsMax() const {\n\treturn get<int>(u\"stars_suggested_post_amount_max\"_q, 100'000);\n}\n\nint64 AppConfig::suggestedPostNanoTonMin() const {\n\treturn get<int64>(u\"ton_suggested_post_amount_min\"_q, 10'000'000LL);\n}\n\nint64 AppConfig::suggestedPostNanoTonMax() const {\n\treturn get<int64>(\n\t\tu\"ton_suggested_post_amount_max\"_q,\n\t\t10'000'000'000'000LL);\n}\n\nint AppConfig::suggestedPostDelayMin() const {\n\treturn get<int>(u\"stars_suggested_post_future_min\"_q, 300);\n}\n\nint AppConfig::suggestedPostDelayMax() const {\n\treturn get<int>(u\"appConfig.stars_suggested_post_future_max\"_q, 2678400);\n}\n\nTimeId AppConfig::suggestedPostAgeMin() const {\n\treturn get<int>(u\"stars_suggested_post_age_min\"_q, 86400);\n}\n\nbool AppConfig::ageVerifyNeeded() const {\n\treturn get<bool>(u\"need_age_video_verification\"_q, false);\n}\n\nQString AppConfig::ageVerifyCountry() const {\n\treturn get<QString>(u\"verify_age_country\"_q, QString());\n}\n\nint AppConfig::ageVerifyMinAge() const {\n\treturn get<int>(u\"verify_age_min\"_q, 18);\n}\n\nQString AppConfig::ageVerifyBotUsername() const {\n\treturn get<QString>(u\"verify_age_bot_username\"_q, QString());\n}\n\nint AppConfig::storiesAlbumsLimit() const {\n\treturn get<int>(u\"stories_albums_limit\"_q, 100);\n}\n\nint AppConfig::storiesAlbumLimit() const {\n\treturn get<int>(u\"stories_album_stories_limit\"_q, 1000);\n}\n\nint AppConfig::groupCallMessageLengthLimit() const {\n\treturn get<int>(u\"group_call_message_length_limit\"_q, 128);\n}\n\nTimeId AppConfig::groupCallMessageTTL() const {\n\treturn get<int>(u\"group_call_message_ttl\"_q, 10);\n}\n\nint AppConfig::passkeysAccountPasskeysMax() const {\n\treturn get<int>(u\"passkeys_account_passkeys_max\"_q, 10);\n}\n\nbool AppConfig::settingsDisplayPasskeys() const {\n\treturn get<bool>(u\"settings_display_passkeys\"_q, false);\n}\n\nint64 AppConfig::stakeDiceNanoTonMin() const {\n\treturn get<int64>(u\"ton_stakedice_stake_amount_min\"_q, 100'000'000LL);\n}\n\nint64 AppConfig::stakeDiceNanoTonMax() const {\n\treturn get<int64>(u\"ton_stakedice_stake_amount_max\"_q, 50'000'000'000LL);\n}\n\nstd::vector<int64> AppConfig::stakeDiceNanoTonSuggested() const {\n\treturn get<std::vector<int64>>(\n\t\tu\"ton_stakedice_stake_suggested_amounts\"_q,\n\t\tstd::vector<int64>{\n\t\t\t100'000'000LL,\n\t\t\t1'000'000'000LL,\n\t\t\t2'000'000'000LL,\n\t\t\t5'000'000'000LL,\n\t\t\t10'000'000'000LL,\n\t\t\t20'000'000'000LL,\n\t\t});\n}\n\nvoid AppConfig::refresh(bool force) {\n\tif (_requestId || !_api) {\n\t\tif (force) {\n\t\t\t_pendingRefresh = true;\n\t\t}\n\t\treturn;\n\t}\n\t_pendingRefresh = false;\n\t_requestId = _api->request(MTPhelp_GetAppConfig(\n\t\tMTP_int(_hash)\n\t)).done([=](const MTPhelp_AppConfig &result) {\n\t\t_requestId = 0;\n\t\tresult.match([&](const MTPDhelp_appConfig &data) {\n\t\t\t_hash = data.vhash().v;\n\n\t\t\tconst auto &config = data.vconfig();\n\t\t\tif (config.type() != mtpc_jsonObject) {\n\t\t\t\tLOG((\"API Error: Unexpected config type.\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto was = ignoredRestrictionReasons();\n\n\t\t\t_data.clear();\n\t\t\tfor (const auto &element : config.c_jsonObject().vvalue().v) {\n\t\t\t\telement.match([&](const MTPDjsonObjectValue &data) {\n\t\t\t\t\t_data.emplace_or_assign(qs(data.vkey()), data.vvalue());\n\t\t\t\t});\n\t\t\t}\n\t\t\tupdateIgnoredRestrictionReasons(std::move(was));\n\n\t\t\t_aiComposeStyles.reset();\n\t\t\t_groupCallColorings = {};\n\n\t\t\tDEBUG_LOG((\"getAppConfig result handled.\"));\n\t\t\t_refreshed.fire({});\n\t\t}, [](const MTPDhelp_appConfigNotModified &) {});\n\n\t\tif (base::take(_pendingRefresh)) {\n\t\t\trefresh();\n\t\t} else {\n\t\t\trefreshDelayed();\n\t\t}\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t\trefreshDelayed();\n\t}).send();\n}\n\nvoid AppConfig::refreshDelayed() {\n\tbase::call_delayed(kRefreshTimeout, _account, [=] {\n\t\trefresh();\n\t});\n}\n\nvoid AppConfig::updateIgnoredRestrictionReasons(std::vector<QString> was) {\n\t_ignoreRestrictionReasons = get<std::vector<QString>>(\n\t\tu\"ignore_restriction_reasons\"_q,\n\t\tstd::vector<QString>());\n\tranges::sort(_ignoreRestrictionReasons);\n\tif (_ignoreRestrictionReasons != was) {\n\t\tfor (const auto &reason : _ignoreRestrictionReasons) {\n\t\t\tconst auto i = ranges::remove(was, reason);\n\t\t\tif (i != end(was)) {\n\t\t\t\twas.erase(i, end(was));\n\t\t\t} else {\n\t\t\t\twas.push_back(reason);\n\t\t\t}\n\t\t}\n\t\t_ignoreRestrictionChanges.fire(std::move(was));\n\t}\n}\n\nrpl::producer<> AppConfig::refreshed() const {\n\treturn _refreshed.events();\n}\n\nrpl::producer<> AppConfig::value() const {\n\treturn _refreshed.events_starting_with({});\n}\n\ntemplate <typename Extractor>\nauto AppConfig::getValue(const QString &key, Extractor &&extractor) const {\n\tconst auto i = _data.find(key);\n\treturn extractor((i != end(_data))\n\t\t? i->second\n\t\t: MTPJSONValue(MTP_jsonNull()));\n}\n\nbool AppConfig::getBool(const QString &key, bool fallback) const {\n\treturn getValue(key, [&](const MTPJSONValue &value) {\n\t\treturn value.match([&](const MTPDjsonBool &data) {\n\t\t\treturn mtpIsTrue(data.vvalue());\n\t\t}, [&](const auto &data) {\n\t\t\treturn fallback;\n\t\t});\n\t});\n}\n\ndouble AppConfig::getDouble(const QString &key, double fallback) const {\n\treturn getValue(key, [&](const MTPJSONValue &value) {\n\t\treturn value.match([&](const MTPDjsonNumber &data) {\n\t\t\treturn data.vvalue().v;\n\t\t}, [&](const auto &data) {\n\t\t\treturn fallback;\n\t\t});\n\t});\n}\n\nQString AppConfig::getString(\n\t\tconst QString &key,\n\t\tconst QString &fallback) const {\n\treturn getValue(key, [&](const MTPJSONValue &value) {\n\t\treturn value.match([&](const MTPDjsonString &data) {\n\t\t\treturn qs(data.vvalue());\n\t\t}, [&](const auto &data) {\n\t\t\treturn fallback;\n\t\t});\n\t});\n}\n\nstd::vector<QString> AppConfig::getStringArray(\n\t\tconst QString &key,\n\t\tstd::vector<QString> &&fallback) const {\n\treturn getValue(key, [&](const MTPJSONValue &value) {\n\t\treturn value.match([&](const MTPDjsonArray &data) {\n\t\t\tauto result = std::vector<QString>();\n\t\t\tresult.reserve(data.vvalue().v.size());\n\t\t\tfor (const auto &entry : data.vvalue().v) {\n\t\t\t\tif (entry.type() != mtpc_jsonString) {\n\t\t\t\t\treturn std::move(fallback);\n\t\t\t\t}\n\t\t\t\tresult.push_back(qs(entry.c_jsonString().vvalue()));\n\t\t\t}\n\t\t\treturn result;\n\t\t}, [&](const auto &data) {\n\t\t\treturn std::move(fallback);\n\t\t});\n\t});\n}\n\nbase::flat_map<QString, QString> AppConfig::getStringMap(\n\t\tconst QString &key,\n\t\tbase::flat_map<QString, QString> &&fallback) const {\n\treturn getValue(key, [&](const MTPJSONValue &value) {\n\t\treturn value.match([&](const MTPDjsonObject &data) {\n\t\t\tauto result = base::flat_map<QString, QString>();\n\t\t\tresult.reserve(data.vvalue().v.size());\n\t\t\tfor (const auto &entry : data.vvalue().v) {\n\t\t\t\tconst auto &data = entry.data();\n\t\t\t\tconst auto &value = data.vvalue();\n\t\t\t\tif (value.type() != mtpc_jsonString) {\n\t\t\t\t\treturn std::move(fallback);\n\t\t\t\t}\n\t\t\t\tresult.emplace(\n\t\t\t\t\tqs(data.vkey()),\n\t\t\t\t\tqs(value.c_jsonString().vvalue()));\n\t\t\t}\n\t\t\treturn result;\n\t\t}, [&](const auto &data) {\n\t\t\treturn std::move(fallback);\n\t\t});\n\t});\n}\n\nstd::vector<int> AppConfig::getIntArray(\n\t\tconst QString &key,\n\t\tstd::vector<int> &&fallback) const {\n\treturn getValue(key, [&](const MTPJSONValue &value) {\n\t\treturn value.match([&](const MTPDjsonArray &data) {\n\t\t\tauto result = std::vector<int>();\n\t\t\tresult.reserve(data.vvalue().v.size());\n\t\t\tfor (const auto &entry : data.vvalue().v) {\n\t\t\t\tif (entry.type() != mtpc_jsonNumber) {\n\t\t\t\t\treturn std::move(fallback);\n\t\t\t\t}\n\t\t\t\tresult.push_back(\n\t\t\t\t\tint(base::SafeRound(entry.c_jsonNumber().vvalue().v)));\n\t\t\t}\n\t\t\treturn result;\n\t\t}, [&](const auto &data) {\n\t\t\treturn std::move(fallback);\n\t\t});\n\t});\n}\n\nstd::vector<std::vector<int>> AppConfig::getIntIntArray(\n\t\tconst QString &key,\n\t\tstd::vector<std::vector<int>> &&fallback) const {\n\treturn getValue(key, [&](const MTPJSONValue &value) {\n\t\treturn value.match([&](const MTPDjsonArray &data) {\n\t\t\tauto result = std::vector<std::vector<int>>();\n\t\t\tresult.reserve(data.vvalue().v.size());\n\t\t\tfor (const auto &entry : data.vvalue().v) {\n\t\t\t\tif (entry.type() != mtpc_jsonArray) {\n\t\t\t\t\treturn std::move(fallback);\n\t\t\t\t}\n\t\t\t\tconst auto &list = entry.c_jsonArray().vvalue().v;\n\t\t\t\tauto &last = result.emplace_back();\n\t\t\t\tlast.reserve(list.size());\n\t\t\t\tfor (const auto &inner : list) {\n\t\t\t\t\tif (inner.type() != mtpc_jsonNumber) {\n\t\t\t\t\t\treturn std::move(fallback);\n\t\t\t\t\t}\n\t\t\t\t\tlast.push_back(\n\t\t\t\t\t\tint(base::SafeRound(inner.c_jsonNumber().vvalue().v)));\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn result;\n\t\t}, [&](const auto &data) {\n\t\t\treturn std::move(fallback);\n\t\t});\n\t});\n}\n\nstd::vector<int64> AppConfig::getInt64Array(\n\t\tconst QString &key,\n\t\tstd::vector<int64> &&fallback) const {\n\treturn getValue(key, [&](const MTPJSONValue &value) {\n\t\treturn value.match([&](const MTPDjsonArray &data) {\n\t\t\tauto result = std::vector<int64>();\n\t\t\tresult.reserve(data.vvalue().v.size());\n\t\t\tfor (const auto &entry : data.vvalue().v) {\n\t\t\t\tif (entry.type() != mtpc_jsonNumber) {\n\t\t\t\t\treturn std::move(fallback);\n\t\t\t\t}\n\t\t\t\tresult.push_back(\n\t\t\t\t\tint64(base::SafeRound(entry.c_jsonNumber().vvalue().v)));\n\t\t\t}\n\t\t\treturn result;\n\t\t}, [&](const auto &data) {\n\t\t\treturn std::move(fallback);\n\t\t});\n\t});\n}\n\nbool AppConfig::newRequirePremiumFree() const {\n\treturn get<bool>(\n\t\tu\"new_noncontact_peers_require_premium_without_ownpremium\"_q,\n\t\tfalse);\n}\n\nstd::vector<AppConfig::AiComposeStyle> AppConfig::aiComposeStyles() const {\n\tif (_aiComposeStyles) {\n\t\treturn *_aiComposeStyles;\n\t}\n\t_aiComposeStyles = getValue(u\"ai_compose_styles\"_q, [&](const auto &value) {\n\t\treturn value.match([&](const MTPDjsonArray &data) {\n\t\t\tauto result = std::vector<AiComposeStyle>();\n\t\t\tresult.reserve(data.vvalue().v.size());\n\t\t\tfor (const auto &entry : data.vvalue().v) {\n\t\t\t\tif (entry.type() != mtpc_jsonArray) {\n\t\t\t\t\treturn std::vector<AiComposeStyle>();\n\t\t\t\t}\n\t\t\t\tconst auto &list = entry.c_jsonArray().vvalue().v;\n\t\t\t\tif (list.size() < 3\n\t\t\t\t\t|| (list[0].type() != mtpc_jsonString)\n\t\t\t\t\t|| (list[1].type() != mtpc_jsonString)\n\t\t\t\t\t|| (list[2].type() != mtpc_jsonString)) {\n\t\t\t\t\treturn std::vector<AiComposeStyle>();\n\t\t\t\t}\n\t\t\t\tresult.push_back({\n\t\t\t\t\t.type = qs(list[0].c_jsonString().vvalue()),\n\t\t\t\t\t.emojiId = qs(list[1].c_jsonString().vvalue()).toULongLong(),\n\t\t\t\t\t.title = qs(list[2].c_jsonString().vvalue()),\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn result;\n\t\t}, [&](const auto &) {\n\t\t\treturn std::vector<AiComposeStyle>();\n\t\t});\n\t});\n\treturn *_aiComposeStyles;\n}\n\nauto AppConfig::groupCallColorings() const -> std::vector<StarsColoring> {\n\tif (!_groupCallColorings.empty()) {\n\t\treturn _groupCallColorings;\n\t}\n\tconst auto key = u\"stars_groupcall_message_limits\"_q;\n\tgetValue(key, [&](const MTPJSONValue &value) {\n\t\tvalue.match([&](const MTPDjsonArray &data) {\n\t\t\tconst auto &list = data.vvalue().v;\n\t\t\t_groupCallColorings.reserve(list.size());\n\t\t\tfor (const auto &entry : list) {\n\t\t\t\tentry.match([&](const MTPDjsonObject &data) {\n\t\t\t\t\tauto &entry = _groupCallColorings.emplace_back();\n\t\t\t\t\tconst auto &fields = data.vvalue().v;\n\t\t\t\t\tfor (const auto &field : fields) {\n\t\t\t\t\t\tconst auto &key = field.data().vkey().v;\n\t\t\t\t\t\tconst auto &value = field.data().vvalue();\n\t\t\t\t\t\tconst auto error = [&] {\n\t\t\t\t\t\t\tLOG((\"API Error: Incorrect value for %1.\"\n\t\t\t\t\t\t\t\t).arg(qs(key)));\n\t\t\t\t\t\t\treturn std::nullopt;\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst auto number = [&]() -> std::optional<int> {\n\t\t\t\t\t\t\tif (value.type() != mtpc_jsonNumber) {\n\t\t\t\t\t\t\t\treturn error();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst auto &data = value.c_jsonNumber();\n\t\t\t\t\t\t\tconst auto v = base::SafeRound(data.vvalue().v);\n\t\t\t\t\t\t\tif (v < 0) {\n\t\t\t\t\t\t\t\treturn error();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn int(v);\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst auto color = [&]() -> std::optional<int> {\n\t\t\t\t\t\t\tif (value.type() != mtpc_jsonString) {\n\t\t\t\t\t\t\t\treturn error();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst auto &data = value.c_jsonString();\n\t\t\t\t\t\t\tconst auto text = data.vvalue().v;\n\t\t\t\t\t\t\tif (text.size() != 6) {\n\t\t\t\t\t\t\t\treturn error();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst auto digit = [&](int i) {\n\t\t\t\t\t\t\t\tExpects(i >= 0 && i < 6);\n\n\t\t\t\t\t\t\t\tconst auto ch = text[i];\n\t\t\t\t\t\t\t\treturn (ch >= '0' && ch <= '9')\n\t\t\t\t\t\t\t\t\t? int(ch - '0')\n\t\t\t\t\t\t\t\t\t: (ch >= 'A' && ch <= 'F')\n\t\t\t\t\t\t\t\t\t? (int(ch - 'A') + 10)\n\t\t\t\t\t\t\t\t\t: (ch >= 'a' && ch <= 'f')\n\t\t\t\t\t\t\t\t\t? (int(ch - 'a') + 10)\n\t\t\t\t\t\t\t\t\t: std::optional<int>();\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tconst auto component = [&](int i) {\n\t\t\t\t\t\t\t\tconst auto a = digit(i), b = digit(i + 1);\n\t\t\t\t\t\t\t\treturn (a && b)\n\t\t\t\t\t\t\t\t\t? ((*a) * 16 + (*b))\n\t\t\t\t\t\t\t\t\t: std::optional<int>();\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tconst auto r = component(0);\n\t\t\t\t\t\t\tconst auto g = component(2);\n\t\t\t\t\t\t\tconst auto b = component(4);\n\t\t\t\t\t\t\tif (!r || !g || !b) {\n\t\t\t\t\t\t\t\treturn error();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn ((*r) << 16) | ((*g) << 8) | (*b);\n\t\t\t\t\t\t};\n\t\t\t\t\t\tif (key == \"stars\"_q) {\n\t\t\t\t\t\t\tif (const auto n = number()) {\n\t\t\t\t\t\t\t\tentry.fromStars = *n;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\treturn _groupCallColorings.pop_back();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (key == \"pin_period\"_q) {\n\t\t\t\t\t\t\tif (const auto n = number()) {\n\t\t\t\t\t\t\t\tentry.secondsPin = *n;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\treturn _groupCallColorings.pop_back();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (key == \"text_length_max\"_q) {\n\t\t\t\t\t\t\tif (const auto n = number()) {\n\t\t\t\t\t\t\t\tentry.charactersMax = *n;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\treturn _groupCallColorings.pop_back();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (key == \"emoji_max\"_q) {\n\t\t\t\t\t\t\tif (const auto n = number()) {\n\t\t\t\t\t\t\t\tentry.emojiLimit = *n;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\treturn _groupCallColorings.pop_back();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (key == \"color1\"_q) {\n\t\t\t\t\t\t\tif (const auto c = color()) {\n\t\t\t\t\t\t\t\tentry.bgLight = *c;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\treturn _groupCallColorings.pop_back();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (key == \"color_bg\"_q) {\n\t\t\t\t\t\t\tif (const auto c = color()) {\n\t\t\t\t\t\t\t\tentry.bgDark = *c;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\treturn _groupCallColorings.pop_back();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}, [](const auto &) {});\n\t\t\t}\n\t\t}, [](const auto &) {});\n\t});\n\tif (_groupCallColorings.empty()) {\n\t\t_groupCallColorings = std::vector<StarsColoring>{\n\t\t\t{ 0x955CDB, 0x49079B, 0, 30, 30, 0 }, // purple\n\t\t\t{ 0x955CDB, 0x49079B, 10, 60, 60, 1 }, // still purple\n\t\t\t{ 0x46A3EB, 0x00508E, 50, 120, 80, 2 }, // blue\n\t\t\t{ 0x40A920, 0x176200, 100, 300, 110, 3 }, // green\n\t\t\t{ 0xE29A09, 0x9A3E00, 250, 600, 150, 4 }, // yellow\n\t\t\t{ 0xED771E, 0x9B3100, 500, 900, 200, 7 }, // orange\n\t\t\t{ 0xE14542, 0x8B0503, 2'000, 1800, 280, 10 }, // red\n\t\t\t{ 0x596473, 0x252C36, 10'000, 3600, 400, 20 }, // silver\n\t\t};\n\t} else {\n\t\tconst auto proj = &StarsColoring::fromStars;\n\t\tif (!ranges::contains(_groupCallColorings, 0, proj)) {\n\t\t\t_groupCallColorings.insert(\n\t\t\t\tbegin(_groupCallColorings),\n\t\t\t\t{ 0x955CDB, 0x49079B, 0, 30, 30, 0 });\n\t\t}\n\t\tranges::sort(_groupCallColorings, ranges::less(), proj);\n\t}\n\treturn _groupCallColorings;\n}\n\nstd::vector<std::vector<int>> AppConfig::craftAttributePermilles() const {\n\treturn get<std::vector<std::vector<int>>>(\n\t\tu\"stargifts_craft_attribute_permilles\"_q,\n\t\t{\n\t\t\t{ 90 },\n\t\t\t{ 80, 200 },\n\t\t\t{ 70, 190, 460 },\n\t\t\t{ 60, 180, 450, 1000 },\n\t\t});\n}\n\n} // namespace Main\n"
  },
  {
    "path": "Telegram/SourceFiles/main/main_app_config.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n#include \"base/algorithm.h\"\n\nnamespace Ui {\nstruct ColorIndicesCompressed;\n} // namespace Ui\n\nnamespace Calls::Group::Ui {\nusing namespace ::Ui;\nstruct StarsColoring;\n} // namespace Calls::Group::Ui\n\nnamespace Main {\n\nclass Account;\n\nclass AppConfig final {\npublic:\n\texplicit AppConfig(not_null<Account*> account);\n\t~AppConfig();\n\n\tvoid start();\n\n\ttemplate <typename Type>\n\t[[nodiscard]] Type get(const QString &key, Type fallback) const {\n\t\tif constexpr (std::is_same_v<Type, double>) {\n\t\t\treturn getDouble(key, fallback);\n\t\t} else if constexpr (std::is_same_v<Type, int>) {\n\t\t\treturn int(base::SafeRound(getDouble(key, double(fallback))));\n\t\t} else if constexpr (std::is_same_v<Type, int64>) {\n\t\t\treturn int64(base::SafeRound(getDouble(key, double(fallback))));\n\t\t} else if constexpr (std::is_same_v<Type, QString>) {\n\t\t\treturn getString(key, fallback);\n\t\t} else if constexpr (std::is_same_v<Type, std::vector<QString>>) {\n\t\t\treturn getStringArray(key, std::move(fallback));\n\t\t} else if constexpr (\n\t\t\t\tstd::is_same_v<Type, base::flat_map<QString, QString>>) {\n\t\t\treturn getStringMap(key, std::move(fallback));\n\t\t} else if constexpr (std::is_same_v<Type, std::vector<int>>) {\n\t\t\treturn getIntArray(key, std::move(fallback));\n\t\t} else if constexpr (std::is_same_v<Type, std::vector<std::vector<int>>>) {\n\t\t\treturn getIntIntArray(key, std::move(fallback));\n\t\t} else if constexpr (std::is_same_v<Type, std::vector<int64>>) {\n\t\t\treturn getInt64Array(key, std::move(fallback));\n\t\t} else if constexpr (std::is_same_v<Type, bool>) {\n\t\t\treturn getBool(key, fallback);\n\t\t}\n\t}\n\n\t[[nodiscard]] rpl::producer<> refreshed() const;\n\t[[nodiscard]] rpl::producer<> value() const;\n\n\t[[nodiscard]] bool newRequirePremiumFree() const;\n\n\t[[nodiscard]] auto ignoredRestrictionReasons() const\n\t\t-> const std::vector<QString> & {\n\t\treturn _ignoreRestrictionReasons;\n\t}\n\t[[nodiscard]] auto ignoredRestrictionReasonsChanges() const {\n\t\treturn _ignoreRestrictionChanges.events();\n\t}\n\n\t[[nodiscard]] int quoteLengthMax() const;\n\t[[nodiscard]] int stargiftConvertPeriodMax() const;\n\t[[nodiscard]] int noForwardsRequestExpirePeriod() const;\n\n\t[[nodiscard]] const std::vector<QString> &startRefPrefixes();\n\t[[nodiscard]] bool starrefSetupAllowed() const;\n\t[[nodiscard]] bool starrefJoinAllowed() const;\n\t[[nodiscard]] int starrefCommissionMin() const;\n\t[[nodiscard]] int starrefCommissionMax() const;\n\n\t[[nodiscard]] int starsWithdrawMax() const;\n\t[[nodiscard]] float64 starsWithdrawRate() const;\n\t[[nodiscard]] float64 currencyWithdrawRate() const;\n\t[[nodiscard]] float64 starsSellRate() const;\n\t[[nodiscard]] float64 currencySellRate() const;\n\t[[nodiscard]] bool paidMessagesAvailable() const;\n\t[[nodiscard]] int paidMessageStarsMax() const;\n\t[[nodiscard]] int paidMessageCommission() const;\n\t[[nodiscard]] int paidMessageChannelStarsDefault() const;\n\n\t[[nodiscard]] int pinnedGiftsLimit() const;\n\t[[nodiscard]] int giftCollectionsLimit() const;\n\t[[nodiscard]] int giftCollectionGiftsLimit() const;\n\n\t[[nodiscard]] bool callsDisabledForSession() const;\n\t[[nodiscard]] int confcallSizeLimit() const;\n\t[[nodiscard]] bool confcallPrioritizeVP8() const;\n\n\t[[nodiscard]] int giftResaleStarsMin() const;\n\t[[nodiscard]] int giftResaleStarsMax() const;\n\t[[nodiscard]] int giftResaleStarsThousandths() const;\n\t[[nodiscard]] int64 giftResaleNanoTonMin() const;\n\t[[nodiscard]] int64 giftResaleNanoTonMax() const;\n\t[[nodiscard]] int giftResaleNanoTonThousandths() const;\n\n\t[[nodiscard]] int pollOptionsLimit() const;\n\t[[nodiscard]] int pollAnswerDeletePeriod() const;\n\t[[nodiscard]] int todoListItemsLimit() const;\n\t[[nodiscard]] int todoListTitleLimit() const;\n\t[[nodiscard]] int todoListItemTextLimit() const;\n\n\t[[nodiscard]] int suggestedPostCommissionStars() const;\n\t[[nodiscard]] int suggestedPostCommissionTon() const;\n\t[[nodiscard]] int suggestedPostStarsMin() const;\n\t[[nodiscard]] int suggestedPostStarsMax() const;\n\t[[nodiscard]] int64 suggestedPostNanoTonMin() const;\n\t[[nodiscard]] int64 suggestedPostNanoTonMax() const;\n\t[[nodiscard]] int suggestedPostDelayMin() const;\n\t[[nodiscard]] int suggestedPostDelayMax() const;\n\t[[nodiscard]] TimeId suggestedPostAgeMin() const;\n\n\t[[nodiscard]] bool ageVerifyNeeded() const;\n\t[[nodiscard]] QString ageVerifyCountry() const;\n\t[[nodiscard]] int ageVerifyMinAge() const;\n\t[[nodiscard]] QString ageVerifyBotUsername() const;\n\n\t[[nodiscard]] int storiesAlbumsLimit() const;\n\t[[nodiscard]] int storiesAlbumLimit() const;\n\n\t[[nodiscard]] int groupCallMessageLengthLimit() const;\n\t[[nodiscard]] TimeId groupCallMessageTTL() const;\n\n\t[[nodiscard]] int passkeysAccountPasskeysMax() const;\n\t[[nodiscard]] bool settingsDisplayPasskeys() const;\n\n\t[[nodiscard]] int64 stakeDiceNanoTonMin() const;\n\t[[nodiscard]] int64 stakeDiceNanoTonMax() const;\n\t[[nodiscard]] std::vector<int64> stakeDiceNanoTonSuggested() const;\n\n\tstruct AiComposeStyle {\n\t\tQString type;\n\t\tDocumentId emojiId = 0;\n\t\tQString title;\n\t};\n\t[[nodiscard]] std::vector<AiComposeStyle> aiComposeStyles() const;\n\n\tusing StarsColoring = Calls::Group::Ui::StarsColoring;\n\t[[nodiscard]] std::vector<StarsColoring> groupCallColorings() const;\n\n\t[[nodiscard]] std::vector<std::vector<int>> craftAttributePermilles() const;\n\n\tvoid refresh(bool force = false);\n\nprivate:\n\tvoid refreshDelayed();\n\n\ttemplate <typename Extractor>\n\t[[nodiscard]] auto getValue(\n\t\tconst QString &key,\n\t\tExtractor &&extractor) const;\n\n\t[[nodiscard]] bool getBool(\n\t\tconst QString &key,\n\t\tbool fallback) const;\n\t[[nodiscard]] double getDouble(\n\t\tconst QString &key,\n\t\tdouble fallback) const;\n\t[[nodiscard]] QString getString(\n\t\tconst QString &key,\n\t\tconst QString &fallback) const;\n\t[[nodiscard]] std::vector<QString> getStringArray(\n\t\tconst QString &key,\n\t\tstd::vector<QString> &&fallback) const;\n\t[[nodiscard]] base::flat_map<QString, QString> getStringMap(\n\t\tconst QString &key,\n\t\tbase::flat_map<QString, QString> &&fallback) const;\n\t[[nodiscard]] std::vector<int> getIntArray(\n\t\tconst QString &key,\n\t\tstd::vector<int> &&fallback) const;\n\t[[nodiscard]] std::vector<std::vector<int>> getIntIntArray(\n\t\tconst QString &key,\n\t\tstd::vector<std::vector<int>> &&fallback) const;\n\t[[nodiscard]] std::vector<int64> getInt64Array(\n\t\tconst QString &key,\n\t\tstd::vector<int64> &&fallback) const;\n\n\tvoid updateIgnoredRestrictionReasons(std::vector<QString> was);\n\n\tconst not_null<Account*> _account;\n\tstd::optional<MTP::Sender> _api;\n\tmtpRequestId _requestId = 0;\n\tint32 _hash = 0;\n\tbool _pendingRefresh = false;\n\tbase::flat_map<QString, MTPJSONValue> _data;\n\trpl::event_stream<> _refreshed;\n\n\tstd::vector<QString> _ignoreRestrictionReasons;\n\trpl::event_stream<std::vector<QString>> _ignoreRestrictionChanges;\n\n\tstd::vector<QString> _startRefPrefixes;\n\n\tmutable std::optional<std::vector<AiComposeStyle>> _aiComposeStyles;\n\tmutable std::vector<StarsColoring> _groupCallColorings;\n\n\tcrl::time _lastFrozenRefresh = 0;\n\trpl::lifetime _frozenTrackLifetime;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Main\n"
  },
  {
    "path": "Telegram/SourceFiles/main/main_app_config_values.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"main/main_app_config_values.h\"\n\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n\nnamespace AppConfig {\n\nstd::optional<QString> FragmentLink(not_null<Main::Session*> session) {\n\tusing Strings = std::vector<QString>;\n\tconst auto domains = session->appConfig().get<Strings>(\n\t\tu\"whitelisted_domains\"_q,\n\t\tstd::vector<QString>());\n\tconst auto proj = [&, domain = u\"fragment\"_q](const QString &p) {\n\t\treturn p.contains(domain);\n\t};\n\tconst auto it = ranges::find_if(domains, proj);\n\treturn (it == end(domains))\n\t\t? std::nullopt\n\t\t: std::make_optional<QString>(*it);\n}\n\n} // namespace AppConfig\n"
  },
  {
    "path": "Telegram/SourceFiles/main/main_app_config_values.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace AppConfig {\n\n[[nodiscard]] std::optional<QString> FragmentLink(not_null<Main::Session*>);\n\n} // namespace AppConfig\n"
  },
  {
    "path": "Telegram/SourceFiles/main/main_domain.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"main/main_domain.h\"\n\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/shortcuts.h\"\n#include \"core/crash_reports.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_user.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"storage/storage_domain.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/localstorage.h\"\n#include \"export/export_settings.h\"\n#include \"window/notifications_manager.h\"\n#include \"window/window_controller.h\"\n#include \"data/data_peer_values.h\" // Data::AmPremiumValue.\n\nnamespace Main {\n\nDomain::Domain(const QString &dataName)\n: _dataName(dataName)\n, _local(std::make_unique<Storage::Domain>(this, dataName)) {\n\t_active.changes(\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t// In case we had a legacy passcoded app we start settings here.\n\t\tCore::App().startSettingsAndBackground();\n\n\t\tcrl::on_main(this, [=] {\n\t\t\tCore::App().notifications().createManager();\n\t\t});\n\t}, _lifetime);\n\n\t_active.changes(\n\t) | rpl::map([](Main::Account *account) {\n\t\treturn account ? account->sessionValue() : rpl::never<Session*>();\n\t\t}) | rpl::flatten_latest(\n\t) | rpl::map([](Main::Session *session) {\n\t\treturn session\n\t\t\t? session->changes().peerFlagsValue(\n\t\t\t\tsession->user(),\n\t\t\t\tData::PeerUpdate::Flag::Username)\n\t\t\t: rpl::never<Data::PeerUpdate>();\n\t}) | rpl::flatten_latest(\n\t) | rpl::on_next([](const Data::PeerUpdate &update) {\n\t\tCrashReports::SetAnnotation(\"Username\", update.peer->username());\n\t}, _lifetime);\n}\n\nDomain::~Domain() = default;\n\nbool Domain::started() const {\n\treturn !_accounts.empty();\n}\n\nStorage::StartResult Domain::start(const QByteArray &passcode) {\n\tExpects(!started());\n\n\tconst auto result = _local->start(passcode);\n\tif (result == Storage::StartResult::Success) {\n\t\tactivateAfterStarting();\n\t\tcrl::on_main(&Core::App(), [=] { suggestExportIfNeeded(); });\n\t} else {\n\t\tAssert(!started());\n\t}\n\treturn result;\n}\n\nvoid Domain::finish() {\n\t_accountToActivate = -1;\n\t_active.reset(nullptr);\n\tbase::take(_accounts);\n}\n\nvoid Domain::suggestExportIfNeeded() {\n\tExpects(started());\n\n\tfor (const auto &[index, account] : _accounts) {\n\t\tif (const auto session = account->maybeSession()) {\n\t\t\tconst auto settings = session->local().readExportSettings();\n\t\t\tif (const auto availableAt = settings.availableAt) {\n\t\t\t\tsession->data().suggestStartExport(availableAt);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Domain::accountAddedInStorage(AccountWithIndex accountWithIndex) {\n\tExpects(accountWithIndex.account != nullptr);\n\n\tfor (const auto &[index, _] : _accounts) {\n\t\tif (index == accountWithIndex.index) {\n\t\t\tUnexpected(\"Repeated account index.\");\n\t\t}\n\t}\n\t_accounts.push_back(std::move(accountWithIndex));\n}\n\nvoid Domain::activateFromStorage(int index) {\n\t_accountToActivate = index;\n}\n\nint Domain::activeForStorage() const {\n\treturn _accountToActivate;\n}\n\nvoid Domain::resetWithForgottenPasscode() {\n\tif (_accounts.empty()) {\n\t\t_local->startFromScratch();\n\t\tactivateAfterStarting();\n\t} else {\n\t\tfor (const auto &[index, account] : _accounts) {\n\t\t\taccount->logOut();\n\t\t}\n\t}\n}\n\nvoid Domain::activateAfterStarting() {\n\tExpects(started());\n\n\tauto toActivate = _accounts.front().account.get();\n\tfor (const auto &[index, account] : _accounts) {\n\t\tif (index == _accountToActivate) {\n\t\t\ttoActivate = account.get();\n\t\t}\n\t\twatchSession(account.get());\n\t}\n\n\tactivate(toActivate);\n\tremovePasscodeIfEmpty();\n}\n\nconst std::vector<Domain::AccountWithIndex> &Domain::accounts() const {\n\treturn _accounts;\n}\n\nstd::vector<not_null<Account*>> Domain::orderedAccounts() const {\n\tconst auto order = Core::App().settings().accountsOrder();\n\tauto accounts = ranges::views::all(\n\t\t_accounts\n\t) | ranges::views::transform([](const Domain::AccountWithIndex &a) {\n\t\treturn not_null{ a.account.get() };\n\t}) | ranges::to_vector;\n\tranges::stable_sort(accounts, [&](\n\t\t\tnot_null<Account*> a,\n\t\t\tnot_null<Account*> b) {\n\t\tconst auto aIt = a->sessionExists()\n\t\t\t? ranges::find(order, a->session().uniqueId())\n\t\t\t: end(order);\n\t\tconst auto bIt = b->sessionExists()\n\t\t\t? ranges::find(order, b->session().uniqueId())\n\t\t\t: end(order);\n\t\treturn aIt < bIt;\n\t});\n\treturn accounts;\n}\n\nrpl::producer<> Domain::accountsChanges() const {\n\treturn _accountsChanges.events();\n}\n\nAccount *Domain::maybeLastOrSomeAuthedAccount() {\n\tauto result = (Account*)nullptr;\n\tfor (const auto &[index, account] : _accounts) {\n\t\tif (!account->sessionExists()) {\n\t\t\tcontinue;\n\t\t} else if (index == _lastActiveIndex) {\n\t\t\treturn account.get();\n\t\t} else if (!result) {\n\t\t\tresult = account.get();\n\t\t}\n\t}\n\treturn result;\n}\n\nint Domain::accountsAuthedCount() const {\n\tauto result = 0;\n\tfor (const auto &[index, account] : _accounts) {\n\t\tif (account->sessionExists()) {\n\t\t\t++result;\n\t\t}\n\t}\n\treturn result;\n}\n\nrpl::producer<Account*> Domain::activeValue() const {\n\treturn _active.value();\n}\n\nAccount &Domain::active() const {\n\tExpects(!_accounts.empty());\n\n\tEnsures(_active.current() != nullptr);\n\treturn *_active.current();\n}\n\nrpl::producer<not_null<Account*>> Domain::activeChanges() const {\n\treturn _active.changes() | rpl::map([](Account *value) {\n\t\treturn not_null{ value };\n\t});\n}\n\nrpl::producer<Session*> Domain::activeSessionChanges() const {\n\treturn _activeSessions.events();\n}\n\nrpl::producer<Session*> Domain::activeSessionValue() const {\n\tconst auto current = _accounts.empty()\n\t\t? nullptr\n\t\t: active().maybeSession();\n\treturn rpl::single(current) | rpl::then(_activeSessions.events());\n}\n\nint Domain::unreadBadge() const {\n\treturn _unreadBadge;\n}\n\nbool Domain::unreadBadgeMuted() const {\n\treturn _unreadBadgeMuted;\n}\n\nrpl::producer<> Domain::unreadBadgeChanges() const {\n\treturn _unreadBadgeChanges.events();\n}\n\nvoid Domain::notifyUnreadBadgeChanged() {\n\tfor (const auto &[index, account] : _accounts) {\n\t\tif (const auto session = account->maybeSession()) {\n\t\t\tsession->data().notifyUnreadBadgeChanged();\n\t\t}\n\t}\n}\n\nvoid Domain::updateUnreadBadge() {\n\t_unreadBadge = 0;\n\t_unreadBadgeMuted = true;\n\tfor (const auto &[index, account] : _accounts) {\n\t\tif (const auto session = account->maybeSession()) {\n\t\t\tconst auto data = &session->data();\n\t\t\t_unreadBadge += data->unreadBadge();\n\t\t\tif (!data->unreadBadgeMuted()) {\n\t\t\t\t_unreadBadgeMuted = false;\n\t\t\t}\n\t\t}\n\t}\n\t_unreadBadgeChanges.fire({});\n}\n\nvoid Domain::scheduleUpdateUnreadBadge() {\n\tif (_unreadBadgeUpdateScheduled) {\n\t\treturn;\n\t}\n\t_unreadBadgeUpdateScheduled = true;\n\tCore::App().postponeCall(crl::guard(&Core::App(), [=] {\n\t\t_unreadBadgeUpdateScheduled = false;\n\t\tupdateUnreadBadge();\n\t}));\n}\n\nnot_null<Main::Account*> Domain::add(MTP::Environment environment) {\n\tExpects(started());\n\tExpects(_accounts.size() < kPremiumMaxAccounts);\n\n\tstatic const auto cloneConfig = [](const MTP::Config &config) {\n\t\treturn std::make_unique<MTP::Config>(config);\n\t};\n\tauto mainDcId = MTP::Instance::Fields::kNotSetMainDc;\n\tconst auto accountConfig = [&](not_null<Account*> account) {\n\t\tmainDcId = account->mtp().mainDcId();\n\t\treturn cloneConfig(account->mtp().config());\n\t};\n\tauto config = [&] {\n\t\tif (_active.current()->mtp().environment() == environment) {\n\t\t\treturn accountConfig(_active.current());\n\t\t}\n\t\tfor (const auto &[index, account] : _accounts) {\n\t\t\tif (account->mtp().environment() == environment) {\n\t\t\t\treturn accountConfig(account.get());\n\t\t\t}\n\t\t}\n\t\treturn (environment == MTP::Environment::Production)\n\t\t\t? cloneConfig(Core::App().fallbackProductionConfig())\n\t\t\t: std::make_unique<MTP::Config>(environment);\n\t}();\n\tauto index = 0;\n\twhile (ranges::contains(_accounts, index, &AccountWithIndex::index)) {\n\t\t++index;\n\t}\n\t_accounts.push_back(AccountWithIndex{\n\t\t.index = index,\n\t\t.account = std::make_unique<Account>(this, _dataName, index)\n\t});\n\tconst auto account = _accounts.back().account.get();\n\taccount->setMtpMainDcId(mainDcId);\n\t_local->startAdded(account, std::move(config));\n\twatchSession(account);\n\t_accountsChanges.fire({});\n\n\tauto &settings = Core::App().settings();\n\tif (_accounts.size() == 2 && !settings.mainMenuAccountsShown()) {\n\t\tsettings.setMainMenuAccountsShown(true);\n\t\tCore::App().saveSettingsDelayed();\n\t}\n\n\treturn account;\n}\n\nvoid Domain::addActivated(MTP::Environment environment, bool newWindow) {\n\tconst auto added = [&](not_null<Main::Account*> account) {\n\t\tif (newWindow) {\n\t\t\tCore::App().ensureSeparateWindowFor(account);\n\t\t} else if (const auto window = Core::App().separateWindowFor(\n\t\t\t\taccount)) {\n\t\t\twindow->activate();\n\t\t} else {\n\t\t\tactivate(account);\n\t\t}\n\t};\n\tif (accounts().size() < maxAccounts()) {\n\t\tadded(add(environment));\n\t} else {\n\t\tfor (auto &[index, account] : accounts()) {\n\t\t\tif (!account->sessionExists()\n\t\t\t\t&& account->mtp().environment() == environment) {\n\t\t\t\tadded(account.get());\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Domain::watchSession(not_null<Account*> account) {\n\taccount->sessionValue(\n\t) | rpl::filter([=](Session *session) {\n\t\treturn session != nullptr;\n\t}) | rpl::on_next([=](Session *session) {\n\t\tsession->data().unreadBadgeChanges(\n\t\t) | rpl::on_next([=] {\n\t\t\tscheduleUpdateUnreadBadge();\n\t\t}, session->lifetime());\n\n\t\tData::AmPremiumValue(\n\t\t\tsession\n\t\t) | rpl::on_next([=] {\n\t\t\t_lastMaxAccounts = maxAccounts();\n\t\t}, session->lifetime());\n\t}, account->lifetime());\n\n\taccount->sessionChanges(\n\t) | rpl::filter([=](Session *session) {\n\t\treturn !session;\n\t}) | rpl::on_next([=] {\n\t\tscheduleUpdateUnreadBadge();\n\t\tcloseAccountWindows(account);\n\t\tcrl::on_main(&Core::App(), [=] {\n\t\t\tremoveRedundantAccounts();\n\t\t});\n\t}, account->lifetime());\n}\n\nvoid Domain::closeAccountWindows(not_null<Main::Account*> account) {\n\tauto another = (Main::Account*)nullptr;\n\tfor (auto i = _accounts.begin(); i != _accounts.end(); ++i) {\n\t\tconst auto other = not_null(i->account.get());\n\t\tif (other == account) {\n\t\t\tcontinue;\n\t\t} else if (Core::App().separateWindowFor(other)) {\n\t\t\tconst auto that = Core::App().separateWindowFor(account);\n\t\t\tif (that) {\n\t\t\t\tthat->close();\n\t\t\t}\n\t\t} else if (!another\n\t\t\t|| (other->sessionExists() && !another->sessionExists())) {\n\t\t\tanother = other;\n\t\t}\n\t}\n\tif (another) {\n\t\tactivate(another);\n\t}\n}\n\nbool Domain::removePasscodeIfEmpty() {\n\tif (_accounts.size() != 1 || _active.current()->sessionExists()) {\n\t\treturn false;\n\t}\n\tLocal::reset();\n\n\t// We completely logged out, remove the passcode if it was there.\n\tif (Core::App().passcodeLocked()) {\n\t\tCore::App().unlockPasscode();\n\t}\n\tif (!_local->hasLocalPasscode()) {\n\t\treturn false;\n\t}\n\t_local->setPasscode(QByteArray());\n\tCore::App().settings().setSystemUnlockEnabled(false);\n\tCore::App().saveSettingsDelayed();\n\treturn true;\n}\n\nvoid Domain::removeRedundantAccounts() {\n\tExpects(started());\n\n\tconst auto was = _accounts.size();\n\tfor (auto i = _accounts.begin(); i != _accounts.end();) {\n\t\tif (Core::App().separateWindowFor(not_null(i->account.get()))\n\t\t\t|| i->account->sessionExists()) {\n\t\t\t++i;\n\t\t\tcontinue;\n\t\t}\n\t\tcheckForLastProductionConfig(i->account.get());\n\t\ti = _accounts.erase(i);\n\t}\n\n\tif (!removePasscodeIfEmpty() && _accounts.size() != was) {\n\t\tscheduleWriteAccounts();\n\t\t_accountsChanges.fire({});\n\t}\n}\n\nvoid Domain::checkForLastProductionConfig(\n\t\tnot_null<Main::Account*> account) {\n\tconst auto mtp = &account->mtp();\n\tif (mtp->environment() != MTP::Environment::Production) {\n\t\treturn;\n\t}\n\tfor (const auto &[index, other] : _accounts) {\n\t\tif (other.get() != account\n\t\t\t&& other->mtp().environment() == MTP::Environment::Production) {\n\t\t\treturn;\n\t\t}\n\t}\n\tCore::App().refreshFallbackProductionConfig(mtp->config());\n}\n\nvoid Domain::maybeActivate(not_null<Main::Account*> account) {\n\tif (Core::App().separateWindowFor(account)) {\n\t\tactivate(account);\n\t} else {\n\t\tCore::App().preventOrInvoke(crl::guard(account, [=] {\n\t\t\tactivate(account);\n\t\t}));\n\t}\n}\n\nvoid Domain::activate(not_null<Main::Account*> account) {\n\tif (const auto window = Core::App().separateWindowFor(account)) {\n\t\twindow->activate();\n\t}\n\tif (_active.current() == account.get()) {\n\t\treturn;\n\t}\n\tconst auto i = ranges::find(_accounts, account.get(), [](\n\t\t\tconst AccountWithIndex &value) {\n\t\treturn value.account.get();\n\t});\n\tAssert(i != end(_accounts));\n\tconst auto changed = (_accountToActivate != i->index);\n\tauto wasAuthed = false;\n\n\t_activeLifetime.destroy();\n\tif (_active.current()) {\n\t\t_lastActiveIndex = _accountToActivate;\n\t\twasAuthed = _active.current()->sessionExists();\n\t}\n\t_accountToActivate = i->index;\n\t_active = account.get();\n\t_active.current()->sessionValue(\n\t) | rpl::start_to_stream(_activeSessions, _activeLifetime);\n\n\tif (changed) {\n\t\tif (wasAuthed) {\n\t\t\tscheduleWriteAccounts();\n\t\t} else {\n\t\t\tcrl::on_main(&Core::App(), [=] {\n\t\t\t\tremoveRedundantAccounts();\n\t\t\t});\n\t\t}\n\t}\n}\n\nvoid Domain::scheduleWriteAccounts() {\n\tif (_writeAccountsScheduled) {\n\t\treturn;\n\t}\n\t_writeAccountsScheduled = true;\n\tcrl::on_main(&Core::App(), [=] {\n\t\t_writeAccountsScheduled = false;\n\t\t_local->writeAccounts();\n\t});\n}\n\nint Domain::maxAccounts() const {\n\tconst auto premiumCount = ranges::count_if(accounts(), [](\n\t\t\tconst Main::Domain::AccountWithIndex &d) {\n\t\treturn d.account->sessionExists()\n\t\t\t&& (d.account->session().premium()\n\t\t\t\t|| d.account->session().isTestMode());\n\t});\n\treturn std::min(int(premiumCount) + kMaxAccounts, kPremiumMaxAccounts);\n}\n\nrpl::producer<int> Domain::maxAccountsChanges() const {\n\treturn _lastMaxAccounts.changes();\n}\n\n} // namespace Main\n"
  },
  {
    "path": "Telegram/SourceFiles/main/main_domain.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n\nnamespace Storage {\nclass Domain;\nenum class StartResult : uchar;\n} // namespace Storage\n\nnamespace MTP {\nenum class Environment : uchar;\n} // namespace MTP\n\nnamespace Main {\n\nclass Account;\nclass Session;\n\nclass Domain final : public base::has_weak_ptr {\npublic:\n\tstruct AccountWithIndex {\n\t\tint index = 0;\n\t\tstd::unique_ptr<Account> account;\n\t};\n\n\tstatic constexpr auto kMaxAccounts = 30;\n\tstatic constexpr auto kPremiumMaxAccounts = kMaxAccounts + 3;\n\n\texplicit Domain(const QString &dataName);\n\t~Domain();\n\n\t[[nodiscard]] bool started() const;\n\t[[nodiscard]] Storage::StartResult start(const QByteArray &passcode);\n\tvoid resetWithForgottenPasscode();\n\tvoid finish();\n\n\t[[nodiscard]] int maxAccounts() const;\n\t[[nodiscard]] rpl::producer<int> maxAccountsChanges() const;\n\n\t[[nodiscard]] Storage::Domain &local() const {\n\t\treturn *_local;\n\t}\n\n\t[[nodiscard]] auto accounts() const\n\t\t-> const std::vector<AccountWithIndex> &;\n\t[[nodiscard]] std::vector<not_null<Account*>> orderedAccounts() const;\n\t[[nodiscard]] rpl::producer<Account*> activeValue() const;\n\t[[nodiscard]] rpl::producer<> accountsChanges() const;\n\t[[nodiscard]] Account *maybeLastOrSomeAuthedAccount();\n\t[[nodiscard]] int accountsAuthedCount() const;\n\n\t// Expects(started());\n\t[[nodiscard]] Account &active() const;\n\t[[nodiscard]] rpl::producer<not_null<Account*>> activeChanges() const;\n\n\t[[nodiscard]] rpl::producer<Session*> activeSessionValue() const;\n\t[[nodiscard]] rpl::producer<Session*> activeSessionChanges() const;\n\n\t[[nodiscard]] int unreadBadge() const;\n\t[[nodiscard]] bool unreadBadgeMuted() const;\n\t[[nodiscard]] rpl::producer<> unreadBadgeChanges() const;\n\tvoid notifyUnreadBadgeChanged();\n\n\t[[nodiscard]] not_null<Main::Account*> add(MTP::Environment environment);\n\tvoid maybeActivate(not_null<Main::Account*> account);\n\tvoid activate(not_null<Main::Account*> account);\n\tvoid addActivated(MTP::Environment environment, bool newWindow = false);\n\n\t// Interface for Storage::Domain.\n\tvoid accountAddedInStorage(AccountWithIndex accountWithIndex);\n\tvoid activateFromStorage(int index);\n\t[[nodiscard]] int activeForStorage() const;\n\nprivate:\n\tvoid activateAfterStarting();\n\tvoid closeAccountWindows(not_null<Main::Account*> account);\n\tbool removePasscodeIfEmpty();\n\tvoid removeRedundantAccounts();\n\tvoid watchSession(not_null<Account*> account);\n\tvoid scheduleWriteAccounts();\n\tvoid checkForLastProductionConfig(not_null<Main::Account*> account);\n\tvoid updateUnreadBadge();\n\tvoid scheduleUpdateUnreadBadge();\n\tvoid suggestExportIfNeeded();\n\n\tconst QString _dataName;\n\tconst std::unique_ptr<Storage::Domain> _local;\n\n\tstd::vector<AccountWithIndex> _accounts;\n\trpl::event_stream<> _accountsChanges;\n\trpl::variable<Account*> _active = nullptr;\n\tint _accountToActivate = -1;\n\tint _lastActiveIndex = -1;\n\tbool _writeAccountsScheduled = false;\n\n\trpl::event_stream<Session*> _activeSessions;\n\n\trpl::event_stream<> _unreadBadgeChanges;\n\tint _unreadBadge = 0;\n\tbool _unreadBadgeMuted = true;\n\tbool _unreadBadgeUpdateScheduled = false;\n\n\trpl::variable<int> _lastMaxAccounts;\n\n\trpl::lifetime _activeLifetime;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Main\n"
  },
  {
    "path": "Telegram/SourceFiles/main/main_session.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"main/main_session.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_peer_colors.h\"\n#include \"api/api_updates.h\"\n#include \"api/api_user_privacy.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session_settings.h\"\n#include \"main/main_app_config.h\"\n#include \"main/session/send_as_peers.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"chat_helpers/stickers_dice_pack.h\"\n#include \"chat_helpers/stickers_gift_box_pack.h\"\n#include \"history/view/reactions/history_view_reactions_strip.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n#include \"storage/file_download.h\"\n#include \"storage/download_manager_mtproto.h\"\n#include \"storage/file_upload.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/storage_facade.h\"\n#include \"data/components/credits.h\"\n#include \"data/components/factchecks.h\"\n#include \"data/components/gift_auctions.h\"\n#include \"data/components/location_pickers.h\"\n#include \"data/components/passkeys.h\"\n#include \"data/components/promo_suggestions.h\"\n#include \"data/components/recent_peers.h\"\n#include \"data/components/recent_shared_media_gifts.h\"\n#include \"data/components/scheduled_messages.h\"\n#include \"data/components/sponsored_messages.h\"\n#include \"data/components/top_peers.h\"\n#include \"settings/settings_faq_suggestions.h\"\n#include \"settings/settings_recent_searches.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_user.h\"\n#include \"data/data_download_manager.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_lock_widgets.h\"\n#include \"base/unixtime.h\"\n#include \"calls/calls_instance.h\"\n#include \"support/support_helper.h\"\n#include \"lang/lang_keys.h\"\n#include \"core/application.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"styles/style_layers.h\"\n\n#ifndef TDESKTOP_DISABLE_SPELLCHECK\n#include \"chat_helpers/spellchecker_common.h\"\n#endif // TDESKTOP_DISABLE_SPELLCHECK\n\nnamespace Main {\nnamespace {\n\nconstexpr auto kTmpPasswordReserveTime = TimeId(10);\n\n[[nodiscard]] QString ValidatedInternalLinksDomain(\n\t\tnot_null<const Session*> session) {\n\t// This domain should start with 'http[s]://' and end with '/'.\n\t// Like 'https://telegram.me/' or 'https://t.me/'.\n\tconst auto &domain = session->serverConfig().internalLinksDomain;\n\tconst auto prefixes = {\n\t\tu\"https://\"_q,\n\t\tu\"http://\"_q,\n\t};\n\tfor (const auto &prefix : prefixes) {\n\t\tif (domain.startsWith(prefix, Qt::CaseInsensitive)) {\n\t\t\treturn domain.endsWith('/')\n\t\t\t\t? domain\n\t\t\t\t: MTP::ConfigFields(\n\t\t\t\t\tsession->mtp().environment()\n\t\t\t\t).internalLinksDomain;\n\t\t}\n\t}\n\treturn MTP::ConfigFields(\n\t\tsession->mtp().environment()\n\t).internalLinksDomain;\n}\n\n} // namespace\n\nSession::Session(\n\tnot_null<Account*> account,\n\tconst MTPUser &user,\n\tstd::unique_ptr<SessionSettings> settings)\n: _userId(user.c_user().vid())\n, _account(account)\n, _settings(std::move(settings))\n, _changes(std::make_unique<Data::Changes>(this))\n, _api(std::make_unique<ApiWrap>(this))\n, _updates(std::make_unique<Api::Updates>(this))\n, _sendProgressManager(std::make_unique<Api::SendProgressManager>(this))\n, _downloader(std::make_unique<Storage::DownloadManagerMtproto>(_api.get()))\n, _uploader(std::make_unique<Storage::Uploader>(_api.get()))\n, _storage(std::make_unique<Storage::Facade>())\n, _data(std::make_unique<Data::Session>(this))\n, _user(_data->processUser(user))\n, _emojiStickersPack(std::make_unique<Stickers::EmojiPack>(this))\n, _diceStickersPacks(std::make_unique<Stickers::DicePacks>(this))\n, _giftBoxStickersPacks(std::make_unique<Stickers::GiftBoxPack>(this))\n, _sendAsPeers(std::make_unique<SendAsPeers>(this))\n, _attachWebView(std::make_unique<InlineBots::AttachWebView>(this))\n, _recentPeers(std::make_unique<Data::RecentPeers>(this))\n, _recentSharedGifts(std::make_unique<Data::RecentSharedMediaGifts>(this))\n, _giftAuctions(std::make_unique<Data::GiftAuctions>(this))\n, _scheduledMessages(std::make_unique<Data::ScheduledMessages>(this))\n, _sponsoredMessages(std::make_unique<Data::SponsoredMessages>(this))\n, _topPeers(std::make_unique<Data::TopPeers>(this, Data::TopPeerType::Chat))\n, _topBotApps(\n\tstd::make_unique<Data::TopPeers>(this, Data::TopPeerType::BotApp))\n, _factchecks(std::make_unique<Data::Factchecks>(this))\n, _locationPickers(std::make_unique<Data::LocationPickers>())\n, _credits(std::make_unique<Data::Credits>(this))\n, _promoSuggestions(std::make_unique<Data::PromoSuggestions>(this, [=] {\n\tusing State = Data::SetupEmailState;\n\tif (_promoSuggestions->setupEmailState() == State::Setup\n\t\t|| _promoSuggestions->setupEmailState() == State::SetupNoSkip) {\n\t\tif (_settings->setupEmailState() == State::Setup\n\t\t\t|| _settings->setupEmailState() == State::SetupNoSkip) {\n\t\t\tcrl::on_main([=] {\n\t\t\t// base::call_delayed(5000, [=] {\n\t\t\t\tCore::App().lockBySetupEmail();\n\t\t\t});\n\t\t\tconst auto unlockLifetime = std::make_shared<rpl::lifetime>();\n\t\t\t_promoSuggestions->setupEmailStateValue(\n\t\t\t) | rpl::filter([](Data::SetupEmailState s) {\n\t\t\t\treturn s == Data::SetupEmailState::None;\n\t\t\t}) | rpl::take(1) | rpl::on_next(crl::guard(this, [=] {\n\t\t\t\tCore::App().unlockSetupEmail();\n\t\t\t\t_settings->setSetupEmailState(State::None);\n\t\t\t\tsaveSettingsDelayed(200);\n\t\t\t\tunlockLifetime->destroy();\n\t\t\t}), *unlockLifetime);\n\t\t} else {\n\t\t\t_settings->setSetupEmailState(\n\t\t\t\t_promoSuggestions->setupEmailState());\n\t\t\tsaveSettingsDelayed(200);\n\t\t}\n\t} else {\n\t\tif (_settings->setupEmailState() == State::Setup\n\t\t\t|| _settings->setupEmailState() == State::SetupNoSkip) {\n\t\t\t_settings->setSetupEmailState(State::None);\n\t\t\tsaveSettingsDelayed(200);\n\t\t}\n\t}\n}))\n, _passkeys(std::make_unique<Data::Passkeys>(this))\n, _faqSuggestions(std::make_unique<Settings::FaqSuggestions>(this))\n, _recentSettingsSearches(std::make_unique<Settings::RecentSearches>(this))\n, _cachedReactionIconFactory(std::make_unique<ReactionIconFactory>())\n, _supportHelper(Support::Helper::Create(this))\n, _fastButtonsBots(std::make_unique<Support::FastButtonsBots>(this))\n, _saveSettingsTimer([=] { saveSettings(); }) {\n\tExpects(_settings != nullptr);\n\n\t_api->requestTermsUpdate();\n\t_api->requestFullPeer(_user);\n\n\t_api->instance().setUserPhone(_user->phone());\n\n\t// Load current userpic and keep it loaded.\n\t_user->loadUserpic();\n\tchanges().peerFlagsValue(\n\t\t_user,\n\t\tData::PeerUpdate::Flag::Photo\n\t) | rpl::on_next([=] {\n\t\tauto view = Ui::PeerUserpicView{ .cloud = _selfUserpicView };\n\t\t[[maybe_unused]] const auto image = _user->userpicCloudImage(view);\n\t\t_selfUserpicView = view.cloud;\n\t}, lifetime());\n\n\tcrl::on_main_queue(this, { [=] {\n\t\tusing Flag = Data::PeerUpdate::Flag;\n\t\tchanges().peerUpdates(\n\t\t\t_user,\n\t\t\tFlag::Name\n\t\t\t| Flag::Username\n\t\t\t| Flag::Photo\n\t\t\t| Flag::About\n\t\t\t| Flag::PhoneNumber\n\t\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\t\tlocal().writeSelf();\n\n\t\t\tif (update.flags & Flag::PhoneNumber) {\n\t\t\t\tconst auto phone = _user->phone();\n\t\t\t\t_api->instance().setUserPhone(phone);\n\t\t\t\tif (!phone.isEmpty()) {\n\t\t\t\t\t_api->instance().requestConfig();\n\t\t\t\t}\n\t\t\t}\n\t\t}, _lifetime);\n\n\t\tif (_settings->hadLegacyCallsPeerToPeerNobody()) {\n\t\t\tapi().userPrivacy().save(\n\t\t\t\tApi::UserPrivacy::Key::CallsPeer2Peer,\n\t\t\t\tApi::UserPrivacy::Rule{\n\t\t\t\t\t.option = Api::UserPrivacy::Option::Nobody\n\t\t\t\t});\n\t\t\tsaveSettingsDelayed();\n\t\t}\n\t}, [=] {\n\t\t// Storage::Account uses Main::Account::session() in those methods.\n\t\t// So they can't be called during Main::Session construction.\n\t\t//\n\t\t// They are deferred via crl::on_main which fires after the\n\t\t// constructor returns and _session is set.\n\t\t//\n\t\t// Steps are chained via crl::on_main so that paint events\n\t\t// can be processed between heavy file reads.\n\t\tlocal().readInstalledStickers();\n\t}, [=] {\n\t\tlocal().readInstalledMasks();\n\t}, [=] {\n\t\tlocal().readInstalledCustomEmoji();\n\t}, [=] {\n\t\tlocal().readFeaturedStickers();\n\t}, [=] {\n\t\tlocal().readFeaturedCustomEmoji();\n\t}, [=] {\n\t\tlocal().readRecentStickers();\n\t\tlocal().readRecentMasks();\n\t\tlocal().readFavedStickers();\n\t\tlocal().readSavedGifs();\n\t}, [=] {\n\t\tdata().stickers().notifyUpdated(Data::StickersType::Stickers);\n\t\tdata().stickers().notifyUpdated(Data::StickersType::Masks);\n\t\tdata().stickers().notifyUpdated(Data::StickersType::Emoji);\n\t\tdata().stickers().notifySavedGifsUpdated();\n\t\tDEBUG_LOG((\"Init: Account stored data load finished.\"));\n\t} }).dispatch();\n\n#ifndef TDESKTOP_DISABLE_SPELLCHECK\n\tSpellchecker::Start(this);\n#endif // TDESKTOP_DISABLE_SPELLCHECK\n\n\t_api->requestNotifySettings(MTP_inputNotifyUsers());\n\t_api->requestNotifySettings(MTP_inputNotifyChats());\n\t_api->requestNotifySettings(MTP_inputNotifyBroadcasts());\n\n\tCore::App().downloadManager().trackSession(this);\n\n\tappConfig().value(\n\t) | rpl::on_next([=] {\n\t\tappConfigRefreshed();\n\t}, _lifetime);\n}\n\nvoid Session::appConfigRefreshed() {\n\tconst auto &config = appConfig();\n\n\t_frozen = FreezeInfo{\n\t\t.since = config.get<int>(u\"freeze_since_date\"_q, 0),\n\t\t.until = config.get<int>(u\"freeze_until_date\"_q, 0),\n\t\t.appealUrl = config.get<QString>(u\"freeze_appeal_url\"_q, QString()),\n\t};\n\n#ifndef OS_MAC_STORE\n\t_premiumPossible = !config.get<bool>(\n\t\tu\"premium_purchase_blocked\"_q,\n\t\ttrue);\n#endif // OS_MAC_STORE\n}\n\nvoid Session::setTmpPassword(const QByteArray &password, TimeId validUntil) {\n\tif (_tmpPassword.isEmpty() || validUntil > _tmpPasswordValidUntil) {\n\t\t_tmpPassword = password;\n\t\t_tmpPasswordValidUntil = validUntil;\n\t}\n}\n\nQByteArray Session::validTmpPassword() const {\n\treturn (_tmpPasswordValidUntil\n\t\t>= base::unixtime::now() + kTmpPasswordReserveTime)\n\t\t? _tmpPassword\n\t\t: QByteArray();\n}\n\n// Can be called only right before ~Session.\nvoid Session::finishLogout() {\n\tunlockTerms();\n\tdata().clear();\n\tdata().clearLocalStorage();\n}\n\nSession::~Session() {\n\tunlockTerms();\n\tdata().clear();\n\tClickHandler::clearActive();\n\tClickHandler::unpressed();\n}\n\nAccount &Session::account() const {\n\treturn *_account;\n}\n\nStorage::Account &Session::local() const {\n\treturn _account->local();\n}\n\nDomain &Session::domain() const {\n\treturn _account->domain();\n}\n\nStorage::Domain &Session::domainLocal() const {\n\treturn _account->domainLocal();\n}\n\nAppConfig &Session::appConfig() const {\n\treturn _account->appConfig();\n}\n\nvoid Session::notifyDownloaderTaskFinished() {\n\tdownloader().notifyTaskFinished();\n}\n\nrpl::producer<> Session::downloaderTaskFinished() const {\n\treturn downloader().taskFinished();\n}\n\nbool Session::premium() const {\n\treturn _user->isPremium();\n}\n\nbool Session::premiumPossible() const {\n\treturn premium() || premiumCanBuy();\n}\n\nbool Session::premiumBadgesShown() const {\n\treturn supportMode() || premiumPossible();\n}\n\nrpl::producer<bool> Session::premiumPossibleValue() const {\n\tusing namespace rpl::mappers;\n\n\tauto premium = _user->flagsValue(\n\t) | rpl::filter([=](UserData::Flags::Change change) {\n\t\treturn (change.diff & UserDataFlag::Premium);\n\t}) | rpl::map([=] {\n\t\treturn _user->isPremium();\n\t});\n\treturn rpl::combine(\n\t\tstd::move(premium),\n\t\t_premiumPossible.value(),\n\t\t_1 || _2);\n}\n\nbool Session::premiumCanBuy() const {\n\treturn _premiumPossible.current();\n}\n\nbool Session::isTestMode() const {\n\treturn mtp().isTestMode();\n}\n\nuint64 Session::uniqueId() const {\n\t// See also Account::willHaveSessionUniqueId.\n\treturn userId().bare\n\t\t| (isTestMode() ? 0x0100'0000'0000'0000ULL : 0ULL);\n}\n\nUserId Session::userId() const {\n\treturn _userId;\n}\n\nPeerId Session::userPeerId() const {\n\treturn _userId;\n}\n\nbool Session::validateSelf(UserId id) {\n\tif (id != userId()) {\n\t\tLOG((\"Auth Error: wrong self user received.\"));\n\t\tcrl::on_main(this, [=] { _account->logOut(); });\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nvoid Session::saveSettings() {\n\tlocal().writeSessionSettings();\n}\n\nvoid Session::saveSettingsDelayed(crl::time delay) {\n\t_saveSettingsTimer.callOnce(delay);\n}\n\nvoid Session::saveSettingsNowIfNeeded() {\n\tif (_saveSettingsTimer.isActive()) {\n\t\t_saveSettingsTimer.cancel();\n\t\tsaveSettings();\n\t}\n}\n\nMTP::DcId Session::mainDcId() const {\n\treturn _account->mtp().mainDcId();\n}\n\nMTP::Instance &Session::mtp() const {\n\treturn _account->mtp();\n}\n\nconst MTP::ConfigFields &Session::serverConfig() const {\n\treturn _account->mtp().configValues();\n}\n\nvoid Session::lockByTerms(const Window::TermsLock &data) {\n\tif (!_termsLock || *_termsLock != data) {\n\t\t_termsLock = std::make_unique<Window::TermsLock>(data);\n\t\t_termsLockChanges.fire(true);\n\t}\n}\n\nvoid Session::unlockTerms() {\n\tif (_termsLock) {\n\t\t_termsLock = nullptr;\n\t\t_termsLockChanges.fire(false);\n\t}\n}\n\nvoid Session::termsDeleteNow() {\n\tapi().request(MTPaccount_DeleteAccount(\n\t\tMTP_flags(0),\n\t\tMTP_string(\"Decline ToS update\"),\n\t\tMTPInputCheckPasswordSRP()\n\t)).send();\n}\n\nstd::optional<Window::TermsLock> Session::termsLocked() const {\n\treturn _termsLock ? base::make_optional(*_termsLock) : std::nullopt;\n}\n\nrpl::producer<bool> Session::termsLockChanges() const {\n\treturn _termsLockChanges.events();\n}\n\nrpl::producer<bool> Session::termsLockValue() const {\n\treturn rpl::single(\n\t\t_termsLock != nullptr\n\t) | rpl::then(termsLockChanges());\n}\n\nQString Session::createInternalLink(const QString &query) const {\n\treturn createInternalLink(TextWithEntities{ .text = query }).text;\n}\n\nQString Session::createInternalLinkFull(const QString &query) const {\n\treturn createInternalLinkFull(TextWithEntities{ .text = query }).text;\n}\n\nTextWithEntities Session::createInternalLink(\n\t\tconst TextWithEntities &query) const {\n\tconst auto result = createInternalLinkFull(query);\n\tconst auto prefixes = {\n\t\tu\"https://\"_q,\n\t\tu\"http://\"_q,\n\t};\n\tfor (auto &prefix : prefixes) {\n\t\tif (result.text.startsWith(prefix, Qt::CaseInsensitive)) {\n\t\t\treturn Ui::Text::Mid(result, prefix.size());\n\t\t}\n\t}\n\tLOG((\"Warning: bad internal url '%1'\").arg(result.text));\n\treturn result;\n}\n\nTextWithEntities Session::createInternalLinkFull(\n\t\tTextWithEntities query) const {\n\treturn TextWithEntities::Simple(ValidatedInternalLinksDomain(this))\n\t\t.append(std::move(query));\n}\n\nbool Session::supportMode() const {\n\treturn (_supportHelper != nullptr);\n}\n\nSupport::Helper &Session::supportHelper() const {\n\tExpects(supportMode());\n\n\treturn *_supportHelper;\n}\n\nSupport::Templates& Session::supportTemplates() const {\n\treturn supportHelper().templates();\n}\n\nSupport::FastButtonsBots &Session::fastButtonsBots() const {\n\treturn *_fastButtonsBots;\n}\n\nFreezeInfo Session::frozen() const {\n\treturn _frozen.current();\n}\n\nrpl::producer<FreezeInfo> Session::frozenValue() const {\n\treturn _frozen.value();\n}\n\nvoid Session::addWindow(not_null<Window::SessionController*> controller) {\n\t_windows.emplace(controller);\n\tcontroller->lifetime().add([=] {\n\t\t_windows.remove(controller);\n\t});\n\tupdates().addActiveChat(controller->activeChatChanges(\n\t) | rpl::map([=](Dialogs::Key chat) {\n\t\treturn chat.peer();\n\t}) | rpl::distinct_until_changed());\n}\n\nbool Session::uploadsInProgress() const {\n\treturn !!_uploader->currentUploadId();\n}\n\nvoid Session::uploadsStopWithConfirmation(Fn<void()> done) {\n\tconst auto id = _uploader->currentUploadId();\n\tconst auto message = data().message(id);\n\tconst auto exists = (message != nullptr);\n\tconst auto window = message\n\t\t? Core::App().windowFor(message->history()->peer)\n\t\t: Core::App().activePrimaryWindow();\n\tif (!window) {\n\t\tdone();\n\t\treturn;\n\t}\n\tauto box = Box([=](not_null<Ui::GenericBox*> box) {\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox.get(),\n\t\t\t\ttr::lng_upload_sure_stop(),\n\t\t\t\tst::boxLabel),\n\t\t\tst::boxPadding + QMargins(0, 0, 0, st::boxPadding.bottom()));\n\t\tbox->setStyle(st::defaultBox);\n\t\tbox->addButton(tr::lng_selected_upload_stop(), [=] {\n\t\t\tbox->closeBox();\n\n\t\t\tuploadsStop();\n\t\t\tif (done) {\n\t\t\t\tdone();\n\t\t\t}\n\t\t}, st::attentionBoxButton);\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t\tif (exists) {\n\t\t\tbox->addLeftButton(tr::lng_upload_show_file(), [=] {\n\t\t\t\tbox->closeBox();\n\n\t\t\t\tif (const auto item = data().message(id)) {\n\t\t\t\t\tif (const auto window = tryResolveWindow()) {\n\t\t\t\t\t\twindow->showMessage(item);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t});\n\twindow->show(std::move(box));\n\twindow->activate();\n}\n\nvoid Session::uploadsStop() {\n\t_uploader->cancelAll();\n}\n\nauto Session::windows() const\n-> const base::flat_set<not_null<Window::SessionController*>> & {\n\treturn _windows;\n}\n\nWindow::SessionController *Session::tryResolveWindow(\n\t\tPeerData *forPeer) const {\n\tif (forPeer) {\n\t\tauto primary = (Window::SessionController*)nullptr;\n\t\tfor (const auto &window : _windows) {\n\t\t\tconst auto thread = window->windowId().thread;\n\t\t\tif (thread && thread->peer() == forPeer) {\n\t\t\t\treturn window;\n\t\t\t} else if (window->isPrimary()) {\n\t\t\t\tprimary = window;\n\t\t\t}\n\t\t}\n\t\tif (primary) {\n\t\t\treturn primary;\n\t\t}\n\t}\n\tif (_windows.empty() || forPeer) {\n\t\tdomain().activate(_account);\n\t\tif (_windows.empty()) {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\tfor (const auto &window : _windows) {\n\t\tif (window->isPrimary()) {\n\t\t\treturn window;\n\t\t}\n\t}\n\treturn _windows.front();\n}\n\nauto Session::colorIndicesValue()\n-> rpl::producer<Ui::ColorIndicesCompressed> {\n\treturn api().peerColors().indicesValue();\n}\n\n} // namespace Main\n"
  },
  {
    "path": "Telegram/SourceFiles/main/main_session.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <rpl/filter.h>\n#include <rpl/variable.h>\n#include \"base/timer.h\"\n\nclass ApiWrap;\n\nnamespace Api {\nclass Updates;\nclass SendProgressManager;\n} // namespace Api\n\nnamespace MTP {\nclass Instance;\nstruct ConfigFields;\n} // namespace MTP\n\nnamespace Support {\nclass Helper;\nclass Templates;\nclass FastButtonsBots;\n} // namespace Support\n\nnamespace Data {\nclass Session;\nclass Changes;\nclass GiftAuctions;\nclass RecentPeers;\nclass RecentSharedMediaGifts;\nclass ScheduledMessages;\nclass SponsoredMessages;\nclass TopPeers;\nclass Factchecks;\nclass LocationPickers;\nclass Credits;\nclass PromoSuggestions;\nclass Passkeys;\n} // namespace Data\n\nnamespace Settings {\nclass FaqSuggestions;\nclass RecentSearches;\n} // namespace Settings\n\nnamespace HistoryView::Reactions {\nclass CachedIconFactory;\n} // namespace HistoryView::Reactions\n\nnamespace Storage {\nclass DownloadManagerMtproto;\nclass Uploader;\nclass Facade;\nclass Account;\nclass Domain;\n} // namespace Storage\n\nnamespace Window {\nclass SessionController;\nstruct TermsLock;\n} // namespace Window\n\nnamespace Stickers {\nclass EmojiPack;\nclass DicePacks;\nclass GiftBoxPack;\n} // namespace Stickers;\n\nnamespace InlineBots {\nclass AttachWebView;\n} // namespace InlineBots\n\nnamespace Ui {\nstruct ColorIndicesCompressed;\n} // namespace Ui\n\nnamespace Main {\n\nclass Account;\nclass AppConfig;\nclass Domain;\nclass SessionSettings;\nclass SendAsPeers;\n\nstruct FreezeInfo {\n\tTimeId since = 0;\n\tTimeId until = 0;\n\tQString appealUrl;\n\n\texplicit operator bool() const {\n\t\treturn since != 0;\n\t}\n\tfriend inline bool operator==(\n\t\tconst FreezeInfo &,\n\t\tconst FreezeInfo &) = default;\n};\n\nclass Session final : public base::has_weak_ptr {\npublic:\n\tSession(\n\t\tnot_null<Account*> account,\n\t\tconst MTPUser &user,\n\t\tstd::unique_ptr<SessionSettings> settings);\n\t~Session();\n\n\tSession(const Session &other) = delete;\n\tSession &operator=(const Session &other) = delete;\n\n\t[[nodiscard]] Account &account() const;\n\t[[nodiscard]] Storage::Account &local() const;\n\t[[nodiscard]] Domain &domain() const;\n\t[[nodiscard]] Storage::Domain &domainLocal() const;\n\n\t[[nodiscard]] AppConfig &appConfig() const;\n\n\t[[nodiscard]] bool premium() const;\n\t[[nodiscard]] bool premiumPossible() const;\n\t[[nodiscard]] rpl::producer<bool> premiumPossibleValue() const;\n\t[[nodiscard]] bool premiumBadgesShown() const;\n\t[[nodiscard]] bool premiumCanBuy() const;\n\n\t[[nodiscard]] bool isTestMode() const;\n\t[[nodiscard]] uint64 uniqueId() const; // userId() with TestDC shift.\n\t[[nodiscard]] UserId userId() const;\n\t[[nodiscard]] PeerId userPeerId() const;\n\t[[nodiscard]] not_null<UserData*> user() const {\n\t\treturn _user;\n\t}\n\tbool validateSelf(UserId id);\n\n\t[[nodiscard]] Data::Changes &changes() const {\n\t\treturn *_changes;\n\t}\n\t[[nodiscard]] Data::RecentPeers &recentPeers() const {\n\t\treturn *_recentPeers;\n\t}\n\t[[nodiscard]] Data::RecentSharedMediaGifts &recentSharedGifts() const {\n\t\treturn *_recentSharedGifts;\n\t}\n\t[[nodiscard]] Data::GiftAuctions &giftAuctions() const {\n\t\treturn *_giftAuctions;\n\t}\n\t[[nodiscard]] Data::SponsoredMessages &sponsoredMessages() const {\n\t\treturn *_sponsoredMessages;\n\t}\n\t[[nodiscard]] Data::ScheduledMessages &scheduledMessages() const {\n\t\treturn *_scheduledMessages;\n\t}\n\t[[nodiscard]] Data::TopPeers &topPeers() const {\n\t\treturn *_topPeers;\n\t}\n\t[[nodiscard]] Data::TopPeers &topBotApps() const {\n\t\treturn *_topBotApps;\n\t}\n\t[[nodiscard]] Data::Factchecks &factchecks() const {\n\t\treturn *_factchecks;\n\t}\n\t[[nodiscard]] Data::LocationPickers &locationPickers() const {\n\t\treturn *_locationPickers;\n\t}\n\t[[nodiscard]] Data::Credits &credits() const {\n\t\treturn *_credits;\n\t}\n\t[[nodiscard]] Api::Updates &updates() const {\n\t\treturn *_updates;\n\t}\n\t[[nodiscard]] Api::SendProgressManager &sendProgressManager() const {\n\t\treturn *_sendProgressManager;\n\t}\n\t[[nodiscard]] Storage::DownloadManagerMtproto &downloader() const {\n\t\treturn *_downloader;\n\t}\n\t[[nodiscard]] Storage::Uploader &uploader() const {\n\t\treturn *_uploader;\n\t}\n\t[[nodiscard]] Storage::Facade &storage() const {\n\t\treturn *_storage;\n\t}\n\t[[nodiscard]] Stickers::EmojiPack &emojiStickersPack() const {\n\t\treturn *_emojiStickersPack;\n\t}\n\t[[nodiscard]] Stickers::DicePacks &diceStickersPacks() const {\n\t\treturn *_diceStickersPacks;\n\t}\n\t[[nodiscard]] Stickers::GiftBoxPack &giftBoxStickersPacks() const {\n\t\treturn *_giftBoxStickersPacks;\n\t}\n\t[[nodiscard]] Data::Session &data() const {\n\t\treturn *_data;\n\t}\n\t[[nodiscard]] SessionSettings &settings() const {\n\t\treturn *_settings;\n\t}\n\t[[nodiscard]] SendAsPeers &sendAsPeers() const {\n\t\treturn *_sendAsPeers;\n\t}\n\t[[nodiscard]] InlineBots::AttachWebView &attachWebView() const {\n\t\treturn *_attachWebView;\n\t}\n\t[[nodiscard]] Data::PromoSuggestions &promoSuggestions() const {\n\t\treturn *_promoSuggestions;\n\t}\n\t[[nodiscard]] Data::Passkeys &passkeys() const {\n\t\treturn *_passkeys;\n\t}\n\t[[nodiscard]] Settings::FaqSuggestions &faqSuggestions() const {\n\t\treturn *_faqSuggestions;\n\t}\n\t[[nodiscard]] Settings::RecentSearches &recentSettingsSearches() const {\n\t\treturn *_recentSettingsSearches;\n\t}\n\t[[nodiscard]] auto cachedReactionIconFactory() const\n\t-> HistoryView::Reactions::CachedIconFactory & {\n\t\treturn *_cachedReactionIconFactory;\n\t}\n\n\tvoid saveSettings();\n\tvoid saveSettingsDelayed(crl::time delay = kDefaultSaveDelay);\n\tvoid saveSettingsNowIfNeeded();\n\n\tvoid addWindow(not_null<Window::SessionController*> controller);\n\t[[nodiscard]] auto windows() const\n\t\t-> const base::flat_set<not_null<Window::SessionController*>> &;\n\t[[nodiscard]] Window::SessionController *tryResolveWindow(\n\t\tPeerData *forPeer = nullptr) const;\n\n\t// Shortcuts.\n\tvoid notifyDownloaderTaskFinished();\n\t[[nodiscard]] rpl::producer<> downloaderTaskFinished() const;\n\t[[nodiscard]] MTP::DcId mainDcId() const;\n\t[[nodiscard]] MTP::Instance &mtp() const;\n\t[[nodiscard]] const MTP::ConfigFields &serverConfig() const;\n\t[[nodiscard]] ApiWrap &api() {\n\t\treturn *_api;\n\t}\n\n\t// Terms lock.\n\tvoid lockByTerms(const Window::TermsLock &data);\n\tvoid unlockTerms();\n\tvoid termsDeleteNow();\n\t[[nodiscard]] std::optional<Window::TermsLock> termsLocked() const;\n\trpl::producer<bool> termsLockChanges() const;\n\trpl::producer<bool> termsLockValue() const;\n\n\t[[nodiscard]] QString createInternalLink(const QString &query) const;\n\t[[nodiscard]] QString createInternalLinkFull(const QString &query) const;\n\t[[nodiscard]] TextWithEntities createInternalLink(\n\t\tconst TextWithEntities &query) const;\n\t[[nodiscard]] TextWithEntities createInternalLinkFull(\n\t\tTextWithEntities query) const;\n\n\tvoid setTmpPassword(const QByteArray &password, TimeId validUntil);\n\t[[nodiscard]] QByteArray validTmpPassword() const;\n\n\t// Can be called only right before ~Session.\n\tvoid finishLogout();\n\n\t// Uploads cancel with confirmation.\n\t[[nodiscard]] bool uploadsInProgress() const;\n\tvoid uploadsStopWithConfirmation(Fn<void()> done);\n\tvoid uploadsStop();\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\n\t[[nodiscard]] bool supportMode() const;\n\t[[nodiscard]] Support::Helper &supportHelper() const;\n\t[[nodiscard]] Support::Templates &supportTemplates() const;\n\t[[nodiscard]] Support::FastButtonsBots &fastButtonsBots() const;\n\n\t[[nodiscard]] FreezeInfo frozen() const;\n\t[[nodiscard]] rpl::producer<FreezeInfo> frozenValue() const;\n\n\t[[nodiscard]] auto colorIndicesValue()\n\t\t-> rpl::producer<Ui::ColorIndicesCompressed>;\n\nprivate:\n\tstatic constexpr auto kDefaultSaveDelay = crl::time(1000);\n\n\tvoid appConfigRefreshed();\n\n\tconst UserId _userId;\n\tconst not_null<Account*> _account;\n\n\tconst std::unique_ptr<SessionSettings> _settings;\n\tconst std::unique_ptr<Data::Changes> _changes;\n\tconst std::unique_ptr<ApiWrap> _api;\n\tconst std::unique_ptr<Api::Updates> _updates;\n\tconst std::unique_ptr<Api::SendProgressManager> _sendProgressManager;\n\tconst std::unique_ptr<Storage::DownloadManagerMtproto> _downloader;\n\tconst std::unique_ptr<Storage::Uploader> _uploader;\n\tconst std::unique_ptr<Storage::Facade> _storage;\n\n\t// _data depends on _downloader / _uploader.\n\tconst std::unique_ptr<Data::Session> _data;\n\tconst not_null<UserData*> _user;\n\n\t// _emojiStickersPack depends on _data.\n\tconst std::unique_ptr<Stickers::EmojiPack> _emojiStickersPack;\n\tconst std::unique_ptr<Stickers::DicePacks> _diceStickersPacks;\n\tconst std::unique_ptr<Stickers::GiftBoxPack> _giftBoxStickersPacks;\n\tconst std::unique_ptr<SendAsPeers> _sendAsPeers;\n\tconst std::unique_ptr<InlineBots::AttachWebView> _attachWebView;\n\tconst std::unique_ptr<Data::RecentPeers> _recentPeers;\n\tconst std::unique_ptr<Data::RecentSharedMediaGifts> _recentSharedGifts;\n\tconst std::unique_ptr<Data::GiftAuctions> _giftAuctions;\n\tconst std::unique_ptr<Data::ScheduledMessages> _scheduledMessages;\n\tconst std::unique_ptr<Data::SponsoredMessages> _sponsoredMessages;\n\tconst std::unique_ptr<Data::TopPeers> _topPeers;\n\tconst std::unique_ptr<Data::TopPeers> _topBotApps;\n\tconst std::unique_ptr<Data::Factchecks> _factchecks;\n\tconst std::unique_ptr<Data::LocationPickers> _locationPickers;\n\tconst std::unique_ptr<Data::Credits> _credits;\n\tconst std::unique_ptr<Data::PromoSuggestions> _promoSuggestions;\n\tconst std::unique_ptr<Data::Passkeys> _passkeys;\n\tconst std::unique_ptr<Settings::FaqSuggestions> _faqSuggestions;\n\tconst std::unique_ptr<Settings::RecentSearches> _recentSettingsSearches;\n\n\tusing ReactionIconFactory = HistoryView::Reactions::CachedIconFactory;\n\tconst std::unique_ptr<ReactionIconFactory> _cachedReactionIconFactory;\n\n\tconst std::unique_ptr<Support::Helper> _supportHelper;\n\tconst std::unique_ptr<Support::FastButtonsBots> _fastButtonsBots;\n\n\tstd::shared_ptr<QImage> _selfUserpicView;\n\trpl::variable<bool> _premiumPossible = false;\n\n\trpl::event_stream<bool> _termsLockChanges;\n\tstd::unique_ptr<Window::TermsLock> _termsLock;\n\n\tbase::flat_set<not_null<Window::SessionController*>> _windows;\n\tbase::Timer _saveSettingsTimer;\n\n\trpl::variable<FreezeInfo> _frozen;\n\n\tQByteArray _tmpPassword;\n\tTimeId _tmpPasswordValidUntil = 0;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Main\n"
  },
  {
    "path": "Telegram/SourceFiles/main/main_session_settings.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"main/main_session_settings.h\"\n\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/chat/attach/attach_send_files_way.h\"\n#include \"window/section_widget.h\"\n#include \"support/support_common.h\"\n#include \"storage/serialize_common.h\"\n#include \"boxes/send_files_box.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/components/promo_suggestions.h\"\n\nnamespace Main {\nnamespace {\n\nconstexpr auto kLegacyCallsPeerToPeerNobody = 4;\nconstexpr auto kVersionTag = -1;\nconstexpr auto kVersion = 2;\n\n} // namespace\n\nSessionSettings::SessionSettings()\n: _selectorTab(ChatHelpers::SelectorTab::Emoji)\n, _supportSwitch(Support::SwitchSettings::Next)\n, _setupEmailState(Data::SetupEmailState::None) {\n}\n\nQByteArray SessionSettings::serialize() const {\n\tconst auto autoDownload = _autoDownload.serialize();\n\tauto size = sizeof(qint32) * 4\n\t\t+ _groupStickersSectionHidden.size() * sizeof(quint64)\n\t\t+ sizeof(qint32) * 4\n\t\t+ Serialize::bytearraySize(autoDownload)\n\t\t+ sizeof(qint32) * 11\n\t\t+ (_mutePeriods.size() * sizeof(quint64))\n\t\t+ sizeof(qint32) * 3\n\t\t+ _groupEmojiSectionHidden.size() * sizeof(quint64)\n\t\t+ sizeof(qint32) * 3\n\t\t+ _hiddenPinnedMessages.size() * (sizeof(quint64) * 4)\n\t\t+ sizeof(qint32)\n\t\t+ sizeof(qint32) // _ringtoneDefaultVolumes size\n\t\t+ (_ringtoneDefaultVolumes.size()\n\t\t\t* (0\n\t\t\t\t+ sizeof(uint8_t) * 1 // Data::DefaultNotify\n\t\t\t\t+ sizeof(ushort))) // Volume\n\t\t+ sizeof(qint32) // _ringtoneVolumes size\n\t\t+ (_ringtoneVolumes.size()\n\t\t\t* (0\n\t\t\t\t+ sizeof(quint64) * 3 // ThreadId\n\t\t\t\t+ sizeof(ushort))) // Volume\n\t\t+ sizeof(qint32) // _ratedTranscriptions size\n\t\t+ (_ratedTranscriptions.size() * sizeof(quint64))\n\t\t+ sizeof(qint32); // _unreviewed size\n\tfor (const auto &auth : _unreviewed) {\n\t\tsize += sizeof(quint64) + sizeof(qint32) + sizeof(qint32)\n\t\t\t+ Serialize::stringSize(auth.device)\n\t\t\t+ Serialize::stringSize(auth.location);\n\t}\n\tsize += sizeof(qint32); // _setupEmailState\n\tsize += sizeof(qint32) // _moderateCommonGroups size\n\t\t+ (_moderateCommonGroups.size() * sizeof(qint32));\n\tsize += sizeof(qint32);\n\tsize += sizeof(qint32)\n\t\t+ _subsectionTabsModes.size() * (sizeof(quint64) + sizeof(qint32));\n\tsize += sizeof(qint32); // _phoneNumberHidden\n\n\tauto result = QByteArray();\n\tresult.reserve(size);\n\t{\n\t\tQDataStream stream(&result, QIODevice::WriteOnly);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\t\tstream\n\t\t\t<< qint32(kVersionTag) << qint32(kVersion)\n\t\t\t<< static_cast<qint32>(_selectorTab)\n\t\t\t<< qint32(_groupStickersSectionHidden.size());\n\t\tfor (const auto &peerId : _groupStickersSectionHidden) {\n\t\t\tstream << SerializePeerId(peerId);\n\t\t}\n\t\tstream\n\t\t\t<< qint32(_supportSwitch)\n\t\t\t<< qint32(_supportFixChatsOrder ? 1 : 0)\n\t\t\t<< qint32(_supportTemplatesAutocomplete ? 1 : 0)\n\t\t\t<< qint32(_supportChatsTimeSlice.current())\n\t\t\t<< autoDownload\n\t\t\t<< qint32(_supportAllSearchResults.current() ? 1 : 0)\n\t\t\t<< qint32(_archiveCollapsed.current() ? 1 : 0)\n\t\t\t<< qint32(_archiveInMainMenu.current() ? 1 : 0)\n\t\t\t<< qint32(_skipArchiveInSearch.current() ? 1 : 0)\n\t\t\t<< qint32(0) // old _mediaLastPlaybackPosition.size());\n\t\t\t<< qint32(0) // very very old _hiddenPinnedMessages.size());\n\t\t\t<< qint32(_dialogsFiltersEnabled ? 1 : 0)\n\t\t\t<< qint32(_supportAllSilent ? 1 : 0)\n\t\t\t<< qint32(_photoEditorHintShowsCount)\n\t\t\t<< qint32(0) // very old _hiddenPinnedMessages.size());\n\t\t\t<< qint32(_mutePeriods.size());\n\t\tfor (const auto &period : _mutePeriods) {\n\t\t\tstream << quint64(period);\n\t\t}\n\t\tstream\n\t\t\t<< qint32(0) // old _skipPremiumStickersSet\n\t\t\t<< qint32(0) // old _hiddenPinnedMessages.size());\n\t\t\t<< qint32(_groupEmojiSectionHidden.size());\n\t\tfor (const auto &peerId : _groupEmojiSectionHidden) {\n\t\t\tstream << SerializePeerId(peerId);\n\t\t}\n\t\tstream\n\t\t\t<< qint32(_lastNonPremiumLimitDownload)\n\t\t\t<< qint32(_lastNonPremiumLimitUpload)\n\t\t\t<< qint32(_hiddenPinnedMessages.size());\n\t\tfor (const auto &[key, value] : _hiddenPinnedMessages) {\n\t\t\tstream\n\t\t\t\t<< SerializePeerId(key.peerId)\n\t\t\t\t<< qint64(key.topicRootId.bare)\n\t\t\t\t<< SerializePeerId(key.monoforumPeerId)\n\t\t\t\t<< qint64(value.bare);\n\t\t}\n\t\tstream << qint32(0);\n\t\tstream << qint32(_ringtoneDefaultVolumes.size());\n\t\tfor (const auto &[key, value] : _ringtoneDefaultVolumes) {\n\t\t\tstream << uint8_t(key) << ushort(value);\n\t\t}\n\t\tstream << qint32(_ringtoneVolumes.size());\n\t\tfor (const auto &[key, value] : _ringtoneVolumes) {\n\t\t\tstream\n\t\t\t\t<< SerializePeerId(key.peerId)\n\t\t\t\t<< qint64(key.topicRootId.bare)\n\t\t\t\t<< SerializePeerId(key.monoforumPeerId)\n\t\t\t\t<< ushort(value);\n\t\t}\n\t\tstream << qint32(_ratedTranscriptions.size());\n\t\tfor (const auto &transcriptionId : _ratedTranscriptions) {\n\t\t\tstream << quint64(transcriptionId);\n\t\t}\n\t\tstream << qint32(_unreviewed.size());\n\t\tfor (const auto &auth : _unreviewed) {\n\t\t\tstream\n\t\t\t\t<< quint64(auth.hash)\n\t\t\t\t<< qint32(auth.unconfirmed ? 1 : 0)\n\t\t\t\t<< qint32(auth.date)\n\t\t\t\t<< auth.device\n\t\t\t\t<< auth.location;\n\t\t}\n\t\tstream << qint32(static_cast<int>(_setupEmailState));\n\t\tstream << qint32(_moderateCommonGroups.size());\n\t\tfor (const auto &filterId : _moderateCommonGroups) {\n\t\t\tstream << qint32(filterId);\n\t\t}\n\t\tstream << qint32(_disableSharingBoxShowsCount);\n\t\tstream << qint32(_subsectionTabsModes.size());\n\t\tfor (const auto &[peerId, mode] : _subsectionTabsModes) {\n\t\t\tstream << SerializePeerId(peerId) << qint32(mode);\n\t\t}\n\t\tstream << qint32(_phoneNumberHidden ? 1 : 0);\n\t}\n\n\tEnsures(result.size() == size);\n\treturn result;\n}\n\nvoid SessionSettings::addFromSerialized(const QByteArray &serialized) {\n\tif (serialized.isEmpty()) {\n\t\treturn;\n\t}\n\n\tauto &app = Core::App().settings();\n\n\tQDataStream stream(serialized);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\tqint32 versionTag = 0;\n\tqint32 version = 0;\n\tqint32 selectorTab = static_cast<qint32>(ChatHelpers::SelectorTab::Emoji);\n\tqint32 appLastSeenWarningSeen = app.lastSeenWarningSeen() ? 1 : 0;\n\tqint32 appTabbedSelectorSectionEnabled = 1;\n\tqint32 legacyTabbedSelectorSectionTooltipShown = 0;\n\tqint32 appFloatPlayerColumn = static_cast<qint32>(Window::Column::Second);\n\tqint32 appFloatPlayerCorner = static_cast<qint32>(RectPart::TopRight);\n\tbase::flat_map<QString, QString> appSoundOverrides;\n\tbase::flat_set<PeerId> groupStickersSectionHidden;\n\tbase::flat_set<PeerId> groupEmojiSectionHidden;\n\tqint32 appThirdSectionInfoEnabled = 0;\n\tqint32 legacySmallDialogsList = 0;\n\tfloat64 appDialogsWidthRatio = app.dialogsWidthRatio(false);\n\tint appThirdColumnWidth = app.thirdColumnWidth();\n\tint appThirdSectionExtendedBy = app.thirdSectionExtendedBy();\n\tqint32 appSendFilesWay = app.sendFilesWay().serialize();\n\tqint32 legacyCallsPeerToPeer = qint32(0);\n\tqint32 appSendSubmitWay = static_cast<qint32>(app.sendSubmitWay());\n\tqint32 supportSwitch = static_cast<qint32>(_supportSwitch);\n\tqint32 supportFixChatsOrder = _supportFixChatsOrder ? 1 : 0;\n\tqint32 supportTemplatesAutocomplete = _supportTemplatesAutocomplete ? 1 : 0;\n\tqint32 supportChatsTimeSlice = _supportChatsTimeSlice.current();\n\tqint32 appIncludeMutedCounter = app.includeMutedCounter() ? 1 : 0;\n\tqint32 appCountUnreadMessages = app.countUnreadMessages() ? 1 : 0;\n\tqint32 legacyAppExeLaunchWarning = 1;\n\tQByteArray autoDownload;\n\tqint32 supportAllSearchResults = _supportAllSearchResults.current() ? 1 : 0;\n\tqint32 archiveCollapsed = _archiveCollapsed.current() ? 1 : 0;\n\tqint32 appNotifyAboutPinned = app.notifyAboutPinned() ? 1 : 0;\n\tqint32 archiveInMainMenu = _archiveInMainMenu.current() ? 1 : 0;\n\tqint32 skipArchiveInSearch = _skipArchiveInSearch.current() ? 1 : 0;\n\tqint32 legacyAutoplayGifs = 1;\n\tqint32 appLoopAnimatedStickers = app.loopAnimatedStickers() ? 1 : 0;\n\tqint32 appLargeEmoji = app.largeEmoji() ? 1 : 0;\n\tqint32 appReplaceEmoji = app.replaceEmoji() ? 1 : 0;\n\tqint32 appSuggestEmoji = app.suggestEmoji() ? 1 : 0;\n\tqint32 appSuggestStickersByEmoji = app.suggestStickersByEmoji() ? 1 : 0;\n\tqint32 appSpellcheckerEnabled = app.spellcheckerEnabled() ? 1 : 0;\n\tqint32 appVideoPlaybackSpeed = app.videoPlaybackSpeedSerialized();\n\tQByteArray appVideoPipGeometry = app.videoPipGeometry();\n\tstd::vector<int> appDictionariesEnabled;\n\tqint32 appAutoDownloadDictionaries = app.autoDownloadDictionaries() ? 1 : 0;\n\tbase::flat_map<ThreadId, MsgId> hiddenPinnedMessages;\n\tbase::flat_map<PeerId, qint32> subsectionTabsModes;\n\tqint32 dialogsFiltersEnabled = _dialogsFiltersEnabled ? 1 : 0;\n\tqint32 supportAllSilent = _supportAllSilent ? 1 : 0;\n\tqint32 photoEditorHintShowsCount = _photoEditorHintShowsCount;\n\tstd::vector<TimeId> mutePeriods;\n\tqint32 legacySkipPremiumStickersSet = 0;\n\tqint32 lastNonPremiumLimitDownload = 0;\n\tqint32 lastNonPremiumLimitUpload = 0;\n\tbase::flat_map<Data::DefaultNotify, ushort> ringtoneDefaultVolumes;\n\tbase::flat_map<ThreadId, ushort> ringtoneVolumes;\n\tbase::flat_set<uint64> ratedTranscriptions;\n\tstd::vector<Data::UnreviewedAuth> unreviewed;\n\tqint32 setupEmailState = 0;\n\tstd::vector<int32> moderateCommonGroups;\n\tqint32 disableSharingBoxShowsCount = 0;\n\tqint32 phoneNumberHidden = 0;\n\n\tstream >> versionTag;\n\tif (versionTag == kVersionTag) {\n\t\tstream >> version;\n\t\tstream >> selectorTab;\n\t} else {\n\t\tselectorTab = versionTag;\n\t}\n\tif (version < 2) {\n\t\tstream >> appLastSeenWarningSeen;\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> appTabbedSelectorSectionEnabled;\n\t\t}\n\t\tif (!stream.atEnd()) {\n\t\t\tauto count = qint32(0);\n\t\t\tstream >> count;\n\t\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\t\tQString key, value;\n\t\t\t\t\tstream >> key >> value;\n\t\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tappSoundOverrides.emplace(key, value);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> legacyTabbedSelectorSectionTooltipShown;\n\t\t}\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> appFloatPlayerColumn >> appFloatPlayerCorner;\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tquint64 peerId;\n\t\t\t\tstream >> peerId;\n\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tgroupStickersSectionHidden.emplace(\n\t\t\t\t\tDeserializePeerId(peerId));\n\t\t\t}\n\t\t}\n\t}\n\tif (version < 2) {\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> appThirdSectionInfoEnabled;\n\t\t\tstream >> legacySmallDialogsList;\n\t\t}\n\t\tif (!stream.atEnd()) {\n\t\t\tqint32 value = 0;\n\t\t\tstream >> value;\n\t\t\tappDialogsWidthRatio = std::clamp(value / 1000000., 0., 1.);\n\n\t\t\tstream >> value;\n\t\t\tappThirdColumnWidth = value;\n\n\t\t\tstream >> value;\n\t\t\tappThirdSectionExtendedBy = value;\n\t\t}\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> appSendFilesWay;\n\t\t}\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> legacyCallsPeerToPeer;\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tif (version < 2) {\n\t\t\tstream >> appSendSubmitWay;\n\t\t}\n\t\tstream >> supportSwitch;\n\t\tstream >> supportFixChatsOrder;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> supportTemplatesAutocomplete;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> supportChatsTimeSlice;\n\t}\n\tif (version < 2) {\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> appIncludeMutedCounter;\n\t\t\tstream >> appCountUnreadMessages;\n\t\t}\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> legacyAppExeLaunchWarning;\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> autoDownload;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> supportAllSearchResults;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> archiveCollapsed;\n\t}\n\tif (version < 2) {\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> appNotifyAboutPinned;\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> archiveInMainMenu;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> skipArchiveInSearch;\n\t}\n\tif (version < 2) {\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> legacyAutoplayGifs;\n\t\t\tstream >> appLoopAnimatedStickers;\n\t\t\tstream >> appLargeEmoji;\n\t\t\tstream >> appReplaceEmoji;\n\t\t\tstream >> appSuggestEmoji;\n\t\t\tstream >> appSuggestStickersByEmoji;\n\t\t}\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> appSpellcheckerEnabled;\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tquint64 documentId;\n\t\t\t\tqint64 time;\n\t\t\t\tstream >> documentId >> time;\n\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Old mediaLastPlaybackPosition.\n\t\t\t}\n\t\t}\n\t}\n\tif (version < 2) {\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> appVideoPlaybackSpeed;\n\t\t}\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> appVideoPipGeometry;\n\t\t}\n\t\tif (!stream.atEnd()) {\n\t\t\tauto count = qint32(0);\n\t\t\tstream >> count;\n\t\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\t\tqint64 langId;\n\t\t\t\t\tstream >> langId;\n\t\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tappDictionariesEnabled.emplace_back(langId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!stream.atEnd()) {\n\t\t\tstream >> appAutoDownloadDictionaries;\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\t// Legacy.\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tauto key = quint64();\n\t\t\t\tauto value = qint32();\n\t\t\t\tstream >> key >> value;\n\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\thiddenPinnedMessages.emplace(\n\t\t\t\t\tThreadId{ DeserializePeerId(key), MsgId(0) },\n\t\t\t\t\tvalue);\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> dialogsFiltersEnabled;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> supportAllSilent;\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> photoEditorHintShowsCount;\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tauto key = quint64();\n\t\t\t\tauto value = qint64();\n\t\t\t\tstream >> key >> value;\n\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\thiddenPinnedMessages.emplace(\n\t\t\t\t\tThreadId{ DeserializePeerId(key), MsgId(0) },\n\t\t\t\t\tvalue);\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tauto period = quint64();\n\t\t\t\tstream >> period;\n\t\t\t\tmutePeriods.emplace_back(period);\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> legacySkipPremiumStickersSet;\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\t// Legacy.\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tauto keyPeerId = quint64();\n\t\t\t\tauto keyTopicRootId = qint64();\n\t\t\t\tauto value = qint64();\n\t\t\t\tstream >> keyPeerId >> keyTopicRootId >> value;\n\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\thiddenPinnedMessages.emplace(\n\t\t\t\t\tThreadId{ DeserializePeerId(keyPeerId), keyTopicRootId },\n\t\t\t\t\tvalue);\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tquint64 peerId;\n\t\t\t\tstream >> peerId;\n\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tgroupEmojiSectionHidden.emplace(DeserializePeerId(peerId));\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream\n\t\t\t>> lastNonPremiumLimitDownload\n\t\t\t>> lastNonPremiumLimitUpload;\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tauto keyPeerId = quint64();\n\t\t\t\tauto keyTopicRootId = qint64();\n\t\t\t\tauto keyMonoforumPeerId = quint64();\n\t\t\t\tauto value = qint64();\n\t\t\t\tstream\n\t\t\t\t\t>> keyPeerId\n\t\t\t\t\t>> keyTopicRootId\n\t\t\t\t\t>> keyMonoforumPeerId\n\t\t\t\t\t>> value;\n\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\thiddenPinnedMessages.emplace(ThreadId{\n\t\t\t\t\tDeserializePeerId(keyPeerId),\n\t\t\t\t\tkeyTopicRootId,\n\t\t\t\t\tDeserializePeerId(keyMonoforumPeerId),\n\t\t\t\t}, value);\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tauto peerId = quint64();\n\t\t\t\tstream >> peerId;\n\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsubsectionTabsModes.emplace(\n\t\t\t\t\tDeserializePeerId(peerId), qint32(1));\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tauto keyDefaultNotify = uint8_t();\n\t\t\t\tauto value = ushort();\n\t\t\t\tstream\n\t\t\t\t\t>> keyDefaultNotify\n\t\t\t\t\t>> value;\n\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tringtoneDefaultVolumes.emplace(\n\t\t\t\t\tstatic_cast<Data::DefaultNotify>(keyDefaultNotify),\n\t\t\t\t\tvalue);\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tauto keyPeerId = quint64();\n\t\t\t\tauto keyTopicRootId = qint64();\n\t\t\t\tauto keyMonoforumPeerId = quint64();\n\t\t\t\tauto value = ushort();\n\t\t\t\tstream\n\t\t\t\t\t>> keyPeerId\n\t\t\t\t\t>> keyTopicRootId\n\t\t\t\t\t>> keyMonoforumPeerId\n\t\t\t\t\t>> value;\n\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tringtoneVolumes.emplace(ThreadId{\n\t\t\t\t\tDeserializePeerId(keyPeerId),\n\t\t\t\t\tkeyTopicRootId,\n\t\t\t\t\tDeserializePeerId(keyMonoforumPeerId),\n\t\t\t\t}, value);\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tauto transcriptionId = quint64();\n\t\t\t\tstream >> transcriptionId;\n\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"\n\t\t\t\t\t\t\"with ratedTranscriptions\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tratedTranscriptions.emplace(transcriptionId);\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tauto hash = quint64();\n\t\t\t\tauto unconfirmed = qint32();\n\t\t\t\tauto date = qint32();\n\t\t\t\tQString device, location;\n\t\t\t\tstream >> hash >> unconfirmed >> date >> device >> location;\n\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"\n\t\t\t\t\t\t\"with unreviewed\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tunreviewed.emplace_back(Data::UnreviewedAuth{\n\t\t\t\t\t.hash = hash,\n\t\t\t\t\t.unconfirmed = (unconfirmed == 1),\n\t\t\t\t\t.date = TimeId(date),\n\t\t\t\t\t.device = device,\n\t\t\t\t\t.location = location,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> setupEmailState;\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tqint32 filterId;\n\t\t\t\tstream >> filterId;\n\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"\n\t\t\t\t\t\t\"with moderateCommonGroups\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tmoderateCommonGroups.emplace_back(filterId);\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> disableSharingBoxShowsCount;\n\t}\n\tif (!stream.atEnd()) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() == QDataStream::Ok) {\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tauto peerId = quint64();\n\t\t\t\tauto mode = qint32(0);\n\t\t\t\tstream >> peerId >> mode;\n\t\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\t\tLOG((\"App Error: \"\n\t\t\t\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsubsectionTabsModes.emplace(\n\t\t\t\t\tDeserializePeerId(peerId), mode);\n\t\t\t}\n\t\t}\n\t}\n\tif (!stream.atEnd()) {\n\t\tstream >> phoneNumberHidden;\n\t}\n\tif (stream.status() != QDataStream::Ok) {\n\t\tLOG((\"App Error: \"\n\t\t\t\"Bad data for SessionSettings::addFromSerialized()\"));\n\t\treturn;\n\t}\n\tif (!autoDownload.isEmpty()\n\t\t&& !_autoDownload.setFromSerialized(autoDownload)) {\n\t\treturn;\n\t}\n\tif (!version) {\n\t\tif (!legacyAutoplayGifs) {\n\t\t\tusing namespace Data::AutoDownload;\n\t\t\t_autoDownload = WithDisabledAutoPlay(_autoDownload);\n\t\t}\n\t}\n\n\tauto uncheckedTab = static_cast<ChatHelpers::SelectorTab>(selectorTab);\n\tswitch (uncheckedTab) {\n\tcase ChatHelpers::SelectorTab::Emoji:\n\tcase ChatHelpers::SelectorTab::Stickers:\n\tcase ChatHelpers::SelectorTab::Gifs: _selectorTab = uncheckedTab; break;\n\t}\n\t_groupStickersSectionHidden = std::move(groupStickersSectionHidden);\n\t_groupEmojiSectionHidden = std::move(groupEmojiSectionHidden);\n\tauto uncheckedSupportSwitch = static_cast<Support::SwitchSettings>(\n\t\tsupportSwitch);\n\tswitch (uncheckedSupportSwitch) {\n\tcase Support::SwitchSettings::None:\n\tcase Support::SwitchSettings::Next:\n\tcase Support::SwitchSettings::Previous: _supportSwitch = uncheckedSupportSwitch; break;\n\t}\n\t_supportFixChatsOrder = (supportFixChatsOrder == 1);\n\t_supportTemplatesAutocomplete = (supportTemplatesAutocomplete == 1);\n\t_supportChatsTimeSlice = supportChatsTimeSlice;\n\t_hadLegacyCallsPeerToPeerNobody = (legacyCallsPeerToPeer == kLegacyCallsPeerToPeerNobody);\n\t_supportAllSearchResults = (supportAllSearchResults == 1);\n\t_archiveCollapsed = (archiveCollapsed == 1);\n\t_archiveInMainMenu = (archiveInMainMenu == 1);\n\t_skipArchiveInSearch = (skipArchiveInSearch == 1);\n\t_hiddenPinnedMessages = std::move(hiddenPinnedMessages);\n\t_dialogsFiltersEnabled = (dialogsFiltersEnabled == 1);\n\t_supportAllSilent = (supportAllSilent == 1);\n\t_photoEditorHintShowsCount = std::move(photoEditorHintShowsCount);\n\t_mutePeriods = std::move(mutePeriods);\n\t_lastNonPremiumLimitDownload = lastNonPremiumLimitDownload;\n\t_lastNonPremiumLimitUpload = lastNonPremiumLimitUpload;\n\t_subsectionTabsModes = std::move(subsectionTabsModes);\n\t_ringtoneDefaultVolumes = std::move(ringtoneDefaultVolumes);\n\t_ringtoneVolumes = std::move(ringtoneVolumes);\n\t_ratedTranscriptions = std::move(ratedTranscriptions);\n\t_unreviewed = std::move(unreviewed);\n\tauto uncheckedSetupEmailState = static_cast<Data::SetupEmailState>(\n\t\tsetupEmailState);\n\tswitch (uncheckedSetupEmailState) {\n\tcase Data::SetupEmailState::None:\n\tcase Data::SetupEmailState::Setup:\n\tcase Data::SetupEmailState::SetupNoSkip:\n\tcase Data::SetupEmailState::SettingUp:\n\tcase Data::SetupEmailState::SettingUpNoSkip:\n\t\t_setupEmailState = uncheckedSetupEmailState;\n\t\tbreak;\n\t}\n\n\t_moderateCommonGroups = std::move(moderateCommonGroups);\n\t_disableSharingBoxShowsCount = disableSharingBoxShowsCount;\n\t_phoneNumberHidden = (phoneNumberHidden == 1);\n\n\tif (version < 2) {\n\t\tapp.setLastSeenWarningSeen(appLastSeenWarningSeen == 1);\n\t\tfor (const auto &[key, value] : appSoundOverrides) {\n\t\t\tapp.setSoundOverride(key, value);\n\t\t}\n\t\tif (const auto sendFilesWay = Ui::SendFilesWay::FromSerialized(appSendFilesWay)) {\n\t\t\tapp.setSendFilesWay(*sendFilesWay);\n\t\t}\n\t\tauto uncheckedSendSubmitWay = static_cast<Ui::InputSubmitSettings>(\n\t\t\tappSendSubmitWay);\n\t\tswitch (uncheckedSendSubmitWay) {\n\t\tcase Ui::InputSubmitSettings::Enter:\n\t\tcase Ui::InputSubmitSettings::CtrlEnter: app.setSendSubmitWay(uncheckedSendSubmitWay); break;\n\t\t}\n\t\tapp.setIncludeMutedCounter(appIncludeMutedCounter == 1);\n\t\tapp.setCountUnreadMessages(appCountUnreadMessages == 1);\n\t\tapp.setNotifyAboutPinned(appNotifyAboutPinned == 1);\n\t\tapp.setLoopAnimatedStickers(appLoopAnimatedStickers == 1);\n\t\tapp.setLargeEmoji(appLargeEmoji == 1);\n\t\tapp.setReplaceEmoji(appReplaceEmoji == 1);\n\t\tapp.setSuggestEmoji(appSuggestEmoji == 1);\n\t\tapp.setSuggestStickersByEmoji(appSuggestStickersByEmoji == 1);\n\t\tapp.setSpellcheckerEnabled(appSpellcheckerEnabled == 1);\n\t\tapp.setVideoPlaybackSpeedSerialized(appVideoPlaybackSpeed);\n\t\tapp.setVideoPipGeometry(appVideoPipGeometry);\n\t\tapp.setDictionariesEnabled(std::move(appDictionariesEnabled));\n\t\tapp.setAutoDownloadDictionaries(appAutoDownloadDictionaries == 1);\n\t\tapp.setTabbedSelectorSectionEnabled(appTabbedSelectorSectionEnabled == 1);\n\t\tauto uncheckedColumn = static_cast<Window::Column>(appFloatPlayerColumn);\n\t\tswitch (uncheckedColumn) {\n\t\tcase Window::Column::First:\n\t\tcase Window::Column::Second:\n\t\tcase Window::Column::Third: app.setFloatPlayerColumn(uncheckedColumn); break;\n\t\t}\n\t\tauto uncheckedCorner = static_cast<RectPart>(appFloatPlayerCorner);\n\t\tswitch (uncheckedCorner) {\n\t\tcase RectPart::TopLeft:\n\t\tcase RectPart::TopRight:\n\t\tcase RectPart::BottomLeft:\n\t\tcase RectPart::BottomRight: app.setFloatPlayerCorner(uncheckedCorner); break;\n\t\t}\n\t\tapp.setThirdSectionInfoEnabled(appThirdSectionInfoEnabled);\n\t\tapp.updateDialogsWidthRatio(appDialogsWidthRatio, false);\n\t\tapp.setThirdColumnWidth(appThirdColumnWidth);\n\t\tapp.setThirdSectionExtendedBy(appThirdSectionExtendedBy);\n\t}\n}\n\nvoid SessionSettings::setSupportChatsTimeSlice(int slice) {\n\t_supportChatsTimeSlice = slice;\n}\n\nint SessionSettings::supportChatsTimeSlice() const {\n\treturn _supportChatsTimeSlice.current();\n}\n\nrpl::producer<int> SessionSettings::supportChatsTimeSliceValue() const {\n\treturn _supportChatsTimeSlice.value();\n}\n\nvoid SessionSettings::setSupportAllSearchResults(bool all) {\n\t_supportAllSearchResults = all;\n}\n\nbool SessionSettings::supportAllSearchResults() const {\n\treturn _supportAllSearchResults.current();\n}\n\nrpl::producer<bool> SessionSettings::supportAllSearchResultsValue() const {\n\treturn _supportAllSearchResults.value();\n}\n\nvoid SessionSettings::setArchiveCollapsed(bool collapsed) {\n\t_archiveCollapsed = collapsed;\n}\n\nbool SessionSettings::archiveCollapsed() const {\n\treturn _archiveCollapsed.current();\n}\n\nrpl::producer<bool> SessionSettings::archiveCollapsedChanges() const {\n\treturn _archiveCollapsed.changes();\n}\n\nvoid SessionSettings::setArchiveInMainMenu(bool inMainMenu) {\n\t_archiveInMainMenu = inMainMenu;\n}\n\nbool SessionSettings::archiveInMainMenu() const {\n\treturn _archiveInMainMenu.current();\n}\n\nrpl::producer<bool> SessionSettings::archiveInMainMenuChanges() const {\n\treturn _archiveInMainMenu.changes();\n}\n\nvoid SessionSettings::setSkipArchiveInSearch(bool skip) {\n\t_skipArchiveInSearch = skip;\n}\n\nbool SessionSettings::skipArchiveInSearch() const {\n\treturn _skipArchiveInSearch.current();\n}\n\nrpl::producer<bool> SessionSettings::skipArchiveInSearchChanges() const {\n\treturn _skipArchiveInSearch.changes();\n}\n\nMsgId SessionSettings::hiddenPinnedMessageId(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId) const {\n\tconst auto i = _hiddenPinnedMessages.find({\n\t\tpeerId,\n\t\ttopicRootId,\n\t\tmonoforumPeerId,\n\t});\n\treturn (i != end(_hiddenPinnedMessages)) ? i->second : 0;\n}\n\nvoid SessionSettings::setHiddenPinnedMessageId(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tMsgId msgId) {\n\tconst auto id = ThreadId{ peerId, topicRootId, monoforumPeerId };\n\tif (msgId) {\n\t\t_hiddenPinnedMessages[id] = msgId;\n\t} else {\n\t\t_hiddenPinnedMessages.remove(id);\n\t}\n}\n\nqint32 SessionSettings::subsectionTabsMode(PeerId peerId) const {\n\tconst auto i = _subsectionTabsModes.find(peerId);\n\treturn (i != end(_subsectionTabsModes)) ? i->second : 0;\n}\n\nvoid SessionSettings::setSubsectionTabsMode(\n\t\tPeerId peerId,\n\t\tqint32 mode) {\n\tif (mode) {\n\t\t_subsectionTabsModes[peerId] = mode;\n\t} else {\n\t\t_subsectionTabsModes.remove(peerId);\n\t}\n}\n\nbool SessionSettings::photoEditorHintShown() const {\n\treturn _photoEditorHintShowsCount < kPhotoEditorHintMaxShowsCount;\n}\n\nvoid SessionSettings::incrementPhotoEditorHintShown() {\n\tif (photoEditorHintShown()) {\n\t\t_photoEditorHintShowsCount++;\n\t}\n}\n\nbool SessionSettings::shouldShowDisableSharingBox() const {\n\treturn _disableSharingBoxShowsCount < kDisableSharingBoxMaxShowsCount;\n}\n\nvoid SessionSettings::incrementDisableSharingBoxShown() {\n\tif (shouldShowDisableSharingBox()) {\n\t\t_disableSharingBoxShowsCount++;\n\t}\n}\n\nvoid SessionSettings::resetDisableSharingBoxShown() {\n\t_disableSharingBoxShowsCount = 0;\n}\n\nstd::vector<TimeId> SessionSettings::mutePeriods() const {\n\treturn _mutePeriods;\n}\n\nvoid SessionSettings::addMutePeriod(TimeId period) {\n\tif (_mutePeriods.empty()) {\n\t\t_mutePeriods.push_back(period);\n\t} else if (_mutePeriods.back() != period) {\n\t\tif (_mutePeriods.back() < period) {\n\t\t\t_mutePeriods = { _mutePeriods.back(), period };\n\t\t} else {\n\t\t\t_mutePeriods = { period, _mutePeriods.back() };\n\t\t}\n\t}\n}\n\nushort SessionSettings::ringtoneVolume(\n\t\tData::DefaultNotify defaultNotify) const {\n\tconst auto i = _ringtoneDefaultVolumes.find(defaultNotify);\n\treturn (i != end(_ringtoneDefaultVolumes)) ? i->second : 0;\n}\n\nvoid SessionSettings::setRingtoneVolume(\n\t\tData::DefaultNotify defaultNotify,\n\t\tushort volume) {\n\tif (volume > 0 && volume <= 100) {\n\t\t_ringtoneDefaultVolumes[defaultNotify] = volume;\n\t} else {\n\t\t_ringtoneDefaultVolumes.remove(defaultNotify);\n\t}\n}\n\nushort SessionSettings::ringtoneVolume(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId) const {\n\tconst auto i = _ringtoneVolumes.find(\n\t\tThreadId{ peerId, topicRootId, monoforumPeerId });\n\treturn (i != end(_ringtoneVolumes)) ? i->second : 0;\n}\n\nvoid SessionSettings::setRingtoneVolume(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tushort volume) {\n\tconst auto id = ThreadId{ peerId, topicRootId, monoforumPeerId };\n\tif (volume > 0 && volume <= 100) {\n\t\t_ringtoneVolumes[id] = volume;\n\t} else {\n\t\t_ringtoneVolumes.remove(id);\n\t}\n}\n\nvoid SessionSettings::markTranscriptionAsRated(uint64 transcriptionId) {\n\t_ratedTranscriptions.emplace(transcriptionId);\n}\n\nbool SessionSettings::isTranscriptionRated(uint64 transcriptionId) const {\n\treturn _ratedTranscriptions.contains(transcriptionId);\n}\n\nvoid SessionSettings::setUnreviewed(std::vector<Data::UnreviewedAuth> auths) {\n\t_unreviewed = std::move(auths);\n}\n\nconst std::vector<Data::UnreviewedAuth> &SessionSettings::unreviewed() const {\n\treturn _unreviewed;\n}\n\nvoid SessionSettings::setSetupEmailState(Data::SetupEmailState state) {\n\t_setupEmailState = state;\n}\n\nData::SetupEmailState SessionSettings::setupEmailState() const {\n\treturn _setupEmailState;\n}\n\n} // namespace Main\n"
  },
  {
    "path": "Telegram/SourceFiles/main/main_session_settings.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_auto_download.h\"\n#include \"data/notify/data_peer_notify_settings.h\"\n#include \"data/data_authorization.h\"\n#include \"ui/rect_part.h\"\n\nnamespace Support {\nenum class SwitchSettings;\n} // namespace Support\n\nnamespace ChatHelpers {\nenum class SelectorTab;\n} // namespace ChatHelpers\n\nnamespace Data {\nenum class SetupEmailState;\n} // namespace Data\n\nnamespace Main {\n\nclass SessionSettings final {\npublic:\n\tSessionSettings();\n\n\t[[nodiscard]] QByteArray serialize() const;\n\tvoid addFromSerialized(const QByteArray &serialized);\n\n\tvoid setSupportSwitch(Support::SwitchSettings value) {\n\t\t_supportSwitch = value;\n\t}\n\t[[nodiscard]] Support::SwitchSettings supportSwitch() const {\n\t\treturn _supportSwitch;\n\t}\n\tvoid setSupportFixChatsOrder(bool fix) {\n\t\t_supportFixChatsOrder = fix;\n\t}\n\t[[nodiscard]] bool supportFixChatsOrder() const {\n\t\treturn _supportFixChatsOrder;\n\t}\n\tvoid setSupportTemplatesAutocomplete(bool enabled) {\n\t\t_supportTemplatesAutocomplete = enabled;\n\t}\n\t[[nodiscard]] bool supportTemplatesAutocomplete() const {\n\t\treturn _supportTemplatesAutocomplete;\n\t}\n\tvoid setSupportChatsTimeSlice(int slice);\n\t[[nodiscard]] int supportChatsTimeSlice() const;\n\t[[nodiscard]] rpl::producer<int> supportChatsTimeSliceValue() const;\n\tvoid setSupportAllSearchResults(bool all);\n\t[[nodiscard]] bool supportAllSearchResults() const;\n\t[[nodiscard]] rpl::producer<bool> supportAllSearchResultsValue() const;\n\tvoid setSupportAllSilent(bool enabled) {\n\t\t_supportAllSilent = enabled;\n\t}\n\t[[nodiscard]] bool supportAllSilent() const {\n\t\treturn _supportAllSilent;\n\t}\n\n\t[[nodiscard]] ChatHelpers::SelectorTab selectorTab() const {\n\t\treturn _selectorTab;\n\t}\n\tvoid setSelectorTab(ChatHelpers::SelectorTab tab) {\n\t\t_selectorTab = tab;\n\t}\n\n\tvoid setGroupStickersSectionHidden(PeerId peerId) {\n\t\t_groupStickersSectionHidden.insert(peerId);\n\t}\n\t[[nodiscard]] bool isGroupStickersSectionHidden(PeerId peerId) const {\n\t\treturn _groupStickersSectionHidden.contains(peerId);\n\t}\n\tvoid removeGroupStickersSectionHidden(PeerId peerId) {\n\t\t_groupStickersSectionHidden.remove(peerId);\n\t}\n\n\tvoid setGroupEmojiSectionHidden(PeerId peerId) {\n\t\t_groupEmojiSectionHidden.insert(peerId);\n\t}\n\t[[nodiscard]] bool isGroupEmojiSectionHidden(PeerId peerId) const {\n\t\treturn _groupEmojiSectionHidden.contains(peerId);\n\t}\n\tvoid removeGroupEmojiSectionHidden(PeerId peerId) {\n\t\t_groupEmojiSectionHidden.remove(peerId);\n\t}\n\n\t[[nodiscard]] Data::AutoDownload::Full &autoDownload() {\n\t\treturn _autoDownload;\n\t}\n\t[[nodiscard]] const Data::AutoDownload::Full &autoDownload() const {\n\t\treturn _autoDownload;\n\t}\n\n\tvoid setArchiveCollapsed(bool collapsed);\n\t[[nodiscard]] bool archiveCollapsed() const;\n\t[[nodiscard]] rpl::producer<bool> archiveCollapsedChanges() const;\n\n\tvoid setArchiveInMainMenu(bool inMainMenu);\n\t[[nodiscard]] bool archiveInMainMenu() const;\n\t[[nodiscard]] rpl::producer<bool> archiveInMainMenuChanges() const;\n\n\tvoid setSkipArchiveInSearch(bool skip);\n\t[[nodiscard]] bool skipArchiveInSearch() const;\n\t[[nodiscard]] rpl::producer<bool> skipArchiveInSearchChanges() const;\n\n\t[[nodiscard]] bool hadLegacyCallsPeerToPeerNobody() const {\n\t\treturn _hadLegacyCallsPeerToPeerNobody;\n\t}\n\n\t[[nodiscard]] MsgId hiddenPinnedMessageId(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId = 0,\n\t\tPeerId monoforumPeerId = 0) const;\n\tvoid setHiddenPinnedMessageId(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tMsgId msgId);\n\n\t[[nodiscard]] qint32 subsectionTabsMode(PeerId peerId) const;\n\tvoid setSubsectionTabsMode(PeerId peerId, qint32 mode);\n\n\t[[nodiscard]] bool dialogsFiltersEnabled() const {\n\t\treturn _dialogsFiltersEnabled;\n\t}\n\tvoid setDialogsFiltersEnabled(bool value) {\n\t\t_dialogsFiltersEnabled = value;\n\t}\n\n\t[[nodiscard]] bool photoEditorHintShown() const;\n\tvoid incrementPhotoEditorHintShown();\n\n\t[[nodiscard]] bool shouldShowDisableSharingBox() const;\n\tvoid incrementDisableSharingBoxShown();\n\tvoid resetDisableSharingBoxShown();\n\n\t[[nodiscard]] std::vector<TimeId> mutePeriods() const;\n\tvoid addMutePeriod(TimeId period);\n\n\t[[nodiscard]] TimeId lastNonPremiumLimitDownload() const {\n\t\treturn _lastNonPremiumLimitDownload;\n\t}\n\t[[nodiscard]] TimeId lastNonPremiumLimitUpload() const {\n\t\treturn _lastNonPremiumLimitUpload;\n\t}\n\tvoid setLastNonPremiumLimitDownload(TimeId when) {\n\t\t_lastNonPremiumLimitDownload = when;\n\t}\n\tvoid setLastNonPremiumLimitUpload(TimeId when) {\n\t\t_lastNonPremiumLimitUpload = when;\n\t}\n\tvoid setRingtoneVolume(\n\t\tData::DefaultNotify defaultNotify,\n\t\tushort volume);\n\t[[nodiscard]] ushort ringtoneVolume(\n\t\tData::DefaultNotify defaultNotify) const;\n\tvoid setRingtoneVolume(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tushort volume);\n\t[[nodiscard]] ushort ringtoneVolume(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId) const;\n\n\tvoid markTranscriptionAsRated(uint64 transcriptionId);\n\t[[nodiscard]] bool isTranscriptionRated(uint64 transcriptionId) const;\n\n\tvoid setUnreviewed(std::vector<Data::UnreviewedAuth> auths);\n\t[[nodiscard]] const std::vector<Data::UnreviewedAuth> &unreviewed() const;\n\n\tvoid setSetupEmailState(Data::SetupEmailState state);\n\t[[nodiscard]] Data::SetupEmailState setupEmailState() const;\n\n\tvoid setModerateCommonGroups(std::vector<int32> groups) {\n\t\t_moderateCommonGroups = std::move(groups);\n\t}\n\t[[nodiscard]] const std::vector<int32> &moderateCommonGroups() const {\n\t\treturn _moderateCommonGroups;\n\t}\n\n\tvoid setPhoneNumberHidden(bool hidden) {\n\t\t_phoneNumberHidden = hidden;\n\t}\n\t[[nodiscard]] bool phoneNumberHidden() const {\n\t\treturn _phoneNumberHidden;\n\t}\n\nprivate:\n\tstatic constexpr auto kDefaultSupportChatsLimitSlice = 7 * 24 * 60 * 60;\n\tstatic constexpr auto kPhotoEditorHintMaxShowsCount = 5;\n\tstatic constexpr auto kDisableSharingBoxMaxShowsCount = 3;\n\n\tstruct ThreadId {\n\t\tPeerId peerId;\n\t\tMsgId topicRootId;\n\t\tPeerId monoforumPeerId;\n\n\t\tfriend inline constexpr auto operator<=>(\n\t\t\tThreadId,\n\t\t\tThreadId) = default;\n\t};\n\n\tChatHelpers::SelectorTab _selectorTab; // per-window\n\tbase::flat_set<PeerId> _groupStickersSectionHidden;\n\tbase::flat_set<PeerId> _groupEmojiSectionHidden;\n\tbool _hadLegacyCallsPeerToPeerNobody = false;\n\tData::AutoDownload::Full _autoDownload;\n\trpl::variable<bool> _archiveCollapsed = false;\n\trpl::variable<bool> _archiveInMainMenu = false;\n\trpl::variable<bool> _skipArchiveInSearch = false;\n\tbase::flat_map<ThreadId, MsgId> _hiddenPinnedMessages;\n\tbase::flat_map<PeerId, qint32> _subsectionTabsModes;\n\tbase::flat_map<Data::DefaultNotify, ushort> _ringtoneDefaultVolumes;\n\tbase::flat_map<ThreadId, ushort> _ringtoneVolumes;\n\tbool _dialogsFiltersEnabled = false;\n\tint _photoEditorHintShowsCount = 0;\n\tint _disableSharingBoxShowsCount = 0;\n\tstd::vector<TimeId> _mutePeriods;\n\tTimeId _lastNonPremiumLimitDownload = 0;\n\tTimeId _lastNonPremiumLimitUpload = 0;\n\n\tSupport::SwitchSettings _supportSwitch;\n\tbool _supportFixChatsOrder = true;\n\tbool _supportTemplatesAutocomplete = true;\n\tbool _supportAllSilent = false;\n\trpl::variable<int> _supportChatsTimeSlice\n\t\t= kDefaultSupportChatsLimitSlice;\n\trpl::variable<bool> _supportAllSearchResults = false;\n\n\tbase::flat_set<uint64> _ratedTranscriptions;\n\n\tstd::vector<Data::UnreviewedAuth> _unreviewed;\n\n\tData::SetupEmailState _setupEmailState;\n\n\tstd::vector<int32> _moderateCommonGroups;\n\n\tbool _phoneNumberHidden = false;\n\n};\n\n} // namespace Main\n"
  },
  {
    "path": "Telegram/SourceFiles/main/session/send_as_peers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"main/session/send_as_peers.h\"\n\n#include \"data/data_user.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n\nnamespace Main {\nnamespace {\n\nconstexpr auto kRequestEach = 30 * crl::time(1000);\n\n} // namespace\n\nSendAsPeers::SendAsPeers(not_null<Session*> session)\n: _session(session)\n, _onlyMe({ { .peer = session->user(), .premiumRequired = false } })\n, _onlyMePaid({ session->user() }) {\n\t_session->changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::Rights\n\t) | rpl::map([=](const Data::PeerUpdate &update) {\n\t\tconst auto peer = update.peer;\n\t\tconst auto channel = peer->asChannel();\n\t\tconst auto bits = 0\n\t\t\t| (peer->amAnonymous() ? (1 << 0) : 0)\n\t\t\t| ((channel && channel->isPublic()) ? (1 << 1) : 0)\n\t\t\t| ((channel && channel->addsSignature()) ? (1 << 2) : 0)\n\t\t\t| ((channel && channel->signatureProfiles()) ? (1 << 3) : 0);\n\t\treturn std::tuple(peer, bits);\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::filter([=](not_null<PeerData*> peer, int) {\n\t\treturn _lists.contains(peer) || _lastRequestTime.contains(peer);\n\t}) | rpl::on_next([=](not_null<PeerData*> peer, int) {\n\t\trefresh(peer, true);\n\t}, _lifetime);\n}\n\nbool SendAsPeers::shouldChoose(SendAsKey key) {\n\trefresh(key);\n\tif (list(key).size() < 2) {\n\t\treturn false;\n\t}\n\tif (key.type == SendAsType::VideoStream) {\n\t\treturn true;\n\t}\n\tconst auto channel = key.peer->asBroadcast();\n\treturn Data::CanSendAnything(key.peer, false)\n\t\t&& (!channel\n\t\t\t|| channel->addsSignature()\n\t\t\t|| channel->signatureProfiles());\n}\n\nvoid SendAsPeers::refresh(SendAsKey key, bool force) {\n\tif (key.type != SendAsType::VideoStream && !key.peer->isChannel()) {\n\t\treturn;\n\t}\n\tconst auto now = crl::now();\n\tconst auto i = _lastRequestTime.find(key);\n\tconst auto when = (i == end(_lastRequestTime)) ? -1 : i->second;\n\tif (!force && (when >= 0 && now < when + kRequestEach)) {\n\t\treturn;\n\t}\n\t_lastRequestTime[key] = now;\n\trequest(key);\n\tif (key.type == SendAsType::Message) {\n\t\trequest({ key.peer, SendAsType::PaidReaction });\n\t}\n}\n\nconst std::vector<SendAsPeer> &SendAsPeers::list(SendAsKey key) const {\n\tif (key.type != SendAsType::VideoStream && !key.peer->isChannel()) {\n\t\treturn _onlyMe;\n\t}\n\tconst auto i = _lists.find(key);\n\treturn (i != end(_lists)) ? i->second : _onlyMe;\n}\n\nrpl::producer<SendAsKey> SendAsPeers::updated() const {\n\treturn _updates.events();\n}\n\nvoid SendAsPeers::saveChosen(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<PeerData*> chosen) {\n\tpeer->session().api().request(MTPmessages_SaveDefaultSendAs(\n\t\tpeer->input(),\n\t\tchosen->input()\n\t)).send();\n\n\tsetChosen(peer, chosen->id);\n}\n\nvoid SendAsPeers::setChosen(not_null<PeerData*> peer, PeerId chosenId) {\n\tif (chosen(peer) == chosenId) {\n\t\treturn;\n\t}\n\tconst auto fallback = peer->amAnonymous()\n\t\t? peer\n\t\t: peer->session().user();\n\tif (fallback->id == chosenId) {\n\t\t_chosen.remove(peer);\n\t} else {\n\t\t_chosen[peer] = chosenId;\n\t}\n\t_updates.fire({ peer });\n}\n\nPeerId SendAsPeers::chosen(not_null<PeerData*> peer) const {\n\tconst auto i = _chosen.find(peer);\n\treturn (i != end(_chosen)) ? i->second : PeerId();\n}\n\nnot_null<PeerData*> SendAsPeers::resolveChosen(\n\t\tnot_null<PeerData*> peer) const {\n\treturn ResolveChosen(peer, list({ peer }), chosen(peer));\n}\n\nnot_null<PeerData*> SendAsPeers::ResolveChosen(\n\t\tnot_null<PeerData*> peer,\n\t\tconst std::vector<SendAsPeer> &list,\n\t\tPeerId chosen) {\n\tconst auto fallback = peer->amAnonymous()\n\t\t? peer\n\t\t: peer->session().user();\n\tif (!chosen) {\n\t\tchosen = fallback->id;\n\t}\n\tconst auto i = ranges::find(list, chosen, [](const SendAsPeer &as) {\n\t\treturn as.peer->id;\n\t});\n\treturn (i != end(list))\n\t\t? i->peer\n\t\t: !list.empty()\n\t\t? list.front().peer\n\t\t: fallback;\n}\n\nvoid SendAsPeers::request(SendAsKey key) {\n\tusing Flag = MTPchannels_GetSendAs::Flag;\n\tkey.peer->session().api().request(MTPchannels_GetSendAs(\n\t\tMTP_flags((key.type == SendAsType::PaidReaction)\n\t\t\t? Flag::f_for_paid_reactions\n\t\t\t: (key.type == SendAsType::VideoStream)\n\t\t\t? Flag::f_for_live_stories\n\t\t\t: Flag()),\n\t\tkey.peer->input()\n\t)).done([=](const MTPchannels_SendAsPeers &result) {\n\t\tauto parsed = std::vector<SendAsPeer>();\n\t\tauto &owner = key.peer->owner();\n\t\tresult.match([&](const MTPDchannels_sendAsPeers &data) {\n\t\t\towner.processUsers(data.vusers());\n\t\t\towner.processChats(data.vchats());\n\t\t\tconst auto &list = data.vpeers().v;\n\t\t\tparsed.reserve(list.size());\n\t\t\tfor (const auto &as : list) {\n\t\t\t\tconst auto &data = as.data();\n\t\t\t\tconst auto peerId = peerFromMTP(data.vpeer());\n\t\t\t\tif (const auto peer = owner.peerLoaded(peerId)) {\n\t\t\t\t\tparsed.push_back({\n\t\t\t\t\t\t.peer = peer,\n\t\t\t\t\t\t.premiumRequired = data.is_premium_required(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tif (parsed.size() > 1) {\n\t\t\tauto &now = _lists[key];\n\t\t\tif (now != parsed) {\n\t\t\t\tnow = std::move(parsed);\n\t\t\t\t_updates.fire_copy(key);\n\t\t\t}\n\t\t} else if (const auto i = _lists.find(key); i != end(_lists)) {\n\t\t\t_lists.erase(i);\n\t\t\t_updates.fire_copy(key);\n\t\t}\n\t}).send();\n}\n\n} // namespace Main\n"
  },
  {
    "path": "Telegram/SourceFiles/main/session/send_as_peers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\n\nnamespace Main {\n\nclass Session;\n\nstruct SendAsPeer {\n\tnot_null<PeerData*> peer;\n\tbool premiumRequired = false;\n\n\tfriend inline auto operator<=>(SendAsPeer, SendAsPeer) = default;\n};\n\nenum class SendAsType : uchar {\n\tMessage,\n\tPaidReaction,\n\tVideoStream,\n};\n\nstruct SendAsKey {\n\tSendAsKey(\n\t\tnot_null<PeerData*> peer,\n\t\tSendAsType type = SendAsType::Message)\n\t: peer(peer)\n\t, type(type) {\n\t}\n\n\tfriend inline auto operator<=>(SendAsKey, SendAsKey) = default;\n\tfriend inline bool operator==(SendAsKey, SendAsKey) = default;\n\n\tnot_null<PeerData*> peer;\n\tSendAsType type = SendAsType::Message;\n};\n\nclass SendAsPeers final {\npublic:\n\texplicit SendAsPeers(not_null<Session*> session);\n\n\tbool shouldChoose(SendAsKey key);\n\tvoid refresh(SendAsKey key, bool force = false);\n\t[[nodiscard]] const std::vector<SendAsPeer> &list(SendAsKey key) const;\n\t[[nodiscard]] rpl::producer<SendAsKey> updated() const;\n\n\tvoid saveChosen(not_null<PeerData*> peer, not_null<PeerData*> chosen);\n\tvoid setChosen(not_null<PeerData*> peer, PeerId chosenId);\n\t[[nodiscard]] PeerId chosen(not_null<PeerData*> peer) const;\n\n\t[[nodiscard]] const std::vector<not_null<PeerData*>> &paidReactionList(\n\t\tnot_null<PeerData*> peer) const;\n\n\t// If !list(peer).empty() then the result will be from that list.\n\t[[nodiscard]] not_null<PeerData*> resolveChosen(\n\t\tnot_null<PeerData*> peer) const;\n\n\t[[nodiscard]] static not_null<PeerData*> ResolveChosen(\n\t\tnot_null<PeerData*> peer,\n\t\tconst std::vector<SendAsPeer> &list,\n\t\tPeerId chosen);\n\nprivate:\n\tvoid request(SendAsKey key);\n\n\tconst not_null<Session*> _session;\n\tconst std::vector<SendAsPeer> _onlyMe;\n\tconst std::vector<not_null<PeerData*>> _onlyMePaid;\n\n\tbase::flat_map<SendAsKey, std::vector<SendAsPeer>> _lists;\n\tbase::flat_map<SendAsKey, crl::time> _lastRequestTime;\n\tbase::flat_map<not_null<PeerData*>, PeerId> _chosen;\n\n\trpl::event_stream<SendAsKey> _updates;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Main\n"
  },
  {
    "path": "Telegram/SourceFiles/main/session/session_show.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"main/session/session_show.h\"\n\n#include \"chat_helpers/message_field.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/layers/generic_box.h\"\n\nnamespace Main {\nnamespace {\n\nclass SimpleSessionShow final : public SessionShow {\npublic:\n\tSimpleSessionShow(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<Session*> session);\n\n\tvoid showOrHideBoxOrLayer(\n\t\tstd::variant<\n\t\t\tv::null_t,\n\t\t\tobject_ptr<Ui::BoxContent>,\n\t\t\tstd::unique_ptr<Ui::LayerWidget>> &&layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) const override;\n\tnot_null<QWidget*> toastParent() const override;\n\tbool valid() const override;\n\toperator bool() const override;\n\n\tSession &session() const override;\n\nprivate:\n\tconst std::shared_ptr<Ui::Show> _show;\n\tconst not_null<Session*> _session;\n\n};\n\nSimpleSessionShow::SimpleSessionShow(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<Session*> session)\n: _show(std::move(show))\n, _session(session) {\n}\n\nvoid SimpleSessionShow::showOrHideBoxOrLayer(\n\t\tstd::variant<\n\t\t\tv::null_t,\n\t\t\tobject_ptr<Ui::BoxContent>,\n\t\t\tstd::unique_ptr<Ui::LayerWidget>> &&layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) const {\n\t_show->showOrHideBoxOrLayer(std::move(layer), options, animated);\n}\n\nnot_null<QWidget*> SimpleSessionShow::toastParent() const {\n\treturn _show->toastParent();\n}\n\nbool SimpleSessionShow::valid() const {\n\treturn _show->valid();\n}\n\nSimpleSessionShow::operator bool() const {\n\treturn _show->operator bool();\n}\n\nSession &SimpleSessionShow::session() const {\n\treturn *_session;\n}\n\n} // namespace\n\nbool SessionShow::showFrozenError() {\n\tif (!session().frozen()) {\n\t\treturn false;\n\t}\n\tshowBox(Box(FrozenInfoBox, &session(), FreezeInfoStyleOverride()));\n\treturn true;\n}\n\nstd::shared_ptr<SessionShow> MakeSessionShow(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<Session*> session) {\n\treturn std::make_shared<SimpleSessionShow>(std::move(show), session);\n}\n\n} // namespace Main\n"
  },
  {
    "path": "Telegram/SourceFiles/main/session/session_show.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/show.h\"\n\nnamespace Main {\n\nclass Session;\n\nclass SessionShow : public Ui::Show {\npublic:\n\t[[nodiscard]] virtual Main::Session &session() const = 0;\n\n\tbool showFrozenError();\n\n};\n\n[[nodiscard]] std::shared_ptr<SessionShow> MakeSessionShow(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<Session*> session);\n\n} // namespace Main\n"
  },
  {
    "path": "Telegram/SourceFiles/main.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"core/launcher.h\"\n\nint main(int argc, char *argv[]) {\n\tconst auto launcher = Core::Launcher::Create(argc, argv);\n\treturn launcher ? launcher->exec() : 1;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/mainwidget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mainwidget.h\"\n\n#include \"api/api_updates.h\"\n#include \"api/api_views.h\"\n#include \"data/components/scheduled_messages.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_document_resolver.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_web_page.h\"\n#include \"data/data_game.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_histories.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/focus_persister.h\"\n#include \"ui/resize_area.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_connecting_widget.h\"\n#include \"window/window_top_bar_wrap.h\"\n#include \"window/notifications_manager.h\"\n#include \"window/window_separate_id.h\"\n#include \"window/window_slide_animation.h\"\n#include \"window/window_history_hider.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller_link_info.h\"\n#include \"window/themes/window_theme.h\"\n#include \"chat_helpers/bot_command.h\"\n#include \"chat_helpers/tabbed_selector.h\" // TabbedSelector::refreshStickers\n#include \"chat_helpers/message_field.h\"\n#include \"info/info_memento.h\"\n#include \"apiwrap.h\"\n#include \"dialogs/dialogs_widget.h\"\n#include \"history/history_widget.h\"\n#include \"history/history_item_helpers.h\" // GetErrorForSending.\n#include \"history/view/media/history_view_media.h\"\n#include \"history/view/history_view_chat_section.h\"\n#include \"history/view/history_view_service_message.h\"\n#include \"lang/lang_keys.h\"\n#include \"lang/lang_cloud_manager.h\"\n#include \"inline_bots/inline_bot_layout_item.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"storage/storage_account.h\"\n#include \"main/main_domain.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/player/media_player_panel.h\"\n#include \"media/player/media_player_widget.h\"\n#include \"media/player/media_player_dropdown.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"base/qthelp_regex.h\"\n#include \"base/options.h\"\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"core/update_checker.h\"\n#include \"core/shortcuts.h\"\n#include \"core/application.h\"\n#include \"core/changelogs.h\"\n#include \"core/mime_type.h\"\n#include \"calls/calls_call.h\"\n#include \"calls/calls_instance.h\"\n#include \"calls/calls_top_bar.h\"\n#include \"calls/group/calls_group_call.h\"\n#include \"export/export_settings.h\"\n#include \"export/export_manager.h\"\n#include \"export/view/export_view_top_bar.h\"\n#include \"export/view/export_view_panel_controller.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"main/main_app_config.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"support/support_helper.h\"\n#include \"storage/storage_user_photos.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_window.h\"\n\n#include <QtCore/QCoreApplication>\n#include <QtCore/QMimeData>\n\nnamespace {\n\nvoid ClearBotStartToken(PeerData *peer) {\n\tif (peer && peer->isUser() && peer->asUser()->isBot()) {\n\t\tpeer->asUser()->botInfo->startToken = QString();\n\t}\n}\n\nbase::options::toggle ForceComposeSearchOneColumn({\n\t.id = kForceComposeSearchOneColumn,\n\t.name = \"Force embedded search in chats\",\n\t.description = \"Force in one-column mode the embedded search in chats.\",\n});\n\n} // namespace\n\nconst char kForceComposeSearchOneColumn[] = \"force-compose-search-one-column\";\n\nenum StackItemType {\n\tHistoryStackItem,\n\tSectionStackItem,\n};\n\nclass StackItem {\npublic:\n\texplicit StackItem(PeerData *peer) : _peer(peer) {\n\t}\n\n\t[[nodiscard]] PeerData *peer() const {\n\t\treturn _peer;\n\t}\n\n\tvoid setThirdSectionMemento(\n\t\tstd::shared_ptr<Window::SectionMemento> memento);\n\t[[nodiscard]] auto takeThirdSectionMemento()\n\t-> std::shared_ptr<Window::SectionMemento> {\n\t\treturn std::move(_thirdSectionMemento);\n\t}\n\n\tvoid setThirdSectionWeak(QPointer<Window::SectionWidget> section) {\n\t\t_thirdSectionWeak = section;\n\t}\n\t[[nodiscard]] QPointer<Window::SectionWidget> thirdSectionWeak() const {\n\t\treturn _thirdSectionWeak;\n\t}\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\n\t[[nodiscard]] virtual StackItemType type() const = 0;\n\t[[nodiscard]] rpl::producer<> removeRequests() const {\n\t\treturn rpl::merge(\n\t\t\t_thirdSectionRemoveRequests.events(),\n\t\t\tsectionRemoveRequests());\n\t}\n\tvirtual ~StackItem() = default;\n\nprivate:\n\t[[nodiscard]] virtual rpl::producer<> sectionRemoveRequests() const = 0;\n\n\tPeerData *_peer = nullptr;\n\tQPointer<Window::SectionWidget> _thirdSectionWeak;\n\tstd::shared_ptr<Window::SectionMemento> _thirdSectionMemento;\n\trpl::event_stream<> _thirdSectionRemoveRequests;\n\n\trpl::lifetime _lifetime;\n\n};\n\nclass StackItemHistory final : public StackItem {\npublic:\n\tStackItemHistory(\n\t\tnot_null<History*> history,\n\t\tMsgId msgId,\n\t\tQVector<FullMsgId> replyReturns)\n\t: StackItem(history->peer)\n\t, history(history)\n\t, msgId(msgId)\n\t, replyReturns(replyReturns) {\n\t}\n\n\tStackItemType type() const override {\n\t\treturn HistoryStackItem;\n\t}\n\n\tnot_null<History*> history;\n\tMsgId msgId;\n\tQVector<FullMsgId> replyReturns;\n\nprivate:\n\trpl::producer<> sectionRemoveRequests() const override {\n\t\treturn rpl::never<>();\n\t}\n\n};\n\nclass StackItemSection : public StackItem {\npublic:\n\tStackItemSection(\n\t\tstd::shared_ptr<Window::SectionMemento> memento);\n\n\tStackItemType type() const override {\n\t\treturn SectionStackItem;\n\t}\n\tstd::shared_ptr<Window::SectionMemento> takeMemento() {\n\t\treturn std::move(_memento);\n\t}\n\nprivate:\n\trpl::producer<> sectionRemoveRequests() const override;\n\n\tstd::shared_ptr<Window::SectionMemento> _memento;\n\n};\n\nvoid StackItem::setThirdSectionMemento(\n\t\tstd::shared_ptr<Window::SectionMemento> memento) {\n\t_thirdSectionMemento = std::move(memento);\n\tif (const auto memento = _thirdSectionMemento.get()) {\n\t\tmemento->removeRequests(\n\t\t) | rpl::start_to_stream(_thirdSectionRemoveRequests, _lifetime);\n\t}\n}\n\nStackItemSection::StackItemSection(\n\tstd::shared_ptr<Window::SectionMemento> memento)\n: StackItem(nullptr)\n, _memento(std::move(memento)) {\n}\n\nrpl::producer<> StackItemSection::sectionRemoveRequests() const {\n\tif (const auto topic = _memento->topicForRemoveRequests()) {\n\t\treturn rpl::merge(_memento->removeRequests(), topic->destroyed());\n\t} else if (const auto sublist = _memento->sublistForRemoveRequests()) {\n\t\treturn rpl::merge(_memento->removeRequests(), sublist->destroyed());\n\t}\n\treturn _memento->removeRequests();\n}\n\nstruct MainWidget::SettingBackground {\n\texplicit SettingBackground(const Data::WallPaper &data);\n\n\tData::WallPaper data;\n\tstd::shared_ptr<Data::DocumentMedia> dataMedia;\n\tbase::binary_guard generating;\n};\n\nMainWidget::SettingBackground::SettingBackground(\n\tconst Data::WallPaper &data)\n: data(data) {\n}\n\nMainWidget::MainWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _dialogsWidth(st::columnMinimalWidthLeft)\n, _thirdColumnWidth(st::columnMinimalWidthThird)\n, _dialogs(windowId().hasChatsList()\n\t? base::make_unique_q<Dialogs::Widget>(\n\t\tthis,\n\t\t_controller,\n\t\tDialogs::Widget::Layout::Main)\n\t: nullptr)\n, _history(std::in_place, this, _controller)\n, _sideShadow(_dialogs\n\t? base::make_unique_q<Ui::PlainShadow>(this)\n\t: nullptr)\n, _playerPlaylist(this, _controller)\n, _changelogs(Core::Changelogs::Create(&controller->session())) {\n\tif (_dialogs) {\n\t\tsetupConnectingWidget();\n\t}\n\n\t_history->cancelRequests(\n\t) | rpl::on_next([=] {\n\t\thandleHistoryBack();\n\t}, lifetime());\n\n\tCore::App().calls().currentCallValue(\n\t) | rpl::on_next([=](Calls::Call *call) {\n\t\tsetCurrentCall(call);\n\t}, lifetime());\n\tCore::App().calls().currentGroupCallValue(\n\t) | rpl::on_next([=](Calls::GroupCall *call) {\n\t\tsetCurrentGroupCall(call);\n\t}, lifetime());\n\tif (_callTopBar) {\n\t\t_callTopBar->finishAnimating();\n\t}\n\n\tcontroller->window().setDefaultFloatPlayerDelegate(\n\t\tfloatPlayerDelegate());\n\n\tCore::App().floatPlayerClosed(\n\t) | rpl::on_next([=](FullMsgId itemId) {\n\t\tfloatPlayerClosed(itemId);\n\t}, lifetime());\n\n\tCore::App().exportManager().currentView(\n\t) | rpl::on_next([=](Export::View::PanelController *view) {\n\t\tsetCurrentExportView(view);\n\t}, lifetime());\n\tif (_exportTopBar) {\n\t\t_exportTopBar->finishAnimating();\n\t}\n\n\tMedia::Player::instance()->closePlayerRequests(\n\t) | rpl::on_next([=] {\n\t\tcloseBothPlayers();\n\t}, lifetime());\n\n\tMedia::Player::instance()->updatedNotifier(\n\t) | rpl::on_next([=](const Media::Player::TrackState &state) {\n\t\thandleAudioUpdate(state);\n\t}, lifetime());\n\thandleAudioUpdate(Media::Player::instance()->getState(AudioMsgId::Type::Song));\n\thandleAudioUpdate(Media::Player::instance()->getState(AudioMsgId::Type::Voice));\n\tif (_player) {\n\t\t_player->finishAnimating();\n\t}\n\n\t_controller->chatsForceDisplayWideChanges(\n\t) | rpl::on_next([=] {\n\t\tcrl::on_main(this, [=] {\n\t\t\tupdateDialogsWidthAnimated();\n\t\t});\n\t}, lifetime());\n\n\tconst auto filter = [=](bool mainSectionShown) {\n\t\treturn rpl::filter([=] {\n\t\t\treturn (_controller->mainSectionShown() == mainSectionShown);\n\t\t});\n\t};\n\trpl::merge(\n\t\tCore::App().settings().dialogsWithChatWidthRatioChanges(\n\t\t) | filter(true) | rpl::to_empty,\n\t\tCore::App().settings().dialogsNoChatWidthRatioChanges(\n\t\t) | filter(false) | rpl::to_empty,\n\t\tCore::App().settings().thirdColumnWidthChanges() | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\tupdateControlsGeometry();\n\t}, lifetime());\n\n\tsession().changes().historyUpdates(\n\t\tData::HistoryUpdate::Flag::MessageSent\n\t) | rpl::on_next([=](const Data::HistoryUpdate &update) {\n\t\tconst auto history = update.history;\n\t\thistory->forgetScrollState();\n\t\tif (const auto from = history->peer->migrateFrom()) {\n\t\t\tauto &owner = history->owner();\n\t\t\tif (const auto migrated = owner.historyLoaded(from)) {\n\t\t\t\tmigrated->forgetScrollState();\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\tsession().changes().entryUpdates(\n\t\tData::EntryUpdate::Flag::LocalDraftSet\n\t) | rpl::on_next([=](const Data::EntryUpdate &update) {\n\t\tauto params = Window::SectionShow();\n\t\tparams.reapplyLocalDraft = true;\n\t\tcontroller->showThread(\n\t\t\tupdate.entry->asThread(),\n\t\t\tShowAtUnreadMsgId,\n\t\t\tparams);\n\t\tcontroller->hideLayer();\n\t}, lifetime());\n\n\t// MSVC BUG + REGRESSION rpl::mappers::tuple :(\n\tusing namespace rpl::mappers;\n\t_controller->activeChatValue(\n\t) | rpl::map([](Dialogs::Key key) {\n\t\tconst auto peer = key.peer();\n\t\tconst auto topic = key.topic();\n\t\tauto canWrite = topic\n\t\t\t? Data::CanSendAnyOfValue(\n\t\t\t\ttopic,\n\t\t\t\tData::TabbedPanelSendRestrictions())\n\t\t\t: peer\n\t\t\t? Data::CanSendAnyOfValue(\n\n\t\t\t\tpeer, Data::TabbedPanelSendRestrictions())\n\t\t\t: rpl::single(false);\n\t\treturn std::move(\n\t\t\tcanWrite\n\t\t) | rpl::map([=](bool can) {\n\t\t\treturn std::make_tuple(key, can);\n\t\t});\n\t}) | rpl::flatten_latest(\n\t) | rpl::on_next([this](Dialogs::Key key, bool canWrite) {\n\t\tupdateThirdColumnToCurrentChat(key, canWrite);\n\t}, lifetime());\n\n\tQCoreApplication::instance()->installEventFilter(this);\n\n\tMedia::Player::instance()->tracksFinished(\n\t) | rpl::on_next([=](AudioMsgId::Type type) {\n\t\tif (type == AudioMsgId::Type::Voice) {\n\t\t\tconst auto songState = Media::Player::instance()->getState(\n\t\t\t\tAudioMsgId::Type::Song);\n\t\t\tif (!songState.id || IsStoppedOrStopping(songState.state)) {\n\t\t\t\tMedia::Player::instance()->stopAndClose();\n\t\t\t}\n\t\t} else if (type == AudioMsgId::Type::Song) {\n\t\t\tconst auto songState = Media::Player::instance()->getState(\n\t\t\t\tAudioMsgId::Type::Song);\n\t\t\tif (!songState.id) {\n\t\t\t\tMedia::Player::instance()->stopAndClose();\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\t_controller->adaptive().changes(\n\t) | rpl::on_next([=] {\n\t\thandleAdaptiveLayoutUpdate();\n\t}, lifetime());\n\n\tif (_dialogs) {\n\t\t_dialogs->show();\n\t}\n\tif (_dialogs && isOneColumn()) {\n\t\t_history->hide();\n\t} else {\n\t\t_history->show();\n\t}\n\torderWidgets();\n\n\tif (!Core::UpdaterDisabled()) {\n\t\tCore::UpdateChecker checker;\n\t\tchecker.start();\n\t}\n\n\tcSetOtherOnline(0);\n\n\tsession().data().stickers().notifySavedGifsUpdated();\n}\n\nMainWidget::~MainWidget() {\n\tif (_controller->activeChatCurrent()) {\n\t\tsession().api().saveCurrentDraftToCloud();\n\t}\n}\n\nMain::Session &MainWidget::session() const {\n\treturn _controller->session();\n}\n\nnot_null<Window::SessionController*> MainWidget::controller() const {\n\treturn _controller;\n}\n\nvoid MainWidget::setupConnectingWidget() {\n\tusing namespace rpl::mappers;\n\t_connecting = std::make_unique<Window::ConnectionState>(\n\t\tthis,\n\t\t&session().account(),\n\t\t_controller->adaptive().oneColumnValue() | rpl::map(!_1));\n\t_controller->connectingBottomSkipValue(\n\t) | rpl::on_next([=](int skip) {\n\t\t_connecting->setBottomSkip(skip);\n\t}, lifetime());\n}\n\nnot_null<Media::Player::FloatDelegate*> MainWidget::floatPlayerDelegate() {\n\treturn static_cast<Media::Player::FloatDelegate*>(this);\n}\n\nnot_null<Ui::RpWidget*> MainWidget::floatPlayerWidget() {\n\treturn this;\n}\n\nvoid MainWidget::floatPlayerToggleGifsPaused(bool paused) {\n\tconstexpr auto kReason = Window::GifPauseReason::RoundPlaying;\n\tif (paused) {\n\t\t_controller->enableGifPauseReason(kReason);\n\t} else {\n\t\t_controller->disableGifPauseReason(kReason);\n\t}\n}\n\nauto MainWidget::floatPlayerGetSection(Window::Column column)\n-> not_null<Media::Player::FloatSectionDelegate*> {\n\tif (isThreeColumn()) {\n\t\tif (_dialogs && column == Window::Column::First) {\n\t\t\treturn _dialogs;\n\t\t} else if (column == Window::Column::Second\n\t\t\t|| !_dialogs\n\t\t\t|| !_thirdSection) {\n\t\t\tif (_mainSection) {\n\t\t\t\treturn _mainSection;\n\t\t\t}\n\t\t\treturn _history;\n\t\t}\n\t\treturn _thirdSection;\n\t} else if (isNormalColumn()) {\n\t\tif (_dialogs && column == Window::Column::First) {\n\t\t\treturn _dialogs;\n\t\t} else if (_mainSection) {\n\t\t\treturn _mainSection;\n\t\t}\n\t\treturn _history;\n\t} else if (_mainSection) {\n\t\treturn _mainSection;\n\t} else if (!isOneColumn() || _history->peer()) {\n\t\treturn _history;\n\t}\n\tAssert(_dialogs != nullptr);\n\treturn _dialogs;\n}\n\nvoid MainWidget::floatPlayerEnumerateSections(Fn<void(\n\t\tnot_null<Media::Player::FloatSectionDelegate*> widget,\n\t\tWindow::Column widgetColumn)> callback) {\n\tif (isThreeColumn()) {\n\t\tif (_dialogs) {\n\t\t\tcallback(_dialogs, Window::Column::First);\n\t\t}\n\t\tif (_mainSection) {\n\t\t\tcallback(_mainSection, Window::Column::Second);\n\t\t} else {\n\t\t\tcallback(_history, Window::Column::Second);\n\t\t}\n\t\tif (_thirdSection) {\n\t\t\tcallback(_thirdSection, Window::Column::Third);\n\t\t}\n\t} else if (isNormalColumn()) {\n\t\tif (_dialogs) {\n\t\t\tcallback(_dialogs, Window::Column::First);\n\t\t}\n\t\tif (_mainSection) {\n\t\t\tcallback(_mainSection, Window::Column::Second);\n\t\t} else {\n\t\t\tcallback(_history, Window::Column::Second);\n\t\t}\n\t} else {\n\t\tif (_mainSection) {\n\t\t\tcallback(_mainSection, Window::Column::Second);\n\t\t} else if (!isOneColumn() || _history->peer()) {\n\t\t\tcallback(_history, Window::Column::Second);\n\t\t} else {\n\t\t\tAssert(_dialogs != nullptr);\n\t\t\tcallback(_dialogs, Window::Column::First);\n\t\t}\n\t}\n}\n\nbool MainWidget::floatPlayerIsVisible(not_null<HistoryItem*> item) {\n\treturn session().data().queryItemVisibility(item);\n}\n\nvoid MainWidget::floatPlayerClosed(FullMsgId itemId) {\n\tif (_player) {\n\t\tconst auto voiceData = Media::Player::instance()->current(\n\t\t\tAudioMsgId::Type::Voice);\n\t\tif (voiceData.contextId() == itemId) {\n\t\t\tstopAndClosePlayer();\n\t\t}\n\t}\n}\n\nvoid MainWidget::floatPlayerDoubleClickEvent(\n\t\tnot_null<const HistoryItem*> item) {\n\t_controller->showMessage(item);\n}\n\nbool MainWidget::setForwardDraft(\n\t\tnot_null<Data::Thread*> thread,\n\t\tData::ForwardDraft &&draft) {\n\tconst auto history = thread->owningHistory();\n\tconst auto items = session().data().idsToItems(draft.ids);\n\tconst auto topicRootId = thread->topicRootId();\n\tconst auto monoforumPeerId = thread->monoforumPeerId();\n\tconst auto error = GetErrorForSending(\n\t\thistory->peer,\n\t\t{\n\t\t\t.topicRootId = topicRootId,\n\t\t\t.forward = &items,\n\t\t\t.ignoreSlowmodeCountdown = true,\n\t\t});\n\tif (error) {\n\t\tData::ShowSendErrorToast(_controller, history->peer, error);\n\t\treturn false;\n\t}\n\n\thistory->setForwardDraft(topicRootId, monoforumPeerId, std::move(draft));\n\t_controller->showThread(\n\t\tthread,\n\t\tShowAtUnreadMsgId,\n\t\tSectionShow::Way::Forward);\n\treturn true;\n}\n\nbool MainWidget::shareUrl(\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst QString &url,\n\t\tconst QString &text) const {\n\tif (!Data::CanSendTexts(thread)) {\n\t\t_controller->show(Ui::MakeInformBox(tr::lng_share_cant()));\n\t\treturn false;\n\t}\n\tconst auto textWithTags = TextWithTags{\n\t\turl + '\\n' + text,\n\t\tTextWithTags::Tags()\n\t};\n\tconst auto cursor = MessageCursor{\n\t\tint(url.size()) + 1,\n\t\tint(url.size()) + 1 + int(text.size()),\n\t\tUi::kQFixedMax\n\t};\n\tconst auto history = thread->owningHistory();\n\tconst auto topicRootId = thread->topicRootId();\n\tconst auto monoforumPeerId = thread->monoforumPeerId();\n\thistory->setLocalDraft(std::make_unique<Data::Draft>(\n\t\ttextWithTags,\n\t\tFullReplyTo{\n\t\t\t.topicRootId = topicRootId,\n\t\t\t.monoforumPeerId = monoforumPeerId,\n\t\t},\n\t\tSuggestOptions(),\n\t\tcursor,\n\t\tData::WebPageDraft()));\n\thistory->clearLocalEditDraft(topicRootId, monoforumPeerId);\n\thistory->session().changes().entryUpdated(\n\t\tthread,\n\t\tData::EntryUpdate::Flag::LocalDraftSet);\n\treturn true;\n}\n\nbool MainWidget::sendPaths(\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst QStringList &paths) {\n\tif (!Data::CanSendAnyOf(thread, Data::FilesSendRestrictions())) {\n\t\t_controller->showToast(\n\t\t\ttr::lng_forward_send_files_cant(tr::now));\n\t\treturn false;\n\t} else if (const auto error = Data::AnyFileRestrictionError(\n\t\t\tthread->peer())) {\n\t\tData::ShowSendErrorToast(controller(), thread->peer(), error);\n\t\treturn false;\n\t} else {\n\t\t_controller->showThread(\n\t\t\tthread,\n\t\t\tShowAtTheEndMsgId,\n\t\t\tWindow::SectionShow::Way::ClearStack);\n\t}\n\treturn (_controller->activeChatCurrent().thread() == thread)\n\t\t&& (_mainSection\n\t\t\t? _mainSection->confirmSendingFiles(paths)\n\t\t\t: _history->confirmSendingFiles(paths));\n}\n\nbool MainWidget::filesOrForwardDrop(\n\t\tnot_null<Data::Thread*> thread,\n\t\tnot_null<const QMimeData*> data,\n\t\tbool forumResolved) {\n\tif (!forumResolved) {\n\t\tif (const auto forum = thread->asForum()) {\n\t\t\tWindow::ShowDropMediaBox(\n\t\t\t\t_controller,\n\t\t\t\tCore::ShareMimeMediaData(data),\n\t\t\t\tforum);\n\t\t\tif (_hider) {\n\t\t\t\t_hider->startHide();\n\t\t\t\tclearHider(_hider);\n\t\t\t}\n\t\t\treturn true;\n\t\t} else if (const auto history = thread->asHistory()\n\t\t\t; history && history->peer->monoforum()) {\n\t\t\tWindow::ShowDropMediaBox(\n\t\t\t\t_controller,\n\t\t\t\tCore::ShareMimeMediaData(data),\n\t\t\t\thistory->peer->monoforum());\n\t\t\tif (_hider) {\n\t\t\t\t_hider->startHide();\n\t\t\t\tclearHider(_hider);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t}\n\tif (data->hasFormat(u\"application/x-td-forward\"_q)) {\n\t\tauto draft = Data::ForwardDraft{\n\t\t\t.ids = session().data().takeMimeForwardIds(),\n\t\t};\n\t\tif (setForwardDraft(thread, std::move(draft))) {\n\t\t\treturn true;\n\t\t}\n\t\t// We've already released the mouse button,\n\t\t// so the forwarding is cancelled.\n\t\tif (_hider) {\n\t\t\t_hider->startHide();\n\t\t\tclearHider(_hider);\n\t\t}\n\t\treturn false;\n\t} else if (!Data::CanSendAnyOf(thread, Data::FilesSendRestrictions())) {\n\t\t_controller->showToast(\n\t\t\ttr::lng_forward_send_files_cant(tr::now));\n\t\treturn false;\n\t} else if (const auto error = Data::AnyFileRestrictionError(\n\t\t\tthread->peer())) {\n\t\tData::ShowSendErrorToast(_controller, thread->peer(), error);\n\t\treturn false;\n\t} else {\n\t\t_controller->showThread(\n\t\t\tthread,\n\t\t\tShowAtTheEndMsgId,\n\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\tif (_controller->activeChatCurrent().thread() != thread) {\n\t\t\treturn false;\n\t\t}\n\t\t(_mainSection\n\t\t\t? _mainSection->confirmSendingFiles(data)\n\t\t\t: _history->confirmSendingFiles(data));\n\t\treturn true;\n\t}\n}\n\nbool MainWidget::notify_switchInlineBotButtonReceived(const QString &query, UserData *samePeerBot, MsgId samePeerReplyTo) {\n\treturn _history->notify_switchInlineBotButtonReceived(query, samePeerBot, samePeerReplyTo);\n}\n\nvoid MainWidget::clearHider(not_null<Window::HistoryHider*> instance) {\n\tif (_hider != instance) {\n\t\treturn;\n\t}\n\t_hider.release();\n}\n\nvoid MainWidget::hiderLayer(base::unique_qptr<Window::HistoryHider> hider) {\n\tif (!_dialogs || _controller->window().locked()) {\n\t\treturn;\n\t}\n\n\t_hider = std::move(hider);\n\t_hider->setParent(this);\n\n\t_hider->hidden(\n\t) | rpl::on_next([=, instance = _hider.get()] {\n\t\tclearHider(instance);\n\t\tinstance->hide();\n\t\tinstance->deleteLater();\n\t}, _hider->lifetime());\n\n\t_hider->show();\n\tupdateControlsGeometry();\n\t_dialogs->setInnerFocus();\n\n\tfloatPlayerCheckVisibility();\n}\n\nvoid MainWidget::showDragForwardInfo() {\n\thiderLayer(base::make_unique_q<Window::HistoryHider>(\n\t\tthis,\n\t\ttr::lng_forward_choose(tr::now)));\n}\n\nvoid MainWidget::hideDragForwardInfo() {\n\tif (_hider) {\n\t\t_hider->startHide();\n\t\t_hider.release();\n\t}\n}\n\nvoid MainWidget::sendBotCommand(Bot::SendCommandRequest request) {\n\tconst auto type = _mainSection\n\t\t? _mainSection->sendBotCommand(request)\n\t\t: Window::SectionActionResult::Fallback;\n\tif (type == Window::SectionActionResult::Fallback) {\n\t\t_controller->showPeerHistory(\n\t\t\trequest.peer,\n\t\t\tSectionShow::Way::Forward,\n\t\t\tShowAtTheEndMsgId);\n\t\t_history->sendBotCommand(request);\n\t}\n}\n\nvoid MainWidget::hideSingleUseKeyboard(FullMsgId replyToId) {\n\t_history->hideSingleUseKeyboard(replyToId);\n}\n\nvoid MainWidget::searchMessages(\n\t\tconst QString &query,\n\t\tDialogs::Key inChat,\n\t\tPeerData *searchFrom) {\n\tconst auto complex = Data::HashtagWithUsernameFromQuery(query);\n\tif (!complex.username.isEmpty()) {\n\t\t_controller->showPeerByLink(Window::PeerByLinkInfo{\n\t\t\t.usernameOrId = complex.username,\n\t\t\t.text = complex.hashtag,\n\t\t\t.resolveType = Window::ResolveType::HashtagSearch,\n\t\t});\n\t\treturn;\n\t}\n\tauto tags = Data::SearchTagsFromQuery(query);\n\tif (_dialogs\n\t\t&& (!ForceComposeSearchOneColumn.value() || !isOneColumn())) {\n\t\tauto state = Dialogs::SearchState{\n\t\t\t.inChat = ((tags.empty() || inChat.sublist())\n\t\t\t\t? inChat\n\t\t\t\t: session().data().history(session().user())),\n\t\t\t.fromPeer = inChat ? searchFrom : nullptr,\n\t\t\t.tags = tags,\n\t\t\t.query = tags.empty() ? query : QString(),\n\t\t};\n\t\tstate.tab = state.defaultTabForMe();\n\t\t_dialogs->searchMessages(std::move(state));\n\t\tif (isOneColumn()) {\n\t\t\t_controller->clearSectionStack();\n\t\t} else {\n\t\t\t_dialogs->setInnerFocus();\n\t\t}\n\t} else {\n\t\tif (const auto sublist = inChat.sublist()) {\n\t\t\tusing namespace HistoryView;\n\t\t\tcontroller()->showSection(\n\t\t\t\tstd::make_shared<ChatMemento>(ChatViewId{\n\t\t\t\t\t.history = sublist->owningHistory(),\n\t\t\t\t\t.sublist = sublist,\n\t\t\t\t}));\n\t\t} else if (!tags.empty()) {\n\t\t\tinChat = controller()->session().data().history(\n\t\t\t\tcontroller()->session().user());\n\t\t}\n\t\tif ((!_mainSection\n\t\t\t|| !_mainSection->searchInChatEmbedded(query, inChat, searchFrom))\n\t\t\t&& !_history->searchInChatEmbedded(query, inChat, searchFrom)) {\n\t\t\tconst auto account = not_null(&session().account());\n\t\t\tif (const auto window = Core::App().windowFor(account)) {\n\t\t\t\tif (const auto controller = window->sessionController()) {\n\t\t\t\t\tcontroller->widget()->activate();\n\t\t\t\t\tcontroller->content()->searchMessages(\n\t\t\t\t\t\tquery,\n\t\t\t\t\t\tinChat,\n\t\t\t\t\t\tsearchFrom);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid MainWidget::handleAudioUpdate(const Media::Player::TrackState &state) {\n\tusing State = Media::Player::State;\n\tconst auto document = state.id.audio();\n\tconst auto item = session().data().message(state.id.contextId());\n\tif (!Media::Player::IsStoppedOrStopping(state.state)) {\n\t\tconst auto ttlSeconds = item\n\t\t\t&& item->media()\n\t\t\t&& item->media()->ttlSeconds();\n\t\tif (!ttlSeconds) {\n\t\t\tcreatePlayer();\n\t\t}\n\t} else if (state.state == State::StoppedAtStart) {\n\t\tMedia::Player::instance()->stopAndClose();\n\t}\n\n\tif (item) {\n\t\tsession().data().requestItemRepaint(item);\n\t}\n\tif (document) {\n\t\tif (const auto items = InlineBots::Layout::documentItems()) {\n\t\t\tif (const auto i = items->find(document); i != items->end()) {\n\t\t\t\tfor (const auto &item : i->second) {\n\t\t\t\t\titem->update();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid MainWidget::closeBothPlayers() {\n\tif (_player) {\n\t\t_player->hide(anim::type::normal);\n\t}\n\t_playerPlaylist->hideIgnoringEnterEvents();\n}\n\nvoid MainWidget::stopAndClosePlayer() {\n\tif (_player) {\n\t\t_player->entity()->stopAndClose();\n\t}\n}\n\nvoid MainWidget::createPlayer() {\n\tif (!_player) {\n\t\t_player.create(\n\t\t\tthis,\n\t\t\tobject_ptr<Media::Player::Widget>(this, this, _controller),\n\t\t\t_controller->adaptive().oneColumnValue());\n\t\trpl::merge(\n\t\t\t_player->heightValue() | rpl::map_to(true),\n\t\t\t_player->shownValue()\n\t\t) | rpl::on_next(\n\t\t\t[this] { playerHeightUpdated(); },\n\t\t\t_player->lifetime());\n\t\t_player->entity()->setCloseCallback([=] {\n\t\t\tMedia::Player::instance()->stopAndClose();\n\t\t});\n\t\t_player->entity()->setShowItemCallback([=](\n\t\t\t\tnot_null<const HistoryItem*> item) {\n\t\t\tconst auto peer = item->history()->peer;\n\t\t\tif (const auto window = Core::App().windowFor(peer)) {\n\t\t\t\tif (const auto controller = window->sessionController()) {\n\t\t\t\t\tcontroller->showMessage(item);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t_controller->showMessage(item);\n\t\t});\n\n\t\t_player->entity()->togglePlaylistRequests(\n\t\t) | rpl::on_next([=](bool shown) {\n\t\t\tif (!shown) {\n\t\t\t\t_playerPlaylist->hideFromOther();\n\t\t\t\treturn;\n\t\t\t} else if (_playerPlaylist->isHidden()) {\n\t\t\t\tauto position = mapFromGlobal(QCursor::pos()).x();\n\t\t\t\tauto bestPosition = _playerPlaylist->bestPositionFor(position);\n\t\t\t\tif (rtl()) bestPosition = position + 2 * (position - bestPosition) - _playerPlaylist->width();\n\t\t\t\tupdateMediaPlaylistPosition(bestPosition);\n\t\t\t}\n\t\t\t_playerPlaylist->showFromOther();\n\t\t}, _player->lifetime());\n\n\t\torderWidgets();\n\t\tif (_showAnimation) {\n\t\t\t_player->show(anim::type::instant);\n\t\t\t_player->setVisible(false);\n\t\t\tShortcuts::ToggleMediaShortcuts(true);\n\t\t} else {\n\t\t\t_player->hide(anim::type::instant);\n\t\t}\n\t}\n\tif (_player && !_player->toggled()) {\n\t\tif (!_showAnimation) {\n\t\t\t_player->show(anim::type::normal);\n\t\t\t_playerHeight = _contentScrollAddToY = _player->contentHeight();\n\t\t\tupdateControlsGeometry();\n\t\t\tShortcuts::ToggleMediaShortcuts(true);\n\t\t}\n\t}\n}\n\nvoid MainWidget::playerHeightUpdated() {\n\tif (!_player) {\n\t\t// Player could be already \"destroyDelayed\", but still handle events.\n\t\treturn;\n\t}\n\tauto playerHeight = _player->contentHeight();\n\tif (playerHeight != _playerHeight) {\n\t\t_contentScrollAddToY += playerHeight - _playerHeight;\n\t\t_playerHeight = playerHeight;\n\t\tupdateControlsGeometry();\n\t}\n\tif (!_playerHeight && _player->isHidden()) {\n\t\tconst auto state = Media::Player::instance()->getState(Media::Player::instance()->getActiveType());\n\t\tif (!state.id || Media::Player::IsStoppedOrStopping(state.state)) {\n\t\t\t_player.destroyDelayed();\n\t\t}\n\t}\n}\n\nvoid MainWidget::setCurrentCall(Calls::Call *call) {\n\tif (!call && _currentGroupCall) {\n\t\treturn;\n\t}\n\t_currentCallLifetime.destroy();\n\t_currentCall = call;\n\tif (call) {\n\t\t_callTopBar.destroy();\n\t\tcall->stateValue(\n\t\t) | rpl::on_next([=](Calls::Call::State state) {\n\t\t\tusing State = Calls::Call::State;\n\t\t\tif (state != State::Established) {\n\t\t\t\tdestroyCallTopBar();\n\t\t\t} else if (!_callTopBar) {\n\t\t\t\tcreateCallTopBar(call, nullptr);\n\t\t\t}\n\t\t}, _currentCallLifetime);\n\t} else {\n\t\tdestroyCallTopBar();\n\t}\n}\n\nvoid MainWidget::setCurrentGroupCall(Calls::GroupCall *call) {\n\tif (!call && _currentCall) {\n\t\treturn;\n\t}\n\t_currentCallLifetime.destroy();\n\t_currentGroupCall = call;\n\tif (call) {\n\t\t_callTopBar.destroy();\n\t\t_currentGroupCall->stateValue(\n\t\t) | rpl::on_next([=](Calls::GroupCall::State state) {\n\t\t\tusing State = Calls::GroupCall::State;\n\t\t\tif (state != State::Creating\n\t\t\t\t&& state != State::Waiting\n\t\t\t\t&& state != State::Joining\n\t\t\t\t&& state != State::Joined\n\t\t\t\t&& state != State::Connecting) {\n\t\t\t\tdestroyCallTopBar();\n\t\t\t} else if (!_callTopBar) {\n\t\t\t\tcreateCallTopBar(nullptr, call);\n\t\t\t}\n\t\t}, _currentCallLifetime);\n\t} else {\n\t\tdestroyCallTopBar();\n\t}\n}\n\nvoid MainWidget::createCallTopBar(\n\t\tCalls::Call *call,\n\t\tCalls::GroupCall *group) {\n\tExpects(call || group);\n\n\tconst auto show = controller()->uiShow();\n\t_callTopBar.create(\n\t\tthis,\n\t\t(call\n\t\t\t? object_ptr<Calls::TopBar>(this, call, show)\n\t\t\t: object_ptr<Calls::TopBar>(this, group, show)));\n\t_callTopBar->entity()->initBlobsUnder(this, _callTopBar->geometryValue());\n\t_callTopBar->heightValue(\n\t) | rpl::on_next([this](int value) {\n\t\tcallTopBarHeightUpdated(value);\n\t}, lifetime());\n\torderWidgets();\n\tif (_showAnimation) {\n\t\t_callTopBar->show(anim::type::instant);\n\t\t_callTopBar->setVisible(false);\n\t} else {\n\t\t_callTopBar->hide(anim::type::instant);\n\t\t_callTopBar->show(anim::type::normal);\n\t\t_callTopBarHeight = _contentScrollAddToY = _callTopBar->height();\n\t\tupdateControlsGeometry();\n\t}\n}\n\nvoid MainWidget::destroyCallTopBar() {\n\tif (_callTopBar) {\n\t\t_callTopBar->hide(anim::type::normal);\n\t}\n}\n\nvoid MainWidget::callTopBarHeightUpdated(int callTopBarHeight) {\n\tif (!callTopBarHeight && !_currentCall && !_currentGroupCall) {\n\t\t_callTopBar.destroyDelayed();\n\t}\n\tif (callTopBarHeight != _callTopBarHeight) {\n\t\t_contentScrollAddToY += callTopBarHeight - _callTopBarHeight;\n\t\t_callTopBarHeight = callTopBarHeight;\n\t\tupdateControlsGeometry();\n\t}\n}\n\nvoid MainWidget::setCurrentExportView(Export::View::PanelController *view) {\n\t_currentExportView = view;\n\tif (_currentExportView) {\n\t\t_currentExportView->progressState(\n\t\t) | rpl::on_next([=](Export::View::Content &&data) {\n\t\t\tif (!data.rows.empty()\n\t\t\t\t&& data.rows[0].id == Export::View::Content::kDoneId) {\n\t\t\t\tLOG((\"Export Info: Destroy top bar by Done.\"));\n\t\t\t\tdestroyExportTopBar();\n\t\t\t} else if (!_exportTopBar) {\n\t\t\t\tLOG((\"Export Info: Create top bar by State.\"));\n\t\t\t\tcreateExportTopBar(std::move(data));\n\t\t\t} else {\n\t\t\t\t_exportTopBar->entity()->updateData(std::move(data));\n\t\t\t}\n\t\t}, _exportViewLifetime);\n\t} else {\n\t\t_exportViewLifetime.destroy();\n\n\t\tLOG((\"Export Info: Destroy top bar by controller removal.\"));\n\t\tdestroyExportTopBar();\n\t}\n}\n\nvoid MainWidget::createExportTopBar(Export::View::Content &&data) {\n\t_exportTopBar.create(\n\t\tthis,\n\t\tobject_ptr<Export::View::TopBar>(this, std::move(data)),\n\t\t_controller->adaptive().oneColumnValue());\n\t_exportTopBar->entity()->clicks(\n\t) | rpl::on_next([=] {\n\t\tif (_currentExportView) {\n\t\t\t_currentExportView->activatePanel();\n\t\t}\n\t}, _exportTopBar->lifetime());\n\torderWidgets();\n\tif (_showAnimation) {\n\t\t_exportTopBar->show(anim::type::instant);\n\t\t_exportTopBar->setVisible(false);\n\t} else {\n\t\t_exportTopBar->hide(anim::type::instant);\n\t\t_exportTopBar->show(anim::type::normal);\n\t\t_exportTopBarHeight = _contentScrollAddToY = _exportTopBar->contentHeight();\n\t\tupdateControlsGeometry();\n\t}\n\trpl::merge(\n\t\t_exportTopBar->heightValue() | rpl::map_to(true),\n\t\t_exportTopBar->shownValue()\n\t) | rpl::on_next([=] {\n\t\texportTopBarHeightUpdated();\n\t}, _exportTopBar->lifetime());\n}\n\nvoid MainWidget::destroyExportTopBar() {\n\tif (_exportTopBar) {\n\t\t_exportTopBar->hide(anim::type::normal);\n\t}\n}\n\nvoid MainWidget::exportTopBarHeightUpdated() {\n\tif (!_exportTopBar) {\n\t\t// Player could be already \"destroyDelayed\", but still handle events.\n\t\treturn;\n\t}\n\tconst auto exportTopBarHeight = _exportTopBar->contentHeight();\n\tif (exportTopBarHeight != _exportTopBarHeight) {\n\t\t_contentScrollAddToY += exportTopBarHeight - _exportTopBarHeight;\n\t\t_exportTopBarHeight = exportTopBarHeight;\n\t\tupdateControlsGeometry();\n\t}\n\tif (!_exportTopBarHeight && _exportTopBar->isHidden()) {\n\t\t_exportTopBar.destroyDelayed();\n\t}\n}\n\nSendMenu::Details MainWidget::sendMenuDetails() const {\n\treturn _history->sendMenuDetails();\n}\n\nvoid MainWidget::dialogsCancelled() {\n\tif (_hider) {\n\t\t_hider->startHide();\n\t\tclearHider(_hider);\n\t}\n\t_history->activate();\n}\n\nvoid MainWidget::toggleFiltersMenu(bool value) const {\n\tif (_dialogs) {\n\t\t_dialogs->toggleFiltersMenu(value);\n\t}\n}\n\nvoid MainWidget::setChatBackground(\n\t\tconst Data::WallPaper &background,\n\t\tQImage &&image) {\n\tusing namespace Window::Theme;\n\n\tif (isReadyChatBackground(background, image)) {\n\t\tsetReadyChatBackground(background, std::move(image));\n\t\treturn;\n\t}\n\n\t_background = std::make_unique<SettingBackground>(background);\n\tif (const auto document = _background->data.document()) {\n\t\t_background->dataMedia = document->createMediaView();\n\t\t_background->dataMedia->thumbnailWanted(\n\t\t\t_background->data.fileOrigin());\n\t}\n\t_background->data.loadDocument();\n\tcheckChatBackground();\n\n\tconst auto tile = Data::IsLegacy1DefaultWallPaper(background);\n\tWindow::Theme::Background()->downloadingStarted(tile);\n}\n\nbool MainWidget::isReadyChatBackground(\n\t\tconst Data::WallPaper &background,\n\t\tconst QImage &image) const {\n\treturn !image.isNull() || !background.document();\n}\n\nvoid MainWidget::setReadyChatBackground(\n\t\tconst Data::WallPaper &background,\n\t\tQImage &&image) {\n\tusing namespace Window::Theme;\n\n\tif (image.isNull()\n\t\t&& !background.document()\n\t\t&& background.localThumbnail()) {\n\t\timage = background.localThumbnail()->original();\n\t}\n\n\tconst auto resetToDefault = image.isNull()\n\t\t&& !background.document()\n\t\t&& background.backgroundColors().empty()\n\t\t&& !Data::IsLegacy1DefaultWallPaper(background);\n\tconst auto ready = resetToDefault\n\t\t? Data::DefaultWallPaper()\n\t\t: background;\n\n\tBackground()->set(ready, std::move(image));\n\tconst auto tile = Data::IsLegacy1DefaultWallPaper(ready);\n\tBackground()->setTile(tile);\n\tUi::ForceFullRepaint(this);\n}\n\nbool MainWidget::chatBackgroundLoading() {\n\treturn (_background != nullptr);\n}\n\nfloat64 MainWidget::chatBackgroundProgress() const {\n\tif (_background) {\n\t\tif (_background->generating) {\n\t\t\treturn 1.;\n\t\t} else if (_background->data.document()) {\n\t\t\treturn _background->dataMedia->progress();\n\t\t}\n\t}\n\treturn 1.;\n}\n\nvoid MainWidget::checkChatBackground() {\n\tif (!_background || _background->generating) {\n\t\treturn;\n\t}\n\tconst auto &media = _background->dataMedia;\n\tAssert(media != nullptr);\n\tif (!media->loaded()) {\n\t\treturn;\n\t}\n\n\tconst auto document = _background->data.document();\n\tAssert(document != nullptr);\n\n\tconst auto generateCallback = [=](QImage &&image) {\n\t\tconst auto background = base::take(_background);\n\t\tconst auto ready = image.isNull()\n\t\t\t? Data::DefaultWallPaper()\n\t\t\t: background->data;\n\t\tsetReadyChatBackground(ready, std::move(image));\n\t};\n\t_background->generating = Data::ReadBackgroundImageAsync(\n\t\tmedia.get(),\n\t\tUi::PreprocessBackgroundImage,\n\t\tgenerateCallback);\n}\n\nImage *MainWidget::newBackgroundThumb() {\n\treturn !_background\n\t\t? nullptr\n\t\t: _background->data.localThumbnail()\n\t\t? _background->data.localThumbnail()\n\t\t: _background->dataMedia\n\t\t? _background->dataMedia->thumbnail()\n\t\t: nullptr;\n}\n\nvoid MainWidget::setInnerFocus() {\n\tconst auto setTo = [&](auto &&widget) {\n\t\tif (widget->isHidden()) {\n\t\t\t// If we try setting focus inside a hidden widget, we may\n\t\t\t// end up focusing search field in dialogs on window activation.\n\t\t\tsetFocus();\n\t\t} else {\n\t\t\twidget->setInnerFocus();\n\t\t}\n\t};\n\tif (_dialogs && _dialogs->searchHasFocus()) {\n\t\tsetTo(_dialogs);\n\t} else if (_hider || !_history->peer()) {\n\t\tif (!_hider && _mainSection) {\n\t\t\tsetTo(_mainSection);\n\t\t} else if (!_hider && _thirdSection) {\n\t\t\tsetTo(_thirdSection);\n\t\t} else if (_dialogs) {\n\t\t\tsetTo(_dialogs);\n\t\t} else {\n\t\t\t// Maybe we're just closing a child window, content is destroyed.\n\t\t\t_history->setFocus();\n\t\t}\n\t} else if (_mainSection) {\n\t\tsetTo(_mainSection);\n\t} else {\n\t\tsetTo(_history);\n\t}\n}\n\nvoid MainWidget::showChooseReportMessages(\n\t\tnot_null<PeerData*> peer,\n\t\tData::ReportInput reportInput,\n\t\tFn<void(std::vector<MsgId>)> done) {\n\t_history->setChooseReportMessagesDetails(reportInput, std::move(done));\n\t_controller->showPeerHistory(\n\t\tpeer,\n\t\tSectionShow::Way::Forward,\n\t\tShowForChooseMessagesMsgId);\n\tcontroller()->showToast(tr::lng_report_please_select_messages(tr::now));\n}\n\nvoid MainWidget::clearChooseReportMessages() {\n\t_history->setChooseReportMessagesDetails({}, nullptr);\n}\n\nvoid MainWidget::toggleChooseChatTheme(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::optional<bool> show) {\n\t_history->toggleChooseChatTheme(peer, show);\n}\n\nbool MainWidget::showHistoryInDifferentWindow(\n\t\tPeerId peerId,\n\t\tconst SectionShow &params,\n\t\tMsgId showAtMsgId) {\n\tif (!peerId) {\n\t\t// In case we don't have dialogs, we can't clear section stack.\n\t\treturn !_dialogs;\n\t}\n\tconst auto peer = session().data().peer(peerId);\n\tif (const auto separateChat = _controller->windowId().chat()) {\n\t\tif (const auto history = separateChat->asHistory()) {\n\t\t\tif (history->peer == peer) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto window = Core::App().windowForShowingHistory(peer);\n\tif (window == &_controller->window()) {\n\t\treturn false;\n\t} else if (window) {\n\t\twindow->sessionController()->showPeerHistory(\n\t\t\tpeerId,\n\t\t\tparams,\n\t\t\tshowAtMsgId);\n\t\twindow->activate();\n\t\treturn true;\n\t} else if (windowId().hasChatsList()) {\n\t\treturn false;\n\t}\n\tconst auto account = not_null(&session().account());\n\tauto primary = Core::App().separateWindowFor(account);\n\tif (!primary) {\n\t\tCore::App().domain().activate(account);\n\t\tprimary = Core::App().separateWindowFor(account);\n\t}\n\tif (primary && &primary->account() == account) {\n\t\tprimary->sessionController()->showPeerHistory(\n\t\t\tpeerId,\n\t\t\tparams,\n\t\t\tshowAtMsgId);\n\t\tprimary->activate();\n\t}\n\treturn true;\n}\n\nvoid MainWidget::showHistory(\n\t\tPeerId peerId,\n\t\tconst SectionShow &params,\n\t\tMsgId showAtMsgId) {\n\tif (peerId && _controller->window().locked()) {\n\t\tif (params.activation != anim::activation::background) {\n\t\t\t_controller->window().activate();\n\t\t}\n\t\treturn;\n\t} else if (auto peer = session().data().peerLoaded(peerId)) {\n\t\tif (peer->migrateTo()) {\n\t\t\tpeer = peer->migrateTo();\n\t\t\tpeerId = peer->id;\n\t\t\tif (showAtMsgId > 0) {\n\t\t\t\tshowAtMsgId = -showAtMsgId;\n\t\t\t}\n\t\t}\n\t\tconst auto unavailable = peer->computeUnavailableReason();\n\t\tif (!unavailable.isEmpty()) {\n\t\t\tif (!isPrimary()) {\n\t\t\t\t_controller->window().close();\n\t\t\t} else if (params.activation != anim::activation::background) {\n\t\t\t\t_controller->show(Ui::MakeInformBox(unavailable));\n\t\t\t\t_controller->window().activate();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t}\n\tif ((IsServerMsgId(showAtMsgId) || Data::IsScheduledMsgId(showAtMsgId))\n\t\t&& _mainSection\n\t\t&& _mainSection->showMessage(peerId, params, showAtMsgId)) {\n\t\tsession().data().hideShownSpoilers();\n\t\tif (params.activation != anim::activation::background) {\n\t\t\t_controller->window().activate();\n\t\t}\n\t\treturn;\n\t} else if (showHistoryInDifferentWindow(peerId, params, showAtMsgId)) {\n\t\treturn;\n\t}\n\n\tif (peerId && params.activation != anim::activation::background) {\n\t\tCore::App().hideMediaView();\n\t\t_controller->window().activate();\n\t}\n\n\tconst auto alreadyThatPeer = _history->peer()\n\t\t&& (_history->peer()->id == peerId);\n\tif (!alreadyThatPeer\n\t\t&& preventsCloseSection(\n\t\t\t[=] { showHistory(peerId, params, showAtMsgId); },\n\t\t\tparams)) {\n\t\treturn;\n\t}\n\n\tusing OriginMessage = SectionShow::OriginMessage;\n\tif (const auto origin = std::get_if<OriginMessage>(&params.origin)) {\n\t\tif (const auto returnTo = session().data().message(origin->id)) {\n\t\t\tif (returnTo->history()->peer->id == peerId) {\n\t\t\t\t_history->pushReplyReturn(returnTo);\n\t\t\t}\n\t\t}\n\t}\n\n\t_a_dialogsWidth.stop();\n\n\tusing Way = SectionShow::Way;\n\tauto way = params.way;\n\tbool back = (way == Way::Backward || !peerId);\n\tbool foundInStack = !peerId;\n\tif (foundInStack || (way == Way::ClearStack)) {\n\t\tfor (const auto &item : _stack) {\n\t\t\tClearBotStartToken(item->peer());\n\t\t}\n\t\t_stack.clear();\n\t} else {\n\t\tif (!params.allowDuplicateInStack) {\n\t\t\tfor (auto i = 0, s = int(_stack.size()); i < s; ++i) {\n\t\t\t\tif (_stack.at(i)->type() == HistoryStackItem\n\t\t\t\t\t&& _stack.at(i)->peer()->id == peerId) {\n\t\t\t\t\tfoundInStack = true;\n\t\t\t\t\twhile (int(_stack.size()) > i + 1) {\n\t\t\t\t\t\tClearBotStartToken(_stack.back()->peer());\n\t\t\t\t\t\t_stack.pop_back();\n\t\t\t\t\t}\n\t\t\t\t\t_stack.pop_back();\n\t\t\t\t\tif (!back) {\n\t\t\t\t\t\tback = true;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (const auto activeChat = _controller->activeChatCurrent()) {\n\t\t\tif (const auto peer = activeChat.peer()) {\n\t\t\t\tif (way == Way::Forward && peer->id == peerId) {\n\t\t\t\t\tway = _mainSection ? Way::Backward : Way::ClearStack;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tconst auto wasActivePeer = _controller->activeChatCurrent().peer();\n\tif (params.activation != anim::activation::background) {\n\t\t_controller->window().hideSettingsAndLayer();\n\t}\n\n\tauto animatedShow = [&] {\n\t\tif (_showAnimation\n\t\t\t|| Core::App().passcodeLocked()\n\t\t\t|| (params.animated == anim::type::instant)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (!peerId) {\n\t\t\tif (isOneColumn()) {\n\t\t\t\treturn _dialogs && _dialogs->isHidden();\n\t\t\t} else {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\tif (_history->isHidden()) {\n\t\t\tif (!isOneColumn() && way == Way::ClearStack) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn (_mainSection != nullptr)\n\t\t\t\t|| (isOneColumn() && _dialogs && !_dialogs->isHidden());\n\t\t}\n\t\tif (back || way == Way::Forward) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\n\tauto animationParams = animatedShow()\n\t\t? prepareHistoryAnimation(peerId)\n\t\t: Window::SectionSlideParams();\n\n\tif (!back && (way != Way::ClearStack)) {\n\t\t// This may modify the current section, for example remove its contents.\n\t\tsaveSectionInStack(params);\n\t}\n\n\tif (_history->peer()\n\t\t&& _history->peer()->id != peerId\n\t\t&& way != Way::Forward) {\n\t\tClearBotStartToken(_history->peer());\n\t}\n\t_history->showHistory(peerId, showAtMsgId, params);\n\tif (alreadyThatPeer && params.reapplyLocalDraft) {\n\t\t_history->applyDraft(HistoryWidget::FieldHistoryAction::NewEntry);\n\t}\n\n\tauto noPeer = !_history->peer();\n\tauto onlyDialogs = noPeer && isOneColumn();\n\t_mainSection.destroy();\n\n\tupdateMainSectionShown();\n\tupdateControlsGeometry();\n\n\tif (noPeer) {\n\t\t_controller->setActiveChatEntry(Dialogs::Key());\n\t\t_controller->setChatStyleTheme(_controller->defaultChatTheme());\n\t}\n\n\tif (onlyDialogs) {\n\t\tAssert(_dialogs != nullptr);\n\t\t_history->hide();\n\t\tif (!_showAnimation) {\n\t\t\tif (animationParams) {\n\t\t\t\tauto direction = back ? Window::SlideDirection::FromLeft : Window::SlideDirection::FromRight;\n\t\t\t\t_dialogs->showAnimated(direction, animationParams);\n\t\t\t} else {\n\t\t\t\t_dialogs->showFast();\n\t\t\t}\n\t\t}\n\t} else {\n\t\tconst auto nowActivePeer = _controller->activeChatCurrent().peer();\n\t\tif (nowActivePeer && nowActivePeer != wasActivePeer) {\n\t\t\tsession().api().views().removeIncremented(nowActivePeer);\n\t\t}\n\t\tif (isOneColumn() && _dialogs && !_dialogs->isHidden()) {\n\t\t\t_dialogs->hide();\n\t\t}\n\t\tif (!_showAnimation) {\n\t\t\tif (!animationParams.oldContentCache.isNull()) {\n\t\t\t\t_history->showAnimated(\n\t\t\t\t\tback\n\t\t\t\t\t\t? Window::SlideDirection::FromLeft\n\t\t\t\t\t\t: Window::SlideDirection::FromRight,\n\t\t\t\t\tanimationParams);\n\t\t\t} else {\n\t\t\t\t_history->showFast();\n\t\t\t\tcrl::on_main(this, [=] {\n\t\t\t\t\t_controller->widget()->setInnerFocus();\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tif (_dialogs && !_dialogs->isHidden()) {\n\t\tif (!back) {\n\t\t\tif (const auto history = _history->history()) {\n\t\t\t\t_dialogs->scrollToEntry(Dialogs::RowDescriptor(\n\t\t\t\t\thistory,\n\t\t\t\t\tFullMsgId(history->peer->id, showAtMsgId)));\n\t\t\t}\n\t\t}\n\t\t_dialogs->update();\n\t}\n\n\tfloatPlayerCheckVisibility();\n\n\tcontroller()->dropSubsectionTabs();\n}\n\nbool MainWidget::handleDrawToReplyRequest(Data::DrawToReplyRequest request) {\n\tif (_mainSection) {\n\t\tusing namespace HistoryView;\n\t\tif (const auto с = dynamic_cast<ChatWidget*>(_mainSection.data())) {\n\t\t\treturn с->handleDrawToReplyRequest(std::move(request));\n\t\t}\n\t\treturn false;\n\t}\n\treturn _history->handleDrawToReplyRequest(std::move(request));\n}\n\nvoid MainWidget::showMessage(\n\t\tnot_null<const HistoryItem*> item,\n\t\tconst SectionShow &params) {\n\tconst auto peerId = item->history()->peer->id;\n\tconst auto itemId = item->id;\n\tif (!v::is_null(params.origin)) {\n\t\tif (_mainSection) {\n\t\t\tif (_mainSection->showMessage(peerId, params, itemId)) {\n\t\t\t\tif (params.activation != anim::activation::background) {\n\t\t\t\t\t_controller->window().activate();\n\t\t\t\t\t_controller->window().hideSettingsAndLayer();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else if (_history->peer() == item->history()->peer) {\n\t\t\t// showHistory may be redirected to different window,\n\t\t\t// so we don't call activate() on current controller's window.\n\t\t\tshowHistory(peerId, params, itemId);\n\t\t\treturn;\n\t\t}\n\t}\n\tif (const auto topic = item->topic()) {\n\t\t_controller->showTopic(topic, item->id, params);\n\t\tif (params.activation != anim::activation::background) {\n\t\t\t_controller->window().activate();\n\t\t}\n\t} else if (const auto sublist = item->savedSublist()\n\t\t; sublist && sublist->parentChat()) {\n\t\t_controller->showSublist(sublist, item->id, params);\n\t\tif (params.activation != anim::activation::background) {\n\t\t\t_controller->window().activate();\n\t\t}\n\t} else {\n\t\t// showPeerHistory may be redirected to different window,\n\t\t// so we don't call activate() on current controller's window.\n\t\t_controller->showPeerHistory(\n\t\t\titem->history(),\n\t\t\tparams,\n\t\t\titem->id);\n\t}\n}\n\nvoid MainWidget::showForum(\n\t\tnot_null<Data::Forum*> forum,\n\t\tconst SectionShow &params) {\n\tExpects(_dialogs != nullptr);\n\n\t_dialogs->showForum(forum, params);\n\n\tif (params.activation != anim::activation::background) {\n\t\t_controller->window().hideSettingsAndLayer();\n\t}\n}\n\nPeerData *MainWidget::peer() const {\n\treturn _history->peer();\n}\n\nUi::ChatTheme *MainWidget::customChatTheme() const {\n\treturn _history->customChatTheme();\n}\n\nbool MainWidget::saveSectionInStack(\n\t\tconst SectionShow &params,\n\t\tWindow::SectionWidget *newMainSection) {\n\tif (_mainSection) {\n\t\tif (auto memento = _mainSection->createMemento()) {\n\t\t\tif (params.dropSameFromStack\n\t\t\t\t&& newMainSection\n\t\t\t\t&& newMainSection->sameTypeAs(memento.get())) {\n\t\t\t\t// When choosing saved sublist we want to save the original\n\t\t\t\t// \"Saved Messages\" in the stack, but don't save every\n\t\t\t\t// sublist in a new stack entry when clicking them through.\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t_stack.push_back(std::make_unique<StackItemSection>(\n\t\t\t\tstd::move(memento)));\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t} else if (const auto history = _history->history()) {\n\t\t_stack.push_back(std::make_unique<StackItemHistory>(\n\t\t\thistory,\n\t\t\t_history->msgId(),\n\t\t\t_history->replyReturns()));\n\t} else {\n\t\t// We pretend that we \"saved\" the chats list state in stack,\n\t\t// so that we do animate a transition from chats list to a section.\n\t\treturn true;\n\t}\n\tconst auto raw = _stack.back().get();\n\traw->setThirdSectionWeak(_thirdSection.data());\n\traw->removeRequests(\n\t) | rpl::on_next([=] {\n\t\tfor (auto i = begin(_stack); i != end(_stack); ++i) {\n\t\t\tif (i->get() == raw) {\n\t\t\t\t_stack.erase(i);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}, raw->lifetime());\n\treturn true;\n}\n\nvoid MainWidget::showSection(\n\t\tstd::shared_ptr<Window::SectionMemento> memento,\n\t\tconst SectionShow &params) {\n\tif (_mainSection && _mainSection->showInternal(\n\t\t\tmemento.get(),\n\t\t\tparams)) {\n\t\tif (params.activation != anim::activation::background) {\n\t\t\t_controller->window().hideSettingsAndLayer();\n\t\t}\n\t\tif (const auto entry = _mainSection->activeChat(); entry.key) {\n\t\t\t_controller->setActiveChatEntry(entry);\n\t\t}\n\t\treturn;\n\t//\n\t// Now third section handles only its own showSection() requests.\n\t// General showSection() should show layer or main_section instead.\n\t//\n\t//} else if (_thirdSection && _thirdSection->showInternal(\n\t//\t\t&memento,\n\t//\t\tparams)) {\n\t//\treturn;\n\t}\n\n\tif (preventsCloseSection(\n\t\t[=] { showSection(memento, params); },\n\t\tparams)) {\n\t\treturn;\n\t}\n\n\t// If the window was not resized, but we've enabled\n\t// tabbedSelectorSectionEnabled or thirdSectionInfoEnabled\n\t// we need to update adaptive layout to Adaptive::ThirdColumn().\n\tupdateColumnLayout();\n\n\tshowNewSection(std::move(memento), params);\n}\n\nvoid MainWidget::updateColumnLayout() {\n\tupdateWindowAdaptiveLayout();\n}\n\nWindow::SectionSlideParams MainWidget::prepareThirdSectionAnimation(Window::SectionWidget *section) {\n\tExpects(_thirdSection != nullptr);\n\n\tWindow::SectionSlideParams result;\n\tresult.withTopBarShadow = section->hasTopBarShadow();\n\tif (!_thirdSection->hasTopBarShadow()) {\n\t\tresult.withTopBarShadow = false;\n\t}\n\tfloatPlayerHideAll();\n\tresult.oldContentCache = _thirdSection->grabForShowAnimation(result);\n\tfloatPlayerShowVisible();\n\treturn result;\n}\n\nWindow::SectionSlideParams MainWidget::prepareShowAnimation(\n\t\tbool willHaveTopBarShadow) {\n\tWindow::SectionSlideParams result;\n\tresult.withTopBarShadow = willHaveTopBarShadow;\n\tif (_mainSection) {\n\t\tif (!_mainSection->hasTopBarShadow()) {\n\t\t\tresult.withTopBarShadow = false;\n\t\t}\n\t} else if (!_history->peer()) {\n\t\tresult.withTopBarShadow = false;\n\t}\n\n\tfloatPlayerHideAll();\n\tif (_player) {\n\t\t_player->entity()->hideShadowAndDropdowns();\n\t}\n\tconst auto playerPlaylistVisible = !_playerPlaylist->isHidden();\n\tif (playerPlaylistVisible) {\n\t\t_playerPlaylist->hide();\n\t}\n\tconst auto hiderVisible = (_hider && !_hider->isHidden());\n\tif (hiderVisible) {\n\t\t_hider->hide();\n\t}\n\n\tauto sectionTop = getMainSectionTop();\n\tif (_mainSection) {\n\t\tresult.oldContentCache = _mainSection->grabForShowAnimation(result);\n\t} else if (!isOneColumn() || !_history->isHidden()) {\n\t\tresult.oldContentCache = _history->grabForShowAnimation(result);\n\t} else {\n\t\tresult.oldContentCache = Ui::GrabWidget(this, QRect(\n\t\t\t0,\n\t\t\tsectionTop,\n\t\t\t_dialogsWidth,\n\t\t\theight() - sectionTop));\n\t}\n\n\tif (_hider && hiderVisible) {\n\t\t_hider->show();\n\t}\n\tif (playerPlaylistVisible) {\n\t\t_playerPlaylist->show();\n\t}\n\tif (_player) {\n\t\t_player->entity()->showShadowAndDropdowns();\n\t}\n\tfloatPlayerShowVisible();\n\n\treturn result;\n}\n\nWindow::SectionSlideParams MainWidget::prepareMainSectionAnimation(Window::SectionWidget *section) {\n\treturn prepareShowAnimation(section->hasTopBarShadow());\n}\n\nWindow::SectionSlideParams MainWidget::prepareHistoryAnimation(PeerId historyPeerId) {\n\treturn prepareShowAnimation(historyPeerId != 0);\n}\n\nWindow::SectionSlideParams MainWidget::prepareDialogsAnimation() {\n\treturn prepareShowAnimation(false);\n}\n\nvoid MainWidget::showNewSection(\n\t\tstd::shared_ptr<Window::SectionMemento> memento,\n\t\tconst SectionShow &params) {\n\tusing Column = Window::Column;\n\n\tif (_controller->window().locked()) {\n\t\treturn;\n\t}\n\tauto saveInStack = (params.way == SectionShow::Way::Forward);\n\tconst auto thirdSectionTop = getThirdSectionTop();\n\tconst auto newThirdGeometry = QRect(\n\t\twidth() - st::columnMinimalWidthThird,\n\t\tthirdSectionTop,\n\t\tst::columnMinimalWidthThird,\n\t\theight() - thirdSectionTop);\n\tauto newThirdSection = (isThreeColumn() && params.thirdColumn)\n\t\t? memento->createWidget(\n\t\t\tthis,\n\t\t\t_controller,\n\t\t\tColumn::Third,\n\t\t\tnewThirdGeometry)\n\t\t: nullptr;\n\tconst auto layerRect = parentWidget()->rect();\n\tif (newThirdSection) {\n\t\tsaveInStack = false;\n\t} else if (auto layer = memento->createLayer(_controller, layerRect)) {\n\t\tif (params.activation != anim::activation::background) {\n\t\t\t_controller->hideLayer(anim::type::instant);\n\t\t}\n\t\t_controller->showSpecialLayer(std::move(layer));\n\t\treturn;\n\t}\n\n\tif (params.activation != anim::activation::background) {\n\t\t_controller->window().hideSettingsAndLayer();\n\t}\n\n\t_a_dialogsWidth.stop();\n\n\tauto mainSectionTop = getMainSectionTop();\n\tauto newMainGeometry = QRect(\n\t\t_history->x(),\n\t\tmainSectionTop,\n\t\t_history->width(),\n\t\theight() - mainSectionTop);\n\tauto newMainSection = newThirdSection\n\t\t? nullptr\n\t\t: memento->createWidget(\n\t\t\tthis,\n\t\t\t_controller,\n\t\t\tisOneColumn() ? Column::First : Column::Second,\n\t\t\tnewMainGeometry);\n\tAssert(newMainSection || newThirdSection);\n\n\tauto animatedShow = [&] {\n\t\tif (_showAnimation\n\t\t\t|| Core::App().passcodeLocked()\n\t\t\t|| (params.animated == anim::type::instant)\n\t\t\t|| memento->instant()) {\n\t\t\treturn false;\n\t\t}\n\t\tif (!isOneColumn() && params.way == SectionShow::Way::ClearStack) {\n\t\t\treturn false;\n\t\t} else if (isOneColumn()\n\t\t\t|| (newThirdSection && _thirdSection)\n\t\t\t|| (newMainSection && isMainSectionShown())) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}();\n\tauto animationParams = animatedShow\n\t\t? (newThirdSection\n\t\t\t? prepareThirdSectionAnimation(newThirdSection)\n\t\t\t: prepareMainSectionAnimation(newMainSection))\n\t\t: Window::SectionSlideParams();\n\n\tsetFocus(); // otherwise dialogs widget could be focused.\n\n\tif (saveInStack) {\n\t\t// This may modify the current section, for example remove its contents.\n\t\tif (!saveSectionInStack(params, newMainSection)) {\n\t\t\tsaveInStack = false;\n\t\t\tanimatedShow = false;\n\t\t\tanimationParams = Window::SectionSlideParams();\n\t\t}\n\t}\n\tauto &settingSection = newThirdSection\n\t\t? _thirdSection\n\t\t: _mainSection;\n\tif (newThirdSection) {\n\t\t_thirdSection = std::move(newThirdSection);\n\t\t_thirdSection->removeRequests(\n\t\t) | rpl::on_next([=] {\n\t\t\tdestroyThirdSection();\n\t\t\t_thirdShadow.destroy();\n\t\t\tupdateControlsGeometry();\n\t\t}, _thirdSection->lifetime());\n\t\tif (!_thirdShadow) {\n\t\t\t_thirdShadow.create(this);\n\t\t\t_thirdShadow->show();\n\t\t\torderWidgets();\n\t\t}\n\t\tupdateControlsGeometry();\n\t} else {\n\t\t_mainSection = std::move(newMainSection);\n\t\t_history->finishAnimating();\n\t\t_history->showHistory(PeerId(), MsgId());\n\n\t\tif (const auto entry = _mainSection->activeChat(); entry.key) {\n\t\t\t_controller->setActiveChatEntry(entry);\n\t\t}\n\t\tupdateMainSectionShown();\n\n\t\t// Depends on SessionController::activeChatEntry\n\t\t// for tabbed selector showing in the third column.\n\t\tupdateControlsGeometry();\n\n\t\t_history->hide();\n\t\tif (isOneColumn() && _dialogs) {\n\t\t\t_dialogs->hide();\n\t\t}\n\t}\n\n\tif (animationParams) {\n\t\tauto back = (params.way == SectionShow::Way::Backward);\n\t\tauto direction = (back || settingSection->forceAnimateBack())\n\t\t\t? Window::SlideDirection::FromLeft\n\t\t\t: Window::SlideDirection::FromRight;\n\t\tif (isOneColumn()) {\n\t\t\t_controller->removeLayerBlackout();\n\t\t}\n\t\tsettingSection->showAnimated(direction, animationParams);\n\t} else {\n\t\tsettingSection->showFast();\n\t}\n\n\tfloatPlayerCheckVisibility();\n\torderWidgets();\n}\n\nvoid MainWidget::checkMainSectionToLayer() {\n\tif (!_mainSection) {\n\t\treturn;\n\t}\n\tUi::FocusPersister persister(this);\n\tif (auto layer = _mainSection->moveContentToLayer(rect())) {\n\t\t_mainSection.destroy();\n\t\t_controller->showBackFromStack(\n\t\t\tSectionShow(\n\t\t\t\tanim::type::instant,\n\t\t\t\tanim::activation::background));\n\t\t_controller->showSpecialLayer(\n\t\t\tstd::move(layer),\n\t\t\tanim::type::instant);\n\t}\n\tupdateMainSectionShown();\n}\n\nWindow::SeparateId MainWidget::windowId() const {\n\treturn _controller->windowId();\n}\n\nbool MainWidget::isPrimary() const {\n\treturn _controller->isPrimary();\n}\n\nbool MainWidget::isMainSectionShown() const {\n\treturn _mainSection || _history->peer();\n}\n\nbool MainWidget::isThirdSectionShown() const {\n\treturn _thirdSection != nullptr;\n}\n\nDialogs::RowDescriptor MainWidget::resolveChatNext(\n\t\tDialogs::RowDescriptor from) const {\n\treturn _dialogs ? _dialogs->resolveChatNext(from) : Dialogs::RowDescriptor();\n}\n\nDialogs::RowDescriptor MainWidget::resolveChatPrevious(\n\t\tDialogs::RowDescriptor from) const {\n\treturn _dialogs ? _dialogs->resolveChatPrevious(from) : Dialogs::RowDescriptor();\n}\n\nbool MainWidget::stackIsEmpty() const {\n\treturn _stack.empty();\n}\n\nbool MainWidget::preventsCloseSection(Fn<void()> callback) const {\n\tif (Core::App().passcodeLocked()) {\n\t\treturn false;\n\t}\n\tauto copy = callback;\n\treturn (_mainSection && _mainSection->preventsClose(std::move(copy)))\n\t\t|| (_history && _history->preventsClose(std::move(callback)));\n}\n\nbool MainWidget::preventsCloseSection(\n\t\tFn<void()> callback,\n\t\tconst SectionShow &params) const {\n\treturn !params.thirdColumn\n\t\t&& (params.activation != anim::activation::background)\n\t\t&& preventsCloseSection(std::move(callback));\n}\n\nvoid MainWidget::showNonPremiumLimitToast(bool download) {\n\tconst auto parent = _mainSection\n\t\t? ((QWidget*)_mainSection.data())\n\t\t: (_dialogs && _history->isHidden())\n\t\t? ((QWidget*)_dialogs.get())\n\t\t: ((QWidget*)_history.get());\n\tconst auto link = download\n\t\t? tr::lng_limit_download_subscribe_link(tr::now)\n\t\t: tr::lng_limit_upload_subscribe_link(tr::now);\n\tconst auto better = session().appConfig().get<double>(download\n\t\t? u\"upload_premium_speedup_download\"_q\n\t\t: u\"upload_premium_speedup_upload\"_q, 10.);\n\tconst auto percent = int(base::SafeRound(better * 100.));\n\tif (percent <= 100) {\n\t\treturn;\n\t}\n\tconst auto increase = ((percent % 100) || percent <= 400)\n\t\t? (download\n\t\t\t? tr::lng_limit_download_increase_speed\n\t\t\t: tr::lng_limit_upload_increase_speed)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_percent,\n\t\t\t\tTextWithEntities{ QString::number(percent - 100) },\n\t\t\t\ttr::rich)\n\t\t: (download\n\t\t\t? tr::lng_limit_download_increase_times\n\t\t\t: tr::lng_limit_upload_increase_times)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tpercent / 100,\n\t\t\t\ttr::rich);\n\tauto text = (download\n\t\t? tr::lng_limit_download_subscribe\n\t\t: tr::lng_limit_upload_subscribe)(\n\t\t\ttr::now,\n\t\t\tlt_link,\n\t\t\ttr::link(tr::bold(link)),\n\t\t\tlt_increase,\n\t\t\tTextWithEntities{ increase },\n\t\t\ttr::rich);\n\tauto filter = [=](ClickHandlerPtr handler, Qt::MouseButton button) {\n\t\tSettings::ShowPremium(\n\t\t\tcontroller(),\n\t\t\tdownload ? u\"download_limit\"_q : u\"upload_limit\"_q);\n\t\treturn false;\n\t};\n\tUi::Toast::Show(parent, {\n\t\t.title = (download\n\t\t\t? tr::lng_limit_download_title\n\t\t\t: tr::lng_limit_upload_title)(tr::now),\n\t\t.text = std::move(text),\n\t\t.filter = std::move(filter),\n\t\t.attach = RectPart::Top,\n\t\t.duration = 5 * crl::time(1000),\n\t});\n}\n\nbool MainWidget::showBackFromStack(const SectionShow &params) {\n\tif (preventsCloseSection([=] { showBackFromStack(params); }, params)) {\n\t\treturn false;\n\t}\n\n\tif (_stack.empty()) {\n\t\tif (_dialogs) {\n\t\t\t_controller->clearSectionStack(params);\n\t\t}\n\t\tcrl::on_main(this, [=] {\n\t\t\t_controller->widget()->setInnerFocus();\n\t\t});\n\t\treturn (_dialogs != nullptr);\n\t}\n\tsession().api().saveCurrentDraftToCloud();\n\n\tauto item = std::move(_stack.back());\n\t_stack.pop_back();\n\tif (const auto currentHistoryPeer = _history->peer()) {\n\t\tClearBotStartToken(currentHistoryPeer);\n\t}\n\t_thirdSectionFromStack = item->takeThirdSectionMemento();\n\tif (item->type() == HistoryStackItem) {\n\t\tauto historyItem = static_cast<StackItemHistory*>(item.get());\n\t\t_controller->showPeerHistory(\n\t\t\thistoryItem->peer()->id,\n\t\t\tparams.withWay(SectionShow::Way::Backward),\n\t\t\tShowAtUnreadMsgId);\n\t\t_history->setReplyReturns(\n\t\t\thistoryItem->peer()->id,\n\t\t\tstd::move(historyItem->replyReturns));\n\t} else if (item->type() == SectionStackItem) {\n\t\tauto sectionItem = static_cast<StackItemSection*>(item.get());\n\t\tshowNewSection(\n\t\t\tsectionItem->takeMemento(),\n\t\t\tparams.withWay(SectionShow::Way::Backward));\n\t}\n\tif (_thirdSectionFromStack && _thirdSection) {\n\t\t_controller->showSection(\n\t\t\tbase::take(_thirdSectionFromStack),\n\t\t\tSectionShow(\n\t\t\t\tSectionShow::Way::ClearStack,\n\t\t\t\tanim::type::instant,\n\t\t\t\tanim::activation::background));\n\n\t}\n\treturn true;\n}\n\nvoid MainWidget::orderWidgets() {\n\tif (_dialogs) {\n\t\t_dialogs->raiseWithTooltip();\n\t}\n\tif (_player) {\n\t\t_player->raise();\n\t}\n\tif (_exportTopBar) {\n\t\t_exportTopBar->raise();\n\t}\n\tif (_callTopBar) {\n\t\t_callTopBar->raise();\n\t}\n\tif (_sideShadow) {\n\t\t_sideShadow->raise();\n\t}\n\tif (_thirdShadow) {\n\t\t_thirdShadow->raise();\n\t}\n\tif (_firstColumnResizeArea) {\n\t\t_firstColumnResizeArea->raise();\n\t}\n\tif (_thirdColumnResizeArea) {\n\t\t_thirdColumnResizeArea->raise();\n\t}\n\tif (_connecting) {\n\t\t_connecting->raise();\n\t}\n\tfloatPlayerRaiseAll();\n\t_playerPlaylist->raise();\n\tif (_player) {\n\t\t_player->entity()->raiseDropdowns();\n\t}\n\tif (_hider) _hider->raise();\n}\n\nQPixmap MainWidget::grabForShowAnimation(const Window::SectionSlideParams &params) {\n\tQPixmap result;\n\tfloatPlayerHideAll();\n\tif (_player) {\n\t\t_player->entity()->hideShadowAndDropdowns();\n\t}\n\tconst auto playerPlaylistVisible = !_playerPlaylist->isHidden();\n\tif (playerPlaylistVisible) {\n\t\t_playerPlaylist->hide();\n\t}\n\tconst auto hiderVisible = (_hider && !_hider->isHidden());\n\tif (hiderVisible) {\n\t\t_hider->hide();\n\t}\n\n\tauto sectionTop = getMainSectionTop();\n\tif (isOneColumn()) {\n\t\tresult = Ui::GrabWidget(this, QRect(\n\t\t\t0,\n\t\t\tsectionTop,\n\t\t\twidth(),\n\t\t\theight() - sectionTop));\n\t} else {\n\t\tif (_sideShadow) {\n\t\t\t_sideShadow->hide();\n\t\t}\n\t\tif (_thirdShadow) {\n\t\t\t_thirdShadow->hide();\n\t\t}\n\t\tresult = Ui::GrabWidget(this, QRect(\n\t\t\t_dialogsWidth,\n\t\t\tsectionTop,\n\t\t\twidth() - _dialogsWidth,\n\t\t\theight() - sectionTop));\n\t\tif (_sideShadow) {\n\t\t\t_sideShadow->show();\n\t\t}\n\t\tif (_thirdShadow) {\n\t\t\t_thirdShadow->show();\n\t\t}\n\t}\n\tif (_hider && hiderVisible) {\n\t\t_hider->show();\n\t}\n\tif (playerPlaylistVisible) {\n\t\t_playerPlaylist->show();\n\t}\n\tif (_player) {\n\t\t_player->entity()->showShadowAndDropdowns();\n\t}\n\tfloatPlayerShowVisible();\n\treturn result;\n}\n\nvoid MainWidget::windowShown() {\n\t_history->windowShown();\n}\n\nvoid MainWidget::dialogsToUp() {\n\tif (_dialogs) {\n\t\t_dialogs->jumpToTop();\n\t}\n}\n\nvoid MainWidget::checkActivation() {\n\t_history->checkActivation();\n\tif (_mainSection) {\n\t\t_mainSection->checkActivation();\n\t}\n}\n\nvoid MainWidget::showAnimated(QPixmap oldContentCache, bool back) {\n\t_showAnimation = nullptr;\n\n\tshowAll();\n\tfloatPlayerHideAll();\n\tauto newContentCache = Ui::GrabWidget(this);\n\thideAll();\n\tfloatPlayerShowVisible();\n\n\t_showAnimation = std::make_unique<Window::SlideAnimation>();\n\t_showAnimation->setDirection(back\n\t\t? Window::SlideDirection::FromLeft\n\t\t: Window::SlideDirection::FromRight);\n\t_showAnimation->setRepaintCallback([=] { update(); });\n\t_showAnimation->setFinishedCallback([=] { showFinished(); });\n\t_showAnimation->setPixmaps(oldContentCache, newContentCache);\n\t_showAnimation->start();\n\n\tshow();\n}\n\nvoid MainWidget::showFinished() {\n\t_showAnimation = nullptr;\n\n\tshowAll();\n\tactivate();\n}\n\nvoid MainWidget::paintEvent(QPaintEvent *e) {\n\tif (_background) {\n\t\tcheckChatBackground();\n\t}\n\tif (_showAnimation) {\n\t\tauto p = QPainter(this);\n\t\t_showAnimation->paintContents(p);\n\t}\n}\n\nint MainWidget::getMainSectionTop() const {\n\treturn _callTopBarHeight + _exportTopBarHeight + _playerHeight;\n}\n\nint MainWidget::getThirdSectionTop() const {\n\treturn 0;\n}\n\nvoid MainWidget::hideAll() {\n\tif (_dialogs) {\n\t\t_dialogs->hide();\n\t}\n\t_history->hide();\n\tif (_mainSection) {\n\t\t_mainSection->hide();\n\t}\n\tif (_thirdSection) {\n\t\t_thirdSection->hide();\n\t}\n\tif (_sideShadow) {\n\t\t_sideShadow->hide();\n\t}\n\tif (_thirdShadow) {\n\t\t_thirdShadow->hide();\n\t}\n\tif (_player) {\n\t\t_player->setVisible(false);\n\t\t_playerHeight = 0;\n\t}\n\tif (_callTopBar) {\n\t\t_callTopBar->setVisible(false);\n\t\t_callTopBarHeight = 0;\n\t}\n}\n\nvoid MainWidget::showAll() {\n\tif (cPasswordRecovered()) {\n\t\tcSetPasswordRecovered(false);\n\t\t_controller->show(Ui::MakeInformBox(\n\t\t\ttr::lng_cloud_password_updated()));\n\t}\n\tif (isOneColumn()) {\n\t\tif (_sideShadow) {\n\t\t\t_sideShadow->hide();\n\t\t}\n\t\tif (_hider) {\n\t\t\t_hider->hide();\n\t\t}\n\t\tif (_mainSection) {\n\t\t\t_mainSection->show();\n\t\t} else if (_history->peer()) {\n\t\t\t_history->show();\n\t\t\t_history->updateControlsGeometry();\n\t\t} else {\n\t\t\tAssert(_dialogs != nullptr);\n\t\t\t_dialogs->showFast();\n\t\t\t_history->hide();\n\t\t}\n\t\tif (_dialogs && isMainSectionShown()) {\n\t\t\t_dialogs->hide();\n\t\t}\n\t} else {\n\t\tif (_sideShadow) {\n\t\t\t_sideShadow->show();\n\t\t}\n\t\tif (_hider) {\n\t\t\t_hider->show();\n\t\t}\n\t\tif (_dialogs) {\n\t\t\t_dialogs->showFast();\n\t\t}\n\t\tif (_mainSection) {\n\t\t\t_mainSection->show();\n\t\t} else {\n\t\t\t_history->show();\n\t\t\t_history->updateControlsGeometry();\n\t\t}\n\t\tif (_thirdSection) {\n\t\t\t_thirdSection->show();\n\t\t}\n\t\tif (_thirdShadow) {\n\t\t\t_thirdShadow->show();\n\t\t}\n\t}\n\tif (_player) {\n\t\t_player->setVisible(true);\n\t\t_playerHeight = _player->contentHeight();\n\t}\n\tif (_callTopBar) {\n\t\t_callTopBar->setVisible(true);\n\n\t\t// show() could've send pending resize event that would update\n\t\t// the height value and destroy the top bar if it was hiding.\n\t\tif (_callTopBar) {\n\t\t\t_callTopBarHeight = _callTopBar->height();\n\t\t}\n\t}\n\tupdateControlsGeometry();\n\tfloatPlayerCheckVisibility();\n\n\t_controller->widget()->checkActivation();\n}\n\nvoid MainWidget::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid MainWidget::updateMainSectionShown() {\n\t_controller->setMainSectionShown(_mainSection || _history->peer());\n}\n\nvoid MainWidget::updateControlsGeometry() {\n\tif (!width()) {\n\t\treturn;\n\t}\n\tupdateWindowAdaptiveLayout();\n\tif (_dialogs) {\n\t\tconst auto nochat = !_controller->mainSectionShown();\n\t\tif (Core::App().settings().dialogsWidthRatio(nochat) > 0) {\n\t\t\t_a_dialogsWidth.stop();\n\t\t}\n\t\tif (!_a_dialogsWidth.animating()) {\n\t\t\t_dialogs->stopWidthAnimation();\n\t\t}\n\t}\n\tif (isThreeColumn()) {\n\t\tif (!_thirdSection\n\t\t\t&& !_controller->takeThirdSectionFromLayer()) {\n\t\t\tauto params = Window::SectionShow(\n\t\t\t\tWindow::SectionShow::Way::ClearStack,\n\t\t\t\tanim::type::instant,\n\t\t\t\tanim::activation::background);\n\t\t\tconst auto active = _controller->activeChatCurrent();\n\t\t\tif (const auto thread = active.thread()) {\n\t\t\t\tif (Core::App().settings().tabbedSelectorSectionEnabled()) {\n\t\t\t\t\tif (_mainSection) {\n\t\t\t\t\t\t_mainSection->pushTabbedSelectorToThirdSection(\n\t\t\t\t\t\t\tthread,\n\t\t\t\t\t\t\tparams);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t_history->pushTabbedSelectorToThirdSection(\n\t\t\t\t\t\t\tthread,\n\t\t\t\t\t\t\tparams);\n\t\t\t\t\t}\n\t\t\t\t} else if (Core::App().settings().thirdSectionInfoEnabled()) {\n\t\t\t\t\t_controller->showSection(\n\t\t\t\t\t\t(thread->asTopic()\n\t\t\t\t\t\t\t? std::make_shared<Info::Memento>(\n\t\t\t\t\t\t\t\tthread->asTopic())\n\t\t\t\t\t\t\t: thread->asSublist()\n\t\t\t\t\t\t\t? std::make_shared<Info::Memento>(\n\t\t\t\t\t\t\t\tthread->asSublist())\n\t\t\t\t\t\t\t: Info::Memento::Default(\n\t\t\t\t\t\t\t\tthread->asHistory()->peer)),\n\t\t\t\t\t\tparams.withThirdColumn());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tdestroyThirdSection();\n\t\t_thirdShadow.destroy();\n\t}\n\tconst auto mainSectionTop = getMainSectionTop();\n\tauto dialogsWidth = _dialogs\n\t\t? qRound(_a_dialogsWidth.value(_dialogsWidth))\n\t\t: isOneColumn()\n\t\t? width()\n\t\t: 0;\n\tif (isOneColumn()) {\n\t\tif (_callTopBar) {\n\t\t\t_callTopBar->resizeToWidth(dialogsWidth);\n\t\t\t_callTopBar->moveToLeft(0, 0);\n\t\t}\n\t\tif (_exportTopBar) {\n\t\t\t_exportTopBar->resizeToWidth(dialogsWidth);\n\t\t\t_exportTopBar->moveToLeft(0, _callTopBarHeight);\n\t\t}\n\t\tif (_player) {\n\t\t\t_player->resizeToWidth(dialogsWidth);\n\t\t\t_player->moveToLeft(0, _callTopBarHeight + _exportTopBarHeight);\n\t\t}\n\t\tconst auto mainSectionGeometry = QRect(\n\t\t\t0,\n\t\t\tmainSectionTop,\n\t\t\tdialogsWidth,\n\t\t\theight() - mainSectionTop);\n\t\tif (_dialogs) {\n\t\t\t_dialogs->setGeometryWithTopMoved(\n\t\t\t\tmainSectionGeometry,\n\t\t\t\t_contentScrollAddToY);\n\t\t}\n\t\t_history->setGeometryWithTopMoved(\n\t\t\tmainSectionGeometry,\n\t\t\t_contentScrollAddToY);\n\t\tif (_hider) _hider->setGeometry(0, 0, dialogsWidth, height());\n\t} else {\n\t\tauto thirdSectionWidth = _thirdSection ? _thirdColumnWidth : 0;\n\t\tif (_thirdSection) {\n\t\t\tauto thirdSectionTop = getThirdSectionTop();\n\t\t\t_thirdSection->setGeometry(\n\t\t\t\twidth() - thirdSectionWidth,\n\t\t\t\tthirdSectionTop,\n\t\t\t\tthirdSectionWidth,\n\t\t\t\theight() - thirdSectionTop);\n\t\t}\n\t\tconst auto shadowTop = _controller->window().verticalShadowTop();\n\t\tconst auto shadowHeight = height() - shadowTop;\n\t\tif (_dialogs) {\n\t\t\taccumulate_min(\n\t\t\t\tdialogsWidth,\n\t\t\t\twidth() - st::columnMinimalWidthMain);\n\t\t\t_dialogs->setGeometryToLeft(0, 0, dialogsWidth, height());\n\t\t}\n\t\tif (_sideShadow) {\n\t\t\t_sideShadow->setGeometryToLeft(\n\t\t\t\tdialogsWidth,\n\t\t\t\tshadowTop,\n\t\t\t\tst::lineWidth,\n\t\t\t\tshadowHeight);\n\t\t}\n\t\tif (_thirdShadow) {\n\t\t\t_thirdShadow->setGeometryToLeft(\n\t\t\t\twidth() - thirdSectionWidth - st::lineWidth,\n\t\t\t\tshadowTop,\n\t\t\t\tst::lineWidth,\n\t\t\t\tshadowHeight);\n\t\t}\n\t\tconst auto mainSectionWidth = width()\n\t\t\t- dialogsWidth\n\t\t\t- thirdSectionWidth;\n\t\tif (_callTopBar) {\n\t\t\t_callTopBar->resizeToWidth(mainSectionWidth);\n\t\t\t_callTopBar->moveToLeft(dialogsWidth, 0);\n\t\t}\n\t\tif (_exportTopBar) {\n\t\t\t_exportTopBar->resizeToWidth(mainSectionWidth);\n\t\t\t_exportTopBar->moveToLeft(dialogsWidth, _callTopBarHeight);\n\t\t}\n\t\tif (_player) {\n\t\t\t_player->resizeToWidth(mainSectionWidth);\n\t\t\t_player->moveToLeft(\n\t\t\t\tdialogsWidth,\n\t\t\t\t_callTopBarHeight + _exportTopBarHeight);\n\t\t}\n\t\t_history->setGeometryWithTopMoved(QRect(\n\t\t\tdialogsWidth,\n\t\t\tmainSectionTop,\n\t\t\tmainSectionWidth,\n\t\t\theight() - mainSectionTop\n\t\t), _contentScrollAddToY);\n\t\tif (_hider) {\n\t\t\t_hider->setGeometryToLeft(\n\t\t\t\tdialogsWidth,\n\t\t\t\t0,\n\t\t\t\tmainSectionWidth,\n\t\t\t\theight());\n\t\t}\n\t}\n\tif (_mainSection) {\n\t\tconst auto mainSectionGeometry = QRect(\n\t\t\t_history->x(),\n\t\t\tmainSectionTop,\n\t\t\t_history->width(),\n\t\t\theight() - mainSectionTop);\n\t\t_mainSection->setGeometryWithTopMoved(\n\t\t\tmainSectionGeometry,\n\t\t\t_contentScrollAddToY);\n\t}\n\trefreshResizeAreas();\n\tif (_player) {\n\t\t_player->entity()->updateDropdownsGeometry();\n\t}\n\tupdateMediaPlaylistPosition(_playerPlaylist->x());\n\t_contentScrollAddToY = 0;\n\n\tfloatPlayerUpdatePositions();\n}\n\nvoid MainWidget::destroyThirdSection() {\n\tif (const auto strong = _thirdSection.data()) {\n\t\tif (Ui::InFocusChain(strong)) {\n\t\t\tsetFocus();\n\t\t}\n\t}\n\t_thirdSection.destroy();\n}\n\nvoid MainWidget::refreshResizeAreas() {\n\tif (!isOneColumn() && _dialogs) {\n\t\tensureFirstColumnResizeAreaCreated();\n\t\t_firstColumnResizeArea->setGeometryToLeft(\n\t\t\t_history->x(),\n\t\t\t0,\n\t\t\tst::historyResizeWidth,\n\t\t\theight());\n\t} else if (_firstColumnResizeArea) {\n\t\t_firstColumnResizeArea.destroy();\n\t}\n\n\tif (isThreeColumn() && _thirdSection) {\n\t\tensureThirdColumnResizeAreaCreated();\n\t\t_thirdColumnResizeArea->setGeometryToLeft(\n\t\t\t_thirdSection->x(),\n\t\t\t0,\n\t\t\tst::historyResizeWidth,\n\t\t\theight());\n\t} else if (_thirdColumnResizeArea) {\n\t\t_thirdColumnResizeArea.destroy();\n\t}\n}\n\ntemplate <typename MoveCallback, typename FinishCallback>\nvoid MainWidget::createResizeArea(\n\t\tobject_ptr<Ui::ResizeArea> &area,\n\t\tMoveCallback &&moveCallback,\n\t\tFinishCallback &&finishCallback) {\n\tarea.create(this);\n\tarea->show();\n\tarea->addMoveLeftCallback(\n\t\tstd::forward<MoveCallback>(moveCallback));\n\tarea->addMoveFinishedCallback(\n\t\tstd::forward<FinishCallback>(finishCallback));\n\torderWidgets();\n}\n\nvoid MainWidget::ensureFirstColumnResizeAreaCreated() {\n\tExpects(_dialogs != nullptr);\n\n\tif (_firstColumnResizeArea) {\n\t\treturn;\n\t}\n\tauto moveLeftCallback = [=](int globalLeft) {\n\t\tconst auto newWidth = globalLeft - mapToGlobal(QPoint(0, 0)).x();\n\t\tconst auto newRatio = (newWidth < st::columnMinimalWidthLeft / 2)\n\t\t\t? 0.\n\t\t\t: float64(newWidth) / width();\n\t\tconst auto nochat = !_controller->mainSectionShown();\n\t\tCore::App().settings().updateDialogsWidthRatio(newRatio, nochat);\n\t};\n\tauto moveFinishedCallback = [=] {\n\t\tif (isOneColumn()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto nochat = !_controller->mainSectionShown();\n\t\tif (Core::App().settings().dialogsWidthRatio(nochat) > 0) {\n\t\t\tCore::App().settings().updateDialogsWidthRatio(\n\t\t\t\tfloat64(_dialogsWidth) / width(),\n\t\t\t\tnochat);\n\t\t}\n\t\tCore::App().saveSettingsDelayed();\n\t};\n\tcreateResizeArea(\n\t\t_firstColumnResizeArea,\n\t\tstd::move(moveLeftCallback),\n\t\tstd::move(moveFinishedCallback));\n}\n\nvoid MainWidget::ensureThirdColumnResizeAreaCreated() {\n\tif (_thirdColumnResizeArea) {\n\t\treturn;\n\t}\n\tauto moveLeftCallback = [=](int globalLeft) {\n\t\tauto newWidth = mapToGlobal(QPoint(width(), 0)).x() - globalLeft;\n\t\tCore::App().settings().setThirdColumnWidth(newWidth);\n\t};\n\tauto moveFinishedCallback = [=] {\n\t\tif (!isThreeColumn() || !_thirdSection) {\n\t\t\treturn;\n\t\t}\n\t\tCore::App().settings().setThirdColumnWidth(std::clamp(\n\t\t\tCore::App().settings().thirdColumnWidth(),\n\t\t\tst::columnMinimalWidthThird,\n\t\t\tst::columnMaximalWidthThird));\n\t\tCore::App().saveSettingsDelayed();\n\t};\n\tcreateResizeArea(\n\t\t_thirdColumnResizeArea,\n\t\tstd::move(moveLeftCallback),\n\t\tstd::move(moveFinishedCallback));\n}\n\nvoid MainWidget::updateDialogsWidthAnimated() {\n\tconst auto nochat = !_controller->mainSectionShown();\n\tif (!_dialogs || Core::App().settings().dialogsWidthRatio(nochat) > 0) {\n\t\treturn;\n\t}\n\tauto dialogsWidth = _dialogsWidth;\n\tupdateWindowAdaptiveLayout();\n\tif (Core::App().settings().dialogsWidthRatio(nochat) == 0.\n\t\t&& (_dialogsWidth != dialogsWidth\n\t\t\t|| _a_dialogsWidth.animating())) {\n\t\t_dialogs->startWidthAnimation();\n\t\t_a_dialogsWidth.start(\n\t\t\t[this] { updateControlsGeometry(); },\n\t\t\tdialogsWidth,\n\t\t\t_dialogsWidth,\n\t\t\tst::dialogsWidthDuration,\n\t\t\tanim::easeOutCirc);\n\t\tupdateControlsGeometry();\n\t}\n}\n\nbool MainWidget::saveThirdSectionToStackBack() const {\n\treturn !_stack.empty()\n\t\t&& _thirdSection != nullptr\n\t\t&& _stack.back()->thirdSectionWeak() == _thirdSection.data();\n}\n\nauto MainWidget::thirdSectionForCurrentMainSection(\n\tDialogs::Key key)\n-> std::shared_ptr<Window::SectionMemento> {\n\tif (_thirdSectionFromStack) {\n\t\treturn std::move(_thirdSectionFromStack);\n\t} else if (const auto topic = key.topic()) {\n\t\treturn std::make_shared<Info::Memento>(topic);\n\t} else if (const auto sublist = key.sublist()\n\t\t; sublist && sublist->parentChat()) {\n\t\treturn std::make_shared<Info::Memento>(sublist);\n\t} else if (const auto peer = key.peer()) {\n\t\treturn std::make_shared<Info::Memento>(\n\t\t\tpeer,\n\t\t\tInfo::Memento::DefaultSection(peer));\n\t} else if (const auto sublist = key.sublist()) {\n\t\treturn std::make_shared<Info::Memento>(\n\t\t\tsublist->owningHistory()->peer,\n\t\t\tInfo::Memento::DefaultSection(sublist->owningHistory()->peer));\n\t}\n\tUnexpected(\"Key in MainWidget::thirdSectionForCurrentMainSection().\");\n}\n\nvoid MainWidget::updateThirdColumnToCurrentChat(\n\t\tDialogs::Key key,\n\t\tbool canWrite) {\n\tauto saveOldThirdSection = [&] {\n\t\tif (saveThirdSectionToStackBack()) {\n\t\t\t_stack.back()->setThirdSectionMemento(\n\t\t\t\t_thirdSection->createMemento());\n\t\t\tdestroyThirdSection();\n\t\t}\n\t};\n\tauto &settings = Core::App().settings();\n\tauto params = Window::SectionShow(\n\t\tWindow::SectionShow::Way::ClearStack,\n\t\tanim::type::instant,\n\t\tanim::activation::background);\n\tauto switchInfoFast = [&] {\n\t\tsaveOldThirdSection();\n\n\t\t//\n\t\t// Like in _controller->showPeerInfo()\n\t\t//\n\t\tif (isThreeColumn()\n\t\t\t&& !settings.thirdSectionInfoEnabled()) {\n\t\t\tsettings.setThirdSectionInfoEnabled(true);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}\n\n\t\t_controller->showSection(\n\t\t\tthirdSectionForCurrentMainSection(key),\n\t\t\tparams.withThirdColumn());\n\t};\n\tauto switchTabbedFast = [&](not_null<Data::Thread*> thread) {\n\t\tsaveOldThirdSection();\n\t\treturn _mainSection\n\t\t\t? _mainSection->pushTabbedSelectorToThirdSection(thread, params)\n\t\t\t: _history->pushTabbedSelectorToThirdSection(thread, params);\n\t};\n\tif (isThreeColumn()\n\t\t&& settings.tabbedSelectorSectionEnabled()\n\t\t&& key) {\n\t\tif (!canWrite) {\n\t\t\tswitchInfoFast();\n\t\t\tsettings.setTabbedSelectorSectionEnabled(true);\n\t\t\tsettings.setTabbedReplacedWithInfo(true);\n\t\t} else if (settings.tabbedReplacedWithInfo()\n\t\t\t&& key.thread()\n\t\t\t&& switchTabbedFast(key.thread())) {\n\t\t\tsettings.setTabbedReplacedWithInfo(false);\n\t\t}\n\t} else {\n\t\tsettings.setTabbedReplacedWithInfo(false);\n\t\tif (!key) {\n\t\t\tif (_thirdSection) {\n\t\t\t\tdestroyThirdSection();\n\t\t\t\t_thirdShadow.destroy();\n\t\t\t\tupdateControlsGeometry();\n\t\t\t}\n\t\t} else if (isThreeColumn()\n\t\t\t&& settings.thirdSectionInfoEnabled()) {\n\t\t\tswitchInfoFast();\n\t\t}\n\t}\n}\n\nvoid MainWidget::updateMediaPlaylistPosition(int x) {\n\tif (_player) {\n\t\tauto playlistLeft = x;\n\t\tauto playlistWidth = _playerPlaylist->width();\n\t\tauto playlistTop = _player->y() + _player->height();\n\t\tauto rightEdge = width();\n\t\tif (playlistLeft + playlistWidth > rightEdge) {\n\t\t\tplaylistLeft = rightEdge - playlistWidth;\n\t\t} else if (playlistLeft < 0) {\n\t\t\tplaylistLeft = 0;\n\t\t}\n\t\t_playerPlaylist->move(playlistLeft, playlistTop);\n\t}\n}\n\nvoid MainWidget::returnTabbedSelector() {\n\tif (!_mainSection || !_mainSection->returnTabbedSelector()) {\n\t\t_history->returnTabbedSelector();\n\t}\n}\n\nbool MainWidget::relevantForDialogsFocus(not_null<QWidget*> widget) const {\n\tif (!_dialogs || widget->window() != window()) {\n\t\treturn false;\n\t}\n\twhile (true) {\n\t\tif (widget.get() == this) {\n\t\t\treturn true;\n\t\t}\n\t\tconst auto parent = widget->parentWidget();\n\t\tif (!parent) {\n\t\t\treturn false;\n\t\t}\n\t\twidget = parent;\n\t}\n\tUnexpected(\"Should never be here.\");\n}\n\nbool MainWidget::eventFilter(QObject *o, QEvent *e) {\n\tconst auto widget = o->isWidgetType()\n\t\t? static_cast<QWidget*>(o)\n\t\t: nullptr;\n\tif (e->type() == QEvent::FocusIn) {\n\t\tif (widget && relevantForDialogsFocus(widget)) {\n\t\t\t_dialogs->updateHasFocus(widget);\n\t\t} else if (widget == window()) {\n\t\t\tcrl::on_main(this, [=] {\n\t\t\t\t_controller->widget()->setInnerFocus();\n\t\t\t});\n\t\t}\n\t} else if (e->type() == QEvent::MouseButtonPress) {\n\t\tif (widget && (widget->window() == window())) {\n\t\t\tconst auto event = static_cast<QMouseEvent*>(e);\n\t\t\tif (event->button() == Qt::BackButton) {\n\t\t\t\tif (!Core::App().hideMediaView()\n\t\t\t\t\t&& (!_dialogs || !_dialogs->cancelSearchByMouseBack())) {\n\t\t\t\t\thandleHistoryBack();\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t} else if (e->type() == QEvent::Wheel) {\n\t\tif (widget && (widget->window() == window())) {\n\t\t\tif (const auto result = floatPlayerFilterWheelEvent(o, e)) {\n\t\t\t\treturn *result;\n\t\t\t}\n\t\t}\n\t}\n\treturn RpWidget::eventFilter(o, e);\n}\n\nvoid MainWidget::handleAdaptiveLayoutUpdate() {\n\tshowAll();\n\tif (_sideShadow) {\n\t\t_sideShadow->setVisible(!isOneColumn());\n\t}\n\tif (_player) {\n\t\t_player->updateAdaptiveLayout();\n\t}\n}\n\nvoid MainWidget::handleHistoryBack() {\n\tconst auto openedFolder = _controller->openedFolder().current();\n\tconst auto openedForum = _controller->shownForum().current();\n\tconst auto rootPeer = !_stack.empty()\n\t\t? _stack.front()->peer()\n\t\t: _history->peer()\n\t\t? _history->peer()\n\t\t: _mainSection\n\t\t? _mainSection->activeChat().key.peer()\n\t\t: nullptr;\n\tconst auto rootHistory = rootPeer\n\t\t? rootPeer->owner().historyLoaded(rootPeer)\n\t\t: nullptr;\n\tconst auto rootFolder = rootHistory ? rootHistory->folder() : nullptr;\n\tif (openedForum\n\t\t&& _stack.empty()\n\t\t&& (!rootPeer || rootPeer->forum() != openedForum)) {\n\t\t_controller->closeForum();\n\t} else if (!openedFolder\n\t\t|| (rootFolder == openedFolder)\n\t\t|| (!_dialogs || _dialogs->isHidden())) {\n\t\t_controller->showBackFromStack();\n\t\tif (_dialogs) {\n\t\t\t_dialogs->setInnerFocus();\n\t\t}\n\t} else {\n\t\t_controller->closeFolder();\n\t}\n}\n\nvoid MainWidget::updateWindowAdaptiveLayout() {\n\tconst auto nochat = !_controller->mainSectionShown();\n\n\tauto layout = _controller->computeColumnLayout();\n\tauto dialogsWidthRatio = Core::App().settings().dialogsWidthRatio(nochat);\n\n\t// Check if we are in a single-column layout in a wide enough window\n\t// for the normal layout. If so, switch to the normal layout.\n\tif (layout.windowLayout == Window::Adaptive::WindowLayout::OneColumn) {\n\t\tauto chatWidth = layout.chatWidth;\n\t\t//if (session().settings().tabbedSelectorSectionEnabled()\n\t\t//\t&& chatWidth >= _history->minimalWidthForTabbedSelectorSection()) {\n\t\t//\tchatWidth -= _history->tabbedSelectorSectionWidth();\n\t\t//}\n\t\tauto minimalNormalWidth = st::columnMinimalWidthLeft\n\t\t\t+ st::columnMinimalWidthMain;\n\t\tif (chatWidth >= minimalNormalWidth) {\n\t\t\t// Switch layout back to normal in a wide enough window.\n\t\t\tlayout.windowLayout = Window::Adaptive::WindowLayout::Normal;\n\t\t\tlayout.dialogsWidth = st::columnMinimalWidthLeft;\n\t\t\tlayout.chatWidth = layout.bodyWidth - layout.dialogsWidth;\n\t\t\tdialogsWidthRatio = float64(layout.dialogsWidth) / layout.bodyWidth;\n\t\t}\n\t}\n\n\t// Check if we are going to create the third column and shrink the\n\t// dialogs widget to provide a wide enough chat history column.\n\t// Don't shrink the column on the first call, when window is inited.\n\tif (layout.windowLayout == Window::Adaptive::WindowLayout::ThreeColumn\n\t\t&& _controller->widget()->positionInited()) {\n\t\t//auto chatWidth = layout.chatWidth;\n\t\t//if (_history->willSwitchToTabbedSelectorWithWidth(chatWidth)) {\n\t\t//\tauto thirdColumnWidth = _history->tabbedSelectorSectionWidth();\n\t\t//\tauto twoColumnsWidth = (layout.bodyWidth - thirdColumnWidth);\n\t\t//\tauto sameRatioChatWidth = twoColumnsWidth - qRound(dialogsWidthRatio * twoColumnsWidth);\n\t\t//\tauto desiredChatWidth = qMax(sameRatioChatWidth, HistoryView::WideChatWidth());\n\t\t//\tchatWidth -= thirdColumnWidth;\n\t\t//\tauto extendChatBy = desiredChatWidth - chatWidth;\n\t\t//\taccumulate_min(extendChatBy, layout.dialogsWidth - st::columnMinimalWidthLeft);\n\t\t//\tif (extendChatBy > 0) {\n\t\t//\t\tlayout.dialogsWidth -= extendChatBy;\n\t\t//\t\tlayout.chatWidth += extendChatBy;\n\t\t//\t\tdialogsWidthRatio = float64(layout.dialogsWidth) / layout.bodyWidth;\n\t\t//\t}\n\t\t//}\n\t}\n\n\tCore::App().settings().updateDialogsWidthRatio(dialogsWidthRatio, nochat);\n\n\tauto useSmallColumnWidth = !isOneColumn()\n\t\t&& !dialogsWidthRatio\n\t\t&& !_controller->chatsForceDisplayWide();\n\t_dialogsWidth = !_dialogs\n\t\t? 0\n\t\t: useSmallColumnWidth\n\t\t? _controller->dialogsSmallColumnWidth()\n\t\t: layout.dialogsWidth;\n\t_thirdColumnWidth = layout.thirdWidth;\n\t_controller->adaptive().setWindowLayout(layout.windowLayout);\n}\n\nint MainWidget::backgroundFromY() const {\n\treturn -getMainSectionTop();\n}\n\nbool MainWidget::contentOverlapped(const QRect &globalRect) {\n\treturn _history->contentOverlapped(globalRect)\n\t\t/*|| _playerPlaylist->overlaps(globalRect)*/;\n}\n\nvoid MainWidget::activate() {\n\tif (_showAnimation) {\n\t\treturn;\n\t}\n\tconst auto urls = base::take(cRefStartUrls());\n\tconst auto interprets = urls | ranges::views::filter([](const QUrl &url) {\n\t\treturn url.scheme() == u\"interpret\"_q;\n\t}) | ranges::views::transform([](const QUrl &url) {\n\t\treturn url.path();\n\t}) | ranges::to<QStringList>;\n\tconst auto paths = urls | ranges::views::filter(\n\t\t&QUrl::isLocalFile\n\t) | ranges::views::transform(\n\t\t&QUrl::toLocalFile\n\t) | ranges::to<QStringList>;\n\tif (!interprets.isEmpty() || !paths.isEmpty()) {\n\t\tif (!interprets.isEmpty()) {\n\t\t\tfor (const auto &interpret : interprets) {\n\t\t\t\tconst auto error = Support::InterpretSendPath(\n\t\t\t\t\t_controller,\n\t\t\t\t\tinterpret);\n\t\t\t\tif (!error.isEmpty()) {\n\t\t\t\t\t_controller->show(Ui::MakeInformBox(error));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!paths.isEmpty()) {\n\t\t\tconst auto chosen = [=](not_null<Data::Thread*> thread) {\n\t\t\t\treturn sendPaths(thread, paths);\n\t\t\t};\n\t\t\tWindow::ShowChooseRecipientBox(_controller, chosen);\n\t\t}\n\t} else if (_mainSection) {\n\t\t_mainSection->setInnerFocus();\n\t} else if (_hider) {\n\t\tAssert(_dialogs != nullptr);\n\t\t_dialogs->setInnerFocus();\n\t} else if (!_controller->isLayerShown()) {\n\t\tif (_history->peer()) {\n\t\t\t_history->activate();\n\t\t} else {\n\t\t\tAssert(_dialogs != nullptr);\n\t\t\t_dialogs->setInnerFocus();\n\t\t}\n\t}\n\t_controller->widget()->fixOrder();\n}\n\nbool MainWidget::animatingShow() const {\n\treturn _showAnimation != nullptr;\n}\n\nbool MainWidget::isOneColumn() const {\n\treturn _controller->adaptive().isOneColumn();\n}\n\nbool MainWidget::isNormalColumn() const {\n\treturn _controller->adaptive().isNormal();\n}\n\nbool MainWidget::isThreeColumn() const {\n\treturn _controller->adaptive().isThreeColumn();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/mainwidget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"media/player/media_player_float.h\"\n#include \"mtproto/sender.h\"\n\nclass HistoryWidget;\nclass StackItem;\nclass Image;\n\nnamespace Bot {\nstruct SendCommandRequest;\n} // namespace Bot\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\nclass Thread;\nclass WallPaper;\nstruct ForwardDraft;\nstruct DrawToReplyRequest;\nclass Forum;\nclass SavedMessages;\nstruct ReportInput;\n} // namespace Data\n\nnamespace Dialogs {\nstruct RowDescriptor;\nclass Row;\nclass Key;\nclass Widget;\n} // namespace Dialogs\n\nnamespace Media {\nnamespace Player {\nclass Widget;\nclass Panel;\nstruct TrackState;\n} // namespace Player\n} // namespace Media\n\nnamespace Export {\nnamespace View {\nclass TopBar;\nclass PanelController;\nstruct Content;\n} // namespace View\n} // namespace Export\n\nnamespace Ui {\nclass ChatTheme;\nclass ResizeArea;\nclass PlainShadow;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\ntemplate <typename Inner>\nclass TopBarWrapWidget;\nclass SectionMemento;\nclass SectionWidget;\nclass SlideAnimation;\nclass ConnectionState;\nstruct SectionSlideParams;\nstruct SectionShow;\nenum class Column;\nclass HistoryHider;\nstruct SeparateId;\n} // namespace Window\n\nnamespace Calls {\nclass Call;\nclass GroupCall;\nclass TopBar;\n} // namespace Calls\n\nnamespace Core {\nclass Changelogs;\n} // namespace Core\n\nextern const char kForceComposeSearchOneColumn[];\n\nclass MainWidget final\n\t: public Ui::RpWidget\n\t, private Media::Player::FloatDelegate {\npublic:\n\tusing SectionShow = Window::SectionShow;\n\n\tMainWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~MainWidget();\n\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] not_null<Window::SessionController*> controller() const;\n\t[[nodiscard]] Window::SeparateId windowId() const;\n\t[[nodiscard]] bool isPrimary() const;\n\t[[nodiscard]] bool isMainSectionShown() const;\n\t[[nodiscard]] bool isThirdSectionShown() const;\n\n\t[[nodiscard]] Dialogs::RowDescriptor resolveChatNext(\n\t\tDialogs::RowDescriptor from) const;\n\t[[nodiscard]] Dialogs::RowDescriptor resolveChatPrevious(\n\t\tDialogs::RowDescriptor from) const;\n\n\tvoid returnTabbedSelector();\n\n\tvoid showAnimated(QPixmap oldContentCache, bool back = false);\n\n\tvoid activate();\n\n\tvoid windowShown();\n\n\tvoid dialogsToUp();\n\tvoid checkActivation();\n\n\t[[nodiscard]] PeerData *peer() const;\n\t[[nodiscard]] Ui::ChatTheme *customChatTheme() const;\n\n\tint backgroundFromY() const;\n\tvoid showSection(\n\t\tstd::shared_ptr<Window::SectionMemento> memento,\n\t\tconst SectionShow &params);\n\tvoid updateColumnLayout();\n\tbool stackIsEmpty() const;\n\tbool showBackFromStack(const SectionShow &params);\n\tvoid orderWidgets();\n\tQPixmap grabForShowAnimation(const Window::SectionSlideParams &params);\n\tvoid checkMainSectionToLayer();\n\n\t[[nodiscard]] SendMenu::Details sendMenuDetails() const;\n\n\t[[nodiscard]] bool animatingShow() const;\n\n\tvoid showDragForwardInfo();\n\tvoid hideDragForwardInfo();\n\n\tbool setForwardDraft(\n\t\tnot_null<Data::Thread*> thread,\n\t\tData::ForwardDraft &&draft);\n\tbool sendPaths(\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst QStringList &paths);\n\tbool shareUrl(\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst QString &url,\n\t\tconst QString &text) const;\n\tbool filesOrForwardDrop(\n\t\tnot_null<Data::Thread*> thread,\n\t\tnot_null<const QMimeData*> data,\n\t\tbool forumResolved = false);\n\n\tvoid sendBotCommand(Bot::SendCommandRequest request);\n\tvoid hideSingleUseKeyboard(FullMsgId replyToId);\n\n\tvoid searchMessages(\n\t\tconst QString &query,\n\t\tDialogs::Key inChat,\n\t\tPeerData *searchFrom = nullptr);\n\n\tvoid setChatBackground(\n\t\tconst Data::WallPaper &background,\n\t\tQImage &&image = QImage());\n\tbool chatBackgroundLoading();\n\tfloat64 chatBackgroundProgress() const;\n\tvoid checkChatBackground();\n\tImage *newBackgroundThumb();\n\n\tvoid setInnerFocus();\n\n\tbool contentOverlapped(const QRect &globalRect);\n\n\tvoid showChooseReportMessages(\n\t\tnot_null<PeerData*> peer,\n\t\tData::ReportInput reportInput,\n\t\tFn<void(std::vector<MsgId>)> done);\n\tvoid clearChooseReportMessages();\n\n\tvoid toggleChooseChatTheme(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::optional<bool> show);\n\n\tvoid showHistory(\n\t\tPeerId peer,\n\t\tconst SectionShow &params,\n\t\tMsgId msgId);\n\tbool handleDrawToReplyRequest(Data::DrawToReplyRequest request);\n\tvoid showMessage(\n\t\tnot_null<const HistoryItem*> item,\n\t\tconst SectionShow &params);\n\tvoid showForum(not_null<Data::Forum*> forum, const SectionShow &params);\n\n\tbool notify_switchInlineBotButtonReceived(const QString &query, UserData *samePeerBot, MsgId samePeerReplyTo);\n\n\tusing FloatDelegate::floatPlayerAreaUpdated;\n\n\tvoid stopAndClosePlayer();\n\n\tbool preventsCloseSection(Fn<void()> callback) const;\n\tbool preventsCloseSection(\n\t\tFn<void()> callback,\n\t\tconst SectionShow &params) const;\n\n\tvoid showNonPremiumLimitToast(bool download);\n\n\tvoid dialogsCancelled();\n\tvoid toggleFiltersMenu(bool value) const;\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tbool eventFilter(QObject *o, QEvent *e) override;\n\n\t[[nodiscard]] bool relevantForDialogsFocus(\n\t\tnot_null<QWidget*> widget) const;\n\n\tvoid showFinished();\n\tvoid handleAdaptiveLayoutUpdate();\n\tvoid updateWindowAdaptiveLayout();\n\tvoid handleAudioUpdate(const Media::Player::TrackState &state);\n\tvoid updateMediaPlaylistPosition(int x);\n\tvoid updateControlsGeometry();\n\tvoid updateMainSectionShown();\n\tvoid updateDialogsWidthAnimated();\n\tvoid updateThirdColumnToCurrentChat(\n\t\tDialogs::Key key,\n\t\tbool canWrite);\n\t[[nodiscard]] bool saveThirdSectionToStackBack() const;\n\t[[nodiscard]] auto thirdSectionForCurrentMainSection(Dialogs::Key key)\n\t\t-> std::shared_ptr<Window::SectionMemento>;\n\n\tvoid setupConnectingWidget();\n\tvoid createPlayer();\n\tvoid playerHeightUpdated();\n\n\tvoid setCurrentCall(Calls::Call *call);\n\tvoid setCurrentGroupCall(Calls::GroupCall *call);\n\tvoid createCallTopBar(Calls::Call *call, Calls::GroupCall *group);\n\tvoid destroyCallTopBar();\n\tvoid callTopBarHeightUpdated(int callTopBarHeight);\n\n\tvoid setCurrentExportView(Export::View::PanelController *view);\n\tvoid createExportTopBar(Export::View::Content &&data);\n\tvoid destroyExportTopBar();\n\tvoid exportTopBarHeightUpdated();\n\n\tWindow::SectionSlideParams prepareShowAnimation(\n\t\tbool willHaveTopBarShadow);\n\tvoid showNewSection(\n\t\tstd::shared_ptr<Window::SectionMemento> memento,\n\t\tconst SectionShow &params);\n\tvoid destroyThirdSection();\n\n\tWindow::SectionSlideParams prepareThirdSectionAnimation(Window::SectionWidget *section);\n\n\t// All this methods use the prepareShowAnimation().\n\tWindow::SectionSlideParams prepareMainSectionAnimation(Window::SectionWidget *section);\n\tWindow::SectionSlideParams prepareHistoryAnimation(PeerId historyPeerId);\n\tWindow::SectionSlideParams prepareDialogsAnimation();\n\n\tbool saveSectionInStack(\n\t\tconst SectionShow &params,\n\t\tWindow::SectionWidget *newMainSection = nullptr);\n\n\tint getMainSectionTop() const;\n\tint getThirdSectionTop() const;\n\n\tvoid hideAll();\n\tvoid showAll();\n\tvoid hiderLayer(base::unique_qptr<Window::HistoryHider> h);\n\tvoid clearHider(not_null<Window::HistoryHider*> instance);\n\n\tvoid closeBothPlayers();\n\n\t[[nodiscard]] auto floatPlayerDelegate()\n\t\t-> not_null<Media::Player::FloatDelegate*>;\n\tnot_null<Ui::RpWidget*> floatPlayerWidget() override;\n\tvoid floatPlayerToggleGifsPaused(bool paused) override;\n\tnot_null<Media::Player::FloatSectionDelegate*> floatPlayerGetSection(\n\t\tWindow::Column column) override;\n\tvoid floatPlayerEnumerateSections(Fn<void(\n\t\tnot_null<Media::Player::FloatSectionDelegate*> widget,\n\t\tWindow::Column widgetColumn)> callback) override;\n\tbool floatPlayerIsVisible(not_null<HistoryItem*> item) override;\n\tvoid floatPlayerClosed(FullMsgId itemId);\n\tvoid floatPlayerDoubleClickEvent(\n\t\tnot_null<const HistoryItem*> item) override;\n\n\tvoid refreshResizeAreas();\n\ttemplate <typename MoveCallback, typename FinishCallback>\n\tvoid createResizeArea(\n\t\tobject_ptr<Ui::ResizeArea> &area,\n\t\tMoveCallback &&moveCallback,\n\t\tFinishCallback &&finishCallback);\n\tvoid ensureFirstColumnResizeAreaCreated();\n\tvoid ensureThirdColumnResizeAreaCreated();\n\n\tbool isReadyChatBackground(\n\t\tconst Data::WallPaper &background,\n\t\tconst QImage &image) const;\n\tvoid setReadyChatBackground(\n\t\tconst Data::WallPaper &background,\n\t\tQImage &&image);\n\n\tvoid handleHistoryBack();\n\tbool showHistoryInDifferentWindow(\n\t\tPeerId peerId,\n\t\tconst SectionShow &params,\n\t\tMsgId showAtMsgId);\n\n\tbool isOneColumn() const;\n\tbool isNormalColumn() const;\n\tbool isThreeColumn() const;\n\n\tconst not_null<Window::SessionController*> _controller;\n\n\tstd::unique_ptr<Window::SlideAnimation> _showAnimation;\n\n\tint _dialogsWidth = 0;\n\tint _thirdColumnWidth = 0;\n\tUi::Animations::Simple _a_dialogsWidth;\n\n\tconst base::unique_qptr<Dialogs::Widget> _dialogs;\n\tconst base::unique_qptr<HistoryWidget> _history;\n\tobject_ptr<Window::SectionWidget> _mainSection = { nullptr };\n\tobject_ptr<Window::SectionWidget> _thirdSection = { nullptr };\n\tstd::shared_ptr<Window::SectionMemento> _thirdSectionFromStack;\n\tstd::unique_ptr<Window::ConnectionState> _connecting;\n\n\tconst base::unique_qptr<Ui::PlainShadow> _sideShadow;\n\tobject_ptr<Ui::PlainShadow> _thirdShadow = { nullptr };\n\tobject_ptr<Ui::ResizeArea> _firstColumnResizeArea = { nullptr };\n\tobject_ptr<Ui::ResizeArea> _thirdColumnResizeArea = { nullptr };\n\n\tbase::weak_ptr<Calls::Call> _currentCall;\n\tbase::weak_ptr<Calls::GroupCall> _currentGroupCall;\n\trpl::lifetime _currentCallLifetime;\n\tobject_ptr<Ui::SlideWrap<Calls::TopBar>> _callTopBar = { nullptr };\n\n\tExport::View::PanelController *_currentExportView = nullptr;\n\tobject_ptr<Window::TopBarWrapWidget<Export::View::TopBar>> _exportTopBar\n\t\t= { nullptr };\n\trpl::lifetime _exportViewLifetime;\n\n\tobject_ptr<Window::TopBarWrapWidget<Media::Player::Widget>> _player\n\t\t= { nullptr };\n\tobject_ptr<Media::Player::Panel> _playerPlaylist;\n\n\tbase::unique_qptr<Window::HistoryHider> _hider;\n\tstd::vector<std::unique_ptr<StackItem>> _stack;\n\n\tint _playerHeight = 0;\n\tint _callTopBarHeight = 0;\n\tint _exportTopBarHeight = 0;\n\tint _contentScrollAddToY = 0;\n\n\tstruct SettingBackground;\n\tstd::unique_ptr<SettingBackground> _background;\n\n\t// _changelogs depends on _data, subscribes on chats loading event.\n\tconst std::unique_ptr<Core::Changelogs> _changelogs;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/mainwindow.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mainwindow.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document_media.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"history/history.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/painter.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/ui_utility.h\"\n#include \"lang/lang_cloud_manager.h\"\n#include \"lang/lang_instance.h\"\n#include \"core/sandbox.h\"\n#include \"core/application.h\"\n#include \"export/export_manager.h\"\n#include \"inline_bots/bot_attach_web_view.h\" // AttachWebView::cancel.\n#include \"intro/intro_widget.h\"\n#include \"main/main_session.h\"\n#include \"main/main_account.h\" // Account::sessionValue.\n#include \"main/main_domain.h\"\n#include \"mainwidget.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/connection_box.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/localstorage.h\"\n#include \"apiwrap.h\"\n#include \"api/api_updates.h\"\n#include \"settings/settings_intro.h\"\n#include \"base/options.h\"\n#include \"window/notifications_manager.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/themes/window_theme_warning.h\"\n#include \"window/window_main_menu.h\"\n#include \"window/window_controller.h\" // App::wnd.\n#include \"window/window_session_controller.h\"\n#include \"window/window_setup_email.h\"\n#include \"window/window_media_preview.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_window.h\"\n\n#include <QtGui/QWindow>\n\nnamespace {\n\n// Code for testing languages is F7-F6-F7-F8\nvoid FeedLangTestingKey(int key) {\n\tstatic auto codeState = 0;\n\tif ((codeState == 0 && key == Qt::Key_F7)\n\t\t|| (codeState == 1 && key == Qt::Key_F6)\n\t\t|| (codeState == 2 && key == Qt::Key_F7)\n\t\t|| (codeState == 3 && key == Qt::Key_F8)) {\n\t\t++codeState;\n\t} else {\n\t\tcodeState = 0;\n\t}\n\tif (codeState == 4) {\n\t\tcodeState = 0;\n\t\tLang::CurrentCloudManager().switchToTestLanguage();\n\t}\n}\n\nbase::options::toggle AutoScrollInactiveChat({\n\t.id = kOptionAutoScrollInactiveChat,\n\t.name = \"Mark as read of inactive chat\",\n\t.description = \"Mark new messages as read and scroll the chat \"\n\t\t\"even when the window is not in focus.\",\n});\n\nconstexpr auto kFpsCounterMeasureWindow = crl::time(1000);\nconstexpr auto kFpsCounterRefreshInterval = crl::time(250);\n\n[[nodiscard]] bool HasOnlyModifiersWithoutKeypad(\n\t\tQt::KeyboardModifiers modifiers,\n\t\tQt::KeyboardModifiers expected) {\n\treturn (modifiers & ~Qt::KeypadModifier) == expected;\n}\n\n[[nodiscard]] bool IsFpsCounterKey(const QKeyEvent *e) {\n\tif (!e || e->isAutoRepeat() || (e->key() != Qt::Key_F)) {\n\t\treturn false;\n\t}\n#ifdef Q_OS_MAC\n\treturn HasOnlyModifiersWithoutKeypad(\n\t\te->modifiers(),\n\t\tQt::AltModifier | Qt::ShiftModifier | Qt::ControlModifier)\n\t\t|| HasOnlyModifiersWithoutKeypad(\n\t\t\te->modifiers(),\n\t\t\tQt::AltModifier | Qt::ShiftModifier | Qt::MetaModifier);\n#else // Q_OS_MAC\n\treturn HasOnlyModifiersWithoutKeypad(\n\t\te->modifiers(),\n\t\tQt::AltModifier | Qt::ShiftModifier | Qt::ControlModifier);\n#endif // !Q_OS_MAC\n}\n\n[[nodiscard]] bool WidgetInHierarchy(\n\t\tnot_null<const QWidget*> widget,\n\t\tnot_null<const QWidget*> root) {\n\treturn (widget == root) || root->isAncestorOf(widget);\n}\n\n} // namespace\n\nconst char kOptionAutoScrollInactiveChat[]\n\t= \"auto-scroll-inactive-chat\";\n\nMainWindow::MainWindow(not_null<Window::Controller*> controller)\n: Platform::MainWindow(controller) {\n\tresize(st::windowDefaultWidth, st::windowDefaultHeight);\n\n\tsetLocale(QLocale(QLocale::English, QLocale::UnitedStates));\n\n\tusing Window::Theme::BackgroundUpdate;\n\tWindow::Theme::Background()->updates(\n\t) | rpl::on_next([=](const BackgroundUpdate &data) {\n\t\tthemeUpdated(data);\n\t}, lifetime());\n\n\tCore::App().passcodeLockChanges(\n\t) | rpl::on_next([=] {\n\t\tupdateGlobalMenu();\n\t}, lifetime());\n\n\tUi::Emoji::Updated(\n\t) | rpl::on_next([=] {\n\t\tUi::ForceFullRepaint(this);\n\t}, lifetime());\n\n\t_fpsCounterTimer.setCallback([=] {\n\t\trefreshFpsCounter();\n\t});\n\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n}\n\nvoid MainWindow::initHook() {\n\tPlatform::MainWindow::initHook();\n\tQCoreApplication::instance()->installEventFilter(this);\n}\n\nvoid MainWindow::applyInitialWorkMode() {\n\tconst auto workMode = Core::App().settings().workMode();\n\tworkmodeUpdated(workMode);\n\n\tif (controller().isPrimary()) {\n\t\tif (Core::App().settings().windowPosition().maximized) {\n\t\t\tDEBUG_LOG((\"Window Pos: First show, setting maximized.\"));\n\t\t\tsetWindowState(Qt::WindowMaximized);\n\t\t}\n\t\tif (cStartInTray()\n\t\t\t|| (cLaunchMode() == LaunchModeAutoStart\n\t\t\t\t&& cStartMinimized()\n\t\t\t\t&& !Core::App().passcodeLocked())) {\n\t\t\tDEBUG_LOG((\"Window Pos: First show, setting minimized after.\"));\n\t\t\tif (workMode == Core::Settings::WorkMode::TrayOnly\n\t\t\t\t|| workMode == Core::Settings::WorkMode::WindowAndTray) {\n\t\t\t\thide();\n\t\t\t} else {\n\t\t\t\tsetWindowState(windowState() | Qt::WindowMinimized);\n\t\t\t}\n\t\t}\n\t}\n\tsetPositionInited();\n}\n\nvoid MainWindow::finishFirstShow() {\n\tapplyInitialWorkMode();\n\tcreateGlobalMenu();\n\n\twindowActiveValue(\n\t) | rpl::skip(1) | rpl::filter(\n\t\t!rpl::mappers::_1\n\t) | rpl::on_next([=] {\n\t\tUi::Tooltip::Hide();\n\t}, lifetime());\n\n\tsetAttribute(Qt::WA_NoSystemBackground);\n\n\tif (!_passcodeLock && !_setupEmailLock && _main) {\n\t\t_main->activate();\n\t} else if (!_passcodeLock && !_setupEmailLock && _intro) {\n\t\t_intro->setInnerFocus();\n\t}\n}\n\nvoid MainWindow::clearWidgetsHook() {\n\t_mediaPreview.destroy();\n\t_main.destroy();\n\t_intro.destroy();\n\tif (!Core::App().passcodeLocked()) {\n\t\t_passcodeLock.destroy();\n\t}\n\t_setupEmailLock.destroy();\n}\n\nbool MainWindow::countsForFpsCounter(QObject *object) const {\n\tconst auto widget = qobject_cast<QWidget*>(object);\n\tif (!_fpsCounterVisible || !widget || (widget->window() != this)) {\n\t\treturn false;\n\t}\n\tif (_fpsCounter && WidgetInHierarchy(widget, _fpsCounter)) {\n\t\treturn false;\n\t}\n\tconst auto visibleRoot = [&]() -> QWidget* {\n\t\tif (_testingThemeWarning && !_testingThemeWarning->isHidden()) {\n\t\t\treturn _testingThemeWarning;\n\t\t} else if (_mediaPreview && !_mediaPreview->isHidden()) {\n\t\t\treturn _mediaPreview;\n\t\t} else if (_layer && !_layer->isHidden()) {\n\t\t\treturn _layer.get();\n\t\t} else if (_passcodeLock && !_passcodeLock->isHidden()) {\n\t\t\treturn _passcodeLock;\n\t\t} else if (_setupEmailLock && !_setupEmailLock->isHidden()) {\n\t\t\treturn _setupEmailLock;\n\t\t} else if (_main && !_main->isHidden()) {\n\t\t\treturn _main;\n\t\t} else if (_intro && !_intro->isHidden()) {\n\t\t\treturn _intro;\n\t\t}\n\t\treturn nullptr;\n\t}();\n\treturn !visibleRoot || WidgetInHierarchy(widget, visibleRoot);\n}\n\nbool MainWindow::matchesFpsCounterKey(\n\t\tQObject *object,\n\t\tconst QKeyEvent *e) const {\n\tif (!IsFpsCounterKey(e)) {\n\t\treturn false;\n\t}\n\tif (const auto widget = qobject_cast<QWidget*>(object)) {\n\t\treturn widget->window() == this;\n\t} else if (const auto window = qobject_cast<QWindow*>(object)) {\n\t\treturn window == windowHandle();\n\t}\n\treturn false;\n}\n\nvoid MainWindow::ensureFpsCounterCreated() {\n\tif (_fpsCounter) {\n\t\treturn;\n\t}\n\n\t_fpsCounter.create(bodyWidget());\n\t_fpsCounter->hide();\n\t_fpsCounter->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_fpsCounter->paintRequest() | rpl::on_next([=] {\n\t\tif (!_fpsCounter) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto &tooltip = st::defaultTooltip;\n\t\tauto p = Painter(_fpsCounter);\n\t\t{\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(QPen(st::boxTextFgGood, st::lineWidth));\n\t\t\tp.setBrush(st::imageBg);\n\t\t\tp.drawRoundedRect(\n\t\t\t\tQRectF(\n\t\t\t\t\t0.5,\n\t\t\t\t\t0.5,\n\t\t\t\t\t_fpsCounter->width() - 1.,\n\t\t\t\t\t_fpsCounter->height() - 1.),\n\t\t\t\tst::roundRadiusSmall,\n\t\t\t\tst::roundRadiusSmall);\n\t\t}\n\t\tp.setPen(st::dialogsOnlineBadgeFg);\n\t\tp.setFont(tooltip.textStyle.font);\n\t\tp.drawText(\n\t\t\tQRect(\n\t\t\t\tst::lineWidth + tooltip.textPadding.left(),\n\t\t\t\tst::lineWidth + tooltip.textPadding.top(),\n\t\t\t\t_fpsCounter->width()\n\t\t\t\t\t- (2 * st::lineWidth)\n\t\t\t\t\t- tooltip.textPadding.left()\n\t\t\t\t\t- tooltip.textPadding.right(),\n\t\t\t\t_fpsCounter->height()\n\t\t\t\t\t- (2 * st::lineWidth)\n\t\t\t\t\t- tooltip.textPadding.top()\n\t\t\t\t\t- tooltip.textPadding.bottom()),\n\t\t\tQt::AlignCenter,\n\t\t\t_fpsCounterText);\n\t}, _fpsCounter->lifetime());\n\tupdateFpsCounterGeometry();\n}\n\nvoid MainWindow::pruneFpsFrames(crl::time now) {\n\tconst auto till = now - kFpsCounterMeasureWindow;\n\twhile (!_fpsFrames.empty() && (_fpsFrames.front() <= till)) {\n\t\t_fpsFrames.pop_front();\n\t}\n}\n\nvoid MainWindow::recordFpsFrame() {\n\tif (!_fpsCounterVisible) {\n\t\treturn;\n\t}\n\tconst auto now = crl::now();\n\t_fpsFrames.push_back(now);\n\tpruneFpsFrames(now);\n}\n\nvoid MainWindow::refreshFpsCounter() {\n\tif (!_fpsCounterVisible || !_fpsCounter) {\n\t\treturn;\n\t}\n\tconst auto now = crl::now();\n\tpruneFpsFrames(now);\n\tconst auto elapsed = std::min(\n\t\tkFpsCounterMeasureWindow,\n\t\tnow - _fpsCounterShownAt);\n\tconst auto fps = (elapsed > 0)\n\t\t? ((int(_fpsFrames.size()) * 1000) + (elapsed / 2)) / elapsed\n\t\t: 0;\n\tconst auto displayed = (fps > 999) ? 999 : int(fps);\n\tconst auto text = QString::number(displayed) + u\" FPS\"_q;\n\tif (_fpsCounterText != text) {\n\t\t_fpsCounterText = text;\n\t\t_fpsCounter->update();\n\t}\n}\n\nvoid MainWindow::toggleFpsCounter() {\n\tensureFpsCounterCreated();\n\tif (!_fpsCounter) {\n\t\treturn;\n\t}\n\n\t_fpsCounterVisible = !_fpsCounterVisible;\n\t_fpsFrameQueued = false;\n\t_fpsFrames.clear();\n\tif (!_fpsCounterVisible) {\n\t\t_fpsCounterTimer.cancel();\n\t\t_fpsCounter->hide();\n\t\treturn;\n\t}\n\n\t_fpsCounterShownAt = crl::now();\n\t_fpsCounterText.clear();\n\tupdateFpsCounterGeometry();\n\t_fpsCounter->show();\n\trefreshFpsCounter();\n\tfixOrder();\n\t_fpsCounterTimer.callEach(kFpsCounterRefreshInterval);\n}\n\nvoid MainWindow::updateFpsCounterGeometry() {\n\tif (!_fpsCounter) {\n\t\treturn;\n\t}\n\tconst auto &tooltip = st::defaultTooltip;\n\t_fpsCounter->resize(\n\t\t(2 * st::lineWidth)\n\t\t\t+ tooltip.textPadding.left()\n\t\t\t+ tooltip.textPadding.right()\n\t\t\t+ tooltip.textStyle.font->width(u\"999 FPS\"_q),\n\t\t(2 * st::lineWidth)\n\t\t\t+ tooltip.textPadding.top()\n\t\t\t+ tooltip.textPadding.bottom()\n\t\t\t+ tooltip.textStyle.font->height);\n\t_fpsCounter->moveToLeft(\n\t\ttooltip.skip,\n\t\ttooltip.skip);\n}\n\nQPixmap MainWindow::grabForSlideAnimation() {\n\treturn Ui::GrabWidget(bodyWidget());\n}\n\nvoid MainWindow::preventOrInvoke(Fn<void()> callback) {\n\tif (_main && _main->preventsCloseSection(callback)) {\n\t\treturn;\n\t}\n\tcallback();\n}\n\nvoid MainWindow::setupPasscodeLock() {\n\tauto animated = (_main || _intro);\n\tauto oldContentCache = animated ? grabForSlideAnimation() : QPixmap();\n\t_passcodeLock.create(bodyWidget(), &controller());\n\tupdateControlsGeometry();\n\n\tui_hideSettingsAndLayer(anim::type::instant);\n\tif (_main) {\n\t\t_main->hide();\n\t}\n\tif (_intro) {\n\t\t_intro->hide();\n\t}\n\tif (animated) {\n\t\t_passcodeLock->showAnimated(std::move(oldContentCache));\n\t} else {\n\t\t_passcodeLock->showFinished();\n\t\tsetInnerFocus();\n\t}\n\tif (const auto sessionController = controller().sessionController()) {\n\t\tsessionController->session().attachWebView().closeAll();\n\t}\n}\n\nvoid MainWindow::setupSetupEmailLock() {\n\tauto animated = (_main || _intro || _passcodeLock);\n\tauto oldContentCache = animated ? grabForSlideAnimation() : QPixmap();\n\t_setupEmailLock.create(bodyWidget(), &controller());\n\tupdateControlsGeometry();\n\n\tui_hideSettingsAndLayer(anim::type::instant);\n\tif (_main) {\n\t\t_main->hide();\n\t}\n\tif (_intro) {\n\t\t_intro->hide();\n\t}\n\tif (_passcodeLock) {\n\t\t_passcodeLock->hide();\n\t}\n\tif (animated) {\n\t\t_setupEmailLock->showAnimated(std::move(oldContentCache));\n\t} else {\n\t\t_setupEmailLock->showFinished();\n\t\tsetInnerFocus();\n\t}\n\tif (const auto sessionController = controller().sessionController()) {\n\t\tsessionController->session().attachWebView().closeAll();\n\t}\n}\n\nvoid MainWindow::clearSetupEmailLock() {\n\tif (!_setupEmailLock) {\n\t\treturn;\n\t}\n\n\tauto oldContentCache = grabForSlideAnimation();\n\t_setupEmailLock.destroy();\n\tif (_passcodeLock) {\n\t\t_passcodeLock->show();\n\t\tupdateControlsGeometry();\n\t\t_passcodeLock->showAnimated(std::move(oldContentCache));\n\t} else if (_intro) {\n\t\t_intro->show();\n\t\tupdateControlsGeometry();\n\t\t_intro->showAnimated(std::move(oldContentCache), true);\n\t} else if (_main) {\n\t\t_main->show();\n\t\tupdateControlsGeometry();\n\t\t_main->showAnimated(std::move(oldContentCache), true);\n\t\tCore::App().checkStartUrls();\n\t}\n}\n\nvoid MainWindow::clearPasscodeLock() {\n\tExpects(_intro || _main);\n\n\tif (!_passcodeLock) {\n\t\treturn;\n\t}\n\n\tauto oldContentCache = grabForSlideAnimation();\n\t_passcodeLock.destroy();\n\tif (_setupEmailLock) {\n\t\t_setupEmailLock->show();\n\t\tupdateControlsGeometry();\n\t\t_setupEmailLock->showAnimated(std::move(oldContentCache));\n\t} else if (_intro) {\n\t\t_intro->show();\n\t\tupdateControlsGeometry();\n\t\t_intro->showAnimated(std::move(oldContentCache), true);\n\t} else if (_main) {\n\t\t_main->show();\n\t\tupdateControlsGeometry();\n\t\t_main->showAnimated(std::move(oldContentCache), true);\n\t\tCore::App().checkStartUrls();\n\t}\n}\n\nvoid MainWindow::setupIntro(\n\t\tIntro::EnterPoint point,\n\t\tQPixmap oldContentCache) {\n\tauto animated = (_main || _passcodeLock || _setupEmailLock);\n\n\tdestroyLayer();\n\tauto created = object_ptr<Intro::Widget>(\n\t\tbodyWidget(),\n\t\t&controller(),\n\t\t&account(),\n\t\tpoint);\n\tcreated->showSettingsRequested(\n\t) | rpl::on_next([=] {\n\t\tshowSettings();\n\t}, created->lifetime());\n\n\tclearWidgets();\n\t_intro = std::move(created);\n\tif (_passcodeLock || _setupEmailLock) {\n\t\t_intro->hide();\n\t} else {\n\t\t_intro->show();\n\t\tupdateControlsGeometry();\n\t\tif (animated) {\n\t\t\t_intro->showAnimated(std::move(oldContentCache));\n\t\t} else {\n\t\t\tsetInnerFocus();\n\t\t}\n\t}\n\tfixOrder();\n}\n\nvoid MainWindow::setupMain(\n\t\tMsgId singlePeerShowAtMsgId,\n\t\tQPixmap oldContentCache) {\n\tExpects(account().sessionExists());\n\n\tconst auto animated = _intro\n\t\t|| (_passcodeLock && !Core::App().passcodeLocked())\n\t\t|| _setupEmailLock;\n\tconst auto weakAnimatedLayer = (_main\n\t\t\t&& _layer\n\t\t\t&& !_passcodeLock\n\t\t\t&& !_setupEmailLock)\n\t\t? base::make_weak(_layer.get())\n\t\t: nullptr;\n\tif (weakAnimatedLayer) {\n\t\tAssert(!animated);\n\t\t_layer->hideAllAnimatedPrepare();\n\t} else {\n\t\tdestroyLayer();\n\t}\n\tauto created = object_ptr<MainWidget>(bodyWidget(), sessionController());\n\tclearWidgets();\n\t_main = std::move(created);\n\tupdateControlsGeometry();\n\tUi::SendPendingMoveResizeEvents(_main);\n\t_main->controller()->showByInitialId(\n\t\tWindow::SectionShow::Way::ClearStack,\n\t\tsinglePeerShowAtMsgId);\n\tif (_passcodeLock || _setupEmailLock) {\n\t\t_main->hide();\n\t} else {\n\t\t_main->show();\n\t\tupdateControlsGeometry();\n\t\tif (animated) {\n\t\t\t_main->showAnimated(std::move(oldContentCache));\n\t\t} else {\n\t\t\t_main->activate();\n\t\t}\n\t\tCore::App().checkStartUrls();\n\t}\n\tfixOrder();\n\tif (const auto strong = weakAnimatedLayer.get()) {\n\t\tstrong->hideAllAnimatedRun();\n\t}\n}\n\nvoid MainWindow::showSettings() {\n\tif (_passcodeLock || _setupEmailLock) {\n\t\treturn;\n\t}\n\n\tif (const auto session = sessionController()) {\n\t\tsession->showSettings();\n\t} else {\n\t\tshowSpecialLayer(\n\t\t\tBox<Settings::LayerWidget>(&controller()),\n\t\t\tanim::type::normal);\n\t}\n}\n\nvoid MainWindow::showSpecialLayer(\n\t\tobject_ptr<Ui::LayerWidget> layer,\n\t\tanim::type animated) {\n\tif (_passcodeLock || _setupEmailLock) {\n\t\treturn;\n\t}\n\n\tif (layer) {\n\t\tensureLayerCreated();\n\t\t_layer->showSpecialLayer(std::move(layer), animated);\n\t} else if (_layer) {\n\t\t_layer->hideSpecialLayer(animated);\n\t}\n}\n\nbool MainWindow::showSectionInExistingLayer(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params) {\n\tif (_layer) {\n\t\treturn _layer->showSectionInternal(memento, params);\n\t}\n\treturn false;\n}\n\nvoid MainWindow::showMainMenu() {\n\tif (_passcodeLock || _setupEmailLock) return;\n\n\tif (isHidden()) showFromTray();\n\n\tensureLayerCreated();\n\t_layer->showMainMenu(\n\t\tobject_ptr<Window::MainMenu>(body(), sessionController()),\n\t\tanim::type::normal);\n}\n\nvoid MainWindow::ensureLayerCreated() {\n\tif (_layer) {\n\t\treturn;\n\t}\n\t_layer = base::make_unique_q<Ui::LayerStackWidget>(\n\t\tbodyWidget(),\n\t\tcrl::guard(this, [=] { return controller().uiShow(); }));\n\n\t_layer->hideFinishEvents(\n\t) | rpl::filter([=] {\n\t\treturn _layer != nullptr; // Last hide finish is sent from destructor.\n\t}) | rpl::on_next([=] {\n\t\tdestroyLayer();\n\t}, _layer->lifetime());\n\n\tif (const auto controller = sessionController()) {\n\t\tcontroller->enableGifPauseReason(Window::GifPauseReason::Layer);\n\t}\n}\n\nvoid MainWindow::destroyLayer() {\n\tif (!_layer) {\n\t\treturn;\n\t}\n\n\tauto layer = base::take(_layer);\n\tconst auto resetFocus = Ui::InFocusChain(layer);\n\tif (resetFocus) {\n\t\tsetFocus();\n\t}\n\tlayer = nullptr;\n\n\tif (const auto controller = sessionController()) {\n\t\tcontroller->disableGifPauseReason(Window::GifPauseReason::Layer);\n\t}\n\tif (resetFocus) {\n\t\tsetInnerFocus();\n\t}\n\tInvokeQueued(this, [=] {\n\t\tcheckActivation();\n\t});\n}\n\nvoid MainWindow::ui_hideSettingsAndLayer(anim::type animated) {\n\tif (animated == anim::type::instant) {\n\t\tdestroyLayer();\n\t} else if (_layer) {\n\t\t_layer->hideAll(animated);\n\t}\n}\n\nvoid MainWindow::ui_removeLayerBlackout() {\n\tif (_layer) {\n\t\t_layer->removeBodyCache();\n\t}\n}\n\nMainWidget *MainWindow::sessionContent() const {\n\treturn _main.data();\n}\n\nvoid MainWindow::showOrHideBoxOrLayer(\n\t\tstd::variant<\n\t\t\tv::null_t,\n\t\t\tobject_ptr<Ui::BoxContent>,\n\t\t\tstd::unique_ptr<Ui::LayerWidget>> &&layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) {\n\tusing UniqueLayer = std::unique_ptr<Ui::LayerWidget>;\n\tusing ObjectBox = object_ptr<Ui::BoxContent>;\n\tif (auto layerWidget = std::get_if<UniqueLayer>(&layer)) {\n\t\tensureLayerCreated();\n\t\t_layer->showLayer(std::move(*layerWidget), options, animated);\n\t} else if (auto box = std::get_if<ObjectBox>(&layer)) {\n\t\tensureLayerCreated();\n\t\t_layer->showBox(std::move(*box), options, animated);\n\t} else {\n\t\tif (_layer) {\n\t\t\t_layer->hideTopLayer(animated);\n\t\t\tif ((animated == anim::type::instant)\n\t\t\t\t&& _layer\n\t\t\t\t&& !_layer->layerShown()) {\n\t\t\t\tdestroyLayer();\n\t\t\t}\n\t\t}\n\t\tCore::App().hideMediaView();\n\t}\n}\n\nbool MainWindow::ui_isLayerShown() const {\n\treturn _layer != nullptr;\n}\n\nbool MainWindow::showMediaPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<DocumentData*> document) {\n\tconst auto media = document->activeMediaView();\n\tconst auto preview = Data::VideoPreviewState(media.get());\n\tif (!document->sticker()\n\t\t&& (!document->isAnimation() || !preview.loaded())) {\n\t\treturn false;\n\t}\n\tif (!_mediaPreview) {\n\t\t_mediaPreview.create(bodyWidget(), sessionController());\n\t\tupdateControlsGeometry();\n\t}\n\tif (_mediaPreview->isHidden()) {\n\t\tfixOrder();\n\t}\n\t_mediaPreview->showPreview(origin, document);\n\treturn true;\n}\n\nbool MainWindow::showMediaPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<PhotoData*> photo) {\n\tif (!_mediaPreview) {\n\t\t_mediaPreview.create(bodyWidget(), sessionController());\n\t\tupdateControlsGeometry();\n\t}\n\tif (_mediaPreview->isHidden()) {\n\t\tfixOrder();\n\t}\n\t_mediaPreview->showPreview(origin, photo);\n\treturn true;\n}\n\nvoid MainWindow::hideMediaPreview() {\n\tif (!_mediaPreview) {\n\t\treturn;\n\t}\n\t_mediaPreview->hidePreview();\n}\n\nvoid MainWindow::themeUpdated(const Window::Theme::BackgroundUpdate &data) {\n\tusing Type = Window::Theme::BackgroundUpdate::Type;\n\n\t// We delay animating theme warning because we want all other\n\t// subscribers to receive palette changed notification before any\n\t// animations (that include pixmap caches with old palette values).\n\tif (data.type == Type::TestingTheme) {\n\t\tif (!_testingThemeWarning) {\n\t\t\t_testingThemeWarning.create(bodyWidget());\n\t\t\t_testingThemeWarning->hide();\n\t\t\t_testingThemeWarning->setGeometry(rect());\n\t\t\t_testingThemeWarning->setHiddenCallback([this] { _testingThemeWarning.destroyDelayed(); });\n\t\t}\n\t\tcrl::on_main(this, [=] {\n\t\t\tif (_testingThemeWarning) {\n\t\t\t\t_testingThemeWarning->showAnimated();\n\t\t\t}\n\t\t});\n\t} else if (data.type == Type::RevertingTheme || data.type == Type::ApplyingTheme) {\n\t\tif (_testingThemeWarning) {\n\t\t\tif (_testingThemeWarning->isHidden()) {\n\t\t\t\t_testingThemeWarning.destroy();\n\t\t\t} else {\n\t\t\t\tcrl::on_main(this, [=] {\n\t\t\t\t\tif (_testingThemeWarning) {\n\t\t\t\t\t\t_testingThemeWarning->hideAnimated();\n\t\t\t\t\t\t_testingThemeWarning = nullptr;\n\t\t\t\t\t}\n\t\t\t\t\tsetInnerFocus();\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n}\n\nbool MainWindow::markingAsRead() const {\n\treturn _main\n\t\t&& !_main->isHidden()\n\t\t&& !_main->animatingShow()\n\t\t&& !_layer\n\t\t&& !isHidden()\n\t\t&& !isMinimized()\n\t\t&& windowHandle()->isExposed()\n\t\t&& (AutoScrollInactiveChat.value()\n\t\t\t|| (isActive() && !_main->session().updates().isIdle()));\n}\n\nvoid MainWindow::checkActivation() {\n\tupdateIsActive();\n\tif (_main) {\n\t\t_main->checkActivation();\n\t}\n}\n\nbool MainWindow::contentOverlapped(const QRect &globalRect) {\n\treturn (_main && _main->contentOverlapped(globalRect))\n\t\t|| (_layer && _layer->contentOverlapped(globalRect));\n}\n\nvoid MainWindow::setInnerFocus() {\n\tif (_testingThemeWarning) {\n\t\t_testingThemeWarning->setFocus();\n\t} else if (_layer && _layer->canSetFocus()) {\n\t\t_layer->setInnerFocus();\n\t} else if (_passcodeLock) {\n\t\t_passcodeLock->setInnerFocus();\n\t} else if (_setupEmailLock) {\n\t\t_setupEmailLock->setInnerFocus();\n\t} else if (_main) {\n\t\t_main->setInnerFocus();\n\t} else if (_intro) {\n\t\t_intro->setInnerFocus();\n\t}\n}\n\nbool MainWindow::eventFilter(QObject *object, QEvent *e) {\n\tswitch (e->type()) {\n\tcase QEvent::ShortcutOverride: {\n\t\tif (matchesFpsCounterKey(object, static_cast<QKeyEvent*>(e))) {\n\t\t\te->accept();\n\t\t\treturn true;\n\t\t}\n\t} break;\n\n\tcase QEvent::KeyPress: {\n\t\tif (matchesFpsCounterKey(object, static_cast<QKeyEvent*>(e))) {\n\t\t\ttoggleFpsCounter();\n\t\t\treturn true;\n\t\t}\n\t\tif (Logs::DebugEnabled()\n\t\t\t&& object == windowHandle()) {\n\t\t\tconst auto key = static_cast<QKeyEvent*>(e)->key();\n\t\t\tFeedLangTestingKey(key);\n\t\t}\n#ifdef _DEBUG\n\t\tif (static_cast<QKeyEvent*>(e)->modifiers().testFlag(\n\t\t\t\tQt::ControlModifier)) {\n\t\t\tswitch (static_cast<QKeyEvent*>(e)->key()) {\n\t\t\tcase Qt::Key_F11:\n\t\t\t\tanim::SetSlowMultiplier((anim::SlowMultiplier() == 10)\n\t\t\t\t\t? 1\n\t\t\t\t\t: 10);\n\t\t\t\treturn true;\n\t\t\tcase Qt::Key_F12:\n\t\t\t\tanim::SetSlowMultiplier((anim::SlowMultiplier() == 50)\n\t\t\t\t\t? 1\n\t\t\t\t\t: 50);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n#endif\n\t} break;\n\n\tcase QEvent::UpdateRequest:\n\tcase QEvent::Paint: {\n\t\tif (countsForFpsCounter(object) && !_fpsFrameQueued) {\n\t\t\t_fpsFrameQueued = true;\n\t\t\trecordFpsFrame();\n\t\t\tInvokeQueued(this, [=] {\n\t\t\t\t_fpsFrameQueued = false;\n\t\t\t});\n\t\t}\n\t} break;\n\n\tcase QEvent::MouseMove: {\n\t\tconst auto position = static_cast<QMouseEvent*>(e)->globalPos();\n\t\tif (_lastMousePosition != position) {\n\t\t\tif (const auto controller = sessionController()) {\n\t\t\t\tif (controller->session().updates().isIdle()) {\n\t\t\t\t\tCore::App().updateNonIdle();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t_lastMousePosition = position;\n\t} break;\n\n\tcase QEvent::MouseButtonRelease: {\n\t\thideMediaPreview();\n\t} break;\n\n\tcase QEvent::ApplicationActivate: {\n\t\tif (object == QCoreApplication::instance()) {\n\t\t\tInvokeQueued(this, [=] {\n\t\t\t\thandleActiveChanged(isActiveWindow());\n\t\t\t});\n\t\t}\n\t} break;\n\n\tcase QEvent::WindowStateChange: {\n\t\tif (object == this) {\n\t\t\tauto state = (windowState() & Qt::WindowMinimized) ? Qt::WindowMinimized :\n\t\t\t\t((windowState() & Qt::WindowMaximized) ? Qt::WindowMaximized :\n\t\t\t\t((windowState() & Qt::WindowFullScreen) ? Qt::WindowFullScreen : Qt::WindowNoState));\n\t\t\thandleStateChanged(state);\n\t\t}\n\t} break;\n\n\tcase QEvent::Move:\n\tcase QEvent::Resize: {\n\t\tif (object == this) {\n\t\t\tpositionUpdated();\n\t\t}\n\t} break;\n\t}\n\n\treturn Platform::MainWindow::eventFilter(object, e);\n}\n\nbool MainWindow::takeThirdSectionFromLayer() {\n\treturn _layer ? _layer->takeToThirdSection() : false;\n}\n\nvoid MainWindow::fixOrder() {\n\tif (_setupEmailLock) _setupEmailLock->raise();\n\tif (_passcodeLock) _passcodeLock->raise();\n\tif (_layer) _layer->raise();\n\tif (_mediaPreview) _mediaPreview->raise();\n\tif (_testingThemeWarning) _testingThemeWarning->raise();\n\tif (_fpsCounter) _fpsCounter->raise();\n}\n\nvoid MainWindow::closeEvent(QCloseEvent *e) {\n\tif (Core::Sandbox::Instance().isSavingSession() || Core::Quitting()) {\n\t\te->accept();\n\t\tCore::Quit();\n\t\treturn;\n\t} else if (Core::App().closeNonLastAsync(&controller())) {\n\t\te->accept();\n\t\treturn;\n\t}\n\te->ignore();\n\tconst auto hasAuth = [&] {\n\t\tif (!Core::App().domain().started()) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (const auto &[_, account] : Core::App().domain().accounts()) {\n\t\t\tif (account->sessionExists()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}();\n\tif (!hasAuth || !hideNoQuit()) {\n\t\tCore::Quit();\n\t}\n}\n\nvoid MainWindow::updateControlsGeometry() {\n\tPlatform::MainWindow::updateControlsGeometry();\n\n\tauto body = bodyWidget()->rect();\n\tif (_passcodeLock) _passcodeLock->setGeometry(body);\n\tif (_setupEmailLock) _setupEmailLock->setGeometry(body);\n\tauto mainLeft = 0;\n\tauto mainWidth = body.width();\n\tif (const auto session = sessionController()) {\n\t\tif (const auto skip = session->filtersWidth()) {\n\t\t\tmainLeft += skip;\n\t\t\tmainWidth -= skip;\n\t\t}\n\t}\n\tif (_main) {\n\t\t_main->setGeometry({\n\t\t\tbody.x() + mainLeft,\n\t\t\tbody.y(),\n\t\t\tmainWidth,\n\t\t\tbody.height() });\n\t}\n\tif (_intro) _intro->setGeometry(body);\n\tif (_layer) _layer->setGeometry(body);\n\tif (_mediaPreview) _mediaPreview->setGeometry(body);\n\tif (_testingThemeWarning) _testingThemeWarning->setGeometry(body);\n\tif (_fpsCounter) updateFpsCounterGeometry();\n\n\tif (_main) _main->checkMainSectionToLayer();\n}\n\nvoid MainWindow::sendPaths() {\n\tif (controller().locked()) {\n\t\treturn;\n\t}\n\tCore::App().hideMediaView();\n\tui_hideSettingsAndLayer(anim::type::instant);\n\tif (_main) {\n\t\t_main->activate();\n\t}\n}\n\nMainWindow::~MainWindow() = default;\n"
  },
  {
    "path": "Telegram/SourceFiles/mainwindow.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <deque>\n\n#include \"base/timer.h\"\n#include \"platform/platform_main_window.h\"\n#include \"ui/layers/layer_widget.h\"\n\nclass MainWidget;\nclass QKeyEvent;\n\nnamespace Intro {\nclass Widget;\nenum class EnterPoint : uchar;\n} // namespace Intro\n\nnamespace Window {\nclass MediaPreviewWidget;\nclass SectionMemento;\nstruct SectionShow;\nclass PasscodeLockWidget;\nclass SetupEmailLockWidget;\nnamespace Theme {\nstruct BackgroundUpdate;\nclass WarningWidget;\n} // namespace Theme\n} // namespace Window\n\nnamespace Ui {\nclass LinkButton;\nclass BoxContent;\nclass LayerStackWidget;\nclass RpWidget;\n} // namespace Ui\n\nclass MediaPreviewWidget;\n\nextern const char kOptionAutoScrollInactiveChat[];\n\nclass MainWindow : public Platform::MainWindow {\npublic:\n\texplicit MainWindow(not_null<Window::Controller*> controller);\n\t~MainWindow();\n\n\tvoid finishFirstShow();\n\n\tvoid preventOrInvoke(Fn<void()> callback);\n\n\tvoid setupPasscodeLock();\n\tvoid clearPasscodeLock();\n\tvoid setupSetupEmailLock();\n\tvoid clearSetupEmailLock();\n\tvoid setupIntro(Intro::EnterPoint point, QPixmap oldContentCache);\n\tvoid setupMain(MsgId singlePeerShowAtMsgId, QPixmap oldContentCache);\n\n\tvoid showSettings();\n\n\tvoid setInnerFocus() override;\n\n\tMainWidget *sessionContent() const;\n\n\tvoid checkActivation() override;\n\t[[nodiscard]] bool markingAsRead() const;\n\n\tbool takeThirdSectionFromLayer();\n\n\tvoid sendPaths();\n\n\t[[nodiscard]] bool contentOverlapped(const QRect &globalRect);\n\t[[nodiscard]] bool contentOverlapped(QWidget *w, QPaintEvent *e) {\n\t\treturn contentOverlapped(\n\t\t\tQRect(w->mapToGlobal(e->rect().topLeft()), e->rect().size()));\n\t}\n\t[[nodiscard]] bool contentOverlapped(QWidget *w, const QRegion &r) {\n\t\treturn contentOverlapped(QRect(\n\t\t\tw->mapToGlobal(r.boundingRect().topLeft()),\n\t\t\tr.boundingRect().size()));\n\t}\n\n\tvoid showMainMenu();\n\tvoid fixOrder() override;\n\n\t[[nodiscard]] QPixmap grabForSlideAnimation();\n\n\tvoid showOrHideBoxOrLayer(\n\t\tstd::variant<\n\t\t\tv::null_t,\n\t\t\tobject_ptr<Ui::BoxContent>,\n\t\t\tstd::unique_ptr<Ui::LayerWidget>> &&layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated);\n\tvoid showSpecialLayer(\n\t\tobject_ptr<Ui::LayerWidget> layer,\n\t\tanim::type animated);\n\tbool showSectionInExistingLayer(\n\t\tnot_null<Window::SectionMemento*> memento,\n\t\tconst Window::SectionShow &params);\n\tvoid ui_hideSettingsAndLayer(anim::type animated);\n\tvoid ui_removeLayerBlackout();\n\t[[nodiscard]] bool ui_isLayerShown() const;\n\tbool showMediaPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<DocumentData*> document);\n\tbool showMediaPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<PhotoData*> photo);\n\tvoid hideMediaPreview();\n\n\tvoid updateControlsGeometry() override;\n\nprotected:\n\tbool eventFilter(QObject *o, QEvent *e) override;\n\tvoid closeEvent(QCloseEvent *e) override;\n\n\tvoid initHook() override;\n\tvoid clearWidgetsHook() override;\n\nprivate:\n\tvoid applyInitialWorkMode();\n\t[[nodiscard]] bool countsForFpsCounter(QObject *object) const;\n\t[[nodiscard]] bool matchesFpsCounterKey(\n\t\tQObject *object,\n\t\tconst QKeyEvent *e) const;\n\tvoid ensureFpsCounterCreated();\n\tvoid pruneFpsFrames(crl::time now);\n\tvoid recordFpsFrame();\n\tvoid refreshFpsCounter();\n\tvoid toggleFpsCounter();\n\tvoid updateFpsCounterGeometry();\n\tvoid ensureLayerCreated();\n\tvoid destroyLayer();\n\n\tvoid themeUpdated(const Window::Theme::BackgroundUpdate &data);\n\n\tQPoint _lastMousePosition;\n\n\tobject_ptr<Ui::RpWidget> _fpsCounter = { nullptr };\n\tbase::Timer _fpsCounterTimer;\n\tstd::deque<crl::time> _fpsFrames;\n\tcrl::time _fpsCounterShownAt = 0;\n\tQString _fpsCounterText;\n\tbool _fpsCounterVisible = false;\n\tbool _fpsFrameQueued = false;\n\n\tobject_ptr<Window::PasscodeLockWidget> _passcodeLock = { nullptr };\n\tobject_ptr<Window::SetupEmailLockWidget> _setupEmailLock = { nullptr };\n\tobject_ptr<Intro::Widget> _intro = { nullptr };\n\tobject_ptr<MainWidget> _main = { nullptr };\n\tbase::unique_qptr<Ui::LayerStackWidget> _layer;\n\tobject_ptr<Window::MediaPreviewWidget> _mediaPreview = { nullptr };\n\n\tobject_ptr<Window::Theme::WarningWidget> _testingThemeWarning = { nullptr };\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/audio/media_audio.h\"\n\n#include \"media/audio/media_audio_ffmpeg_loader.h\"\n#include \"media/audio/media_child_ffmpeg_loader.h\"\n#include \"media/audio/media_audio_loaders.h\"\n#include \"media/audio/media_audio_track.h\"\n#include \"media/media_common.h\"\n#include \"media/streaming/media_streaming_utility.h\"\n#include \"webrtc/webrtc_environment.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_session.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n\n#include <al.h>\n#include <alc.h>\n\n#include <numeric>\n\nQ_DECLARE_METATYPE(AudioMsgId);\nQ_DECLARE_METATYPE(VoiceWaveform);\n\nnamespace {\n\nconstexpr auto kSuppressRatioAll = 0.2;\nconstexpr auto kSuppressRatioSong = 0.05;\nconstexpr auto kWaveformCounterBufferSize = 256 * 1024;\n\nQMutex AudioMutex;\nALCdevice *AudioDevice = nullptr;\nALCcontext *AudioContext = nullptr;\nWebrtc::DeviceResolvedId AudioDeviceLastUsedId{\n\t.type = Webrtc::DeviceType::Playback\n};\n\nauto VolumeMultiplierAll = 1.;\nauto VolumeMultiplierSong = 1.;\n\n} // namespace\n\nnamespace Media {\nnamespace Audio {\nnamespace {\n\nPlayer::Mixer *MixerInstance = nullptr;\n\n// Thread: Any.\nbool ContextErrorHappened() {\n\tALenum errCode;\n\tif ((errCode = alcGetError(AudioDevice)) != ALC_NO_ERROR) {\n\t\tLOG((\"Audio Context Error: %1, %2\").arg(errCode).arg((const char *)alcGetString(AudioDevice, errCode)));\n\t\treturn true;\n\t}\n\treturn false;\n}\n\n// Thread: Any.\nbool PlaybackErrorHappened() {\n\tALenum errCode;\n\tif ((errCode = alGetError()) != AL_NO_ERROR) {\n\t\tLOG((\"Audio Playback Error: %1, %2\").arg(errCode).arg((const char *)alGetString(errCode)));\n\t\treturn true;\n\t}\n\treturn false;\n}\n\n// Thread: Any. Must be locked: AudioMutex.\nvoid DestroyPlaybackDevice() {\n\tif (AudioContext) {\n\t\talcMakeContextCurrent(nullptr);\n\t\talcDestroyContext(AudioContext);\n\t\tAudioContext = nullptr;\n\t}\n\n\tif (AudioDevice) {\n\t\talcCloseDevice(AudioDevice);\n\t\tAudioDevice = nullptr;\n\t}\n}\n\n// Thread: Any. Must be locked: AudioMutex.\nbool CreatePlaybackDevice() {\n\tif (AudioDevice) return true;\n\n\tAudioDeviceLastUsedId = Current().playbackDeviceId();\n\n\tconst auto id = AudioDeviceLastUsedId.isDefault()\n\t\t? std::string()\n\t\t: AudioDeviceLastUsedId.value.toStdString();\n\tAudioDevice = alcOpenDevice(id.empty() ? nullptr : id.c_str());\n\tif (!AudioDevice) {\n\t\tLOG((\"Audio Error: Could not create default playback device, refreshing..\"));\n\t\tcrl::on_main([] {\n\t\t\tconst auto type = Webrtc::DeviceType::Playback;\n\t\t\tCore::App().mediaDevices().forceRefresh(type);\n\t\t});\n\t\treturn false;\n\t}\n\n\tAudioContext = alcCreateContext(AudioDevice, nullptr);\n\talcMakeContextCurrent(AudioContext);\n\tif (ContextErrorHappened()) {\n\t\tDestroyPlaybackDevice();\n\t\treturn false;\n\t}\n\n\tALfloat v[] = { 0.f, 0.f, -1.f, 0.f, 1.f, 0.f };\n\talListener3f(AL_POSITION, 0.f, 0.f, 0.f);\n\talListener3f(AL_VELOCITY, 0.f, 0.f, 0.f);\n\talListenerfv(AL_ORIENTATION, v);\n\n\talDistanceModel(AL_NONE);\n\n\treturn true;\n}\n\n// Thread: Main. Must be locked: AudioMutex.\nvoid ClosePlaybackDevice(not_null<Instance*> instance) {\n\tif (!AudioDevice) return;\n\n\tLOG((\"Audio Info: Closing audio playback device.\"));\n\n\tif (Player::mixer()) {\n\t\tPlayer::mixer()->prepareToCloseDevice();\n\t}\n\tinstance->detachTracks();\n\n\tDestroyPlaybackDevice();\n}\n\n} // namespace\n\n// Thread: Main.\nvoid Start(not_null<Instance*> instance) {\n\tAssert(AudioDevice == nullptr);\n\n\tqRegisterMetaType<AudioMsgId>();\n\tqRegisterMetaType<VoiceWaveform>();\n\n\tconst auto loglevel = getenv(\"ALSOFT_LOGLEVEL\");\n\tLOG((\"OpenAL Logging Level: %1\").arg(loglevel ? loglevel : \"(not set)\"));\n\n\tMixerInstance = new Player::Mixer(instance);\n}\n\n// Thread: Main.\nvoid Finish(not_null<Instance*> instance) {\n\t// MixerInstance variable should be modified under AudioMutex protection.\n\t// So it is modified in the ~Mixer() destructor after all tracks are cleared.\n\tdelete MixerInstance;\n\n\t// No sync required already.\n\tClosePlaybackDevice(instance);\n}\n\n// Thread: Main. Locks: AudioMutex.\nbool IsAttachedToDevice() {\n\tQMutexLocker lock(&AudioMutex);\n\treturn (AudioDevice != nullptr);\n}\n\n// Thread: Any. Must be locked: AudioMutex.\nbool AttachToDevice() {\n\tif (AudioDevice) {\n\t\treturn true;\n\t}\n\tLOG((\"Audio Info: recreating audio device and reattaching the tracks\"));\n\n\tCreatePlaybackDevice();\n\tif (!AudioDevice) {\n\t\treturn false;\n\t}\n\n\tif (const auto m = Player::mixer()) {\n\t\tm->reattachTracks();\n\t\tm->scheduleFaderCallback();\n\t}\n\n\tcrl::on_main([] {\n\t\tif (!Core::Quitting()) {\n\t\t\tCurrent().reattachTracks();\n\t\t}\n\t});\n\treturn true;\n}\n\nvoid ScheduleDetachFromDeviceSafe() {\n\tcrl::on_main([] {\n\t\tif (!Core::Quitting()) {\n\t\t\tCurrent().scheduleDetachFromDevice();\n\t\t}\n\t});\n}\n\nvoid ScheduleDetachIfNotUsedSafe() {\n\tcrl::on_main([] {\n\t\tif (!Core::Quitting()) {\n\t\t\tCurrent().scheduleDetachIfNotUsed();\n\t\t}\n\t});\n}\n\nvoid StopDetachIfNotUsedSafe() {\n\tcrl::on_main([] {\n\t\tif (!Core::Quitting()) {\n\t\t\tCurrent().stopDetachIfNotUsed();\n\t\t}\n\t});\n}\n\nbool SupportsSpeedControl() {\n\tstatic const auto result = [] {\n\t\treturn avfilter_get_by_name(\"abuffer\")\n\t\t\t&& avfilter_get_by_name(\"abuffersink\")\n\t\t\t&& avfilter_get_by_name(\"atempo\");\n\t}();\n\treturn result;\n}\n\n} // namespace Audio\n\nnamespace Player {\nnamespace {\n\nconstexpr auto kVolumeRound = 10000;\nconstexpr auto kPreloadSeconds = 2LL; // preload next part if less than 2 seconds remains\nconstexpr auto kFadeDuration = crl::time(500);\nconstexpr auto kCheckPlaybackPositionTimeout = crl::time(100); // 100ms per check audio position\nconstexpr auto kCheckPlaybackPositionDelta = 2400LL; // update position called each 2400 samples\nconstexpr auto kCheckFadingTimeout = crl::time(7); // 7ms\n\nrpl::event_stream<AudioMsgId> UpdatedStream;\n\n} // namespace\n\nrpl::producer<AudioMsgId> Updated() {\n\treturn UpdatedStream.events();\n}\n\n// Thread: Any. Must be locked: AudioMutex.\nfloat64 ComputeVolume(AudioMsgId::Type type) {\n\tconst auto gain = [&] {\n\t\tswitch (type) {\n\t\tcase AudioMsgId::Type::Voice:\n\t\t\treturn VolumeMultiplierAll * mixer()->getSongVolume();\n\t\tcase AudioMsgId::Type::Song:\n\t\t\treturn VolumeMultiplierSong * mixer()->getSongVolume();\n\t\tcase AudioMsgId::Type::Video: return mixer()->getVideoVolume();\n\t\t}\n\t\treturn 1.;\n\t}();\n\treturn gain * gain * gain;\n}\n\nMixer *mixer() {\n\treturn Audio::MixerInstance;\n}\n\nvoid Mixer::Track::createStream(AudioMsgId::Type type) {\n\talGenSources(1, &stream.source);\n\talSourcef(stream.source, AL_PITCH, 1.f);\n\talSource3f(stream.source, AL_POSITION, 0, 0, 0);\n\talSource3f(stream.source, AL_VELOCITY, 0, 0, 0);\n\talSourcei(stream.source, AL_LOOPING, 0);\n\talSourcei(stream.source, AL_SOURCE_RELATIVE, 1);\n\talSourcei(stream.source, AL_ROLLOFF_FACTOR, 0);\n\tif (alIsExtensionPresent(\"AL_SOFT_direct_channels_remix\")) {\n\t\talSourcei(\n\t\t\tstream.source,\n\t\t\talGetEnumValue(\"AL_DIRECT_CHANNELS_SOFT\"),\n\t\t\talGetEnumValue(\"AL_REMIX_UNMATCHED_SOFT\"));\n\t}\n\talGenBuffers(3, stream.buffers);\n}\n\nvoid Mixer::Track::destroyStream() {\n\tif (isStreamCreated()) {\n\t\talDeleteBuffers(3, stream.buffers);\n\t\talDeleteSources(1, &stream.source);\n\t}\n\tstream.source = 0;\n\tfor (auto i = 0; i != 3; ++i) {\n\t\tstream.buffers[i] = 0;\n\t}\n}\n\nvoid Mixer::Track::reattach(AudioMsgId::Type type) {\n\tif (isStreamCreated()\n\t\t|| (!withSpeed.samples[0] && !state.id.externalPlayId())) {\n\t\treturn;\n\t}\n\n\tcreateStream(type);\n\tfor (auto i = 0; i != kBuffersCount; ++i) {\n\t\tif (!withSpeed.samples[i]) {\n\t\t\tbreak;\n\t\t}\n\t\talBufferData(\n\t\t\tstream.buffers[i],\n\t\t\tformat,\n\t\t\twithSpeed.buffered[i].constData(),\n\t\t\twithSpeed.buffered[i].size(),\n\t\t\tstate.frequency);\n\t\talSourceQueueBuffers(stream.source, 1, stream.buffers + i);\n\t}\n\n\talSourcei(\n\t\tstream.source,\n\t\tAL_SAMPLE_OFFSET,\n\t\tqMax(withSpeed.position - withSpeed.bufferedPosition, 0LL));\n\tif (!IsStopped(state.state)\n\t\t&& (state.state != State::PausedAtEnd)\n\t\t&& !state.waitingForData) {\n\t\talSourcef(stream.source, AL_GAIN, ComputeVolume(type));\n\t\talSourcePlay(stream.source);\n\t\tif (IsPaused(state.state)) {\n\t\t\t// We must always start the source if we want the AL_SAMPLE_OFFSET to be applied.\n\t\t\t// Otherwise it won't be read by alGetSource and we'll get a corrupt position.\n\t\t\t// So in case of a paused source we start it and then immediately pause it.\n\t\t\talSourcePause(stream.source);\n\t\t}\n\t}\n}\n\nvoid Mixer::Track::detach() {\n\tgetNotQueuedBufferIndex();\n\tresetStream();\n\tdestroyStream();\n}\n\nvoid Mixer::Track::clear() {\n\tdetach();\n\n\tstate = TrackState();\n\tfile = Core::FileLocation();\n\tdata = QByteArray();\n\tformat = 0;\n\tloading = false;\n\tloaded = false;\n\twaitingForBuffer = false;\n\twithSpeed = WithSpeed();\n\tspeed = 1.;\n\n\tsetExternalData(nullptr);\n\tlastUpdateWhen = 0;\n\tlastUpdatePosition = 0;\n}\n\nvoid Mixer::Track::started() {\n\tresetStream();\n\n\tformat = 0;\n\tloaded = false;\n\twithSpeed = WithSpeed();\n}\n\nbool Mixer::Track::isStreamCreated() const {\n\treturn alIsSource(stream.source);\n}\n\nvoid Mixer::Track::ensureStreamCreated(AudioMsgId::Type type) {\n\tif (!isStreamCreated()) {\n\t\tcreateStream(type);\n\t}\n}\n\nint Mixer::Track::getNotQueuedBufferIndex() {\n\t// See if there are no free buffers right now.\n\twhile (withSpeed.samples[kBuffersCount - 1] != 0) {\n\t\t// Try to unqueue some buffer.\n\t\tALint processed = 0;\n\t\talGetSourcei(stream.source, AL_BUFFERS_PROCESSED, &processed);\n\t\tif (processed < 1) { // No processed buffers, wait.\n\t\t\treturn -1;\n\t\t}\n\n\t\t// Unqueue some processed buffer.\n\t\tALuint buffer = 0;\n\t\talSourceUnqueueBuffers(stream.source, 1, &buffer);\n\n\t\t// Find it in the list and clear it.\n\t\tbool found = false;\n\t\tfor (auto i = 0; i != kBuffersCount; ++i) {\n\t\t\tif (stream.buffers[i] == buffer) {\n\t\t\t\tconst auto samplesInBuffer = withSpeed.samples[i];\n\t\t\t\twithSpeed.bufferedPosition += samplesInBuffer;\n\t\t\t\twithSpeed.bufferedLength -= samplesInBuffer;\n\t\t\t\tfor (auto j = i + 1; j != kBuffersCount; ++j) {\n\t\t\t\t\twithSpeed.samples[j - 1] = withSpeed.samples[j];\n\t\t\t\t\tstream.buffers[j - 1] = stream.buffers[j];\n\t\t\t\t\twithSpeed.buffered[j - 1] = withSpeed.buffered[j];\n\t\t\t\t}\n\t\t\t\twithSpeed.samples[kBuffersCount - 1] = 0;\n\t\t\t\tstream.buffers[kBuffersCount - 1] = buffer;\n\t\t\t\twithSpeed.buffered[kBuffersCount - 1] = QByteArray();\n\t\t\t\tfound = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (!found) {\n\t\t\tLOG((\"Audio Error: Could not find the unqueued buffer! Buffer %1 in source %2 with processed count %3\").arg(buffer).arg(stream.source).arg(processed));\n\t\t\treturn -1;\n\t\t}\n\t}\n\n\tfor (auto i = 0; i != kBuffersCount; ++i) {\n\t\tif (!withSpeed.samples[i]) {\n\t\t\treturn i;\n\t\t}\n\t}\n\treturn -1;\n}\n\nvoid Mixer::Track::setExternalData(\n\t\tstd::unique_ptr<ExternalSoundData> data) {\n\tnextSpeed = speed = data ? data->speed : 1.;\n\texternalData = std::move(data);\n}\n\nvoid Mixer::Track::updateStatePosition() {\n\tstate.position = SpeedIndependentPosition(withSpeed.position, speed);\n}\n\nvoid Mixer::Track::updateWithSpeedPosition() {\n\twithSpeed.position = SpeedDependentPosition(state.position, speed);\n\twithSpeed.fineTunedPosition = withSpeed.position;\n}\n\nint64 Mixer::Track::SpeedIndependentPosition(\n\t\tint64 position,\n\t\tfloat64 speed) {\n\tExpects(speed <= kSpeedMax);\n\n\treturn int64(base::SafeRound(position * speed));\n}\n\nint64 Mixer::Track::SpeedDependentPosition(\n\t\tint64 position,\n\t\tfloat64 speed) {\n\tExpects(speed >= kSpeedMin);\n\n\treturn int64(base::SafeRound(position / speed));\n}\n\nvoid Mixer::Track::resetStream() {\n\tif (isStreamCreated()) {\n\t\talSourceStop(stream.source);\n\t\talSourcei(stream.source, AL_BUFFER, AL_NONE);\n\t}\n}\n\nMixer::Track::~Track() = default;\n\nMixer::Mixer(not_null<Audio::Instance*> instance)\n: _instance(instance)\n, _volumeVideo(kVolumeRound)\n, _volumeSong(kVolumeRound)\n, _fader(new Fader(&_faderThread))\n, _loader(new Loaders(&_loaderThread)) {\n\tconnect(this, SIGNAL(suppressSong()), _fader, SLOT(onSuppressSong()));\n\tconnect(this, SIGNAL(unsuppressSong()), _fader, SLOT(onUnsuppressSong()));\n\tconnect(this, SIGNAL(suppressAll(qint64)), _fader, SLOT(onSuppressAll(qint64)));\n\n\tCore::App().settings().songVolumeChanges(\n\t) | rpl::on_next([=] {\n\t\tInvokeQueued(_fader, [fader = _fader] {\n\t\t\tfader->songVolumeChanged();\n\t\t});\n\t}, _lifetime);\n\n\tCore::App().settings().videoVolumeChanges(\n\t) | rpl::on_next([=] {\n\t\tInvokeQueued(_fader, [fader = _fader] {\n\t\t\tfader->videoVolumeChanged();\n\t\t});\n\t}, _lifetime);\n\n\tconnect(this, SIGNAL(loaderOnStart(AudioMsgId,qint64)), _loader, SLOT(onStart(AudioMsgId,qint64)));\n\tconnect(this, SIGNAL(loaderOnCancel(AudioMsgId)), _loader, SLOT(onCancel(AudioMsgId)), Qt::QueuedConnection);\n\tconnect(_loader, SIGNAL(needToCheck()), _fader, SLOT(onTimer()));\n\tconnect(_loader, SIGNAL(error(AudioMsgId)), this, SLOT(onError(AudioMsgId)));\n\tconnect(_fader, SIGNAL(needToPreload(AudioMsgId)), _loader, SLOT(onLoad(AudioMsgId)));\n\tconnect(_fader, SIGNAL(playPositionUpdated(AudioMsgId)), this, SIGNAL(updated(AudioMsgId)));\n\tconnect(_fader, SIGNAL(audioStopped(AudioMsgId)), this, SLOT(onStopped(AudioMsgId)));\n\tconnect(_fader, SIGNAL(error(AudioMsgId)), this, SLOT(onError(AudioMsgId)));\n\tconnect(this, SIGNAL(stoppedOnError(AudioMsgId)), this, SIGNAL(updated(AudioMsgId)), Qt::QueuedConnection);\n\tconnect(this, SIGNAL(updated(AudioMsgId)), this, SLOT(onUpdated(AudioMsgId)));\n\n\t_loaderThread.start();\n\t_faderThread.start();\n}\n\n// Thread: Main. Locks: AudioMutex.\nMixer::~Mixer() {\n\t{\n\t\tQMutexLocker lock(&AudioMutex);\n\n\t\tfor (auto i = 0; i != kTogetherLimit; ++i) {\n\t\t\ttrackForType(AudioMsgId::Type::Voice, i)->clear();\n\t\t\ttrackForType(AudioMsgId::Type::Song, i)->clear();\n\t\t}\n\t\t_videoTrack.clear();\n\n\t\tAudio::ClosePlaybackDevice(_instance);\n\t\tAudio::MixerInstance = nullptr;\n\t}\n\n\t_faderThread.quit();\n\t_loaderThread.quit();\n\t_faderThread.wait();\n\t_loaderThread.wait();\n}\n\nvoid Mixer::scheduleFaderCallback() {\n\tInvokeQueued(_fader, [fader = _fader] {\n\t\tfader->onTimer();\n\t});\n}\n\nvoid Mixer::onUpdated(const AudioMsgId &audio) {\n\tif (audio.externalPlayId()) {\n\t\texternalSoundProgress(audio);\n\t}\n\tcrl::on_main([=] {\n\t\t// We've replaced base::Observable with on_main, because\n\t\t// base::Observable::notify is not syncronous by default.\n\t\tUpdatedStream.fire_copy(audio);\n\t});\n}\n\nvoid Mixer::onError(const AudioMsgId &audio) {\n\tstoppedOnError(audio);\n\n\tQMutexLocker lock(&AudioMutex);\n\tauto type = audio.type();\n\tif (type == AudioMsgId::Type::Voice) {\n\t\tif (auto current = trackForType(type)) {\n\t\t\tif (current->state.id == audio) {\n\t\t\t\tunsuppressSong();\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Mixer::onStopped(const AudioMsgId &audio) {\n\tupdated(audio);\n\n\tQMutexLocker lock(&AudioMutex);\n\tauto type = audio.type();\n\tif (type == AudioMsgId::Type::Voice) {\n\t\tif (auto current = trackForType(type)) {\n\t\t\tif (current->state.id == audio) {\n\t\t\t\tunsuppressSong();\n\t\t\t}\n\t\t}\n\t}\n}\n\nMixer::Track *Mixer::trackForType(AudioMsgId::Type type, int index) {\n\tif (index < 0) {\n\t\tif (auto indexPtr = currentIndex(type)) {\n\t\t\tindex = *indexPtr;\n\t\t} else {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\tswitch (type) {\n\tcase AudioMsgId::Type::Voice: return &_audioTracks[index];\n\tcase AudioMsgId::Type::Song: return &_songTracks[index];\n\tcase AudioMsgId::Type::Video: return &_videoTrack;\n\t}\n\treturn nullptr;\n}\n\nconst Mixer::Track *Mixer::trackForType(AudioMsgId::Type type, int index) const {\n\treturn const_cast<Mixer*>(this)->trackForType(type, index);\n}\n\nint *Mixer::currentIndex(AudioMsgId::Type type) {\n\tswitch (type) {\n\tcase AudioMsgId::Type::Voice: return &_audioCurrent;\n\tcase AudioMsgId::Type::Song: return &_songCurrent;\n\tcase AudioMsgId::Type::Video: { static int videoIndex = 0; return &videoIndex; }\n\t}\n\treturn nullptr;\n}\n\nconst int *Mixer::currentIndex(AudioMsgId::Type type) const {\n\treturn const_cast<Mixer*>(this)->currentIndex(type);\n}\n\nvoid Mixer::resetFadeStartPosition(AudioMsgId::Type type, int positionInBuffered) {\n\tauto track = trackForType(type);\n\tif (!track) return;\n\n\tif (positionInBuffered < 0) {\n\t\tAudio::AttachToDevice();\n\t\tif (track->isStreamCreated()) {\n\t\t\tALint alSampleOffset = 0;\n\t\t\tALint alState = AL_INITIAL;\n\t\t\talGetSourcei(track->stream.source, AL_SAMPLE_OFFSET, &alSampleOffset);\n\t\t\talGetSourcei(track->stream.source, AL_SOURCE_STATE, &alState);\n\t\t\tif (Audio::PlaybackErrorHappened()) {\n\t\t\t\tsetStoppedState(track, State::StoppedAtError);\n\t\t\t\tonError(track->state.id);\n\t\t\t\treturn;\n\t\t\t} else if ((alState == AL_STOPPED)\n\t\t\t\t&& (alSampleOffset == 0)\n\t\t\t\t&& !internal::CheckAudioDeviceConnected()) {\n\t\t\t\ttrack->withSpeed.fadeStartPosition = track->withSpeed.position;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst auto stoppedAtEnd = track->state.waitingForData\n\t\t\t\t|| ((alState == AL_STOPPED)\n\t\t\t\t\t&& (!IsStopped(track->state.state)\n\t\t\t\t\t\t|| IsStoppedAtEnd(track->state.state)));\n\t\t\tpositionInBuffered = stoppedAtEnd\n\t\t\t\t? track->withSpeed.bufferedLength\n\t\t\t\t: alSampleOffset;\n\t\t} else {\n\t\t\tpositionInBuffered = 0;\n\t\t}\n\t}\n\tconst auto withSpeedPosition = track->withSpeed.samples[0]\n\t\t? (track->withSpeed.bufferedPosition + positionInBuffered)\n\t\t: track->withSpeed.position;\n\ttrack->withSpeed.fineTunedPosition = withSpeedPosition;\n\ttrack->withSpeed.position = withSpeedPosition;\n\ttrack->withSpeed.fadeStartPosition = withSpeedPosition;\n\ttrack->updateStatePosition();\n}\n\nbool Mixer::fadedStop(AudioMsgId::Type type, bool *fadedStart) {\n\tauto current = trackForType(type);\n\tif (!current) return false;\n\n\tswitch (current->state.state) {\n\tcase State::Starting:\n\tcase State::Resuming:\n\tcase State::Playing: {\n\t\tcurrent->state.state = State::Stopping;\n\t\tresetFadeStartPosition(type);\n\t\tif (fadedStart) *fadedStart = true;\n\t} break;\n\tcase State::Pausing: {\n\t\tcurrent->state.state = State::Stopping;\n\t\tif (fadedStart) *fadedStart = true;\n\t} break;\n\tcase State::Paused:\n\tcase State::PausedAtEnd: {\n\t\tsetStoppedState(current);\n\t} return true;\n\t}\n\treturn false;\n}\n\nvoid Mixer::play(\n\t\tconst AudioMsgId &audio,\n\t\tstd::unique_ptr<ExternalSoundData> externalData,\n\t\tcrl::time positionMs) {\n\tExpects(externalData != nullptr);\n\tExpects(audio.externalPlayId() != 0);\n\n\tsetSongVolume(Core::App().settings().songVolume());\n\tsetVideoVolume(Core::App().settings().videoVolume());\n\n\tauto type = audio.type();\n\tAudioMsgId stopped;\n\t{\n\t\tQMutexLocker lock(&AudioMutex);\n\t\tAudio::AttachToDevice();\n\t\tif (!AudioDevice) return;\n\n\t\tauto fadedStart = false;\n\t\tauto current = trackForType(type);\n\t\tif (!current) return;\n\n\t\tif (current->state.id != audio) {\n\t\t\tif (fadedStop(type, &fadedStart)) {\n\t\t\t\tstopped = current->state.id;\n\t\t\t}\n\t\t\tif (current->state.id) {\n\t\t\t\tloaderOnCancel(current->state.id);\n\t\t\t\tscheduleFaderCallback();\n\t\t\t}\n\t\t\tif (type != AudioMsgId::Type::Video) {\n\t\t\t\tauto foundCurrent = currentIndex(type);\n\t\t\t\tauto index = 0;\n\t\t\t\tfor (; index != kTogetherLimit; ++index) {\n\t\t\t\t\tif (trackForType(type, index)->state.id == audio) {\n\t\t\t\t\t\t*foundCurrent = index;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (index == kTogetherLimit && ++*foundCurrent >= kTogetherLimit) {\n\t\t\t\t\t*foundCurrent -= kTogetherLimit;\n\t\t\t\t}\n\t\t\t\tcurrent = trackForType(type);\n\t\t\t}\n\t\t}\n\n\t\tcurrent->clear(); // Clear all previous state.\n\t\tcurrent->state.id = audio;\n\t\tcurrent->lastUpdateWhen = 0;\n\t\tcurrent->lastUpdatePosition = 0;\n\t\tcurrent->setExternalData(std::move(externalData));\n\t\tcurrent->state.position = (positionMs * current->state.frequency)\n\t\t\t/ 1000LL;\n\t\tcurrent->updateWithSpeedPosition();\n\t\tcurrent->state.state = current->externalData\n\t\t\t? State::Paused\n\t\t\t: fadedStart\n\t\t\t? State::Starting\n\t\t\t: State::Playing;\n\t\tcurrent->loading = true;\n\t\tloaderOnStart(current->state.id, positionMs);\n\t\tif (type == AudioMsgId::Type::Voice) {\n\t\t\tsuppressSong();\n\t\t}\n\t}\n\tif (stopped) {\n\t\tupdated(stopped);\n\t}\n}\n\nvoid Mixer::feedFromExternal(ExternalSoundPart &&part) {\n\t_loader->feedFromExternal(std::move(part));\n}\n\nvoid Mixer::forceToBufferExternal(const AudioMsgId &audioId) {\n\t_loader->forceToBufferExternal(audioId);\n}\n\n// Thread: Main. Locks: AudioMutex.\nvoid Mixer::setSpeedFromExternal(const AudioMsgId &audioId, float64 speed) {\n\tQMutexLocker lock(&AudioMutex);\n\tconst auto track = trackForType(audioId.type());\n\tif (track->state.id == audioId) {\n\t\ttrack->nextSpeed = speed;\n\t\tif (!EqualSpeeds(track->speed, track->nextSpeed)\n\t\t\t&& !IsStoppedOrStopping(track->state.state)) {\n\t\t\ttrack->loading = true;\n\t\t\ttrack->loaded = false;\n\t\t\tInvokeQueued(_loader, [loader = _loader, id = audioId] {\n\t\t\t\tloader->onLoad(id);\n\t\t\t});\n\t\t}\n\t}\n}\n\nStreaming::TimePoint Mixer::getExternalSyncTimePoint(\n\t\tconst AudioMsgId &audio) const {\n\tExpects(audio.externalPlayId() != 0);\n\n\tauto result = Streaming::TimePoint();\n\tconst auto type = audio.type();\n\n\tQMutexLocker lock(&AudioMutex);\n\tconst auto track = trackForType(type);\n\tif (track && track->state.id == audio && track->lastUpdateWhen > 0) {\n\t\tresult.trackTime = track->lastUpdatePosition;\n\t\tresult.worldTime = track->lastUpdateWhen;\n\t}\n\treturn result;\n}\n\ncrl::time Mixer::getExternalCorrectedTime(const AudioMsgId &audio, crl::time frameMs, crl::time systemMs) {\n\tauto result = frameMs;\n\tconst auto type = audio.type();\n\n\tQMutexLocker lock(&AudioMutex);\n\tconst auto track = trackForType(type);\n\tif (track && track->state.id == audio && track->lastUpdateWhen > 0) {\n\t\tresult = static_cast<crl::time>(track->lastUpdatePosition);\n\t\tif (systemMs > track->lastUpdateWhen) {\n\t\t\tresult += (systemMs - track->lastUpdateWhen);\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid Mixer::externalSoundProgress(const AudioMsgId &audio) {\n\tconst auto type = audio.type();\n\n\tQMutexLocker lock(&AudioMutex);\n\tconst auto current = trackForType(type);\n\tif (current && current->state.length && current->state.frequency) {\n\t\tif (current->state.id == audio\n\t\t\t&& current->state.state == State::Playing) {\n\t\t\tcurrent->lastUpdateWhen = crl::now();\n\t\t\tcurrent->lastUpdatePosition = (current->state.position * 1000LL)\n\t\t\t\t/ current->state.frequency;\n\t\t}\n\t}\n}\n\nbool Mixer::checkCurrentALError(AudioMsgId::Type type) {\n\tif (!Audio::PlaybackErrorHappened()) {\n\t\treturn true;\n\t} else if (const auto data = trackForType(type)) {\n\t\tsetStoppedState(data, State::StoppedAtError);\n\t\tonError(data->state.id);\n\t}\n\treturn false;\n}\n\nvoid Mixer::pause(const AudioMsgId &audio, bool fast) {\n\tAudioMsgId current;\n\t{\n\t\tQMutexLocker lock(&AudioMutex);\n\t\tauto type = audio.type();\n\t\tauto track = trackForType(type);\n\t\tif (!track || track->state.id != audio) {\n\t\t\treturn;\n\t\t}\n\n\t\tcurrent = track->state.id;\n\t\tswitch (track->state.state) {\n\t\tcase State::Starting:\n\t\tcase State::Resuming:\n\t\tcase State::Playing: {\n\t\t\ttrack->state.state = fast ? State::Paused : State::Pausing;\n\t\t\tresetFadeStartPosition(type);\n\t\t\tif (type == AudioMsgId::Type::Voice) {\n\t\t\t\tunsuppressSong();\n\t\t\t}\n\t\t} break;\n\n\t\tcase State::Pausing:\n\t\tcase State::Stopping: {\n\t\t\ttrack->state.state = fast ? State::Paused : State::Pausing;\n\t\t} break;\n\t\t}\n\n\t\tif (fast && track->isStreamCreated()) {\n\t\t\tALint state = AL_INITIAL;\n\t\t\talGetSourcei(track->stream.source, AL_SOURCE_STATE, &state);\n\t\t\tif (!checkCurrentALError(type)) return;\n\n\t\t\tif (state == AL_PLAYING) {\n\t\t\t\talSourcePause(track->stream.source);\n\t\t\t\tif (!checkCurrentALError(type)) return;\n\t\t\t}\n\t\t}\n\n\t\tscheduleFaderCallback();\n\n\t\ttrack->lastUpdateWhen = 0;\n\t\ttrack->lastUpdatePosition = 0;\n\t}\n\tif (current) updated(current);\n}\n\nvoid Mixer::resume(const AudioMsgId &audio, bool fast) {\n\tAudioMsgId current;\n\t{\n\t\tQMutexLocker lock(&AudioMutex);\n\t\tauto type = audio.type();\n\t\tauto track = trackForType(type);\n\t\tif (!track || track->state.id != audio) {\n\t\t\treturn;\n\t\t}\n\n\t\tcurrent = track->state.id;\n\t\tswitch (track->state.state) {\n\t\tcase State::Pausing:\n\t\tcase State::Paused:\n\t\tcase State::PausedAtEnd: {\n\t\t\tif (track->state.state == State::Paused) {\n\t\t\t\t// This calls Audio::AttachToDevice().\n\t\t\t\tresetFadeStartPosition(type);\n\t\t\t} else {\n\t\t\t\tAudio::AttachToDevice();\n\t\t\t}\n\t\t\ttrack->state.state = fast ? State::Playing : State::Resuming;\n\n\t\t\tif (track->isStreamCreated()) {\n\t\t\t\t// When starting the video audio is in paused state and\n\t\t\t\t// gets resumed before the stream is created with any data.\n\t\t\t\tALint state = AL_INITIAL;\n\t\t\t\talGetSourcei(track->stream.source, AL_SOURCE_STATE, &state);\n\t\t\t\tif (!checkCurrentALError(type)) return;\n\n\t\t\t\tif (state != AL_PLAYING) {\n\t\t\t\t\tif (state == AL_STOPPED && !internal::CheckAudioDeviceConnected()) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\talSourcef(track->stream.source, AL_GAIN, ComputeVolume(type));\n\t\t\t\t\tif (!checkCurrentALError(type)) return;\n\n\t\t\t\t\tif (state == AL_STOPPED) {\n\t\t\t\t\t\talSourcei(\n\t\t\t\t\t\t\ttrack->stream.source,\n\t\t\t\t\t\t\tAL_SAMPLE_OFFSET,\n\t\t\t\t\t\t\tqMax(track->withSpeed.position - track->withSpeed.bufferedPosition, 0LL));\n\t\t\t\t\t\tif (!checkCurrentALError(type)) return;\n\t\t\t\t\t}\n\t\t\t\t\talSourcePlay(track->stream.source);\n\t\t\t\t\tif (!checkCurrentALError(type)) return;\n\t\t\t\t}\n\t\t\t\tif (type == AudioMsgId::Type::Voice) {\n\t\t\t\t\tsuppressSong();\n\t\t\t\t}\n\t\t\t}\n\t\t} break;\n\t\t}\n\t\tscheduleFaderCallback();\n\t}\n\tif (current) updated(current);\n}\n\nvoid Mixer::stop(const AudioMsgId &audio) {\n\tAudioMsgId current;\n\t{\n\t\tQMutexLocker lock(&AudioMutex);\n\t\tauto type = audio.type();\n\t\tauto track = trackForType(type);\n\t\tif (!track || track->state.id != audio) {\n\t\t\treturn;\n\t\t}\n\n\t\tcurrent = audio;\n\t\tfadedStop(type);\n\t\tif (type == AudioMsgId::Type::Voice) {\n\t\t\tunsuppressSong();\n\t\t} else if (type == AudioMsgId::Type::Video) {\n\t\t\ttrack->clear();\n\t\t\tloaderOnCancel(audio);\n\t\t}\n\t}\n\tif (current) updated(current);\n}\n\nvoid Mixer::stop(const AudioMsgId &audio, State state) {\n\tExpects(IsStopped(state));\n\n\tAudioMsgId current;\n\t{\n\t\tQMutexLocker lock(&AudioMutex);\n\t\tauto type = audio.type();\n\t\tauto track = trackForType(type);\n\t\tif (!track\n\t\t\t|| track->state.id != audio\n\t\t\t|| IsStopped(track->state.state)) {\n\t\t\treturn;\n\t\t}\n\n\t\tcurrent = audio;\n\t\tsetStoppedState(track, state);\n\t\tif (type == AudioMsgId::Type::Voice) {\n\t\t\tunsuppressSong();\n\t\t} else if (type == AudioMsgId::Type::Video) {\n\t\t\ttrack->clear();\n\t\t}\n\t}\n\tif (current) updated(current);\n}\n\nvoid Mixer::stopAndClear() {\n\tTrack *current_audio = nullptr, *current_song = nullptr;\n\t{\n\t\tQMutexLocker lock(&AudioMutex);\n\t\tif ((current_audio = trackForType(AudioMsgId::Type::Voice))) {\n\t\t\tsetStoppedState(current_audio);\n\t\t}\n\t\tif ((current_song = trackForType(AudioMsgId::Type::Song))) {\n\t\t\tsetStoppedState(current_song);\n\t\t}\n\t}\n\tif (current_song) {\n\t\tupdated(current_song->state.id);\n\t}\n\tif (current_audio) {\n\t\tupdated(current_audio->state.id);\n\t}\n\t{\n\t\tQMutexLocker lock(&AudioMutex);\n\t\tauto clearAndCancel = [this](AudioMsgId::Type type, int index) {\n\t\t\tauto track = trackForType(type, index);\n\t\t\tif (track->state.id) {\n\t\t\t\tloaderOnCancel(track->state.id);\n\t\t\t}\n\t\t\ttrack->clear();\n\t\t};\n\t\tfor (auto index = 0; index != kTogetherLimit; ++index) {\n\t\t\tclearAndCancel(AudioMsgId::Type::Voice, index);\n\t\t\tclearAndCancel(AudioMsgId::Type::Song, index);\n\t\t}\n\t\t_videoTrack.clear();\n\t}\n}\n\nTrackState Mixer::currentState(AudioMsgId::Type type) {\n\tQMutexLocker lock(&AudioMutex);\n\tauto current = trackForType(type);\n\tif (!current) {\n\t\treturn TrackState();\n\t}\n\treturn current->state;\n}\n\nvoid Mixer::setStoppedState(Track *current, State state) {\n\tcurrent->state.state = state;\n\tcurrent->state.position = 0;\n\tcurrent->withSpeed.position = 0;\n\tcurrent->withSpeed.fineTunedPosition = 0;\n\tif (current->isStreamCreated()) {\n\t\talSourceStop(current->stream.source);\n\t\talSourcef(current->stream.source, AL_GAIN, 1);\n\t}\n\tif (current->state.id) {\n\t\tloaderOnCancel(current->state.id);\n\t}\n}\n\n// Thread: Main. Must be locked: AudioMutex.\nvoid Mixer::prepareToCloseDevice() {\n\tfor (auto i = 0; i != kTogetherLimit; ++i) {\n\t\ttrackForType(AudioMsgId::Type::Voice, i)->detach();\n\t\ttrackForType(AudioMsgId::Type::Song, i)->detach();\n\t}\n\t_videoTrack.detach();\n}\n\n// Thread: Main. Must be locked: AudioMutex.\nvoid Mixer::reattachIfNeeded() {\n\tAudio::Current().stopDetachIfNotUsed();\n\n\tauto reattachNeeded = [this] {\n\t\tauto isPlayingState = [](const Track &track) {\n\t\t\tauto state = track.state.state;\n\t\t\treturn (state == State::Playing) || IsFading(state);\n\t\t};\n\t\tfor (auto i = 0; i != kTogetherLimit; ++i) {\n\t\t\tif (isPlayingState(*trackForType(AudioMsgId::Type::Voice, i))\n\t\t\t\t|| isPlayingState(*trackForType(AudioMsgId::Type::Song, i))) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn isPlayingState(_videoTrack);\n\t};\n\n\tif (reattachNeeded() || Audio::Current().hasActiveTracks()) {\n\t\tAudio::AttachToDevice();\n\t}\n}\n\n// Thread: Any. Must be locked: AudioMutex.\nvoid Mixer::reattachTracks() {\n\tfor (auto i = 0; i != kTogetherLimit; ++i) {\n\t\ttrackForType(AudioMsgId::Type::Voice, i)->reattach(AudioMsgId::Type::Voice);\n\t\ttrackForType(AudioMsgId::Type::Song, i)->reattach(AudioMsgId::Type::Song);\n\t}\n\t_videoTrack.reattach(AudioMsgId::Type::Video);\n}\n\nvoid Mixer::setSongVolume(float64 volume) {\n\t_volumeSong.storeRelease(qRound(volume * kVolumeRound));\n}\n\nfloat64 Mixer::getSongVolume() const {\n\treturn float64(_volumeSong.loadAcquire()) / kVolumeRound;\n}\n\nvoid Mixer::setVideoVolume(float64 volume) {\n\t_volumeVideo.storeRelease(qRound(volume * kVolumeRound));\n}\n\nfloat64 Mixer::getVideoVolume() const {\n\treturn float64(_volumeVideo.loadAcquire()) / kVolumeRound;\n}\n\nFader::Fader(QThread *thread) : QObject()\n, _timer(this)\n, _suppressVolumeAll(1., 1.)\n, _suppressVolumeSong(1., 1.) {\n\tmoveToThread(thread);\n\t_timer.moveToThread(thread);\n\tconnect(thread, SIGNAL(started()), this, SLOT(onInit()));\n\tconnect(thread, SIGNAL(finished()), this, SLOT(deleteLater()));\n\n\t_timer.setSingleShot(true);\n\tconnect(&_timer, SIGNAL(timeout()), this, SLOT(onTimer()));\n}\n\nvoid Fader::onInit() {\n}\n\nvoid Fader::onTimer() {\n\tQMutexLocker lock(&AudioMutex);\n\tif (!mixer()) return;\n\n\tconstexpr auto kMediaPlayerSuppressDuration = crl::time(150);\n\tauto fadeDuration = getFadeDuration();\n\n\tauto volumeChangedAll = false;\n\tauto volumeChangedSong = false;\n\tif (_suppressAll || _suppressSongAnim) {\n\t\tauto ms = crl::now();\n\t\tif (_suppressAll) {\n\t\t\tif (ms >= _suppressAllEnd || ms < _suppressAllStart) {\n\t\t\t\t_suppressAll = _suppressAllAnim = false;\n\t\t\t\t_suppressVolumeAll = anim::value(1., 1.);\n\t\t\t} else if (ms > _suppressAllEnd - fadeDuration) {\n\t\t\t\tif (_suppressVolumeAll.to() != 1.) _suppressVolumeAll.start(1.);\n\t\t\t\t_suppressVolumeAll.update(1. - ((_suppressAllEnd - ms) / float64(fadeDuration)), anim::linear);\n\t\t\t} else if (ms >= _suppressAllStart + kMediaPlayerSuppressDuration) {\n\t\t\t\tif (_suppressAllAnim) {\n\t\t\t\t\t_suppressVolumeAll.finish();\n\t\t\t\t\t_suppressAllAnim = false;\n\t\t\t\t}\n\t\t\t} else if (ms > _suppressAllStart) {\n\t\t\t\t_suppressVolumeAll.update((ms - _suppressAllStart) / float64(kMediaPlayerSuppressDuration), anim::linear);\n\t\t\t}\n\t\t\tauto wasVolumeMultiplierAll = VolumeMultiplierAll;\n\t\t\tVolumeMultiplierAll = _suppressVolumeAll.current();\n\t\t\tvolumeChangedAll = (VolumeMultiplierAll != wasVolumeMultiplierAll);\n\t\t}\n\t\tif (_suppressSongAnim) {\n\t\t\tif (ms >= _suppressSongStart + fadeDuration) {\n\t\t\t\t_suppressVolumeSong.finish();\n\t\t\t\t_suppressSongAnim = false;\n\t\t\t} else {\n\t\t\t\t_suppressVolumeSong.update((ms - _suppressSongStart) / float64(fadeDuration), anim::linear);\n\t\t\t}\n\t\t}\n\t\tauto wasVolumeMultiplierSong = VolumeMultiplierSong;\n\t\tVolumeMultiplierSong = _suppressVolumeSong.current();\n\t\taccumulate_min(VolumeMultiplierSong, VolumeMultiplierAll);\n\t\tvolumeChangedSong = (VolumeMultiplierSong != wasVolumeMultiplierSong);\n\t}\n\tauto hasFading = (_suppressAll || _suppressSongAnim);\n\tauto hasPlaying = false;\n\n\tauto updatePlayback = [this, &hasPlaying, &hasFading](AudioMsgId::Type type, int index, float64 volumeMultiplier, bool suppressGainChanged) {\n\t\tauto track = mixer()->trackForType(type, index);\n\t\tif (IsStopped(track->state.state) || track->state.state == State::Paused || !track->isStreamCreated()) return;\n\n\t\tauto emitSignals = updateOnePlayback(track, hasPlaying, hasFading, volumeMultiplier, suppressGainChanged);\n\t\tif (emitSignals & EmitError) error(track->state.id);\n\t\tif (emitSignals & EmitStopped) audioStopped(track->state.id);\n\t\tif (emitSignals & EmitPositionUpdated) playPositionUpdated(track->state.id);\n\t\tif (emitSignals & EmitNeedToPreload) needToPreload(track->state.id);\n\t};\n\tauto suppressGainForMusic = ComputeVolume(AudioMsgId::Type::Song);\n\tauto suppressGainForMusicChanged = volumeChangedSong || _volumeChangedSong;\n\tauto suppressGainForVoice = ComputeVolume(AudioMsgId::Type::Voice);\n\tfor (auto i = 0; i != kTogetherLimit; ++i) {\n\t\tupdatePlayback(AudioMsgId::Type::Voice, i, suppressGainForVoice, suppressGainForMusicChanged);\n\t\tupdatePlayback(AudioMsgId::Type::Song, i, suppressGainForMusic, suppressGainForMusicChanged);\n\t}\n\tauto suppressGainForVideo = ComputeVolume(AudioMsgId::Type::Video);\n\tauto suppressGainForVideoChanged = volumeChangedAll || _volumeChangedVideo;\n\tupdatePlayback(AudioMsgId::Type::Video, 0, suppressGainForVideo, suppressGainForVideoChanged);\n\n\t_volumeChangedSong = _volumeChangedVideo = false;\n\n\tif (hasFading) {\n\t\t_timer.start(kCheckFadingTimeout);\n\t\tAudio::StopDetachIfNotUsedSafe();\n\t} else if (hasPlaying) {\n\t\t_timer.start(kCheckPlaybackPositionTimeout);\n\t\tAudio::StopDetachIfNotUsedSafe();\n\t} else {\n\t\tAudio::ScheduleDetachIfNotUsedSafe();\n\t}\n}\n\nint32 Fader::updateOnePlayback(Mixer::Track *track, bool &hasPlaying, bool &hasFading, float64 volumeMultiplier, bool volumeChanged) {\n\tconst auto errorHappened = [&] {\n\t\tif (Audio::PlaybackErrorHappened()) {\n\t\t\tsetStoppedState(track, State::StoppedAtError);\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\n\tALint alSampleOffset = 0;\n\tALint alState = AL_INITIAL;\n\talGetSourcei(track->stream.source, AL_SAMPLE_OFFSET, &alSampleOffset);\n\talGetSourcei(track->stream.source, AL_SOURCE_STATE, &alState);\n\tif (errorHappened()) {\n\t\treturn EmitError;\n\t} else if ((alState == AL_STOPPED)\n\t\t&& (alSampleOffset == 0)\n\t\t&& !internal::CheckAudioDeviceConnected()) {\n\t\treturn 0;\n\t}\n\n\tauto fadeDuration = getFadeDuration();\n\n\tint32 emitSignals = 0;\n\tconst auto stoppedAtEnd = track->state.waitingForData\n\t\t|| ((alState == AL_STOPPED)\n\t\t\t&& (!IsStopped(track->state.state)\n\t\t\t\t|| IsStoppedAtEnd(track->state.state)));\n\tconst auto positionInBuffered = stoppedAtEnd\n\t\t? track->withSpeed.bufferedLength\n\t\t: alSampleOffset;\n\tconst auto waitingForDataOld = track->state.waitingForData;\n\ttrack->state.waitingForData = stoppedAtEnd\n\t\t&& (track->state.state != State::Stopping);\n\tconst auto withSpeedPosition = track->withSpeed.bufferedPosition\n\t\t+ positionInBuffered;\n\n\tauto playing = (track->state.state == State::Playing);\n\tauto fading = IsFading(track->state.state);\n\tif (alState != AL_PLAYING && !track->loading) {\n\t\tif (fading || playing) {\n\t\t\tfading = false;\n\t\t\tplaying = false;\n\t\t\tif (track->state.state == State::Pausing) {\n\t\t\t\tsetStoppedState(track, State::PausedAtEnd);\n\t\t\t} else if (track->state.state == State::Stopping) {\n\t\t\t\tsetStoppedState(track, State::Stopped);\n\t\t\t} else {\n\t\t\t\tsetStoppedState(track, State::StoppedAtEnd);\n\t\t\t}\n\t\t\tif (errorHappened()) return EmitError;\n\t\t\temitSignals |= EmitStopped;\n\t\t}\n\t} else if (fading && alState == AL_PLAYING) {\n\t\tconst auto fadingForSamplesCount = withSpeedPosition\n\t\t\t- track->withSpeed.fadeStartPosition;\n\t\tif (crl::time(1000) * fadingForSamplesCount >= fadeDuration * track->state.frequency) {\n\t\t\tfading = false;\n\t\t\talSourcef(track->stream.source, AL_GAIN, 1. * volumeMultiplier);\n\t\t\tif (errorHappened()) return EmitError;\n\n\t\t\tswitch (track->state.state) {\n\t\t\tcase State::Stopping: {\n\t\t\t\tsetStoppedState(track);\n\t\t\t\talState = AL_STOPPED;\n\t\t\t} break;\n\t\t\tcase State::Pausing: {\n\t\t\t\talSourcePause(track->stream.source);\n\t\t\t\tif (errorHappened()) return EmitError;\n\n\t\t\t\ttrack->state.state = State::Paused;\n\t\t\t} break;\n\t\t\tcase State::Starting:\n\t\t\tcase State::Resuming: {\n\t\t\t\ttrack->state.state = State::Playing;\n\t\t\t\tplaying = true;\n\t\t\t} break;\n\t\t\t}\n\t\t} else {\n\t\t\tauto newGain = crl::time(1000) * fadingForSamplesCount / float64(fadeDuration * track->state.frequency);\n\t\t\tif (track->state.state == State::Pausing || track->state.state == State::Stopping) {\n\t\t\t\tnewGain = 1. - newGain;\n\t\t\t}\n\t\t\talSourcef(track->stream.source, AL_GAIN, newGain * volumeMultiplier);\n\t\t\tif (errorHappened()) return EmitError;\n\t\t}\n\t} else if (playing && alState == AL_PLAYING) {\n\t\tif (volumeChanged) {\n\t\t\talSourcef(track->stream.source, AL_GAIN, 1. * volumeMultiplier);\n\t\t\tif (errorHappened()) return EmitError;\n\t\t}\n\t}\n\ttrack->withSpeed.fineTunedPosition = withSpeedPosition;\n\tif (alState == AL_PLAYING && withSpeedPosition >= track->withSpeed.position + kCheckPlaybackPositionDelta) {\n\t\ttrack->withSpeed.position = withSpeedPosition;\n\t\ttrack->updateStatePosition();\n\t\temitSignals |= EmitPositionUpdated;\n\t} else if (track->state.waitingForData && !waitingForDataOld) {\n\t\tif (withSpeedPosition > track->withSpeed.position) {\n\t\t\ttrack->withSpeed.position = withSpeedPosition;\n\t\t}\n\t\t// When stopped because of insufficient data while streaming,\n\t\t// inform the player about the last position we were at.\n\t\temitSignals |= EmitPositionUpdated;\n\t}\n\tif (playing || track->state.state == State::Starting || track->state.state == State::Resuming) {\n\t\tif ((!track->loaded && !track->loading) || track->waitingForBuffer) {\n\t\t\tauto needPreload = (track->withSpeed.position + kPreloadSeconds * track->state.frequency > track->withSpeed.bufferedPosition + track->withSpeed.bufferedLength);\n\t\t\tif (needPreload) {\n\t\t\t\ttrack->loading = true;\n\t\t\t\temitSignals |= EmitNeedToPreload;\n\t\t\t}\n\t\t}\n\t}\n\tif (playing) hasPlaying = true;\n\tif (fading) hasFading = true;\n\n\treturn emitSignals;\n}\n\nvoid Fader::setStoppedState(Mixer::Track *track, State state) {\n\tmixer()->setStoppedState(track, state);\n}\n\nvoid Fader::onSuppressSong() {\n\tif (!_suppressSong) {\n\t\t_suppressSong = true;\n\t\t_suppressSongAnim = true;\n\t\t_suppressSongStart = crl::now();\n\t\t_suppressVolumeSong.start(kSuppressRatioSong);\n\t\tonTimer();\n\t}\n}\n\nvoid Fader::onUnsuppressSong() {\n\tif (_suppressSong) {\n\t\t_suppressSong = false;\n\t\t_suppressSongAnim = true;\n\t\t_suppressSongStart = crl::now();\n\t\t_suppressVolumeSong.start(1.);\n\t\tonTimer();\n\t}\n}\n\nvoid Fader::onSuppressAll(qint64 duration) {\n\t_suppressAll = true;\n\tauto now = crl::now();\n\n\tauto fadeDuration = getFadeDuration();\n\n\tif (_suppressAllEnd < now + fadeDuration) {\n\t\t_suppressAllStart = now;\n\t}\n\t_suppressAllEnd = now + duration;\n\t_suppressVolumeAll.start(kSuppressRatioAll);\n\tonTimer();\n}\n\nvoid Fader::songVolumeChanged() {\n\t_volumeChangedSong = true;\n\tonTimer();\n}\n\nvoid Fader::videoVolumeChanged() {\n\t_volumeChangedVideo = true;\n\tonTimer();\n}\n\ncrl::time Fader::getFadeDuration() {\n\tif (!Core::App().settings().fork().audioFade()) {\n\t\treturn crl::time(0);\n\t}\n\treturn kFadeDuration;\n}\n\nnamespace internal {\n\n// Thread: Any.\nQMutex *audioPlayerMutex() {\n\treturn &AudioMutex;\n}\n\n// Thread: Any.\nbool audioCheckError() {\n\treturn !Audio::PlaybackErrorHappened();\n}\n\n// Thread: Any. Must be locked: AudioMutex.\nbool audioDeviceIsConnected() {\n\tif (!AudioDevice) {\n\t\treturn false;\n\t}\n\t// always connected in the basic OpenAL, disconnect status is an extension\n\tauto isConnected = ALint(1);\n\tif (alcIsExtensionPresent(nullptr, \"ALC_EXT_disconnect\")) {\n\t\talcGetIntegerv(AudioDevice, alcGetEnumValue(nullptr, \"ALC_CONNECTED\"), 1, &isConnected);\n\t}\n\tif (Audio::ContextErrorHappened()) {\n\t\treturn false;\n\t}\n\treturn (isConnected != 0);\n}\n\n// Thread: Any. Must be locked: AudioMutex.\nbool CheckAudioDeviceConnected() {\n\tif (audioDeviceIsConnected()) {\n\t\treturn true;\n\t}\n\tAudio::ScheduleDetachFromDeviceSafe();\n\treturn false;\n}\n\n// Thread: Main. Locks: AudioMutex.\nvoid DetachFromDevice(not_null<Audio::Instance*> instance) {\n\tQMutexLocker lock(&AudioMutex);\n\tAudio::ClosePlaybackDevice(instance);\n\tif (mixer()) {\n\t\tmixer()->reattachIfNeeded();\n\t}\n}\n\nbool DetachIfDeviceChanged(\n\t\tnot_null<Audio::Instance*> instance,\n\t\tconst Webrtc::DeviceResolvedId &nowDeviceId) {\n\tQMutexLocker lock(&AudioMutex);\n\tif (AudioDeviceLastUsedId == nowDeviceId) {\n\t\treturn false;\n\t}\n\tAudio::ClosePlaybackDevice(instance);\n\tif (mixer()) {\n\t\tmixer()->reattachIfNeeded();\n\t}\n\treturn true;\n}\n\n} // namespace internal\n\n} // namespace Player\n\nclass FFMpegAttributesReader : public AbstractFFMpegLoader {\npublic:\n\tFFMpegAttributesReader(const Core::FileLocation &file, const QByteArray &data)\n\t: AbstractFFMpegLoader(file, data, bytes::vector()) {\n\t}\n\n\tbool open(crl::time positionMs, float64 speed = 1.) override {\n\t\tif (!AbstractFFMpegLoader::open(positionMs, speed)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int32 i = 0, l = fmtContext->nb_streams; i < l; ++i) {\n\t\t\tconst auto stream = fmtContext->streams[i];\n\t\t\tif (stream->disposition & AV_DISPOSITION_ATTACHED_PIC) {\n\t\t\t\tif (!_cover.isNull()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto &packet = stream->attached_pic;\n\t\t\t\tif (packet.size) {\n\t\t\t\t\tconst auto coverBytes = QByteArray(\n\t\t\t\t\t\t(const char*)packet.data,\n\t\t\t\t\t\tpacket.size);\n\t\t\t\t\tauto read = Images::Read({\n\t\t\t\t\t\t.content = coverBytes,\n\t\t\t\t\t\t.forceOpaque = true,\n\t\t\t\t\t});\n\t\t\t\t\tif (!read.image.isNull()) {\n\t\t\t\t\t\t_cover = std::move(read.image);\n\t\t\t\t\t\t_coverBytes = coverBytes;\n\t\t\t\t\t\t_coverFormat = read.format;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {\n\t\t\t\tDEBUG_LOG((\"Audio Read Error: Found video stream in file '%1', data size '%2', stream %3.\")\n\t\t\t\t\t.arg(_file.name())\n\t\t\t\t\t.arg(_data.size())\n\t\t\t\t\t.arg(i));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\textractMetaData(fmtContext->streams[streamId]->metadata);\n\t\textractMetaData(fmtContext->metadata);\n\n\t\treturn true;\n\t}\n\n\tvoid trySet(QString &to, AVDictionary *dict, const char *key) {\n\t\tif (!to.isEmpty()) return;\n\t\tif (AVDictionaryEntry* tag = av_dict_get(dict, key, nullptr, 0)) {\n\t\t\tto = QString::fromUtf8(tag->value);\n\t\t}\n\t}\n\tvoid extractMetaData(AVDictionary *dict) {\n\t\ttrySet(_title, dict, \"title\");\n\t\ttrySet(_performer, dict, \"artist\");\n\t\ttrySet(_performer, dict, \"performer\");\n\t\ttrySet(_performer, dict, \"album_artist\");\n\t\t//for (AVDictionaryEntry *tag = av_dict_get(dict, \"\", 0, AV_DICT_IGNORE_SUFFIX); tag; tag = av_dict_get(dict, \"\", tag, AV_DICT_IGNORE_SUFFIX)) {\n\t\t//\tconst char *key = tag->key;\n\t\t//\tconst char *value = tag->value;\n\t\t//\tQString tmp = QString::fromUtf8(value);\n\t\t//}\n\t}\n\n\tint sampleSize() override {\n\t\tUnexpected(\"We shouldn't try to read sample size here.\");\n\t}\n\n\tint format() override {\n\t\treturn 0;\n\t}\n\n\tQString title() {\n\t\treturn _title;\n\t}\n\n\tQString performer() {\n\t\treturn _performer;\n\t}\n\n\tQImage cover() {\n\t\treturn _cover;\n\t}\n\n\tQByteArray coverBytes() {\n\t\treturn _coverBytes;\n\t}\n\n\tQByteArray coverFormat() {\n\t\treturn _coverFormat;\n\t}\n\n\tReadResult readMore() override {\n\t\tDEBUG_LOG((\"Audio Read Error: should not call this\"));\n\t\treturn ReadError::Other;\n\t}\n\n\t~FFMpegAttributesReader() {\n\t}\n\nprivate:\n\n\tQString _title, _performer;\n\tQImage _cover;\n\tQByteArray _coverBytes, _coverFormat;\n\n};\n\nnamespace Player {\n\nUi::PreparedFileInformation PrepareForSending(\n\t\tconst QString &fname,\n\t\tconst QByteArray &data) {\n\tauto result = Ui::PreparedFileInformation::Song();\n\tFFMpegAttributesReader reader(Core::FileLocation(fname), data);\n\tconst auto positionMs = crl::time(0);\n\tif (reader.open(positionMs) && reader.duration() > 0) {\n\t\tresult.duration = reader.duration();\n\t\tresult.title = reader.title();\n\t\tresult.performer = reader.performer();\n\t\tresult.cover = reader.cover();\n\t}\n\treturn { .media = result };\n}\n\n} // namespace Player\n\nclass FFMpegWaveformCounter : public FFMpegLoader {\npublic:\n\tFFMpegWaveformCounter(const Core::FileLocation &file, const QByteArray &data) : FFMpegLoader(file, data, bytes::vector()) {\n\t}\n\n\tbool open(crl::time positionMs, float64 speed = 1.) override {\n\t\tif (!FFMpegLoader::open(positionMs, speed)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tQByteArray buffer;\n\t\tbuffer.reserve(kWaveformCounterBufferSize);\n\t\tconst auto samplesCount = samplesFrequency() * duration() / 1000;\n\t\tint64 countbytes = sampleSize() * samplesCount;\n\t\tint64 processed = 0;\n\t\tint64 sumbytes = 0;\n\t\tif (samplesCount < Media::Player::kWaveformSamplesCount) {\n\t\t\treturn false;\n\t\t}\n\n\t\tQVector<uint16> peaks;\n\t\tpeaks.reserve(Media::Player::kWaveformSamplesCount);\n\n\t\tauto fmt = format();\n\t\tauto peak = uint16(0);\n\t\tauto callback = [&](uint16 sample) {\n\t\t\taccumulate_max(peak, sample);\n\t\t\tsumbytes += Media::Player::kWaveformSamplesCount;\n\t\t\tif (sumbytes >= countbytes) {\n\t\t\t\tsumbytes -= countbytes;\n\t\t\t\tpeaks.push_back(peak);\n\t\t\t\tpeak = 0;\n\t\t\t}\n\t\t};\n\t\twhile (processed < countbytes) {\n\t\t\tconst auto result = readMore();\n\t\t\tAssert(result != ReadError::Wait); // Not a child loader.\n\t\t\tif (result == ReadError::Retry) {\n\t\t\t\tcontinue;\n\t\t\t} else if (result == ReadError::Other\n\t\t\t\t|| result == ReadError::EndOfFile) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tAssert(v::is<bytes::const_span>(result));\n\t\t\tconst auto sampleBytes = v::get<bytes::const_span>(result);\n\t\t\tAssert(!sampleBytes.empty());\n\t\t\tif (fmt == AL_FORMAT_MONO8 || fmt == AL_FORMAT_STEREO8) {\n\t\t\t\tMedia::Audio::IterateSamples<uchar>(sampleBytes, callback);\n\t\t\t} else if (fmt == AL_FORMAT_MONO16 || fmt == AL_FORMAT_STEREO16) {\n\t\t\t\tMedia::Audio::IterateSamples<int16>(sampleBytes, callback);\n\t\t\t}\n\t\t\tprocessed += sampleBytes.size();\n\t\t}\n\t\tif (sumbytes > 0 && peaks.size() < Media::Player::kWaveformSamplesCount) {\n\t\t\tpeaks.push_back(peak);\n\t\t}\n\n\t\tif (peaks.isEmpty()) {\n\t\t\treturn false;\n\t\t}\n\n\t\tauto sum = std::accumulate(peaks.cbegin(), peaks.cend(), 0LL);\n\t\tpeak = qMax(int32(sum * 1.8 / peaks.size()), 2500);\n\n\t\tresult.resize(peaks.size());\n\t\tfor (int32 i = 0, l = peaks.size(); i != l; ++i) {\n\t\t\tresult[i] = char(qMin(31U, uint32(qMin(peaks.at(i), peak)) * 31 / peak));\n\t\t}\n\n\t\treturn true;\n\t}\n\n\tconst VoiceWaveform &waveform() const {\n\t\treturn result;\n\t}\n\n\t~FFMpegWaveformCounter() {\n\t}\n\nprivate:\n\tVoiceWaveform result;\n\n};\n\n} // namespace Media\n\nVoiceWaveform audioCountWaveform(\n\t\tconst Core::FileLocation &file,\n\t\tconst QByteArray &data) {\n\tMedia::FFMpegWaveformCounter counter(file, data);\n\tconst auto positionMs = crl::time(0);\n\tif (counter.open(positionMs)) {\n\t\treturn counter.waveform();\n\t}\n\treturn VoiceWaveform();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animation_value.h\"\n#include \"core/file_location.h\"\n#include \"data/data_audio_msg_id.h\"\n#include \"base/bytes.h\"\n#include \"base/timer.h\"\n\n#include <QtCore/QTimer>\n\nnamespace Ui {\nstruct PreparedFileInformation;\n} // namespace Ui\n\nnamespace Media {\nstruct ExternalSoundData;\nstruct ExternalSoundPart;\n} // namespace Media\n\nnamespace Media {\nnamespace Streaming {\nstruct TimePoint;\n} // namespace Streaming\n} // namespace Media\n\nnamespace Webrtc {\nstruct DeviceResolvedId;\n} // namespace Webrtc\n\nnamespace Media {\nnamespace Audio {\n\nclass Instance;\n\n// Thread: Main.\nvoid Start(not_null<Instance*> instance);\nvoid Finish(not_null<Instance*> instance);\n\n// Thread: Main. Locks: AudioMutex.\nbool IsAttachedToDevice();\n\n// Thread: Any. Must be locked: AudioMutex.\nbool AttachToDevice();\n\n// Thread: Any.\nvoid ScheduleDetachFromDeviceSafe();\nvoid ScheduleDetachIfNotUsedSafe();\nvoid StopDetachIfNotUsedSafe();\nbool SupportsSpeedControl();\n\n} // namespace Audio\n\nnamespace Player {\n\nconstexpr auto kDefaultFrequency = 48000; // 48 kHz\nconstexpr auto kTogetherLimit = 4;\nconstexpr auto kWaveformSamplesCount = 100;\n\nclass Fader;\nclass Loaders;\n\n[[nodiscard]] rpl::producer<AudioMsgId> Updated();\n\nfloat64 ComputeVolume(AudioMsgId::Type type);\n\nenum class State {\n\tStopped = 0x01,\n\tStoppedAtEnd = 0x02,\n\tStoppedAtError = 0x03,\n\tStoppedAtStart = 0x04,\n\n\tStarting = 0x08,\n\tPlaying = 0x10,\n\tStopping = 0x18,\n\tPausing = 0x20,\n\tPaused = 0x28,\n\tPausedAtEnd = 0x30,\n\tResuming = 0x38,\n};\n\ninline bool IsStopped(State state) {\n\treturn (state == State::Stopped)\n\t\t|| (state == State::StoppedAtEnd)\n\t\t|| (state == State::StoppedAtError)\n\t\t|| (state == State::StoppedAtStart);\n}\n\ninline bool IsStoppedOrStopping(State state) {\n\treturn IsStopped(state) || (state == State::Stopping);\n}\n\ninline bool IsStoppedAtEnd(State state) {\n\treturn (state == State::StoppedAtEnd);\n}\n\ninline bool IsPaused(State state) {\n\treturn (state == State::Paused)\n\t\t|| (state == State::PausedAtEnd);\n}\n\ninline bool IsPausedOrPausing(State state) {\n\treturn IsPaused(state) || (state == State::Pausing);\n}\n\ninline bool IsFading(State state) {\n\treturn (state == State::Starting)\n\t\t|| (state == State::Stopping)\n\t\t|| (state == State::Pausing)\n\t\t|| (state == State::Resuming);\n}\n\ninline bool IsActive(State state) {\n\treturn !IsStopped(state) && !IsPaused(state);\n}\n\ninline bool ShowPauseIcon(State state) {\n\treturn !IsStoppedOrStopping(state)\n\t\t&& !IsPausedOrPausing(state);\n}\n\nstruct TrackState {\n\tAudioMsgId id;\n\tState state = State::Stopped;\n\tint64 position = 0;\n\tint64 receivedTill = 0;\n\tint64 length = 0;\n\tint frequency = kDefaultFrequency;\n\tint fileHeaderSize = 0;\n\tbool waitingForData = false;\n};\n\nclass Mixer final : public QObject {\n\tQ_OBJECT\n\npublic:\n\texplicit Mixer(not_null<Audio::Instance*> instance);\n\n\tvoid play(\n\t\tconst AudioMsgId &audio,\n\t\tstd::unique_ptr<ExternalSoundData> externalData,\n\t\tcrl::time positionMs);\n\tvoid pause(const AudioMsgId &audio, bool fast = false);\n\tvoid resume(const AudioMsgId &audio, bool fast = false);\n\tvoid stop(const AudioMsgId &audio);\n\tvoid stop(const AudioMsgId &audio, State state);\n\n\t// External player audio stream interface.\n\tvoid feedFromExternal(ExternalSoundPart &&part);\n\tvoid forceToBufferExternal(const AudioMsgId &audioId);\n\n\t// Thread: Main. Locks: AudioMutex.\n\tvoid setSpeedFromExternal(const AudioMsgId &audioId, float64 speed);\n\n\tStreaming::TimePoint getExternalSyncTimePoint(\n\t\tconst AudioMsgId &audio) const;\n\tcrl::time getExternalCorrectedTime(\n\t\tconst AudioMsgId &id,\n\t\tcrl::time frameMs,\n\t\tcrl::time systemMs);\n\n\tvoid stopAndClear();\n\n\tTrackState currentState(AudioMsgId::Type type);\n\n\t// Thread: Main. Must be locked: AudioMutex.\n\tvoid prepareToCloseDevice();\n\n\t// Thread: Main. Must be locked: AudioMutex.\n\tvoid reattachIfNeeded();\n\n\t// Thread: Any. Must be locked: AudioMutex.\n\tvoid reattachTracks();\n\n\t// Thread: Any.\n\tvoid setSongVolume(float64 volume);\n\tfloat64 getSongVolume() const;\n\tvoid setVideoVolume(float64 volume);\n\tfloat64 getVideoVolume() const;\n\n\tvoid scheduleFaderCallback();\n\n\t~Mixer();\n\nprivate Q_SLOTS:\n\tvoid onError(const AudioMsgId &audio);\n\tvoid onStopped(const AudioMsgId &audio);\n\n\tvoid onUpdated(const AudioMsgId &audio);\n\nQ_SIGNALS:\n\tvoid updated(const AudioMsgId &audio);\n\tvoid stoppedOnError(const AudioMsgId &audio);\n\tvoid loaderOnStart(const AudioMsgId &audio, qint64 positionMs);\n\tvoid loaderOnCancel(const AudioMsgId &audio);\n\n\tvoid suppressSong();\n\tvoid unsuppressSong();\n\tvoid suppressAll(qint64 duration);\n\nprivate:\n\tclass Track {\n\tpublic:\n\t\tstatic constexpr int kBuffersCount = 3;\n\n\t\t// Thread: Any. Must be locked: AudioMutex.\n\t\tvoid reattach(AudioMsgId::Type type);\n\n\t\t// Thread: Main. Must be locked: AudioMutex.\n\t\tvoid detach();\n\t\tvoid clear();\n\n\t\tvoid started();\n\n\t\tbool isStreamCreated() const;\n\t\tvoid ensureStreamCreated(AudioMsgId::Type type);\n\n\t\tint getNotQueuedBufferIndex();\n\n\t\t// Thread: Main. Must be locked: AudioMutex.\n\t\tvoid setExternalData(std::unique_ptr<ExternalSoundData> data);\n\n\t\tvoid updateStatePosition();\n\t\tvoid updateWithSpeedPosition();\n\n\t\t[[nodiscard]] static int64 SpeedIndependentPosition(\n\t\t\tint64 position,\n\t\t\tfloat64 speed);\n\t\t[[nodiscard]] static int64 SpeedDependentPosition(\n\t\t\tint64 position,\n\t\t\tfloat64 speed);\n\n\t\t~Track();\n\n\t\tTrackState state;\n\n\t\tCore::FileLocation file;\n\t\tQByteArray data;\n\n\t\tint format = 0;\n\t\tbool loading = false;\n\t\tbool loaded = false;\n\t\tbool waitingForBuffer = false;\n\n\t\t// Speed dependent values.\n\t\tfloat64 speed = 1.;\n\t\tfloat64 nextSpeed = 1.;\n\t\tstruct WithSpeed {\n\t\t\tint64 fineTunedPosition = 0;\n\t\t\tint64 position = 0;\n\t\t\tint64 length = 0;\n\t\t\tint64 bufferedPosition = 0;\n\t\t\tint64 bufferedLength = 0;\n\t\t\tint64 fadeStartPosition = 0;\n\t\t\tint samples[kBuffersCount] = { 0 };\n\t\t\tQByteArray buffered[kBuffersCount];\n\t\t};\n\t\tWithSpeed withSpeed;\n\n\t\tstruct Stream {\n\t\t\tuint32 source = 0;\n\t\t\tuint32 buffers[kBuffersCount] = { 0 };\n\t\t};\n\t\tStream stream;\n\n\t\tstd::unique_ptr<ExternalSoundData> externalData;\n\n\t\tcrl::time lastUpdateWhen = 0;\n\t\tcrl::time lastUpdatePosition = 0;\n\n\tprivate:\n\t\tvoid createStream(AudioMsgId::Type type);\n\t\tvoid destroyStream();\n\t\tvoid resetStream();\n\n\t};\n\n\tbool fadedStop(AudioMsgId::Type type, bool *fadedStart = 0);\n\tvoid resetFadeStartPosition(AudioMsgId::Type type, int positionInBuffered = -1);\n\tbool checkCurrentALError(AudioMsgId::Type type);\n\n\tvoid externalSoundProgress(const AudioMsgId &audio);\n\n\t// Thread: Any. Must be locked: AudioMutex.\n\tvoid setStoppedState(Track *current, State state = State::Stopped);\n\n\tTrack *trackForType(AudioMsgId::Type type, int index = -1); // -1 uses currentIndex(type)\n\tconst Track *trackForType(AudioMsgId::Type type, int index = -1) const;\n\tint *currentIndex(AudioMsgId::Type type);\n\tconst int *currentIndex(AudioMsgId::Type type) const;\n\n\tconst not_null<Audio::Instance*> _instance;\n\n\tint _audioCurrent = 0;\n\tTrack _audioTracks[kTogetherLimit];\n\n\tint _songCurrent = 0;\n\tTrack _songTracks[kTogetherLimit];\n\n\tTrack _videoTrack;\n\n\tQAtomicInt _volumeVideo;\n\tQAtomicInt _volumeSong;\n\n\tfriend class Fader;\n\tfriend class Loaders;\n\n\tQThread _faderThread, _loaderThread;\n\tFader *_fader;\n\tLoaders *_loader;\n\n\trpl::lifetime _lifetime;\n\n};\n\nMixer *mixer();\n\nclass Fader : public QObject {\n\tQ_OBJECT\n\npublic:\n\tFader(QThread *thread);\n\n\tvoid songVolumeChanged();\n\tvoid videoVolumeChanged();\n\nQ_SIGNALS:\n\tvoid error(const AudioMsgId &audio);\n\tvoid playPositionUpdated(const AudioMsgId &audio);\n\tvoid audioStopped(const AudioMsgId &audio);\n\tvoid needToPreload(const AudioMsgId &audio);\n\npublic Q_SLOTS:\n\tvoid onInit();\n\tvoid onTimer();\n\n\tvoid onSuppressSong();\n\tvoid onUnsuppressSong();\n\tvoid onSuppressAll(qint64 duration);\n\nprivate:\n\tenum {\n\t\tEmitError = 0x01,\n\t\tEmitStopped = 0x02,\n\t\tEmitPositionUpdated = 0x04,\n\t\tEmitNeedToPreload = 0x08,\n\t};\n\tint32 updateOnePlayback(Mixer::Track *track, bool &hasPlaying, bool &hasFading, float64 volumeMultiplier, bool volumeChanged);\n\tvoid setStoppedState(Mixer::Track *track, State state = State::Stopped);\n\n\tQTimer _timer;\n\n\tbool _volumeChangedSong = false;\n\tbool _volumeChangedVideo = false;\n\n\tbool _suppressAll = false;\n\tbool _suppressAllAnim = false;\n\tbool _suppressSong = false;\n\tbool _suppressSongAnim = false;\n\tanim::value _suppressVolumeAll;\n\tanim::value _suppressVolumeSong;\n\tcrl::time _suppressAllStart = 0;\n\tcrl::time _suppressAllEnd = 0;\n\tcrl::time _suppressSongStart = 0;\n\n\tcrl::time getFadeDuration();\n\n};\n\n[[nodiscard]] Ui::PreparedFileInformation PrepareForSending(\n\tconst QString &fname,\n\tconst QByteArray &data);\n\nnamespace internal {\n\n// Thread: Any. Must be locked: AudioMutex.\nbool CheckAudioDeviceConnected();\n\n// Thread: Main. Locks: AudioMutex.\nvoid DetachFromDevice(not_null<Audio::Instance*> instance);\nbool DetachIfDeviceChanged(\n\tnot_null<Audio::Instance*> instance,\n\tconst Webrtc::DeviceResolvedId &nowDeviceId);\n\n// Thread: Any.\nQMutex *audioPlayerMutex();\n\n// Thread: Any.\nbool audioCheckError();\n\n} // namespace internal\n\n} // namespace Player\n} // namespace Media\n\nVoiceWaveform audioCountWaveform(const Core::FileLocation &file, const QByteArray &data);\n\nnamespace Media {\nnamespace Audio {\n\nTG_FORCE_INLINE uint16 ReadOneSample(uchar data) {\n\treturn qAbs((static_cast<int16>(data) - 0x80) * 0x100);\n}\n\nTG_FORCE_INLINE uint16 ReadOneSample(int16 data) {\n\treturn qAbs(data);\n}\n\ntemplate <typename SampleType, typename Callback>\nvoid IterateSamples(bytes::const_span bytes, Callback &&callback) {\n\tauto samplesPointer = reinterpret_cast<const SampleType*>(bytes.data());\n\tauto samplesCount = bytes.size() / sizeof(SampleType);\n\tauto samplesData = gsl::make_span(samplesPointer, samplesCount);\n\tfor (auto sampleData : samplesData) {\n\t\tcallback(ReadOneSample(sampleData));\n\t}\n}\n\n} // namespace Audio\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio_capture.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/audio/media_audio_capture.h\"\n\n#include \"media/audio/media_audio_capture_common.h\"\n#include \"media/audio/media_audio_ffmpeg_loader.h\"\n#include \"media/audio/media_audio_track.h\"\n#include \"ffmpeg/ffmpeg_utility.h\"\n#include \"base/timer.h\"\n\n#include <al.h>\n#include <alc.h>\n\n#include <numeric>\n\nnamespace Media {\nnamespace Capture {\nnamespace {\n\nconstexpr auto kCaptureFrequency = Player::kDefaultFrequency;\nconstexpr auto kCaptureSkipDuration = crl::time(400);\nconstexpr auto kCaptureFadeInDuration = crl::time(300);\nconstexpr auto kCaptureBufferSlice = 256 * 1024;\nconstexpr auto kCaptureUpdateDelta = crl::time(100);\n\nInstance *CaptureInstance = nullptr;\n\nbool ErrorHappened(ALCdevice *device) {\n\tALenum errCode;\n\tif ((errCode = alcGetError(device)) != ALC_NO_ERROR) {\n\t\tLOG((\"Audio Capture Error: %1, %2\").arg(errCode).arg((const char *)alcGetString(device, errCode)));\n\t\treturn true;\n\t}\n\treturn false;\n}\n\n[[nodiscard]] VoiceWaveform CollectWaveform(\n\t\tconst QVector<uchar> &waveformVector) {\n\tif (waveformVector.isEmpty()) {\n\t\treturn {};\n\t}\n\tauto waveform = VoiceWaveform();\n\tauto count = int64(waveformVector.size());\n\tauto sum = int64(0);\n\tif (count >= Player::kWaveformSamplesCount) {\n\t\tauto peaks = QVector<uint16>();\n\t\tpeaks.reserve(Player::kWaveformSamplesCount);\n\n\t\tauto peak = uint16(0);\n\t\tfor (auto i = int32(0); i < count; ++i) {\n\t\t\tauto sample = uint16(waveformVector.at(i)) * 256;\n\t\t\tif (peak < sample) {\n\t\t\t\tpeak = sample;\n\t\t\t}\n\t\t\tsum += Player::kWaveformSamplesCount;\n\t\t\tif (sum >= count) {\n\t\t\t\tsum -= count;\n\t\t\t\tpeaks.push_back(peak);\n\t\t\t\tpeak = 0;\n\t\t\t}\n\t\t}\n\n\t\tauto sum = std::accumulate(peaks.cbegin(), peaks.cend(), 0LL);\n\t\tpeak = qMax(int32(sum * 1.8 / peaks.size()), 2500);\n\n\t\twaveform.resize(peaks.size());\n\t\tfor (int32 i = 0, l = peaks.size(); i != l; ++i) {\n\t\t\twaveform[i] = char(qMin(\n\t\t\t\t31U,\n\t\t\t\tuint32(qMin(peaks.at(i), peak)) * 31 / peak));\n\t\t}\n\t}\n\treturn waveform;\n}\n\n} // namespace\n\nclass Instance::Inner final : public QObject {\npublic:\n\tInner(QThread *thread);\n\t~Inner();\n\n\tvoid start(\n\t\tWebrtc::DeviceResolvedId id,\n\t\tFn<void(Update)> updated,\n\t\tFn<void()> error,\n\t\tFn<void(Chunk)> externalProcessing);\n\tvoid stop(Fn<void(Result&&)> callback = nullptr);\n\tvoid pause(bool value, Fn<void(Result&&)> callback);\n\nprivate:\n\tvoid process();\n\n\tbool initializeFFmpeg();\n\t[[nodiscard]] bool processFrame(int32 offset, int32 framesize);\n\tvoid fail();\n\n\t[[nodiscard]] bool writeFrame(AVFrame *frame);\n\n\t// Writes the packets till EAGAIN is got from av_receive_packet()\n\t// Returns number of packets written or -1 on error\n\t[[nodiscard]] int writePackets();\n\n\tFn<void(Chunk)> _externalProcessing;\n\tFn<void(Update)> _updated;\n\tFn<void()> _error;\n\n\tstruct Private;\n\tconst std::unique_ptr<Private> d;\n\tbase::Timer _timer;\n\tQByteArray _captured;\n\n\tbool _paused = false;\n\n};\n\nvoid Start() {\n\tAssert(CaptureInstance == nullptr);\n\tCaptureInstance = new Instance();\n\tinstance()->check();\n}\n\nvoid Finish() {\n\tdelete base::take(CaptureInstance);\n}\n\nInstance::Instance() : _inner(std::make_unique<Inner>(&_thread)) {\n\tCaptureInstance = this;\n\t_thread.start();\n}\n\nvoid Instance::start(Fn<void(Chunk)> externalProcessing) {\n\t_updates.fire_done();\n\tconst auto id = Audio::Current().captureDeviceId();\n\tInvokeQueued(_inner.get(), [=] {\n\t\t_inner->start(id, [=](Update update) {\n\t\t\tcrl::on_main(this, [=] {\n\t\t\t\t_updates.fire_copy(update);\n\t\t\t});\n\t\t}, [=] {\n\t\t\tcrl::on_main(this, [=] {\n\t\t\t\t_updates.fire_error(Error::Other);\n\t\t\t});\n\t\t}, externalProcessing);\n\t\tcrl::on_main(this, [=] {\n\t\t\t_started = true;\n\t\t});\n\t});\n}\n\nvoid Instance::stop(Fn<void(Result&&)> callback) {\n\tInvokeQueued(_inner.get(), [=] {\n\t\tif (!callback) {\n\t\t\t_inner->stop();\n\t\t\tcrl::on_main(this, [=] { _started = false; });\n\t\t\treturn;\n\t\t}\n\t\t_inner->stop([=](Result &&result) {\n\t\t\tcrl::on_main([=, result = std::move(result)]() mutable {\n\t\t\t\tcallback(std::move(result));\n\t\t\t\t_started = false;\n\t\t\t});\n\t\t});\n\t});\n}\n\nvoid Instance::pause(bool value, Fn<void(Result&&)> callback) {\n\tInvokeQueued(_inner.get(), [=] {\n\t\tauto done = callback\n\t\t\t? [=](Result &&result) {\n\t\t\t\tcrl::on_main([=, result = std::move(result)]() mutable {\n\t\t\t\t\tcallback(std::move(result));\n\t\t\t\t});\n\t\t\t}\n\t\t\t: std::move(callback);\n\t\t_inner->pause(value, std::move(done));\n\t});\n}\n\nvoid Instance::check() {\n\t_available = false;\n\tif (auto device = alcGetString(0, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER)) {\n\t\tif (!QString::fromUtf8(device).isEmpty()) {\n\t\t\t_available = true;\n\t\t\treturn;\n\t\t}\n\t}\n\tLOG((\"Audio Error: No capture device found!\"));\n}\n\nInstance::~Instance() {\n\t// Send _inner to it's thread for destruction.\n\tif (const auto context = _inner.get()) {\n\t\tInvokeQueued(context, [copy = base::take(_inner)]{});\n\t}\n\n\t// And wait for it to finish.\n\t_thread.quit();\n\t_thread.wait();\n}\n\nInstance *instance() {\n\treturn CaptureInstance;\n}\n\nstruct Instance::Inner::Private {\n\tALCdevice *device = nullptr;\n\tAVOutputFormat *fmt = nullptr;\n\tuchar *ioBuffer = nullptr;\n\tAVIOContext *ioContext = nullptr;\n\tAVFormatContext *fmtContext = nullptr;\n\tAVStream *stream = nullptr;\n\tconst AVCodec *codec = nullptr;\n\tAVCodecContext *codecContext = nullptr;\n\tint channels = 0;\n\tbool opened = false;\n\tbool processing = false;\n\n\tint srcSamples = 0;\n\tint dstSamples = 0;\n\tint maxDstSamples = 0;\n\tint dstSamplesSize = 0;\n\tint fullSamples = 0;\n\tuint8_t **srcSamplesData = nullptr;\n\tuint8_t **dstSamplesData = nullptr;\n\tSwrContext *swrContext = nullptr;\n\n\tint32 lastUpdate = 0;\n\tuint16 levelMax = 0;\n\n\tQByteArray data;\n\tint32 dataPos = 0;\n\n\tint64 waveformMod = 0;\n\tint64 waveformEach = (kCaptureFrequency / 100);\n\tuint16 waveformPeak = 0;\n\tQVector<uchar> waveform;\n\n\tstatic int ReadData(void *opaque, uint8_t *buf, int buf_size) {\n\t\tauto l = reinterpret_cast<Private*>(opaque);\n\n\t\tint32 nbytes = qMin(l->data.size() - l->dataPos, int32(buf_size));\n\t\tif (nbytes <= 0) {\n\t\t\treturn AVERROR_EOF;\n\t\t}\n\n\t\tmemcpy(buf, l->data.constData() + l->dataPos, nbytes);\n\t\tl->dataPos += nbytes;\n\t\treturn nbytes;\n\t}\n\n#if DA_FFMPEG_CONST_WRITE_CALLBACK\n\tstatic int WriteData(void *opaque, const uint8_t *buf, int buf_size) {\n#else\n\tstatic int WriteData(void *opaque, uint8_t *buf, int buf_size) {\n#endif\n\t\tauto l = reinterpret_cast<Private*>(opaque);\n\n\t\tif (buf_size <= 0) return 0;\n\t\tif (l->dataPos + buf_size > l->data.size()) l->data.resize(l->dataPos + buf_size);\n\t\tmemcpy(l->data.data() + l->dataPos, buf, buf_size);\n\t\tl->dataPos += buf_size;\n\t\treturn buf_size;\n\t}\n\n\tstatic int64_t SeekData(void *opaque, int64_t offset, int whence) {\n\t\tauto l = reinterpret_cast<Private*>(opaque);\n\n\t\tint32 newPos = -1;\n\t\tswitch (whence) {\n\t\tcase SEEK_SET: newPos = offset; break;\n\t\tcase SEEK_CUR: newPos = l->dataPos + offset; break;\n\t\tcase SEEK_END: newPos = l->data.size() + offset; break;\n\t\tcase AVSEEK_SIZE: {\n\t\t\t// Special whence for determining filesize without any seek.\n\t\t\treturn l->data.size();\n\t\t} break;\n\t\t}\n\t\tif (newPos < 0) {\n\t\t\treturn -1;\n\t\t}\n\t\tl->dataPos = newPos;\n\t\treturn l->dataPos;\n\t}\n};\n\nInstance::Inner::Inner(QThread *thread)\n: d(std::make_unique<Private>())\n, _timer(thread, [=] { process(); }) {\n\tmoveToThread(thread);\n}\n\nInstance::Inner::~Inner() {\n\tstop();\n}\n\nvoid Instance::Inner::fail() {\n\tstop();\n\tif (const auto error = base::take(_error)) {\n\t\tInvokeQueued(this, error);\n\t}\n}\n\nvoid Instance::Inner::start(\n\t\tWebrtc::DeviceResolvedId id,\n\t\tFn<void(Update)> updated,\n\t\tFn<void()> error,\n\t\tFn<void(Chunk)> externalProcessing) {\n\t_externalProcessing = std::move(externalProcessing);\n\t_updated = std::move(updated);\n\t_error = std::move(error);\n\tif (_paused) {\n\t\t_paused = false;\n\t}\n\n\t// Start OpenAL Capture\n\tconst auto utf = id.isDefault() ? std::string() : id.value.toStdString();\n\td->device = alcCaptureOpenDevice(\n\t\tutf.empty() ? nullptr : utf.c_str(),\n\t\tkCaptureFrequency,\n\t\tAL_FORMAT_MONO16,\n\t\tkCaptureFrequency / 5);\n\tif (!d->device) {\n\t\tLOG((\"Audio Error: capture device not present!\"));\n\t\tfail();\n\t\treturn;\n\t}\n\talcCaptureStart(d->device);\n\tif (ErrorHappened(d->device)) {\n\t\talcCaptureCloseDevice(d->device);\n\t\td->device = nullptr;\n\t\tfail();\n\t\treturn;\n\t} else if (!_externalProcessing) {\n\t\tif (!initializeFFmpeg()) {\n\t\t\tfail();\n\t\t\treturn;\n\t\t}\n\t}\n\t_timer.callEach(50);\n\t_captured.clear();\n\t_captured.reserve(kCaptureBufferSlice);\n\tDEBUG_LOG((\"Audio Capture: started!\"));\n}\n\nbool Instance::Inner::initializeFFmpeg() {\n\t// Create encoding context\n\n\td->ioBuffer = (uchar*)av_malloc(FFmpeg::kAVBlockSize);\n\n\td->ioContext = avio_alloc_context(d->ioBuffer, FFmpeg::kAVBlockSize, 1, static_cast<void*>(d.get()), &Private::ReadData, &Private::WriteData, &Private::SeekData);\n\tint res = 0;\n\tchar err[AV_ERROR_MAX_STRING_SIZE] = { 0 };\n\tconst AVOutputFormat *fmt = nullptr;\n\tvoid *i = nullptr;\n\twhile ((fmt = av_muxer_iterate(&i))) {\n\t\tif (fmt->name == u\"opus\"_q) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (!fmt) {\n\t\tLOG((\"Audio Error: Unable to find opus AVOutputFormat for capture\"));\n\t\treturn false;\n\t}\n\n\tif ((res = avformat_alloc_output_context2(&d->fmtContext, (AVOutputFormat*)fmt, 0, 0)) < 0) {\n\t\tLOG((\"Audio Error: Unable to avformat_alloc_output_context2 for capture, error %1, %2\").arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\treturn false;\n\t}\n\td->fmtContext->pb = d->ioContext;\n\td->fmtContext->flags |= AVFMT_FLAG_CUSTOM_IO;\n\td->opened = true;\n\n\t// Add audio stream\n\td->codec = avcodec_find_encoder(fmt->audio_codec);\n\tif (!d->codec) {\n\t\tLOG((\"Audio Error: Unable to avcodec_find_encoder for capture\"));\n\t\treturn false;\n\t}\n\td->stream = avformat_new_stream(d->fmtContext, d->codec);\n\tif (!d->stream) {\n\t\tLOG((\"Audio Error: Unable to avformat_new_stream for capture\"));\n\t\treturn false;\n\t}\n\td->stream->id = d->fmtContext->nb_streams - 1;\n\td->codecContext = avcodec_alloc_context3(d->codec);\n\tif (!d->codecContext) {\n\t\tLOG((\"Audio Error: Unable to avcodec_alloc_context3 for capture\"));\n\t\treturn false;\n\t}\n\n\tav_opt_set_int(d->codecContext, \"refcounted_frames\", 1, 0);\n\n\td->codecContext->sample_fmt = AV_SAMPLE_FMT_FLTP;\n\td->codecContext->bit_rate = 32000;\n\td->codecContext->ch_layout = AV_CHANNEL_LAYOUT_MONO;\n\td->channels = d->codecContext->ch_layout.nb_channels;\n\td->codecContext->sample_rate = kCaptureFrequency;\n\n\tif (d->fmtContext->oformat->flags & AVFMT_GLOBALHEADER) {\n\t\td->codecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;\n\t}\n\n\t// Open audio stream\n\tif ((res = avcodec_open2(d->codecContext, d->codec, nullptr)) < 0) {\n\t\tLOG((\"Audio Error: Unable to avcodec_open2 for capture, error %1, %2\").arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\treturn false;\n\t}\n\n\t// Alloc source samples\n\n\td->srcSamples = (d->codecContext->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE) ? 10000 : d->codecContext->frame_size;\n\t//if ((res = av_samples_alloc_array_and_samples(&d->srcSamplesData, 0, d->codecContext->channels, d->srcSamples, d->codecContext->sample_fmt, 0)) < 0) {\n\t//\tLOG((\"Audio Error: Unable to av_samples_alloc_array_and_samples for capture, error %1, %2\").arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t//\tonStop(false);\n\t//\temit error();\n\t//\treturn;\n\t//}\n\t// Using _captured directly\n\n\t// Prepare resampling\n\tres = swr_alloc_set_opts2(\n\t\t&d->swrContext,\n\t\t&d->codecContext->ch_layout,\n\t\td->codecContext->sample_fmt,\n\t\td->codecContext->sample_rate,\n\t\t&d->codecContext->ch_layout,\n\t\tAV_SAMPLE_FMT_S16,\n\t\td->codecContext->sample_rate,\n\t\t0,\n\t\tnullptr);\n\tif (res < 0 || !d->swrContext) {\n\t\tLOG((\"Audio Error: Unable to swr_alloc_set_opts2 for capture, error %1, %2\").arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\treturn false;\n\t} else if ((res = swr_init(d->swrContext)) < 0) {\n\t\tLOG((\"Audio Error: Unable to swr_init for capture, error %1, %2\").arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\treturn false;\n\t}\n\td->maxDstSamples = d->srcSamples;\n\tif ((res = av_samples_alloc_array_and_samples(&d->dstSamplesData, 0, d->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0)) < 0) {\n\t\tLOG((\"Audio Error: Unable to av_samples_alloc_array_and_samples for capture, error %1, %2\").arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\treturn false;\n\t}\n\td->dstSamplesSize = av_samples_get_buffer_size(0, d->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0);\n\tif ((res = avcodec_parameters_from_context(d->stream->codecpar, d->codecContext)) < 0) {\n\t\tLOG((\"Audio Error: Unable to avcodec_parameters_from_context for capture, error %1, %2\").arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\treturn false;\n\t}\n\t// Write file header\n\tif ((res = avformat_write_header(d->fmtContext, 0)) < 0) {\n\t\tLOG((\"Audio Error: Unable to avformat_write_header for capture, error %1, %2\").arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nvoid Instance::Inner::pause(bool value, Fn<void(Result&&)> callback) {\n\t_paused = value;\n\tif (!_paused) {\n\t\treturn;\n\t}\n\tif (callback) {\n\t\tcallback({\n\t\t\t.bytes = d->fullSamples ? d->data : QByteArray(),\n\t\t\t.waveform = (d->fullSamples\n\t\t\t\t? CollectWaveform(d->waveform)\n\t\t\t\t: VoiceWaveform()),\n\t\t\t.duration = ((d->fullSamples * crl::time(1000))\n\t\t\t\t/ int64(kCaptureFrequency)),\n\t\t});\n\t}\n}\n\nvoid Instance::Inner::stop(Fn<void(Result&&)> callback) {\n\tif (!_timer.isActive()) {\n\t\treturn; // in stop() already\n\t}\n\t_paused = false;\n\t_timer.cancel();\n\n\tconst auto needResult = (callback != nullptr);\n\tconst auto hadDevice = (d->device != nullptr);\n\tif (hadDevice) {\n\t\talcCaptureStop(d->device);\n\t\tif (d->processing) {\n\t\t\tAssert(!needResult); // stop in the middle of processing - error.\n\t\t} else {\n\t\t\tprocess(); // get last data\n\t\t}\n\t\talcCaptureCloseDevice(d->device);\n\t\td->device = nullptr;\n\t}\n\n\t// Write what is left\n\tif (needResult && !_captured.isEmpty()) {\n\t\tauto fadeSamples = kCaptureFadeInDuration * kCaptureFrequency / 1000;\n\t\tauto capturedSamples = static_cast<int>(_captured.size() / sizeof(short));\n\t\tif ((_captured.size() % sizeof(short)) || (d->fullSamples + capturedSamples < kCaptureFrequency) || (capturedSamples < fadeSamples)) {\n\t\t\td->fullSamples = 0;\n\t\t\td->dataPos = 0;\n\t\t\td->data.clear();\n\t\t\td->waveformMod = 0;\n\t\t\td->waveformPeak = 0;\n\t\t\td->waveform.clear();\n\t\t} else {\n\t\t\tfloat64 coef = 1. / fadeSamples, fadedFrom = 0;\n\t\t\tfor (short *ptr = ((short*)_captured.data()) + capturedSamples, *end = ptr - fadeSamples; ptr != end; ++fadedFrom) {\n\t\t\t\t--ptr;\n\t\t\t\t*ptr = qRound(fadedFrom * coef * *ptr);\n\t\t\t}\n\t\t\tif (capturedSamples % d->srcSamples) {\n\t\t\t\tint32 s = _captured.size();\n\t\t\t\t_captured.resize(s + (d->srcSamples - (capturedSamples % d->srcSamples)) * sizeof(short));\n\t\t\t\tmemset(_captured.data() + s, 0, _captured.size() - s);\n\t\t\t}\n\n\t\t\tint32 framesize = d->srcSamples * d->channels * sizeof(short), encoded = 0;\n\t\t\twhile (_captured.size() >= encoded + framesize) {\n\t\t\t\tif (!processFrame(encoded, framesize)) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tencoded += framesize;\n\t\t\t}\n\t\t\t// Drain the codec.\n\t\t\tif (!writeFrame(nullptr) || encoded != _captured.size()) {\n\t\t\t\td->fullSamples = 0;\n\t\t\t\td->dataPos = 0;\n\t\t\t\td->data.clear();\n\t\t\t\td->waveformMod = 0;\n\t\t\t\td->waveformPeak = 0;\n\t\t\t\td->waveform.clear();\n\t\t\t}\n\t\t}\n\t}\n\tDEBUG_LOG((\"Audio Capture: \"\n\t\t\"stopping (need result: %1), size: %2, samples: %3\"\n\t\t).arg(Logs::b(callback != nullptr)\n\t\t).arg(d->data.size()\n\t\t).arg(d->fullSamples));\n\t_captured = QByteArray();\n\n\t// Finish stream\n\tif (needResult && hadDevice && d->fmtContext) {\n\t\tav_write_trailer(d->fmtContext);\n\t}\n\n\tQByteArray result = d->fullSamples ? d->data : QByteArray();\n\tVoiceWaveform waveform;\n\tqint32 samples = d->fullSamples;\n\tif (needResult && samples && !d->waveform.isEmpty()) {\n\t\twaveform = CollectWaveform(d->waveform);\n\t}\n\tif (hadDevice) {\n\t\tif (d->codecContext) {\n\t\t\tavcodec_free_context(&d->codecContext);\n\t\t\td->codecContext = nullptr;\n\t\t}\n\t\tif (d->srcSamplesData) {\n\t\t\tif (d->srcSamplesData[0]) {\n\t\t\t\tav_freep(&d->srcSamplesData[0]);\n\t\t\t}\n\t\t\tav_freep(&d->srcSamplesData);\n\t\t}\n\t\tif (d->dstSamplesData) {\n\t\t\tif (d->dstSamplesData[0]) {\n\t\t\t\tav_freep(&d->dstSamplesData[0]);\n\t\t\t}\n\t\t\tav_freep(&d->dstSamplesData);\n\t\t}\n\t\td->fullSamples = 0;\n\t\tif (d->swrContext) {\n\t\t\tswr_free(&d->swrContext);\n\t\t\td->swrContext = nullptr;\n\t\t}\n\t\tif (d->opened) {\n\t\t\tavformat_close_input(&d->fmtContext);\n\t\t\td->opened = false;\n\t\t}\n\t\tif (d->ioContext) {\n\t\t\tav_freep(&d->ioContext->buffer);\n\t\t\tav_freep(&d->ioContext);\n\t\t\td->ioBuffer = nullptr;\n\t\t} else if (d->ioBuffer) {\n\t\t\tav_freep(&d->ioBuffer);\n\t\t}\n\t\tif (d->fmtContext) {\n\t\t\tavformat_free_context(d->fmtContext);\n\t\t\td->fmtContext = nullptr;\n\t\t}\n\t\td->fmt = nullptr;\n\t\td->stream = nullptr;\n\t\td->codec = nullptr;\n\n\t\td->lastUpdate = 0;\n\t\td->levelMax = 0;\n\n\t\td->dataPos = 0;\n\t\td->data.clear();\n\n\t\td->waveformMod = 0;\n\t\td->waveformPeak = 0;\n\t\td->waveform.clear();\n\t}\n\n\tif (needResult) {\n\t\tcallback({\n\t\t\t.bytes = result,\n\t\t\t.waveform = waveform,\n\t\t\t.duration = (samples * crl::time(1000)) / kCaptureFrequency,\n\t\t});\n\t}\n}\n\nvoid Instance::Inner::process() {\n\tExpects(!d->processing);\n\n\tif (_paused) {\n\t\treturn;\n\t}\n\n\td->processing = true;\n\tconst auto guard = gsl::finally([&] { d->processing = false; });\n\n\tif (!d->device) {\n\t\t_timer.cancel();\n\t\treturn;\n\t}\n\tALint samples;\n\talcGetIntegerv(d->device, ALC_CAPTURE_SAMPLES, 1, &samples);\n\tif (ErrorHappened(d->device)) {\n\t\tfail();\n\t\treturn;\n\t}\n\tif (samples > 0) {\n\t\t// Get samples from OpenAL\n\t\tauto s = _captured.size();\n\t\tauto news = s + static_cast<int>(samples * sizeof(short));\n\t\tif (news / kCaptureBufferSlice > s / kCaptureBufferSlice) {\n\t\t\t_captured.reserve(((news / kCaptureBufferSlice) + 1) * kCaptureBufferSlice);\n\t\t}\n\t\t_captured.resize(news);\n\t\talcCaptureSamples(d->device, (ALCvoid *)(_captured.data() + s), samples);\n\t\tif (ErrorHappened(d->device)) {\n\t\t\tfail();\n\t\t\treturn;\n\t\t} else if (_externalProcessing) {\n\t\t\t_externalProcessing({\n\t\t\t\t.finished = crl::now(),\n\t\t\t\t.samples = base::take(_captured),\n\t\t\t\t.frequency = kCaptureFrequency,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\t// Count new recording level and update view\n\t\tauto skipSamples = kCaptureSkipDuration * kCaptureFrequency / 1000;\n\t\tauto fadeSamples = kCaptureFadeInDuration * kCaptureFrequency / 1000;\n\t\tauto levelindex = d->fullSamples + static_cast<int>(s / sizeof(short));\n\t\tfor (auto ptr = (const short*)(_captured.constData() + s), end = (const short*)(_captured.constData() + news); ptr < end; ++ptr, ++levelindex) {\n\t\t\tif (levelindex > skipSamples) {\n\t\t\t\tuint16 value = qAbs(*ptr);\n\t\t\t\tif (levelindex < skipSamples + fadeSamples) {\n\t\t\t\t\tvalue = qRound(value * float64(levelindex - skipSamples) / fadeSamples);\n\t\t\t\t}\n\t\t\t\tif (d->levelMax < value) {\n\t\t\t\t\td->levelMax = value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tqint32 samplesFull = d->fullSamples + _captured.size() / sizeof(short), samplesSinceUpdate = samplesFull - d->lastUpdate;\n\t\tif (samplesSinceUpdate > kCaptureUpdateDelta * kCaptureFrequency / 1000) {\n\t\t\t_updated(Update{ .samples = samplesFull, .level = d->levelMax });\n\t\t\td->lastUpdate = samplesFull;\n\t\t\td->levelMax = 0;\n\t\t}\n\t\t// Write frames\n\t\tint32 framesize = d->srcSamples * d->channels * sizeof(short), encoded = 0;\n\t\twhile (uint32(_captured.size()) >= encoded + framesize + fadeSamples * sizeof(short)) {\n\t\t\tif (!processFrame(encoded, framesize)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tencoded += framesize;\n\t\t}\n\n\t\t// Collapse the buffer\n\t\tif (encoded > 0) {\n\t\t\tint32 goodSize = _captured.size() - encoded;\n\t\t\tmemmove(_captured.data(), _captured.constData() + encoded, goodSize);\n\t\t\t_captured.resize(goodSize);\n\t\t}\n\t} else {\n\t\tDEBUG_LOG((\"Audio Capture: no samples to capture.\"));\n\t}\n}\n\nbool Instance::Inner::processFrame(int32 offset, int32 framesize) {\n\t// Prepare audio frame\n\n\tif (framesize % sizeof(short)) { // in the middle of a sample\n\t\tLOG((\"Audio Error: Bad framesize in writeFrame() for capture, framesize %1, %2\").arg(framesize));\n\t\tfail();\n\t\treturn false;\n\t}\n\tauto samplesCnt = static_cast<int>(framesize / sizeof(short));\n\n\tint res = 0;\n\tchar err[AV_ERROR_MAX_STRING_SIZE] = { 0 };\n\n\tauto srcSamplesDataChannel = (short*)(_captured.data() + offset);\n\tauto srcSamplesData = &srcSamplesDataChannel;\n\n\t//\tmemcpy(d->srcSamplesData[0], _captured.constData() + offset, framesize);\n\tauto skipSamples = static_cast<int>(kCaptureSkipDuration * kCaptureFrequency / 1000);\n\tauto fadeSamples = static_cast<int>(kCaptureFadeInDuration * kCaptureFrequency / 1000);\n\tif (d->fullSamples < skipSamples + fadeSamples) {\n\t\tint32 fadedCnt = qMin(samplesCnt, skipSamples + fadeSamples - d->fullSamples);\n\t\tfloat64 coef = 1. / fadeSamples, fadedFrom = d->fullSamples - skipSamples;\n\t\tshort *ptr = srcSamplesDataChannel, *zeroEnd = ptr + qMin(samplesCnt, qMax(0, skipSamples - d->fullSamples)), *end = ptr + fadedCnt;\n\t\tfor (; ptr != zeroEnd; ++ptr, ++fadedFrom) {\n\t\t\t*ptr = 0;\n\t\t}\n\t\tfor (; ptr != end; ++ptr, ++fadedFrom) {\n\t\t\t*ptr = qRound(fadedFrom * coef * *ptr);\n\t\t}\n\t}\n\n\td->waveform.reserve(d->waveform.size() + (samplesCnt / d->waveformEach) + 1);\n\tfor (short *ptr = srcSamplesDataChannel, *end = ptr + samplesCnt; ptr != end; ++ptr) {\n\t\tuint16 value = qAbs(*ptr);\n\t\tif (d->waveformPeak < value) {\n\t\t\td->waveformPeak = value;\n\t\t}\n\t\tif (++d->waveformMod == d->waveformEach) {\n\t\t\td->waveformMod -= d->waveformEach;\n\t\t\td->waveform.push_back(uchar(d->waveformPeak / 256));\n\t\t\td->waveformPeak = 0;\n\t\t}\n\t}\n\n\t// Convert to final format\n\n\td->dstSamples = av_rescale_rnd(swr_get_delay(d->swrContext, d->codecContext->sample_rate) + d->srcSamples, d->codecContext->sample_rate, d->codecContext->sample_rate, AV_ROUND_UP);\n\tif (d->dstSamples > d->maxDstSamples) {\n\t\td->maxDstSamples = d->dstSamples;\n\t\tav_freep(&d->dstSamplesData[0]);\n\t\tif ((res = av_samples_alloc(d->dstSamplesData, 0, d->channels, d->dstSamples, d->codecContext->sample_fmt, 1)) < 0) {\n\t\t\tLOG((\"Audio Error: Unable to av_samples_alloc for capture, error %1, %2\").arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\t\tfail();\n\t\t\treturn false;\n\t\t}\n\t\td->dstSamplesSize = av_samples_get_buffer_size(0, d->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0);\n\t}\n\n\tif ((res = swr_convert(d->swrContext, d->dstSamplesData, d->dstSamples, (const uint8_t **)srcSamplesData, d->srcSamples)) < 0) {\n\t\tLOG((\"Audio Error: Unable to swr_convert for capture, error %1, %2\").arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\tfail();\n\t\treturn false;\n\t}\n\n\t// Write audio frame\n\n\tAVFrame *frame = av_frame_alloc();\n\n\tframe->format = d->codecContext->sample_fmt;\n\tav_channel_layout_copy(&frame->ch_layout, &d->codecContext->ch_layout);\n\tframe->sample_rate = d->codecContext->sample_rate;\n\tframe->nb_samples = d->dstSamples;\n\tframe->pts = av_rescale_q(d->fullSamples, AVRational { 1, d->codecContext->sample_rate }, d->codecContext->time_base);\n\n\tavcodec_fill_audio_frame(frame, d->channels, d->codecContext->sample_fmt, d->dstSamplesData[0], d->dstSamplesSize, 0);\n\n\tif (!writeFrame(frame)) {\n\t\treturn false;\n\t}\n\n\td->fullSamples += samplesCnt;\n\n\tav_frame_free(&frame);\n\treturn true;\n}\n\nbool Instance::Inner::writeFrame(AVFrame *frame) {\n\tint res = 0;\n\tchar err[AV_ERROR_MAX_STRING_SIZE] = { 0 };\n\n\tres = avcodec_send_frame(d->codecContext, frame);\n\tif (res == AVERROR(EAGAIN)) {\n\t\tconst auto packetsWritten = writePackets();\n\t\tif (packetsWritten < 0) {\n\t\t\tif (frame && packetsWritten == AVERROR_EOF) {\n\t\t\t\tLOG((\"Audio Error: EOF in packets received when EAGAIN was got in avcodec_send_frame()\"));\n\t\t\t\tfail();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t} else if (!packetsWritten) {\n\t\t\tLOG((\"Audio Error: No packets received when EAGAIN was got in avcodec_send_frame()\"));\n\t\t\tfail();\n\t\t\treturn false;\n\t\t}\n\t\tres = avcodec_send_frame(d->codecContext, frame);\n\t}\n\tif (res < 0) {\n\t\tLOG((\"Audio Error: Unable to avcodec_send_frame for capture, error %1, %2\").arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\tfail();\n\t\treturn false;\n\t}\n\n\tif (!frame) { // drain\n\t\tif ((res = writePackets()) != AVERROR_EOF) {\n\t\t\tLOG((\"Audio Error: not EOF in packets received when draining the codec, result %1\").arg(res));\n\t\t\tfail();\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nint Instance::Inner::writePackets() {\n\tAVPacket *pkt = av_packet_alloc();\n\tconst auto guard = gsl::finally([&] { av_packet_free(&pkt); });\n\n\tint res = 0;\n\tchar err[AV_ERROR_MAX_STRING_SIZE] = { 0 };\n\n\tint written = 0;\n\tdo {\n\t\tif ((res = avcodec_receive_packet(d->codecContext, pkt)) < 0) {\n\t\t\tif (res == AVERROR(EAGAIN)) {\n\t\t\t\treturn written;\n\t\t\t} else if (res == AVERROR_EOF) {\n\t\t\t\treturn res;\n\t\t\t}\n\t\t\tLOG((\"Audio Error: Unable to avcodec_receive_packet for capture, error %1, %2\").arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\t\tfail();\n\t\t\treturn res;\n\t\t}\n\n\t\tav_packet_rescale_ts(pkt, d->codecContext->time_base, d->stream->time_base);\n\t\tpkt->stream_index = d->stream->index;\n\t\tif ((res = av_interleaved_write_frame(d->fmtContext, pkt)) < 0) {\n\t\t\tLOG((\"Audio Error: Unable to av_interleaved_write_frame for capture, error %1, %2\").arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\t\tfail();\n\t\t\treturn -1;\n\t\t}\n\n\t\t++written;\n\t\tav_packet_unref(pkt);\n\t} while (true);\n\treturn written;\n}\n\n} // namespace Capture\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio_capture.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QThread>\n#include <QtCore/QTimer>\n\nnamespace Media {\nnamespace Capture {\n\nstruct Update {\n\tint samples = 0;\n\tushort level = 0;\n\n\tbool finished = false;\n};\n\nenum class Error : uchar {\n\tOther,\n\tAudioInit,\n\tVideoInit,\n\tAudioTimeout,\n\tVideoTimeout,\n\tEncoding,\n};\n\nstruct Chunk {\n\tcrl::time finished = 0;\n\tQByteArray samples;\n\tint frequency = 0;\n};\n\nstruct Result;\n\nvoid Start();\nvoid Finish();\n\nclass Instance final : public QObject {\npublic:\n\tInstance();\n\t~Instance();\n\n\tvoid check();\n\t[[nodiscard]] bool available() const {\n\t\treturn _available;\n\t}\n\n\t[[nodiscard]] rpl::producer<Update, Error> updated() const {\n\t\treturn _updates.events();\n\t}\n\n\t[[nodiscard]] bool started() const {\n\t\treturn _started.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> startedChanges() const {\n\t\treturn _started.changes();\n\t}\n\n\tvoid start(Fn<void(Chunk)> externalProcessing = nullptr);\n\tvoid stop(Fn<void(Result&&)> callback = nullptr);\n\tvoid pause(bool value, Fn<void(Result&&)> callback = nullptr);\n\nprivate:\n\tclass Inner;\n\tfriend class Inner;\n\n\tbool _available = false;\n\trpl::variable<bool> _started = false;\n\trpl::event_stream<Update, Error> _updates;\n\tQThread _thread;\n\tstd::unique_ptr<Inner> _inner;\n\n};\n\n[[nodiscard]] Instance *instance();\n\n} // namespace Capture\n} // namespace Media\n\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio_capture_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Media::Capture {\n\nstruct Result {\n\tQByteArray bytes;\n\tVoiceWaveform waveform;\n\tcrl::time duration;\n\tbool video = false;\n};\n\n} // namespace Media::Capture\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio_edit.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/audio/media_audio_edit.h\"\n\n#include \"ffmpeg/ffmpeg_bytes_io_wrap.h\"\n#include \"ffmpeg/ffmpeg_utility.h\"\n\nnamespace Media {\n\n[[nodiscard]] AudioEditResult TrimAudioToRange(\n\t\tconst QByteArray &content,\n\t\tcrl::time from,\n\t\tcrl::time till) {\n\tusing namespace FFmpeg;\n\n\tif (content.isEmpty() || (from < 0) || (till <= from)) {\n\t\treturn {};\n\t}\n\n\tauto inputWrap = ReadBytesWrap{\n\t\t.size = content.size(),\n\t\t.data = reinterpret_cast<const uchar*>(content.constData()),\n\t};\n\tauto input = MakeFormatPointer(\n\t\t&inputWrap,\n\t\t&ReadBytesWrap::Read,\n\t\tnullptr,\n\t\t&ReadBytesWrap::Seek);\n\tif (!input) {\n\t\treturn {};\n\t}\n\n\tauto error = AvErrorWrap(avformat_find_stream_info(input.get(), nullptr));\n\tif (error) {\n\t\tLogError(u\"avformat_find_stream_info\"_q, error);\n\t\treturn {};\n\t}\n\n\tconst auto streamId = av_find_best_stream(\n\t\tinput.get(),\n\t\tAVMEDIA_TYPE_AUDIO,\n\t\t-1,\n\t\t-1,\n\t\tnullptr,\n\t\t0);\n\tif (streamId < 0) {\n\t\tLogError(u\"av_find_best_stream\"_q, AvErrorWrap(streamId));\n\t\treturn {};\n\t}\n\n\tconst auto inStream = input->streams[streamId];\n\tauto outputWrap = WriteBytesWrap();\n\tauto output = MakeWriteFormatPointer(\n\t\tstatic_cast<void*>(&outputWrap),\n\t\tnullptr,\n\t\t&WriteBytesWrap::Write,\n\t\t&WriteBytesWrap::Seek,\n\t\t\"opus\"_q);\n\tif (!output) {\n\t\treturn {};\n\t}\n\n\tconst auto outStream = avformat_new_stream(output.get(), nullptr);\n\tif (!outStream) {\n\t\tLogError(u\"avformat_new_stream\"_q);\n\t\treturn {};\n\t}\n\n\terror = AvErrorWrap(avcodec_parameters_copy(\n\t\toutStream->codecpar,\n\t\tinStream->codecpar));\n\tif (error) {\n\t\tLogError(u\"avcodec_parameters_copy\"_q, error);\n\t\treturn {};\n\t}\n\toutStream->codecpar->codec_tag = 0;\n\toutStream->time_base = inStream->time_base;\n\n\terror = AvErrorWrap(avformat_write_header(output.get(), nullptr));\n\tif (error) {\n\t\tLogError(u\"avformat_write_header\"_q, error);\n\t\treturn {};\n\t}\n\n\tconst auto fromPts = TimeToPts(from, inStream->time_base);\n\tconst auto tillPts = TimeToPts(till, inStream->time_base);\n\tauto firstPts = int64(AV_NOPTS_VALUE);\n\tauto firstDts = int64(AV_NOPTS_VALUE);\n\tauto lastPts = std::numeric_limits<int64>::min();\n\tauto lastDts = std::numeric_limits<int64>::min();\n\tauto durationPts = int64(0);\n\tauto copied = 0;\n\n\tauto packet = AVPacket();\n\tav_init_packet(&packet);\n\twhile (true) {\n\t\terror = AvErrorWrap(av_read_frame(input.get(), &packet));\n\t\tif (error.code() == AVERROR_EOF) {\n\t\t\tbreak;\n\t\t} else if (error) {\n\t\t\tLogError(u\"av_read_frame\"_q, error);\n\t\t\treturn {};\n\t\t}\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\tav_packet_unref(&packet);\n\t\t});\n\t\tif (packet.stream_index != streamId) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto packetStart = (packet.pts != AV_NOPTS_VALUE)\n\t\t\t? packet.pts\n\t\t\t: packet.dts;\n\t\tconst auto packetDuration = std::max(int64(packet.duration), int64());\n\t\tconst auto packetEnd = (packetStart != AV_NOPTS_VALUE)\n\t\t\t? (packetStart + packetDuration)\n\t\t\t: AV_NOPTS_VALUE;\n\t\tif ((packetStart != AV_NOPTS_VALUE) && (packetStart >= tillPts)) {\n\t\t\tbreak;\n\t\t}\n\t\tif ((packetEnd != AV_NOPTS_VALUE) && (packetEnd <= fromPts)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (packet.pts != AV_NOPTS_VALUE) {\n\t\t\tif (firstPts == AV_NOPTS_VALUE) {\n\t\t\t\tfirstPts = packet.pts;\n\t\t\t}\n\t\t\tpacket.pts -= firstPts;\n\t\t\tif (packet.pts < 0) {\n\t\t\t\tpacket.pts = 0;\n\t\t\t}\n\t\t\tif (packet.pts <= lastPts) {\n\t\t\t\tpacket.pts = lastPts + 1;\n\t\t\t}\n\t\t\tlastPts = packet.pts;\n\t\t}\n\t\tif (packet.dts != AV_NOPTS_VALUE) {\n\t\t\tif (firstDts == AV_NOPTS_VALUE) {\n\t\t\t\tfirstDts = packet.dts;\n\t\t\t}\n\t\t\tpacket.dts -= firstDts;\n\t\t\tif (packet.dts < 0) {\n\t\t\t\tpacket.dts = 0;\n\t\t\t}\n\t\t\tif (packet.dts <= lastDts) {\n\t\t\t\tpacket.dts = lastDts + 1;\n\t\t\t}\n\t\t\tlastDts = packet.dts;\n\t\t}\n\n\t\tconst auto packetPosition = (packet.pts != AV_NOPTS_VALUE)\n\t\t\t? packet.pts\n\t\t\t: packet.dts;\n\t\tif (packetPosition != AV_NOPTS_VALUE) {\n\t\t\tdurationPts = std::max(\n\t\t\t\tdurationPts,\n\t\t\t\tpacketPosition + std::max(int64(packet.duration), int64()));\n\t\t}\n\n\t\tpacket.stream_index = outStream->index;\n\t\terror = AvErrorWrap(av_interleaved_write_frame(output.get(), &packet));\n\t\tif (error) {\n\t\t\tLogError(u\"av_interleaved_write_frame\"_q, error);\n\t\t\treturn {};\n\t\t}\n\t\t++copied;\n\t}\n\n\tif (!copied) {\n\t\treturn {};\n\t}\n\n\terror = AvErrorWrap(av_write_trailer(output.get()));\n\tif (error) {\n\t\tLogError(u\"av_write_trailer\"_q, error);\n\t\treturn {};\n\t}\n\n\tauto result = AudioEditResult();\n\tresult.content = std::move(outputWrap.content);\n\tresult.waveform = audioCountWaveform(Core::FileLocation(), result.content);\n\tresult.duration = durationPts\n\t\t? PtsToTimeCeil(durationPts, outStream->time_base)\n\t\t: (till - from);\n\treturn result;\n}\n\n[[nodiscard]] AudioEditResult ConcatAudio(\n\t\tconst QByteArray &first,\n\t\tconst QByteArray &second) {\n\tusing namespace FFmpeg;\n\n\tif (first.isEmpty() || second.isEmpty()) {\n\t\treturn {};\n\t}\n\n\tauto firstWrap = ReadBytesWrap{\n\t\t.size = first.size(),\n\t\t.data = reinterpret_cast<const uchar*>(first.constData()),\n\t};\n\tauto firstInput = MakeFormatPointer(\n\t\t&firstWrap,\n\t\t&ReadBytesWrap::Read,\n\t\tnullptr,\n\t\t&ReadBytesWrap::Seek);\n\tif (!firstInput) {\n\t\treturn {};\n\t}\n\n\tauto secondWrap = ReadBytesWrap{\n\t\t.size = second.size(),\n\t\t.data = reinterpret_cast<const uchar*>(second.constData()),\n\t};\n\tauto secondInput = MakeFormatPointer(\n\t\t&secondWrap,\n\t\t&ReadBytesWrap::Read,\n\t\tnullptr,\n\t\t&ReadBytesWrap::Seek);\n\tif (!secondInput) {\n\t\treturn {};\n\t}\n\n\tconst auto prepareStream = [](not_null<AVFormatContext*> input) {\n\t\tauto error = AvErrorWrap(avformat_find_stream_info(input, nullptr));\n\t\tif (error) {\n\t\t\tLogError(u\"avformat_find_stream_info\"_q, error);\n\t\t\treturn static_cast<AVStream*>(nullptr);\n\t\t}\n\t\tconst auto streamId = av_find_best_stream(\n\t\t\tinput,\n\t\t\tAVMEDIA_TYPE_AUDIO,\n\t\t\t-1,\n\t\t\t-1,\n\t\t\tnullptr,\n\t\t\t0);\n\t\tif (streamId < 0) {\n\t\t\tLogError(u\"av_find_best_stream\"_q, AvErrorWrap(streamId));\n\t\t\treturn static_cast<AVStream*>(nullptr);\n\t\t}\n\t\treturn input->streams[streamId];\n\t};\n\tconst auto firstStream = prepareStream(firstInput.get());\n\tif (!firstStream) {\n\t\treturn {};\n\t}\n\tconst auto secondStream = prepareStream(secondInput.get());\n\tif (!secondStream) {\n\t\treturn {};\n\t}\n\n\tauto outputWrap = WriteBytesWrap();\n\tauto output = MakeWriteFormatPointer(\n\t\tstatic_cast<void*>(&outputWrap),\n\t\tnullptr,\n\t\t&WriteBytesWrap::Write,\n\t\t&WriteBytesWrap::Seek,\n\t\t\"opus\"_q);\n\tif (!output) {\n\t\treturn {};\n\t}\n\tconst auto outStream = avformat_new_stream(output.get(), nullptr);\n\tif (!outStream) {\n\t\tLogError(u\"avformat_new_stream\"_q);\n\t\treturn {};\n\t}\n\n\tauto error = AvErrorWrap(avcodec_parameters_copy(\n\t\toutStream->codecpar,\n\t\tfirstStream->codecpar));\n\tif (error) {\n\t\tLogError(u\"avcodec_parameters_copy\"_q, error);\n\t\treturn {};\n\t}\n\toutStream->codecpar->codec_tag = 0;\n\toutStream->time_base = firstStream->time_base;\n\n\terror = AvErrorWrap(avformat_write_header(output.get(), nullptr));\n\tif (error) {\n\t\tLogError(u\"avformat_write_header\"_q, error);\n\t\treturn {};\n\t}\n\n\tauto offsetPts = int64(0);\n\tauto durationPts = int64(0);\n\tauto lastPts = std::numeric_limits<int64>::min();\n\tauto lastDts = std::numeric_limits<int64>::min();\n\tauto copied = 0;\n\n\tconst auto append = [&](\n\t\t\tnot_null<AVFormatContext*> input,\n\t\t\tnot_null<AVStream*> inStream) {\n\t\tauto firstPts = int64(AV_NOPTS_VALUE);\n\t\tauto firstDts = int64(AV_NOPTS_VALUE);\n\t\tauto sourceEndPts = offsetPts;\n\n\t\tauto packet = AVPacket();\n\t\tav_init_packet(&packet);\n\t\twhile (true) {\n\t\t\tauto error = AvErrorWrap(av_read_frame(input, &packet));\n\t\t\tif (error.code() == AVERROR_EOF) {\n\t\t\t\tbreak;\n\t\t\t} else if (error) {\n\t\t\t\tLogError(u\"av_read_frame\"_q, error);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto guard = gsl::finally([&] {\n\t\t\t\tav_packet_unref(&packet);\n\t\t\t});\n\t\t\tif (packet.stream_index != inStream->index) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (packet.pts != AV_NOPTS_VALUE) {\n\t\t\t\tif (firstPts == AV_NOPTS_VALUE) {\n\t\t\t\t\tfirstPts = packet.pts;\n\t\t\t\t}\n\t\t\t\tpacket.pts -= firstPts;\n\t\t\t\tif (packet.pts < 0) {\n\t\t\t\t\tpacket.pts = 0;\n\t\t\t\t}\n\t\t\t\tpacket.pts = av_rescale_q(\n\t\t\t\t\tpacket.pts,\n\t\t\t\t\tinStream->time_base,\n\t\t\t\t\toutStream->time_base);\n\t\t\t\tpacket.pts += offsetPts;\n\t\t\t\tif (packet.pts <= lastPts) {\n\t\t\t\t\tpacket.pts = lastPts + 1;\n\t\t\t\t}\n\t\t\t\tlastPts = packet.pts;\n\t\t\t}\n\t\t\tif (packet.dts != AV_NOPTS_VALUE) {\n\t\t\t\tif (firstDts == AV_NOPTS_VALUE) {\n\t\t\t\t\tfirstDts = packet.dts;\n\t\t\t\t}\n\t\t\t\tpacket.dts -= firstDts;\n\t\t\t\tif (packet.dts < 0) {\n\t\t\t\t\tpacket.dts = 0;\n\t\t\t\t}\n\t\t\t\tpacket.dts = av_rescale_q(\n\t\t\t\t\tpacket.dts,\n\t\t\t\t\tinStream->time_base,\n\t\t\t\t\toutStream->time_base);\n\t\t\t\tpacket.dts += offsetPts;\n\t\t\t\tif (packet.dts <= lastDts) {\n\t\t\t\t\tpacket.dts = lastDts + 1;\n\t\t\t\t}\n\t\t\t\tlastDts = packet.dts;\n\t\t\t}\n\t\t\tpacket.duration = (packet.duration > 0)\n\t\t\t\t? av_rescale_q(\n\t\t\t\t\tpacket.duration,\n\t\t\t\t\tinStream->time_base,\n\t\t\t\t\toutStream->time_base)\n\t\t\t\t: 0;\n\n\t\t\tconst auto packetPosition = (packet.pts != AV_NOPTS_VALUE)\n\t\t\t\t? packet.pts\n\t\t\t\t: packet.dts;\n\t\t\tif (packetPosition != AV_NOPTS_VALUE) {\n\t\t\t\tconst auto packetEnd = packetPosition\n\t\t\t\t\t+ std::max(int64(packet.duration), int64());\n\t\t\t\tdurationPts = std::max(durationPts, packetEnd);\n\t\t\t\tsourceEndPts = std::max(sourceEndPts, packetEnd);\n\t\t\t}\n\n\t\t\tpacket.stream_index = outStream->index;\n\t\t\terror = AvErrorWrap(av_interleaved_write_frame(output.get(), &packet));\n\t\t\tif (error) {\n\t\t\t\tLogError(u\"av_interleaved_write_frame\"_q, error);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t++copied;\n\t\t}\n\n\t\toffsetPts = sourceEndPts;\n\t\treturn true;\n\t};\n\tif (!append(firstInput.get(), firstStream)\n\t\t|| !append(secondInput.get(), secondStream)) {\n\t\treturn {};\n\t}\n\tif (!copied) {\n\t\treturn {};\n\t}\n\n\terror = AvErrorWrap(av_write_trailer(output.get()));\n\tif (error) {\n\t\tLogError(u\"av_write_trailer\"_q, error);\n\t\treturn {};\n\t}\n\n\tauto result = AudioEditResult();\n\tresult.content = std::move(outputWrap.content);\n\tresult.waveform = audioCountWaveform(Core::FileLocation(), result.content);\n\tresult.duration = durationPts\n\t\t? PtsToTimeCeil(durationPts, outStream->time_base)\n\t\t: 0;\n\treturn result;\n}\n\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio_edit.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/audio/media_audio.h\"\n\nnamespace Media {\n\nstruct AudioEditResult {\n\tQByteArray content;\n\tVoiceWaveform waveform;\n\tcrl::time duration = 0;\n};\n\n[[nodiscard]] AudioEditResult TrimAudioToRange(\n\tconst QByteArray &content,\n\tcrl::time from,\n\tcrl::time till);\n\n[[nodiscard]] AudioEditResult ConcatAudio(\n\tconst QByteArray &first,\n\tconst QByteArray &second);\n\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio_ffmpeg_loader.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/audio/media_audio_ffmpeg_loader.h\"\n\n#include \"base/bytes.h\"\n#include \"core/file_location.h\"\n#include \"ffmpeg/ffmpeg_utility.h\"\n#include \"media/media_common.h\"\n\nextern \"C\" {\n#include <libavfilter/buffersink.h>\n#include <libavfilter/buffersrc.h>\n} // extern \"C\"\n\nnamespace Media {\nnamespace {\n\nusing FFmpeg::AvErrorWrap;\nusing FFmpeg::LogError;\n\n} // namespace\n\nint64 AbstractFFMpegLoader::Mul(int64 value, AVRational rational) {\n\treturn value * rational.num / rational.den;\n}\n\nbool AbstractFFMpegLoader::open(crl::time positionMs, float64 speed) {\n\tif (!AudioPlayerLoader::openFile()) {\n\t\treturn false;\n\t}\n\n\tioBuffer = (uchar *)av_malloc(FFmpeg::kAVBlockSize);\n\tif (!_data.isEmpty()) {\n\t\tioContext = avio_alloc_context(ioBuffer, FFmpeg::kAVBlockSize, 0, reinterpret_cast<void *>(this), &AbstractFFMpegLoader::ReadData, 0, &AbstractFFMpegLoader::SeekData);\n\t} else if (!_bytes.empty()) {\n\t\tioContext = avio_alloc_context(ioBuffer, FFmpeg::kAVBlockSize, 0, reinterpret_cast<void *>(this), &AbstractFFMpegLoader::ReadBytes, 0, &AbstractFFMpegLoader::SeekBytes);\n\t} else {\n\t\tioContext = avio_alloc_context(ioBuffer, FFmpeg::kAVBlockSize, 0, reinterpret_cast<void *>(this), &AbstractFFMpegLoader::ReadFile, 0, &AbstractFFMpegLoader::SeekFile);\n\t}\n\tfmtContext = avformat_alloc_context();\n\tif (!fmtContext) {\n\t\tLogError(u\"avformat_alloc_context\"_q);\n\t\treturn false;\n\t}\n\tfmtContext->pb = ioContext;\n\n\tif (AvErrorWrap error = avformat_open_input(&fmtContext, 0, 0, 0)) {\n\t\tioBuffer = nullptr;\n\t\tLogError(u\"avformat_open_input\"_q, error);\n\t\treturn false;\n\t}\n\t_opened = true;\n\n\tif (AvErrorWrap error = avformat_find_stream_info(fmtContext, 0)) {\n\t\tLogError(u\"avformat_find_stream_info\"_q, error);\n\t\treturn false;\n\t}\n\n\tstreamId = av_find_best_stream(fmtContext, AVMEDIA_TYPE_AUDIO, -1, -1, &codec, 0);\n\tif (streamId < 0) {\n\t\tFFmpeg::LogError(u\"av_find_best_stream\"_q, AvErrorWrap(streamId));\n\t\treturn false;\n\t}\n\n\tconst auto stream = fmtContext->streams[streamId];\n\tconst auto params = stream->codecpar;\n\t_samplesFrequency = params->sample_rate;\n\tif (stream->duration != AV_NOPTS_VALUE) {\n\t\t_duration = Mul(stream->duration * 1000, stream->time_base);\n\t} else {\n\t\t_duration = Mul(fmtContext->duration * 1000, { 1, AV_TIME_BASE });\n\t}\n\t_startedAtSample = (positionMs * _samplesFrequency) / 1000LL;\n\n\treturn true;\n}\n\nAbstractFFMpegLoader::~AbstractFFMpegLoader() {\n\tif (_opened) {\n\t\tavformat_close_input(&fmtContext);\n\t}\n\tif (ioContext) {\n\t\tav_freep(&ioContext->buffer);\n\t\tav_freep(&ioContext);\n\t} else if (ioBuffer) {\n\t\tav_freep(&ioBuffer);\n\t}\n\tif (fmtContext) avformat_free_context(fmtContext);\n}\n\nint AbstractFFMpegLoader::ReadData(void *opaque, uint8_t *buf, int buf_size) {\n\tauto l = reinterpret_cast<AbstractFFMpegLoader *>(opaque);\n\n\tauto nbytes = qMin(l->_data.size() - l->_dataPos, int32(buf_size));\n\tif (nbytes <= 0) {\n\t\treturn AVERROR_EOF;\n\t}\n\n\tmemcpy(buf, l->_data.constData() + l->_dataPos, nbytes);\n\tl->_dataPos += nbytes;\n\treturn nbytes;\n}\n\nint64_t AbstractFFMpegLoader::SeekData(void *opaque, int64_t offset, int whence) {\n\tauto l = reinterpret_cast<AbstractFFMpegLoader *>(opaque);\n\n\tint32 newPos = -1;\n\tswitch (whence) {\n\tcase SEEK_SET: newPos = offset; break;\n\tcase SEEK_CUR: newPos = l->_dataPos + offset; break;\n\tcase SEEK_END: newPos = l->_data.size() + offset; break;\n\tcase AVSEEK_SIZE: {\n\t\t// Special whence for determining filesize without any seek.\n\t\treturn l->_data.size();\n\t} break;\n\t}\n\tif (newPos < 0 || newPos > l->_data.size()) {\n\t\treturn -1;\n\t}\n\tl->_dataPos = newPos;\n\treturn l->_dataPos;\n}\n\nint AbstractFFMpegLoader::ReadBytes(void *opaque, uint8_t *buf, int buf_size) {\n\tauto l = reinterpret_cast<AbstractFFMpegLoader *>(opaque);\n\n\tauto nbytes = qMin(static_cast<int>(l->_bytes.size()) - l->_dataPos, buf_size);\n\tif (nbytes <= 0) {\n\t\treturn AVERROR_EOF;\n\t}\n\n\tmemcpy(buf, l->_bytes.data() + l->_dataPos, nbytes);\n\tl->_dataPos += nbytes;\n\treturn nbytes;\n}\n\nint64_t AbstractFFMpegLoader::SeekBytes(void *opaque, int64_t offset, int whence) {\n\tauto l = reinterpret_cast<AbstractFFMpegLoader *>(opaque);\n\n\tint32 newPos = -1;\n\tswitch (whence) {\n\tcase SEEK_SET: newPos = offset; break;\n\tcase SEEK_CUR: newPos = l->_dataPos + offset; break;\n\tcase SEEK_END: newPos = static_cast<int>(l->_bytes.size()) + offset; break;\n\tcase AVSEEK_SIZE:\n\t{\n\t\t// Special whence for determining filesize without any seek.\n\t\treturn l->_bytes.size();\n\t} break;\n\t}\n\tif (newPos < 0 || newPos > l->_bytes.size()) {\n\t\treturn -1;\n\t}\n\tl->_dataPos = newPos;\n\treturn l->_dataPos;\n}\n\nint AbstractFFMpegLoader::ReadFile(void *opaque, uint8_t *buf, int buf_size) {\n\tauto l = reinterpret_cast<AbstractFFMpegLoader *>(opaque);\n\tint ret = l->_f.read((char *)(buf), buf_size);\n\tswitch (ret) {\n\tcase -1: return AVERROR_EXTERNAL;\n\tcase 0: return AVERROR_EOF;\n\tdefault: return ret;\n\t}\n}\n\nint64_t AbstractFFMpegLoader::SeekFile(void *opaque, int64_t offset, int whence) {\n\tauto l = reinterpret_cast<AbstractFFMpegLoader *>(opaque);\n\n\tswitch (whence) {\n\tcase SEEK_SET: return l->_f.seek(offset) ? l->_f.pos() : -1;\n\tcase SEEK_CUR: return l->_f.seek(l->_f.pos() + offset) ? l->_f.pos() : -1;\n\tcase SEEK_END: return l->_f.seek(l->_f.size() + offset) ? l->_f.pos() : -1;\n\tcase AVSEEK_SIZE:\n\t{\n\t\t// Special whence for determining filesize without any seek.\n\t\treturn l->_f.size();\n\t} break;\n\t}\n\treturn -1;\n}\n\nAbstractAudioFFMpegLoader::AbstractAudioFFMpegLoader(\n\tconst Core::FileLocation &file,\n\tconst QByteArray &data,\n\tbytes::vector &&buffer)\n: AbstractFFMpegLoader(file, data, std::move(buffer))\n, _frame(FFmpeg::MakeFramePointer()) {\n}\n\nvoid AbstractAudioFFMpegLoader::dropFramesTill(int64 samples) {\n\tconst auto isAfter = [&](const EnqueuedFrame &frame) {\n\t\treturn frame.position > samples;\n\t};\n\tconst auto from = begin(_framesQueued);\n\tconst auto after = ranges::find_if(_framesQueued, isAfter);\n\tif (from == after) {\n\t\treturn;\n\t}\n\tconst auto till = after - 1;\n\tconst auto erasing = till - from;\n\tif (erasing > 0) {\n\t\tif (_framesQueuedIndex >= 0) {\n\t\t\tAssert(_framesQueuedIndex >= erasing);\n\t\t\t_framesQueuedIndex -= erasing;\n\t\t}\n\t\t_framesQueued.erase(from, till);\n\t\tif (_framesQueued.empty()) {\n\t\t\t_framesQueuedIndex = -1;\n\t\t}\n\t}\n}\n\nint64 AbstractAudioFFMpegLoader::startReadingQueuedFrames(float64 newSpeed) {\n\tchangeSpeedFilter(newSpeed);\n\tif (_framesQueued.empty()) {\n\t\t_framesQueuedIndex = -1;\n\t\treturn -1;\n\t}\n\t_framesQueuedIndex = 0;\n\treturn _framesQueued.front().position;\n}\n\nbool AbstractAudioFFMpegLoader::initUsingContext(\n\t\tnot_null<AVCodecContext*> context,\n\t\tfloat64 speed) {\n\t_swrSrcSampleFormat = context->sample_fmt;\n\tconst AVChannelLayout mono = AV_CHANNEL_LAYOUT_MONO;\n\tconst AVChannelLayout stereo = AV_CHANNEL_LAYOUT_STEREO;\n\tif (!av_channel_layout_compare(&context->ch_layout, &mono)) {\n\t\tswitch (_swrSrcSampleFormat) {\n\t\tcase AV_SAMPLE_FMT_U8:\n\t\tcase AV_SAMPLE_FMT_U8P:\n\t\t\t_swrDstSampleFormat = _swrSrcSampleFormat;\n\t\t\tav_channel_layout_copy(&_swrDstChannelLayout, &context->ch_layout);\n\t\t\t_outputChannels = 1;\n\t\t\t_outputSampleSize = 1;\n\t\t\t_outputFormat = AL_FORMAT_MONO8;\n\t\t\tbreak;\n\t\tcase AV_SAMPLE_FMT_S16:\n\t\tcase AV_SAMPLE_FMT_S16P:\n\t\t\t_swrDstSampleFormat = _swrSrcSampleFormat;\n\t\t\tav_channel_layout_copy(&_swrDstChannelLayout, &context->ch_layout);\n\t\t\t_outputChannels = 1;\n\t\t\t_outputSampleSize = sizeof(uint16);\n\t\t\t_outputFormat = AL_FORMAT_MONO16;\n\t\t\tbreak;\n\t\t}\n\t} else if (!av_channel_layout_compare(&context->ch_layout, &stereo)) {\n\t\tswitch (_swrSrcSampleFormat) {\n\t\tcase AV_SAMPLE_FMT_U8:\n\t\t\t_swrDstSampleFormat = _swrSrcSampleFormat;\n\t\t\tav_channel_layout_copy(&_swrDstChannelLayout, &context->ch_layout);\n\t\t\t_outputChannels = 2;\n\t\t\t_outputSampleSize = 2;\n\t\t\t_outputFormat = AL_FORMAT_STEREO8;\n\t\t\tbreak;\n\t\tcase AV_SAMPLE_FMT_S16:\n\t\t\t_swrDstSampleFormat = _swrSrcSampleFormat;\n\t\t\tav_channel_layout_copy(&_swrDstChannelLayout, &context->ch_layout);\n\t\t\t_outputChannels = 2;\n\t\t\t_outputSampleSize = 2 * sizeof(uint16);\n\t\t\t_outputFormat = AL_FORMAT_STEREO16;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tcreateSpeedFilter(speed);\n\n\treturn true;\n}\n\nauto AbstractAudioFFMpegLoader::replaceFrameAndRead(\n\tFFmpeg::FramePointer frame)\n-> ReadResult {\n\t_frame = std::move(frame);\n\treturn readFromReadyFrame();\n}\n\nauto AbstractAudioFFMpegLoader::readFromReadyContext(\n\tnot_null<AVCodecContext*> context)\n-> ReadResult {\n\tif (_filterGraph) {\n\t\tAvErrorWrap error = av_buffersink_get_frame(\n\t\t\t_filterSink,\n\t\t\t_filteredFrame.get());\n\t\tif (!error) {\n\t\t\tif (!_filteredFrame->nb_samples) {\n\t\t\t\treturn ReadError::Retry;\n\t\t\t}\n\t\t\treturn bytes::const_span(\n\t\t\t\treinterpret_cast<const bytes::type*>(\n\t\t\t\t\t_filteredFrame->extended_data[0]),\n\t\t\t\t_filteredFrame->nb_samples * _outputSampleSize);\n\t\t} else if (error.code() == AVERROR_EOF) {\n\t\t\treturn ReadError::EndOfFile;\n\t\t} else if (error.code() != AVERROR(EAGAIN)) {\n\t\t\tLogError(u\"av_buffersink_get_frame\"_q, error);\n\t\t\treturn ReadError::Other;\n\t\t}\n\t}\n\tusing Enqueued = not_null<const EnqueuedFrame*>;\n\tconst auto queueResult = fillFrameFromQueued();\n\tif (queueResult == ReadError::RetryNotQueued) {\n\t\treturn ReadError::RetryNotQueued;\n\t} else if (const auto enqueued = std::get_if<Enqueued>(&queueResult)) {\n\t\tconst auto raw = (*enqueued)->frame.get();\n\t\tAssert(frameHasDesiredFormat(raw));\n\t\treturn readOrBufferForFilter(raw, (*enqueued)->samples);\n\t}\n\n\tconst auto queueError = v::get<ReadError>(queueResult);\n\tAvErrorWrap error = (queueError == ReadError::EndOfFile)\n\t\t? AVERROR_EOF\n\t\t: avcodec_receive_frame(context, _frame.get());\n\tif (!error) {\n\t\treturn readFromReadyFrame();\n\t}\n\n\tif (error.code() == AVERROR_EOF) {\n\t\tenqueueFramesFinished();\n\t\tif (!_filterGraph) {\n\t\t\treturn ReadError::EndOfFile;\n\t\t}\n\t\tAvErrorWrap error = av_buffersrc_add_frame(_filterSrc, nullptr);\n\t\tif (!error) {\n\t\t\treturn ReadError::Retry;\n\t\t}\n\t\tLogError(u\"av_buffersrc_add_frame\"_q, error);\n\t\treturn ReadError::Other;\n\t} else if (error.code() != AVERROR(EAGAIN)) {\n\t\tLogError(u\"avcodec_receive_frame\"_q, error);\n\t\treturn ReadError::Other;\n\t}\n\treturn ReadError::Wait;\n}\n\nauto AbstractAudioFFMpegLoader::fillFrameFromQueued()\n-> std::variant<not_null<const EnqueuedFrame*>, ReadError> {\n\tif (_framesQueuedIndex == _framesQueued.size()) {\n\t\t_framesQueuedIndex = -1;\n\t\treturn ReadError::RetryNotQueued;\n\t} else if (_framesQueuedIndex < 0) {\n\t\treturn ReadError::Wait;\n\t}\n\tconst auto &queued = _framesQueued[_framesQueuedIndex];\n\t++_framesQueuedIndex;\n\n\tif (!queued.frame) {\n\t\treturn ReadError::EndOfFile;\n\t}\n\treturn &queued;\n}\n\nbool AbstractAudioFFMpegLoader::frameHasDesiredFormat(\n\t\tnot_null<AVFrame*> frame) const {\n\treturn true\n\t\t&& (frame->format == _swrDstSampleFormat)\n\t\t&& (frame->sample_rate == _swrDstRate)\n\t\t&& !av_channel_layout_compare(\n\t\t\t&frame->ch_layout,\n\t\t\t&_swrDstChannelLayout);\n}\n\nbool AbstractAudioFFMpegLoader::initResampleForFrame() {\n\tif (!_frame->ch_layout.nb_channels) {\n\t\tLOG((\"Audio Error: \"\n\t\t\t\"Unknown channel layout for frame in file '%1', \"\n\t\t\t\"data size '%2'\"\n\t\t\t).arg(_file.name()\n\t\t\t).arg(_data.size()\n\t\t\t));\n\t\treturn false;\n\t} else if (_frame->format == -1) {\n\t\tLOG((\"Audio Error: \"\n\t\t\t\"Unknown frame format in file '%1', data size '%2'\"\n\t\t\t).arg(_file.name()\n\t\t\t).arg(_data.size()\n\t\t\t));\n\t\treturn false;\n\t} else if (_swrContext) {\n\t\tif (true\n\t\t\t&& (_frame->format == _swrSrcSampleFormat)\n\t\t\t&& (_frame->sample_rate == _swrSrcRate)\n\t\t\t&& !av_channel_layout_compare(\n\t\t\t\t&_frame->ch_layout,\n\t\t\t\t&_swrSrcChannelLayout)) {\n\t\t\treturn true;\n\t\t}\n\t\tswr_close(_swrContext);\n\t}\n\n\t_swrSrcSampleFormat = static_cast<AVSampleFormat>(_frame->format);\n\tav_channel_layout_copy(&_swrSrcChannelLayout, &_frame->ch_layout);\n\t_swrSrcRate = _frame->sample_rate;\n\treturn initResampleUsingFormat();\n}\n\nbool AbstractAudioFFMpegLoader::initResampleUsingFormat() {\n\tauto error = swr_alloc_set_opts2(\n\t\t&_swrContext,\n\t\t&_swrDstChannelLayout,\n\t\t_swrDstSampleFormat,\n\t\t_swrDstRate,\n\t\t&_swrSrcChannelLayout,\n\t\t_swrSrcSampleFormat,\n\t\t_swrSrcRate,\n\t\t0,\n\t\tnullptr);\n\tif (error || !_swrContext) {\n\t\tLogError(u\"swr_alloc_set_opts2\"_q, error);\n\t\treturn false;\n\t} else if (AvErrorWrap error = swr_init(_swrContext)) {\n\t\tLogError(u\"swr_init\"_q, error);\n\t\treturn false;\n\t}\n\t_resampledFrame = nullptr;\n\t_resampledFrameCapacity = 0;\n\treturn true;\n}\n\nbool AbstractAudioFFMpegLoader::ensureResampleSpaceAvailable(int samples) {\n\tconst auto enlarge = (_resampledFrameCapacity < samples);\n\tif (!_resampledFrame) {\n\t\t_resampledFrame = FFmpeg::MakeFramePointer();\n\t} else if (enlarge || !av_frame_is_writable(_resampledFrame.get())) {\n\t\tav_frame_unref(_resampledFrame.get());\n\t} else {\n\t\treturn true;\n\t}\n\tconst auto allocate = std::max(samples, int(av_rescale_rnd(\n\t\tFFmpeg::kAVBlockSize / _outputSampleSize,\n\t\t_swrDstRate,\n\t\t_swrSrcRate,\n\t\tAV_ROUND_UP)));\n\t_resampledFrame->sample_rate = _swrDstRate;\n\t_resampledFrame->format = _swrDstSampleFormat;\n\tav_channel_layout_copy(\n\t\t&_resampledFrame->ch_layout,\n\t\t&_swrDstChannelLayout);\n\t_resampledFrame->nb_samples = allocate;\n\tif (AvErrorWrap error = av_frame_get_buffer(_resampledFrame.get(), 0)) {\n\t\tLogError(u\"av_frame_get_buffer\"_q, error);\n\t\treturn false;\n\t}\n\t_resampledFrameCapacity = allocate;\n\treturn true;\n}\n\nbool AbstractAudioFFMpegLoader::changeSpeedFilter(float64 speed) {\n\tspeed = std::clamp(speed, kSpeedMin, kSpeedMax);\n\tif (EqualSpeeds(_filterSpeed, speed)) {\n\t\treturn false;\n\t}\n\tavfilter_graph_free(&_filterGraph);\n\tconst auto guard = gsl::finally([&] {\n\t\tif (!_filterGraph) {\n\t\t\t_filteredFrame = nullptr;\n\t\t\t_filterSpeed = 1.;\n\t\t}\n\t});\n\tcreateSpeedFilter(speed);\n\treturn true;\n}\n\nvoid AbstractAudioFFMpegLoader::createSpeedFilter(float64 speed) {\n\tExpects(!_filterGraph);\n\n\tif (EqualSpeeds(speed, 1.)) {\n\t\treturn;\n\t}\n\tconst auto abuffer = avfilter_get_by_name(\"abuffer\");\n\tconst auto abuffersink = avfilter_get_by_name(\"abuffersink\");\n\tconst auto atempo = avfilter_get_by_name(\"atempo\");\n\tif (!abuffer || !abuffersink || !atempo) {\n\t\tLOG((\"FFmpeg Error: Could not find abuffer / abuffersink /atempo.\"));\n\t\treturn;\n\t}\n\n\tauto graph = avfilter_graph_alloc();\n\tif (!graph) {\n\t\tLOG((\"FFmpeg Error: Unable to create filter graph.\"));\n\t\treturn;\n\t}\n\tconst auto guard = gsl::finally([&] {\n\t\tavfilter_graph_free(&graph);\n\t});\n\n\t_filterSrc = avfilter_graph_alloc_filter(graph, abuffer, \"src\");\n\t_atempo = avfilter_graph_alloc_filter(graph, atempo, \"atempo\");\n\t_filterSink = avfilter_graph_alloc_filter(graph, abuffersink, \"sink\");\n\tif (!_filterSrc || !atempo || !_filterSink) {\n\t\tLOG((\"FFmpeg Error: \"\n\t\t\t\"Could not allocate abuffer / abuffersink /atempo.\"));\n\t\treturn;\n\t}\n\n\tchar layout[64] = { 0 };\n\tav_channel_layout_describe(\n\t\t&_swrDstChannelLayout,\n\t\tlayout,\n\t\tsizeof(layout));\n\n\tav_opt_set(\n\t\t_filterSrc,\n\t\t\"channel_layout\",\n\t\tlayout,\n\t\tAV_OPT_SEARCH_CHILDREN);\n\tav_opt_set_sample_fmt(\n\t\t_filterSrc,\n\t\t\"sample_fmt\",\n\t\t_swrDstSampleFormat,\n\t\tAV_OPT_SEARCH_CHILDREN);\n\tav_opt_set_q(\n\t\t_filterSrc,\n\t\t\"time_base\",\n\t\tAVRational{ 1, _swrDstRate },\n\t\tAV_OPT_SEARCH_CHILDREN);\n\tav_opt_set_int(\n\t\t_filterSrc,\n\t\t\"sample_rate\",\n\t\t_swrDstRate,\n\t\tAV_OPT_SEARCH_CHILDREN);\n\tav_opt_set_double(\n\t\t_atempo,\n\t\t\"tempo\",\n\t\tspeed,\n\t\tAV_OPT_SEARCH_CHILDREN);\n\n\tAvErrorWrap error = 0;\n\tif ((error = avfilter_init_str(_filterSrc, nullptr))) {\n\t\tLogError(u\"avfilter_init_str(src)\"_q, error);\n\t\treturn;\n\t} else if ((error = avfilter_init_str(_atempo, nullptr))) {\n\t\tLogError(u\"avfilter_init_str(atempo)\"_q, error);\n\t\tavfilter_graph_free(&graph);\n\t\treturn;\n\t} else if ((error = avfilter_init_str(_filterSink, nullptr))) {\n\t\tLogError(u\"avfilter_init_str(sink)\"_q, error);\n\t\tavfilter_graph_free(&graph);\n\t\treturn;\n\t} else if ((error = avfilter_link(_filterSrc, 0, _atempo, 0))) {\n\t\tLogError(u\"avfilter_link(src->atempo)\"_q, error);\n\t\tavfilter_graph_free(&graph);\n\t\treturn;\n\t} else if ((error = avfilter_link(_atempo, 0, _filterSink, 0))) {\n\t\tLogError(u\"avfilter_link(atempo->sink)\"_q, error);\n\t\tavfilter_graph_free(&graph);\n\t\treturn;\n\t} else if ((error = avfilter_graph_config(graph, nullptr))) {\n\t\tLogError(\"avfilter_link(atempo->sink)\"_q, error);\n\t\tavfilter_graph_free(&graph);\n\t\treturn;\n\t}\n\t_filterGraph = base::take(graph);\n\t_filteredFrame = FFmpeg::MakeFramePointer();\n\t_filterSpeed = speed;\n}\n\nvoid AbstractAudioFFMpegLoader::enqueueNormalFrame(\n\t\tnot_null<AVFrame*> frame,\n\t\tint64 samples) {\n\tif (_framesQueuedIndex >= 0) {\n\t\treturn;\n\t}\n\tif (!samples) {\n\t\tsamples = frame->nb_samples;\n\t}\n\t_framesQueued.push_back({\n\t\t.position = startedAtSample() + _framesQueuedSamples,\n\t\t.samples = samples,\n\t\t.frame = FFmpeg::DuplicateFramePointer(frame),\n\t});\n\t_framesQueuedSamples += samples;\n}\n\nvoid AbstractAudioFFMpegLoader::enqueueFramesFinished() {\n\tif (_framesQueuedIndex >= 0) {\n\t\treturn;\n\t}\n\t_framesQueued.push_back({\n\t\t.position = startedAtSample() + _framesQueuedSamples,\n\t});\n}\n\nauto AbstractAudioFFMpegLoader::readFromReadyFrame()\n-> ReadResult {\n\tconst auto raw = _frame.get();\n\tif (frameHasDesiredFormat(raw)) {\n\t\tif (!raw->nb_samples) {\n\t\t\treturn ReadError::Retry;\n\t\t}\n\t\treturn readOrBufferForFilter(raw, raw->nb_samples);\n\t} else if (!initResampleForFrame()) {\n\t\treturn ReadError::Other;\n\t}\n\n\tconst auto maxSamples = av_rescale_rnd(\n\t\tswr_get_delay(_swrContext, _swrSrcRate) + _frame->nb_samples,\n\t\t_swrDstRate,\n\t\t_swrSrcRate,\n\t\tAV_ROUND_UP);\n\tif (!ensureResampleSpaceAvailable(maxSamples)) {\n\t\treturn ReadError::Other;\n\t}\n\tconst auto samples = swr_convert(\n\t\t_swrContext,\n\t\t(uint8_t**)_resampledFrame->extended_data,\n\t\tmaxSamples,\n\t\t(const uint8_t **)_frame->extended_data,\n\t\t_frame->nb_samples);\n\tif (AvErrorWrap error = samples) {\n\t\tLogError(u\"swr_convert\"_q, error);\n\t\treturn ReadError::Other;\n\t} else if (!samples) {\n\t\treturn ReadError::Retry;\n\t}\n\treturn readOrBufferForFilter(_resampledFrame.get(), samples);\n}\n\nauto AbstractAudioFFMpegLoader::readOrBufferForFilter(\n\tnot_null<AVFrame*> frame,\n\tint64 samplesOverride)\n-> ReadResult {\n\tenqueueNormalFrame(frame, samplesOverride);\n\n\tconst auto was = frame->nb_samples;\n\tframe->nb_samples = samplesOverride;\n\tconst auto guard = gsl::finally([&] {\n\t\tframe->nb_samples = was;\n\t});\n\n\tif (!_filterGraph) {\n\t\treturn bytes::const_span(\n\t\t\treinterpret_cast<const bytes::type*>(frame->extended_data[0]),\n\t\t\tframe->nb_samples * _outputSampleSize);\n\t}\n\tAvErrorWrap error = av_buffersrc_add_frame_flags(\n\t\t_filterSrc,\n\t\tframe,\n\t\tAV_BUFFERSRC_FLAG_KEEP_REF);\n\tif (error) {\n\t\tLogError(u\"av_buffersrc_add_frame_flags\"_q, error);\n\t\treturn ReadError::Other;\n\t}\n\treturn ReadError::Retry;\n}\n\nAbstractAudioFFMpegLoader::~AbstractAudioFFMpegLoader() {\n\tif (_filterGraph) {\n\t\tavfilter_graph_free(&_filterGraph);\n\t}\n\tif (_swrContext) {\n\t\tswr_free(&_swrContext);\n\t}\n}\n\nFFMpegLoader::FFMpegLoader(\n\tconst Core::FileLocation &file,\n\tconst QByteArray &data,\n\tbytes::vector &&buffer)\n: AbstractAudioFFMpegLoader(file, data, std::move(buffer)) {\n}\n\nbool FFMpegLoader::open(crl::time positionMs, float64 speed) {\n\treturn AbstractFFMpegLoader::open(positionMs)\n\t\t&& openCodecContext()\n\t\t&& initUsingContext(_codecContext, speed)\n\t\t&& seekTo(positionMs);\n}\n\nbool FFMpegLoader::openCodecContext() {\n\t_codecContext = avcodec_alloc_context3(nullptr);\n\tif (!_codecContext) {\n\t\tLOG((\"Audio Error: \"\n\t\t\t\"Unable to avcodec_alloc_context3 for file '%1', data size '%2'\"\n\t\t\t).arg(_file.name()\n\t\t\t).arg(_data.size()\n\t\t\t));\n\t\treturn false;\n\t}\n\n\tconst auto stream = fmtContext->streams[streamId];\n\tAvErrorWrap error = avcodec_parameters_to_context(\n\t\t_codecContext,\n\t\tstream->codecpar);\n\tif (error) {\n\t\tLogError(u\"avcodec_parameters_to_context\"_q, error);\n\t\treturn false;\n\t}\n\t_codecContext->pkt_timebase = stream->time_base;\n\tav_opt_set_int(_codecContext, \"refcounted_frames\", 1, 0);\n\n\tif (AvErrorWrap error = avcodec_open2(_codecContext, codec, 0)) {\n\t\tLogError(u\"avcodec_open2\"_q, error);\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nbool FFMpegLoader::seekTo(crl::time positionMs) {\n\tif (positionMs) {\n\t\tconst auto stream = fmtContext->streams[streamId];\n\t\tconst auto timeBase = stream->time_base;\n\t\tconst auto timeStamp = (positionMs * timeBase.den)\n\t\t\t/ (1000LL * timeBase.num);\n\t\tconst auto flags1 = AVSEEK_FLAG_ANY;\n\t\tif (av_seek_frame(fmtContext, streamId, timeStamp, flags1) < 0) {\n\t\t\tconst auto flags2 = 0;\n\t\t\tif (av_seek_frame(fmtContext, streamId, timeStamp, flags2) < 0) {\n\t\t\t}\n\t\t}\n\t}\n\n\treturn true;\n}\n\nFFMpegLoader::ReadResult FFMpegLoader::readMore() {\n\tif (_readTillEnd) {\n\t\treturn ReadError::EndOfFile;\n\t}\n\tconst auto readResult = readFromReadyContext(_codecContext);\n\tif (readResult != ReadError::Wait) {\n\t\tif (readResult == ReadError::EndOfFile) {\n\t\t\t_readTillEnd = true;\n\t\t}\n\t\treturn readResult;\n\t}\n\n\tif (AvErrorWrap error = av_read_frame(fmtContext, &_packet)) {\n\t\tif (error.code() != AVERROR_EOF) {\n\t\t\tLogError(u\"av_read_frame\"_q, error);\n\t\t\treturn ReadError::Other;\n\t\t}\n\t\terror = avcodec_send_packet(_codecContext, nullptr); // drain\n\t\tif (!error) {\n\t\t\treturn ReadError::Retry;\n\t\t}\n\t\tLogError(u\"avcodec_send_packet\"_q, error);\n\t\treturn ReadError::Other;\n\t}\n\n\tif (_packet.stream_index == streamId) {\n\t\tAvErrorWrap error = avcodec_send_packet(_codecContext, &_packet);\n\t\tif (error) {\n\t\t\tav_packet_unref(&_packet);\n\t\t\tLogError(u\"avcodec_send_packet\"_q, error);\n\t\t\t// There is a sample voice message where skipping such packet\n\t\t\t// results in a crash (read_access to nullptr) in swr_convert().\n\t\t\t//if (error.code() == AVERROR_INVALIDDATA) {\n\t\t\t//\treturn ReadResult::Retry; // try to skip bad packet\n\t\t\t//}\n\t\t\treturn ReadError::Other;\n\t\t}\n\t}\n\tav_packet_unref(&_packet);\n\treturn ReadError::Retry;\n}\n\nFFMpegLoader::~FFMpegLoader() {\n\tif (_codecContext) {\n\t\tavcodec_free_context(&_codecContext);\n\t}\n}\n\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio_ffmpeg_loader.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/audio/media_audio.h\"\n#include \"media/audio/media_audio_loader.h\"\n#include \"media/streaming/media_streaming_utility.h\"\n\nextern \"C\" {\n#include <libavcodec/avcodec.h>\n#include <libavformat/avformat.h>\n#include <libavutil/opt.h>\n#include <libswresample/swresample.h>\n#include <libavfilter/avfilter.h>\n} // extern \"C\"\n\n#include <al.h>\n\nnamespace Core {\nclass FileLocation;\n} // namespace Core\n\nnamespace Media {\n\nclass AbstractFFMpegLoader : public AudioPlayerLoader {\npublic:\n\tAbstractFFMpegLoader(\n\t\tconst Core::FileLocation &file,\n\t\tconst QByteArray &data,\n\t\tbytes::vector &&buffer)\n\t: AudioPlayerLoader(file, data, std::move(buffer)) {\n\t}\n\n\tbool open(crl::time positionMs, float64 speed = 1.) override;\n\n\tcrl::time duration() override {\n\t\treturn _duration;\n\t}\n\tvoid overrideDuration(int64 startedAtSample, crl::time duration) {\n\t\t_startedAtSample = startedAtSample;\n\t\t_duration = duration;\n\t}\n\n\tint samplesFrequency() override {\n\t\treturn _samplesFrequency;\n\t}\n\n\t[[nodiscard]] int64 startedAtSample() const {\n\t\treturn _startedAtSample;\n\t}\n\n\t~AbstractFFMpegLoader();\n\nprotected:\n\tstatic int64 Mul(int64 value, AVRational rational);\n\n\tint _samplesFrequency = Media::Player::kDefaultFrequency;\n\tint64 _startedAtSample = 0;\n\tcrl::time _duration = 0;\n\n\tuchar *ioBuffer = nullptr;\n\tAVIOContext *ioContext = nullptr;\n\tAVFormatContext *fmtContext = nullptr;\n\tconst AVCodec *codec = nullptr;\n\tint32 streamId = 0;\n\n\tbool _opened = false;\n\nprivate:\n\tstatic int ReadData(void *opaque, uint8_t *buf, int buf_size);\n\tstatic int64_t SeekData(void *opaque, int64_t offset, int whence);\n\tstatic int ReadBytes(void *opaque, uint8_t *buf, int buf_size);\n\tstatic int64_t SeekBytes(void *opaque, int64_t offset, int whence);\n\tstatic int ReadFile(void *opaque, uint8_t *buf, int buf_size);\n\tstatic int64_t SeekFile(void *opaque, int64_t offset, int whence);\n\n};\n\nclass AbstractAudioFFMpegLoader : public AbstractFFMpegLoader {\npublic:\n\tAbstractAudioFFMpegLoader(\n\t\tconst Core::FileLocation &file,\n\t\tconst QByteArray &data,\n\t\tbytes::vector &&buffer);\n\n\tvoid dropFramesTill(int64 samples) override;\n\tint64 startReadingQueuedFrames(float64 newSpeed) override;\n\n\tint samplesFrequency() override {\n\t\treturn _swrDstRate;\n\t}\n\n\tint sampleSize() override {\n\t\treturn _outputSampleSize;\n\t}\n\n\tint format() override {\n\t\treturn _outputFormat;\n\t}\n\n\t~AbstractAudioFFMpegLoader();\n\nprotected:\n\tbool initUsingContext(not_null<AVCodecContext*> context, float64 speed);\n\t[[nodiscard]] ReadResult readFromReadyContext(\n\t\tnot_null<AVCodecContext*> context);\n\n\t// Streaming player provides the first frame to the ChildFFMpegLoader\n\t// so we replace our allocated frame with the one provided.\n\t[[nodiscard]] ReadResult replaceFrameAndRead(FFmpeg::FramePointer frame);\n\nprivate:\n\tstruct EnqueuedFrame {\n\t\tint64 position = 0;\n\t\tint64 samples = 0;\n\t\tFFmpeg::FramePointer frame;\n\t};\n\t[[nodiscard]] ReadResult readFromReadyFrame();\n\t[[nodiscard]] ReadResult readOrBufferForFilter(\n\t\tnot_null<AVFrame*> frame,\n\t\tint64 samplesOverride);\n\tbool frameHasDesiredFormat(not_null<AVFrame*> frame) const;\n\tbool initResampleForFrame();\n\tbool initResampleUsingFormat();\n\tbool ensureResampleSpaceAvailable(int samples);\n\n\tbool changeSpeedFilter(float64 speed);\n\tvoid createSpeedFilter(float64 speed);\n\n\tvoid enqueueNormalFrame(\n\t\tnot_null<AVFrame*> frame,\n\t\tint64 samples = 0);\n\tvoid enqueueFramesFinished();\n\t[[nodiscard]] auto fillFrameFromQueued()\n\t\t-> std::variant<not_null<const EnqueuedFrame*>, ReadError>;\n\n\tFFmpeg::FramePointer _frame;\n\tFFmpeg::FramePointer _resampledFrame;\n\tFFmpeg::FramePointer _filteredFrame;\n\tint _resampledFrameCapacity = 0;\n\n\tint64 _framesQueuedSamples = 0;\n\tstd::deque<EnqueuedFrame> _framesQueued;\n\tint _framesQueuedIndex = -1;\n\n\tint _outputFormat = AL_FORMAT_STEREO16;\n\tint _outputChannels = 2;\n\tint _outputSampleSize = 2 * sizeof(uint16);\n\n\tSwrContext *_swrContext = nullptr;\n\n\tint _swrSrcRate = 0;\n\tAVSampleFormat _swrSrcSampleFormat = AV_SAMPLE_FMT_NONE;\n\n\tconst int _swrDstRate = Media::Player::kDefaultFrequency;\n\tAVSampleFormat _swrDstSampleFormat = AV_SAMPLE_FMT_S16;\n\n\tAVChannelLayout _swrSrcChannelLayout = AV_CHANNEL_LAYOUT_STEREO;\n\tAVChannelLayout _swrDstChannelLayout = AV_CHANNEL_LAYOUT_STEREO;\n\n\tAVFilterGraph *_filterGraph = nullptr;\n\tfloat64 _filterSpeed = 1.;\n\tAVFilterContext *_filterSrc = nullptr;\n\tAVFilterContext *_atempo = nullptr;\n\tAVFilterContext *_filterSink = nullptr;\n\n};\n\nclass FFMpegLoader : public AbstractAudioFFMpegLoader {\npublic:\n\tFFMpegLoader(\n\t\tconst Core::FileLocation &file,\n\t\tconst QByteArray &data,\n\t\tbytes::vector &&buffer);\n\n\tbool open(crl::time positionMs, float64 speed = 1.) override;\n\n\tReadResult readMore() override;\n\n\t~FFMpegLoader();\n\nprivate:\n\tbool openCodecContext();\n\tbool seekTo(crl::time positionMs);\n\n\tAVCodecContext *_codecContext = nullptr;\n\tAVPacket _packet;\n\tbool _readTillEnd = false;\n\n};\n\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio_loader.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/audio/media_audio_loader.h\"\n\nnamespace Media {\n\nAudioPlayerLoader::AudioPlayerLoader(\n\tconst Core::FileLocation &file,\n\tconst QByteArray &data,\n\tbytes::vector &&buffer)\n: _file(file)\n, _data(data)\n, _bytes(std::move(buffer)) {\n}\n\nAudioPlayerLoader::~AudioPlayerLoader() {\n\tif (_access) {\n\t\t_file.accessDisable();\n\t\t_access = false;\n\t}\n}\n\nbool AudioPlayerLoader::check(\n\t\tconst Core::FileLocation &file,\n\t\tconst QByteArray &data) {\n\treturn (this->_file == file) && (this->_data.size() == data.size());\n}\n\nvoid AudioPlayerLoader::saveDecodedSamples(not_null<QByteArray*> samples) {\n\tExpects(_savedSamples.isEmpty());\n\tExpects(!_holdsSavedSamples);\n\n\tsamples->swap(_savedSamples);\n\t_holdsSavedSamples = true;\n}\n\nvoid AudioPlayerLoader::takeSavedDecodedSamples(\n\t\tnot_null<QByteArray*> samples) {\n\tExpects(samples->isEmpty());\n\tExpects(_holdsSavedSamples);\n\n\tsamples->swap(_savedSamples);\n\t_holdsSavedSamples = false;\n}\n\nbool AudioPlayerLoader::holdsSavedDecodedSamples() const {\n\treturn _holdsSavedSamples;\n}\n\nvoid AudioPlayerLoader::dropDecodedSamples() {\n\t_savedSamples = {};\n\t_holdsSavedSamples = false;\n}\n\nint AudioPlayerLoader::bytesPerBuffer() {\n\tif (!_bytesPerBuffer) {\n\t\t_bytesPerBuffer = samplesFrequency() * sampleSize();\n\t}\n\treturn _bytesPerBuffer;\n}\n\nbool AudioPlayerLoader::openFile() {\n\tif (_data.isEmpty() && _bytes.empty()) {\n\t\tif (_f.isOpen()) _f.close();\n\t\tif (!_access) {\n\t\t\tif (!_file.accessEnable()) {\n\t\t\t\tLOG((\"Audio Error: could not open file access '%1', \"\n\t\t\t\t\t\"data size '%2', error %3, %4\"\n\t\t\t\t\t).arg(_file.name()\n\t\t\t\t\t).arg(_data.size()\n\t\t\t\t\t).arg(_f.error()\n\t\t\t\t\t).arg(_f.errorString()));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t_access = true;\n\t\t}\n\t\t_f.setFileName(_file.name());\n\t\tif (!_f.open(QIODevice::ReadOnly)) {\n\t\t\tLOG((\"Audio Error: could not open file '%1', \"\n\t\t\t\t\"data size '%2', error %3, %4\"\n\t\t\t\t).arg(_file.name()\n\t\t\t\t).arg(_data.size()\n\t\t\t\t).arg(_f.error()\n\t\t\t\t).arg(_f.errorString()));\n\t\t\treturn false;\n\t\t}\n\t}\n\t_dataPos = 0;\n\treturn true;\n}\n\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio_loader.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/bytes.h\"\n#include \"core/file_location.h\"\n#include \"media/streaming/media_streaming_utility.h\"\n\nnamespace Media {\n\nclass AudioPlayerLoader {\npublic:\n\tAudioPlayerLoader(\n\t\tconst Core::FileLocation &file,\n\t\tconst QByteArray &data,\n\t\tbytes::vector &&buffer);\n\tvirtual ~AudioPlayerLoader();\n\n\tvirtual bool check(\n\t\tconst Core::FileLocation &file,\n\t\tconst QByteArray &data);\n\n\tvirtual bool open(crl::time positionMs, float64 speed = 1.) = 0;\n\tvirtual crl::time duration() = 0;\n\tvirtual int samplesFrequency() = 0;\n\tvirtual int sampleSize() = 0;\n\tvirtual int format() = 0;\n\n\tvirtual void dropFramesTill(int64 samples) {\n\t}\n\t[[nodiscard]] virtual int64 startReadingQueuedFrames(float64 newSpeed) {\n\t\tUnexpected(\n\t\t\t\"startReadingQueuedFrames() on not AbstractAudioFFMpegLoader\");\n\t}\n\n\t[[nodiscard]] int bytesPerBuffer();\n\n\tenum class ReadError {\n\t\tOther,\n\t\tRetry,\n\t\tRetryNotQueued,\n\t\tWait,\n\t\tEndOfFile,\n\t};\n\tusing ReadResult = std::variant<bytes::const_span, ReadError>;\n\t[[nodiscard]] virtual ReadResult readMore() = 0;\n\n\tvirtual void enqueuePackets(std::deque<FFmpeg::Packet> &&packets) {\n\t\tUnexpected(\"enqueuePackets() call on not ChildFFMpegLoader.\");\n\t}\n\tvirtual void setForceToBuffer(bool force) {\n\t\tUnexpected(\"setForceToBuffer() call on not ChildFFMpegLoader.\");\n\t}\n\tvirtual bool forceToBuffer() const {\n\t\treturn false;\n\t}\n\n\tvoid saveDecodedSamples(not_null<QByteArray*> samples);\n\tvoid takeSavedDecodedSamples(not_null<QByteArray*> samples);\n\tbool holdsSavedDecodedSamples() const;\n\tvoid dropDecodedSamples();\n\nprotected:\n\tCore::FileLocation _file;\n\tbool _access = false;\n\tQByteArray _data;\n\tbytes::vector _bytes;\n\n\tQFile _f;\n\tint _dataPos = 0;\n\n\tbool openFile();\n\nprivate:\n\tQByteArray _savedSamples;\n\tbool _holdsSavedSamples = false;\n\n\tint _bytesPerBuffer = 0;\n\n};\n\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio_loaders.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/audio/media_audio_loaders.h\"\n\n#include \"media/audio/media_audio.h\"\n#include \"media/audio/media_audio_ffmpeg_loader.h\"\n#include \"media/audio/media_child_ffmpeg_loader.h\"\n#include \"media/media_common.h\"\n\nnamespace Media {\nnamespace Player {\n\nLoaders::Loaders(QThread *thread)\n: _fromExternalNotify([=] { videoSoundAdded(); }) {\n\tmoveToThread(thread);\n\t_fromExternalNotify.moveToThread(thread);\n\tconnect(thread, SIGNAL(started()), this, SLOT(onInit()));\n\tconnect(thread, SIGNAL(finished()), this, SLOT(deleteLater()));\n}\n\nvoid Loaders::feedFromExternal(ExternalSoundPart &&part) {\n\tauto invoke = false;\n\t{\n\t\tQMutexLocker lock(&_fromExternalMutex);\n\t\tinvoke = _fromExternalQueues.empty()\n\t\t\t&& _fromExternalForceToBuffer.empty();\n\t\tauto &queue = _fromExternalQueues[part.audio];\n\t\tqueue.insert(\n\t\t\tend(queue),\n\t\t\tstd::make_move_iterator(part.packets.begin()),\n\t\t\tstd::make_move_iterator(part.packets.end()));\n\t}\n\tif (invoke) {\n\t\t_fromExternalNotify.call();\n\t}\n}\n\nvoid Loaders::forceToBufferExternal(const AudioMsgId &audioId) {\n\tauto invoke = false;\n\t{\n\t\tQMutexLocker lock(&_fromExternalMutex);\n\t\tinvoke = _fromExternalQueues.empty()\n\t\t\t&& _fromExternalForceToBuffer.empty();\n\t\t_fromExternalForceToBuffer.emplace(audioId);\n\t}\n\tif (invoke) {\n\t\t_fromExternalNotify.call();\n\t}\n}\n\nvoid Loaders::videoSoundAdded() {\n\tauto queues = decltype(_fromExternalQueues)();\n\tauto forces = decltype(_fromExternalForceToBuffer)();\n\t{\n\t\tQMutexLocker lock(&_fromExternalMutex);\n\t\tqueues = base::take(_fromExternalQueues);\n\t\tforces = base::take(_fromExternalForceToBuffer);\n\t}\n\tfor (const auto &audioId : forces) {\n\t\tconst auto tryLoader = [&](const auto &id, auto &loader) {\n\t\t\tif (audioId == id && loader) {\n\t\t\t\tloader->setForceToBuffer(true);\n\t\t\t\tif (loader->holdsSavedDecodedSamples()\n\t\t\t\t\t&& !queues.contains(audioId)) {\n\t\t\t\t\tloadData(audioId);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\t\ttryLoader(_audio, _audioLoader)\n\t\t\t|| tryLoader(_song, _songLoader)\n\t\t\t|| tryLoader(_video, _videoLoader);\n\t}\n\tfor (auto &pair : queues) {\n\t\tconst auto audioId = pair.first;\n\t\tauto &packets = pair.second;\n\t\tconst auto tryLoader = [&](const auto &id, auto &loader) {\n\t\t\tif (id == audioId && loader) {\n\t\t\t\tloader->enqueuePackets(std::move(packets));\n\t\t\t\tif (loader->holdsSavedDecodedSamples()) {\n\t\t\t\t\tloadData(audioId);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\t\ttryLoader(_audio, _audioLoader)\n\t\t\t|| tryLoader(_song, _songLoader)\n\t\t\t|| tryLoader(_video, _videoLoader);\n\t}\n}\n\nvoid Loaders::onInit() {\n}\n\nvoid Loaders::onStart(const AudioMsgId &audio, qint64 positionMs) {\n\tauto type = audio.type();\n\tclear(type);\n\t{\n\t\tQMutexLocker lock(internal::audioPlayerMutex());\n\t\tif (!mixer()) return;\n\n\t\tauto track = mixer()->trackForType(type);\n\t\tif (!track) return;\n\n\t\ttrack->loading = true;\n\t}\n\n\tloadData(audio, positionMs);\n}\n\nAudioMsgId Loaders::clear(AudioMsgId::Type type) {\n\tAudioMsgId result;\n\tswitch (type) {\n\tcase AudioMsgId::Type::Voice:\n\t\tstd::swap(result, _audio);\n\t\t_audioLoader = nullptr;\n\t\tbreak;\n\tcase AudioMsgId::Type::Song:\n\t\tstd::swap(result, _song);\n\t\t_songLoader = nullptr;\n\t\tbreak;\n\tcase AudioMsgId::Type::Video:\n\t\tstd::swap(result, _video);\n\t\t_videoLoader = nullptr;\n\t\tbreak;\n\t}\n\treturn result;\n}\n\nvoid Loaders::setStoppedState(Mixer::Track *track, State state) {\n\tmixer()->setStoppedState(track, state);\n}\n\nvoid Loaders::emitError(AudioMsgId::Type type) {\n\terror(clear(type));\n}\n\nvoid Loaders::onLoad(const AudioMsgId &audio) {\n\tloadData(audio);\n}\n\nvoid Loaders::loadData(AudioMsgId audio, crl::time positionMs) {\n\tauto type = audio.type();\n\tauto setup = setupLoader(audio, positionMs);\n\tconst auto l = setup.loader;\n\tif (!l) {\n\t\tif (setup.errorAtStart) {\n\t\t\temitError(type);\n\t\t}\n\t\treturn;\n\t}\n\n\tconst auto sampleSize = l->sampleSize();\n\tconst auto speedChanged = !EqualSpeeds(setup.newSpeed, setup.oldSpeed);\n\tauto updatedWithSpeed = speedChanged\n\t\t? rebufferOnSpeedChange(setup)\n\t\t: std::optional<Mixer::Track::WithSpeed>();\n\tif (!speedChanged && setup.oldSpeed > 0.) {\n\t\tconst auto normalPosition = Mixer::Track::SpeedIndependentPosition(\n\t\t\tsetup.position,\n\t\t\tsetup.oldSpeed);\n\t\tl->dropFramesTill(normalPosition);\n\t}\n\n\tconst auto started = setup.justStarted;\n\tauto finished = false;\n\tauto waiting = false;\n\tauto errAtStart = started;\n\n\tauto accumulated = QByteArray();\n\tauto accumulatedCount = 0;\n\tif (l->holdsSavedDecodedSamples()) {\n\t\tl->takeSavedDecodedSamples(&accumulated);\n\t\taccumulatedCount = accumulated.size() / sampleSize;\n\t}\n\tconst auto accumulateTill = l->bytesPerBuffer();\n\twhile (accumulated.size() < accumulateTill) {\n\t\tusing Error = AudioPlayerLoader::ReadError;\n\t\tconst auto result = l->readMore();\n\t\tif (result == Error::Retry) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto sampleBytes = v::is<bytes::const_span>(result)\n\t\t\t? v::get<bytes::const_span>(result)\n\t\t\t: bytes::const_span();\n\t\tif (!sampleBytes.empty()) {\n\t\t\taccumulated.append(\n\t\t\t\treinterpret_cast<const char*>(sampleBytes.data()),\n\t\t\t\tsampleBytes.size());\n\t\t\taccumulatedCount += sampleBytes.size() / sampleSize;\n\t\t} else if (result == Error::Other) {\n\t\t\tif (errAtStart) {\n\t\t\t\t{\n\t\t\t\t\tQMutexLocker lock(internal::audioPlayerMutex());\n\t\t\t\t\tif (auto track = checkLoader(type)) {\n\t\t\t\t\t\ttrack->state.state = State::StoppedAtStart;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\temitError(type);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfinished = true;\n\t\t\tbreak;\n\t\t} else if (result == Error::EndOfFile) {\n\t\t\tfinished = true;\n\t\t\tbreak;\n\t\t} else if (result == Error::Wait) {\n\t\t\twaiting = (accumulated.size() < accumulateTill)\n\t\t\t\t&& (accumulated.isEmpty() || !l->forceToBuffer());\n\t\t\tif (waiting) {\n\t\t\t\tl->saveDecodedSamples(&accumulated);\n\t\t\t}\n\t\t\tbreak;\n\t\t} else if (v::is<bytes::const_span>(result)) {\n\t\t\terrAtStart = false;\n\t\t}\n\n\t\tQMutexLocker lock(internal::audioPlayerMutex());\n\t\tif (!checkLoader(type)) {\n\t\t\tclear(type);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tQMutexLocker lock(internal::audioPlayerMutex());\n\tauto track = checkLoader(type);\n\tif (!track) {\n\t\tclear(type);\n\t\treturn;\n\t}\n\n\tif (started || !accumulated.isEmpty() || updatedWithSpeed) {\n\t\tAudio::AttachToDevice();\n\t}\n\tif (started) {\n\t\tAssert(!updatedWithSpeed);\n\t\ttrack->started();\n\t\tif (!internal::audioCheckError()) {\n\t\t\tsetStoppedState(track, State::StoppedAtStart);\n\t\t\temitError(type);\n\t\t\treturn;\n\t\t}\n\n\t\ttrack->format = l->format();\n\t\ttrack->state.frequency = l->samplesFrequency();\n\n\t\ttrack->state.position = (positionMs * track->state.frequency)\n\t\t\t/ 1000LL;\n\t\ttrack->updateWithSpeedPosition();\n\t\ttrack->withSpeed.bufferedPosition = track->withSpeed.position;\n\t\ttrack->withSpeed.fadeStartPosition = track->withSpeed.position;\n\t} else if (updatedWithSpeed) {\n\t\tauto old = Mixer::Track();\n\t\told.stream = base::take(track->stream);\n\t\told.withSpeed = std::exchange(track->withSpeed, *updatedWithSpeed);\n\t\ttrack->speed = setup.newSpeed;\n\t\ttrack->reattach(type);\n\t\told.detach();\n\t}\n\tif (!accumulated.isEmpty()) {\n\t\ttrack->ensureStreamCreated(type);\n\n\t\tauto bufferIndex = track->getNotQueuedBufferIndex();\n\n\t\tif (!internal::audioCheckError()) {\n\t\t\tsetStoppedState(track, State::StoppedAtError);\n\t\t\temitError(type);\n\t\t\treturn;\n\t\t}\n\n\t\tif (bufferIndex < 0) { // No free buffers, wait.\n\t\t\ttrack->waitingForBuffer = true;\n\t\t\tl->saveDecodedSamples(&accumulated);\n\t\t\treturn;\n\t\t} else if (l->forceToBuffer()) {\n\t\t\tl->setForceToBuffer(false);\n\t\t}\n\t\ttrack->waitingForBuffer = false;\n\n\t\ttrack->withSpeed.buffered[bufferIndex] = accumulated;\n\t\ttrack->withSpeed.samples[bufferIndex] = accumulatedCount;\n\t\ttrack->withSpeed.bufferedLength += accumulatedCount;\n\t\talBufferData(\n\t\t\ttrack->stream.buffers[bufferIndex],\n\t\t\ttrack->format,\n\t\t\taccumulated.constData(),\n\t\t\taccumulated.size(),\n\t\t\ttrack->state.frequency);\n\n\t\talSourceQueueBuffers(\n\t\t\ttrack->stream.source,\n\t\t\t1,\n\t\t\ttrack->stream.buffers + bufferIndex);\n\n\t\tif (!internal::audioCheckError()) {\n\t\t\tsetStoppedState(track, State::StoppedAtError);\n\t\t\temitError(type);\n\t\t\treturn;\n\t\t}\n\t} else {\n\t\tif (waiting) {\n\t\t\treturn;\n\t\t}\n\t\tfinished = true;\n\t}\n\ttrack->state.waitingForData = false;\n\n\tif (finished) {\n\t\ttrack->loaded = true;\n\t\ttrack->withSpeed.length = track->withSpeed.bufferedPosition\n\t\t\t+ track->withSpeed.bufferedLength;\n\t\ttrack->state.length = Mixer::Track::SpeedIndependentPosition(\n\t\t\ttrack->withSpeed.length,\n\t\t\ttrack->speed);\n\t}\n\n\ttrack->loading = false;\n\tif (IsPausedOrPausing(track->state.state)\n\t\t|| IsStoppedOrStopping(track->state.state)) {\n\t\treturn;\n\t}\n\tALint state = AL_INITIAL;\n\talGetSourcei(track->stream.source, AL_SOURCE_STATE, &state);\n\tif (!internal::audioCheckError()) {\n\t\tsetStoppedState(track, State::StoppedAtError);\n\t\temitError(type);\n\t\treturn;\n\t}\n\n\tif (state == AL_PLAYING) {\n\t\treturn;\n\t} else if (state == AL_STOPPED && !internal::CheckAudioDeviceConnected()) {\n\t\treturn;\n\t}\n\n\talSourcef(track->stream.source, AL_GAIN, ComputeVolume(type));\n\tif (!internal::audioCheckError()) {\n\t\tsetStoppedState(track, State::StoppedAtError);\n\t\temitError(type);\n\t\treturn;\n\t}\n\n\tif (state == AL_STOPPED) {\n\t\talSourcei(\n\t\t\ttrack->stream.source,\n\t\t\tAL_SAMPLE_OFFSET,\n\t\t\tqMax(track->withSpeed.position - track->withSpeed.bufferedPosition, 0LL));\n\t\tif (!internal::audioCheckError()) {\n\t\t\tsetStoppedState(track, State::StoppedAtError);\n\t\t\temitError(type);\n\t\t\treturn;\n\t\t}\n\t}\n\talSourcePlay(track->stream.source);\n\tif (!internal::audioCheckError()) {\n\t\tsetStoppedState(track, State::StoppedAtError);\n\t\temitError(type);\n\t\treturn;\n\t}\n\n\tneedToCheck();\n}\n\nLoaders::SetupLoaderResult Loaders::setupLoader(\n\t\tconst AudioMsgId &audio,\n\t\tcrl::time positionMs) {\n\tQMutexLocker lock(internal::audioPlayerMutex());\n\tif (!mixer()) {\n\t\treturn {};\n\t}\n\n\tauto track = mixer()->trackForType(audio.type());\n\tif (!track || track->state.id != audio || !track->loading) {\n\t\terror(audio);\n\t\tLOG((\"Audio Error: trying to load part of audio, that is not current at the moment\"));\n\t\treturn {};\n\t}\n\n\tbool isGoodId = false;\n\tAudioPlayerLoader *l = nullptr;\n\tswitch (audio.type()) {\n\tcase AudioMsgId::Type::Voice: l = _audioLoader.get(); isGoodId = (_audio == audio); break;\n\tcase AudioMsgId::Type::Song: l = _songLoader.get(); isGoodId = (_song == audio); break;\n\tcase AudioMsgId::Type::Video: l = _videoLoader.get(); isGoodId = (_video == audio); break;\n\t}\n\n\tif (l && (!isGoodId || !l->check(track->file, track->data))) {\n\t\tclear(audio.type());\n\t\tl = nullptr;\n\t}\n\n\tauto SpeedDependentPosition = Mixer::Track::SpeedDependentPosition;\n\tif (!l) {\n\t\tstd::unique_ptr<AudioPlayerLoader> *loader = nullptr;\n\t\tswitch (audio.type()) {\n\t\tcase AudioMsgId::Type::Voice: _audio = audio; loader = &_audioLoader; break;\n\t\tcase AudioMsgId::Type::Song: _song = audio; loader = &_songLoader; break;\n\t\tcase AudioMsgId::Type::Video: _video = audio; loader = &_videoLoader; break;\n\t\t}\n\n\t\tif (audio.externalPlayId()) {\n\t\t\tif (!track->externalData) {\n\t\t\t\tclear(audio.type());\n\t\t\t\ttrack->state.state = State::StoppedAtError;\n\t\t\t\terror(audio);\n\t\t\t\tLOG((\"Audio Error: video sound data not ready\"));\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\t*loader = std::make_unique<ChildFFMpegLoader>(\n\t\t\t\tstd::move(track->externalData));\n\t\t} else {\n\t\t\t*loader = std::make_unique<FFMpegLoader>(\n\t\t\t\ttrack->file,\n\t\t\t\ttrack->data,\n\t\t\t\tbytes::vector());\n\t\t}\n\t\tl = loader->get();\n\n\t\ttrack->speed = track->nextSpeed;\n\t\tif (!l->open(positionMs, track->speed)) {\n\t\t\ttrack->state.state = State::StoppedAtStart;\n\t\t\treturn { .errorAtStart = true };\n\t\t}\n\t\tconst auto duration = l->duration();\n\t\tif (duration <= 0) {\n\t\t\ttrack->state.state = State::StoppedAtStart;\n\t\t\treturn { .errorAtStart = true };\n\t\t}\n\t\ttrack->state.frequency = l->samplesFrequency();\n\t\ttrack->state.length = (duration * track->state.frequency) / 1000;\n\t\ttrack->withSpeed.length = SpeedDependentPosition(\n\t\t\ttrack->state.length,\n\t\t\ttrack->speed);\n\t\treturn { .loader = l, .justStarted = true };\n\t} else if (!EqualSpeeds(track->nextSpeed, track->speed)) {\n\t\treturn {\n\t\t\t.loader = l,\n\t\t\t.oldSpeed = track->speed,\n\t\t\t.newSpeed = track->nextSpeed,\n\t\t\t.fadeStartPosition = track->withSpeed.fadeStartPosition,\n\t\t\t.position = track->withSpeed.fineTunedPosition,\n\t\t\t.normalLength = track->state.length,\n\t\t\t.frequency = track->state.frequency,\n\t\t};\n\t} else if (track->loaded) {\n\t\tLOG((\"Audio Error: trying to load part of audio, that is already loaded to the end\"));\n\t\treturn {};\n\t}\n\treturn {\n\t\t.loader = l,\n\t\t.oldSpeed = track->speed,\n\t\t.newSpeed = track->nextSpeed,\n\t\t.position = track->withSpeed.fineTunedPosition,\n\t\t.frequency = track->state.frequency,\n\t};\n}\n\nMixer::Track::WithSpeed Loaders::rebufferOnSpeedChange(\n\t\tconst SetupLoaderResult &setup) {\n\tExpects(setup.oldSpeed > 0. && setup.newSpeed > 0.);\n\tExpects(setup.loader != nullptr);\n\n\tconst auto speed = setup.newSpeed;\n\tconst auto change = setup.oldSpeed / speed;\n\tconst auto normalPosition = Mixer::Track::SpeedIndependentPosition(\n\t\tsetup.position,\n\t\tsetup.oldSpeed);\n\tconst auto newPosition = int64(base::SafeRound(setup.position * change));\n\tauto result = Mixer::Track::WithSpeed{\n\t\t.fineTunedPosition = newPosition,\n\t\t.position = newPosition,\n\t\t.length = Mixer::Track::SpeedDependentPosition(\n\t\t\tsetup.normalLength,\n\t\t\tspeed),\n\t\t.fadeStartPosition = int64(\n\t\t\tbase::SafeRound(setup.fadeStartPosition * change)),\n\t};\n\tconst auto l = setup.loader;\n\tl->dropFramesTill(normalPosition);\n\tconst auto normalFrom = l->startReadingQueuedFrames(speed);\n\tif (normalFrom < 0) {\n\t\tresult.bufferedPosition = newPosition;\n\t\treturn result;\n\t}\n\n\tresult.bufferedPosition = Mixer::Track::SpeedDependentPosition(\n\t\tnormalFrom,\n\t\tspeed);\n\tfor (auto i = 0; i != Mixer::Track::kBuffersCount; ++i) {\n\t\tauto finished = false;\n\t\tauto accumulated = QByteArray();\n\t\tauto accumulatedCount = int64();\n\t\tconst auto sampleSize = l->sampleSize();\n\t\tconst auto accumulateTill = l->bytesPerBuffer();\n\t\twhile (accumulated.size() < accumulateTill) {\n\t\t\tconst auto result = l->readMore();\n\t\t\tconst auto sampleBytes = v::is<bytes::const_span>(result)\n\t\t\t\t? v::get<bytes::const_span>(result)\n\t\t\t\t: bytes::const_span();\n\t\t\tif (!sampleBytes.empty()) {\n\t\t\t\taccumulated.append(\n\t\t\t\t\treinterpret_cast<const char*>(sampleBytes.data()),\n\t\t\t\t\tsampleBytes.size());\n\t\t\t\taccumulatedCount += sampleBytes.size() / sampleSize;\n\t\t\t\tcontinue;\n\t\t\t} else if (result == AudioPlayerLoader::ReadError::Retry) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tAssert(result == AudioPlayerLoader::ReadError::RetryNotQueued\n\t\t\t\t|| result == AudioPlayerLoader::ReadError::EndOfFile);\n\t\t\tfinished = true;\n\t\t\tbreak;\n\t\t}\n\t\tif (!accumulated.isEmpty()) {\n\t\t\tresult.samples[i] = accumulatedCount;\n\t\t\tresult.bufferedLength += accumulatedCount;\n\t\t\tresult.buffered[i] = accumulated;\n\t\t}\n\t\tif (finished) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst auto limit = result.bufferedPosition + result.bufferedLength;\n\tif (newPosition > limit) {\n\t\tresult.fineTunedPosition = limit;\n\t\tresult.position = limit;\n\t}\n\tif (limit > result.length) {\n\t\tresult.length = limit;\n\t}\n\n\treturn result;\n}\n\nMixer::Track *Loaders::checkLoader(AudioMsgId::Type type) {\n\tif (!mixer()) return nullptr;\n\n\tauto track = mixer()->trackForType(type);\n\tauto isGoodId = false;\n\tAudioPlayerLoader *l = nullptr;\n\tswitch (type) {\n\tcase AudioMsgId::Type::Voice: l = _audioLoader.get(); isGoodId = (track->state.id == _audio); break;\n\tcase AudioMsgId::Type::Song: l = _songLoader.get(); isGoodId = (track->state.id == _song); break;\n\tcase AudioMsgId::Type::Video: l = _videoLoader.get(); isGoodId = (track->state.id == _video); break;\n\t}\n\tif (!l || !track) return nullptr;\n\n\tif (!isGoodId || !track->loading || !l->check(track->file, track->data)) {\n\t\tLOG((\"Audio Error: playing changed while loading\"));\n\t\treturn nullptr;\n\t}\n\n\treturn track;\n}\n\nvoid Loaders::onCancel(const AudioMsgId &audio) {\n\tExpects(audio.type() != AudioMsgId::Type::Unknown);\n\n\tswitch (audio.type()) {\n\tcase AudioMsgId::Type::Voice: if (_audio == audio) clear(audio.type()); break;\n\tcase AudioMsgId::Type::Song: if (_song == audio) clear(audio.type()); break;\n\tcase AudioMsgId::Type::Video: if (_video == audio) clear(audio.type()); break;\n\t}\n\n\tQMutexLocker lock(internal::audioPlayerMutex());\n\tif (!mixer()) return;\n\n\tfor (auto i = 0; i != kTogetherLimit; ++i) {\n\t\tauto track = mixer()->trackForType(audio.type(), i);\n\t\tif (track->state.id == audio) {\n\t\t\ttrack->loading = false;\n\t\t}\n\t}\n}\n\nLoaders::~Loaders() = default;\n\n} // namespace Player\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio_loaders.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/audio/media_audio.h\"\n#include \"media/audio/media_child_ffmpeg_loader.h\"\n\nclass AudioPlayerLoader;\nclass ChildFFMpegLoader;\n\nnamespace Media {\nnamespace Player {\n\nclass Loaders : public QObject {\n\tQ_OBJECT\n\npublic:\n\tLoaders(QThread *thread);\n\tvoid feedFromExternal(ExternalSoundPart &&part);\n\tvoid forceToBufferExternal(const AudioMsgId &audioId);\n\t~Loaders();\n\nQ_SIGNALS:\n\tvoid error(const AudioMsgId &audio);\n\tvoid needToCheck();\n\npublic Q_SLOTS:\n\tvoid onInit();\n\n\tvoid onStart(const AudioMsgId &audio, qint64 positionMs);\n\tvoid onLoad(const AudioMsgId &audio);\n\tvoid onCancel(const AudioMsgId &audio);\n\nprivate:\n\tstruct SetupLoaderResult {\n\t\tAudioPlayerLoader *loader = nullptr;\n\t\tfloat64 oldSpeed = 0.;\n\t\tfloat64 newSpeed = 0.;\n\t\tint64 fadeStartPosition = 0;\n\t\tint64 position = 0;\n\t\tint64 normalLength = 0;\n\t\tint frequency = 0;\n\t\tbool errorAtStart = false;\n\t\tbool justStarted = false;\n\t};\n\n\tvoid videoSoundAdded();\n\t[[nodiscard]] Mixer::Track::WithSpeed rebufferOnSpeedChange(\n\t\tconst SetupLoaderResult &setup);\n\n\tvoid emitError(AudioMsgId::Type type);\n\tAudioMsgId clear(AudioMsgId::Type type);\n\tvoid setStoppedState(Mixer::Track *m, State state = State::Stopped);\n\n\tvoid loadData(AudioMsgId audio, crl::time positionMs = 0);\n\t[[nodiscard]] SetupLoaderResult setupLoader(\n\t\tconst AudioMsgId &audio,\n\t\tcrl::time positionMs);\n\tMixer::Track *checkLoader(AudioMsgId::Type type);\n\n\tAudioMsgId _audio, _song, _video;\n\tstd::unique_ptr<AudioPlayerLoader> _audioLoader;\n\tstd::unique_ptr<AudioPlayerLoader> _songLoader;\n\tstd::unique_ptr<AudioPlayerLoader> _videoLoader;\n\n\tQMutex _fromExternalMutex;\n\tbase::flat_map<\n\t\tAudioMsgId,\n\t\tstd::deque<FFmpeg::Packet>> _fromExternalQueues;\n\tbase::flat_set<AudioMsgId> _fromExternalForceToBuffer;\n\tSingleQueuedInvokation _fromExternalNotify;\n\n};\n\n} // namespace Player\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio_local_cache.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/audio/media_audio_local_cache.h\"\n\n#include \"ffmpeg/ffmpeg_bytes_io_wrap.h\"\n#include \"ffmpeg/ffmpeg_utility.h\"\n\nnamespace Media::Audio {\nnamespace {\n\nconstexpr auto kMaxDuration = 3 * crl::time(1000);\nconstexpr auto kMaxStreams = 2;\nconstexpr auto kFrameSize = 4096;\n\n[[nodiscard]] QByteArray ConvertAndCut(const QByteArray &bytes) {\n\tusing namespace FFmpeg;\n\n\tif (bytes.isEmpty()) {\n\t\treturn {};\n\t}\n\n\tauto wrap = ReadBytesWrap{\n\t\t.size = bytes.size(),\n\t\t.data = reinterpret_cast<const uchar*>(bytes.constData()),\n\t};\n\n\tauto input = MakeFormatPointer(\n\t\t&wrap,\n\t\t&ReadBytesWrap::Read,\n\t\tnullptr,\n\t\t&ReadBytesWrap::Seek);\n\tif (!input) {\n\t\treturn {};\n\t}\n\n\tauto error = AvErrorWrap(avformat_find_stream_info(input.get(), 0));\n\tif (error) {\n\t\tLogError(u\"avformat_find_stream_info\"_q, error);\n\t\treturn {};\n\t}\n\n\n\tauto inCodec = (const AVCodec*)nullptr;\n\tconst auto streamId = av_find_best_stream(\n\t\tinput.get(),\n\t\tAVMEDIA_TYPE_AUDIO,\n\t\t-1,\n\t\t-1,\n\t\t&inCodec,\n\t\t0);\n\tif (streamId < 0) {\n\t\tLogError(u\"av_find_best_stream\"_q, AvErrorWrap(streamId));\n\t\treturn {};\n\t}\n\n\tauto inStream = input->streams[streamId];\n\tauto inCodecPar = inStream->codecpar;\n\tauto inCodecContext = CodecPointer(avcodec_alloc_context3(nullptr));\n\tif (!inCodecContext) {\n\t\treturn {};\n\t}\n\n\tif (avcodec_parameters_to_context(inCodecContext.get(), inCodecPar) < 0) {\n\t\treturn {};\n\t}\n\n\tif (avcodec_open2(inCodecContext.get(), inCodec, nullptr) < 0) {\n\t\treturn {};\n\t}\n\n\tauto result = WriteBytesWrap();\n\tauto outFormat = MakeWriteFormatPointer(\n\t\tstatic_cast<void*>(&result),\n\t\tnullptr,\n\t\t&WriteBytesWrap::Write,\n\t\t&WriteBytesWrap::Seek,\n\t\t\"wav\"_q);\n\tif (!outFormat) {\n\t\treturn {};\n\t}\n\n\t// Find and open output codec\n\tauto outCodec = avcodec_find_encoder(AV_CODEC_ID_PCM_S16LE);\n\tif (!outCodec) {\n\t\treturn {};\n\t}\n\n\tauto outStream = avformat_new_stream(outFormat.get(), outCodec);\n\tif (!outStream) {\n\t\treturn {};\n\t}\n\n\tauto outCodecContext = CodecPointer(\n\t\tavcodec_alloc_context3(outCodec));\n\tif (!outCodecContext) {\n\t\treturn {};\n\t}\n\n\tauto mono = AVChannelLayout(AV_CHANNEL_LAYOUT_MONO);\n\tauto stereo = AVChannelLayout(AV_CHANNEL_LAYOUT_STEREO);\n\tconst auto in = &inCodecContext->ch_layout;\n\tif (!av_channel_layout_compare(in, &mono)\n\t\t|| !av_channel_layout_compare(in, &stereo)) {\n\t\tav_channel_layout_copy(&outCodecContext->ch_layout, in);\n\t} else {\n\t\toutCodecContext->ch_layout = AV_CHANNEL_LAYOUT_STEREO;\n\t}\n\tconst auto rate = 44'100;\n\toutCodecContext->sample_fmt = AV_SAMPLE_FMT_S16;\n\toutCodecContext->time_base = AVRational{ 1, rate };\n\toutCodecContext->sample_rate = rate;\n\n\terror = avcodec_open2(outCodecContext.get(), outCodec, nullptr);\n\tif (error) {\n\t\tLogError(\"avcodec_open2\", error);\n\t\treturn {};\n\t}\n\n\terror = avcodec_parameters_from_context(\n\t\toutStream->codecpar,\n\t\toutCodecContext.get());\n\tif (error) {\n\t\tLogError(\"avcodec_parameters_from_context\", error);\n\t\treturn {};\n\t}\n\n\terror = avformat_write_header(outFormat.get(), nullptr);\n\tif (error) {\n\t\tLogError(\"avformat_write_header\", error);\n\t\treturn {};\n\t}\n\n\tauto swrContext = MakeSwresamplePointer(\n\t\t&inCodecContext->ch_layout,\n\t\tinCodecContext->sample_fmt,\n\t\tinCodecContext->sample_rate,\n\t\t&outCodecContext->ch_layout,\n\t\toutCodecContext->sample_fmt,\n\t\toutCodecContext->sample_rate);\n\tif (!swrContext) {\n\t\treturn {};\n\t}\n\n\tauto packet = av_packet_alloc();\n\tconst auto guard = gsl::finally([&] {\n\t\tav_packet_free(&packet);\n\t});\n\n\tauto frame = MakeFramePointer();\n\tif (!frame) {\n\t\treturn {};\n\t}\n\n\tauto outFrame = MakeFramePointer();\n\tif (!outFrame) {\n\t\treturn {};\n\t}\n\n\toutFrame->nb_samples = kFrameSize;\n\toutFrame->format = outCodecContext->sample_fmt;\n\tav_channel_layout_copy(\n\t\t&outFrame->ch_layout,\n\t\t&outCodecContext->ch_layout);\n\toutFrame->sample_rate = outCodecContext->sample_rate;\n\n\terror = av_frame_get_buffer(outFrame.get(), 0);\n\tif (error) {\n\t\tLogError(\"av_frame_get_buffer\", error);\n\t\treturn {};\n\t}\n\n\tauto pts = int64_t(0);\n\tauto maxPts = int64_t(kMaxDuration) * rate / 1000;\n\tconst auto writeFrame = [&](AVFrame *frame) { // nullptr to flush\n\t\terror = avcodec_send_frame(outCodecContext.get(), frame);\n\t\tif (error) {\n\t\t\tLogError(\"avcodec_send_frame\", error);\n\t\t\treturn error;\n\t\t}\n\t\tauto pkt = av_packet_alloc();\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\tav_packet_free(&pkt);\n\t\t});\n\t\twhile (true) {\n\t\t\terror = avcodec_receive_packet(outCodecContext.get(), pkt);\n\t\t\tif (error) {\n\t\t\t\tif (error.code() != AVERROR(EAGAIN)\n\t\t\t\t\t&& error.code() != AVERROR_EOF) {\n\t\t\t\t\tLogError(\"avcodec_receive_packet\", error);\n\t\t\t\t}\n\t\t\t\treturn error;\n\t\t\t}\n\t\t\tpkt->stream_index = outStream->index;\n\t\t\tav_packet_rescale_ts(\n\t\t\t\tpkt,\n\t\t\t\toutCodecContext->time_base,\n\t\t\t\toutStream->time_base);\n\t\t\terror = av_interleaved_write_frame(outFormat.get(), pkt);\n\t\t\tif (error) {\n\t\t\t\tLogError(\"av_interleaved_write_frame\", error);\n\t\t\t\treturn error;\n\t\t\t}\n\t\t}\n\t};\n\n\twhile (pts < maxPts) {\n\t\terror = av_read_frame(input.get(), packet);\n\t\tconst auto finished = (error.code() == AVERROR_EOF);\n\t\tif (!finished) {\n\t\t\tif (error) {\n\t\t\t\tLogError(\"av_read_frame\", error);\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\tauto guard = gsl::finally([&] {\n\t\t\t\tav_packet_unref(packet);\n\t\t\t});\n\t\t\tif (packet->stream_index != streamId) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\terror = avcodec_send_packet(inCodecContext.get(), packet);\n\t\t\tif (error) {\n\t\t\t\tLogError(\"avcodec_send_packet\", error);\n\t\t\t\treturn {};\n\t\t\t}\n\t\t}\n\n\t\twhile (true) {\n\t\t\terror = avcodec_receive_frame(inCodecContext.get(), frame.get());\n\t\t\tif (error) {\n\t\t\t\tif (error.code() == AVERROR(EAGAIN)\n\t\t\t\t\t|| error.code() == AVERROR_EOF) {\n\t\t\t\t\tbreak;\n\t\t\t\t} else {\n\t\t\t\t\tLogError(\"avcodec_receive_frame\", error);\n\t\t\t\t\treturn {};\n\t\t\t\t}\n\t\t\t}\n\t\t\terror = swr_convert(\n\t\t\t\tswrContext.get(),\n\t\t\t\toutFrame->data,\n\t\t\t\tkFrameSize,\n\t\t\t\t(const uint8_t**)frame->data,\n\t\t\t\tframe->nb_samples);\n\t\t\tif (error) {\n\t\t\t\tLogError(\"swr_convert\", error);\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\tconst auto samples = error.code();\n\t\t\tif (!samples) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\toutFrame->nb_samples = samples;\n\t\t\toutFrame->pts = pts;\n\t\t\tpts += samples;\n\t\t\tif (pts > maxPts) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\terror = writeFrame(outFrame.get());\n\t\t\tif (error && error.code() != AVERROR(EAGAIN)) {\n\t\t\t\treturn {};\n\t\t\t}\n\t\t}\n\n\t\tif (finished) {\n\t\t\tbreak;\n\t\t}\n\t}\n\terror = writeFrame(nullptr);\n\tif (error && error.code() != AVERROR_EOF) {\n\t\treturn {};\n\t}\n\terror = av_write_trailer(outFormat.get());\n\tif (error) {\n\t\tLogError(\"av_write_trailer\", error);\n\t\treturn {};\n\t}\n\treturn result.content;\n}\n\n} // namespace\n\nLocalSound LocalCache::sound(\n\t\tDocumentId id,\n\t\tFn<QByteArray()> resolveOriginalBytes,\n\t\tFn<QByteArray()> fallbackOriginalBytes) {\n\tauto &result = _cache[id];\n\tif (!result.isEmpty()) {\n\t\treturn { id, result };\n\t}\n\tresult = ConvertAndCut(resolveOriginalBytes());\n\treturn !result.isEmpty()\n\t\t? LocalSound{ id, result }\n\t\t: fallbackOriginalBytes\n\t\t? sound(0, fallbackOriginalBytes, nullptr)\n\t\t: LocalSound();\n}\n\nLocalDiskCache::LocalDiskCache(const QString &folder)\n: _base(folder + '/') {\n\tQDir().mkpath(_base);\n}\n\nQString LocalDiskCache::name(const LocalSound &sound) {\n\tif (!sound) {\n\t\treturn {};\n\t}\n\tconst auto i = _paths.find(sound.id);\n\tif (i != end(_paths)) {\n\t\treturn i->second;\n\t}\n\n\tauto result = u\"TD_%1\"_q.arg(sound.id\n\t\t? QString::number(sound.id, 16).toUpper()\n\t\t: u\"Default\"_q);\n\tconst auto path = _base + u\"%1.wav\"_q.arg(result);\n\n\tauto f = QFile(path);\n\tif (f.open(QIODevice::WriteOnly)) {\n\t\tf.write(sound.wav);\n\t\tf.close();\n\t}\n\n\t_paths.emplace(sound.id, result);\n\treturn result;\n}\n\nQString LocalDiskCache::path(const LocalSound &sound) {\n\tconst auto part = name(sound);\n\treturn part.isEmpty() ? QString() : _base + part + u\".wav\"_q;\n}\n\n} // namespace Media::Audio\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio_local_cache.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Media::Audio {\n\nstruct LocalSound {\n    DocumentId id = 0;\n    QByteArray wav;\n\n    explicit operator bool() const {\n        return !wav.isEmpty();\n    }\n};\n\nclass LocalCache final {\npublic:\n    [[nodiscard]] LocalSound sound(\n        DocumentId id,\n        Fn<QByteArray()> resolveOriginalBytes,\n        Fn<QByteArray()> fallbackOriginalBytes);\n\nprivate:\n    base::flat_map<DocumentId, QByteArray> _cache;\n\n};\n\nclass LocalDiskCache final {\npublic:\n    explicit LocalDiskCache(const QString &folder);\n\n    [[nodiscard]] QString name(const LocalSound &sound);\n    [[nodiscard]] QString path(const LocalSound &sound);\n\nprivate:\n    const QString _base;\n\tbase::flat_map<DocumentId, QString> _paths;\n\n};\n\n} // namespace Media::Audio\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio_track.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/audio/media_audio_track.h\"\n\n#include \"media/audio/media_audio_ffmpeg_loader.h\"\n#include \"media/audio/media_audio.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/file_location.h\"\n\n#include <al.h>\n#include <alc.h>\n\nnamespace Media {\nnamespace Audio {\nnamespace {\n\nconstexpr auto kMaxFileSize = 10 * 1024 * 1024;\nconstexpr auto kDetachDeviceTimeout = crl::time(500); // destroy the audio device after 500ms of silence\nconstexpr auto kTrackUpdateTimeout = crl::time(100);\n\nALuint CreateSource() {\n\tauto source = ALuint(0);\n\talGenSources(1, &source);\n\talSourcef(source, AL_PITCH, 1.f);\n\talSourcef(source, AL_GAIN, 1.f);\n\talSource3f(source, AL_POSITION, 0, 0, 0);\n\talSource3f(source, AL_VELOCITY, 0, 0, 0);\n\treturn source;\n}\n\nALuint CreateBuffer() {\n\tauto buffer = ALuint(0);\n\talGenBuffers(1, &buffer);\n\treturn buffer;\n}\n\n} // namespace\n\nTrack::Track(not_null<Instance*> instance) : _instance(instance) {\n\t_instance->registerTrack(this);\n}\n\nvoid Track::samplePeakEach(crl::time peakDuration) {\n\t_peakDurationMs = peakDuration;\n}\n\nvoid Track::fillFromData(bytes::vector &&data) {\n\tFFMpegLoader loader(Core::FileLocation(), QByteArray(), std::move(data));\n\n\tauto position = qint64(0);\n\tif (!loader.open(position)) {\n\t\t_failed = true;\n\t\treturn;\n\t}\n\tauto format = loader.format();\n\t_peakEachPosition = _peakDurationMs ? ((loader.samplesFrequency() * _peakDurationMs) / 1000) : 0;\n\tconst auto samplesCount = (loader.duration() * loader.samplesFrequency()) / 1000;\n\tconst auto peaksCount = _peakEachPosition ? (samplesCount / _peakEachPosition) : 0;\n\t_peaks.reserve(peaksCount);\n\tauto peakValue = uint16(0);\n\tauto peakSamples = 0;\n\tauto peakEachSample = (format == AL_FORMAT_STEREO8 || format == AL_FORMAT_STEREO16) ? (_peakEachPosition * 2) : _peakEachPosition;\n\t_peakValueMin = 0x7FFF;\n\t_peakValueMax = 0;\n\tauto peakCallback = [this, &peakValue, &peakSamples, peakEachSample](uint16 sample) {\n\t\taccumulate_max(peakValue, sample);\n\t\tif (++peakSamples >= peakEachSample) {\n\t\t\tpeakSamples -= peakEachSample;\n\t\t\t_peaks.push_back(peakValue);\n\t\t\taccumulate_max(_peakValueMax, peakValue);\n\t\t\taccumulate_min(_peakValueMin, peakValue);\n\t\t\tpeakValue = 0;\n\t\t}\n\t};\n\tdo {\n\t\tusing Error = AudioPlayerLoader::ReadError;\n\t\tconst auto result = loader.readMore();\n\t\tAssert(result != Error::Wait && result != Error::RetryNotQueued);\n\n\t\tif (result == Error::Retry) {\n\t\t\tcontinue;\n\t\t} else if (result == Error::EndOfFile) {\n\t\t\tbreak;\n\t\t} else if (result == Error::Other || result == Error::Wait) {\n\t\t\t_failed = true;\n\t\t\tbreak;\n\t\t}\n\t\tAssert(v::is<bytes::const_span>(result));\n\t\tconst auto sampleBytes = v::get<bytes::const_span>(result);\n\t\tAssert(!sampleBytes.empty());\n\t\t_samplesCount += sampleBytes.size() / loader.sampleSize();\n\t\t_samples.insert(_samples.end(), sampleBytes.data(), sampleBytes.data() + sampleBytes.size());\n\t\tif (peaksCount) {\n\t\t\tif (format == AL_FORMAT_MONO8 || format == AL_FORMAT_STEREO8) {\n\t\t\t\tMedia::Audio::IterateSamples<uchar>(sampleBytes, peakCallback);\n\t\t\t} else if (format == AL_FORMAT_MONO16 || format == AL_FORMAT_STEREO16) {\n\t\t\t\tMedia::Audio::IterateSamples<int16>(sampleBytes, peakCallback);\n\t\t\t}\n\t\t}\n\t} while (true);\n\n\t_alFormat = loader.format();\n\t_sampleRate = loader.samplesFrequency();\n\t_lengthMs = loader.duration();\n}\n\nvoid Track::fillFromFile(const Core::FileLocation &location) {\n\tif (location.accessEnable()) {\n\t\tfillFromFile(location.name());\n\t\tlocation.accessDisable();\n\t} else {\n\t\tLOG((\"Track Error: Could not enable access to file '%1'.\").arg(location.name()));\n\t\t_failed = true;\n\t}\n}\n\nvoid Track::fillFromFile(const QString &filePath) {\n\tQFile f(filePath);\n\tif (f.open(QIODevice::ReadOnly)) {\n\t\tauto size = f.size();\n\t\tif (size > 0 && size <= kMaxFileSize) {\n\t\t\tauto bytes = bytes::vector(size);\n\t\t\tif (f.read(reinterpret_cast<char*>(bytes.data()), bytes.size()) == bytes.size()) {\n\t\t\t\tfillFromData(std::move(bytes));\n\t\t\t} else {\n\t\t\t\tLOG((\"Track Error: Could not read %1 bytes from file '%2'.\").arg(bytes.size()).arg(filePath));\n\t\t\t\t_failed = true;\n\t\t\t}\n\t\t} else {\n\t\t\tLOG((\"Track Error: Bad file '%1' size: %2.\").arg(filePath).arg(size));\n\t\t\t_failed = true;\n\t\t}\n\t} else {\n\t\tLOG((\"Track Error: Could not open file '%1'.\").arg(filePath));\n\t\t_failed = true;\n\t}\n}\n\nvoid Track::playWithLooping(bool looping, float64 volumeOverride) {\n\t_active = true;\n\tif (failed() || _samples.empty()) {\n\t\tfinish();\n\t\treturn;\n\t}\n\tensureSourceCreated();\n\talSourceStop(_alSource);\n\t_looping = looping;\n\talSourcei(_alSource, AL_LOOPING, _looping ? 1 : 0);\n\talSourcef(\n\t\t_alSource,\n\t\tAL_GAIN,\n\t\t(volumeOverride > 0)\n\t\t\t? volumeOverride\n\t\t\t: float64(Core::App().settings().notificationsVolume()) / 100.);\n\talSourcePlay(_alSource);\n\t_instance->trackStarted(this);\n}\n\nvoid Track::finish() {\n\tif (_active) {\n\t\t_active = false;\n\t\t_instance->trackFinished(this);\n\t}\n\t_alPosition = 0;\n}\n\nvoid Track::ensureSourceCreated() {\n\tif (alIsSource(_alSource)) {\n\t\treturn;\n\t}\n\n\t{\n\t\tQMutexLocker lock(Player::internal::audioPlayerMutex());\n\t\tif (!AttachToDevice()) {\n\t\t\t_failed = true;\n\t\t\treturn;\n\t\t}\n\t}\n\n\t_alSource = CreateSource();\n\t_alBuffer = CreateBuffer();\n\n\talBufferData(_alBuffer, _alFormat, _samples.data(), _samples.size(), _sampleRate);\n\talSourcei(_alSource, AL_BUFFER, _alBuffer);\n}\n\nvoid Track::updateState() {\n\tif (!isActive() || !alIsSource(_alSource)) {\n\t\treturn;\n\t}\n\n\t_stateUpdatedAt = crl::now();\n\tauto state = ALint(0);\n\talGetSourcei(_alSource, AL_SOURCE_STATE, &state);\n\tif (state != AL_PLAYING) {\n\t\tfinish();\n\t} else {\n\t\tauto currentPosition = ALint(0);\n\t\talGetSourcei(_alSource, AL_SAMPLE_OFFSET, &currentPosition);\n\t\t_alPosition = currentPosition;\n\t}\n}\n\nfloat64 Track::getPeakValue(crl::time when) const {\n\tif (!isActive() || !_samplesCount || _peaks.empty() || _peakValueMin == _peakValueMax) {\n\t\treturn 0.;\n\t}\n\tauto sampleIndex = (_alPosition + ((when - _stateUpdatedAt) * _sampleRate / 1000));\n\twhile (sampleIndex < 0) {\n\t\tsampleIndex += _samplesCount;\n\t}\n\tsampleIndex = sampleIndex % _samplesCount;\n\tauto peakIndex = (sampleIndex / _peakEachPosition) % _peaks.size();\n\treturn (_peaks[peakIndex] - _peakValueMin) / float64(_peakValueMax - _peakValueMin);\n}\n\nvoid Track::detachFromDevice() {\n\tif (alIsSource(_alSource)) {\n\t\tupdateState();\n\t\talSourceStop(_alSource);\n\t\talSourcei(_alSource, AL_BUFFER, AL_NONE);\n\t\talDeleteBuffers(1, &_alBuffer);\n\t\talDeleteSources(1, &_alSource);\n\t}\n\t_alBuffer = 0;\n\t_alSource = 0;\n}\n\nvoid Track::reattachToDevice() {\n\tif (!isActive() || alIsSource(_alSource)) {\n\t\treturn;\n\t}\n\tensureSourceCreated();\n\n\talSourcei(_alSource, AL_LOOPING, _looping ? 1 : 0);\n\talSourcei(_alSource, AL_SAMPLE_OFFSET, static_cast<ALint>(_alPosition));\n\talSourcePlay(_alSource);\n}\n\nTrack::~Track() {\n\tdetachFromDevice();\n\t_instance->unregisterTrack(this);\n}\n\nInstance::Instance()\n: _playbackDeviceId(\n\t&Core::App().mediaDevices(),\n\tWebrtc::DeviceType::Playback,\n\tWebrtc::DeviceIdOrDefault(\n\t\tCore::App().settings().playbackDeviceIdValue()))\n, _captureDeviceId(\n\t&Core::App().mediaDevices(),\n\tWebrtc::DeviceType::Capture,\n\tWebrtc::DeviceIdOrDefault(\n\t\tCore::App().settings().captureDeviceIdValue())) {\n\t_updateTimer.setCallback([this] {\n\t\tauto hasActive = false;\n\t\tfor (auto track : _tracks) {\n\t\t\ttrack->updateState();\n\t\t\tif (track->isActive()) {\n\t\t\t\thasActive = true;\n\t\t\t}\n\t\t}\n\t\tif (hasActive) {\n\t\t\tAudio::StopDetachIfNotUsedSafe();\n\t\t}\n\t});\n\n\t_detachFromDeviceTimer.setCallback([=] {\n\t\t_detachFromDeviceForce = false;\n\t\tPlayer::internal::DetachFromDevice(this);\n\t});\n\n\t_playbackDeviceId.changes(\n\t) | rpl::on_next([=](Webrtc::DeviceResolvedId id) {\n\t\tif (Player::internal::DetachIfDeviceChanged(this, id)) {\n\t\t\t_detachFromDeviceForce = false;\n\t\t}\n\t}, _lifetime);\n}\n\nWebrtc::DeviceResolvedId Instance::playbackDeviceId() const {\n\treturn _playbackDeviceId.threadSafeCurrent();\n}\n\nWebrtc::DeviceResolvedId Instance::captureDeviceId() const {\n\treturn _captureDeviceId.current();\n}\n\nstd::unique_ptr<Track> Instance::createTrack() {\n\treturn std::make_unique<Track>(this);\n}\n\nInstance::~Instance() {\n\tExpects(_tracks.empty());\n}\n\nvoid Instance::registerTrack(Track *track) {\n\t_tracks.insert(track);\n}\n\nvoid Instance::unregisterTrack(Track *track) {\n\t_tracks.erase(track);\n}\n\nvoid Instance::trackStarted(Track *track) {\n\tstopDetachIfNotUsed();\n\tif (!_updateTimer.isActive()) {\n\t\t_updateTimer.callEach(kTrackUpdateTimeout);\n\t}\n}\n\nvoid Instance::trackFinished(Track *track) {\n\tif (!hasActiveTracks()) {\n\t\t_updateTimer.cancel();\n\t\tscheduleDetachIfNotUsed();\n\t}\n}\n\nvoid Instance::detachTracks() {\n\tfor (auto track : _tracks) {\n\t\ttrack->detachFromDevice();\n\t}\n}\n\nvoid Instance::reattachTracks() {\n\tif (!IsAttachedToDevice()) {\n\t\treturn;\n\t}\n\tfor (auto track : _tracks) {\n\t\ttrack->reattachToDevice();\n\t}\n}\n\nbool Instance::hasActiveTracks() const {\n\tfor (auto track : _tracks) {\n\t\tif (track->isActive()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Instance::scheduleDetachFromDevice() {\n\t_detachFromDeviceForce = true;\n\tscheduleDetachIfNotUsed();\n}\n\nvoid Instance::scheduleDetachIfNotUsed() {\n\tif (!_detachFromDeviceTimer.isActive()) {\n\t\t_detachFromDeviceTimer.callOnce(kDetachDeviceTimeout);\n\t}\n}\n\nvoid Instance::stopDetachIfNotUsed() {\n\tif (!_detachFromDeviceForce) {\n\t\t_detachFromDeviceTimer.cancel();\n\t}\n}\n\nInstance &Current() {\n\treturn Core::App().audio();\n}\n\n} // namespace Audio\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_audio_track.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"base/bytes.h\"\n#include \"webrtc/webrtc_device_resolver.h\"\n\nnamespace Core {\nclass FileLocation;\n} // namespace Core\n\nnamespace Media {\nnamespace Audio {\n\nclass Instance;\n\nclass Track {\npublic:\n\tTrack(not_null<Instance*> instance);\n\n\tvoid samplePeakEach(crl::time peakDuration);\n\n\tvoid fillFromData(bytes::vector &&data);\n\tvoid fillFromFile(const Core::FileLocation &location);\n\tvoid fillFromFile(const QString &filePath);\n\n\tvoid playOnce(float64 volumeOverride = -1) {\n\t\tplayWithLooping(false, volumeOverride);\n\t}\n\tvoid playInLoop(float64 volumeOverride = -1) {\n\t\tplayWithLooping(true, volumeOverride);\n\t}\n\n\tbool isLooping() const {\n\t\treturn _looping;\n\t}\n\tbool isActive() const {\n\t\treturn _active;\n\t}\n\tbool failed() const {\n\t\treturn _failed;\n\t}\n\n\tint64 getLengthMs() const {\n\t\treturn _lengthMs;\n\t}\n\tfloat64 getPeakValue(crl::time when) const;\n\n\tvoid detachFromDevice();\n\tvoid reattachToDevice();\n\tvoid updateState();\n\n\t~Track();\n\nprivate:\n\tvoid finish();\n\tvoid ensureSourceCreated();\n\tvoid playWithLooping(bool looping, float64 volumeOverride);\n\n\tnot_null<Instance*> _instance;\n\n\tbool _failed = false;\n\tbool _active = false;\n\tbool _looping = false;\n\n\tint64 _samplesCount = 0;\n\tint32 _sampleRate = 0;\n\tbytes::vector _samples;\n\n\tcrl::time _peakDurationMs = 0;\n\tint _peakEachPosition = 0;\n\tstd::vector<uint16> _peaks;\n\tuint16 _peakValueMin = 0;\n\tuint16 _peakValueMax = 0;\n\n\tcrl::time _lengthMs = 0;\n\tcrl::time _stateUpdatedAt = 0;\n\n\tint32 _alFormat = 0;\n\tint64 _alPosition = 0;\n\tuint32 _alSource = 0;\n\tuint32 _alBuffer = 0;\n\n};\n\nclass Instance {\npublic:\n\t// Thread: Main.\n\tInstance();\n\n\t// Thread: Any. Must be locked: AudioMutex.\n\t[[nodiscard]] Webrtc::DeviceResolvedId playbackDeviceId() const;\n\n\t// Thread: Main.\n\t[[nodiscard]] Webrtc::DeviceResolvedId captureDeviceId() const;\n\n\t[[nodiscard]] std::unique_ptr<Track> createTrack();\n\n\tvoid detachTracks();\n\tvoid reattachTracks();\n\tbool hasActiveTracks() const;\n\n\tvoid scheduleDetachFromDevice();\n\tvoid scheduleDetachIfNotUsed();\n\tvoid stopDetachIfNotUsed();\n\n\t~Instance();\n\nprivate:\n\tfriend class Track;\n\tvoid registerTrack(Track *track);\n\tvoid unregisterTrack(Track *track);\n\tvoid trackStarted(Track *track);\n\tvoid trackFinished(Track *track);\n\nprivate:\n\tstd::set<Track*> _tracks;\n\tWebrtc::DeviceResolver _playbackDeviceId;\n\tWebrtc::DeviceResolver _captureDeviceId;\n\n\tbase::Timer _updateTimer;\n\n\tbase::Timer _detachFromDeviceTimer;\n\tbool _detachFromDeviceForce = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] Instance &Current();\n\n} // namespace Audio\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_child_ffmpeg_loader.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/audio/media_child_ffmpeg_loader.h\"\n\n#include \"core/crash_reports.h\"\n#include \"core/file_location.h\"\n\nnamespace Media {\nnamespace {\n\nusing FFmpeg::AvErrorWrap;\n\n} // namespace\n\nChildFFMpegLoader::ChildFFMpegLoader(\n\tstd::unique_ptr<ExternalSoundData> &&data)\n: AbstractAudioFFMpegLoader(\n\tCore::FileLocation(),\n\tQByteArray(),\n\tbytes::vector())\n, _parentData(std::move(data)) {\n\tExpects(_parentData->codec != nullptr);\n}\n\nbool ChildFFMpegLoader::open(crl::time positionMs, float64 speed) {\n\tconst auto sample = (positionMs * samplesFrequency()) / 1000LL;\n\toverrideDuration(sample, _parentData->duration);\n\treturn initUsingContext(_parentData->codec.get(), speed);\n}\n\nauto ChildFFMpegLoader::readFromInitialFrame() -> ReadResult {\n\tif (!_parentData->frame) {\n\t\treturn ReadError::Wait;\n\t}\n\treturn replaceFrameAndRead(base::take(_parentData->frame));\n}\n\nauto ChildFFMpegLoader::readMore() -> ReadResult {\n\tif (_readTillEnd) {\n\t\treturn ReadError::EndOfFile;\n\t}\n\tconst auto initialFrameResult = readFromInitialFrame();\n\tif (initialFrameResult != ReadError::Wait) {\n\t\treturn initialFrameResult;\n\t}\n\n\tconst auto readResult = readFromReadyContext(\n\t\t_parentData->codec.get());\n\tif (readResult != ReadError::Wait) {\n\t\treturn readResult;\n\t}\n\n\tif (_queue.empty()) {\n\t\tif (!_eofReached) {\n\t\t\treturn ReadError::Wait;\n\t\t}\n\t\t_readTillEnd = true;\n\t\treturn ReadError::EndOfFile;\n\t}\n\n\tauto packet = std::move(_queue.front());\n\t_queue.pop_front();\n\n\t_eofReached = packet.empty();\n\tif (_eofReached) {\n\t\tavcodec_send_packet(_parentData->codec.get(), nullptr); // drain\n\t\treturn ReadError::Retry;\n\t}\n\n\tAvErrorWrap error = avcodec_send_packet(\n\t\t_parentData->codec.get(),\n\t\t&packet.fields());\n\tif (error) {\n\t\tLogError(u\"avcodec_send_packet\"_q, error);\n\t\t// There is a sample voice message where skipping such packet\n\t\t// results in a crash (read_access to nullptr) in swr_convert().\n\t\tif (error.code() == AVERROR_INVALIDDATA) {\n\t\t\treturn ReadError::Retry; // try to skip bad packet\n\t\t}\n\t\treturn ReadError::Other;\n\t}\n\treturn ReadError::Retry;\n}\n\nvoid ChildFFMpegLoader::enqueuePackets(\n\t\tstd::deque<FFmpeg::Packet> &&packets) {\n\tif (_queue.empty()) {\n\t\t_queue = std::move(packets);\n\t} else {\n\t\t_queue.insert(\n\t\t\tend(_queue),\n\t\t\tstd::make_move_iterator(packets.begin()),\n\t\t\tstd::make_move_iterator(packets.end()));\n\t}\n\tpackets.clear();\n}\n\nvoid ChildFFMpegLoader::setForceToBuffer(bool force) {\n\t_forceToBuffer = force;\n}\n\nbool ChildFFMpegLoader::forceToBuffer() const {\n\treturn _forceToBuffer;\n}\n\nChildFFMpegLoader::~ChildFFMpegLoader() = default;\n\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/audio/media_child_ffmpeg_loader.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/audio/media_audio_ffmpeg_loader.h\"\n#include \"media/streaming/media_streaming_utility.h\"\n\nnamespace Media {\n\nstruct ExternalSoundData {\n\tFFmpeg::CodecPointer codec;\n\tFFmpeg::FramePointer frame;\n\tcrl::time duration = 0;\n\tfloat64 speed = 1.; // 0.5 <= speed <= 2.\n};\n\nstruct ExternalSoundPart {\n\tAudioMsgId audio;\n\tgsl::span<FFmpeg::Packet> packets;\n};\n\nclass ChildFFMpegLoader : public AbstractAudioFFMpegLoader {\npublic:\n\tChildFFMpegLoader(std::unique_ptr<ExternalSoundData> &&data);\n\n\tbool open(crl::time positionMs, float64 speed = 1.) override;\n\n\tbool check(const Core::FileLocation &file, const QByteArray &data) override {\n\t\treturn true;\n\t}\n\n\tReadResult readMore() override;\n\tvoid enqueuePackets(std::deque<FFmpeg::Packet> &&packets) override;\n\tvoid setForceToBuffer(bool force) override;\n\tbool forceToBuffer() const override;\n\n\tbool eofReached() const {\n\t\treturn _eofReached;\n\t}\n\n\t~ChildFFMpegLoader();\n\nprivate:\n\t// Streaming player reads first frame by itself and provides it together\n\t// with the codec context. So we first read data from this frame and\n\t// only after that we try to read next packets.\n\tReadResult readFromInitialFrame();\n\n\tstd::unique_ptr<ExternalSoundData> _parentData;\n\tstd::deque<FFmpeg::Packet> _queue;\n\tbool _forceToBuffer = false;\n\tbool _eofReached = false;\n\tbool _readTillEnd = false;\n\n};\n\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/clip/media_clip_check_streaming.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/clip/media_clip_check_streaming.h\"\n\n#include \"core/file_location.h\"\n#include \"base/bytes.h\"\n#include \"logs.h\"\n\n#include <QtCore/QtEndian>\n#include <QtCore/QBuffer>\n#include <QtCore/QFile>\n\nnamespace Media {\nnamespace Clip {\nnamespace {\n\nconstexpr auto kHeaderSize = 8;\nconstexpr auto kFindMoovBefore = 128 * 1024;\n\ntemplate <typename Type>\nType ReadBigEndian(bytes::const_span data) {\n\tconst auto bytes = data.subspan(0, sizeof(Type)).data();\n\treturn qFromBigEndian(*reinterpret_cast<const Type*>(bytes));\n}\n\nbool IsAtom(bytes::const_span header, const char (&atom)[5]) {\n\treturn bytes::compare(\n\t\theader.subspan(4, 4),\n\t\tbytes::make_span(atom).subspan(0, 4)) == 0;\n}\n\n} // namespace\n\nbool CheckStreamingSupport(\n\t\tconst Core::FileLocation &location,\n\t\tQByteArray data) {\n\tQBuffer buffer;\n\tQFile file;\n\tif (data.isEmpty()) {\n\t\tfile.setFileName(location.name());\n\t} else {\n\t\tbuffer.setBuffer(&data);\n\t}\n\tconst auto size = data.isEmpty()\n\t\t? file.size()\n\t\t: data.size();\n\tconst auto device = data.isEmpty()\n\t\t? static_cast<QIODevice*>(&file)\n\t\t: static_cast<QIODevice*>(&buffer);\n\n\tif (size < kHeaderSize || !device->open(QIODevice::ReadOnly)) {\n\t\treturn false;\n\t}\n\n\tauto lastReadPosition = 0;\n\tchar atomHeader[kHeaderSize] = { 0 };\n\tauto atomHeaderBytes = bytes::make_span(atomHeader);\n\twhile (true) {\n\t\tconst auto position = device->pos();\n\t\tif (device->read(atomHeader, kHeaderSize) != kHeaderSize) {\n\t\t\tbreak;\n\t\t}\n\n\t\tif (lastReadPosition >= kFindMoovBefore) {\n\t\t\treturn false;\n\t\t} else if (IsAtom(atomHeaderBytes, \"moov\")) {\n\t\t\treturn true;\n\t\t}\n\n\t\tconst auto length = [&] {\n\t\t\tconst auto result = ReadBigEndian<uint32>(atomHeaderBytes);\n\t\t\tif (result != 1) {\n\t\t\t\treturn uint64(result);\n\t\t\t}\n\t\t\tchar atomSize64[kHeaderSize] = { 0 };\n\t\t\tif (device->read(atomSize64, kHeaderSize) != kHeaderSize) {\n\t\t\t\treturn uint64(-1);\n\t\t\t}\n\t\t\tauto atomSize64Bytes = bytes::make_span(atomSize64);\n\t\t\treturn ReadBigEndian<uint64>(atomSize64Bytes);\n\t\t}();\n\t\tif (position + length > size) {\n\t\t\tbreak;\n\t\t}\n\t\tdevice->seek(position + length);\n\t\tlastReadPosition = position;\n\t}\n\treturn false;\n}\n\n} // namespace Clip\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/clip/media_clip_check_streaming.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Core {\nclass FileLocation;\n} // namespace Core\n\nnamespace Media {\nnamespace Clip {\n\nbool CheckStreamingSupport(\n\tconst Core::FileLocation &location,\n\tQByteArray data);\n\n} // namespace Clip\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/clip/media_clip_ffmpeg.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/clip/media_clip_ffmpeg.h\"\n\n#include \"core/file_location.h\"\n#include \"logs.h\"\n\nnamespace Media {\nnamespace Clip {\nnamespace internal {\nnamespace {\n\nconstexpr auto kSkipInvalidDataPackets = 10;\nconstexpr auto kMaxInlineArea = 1280 * 720;\nconstexpr auto kMaxSendingArea = 3840 * 2160; // usual 4K\n\n// See https://github.com/telegramdesktop/tdesktop/issues/7225\nconstexpr auto kAlignImageBy = 64;\n\nvoid alignedImageBufferCleanupHandler(void *data) {\n\tauto buffer = static_cast<uchar*>(data);\n\tdelete[] buffer;\n}\n\n// Create a QImage of desired size where all the data is aligned to 16 bytes.\nQImage createAlignedImage(QSize size) {\n\tauto width = size.width();\n\tauto height = size.height();\n\tauto widthalign = kAlignImageBy / 4;\n\tauto neededwidth = width + ((width % widthalign) ? (widthalign - (width % widthalign)) : 0);\n\tauto bytesperline = neededwidth * 4;\n\tauto buffer = new uchar[bytesperline * height + kAlignImageBy];\n\tauto cleanupdata = static_cast<void*>(buffer);\n\tauto bufferval = reinterpret_cast<uintptr_t>(buffer);\n\tauto alignedbuffer = buffer + ((bufferval % kAlignImageBy) ? (kAlignImageBy - (bufferval % kAlignImageBy)) : 0);\n\treturn QImage(alignedbuffer, width, height, bytesperline, QImage::Format_ARGB32_Premultiplied, alignedImageBufferCleanupHandler, cleanupdata);\n}\n\nbool isAlignedImage(const QImage &image) {\n\treturn !(reinterpret_cast<uintptr_t>(image.constBits()) % kAlignImageBy) && !(image.bytesPerLine() % kAlignImageBy);\n}\n\n} // namespace\n\nFFMpegReaderImplementation::FFMpegReaderImplementation(\n\tCore::FileLocation *location,\n\tQByteArray *data)\n: ReaderImplementation(location, data)\n, _frame(FFmpeg::MakeFramePointer()) {\n}\n\nReaderImplementation::ReadResult FFMpegReaderImplementation::readNextFrame() {\n\tdo {\n\t\tint res = avcodec_receive_frame(_codecContext, _frame.get());\n\t\tif (res >= 0) {\n\t\t\tconst auto limit = (_mode == Mode::Inspecting)\n\t\t\t\t? kMaxSendingArea\n\t\t\t\t: kMaxInlineArea;\n\t\t\tif (_frame->width * _frame->height > limit) {\n\t\t\t\treturn ReadResult::Error;\n\t\t\t}\n\t\t\tprocessReadFrame();\n\t\t\treturn ReadResult::Success;\n\t\t}\n\n\t\tif (res == AVERROR_EOF) {\n\t\t\t_packetQueue.clear();\n\t\t\tif (!_hadFrame) {\n\t\t\t\tLOG((\"Gif Error: Got EOF before a single frame was read!\"));\n\t\t\t\treturn ReadResult::Error;\n\t\t\t}\n\n\t\t\tif ((res = avformat_seek_file(_fmtContext, _streamId, std::numeric_limits<int64_t>::min(), 0, std::numeric_limits<int64_t>::max(), 0)) < 0) {\n\t\t\t\tif ((res = av_seek_frame(_fmtContext, _streamId, 0, AVSEEK_FLAG_BYTE)) < 0) {\n\t\t\t\t\tif ((res = av_seek_frame(_fmtContext, _streamId, 0, AVSEEK_FLAG_FRAME)) < 0) {\n\t\t\t\t\t\tif ((res = av_seek_frame(_fmtContext, _streamId, 0, 0)) < 0) {\n\t\t\t\t\t\t\tchar err[AV_ERROR_MAX_STRING_SIZE] = { 0 };\n\t\t\t\t\t\t\tLOG((\"Gif Error: Unable to av_seek_frame() to the start %1, error %2, %3\").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\t\t\t\t\t\treturn ReadResult::Error;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tavcodec_flush_buffers(_codecContext);\n\t\t\t_hadFrame = false;\n\t\t\t_frameMs = 0;\n\t\t\t_lastReadVideoMs = _lastReadAudioMs = 0;\n\t\t\t_skippedInvalidDataPackets = 0;\n\t\t\t_frameIndex = -1;\n\n\t\t\tcontinue;\n\t\t} else if (res != AVERROR(EAGAIN)) {\n\t\t\tchar err[AV_ERROR_MAX_STRING_SIZE] = { 0 };\n\t\t\tLOG((\"Gif Error: Unable to avcodec_receive_frame() %1, error %2, %3\").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\t\treturn ReadResult::Error;\n\t\t}\n\n\t\twhile (_packetQueue.empty()) {\n\t\t\tauto packetResult = readAndProcessPacket();\n\t\t\tif (packetResult == PacketResult::Error) {\n\t\t\t\treturn ReadResult::Error;\n\t\t\t} else if (packetResult == PacketResult::EndOfFile) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (_packetQueue.empty()) {\n\t\t\tavcodec_send_packet(_codecContext, nullptr); // drain\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto packet = std::move(_packetQueue.front());\n\t\t_packetQueue.pop_front();\n\n\t\tconst auto native = &packet.fields();\n\t\tconst auto guard = gsl::finally([\n\t\t\t&,\n\t\t\tsize = native->size,\n\t\t\tdata = native->data\n\t\t] {\n\t\t\tnative->size = size;\n\t\t\tnative->data = data;\n\t\t\tpacket = FFmpeg::Packet();\n\t\t});\n\n\t\tres = avcodec_send_packet(_codecContext, native);\n\t\tif (res < 0) {\n\t\t\tchar err[AV_ERROR_MAX_STRING_SIZE] = { 0 };\n\t\t\tLOG((\"Gif Error: Unable to avcodec_send_packet() %1, error %2, %3\").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\t\tif (res == AVERROR_INVALIDDATA) {\n\t\t\t\tif (++_skippedInvalidDataPackets < kSkipInvalidDataPackets) {\n\t\t\t\t\tcontinue; // try to skip bad packet\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ReadResult::Error;\n\t\t}\n\t} while (true);\n\n\treturn ReadResult::Error;\n}\n\nvoid FFMpegReaderImplementation::processReadFrame() {\n\tint64 duration = _frame->duration;\n\tint64 framePts = _frame->pts;\n\tcrl::time frameMs = (framePts * 1000LL * _fmtContext->streams[_streamId]->time_base.num) / _fmtContext->streams[_streamId]->time_base.den;\n\t_currentFrameDelay = _nextFrameDelay;\n\tif (_frameMs + _currentFrameDelay < frameMs) {\n\t\t_currentFrameDelay = int32(frameMs - _frameMs);\n\t} else if (frameMs < _frameMs + _currentFrameDelay) {\n\t\tframeMs = _frameMs + _currentFrameDelay;\n\t}\n\n\tif (duration == AV_NOPTS_VALUE) {\n\t\t_nextFrameDelay = 0;\n\t} else {\n\t\t_nextFrameDelay = (duration * 1000LL * _fmtContext->streams[_streamId]->time_base.num) / _fmtContext->streams[_streamId]->time_base.den;\n\t}\n\t_frameMs = frameMs;\n\n\t_hadFrame = _frameRead = true;\n\t_frameTime += _currentFrameDelay;\n\t++_frameIndex;\n}\n\nReaderImplementation::ReadResult FFMpegReaderImplementation::readFramesTill(crl::time frameMs, crl::time systemMs) {\n\tif (_frameRead && _frameTime > frameMs) {\n\t\treturn ReadResult::Success;\n\t}\n\tauto readResult = readNextFrame();\n\tif (readResult != ReadResult::Success || _frameTime > frameMs) {\n\t\treturn readResult;\n\t}\n\treadResult = readNextFrame();\n\tif (_frameTime <= frameMs) {\n\t\t_frameTime = frameMs + 5; // keep up\n\t}\n\treturn readResult;\n}\n\ncrl::time FFMpegReaderImplementation::frameRealTime() const {\n\treturn _frameMs;\n}\n\ncrl::time FFMpegReaderImplementation::framePresentationTime() const {\n\treturn qMax(_frameTime + _frameTimeCorrection, crl::time(0));\n}\n\ncrl::time FFMpegReaderImplementation::durationMs() const {\n\tconst auto rebase = [](int64_t duration, const AVRational &base) {\n\t\treturn (duration * 1000LL * base.num) / base.den;\n\t};\n\tconst auto stream = _fmtContext->streams[_streamId];\n\tif (stream->duration != AV_NOPTS_VALUE) {\n\t\treturn rebase(stream->duration, stream->time_base);\n\t} else if (_fmtContext->duration != AV_NOPTS_VALUE) {\n\t\treturn rebase(_fmtContext->duration, AVRational{ 1, AV_TIME_BASE });\n\t}\n\treturn 0;\n}\n\nbool FFMpegReaderImplementation::renderFrame(\n\t\tQImage &to,\n\t\tbool &hasAlpha,\n\t\tint &index,\n\t\tconst QSize &size) {\n\tExpects(_frameRead);\n\n\t_frameRead = false;\n\tindex = _frameIndex;\n\tif (!_width || !_height) {\n\t\t_width = _frame->width;\n\t\t_height = _frame->height;\n\t\tif (!_width || !_height) {\n\t\t\tLOG((\"Gif Error: Bad frame size %1\").arg(logData()));\n\t\t\treturn false;\n\t\t}\n\t}\n\tQSize toSize(size.isEmpty() ? QSize(_width, _height) : size);\n\tif (!size.isEmpty() && rotationSwapWidthHeight()) {\n\t\ttoSize.transpose();\n\t}\n\tif (to.isNull() || to.size() != toSize || !to.isDetached() || !isAlignedImage(to)) {\n\t\tto = createAlignedImage(toSize);\n\t}\n\tconst auto format = (_frame->format == AV_PIX_FMT_NONE)\n\t\t? _codecContext->pix_fmt\n\t\t: _frame->format;\n\tconst auto bgra = (format == AV_PIX_FMT_BGRA);\n\thasAlpha = bgra || (format == AV_PIX_FMT_YUVA420P);\n\tif (bgra\n\t\t&& _frame->width == toSize.width()\n\t\t&& _frame->height == toSize.height()\n\t\t&& _frame->linesize[0] > 0) {\n\t\tint32 sbpl = _frame->linesize[0], dbpl = to.bytesPerLine(), bpl = qMin(sbpl, dbpl);\n\t\tuchar *s = _frame->data[0], *d = to.bits();\n\t\tfor (int32 i = 0, l = _frame->height; i < l; ++i) {\n\t\t\tmemcpy(d + i * dbpl, s + i * sbpl, bpl);\n\t\t}\n\t} else {\n\t\tif ((_swsSize != toSize) || (_frame->format != -1 && _frame->format != _codecContext->pix_fmt) || !_swsContext) {\n\t\t\t_swsSize = toSize;\n\t\t\t_swsContext = sws_getCachedContext(_swsContext, _frame->width, _frame->height, AVPixelFormat(_frame->format), toSize.width(), toSize.height(), AV_PIX_FMT_BGRA, 0, nullptr, nullptr, nullptr);\n\t\t}\n\t\t// AV_NUM_DATA_POINTERS defined in AVFrame struct\n\t\tuint8_t *toData[AV_NUM_DATA_POINTERS] = { to.bits(), nullptr };\n\t\tint toLinesize[AV_NUM_DATA_POINTERS] = { int(to.bytesPerLine()), 0 };\n\t\tsws_scale(_swsContext, _frame->data, _frame->linesize, 0, _frame->height, toData, toLinesize);\n\t}\n\tif (hasAlpha) {\n\t\tFFmpeg::PremultiplyInplace(to);\n\t}\n\tif (_rotation != Rotation::None) {\n\t\tQTransform rotationTransform;\n\t\tswitch (_rotation) {\n\t\tcase Rotation::Degrees90: rotationTransform.rotate(90); break;\n\t\tcase Rotation::Degrees180: rotationTransform.rotate(180); break;\n\t\tcase Rotation::Degrees270: rotationTransform.rotate(270); break;\n\t\t}\n\t\tto = to.transformed(rotationTransform);\n\t}\n\n\tFFmpeg::ClearFrameMemory(_frame.get());\n\n\treturn true;\n}\n\nFFMpegReaderImplementation::Rotation FFMpegReaderImplementation::rotationFromDegrees(int degrees) const {\n\tswitch (degrees) {\n\tcase 90: return Rotation::Degrees90;\n\tcase 180: return Rotation::Degrees180;\n\tcase 270: return Rotation::Degrees270;\n\t}\n\treturn Rotation::None;\n}\n\nbool FFMpegReaderImplementation::start(Mode mode, crl::time &positionMs) {\n\t_mode = mode;\n\n\tinitDevice();\n\tif (!_device->open(QIODevice::ReadOnly)) {\n\t\tLOG((\"Gif Error: Unable to open device %1\").arg(logData()));\n\t\treturn false;\n\t}\n\t_ioBuffer = (uchar*)av_malloc(FFmpeg::kAVBlockSize);\n\t_ioContext = avio_alloc_context(_ioBuffer, FFmpeg::kAVBlockSize, 0, static_cast<void*>(this), &FFMpegReaderImplementation::Read, nullptr, &FFMpegReaderImplementation::Seek);\n\t_fmtContext = avformat_alloc_context();\n\tif (!_fmtContext) {\n\t\tLOG((\"Gif Error: Unable to avformat_alloc_context %1\").arg(logData()));\n\t\treturn false;\n\t}\n\t_fmtContext->pb = _ioContext;\n\n\tint res = 0;\n\tchar err[AV_ERROR_MAX_STRING_SIZE] = { 0 };\n\tif ((res = avformat_open_input(&_fmtContext, nullptr, nullptr, nullptr)) < 0) {\n\t\t_ioBuffer = nullptr;\n\n\t\tLOG((\"Gif Error: Unable to avformat_open_input %1, error %2, %3\").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\treturn false;\n\t}\n\t_opened = true;\n\n\tif ((res = avformat_find_stream_info(_fmtContext, nullptr)) < 0) {\n\t\tLOG((\"Gif Error: Unable to avformat_find_stream_info %1, error %2, %3\").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\treturn false;\n\t}\n\n\t_streamId = av_find_best_stream(_fmtContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);\n\tif (_streamId < 0) {\n\t\tLOG((\"Gif Error: Unable to av_find_best_stream %1, error %2, %3\").arg(logData()).arg(_streamId).arg(av_make_error_string(err, sizeof(err), _streamId)));\n\t\treturn false;\n\t}\n\n\tauto rotateTag = av_dict_get(_fmtContext->streams[_streamId]->metadata, \"rotate\", nullptr, 0);\n\tif (rotateTag && *rotateTag->value) {\n\t\tauto stringRotateTag = QString::fromUtf8(rotateTag->value);\n\t\tauto toIntSucceeded = false;\n\t\tauto rotateDegrees = stringRotateTag.toInt(&toIntSucceeded);\n\t\tif (toIntSucceeded) {\n\t\t\t_rotation = rotationFromDegrees(rotateDegrees);\n\t\t}\n\t}\n\n\t_codecContext = avcodec_alloc_context3(nullptr);\n\tif (!_codecContext) {\n\t\tLOG((\"Gif Error: Unable to avcodec_alloc_context3 %1\").arg(logData()));\n\t\treturn false;\n\t}\n\tif ((res = avcodec_parameters_to_context(_codecContext, _fmtContext->streams[_streamId]->codecpar)) < 0) {\n\t\tLOG((\"Gif Error: Unable to avcodec_parameters_to_context %1, error %2, %3\").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\treturn false;\n\t}\n\t_codecContext->pkt_timebase = _fmtContext->streams[_streamId]->time_base;\n\tav_opt_set_int(_codecContext, \"refcounted_frames\", 1, 0);\n\n\tconst auto codec = FFmpeg::FindDecoder(_codecContext);\n\tif (!codec) {\n\t\tLOG((\"Gif Error: Unable to avcodec_find_decoder %1\").arg(logData()));\n\t\treturn false;\n\t}\n\tif (_mode == Mode::Inspecting) {\n\t\tconst auto audioStreamId = av_find_best_stream(_fmtContext, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);\n\t\t_hasAudioStream = (audioStreamId >= 0);\n\t}\n\n\tif ((res = avcodec_open2(_codecContext, codec, nullptr)) < 0) {\n\t\tLOG((\"Gif Error: Unable to avcodec_open2 %1, error %2, %3\").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\treturn false;\n\t}\n\n\tif (positionMs > 0) {\n\t\tconst auto timeBase = _fmtContext->streams[_streamId]->time_base;\n\t\tconst auto timeStamp = (positionMs * timeBase.den)\n\t\t\t/ (1000LL * timeBase.num);\n\t\tif (av_seek_frame(_fmtContext, _streamId, timeStamp, 0) < 0) {\n\t\t\tif (av_seek_frame(_fmtContext, _streamId, timeStamp, AVSEEK_FLAG_BACKWARD) < 0) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\n\tFFmpeg::Packet packet;\n\tauto readResult = readPacket(packet);\n\tif (readResult == PacketResult::Ok && positionMs > 0) {\n\t\tpositionMs = countPacketMs(packet);\n\t}\n\n\tif (readResult == PacketResult::Ok) {\n\t\tprocessPacket(std::move(packet));\n\t}\n\n\treturn true;\n}\n\nbool FFMpegReaderImplementation::inspectAt(crl::time &positionMs) {\n\tif (positionMs > 0) {\n\t\tconst auto timeBase = _fmtContext->streams[_streamId]->time_base;\n\t\tconst auto timeStamp = (positionMs * timeBase.den)\n\t\t\t/ (1000LL * timeBase.num);\n\t\tif (av_seek_frame(_fmtContext, _streamId, timeStamp, 0) < 0) {\n\t\t\tif (av_seek_frame(_fmtContext, _streamId, timeStamp, AVSEEK_FLAG_BACKWARD) < 0) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\n\t_packetQueue.clear();\n\n\tFFmpeg::Packet packet;\n\tauto readResult = readPacket(packet);\n\tif (readResult == PacketResult::Ok && positionMs > 0) {\n\t\tpositionMs = countPacketMs(packet);\n\t}\n\n\tif (readResult == PacketResult::Ok) {\n\t\tprocessPacket(std::move(packet));\n\t}\n\n\treturn true;\n}\n\nbool FFMpegReaderImplementation::isGifv() const {\n\tif (_hasAudioStream) {\n\t\treturn false;\n\t}\n\tif (dataSize() > kMaxInMemory) {\n\t\treturn false;\n\t}\n\tif (_codecContext->codec_id != AV_CODEC_ID_H264) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nbool FFMpegReaderImplementation::isWebmSticker() const {\n\tif (_hasAudioStream) {\n\t\treturn false;\n\t}\n\tif (dataSize() > kMaxInMemory) {\n\t\treturn false;\n\t}\n\tif (_codecContext->codec_id != AV_CODEC_ID_VP9) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nQString FFMpegReaderImplementation::logData() const {\n\treturn u\"for file '%1', data size '%2'\"_q.arg(_location ? _location->name() : QString()).arg(_data->size());\n}\n\nFFMpegReaderImplementation::~FFMpegReaderImplementation() {\n\tif (_codecContext) avcodec_free_context(&_codecContext);\n\tif (_swsContext) sws_freeContext(_swsContext);\n\tif (_opened) {\n\t\tavformat_close_input(&_fmtContext);\n\t}\n\tif (_ioContext) {\n\t\tav_freep(&_ioContext->buffer);\n\t\tav_freep(&_ioContext);\n\t} else if (_ioBuffer) {\n\t\tav_freep(&_ioBuffer);\n\t}\n\tif (_fmtContext) avformat_free_context(_fmtContext);\n}\n\nFFMpegReaderImplementation::PacketResult FFMpegReaderImplementation::readPacket(FFmpeg::Packet &packet) {\n\tint res = 0;\n\tif ((res = av_read_frame(_fmtContext, &packet.fields())) < 0) {\n\t\tif (res == AVERROR_EOF) {\n\t\t\treturn PacketResult::EndOfFile;\n\t\t}\n\t\tchar err[AV_ERROR_MAX_STRING_SIZE] = { 0 };\n\t\tLOG((\"Gif Error: Unable to av_read_frame() %1, error %2, %3\").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));\n\t\treturn PacketResult::Error;\n\t}\n\treturn PacketResult::Ok;\n}\n\nvoid FFMpegReaderImplementation::processPacket(FFmpeg::Packet &&packet) {\n\tconst auto &native = packet.fields();\n\tauto videoPacket = (native.stream_index == _streamId);\n\tif (videoPacket) {\n\t\t_lastReadVideoMs = countPacketMs(packet);\n\t\t_packetQueue.push_back(std::move(packet));\n\t}\n}\n\ncrl::time FFMpegReaderImplementation::countPacketMs(\n\t\tconst FFmpeg::Packet &packet) const {\n\tconst auto &native = packet.fields();\n\tint64 packetPts = (native.pts == AV_NOPTS_VALUE) ? native.dts : native.pts;\n\tcrl::time packetMs = (packetPts * 1000LL * _fmtContext->streams[native.stream_index]->time_base.num) / _fmtContext->streams[native.stream_index]->time_base.den;\n\treturn packetMs;\n}\n\nFFMpegReaderImplementation::PacketResult FFMpegReaderImplementation::readAndProcessPacket() {\n\tFFmpeg::Packet packet;\n\tauto result = readPacket(packet);\n\tif (result == PacketResult::Ok) {\n\t\tprocessPacket(std::move(packet));\n\t}\n\treturn result;\n}\n\nint FFMpegReaderImplementation::Read(void *opaque, uint8_t *buf, int buf_size) {\n\tFFMpegReaderImplementation *l = reinterpret_cast<FFMpegReaderImplementation*>(opaque);\n\tint ret = l->_device->read((char*)(buf), buf_size);\n\tswitch (ret) {\n\tcase -1: return AVERROR_EXTERNAL;\n\tcase 0: return AVERROR_EOF;\n\tdefault: return ret;\n\t}\n}\n\nint64_t FFMpegReaderImplementation::Seek(void *opaque, int64_t offset, int whence) {\n\tFFMpegReaderImplementation *l = reinterpret_cast<FFMpegReaderImplementation*>(opaque);\n\n\tswitch (whence) {\n\tcase SEEK_SET: return l->_device->seek(offset) ? l->_device->pos() : -1;\n\tcase SEEK_CUR: return l->_device->seek(l->_device->pos() + offset) ? l->_device->pos() : -1;\n\tcase SEEK_END: return l->_device->seek(l->_device->size() + offset) ? l->_device->pos() : -1;\n\tcase AVSEEK_SIZE: {\n\t\t// Special whence for determining filesize without any seek.\n\t\treturn l->_dataSize;\n\t} break;\n\t}\n\treturn -1;\n}\n\n} // namespace internal\n} // namespace Clip\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/clip/media_clip_ffmpeg.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/clip/media_clip_implementation.h\"\n#include \"ffmpeg/ffmpeg_utility.h\"\n\nextern \"C\" {\n#include <libswscale/swscale.h>\n#include <libavutil/opt.h>\n} // extern \"C\"\n#include <deque>\n\n//#include \"media/streaming/media_streaming_utility.h\"\n\nnamespace Media {\nnamespace Clip {\nnamespace internal {\n\nconstexpr auto kMaxInMemory = 10 * 1024 * 1024;\n\nclass FFMpegReaderImplementation : public ReaderImplementation {\npublic:\n\tFFMpegReaderImplementation(Core::FileLocation *location, QByteArray *data);\n\n\tReadResult readFramesTill(crl::time frameMs, crl::time systemMs) override;\n\n\tcrl::time frameRealTime() const override;\n\tcrl::time framePresentationTime() const override;\n\n\tbool renderFrame(\n\t\tQImage &to,\n\t\tbool &hasAlpha,\n\t\tint &index,\n\t\tconst QSize &size) override;\n\n\tcrl::time durationMs() const override;\n\n\tbool start(Mode mode, crl::time &positionMs) override;\n\tbool inspectAt(crl::time &positionMs);\n\n\tQString logData() const;\n\n\tbool isGifv() const;\n\tbool isWebmSticker() const;\n\n\t~FFMpegReaderImplementation();\n\nprivate:\n\tReadResult readNextFrame();\n\tvoid processReadFrame();\n\n\tenum class PacketResult {\n\t\tOk,\n\t\tEndOfFile,\n\t\tError,\n\t};\n\tPacketResult readPacket(FFmpeg::Packet &packet);\n\tvoid processPacket(FFmpeg::Packet &&packet);\n\tcrl::time countPacketMs(const FFmpeg::Packet &packet) const;\n\tPacketResult readAndProcessPacket();\n\n\tenum class Rotation {\n\t\tNone,\n\t\tDegrees90,\n\t\tDegrees180,\n\t\tDegrees270,\n\t};\n\tRotation rotationFromDegrees(int degrees) const;\n\tbool rotationSwapWidthHeight() const {\n\t\treturn (_rotation == Rotation::Degrees90) || (_rotation == Rotation::Degrees270);\n\t}\n\n\tstatic int Read(void *opaque, uint8_t *buf, int buf_size);\n\tstatic int64_t Seek(void *opaque, int64_t offset, int whence);\n\n\tMode _mode = Mode::Silent;\n\n\tRotation _rotation = Rotation::None;\n\n\tuchar *_ioBuffer = nullptr;\n\tAVIOContext *_ioContext = nullptr;\n\tAVFormatContext *_fmtContext = nullptr;\n\tAVCodecContext *_codecContext = nullptr;\n\tint _streamId = 0;\n\tFFmpeg::FramePointer _frame;\n\tint _frameIndex = -1;\n\tbool _opened = false;\n\tbool _hadFrame = false;\n\tbool _frameRead = false;\n\tint _skippedInvalidDataPackets = 0;\n\n\tbool _hasAudioStream = false;\n\tcrl::time _lastReadVideoMs = 0;\n\tcrl::time _lastReadAudioMs = 0;\n\n\tstd::deque<FFmpeg::Packet> _packetQueue;\n\n\tint _width = 0;\n\tint _height = 0;\n\tSwsContext *_swsContext = nullptr;\n\tQSize _swsSize;\n\n\tcrl::time _frameMs = 0;\n\tint _nextFrameDelay = 0;\n\tint _currentFrameDelay = 0;\n\n\tcrl::time _frameTime = 0;\n\tcrl::time _frameTimeCorrection = 0;\n\n};\n\n} // namespace internal\n} // namespace Clip\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/clip/media_clip_implementation.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/clip/media_clip_implementation.h\"\n\n#include \"core/file_location.h\"\n\nnamespace Media {\nnamespace Clip {\nnamespace internal {\n\nvoid ReaderImplementation::initDevice() {\n\tif (_data->isEmpty()) {\n\t\tif (_file.isOpen()) _file.close();\n\t\t_file.setFileName(_location->name());\n\t\t_dataSize = _file.size();\n\t} else {\n\t\tif (_buffer.isOpen()) _buffer.close();\n\t\t_buffer.setBuffer(_data);\n\t\t_dataSize = _data->size();\n\t}\n\t_device = _data->isEmpty() ? static_cast<QIODevice*>(&_file) : static_cast<QIODevice*>(&_buffer);\n}\n\n} // namespace internal\n} // namespace Clip\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/clip/media_clip_implementation.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QBuffer>\n#include <QtCore/QFile>\n\nnamespace Core {\nclass FileLocation;\n} // namespace Core\n\nnamespace Media {\nnamespace Clip {\nnamespace internal {\n\nclass ReaderImplementation {\npublic:\n\tReaderImplementation(Core::FileLocation *location, QByteArray *data)\n\t: _location(location)\n\t, _data(data) {\n\t}\n\tenum class Mode {\n\t\tSilent,\n\t\tInspecting, // Not playing video, but reading data.\n\t};\n\n\tenum class ReadResult {\n\t\tSuccess,\n\t\tError,\n\t\tEndOfFile,\n\t};\n\t// Read frames till current frame will have presentation time > frameMs, systemMs = crl::now().\n\tvirtual ReadResult readFramesTill(crl::time frameMs, crl::time systemMs) = 0;\n\n\t// Get current frame real and presentation time.\n\tvirtual crl::time frameRealTime() const = 0;\n\tvirtual crl::time framePresentationTime() const = 0;\n\n\t// Render current frame to an image with specific size.\n\tvirtual bool renderFrame(\n\t\tQImage &to,\n\t\tbool &hasAlpha,\n\t\tint &index,\n\t\tconst QSize &size) = 0;\n\n\tvirtual crl::time durationMs() const = 0;\n\n\tvirtual bool start(Mode mode, crl::time &positionMs) = 0;\n\n\tvirtual ~ReaderImplementation() {\n\t}\n\tint64 dataSize() const {\n\t\treturn _dataSize;\n\t}\n\nprotected:\n\tCore::FileLocation *_location = nullptr;\n\tQByteArray *_data = nullptr;\n\tQFile _file;\n\tQBuffer _buffer;\n\tQIODevice *_device = nullptr;\n\tint64 _dataSize = 0;\n\n\tvoid initDevice();\n\n};\n\n} // namespace internal\n} // namespace Clip\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/clip/media_clip_reader.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/clip/media_clip_reader.h\"\n\n#include \"media/clip/media_clip_ffmpeg.h\"\n#include \"media/clip/media_clip_check_streaming.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/painter.h\"\n#include \"core/file_location.h\"\n#include \"base/random.h\"\n#include \"base/invoke_queued.h\"\n#include \"logs.h\"\n\n#include <QtCore/QBuffer>\n#include <QtCore/QAbstractEventDispatcher>\n#include <QtCore/QCoreApplication>\n#include <QtCore/QThread>\n#include <QtCore/QFileInfo>\n\nextern \"C\" {\n#include <libavcodec/avcodec.h>\n#include <libavformat/avformat.h>\n#include <libavutil/opt.h>\n#include <libswscale/swscale.h>\n} // extern \"C\"\n\nnamespace Media {\nnamespace Clip {\nnamespace {\n\nconstexpr auto kClipThreadsCount = 8;\nconstexpr auto kAverageGifSize = 320 * 240;\nconstexpr auto kWaitBeforeGifPause = crl::time(200);\n\nQImage PrepareFrame(\n\t\tconst FrameRequest &request,\n\t\tconst QImage &original,\n\t\tbool hasAlpha,\n\t\tQImage &cache) {\n\tconst auto needResize = (original.size() != request.frame);\n\tconst auto needOuterFill = request.outer.isValid()\n\t\t&& (request.outer != request.frame);\n\tconst auto needRounding = (request.radius != ImageRoundRadius::None);\n\tconst auto colorizing = (request.colored.alpha() != 0);\n\tif (!needResize\n\t\t&& !needOuterFill\n\t\t&& !hasAlpha\n\t\t&& !needRounding\n\t\t&& !colorizing) {\n\t\treturn original;\n\t}\n\n\tconst auto factor = request.factor;\n\tconst auto size = request.outer.isValid() ? request.outer : request.frame;\n\tconst auto needNewCache = (cache.size() != size);\n\tif (needNewCache) {\n\t\tcache = QImage(size, QImage::Format_ARGB32_Premultiplied);\n\t\tcache.setDevicePixelRatio(factor);\n\t}\n\tif (hasAlpha && request.keepAlpha) {\n\t\tcache.fill(Qt::transparent);\n\t}\n\t{\n\t\tauto p = QPainter(&cache);\n\t\tconst auto framew = request.frame.width();\n\t\tconst auto outerw = size.width();\n\t\tconst auto frameh = request.frame.height();\n\t\tconst auto outerh = size.height();\n\t\tif (needNewCache && (!hasAlpha || !request.keepAlpha)) {\n\t\t\tif (framew < outerw) {\n\t\t\t\tp.fillRect(0, 0, (outerw - framew) / (2 * factor), cache.height() / factor, st::imageBg);\n\t\t\t\tp.fillRect((outerw - framew) / (2 * factor) + (framew / factor), 0, (cache.width() / factor) - ((outerw - framew) / (2 * factor) + (framew / factor)), cache.height() / factor, st::imageBg);\n\t\t\t}\n\t\t\tif (frameh < outerh) {\n\t\t\t\tp.fillRect(qMax(0, (outerw - framew) / (2 * factor)), 0, qMin(cache.width(), framew) / factor, (outerh - frameh) / (2 * factor), st::imageBg);\n\t\t\t\tp.fillRect(qMax(0, (outerw - framew) / (2 * factor)), (outerh - frameh) / (2 * factor) + (frameh / factor), qMin(cache.width(), framew) / factor, (cache.height() / factor) - ((outerh - frameh) / (2 * factor) + (frameh / factor)), st::imageBg);\n\t\t\t}\n\t\t}\n\t\tif (hasAlpha && !request.keepAlpha) {\n\t\t\tp.fillRect(qMax(0, (outerw - framew) / (2 * factor)), qMax(0, (outerh - frameh) / (2 * factor)), qMin(cache.width(), framew) / factor, qMin(cache.height(), frameh) / factor, st::imageBgTransparent);\n\t\t}\n\t\tconst auto position = QPoint((outerw - framew) / (2 * factor), (outerh - frameh) / (2 * factor));\n\t\tif (needResize) {\n\t\t\tPainterHighQualityEnabler hq(p);\n\n\t\t\tconst auto dst = QRect(position, QSize(framew / factor, frameh / factor));\n\t\t\tconst auto src = QRect(0, 0, original.width(), original.height());\n\t\t\tp.drawImage(dst, original, src, Qt::ColorOnly);\n\t\t} else {\n\t\t\tp.drawImage(position, original);\n\t\t}\n\t}\n\tif (needRounding) {\n\t\tcache = Images::Round(\n\t\t\tstd::move(cache),\n\t\t\trequest.radius,\n\t\t\trequest.corners);\n\t}\n\tif (colorizing) {\n\t\tcache = Images::Colored(std::move(cache), request.colored);\n\t}\n\treturn cache;\n}\n\n} // namespace\n\nenum class ProcessResult {\n\tError,\n\tStarted,\n\tFinished,\n\tPaused,\n\tRepaint,\n\tCopyFrame,\n\tWait,\n};\n\nclass Manager final : public QObject {\npublic:\n\texplicit Manager(not_null<QThread*> thread);\n\t~Manager();\n\n\tint loadLevel() const {\n\t\treturn _loadLevel;\n\t}\n\tvoid append(Reader *reader, const Core::FileLocation &location, const QByteArray &data);\n\tvoid start(Reader *reader);\n\tvoid update(Reader *reader);\n\tvoid stop(Reader *reader);\n\tbool carries(Reader *reader) const;\n\nprivate:\n\tvoid process();\n\tvoid finish();\n\tvoid callback(Reader *reader, Notification notification);\n\tvoid clear();\n\n\tQAtomicInt _loadLevel;\n\tusing ReaderPointers = QMap<Reader*, QAtomicInt>;\n\tReaderPointers _readerPointers;\n\tmutable QMutex _readerPointersMutex;\n\n\tReaderPointers::const_iterator constUnsafeFindReaderPointer(ReaderPrivate *reader) const;\n\tReaderPointers::iterator unsafeFindReaderPointer(ReaderPrivate *reader);\n\n\tbool handleProcessResult(ReaderPrivate *reader, ProcessResult result, crl::time ms);\n\n\tenum ResultHandleState {\n\t\tResultHandleRemove,\n\t\tResultHandleStop,\n\t\tResultHandleContinue,\n\t};\n\tResultHandleState handleResult(ReaderPrivate *reader, ProcessResult result, crl::time ms);\n\n\tusing Readers = QMap<ReaderPrivate*, crl::time>;\n\tReaders _readers;\n\n\tQTimer _timer;\n\tQThread *_processingInThread = nullptr;\n\tbool _needReProcess = false;\n\n};\n\nnamespace {\n\nstruct Worker {\n\tWorker() : manager(&thread) {\n\t\tthread.start();\n\t}\n\t~Worker() {\n\t\tthread.quit();\n\t\tthread.wait();\n\t}\n\n\tQThread thread;\n\tManager manager;\n};\n\nstd::vector<std::unique_ptr<Worker>> Workers;\n\n} // namespace\n\nReader::Reader(\n\tconst Core::FileLocation &location,\n\tconst QByteArray &data,\n\tCallback &&callback)\n: _callback(std::move(callback)) {\n\tinit(location, data);\n}\n\nReader::Reader(const QString &filepath, Callback &&callback)\n: _callback(std::move(callback)) {\n\tinit(Core::FileLocation(filepath), QByteArray());\n}\n\nReader::Reader(const QByteArray &data, Callback &&callback)\n: _callback(std::move(callback)) {\n\tinit(Core::FileLocation(QString()), data);\n}\n\nvoid Reader::init(const Core::FileLocation &location, const QByteArray &data) {\n\tif (Workers.size() < kClipThreadsCount) {\n\t\t_threadIndex = Workers.size();\n\t\tWorkers.push_back(std::make_unique<Worker>());\n\t} else {\n\t\t_threadIndex = base::RandomIndex(Workers.size());\n\t\tauto loadLevel = 0x7FFFFFFF;\n\t\tfor (int i = 0, l = int(Workers.size()); i < l; ++i) {\n\t\t\tconst auto level = Workers[i]->manager.loadLevel();\n\t\t\tif (level < loadLevel) {\n\t\t\t\t_threadIndex = i;\n\t\t\t\tloadLevel = level;\n\t\t\t}\n\t\t}\n\t}\n\tWorkers[_threadIndex]->manager.append(this, location, data);\n}\n\nReader::Frame *Reader::frameToShow(int32 *index) const { // 0 means not ready\n\tint step = _step.loadAcquire(), i;\n\tif (step == kWaitingForDimensionsStep) {\n\t\tif (index) *index = 0;\n\t\treturn nullptr;\n\t} else if (step == kWaitingForRequestStep) {\n\t\ti = 0;\n\t} else if (step == kWaitingForFirstFrameStep) {\n\t\ti = 0;\n\t} else {\n\t\ti = (step / 2) % 3;\n\t}\n\tif (index) *index = i;\n\treturn _frames + i;\n}\n\nReader::Frame *Reader::frameToWrite(int32 *index) const { // 0 means not ready\n\tint32 step = _step.loadAcquire(), i;\n\tif (step == kWaitingForDimensionsStep) {\n\t\ti = 0;\n\t} else if (step == kWaitingForRequestStep) {\n\t\tif (index) *index = 0;\n\t\treturn nullptr;\n\t} else if (step == kWaitingForFirstFrameStep) {\n\t\ti = 0;\n\t} else {\n\t\ti = ((step + 2) / 2) % 3;\n\t}\n\tif (index) *index = i;\n\treturn _frames + i;\n}\n\nReader::Frame *Reader::frameToWriteNext(bool checkNotWriting, int32 *index) const {\n\tint32 step = _step.loadAcquire(), i;\n\tif (step == kWaitingForDimensionsStep\n\t\t|| step == kWaitingForRequestStep\n\t\t|| (checkNotWriting && (step % 2))) {\n\t\tif (index) *index = 0;\n\t\treturn nullptr;\n\t}\n\ti = ((step + 4) / 2) % 3;\n\tif (index) *index = i;\n\treturn _frames + i;\n}\n\nbool Reader::moveToNextShow() const {\n\tconst auto step = _step.loadAcquire();\n\tif (step == kWaitingForDimensionsStep) {\n\t} else if (step == kWaitingForRequestStep) {\n\t\t_step.storeRelease(kWaitingForFirstFrameStep);\n\t\treturn true;\n\t} else if (step == kWaitingForFirstFrameStep) {\n\t} else if (!(step % 2)) {\n\t\t_step.storeRelease(step + 1);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Reader::moveToNextWrite() const {\n\tint32 step = _step.loadAcquire();\n\tif (step == kWaitingForDimensionsStep) {\n\t\t_step.storeRelease(kWaitingForRequestStep);\n\t} else if (step == kWaitingForRequestStep) {\n\t} else if (step == kWaitingForFirstFrameStep) {\n\t\t_step.storeRelease(0);\n\n\t\t// Force paint the first frame so moveToNextShow() is called.\n\t\t_frames[0].displayed.storeRelease(0);\n\t} else if (step % 2) {\n\t\t_step.storeRelease((step + 1) % 6);\n\t}\n}\n\nvoid Reader::SafeCallback(\n\t\tReader *reader,\n\t\tint threadIndex,\n\t\tNotification notification) {\n\t// Check if reader is not deleted already\n\tif (Workers.size() > threadIndex\n\t\t&& Workers[threadIndex]->manager.carries(reader)\n\t\t&& reader->_callback) {\n\t\treader->_callback(Notification(notification));\n\t}\n}\n\nvoid Reader::start(FrameRequest request) {\n\tif (Workers.size() <= _threadIndex) {\n\t\terror();\n\t}\n\tif (_state == State::Error\n\t\t|| (_step.loadAcquire() != kWaitingForRequestStep)) {\n\t\treturn;\n\t}\n\tconst auto factor = style::DevicePixelRatio();\n\trequest.factor = factor;\n\trequest.frame *= factor;\n\tif (request.outer.isValid()) {\n\t\trequest.outer *= factor;\n\t}\n\t_frames[0].request = _frames[1].request = _frames[2].request = request;\n\tmoveToNextShow();\n\tWorkers[_threadIndex]->manager.start(this);\n}\n\nReader::FrameInfo Reader::frameInfo(FrameRequest request, crl::time now) {\n\tExpects(!(request.outer.isValid()\n\t\t? request.outer\n\t\t: request.frame).isEmpty());\n\n\tconst auto frame = frameToShow();\n\tAssert(frame != nullptr);\n\n\tconst auto shouldBePaused = !now;\n\tif (!shouldBePaused) {\n\t\tframe->displayed.storeRelease(1);\n\t\tif (_autoPausedGif.loadAcquire()) {\n\t\t\t_autoPausedGif.storeRelease(0);\n\t\t\tif (Workers.size() <= _threadIndex) {\n\t\t\t\terror();\n\t\t\t} else if (_state != State::Error) {\n\t\t\t\tWorkers[_threadIndex]->manager.update(this);\n\t\t\t}\n\t\t}\n\t} else {\n\t\tframe->displayed.storeRelease(-1);\n\t}\n\n\tconst auto factor = style::DevicePixelRatio();\n\trequest.factor = factor;\n\trequest.frame *= factor;\n\tif (request.outer.isValid()) {\n\t\trequest.outer *= factor;\n\t}\n\tconst auto size = request.outer.isValid()\n\t\t? request.outer\n\t\t: request.frame;\n\tAssert(frame->request.radius == request.radius\n\t\t&& frame->request.corners == request.corners\n\t\t&& frame->request.keepAlpha == request.keepAlpha);\n\tif (frame->prepared.size() != size\n\t\t|| frame->preparedColored != request.colored) {\n\t\tframe->request.frame = request.frame;\n\t\tframe->request.outer = request.outer;\n\t\tframe->request.colored = request.colored;\n\n\t\tQImage cacheForResize;\n\t\tframe->original.setDevicePixelRatio(factor);\n\t\tframe->prepared = QImage();\n\t\tframe->prepared = PrepareFrame(\n\t\t\tframe->request,\n\t\t\tframe->original,\n\t\t\ttrue,\n\t\t\tcacheForResize);\n\t\tframe->preparedColored = request.colored;\n\n\t\tauto other = frameToWriteNext(true);\n\t\tif (other) other->request = frame->request;\n\n\t\tif (Workers.size() <= _threadIndex) {\n\t\t\terror();\n\t\t} else if (_state != State::Error) {\n\t\t\tWorkers[_threadIndex]->manager.update(this);\n\t\t}\n\t}\n\treturn { frame->prepared, frame->index };\n}\n\nbool Reader::ready() const {\n\tif (_width && _height) {\n\t\treturn true;\n\t}\n\n\tconst auto frame = frameToShow();\n\tif (frame) {\n\t\t_width = frame->original.width();\n\t\t_height = frame->original.height();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\ncrl::time Reader::getPositionMs() const {\n\tif (const auto frame = frameToShow()) {\n\t\treturn frame->positionMs;\n\t}\n\treturn 0;\n}\n\ncrl::time Reader::getDurationMs() const {\n\treturn ready() ? _durationMs : 0;\n}\n\nvoid Reader::pauseResumeVideo() {\n\tif (Workers.size() <= _threadIndex) {\n\t\terror();\n\t}\n\tif (_state == State::Error) return;\n\n\t_videoPauseRequest.storeRelease(1 - _videoPauseRequest.loadAcquire());\n\tWorkers[_threadIndex]->manager.start(this);\n}\n\nbool Reader::videoPaused() const {\n\treturn _videoPauseRequest.loadAcquire() != 0;\n}\n\nint32 Reader::width() const {\n\treturn _width;\n}\n\nint32 Reader::height() const {\n\treturn _height;\n}\n\nState Reader::state() const {\n\treturn _state;\n}\n\nvoid Reader::stop() {\n\tif (Workers.size() <= _threadIndex) {\n\t\terror();\n\t}\n\tif (_state != State::Error) {\n\t\tWorkers[_threadIndex]->manager.stop(this);\n\t\t_width = _height = 0;\n\t}\n}\n\nvoid Reader::error() {\n\t_state = State::Error;\n\t_private = nullptr;\n}\n\nvoid Reader::finished() {\n\t_state = State::Finished;\n\t_private = nullptr;\n}\n\nReader::~Reader() {\n\tstop();\n}\n\nclass ReaderPrivate {\npublic:\n\tReaderPrivate(Reader *reader, const Core::FileLocation &location, const QByteArray &data)\n\t: _interface(reader)\n\t, _data(data) {\n\t\tif (_data.isEmpty()) {\n\t\t\t_location = std::make_unique<Core::FileLocation>(location);\n\t\t\tif (!_location->accessEnable()) {\n\t\t\t\terror();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\t_accessed = true;\n\t}\n\n\tProcessResult start(crl::time ms) {\n\t\tif (!_implementation && !init()) {\n\t\t\treturn error();\n\t\t}\n\t\tif (frame()->original.isNull()) {\n\t\t\tauto readResult = _implementation->readFramesTill(-1, ms);\n\t\t\tif (readResult == internal::ReaderImplementation::ReadResult::EndOfFile && _seekPositionMs > 0) {\n\t\t\t\t// If seek was done to the end: try to read the first frame,\n\t\t\t\t// get the frame size and return a black frame with that size.\n\n\t\t\t\tauto firstFramePositionMs = crl::time(0);\n\t\t\t\tauto reader = std::make_unique<internal::FFMpegReaderImplementation>(_location.get(), &_data);\n\t\t\t\tif (reader->start(internal::ReaderImplementation::Mode::Silent, firstFramePositionMs)) {\n\t\t\t\t\tauto firstFrameReadResult = reader->readFramesTill(-1, ms);\n\t\t\t\t\tif (firstFrameReadResult == internal::ReaderImplementation::ReadResult::Success) {\n\t\t\t\t\t\tif (reader->renderFrame(frame()->original, frame()->alpha, frame()->index, QSize())) {\n\t\t\t\t\t\t\tframe()->original.fill(QColor(0, 0, 0));\n\n\t\t\t\t\t\t\tframe()->positionMs = _seekPositionMs;\n\n\t\t\t\t\t\t\t_width = frame()->original.width();\n\t\t\t\t\t\t\t_height = frame()->original.height();\n\t\t\t\t\t\t\t_durationMs = _implementation->durationMs();\n\t\t\t\t\t\t\treturn ProcessResult::Started;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn error();\n\t\t\t} else if (readResult != internal::ReaderImplementation::ReadResult::Success) { // Read the first frame.\n\t\t\t\treturn error();\n\t\t\t}\n\t\t\tif (!_implementation->renderFrame(frame()->original, frame()->alpha, frame()->index, QSize())) {\n\t\t\t\treturn error();\n\t\t\t}\n\t\t\tframe()->positionMs = _implementation->frameRealTime();\n\n\t\t\t_width = frame()->original.width();\n\t\t\t_height = frame()->original.height();\n\t\t\t_durationMs = _implementation->durationMs();\n\t\t\treturn ProcessResult::Started;\n\t\t}\n\t\treturn ProcessResult::Wait;\n\t}\n\n\tProcessResult process(crl::time ms) { // -1 - do nothing, 0 - update, 1 - reinit\n\t\tif (_state == State::Error) {\n\t\t\treturn ProcessResult::Error;\n\t\t} else if (_state == State::Finished) {\n\t\t\treturn ProcessResult::Finished;\n\t\t}\n\n\t\tif (!_request.valid()) {\n\t\t\treturn start(ms);\n\t\t}\n\t\tif (!_started) {\n\t\t\t_started = true;\n\t\t}\n\n\t\tif (!_autoPausedGif && !_videoPausedAtMs && ms >= _nextFrameWhen) {\n\t\t\treturn ProcessResult::Repaint;\n\t\t}\n\t\treturn ProcessResult::Wait;\n\t}\n\n\tProcessResult finishProcess(crl::time ms) {\n\t\tauto frameMs = _seekPositionMs + ms - _animationStarted;\n\t\tauto readResult = _implementation->readFramesTill(frameMs, ms);\n\t\tif (readResult == internal::ReaderImplementation::ReadResult::EndOfFile) {\n\t\t\tstop();\n\t\t\t_state = State::Finished;\n\t\t\treturn ProcessResult::Finished;\n\t\t} else if (readResult == internal::ReaderImplementation::ReadResult::Error) {\n\t\t\treturn error();\n\t\t}\n\t\t_nextFramePositionMs = _implementation->frameRealTime();\n\t\t_nextFrameWhen = _animationStarted + _implementation->framePresentationTime();\n\t\tif (_nextFrameWhen > _seekPositionMs) {\n\t\t\t_nextFrameWhen -= _seekPositionMs;\n\t\t} else {\n\t\t\t_nextFrameWhen = 1;\n\t\t}\n\n\t\tif (!renderFrame()) {\n\t\t\treturn error();\n\t\t}\n\t\treturn ProcessResult::CopyFrame;\n\t}\n\n\tbool renderFrame() {\n\t\tExpects(_request.valid());\n\n\t\tif (!_implementation->renderFrame(frame()->original, frame()->alpha, frame()->index, _request.frame)) {\n\t\t\treturn false;\n\t\t}\n\t\tframe()->original.setDevicePixelRatio(_request.factor);\n\t\tframe()->prepared = QImage();\n\t\tframe()->prepared = PrepareFrame(\n\t\t\t_request,\n\t\t\tframe()->original,\n\t\t\tframe()->alpha,\n\t\t\tframe()->cache);\n\t\tframe()->preparedColored = _request.colored;\n\t\tframe()->when = _nextFrameWhen;\n\t\tframe()->positionMs = _nextFramePositionMs;\n\t\treturn true;\n\t}\n\n\tbool init() {\n\t\tif (_data.isEmpty() && QFileInfo(_location->name()).size() <= internal::kMaxInMemory) {\n\t\t\tQFile f(_location->name());\n\t\t\tif (f.open(QIODevice::ReadOnly)) {\n\t\t\t\t_data = f.readAll();\n\t\t\t\tif (f.error() != QFile::NoError) {\n\t\t\t\t\t_data = QByteArray();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t_implementation = std::make_unique<internal::FFMpegReaderImplementation>(_location.get(), &_data);\n\n\t\treturn _implementation->start(internal::ReaderImplementation::Mode::Silent, _seekPositionMs);\n\t}\n\n\tvoid startedAt(crl::time ms) {\n\t\t_animationStarted = _nextFrameWhen = ms;\n\t}\n\n\tvoid pauseVideo(crl::time ms) {\n\t\tif (_videoPausedAtMs) return; // Paused already.\n\n\t\t_videoPausedAtMs = ms;\n\t}\n\n\tvoid resumeVideo(crl::time ms) {\n\t\tif (!_videoPausedAtMs) return; // Not paused.\n\n\t\tauto delta = ms - _videoPausedAtMs;\n\t\t_animationStarted += delta;\n\t\t_nextFrameWhen += delta;\n\n\t\t_videoPausedAtMs = 0;\n\t}\n\n\tProcessResult error() {\n\t\tstop();\n\t\t_state = State::Error;\n\t\treturn ProcessResult::Error;\n\t}\n\n\tvoid stop() {\n\t\t_implementation = nullptr;\n\t\tif (_location) {\n\t\t\tif (_accessed) {\n\t\t\t\t_location->accessDisable();\n\t\t\t}\n\t\t\t_location = nullptr;\n\t\t}\n\t\t_accessed = false;\n\t}\n\n\t~ReaderPrivate() {\n\t\tstop();\n\t\t_data.clear();\n\t}\n\nprivate:\n\tReader *_interface;\n\tState _state = State::Reading;\n\tcrl::time _seekPositionMs = 0;\n\n\tQByteArray _data;\n\tstd::unique_ptr<Core::FileLocation> _location;\n\tbool _accessed = false;\n\n\tQBuffer _buffer;\n\tstd::unique_ptr<internal::ReaderImplementation> _implementation;\n\n\tFrameRequest _request;\n\tstruct Frame {\n\t\tQImage prepared;\n\t\tQColor preparedColored = QColor(0, 0, 0, 0);\n\t\tQImage original;\n\t\tQImage cache;\n\t\tint index = 0;\n\t\tbool alpha = true;\n\t\tcrl::time when = 0;\n\n\t\t// Counted from the end, so that positionMs <= durationMs despite keep up delays.\n\t\tcrl::time positionMs = 0;\n\t};\n\tFrame _frames[3];\n\tint _frame = 0;\n\tnot_null<Frame*> frame() {\n\t\treturn _frames + _frame;\n\t}\n\n\tint _width = 0;\n\tint _height = 0;\n\n\tcrl::time _durationMs = 0;\n\tcrl::time _animationStarted = 0;\n\tcrl::time _nextFrameWhen = 0;\n\tcrl::time _nextFramePositionMs = 0;\n\n\tbool _autoPausedGif = false;\n\tbool _started = false;\n\tcrl::time _videoPausedAtMs = 0;\n\n\tfriend class Manager;\n\n};\n\nManager::Manager(not_null<QThread*> thread) {\n\tmoveToThread(thread);\n\tconnect(thread, &QThread::started, this, [=] { process(); });\n\tconnect(thread, &QThread::finished, this, [=] { finish(); });\n\n\t_timer.setSingleShot(true);\n\t_timer.moveToThread(thread);\n\tconnect(&_timer, &QTimer::timeout, this, [=] { process(); });\n}\n\nvoid Manager::append(Reader *reader, const Core::FileLocation &location, const QByteArray &data) {\n\treader->_private = new ReaderPrivate(reader, location, data);\n\t_loadLevel.fetchAndAddRelaxed(kAverageGifSize);\n\tupdate(reader);\n}\n\nvoid Manager::start(Reader *reader) {\n\tupdate(reader);\n}\n\nvoid Manager::update(Reader *reader) {\n\tQMutexLocker lock(&_readerPointersMutex);\n\tauto i = _readerPointers.find(reader);\n\tif (i == _readerPointers.cend()) {\n\t\t_readerPointers.insert(reader, QAtomicInt(1));\n\t} else {\n\t\ti->storeRelease(1);\n\t}\n\tInvokeQueued(this, [=] { process(); });\n}\n\nvoid Manager::stop(Reader *reader) {\n\tif (!carries(reader)) return;\n\n\tQMutexLocker lock(&_readerPointersMutex);\n\t_readerPointers.remove(reader);\n\tInvokeQueued(this, [=] { process(); });\n}\n\nbool Manager::carries(Reader *reader) const {\n\tQMutexLocker lock(&_readerPointersMutex);\n\treturn _readerPointers.contains(reader);\n}\n\nauto Manager::unsafeFindReaderPointer(ReaderPrivate *reader)\n-> ReaderPointers::iterator {\n\tconst auto it = _readerPointers.find(reader->_interface);\n\n\t// could be a new reader which was realloced in the same address\n\treturn (it == _readerPointers.cend() || it.key()->_private == reader)\n\t\t? it\n\t\t: _readerPointers.end();\n}\n\nauto Manager::constUnsafeFindReaderPointer(ReaderPrivate *reader) const\n-> ReaderPointers::const_iterator {\n\tconst auto it = _readerPointers.constFind(reader->_interface);\n\n\t// could be a new reader which was realloced in the same address\n\treturn (it == _readerPointers.cend() || it.key()->_private == reader)\n\t\t? it\n\t\t: _readerPointers.cend();\n}\n\nvoid Manager::callback(Reader *reader, Notification notification) {\n\tcrl::on_main([=, threadIndex = reader->threadIndex()] {\n\t\tReader::SafeCallback(reader, threadIndex, notification);\n\t});\n}\n\nbool Manager::handleProcessResult(ReaderPrivate *reader, ProcessResult result, crl::time ms) {\n\tQMutexLocker lock(&_readerPointersMutex);\n\tauto it = unsafeFindReaderPointer(reader);\n\tif (result == ProcessResult::Error) {\n\t\tif (it != _readerPointers.cend()) {\n\t\t\tit.key()->error();\n\t\t\tcallback(it.key(), Notification::Reinit);\n\t\t\t_readerPointers.erase(it);\n\t\t}\n\t\treturn false;\n\t} else if (result == ProcessResult::Finished) {\n\t\tif (it != _readerPointers.cend()) {\n\t\t\tit.key()->finished();\n\t\t\tcallback(it.key(), Notification::Reinit);\n\t\t}\n\t\treturn false;\n\t}\n\tif (it == _readerPointers.cend()) {\n\t\treturn false;\n\t}\n\n\tif (result == ProcessResult::Started) {\n\t\t_loadLevel.fetchAndAddRelaxed(reader->_width * reader->_height - kAverageGifSize);\n\t\tit.key()->_durationMs = reader->_durationMs;\n\t}\n\t// See if we need to pause GIF because it is not displayed right now.\n\tif (!reader->_autoPausedGif && result == ProcessResult::Repaint) {\n\t\tint32 ishowing, iprevious;\n\t\tauto showing = it.key()->frameToShow(&ishowing), previous = it.key()->frameToWriteNext(false, &iprevious);\n\t\tAssert(previous != nullptr && showing != nullptr && ishowing >= 0 && iprevious >= 0);\n\t\tif (reader->_frames[ishowing].when > 0 && showing->displayed.loadAcquire() <= 0) { // current frame was not shown\n\t\t\tif (reader->_frames[ishowing].when + kWaitBeforeGifPause < ms || (reader->_frames[iprevious].when && previous->displayed.loadAcquire() <= 0)) {\n\t\t\t\treader->_autoPausedGif = true;\n\t\t\t\tit.key()->_autoPausedGif.storeRelease(1);\n\t\t\t\tresult = ProcessResult::Paused;\n\t\t\t}\n\t\t}\n\t}\n\tif (result == ProcessResult::Started || result == ProcessResult::CopyFrame) {\n\t\tAssert(reader->_frame >= 0);\n\t\tauto frame = it.key()->_frames + reader->_frame;\n\t\tframe->clear();\n\t\tframe->prepared = reader->frame()->prepared;\n\t\tframe->preparedColored = reader->frame()->preparedColored;\n\t\tframe->original = reader->frame()->original;\n\t\tframe->index = reader->frame()->index;\n\t\tframe->displayed.storeRelease(0);\n\t\tframe->positionMs = reader->frame()->positionMs;\n\t\tif (result == ProcessResult::Started) {\n\t\t\treader->startedAt(ms);\n\t\t\tit.key()->moveToNextWrite();\n\t\t\tcallback(it.key(), Notification::Reinit);\n\t\t}\n\t} else if (result == ProcessResult::Paused) {\n\t\tit.key()->moveToNextWrite();\n\t\tcallback(it.key(), Notification::Reinit);\n\t} else if (result == ProcessResult::Repaint) {\n\t\tit.key()->moveToNextWrite();\n\t\tcallback(it.key(), Notification::Repaint);\n\t}\n\treturn true;\n}\n\nManager::ResultHandleState Manager::handleResult(ReaderPrivate *reader, ProcessResult result, crl::time ms) {\n\tif (!handleProcessResult(reader, result, ms)) {\n\t\t_loadLevel.fetchAndAddRelaxed(-1 * (reader->_width > 0 ? reader->_width * reader->_height : kAverageGifSize));\n\t\tdelete reader;\n\t\treturn ResultHandleRemove;\n\t}\n\n\t_processingInThread->eventDispatcher()->processEvents(QEventLoop::AllEvents);\n\tif (_processingInThread->isInterruptionRequested()) {\n\t\treturn ResultHandleStop;\n\t}\n\n\tif (result == ProcessResult::Repaint) {\n\t\t{\n\t\t\tQMutexLocker lock(&_readerPointersMutex);\n\t\t\tauto it = constUnsafeFindReaderPointer(reader);\n\t\t\tif (it != _readerPointers.cend()) {\n\t\t\t\tint32 index = 0;\n\t\t\t\tReader::Frame *frame = it.key()->frameToWrite(&index);\n\t\t\t\tif (frame) {\n\t\t\t\t\tframe->clear();\n\t\t\t\t} else {\n\t\t\t\t\tAssert(!reader->_request.valid());\n\t\t\t\t}\n\t\t\t\treader->_frame = index;\n\t\t\t}\n\t\t}\n\t\treturn handleResult(reader, reader->finishProcess(ms), ms);\n\t}\n\n\treturn ResultHandleContinue;\n}\n\nvoid Manager::process() {\n\tif (_processingInThread) {\n\t\t_needReProcess = true;\n\t\treturn;\n\t}\n\n\t_timer.stop();\n\t_processingInThread = thread();\n\n\tbool checkAllReaders = false;\n\tauto ms = crl::now(), minms = ms + 86400 * crl::time(1000);\n\t{\n\t\tQMutexLocker lock(&_readerPointersMutex);\n\t\tfor (auto it = _readerPointers.begin(), e = _readerPointers.end(); it != e; ++it) {\n\t\t\tif (it->loadAcquire() && it.key()->_private != nullptr) {\n\t\t\t\tauto i = _readers.find(it.key()->_private);\n\t\t\t\tif (i == _readers.cend()) {\n\t\t\t\t\t_readers.insert(it.key()->_private, 0);\n\t\t\t\t} else {\n\t\t\t\t\ti.value() = ms;\n\t\t\t\t\tif (i.key()->_autoPausedGif && !it.key()->_autoPausedGif.loadAcquire()) {\n\t\t\t\t\t\ti.key()->_autoPausedGif = false;\n\t\t\t\t\t}\n\t\t\t\t\tif (it.key()->_videoPauseRequest.loadAcquire()) {\n\t\t\t\t\t\ti.key()->pauseVideo(ms);\n\t\t\t\t\t} else {\n\t\t\t\t\t\ti.key()->resumeVideo(ms);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tauto frame = it.key()->frameToWrite();\n\t\t\t\tif (frame) it.key()->_private->_request = frame->request;\n\t\t\t\tit->storeRelease(0);\n\t\t\t}\n\t\t}\n\t\tcheckAllReaders = (_readers.size() > _readerPointers.size());\n\t}\n\n\tfor (auto i = _readers.begin(), e = _readers.end(); i != e;) {\n\t\tReaderPrivate *reader = i.key();\n\t\tif (i.value() <= ms) {\n\t\t\tResultHandleState state = handleResult(reader, reader->process(ms), ms);\n\t\t\tif (state == ResultHandleRemove) {\n\t\t\t\ti = _readers.erase(i);\n\t\t\t\tcontinue;\n\t\t\t} else if (state == ResultHandleStop) {\n\t\t\t\t_processingInThread = nullptr;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tms = crl::now();\n\t\t\tif (reader->_videoPausedAtMs) {\n\t\t\t\ti.value() = ms + 86400 * 1000ULL;\n\t\t\t} else if (reader->_nextFrameWhen && reader->_started) {\n\t\t\t\ti.value() = reader->_nextFrameWhen;\n\t\t\t} else {\n\t\t\t\ti.value() = (ms + 86400 * 1000ULL);\n\t\t\t}\n\t\t} else if (checkAllReaders) {\n\t\t\tQMutexLocker lock(&_readerPointersMutex);\n\t\t\tauto it = constUnsafeFindReaderPointer(reader);\n\t\t\tif (it == _readerPointers.cend()) {\n\t\t\t\t_loadLevel.fetchAndAddRelaxed(-1 * (reader->_width > 0 ? reader->_width * reader->_height : kAverageGifSize));\n\t\t\t\tdelete reader;\n\t\t\t\ti = _readers.erase(i);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (!reader->_autoPausedGif && i.value() < minms) {\n\t\t\tminms = i.value();\n\t\t}\n\t\t++i;\n\t}\n\n\tms = crl::now();\n\tif (_needReProcess || minms <= ms) {\n\t\t_needReProcess = false;\n\t\t_timer.start(1);\n\t} else {\n\t\t_timer.start(minms - ms);\n\t}\n\n\t_processingInThread = nullptr;\n}\n\nvoid Manager::finish() {\n\t_timer.stop();\n\tclear();\n}\n\nvoid Manager::clear() {\n\t{\n\t\tQMutexLocker lock(&_readerPointersMutex);\n\t\tfor (auto it = _readerPointers.begin(), e = _readerPointers.end(); it != e; ++it) {\n\t\t\tit.key()->_private = nullptr;\n\t\t}\n\t\t_readerPointers.clear();\n\t}\n\n\tfor (Readers::iterator i = _readers.begin(), e = _readers.end(); i != e; ++i) {\n\t\tdelete i.key();\n\t}\n\t_readers.clear();\n}\n\nManager::~Manager() {\n\tclear();\n}\n\nUi::PreparedFileInformation PrepareForSending(\n\t\tconst QString &fname,\n\t\tconst QByteArray &data) {\n\tauto result = Ui::PreparedFileInformation::Video();\n\tauto localLocation = Core::FileLocation(fname);\n\tauto localData = QByteArray(data);\n\n\tauto seekPositionMs = crl::time(0);\n\tauto reader = std::make_unique<internal::FFMpegReaderImplementation>(&localLocation, &localData);\n\tif (reader->start(internal::ReaderImplementation::Mode::Inspecting, seekPositionMs)) {\n\t\tconst auto durationMs = reader->durationMs();\n\t\tif (durationMs > 0) {\n\t\t\tresult.isGifv = reader->isGifv();\n\t\t\tresult.isWebmSticker = reader->isWebmSticker();\n\t\t\t// Use first video frame as a thumbnail.\n\t\t\t// All other apps and server do that way.\n\t\t\t//if (!result.isGifv) {\n\t\t\t//\tauto middleMs = durationMs / 2;\n\t\t\t//\tif (!reader->inspectAt(middleMs)) {\n\t\t\t//\t\treturn result;\n\t\t\t//\t}\n\t\t\t//}\n\t\t\tauto index = 0;\n\t\t\tauto hasAlpha = false;\n\t\t\tauto readResult = reader->readFramesTill(-1, crl::now());\n\t\t\tauto readFrame = (readResult == internal::ReaderImplementation::ReadResult::Success);\n\t\t\tif (readFrame && reader->renderFrame(result.thumbnail, hasAlpha, index, QSize())) {\n\t\t\t\tif (hasAlpha && !result.isWebmSticker) {\n\t\t\t\t\tresult.thumbnail = Images::Opaque(std::move(result.thumbnail));\n\t\t\t\t}\n\t\t\t\tresult.duration = durationMs;\n\t\t\t}\n\n\t\t\tresult.supportsStreaming = CheckStreamingSupport(\n\t\t\t\tlocalLocation,\n\t\t\t\tlocalData);\n\t\t}\n\t}\n\treturn { .media = result };\n}\n\nvoid Finish() {\n\tWorkers.clear();\n}\n\nReader *const ReaderPointer::BadPointer = reinterpret_cast<Reader*>(1);\n\nReaderPointer::~ReaderPointer() {\n\tif (valid()) {\n\t\tdelete _pointer;\n\t}\n\t_pointer = nullptr;\n}\n\n} // namespace Clip\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/clip/media_clip_reader.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/image/image_prepare.h\"\n\n#include <QtCore/QTimer>\n#include <QtCore/QMutex>\n\nnamespace Ui {\nstruct PreparedFileInformation;\n} // namespace Ui\n\nnamespace Core {\nclass FileLocation;\n} // namespace Core\n\nnamespace Media {\nnamespace Clip {\n\nenum class State {\n\tReading,\n\tError,\n\tFinished,\n};\n\nstruct FrameRequest {\n\t[[nodiscard]] bool valid() const {\n\t\treturn factor > 0;\n\t}\n\n\tQSize frame;\n\tQSize outer;\n\tint factor = 0;\n\tImageRoundRadius radius = ImageRoundRadius::None;\n\tRectParts corners = RectPart::AllCorners;\n\tQColor colored = QColor(0, 0, 0, 0);\n\tbool keepAlpha = false;\n};\n\n// Before ReaderPrivate read the first image and got the original frame size.\ninline constexpr auto kWaitingForDimensionsStep = -3;\n\n// Before Reader got the original frame size and prepared the frame request.\ninline constexpr auto kWaitingForRequestStep = -2;\n\n// Before ReaderPrivate got the frame request\n// and started waiting for the 1-2 delay.\ninline constexpr auto kWaitingForFirstFrameStep = -1;\n\nenum class Notification {\n\tReinit,\n\tRepaint,\n};\n\nclass Manager;\nclass ReaderPrivate;\nclass Reader {\npublic:\n\tusing Callback = Fn<void(Notification)>;\n\tenum class Mode {\n\t\tGif,\n\t\tVideo,\n\t};\n\n\tReader(const Core::FileLocation &location, const QByteArray &data, Callback &&callback);\n\tReader(const QString &filepath, Callback &&callback);\n\tReader(const QByteArray &data, Callback &&callback);\n\n\t// Reader can be already deleted.\n\tstatic void SafeCallback(\n\t\tReader *reader,\n\t\tint threadIndex,\n\t\tNotification notification);\n\n\tvoid start(FrameRequest request);\n\n\tstruct FrameInfo {\n\t\tQImage image;\n\t\tint index = 0;\n\t};\n\t[[nodiscard]] FrameInfo frameInfo(FrameRequest request, crl::time now);\n\t[[nodiscard]] QImage current(FrameRequest request, crl::time now) {\n\t\tauto result = frameInfo(request, now).image;\n\t\tmoveToNextFrame();\n\t\treturn result;\n\t}\n\t[[nodiscard]] QImage frameOriginal() const {\n\t\tif (const auto frame = frameToShow()) {\n\t\t\tauto result = frame->original;\n\t\t\tresult.detach();\n\t\t\treturn result;\n\t\t}\n\t\treturn QImage();\n\t}\n\tbool moveToNextFrame() {\n\t\treturn moveToNextShow();\n\t}\n\t[[nodiscard]] bool currentDisplayed() const {\n\t\tconst auto frame = frameToShow();\n\t\treturn !frame || (frame->displayed.loadAcquire() != 0);\n\t}\n\t[[nodiscard]] bool autoPausedGif() const {\n\t\treturn _autoPausedGif.loadAcquire();\n\t}\n\t[[nodiscard]] bool videoPaused() const;\n\t[[nodiscard]] int threadIndex() const {\n\t\treturn _threadIndex;\n\t}\n\n\t[[nodiscard]] int width() const;\n\t[[nodiscard]] int height() const;\n\n\t[[nodiscard]] State state() const;\n\t[[nodiscard]] bool started() const {\n\t\tconst auto step = _step.loadAcquire();\n\t\treturn (step == kWaitingForFirstFrameStep) || (step >= 0);\n\t}\n\t[[nodiscard]] bool ready() const;\n\n\t[[nodiscard]] crl::time getPositionMs() const;\n\t[[nodiscard]] crl::time getDurationMs() const;\n\tvoid pauseResumeVideo();\n\n\tvoid stop();\n\tvoid error();\n\tvoid finished();\n\n\t~Reader();\n\nprivate:\n\tvoid init(const Core::FileLocation &location, const QByteArray &data);\n\n\tCallback _callback;\n\tState _state = State::Reading;\n\n\tcrl::time _durationMs = 0;\n\n\tmutable int _width = 0;\n\tmutable int _height = 0;\n\n\t// -2, -1 - init, 0-5 - work, show ((state + 1) / 2) % 3 state, write ((state + 3) / 2) % 3\n\tmutable QAtomicInt _step = kWaitingForDimensionsStep;\n\tstruct Frame {\n\t\tvoid clear() {\n\t\t\tprepared = QImage();\n\t\t\tpreparedColored = QColor(0, 0, 0, 0);\n\t\t\toriginal = QImage();\n\t\t}\n\n\t\tQImage prepared;\n\t\tQColor preparedColored = QColor(0, 0, 0, 0);\n\t\tQImage original;\n\t\tFrameRequest request;\n\t\tQAtomicInt displayed = 0;\n\t\tint index = 0;\n\n\t\t// Should be counted from the end,\n\t\t// so that positionMs <= _durationMs.\n\t\tcrl::time positionMs = 0;\n\t};\n\tmutable Frame _frames[3];\n\tFrame *frameToShow(int *index = nullptr) const; // 0 means not ready\n\tFrame *frameToWrite(int *index = nullptr) const; // 0 means not ready\n\tFrame *frameToWriteNext(bool check, int *index = nullptr) const;\n\tbool moveToNextShow() const;\n\tvoid moveToNextWrite() const;\n\n\tQAtomicInt _autoPausedGif = 0;\n\tQAtomicInt _videoPauseRequest = 0;\n\tint32 _threadIndex;\n\n\tfriend class Manager;\n\n\tReaderPrivate *_private = nullptr;\n\n};\n\nclass ReaderPointer {\npublic:\n\tReaderPointer(std::nullptr_t = nullptr) {\n\t}\n\texplicit ReaderPointer(Reader *pointer) : _pointer(pointer) {\n\t}\n\tReaderPointer(const ReaderPointer &other) = delete;\n\tReaderPointer &operator=(const ReaderPointer &other) = delete;\n\tReaderPointer(ReaderPointer &&other) : _pointer(base::take(other._pointer)) {\n\t}\n\tReaderPointer &operator=(ReaderPointer &&other) {\n\t\tswap(other);\n\t\treturn *this;\n\t}\n\tvoid swap(ReaderPointer &other) {\n\t\tqSwap(_pointer, other._pointer);\n\t}\n\tReader *get() const {\n\t\treturn valid() ? _pointer : nullptr;\n\t}\n\tReader *operator->() const {\n\t\treturn get();\n\t}\n\tvoid setBad() {\n\t\treset();\n\t\t_pointer = BadPointer;\n\t}\n\tvoid reset() {\n\t\tReaderPointer temp;\n\t\tswap(temp);\n\t}\n\tbool isBad() const {\n\t\treturn (_pointer == BadPointer);\n\t}\n\tbool valid() const {\n\t\treturn _pointer && !isBad();\n\t}\n\texplicit operator bool() const {\n\t\treturn valid();\n\t}\n\tstatic inline ReaderPointer Bad() {\n\t\tReaderPointer result;\n\t\tresult.setBad();\n\t\treturn result;\n\t}\n\t~ReaderPointer();\n\nprivate:\n\tReader *_pointer = nullptr;\n\tstatic Reader *const BadPointer;\n\n};\n\ntemplate <typename ...Args>\ninline ReaderPointer MakeReader(Args&&... args) {\n\treturn ReaderPointer(new Reader(std::forward<Args>(args)...));\n}\n\n[[nodiscard]] Ui::PreparedFileInformation PrepareForSending(\n\tconst QString &fname,\n\tconst QByteArray &data);\n\nvoid Finish();\n\n} // namespace Clip\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/media_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/algorithm.h\"\n\n#include <compare>\n\nnamespace Media {\n\nenum class RepeatMode {\n\tNone,\n\tOne,\n\tAll,\n};\n\nenum class OrderMode {\n\tDefault,\n\tReverse,\n\tShuffle,\n};\n\nstruct VideoQuality {\n\tuint32 manual : 1 = 0;\n\tuint32 height : 31 = 0;\n\n\tfriend inline constexpr auto operator<=>(\n\t\tVideoQuality,\n\t\tVideoQuality) = default;\n\tfriend inline constexpr bool operator==(\n\t\tVideoQuality,\n\t\tVideoQuality) = default;\n};\n\ninline constexpr auto kSpeedMin = 0.5;\ninline constexpr auto kSpeedMax = 2.5;\ninline constexpr auto kSpedUpDefault = 1.7;\n\n[[nodiscard]] inline bool EqualSpeeds(float64 a, float64 b) {\n\treturn int(base::SafeRound(a * 10.)) == int(base::SafeRound(b * 10.));\n}\n\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\nusing \"ui/basic.style\";\nusing \"ui/widgets/widgets.style\";\nusing \"overview/overview.style\";\n\nMediaPlayerButton {\n\tplayPosition: point;\n\tplayOuter: size;\n\tpausePosition: point;\n\tpauseOuter: size;\n\tpauseStroke: pixels;\n\tcancelPosition: point;\n\tcancelOuter: size;\n\tcancelStroke: pixels;\n\n\trippleAreaPosition: point;\n\trippleAreaSize: pixels;\n\tripple: RippleAnimation;\n\n\tduration: int;\n}\n\nMediaSpeedMenu {\n\tdropdown: DropdownMenu;\n\tqualityMenu: Menu;\n\tactiveCheck: icon;\n\tactiveCheckSkip: pixels;\n\tsliderStyle: TextStyle;\n\tsliderPadding: margins;\n\tsliderWidth: pixels;\n\tslider: MediaSlider;\n\n\tslow: icon;\n\tslowActive: icon;\n\tnormal: icon;\n\tnormalActive: icon;\n\tmedium: icon;\n\tmediumActive: icon;\n\tfast: icon;\n\tfastActive: icon;\n\tveryFast: icon;\n\tveryFastActive: icon;\n\tsuperFast: icon;\n\tsuperFastActive: icon;\n}\n\nMediaSpeedButton {\n\tsize: size;\n\tpadding: margins;\n\tfont: font;\n\tfg: color;\n\toverFg: color;\n\tactiveFg: color;\n\ticon: icon;\n\tripple: RippleAnimation;\n\trippleActiveColor: color;\n\trippleRadius: pixels;\n\tmenu: MediaSpeedMenu;\n\tmenuAlign: align;\n\tmenuPosition: point;\n}\n\nmediaPlayerButton: MediaPlayerButton {\n\tplayPosition: point(2px, 0px);\n\tplayOuter: size(17px, 15px);\n\tpausePosition: point(1px, 1px);\n\tpauseOuter: size(15px, 15px);\n\tpauseStroke: 5px;\n\tcancelPosition: point(1px, 1px);\n\tcancelOuter: size(15px, 15px);\n\tcancelStroke: 3px;\n\n\trippleAreaPosition: point(0px, 5px);\n\trippleAreaSize: 25px;\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: lightButtonBgOver;\n\t}\n\n\tduration: 200;\n}\nmediaPlayerWideWidth: 460px;\nmediaPlayerHeight: 35px;\nmediaPlayerPadding: 8px;\nmediaPlayerNameTop: 22px;\nmediaPlayerPlayLeft: 9px;\nmediaPlayerPlaySkip: 1px;\nmediaPlayerPlayTop: 0px;\nmediaPlayerCloseRight: 0px;\n\nmediaPlayerName: FlatLabel(defaultFlatLabel) {\n\tmaxHeight: 20px;\n}\nmediaPlayerTime: LabelSimple(defaultLabelSimple) {\n\ttextFg: windowSubTextFg;\n}\n\nmediaPlayerRepeatButton: IconButton {\n\twidth: 30px;\n\theight: 30px;\n\n\ticon: icon {\n\t\t{ \"player/player_repeat\", mediaPlayerActiveFg }\n\t};\n\ticonPosition: point(2px, 5px);\n\n\trippleAreaPosition: point(2px, 6px);\n\trippleAreaSize: 24px;\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: lightButtonBgOver;\n\t}\n}\nmediaPlayerRepeatDisabledIcon: icon {\n\t{ \"player/player_repeat\", menuIconFg }\n};\nmediaPlayerRepeatDisabledIconOver: icon {\n\t{ \"player/player_repeat\", menuIconFgOver }\n};\nmediaPlayerRepeatOneIcon: icon {\n\t{ \"player/player_repeat_single\", mediaPlayerActiveFg }\n};\nmediaPlayerReverseIcon: icon {\n\t{ \"player/player_order\", mediaPlayerActiveFg }\n};\nmediaPlayerReverseDisabledIcon: icon {\n\t{ \"player/player_order\", menuIconFg }\n};\nmediaPlayerReverseDisabledIconOver: icon {\n\t{ \"player/player_order\", menuIconFgOver }\n};\nmediaPlayerShuffleIcon: icon {\n\t{ \"player/player_shuffle\", mediaPlayerActiveFg }\n};\nmediaPlayerOrderButton: IconButton(mediaPlayerRepeatButton) {\n\ticonPosition: point(2px, 6px);\n\trippleAreaPosition: point(2px, 6px);\n}\nmediaPlayerRepeatDisabledRippleBg: windowBgOver;\n\nmediaPlayerPlayButton: IconButton(mediaPlayerRepeatButton) {\n\twidth: 24px;\n\ticon: icon{\n\t\t{ \"player/player_play\", mediaPlayerActiveFg }\n\t};\n\ticonPosition: point(0px, 5px);\n\trippleAreaPosition: point(0px, 5px);\n\trippleAreaSize: 24px;\n}\nmediaPlayerPauseIcon: icon{\n\t{ \"player/player_pause\", mediaPlayerActiveFg }\n};\nmediaPlayerCancelIcon: icon{\n\t{ \"player/panel_close\", mediaPlayerActiveFg }\n};\n\nmediaPlayerMenu: DropdownMenu(defaultDropdownMenu) {\n\twrap: InnerDropdown(defaultInnerDropdown) {\n\t\tscrollPadding: margins(0px, 6px, 0px, 6px);\n\t\tpadding: margins(10px, 10px, 10px, 10px);\n\t}\n}\nmediaPlayerMenuCheck: icon {{ \"player/player_check\", mediaPlayerActiveFg }};\n\nmediaPlayerSpeedMenuInner: Menu(menuWithIcons) {\n\tseparator: MenuSeparator(defaultMenuSeparator) {\n\t\tpadding: margins(0px, 4px, 0px, 4px);\n\t\twidth: 6px;\n\t}\n\titemPadding: margins(54px, 7px, 54px, 9px);\n\titemFgDisabled: mediaPlayerActiveFg;\n}\nmediaPlayerSpeedMenu: MediaSpeedMenu {\n\tdropdown: DropdownMenu(mediaPlayerMenu) {\n\t\tmenu: mediaPlayerSpeedMenuInner;\n\t}\n\tqualityMenu: Menu(mediaPlayerSpeedMenuInner) {\n\t\titemPadding: margins(17px, 7px, 54px, 9px);\n\t}\n\tactiveCheck: mediaPlayerMenuCheck;\n\tactiveCheckSkip: 8px;\n\tsliderStyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px semibold);\n\t}\n\tsliderPadding: margins(50px, 8px, 12px, 8px);\n\tsliderWidth: 122px;\n\tslider: MediaSlider(defaultContinuousSlider) {\n\t\tactiveFg: mediaPlayerActiveFg;\n\t\tinactiveFg: windowBgOver;\n\t\tactiveFgOver: mediaPlayerActiveFg;\n\t\tinactiveFgOver: windowBgOver;\n\t\tactiveFgDisabled: windowBgOver;\n\t\treceivedTillFg: windowBgOver;\n\t\twidth: 6px;\n\t\tseekSize: size(6px, 6px);\n\t}\n\n\tslow: playerSpeedSlow;\n\tslowActive: playerSpeedSlowActive;\n\tnormal: playerSpeedNormal;\n\tnormalActive: playerSpeedNormalActive;\n\tmedium: playerSpeedMedium;\n\tmediumActive: playerSpeedMediumActive;\n\tfast: playerSpeedFast;\n\tfastActive: playerSpeedFastActive;\n\tveryFast: playerSpeedVeryFast;\n\tveryFastActive: playerSpeedVeryFastActive;\n\tsuperFast: playerSpeedSuperFast;\n\tsuperFastActive: playerSpeedSuperFastActive;\n}\n\nmediaPlayerSpeedButton: MediaSpeedButton {\n\tsize: size(30px, 30px);\n\tpadding: margins(0px, 6px, 0px, 0px);\n\tfont: font(11px bold);\n\tfg: menuIconFg;\n\toverFg: menuIconFgOver;\n\tactiveFg: mediaPlayerActiveFg;\n\ticon: icon{{ \"player/player_speed\", menuIconFg }};\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: windowBgOver;\n\t}\n\trippleActiveColor: lightButtonBgOver;\n\trippleRadius: 4px;\n\tmenu: mediaPlayerSpeedMenu;\n\tmenuAlign: align(topright);\n\tmenuPosition: point(-2px, -1px);\n}\n\nmediaPlayerVolumeIcon0: icon {\n\t{ \"player/player_mini_off\", mediaPlayerActiveFg },\n};\nmediaPlayerVolumeIcon1: icon {\n\t{ \"player/player_mini_half\", mediaPlayerActiveFg },\n};\nmediaPlayerVolumeToggle: IconButton(mediaPlayerRepeatButton) {\n\twidth: 34px;\n\ticon: icon {\n\t\t{ \"player/player_mini_full\", mediaPlayerActiveFg },\n\t};\n\ticonPosition: point(5px, 6px);\n\trippleAreaPosition: point(5px, 6px);\n}\nmediaPlayerVolumeMargin: 10px;\nmediaPlayerVolumeSize: size(27px, 100px);\n\nmediaPlayerControlsFade: icon {{ \"fade_horizontal\", mediaPlayerBg }};\n\nmediaPlayerNextButton: IconButton(mediaPlayerPlayButton) {\n\ticon: icon {\n\t\t{ \"player/player_forward\", mediaPlayerActiveFg },\n\t};\n}\nmediaPlayerNextDisabledIcon: icon {\n\t{ \"player/player_forward\", mediaPlayerInactiveFg },\n};\nmediaPlayerPreviousButton: IconButton(mediaPlayerNextButton) {\n\ticon: icon {\n\t\t{ \"player/player_backward\", mediaPlayerActiveFg },\n\t};\n}\nmediaPlayerPreviousDisabledIcon: icon {\n\t{ \"player/player_backward\", mediaPlayerInactiveFg },\n};\n\ntouchBarIconPlayerClose: icon {{ \"player/panel_close\", windowFg }};\ntouchBarIconPlayerPlay: icon {{ \"media_play\", windowFg }};\ntouchBarIconPlayerPause: icon {{ \"media_pause\", windowFg }};\ntouchBarIconPlayerNext: icon {{ \"player/player_forward\", windowFg }};\ntouchBarIconPlayerPrevious: icon {{ \"player/player_backward\", windowFg }};\n\nmediaPlayerClose: IconButton(mediaPlayerRepeatButton) {\n\twidth: 39px;\n\ticon: icon {{ \"player/panel_close\", menuIconFg }};\n\ticonOver: icon {{ \"player/panel_close\", menuIconFgOver }};\n\ticonPosition: point(4px, 6px);\n\n\trippleAreaPosition: point(4px, 6px);\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: windowBgOver;\n\t}\n}\nmediaPlayerPlayback: FilledSlider {\n\tfullWidth: 6px;\n\tlineWidth: 2px;\n\tactiveFg: mediaPlayerActiveFg;\n\tinactiveFg: mediaPlayerInactiveFg;\n\tdisabledFg: mediaPlayerDisabledFg;\n\tduration: 150;\n}\n\nmediaPlayerPanelMarginLeft: 10px;\nmediaPlayerPanelMarginBottom: 10px;\nmediaPlayerPanelWidth: 344px;\n\nmediaPlayerPanelNextButton: IconButton(mediaPlayerRepeatButton) {\n\twidth: 37px;\n\ticon: icon {{ \"player/player_forward\", mediaPlayerActiveFg, point(6px, 4px) }};\n}\n\nmediaPlayerPanelPlaybackPadding: 8px;\nmediaPlayerPanelPlayback: defaultContinuousSlider;\n\nmediaPlayerPanelVolumeWidth: 64px;\n\nmediaPlayerScroll: ScrollArea(defaultSolidScroll) {\n\tdeltat: 10px;\n\tdeltab: 10px;\n}\nmediaPlayerListHeightMax: 280px;\nmediaPlayerListMarginBottom: 10px;\n\nmediaPlayerFileLayout: OverviewFileLayout(overviewFileLayout) {\n\tmaxWidth: 344px;\n\tsongIconBg: mediaPlayerActiveFg;\n\tsongOverBg: mediaPlayerActiveFg;\n}\n\nmediaPlayerFloatSize: 128px;\nmediaPlayerFloatMargin: 12px;\n\nmediaPlayerMenuPosition: point(-2px, -1px);\nmediaPlayerOrderMenu: Menu(defaultMenu) {\n\titemIconPosition: point(13px, 8px);\n\titemPadding: margins(49px, 9px, 17px, 11px);\n\titemStyle: boxTextStyle;\n}\nmediaPlayerOrderMenuActive: Menu(mediaPlayerOrderMenu) {\n\titemFg: windowActiveTextFg;\n\titemFgOver: windowActiveTextFg;\n}\nmediaPlayerOrderIconReverse: icon{{ \"player/player_order\", windowFg }};\nmediaPlayerOrderIconReverseActive: icon{{ \"player/player_order\", windowActiveTextFg }};\nmediaPlayerOrderIconShuffle: icon{{ \"player/player_shuffle\", windowFg }};\nmediaPlayerOrderIconShuffleActive: icon{{ \"player/player_shuffle\", windowActiveTextFg }};\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/player/media_player_button.h\"\n\n#include \"media/media_common.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_media_player.h\"\n#include \"styles/style_media_view.h\"\n\n#include <QtCore/QtMath>\n\nnamespace Media::Player {\nnamespace {\n\n[[nodiscard]] QString SpeedText(float64 speed) {\n\treturn QString::number(base::SafeRound(speed * 10) / 10.) + 'X';\n}\n\n} // namespace\n\nPlayButtonLayout::PlayButtonLayout(\n\tconst style::MediaPlayerButton &st,\n\tFn<void()> callback)\n: _st(st)\n, _callback(std::move(callback)) {\n}\n\nvoid PlayButtonLayout::setState(State state) {\n\tif (_nextState == state) {\n\t\treturn;\n\t}\n\n\t_nextState = state;\n\tif (!_transformProgress.animating()) {\n\t\t_oldState = _state;\n\t\t_state = _nextState;\n\t\t_transformBackward = false;\n\t\tif (_state != _oldState) {\n\t\t\tstartTransform(0., 1.);\n\t\t\tif (_callback) _callback();\n\t\t}\n\t} else if (_oldState == _nextState) {\n\t\tqSwap(_oldState, _state);\n\t\tstartTransform(_transformBackward ? 0. : 1., _transformBackward ? 1. : 0.);\n\t\t_transformBackward = !_transformBackward;\n\t}\n}\n\nvoid PlayButtonLayout::finishTransform() {\n\t_transformProgress.stop();\n\t_transformBackward = false;\n\tif (_callback) _callback();\n}\n\nvoid PlayButtonLayout::paint(QPainter &p, const QBrush &brush) {\n\tif (_transformProgress.animating()) {\n\t\tauto from = _oldState, to = _state;\n\t\tauto backward = _transformBackward;\n\t\tauto progress = _transformProgress.value(1.);\n\t\tif (from == State::Cancel || (from == State::Pause && to == State::Play)) {\n\t\t\tqSwap(from, to);\n\t\t\tbackward = !backward;\n\t\t}\n\t\tif (backward) progress = 1. - progress;\n\n\t\tAssert(from != to);\n\t\tif (from == State::Play) {\n\t\t\tif (to == State::Pause) {\n\t\t\t\tpaintPlayToPause(p, brush, progress);\n\t\t\t} else {\n\t\t\t\tAssert(to == State::Cancel);\n\t\t\t\tpaintPlayToCancel(p, brush, progress);\n\t\t\t}\n\t\t} else {\n\t\t\tAssert(from == State::Pause && to == State::Cancel);\n\t\t\tpaintPauseToCancel(p, brush, progress);\n\t\t}\n\t} else {\n\t\tswitch (_state) {\n\t\tcase State::Play: paintPlay(p, brush); break;\n\t\tcase State::Pause: paintPlayToPause(p, brush, 1.); break;\n\t\tcase State::Cancel: paintPlayToCancel(p, brush, 1.); break;\n\t\t}\n\t}\n}\n\nvoid PlayButtonLayout::paintPlay(QPainter &p, const QBrush &brush) {\n\tauto playLeft = 0. + _st.playPosition.x();\n\tauto playTop = 0. + _st.playPosition.y();\n\tauto playWidth = _st.playOuter.width() - 2 * playLeft;\n\tauto playHeight = _st.playOuter.height() - 2 * playTop;\n\n\tPainterHighQualityEnabler hq(p);\n\n\tp.setPen(Qt::NoPen);\n\n\tQPainterPath pathPlay;\n\tpathPlay.moveTo(playLeft, playTop);\n\tpathPlay.lineTo(playLeft + playWidth, playTop + (playHeight / 2.));\n\tpathPlay.lineTo(playLeft, playTop + playHeight);\n\tpathPlay.lineTo(playLeft, playTop);\n\tp.fillPath(pathPlay, brush);\n}\n\nvoid PlayButtonLayout::paintPlayToPause(QPainter &p, const QBrush &brush, float64 progress) {\n\tauto playLeft = 0. + _st.playPosition.x();\n\tauto playTop = 0. + _st.playPosition.y();\n\tauto playWidth = _st.playOuter.width() - 2 * playLeft;\n\tauto playHeight = _st.playOuter.height() - 2 * playTop;\n\n\tauto pauseLeft = 0. + _st.pausePosition.x();\n\tauto pauseTop = 0. + _st.pausePosition.y();\n\tauto pauseWidth = _st.pauseOuter.width() - 2 * pauseLeft;\n\tauto pauseHeight = _st.pauseOuter.height() - 2 * pauseTop;\n\tauto pauseStroke = 0. + _st.pauseStroke;\n\n\tp.setPen(Qt::NoPen);\n\tPainterHighQualityEnabler hq(p);\n\n\tQPointF pathLeftPause[] = {\n\t\t{ pauseLeft, pauseTop },\n\t\t{ pauseLeft + pauseStroke, pauseTop },\n\t\t{ pauseLeft + pauseStroke, pauseTop + pauseHeight },\n\t\t{ pauseLeft, pauseTop + pauseHeight },\n\t};\n\tQPointF pathLeftPlay[] = {\n\t\t{ playLeft, playTop },\n\t\t{ playLeft + (playWidth / 2.), playTop + (playHeight / 4.) },\n\t\t{ playLeft + (playWidth / 2.), playTop + (3 * playHeight / 4.) },\n\t\t{ playLeft, playTop + playHeight },\n\t};\n\tp.fillPath(anim::interpolate(pathLeftPlay, pathLeftPause, progress), brush);\n\n\tQPointF pathRightPause[] = {\n\t\t{ pauseLeft + pauseWidth - pauseStroke, pauseTop },\n\t\t{ pauseLeft + pauseWidth, pauseTop },\n\t\t{ pauseLeft + pauseWidth, pauseTop + pauseHeight },\n\t\t{ pauseLeft + pauseWidth - pauseStroke, pauseTop + pauseHeight },\n\t};\n\tQPointF pathRightPlay[] = {\n\t\t{ playLeft + (playWidth / 2.), playTop + (playHeight / 4.) },\n\t\t{ playLeft + playWidth, playTop + (playHeight / 2.) },\n\t\t{ playLeft + playWidth, playTop + (playHeight / 2.) },\n\t\t{ playLeft + (playWidth / 2.), playTop + (3 * playHeight / 4.) },\n\t};\n\tp.fillPath(anim::interpolate(pathRightPlay, pathRightPause, progress), brush);\n}\n\nvoid PlayButtonLayout::paintPlayToCancel(QPainter &p, const QBrush &brush, float64 progress) {\n\tauto playLeft = 0. + _st.playPosition.x();\n\tauto playTop = 0. + _st.playPosition.y();\n\tauto playWidth = _st.playOuter.width() - 2 * playLeft;\n\tauto playHeight = _st.playOuter.height() - 2 * playTop;\n\n\tauto cancelLeft = 0. + _st.cancelPosition.x();\n\tauto cancelTop = 0. + _st.cancelPosition.y();\n\tauto cancelWidth = _st.cancelOuter.width() - 2 * cancelLeft;\n\tauto cancelHeight = _st.cancelOuter.height() - 2 * cancelTop;\n\tauto cancelStroke = (0. + _st.cancelStroke) / M_SQRT2;\n\n\tp.setPen(Qt::NoPen);\n\tPainterHighQualityEnabler hq(p);\n\n\tQPointF pathPlay[] = {\n\t\t{ playLeft, playTop },\n\t\t{ playLeft, playTop },\n\t\t{ playLeft + (playWidth / 2.), playTop + (playHeight / 4.) },\n\t\t{ playLeft + playWidth, playTop + (playHeight / 2.) },\n\t\t{ playLeft + playWidth, playTop + (playHeight / 2.) },\n\t\t{ playLeft + playWidth, playTop + (playHeight / 2.) },\n\t\t{ playLeft + playWidth, playTop + (playHeight / 2.) },\n\t\t{ playLeft + playWidth, playTop + (playHeight / 2.) },\n\t\t{ playLeft + (playWidth / 2.), playTop + (3 * playHeight / 4.) },\n\t\t{ playLeft, playTop + playHeight },\n\t\t{ playLeft, playTop + playHeight },\n\t\t{ playLeft, playTop + (playHeight / 2.) },\n\t};\n\tQPointF pathCancel[] = {\n\t\t{ cancelLeft, cancelTop + cancelStroke },\n\t\t{ cancelLeft + cancelStroke, cancelTop },\n\t\t{ cancelLeft + (cancelWidth / 2.), cancelTop + (cancelHeight / 2.) - cancelStroke },\n\t\t{ cancelLeft + cancelWidth - cancelStroke, cancelTop },\n\t\t{ cancelLeft + cancelWidth, cancelTop + cancelStroke },\n\t\t{ cancelLeft + (cancelWidth / 2.) + cancelStroke, cancelTop + (cancelHeight / 2.) },\n\t\t{ cancelLeft + cancelWidth, cancelTop + cancelHeight - cancelStroke },\n\t\t{ cancelLeft + cancelWidth - cancelStroke, cancelTop + cancelHeight },\n\t\t{ cancelLeft + (cancelWidth / 2.), cancelTop + (cancelHeight / 2.) + cancelStroke },\n\t\t{ cancelLeft + cancelStroke, cancelTop + cancelHeight },\n\t\t{ cancelLeft, cancelTop + cancelHeight - cancelStroke },\n\t\t{ cancelLeft + (cancelWidth / 2.) - cancelStroke, cancelTop + (cancelHeight / 2.) },\n\t};\n\tp.fillPath(anim::interpolate(pathPlay, pathCancel, progress), brush);\n}\n\nvoid PlayButtonLayout::paintPauseToCancel(QPainter &p, const QBrush &brush, float64 progress) {\n\tauto pauseLeft = 0. + _st.pausePosition.x();\n\tauto pauseTop = 0. + _st.pausePosition.y();\n\tauto pauseWidth = _st.pauseOuter.width() - 2 * pauseLeft;\n\tauto pauseHeight = _st.pauseOuter.height() - 2 * pauseTop;\n\tauto pauseStroke = 0. + _st.pauseStroke;\n\n\tauto cancelLeft = 0. + _st.cancelPosition.x();\n\tauto cancelTop = 0. + _st.cancelPosition.y();\n\tauto cancelWidth = _st.cancelOuter.width() - 2 * cancelLeft;\n\tauto cancelHeight = _st.cancelOuter.height() - 2 * cancelTop;\n\tauto cancelStroke = (0. + _st.cancelStroke) / M_SQRT2;\n\n\tp.setPen(Qt::NoPen);\n\tPainterHighQualityEnabler hq(p);\n\n\tQPointF pathLeftPause[] = {\n\t\t{ pauseLeft, pauseTop },\n\t\t{ pauseLeft + pauseStroke, pauseTop },\n\t\t{ pauseLeft + pauseStroke, pauseTop + pauseHeight },\n\t\t{ pauseLeft, pauseTop + pauseHeight },\n\t};\n\tQPointF pathLeftCancel[] = {\n\t\t{ cancelLeft, cancelTop + cancelStroke },\n\t\t{ cancelLeft + cancelStroke, cancelTop },\n\t\t{ cancelLeft + cancelWidth, cancelTop + cancelHeight - cancelStroke },\n\t\t{ cancelLeft + cancelWidth - cancelStroke, cancelTop + cancelHeight },\n\t};\n\tp.fillPath(anim::interpolate(pathLeftPause, pathLeftCancel, progress), brush);\n\n\tQPointF pathRightPause[] = {\n\t\t{ pauseLeft + pauseWidth - pauseStroke, pauseTop },\n\t\t{ pauseLeft + pauseWidth, pauseTop },\n\t\t{ pauseLeft + pauseWidth, pauseTop + pauseHeight },\n\t\t{ pauseLeft + pauseWidth - pauseStroke, pauseTop + pauseHeight },\n\t};\n\tQPointF pathRightCancel[] = {\n\t\t{ cancelLeft + cancelWidth - cancelStroke, cancelTop },\n\t\t{ cancelLeft + cancelWidth, cancelTop + cancelStroke },\n\t\t{ cancelLeft + cancelStroke, cancelTop + cancelHeight },\n\t\t{ cancelLeft, cancelTop + cancelHeight - cancelStroke },\n\t};\n\tp.fillPath(anim::interpolate(pathRightPause, pathRightCancel, progress), brush);\n}\n\nvoid PlayButtonLayout::animationCallback() {\n\tif (!_transformProgress.animating()) {\n\t\tauto finalState = _nextState;\n\t\t_nextState = _state;\n\t\tsetState(finalState);\n\t}\n\t_callback();\n}\n\nvoid PlayButtonLayout::startTransform(float64 from, float64 to) {\n\t_transformProgress.start(\n\t\t[=] { animationCallback(); },\n\t\tfrom,\n\t\tto,\n\t\t_st.duration);\n}\n\nSpeedButtonLayout::SpeedButtonLayout(\n\tconst style::MediaSpeedButton &st,\n\tFn<void()> callback,\n\tfloat64 speed)\n: _st(st)\n, _speed(speed)\n, _metrics(_st.font->f)\n, _text(SpeedText(speed))\n, _textWidth(_metrics.horizontalAdvance(_text))\n, _callback(std::move(callback)) {\n\tconst auto result = style::FindAdjustResult(_st.font->f);\n\t_adjustedAscent = result ? result->ascent : _metrics.ascent();\n\t_adjustedHeight = result ? result->height : _metrics.height();\n}\n\nvoid SpeedButtonLayout::setSpeed(float64 speed) {\n\tspeed = base::SafeRound(speed * 10.) / 10.;\n\tif (!EqualSpeeds(_speed, speed)) {\n\t\t_speed = speed;\n\t\t_text = SpeedText(_speed);\n\t\t_textWidth = _metrics.horizontalAdvance(_text);\n\t\tif (_callback) _callback();\n\t}\n}\n\nvoid SpeedButtonLayout::paint(QPainter &p, bool over, bool active) {\n\tconst auto &color = active ? _st.activeFg : over ? _st.overFg : _st.fg;\n\tconst auto inner = QRect(QPoint(), _st.size).marginsRemoved(_st.padding);\n\t_st.icon.paintInCenter(p, inner, color->c);\n\n\tp.setPen(color);\n\tp.setFont(_st.font);\n\n\tp.drawText(\n\t\tQPointF(inner.topLeft()) + QPointF(\n\t\t\t(inner.width() - _textWidth) / 2.,\n\t\t\t(inner.height() - _adjustedHeight) / 2. + _adjustedAscent),\n\t\t_text);\n}\n\nSpeedButton::SpeedButton(QWidget *parent, const style::MediaSpeedButton &st)\n: RippleButton(parent, st.ripple)\n, _st(st)\n, _layout(st, [=] { update(); }, 2.)\n, _isDefault(true) {\n\tresize(_st.size);\n}\n\nvoid SpeedButton::setSpeed(float64 speed) {\n\t_isDefault = EqualSpeeds(speed, 1.);\n\t_layout.setSpeed(speed);\n\tupdate();\n}\n\nvoid SpeedButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tpaintRipple(\n\t\tp,\n\t\tQPoint(_st.padding.left(), _st.padding.top()),\n\t\t_isDefault ? nullptr : &_st.rippleActiveColor->c);\n\t_layout.paint(p, isOver(), !_isDefault);\n}\n\nQPoint SpeedButton::prepareRippleStartPosition() const {\n\tconst auto inner = rect().marginsRemoved(_st.padding);\n\tconst auto result = mapFromGlobal(QCursor::pos()) - inner.topLeft();\n\treturn inner.contains(result)\n\t\t? result\n\t\t: DisabledRippleStartPosition();\n}\n\nQImage SpeedButton::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::RoundRectMask(\n\t\trect().marginsRemoved(_st.padding).size(),\n\t\t_st.rippleRadius);\n}\n\nSettingsButton::SettingsButton(\n\tQWidget *parent,\n\tconst style::MediaSpeedButton &st)\n: RippleButton(parent, st.ripple)\n, _st(st)\n, _isDefaultSpeed(true) {\n\tresize(_st.size);\n}\n\nvoid SettingsButton::setSpeed(float64 speed) {\n\tif (_speed != speed) {\n\t\t_speed = speed;\n\t\t_isDefaultSpeed = EqualSpeeds(speed, 1.);\n\t\tupdate();\n\t}\n}\n\nvoid SettingsButton::setQuality(int quality) {\n\tif (_quality != quality) {\n\t\t_quality = quality;\n\t\tupdate();\n\t}\n}\n\nvoid SettingsButton::setActive(bool active) {\n\tif (_active == active) {\n\t\treturn;\n\t}\n\t_active = active;\n\t_activeAnimation.start([=] {\n\t\tupdate();\n\t}, active ? 0. : 1., active ? 1. : 0., st::mediaviewOverDuration);\n}\n\nvoid SettingsButton::onStateChanged(State was, StateChangeSource source) {\n\tRippleButton::onStateChanged(was, source);\n\n\tconst auto nowOver = isOver();\n\tconst auto wasOver = static_cast<bool>(was & StateFlag::Over);\n\tif (nowOver != wasOver) {\n\t\t_overAnimation.start([=] {\n\t\t\tupdate();\n\t\t}, nowOver ? 0. : 1., nowOver ? 1. : 0., st::mediaviewOverDuration);\n\t}\n}\n\nvoid SettingsButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tpaintRipple(\n\t\tp,\n\t\tQPoint(_st.padding.left(), _st.padding.top()),\n\t\t_isDefaultSpeed ? nullptr : &_st.rippleActiveColor->c);\n\n\tprepareFrame();\n\tp.drawImage(0, 0, _frameCache);\n}\n\nvoid SettingsButton::prepareFrame() {\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (_frameCache.size() != _st.size * ratio) {\n\t\t_frameCache = QImage(\n\t\t\t_st.size * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t_frameCache.setDevicePixelRatio(ratio);\n\t}\n\t_frameCache.fill(Qt::transparent);\n\tauto p = QPainter(&_frameCache);\n\n\tconst auto inner = QRect(\n\t\tQPoint(),\n\t\t_st.size\n\t).marginsRemoved(_st.padding);\n\n\tauto hq = std::optional<PainterHighQualityEnabler>();\n\tconst auto over = _overAnimation.value(isOver() ? 1. : 0.);\n\tconst auto color = anim::color(_st.fg, _st.overFg, over);\n\tconst auto active = _activeAnimation.value(_active ? 1. : 0.);\n\tif (active > 0.) {\n\t\tconst auto shift = QRectF(inner).center();\n\t\tp.save();\n\t\tp.translate(shift);\n\t\tp.rotate(active * 60.);\n\t\tp.translate(-shift);\n\t\thq.emplace(p);\n\t}\n\t_st.icon.paintInCenter(p, inner, color);\n\tif (active > 0.) {\n\t\tp.restore();\n\t\thq.reset();\n\t}\n\n\tconst auto rounded = int(base::SafeRound(_speed * 10));\n\tif (rounded != 10) {\n\t\tconst auto text = (rounded % 10)\n\t\t\t? QString::number(rounded / 10.)\n\t\t\t: u\"%1X\"_q.arg(rounded / 10);\n\t\tpaintBadge(p, text, RectPart::TopLeft, color);\n\t}\n\tconst auto text = (!_quality)\n\t\t? QString()\n\t\t: (_quality > 2000)\n\t\t? u\"4K\"_q\n\t\t: (_quality > 1000)\n\t\t? u\"FHD\"_q\n\t\t: (_quality > 700)\n\t\t? u\"HD\"_q\n\t\t: u\"SD\"_q;\n\tif (!text.isEmpty()) {\n\t\tpaintBadge(p, text, RectPart::BottomRight, color);\n\t}\n}\n\nvoid SettingsButton::paintBadge(\n\t\tQPainter &p,\n\t\tconst QString &text,\n\t\tRectPart origin,\n\t\tQColor color) {\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto xpadding = style::ConvertScale(2.);\n\tconst auto ypadding = 0;\n\tconst auto skip = style::ConvertScale(2.);\n\tconst auto width = _st.font->width(text);\n\tconst auto height = _st.font->height;\n\tconst auto radius = height / 3.;\n\tconst auto left = (origin == RectPart::TopLeft)\n\t\t|| (origin == RectPart::BottomLeft);\n\tconst auto top = (origin == RectPart::TopLeft)\n\t\t|| (origin == RectPart::TopRight);\n\tconst auto x = left ? 0 : (_st.size.width() - width - 2 * xpadding);\n\tconst auto y = top\n\t\t? skip\n\t\t: (_st.size.height() - height - 2 * ypadding - skip);\n\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\tconst auto stroke = style::ConvertScaleExact(1.);\n\tp.setPen(QPen(Qt::transparent, stroke));\n\tp.setFont(_st.font);\n\tp.setBrush(color);\n\tp.drawRoundedRect(\n\t\tQRectF(\n\t\t\tx - stroke / 2.,\n\t\t\ty - stroke / 2.,\n\t\t\twidth + 2 * xpadding + stroke,\n\t\t\theight + 2 * ypadding + stroke),\n\t\tradius,\n\t\tradius);\n\tp.setPen(Qt::transparent);\n\tp.drawText(x + xpadding, y + ypadding + _st.font->ascent, text);\n}\n\nQPoint SettingsButton::prepareRippleStartPosition() const {\n\tconst auto inner = rect().marginsRemoved(_st.padding);\n\tconst auto result = mapFromGlobal(QCursor::pos()) - inner.topLeft();\n\treturn inner.contains(result)\n\t\t? result\n\t\t: DisabledRippleStartPosition();\n}\n\nQImage SettingsButton::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::RoundRectMask(\n\t\trect().marginsRemoved(_st.padding).size(),\n\t\t_st.rippleRadius);\n}\n\n} // namespace Media::Player\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/rect_part.h\"\n\n#include <QtGui/QFontMetrics>\n\nnamespace style {\nstruct MediaPlayerButton;\nstruct MediaSpeedButton;\n} // namespace style\n\nnamespace Media::Player {\n\nclass PlayButtonLayout {\npublic:\n\tenum class State {\n\t\tPlay,\n\t\tPause,\n\t\tCancel,\n\t};\n\tPlayButtonLayout(const style::MediaPlayerButton &st, Fn<void()> callback);\n\n\tvoid setState(State state);\n\tvoid finishTransform();\n\tvoid paint(QPainter &p, const QBrush &brush);\n\nprivate:\n\tvoid animationCallback();\n\tvoid startTransform(float64 from, float64 to);\n\n\tvoid paintPlay(QPainter &p, const QBrush &brush);\n\tvoid paintPlayToPause(QPainter &p, const QBrush &brush, float64 progress);\n\tvoid paintPlayToCancel(QPainter &p, const QBrush &brush, float64 progress);\n\tvoid paintPauseToCancel(QPainter &p, const QBrush &brush, float64 progress);\n\n\tconst style::MediaPlayerButton &_st;\n\n\tState _state = State::Play;\n\tState _oldState = State::Play;\n\tState _nextState = State::Play;\n\tUi::Animations::Simple _transformProgress;\n\tbool _transformBackward = false;\n\n\tFn<void()> _callback;\n\n};\n\nclass SpeedButtonLayout {\npublic:\n\tSpeedButtonLayout(\n\t\tconst style::MediaSpeedButton &st,\n\t\tFn<void()> callback,\n\t\tfloat64 speed);\n\n\tvoid setSpeed(float64 speed);\n\tvoid paint(QPainter &p, bool over, bool active);\n\nprivate:\n\tconst style::MediaSpeedButton &_st;\n\n\tfloat64 _speed = 1.;\n\n\tQFontMetricsF _metrics;\n\tfloat64 _adjustedAscent = 0.;\n\tfloat64 _adjustedHeight = 0.;\n\n\tQString _text;\n\tfloat64 _textWidth = 0;\n\n\tFn<void()> _callback;\n\n};\n\nclass SpeedButton final : public Ui::RippleButton {\npublic:\n\tSpeedButton(QWidget *parent, const style::MediaSpeedButton &st);\n\n\t[[nodiscard]] const style::MediaSpeedButton &st() const {\n\t\treturn _st;\n\t}\n\n\tvoid setSpeed(float64 speed);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tQPoint prepareRippleStartPosition() const override;\n\tQImage prepareRippleMask() const override;\n\n\tconst style::MediaSpeedButton &_st;\n\tSpeedButtonLayout _layout;\n\tbool _isDefault = false;\n\n};\n\nclass SettingsButton final : public Ui::RippleButton {\npublic:\n\tSettingsButton(QWidget *parent, const style::MediaSpeedButton &st);\n\n\t[[nodiscard]] const style::MediaSpeedButton &st() const {\n\t\treturn _st;\n\t}\n\n\tvoid setSpeed(float64 speed);\n\tvoid setQuality(int quality);\n\tvoid setActive(bool active);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tQPoint prepareRippleStartPosition() const override;\n\tQImage prepareRippleMask() const override;\n\n\tvoid onStateChanged(State was, StateChangeSource source) override;\n\n\tvoid paintBadge(\n\t\tQPainter &p,\n\t\tconst QString &text,\n\t\tRectPart origin,\n\t\tQColor color);\n\tvoid prepareFrame();\n\n\tconst style::MediaSpeedButton &_st;\n\tUi::Animations::Simple _activeAnimation;\n\tUi::Animations::Simple _overAnimation;\n\tQImage _frameCache;\n\tfloat _speed = 1.;\n\tint _quality = 0;\n\tbool _isDefaultSpeed = false;\n\tbool _active = false;\n\n};\n\n} // namespace Media::Player\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_dropdown.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/player/media_player_dropdown.h\"\n\n#include \"base/invoke_queued.h\"\n#include \"base/timer.h\"\n#include \"lang/lang_keys.h\"\n#include \"media/player/media_player_button.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/widgets/menu/menu.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_media_player.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Media::Player {\nnamespace {\n\nconstexpr auto kSpeedDebounceTimeout = crl::time(1000);\n\n[[nodiscard]] float64 SpeedToSliderValue(float64 speed) {\n\treturn (speed - kSpeedMin) / (kSpeedMax - kSpeedMin);\n}\n\n[[nodiscard]] float64 SliderValueToSpeed(float64 value) {\n\tconst auto speed = value * (kSpeedMax - kSpeedMin) + kSpeedMin;\n\treturn base::SafeRound(speed * 10) / 10.;\n}\n\nconstexpr auto kSpeedStickedValues\n\t= std::array<std::pair<float64, float64>, 7>{{\n\t\t{ 0.8, 0.05 },\n\t\t{ 1.0, 0.05 },\n\t\t{ 1.2, 0.05 },\n\t\t{ 1.5, 0.05 },\n\t\t{ 1.7, 0.05 },\n\t\t{ 2.0, 0.05 },\n\t\t{ 2.2, 0.05 },\n\t}};\n\nclass SpeedSliderItem final : public Ui::Menu::ItemBase {\npublic:\n\tSpeedSliderItem(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::MediaSpeedMenu &st,\n\t\trpl::producer<float64> value);\n\n\tnot_null<QAction*> action() const override;\n\tbool isEnabled() const override;\n\n\t[[nodiscard]] float64 current() const;\n\t[[nodiscard]] rpl::producer<float64> changing() const;\n\t[[nodiscard]] rpl::producer<float64> changed() const;\n\t[[nodiscard]] rpl::producer<float64> debouncedChanges() const;\n\nprotected:\n\tint contentHeight() const override;\n\nprivate:\n\tvoid setExternalValue(float64 speed);\n\tvoid setSliderValue(float64 speed);\n\n\tconst base::unique_qptr<Ui::MediaSlider> _slider;\n\tconst not_null<QAction*> _dummyAction;\n\tconst style::MediaSpeedMenu &_st;\n\tUi::Text::String _text;\n\tint _height = 0;\n\n\trpl::event_stream<float64> _changing;\n\trpl::event_stream<float64> _changed;\n\trpl::event_stream<float64> _debounced;\n\tbase::Timer _debounceTimer;\n\trpl::variable<float64> _last = 0.;\n\n};\n\nSpeedSliderItem::SpeedSliderItem(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::MediaSpeedMenu &st,\n\trpl::producer<float64> value)\n: Ui::Menu::ItemBase(parent, st.dropdown.menu)\n, _slider(base::make_unique_q<Ui::MediaSlider>(this, st.slider))\n, _dummyAction(new QAction(parent))\n, _st(st)\n, _height(st.sliderPadding.top()\n\t+ st.dropdown.menu.itemStyle.font->height\n\t+ st.sliderPadding.bottom())\n, _debounceTimer([=] { _debounced.fire(current()); }) {\n\tfitToMenuWidth();\n\tenableMouseSelecting();\n\tenableMouseSelecting(_slider.get());\n\n\tsetPointerCursor(false);\n\tsetMinWidth(st.sliderPadding.left()\n\t\t+ st.sliderWidth\n\t\t+ st.sliderPadding.right());\n\t_slider->setAlwaysDisplayMarker(true);\n\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\tconst auto geometry = QRect(QPoint(), size);\n\t\tconst auto padding = _st.sliderPadding;\n\t\tconst auto inner = geometry - padding;\n\t\t_slider->setGeometry(\n\t\t\tpadding.left(),\n\t\t\tinner.y(),\n\t\t\t(geometry.width() - padding.left() - padding.right()),\n\t\t\tinner.height());\n\t}, lifetime());\n\n\tpaintRequest(\n\t) | rpl::on_next([=](const QRect &clip) {\n\t\tauto p = Painter(this);\n\n\t\tp.fillRect(clip, _st.dropdown.menu.itemBg);\n\n\t\tconst auto left = (_st.sliderPadding.left() - _text.maxWidth()) / 2;\n\t\tconst auto top = _st.dropdown.menu.itemPadding.top();\n\t\tp.setPen(_st.dropdown.menu.itemFg);\n\t\t_text.drawLeftElided(p, left, top, _text.maxWidth(), width());\n\t}, lifetime());\n\n\t_slider->setChangeProgressCallback([=](float64 value) {\n\t\tconst auto speed = SliderValueToSpeed(value);\n\t\tif (!EqualSpeeds(current(), speed)) {\n\t\t\t_last = speed;\n\t\t\t_changing.fire_copy(speed);\n\t\t\t_debounceTimer.callOnce(kSpeedDebounceTimeout);\n\t\t}\n\t});\n\n\t_slider->setChangeFinishedCallback([=](float64 value) {\n\t\tconst auto speed = SliderValueToSpeed(value);\n\t\t_last = speed;\n\t\t_changed.fire_copy(speed);\n\t\t_debounced.fire_copy(speed);\n\t\t_debounceTimer.cancel();\n\t});\n\n\tstd::move(\n\t\tvalue\n\t) | rpl::on_next([=](float64 external) {\n\t\tsetExternalValue(external);\n\t}, lifetime());\n\n\t_last.value(\n\t) | rpl::on_next([=](float64 value) {\n\t\tconst auto text = QString::number(value, 'f', 1) + 'x';\n\t\tif (_text.toString() != text) {\n\t\t\t_text.setText(_st.sliderStyle, text);\n\t\t\tupdate();\n\t\t}\n\t}, lifetime());\n\n\t_slider->setAdjustCallback([=](float64 value) {\n\t\tconst auto speed = SliderValueToSpeed(value);\n\t\tfor (const auto &snap : kSpeedStickedValues) {\n\t\t\tif (speed > (snap.first - snap.second)\n\t\t\t\t&& speed < (snap.first + snap.second)) {\n\t\t\t\treturn SpeedToSliderValue(snap.first);\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t});\n}\n\nvoid FillSpeedMenu(\n\t\tnot_null<Ui::Menu::Menu*> menu,\n\t\tconst style::MediaSpeedMenu &st,\n\t\trpl::producer<float64> value,\n\t\tFn<void(float64)> callback,\n\t\tbool onlySlider) {\n\tauto slider = base::make_unique_q<SpeedSliderItem>(\n\t\tmenu,\n\t\tst,\n\t\trpl::duplicate(value));\n\n\tslider->debouncedChanges(\n\t) | rpl::on_next(callback, slider->lifetime());\n\n\tstruct State {\n\t\trpl::variable<float64> realtime;\n\t};\n\tconst auto state = slider->lifetime().make_state<State>();\n\tstate->realtime = rpl::single(\n\t\tslider->current()\n\t) | rpl::then(rpl::merge(\n\t\tslider->changing(),\n\t\tslider->changed()\n\t));\n\n\tmenu->addAction(std::move(slider));\n\n\tif (onlySlider) {\n\t\treturn;\n\t}\n\n\tmenu->addSeparator(&st.dropdown.menu.separator);\n\n\tstruct SpeedPoint {\n\t\tfloat64 speed = 0.;\n\t\ttr::phrase<> text;\n\t\tconst style::icon &icon;\n\t\tconst style::icon &iconActive;\n\t};\n\tconst auto points = std::vector<SpeedPoint>{\n\t\t{\n\t\t\t0.5,\n\t\t\ttr::lng_voice_speed_slow,\n\t\t\tst.slow,\n\t\t\tst.slowActive },\n\t\t{\n\t\t\t1.0,\n\t\t\ttr::lng_voice_speed_normal,\n\t\t\tst.normal,\n\t\t\tst.normalActive },\n\t\t{\n\t\t\t1.2,\n\t\t\ttr::lng_voice_speed_medium,\n\t\t\tst.medium,\n\t\t\tst.mediumActive },\n\t\t{\n\t\t\t1.5,\n\t\t\ttr::lng_voice_speed_fast,\n\t\t\tst.fast,\n\t\t\tst.fastActive },\n\t\t{\n\t\t\t1.7,\n\t\t\ttr::lng_voice_speed_very_fast,\n\t\t\tst.veryFast,\n\t\t\tst.veryFastActive },\n\t\t{\n\t\t\t2.0,\n\t\t\ttr::lng_voice_speed_super_fast,\n\t\t\tst.superFast,\n\t\t\tst.superFastActive },\n\t};\n\tfor (const auto &point : points) {\n\t\tconst auto speed = point.speed;\n\t\tconst auto text = point.text(tr::now);\n\t\tconst auto icon = &point.icon;\n\t\tconst auto iconActive = &point.iconActive;\n\t\tauto action = base::make_unique_q<Ui::Menu::Action>(\n\t\t\tmenu,\n\t\t\tst.dropdown.menu,\n\t\t\tUi::Menu::CreateAction(menu, text, [=] { callback(speed); }),\n\t\t\t&point.icon,\n\t\t\t&point.icon);\n\t\tconst auto raw = action.get();\n\t\tconst auto check = Ui::CreateChild<Ui::RpWidget>(raw);\n\t\tcheck->resize(st.activeCheck.size());\n\t\tcheck->paintRequest(\n\t\t) | rpl::on_next([check, icon = &st.activeCheck] {\n\t\t\tauto p = QPainter(check);\n\t\t\ticon->paint(p, 0, 0, check->width());\n\t\t}, check->lifetime());\n\t\traw->sizeValue(\n\t\t) | rpl::on_next([=, skip = st.activeCheckSkip](QSize size) {\n\t\t\tcheck->moveToRight(\n\t\t\t\tskip,\n\t\t\t\t(size.height() - check->height()) / 2,\n\t\t\t\tsize.width());\n\t\t}, check->lifetime());\n\t\tcheck->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tstate->realtime.value(\n\t\t) | rpl::on_next([=](float64 now) {\n\t\t\tconst auto chosen = EqualSpeeds(speed, now);\n\t\t\tconst auto overriden = chosen ? iconActive : icon;\n\t\t\traw->setIcon(overriden, overriden);\n\t\t\traw->action()->setEnabled(!chosen);\n\t\t\tcheck->setVisible(chosen);\n\t\t}, raw->lifetime());\n\t\tmenu->addAction(std::move(action));\n\t}\n}\n\nvoid SpeedSliderItem::setExternalValue(float64 speed) {\n\tif (!_slider->isChanging()) {\n\t\tsetSliderValue(speed);\n\t}\n}\n\nvoid SpeedSliderItem::setSliderValue(float64 speed) {\n\tconst auto value = SpeedToSliderValue(speed);\n\t_slider->setValue(value);\n\t_last = speed;\n\t_changed.fire_copy(speed);\n}\n\nnot_null<QAction*> SpeedSliderItem::action() const {\n\treturn _dummyAction;\n}\n\nbool SpeedSliderItem::isEnabled() const {\n\treturn false;\n}\n\nint SpeedSliderItem::contentHeight() const {\n\treturn _height;\n}\n\nfloat64 SpeedSliderItem::current() const {\n\treturn _last.current();\n}\n\nrpl::producer<float64> SpeedSliderItem::changing() const {\n\treturn _changing.events();\n}\n\nrpl::producer<float64> SpeedSliderItem::changed() const {\n\treturn _changed.events();\n}\n\nrpl::producer<float64> SpeedSliderItem::debouncedChanges() const {\n\treturn _debounced.events();\n}\n\n} // namespace\n\nDropdown::Dropdown(QWidget *parent)\n: RpWidget(parent)\n, _hideTimer([=] { startHide(); })\n, _showTimer([=] { startShow(); }) {\n\thide();\n\n\tmacWindowDeactivateEvents(\n\t) | rpl::filter([=] {\n\t\treturn !isHidden();\n\t}) | rpl::on_next([=] {\n\t\tleaveEvent(nullptr);\n\t}, lifetime());\n\n\thide();\n\tauto margin = getMargin();\n\tresize(margin.left() + st::mediaPlayerVolumeSize.width() + margin.right(), margin.top() + st::mediaPlayerVolumeSize.height() + margin.bottom());\n}\n\nQMargins Dropdown::getMargin() const {\n\tconst auto top1 = st::mediaPlayerHeight\n\t\t+ st::lineWidth\n\t\t- st::mediaPlayerPlayTop\n\t\t- st::mediaPlayerVolumeToggle.height;\n\tconst auto top2 = st::mediaPlayerPlayback.fullWidth;\n\tconst auto top = std::max(top1, top2);\n\treturn QMargins(st::mediaPlayerVolumeMargin, top, st::mediaPlayerVolumeMargin, st::mediaPlayerVolumeMargin);\n}\n\nbool Dropdown::overlaps(const QRect &globalRect) {\n\tif (isHidden() || _a_appearance.animating()) return false;\n\n\treturn rect().marginsRemoved(getMargin()).contains(QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size()));\n}\n\nvoid Dropdown::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tif (!_cache.isNull()) {\n\t\tbool animating = _a_appearance.animating();\n\t\tif (animating) {\n\t\t\tp.setOpacity(_a_appearance.value(_hiding ? 0. : 1.));\n\t\t} else if (_hiding || isHidden()) {\n\t\t\thidingFinished();\n\t\t\treturn;\n\t\t}\n\t\tp.drawPixmap(0, 0, _cache);\n\t\tif (!animating) {\n\t\t\tshowChildren();\n\t\t\t_cache = QPixmap();\n\t\t}\n\t\treturn;\n\t}\n\n\t// draw shadow\n\tauto shadowedRect = rect().marginsRemoved(getMargin());\n\tauto shadowedSides = RectPart::Left | RectPart::Right | RectPart::Bottom;\n\tUi::Shadow::paint(p, shadowedRect, width(), st::roundShadowRadius8px, shadowedSides);\n\tconst auto &corners = Ui::CachedCornerPixmaps(Ui::MenuCorners);\n\tconst auto fill = Ui::CornersPixmaps{\n\t\t.p = { QPixmap(), QPixmap(), corners.p[2], corners.p[3] },\n\t};\n\tUi::FillRoundRect(\n\t\tp,\n\t\tshadowedRect.x(),\n\t\t0,\n\t\tshadowedRect.width(),\n\t\tshadowedRect.y() + shadowedRect.height(),\n\t\tst::menuBg,\n\t\tfill);\n}\n\nvoid Dropdown::enterEventHook(QEnterEvent *e) {\n\t_hideTimer.cancel();\n\tif (_a_appearance.animating()) {\n\t\tstartShow();\n\t} else {\n\t\t_showTimer.callOnce(0);\n\t}\n\treturn RpWidget::enterEventHook(e);\n}\n\nvoid Dropdown::leaveEventHook(QEvent *e) {\n\t_showTimer.cancel();\n\tif (_a_appearance.animating()) {\n\t\tstartHide();\n\t} else {\n\t\t_hideTimer.callOnce(300);\n\t}\n\treturn RpWidget::leaveEventHook(e);\n}\n\nvoid Dropdown::otherEnter() {\n\t_hideTimer.cancel();\n\tif (_a_appearance.animating()) {\n\t\tstartShow();\n\t} else {\n\t\t_showTimer.callOnce(0);\n\t}\n}\n\nvoid Dropdown::otherLeave() {\n\t_showTimer.cancel();\n\tif (_a_appearance.animating()) {\n\t\tstartHide();\n\t} else {\n\t\t_hideTimer.callOnce(0);\n\t}\n}\n\nvoid Dropdown::startShow() {\n\tif (isHidden()) {\n\t\tshow();\n\t} else if (!_hiding) {\n\t\treturn;\n\t}\n\t_hiding = false;\n\tstartAnimation();\n}\n\nvoid Dropdown::startHide() {\n\tif (_hiding) {\n\t\treturn;\n\t}\n\n\t_hiding = true;\n\tstartAnimation();\n}\n\nvoid Dropdown::startAnimation() {\n\tif (_cache.isNull()) {\n\t\tshowChildren();\n\t\t_cache = Ui::GrabWidget(this);\n\t}\n\thideChildren();\n\t_a_appearance.start(\n\t\t[=] { appearanceCallback(); },\n\t\t_hiding ? 1. : 0.,\n\t\t_hiding ? 0. : 1.,\n\t\tst::defaultInnerDropdown.duration);\n}\n\nvoid Dropdown::appearanceCallback() {\n\tif (!_a_appearance.animating() && _hiding) {\n\t\t_hiding = false;\n\t\thidingFinished();\n\t} else {\n\t\tupdate();\n\t}\n}\n\nvoid Dropdown::hidingFinished() {\n\thide();\n\t_cache = QPixmap();\n}\n\nbool Dropdown::eventFilter(QObject *obj, QEvent *e) {\n\tif (e->type() == QEvent::Enter) {\n\t\totherEnter();\n\t} else if (e->type() == QEvent::Leave) {\n\t\totherLeave();\n\t}\n\treturn false;\n}\n\nWithDropdownController::WithDropdownController(\n\tnot_null<Ui::AbstractButton*> button,\n\tnot_null<QWidget*> menuParent,\n\tconst style::DropdownMenu &menuSt,\n\tQt::Alignment menuAlign,\n\tQPoint menuPosition,\n\tFn<void(bool)> menuOverCallback)\n: _button(button)\n, _menuParent(menuParent)\n, _menuSt(menuSt)\n, _menuAlign(menuAlign)\n, _menuPosition(menuPosition)\n, _menuOverCallback(std::move(menuOverCallback)) {\n\tbutton->events(\n\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\treturn (e->type() == QEvent::Enter)\n\t\t\t|| (e->type() == QEvent::Leave);\n\t}) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\t_overButton = (e->type() == QEvent::Enter);\n\t\tif (_overButton) {\n\t\t\tInvokeQueued(button, [=] {\n\t\t\t\tif (_overButton) {\n\t\t\t\t\tshowMenu();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}, button->lifetime());\n}\n\nnot_null<Ui::AbstractButton*> WithDropdownController::button() const {\n\treturn _button;\n}\n\nUi::DropdownMenu *WithDropdownController::menu() const {\n\treturn _menu.get();\n}\n\nvoid WithDropdownController::updateDropdownGeometry() {\n\tif (!_menu) {\n\t\treturn;\n\t}\n\tconst auto bwidth = _button->width();\n\tconst auto bheight = _button->height();\n\tconst auto mwidth = _menu->width();\n\tconst auto mheight = _menu->height();\n\tconst auto padding = _menuSt.wrap.padding;\n\tconst auto x = _menuPosition.x();\n\tconst auto y = _menuPosition.y();\n\tconst auto position = _menu->parentWidget()->mapFromGlobal(\n\t\t_button->mapToGlobal(QPoint())\n\t) + [&] {\n\t\tswitch (_menuAlign) {\n\t\tcase style::al_topleft: return QPoint(\n\t\t\t-padding.left() - x,\n\t\t\tbheight - padding.top() + y);\n\t\tcase style::al_topright: return QPoint(\n\t\t\tbwidth - mwidth + padding.right() + x,\n\t\t\tbheight - padding.top() + y);\n\t\tcase style::al_bottomright: return QPoint(\n\t\t\tbwidth - mwidth + padding.right() + x,\n\t\t\t-mheight + padding.bottom() - y);\n\t\tcase style::al_bottomleft: return QPoint(\n\t\t\t-padding.left() - x,\n\t\t\t-mheight + padding.bottom() - y);\n\t\t}\n\t\tUnexpected(\"Menu align value.\");\n\t}();\n\t_menu->move(position);\n}\n\nrpl::producer<bool> WithDropdownController::menuToggledValue() const {\n\treturn _menuToggled.value();\n}\n\nvoid WithDropdownController::hideTemporarily() {\n\tif (_menu && !_menu->isHidden()) {\n\t\t_temporarilyHidden = true;\n\t\t_menu->hide();\n\t}\n}\n\nvoid WithDropdownController::showBack() {\n\tif (_temporarilyHidden) {\n\t\t_temporarilyHidden = false;\n\t\tif (_menu && _menu->isHidden()) {\n\t\t\t_menu->show();\n\t\t}\n\t}\n}\n\nvoid WithDropdownController::showMenu() {\n\tif (_menu) {\n\t\treturn;\n\t}\n\t_menu.emplace(_menuParent, _menuSt);\n\tconst auto raw = _menu.get();\n\t_menu->events(\n\t) | rpl::on_next([this](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::Enter) {\n\t\t\t_menuOverCallback(true);\n\t\t} else if (type == QEvent::Leave) {\n\t\t\t_menuOverCallback(false);\n\t\t}\n\t}, _menu->lifetime());\n\t_menu->setHiddenCallback([=]{\n\t\tif (_menu.get() == raw) {\n\t\t\t_menuToggled = false;\n\t\t}\n\t\tUi::PostponeCall(raw, [this] {\n\t\t\t_menu = nullptr;\n\t\t\t_menuToggled = false;\n\t\t});\n\t});\n\t_menu->setShowStartCallback([=] {\n\t\t_menuToggled = true;\n\t});\n\t_menu->setHideStartCallback([=] {\n\t\t_menuToggled = false;\n\t});\n\t_button->installEventFilter(raw);\n\tfillMenu(raw);\n\tupdateDropdownGeometry();\n\tconst auto origin = [&] {\n\t\tusing Origin = Ui::PanelAnimation::Origin;\n\t\tswitch (_menuAlign) {\n\t\tcase style::al_topleft: return Origin::TopLeft;\n\t\tcase style::al_topright: return Origin::TopRight;\n\t\tcase style::al_bottomright: return Origin::BottomRight;\n\t\tcase style::al_bottomleft: return Origin::BottomLeft;\n\t\t}\n\t\tUnexpected(\"Menu align value.\");\n\t}();\n\t_menu->showAnimated(origin);\n\t_menuToggled = true;\n}\n\nOrderController::OrderController(\n\tnot_null<Ui::IconButton*> button,\n\tnot_null<QWidget*> menuParent,\n\tFn<void(bool)> menuOverCallback,\n\trpl::producer<OrderMode> value,\n\tFn<void(OrderMode)> change)\n: WithDropdownController(\n\tbutton,\n\tmenuParent,\n\tst::mediaPlayerMenu,\n\tstyle::al_topright,\n\tst::mediaPlayerMenuPosition,\n\tstd::move(menuOverCallback))\n, _button(button)\n, _appOrder(std::move(value))\n, _change(std::move(change)) {\n\tbutton->setClickedCallback([=] {\n\t\tshowMenu();\n\t});\n\n\t_appOrder.value(\n\t) | rpl::on_next([=] {\n\t\tupdateIcon();\n\t}, button->lifetime());\n}\n\nvoid OrderController::fillMenu(not_null<Ui::DropdownMenu*> menu) {\n\tconst auto addOrderAction = [&](OrderMode mode) {\n\t\tstruct Fields {\n\t\t\tQString label;\n\t\t\tconst style::icon &icon;\n\t\t\tconst style::icon &activeIcon;\n\t\t};\n\t\tconst auto active = (_appOrder.current() == mode);\n\t\tconst auto callback = [change = _change, mode, active] {\n\t\t\tchange(active ? OrderMode::Default : mode);\n\t\t};\n\t\tconst auto fields = [&]() -> Fields {\n\t\t\tswitch (mode) {\n\t\t\tcase OrderMode::Reverse: return {\n\t\t\t\t.label = tr::lng_audio_player_reverse(tr::now),\n\t\t\t\t.icon = st::mediaPlayerOrderIconReverse,\n\t\t\t\t.activeIcon = st::mediaPlayerOrderIconReverseActive,\n\t\t\t};\n\t\t\tcase OrderMode::Shuffle: return {\n\t\t\t\t.label = tr::lng_audio_player_shuffle(tr::now),\n\t\t\t\t.icon = st::mediaPlayerOrderIconShuffle,\n\t\t\t\t.activeIcon = st::mediaPlayerOrderIconShuffleActive,\n\t\t\t};\n\t\t\t}\n\t\t\tUnexpected(\"Order mode in addOrderAction.\");\n\t\t}();\n\t\tmenu->addAction(base::make_unique_q<Ui::Menu::Action>(\n\t\t\tmenu->menu(),\n\t\t\t(active\n\t\t\t\t? st::mediaPlayerOrderMenuActive\n\t\t\t\t: st::mediaPlayerOrderMenu),\n\t\t\tUi::Menu::CreateAction(menu, fields.label, callback),\n\t\t\t&(active ? fields.activeIcon : fields.icon),\n\t\t\t&(active ? fields.activeIcon : fields.icon)));\n\t};\n\taddOrderAction(OrderMode::Reverse);\n\taddOrderAction(OrderMode::Shuffle);\n}\n\nvoid OrderController::updateIcon() {\n\tswitch (_appOrder.current()) {\n\tcase OrderMode::Default:\n\t\t_button->setIconOverride(\n\t\t\t&st::mediaPlayerReverseDisabledIcon,\n\t\t\t&st::mediaPlayerReverseDisabledIconOver);\n\t\t_button->setRippleColorOverride(\n\t\t\t&st::mediaPlayerRepeatDisabledRippleBg);\n\t\tbreak;\n\tcase OrderMode::Reverse:\n\t\t_button->setIconOverride(&st::mediaPlayerReverseIcon);\n\t\t_button->setRippleColorOverride(nullptr);\n\t\tbreak;\n\tcase OrderMode::Shuffle:\n\t\t_button->setIconOverride(&st::mediaPlayerShuffleIcon);\n\t\t_button->setRippleColorOverride(nullptr);\n\t\tbreak;\n\t}\n}\n\nSpeedController::SpeedController(\n\tnot_null<Ui::AbstractButton*> button,\n\tconst style::MediaSpeedButton &st,\n\tnot_null<QWidget*> menuParent,\n\tFn<void(bool)> menuOverCallback,\n\tFn<float64(bool lastNonDefault)> value,\n\tFn<void(float64)> change,\n\tstd::vector<int> qualities,\n\tFn<VideoQuality()> quality,\n\tFn<void(int)> changeQuality)\n: WithDropdownController(\n\tbutton,\n\tmenuParent,\n\tst.menu.dropdown,\n\tst.menuAlign,\n\tst.menuPosition,\n\tstd::move(menuOverCallback))\n, _st(st)\n, _lookup(std::move(value))\n, _change(std::move(change))\n, _qualities(std::move(qualities))\n, _lookupQuality(std::move(quality))\n, _changeQuality(std::move(changeQuality)) {\n\tExpects(_qualities.empty() || (_lookupQuality && _changeQuality));\n\n\tbutton->setClickedCallback([=] {\n\t\tif (_lookup && !_lookupQuality && !_changeQuality) {\n\t\t\ttoggleDefault();\n\t\t\tsave();\n\t\t\tif (const auto current = menu()) {\n\t\t\t\tcurrent->otherEnter();\n\t\t\t}\n\t\t} else {\n\t\t\tshowMenu();\n\t\t}\n\t});\n\tif (const auto lookup = _lookup) {\n\t\tsetSpeed(lookup(false));\n\t\t_speed = lookup(true);\n\t}\n}\n\nrpl::producer<> SpeedController::saved() const {\n\treturn _saved.events();\n}\n\nrpl::producer<float64> SpeedController::realtimeValue() const {\n\treturn _speedChanged.events_starting_with(speed());\n}\n\nvoid SpeedController::reloadFromLookup() {\n\tif (const auto lookup = _lookup) {\n\t\tsetSpeed(lookup(false));\n\t\t_speed = lookup(true);\n\t}\n}\n\nfloat64 SpeedController::speed() const {\n\treturn _isDefault ? 1. : _speed;\n}\n\nbool SpeedController::isDefault() const {\n\treturn _isDefault;\n}\n\nfloat64 SpeedController::lastNonDefaultSpeed() const {\n\treturn _speed;\n}\n\nvoid SpeedController::toggleDefault() {\n\t_isDefault = !_isDefault;\n\t_speedChanged.fire(speed());\n}\n\nvoid SpeedController::setSpeed(float64 newSpeed) {\n\tif (!(_isDefault = EqualSpeeds(newSpeed, 1.))) {\n\t\t_speed = newSpeed;\n\t}\n\t_speedChanged.fire(speed());\n}\n\nvoid SpeedController::save() {\n\tif (const auto change = _change) {\n\t\tchange(speed());\n\t}\n\t_saved.fire({});\n}\n\nvoid SpeedController::setQuality(VideoQuality quality) {\n\t_quality = quality;\n\t_changeQuality(quality.manual ? quality.height : 0);\n}\n\nvoid SpeedController::fillMenu(not_null<Ui::DropdownMenu*> menu) {\n\tif (_lookup) {\n\t\tFillSpeedMenu(\n\t\t\tmenu->menu(),\n\t\t\t_st.menu,\n\t\t\t_speedChanged.events_starting_with(speed()),\n\t\t\t[=](float64 speed) { setSpeed(speed); save(); },\n\t\t\t!_qualities.empty());\n\t}\n\tif (_qualities.empty()) {\n\t\treturn;\n\t}\n\t_quality = _lookupQuality();\n\tconst auto raw = menu->menu();\n\tconst auto &st = _st.menu;\n\tif (_lookup) {\n\t\traw->addSeparator(&st.dropdown.menu.separator);\n\t}\n\n\tconst auto add = [&](int quality) {\n\t\tconst auto automatic = tr::lng_mediaview_quality_auto(tr::now);\n\t\tconst auto text = quality ? u\"%1p\"_q.arg(quality) : automatic;\n\t\tauto action = base::make_unique_q<Ui::Menu::Action>(\n\t\t\traw,\n\t\t\tst.qualityMenu,\n\t\t\tUi::Menu::CreateAction(\n\t\t\t\traw,\n\t\t\t\ttext,\n\t\t\t\t[=] { _changeQuality(quality); }),\n\t\t\tnullptr,\n\t\t\tnullptr);\n\t\tconst auto raw = action.get();\n\t\tconst auto check = Ui::CreateChild<Ui::RpWidget>(raw);\n\t\tcheck->resize(st.activeCheck.size());\n\t\tcheck->paintRequest(\n\t\t) | rpl::on_next([check, icon = &st.activeCheck] {\n\t\t\tauto p = QPainter(check);\n\t\t\ticon->paint(p, 0, 0, check->width());\n\t\t}, check->lifetime());\n\t\traw->sizeValue(\n\t\t) | rpl::on_next([=, skip = st.activeCheckSkip](QSize size) {\n\t\t\tcheck->moveToRight(\n\t\t\t\tskip,\n\t\t\t\t(size.height() - check->height()) / 2,\n\t\t\t\tsize.width());\n\t\t}, check->lifetime());\n\t\tcheck->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t_quality.value(\n\t\t) | rpl::on_next([=](VideoQuality now) {\n\t\t\tconst auto chosen = now.manual\n\t\t\t\t? (now.height == quality)\n\t\t\t\t: !quality;\n\t\t\traw->action()->setEnabled(!chosen);\n\t\t\tif (!quality) {\n\t\t\t\traw->action()->setText(automatic\n\t\t\t\t\t+ (now.manual ? QString() : u\"\\t%1p\"_q.arg(now.height)));\n\t\t\t}\n\t\t\tcheck->setVisible(chosen);\n\t\t}, raw->lifetime());\n\t\tmenu->addAction(std::move(action));\n\t};\n\n\tadd(0);\n\tfor (const auto quality : _qualities) {\n\t\tadd(quality);\n\t}\n}\n\n} // namespace Media::Player\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_dropdown.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"media/media_common.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace style {\nstruct MediaSpeedMenu;\nstruct MediaSpeedButton;\nstruct DropdownMenu;\n} // namespace style\n\nnamespace Ui {\nclass DropdownMenu;\nclass AbstractButton;\nclass IconButton;\n} // namespace Ui\n\nnamespace Ui::Menu {\nclass Menu;\n} // namespace Ui::Menu\n\nnamespace Media::Player {\n\nclass Dropdown final : public Ui::RpWidget {\npublic:\n\texplicit Dropdown(QWidget *parent);\n\n\tbool overlaps(const QRect &globalRect);\n\n\tQMargins getMargin() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\n\tbool eventFilter(QObject *obj, QEvent *e) override;\n\nprivate:\n\tvoid startHide();\n\tvoid startShow();\n\n\tvoid otherEnter();\n\tvoid otherLeave();\n\n\tvoid appearanceCallback();\n\tvoid hidingFinished();\n\tvoid startAnimation();\n\n\tbool _hiding = false;\n\n\tQPixmap _cache;\n\tUi::Animations::Simple _a_appearance;\n\n\tbase::Timer _hideTimer;\n\tbase::Timer _showTimer;\n\n};\n\nclass WithDropdownController {\npublic:\n\tWithDropdownController(\n\t\tnot_null<Ui::AbstractButton*> button,\n\t\tnot_null<QWidget*> menuParent,\n\t\tconst style::DropdownMenu &menuSt,\n\t\tQt::Alignment menuAlign,\n\t\tQPoint menuPosition,\n\t\tFn<void(bool)> menuOverCallback);\n\tvirtual ~WithDropdownController() = default;\n\n\t[[nodiscard]] not_null<Ui::AbstractButton*> button() const;\n\tUi::DropdownMenu *menu() const;\n\n\tvoid updateDropdownGeometry();\n\t[[nodiscard]] rpl::producer<bool> menuToggledValue() const;\n\n\tvoid hideTemporarily();\n\tvoid showBack();\n\nprotected:\n\tvoid showMenu();\n\nprivate:\n\tvirtual void fillMenu(not_null<Ui::DropdownMenu*> menu) = 0;\n\n\tconst not_null<Ui::AbstractButton*> _button;\n\tconst not_null<QWidget*> _menuParent;\n\tconst style::DropdownMenu &_menuSt;\n\tconst Qt::Alignment _menuAlign = Qt::AlignTop | Qt::AlignRight;\n\tconst QPoint _menuPosition;\n\tconst Fn<void(bool)> _menuOverCallback;\n\tbase::unique_qptr<Ui::DropdownMenu> _menu;\n\trpl::variable<bool> _menuToggled;\n\tbool _temporarilyHidden = false;\n\tbool _overButton = false;\n\n};\n\nclass OrderController final : public WithDropdownController {\npublic:\n\tOrderController(\n\t\tnot_null<Ui::IconButton*> button,\n\t\tnot_null<QWidget*> menuParent,\n\t\tFn<void(bool)> menuOverCallback,\n\t\trpl::producer<OrderMode> value,\n\t\tFn<void(OrderMode)> change);\n\nprivate:\n\tvoid fillMenu(not_null<Ui::DropdownMenu*> menu) override;\n\tvoid updateIcon();\n\n\tconst not_null<Ui::IconButton*> _button;\n\trpl::variable<OrderMode> _appOrder;\n\tFn<void(OrderMode)> _change;\n\n};\n\nclass SpeedController final : public WithDropdownController {\npublic:\n\tSpeedController(\n\t\tnot_null<Ui::AbstractButton*> button,\n\t\tconst style::MediaSpeedButton &st,\n\t\tnot_null<QWidget*> menuParent,\n\t\tFn<void(bool)> menuOverCallback,\n\t\tFn<float64(bool lastNonDefault)> value,\n\t\tFn<void(float64)> change,\n\t\tstd::vector<int> qualities = {},\n\t\tFn<VideoQuality()> quality = nullptr,\n\t\tFn<void(int)> changeQuality = nullptr);\n\n\t[[nodiscard]] rpl::producer<> saved() const;\n\t[[nodiscard]] rpl::producer<float64> realtimeValue() const;\n\tvoid reloadFromLookup();\n\nprivate:\n\tvoid fillMenu(not_null<Ui::DropdownMenu*> menu) override;\n\n\t[[nodiscard]] float64 speed() const;\n\t[[nodiscard]] bool isDefault() const;\n\t[[nodiscard]] float64 lastNonDefaultSpeed() const;\n\tvoid toggleDefault();\n\tvoid setSpeed(float64 newSpeed);\n\tvoid setQuality(VideoQuality quality);\n\tvoid save();\n\n\tconst style::MediaSpeedButton &_st;\n\tFn<float64(bool lastNonDefault)> _lookup;\n\tFn<void(float64)> _change;\n\tfloat64 _speed = kSpedUpDefault;\n\tbool _isDefault = true;\n\trpl::event_stream<float64> _speedChanged;\n\trpl::event_stream<> _saved;\n\n\tstd::vector<int> _qualities;\n\tFn<VideoQuality()> _lookupQuality;\n\tFn<void(int)> _changeQuality;\n\trpl::variable<VideoQuality> _quality;\n\n};\n\n} // namespace Media::Player\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_float.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/player/media_player_float.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"history/view/media/history_view_media.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_element.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/streaming/media_streaming_instance.h\"\n#include \"media/view/media_view_playback_progress.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/section_widget.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"main/main_session.h\"\n#include \"main/main_account.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_media_player.h\"\n#include \"styles/style_chat.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace Media {\nnamespace Player {\n\nusing DoubleClickedCallback = Fn<void(not_null<const HistoryItem*>)>;\n\nRoundPainter::RoundPainter(not_null<HistoryItem*> item)\n: _item(item) {\n}\n\nbool RoundPainter::fillFrame(const QSize &size) {\n\tauto creating = _frame.isNull();\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (creating) {\n\t\t_frame = QImage(\n\t\t\tsize * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t_frame.setDevicePixelRatio(ratio);\n\t}\n\tauto frameInner = [&] {\n\t\treturn QRect(QPoint(), _frame.size() / ratio);\n\t};\n\tif (const auto streamed = instance()->roundVideoStreamed(_item)) {\n\t\tauto request = Streaming::FrameRequest::NonStrict();\n\t\trequest.outer = request.resize = _frame.size();\n\t\tif (_roundingMask.size() != request.outer) {\n\t\t\t_roundingMask = Images::EllipseMask(frameInner().size());\n\t\t}\n\t\trequest.mask = _roundingMask;\n\t\tauto frame = streamed->frame(request);\n\t\tif (!frame.isNull()) {\n\t\t\t_frame.fill(Qt::transparent);\n\n\t\t\tauto p = QPainter(&_frame);\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawImage(frameInner(), frame);\n\t\t\treturn true;\n\t\t}\n\t}\n\tif (creating) {\n\t\t_frame.fill(Qt::transparent);\n\n\t\tauto p = QPainter(&_frame);\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::imageBg);\n\t\tp.drawEllipse(frameInner());\n\t}\n\treturn false;\n}\n\nconst QImage &RoundPainter::frame() const {\n\treturn _frame;\n}\n\nFloat::Float(\n\tQWidget *parent,\n\tnot_null<HistoryItem*> item,\n\tFn<void(bool visible)> toggleCallback,\n\tFn<void(bool closed)> draggedCallback,\n\tDoubleClickedCallback doubleClickedCallback)\n: RpWidget(parent)\n, _item(item)\n, _toggleCallback(std::move(toggleCallback))\n, _draggedCallback(std::move(draggedCallback))\n, _doubleClickedCallback(std::move(doubleClickedCallback)) {\n\tauto media = _item->media();\n\tAssert(media != nullptr);\n\n\tauto document = media->document();\n\tAssert(document != nullptr);\n\tAssert(document->isVideoMessage());\n\n\tauto margin = st::mediaPlayerFloatMargin;\n\tauto size = 2 * margin + st::mediaPlayerFloatSize;\n\tresize(size, size);\n\n\t_roundPainter = std::make_unique<RoundPainter>(item);\n\tprepareShadow();\n\n\tdocument->session().data().itemRepaintRequest(\n\t) | rpl::on_next([this](auto item) {\n\t\tif (_item == item) {\n\t\t\trepaintItem();\n\t\t}\n\t}, lifetime());\n\n\tdocument->session().data().itemRemoved(\n\t) | rpl::on_next([this](auto item) {\n\t\tif (_item == item) {\n\t\t\tdetach();\n\t\t}\n\t}, lifetime());\n\n\tdocument->session().account().sessionChanges(\n\t) | rpl::on_next([=] {\n\t\tdetach();\n\t}, lifetime());\n\n\tsetCursor(style::cur_pointer);\n}\n\nvoid Float::mousePressEvent(QMouseEvent *e) {\n\t_down = true;\n\t_downPoint = e->pos();\n}\n\nvoid Float::mouseMoveEvent(QMouseEvent *e) {\n\tif (_down && (e->pos() - _downPoint).manhattanLength() > QApplication::startDragDistance()) {\n\t\t_down = false;\n\t\t_drag = true;\n\t\t_dragLocalPoint = e->pos();\n\t} else if (_drag) {\n\t\tauto delta = (e->pos() - _dragLocalPoint);\n\t\tmove(pos() + delta);\n\t\tsetOpacity(outRatio());\n\t}\n}\n\nfloat64 Float::outRatio() const {\n\tauto parent = parentWidget()->rect();\n\tauto min = 1.;\n\tif (x() < parent.x()) {\n\t\taccumulate_min(min, 1. - (parent.x() - x()) / float64(width()));\n\t}\n\tif (y() < parent.y()) {\n\t\taccumulate_min(min, 1. - (parent.y() - y()) / float64(height()));\n\t}\n\tif (x() + width() > parent.x() + parent.width()) {\n\t\taccumulate_min(min, 1. - (x() + width() - parent.x() - parent.width()) / float64(width()));\n\t}\n\tif (y() + height() > parent.y() + parent.height()) {\n\t\taccumulate_min(min, 1. - (y() + height() - parent.y() - parent.height()) / float64(height()));\n\t}\n\treturn std::clamp(min, 0., 1.);\n}\n\nvoid Float::mouseReleaseEvent(QMouseEvent *e) {\n\tif (base::take(_down) && _item) {\n\t\tpauseResume();\n\t}\n\tif (_drag) {\n\t\tfinishDrag(outRatio() < 0.5);\n\t}\n}\n\nvoid Float::finishDrag(bool closed) {\n\t_drag = false;\n\tif (_draggedCallback) {\n\t\t_draggedCallback(closed);\n\t}\n}\n\nvoid Float::mouseDoubleClickEvent(QMouseEvent *e) {\n\tif (_item && _doubleClickedCallback) {\n\t\t// Handle second click.\n\t\tpauseResume();\n\t\t_doubleClickedCallback(_item);\n\t}\n}\n\nvoid Float::pauseResume() {\n\tif (const auto streamed = getStreamed()) {\n\t\tif (streamed->paused()) {\n\t\t\tstreamed->resume();\n\t\t} else {\n\t\t\tstreamed->pause();\n\t\t}\n\t}\n}\n\nvoid Float::detach() {\n\tif (_item) {\n\t\t_item = nullptr;\n\t\t_roundPainter = nullptr;\n\t\tif (_toggleCallback) {\n\t\t\t_toggleCallback(false);\n\t\t}\n\t}\n}\n\nvoid Float::prepareShadow() {\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto shadow = QImage(\n\t\tsize() * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tshadow.fill(Qt::transparent);\n\tshadow.setDevicePixelRatio(ratio);\n\t{\n\t\tauto p = QPainter(&shadow);\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::shadowFg);\n\t\tauto extend = 2 * st::lineWidth;\n\t\tp.drawEllipse(getInnerRect().marginsAdded(QMargins(extend, extend, extend, extend)));\n\t}\n\t_shadow = Ui::PixmapFromImage(Images::Blur(std::move(shadow)));\n}\n\nQRect Float::getInnerRect() const {\n\tauto margin = st::mediaPlayerFloatMargin;\n\treturn rect().marginsRemoved(QMargins(margin, margin, margin, margin));\n}\n\nvoid Float::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tp.setOpacity(_opacity);\n\tp.drawPixmap(0, 0, _shadow);\n\n\tconst auto inner = getInnerRect();\n\tif (!(_roundPainter && _roundPainter->fillFrame(inner.size()))\n\t\t&& _toggleCallback) {\n\t\t_toggleCallback(false);\n\t}\n\n\tif (_roundPainter) {\n\t\tp.drawImage(inner.topLeft(), _roundPainter->frame());\n\t}\n\n\tconst auto playback = getPlayback();\n\tconst auto progress = playback ? playback->value() : 1.;\n\tif (progress > 0.) {\n\t\tauto pen = st::historyVideoMessageProgressFg->p;\n\t\t//auto was = p.pen();\n\t\tpen.setWidth(st::radialLine);\n\t\tpen.setCapStyle(Qt::RoundCap);\n\t\tp.setPen(pen);\n\t\tp.setOpacity(_opacity * st::historyVideoMessageProgressOpacity);\n\n\t\tauto from = arc::kQuarterLength;\n\t\tauto len = -qRound(arc::kFullLength * progress);\n\t\tauto stepInside = st::radialLine / 2;\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawArc(inner.marginsRemoved(QMargins(stepInside, stepInside, stepInside, stepInside)), from, len);\n\t\t}\n\n\t\t//p.setPen(was);\n\t\t//p.setOpacity(_opacity);\n\t}\n}\n\nStreaming::Instance *Float::getStreamed() const {\n\treturn instance()->roundVideoStreamed(_item);\n}\n\nView::PlaybackProgress *Float::getPlayback() const {\n\treturn instance()->roundVideoPlayback(_item);\n}\n\nbool Float::hasFrame() const {\n\treturn (getStreamed() != nullptr);\n}\n\nvoid Float::repaintItem() {\n\tupdate();\n\tif (hasFrame() && _toggleCallback) {\n\t\t_toggleCallback(true);\n\t}\n}\n\n\ntemplate <typename ToggleCallback, typename DraggedCallback>\nFloatController::Item::Item(\n\tnot_null<QWidget*> parent,\n\tnot_null<HistoryItem*> item,\n\tToggleCallback toggle,\n\tDraggedCallback dragged,\n\tDoubleClickedCallback doubleClicked)\n: animationSide(RectPart::Right)\n, column(Window::Column::Second)\n, corner(RectPart::TopRight)\n, widget(\n\tparent,\n\titem,\n\t[=, toggle = std::move(toggle)](bool visible) {\n\t\ttoggle(this, visible);\n\t},\n\t[=, dragged = std::move(dragged)](bool closed) {\n\t\tdragged(this, closed);\n\t},\n\tstd::move(doubleClicked)) {\n}\n\nFloatController::FloatController(not_null<FloatDelegate*> delegate)\n: _delegate(delegate)\n, _parent(_delegate->floatPlayerWidget()) {\n\tMedia::Player::instance()->trackChanged(\n\t) | rpl::filter([=](AudioMsgId::Type type) {\n\t\treturn (type == AudioMsgId::Type::Voice);\n\t}) | rpl::on_next([=] {\n\t\tcheckCurrent();\n\t}, _lifetime);\n\n\tstartDelegateHandling();\n}\n\nvoid FloatController::replaceDelegate(not_null<FloatDelegate*> delegate) {\n\t_delegateLifetime.destroy();\n\n\t_delegate = delegate;\n\t_parent = _delegate->floatPlayerWidget();\n\n\tstartDelegateHandling();\n\n\tfor (const auto &item : _items) {\n\t\titem->widget->setParent(_parent);\n\t}\n\tcheckVisibility();\n}\n\nvoid FloatController::startDelegateHandling() {\n\t_delegate->floatPlayerCheckVisibilityRequests(\n\t) | rpl::on_next([=] {\n\t\tcheckVisibility();\n\t}, _delegateLifetime);\n\n\t_delegate->floatPlayerHideAllRequests(\n\t) | rpl::on_next([=] {\n\t\thideAll();\n\t}, _delegateLifetime);\n\n\t_delegate->floatPlayerShowVisibleRequests(\n\t) | rpl::on_next([=] {\n\t\tshowVisible();\n\t}, _delegateLifetime);\n\n\t_delegate->floatPlayerRaiseAllRequests(\n\t) | rpl::on_next([=] {\n\t\traiseAll();\n\t}, _delegateLifetime);\n\n\t_delegate->floatPlayerUpdatePositionsRequests(\n\t) | rpl::on_next([=] {\n\t\tupdatePositions();\n\t}, _delegateLifetime);\n\n\t_delegate->floatPlayerFilterWheelEventRequests(\n\t) | rpl::on_next([=](\n\t\t\tconst FloatDelegate::FloatPlayerFilterWheelEventRequest &request) {\n\t\t*request.result = filterWheelEvent(request.object, request.event);\n\t}, _delegateLifetime);\n\n\t_delegate->floatPlayerAreaUpdates(\n\t) | rpl::on_next([=] {\n\t\tcheckVisibility();\n\t}, _delegateLifetime);\n}\n\nvoid FloatController::checkCurrent() {\n\tconst auto state = Media::Player::instance()->current(AudioMsgId::Type::Voice);\n\tconst auto audio = state.audio();\n\tconst auto fullId = state.contextId();\n\tconst auto last = current();\n\tif (last\n\t\t&& audio\n\t\t&& !last->widget->detached()\n\t\t&& (&last->widget->item()->history()->session() == &audio->session())\n\t\t&& (last->widget->item()->fullId() == fullId)) {\n\t\treturn;\n\t}\n\tif (last) {\n\t\tlast->widget->detach();\n\t}\n\tif (!audio) {\n\t\treturn;\n\t}\n\tif (const auto item = audio->session().data().message(fullId)) {\n\t\tif (const auto media = item->media()) {\n\t\t\tif (const auto document = media->document()) {\n\t\t\t\tif (document->isVideoMessage()) {\n\t\t\t\t\tcreate(item);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid FloatController::create(not_null<HistoryItem*> item) {\n\t_items.push_back(std::make_unique<Item>(\n\t\t_parent,\n\t\titem,\n\t\t[=](not_null<Item*> instance, bool visible) {\n\t\t\tinstance->hiddenByWidget = !visible;\n\t\t\ttoggle(instance);\n\t\t},\n\t\t[=](not_null<Item*> instance, bool closed) {\n\t\t\tfinishDrag(instance, closed);\n\t\t},\n\t\t[=](not_null<const HistoryItem*> item) {\n\t\t\t_delegate->floatPlayerDoubleClickEvent(item);\n\t\t}));\n\tcurrent()->column = Core::App().settings().floatPlayerColumn();\n\tcurrent()->corner = Core::App().settings().floatPlayerCorner();\n\tcheckVisibility();\n}\n\nvoid FloatController::toggle(not_null<Item*> instance) {\n\tauto visible = !instance->hiddenByHistory && !instance->hiddenByWidget && instance->widget->isReady();\n\tif (instance->visible != visible) {\n\t\tinstance->widget->resetMouseState();\n\t\tinstance->visible = visible;\n\t\tif (!instance->visibleAnimation.animating() && !instance->hiddenByDrag) {\n\t\t\tauto finalRect = QRect(getPosition(instance), instance->widget->size());\n\t\t\tinstance->animationSide = getSide(finalRect.center());\n\t\t}\n\t\tinstance->visibleAnimation.start([=] {\n\t\t\tupdatePosition(instance);\n\t\t}, visible ? 0. : 1., visible ? 1. : 0., st::slideDuration, visible ? anim::easeOutCirc : anim::linear);\n\t\tupdatePosition(instance);\n\t}\n}\n\nvoid FloatController::checkVisibility() {\n\tconst auto instance = current();\n\tif (!instance) {\n\t\treturn;\n\t}\n\n\tconst auto item = instance->widget->item();\n\tinstance->hiddenByHistory = item\n\t\t? _delegate->floatPlayerIsVisible(item)\n\t\t: false;\n\ttoggle(instance);\n\tupdatePosition(instance);\n}\n\nvoid FloatController::hideAll() {\n\tfor (const auto &instance : _items) {\n\t\tinstance->widget->hide();\n\t}\n}\n\nvoid FloatController::showVisible() {\n\tfor (const auto &instance : _items) {\n\t\tif (instance->visible) {\n\t\t\tinstance->widget->show();\n\t\t}\n\t}\n}\n\nvoid FloatController::raiseAll() {\n\tfor (const auto &instance : _items) {\n\t\tinstance->widget->raise();\n\t}\n}\n\nvoid FloatController::updatePositions() {\n\tfor (const auto &instance : _items) {\n\t\tupdatePosition(instance.get());\n\t}\n}\n\nstd::optional<bool> FloatController::filterWheelEvent(\n\t\tnot_null<QObject*> object,\n\t\tnot_null<QEvent*> event) {\n\tfor (const auto &instance : _items) {\n\t\tif (instance->widget == object) {\n\t\t\tconst auto section = _delegate->floatPlayerGetSection(\n\t\t\t\tinstance->column);\n\t\t\treturn section->floatPlayerHandleWheelEvent(event);\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nvoid FloatController::updatePosition(not_null<Item*> instance) {\n\tauto visible = instance->visibleAnimation.value(instance->visible ? 1. : 0.);\n\tif (visible == 0. && !instance->visible) {\n\t\tinstance->widget->hide();\n\t\tif (instance->widget->detached()) {\n\t\t\tInvokeQueued(instance->widget, [=] {\n\t\t\t\tremove(instance);\n\t\t\t});\n\t\t}\n\t\treturn;\n\t}\n\n\tif (!instance->widget->dragged()) {\n\t\tif (instance->widget->isHidden()) {\n\t\t\tinstance->widget->show();\n\t\t}\n\n\t\tauto dragged = instance->draggedAnimation.value(1.);\n\t\tauto position = QPoint();\n\t\tif (instance->hiddenByDrag) {\n\t\t\tinstance->widget->setOpacity(instance->widget->countOpacityByParent());\n\t\t\tposition = getHiddenPosition(instance->dragFrom, instance->widget->size(), instance->animationSide);\n\t\t} else {\n\t\t\tinstance->widget->setOpacity(visible * visible);\n\t\t\tposition = getPosition(instance);\n\t\t\tif (visible < 1.) {\n\t\t\t\tauto hiddenPosition = getHiddenPosition(position, instance->widget->size(), instance->animationSide);\n\t\t\t\tposition.setX(anim::interpolate(hiddenPosition.x(), position.x(), visible));\n\t\t\t\tposition.setY(anim::interpolate(hiddenPosition.y(), position.y(), visible));\n\t\t\t}\n\t\t}\n\t\tif (dragged < 1.) {\n\t\t\tposition.setX(anim::interpolate(instance->dragFrom.x(), position.x(), dragged));\n\t\t\tposition.setY(anim::interpolate(instance->dragFrom.y(), position.y(), dragged));\n\t\t}\n\t\tinstance->widget->move(position);\n\t}\n}\n\nQPoint FloatController::getHiddenPosition(\n\t\tQPoint position,\n\t\tQSize size,\n\t\tRectPart side) const {\n\tswitch (side) {\n\tcase RectPart::Left: return { -size.width(), position.y() };\n\tcase RectPart::Top: return { position.x(), -size.height() };\n\tcase RectPart::Right: return { _parent->width(), position.y() };\n\tcase RectPart::Bottom: return { position.x(), _parent->height() };\n\t}\n\tUnexpected(\"Bad side in MainWidget::getFloatPlayerHiddenPosition().\");\n}\n\nQPoint FloatController::getPosition(not_null<Item*> instance) const {\n\tconst auto section = _delegate->floatPlayerGetSection(instance->column);\n\tconst auto rect = section->floatPlayerAvailableRect();\n\tauto position = rect.topLeft();\n\tif (IsBottomCorner(instance->corner)) {\n\t\tposition.setY(position.y() + rect.height() - instance->widget->height());\n\t}\n\tif (IsRightCorner(instance->corner)) {\n\t\tposition.setX(position.x() + rect.width() - instance->widget->width());\n\t}\n\treturn _parent->mapFromGlobal(position);\n}\n\nRectPart FloatController::getSide(QPoint center) const {\n\tconst auto left = std::abs(center.x());\n\tconst auto right = std::abs(_parent->width() - center.x());\n\tconst auto top = std::abs(center.y());\n\tconst auto bottom = std::abs(_parent->height() - center.y());\n\tif (left < right && left < top && left < bottom) {\n\t\treturn RectPart::Left;\n\t} else if (right < top && right < bottom) {\n\t\treturn RectPart::Right;\n\t} else if (top < bottom) {\n\t\treturn RectPart::Top;\n\t}\n\treturn RectPart::Bottom;\n}\n\nvoid FloatController::remove(not_null<Item*> instance) {\n\tauto widget = std::move(instance->widget);\n\tauto i = ranges::find_if(_items, [&](auto &item) {\n\t\treturn (item.get() == instance);\n\t});\n\tAssert(i != _items.end());\n\t_items.erase(i);\n\n\t// ~QWidget() can call HistoryInner::enterEvent() which can\n\t// lead to repaintHistoryItem() and we'll have an instance\n\t// in _items with destroyed widget. So we destroy the\n\t// instance first and only after that destroy the widget.\n\twidget.destroy();\n}\n\nvoid FloatController::updateColumnCorner(QPoint center) {\n\tExpects(!_items.empty());\n\n\tauto size = _items.back()->widget->size();\n\tauto min = INT_MAX;\n\tauto column = Core::App().settings().floatPlayerColumn();\n\tauto corner = Core::App().settings().floatPlayerCorner();\n\tauto checkSection = [&](\n\t\t\tnot_null<FloatSectionDelegate*> widget,\n\t\t\tWindow::Column widgetColumn) {\n\t\tauto rect = _parent->mapFromGlobal(\n\t\t\twidget->floatPlayerAvailableRect());\n\t\tauto left = rect.x() + (size.width() / 2);\n\t\tauto right = rect.x() + rect.width() - (size.width() / 2);\n\t\tauto top = rect.y() + (size.height() / 2);\n\t\tauto bottom = rect.y() + rect.height() - (size.height() / 2);\n\t\tauto checkCorner = [&](QPoint point, RectPart checked) {\n\t\t\tauto distance = (point - center).manhattanLength();\n\t\t\tif (min > distance) {\n\t\t\t\tmin = distance;\n\t\t\t\tcolumn = widgetColumn;\n\t\t\t\tcorner = checked;\n\t\t\t}\n\t\t};\n\t\tcheckCorner({ left, top }, RectPart::TopLeft);\n\t\tcheckCorner({ right, top }, RectPart::TopRight);\n\t\tcheckCorner({ left, bottom }, RectPart::BottomLeft);\n\t\tcheckCorner({ right, bottom }, RectPart::BottomRight);\n\t};\n\n\t_delegate->floatPlayerEnumerateSections(checkSection);\n\n\tauto &settings = Core::App().settings();\n\tif (settings.floatPlayerColumn() != column) {\n\t\tsettings.setFloatPlayerColumn(column);\n\t\tCore::App().saveSettingsDelayed();\n\t}\n\tif (settings.floatPlayerCorner() != corner) {\n\t\tsettings.setFloatPlayerCorner(corner);\n\t\tCore::App().saveSettingsDelayed();\n\t}\n}\n\nvoid FloatController::finishDrag(not_null<Item*> instance, bool closed) {\n\tinstance->dragFrom = instance->widget->pos();\n\tconst auto center = instance->widget->geometry().center();\n\tif (closed) {\n\t\tinstance->hiddenByDrag = true;\n\t\tinstance->animationSide = getSide(center);\n\t}\n\tupdateColumnCorner(center);\n\tinstance->column = Core::App().settings().floatPlayerColumn();\n\tinstance->corner = Core::App().settings().floatPlayerCorner();\n\n\tinstance->draggedAnimation.stop();\n\tinstance->draggedAnimation.start(\n\t\t[=] { updatePosition(instance); },\n\t\t0.,\n\t\t1.,\n\t\tst::slideDuration,\n\t\tanim::sineInOut);\n\tupdatePosition(instance);\n\n\tif (closed) {\n\t\tif (const auto item = instance->widget->item()) {\n\t\t\t_closeEvents.fire(item->fullId());\n\t\t}\n\t\tinstance->widget->detach();\n\t\tMedia::Player::instance()->stop(AudioMsgId::Type::Voice);\n\t}\n}\n\n} // namespace Player\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_float.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/rect_part.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Window {\nclass SessionController;\nenum class Column;\n} // namespace Window\n\nnamespace Media {\nnamespace View {\nclass PlaybackProgress;\n} // namespace View\n} // namespace Media\n\nnamespace Media {\nnamespace Streaming {\nclass Instance;\n} // namespace Streaming\n} // namespace Media\n\nnamespace Media {\nnamespace Player {\n\nclass RoundPainter {\npublic:\n\tRoundPainter(not_null<HistoryItem*> item);\n\n\tbool fillFrame(const QSize &size);\n\tconst QImage &frame() const;\n\nprivate:\n\tconst not_null<HistoryItem*> _item;\n\n\tQImage _roundingMask;\n\tQImage _frame;\n\n};\n\nclass Float final : public Ui::RpWidget {\npublic:\n\tFloat(\n\t\tQWidget *parent,\n\t\tnot_null<HistoryItem*> item,\n\t\tFn<void(bool visible)> toggleCallback,\n\t\tFn<void(bool closed)> draggedCallback,\n\t\tFn<void(not_null<const HistoryItem*>)> doubleClickedCallback);\n\n\t[[nodiscard]] HistoryItem *item() const {\n\t\treturn _item;\n\t}\n\tvoid setOpacity(float64 opacity) {\n\t\tif (_opacity != opacity) {\n\t\t\t_opacity = opacity;\n\t\t\tupdate();\n\t\t}\n\t}\n\t[[nodiscard]] float64 countOpacityByParent() const {\n\t\treturn outRatio();\n\t}\n\t[[nodiscard]] bool isReady() const {\n\t\treturn (getStreamed() != nullptr);\n\t}\n\tvoid detach();\n\t[[nodiscard]] bool detached() const {\n\t\treturn !_item;\n\t}\n\t[[nodiscard]] bool dragged() const {\n\t\treturn _drag;\n\t}\n\tvoid resetMouseState() {\n\t\t_down = false;\n\t\tif (_drag) {\n\t\t\tfinishDrag(false);\n\t\t}\n\t}\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid mouseDoubleClickEvent(QMouseEvent *e) override;\n\nprivate:\n\t[[nodiscard]] float64 outRatio() const;\n\t[[nodiscard]] Streaming::Instance *getStreamed() const;\n\t[[nodiscard]] View::PlaybackProgress *getPlayback() const;\n\tvoid repaintItem();\n\tvoid prepareShadow();\n\tbool hasFrame() const;\n\t[[nodiscard]] QRect getInnerRect() const;\n\tvoid finishDrag(bool closed);\n\tvoid pauseResume();\n\n\tHistoryItem *_item = nullptr;\n\tFn<void(bool visible)> _toggleCallback;\n\n\tstd::unique_ptr<RoundPainter> _roundPainter;\n\n\tfloat64 _opacity = 1.;\n\n\tQPixmap _shadow;\n\tbool _down = false;\n\tQPoint _downPoint;\n\n\tbool _drag = false;\n\tQPoint _dragLocalPoint;\n\tFn<void(bool closed)> _draggedCallback;\n\tFn<void(not_null<const HistoryItem*>)> _doubleClickedCallback;\n\n};\n\nclass FloatSectionDelegate {\npublic:\n\tvirtual QRect floatPlayerAvailableRect() = 0;\n\tvirtual bool floatPlayerHandleWheelEvent(QEvent *e) = 0;\n};\n\nclass FloatDelegate {\npublic:\n\tvirtual not_null<Ui::RpWidget*> floatPlayerWidget() = 0;\n\tvirtual void floatPlayerToggleGifsPaused(bool paused) = 0;\n\tvirtual not_null<FloatSectionDelegate*> floatPlayerGetSection(\n\t\tWindow::Column column) = 0;\n\tvirtual void floatPlayerEnumerateSections(Fn<void(\n\t\tnot_null<FloatSectionDelegate*> widget,\n\t\tWindow::Column widgetColumn)> callback) = 0;\n\tvirtual bool floatPlayerIsVisible(not_null<HistoryItem*> item) = 0;\n\n\tvirtual rpl::producer<> floatPlayerCheckVisibilityRequests() {\n\t\treturn _checkVisibility.events();\n\t}\n\tvirtual rpl::producer<> floatPlayerHideAllRequests() {\n\t\treturn _hideAll.events();\n\t}\n\tvirtual rpl::producer<> floatPlayerShowVisibleRequests() {\n\t\treturn _showVisible.events();\n\t}\n\tvirtual rpl::producer<> floatPlayerRaiseAllRequests() {\n\t\treturn _raiseAll.events();\n\t}\n\tvirtual rpl::producer<> floatPlayerUpdatePositionsRequests() {\n\t\treturn _updatePositions.events();\n\t}\n\tvirtual rpl::producer<> floatPlayerAreaUpdates() {\n\t\treturn _areaUpdates.events();\n\t}\n\tvirtual void floatPlayerDoubleClickEvent(\n\t\tnot_null<const HistoryItem*> item) {\n\t}\n\n\tstruct FloatPlayerFilterWheelEventRequest {\n\t\tnot_null<QObject*> object;\n\t\tnot_null<QEvent*> event;\n\t\tnot_null<std::optional<bool>*> result;\n\t};\n\tvirtual auto floatPlayerFilterWheelEventRequests()\n\t-> rpl::producer<FloatPlayerFilterWheelEventRequest> {\n\t\treturn _filterWheelEvent.events();\n\t}\n\n\tvirtual ~FloatDelegate() = default;\n\nprotected:\n\tvoid floatPlayerCheckVisibility() {\n\t\t_checkVisibility.fire({});\n\t}\n\tvoid floatPlayerHideAll() {\n\t\t_hideAll.fire({});\n\t}\n\tvoid floatPlayerShowVisible() {\n\t\t_showVisible.fire({});\n\t}\n\tvoid floatPlayerRaiseAll() {\n\t\t_raiseAll.fire({});\n\t}\n\tvoid floatPlayerUpdatePositions() {\n\t\t_updatePositions.fire({});\n\t}\n\tvoid floatPlayerAreaUpdated() {\n\t\t_areaUpdates.fire({});\n\t}\n\tstd::optional<bool> floatPlayerFilterWheelEvent(\n\t\t\tnot_null<QObject*> object,\n\t\t\tnot_null<QEvent*> event) {\n\t\tauto result = std::optional<bool>();\n\t\t_filterWheelEvent.fire({ object, event, &result });\n\t\treturn result;\n\t}\n\nprivate:\n\trpl::event_stream<> _checkVisibility;\n\trpl::event_stream<> _hideAll;\n\trpl::event_stream<> _showVisible;\n\trpl::event_stream<> _raiseAll;\n\trpl::event_stream<> _updatePositions;\n\trpl::event_stream<> _areaUpdates;\n\trpl::event_stream<FloatPlayerFilterWheelEventRequest> _filterWheelEvent;\n\n};\n\nclass FloatController final {\npublic:\n\texplicit FloatController(not_null<FloatDelegate*> delegate);\n\n\tvoid replaceDelegate(not_null<FloatDelegate*> delegate);\n\n\t[[nodiscard]] rpl::producer<FullMsgId> closeEvents() const {\n\t\treturn _closeEvents.events();\n\t}\n\nprivate:\n\tstruct Item {\n\t\ttemplate <typename ToggleCallback, typename DraggedCallback>\n\t\tItem(\n\t\t\tnot_null<QWidget*> parent,\n\t\t\tnot_null<HistoryItem*> item,\n\t\t\tToggleCallback toggle,\n\t\t\tDraggedCallback dragged,\n\t\t\tFn<void(not_null<const HistoryItem*>)> doubleClicked);\n\n\t\tbool hiddenByWidget = false;\n\t\tbool hiddenByHistory = false;\n\t\tbool visible = false;\n\t\tRectPart animationSide;\n\t\tUi::Animations::Simple visibleAnimation;\n\t\tWindow::Column column;\n\t\tRectPart corner;\n\t\tQPoint dragFrom;\n\t\tUi::Animations::Simple draggedAnimation;\n\t\tbool hiddenByDrag = false;\n\t\tobject_ptr<Float> widget;\n\t};\n\n\tvoid checkCurrent();\n\tvoid create(not_null<HistoryItem*> item);\n\tvoid toggle(not_null<Item*> instance);\n\tvoid updatePosition(not_null<Item*> instance);\n\tvoid remove(not_null<Item*> instance);\n\tItem *current() const {\n\t\treturn _items.empty() ? nullptr : _items.back().get();\n\t}\n\tvoid finishDrag(\n\t\tnot_null<Item*> instance,\n\t\tbool closed);\n\tvoid updateColumnCorner(QPoint center);\n\tQPoint getPosition(not_null<Item*> instance) const;\n\tQPoint getHiddenPosition(\n\t\tQPoint position,\n\t\tQSize size,\n\t\tRectPart side) const;\n\tRectPart getSide(QPoint center) const;\n\n\tvoid startDelegateHandling();\n\tvoid checkVisibility();\n\tvoid hideAll();\n\tvoid showVisible();\n\tvoid raiseAll();\n\tvoid updatePositions();\n\tstd::optional<bool> filterWheelEvent(\n\t\tnot_null<QObject*> object,\n\t\tnot_null<QEvent*> event);\n\n\tnot_null<FloatDelegate*> _delegate;\n\tnot_null<Ui::RpWidget*> _parent;\n\tstd::vector<std::unique_ptr<Item>> _items;\n\n\trpl::event_stream<FullMsgId> _closeEvents;\n\trpl::lifetime _delegateLifetime;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Player\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_instance.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/player/media_player_instance.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_streaming.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"base/options.h\"\n#include \"base/random.h\"\n#include \"base/power_save_blocker.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/audio/media_audio_capture.h\"\n#include \"media/player/media_player_listen_tracker.h\"\n#include \"media/streaming/media_streaming_instance.h\"\n#include \"media/streaming/media_streaming_player.h\"\n#include \"media/view/media_view_playback_progress.h\"\n#include \"calls/calls_instance.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_file_origin.h\"\n#include \"core/shortcuts.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"window/window_controller.h\"\n#include \"mainwindow.h\"\n#include \"main/main_domain.h\" // Domain::activeSessionValue.\n#include \"main/main_session.h\"\n#include \"main/main_account.h\" // session->account().sessionChanges().\n#include \"main/main_session_settings.h\"\n#include \"storage/storage_account.h\"\n\nnamespace Media {\nnamespace Player {\nnamespace {\n\nInstance *SingleInstance = nullptr;\n\n// Preload X message ids before and after current.\nconstexpr auto kIdsLimit = 32;\n\n// Preload next messages if we went further from current than that.\nconstexpr auto kIdsPreloadAfter = 28;\n\nconstexpr auto kShufflePlaylistLimit = 10'000;\nconstexpr auto kRememberShuffledOrderItems = 16;\n\nconstexpr auto kMinLengthForSavePositionVideo = TimeId(60); // 1 minute.\nconstexpr auto kMinLengthForSavePositionMusic = 20 * TimeId(60); // 20.\n\nbase::options::toggle OptionDisableAutoplayNext({\n\t.id = kOptionDisableAutoplayNext,\n\t.name = \"Disable auto-play of the next track\",\n\t.description = \"Disable auto-play of the next \"\n\t\t\"Audio file / Voice Message / Video message.\",\n});\n\n[[nodiscard]] float64 LookupPlaybackSpeed(const AudioMsgId &audioId) {\n\tif (!audioId.changeablePlaybackSpeed()) {\n\t\treturn 1.;\n\t}\n\tconst auto document = audioId.audio();\n\treturn (document\n\t\t&& !document->isVoiceMessage()\n\t\t&& !document->isVideoMessage())\n\t\t? Core::App().settings().audioPlaybackSpeed()\n\t\t: Core::App().settings().voicePlaybackSpeed();\n}\n\n} // namespace\n\nconst char kOptionDisableAutoplayNext[] = \"disable-autoplay-next\";\n\nstruct Instance::Streamed {\n\tStreamed(\n\t\tAudioMsgId id,\n\t\tstd::shared_ptr<Streaming::Document> document);\n\n\tAudioMsgId id;\n\tStreaming::Instance instance;\n\tView::PlaybackProgress progress;\n\tbool clearing = false;\n\trpl::lifetime lifetime;\n};\n\nstruct Instance::ShuffleData {\n\tusing UniversalMsgId = MsgId;\n\n\tstd::vector<UniversalMsgId> playlist;\n\tstd::vector<UniversalMsgId> nonPlayedIds;\n\tstd::vector<UniversalMsgId> playedIds;\n\tHistory *history = nullptr;\n\tMsgId topicRootId = 0;\n\tPeerId monoforumPeerId = 0;\n\tHistory *migrated = nullptr;\n\tbool scheduled = false;\n\tbool savedMusic = false;\n\tint indexInPlayedIds = 0;\n\tbool allLoaded = false;\n\trpl::lifetime nextSliceLifetime;\n\trpl::lifetime lifetime;\n};\n\nvoid start(not_null<Audio::Instance*> instance) {\n\tAudio::Start(instance);\n\tCapture::Start();\n\n\tSingleInstance = new Instance();\n}\n\nvoid finish(not_null<Audio::Instance*> instance) {\n\tdelete base::take(SingleInstance);\n\n\tCapture::Finish();\n\tAudio::Finish(instance);\n}\n\nvoid SaveLastPlaybackPosition(\n\t\tnot_null<DocumentData*> document,\n\t\tconst TrackState &state) {\n\tconst auto limit = document->isVideoFile()\n\t\t? kMinLengthForSavePositionVideo\n\t\t: kMinLengthForSavePositionMusic;\n\tconst auto time = (state.position == kTimeUnknown\n\t\t|| state.length == kTimeUnknown\n\t\t|| state.state == State::PausedAtEnd\n\t\t|| IsStopped(state.state))\n\t\t? TimeId(0)\n\t\t: (state.length >= limit * state.frequency)\n\t\t? (state.position / state.frequency) * crl::time(1000)\n\t\t: TimeId(0);\n\tauto &session = document->session();\n\tif (session.local().mediaLastPlaybackPosition(document->id) != time) {\n\t\tsession.local().setMediaLastPlaybackPosition(document->id, time);\n\t}\n}\n\nInstance::Streamed::Streamed(\n\tAudioMsgId id,\n\tstd::shared_ptr<Streaming::Document> document)\n: id(id)\n, instance(std::move(document), nullptr) {\n}\n\nInstance::Data::Data(AudioMsgId::Type type, SharedMediaType overview)\n: type(type)\n, overview(overview) {\n}\n\nInstance::Data::Data(Data &&other) = default;\nInstance::Data &Instance::Data::operator=(Data &&other) = default;\nInstance::Data::~Data() = default;\n\nInstance::Instance()\n: _songData(AudioMsgId::Type::Song, SharedMediaType::MusicFile)\n, _voiceData(AudioMsgId::Type::Voice, SharedMediaType::RoundVoiceFile) {\n\tMedia::Player::Updated(\n\t) | rpl::on_next([=](const AudioMsgId &audioId) {\n\t\thandleSongUpdate(audioId);\n\t}, _lifetime);\n\n\trepeatChanges(\n\t\t&_songData\n\t) | rpl::on_next([=](RepeatMode mode) {\n\t\tif (mode == RepeatMode::All) {\n\t\t\trefreshPlaylist(&_songData);\n\t\t}\n\t}, _lifetime);\n\n\torderChanges(\n\t\t&_songData\n\t) | rpl::on_next([=](OrderMode mode) {\n\t\tif (mode == OrderMode::Shuffle) {\n\t\t\tvalidateShuffleData(&_songData);\n\t\t} else {\n\t\t\t_songData.shuffleData = nullptr;\n\t\t}\n\t}, _lifetime);\n\n\tusing namespace rpl::mappers;\n\trpl::combine(\n\t\tCore::App().calls().currentCallValue(),\n\t\tCore::App().calls().currentGroupCallValue(),\n\t\t_1 || _2\n\t) | rpl::on_next([=](bool call) {\n\t\tif (call) {\n\t\t\tpauseOnCall(AudioMsgId::Type::Voice);\n\t\t\tpauseOnCall(AudioMsgId::Type::Song);\n\t\t} else {\n\t\t\tresumeOnCall(AudioMsgId::Type::Voice);\n\t\t\tresumeOnCall(AudioMsgId::Type::Song);\n\t\t}\n\t}, _lifetime);\n\n\tsetupShortcuts();\n\n\t_listenTracker = std::make_unique<MusicListenTracker>();\n}\n\nInstance::~Instance() {\n\t_listenTracker->finalize();\n}\n\nAudioMsgId::Type Instance::getActiveType() const {\n\tif (const auto data = getData(AudioMsgId::Type::Voice)) {\n\t\tif (data->current) {\n\t\t\tconst auto state = getState(data->type);\n\t\t\tif (!IsStoppedOrStopping(state.state)) {\n\t\t\t\treturn data->type;\n\t\t\t}\n\t\t}\n\t}\n\treturn AudioMsgId::Type::Song;\n}\n\nvoid Instance::handleSongUpdate(const AudioMsgId &audioId) {\n\temitUpdate(audioId.type(), [&](const AudioMsgId &playing) {\n\t\treturn (audioId == playing);\n\t});\n}\n\nvoid Instance::setCurrent(const AudioMsgId &audioId) {\n\tif (const auto data = getData(audioId.type())) {\n\t\tif (data->current == audioId) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto changed = [&](const AudioMsgId & check) {\n\t\t\treturn (check.audio() != audioId.audio())\n\t\t\t\t|| (check.contextId() != audioId.contextId());\n\t\t};\n\t\tif (changed(data->current)\n\t\t\t&& data->streamed\n\t\t\t&& changed(data->streamed->id)) {\n\t\t\tclearStreamed(data);\n\t\t}\n\t\tdata->current = audioId;\n\t\tdata->isPlaying = false;\n\n\t\tconst auto item = (audioId.audio() && audioId.contextId())\n\t\t\t? audioId.audio()->owner().message(audioId.contextId())\n\t\t\t: nullptr;\n\t\tif (item) {\n\t\t\tsetHistory(data, item->history());\n\t\t} else {\n\t\t\tsetHistory(\n\t\t\t\tdata,\n\t\t\t\tnullptr,\n\t\t\t\taudioId.audio() ? &audioId.audio()->session() : nullptr);\n\t\t}\n\t\t_trackChanged.fire_copy(data->type);\n\t\trefreshPlaylist(data);\n\t}\n}\n\nvoid Instance::setHistory(\n\t\tnot_null<Data*> data,\n\t\tHistory *history,\n\t\tMain::Session *sessionFallback) {\n\tif (history) {\n\t\tdata->history = history->migrateToOrMe();\n\t\tdata->topicRootId = 0;\n\t\tdata->monoforumPeerId = 0;\n\t\tdata->migrated = data->history->migrateFrom();\n\t\tsetSession(data, &history->session());\n\t} else {\n\t\tdata->history = data->migrated = nullptr;\n\t\tsetSession(data, sessionFallback);\n\t}\n}\n\nvoid Instance::setSession(not_null<Data*> data, Main::Session *session) {\n\tif (data->session == session) {\n\t\treturn;\n\t}\n\tdata->playlistLifetime.destroy();\n\tdata->playlistOtherLifetime.destroy();\n\tdata->sessionLifetime.destroy();\n\tdata->session = session;\n\tif (session) {\n\t\tsession->account().sessionChanges(\n\t\t) | rpl::on_next([=] {\n\t\t\tsetSession(data, nullptr);\n\t\t}, data->sessionLifetime);\n\n\t\tsession->data().documentLoadProgress(\n\t\t) | rpl::filter([=](not_null<DocumentData*> document) {\n\t\t\t// Before refactoring it was called only for audio files.\n\t\t\treturn document->isAudioFile();\n\t\t}) | rpl::on_next([=](not_null<DocumentData*> document) {\n\t\t\tconst auto type = AudioMsgId::Type::Song;\n\t\t\temitUpdate(type, [&](const AudioMsgId &audioId) {\n\t\t\t\treturn (audioId.audio() == document);\n\t\t\t});\n\t\t}, data->sessionLifetime);\n\n\t\tsession->data().itemRemoved(\n\t\t) | rpl::filter([=](not_null<const HistoryItem*> item) {\n\t\t\treturn (data->current.contextId() == item->fullId());\n\t\t}) | rpl::on_next([=] {\n\t\t\tstopAndClear(data);\n\t\t}, data->sessionLifetime);\n\t} else {\n\t\tstopAndClear(data);\n\t}\n}\n\nvoid Instance::clearStreamed(not_null<Data*> data, bool savePosition) {\n\tif (!data->streamed || data->streamed->clearing) {\n\t\treturn;\n\t}\n\tdata->streamed->clearing = true;\n\tif (savePosition) {\n\t\tSaveLastPlaybackPosition(\n\t\t\tdata->current.audio(),\n\t\t\tdata->streamed->instance.player().prepareLegacyState());\n\t}\n\tdata->streamed->instance.stop();\n\tdata->isPlaying = false;\n\trequestRoundVideoResize();\n\temitUpdate(data->type);\n\tdata->streamed = nullptr;\n\n\t_roundPlaying = false;\n\tCore::App().floatPlayerToggleGifsPaused(false);\n}\n\nvoid Instance::refreshPlaylist(not_null<Data*> data) {\n\tif (!validPlaylist(data)) {\n\t\tvalidatePlaylist(data);\n\t} else {\n\t\trefreshOtherPlaylist(data);\n\t}\n}\n\nvoid Instance::refreshOtherPlaylist(not_null<Data*> data) {\n\tif (!validOtherPlaylist(data)) {\n\t\tvalidateOtherPlaylist(data);\n\t}\n\tplaylistUpdated(data);\n}\n\nvoid Instance::playlistUpdated(not_null<Data*> data) {\n\tif (data->playlistSlice) {\n\t\tconst auto fullId = data->current.contextId();\n\t\tdata->playlistIndex = data->playlistSlice->indexOf(fullId);\n\t\tif (order(data) == OrderMode::Shuffle) {\n\t\t\tvalidateShuffleData(data);\n\t\t}\n\t} else {\n\t\tdata->playlistIndex = std::nullopt;\n\t\tdata->shuffleData = nullptr;\n\t}\n\tdata->playlistChanges.fire({});\n}\n\nbool Instance::validPlaylist(not_null<const Data*> data) const {\n\tif (const auto key = playlistKey(data)) {\n\t\tif (!data->playlistSlice) {\n\t\t\treturn false;\n\t\t}\n\t\tusing Key = SliceKey;\n\t\tconst auto inSameDomain = [](const Key &a, const Key &b) {\n\t\t\treturn (a.peerId == b.peerId)\n\t\t\t\t&& (a.topicRootId == b.topicRootId)\n\t\t\t\t&& (a.monoforumPeerId == b.monoforumPeerId)\n\t\t\t\t&& (a.migratedPeerId == b.migratedPeerId);\n\t\t};\n\t\tconst auto countDistanceInData = [&](const Key &a, const Key &b) {\n\t\t\treturn [&](const SparseIdsMergedSlice &data) {\n\t\t\t\treturn inSameDomain(a, b)\n\t\t\t\t\t? data.distance(a, b)\n\t\t\t\t\t: std::optional<int>();\n\t\t\t};\n\t\t};\n\n\t\tif (key == data->playlistRequestedKey) {\n\t\t\treturn true;\n\t\t} else if (!data->playlistSliceKey\n\t\t\t|| !data->playlistRequestedKey\n\t\t\t|| *data->playlistRequestedKey != *data->playlistSliceKey) {\n\t\t\treturn false;\n\t\t}\n\t\tauto distance = data->playlistSlice\n\t\t\t| countDistanceInData(*key, *data->playlistRequestedKey)\n\t\t\t| func::abs;\n\t\tif (distance) {\n\t\t\treturn (*distance < kIdsPreloadAfter);\n\t\t}\n\t}\n\treturn !data->playlistSlice;\n}\n\nvoid Instance::validatePlaylist(not_null<Data*> data) {\n\tdata->playlistLifetime.destroy();\n\tif (const auto key = playlistKey(data)) {\n\t\tdata->playlistRequestedKey = key;\n\n\t\tconst auto sharedMediaViewer = (key->topicRootId\n\t\t\t== SparseIdsMergedSlice::kScheduledTopicId)\n\t\t\t? SharedScheduledMediaViewer\n\t\t\t: (key->topicRootId == SparseIdsMergedSlice::kSavedMusicTopicId)\n\t\t\t? SavedMusicMediaViewer\n\t\t\t: SharedMediaMergedViewer;\n\t\tsharedMediaViewer(\n\t\t\t&data->history->session(),\n\t\t\tSharedMediaMergedKey(*key, data->overview),\n\t\t\tkIdsLimit,\n\t\t\tkIdsLimit\n\t\t) | rpl::on_next([=](SparseIdsMergedSlice &&update) {\n\t\t\tdata->playlistSlice = std::move(update);\n\t\t\tdata->playlistSliceKey = key;\n\t\t\trefreshOtherPlaylist(data);\n\t\t}, data->playlistLifetime);\n\t} else {\n\t\tdata->playlistSlice = std::nullopt;\n\t\tdata->playlistSliceKey = data->playlistRequestedKey = std::nullopt;\n\t\trefreshOtherPlaylist(data);\n\t}\n}\n\nauto Instance::playlistKey(not_null<const Data*> data) const\n-> std::optional<SliceKey> {\n\tconst auto contextId = data->current.contextId();\n\tconst auto history = data->history;\n\tif (!contextId || !history) {\n\t\treturn {};\n\t}\n\tconst auto item = data->history->owner().message(contextId);\n\tif (!item\n\t\t|| (!item->isRegular()\n\t\t\t&& !item->isScheduled()\n\t\t\t&& !item->isSavedMusicItem())) {\n\t\treturn {};\n\t}\n\n\tconst auto universalId = (contextId.peer == history->peer->id)\n\t\t? contextId.msg\n\t\t: (contextId.msg - ServerMaxMsgId);\n\treturn SliceKey(\n\t\tdata->history->peer->id,\n\t\t(item->isScheduled()\n\t\t\t? SparseIdsMergedSlice::kScheduledTopicId\n\t\t\t: item->isSavedMusicItem()\n\t\t\t? SparseIdsMergedSlice::kSavedMusicTopicId\n\t\t\t: data->topicRootId),\n\t\tdata->monoforumPeerId,\n\t\tdata->migrated ? data->migrated->peer->id : 0,\n\t\tuniversalId);\n}\n\nbool Instance::validOtherPlaylist(not_null<const Data*> data) const {\n\tif (const auto key = playlistOtherKey(data)) {\n\t\treturn data->playlistOtherSlice\n\t\t\t&& (key == data->playlistOtherRequestedKey);\n\t}\n\treturn !data->playlistOtherSlice;\n}\n\nvoid Instance::validateOtherPlaylist(not_null<Data*> data) {\n\tdata->playlistOtherLifetime.destroy();\n\tif (const auto key = playlistOtherKey(data)) {\n\t\tdata->playlistOtherRequestedKey = key;\n\n\t\tSharedMediaMergedViewer(\n\t\t\t&data->history->session(),\n\t\t\tSharedMediaMergedKey(*key, data->overview),\n\t\t\tkIdsLimit,\n\t\t\tkIdsLimit\n\t\t) | rpl::on_next([=](SparseIdsMergedSlice &&update) {\n\t\t\tdata->playlistOtherSlice = std::move(update);\n\t\t\tplaylistUpdated(data);\n\t\t}, data->playlistOtherLifetime);\n\t} else {\n\t\tdata->playlistOtherSlice = std::nullopt;\n\t\tdata->playlistOtherRequestedKey = std::nullopt;\n\t\tplaylistUpdated(data);\n\t}\n}\n\nauto Instance::playlistOtherKey(not_null<const Data*> data) const\n-> std::optional<SliceKey> {\n\tif (repeat(data) != RepeatMode::All\n\t\t|| order(data) == OrderMode::Shuffle\n\t\t|| !data->playlistSlice\n\t\t|| (data->playlistSlice->skippedBefore() != 0\n\t\t\t&& data->playlistSlice->skippedAfter() != 0)\n\t\t|| (data->playlistSlice->skippedBefore() == 0\n\t\t\t&& data->playlistSlice->skippedAfter() == 0)) {\n\t\treturn {};\n\t}\n\tconst auto contextId = data->current.contextId();\n\tconst auto history = data->history;\n\tif (!contextId || !history) {\n\t\treturn {};\n\t}\n\tconst auto item = data->history->owner().message(contextId);\n\tif (!item || !item->isRegular()) {\n\t\treturn {};\n\t}\n\n\treturn SliceKey(\n\t\tdata->history->peer->id,\n\t\tdata->topicRootId,\n\t\tdata->monoforumPeerId,\n\t\tdata->migrated ? data->migrated->peer->id : 0,\n\t\t(data->playlistSlice->skippedBefore() == 0\n\t\t\t? ServerMaxMsgId - 1\n\t\t\t: data->migrated\n\t\t\t? (1 - ServerMaxMsgId)\n\t\t\t: 1));\n}\n\nHistoryItem *Instance::itemByIndex(not_null<Data*> data, int index) {\n\tif (!data->playlistSlice\n\t\t|| index < 0\n\t\t|| index >= data->playlistSlice->size()) {\n\t\treturn nullptr;\n\t}\n\tAssert(data->history != nullptr);\n\tconst auto fullId = (*data->playlistSlice)[index];\n\treturn data->history->owner().message(fullId);\n}\n\nbool Instance::moveInPlaylist(\n\t\tnot_null<Data*> data,\n\t\tint delta,\n\t\tbool autonext) {\n\tif (!data->playlistIndex) {\n\t\treturn false;\n\t}\n\tconst auto jumpByItem = [&](not_null<HistoryItem*> item) {\n\t\tif (const auto media = item->media()) {\n\t\t\tif (media->ttlSeconds()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (const auto document = media->document()) {\n\t\t\t\tif (autonext) {\n\t\t\t\t\t_switchToNext.fire({\n\t\t\t\t\t\tdata->current,\n\t\t\t\t\t\titem->fullId()\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (document->isAudioFile()\n\t\t\t\t\t|| document->isVoiceMessage()\n\t\t\t\t\t|| document->isVideoMessage()) {\n\t\t\t\t\tplay(AudioMsgId(document, item->fullId()));\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\tconst auto jumpById = [&](FullMsgId id) {\n\t\treturn jumpByItem(data->history->owner().message(id));\n\t};\n\tconst auto repeatAll = (repeat(data) == RepeatMode::All);\n\n\tif (order(data) == OrderMode::Shuffle) {\n\t\tconst auto raw = data->shuffleData.get();\n\t\tif (!raw || !raw->history) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto universal = computeCurrentUniversalId(data);\n\t\tconst auto byUniversal = [&](ShuffleData::UniversalMsgId id) {\n\t\t\treturn (id < 0 && raw->migrated)\n\t\t\t\t? jumpById({ raw->migrated->peer->id, id + ServerMaxMsgId })\n\t\t\t\t: jumpById({ raw->history->peer->id, id });\n\t\t};\n\t\tif (universal && raw->indexInPlayedIds == raw->playedIds.size()) {\n\t\t\traw->playedIds.push_back(universal);\n\t\t\tconst auto i = ranges::find(raw->nonPlayedIds, universal);\n\t\t\tif (i != end(raw->nonPlayedIds)) {\n\t\t\t\traw->nonPlayedIds.erase(i);\n\t\t\t}\n\t\t}\n\t\tif (repeatAll) {\n\t\t\tensureShuffleMove(data, delta);\n\t\t}\n\t\tif (raw->nonPlayedIds.empty()\n\t\t\t&& raw->indexInPlayedIds + 1 == raw->playedIds.size()) {\n\t\t\traw->nonPlayedIds.push_back(raw->playedIds.back());\n\t\t\traw->playedIds.pop_back();\n\t\t}\n\t\tconst auto shuffleCompleted = raw->nonPlayedIds.empty()\n\t\t\t|| (raw->nonPlayedIds.size() == 1\n\t\t\t\t&& raw->nonPlayedIds.front() == universal);\n\t\tif (delta < 0) {\n\t\t\treturn (raw->indexInPlayedIds > 0)\n\t\t\t\t&& byUniversal(raw->playedIds[--raw->indexInPlayedIds]);\n\t\t} else if (raw->indexInPlayedIds + 1 < raw->playedIds.size()) {\n\t\t\treturn byUniversal(raw->playedIds[++raw->indexInPlayedIds]);\n\t\t}\n\t\tif (shuffleCompleted) {\n\t\t\treturn false;\n\t\t} else if (raw->indexInPlayedIds < raw->playedIds.size()) {\n\t\t\t++raw->indexInPlayedIds;\n\t\t}\n\t\tconst auto index = base::RandomIndex(raw->nonPlayedIds.size());\n\t\treturn byUniversal(raw->nonPlayedIds[index]);\n\t}\n\n\tconst auto newIndex = *data->playlistIndex\n\t\t+ (order(data) == OrderMode::Reverse ? -delta : delta);\n\tconst auto useIndex = (!repeatAll\n\t\t|| !data->playlistSlice\n\t\t|| data->playlistSlice->skippedAfter() != 0\n\t\t|| data->playlistSlice->skippedBefore() != 0\n\t\t|| !data->playlistSlice->size())\n\t\t? newIndex\n\t\t: ((newIndex + int(data->playlistSlice->size()))\n\t\t\t% int(data->playlistSlice->size()));\n\tif (const auto item = itemByIndex(data, useIndex)) {\n\t\treturn jumpByItem(item);\n\t} else if (repeatAll\n\t\t&& data->playlistOtherSlice\n\t\t&& data->playlistOtherSlice->size() > 0) {\n\t\tconst auto &other = *data->playlistOtherSlice;\n\t\tif (newIndex < 0 && other.skippedAfter() == 0) {\n\t\t\treturn jumpById(other[other.size() - 1]);\n\t\t} else if (newIndex > 0 && other.skippedBefore() == 0) {\n\t\t\treturn jumpById(other[0]);\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Instance::updatePowerSaveBlocker(\n\t\tnot_null<Data*> data,\n\t\tconst TrackState &state) {\n\tconst auto block = !IsPausedOrPausing(state.state)\n\t\t&& !IsStoppedOrStopping(state.state);\n\tconst auto blockVideo = block\n\t\t&& data->current.audio()\n\t\t&& data->current.audio()->isVideoMessage();\n\tconst auto windowResolver = [] {\n\t\tconst auto window = Core::App().activeWindow();\n\t\treturn window ? window->widget()->windowHandle() : nullptr;\n\t};\n\tbase::UpdatePowerSaveBlocker(\n\t\tdata->powerSaveBlocker,\n\t\tblock,\n\t\tbase::PowerSaveBlockType::PreventAppSuspension,\n\t\t[] { return u\"Audio playback is active\"_q; },\n\t\twindowResolver);\n\tbase::UpdatePowerSaveBlocker(\n\t\tdata->powerSaveBlockerVideo,\n\t\tblockVideo,\n\t\tbase::PowerSaveBlockType::PreventDisplaySleep,\n\t\t[] { return u\"Video playback is active\"_q; },\n\t\twindowResolver);\n}\n\nvoid Instance::ensureShuffleMove(not_null<Data*> data, int delta) {\n\tconst auto raw = data->shuffleData.get();\n\tif (delta < 0) {\n\t\tif (raw->indexInPlayedIds > 0) {\n\t\t\treturn;\n\t\t} else if (raw->nonPlayedIds.size() < 2) {\n\t\t\tconst auto freeUp = std::max(\n\t\t\t\tint(raw->playedIds.size() / 2),\n\t\t\t\tint(raw->playlist.size()) - kRememberShuffledOrderItems);\n\t\t\tconst auto till = end(raw->playedIds);\n\t\t\tconst auto from = end(raw->playedIds) - freeUp;\n\t\t\traw->nonPlayedIds.insert(end(raw->nonPlayedIds), from, till);\n\t\t\traw->playedIds.erase(from, till);\n\t\t}\n\t\tif (raw->nonPlayedIds.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto index = base::RandomIndex(raw->nonPlayedIds.size());\n\t\traw->playedIds.insert(\n\t\t\tbegin(raw->playedIds),\n\t\t\traw->nonPlayedIds[index]);\n\t\traw->nonPlayedIds.erase(begin(raw->nonPlayedIds) + index);\n\t\t++raw->indexInPlayedIds;\n\t\tif (raw->nonPlayedIds.empty() && raw->playedIds.size() > 1) {\n\t\t\traw->nonPlayedIds.push_back(raw->playedIds.back());\n\t\t\traw->playedIds.pop_back();\n\t\t}\n\t\treturn;\n\t} else if (raw->indexInPlayedIds + 1 < raw->playedIds.size()) {\n\t\treturn;\n\t} else if (raw->nonPlayedIds.size() < 2) {\n\t\tconst auto freeUp = std::max(\n\t\t\tint(raw->playedIds.size() / 2),\n\t\t\tint(raw->playlist.size()) - kRememberShuffledOrderItems);\n\t\tconst auto from = begin(raw->playedIds);\n\t\tconst auto till = begin(raw->playedIds) + freeUp;\n\t\traw->nonPlayedIds.insert(end(raw->nonPlayedIds), from, till);\n\t\traw->playedIds.erase(from, till);\n\t\traw->indexInPlayedIds -= freeUp;\n\t}\n}\n\nMsgId Instance::computeCurrentUniversalId(not_null<const Data*> data) const {\n\tconst auto raw = data->shuffleData.get();\n\tif (!raw) {\n\t\treturn MsgId(0);\n\t}\n\tconst auto current = data->current.contextId();\n\tconst auto item = raw->history->owner().message(current);\n\treturn !item\n\t\t? MsgId(0)\n\t\t: (item->history() == raw->history)\n\t\t? item->id\n\t\t: (item->history() == raw->migrated)\n\t\t? (item->id - ServerMaxMsgId)\n\t\t: MsgId(0);\n}\n\nbool Instance::previousAvailable(AudioMsgId::Type type) const {\n\tconst auto data = getData(type);\n\tAssert(data != nullptr);\n\n\tif (!data->playlistIndex || !data->playlistSlice) {\n\t\treturn false;\n\t} else if (repeat(data) == RepeatMode::All) {\n\t\treturn true;\n\t} else if (order(data) == OrderMode::Shuffle) {\n\t\tconst auto raw = data->shuffleData.get();\n\t\treturn raw && (raw->indexInPlayedIds > 0);\n\t}\n\treturn (order(data) == OrderMode::Reverse)\n\t\t? (*data->playlistIndex + 1 < data->playlistSlice->size())\n\t\t: (*data->playlistIndex > 0);\n}\n\nbool Instance::nextAvailable(AudioMsgId::Type type) const {\n\tconst auto data = getData(type);\n\tAssert(data != nullptr);\n\n\tif (!data->playlistIndex || !data->playlistSlice) {\n\t\treturn false;\n\t} else if (repeat(data) == RepeatMode::All) {\n\t\treturn true;\n\t} else if (order(data) == OrderMode::Shuffle) {\n\t\tconst auto raw = data->shuffleData.get();\n\t\tconst auto universal = computeCurrentUniversalId(data);\n\t\treturn raw\n\t\t\t&& ((raw->indexInPlayedIds + 1 < raw->playedIds.size())\n\t\t\t\t|| (raw->nonPlayedIds.size() > 1)\n\t\t\t\t|| (!raw->nonPlayedIds.empty()\n\t\t\t\t\t&& raw->nonPlayedIds.front() != universal));\n\t}\n\treturn (order(data) == OrderMode::Reverse)\n\t\t? (*data->playlistIndex > 0)\n\t\t: (*data->playlistIndex + 1 < data->playlistSlice->size());\n}\n\nrpl::producer<> Media::Player::Instance::playlistChanges(\n\t\tAudioMsgId::Type type) const {\n\tconst auto data = getData(type);\n\tAssert(data != nullptr);\n\n\treturn rpl::merge(\n\t\tdata->playlistChanges.events(),\n\t\torderChanges(data) | rpl::to_empty,\n\t\trepeatChanges(data) | rpl::to_empty);\n}\n\nrpl::producer<> Media::Player::Instance::stops(AudioMsgId::Type type) const {\n\treturn _playerStopped.events(\n\t) | rpl::filter([=](auto t) {\n\t\treturn t == type;\n\t}) | rpl::to_empty;\n}\n\nrpl::producer<> Media::Player::Instance::startsPlay(\n\t\tAudioMsgId::Type type) const {\n\treturn _playerStartedPlay.events(\n\t) | rpl::filter([=](auto t) {\n\t\treturn t == type;\n\t}) | rpl::to_empty;\n}\n\nauto Media::Player::Instance::seekingChanges(AudioMsgId::Type type) const\n-> rpl::producer<Media::Player::Instance::Seeking> {\n\treturn _seekingChanges.events(\n\t) | rpl::filter([=](SeekingChanges data) {\n\t\treturn data.type == type;\n\t}) | rpl::map([](SeekingChanges data) {\n\t\treturn data.seeking;\n\t});\n}\n\nnot_null<Instance*> instance() {\n\tExpects(SingleInstance != nullptr);\n\n\treturn SingleInstance;\n}\n\nvoid Instance::play(AudioMsgId::Type type) {\n\tif (const auto data = getData(type)) {\n\t\tif (!data->streamed || IsStopped(getState(type).state)) {\n\t\t\tplay(data->current);\n\t\t} else {\n\t\t\tif (data->streamed->instance.active()) {\n\t\t\t\tdata->streamed->instance.resume();\n\t\t\t}\n\t\t\temitUpdate(type);\n\t\t}\n\t\tdata->resumeOnCallEnd = false;\n\t}\n}\n\nvoid Instance::play(const AudioMsgId &audioId) {\n\tconst auto document = audioId.audio();\n\tif (!document) {\n\t\treturn;\n\t}\n\tif (document->isAudioFile()\n\t\t|| document->isVoiceMessage()\n\t\t|| document->isVideoMessage()) {\n\t\tauto shared = document->owner().streaming().sharedDocument(\n\t\t\tdocument,\n\t\t\taudioId.contextId());\n\t\tif (!shared) {\n\t\t\treturn;\n\t\t}\n\t\tplayStreamed(audioId, std::move(shared));\n\t}\n\tif (document->isVoiceMessage() || document->isVideoMessage()) {\n\t\tdocument->owner().markMediaRead(document);\n\t}\n\t_playerStartedPlay.fire_copy({audioId.type()});\n}\n\nvoid Instance::playPause(const AudioMsgId &audioId) {\n\tconst auto now = current(audioId.type());\n\tif (now.audio() == audioId.audio()\n\t\t&& now.contextId() == audioId.contextId()) {\n\t\tplayPause(audioId.type());\n\t} else {\n\t\tplay(audioId);\n\t}\n}\n\nvoid Instance::playStreamed(\n\t\tconst AudioMsgId &audioId,\n\t\tstd::shared_ptr<Streaming::Document> shared) {\n\tExpects(audioId.audio() != nullptr);\n\n\tconst auto data = getData(audioId.type());\n\tAssert(data != nullptr);\n\n\tclearStreamed(data, data->current.audio() != audioId.audio());\n\tdata->streamed = std::make_unique<Streamed>(\n\t\taudioId,\n\t\tstd::move(shared));\n\tdata->streamed->instance.lockPlayer();\n\n\tdata->streamed->instance.player().updates(\n\t) | rpl::on_next_error([=](Streaming::Update &&update) {\n\t\thandleStreamingUpdate(data, std::move(update));\n\t}, [=](Streaming::Error &&error) {\n\t\thandleStreamingError(data, std::move(error));\n\t}, data->streamed->lifetime);\n\n\tdata->streamed->instance.play(streamingOptions(audioId));\n\n\temitUpdate(audioId.type());\n}\n\nStreaming::PlaybackOptions Instance::streamingOptions(\n\t\tconst AudioMsgId &audioId,\n\t\tcrl::time position) {\n\tconst auto document = audioId.audio();\n\tauto result = Streaming::PlaybackOptions();\n\tresult.mode = (document && document->isVideoMessage())\n\t\t? Streaming::Mode::Both\n\t\t: Streaming::Mode::Audio;\n\tresult.speed = LookupPlaybackSpeed(audioId);\n\tresult.audioId = audioId;\n\tif (position >= 0) {\n\t\tresult.position = position;\n\t} else if (document) {\n\t\tauto &local = document->session().local();\n\t\tresult.position = local.mediaLastPlaybackPosition(document->id);\n\t\tlocal.setMediaLastPlaybackPosition(document->id, 0);\n\t} else {\n\t\tresult.position = 0;\n\t}\n\treturn result;\n}\n\nvoid Instance::pause(AudioMsgId::Type type) {\n\tif (const auto data = getData(type)) {\n\t\tif (data->streamed) {\n\t\t\tif (data->streamed->instance.active()) {\n\t\t\t\tdata->streamed->instance.pause();\n\t\t\t}\n\t\t\temitUpdate(type);\n\t\t}\n\t}\n}\n\nvoid Instance::stop(AudioMsgId::Type type, bool asFinished) {\n\tif (const auto data = getData(type)) {\n\t\tif (data->streamed) {\n\t\t\tclearStreamed(data);\n\t\t}\n\t\tif (type == AudioMsgId::Type::Song) {\n\t\t\t_listenTracker->finalize();\n\t\t}\n\t\tdata->resumeOnCallEnd = false;\n\t\t_playerStopped.fire_copy({type});\n\t}\n\tif (asFinished) {\n\t\t_tracksFinished.fire_copy(type);\n\t}\n}\n\nvoid Instance::stopAndClear(not_null<Data*> data) {\n\tstop(data->type);\n\t*data = Data(data->type, data->overview);\n\t_tracksFinished.fire_copy(data->type);\n}\n\nvoid Instance::validateShuffleData(not_null<Data*> data) {\n\tif (!data->history) {\n\t\tdata->shuffleData = nullptr;\n\t\treturn;\n\t} else if (!data->shuffleData) {\n\t\tsetupShuffleData(data);\n\t}\n\tconst auto raw = data->shuffleData.get();\n\tconst auto key = playlistKey(data);\n\tconst auto scheduled = key\n\t\t&& (key->topicRootId == SparseIdsMergedSlice::kScheduledTopicId);\n\tconst auto savedMusic = key\n\t\t&& (key->topicRootId == SparseIdsMergedSlice::kSavedMusicTopicId);\n\tif (raw->history != data->history\n\t\t|| raw->topicRootId != data->topicRootId\n\t\t|| raw->monoforumPeerId != data->monoforumPeerId\n\t\t|| raw->migrated != data->migrated\n\t\t|| raw->scheduled != scheduled\n\t\t|| raw->savedMusic != savedMusic) {\n\t\traw->history = data->history;\n\t\traw->migrated = data->migrated;\n\t\traw->scheduled = scheduled;\n\t\traw->savedMusic = savedMusic;\n\t\traw->nextSliceLifetime.destroy();\n\t\traw->allLoaded = false;\n\t\traw->playlist.clear();\n\t\traw->nonPlayedIds.clear();\n\t\traw->playedIds.clear();\n\t\traw->indexInPlayedIds = 0;\n\t} else if (raw->nextSliceLifetime) {\n\t\treturn;\n\t} else if (raw->allLoaded) {\n\t\tconst auto universal = computeCurrentUniversalId(data);\n\t\tif (!universal\n\t\t\t|| (raw->indexInPlayedIds < raw->playedIds.size()\n\t\t\t\t? (raw->playedIds[raw->indexInPlayedIds] == universal)\n\t\t\t\t: ranges::contains(raw->nonPlayedIds, universal))) {\n\t\t\treturn;\n\t\t}\n\t\t// We started playing some track not from the tracks that are left.\n\t\t// Start the whole playlist thing once again.\n\t\traw->playedIds.clear();\n\t\traw->indexInPlayedIds = 0;\n\t\tif (ranges::contains(raw->playlist, universal)) {\n\t\t\traw->nonPlayedIds = raw->playlist;\n\t\t} else {\n\t\t\traw->allLoaded = false;\n\t\t\traw->playlist.clear();\n\t\t\traw->nonPlayedIds.clear();\n\t\t}\n\t}\n\tif (raw->scheduled) {\n\t\tconst auto count = data->playlistSlice\n\t\t\t? int(data->playlistSlice->size())\n\t\t\t: 0;\n\t\tif (raw->playlist.empty() && count > 0) {\n\t\t\traw->playlist.reserve(count);\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\traw->playlist.push_back((*data->playlistSlice)[i].msg);\n\t\t\t}\n\t\t\traw->nonPlayedIds = raw->playlist;\n\t\t\traw->allLoaded = true;\n\t\t\tdata->playlistChanges.fire({});\n\t\t}\n\t\treturn;\n\t}\n\tconst auto last = raw->playlist.empty()\n\t\t? MsgId(ServerMaxMsgId - 1)\n\t\t: raw->playlist.back();\n\tconst auto sharedMediaViewer = raw->savedMusic\n\t\t? SavedMusicMediaViewer\n\t\t: SharedMediaMergedViewer;\n\tsharedMediaViewer(\n\t\t&raw->history->session(),\n\t\tSharedMediaMergedKey(\n\t\t\tSliceKey(\n\t\t\t\traw->history->peer->id,\n\t\t\t\traw->topicRootId,\n\t\t\t\traw->monoforumPeerId,\n\t\t\t\traw->migrated ? raw->migrated->peer->id : 0,\n\t\t\t\tlast),\n\t\t\tdata->overview),\n\t\tkIdsLimit,\n\t\tkIdsLimit\n\t) | rpl::on_next([=](SparseIdsMergedSlice &&update) {\n\t\traw->nextSliceLifetime.destroy();\n\n\t\tconst auto size = update.size();\n\t\tconst auto peer = raw->history->peer->id;\n\t\traw->playlist.reserve(raw->playlist.size() + size);\n\t\traw->nonPlayedIds.reserve(raw->nonPlayedIds.size() + size);\n\t\tfor (auto i = size; i != 0;) {\n\t\t\tconst auto fullId = update[--i];\n\t\t\tconst auto universal = (fullId.peer == peer)\n\t\t\t\t? fullId.msg\n\t\t\t\t: (fullId.msg - ServerMaxMsgId);\n\t\t\tif (raw->playlist.empty() || raw->playlist.back() > universal) {\n\t\t\t\traw->playlist.push_back(universal);\n\t\t\t\traw->nonPlayedIds.push_back(universal);\n\t\t\t}\n\t\t}\n\t\tif (update.skippedBefore() == 0\n\t\t\t|| raw->playlist.size() >= kShufflePlaylistLimit) {\n\t\t\traw->allLoaded = true;\n\t\t}\n\t\tdata->playlistChanges.fire({});\n\t}, raw->nextSliceLifetime);\n}\n\nvoid Instance::setupShuffleData(not_null<Data*> data) {\n\tdata->shuffleData = std::make_unique<ShuffleData>();\n\tconst auto raw = data->shuffleData.get();\n\tdata->history->session().changes().messageUpdates(\n\t\t::Data::MessageUpdate::Flag::Destroyed\n\t) | rpl::map([=](const ::Data::MessageUpdate &update) {\n\t\tconst auto item = update.item;\n\t\tconst auto history = item->history().get();\n\t\treturn (history == raw->history)\n\t\t\t? item->id\n\t\t\t: (history == raw->migrated)\n\t\t\t? (item->id - ServerMaxMsgId)\n\t\t\t: MsgId(0);\n\t}) | rpl::filter(\n\t\trpl::mappers::_1 != MsgId(0)\n\t) | rpl::on_next([=](MsgId id) {\n\t\tconst auto i = ranges::find(raw->playlist, id);\n\t\tif (i != end(raw->playlist)) {\n\t\t\traw->playlist.erase(i);\n\t\t}\n\t\tconst auto j = ranges::find(raw->nonPlayedIds, id);\n\t\tif (j != end(raw->nonPlayedIds)) {\n\t\t\traw->nonPlayedIds.erase(j);\n\t\t}\n\t\tconst auto k = ranges::find(raw->playedIds, id);\n\t\tif (k != end(raw->playedIds)) {\n\t\t\tconst auto index = (k - begin(raw->playedIds));\n\t\t\traw->playedIds.erase(k);\n\t\t\tif (raw->indexInPlayedIds > index) {\n\t\t\t\t--raw->indexInPlayedIds;\n\t\t\t}\n\t\t}\n\t}, data->shuffleData->lifetime);\n}\n\nvoid Instance::playPause(AudioMsgId::Type type) {\n\tif (const auto data = getData(type)) {\n\t\tif (!data->streamed) {\n\t\t\tplay(data->current);\n\t\t} else {\n\t\t\tauto &streamed = data->streamed->instance;\n\t\t\tif (!streamed.active()) {\n\t\t\t\tstreamed.play(streamingOptions(data->streamed->id));\n\t\t\t} else if (streamed.paused()) {\n\t\t\t\tstreamed.resume();\n\t\t\t} else {\n\t\t\t\tstreamed.pause();\n\t\t\t}\n\t\t\temitUpdate(type);\n\t\t}\n\t\tdata->resumeOnCallEnd = false;\n\t}\n}\n\nvoid Instance::pauseOnCall(AudioMsgId::Type type) {\n\tconst auto state = getState(type);\n\tif (!state.id\n\t\t|| IsStopped(state.state)\n\t\t|| IsPaused(state.state)\n\t\t|| state.state == State::Pausing) {\n\t\treturn;\n\t}\n\tpause(type);\n\tif (const auto data = getData(type)) {\n\t\tdata->resumeOnCallEnd = true;\n\t}\n}\n\nvoid Instance::resumeOnCall(AudioMsgId::Type type) {\n\tif (const auto data = getData(type)) {\n\t\tif (data->resumeOnCallEnd) {\n\t\t\tdata->resumeOnCallEnd = false;\n\t\t\tplay(type);\n\t\t}\n\t}\n}\n\nbool Instance::next(AudioMsgId::Type type) {\n\tif (const auto data = getData(type)) {\n\t\treturn moveInPlaylist(data, 1, false);\n\t}\n\treturn false;\n}\n\nbool Instance::previous(AudioMsgId::Type type) {\n\tif (const auto data = getData(type)) {\n\t\treturn moveInPlaylist(data, -1, false);\n\t}\n\treturn false;\n}\n\nvoid Instance::playPauseCancelClicked(AudioMsgId::Type type) {\n\tif (isSeeking(type)) {\n\t\treturn;\n\t}\n\n\tconst auto data = getData(type);\n\tif (!data) {\n\t\treturn;\n\t}\n\tconst auto state = getState(type);\n\tconst auto showPause = ShowPauseIcon(state.state);\n\tconst auto audio = state.id.audio();\n\tif (audio && audio->loading() && !data->streamed) {\n\t\taudio->cancel();\n\t} else if (showPause) {\n\t\tpause(type);\n\t} else {\n\t\tplay(type);\n\t}\n}\n\nvoid Instance::startSeeking(AudioMsgId::Type type) {\n\tif (auto data = getData(type)) {\n\t\tdata->seeking = data->current;\n\t}\n\tpause(type);\n\temitUpdate(type);\n\t_seekingChanges.fire({ .seeking = Seeking::Start, .type = type });\n}\n\nvoid Instance::finishSeeking(AudioMsgId::Type type, float64 progress) {\n\tif (const auto data = getData(type)) {\n\t\tif (const auto streamed = data->streamed.get()) {\n\t\t\tconst auto &info = streamed->instance.info();\n\t\t\tconst auto duration = info.audio.state.duration;\n\t\t\tif (duration != kTimeUnknown) {\n\t\t\t\tconst auto position = crl::time(base::SafeRound(\n\t\t\t\t\tstd::clamp(progress, 0., 1.) * duration));\n\t\t\t\tstreamed->instance.play(streamingOptions(\n\t\t\t\t\tstreamed->id,\n\t\t\t\t\tposition));\n\t\t\t\temitUpdate(type);\n\t\t\t}\n\t\t}\n\t}\n\tcancelSeeking(type);\n\t_seekingChanges.fire({ .seeking = Seeking::Finish, .type = type });\n}\n\nvoid Instance::cancelSeeking(AudioMsgId::Type type) {\n\tif (const auto data = getData(type)) {\n\t\tdata->seeking = AudioMsgId();\n\t}\n\temitUpdate(type);\n\t_seekingChanges.fire({ .seeking = Seeking::Cancel, .type = type });\n}\n\nvoid Instance::updatePlaybackSpeed() {\n\tif (const auto data = getData(getActiveType())) {\n\t\tif (!data->current.changeablePlaybackSpeed()) {\n\t\t\treturn;\n\t\t}\n\t\tif (const auto streamed = data->streamed.get()) {\n\t\t\tstreamed->instance.setSpeed(LookupPlaybackSpeed(data->current));\n\t\t}\n\t}\n}\n\nvoid Instance::emitUpdate(AudioMsgId::Type type) {\n\temitUpdate(type, [](const AudioMsgId &playing) { return true; });\n}\n\nRepeatMode Instance::repeat(not_null<const Data*> data) const {\n\treturn (data->type == AudioMsgId::Type::Song)\n\t\t? Core::App().settings().playerRepeatMode()\n\t\t: RepeatMode::None;\n}\n\nrpl::producer<RepeatMode> Instance::repeatChanges(\n\t\tnot_null<const Data*> data) const {\n\treturn (data->type == AudioMsgId::Type::Song)\n\t\t? Core::App().settings().playerRepeatModeChanges()\n\t\t: rpl::never<RepeatMode>();\n}\n\nOrderMode Instance::order(not_null<const Data*> data) const {\n\treturn (data->type == AudioMsgId::Type::Song)\n\t\t? Core::App().settings().playerOrderMode()\n\t\t: OrderMode::Default;\n}\n\nrpl::producer<OrderMode> Instance::orderChanges(\n\t\tnot_null<const Data*> data) const {\n\treturn (data->type == AudioMsgId::Type::Song)\n\t\t? Core::App().settings().playerOrderModeChanges()\n\t\t: rpl::never<OrderMode>();\n}\n\nTrackState Instance::getState(AudioMsgId::Type type) const {\n\tif (const auto data = getData(type)) {\n\t\tif (data->streamed) {\n\t\t\treturn data->streamed->instance.player().prepareLegacyState();\n\t\t}\n\t}\n\treturn TrackState();\n}\n\nStreaming::Instance *Instance::roundVideoStreamed(HistoryItem *item) const {\n\tif (!item) {\n\t\treturn nullptr;\n\t} else if (const auto data = getData(AudioMsgId::Type::Voice)) {\n\t\tif (const auto streamed = data->streamed.get()) {\n\t\t\tif (streamed->id.contextId() == item->fullId()) {\n\t\t\t\tconst auto player = &streamed->instance.player();\n\t\t\t\tif (player->ready() && !player->videoSize().isEmpty()) {\n\t\t\t\t\treturn &streamed->instance;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nStreaming::Instance *Instance::roundVideoPreview(\n\t\tnot_null<DocumentData*> document) const {\n\tif (const auto data = getData(AudioMsgId::Type::Voice)) {\n\t\tif (const auto streamed = data->streamed.get()) {\n\t\t\tif (streamed->id.audio() == document) {\n\t\t\t\tconst auto player = &streamed->instance.player();\n\t\t\t\tif (player->ready() && !player->videoSize().isEmpty()) {\n\t\t\t\t\treturn &streamed->instance;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nView::PlaybackProgress *Instance::roundVideoPlayback(\n\t\tHistoryItem *item) const {\n\treturn roundVideoStreamed(item)\n\t\t? &getData(AudioMsgId::Type::Voice)->streamed->progress\n\t\t: nullptr;\n}\n\ntemplate <typename CheckCallback>\nvoid Instance::emitUpdate(AudioMsgId::Type type, CheckCallback check) {\n\tif (const auto data = getData(type)) {\n\t\tconst auto state = getState(type);\n\t\tif (!state.id || !check(state.id)) {\n\t\t\treturn;\n\t\t}\n\t\tsetCurrent(state.id);\n\t\tif (const auto streamed = data->streamed.get()) {\n\t\t\tif (!streamed->instance.info().video.size.isEmpty()) {\n\t\t\t\tstreamed->progress.updateState(state);\n\t\t\t}\n\t\t}\n\t\tupdatePowerSaveBlocker(data, state);\n\t\tif (type == AudioMsgId::Type::Song) {\n\t\t\t_listenTracker->update(state);\n\t\t}\n\n\t\tauto finished = false;\n\t\t_updatedNotifier.fire_copy({state});\n\t\tif (data->isPlaying && state.state == State::StoppedAtEnd) {\n\t\t\tif (repeat(data) == RepeatMode::One) {\n\t\t\t\tplay(data->current);\n\t\t\t} else if (OptionDisableAutoplayNext.value()) {\n\t\t\t\tfinished = true;\n\t\t\t} else if (!moveInPlaylist(data, 1, true)) {\n\t\t\t\tfinished = true;\n\t\t\t}\n\t\t}\n\t\tdata->isPlaying = !IsStopped(state.state);\n\t\tif (finished) {\n\t\t\t_tracksFinished.fire_copy(type);\n\t\t}\n\t}\n}\n\nvoid Instance::setupShortcuts() {\n\tShortcuts::Requests(\n\t) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {\n\t\tusing Command = Shortcuts::Command;\n\t\trequest->check(Command::MediaPlay) && request->handle([=] {\n\t\t\tplayPause();\n\t\t\treturn true;\n\t\t});\n\t\trequest->check(Command::MediaPause) && request->handle([=] {\n\t\t\tpause();\n\t\t\treturn true;\n\t\t});\n\t\trequest->check(Command::MediaPlayPause) && request->handle([=] {\n\t\t\tplayPause();\n\t\t\treturn true;\n\t\t});\n\t\trequest->check(Command::MediaStop) && request->handle([=] {\n\t\t\tstop();\n\t\t\treturn true;\n\t\t});\n\t\trequest->check(Command::MediaPrevious) && request->handle([=] {\n\t\t\tprevious();\n\t\t\treturn true;\n\t\t});\n\t\trequest->check(Command::MediaNext) && request->handle([=] {\n\t\t\tnext();\n\t\t\treturn true;\n\t\t});\n\t}, _lifetime);\n}\n\nvoid Instance::stopAndClose() {\n\t_listenTracker->finalize();\n\t_closePlayerRequests.fire({});\n\n\tstop(AudioMsgId::Type::Voice);\n\tstop(AudioMsgId::Type::Song);\n\n\tShortcuts::ToggleMediaShortcuts(false);\n}\n\nvoid Instance::handleStreamingUpdate(\n\t\tnot_null<Data*> data,\n\t\tStreaming::Update &&update) {\n\tusing namespace Streaming;\n\n\tv::match(update.data, [&](const Information &update) {\n\t\tif (!update.video.size.isEmpty()) {\n\t\t\tdata->streamed->progress.setValueChangedCallback([=](\n\t\t\t\t\tfloat64,\n\t\t\t\t\tfloat64) {\n\t\t\t\trequestRoundVideoRepaint();\n\t\t\t});\n\t\t\t_roundPlaying = true;\n\t\t\tCore::App().floatPlayerToggleGifsPaused(true);\n\t\t\trequestRoundVideoResize();\n\t\t}\n\t\temitUpdate(data->type);\n\t}, [&](PreloadedVideo) {\n\t\t//emitUpdate(data->type, [](AudioMsgId) { return true; });\n\t}, [&](UpdateVideo) {\n\t\temitUpdate(data->type);\n\t}, [&](PreloadedAudio) {\n\t\t//emitUpdate(data->type, [](AudioMsgId) { return true; });\n\t}, [&](UpdateAudio) {\n\t\temitUpdate(data->type);\n\t}, [](WaitingForData) {\n\t}, [](SpeedEstimate) {\n\t}, [](MutedByOther) {\n\t}, [&](Finished) {\n\t\temitUpdate(data->type);\n\t\tif (data->streamed && data->streamed->instance.player().finished()) {\n\t\t\tclearStreamed(data);\n\t\t}\n\t});\n}\n\nHistoryItem *Instance::roundVideoItem() const {\n\tconst auto data = getData(AudioMsgId::Type::Voice);\n\treturn (data->streamed\n\t\t&& !data->streamed->instance.info().video.size.isEmpty()\n\t\t&& data->history)\n\t\t? data->history->owner().message(data->streamed->id.contextId())\n\t\t: nullptr;\n}\n\nvoid Instance::requestRoundVideoResize() const {\n\tif (const auto item = roundVideoItem()) {\n\t\titem->history()->owner().requestItemResize(item);\n\t}\n}\n\nvoid Instance::requestRoundVideoRepaint() const {\n\tif (const auto item = roundVideoItem()) {\n\t\titem->history()->owner().requestItemRepaint(item);\n\t}\n}\n\nvoid Instance::handleStreamingError(\n\t\tnot_null<Data*> data,\n\t\tStreaming::Error &&error) {\n\tExpects(data->streamed != nullptr);\n\n\tconst auto document = data->streamed->id.audio();\n\tconst auto contextId = data->streamed->id.contextId();\n\tif (error == Streaming::Error::NotStreamable) {\n\t\tDocumentSaveClickHandler::SaveAndTrack(\n\t\t\tcontextId,\n\t\t\tdocument);\n\t} else if (error == Streaming::Error::OpenFailed) {\n\t\tDocumentSaveClickHandler::SaveAndTrack(\n\t\t\tcontextId,\n\t\t\tdocument,\n\t\t\tDocumentSaveClickHandler::Mode::ToFile);\n\t}\n\temitUpdate(data->type);\n\tif (data->streamed && data->streamed->instance.player().failed()) {\n\t\tclearStreamed(data);\n\t}\n}\n\n} // namespace Player\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_instance.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_audio_msg_id.h\"\n#include \"data/data_shared_media.h\"\n\nclass AudioMsgId;\nclass DocumentData;\nclass History;\n\nnamespace Media {\nenum class RepeatMode;\nenum class OrderMode;\n} // namespace Media\n\nnamespace Media {\nnamespace Audio {\nclass Instance;\n} // namespace Audio\n} // namespace Media\n\nnamespace Media {\nnamespace View {\nclass PlaybackProgress;\n} // namespace View\n} // namespace Media\n\nnamespace Media {\nnamespace Streaming {\nclass Document;\nclass Instance;\nstruct PlaybackOptions;\nstruct Update;\nenum class Error;\n} // namespace Streaming\n} // namespace Media\n\nnamespace base {\nclass PowerSaveBlocker;\n} // namespace base\n\nnamespace Media {\nnamespace Player {\n\nextern const char kOptionDisableAutoplayNext[];\n\nclass Instance;\nclass MusicListenTracker;\nstruct TrackState;\n\nvoid start(not_null<Audio::Instance*> instance);\nvoid finish(not_null<Audio::Instance*> instance);\n\nvoid SaveLastPlaybackPosition(\n\tnot_null<DocumentData*> document,\n\tconst TrackState &state);\n\nnot_null<Instance*> instance();\n\nclass Instance final {\npublic:\n\tenum class Seeking {\n\t\tStart,\n\t\tFinish,\n\t\tCancel,\n\t};\n\n\tvoid play(AudioMsgId::Type type);\n\tvoid pause(AudioMsgId::Type type);\n\tvoid stop(AudioMsgId::Type type, bool asFinished = false);\n\tvoid playPause(AudioMsgId::Type type);\n\tbool next(AudioMsgId::Type type);\n\tbool previous(AudioMsgId::Type type);\n\n\tAudioMsgId::Type getActiveType() const;\n\n\tvoid play() {\n\t\tplay(getActiveType());\n\t}\n\tvoid pause() {\n\t\tpause(getActiveType());\n\t}\n\tvoid stop() {\n\t\tstop(getActiveType());\n\t}\n\tvoid playPause() {\n\t\tplayPause(getActiveType());\n\t}\n\tbool next() {\n\t\treturn next(getActiveType());\n\t}\n\tbool previous() {\n\t\treturn previous(getActiveType());\n\t}\n\n\tvoid playPauseCancelClicked(AudioMsgId::Type type);\n\n\tvoid play(const AudioMsgId &audioId);\n\tvoid playPause(const AudioMsgId &audioId);\n\t[[nodiscard]] TrackState getState(AudioMsgId::Type type) const;\n\n\t[[nodiscard]] Streaming::Instance *roundVideoStreamed(\n\t\tHistoryItem *item) const;\n\t[[nodiscard]] View::PlaybackProgress *roundVideoPlayback(\n\t\tHistoryItem *item) const;\n\n\t[[nodiscard]] Streaming::Instance *roundVideoPreview(\n\t\tnot_null<DocumentData*> document) const;\n\n\t[[nodiscard]] AudioMsgId current(AudioMsgId::Type type) const {\n\t\tif (const auto data = getData(type)) {\n\t\t\treturn data->current;\n\t\t}\n\t\treturn AudioMsgId();\n\t}\n\n\t[[nodiscard]] bool isSeeking(AudioMsgId::Type type) const {\n\t\tif (const auto data = getData(type)) {\n\t\t\treturn (data->seeking == data->current);\n\t\t}\n\t\treturn false;\n\t}\n\tvoid startSeeking(AudioMsgId::Type type);\n\tvoid finishSeeking(AudioMsgId::Type type, float64 progress);\n\tvoid cancelSeeking(AudioMsgId::Type type);\n\n\tvoid updatePlaybackSpeed();\n\n\t[[nodiscard]] bool nextAvailable(AudioMsgId::Type type) const;\n\t[[nodiscard]] bool previousAvailable(AudioMsgId::Type type) const;\n\n\tstruct Switch {\n\t\tAudioMsgId from;\n\t\tFullMsgId to;\n\t};\n\n\t[[nodiscard]] rpl::producer<Switch> switchToNextEvents() const {\n\t\treturn _switchToNext.events();\n\t}\n\t[[nodiscard]] rpl::producer<AudioMsgId::Type> tracksFinished() const {\n\t\treturn _tracksFinished.events();\n\t}\n\t[[nodiscard]] rpl::producer<AudioMsgId::Type> trackChanged() const {\n\t\treturn _trackChanged.events();\n\t}\n\n\t[[nodiscard]] rpl::producer<> playlistChanges(\n\t\tAudioMsgId::Type type) const;\n\n\t[[nodiscard]] rpl::producer<TrackState> updatedNotifier() const {\n\t\treturn _updatedNotifier.events();\n\t}\n\n\t[[nodiscard]] rpl::producer<> stops(AudioMsgId::Type type) const;\n\t[[nodiscard]] rpl::producer<> startsPlay(AudioMsgId::Type type) const;\n\n\t[[nodiscard]] rpl::producer<Seeking> seekingChanges(\n\t\tAudioMsgId::Type type) const;\n\n\t[[nodiscard]] rpl::producer<> closePlayerRequests() const {\n\t\treturn _closePlayerRequests.events();\n\t}\n\tvoid stopAndClose();\n\nprivate:\n\tusing SharedMediaType = Storage::SharedMediaType;\n\tusing SliceKey = SparseIdsMergedSlice::Key;\n\tstruct Streamed;\n\tstruct ShuffleData;\n\tstruct Data {\n\t\tData(AudioMsgId::Type type, SharedMediaType overview);\n\t\tData(Data &&other);\n\t\tData &operator=(Data &&other);\n\t\t~Data();\n\n\t\tAudioMsgId::Type type;\n\t\tStorage::SharedMediaType overview;\n\t\tAudioMsgId current;\n\t\tAudioMsgId seeking;\n\t\tstd::optional<SparseIdsMergedSlice> playlistSlice;\n\t\tstd::optional<SliceKey> playlistSliceKey;\n\t\tstd::optional<SliceKey> playlistRequestedKey;\n\t\tstd::optional<SparseIdsMergedSlice> playlistOtherSlice;\n\t\tstd::optional<SliceKey> playlistOtherRequestedKey;\n\t\tstd::optional<int> playlistIndex;\n\t\trpl::lifetime playlistLifetime;\n\t\trpl::lifetime playlistOtherLifetime;\n\t\trpl::lifetime sessionLifetime;\n\t\trpl::event_stream<> playlistChanges;\n\t\tHistory *history = nullptr;\n\t\tMsgId topicRootId = 0;\n\t\tPeerId monoforumPeerId = 0;\n\t\tHistory *migrated = nullptr;\n\t\tMain::Session *session = nullptr;\n\t\tbool isPlaying = false;\n\t\tbool resumeOnCallEnd = false;\n\t\tstd::unique_ptr<Streamed> streamed;\n\t\tstd::unique_ptr<ShuffleData> shuffleData;\n\t\tstd::unique_ptr<base::PowerSaveBlocker> powerSaveBlocker;\n\t\tstd::unique_ptr<base::PowerSaveBlocker> powerSaveBlockerVideo;\n\t};\n\n\tstruct SeekingChanges {\n\t\tSeeking seeking;\n\t\tAudioMsgId::Type type;\n\t};\n\n\tInstance();\n\t~Instance();\n\n\tfriend void start(not_null<Audio::Instance*> instance);\n\tfriend void finish(not_null<Audio::Instance*> instance);\n\n\tvoid setupShortcuts();\n\tvoid playStreamed(\n\t\tconst AudioMsgId &audioId,\n\t\tstd::shared_ptr<Streaming::Document> shared);\n\tStreaming::PlaybackOptions streamingOptions(\n\t\tconst AudioMsgId &audioId,\n\t\tcrl::time position = -1);\n\n\t// Observed notifications.\n\tvoid handleSongUpdate(const AudioMsgId &audioId);\n\n\tvoid pauseOnCall(AudioMsgId::Type type);\n\tvoid resumeOnCall(AudioMsgId::Type type);\n\n\tvoid setCurrent(const AudioMsgId &audioId);\n\tvoid refreshPlaylist(not_null<Data*> data);\n\tvoid refreshOtherPlaylist(not_null<Data*> data);\n\tstd::optional<SliceKey> playlistKey(not_null<const Data*> data) const;\n\tbool validPlaylist(not_null<const Data*> data) const;\n\tvoid validatePlaylist(not_null<Data*> data);\n\tstd::optional<SliceKey> playlistOtherKey(\n\t\tnot_null<const Data*> data) const;\n\tbool validOtherPlaylist(not_null<const Data*> data) const;\n\tvoid validateOtherPlaylist(not_null<Data*> data);\n\tvoid playlistUpdated(not_null<Data*> data);\n\tbool moveInPlaylist(not_null<Data*> data, int delta, bool autonext);\n\tvoid updatePowerSaveBlocker(\n\t\tnot_null<Data*> data,\n\t\tconst TrackState &state);\n\tHistoryItem *itemByIndex(not_null<Data*> data, int index);\n\tvoid stopAndClear(not_null<Data*> data);\n\n\t[[nodiscard]] MsgId computeCurrentUniversalId(\n\t\tnot_null<const Data*> data) const;\n\tvoid validateShuffleData(not_null<Data*> data);\n\tvoid setupShuffleData(not_null<Data*> data);\n\tvoid ensureShuffleMove(not_null<Data*> data, int delta);\n\n\tvoid handleStreamingUpdate(\n\t\tnot_null<Data*> data,\n\t\tStreaming::Update &&update);\n\tvoid handleStreamingError(\n\t\tnot_null<Data*> data,\n\t\tStreaming::Error &&error);\n\n\tvoid clearStreamed(not_null<Data*> data, bool savePosition = true);\n\tvoid emitUpdate(AudioMsgId::Type type);\n\ttemplate <typename CheckCallback>\n\tvoid emitUpdate(AudioMsgId::Type type, CheckCallback check);\n\n\t[[nodiscard]] RepeatMode repeat(not_null<const Data*> data) const;\n\t[[nodiscard]] rpl::producer<RepeatMode> repeatChanges(\n\t\tnot_null<const Data*> data) const;\n\t[[nodiscard]] OrderMode order(not_null<const Data*> data) const;\n\t[[nodiscard]] rpl::producer<OrderMode> orderChanges(\n\t\tnot_null<const Data*> data) const;\n\n\tData *getData(AudioMsgId::Type type) {\n\t\tif (type == AudioMsgId::Type::Song) {\n\t\t\treturn &_songData;\n\t\t} else if (type == AudioMsgId::Type::Voice) {\n\t\t\treturn &_voiceData;\n\t\t}\n\t\treturn nullptr;\n\t}\n\n\tconst Data *getData(AudioMsgId::Type type) const {\n\t\tif (type == AudioMsgId::Type::Song) {\n\t\t\treturn &_songData;\n\t\t} else if (type == AudioMsgId::Type::Voice) {\n\t\t\treturn &_voiceData;\n\t\t}\n\t\treturn nullptr;\n\t}\n\n\tHistoryItem *roundVideoItem() const;\n\tvoid requestRoundVideoResize() const;\n\tvoid requestRoundVideoRepaint() const;\n\n\tvoid setHistory(\n\t\tnot_null<Data*> data,\n\t\tHistory *history,\n\t\tMain::Session *sessionFallback = nullptr);\n\tvoid setSession(not_null<Data*> data, Main::Session *session);\n\n\tData _songData;\n\tData _voiceData;\n\tstd::unique_ptr<MusicListenTracker> _listenTracker;\n\tbool _roundPlaying = false;\n\n\trpl::event_stream<Switch> _switchToNext;\n\trpl::event_stream<AudioMsgId::Type> _tracksFinished;\n\trpl::event_stream<AudioMsgId::Type> _trackChanged;\n\trpl::event_stream<AudioMsgId::Type> _playerStopped;\n\trpl::event_stream<AudioMsgId::Type> _playerStartedPlay;\n\trpl::event_stream<TrackState> _updatedNotifier;\n\trpl::event_stream<SeekingChanges> _seekingChanges;\n\trpl::event_stream<> _closePlayerRequests;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Player\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_listen_tracker.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/player/media_player_listen_tracker.h\"\n\n#include \"apiwrap.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"main/main_session.h\"\n#include \"media/audio/media_audio.h\"\n\nnamespace Media::Player {\nnamespace {\n\nconstexpr auto kReportDurationSecondsMin = TimeId(3);\n\n} // namespace\n\nMusicListenTracker::MusicListenTracker()\n: _pauseTimer([=] { pauseTimedOut(); }) {\n}\n\nvoid MusicListenTracker::update(const TrackState &state) {\n\tconst auto document = state.id.audio();\n\tif (!document || !document->isSong()) {\n\t\tfinalize();\n\t\treturn;\n\t} else if (_document != document) {\n\t\tfinalize();\n\t\t_document = document;\n\t\t_contextId = state.id.contextId();\n\t\t_listenedMs = 0;\n\t\t_playing = false;\n\t\t_pauseTimer.cancel();\n\t}\n\tconst auto stopped = IsStopped(state.state);\n\tconst auto playing = !stopped && !IsPausedOrPausing(state.state);\n\tif (_playing == playing) {\n\t\treturn;\n\t} else if (!_playing) {\n\t\t_playing = true;\n\t\t_playStartedAt = crl::now();\n\t\t_pauseTimer.cancel();\n\t} else {\n\t\t_playing = false;\n\t\t_listenedMs += crl::now() - _playStartedAt;\n\t\tif (stopped) {\n\t\t\treport();\n\t\t} else {\n\t\t\t_pauseTimer.callOnce(60 * crl::time(1000));\n\t\t}\n\t}\n}\n\nvoid MusicListenTracker::finalize() {\n\tif (base::take(_playing)) {\n\t\t_listenedMs += crl::now() - _playStartedAt;\n\t}\n\t_pauseTimer.cancel();\n\treport();\n}\n\nvoid MusicListenTracker::report() {\n\tconst auto document = base::take(_document);\n\tconst auto contextId = base::take(_contextId);\n\tconst auto duration = static_cast<int>(base::take(_listenedMs) / 1000);\n\tif (!document || duration < kReportDurationSecondsMin) {\n\t\treturn;\n\t}\n\n\tconst auto origin = Data::FileOrigin(\n\t\tData::FileOriginMessage(contextId));\n\tconst auto send = [=](auto resend) -> void {\n\t\tconst auto usedFileReference = document->fileReference();\n\t\tdocument->session().api().request(MTPmessages_ReportMusicListen(\n\t\t\tdocument->mtpInput(),\n\t\t\tMTP_int(duration)\n\t\t)).fail([=](const MTP::Error &error) {\n\t\t\tif (error.code() == 400\n\t\t\t\t&& error.type().startsWith(u\"FILE_REFERENCE_\"_q)) {\n\t\t\t\tdocument->session().api().refreshFileReference(\n\t\t\t\t\torigin,\n\t\t\t\t\t[=](const auto &) {\n\t\t\t\t\t\tif (document->fileReference() != usedFileReference) {\n\t\t\t\t\t\t\tresend(resend);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\t\t}).send();\n\t};\n\tsend(send);\n}\n\nvoid MusicListenTracker::pauseTimedOut() {\n\treport();\n}\n\n} // namespace Media::Player\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_listen_tracker.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n\n#include <crl/crl_time.h>\n\nclass DocumentData;\n\nnamespace Media::Player {\n\nstruct TrackState;\n\nclass MusicListenTracker final {\npublic:\n\tMusicListenTracker();\n\n\tvoid update(const TrackState &state);\n\tvoid finalize();\n\nprivate:\n\tvoid report();\n\tvoid pauseTimedOut();\n\n\tDocumentData *_document = nullptr;\n\tFullMsgId _contextId;\n\tcrl::time _listenedMs = 0;\n\tcrl::time _playStartedAt = 0;\n\tbool _playing = false;\n\tbase::Timer _pauseTimer;\n\n};\n\n} // namespace Media::Player\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_panel.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/player/media_player_panel.h\"\n\n#include \"media/player/media_player_instance.h\"\n#include \"info/media/info_media_list_widget.h\"\n#include \"info/saved/info_saved_music_widget.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/ui_utility.h\"\n#include \"mainwindow.h\"\n#include \"main/main_session.h\"\n#include \"styles/style_overview.h\"\n#include \"styles/style_media_player.h\"\n#include \"styles/style_info.h\"\n\nnamespace Media {\nnamespace Player {\nnamespace {\n\nusing ListWidget = Info::Media::ListWidget;\n\nconstexpr auto kPlaylistIdsLimit = 32;\nconstexpr auto kDelayedHideTimeout = crl::time(3000);\n\n} // namespace\n\nPanel::Panel(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> window)\n: RpWidget(parent)\n, AbstractController(window)\n, _showTimer([=] { startShow(); })\n, _hideTimer([=] { startHideChecked(); })\n, _scroll(this, st::mediaPlayerScroll) {\n\thide();\n\tupdateSize();\n}\n\nbool Panel::overlaps(const QRect &globalRect) {\n\tif (isHidden() || _a_appearance.animating()) return false;\n\n\tauto marginLeft = rtl() ? contentRight() : contentLeft();\n\tauto marginRight = rtl() ? contentLeft() : contentRight();\n\treturn rect().marginsRemoved(QMargins(marginLeft, contentTop(), marginRight, contentBottom())).contains(QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size()));\n}\n\nvoid Panel::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid Panel::listHeightUpdated(int newHeight) {\n\tif (newHeight > emptyInnerHeight()) {\n\t\tupdateSize();\n\t} else {\n\t\t_hideTimer.callOnce(0);\n\t}\n}\n\nbool Panel::contentTooSmall() const {\n\tconst auto innerHeight = _scroll->widget()\n\t\t? _scroll->widget()->height()\n\t\t: emptyInnerHeight();\n\treturn (innerHeight <= emptyInnerHeight());\n}\n\nint Panel::emptyInnerHeight() const {\n\treturn st::infoMediaMargin.top()\n\t\t+ st::overviewFileLayout.songPadding.top()\n\t\t+ st::overviewFileLayout.songThumbSize\n\t\t+ st::overviewFileLayout.songPadding.bottom()\n\t\t+ st::infoMediaMargin.bottom();\n}\n\nbool Panel::preventAutoHide() const {\n\tif (const auto list = static_cast<ListWidget*>(_scroll->widget())) {\n\t\treturn list->preventAutoHide();\n\t}\n\treturn false;\n}\n\nvoid Panel::updateControlsGeometry() {\n\tconst auto scrollTop = contentTop();\n\tconst auto width = contentWidth();\n\tconst auto scrollHeight = qMax(\n\t\theight() - scrollTop - contentBottom() - scrollMarginBottom(),\n\t\t0);\n\tif (scrollHeight > 0) {\n\t\t_scroll->setGeometryToRight(contentRight(), scrollTop, width, scrollHeight);\n\t}\n\tif (const auto widget = static_cast<RpWidget*>(_scroll->widget())) {\n\t\twidget->resizeToWidth(width);\n\t}\n}\n\nint Panel::bestPositionFor(int left) const {\n\tleft -= contentLeft();\n\tleft -= st::mediaPlayerFileLayout.songPadding.left();\n\tleft -= st::mediaPlayerFileLayout.songThumbSize / 2;\n\treturn left;\n}\n\nvoid Panel::scrollPlaylistToCurrentTrack() {\n\tif (const auto list = static_cast<ListWidget*>(_scroll->widget())) {\n\t\tconst auto rect = list->getCurrentSongGeometry();\n\t\t_scroll->scrollToY(rect.y() - st::infoMediaMargin.top());\n\t}\n}\n\nvoid Panel::updateSize() {\n\tauto width = contentLeft() + st::mediaPlayerPanelWidth + contentRight();\n\tauto height = contentTop();\n\tauto listHeight = 0;\n\tif (auto widget = _scroll->widget()) {\n\t\tlistHeight = widget->height();\n\t}\n\tauto scrollVisible = (listHeight > 0);\n\tauto scrollHeight = scrollVisible ? (qMin(listHeight, st::mediaPlayerListHeightMax) + st::mediaPlayerListMarginBottom) : 0;\n\theight += scrollHeight + contentBottom();\n\tresize(width, height);\n\t_scroll->setVisible(scrollVisible);\n}\n\nvoid Panel::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tif (!_cache.isNull()) {\n\t\tbool animating = _a_appearance.animating();\n\t\tif (animating) {\n\t\t\tp.setOpacity(_a_appearance.value(_hiding ? 0. : 1.));\n\t\t} else if (_hiding || isHidden()) {\n\t\t\thideFinished();\n\t\t\treturn;\n\t\t}\n\t\tp.drawPixmap(0, 0, _cache);\n\t\tif (!animating) {\n\t\t\tshowChildren();\n\t\t\t_cache = QPixmap();\n\t\t}\n\t\treturn;\n\t}\n\n\t// draw shadow\n\tauto shadowedRect = myrtlrect(contentLeft(), contentTop(), contentWidth(), contentHeight());\n\tauto shadowedSides = (rtl() ? RectPart::Right : RectPart::Left)\n\t\t| RectPart::Bottom\n\t\t| (rtl() ? RectPart::Left : RectPart::Right)\n\t\t| RectPart::Top;\n\tUi::Shadow::paint(p, shadowedRect, width(), st::roundShadowRadius8px, shadowedSides);\n\tUi::FillRoundRect(p, shadowedRect, st::menuBg, Ui::MenuCorners);\n}\n\nvoid Panel::enterEventHook(QEnterEvent *e) {\n\tif (_ignoringEnterEvents || contentTooSmall()) return;\n\n\t_hideTimer.cancel();\n\tif (_a_appearance.animating()) {\n\t\tstartShow();\n\t} else {\n\t\t_showTimer.callOnce(0);\n\t}\n\treturn RpWidget::enterEventHook(e);\n}\n\nvoid Panel::leaveEventHook(QEvent *e) {\n\tif (preventAutoHide()) {\n\t\treturn;\n\t}\n\t_showTimer.cancel();\n\tif (_a_appearance.animating()) {\n\t\tstartHide();\n\t} else {\n\t\t_hideTimer.callOnce(300);\n\t}\n\treturn RpWidget::leaveEventHook(e);\n}\n\nvoid Panel::showFromOther() {\n\t_hideTimer.cancel();\n\tif (_a_appearance.animating()) {\n\t\tstartShow();\n\t} else {\n\t\t_showTimer.callOnce(300);\n\t}\n}\n\nvoid Panel::hideFromOther() {\n\t_showTimer.cancel();\n\tif (_a_appearance.animating()) {\n\t\tstartHide();\n\t} else {\n\t\t_hideTimer.callOnce(0);\n\t}\n}\n\nvoid Panel::ensureCreated() {\n\tif (_scroll->widget()) return;\n\n\t_refreshListLifetime = instance()->playlistChanges(\n\t\tAudioMsgId::Type::Song\n\t) | rpl::on_next([=] {\n\t\trefreshList();\n\t});\n\trefreshList();\n\n\tmacWindowDeactivateEvents(\n\t) | rpl::filter([=] {\n\t\treturn !isHidden();\n\t}) | rpl::on_next([=] {\n\t\tleaveEvent(nullptr);\n\t}, _refreshListLifetime);\n\n\t_ignoringEnterEvents = false;\n}\n\nvoid Panel::refreshList() {\n\tconst auto current = instance()->current(AudioMsgId::Type::Song);\n\tconst auto contextId = current.contextId();\n\tauto savedMusicItem = false;\n\tconst auto peer = [&]() -> PeerData* {\n\t\tif (const auto document = current.audio()) {\n\t\t\tif (&document->session() != &session()) {\n\t\t\t\t// Different account is playing music.\n\t\t\t\treturn nullptr;\n\t\t\t}\n\t\t}\n\t\tconst auto item = contextId\n\t\t\t? session().data().message(contextId)\n\t\t\t: nullptr;\n\t\tconst auto media = item ? item->media() : nullptr;\n\t\tconst auto document = media ? media->document() : nullptr;\n\t\tif (!document\n\t\t\t|| !document->isSharedMediaMusic()\n\t\t\t|| (!item->isRegular()\n\t\t\t\t&& !item->isScheduled()\n\t\t\t\t&& !item->isSavedMusicItem())) {\n\t\t\treturn nullptr;\n\t\t}\n\t\tsavedMusicItem = item->isSavedMusicItem();\n\t\tconst auto result = item->history()->peer;\n\t\tif (const auto migrated = result->migrateTo()) {\n\t\t\treturn migrated;\n\t\t}\n\t\treturn result;\n\t}();\n\tconst auto migrated = peer ? peer->migrateFrom() : nullptr;\n\tconst auto listPeer = savedMusicItem ? nullptr : peer;\n\tconst auto listMusicPeer = savedMusicItem ? peer : nullptr;\n\tconst auto listMigratedPeer = savedMusicItem ? nullptr : migrated;\n\tif (_listPeer != listPeer\n\t\t|| _listMusicPeer != listMusicPeer\n\t\t|| _listMigratedPeer != listMigratedPeer) {\n\t\t_scroll->takeWidget<QWidget>().destroy();\n\t\t_listPeer = _listMusicPeer = _listMigratedPeer = nullptr;\n\t}\n\tif ((listPeer && !_listPeer) || (listMusicPeer && !_listMusicPeer)) {\n\t\t_listPeer = listPeer;\n\t\t_listMusicPeer = listMusicPeer;\n\t\t_listMigratedPeer = listMigratedPeer;\n\t\tauto list = object_ptr<ListWidget>(this, infoController());\n\n\t\tconst auto weak = _scroll->setOwnedWidget(std::move(list));\n\n\t\tupdateSize();\n\t\tupdateControlsGeometry();\n\n\t\tweak->checkForHide(\n\t\t) | rpl::on_next([this] {\n\t\t\tif (!rect().contains(mapFromGlobal(QCursor::pos()))) {\n\t\t\t\t_hideTimer.callOnce(kDelayedHideTimeout);\n\t\t\t}\n\t\t}, weak->lifetime());\n\n\t\tweak->heightValue(\n\t\t) | rpl::on_next([this](int newHeight) {\n\t\t\tlistHeightUpdated(newHeight);\n\t\t}, weak->lifetime());\n\n\t\tweak->scrollToRequests(\n\t\t) | rpl::on_next([this](int newScrollTop) {\n\t\t\t_scroll->scrollToY(newScrollTop);\n\t\t}, weak->lifetime());\n\n\t\t// MSVC BUG + REGRESSION rpl::mappers::tuple :(\n\t\tusing namespace rpl::mappers;\n\t\trpl::combine(\n\t\t\t_scroll->scrollTopValue(),\n\t\t\t_scroll->heightValue()\n\t\t) | rpl::on_next([=](int top, int height) {\n\t\t\tconst auto bottom = top + height;\n\t\t\tweak->setVisibleTopBottom(top, bottom);\n\t\t}, weak->lifetime());\n\n\t\tauto musicMemento = Info::Saved::MusicMemento(peer);\n\t\tauto mediaMemento = Info::Media::Memento(\n\t\t\tpeer,\n\t\t\tmigratedPeerId(),\n\t\t\t(listMusicPeer\n\t\t\t\t? Storage::SharedMediaType::MusicFile\n\t\t\t\t: section().mediaType()));\n\t\tauto &memento = listMusicPeer ? musicMemento.media() : mediaMemento;\n\t\tmemento.setAroundId(contextId);\n\t\tmemento.setIdsLimit(kPlaylistIdsLimit);\n\t\tmemento.setScrollTopItem({ contextId, peer->session().uniqueId() });\n\t\tmemento.setScrollTopShift(-st::infoMediaMargin.top());\n\t\tweak->restoreState(&memento);\n\t}\n}\n\nvoid Panel::performDestroy() {\n\tif (!_scroll->widget()) return;\n\n\t_scroll->takeWidget<QWidget>().destroy();\n\t_listPeer = _listMusicPeer = _listMigratedPeer = nullptr;\n\t_refreshListLifetime.destroy();\n}\n\nInfo::Key Panel::key() const {\n\treturn _listMusicPeer\n\t\t? Info::Key(Info::Saved::MusicTag{ _listMusicPeer })\n\t\t: Info::Key(_listPeer);\n}\n\nPeerData *Panel::migrated() const {\n\treturn _listMigratedPeer;\n}\n\nInfo::Section Panel::section() const {\n\treturn _listMusicPeer\n\t\t? Info::Section(Info::Section::Type::SavedMusic)\n\t\t: Info::Section(Info::Section::MediaType::MusicFile);\n}\n\nvoid Panel::startShow() {\n\tensureCreated();\n\tif (contentTooSmall()) {\n\t\treturn;\n\t}\n\n\tif (isHidden()) {\n\t\tscrollPlaylistToCurrentTrack();\n\t\tshow();\n\t} else if (!_hiding) {\n\t\treturn;\n\t}\n\t_hiding = false;\n\tstartAnimation();\n}\n\nvoid Panel::hideIgnoringEnterEvents() {\n\t_ignoringEnterEvents = true;\n\tif (isHidden()) {\n\t\thideFinished();\n\t} else {\n\t\tstartHide();\n\t}\n}\n\nvoid Panel::startHideChecked() {\n\tif (!contentTooSmall() && preventAutoHide()) {\n\t\treturn;\n\t}\n\tif (isHidden()) {\n\t\thideFinished();\n\t} else {\n\t\tstartHide();\n\t}\n}\n\nvoid Panel::startHide() {\n\tif (_hiding || isHidden()) return;\n\n\t_hiding = true;\n\tstartAnimation();\n}\n\nvoid Panel::startAnimation() {\n\tauto from = _hiding ? 1. : 0.;\n\tauto to = _hiding ? 0. : 1.;\n\tif (_cache.isNull()) {\n\t\tshowChildren();\n\t\t_cache = Ui::GrabWidget(this);\n\t}\n\thideChildren();\n\t_a_appearance.start([this] { appearanceCallback(); }, from, to, st::defaultInnerDropdown.duration);\n}\n\nvoid Panel::appearanceCallback() {\n\tif (!_a_appearance.animating() && _hiding) {\n\t\t_hiding = false;\n\t\thideFinished();\n\t} else {\n\t\tupdate();\n\t}\n}\n\nvoid Panel::hideFinished() {\n\thide();\n\t_cache = QPixmap();\n\tperformDestroy();\n}\n\nint Panel::contentLeft() const {\n\treturn st::mediaPlayerPanelMarginLeft;\n}\n\nint Panel::contentTop() const {\n\treturn st::mediaPlayerPanelMarginLeft;\n}\n\nint Panel::contentRight() const {\n\treturn st::mediaPlayerPanelMarginLeft;\n}\n\nint Panel::contentBottom() const {\n\treturn st::mediaPlayerPanelMarginBottom;\n}\n\nint Panel::scrollMarginBottom() const {\n\treturn 0;// st::mediaPlayerPanelMarginBottom;\n}\n\n} // namespace Player\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_panel.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n#include \"info/info_controller.h\"\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass ScrollArea;\nclass Shadow;\n} // namespace Ui\n\nnamespace Media {\nnamespace Player {\n\nclass CoverWidget;\n\nclass Panel : public Ui::RpWidget, private Info::AbstractController {\npublic:\n\tPanel(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\tbool overlaps(const QRect &globalRect);\n\n\tvoid hideIgnoringEnterEvents();\n\n\tvoid showFromOther();\n\tvoid hideFromOther();\n\n\tint bestPositionFor(int left) const;\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\nprivate:\n\t// Info::AbstractController implementation.\n\tInfo::Key key() const override;\n\tPeerData *migrated() const override;\n\tInfo::Section section() const override;\n\n\tvoid startShow();\n\tvoid startHide();\n\tvoid startHideChecked();\n\tbool preventAutoHide() const;\n\tvoid listHeightUpdated(int newHeight);\n\tint emptyInnerHeight() const;\n\tbool contentTooSmall() const;\n\n\tvoid ensureCreated();\n\tvoid performDestroy();\n\n\tvoid updateControlsGeometry();\n\tvoid refreshList();\n\tvoid updateSize();\n\tvoid appearanceCallback();\n\tvoid hideFinished();\n\tint contentLeft() const;\n\tint contentTop() const;\n\tint contentRight() const;\n\tint contentBottom() const;\n\tint scrollMarginBottom() const;\n\tint contentWidth() const {\n\t\treturn width() - contentLeft() - contentRight();\n\t}\n\tint contentHeight() const {\n\t\treturn height() - contentTop() - contentBottom();\n\t}\n\n\tvoid startAnimation();\n\tvoid scrollPlaylistToCurrentTrack();\n\tnot_null<Info::AbstractController*> infoController() {\n\t\treturn static_cast<Info::AbstractController*>(this);\n\t}\n\n\tbool _hiding = false;\n\n\tQPixmap _cache;\n\tUi::Animations::Simple _a_appearance;\n\n\tbool _ignoringEnterEvents = false;\n\n\tbase::Timer _showTimer;\n\tbase::Timer _hideTimer;\n\n\tobject_ptr<Ui::ScrollArea> _scroll;\n\n\trpl::lifetime _refreshListLifetime;\n\tPeerData *_listPeer = nullptr;\n\tPeerData *_listMusicPeer = nullptr;\n\tPeerData *_listMigratedPeer = nullptr;\n\n};\n\n} // namespace Player\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_volume_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/player/media_player_volume_controller.h\"\n\n#include \"media/audio/media_audio.h\"\n#include \"media/player/media_player_dropdown.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"mainwindow.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"styles/style_media_player.h\"\n#include \"styles/style_widgets.h\"\n\n#include <QtGui/QGuiApplication>\n\nnamespace Media::Player {\n\nVolumeController::VolumeController(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: RpWidget(parent)\n, _slider(this, st::mediaPlayerPanelPlayback) {\n\t_slider->setMoveByWheel(true);\n\t_slider->setChangeProgressCallback([=](float64 volume) {\n\t\tapplyVolumeChange(volume);\n\t});\n\t_slider->setChangeFinishedCallback([=](float64 volume) {\n\t\tif (volume > 0) {\n\t\t\tCore::App().settings().setRememberedSongVolume(volume);\n\t\t}\n\t\tapplyVolumeChange(volume);\n\t\tCore::App().saveSettingsDelayed();\n\t});\n\tCore::App().settings().songVolumeChanges(\n\t) | rpl::on_next([=](float64 volume) {\n\t\tif (!_slider->isChanging()) {\n\t\t\t_slider->setValue(volume);\n\t\t}\n\t}, lifetime());\n\tsetVolume(Core::App().settings().songVolume());\n\n\tresize(st::mediaPlayerPanelVolumeWidth, 2 * st::mediaPlayerPanelPlaybackPadding + st::mediaPlayerPanelPlayback.width);\n}\n\nvoid VolumeController::setIsVertical(bool vertical) {\n\tusing Direction = Ui::MediaSlider::Direction;\n\t_slider->setDirection(vertical ? Direction::Vertical : Direction::Horizontal);\n\t_slider->setAlwaysDisplayMarker(vertical);\n}\n\nvoid VolumeController::outerWheelEvent(not_null<QWheelEvent*> e) {\n\tQGuiApplication::sendEvent(_slider.data(), e);\n}\n\nvoid VolumeController::resizeEvent(QResizeEvent *e) {\n\t_slider->setGeometry(rect());\n}\n\nvoid VolumeController::setVolume(float64 volume) {\n\t_slider->setValue(volume);\n\tif (volume > 0) {\n\t\tCore::App().settings().setRememberedSongVolume(volume);\n\t}\n\tapplyVolumeChange(volume);\n}\n\nvoid VolumeController::applyVolumeChange(float64 volume) {\n\tif (volume != Core::App().settings().songVolume()) {\n\t\tmixer()->setSongVolume(volume);\n\t\tCore::App().settings().setSongVolume(volume);\n\t}\n}\n\nvoid PrepareVolumeDropdown(\n\t\tnot_null<Dropdown*> dropdown,\n\t\tnot_null<Window::SessionController*> controller,\n\t\trpl::producer<not_null<QWheelEvent*>> outerWheelEvents) {\n\tconst auto volume = Ui::CreateChild<VolumeController>(\n\t\tdropdown.get(),\n\t\tcontroller);\n\tvolume->show();\n\tvolume->setIsVertical(true);\n\n\tdropdown->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tconst auto rect = QRect(QPoint(), size);\n\t\tconst auto inner = rect.marginsRemoved(dropdown->getMargin());\n\t\tvolume->setGeometry(\n\t\t\tinner.x(),\n\t\t\tinner.y() - st::lineWidth,\n\t\t\tinner.width(),\n\t\t\t(inner.height()\n\t\t\t\t+ st::lineWidth\n\t\t\t\t- ((st::mediaPlayerVolumeSize.width()\n\t\t\t\t\t- st::mediaPlayerPanelPlayback.width) / 2)));\n\t}, volume->lifetime());\n\n\tstd::move(\n\t\touterWheelEvents\n\t) | rpl::on_next([=](not_null<QWheelEvent*> e) {\n\t\tvolume->outerWheelEvent(e);\n\t}, volume->lifetime());\n}\n\n} // namespace Media::Player\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_volume_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"base/object_ptr.h\"\n\nclass QWheelEvent;\n\nnamespace Ui {\nclass MediaSlider;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Media::Player {\n\nclass Dropdown;\n\nclass VolumeController final : public Ui::RpWidget {\npublic:\n\tVolumeController(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\tvoid setIsVertical(bool vertical);\n\tvoid outerWheelEvent(not_null<QWheelEvent*> e);\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid setVolume(float64 volume);\n\tvoid applyVolumeChange(float64 volume);\n\n\tobject_ptr<Ui::MediaSlider> _slider;\n\n};\n\nvoid PrepareVolumeDropdown(\n\tnot_null<Dropdown*> dropdown,\n\tnot_null<Window::SessionController*> controller,\n\trpl::producer<not_null<QWheelEvent*>> outerWheelEvents);\n\n} // namespace Media::Player\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/player/media_player_widget.h\"\n\n#include \"platform/platform_specific.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_peer.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/format_song_document_name.h\"\n#include \"lang/lang_keys.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/view/media_view_playback_progress.h\"\n#include \"media/player/media_player_button.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"media/player/media_player_dropdown.h\"\n#include \"media/player/media_player_volume_controller.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_helpers.h\"\n#include \"storage/storage_account.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_media_player.h\"\n#include \"styles/style_media_view.h\"\n#include \"styles/style_chat.h\" // expandedMenuSeparator.\n\nnamespace Media {\nnamespace Player {\n\nWidget::Widget(\n\tQWidget *parent,\n\tnot_null<Ui::RpWidget*> dropdownsParent,\n\tnot_null<Window::SessionController*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _orderMenuParent(dropdownsParent)\n, _nameLabel(this, st::mediaPlayerName)\n, _rightControls(this, object_ptr<Ui::RpWidget>(this))\n, _timeLabel(rightControls(), st::mediaPlayerTime)\n, _playPause(this, st::mediaPlayerPlayButton)\n, _volumeToggle(rightControls(), st::mediaPlayerVolumeToggle)\n, _repeatToggle(rightControls(), st::mediaPlayerRepeatButton)\n, _orderToggle(rightControls(), st::mediaPlayerOrderButton)\n, _speedToggle(rightControls(), st::mediaPlayerSpeedButton)\n, _close(this, st::mediaPlayerClose)\n, _shadow(this)\n, _playbackSlider(this, st::mediaPlayerPlayback)\n, _volume(std::in_place, dropdownsParent.get())\n, _playbackProgress(std::make_unique<View::PlaybackProgress>())\n, _orderController(\n\tstd::make_unique<OrderController>(\n\t\t_orderToggle.data(),\n\t\tdropdownsParent,\n\t\t[=](bool over) { markOver(over); },\n\t\tCore::App().settings().playerOrderModeValue(),\n\t\t[=](OrderMode value) { saveOrder(value); }))\n, _speedController(\n\tstd::make_unique<SpeedController>(\n\t\t_speedToggle.data(),\n\t\t_speedToggle->st(),\n\t\tdropdownsParent,\n\t\t[=](bool over) { markOver(over); },\n\t\t[=](bool lastNonDefault) { return speedLookup(lastNonDefault); },\n\t\t[=](float64 speed) { saveSpeed(speed); })) {\n\t_speedController->realtimeValue(\n\t) | rpl::on_next([=](float64 speed) {\n\t\t_speedToggle->setSpeed(speed);\n\t\t_speedToggle->setAccessibleName(tr::lng_mediaview_playback_speed(\n\t\t\ttr::now,\n\t\t\tlt_speed,\n\t\t\tQString::number(base::SafeRound(speed * 10) / 10.) + \"x\"));\n\t}, _speedToggle->lifetime());\n\t_speedToggle->finishAnimating();\n\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\tsetMouseTracking(true);\n\tresize(width(), st::mediaPlayerHeight + st::lineWidth);\n\n\tsetupRightControls();\n\n\t_volumeToggle->setAccessibleName(tr::lng_ringtones_box_volume(tr::now));\n\t_repeatToggle->setAccessibleName(tr::lng_schedule_repeat_label(tr::now));\n\t_orderToggle->setAccessibleName(tr::lng_sr_playback_order(tr::now));\n\t_close->setAccessibleName(tr::lng_sr_player_close(tr::now));\n\n\t_nameLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_timeLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t_playbackProgress->setInLoadingStateChangedCallback([=](bool loading) {\n\t\t_playbackSlider->setDisabled(loading);\n\t});\n\t_playbackProgress->setValueChangedCallback([=](float64 value, float64) {\n\t\t_playbackSlider->setValue(value);\n\t});\n\t_playbackSlider->setChangeProgressCallback([=](float64 value) {\n\t\tif (_type != AudioMsgId::Type::Song) {\n\t\t\treturn; // Round video seek is not supported for now :(\n\t\t}\n\t\t_playbackProgress->setValue(value, false);\n\t\thandleSeekProgress(value);\n\t});\n\t_playbackSlider->setChangeFinishedCallback([=](float64 value) {\n\t\tif (_type != AudioMsgId::Type::Song) {\n\t\t\treturn; // Round video seek is not supported for now :(\n\t\t}\n\t\t_playbackProgress->setValue(value, false);\n\t\thandleSeekFinished(value);\n\t});\n\t_playPause->setClickedCallback([=] {\n\t\tinstance()->playPauseCancelClicked(_type);\n\t});\n\n\tupdateVolumeToggleIcon();\n\t_volumeToggle->setClickedCallback([=] {\n\t\tconst auto volume = (Core::App().settings().songVolume() > 0)\n\t\t\t? 0.\n\t\t\t: Core::App().settings().rememberedSongVolume();\n\t\tCore::App().settings().setSongVolume(volume);\n\t\tCore::App().saveSettingsDelayed();\n\t\tmixer()->setSongVolume(volume);\n\t});\n\tCore::App().settings().songVolumeChanges(\n\t) | rpl::on_next([=] {\n\t\tupdateVolumeToggleIcon();\n\t}, lifetime());\n\n\tCore::App().settings().playerRepeatModeValue(\n\t) | rpl::on_next([=] {\n\t\tupdateRepeatToggleIcon();\n\t}, lifetime());\n\n\t_repeatToggle->setClickedCallback([=] {\n\t\tauto &settings = Core::App().settings();\n\t\tsettings.setPlayerRepeatMode([&] {\n\t\t\tswitch (settings.playerRepeatMode()) {\n\t\t\tcase RepeatMode::None: return RepeatMode::One;\n\t\t\tcase RepeatMode::One: return RepeatMode::All;\n\t\t\tcase RepeatMode::All: return RepeatMode::None;\n\t\t\t}\n\t\t\tUnexpected(\"Repeat mode in Settings.\");\n\t\t}());\n\t\tCore::App().saveSettingsDelayed();\n\t});\n\n\t_speedController->saved(\n\t) | rpl::on_next([=] {\n\t\tinstance()->updatePlaybackSpeed();\n\t}, lifetime());\n\n\trpl::merge(\n\t\tCore::App().settings().voicePlaybackSpeedChanges() | rpl::to_empty,\n\t\tCore::App().settings().audioPlaybackSpeedChanges() | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\t_speedController->reloadFromLookup();\n\t}, lifetime());\n\n\tinstance()->trackChanged(\n\t) | rpl::filter([=](AudioMsgId::Type type) {\n\t\treturn (type == _type);\n\t}) | rpl::on_next([=](AudioMsgId::Type type) {\n\t\thandleSongChange();\n\t\tupdateControlsVisibility();\n\t\tupdateLabelsGeometry();\n\t}, lifetime());\n\n\trpl::merge(\n\t\tinstance()->tracksFinished(\n\t\t) | rpl::filter([=](AudioMsgId::Type type) {\n\t\t\treturn (type == AudioMsgId::Type::Voice);\n\t\t}) | rpl::to_empty,\n\t\tinstance()->stops(AudioMsgId::Type::Voice)\n\t) | rpl::on_next([=] {\n\t\t_voiceIsActive = false;\n\t\tconst auto currentSong = instance()->current(AudioMsgId::Type::Song);\n\t\tconst auto songState = instance()->getState(AudioMsgId::Type::Song);\n\t\tif (currentSong == songState.id && !IsStoppedOrStopping(songState.state)) {\n\t\t\tsetType(AudioMsgId::Type::Song);\n\t\t}\n\t}, lifetime());\n\n\tinstance()->updatedNotifier(\n\t) | rpl::on_next([=](const TrackState &state) {\n\t\thandleSongUpdate(state);\n\t}, lifetime());\n\n\tPrepareVolumeDropdown(_volume.get(), controller, _volumeToggle->events(\n\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\treturn (e->type() == QEvent::Wheel);\n\t}) | rpl::map([=](not_null<QEvent*> e) {\n\t\treturn not_null{ static_cast<QWheelEvent*>(e.get()) };\n\t}));\n\t_volumeToggle->installEventFilter(_volume.get());\n\t_volume->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::Enter) {\n\t\t\tmarkOver(true);\n\t\t} else if (e->type() == QEvent::Leave) {\n\t\t\tmarkOver(false);\n\t\t}\n\t}, _volume->lifetime());\n\n\thidePlaylistOn(_playPause);\n\thidePlaylistOn(_close);\n\thidePlaylistOn(_rightControls);\n\n\tsetType(AudioMsgId::Type::Song);\n}\n\nvoid Widget::hidePlaylistOn(not_null<Ui::RpWidget*> widget) {\n\twidget->events(\n\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\treturn (e->type() == QEvent::Enter);\n\t}) | rpl::on_next([=] {\n\t\tupdateOverLabelsState(false);\n\t}, widget->lifetime());\n}\n\nvoid Widget::setupRightControls() {\n\tconst auto raw = rightControls();\n\traw->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(raw);\n\t\tconst auto &icon = st::mediaPlayerControlsFade;\n\t\tconst auto fade = QRect(0, 0, icon.width(), raw->height());\n\t\tif (fade.intersects(clip)) {\n\t\t\ticon.fill(p, fade);\n\t\t}\n\t\tconst auto fill = clip.intersected(\n\t\t\t{ icon.width(), 0, raw->width() - icon.width(), raw->height() });\n\t\tif (!fill.isEmpty()) {\n\t\t\tp.fillRect(fill, st::mediaPlayerBg);\n\t\t}\n\t}, raw->lifetime());\n\t_rightControls->show(anim::type::instant);\n}\n\nvoid Widget::updateVolumeToggleIcon() {\n\t_volumeToggle->setIconOverride([] {\n\t\tconst auto volume = Core::App().settings().songVolume();\n\t\treturn (volume == 0.)\n\t\t\t? &st::mediaPlayerVolumeIcon0\n\t\t\t: (volume < 0.66)\n\t\t\t? &st::mediaPlayerVolumeIcon1\n\t\t\t: nullptr;\n\t}());\n}\n\nvoid Widget::setCloseCallback(Fn<void()> callback) {\n\t_closeCallback = std::move(callback);\n\t_close->setClickedCallback([this] { stopAndClose(); });\n}\n\nvoid Widget::setShowItemCallback(\n\t\tFn<void(not_null<const HistoryItem*>)> callback) {\n\t_showItemCallback = std::move(callback);\n}\n\nvoid Widget::stopAndClose() {\n\t_voiceIsActive = false;\n\tif (_type == AudioMsgId::Type::Voice) {\n\t\tconst auto songData = instance()->current(AudioMsgId::Type::Song);\n\t\tconst auto songState = instance()->getState(AudioMsgId::Type::Song);\n\t\tif (songData == songState.id && !IsStoppedOrStopping(songState.state)) {\n\t\t\tinstance()->stop(AudioMsgId::Type::Voice);\n\t\t\treturn;\n\t\t}\n\t}\n\tif (_closeCallback) {\n\t\t_closeCallback();\n\t}\n}\n\nvoid Widget::setShadowGeometryToLeft(int x, int y, int w, int h) {\n\t_shadow->setGeometryToLeft(x, y, w, h);\n}\n\nvoid Widget::showShadowAndDropdowns() {\n\t_shadow->show();\n\t_playbackSlider->setVisible(_type == AudioMsgId::Type::Song);\n\tif (_volumeHidden) {\n\t\t_volumeHidden = false;\n\t\t_volume->show();\n\t}\n\t_speedController->showBack();\n\t_orderController->showBack();\n}\n\nvoid Widget::updateDropdownsGeometry() {\n\tconst auto dropdownWidth = st::mediaPlayerVolumeSize.width();\n\tconst auto position = _volume->parentWidget()->mapFromGlobal(\n\t\t_volumeToggle->mapToGlobal(\n\t\t\tQPoint(\n\t\t\t\t(_volumeToggle->width() - dropdownWidth) / 2,\n\t\t\t\theight())));\n\tconst auto playerMargins = _volume->getMargin();\n\tconst auto shift = QPoint(playerMargins.left(), playerMargins.top());\n\t_volume->move(position - shift);\n\n\t_orderController->updateDropdownGeometry();\n\t_speedController->updateDropdownGeometry();\n}\n\nvoid Widget::hideShadowAndDropdowns() {\n\t_shadow->hide();\n\t_playbackSlider->hide();\n\tif (!_volume->isHidden()) {\n\t\t_volumeHidden = true;\n\t\t_volume->hide();\n\t}\n\t_speedController->hideTemporarily();\n\t_orderController->hideTemporarily();\n}\n\nvoid Widget::raiseDropdowns() {\n\t_volume->raise();\n}\n\nWidget::~Widget() = default;\n\nnot_null<Ui::RpWidget*> Widget::rightControls() {\n\treturn _rightControls->entity();\n}\n\nvoid Widget::handleSeekProgress(float64 progress) {\n\tif (!_lastDurationMs) return;\n\n\tconst auto positionMs = std::clamp(\n\t\tstatic_cast<crl::time>(progress * _lastDurationMs),\n\t\tcrl::time(0),\n\t\t_lastDurationMs);\n\tif (_seekPositionMs != positionMs) {\n\t\t_seekPositionMs = positionMs;\n\t\tupdateTimeLabel();\n\n\t\tinstance()->startSeeking(_type);\n\t}\n}\n\nvoid Widget::handleSeekFinished(float64 progress) {\n\tif (!_lastDurationMs) return;\n\n\t_seekPositionMs = -1;\n\n\tinstance()->finishSeeking(_type, progress);\n}\n\nvoid Widget::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n\t_narrow = (width() < st::mediaPlayerWideWidth);\n\tupdateControlsWrapVisibility();\n}\n\nvoid Widget::updateControlsGeometry() {\n\t_close->moveToRight(st::mediaPlayerCloseRight, st::mediaPlayerPlayTop);\n\tauto right = 0;\n\tif (hasPlaybackSpeedControl()) {\n\t\t_speedToggle->moveToRight(right, 0); right += _speedToggle->width();\n\t}\n\tif (_type == AudioMsgId::Type::Song) {\n\t\t_repeatToggle->moveToRight(right, 0); right += _repeatToggle->width();\n\t\t_orderToggle->moveToRight(right, 0); right += _orderToggle->width();\n\t}\n\t_volumeToggle->moveToRight(right, 0); right += _volumeToggle->width();\n\n\tupdateControlsWrapGeometry();\n\n\tupdatePlayPrevNextPositions();\n\n\t_playbackSlider->setGeometry(\n\t\t0,\n\t\theight() - st::mediaPlayerPlayback.fullWidth,\n\t\twidth(),\n\t\tst::mediaPlayerPlayback.fullWidth);\n\n\tupdateDropdownsGeometry();\n}\n\nvoid Widget::updateControlsWrapGeometry() {\n\tconst auto fade = st::mediaPlayerControlsFade.width();\n\tconst auto controls = getTimeRight() + _timeLabel->width() + fade;\n\trightControls()->resize(controls, _repeatToggle->height());\n\t_rightControls->move(\n\t\twidth() - st::mediaPlayerCloseRight - _close->width() - controls,\n\t\tst::mediaPlayerPlayTop);\n}\n\nvoid Widget::updateControlsWrapVisibility() {\n\t_rightControls->toggle(\n\t\t_over || !_narrow,\n\t\tisHidden() ? anim::type::instant : anim::type::normal);\n}\n\nvoid Widget::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tauto fill = e->rect().intersected(\n\t\tQRect(0, 0, width(), st::mediaPlayerHeight + st::lineWidth));\n\tif (!fill.isEmpty()) {\n\t\tp.fillRect(fill, st::mediaPlayerBg);\n\t}\n}\n\nvoid Widget::enterEventHook(QEnterEvent *e) {\n\tmarkOver(true);\n}\n\nvoid Widget::leaveEventHook(QEvent *e) {\n\tmarkOver(false);\n}\n\nvoid Widget::markOver(bool over) {\n\tif (over) {\n\t\t_over = true;\n\t\t_wontBeOver = false;\n\t\tInvokeQueued(this, [=] {\n\t\t\tupdateControlsWrapVisibility();\n\t\t});\n\t} else {\n\t\t_wontBeOver = true;\n\t\tInvokeQueued(this, [=] {\n\t\t\tif (!_wontBeOver) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_wontBeOver = false;\n\t\t\t_over = false;\n\t\t\tupdateControlsWrapVisibility();\n\t\t});\n\t\tupdateOverLabelsState(false);\n\t}\n}\n\nvoid Widget::saveOrder(OrderMode mode) {\n\tCore::App().settings().setPlayerOrderMode(mode);\n\tCore::App().saveSettingsDelayed();\n}\n\nfloat64 Widget::speedLookup(bool lastNonDefault) const {\n\tconst auto &settings = Core::App().settings();\n\treturn (_type == AudioMsgId::Type::Song)\n\t\t? settings.audioPlaybackSpeed(lastNonDefault)\n\t\t: settings.voicePlaybackSpeed(lastNonDefault);\n}\n\nvoid Widget::saveSpeed(float64 speed) {\n\tauto &settings = Core::App().settings();\n\tif (_type == AudioMsgId::Type::Song) {\n\t\tsettings.setAudioPlaybackSpeed(speed);\n\t} else {\n\t\tsettings.setVoicePlaybackSpeed(speed);\n\t}\n\tCore::App().saveSettingsDelayed();\n}\n\nvoid Widget::mouseMoveEvent(QMouseEvent *e) {\n\tupdateOverLabelsState(e->pos());\n}\n\nvoid Widget::mousePressEvent(QMouseEvent *e) {\n\t_labelsDown = _labelsOver;\n}\n\nvoid Widget::mouseReleaseEvent(QMouseEvent *e) {\n\tif (auto downLabels = base::take(_labelsDown)) {\n\t\tif (_labelsOver != downLabels) {\n\t\t\treturn;\n\t\t}\n\t\tif ((_type == AudioMsgId::Type::Voice)\n\t\t\t\t|| _lastSongFromAnotherSession) {\n\t\t\tconst auto current = instance()->current(_type);\n\t\t\tconst auto document = current.audio();\n\t\t\tconst auto context = current.contextId();\n\t\t\tif (document && context && _showItemCallback) {\n\t\t\t\tif (const auto item = document->owner().message(context)) {\n\t\t\t\t\t_showItemCallback(item);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Widget::updateOverLabelsState(QPoint pos) {\n\tconst auto left = getNameLeft();\n\tconst auto right = width()\n\t\t- _rightControls->x()\n\t\t- _rightControls->width()\n\t\t+ getTimeRight();\n\tconst auto labels = myrtlrect(left, 0, width() - right - left, height() - st::mediaPlayerPlayback.fullWidth);\n\tconst auto over = labels.contains(pos);\n\tupdateOverLabelsState(over);\n}\n\nvoid Widget::updateOverLabelsState(bool over) {\n\t_labelsOver = over;\n\tconst auto pressShowsItem = _labelsOver\n\t\t&& ((_type == AudioMsgId::Type::Voice)\n\t\t\t|| _lastSongFromAnotherSession);\n\tsetCursor(pressShowsItem ? style::cur_pointer : style::cur_default);\n\t_togglePlaylistRequests.fire(over && (_type == AudioMsgId::Type::Song));\n}\n\nvoid Widget::updatePlayPrevNextPositions() {\n\tauto left = st::mediaPlayerPlayLeft;\n\tauto top = st::mediaPlayerPlayTop;\n\tif (_previousTrack) {\n\t\t_previousTrack->moveToLeft(left, top); left += _previousTrack->width() + st::mediaPlayerPlaySkip;\n\t\t_playPause->moveToLeft(left, top); left += _playPause->width() + st::mediaPlayerPlaySkip;\n\t\t_nextTrack->moveToLeft(left, top);\n\t} else {\n\t\t_playPause->moveToLeft(left, top);\n\t}\n\tupdateLabelsGeometry();\n}\n\nint Widget::getNameLeft() const {\n\tauto result = st::mediaPlayerPlayLeft + _playPause->width();\n\tif (_previousTrack) {\n\t\tresult += _previousTrack->width() + st::mediaPlayerPlaySkip + _nextTrack->width() + st::mediaPlayerPlaySkip;\n\t}\n\tresult += st::mediaPlayerPadding;\n\treturn result;\n}\n\nint Widget::getNameRight() const {\n\treturn st::mediaPlayerCloseRight\n\t\t+ _close->width()\n\t\t+ st::mediaPlayerPadding;\n}\n\nint Widget::getTimeRight() const {\n\tauto result = 0;\n\tresult += _volumeToggle->width();\n\tif (_type == AudioMsgId::Type::Song) {\n\t\tresult += _repeatToggle->width()\n\t\t\t+ _orderToggle->width();\n\t}\n\tif (hasPlaybackSpeedControl()) {\n\t\tresult += _speedToggle->width();\n\t}\n\tresult += st::mediaPlayerPadding;\n\treturn result;\n}\n\nvoid Widget::updateLabelsGeometry() {\n\tconst auto left = getNameLeft();\n\tconst auto widthForName = width()\n\t\t- left\n\t\t- getNameRight();\n\t_nameLabel->resizeToNaturalWidth(widthForName);\n\t_nameLabel->moveToLeft(left, st::mediaPlayerNameTop - st::mediaPlayerName.style.font->ascent);\n\n\tconst auto right = getTimeRight();\n\t_timeLabel->moveToRight(right, st::mediaPlayerNameTop - st::mediaPlayerTime.font->ascent);\n\n\tupdateControlsWrapGeometry();\n}\n\nvoid Widget::updateRepeatToggleIcon() {\n\tswitch (Core::App().settings().playerRepeatMode()) {\n\tcase RepeatMode::None:\n\t\t_repeatToggle->setIconOverride(\n\t\t\t&st::mediaPlayerRepeatDisabledIcon,\n\t\t\t&st::mediaPlayerRepeatDisabledIconOver);\n\t\t_repeatToggle->setRippleColorOverride(\n\t\t\t&st::mediaPlayerRepeatDisabledRippleBg);\n\t\tbreak;\n\tcase RepeatMode::One:\n\t\t_repeatToggle->setIconOverride(&st::mediaPlayerRepeatOneIcon);\n\t\t_repeatToggle->setRippleColorOverride(nullptr);\n\t\tbreak;\n\tcase RepeatMode::All:\n\t\t_repeatToggle->setIconOverride(nullptr);\n\t\t_repeatToggle->setRippleColorOverride(nullptr);\n\t\tbreak;\n\t}\n}\n\nvoid Widget::checkForTypeChange() {\n\tauto hasActiveType = [](AudioMsgId::Type type) {\n\t\tconst auto current = instance()->current(type);\n\t\tconst auto state = instance()->getState(type);\n\t\treturn (current == state.id && !IsStoppedOrStopping(state.state));\n\t};\n\tif (hasActiveType(AudioMsgId::Type::Voice)) {\n\t\t_voiceIsActive = true;\n\t\tsetType(AudioMsgId::Type::Voice);\n\t} else if (!_voiceIsActive && hasActiveType(AudioMsgId::Type::Song)) {\n\t\tsetType(AudioMsgId::Type::Song);\n\t}\n}\n\nbool Widget::hasPlaybackSpeedControl() const {\n\treturn _lastSongId.changeablePlaybackSpeed()\n\t\t&& Media::Audio::SupportsSpeedControl();\n}\n\nvoid Widget::updateControlsVisibility() {\n\t_repeatToggle->setVisible(_type == AudioMsgId::Type::Song);\n\t_orderToggle->setVisible(_type == AudioMsgId::Type::Song);\n\t_speedToggle->setVisible(hasPlaybackSpeedControl());\n\tif (!_shadow->isHidden()) {\n\t\t_playbackSlider->setVisible(_type == AudioMsgId::Type::Song);\n\t}\n\tupdateControlsGeometry();\n}\n\nvoid Widget::setType(AudioMsgId::Type type) {\n\tif (_type != type) {\n\t\t_type = type;\n\t\thandleSongChange();\n\t\tupdateControlsVisibility();\n\t\tupdateLabelsGeometry();\n\t\thandleSongUpdate(instance()->getState(_type));\n\t\tupdateOverLabelsState(_labelsOver);\n\t\t_playlistChangesLifetime = instance()->playlistChanges(\n\t\t\t_type\n\t\t) | rpl::on_next([=] {\n\t\t\thandlePlaylistUpdate();\n\t\t});\n\t\t// maybe the type change causes a change of the button layout\n\t\tQResizeEvent event = { size(), size() };\n\t\tresizeEvent(&event);\n\t}\n}\n\nvoid Widget::handleSongUpdate(const TrackState &state) {\n\tcheckForTypeChange();\n\tif (state.id.type() != _type || !state.id.audio()) {\n\t\treturn;\n\t}\n\n\tif (state.id.audio()->loading()) {\n\t\t_playbackProgress->updateLoadingState(state.id.audio()->progress());\n\t} else {\n\t\t_playbackProgress->updateState(state);\n\t}\n\n\tauto showPause = ShowPauseIcon(state.state);\n\tif (instance()->isSeeking(_type)) {\n\t\tshowPause = true;\n\t}\n\t_playPause->setIconOverride(state.id.audio()->loading()\n\t\t? &st::mediaPlayerCancelIcon\n\t\t: showPause\n\t\t? &st::mediaPlayerPauseIcon\n\t\t: nullptr);\n\t_playPause->setAccessibleName(showPause\n\t\t? tr::lng_shortcuts_media_pause(tr::now)\n\t\t: tr::lng_shortcuts_media_play(tr::now));\n\n\tupdateTimeText(state);\n}\n\nvoid Widget::updateTimeText(const TrackState &state) {\n\tqint64 display = 0;\n\tconst auto frequency = state.frequency;\n\tconst auto document = state.id.audio();\n\tif (!IsStoppedOrStopping(state.state)) {\n\t\tdisplay = state.position;\n\t} else if (state.length) {\n\t\tdisplay = state.length;\n\t} else if (document->song()) {\n\t\tdisplay = (document->duration() * frequency) / 1000;\n\t}\n\n\t_lastDurationMs = (state.length * 1000LL) / frequency;\n\n\tif (document->loading()) {\n\t\t_time = QString::number(qRound(document->progress() * 100)) + '%';\n\t\t_playbackSlider->setDisabled(true);\n\t} else {\n\t\tdisplay = display / frequency;\n\t\t_time = Ui::FormatDurationText(display);\n\t\t_playbackSlider->setDisabled(false);\n\t}\n\tif (_seekPositionMs < 0) {\n\t\tupdateTimeLabel();\n\t}\n}\n\nvoid Widget::updateTimeLabel() {\n\tauto timeLabelWidth = _timeLabel->width();\n\tif (_seekPositionMs >= 0) {\n\t\tauto playAlready = _seekPositionMs / 1000LL;\n\t\t_timeLabel->setText(Ui::FormatDurationText(playAlready));\n\t} else {\n\t\t_timeLabel->setText(_time);\n\t}\n\tif (timeLabelWidth != _timeLabel->width()) {\n\t\tupdateLabelsGeometry();\n\t}\n}\n\nvoid Widget::handleSongChange() {\n\tconst auto current = instance()->current(_type);\n\tconst auto document = current.audio();\n\t_lastSongFromAnotherSession = document\n\t\t&& (document->session().uniqueId()\n\t\t\t!= _controller->session().uniqueId());\n\tif (!current\n\t\t|| !document\n\t\t|| ((_lastSongId.audio() == document)\n\t\t\t&& (_lastSongId.contextId() == current.contextId()))) {\n\t\treturn;\n\t}\n\t_lastSongId = current;\n\t_speedController->reloadFromLookup();\n\n\tauto textWithEntities = TextWithEntities();\n\tif (document->isVoiceMessage() || document->isVideoMessage()) {\n\t\ttextWithEntities = Ui::Text::FormatVoiceName(\n\t\t\tdocument,\n\t\t\tcurrent.contextId()).textWithEntities(true);\n\t} else {\n\t\ttextWithEntities = Ui::Text::FormatSongNameFor(document)\n\t\t\t.textWithEntities(true);\n\t}\n\t_nameLabel->setMarkedText(textWithEntities);\n\thandlePlaylistUpdate();\n\tupdateLabelsGeometry();\n}\n\nvoid Widget::handlePlaylistUpdate() {\n\tconst auto previousEnabled = instance()->previousAvailable(_type);\n\tconst auto nextEnabled = instance()->nextAvailable(_type);\n\tif (!previousEnabled && !nextEnabled) {\n\t\tdestroyPrevNextButtons();\n\t} else {\n\t\tcreatePrevNextButtons();\n\t\t_previousTrack->setIconOverride(previousEnabled ? nullptr : &st::mediaPlayerPreviousDisabledIcon);\n\t\t_previousTrack->setRippleColorOverride(previousEnabled ? nullptr : &st::mediaPlayerBg);\n\t\t_previousTrack->setPointerCursor(previousEnabled);\n\t\t_nextTrack->setIconOverride(nextEnabled ? nullptr : &st::mediaPlayerNextDisabledIcon);\n\t\t_nextTrack->setRippleColorOverride(nextEnabled ? nullptr : &st::mediaPlayerBg);\n\t\t_nextTrack->setPointerCursor(nextEnabled);\n\t}\n}\n\nvoid Widget::createPrevNextButtons() {\n\tif (!_previousTrack) {\n\t\t_previousTrack.create(this, st::mediaPlayerPreviousButton);\n\t\t_previousTrack->show();\n\t\t_previousTrack->setClickedCallback([=]() {\n\t\t\tinstance()->previous(_type);\n\t\t});\n\t\t_previousTrack->setAccessibleName(tr::lng_shortcuts_media_previous(tr::now));\n\t\t_nextTrack.create(this, st::mediaPlayerNextButton);\n\t\t_nextTrack->show();\n\t\t_nextTrack->setClickedCallback([=]() {\n\t\t\tinstance()->next(_type);\n\t\t});\n\t\t_nextTrack->setAccessibleName(tr::lng_shortcuts_media_next(tr::now));\n\t\thidePlaylistOn(_previousTrack);\n\t\thidePlaylistOn(_nextTrack);\n\t\tupdatePlayPrevNextPositions();\n\t}\n}\n\nvoid Widget::destroyPrevNextButtons() {\n\tif (_previousTrack) {\n\t\t_previousTrack.destroy();\n\t\t_nextTrack.destroy();\n\t\tupdatePlayPrevNextPositions();\n\t}\n}\n\n} // namespace Player\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/player/media_player_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_audio_msg_id.h\"\n#include \"ui/rp_widget.h\"\n#include \"base/object_ptr.h\"\n\nclass AudioMsgId;\n\nnamespace Ui {\nclass FlatLabel;\nclass LabelSimple;\nclass IconButton;\nclass PlainShadow;\nclass FilledSlider;\ntemplate <typename Widget>\nclass FadeWrap;\n} // namespace Ui\n\nnamespace Media {\nenum class OrderMode;\n} // namespace Media\n\nnamespace Media::View {\nclass PlaybackProgress;\n} // namespace Media::View\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Media::Player {\n\nclass Dropdown;\nclass SpeedButton;\nclass OrderController;\nclass SpeedController;\nstruct TrackState;\n\nclass Widget final : public Ui::RpWidget {\npublic:\n\tWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Ui::RpWidget*> dropdownsParent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~Widget();\n\n\tvoid setCloseCallback(Fn<void()> callback);\n\tvoid setShowItemCallback(Fn<void(not_null<const HistoryItem*>)> callback);\n\tvoid stopAndClose();\n\tvoid setShadowGeometryToLeft(int x, int y, int w, int h);\n\tvoid hideShadowAndDropdowns();\n\tvoid showShadowAndDropdowns();\n\tvoid updateDropdownsGeometry();\n\tvoid raiseDropdowns();\n\n\t[[nodiscard]] rpl::producer<bool> togglePlaylistRequests() const {\n\t\treturn _togglePlaylistRequests.events();\n\t}\n\nprivate:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\n\t[[nodiscard]] not_null<Ui::RpWidget*> rightControls();\n\tvoid setupRightControls();\n\n\tvoid handleSeekProgress(float64 progress);\n\tvoid handleSeekFinished(float64 progress);\n\n\t[[nodiscard]] int getNameLeft() const;\n\t[[nodiscard]] int getNameRight() const;\n\t[[nodiscard]] int getTimeRight() const;\n\tvoid updateOverLabelsState(QPoint pos);\n\tvoid updateOverLabelsState(bool over);\n\tvoid hidePlaylistOn(not_null<Ui::RpWidget*> widget);\n\n\tvoid updatePlayPrevNextPositions();\n\tvoid updateLabelsGeometry();\n\tvoid updateRepeatToggleIcon();\n\tvoid updateControlsVisibility();\n\tvoid updateControlsGeometry();\n\tvoid updateControlsWrapGeometry();\n\tvoid updateControlsWrapVisibility();\n\tvoid createPrevNextButtons();\n\tvoid destroyPrevNextButtons();\n\n\tbool hasPlaybackSpeedControl() const;\n\tvoid updateVolumeToggleIcon();\n\n\tvoid checkForTypeChange();\n\tvoid setType(AudioMsgId::Type type);\n\tvoid handleSongUpdate(const TrackState &state);\n\tvoid handleSongChange();\n\tvoid handlePlaylistUpdate();\n\n\tvoid updateTimeText(const TrackState &state);\n\tvoid updateTimeLabel();\n\tvoid markOver(bool over);\n\n\tvoid saveOrder(OrderMode mode);\n\t[[nodiscard]] float64 speedLookup(bool lastNonDefault) const;\n\tvoid saveSpeed(float64 speed);\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<Ui::RpWidget*> _orderMenuParent;\n\n\tcrl::time _seekPositionMs = -1;\n\tcrl::time _lastDurationMs = 0;\n\tQString _time;\n\n\t// We display all the controls according to _type.\n\t// We switch to Type::Voice if a voice/video message is played.\n\t// We switch to Type::Song only if _voiceIsActive == false.\n\t// We change _voiceIsActive to false only manually or from tracksFinished().\n\tAudioMsgId::Type _type = AudioMsgId::Type::Unknown;\n\tAudioMsgId _lastSongId;\n\tbool _lastSongFromAnotherSession = false;\n\tbool _voiceIsActive = false;\n\tFn<void()> _closeCallback;\n\tFn<void(not_null<const HistoryItem*>)> _showItemCallback;\n\n\tbool _labelsOver = false;\n\tbool _labelsDown = false;\n\trpl::event_stream<bool> _togglePlaylistRequests;\n\tbool _narrow = false;\n\tbool _over = false;\n\tbool _wontBeOver = false;\n\tbool _volumeHidden = false;\n\n\tobject_ptr<Ui::FlatLabel> _nameLabel;\n\tobject_ptr<Ui::FadeWrap<Ui::RpWidget>> _rightControls;\n\tobject_ptr<Ui::LabelSimple> _timeLabel;\n\tobject_ptr<Ui::IconButton> _previousTrack = { nullptr };\n\tobject_ptr<Ui::IconButton> _playPause;\n\tobject_ptr<Ui::IconButton> _nextTrack = { nullptr };\n\tobject_ptr<Ui::IconButton> _volumeToggle;\n\tobject_ptr<Ui::IconButton> _repeatToggle;\n\tobject_ptr<Ui::IconButton> _orderToggle;\n\tobject_ptr<SpeedButton> _speedToggle;\n\tobject_ptr<Ui::IconButton> _close;\n\tobject_ptr<Ui::PlainShadow> _shadow = { nullptr };\n\tobject_ptr<Ui::FilledSlider> _playbackSlider;\n\tbase::unique_qptr<Dropdown> _volume;\n\tstd::unique_ptr<View::PlaybackProgress> _playbackProgress;\n\tstd::unique_ptr<OrderController> _orderController;\n\tstd::unique_ptr<SpeedController> _speedController;\n\n\trpl::lifetime _playlistChangesLifetime;\n\n};\n\n} // namespace Media::Player\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\nusing \"ui/widgets/widgets.style\";\nusing \"boxes/boxes.style\";\n\nStealthBoxStyle {\n\tbox: Box;\n\tbuttonLabel: FlatLabel;\n\tlockIcon: icon;\n\tboxClose: IconButton;\n\tabout: FlatLabel;\n\tfeatureTitle: FlatLabel;\n\tfeatureAbout: FlatLabel;\n\tfeaturePastIcon: icon;\n\tfeatureNextIcon: icon;\n\tlogoIcon: icon;\n\tlogoBg: color;\n}\n\nstoriesStealthStyleDefault: StealthBoxStyle {\n\tbox: Box(defaultBox) {\n\t\tbuttonPadding: margins(10px, 10px, 10px, 10px);\n\t\tbuttonHeight: 42px;\n\t\tbutton: RoundButton(defaultActiveButton) {\n\t\t\theight: 42px;\n\t\t\ttextTop: 12px;\n\t\t\tstyle: semiboldTextStyle;\n\t\t}\n\t\tmargin: margins(0px, 56px, 0px, 10px);\n\t\tbg: windowBg;\n\t\ttitle: FlatLabel(boxTitle) {\n\t\t\talign: align(top);\n\t\t}\n\t\ttitleAdditionalFg: windowSubTextFg;\n\t}\n\tbuttonLabel: FlatLabel(defaultFlatLabel) {\n\t\ttextFg: activeButtonFg;\n\t\tstyle: semiboldTextStyle;\n\t\talign: align(top);\n\t\tminWidth: 20px;\n\t\tmaxHeight: 20px;\n\t}\n\tlockIcon: icon {{ \"dialogs/dialogs_lock_on\", windowFgActive }};\n\tboxClose: IconButton(defaultIconButton) {\n\t\twidth: boxTitleHeight;\n\t\theight: boxTitleHeight;\n\t\ticon: icon {{ \"box_button_close\", boxTitleCloseFg }};\n\t\ticonOver: icon {{ \"box_button_close\", boxTitleCloseFgOver }};\n\t\trippleAreaPosition: point(4px, 4px);\n\t\trippleAreaSize: 40px;\n\t\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\t\tcolor: windowBgOver;\n\t\t}\n\t}\n\tabout: FlatLabel(defaultFlatLabel) {\n\t\ttextFg: windowSubTextFg;\n\t\talign: align(top);\n\t\tminWidth: 20px;\n\t}\n\tfeatureTitle: FlatLabel(defaultFlatLabel) {\n\t\ttextFg: windowBoldFg;\n\t\tstyle: semiboldTextStyle;\n\t\tminWidth: 10px;\n\t\tmaxHeight: 20px;\n\t}\n\tfeatureAbout: FlatLabel(defaultFlatLabel) {\n\t\ttextFg: windowSubTextFg;\n\t\tminWidth: 20px;\n\t}\n\tfeaturePastIcon: icon {{ \"stories/stealth_5m\", windowActiveTextFg }};\n\tfeatureNextIcon: icon {{ \"stories/stealth_25m\", windowActiveTextFg }};\n\tlogoIcon: icon {{ \"stories/stealth_logo\", windowFgActive }};\n\tlogoBg: windowBgActive;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/stories/media_stories_caption_full_view.h\"\n\n#include \"base/event_filter.h\"\n#include \"core/ui_integration.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"media/stories/media_stories_controller.h\"\n#include \"media/stories/media_stories_view.h\"\n#include \"media/view/media_view_open_common.h\"\n#include \"ui/widgets/elastic_scroll.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/click_handler.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Media::Stories {\n\nCaptionFullView::CaptionFullView(not_null<Controller*> controller)\n: _controller(controller)\n, _scroll(std::make_unique<Ui::ElasticScroll>(controller->wrap()))\n, _wrap(_scroll->setOwnedWidget(\n\tobject_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\t_scroll.get(),\n\t\tobject_ptr<Ui::FlatLabel>(_scroll.get(), st::storiesCaptionFull),\n\t\tst::mediaviewCaptionPadding + _controller->repostCaptionPadding())))\n, _text(_wrap->entity()) {\n\tusing namespace Media::View;\n\tconst auto text = StripQuoteEntities(controller->captionText());\n\t_text->setMarkedText(text, Core::TextContext({\n\t\t.session = &controller->uiShow()->session(),\n\t}));\n\n\tstartAnimation();\n\t_controller->layoutValue(\n\t) | rpl::on_next([=](const Layout &layout) {\n\t\tif (_outer != layout.content) {\n\t\t\tconst auto skip = layout.header.y()\n\t\t\t\t+ layout.header.height()\n\t\t\t\t- layout.content.y();\n\t\t\t_outer = layout.content.marginsRemoved({ 0, skip, 0, 0 });\n\t\t\tupdateGeometry();\n\t\t}\n\t}, _scroll->lifetime());\n\n\tconst auto filter = [=](not_null<QEvent*> e) {\n\t\tconst auto mouse = [&] {\n\t\t\treturn static_cast<QMouseEvent*>(e.get());\n\t\t};\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::MouseButtonPress\n\t\t\t&& mouse()->button() == Qt::LeftButton\n\t\t\t&& !ClickHandler::getActive()) {\n\t\t\t_down = true;\n\t\t} else if (type == QEvent::MouseButtonRelease && _down) {\n\t\t\t_down = false;\n\t\t\tif (!ClickHandler::getPressed()) {\n\t\t\t\tclose();\n\t\t\t}\n\t\t} else if (type == QEvent::KeyPress\n\t\t\t&& static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Escape) {\n\t\t\tclose();\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t};\n\tbase::install_event_filter(_text.get(), filter);\n\tif (_controller->repost()) {\n\t\t_wrap->setMouseTracking(true);\n\t\tbase::install_event_filter(_wrap.get(), [=](not_null<QEvent*> e) {\n\t\t\tconst auto mouse = [&] {\n\t\t\t\treturn static_cast<QMouseEvent*>(e.get());\n\t\t\t};\n\t\t\tconst auto type = e->type();\n\t\t\tif (type == QEvent::MouseMove) {\n\t\t\t\tconst auto handler = _controller->lookupRepostHandler(\n\t\t\t\t\tmouse()->pos() - QPoint(\n\t\t\t\t\t\tst::mediaviewCaptionPadding.left(),\n\t\t\t\t\t\t(_wrap->padding().top()\n\t\t\t\t\t\t\t- _controller->repostCaptionPadding().top())));\n\t\t\t\tClickHandler::setActive(handler.link, handler.host);\n\t\t\t\t_wrap->setCursor(handler.link\n\t\t\t\t\t? style::cur_pointer\n\t\t\t\t\t: style::cur_default);\n\t\t\t} else if (type == QEvent::MouseButtonPress\n\t\t\t\t&& mouse()->button() == Qt::LeftButton\n\t\t\t\t&& ClickHandler::getActive()) {\n\t\t\t\tClickHandler::pressed();\n\t\t\t} else if (type == QEvent::MouseButtonRelease) {\n\t\t\t\tif (const auto activated = ClickHandler::unpressed()) {\n\t\t\t\t\tActivateClickHandler(_wrap.get(), activated, {\n\t\t\t\t\t\tmouse()->button(), QVariant(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t});\n\t}\n\tbase::install_event_filter(_wrap.get(), filter);\n\n\tusing Type = Ui::ElasticScroll::OverscrollType;\n\n\trpl::combine(\n\t\t_scroll->positionValue(),\n\t\t_scroll->movementValue()\n\t) | rpl::filter([=] {\n\t\treturn !_closing;\n\t}) | rpl::on_next([=](\n\t\t\tUi::ElasticScrollPosition position,\n\t\t\tUi::ElasticScrollMovement movement) {\n\t\tconst auto overscrollTop = std::max(-position.overscroll, 0);\n\t\tusing Phase = Ui::ElasticScrollMovement;\n\t\tif (movement == Phase::Progress) {\n\t\t\tif (overscrollTop > 0) {\n\t\t\t\t_pulling = true;\n\t\t\t} else {\n\t\t\t\t_pulling = false;\n\t\t\t}\n\t\t} else if (_pulling\n\t\t\t&& (movement == Phase::Momentum\n\t\t\t\t|| movement == Phase::Returning)) {\n\t\t\t_pulling = false;\n\t\t\tif (overscrollTop > st::storiesCaptionPullThreshold) {\n\t\t\t\t_closingTopAdded = overscrollTop;\n\t\t\t\t_scroll->setOverscrollTypes(Type::None, Type::Real);\n\t\t\t\tclose();\n\t\t\t\tupdateGeometry();\n\t\t\t}\n\t\t}\n\t}, _scroll->lifetime());\n\n\t_wrap->paintRequest() | rpl::on_next([=] {\n\t\tif (_controller->repost()) {\n\t\t\tauto p = Painter(_wrap.get());\n\t\t\t_controller->drawRepostInfo(\n\t\t\t\tp,\n\t\t\t\tst::mediaviewCaptionPadding.left(),\n\t\t\t\t(_wrap->padding().top()\n\t\t\t\t\t- _controller->repostCaptionPadding().top()),\n\t\t\t\t_wrap->width());\n\t\t}\n\t}, _wrap->lifetime());\n\n\t_scroll->show();\n\t_scroll->setOverscrollBg(QColor(0, 0, 0, 0));\n\t_scroll->setOverscrollTypes(Type::Real, Type::Real);\n\t_text->show();\n\t_text->setFocus();\n}\n\nCaptionFullView::~CaptionFullView() = default;\n\nbool CaptionFullView::closing() const {\n\treturn _closing;\n}\n\nbool CaptionFullView::focused() const {\n\treturn Ui::InFocusChain(_scroll.get());\n}\n\nvoid CaptionFullView::close() {\n\tif (_closing) {\n\t\treturn;\n\t}\n\t_closing = true;\n\t_controller->captionClosing();\n\tstartAnimation();\n}\n\nvoid CaptionFullView::repaint() {\n\t_wrap->update();\n}\n\nvoid CaptionFullView::updateGeometry() {\n\tif (_outer.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto lineHeight = st::mediaviewCaptionStyle.font->height;\n\tconst auto padding = st::mediaviewCaptionPadding\n\t\t+ _controller->repostCaptionPadding();\n\t_text->resizeToWidth(_outer.width() - padding.left() - padding.right());\n\tconst auto add = padding.top() + padding.bottom();\n\tconst auto maxShownHeight = lineHeight * kMaxShownCaptionLines;\n\tconst auto shownHeight = (_text->height() > maxShownHeight)\n\t\t? (lineHeight * kCollapsedCaptionLines)\n\t\t: _text->height();\n\tconst auto collapsedHeight = shownHeight + add;\n\tconst auto addedToBottom = lineHeight;\n\tconst auto expandedHeight = _text->height() + add + addedToBottom;\n\tconst auto fullHeight = std::min(expandedHeight, _outer.height());\n\tconst auto shown = _animation.value(_closing ? 0. : 1.);\n\tconst auto height = (_closing || _animation.animating())\n\t\t? anim::interpolate(collapsedHeight, fullHeight, shown)\n\t\t: _outer.height();\n\tconst auto added = anim::interpolate(0, _closingTopAdded, shown);\n\tconst auto bottomPadding = anim::interpolate(0, addedToBottom, shown);\n\tconst auto use = padding + ((_closing || _animation.animating())\n\t\t? QMargins(0, 0, 0, bottomPadding)\n\t\t: QMargins(0, height - fullHeight, 0, bottomPadding));\n\t_wrap->setPadding(use);\n\t_scroll->setGeometry(\n\t\t_outer.x(),\n\t\tadded + _outer.y() + _outer.height() - height,\n\t\t_outer.width(),\n\t\tstd::max(height - added, 0));\n\tif (_closing && !_animation.animating()) {\n\t\t_controller->captionClosed();\n\t}\n}\n\nvoid CaptionFullView::startAnimation() {\n\t_animation.start(\n\t\t[=] { updateGeometry(); },\n\t\t_closing ? 1. : 0.,\n\t\t_closing ? 0. : 1.,\n\t\tst::fadeWrapDuration,\n\t\tanim::sineInOut);\n}\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_caption_full_view.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass FlatLabel;\nclass ElasticScroll;\ntemplate <typename Widget>\nclass PaddingWrap;\n} // namespace Ui\n\nnamespace Media::Stories {\n\nclass Controller;\n\nclass CaptionFullView final {\npublic:\n\texplicit CaptionFullView(not_null<Controller*> controller);\n\t~CaptionFullView();\n\n\tvoid close();\n\tvoid repaint();\n\t[[nodiscard]] bool closing() const;\n\t[[nodiscard]] bool focused() const;\n\nprivate:\n\tvoid updateGeometry();\n\tvoid startAnimation();\n\n\tconst not_null<Controller*> _controller;\n\tconst std::unique_ptr<Ui::ElasticScroll> _scroll;\n\tconst not_null<Ui::PaddingWrap<Ui::FlatLabel>*> _wrap;\n\tconst not_null<Ui::FlatLabel*> _text;\n\tUi::Animations::Simple _animation;\n\tQRect _outer;\n\tint _closingTopAdded = 0;\n\tbool _pulling = false;\n\tbool _closing = false;\n\tbool _down = false;\n\n};\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/stories/media_stories_controller.h\"\n\n#include \"base/platform/base_platform_info.h\"\n#include \"base/power_save_blocker.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/peers/prepare_short_info_box.h\"\n#include \"boxes/report_messages_box.h\"\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_messages.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/core_settings.h\"\n#include \"core/local_url_handlers.h\"\n#include \"core/update_checker.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_document.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"history/view/controls/compose_controls_common.h\"\n#include \"history/view/reactions/history_view_reactions_strip.h\"\n#include \"history/view/history_view_paid_reaction_toast.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"media/stories/media_stories_caption_full_view.h\"\n#include \"media/stories/media_stories_delegate.h\"\n#include \"media/stories/media_stories_header.h\"\n#include \"media/stories/media_stories_sibling.h\"\n#include \"media/stories/media_stories_slider.h\"\n#include \"media/stories/media_stories_reactions.h\"\n#include \"media/stories/media_stories_recent_views.h\"\n#include \"media/stories/media_stories_reply.h\"\n#include \"media/stories/media_stories_repost_view.h\"\n#include \"media/stories/media_stories_share.h\"\n#include \"media/stories/media_stories_stealth.h\"\n#include \"media/stories/media_stories_view.h\"\n#include \"media/audio/media_audio.h\"\n#include \"info/stories/info_stories_common.h\"\n#include \"payments/payments_reaction_process.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/report_box_graphics.h\"\n#include \"ui/controls/send_button.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/round_rect.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat_helpers.h\" // defaultReportBox\n#include \"styles/style_media_view.h\"\n#include \"styles/style_boxes.h\" // UserpicButton\n\n#include <QtGui/QWindow>\n\nnamespace Media::Stories {\nnamespace {\n\nconstexpr auto kPhotoProgressInterval = crl::time(100);\nconstexpr auto kPhotoDuration = 5 * crl::time(1000);\nconstexpr auto kFullContentFade = 0.6;\nconstexpr auto kSiblingMultiplierDefault = 0.448;\nconstexpr auto kSiblingMultiplierMax = 0.72;\nconstexpr auto kSiblingOutsidePart = 0.24;\nconstexpr auto kSiblingUserpicSize = 0.3;\nconstexpr auto kInnerHeightMultiplier = 1.6;\nconstexpr auto kPreloadPeersCount = 3;\nconstexpr auto kPreloadStoriesCount = 5;\nconstexpr auto kPreloadNextMediaCount = 3;\nconstexpr auto kPreloadPreviousMediaCount = 1;\nconstexpr auto kMarkAsReadAfterSeconds = 0.2;\nconstexpr auto kMarkAsReadAfterProgress = 0.;\n\nstruct SameDayRange {\n\tint from = 0;\n\tint till = 0;\n};\n[[nodiscard]] SameDayRange ComputeSameDayRange(\n\t\tnot_null<Data::Story*> story,\n\t\tconst Data::StoriesIds &ids,\n\t\tconst std::vector<StoryId> &sorted,\n\t\tint index) {\n\tExpects(index >= 0 && index < ids.list.size());\n\tExpects(index >= 0 && index < sorted.size());\n\n\tconst auto pinned = int(ids.pinnedToTop.size());\n\tif (index < pinned) {\n\t\treturn SameDayRange{ .from = 0, .till = pinned - 1 };\n\t}\n\n\tauto result = SameDayRange{ .from = index, .till = index };\n\tconst auto peerId = story->peer()->id;\n\tconst auto stories = &story->owner().stories();\n\tconst auto now = base::unixtime::parse(story->date());\n\tfor (auto i = index; i != 0;) {\n\t\tconst auto storyId = sorted[--i];\n\t\tif (const auto maybeStory = stories->lookup({ peerId, storyId })) {\n\t\t\tconst auto day = base::unixtime::parse((*maybeStory)->date());\n\t\t\tif (day.date() != now.date()) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t--result.from;\n\t}\n\tfor (auto i = index + 1, c = int(sorted.size()); i != c; ++i) {\n\t\tconst auto storyId = sorted[i];\n\t\tif (const auto maybeStory = stories->lookup({ peerId, storyId })) {\n\t\t\tconst auto day = base::unixtime::parse((*maybeStory)->date());\n\t\t\tif (day.date() != now.date()) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t++result.till;\n\t}\n\treturn result;\n}\n\n[[nodiscard]] QPoint Rotated(QPoint point, QPoint origin, float64 angle) {\n\tif (std::abs(angle) < 1.) {\n\t\treturn point;\n\t}\n\tconst auto alpha = angle / 180. * M_PI;\n\tconst auto acos = cos(alpha);\n\tconst auto asin = sin(alpha);\n\tpoint -= origin;\n\treturn origin + QPoint(\n\t\tint(base::SafeRound(acos * point.x() - asin * point.y())),\n\t\tint(base::SafeRound(asin * point.x() + acos * point.y())));\n}\n\n[[nodiscard]] bool ResolveWeatherInCelsius() {\n\tconst auto saved = Core::App().settings().weatherInCelsius();\n\treturn saved.value_or(!ranges::contains(\n\t\tstd::array{ u\"US\"_q, u\"BS\"_q, u\"KY\"_q, u\"LR\"_q, u\"BZ\"_q },\n\t\tPlatform::SystemCountry().toUpper()));\n}\n\n} // namespace\n\nclass Controller::PhotoPlayback final {\npublic:\n\texplicit PhotoPlayback(not_null<Controller*> controller);\n\n\t[[nodiscard]] bool paused() const;\n\tvoid togglePaused(bool paused);\n\nprivate:\n\tvoid callback();\n\n\tconst not_null<Controller*> _controller;\n\n\tbase::Timer _timer;\n\tcrl::time _started = 0;\n\tcrl::time _paused = 0;\n\n};\n\nclass Controller::Unsupported final {\npublic:\n\tUnsupported(not_null<Controller*> controller, not_null<PeerData*> peer);\n\nprivate:\n\tvoid setup(not_null<PeerData*> peer);\n\n\tconst not_null<Controller*> _controller;\n\tstd::unique_ptr<Ui::RpWidget> _bg;\n\tstd::unique_ptr<Ui::FlatLabel> _text;\n\tstd::unique_ptr<Ui::RoundButton> _button;\n\tUi::RoundRect _bgRound;\n\n};\n\nController::PhotoPlayback::PhotoPlayback(not_null<Controller*> controller)\n: _controller(controller)\n, _timer([=] { callback(); })\n, _started(crl::now())\n, _paused(_started) {\n}\n\nbool Controller::PhotoPlayback::paused() const {\n\treturn _paused != 0;\n}\n\nvoid Controller::PhotoPlayback::togglePaused(bool paused) {\n\tif (!_paused == !paused) {\n\t\treturn;\n\t} else if (paused) {\n\t\tconst auto now = crl::now();\n\t\tif (now - _started >= kPhotoDuration) {\n\t\t\treturn;\n\t\t}\n\t\t_paused = now;\n\t\t_timer.cancel();\n\t} else {\n\t\t_started += crl::now() - _paused;\n\t\t_paused = 0;\n\t\t_timer.callEach(kPhotoProgressInterval);\n\t}\n\tcallback();\n}\n\nvoid Controller::PhotoPlayback::callback() {\n\tconst auto now = crl::now();\n\tconst auto elapsed = now - _started;\n\tconst auto finished = (now - _started >= kPhotoDuration);\n\tif (finished) {\n\t\t_timer.cancel();\n\t}\n\tusing State = Player::State;\n\tconst auto state = finished\n\t\t? State::StoppedAtEnd\n\t\t: _paused\n\t\t? State::Paused\n\t\t: State::Playing;\n\t_controller->updatePhotoPlayback({\n\t\t.state = state,\n\t\t.position = elapsed,\n\t\t.receivedTill = kPhotoDuration,\n\t\t.length = kPhotoDuration,\n\t\t.frequency = 1000,\n\t});\n}\n\nController::Unsupported::Unsupported(\n\tnot_null<Controller*> controller,\n\tnot_null<PeerData*> peer)\n: _controller(controller)\n, _bgRound(st::storiesRadius, st::storiesComposeBg) {\n\tsetup(peer);\n}\n\nvoid Controller::Unsupported::setup(not_null<PeerData*> peer) {\n\tconst auto wrap = _controller->wrap();\n\n\t_bg = std::make_unique<Ui::RpWidget>(wrap);\n\t_bg->show();\n\t_bg->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(_bg.get());\n\t\t_bgRound.paint(p, _bg->rect());\n\t}, _bg->lifetime());\n\n\t_controller->layoutValue(\n\t) | rpl::on_next([=](const Layout &layout) {\n\t\t_bg->setGeometry(layout.content);\n\t}, _bg->lifetime());\n\n\t_text = std::make_unique<Ui::FlatLabel>(\n\t\twrap,\n\t\ttr::lng_stories_unsupported(),\n\t\tst::storiesUnsupportedLabel);\n\t_text->show();\n\n\t_button = std::make_unique<Ui::RoundButton>(\n\t\twrap,\n\t\ttr::lng_update_telegram(),\n\t\tst::storiesUnsupportedUpdate);\n\t_button->show();\n\n\trpl::combine(\n\t\t_controller->layoutValue(),\n\t\t_text->sizeValue(),\n\t\t_button->sizeValue()\n\t) | rpl::on_next([=](\n\t\t\tconst Layout &layout,\n\t\t\tQSize text,\n\t\t\tQSize button) {\n\t\tconst auto wrap = layout.content;\n\t\tconst auto totalHeight = st::storiesUnsupportedTop\n\t\t\t+ text.height()\n\t\t\t+ st::storiesUnsupportedSkip\n\t\t\t+ button.height();\n\t\tconst auto top = (wrap.height() - totalHeight) / 2;\n\t\t_text->move(\n\t\t\twrap.x() + (wrap.width() - text.width()) / 2,\n\t\t\twrap.y() + top + st::storiesUnsupportedTop);\n\t\t_button->move(\n\t\t\twrap.x() + (wrap.width() - button.width()) / 2,\n\t\t\twrap.y() + top + totalHeight - button.height());\n\t}, _button->lifetime());\n\n\t_button->setClickedCallback([=] {\n\t\tCore::UpdateApplication();\n\t});\n}\n\nController::Controller(not_null<Delegate*> delegate)\n: _delegate(delegate)\n, _wrap(_delegate->storiesWrap())\n, _header(std::make_unique<Header>(this))\n, _slider(std::make_unique<Slider>(this))\n, _replyArea(std::make_unique<ReplyArea>(this))\n, _reactions(std::make_unique<Reactions>(this))\n, _recentViews(std::make_unique<RecentViews>(this))\n, _paidReactionToast(std::make_unique<PaidReactionToast>(\n\t_wrap,\n\t&delegate->storiesShow()->session().data(),\n\tpaidReactionToastTopValue(),\n\t[=](not_null<Calls::GroupCall*> call) {\n\t\treturn _videoStreamCall.get() == call;\n\t}))\n, _weatherInCelsius(ResolveWeatherInCelsius()){\n\tinitLayout();\n\n\tusing namespace rpl::mappers;\n\n\trpl::combine(\n\t\t_replyArea->activeValue(),\n\t\t_reactions->activeValue(),\n\t\t_1 || _2\n\t) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](bool active) {\n\t\t_replyActive = active;\n\t\tupdateContentFaded();\n\t}, _lifetime);\n\n\t_reactions->setReplyFieldState(\n\t\t_replyArea->focusedValue(),\n\t\t_replyArea->hasSendTextValue());\n\tif (const auto like = _replyArea->likeAnimationTarget()) {\n\t\t_reactions->attachToReactionButton(like);\n\t}\n\n\t_reactions->chosen(\n\t) | rpl::on_next([=](Reactions::Chosen chosen) {\n\t\tif (reactionChosen(chosen.mode, chosen.reaction)) {\n\t\t\t_reactions->animateAndProcess(std::move(chosen));\n\t\t}\n\t}, _lifetime);\n\n\t_delegate->storiesLayerShown(\n\t) | rpl::on_next([=](bool shown) {\n\t\tif (_layerShown != shown) {\n\t\t\t_layerShown = shown;\n\t\t\tupdatePlayingAllowed();\n\t\t}\n\t}, _lifetime);\n\n\t_header->tooltipShownValue(\n\t) | rpl::on_next([=](bool shown) {\n\t\tif (_tooltipShown != shown) {\n\t\t\t_tooltipShown = shown;\n\t\t\tupdatePlayingAllowed();\n\t\t}\n\t}, _lifetime);\n\n\t_wrap->windowActiveValue(\n\t) | rpl::on_next([=](bool active) {\n\t\t_windowActive = active;\n\t\tupdatePlayingAllowed();\n\t}, _lifetime);\n\n\t_contentFadeAnimation.stop();\n}\n\nController::~Controller() {\n\t_captionFullView = nullptr;\n\t_repostView = nullptr;\n\tchangeShown(nullptr);\n}\n\nvoid Controller::updateContentFaded() {\n\tconst auto faded = _replyActive\n\t\t|| (_captionFullView && !_captionFullView->closing());\n\tif (_contentFaded == faded) {\n\t\treturn;\n\t}\n\t_contentFaded = faded;\n\t_contentFadeAnimation.start(\n\t\t[=] { _delegate->storiesRepaint(); },\n\t\t_contentFaded ? 0. : 1.,\n\t\t_contentFaded ? 1. : 0.,\n\t\tst::fadeWrapDuration);\n\tupdatePlayingAllowed();\n}\n\nvoid Controller::initLayout() {\n\tconst auto headerHeight = st::storiesHeaderMargin.top()\n\t\t+ st::storiesHeaderPhoto.photoSize\n\t\t+ st::storiesHeaderMargin.bottom();\n\tconst auto sliderHeight = st::storiesSliderMargin.top()\n\t\t+ st::storiesSliderWidth\n\t\t+ st::storiesSliderMargin.bottom();\n\tconst auto outsideHeaderHeight = headerHeight\n\t\t+ sliderHeight\n\t\t+ st::storiesSliderOutsideSkip;\n\tconst auto fieldMinHeight = st::storiesFieldMargin.top()\n\t\t+ st::storiesAttach.height\n\t\t+ st::storiesFieldMargin.bottom();\n\tconst auto minHeightForOutsideHeader = st::storiesFieldMargin.bottom()\n\t\t+ outsideHeaderHeight\n\t\t+ st::storiesMaxSize.height()\n\t\t+ fieldMinHeight;\n\n\t_layout = _wrap->sizeValue(\n\t) | rpl::map([=](QSize size) {\n\t\tconst auto topNotchSkip = _delegate->storiesTopNotchSkip();\n\n\t\tsize = QSize(\n\t\t\tstd::max(size.width(), st::mediaviewMinWidth),\n\t\t\tstd::max(size.height(), st::mediaviewMinHeight));\n\n\t\tauto layout = Layout();\n\t\tlayout.headerLayout = (size.height() >= minHeightForOutsideHeader)\n\t\t\t? HeaderLayout::Outside\n\t\t\t: HeaderLayout::Normal;\n\n\t\tconst auto topSkip = topNotchSkip\n\t\t\t+ st::storiesFieldMargin.bottom()\n\t\t\t+ (layout.headerLayout == HeaderLayout::Outside\n\t\t\t\t? outsideHeaderHeight\n\t\t\t\t: 0);\n\t\tconst auto bottomSkip = fieldMinHeight;\n\t\tconst auto maxWidth = size.width() - 2 * st::storiesSideSkip;\n\t\tconst auto availableHeight = size.height() - topSkip - bottomSkip;\n\t\tconst auto maxContentHeight = std::min(\n\t\t\tavailableHeight,\n\t\t\tst::storiesMaxSize.height());\n\t\tconst auto nowWidth = maxContentHeight * st::storiesMaxSize.width()\n\t\t\t/ st::storiesMaxSize.height();\n\t\tconst auto contentWidth = std::min(nowWidth, maxWidth);\n\t\tconst auto contentHeight = (contentWidth < nowWidth)\n\t\t\t? (contentWidth * st::storiesMaxSize.height()\n\t\t\t\t/ st::storiesMaxSize.width())\n\t\t\t: maxContentHeight;\n\t\tconst auto addedTopSkip = (availableHeight - contentHeight) / 2;\n\t\tlayout.content = QRect(\n\t\t\t(size.width() - contentWidth) / 2,\n\t\t\taddedTopSkip + topSkip,\n\t\t\tcontentWidth,\n\t\t\tcontentHeight);\n\n\t\tconst auto reactionsWidth = st::storiesReactionsWidth;\n\t\tlayout.reactions = QRect(\n\t\t\t(size.width() - reactionsWidth) / 2,\n\t\t\tlayout.content.y(),\n\t\t\treactionsWidth,\n\t\t\tcontentHeight);\n\n\t\tif (layout.headerLayout == HeaderLayout::Outside) {\n\t\t\tlayout.header = QRect(\n\t\t\t\tlayout.content.topLeft() - QPoint(0, outsideHeaderHeight),\n\t\t\t\tQSize(contentWidth, outsideHeaderHeight));\n\t\t\tlayout.slider = QRect(\n\t\t\t\tlayout.header.topLeft() + QPoint(0, headerHeight),\n\t\t\t\tQSize(contentWidth, sliderHeight));\n\t\t} else {\n\t\t\tlayout.slider = QRect(\n\t\t\t\tlayout.content.topLeft(),\n\t\t\t\tQSize(contentWidth, sliderHeight));\n\t\t\tlayout.header = QRect(\n\t\t\t\tlayout.slider.topLeft() + QPoint(0, sliderHeight),\n\t\t\t\tQSize(contentWidth, headerHeight));\n\t\t}\n\t\tlayout.controlsWidth = std::max(\n\t\t\tlayout.content.width() + st::storiesControlsExtend * 2,\n\t\t\tst::storiesControlsMinWidth);\n\t\tlayout.controlsBottomPosition = QPoint(\n\t\t\t(size.width() - layout.controlsWidth) / 2,\n\t\t\t(layout.content.y()\n\t\t\t\t+ layout.content.height()\n\t\t\t\t+ fieldMinHeight\n\t\t\t\t- st::storiesFieldMargin.bottom()));\n\t\tlayout.views = QRect(\n\t\t\tlayout.controlsBottomPosition - QPoint(0, fieldMinHeight),\n\t\t\tQSize(layout.controlsWidth, fieldMinHeight));\n\t\tlayout.autocompleteRect = QRect(\n\t\t\tlayout.controlsBottomPosition.x(),\n\t\t\t0,\n\t\t\tlayout.controlsWidth,\n\t\t\tlayout.controlsBottomPosition.y());\n\n\t\tconst auto sidesAvailable = size.width() - layout.content.width();\n\t\tconst auto widthForSiblings = sidesAvailable\n\t\t\t- 2 * st::storiesFieldMargin.bottom();\n\t\tconst auto siblingWidthMax = widthForSiblings\n\t\t\t/ (2 * (1. - kSiblingOutsidePart));\n\t\tconst auto siblingMultiplierMax = std::max(\n\t\t\tkSiblingMultiplierDefault,\n\t\t\tst::storiesSiblingWidthMin / float64(layout.content.width()));\n\t\tconst auto siblingMultiplier = std::min({\n\t\t\tsiblingMultiplierMax,\n\t\t\tkSiblingMultiplierMax,\n\t\t\tsiblingWidthMax / layout.content.width(),\n\t\t});\n\t\tconst auto siblingSize = layout.content.size() * siblingMultiplier;\n\t\tconst auto siblingTop = (size.height() - siblingSize.height()) / 2;\n\t\tconst auto outsideMax = int(base::SafeRound(\n\t\t\tsiblingSize.width() * kSiblingOutsidePart));\n\t\tconst auto leftAvailable = layout.content.x() - siblingSize.width();\n\t\tconst auto xDesired = leftAvailable / 3;\n\t\tconst auto xPossible = std::min(\n\t\t\txDesired,\n\t\t\t(leftAvailable - st::storiesControlSize));\n\t\tconst auto xLeft = std::max(xPossible, -outsideMax);\n\t\tconst auto xRight = size.width() - siblingSize.width() - xLeft;\n\t\tconst auto userpicSize = int(base::SafeRound(\n\t\t\tsiblingSize.width() * kSiblingUserpicSize));\n\t\tconst auto innerHeight = userpicSize * kInnerHeightMultiplier;\n\t\tconst auto userpic = [&](QRect geometry) {\n\t\t\treturn QRect(\n\t\t\t\t(geometry.width() - userpicSize) / 2,\n\t\t\t\t(geometry.height() - innerHeight) / 2,\n\t\t\t\tuserpicSize,\n\t\t\t\tuserpicSize\n\t\t\t).translated(geometry.topLeft());\n\t\t};\n\t\tconst auto nameFontSize = std::max(\n\t\t\t(st::storiesMaxNameFontSize * contentHeight\n\t\t\t\t/ st::storiesMaxSize.height()),\n\t\t\tst::fsize);\n\t\tconst auto nameBoundingRect = [&](QRect geometry, bool left) {\n\t\t\tconst auto skipSmall = nameFontSize;\n\t\t\tconst auto skipBig = skipSmall - std::min(xLeft, 0);\n\t\t\treturn QRect(\n\t\t\t\tleft ? skipBig : skipSmall,\n\t\t\t\t(geometry.height() - innerHeight) / 2,\n\t\t\t\tgeometry.width() - skipSmall - skipBig,\n\t\t\t\tinnerHeight\n\t\t\t).translated(geometry.topLeft());\n\t\t};\n\t\tconst auto left = QRect({ xLeft, siblingTop }, siblingSize);\n\t\tconst auto right = QRect({ xRight, siblingTop }, siblingSize);\n\t\tlayout.siblingLeft = {\n\t\t\t.geometry = left,\n\t\t\t.userpic = userpic(left),\n\t\t\t.nameBoundingRect = nameBoundingRect(left, true),\n\t\t\t.nameFontSize = nameFontSize,\n\t\t};\n\t\tlayout.siblingRight = {\n\t\t\t.geometry = right,\n\t\t\t.userpic = userpic(right),\n\t\t\t.nameBoundingRect = nameBoundingRect(right, false),\n\t\t\t.nameFontSize = nameFontSize,\n\t\t};\n\t\tif (!_areas.empty()) {\n\t\t\trebuildActiveAreas(layout);\n\t\t}\n\t\treturn layout;\n\t});\n}\n\nvoid Controller::rebuildActiveAreas(const Layout &layout) const {\n\tconst auto origin = layout.content.topLeft();\n\tconst auto scale = layout.content.size();\n\tfor (auto &area : _areas) {\n\t\tconst auto &general = area.original;\n\t\tarea.geometry = QRect(\n\t\t\tint(base::SafeRound(general.x() * scale.width())),\n\t\t\tint(base::SafeRound(general.y() * scale.height())),\n\t\t\tint(base::SafeRound(general.width() * scale.width())),\n\t\t\tint(base::SafeRound(general.height() * scale.height()))\n\t\t).translated(origin);\n\t\tarea.radius = scale.width() * area.radiusOriginal / 100.;\n\t\tif (const auto view = area.view.get()) {\n\t\t\tview->setAreaGeometry(area.geometry, area.radius);\n\t\t\tview->setContentRect(\n\t\t\t\tlayout.content,\n\t\t\t\tst::storiesRadius);\n\t\t}\n\t}\n}\n\nData::Story *Controller::story() const {\n\tif (!_session) {\n\t\treturn nullptr;\n\t}\n\tconst auto maybeStory = _session->data().stories().lookup(_shown);\n\treturn maybeStory ? maybeStory->get() : nullptr;\n}\n\nnot_null<Ui::RpWidget*> Controller::wrap() const {\n\treturn _wrap;\n}\n\nLayout Controller::layout() const {\n\tExpects(_layout.current().has_value());\n\n\treturn *_layout.current();\n}\n\nrpl::producer<Layout> Controller::layoutValue() const {\n\treturn _layout.value() | rpl::filter_optional();\n}\n\nContentLayout Controller::contentLayout() const {\n\tconst auto &current = _layout.current();\n\tAssert(current.has_value());\n\n\treturn {\n\t\t.geometry = current->content,\n\t\t.fade = (_contentFadeAnimation.value(_contentFaded ? 1. : 0.)\n\t\t\t* kFullContentFade),\n\t\t.radius = st::storiesRadius,\n\t\t.headerOutside = (current->headerLayout == HeaderLayout::Outside),\n\t};\n}\n\nbool Controller::closeByClickAt(QPoint position) const {\n\tconst auto &current = _layout.current();\n\tAssert(current.has_value());\n\n\treturn (position.x() < current->content.x() - st::storiesControlSize)\n\t\t|| (position.x() > current->content.x() + current->content.width()\n\t\t\t+ st::storiesControlSize);\n}\n\nData::FileOrigin Controller::fileOrigin() const {\n\treturn _shown;\n}\n\nTextWithEntities Controller::captionText() const {\n\treturn _captionText;\n}\n\nbool Controller::skipCaption() const {\n\treturn _videoStream\n\t\t|| (_captionFullView != nullptr)\n\t\t|| (_captionText.empty() && !repost());\n}\n\nbool Controller::repost() const {\n\treturn _repostView != nullptr;\n}\n\nint Controller::repostSkipTop() const {\n\treturn _repostView\n\t\t? (_repostView->height()\n\t\t\t+ (_captionText.empty() ? 0 : st::mediaviewTextSkip))\n\t\t: 0;\n}\n\nQMargins Controller::repostCaptionPadding() const {\n\treturn { 0, repostSkipTop(), 0, 0 };\n}\n\nvoid Controller::drawRepostInfo(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth) const {\n\tExpects(_repostView != nullptr);\n\n\t_repostView->draw(p, x, y, availableWidth);\n}\n\nRepostClickHandler Controller::lookupRepostHandler(QPoint position) const {\n\treturn _repostView\n\t\t? _repostView->lookupHandler(position)\n\t\t: RepostClickHandler();\n}\n\nvoid Controller::toggleLiked() {\n\t_reactions->toggleLiked();\n}\n\nbool Controller::reactionChosen(ReactionsMode mode, ChosenReaction chosen) {\n\tauto result = true;\n\tif (mode == ReactionsMode::Message) {\n\t\tresult = _replyArea->sendReaction(chosen.id);\n\t} else if (const auto peer = shownPeer()) {\n\t\tpeer->owner().stories().sendReaction(_shown, chosen.id);\n\t}\n\tunfocusReply();\n\treturn result;\n}\n\nrpl::producer<int> Controller::paidReactionToastTopValue() const {\n\treturn _layout.value(\n\t) | rpl::map([](const std::optional<Layout> &layout) {\n\t\tconst auto base = !layout\n\t\t\t? 0\n\t\t\t: (layout->headerLayout == HeaderLayout::Normal)\n\t\t\t? (layout->header.y() + layout->header.height())\n\t\t\t: layout->content.y();\n\t\treturn base + st::storiesHeaderMargin.bottom();\n\t});\n}\n\nvoid Controller::showFullCaption() {\n\tif (_captionText.empty()) {\n\t\treturn;\n\t}\n\t_captionFullView = std::make_unique<CaptionFullView>(this);\n\tupdateContentFaded();\n}\n\nvoid Controller::captionClosing() {\n\tupdateContentFaded();\n}\n\nvoid Controller::captionClosed() {\n\tif (!_captionFullView) {\n\t\treturn;\n\t} else if (_captionFullView->focused()) {\n\t\t_wrap->setFocus();\n\t}\n\t_captionFullView = nullptr;\n}\n\nstd::shared_ptr<ChatHelpers::Show> Controller::uiShow() const {\n\treturn _delegate->storiesShow();\n}\n\nauto Controller::stickerOrEmojiChosen() const\n-> rpl::producer<ChatHelpers::FileChosen> {\n\treturn _delegate->storiesStickerOrEmojiChosen();\n}\n\nvoid Controller::rebuildFromContext(\n\t\tnot_null<PeerData*> peer,\n\t\tFullStoryId storyId) {\n\tusing namespace Data;\n\n\tauto &stories = peer->owner().stories();\n\tauto list = std::optional<StoriesList>();\n\tauto source = (const StoriesSource*)nullptr;\n\tconst auto peerId = storyId.peer;\n\tconst auto id = storyId.story;\n\tv::match(_context.data, [&](StoriesContextSingle) {\n\t\thideSiblings();\n\t}, [&](StoriesContextPeer) {\n\t\tsource = stories.source(peerId);\n\t\thideSiblings();\n\t}, [&](StoriesContextAlbum album) {\n\t\tconst auto known = stories.albumIdsCountKnown(peerId, album.id);\n\t\tif (known) {\n\t\t\tconst auto &ids = stories.albumIds(peerId, album.id);\n\t\t\tauto sorted = RespectingPinned(ids);\n\t\t\tconst auto i = ranges::find(sorted, id);\n\t\t\tconst auto tillEnd = int(end(sorted) - i);\n\t\t\tif (tillEnd > 0) {\n\t\t\t\t_index = int(i - begin(sorted));\n\t\t\t\tconst auto total = stories.albumIdsCount(peerId, album.id);\n\t\t\t\tlist = StoriesList{\n\t\t\t\t\t.peer = peer,\n\t\t\t\t\t.ids = ids,\n\t\t\t\t\t.sorted = std::move(sorted),\n\t\t\t\t\t.total = total,\n\t\t\t\t};\n\t\t\t\tif (ids.list.size() < list->total\n\t\t\t\t\t&& tillEnd < kPreloadStoriesCount) {\n\t\t\t\t\tstories.albumIdsLoadMore(peerId, album.id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\thideSiblings();\n\t}, [&](StorySourcesList list) {\n\t\tsource = stories.source(peerId);\n\t\tconst auto &sources = stories.sources(list);\n\t\tconst auto i = ranges::find(\n\t\t\tsources,\n\t\t\tstoryId.peer,\n\t\t\t&StoriesSourceInfo::id);\n\t\tif (i != end(sources)) {\n\t\t\tif (_cachedSourcesList.empty()) {\n\t\t\t\t_showingUnreadSources = source && (source->readTill < id);\n\t\t\t}\n\t\t\trebuildCachedSourcesList(sources, (i - begin(sources)));\n\t\t\t_cachedSourcesList[_cachedSourceIndex].shownId = storyId.story;\n\t\t\tshowSiblings(&peer->session());\n\t\t\tif (int(sources.end() - i) < kPreloadPeersCount) {\n\t\t\t\tstories.loadMore(list);\n\t\t\t}\n\t\t}\n\t});\n\t_sliderIndex = 0;\n\t_sliderCount = 0;\n\tif (list) {\n\t\t_source = std::nullopt;\n\t\tif (_list != list) {\n\t\t\t_list = std::move(list);\n\t\t}\n\t\tif (const auto maybe = peer->owner().stories().lookup(storyId)) {\n\t\t\tconst auto now = *maybe;\n\t\t\tconst auto range = ComputeSameDayRange(\n\t\t\t\tnow,\n\t\t\t\t_list->ids,\n\t\t\t\t_list->sorted,\n\t\t\t\t_index);\n\t\t\t_sliderCount = range.till - range.from + 1;\n\t\t\t_sliderIndex = _index - range.from;\n\t\t}\n\t} else {\n\t\tif (source) {\n\t\t\tconst auto i = source->ids.lower_bound(StoryIdDates{ id });\n\t\t\tif (i != end(source->ids) && i->id == id) {\n\t\t\t\t_index = int(i - begin(source->ids));\n\t\t\t} else {\n\t\t\t\tsource = nullptr;\n\t\t\t}\n\t\t}\n\t\tif (!source) {\n\t\t\t_source = std::nullopt;\n\t\t\t_list = StoriesList{\n\t\t\t\t.peer = peer,\n\t\t\t\t.ids = { { id } },\n\t\t\t\t.sorted = { id },\n\t\t\t\t.total = 1,\n\t\t\t};\n\t\t\t_index = 0;\n\t\t} else {\n\t\t\t_list = std::nullopt;\n\t\t\tif (_source != *source) {\n\t\t\t\t_source = *source;\n\t\t\t}\n\t\t}\n\t}\n\tpreloadNext();\n\t_slider->show({\n\t\t.index = _sliderCount ? _sliderIndex : _index,\n\t\t.total = _sliderCount ? _sliderCount : shownCount(),\n\t\t.videoStream = videoStream(),\n\t});\n}\n\nvoid Controller::preloadNext() {\n\tExpects(shown());\n\n\tauto ids = std::vector<FullStoryId>();\n\tids.reserve(kPreloadPreviousMediaCount + kPreloadNextMediaCount);\n\tconst auto peer = shownPeer();\n\tconst auto count = shownCount();\n\tconst auto till = std::min(_index + kPreloadNextMediaCount, count);\n\tfor (auto i = _index + 1; i != till; ++i) {\n\t\tids.push_back({ .peer = peer->id, .story = shownId(i) });\n\t}\n\tconst auto from = std::max(_index - kPreloadPreviousMediaCount, 0);\n\tfor (auto i = _index; i != from;) {\n\t\tids.push_back({ .peer = peer->id, .story = shownId(--i) });\n\t}\n\tpeer->owner().stories().setPreloadingInViewer(std::move(ids));\n}\n\nvoid Controller::checkMoveByDelta() {\n\tconst auto index = _index + _waitingForDelta;\n\tif (_waitingForDelta && shown() && index >= 0 && index < shownCount()) {\n\t\tsubjumpTo(index);\n\t}\n}\n\nvoid Controller::show(\n\t\tnot_null<Data::Story*> story,\n\t\tData::StoriesContext context) {\n\tauto &stories = story->owner().stories();\n\tconst auto storyId = story->fullId();\n\tconst auto peer = story->peer();\n\t_context = context;\n\t_waitingForId = {};\n\t_waitingForDelta = 0;\n\t_videoStream = story->call();\n\tif (!_videoStream) {\n\t\tclearVideoStreamCall();\n\t}\n\n\trebuildFromContext(peer, storyId);\n\t_contextLifetime.destroy();\n\tconst auto subscribeToSource = [&] {\n\t\tstories.sourceChanged() | rpl::filter(\n\t\t\trpl::mappers::_1 == storyId.peer\n\t\t) | rpl::on_next([=] {\n\t\t\trebuildFromContext(peer, storyId);\n\t\t}, _contextLifetime);\n\t};\n\tv::match(_context.data, [&](Data::StoriesContextSingle) {\n\t}, [&](Data::StoriesContextPeer) {\n\t\tsubscribeToSource();\n\t}, [&](Data::StoriesContextAlbum album) {\n\t\tconst auto key = Data::StoryAlbumIdsKey{ storyId.peer, album.id };\n\t\tstories.albumIdsChanged() | rpl::filter(\n\t\t\trpl::mappers::_1 == key\n\t\t) | rpl::on_next([=] {\n\t\t\trebuildFromContext(peer, storyId);\n\t\t\tcheckMoveByDelta();\n\t\t}, _contextLifetime);\n\t}, [&](Data::StorySourcesList) {\n\t\tsubscribeToSource();\n\t});\n\n\tconst auto guard = gsl::finally([&] {\n\t\t_paused = false;\n\t\t_started = false;\n\t\tif (!story->document()) {\n\t\t\t_photoPlayback = std::make_unique<PhotoPlayback>(this);\n\t\t} else {\n\t\t\t_photoPlayback = nullptr;\n\t\t}\n\t});\n\n\tconst auto unsupported = story->unsupported();\n\tif (!unsupported) {\n\t\t_unsupported = nullptr;\n\t} else {\n\t\t_unsupported = std::make_unique<Unsupported>(this, peer);\n\t\t_header->raise();\n\t\t_slider->raise();\n\t}\n\n\tcaptionClosed();\n\t_repostView = validateRepostView(story);\n\t_captionText = story->caption();\n\t_contentFaded = false;\n\t_contentFadeAnimation.stop();\n\tconst auto document = story->document();\n\t_header->show({\n\t\t.peer = peer,\n\t\t.fromPeer = story->fromPeer(),\n\t\t.repostPeer = _repostView ? _repostView->fromPeer() : nullptr,\n\t\t.repostFrom = _repostView ? _repostView->fromName() : nullptr,\n\t\t.date = story->date(),\n\t\t.fullIndex = _sliderCount ? _index : 0,\n\t\t.fullCount = _sliderCount ? shownCount() : 0,\n\t\t.privacy = story->privacy(),\n\t\t.edited = story->edited(),\n\t\t.video = (document != nullptr),\n\t\t.videoStream = videoStream(),\n\t\t.silent = (document && document->isSilentVideo()),\n\t}, _videoStream ? _videoStream->fullCountValue() : nullptr);\n\tuiShow()->hideLayer(anim::type::instant);\n\tif (!changeShown(story)) {\n\t\treturn;\n\t}\n\n\tclearVideoStreamCall();\n\t_replyArea->show({\n\t\t.peer = unsupported ? nullptr : peer.get(),\n\t\t.id = story->id(),\n\t\t.videoStream = _videoStream,\n\t}, _reactions->likedValue());\n\n\tconst auto wasLikeButton = QPointer(_recentViews->likeButton());\n\t_recentViews->show({\n\t\t.list = story->recentViewers(),\n\t\t.reactions = story->reactions(),\n\t\t.forwards = story->forwards(),\n\t\t.views = story->views(),\n\t\t.total = story->interactions(),\n\t\t.type = RecentViewsTypeFor(peer, videoStream()),\n\t\t.canViewReactions = (!_videoStream\n\t\t\t&& CanViewReactionsFor(peer)\n\t\t\t&& !peer->isMegagroup()),\n\t}, _reactions->likedValue());\n\tif (const auto nowLikeButton = _recentViews->likeButton()) {\n\t\tif (wasLikeButton != nowLikeButton) {\n\t\t\t_reactions->attachToReactionButton(nowLikeButton);\n\t\t}\n\t}\n\n\tif (peer->isSelf() || peer->isBroadcast() || peer->isServiceUser()) {\n\t\t_reactions->setReactionIconWidget(_recentViews->likeIconWidget());\n\t} else if (const auto like = _replyArea->likeAnimationTarget()) {\n\t\t_reactions->setReactionIconWidget(like);\n\t}\n\t_reactions->showLikeFrom(story);\n\n\tstories.loadAround(storyId, context);\n\n\tupdatePlayingAllowed();\n\tpeer->updateFull();\n}\n\nvoid Controller::jumpTo(\n\t\tnot_null<Data::Story*> story,\n\t\tData::StoriesContext context) {\n\tshow(story, std::move(context));\n\t_delegate->storiesRedisplay(story);\n}\n\nbool Controller::changeShown(Data::Story *story) {\n\tconst auto id = story ? story->fullId() : FullStoryId();\n\tconst auto session = story ? &story->session() : nullptr;\n\tconst auto sessionChanged = (_session != session);\n\n\tupdateAreas(story);\n\n\tif (_shown == id && !sessionChanged) {\n\t\treturn false;\n\t}\n\tif (_shown) {\n\t\tAssert(_session != nullptr);\n\t\t_session->data().stories().unregisterPolling(\n\t\t\t_shown,\n\t\t\tData::Stories::Polling::Viewer);\n\t}\n\tif (sessionChanged) {\n\t\t_sessionLifetime.destroy();\n\t}\n\t_shown = id;\n\t_session = session;\n\tif (sessionChanged) {\n\t\tsubscribeToSession();\n\t}\n\tif (story) {\n\t\tstory->owner().stories().registerPolling(\n\t\t\tstory,\n\t\t\tData::Stories::Polling::Viewer);\n\t}\n\n\t_viewed = false;\n\tinvalidate_weak_ptrs(&_viewsLoadGuard);\n\t_reactions->hide();\n\t_reactions->setReactionIconWidget(nullptr);\n\tif (_replyArea->focused()) {\n\t\tunfocusReply();\n\t}\n\n\treturn true;\n}\n\nvoid Controller::subscribeToSession() {\n\tExpects(!_sessionLifetime);\n\n\tif (!_session) {\n\t\treturn;\n\t}\n\t_session->changes().storyUpdates(\n\t\tData::StoryUpdate::Flag::Destroyed\n\t) | rpl::on_next([=](Data::StoryUpdate update) {\n\t\tif (update.story->fullId() == _shown) {\n\t\t\t_delegate->storiesClose();\n\t\t}\n\t}, _sessionLifetime);\n\t_session->data().stories().itemsChanged(\n\t) | rpl::on_next([=](PeerId peerId) {\n\t\tif (_waitingForId.peer == peerId) {\n\t\t\tcheckWaitingFor();\n\t\t}\n\t}, _sessionLifetime);\n\t_session->changes().storyUpdates(\n\t\tData::StoryUpdate::Flag::Edited\n\t\t| Data::StoryUpdate::Flag::ViewsChanged\n\t\t| Data::StoryUpdate::Flag::Reaction\n\t) | rpl::filter([=](const Data::StoryUpdate &update) {\n\t\treturn (update.story == this->story());\n\t}) | rpl::on_next([=](const Data::StoryUpdate &update) {\n\t\tif (update.flags & Data::StoryUpdate::Flag::Edited) {\n\t\t\tshow(update.story, _context);\n\t\t\t_delegate->storiesRedisplay(update.story);\n\t\t} else {\n\t\t\tconst auto peer = update.story->peer();\n\t\t\t_recentViews->show({\n\t\t\t\t.list = update.story->recentViewers(),\n\t\t\t\t.reactions = update.story->reactions(),\n\t\t\t\t.forwards = update.story->forwards(),\n\t\t\t\t.views = update.story->views(),\n\t\t\t\t.total = update.story->interactions(),\n\t\t\t\t.type = RecentViewsTypeFor(peer, videoStream()),\n\t\t\t\t.canViewReactions = (!_videoStream\n\t\t\t\t\t&& CanViewReactionsFor(peer)\n\t\t\t\t\t&& !peer->isMegagroup()),\n\t\t\t});\n\t\t\tupdateAreas(update.story);\n\t\t}\n\t}, _sessionLifetime);\n\t_sessionLifetime.add([=] {\n\t\t_session->data().stories().setPreloadingInViewer({});\n\t});\n}\n\nvoid Controller::updateAreas(Data::Story *story) {\n\tconst auto &locations = story\n\t\t? story->locations()\n\t\t: std::vector<Data::StoryLocation>();\n\tconst auto &suggestedReactions = story\n\t\t? story->suggestedReactions()\n\t\t: std::vector<Data::SuggestedReaction>();\n\tconst auto &channelPosts = story\n\t\t? story->channelPosts()\n\t\t: std::vector<Data::ChannelPost>();\n\tconst auto &urlAreas = story\n\t\t? story->urlAreas()\n\t\t: std::vector<Data::UrlArea>();\n\tconst auto &weatherAreas = story\n\t\t? story->weatherAreas()\n\t\t: std::vector<Data::WeatherArea>();\n\tif (_locations != locations) {\n\t\t_locations = locations;\n\t\t_areas.clear();\n\t}\n\tif (_channelPosts != channelPosts) {\n\t\t_channelPosts = channelPosts;\n\t\t_areas.clear();\n\t}\n\tif (_urlAreas != urlAreas) {\n\t\t_urlAreas = urlAreas;\n\t\t_areas.clear();\n\t}\n\tif (_weatherAreas != weatherAreas) {\n\t\t_weatherAreas = weatherAreas;\n\t\t_areas.clear();\n\t}\n\tconst auto reactionsCount = int(suggestedReactions.size());\n\tif (_suggestedReactions.size() == reactionsCount && !_areas.empty()) {\n\t\tfor (auto i = 0; i != reactionsCount; ++i) {\n\t\t\tconst auto count = suggestedReactions[i].count;\n\t\t\tif (_suggestedReactions[i].count != count) {\n\t\t\t\t_suggestedReactions[i].count = count;\n\t\t\t\tconst auto view = _areas[i + _locations.size()].view.get();\n\t\t\t\tview->updateReactionsCount(count);\n\t\t\t}\n\t\t\tif (_suggestedReactions[i] != suggestedReactions[i]) {\n\t\t\t\t_suggestedReactions = suggestedReactions;\n\t\t\t\t_areas.clear();\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} else if (_suggestedReactions != suggestedReactions) {\n\t\t_suggestedReactions = suggestedReactions;\n\t\t_areas.clear();\n\t}\n}\n\nPauseState Controller::pauseState() const {\n\tconst auto inactive = !_windowActive\n\t\t|| _replyActive\n\t\t|| _layerShown\n\t\t|| _menuShown;\n\tconst auto playing = !inactive && !_paused;\n\treturn playing\n\t\t? PauseState::Playing\n\t\t: !inactive\n\t\t? PauseState::Paused\n\t\t: _paused\n\t\t? PauseState::InactivePaused\n\t\t: PauseState::Inactive;\n}\n\nfloat64 Controller::currentVolume() const {\n\treturn Core::App().settings().videoVolume();\n}\n\nvoid Controller::toggleVolume() {\n\t_delegate->storiesVolumeToggle();\n}\n\nvoid Controller::changeVolume(float64 volume) {\n\t_delegate->storiesVolumeChanged(volume);\n}\n\nvoid Controller::volumeChangeFinished() {\n\t_delegate->storiesVolumeChangeFinished();\n}\n\nvoid Controller::updatePlayingAllowed() {\n\tif (!_shown) {\n\t\treturn;\n\t}\n\t_header->updatePauseState();\n\tsetPlayingAllowed(_started\n\t\t&& _windowActive\n\t\t&& !_paused\n\t\t&& !_replyActive\n\t\t&& (!_captionFullView || _captionFullView->closing())\n\t\t&& !_layerShown\n\t\t&& !_menuShown\n\t\t&& !_tooltipShown);\n}\n\nvoid Controller::setPlayingAllowed(bool allowed) {\n\tif (_photoPlayback) {\n\t\t_photoPlayback->togglePaused(!allowed);\n\t} else {\n\t\t_delegate->storiesTogglePaused(!allowed);\n\t}\n}\n\nvoid Controller::showSiblings(not_null<Main::Session*> session) {\n\tshowSibling(\n\t\t_siblingLeft,\n\t\tsession,\n\t\t(_cachedSourceIndex > 0\n\t\t\t? _cachedSourcesList[_cachedSourceIndex - 1]\n\t\t\t: CachedSource()));\n\tshowSibling(\n\t\t_siblingRight,\n\t\tsession,\n\t\t(_cachedSourceIndex + 1 < _cachedSourcesList.size()\n\t\t\t? _cachedSourcesList[_cachedSourceIndex + 1]\n\t\t\t: CachedSource()));\n}\n\nvoid Controller::hideSiblings() {\n\t_siblingLeft = nullptr;\n\t_siblingRight = nullptr;\n}\n\nvoid Controller::showSibling(\n\t\tstd::unique_ptr<Sibling> &sibling,\n\t\tnot_null<Main::Session*> session,\n\t\tCachedSource cached) {\n\tif (!cached) {\n\t\tsibling = nullptr;\n\t\treturn;\n\t}\n\tconst auto source = session->data().stories().source(cached.peerId);\n\tif (!source) {\n\t\tsibling = nullptr;\n\t} else if (!sibling || !sibling->shows(*source, cached.shownId)) {\n\t\tsibling = std::make_unique<Sibling>(this, *source, cached.shownId);\n\t}\n}\n\nvoid Controller::ready() {\n\tif (_started) {\n\t\treturn;\n\t}\n\t_started = true;\n\tupdatePlayingAllowed();\n\t_reactions->ready();\n}\n\nvoid Controller::updateVideoPlayback(const Player::TrackState &state) {\n\tupdatePlayback(state);\n}\n\nvoid Controller::updatePhotoPlayback(const Player::TrackState &state) {\n\tupdatePlayback(state);\n}\n\nvoid Controller::updatePlayback(const Player::TrackState &state) {\n\t_slider->updatePlayback(state);\n\tupdatePowerSaveBlocker(state);\n\tmaybeMarkAsRead(state);\n\tif (Player::IsStoppedAtEnd(state.state)) {\n\t\tif (!subjumpFor(1)) {\n\t\t\t_delegate->storiesClose();\n\t\t}\n\t}\n}\n\nClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const {\n\tconst auto &layout = _layout.current();\n\tif (!layout\n\t\t|| (_locations.empty()\n\t\t\t&& _suggestedReactions.empty()\n\t\t\t&& _channelPosts.empty()\n\t\t\t&& _urlAreas.empty()\n\t\t\t&& _weatherAreas.empty())) {\n\t\treturn nullptr;\n\t} else if (_areas.empty()) {\n\t\tconst auto now = story();\n\t\t_areas.reserve(_locations.size()\n\t\t\t+ _suggestedReactions.size()\n\t\t\t+ _channelPosts.size()\n\t\t\t+ _urlAreas.size());\n\t\tfor (const auto &location : _locations) {\n\t\t\t_areas.push_back({\n\t\t\t\t.original = location.area.geometry,\n\t\t\t\t.rotation = location.area.rotation,\n\t\t\t\t.handler = std::make_shared<LocationClickHandler>(\n\t\t\t\t\tlocation.point),\n\t\t\t});\n\t\t}\n\t\tfor (const auto &suggestedReaction : _suggestedReactions) {\n\t\t\tconst auto id = suggestedReaction.reaction;\n\t\t\tauto widget = _reactions->makeSuggestedReactionWidget(\n\t\t\t\tsuggestedReaction);\n\t\t\tconst auto raw = widget.get();\n\t\t\t_areas.push_back({\n\t\t\t\t.original = suggestedReaction.area.geometry,\n\t\t\t\t.rotation = suggestedReaction.area.rotation,\n\t\t\t\t.handler = std::make_shared<LambdaClickHandler>([=] {\n\t\t\t\t\traw->playEffect();\n\t\t\t\t\tif (const auto now = story()) {\n\t\t\t\t\t\tif (now->sentReactionId() != id) {\n\t\t\t\t\t\t\tnow->owner().stories().sendReaction(\n\t\t\t\t\t\t\t\tnow->fullId(),\n\t\t\t\t\t\t\t\tid);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t\t.view = std::move(widget),\n\t\t\t});\n\t\t}\n\t\tif (const auto session = now ? &now->session() : nullptr) {\n\t\t\tfor (const auto &channelPost : _channelPosts) {\n\t\t\t\t_areas.push_back({\n\t\t\t\t\t.original = channelPost.area.geometry,\n\t\t\t\t\t.rotation = channelPost.area.rotation,\n\t\t\t\t\t.handler = MakeChannelPostHandler(\n\t\t\t\t\t\tsession,\n\t\t\t\t\t\tchannelPost.itemId),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tconst auto weak = base::make_weak(this);\n\t\tfor (const auto &url : _urlAreas) {\n\t\t\t_areas.push_back({\n\t\t\t\t.original = url.area.geometry,\n\t\t\t\t.rotation = url.area.rotation,\n\t\t\t\t.handler = MakeUrlAreaHandler(weak, url.url),\n\t\t\t});\n\t\t}\n\t\tfor (const auto &weather : _weatherAreas) {\n\t\t\t_areas.push_back({\n\t\t\t\t.original = weather.area.geometry,\n\t\t\t\t.radiusOriginal = weather.area.radius,\n\t\t\t\t.rotation = weather.area.rotation,\n\t\t\t\t.handler = std::make_shared<LambdaClickHandler>([=] {\n\t\t\t\t\ttoggleWeatherMode();\n\t\t\t\t}),\n\t\t\t\t.view = _reactions->makeWeatherAreaWidget(\n\t\t\t\t\tweather,\n\t\t\t\t\t_weatherInCelsius.value()),\n\t\t\t});\n\t\t}\n\t\trebuildActiveAreas(*layout);\n\t}\n\n\tfor (const auto &area : _areas) {\n\t\tconst auto center = area.geometry.center();\n\t\tconst auto angle = -area.rotation;\n\t\tconst auto contains = area.view\n\t\t\t? area.view->contains(point)\n\t\t\t: area.geometry.contains(Rotated(point, center, angle));\n\t\tif (contains) {\n\t\t\treturn area.handler;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid Controller::toggleWeatherMode() const {\n\tconst auto now = !_weatherInCelsius.current();\n\tCore::App().settings().setWeatherInCelsius(now);\n\tCore::App().saveSettingsDelayed();\n\t_weatherInCelsius = now;\n}\n\nvoid Controller::maybeMarkAsRead(const Player::TrackState &state) {\n\tconst auto length = state.length;\n\tconst auto position = Player::IsStoppedAtEnd(state.state)\n\t\t? state.length\n\t\t: Player::IsStoppedOrStopping(state.state)\n\t\t? 0\n\t\t: state.position;\n\tif (position > state.frequency * kMarkAsReadAfterSeconds) {\n\t\tif (position > kMarkAsReadAfterProgress * length) {\n\t\t\tmarkAsRead();\n\t\t}\n\t}\n}\n\nvoid Controller::markAsRead() {\n\tExpects(shown());\n\n\tif (_viewed) {\n\t\treturn;\n\t}\n\t_viewed = true;\n\tshownPeer()->owner().stories().markAsRead(_shown, _started);\n}\n\nbool Controller::subjumpAvailable(int delta) const {\n\tconst auto index = _index + delta;\n\tif (index < 0) {\n\t\treturn _siblingLeft && _siblingLeft->shownId().valid();\n\t} else if (index >= shownCount()) {\n\t\treturn _siblingRight && _siblingRight->shownId().valid();\n\t}\n\treturn index >= 0 && index < shownCount();\n}\n\nbool Controller::subjumpFor(int delta) {\n\tif (delta > 0) {\n\t\tmarkAsRead();\n\t}\n\tconst auto index = _index + delta;\n\tif (index < 0) {\n\t\tif (_siblingLeft && _siblingLeft->shownId().valid()) {\n\t\t\treturn jumpFor(-1);\n\t\t} else if (!shown() || !shownCount()) {\n\t\t\treturn false;\n\t\t}\n\t\tsubjumpTo(0);\n\t\treturn true;\n\t} else if (index >= shownCount()) {\n\t\treturn _siblingRight\n\t\t\t&& _siblingRight->shownId().valid()\n\t\t\t&& jumpFor(1);\n\t} else {\n\t\tsubjumpTo(index);\n\t}\n\treturn true;\n}\n\nvoid Controller::subjumpTo(int index) {\n\tExpects(shown());\n\tExpects(index >= 0 && index < shownCount());\n\n\tconst auto peer = shownPeer();\n\tconst auto id = FullStoryId{\n\t\t.peer = peer->id,\n\t\t.story = shownId(index),\n\t};\n\tauto &stories = peer->owner().stories();\n\tif (!id.story) {\n\t\tconst auto delta = index - _index;\n\t\tif (_waitingForDelta != delta) {\n\t\t\t_waitingForDelta = delta;\n\t\t\t_waitingForId = {};\n\t\t\tloadMoreToList();\n\t\t}\n\t} else if (stories.lookup(id)) {\n\t\t_delegate->storiesJumpTo(&peer->session(), id, _context);\n\t} else if (_waitingForId != id) {\n\t\t_waitingForId = id;\n\t\t_waitingForDelta = 0;\n\t\tstories.loadAround(id, _context);\n\t}\n}\n\nvoid Controller::checkWaitingFor() {\n\tExpects(_waitingForId.valid());\n\tExpects(shown());\n\n\tconst auto peer = shownPeer();\n\tauto &stories = peer->owner().stories();\n\tconst auto maybe = stories.lookup(_waitingForId);\n\tif (!maybe) {\n\t\tif (maybe.error() == Data::NoStory::Deleted) {\n\t\t\t_waitingForId = {};\n\t\t}\n\t\treturn;\n\t}\n\t_delegate->storiesJumpTo(\n\t\t&peer->session(),\n\t\tbase::take(_waitingForId),\n\t\t_context);\n}\n\nbool Controller::jumpFor(int delta) {\n\tif (delta == -1) {\n\t\tif (const auto left = _siblingLeft.get()) {\n\t\t\t_delegate->storiesJumpTo(\n\t\t\t\t&left->peer()->session(),\n\t\t\t\tleft->shownId(),\n\t\t\t\t_context);\n\t\t\treturn true;\n\t\t}\n\t} else if (delta == 1) {\n\t\tif (shown() && _index + 1 >= shownCount()) {\n\t\t\tmarkAsRead();\n\t\t}\n\t\tif (const auto right = _siblingRight.get()) {\n\t\t\t_delegate->storiesJumpTo(\n\t\t\t\t&right->peer()->session(),\n\t\t\t\tright->shownId(),\n\t\t\t\t_context);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool Controller::paused() const {\n\treturn _paused;\n}\n\nvoid Controller::togglePaused(bool paused) {\n\tif (_paused != paused) {\n\t\t_paused = paused;\n\t\tupdatePlayingAllowed();\n\t}\n}\n\nvoid Controller::contentPressed(bool pressed) {\n\ttogglePaused(pressed);\n\tif (_captionFullView) {\n\t\t_captionFullView->close();\n\t}\n\tif (pressed) {\n\t\t_reactions->outsidePressed();\n\t}\n}\n\nvoid Controller::setMenuShown(bool shown) {\n\tif (_menuShown != shown) {\n\t\t_menuShown = shown;\n\t\tupdatePlayingAllowed();\n\t}\n}\n\nvoid Controller::repaintSibling(not_null<Sibling*> sibling) {\n\tif (sibling == _siblingLeft.get() || sibling == _siblingRight.get()) {\n\t\t_delegate->storiesRepaint();\n\t}\n}\n\nvoid Controller::repaint() {\n\tif (_captionFullView) {\n\t\t_captionFullView->repaint();\n\t}\n\t_delegate->storiesRepaint();\n}\n\nSiblingView Controller::sibling(SiblingType type) const {\n\tconst auto &pointer = (type == SiblingType::Left)\n\t\t? _siblingLeft\n\t\t: _siblingRight;\n\tif (const auto value = pointer.get()) {\n\t\tconst auto over = _delegate->storiesSiblingOver(type);\n\t\tconst auto layout = (type == SiblingType::Left)\n\t\t\t? _layout.current()->siblingLeft\n\t\t\t: _layout.current()->siblingRight;\n\t\treturn value->view(layout, over);\n\t}\n\treturn {};\n}\n\nconst Data::StoryViews &Controller::views(int limit, bool initial) {\n\tinvalidate_weak_ptrs(&_viewsLoadGuard);\n\tif (initial) {\n\t\trefreshViewsFromData();\n\t}\n\tif (_viewsSlice.total > _viewsSlice.list.size()\n\t\t&& _viewsSlice.list.size() < limit) {\n\t\tconst auto done = viewsGotMoreCallback();\n\t\tconst auto peer = shownPeer();\n\t\tauto &stories = peer->owner().stories();\n\t\tif (peer->isChannel()) {\n\t\t\tstories.loadReactionsSlice(\n\t\t\t\tpeer,\n\t\t\t\t_shown.story,\n\t\t\t\t_viewsSlice.nextOffset,\n\t\t\t\tdone);\n\t\t} else {\n\t\t\tstories.loadViewsSlice(\n\t\t\t\tpeer,\n\t\t\t\t_shown.story,\n\t\t\t\t_viewsSlice.nextOffset,\n\t\t\t\tdone);\n\t\t}\n\t}\n\treturn _viewsSlice;\n}\n\nrpl::producer<> Controller::moreViewsLoaded() const {\n\treturn _moreViewsLoaded.events();\n}\n\nFn<void(Data::StoryViews)> Controller::viewsGotMoreCallback() {\n\treturn crl::guard(&_viewsLoadGuard, [=](Data::StoryViews result) {\n\t\tif (_viewsSlice.list.empty()) {\n\t\t\tconst auto peer = shownPeer();\n\t\t\tauto &stories = peer->owner().stories();\n\t\t\tif (const auto maybeStory = stories.lookup(_shown)) {\n\t\t\t\tif (peer->isChannel()) {\n\t\t\t\t\t_viewsSlice = (*maybeStory)->channelReactionsList();\n\t\t\t\t} else {\n\t\t\t\t\t_viewsSlice = (*maybeStory)->viewsList();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t_viewsSlice = {};\n\t\t\t}\n\t\t} else {\n\t\t\t_viewsSlice.list.insert(\n\t\t\t\tend(_viewsSlice.list),\n\t\t\t\tbegin(result.list),\n\t\t\t\tend(result.list));\n\t\t\t_viewsSlice.total = result.nextOffset.isEmpty()\n\t\t\t\t? int(_viewsSlice.list.size())\n\t\t\t\t: std::max(result.total, int(_viewsSlice.list.size()));\n\t\t\t_viewsSlice.nextOffset = result.nextOffset;\n\t\t}\n\t\t_moreViewsLoaded.fire({});\n\t});\n}\n\nbool Controller::shown() const {\n\treturn _source || _list;\n}\n\nPeerData *Controller::shownPeer() const {\n\treturn _source\n\t\t? _source->peer.get()\n\t\t: _list\n\t\t? _list->peer.get()\n\t\t: nullptr;\n}\n\nint Controller::shownCount() const {\n\treturn _source ? int(_source->ids.size()) : _list ? _list->total : 0;\n}\n\nStoryId Controller::shownId(int index) const {\n\tExpects(index >= 0 && index < shownCount());\n\n\treturn _source\n\t\t? (_source->ids.begin() + index)->id\n\t\t: (index < int(_list->sorted.size()))\n\t\t? _list->sorted[index]\n\t\t: StoryId();\n}\n\nstd::unique_ptr<RepostView> Controller::validateRepostView(\n\t\tnot_null<Data::Story*> story) {\n\treturn (story->repost() || !story->channelPosts().empty())\n\t\t? std::make_unique<RepostView>(this, story)\n\t\t: nullptr;\n}\n\nvoid Controller::loadMoreToList() {\n\tExpects(shown());\n\n\tusing namespace Data;\n\n\tconst auto peer = shownPeer();\n\tconst auto peerId = _shown.peer;\n\tauto &stories = peer->owner().stories();\n\tv::match(_context.data, [&](StoriesContextAlbum album) {\n\t\tstories.albumIdsLoadMore(peerId, album.id);\n\t}, [](const auto &) {\n\t});\n}\n\nvoid Controller::rebuildCachedSourcesList(\n\t\tconst std::vector<Data::StoriesSourceInfo> &lists,\n\t\tint index) {\n\tExpects(index >= 0 && index < lists.size());\n\n\tconst auto currentPeerId = lists[index].id;\n\n\t// Remove removed.\n\t_cachedSourcesList.erase(ranges::remove_if(_cachedSourcesList, [&](\n\t\t\tCachedSource source) {\n\t\treturn !ranges::contains(\n\t\t\tlists,\n\t\t\tsource.peerId,\n\t\t\t&Data::StoriesSourceInfo::id);\n\t}), end(_cachedSourcesList));\n\n\t// Find current, full rebuild if can't find.\n\tconst auto i = ranges::find(\n\t\t_cachedSourcesList,\n\t\tcurrentPeerId,\n\t\t&CachedSource::peerId);\n\tif (i == end(_cachedSourcesList)) {\n\t\t_cachedSourcesList.clear();\n\t} else {\n\t\t_cachedSourceIndex = int(i - begin(_cachedSourcesList));\n\t}\n\n\tif (_cachedSourcesList.empty()) {\n\t\t// Full rebuild.\n\t\tconst auto predicate = [&](const Data::StoriesSourceInfo &info) {\n\t\t\treturn !_showingUnreadSources\n\t\t\t\t|| (info.unreadCount > 0)\n\t\t\t\t|| (info.id == currentPeerId);\n\t\t};\n\t\tconst auto mapper = [](const Data::StoriesSourceInfo &info) {\n\t\t\treturn CachedSource{ info.id };\n\t\t};\n\t\t_cachedSourcesList = lists\n\t\t\t| ranges::views::filter(predicate)\n\t\t\t| ranges::views::transform(mapper)\n\t\t\t| ranges::to_vector;\n\t\t_cachedSourceIndex = ranges::find(\n\t\t\t_cachedSourcesList,\n\t\t\tcurrentPeerId,\n\t\t\t&CachedSource::peerId\n\t\t) - begin(_cachedSourcesList);\n\t} else if (ranges::equal(\n\t\t\tlists,\n\t\t\t_cachedSourcesList,\n\t\t\tranges::equal_to(),\n\t\t\t&Data::StoriesSourceInfo::id,\n\t\t\t&CachedSource::peerId)) {\n\t\t// No rebuild needed.\n\t} else {\n\t\t// All that go before the current push to front.\n\t\tfor (auto before = index; before > 0;) {\n\t\t\tconst auto &info = lists[--before];\n\t\t\tif (_showingUnreadSources && !info.unreadCount) {\n\t\t\t\tcontinue;\n\t\t\t} else if (!ranges::contains(\n\t\t\t\t\t_cachedSourcesList,\n\t\t\t\t\tinfo.id,\n\t\t\t\t\t&CachedSource::peerId)) {\n\t\t\t\t_cachedSourcesList.insert(\n\t\t\t\t\tbegin(_cachedSourcesList),\n\t\t\t\t\t{ info.id });\n\t\t\t\t++_cachedSourceIndex;\n\t\t\t}\n\t\t}\n\t\t// All that go after the current push to back.\n\t\tfor (auto after = index + 1, count = int(lists.size())\n\t\t\t; after != count\n\t\t\t; ++after) {\n\t\t\tconst auto &info = lists[after];\n\t\t\tif (_showingUnreadSources && !info.unreadCount) {\n\t\t\t\tcontinue;\n\t\t\t} else if (!ranges::contains(\n\t\t\t\t\t_cachedSourcesList,\n\t\t\t\t\tinfo.id,\n\t\t\t\t\t&CachedSource::peerId)) {\n\t\t\t\t_cachedSourcesList.push_back({ info.id });\n\t\t\t}\n\t\t}\n\t}\n\n\tEnsures(_cachedSourcesList.size() <= lists.size());\n\tEnsures(_cachedSourceIndex >= 0\n\t\t&& _cachedSourceIndex < _cachedSourcesList.size());\n}\n\nvoid Controller::refreshViewsFromData() {\n\tExpects(shown());\n\n\tconst auto peer = shownPeer();\n\tauto &stories = peer->owner().stories();\n\tconst auto maybeStory = stories.lookup(_shown);\n\tconst auto check = peer->isSelf()\n\t\t|| CanViewReactionsFor(peer);\n\tif (!maybeStory || !check) {\n\t\t_viewsSlice = {};\n\t} else if (peer->isChannel()) {\n\t\t_viewsSlice = (*maybeStory)->channelReactionsList();\n\t} else {\n\t\t_viewsSlice = (*maybeStory)->viewsList();\n\t}\n}\n\nvoid Controller::unfocusReply() {\n\t_wrap->setFocus();\n}\n\nrpl::producer<CommentsState> Controller::commentsStateValue() const {\n\treturn _commentsState.value();\n}\n\nvoid Controller::setCommentsShownToggles(rpl::producer<> toggles) {\n\tauto fromButton = std::move(\n\t\ttoggles\n\t) | rpl::map([=] {\n\t\tif (_commentsState.current() != CommentsState::Shown) {\n\t\t\t_commentsLastReadId = _commentsLastId;\n\t\t\tif (_commentsHas.current() == CommentsHas::WithUnread) {\n\t\t\t\t_commentsHas = CommentsHas::AllRead;\n\t\t\t}\n\t\t}\n\t\treturn (_commentsState.current() == CommentsState::Shown)\n\t\t\t? CommentsState::Hidden\n\t\t\t: CommentsState::Shown;\n\t});\n\tauto fromUnread = _commentsHas.value(\n\t) | rpl::map([=](CommentsHas value) {\n\t\tconst auto now = _commentsState.current();\n\t\treturn (value == CommentsHas::None)\n\t\t\t? CommentsState::Empty\n\t\t\t: (value == CommentsHas::WithUnread)\n\t\t\t? CommentsState::WithNew\n\t\t\t: (now == CommentsState::Shown || now == CommentsState::Empty)\n\t\t\t? CommentsState::Shown\n\t\t\t: CommentsState::Hidden;\n\t});\n\t_commentsState = rpl::merge(\n\t\tstd::move(fromButton),\n\t\tstd::move(fromUnread),\n\t\t_commentsStateShowFromPinned.events());\n}\n\nauto Controller::starsReactionsValue() const\n-> rpl::producer<Ui::SendStarButtonState> {\n\treturn rpl::combine(\n\t\t_starsReactions.value(),\n\t\t_starsReactionHighlighted.value()\n\t) | rpl::map([=](int stars, bool highlighted) {\n\t\treturn Ui::SendStarButtonState{ stars, highlighted };\n\t});\n}\n\nauto Controller::starsReactionsEffects() const\n-> rpl::producer<SendStarButtonEffect> {\n\treturn _starsReactionEffects.events();\n}\n\nvoid Controller::setStarsReactionIncrements(rpl::producer<int> increments) {\n\tstd::move(\n\t\tincrements\n\t) | rpl::on_next([=](int count) {\n\t\tif (const auto call = _videoStreamCall.get()) {\n\t\t\tconst auto show = _delegate->storiesShow();\n\t\t\tPayments::TryAddingPaidReaction(call, count, show);\n\t\t}\n\t}, _videoStreamLifetime);\n}\n\nvoid Controller::shareRequested() {\n\tconst auto show = _delegate->storiesShow();\n\tif (auto box = PrepareShareBox(show, _shown, true)) {\n\t\tshow->show(std::move(box));\n\t}\n}\n\nvoid Controller::deleteRequested() {\n\tconst auto story = this->story();\n\tif (!story) {\n\t\treturn;\n\t}\n\tconst auto id = story->fullId();\n\tconst auto weak = base::make_weak(this);\n\tconst auto owner = &story->owner();\n\tconst auto confirmed = [=](Fn<void()> close) {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tif (const auto story = strong->story()) {\n\t\t\t\tif (story->fullId() == id) {\n\t\t\t\t\tmoveFromShown();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\towner->stories().deleteList({ id });\n\t\tclose();\n\t};\n\tuiShow()->show(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_stories_delete_one_sure(),\n\t\t.confirmed = confirmed,\n\t\t.confirmText = tr::lng_selected_delete(),\n\t\t.labelStyle = &st::storiesBoxLabel,\n\t}));\n}\n\nvoid Controller::reportRequested() {\n\tReportRequested(uiShow(), _shown, &st::storiesReportBox);\n}\n\nvoid Controller::toggleInProfileRequested(bool inProfile) {\n\tconst auto story = this->story();\n\tif (!story || !story->peer()->isSelf()) {\n\t\treturn;\n\t}\n\tif (!inProfile\n\t\t&& v::is<Data::StoriesContextAlbum>(_context.data)\n\t\t&& (v::get<Data::StoriesContextAlbum>(_context.data).id\n\t\t\t!= Data::kStoriesAlbumIdArchive)) {\n\t\tmoveFromShown();\n\t}\n\tstory->owner().stories().toggleInProfileList(\n\t\t{ story->fullId() },\n\t\tinProfile);\n\tconst auto channel = story->peer()->isChannel();\n\tuiShow()->showToast(PrepareToggleInProfileToast(channel, 1, inProfile));\n}\n\nvoid Controller::moveFromShown() {\n\tif (!subjumpFor(1)) {\n\t\t[[maybe_unused]] const auto jumped = subjumpFor(-1);\n\t}\n}\n\nbool Controller::ignoreWindowMove(QPoint position) const {\n\treturn _replyArea->ignoreWindowMove(position)\n\t\t|| _header->ignoreWindowMove(position);\n}\n\nvoid Controller::tryProcessKeyInput(not_null<QKeyEvent*> e) {\n\t_replyArea->tryProcessKeyInput(e);\n}\n\nbool Controller::allowStealthMode() const {\n\tconst auto story = this->story();\n\treturn story\n\t\t&& !story->peer()->isSelf()\n\t\t&& story->peer()->session().premiumPossible();\n}\n\nvoid Controller::setupStealthMode() {\n\tSetupStealthMode(uiShow());\n}\n\nauto Controller::attachReactionsToMenu(\n\tnot_null<Ui::PopupMenu*> menu,\n\tQPoint desiredPosition)\n-> AttachStripResult {\n\treturn _reactions->attachToMenu(menu, desiredPosition);\n}\n\nvoid Controller::updateVideoStream(not_null<Calls::GroupCall*> videoStream) {\n\t_videoStreamCall = videoStream;\n\n\tusing namespace Calls::Group;\n\tvideoStream->messages()->listValue(\n\t) | rpl::on_next([=](const std::vector<Message> &messages) {\n\t\tif (_commentsState.current() == CommentsState::Shown\n\t\t\t|| _commentsState.current() == CommentsState::Empty) {\n\t\t\tfor (const auto &message : messages | ranges::views::reverse) {\n\t\t\t\tif (message.id > 0) {\n\t\t\t\t\t_commentsLastId = _commentsLastReadId = message.id;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\t_commentsHas = messages.empty()\n\t\t\t\t? CommentsHas::None\n\t\t\t\t: CommentsHas::AllRead;\n\t\t\treturn;\n\t\t}\n\t\tauto has = false;\n\t\tconst auto from = videoStream->messagesFrom();\n\t\tfor (const auto &message : messages | ranges::views::reverse) {\n\t\t\tif (message.peer != from) {\n\t\t\t\t_commentsLastId = message.id;\n\t\t\t\tif (message.id > _commentsLastReadId) {\n\t\t\t\t\thas = true;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t_commentsHas = messages.empty()\n\t\t\t? CommentsHas::None\n\t\t\t: has\n\t\t\t? CommentsHas::WithUnread\n\t\t\t: CommentsHas::AllRead;\n\t}, _videoStreamLifetime);\n\n\tvideoStream->messages()->hiddenShowRequested(\n\t) | rpl::filter([=] {\n\t\treturn _commentsState.current() != CommentsState::Empty;\n\t}) | rpl::map_to(\n\t\tCommentsState::Shown\n\t) | rpl::start_to_stream(\n\t\t_commentsStateShowFromPinned,\n\t\t_videoStreamLifetime);\n\n\t_starsReactions = rpl::single(Calls::Group::StarsDonor()) | rpl::then(\n\t\tvideoStream->messages()->starsValueChanges()\n\t) | rpl::map([=](const Calls::Group::StarsDonor &donor) {\n\t\tif (const auto peer = donor.peer) {\n\t\t\t_starsReactionEffects.fire({\n\t\t\t\t.from = peer,\n\t\t\t\t.stars = donor.stars,\n\t\t\t});\n\t\t}\n\t\treturn videoStream->messages()->starsLocalState().total;\n\t});\n\t_paidReactionToast->shownForCall(\n\t) | rpl::on_next([=](Calls::GroupCall *call) {\n\t\t_starsReactionHighlighted = (call == videoStream);\n\t}, _videoStreamLifetime);\n\n\t_replyArea->updateVideoStream(videoStream);\n}\n\nvoid Controller::clearVideoStreamCall() {\n\t_videoStreamCall = nullptr;\n\t_starsReactionHighlighted = false;\n\t_starsReactions = 0;\n\t_videoStreamLifetime.destroy();\n}\n\nbool Controller::videoStream() const {\n\treturn _videoStream != nullptr;\n}\n\nrpl::lifetime &Controller::lifetime() {\n\treturn _lifetime;\n}\n\nvoid Controller::updatePowerSaveBlocker(const Player::TrackState &state) {\n\tconst auto block = !Player::IsPausedOrPausing(state.state)\n\t\t&& !Player::IsStoppedOrStopping(state.state);\n\tbase::UpdatePowerSaveBlocker(\n\t\t_powerSaveBlocker,\n\t\tblock,\n\t\tbase::PowerSaveBlockType::PreventDisplaySleep,\n\t\t[] { return u\"Stories playback is active\"_q; },\n\t\t[=] { return _wrap->window()->windowHandle(); });\n}\n\nUi::Toast::Config PrepareToggleInProfileToast(\n\t\tbool channel,\n\t\tint count,\n\t\tbool inProfile) {\n\treturn {\n\t\t.text = (inProfile\n\t\t\t? (count == 1\n\t\t\t\t? (channel\n\t\t\t\t\t? tr::lng_stories_channel_save_done\n\t\t\t\t\t: tr::lng_stories_save_done)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::bold)\n\t\t\t\t: (channel\n\t\t\t\t\t? tr::lng_stories_channel_save_done_many\n\t\t\t\t\t: tr::lng_stories_save_done_many)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tcount,\n\t\t\t\t\t\ttr::bold)).append(\n\t\t\t\t\t\t\t'\\n').append((channel\n\t\t\t\t\t\t\t\t? tr::lng_stories_channel_save_done_about\n\t\t\t\t\t\t\t\t: tr::lng_stories_save_done_about)(tr::now))\n\t\t\t: (count == 1\n\t\t\t\t? (channel\n\t\t\t\t\t? tr::lng_stories_channel_archive_done\n\t\t\t\t\t: tr::lng_stories_archive_done)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::marked)\n\t\t\t\t: (channel\n\t\t\t\t\t? tr::lng_stories_channel_archive_done_many\n\t\t\t\t\t: tr::lng_stories_archive_done_many)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tcount,\n\t\t\t\t\t\ttr::marked))),\n\t\t.st = &st::storiesActionToast,\n\t\t.duration = (inProfile\n\t\t\t? Data::Stories::kInProfileToastDuration\n\t\t\t: Ui::Toast::kDefaultDuration),\n\t};\n}\n\nUi::Toast::Config PrepareTogglePinToast(\n\t\tbool channel,\n\t\tint count,\n\t\tbool pin) {\n\treturn {\n\t\t.title = (pin\n\t\t\t? (count == 1\n\t\t\t\t? tr::lng_mediaview_pin_story_done(tr::now)\n\t\t\t\t: tr::lng_mediaview_pin_stories_done(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tcount))\n\t\t\t: QString()),\n\t\t.text = { (pin\n\t\t\t? (count == 1\n\t\t\t\t? tr::lng_mediaview_pin_story_about(tr::now)\n\t\t\t\t: tr::lng_mediaview_pin_stories_about(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tcount))\n\t\t\t: (count == 1\n\t\t\t\t? tr::lng_mediaview_unpin_story_done(tr::now)\n\t\t\t\t: tr::lng_mediaview_unpin_stories_done(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tcount))) },\n\t\t.st = &st::storiesActionToast,\n\t\t.duration = (pin\n\t\t\t? Data::Stories::kInProfileToastDuration\n\t\t\t: Ui::Toast::kDefaultDuration),\n\t};\n}\n\nvoid ReportRequested(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tFullStoryId id,\n\t\tconst style::ReportBox *stOverride) {\n\tif (const auto maybeStory = show->session().data().stories().lookup(id)) {\n\t\tconst auto story = *maybeStory;\n\t\tconst auto st = stOverride ? stOverride : &st::defaultReportBox;\n\t\t// show->hideLayer();\n\t\tShowReportMessageBox(show, story->peer(), {}, { story->id() }, st);\n\t}\n}\n\nobject_ptr<Ui::BoxContent> PrepareShortInfoBox(not_null<PeerData*> peer) {\n\tconst auto open = [=] {\n\t\tif (const auto window = Core::App().windowFor(peer)) {\n\t\t\twindow->invokeForSessionController(\n\t\t\t\t&peer->session().account(),\n\t\t\t\tpeer,\n\t\t\t\t[&](not_null<Window::SessionController*> controller) {\n\t\t\t\t\tCore::App().hideMediaView();\n\t\t\t\t\tcontroller->showPeerHistory(peer);\n\t\t\t\t});\n\t\t}\n\t};\n\treturn ::PrepareShortInfoBox(\n\t\tpeer,\n\t\topen,\n\t\t[] { return false; },\n\t\tnullptr,\n\t\t&st::storiesShortInfoBox);\n}\n\nClickHandlerPtr MakeChannelPostHandler(\n\t\tnot_null<Main::Session*> session,\n\t\tFullMsgId item) {\n\treturn std::make_shared<LambdaClickHandler>(crl::guard(session, [=] {\n\t\tconst auto peer = session->data().peer(item.peer);\n\t\tif (const auto controller = session->tryResolveWindow(peer)) {\n\t\t\tCore::App().hideMediaView();\n\t\t\tcontroller->showPeerHistory(\n\t\t\t\tpeer,\n\t\t\t\tWindow::SectionShow::Way::ClearStack,\n\t\t\t\titem.msg);\n\t\t}\n\t}));\n}\n\nClickHandlerPtr MakeUrlAreaHandler(\n\t\tbase::weak_ptr<Controller> weak,\n\t\tconst QString &url) {\n\tclass Handler final : public HiddenUrlClickHandler {\n\tpublic:\n\t\tHandler(const QString &url, base::weak_ptr<Controller> weak)\n\t\t: HiddenUrlClickHandler(url), _weak(weak) {\n\t\t}\n\n\t\tvoid onClick(ClickContext context) const override {\n\t\t\tconst auto raw = url();\n\t\t\tconst auto strong = _weak.get();\n\t\t\tconst auto prefix = u\"tg://nft?slug=\"_q;\n\t\t\tif (raw.startsWith(prefix) && strong) {\n\t\t\t\tconst auto slug = raw.mid(\n\t\t\t\t\tprefix.size()\n\t\t\t\t).split('&').front().split('#').front();\n\t\t\t\tCore::ResolveAndShowUniqueGift(\n\t\t\t\t\tstrong->uiShow(),\n\t\t\t\t\tslug,\n\t\t\t\t\t::Settings::DarkCreditsEntryBoxStyle());\n\t\t\t} else {\n\t\t\t\tHiddenUrlClickHandler::onClick(context);\n\t\t\t}\n\t\t}\n\n\tprivate:\n\t\tbase::weak_ptr<Controller> _weak;\n\n\t};\n\treturn std::make_shared<Handler>(url, weak);\n}\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"data/data_stories.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace style {\nstruct ReportBox;\n} // namespace style\n\nnamespace base {\nclass PowerSaveBlocker;\n} // namespace base\n\nnamespace Calls {\nclass GroupCall;\n} // namespace Calls\n\nnamespace ChatHelpers {\nclass Show;\nstruct FileChosen;\n} // namespace ChatHelpers\n\nnamespace Data {\nstruct FileOrigin;\nclass DocumentMedia;\n} // namespace Data\n\nnamespace HistoryView::Controls {\nenum class ToggleCommentsState;\nstruct SendStarButtonEffect;\n} // namespace HistoryView::Controls\n\nnamespace HistoryView::Reactions {\nstruct ChosenReaction;\nenum class AttachSelectorResult;\n} // namespace HistoryView::Reactions\n\nnamespace HistoryView {\nclass PaidReactionToast;\n} // namespace HistoryView\n\nnamespace Ui {\nclass RpWidget;\nclass BoxContent;\nclass PopupMenu;\nstruct SendStarButtonState;\n} // namespace Ui\n\nnamespace Ui::Toast {\nstruct Config;\n} // namespace Ui::Toast\n\nnamespace Main {\nclass Session;\nclass SessionShow;\n} // namespace Main\n\nnamespace Media::Player {\nstruct TrackState;\n} // namespace Media::Player\n\nnamespace Media::Stories {\n\nclass Header;\nclass Slider;\nclass ReplyArea;\nclass Reactions;\nclass RecentViews;\nclass Sibling;\nclass Delegate;\nstruct SiblingView;\nenum class SiblingType;\nstruct ContentLayout;\nclass CaptionFullView;\nclass RepostView;\nenum class ReactionsMode;\nclass StoryAreaView;\nstruct RepostClickHandler;\n\nusing CommentsState = HistoryView::Controls::ToggleCommentsState;\nusing PaidReactionToast = HistoryView::PaidReactionToast;\nusing SendStarButtonEffect = HistoryView::Controls::SendStarButtonEffect;\n\nenum class HeaderLayout {\n\tNormal,\n\tOutside,\n};\n\nenum class PauseState {\n\tPlaying,\n\tPaused,\n\tInactive,\n\tInactivePaused,\n};\n\nstruct SiblingLayout {\n\tQRect geometry;\n\tQRect userpic;\n\tQRect nameBoundingRect;\n\tint nameFontSize = 0;\n\n\tfriend inline bool operator==(SiblingLayout, SiblingLayout) = default;\n};\n\nstruct Layout {\n\tQRect content;\n\tQRect header;\n\tQRect slider;\n\tQRect reactions;\n\tint controlsWidth = 0;\n\tQPoint controlsBottomPosition;\n\tQRect views;\n\tQRect autocompleteRect;\n\tHeaderLayout headerLayout = HeaderLayout::Normal;\n\tSiblingLayout siblingLeft;\n\tSiblingLayout siblingRight;\n\n\tfriend inline bool operator==(Layout, Layout) = default;\n};\n\nclass Controller final : public base::has_weak_ptr {\npublic:\n\texplicit Controller(not_null<Delegate*> delegate);\n\t~Controller();\n\n\t[[nodiscard]] Data::Story *story() const;\n\t[[nodiscard]] not_null<Ui::RpWidget*> wrap() const;\n\t[[nodiscard]] Layout layout() const;\n\t[[nodiscard]] rpl::producer<Layout> layoutValue() const;\n\t[[nodiscard]] ContentLayout contentLayout() const;\n\t[[nodiscard]] bool closeByClickAt(QPoint position) const;\n\t[[nodiscard]] Data::FileOrigin fileOrigin() const;\n\t[[nodiscard]] TextWithEntities captionText() const;\n\t[[nodiscard]] bool videoStream() const;\n\t[[nodiscard]] bool skipCaption() const;\n\t[[nodiscard]] bool repost() const;\n\tvoid toggleLiked();\n\tvoid showFullCaption();\n\tvoid captionClosing();\n\tvoid captionClosed();\n\n\t[[nodiscard]] QMargins repostCaptionPadding() const;\n\tvoid drawRepostInfo(Painter &p, int x, int y, int availableWidth) const;\n\t[[nodiscard]] RepostClickHandler lookupRepostHandler(\n\t\tQPoint position) const;\n\n\t[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() const;\n\t[[nodiscard]] auto stickerOrEmojiChosen() const\n\t-> rpl::producer<ChatHelpers::FileChosen>;\n\n\tvoid show(not_null<Data::Story*> story, Data::StoriesContext context);\n\tvoid jumpTo(not_null<Data::Story*> story, Data::StoriesContext context);\n\tvoid ready();\n\n\tvoid updateVideoPlayback(const Player::TrackState &state);\n\t[[nodiscard]] ClickHandlerPtr lookupAreaHandler(QPoint point) const;\n\n\t[[nodiscard]] bool subjumpAvailable(int delta) const;\n\t[[nodiscard]] bool subjumpFor(int delta);\n\t[[nodiscard]] bool jumpFor(int delta);\n\t[[nodiscard]] bool paused() const;\n\tvoid togglePaused(bool paused);\n\tvoid contentPressed(bool pressed);\n\tvoid setMenuShown(bool shown);\n\n\t[[nodiscard]] PauseState pauseState() const;\n\t[[nodiscard]] float64 currentVolume() const;\n\tvoid toggleVolume();\n\tvoid changeVolume(float64 volume);\n\tvoid volumeChangeFinished();\n\n\tvoid repaint();\n\tvoid repaintSibling(not_null<Sibling*> sibling);\n\t[[nodiscard]] SiblingView sibling(SiblingType type) const;\n\n\t[[nodiscard]] const Data::StoryViews &views(int limit, bool initial);\n\t[[nodiscard]] rpl::producer<> moreViewsLoaded() const;\n\n\t[[nodiscard]] rpl::producer<CommentsState> commentsStateValue() const;\n\tvoid setCommentsShownToggles(rpl::producer<> toggles);\n\t[[nodiscard]] auto starsReactionsValue() const\n\t\t-> rpl::producer<Ui::SendStarButtonState>;\n\t[[nodiscard]] auto starsReactionsEffects() const\n\t\t-> rpl::producer<SendStarButtonEffect>;\n\tvoid setStarsReactionIncrements(rpl::producer<int> increments);\n\n\tvoid unfocusReply();\n\tvoid shareRequested();\n\tvoid deleteRequested();\n\tvoid reportRequested();\n\tvoid toggleInProfileRequested(bool inProfile);\n\n\t[[nodiscard]] bool ignoreWindowMove(QPoint position) const;\n\tvoid tryProcessKeyInput(not_null<QKeyEvent*> e);\n\n\t[[nodiscard]] bool allowStealthMode() const;\n\tvoid setupStealthMode();\n\n\tusing AttachStripResult = HistoryView::Reactions::AttachSelectorResult;\n\t[[nodiscard]] AttachStripResult attachReactionsToMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tQPoint desiredPosition);\n\n\tvoid updateVideoStream(not_null<Calls::GroupCall*> videoStream);\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tclass PhotoPlayback;\n\tclass Unsupported;\n\tusing ChosenReaction = HistoryView::Reactions::ChosenReaction;\n\tstruct StoriesList {\n\t\tnot_null<PeerData*> peer;\n\t\tData::StoriesIds ids;\n\t\tstd::vector<StoryId> sorted;\n\t\tint total = 0;\n\n\t\tfriend inline bool operator==(\n\t\t\tconst StoriesList &,\n\t\t\tconst StoriesList &) = default;\n\t};\n\tstruct CachedSource {\n\t\tPeerId peerId = 0;\n\t\tStoryId shownId = 0;\n\n\t\texplicit operator bool() const {\n\t\t\treturn peerId != 0;\n\t\t}\n\t};\n\tstruct ActiveArea {\n\t\tQRectF original;\n\t\tfloat64 radiusOriginal = 0.;\n\t\tQRect geometry;\n\t\tfloat64 rotation = 0.;\n\t\tfloat64 radius = 0.;\n\t\tClickHandlerPtr handler;\n\t\tstd::unique_ptr<StoryAreaView> view;\n\t};\n\tenum class CommentsHas {\n\t\tNone,\n\t\tAllRead,\n\t\tWithUnread,\n\t};\n\n\tvoid initLayout();\n\tbool changeShown(Data::Story *story);\n\tvoid subscribeToSession();\n\tvoid updatePhotoPlayback(const Player::TrackState &state);\n\tvoid updatePlayback(const Player::TrackState &state);\n\tvoid updatePowerSaveBlocker(const Player::TrackState &state);\n\tvoid maybeMarkAsRead(const Player::TrackState &state);\n\tvoid markAsRead();\n\n\tvoid updateContentFaded();\n\tvoid updatePlayingAllowed();\n\tvoid setPlayingAllowed(bool allowed);\n\tvoid rebuildActiveAreas(const Layout &layout) const;\n\tvoid toggleWeatherMode() const;\n\n\tvoid hideSiblings();\n\tvoid showSiblings(not_null<Main::Session*> session);\n\tvoid showSibling(\n\t\tstd::unique_ptr<Sibling> &sibling,\n\t\tnot_null<Main::Session*> session,\n\t\tCachedSource cached);\n\n\tvoid subjumpTo(int index);\n\tvoid checkWaitingFor();\n\tvoid moveFromShown();\n\n\tvoid refreshViewsFromData();\n\t[[nodiscard]] auto viewsGotMoreCallback()\n\t\t-> Fn<void(Data::StoryViews)>;\n\n\t[[nodiscard]] bool shown() const;\n\t[[nodiscard]] PeerData *shownPeer() const;\n\t[[nodiscard]] int shownCount() const;\n\t[[nodiscard]] StoryId shownId(int index) const;\n\t[[nodiscard]] std::unique_ptr<RepostView> validateRepostView(\n\t\tnot_null<Data::Story*> story);\n\tvoid rebuildFromContext(not_null<PeerData*> peer, FullStoryId storyId);\n\tvoid checkMoveByDelta();\n\tvoid loadMoreToList();\n\tvoid preloadNext();\n\tvoid rebuildCachedSourcesList(\n\t\tconst std::vector<Data::StoriesSourceInfo> &lists,\n\t\tint index);\n\n\t[[nodiscard]] int repostSkipTop() const;\n\tvoid updateAreas(Data::Story *story);\n\tbool reactionChosen(ReactionsMode mode, ChosenReaction chosen);\n\t[[nodiscard]] rpl::producer<int> paidReactionToastTopValue() const;\n\tvoid clearVideoStreamCall();\n\n\tconst not_null<Delegate*> _delegate;\n\n\trpl::variable<std::optional<Layout>> _layout;\n\n\tconst not_null<Ui::RpWidget*> _wrap;\n\tconst std::unique_ptr<Header> _header;\n\tconst std::unique_ptr<Slider> _slider;\n\tconst std::unique_ptr<ReplyArea> _replyArea;\n\tconst std::unique_ptr<Reactions> _reactions;\n\tconst std::unique_ptr<RecentViews> _recentViews;\n\tstd::unique_ptr<Unsupported> _unsupported;\n\tstd::unique_ptr<PhotoPlayback> _photoPlayback;\n\tstd::unique_ptr<CaptionFullView> _captionFullView;\n\tstd::unique_ptr<RepostView> _repostView;\n\n\tstd::shared_ptr<Data::GroupCall> _videoStream;\n\tbase::weak_ptr<Calls::GroupCall> _videoStreamCall;\n\tstd::unique_ptr<PaidReactionToast> _paidReactionToast;\n\trpl::lifetime _videoStreamLifetime;\n\n\tUi::Animations::Simple _contentFadeAnimation;\n\tbool _contentFaded = false;\n\n\tbool _windowActive = false;\n\tbool _replyActive = false;\n\tbool _layerShown = false;\n\tbool _menuShown = false;\n\tbool _tooltipShown = false;\n\tbool _paused = false;\n\n\tFullStoryId _shown;\n\tTextWithEntities _captionText;\n\tData::StoriesContext _context;\n\tstd::optional<Data::StoriesSource> _source;\n\tstd::optional<StoriesList> _list;\n\tFullStoryId _waitingForId;\n\tint _waitingForDelta = 0;\n\tint _index = 0;\n\tint _sliderIndex = 0;\n\tint _sliderCount = 0;\n\tbool _started = false;\n\tbool _viewed = false;\n\n\tstd::vector<Data::StoryLocation> _locations;\n\tstd::vector<Data::SuggestedReaction> _suggestedReactions;\n\tstd::vector<Data::ChannelPost> _channelPosts;\n\tstd::vector<Data::UrlArea> _urlAreas;\n\tstd::vector<Data::WeatherArea> _weatherAreas;\n\tmutable std::vector<ActiveArea> _areas;\n\tmutable rpl::variable<bool> _weatherInCelsius;\n\n\trpl::variable<CommentsState> _commentsState;\n\trpl::event_stream<CommentsState> _commentsStateShowFromPinned;\n\trpl::variable<CommentsHas> _commentsHas;\n\tMsgId _commentsLastReadId = 0;\n\tMsgId _commentsLastId = 0;\n\trpl::variable<int> _starsReactions;\n\trpl::variable<bool> _starsReactionHighlighted;\n\trpl::event_stream<SendStarButtonEffect> _starsReactionEffects;\n\n\tstd::vector<CachedSource> _cachedSourcesList;\n\tint _cachedSourceIndex = -1;\n\tbool _showingUnreadSources = false;\n\n\tData::StoryViews _viewsSlice;\n\trpl::event_stream<> _moreViewsLoaded;\n\tbase::has_weak_ptr _viewsLoadGuard;\n\n\tstd::unique_ptr<Sibling> _siblingLeft;\n\tstd::unique_ptr<Sibling> _siblingRight;\n\n\tstd::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;\n\n\tMain::Session *_session = nullptr;\n\trpl::lifetime _sessionLifetime;\n\n\trpl::lifetime _contextLifetime;\n\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] Ui::Toast::Config PrepareToggleInProfileToast(\n\tbool channel,\n\tint count,\n\tbool inProfile);\n[[nodiscard]] Ui::Toast::Config PrepareTogglePinToast(\n\tbool channel,\n\tint count,\n\tbool pin);\nvoid ReportRequested(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tFullStoryId id,\n\tconst style::ReportBox *stOverride = nullptr);\n[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShortInfoBox(\n\tnot_null<PeerData*> peer);\n[[nodiscard]] ClickHandlerPtr MakeChannelPostHandler(\n\tnot_null<Main::Session*> session,\n\tFullMsgId item);\n[[nodiscard]] ClickHandlerPtr MakeUrlAreaHandler(\n\tbase::weak_ptr<Controller> weak,\n\tconst QString &url);\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_delegate.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/stories/media_stories_delegate.h\"\n\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_delegate.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace ChatHelpers {\nclass Show;\nstruct FileChosen;\n} // namespace ChatHelpers\n\nnamespace Data {\nclass Story;\nstruct StoriesContext;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Media::Stories {\n\nenum class JumpReason {\n\tFinished,\n\tUser,\n};\n\nenum class SiblingType {\n\tLeft,\n\tRight,\n};\n\nclass Delegate {\npublic:\n\t[[nodiscard]] virtual not_null<Ui::RpWidget*> storiesWrap() = 0;\n\t[[nodiscard]] virtual auto storiesShow()\n\t\t-> std::shared_ptr<ChatHelpers::Show> = 0;\n\t[[nodiscard]] virtual auto storiesStickerOrEmojiChosen()\n\t\t-> rpl::producer<ChatHelpers::FileChosen> = 0;\n\tvirtual void storiesRedisplay(not_null<Data::Story*> story) = 0;\n\tvirtual void storiesJumpTo(\n\t\tnot_null<Main::Session*> session,\n\t\tFullStoryId id,\n\t\tData::StoriesContext context) = 0;\n\tvirtual void storiesClose() = 0;\n\t[[nodiscard]] virtual bool storiesPaused() = 0;\n\t[[nodiscard]] virtual rpl::producer<bool> storiesLayerShown() = 0;\n\t[[nodiscard]] virtual float64 storiesSiblingOver(SiblingType type) = 0;\n\tvirtual void storiesTogglePaused(bool paused) = 0;\n\tvirtual void storiesRepaint() = 0;\n\tvirtual void storiesVolumeToggle() = 0;\n\tvirtual void storiesVolumeChanged(float64 volume) = 0;\n\tvirtual void storiesVolumeChangeFinished() = 0;\n\t[[nodiscard]] virtual int storiesTopNotchSkip() = 0;\n};\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_header.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/stories/media_stories_header.h\"\n\n#include \"base/unixtime.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"core/ui_integration.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"media/stories/media_stories_controller.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/layers/box_content.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/custom_emoji_text_badge.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/painter.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_media_view.h\"\n\n#include <QtGui/QGuiApplication>\n\nnamespace Media::Stories {\nnamespace {\n\nconstexpr auto kNameOpacity = 1.;\nconstexpr auto kDateOpacity = 0.8;\nconstexpr auto kControlOpacity = 0.65;\nconstexpr auto kControlOpacityOver = 1.;\nconstexpr auto kControlOpacityDisabled = 0.45;\nconstexpr auto kVolumeHideTimeoutShort = crl::time(20);\nconstexpr auto kVolumeHideTimeoutLong = crl::time(200);\n\nstruct Timestamp {\n\tQString text;\n\tTimeId changes = 0;\n};\n\nstruct PrivacyBadge {\n\tconst style::icon *icon = nullptr;\n\tconst style::color *bg1 = nullptr;\n\tconst style::color *bg2 = nullptr;\n};\n\nclass UserpicBadge final : public Ui::RpWidget {\npublic:\n\tUserpicBadge(not_null<QWidget*> userpic, PrivacyBadge badge);\n\n\t[[nodiscard]] QRect badgeGeometry() const;\n\nprivate:\n\tbool eventFilter(QObject *o, QEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid updateGeometry();\n\n\tconst not_null<QWidget*> _userpic;\n\tconst PrivacyBadge _badgeData;\n\tQRect _badge;\n\tQImage _layer;\n\tbool _grabbing = false;\n\n};\n\n[[nodiscard]] PrivacyBadge LookupPrivacyBadge(Data::StoryPrivacy privacy) {\n\tusing namespace Data;\n\tstatic const auto badges = base::flat_map<StoryPrivacy, PrivacyBadge>{\n\t\t{ StoryPrivacy::CloseFriends, PrivacyBadge{\n\t\t\t&st::storiesBadgeCloseFriends,\n\t\t\t&st::historyPeer2UserpicBg,\n\t\t\t&st::historyPeer2UserpicBg2,\n\t\t} },\n\t\t{ StoryPrivacy::Contacts, PrivacyBadge{\n\t\t\t&st::storiesBadgeContacts,\n\t\t\t&st::historyPeer5UserpicBg,\n\t\t\t&st::historyPeer5UserpicBg2,\n\t\t} },\n\t\t{ StoryPrivacy::SelectedContacts, PrivacyBadge{\n\t\t\t&st::storiesBadgeSelectedContacts,\n\t\t\t&st::historyPeer8UserpicBg,\n\t\t\t&st::historyPeer8UserpicBg2,\n\t\t} },\n\t};\n\tif (const auto i = badges.find(privacy); i != end(badges)) {\n\t\treturn i->second;\n\t}\n\treturn {};\n}\n\nUserpicBadge::UserpicBadge(not_null<QWidget*> userpic, PrivacyBadge badge)\n: RpWidget(userpic->parentWidget())\n, _userpic(userpic)\n, _badgeData(badge) {\n\tuserpic->installEventFilter(this);\n\tupdateGeometry();\n\tsetAttribute(Qt::WA_TransparentForMouseEvents);\n\tUi::PostponeCall(this, [=] {\n\t\t_userpic->raise();\n\t});\n\tshow();\n}\n\nQRect UserpicBadge::badgeGeometry() const {\n\treturn _badge;\n}\n\nbool UserpicBadge::eventFilter(QObject *o, QEvent *e) {\n\tif (o != _userpic) {\n\t\treturn false;\n\t}\n\tconst auto type = e->type();\n\tswitch (type) {\n\tcase QEvent::Move:\n\tcase QEvent::Resize:\n\t\tupdateGeometry();\n\t\treturn false;\n\tcase QEvent::Paint:\n\t\treturn !_grabbing;\n\t}\n\treturn false;\n}\n\nvoid UserpicBadge::paintEvent(QPaintEvent *e) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto layerSize = size() * ratio;\n\tif (_layer.size() != layerSize) {\n\t\t_layer = QImage(layerSize, QImage::Format_ARGB32_Premultiplied);\n\t\t_layer.setDevicePixelRatio(ratio);\n\t}\n\t_layer.fill(Qt::transparent);\n\tauto q = QPainter(&_layer);\n\n\t_grabbing = true;\n\tUi::RenderWidget(q, _userpic);\n\t_grabbing = false;\n\n\tauto hq = PainterHighQualityEnabler(q);\n\tauto pen = st::transparent->p;\n\tpen.setWidthF(st::storiesBadgeOutline);\n\tconst auto half = st::storiesBadgeOutline / 2.;\n\tauto outer = QRectF(_badge).marginsAdded({ half, half, half, half });\n\tauto gradient = QLinearGradient(outer.topLeft(), outer.bottomLeft());\n\tgradient.setStops({\n\t\t{ 0., (*_badgeData.bg1)->c },\n\t\t{ 1., (*_badgeData.bg2)->c },\n\t});\n\tq.setPen(pen);\n\tq.setBrush(gradient);\n\tq.setCompositionMode(QPainter::CompositionMode_Source);\n\tif (style::SquareUserpics()) {\n\tq.drawRect(outer);\n\t} else {\n\tq.drawEllipse(outer);\n\t}\n\tq.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t_badgeData.icon->paintInCenter(q, _badge);\n\tq.end();\n\n\tQPainter(this).drawImage(0, 0, _layer);\n}\n\nvoid UserpicBadge::updateGeometry() {\n\tconst auto width = _userpic->width() + st::storiesBadgeShift.x();\n\tconst auto height = _userpic->height() + st::storiesBadgeShift.y();\n\tsetGeometry(QRect(_userpic->pos(), QSize{ width, height }));\n\tconst auto inner = QRect(QPoint(), _badgeData.icon->size());\n\tconst auto badge = inner.marginsAdded(st::storiesBadgePadding).size();\n\t_badge = QRect(\n\t\tQPoint(width - badge.width(), height - badge.height()),\n\t\tbadge);\n\tupdate();\n}\n\nstruct MadePrivacyBadge {\n\tstd::unique_ptr<Ui::RpWidget> widget;\n\tQRect geometry;\n};\n\n[[nodiscard]] MadePrivacyBadge MakePrivacyBadge(\n\t\tnot_null<QWidget*> userpic,\n\t\tData::StoryPrivacy privacy) {\n\tconst auto badge = LookupPrivacyBadge(privacy);\n\tif (!badge.icon) {\n\t\treturn {};\n\t}\n\tauto widget = std::make_unique<UserpicBadge>(userpic, badge);\n\tconst auto geometry = widget->badgeGeometry();\n\treturn {\n\t\t.widget = std::move(widget),\n\t\t.geometry = geometry,\n\t};\n}\n\n[[nodiscard]] Timestamp ComposeTimestamp(TimeId when, TimeId now) {\n\tconst auto minutes = (now - when) / 60;\n\tif (!minutes) {\n\t\treturn { tr::lng_mediaview_just_now(tr::now), 61 - (now - when) };\n\t} else if (minutes < 60) {\n\t\treturn {\n\t\t\ttr::lng_mediaview_minutes_ago(tr::now, lt_count, minutes),\n\t\t\t61 - ((now - when) % 60),\n\t\t};\n\t}\n\tconst auto hours = (now - when) / 3600;\n\tif (hours < 12) {\n\t\treturn {\n\t\t\ttr::lng_mediaview_hours_ago(tr::now, lt_count, hours),\n\t\t\t3601 - ((now - when) % 3600),\n\t\t};\n\t}\n\tconst auto whenFull = base::unixtime::parse(when);\n\tconst auto nowFull = base::unixtime::parse(now);\n\tconst auto locale = QLocale();\n\tauto tomorrow = nowFull;\n\ttomorrow.setDate(nowFull.date().addDays(1));\n\ttomorrow.setTime(QTime(0, 0, 1));\n\tconst auto seconds = int(nowFull.secsTo(tomorrow));\n\tif (whenFull.date() == nowFull.date()) {\n\t\tconst auto whenTime = locale.toString(\n\t\t\twhenFull.time(),\n\t\t\tQLocale::ShortFormat);\n\t\treturn {\n\t\t\ttr::lng_mediaview_today(tr::now, lt_time, whenTime),\n\t\t\tseconds,\n\t\t};\n\t} else if (whenFull.date().addDays(1) == nowFull.date()) {\n\t\tconst auto whenTime = locale.toString(\n\t\t\twhenFull.time(),\n\t\t\tQLocale::ShortFormat);\n\t\treturn {\n\t\t\ttr::lng_mediaview_yesterday(tr::now, lt_time, whenTime),\n\t\t\tseconds,\n\t\t};\n\t}\n\treturn { Ui::FormatDateTime(whenFull) };\n}\n\n[[nodiscard]] QString ComposeCounter(HeaderData data) {\n\tconst auto index = data.fullIndex + 1;\n\tconst auto count = data.fullCount;\n\treturn count\n\t\t? QString::fromUtf8(\" %3 %1/%2\")\n\t\t\t.arg(index)\n\t\t\t.arg(count)\n\t\t\t.arg(Ui::kQBullet)\n\t\t: QString();\n}\n\n[[nodiscard]] Timestamp ComposeDetails(HeaderData data, TimeId now) {\n\tauto result = ComposeTimestamp(data.date, now);\n\tif (data.edited) {\n\t\tresult.text.append(' '\n\t\t\t+ Ui::kQBullet\n\t\t\t+ ' '\n\t\t\t+ tr::lng_edited(tr::now));\n\t}\n\tif (data.fromPeer || !data.repostFrom.isEmpty()) {\n\t\tresult.text = Ui::kQBullet + ' ' + result.text;\n\t}\n\treturn result;\n}\n\n[[nodiscard]] TextWithEntities FromNameValue(not_null<PeerData*> from) {\n\tauto result = Ui::Text::SingleCustomEmoji(\n\t\tfrom->owner().customEmojiManager().peerUserpicEmojiData(\n\t\t\tfrom,\n\t\t\tst::storiesRepostUserpicPadding));\n\tresult.append(from->name());\n\treturn tr::link(result);\n}\n\n[[nodiscard]] TextWithEntities RepostNameValue(\n\t\tnot_null<Data::Session*> owner,\n\t\tPeerData *peer,\n\t\tQString name) {\n\tauto result = Ui::Text::IconEmoji(&st::storiesRepostIcon);\n\tif (peer) {\n\t\tresult.append(Ui::Text::SingleCustomEmoji(\n\t\t\towner->customEmojiManager().peerUserpicEmojiData(\n\t\t\t\tpeer,\n\t\t\t\tst::storiesRepostUserpicPadding)));\n\t}\n\tresult.append(name);\n\treturn tr::link(result);\n}\n\n} // namespace\n\nHeader::Header(not_null<Controller*> controller)\n: _controller(controller)\n, _dateUpdateTimer([=] { updateDateText(); }) {\n}\n\nHeader::~Header() = default;\n\nvoid Header::show(HeaderData data, rpl::producer<int> videoStreamViewers) {\n\tif (_data == data) {\n\t\tsetVideoStreamViewers(std::move(videoStreamViewers));\n\t\treturn;\n\t}\n\tconst auto peerChanged = !_data || (_data->peer != data.peer);\n\t_data = data;\n\tconst auto updateInfoGeometry = [=] {\n\t\tif (_name && _date) {\n\t\t\tconst auto namex = st::storiesHeaderNamePosition.x();\n\t\t\tconst auto namer = namex + _name->width();\n\t\t\tconst auto datex = st::storiesHeaderDatePosition.x();\n\t\t\tconst auto dater = datex\n\t\t\t\t+ (_repost ? _repost->width() : 0)\n\t\t\t\t+ _date->width();\n\t\t\tconst auto r = std::max(namer, dater);\n\t\t\t_info->setGeometry({ 0, 0, r, _widget->height() });\n\t\t}\n\t};\n\t_tooltip = nullptr;\n\t_tooltipShown = false;\n\tif (peerChanged) {\n\t\t_volume = nullptr;\n\t\t_date = nullptr;\n\t\t_repost = nullptr;\n\t\t_name = nullptr;\n\t\t_counter = nullptr;\n\t\t_userpic = nullptr;\n\t\t_info = nullptr;\n\t\t_privacy = nullptr;\n\t\t_playPause = nullptr;\n\t\t_volumeToggle = nullptr;\n\t\tconst auto parent = _controller->wrap();\n\t\tauto widget = std::make_unique<Ui::RpWidget>(parent);\n\t\tconst auto raw = widget.get();\n\n\t\t_info = std::make_unique<Ui::AbstractButton>(raw);\n\t\t_info->setClickedCallback([=] {\n\t\t\t_controller->uiShow()->show(PrepareShortInfoBox(_data->peer));\n\t\t});\n\n\t\t_userpic = std::make_unique<Ui::UserpicButton>(\n\t\t\traw,\n\t\t\tdata.peer,\n\t\t\tst::storiesHeaderPhoto);\n\t\t_userpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t_userpic->show();\n\t\t_userpic->move(\n\t\t\tst::storiesHeaderMargin.left(),\n\t\t\tst::storiesHeaderMargin.top());\n\n\t\t_name = std::make_unique<Ui::FlatLabel>(\n\t\t\traw,\n\t\t\trpl::single(data.peer->isSelf()\n\t\t\t\t? tr::lng_stories_my_name(tr::now)\n\t\t\t\t: data.peer->name()),\n\t\t\tst::storiesHeaderName);\n\t\t_name->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t_name->setOpacity(kNameOpacity);\n\t\t_name->show();\n\t\t_name->move(st::storiesHeaderNamePosition);\n\n\t\trpl::combine(\n\t\t\t_name->widthValue(),\n\t\t\traw->heightValue()\n\t\t) | rpl::on_next(updateInfoGeometry, _name->lifetime());\n\n\t\traw->show();\n\t\t_widget = std::move(widget);\n\n\t\t_controller->layoutValue(\n\t\t) | rpl::on_next([=](const Layout &layout) {\n\t\t\traw->setGeometry(layout.header);\n\t\t\t_contentGeometry = layout.content;\n\t\t\tupdateTooltipGeometry();\n\t\t}, raw->lifetime());\n\t}\n\tauto timestamp = ComposeDetails(data, base::unixtime::now());\n\t_date = std::make_unique<Ui::FlatLabel>(\n\t\t_widget.get(),\n\t\tstd::move(timestamp.text),\n\t\tst::storiesHeaderDate);\n\t_date->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_date->show();\n\t_date->move(st::storiesHeaderDatePosition);\n\tsetVideoStreamViewers(std::move(videoStreamViewers));\n\n\t_date->widthValue(\n\t) | rpl::on_next(updateInfoGeometry, _date->lifetime());\n\n\tif (!data.fromPeer && data.repostFrom.isEmpty()) {\n\t\t_repost = nullptr;\n\t} else {\n\t\t_repost = std::make_unique<Ui::FlatLabel>(\n\t\t\t_widget.get(),\n\t\t\tst::storiesHeaderDate);\n\t\tconst auto prefixName = data.fromPeer\n\t\t\t? FromNameValue(data.fromPeer)\n\t\t\t: RepostNameValue(\n\t\t\t\t&data.peer->owner(),\n\t\t\t\tdata.repostPeer,\n\t\t\t\tdata.repostFrom);\n\t\tconst auto prefix = data.fromPeer ? data.fromPeer : data.repostPeer;\n\t\t_repost->setMarkedText(\n\t\t\t(prefix ? tr::link(prefixName) : prefixName),\n\t\t\tCore::TextContext({ .session = &data.peer->session() }));\n\t\tif (prefix) {\n\t\t\t_repost->setClickHandlerFilter([=](const auto &...) {\n\t\t\t\t_controller->uiShow()->show(PrepareShortInfoBox(prefix));\n\t\t\t\treturn false;\n\t\t\t});\n\t\t}\n\t\t_repost->show();\n\t\t_repost->widthValue(\n\t\t) | rpl::on_next(updateInfoGeometry, _repost->lifetime());\n\t}\n\n\tauto counter = ComposeCounter(data);\n\tif (!counter.isEmpty()) {\n\t\t_counter = std::make_unique<Ui::FlatLabel>(\n\t\t\t_widget.get(),\n\t\t\tstd::move(counter),\n\t\t\tst::storiesHeaderDate);\n\t\t_counter->resizeToWidth(_counter->textMaxWidth());\n\t\t_counter->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t_counter->setOpacity(kNameOpacity);\n\t\t_counter->show();\n\t} else {\n\t\t_counter = nullptr;\n\t}\n\n\tauto made = MakePrivacyBadge(_userpic.get(), data.privacy);\n\t_privacy = std::move(made.widget);\n\t_privacyBadgeOver = false;\n\t_privacyBadgeGeometry = _privacy\n\t\t? Ui::MapFrom(_info.get(), _privacy.get(), made.geometry)\n\t\t: QRect();\n\tif (_privacy) {\n\t\t_info->setMouseTracking(true);\n\t\t_info->events(\n\t\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\t\tconst auto type = e->type();\n\t\t\tif (type != QEvent::Leave && type != QEvent::MouseMove) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto over = (type == QEvent::MouseMove)\n\t\t\t\t&& _privacyBadgeGeometry.contains(\n\t\t\t\t\tstatic_cast<QMouseEvent*>(e.get())->pos());\n\t\t\treturn (_privacyBadgeOver != over);\n\t\t}) | rpl::on_next([=] {\n\t\t\t_privacyBadgeOver = !_privacyBadgeOver;\n\t\t\ttoggleTooltip(Tooltip::Privacy, _privacyBadgeOver);\n\t\t}, _privacy->lifetime());\n\t}\n\n\tif (data.video || data.videoStream) {\n\t\tif (data.video) {\n\t\t\tcreatePlayPause();\n\t\t}\n\t\tcreateVolumeToggle();\n\n\t\t_widget->widthValue() | rpl::on_next([=](int width) {\n\t\t\tif (_playPause) {\n\t\t\t\tconst auto playPause = st::storiesPlayButtonPosition;\n\t\t\t\t_playPause->moveToRight(playPause.x(), playPause.y(), width);\n\t\t\t}\n\t\t\tconst auto volume = st::storiesVolumeButtonPosition;\n\t\t\t_volumeToggle->moveToRight(volume.x(), volume.y(), width);\n\t\t\tupdateTooltipGeometry();\n\t\t}, _volumeToggle->lifetime());\n\n\t\tif (data.video) {\n\t\t\t_pauseState = _controller->pauseState();\n\t\t\tapplyPauseState();\n\t\t}\n\t} else {\n\t\t_playPause = nullptr;\n\t\t_volumeToggle = nullptr;\n\t\t_volume = nullptr;\n\t}\n\n\trpl::combine(\n\t\t_widget->widthValue(),\n\t\t_counter ? _counter->widthValue() : rpl::single(0),\n\t\t_dateUpdated.events_starting_with_copy(rpl::empty)\n\t) | rpl::on_next([=](int outer, int counter, auto) {\n\t\tconst auto right = _playPause\n\t\t\t? _playPause->x()\n\t\t\t: (outer - st::storiesHeaderMargin.right());\n\t\tconst auto nameLeft = st::storiesHeaderNamePosition.x();\n\t\tif (counter) {\n\t\t\tcounter += st::normalFont->spacew;\n\t\t}\n\t\tconst auto nameAvailable = right - nameLeft - counter;\n\t\tauto counterLeft = nameLeft;\n\t\tif (nameAvailable <= 0) {\n\t\t\t_name->hide();\n\t\t} else {\n\t\t\t_name->show();\n\t\t\t_name->resizeToNaturalWidth(nameAvailable);\n\t\t\tcounterLeft += _name->width() + st::normalFont->spacew;\n\t\t}\n\t\tif (_counter) {\n\t\t\t_counter->move(counterLeft, _name->y());\n\t\t}\n\t\tconst auto dateLeft = st::storiesHeaderDatePosition.x();\n\t\tconst auto dateTop = st::storiesHeaderDatePosition.y();\n\t\tconst auto dateSkip = _repost ? st::storiesHeaderRepostWidthMin : 0;\n\t\tconst auto dateAvailable = right - dateLeft - dateSkip;\n\t\tif (dateAvailable <= 0) {\n\t\t\t_date->hide();\n\t\t} else {\n\t\t\t_date->show();\n\t\t\t_date->resizeToNaturalWidth(dateAvailable);\n\t\t}\n\t\tif (_repost) {\n\t\t\tconst auto repostAvailable = dateAvailable\n\t\t\t\t+ dateSkip\n\t\t\t\t- _date->width();\n\t\t\tif (repostAvailable <= 0) {\n\t\t\t\t_repost->hide();\n\t\t\t} else {\n\t\t\t\t_repost->show();\n\t\t\t\t_repost->resizeToNaturalWidth(repostAvailable);\n\t\t\t}\n\t\t\t_repost->move(dateLeft, dateTop);\n\t\t\tconst auto space = st::normalFont->spacew;\n\t\t\t_date->move(dateLeft + _repost->width() + space, dateTop);\n\t\t} else {\n\t\t\t_date->move(dateLeft, dateTop);\n\t\t}\n\t}, _date->lifetime());\n\n\tif (timestamp.changes > 0) {\n\t\t_dateUpdateTimer.callOnce(timestamp.changes * crl::time(1000));\n\t}\n}\n\nvoid Header::setVideoStreamViewers(rpl::producer<int> viewers) {\n\t_videoStreamViewersLifetime.destroy();\n\tif (!_date) {\n\t\treturn;\n\t} else if (!viewers) {\n\t\t_date->setOpacity(kDateOpacity);\n\t\treturn;\n\t}\n\t_date->setOpacity(1.);\n\tauto helper = Ui::Text::CustomEmojiHelper();\n\tconst auto badge = helper.paletteDependent(\n\t\tUi::Text::CustomEmojiTextBadge(\n\t\t\ttr::lng_video_stream_live(tr::now).toUpper(),\n\t\t\tst::groupCallMessageBadge,\n\t\t\tst::groupCallMessageBadgeMargin));\n\tconst auto context = helper.context();\n\t_videoStreamViewersLifetime = std::move(\n\t\tviewers\n\t) | rpl::on_next([=](int count) {\n\t\tauto text = badge;\n\t\tif (count) {\n\t\t\ttext.append(' ').append(tr::lng_group_call_rtmp_viewers(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\tcount));\n\t\t}\n\t\t_date->setMarkedText(text, context);\n\t\t_dateUpdated.fire({});\n\t});\n}\n\nvoid Header::createPlayPause() {\n\tstruct PlayPauseState {\n\t\tUi::Animations::Simple overAnimation;\n\t\tbool over = false;\n\t\tbool down = false;\n\t};\n\t_playPause = std::make_unique<Ui::RpWidget>(_widget.get());\n\tauto &lifetime = _playPause->lifetime();\n\tconst auto state = lifetime.make_state<PlayPauseState>();\n\n\t_playPause->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::Enter || type == QEvent::Leave) {\n\t\t\tconst auto over = (e->type() == QEvent::Enter);\n\t\t\tif (state->over != over) {\n\t\t\t\tstate->over = over;\n\t\t\t\tstate->overAnimation.start(\n\t\t\t\t\t[=] { _playPause->update(); },\n\t\t\t\t\tover ? 0. : 1.,\n\t\t\t\t\tover ? 1. : 0.,\n\t\t\t\t\tst::mediaviewFadeDuration);\n\t\t\t}\n\t\t} else if (type == QEvent::MouseButtonPress && state->over) {\n\t\t\tstate->down = true;\n\t\t} else if (type == QEvent::MouseButtonRelease) {\n\t\t\tconst auto down = base::take(state->down);\n\t\t\tif (down && state->over) {\n\t\t\t\tconst auto paused = (_pauseState == PauseState::Paused)\n\t\t\t\t\t|| (_pauseState == PauseState::InactivePaused);\n\t\t\t\t_controller->togglePaused(!paused);\n\t\t\t}\n\t\t}\n\t}, lifetime);\n\n\t_playPause->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(_playPause.get());\n\t\tconst auto paused = (_pauseState == PauseState::Paused)\n\t\t\t|| (_pauseState == PauseState::InactivePaused);\n\t\tconst auto icon = paused\n\t\t\t? &st::storiesPlayIcon\n\t\t\t: &st::storiesPauseIcon;\n\t\tconst auto over = state->overAnimation.value(\n\t\t\tstate->over ? 1. : 0.);\n\t\tp.setOpacity(over * kControlOpacityOver\n\t\t\t+ (1. - over) * kControlOpacity);\n\t\ticon->paint(\n\t\t\tp,\n\t\t\tst::storiesPlayButton.iconPosition,\n\t\t\t_playPause->width());\n\t}, lifetime);\n\n\t_playPause->resize(\n\t\tst::storiesPlayButton.width,\n\t\tst::storiesPlayButton.height);\n\t_playPause->show();\n\t_playPause->setCursor(style::cur_pointer);\n}\n\nvoid Header::createVolumeToggle() {\n\tExpects(_data.has_value());\n\n\tstruct VolumeState {\n\t\tbase::Timer hideTimer;\n\t\tbool over = false;\n\t\tbool silent = false;\n\t\tbool dropdownOver = false;\n\t};\n\t_volumeToggle = std::make_unique<Ui::RpWidget>(_widget.get());\n\t_volume = std::make_unique<Ui::FadeWrap<Ui::RpWidget>>(\n\t\t_widget->parentWidget(),\n\t\tobject_ptr<Ui::RpWidget>(_widget->parentWidget()));\n\n\tauto &lifetime = _volume->lifetime();\n\tconst auto state = lifetime.make_state<VolumeState>();\n\tstate->silent = _data->silent;\n\tstate->hideTimer.setCallback([=] {\n\t\t_volume->toggle(false, anim::type::normal);\n\t});\n\n\t_volumeToggle->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::Enter || type == QEvent::Leave) {\n\t\t\tconst auto over = (e->type() == QEvent::Enter);\n\t\t\tif (state->over != over) {\n\t\t\t\tstate->over = over;\n\t\t\t\tif (state->silent) {\n\t\t\t\t\ttoggleTooltip(Tooltip::SilentVideo, over);\n\t\t\t\t} else if (over) {\n\t\t\t\t\tstate->hideTimer.cancel();\n\t\t\t\t\t_volume->toggle(true, anim::type::normal);\n\t\t\t\t} else if (!state->dropdownOver) {\n\t\t\t\t\tstate->hideTimer.callOnce(kVolumeHideTimeoutShort);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, lifetime);\n\n\t_volumeToggle->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(_volumeToggle.get());\n\t\tp.setOpacity(state->silent\n\t\t\t? kControlOpacityDisabled\n\t\t\t: kControlOpacity);\n\t\t_volumeIcon.current()->paint(\n\t\t\tp,\n\t\t\tst::storiesVolumeButton.iconPosition,\n\t\t\t_volumeToggle->width());\n\t}, lifetime);\n\tupdateVolumeIcon();\n\n\t_volume->toggle(false, anim::type::instant);\n\t_volume->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::Enter || type == QEvent::Leave) {\n\t\t\tconst auto over = (e->type() == QEvent::Enter);\n\t\t\tif (state->dropdownOver != over) {\n\t\t\t\tstate->dropdownOver = over;\n\t\t\t\tif (over) {\n\t\t\t\t\tstate->hideTimer.cancel();\n\t\t\t\t\t_volume->toggle(true, anim::type::normal);\n\t\t\t\t} else if (!state->over) {\n\t\t\t\t\tstate->hideTimer.callOnce(kVolumeHideTimeoutLong);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, lifetime);\n\trebuildVolumeControls(_volume->entity(), false);\n\n\trpl::combine(\n\t\t_widget->positionValue(),\n\t\t_volumeToggle->positionValue(),\n\t\trpl::mappers::_1 + rpl::mappers::_2\n\t) | rpl::on_next([=](QPoint position) {\n\t\t_volume->move(position);\n\t}, _volume->lifetime());\n\n\t_volumeToggle->resize(\n\t\tst::storiesVolumeButton.width,\n\t\tst::storiesVolumeButton.height);\n\t_volumeToggle->show();\n\tif (!state->silent) {\n\t\t_volumeToggle->setCursor(style::cur_pointer);\n\t}\n}\n\nvoid Header::toggleTooltip(Tooltip type, bool show) {\n\tconst auto guard = gsl::finally([&] {\n\t\t_tooltipShown = (_tooltip != nullptr);\n\t});\n\tif (const auto was = _tooltip.release()) {\n\t\twas->toggleAnimated(false);\n\t}\n\tif (!show) {\n\t\treturn;\n\t}\n\tconst auto text = [&]() -> TextWithEntities {\n\t\tusing Privacy = Data::StoryPrivacy;\n\t\tconst auto boldName = tr::bold(_data->peer->shortName());\n\t\tconst auto self = _data->peer->isSelf();\n\t\tswitch (type) {\n\t\tcase Tooltip::SilentVideo:\n\t\t\treturn { tr::lng_stories_about_silent(tr::now) };\n\t\tcase Tooltip::Privacy: switch (_data->privacy) {\n\t\t\tcase Privacy::CloseFriends:\n\t\t\t\treturn self\n\t\t\t\t\t? tr::lng_stories_about_close_friends_my(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: tr::lng_stories_about_close_friends(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\tboldName,\n\t\t\t\t\t\ttr::rich);\n\t\t\tcase Privacy::Contacts:\n\t\t\t\treturn self\n\t\t\t\t\t? tr::lng_stories_about_contacts_my(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: tr::lng_stories_about_contacts(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\tboldName,\n\t\t\t\t\t\ttr::rich);\n\t\t\tcase Privacy::SelectedContacts:\n\t\t\t\treturn self\n\t\t\t\t\t? tr::lng_stories_about_selected_contacts_my(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: tr::lng_stories_about_selected_contacts(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\tboldName,\n\t\t\t\t\t\ttr::rich);\n\t\t\t}\n\t\t}\n\t\treturn {};\n\t}();\n\tif (text.empty()) {\n\t\treturn;\n\t}\n\t_tooltipType = type;\n\t_tooltip = std::make_unique<Ui::ImportantTooltip>(\n\t\t_widget->parentWidget(),\n\t\tUi::MakeNiceTooltipLabel(\n\t\t\t_widget.get(),\n\t\t\trpl::single(text),\n\t\t\tst::storiesInfoTooltipMaxWidth,\n\t\t\tst::storiesInfoTooltipLabel),\n\t\tst::storiesInfoTooltip);\n\tconst auto tooltip = _tooltip.get();\n\tconst auto weak = base::make_weak(tooltip);\n\tconst auto destroy = [=] {\n\t\tdelete weak.get();\n\t};\n\ttooltip->setAttribute(Qt::WA_TransparentForMouseEvents);\n\ttooltip->setHiddenCallback(destroy);\n\tupdateTooltipGeometry();\n\ttooltip->toggleAnimated(true);\n}\n\nvoid Header::updateTooltipGeometry() {\n\tif (!_tooltip) {\n\t\treturn;\n\t}\n\tconst auto geometry = [&] {\n\t\tswitch (_tooltipType) {\n\t\tcase Tooltip::SilentVideo:\n\t\t\treturn Ui::MapFrom(\n\t\t\t\t_widget->parentWidget(),\n\t\t\t\t_volumeToggle.get(),\n\t\t\t\t_volumeToggle->rect());\n\t\tcase Tooltip::Privacy:\n\t\t\treturn Ui::MapFrom(\n\t\t\t\t_widget->parentWidget(),\n\t\t\t\t_info.get(),\n\t\t\t\t_privacyBadgeGeometry.marginsAdded(\n\t\t\t\t\tst::storiesInfoTooltip.padding));\n\t\t}\n\t\treturn QRect();\n\t}();\n\tif (geometry.isEmpty()) {\n\t\ttoggleTooltip(Tooltip::None, false);\n\t\treturn;\n\t}\n\tconst auto weak = QPointer<QWidget>(_tooltip.get());\n\tconst auto countPosition = [=](QSize size) {\n\t\tconst auto result = geometry.bottomLeft()\n\t\t\t- QPoint(size.width() / 2, 0);\n\t\tconst auto inner = _contentGeometry.marginsRemoved(\n\t\t\tst::storiesInfoTooltip.padding);\n\t\tif (size.width() > inner.width()) {\n\t\t\treturn QPoint(\n\t\t\t\tinner.x() + (inner.width() - size.width()) / 2,\n\t\t\t\tresult.y());\n\t\t} else if (result.x() < inner.x()) {\n\t\t\treturn QPoint(inner.x(), result.y());\n\t\t}\n\t\treturn result;\n\t};\n\t_tooltip->pointAt(geometry, RectPart::Bottom, countPosition);\n}\n\nvoid Header::rebuildVolumeControls(\n\t\tnot_null<Ui::RpWidget*> dropdown,\n\t\tbool horizontal) {\n\tauto removed = false;\n\tdo {\n\t\tremoved = false;\n\t\tfor (const auto &child : dropdown->children()) {\n\t\t\tif (child->isWidgetType()) {\n\t\t\t\tremoved = true;\n\t\t\t\tdelete child;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} while (removed);\n\n\tconst auto button = Ui::CreateChild<Ui::IconButton>(\n\t\tdropdown.get(),\n\t\tst::storiesVolumeButton);\n\t_volumeIcon.value(\n\t) | rpl::on_next([=](const style::icon *icon) {\n\t\tbutton->setIconOverride(icon, icon);\n\t}, button->lifetime());\n\n\tconst auto slider = Ui::CreateChild<Ui::MediaSlider>(\n\t\tdropdown.get(),\n\t\tst::storiesVolumeSlider);\n\tslider->setMoveByWheel(true);\n\tslider->setAlwaysDisplayMarker(true);\n\tusing Direction = Ui::MediaSlider::Direction;\n\tslider->setDirection(horizontal\n\t\t? Direction::Horizontal\n\t\t: Direction::Vertical);\n\n\tslider->setChangeProgressCallback([=](float64 value) {\n\t\t_ignoreWindowMove = true;\n\t\t_controller->changeVolume(value);\n\t\tupdateVolumeIcon();\n\t});\n\tslider->setChangeFinishedCallback([=](float64 value) {\n\t\t_ignoreWindowMove = false;\n\t\t_controller->volumeChangeFinished();\n\t});\n\tbutton->setClickedCallback([=] {\n\t\t_controller->toggleVolume();\n\t\tslider->setValue(_controller->currentVolume());\n\t\tupdateVolumeIcon();\n\t});\n\tslider->setValue(_controller->currentVolume());\n\n\tconst auto size = button->width()\n\t\t+ st::storiesVolumeSize\n\t\t+ st::storiesVolumeBottom;\n\tconst auto seekSize = st::storiesVolumeSlider.seekSize;\n\n\tbutton->move(0, 0);\n\tif (horizontal) {\n\t\tdropdown->resize(size, button->height());\n\t\tslider->resize(st::storiesVolumeSize, seekSize.height());\n\t\tslider->move(\n\t\t\tbutton->width(),\n\t\t\t(button->height() - slider->height()) / 2);\n\t} else {\n\t\tdropdown->resize(button->width(), size);\n\t\tslider->resize(seekSize.width(), st::storiesVolumeSize);\n\t\tslider->move(\n\t\t\t(button->width() - slider->width()) / 2,\n\t\t\tbutton->height());\n\t}\n\n\tdropdown->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(dropdown);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto radius = button->width() / 2.;\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::mediaviewSaveMsgBg);\n\t\tp.drawRoundedRect(dropdown->rect(), radius, radius);\n\t}, button->lifetime());\n}\n\nvoid Header::updatePauseState() {\n\tif (!_playPause) {\n\t\treturn;\n\t} else if (const auto s = _controller->pauseState(); _pauseState != s) {\n\t\t_pauseState = s;\n\t\tapplyPauseState();\n\t}\n}\n\nvoid Header::updateVolumeIcon() {\n\tconst auto volume = _controller->currentVolume();\n\t_volumeIcon = (volume <= 0. || (_data && _data->silent))\n\t\t? &st::mediaviewVolumeIcon0Over\n\t\t: (volume < 1 / 2.)\n\t\t? &st::mediaviewVolumeIcon1Over\n\t\t: &st::mediaviewVolumeIcon2Over;\n}\n\nvoid Header::applyPauseState() {\n\tExpects(_playPause != nullptr);\n\n\tconst auto inactive = (_pauseState == PauseState::Inactive)\n\t\t|| (_pauseState == PauseState::InactivePaused);\n\t_playPause->setAttribute(Qt::WA_TransparentForMouseEvents, inactive);\n\tif (inactive) {\n\t\tQEvent e(QEvent::Leave);\n\t\tQGuiApplication::sendEvent(_playPause.get(), &e);\n\t}\n\t_playPause->update();\n}\n\nvoid Header::raise() {\n\tif (_widget) {\n\t\t_widget->raise();\n\t}\n}\n\nbool Header::ignoreWindowMove(QPoint position) const {\n\treturn _ignoreWindowMove;\n}\n\nrpl::producer<bool> Header::tooltipShownValue() const {\n\treturn _tooltipShown.value();\n}\n\nvoid Header::updateDateText() {\n\tif (!_date || !_data || !_data->date || _videoStreamViewersLifetime) {\n\t\treturn;\n\t}\n\tauto timestamp = ComposeDetails(*_data, base::unixtime::now());\n\t_date->setText(timestamp.text);\n\t_dateUpdated.fire({});\n\tif (timestamp.changes > 0) {\n\t\t_dateUpdateTimer.callOnce(timestamp.changes * crl::time(1000));\n\t}\n}\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_header.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"ui/userpic_view.h\"\n\nnamespace Data {\nenum class StoryPrivacy : uchar;\n} // namespace Data\n\nnamespace Ui {\nclass RpWidget;\nclass FlatLabel;\nclass IconButton;\nclass AbstractButton;\nclass UserpicButton;\nclass ImportantTooltip;\ntemplate <typename Widget>\nclass FadeWrap;\n} // namespace Ui\n\nnamespace Media::Stories {\n\nclass Controller;\nenum class PauseState;\n\nstruct HeaderData {\n\tnot_null<PeerData*> peer;\n\tPeerData *fromPeer = nullptr;\n\tPeerData *repostPeer = nullptr;\n\tQString repostFrom;\n\tTimeId date = 0;\n\tint fullIndex = 0;\n\tint fullCount = 0;\n\tData::StoryPrivacy privacy = {};\n\tbool edited = false;\n\tbool video = false;\n\tbool videoStream = false;\n\tbool silent = false;\n\n\tfriend inline auto operator<=>(HeaderData, HeaderData) = default;\n\tfriend inline bool operator==(HeaderData, HeaderData) = default;\n};\n\nclass Header final {\npublic:\n\texplicit Header(not_null<Controller*> controller);\n\t~Header();\n\n\tvoid updatePauseState();\n\tvoid updateVolumeIcon();\n\n\tvoid show(HeaderData data, rpl::producer<int> videoStreamViewers);\n\tvoid raise();\n\n\t[[nodiscard]] bool ignoreWindowMove(QPoint position) const;\n\t[[nodiscard]] rpl::producer<bool> tooltipShownValue() const;\n\nprivate:\n\tenum class Tooltip {\n\t\tNone,\n\t\tSilentVideo,\n\t\tPrivacy,\n\t};\n\n\tvoid updateDateText();\n\tvoid applyPauseState();\n\tvoid createPlayPause();\n\tvoid createVolumeToggle();\n\tvoid rebuildVolumeControls(\n\t\tnot_null<Ui::RpWidget*> dropdown,\n\t\tbool horizontal);\n\tvoid toggleTooltip(Tooltip type, bool show);\n\tvoid updateTooltipGeometry();\n\tvoid setVideoStreamViewers(rpl::producer<int> viewers);\n\n\tconst not_null<Controller*> _controller;\n\n\tPauseState _pauseState = {};\n\n\tstd::unique_ptr<Ui::RpWidget> _widget;\n\tstd::unique_ptr<Ui::AbstractButton> _info;\n\tstd::unique_ptr<Ui::UserpicButton> _userpic;\n\tstd::unique_ptr<Ui::FlatLabel> _name;\n\tstd::unique_ptr<Ui::FlatLabel> _counter;\n\tstd::unique_ptr<Ui::FlatLabel> _repost;\n\tstd::unique_ptr<Ui::FlatLabel> _date;\n\trpl::event_stream<> _dateUpdated;\n\tstd::unique_ptr<Ui::RpWidget> _playPause;\n\tstd::unique_ptr<Ui::RpWidget> _volumeToggle;\n\tstd::unique_ptr<Ui::FadeWrap<Ui::RpWidget>> _volume;\n\trpl::variable<const style::icon*> _volumeIcon;\n\tstd::unique_ptr<Ui::RpWidget> _privacy;\n\tQRect _privacyBadgeGeometry;\n\tstd::optional<HeaderData> _data;\n\tstd::unique_ptr<Ui::ImportantTooltip> _tooltip;\n\trpl::variable<bool> _tooltipShown = false;\n\tQRect _contentGeometry;\n\tTooltip _tooltipType = {};\n\tbase::Timer _dateUpdateTimer;\n\tbool _ignoreWindowMove = false;\n\tbool _privacyBadgeOver = false;\n\n\trpl::lifetime _videoStreamViewersLifetime;\n\n};\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_reactions.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/stories/media_stories_reactions.h\"\n\n#include \"base/event_filter.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"history/admin_log/history_admin_log_item.h\"\n#include \"history/view/media/history_view_custom_emoji.h\"\n#include \"history/view/media/history_view_media_unwrapped.h\"\n#include \"history/view/media/history_view_sticker_player.h\"\n#include \"history/view/reactions/history_view_reactions_selector.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history_item_reply_markup.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n#include \"media/stories/media_stories_controller.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/emoji_fly_animation.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/effects/reaction_fly_animation.h\"\n#include \"ui/text/text_isolated_emoji.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/animated_icon.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_media_view.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_window.h\"\n\nnamespace Media::Stories {\nnamespace {\n\nconstexpr auto kReactionScaleOutTarget = 0.7;\nconstexpr auto kReactionScaleOutDuration = crl::time(1000);\nconstexpr auto kMessageReactionScaleOutDuration = crl::time(400);\nconstexpr auto kSuggestedBubbleSize = 1.0;\nconstexpr auto kSuggestedTailBigSize = 0.264;\nconstexpr auto kSuggestedTailBigOffset = 0.464;\nconstexpr auto kSuggestedTailSmallSize = 0.110;\nconstexpr auto kSuggestedTailSmallOffset = 0.697;\nconstexpr auto kSuggestedTailBigRotation = -42.29;\nconstexpr auto kSuggestedTailSmallRotation = -40.87;\nconstexpr auto kSuggestedReactionSize = 0.7;\nconstexpr auto kSuggestedWithCountSize = 0.55;\nconstexpr auto kStoppingFadeDuration = crl::time(150);\n\nclass ReactionView final\n\t: public Ui::RpWidget\n\t, public StoryAreaView\n\t, public HistoryView::DefaultElementDelegate {\npublic:\n\tReactionView(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Session*> session,\n\t\tconst Data::SuggestedReaction &reaction);\n\n\tvoid setAreaGeometry(QRect geometry, float64 radius) override;\n\tvoid setContentRect(QRect rect, int radius) override;\n\tvoid updateReactionsCount(int count) override;\n\tvoid playEffect() override;\n\tbool contains(QPoint point) override;\n\nprivate:\n\tusing Element = HistoryView::Element;\n\n\tstruct Stopping {\n\t\tstd::unique_ptr<Ui::ReactionFlyAnimation> effect;\n\t\tUi::Animations::Simple animation;\n\t};\n\n\tnot_null<HistoryView::ElementDelegate*> delegate();\n\tHistoryView::Context elementContext() override;\n\tbool elementAnimationsPaused() override;\n\tbool elementShownUnread(not_null<const Element*> view) override;\n\tnot_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid setupCustomChatStylePalette();\n\tvoid cacheBackground();\n\tvoid paintEffectFrame(\n\t\tQPainter &p,\n\t\tnot_null<Ui::ReactionFlyAnimation*> effect,\n\t\tcrl::time now);\n\tvoid updateEffectGeometry();\n\tvoid createEffectCanvas();\n\tvoid stopEffect();\n\n\tData::SuggestedReaction _data;\n\tstd::unique_ptr<Ui::ChatStyle> _chatStyle;\n\tstd::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\tAdminLog::OwnedItem _fake;\n\tQImage _background;\n\tQString _countShort;\n\tUi::Text::String _counter;\n\tUi::Animations::Simple _counterAnimation;\n\tQRectF _bubbleGeometry;\n\tQRect _apiGeometry;\n\tint _size = 0;\n\tint _mediaLeft = 0;\n\tint _mediaTop = 0;\n\tint _mediaWidth = 0;\n\tint _mediaHeight = 0;\n\tfloat64 _bubble = 0;\n\tfloat64 _bigOffset = 0;\n\tfloat64 _bigSize = 0;\n\tfloat64 _smallOffset = 0;\n\tfloat64 _smallSize = 0;\n\n\tstd::unique_ptr<Ui::RpWidget> _effectCanvas;\n\tstd::unique_ptr<Ui::ReactionFlyAnimation> _effect;\n\tstd::vector<Stopping> _effectStopping;\n\tQRect _effectTarget;\n\tQRect _contentRect;\n\n};\n\nclass WeatherView final : public Ui::RpWidget, public StoryAreaView {\npublic:\n\tWeatherView(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Session*> session,\n\t\tconst Data::WeatherArea &data,\n\t\trpl::producer<bool> weatherInCelsius);\n\n\tvoid setAreaGeometry(QRect geometry, float64 radius) override;\n\tvoid setContentRect(QRect rect, int radius) override;\n\tvoid updateReactionsCount(int count) override;\n\tvoid playEffect() override;\n\tbool contains(QPoint point) override;\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid cacheBackground();\n\tvoid watchForSticker();\n\tvoid setStickerFrom(not_null<DocumentData*> document);\n\t[[nodiscard]] QSize stickerSize() const;\n\n\tconst not_null<Main::Session*> _session;\n\tData::WeatherArea _data;\n\tEmojiPtr _emoji;\n\tQColor _fg;\n\tQImage _background;\n\tQFont _font;\n\tQRectF _rect;\n\tQRect _wrapped;\n\tfloat64 _radius = 0.;\n\tint _emojiSize = 0;\n\tint _padding = 0;\n\tbool _celsius = true;\n\n\tstd::shared_ptr<HistoryView::StickerPlayer> _sticker;\n\trpl::lifetime _lifetime;\n\tQRect _contentRect;\n\n};\n\n[[nodiscard]] QPoint Rotated(QPoint point, QPoint origin, float64 angle) {\n\tif (std::abs(angle) < 1.) {\n\t\treturn point;\n\t}\n\tconst auto alpha = angle / 180. * M_PI;\n\tconst auto acos = cos(alpha);\n\tconst auto asin = sin(alpha);\n\tpoint -= origin;\n\treturn origin + QPoint(\n\t\tint(base::SafeRound(acos * point.x() - asin * point.y())),\n\t\tint(base::SafeRound(asin * point.x() + acos * point.y())));\n}\n\n[[nodiscard]] AdminLog::OwnedItem GenerateFakeItem(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate,\n\t\tnot_null<History*> history) {\n\tExpects(history->peer->isUser());\n\n\tconst auto item = history->makeMessage({\n\t\t.id = history->nextNonHistoryEntryId(),\n\t\t.flags = MessageFlag::FakeHistoryItem | MessageFlag::HasFromId,\n\t\t.from = history->peer->id,\n\t\t.date = base::unixtime::now(),\n\t}, TextWithEntities(), MTP_messageMediaEmpty());\n\treturn AdminLog::OwnedItem(delegate, item);\n}\n\n[[nodiscard]] QColor ChooseWeatherFg(const QColor &bg) {\n\tconst auto luminance = (0.2126 * bg.redF())\n\t\t+ (0.7152 * bg.greenF())\n\t\t+ (0.0722 * bg.blueF());\n\treturn (luminance > 0.705) ? QColor(0, 0, 0) : QColor(255, 255, 255);\n}\n\nvoid ClipToContentRect(\n\t\tQPainter &p,\n\t\tQRect contentRect,\n\t\tQRect widgetGeometry) {\n\tif (contentRect.isEmpty()\n\t\t|| contentRect.contains(widgetGeometry)) {\n\t\treturn;\n\t}\n\tp.setClipRect(QRectF(\n\t\tcontentRect.translated(-widgetGeometry.topLeft())));\n}\n\nReactionView::ReactionView(\n\tQWidget *parent,\n\tnot_null<Main::Session*> session,\n\tconst Data::SuggestedReaction &reaction)\n: RpWidget(parent)\n, _data(reaction)\n, _chatStyle(std::make_unique<Ui::ChatStyle>(session->colorIndicesValue()))\n, _pathGradient(\n\tstd::make_unique<Ui::PathShiftGradient>(\n\t\tst::shadowFg,\n\t\tst::shadowFg,\n\t\t[=] { update(); }))\n, _fake(\n\tGenerateFakeItem(\n\t\tdelegate(),\n\t\tsession->data().history(PeerData::kServiceNotificationsId))) {\n\tstyle::PaletteChanged() | rpl::on_next([=] {\n\t\t_background = QImage();\n\t}, lifetime());\n\n\tconst auto view = _fake.get();\n\tconst auto entityData = [&] {\n\t\tconst auto &id = _data.reaction;\n\t\tconst auto reactions = &session->data().reactions();\n\t\treactions->preloadAnimationsFor(id);\n\t\tif (const auto customId = id.custom()) {\n\t\t\treturn Data::SerializeCustomEmojiId(customId);\n\t\t}\n\t\tconst auto type = Data::Reactions::Type::All;\n\t\tconst auto &list = reactions->list(type);\n\t\tconst auto i = ranges::find(list, id, &Data::Reaction::id);\n\t\treturn (i != end(list))\n\t\t\t? Data::SerializeCustomEmojiId(i->selectAnimation->id)\n\t\t\t: QString();\n\t}();\n\n\tconst auto emoji = Ui::Text::OnlyCustomEmoji{\n\t\t{ { { entityData } } }\n\t};\n\tview->overrideMedia(std::make_unique<HistoryView::UnwrappedMedia>(\n\t\tview,\n\t\tstd::make_unique<HistoryView::CustomEmoji>(view, emoji)));\n\tview->initDimensions();\n\n\t_mediaLeft = st::msgMargin.left();\n\t_mediaTop = st::msgMargin.top();\n\t_mediaWidth = _mediaHeight = view->resizeGetHeight(st::windowMinWidth)\n\t\t- _mediaTop\n\t\t- st::msgMargin.bottom();\n\n\tsession->data().viewRepaintRequest(\n\t) | rpl::on_next([=](Data::RequestViewRepaint data) {\n\t\tif (data.view == view) {\n\t\t\tupdate();\n\t\t}\n\t}, lifetime());\n\n\t_data.count = 0;\n\tupdateReactionsCount(reaction.count);\n\t_counterAnimation.stop();\n\n\tsetupCustomChatStylePalette();\n\tsetAttribute(Qt::WA_TransparentForMouseEvents);\n\tshow();\n}\n\nvoid ReactionView::setupCustomChatStylePalette() {\n\tconst auto color = uchar(_data.dark ? 255 : 0);\n\t_chatStyle->historyTextInFg().set(color, color, color, 255);\n\t_chatStyle->applyCustomPalette(_chatStyle.get());\n}\n\nvoid ReactionView::setAreaGeometry(QRect geometry, float64 radius) {\n\t_apiGeometry = geometry;\n\t_size = std::min(geometry.width(), geometry.height());\n\t_bubble = _size * kSuggestedBubbleSize;\n\t_bigOffset = _bubble * kSuggestedTailBigOffset;\n\t_bigSize = _bubble * kSuggestedTailBigSize;\n\t_smallOffset = _bubble * kSuggestedTailSmallOffset;\n\t_smallSize = _bubble * kSuggestedTailSmallSize;\n\tconst auto add = int(base::SafeRound(_smallOffset + _smallSize))\n\t\t- (_size / 2);\n\tsetGeometry(geometry.marginsAdded({ add, add, add, add }));\n\tconst auto sub = int(base::SafeRound(\n\t\t(1. - kSuggestedReactionSize) * _size / 2));\n\t_effectTarget = geometry.marginsRemoved({ sub, sub, sub, sub });\n\tupdateEffectGeometry();\n}\n\nvoid ReactionView::setContentRect(QRect rect, int radius) {\n\t_contentRect = rect;\n}\n\nvoid ReactionView::updateReactionsCount(int count) {\n\tif (_data.count == count) {\n\t\treturn;\n\t}\n\t_data.count = count;\n\tconst auto countShort = count\n\t\t? Lang::FormatCountToShort(count).string\n\t\t: QString();\n\tif (_countShort == countShort) {\n\t\treturn;\n\t}\n\tconst auto was = !_countShort.isEmpty();\n\t_countShort = countShort;\n\tconst auto now = !_countShort.isEmpty();\n\n\tif (!_countShort.isEmpty()) {\n\t\t_counter = { st::storiesLikeCountStyle, _countShort };\n\t}\n\tif (now != was) {\n\t\t_counterAnimation.start(\n\t\t\t[=] { update(); },\n\t\t\twas ? 1. : 0.,\n\t\t\twas ? 0. : 1.,\n\t\t\tst::fadeWrapDuration);\n\t}\n\tupdate();\n}\n\nvoid ReactionView::playEffect() {\n\tconst auto exists = (_effectCanvas != nullptr);\n\tif (exists) {\n\t\tstopEffect();\n\t} else {\n\t\tcreateEffectCanvas();\n\t}\n\tconst auto reactions = &_fake->history()->owner().reactions();\n\tconst auto scaleDown = _bubbleGeometry.width() / float64(_mediaWidth);\n\tauto args = Ui::ReactionFlyAnimationArgs{\n\t\t.id = _data.reaction,\n\t\t.miniCopyMultiplier = std::min(1., scaleDown),\n\t\t.effectOnly = true,\n\t};\n\t_effect = std::make_unique<Ui::ReactionFlyAnimation>(\n\t\treactions,\n\t\tstd::move(args),\n\t\t[=] { _effectCanvas->update(); },\n\t\t_size / 2,\n\t\tData::CustomEmojiSizeTag::Isolated);\n\tif (exists) {\n\t\t_effectStopping.back().animation.start([=] {\n\t\t\t_effectCanvas->update();\n\t\t}, 1., 0., kStoppingFadeDuration);\n\t}\n}\n\nbool ReactionView::contains(QPoint point) {\n\tif (!_contentRect.isEmpty() && !_contentRect.contains(point)) {\n\t\treturn false;\n\t}\n\tconst auto circle = _apiGeometry;\n\tconst auto radius = std::min(circle.width(), circle.height()) / 2;\n\tconst auto delta = circle.center() - point;\n\treturn QPoint::dotProduct(delta, delta) < (radius * radius);\n}\n\nvoid ReactionView::paintEffectFrame(\n\t\tQPainter &p,\n\t\tnot_null<Ui::ReactionFlyAnimation*> effect,\n\t\tcrl::time now) {\n\teffect->paintGetArea(\n\t\tp,\n\t\tQPoint(),\n\t\t_effectTarget.translated(-_effectCanvas->pos()),\n\t\t_data.dark ? Qt::white : Qt::black,\n\t\tQRect(),\n\t\tnow);\n}\n\nvoid ReactionView::createEffectCanvas() {\n\t_effectCanvas = std::make_unique<Ui::RpWidget>(parentWidget());\n\tconst auto raw = _effectCanvas.get();\n\traw->setAttribute(Qt::WA_TransparentForMouseEvents);\n\traw->show();\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tif (!_effect || _effect->finished()) {\n\t\t\tcrl::on_main(_effectCanvas.get(), [=] {\n\t\t\t\t_effect = nullptr;\n\t\t\t\t_effectStopping.clear();\n\t\t\t\t_effectCanvas = nullptr;\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tconst auto now = crl::now();\n\t\tauto p = QPainter(raw);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tClipToContentRect(p, _contentRect, raw->geometry());\n\t\t_effectStopping.erase(ranges::remove_if(_effectStopping, [&](\n\t\t\t\tconst Stopping &stopping) {\n\t\t\tif (!stopping.animation.animating()\n\t\t\t\t|| stopping.effect->finished()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tp.setOpacity(stopping.animation.value(0.));\n\t\t\tpaintEffectFrame(p, stopping.effect.get(), now);\n\t\t\treturn false;\n\t\t}), end(_effectStopping));\n\t\tpaintEffectFrame(p, _effect.get(), now);\n\t}, raw->lifetime());\n\tupdateEffectGeometry();\n}\n\nvoid ReactionView::stopEffect() {\n\t_effectStopping.push_back({ .effect = std::move(_effect) });\n\t_effectStopping.back().animation.start([=] {\n\t\t_effectCanvas->update();\n\t}, 1., 0., kStoppingFadeDuration);\n}\n\nvoid ReactionView::updateEffectGeometry() {\n\tif (!_effectCanvas) {\n\t\treturn;\n\t}\n\tconst auto center = geometry().center();\n\t_effectCanvas->setGeometry(\n\t\tcenter.x() - _size,\n\t\tcenter.y() - _size,\n\t\t_size * 2,\n\t\t_size * 3);\n}\n\nnot_null<HistoryView::ElementDelegate*> ReactionView::delegate() {\n\treturn static_cast<HistoryView::ElementDelegate*>(this);\n}\n\nvoid ReactionView::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\tif (!_size) {\n\t\treturn;\n\t}\n\tClipToContentRect(p, _contentRect, geometry());\n\tif (_background.size() != size() * style::DevicePixelRatio()) {\n\t\tcacheBackground();\n\t}\n\tp.drawImage(0, 0, _background);\n\n\tconst auto counted = _counterAnimation.value(_countShort.isEmpty()\n\t\t? 0.\n\t\t: 1.);\n\tconst auto scale = kSuggestedReactionSize\n\t\t+ (kSuggestedWithCountSize - kSuggestedReactionSize) * counted;\n\tconst auto counterSkip = (kSuggestedReactionSize - scale) * _mediaHeight / 2;\n\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.translate(_bubbleGeometry.center());\n\tp.scale(\n\t\tscale * _bubbleGeometry.width() / _mediaWidth,\n\t\tscale * _bubbleGeometry.height() / _mediaHeight);\n\tp.rotate(_data.area.rotation);\n\tp.translate(\n\t\t-(_mediaLeft + (_mediaWidth / 2)),\n\t\t-(_mediaTop + (_mediaHeight / 2) + counterSkip));\n\n\tauto context = Ui::ChatPaintContext{\n\t\t.st = _chatStyle.get(),\n\t\t.viewport = rect(),\n\t\t.clip = rect(),\n\t\t.now = crl::now(),\n\t};\n\t_fake->draw(p, context);\n\n\tif (counted > 0.) {\n\t\tp.setPen(_data.dark ? Qt::white : Qt::black);\n\t\tconst auto countTop = _mediaTop + _mediaHeight;\n\t\tif (counted < 1.) {\n\t\t\tconst auto center = QPoint(\n\t\t\t\t_mediaLeft + (_mediaWidth / 2),\n\t\t\t\tcountTop + st::storiesLikeCountStyle.font->height / 2);\n\t\t\tp.translate(center);\n\t\t\tp.scale(counted, counted);\n\t\t\tp.translate(-center);\n\t\t}\n\t\t_counter.draw(p, _mediaLeft, countTop, _mediaWidth, style::al_top);\n\t}\n}\n\nvoid ReactionView::cacheBackground() {\n\tconst auto ratio = style::DevicePixelRatio();\n\t_background = QImage(\n\t\tsize() * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_background.setDevicePixelRatio(ratio);\n\t_background.fill(Qt::transparent);\n\n\tconst auto paintShape = [&](QColor color) {\n\t\tauto p = QPainter(&_background);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tp.setBrush(color);\n\t\t_bubbleGeometry = QRectF(\n\t\t\t(width() - _bubble) / 2.,\n\t\t\t(height() - _bubble) / 2.,\n\t\t\t_bubble,\n\t\t\t_bubble);\n\t\tp.drawEllipse(_bubbleGeometry);\n\n\t\tconst auto center = QPointF(width() / 2., height() / 2.);\n\t\tp.translate(center);\n\n\t\tauto previous = 0.;\n\t\tconst auto rotate = [&](float64 initial) {\n\t\t\tif (_data.flipped) {\n\t\t\t\tinitial = 180 - initial;\n\t\t\t}\n\t\t\tauto rotation = _data.area.rotation - initial;\n\t\t\twhile (rotation < 0) {\n\t\t\t\trotation += 360;\n\t\t\t}\n\t\t\twhile (rotation >= 360) {\n\t\t\t\trotation -= 360;\n\t\t\t}\n\t\t\tconst auto delta = rotation - previous;\n\t\t\tprevious = rotation;\n\t\t\tp.rotate(delta);\n\t\t};\n\t\tconst auto paintTailPart = [&](float64 offset, float64 size) {\n\t\t\tconst auto part = QRectF(-size / 2., -size / 2., size, size);\n\t\t\tp.drawEllipse(part.translated(offset, 0));\n\t\t};\n\t\trotate(kSuggestedTailBigRotation);\n\t\tpaintTailPart(_bigOffset, _bigSize);\n\t\trotate(kSuggestedTailSmallRotation);\n\t\tpaintTailPart(_smallOffset, _smallSize);\n\t};\n\tconst auto dark = QColor(0, 0, 0, 128);\n\tif (!_data.dark) {\n\t\tpaintShape(dark);\n\t\t_background = Images::Blur(std::move(_background), true);\n\t}\n\tpaintShape(_data.dark ? dark : QColor(255, 255, 255));\n}\n\nWeatherView::WeatherView(\n\tQWidget *parent,\n\tnot_null<Main::Session*> session,\n\tconst Data::WeatherArea &data,\n\trpl::producer<bool> weatherInCelsius)\n: RpWidget(parent)\n, _session(session)\n, _data(data)\n, _emoji(Ui::Emoji::Find(_data.emoji))\n, _fg(ChooseWeatherFg(_data.color)) {\n\twatchForSticker();\n\tsetAttribute(Qt::WA_TransparentForMouseEvents);\n\tshow();\n\n\tstd::move(weatherInCelsius) | rpl::on_next([=](bool celsius) {\n\t\t_celsius = celsius;\n\t\t_background = {};\n\t\tupdate();\n\t}, lifetime());\n}\n\nvoid WeatherView::watchForSticker() {\n\tif (!_emoji) {\n\t\treturn;\n\t}\n\tconst auto emojiStickers = &_session->emojiStickersPack();\n\tif (const auto sticker = emojiStickers->stickerForEmoji(_emoji)) {\n\t\tsetStickerFrom(sticker.document);\n\t} else {\n\t\temojiStickers->refreshed() | rpl::map([=] {\n\t\t\treturn emojiStickers->stickerForEmoji(_emoji).document;\n\t\t}) | rpl::filter([=](DocumentData *document) {\n\t\t\treturn document != nullptr;\n\t\t}) | rpl::take(\n\t\t\t1\n\t\t) | rpl::on_next([=](not_null<DocumentData*> document) {\n\t\t\tsetStickerFrom(document);\n\t\t\tupdate();\n\t\t}, lifetime());\n\t}\n}\n\nvoid WeatherView::setAreaGeometry(QRect geometry, float64 radius) {\n\tconst auto diagxdiag = (geometry.width() * geometry.width())\n\t\t+ (geometry.height() * geometry.height());\n\tconst auto diag = std::sqrt(diagxdiag);\n\tconst auto shift = diag * 2 / 3.;\n\tconst auto topleft = QRectF(geometry).center()\n\t\t- QPointF(shift, shift);\n\tconst auto bottomright = topleft + QPointF(shift, shift) * 2;\n\tconst auto left = int(std::floor(topleft.x()));\n\tconst auto top = int(std::floor(topleft.y()));\n\tconst auto right = int(std::ceil(bottomright.x()));\n\tconst auto bottom = int(std::ceil(bottomright.y()));\n\tsetGeometry(left, top, right - left, bottom - top);\n\t_rect = QRectF(geometry).translated(-left, -top);\n\t_radius = radius;\n\n\t_emojiSize = int(base::SafeRound(_rect.height() * 2 / 3.));\n\t_font = st::semiboldFont->f;\n\t_font.setPixelSize(_emojiSize);\n\t_background = {};\n}\n\nvoid WeatherView::setContentRect(QRect rect, int radius) {\n\t_contentRect = rect;\n}\n\nvoid WeatherView::updateReactionsCount(int count) {\n\tUnexpected(\"WeatherView::updateRactionsCount.\");\n}\n\nvoid WeatherView::playEffect() {\n\tUnexpected(\"WeatherView::playEffect.\");\n}\n\nbool WeatherView::contains(QPoint point) {\n\tif (!_contentRect.isEmpty() && !_contentRect.contains(point)) {\n\t\treturn false;\n\t}\n\tconst auto geometry = _rect.translated(pos()).toRect();\n\tconst auto angle = -_data.area.rotation;\n\treturn geometry.contains(Rotated(point, geometry.center(), angle));\n}\n\nvoid WeatherView::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\tClipToContentRect(p, _contentRect, geometry());\n\tif (_background.size() != size() * style::DevicePixelRatio()) {\n\t\tcacheBackground();\n\t}\n\tp.drawImage(0, 0, _background);\n\tif (_sticker && _sticker->ready()) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto rcenter = _wrapped.center();\n\t\tp.translate(rcenter);\n\t\tp.rotate(_data.area.rotation);\n\t\tp.translate(-rcenter);\n\n\t\tconst auto image = _sticker->frame(\n\t\t\tstickerSize(),\n\t\t\tQColor(0, 0, 0, 0),\n\t\t\tfalse,\n\t\t\tcrl::now(),\n\t\t\tfalse).image;\n\t\tconst auto size = image.size() / style::DevicePixelRatio();\n\t\tconst auto rect = QRectF(\n\t\t\t_wrapped.x() + _padding + (_emojiSize - size.width()) / 2.,\n\t\t\t_wrapped.y() + (_wrapped.height() - size.height()) / 2.,\n\t\t\tsize.width(),\n\t\t\tsize.height());\n\t\tconst auto scenter = rect.center();\n\t\tconst auto scale = (_emojiSize * 1.) / stickerSize().width();\n\t\tp.translate(scenter);\n\t\tp.scale(scale, scale);\n\t\tp.translate(-scenter);\n\t\tp.drawImage(rect, image);\n\t\t_sticker->markFrameShown();\n\t}\n}\n\nQSize WeatherView::stickerSize() const {\n\treturn QSize(st::chatIntroStickerSize, st::chatIntroStickerSize);\n}\n\nvoid WeatherView::setStickerFrom(not_null<DocumentData*> document) {\n\tif (_sticker || !_emoji) {\n\t\treturn;\n\t}\n\tconst auto media = document->createMediaView();\n\tmedia->checkStickerLarge();\n\tmedia->goodThumbnailWanted();\n\n\trpl::single() | rpl::then(\n\t\tdocument->session().downloaderTaskFinished()\n\t) | rpl::filter([=] {\n\t\treturn media->loaded();\n\t}) | rpl::take(1) | rpl::on_next([=] {\n\t\tconst auto sticker = document->sticker();\n\t\tif (sticker->isLottie()) {\n\t\t\t_sticker = std::make_shared<HistoryView::LottiePlayer>(\n\t\t\t\tChatHelpers::LottiePlayerFromDocument(\n\t\t\t\t\tmedia.get(),\n\t\t\t\t\tChatHelpers::StickerLottieSize::StickerSet,\n\t\t\t\t\tstickerSize(),\n\t\t\t\t\tLottie::Quality::High));\n\t\t} else if (sticker->isWebm()) {\n\t\t\t_sticker = std::make_shared<HistoryView::WebmPlayer>(\n\t\t\t\tmedia->owner()->location(),\n\t\t\t\tmedia->bytes(),\n\t\t\t\tstickerSize());\n\t\t} else {\n\t\t\t_sticker = std::make_shared<HistoryView::StaticStickerPlayer>(\n\t\t\t\tmedia->owner()->location(),\n\t\t\t\tmedia->bytes(),\n\t\t\t\tstickerSize());\n\t\t}\n\t\t_sticker->setRepaintCallback([=] { update(); });\n\t\tupdate();\n\t}, lifetime());\n}\n\nvoid WeatherView::cacheBackground() {\n\tconst auto ratio = style::DevicePixelRatio();\n\t_background = QImage(\n\t\tsize() * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_background.setDevicePixelRatio(ratio);\n\t_background.fill(Qt::transparent);\n\n\tauto p = QPainter(&_background);\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setBrush(_data.color);\n\tp.setPen(Qt::NoPen);\n\tconst auto center = _rect.center();\n\tp.translate(center);\n\tp.rotate(_data.area.rotation);\n\tp.translate(-center);\n\n\tconst auto format = [](float64 value) {\n\t\treturn QString::number(int(base::SafeRound(value)));\n\t};\n\tconst auto text = [&] {\n\t\tconst auto celsius = _data.millicelsius / 1000.;\n\t\tif (_celsius) {\n\t\t\treturn format(celsius);\n\t\t}\n\t\tconst auto fahrenheit = (celsius * 9.0 / 5.0) + 32;\n\t\treturn format(fahrenheit);\n\t}().append(QChar(0xb0)).append(_celsius ? \"C\" : \"F\");\n\tconst auto metrics = QFontMetrics(_font);\n\tconst auto textWidth = qCeil(metrics.horizontalAdvance(text));\n\t_padding = int(_rect.height() / 5);\n\tconst auto fullWidth = (_emoji ? (_emojiSize - _padding) : 0)\n\t\t+ textWidth\n\t\t+ (4 * _padding);\n\tconst auto left = _rect.x() + (_rect.width() - fullWidth) / 2;\n\t_wrapped = QRect(left, _rect.y(), fullWidth, _rect.height());\n\n\tp.drawRoundedRect(_wrapped, _radius, _radius);\n\n\tp.setPen(_fg);\n\tp.setFont(_font);\n\tp.drawText(_wrapped.marginsRemoved(\n\t\t{ 2 * _padding + (_emoji ? (_emojiSize - _padding) : 0), 0, 2 * _padding, 0 }),\n\t\ttext,\n\t\tstyle::al_center);\n}\n\n[[nodiscard]] Data::ReactionId HeartReactionId() {\n\treturn { QString() + QChar(10084) };\n}\n\nHistoryView::Context ReactionView::elementContext() {\n\treturn HistoryView::Context::ContactPreview;\n}\n\nbool ReactionView::elementAnimationsPaused() {\n\treturn false;\n}\n\nbool ReactionView::elementShownUnread(\n\t\tnot_null<const Element*> view) {\n\treturn false;\n}\n\nauto ReactionView::elementPathShiftGradient()\n-> not_null<Ui::PathShiftGradient*> {\n\treturn _pathGradient.get();\n}\n\n} // namespace\n\nclass Reactions::Panel final {\npublic:\n\texplicit Panel(not_null<Controller*> controller);\n\t~Panel();\n\n\t[[nodiscard]] rpl::producer<bool> expandedValue() const {\n\t\treturn _expanded.value();\n\t}\n\t[[nodiscard]] rpl::producer<bool> shownValue() const {\n\t\treturn _shown.value();\n\t}\n\n\t[[nodiscard]] rpl::producer<Chosen> chosen() const;\n\n\tvoid show(Mode mode);\n\tvoid hide(Mode mode);\n\tvoid hideIfCollapsed(Mode mode);\n\tvoid collapse(Mode mode);\n\n\tvoid attachToReactionButton(not_null<Ui::RpWidget*> button);\n\nprivate:\n\tstruct Hiding;\n\n\tvoid create();\n\tvoid updateShowState();\n\tvoid fadeOutSelector();\n\tvoid startAnimation();\n\n\tconst not_null<Controller*> _controller;\n\n\tstd::unique_ptr<Ui::RpWidget> _parent;\n\tstd::unique_ptr<HistoryView::Reactions::Selector> _selector;\n\tstd::vector<std::unique_ptr<Hiding>> _hiding;\n\trpl::event_stream<Chosen> _chosen;\n\tUi::Animations::Simple _showing;\n\trpl::variable<float64> _shownValue;\n\trpl::variable<bool> _expanded;\n\trpl::variable<Mode> _mode;\n\trpl::variable<bool> _shown = false;\n\n};\n\nstruct Reactions::Panel::Hiding {\n\texplicit Hiding(not_null<QWidget*> parent) : widget(parent) {\n\t}\n\n\tUi::RpWidget widget;\n\tUi::Animations::Simple animation;\n\tQImage frame;\n};\n\nReactions::Panel::Panel(not_null<Controller*> controller)\n: _controller(controller) {\n}\n\nReactions::Panel::~Panel() = default;\n\nauto Reactions::Panel::chosen() const -> rpl::producer<Chosen> {\n\treturn _chosen.events();\n}\n\nvoid Reactions::Panel::show(Mode mode) {\n\tconst auto was = _mode.current();\n\tif (_shown.current() && was == mode) {\n\t\treturn;\n\t} else if (_shown.current()) {\n\t\thide(was);\n\t}\n\t_mode = mode;\n\tcreate();\n\tif (!_selector) {\n\t\treturn;\n\t}\n\tconst auto duration = st::defaultPanelAnimation.heightDuration\n\t\t* st::defaultPopupMenu.showDuration;\n\t_shown = true;\n\t_showing.start([=] { updateShowState(); }, 0., 1., duration);\n\tupdateShowState();\n\t_parent->show();\n}\n\nvoid Reactions::Panel::hide(Mode mode) {\n\tif (!_selector || _mode.current() != mode) {\n\t\treturn;\n\t}\n\t_selector->beforeDestroy();\n\tif (!anim::Disabled()) {\n\t\tfadeOutSelector();\n\t}\n\t_shown = false;\n\t_expanded = false;\n\t_showing.stop();\n\t_selector = nullptr;\n\t_parent = nullptr;\n}\n\nvoid Reactions::Panel::hideIfCollapsed(Mode mode) {\n\tif (!_expanded.current() && _mode.current() == mode) {\n\t\thide(mode);\n\t}\n}\n\nvoid Reactions::Panel::collapse(Mode mode) {\n\tif (_expanded.current() && _mode.current() == mode) {\n\t\thide(mode);\n\t\tshow(mode);\n\t}\n}\n\nvoid Reactions::Panel::attachToReactionButton(\n\t\tnot_null<Ui::RpWidget*> button) {\n\tbase::install_event_filter(button, [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::ContextMenu && !button->isHidden()) {\n\t\t\tshow(Reactions::Mode::Reaction);\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t} else if (e->type() == QEvent::Hide) {\n\t\t\thide(Reactions::Mode::Reaction);\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n}\n\nvoid Reactions::Panel::create() {\n\tauto reactions = Data::LookupPossibleReactions(\n\t\t&_controller->uiShow()->session());\n\tif (reactions.recent.empty()\n\t\t|| (_mode.current() == Mode::Message\n\t\t\t&& _controller->videoStream())) {\n\t\treturn;\n\t}\n\t_parent = std::make_unique<Ui::RpWidget>(_controller->wrap().get());\n\t_parent->show();\n\n\tconst auto mode = _mode.current();\n\n\t_parent->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::MouseButtonPress) {\n\t\t\tconst auto event = static_cast<QMouseEvent*>(e.get());\n\t\t\tif (event->button() == Qt::LeftButton) {\n\t\t\t\tif (!_selector\n\t\t\t\t\t|| !_selector->geometry().contains(event->pos())) {\n\t\t\t\t\tif (mode == Mode::Message) {\n\t\t\t\t\t\tcollapse(mode);\n\t\t\t\t\t} else {\n\t\t\t\t\t\thide(mode);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, _parent->lifetime());\n\n\t_selector = std::make_unique<HistoryView::Reactions::Selector>(\n\t\t_parent.get(),\n\t\tst::storiesReactionsPan,\n\t\t_controller->uiShow(),\n\t\tstd::move(reactions),\n\t\tTextWithEntities{ (mode == Mode::Message\n\t\t\t? tr::lng_stories_reaction_as_message(tr::now)\n\t\t\t: QString()) },\n\t\t[=](bool fast) { hide(mode); },\n\t\tnullptr, // iconFactory\n\t\tnullptr, // paused\n\t\ttrue);\n\n\t_selector->chosen(\n\t) | rpl::on_next([=](\n\t\t\tHistoryView::Reactions::ChosenReaction reaction) {\n\t\t_chosen.fire({ .reaction = reaction, .mode = mode });\n\t\thide(mode);\n\t}, _selector->lifetime());\n\n\tconst auto desiredWidth = st::storiesReactionsWidth;\n\tconst auto maxWidth = desiredWidth * 2;\n\tconst auto width = _selector->countWidth(desiredWidth, maxWidth);\n\tconst auto margins = _selector->marginsForShadow();\n\tconst auto categoriesTop = _selector->extendTopForCategoriesAndAbout(\n\t\twidth);\n\tconst auto full = margins.left() + width + margins.right();\n\n\t_shownValue = 0.;\n\trpl::combine(\n\t\t_controller->layoutValue(),\n\t\t_shownValue.value()\n\t) | rpl::on_next([=](const Layout &layout, float64 shown) {\n\t\tconst auto story = _controller->story();\n\t\tconst auto viewsReactionsMode = story && story->peer()->isChannel();\n\t\tconst auto width = margins.left()\n\t\t\t+ _selector->countAppearedWidth(shown)\n\t\t\t+ margins.right();\n\t\tconst auto height = layout.reactions.height();\n\t\tconst auto shift = (width / 2);\n\t\tconst auto right = (mode == Mode::Message)\n\t\t\t? (layout.reactions.x() + layout.reactions.width() / 2 + shift)\n\t\t\t: viewsReactionsMode\n\t\t\t? (layout.content.x() + layout.content.width())\n\t\t\t: (layout.controlsBottomPosition.x()\n\t\t\t\t+ layout.controlsWidth\n\t\t\t\t- st::storiesLikeReactionsPosition.x());\n\t\tconst auto top = (mode == Mode::Message)\n\t\t\t? layout.reactions.y()\n\t\t\t: (layout.controlsBottomPosition.y()\n\t\t\t\t- height\n\t\t\t\t- st::storiesLikeReactionsPosition.y());\n\t\t_parent->setGeometry(QRect((right - width), top, full, height));\n\t\tconst auto innerTop = height\n\t\t\t- st::storiesReactionsBottomSkip\n\t\t\t- st::reactStripHeight;\n\t\tconst auto maxAdded = innerTop - margins.top() - categoriesTop;\n\t\tconst auto added = std::min(maxAdded, st::storiesReactionsAddedTop);\n\t\t_selector->setSpecialExpandTopSkip(added);\n\t\t_selector->initGeometry(innerTop);\n\t}, _selector->lifetime());\n\n\t_selector->willExpand(\n\t) | rpl::on_next([=] {\n\t\t_expanded = true;\n\t}, _selector->lifetime());\n\n\t_selector->escapes() | rpl::on_next([=] {\n\t\tif (mode == Mode::Message) {\n\t\t\tcollapse(mode);\n\t\t} else {\n\t\t\thide(mode);\n\t\t}\n\t}, _selector->lifetime());\n}\n\nvoid Reactions::Panel::fadeOutSelector() {\n\tconst auto wrap = _controller->wrap().get();\n\tconst auto geometry = Ui::MapFrom(\n\t\twrap,\n\t\t_parent.get(),\n\t\t_selector->geometry());\n\t_hiding.push_back(std::make_unique<Hiding>(wrap));\n\tconst auto raw = _hiding.back().get();\n\traw->frame = Ui::GrabWidgetToImage(_selector.get());\n\traw->widget.setGeometry(geometry);\n\traw->widget.show();\n\traw->widget.paintRequest(\n\t) | rpl::on_next([=] {\n\t\tif (const auto opacity = raw->animation.value(0.)) {\n\t\t\tauto p = QPainter(&raw->widget);\n\t\t\tp.setOpacity(opacity);\n\t\t\tp.drawImage(0, 0, raw->frame);\n\t\t}\n\t}, raw->widget.lifetime());\n\tUi::PostponeCall(&raw->widget, [=] {\n\t\traw->animation.start([=] {\n\t\t\tif (raw->animation.animating()) {\n\t\t\t\traw->widget.update();\n\t\t\t} else {\n\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\t_hiding,\n\t\t\t\t\traw,\n\t\t\t\t\t&std::unique_ptr<Hiding>::get);\n\t\t\t\tif (i != end(_hiding)) {\n\t\t\t\t\t_hiding.erase(i);\n\t\t\t\t}\n\t\t\t}\n\t\t}, 1., 0., st::slideWrapDuration);\n\t});\n}\n\nvoid Reactions::Panel::updateShowState() {\n\tconst auto progress = _showing.value(_shown.current() ? 1. : 0.);\n\tconst auto opacity = 1.;\n\tconst auto appearing = _showing.animating();\n\tconst auto toggling = false;\n\t_shownValue = progress;\n\t_selector->updateShowState(progress, opacity, appearing, toggling);\n}\n\nReactions::Reactions(not_null<Controller*> controller)\n: _controller(controller)\n, _panel(std::make_unique<Panel>(_controller)) {\n\t_panel->chosen() | rpl::on_next([=](Chosen &&chosen) {\n\t\t_chosen.fire(std::move(chosen));\n\t}, _lifetime);\n}\n\nReactions::~Reactions() = default;\n\nrpl::producer<bool> Reactions::activeValue() const {\n\tusing namespace rpl::mappers;\n\treturn rpl::combine(\n\t\t_panel->expandedValue(),\n\t\t_panel->shownValue(),\n\t\t_1 || _2);\n}\n\nauto Reactions::chosen() const -> rpl::producer<Chosen> {\n\treturn _chosen.events();\n}\n\nauto Reactions::makeSuggestedReactionWidget(\n\tconst Data::SuggestedReaction &reaction)\n-> std::unique_ptr<StoryAreaView> {\n\treturn std::make_unique<ReactionView>(\n\t\t_controller->wrap(),\n\t\t&_controller->uiShow()->session(),\n\t\treaction);\n}\n\nauto Reactions::makeWeatherAreaWidget(\n\tconst Data::WeatherArea &data,\n\trpl::producer<bool> weatherInCelsius)\n-> std::unique_ptr<StoryAreaView> {\n\treturn std::make_unique<WeatherView>(\n\t\t_controller->wrap(),\n\t\t&_controller->uiShow()->session(),\n\t\tdata,\n\t\tstd::move(weatherInCelsius));\n}\n\nvoid Reactions::setReplyFieldState(\n\t\trpl::producer<bool> focused,\n\t\trpl::producer<bool> hasSendText) {\n\tstd::move(\n\t\tfocused\n\t) | rpl::on_next([=](bool focused) {\n\t\t_replyFocused = focused;\n\t\tif (!_replyFocused) {\n\t\t\t_panel->hideIfCollapsed(Reactions::Mode::Message);\n\t\t} else if (!_hasSendText) {\n\t\t\t_panel->show(Reactions::Mode::Message);\n\t\t}\n\t}, _lifetime);\n\n\tstd::move(\n\t\thasSendText\n\t) | rpl::on_next([=](bool has) {\n\t\t_hasSendText = has;\n\t\tif (_replyFocused) {\n\t\t\tif (_hasSendText) {\n\t\t\t\t_panel->hide(Reactions::Mode::Message);\n\t\t\t} else {\n\t\t\t\t_panel->show(Reactions::Mode::Message);\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n}\n\nvoid Reactions::attachToReactionButton(not_null<Ui::RpWidget*> button) {\n\t_panel->attachToReactionButton(button);\n}\n\nvoid Reactions::setReactionIconWidget(Ui::RpWidget *widget) {\n\tif (_likeIconWidget != widget) {\n\t\tassignLikedId({});\n\t\t_likeIconWidget = widget;\n\t\t_reactionAnimation = nullptr;\n\t}\n}\n\nauto Reactions::attachToMenu(\n\tnot_null<Ui::PopupMenu*> menu,\n\tQPoint desiredPosition)\n-> AttachStripResult {\n\tusing namespace HistoryView::Reactions;\n\n\tconst auto story = _controller->story();\n\tif (!story || story->peer()->isSelf() || story->call()) {\n\t\treturn AttachStripResult::Skipped;\n\t}\n\n\tconst auto show = _controller->uiShow();\n\tconst auto result = AttachSelectorToMenu(\n\t\tmenu,\n\t\tdesiredPosition,\n\t\tst::storiesReactionsPan,\n\t\tshow,\n\t\tData::LookupPossibleReactions(&show->session()),\n\t\tTextWithEntities());\n\tif (!result) {\n\t\treturn result.error();\n\t}\n\tconst auto selector = *result;\n\n\tselector->chosen() | rpl::on_next([=](ChosenReaction reaction) {\n\t\tmenu->hideMenu();\n\t\t_chosen.fire({ reaction, ReactionsMode::Reaction });\n\t}, selector->lifetime());\n\n\treturn AttachSelectorResult::Attached;\n}\n\nData::ReactionId Reactions::liked() const {\n\treturn _liked.current();\n}\n\nrpl::producer<Data::ReactionId> Reactions::likedValue() const {\n\treturn _liked.value();\n}\n\nvoid Reactions::showLikeFrom(Data::Story *story) {\n\tsetLikedIdFrom(story);\n\n\tif (!story) {\n\t\t_likeFromLifetime.destroy();\n\t\treturn;\n\t}\n\t_likeFromLifetime = story->session().changes().storyUpdates(\n\t\tstory,\n\t\tData::StoryUpdate::Flag::Reaction\n\t) | rpl::on_next([=](const Data::StoryUpdate &update) {\n\t\tsetLikedIdFrom(update.story);\n\t});\n}\n\nvoid Reactions::hide() {\n\t_panel->hide(Reactions::Mode::Message);\n\t_panel->hide(Reactions::Mode::Reaction);\n}\n\nvoid Reactions::outsidePressed() {\n\t_panel->hide(Reactions::Mode::Reaction);\n\t_panel->collapse(Reactions::Mode::Message);\n}\n\nvoid Reactions::toggleLiked() {\n\tconst auto liked = !_liked.current().empty();\n\tapplyLike(liked ? Data::ReactionId() : HeartReactionId());\n}\n\nvoid Reactions::applyLike(Data::ReactionId id) {\n\tif (_liked.current() != id) {\n\t\t_chosen.fire({ { .id = id }, ReactionsMode::Reaction });\n\t}\n}\n\nvoid Reactions::ready() {\n\tif (const auto story = _controller->story()) {\n\t\tstory->owner().reactions().preloadAnimationsFor(HeartReactionId());\n\t}\n}\n\nvoid Reactions::animateAndProcess(Chosen &&chosen) {\n\tconst auto like = (chosen.mode == Mode::Reaction);\n\tconst auto wrap = _controller->wrap();\n\tconst auto target = like ? _likeIconWidget : wrap.get();\n\tconst auto story = _controller->story();\n\tif (!story || !target) {\n\t\treturn;\n\t}\n\n\tauto done = like\n\t\t? setLikedIdIconInit(&story->owner(), chosen.reaction.id)\n\t\t: Fn<void(Ui::ReactionFlyCenter)>();\n\tconst auto scaleOutDuration = like\n\t\t? kReactionScaleOutDuration\n\t\t: kMessageReactionScaleOutDuration;\n\tconst auto scaleOutTarget = like ? kReactionScaleOutTarget : 0.;\n\n\tif (!chosen.reaction.id.empty()) {\n\t\tstartReactionAnimation({\n\t\t\t.id = chosen.reaction.id,\n\t\t\t.flyIcon = chosen.reaction.icon,\n\t\t\t.flyFrom = (chosen.reaction.globalGeometry.isEmpty()\n\t\t\t\t? QRect()\n\t\t\t\t: wrap->mapFromGlobal(chosen.reaction.globalGeometry)),\n\t\t\t.scaleOutDuration = scaleOutDuration,\n\t\t\t.scaleOutTarget = scaleOutTarget,\n\t\t}, target, std::move(done));\n\t}\n}\n\nvoid Reactions::assignLikedId(Data::ReactionId id) {\n\tinvalidate_weak_ptrs(&_likeIconGuard);\n\t_likeIcon = nullptr;\n\t_liked = id;\n}\n\nFn<void(Ui::ReactionFlyCenter)> Reactions::setLikedIdIconInit(\n\t\tnot_null<Data::Session*> owner,\n\t\tData::ReactionId id,\n\t\tbool force) {\n\tif (_liked.current() != id) {\n\t\t_likeIconMedia = nullptr;\n\t} else if (!force) {\n\t\treturn nullptr;\n\t}\n\tassignLikedId(id);\n\tif (id.empty() || !_likeIconWidget) {\n\t\treturn nullptr;\n\t}\n\treturn crl::guard(&_likeIconGuard, [=](Ui::ReactionFlyCenter center) {\n\t\tif (!id.custom() && !center.icon && !_likeIconMedia) {\n\t\t\twaitForLikeIcon(owner, id);\n\t\t} else {\n\t\t\tinitLikeIcon(owner, id, std::move(center));\n\t\t}\n\t});\n}\n\nvoid Reactions::initLikeIcon(\n\t\tnot_null<Data::Session*> owner,\n\t\tData::ReactionId id,\n\t\tUi::ReactionFlyCenter center) {\n\tExpects(_likeIconWidget != nullptr);\n\n\t_likeIcon = std::make_unique<Ui::RpWidget>(_likeIconWidget);\n\tconst auto icon = _likeIcon.get();\n\ticon->show();\n\t_likeIconWidget->sizeValue() | rpl::on_next([=](QSize size) {\n\t\ticon->setGeometry(QRect(QPoint(), size));\n\t}, icon->lifetime());\n\n\tif (!id.custom() && !center.icon) {\n\t\treturn;\n\t}\n\n\tstruct State {\n\t\tUi::ReactionFlyCenter center;\n\t\tQImage cache;\n\t};\n\tconst auto fly = icon->lifetime().make_state<State>(State{\n\t\t.center = std::move(center),\n\t});\n\tif (const auto customId = id.custom()) {\n\t\tauto withCorrectCallback = owner->customEmojiManager().create(\n\t\t\tcustomId,\n\t\t\t[=] { icon->update(); },\n\t\t\tData::CustomEmojiSizeTag::Isolated);\n\t\t[[maybe_unused]] const auto load = withCorrectCallback->ready();\n\t\tfly->center.custom = std::move(withCorrectCallback);\n\t\tfly->center.icon = nullptr;\n\t} else {\n\t\tfly->center.icon->jumpToStart(nullptr);\n\t\tfly->center.custom = nullptr;\n\t}\n\tconst auto paintNonCached = [=](QPainter &p) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tconst auto size = fly->center.size;\n\t\tconst auto target = QRect(\n\t\t\t(icon->width() - size) / 2,\n\t\t\t(icon->height() - size) / 2,\n\t\t\tsize,\n\t\t\tsize);\n\t\tconst auto scale = fly->center.scale;\n\t\tif (scale < 1.) {\n\t\t\tconst auto shift = QRectF(target).center();\n\t\t\tp.translate(shift);\n\t\t\tp.scale(scale, scale);\n\t\t\tp.translate(-shift);\n\t\t}\n\t\tconst auto multiplier = fly->center.centerSizeMultiplier;\n\t\tconst auto inner = int(base::SafeRound(size * multiplier));\n\t\tif (const auto icon = fly->center.icon.get()) {\n\t\t\tconst auto rect = QRect(\n\t\t\t\ttarget.x() + (target.width() - inner) / 2,\n\t\t\t\ttarget.y() + (target.height() - inner) / 2,\n\t\t\t\tinner,\n\t\t\t\tinner);\n\t\t\tp.drawImage(rect, icon->frame(st::storiesComposeWhiteText->c));\n\t\t} else {\n\t\t\tconst auto customSize = fly->center.customSize;\n\t\t\tconst auto scaled = (inner != customSize);\n\t\t\tfly->center.custom->paint(p, {\n\t\t\t\t.textColor = st::storiesComposeWhiteText->c,\n\t\t\t\t.size = { customSize, customSize },\n\t\t\t\t.now = crl::now(),\n\t\t\t\t.scale = (scaled ? (inner / float64(customSize)) : 1.),\n\t\t\t\t.position = QPoint(\n\t\t\t\t\ttarget.x() + (target.width() - customSize) / 2,\n\t\t\t\t\ttarget.y() + (target.height() - customSize) / 2),\n\t\t\t\t.scaled = scaled,\n\t\t\t});\n\t\t}\n\t};\n\ticon->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(icon);\n\t\tif (!fly->cache.isNull()) {\n\t\t\tp.drawImage(0, 0, fly->cache);\n\t\t} else if (fly->center.icon\n\t\t\t|| fly->center.custom->readyInDefaultState()) {\n\t\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t\tfly->cache = QImage(\n\t\t\t\ticon->size() * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tfly->cache.setDevicePixelRatio(ratio);\n\t\t\tfly->cache.fill(Qt::transparent);\n\t\t\tauto q = QPainter(&fly->cache);\n\t\t\tpaintNonCached(q);\n\t\t\tq.end();\n\n\t\t\tfly->center.icon = nullptr;\n\t\t\tfly->center.custom = nullptr;\n\t\t\tp.drawImage(0, 0, fly->cache);\n\t\t} else {\n\t\t\tpaintNonCached(p);\n\t\t}\n\t}, icon->lifetime());\n}\n\nvoid Reactions::waitForLikeIcon(\n\t\tnot_null<Data::Session*> owner,\n\t\tData::ReactionId id) {\n\t_likeIconWaitLifetime = rpl::single(\n\t\trpl::empty\n\t) | rpl::then(\n\t\towner->reactions().defaultUpdates()\n\t) | rpl::map([=]() -> rpl::producer<bool> {\n\t\tconst auto &list = owner->reactions().list(\n\t\t\tData::Reactions::Type::All);\n\t\tconst auto i = ranges::find(list, id, &Data::Reaction::id);\n\t\tif (i == end(list)) {\n\t\t\treturn rpl::single(false);\n\t\t}\n\t\tconst auto document = i->centerIcon\n\t\t\t? not_null(i->centerIcon)\n\t\t\t: i->selectAnimation;\n\t\t_likeIconMedia = document->createMediaView();\n\t\t_likeIconMedia->checkStickerLarge();\n\t\treturn rpl::single(\n\t\t\trpl::empty\n\t\t) | rpl::then(\n\t\t\tdocument->session().downloaderTaskFinished()\n\t\t) | rpl::map([=] {\n\t\t\treturn _likeIconMedia->loaded();\n\t\t});\n\t}) | rpl::flatten_latest(\n\t) | rpl::filter(\n\t\trpl::mappers::_1\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\tsetLikedId(owner, id, true);\n\n\t\tcrl::on_main(&_likeIconGuard, [=] {\n\t\t\t_likeIconMedia = nullptr;\n\t\t\t_likeIconWaitLifetime.destroy();\n\t\t});\n\t});\n}\n\nvoid Reactions::setLikedIdFrom(Data::Story *story) {\n\tif (!story) {\n\t\tassignLikedId({});\n\t} else {\n\t\tsetLikedId(&story->owner(), story->sentReactionId());\n\t}\n}\n\nvoid Reactions::setLikedId(\n\t\tnot_null<Data::Session*> owner,\n\t\tData::ReactionId id,\n\t\tbool force) {\n\tif (const auto done = setLikedIdIconInit(owner, id, force)) {\n\t\tconst auto reactions = &owner->reactions();\n\t\tconst auto colored = [] { return st::storiesComposeWhiteText->c; };\n\t\tconst auto sizeTag = Data::CustomEmojiSizeTag::Isolated;\n\t\tdone(Ui::EmojiFlyAnimation(_controller->wrap(), reactions, {\n\t\t\t.id = id,\n\t\t\t.scaleOutDuration = kReactionScaleOutDuration,\n\t\t\t.scaleOutTarget = kReactionScaleOutTarget,\n\t\t}, [] {}, colored, sizeTag).grabBadgeCenter());\n\t}\n}\n\nvoid Reactions::startReactionAnimation(\n\t\tUi::ReactionFlyAnimationArgs args,\n\t\tnot_null<QWidget*> target,\n\t\tFn<void(Ui::ReactionFlyCenter)> done) {\n\tconst auto wrap = _controller->wrap();\n\tconst auto story = _controller->story();\n\t_reactionAnimation = std::make_unique<Ui::EmojiFlyAnimation>(\n\t\twrap,\n\t\t&story->owner().reactions(),\n\t\tstd::move(args),\n\t\t[=] { _reactionAnimation->repaint(); },\n\t\t[] { return st::storiesComposeWhiteText->c; },\n\t\tData::CustomEmojiSizeTag::Isolated);\n\tconst auto layer = _reactionAnimation->layer();\n\twrap->paintRequest() | rpl::on_next([=] {\n\t\tif (!_reactionAnimation->paintBadgeFrame(target)) {\n\t\t\tInvokeQueued(layer, [=] {\n\t\t\t\t_reactionAnimation = nullptr;\n\t\t\t\twrap->update();\n\t\t\t});\n\t\t\tif (done) {\n\t\t\t\tdone(_reactionAnimation->grabBadgeCenter());\n\t\t\t}\n\t\t}\n\t}, layer->lifetime());\n\twrap->update();\n}\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_reactions.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_message_reaction_id.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace Data {\nclass DocumentMedia;\nstruct ReactionId;\nclass Session;\nclass Story;\nstruct SuggestedReaction;\nstruct WeatherArea;\n} // namespace Data\n\nnamespace HistoryView::Reactions {\nclass Selector;\nstruct ChosenReaction;\nenum class AttachSelectorResult;\n} // namespace HistoryView::Reactions\n\nnamespace Ui {\nclass RpWidget;\nstruct ReactionFlyAnimationArgs;\nstruct ReactionFlyCenter;\nclass EmojiFlyAnimation;\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Media::Stories {\n\nclass Controller;\n\nenum class ReactionsMode {\n\tMessage,\n\tReaction,\n};\n\nclass StoryAreaView {\npublic:\n\tvirtual ~StoryAreaView() = default;\n\n\tvirtual void setAreaGeometry(QRect geometry, float64 radius) = 0;\n\tvirtual void setContentRect(QRect rect, int radius) = 0;\n\tvirtual void updateReactionsCount(int count) = 0;\n\tvirtual void playEffect() = 0;\n\tvirtual bool contains(QPoint point) = 0;\n};\n\nclass Reactions final {\npublic:\n\texplicit Reactions(not_null<Controller*> controller);\n\t~Reactions();\n\n\tusing Mode = ReactionsMode;\n\n\ttemplate <typename Reaction>\n\tstruct ChosenWrap {\n\t\tReaction reaction;\n\t\tMode mode;\n\t};\n\tusing Chosen = ChosenWrap<HistoryView::Reactions::ChosenReaction>;\n\n\t[[nodiscard]] rpl::producer<bool> activeValue() const;\n\t[[nodiscard]] rpl::producer<Chosen> chosen() const;\n\n\t[[nodiscard]] Data::ReactionId liked() const;\n\t[[nodiscard]] rpl::producer<Data::ReactionId> likedValue() const;\n\tvoid showLikeFrom(Data::Story *story);\n\n\tvoid hide();\n\tvoid outsidePressed();\n\tvoid toggleLiked();\n\tvoid applyLike(Data::ReactionId id);\n\tvoid ready();\n\n\t[[nodiscard]] auto makeSuggestedReactionWidget(\n\t\tconst Data::SuggestedReaction &reaction)\n\t-> std::unique_ptr<StoryAreaView>;\n\t[[nodiscard]] auto makeWeatherAreaWidget(\n\t\tconst Data::WeatherArea &data,\n\t\trpl::producer<bool> weatherInCelsius)\n\t-> std::unique_ptr<StoryAreaView>;\n\n\tvoid setReplyFieldState(\n\t\trpl::producer<bool> focused,\n\t\trpl::producer<bool> hasSendText);\n\tvoid attachToReactionButton(not_null<Ui::RpWidget*> button);\n\tvoid setReactionIconWidget(Ui::RpWidget *widget);\n\n\tvoid animateAndProcess(Chosen &&chosen);\n\n\tusing AttachStripResult = HistoryView::Reactions::AttachSelectorResult;\n\t[[nodiscard]] AttachStripResult attachToMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tQPoint desiredPosition);\n\nprivate:\n\tclass Panel;\n\n\tvoid assignLikedId(Data::ReactionId id);\n\t[[nodiscard]] Fn<void(Ui::ReactionFlyCenter)> setLikedIdIconInit(\n\t\tnot_null<Data::Session*> owner,\n\t\tData::ReactionId id,\n\t\tbool force = false);\n\tvoid setLikedIdFrom(Data::Story *story);\n\tvoid setLikedId(\n\t\tnot_null<Data::Session*> owner,\n\t\tData::ReactionId id,\n\t\tbool force = false);\n\tvoid startReactionAnimation(\n\t\tUi::ReactionFlyAnimationArgs from,\n\t\tnot_null<QWidget*> target,\n\t\tFn<void(Ui::ReactionFlyCenter)> done = nullptr);\n\tvoid waitForLikeIcon(\n\t\tnot_null<Data::Session*> owner,\n\t\tData::ReactionId id);\n\tvoid initLikeIcon(\n\t\tnot_null<Data::Session*> owner,\n\t\tData::ReactionId id,\n\t\tUi::ReactionFlyCenter center);\n\n\tconst not_null<Controller*> _controller;\n\tconst std::unique_ptr<Panel> _panel;\n\n\trpl::event_stream<Chosen> _chosen;\n\tbool _replyFocused = false;\n\tbool _hasSendText = false;\n\n\tUi::RpWidget *_likeIconWidget = nullptr;\n\trpl::variable<Data::ReactionId> _liked;\n\tbase::has_weak_ptr _likeIconGuard;\n\tstd::unique_ptr<Ui::RpWidget> _likeIcon;\n\tstd::shared_ptr<Data::DocumentMedia> _likeIconMedia;\n\n\tstd::unique_ptr<Ui::EmojiFlyAnimation> _reactionAnimation;\n\n\trpl::lifetime _likeIconWaitLifetime;\n\trpl::lifetime _likeFromLifetime;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/stories/media_stories_recent_views.h\"\n\n#include \"api/api_who_reacted.h\" // FormatReadDate.\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"history/history_item.h\"\n#include \"main/main_session.h\"\n#include \"media/stories/media_stories_controller.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/chat/group_call_userpics.h\"\n#include \"ui/controls/who_reacted_context_action.h\"\n#include \"ui/layers/box_content.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/painter.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/userpic_view.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Media::Stories {\nnamespace {\n\nconstexpr auto kAddPerPage = 50;\nconstexpr auto kLoadViewsPages = 2;\n\n[[nodiscard]] rpl::producer<std::vector<Ui::GroupCallUser>> ContentByUsers(\n\t\tconst std::vector<not_null<PeerData*>> &list) {\n\tstruct Userpic {\n\t\tnot_null<PeerData*> peer;\n\t\tmutable Ui::PeerUserpicView view;\n\t\tmutable InMemoryKey uniqueKey;\n\t};\n\n\tstruct State {\n\t\tstd::vector<Userpic> userpics;\n\t\tstd::vector<Ui::GroupCallUser> current;\n\t\tbase::has_weak_ptr guard;\n\t\tbool someUserpicsNotLoaded = false;\n\t\tbool scheduled = false;\n\t};\n\n\tstatic const auto size = st::storiesWhoViewed.userpics.size;\n\n\tstatic const auto GenerateUserpic = [](Userpic &userpic) {\n\t\tauto result = PeerData::GenerateUserpicImage(\n\t\t\tuserpic.peer,\n\t\t\tuserpic.view,\n\t\t\tsize * style::DevicePixelRatio());\n\t\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\t\treturn result;\n\t};\n\n\tstatic const auto RegenerateUserpics = [](not_null<State*> state) {\n\t\tExpects(state->userpics.size() == state->current.size());\n\n\t\tstate->someUserpicsNotLoaded = false;\n\t\tconst auto count = int(state->userpics.size());\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tauto &userpic = state->userpics[i];\n\t\t\tauto &participant = state->current[i];\n\t\t\tconst auto peer = userpic.peer;\n\t\t\tconst auto key = peer->userpicUniqueKey(userpic.view);\n\t\t\tif (peer->hasUserpic() && peer->useEmptyUserpic(userpic.view)) {\n\t\t\t\tstate->someUserpicsNotLoaded = true;\n\t\t\t}\n\t\t\tif (userpic.uniqueKey == key) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tparticipant.userpicKey = userpic.uniqueKey = key;\n\t\t\tparticipant.userpic = GenerateUserpic(userpic);\n\t\t}\n\t};\n\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tconst auto state = lifetime.make_state<State>();\n\t\tconst auto pushNext = [=] {\n\t\t\tRegenerateUserpics(state);\n\t\t\tconsumer.put_next_copy(state->current);\n\t\t};\n\n\t\tfor (const auto &peer : list) {\n\t\t\tstate->userpics.push_back(Userpic{\n\t\t\t\t.peer = peer,\n\t\t\t});\n\t\t\tstate->current.push_back(Ui::GroupCallUser{\n\t\t\t\t.id = uint64(peer->id.value),\n\t\t\t});\n\t\t\tpeer->loadUserpic();\n\t\t}\n\t\tpushNext();\n\n\t\tif (!list.empty()) {\n\t\t\tlist.front()->session().downloaderTaskFinished(\n\t\t\t) | rpl::filter([=] {\n\t\t\t\treturn state->someUserpicsNotLoaded && !state->scheduled;\n\t\t\t}) | rpl::on_next([=] {\n\t\t\t\tfor (const auto &userpic : state->userpics) {\n\t\t\t\t\tif (userpic.peer->userpicUniqueKey(userpic.view)\n\t\t\t\t\t\t!= userpic.uniqueKey) {\n\t\t\t\t\t\tstate->scheduled = true;\n\t\t\t\t\t\tcrl::on_main(&state->guard, [=] {\n\t\t\t\t\t\t\tstate->scheduled = false;\n\t\t\t\t\t\t\tpushNext();\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, lifetime);\n\t\t}\n\t\treturn lifetime;\n\t};\n}\n\n[[nodiscard]] QString ComposeRepostStatus(\n\t\tconst QString &date,\n\t\tnot_null<Data::Story*> repost) {\n\treturn date + (repost->repostModified()\n\t\t? (' ' + Ui::kQBullet + ' ' + tr::lng_edited(tr::now))\n\t\t: !repost->caption().empty()\n\t\t? (' ' + Ui::kQBullet + ' ' + tr::lng_commented(tr::now))\n\t\t: QString());\n}\n\n} // namespace\n\nRecentViewsType RecentViewsTypeFor(\n\t\tnot_null<PeerData*> peer,\n\t\tbool videoStream) {\n\treturn videoStream\n\t\t? RecentViewsType::Other\n\t\t: peer->isSelf()\n\t\t? RecentViewsType::Self\n\t\t: peer->isBroadcast()\n\t\t? RecentViewsType::Channel\n\t\t: peer->isServiceUser()\n\t\t? RecentViewsType::Changelog\n\t\t: RecentViewsType::Other;\n}\n\nbool CanViewReactionsFor(not_null<PeerData*> peer) {\n\tif (const auto channel = peer->asChannel()) {\n\t\treturn channel->amCreator() || channel->hasAdminRights();\n\t}\n\treturn false;\n}\n\nRecentViews::RecentViews(not_null<Controller*> controller)\n: _controller(controller) {\n}\n\nRecentViews::~RecentViews() = default;\n\nvoid RecentViews::show(\n\t\tRecentViewsData data,\n\t\trpl::producer<Data::ReactionId> likedValue) {\n\tconst auto guard = gsl::finally([&] {\n\t\tif (_likeIcon && likedValue) {\n\t\t\tstd::move(\n\t\t\t\tlikedValue\n\t\t\t) | rpl::map([](const Data::ReactionId &id) {\n\t\t\t\treturn !id.empty();\n\t\t\t}) | rpl::on_next([=](bool liked) {\n\t\t\t\tconst auto icon = liked\n\t\t\t\t\t? &st::storiesComposeControls.liked\n\t\t\t\t\t: &st::storiesLikesIcon;\n\t\t\t\t_likeIcon->setIconOverride(icon, icon);\n\t\t\t}, _likeIcon->lifetime());\n\t\t}\n\t});\n\n\tif (_data == data) {\n\t\treturn;\n\t}\n\tconst auto countersChanged = _text.isEmpty()\n\t\t|| (_data.total != data.total)\n\t\t|| (_data.views != data.views)\n\t\t|| (_data.forwards != data.forwards)\n\t\t|| (_data.reactions != data.reactions);\n\tconst auto usersChanged = !_userpics || (_data.list != data.list);\n\tconst auto canViewReactions = data.canViewReactions\n\t\t&& (data.reactions > 0 || data.forwards > 0);\n\t_data = data;\n\tif (_data.type != RecentViewsType::Self && !canViewReactions) {\n\t\t_text = {};\n\t\t_clickHandlerLifetime.destroy();\n\t\t_userpicsLifetime.destroy();\n\t\t_userpics = nullptr;\n\t\t_widget = nullptr;\n\t} else {\n\t\tif (!_widget) {\n\t\t\tsetupWidget();\n\t\t}\n\t\tif (!_userpics) {\n\t\t\tsetupUserpics();\n\t\t}\n\t\tif (countersChanged) {\n\t\t\tupdateText();\n\t\t}\n\t\tif (usersChanged) {\n\t\t\tupdateUserpics();\n\t\t}\n\t\trefreshClickHandler();\n\t}\n\n\tif (_data.type != RecentViewsType::Channel\n\t\t&& _data.type != RecentViewsType::Changelog) {\n\t\t_likeIcon = nullptr;\n\t\t_likeWrap = nullptr;\n\t\t_viewsWrap = nullptr;\n\t} else {\n\t\t_viewsCounter = (_data.type == RecentViewsType::Channel)\n\t\t\t? Lang::FormatCountDecimal(std::max(_data.views, 1))\n\t\t\t: tr::lng_stories_cant_reply(tr::now);\n\t\t_likesCounter = ((_data.type == RecentViewsType::Channel)\n\t\t\t&& _data.reactions)\n\t\t\t? Lang::FormatCountDecimal(_data.reactions)\n\t\t\t: QString();\n\t\tif (!_likeWrap || !_likeIcon || !_viewsWrap) {\n\t\t\tsetupViewsReactions();\n\t\t}\n\t}\n}\n\nUi::RpWidget *RecentViews::likeButton() const {\n\treturn _likeWrap.get();\n}\n\nUi::RpWidget *RecentViews::likeIconWidget() const {\n\treturn _likeIcon.get();\n}\n\nvoid RecentViews::refreshClickHandler() {\n\tconst auto nowEmpty = (_data.type != RecentViewsType::Channel)\n\t\t&& _data.list.empty();\n\tconst auto wasEmpty = !_clickHandlerLifetime;\n\tconst auto raw = _widget.get();\n\tif (wasEmpty == nowEmpty) {\n\t\treturn;\n\t} else if (nowEmpty) {\n\t\t_clickHandlerLifetime.destroy();\n\t} else {\n\t\t_clickHandlerLifetime = raw->events(\n\t\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\t\treturn (_data.total > 0)\n\t\t\t\t&& (e->type() == QEvent::MouseButtonPress)\n\t\t\t\t&& (static_cast<QMouseEvent*>(e.get())->button()\n\t\t\t\t\t== Qt::LeftButton);\n\t\t}) | rpl::on_next([=] {\n\t\t\tshowMenu();\n\t\t});\n\t}\n\traw->setCursor(_clickHandlerLifetime\n\t\t? style::cur_pointer\n\t\t: style::cur_default);\n}\n\nvoid RecentViews::updateUserpics() {\n\t_userpicsLifetime = ContentByUsers(\n\t\t_data.list\n\t) | rpl::on_next([=](\n\t\t\tconst std::vector<Ui::GroupCallUser> &list) {\n\t\t_userpics->update(list, true);\n\t});\n\t_userpics->finishAnimating();\n}\n\nvoid RecentViews::setupUserpics() {\n\t_userpics = std::make_unique<Ui::GroupCallUserpics>(\n\t\tst::storiesWhoViewed.userpics,\n\t\trpl::single(true),\n\t\t[=] { _widget->update(); });\n\n\t_userpics->widthValue() | rpl::on_next([=](int width) {\n\t\tif (_userpicsWidth != width) {\n\t\t\t_userpicsWidth = width;\n\t\t\tupdatePartsGeometry();\n\t\t}\n\t}, _widget->lifetime());\n}\n\nvoid RecentViews::setupWidget() {\n\t_widget = std::make_unique<Ui::RpWidget>(_controller->wrap());\n\tconst auto raw = _widget.get();\n\traw->show();\n\n\t_controller->layoutValue(\n\t) | rpl::on_next([=](const Layout &layout) {\n\t\t_outer = layout.views;\n\t\tupdatePartsGeometry();\n\t}, raw->lifetime());\n\n\traw->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = Painter(raw);\n\t\t_userpics->paint(\n\t\t\tp,\n\t\t\t_userpicsPosition.x(),\n\t\t\t_userpicsPosition.y(),\n\t\t\tst::storiesWhoViewed.userpics.size);\n\t\tp.setPen(st::storiesComposeWhiteText);\n\t\t_text.drawElided(\n\t\t\tp,\n\t\t\t_textPosition.x(),\n\t\t\t_textPosition.y(),\n\t\t\traw->width() - _userpicsWidth - st::storiesRecentViewsSkip);\n\t}, raw->lifetime());\n}\n\nvoid RecentViews::setupViewsReactions() {\n\t_viewsWrap = std::make_unique<Ui::RpWidget>(_controller->wrap());\n\t_likeWrap = std::make_unique<Ui::AbstractButton>(_controller->wrap());\n\t_likeIcon = std::make_unique<Ui::IconButton>(\n\t\t_likeWrap.get(),\n\t\tst::storiesComposeControls.like);\n\t_likeIcon->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t_controller->layoutValue(\n\t) | rpl::on_next([=](const Layout &layout) {\n\t\t_outer = QRect(\n\t\t\tlayout.content.x(),\n\t\t\tlayout.views.y(),\n\t\t\tlayout.content.width(),\n\t\t\tlayout.views.height());\n\t\tupdateViewsReactionsGeometry();\n\t}, _likeWrap->lifetime());\n\n\tconst auto views = Ui::CreateChild<Ui::FlatLabel>(\n\t\t_viewsWrap.get(),\n\t\t_viewsCounter.value(),\n\t\tst::storiesViewsText);\n\tviews->show();\n\tviews->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tviews->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto left = (_data.type == RecentViewsType::Changelog)\n\t\t\t? st::mediaviewCaptionPadding.left()\n\t\t\t: st::storiesViewsTextPosition.x();\n\t\tviews->move(left, st::storiesViewsTextPosition.y());\n\t\t_viewsWrap->resize(left + width, _likeIcon->height());\n\t\tupdateViewsReactionsGeometry();\n\t}, _viewsWrap->lifetime());\n\t_viewsWrap->paintRequest() | rpl::filter([=] {\n\t\treturn (_data.type != RecentViewsType::Changelog);\n\t}) | rpl::on_next([=] {\n\t\tauto p = QPainter(_viewsWrap.get());\n\t\tconst auto &icon = st::storiesViewsIcon;\n\t\ticon.paint(p, 0, st::storiesViewsIconTop, _viewsWrap->width());\n\t}, _viewsWrap->lifetime());\n\n\t_likeIcon->move(0, 0);\n\tconst auto likes = Ui::CreateChild<Ui::FlatLabel>(\n\t\t_likeWrap.get(),\n\t\t_likesCounter.value(),\n\t\tst::storiesLikesText);\n\tlikes->show();\n\tlikes->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tlikes->move(st::storiesLikesTextPosition);\n\n\tlikes->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\twidth += width\n\t\t\t? st::storiesLikesTextRightSkip\n\t\t\t: st::storiesLikesEmptyRightSkip;\n\t\t_likeWrap->resize(likes->x() + width, _likeIcon->height());\n\t\tupdateViewsReactionsGeometry();\n\t}, _likeWrap->lifetime());\n\n\t_viewsWrap->show();\n\t_likeIcon->show();\n\t_likeWrap->show();\n\n\t_likeWrap->setClickedCallback([=] {\n\t\t_controller->toggleLiked();\n\t});\n}\n\nvoid RecentViews::updateViewsReactionsGeometry() {\n\tconst auto outerWidth = (_data.type == RecentViewsType::Changelog)\n\t\t? std::max(_outer.width(), st::storiesChangelogFooterWidthMin)\n\t\t: _outer.width();\n\tconst auto outerOrigin = _outer.topLeft()\n\t\t+ QPoint((_outer.width() - outerWidth) / 2, 0);\n\t_viewsWrap->move(outerOrigin + st::storiesViewsPosition);\n\t_likeWrap->move(outerOrigin\n\t\t+ QPoint(outerWidth - _likeWrap->width(), 0)\n\t\t+ st::storiesLikesPosition);\n}\n\nvoid RecentViews::updatePartsGeometry() {\n\tconst auto skip = st::storiesRecentViewsSkip;\n\tconst auto full = _userpicsWidth + skip + _text.maxWidth();\n\tconst auto add = (_data.type == RecentViewsType::Channel)\n\t\t? st::storiesChannelReactionsTextTop\n\t\t: 0;\n\tconst auto use = std::min(full, _outer.width());\n\tconst auto ux = _outer.x() + (_outer.width() - use) / 2;\n\tconst auto uheight = st::storiesWhoViewed.userpics.size;\n\tconst auto uy = _outer.y() + (_outer.height() - uheight) / 2 + add;\n\tconst auto tx = ux + _userpicsWidth + skip;\n\tconst auto theight = st::normalFont->height;\n\tconst auto ty = _outer.y() + (_outer.height() - theight) / 2 + add;\n\tconst auto my = std::min(uy, ty);\n\tconst auto mheight = std::max(uheight, theight);\n\tconst auto padding = skip;\n\t_userpicsPosition = QPoint(padding, uy - my);\n\t_textPosition = QPoint(tx - ux + padding, ty - my);\n\t_widget->setGeometry(ux - padding, my, use + 2 * padding, mheight);\n\t_widget->update();\n}\n\nvoid RecentViews::updateText() {\n\tconst auto text = (_data.type == RecentViewsType::Channel)\n\t\t? tr::lng_stories_view_reactions(tr::now)\n\t\t: _data.views\n\t\t? (tr::lng_stories_views(tr::now, lt_count, _data.views)\n\t\t\t+ (_data.reactions\n\t\t\t\t? (u\"  \"_q + QChar(10084) + QString::number(_data.reactions))\n\t\t\t\t: QString()))\n\t\t: tr::lng_stories_no_views(tr::now);\n\t_text.setText(st::defaultTextStyle, text);\n\tupdatePartsGeometry();\n}\n\nvoid RecentViews::showMenu() {\n\tif (_menu\n\t\t|| (_data.type != RecentViewsType::Channel && _data.list.empty())) {\n\t\treturn;\n\t}\n\n\tconst auto views = _controller->views(kAddPerPage * 2, true);\n\tif (views.list.empty() && !views.total) {\n\t\treturn;\n\t}\n\n\tusing namespace Ui;\n\t_menuShortLifetime.destroy();\n\t_menu = base::make_unique_q<PopupMenu>(\n\t\t_widget.get(),\n\t\tst::storiesViewsMenu);\n\tauto count = 0;\n\tconst auto session = &_controller->story()->session();\n\tconst auto added = std::min(int(views.list.size()), kAddPerPage);\n\tconst auto add = std::min(views.total, kAddPerPage);\n\tconst auto now = QDateTime::currentDateTime();\n\tfor (const auto &entry : views.list) {\n\t\taddMenuRow(entry, now);\n\t\tif (++count >= add) {\n\t\t\tbreak;\n\t\t}\n\t}\n\twhile (count++ < add) {\n\t\taddMenuRowPlaceholder(session);\n\t}\n\trpl::merge(\n\t\t_controller->moreViewsLoaded(),\n\t\trpl::combine(\n\t\t\t_menu->scrollTopValue(),\n\t\t\t_menuEntriesCount.value()\n\t\t) | rpl::filter([=](int scrollTop, int count) {\n\t\t\tconst auto fullHeight = count\n\t\t\t\t* (st::defaultWhoRead.photoSkip * 2\n\t\t\t\t\t+ st::defaultWhoRead.photoSize);\n\t\t\treturn fullHeight\n\t\t\t\t< (scrollTop\n\t\t\t\t\t+ st::storiesViewsMenu.maxHeight * kLoadViewsPages);\n\t\t}) | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\trebuildMenuTail();\n\t}, _menuShortLifetime);\n\n\t_controller->setMenuShown(true);\n\t_menu->setDestroyedCallback(crl::guard(_widget.get(), [=] {\n\t\t_controller->setMenuShown(false);\n\t\t_waitingForUserpicsLifetime.destroy();\n\t\t_waitingForUserpics.clear();\n\t\t_menuShortLifetime.destroy();\n\t\t_menuEntries.clear();\n\t\t_menuEntriesCount = 0;\n\t\t_menuPlaceholderCount = 0;\n\t}));\n\n\tconst auto size = _menu->size();\n\tconst auto geometry = _widget->mapToGlobal(_widget->rect());\n\t_menu->setForcedVerticalOrigin(PopupMenu::VerticalOrigin::Bottom);\n\t_menu->popup(QPoint(\n\t\tgeometry.x() + (_widget->width() - size.width()) / 2,\n\t\tgeometry.y()));\n\n\t_menuEntriesCount = _menuEntriesCount.current() + added;\n}\n\nvoid RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) {\n\tExpects(_menu != nullptr);\n\n\tconst auto peer = entry.peer;\n\tconst auto repost = entry.repostId\n\t\t? peer->owner().stories().lookup({ peer->id, entry.repostId })\n\t\t: base::make_unexpected(Data::NoStory::Deleted);\n\tconst auto forward = entry.forwardId\n\t\t? peer->owner().message({ peer->id, entry.forwardId })\n\t\t: nullptr;\n\tconst auto date = Api::FormatReadDate(\n\t\trepost ? (*repost)->date() : forward ? forward->date() : entry.date,\n\t\tnow);\n\tconst auto type = forward\n\t\t? Ui::WhoReactedType::Forwarded\n\t\t: repost\n\t\t? Ui::WhoReactedType::Reposted\n\t\t: Ui::WhoReactedType::Viewed;\n\tconst auto status = repost ? ComposeRepostStatus(date, *repost) : date;\n\tconst auto show = _controller->uiShow();\n\tconst auto prepare = [&](Ui::PeerUserpicView &view) {\n\t\tconst auto size = st::storiesWhoViewed.photoSize;\n\t\tauto userpic = PeerData::GenerateUserpicImage(\n\t\t\tpeer,\n\t\t\tview,\n\t\t\tsize * style::DevicePixelRatio());\n\t\tuserpic.setDevicePixelRatio(style::DevicePixelRatio());\n\t\treturn Ui::WhoReactedEntryData{\n\t\t\t.text = peer->name(),\n\t\t\t.date = status,\n\t\t\t.type = type,\n\t\t\t.customEntityData = Data::ReactionEntityData(entry.reaction),\n\t\t\t.userpic = std::move(userpic),\n\t\t\t.callback = [=] { show->show(PrepareShortInfoBox(peer)); },\n\t\t};\n\t};\n\tif (_menuPlaceholderCount > 0) {\n\t\tconst auto i = _menuEntries.end() - (_menuPlaceholderCount--);\n\t\tauto data = prepare(i->view);\n\t\ti->peer = peer;\n\t\ti->type = type;\n\t\ti->status = status;\n\t\ti->customEntityData = data.customEntityData;\n\t\ti->callback = data.callback;\n\t\ti->action->setData(std::move(data));\n\t} else {\n\t\tauto view = Ui::PeerUserpicView();\n\t\tauto data = prepare(view);\n\t\tauto callback = data.callback;\n\t\tauto customEntityData = data.customEntityData;\n\t\tauto action = base::make_unique_q<Ui::WhoReactedEntryAction>(\n\t\t\t_menu->menu(),\n\t\t\tData::ReactedMenuFactory(&entry.peer->session()),\n\t\t\t_menu->menu()->st(),\n\t\t\tprepare(view));\n\t\tconst auto raw = action.get();\n\t\t_menu->addAction(std::move(action));\n\t\t_menuEntries.push_back({\n\t\t\t.action = raw,\n\t\t\t.peer = peer,\n\t\t\t.type = type,\n\t\t\t.status = status,\n\t\t\t.customEntityData = std::move(customEntityData),\n\t\t\t.callback = std::move(callback),\n\t\t\t.view = std::move(view),\n\t\t});\n\t}\n\tconst auto i = end(_menuEntries) - _menuPlaceholderCount - 1;\n\ti->key = peer->userpicUniqueKey(i->view);\n\tif (peer->hasUserpic() && peer->useEmptyUserpic(i->view)) {\n\t\tif (_waitingForUserpics.emplace(i - begin(_menuEntries)).second\n\t\t\t&& _waitingForUserpics.size() == 1) {\n\t\t\tsubscribeToMenuUserpicsLoading(&peer->session());\n\t\t}\n\t}\n}\n\nvoid RecentViews::addMenuRowPlaceholder(not_null<Main::Session*> session) {\n\tauto action = base::make_unique_q<Ui::WhoReactedEntryAction>(\n\t\t_menu->menu(),\n\t\tData::ReactedMenuFactory(session),\n\t\t_menu->menu()->st(),\n\t\tUi::WhoReactedEntryData{ .type = Ui::WhoReactedType::Preloader });\n\tconst auto raw = action.get();\n\t_menu->addAction(std::move(action));\n\t_menuEntries.push_back({ .action = raw });\n\t++_menuPlaceholderCount;\n}\n\nvoid RecentViews::rebuildMenuTail() {\n\tconst auto elements = _menuEntries.size() - _menuPlaceholderCount;\n\tconst auto views = _controller->views(elements + kAddPerPage, false);\n\tif (views.list.size() <= elements) {\n\t\treturn;\n\t}\n\tconst auto now = QDateTime::currentDateTime();\n\tconst auto added = std::min(\n\t\t_menuPlaceholderCount + kAddPerPage,\n\t\tint(views.list.size() - elements));\n\tconst auto height = _menu->height();\n\tfor (auto i = elements, till = i + added; i != till; ++i) {\n\t\tconst auto &entry = views.list[i];\n\t\taddMenuRow(entry, now);\n\t}\n\t_menuEntriesCount = _menuEntriesCount.current() + added;\n\tif (const auto delta = _menu->height() - height) {\n\t\t_menu->move(_menu->x(), _menu->y() - delta);\n\t}\n}\n\nvoid RecentViews::subscribeToMenuUserpicsLoading(\n\t\tnot_null<Main::Session*> session) {\n\t_shortAnimationPlaying = style::ShortAnimationPlaying();\n\t_waitingForUserpicsLifetime = rpl::merge(\n\t\t_shortAnimationPlaying.changes() | rpl::filter([=](bool playing) {\n\t\t\treturn !playing && _waitingUserpicsCheck;\n\t\t}) | rpl::to_empty,\n\t\tsession->downloaderTaskFinished(\n\t\t) | rpl::filter([=] {\n\t\t\tif (_shortAnimationPlaying.current()) {\n\t\t\t\t_waitingUserpicsCheck = true;\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t})\n\t) | rpl::on_next([=] {\n\t\t_waitingUserpicsCheck = false;\n\t\tfor (auto i = begin(_waitingForUserpics)\n\t\t\t; i != end(_waitingForUserpics)\n\t\t\t;) {\n\t\t\tauto &entry = _menuEntries[*i];\n\t\t\tauto &view = entry.view;\n\t\t\tconst auto peer = entry.peer;\n\t\t\tconst auto key = peer->userpicUniqueKey(view);\n\t\t\tconst auto update = (entry.key != key);\n\t\t\tif (update) {\n\t\t\t\tconst auto size = st::storiesWhoViewed.photoSize;\n\t\t\t\tauto userpic = PeerData::GenerateUserpicImage(\n\t\t\t\t\tpeer,\n\t\t\t\t\tview,\n\t\t\t\t\tsize * style::DevicePixelRatio());\n\t\t\t\tuserpic.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\t\tentry.action->setData({\n\t\t\t\t\t.text = peer->name(),\n\t\t\t\t\t.date = entry.status,\n\t\t\t\t\t.type = entry.type,\n\t\t\t\t\t.customEntityData = entry.customEntityData,\n\t\t\t\t\t.userpic = std::move(userpic),\n\t\t\t\t\t.callback = entry.callback,\n\t\t\t\t});\n\t\t\t\tentry.key = key;\n\t\t\t\tif (!peer->hasUserpic() || !peer->useEmptyUserpic(view)) {\n\t\t\t\t\ti = _waitingForUserpics.erase(i);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t\tif (_waitingForUserpics.empty()) {\n\t\t\t_waitingForUserpicsLifetime.destroy();\n\t\t}\n\t});\n}\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_recent_views.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n#include \"ui/text/text.h\"\n#include \"ui/userpic_view.h\"\n\nnamespace Data {\nstruct StoryView;\nstruct ReactionId;\n} // namespace Data\n\nnamespace Ui {\nclass AbstractButton;\nclass IconButton;\nclass RpWidget;\nclass GroupCallUserpics;\nclass PopupMenu;\nclass WhoReactedEntryAction;\nenum class WhoReactedType : uchar;\n} // namespace Ui\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Media::Stories {\n\nclass Controller;\n\nenum class RecentViewsType {\n\tOther,\n\tSelf,\n\tChannel,\n\tChangelog,\n};\n\nstruct RecentViewsData {\n\tstd::vector<not_null<PeerData*>> list;\n\tint reactions = 0;\n\tint forwards = 0;\n\tint views = 0;\n\tint total = 0;\n\tRecentViewsType type = RecentViewsType::Other;\n\tbool canViewReactions = false;\n\n\tfriend inline auto operator<=>(\n\t\tconst RecentViewsData &,\n\t\tconst RecentViewsData &) = default;\n\tfriend inline bool operator==(\n\t\tconst RecentViewsData &,\n\t\tconst RecentViewsData &) = default;\n};\n\n[[nodiscard]] RecentViewsType RecentViewsTypeFor(\n\tnot_null<PeerData*> peer,\n\tbool videoStream);\n[[nodiscard]] bool CanViewReactionsFor(not_null<PeerData*> peer);\n\nclass RecentViews final {\npublic:\n\texplicit RecentViews(not_null<Controller*> controller);\n\t~RecentViews();\n\n\tvoid show(\n\t\tRecentViewsData data,\n\t\trpl::producer<Data::ReactionId> likedValue = nullptr);\n\n\t[[nodiscard]] Ui::RpWidget *likeButton() const;\n\t[[nodiscard]] Ui::RpWidget *likeIconWidget() const;\n\nprivate:\n\tstruct MenuEntry {\n\t\tnot_null<Ui::WhoReactedEntryAction*> action;\n\t\tPeerData *peer = nullptr;\n\t\tUi::WhoReactedType type = {};\n\t\tQString status;\n\t\tQString customEntityData;\n\t\tFn<void()> callback;\n\t\tUi::PeerUserpicView view;\n\t\tInMemoryKey key;\n\t};\n\n\tvoid setupWidget();\n\tvoid setupUserpics();\n\tvoid updateUserpics();\n\tvoid updateText();\n\tvoid updatePartsGeometry();\n\tvoid showMenu();\n\n\tvoid setupViewsReactions();\n\tvoid updateViewsReactionsGeometry();\n\n\tvoid addMenuRow(Data::StoryView entry, const QDateTime &now);\n\tvoid addMenuRowPlaceholder(not_null<Main::Session*> session);\n\tvoid rebuildMenuTail();\n\tvoid subscribeToMenuUserpicsLoading(not_null<Main::Session*> session);\n\tvoid refreshClickHandler();\n\n\tconst not_null<Controller*> _controller;\n\n\tstd::unique_ptr<Ui::RpWidget> _widget;\n\tstd::unique_ptr<Ui::GroupCallUserpics> _userpics;\n\tUi::Text::String _text;\n\tRecentViewsData _data;\n\trpl::lifetime _userpicsLifetime;\n\n\trpl::variable<QString> _viewsCounter;\n\trpl::variable<QString> _likesCounter;\n\tstd::unique_ptr<Ui::RpWidget> _viewsWrap;\n\tstd::unique_ptr<Ui::AbstractButton> _likeWrap;\n\tstd::unique_ptr<Ui::IconButton> _likeIcon;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\trpl::lifetime _menuShortLifetime;\n\tstd::vector<MenuEntry> _menuEntries;\n\trpl::variable<int> _menuEntriesCount = 0;\n\tint _menuPlaceholderCount = 0;\n\tbase::flat_set<int> _waitingForUserpics;\n\trpl::variable<bool> _shortAnimationPlaying;\n\tbool _waitingUserpicsCheck = false;\n\trpl::lifetime _waitingForUserpicsLifetime;\n\trpl::lifetime _clickHandlerLifetime;\n\n\tQRect _outer;\n\tQPoint _userpicsPosition;\n\tQPoint _textPosition;\n\tint _userpicsWidth = 0;\n\n};\n\n} // namespace Media::Stories"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_reply.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/stories/media_stories_reply.h\"\n\n#include \"api/api_common.h\"\n#include \"api/api_sending.h\"\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"boxes/send_files_box.h\"\n#include \"boxes/share_box.h\" // ShareBoxStyleOverrides\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_messages.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/file_utilities.h\"\n#include \"core/mime_type.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"data/data_document.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_message_reaction_id.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/view/controls/compose_controls_common.h\"\n#include \"history/view/controls/history_view_compose_controls.h\"\n#include \"history/view/history_view_schedule_box.h\" // ScheduleBoxStyleArgs\n#include \"history/history_item_helpers.h\"\n#include \"history/history.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/session/send_as_peers.h\"\n#include \"main/main_session.h\"\n#include \"media/stories/media_stories_controller.h\"\n#include \"media/stories/media_stories_stealth.h\"\n#include \"media/view/media_view_video_stream.h\"\n#include \"menu/menu_send.h\"\n#include \"payments/ui/payments_reaction_box.h\" // MaxTopPaidDonorsShown\n#include \"settings/settings_credits_graphics.h\" // DarkCreditsEntryBoxStyle\n#include \"storage/localimageloader.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/round_rect.h\"\n#include \"window/section_widget.h\"\n#include \"styles/style_boxes.h\" // sendMediaPreviewSize.\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Media::Stories {\nnamespace {\n\n[[nodiscard]] rpl::producer<QString> PlaceholderText(\n\t\tconst std::shared_ptr<ChatHelpers::Show> &show,\n\t\trpl::producer<ReplyAreaType> type,\n\t\trpl::producer<int> starsPerMessage) {\n\treturn rpl::combine(\n\t\tshow->session().data().stories().stealthModeValue(),\n\t\tstd::move(type),\n\t\tstd::move(starsPerMessage)\n\t) | rpl::map([](\n\t\t\tData::StealthMode value,\n\t\t\tReplyAreaType type,\n\t\t\tint starsPerMessage) {\n\t\treturn std::tuple(value.enabledTill, type, starsPerMessage);\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::map([](TimeId till, ReplyAreaType type, int starsPerMessage) {\n\t\treturn rpl::single(\n\t\t\trpl::empty\n\t\t) | rpl::then(\n\t\t\tbase::timer_each(250)\n\t\t) | rpl::map([=] {\n\t\t\treturn till - base::unixtime::now();\n\t\t}) | rpl::take_while([](TimeId left) {\n\t\t\treturn left > 0;\n\t\t}) | rpl::then(\n\t\t\trpl::single(0)\n\t\t) | rpl::map([=](TimeId left) {\n\t\t\treturn (type == ReplyAreaType::VideoStreamComment)\n\t\t\t\t? (starsPerMessage\n\t\t\t\t\t? tr::lng_video_stream_comment_paid_ph(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single(starsPerMessage * 1.))\n\t\t\t\t\t: tr::lng_video_stream_comment_ph())\n\t\t\t\t: starsPerMessage\n\t\t\t\t? tr::lng_message_stars_ph(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(starsPerMessage * 1.))\n\t\t\t\t: left\n\t\t\t\t? tr::lng_stealth_mode_countdown(\n\t\t\t\t\tlt_left,\n\t\t\t\t\trpl::single(TimeLeftText(left)))\n\t\t\t\t: (type == ReplyAreaType::Comment)\n\t\t\t\t? tr::lng_story_comment_ph()\n\t\t\t\t: tr::lng_story_reply_ph();\n\t\t}) | rpl::flatten_latest();\n\t}) | rpl::flatten_latest();\n}\n\n[[nodiscard]] ChatHelpers::ComposeFeatures Features(\n\t\tbool videoStream,\n\t\tbool videoStreamManager) {\n\treturn {\n\t\t.likes = !videoStream,\n\t\t.sendAs = videoStream,\n\t\t.ttlInfo = false,\n\t\t.attachments = !videoStream,\n\t\t.botCommandSend = false,\n\t\t.silentBroadcastToggle = false,\n\t\t.attachBotsMenu = false,\n\t\t.inlineBots = false,\n\t\t.megagroupSet = false,\n\t\t.stickersSettings = false,\n\t\t.openStickerSets = false,\n\t\t.autocompleteHashtags = false,\n\t\t.autocompleteMentions = false,\n\t\t.autocompleteCommands = false,\n\t\t.recordMediaMessage = !videoStream,\n\t\t.editMessageStars = videoStream,\n\t\t.emojiOnlyPanel = videoStream,\n\t};\n}\n\n} // namespace\n\nclass ReplyArea::Cant final : public Ui::RpWidget {\npublic:\n\texplicit Cant(not_null<QWidget*> parent);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tUi::RoundRect _bg;\n\n};\n\nReplyArea::Cant::Cant(not_null<QWidget*> parent)\n: RpWidget(parent)\n, _bg(st::storiesRadius, st::storiesComposeBg) {\n\tshow();\n}\n\nvoid ReplyArea::Cant::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\t_bg.paint(p, rect());\n\n\tp.setPen(st::storiesComposeGrayText);\n\tp.setFont(st::normalFont);\n\tp.drawText(\n\t\trect(),\n\t\ttr::lng_stories_cant_reply(tr::now),\n\t\tstyle::al_center);\n}\n\nReplyArea::ReplyArea(not_null<Controller*> controller)\n: _controller(controller)\n, _controls(std::make_unique<HistoryView::ComposeControls>(\n\t_controller->wrap(),\n\tHistoryView::ComposeControlsDescriptor{\n\t\t.stOverride = &st::storiesComposeControls,\n\t\t.show = _controller->uiShow(),\n\t\t.unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) {\n\t\t\tshowPremiumToast(emoji);\n\t\t},\n\t\t.mode = HistoryView::ComposeControlsMode::Normal,\n\t\t.sendMenuDetails = sendMenuDetails(),\n\t\t.stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(),\n\t\t.customPlaceholder = PlaceholderText(\n\t\t\t_controller->uiShow(),\n\t\t\trpl::deferred([=] { return _type.value(); }),\n\t\t\trpl::deferred([=] { return _starsForMessage.value(); })),\n\t\t.voiceCustomCancelText = tr::lng_record_cancel_stories(tr::now),\n\t\t.voiceLockFromBottom = true,\n\t\t.features = Features(false, false),\n\t}\n)) {\n\tinitGeometry();\n\tinitActions();\n\t_controls->hide();\n}\n\nReplyArea::~ReplyArea() {\n}\n\nvoid ReplyArea::initGeometry() {\n\trpl::combine(\n\t\t_controller->layoutValue(),\n\t\t_controls->height()\n\t) | rpl::on_next([=](const Layout &layout, int height) {\n\t\tconst auto content = layout.content;\n\t\t_controls->resizeToWidth(layout.controlsWidth);\n\t\tif (_controls->heightCurrent() == height) {\n\t\t\tconst auto position = layout.controlsBottomPosition\n\t\t\t\t- QPoint(0, height);\n\t\t\t_controls->move(position.x(), position.y());\n\t\t\tconst auto &tabbed = st::storiesComposeControls.tabbed;\n\t\t\tconst auto upper = QRect(\n\t\t\t\tposition.x(),\n\t\t\t\tcontent.y(),\n\t\t\t\tlayout.controlsWidth,\n\t\t\t\t(position.y()\n\t\t\t\t\t+ tabbed.autocompleteBottomSkip\n\t\t\t\t\t- content.y()));\n\t\t\t_controls->setAutocompleteBoundingRect(\n\t\t\t\tlayout.autocompleteRect.intersected(upper));\n\t\t}\n\t}, _lifetime);\n}\n\nbool ReplyArea::sendReaction(const Data::ReactionId &id) {\n\tExpects(_data.peer != nullptr);\n\n\tauto message = Api::MessageToSend(prepareSendAction({}));\n\tif (const auto emoji = id.emoji(); !emoji.isEmpty()) {\n\t\tmessage.textWithTags = { emoji };\n\t} else if (const auto customId = id.custom()) {\n\t\tconst auto document = _data.peer->owner().document(customId);\n\t\tif (const auto sticker = document->sticker()) {\n\t\t\tconst auto text = sticker->alt;\n\t\t\tconst auto id = Data::SerializeCustomEmojiId(customId);\n\t\t\tmessage.textWithTags = {\n\t\t\t\ttext,\n\t\t\t\t{ { 0, int(text.size()), Ui::InputField::CustomEmojiLink(id) } }\n\t\t\t};\n\t\t}\n\t}\n\treturn !message.textWithTags.empty()\n\t\t&& send(std::move(message), true);\n}\n\nvoid ReplyArea::send(Api::SendOptions options) {\n\tauto text = _controls->getTextWithAppliedMarkdown();\n\tconst auto stars = _controls->chosenStarsForMessage();\n\tif (const auto stream = _videoStream.get()) {\n\t\tif (stars > 0) {\n\t\t\tconst auto weak = _videoStream;\n\t\t\tconst auto done = [=](Settings::SmallBalanceResult result) {\n\t\t\t\tif (result == Settings::SmallBalanceResult::Success\n\t\t\t\t\t|| result == Settings::SmallBalanceResult::Already) {\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->messages()->send(text, stars);\n\t\t\t\t\t\t_controls->clear();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\tusing namespace Settings;\n\t\t\tMaybeRequestBalanceIncrease(\n\t\t\t\t_controller->uiShow(),\n\t\t\t\tstars,\n\t\t\t\tSmallBalanceVideoStream{ stream->peer()->id },\n\t\t\t\tcrl::guard(this, done));\n\t\t} else {\n\t\t\tstream->messages()->send(std::move(text), stars);\n\t\t\t_controls->clear();\n\t\t}\n\t\treturn;\n\t}\n\tconst auto webPageDraft = _controls->webPageDraft();\n\n\tauto message = Api::MessageToSend(prepareSendAction(options));\n\tmessage.textWithTags = std::move(text);\n\tmessage.webPage = webPageDraft;\n\n\tsend(std::move(message));\n}\n\nbool ReplyArea::send(\n\t\tApi::MessageToSend message,\n\t\tbool skipToast) {\n\tif (!message.action.options.scheduled && showSlowmodeError()) {\n\t\treturn false;\n\t}\n\n\tauto request = SendingErrorRequest{\n\t\t.topicRootId = MsgId(0),\n\t\t.text = &message.textWithTags,\n\t\t.ignoreSlowmodeCountdown = (message.action.options.scheduled != 0),\n\t};\n\trequest.messagesCount = ComputeSendingMessagesCount(\n\t\tmessage.action.history,\n\t\trequest);\n\tconst auto error = GetErrorForSending(_data.peer, request);\n\tif (error) {\n\t\tData::ShowSendErrorToast(_controller->uiShow(), _data.peer, error);\n\t\treturn false;\n\t}\n\n\tif (!message.action.options.scheduled) {\n\t\tconst auto withPaymentApproved = [=](int approved) {\n\t\t\tauto copy = message;\n\t\t\tcopy.action.options.starsApproved = approved;\n\t\t\tsend(copy);\n\t\t};\n\t\tconst auto checked = checkSendPayment(\n\t\t\trequest.messagesCount,\n\t\t\tmessage.action.options,\n\t\t\twithPaymentApproved);\n\t\tif (!checked) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tsession().api().sendMessage(std::move(message));\n\n\tfinishSending(skipToast);\n\t_controls->clear();\n\treturn true;\n}\n\nbool ReplyArea::checkSendPayment(\n\t\tint messagesCount,\n\t\tApi::SendOptions options,\n\t\tFn<void(int)> withPaymentApproved) {\n\tconst auto st1 = ::Settings::DarkCreditsEntryBoxStyle();\n\tconst auto st2 = st1.shareBox.get();\n\tconst auto st3 = st2 ? st2->scheduleBox.get() : nullptr;\n\treturn _data.peer\n\t\t&& _sendPayment.check(\n\t\t\t_controller->uiShow(),\n\t\t\t_data.peer,\n\t\t\toptions,\n\t\t\tmessagesCount,\n\t\t\tstd::move(withPaymentApproved),\n\t\t\t{\n\t\t\t\t.label = st3 ? st3->chooseDateTimeArgs.labelStyle : nullptr,\n\t\t\t\t.checkbox = st2 ? st2->checkbox : nullptr,\n\t\t\t});\n}\n\nvoid ReplyArea::sendVoice(const VoiceToSend &data) {\n\tauto action = prepareSendAction(data.options);\n\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = data;\n\t\tcopy.options.starsApproved = approved;\n\t\tsendVoice(copy);\n\t};\n\tconst auto checked = checkSendPayment(\n\t\t1,\n\t\taction.options,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn;\n\t}\n\n\tsession().api().sendVoiceMessage(\n\t\tdata.bytes,\n\t\tdata.waveform,\n\t\tdata.duration,\n\t\tdata.video,\n\t\tstd::move(action));\n\n\t_controls->clearListenState();\n\tfinishSending();\n}\n\nbool ReplyArea::sendExistingDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tApi::MessageToSend messageToSend,\n\t\tstd::optional<MsgId> localId) {\n\tExpects(_data.peer != nullptr);\n\n\tconst auto show = _controller->uiShow();\n\tconst auto error = Data::RestrictionError(\n\t\t_data.peer,\n\t\tChatRestriction::SendStickers);\n\tif (error) {\n\t\tData::ShowSendErrorToast(show, _data.peer, error);\n\t\treturn false;\n\t} else if (showSlowmodeError()\n\t\t|| Window::ShowSendPremiumError(show, document)) {\n\t\treturn false;\n\t}\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = messageToSend;\n\t\tcopy.action.options.starsApproved = approved;\n\t\tsendExistingDocument(document, std::move(copy), localId);\n\t};\n\tconst auto checked = checkSendPayment(\n\t\t1,\n\t\tmessageToSend.action.options,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn false;\n\t}\n\n\tApi::SendExistingDocument(std::move(messageToSend), document, localId);\n\n\t_controls->cancelReplyMessage();\n\tfinishSending();\n\treturn true;\n}\n\nvoid ReplyArea::sendExistingPhoto(not_null<PhotoData*> photo) {\n\tsendExistingPhoto(photo, {});\n}\n\nbool ReplyArea::sendExistingPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tApi::SendOptions options) {\n\tExpects(_data.peer != nullptr);\n\n\tconst auto show = _controller->uiShow();\n\tconst auto error = Data::RestrictionError(\n\t\t_data.peer,\n\t\tChatRestriction::SendPhotos);\n\tif (error) {\n\t\tData::ShowSendErrorToast(show, _data.peer, error);\n\t\treturn false;\n\t} else if (showSlowmodeError()) {\n\t\treturn false;\n\t}\n\tconst auto action = prepareSendAction(options);\n\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = options;\n\t\tcopy.starsApproved = approved;\n\t\tsendExistingPhoto(photo, copy);\n\t};\n\tconst auto checked = checkSendPayment(\n\t\t1,\n\t\taction.options,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn false;\n\t}\n\n\tApi::SendExistingPhoto(Api::MessageToSend(action), photo);\n\n\t_controls->cancelReplyMessage();\n\tfinishSending();\n\treturn true;\n}\n\nvoid ReplyArea::sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot) {\n\tif (const auto error = result->getErrorOnSend(history())) {\n\t\tconst auto show = _controller->uiShow();\n\t\tData::ShowSendErrorToast(show, history()->peer, error);\n\t\treturn;\n\t}\n\tsendInlineResult(result, bot, {}, std::nullopt);\n}\n\nvoid ReplyArea::sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot,\n\t\tApi::SendOptions options,\n\t\tstd::optional<MsgId> localMessageId) {\n\tauto action = prepareSendAction(options);\n\taction.generateLocal = true;\n\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = options;\n\t\tcopy.starsApproved = approved;\n\t\tsendInlineResult(result, bot, copy, localMessageId);\n\t};\n\tconst auto checked = checkSendPayment(\n\t\t1,\n\t\taction.options,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn;\n\t}\n\n\tsession().api().sendInlineResult(\n\t\tbot,\n\t\tresult.get(),\n\t\taction,\n\t\tlocalMessageId);\n\n\tauto &bots = cRefRecentInlineBots();\n\tconst auto index = bots.indexOf(bot);\n\tif (index) {\n\t\tif (index > 0) {\n\t\t\tbots.removeAt(index);\n\t\t} else if (bots.size() >= RecentInlineBotsLimit) {\n\t\t\tbots.resize(RecentInlineBotsLimit - 1);\n\t\t}\n\t\tbots.push_front(bot);\n\t\tbot->session().local().writeRecentHashtagsAndBots();\n\t}\n\tfinishSending();\n\t_controls->clear();\n}\n\nvoid ReplyArea::finishSending(bool skipToast) {\n\t_controls->hidePanelsAnimated();\n\t_controller->unfocusReply();\n\tif (!skipToast) {\n\t\t_controller->uiShow()->showToast(\n\t\t\ttr::lng_stories_reply_sent(tr::now));\n\t}\n}\n\nvoid ReplyArea::uploadFile(\n\t\tconst QByteArray &fileContent,\n\t\tSendMediaType type) {\n\tsession().api().sendFile(fileContent, type, prepareSendAction({}));\n}\n\nbool ReplyArea::showSendingFilesError(\n\t\tconst Ui::PreparedList &list) const {\n\tconst auto show = _controller->uiShow();\n\tconst auto peer = _data.peer;\n\treturn Data::ShowSendError(show, peer, list, std::nullopt, true);\n}\n\nbool ReplyArea::showSendingFilesError(\n\t\tconst Ui::PreparedBundle &bundle) const {\n\tconst auto show = _controller->uiShow();\n\treturn Data::ShowSendError(show, _data.peer, bundle, true);\n}\n\nnot_null<History*> ReplyArea::history() const {\n\tExpects(_data.peer != nullptr);\n\n\treturn _data.peer->owner().history(_data.peer);\n}\n\nApi::SendAction ReplyArea::prepareSendAction(\n\t\tApi::SendOptions options) const {\n\tExpects(_data.peer != nullptr);\n\n\tauto result = Api::SendAction(history(), options);\n\tresult.options.sendAs = _controls->sendAsPeer();\n\tresult.replyTo.storyId = { .peer = _data.peer->id, .story = _data.id };\n\treturn result;\n}\n\nvoid ReplyArea::chooseAttach(\n\t\tstd::optional<bool> overrideSendImagesAsPhotos) {\n\t_chooseAttachRequest = false;\n\tif (!_data.peer) {\n\t\treturn;\n\t}\n\tconst auto peer = not_null(_data.peer);\n\tif (const auto error = Data::AnyFileRestrictionError(peer)) {\n\t\tData::ShowSendErrorToast(_controller->uiShow(), peer, error);\n\t\treturn;\n\t} else if (showSlowmodeError()) {\n\t\treturn;\n\t}\n\n\tconst auto filter = (overrideSendImagesAsPhotos == true)\n\t\t? FileDialog::PhotoVideoFilesFilter()\n\t\t: FileDialog::AllOrImagesFilter();\n\tconst auto weak = make_weak(&_shownPeerGuard);\n\tconst auto callback = [=](FileDialog::OpenResult &&result) {\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\t_choosingAttach = false;\n\t\t});\n\t\tif (!weak\n\t\t\t|| (result.paths.isEmpty() && result.remoteContent.isEmpty())) {\n\t\t\treturn;\n\t\t} else if (!result.remoteContent.isEmpty()) {\n\t\t\tauto read = Images::Read({\n\t\t\t\t.content = result.remoteContent,\n\t\t\t});\n\t\t\tif (!read.image.isNull() && !read.animated) {\n\t\t\t\tconfirmSendingFiles(\n\t\t\t\t\tstd::move(read.image),\n\t\t\t\t\tstd::move(result.remoteContent),\n\t\t\t\t\toverrideSendImagesAsPhotos);\n\t\t\t} else {\n\t\t\t\tuploadFile(result.remoteContent, SendMediaType::File);\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto premium = session().premium();\n\t\t\tauto list = Storage::PrepareMediaList(\n\t\t\t\tresult.paths,\n\t\t\t\tst::sendMediaPreviewSize,\n\t\t\t\tpremium);\n\t\t\tlist.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;\n\t\t\tconfirmSendingFiles(std::move(list));\n\t\t}\n\t};\n\n\t_choosingAttach = true;\n\tFileDialog::GetOpenPaths(\n\t\t_controller->wrap().get(),\n\t\ttr::lng_choose_files(tr::now),\n\t\tfilter,\n\t\tcrl::guard(this, callback),\n\t\tcrl::guard(this, [=] { _choosingAttach = false; }));\n}\n\nFn<SendMenu::Details()> ReplyArea::sendMenuDetails() const {\n\treturn crl::guard(this, [=] {\n\t\tconst auto call = _videoStream\n\t\t\t? _videoStream->lookupReal()\n\t\t\t: nullptr;\n\t\treturn SendMenu::Details{\n\t\t\t.type = (!_data.videoStream\n\t\t\t\t? SendMenu::Type::SilentOnly\n\t\t\t\t: !call\n\t\t\t\t? SendMenu::Type::Disabled\n\t\t\t\t: SendMenu::Type::EditCommentPrice),\n\t\t\t.commentStreamerName = (call\n\t\t\t\t? call->peer()->shortName()\n\t\t\t\t: QString()),\n\t\t\t.price = (_data.videoStream\n\t\t\t\t? uint64(_controls->chosenStarsForMessage())\n\t\t\t\t: std::optional<uint64>()),\n\t\t\t.commentPriceMin = (call\n\t\t\t\t? uint64(call->canManage() ? call->messagesMinPrice() : 0)\n\t\t\t\t: std::optional<uint64>()),\n\t\t\t.effectAllowed = (!_data.videoStream\n\t\t\t\t&& _data.peer\n\t\t\t\t&& _data.peer->isUser()),\n\t\t};\n\t});\n}\n\nbool ReplyArea::confirmSendingFiles(\n\t\tnot_null<const QMimeData*> data,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos,\n\t\tconst QString &insertTextOnCancel) {\n\tconst auto hasImage = data->hasImage();\n\tconst auto premium = session().user()->isPremium();\n\n\tif (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) {\n\t\tauto list = Storage::PrepareMediaList(\n\t\t\turls,\n\t\t\tst::sendMediaPreviewSize,\n\t\t\tpremium);\n\t\tif (list.error != Ui::PreparedList::Error::NonLocalUrl) {\n\t\t\tif (list.error == Ui::PreparedList::Error::None\n\t\t\t\t|| !hasImage) {\n\t\t\t\tconst auto emptyTextOnCancel = QString();\n\t\t\t\tlist.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;\n\t\t\t\tconfirmSendingFiles(std::move(list), emptyTextOnCancel);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (auto read = Core::ReadMimeImage(data)) {\n\t\tconfirmSendingFiles(\n\t\t\tstd::move(read.image),\n\t\t\tstd::move(read.content),\n\t\t\toverrideSendImagesAsPhotos,\n\t\t\tinsertTextOnCancel);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool ReplyArea::confirmSendingFiles(\n\t\tUi::PreparedList &&list,\n\t\tconst QString &insertTextOnCancel) {\n\tif (_controls->confirmMediaEdit(list)) {\n\t\treturn true;\n\t} else if (showSendingFilesError(list)) {\n\t\treturn false;\n\t}\n\n\tconst auto show = _controller->uiShow();\n\tauto confirmed = [=](auto &&...args) {\n\t\tsendingFilesConfirmed(std::forward<decltype(args)>(args)...);\n\t};\n\tshow->show(Box<SendFilesBox>(SendFilesBoxDescriptor{\n\t\t.show = show,\n\t\t.list = std::move(list),\n\t\t.caption = _controls->getTextWithAppliedMarkdown(),\n\t\t.toPeer = _data.peer,\n\t\t.limits = DefaultLimitsForPeer(_data.peer),\n\t\t.check = DefaultCheckForPeer(show, _data.peer),\n\t\t.sendType = Api::SendType::Normal,\n\t\t.sendMenuDetails = sendMenuDetails(),\n\t\t.stOverride = &st::storiesComposeControls,\n\t\t.confirmed = crl::guard(this, confirmed),\n\t\t.cancelled = _controls->restoreTextCallback(insertTextOnCancel),\n\t}));\n\n\treturn true;\n}\n\nvoid ReplyArea::sendingFilesConfirmed(\n\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\tApi::SendOptions options) {\n\tif (showSendingFilesError(*bundle)) {\n\t\treturn;\n\t}\n\tconst auto compress = bundle->way.sendImagesAsPhotos();\n\tconst auto type = compress ? SendMediaType::Photo : SendMediaType::File;\n\tauto action = prepareSendAction(options);\n\taction.clearDraft = false;\n\n\tconst auto withPaymentApproved = [=](int approved) {\n\t\tauto copy = options;\n\t\tcopy.starsApproved = approved;\n\t\tsendingFilesConfirmed(bundle, copy);\n\t};\n\tconst auto checked = checkSendPayment(\n\t\tbundle->totalCount,\n\t\taction.options,\n\t\twithPaymentApproved);\n\tif (!checked) {\n\t\treturn;\n\t}\n\n\tauto &api = session().api();\n\tfor (auto &group : bundle->groups) {\n\t\tconst auto album = (group.type != Ui::AlbumType::None)\n\t\t\t? std::make_shared<SendingAlbum>()\n\t\t\t: nullptr;\n\t\tapi.sendFiles(std::move(group.list), type, album, action);\n\t}\n\tfinishSending();\n}\n\nbool ReplyArea::confirmSendingFiles(\n\t\tQImage &&image,\n\t\tQByteArray &&content,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos,\n\t\tconst QString &insertTextOnCancel) {\n\tif (image.isNull()) {\n\t\treturn false;\n\t}\n\n\tauto list = Storage::PrepareMediaFromImage(\n\t\tstd::move(image),\n\t\tstd::move(content),\n\t\tst::sendMediaPreviewSize);\n\tlist.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;\n\treturn confirmSendingFiles(std::move(list), insertTextOnCancel);\n}\n\nvoid ReplyArea::initActions() {\n\t_controls->cancelRequests(\n\t) | rpl::on_next([=] {\n\t\t_controller->unfocusReply();\n\t}, _lifetime);\n\n\t_controls->sendRequests(\n\t) | rpl::on_next([=](Api::SendOptions options) {\n\t\tsend(options);\n\t}, _lifetime);\n\n\t_controls->sendVoiceRequests(\n\t) | rpl::on_next([=](const VoiceToSend &data) {\n\t\tsendVoice(data);\n\t}, _lifetime);\n\n\t_controls->attachRequests(\n\t) | rpl::filter([=] {\n\t\treturn !_chooseAttachRequest;\n\t}) | rpl::on_next([=](std::optional<bool> overrideCompress) {\n\t\t_chooseAttachRequest = true;\n\t\tbase::call_delayed(\n\t\t\tst::storiesAttach.ripple.hideDuration,\n\t\t\tthis,\n\t\t\t[=] { chooseAttach(overrideCompress); });\n\t}, _lifetime);\n\n\t_controls->setSendAsFileConfirmed(crl::guard(this, [=](\n\t\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\t\tApi::SendOptions options) {\n\t\tsendingFilesConfirmed(std::move(bundle), options);\n\t}));\n\n\t_controls->fileChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\t_controller->uiShow()->hideLayer();\n\t\tauto messageToSend = Api::MessageToSend(\n\t\t\tprepareSendAction(data.options));\n\t\tmessageToSend.textWithTags = base::take(data.caption);\n\t\tsendExistingDocument(\n\t\t\tdata.document,\n\t\t\tstd::move(messageToSend),\n\t\t\tdata.messageSendingFrom.localId);\n\t}, _lifetime);\n\n\t_controls->photoChosen(\n\t) | rpl::on_next([=](ChatHelpers::PhotoChosen chosen) {\n\t\tsendExistingPhoto(chosen.photo, chosen.options);\n\t}, _lifetime);\n\n\t_controls->inlineResultChosen(\n\t) | rpl::on_next([=](ChatHelpers::InlineChosen chosen) {\n\t\tconst auto localId = chosen.messageSendingFrom.localId;\n\t\tsendInlineResult(chosen.result, chosen.bot, chosen.options, localId);\n\t}, _lifetime);\n\n\t_controls->likeToggled(\n\t) | rpl::on_next([=] {\n\t\t_controller->toggleLiked();\n\t}, _lifetime);\n\n\t_controls->setMimeDataHook([=](\n\t\t\tnot_null<const QMimeData*> data,\n\t\t\tUi::InputField::MimeAction action) {\n\t\tif (action == Ui::InputField::MimeAction::Check) {\n\t\t\treturn Core::CanSendFiles(data);\n\t\t} else if (action == Ui::InputField::MimeAction::Insert) {\n\t\t\treturn confirmSendingFiles(\n\t\t\t\tdata,\n\t\t\t\tstd::nullopt,\n\t\t\t\tCore::ReadMimeText(data));\n\t\t}\n\t\tUnexpected(\"action in MimeData hook.\");\n\t});\n\n\t_controls->lockShowStarts(\n\t) | rpl::on_next([=] {\n\t}, _lifetime);\n\n\t_controls->show();\n\t_controls->finishAnimating();\n\t_controls->showFinished();\n}\n\nvoid ReplyArea::show(\n\t\tReplyAreaData data,\n\t\trpl::producer<Data::ReactionId> likedValue) {\n\tif (_data == data) {\n\t\treturn;\n\t}\n\tconst auto stream = data.videoStream.get();\n\tconst auto peerChanged = (_data.peer != data.peer);\n\tconst auto streamChanged = (_data.videoStream.get() != stream);\n\t_data = data;\n\tif (streamChanged) {\n\t\tconst auto manager = stream && stream->canManage();\n\t\t_controls->updateFeatures(Features(stream != nullptr, manager));\n\t\t_controls->setToggleCommentsButton(stream\n\t\t\t? _controller->commentsStateValue()\n\t\t\t: nullptr);\n\t\t_controller->setCommentsShownToggles(\n\t\t\t_controls->commentsShownToggles());\n\t}\n\tusing Controls = HistoryView::ComposeControls;\n\t_controls->setStarsReactionCounter(\n\t\tstream ? _controller->starsReactionsValue() : nullptr,\n\t\tstream ? _controller->starsReactionsEffects() : nullptr);\n\t_controller->setStarsReactionIncrements(\n\t\t_controls->starsReactionIncrements(\n\t\t) | rpl::map([](Controls::StarReactionIncrement increment) {\n\t\t\treturn increment.count;\n\t\t}));\n\t_starsForMessage = starsPerMessageValue();\n\tif (!peerChanged) {\n\t\tif (_data.peer) {\n\t\t\t_controls->clear();\n\t\t}\n\t\treturn;\n\t}\n\tinvalidate_weak_ptrs(&_shownPeerGuard);\n\tconst auto peer = data.peer;\n\tconst auto history = peer ? peer->owner().history(peer).get() : nullptr;\n\tconst auto user = peer->asUser();\n\t_type = peer->isMegagroup()\n\t\t? ReplyAreaType::Comment\n\t\t: ReplyAreaType::Reply;\n\tauto writeRestriction = stream\n\t\t? rpl::combine(\n\t\t\tstream->messagesEnabledValue(),\n\t\t\tstream->loadedValue()\n\t\t) | rpl::map([=](bool enabled, bool loaded) {\n\t\t\tusing namespace HistoryView::Controls;\n\t\t\treturn !loaded\n\t\t\t\t? WriteRestriction{ .type = WriteRestrictionType::Hidden }\n\t\t\t\t: enabled\n\t\t\t\t? WriteRestriction()\n\t\t\t\t: WriteRestriction{\n\t\t\t\t\t.text = tr::lng_video_stream_comments_disabled(tr::now),\n\t\t\t\t\t.type = WriteRestrictionType::Rights,\n\t\t\t\t};\n\t\t}) | rpl::type_erased\n\t\t: Data::CanSendAnythingValue(\n\t\t\tpeer\n\t\t) | rpl::map([=](bool can) {\n\t\t\tusing namespace HistoryView::Controls;\n\t\t\treturn peer->session().frozen()\n\t\t\t\t? WriteRestriction{ .type = WriteRestrictionType::Frozen }\n\t\t\t\t: (can\n\t\t\t\t|| !user\n\t\t\t\t|| !user->requiresPremiumToWrite()\n\t\t\t\t|| user->session().premium())\n\t\t\t\t? WriteRestriction()\n\t\t\t\t: WriteRestriction{\n\t\t\t\t\t.text = tr::lng_send_non_premium_story(tr::now),\n\t\t\t\t\t.button = tr::lng_send_non_premium_unlock(tr::now),\n\t\t\t\t\t.type = WriteRestrictionType::PremiumRequired,\n\t\t\t\t};\n\t\t});\n\tusing namespace HistoryView;\n\t_controls->setHistory({\n\t\t.history = history,\n\t\t.videoStream = _data.videoStream,\n\t\t.showSlowmodeError = [=] { return showSlowmodeError(); },\n\t\t.sendActionFactory = [=] { return prepareSendAction({}); },\n\t\t.slowmodeSecondsLeft = SlowmodeSecondsLeft(history->peer),\n\t\t.sendDisabledBySlowmode = SendDisabledBySlowmode(history->peer),\n\t\t.liked = std::move(\n\t\t\tlikedValue\n\t\t) | rpl::map([](const Data::ReactionId &id) {\n\t\t\treturn !id.empty();\n\t\t}),\n\t\t.minStarsCount = (stream\n\t\t\t? _starsForMessage.value()\n\t\t\t: rpl::producer<int>()),\n\t\t.writeRestriction = std::move(writeRestriction),\n\t});\n\t_controls->clear();\n\tconst auto hidden = peer\n\t\t&& (peer->isBroadcast() || peer->isSelf() || peer->isServiceUser())\n\t\t&& !stream;\n\tconst auto cant = !peer;\n\tif (!hidden && !cant) {\n\t\t_controls->show();\n\t} else {\n\t\t_controls->hide();\n\t\tif (cant) {\n\t\t\t_cant = std::make_unique<Cant>(_controller->wrap());\n\t\t\t_controller->layoutValue(\n\t\t\t) | rpl::on_next([=](const Layout &layout) {\n\t\t\t\tconst auto height = st::storiesComposeControls.attach.height;\n\t\t\t\tconst auto position = layout.controlsBottomPosition\n\t\t\t\t\t- QPoint(0, height);\n\t\t\t\t_cant->setGeometry(\n\t\t\t\t\t{ position, QSize{ layout.controlsWidth, height } });\n\t\t\t}, _cant->lifetime());\n\t\t} else {\n\t\t\t_cant = nullptr;\n\t\t}\n\t}\n}\n\nrpl::producer<int> ReplyArea::starsPerMessageValue() const {\n\tif (const auto stream = _data.videoStream.get()) {\n\t\treturn rpl::combine(\n\t\t\tData::CanManageGroupCallValue(stream->peer()),\n\t\t\tstream->messagesMinPriceValue()\n\t\t) | rpl::map([=](bool canManage, int price) {\n\t\t\treturn canManage ? 0 : price;\n\t\t});\n\t} else if (const auto peer = _data.peer) {\n\t\tusing Flag = Data::PeerUpdate::Flag;\n\t\treturn peer->session().changes().peerFlagsValue(\n\t\t\tpeer,\n\t\t\tFlag::StarsPerMessage | Flag::FullInfo\n\t\t) | rpl::map([=] {\n\t\t\treturn peer->starsPerMessageChecked();\n\t\t});\n\t}\n\treturn rpl::single(0);\n}\n\nvoid ReplyArea::updateVideoStream(not_null<Calls::GroupCall*> videoStream) {\n\t_type = ReplyAreaType::VideoStreamComment;\n\t_videoStream = videoStream;\n\t_controls->setStarsReactionTop(View::TopVideoStreamDonors(videoStream));\n}\n\nbool ReplyArea::showSlowmodeError() {\n\tconst auto text = [&] {\n\t\tconst auto story = _controller->story();\n\t\tif (!story) {\n\t\t\treturn QString();\n\t\t}\n\t\tconst auto peer = story->peer();\n\t\tif (const auto left = peer->slowmodeSecondsLeft()) {\n\t\t\treturn tr::lng_slowmode_enabled(\n\t\t\t\ttr::now,\n\t\t\t\tlt_left,\n\t\t\t\tUi::FormatDurationWordsSlowmode(left));\n\t\t} else if (peer->slowmodeApplied()) {\n\t\t\tif (peer->owner().history(peer)->latestSendingMessage()) {\n\t\t\t\treturn tr::lng_slowmode_no_many(tr::now);\n\t\t\t}\n\t\t}\n\t\treturn QString();\n\t}();\n\tif (text.isEmpty()) {\n\t\treturn false;\n\t}\n\t_controller->uiShow()->showToast(text);\n\treturn true;\n}\n\nMain::Session &ReplyArea::session() const {\n\tExpects(_data.peer != nullptr);\n\n\treturn _data.peer->session();\n}\n\nbool ReplyArea::focused() const {\n\treturn _controls->focused();\n}\n\nrpl::producer<bool> ReplyArea::focusedValue() const {\n\treturn _controls->focusedValue();\n}\n\nrpl::producer<bool> ReplyArea::hasSendTextValue() const {\n\treturn _controls->hasSendTextValue();\n}\n\nrpl::producer<bool> ReplyArea::activeValue() const {\n\tusing namespace rpl::mappers;\n\treturn rpl::combine(\n\t\t_controls->focusedValue(),\n\t\t_controls->recordingActiveValue(),\n\t\t_controls->tabbedPanelShownValue(),\n\t\t_controls->fieldMenuShownValue(),\n\t\t_choosingAttach.value(),\n\t\t_1 || _2 || _3 || _4 || _5\n\t) | rpl::distinct_until_changed();\n}\n\nbool ReplyArea::ignoreWindowMove(QPoint position) const {\n\treturn _controls->isRecordingPressed();\n}\n\nvoid ReplyArea::tryProcessKeyInput(not_null<QKeyEvent*> e) {\n\t_controls->tryProcessKeyInput(e);\n}\n\nUi::RpWidget *ReplyArea::likeAnimationTarget() const {\n\treturn _controls->likeAnimationTarget();\n}\n\nvoid ReplyArea::showPremiumToast(not_null<DocumentData*> emoji) {\n\t// #TODO stories\n}\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_reply.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"history/history_item_helpers.h\"\n\nclass History;\nenum class SendMediaType;\n\nnamespace Api {\nstruct MessageToSend;\nstruct SendAction;\nstruct SendOptions;\n} // namespace Api\n\nnamespace Calls {\nclass GroupCall;\n} // namespace Calls\n\nnamespace Data {\nclass GroupCall;\nstruct ReactionId;\n} // namespace Data\n\nnamespace HistoryView {\nclass ComposeControls;\n} // namespace HistoryView\n\nnamespace HistoryView::Controls {\nstruct VoiceToSend;\n} // namespace HistoryView::Controls\n\nnamespace InlineBots {\nclass Result;\n} // namespace InlineBots\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace SendMenu {\nstruct Details;\n} // namespace SendMenu\n\nnamespace Ui {\nstruct PreparedList;\nstruct PreparedBundle;\nclass SendFilesWay;\nclass RpWidget;\n} // namespace Ui\n\nnamespace Media::Stories {\n\nclass Controller;\n\nstruct ReplyAreaData {\n\tPeerData *peer = nullptr;\n\tStoryId id = 0;\n\tstd::shared_ptr<Data::GroupCall> videoStream;\n\n\tfriend inline auto operator<=>(ReplyAreaData, ReplyAreaData) = default;\n\tfriend inline bool operator==(ReplyAreaData, ReplyAreaData) = default;\n};\n\nenum class ReplyAreaType {\n\tReply,\n\tComment,\n\tVideoStreamComment,\n};\n\nclass ReplyArea final : public base::has_weak_ptr {\npublic:\n\texplicit ReplyArea(not_null<Controller*> controller);\n\t~ReplyArea();\n\n\tvoid show(\n\t\tReplyAreaData data,\n\t\trpl::producer<Data::ReactionId> likedValue);\n\tbool sendReaction(const Data::ReactionId &id);\n\n\t[[nodiscard]] bool focused() const;\n\t[[nodiscard]] rpl::producer<bool> focusedValue() const;\n\t[[nodiscard]] rpl::producer<bool> activeValue() const;\n\t[[nodiscard]] rpl::producer<bool> hasSendTextValue() const;\n\n\t[[nodiscard]] bool ignoreWindowMove(QPoint position) const;\n\tvoid tryProcessKeyInput(not_null<QKeyEvent*> e);\n\n\t[[nodiscard]] Ui::RpWidget *likeAnimationTarget() const;\n\n\tvoid updateVideoStream(not_null<Calls::GroupCall*> videoStream);\n\nprivate:\n\tclass Cant;\n\n\tusing VoiceToSend = HistoryView::Controls::VoiceToSend;\n\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] not_null<History*> history() const;\n\n\tbool send(\n\t\tApi::MessageToSend message,\n\t\tbool skipToast = false);\n\n\t[[nodiscard]] bool checkSendPayment(\n\t\tint messagesCount,\n\t\tApi::SendOptions options,\n\t\tFn<void(int)> withPaymentApproved);\n\n\tvoid uploadFile(const QByteArray &fileContent, SendMediaType type);\n\tbool confirmSendingFiles(\n\t\tQImage &&image,\n\t\tQByteArray &&content,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos = std::nullopt,\n\t\tconst QString &insertTextOnCancel = QString());\n\tbool confirmSendingFiles(\n\t\tUi::PreparedList &&list,\n\t\tconst QString &insertTextOnCancel = QString());\n\tbool confirmSendingFiles(\n\t\tnot_null<const QMimeData*> data,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos,\n\t\tconst QString &insertTextOnCancel = QString());\n\tbool showSendingFilesError(const Ui::PreparedList &list) const;\n\tbool showSendingFilesError(const Ui::PreparedBundle &bundle) const;\n\n\tvoid sendingFilesConfirmed(\n\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\tApi::SendOptions options);\n\tvoid finishSending(bool skipToast = false);\n\n\tbool sendExistingDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tApi::MessageToSend messageToSend,\n\t\tstd::optional<MsgId> localId);\n\tvoid sendExistingPhoto(not_null<PhotoData*> photo);\n\tbool sendExistingPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tApi::SendOptions options);\n\tvoid sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot);\n\tvoid sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot,\n\t\tApi::SendOptions options,\n\t\tstd::optional<MsgId> localMessageId);\n\n\tvoid initGeometry();\n\tvoid initActions();\n\n\t[[nodiscard]] Api::SendAction prepareSendAction(\n\t\tApi::SendOptions options) const;\n\tvoid send(Api::SendOptions options);\n\tvoid sendVoice(const VoiceToSend &data);\n\tvoid chooseAttach(std::optional<bool> overrideSendImagesAsPhotos);\n\n\t[[nodiscard]] Fn<SendMenu::Details()> sendMenuDetails() const;\n\t[[nodiscard]] rpl::producer<int> starsPerMessageValue() const;\n\n\tvoid showPremiumToast(not_null<DocumentData*> emoji);\n\t[[nodiscard]] bool showSlowmodeError();\n\n\tconst not_null<Controller*> _controller;\n\trpl::variable<ReplyAreaType> _type;\n\trpl::variable<int> _starsForMessage;\n\tbase::weak_ptr<Calls::GroupCall> _videoStream;\n\n\tconst std::unique_ptr<HistoryView::ComposeControls> _controls;\n\tstd::unique_ptr<Cant> _cant;\n\n\tReplyAreaData _data;\n\tbase::has_weak_ptr _shownPeerGuard;\n\tbool _chooseAttachRequest = false;\n\trpl::variable<bool> _choosingAttach;\n\n\tSendPaymentHelper _sendPayment;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/stories/media_stories_repost_view.h\"\n\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"history/view/history_view_reply.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"media/stories/media_stories_controller.h\"\n#include \"media/stories/media_stories_view.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/layers/box_content.h\"\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Media::Stories {\n\nRepostView::RepostView(\n\tnot_null<Controller*> controller,\n\tnot_null<Data::Story*> story)\n: _controller(controller)\n, _story(story)\n, _sourcePeer(_story->repost()\n\t? _story->repostSourcePeer()\n\t: _story->owner().peer(\n\t\t_story->channelPosts().front().itemId.peer).get()) {\n\tExpects(_story->repost() || !_story->channelPosts().empty());\n\n\tif (!_story->repost()) {\n\t\t_link = MakeChannelPostHandler(\n\t\t\t&_story->session(),\n\t\t\t_story->channelPosts().front().itemId);\n\t}\n\n\t_story->session().colorIndicesValue(\n\t) | rpl::on_next([=](Ui::ColorIndicesCompressed &&indices) {\n\t\t_colorIndices = std::move(indices);\n\t\tif (_maxWidth) {\n\t\t\t_controller->repaint();\n\t\t}\n\t}, _lifetime);\n}\n\nRepostView::~RepostView() = default;\n\nint RepostView::height() const {\n\treturn st::historyReplyPadding.top()\n\t\t+ st::semiboldFont->height\n\t\t+ st::normalFont->height\n\t\t+ st::historyReplyPadding.bottom();\n}\n\nvoid RepostView::draw(Painter &p, int x, int y, int availableWidth) {\n\tif (!_maxWidth) {\n\t\trecountDimensions();\n\t}\n\tif (_loading) {\n\t\treturn;\n\t}\n\tconst auto simple = _text.isEmpty();\n\tif (simple) {\n\t\ty += st::normalFont->height;\n\t}\n\tconst auto w = _lastWidth = std::min(int(_maxWidth), availableWidth);\n\tconst auto h = height() - (simple ? st::normalFont->height : 0);\n\tconst auto rect = QRect(x, y, w, h);\n\tconst auto backgroundEmojiId = (!simple && _sourcePeer)\n\t\t? _sourcePeer->backgroundEmojiId()\n\t\t: DocumentId();\n\tconst auto cache = &_quoteCache;\n\tconst auto &quoteSt = simple\n\t\t? st::storiesRepostSimpleStyle\n\t\t: st::messageQuoteStyle;\n\tconst auto backgroundEmoji = backgroundEmojiId\n\t\t? &_backgroundEmojiData\n\t\t: nullptr;\n\tconst auto backgroundEmojiCache = backgroundEmoji\n\t\t? &backgroundEmoji->caches[0]\n\t\t: nullptr;\n\n\tauto rippleColor = cache->bg;\n\tcache->bg = QColor(0, 0, 0, 64);\n\tUi::Text::ValidateQuotePaintCache(*cache, quoteSt);\n\tUi::Text::FillQuotePaint(p, rect, *cache, quoteSt);\n\tif (backgroundEmoji) {\n\t\tusing namespace HistoryView;\n\t\tif (backgroundEmoji->firstFrameMask.isNull()\n\t\t\t&& !backgroundEmoji->emoji) {\n\t\t\tbackgroundEmoji->emoji = CreateBackgroundEmojiInstance(\n\t\t\t\t&_story->owner(),\n\t\t\t\tbackgroundEmojiId,\n\t\t\t\tcrl::guard(this, [=] { _controller->repaint(); }));\n\t\t}\n\t\tValidateBackgroundEmoji(\n\t\t\tbackgroundEmoji,\n\t\t\tbackgroundEmojiCache,\n\t\t\tcache);\n\t\tif (!backgroundEmojiCache->frames[0].isNull()) {\n\t\t\tconst auto hasQuoteIcon = false;\n\t\t\tFillBackgroundEmoji(\n\t\t\t\tp,\n\t\t\t\trect,\n\t\t\t\thasQuoteIcon,\n\t\t\t\t*backgroundEmojiCache,\n\t\t\t\tbackgroundEmoji->firstGiftFrame);\n\t\t}\n\t}\n\tcache->bg = rippleColor;\n\n\tif (_ripple) {\n\t\t_ripple->paint(p, x, y, w, &rippleColor);\n\t\tif (_ripple->empty()) {\n\t\t\t_ripple.reset();\n\t\t}\n\t}\n\n\tif (w > st::historyReplyPadding.left()) {\n\t\tconst auto textw = w\n\t\t\t- st::historyReplyPadding.left()\n\t\t\t- st::historyReplyPadding.right();\n\t\tconst auto namew = textw;\n\t\tif (namew > 0) {\n\t\t\tp.setPen(cache->icon);\n\t\t\t_name.drawLeftElided(\n\t\t\t\tp,\n\t\t\t\tx + st::historyReplyPadding.left(),\n\t\t\t\ty + st::historyReplyPadding.top(),\n\t\t\t\tnamew,\n\t\t\t\tw + 2 * x);\n\t\t\tif (!simple) {\n\t\t\t\tconst auto textLeft = x + st::historyReplyPadding.left();\n\t\t\t\tconst auto textTop = y\n\t\t\t\t\t+ st::historyReplyPadding.top()\n\t\t\t\t\t+ st::semiboldFont->height;\n\t\t\t\t_text.draw(p, {\n\t\t\t\t\t.position = { textLeft, textTop },\n\t\t\t\t\t.availableWidth = textw,\n\t\t\t\t\t.palette = &st::mediaviewTextPalette,\n\t\t\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t\t\t.pausedEmoji = On(PowerSaving::kEmojiChat),\n\t\t\t\t\t.pausedSpoiler = On(PowerSaving::kChatSpoiler),\n\t\t\t\t\t.elisionLines = 1,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n}\n\nRepostClickHandler RepostView::lookupHandler(QPoint position) {\n\tif (_loading) {\n\t\treturn {};\n\t}\n\tconst auto simple = _text.isEmpty();\n\tconst auto w = _lastWidth;\n\tconst auto skip = simple ? st::normalFont->height : 0;\n\tconst auto h = height() - skip;\n\tconst auto rect = QRect(0, skip, w, h);\n\tif (!rect.contains(position)) {\n\t\treturn {};\n\t} else if (!_link) {\n\t\t_link = std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {\n\t\t\tconst auto peer = _story->repostSourcePeer();\n\t\t\tconst auto owner = &_story->owner();\n\t\t\tif (const auto id = peer ? _story->repostSourceId() : 0) {\n\t\t\t\tconst auto of = owner->stories().lookup({ peer->id, id });\n\t\t\t\tif (of) {\n\t\t\t\t\tusing namespace Data;\n\t\t\t\t\t_controller->jumpTo(*of, { StoriesContextSingle() });\n\t\t\t\t} else {\n\t\t\t\t\t_controller->uiShow()->show(PrepareShortInfoBox(peer));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t_controller->uiShow()->showToast(\n\t\t\t\t\ttr::lng_forwarded_story_expired(tr::now));\n\t\t\t}\n\t\t}));\n\t}\n\t_lastPosition = position;\n\treturn { _link, this };\n}\n\nPeerData *RepostView::fromPeer() const {\n\treturn _sourcePeer;\n}\n\nQString RepostView::fromName() const {\n\treturn _sourcePeer ? _sourcePeer->name() : _story->repostSourceName();\n}\n\nvoid RepostView::recountDimensions() {\n\tconst auto name = _sourcePeer\n\t\t? _sourcePeer->name()\n\t\t: _story->repostSourceName();\n\tconst auto owner = &_story->owner();\n\tconst auto repostId = _story->repost() ? _story->repostSourceId() : 0;\n\n\tconst auto colorIndexPlusOne = _sourcePeer\n\t\t? (_sourcePeer->colorIndex() + 1)\n\t\t: 1;\n\tconst auto dark = true;\n\tconst auto colorPattern = colorIndexPlusOne\n\t\t? Ui::ColorPatternIndex(_colorIndices, colorIndexPlusOne - 1, dark)\n\t\t: 0;\n\tAssert(colorPattern < Ui::Text::kMaxQuoteOutlines);\n\tconst auto values = Ui::SimpleColorIndexValues(\n\t\tQColor(255, 255, 255),\n\t\tcolorPattern);\n\t_quoteCache.bg = values.bg;\n\t_quoteCache.outlines = values.outlines;\n\t_quoteCache.icon = values.name;\n\n\tauto text = TextWithEntities();\n\tauto unavailable = false;\n\tif (_sourcePeer && repostId) {\n\t\tconst auto senderId = _sourcePeer->id;\n\t\tconst auto of = owner->stories().lookup({ senderId, repostId });\n\t\tunavailable = !of && (of.error() == Data::NoStory::Deleted);\n\t\tif (of) {\n\t\t\ttext = (*of)->caption();\n\t\t} else if (!unavailable) {\n\t\t\tconst auto done = crl::guard(this, [=] {\n\t\t\t\t_maxWidth = 0;\n\t\t\t\t_controller->repaint();\n\t\t\t});\n\t\t\towner->stories().resolve({ _sourcePeer->id, repostId }, done);\n\t\t}\n\t}\n\n\tauto nameFull = TextWithEntities();\n\tnameFull.append(HistoryView::Reply::PeerEmoji(_sourcePeer));\n\tnameFull.append(name);\n\tauto context = Core::TextContext({\n\t\t.session = &_story->session(),\n\t\t.customEmojiLoopLimit = 1,\n\t});\n\t_name.setMarkedText(\n\t\tst::semiboldTextStyle,\n\t\tnameFull,\n\t\tUi::NameTextOptions(),\n\t\tcontext);\n\tcontext.repaint = crl::guard(this, [=] {\n\t\t_controller->repaint();\n\t});\n\t_text.setMarkedText(\n\t\tst::defaultTextStyle,\n\t\ttext,\n\t\tUi::DialogTextOptions(),\n\t\tcontext);\n\n\tconst auto nameMaxWidth = _name.maxWidth();\n\tconst auto optimalTextWidth = _text.isEmpty()\n\t\t? 0\n\t\t: std::min(_text.maxWidth(), st::maxSignatureSize);\n\t_maxWidth = std::max(nameMaxWidth, optimalTextWidth);\n\t_maxWidth = st::historyReplyPadding.left()\n\t\t+ _maxWidth\n\t\t+ st::historyReplyPadding.right();\n}\n\nvoid RepostView::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &action,\n\t\tbool pressed) {\n\tif (action == _link) {\n\t\tif (pressed) {\n\t\t\tconst auto simple = _text.isEmpty();\n\t\t\tconst auto skip = simple ? st::normalFont->height : 0;\n\t\t\tif (!_ripple) {\n\t\t\t\tconst auto h = height() - skip;\n\t\t\t\t_ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\t\tUi::RippleAnimation::RoundRectMask(\n\t\t\t\t\t\tQSize(_lastWidth, h),\n\t\t\t\t\t\t(simple\n\t\t\t\t\t\t\t? st::storiesRepostSimpleStyle\n\t\t\t\t\t\t\t: st::messageQuoteStyle).radius),\n\t\t\t\t\t[=] { _controller->repaint(); });\n\t\t\t}\n\t\t\t_ripple->add(_lastPosition - QPoint(0, skip));\n\t\t} else if (_ripple) {\n\t\t\t_ripple->lastStop();\n\t\t}\n\t}\n}\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_repost_view.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"ui/chat/chat_style.h\"\n\nclass Painter;\n\nnamespace Data {\nclass Story;\n} // namespace Data\n\nnamespace Ui {\nclass RippleAnimation;\n} // namespace Ui\n\nnamespace Media::Stories {\n\nclass Controller;\nstruct RepostClickHandler;\n\nclass RepostView final\n\t: public base::has_weak_ptr\n\t, public ClickHandlerHost {\npublic:\n\tRepostView(\n\t\tnot_null<Controller*> controller,\n\t\tnot_null<Data::Story*> story);\n\t~RepostView();\n\n\t[[nodiscard]] int height() const;\n\tvoid draw(Painter &p, int x, int y, int availableWidth);\n\t[[nodiscard]] RepostClickHandler lookupHandler(QPoint position);\n\n\t[[nodiscard]] PeerData *fromPeer() const;\n\t[[nodiscard]] QString fromName() const;\n\nprivate:\n\tvoid recountDimensions();\n\n\tvoid clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &action,\n\t\tbool pressed);\n\n\tconst not_null<Controller*> _controller;\n\tconst not_null<Data::Story*> _story;\n\tPeerData *_sourcePeer = nullptr;\n\tClickHandlerPtr _link;\n\tstd::unique_ptr<Ui::RippleAnimation> _ripple;\n\n\tUi::Text::String _name;\n\tUi::Text::String _text;\n\tUi::Text::QuotePaintCache _quoteCache;\n\tUi::BackgroundEmojiData _backgroundEmojiData;\n\tUi::ColorIndicesCompressed _colorIndices;\n\tQPoint _lastPosition;\n\tmutable int _lastWidth = 0;\n\tuint32 _maxWidth : 31 = 0;\n\tuint32 _loading : 1 = 0;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_share.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/stories/media_stories_share.h\"\n\n#include \"api/api_common.h\"\n#include \"apiwrap.h\"\n#include \"base/random.h\"\n#include \"boxes/share_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_thread.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"history/history_item_helpers.h\" // GetErrorForSending.\n#include \"history/view/history_view_context_menu.h\" // CopyStoryLink.\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"styles/style_calls.h\"\n\nnamespace Media::Stories {\n\n[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShareBox(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tFullStoryId id,\n\t\tbool viewerStyle) {\n\tconst auto session = &show->session();\n\tconst auto resolve = [=] {\n\t\tconst auto maybeStory = session->data().stories().lookup(id);\n\t\treturn maybeStory ? maybeStory->get() : nullptr;\n\t};\n\tconst auto story = resolve();\n\tif (!story) {\n\t\treturn { nullptr };\n\t}\n\tconst auto canCopyLink = story->hasDirectLink();\n\tconst auto shareJustLink = (story->call() != nullptr);\n\tif (!canCopyLink && shareJustLink) {\n\t\treturn { nullptr };\n\t}\n\n\tauto copyCallback = [=] {\n\t\tconst auto story = resolve();\n\t\tif (!story) {\n\t\t\treturn;\n\t\t}\n\t\tif (story->hasDirectLink()) {\n\t\t\tusing namespace HistoryView;\n\t\t\tCopyStoryLink(show, story->fullId());\n\t\t}\n\t};\n\n\tstruct State {\n\t\tint requests = 0;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tauto filterCallback = [=](not_null<Data::Thread*> thread) {\n\t\tif (const auto user = thread->peer()->asUser()) {\n\t\t\tif (user->canSendIgnoreMoneyRestrictions()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\tif (shareJustLink) {\n\t\t\treturn ::Data::CanSend(thread, ChatRestriction::SendOther);\n\t\t}\n\t\treturn Data::CanSend(thread, ChatRestriction::SendPhotos)\n\t\t\t&& Data::CanSend(thread, ChatRestriction::SendVideos);\n\t};\n\tauto copyLinkCallback = canCopyLink\n\t\t? Fn<void()>(std::move(copyCallback))\n\t\t: Fn<void()>();\n\tauto countMessagesCallback = [=](const TextWithTags &comment) {\n\t\treturn (shareJustLink || comment.text.isEmpty()) ? 1 : 2;\n\t};\n\tauto submitCallback = [=](\n\t\t\tstd::vector<not_null<Data::Thread*>> &&result,\n\t\t\tFn<bool()> checkPaid,\n\t\t\tTextWithTags &&comment,\n\t\t\tApi::SendOptions options,\n\t\t\tData::ForwardOptions forwardOptions) {\n\t\tif (state->requests) {\n\t\t\treturn; // Share clicked already.\n\t\t}\n\t\tconst auto story = resolve();\n\t\tif (!story) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto peer = story->peer();\n\t\tconst auto error = GetErrorForSending(\n\t\t\tresult,\n\t\t\t{ .story = shareJustLink ? nullptr : story, .text = &comment });\n\t\tif (error.error) {\n\t\t\tshow->showBox(MakeSendErrorBox(error, result.size() > 1));\n\t\t\treturn;\n\t\t} else if (!checkPaid()) {\n\t\t\treturn;\n\t\t} else if (shareJustLink) {\n\t\t\tconst auto url = session->api().exportDirectStoryLink(story);\n\t\t\tif (!comment.text.isEmpty()) {\n\t\t\t\tcomment.text = url + \"\\n\" + comment.text;\n\t\t\t\tconst auto add = url.size() + 1;\n\t\t\t\tfor (auto &tag : comment.tags) {\n\t\t\t\t\ttag.offset += add;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcomment.text = url;\n\t\t\t}\n\t\t\tauto &api = show->session().api();\n\t\t\tfor (const auto &thread : result) {\n\t\t\t\tauto message = Api::MessageToSend(\n\t\t\t\t\tApi::SendAction(thread, options));\n\t\t\t\tmessage.textWithTags = comment;\n\t\t\t\tmessage.action.clearDraft = false;\n\t\t\t\tapi.sendMessage(std::move(message));\n\t\t\t}\n\t\t\tshow->showToast(tr::lng_share_done(tr::now));\n\t\t\tshow->hideLayer();\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto api = &story->session().api();\n\t\tauto &histories = story->owner().histories();\n\t\tfor (const auto &thread : result) {\n\t\t\tconst auto action = Api::SendAction(thread, options);\n\t\t\tif (!comment.text.isEmpty()) {\n\t\t\t\tauto message = Api::MessageToSend(action);\n\t\t\t\tmessage.textWithTags = comment;\n\t\t\t\tmessage.action.clearDraft = false;\n\t\t\t\tapi->sendMessage(std::move(message));\n\t\t\t}\n\t\t\tconst auto session = &thread->session();\n\t\t\tconst auto threadPeer = thread->peer();\n\t\t\tconst auto threadHistory = thread->owningHistory();\n\t\t\tconst auto randomId = base::RandomValue<uint64>();\n\t\t\tusing SendFlag = MTPmessages_SendMedia::Flag;\n\t\t\tauto sendFlags = SendFlag(0) | SendFlag(0);\n\t\t\tif (action.replyTo) {\n\t\t\t\tsendFlags |= SendFlag::f_reply_to;\n\t\t\t}\n\t\t\tconst auto silentPost = ShouldSendSilent(threadPeer, options);\n\t\t\tif (silentPost) {\n\t\t\t\tsendFlags |= SendFlag::f_silent;\n\t\t\t}\n\t\t\tif (options.scheduled) {\n\t\t\t\tsendFlags |= SendFlag::f_schedule_date;\n\t\t\t\tif (options.scheduleRepeatPeriod) {\n\t\t\t\t\tsendFlags |= SendFlag::f_schedule_repeat_period;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (options.shortcutId) {\n\t\t\t\tsendFlags |= SendFlag::f_quick_reply_shortcut;\n\t\t\t}\n\t\t\tif (options.effectId) {\n\t\t\t\tsendFlags |= SendFlag::f_effect;\n\t\t\t}\n\t\t\tif (options.suggest) {\n\t\t\t\tsendFlags |= SendFlag::f_suggested_post;\n\t\t\t}\n\t\t\tif (options.invertCaption) {\n\t\t\t\tsendFlags |= SendFlag::f_invert_media;\n\t\t\t}\n\t\t\tconst auto starsPaid = std::min(\n\t\t\t\tthreadHistory->peer->starsPerMessageChecked(),\n\t\t\t\toptions.starsApproved);\n\t\t\tif (starsPaid) {\n\t\t\t\toptions.starsApproved -= starsPaid;\n\t\t\t\tsendFlags |= SendFlag::f_allow_paid_stars;\n\t\t\t}\n\t\t\tconst auto done = [=] {\n\t\t\t\tif (!--state->requests) {\n\t\t\t\t\tif (show->valid()) {\n\t\t\t\t\t\tshow->showToast(tr::lng_share_done(tr::now));\n\t\t\t\t\t\tshow->hideLayer();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\thistories.sendPreparedMessage(\n\t\t\t\tthreadHistory,\n\t\t\t\taction.replyTo,\n\t\t\t\trandomId,\n\t\t\t\tData::Histories::PrepareMessage<MTPmessages_SendMedia>(\n\t\t\t\t\tMTP_flags(sendFlags),\n\t\t\t\t\tthreadPeer->input(),\n\t\t\t\t\tData::Histories::ReplyToPlaceholder(),\n\t\t\t\t\tMTP_inputMediaStory(peer->input(), MTP_int(id.story)),\n\t\t\t\t\tMTPstring(),\n\t\t\t\t\tMTP_long(randomId),\n\t\t\t\t\tMTPReplyMarkup(),\n\t\t\t\t\tMTPVector<MTPMessageEntity>(),\n\t\t\t\t\tMTP_int(options.scheduled),\n\t\t\t\t\tMTP_int(options.scheduleRepeatPeriod),\n\t\t\t\t\tMTP_inputPeerEmpty(),\n\t\t\t\t\tData::ShortcutIdToMTP(session, options.shortcutId),\n\t\t\t\t\tMTP_long(options.effectId),\n\t\t\t\t\tMTP_long(starsPaid),\n\t\t\t\t\tApi::SuggestToMTP(options.suggest)\n\t\t\t\t), [=](\n\t\t\t\t\t\tconst MTPUpdates &result,\n\t\t\t\t\t\tconst MTP::Response &response) {\n\t\t\t\t\tdone();\n\t\t\t\t}, [=](\n\t\t\t\t\t\tconst MTP::Error &error,\n\t\t\t\t\t\tconst MTP::Response &response) {\n\t\t\t\t\tapi->sendMessageFail(error, threadPeer, randomId);\n\t\t\t\t\tdone();\n\t\t\t\t});\n\t\t\t++state->requests;\n\t\t}\n\t};\n\tconst auto st = viewerStyle\n\t\t? ::Settings::DarkCreditsEntryBoxStyle()\n\t\t: ::Settings::CreditsEntryBoxStyleOverrides();\n\treturn Box<ShareBox>(ShareBox::Descriptor{\n\t\t.session = session,\n\t\t.copyCallback = std::move(copyLinkCallback),\n\t\t.countMessagesCallback = std::move(countMessagesCallback),\n\t\t.submitCallback = std::move(submitCallback),\n\t\t.filterCallback = std::move(filterCallback),\n\t\t.st = st.shareBox ? *st.shareBox : ShareBoxStyleOverrides(),\n\t\t.moneyRestrictionError = ShareMessageMoneyRestrictionError(),\n\t});\n}\n\nQString FormatShareAtTime(TimeId seconds) {\n\tconst auto minutes = seconds / 60;\n\tconst auto h = minutes / 60;\n\tconst auto m = minutes % 60;\n\tconst auto s = seconds % 60;\n\tconst auto zero = QChar('0');\n\treturn h\n\t\t? u\"%1:%2:%3\"_q.arg(h).arg(m, 2, 10, zero).arg(s, 2, 10, zero)\n\t\t: u\"%1:%2\"_q.arg(m).arg(s, 2, 10, zero);\n}\n\nobject_ptr<Ui::BoxContent> PrepareShareAtTimeBox(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<HistoryItem*> item,\n\t\tTimeId videoTimestamp) {\n\tconst auto id = item->fullId();\n\tconst auto history = item->history();\n\tconst auto owner = &history->owner();\n\tconst auto session = &history->session();\n\tconst auto canCopyLink = item->hasDirectLink()\n\t\t&& history->peer->isBroadcast()\n\t\t&& history->peer->asBroadcast()->hasUsername();\n\tconst auto hasCaptions = item->media()\n\t\t&& !item->originalText().text.isEmpty()\n\t\t&& item->media()->allowsEditCaption();\n\tconst auto hasOnlyForcedForwardedInfo = !hasCaptions\n\t\t&& item->media()\n\t\t&& item->media()->forceForwardedInfo();\n\n\tauto copyCallback = [=] {\n\t\tconst auto item = owner->message(id);\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\t\tCopyPostLink(\n\t\t\tshow,\n\t\t\titem->fullId(),\n\t\t\tHistoryView::Context::History,\n\t\t\tvideoTimestamp);\n\t};\n\n\tconst auto requiredRight = item->requiredSendRight();\n\tconst auto requiresInline = item->requiresSendInlineRight();\n\tauto filterCallback = [=](not_null<Data::Thread*> thread) {\n\t\tif (const auto user = thread->peer()->asUser()) {\n\t\t\tif (user->canSendIgnoreMoneyRestrictions()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn Data::CanSend(thread, requiredRight)\n\t\t\t&& (!requiresInline\n\t\t\t\t|| Data::CanSend(thread, ChatRestriction::SendInline));\n\t};\n\tauto copyLinkCallback = canCopyLink\n\t\t? Fn<void()>(std::move(copyCallback))\n\t\t: Fn<void()>();\n\tconst auto st = ::Settings::DarkCreditsEntryBoxStyle();\n\treturn Box<ShareBox>(ShareBox::Descriptor{\n\t\t.session = session,\n\t\t.copyCallback = std::move(copyLinkCallback),\n\t\t.countMessagesCallback = ShareBox::DefaultForwardCountMessages(\n\t\t\thistory,\n\t\t\t{ id }),\n\t\t.submitCallback = ShareBox::DefaultForwardCallback(\n\t\t\tshow,\n\t\t\thistory,\n\t\t\t{ id },\n\t\t\tvideoTimestamp),\n\t\t.filterCallback = std::move(filterCallback),\n\t\t.titleOverride = tr::lng_share_at_time_title(\n\t\t\tlt_time,\n\t\t\trpl::single(FormatShareAtTime(videoTimestamp))),\n\t\t.st = st.shareBox ? *st.shareBox : ShareBoxStyleOverrides(),\n\t\t.forwardOptions = {\n\t\t\t.sendersCount = ItemsForwardSendersCount({ item }),\n\t\t\t.captionsCount = ItemsForwardCaptionsCount({ item }),\n\t\t\t.show = !hasOnlyForcedForwardedInfo,\n\t\t},\n\t\t.moneyRestrictionError = ShareMessageMoneyRestrictionError(),\n\t});\n}\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_share.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Ui {\nclass BoxContent;\n} // namespace Ui\n\nnamespace Media::Stories {\n\n[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShareBox(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tFullStoryId id,\n\tbool viewerStyle = false);\n\n[[nodiscard]] QString FormatShareAtTime(TimeId seconds);\n\n[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShareAtTimeBox(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<HistoryItem*> item,\n\tTimeId videoTimestamp);\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_sibling.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/stories/media_stories_sibling.h\"\n\n#include \"base/weak_ptr.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"media/stories/media_stories_controller.h\"\n#include \"media/stories/media_stories_view.h\"\n#include \"media/streaming/media_streaming_instance.h\"\n#include \"media/streaming/media_streaming_player.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Media::Stories {\nnamespace {\n\nconstexpr auto kGoodFadeDuration = crl::time(200);\nconstexpr auto kSiblingFade = 0.5;\nconstexpr auto kSiblingFadeOver = 0.4;\nconstexpr auto kSiblingNameOpacity = 0.8;\nconstexpr auto kSiblingNameOpacityOver = 1.;\nconstexpr auto kSiblingScaleOver = 0.05;\n\n[[nodiscard]] StoryId LookupShownId(\n\t\tconst Data::StoriesSource &source,\n\t\tStoryId suggestedId) {\n\tconst auto i = suggestedId\n\t\t? source.ids.lower_bound(Data::StoryIdDates{ suggestedId })\n\t\t: end(source.ids);\n\treturn (i != end(source.ids) && i->id == suggestedId)\n\t\t? suggestedId\n\t\t: source.toOpen().id;\n}\n\n} // namespace\n\nclass Sibling::Loader {\npublic:\n\tvirtual ~Loader() = default;\n\n\tvirtual QImage blurred() = 0;\n\tvirtual QImage good() = 0;\n};\n\nclass Sibling::LoaderPhoto final : public Sibling::Loader {\npublic:\n\tLoaderPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tData::FileOrigin origin,\n\t\tFn<void()> update);\n\n\tQImage blurred() override;\n\tQImage good() override;\n\nprivate:\n\tconst not_null<PhotoData*> _photo;\n\tconst Fn<void()> _update;\n\tstd::shared_ptr<Data::PhotoMedia> _media;\n\trpl::lifetime _waitingLoading;\n\n};\n\nclass Sibling::LoaderVideo final\n\t: public Sibling::Loader\n\t, public base::has_weak_ptr {\npublic:\n\tLoaderVideo(\n\t\tnot_null<DocumentData*> video,\n\t\tData::FileOrigin origin,\n\t\tFn<void()> update);\n\n\tQImage blurred() override;\n\tQImage good() override;\n\nprivate:\n\tvoid waitForGoodThumbnail();\n\tbool updateAfterGoodCheck();\n\tvoid createStreamedPlayer();\n\tvoid streamedFailed();\n\n\tconst not_null<DocumentData*> _video;\n\tconst Data::FileOrigin _origin;\n\tconst Fn<void()> _update;\n\tstd::shared_ptr<Data::DocumentMedia> _media;\n\tstd::unique_ptr<Streaming::Instance> _streamed;\n\trpl::lifetime _waitingGoodGeneration;\n\tbool _checkingGoodInCache = false;\n\tbool _failed = false;\n\n};\n\nSibling::LoaderPhoto::LoaderPhoto(\n\tnot_null<PhotoData*> photo,\n\tData::FileOrigin origin,\n\tFn<void()> update)\n: _photo(photo)\n, _update(std::move(update))\n, _media(_photo->createMediaView()) {\n\t_photo->load(origin, LoadFromCloudOrLocal, true);\n}\n\nQImage Sibling::LoaderPhoto::blurred() {\n\tif (const auto image = _media->thumbnailInline()) {\n\t\treturn image->original();\n\t}\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto result = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::black);\n\tresult.setDevicePixelRatio(ratio);\n\treturn result;\n}\n\nQImage Sibling::LoaderPhoto::good() {\n\tif (const auto image = _media->image(Data::PhotoSize::Large)) {\n\t\treturn image->original();\n\t} else if (!_waitingLoading) {\n\t\t_photo->session().downloaderTaskFinished(\n\t\t) | rpl::on_next([=] {\n\t\t\tif (_media->loaded()) {\n\t\t\t\t_update();\n\t\t\t}\n\t\t}, _waitingLoading);\n\t}\n\treturn QImage();\n}\n\nSibling::LoaderVideo::LoaderVideo(\n\tnot_null<DocumentData*> video,\n\tData::FileOrigin origin,\n\tFn<void()> update)\n: _video(video)\n, _origin(origin)\n, _update(std::move(update))\n, _media(_video->createMediaView()) {\n\t_media->goodThumbnailWanted();\n}\n\nQImage Sibling::LoaderVideo::blurred() {\n\tif (const auto image = _media->thumbnailInline()) {\n\t\treturn image->original();\n\t}\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto result = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::black);\n\tresult.setDevicePixelRatio(ratio);\n\treturn result;\n}\n\nQImage Sibling::LoaderVideo::good() {\n\tif (const auto image = _media->goodThumbnail()) {\n\t\treturn image->original();\n\t} else if (!_video->goodThumbnailChecked()\n\t\t&& !_video->goodThumbnailNoData()) {\n\t\tif (!_checkingGoodInCache) {\n\t\t\twaitForGoodThumbnail();\n\t\t}\n\t} else if (_failed) {\n\t\treturn QImage();\n\t} else if (!_streamed) {\n\t\tcreateStreamedPlayer();\n\t} else if (_streamed->ready()) {\n\t\treturn _streamed->info().video.cover;\n\t}\n\treturn QImage();\n}\n\nvoid Sibling::LoaderVideo::createStreamedPlayer() {\n\t_streamed = std::make_unique<Streaming::Instance>(\n\t\t_video,\n\t\t_origin,\n\t\t[] {}); // waitingCallback\n\t_streamed->lockPlayer();\n\t_streamed->player().updates(\n\t) | rpl::on_next_error([=](Streaming::Update &&update) {\n\t\tv::match(update.data, [&](Streaming::Information &update) {\n\t\t\t_update();\n\t\t}, [](const auto &update) {\n\t\t});\n\t}, [=](Streaming::Error &&error) {\n\t\tstreamedFailed();\n\t}, _streamed->lifetime());\n\tif (_streamed->ready()) {\n\t\t_update();\n\t} else if (!_streamed->valid()) {\n\t\tstreamedFailed();\n\t} else if (!_streamed->player().active()\n\t\t&& !_streamed->player().finished()) {\n\t\t_streamed->play({\n\t\t\t.mode = Streaming::Mode::Video,\n\t\t});\n\t\t_streamed->pause();\n\t}\n}\n\nvoid Sibling::LoaderVideo::streamedFailed() {\n\t_failed = true;\n\t_streamed = nullptr;\n\t_update();\n}\n\nvoid Sibling::LoaderVideo::waitForGoodThumbnail() {\n\t_checkingGoodInCache = true;\n\tconst auto weak = make_weak(this);\n\t_video->owner().cache().get({}, [=](const auto &) {\n\t\tcrl::on_main([=] {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tif (!strong->updateAfterGoodCheck()) {\n\t\t\t\t\tstrong->_video->session().downloaderTaskFinished(\n\t\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\t\tstrong->updateAfterGoodCheck();\n\t\t\t\t\t}, strong->_waitingGoodGeneration);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n}\n\nbool Sibling::LoaderVideo::updateAfterGoodCheck() {\n\tif (!_video->goodThumbnailChecked()\n\t\t&& !_video->goodThumbnailNoData()) {\n\t\treturn false;\n\t}\n\t_checkingGoodInCache = false;\n\t_waitingGoodGeneration.destroy();\n\t_update();\n\treturn true;\n}\n\nSibling::Sibling(\n\tnot_null<Controller*> controller,\n\tconst Data::StoriesSource &source,\n\tStoryId suggestedId)\n: _controller(controller)\n, _id{ source.peer->id, LookupShownId(source, suggestedId) }\n, _peer(source.peer) {\n\tcheckStory();\n\t_goodShown.stop();\n}\n\nSibling::~Sibling() = default;\n\nvoid Sibling::checkStory() {\n\tconst auto maybeStory = _peer->owner().stories().lookup(_id);\n\tif (!maybeStory) {\n\t\tif (_blurred.isNull()) {\n\t\t\tsetBlackThumbnail();\n\t\t\tif (maybeStory.error() == Data::NoStory::Unknown) {\n\t\t\t\t_peer->owner().stories().resolve(_id, crl::guard(this, [=] {\n\t\t\t\t\tcheckStory();\n\t\t\t\t}));\n\t\t\t}\n\t\t}\n\t\treturn;\n\t}\n\tconst auto story = *maybeStory;\n\tconst auto origin = Data::FileOrigin();\n\tv::match(story->media().data, [&](not_null<PhotoData*> photo) {\n\t\t_loader = std::make_unique<LoaderPhoto>(photo, origin, [=] {\n\t\t\tcheck();\n\t\t});\n\t}, [&](not_null<DocumentData*> document) {\n\t\t_loader = std::make_unique<LoaderVideo>(document, origin, [=] {\n\t\t\tcheck();\n\t\t});\n\t}, [&](const std::shared_ptr<Data::GroupCall> &call) {\n\t\t_loader = nullptr;\n\t}, [&](v::null_t) {\n\t\t_loader = nullptr;\n\t});\n\tif (!_loader) {\n\t\tsetBlackThumbnail();\n\t\treturn;\n\t}\n\t_blurred = _loader->blurred();\n\tcheck();\n}\n\nvoid Sibling::setBlackThumbnail() {\n\t_blurred = QImage(\n\t\tst::storiesMaxSize,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_blurred.fill(Qt::black);\n}\n\nFullStoryId Sibling::shownId() const {\n\treturn _id;\n}\n\nnot_null<PeerData*> Sibling::peer() const {\n\treturn _peer;\n}\n\nbool Sibling::shows(\n\t\tconst Data::StoriesSource &source,\n\t\tStoryId suggestedId) const {\n\tconst auto fullId = FullStoryId{\n\t\tsource.peer->id,\n\t\tLookupShownId(source, suggestedId),\n\t};\n\treturn (_id == fullId);\n}\n\nSiblingView Sibling::view(const SiblingLayout &layout, float64 over) {\n\tconst auto name = nameImage(layout);\n\treturn {\n\t\t.image = _good.isNull() ? _blurred : _good,\n\t\t.layout = {\n\t\t\t.geometry = layout.geometry,\n\t\t\t.fade = kSiblingFade * (1 - over) + kSiblingFadeOver * over,\n\t\t\t.radius = st::storiesRadius,\n\t\t},\n\t\t.userpic = userpicImage(layout),\n\t\t.userpicPosition = layout.userpic.topLeft(),\n\t\t.name = name,\n\t\t.namePosition = namePosition(layout, name),\n\t\t.nameOpacity = (kSiblingNameOpacity * (1 - over)\n\t\t\t+ kSiblingNameOpacityOver * over),\n\t\t.scale = 1. + (over * kSiblingScaleOver),\n\t};\n}\n\nQImage Sibling::userpicImage(const SiblingLayout &layout) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto size = layout.userpic.width() * ratio;\n\tconst auto key = _peer->userpicUniqueKey(_userpicView);\n\tif (_userpicImage.width() != size || _userpicKey != key) {\n\t\t_userpicKey = key;\n\t\t_userpicImage = PeerData::GenerateUserpicImage(\n\t\t\t_peer,\n\t\t\t_userpicView,\n\t\t\tsize);\n\t\t_userpicImage.setDevicePixelRatio(ratio);\n\t}\n\treturn _userpicImage;\n}\n\nQImage Sibling::nameImage(const SiblingLayout &layout) {\n\tif (_nameFontSize != layout.nameFontSize) {\n\t\t_nameFontSize = layout.nameFontSize;\n\n\t\tconst auto family = 0; // Default font family.\n\t\tconst auto font = style::font(\n\t\t\t_nameFontSize,\n\t\t\tstyle::FontFlag::Semibold,\n\t\t\tfamily);\n\t\t_name.reset();\n\t\t_nameStyle = std::make_unique<style::TextStyle>(style::TextStyle{\n\t\t\t.font = font,\n\t\t});\n\t};\n\tconst auto text = _peer->isSelf()\n\t\t? tr::lng_stories_my_name(tr::now)\n\t\t: _peer->shortName();\n\tif (_nameText != text) {\n\t\t_name.reset();\n\t\t_nameText = text;\n\t}\n\tif (!_name) {\n\t\t_nameAvailableWidth = 0;\n\t\t_name.emplace(*_nameStyle, _nameText);\n\t}\n\tconst auto available = layout.nameBoundingRect.width();\n\tconst auto wasCut = (_nameAvailableWidth < _name->maxWidth());\n\tconst auto nowCut = (available < _name->maxWidth());\n\tif (_nameImage.isNull()\n\t\t|| _nameAvailableWidth != layout.nameBoundingRect.width()) {\n\t\t_nameAvailableWidth = layout.nameBoundingRect.width();\n\t\tif (_nameImage.isNull() || nowCut || wasCut) {\n\t\t\tconst auto w = std::min(_nameAvailableWidth, _name->maxWidth());\n\t\t\tconst auto h = _nameStyle->font->height;\n\t\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t\t_nameImage = QImage(\n\t\t\t\tQSize(w, h) * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_nameImage.setDevicePixelRatio(ratio);\n\t\t\t_nameImage.fill(Qt::transparent);\n\t\t\tauto p = Painter(&_nameImage);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setFont(_nameStyle->font);\n\t\t\tp.setPen(Qt::white);\n\t\t\t_name->drawLeftElided(p, 0, 0, w, w);\n\t\t}\n\t}\n\treturn _nameImage;\n}\n\nQPoint Sibling::namePosition(\n\t\tconst SiblingLayout &layout,\n\t\tconst QImage &image) const {\n\tconst auto size = image.size() / image.devicePixelRatio();\n\tconst auto width = size.width();\n\tconst auto bounding = layout.nameBoundingRect;\n\tconst auto left = layout.geometry.x()\n\t\t+ (layout.geometry.width() - width) / 2;\n\tconst auto top = bounding.y() + bounding.height() - size.height();\n\tif (left < bounding.x()) {\n\t\treturn { bounding.x(), top };\n\t} else if (left + width > bounding.x() + bounding.width()) {\n\t\treturn { bounding.x() + bounding.width() - width, top };\n\t}\n\treturn { left, top };\n}\n\nvoid Sibling::check() {\n\tExpects(_loader != nullptr);\n\n\tauto good = _loader->good();\n\tif (good.isNull()) {\n\t\treturn;\n\t}\n\t_loader = nullptr;\n\t_good = std::move(good);\n\t_goodShown.start([=] {\n\t\t_controller->repaintSibling(this);\n\t}, 0., 1., kGoodFadeDuration, anim::linear);\n}\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_sibling.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"data/data_stories.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/userpic_view.h\"\n\nnamespace style {\nstruct TextStyle;\n} // namespace style\n\nnamespace Media::Stories {\n\nclass Controller;\nstruct SiblingView;\nstruct SiblingLayout;\n\nclass Sibling final : public base::has_weak_ptr {\npublic:\n\tSibling(\n\t\tnot_null<Controller*> controller,\n\t\tconst Data::StoriesSource &source,\n\t\tStoryId suggestedId);\n\t~Sibling();\n\n\t[[nodiscard]] FullStoryId shownId() const;\n\t[[nodiscard]] not_null<PeerData*> peer() const;\n\t[[nodiscard]] bool shows(\n\t\tconst Data::StoriesSource &source,\n\t\tStoryId suggestedId) const;\n\n\t[[nodiscard]] SiblingView view(\n\t\tconst SiblingLayout &layout,\n\t\tfloat64 over);\n\nprivate:\n\tclass Loader;\n\tclass LoaderPhoto;\n\tclass LoaderVideo;\n\n\tvoid checkStory();\n\tvoid check();\n\n\tvoid setBlackThumbnail();\n\t[[nodiscard]] QImage userpicImage(const SiblingLayout &layout);\n\t[[nodiscard]] QImage nameImage(const SiblingLayout &layout);\n\t[[nodiscard]] QPoint namePosition(\n\t\tconst SiblingLayout &layout,\n\t\tconst QImage &image) const;\n\n\tconst not_null<Controller*> _controller;\n\n\tFullStoryId _id;\n\tnot_null<PeerData*> _peer;\n\tQImage _blurred;\n\tQImage _good;\n\tUi::Animations::Simple _goodShown;\n\n\tQImage _userpicImage;\n\tInMemoryKey _userpicKey = {};\n\tUi::PeerUserpicView _userpicView;\n\n\tQImage _nameImage;\n\tstd::unique_ptr<style::TextStyle> _nameStyle;\n\tstd::optional<Ui::Text::String> _name;\n\tQString _nameText;\n\tint _nameAvailableWidth = 0;\n\tint _nameFontSize = 0;\n\n\tstd::unique_ptr<Loader> _loader;\n\n};\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_slider.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/stories/media_stories_slider.h\"\n\n#include \"media/stories/media_stories_controller.h\"\n#include \"media/view/media_view_playback_progress.h\"\n#include \"media/audio/media_audio.h\"\n#include \"ui/painter.h\"\n#include \"ui/rp_widget.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Media::Stories {\nnamespace {\n\nconstexpr auto kOpacityInactive = 0.4;\nconstexpr auto kOpacityActive = 1.;\n\n} // namespace\n\nSlider::Slider(not_null<Controller*> controller)\n: _controller(controller)\n, _progress(std::make_unique<View::PlaybackProgress>()) {\n}\n\nSlider::~Slider() {\n}\n\nvoid Slider::show(SliderData data) {\n\tresetProgress();\n\tdata.total = std::max(data.total, 1);\n\tdata.index = std::clamp(data.index, 0, data.total - 1);\n\n\tif (_data == data) {\n\t\treturn;\n\t}\n\t_data = data;\n\n\tconst auto parent = _controller->wrap();\n\tauto widget = std::make_unique<Ui::RpWidget>(parent);\n\tconst auto raw = widget.get();\n\n\t_rects.resize(_data.total);\n\n\traw->widthValue() | rpl::filter([=](int width) {\n\t\treturn (width >= st::storiesSliderWidth);\n\t}) | rpl::on_next([=](int width) {\n\t\tlayout(width);\n\t}, raw->lifetime());\n\n\traw->paintRequest(\n\t) | rpl::filter([=] {\n\t\treturn !_data.videoStream\n\t\t\t&& (raw->width() >= st::storiesSliderWidth);\n\t}) | rpl::on_next([=](QRect clip) {\n\t\tpaint(QRectF(clip));\n\t}, raw->lifetime());\n\n\traw->show();\n\t_widget = std::move(widget);\n\n\t_progress->setValueChangedCallback([=](float64, float64) {\n\t\t_widget->update(_activeBoundingRect);\n\t});\n\n\t_controller->layoutValue(\n\t) | rpl::on_next([=](const Layout &layout) {\n\t\traw->setGeometry(layout.slider - st::storiesSliderMargin);\n\t}, raw->lifetime());\n}\n\nvoid Slider::raise() {\n\tif (_widget) {\n\t\t_widget->raise();\n\t}\n}\n\nvoid Slider::updatePlayback(const Player::TrackState &state) {\n\t_progress->updateState(state);\n}\n\nvoid Slider::resetProgress() {\n\t_progress->updateState({});\n}\n\nvoid Slider::layout(int width) {\n\tconst auto single = st::storiesSliderWidth;\n\tconst auto skip = st::storiesSliderSkip;\n\t// width == single * max + skip * (max - 1);\n\t// max == (width + skip) / (single + skip);\n\tconst auto max = (width + skip) / (single + skip);\n\tAssert(max > 0);\n\tconst auto count = std::clamp(_data.total, 1, max);\n\tconst auto one = (width - (count - 1) * skip) / float64(count);\n\tauto left = 0.;\n\tfor (auto i = 0; i != count; ++i) {\n\t\t_rects[i] = QRectF(left, 0, one, single);\n\t\tif (i == _data.index) {\n\t\t\tconst auto from = int(std::floor(left));\n\t\t\tconst auto size = int(std::ceil(left + one)) - from;\n\t\t\t_activeBoundingRect = QRect(from, 0, size, single);\n\t\t}\n\t\tleft += one + skip;\n\t}\n\tfor (auto i = count; i != _rects.size(); ++i) {\n\t\t_rects[i] = QRectF();\n\t}\n}\n\nvoid Slider::paint(QRectF clip) {\n\tauto p = QPainter(_widget.get());\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tp.setBrush(st::mediaviewControlFg);\n\tp.setPen(Qt::NoPen);\n\tconst auto radius = st::storiesSliderWidth / 2.;\n\tfor (auto i = 0; i != int(_rects.size()); ++i) {\n\t\tif (_rects[i].isEmpty()) {\n\t\t\tbreak;\n\t\t} else if (!_rects[i].intersects(clip)) {\n\t\t\tcontinue;\n\t\t} else if (i == _data.index) {\n\t\t\tconst auto progress = _progress->value();\n\t\t\tconst auto full = _rects[i].width();\n\t\t\tconst auto height = _rects[i].height();\n\t\t\tconst auto min = height;\n\t\t\tconst auto activeWidth = std::max(full * progress, min);\n\t\t\tconst auto inactiveWidth = full - activeWidth + min;\n\t\t\tconst auto activeLeft = _rects[i].left();\n\t\t\tconst auto inactiveLeft = activeLeft + activeWidth - min;\n\t\t\tp.setOpacity(kOpacityInactive);\n\t\t\tp.drawRoundedRect(\n\t\t\t\tQRectF(inactiveLeft, 0, inactiveWidth, height),\n\t\t\t\tradius,\n\t\t\t\tradius);\n\t\t\tif (activeWidth > 0.) {\n\t\t\t\tp.setOpacity(kOpacityActive);\n\t\t\t\tp.drawRoundedRect(\n\t\t\t\t\tQRectF(activeLeft, 0, activeWidth, height),\n\t\t\t\t\tradius,\n\t\t\t\t\tradius);\n\t\t\t}\n\t\t} else {\n\t\t\tp.setOpacity((i < _data.index)\n\t\t\t\t? kOpacityActive\n\t\t\t\t: kOpacityInactive);\n\t\t\tp.drawRoundedRect(_rects[i], radius, radius);\n\t\t}\n\t}\n}\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_slider.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Media::View {\nclass PlaybackProgress;\n} // namespace Media::View\n\nnamespace Media::Player {\nstruct TrackState;\n} // namespace Media::Player\n\nnamespace Media::Stories {\n\nclass Controller;\n\nstruct SliderData {\n\tint index = 0;\n\tint total = 0;\n\tbool videoStream = false;\n\n\tfriend inline auto operator<=>(SliderData, SliderData) = default;\n\tfriend inline bool operator==(SliderData, SliderData) = default;\n};\n\nclass Slider final {\npublic:\n\texplicit Slider(not_null<Controller*> controller);\n\t~Slider();\n\n\tvoid show(SliderData data);\n\tvoid raise();\n\n\tvoid updatePlayback(const Player::TrackState &state);\n\nprivate:\n\tvoid resetProgress();\n\n\tvoid layout(int width);\n\tvoid paint(QRectF clip);\n\n\tconst not_null<Controller*> _controller;\n\tconst std::unique_ptr<Media::View::PlaybackProgress> _progress;\n\n\tstd::unique_ptr<Ui::RpWidget> _widget;\n\tstd::vector<QRectF> _rects;\n\tQRect _activeBoundingRect;\n\n\tSliderData _data;\n\n\n};\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_stealth.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/stories/media_stories_stealth.h\"\n\n#include \"base/timer_rpl.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/controls/feature_list.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/painter.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_media_view.h\"\n#include \"styles/style_media_stories.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Media::Stories {\nnamespace {\n\nconstexpr auto kAlreadyToastDuration = 4 * crl::time(1000);\nconstexpr auto kCooldownButtonLabelOpacity = 0.5;\n\nstruct State {\n\tData::StealthMode mode;\n\tTimeId now = 0;\n\tbool premium = false;\n\tbool hasCallback = false;\n};\n\n[[nodiscard]] Ui::Toast::Config ToastAlready(TimeId left) {\n\treturn {\n\t\t.title = tr::lng_stealth_mode_already_title(tr::now),\n\t\t.text = tr::lng_stealth_mode_already_about(\n\t\t\ttr::now,\n\t\t\tlt_left,\n\t\t\tTextWithEntities{ TimeLeftText(left) },\n\t\t\ttr::rich),\n\t\t.st = &st::storiesStealthToast,\n\t\t.adaptive = true,\n\t\t.duration = kAlreadyToastDuration,\n\t};\n}\n\n[[nodiscard]] Ui::Toast::Config ToastActivated() {\n\treturn {\n\t\t.title = tr::lng_stealth_mode_enabled_tip_title(tr::now),\n\t\t.text = tr::lng_stealth_mode_enabled_tip(\n\t\t\ttr::now,\n\t\t\ttr::rich),\n\t\t.st = &st::storiesStealthToast,\n\t\t.adaptive = true,\n\t\t.duration = kAlreadyToastDuration,\n\t};\n}\n\n[[nodiscard]] Ui::Toast::Config ToastCooldown() {\n\treturn {\n\t\t.text = tr::lng_stealth_mode_cooldown_tip(\n\t\t\ttr::now,\n\t\t\ttr::rich),\n\t\t.st = &st::storiesStealthToast,\n\t\t.adaptive = true,\n\t\t.duration = kAlreadyToastDuration,\n\t};\n}\n\n[[nodiscard]] rpl::producer<State> StateValue(\n\t\tnot_null<Main::Session*> session,\n\t\tbool hasCallback = false) {\n\treturn rpl::combine(\n\t\tsession->data().stories().stealthModeValue(),\n\t\tData::AmPremiumValue(session)\n\t) | rpl::map([=](Data::StealthMode mode, bool premium) {\n\t\treturn rpl::make_producer<State>([=](auto consumer) {\n\t\t\tstruct Info {\n\t\t\t\tbase::Timer timer;\n\t\t\t\tbool firstSent = false;\n\t\t\t\tbool enabledSent = false;\n\t\t\t\tbool cooldownSent = false;\n\t\t\t};\n\t\t\tauto lifetime = rpl::lifetime();\n\t\t\tconst auto info = lifetime.make_state<Info>();\n\t\t\tconst auto check = [=] {\n\t\t\t\tauto send = !info->firstSent;\n\t\t\t\tconst auto now = base::unixtime::now();\n\t\t\t\tconst auto left1 = (mode.enabledTill - now);\n\t\t\t\tconst auto left2 = (mode.cooldownTill - now);\n\t\t\t\tinfo->firstSent = true;\n\t\t\t\tif (!info->enabledSent && left1 <= 0) {\n\t\t\t\t\tsend = true;\n\t\t\t\t\tinfo->enabledSent = true;\n\t\t\t\t}\n\t\t\t\tif (!info->cooldownSent && left2 <= 0) {\n\t\t\t\t\tsend = true;\n\t\t\t\t\tinfo->cooldownSent = true;\n\t\t\t\t}\n\t\t\t\tconst auto left = (left1 <= 0)\n\t\t\t\t\t? left2\n\t\t\t\t\t: (left2 <= 0)\n\t\t\t\t\t? left1\n\t\t\t\t\t: std::min(left1, left2);\n\t\t\t\tif (left > 0) {\n\t\t\t\t\tinfo->timer.callOnce(left * crl::time(1000));\n\t\t\t\t}\n\t\t\t\tif (send) {\n\t\t\t\t\tconsumer.put_next(\n\t\t\t\t\t\tState{ mode, now, premium, hasCallback });\n\t\t\t\t}\n\t\t\t\tif (left <= 0) {\n\t\t\t\t\tconsumer.put_done();\n\t\t\t\t}\n\t\t\t};\n\t\t\tinfo->timer.setCallback(check);\n\t\t\tcheck();\n\t\t\treturn lifetime;\n\t\t});\n\t}) | rpl::flatten_latest();\n}\n\n[[nodiscard]] Ui::FeatureListEntry FeaturePast(\n\t\tconst style::StealthBoxStyle &st) {\n\treturn {\n\t\t.icon = st.featurePastIcon,\n\t\t.title = tr::lng_stealth_mode_past_title(tr::now),\n\t\t.about = { tr::lng_stealth_mode_past_about(tr::now) },\n\t};\n}\n\n[[nodiscard]] Ui::FeatureListEntry FeatureNext(\n\t\tconst style::StealthBoxStyle &st) {\n\treturn {\n\t\t.icon = st.featureNextIcon,\n\t\t.title = tr::lng_stealth_mode_next_title(tr::now),\n\t\t.about = { tr::lng_stealth_mode_next_about(tr::now) },\n\t};\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeLogo(\n\t\tQWidget *parent,\n\t\tconst style::StealthBoxStyle &st) {\n\tconst auto add = st::storiesStealthLogoAdd;\n\tconst auto icon = &st.logoIcon;\n\tconst auto size = QSize(2 * add, 2 * add) + icon->size();\n\tauto result = object_ptr<Ui::PaddingWrap<Ui::RpWidget>>(\n\t\tparent,\n\t\tobject_ptr<Ui::RpWidget>(parent),\n\t\tst::storiesStealthLogoMargin);\n\tconst auto inner = result->entity();\n\tinner->resize(size);\n\tinner->paintRequest(\n\t) | rpl::on_next([=, &st] {\n\t\tauto p = QPainter(inner);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setBrush(st.logoBg);\n\t\tp.setPen(Qt::NoPen);\n\t\tconst auto left = (inner->width() - size.width()) / 2;\n\t\tconst auto top = (inner->height() - size.height()) / 2;\n\t\tconst auto rect = QRect(QPoint(left, top), size);\n\t\tp.drawEllipse(rect);\n\t\ticon->paintInCenter(p, rect);\n\t}, inner->lifetime());\n\treturn result;\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeTitle(\n\t\tQWidget *parent,\n\t\tconst style::StealthBoxStyle &st) {\n\treturn object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\tparent,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tparent,\n\t\t\ttr::lng_stealth_mode_title(tr::now),\n\t\t\tst.box.title),\n\t\tst::storiesStealthTitleMargin);\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeAbout(\n\t\tQWidget *parent,\n\t\trpl::producer<State> state,\n\t\tconst style::StealthBoxStyle &st) {\n\tauto text = std::move(state) | rpl::map([](const State &state) {\n\t\treturn state.premium\n\t\t\t? tr::lng_stealth_mode_about(tr::now)\n\t\t\t: tr::lng_stealth_mode_unlock_about(tr::now);\n\t});\n\treturn object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\tparent,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tparent,\n\t\t\tstd::move(text),\n\t\t\tst.about),\n\t\tst::storiesStealthAboutMargin);\n}\n\n[[nodiscard]] object_ptr<Ui::RoundButton> MakeButton(\n\t\tQWidget *parent,\n\t\trpl::producer<State> state,\n\t\tconst style::StealthBoxStyle &st) {\n\tauto text = rpl::duplicate(state) | rpl::map([](const State &state) {\n\t\tif (!state.premium) {\n\t\t\treturn tr::lng_stealth_mode_unlock();\n\t\t} else if (state.mode.cooldownTill <= state.now) {\n\t\t\treturn state.hasCallback\n\t\t\t\t? tr::lng_stealth_mode_enable_and_open()\n\t\t\t\t: tr::lng_stealth_mode_enable();\n\t\t}\n\t\treturn rpl::single(\n\t\t\trpl::empty\n\t\t) | rpl::then(\n\t\t\tbase::timer_each(250)\n\t\t) | rpl::map([=] {\n\t\t\tconst auto now = base::unixtime::now();\n\t\t\tconst auto left = std::max(state.mode.cooldownTill - now, 1);\n\t\t\treturn tr::lng_stealth_mode_cooldown_in(\n\t\t\t\ttr::now,\n\t\t\t\tlt_left,\n\t\t\t\tTimeLeftText(left));\n\t\t}) | rpl::type_erased;\n\t}) | rpl::flatten_latest();\n\n\tauto result = object_ptr<Ui::RoundButton>(\n\t\tparent,\n\t\trpl::single(QString()),\n\t\tst.box.button);\n\tconst auto raw = result.data();\n\traw->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\traw,\n\t\tstd::move(text),\n\t\tst.buttonLabel);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tlabel->show();\n\n\tconst auto lock = Ui::CreateChild<Ui::RpWidget>(raw);\n\tlock->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tlock->resize(st.lockIcon.size());\n\tlock->paintRequest(\n\t) | rpl::on_next([=, &st] {\n\t\tauto p = QPainter(lock);\n\t\tst.lockIcon.paintInCenter(p, lock->rect());\n\t}, lock->lifetime());\n\n\tconst auto lockLeft = -st.buttonLabel.style.font->height;\n\tconst auto updateLabelLockGeometry = [=, &st] {\n\t\tconst auto outer = raw->width();\n\t\tconst auto added = -st.box.button.width;\n\t\tconst auto skip = lock->isHidden() ? 0 : (lockLeft + lock->width());\n\t\tconst auto width = outer - added - skip;\n\t\tconst auto top = st.box.button.textTop;\n\t\tlabel->resizeToWidth(width);\n\t\tlabel->move(added / 2, top);\n\t\tconst auto inner = std::min(label->textMaxWidth(), width);\n\t\tconst auto right = (added / 2) + (outer - inner) / 2 + inner;\n\t\tconst auto lockTop = (label->height() - lock->height()) / 2;\n\t\tlock->move(right + lockLeft, top + lockTop);\n\t};\n\n\tstd::move(state) | rpl::on_next([=](const State &state) {\n\t\tconst auto cooldown = state.premium\n\t\t\t&& (state.mode.cooldownTill > state.now);\n\t\tlabel->setOpacity(cooldown ? kCooldownButtonLabelOpacity : 1.);\n\t\tlock->setVisible(!state.premium);\n\t\tupdateLabelLockGeometry();\n\t}, label->lifetime());\n\n\traw->widthValue(\n\t) | rpl::on_next(updateLabelLockGeometry, label->lifetime());\n\n\treturn result;\n}\n\n[[nodiscard]] object_ptr<Ui::BoxContent> StealthModeBox(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tFn<void()> onActivated,\n\t\tconst style::StealthBoxStyle &st) {\n\treturn Box([=](not_null<Ui::GenericBox*> box) {\n\t\tstruct Data {\n\t\t\trpl::variable<State> state;\n\t\t\tbool requested = false;\n\t\t};\n\t\tconst auto data = box->lifetime().make_state<Data>();\n\t\tdata->state = StateValue(&show->session(), onActivated != nullptr);\n\t\tbox->setWidth(st::boxWideWidth);\n\t\tbox->setStyle(st.box);\n\t\tbox->addRow(MakeLogo(box, st));\n\t\tbox->addRow(MakeTitle(box, st), style::al_top);\n\t\tbox->addRow(MakeAbout(box, data->state.value(), st), style::al_top);\n\t\tconst auto make = [&](const Ui::FeatureListEntry &entry) {\n\t\t\treturn Ui::MakeFeatureListEntry(\n\t\t\t\tbox,\n\t\t\t\tentry,\n\t\t\t\t{},\n\t\t\t\tst.featureTitle,\n\t\t\t\tst.featureAbout);\n\t\t};\n\t\tbox->addRow(make(FeaturePast(st)));\n\t\tbox->addRow(\n\t\t\tmake(FeatureNext(st)),\n\t\t\t(st::boxRowPadding\n\t\t\t\t+ QMargins(0, 0, 0, st::storiesStealthBoxBottom)));\n\t\tbox->setNoContentMargin(true);\n\t\tbox->addTopButton(st.boxClose, [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t\tconst auto button = box->addButton(\n\t\t\tMakeButton(box, data->state.value(), st));\n\t\tbutton->resizeToWidth(st::boxWideWidth\n\t\t\t- st.box.buttonPadding.left()\n\t\t\t- st.box.buttonPadding.right());\n\t\tbutton->setClickedCallback([=] {\n\t\t\tconst auto now = data->state.current();\n\t\t\tif (now.mode.enabledTill > now.now) {\n\t\t\t\tshow->showToast(ToastActivated());\n\t\t\t\tbox->closeBox();\n\t\t\t} else if (!now.premium) {\n\t\t\t\tdata->requested = false;\n\t\t\t\tif (const auto window = show->resolveWindow()) {\n\t\t\t\t\tShowPremiumPreviewBox(window, PremiumFeature::Stories);\n\t\t\t\t\twindow->window().activate();\n\t\t\t\t}\n\t\t\t} else if (now.mode.cooldownTill > now.now) {\n\t\t\t\tshow->showToast(ToastCooldown());\n\t\t\t\tbox->closeBox();\n\t\t\t} else if (!data->requested) {\n\t\t\t\tdata->requested = true;\n\t\t\t\tshow->session().data().stories().activateStealthMode(\n\t\t\t\t\tcrl::guard(box, [=] { data->requested = false; }));\n\t\t\t}\n\t\t});\n\t\tdata->state.value() | rpl::filter([](const State &state) {\n\t\t\treturn state.mode.enabledTill > state.now;\n\t\t}) | rpl::on_next([=] {\n\t\t\tbox->closeBox();\n\t\t\tshow->showToast(ToastActivated());\n\t\t\tif (onActivated) {\n\t\t\t\tonActivated();\n\t\t\t}\n\t\t}, box->lifetime());\n\t});\n}\n\n} // namespace\n\nvoid SetupStealthMode(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tStealthModeDescriptor descriptor) {\n\tconst auto onActivated = descriptor.onActivated;\n\tconst auto st = descriptor.st;\n\tconst auto now = base::unixtime::now();\n\tconst auto mode = show->session().data().stories().stealthMode();\n\tif (const auto left = mode.enabledTill - now; left > 0) {\n\t\tshow->showToast(ToastAlready(left));\n\t\tif (onActivated) {\n\t\t\tonActivated();\n\t\t}\n\t} else {\n\t\tconst auto &style = st ? *st : st::storiesStealthStyle;\n\t\tshow->show(StealthModeBox(show, onActivated, style));\n\t}\n}\n\nvoid AddStealthModeMenu(\n\t\tconst Ui::Menu::MenuCallback &add,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<Window::SessionController*> controller) {\n\tif (!peer->session().premiumPossible() || !peer->isUser()) {\n\t\treturn;\n\t}\n\tconst auto now = base::unixtime::now();\n\tconst auto stealth = peer->owner().stories().stealthMode();\n\tadd(\n\t\ttr::lng_stories_view_anonymously(tr::now),\n\t\t[=] {\n\t\t\tSetupStealthMode(\n\t\t\t\tcontroller->uiShow(),\n\t\t\t\tStealthModeDescriptor{\n\t\t\t\t\t[=] { controller->openPeerStories(peer->id); },\n\t\t\t\t\t&st::storiesStealthStyleDefault,\n\t\t\t\t});\n\t\t},\n\t\t((peer->session().premium() || (stealth.enabledTill > now))\n\t\t\t? &st::menuIconStealth\n\t\t\t: &st::menuIconStealthLocked));\n}\n\nQString TimeLeftText(int left) {\n\tExpects(left >= 0);\n\n\tconst auto hours = left / 3600;\n\tconst auto minutes = (left % 3600) / 60;\n\tconst auto seconds = left % 60;\n\tconst auto zero = QChar('0');\n\tif (hours) {\n\t\treturn u\"%1:%2:%3\"_q\n\t\t\t.arg(hours)\n\t\t\t.arg(minutes, 2, 10, zero)\n\t\t\t.arg(seconds, 2, 10, zero);\n\t} else if (minutes) {\n\t\treturn u\"%1:%2\"_q.arg(minutes).arg(seconds, 2, 10, zero);\n\t}\n\treturn u\"0:%1\"_q.arg(left, 2, 10, zero);\n}\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_stealth.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace style {\nstruct StealthBoxStyle;\n} // namespace style\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Ui::Menu {\nstruct MenuCallback;\n} // namespace Ui::Menu\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Media::Stories {\n\nstruct StealthModeDescriptor {\n\tFn<void()> onActivated = nullptr;\n\tconst style::StealthBoxStyle *st = nullptr;\n};\n\nvoid SetupStealthMode(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tStealthModeDescriptor descriptor = {});\n\nvoid AddStealthModeMenu(\n\tconst Ui::Menu::MenuCallback &add,\n\tnot_null<PeerData*> peer,\n\tnot_null<Window::SessionController*> controller);\n\n[[nodiscard]] QString TimeLeftText(int left);\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_view.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/stories/media_stories_view.h\"\n\n#include \"data/data_file_origin.h\"\n#include \"history/view/controls/compose_controls_common.h\"\n#include \"media/stories/media_stories_controller.h\"\n#include \"media/stories/media_stories_delegate.h\"\n#include \"media/stories/media_stories_header.h\"\n#include \"media/stories/media_stories_slider.h\"\n#include \"media/stories/media_stories_reply.h\"\n\nnamespace Media::Stories {\n\nView::View(not_null<Delegate*> delegate)\n: _controller(std::make_unique<Controller>(delegate)) {\n}\n\nView::~View() = default;\n\nvoid View::show(\n\t\tnot_null<Data::Story*> story,\n\t\tData::StoriesContext context) {\n\t_controller->show(story, context);\n}\n\nvoid View::ready() {\n\t_controller->ready();\n}\n\nData::Story *View::story() const {\n\treturn _controller->story();\n}\n\nQRect View::finalShownGeometry() const {\n\treturn _controller->layout().content;\n}\n\nrpl::producer<QRect> View::finalShownGeometryValue() const {\n\treturn _controller->layoutValue(\n\t\t) | rpl::map([=](const Layout &layout) {\n\t\t\treturn layout.content;\n\t\t}) | rpl::distinct_until_changed();\n}\n\nContentLayout View::contentLayout() const {\n\treturn _controller->contentLayout();\n}\n\nbool View::closeByClickAt(QPoint position) const {\n\treturn _controller->closeByClickAt(position);\n}\n\nvoid View::updatePlayback(const Player::TrackState &state) {\n\t_controller->updateVideoPlayback(state);\n}\n\nClickHandlerPtr View::lookupAreaHandler(QPoint point) const {\n\treturn _controller->lookupAreaHandler(point);\n}\n\nbool View::subjumpAvailable(int delta) const {\n\treturn _controller->subjumpAvailable(delta);\n}\n\nbool View::subjumpFor(int delta) const {\n\treturn _controller->subjumpFor(delta);\n}\n\nbool View::jumpFor(int delta) const {\n\treturn _controller->jumpFor(delta);\n}\n\nbool View::paused() const {\n\treturn _controller->paused();\n}\n\nvoid View::togglePaused(bool paused) {\n\t_controller->togglePaused(paused);\n}\n\nvoid View::contentPressed(bool pressed) {\n\t_controller->contentPressed(pressed);\n}\n\nvoid View::menuShown(bool shown) {\n\t_controller->setMenuShown(shown);\n}\n\nvoid View::shareRequested() {\n\t_controller->shareRequested();\n}\n\nvoid View::deleteRequested() {\n\t_controller->deleteRequested();\n}\n\nvoid View::reportRequested() {\n\t_controller->reportRequested();\n}\n\nvoid View::toggleInProfileRequested(bool inProfile) {\n\t_controller->toggleInProfileRequested(inProfile);\n}\n\nbool View::ignoreWindowMove(QPoint position) const {\n\treturn _controller->ignoreWindowMove(position);\n}\n\nvoid View::tryProcessKeyInput(not_null<QKeyEvent*> e) {\n\t_controller->tryProcessKeyInput(e);\n}\n\nbool View::allowStealthMode() const {\n\treturn _controller->allowStealthMode();\n}\n\nvoid View::setupStealthMode() {\n\t_controller->setupStealthMode();\n}\n\nauto View::attachReactionsToMenu(\n\tnot_null<Ui::PopupMenu*> menu,\n\tQPoint desiredPosition)\n-> AttachStripResult {\n\treturn _controller->attachReactionsToMenu(menu, desiredPosition);\n}\n\nSiblingView View::sibling(SiblingType type) const {\n\treturn _controller->sibling(type);\n}\n\nData::FileOrigin View::fileOrigin() const {\n\treturn _controller->fileOrigin();\n}\n\nTextWithEntities View::captionText() const {\n\treturn _controller->captionText();\n}\n\nbool View::skipCaption() const {\n\treturn _controller->skipCaption();\n}\n\nbool View::repost() const {\n\treturn _controller->repost();\n}\n\nQMargins View::repostCaptionPadding() const {\n\treturn _controller->repostCaptionPadding();\n}\n\nvoid View::drawRepostInfo(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth) const {\n\t_controller->drawRepostInfo(p, x, y, availableWidth);\n}\n\nRepostClickHandler View::lookupRepostHandler(QPoint position) const {\n\treturn _controller->lookupRepostHandler(position);\n}\n\nvoid View::showFullCaption() {\n\t_controller->showFullCaption();\n}\n\nstd::shared_ptr<ChatHelpers::Show> View::uiShow() const {\n\treturn _controller->uiShow();\n}\n\n\nvoid View::updateVideoStream(not_null<Calls::GroupCall*> videoStream) {\n\t_controller->updateVideoStream(videoStream);\n}\n\nrpl::producer<bool> View::commentsShownValue() const {\n\treturn _controller->commentsStateValue(\n\t) | rpl::map([=](CommentsState state) {\n\t\treturn (state == CommentsState::Shown);\n\t}) | rpl::distinct_until_changed();\n}\n\nrpl::lifetime &View::lifetime() {\n\treturn _controller->lifetime();\n}\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/stories/media_stories_view.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass ClickHandlerHost;\n\nnamespace Calls {\nclass GroupCall;\n} // namespace Calls\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nclass Story;\nstruct StoriesContext;\nstruct FileOrigin;\n} // namespace Data\n\nnamespace Media::Player {\nstruct TrackState;\n} // namespace Media::Player\n\nnamespace HistoryView::Reactions {\nenum class AttachSelectorResult;\n} // namespace HistoryView::Reactions\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Media::Stories {\n\nclass Delegate;\nclass Controller;\n\nstruct ContentLayout {\n\tQRect geometry;\n\tfloat64 fade = 0.;\n\tfloat64 scale = 1.;\n\tint radius = 0;\n\tbool headerOutside = false;\n};\n\nenum class SiblingType;\n\nstruct SiblingView {\n\tQImage image;\n\tContentLayout layout;\n\tQImage userpic;\n\tQPoint userpicPosition;\n\tQImage name;\n\tQPoint namePosition;\n\tfloat64 nameOpacity = 0.;\n\tfloat64 scale = 1.;\n\n\t[[nodiscard]] bool valid() const {\n\t\treturn !image.isNull();\n\t}\n\texplicit operator bool() const {\n\t\treturn valid();\n\t}\n};\n\nstruct RepostClickHandler {\n\tClickHandlerPtr link;\n\tClickHandlerHost *host = nullptr;\n\n\texplicit operator bool() const {\n\t\treturn link && host;\n\t}\n};\n\ninline constexpr auto kCollapsedCaptionLines = 2;\ninline constexpr auto kMaxShownCaptionLines = 4;\n\nclass View final {\npublic:\n\texplicit View(not_null<Delegate*> delegate);\n\t~View();\n\n\tvoid show(not_null<Data::Story*> story, Data::StoriesContext context);\n\tvoid ready();\n\n\t[[nodiscard]] Data::Story *story() const;\n\t[[nodiscard]] QRect finalShownGeometry() const;\n\t[[nodiscard]] rpl::producer<QRect> finalShownGeometryValue() const;\n\t[[nodiscard]] ContentLayout contentLayout() const;\n\t[[nodiscard]] bool closeByClickAt(QPoint position) const;\n\t[[nodiscard]] SiblingView sibling(SiblingType type) const;\n\t[[nodiscard]] Data::FileOrigin fileOrigin() const;\n\t[[nodiscard]] TextWithEntities captionText() const;\n\t[[nodiscard]] bool skipCaption() const;\n\t[[nodiscard]] bool repost() const;\n\tvoid showFullCaption();\n\n\t[[nodiscard]] QMargins repostCaptionPadding() const;\n\tvoid drawRepostInfo(Painter &p, int x, int y, int availableWidth) const;\n\t[[nodiscard]] RepostClickHandler lookupRepostHandler(\n\t\tQPoint position) const;\n\n\tvoid updatePlayback(const Player::TrackState &state);\n\t[[nodiscard]] ClickHandlerPtr lookupAreaHandler(QPoint point) const;\n\n\t[[nodiscard]] bool subjumpAvailable(int delta) const;\n\t[[nodiscard]] bool subjumpFor(int delta) const;\n\t[[nodiscard]] bool jumpFor(int delta) const;\n\n\t[[nodiscard]] bool paused() const;\n\tvoid togglePaused(bool paused);\n\tvoid contentPressed(bool pressed);\n\tvoid menuShown(bool shown);\n\n\tvoid shareRequested();\n\tvoid deleteRequested();\n\tvoid reportRequested();\n\tvoid toggleInProfileRequested(bool inProfile);\n\n\t[[nodiscard]] bool ignoreWindowMove(QPoint position) const;\n\tvoid tryProcessKeyInput(not_null<QKeyEvent*> e);\n\n\t[[nodiscard]] bool allowStealthMode() const;\n\tvoid setupStealthMode();\n\n\tusing AttachStripResult = HistoryView::Reactions::AttachSelectorResult;\n\t[[nodiscard]] AttachStripResult attachReactionsToMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tQPoint desiredPosition);\n\n\t[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() const;\n\n\tvoid updateVideoStream(not_null<Calls::GroupCall*> videoStream);\n\t[[nodiscard]] rpl::producer<bool> commentsShownValue() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tconst std::unique_ptr<Controller> _controller;\n\n};\n\n} // namespace Media::Stories\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_audio_track.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/streaming/media_streaming_audio_track.h\"\n\n#include \"media/streaming/media_streaming_utility.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/audio/media_child_ffmpeg_loader.h\"\n#include \"media/player/media_player_instance.h\"\n\nnamespace Media {\nnamespace Streaming {\n\nAudioTrack::AudioTrack(\n\tconst PlaybackOptions &options,\n\tStream &&stream,\n\tAudioMsgId audioId,\n\tFnMut<void(const Information &)> ready,\n\tFn<void(Error)> error)\n: _options(options)\n, _stream(std::move(stream))\n, _audioId(audioId)\n, _ready(std::move(ready))\n, _error(std::move(error))\n, _playPosition(options.position) {\n\tExpects(_stream.duration > 1);\n\tExpects(_stream.duration != kDurationUnavailable); // Not supported.\n\tExpects(_ready != nullptr);\n\tExpects(_error != nullptr);\n\tExpects(_audioId.externalPlayId() != 0);\n}\n\nint AudioTrack::streamIndex() const {\n\t// Thread-safe, because _stream.index is immutable.\n\treturn _stream.index;\n}\n\nAVRational AudioTrack::streamTimeBase() const {\n\treturn _stream.timeBase;\n}\n\ncrl::time AudioTrack::streamDuration() const {\n\treturn _stream.duration;\n}\n\nvoid AudioTrack::process(std::vector<FFmpeg::Packet> &&packets) {\n\tif (packets.empty()) {\n\t\treturn;\n\t} else if (packets.front().empty()) {\n\t\tAssert(packets.size() == 1);\n\t\t_readTillEnd = true;\n\t}\n\tfor (auto i = begin(packets), e = end(packets); i != e; ++i) {\n\t\tif (initialized()) {\n\t\t\tmixerEnqueue(gsl::make_span(&*i, (e - i)));\n\t\t\tbreak;\n\t\t} else if (!tryReadFirstFrame(std::move(*i))) {\n\t\t\t_error(Error::InvalidData);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nvoid AudioTrack::waitForData() {\n\tif (initialized()) {\n\t\tmixerForceToBuffer();\n\t}\n}\n\nbool AudioTrack::initialized() const {\n\treturn !_ready;\n}\n\nbool AudioTrack::tryReadFirstFrame(FFmpeg::Packet &&packet) {\n\tif (ProcessPacket(_stream, std::move(packet)).failed()) {\n\t\treturn false;\n\t}\n\twhile (true) {\n\t\tif (const auto error = ReadNextFrame(_stream)) {\n\t\t\tif (error.code() == AVERROR_EOF) {\n\t\t\t\tif (!_initialSkippingFrame) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t// Return the last valid frame if we seek too far.\n\t\t\t\t_stream.decodedFrame = std::move(_initialSkippingFrame);\n\t\t\t\treturn processFirstFrame();\n\t\t\t} else if (error.code() != AVERROR(EAGAIN) || _readTillEnd) {\n\t\t\t\treturn false;\n\t\t\t} else {\n\t\t\t\t// Waiting for more packets.\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (!fillStateFromFrame()) {\n\t\t\treturn false;\n\t\t} else if (_startedPosition >= _options.position) {\n\t\t\treturn processFirstFrame();\n\t\t}\n\n\t\t// Seek was with AVSEEK_FLAG_BACKWARD so first we get old frames.\n\t\t// Try skipping frames until one is after the requested position.\n\t\tstd::swap(_initialSkippingFrame, _stream.decodedFrame);\n\t\tif (!_stream.decodedFrame) {\n\t\t\t_stream.decodedFrame = FFmpeg::MakeFramePointer();\n\t\t}\n\t}\n}\n\nbool AudioTrack::processFirstFrame() {\n\tif (!FFmpeg::FrameHasData(_stream.decodedFrame.get())) {\n\t\treturn false;\n\t}\n\tmixerInit();\n\tcallReady();\n\treturn true;\n}\n\nbool AudioTrack::fillStateFromFrame() {\n\tconst auto position = FramePosition(_stream);\n\tif (position == kTimeUnknown) {\n\t\treturn false;\n\t}\n\t_startedPosition = position;\n\treturn true;\n}\n\nvoid AudioTrack::mixerInit() {\n\tExpects(!initialized());\n\n\tauto data = std::make_unique<ExternalSoundData>();\n\tdata->frame = std::move(_stream.decodedFrame);\n\tdata->codec = std::move(_stream.codec);\n\tdata->duration = _stream.duration;\n\tdata->speed = _options.speed;\n\n\tMedia::Player::mixer()->play(\n\t\t_audioId,\n\t\tstd::move(data),\n\t\t_startedPosition);\n}\n\nvoid AudioTrack::callReady() {\n\tExpects(_ready != nullptr);\n\n\tauto data = AudioInformation();\n\tdata.state.duration = _stream.duration;\n\tdata.state.position = _startedPosition;\n\tdata.state.receivedTill = _readTillEnd\n\t\t? _stream.duration\n\t\t: _startedPosition;\n\tbase::take(_ready)({ VideoInformation(), data });\n}\n\nvoid AudioTrack::mixerEnqueue(gsl::span<FFmpeg::Packet> packets) {\n\tMedia::Player::mixer()->feedFromExternal({\n\t\t_audioId,\n\t\tpackets\n\t});\n}\n\nvoid AudioTrack::mixerForceToBuffer() {\n\tMedia::Player::mixer()->forceToBufferExternal(_audioId);\n}\n\nvoid AudioTrack::pause(crl::time time) {\n\tExpects(initialized());\n\n\tMedia::Player::mixer()->pause(_audioId, true);\n}\n\nvoid AudioTrack::resume(crl::time time) {\n\tExpects(initialized());\n\n\tMedia::Player::mixer()->resume(_audioId, true);\n}\n\nvoid AudioTrack::stop() {\n\tif (_audioId.externalPlayId()) {\n\t\tMedia::Player::mixer()->stop(_audioId);\n\t}\n}\n\nvoid AudioTrack::setSpeed(float64 speed) {\n\t_options.speed = speed;\n\tMedia::Player::mixer()->setSpeedFromExternal(_audioId, speed);\n}\n\nrpl::producer<> AudioTrack::waitingForData() const {\n\treturn _waitingForData.events();\n}\n\nrpl::producer<crl::time> AudioTrack::playPosition() {\n\tExpects(_ready == nullptr);\n\n\tif (!_subscription) {\n\t\t_subscription = Media::Player::Updated(\n\t\t) | rpl::on_next([=](const AudioMsgId &id) {\n\t\t\tusing State = Media::Player::State;\n\t\t\tif (id != _audioId) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto state = Media::Player::mixer()->currentState(\n\t\t\t\t_audioId.type());\n\t\t\tif (state.id != _audioId) {\n\t\t\t\t// #TODO streaming later muted by other\n\t\t\t\treturn;\n\t\t\t} else switch (state.state) {\n\t\t\tcase State::Stopped:\n\t\t\tcase State::StoppedAtEnd:\n\t\t\tcase State::PausedAtEnd:\n\t\t\t\t_playPosition.reset();\n\t\t\t\treturn;\n\t\t\tcase State::StoppedAtError:\n\t\t\tcase State::StoppedAtStart:\n\t\t\t\t_error(Error::InvalidData);\n\t\t\t\treturn;\n\t\t\tcase State::Starting:\n\t\t\tcase State::Playing:\n\t\t\tcase State::Stopping:\n\t\t\tcase State::Pausing:\n\t\t\tcase State::Resuming:\n\t\t\t\tif (state.waitingForData) {\n\t\t\t\t\t_waitingForData.fire({});\n\t\t\t\t}\n\t\t\t\t_playPosition = std::clamp(\n\t\t\t\t\tcrl::time((state.position * 1000 + (state.frequency / 2))\n\t\t\t\t\t\t/ state.frequency),\n\t\t\t\t\tcrl::time(0),\n\t\t\t\t\t_stream.duration - 1);\n\t\t\t\treturn;\n\t\t\tcase State::Paused:\n\t\t\t\treturn;\n\t\t\t}\n\t\t});\n\t}\n\treturn _playPosition.value();\n}\n\nAudioTrack::~AudioTrack() {\n\tstop();\n}\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_audio_track.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/streaming/media_streaming_utility.h\"\n\nnamespace Media {\nnamespace Streaming {\n\nclass AudioTrack final {\npublic:\n\t// Called from some unspecified thread.\n\t// Callbacks are assumed to be thread-safe.\n\tAudioTrack(\n\t\tconst PlaybackOptions &options,\n\t\tStream &&stream,\n\t\tAudioMsgId audioId,\n\t\tFnMut<void(const Information &)> ready,\n\t\tFn<void(Error)> error);\n\n\t// Called from the main thread.\n\t// Must be called after 'ready' was invoked.\n\tvoid pause(crl::time time);\n\tvoid resume(crl::time time);\n\n\t// Allow to irreversibly stop only audio track.\n\tvoid stop();\n\n\t// Called from the main thread.\n\tvoid setSpeed(float64 speed);\n\t[[nodiscard]] rpl::producer<> waitingForData() const;\n\n\t// Called from the main thread.\n\t// Non-const, because we subscribe to changes on the first call.\n\t// Must be called after 'ready' was invoked.\n\t[[nodiscard]] rpl::producer<crl::time> playPosition();\n\n\t// Thread-safe.\n\t[[nodiscard]] int streamIndex() const;\n\t[[nodiscard]] AVRational streamTimeBase() const;\n\t[[nodiscard]] crl::time streamDuration() const;\n\n\t// Called from the same unspecified thread.\n\tvoid process(std::vector<FFmpeg::Packet> &&packets);\n\tvoid waitForData();\n\n\t// Called from the main thread.\n\t~AudioTrack();\n\nprivate:\n\t// Called from the same unspecified thread.\n\t[[nodiscard]] bool initialized() const;\n\t[[nodiscard]] bool tryReadFirstFrame(FFmpeg::Packet &&packet);\n\t[[nodiscard]] bool fillStateFromFrame();\n\t[[nodiscard]] bool processFirstFrame();\n\tvoid mixerInit();\n\tvoid mixerEnqueue(gsl::span<FFmpeg::Packet> packets);\n\tvoid mixerForceToBuffer();\n\tvoid callReady();\n\n\tPlaybackOptions _options;\n\n\t// Accessed from the same unspecified thread.\n\tStream _stream;\n\tconst AudioMsgId _audioId;\n\tbool _readTillEnd = false;\n\n\t// Assumed to be thread-safe.\n\tFnMut<void(const Information &)> _ready;\n\tconst Fn<void(Error)> _error;\n\n\t// First set from the same unspecified thread before _ready is called.\n\t// After that is immutable.\n\tcrl::time _startedPosition = kTimeUnknown;\n\n\t// Accessed from the main thread.\n\trpl::lifetime _subscription;\n\trpl::event_stream<> _waitingForData;\n\t// First set from the same unspecified thread before _ready is called.\n\t// After that accessed from the main thread.\n\trpl::variable<crl::time> _playPosition;\n\n\t// For initial frame skipping for an exact seek.\n\tFFmpeg::FramePointer _initialSkippingFrame;\n\n};\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_audio_msg_id.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/rect_part.h\"\n\nnamespace Media {\n\ninline constexpr auto kTimeUnknown = std::numeric_limits<crl::time>::min();\ninline constexpr auto kDurationMax = crl::time(std::numeric_limits<int>::max());\ninline constexpr auto kDurationUnavailable = std::numeric_limits<crl::time>::max();\n\nnamespace Audio {\nbool SupportsSpeedControl();\n} // namespace Audio\n\nnamespace Streaming {\n\ninline bool SupportsSpeedControl() {\n\treturn Media::Audio::SupportsSpeedControl();\n}\n\nclass VideoTrack;\nclass AudioTrack;\n\nenum class Mode {\n\tBoth,\n\tAudio,\n\tVideo,\n\tInspection,\n};\n\nstruct PlaybackOptions {\n\tMode mode = Mode::Both;\n\tcrl::time position = 0;\n\tcrl::time durationOverride = 0;\n\tfloat64 speed = 1.; // Valid values between 0.5 and 2.\n\tAudioMsgId audioId;\n\tbool syncVideoByAudio = true;\n\tbool waitForMarkAsShown = false;\n\tbool hwAllowed = false;\n\tbool seekable = true;\n\tbool loop = false;\n};\n\nstruct TrackState {\n\tcrl::time position = kTimeUnknown;\n\tcrl::time receivedTill = kTimeUnknown;\n\tcrl::time duration = kTimeUnknown;\n};\n\nstruct VideoInformation {\n\tTrackState state;\n\tQSize size;\n\tQImage cover;\n\tint rotation = 0;\n\tfloat64 fps = 0.;\n\tbool alpha = false;\n};\n\nstruct AudioInformation {\n\tTrackState state;\n};\n\nstruct Information {\n\tVideoInformation video;\n\tAudioInformation audio;\n\tint headerSize = 0;\n};\n\ntemplate <typename Track>\nstruct PreloadedUpdate {\n\tcrl::time till = kTimeUnknown;\n};\n\ntemplate <typename Track>\nstruct PlaybackUpdate {\n\tcrl::time position = kTimeUnknown;\n};\n\nusing PreloadedVideo = PreloadedUpdate<VideoTrack>;\nusing UpdateVideo = PlaybackUpdate<VideoTrack>;\nusing PreloadedAudio = PreloadedUpdate<AudioTrack>;\nusing UpdateAudio = PlaybackUpdate<AudioTrack>;\n\nstruct WaitingForData {\n\tbool waiting = false;\n};\n\nstruct SpeedEstimate {\n\tint bytesPerSecond = 0;\n\tbool unreliable = false;\n};\n\nstruct MutedByOther {\n};\n\nstruct Finished {\n};\n\nstruct Update {\n\tstd::variant<\n\t\tInformation,\n\t\tPreloadedVideo,\n\t\tUpdateVideo,\n\t\tPreloadedAudio,\n\t\tUpdateAudio,\n\t\tWaitingForData,\n\t\tSpeedEstimate,\n\t\tMutedByOther,\n\t\tFinished> data;\n};\n\nenum class Error {\n\tOpenFailed,\n\tLoadFailed,\n\tInvalidData,\n\tNotStreamable,\n};\n\nstruct FrameRequest {\n\tQSize resize;\n\tQSize outer;\n\tImages::CornersMaskRef rounding;\n\tQImage mask;\n\tQColor colored = QColor(0, 0, 0, 0);\n\tbool blurredBackground = false;\n\tbool requireARGB32 = true;\n\tbool keepAlpha = false;\n\tbool strict = true;\n\n\tstatic FrameRequest NonStrict() {\n\t\tauto result = FrameRequest();\n\t\tresult.strict = false;\n\t\treturn result;\n\t}\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn blurredBackground ? outer.isEmpty() : resize.isEmpty();\n\t}\n\n\t[[nodiscard]] bool operator==(const FrameRequest &other) const {\n\t\treturn (resize == other.resize)\n\t\t\t&& (outer == other.outer)\n\t\t\t&& (rounding == other.rounding)\n\t\t\t&& (mask.constBits() == other.mask.constBits())\n\t\t\t&& (colored == other.colored)\n\t\t\t&& (keepAlpha == other.keepAlpha)\n\t\t\t&& (requireARGB32 == other.requireARGB32)\n\t\t\t&& (blurredBackground == other.blurredBackground);\n\t}\n\t[[nodiscard]] bool operator!=(const FrameRequest &other) const {\n\t\treturn !(*this == other);\n\t}\n\n\t[[nodiscard]] bool goodFor(const FrameRequest &other) const {\n\t\treturn (blurredBackground == other.blurredBackground)\n\t\t\t&& (requireARGB32 == other.requireARGB32)\n\t\t\t&& (keepAlpha == other.keepAlpha)\n\t\t\t&& (colored == other.colored)\n\t\t\t&& ((strict && !other.strict) || (*this == other));\n\t}\n};\n\nenum class FrameFormat {\n\tNone,\n\tARGB32,\n\tYUV420,\n\tNV12,\n\tNativeTexture,\n};\n\nstruct FrameChannel {\n\tconst void *data = nullptr;\n\tint stride = 0;\n};\n\nstruct FrameYUV {\n\tQSize size;\n\tQSize chromaSize;\n\tFrameChannel y;\n\tFrameChannel u;\n\tFrameChannel v;\n};\n\nstruct NativeFrame {\n\tvoid *pixelBuffer = nullptr;\n\tQSize size;\n\tQSize chromaSize;\n};\n\nstruct FrameWithInfo {\n\tQImage image;\n\tFrameYUV *yuv = nullptr;\n\tNativeFrame *nativeFrame = nullptr;\n\tFrameFormat format = FrameFormat::None;\n\tint index = -1;\n\tbool alpha = false;\n};\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_document.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/streaming/media_streaming_document.h\"\n\n#include \"media/streaming/media_streaming_instance.h\"\n#include \"media/streaming/media_streaming_loader.h\"\n#include \"media/streaming/media_streaming_reader.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"main/main_session.h\"\n#include \"storage/file_download.h\" // Storage::kMaxFileInMemory.\n#include \"styles/style_widgets.h\"\n\n#include <QtCore/QBuffer>\n\nnamespace Media {\nnamespace Streaming {\nnamespace {\n\nconstexpr auto kWaitingFastDuration = crl::time(200);\nconstexpr auto kWaitingShowDuration = crl::time(500);\nconstexpr auto kWaitingShowDelay = crl::time(500);\nconstexpr auto kGoodThumbQuality = 87;\nconstexpr auto kSwitchQualityUpPreloadedThreshold = 4 * crl::time(1000);\nconstexpr auto kSwitchQualityUpSpeedMultiplier = 1.2;\n\n} // namespace\n\nDocument::Document(\n\tnot_null<DocumentData*> document,\n\tstd::shared_ptr<Reader> reader,\n\tstd::vector<QualityDescriptor> otherQualities)\n: Document(std::move(reader), document, {}, std::move(otherQualities)) {\n\t_player.fullInCache(\n\t) | rpl::on_next([=](bool fullInCache) {\n\t\t_document->setLoadedInMediaCache(fullInCache);\n\t}, _player.lifetime());\n}\n\nDocument::Document(\n\tnot_null<PhotoData*> photo,\n\tstd::shared_ptr<Reader> reader,\n\tstd::vector<QualityDescriptor> otherQualities)\n: Document(std::move(reader), {}, photo, {}) {\n}\n\nDocument::Document(std::unique_ptr<Loader> loader)\n: Document(std::make_shared<Reader>(std::move(loader)), {}, {}, {}) {\n}\n\nDocument::Document(\n\tstd::shared_ptr<Reader> reader,\n\tDocumentData *document,\n\tPhotoData *photo,\n\tstd::vector<QualityDescriptor> otherQualities)\n: _document(document)\n, _photo(photo)\n, _player(std::move(reader))\n, _radial(\n\t[=] { waitingCallback(); },\n\tst::defaultInfiniteRadialAnimation)\n, _otherQualities(std::move(otherQualities)) {\n\tresubscribe();\n}\n\nvoid Document::resubscribe() {\n\t_subscription = _player.updates(\n\t) | rpl::on_next_error([=](Update &&update) {\n\t\thandleUpdate(std::move(update));\n\t}, [=](Streaming::Error &&error) {\n\t\thandleError(std::move(error));\n\t\tresubscribe();\n\t});\n}\n\nPlayer &Document::player() {\n\treturn _player;\n}\n\nconst Player &Document::player() const {\n\treturn _player;\n}\n\nconst Information &Document::info() const {\n\treturn _info;\n}\n\nvoid Document::play(const PlaybackOptions &options) {\n\t_player.play(options);\n\t_info.audio.state.position\n\t\t= _info.video.state.position\n\t\t= options.position;\n\twaitingChange(true);\n}\n\nvoid Document::saveFrameToCover() {\n\t_info.video.cover = _player.ready()\n\t\t? _player.currentFrameImage()\n\t\t: _info.video.cover;\n}\n\nvoid Document::registerInstance(not_null<Instance*> instance) {\n\t_instances.emplace(instance);\n}\n\nvoid Document::unregisterInstance(not_null<Instance*> instance) {\n\t_instances.remove(instance);\n\t_player.unregisterInstance(instance);\n\trefreshPlayerPriority();\n}\n\nvoid Document::refreshPlayerPriority() {\n\tif (_instances.empty()) {\n\t\treturn;\n\t}\n\tconst auto max = ranges::max_element(\n\t\t_instances,\n\t\tranges::less(),\n\t\t&Instance::priority);\n\t_player.setLoaderPriority((*max)->priority());\n}\n\nbool Document::waitingShown() const {\n\tif (!_fading.animating() && !_waiting) {\n\t\t_radial.stop(anim::type::instant);\n\t\treturn false;\n\t}\n\treturn _radial.animating();\n}\n\nfloat64 Document::waitingOpacity() const {\n\treturn _fading.value(_waiting ? 1. : 0.);\n}\n\nUi::RadialState Document::waitingState() const {\n\treturn _radial.computeState();\n}\n\nrpl::producer<int> Document::switchQualityRequests() const {\n\treturn _switchQualityRequests.events();\n}\n\nvoid Document::handleUpdate(Update &&update) {\n\tv::match(update.data, [&](Information &update) {\n\t\tready(std::move(update));\n\t}, [&](PreloadedVideo update) {\n\t\t_info.video.state.receivedTill = update.till;\n\t\tcheckSwitchToHigherQuality();\n\t}, [&](UpdateVideo update) {\n\t\t_info.video.state.position = update.position;\n\t}, [&](PreloadedAudio update) {\n\t\t_info.audio.state.receivedTill = update.till;\n\t}, [&](UpdateAudio update) {\n\t\t_info.audio.state.position = update.position;\n\t}, [&](WaitingForData update) {\n\t\twaitingChange(update.waiting);\n\t}, [&](SpeedEstimate update) {\n\t\tcheckForQualitySwitch(update);\n\t}, [](MutedByOther) {\n\t}, [&](Finished) {\n\t\tconst auto finishTrack = [](TrackState &state) {\n\t\t\tstate.position = state.receivedTill = state.duration;\n\t\t};\n\t\tfinishTrack(_info.audio.state);\n\t\tfinishTrack(_info.video.state);\n\t});\n}\n\nvoid Document::setOtherQualities(std::vector<QualityDescriptor> value) {\n\t_otherQualities = std::move(value);\n\tcheckForQualitySwitch(_lastSpeedEstimate);\n}\n\nvoid Document::checkForQualitySwitch(SpeedEstimate estimate) {\n\t_lastSpeedEstimate = estimate;\n\tif (!checkSwitchToHigherQuality()) {\n\t\tcheckSwitchToLowerQuality();\n\t}\n}\n\nbool Document::checkSwitchToHigherQuality() {\n\tif (_otherQualities.empty()\n\t\t|| (_info.video.state.duration == kTimeUnknown)\n\t\t|| (_info.video.state.duration == kDurationUnavailable)\n\t\t|| (_info.video.state.position == kTimeUnknown)\n\t\t|| (_info.video.state.receivedTill == kTimeUnknown)\n\t\t|| !_lastSpeedEstimate.bytesPerSecond\n\t\t|| _lastSpeedEstimate.unreliable\n\t\t|| (_info.video.state.receivedTill\n\t\t\t< std::min(\n\t\t\t\t_info.video.state.duration,\n\t\t\t\t(_info.video.state.position\n\t\t\t\t\t+ kSwitchQualityUpPreloadedThreshold)))) {\n\t\treturn false;\n\t}\n\tconst auto size = _player.fileSize();\n\tAssert(size >= 0 && size <= std::numeric_limits<uint32>::max());\n\tauto to = QualityDescriptor{ .sizeInBytes = uint32(size) };\n\tconst auto duration = _info.video.state.duration / 1000.;\n\tconst auto speed = _player.speed();\n\tconst auto multiplier = speed * kSwitchQualityUpSpeedMultiplier;\n\tfor (const auto &descriptor : _otherQualities) {\n\t\tconst auto perSecond = descriptor.sizeInBytes / duration;\n\t\tif (descriptor.sizeInBytes > to.sizeInBytes\n\t\t\t&& _lastSpeedEstimate.bytesPerSecond >= perSecond * multiplier) {\n\t\t\tto = descriptor;\n\t\t}\n\t}\n\tif (!to.height) {\n\t\treturn false;\n\t}\n\t_switchQualityRequests.fire_copy(to.height);\n\treturn true;\n}\n\nbool Document::checkSwitchToLowerQuality() {\n\tif (_otherQualities.empty()\n\t\t|| !_waiting\n\t\t|| !_radial.animating()\n\t\t|| !_lastSpeedEstimate.bytesPerSecond) {\n\t\treturn false;\n\t}\n\tconst auto size = _player.fileSize();\n\tAssert(size >= 0 && size <= std::numeric_limits<uint32>::max());\n\tauto to = QualityDescriptor();\n\tfor (const auto &descriptor : _otherQualities) {\n\t\tif (descriptor.sizeInBytes < size\n\t\t\t&& descriptor.sizeInBytes > to.sizeInBytes) {\n\t\t\tto = descriptor;\n\t\t}\n\t}\n\tif (!to.height) {\n\t\treturn false;\n\t}\n\t_switchQualityRequests.fire_copy(to.height);\n\treturn true;\n}\n\nvoid Document::handleError(Error &&error) {\n\tif (_document) {\n\t\tif (error == Error::NotStreamable) {\n\t\t\t_document->setNotSupportsStreaming();\n\t\t} else if (error == Error::OpenFailed) {\n\t\t\t_document->setInappPlaybackFailed();\n\t\t}\n\t} else if (_photo) {\n\t\tif (error == Error::NotStreamable || error == Error::OpenFailed) {\n\t\t\t_photo->setVideoPlaybackFailed();\n\t\t}\n\t}\n\twaitingChange(false);\n}\n\nvoid Document::ready(Information &&info) {\n\t_info = std::move(info);\n\tvalidateGoodThumbnail();\n\twaitingChange(false);\n}\n\nvoid Document::waitingChange(bool waiting) {\n\tif (_waiting == waiting) {\n\t\treturn;\n\t}\n\t_waiting = waiting;\n\tconst auto fade = [=](crl::time duration) {\n\t\tif (!_radial.animating()) {\n\t\t\t_radial.start(\n\t\t\t\tst::defaultInfiniteRadialAnimation.sineDuration);\n\t\t}\n\t\t_fading.start([=] {\n\t\t\twaitingCallback();\n\t\t}, _waiting ? 0. : 1., _waiting ? 1. : 0., duration);\n\n\t\tcheckSwitchToLowerQuality();\n\t};\n\tif (waiting) {\n\t\tif (_radial.animating()) {\n\t\t\t_timer.cancel();\n\t\t\tfade(kWaitingFastDuration);\n\t\t} else {\n\t\t\t_timer.callOnce(kWaitingShowDelay);\n\t\t\t_timer.setCallback([=] {\n\t\t\t\tfade(kWaitingShowDuration);\n\t\t\t});\n\t\t}\n\t} else {\n\t\t_timer.cancel();\n\t\tif (_radial.animating()) {\n\t\t\tfade(kWaitingFastDuration);\n\t\t}\n\t}\n}\n\nvoid Document::validateGoodThumbnail() {\n\tif (_info.video.cover.isNull()\n\t\t|| !_document\n\t\t|| _document->goodThumbnailChecked()) {\n\t\treturn;\n\t}\n\tconst auto sticker = (_document->sticker() != nullptr);\n\tconst auto document = _document;\n\tconst auto information = _info.video;\n\tconst auto key = document->goodThumbnailCacheKey();\n\tconst auto guard = base::make_weak(&document->session());\n\tdocument->owner().cache().get(key, [=](QByteArray value) {\n\t\tif (!value.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto image = [&] {\n\t\t\tauto result = information.cover;\n\t\t\tif (information.rotation != 0) {\n\t\t\t\tauto transform = QTransform();\n\t\t\t\ttransform.rotate(information.rotation);\n\t\t\t\tresult = result.transformed(transform);\n\t\t\t}\n\t\t\tif (result.size() != information.size) {\n\t\t\t\tresult = result.scaled(\n\t\t\t\t\tinformation.size,\n\t\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\t\tQt::SmoothTransformation);\n\t\t\t}\n\t\t\tif (!sticker && information.alpha) {\n\t\t\t\tresult = Images::Opaque(std::move(result));\n\t\t\t}\n\t\t\treturn result;\n\t\t}();\n\t\tauto bytes = QByteArray();\n\t\t{\n\t\t\tauto buffer = QBuffer(&bytes);\n\t\t\timage.save(&buffer, sticker ? \"WEBP\" : \"JPG\", kGoodThumbQuality);\n\t\t}\n\t\tconst auto length = bytes.size();\n\t\tif (!length || length > Storage::kMaxFileInMemory) {\n\t\t\tLOG((\"App Error: Bad thumbnail data for saving to cache.\"));\n\t\t\tbytes = \"(failed)\"_q;\n\t\t}\n\t\tcrl::on_main(guard, [=] {\n\t\t\tif (const auto active = document->activeMediaView()) {\n\t\t\t\tactive->setGoodThumbnail(image);\n\t\t\t}\n\t\t\tif (bytes != \"(failed)\"_q) {\n\t\t\t\tdocument->setGoodThumbnailChecked(true);\n\t\t\t}\n\t\t\tdocument->owner().cache().putIfEmpty(\n\t\t\t\tdocument->goodThumbnailCacheKey(),\n\t\t\t\tStorage::Cache::Database::TaggedValue(\n\t\t\t\t\tbase::duplicate(bytes),\n\t\t\t\t\tData::kImageCacheTag));\n\t\t});\n\t});\n}\n\nvoid Document::waitingCallback() {\n\tfor (const auto &instance : _instances) {\n\t\tinstance->callWaitingCallback();\n\t}\n}\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_document.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/streaming/media_streaming_player.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/effects/animations.h\"\n#include \"base/timer.h\"\n\nclass DocumentData;\n\nnamespace Media::Streaming {\n\nclass Instance;\nclass Loader;\n\nstruct QualityDescriptor {\n\tuint32 sizeInBytes = 0;\n\tuint32 height = 0;\n};\n\nclass Document {\npublic:\n\tDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tstd::shared_ptr<Reader> reader,\n\t\tstd::vector<QualityDescriptor> otherQualities = {});\n\tDocument(\n\t\tnot_null<PhotoData*> photo,\n\t\tstd::shared_ptr<Reader> reader,\n\t\tstd::vector<QualityDescriptor> otherQualities = {});\n\texplicit Document(std::unique_ptr<Loader> loader);\n\n\tvoid play(const PlaybackOptions &options);\n\tvoid saveFrameToCover();\n\n\t[[nodiscard]] Player &player();\n\t[[nodiscard]] const Player &player() const;\n\t[[nodiscard]] const Information &info() const;\n\n\t[[nodiscard]] bool waitingShown() const;\n\t[[nodiscard]] float64 waitingOpacity() const;\n\t[[nodiscard]] Ui::RadialState waitingState() const;\n\n\tvoid setOtherQualities(std::vector<QualityDescriptor> value);\n\t[[nodiscard]] rpl::producer<int> switchQualityRequests() const;\n\nprivate:\n\tDocument(\n\t\tstd::shared_ptr<Reader> reader,\n\t\tDocumentData *document,\n\t\tPhotoData *photo,\n\t\tstd::vector<QualityDescriptor> otherQualities);\n\n\tfriend class Instance;\n\n\tvoid registerInstance(not_null<Instance*> instance);\n\tvoid unregisterInstance(not_null<Instance*> instance);\n\tvoid refreshPlayerPriority();\n\n\tvoid waitingCallback();\n\tvoid checkForQualitySwitch(SpeedEstimate estimate);\n\tbool checkSwitchToHigherQuality();\n\tbool checkSwitchToLowerQuality();\n\n\tvoid handleUpdate(Update &&update);\n\tvoid handleError(Error &&error);\n\n\tvoid ready(Information &&info);\n\tvoid waitingChange(bool waiting);\n\n\tvoid validateGoodThumbnail();\n\tvoid resubscribe();\n\n\tDocumentData *_document = nullptr;\n\tPhotoData *_photo = nullptr;\n\tPlayer _player;\n\tInformation _info;\n\n\trpl::lifetime _subscription;\n\n\tmutable Ui::InfiniteRadialAnimation _radial;\n\tUi::Animations::Simple _fading;\n\tbase::Timer _timer;\n\tbase::flat_set<not_null<Instance*>> _instances;\n\tstd::vector<QualityDescriptor> _otherQualities;\n\trpl::event_stream<int> _switchQualityRequests;\n\tSpeedEstimate _lastSpeedEstimate;\n\tbool _waiting = false;\n\n};\n\n} // namespace Media::Streaming\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_file.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/streaming/media_streaming_file.h\"\n\n#include \"media/streaming/media_streaming_loader.h\"\n#include \"media/streaming/media_streaming_file_delegate.h\"\n#include \"ffmpeg/ffmpeg_utility.h\"\n\nnamespace Media {\nnamespace Streaming {\nnamespace {\n\nconstexpr auto kMaxSingleReadAmount = 8 * 1024 * 1024;\nconstexpr auto kMaxQueuedPackets = 1024;\n\n[[nodiscard]] bool UnreliableFormatDuration(\n\t\tnot_null<AVFormatContext*> format,\n\t\tnot_null<AVStream*> stream,\n\t\tMode mode) {\n\treturn (mode == Mode::Video || mode == Mode::Inspection)\n\t\t&& stream->codecpar\n\t\t&& (stream->codecpar->codec_id == AV_CODEC_ID_VP9)\n\t\t&& format->iformat\n\t\t&& format->iformat->name\n\t\t&& QString::fromLatin1(\n\t\t\tformat->iformat->name\n\t\t).split(QChar(',')).contains(u\"webm\");\n}\n\n} // namespace\n\nFile::Context::Context(\n\tnot_null<FileDelegate*> delegate,\n\tnot_null<Reader*> reader)\n: _delegate(delegate)\n, _reader(reader)\n, _size(reader->size()) {\n}\n\nFile::Context::~Context() = default;\n\nint File::Context::Read(void *opaque, uint8_t *buffer, int bufferSize) {\n\treturn static_cast<Context*>(opaque)->read(\n\t\tbytes::make_span(buffer, bufferSize));\n}\n\nint64_t File::Context::Seek(void *opaque, int64_t offset, int whence) {\n\treturn static_cast<Context*>(opaque)->seek(offset, whence);\n}\n\nint File::Context::read(bytes::span buffer) {\n\tExpects(_size >= _offset);\n\n\tconst auto amount = std::min(_size - _offset, int64(buffer.size()));\n\n\tif (unroll()) {\n\t\treturn AVERROR_EXTERNAL;\n\t} else if (amount > kMaxSingleReadAmount) {\n\t\tLOG((\"Streaming Error: Read callback asked for too much data: %1\"\n\t\t\t).arg(amount));\n\t\treturn AVERROR_EXTERNAL;\n\t} else if (!amount) {\n\t\treturn AVERROR_EOF;\n\t}\n\n\tbuffer = buffer.subspan(0, amount);\n\twhile (true) {\n\t\tconst auto result = _reader->fill(_offset, buffer, &_semaphore);\n\t\tif (result == Reader::FillState::Success) {\n\t\t\tbreak;\n\t\t} else if (result == Reader::FillState::WaitingRemote) {\n\t\t\t// Perhaps for the correct sleeping in case of enough packets\n\t\t\t// being read already we require SleepPolicy::Allowed here.\n\t\t\t// Otherwise if we wait for the remote frequently and\n\t\t\t// _queuedPackets never get to kMaxQueuedPackets and we don't call\n\t\t\t// processQueuedPackets(SleepPolicy::Allowed) ever.\n\t\t\t//\n\t\t\t// But right now we can't simply pass SleepPolicy::Allowed here,\n\t\t\t// it freezes because of two _semaphore.acquire one after another.\n\t\t\tprocessQueuedPackets(SleepPolicy::Disallowed);\n\t\t\t_delegate->fileWaitingForData();\n\t\t}\n\t\t_semaphore.acquire();\n\t\tif (_interrupted) {\n\t\t\treturn AVERROR_EXTERNAL;\n\t\t} else if (const auto error = _reader->streamingError()) {\n\t\t\tfail(*error);\n\t\t\treturn AVERROR_EXTERNAL;\n\t\t}\n\t}\n\n\tsendFullInCache();\n\n\t_offset += amount;\n\treturn amount;\n}\n\nint64_t File::Context::seek(int64_t offset, int whence) {\n\tconst auto checkedSeek = [&](int64_t offset) {\n\t\tif (_failed || offset < 0 || offset > _size) {\n\t\t\treturn int64(-1);\n\t\t}\n\t\treturn (_offset = offset);\n\t};\n\tswitch (whence) {\n\tcase SEEK_SET: return checkedSeek(offset);\n\tcase SEEK_CUR: return checkedSeek(_offset + offset);\n\tcase SEEK_END: return checkedSeek(_size + offset);\n\tcase AVSEEK_SIZE: return _size;\n\t}\n\treturn -1;\n}\n\nvoid File::Context::logError(QLatin1String method) {\n\tif (!unroll()) {\n\t\tFFmpeg::LogError(method);\n\t}\n}\n\nvoid File::Context::logError(\n\t\tQLatin1String method,\n\t\tFFmpeg::AvErrorWrap error) {\n\tif (!unroll()) {\n\t\tFFmpeg::LogError(method, error);\n\t}\n}\n\nvoid File::Context::logFatal(QLatin1String method) {\n\tif (!unroll()) {\n\t\tFFmpeg::LogError(method);\n\t\tfail(_format ? Error::InvalidData : Error::OpenFailed);\n\t}\n}\n\nvoid File::Context::logFatal(\n\t\tQLatin1String method,\n\t\tFFmpeg::AvErrorWrap error) {\n\tif (!unroll()) {\n\t\tFFmpeg::LogError(method, error);\n\t\tfail(_format ? Error::InvalidData : Error::OpenFailed);\n\t}\n}\n\nStream File::Context::initStream(\n\t\tnot_null<AVFormatContext*> format,\n\t\tAVMediaType type,\n\t\tMode mode,\n\t\tStartOptions options) {\n\tauto result = Stream();\n\tconst auto index = result.index = av_find_best_stream(\n\t\tformat,\n\t\ttype,\n\t\t-1,\n\t\t-1,\n\t\tnullptr,\n\t\t0);\n\tif (index < 0) {\n\t\treturn {};\n\t}\n\n\tconst auto info = format->streams[index];\n\tif (type == AVMEDIA_TYPE_VIDEO) {\n\t\tif (info->disposition & AV_DISPOSITION_ATTACHED_PIC) {\n\t\t\t// ignore cover streams\n\t\t\treturn Stream();\n\t\t}\n\t\tresult.codec = FFmpeg::MakeCodecPointer({\n\t\t\t.stream = info,\n\t\t\t.hwAllowed = options.hwAllow,\n\t\t});\n\t\tif (!result.codec) {\n\t\t\treturn result;\n\t\t}\n\t\tresult.rotation = FFmpeg::ReadRotationFromMetadata(info);\n\t\tresult.aspect = FFmpeg::ValidateAspectRatio(\n\t\t\tinfo->sample_aspect_ratio);\n\t\tconst auto guessed = av_guess_frame_rate(format, info, nullptr);\n\t\tif (guessed.num > 0 && guessed.den > 0) {\n\t\t\tresult.fps = av_q2d(guessed);\n\t\t}\n\t} else if (type == AVMEDIA_TYPE_AUDIO) {\n\t\tresult.frequency = info->codecpar->sample_rate;\n\t\tif (!result.frequency) {\n\t\t\treturn result;\n\t\t}\n\t\tresult.codec = FFmpeg::MakeCodecPointer({ .stream = info });\n\t\tif (!result.codec) {\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tresult.decodedFrame = FFmpeg::MakeFramePointer();\n\tif (!result.decodedFrame) {\n\t\tresult.codec = nullptr;\n\t\treturn result;\n\t}\n\tresult.timeBase = info->time_base;\n\tresult.duration = options.durationOverride\n\t\t? options.durationOverride\n\t\t: (info->duration != AV_NOPTS_VALUE)\n\t\t? FFmpeg::PtsToTime(info->duration, result.timeBase)\n\t\t: UnreliableFormatDuration(format, info, mode)\n\t\t? kTimeUnknown\n\t\t: FFmpeg::PtsToTime(format->duration, FFmpeg::kUniversalTimeBase);\n\tif (result.duration == kTimeUnknown) {\n\t\tresult.duration = kDurationUnavailable;\n\t} else if (result.duration <= 0) {\n\t\tresult.codec = nullptr;\n\t} else {\n\t\t++result.duration;\n\t\tif (result.duration > kDurationMax) {\n\t\t\tresult.duration = 0;\n\t\t\tresult.codec = nullptr;\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid File::Context::seekToPosition(\n\t\tnot_null<AVFormatContext*> format,\n\t\tconst Stream &stream,\n\t\tcrl::time position) {\n\tauto error = FFmpeg::AvErrorWrap();\n\n\tif (!position) {\n\t\treturn;\n\t} else if (stream.duration == kDurationUnavailable) {\n\t\t// Seek in files with unknown duration is not supported.\n\t\treturn;\n\t}\n\t//\n\t// Non backward search reads the whole file if the position is after\n\t// the last keyframe inside the index. So we search only backward.\n\t//\n\t//const auto seekFlags = 0;\n\t//error = av_seek_frame(\n\t//\tformat,\n\t//\tstreamIndex,\n\t//\tTimeToPts(position, kUniversalTimeBase),\n\t//\tseekFlags);\n\t//if (!error) {\n\t//\treturn;\n\t//}\n\t//\n\terror = av_seek_frame(\n\t\tformat,\n\t\tstream.index,\n\t\tFFmpeg::TimeToPts(\n\t\t\tstd::clamp(position, crl::time(0), stream.duration - 1),\n\t\t\tstream.timeBase),\n\t\tAVSEEK_FLAG_BACKWARD);\n\tif (!error) {\n\t\treturn;\n\t}\n\treturn logFatal(qstr(\"av_seek_frame\"), error);\n}\n\nstd::variant<FFmpeg::Packet, FFmpeg::AvErrorWrap> File::Context::readPacket() {\n\tauto error = FFmpeg::AvErrorWrap();\n\n\tauto result = FFmpeg::Packet();\n\terror = av_read_frame(_format.get(), &result.fields());\n\tif (unroll()) {\n\t\treturn FFmpeg::AvErrorWrap();\n\t} else if (!error) {\n\t\treturn result;\n\t} else if (error.code() != AVERROR_EOF) {\n\t\tlogFatal(qstr(\"av_read_frame\"), error);\n\t}\n\treturn error;\n}\n\nvoid File::Context::start(StartOptions options) {\n\tExpects(options.seekable || !options.position);\n\n\tauto error = FFmpeg::AvErrorWrap();\n\n\tif (unroll()) {\n\t\treturn;\n\t}\n\tauto format = FFmpeg::MakeFormatPointer(\n\t\tstatic_cast<void*>(this),\n\t\t&Context::Read,\n\t\tnullptr,\n\t\toptions.seekable ? &Context::Seek : nullptr);\n\tif (!format) {\n\t\treturn fail(Error::OpenFailed);\n\t}\n\n\tif ((error = avformat_find_stream_info(format.get(), nullptr))) {\n\t\treturn logFatal(qstr(\"avformat_find_stream_info\"), error);\n\t}\n\n\tconst auto mode = _delegate->fileOpenMode();\n\tauto video = initStream(\n\t\tformat.get(),\n\t\tAVMEDIA_TYPE_VIDEO,\n\t\tmode,\n\t\toptions);\n\tif (unroll()) {\n\t\treturn;\n\t}\n\n\tauto audio = initStream(\n\t\tformat.get(),\n\t\tAVMEDIA_TYPE_AUDIO,\n\t\tmode,\n\t\toptions);\n\tif (unroll()) {\n\t\treturn;\n\t}\n\n\t_reader->headerDone();\n\tif (_reader->isRemoteLoader()) {\n\t\tsendFullInCache(true);\n\t}\n\tif (options.seekable && (video.codec || audio.codec)) {\n\t\tseekToPosition(\n\t\t\tformat.get(),\n\t\t\tvideo.codec ? video : audio,\n\t\t\toptions.position);\n\t}\n\tif (unroll()) {\n\t\treturn;\n\t}\n\n\tif (video.codec) {\n\t\t_queuedPackets[video.index].reserve(kMaxQueuedPackets);\n\t}\n\tif (audio.codec) {\n\t\t_queuedPackets[audio.index].reserve(kMaxQueuedPackets);\n\t}\n\n\tconst auto header = _reader->headerSize();\n\tif (!_delegate->fileReady(header, std::move(video), std::move(audio))) {\n\t\treturn fail(Error::OpenFailed);\n\t}\n\t_format = std::move(format);\n}\n\nvoid File::Context::sendFullInCache(bool force) {\n\tconst auto started = _fullInCache.has_value();\n\tif (force || started) {\n\t\tconst auto nowFullInCache = _reader->fullInCache();\n\t\tif (!started || *_fullInCache != nowFullInCache) {\n\t\t\t_fullInCache = nowFullInCache;\n\t\t\t_delegate->fileFullInCache(nowFullInCache);\n\t\t}\n\t}\n}\n\nvoid File::Context::readNextPacket() {\n\tauto result = readPacket();\n\tif (unroll()) {\n\t\treturn;\n\t} else if (const auto packet = std::get_if<FFmpeg::Packet>(&result)) {\n\t\tconst auto index = packet->fields().stream_index;\n\t\tconst auto i = _queuedPackets.find(index);\n\t\tif (i == end(_queuedPackets)) {\n\t\t\treturn;\n\t\t}\n\t\ti->second.push_back(std::move(*packet));\n\t\tif (i->second.size() == kMaxQueuedPackets) {\n\t\t\tprocessQueuedPackets(SleepPolicy::Allowed);\n\t\t}\n\t\tAssert(i->second.size() < kMaxQueuedPackets);\n\t} else {\n\t\t// Still trying to read by drain.\n\t\tAssert(v::is<FFmpeg::AvErrorWrap>(result));\n\t\tAssert(v::get<FFmpeg::AvErrorWrap>(result).code() == AVERROR_EOF);\n\t\tprocessQueuedPackets(SleepPolicy::Allowed);\n\t\tif (!finished()) {\n\t\t\thandleEndOfFile();\n\t\t}\n\t}\n}\n\nvoid File::Context::handleEndOfFile() {\n\t_delegate->fileProcessEndOfFile();\n\tif (_delegate->fileReadMore()) {\n\t\t_readTillEnd = false;\n\t\tauto error = FFmpeg::AvErrorWrap(av_seek_frame(\n\t\t\t_format.get(),\n\t\t\t-1, // stream_index\n\t\t\t0, // timestamp\n\t\t\tAVSEEK_FLAG_BACKWARD));\n\t\tif (error) {\n\t\t\tlogFatal(qstr(\"av_seek_frame\"));\n\t\t}\n\n\t\t// If we loaded a file till the end then we think it is fully cached,\n\t\t// assume we finished loading and don't want to keep all other\n\t\t// download tasks throttled because of an active streaming.\n\t\t_reader->tryRemoveLoaderAsync();\n\t} else {\n\t\t_readTillEnd = true;\n\t}\n}\n\nvoid File::Context::processQueuedPackets(SleepPolicy policy) {\n\tconst auto more = _delegate->fileProcessPackets(_queuedPackets);\n\tif (!more && policy == SleepPolicy::Allowed) {\n\t\tdo {\n\t\t\t_reader->startSleep(&_semaphore);\n\t\t\t_semaphore.acquire();\n\t\t\t_reader->stopSleep();\n\t\t} while (!unroll() && !_delegate->fileReadMore());\n\t}\n}\n\nvoid File::Context::interrupt() {\n\t_interrupted = true;\n\t_semaphore.release();\n}\n\nvoid File::Context::wake() {\n\t_semaphore.release();\n}\n\nbool File::Context::interrupted() const {\n\treturn _interrupted;\n}\n\nbool File::Context::failed() const {\n\treturn _failed;\n}\n\nbool File::Context::unroll() const {\n\treturn failed() || interrupted();\n}\n\nvoid File::Context::fail(Error error) {\n\t_failed = true;\n\t_delegate->fileError(error);\n}\n\nbool File::Context::finished() const {\n\treturn unroll() || _readTillEnd;\n}\n\nvoid File::Context::stopStreamingAsync() {\n\t// If we finished loading we don't want to keep all other\n\t// download tasks throttled because of an active streaming.\n\t_reader->stopStreamingAsync();\n}\n\nFile::File(std::shared_ptr<Reader> reader)\n: _reader(std::move(reader)) {\n}\n\nvoid File::start(not_null<FileDelegate*> delegate, StartOptions options) {\n\tstop(true);\n\n\t_reader->startStreaming();\n\t_context.emplace(delegate, _reader.get());\n\n\t_thread = std::thread([=, context = &*_context] {\n\t\tcrl::toggle_fp_exceptions(true);\n\t\tcontext->start(options);\n\t\twhile (!context->finished()) {\n\t\t\tcontext->readNextPacket();\n\t\t}\n\t\tif (!context->interrupted()) {\n\t\t\tcontext->stopStreamingAsync();\n\t\t}\n\t});\n}\n\nvoid File::wake() {\n\tExpects(_context.has_value());\n\n\t_context->wake();\n}\n\nvoid File::stop(bool stillActive) {\n\tif (_thread.joinable()) {\n\t\t_context->interrupt();\n\t\t_thread.join();\n\t}\n\t_reader->stopStreaming(stillActive);\n\t_context.reset();\n}\n\nbool File::isRemoteLoader() const {\n\treturn _reader->isRemoteLoader();\n}\n\nvoid File::setLoaderPriority(int priority) {\n\t_reader->setLoaderPriority(priority);\n}\n\nint64 File::size() const {\n\treturn _reader->size();\n}\n\nrpl::producer<SpeedEstimate> File::speedEstimate() const {\n\treturn _reader->speedEstimate();\n}\n\nFile::~File() {\n\tstop();\n}\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_file.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/streaming/media_streaming_common.h\"\n#include \"media/streaming/media_streaming_utility.h\"\n#include \"media/streaming/media_streaming_reader.h\"\n#include \"ffmpeg/ffmpeg_utility.h\"\n#include \"base/bytes.h\"\n#include \"base/weak_ptr.h\"\n\n#include <thread>\n\nnamespace Media {\nnamespace Streaming {\n\nclass FileDelegate;\n\nstruct StartOptions {\n\tcrl::time position = 0;\n\tcrl::time durationOverride = 0;\n\tbool seekable = true;\n\tbool hwAllow = false;\n};\n\nclass File final {\npublic:\n\texplicit File(std::shared_ptr<Reader> reader);\n\n\tFile(const File &other) = delete;\n\tFile &operator=(const File &other) = delete;\n\n\tvoid start(not_null<FileDelegate*> delegate, StartOptions options);\n\tvoid wake();\n\tvoid stop(bool stillActive = false);\n\n\t[[nodiscard]] bool isRemoteLoader() const;\n\tvoid setLoaderPriority(int priority);\n\n\t[[nodiscard]] int64 size() const;\n\t[[nodiscard]] rpl::producer<SpeedEstimate> speedEstimate() const;\n\n\t~File();\n\nprivate:\n\tclass Context final : public base::has_weak_ptr {\n\tpublic:\n\t\tContext(not_null<FileDelegate*> delegate, not_null<Reader*> reader);\n\t\t~Context();\n\n\t\tvoid start(StartOptions options);\n\t\tvoid readNextPacket();\n\n\t\tvoid interrupt();\n\t\tvoid wake();\n\t\t[[nodiscard]] bool interrupted() const;\n\t\t[[nodiscard]] bool failed() const;\n\t\t[[nodiscard]] bool finished() const;\n\n\t\tvoid stopStreamingAsync();\n\n\tprivate:\n\t\tenum class SleepPolicy {\n\t\t\tAllowed,\n\t\t\tDisallowed,\n\t\t};\n\t\tstatic int Read(void *opaque, uint8_t *buffer, int bufferSize);\n\t\tstatic int64_t Seek(void *opaque, int64_t offset, int whence);\n\n\t\t[[nodiscard]] int read(bytes::span buffer);\n\t\t[[nodiscard]] int64_t seek(int64_t offset, int whence);\n\n\t\t[[nodiscard]] bool unroll() const;\n\t\tvoid logError(QLatin1String method);\n\t\tvoid logError(QLatin1String method, FFmpeg::AvErrorWrap error);\n\t\tvoid logFatal(QLatin1String method);\n\t\tvoid logFatal(QLatin1String method, FFmpeg::AvErrorWrap error);\n\t\tvoid fail(Error error);\n\n\t\t[[nodiscard]] Stream initStream(\n\t\t\tnot_null<AVFormatContext *> format,\n\t\t\tAVMediaType type,\n\t\t\tMode mode,\n\t\t\tStartOptions options);\n\t\tvoid seekToPosition(\n\t\t\tnot_null<AVFormatContext *> format,\n\t\t\tconst Stream &stream,\n\t\t\tcrl::time position);\n\n\t\t// TODO base::expected.\n\t\t[[nodiscard]] auto readPacket()\n\t\t-> std::variant<FFmpeg::Packet, FFmpeg::AvErrorWrap>;\n\t\tvoid processQueuedPackets(SleepPolicy policy);\n\n\t\tvoid handleEndOfFile();\n\t\tvoid sendFullInCache(bool force = false);\n\n\t\tconst not_null<FileDelegate*> _delegate;\n\t\tconst not_null<Reader*> _reader;\n\n\t\tbase::flat_map<int, std::vector<FFmpeg::Packet>> _queuedPackets;\n\t\tint64 _offset = 0;\n\t\tint64 _size = 0;\n\t\tbool _failed = false;\n\t\tbool _readTillEnd = false;\n\t\tstd::optional<bool> _fullInCache;\n\t\tcrl::semaphore _semaphore;\n\t\tstd::atomic<bool> _interrupted = false;\n\n\t\tFFmpeg::FormatPointer _format;\n\n\t};\n\n\tstd::optional<Context> _context;\n\tstd::shared_ptr<Reader> _reader;\n\tstd::thread _thread;\n\n};\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_file_delegate.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace FFmpeg {\nclass Packet;\n} // namespace FFmpeg\n\nnamespace Media {\nnamespace Streaming {\n\nstruct Stream;\nenum class Error;\n\nclass FileDelegate {\npublic:\n\t[[nodiscard]] virtual Mode fileOpenMode() = 0;\n\t[[nodiscard]] virtual bool fileReady(\n\t\tint headerSize,\n\t\tStream &&video,\n\t\tStream &&audio) = 0;\n\tvirtual void fileError(Error error) = 0;\n\tvirtual void fileWaitingForData() = 0;\n\tvirtual void fileFullInCache(bool fullInCache) = 0;\n\n\tvirtual void fileProcessEndOfFile() = 0;\n\t// Return true if reading and processing more packets is desired.\n\t// Return false if sleeping until 'wake()' is called is desired.\n\t[[nodiscard]] virtual bool fileProcessPackets(\n\t\tbase::flat_map<int, std::vector<FFmpeg::Packet>> &packets) = 0;\n\t// Also returns true after fileProcessEndOfFile() if looping is desired.\n\t[[nodiscard]] virtual bool fileReadMore() = 0;\n};\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_instance.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/streaming/media_streaming_instance.h\"\n\n#include \"media/streaming/media_streaming_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_streaming.h\"\n\nnamespace Media {\nnamespace Streaming {\n\nInstance::Instance(const Instance &other)\n: _shared(other._shared)\n, _waitingCallback(other._waitingCallback)\n, _priority(other._priority)\n, _playerLocked(other._playerLocked) {\n\tif (_shared) {\n\t\t_shared->registerInstance(this);\n\t\tif (_playerLocked) {\n\t\t\t_shared->player().lock();\n\t\t}\n\t}\n}\n\nInstance::Instance(\n\tstd::shared_ptr<Document> shared,\n\tFn<void()> waitingCallback)\n: _shared(std::move(shared))\n, _waitingCallback(std::move(waitingCallback)) {\n\tif (_shared) {\n\t\t_shared->registerInstance(this);\n\t}\n}\n\nInstance::Instance(\n\tnot_null<DocumentData*> document,\n\tData::FileOrigin origin,\n\tFn<void()> waitingCallback)\n: Instance(\n\tdocument->owner().streaming().sharedDocument(document, origin),\n\tstd::move(waitingCallback)) {\n}\n\nInstance::Instance(\n\tnot_null<DocumentData*> quality,\n\tnot_null<DocumentData*> original,\n\tHistoryItem *context,\n\tData::FileOrigin origin,\n\tFn<void()> waitingCallback)\n: Instance(\n\tquality->owner().streaming().sharedDocument(\n\t\tquality,\n\t\toriginal,\n\t\tcontext,\n\t\torigin),\n\tstd::move(waitingCallback)) {\n}\n\nInstance::Instance(\n\tnot_null<PhotoData*> photo,\n\tData::FileOrigin origin,\n\tFn<void()> waitingCallback)\n: Instance(\n\tphoto->owner().streaming().sharedDocument(photo, origin),\n\tstd::move(waitingCallback)) {\n}\n\nInstance::~Instance() {\n\tif (_shared) {\n\t\tunlockPlayer();\n\t\t_shared->unregisterInstance(this);\n\t}\n}\n\nbool Instance::valid() const {\n\treturn (_shared != nullptr);\n}\n\nstd::shared_ptr<Document> Instance::shared() const {\n\treturn _shared;\n}\n\nconst Player &Instance::player() const {\n\tExpects(_shared != nullptr);\n\n\treturn _shared->player();\n}\n\nconst Information &Instance::info() const {\n\tExpects(_shared != nullptr);\n\n\treturn _shared->info();\n}\n\nrpl::producer<int> Instance::switchQualityRequests() const {\n\treturn _shared->switchQualityRequests();\n}\n\nvoid Instance::play(const PlaybackOptions &options) {\n\tExpects(_shared != nullptr);\n\n\t_shared->play(options);\n}\n\nvoid Instance::pause() {\n\tExpects(_shared != nullptr);\n\n\t_shared->player().pause();\n}\n\nvoid Instance::resume() {\n\tExpects(_shared != nullptr);\n\n\t_shared->player().resume();\n}\n\nvoid Instance::stop() {\n\tExpects(_shared != nullptr);\n\n\t_shared->player().stop();\n}\n\nvoid Instance::stopAudio() {\n\tExpects(_shared != nullptr);\n\n\t_shared->player().stopAudio();\n}\n\nvoid Instance::saveFrameToCover() {\n\tExpects(_shared != nullptr);\n\n\t_shared->saveFrameToCover();\n}\n\nbool Instance::active() const {\n\tExpects(_shared != nullptr);\n\n\treturn _shared->player().active();\n}\n\nbool Instance::ready() const {\n\tExpects(_shared != nullptr);\n\n\treturn _shared->player().ready();\n}\n\nstd::optional<Error> Instance::failed() const {\n\tExpects(_shared != nullptr);\n\n\treturn _shared->player().failed();\n}\n\nbool Instance::paused() const {\n\tExpects(_shared != nullptr);\n\n\treturn _shared->player().paused();\n}\n\nfloat64 Instance::speed() const {\n\tExpects(_shared != nullptr);\n\n\treturn _shared->player().speed();\n}\n\nvoid Instance::setSpeed(float64 speed) {\n\tExpects(_shared != nullptr);\n\n\t_shared->player().setSpeed(speed);\n}\n\nbool Instance::waitingShown() const {\n\tExpects(_shared != nullptr);\n\n\treturn _shared->waitingShown();\n}\n\nfloat64 Instance::waitingOpacity() const {\n\tExpects(_shared != nullptr);\n\n\treturn _shared->waitingOpacity();\n}\n\nUi::RadialState Instance::waitingState() const {\n\tExpects(_shared != nullptr);\n\n\treturn _shared->waitingState();\n}\n\nvoid Instance::callWaitingCallback() {\n\tif (_waitingCallback) {\n\t\t_waitingCallback();\n\t}\n}\n\nQImage Instance::frame(const FrameRequest &request) const {\n\treturn player().frame(request, this);\n}\n\nFrameWithInfo Instance::frameWithInfo(const FrameRequest &request) const {\n\treturn player().frameWithInfo(request, this);\n}\n\nFrameWithInfo Instance::frameWithInfo() const {\n\treturn player().frameWithInfo(this);\n}\n\nbool Instance::markFrameShown() const {\n\tExpects(_shared != nullptr);\n\n\treturn _shared->player().markFrameShown();\n}\n\nvoid Instance::lockPlayer() {\n\tExpects(_shared != nullptr);\n\n\tif (!_playerLocked) {\n\t\t_playerLocked = true;\n\t\t_shared->player().lock();\n\t}\n}\n\nvoid Instance::unlockPlayer() {\n\tExpects(_shared != nullptr);\n\n\tif (_playerLocked) {\n\t\t_playerLocked = false;\n\t\t_shared->player().unlock();\n\t}\n}\n\nbool Instance::playerLocked() const {\n\tExpects(_shared != nullptr);\n\n\treturn _shared->player().locked();\n}\n\nvoid Instance::setPriority(int priority) {\n\tExpects(_shared != nullptr);\n\n\tif (_priority == priority) {\n\t\treturn;\n\t}\n\t_priority = priority;\n\t_shared->refreshPlayerPriority();\n}\n\nint Instance::priority() const {\n\treturn _priority;\n}\n\nrpl::lifetime &Instance::lifetime() {\n\treturn _lifetime;\n}\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_instance.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/streaming/media_streaming_common.h\"\n\nclass DocumentData;\n\nnamespace Ui {\nstruct RadialState;\n} // namespace Ui\n\nnamespace Data {\nstruct FileOrigin;\n} // namespace Data\n\nnamespace Media {\nnamespace Streaming {\n\nclass Document;\nclass Player;\n\nclass Instance {\npublic:\n\tInstance(const Instance &other);\n\tInstance(\n\t\tstd::shared_ptr<Document> shared,\n\t\tFn<void()> waitingCallback);\n\tInstance(\n\t\tnot_null<DocumentData*> document,\n\t\tData::FileOrigin origin,\n\t\tFn<void()> waitingCallback);\n\tInstance(\n\t\tnot_null<DocumentData*> quality,\n\t\tnot_null<DocumentData*> original,\n\t\tHistoryItem *context,\n\t\tData::FileOrigin origin,\n\t\tFn<void()> waitingCallback);\n\tInstance(\n\t\tnot_null<PhotoData*> photo,\n\t\tData::FileOrigin origin,\n\t\tFn<void()> waitingCallback);\n\t~Instance();\n\n\t[[nodiscard]] bool valid() const;\n\t[[nodiscard]] std::shared_ptr<Document> shared() const;\n\n\t[[nodiscard]] const Player &player() const;\n\t[[nodiscard]] const Information &info() const;\n\t[[nodiscard]] rpl::producer<int> switchQualityRequests() const;\n\n\tvoid play(const PlaybackOptions &options);\n\tvoid pause();\n\tvoid resume();\n\tvoid stop();\n\tvoid stopAudio();\n\tvoid saveFrameToCover();\n\n\t[[nodiscard]] bool active() const;\n\t[[nodiscard]] bool ready() const;\n\t[[nodiscard]] std::optional<Error> failed() const;\n\n\t[[nodiscard]] bool paused() const;\n\n\t[[nodiscard]] float64 speed() const;\n\tvoid setSpeed(float64 speed);\n\n\t[[nodiscard]] bool waitingShown() const;\n\t[[nodiscard]] float64 waitingOpacity() const;\n\t[[nodiscard]] Ui::RadialState waitingState() const;\n\n\tvoid callWaitingCallback();\n\n\t[[nodiscard]] QImage frame(const FrameRequest &request) const;\n\t[[nodiscard]] FrameWithInfo frameWithInfo(\n\t\tconst FrameRequest &request) const;\n\t[[nodiscard]] FrameWithInfo frameWithInfo() const;\n\tbool markFrameShown() const;\n\n\tvoid lockPlayer();\n\tvoid unlockPlayer();\n\t[[nodiscard]] bool playerLocked() const;\n\n\tvoid setPriority(int priority);\n\t[[nodiscard]] int priority() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tconst std::shared_ptr<Document> _shared;\n\tFn<void()> _waitingCallback;\n\tint _priority = 1;\n\tbool _playerLocked = false;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_loader.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/streaming/media_streaming_loader.h\"\n\nnamespace Media {\nnamespace Streaming {\n\nbool LoadedPart::valid(int64 size) const {\n\treturn (offset != kFailedOffset)\n\t\t&& ((bytes.size() == Loader::kPartSize)\n\t\t\t|| (offset + bytes.size() == size));\n}\n\nbool operator<(\n\t\tconst PriorityQueue::Entry &a,\n\t\tconst PriorityQueue::Entry &b) {\n\tif (a.priority > b.priority) {\n\t\treturn true;\n\t} else if (a.priority < b.priority) {\n\t\treturn false;\n\t} else {\n\t\treturn a.value < b.value;\n\t}\n}\n\nbool PriorityQueue::add(int64 value) {\n\tconst auto i = ranges::find(_data, value, &Entry::value);\n\tif (i == end(_data)) {\n\t\t_data.insert({ value, _priority });\n\t\treturn true;\n\t} else if (i->priority != _priority) {\n\t\t_data.erase(i);\n\t\t_data.insert({ value, _priority });\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool PriorityQueue::remove(int64 value) {\n\tconst auto i = ranges::find(_data, value, &Entry::value);\n\tif (i == end(_data)) {\n\t\treturn false;\n\t}\n\t_data.erase(i);\n\treturn true;\n}\n\nbool PriorityQueue::empty() const {\n\treturn _data.empty();\n}\n\nstd::optional<int64> PriorityQueue::front() const {\n\treturn _data.empty()\n\t\t? std::nullopt\n\t\t: std::make_optional(_data.front().value);\n}\n\nstd::optional<int64> PriorityQueue::take() {\n\tif (_data.empty()) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto result = _data.front().value;\n\t_data.erase(_data.begin());\n\treturn result;\n}\n\nbase::flat_set<int64> PriorityQueue::takeInRange(int64 from, int64 till) {\n\tauto result = base::flat_set<int64>();\n\tfor (auto i = _data.begin(); i != _data.end();) {\n\t\tif (i->value >= from && i->value < till) {\n\t\t\tresult.emplace(i->value);\n\t\t\ti = _data.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid PriorityQueue::clear() {\n\t_data.clear();\n}\n\nvoid PriorityQueue::resetPriorities() {\n\t++_priority;\n}\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_loader.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/streaming/media_streaming_common.h\"\n\nnamespace Storage {\nclass StreamedFileDownloader;\n} // namespace Storage\n\nnamespace Media::Streaming {\n\nstruct LoadedPart {\n\tint64 offset = 0;\n\tQByteArray bytes;\n\n\tstatic constexpr auto kFailedOffset = int64(-1);\n\n\t[[nodiscard]] bool valid(int64 size) const;\n};\n\nclass Loader {\npublic:\n\tstatic constexpr auto kPartSize = int64(128 * 1024);\n\n\t[[nodiscard]] virtual Storage::Cache::Key baseCacheKey() const = 0;\n\t[[nodiscard]] virtual int64 size() const = 0;\n\n\tvirtual void load(int64 offset) = 0;\n\tvirtual void cancel(int64 offset) = 0;\n\tvirtual void resetPriorities() = 0;\n\tvirtual void setPriority(int priority) = 0;\n\tvirtual void stop() = 0;\n\n\t// Remove from queue if no requests are in progress.\n\tvirtual void tryRemoveFromQueue() = 0;\n\n\t// Parts will be sent from the main thread.\n\t[[nodiscard]] virtual rpl::producer<LoadedPart> parts() const = 0;\n\t[[nodiscard]] virtual auto speedEstimate() const\n\t\t-> rpl::producer<SpeedEstimate> = 0;\n\n\tvirtual void attachDownloader(\n\t\tnot_null<Storage::StreamedFileDownloader*> downloader) = 0;\n\tvirtual void clearAttachedDownloader() = 0;\n\n\tvirtual ~Loader() = default;\n\n};\n\nclass PriorityQueue {\npublic:\n\tbool add(int64 value);\n\tbool remove(int64 value);\n\tvoid resetPriorities();\n\t[[nodiscard]] bool empty() const;\n\t[[nodiscard]] std::optional<int64> front() const;\n\t[[nodiscard]] std::optional<int64> take();\n\t[[nodiscard]] base::flat_set<int64> takeInRange(int64 from, int64 till);\n\tvoid clear();\n\nprivate:\n\tstruct Entry {\n\t\tint64 value = 0;\n\t\tint priority = 0;\n\t};\n\n\tfriend bool operator<(const Entry &a, const Entry &b);\n\n\tbase::flat_set<Entry> _data;\n\tint _priority = 0;\n\n};\n\n} // namespace Media::Streaming\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_loader_local.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/streaming/media_streaming_loader_local.h\"\n\n#include \"storage/cache/storage_cache_types.h\"\n\n#include <QtCore/QBuffer>\n\nnamespace Media {\nnamespace Streaming {\nnamespace {\n\n// This is the maximum file size in Telegram API.\nconstexpr auto kMaxFileSize = 8000 * int64(512 * 1024);\n\n[[nodiscard]] int64 ValidateLocalSize(int64 size) {\n\treturn (size > 0 && size <= kMaxFileSize) ? size : 0;\n}\n\n} // namespace\n\nLoaderLocal::LoaderLocal(std::unique_ptr<QIODevice> device)\n: _device(std::move(device))\n, _size(ValidateLocalSize(_device->size())) {\n\tExpects(_device != nullptr);\n\n\tif (!_size || !_device->open(QIODevice::ReadOnly)) {\n\t\tfail();\n\t}\n}\n\nStorage::Cache::Key LoaderLocal::baseCacheKey() const {\n\treturn {};\n}\n\nint64 LoaderLocal::size() const {\n\treturn _size;\n}\n\nvoid LoaderLocal::load(int64 offset) {\n\tif (_device->pos() != offset && !_device->seek(offset)) {\n\t\tfail();\n\t\treturn;\n\t}\n\tauto result = _device->read(kPartSize);\n\tif (result.isEmpty()\n\t\t|| ((result.size() != kPartSize)\n\t\t\t&& (offset + result.size() != size()))) {\n\t\tfail();\n\t\treturn;\n\t}\n\tcrl::on_main(this, [=, result = std::move(result)]() mutable {\n\t\t_parts.fire({ offset, std::move(result) });\n\t});\n}\n\nvoid LoaderLocal::fail() {\n\tcrl::on_main(this, [=] {\n\t\t_parts.fire({ LoadedPart::kFailedOffset });\n\t});\n}\n\nvoid LoaderLocal::cancel(int64 offset) {\n}\n\nvoid LoaderLocal::resetPriorities() {\n}\n\nvoid LoaderLocal::setPriority(int priority) {\n}\n\nvoid LoaderLocal::stop() {\n}\n\nvoid LoaderLocal::tryRemoveFromQueue() {\n}\n\nrpl::producer<LoadedPart> LoaderLocal::parts() const {\n\treturn _parts.events();\n}\n\nrpl::producer<SpeedEstimate> LoaderLocal::speedEstimate() const {\n\treturn rpl::never<SpeedEstimate>();\n}\n\nvoid LoaderLocal::attachDownloader(\n\t\tnot_null<Storage::StreamedFileDownloader*> downloader) {\n\tUnexpected(\"Downloader attached to a local streaming loader.\");\n}\n\nvoid LoaderLocal::clearAttachedDownloader() {\n\tUnexpected(\"Downloader detached from a local streaming loader.\");\n}\n\nstd::unique_ptr<LoaderLocal> MakeFileLoader(const QString &path) {\n\treturn std::make_unique<LoaderLocal>(std::make_unique<QFile>(path));\n}\n\nstd::unique_ptr<LoaderLocal> MakeBytesLoader(const QByteArray &bytes) {\n\tauto device = std::make_unique<QBuffer>();\n\tauto copy = new QByteArray(bytes);\n\tQObject::connect(device.get(), &QBuffer::destroyed, [=] {\n\t\tdelete copy;\n\t});\n\tdevice->setBuffer(copy);\n\treturn std::make_unique<LoaderLocal>(std::move(device));\n}\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_loader_local.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/streaming/media_streaming_loader.h\"\n#include \"mtproto/sender.h\"\n#include \"data/data_file_origin.h\"\n\nclass ApiWrap;\n\nnamespace Media {\nnamespace Streaming {\n\nclass LoaderLocal : public Loader, public base::has_weak_ptr {\npublic:\n\tLoaderLocal(std::unique_ptr<QIODevice> device);\n\n\t[[nodiscard]] Storage::Cache::Key baseCacheKey() const override;\n\t[[nodiscard]] int64 size() const override;\n\n\tvoid load(int64 offset) override;\n\tvoid cancel(int64 offset) override;\n\tvoid resetPriorities() override;\n\tvoid setPriority(int priority) override;\n\tvoid stop() override;\n\n\tvoid tryRemoveFromQueue() override;\n\n\t// Parts will be sent from the main thread.\n\t[[nodiscard]] rpl::producer<LoadedPart> parts() const override;\n\t[[nodiscard]] rpl::producer<SpeedEstimate> speedEstimate() const override;\n\n\tvoid attachDownloader(\n\t\tnot_null<Storage::StreamedFileDownloader*> downloader) override;\n\tvoid clearAttachedDownloader() override;\n\nprivate:\n\tvoid fail();\n\n\tconst std::unique_ptr<QIODevice> _device;\n\tconst int64 _size = 0;\n\trpl::event_stream<LoadedPart> _parts;\n\n};\n\nstd::unique_ptr<LoaderLocal> MakeFileLoader(const QString &path);\nstd::unique_ptr<LoaderLocal> MakeBytesLoader(const QByteArray &bytes);\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_loader_mtproto.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/streaming/media_streaming_loader_mtproto.h\"\n\n#include \"apiwrap.h\"\n#include \"main/main_session.h\"\n#include \"storage/streamed_file_downloader.h\"\n#include \"storage/cache/storage_cache_types.h\"\n\nnamespace Media {\nnamespace Streaming {\nnamespace {\n\nconstexpr auto kCheckStatsInterval = crl::time(1000);\nconstexpr auto kInitialStatsWait = 5 * crl::time(1000);\n\n} // namespace\n\nLoaderMtproto::LoaderMtproto(\n\tnot_null<Storage::DownloadManagerMtproto*> owner,\n\tconst StorageFileLocation &location,\n\tint64 size,\n\tData::FileOrigin origin)\n: DownloadMtprotoTask(owner, location, origin)\n, _size(size)\n, _api(&api().instance())\n, _statsTimer([=] { checkStats(); }) {\n}\n\nStorage::Cache::Key LoaderMtproto::baseCacheKey() const {\n\treturn v::get<StorageFileLocation>(\n\t\tlocation().data\n\t).bigFileBaseCacheKey();\n}\n\nint64 LoaderMtproto::size() const {\n\treturn _size;\n}\n\nvoid LoaderMtproto::load(int64 offset) {\n\tcrl::on_main(this, [=] {\n\t\tif (_downloader) {\n\t\t\tauto bytes = _downloader->readLoadedPart(offset);\n\t\t\tif (!bytes.isEmpty()) {\n\t\t\t\tcancelForOffset(offset);\n\t\t\t\t_parts.fire({ offset, std::move(bytes) });\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (haveSentRequestForOffset(offset)) {\n\t\t\treturn;\n\t\t} else if (_requested.add(offset)) {\n\t\t\taddToQueueWithPriority();\n\t\t}\n\t});\n}\n\nvoid LoaderMtproto::addToQueueWithPriority() {\n\taddToQueue(_priority);\n}\n\nvoid LoaderMtproto::stop() {\n\tcrl::on_main(this, [=] {\n\t\tcancelAllRequests();\n\t\t_requested.clear();\n\t\tremoveFromQueue();\n\t});\n}\n\nvoid LoaderMtproto::tryRemoveFromQueue() {\n\tcrl::on_main(this, [=] {\n\t\tif (_requested.empty() && !haveSentRequests()) {\n\t\t\tremoveFromQueue();\n\t\t}\n\t});\n}\n\nvoid LoaderMtproto::cancel(int64 offset) {\n\tcrl::on_main(this, [=] {\n\t\tcancelForOffset(offset);\n\t});\n}\n\nvoid LoaderMtproto::cancelForOffset(int64 offset) {\n\tif (haveSentRequestForOffset(offset)) {\n\t\tcancelRequestForOffset(offset);\n\t\tif (!_requested.empty()) {\n\t\t\taddToQueueWithPriority();\n\t\t}\n\t} else {\n\t\t_requested.remove(offset);\n\t}\n}\n\nvoid LoaderMtproto::attachDownloader(\n\t\tnot_null<Storage::StreamedFileDownloader*> downloader) {\n\t_downloader = downloader;\n}\n\nvoid LoaderMtproto::clearAttachedDownloader() {\n\t_downloader = nullptr;\n}\n\nvoid LoaderMtproto::resetPriorities() {\n\tcrl::on_main(this, [=] {\n\t\t_requested.resetPriorities();\n\t});\n}\n\nvoid LoaderMtproto::setPriority(int priority) {\n\tif (_priority == priority) {\n\t\treturn;\n\t}\n\t_priority = priority;\n\tif (haveSentRequests()) {\n\t\taddToQueueWithPriority();\n\t}\n}\n\nbool LoaderMtproto::readyToRequest() const {\n\treturn !_requested.empty();\n}\n\nint64 LoaderMtproto::takeNextRequestOffset() {\n\tconst auto offset = _requested.take();\n\tAssert(offset.has_value());\n\n\tconst auto time = crl::now();\n\tif (!_firstRequestStart) {\n\t\t_firstRequestStart = time;\n\t}\n\t_stats.push_back({ .start = crl::now(), .offset = *offset });\n\n\tEnsures(offset.has_value());\n\treturn *offset;\n}\n\nbool LoaderMtproto::feedPart(int64 offset, const QByteArray &bytes) {\n\tconst auto time = crl::now();\n\tfor (auto &entry : _stats) {\n\t\tif (entry.offset == offset && entry.start < time) {\n\t\t\tentry.end = time;\n\t\t\tif (!_statsTimer.isActive()) {\n\t\t\t\tconst auto checkAt = std::max(\n\t\t\t\t\ttime + kCheckStatsInterval,\n\t\t\t\t\t_firstRequestStart + kInitialStatsWait);\n\t\t\t\t_statsTimer.callOnce(checkAt - time);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\t_parts.fire({ offset, bytes });\n\treturn true;\n}\n\nvoid LoaderMtproto::cancelOnFail() {\n\t_parts.fire({ LoadedPart::kFailedOffset });\n}\n\nrpl::producer<LoadedPart> LoaderMtproto::parts() const {\n\treturn _parts.events();\n}\n\nrpl::producer<SpeedEstimate> LoaderMtproto::speedEstimate() const {\n\treturn _speedEstimate.events();\n}\n\nvoid LoaderMtproto::checkStats() {\n\tconst auto time = crl::now();\n\tconst auto from = time - kInitialStatsWait;\n\t{ // Erase all stats entries that are too old.\n\t\tfor (auto i = begin(_stats); i != end(_stats);) {\n\t\t\tif (i->start >= from) {\n\t\t\t\tbreak;\n\t\t\t} else if (i->end && i->end < from) {\n\t\t\t\ti = _stats.erase(i);\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t}\n\tif (_stats.empty()) {\n\t\treturn;\n\t}\n\t// Count duration for which at least one request was in progress.\n\t// This is the time we should consider for download speed.\n\t// We don't count time when no requests were in progress.\n\tauto durationCountedTill = _stats.front().start;\n\tauto duration = crl::time(0);\n\tauto received = int64(0);\n\tfor (const auto &entry : _stats) {\n\t\tif (entry.start > durationCountedTill) {\n\t\t\tdurationCountedTill = entry.start;\n\t\t}\n\t\tconst auto till = entry.end ? entry.end : time;\n\t\tif (till > durationCountedTill) {\n\t\t\tduration += (till - durationCountedTill);\n\t\t\tdurationCountedTill = till;\n\t\t}\n\t\tif (entry.end) {\n\t\t\treceived += Storage::kDownloadPartSize;\n\t\t}\n\t}\n\tif (duration) {\n\t\t_speedEstimate.fire({\n\t\t\t.bytesPerSecond = int(std::clamp(\n\t\t\t\tint64(received * 1000 / duration),\n\t\t\t\tint64(0),\n\t\t\t\tint64(64 * 1024 * 1024))),\n\t\t\t.unreliable = (received < 3 * Storage::kDownloadPartSize),\n\t\t});\n\t}\n}\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_loader_mtproto.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"media/streaming/media_streaming_loader.h\"\n#include \"mtproto/sender.h\"\n#include \"data/data_file_origin.h\"\n#include \"storage/download_manager_mtproto.h\"\n\nnamespace Media {\nnamespace Streaming {\n\nclass LoaderMtproto : public Loader, public Storage::DownloadMtprotoTask {\npublic:\n\tLoaderMtproto(\n\t\tnot_null<Storage::DownloadManagerMtproto*> owner,\n\t\tconst StorageFileLocation &location,\n\t\tint64 size,\n\t\tData::FileOrigin origin);\n\n\t[[nodiscard]] Storage::Cache::Key baseCacheKey() const override;\n\t[[nodiscard]] int64 size() const override;\n\n\tvoid load(int64 offset) override;\n\tvoid cancel(int64 offset) override;\n\tvoid resetPriorities() override;\n\tvoid setPriority(int priority) override;\n\tvoid stop() override;\n\n\tvoid tryRemoveFromQueue() override;\n\n\t// Parts will be sent from the main thread.\n\t[[nodiscard]] rpl::producer<LoadedPart> parts() const override;\n\t[[nodiscard]] rpl::producer<SpeedEstimate> speedEstimate() const override;\n\n\tvoid attachDownloader(\n\t\tnot_null<Storage::StreamedFileDownloader*> downloader) override;\n\tvoid clearAttachedDownloader() override;\n\nprivate:\n\tstruct StatsEntry {\n\t\tcrl::time start = 0;\n\t\tcrl::time end = 0;\n\t\tint64 offset = 0;\n\t};\n\n\tbool readyToRequest() const override;\n\tint64 takeNextRequestOffset() override;\n\tbool feedPart(int64 offset, const QByteArray &bytes) override;\n\tvoid cancelOnFail() override;\n\n\tvoid cancelForOffset(int64 offset);\n\tvoid addToQueueWithPriority();\n\n\tvoid checkStats();\n\n\tconst int64 _size = 0;\n\tint _priority = 0;\n\n\tMTP::Sender _api;\n\n\tPriorityQueue _requested;\n\trpl::event_stream<LoadedPart> _parts;\n\trpl::event_stream<SpeedEstimate> _speedEstimate;\n\n\tstd::vector<StatsEntry> _stats;\n\tcrl::time _firstRequestStart = 0;\n\tbase::Timer _statsTimer;\n\n\tStorage::StreamedFileDownloader *_downloader = nullptr;\n\n};\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_player.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/streaming/media_streaming_player.h\"\n\n#include \"media/streaming/media_streaming_file.h\"\n#include \"media/streaming/media_streaming_loader.h\"\n#include \"media/streaming/media_streaming_audio_track.h\"\n#include \"media/streaming/media_streaming_video_track.h\"\n#include \"media/audio/media_audio.h\" // for SupportsSpeedControl()\n#include \"media/media_common.h\"\n#include \"data/data_document.h\" // for DocumentData::duration()\n\nnamespace Media {\nnamespace Streaming {\nnamespace {\n\nconstexpr auto kBufferFor = 3 * crl::time(1000);\nconstexpr auto kLoadInAdvanceForRemote = 32 * crl::time(1000);\nconstexpr auto kLoadInAdvanceForLocal = 5 * crl::time(1000);\nconstexpr auto kMsFrequency = 1000; // 1000 ms per second.\n\n// If we played for 3 seconds and got stuck it looks like we're loading\n// slower than we're playing, so load full file in that case.\n//constexpr auto kLoadFullIfStuckAfterPlayback = 3 * crl::time(1000);\n\n[[nodiscard]] bool FullTrackReceived(const TrackState &state) {\n\treturn (state.duration != kTimeUnknown)\n\t\t&& (state.receivedTill == state.duration);\n}\n\nvoid SaveValidStateInformation(TrackState &to, TrackState &&from) {\n\tExpects(from.position != kTimeUnknown);\n\tExpects(from.receivedTill != kTimeUnknown);\n\tExpects(from.duration != kTimeUnknown);\n\n\tto.duration = from.duration;\n\tto.position = from.position;\n\tto.receivedTill = (to.receivedTill == kTimeUnknown)\n\t\t? from.receivedTill\n\t\t: std::clamp(\n\t\t\tstd::max(from.receivedTill, to.receivedTill),\n\t\t\tto.position,\n\t\t\tto.duration);\n}\n\nvoid SaveValidAudioInformation(\n\t\tAudioInformation &to,\n\t\tAudioInformation &&from) {\n\tSaveValidStateInformation(to.state, std::move(from.state));\n}\n\nvoid SaveValidVideoInformation(\n\t\tVideoInformation &to,\n\t\tVideoInformation &&from) {\n\tExpects(!from.size.isEmpty());\n\tExpects(!from.cover.isNull());\n\n\tSaveValidStateInformation(to.state, std::move(from.state));\n\tto.size = from.size;\n\tto.cover = std::move(from.cover);\n\tto.rotation = from.rotation;\n\tto.fps = from.fps;\n\tto.alpha = from.alpha;\n}\n\nvoid SaveValidStartInformation(Information &to, Information &&from) {\n\tif (from.audio.state.duration != kTimeUnknown) {\n\t\tSaveValidAudioInformation(to.audio, std::move(from.audio));\n\t}\n\tif (from.video.state.duration != kTimeUnknown) {\n\t\tSaveValidVideoInformation(to.video, std::move(from.video));\n\t}\n\tif (from.headerSize && !to.headerSize) {\n\t\tto.headerSize = from.headerSize;\n\t}\n}\n\n} // namespace\n\nPlayer::Player(std::shared_ptr<Reader> reader)\n: _file(std::make_unique<File>(std::move(reader)))\n, _remoteLoader(_file->isRemoteLoader())\n, _renderFrameTimer([=] { checkNextFrameRender(); }) {\n}\n\nnot_null<FileDelegate*> Player::delegate() {\n\treturn static_cast<FileDelegate*>(this);\n}\n\nvoid Player::checkNextFrameRender() {\n\tExpects(_nextFrameTime != kTimeUnknown);\n\n\tconst auto now = crl::now();\n\tif (now < _nextFrameTime) {\n\t\tif (!_renderFrameTimer.isActive()) {\n\t\t\t_renderFrameTimer.callOnce(_nextFrameTime - now);\n\t\t}\n\t} else {\n\t\t_renderFrameTimer.cancel();\n\t\trenderFrame(now);\n\t}\n}\n\nvoid Player::checkNextFrameAvailability() {\n\tExpects(_video != nullptr);\n\n\t_nextFrameTime = _video->nextFrameDisplayTime();\n\tAssert(_nextFrameTime != kFrameDisplayTimeAlreadyDone);\n\tif (_nextFrameTime != kTimeUnknown) {\n\t\tcheckNextFrameRender();\n\t}\n}\n\nvoid Player::renderFrame(crl::time now) {\n\tExpects(_video != nullptr);\n\tExpects(_nextFrameTime != kTimeUnknown);\n\tExpects(_nextFrameTime != kFrameDisplayTimeAlreadyDone);\n\n\tconst auto position = _video->markFrameDisplayed(now);\n\tif (_options.waitForMarkAsShown) {\n\t\t_currentFrameTime = _nextFrameTime;\n\t\t_nextFrameTime = kFrameDisplayTimeAlreadyDone;\n\t} else {\n\t\t_video->markFrameShown();\n\t\t_nextFrameTime = kTimeUnknown;\n\t}\n\n\tAssert(position != kTimeUnknown);\n\tvideoPlayedTill(position);\n}\n\nbool Player::markFrameShown() {\n\tExpects(_video != nullptr);\n\n\tif (_nextFrameTime == kFrameDisplayTimeAlreadyDone) {\n\t\t_nextFrameTime = kTimeUnknown;\n\t\t_video->addTimelineDelay(crl::now() - _currentFrameTime);\n\t}\n\treturn _video->markFrameShown();\n}\n\nvoid Player::setLoaderPriority(int priority) {\n\t_file->setLoaderPriority(priority);\n}\n\ntemplate <typename Track>\nvoid Player::trackReceivedTill(\n\t\tconst Track &track,\n\t\tTrackState &state,\n\t\tcrl::time position) {\n\tif (position == kTimeUnknown) {\n\t\treturn;\n\t} else if (state.duration != kTimeUnknown) {\n\t\tif (state.receivedTill < position) {\n\t\t\tstate.receivedTill = position;\n\t\t\ttrackSendReceivedTill(track, state);\n\t\t}\n\t} else {\n\t\tstate.receivedTill = position;\n\t}\n\tif (!_pauseReading\n\t\t&& bothReceivedEnough(loadInAdvanceFor())\n\t\t&& !receivedTillEnd()) {\n\t\t_pauseReading = true;\n\t}\n}\n\ntemplate <typename Track>\nvoid Player::trackPlayedTill(\n\t\tconst Track &track,\n\t\tTrackState &state,\n\t\tcrl::time position) {\n\tconst auto guard = base::make_weak(&_sessionGuard);\n\ttrackReceivedTill(track, state, position);\n\tif (guard && position != kTimeUnknown) {\n\t\tstate.position = position;\n\t\tconst auto value = _options.loop\n\t\t\t? (position % computeTotalDuration())\n\t\t\t: position;\n\t\t_updates.fire({ PlaybackUpdate<Track>{ value } });\n\t}\n\tif (_pauseReading\n\t\t&& (!bothReceivedEnough(loadInAdvanceFor()) || receivedTillEnd())) {\n\t\t_pauseReading = false;\n\t\t_file->wake();\n\t}\n}\n\ntemplate <typename Track>\nvoid Player::trackSendReceivedTill(\n\t\tconst Track &track,\n\t\tTrackState &state) {\n\tExpects(state.duration != kTimeUnknown);\n\tExpects(state.receivedTill != kTimeUnknown);\n\n\tif (!_remoteLoader || _fullInCacheSinceStart.value_or(false)) {\n\t\treturn;\n\t}\n\tconst auto receivedTill = std::max(\n\t\tstate.receivedTill,\n\t\t_previousReceivedTill);\n\tconst auto value = _options.loop\n\t\t? (receivedTill % computeTotalDuration())\n\t\t: receivedTill;\n\t_updates.fire({ PreloadedUpdate<Track>{ value } });\n}\n\nvoid Player::audioReceivedTill(crl::time position) {\n\tExpects(_audio != nullptr);\n\n\ttrackReceivedTill(*_audio, _information.audio.state, position);\n\tcheckResumeFromWaitingForData();\n}\n\nvoid Player::audioPlayedTill(crl::time position) {\n\tExpects(_audio != nullptr);\n\n\ttrackPlayedTill(*_audio, _information.audio.state, position);\n}\n\nvoid Player::videoReceivedTill(crl::time position) {\n\tExpects(_video != nullptr);\n\n\ttrackReceivedTill(*_video, _information.video.state, position);\n\tcheckResumeFromWaitingForData();\n}\n\nvoid Player::videoPlayedTill(crl::time position) {\n\tExpects(_video != nullptr);\n\n\ttrackPlayedTill(*_video, _information.video.state, position);\n}\n\nMode Player::fileOpenMode() {\n\treturn _options.mode;\n}\n\nbool Player::fileReady(int headerSize, Stream &&video, Stream &&audio) {\n\t_waitingForData = false;\n\n\tconst auto weak = base::make_weak(&_sessionGuard);\n\tconst auto ready = [=](const Information &data) {\n\t\tcrl::on_main(weak, [=, data = data]() mutable {\n\t\t\tdata.headerSize = headerSize;\n\t\t\tstreamReady(std::move(data));\n\t\t});\n\t};\n\tconst auto error = [=](Error error) {\n\t\tcrl::on_main(weak, [=] {\n\t\t\tstreamFailed(error);\n\t\t});\n\t};\n\tconst auto mode = _options.mode;\n\tif ((mode != Mode::Audio && mode != Mode::Both)\n\t\t|| audio.duration == kDurationUnavailable) {\n\t\taudio = Stream();\n\t}\n\tif (mode != Mode::Video && mode != Mode::Both) {\n\t\tvideo = Stream();\n\t}\n\tif (audio.duration == kDurationUnavailable) {\n\t\tLOG((\"Streaming Error: Audio stream with unknown duration.\"));\n\t\treturn false;\n\t} else if (audio.codec) {\n\t\tif (_options.audioId.audio() != nullptr) {\n\t\t\t_audioId = AudioMsgId(\n\t\t\t\t_options.audioId.audio(),\n\t\t\t\t_options.audioId.contextId(),\n\t\t\t\tAudioMsgId::CreateExternalPlayId());\n\t\t} else {\n\t\t\t_audioId = AudioMsgId::ForVideo();\n\t\t}\n\t\t_audio = std::make_unique<AudioTrack>(\n\t\t\t_options,\n\t\t\tstd::move(audio),\n\t\t\t_audioId,\n\t\t\tready,\n\t\t\terror);\n\t} else if (audio.index >= 0) {\n\t\tLOG((\"Streaming Error: No codec for audio stream %1, mode %2.\"\n\t\t\t).arg(audio.index\n\t\t\t).arg(int(mode)));\n\t\treturn false;\n\t} else {\n\t\t_audioId = AudioMsgId();\n\t}\n\tif (video.codec) {\n\t\t_video = std::make_unique<VideoTrack>(\n\t\t\t_options,\n\t\t\tstd::move(video),\n\t\t\t_audioId,\n\t\t\tready,\n\t\t\terror);\n\t} else if (video.index >= 0) {\n\t\tLOG((\"Streaming Error: No codec for video stream %1, mode %2.\"\n\t\t\t).arg(video.index\n\t\t\t).arg(int(mode)));\n\t\treturn false;\n\t}\n\tif ((mode == Mode::Audio && !_audio)\n\t\t|| (mode == Mode::Video && !_video)\n\t\t|| (!_audio && !_video)) {\n\t\tLOG((\"Streaming Error: Required stream not found for mode %1.\"\n\t\t\t).arg(int(mode)));\n\t\treturn false;\n\t} else if (_audio\n\t\t&& _video\n\t\t&& _video->streamDuration() == kDurationUnavailable) {\n\t\tLOG((\"Streaming Error: Both streams with unknown video duration.\"));\n\t\treturn false;\n\t}\n\t_totalDuration = std::max(\n\t\t_audio ? _audio->streamDuration() : kTimeUnknown,\n\t\t_video ? _video->streamDuration() : kTimeUnknown);\n\n\tEnsures(_totalDuration > 1);\n\treturn true;\n}\n\nvoid Player::fileError(Error error) {\n\t_waitingForData = false;\n\n\tcrl::on_main(&_sessionGuard, [=] {\n\t\tfail(error);\n\t});\n}\n\nvoid Player::fileFullInCache(bool fullInCache) {\n\tcrl::on_main(&_sessionGuard, [=] {\n\t\tif (!_fullInCacheSinceStart.has_value()) {\n\t\t\t_fullInCacheSinceStart = fullInCache;\n\t\t}\n\t\t_fullInCache.fire_copy(fullInCache);\n\t});\n}\n\nvoid Player::fileWaitingForData() {\n\tif (_waitingForData) {\n\t\treturn;\n\t}\n\t_waitingForData = true;\n\tif (_audio) {\n\t\t_audio->waitForData();\n\t}\n\tif (_video) {\n\t\t_video->waitForData();\n\t}\n}\n\nbool Player::fileProcessPackets(\n\t\tbase::flat_map<int, std::vector<FFmpeg::Packet>> &packets) {\n\t_waitingForData = false;\n\tfor (auto &[index, list] : packets) {\n\t\tif (list.empty()) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (_audio && _audio->streamIndex() == index) {\n\t\t\t//for (const auto &packet : list) {\n\t\t\t//\t// Maybe it is enough to count by list.back()?.. hope so.\n\t\t\t//\taccumulate_max(\n\t\t\t//\t\t_durationByLastAudioPacket,\n\t\t\t//\t\tdurationByPacket(*_audio, packet));\n\t\t\t//}\n\t\t\taccumulate_max(\n\t\t\t\t_durationByLastAudioPacket,\n\t\t\t\tdurationByPacket(*_audio, list.back()));\n\t\t\tconst auto till = _loopingShift + std::clamp(\n\t\t\t\tFFmpeg::PacketPosition(\n\t\t\t\t\tlist.back(),\n\t\t\t\t\t_audio->streamTimeBase()),\n\t\t\t\tcrl::time(0),\n\t\t\t\tcomputeAudioDuration() - 1);\n\t\t\tcrl::on_main(&_sessionGuard, [=] {\n\t\t\t\taudioReceivedTill(till);\n\t\t\t});\n\t\t\t_audio->process(base::take(list));\n\t\t} else if (_video && _video->streamIndex() == index) {\n\t\t\t//for (const auto &packet : list) {\n\t\t\t//\t// Maybe it is enough to count by list.back()?.. hope so.\n\t\t\t//\taccumulate_max(\n\t\t\t//\t\t_durationByLastVideoPacket,\n\t\t\t//\t\tdurationByPacket(*_video, packet));\n\t\t\t//}\n\t\t\taccumulate_max(\n\t\t\t\t_durationByLastVideoPacket,\n\t\t\t\tdurationByPacket(*_video, list.back()));\n\t\t\tconst auto till = _loopingShift + std::clamp(\n\t\t\t\tFFmpeg::PacketPosition(\n\t\t\t\t\tlist.back(),\n\t\t\t\t\t_video->streamTimeBase()),\n\t\t\t\tcrl::time(0),\n\t\t\t\tcomputeVideoDuration() - 1);\n\t\t\tcrl::on_main(&_sessionGuard, [=] {\n\t\t\t\tvideoReceivedTill(till);\n\t\t\t});\n\t\t\t_video->process(base::take(list));\n\t\t} else {\n\t\t\tlist.clear(); // Free non-needed packets.\n\t\t}\n\t}\n\treturn fileReadMore();\n}\n\nvoid Player::fileProcessEndOfFile() {\n\t_waitingForData = false;\n\t_readTillEnd = true;\n\tsetDurationByPackets();\n\tconst auto generateEmptyQueue = [] {\n\t\tauto result = std::vector<FFmpeg::Packet>();\n\t\tresult.emplace_back();\n\t\treturn result;\n\t};\n\tif (_audio) {\n\t\tconst auto till = _loopingShift + computeAudioDuration();\n\t\tcrl::on_main(&_sessionGuard, [=] {\n\t\t\taudioReceivedTill(till);\n\t\t});\n\t\t_audio->process(generateEmptyQueue());\n\t}\n\tif (_video) {\n\t\tconst auto till = _loopingShift + computeVideoDuration();\n\t\tcrl::on_main(&_sessionGuard, [=] {\n\t\t\tvideoReceivedTill(till);\n\t\t});\n\t\t_video->process(generateEmptyQueue());\n\t}\n}\n\nbool Player::fileReadMore() {\n\tif (_options.loop && _readTillEnd) {\n\t\tconst auto duration = computeTotalDuration();\n\t\tif (duration == kDurationUnavailable) {\n\t\t\tLOG((\"Streaming Error: \"\n\t\t\t\t\"Couldn't find out the real stream duration.\"));\n\t\t\tfileError(Error::InvalidData);\n\t\t\treturn false;\n\t\t}\n\t\t_loopingShift += duration;\n\t\t_readTillEnd = false;\n\t\treturn true;\n\t}\n\treturn !_readTillEnd && !_pauseReading;\n}\n\nvoid Player::streamReady(Information &&information) {\n\tSaveValidStartInformation(_information, std::move(information));\n\tprovideStartInformation();\n}\n\nvoid Player::streamFailed(Error error) {\n\tfail(error);\n}\n\ntemplate <typename Track>\nint Player::durationByPacket(\n\t\tconst Track &track,\n\t\tconst FFmpeg::Packet &packet) {\n\t// We've set this value on the first cycle.\n\tif (_loopingShift || _totalDuration != kDurationUnavailable) {\n\t\treturn 0;\n\t}\n\tconst auto result = DurationByPacket(packet, track.streamTimeBase());\n\tif (result < 0) {\n\t\tfileError(Error::InvalidData);\n\t\treturn 0;\n\t}\n\n\tEnsures(result > 0);\n\treturn result;\n}\n\nvoid Player::setDurationByPackets() {\n\tif (_loopingShift || _totalDuration != kDurationUnavailable) {\n\t\treturn;\n\t}\n\tconst auto duration = std::max(\n\t\t_durationByLastAudioPacket,\n\t\t_durationByLastVideoPacket);\n\tif (duration > 1) {\n\t\t_durationByPackets = duration;\n\t} else {\n\t\tLOG((\"Streaming Error: Bad total duration by packets: %1\"\n\t\t\t).arg(duration));\n\t\tfileError(Error::InvalidData);\n\t}\n}\n\nvoid Player::provideStartInformation() {\n\tExpects(_stage == Stage::Initializing);\n\n\tif ((_audio && _information.audio.state.duration == kTimeUnknown)\n\t\t|| (_video && _information.video.state.duration == kTimeUnknown)) {\n\t\treturn; // Not ready yet.\n\t} else if ((!_audio && !_video)\n\t\t|| (!_audio && _options.mode == Mode::Audio)\n\t\t|| (!_video && _options.mode == Mode::Video)) {\n\t\tfail(Error::OpenFailed);\n\t} else {\n\t\t_stage = Stage::Ready;\n\n\t\tif (_audio && _audioFinished) {\n\t\t\t// Audio was stopped before it was ready.\n\t\t\t_audio->stop();\n\t\t}\n\n\t\t// Don't keep the reference to the video cover.\n\t\tauto copy = _information;\n\t\t_information.video.cover = QImage();\n\n\t\t_updates.fire(Update{ std::move(copy) });\n\n\t\tif (_stage == Stage::Ready && !_paused) {\n\t\t\t_paused = true;\n\t\t\tupdatePausedState();\n\t\t}\n\t}\n}\n\nvoid Player::fail(Error error) {\n\t_sessionLifetime = rpl::lifetime();\n\tconst auto stopGuarded = crl::guard(&_sessionGuard, [=] { stop(); });\n\t_lastFailure = error;\n\t_updates.fire_error(std::move(error));\n\tstopGuarded();\n}\n\nvoid Player::play(const PlaybackOptions &options) {\n\tExpects(options.speed >= kSpeedMin && options.speed <= kSpeedMax);\n\n\t// Looping video with audio is not supported for now.\n\tExpects(!options.loop || (options.mode != Mode::Both));\n\n\tconst auto previous = getCurrentReceivedTill(computeTotalDuration());\n\n\tstop(true);\n\t_lastFailure = std::nullopt;\n\n\tsavePreviousReceivedTill(options, previous);\n\t_options = options;\n\tif (!Media::Audio::SupportsSpeedControl()) {\n\t\t_options.speed = 1.;\n\t}\n\tif (!_options.seekable) {\n\t\t_options.position = 0;\n\t}\n\t_stage = Stage::Initializing;\n\t_file->start(delegate(), {\n\t\t.position = _options.position,\n\t\t.durationOverride = options.durationOverride,\n\t\t.seekable = _options.seekable,\n\t\t.hwAllow = _options.hwAllowed,\n\t});\n}\n\nvoid Player::savePreviousReceivedTill(\n\t\tconst PlaybackOptions &options,\n\t\tcrl::time previousReceivedTill) {\n\t// Save previous 'receivedTill' values if we seek inside the range.\n\t_previousReceivedTill = ((options.position >= _options.position)\n\t\t&& (options.mode == _options.mode)\n\t\t&& (options.position < previousReceivedTill))\n\t\t? previousReceivedTill\n\t\t: kTimeUnknown;\n}\n\ncrl::time Player::loadInAdvanceFor() const {\n\treturn _remoteLoader ? kLoadInAdvanceForRemote : kLoadInAdvanceForLocal;\n}\n\ncrl::time Player::computeTotalDuration() const {\n\tif (_totalDuration != kDurationUnavailable) {\n\t\treturn _totalDuration;\n\t} else if (const auto byPackets = _durationByPackets.load()) {\n\t\treturn byPackets;\n\t}\n\treturn kDurationUnavailable;\n}\n\ncrl::time Player::computeAudioDuration() const {\n\tExpects(_audio != nullptr);\n\n\tconst auto result = _audio->streamDuration();\n\tif (result != kDurationUnavailable) {\n\t\treturn result;\n\t} else if ((_loopingShift || _readTillEnd)\n\t\t&& _durationByLastAudioPacket) {\n\t\t// We looped, so it already holds full stream duration.\n\t\treturn _durationByLastAudioPacket;\n\t}\n\treturn kDurationUnavailable;\n}\n\ncrl::time Player::computeVideoDuration() const {\n\tExpects(_video != nullptr);\n\n\tconst auto result = _video->streamDuration();\n\tif (result != kDurationUnavailable) {\n\t\treturn result;\n\t} else if ((_loopingShift || _readTillEnd)\n\t\t&& _durationByLastVideoPacket) {\n\t\t// We looped, so it already holds full stream duration.\n\t\treturn _durationByLastVideoPacket;\n\t}\n\treturn kDurationUnavailable;\n}\n\nvoid Player::pause() {\n\tExpects(active());\n\n\t_pausedByUser = true;\n\tupdatePausedState();\n}\n\nvoid Player::resume() {\n\tExpects(active());\n\n\t_pausedByUser = false;\n\tupdatePausedState();\n}\n\nvoid Player::stop() {\n\tstop(false);\n}\n\nvoid Player::stopAudio() {\n\tif (!_video) {\n\t\tstop();\n\t} else if (_audio) {\n\t\t_audioFinished = true;\n\t\tif (_information.audio.state.duration != kTimeUnknown) {\n\t\t\t// Audio is ready.\n\t\t\t_audio->stop();\n\t\t}\n\t}\n}\n\nvoid Player::updatePausedState() {\n\tconst auto paused = _pausedByUser || _pausedByWaitingForData;\n\tif (_paused == paused) {\n\t\treturn;\n\t}\n\t_paused = paused;\n\tif (!_paused && _stage == Stage::Ready) {\n\t\tconst auto guard = base::make_weak(&_sessionGuard);\n\t\tstart();\n\t\tif (!guard) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tif (_stage != Stage::Started) {\n\t\treturn;\n\t}\n\tif (_paused) {\n\t\t_pausedTime = crl::now();\n\t\t//if (_pausedByWaitingForData\n\t\t//\t&& _pausedTime - _startedTime > kLoadFullIfStuckAfterPlayback) {\n\t\t//\t_loadFull = true;\n\t\t//}\n\t\tif (_audio) {\n\t\t\t_audio->pause(_pausedTime);\n\t\t}\n\t\tif (_video) {\n\t\t\t_video->pause(_pausedTime);\n\t\t}\n\t} else {\n\t\t_startedTime = crl::now();\n\t\tif (_audio) {\n\t\t\t_audio->resume(_startedTime);\n\t\t}\n\t\tif (_video) {\n\t\t\t_video->resume(_startedTime);\n\t\t}\n\t}\n}\n\nbool Player::trackReceivedEnough(\n\t\tconst TrackState &state,\n\t\tcrl::time amount) const {\n\treturn (!_options.loop && FullTrackReceived(state))\n\t\t|| (state.position != kTimeUnknown\n\t\t\t&& (state.position + std::min(amount, state.duration)\n\t\t\t\t<= state.receivedTill));\n}\n\nbool Player::bothReceivedEnough(crl::time amount) const {\n\tconst auto &info = _information;\n\treturn (!_audio || trackReceivedEnough(info.audio.state, amount))\n\t\t&& (!_video || trackReceivedEnough(info.video.state, amount));\n}\n\nbool Player::receivedTillEnd() const {\n\tif (_options.loop) {\n\t\treturn false;\n\t}\n\treturn (!_video || FullTrackReceived(_information.video.state))\n\t\t&& (!_audio || FullTrackReceived(_information.audio.state));\n}\n\nvoid Player::checkResumeFromWaitingForData() {\n\tif (_pausedByWaitingForData && bothReceivedEnough(kBufferFor)) {\n\t\t_pausedByWaitingForData = false;\n\t\tupdatePausedState();\n\t\t_updates.fire({ WaitingForData{ false } });\n\t}\n}\n\nvoid Player::start() {\n\tExpects(_stage == Stage::Ready);\n\n\t_stage = Stage::Started;\n\tconst auto guard = base::make_weak(&_sessionGuard);\n\n\t_file->speedEstimate() | rpl::on_next([=](SpeedEstimate value) {\n\t\t_updates.fire({ value });\n\t}, _sessionLifetime);\n\n\trpl::merge(\n\t\t_audio ? _audio->waitingForData() : nullptr,\n\t\t_video ? _video->waitingForData() : nullptr\n\t) | rpl::filter([=] {\n\t\treturn !bothReceivedEnough(kBufferFor);\n\t}) | rpl::on_next([=] {\n\t\t_pausedByWaitingForData = true;\n\t\tupdatePausedState();\n\t\t_updates.fire({ WaitingForData{ true } });\n\t}, _sessionLifetime);\n\n\tif (guard && _audio && !_audioFinished) {\n\t\t_audio->playPosition(\n\t\t) | rpl::on_next_done([=](crl::time position) {\n\t\t\taudioPlayedTill(position);\n\t\t}, [=] {\n\t\t\tExpects(_stage == Stage::Started);\n\n\t\t\t_audioFinished = true;\n\t\t\tif (!_video || _videoFinished) {\n\t\t\t\t_updates.fire({ Finished() });\n\t\t\t}\n\t\t}, _sessionLifetime);\n\t}\n\n\tif (guard && _video) {\n\t\t_video->checkNextFrame(\n\t\t) | rpl::on_next_done([=] {\n\t\t\tcheckVideoStep();\n\t\t}, [=] {\n\t\t\tAssert(_stage == Stage::Started);\n\n\t\t\t_videoFinished = true;\n\t\t\tif (!_audio || _audioFinished) {\n\t\t\t\t_updates.fire({ Finished() });\n\t\t\t}\n\t\t}, _sessionLifetime);\n\n\t\tcrl::on_main_update_requests(\n\t\t) | rpl::filter([=] {\n\t\t\treturn !_videoFinished;\n\t\t}) | rpl::on_next([=] {\n\t\t\tcheckVideoStep();\n\t\t}, _sessionLifetime);\n\t}\n\tif (guard && _audio) {\n\t\tif (_audioFinished) {\n\t\t\tif (!_video || _videoFinished) {\n\t\t\t\t_updates.fire({ Finished() });\n\t\t\t}\n\t\t} else {\n\t\t\ttrackSendReceivedTill(*_audio, _information.audio.state);\n\t\t}\n\t}\n\tif (guard && _video) {\n\t\ttrackSendReceivedTill(*_video, _information.video.state);\n\t}\n}\n\nvoid Player::checkVideoStep() {\n\tif (_nextFrameTime == kFrameDisplayTimeAlreadyDone) {\n\t\treturn;\n\t} else if (_nextFrameTime != kTimeUnknown) {\n\t\tcheckNextFrameRender();\n\t} else {\n\t\tcheckNextFrameAvailability();\n\t}\n}\n\nvoid Player::stop(bool stillActive) {\n\t_file->stop(stillActive);\n\t_sessionLifetime = rpl::lifetime();\n\t_stage = Stage::Uninitialized;\n\t_audio = nullptr;\n\t_video = nullptr;\n\tinvalidate_weak_ptrs(&_sessionGuard);\n\t_pausedByUser = _pausedByWaitingForData = _paused = false;\n\t_renderFrameTimer.cancel();\n\t_nextFrameTime = kTimeUnknown;\n\t_audioFinished = false;\n\t_videoFinished = false;\n\t_pauseReading = false;\n\t_readTillEnd = false;\n\t_loopingShift = 0;\n\t_durationByPackets = 0;\n\t_durationByLastAudioPacket = 0;\n\t_durationByLastVideoPacket = 0;\n\tconst auto header = _information.headerSize;\n\t_information = Information();\n\t_information.headerSize = header;\n}\n\nstd::optional<Error> Player::failed() const {\n\treturn _lastFailure;\n}\n\nbool Player::playing() const {\n\treturn (_stage == Stage::Started)\n\t\t&& !paused()\n\t\t&& !finished()\n\t\t&& !failed();\n}\n\nbool Player::buffering() const {\n\treturn _pausedByWaitingForData;\n}\n\nbool Player::paused() const {\n\treturn _pausedByUser && active();\n}\n\nbool Player::finished() const {\n\treturn (_stage == Stage::Started)\n\t\t&& (!_audio || _audioFinished)\n\t\t&& (!_video || _videoFinished);\n}\n\nfloat64 Player::speed() const {\n\treturn _options.speed;\n}\n\nvoid Player::setSpeed(float64 speed) {\n\tExpects(speed >= kSpeedMin && speed <= kSpeedMax);\n\n\tif (!Media::Audio::SupportsSpeedControl()) {\n\t\tspeed = 1.;\n\t}\n\tif (!EqualSpeeds(_options.speed, speed)) {\n\t\t_options.speed = speed;\n\t\tif (active()) {\n\t\t\tif (_audio) {\n\t\t\t\t_audio->setSpeed(speed);\n\t\t\t}\n\t\t\tif (_video) {\n\t\t\t\t_video->setSpeed(speed);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Player::setWaitForMarkAsShown(bool wait) {\n\tif (_options.waitForMarkAsShown != wait) {\n\t\t_options.waitForMarkAsShown = wait;\n\t\tif (_video) {\n\t\t\t_video->setWaitForMarkAsShown(wait);\n\t\t}\n\t}\n}\n\nbool Player::active() const {\n\treturn (_stage != Stage::Uninitialized) && !finished() && !failed();\n}\n\nbool Player::ready() const {\n\treturn (_stage != Stage::Uninitialized)\n\t\t&& (_stage != Stage::Initializing);\n}\n\nrpl::producer<Update, Error> Player::updates() const {\n\treturn _updates.events();\n}\n\nrpl::producer<bool> Player::fullInCache() const {\n\treturn _fullInCache.events();\n}\n\nint64 Player::fileSize() const {\n\treturn _file->size();\n}\n\nQSize Player::videoSize() const {\n\treturn _information.video.size;\n}\n\nQImage Player::frame(\n\t\tconst FrameRequest &request,\n\t\tconst Instance *instance) const {\n\tExpects(_video != nullptr);\n\n\treturn _video->frame(request, instance);\n}\n\nFrameWithInfo Player::frameWithInfo(\n\t\tconst FrameRequest &request,\n\t\tconst Instance *instance) const {\n\tExpects(_video != nullptr);\n\n\treturn _video->frameWithInfo(request, instance);\n}\n\nFrameWithInfo Player::frameWithInfo(const Instance *instance) const {\n\tExpects(_video != nullptr);\n\n\treturn _video->frameWithInfo(instance);\n}\n\nQImage Player::currentFrameImage() const {\n\tExpects(_video != nullptr);\n\n\treturn _video->currentFrameImage();\n}\n\nvoid Player::unregisterInstance(not_null<const Instance*> instance) {\n\tif (_video) {\n\t\t_video->unregisterInstance(instance);\n\t}\n}\n\nMedia::Player::TrackState Player::prepareLegacyState() const {\n\tusing namespace Media::Player;\n\n\tauto result = Media::Player::TrackState();\n\tresult.id = _audioId.externalPlayId() ? _audioId : _options.audioId;\n\tresult.state = (_lastFailure == Error::OpenFailed\n\t\t|| _lastFailure == Error::NotStreamable)\n\t\t? State::StoppedAtStart\n\t\t: _lastFailure\n\t\t? State::StoppedAtError\n\t\t: finished()\n\t\t? State::StoppedAtEnd\n\t\t: (_stage == Stage::Uninitialized)\n\t\t? State::Stopped\n\t\t: paused()\n\t\t? State::Paused\n\t\t: State::Playing;\n\tresult.position = std::max(\n\t\t_information.audio.state.position,\n\t\t_information.video.state.position);\n\tresult.length = computeTotalDuration();\n\tif (result.position == kTimeUnknown) {\n\t\tresult.position = _options.position;\n\t} else if (_options.loop && result.length > 0) {\n\t\tresult.position %= result.length;\n\t}\n\tresult.receivedTill = (_remoteLoader\n\t\t&& !_fullInCacheSinceStart.value_or(false))\n\t\t? getCurrentReceivedTill(result.length)\n\t\t: 0;\n\tresult.frequency = kMsFrequency;\n\tresult.fileHeaderSize = _information.headerSize;\n\n\tif (result.length == kTimeUnknown) {\n\t\tconst auto document = _options.audioId.audio();\n\t\tconst auto duration = document ? document->duration() : 0;\n\t\tif (duration > 0) {\n\t\t\tresult.length = duration;\n\t\t} else {\n\t\t\tresult.length = std::max(\n\t\t\t\tcrl::time(result.position),\n\t\t\t\tcrl::time(0));\n\t\t}\n\t}\n\treturn result;\n}\n\ncrl::time Player::getCurrentReceivedTill(crl::time duration) const {\n\tconst auto forTrack = [&](const TrackState &state) {\n\t\treturn (state.duration > 0 && state.receivedTill == state.duration)\n\t\t\t? std::max(state.receivedTill, duration)\n\t\t\t: state.receivedTill;\n\t};\n\tconst auto previous = std::max(_previousReceivedTill, crl::time(0));\n\tconst auto result = std::min(\n\t\tstd::max(forTrack(_information.audio.state), previous),\n\t\tstd::max(forTrack(_information.video.state), previous));\n\treturn (result >= 0 && duration > 1 && _options.loop)\n\t\t? (result % duration)\n\t\t: result;\n}\n\nvoid Player::lock() {\n\t++_locks;\n}\n\nvoid Player::unlock() {\n\tExpects(_locks > 0);\n\n\t--_locks;\n\tif (!_locks) {\n\t\tstopAudio();\n\t\tif (active()) {\n\t\t\tsetSpeed(1.);\n\t\t}\n\t\tsetWaitForMarkAsShown(true);\n\t}\n}\n\nbool Player::locked() const {\n\treturn (_locks > 0);\n}\n\nrpl::lifetime &Player::lifetime() {\n\treturn _lifetime;\n}\n\nPlayer::~Player() {\n\t// The order of field destruction is important.\n\t//\n\t// We are forced to maintain the correct order in the stop() method,\n\t// because it can be called even before the player destruction.\n\t//\n\t// So instead of maintaining it in the class definition as well we\n\t// simply call stop() here, after that the destruction is trivial.\n\tstop();\n}\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_player.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/streaming/media_streaming_common.h\"\n#include \"media/streaming/media_streaming_file_delegate.h\"\n#include \"base/weak_ptr.h\"\n#include \"base/timer.h\"\n\nnamespace Media {\nnamespace Player {\nstruct TrackState;\n} // namespace Player\n} // namespace Media\n\nnamespace Media {\nnamespace Streaming {\n\nclass Reader;\nclass File;\nclass AudioTrack;\nclass VideoTrack;\nclass Instance;\n\nclass Player final : private FileDelegate {\npublic:\n\t// Public interfaces is used from the main thread.\n\texplicit Player(std::shared_ptr<Reader> reader);\n\n\t// Because we remember 'this' in calls to crl::on_main.\n\tPlayer(const Player &other) = delete;\n\tPlayer &operator=(const Player &other) = delete;\n\n\tvoid play(const PlaybackOptions &options);\n\tvoid pause();\n\tvoid resume();\n\tvoid stop();\n\n\t// Allow to irreversibly stop only audio track.\n\tvoid stopAudio();\n\n\t[[nodiscard]] bool active() const;\n\t[[nodiscard]] bool ready() const;\n\n\t[[nodiscard]] float64 speed() const;\n\tvoid setSpeed(float64 speed);\n\tvoid setWaitForMarkAsShown(bool wait);\n\n\t[[nodiscard]] bool playing() const;\n\t[[nodiscard]] bool buffering() const;\n\t[[nodiscard]] bool paused() const;\n\t[[nodiscard]] std::optional<Error> failed() const;\n\t[[nodiscard]] bool finished() const;\n\n\t[[nodiscard]] rpl::producer<Update, Error> updates() const;\n\t[[nodiscard]] rpl::producer<bool> fullInCache() const;\n\n\t[[nodiscard]] int64 fileSize() const;\n\t[[nodiscard]] QSize videoSize() const;\n\t[[nodiscard]] QImage frame(\n\t\tconst FrameRequest &request,\n\t\tconst Instance *instance = nullptr) const;\n\t[[nodiscard]] FrameWithInfo frameWithInfo(\n\t\tconst FrameRequest &request,\n\t\tconst Instance *instance = nullptr) const;\n\t[[nodiscard]] FrameWithInfo frameWithInfo(\n\t\tconst Instance *instance = nullptr) const; // !requireARGB32\n\n\t[[nodiscard]] QImage currentFrameImage() const; // Converts if needed.\n\n\tvoid unregisterInstance(not_null<const Instance*> instance);\n\tbool markFrameShown();\n\n\tvoid setLoaderPriority(int priority);\n\n\t[[nodiscard]] Media::Player::TrackState prepareLegacyState() const;\n\n\tvoid lock();\n\tvoid unlock();\n\t[[nodiscard]] bool locked() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\n\t~Player();\n\nprivate:\n\tenum class Stage {\n\t\tUninitialized,\n\t\tInitializing,\n\t\tReady,\n\t\tStarted,\n\t};\n\n\t// Thread-safe.\n\tnot_null<FileDelegate*> delegate();\n\n\t// FileDelegate methods are called only from the File thread.\n\tMode fileOpenMode() override;\n\tbool fileReady(int headerSize, Stream &&video, Stream &&audio) override;\n\tvoid fileError(Error error) override;\n\tvoid fileWaitingForData() override;\n\tvoid fileFullInCache(bool fullInCache) override;\n\tbool fileProcessPackets(\n\t\tbase::flat_map<int, std::vector<FFmpeg::Packet>> &packets) override;\n\tvoid fileProcessEndOfFile() override;\n\tbool fileReadMore() override;\n\n\t// Called from the main thread.\n\tvoid streamReady(Information &&information);\n\tvoid streamFailed(Error error);\n\tvoid start();\n\tvoid stop(bool stillActive);\n\tvoid provideStartInformation();\n\tvoid fail(Error error);\n\tvoid checkVideoStep();\n\tvoid checkNextFrameRender();\n\tvoid checkNextFrameAvailability();\n\tvoid renderFrame(crl::time now);\n\tvoid audioReceivedTill(crl::time position);\n\tvoid audioPlayedTill(crl::time position);\n\tvoid videoReceivedTill(crl::time position);\n\tvoid videoPlayedTill(crl::time position);\n\n\tvoid updatePausedState();\n\t[[nodiscard]] bool trackReceivedEnough(\n\t\tconst TrackState &state,\n\t\tcrl::time amount) const;\n\t[[nodiscard]] bool bothReceivedEnough(crl::time amount) const;\n\t[[nodiscard]] bool receivedTillEnd() const;\n\tvoid checkResumeFromWaitingForData();\n\t[[nodiscard]] crl::time getCurrentReceivedTill(crl::time duration) const;\n\tvoid savePreviousReceivedTill(\n\t\tconst PlaybackOptions &options,\n\t\tcrl::time previousReceivedTill);\n\t[[nodiscard]] crl::time loadInAdvanceFor() const;\n\n\ttemplate <typename Track>\n\tint durationByPacket(const Track &track, const FFmpeg::Packet &packet);\n\n\t// Valid after fileReady call ends. Thread-safe.\n\t[[nodiscard]] crl::time computeAudioDuration() const;\n\t[[nodiscard]] crl::time computeVideoDuration() const;\n\t[[nodiscard]] crl::time computeTotalDuration() const;\n\tvoid setDurationByPackets();\n\n\ttemplate <typename Track>\n\tvoid trackReceivedTill(\n\t\tconst Track &track,\n\t\tTrackState &state,\n\t\tcrl::time position);\n\n\ttemplate <typename Track>\n\tvoid trackSendReceivedTill(\n\t\tconst Track &track,\n\t\tTrackState &state);\n\n\ttemplate <typename Track>\n\tvoid trackPlayedTill(\n\t\tconst Track &track,\n\t\tTrackState &state,\n\t\tcrl::time position);\n\n\tconst std::unique_ptr<File> _file;\n\n\t// Immutable while File is active after it is ready.\n\tAudioMsgId _audioId;\n\tstd::unique_ptr<AudioTrack> _audio;\n\tstd::unique_ptr<VideoTrack> _video;\n\n\t// Immutable while File is active.\n\tbase::has_weak_ptr _sessionGuard;\n\n\t// Immutable while File is active except '.speed'.\n\t// '.speed' is changed from the main thread.\n\tPlaybackOptions _options;\n\n\t// Belongs to the File thread while File is active.\n\tbool _readTillEnd = false;\n\tbool _waitingForData = false;\n\n\tstd::atomic<bool> _pauseReading = false;\n\n\t// Belongs to the main thread.\n\tInformation _information;\n\tStage _stage = Stage::Uninitialized;\n\tstd::optional<Error> _lastFailure;\n\tbool _pausedByUser = false;\n\tbool _pausedByWaitingForData = false;\n\tbool _paused = false;\n\tbool _audioFinished = false;\n\tbool _videoFinished = false;\n\tbool _remoteLoader = false;\n\n\tcrl::time _startedTime = kTimeUnknown;\n\tcrl::time _pausedTime = kTimeUnknown;\n\tcrl::time _currentFrameTime = kTimeUnknown;\n\tcrl::time _nextFrameTime = kTimeUnknown;\n\tbase::Timer _renderFrameTimer;\n\trpl::event_stream<Update, Error> _updates;\n\trpl::event_stream<bool> _fullInCache;\n\tstd::optional<bool> _fullInCacheSinceStart;\n\n\tcrl::time _totalDuration = kTimeUnknown;\n\tcrl::time _loopingShift = 0;\n\tcrl::time _previousReceivedTill = kTimeUnknown;\n\tstd::atomic<int> _durationByPackets = 0;\n\tint _durationByLastAudioPacket = 0;\n\tint _durationByLastVideoPacket = 0;\n\n\tint _locks = 0;\n\n\trpl::lifetime _lifetime;\n\trpl::lifetime _sessionLifetime;\n\n};\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/streaming/media_streaming_reader.h\"\n\n#include \"media/streaming/media_streaming_common.h\"\n#include \"media/streaming/media_streaming_loader.h\"\n#include \"storage/cache/storage_cache_database.h\"\n\nnamespace Media {\nnamespace Streaming {\nnamespace {\n\nconstexpr auto kPartSize = Loader::kPartSize;\nconstexpr auto kPartsInSlice = 64;\nconstexpr auto kInSlice = uint32(kPartsInSlice * kPartSize);\nconstexpr auto kMaxPartsInHeader = 64;\nconstexpr auto kMaxOnlyInHeader = 80 * kPartSize;\nconstexpr auto kPartsOutsideFirstSliceGood = 8;\nconstexpr auto kSlicesInMemory = 2;\n\n// 1 MB of parts are requested from cloud ahead of reading demand.\nconstexpr auto kPreloadPartsAhead = 8;\nconstexpr auto kDownloaderRequestsLimit = 4;\n\nusing PartsMap = base::flat_map<uint32, QByteArray>;\n\nstruct ParsedCacheEntry {\n\tPartsMap parts;\n\tstd::optional<PartsMap> included;\n};\n\nbool IsContiguousSerialization(int serializedSize, int maxSliceSize) {\n\treturn !(serializedSize % kPartSize) || (serializedSize == maxSliceSize);\n}\n\nbool IsFullInHeader(int64 size) {\n\treturn (size <= kMaxOnlyInHeader);\n}\n\nbool ComputeIsGoodHeader(int64 size, const PartsMap &header) {\n\tif (IsFullInHeader(size)) {\n\t\treturn false;\n\t}\n\tconst auto outsideFirstSliceIt = ranges::lower_bound(\n\t\theader,\n\t\tkInSlice,\n\t\tranges::less(),\n\t\t&PartsMap::value_type::first);\n\tconst auto outsideFirstSlice = end(header) - outsideFirstSliceIt;\n\treturn (outsideFirstSlice <= kPartsOutsideFirstSliceGood);\n}\n\nint SlicesCount(uint32 size) {\n\tconst auto result = (size + kInSlice - 1) / kInSlice;\n\n\tEnsures(result < 0x1FFU);\n\treturn result;\n}\n\nint MaxSliceSize(int sliceNumber, uint32 size) {\n\treturn !sliceNumber\n\t\t? size\n\t\t: (sliceNumber == SlicesCount(size))\n\t\t? (size - (sliceNumber - 1) * kInSlice)\n\t\t: kInSlice;\n}\n\nbytes::const_span ParseComplexCachedMap(\n\t\tPartsMap &result,\n\t\tbytes::const_span data,\n\t\tint maxSize) {\n\tconst auto takeInt = [&]() -> std::optional<uint32> {\n\t\tif (data.size() < sizeof(uint32)) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\tconst auto bytes = data.data();\n\t\tconst auto result = *reinterpret_cast<const uint32*>(bytes);\n\t\tdata = data.subspan(sizeof(uint32));\n\t\treturn result;\n\t};\n\tconst auto takeBytes = [&](int count) {\n\t\tif (count <= 0 || data.size() < count) {\n\t\t\treturn bytes::const_span();\n\t\t}\n\t\tconst auto result = data.subspan(0, count);\n\t\tdata = data.subspan(count);\n\t\treturn result;\n\t};\n\tconst auto maybeCount = takeInt();\n\tif (!maybeCount) {\n\t\treturn {};\n\t}\n\tconst auto count = *maybeCount;\n\tif (!count || count > (kMaxOnlyInHeader / kPartSize)) {\n\t\treturn data;\n\t}\n\tfor (auto i = 0; i != count; ++i) {\n\t\tconst auto offset = takeInt().value_or(0);\n\t\tconst auto size = takeInt().value_or(0);\n\t\tconst auto bytes = takeBytes(size);\n\t\tif (offset >= maxSize\n\t\t\t|| !size\n\t\t\t|| size > maxSize\n\t\t\t|| offset + size > maxSize\n\t\t\t|| bytes.size() != size) {\n\t\t\treturn {};\n\t\t}\n\t\tresult.try_emplace(\n\t\t\toffset,\n\t\t\treinterpret_cast<const char*>(bytes.data()),\n\t\t\tbytes.size());\n\t}\n\treturn data;\n}\n\nbytes::const_span ParseCachedMap(\n\t\tPartsMap &result,\n\t\tbytes::const_span data,\n\t\tint maxSize) {\n\tconst auto size = int(data.size());\n\tif (IsContiguousSerialization(size, maxSize)) {\n\t\tif (size > maxSize) {\n\t\t\treturn {};\n\t\t}\n\t\tfor (auto offset = int64(); offset < size; offset += kPartSize) {\n\t\t\tconst auto part = data.subspan(\n\t\t\t\toffset,\n\t\t\t\tstd::min(kPartSize, size - offset));\n\t\t\tresult.try_emplace(\n\t\t\t\tuint32(offset),\n\t\t\t\treinterpret_cast<const char*>(part.data()),\n\t\t\t\tpart.size());\n\t\t}\n\t\treturn {};\n\t}\n\treturn ParseComplexCachedMap(result, data, maxSize);\n}\n\nParsedCacheEntry ParseCacheEntry(\n\t\tbytes::const_span data,\n\t\tint sliceNumber,\n\t\tint64 size) {\n\tauto result = ParsedCacheEntry();\n\tconst auto remaining = ParseCachedMap(\n\t\tresult.parts,\n\t\tdata,\n\t\tMaxSliceSize(sliceNumber, size));\n\tif (!sliceNumber && ComputeIsGoodHeader(size, result.parts)) {\n\t\tresult.included = PartsMap();\n\t\tParseCachedMap(*result.included, remaining, MaxSliceSize(1, size));\n\t}\n\treturn result;\n}\n\ntemplate <typename Range> // Range::value_type is Pair<int, QByteArray>\nuint32 FindNotLoadedStart(Range &&parts, uint32 offset) {\n\tauto result = offset;\n\tfor (const auto &part : parts) {\n\t\tconst auto partStart = part.first;\n\t\tconst auto partEnd = partStart + part.second.size();\n\t\tif (partStart <= result && partEnd >= result) {\n\t\t\tresult = partEnd;\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn result;\n}\n\ntemplate <typename Range> // Range::value_type is Pair<uint32, QByteArray>\nvoid CopyLoaded(\n\t\tbytes::span buffer,\n\t\tRange &&parts,\n\t\tuint32 offset,\n\t\tuint32 till) {\n\tauto filled = offset;\n\tfor (const auto &part : parts) {\n\t\tconst auto bytes = bytes::make_span(part.second);\n\t\tconst auto partStart = part.first;\n\t\tconst auto partEnd = uint32(partStart + bytes.size());\n\t\tconst auto copyTill = std::min(partEnd, till);\n\t\tAssert(partStart <= filled && filled < copyTill);\n\n\t\tconst auto from = filled - partStart;\n\t\tconst auto copy = copyTill - filled;\n\t\tbytes::copy(buffer, bytes.subspan(from, copy));\n\t\tbuffer = buffer.subspan(copy);\n\t\tfilled += copy;\n\t}\n}\n\n} // namespace\n\ntemplate <int Size>\nbool Reader::StackIntVector<Size>::add(uint32 value) {\n\tusing namespace rpl::mappers;\n\n\tconst auto i = ranges::find_if(_storage, _1 == uint32(-1));\n\tif (i == end(_storage)) {\n\t\treturn false;\n\t}\n\t*i = value;\n\tconst auto next = i + 1;\n\tif (next != end(_storage)) {\n\t\t*next = -1;\n\t}\n\treturn true;\n}\n\ntemplate <int Size>\nauto Reader::StackIntVector<Size>::values() const {\n\tusing namespace rpl::mappers;\n\n\treturn ranges::views::all(\n\t\t_storage\n\t) | ranges::views::take_while(_1 != uint32(-1));\n}\n\nstruct Reader::CacheHelper {\n\texplicit CacheHelper(Storage::Cache::Key baseKey);\n\n\tStorage::Cache::Key key(int sliceNumber) const;\n\n\tconst Storage::Cache::Key baseKey;\n\n\tQMutex mutex;\n\tbase::flat_map<uint32, PartsMap> results;\n\tstd::vector<int> sizes;\n\tstd::atomic<crl::semaphore*> waiting = nullptr;\n};\n\nReader::CacheHelper::CacheHelper(Storage::Cache::Key baseKey)\n: baseKey(baseKey) {\n}\n\nStorage::Cache::Key Reader::CacheHelper::key(int sliceNumber) const {\n\treturn Storage::Cache::Key{ baseKey.high, baseKey.low + sliceNumber };\n}\n\nvoid Reader::Slice::processCacheData(PartsMap &&data) {\n\tExpects((flags & Flag::LoadingFromCache) != 0);\n\tExpects(!(flags & Flag::LoadedFromCache));\n\n\tconst auto guard = gsl::finally([&] {\n\t\tflags |= Flag::LoadedFromCache;\n\t\tflags &= ~Flag::LoadingFromCache;\n\t});\n\tif (parts.empty()) {\n\t\tparts = std::move(data);\n\t} else {\n\t\tfor (auto &[offset, bytes] : data) {\n\t\t\tparts.emplace(offset, std::move(bytes));\n\t\t}\n\t}\n}\n\nvoid Reader::Slice::addPart(uint32 offset, QByteArray bytes) {\n\tExpects(!parts.contains(offset));\n\n\tparts.emplace(offset, std::move(bytes));\n\tif (flags & Flag::LoadedFromCache) {\n\t\tflags |= Flag::ChangedSinceCache;\n\t}\n}\n\nauto Reader::Slice::prepareFill(\n\t\tuint32 from,\n\t\tuint32 till) -> PrepareFillResult {\n\tauto result = PrepareFillResult();\n\n\tresult.ready = false;\n\tconst auto fromOffset = (from / kPartSize) * kPartSize;\n\tconst auto tillPart = (till + kPartSize - 1) / kPartSize;\n\tconst auto preloadTillOffset = (tillPart + kPreloadPartsAhead)\n\t\t* kPartSize;\n\n\tconst auto after = ranges::upper_bound(\n\t\tparts,\n\t\tfrom,\n\t\tranges::less(),\n\t\t&PartsMap::value_type::first);\n\tif (after == begin(parts)) {\n\t\tresult.offsetsFromLoader = offsetsFromLoader(\n\t\t\tfromOffset,\n\t\t\tpreloadTillOffset);\n\t\treturn result;\n\t}\n\n\tconst auto start = after - 1;\n\tconst auto finish = ranges::lower_bound(\n\t\tstart,\n\t\tend(parts),\n\t\ttill,\n\t\tranges::less(),\n\t\t&PartsMap::value_type::first);\n\tconst auto haveTill = FindNotLoadedStart(\n\t\tranges::make_subrange(start, finish),\n\t\tfromOffset);\n\tif (haveTill < till) {\n\t\tresult.offsetsFromLoader = offsetsFromLoader(\n\t\t\thaveTill,\n\t\t\tpreloadTillOffset);\n\t\treturn result;\n\t}\n\tresult.ready = true;\n\tresult.start = start;\n\tresult.finish = finish;\n\tresult.offsetsFromLoader = offsetsFromLoader(\n\t\ttillPart * kPartSize,\n\t\tpreloadTillOffset);\n\treturn result;\n}\n\nauto Reader::Slice::offsetsFromLoader(uint32 from, uint32 till) const\n-> StackIntVector<Reader::kLoadFromRemoteMax> {\n\tauto result = StackIntVector<kLoadFromRemoteMax>();\n\n\tconst auto after = ranges::upper_bound(\n\t\tparts,\n\t\tfrom,\n\t\tranges::less(),\n\t\t&PartsMap::value_type::first);\n\tauto check = (after == begin(parts)) ? after : (after - 1);\n\tconst auto end = parts.end();\n\tfor (auto offset = from; offset != till; offset += kPartSize) {\n\t\twhile (check != end && check->first < offset) {\n\t\t\t++check;\n\t\t}\n\t\tif (check != end && check->first == offset) {\n\t\t\tcontinue;\n\t\t} else if (!result.add(offset)) {\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn result;\n}\n\nReader::Slices::Slices(uint32 size, bool useCache)\n: _size(size) {\n\tExpects(size > 0);\n\n\tif (useCache) {\n\t\t_header.flags |= Slice::Flag::LoadingFromCache;\n\t} else {\n\t\t_headerMode = HeaderMode::NoCache;\n\t}\n\tif (!isFullInHeader()) {\n\t\t_data.resize(SlicesCount(_size));\n\t}\n}\n\nbool Reader::Slices::headerModeUnknown() const {\n\treturn (_headerMode == HeaderMode::Unknown);\n}\n\nbool Reader::Slices::isFullInHeader() const {\n\treturn IsFullInHeader(_size);\n}\n\nbool Reader::Slices::isGoodHeader() const {\n\treturn (_headerMode == HeaderMode::Good);\n}\n\nbool Reader::Slices::computeIsGoodHeader() const {\n\treturn ComputeIsGoodHeader(_size, _header.parts);\n}\n\nvoid Reader::Slices::headerDone(bool fromCache) {\n\tif (_headerMode != HeaderMode::Unknown) {\n\t\treturn;\n\t}\n\t_headerMode = isFullInHeader()\n\t\t? HeaderMode::Full\n\t\t: computeIsGoodHeader()\n\t\t? HeaderMode::Good\n\t\t: HeaderMode::Small;\n\tif (!fromCache) {\n\t\tfor (auto &slice : _data) {\n\t\t\tusing Flag = Slice::Flag;\n\t\t\tAssert(!(slice.flags\n\t\t\t\t& (Flag::LoadingFromCache | Flag::LoadedFromCache)));\n\t\t\tslice.flags |= Slice::Flag::LoadedFromCache;\n\t\t}\n\t}\n}\n\nint Reader::Slices::headerSize() const {\n\treturn _header.parts.size() * kPartSize;\n}\n\nbool Reader::Slices::fullInCache() const {\n\treturn _fullInCache;\n}\n\nint Reader::Slices::requestSliceSizesCount() const {\n\tif (!headerModeUnknown() || isFullInHeader()) {\n\t\treturn 0;\n\t}\n\treturn _data.size();\n}\n\nbool Reader::Slices::headerWontBeFilled() const {\n\treturn headerModeUnknown()\n\t\t&& (_header.parts.size() >= kMaxPartsInHeader);\n}\n\nvoid Reader::Slices::applyHeaderCacheData() {\n\tusing namespace rpl::mappers;\n\n\tconst auto applyWhile = [&](auto &&predicate) {\n\t\tfor (const auto &[offset, part] : _header.parts) {\n\t\t\tconst auto index = int(offset / kInSlice);\n\t\t\tif (!predicate(index)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t_data[index].addPart(\n\t\t\t\toffset - index * kInSlice,\n\t\t\t\tbase::duplicate(part));\n\t\t}\n\t};\n\tif (_header.parts.empty()) {\n\t\treturn;\n\t} else if (_headerMode == HeaderMode::Good) {\n\t\t// Always apply data to first block if it is cached in the header.\n\t\tapplyWhile(_1 == 0);\n\t} else if (_headerMode != HeaderMode::Unknown) {\n\t\treturn;\n\t} else if (isFullInHeader()) {\n\t\theaderDone(true);\n\t} else {\n\t\tapplyWhile(_1 < int(_data.size()));\n\t\theaderDone(true);\n\t}\n}\n\nvoid Reader::Slices::processCacheResult(int sliceNumber, PartsMap &&result) {\n\tExpects(sliceNumber >= 0 && sliceNumber <= _data.size());\n\n\tauto &slice = (sliceNumber ? _data[sliceNumber - 1] : _header);\n\tif (!sliceNumber && isGoodHeader()) {\n\t\t// We've loaded header slice because really we wanted first slice.\n\t\tif (!(_data[0].flags & Slice::Flag::LoadingFromCache)) {\n\t\t\t// We could've already unloaded this slice using LRU _usedSlices.\n\t\t\treturn;\n\t\t}\n\t\t// So just process whole result even if we didn't want header really.\n\t\tslice.flags |= Slice::Flag::LoadingFromCache;\n\t\tslice.flags &= ~Slice::Flag::LoadedFromCache;\n\t}\n\tif (!(slice.flags & Slice::Flag::LoadingFromCache)) {\n\t\t// We could've already unloaded this slice using LRU _usedSlices.\n\t\treturn;\n\t}\n\tslice.processCacheData(std::move(result));\n\tcheckSliceFullLoaded(sliceNumber);\n\tif (!sliceNumber) {\n\t\tapplyHeaderCacheData();\n\t\tif (isGoodHeader()) {\n\t\t\t// When we first read header we don't request the first slice.\n\t\t\t// But we get it, so let's apply it anyway.\n\t\t\t_data[0].flags |= Slice::Flag::LoadingFromCache;\n\t\t}\n\t}\n}\n\nvoid Reader::Slices::processCachedSizes(const std::vector<int> &sizes) {\n\tExpects(sizes.size() == _data.size());\n\n\tusing Flag = Slice::Flag;\n\tconst auto count = int(sizes.size());\n\tauto loadedCount = 0;\n\tfor (auto i = 0; i != count; ++i) {\n\t\tconst auto sliceNumber = (i + 1);\n\t\tconst auto sliceSize = (sliceNumber < _data.size())\n\t\t\t? kInSlice\n\t\t\t: (_size - (sliceNumber - 1) * kInSlice);\n\t\tconst auto loaded = (sizes[i] == sliceSize);\n\n\t\tif (_data[i].flags & Flag::FullInCache) {\n\t\t\t++loadedCount;\n\t\t} else if (loaded) {\n\t\t\t_data[i].flags |= Flag::FullInCache;\n\t\t\t++loadedCount;\n\t\t}\n\t}\n\t_fullInCache = (loadedCount == count);\n}\n\nvoid Reader::Slices::checkSliceFullLoaded(int sliceNumber) {\n\tif (!sliceNumber && !isFullInHeader()) {\n\t\treturn;\n\t}\n\tconst auto partsCount = [&] {\n\t\tif (!sliceNumber) {\n\t\t\treturn (_size + kPartSize - 1) / kPartSize;\n\t\t}\n\t\treturn (sliceNumber < _data.size())\n\t\t\t? kPartsInSlice\n\t\t\t: ((_size - (sliceNumber - 1) * kInSlice + kPartSize - 1)\n\t\t\t\t/ kPartSize);\n\t}();\n\tauto &slice = (sliceNumber ? _data[sliceNumber - 1] : _header);\n\tconst auto loaded = (slice.parts.size() == partsCount);\n\n\tusing Flag = Slice::Flag;\n\tif ((slice.flags & Flag::FullInCache) && !loaded) {\n\t\tslice.flags &= ~Flag::FullInCache;\n\t\t_fullInCache = false;\n\t} else if (!(slice.flags & Flag::FullInCache) && loaded) {\n\t\tslice.flags |= Flag::FullInCache;\n\t\t_fullInCache = checkFullInCache();\n\t}\n}\n\nbool Reader::Slices::checkFullInCache() const {\n\tusing Flag = Slice::Flag;\n\tif (isFullInHeader()) {\n\t\treturn (_header.flags & Flag::FullInCache);\n\t}\n\treturn ranges::none_of(_data, [](const Slice &slice) {\n\t\treturn !(slice.flags & Flag::FullInCache);\n\t});\n}\n\nvoid Reader::Slices::processPart(\n\t\tuint32 offset,\n\t\tQByteArray &&bytes) {\n\tExpects(isFullInHeader() || (offset / kInSlice < _data.size()));\n\n\tif (isFullInHeader()) {\n\t\t_header.addPart(offset, bytes);\n\t\tcheckSliceFullLoaded(0);\n\t\treturn;\n\t//} else if (_headerMode == HeaderMode::Unknown) {\n\t//\tif (_header.parts.contains(offset)) {\n\t//\t\treturn;\n\t//\t} else if (_header.parts.size() < kMaxPartsInHeader) {\n\t//\t\t_header.addPart(offset, bytes);\n\t//\t}\n\t}\n\tconst auto index = offset / kInSlice;\n\t_data[index].addPart(offset - index * kInSlice, std::move(bytes));\n\tcheckSliceFullLoaded(index + 1);\n}\n\nauto Reader::Slices::fill(uint32 offset, bytes::span buffer) -> FillResult {\n\tExpects(!buffer.empty());\n\tExpects(offset < _size);\n\tExpects(offset + buffer.size() <= _size);\n\tExpects(buffer.size() <= kInSlice);\n\n\tusing Flag = Slice::Flag;\n\n\tif (_headerMode != HeaderMode::NoCache\n\t\t&& !(_header.flags & Flag::LoadedFromCache)) {\n\t\t// Waiting for initial cache query.\n\t\tAssert(waitingForHeaderCache());\n\t\treturn {};\n\t} else if (isFullInHeader()) {\n\t\treturn fillFromHeader(offset, buffer);\n\t}\n\n\tauto result = FillResult();\n\tconst auto till = uint32(offset + buffer.size());\n\tconst auto fromSlice = offset / kInSlice;\n\tconst auto tillSlice = (till + kInSlice - 1) / kInSlice;\n\tAssert((fromSlice + 1 == tillSlice || fromSlice + 2 == tillSlice)\n\t\t&& tillSlice <= _data.size());\n\n\tconst auto cacheNotLoaded = [&](int sliceIndex) {\n\t\treturn (_headerMode != HeaderMode::NoCache)\n\t\t\t&& (_headerMode != HeaderMode::Unknown)\n\t\t\t&& !(_data[sliceIndex].flags & Flag::LoadedFromCache);\n\t};\n\tconst auto handlePrepareResult = [&](\n\t\t\tint sliceIndex,\n\t\t\tconst Slice::PrepareFillResult &prepared) {\n\t\tif (cacheNotLoaded(sliceIndex)) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const auto offset : prepared.offsetsFromLoader.values()) {\n\t\t\tconst auto full = offset + sliceIndex * kInSlice;\n\t\t\tif (offset < kInSlice && full < _size) {\n\t\t\t\tresult.offsetsFromLoader.add(full);\n\t\t\t}\n\t\t}\n\t};\n\tconst auto handleReadFromCache = [&](int sliceIndex) {\n\t\tif (cacheNotLoaded(sliceIndex)) {\n\t\t\tif (!(_data[sliceIndex].flags & Flag::LoadingFromCache)) {\n\t\t\t\t_data[sliceIndex].flags |= Flag::LoadingFromCache;\n\t\t\t\tresult.sliceNumbersFromCache.add(sliceIndex + 1);\n\t\t\t}\n\t\t\tresult.state = FillState::WaitingCache;\n\t\t}\n\t};\n\tconst auto addToHeader = [&](int slice, auto parts) {\n\t\tif (_headerMode == HeaderMode::Unknown) {\n\t\t\tfor (const auto &part : parts) {\n\t\t\t\tconst auto totalOffset = slice * kInSlice + part.first;\n\t\t\t\tif (!_header.parts.contains(totalOffset)\n\t\t\t\t\t&& _header.parts.size() < kMaxPartsInHeader) {\n\t\t\t\t\t_header.addPart(totalOffset, part.second);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\tconst auto firstFrom = offset - fromSlice * kInSlice;\n\tconst auto firstTill = std::min(kInSlice, till - fromSlice * kInSlice);\n\tconst auto secondFrom = 0;\n\tconst auto secondTill = (till > (fromSlice + 1) * kInSlice)\n\t\t? (till - (fromSlice + 1) * kInSlice)\n\t\t: 0;\n\tconst auto first = _data[fromSlice].prepareFill(firstFrom, firstTill);\n\tconst auto second = (fromSlice + 1 < tillSlice)\n\t\t? _data[fromSlice + 1].prepareFill(secondFrom, secondTill)\n\t\t: Slice::PrepareFillResult();\n\thandlePrepareResult(fromSlice, first);\n\tif (fromSlice + 1 < tillSlice) {\n\t\thandlePrepareResult(fromSlice + 1, second);\n\t}\n\tif (first.ready && second.ready) {\n\t\tmarkSliceUsed(fromSlice);\n\t\tauto &&list = ranges::make_subrange(first.start, first.finish);\n\t\tCopyLoaded(buffer, list, firstFrom, firstTill);\n\t\taddToHeader(fromSlice, list);\n\t\tif (fromSlice + 1 < tillSlice) {\n\t\t\tmarkSliceUsed(fromSlice + 1);\n\t\t\tauto &&list = ranges::make_subrange(second.start, second.finish);\n\t\t\tCopyLoaded(\n\t\t\t\tbuffer.subspan(firstTill - firstFrom),\n\t\t\t\tlist,\n\t\t\t\tsecondFrom,\n\t\t\t\tsecondTill);\n\t\t\taddToHeader(fromSlice + 1, list);\n\t\t}\n\t\tresult.toCache = serializeAndUnloadUnused();\n\t\tresult.state = FillState::Success;\n\t} else {\n\t\thandleReadFromCache(fromSlice);\n\t\tif (fromSlice + 1 < tillSlice) {\n\t\t\thandleReadFromCache(fromSlice + 1);\n\t\t}\n\t}\n\treturn result;\n}\n\nauto Reader::Slices::fillFromHeader(uint32 offset, bytes::span buffer)\n-> FillResult {\n\tauto result = FillResult();\n\tconst auto from = offset;\n\tconst auto till = uint32(offset + buffer.size());\n\n\tconst auto prepared = _header.prepareFill(from, till);\n\tfor (const auto full : prepared.offsetsFromLoader.values()) {\n\t\tif (full < _size) {\n\t\t\tresult.offsetsFromLoader.add(full);\n\t\t}\n\t}\n\tif (prepared.ready) {\n\t\tCopyLoaded(\n\t\t\tbuffer,\n\t\t\tranges::make_subrange(prepared.start, prepared.finish),\n\t\t\tfrom,\n\t\t\ttill);\n\t\tresult.state = FillState::Success;\n\t}\n\treturn result;\n}\n\nQByteArray Reader::Slices::partForDownloader(uint32 offset) const {\n\tExpects(offset < _size);\n\n\tif (const auto i = _header.parts.find(offset); i != end(_header.parts)) {\n\t\treturn i->second;\n\t} else if (isFullInHeader()) {\n\t\treturn QByteArray();\n\t}\n\tconst auto index = offset / kInSlice;\n\tconst auto &slice = _data[index];\n\tconst auto i = slice.parts.find(offset - index * kInSlice);\n\treturn (i != end(slice.parts)) ? i->second : QByteArray();\n}\n\nbool Reader::Slices::waitingForHeaderCache() const {\n\treturn (_header.flags & Slice::Flag::LoadingFromCache);\n}\n\nbool Reader::Slices::readCacheForDownloaderRequired(uint32 offset) {\n\tExpects(offset < _size);\n\tExpects(!waitingForHeaderCache());\n\n\tif (isFullInHeader()) {\n\t\treturn false;\n\t}\n\tconst auto index = offset / kInSlice;\n\tauto &slice = _data[index];\n\treturn !(slice.flags & Slice::Flag::LoadedFromCache);\n}\n\nvoid Reader::Slices::markSliceUsed(int sliceIndex) {\n\tconst auto i = ranges::find(_usedSlices, sliceIndex);\n\tconst auto end = _usedSlices.end();\n\tif (i == end) {\n\t\t_usedSlices.push_back(sliceIndex);\n\t} else {\n\t\tconst auto next = i + 1;\n\t\tif (next != end) {\n\t\t\tstd::rotate(i, next, end);\n\t\t}\n\t}\n}\n\nint Reader::Slices::maxSliceSize(int sliceNumber) const {\n\treturn MaxSliceSize(sliceNumber, _size);\n}\n\nReader::SerializedSlice Reader::Slices::serializeAndUnloadUnused() {\n\tusing Flag = Slice::Flag;\n\n\tif (_headerMode == HeaderMode::Unknown\n\t\t|| _usedSlices.size() <= kSlicesInMemory) {\n\t\treturn {};\n\t}\n\tconst auto purgeSlice = _usedSlices.front();\n\t_usedSlices.pop_front();\n\tif (!(_data[purgeSlice].flags & Flag::LoadedFromCache)) {\n\t\t// If the only data in this slice was from _header, just leave it.\n\t\treturn {};\n\t}\n\tconst auto noNeedToSaveToCache = [&] {\n\t\tif (_headerMode == HeaderMode::NoCache) {\n\t\t\t// Cache is not used.\n\t\t\treturn true;\n\t\t} else if (!(_data[purgeSlice].flags & Flag::ChangedSinceCache)) {\n\t\t\t// If no data was changed we should still save first slice,\n\t\t\t// if header data was changed since loading from cache.\n\t\t\t// Otherwise in destructor we won't be able to unload header.\n\t\t\tif (!isGoodHeader()\n\t\t\t\t|| (purgeSlice > 0)\n\t\t\t\t|| (!(_header.flags & Flag::ChangedSinceCache))) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}();\n\tif (noNeedToSaveToCache) {\n\t\tunloadSlice(_data[purgeSlice]);\n\t\treturn {};\n\t}\n\treturn serializeAndUnloadSlice(purgeSlice + 1);\n}\n\nReader::SerializedSlice Reader::Slices::serializeAndUnloadSlice(\n\t\tint sliceNumber) {\n\tExpects(_headerMode != HeaderMode::Unknown);\n\tExpects(_headerMode != HeaderMode::NoCache);\n\tExpects(sliceNumber >= 0 && sliceNumber <= _data.size());\n\n\tif (isGoodHeader() && (sliceNumber == 1)) {\n\t\treturn serializeAndUnloadSlice(0);\n\t}\n\tconst auto writeHeaderAndSlice = isGoodHeader() && !sliceNumber;\n\n\tauto &slice = sliceNumber ? _data[sliceNumber - 1] : _header;\n\tconst auto count = slice.parts.size();\n\tAssert(count > 0);\n\n\tauto result = SerializedSlice();\n\tresult.number = sliceNumber;\n\n\t// We always use complex serialization for header + first slice.\n\tconst auto continuousTill = writeHeaderAndSlice\n\t\t? 0\n\t\t: FindNotLoadedStart(slice.parts, 0);\n\tconst auto continuous = (continuousTill > slice.parts.back().first);\n\tif (continuous) {\n\t\t// All data is continuous.\n\t\tresult.data.reserve(count * kPartSize);\n\t\tfor (const auto &[offset, part] : slice.parts) {\n\t\t\tresult.data.append(part);\n\t\t}\n\t} else {\n\t\tresult.data = serializeComplexSlice(slice);\n\t\tif (writeHeaderAndSlice) {\n\t\t\tresult.data.append(serializeAndUnloadFirstSliceNoHeader());\n\t\t}\n\n\t\t// Make sure this data won't be taken for full continuous data.\n\t\tconst auto maxSize = maxSliceSize(sliceNumber);\n\t\twhile (IsContiguousSerialization(result.data.size(), maxSize)) {\n\t\t\tresult.data.push_back(char(0));\n\t\t}\n\t}\n\n\t// We may serialize header in the middle of streaming, if we use\n\t// HeaderMode::Good and we unload first slice. We still require\n\t// header data to continue working, so don't really unload the header.\n\tif (sliceNumber) {\n\t\tunloadSlice(slice);\n\t} else {\n\t\tslice.flags &= ~Slice::Flag::ChangedSinceCache;\n\t}\n\treturn result;\n}\n\nvoid Reader::Slices::unloadSlice(Slice &slice) const {\n\tconst auto full = (slice.flags & Slice::Flag::FullInCache);\n\tslice = Slice();\n\tif (full) {\n\t\tslice.flags |= Slice::Flag::FullInCache;\n\t}\n}\n\nQByteArray Reader::Slices::serializeComplexSlice(const Slice &slice) const {\n\treturn SerializeComplexPartsMap(slice.parts);\n}\n\nQByteArray Reader::Slices::serializeAndUnloadFirstSliceNoHeader() {\n\tExpects(_data[0].flags & Slice::Flag::LoadedFromCache);\n\n\tauto &slice = _data[0];\n\tfor (const auto &[offset, part] : _header.parts) {\n\t\tslice.parts.erase(offset);\n\t}\n\tauto result = serializeComplexSlice(slice);\n\tunloadSlice(slice);\n\treturn result;\n}\n\nReader::SerializedSlice Reader::Slices::unloadToCache() {\n\tif (_headerMode == HeaderMode::Unknown\n\t\t|| _headerMode == HeaderMode::NoCache) {\n\t\treturn {};\n\t}\n\tif (_header.flags & Slice::Flag::ChangedSinceCache) {\n\t\treturn serializeAndUnloadSlice(0);\n\t}\n\tfor (auto i = 0, count = int(_data.size()); i != count; ++i) {\n\t\tif (_data[i].flags & Slice::Flag::ChangedSinceCache) {\n\t\t\treturn serializeAndUnloadSlice(i + 1);\n\t\t}\n\t}\n\treturn {};\n}\n\nReader::Reader(\n\tstd::unique_ptr<Loader> loader,\n\tStorage::Cache::Database *cache)\n: _loader(std::move(loader))\n, _cache(cache)\n, _cacheHelper(cache ? InitCacheHelper(_loader->baseCacheKey()) : nullptr)\n, _slices(_loader->size(), _cacheHelper != nullptr) {\n\t_loader->parts(\n\t) | rpl::on_next([=](LoadedPart &&part) {\n\t\tif (_attachedDownloader) {\n\t\t\t_partsForDownloader.fire_copy(part);\n\t\t}\n\t\tif (_streamingActive) {\n\t\t\t_loadedParts.emplace(std::move(part));\n\t\t}\n\t\tif (const auto waiting = _waiting.load(std::memory_order_acquire)) {\n\t\t\t_waiting.store(nullptr, std::memory_order_release);\n\t\t\twaiting->release();\n\t\t}\n\t}, _lifetime);\n\n\tif (_cacheHelper) {\n\t\treadFromCache(0);\n\t}\n}\n\nvoid Reader::startSleep(not_null<crl::semaphore*> wake) {\n\t_sleeping.store(wake, std::memory_order_release);\n\tprocessDownloaderRequests();\n}\n\nvoid Reader::wakeFromSleep() {\n\tif (const auto sleeping = _sleeping.load(std::memory_order_acquire)) {\n\t\t_sleeping.store(nullptr, std::memory_order_release);\n\t\tsleeping->release();\n\t}\n}\n\nvoid Reader::stopSleep() {\n\t_sleeping.store(nullptr, std::memory_order_release);\n}\n\nvoid Reader::stopStreamingAsync() {\n\t_stopStreamingAsync = true;\n\tcrl::on_main(this, [=] {\n\t\tif (_stopStreamingAsync) {\n\t\t\tstopStreaming(false);\n\t\t}\n\t});\n}\n\nvoid Reader::tryRemoveLoaderAsync() {\n\t_loader->tryRemoveFromQueue();\n}\n\nvoid Reader::startStreaming() {\n\t_streamingActive = true;\n\trefreshLoaderPriority();\n}\n\nvoid Reader::stopStreaming(bool stillActive) {\n\tExpects(_sleeping == nullptr);\n\n\t_stopStreamingAsync = false;\n\t_waiting.store(nullptr, std::memory_order_release);\n\tif (_cacheHelper && _cacheHelper->waiting != nullptr) {\n\t\tQMutexLocker lock(&_cacheHelper->mutex);\n\t\t_cacheHelper->waiting.store(nullptr, std::memory_order_release);\n\t}\n\tif (!stillActive) {\n\t\t_streamingActive = false;\n\t\trefreshLoaderPriority();\n\t\t_loadingOffsets.clear();\n\t\tprocessDownloaderRequests();\n\t}\n}\n\nrpl::producer<LoadedPart> Reader::partsForDownloader() const {\n\treturn _partsForDownloader.events();\n}\n\nvoid Reader::loadForDownloader(\n\t\tnot_null<Storage::StreamedFileDownloader*> downloader,\n\t\tint64 offset) {\n\tExpects(offset >= 0 && offset <= std::numeric_limits<uint32>::max());\n\n\tif (_attachedDownloader != downloader) {\n\t\tif (_attachedDownloader) {\n\t\t\tcancelForDownloader(_attachedDownloader);\n\t\t}\n\t\t_attachedDownloader = downloader;\n\t\t_loader->attachDownloader(downloader);\n\t}\n\t_downloaderOffsetRequests.emplace(uint32(offset));\n\t// Will be processed in continueDownloaderFromMainThread()\n\t// from StreamedFileDownloader::requestParts().\n}\n\nvoid Reader::doneForDownloader(int64 offset) {\n\tExpects(offset >= 0 && offset <= std::numeric_limits<uint32>::max());\n\n\t_downloaderOffsetAcks.emplace(offset);\n\t// Will be processed in continueDownloaderFromMainThread()\n\t// from StreamedFileDownloader::requestParts().\n}\n\nvoid Reader::cancelForDownloader(\n\t\tnot_null<Storage::StreamedFileDownloader*> downloader) {\n\tif (_attachedDownloader == downloader) {\n\t\t_downloaderOffsetRequests.take();\n\t\t_attachedDownloader = nullptr;\n\t\t_loader->clearAttachedDownloader();\n\t}\n}\n\nvoid Reader::enqueueDownloaderOffsets() {\n\tauto offsets = _downloaderOffsetRequests.take();\n\tif (!empty(offsets)) {\n\t\tif (!empty(_offsetsForDownloader)) {\n\t\t\t_offsetsForDownloader.insert(\n\t\t\t\tend(_offsetsForDownloader),\n\t\t\t\tstd::make_move_iterator(begin(offsets)),\n\t\t\t\tstd::make_move_iterator(end(offsets)));\n\t\t\tcheckForDownloaderChange(offsets.size() + 1);\n\t\t} else {\n\t\t\t_offsetsForDownloader = std::move(offsets);\n\t\t\tcheckForDownloaderChange(offsets.size());\n\t\t}\n\t}\n}\n\nvoid Reader::checkForDownloaderChange(int checkItemsCount) {\n\tExpects(checkItemsCount <= _offsetsForDownloader.size());\n\n\t// If a requested offset is less-or-equal of some previously requested\n\t// offset, it means that the downloader was changed, ignore old offsets.\n\tconst auto end = _offsetsForDownloader.end();\n\tconst auto changed = std::adjacent_find(\n\t\tend - checkItemsCount,\n\t\tend,\n\t\t[](uint32 first, uint32 second) { return (second <= first); });\n\tif (changed != end) {\n\t\t_offsetsForDownloader.erase(\n\t\t\tbegin(_offsetsForDownloader),\n\t\t\tchanged + 1);\n\t\t_downloaderReadCache.clear();\n\t\t_downloaderOffsetAcks.take();\n\t}\n}\n\nvoid Reader::checkForDownloaderReadyOffsets() {\n\t// If a requested part is available right now we simply fire it on the\n\t// main thread, until the first not-available-right-now offset is found.\n\tconst auto unavailableInBytes = [&](uint32 offset, QByteArray &&bytes) {\n\t\tif (bytes.isEmpty()) {\n\t\t\treturn true;\n\t\t}\n\t\tcrl::on_main(this, [=, bytes = std::move(bytes)]() mutable {\n\t\t\t_partsForDownloader.fire({ int64(offset), std::move(bytes) });\n\t\t});\n\t\treturn false;\n\t};\n\tconst auto unavailableInCache = [&](uint32 offset) {\n\t\tconst auto index = (offset / kInSlice);\n\t\tconst auto sliceNumber = index + 1;\n\t\tconst auto i = _downloaderReadCache.find(sliceNumber);\n\t\tif (i == end(_downloaderReadCache) || !i->second) {\n\t\t\treturn true;\n\t\t}\n\t\tconst auto j = i->second->find(offset - index * kInSlice);\n\t\tif (j == end(*i->second)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn unavailableInBytes(offset, std::move(j->second));\n\t};\n\tconst auto unavailable = [&](uint32 offset) {\n\t\treturn unavailableInBytes(offset, _slices.partForDownloader(offset))\n\t\t\t&& unavailableInCache(offset);\n\t};\n\t_offsetsForDownloader.erase(\n\t\tbegin(_offsetsForDownloader),\n\t\tranges::find_if(_offsetsForDownloader, unavailable));\n}\n\nvoid Reader::processDownloaderRequests() {\n\tprocessCacheResults();\n\tenqueueDownloaderOffsets();\n\tcheckForDownloaderReadyOffsets();\n\tpruneDoneDownloaderRequests();\n\tif (!empty(_offsetsForDownloader)) {\n\t\tpruneDownloaderCache(_offsetsForDownloader.front());\n\t\tsendDownloaderRequests();\n\t}\n}\n\nvoid Reader::pruneDownloaderCache(uint32 minimalOffset) {\n\tconst auto minimalSliceNumber = (minimalOffset / kInSlice) + 1;\n\tconst auto removeTill = ranges::lower_bound(\n\t\t_downloaderReadCache,\n\t\tminimalSliceNumber,\n\t\tranges::less(),\n\t\t&base::flat_map<uint32, std::optional<PartsMap>>::value_type::first);\n\t_downloaderReadCache.erase(_downloaderReadCache.begin(), removeTill);\n}\n\nvoid Reader::pruneDoneDownloaderRequests() {\n\tfor (const auto done : _downloaderOffsetAcks.take()) {\n\t\t_downloaderOffsetsRequested.remove(done);\n\t\tconst auto i = ranges::find(_offsetsForDownloader, done);\n\t\tif (i != end(_offsetsForDownloader)) {\n\t\t\t_offsetsForDownloader.erase(i);\n\t\t}\n\t}\n}\n\nvoid Reader::sendDownloaderRequests() {\n\tauto &&offsets = ranges::views::all(\n\t\t_offsetsForDownloader\n\t) | ranges::views::take(kDownloaderRequestsLimit);\n\tfor (const auto offset : offsets) {\n\t\tif ((!_cacheHelper || !downloaderWaitForCachedSlice(offset))\n\t\t\t&& _downloaderOffsetsRequested.emplace(offset).second) {\n\t\t\t_loader->load(offset);\n\t\t}\n\t}\n}\n\nbool Reader::downloaderWaitForCachedSlice(uint32 offset) {\n\tif (_slices.waitingForHeaderCache()) {\n\t\treturn true;\n\t}\n\tif (!_slices.readCacheForDownloaderRequired(offset)) {\n\t\treturn false;\n\t}\n\tconst auto sliceNumber = (offset / kInSlice) + 1;\n\tauto i = _downloaderReadCache.find(sliceNumber);\n\tif (i == _downloaderReadCache.end()) {\n\t\t// If we didn't request that slice yet, try requesting it.\n\t\t// If there is no need to (header mode is unknown) - place empty map.\n\t\t// Otherwise place std::nullopt and wait for the cache result.\n\t\ti = _downloaderReadCache.emplace(\n\t\t\tsliceNumber,\n\t\t\t(readFromCacheForDownloader(sliceNumber)\n\t\t\t\t? std::nullopt\n\t\t\t\t: std::make_optional(PartsMap()))).first;\n\t}\n\treturn !i->second;\n}\n\nvoid Reader::checkCacheResultsForDownloader() {\n\tcontinueDownloaderFromMainThread();\n}\n\nvoid Reader::continueDownloaderFromMainThread() {\n\tif (_streamingActive) {\n\t\twakeFromSleep();\n\t} else {\n\t\tprocessDownloaderRequests();\n\t}\n}\n\nrpl::producer<SpeedEstimate> Reader::speedEstimate() const {\n\treturn _loader->speedEstimate();\n}\n\nvoid Reader::setLoaderPriority(int priority) {\n\tif (_realPriority == priority) {\n\t\treturn;\n\t}\n\t_realPriority = priority;\n\trefreshLoaderPriority();\n}\n\nvoid Reader::refreshLoaderPriority() {\n\t_loader->setPriority(_streamingActive ? _realPriority : 0);\n}\n\nbool Reader::isRemoteLoader() const {\n\treturn _loader->baseCacheKey().valid();\n}\n\nstd::shared_ptr<Reader::CacheHelper> Reader::InitCacheHelper(\n\t\tStorage::Cache::Key baseKey) {\n\tif (!baseKey) {\n\t\treturn nullptr;\n\t}\n\treturn std::make_shared<Reader::CacheHelper>(baseKey);\n}\n\n// 0 is for headerData, slice index = sliceNumber - 1.\nvoid Reader::readFromCache(int sliceNumber) {\n\tExpects(_cache != nullptr);\n\tExpects(_cacheHelper != nullptr);\n\tExpects(!sliceNumber || !_slices.headerModeUnknown());\n\n\tif (sliceNumber == 1 && _slices.isGoodHeader()) {\n\t\treturn readFromCache(0);\n\t}\n\tconst auto size = _loader->size();\n\tconst auto key = _cacheHelper->key(sliceNumber);\n\tconst auto cache = std::weak_ptr<CacheHelper>(_cacheHelper);\n\tconst auto weak = base::make_weak(this);\n\tconst auto ready = [=](\n\t\t\tQByteArray &&result,\n\t\t\tstd::vector<int> &&sizes = {}) {\n\t\tcrl::async([\n\t\t\t=,\n\t\t\tresult = std::move(result),\n\t\t\tsizes = std::move(sizes)\n\t\t]() mutable{\n\t\t\tauto entry = ParseCacheEntry(\n\t\t\t\tbytes::make_span(result),\n\t\t\t\tsliceNumber,\n\t\t\t\tsize);\n\t\t\tif (const auto strong = cache.lock()) {\n\t\t\t\tQMutexLocker lock(&strong->mutex);\n\t\t\t\tstrong->results.emplace(sliceNumber, std::move(entry.parts));\n\t\t\t\tif (!sliceNumber && entry.included) {\n\t\t\t\t\tstrong->results.emplace(1, std::move(*entry.included));\n\t\t\t\t}\n\t\t\t\tstrong->sizes = std::move(sizes);\n\t\t\t\tif (const auto waiting = strong->waiting.load()) {\n\t\t\t\t\tstrong->waiting.store(nullptr, std::memory_order_release);\n\t\t\t\t\twaiting->release();\n\t\t\t\t} else {\n\t\t\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\t\t\tcheckCacheResultsForDownloader();\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t};\n\tauto keys = std::vector<Storage::Cache::Key>();\n\tconst auto count = _slices.requestSliceSizesCount();\n\tfor (auto i = 0; i != count; ++i) {\n\t\tkeys.push_back(_cacheHelper->key(i + 1));\n\t}\n\t_cache->getWithSizes(key, std::move(keys), ready);\n}\n\nbool Reader::readFromCacheForDownloader(int sliceNumber) {\n\tExpects(_cacheHelper != nullptr);\n\tExpects(sliceNumber > 0);\n\n\tif (_slices.headerModeUnknown()) {\n\t\treturn false;\n\t}\n\treadFromCache(sliceNumber);\n\treturn true;\n}\n\nvoid Reader::putToCache(SerializedSlice &&slice) {\n\tExpects(_cache != nullptr);\n\tExpects(_cacheHelper != nullptr);\n\tExpects(slice.number >= 0);\n\n\t_cache->put(_cacheHelper->key(slice.number), std::move(slice.data));\n}\n\nint64 Reader::size() const {\n\treturn _loader->size();\n}\n\nstd::optional<Error> Reader::streamingError() const {\n\treturn _streamingError;\n}\n\nvoid Reader::headerDone() {\n\t_slices.headerDone(false);\n}\n\nint Reader::headerSize() const {\n\treturn _slices.headerSize();\n}\n\nbool Reader::fullInCache() const {\n\treturn _slices.fullInCache();\n}\n\nReader::FillState Reader::fill(\n\t\tint64 offset,\n\t\tbytes::span buffer,\n\t\tnot_null<crl::semaphore*> notify) {\n\tExpects(offset + buffer.size() <= size());\n\tExpects(offset >= 0 && size() <= std::numeric_limits<uint32>::max());\n\n\tconst auto startWaiting = [&] {\n\t\tif (_cacheHelper) {\n\t\t\t_cacheHelper->waiting = notify.get();\n\t\t}\n\t\t_waiting.store(notify.get(), std::memory_order_release);\n\t};\n\tconst auto clearWaiting = [&] {\n\t\t_waiting.store(nullptr, std::memory_order_release);\n\t\tif (_cacheHelper) {\n\t\t\t_cacheHelper->waiting.store(nullptr, std::memory_order_release);\n\t\t}\n\t};\n\tconst auto done = [&] {\n\t\tclearWaiting();\n\t\treturn FillState::Success;\n\t};\n\tconst auto failed = [&] {\n\t\tclearWaiting();\n\t\tnotify->release();\n\t\treturn FillState::Failed;\n\t};\n\n\tcheckForSomethingMoreReceived();\n\tif (_streamingError) {\n\t\treturn FillState::Failed;\n\t}\n\n\tauto lastResult = FillState();\n\tdo {\n\t\tlastResult = fillFromSlices(uint32(offset), buffer);\n\t\tif (lastResult == FillState::Success) {\n\t\t\treturn done();\n\t\t}\n\t\tstartWaiting();\n\t} while (checkForSomethingMoreReceived());\n\n\treturn _streamingError ? failed() : lastResult;\n}\n\nReader::FillState Reader::fillFromSlices(uint32 offset, bytes::span buffer) {\n\tusing namespace rpl::mappers;\n\n\tauto result = _slices.fill(offset, buffer);\n\tif (result.state != FillState::Success && _slices.headerWontBeFilled()) {\n\t\t_streamingError = Error::NotStreamable;\n\t\treturn FillState::Failed;\n\t}\n\n\tfor (const auto sliceNumber : result.sliceNumbersFromCache.values()) {\n\t\treadFromCache(sliceNumber);\n\t}\n\n\tif (_cacheHelper && result.toCache.number >= 0) {\n\t\t// If we put to cache the header (number == 0) that means we're in\n\t\t// HeaderMode::Good and really are putting the first slice to cache.\n\t\tAssert(result.toCache.number > 0 || _slices.isGoodHeader());\n\n\t\tconst auto index = std::max(result.toCache.number, 1) - 1;\n\t\tcancelLoadInRange(index * kInSlice, (index + 1) * kInSlice);\n\t\tputToCache(std::move(result.toCache));\n\t}\n\tauto checkPriority = true;\n\tfor (const auto offset : result.offsetsFromLoader.values()) {\n\t\tif (checkPriority) {\n\t\t\tcheckLoadWillBeFirst(offset);\n\t\t\tcheckPriority = false;\n\t\t}\n\t\tloadAtOffset(offset);\n\t}\n\treturn result.state;\n}\n\nvoid Reader::cancelLoadInRange(uint32 from, uint32 till) {\n\tExpects(from < till);\n\n\tfor (const auto offset : _loadingOffsets.takeInRange(from, till)) {\n\t\tif (!_downloaderOffsetsRequested.contains(offset)) {\n\t\t\t_loader->cancel(offset);\n\t\t}\n\t}\n}\n\nvoid Reader::checkLoadWillBeFirst(uint32 offset) {\n\tif (_loadingOffsets.front().value_or(offset) != offset) {\n\t\t_loadingOffsets.resetPriorities();\n\t\t_loader->resetPriorities();\n\t}\n}\n\nbool Reader::processCacheResults() {\n\tif (!_cacheHelper) {\n\t\treturn false;\n\t}\n\n\tQMutexLocker lock(&_cacheHelper->mutex);\n\tauto loaded = base::take(_cacheHelper->results);\n\tauto sizes = base::take(_cacheHelper->sizes);\n\tlock.unlock();\n\n\tfor (auto &[sliceNumber, cachedParts] : _downloaderReadCache) {\n\t\tif (!cachedParts) {\n\t\t\tconst auto i = loaded.find(sliceNumber);\n\t\t\tif (i != end(loaded)) {\n\t\t\t\tcachedParts = i->second;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (_streamingError) {\n\t\treturn false;\n\t}\n\tfor (auto &[sliceNumber, result] : loaded) {\n\t\t_slices.processCacheResult(sliceNumber, std::move(result));\n\t}\n\tif (!sizes.empty()) {\n\t\t_slices.processCachedSizes(sizes);\n\t}\n\tif (!loaded.empty()\n\t\t&& (loaded.front().first == 0)\n\t\t&& _slices.isGoodHeader()) {\n\t\tAssert(loaded.size() > 1);\n\t\tAssert((loaded.begin() + 1)->first == 1);\n\t}\n\treturn !loaded.empty();\n}\n\nbool Reader::processLoadedParts() {\n\tif (_streamingError) {\n\t\treturn false;\n\t}\n\n\tauto loaded = _loadedParts.take();\n\tfor (auto &part : loaded) {\n\t\tif (!part.valid(size())) {\n\t\t\t_streamingError = Error::LoadFailed;\n\t\t\treturn false;\n\t\t} else if (!_loadingOffsets.remove(part.offset)) {\n\t\t\tcontinue;\n\t\t}\n\t\t_slices.processPart(\n\t\t\tpart.offset,\n\t\t\tstd::move(part.bytes));\n\t}\n\treturn !loaded.empty();\n}\n\nbool Reader::checkForSomethingMoreReceived() {\n\tconst auto result1 = processCacheResults();\n\tconst auto result2 = processLoadedParts();\n\treturn result1 || result2;\n}\n\nvoid Reader::loadAtOffset(uint32 offset) {\n\tif (_loadingOffsets.add(offset)) {\n\t\t_loader->load(offset);\n\t}\n}\n\nvoid Reader::finalizeCache() {\n\tif (!_cacheHelper) {\n\t\treturn;\n\t}\n\tAssert(_cache != nullptr);\n\tauto toCache = _slices.unloadToCache();\n\twhile (toCache.number >= 0) {\n\t\tputToCache(std::move(toCache));\n\t\ttoCache = _slices.unloadToCache();\n\t}\n\t_cache->sync();\n}\n\nReader::~Reader() {\n\tfinalizeCache();\n}\n\nQByteArray SerializeComplexPartsMap(\n\t\tconst base::flat_map<uint32, QByteArray> &parts) {\n\tauto result = QByteArray();\n\tconst auto count = parts.size();\n\tconst auto intSize = sizeof(int32);\n\tresult.reserve(count * kPartSize + 2 * intSize * (count + 1));\n\tconst auto appendInt = [&](int value) {\n\t\tauto serialized = int32(value);\n\t\tresult.append(\n\t\t\treinterpret_cast<const char*>(&serialized),\n\t\t\tintSize);\n\t};\n\tappendInt(count);\n\tfor (const auto &[offset, part] : parts) {\n\t\tappendInt(offset);\n\t\tappendInt(part.size());\n\t\tresult.append(part);\n\t}\n\treturn result;\n}\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_reader.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/streaming/media_streaming_common.h\"\n#include \"media/streaming/media_streaming_loader.h\"\n#include \"base/bytes.h\"\n#include \"base/weak_ptr.h\"\n#include \"base/thread_safe_wrap.h\"\n\nnamespace Storage {\nclass StreamedFileDownloader;\n} // namespace Storage\n\nnamespace Storage {\nnamespace Cache {\nstruct Key;\nclass Database;\n} // namespace Cache\n} // namespace Storage\n\nnamespace Media {\nnamespace Streaming {\n\nclass Loader;\nstruct LoadedPart;\nenum class Error;\n\nclass Reader final : public base::has_weak_ptr {\npublic:\n\tenum class FillState : uchar {\n\t\tSuccess,\n\t\tWaitingCache,\n\t\tWaitingRemote,\n\t\tFailed,\n\t};\n\n\t// Main thread.\n\texplicit Reader(\n\t\tstd::unique_ptr<Loader> loader,\n\t\tStorage::Cache::Database *cache = nullptr);\n\n\tvoid setLoaderPriority(int priority);\n\n\t// Any thread.\n\t[[nodiscard]] int64 size() const;\n\t[[nodiscard]] bool isRemoteLoader() const;\n\n\t// Single thread.\n\t[[nodiscard]] FillState fill(\n\t\tint64 offset,\n\t\tbytes::span buffer,\n\t\tnot_null<crl::semaphore*> notify);\n\t[[nodiscard]] std::optional<Error> streamingError() const;\n\tvoid headerDone();\n\t[[nodiscard]] int headerSize() const;\n\t[[nodiscard]] bool fullInCache() const;\n\n\t// Thread safe.\n\tvoid startSleep(not_null<crl::semaphore*> wake);\n\tvoid wakeFromSleep();\n\tvoid stopSleep();\n\tvoid stopStreamingAsync();\n\tvoid tryRemoveLoaderAsync();\n\n\t// Main thread.\n\tvoid startStreaming();\n\tvoid stopStreaming(bool stillActive = false);\n\t[[nodiscard]] rpl::producer<LoadedPart> partsForDownloader() const;\n\tvoid loadForDownloader(\n\t\tnot_null<Storage::StreamedFileDownloader*> downloader,\n\t\tint64 offset);\n\tvoid doneForDownloader(int64 offset);\n\tvoid cancelForDownloader(\n\t\tnot_null<Storage::StreamedFileDownloader*> downloader);\n\tvoid continueDownloaderFromMainThread();\n\t[[nodiscard]] rpl::producer<SpeedEstimate> speedEstimate() const;\n\n\t~Reader();\n\nprivate:\n\tstatic constexpr auto kLoadFromRemoteMax = 8;\n\n\tstruct CacheHelper;\n\n\t// FileSize: Right now any file size fits 32 bit.\n\n\tusing PartsMap = base::flat_map<uint32, QByteArray>;\n\n\ttemplate <int Size>\n\tclass StackIntVector {\n\tpublic:\n\t\tbool add(uint32 value);\n\t\tauto values() const;\n\n\tprivate:\n\t\tstd::array<uint32, Size> _storage = { uint32(-1) };\n\n\t};\n\n\tstruct SerializedSlice {\n\t\tint number = -1;\n\t\tQByteArray data;\n\t};\n\tstruct FillResult {\n\t\tstatic constexpr auto kReadFromCacheMax = 2;\n\n\t\tStackIntVector<kReadFromCacheMax> sliceNumbersFromCache;\n\t\tStackIntVector<kLoadFromRemoteMax> offsetsFromLoader;\n\t\tSerializedSlice toCache;\n\t\tFillState state = FillState::WaitingRemote;\n\t};\n\tstruct Slice {\n\t\tenum class Flag : uchar {\n\t\t\tLoadingFromCache = 0x01,\n\t\t\tLoadedFromCache = 0x02,\n\t\t\tChangedSinceCache = 0x04,\n\t\t\tFullInCache = 0x08,\n\t\t};\n\t\tfriend constexpr inline bool is_flag_type(Flag) { return true; }\n\t\tusing Flags = base::flags<Flag>;\n\n\t\tstruct PrepareFillResult {\n\t\t\tStackIntVector<kLoadFromRemoteMax> offsetsFromLoader;\n\t\t\tPartsMap::const_iterator start;\n\t\t\tPartsMap::const_iterator finish;\n\t\t\tbool ready = true;\n\t\t};\n\n\t\tvoid processCacheData(PartsMap &&data);\n\t\tvoid addPart(uint32 offset, QByteArray bytes);\n\t\tPrepareFillResult prepareFill(uint32 from, uint32 till);\n\n\t\t// Get up to kLoadFromRemoteMax not loaded parts in from-till range.\n\t\tStackIntVector<kLoadFromRemoteMax> offsetsFromLoader(\n\t\t\tuint32 from,\n\t\t\tuint32 till) const;\n\n\t\tPartsMap parts;\n\t\tFlags flags;\n\n\t};\n\n\tclass Slices {\n\tpublic:\n\t\tSlices(uint32 size, bool useCache);\n\n\t\tvoid headerDone(bool fromCache);\n\t\t[[nodiscard]] int headerSize() const;\n\t\t[[nodiscard]] bool fullInCache() const;\n\t\t[[nodiscard]] bool headerWontBeFilled() const;\n\t\t[[nodiscard]] bool headerModeUnknown() const;\n\t\t[[nodiscard]] bool isFullInHeader() const;\n\t\t[[nodiscard]] bool isGoodHeader() const;\n\t\t[[nodiscard]] bool waitingForHeaderCache() const;\n\n\t\t[[nodiscard]] int requestSliceSizesCount() const;\n\n\t\tvoid processCacheResult(int sliceNumber, PartsMap &&result);\n\t\tvoid processCachedSizes(const std::vector<int> &sizes);\n\t\tvoid processPart(uint32 offset, QByteArray &&bytes);\n\n\t\t[[nodiscard]] FillResult fill(uint32 offset, bytes::span buffer);\n\t\t[[nodiscard]] SerializedSlice unloadToCache();\n\n\t\t[[nodiscard]] QByteArray partForDownloader(uint32 offset) const;\n\t\t[[nodiscard]] bool readCacheForDownloaderRequired(uint32 offset);\n\n\tprivate:\n\t\tenum class HeaderMode {\n\t\t\tUnknown,\n\t\t\tSmall,\n\t\t\tGood,\n\t\t\tFull,\n\t\t\tNoCache,\n\t\t};\n\n\t\tvoid applyHeaderCacheData();\n\t\t[[nodiscard]] int maxSliceSize(int sliceNumber) const;\n\t\t[[nodiscard]] SerializedSlice serializeAndUnloadSlice(\n\t\t\tint sliceNumber);\n\t\t[[nodiscard]] SerializedSlice serializeAndUnloadUnused();\n\t\t[[nodiscard]] QByteArray serializeComplexSlice(\n\t\t\tconst Slice &slice) const;\n\t\t[[nodiscard]] QByteArray serializeAndUnloadFirstSliceNoHeader();\n\t\tvoid markSliceUsed(int sliceIndex);\n\t\t[[nodiscard]] bool computeIsGoodHeader() const;\n\t\t[[nodiscard]] FillResult fillFromHeader(\n\t\t\tuint32 offset,\n\t\t\tbytes::span buffer);\n\t\tvoid unloadSlice(Slice &slice) const;\n\t\tvoid checkSliceFullLoaded(int sliceNumber);\n\t\t[[nodiscard]] bool checkFullInCache() const;\n\n\t\tstd::vector<Slice> _data;\n\t\tSlice _header;\n\t\tstd::deque<int> _usedSlices;\n\t\tuint32 _size = 0;\n\t\tHeaderMode _headerMode = HeaderMode::Unknown;\n\t\tbool _fullInCache = false;\n\n\t};\n\n\t// 0 is for headerData, slice index = sliceNumber - 1.\n\t// returns false if asked for a known-empty downloader slice cache.\n\tvoid readFromCache(int sliceNumber);\n\t[[nodiscard]] bool readFromCacheForDownloader(int sliceNumber);\n\tbool processCacheResults();\n\tvoid putToCache(SerializedSlice &&data);\n\n\tvoid cancelLoadInRange(uint32 from, uint32 till);\n\tvoid loadAtOffset(uint32 offset);\n\tvoid checkLoadWillBeFirst(uint32 offset);\n\tbool processLoadedParts();\n\n\tbool checkForSomethingMoreReceived();\n\n\tFillState fillFromSlices(uint32 offset, bytes::span buffer);\n\n\tvoid finalizeCache();\n\n\tvoid processDownloaderRequests();\n\tvoid checkCacheResultsForDownloader();\n\tvoid pruneDownloaderCache(uint32 minimalOffset);\n\tvoid pruneDoneDownloaderRequests();\n\tvoid sendDownloaderRequests();\n\t[[nodiscard]] bool downloaderWaitForCachedSlice(uint32 offset);\n\tvoid enqueueDownloaderOffsets();\n\tvoid checkForDownloaderChange(int checkItemsCount);\n\tvoid checkForDownloaderReadyOffsets();\n\n\tvoid refreshLoaderPriority();\n\n\tstatic std::shared_ptr<CacheHelper> InitCacheHelper(\n\t\tStorage::Cache::Key baseKey);\n\n\tconst std::unique_ptr<Loader> _loader;\n\tStorage::Cache::Database * const _cache = nullptr;\n\n\t// shared_ptr is used to be able to have weak_ptr.\n\tconst std::shared_ptr<CacheHelper> _cacheHelper;\n\n\tbase::thread_safe_queue<LoadedPart, std::vector> _loadedParts;\n\tstd::atomic<crl::semaphore*> _waiting = nullptr;\n\tstd::atomic<crl::semaphore*> _sleeping = nullptr;\n\tstd::atomic<bool> _stopStreamingAsync = false;\n\tPriorityQueue _loadingOffsets;\n\n\tSlices _slices;\n\n\t// Even if streaming had failed, the Reader can work for the downloader.\n\tstd::optional<Error> _streamingError;\n\n\t// In case streaming is active both main and streaming threads have work.\n\t// In case only downloader is active, all work is done on main thread.\n\n\t// Main thread.\n\tStorage::StreamedFileDownloader *_attachedDownloader = nullptr;\n\trpl::event_stream<LoadedPart> _partsForDownloader;\n\tint _realPriority = 1;\n\tbool _streamingActive = false;\n\n\t// Streaming thread.\n\tstd::deque<uint32> _offsetsForDownloader;\n\tbase::flat_set<uint32> _downloaderOffsetsRequested;\n\tbase::flat_map<uint32, std::optional<PartsMap>> _downloaderReadCache;\n\n\t// Communication from main thread to streaming thread.\n\t// Streaming thread to main thread communicates using crl::on_main.\n\tbase::thread_safe_queue<uint32> _downloaderOffsetRequests;\n\tbase::thread_safe_queue<uint32> _downloaderOffsetAcks;\n\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] QByteArray SerializeComplexPartsMap(\n\tconst base::flat_map<uint32, QByteArray> &parts);\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_round_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/streaming/media_streaming_round_preview.h\"\n\nnamespace Media::Streaming {\n\nRoundPreview::RoundPreview(const QByteArray &bytes, int size)\n: _bytes(bytes)\n, _reader(\n\tClip::MakeReader(_bytes, [=](Clip::Notification update) {\n\t\tclipCallback(update);\n\t}))\n, _size(size) {\n}\n\nstd::shared_ptr<Ui::DynamicImage> RoundPreview::clone() {\n\tUnexpected(\"RoundPreview::clone.\");\n}\n\nQImage RoundPreview::image(int size) {\n\tif (!_reader || !_reader->started()) {\n\t\treturn QImage();\n\t}\n\treturn _reader->current({\n\t\t.frame = QSize(_size, _size),\n\t\t.factor = style::DevicePixelRatio(),\n\t\t.radius = ImageRoundRadius::Ellipse,\n\t}, crl::now());\n}\n\nvoid RoundPreview::subscribeToUpdates(Fn<void()> callback) {\n\t_repaint = std::move(callback);\n}\n\nvoid RoundPreview::clipCallback(Clip::Notification notification) {\n\tswitch (notification) {\n\tcase Clip::Notification::Reinit: {\n\t\tif (_reader->state() == ::Media::Clip::State::Error) {\n\t\t\t_reader.setBad();\n\t\t} else if (_reader->ready() && !_reader->started()) {\n\t\t\t_reader->start({\n\t\t\t\t.frame = QSize(_size, _size),\n\t\t\t\t.factor = style::DevicePixelRatio(),\n\t\t\t\t.radius = ImageRoundRadius::Ellipse,\n\t\t\t});\n\t\t}\n\t} break;\n\n\tcase Clip::Notification::Repaint: break;\n\t}\n\n\tif (const auto onstack = _repaint) {\n\t\tonstack();\n\t}\n}\n\n} // namespace Media::Streaming\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_round_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/dynamic_image.h\"\n\n#include \"media/clip/media_clip_reader.h\"\n\nnamespace Media::Streaming {\n\nclass RoundPreview final : public Ui::DynamicImage {\npublic:\n\tRoundPreview(const QByteArray &bytes, int size);\n\n\tstd::shared_ptr<DynamicImage> clone() override;\n\n\tQImage image(int size) override;\n\tvoid subscribeToUpdates(Fn<void()> callback) override;\n\nprivate:\n\tvoid clipCallback(Clip::Notification notification);\n\n\tconst QByteArray _bytes;\n\tClip::ReaderPointer _reader;\n\tFn<void()> _repaint;\n\tint _size = 0;\n\n};\n\n} // namespace Media::Streaming\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/streaming/media_streaming_utility.h\"\n\n#include \"media/streaming/media_streaming_common.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"ffmpeg/ffmpeg_utility.h\"\n\nnamespace Media {\nnamespace Streaming {\nnamespace {\n\nconstexpr auto kSkipInvalidDataPackets = 10;\n\n} // namespace\n\ncrl::time FramePosition(const Stream &stream) {\n\tconst auto pts = !stream.decodedFrame\n\t\t? AV_NOPTS_VALUE\n\t\t: (stream.decodedFrame->best_effort_timestamp != AV_NOPTS_VALUE)\n\t\t? stream.decodedFrame->best_effort_timestamp\n\t\t: (stream.decodedFrame->pts != AV_NOPTS_VALUE)\n\t\t? stream.decodedFrame->pts\n\t\t: stream.decodedFrame->pkt_dts;\n\tconst auto result = FFmpeg::PtsToTime(pts, stream.timeBase);\n\n\t// Sometimes the result here may be larger than the stream duration.\n\treturn (stream.duration == kDurationUnavailable)\n\t\t? result\n\t\t: std::min(result, stream.duration);\n}\n\nFFmpeg::AvErrorWrap ProcessPacket(Stream &stream, FFmpeg::Packet &&packet) {\n\tExpects(stream.codec != nullptr);\n\n\tauto error = FFmpeg::AvErrorWrap();\n\n\tconst auto native = &packet.fields();\n\tconst auto guard = gsl::finally([\n\t\t&,\n\t\tsize = native->size,\n\t\tdata = native->data\n\t] {\n\t\tnative->size = size;\n\t\tnative->data = data;\n\t\tpacket = FFmpeg::Packet();\n\t});\n\n\terror = avcodec_send_packet(\n\t\tstream.codec.get(),\n\t\tnative->data ? native : nullptr); // Drain on eof.\n\tif (error) {\n\t\tLogError(u\"avcodec_send_packet\"_q, error);\n\t\tif (error.code() == AVERROR_INVALIDDATA\n\t\t\t// There is a sample voice message where skipping such packet\n\t\t\t// results in a crash (read_access to nullptr) in swr_convert().\n\t\t\t&& stream.codec->codec_id != AV_CODEC_ID_OPUS) {\n\t\t\tif (++stream.invalidDataPackets < kSkipInvalidDataPackets) {\n\t\t\t\treturn FFmpeg::AvErrorWrap(); // Try to skip a bad packet.\n\t\t\t}\n\t\t}\n\t} else {\n\t\tstream.invalidDataPackets = 0;\n\t}\n\treturn error;\n}\n\nFFmpeg::AvErrorWrap ReadNextFrame(Stream &stream) {\n\tExpects(stream.decodedFrame != nullptr);\n\n\tauto error = FFmpeg::AvErrorWrap();\n\n\tdo {\n\t\terror = avcodec_receive_frame(\n\t\t\tstream.codec.get(),\n\t\t\tstream.decodedFrame.get());\n\t\tif (!error\n\t\t\t|| error.code() != AVERROR(EAGAIN)\n\t\t\t|| stream.queue.empty()) {\n\t\t\treturn error;\n\t\t}\n\n\t\terror = ProcessPacket(stream, std::move(stream.queue.front()));\n\t\tstream.queue.pop_front();\n\t} while (!error);\n\n\treturn error;\n}\n\nbool GoodForRequest(\n\t\tconst QImage &image,\n\t\tbool hasAlpha,\n\t\tint rotation,\n\t\tconst FrameRequest &request) {\n\tif (image.isNull()\n\t\t|| (hasAlpha && !request.keepAlpha)\n\t\t|| request.colored.alpha() != 0) {\n\t\treturn false;\n\t} else if (!request.blurredBackground && request.resize.isEmpty()) {\n\t\treturn true;\n\t} else if (rotation != 0) {\n\t\treturn false;\n\t} else if (!request.rounding.empty() || !request.mask.isNull()) {\n\t\treturn false;\n\t}\n\tconst auto size = request.blurredBackground\n\t\t? request.outer\n\t\t: request.resize;\n\treturn (size == request.outer) && (size == image.size());\n}\n\nbool TransferFrame(\n\t\tStream &stream,\n\t\tnot_null<AVFrame*> decodedFrame,\n\t\tnot_null<AVFrame*> transferredFrame) {\n\tExpects(decodedFrame->hw_frames_ctx != nullptr);\n\n\tconst auto error = FFmpeg::AvErrorWrap(\n\t\tav_hwframe_transfer_data(transferredFrame, decodedFrame, 0));\n\tif (error) {\n\t\tLogError(u\"av_hwframe_transfer_data\"_q, error);\n\t\treturn false;\n\t}\n\tFFmpeg::ClearFrameMemory(decodedFrame);\n\treturn true;\n}\n\nQImage ConvertFrame(\n\t\tStream &stream,\n\t\tnot_null<AVFrame*> frame,\n\t\tQSize resize,\n\t\tQImage storage) {\n\tconst auto frameSize = QSize(frame->width, frame->height);\n\tif (frameSize.isEmpty()) {\n\t\tLOG((\"Streaming Error: Bad frame size %1,%2\"\n\t\t\t).arg(frameSize.width()\n\t\t\t).arg(frameSize.height()));\n\t\treturn QImage();\n\t} else if (!FFmpeg::FrameHasData(frame)) {\n\t\tLOG((\"Streaming Error: Bad frame data.\"));\n\t\treturn QImage();\n\t}\n\tif (resize.isEmpty()) {\n\t\tresize = frameSize;\n\t} else if (FFmpeg::RotationSwapWidthHeight(stream.rotation)) {\n\t\tresize.transpose();\n\t}\n\n\tif (!FFmpeg::GoodStorageForFrame(storage, resize)) {\n\t\tstorage = FFmpeg::CreateFrameStorage(resize);\n\t}\n\n\tconst auto format = AV_PIX_FMT_BGRA;\n\tconst auto hasDesiredFormat = (frame->format == format);\n\tif (hasDesiredFormat\n\t\t&& frameSize == storage.size()\n\t\t&& frame->linesize[0] > 0) {\n\t\tstatic_assert(sizeof(uint32) == FFmpeg::kPixelBytesSize);\n\t\tauto to = reinterpret_cast<uint32*>(storage.bits());\n\t\tauto from = reinterpret_cast<const uint32*>(frame->data[0]);\n\t\tconst auto deltaTo = (storage.bytesPerLine() / sizeof(uint32))\n\t\t\t- storage.width();\n\t\tconst auto deltaFrom = (frame->linesize[0] / sizeof(uint32))\n\t\t\t- frame->width;\n\t\tfor ([[maybe_unused]] const auto y : ranges::views::ints(0, frame->height)) {\n\t\t\tfor ([[maybe_unused]] const auto x : ranges::views::ints(0, frame->width)) {\n\t\t\t\t// Wipe out possible alpha values.\n\t\t\t\t*to++ = 0xFF000000U | *from++;\n\t\t\t}\n\t\t\tto += deltaTo;\n\t\t\tfrom += deltaFrom;\n\t\t}\n\t} else {\n\t\tstream.swscale = MakeSwscalePointer(\n\t\t\tframe,\n\t\t\tresize,\n\t\t\t&stream.swscale);\n\t\tif (!stream.swscale) {\n\t\t\treturn QImage();\n\t\t}\n\n\t\t// AV_NUM_DATA_POINTERS defined in AVFrame struct\n\t\tuint8_t *data[AV_NUM_DATA_POINTERS] = { storage.bits(), nullptr };\n\t\tint linesize[AV_NUM_DATA_POINTERS] = { int(storage.bytesPerLine()), 0 };\n\n\t\tsws_scale(\n\t\t\tstream.swscale.get(),\n\t\t\tframe->data,\n\t\t\tframe->linesize,\n\t\t\t0,\n\t\t\tframe->height,\n\t\t\tdata,\n\t\t\tlinesize);\n\n\t\tif (frame->format == AV_PIX_FMT_YUVA420P) {\n\t\t\tFFmpeg::PremultiplyInplace(storage);\n\t\t}\n\t}\n\n\tFFmpeg::ClearFrameMemory(frame);\n\treturn storage;\n}\n\nFrameYUV ExtractYUV(Stream &stream, AVFrame *frame) {\n\treturn {\n\t\t.size = { frame->width, frame->height },\n\t\t.chromaSize = {\n\t\t\tAV_CEIL_RSHIFT(frame->width, 1), // SWScale does that.\n\t\t\tAV_CEIL_RSHIFT(frame->height, 1)\n\t\t},\n\t\t.y = { .data = frame->data[0], .stride = frame->linesize[0] },\n\t\t.u = { .data = frame->data[1], .stride = frame->linesize[1] },\n\t\t.v = { .data = frame->data[2], .stride = frame->linesize[2] },\n\t};\n}\n\nvoid PaintFrameOuter(QPainter &p, const QRect &inner, QSize outer) {\n\tconst auto left = inner.x();\n\tconst auto right = outer.width() - inner.width() - left;\n\tconst auto top = inner.y();\n\tconst auto bottom = outer.height() - inner.height() - top;\n\tif (left > 0) {\n\t\tp.fillRect(0, 0, left, outer.height(), st::imageBg);\n\t}\n\tif (right > 0) {\n\t\tp.fillRect(\n\t\t\touter.width() - right,\n\t\t\t0,\n\t\t\tright,\n\t\t\touter.height(),\n\t\t\tst::imageBg);\n\t}\n\tif (top > 0) {\n\t\tp.fillRect(left, 0, inner.width(), top, st::imageBg);\n\t}\n\tif (bottom > 0) {\n\t\tp.fillRect(\n\t\t\tleft,\n\t\t\touter.height() - bottom,\n\t\t\tinner.width(),\n\t\t\tbottom,\n\t\t\tst::imageBg);\n\t}\n}\n\nvoid PaintFrameInner(\n\t\tQPainter &p,\n\t\tQRect to,\n\t\tconst QImage &original,\n\t\tbool alpha,\n\t\tint rotation) {\n\tconst auto rotated = [](QRect rect, int rotation) {\n\t\tswitch (rotation) {\n\t\tcase 0: return rect;\n\t\tcase 90: return QRect(\n\t\t\trect.y(),\n\t\t\t-rect.x() - rect.width(),\n\t\t\trect.height(),\n\t\t\trect.width());\n\t\tcase 180: return QRect(\n\t\t\t-rect.x() - rect.width(),\n\t\t\t-rect.y() - rect.height(),\n\t\t\trect.width(),\n\t\t\trect.height());\n\t\tcase 270: return QRect(\n\t\t\t-rect.y() - rect.height(),\n\t\t\trect.x(),\n\t\t\trect.height(),\n\t\t\trect.width());\n\t\t}\n\t\tUnexpected(\"Rotation in PaintFrameInner.\");\n\t};\n\n\tPainterHighQualityEnabler hq(p);\n\tif (rotation) {\n\t\tp.rotate(rotation);\n\t}\n\tconst auto rect = rotated(to, rotation);\n\tif (alpha) {\n\t\tp.fillRect(rect, Qt::white);\n\t}\n\tp.drawImage(rect, original);\n}\n\nQImage PrepareBlurredBackground(QSize outer, QImage frame) {\n\tconst auto bsize = frame.size();\n\tconst auto copyw = std::min(\n\t\tbsize.width(),\n\t\tstd::max(outer.width() * bsize.height() / outer.height(), 1));\n\tconst auto copyh = std::min(\n\t\tbsize.height(),\n\t\tstd::max(outer.height() * bsize.width() / outer.width(), 1));\n\tauto copy = (bsize == QSize(copyw, copyh))\n\t\t? std::move(frame)\n\t\t: frame.copy(\n\t\t\t(bsize.width() - copyw) / 2,\n\t\t\t(bsize.height() - copyh) / 2,\n\t\t\tcopyw,\n\t\t\tcopyh);\n\tauto scaled = (copy.width() <= 100 && copy.height() <= 100)\n\t\t? std::move(copy)\n\t\t: copy.scaled(40, 40, Qt::KeepAspectRatio, Qt::FastTransformation);\n\treturn Images::Blur(std::move(scaled), true);\n}\n\nvoid FillBlurredBackground(QPainter &p, QSize outer, QImage bg) {\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto rect = QRect(QPoint(), outer);\n\tconst auto ratio = p.device()->devicePixelRatio();\n\tp.drawImage(\n\t\trect,\n\t\tPrepareBlurredBackground(outer * ratio, std::move(bg)));\n\tp.fillRect(rect, QColor(0, 0, 0, 48));\n}\n\nvoid PaintFrameContent(\n\t\tQPainter &p,\n\t\tconst QImage &original,\n\t\tbool hasAlpha,\n\t\tconst AVRational &aspect,\n\t\tint rotation,\n\t\tconst FrameRequest &request) {\n\tconst auto outer = request.outer;\n\tconst auto full = request.outer.isEmpty() ? original.size() : outer;\n\tconst auto deAlpha = hasAlpha && !request.keepAlpha;\n\tconst auto resize = request.blurredBackground\n\t\t? DecideVideoFrameResize(\n\t\t\touter,\n\t\t\tFFmpeg::TransposeSizeByRotation(\n\t\t\t\tFFmpeg::CorrectByAspect(original.size(), aspect), rotation))\n\t\t: ExpandDecision{ request.resize.isEmpty()\n\t\t\t? original.size()\n\t\t\t: request.resize };\n\tconst auto size = resize.result;\n\tconst auto target = QRect(\n\t\t(full.width() - size.width()) / 2,\n\t\t(full.height() - size.height()) / 2,\n\t\tsize.width(),\n\t\tsize.height());\n\tif (request.blurredBackground) {\n\t\tif (!resize.expanding) {\n\t\t\tFillBlurredBackground(p, full, original);\n\t\t}\n\t} else if (!hasAlpha || !request.keepAlpha) {\n\t\tPaintFrameOuter(p, target, full);\n\t}\n\tPaintFrameInner(p, target, original, deAlpha, rotation);\n}\n\nvoid ApplyFrameRounding(QImage &storage, const FrameRequest &request) {\n\tif (!request.mask.isNull()) {\n\t\tauto p = QPainter(&storage);\n\t\tp.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\tp.drawImage(\n\t\t\tQRect(QPoint(), storage.size() / storage.devicePixelRatio()),\n\t\t\trequest.mask);\n\t} else if (!request.rounding.empty()) {\n\t\tstorage = Images::Round(std::move(storage), request.rounding);\n\t}\n}\n\nExpandDecision DecideFrameResize(\n\t\tQSize outer,\n\t\tQSize original,\n\t\tint minVisibleNominator,\n\t\tint minVisibleDenominator) {\n\tif (outer.isEmpty()) {\n\t\t// Often \"expanding\" means that we don't need to fill the background.\n\t\treturn { .result = original, .expanding = true };\n\t}\n\tconst auto big = original.scaled(outer, Qt::KeepAspectRatioByExpanding);\n\tif ((big.width() <= outer.width())\n\t\t&& (big.height() * minVisibleNominator\n\t\t\t<= outer.height() * minVisibleDenominator)) {\n\t\treturn { .result = big, .expanding = true };\n\t}\n\treturn { .result = original.scaled(outer, Qt::KeepAspectRatio) };\n}\n\nbool FrameResizeMayExpand(\n\t\tQSize outer,\n\t\tQSize original,\n\t\tint minVisibleNominator,\n\t\tint minVisibleDenominator) {\n\tconst auto min = std::min({\n\t\touter.width(),\n\t\touter.height(),\n\t\toriginal.width(),\n\t\toriginal.height(),\n\t});\n\t// Count for: (nominator / denominator) - (1 / min).\n\t// In case the result is less than 1 / 2, just return.\n\tif (2 * minVisibleNominator * min\n\t\t< 2 * minVisibleDenominator + minVisibleDenominator * min) {\n\t\treturn false;\n\t}\n\treturn DecideFrameResize(\n\t\touter,\n\t\toriginal,\n\t\tminVisibleNominator * min - minVisibleDenominator,\n\t\tminVisibleDenominator * min).expanding;\n}\n\nExpandDecision DecideVideoFrameResize(QSize outer, QSize original) {\n\treturn DecideFrameResize(outer, original, 1, 2);\n}\n\nQSize CalculateResizeFromOuter(QSize outer, QSize original) {\n\treturn DecideVideoFrameResize(outer, original).result;\n}\n\nQImage PrepareByRequest(\n\t\tconst QImage &original,\n\t\tbool hasAlpha,\n\t\tconst AVRational &aspect,\n\t\tint rotation,\n\t\tconst FrameRequest &request,\n\t\tQImage storage) {\n\tExpects(!request.outer.isEmpty() || hasAlpha);\n\n\tconst auto outer = request.outer.isEmpty()\n\t\t? original.size()\n\t\t: request.outer;\n\tif (!FFmpeg::GoodStorageForFrame(storage, outer)) {\n\t\tstorage = FFmpeg::CreateFrameStorage(outer);\n\t}\n\n\tif (hasAlpha && request.keepAlpha) {\n\t\tstorage.fill(Qt::transparent);\n\t}\n\n\tQPainter p(&storage);\n\tPaintFrameContent(p, original, hasAlpha, aspect, rotation, request);\n\tp.end();\n\n\tApplyFrameRounding(storage, request);\n\tif (request.colored.alpha() != 0) {\n\t\tstorage = Images::Colored(std::move(storage), request.colored);\n\t}\n\treturn storage;\n}\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_utility.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/streaming/media_streaming_common.h\"\n#include \"ffmpeg/ffmpeg_utility.h\"\n\nnamespace Media {\nnamespace Streaming {\n\nstruct TimePoint {\n\tcrl::time trackTime = kTimeUnknown;\n\tcrl::time worldTime = kTimeUnknown;\n\n\tbool valid() const {\n\t\treturn (trackTime != kTimeUnknown) && (worldTime != kTimeUnknown);\n\t}\n\texplicit operator bool() const {\n\t\treturn valid();\n\t}\n};\n\nstruct Stream {\n\tint index = -1;\n\tcrl::time duration = kTimeUnknown;\n\tAVRational timeBase = FFmpeg::kUniversalTimeBase;\n\tFFmpeg::CodecPointer codec;\n\tFFmpeg::FramePointer decodedFrame;\n\tFFmpeg::FramePointer transferredFrame;\n\tstd::deque<FFmpeg::Packet> queue;\n\tint invalidDataPackets = 0;\n\n\t// Audio only.\n\tint frequency = 0;\n\n\t// Video only.\n\tint rotation = 0;\n\tAVRational aspect = FFmpeg::kNormalAspect;\n\tfloat64 fps = 0.;\n\tFFmpeg::SwscalePointer swscale;\n};\n\n[[nodiscard]] crl::time FramePosition(const Stream &stream);\n[[nodiscard]] FFmpeg::AvErrorWrap ProcessPacket(\n\tStream &stream,\n\tFFmpeg::Packet &&packet);\n[[nodiscard]] FFmpeg::AvErrorWrap ReadNextFrame(Stream &stream);\n\n[[nodiscard]] bool GoodForRequest(\n\tconst QImage &image,\n\tbool hasAlpha,\n\tint rotation,\n\tconst FrameRequest &request);\n[[nodiscard]] bool TransferFrame(\n\tStream &stream,\n\tnot_null<AVFrame*> decodedFrame,\n\tnot_null<AVFrame*> transferredFrame);\n[[nodiscard]] QImage ConvertFrame(\n\tStream &stream,\n\tnot_null<AVFrame*> frame,\n\tQSize resize,\n\tQImage storage);\n[[nodiscard]] FrameYUV ExtractYUV(Stream &stream, AVFrame *frame);\n\nstruct ExpandDecision {\n\tQSize result;\n\tbool expanding = false;\n};\n[[nodiscard]] ExpandDecision DecideFrameResize(\n\tQSize outer,\n\tQSize original,\n\tint minVisibleNominator = 3, // If we cut out no more than 0.25 of\n\tint minVisibleDenominator = 4); // the original, let's expand.\n[[nodiscard]] bool FrameResizeMayExpand(\n\tQSize outer,\n\tQSize original,\n\tint minVisibleNominator = 3,\n\tint minVisibleDenominator = 4);\n[[nodiscard]] ExpandDecision DecideVideoFrameResize(\n\tQSize outer,\n\tQSize original);\n[[nodiscard]] QSize CalculateResizeFromOuter(QSize outer, QSize original);\n[[nodiscard]] QImage PrepareBlurredBackground(QSize outer, QImage frame);\nvoid FillBlurredBackground(QPainter &p, QSize outer, QImage bg);\n[[nodiscard]] QImage PrepareByRequest(\n\tconst QImage &original,\n\tbool hasAlpha,\n\tconst AVRational &aspect,\n\tint rotation,\n\tconst FrameRequest &request,\n\tQImage storage);\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/streaming/media_streaming_video_track.h\"\n\n#include \"ffmpeg/ffmpeg_utility.h\"\n#include \"media/audio/media_audio.h\"\n#include \"base/concurrent_timer.h\"\n#include \"core/crash_reports.h\"\n#include \"base/debug_log.h\"\n\nnamespace Media {\nnamespace Streaming {\nnamespace {\n\nconstexpr auto kMaxFrameArea = 3840 * 2160; // usual 4K\nconstexpr auto kDisplaySkipped = crl::time(-1);\nconstexpr auto kFinishedPosition = std::numeric_limits<crl::time>::max();\nstatic_assert(kDisplaySkipped != kTimeUnknown);\n\n[[nodiscard]] QImage ConvertToARGB32(\n\t\tFrameFormat format,\n\t\tconst FrameYUV &data) {\n\tExpects(data.y.data != nullptr);\n\tExpects(data.u.data != nullptr);\n\tExpects((format == FrameFormat::NV12) || (data.v.data != nullptr));\n\tExpects(!data.size.isEmpty());\n\n\t//if (FFmpeg::RotationSwapWidthHeight(stream.rotation)) {\n\t//\tresize.transpose();\n\t//}\n\n\tauto result = FFmpeg::CreateFrameStorage(data.size);\n\tconst auto swscale = FFmpeg::MakeSwscalePointer(\n\t\tdata.size,\n\t\t(format == FrameFormat::YUV420\n\t\t\t? AV_PIX_FMT_YUV420P\n\t\t\t: AV_PIX_FMT_NV12),\n\t\tdata.size,\n\t\tAV_PIX_FMT_BGRA);\n\tif (!swscale) {\n\t\treturn QImage();\n\t}\n\n\t// AV_NUM_DATA_POINTERS defined in AVFrame struct\n\tconst uint8_t *srcData[AV_NUM_DATA_POINTERS] = {\n\t\tstatic_cast<const uint8_t*>(data.y.data),\n\t\tstatic_cast<const uint8_t*>(data.u.data),\n\t\tstatic_cast<const uint8_t*>(data.v.data),\n\t\tnullptr,\n\t};\n\tint srcLinesize[AV_NUM_DATA_POINTERS] = {\n\t\tdata.y.stride,\n\t\tdata.u.stride,\n\t\tdata.v.stride,\n\t\t0,\n\t};\n\tuint8_t *dstData[AV_NUM_DATA_POINTERS] = { result.bits(), nullptr };\n\tint dstLinesize[AV_NUM_DATA_POINTERS] = { int(result.bytesPerLine()), 0 };\n\n\tsws_scale(\n\t\tswscale.get(),\n\t\tsrcData,\n\t\tsrcLinesize,\n\t\t0,\n\t\tdata.size.height(),\n\t\tdstData,\n\t\tdstLinesize);\n\n\treturn result;\n}\n\n} // namespace\n\nclass VideoTrackObject final {\npublic:\n\tusing Frame = VideoTrack::Frame;\n\tusing Shared = VideoTrack::Shared;\n\n\tVideoTrackObject(\n\t\tcrl::weak_on_queue<VideoTrackObject> weak,\n\t\tconst PlaybackOptions &options,\n\t\tnot_null<Shared*> shared,\n\t\tStream &&stream,\n\t\tconst AudioMsgId &audioId,\n\t\tFnMut<void(const Information &)> ready,\n\t\tFn<void(Error)> error);\n\n\tvoid process(std::vector<FFmpeg::Packet> &&packets);\n\n\t[[nodiscard]] rpl::producer<> checkNextFrame() const;\n\t[[nodiscard]] rpl::producer<> waitingForData() const;\n\n\tvoid pause(crl::time time);\n\tvoid resume(crl::time time);\n\tvoid setSpeed(float64 speed);\n\tvoid setWaitForMarkAsShown(bool wait);\n\tvoid interrupt();\n\tvoid frameShown();\n\tvoid addTimelineDelay(crl::time delayed);\n\tvoid updateFrameRequest(\n\t\tconst Instance *instance,\n\t\tconst FrameRequest &request);\n\tvoid removeFrameRequest(const Instance *instance);\n\n\tvoid rasterizeFrame(not_null<Frame*> frame);\n\t[[nodiscard]] bool requireARGB32() const;\n\nprivate:\n\tenum class FrameResult {\n\t\tDone,\n\t\tError,\n\t\tWaiting,\n\t\tLooped,\n\t\tFinished,\n\t};\n\tusing ReadEnoughState = std::variant<\n\t\tv::null_t,\n\t\tFrameResult,\n\t\tShared::PrepareNextCheck>;\n\n\tvoid fail(Error error);\n\t[[nodiscard]] bool interrupted() const;\n\t[[nodiscard]] bool tryReadFirstFrame(FFmpeg::Packet &&packet);\n\t[[nodiscard]] bool fillStateFromFrame();\n\t[[nodiscard]] bool processFirstFrame();\n\tvoid queueReadFrames(crl::time delay = 0);\n\tvoid readFrames();\n\t[[nodiscard]] ReadEnoughState readEnoughFrames(crl::time trackTime);\n\t[[nodiscard]] FrameResult readFrame(not_null<Frame*> frame);\n\tvoid fillRequests(not_null<Frame*> frame) const;\n\t[[nodiscard]] QSize chooseOriginalResize(QSize encoded) const;\n\tvoid presentFrameIfNeeded();\n\tvoid callReady();\n\t[[nodiscard]] bool loopAround();\n\t[[nodiscard]] crl::time computeDuration() const;\n\t[[nodiscard]] int durationByPacket(const FFmpeg::Packet &packet);\n\n\t// Force frame position to be clamped to [0, duration] and monotonic.\n\t[[nodiscard]] crl::time currentFramePosition() const;\n\n\t[[nodiscard]] TimePoint trackTime() const;\n\n\tconst crl::weak_on_queue<VideoTrackObject> _weak;\n\tPlaybackOptions _options;\n\n\t// Main thread wrapper destructor will set _shared back to nullptr.\n\t// All queued method calls after that should be discarded.\n\tShared *_shared = nullptr;\n\n\tStream _stream;\n\tAudioMsgId _audioId;\n\tbool _readTillEnd = false;\n\tFnMut<void(const Information &)> _ready;\n\tFn<void(Error)> _error;\n\tcrl::time _pausedTime = kTimeUnknown;\n\tcrl::time _resumedTime = kTimeUnknown;\n\tint _frameIndex = 0;\n\tint _durationByLastPacket = 0;\n\tmutable TimePoint _syncTimePoint;\n\tcrl::time _loopingShift = 0;\n\trpl::event_stream<> _checkNextFrame;\n\trpl::event_stream<> _waitingForData;\n\tbase::flat_map<const Instance*, FrameRequest> _requests;\n\n\tbool _queued = false;\n\tbase::ConcurrentTimer _readFramesTimer;\n\n\t// For initial frame skipping for an exact seek.\n\tFFmpeg::FramePointer _initialSkippingFrame;\n\n};\n\nVideoTrackObject::VideoTrackObject(\n\tcrl::weak_on_queue<VideoTrackObject> weak,\n\tconst PlaybackOptions &options,\n\tnot_null<Shared*> shared,\n\tStream &&stream,\n\tconst AudioMsgId &audioId,\n\tFnMut<void(const Information &)> ready,\n\tFn<void(Error)> error)\n: _weak(std::move(weak))\n, _options(options)\n, _shared(shared)\n, _stream(std::move(stream))\n, _audioId(audioId)\n, _ready(std::move(ready))\n, _error(std::move(error))\n, _readFramesTimer(_weak, [=] { readFrames(); }) {\n\tExpects(_stream.duration > 1);\n\tExpects(_ready != nullptr);\n\tExpects(_error != nullptr);\n}\n\nrpl::producer<> VideoTrackObject::checkNextFrame() const {\n\treturn interrupted()\n\t\t? (rpl::complete<>() | rpl::type_erased)\n\t\t: !_shared->firstPresentHappened()\n\t\t? (_checkNextFrame.events() | rpl::type_erased)\n\t\t: _checkNextFrame.events_starting_with({});\n}\n\nrpl::producer<> VideoTrackObject::waitingForData() const {\n\treturn interrupted()\n\t\t? (rpl::never() | rpl::type_erased)\n\t\t: _waitingForData.events();\n}\n\nvoid VideoTrackObject::process(std::vector<FFmpeg::Packet> &&packets) {\n\tif (interrupted() || packets.empty()) {\n\t\treturn;\n\t}\n\tif (packets.front().empty()) {\n\t\tAssert(packets.size() == 1);\n\t\t_readTillEnd = true;\n\t} else if (!_readTillEnd) {\n\t\t//for (const auto &packet : packets) {\n\t\t//\t// Maybe it is enough to count by list.back()?.. hope so.\n\t\t//\taccumulate_max(\n\t\t//\t\t_durationByLastPacket,\n\t\t//\t\tdurationByPacket(packet));\n\t\t//\tif (interrupted()) {\n\t\t//\t\treturn;\n\t\t//\t}\n\t\t//}\n\t\taccumulate_max(\n\t\t\t_durationByLastPacket,\n\t\t\tdurationByPacket(packets.back()));\n\t\tif (interrupted()) {\n\t\t\treturn;\n\t\t}\n\t}\n\tfor (auto i = begin(packets), e = end(packets); i != e; ++i) {\n\t\tif (_shared->initialized()) {\n\t\t\t_stream.queue.insert(\n\t\t\t\tend(_stream.queue),\n\t\t\t\tstd::make_move_iterator(i),\n\t\t\t\tstd::make_move_iterator(e));\n\t\t\tqueueReadFrames();\n\t\t\tbreak;\n\t\t} else if (!tryReadFirstFrame(std::move(*i))) {\n\t\t\tfail(Error::InvalidData);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nint VideoTrackObject::durationByPacket(const FFmpeg::Packet &packet) {\n\t// We've set this value on the first cycle.\n\tif (_loopingShift || _stream.duration != kDurationUnavailable) {\n\t\treturn 0;\n\t}\n\tconst auto result = FFmpeg::DurationByPacket(packet, _stream.timeBase);\n\tif (result < 0) {\n\t\tfail(Error::InvalidData);\n\t\treturn 0;\n\t}\n\n\tEnsures(result > 0);\n\treturn result;\n}\n\nvoid VideoTrackObject::queueReadFrames(crl::time delay) {\n\tif (delay > 0) {\n\t\t_readFramesTimer.callOnce(delay);\n\t} else if (!_queued) {\n\t\t_queued = true;\n\t\t_weak.with([](VideoTrackObject &that) {\n\t\t\tthat._queued = false;\n\t\t\tthat.readFrames();\n\t\t});\n\t}\n}\n\nvoid VideoTrackObject::readFrames() {\n\tif (interrupted()) {\n\t\treturn;\n\t}\n\tauto time = trackTime().trackTime;\n\twhile (true) {\n\t\tconst auto result = readEnoughFrames(time);\n\t\tv::match(result, [&](FrameResult result) {\n\t\t\tif (result == FrameResult::Done\n\t\t\t\t|| result == FrameResult::Finished) {\n\t\t\t\tpresentFrameIfNeeded();\n\t\t\t} else if (result == FrameResult::Looped) {\n\t\t\t\tconst auto duration = computeDuration();\n\t\t\t\tAssert(duration != kDurationUnavailable);\n\t\t\t\ttime -= duration;\n\t\t\t}\n\t\t}, [&](Shared::PrepareNextCheck delay) {\n\t\t\tExpects(delay == kTimeUnknown || delay > 0);\n\n\t\t\tif (delay != kTimeUnknown) {\n\t\t\t\tqueueReadFrames(delay);\n\t\t\t}\n\t\t}, [](v::null_t) {\n\t\t});\n\t\tif (!v::is_null(result)) {\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nauto VideoTrackObject::readEnoughFrames(crl::time trackTime)\n-> ReadEnoughState {\n\tconst auto dropStaleFrames = !_options.waitForMarkAsShown;\n\tconst auto state = _shared->prepareState(trackTime, dropStaleFrames);\n\treturn v::match(state, [&](Shared::PrepareFrame frame)\n\t-> ReadEnoughState {\n\t\twhile (true) {\n\t\t\tconst auto result = readFrame(frame);\n\t\t\tif (result != FrameResult::Done) {\n\t\t\t\treturn result;\n\t\t\t} else if (!dropStaleFrames\n\t\t\t\t|| !VideoTrack::IsStale(frame, trackTime)) {\n\t\t\t\treturn v::null;\n\t\t\t}\n\t\t}\n\t}, [&](Shared::PrepareNextCheck delay) -> ReadEnoughState {\n\t\treturn delay;\n\t}, [&](v::null_t) -> ReadEnoughState {\n\t\treturn FrameResult::Done;\n\t});\n}\n\nbool VideoTrackObject::loopAround() {\n\tconst auto duration = computeDuration();\n\tif (duration == kDurationUnavailable) {\n\t\tLOG((\"Streaming Error: \"\n\t\t\t\"Couldn't find out the real video stream duration.\"));\n\t\treturn false;\n\t}\n\tavcodec_flush_buffers(_stream.codec.get());\n\t_frameIndex = 0;\n\t_loopingShift += duration;\n\t_readTillEnd = false;\n\treturn true;\n}\n\ncrl::time VideoTrackObject::computeDuration() const {\n\tif (_stream.duration != kDurationUnavailable) {\n\t\treturn _stream.duration;\n\t} else if ((_loopingShift || _readTillEnd) && _durationByLastPacket) {\n\t\t// We looped, so it already holds full stream duration.\n\t\treturn _durationByLastPacket;\n\t}\n\treturn kDurationUnavailable;\n}\n\nauto VideoTrackObject::readFrame(not_null<Frame*> frame) -> FrameResult {\n\tif (const auto error = ReadNextFrame(_stream)) {\n\t\tif (error.code() == AVERROR_EOF) {\n\t\t\tif (!_options.loop) {\n\t\t\t\tframe->position = kFinishedPosition;\n\t\t\t\tframe->displayed = kTimeUnknown;\n\t\t\t\treturn FrameResult::Finished;\n\t\t\t} else if (loopAround()) {\n\t\t\t\treturn FrameResult::Looped;\n\t\t\t} else {\n\t\t\t\tfail(Error::InvalidData);\n\t\t\t\treturn FrameResult::Error;\n\t\t\t}\n\t\t} else if (error.code() != AVERROR(EAGAIN) || _readTillEnd) {\n\t\t\tfail(Error::InvalidData);\n\t\t\treturn FrameResult::Error;\n\t\t}\n\t\tAssert(_stream.queue.empty());\n\t\t_waitingForData.fire({});\n\t\treturn FrameResult::Waiting;\n\t}\n\tconst auto decodedFrame = _stream.decodedFrame.get();\n\tif (int64(decodedFrame->width) * decodedFrame->height > kMaxFrameArea) {\n\t\tfail(Error::InvalidData);\n\t\treturn FrameResult::Error;\n\t}\n\tconst auto position = currentFramePosition();\n\tif (position == kTimeUnknown) {\n\t\tfail(Error::InvalidData);\n\t\treturn FrameResult::Error;\n\t}\n\tstd::swap(frame->decoded, _stream.decodedFrame);\n\tstd::swap(frame->transferred, _stream.transferredFrame);\n\tframe->index = _frameIndex++;\n\tframe->position = position;\n\tframe->displayed = kTimeUnknown;\n\treturn FrameResult::Done;\n}\n\nvoid VideoTrackObject::fillRequests(not_null<Frame*> frame) const {\n\tauto i = frame->prepared.begin();\n\tfor (const auto &[instance, request] : _requests) {\n\t\twhile (i != frame->prepared.end() && i->first < instance) {\n\t\t\ti = frame->prepared.erase(i);\n\t\t}\n\t\tif (i == frame->prepared.end() || i->first > instance) {\n\t\t\ti = frame->prepared.emplace(instance, request).first;\n\t\t}\n\t\t++i;\n\t}\n\twhile (i != frame->prepared.end()) {\n\t\ti = frame->prepared.erase(i);\n\t}\n}\n\nQSize VideoTrackObject::chooseOriginalResize(QSize encoded) const {\n\tauto chosen = QSize();\n\tif (FFmpeg::RotationSwapWidthHeight(_stream.rotation)) {\n\t\tencoded.transpose();\n\t}\n\tfor (const auto &[_, request] : _requests) {\n\t\tconst auto resize = request.blurredBackground\n\t\t\t? CalculateResizeFromOuter(request.outer, encoded)\n\t\t\t: request.resize;\n\t\tif (resize.isEmpty()) {\n\t\t\treturn QSize();\n\t\t}\n\t\tconst auto byWidth = (resize.width() >= chosen.width());\n\t\tconst auto byHeight = (resize.height() >= chosen.height());\n\t\tif (byWidth && byHeight) {\n\t\t\tchosen = resize;\n\t\t} else if (byWidth || byHeight) {\n\t\t\treturn QSize();\n\t\t}\n\t}\n\treturn chosen;\n}\n\nbool VideoTrackObject::requireARGB32() const {\n\tfor (const auto &[_, request] : _requests) {\n\t\tif (!request.requireARGB32) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid VideoTrackObject::rasterizeFrame(not_null<Frame*> frame) {\n\tExpects(frame->position != kFinishedPosition);\n\n\tfillRequests(frame);\n\tframe->format = FrameFormat::None;\n\tframe->nativeFrame = NativeFrame();\n\tif (frame->decoded->hw_frames_ctx) {\n#ifdef Q_OS_MAC\n\t\tconst auto hwFormat = frame->decoded->format;\n\t\tconst auto wantARGB = requireARGB32();\n\t\tconst auto isVT = (hwFormat == AV_PIX_FMT_VIDEOTOOLBOX);\n\t\tconst auto pb = isVT ? (void*)frame->decoded->data[3] : nullptr;\n\t\tif (!wantARGB && isVT && pb) {\n\t\t\t\tconst auto w = frame->decoded->width;\n\t\t\t\tconst auto h = frame->decoded->height;\n\t\t\t\tframe->nativeFrame = NativeFrame{\n\t\t\t\t\t.pixelBuffer = pb,\n\t\t\t\t\t.size = { w, h },\n\t\t\t\t\t.chromaSize = {\n\t\t\t\t\t\t(w + 1) / 2,\n\t\t\t\t\t\t(h + 1) / 2,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t\tframe->alpha = false;\n\t\t\t\tframe->format = FrameFormat::NativeTexture;\n\t\t\t\tif (!frame->original.isNull()) {\n\t\t\t\t\tframe->original = QImage();\n\t\t\t\t\tfor (auto &[_, prepared] : frame->prepared) {\n\t\t\t\t\t\tprepared.image = QImage();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t}\n#endif // Q_OS_MAC\n\t\tif (!frame->transferred) {\n\t\t\tframe->transferred = FFmpeg::MakeFramePointer();\n\t\t}\n\t\tconst auto success = TransferFrame(\n\t\t\t_stream,\n\t\t\tframe->decoded.get(),\n\t\t\tframe->transferred.get());\n\t\tif (!success) {\n\t\t\tframe->prepared.clear();\n\t\t\tfail(Error::InvalidData);\n\t\t\treturn;\n\t\t}\n\t} else {\n\t\tframe->transferred = nullptr;\n\t}\n\tconst auto frameWithData = frame->transferred\n\t\t? frame->transferred.get()\n\t\t: frame->decoded.get();\n\tif ((frameWithData->format == AV_PIX_FMT_YUV420P\n\t\t|| frameWithData->format == AV_PIX_FMT_NV12) && !requireARGB32()) {\n\t\tconst auto nv12 = (frameWithData->format == AV_PIX_FMT_NV12);\n\t\tframe->alpha = false;\n\t\tframe->yuv = ExtractYUV(_stream, frameWithData);\n\t\tif (frame->yuv.size.isEmpty()\n\t\t\t|| frame->yuv.chromaSize.isEmpty()\n\t\t\t|| !frame->yuv.y.data\n\t\t\t|| !frame->yuv.u.data\n\t\t\t|| (!nv12 && !frame->yuv.v.data)) {\n\t\t\tframe->prepared.clear();\n\t\t\tfail(Error::InvalidData);\n\t\t\treturn;\n\t\t}\n\t\tif (!frame->original.isNull()) {\n\t\t\tframe->original = QImage();\n\t\t\tfor (auto &[_, prepared] : frame->prepared) {\n\t\t\t\tprepared.image = QImage();\n\t\t\t}\n\t\t}\n\t\tframe->format = nv12 ? FrameFormat::NV12 : FrameFormat::YUV420;\n\t} else {\n\t\tframe->alpha = (frameWithData->format == AV_PIX_FMT_BGRA)\n\t\t\t|| (frameWithData->format == AV_PIX_FMT_YUVA420P);\n\t\tframe->yuv.size = {\n\t\t\tframeWithData->width,\n\t\t\tframeWithData->height\n\t\t};\n\t\tframe->original = ConvertFrame(\n\t\t\t_stream,\n\t\t\tframeWithData,\n\t\t\tchooseOriginalResize(\n\t\t\t\t{ frameWithData->width, frameWithData->height }),\n\t\t\tstd::move(frame->original));\n\t\tif (frame->original.isNull()) {\n\t\t\tframe->prepared.clear();\n\t\t\tfail(Error::InvalidData);\n\t\t\treturn;\n\t\t}\n\t\tframe->format = FrameFormat::ARGB32;\n\t}\n\n\tVideoTrack::PrepareFrameByRequests(\n\t\tframe,\n\t\t_stream.aspect,\n\t\t_stream.rotation);\n\n\tEnsures(VideoTrack::IsRasterized(frame));\n}\n\nvoid VideoTrackObject::presentFrameIfNeeded() {\n\tif (_pausedTime != kTimeUnknown || _resumedTime == kTimeUnknown) {\n\t\treturn;\n\t}\n\tconst auto dropStaleFrames = !_options.waitForMarkAsShown;\n\tconst auto time = trackTime();\n\tconst auto presented = _shared->presentFrame(\n\t\tthis,\n\t\ttime,\n\t\t_options.speed,\n\t\tdropStaleFrames);\n\taddTimelineDelay(presented.addedWorldTimeDelay);\n\tif (presented.displayPosition == kFinishedPosition) {\n\t\tinterrupt();\n\t\t_checkNextFrame = rpl::event_stream<>();\n\t\treturn;\n\t} else if (presented.displayPosition != kTimeUnknown) {\n\t\t_checkNextFrame.fire({});\n\t}\n\tif (presented.nextCheckDelay != kTimeUnknown) {\n\t\tAssert(presented.nextCheckDelay >= 0);\n\t\tqueueReadFrames(presented.nextCheckDelay);\n\t}\n}\n\nvoid VideoTrackObject::pause(crl::time time) {\n\tExpects(_syncTimePoint.valid());\n\n\tif (interrupted()) {\n\t\treturn;\n\t} else if (_pausedTime == kTimeUnknown) {\n\t\t_pausedTime = time;\n\t}\n}\n\nvoid VideoTrackObject::resume(crl::time time) {\n\tExpects(_syncTimePoint.trackTime != kTimeUnknown);\n\n\tif (interrupted()) {\n\t\treturn;\n\t}\n\n\t// Resumed time used to validate sync to audio.\n\t_resumedTime = time;\n\tif (_pausedTime != kTimeUnknown) {\n\t\tAssert(_pausedTime <= time);\n\t\t_syncTimePoint.worldTime += (time - _pausedTime);\n\t\t_pausedTime = kTimeUnknown;\n\t} else {\n\t\t_syncTimePoint.worldTime = time;\n\t}\n\tqueueReadFrames();\n\n\tEnsures(_syncTimePoint.valid());\n\tEnsures(_pausedTime == kTimeUnknown);\n}\n\nvoid VideoTrackObject::setSpeed(float64 speed) {\n\tif (interrupted()) {\n\t\treturn;\n\t}\n\tif (_syncTimePoint.valid()) {\n\t\tconst auto time = trackTime();\n\t\t_syncTimePoint = time;\n\t}\n\t_options.speed = speed;\n}\n\nvoid VideoTrackObject::setWaitForMarkAsShown(bool wait) {\n\tif (interrupted()) {\n\t\treturn;\n\t}\n\t_options.waitForMarkAsShown = wait;\n}\n\nbool VideoTrackObject::interrupted() const {\n\treturn !_shared;\n}\n\nvoid VideoTrackObject::frameShown() {\n\tif (interrupted()) {\n\t\treturn;\n\t}\n\tqueueReadFrames();\n}\n\nvoid VideoTrackObject::addTimelineDelay(crl::time delayed) {\n\tExpects(_syncTimePoint.valid());\n\n\tif (!delayed) {\n\t\treturn;\n\t}\n\t_syncTimePoint.worldTime += delayed;\n}\n\nvoid VideoTrackObject::updateFrameRequest(\n\t\tconst Instance *instance,\n\t\tconst FrameRequest &request) {\n\t_requests[instance] = request;\n}\n\nvoid VideoTrackObject::removeFrameRequest(const Instance *instance) {\n\t_requests.remove(instance);\n}\n\nbool VideoTrackObject::tryReadFirstFrame(FFmpeg::Packet &&packet) {\n\tif (ProcessPacket(_stream, std::move(packet)).failed()) {\n\t\treturn false;\n\t}\n\twhile (true) {\n\t\tif (const auto error = ReadNextFrame(_stream)) {\n\t\t\tif (error.code() == AVERROR_EOF) {\n\t\t\t\tif (!_initialSkippingFrame) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t// Return the last valid frame if we seek too far.\n\t\t\t\t_stream.decodedFrame = std::move(_initialSkippingFrame);\n\t\t\t\treturn processFirstFrame();\n\t\t\t} else if (error.code() != AVERROR(EAGAIN) || _readTillEnd) {\n\t\t\t\treturn false;\n\t\t\t} else {\n\t\t\t\t// Waiting for more packets.\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (!fillStateFromFrame()) {\n\t\t\treturn false;\n\t\t} else if (_syncTimePoint.trackTime >= _options.position) {\n\t\t\treturn processFirstFrame();\n\t\t}\n\n\t\t// Seek was with AVSEEK_FLAG_BACKWARD so first we get old frames.\n\t\t// Try skipping frames until one is after the requested position.\n\t\tstd::swap(_initialSkippingFrame, _stream.decodedFrame);\n\t\tif (!_stream.decodedFrame) {\n\t\t\t_stream.decodedFrame = FFmpeg::MakeFramePointer();\n\t\t}\n\t}\n}\n\nbool VideoTrackObject::processFirstFrame() {\n\tconst auto decodedFrame = _stream.decodedFrame.get();\n\tif (int64(decodedFrame->width) * decodedFrame->height > kMaxFrameArea) {\n\t\treturn false;\n\t} else if (decodedFrame->hw_frames_ctx) {\n\t\tif (!_stream.transferredFrame) {\n\t\t\t_stream.transferredFrame = FFmpeg::MakeFramePointer();\n\t\t}\n\t\tconst auto success = TransferFrame(\n\t\t\t_stream,\n\t\t\tdecodedFrame,\n\t\t\t_stream.transferredFrame.get());\n\t\tif (!success) {\n\t\t\tLOG((\"Video Error: Failed accelerated decoding from format %1.\"\n\t\t\t\t).arg(int(decodedFrame->format)));\n\t\t\treturn false;\n\t\t}\n\t\tDEBUG_LOG((\"Video Info: \"\n\t\t\t\"Using accelerated decoding from format %1 to format %2.\"\n\t\t\t).arg(int(decodedFrame->format)\n\t\t\t).arg(int(_stream.transferredFrame->format)));\n\t} else {\n\t\t_stream.transferredFrame = nullptr;\n\t}\n\tconst auto frameWithData = _stream.transferredFrame\n\t\t? _stream.transferredFrame.get()\n\t\t: decodedFrame;\n\tconst auto alpha = (frameWithData->format == AV_PIX_FMT_BGRA)\n\t\t|| (frameWithData->format == AV_PIX_FMT_YUVA420P);\n\tauto frame = ConvertFrame(\n\t\t_stream,\n\t\tframeWithData,\n\t\tQSize(),\n\t\tQImage());\n\tif (frame.isNull()) {\n\t\treturn false;\n\t}\n\t_shared->init(std::move(frame), alpha, _syncTimePoint.trackTime);\n\tcallReady();\n\tqueueReadFrames();\n\treturn true;\n}\n\ncrl::time VideoTrackObject::currentFramePosition() const {\n\tconst auto position = FramePosition(_stream);\n\tif (position == kTimeUnknown || position == kFinishedPosition) {\n\t\treturn kTimeUnknown;\n\t}\n\treturn _loopingShift + std::clamp(\n\t\tposition,\n\t\tcrl::time(0),\n\t\tcomputeDuration() - 1);\n}\n\nbool VideoTrackObject::fillStateFromFrame() {\n\tconst auto position = currentFramePosition();\n\tif (position == kTimeUnknown) {\n\t\treturn false;\n\t}\n\t_syncTimePoint.trackTime = position;\n\treturn true;\n}\n\nvoid VideoTrackObject::callReady() {\n\tExpects(_ready != nullptr);\n\n\tconst auto frame = _shared->frameForPaint();\n\t++_frameIndex;\n\n\tbase::take(_ready)({ VideoInformation{\n\t\t.state = {\n\t\t\t.position = _syncTimePoint.trackTime,\n\t\t\t.receivedTill = (_readTillEnd\n\t\t\t\t? _stream.duration\n\t\t\t\t: _syncTimePoint.trackTime),\n\t\t\t.duration = _stream.duration,\n\t\t},\n\t\t.size = FFmpeg::TransposeSizeByRotation(\n\t\t\tFFmpeg::CorrectByAspect(frame->original.size(), _stream.aspect),\n\t\t\t_stream.rotation),\n\t\t.cover = frame->original,\n\t\t.rotation = _stream.rotation,\n\t\t.fps = _stream.fps,\n\t\t.alpha = frame->alpha,\n\t} });\n}\n\nTimePoint VideoTrackObject::trackTime() const {\n\tauto result = TimePoint();\n\tresult.worldTime = (_pausedTime != kTimeUnknown)\n\t\t? _pausedTime\n\t\t: crl::now();\n\tif (!_syncTimePoint) {\n\t\tresult.trackTime = _syncTimePoint.trackTime;\n\t\treturn result;\n\t}\n\n\tAssert(_resumedTime != kTimeUnknown);\n\tif (_options.syncVideoByAudio && _audioId.externalPlayId()) {\n\t\tconst auto mixer = Media::Player::mixer();\n\t\tconst auto point = mixer->getExternalSyncTimePoint(_audioId);\n\t\tif (point && point.worldTime > _resumedTime) {\n\t\t\t_syncTimePoint = point;\n\t\t}\n\t}\n\tconst auto adjust = (result.worldTime - _syncTimePoint.worldTime);\n\tconst auto adjustSpeed = adjust * _options.speed;\n\tconst auto roundAdjustSpeed = base::SafeRound(adjustSpeed);\n\tconst auto timeRoundAdjustSpeed = crl::time(roundAdjustSpeed);\n\tresult.trackTime = _syncTimePoint.trackTime + timeRoundAdjustSpeed;\n\treturn result;\n}\n\nvoid VideoTrackObject::interrupt() {\n\t_shared = nullptr;\n}\n\nvoid VideoTrackObject::fail(Error error) {\n\tinterrupt();\n\t_error(error);\n}\n\nvoid VideoTrack::Shared::init(\n\t\tQImage &&cover,\n\t\tbool hasAlpha,\n\t\tcrl::time position) {\n\tExpects(!initialized());\n\n\t_frames[0].original = std::move(cover);\n\t_frames[0].position = position;\n\t_frames[0].format = FrameFormat::ARGB32;\n\t_frames[0].alpha = hasAlpha;\n\n\t// Usually main thread sets displayed time before _counter increment.\n\t// But in this case we update _counter, so we set a fake displayed time.\n\t_frames[0].displayed = kDisplaySkipped;\n\n\t_delay = 0;\n\t_counter.store(0, std::memory_order_release);\n}\n\nint VideoTrack::Shared::counter() const {\n\treturn _counter.load(std::memory_order_acquire);\n}\n\nbool VideoTrack::Shared::initialized() const {\n\treturn (counter() != kCounterUninitialized);\n}\n\nnot_null<VideoTrack::Frame*> VideoTrack::Shared::getFrame(int index) {\n\tExpects(index >= 0 && index < kFramesCount);\n\n\treturn &_frames[index];\n}\n\nnot_null<const VideoTrack::Frame*> VideoTrack::Shared::getFrame(\n\t\tint index) const {\n\tExpects(index >= 0 && index < kFramesCount);\n\n\treturn &_frames[index];\n}\n\nauto VideoTrack::Shared::prepareState(\n\tcrl::time trackTime,\n\tbool dropStaleFrames)\n-> PrepareState {\n\tconst auto prepareNext = [&](int index) -> PrepareState {\n\t\tconst auto frame = getFrame(index);\n\t\tconst auto next = getFrame((index + 1) % kFramesCount);\n\t\tif (!IsDecoded(frame)) {\n\t\t\treturn frame;\n\t\t} else if (!IsDecoded(next)) {\n\t\t\treturn next;\n\t\t} else if (next->position < frame->position) {\n\t\t\tstd::swap(*frame, *next);\n\t\t}\n\t\tif (next->position == kFinishedPosition || !dropStaleFrames) {\n\t\t\treturn PrepareNextCheck(kTimeUnknown);\n\t\t} else if (IsStale(frame, trackTime)) {\n\t\t\tstd::swap(*frame, *next);\n\t\t\tnext->displayed = kDisplaySkipped;\n\t\t\treturn next;\n\t\t} else {\n\t\t\tif (frame->position - trackTime + 1 <= 0) { // Debugging crash.\n\t\t\t\tCrashReports::SetAnnotation(\n\t\t\t\t\t\"DelayValues\",\n\t\t\t\t\t(QString::number(frame->position)\n\t\t\t\t\t\t+ \" + 1 <= \"\n\t\t\t\t\t\t+ QString::number(trackTime)));\n\t\t\t}\n\t\t\tAssert(frame->position >= trackTime);\n\t\t\tAssert(frame->position - trackTime + 1 > 0);\n\n\t\t\treturn PrepareNextCheck(frame->position - trackTime + 1);\n\t\t}\n\t};\n\tconst auto finishPrepare = [&](int index) -> PrepareState {\n\t\t// If player already awaits next frame - we ignore if it's stale.\n\t\tdropStaleFrames = false;\n\t\tconst auto result = prepareNext(index);\n\t\treturn v::is<PrepareNextCheck>(result) ? PrepareState() : result;\n\t};\n\n\tswitch (counter()) {\n\tcase 0: return finishPrepare(1);\n\tcase 1: return prepareNext(2);\n\tcase 2: return finishPrepare(2);\n\tcase 3: return prepareNext(3);\n\tcase 4: return finishPrepare(3);\n\tcase 5: return prepareNext(0);\n\tcase 6: return finishPrepare(0);\n\tcase 7: return prepareNext(1);\n\t}\n\tUnexpected(\"Counter value in VideoTrack::Shared::prepareState.\");\n}\n\n// Sometimes main thread subscribes to check frame requests before\n// the first frame is ready and presented and sometimes after.\nbool VideoTrack::Shared::firstPresentHappened() const {\n\tswitch (counter()) {\n\tcase 0: return false;\n\tcase 1: return true;\n\t}\n\tUnexpected(\"Counter value in VideoTrack::Shared::firstPresentHappened.\");\n}\n\nauto VideoTrack::Shared::presentFrame(\n\tnot_null<VideoTrackObject*> object,\n\tTimePoint time,\n\tfloat64 playbackSpeed,\n\tbool dropStaleFrames)\n-> PresentFrame {\n\tconst auto present = [&](int counter, int index) -> PresentFrame {\n\t\tconst auto frame = getFrame(index);\n\t\tconst auto position = frame->position;\n\t\tconst auto addedWorldTimeDelay = base::take(_delay);\n\t\tif (position == kFinishedPosition) {\n\t\t\treturn { kFinishedPosition, kTimeUnknown, addedWorldTimeDelay };\n\t\t}\n\t\tobject->rasterizeFrame(frame);\n\t\tif (!IsRasterized(frame)) {\n\t\t\t// Error happened during frame prepare.\n\t\t\treturn { kTimeUnknown, kTimeUnknown, addedWorldTimeDelay };\n\t\t}\n\t\tconst auto trackLeft = position - time.trackTime;\n\t\tconst auto adjustedBySpeed = trackLeft / playbackSpeed;\n\t\tconst auto roundedAdjustedBySpeed = base::SafeRound(adjustedBySpeed);\n\t\tframe->display = time.worldTime\n\t\t\t+ addedWorldTimeDelay\n\t\t\t+ crl::time(roundedAdjustedBySpeed);\n\n\t\t// Release this frame to the main thread for rendering.\n\t\t_counter.store(\n\t\t\tcounter + 1,\n\t\t\tstd::memory_order_release);\n\t\treturn { position, crl::time(0), addedWorldTimeDelay };\n\t};\n\tconst auto nextCheckDelay = [&](int index) -> PresentFrame {\n\t\tconst auto frame = getFrame(index);\n\t\tif (frame->position == kFinishedPosition) {\n\t\t\treturn { kFinishedPosition, kTimeUnknown };\n\t\t}\n\t\tconst auto next = getFrame((index + 1) % kFramesCount);\n\t\tif (!IsDecoded(frame) || !IsDecoded(next)) {\n\t\t\treturn { kTimeUnknown, crl::time(0) };\n\t\t} else if (next->position == kFinishedPosition\n\t\t\t|| !dropStaleFrames\n\t\t\t|| IsStale(frame, time.trackTime)) {\n\t\t\treturn { kTimeUnknown, kTimeUnknown };\n\t\t}\n\t\treturn { kTimeUnknown, (frame->position - time.trackTime + 1) };\n\t};\n\n\tswitch (counter()) {\n\tcase 0: return present(0, 1);\n\tcase 1: return nextCheckDelay(2);\n\tcase 2: return present(2, 2);\n\tcase 3: return nextCheckDelay(3);\n\tcase 4: return present(4, 3);\n\tcase 5: return nextCheckDelay(0);\n\tcase 6: return present(6, 0);\n\tcase 7: return nextCheckDelay(1);\n\t}\n\tUnexpected(\"Counter value in VideoTrack::Shared::prepareState.\");\n}\n\ncrl::time VideoTrack::Shared::nextFrameDisplayTime() const {\n\tconst auto frameDisplayTime = [&](int counter) {\n\t\tconst auto next = (counter + 1) % (2 * kFramesCount);\n\t\tconst auto index = next / 2;\n\t\tconst auto frame = getFrame(index);\n\t\tif (frame->displayed != kTimeUnknown) {\n\t\t\t// Frame already displayed, but not yet shown.\n\t\t\treturn kFrameDisplayTimeAlreadyDone;\n\t\t}\n\t\tAssert(IsRasterized(frame));\n\t\tAssert(frame->display != kTimeUnknown);\n\n\t\treturn frame->display;\n\t};\n\n\tswitch (counter()) {\n\tcase 0: return kTimeUnknown;\n\tcase 1: return frameDisplayTime(1);\n\tcase 2: return kTimeUnknown;\n\tcase 3: return frameDisplayTime(3);\n\tcase 4: return kTimeUnknown;\n\tcase 5: return frameDisplayTime(5);\n\tcase 6: return kTimeUnknown;\n\tcase 7: return frameDisplayTime(7);\n\t}\n\tUnexpected(\"Counter value in VideoTrack::Shared::nextFrameDisplayTime.\");\n}\n\ncrl::time VideoTrack::Shared::markFrameDisplayed(crl::time now) {\n\tconst auto mark = [&](int counter) {\n\t\tconst auto next = (counter + 1) % (2 * kFramesCount);\n\t\tconst auto index = next / 2;\n\t\tconst auto frame = getFrame(index);\n\t\tAssert(frame->position != kTimeUnknown);\n\t\tif (frame->displayed == kTimeUnknown) {\n\t\t\tframe->displayed = now;\n\t\t}\n\t\treturn frame->position;\n\t};\n\n\tswitch (counter()) {\n\tcase 0: Unexpected(\"Value 0 in VideoTrack::Shared::markFrameDisplayed.\");\n\tcase 1: return mark(1);\n\tcase 2: Unexpected(\"Value 2 in VideoTrack::Shared::markFrameDisplayed.\");\n\tcase 3: return mark(3);\n\tcase 4: Unexpected(\"Value 4 in VideoTrack::Shared::markFrameDisplayed.\");\n\tcase 5: return mark(5);\n\tcase 6: Unexpected(\"Value 6 in VideoTrack::Shared::markFrameDisplayed.\");\n\tcase 7: return mark(7);\n\t}\n\tUnexpected(\"Counter value in VideoTrack::Shared::markFrameDisplayed.\");\n}\n\nvoid VideoTrack::Shared::addTimelineDelay(crl::time delayed) {\n\tif (!delayed) {\n\t\treturn;\n\t}\n\tconst auto recountCurrentFrame = [&](int counter) {\n\t\t_delay += delayed;\n\t\t//const auto next = (counter + 1) % (2 * kFramesCount);\n\t\t//const auto index = next / 2;\n\t\t//const auto frame = getFrame(index);\n\t\t//if (frame->displayed != kTimeUnknown) {\n\t\t//\t// Frame already displayed.\n\t\t//\treturn;\n\t\t//}\n\t\t//Assert(IsRasterized(frame));\n\t\t//Assert(frame->display != kTimeUnknown);\n\t\t//frame->display = countFrameDisplayTime(frame->index);\n\t};\n\n\tswitch (counter()) {\n\tcase 0: Unexpected(\"Value 0 in VideoTrack::Shared::addTimelineDelay.\");\n\tcase 1: return recountCurrentFrame(1);\n\tcase 2: Unexpected(\"Value 2 in VideoTrack::Shared::addTimelineDelay.\");\n\tcase 3: return recountCurrentFrame(3);\n\tcase 4: Unexpected(\"Value 4 in VideoTrack::Shared::addTimelineDelay.\");\n\tcase 5: return recountCurrentFrame(5);\n\tcase 6: Unexpected(\"Value 6 in VideoTrack::Shared::addTimelineDelay.\");\n\tcase 7: return recountCurrentFrame(7);\n\t}\n\tUnexpected(\"Counter value in VideoTrack::Shared::addTimelineDelay.\");\n}\n\nbool VideoTrack::Shared::markFrameShown() {\n\tconst auto jump = [&](int counter) {\n\t\tconst auto next = (counter + 1) % (2 * kFramesCount);\n\t\tconst auto index = next / 2;\n\t\tconst auto frame = getFrame(index);\n\t\tif (frame->displayed == kTimeUnknown) {\n\t\t\treturn false;\n\t\t}\n\t\t_counter.store(\n\t\t\tnext,\n\t\t\tstd::memory_order_release);\n\t\treturn true;\n\t};\n\n\tswitch (counter()) {\n\tcase 0: return false;\n\tcase 1: return jump(1);\n\tcase 2: return false;\n\tcase 3: return jump(3);\n\tcase 4: return false;\n\tcase 5: return jump(5);\n\tcase 6: return false;\n\tcase 7: return jump(7);\n\t}\n\tUnexpected(\"Counter value in VideoTrack::Shared::markFrameShown.\");\n}\n\nnot_null<VideoTrack::Frame*> VideoTrack::Shared::frameForPaint() {\n\treturn frameForPaintWithIndex().frame;\n}\n\nVideoTrack::FrameWithIndex VideoTrack::Shared::frameForPaintWithIndex() {\n\tconst auto index = counter() / 2;\n\tconst auto frame = getFrame(index);\n\tAssert(frame->format != FrameFormat::None);\n\tAssert(frame->position != kTimeUnknown);\n\tAssert(frame->displayed != kTimeUnknown);\n\treturn {\n\t\t.frame = frame,\n\t\t.index = frame->index,\n\t};\n}\n\nVideoTrack::VideoTrack(\n\tconst PlaybackOptions &options,\n\tStream &&stream,\n\tconst AudioMsgId &audioId,\n\tFnMut<void(const Information &)> ready,\n\tFn<void(Error)> error)\n: _streamIndex(stream.index)\n, _streamTimeBase(stream.timeBase)\n, _streamDuration(stream.duration)\n, _streamRotation(stream.rotation)\n, _streamAspect(stream.aspect)\n, _shared(std::make_unique<Shared>())\n, _wrapped(\n\toptions,\n\t_shared.get(),\n\tstd::move(stream),\n\taudioId,\n\tstd::move(ready),\n\tstd::move(error)) {\n}\n\nint VideoTrack::streamIndex() const {\n\treturn _streamIndex;\n}\n\nAVRational VideoTrack::streamTimeBase() const {\n\treturn _streamTimeBase;\n}\n\ncrl::time VideoTrack::streamDuration() const {\n\treturn _streamDuration;\n}\n\nvoid VideoTrack::process(std::vector<FFmpeg::Packet> &&packets) {\n\t_wrapped.with([\n\t\tpackets = std::move(packets)\n\t](Implementation &unwrapped) mutable {\n\t\tunwrapped.process(std::move(packets));\n\t});\n}\n\nvoid VideoTrack::waitForData() {\n}\n\nvoid VideoTrack::pause(crl::time time) {\n\t_wrapped.with([=](Implementation &unwrapped) {\n\t\tunwrapped.pause(time);\n\t});\n}\n\nvoid VideoTrack::resume(crl::time time) {\n\t_wrapped.with([=](Implementation &unwrapped) {\n\t\tunwrapped.resume(time);\n\t});\n}\n\nvoid VideoTrack::setSpeed(float64 speed) {\n\t_wrapped.with([=](Implementation &unwrapped) {\n\t\tunwrapped.setSpeed(speed);\n\t});\n}\n\nvoid VideoTrack::setWaitForMarkAsShown(bool wait) {\n\t_wrapped.with([=](Implementation &unwrapped) {\n\t\tunwrapped.setWaitForMarkAsShown(wait);\n\t});\n}\n\ncrl::time VideoTrack::nextFrameDisplayTime() const {\n\treturn _shared->nextFrameDisplayTime();\n}\n\ncrl::time VideoTrack::markFrameDisplayed(crl::time now) {\n\tconst auto result = _shared->markFrameDisplayed(now);\n\n\tEnsures(result != kTimeUnknown);\n\treturn result;\n}\n\nvoid VideoTrack::addTimelineDelay(crl::time delayed) {\n\t_shared->addTimelineDelay(delayed);\n\t//if (!delayed) {\n\t//\treturn;\n\t//}\n\t//_wrapped.with([=](Implementation &unwrapped) mutable {\n\t//\tunwrapped.addTimelineDelay(delayed);\n\t//});\n}\n\nbool VideoTrack::markFrameShown() {\n\tif (!_shared->markFrameShown()) {\n\t\treturn false;\n\t}\n\t_wrapped.with([](Implementation &unwrapped) {\n\t\tunwrapped.frameShown();\n\t});\n\treturn true;\n}\n\nQImage VideoTrack::frame(\n\t\tconst FrameRequest &request,\n\t\tconst Instance *instance) {\n\treturn frameImage(_shared->frameForPaint(), request, instance);\n}\n\nFrameWithInfo VideoTrack::frameWithInfo(\n\t\tconst FrameRequest &request,\n\t\tconst Instance *instance) {\n\tconst auto data = _shared->frameForPaintWithIndex();\n\treturn {\n\t\t.image = frameImage(data.frame, request, instance),\n\t\t.format = FrameFormat::ARGB32,\n\t\t.index = data.index,\n\t};\n}\n\nFrameWithInfo VideoTrack::frameWithInfo(const Instance *instance) {\n\tconst auto data = _shared->frameForPaintWithIndex();\n\tconst auto i = data.frame->prepared.find(instance);\n\tconst auto none = (i == data.frame->prepared.end());\n\tif (none || i->second.request.requireARGB32) {\n\t\t_wrapped.with([=](Implementation &unwrapped) {\n\t\t\tunwrapped.updateFrameRequest(\n\t\t\t\tinstance,\n\t\t\t\t{ .requireARGB32 = false });\n\t\t});\n\t}\n\treturn {\n\t\t.image = data.frame->original,\n\t\t.yuv = &data.frame->yuv,\n\t\t.nativeFrame = &data.frame->nativeFrame,\n\t\t.format = data.frame->format,\n\t\t.index = data.index,\n\t\t.alpha = data.frame->alpha,\n\t};\n}\n\nQImage VideoTrack::frameImage(\n\t\tnot_null<Frame*> frame,\n\t\tconst FrameRequest &request,\n\t\tconst Instance *instance) {\n\tconst auto i = frame->prepared.find(instance);\n\tconst auto none = (i == frame->prepared.end());\n\tconst auto preparedFor = frame->prepared.empty()\n\t\t? FrameRequest::NonStrict()\n\t\t: (none ? frame->prepared.begin() : i)->second.request;\n\tconst auto changed = !preparedFor.goodFor(request);\n\tconst auto useRequest = changed ? request : preparedFor;\n\tif (changed) {\n\t\t_wrapped.with([=](Implementation &unwrapped) {\n\t\t\tunwrapped.updateFrameRequest(instance, useRequest);\n\t\t});\n\t}\n\tif (frame->original.isNull()\n\t\t&& (frame->format == FrameFormat::YUV420\n\t\t\t|| frame->format == FrameFormat::NV12)) {\n\t\tframe->original = ConvertToARGB32(frame->format, frame->yuv);\n\t}\n\tif (GoodForRequest(\n\t\t\tframe->original,\n\t\t\tframe->alpha,\n\t\t\t_streamRotation,\n\t\t\tuseRequest)) {\n\t\treturn frame->original;\n\t} else if (changed || none || i->second.image.isNull()) {\n\t\tconst auto j = none\n\t\t\t? frame->prepared.emplace(instance, useRequest).first\n\t\t\t: i;\n\t\tif (changed && !none) {\n\t\t\ti->second.request = useRequest;\n\t\t}\n\t\tif (frame->prepared.size() > 1) {\n\t\t\tfor (auto &[alreadyInstance, prepared] : frame->prepared) {\n\t\t\t\tif (alreadyInstance != instance\n\t\t\t\t\t&& prepared.request == useRequest\n\t\t\t\t\t&& !prepared.image.isNull()) {\n\t\t\t\t\treturn prepared.image;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tj->second.image = PrepareByRequest(\n\t\t\tframe->original,\n\t\t\tframe->alpha,\n\t\t\t_streamAspect,\n\t\t\t_streamRotation,\n\t\t\tuseRequest,\n\t\t\tstd::move(j->second.image));\n\t\treturn j->second.image;\n\t}\n\treturn i->second.image;\n}\n\nQImage VideoTrack::currentFrameImage() {\n\tconst auto frame = _shared->frameForPaint();\n\tif (frame->original.isNull()\n\t\t&& (frame->format == FrameFormat::YUV420\n\t\t\t|| frame->format == FrameFormat::NV12)) {\n\t\tframe->original = ConvertToARGB32(frame->format, frame->yuv);\n\t}\n\treturn frame->original;\n}\n\nvoid VideoTrack::unregisterInstance(not_null<const Instance*> instance) {\n\t_wrapped.with([=](Implementation &unwrapped) {\n\t\tunwrapped.removeFrameRequest(instance);\n\t});\n}\n\nvoid VideoTrack::PrepareFrameByRequests(\n\t\tnot_null<Frame*> frame,\n\t\tconst AVRational &aspect,\n\t\tint rotation) {\n\tExpects(frame->format != FrameFormat::ARGB32\n\t\t|| !frame->original.isNull());\n\n\tif (frame->format != FrameFormat::ARGB32) {\n\t\treturn;\n\t}\n\n\tconst auto begin = frame->prepared.begin();\n\tconst auto end = frame->prepared.end();\n\tfor (auto i = begin; i != end; ++i) {\n\t\tauto &prepared = i->second;\n\t\tif (!GoodForRequest(\n\t\t\t\tframe->original,\n\t\t\t\tframe->alpha,\n\t\t\t\trotation,\n\t\t\t\tprepared.request)) {\n\t\t\tauto j = begin;\n\t\t\tfor (; j != i; ++j) {\n\t\t\t\tif (j->second.request == prepared.request) {\n\t\t\t\t\tprepared.image = QImage();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (j == i) {\n\t\t\t\tprepared.image = PrepareByRequest(\n\t\t\t\t\tframe->original,\n\t\t\t\t\tframe->alpha,\n\t\t\t\t\taspect,\n\t\t\t\t\trotation,\n\t\t\t\t\tprepared.request,\n\t\t\t\t\tstd::move(prepared.image));\n\t\t\t}\n\t\t}\n\t}\n}\n\nbool VideoTrack::IsDecoded(not_null<const Frame*> frame) {\n\treturn (frame->position != kTimeUnknown)\n\t\t&& (frame->displayed == kTimeUnknown);\n}\n\nbool VideoTrack::IsRasterized(not_null<const Frame*> frame) {\n\treturn IsDecoded(frame)\n\t\t&& (!frame->original.isNull()\n\t\t\t|| frame->format == FrameFormat::YUV420\n\t\t\t|| frame->format == FrameFormat::NV12\n\t\t\t|| frame->format == FrameFormat::NativeTexture);\n}\n\nbool VideoTrack::IsStale(not_null<const Frame*> frame, crl::time trackTime) {\n\tExpects(IsDecoded(frame));\n\n\treturn (frame->position < trackTime);\n}\n\nrpl::producer<> VideoTrack::checkNextFrame() const {\n\treturn _wrapped.producer_on_main([](const Implementation &unwrapped) {\n\t\treturn unwrapped.checkNextFrame();\n\t});\n}\n\nrpl::producer<> VideoTrack::waitingForData() const {\n\treturn _wrapped.producer_on_main([](const Implementation &unwrapped) {\n\t\treturn unwrapped.waitingForData();\n\t});\n}\n\nVideoTrack::~VideoTrack() {\n\t_wrapped.with([shared = std::move(_shared)](Implementation &unwrapped) {\n\t\tunwrapped.interrupt();\n\t});\n}\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/streaming/media_streaming_video_track.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/streaming/media_streaming_utility.h\"\n\n#include <crl/crl_object_on_queue.h>\n\nnamespace Media {\nnamespace Streaming {\n\nconstexpr auto kFrameDisplayTimeAlreadyDone\n\t= std::numeric_limits<crl::time>::max();\n\nclass VideoTrackObject;\nclass Instance;\n\nclass VideoTrack final {\npublic:\n\t// Called from some unspecified thread.\n\t// Callbacks are assumed to be thread-safe.\n\tVideoTrack(\n\t\tconst PlaybackOptions &options,\n\t\tStream &&stream,\n\t\tconst AudioMsgId &audioId,\n\t\tFnMut<void(const Information &)> ready,\n\t\tFn<void(Error)> error);\n\n\t// Thread-safe.\n\t[[nodiscard]] int streamIndex() const;\n\t[[nodiscard]] AVRational streamTimeBase() const;\n\t[[nodiscard]] crl::time streamDuration() const;\n\n\t// Called from the same unspecified thread.\n\tvoid process(std::vector<FFmpeg::Packet> &&packets);\n\tvoid waitForData();\n\n\t// Called from the main thread.\n\t// Must be called after 'ready' was invoked.\n\tvoid pause(crl::time time);\n\tvoid resume(crl::time time);\n\n\t// Called from the main thread.\n\tvoid setSpeed(float64 speed);\n\tvoid setWaitForMarkAsShown(bool wait);\n\n\t// Called from the main thread.\n\t// Returns the position of the displayed frame.\n\t[[nodiscard]] crl::time markFrameDisplayed(crl::time now);\n\tvoid addTimelineDelay(crl::time delayed);\n\tbool markFrameShown();\n\t[[nodiscard]] crl::time nextFrameDisplayTime() const;\n\t[[nodiscard]] QImage frame(\n\t\tconst FrameRequest &request,\n\t\tconst Instance *instance);\n\t[[nodiscard]] FrameWithInfo frameWithInfo(\n\t\tconst FrameRequest &request,\n\t\tconst Instance *instance);\n\t[[nodiscard]] FrameWithInfo frameWithInfo(const Instance *instance);\n\t[[nodiscard]] QImage currentFrameImage();\n\tvoid unregisterInstance(not_null<const Instance*> instance);\n\t[[nodiscard]] rpl::producer<> checkNextFrame() const;\n\t[[nodiscard]] rpl::producer<> waitingForData() const;\n\n\t// Called from the main thread.\n\t~VideoTrack();\n\nprivate:\n\tfriend class VideoTrackObject;\n\n\tstruct Prepared {\n\t\tPrepared(const FrameRequest &request) : request(request) {\n\t\t}\n\n\t\tFrameRequest request = FrameRequest::NonStrict();\n\t\tQImage image;\n\t};\n\tstruct Frame {\n\t\tFFmpeg::FramePointer decoded = FFmpeg::MakeFramePointer();\n\t\tFFmpeg::FramePointer transferred;\n\t\tQImage original;\n\t\tFrameYUV yuv;\n\t\tNativeFrame nativeFrame;\n\t\tcrl::time position = kTimeUnknown;\n\t\tcrl::time displayed = kTimeUnknown;\n\t\tcrl::time display = kTimeUnknown;\n\t\tFrameFormat format = FrameFormat::None;\n\n\t\tbase::flat_map<const Instance*, Prepared> prepared;\n\n\t\tint index = 0;\n\t\tbool alpha = false;\n\t};\n\tstruct FrameWithIndex {\n\t\tnot_null<Frame*> frame;\n\t\tint index = -1;\n\t};\n\n\tclass Shared {\n\tpublic:\n\t\tusing PrepareFrame = not_null<Frame*>;\n\t\tusing PrepareNextCheck = crl::time;\n\t\tusing PrepareState = std::variant<\n\t\t\tv::null_t,\n\t\t\tPrepareFrame,\n\t\t\tPrepareNextCheck>;\n\t\tstruct PresentFrame {\n\t\t\tcrl::time displayPosition = kTimeUnknown;\n\t\t\tcrl::time nextCheckDelay = 0;\n\t\t\tcrl::time addedWorldTimeDelay = 0;\n\t\t};\n\n\t\t// Called from the wrapped object queue.\n\t\tvoid init(QImage &&cover, bool hasAlpha, crl::time position);\n\t\t[[nodiscard]] bool initialized() const;\n\n\t\t[[nodiscard]] PrepareState prepareState(\n\t\t\tcrl::time trackTime,\n\t\t\tbool dropStaleFrames);\n\n\t\t[[nodiscard]] PresentFrame presentFrame(\n\t\t\tnot_null<VideoTrackObject*> object,\n\t\t\tTimePoint trackTime,\n\t\t\tfloat64 playbackSpeed,\n\t\t\tbool dropStaleFrames);\n\t\t[[nodiscard]] bool firstPresentHappened() const;\n\n\t\t// Called from the main thread.\n\t\t// Returns the position of the displayed frame.\n\t\t[[nodiscard]] crl::time markFrameDisplayed(crl::time now);\n\t\tvoid addTimelineDelay(crl::time delayed);\n\t\tbool markFrameShown();\n\t\t[[nodiscard]] crl::time nextFrameDisplayTime() const;\n\t\t[[nodiscard]] not_null<Frame*> frameForPaint();\n\t\t[[nodiscard]] FrameWithIndex frameForPaintWithIndex();\n\n\tprivate:\n\t\t[[nodiscard]] not_null<Frame*> getFrame(int index);\n\t\t[[nodiscard]] not_null<const Frame*> getFrame(int index) const;\n\t\t[[nodiscard]] int counter() const;\n\n\t\tstatic constexpr auto kCounterUninitialized = -1;\n\t\tstd::atomic<int> _counter = kCounterUninitialized;\n\n\t\tstatic constexpr auto kFramesCount = 4;\n\t\tstd::array<Frame, kFramesCount> _frames;\n\n\t\t// (_counter % 2) == 1 main thread can write _delay.\n\t\t// (_counter % 2) == 0 crl::queue can read _delay.\n\t\tcrl::time _delay = kTimeUnknown;\n\n\t};\n\n\tstatic void PrepareFrameByRequests(\n\t\tnot_null<Frame*> frame,\n\t\tconst AVRational &aspect,\n\t\tint rotation);\n\t[[nodiscard]] static bool IsDecoded(not_null<const Frame*> frame);\n\t[[nodiscard]] static bool IsRasterized(not_null<const Frame*> frame);\n\t[[nodiscard]] static bool IsStale(\n\t\tnot_null<const Frame*> frame,\n\t\tcrl::time trackTime);\n\n\t[[nodiscard]] QImage frameImage(\n\t\tnot_null<Frame*> frame,\n\t\tconst FrameRequest &request,\n\t\tconst Instance *instance);\n\n\tconst int _streamIndex = 0;\n\tconst AVRational _streamTimeBase;\n\tconst crl::time _streamDuration = 0;\n\tconst int _streamRotation = 0;\n\tconst AVRational _streamAspect = FFmpeg::kNormalAspect;\n\tstd::unique_ptr<Shared> _shared;\n\n\tusing Implementation = VideoTrackObject;\n\tcrl::object_on_queue<Implementation> _wrapped;\n\n};\n\n} // namespace Streaming\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/system_media_controls_manager.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/system_media_controls_manager.h\"\n\n#include \"base/platform/base_platform_system_media_controls.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/streaming/media_streaming_instance.h\"\n#include \"media/streaming/media_streaming_player.h\"\n#include \"ui/text/format_song_document_name.h\"\n\nnamespace Media {\nnamespace {\n\n[[nodiscard]] auto RepeatModeToLoopStatus(Media::RepeatMode mode) {\n\tusing Mode = Media::RepeatMode;\n\tusing Status = base::Platform::SystemMediaControls::LoopStatus;\n\tswitch (mode) {\n\tcase Mode::None: return Status::None;\n\tcase Mode::One: return Status::Track;\n\tcase Mode::All: return Status::Playlist;\n\t}\n\tUnexpected(\"RepeatModeToLoopStatus in SystemMediaControlsManager\");\n}\n\n} // namespace\n\nbool SystemMediaControlsManager::Supported() {\n\treturn base::Platform::SystemMediaControls::Supported();\n}\n\nSystemMediaControlsManager::SystemMediaControlsManager()\n: _controls(std::make_unique<base::Platform::SystemMediaControls>()) {\n\n\tusing PlaybackStatus\n\t\t= base::Platform::SystemMediaControls::PlaybackStatus;\n\tusing Command = base::Platform::SystemMediaControls::Command;\n\n\t_controls->setApplicationName(AppName.utf16());\n\tconst auto inited = _controls->init();\n\tif (!inited) {\n\t\tLOG((\"SystemMediaControlsManager failed to init.\"));\n\t\treturn;\n\t}\n\tusing TrackState = Media::Player::TrackState;\n\tconst auto mediaPlayer = Media::Player::instance();\n\n\tauto trackFilter = rpl::filter([=](const TrackState &state) {\n\t\tconst auto type = state.id.type();\n\t\treturn (type == AudioMsgId::Type::Song)\n\t\t\t|| (type == AudioMsgId::Type::Voice);\n\t});\n\n\tmediaPlayer->updatedNotifier(\n\t) | trackFilter | rpl::map([=](const TrackState &state) {\n\t\tusing namespace Media::Player;\n\t\tif (_streamed) {\n\t\t\tconst auto &player = _streamed->player();\n\t\t\tif (player.buffering() || !player.playing()) {\n\t\t\t\treturn PlaybackStatus::Paused;\n\t\t\t}\n\t\t}\n\t\tif (IsStoppedOrStopping(state.state)) {\n\t\t\treturn PlaybackStatus::Stopped;\n\t\t} else if (IsPausedOrPausing(state.state)) {\n\t\t\treturn PlaybackStatus::Paused;\n\t\t}\n\t\treturn PlaybackStatus::Playing;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](PlaybackStatus status) {\n\t\t_controls->setPlaybackStatus(status);\n\t}, _lifetime);\n\n\trpl::merge(\n\t\tmediaPlayer->stops(AudioMsgId::Type::Song) | rpl::map_to(false),\n\t\tmediaPlayer->startsPlay(AudioMsgId::Type::Song) | rpl::map_to(true),\n\t\tmediaPlayer->stops(AudioMsgId::Type::Voice) | rpl::map_to(false),\n\t\tmediaPlayer->startsPlay(AudioMsgId::Type::Voice) | rpl::map_to(true)\n\t) | rpl::distinct_until_changed() | rpl::on_next([=](bool audio) {\n\t\tconst auto type = mediaPlayer->current(AudioMsgId::Type::Song)\n\t\t\t? AudioMsgId::Type::Song\n\t\t\t: AudioMsgId::Type::Voice;\n\t\t_controls->setEnabled(audio);\n\t\tif (audio) {\n\t\t\t_controls->setIsNextEnabled(mediaPlayer->nextAvailable(type));\n\t\t\t_controls->setIsPreviousEnabled(\n\t\t\t\tmediaPlayer->previousAvailable(type));\n\t\t\t_controls->setIsPlayPauseEnabled(true);\n\t\t\t_controls->setIsStopEnabled(true);\n\t\t\t_controls->setPlaybackStatus(PlaybackStatus::Playing);\n\t\t\t_controls->updateDisplay();\n\t\t} else {\n\t\t\t_cachedMediaView.clear();\n\t\t\t_streamed = nullptr;\n\t\t\t_controls->clearMetadata();\n\t\t}\n\t\t_lifetimeDownload.destroy();\n\t}, _lifetime);\n\n\tauto trackChanged = mediaPlayer->trackChanged(\n\t) | rpl::filter([=](AudioMsgId::Type audioType) {\n\t\treturn (audioType == AudioMsgId::Type::Song)\n\t\t\t|| (audioType == AudioMsgId::Type::Voice);\n\t});\n\n\tauto unlocked = Core::App().passcodeLockChanges(\n\t) | rpl::filter([=](bool locked) {\n\t\treturn !locked && (mediaPlayer->current(AudioMsgId::Type::Song)\n\t\t\t|| mediaPlayer->current(AudioMsgId::Type::Voice));\n\t}) | rpl::map([=]() -> AudioMsgId::Type {\n\t\treturn mediaPlayer->current(AudioMsgId::Type::Song)\n\t\t\t? AudioMsgId::Type::Song\n\t\t\t: AudioMsgId::Type::Voice;\n\t}) | rpl::before_next([=] {\n\t\t_controls->setEnabled(true);\n\t\t_controls->updateDisplay();\n\t});\n\n\trpl::merge(\n\t\tstd::move(trackChanged),\n\t\tstd::move(unlocked)\n\t) | rpl::on_next([=](AudioMsgId::Type audioType) {\n\t\t_lifetimeDownload.destroy();\n\n\t\tconst auto current = mediaPlayer->current(audioType);\n\t\tif (!current) {\n\t\t\treturn;\n\t\t}\n\t\tif ((_lastAudioMsgId.contextId() == current.contextId())\n\t\t\t&& (_lastAudioMsgId.audio() == current.audio())\n\t\t\t&& (_lastAudioMsgId.type() == current.type())) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto document = current.audio();\n\n\t\tconst auto &[title, performer] = (audioType\n\t\t\t\t== AudioMsgId::Type::Voice)\n\t\t\t? Ui::Text::FormatVoiceName(\n\t\t\t\tdocument,\n\t\t\t\tcurrent.contextId()).composedName()\n\t\t\t: Ui::Text::FormatSongNameFor(document).composedName();\n\t\t_controls->setArtist(performer);\n\t\t_controls->setTitle(title);\n\n\t\tif (_controls->seekingSupported()) {\n\t\t\tconst auto state = mediaPlayer->getState(audioType);\n\t\t\t_controls->setDuration(state.length);\n\t\t\t// macOS NowPlaying and Linux MPRIS update the track position\n\t\t\t// according to the rate property\n\t\t\t// while the playback status is \"playing\",\n\t\t\t// so we should change the track position only when\n\t\t\t// the track is changed\n\t\t\t// or when the position is changed by the user.\n\t\t\t_controls->setPosition(state.position);\n\n\t\t\t_streamed = std::make_unique<Media::Streaming::Instance>(\n\t\t\t\tdocument,\n\t\t\t\tcurrent.contextId(),\n\t\t\t\tnullptr);\n\t\t}\n\n\t\t// Setting a thumbnail can take a long time,\n\t\t// so we need to update the display before that.\n\t\t_controls->updateDisplay();\n\n\t\tif (audioType == AudioMsgId::Type::Voice) {\n\t\t\tif (const auto item = document->owner().message(\n\t\t\t\t\tcurrent.contextId())) {\n\t\t\t\tconstexpr auto kUserpicSize = 50;\n\t\t\t\tconst auto forwarded = item->Get<HistoryMessageForwarded>();\n\t\t\t\tconst auto peer = (forwarded && forwarded->originalSender)\n\t\t\t\t\t? forwarded->originalSender\n\t\t\t\t\t: (!item->out() || item->isPost())\n\t\t\t\t\t? item->fromOriginal().get()\n\t\t\t\t\t: item->history()->peer.get();\n\t\t\t\tconst auto userpic = PeerData::GenerateUserpicImage(\n\t\t\t\t\tpeer,\n\t\t\t\t\t_cachedUserpicView,\n\t\t\t\t\tkUserpicSize,\n\t\t\t\t\t0);\n\t\t\t\tif (!userpic.isNull()) {\n\t\t\t\t\t_controls->setThumbnail(userpic);\n\t\t\t\t} else {\n\t\t\t\t\tpeer->session().downloaderTaskFinished(\n\t\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\t\tconst auto userpic = PeerData::GenerateUserpicImage(\n\t\t\t\t\t\t\tpeer,\n\t\t\t\t\t\t\t_cachedUserpicView,\n\t\t\t\t\t\t\tkUserpicSize,\n\t\t\t\t\t\t\t0);\n\t\t\t\t\t\tif (!userpic.isNull()) {\n\t\t\t\t\t\t\t_controls->setThumbnail(userpic);\n\t\t\t\t\t\t\t_lifetimeDownload.destroy();\n\t\t\t\t\t\t}\n\t\t\t\t\t}, _lifetimeDownload);\n\t\t\t\t\t_controls->clearThumbnail();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t_controls->clearThumbnail();\n\t\t\t}\n\t\t} else if (document && document->isSongWithCover()) {\n\t\t\tconst auto view = document->createMediaView();\n\t\t\tview->thumbnailWanted(current.contextId());\n\t\t\t_cachedMediaView.push_back(view);\n\t\t\tif (const auto imagePtr = view->thumbnail()) {\n\t\t\t\t_controls->setThumbnail(imagePtr->original());\n\t\t\t} else {\n\t\t\t\tdocument->session().downloaderTaskFinished(\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\tif (const auto imagePtr = view->thumbnail()) {\n\t\t\t\t\t\t_controls->setThumbnail(imagePtr->original());\n\t\t\t\t\t\t_lifetimeDownload.destroy();\n\t\t\t\t\t}\n\t\t\t\t}, _lifetimeDownload);\n\t\t\t\t_controls->clearThumbnail();\n\t\t\t}\n\t\t} else {\n\t\t\t_controls->clearThumbnail();\n\t\t}\n\n\t\t_lastAudioMsgId = current;\n\t}, _lifetime);\n\n\trpl::merge(\n\t\tmediaPlayer->playlistChanges(AudioMsgId::Type::Song)\n\t\t\t| rpl::map_to(AudioMsgId::Type::Song),\n\t\tmediaPlayer->playlistChanges(AudioMsgId::Type::Voice)\n\t\t\t| rpl::map_to(AudioMsgId::Type::Voice)\n\t) | rpl::on_next([=](AudioMsgId::Type type) {\n\t\t_controls->setIsNextEnabled(mediaPlayer->nextAvailable(type));\n\t\t_controls->setIsPreviousEnabled(mediaPlayer->previousAvailable(type));\n\t}, _lifetime);\n\n\tusing Media::RepeatMode;\n\tusing Media::OrderMode;\n\n\tCore::App().settings().playerRepeatModeValue(\n\t) | rpl::on_next([=](RepeatMode mode) {\n\t\t_controls->setLoopStatus(RepeatModeToLoopStatus(mode));\n\t}, _lifetime);\n\n\tCore::App().settings().playerOrderModeValue(\n\t) | rpl::on_next([=](OrderMode mode) {\n\t\tif (mode != OrderMode::Shuffle) {\n\t\t\t_lastOrderMode = mode;\n\t\t}\n\t\t_controls->setShuffle(mode == OrderMode::Shuffle);\n\t}, _lifetime);\n\n\t_controls->commandRequests(\n\t) | rpl::on_next([=](Command command) {\n\t\tconst auto type = mediaPlayer->current(AudioMsgId::Type::Song)\n\t\t\t? AudioMsgId::Type::Song\n\t\t\t: AudioMsgId::Type::Voice;\n\t\tswitch (command) {\n\t\tcase Command::PlayPause: mediaPlayer->playPause(type); break;\n\t\tcase Command::Play: mediaPlayer->play(type); break;\n\t\tcase Command::Pause: mediaPlayer->pause(type); break;\n\t\tcase Command::Next: mediaPlayer->next(type); break;\n\t\tcase Command::Previous: mediaPlayer->previous(type); break;\n\t\tcase Command::Stop: mediaPlayer->stop(type); break;\n\t\tcase Command::Raise: Core::App().activate(); break;\n\t\tcase Command::LoopNone: {\n\t\t\tCore::App().settings().setPlayerRepeatMode(RepeatMode::None);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\tbreak;\n\t\t}\n\t\tcase Command::LoopTrack: {\n\t\t\tCore::App().settings().setPlayerRepeatMode(RepeatMode::One);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\tbreak;\n\t\t}\n\t\tcase Command::LoopPlaylist: {\n\t\t\tCore::App().settings().setPlayerRepeatMode(RepeatMode::All);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\tbreak;\n\t\t}\n\t\tcase Command::Shuffle: {\n\t\t\tconst auto current = Core::App().settings().playerOrderMode();\n\t\t\tCore::App().settings().setPlayerOrderMode((current == OrderMode::Shuffle)\n\t\t\t\t? _lastOrderMode\n\t\t\t\t: OrderMode::Shuffle);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\tbreak;\n\t\t}\n\t\tcase Command::Quit: {\n\t\t\tMedia::Player::instance()->stopAndClose();\n\t\t\tbreak;\n\t\t}\n\t\t}\n\t}, _lifetime);\n\n\tif (_controls->seekingSupported()) {\n\t\trpl::merge(\n\t\t\tmediaPlayer->seekingChanges(AudioMsgId::Type::Song),\n\t\t\tmediaPlayer->seekingChanges(AudioMsgId::Type::Voice)\n\t\t) | rpl::filter([](Media::Player::Instance::Seeking seeking) {\n\t\t\treturn (seeking == Media::Player::Instance::Seeking::Finish);\n\t\t}) | rpl::map([=] {\n\t\t\tconst auto type = mediaPlayer->current(AudioMsgId::Type::Song)\n\t\t\t\t? AudioMsgId::Type::Song\n\t\t\t\t: AudioMsgId::Type::Voice;\n\t\t\treturn mediaPlayer->getState(type).position;\n\t\t}) | rpl::distinct_until_changed(\n\t\t) | rpl::on_next([=](int position) {\n\t\t\t_controls->setPosition(position);\n\t\t\t_controls->updateDisplay();\n\t\t}, _lifetime);\n\n\t\t_controls->seekRequests(\n\t\t) | rpl::on_next([=](float64 progress) {\n\t\t\tconst auto type = mediaPlayer->current(AudioMsgId::Type::Song)\n\t\t\t\t? AudioMsgId::Type::Song\n\t\t\t\t: AudioMsgId::Type::Voice;\n\t\t\tmediaPlayer->finishSeeking(type, progress);\n\t\t}, _lifetime);\n\n\t\t_controls->updatePositionRequests(\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto type = mediaPlayer->current(AudioMsgId::Type::Song)\n\t\t\t\t? AudioMsgId::Type::Song\n\t\t\t\t: AudioMsgId::Type::Voice;\n\t\t\t_controls->setPosition(mediaPlayer->getState(type).position);\n\t\t}, _lifetime);\n\t}\n\n\tCore::App().passcodeLockValue(\n\t) | rpl::filter([=](bool locked) {\n\t\treturn locked && Core::App().maybePrimarySession();\n\t}) | rpl::on_next([=] {\n\t\t_controls->setEnabled(false);\n\t}, _lifetime);\n\n\tif (_controls->volumeSupported()) {\n\t\trpl::single(\n\t\t\tCore::App().settings().songVolume()\n\t\t) | rpl::then(\n\t\t\tCore::App().settings().songVolumeChanges()\n\t\t) | rpl::on_next([=](float64 volume) {\n\t\t\t_controls->setVolume(volume);\n\t\t}, _lifetime);\n\n\t\t_controls->volumeChangeRequests(\n\t\t) | rpl::on_next([](float64 volume) {\n\t\t\tPlayer::mixer()->setSongVolume(volume);\n\t\t\tif (volume > 0) {\n\t\t\t\tCore::App().settings().setRememberedSongVolume(volume);\n\t\t\t}\n\t\t\tCore::App().settings().setSongVolume(volume);\n\t\t}, _lifetime);\n\t}\n\n}\n\nSystemMediaControlsManager::~SystemMediaControlsManager() = default;\n\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/system_media_controls_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_audio_msg_id.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"media/media_common.h\"\n#include \"ui/userpic_view.h\"\n\nnamespace base::Platform {\nclass SystemMediaControls;\n} // namespace base::Platform\n\nnamespace Data {\nclass DocumentMedia;\n} // namespace Data\n\nnamespace Window {\nclass Controller;\n} // namespace Window\n\nnamespace Media::Streaming {\nclass Instance;\n} // namespace Media::Streaming\n\nnamespace Media {\n\nclass SystemMediaControlsManager {\npublic:\n\tSystemMediaControlsManager();\n\t~SystemMediaControlsManager();\n\n\tstatic bool Supported();\n\nprivate:\n\tconst std::unique_ptr<base::Platform::SystemMediaControls> _controls;\n\n\tstd::vector<std::shared_ptr<Data::DocumentMedia>> _cachedMediaView;\n\tUi::PeerUserpicView _cachedUserpicView;\n\tstd::unique_ptr<Streaming::Instance> _streamed;\n\tAudioMsgId _lastAudioMsgId;\n\tOrderMode _lastOrderMode = OrderMode::Default;\n\n\trpl::lifetime _lifetimeDownload;\n\trpl::lifetime _lifetime;\n};\n\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\nusing \"ui/basic.style\";\nusing \"ui/widgets/widgets.style\";\nusing \"ui/menu_icons.style\";\nusing \"media/player/media_player.style\";\nusing \"media/stories/media_stories.style\";\nusing \"boxes/boxes.style\";\nusing \"calls/calls.style\";\nusing \"chat_helpers/chat_helpers.style\";\n\nmediaviewOverDuration: 150;\n\nmediaviewPlayback: MediaSlider {\n\twidth: 3px;\n\tactiveFg: mediaviewPlaybackActive;\n\tinactiveFg: mediaviewPlaybackInactive;\n\tactiveFgOver: mediaviewPlaybackActiveOver;\n\tinactiveFgOver: mediaviewPlaybackInactive;\n\tactiveFgDisabled: mediaviewPlaybackActive;\n\tinactiveFgDisabled: mediaviewPlaybackInactive;\n\treceivedTillFg: mediaviewPlaybackInactiveOver;\n\tseekSize: size(12px, 12px);\n\tduration: mediaviewOverDuration;\n}\nmediaviewPlaybackTimestamp: size(2px, 10px);\nmediaviewPlaybackTop: 49px;\nmediaviewPlayProgressTop: 46px;\n\nmediaviewControlsButton: IconButton {\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: mediaviewPlaybackIconRipple;\n\t}\n\trippleAreaPosition: point(0px, 0px);\n\tduration: mediaviewOverDuration;\n}\n\nmediaviewControllerSize: size(480px, 72px);\nmediaviewTimestampLabelTop: 63px;\nmediaviewTimestampLabelHeight: 10px;\nmediaviewTimestampLabel: LabelSimple(defaultLabelSimple) {\n\tfont: font(12px);\n\ttextFg: mediaviewPlaybackProgressFg;\n}\nmediaviewPlayProgressLabel: LabelSimple(defaultLabelSimple) {\n\tfont: font(12px semibold);\n\ttextFg: mediaviewPlaybackProgressFg;\n}\nmediaviewPlayProgressSkip: 10px;\nmediaviewPlayProgressLeft: 4px;\nmediaviewPlayButtonTop: 2px;\nmediaviewPlayButton: IconButton(mediaviewControlsButton) {\n\twidth: 40px;\n\theight: 40px;\n\trippleAreaSize: 40px;\n\n\ticon: icon {{ \"player/player_play_big\", mediaviewPlaybackIconFg }};\n\ticonOver: icon {{ \"player/player_play_big\", mediaviewPlaybackIconFgOver }};\n\ticonPosition: point(8px, 8px);\n}\nmediaviewPauseIcon: icon {{ \"player/player_pause_big\", mediaviewPlaybackIconFg }};\nmediaviewPauseIconOver: icon {{ \"player/player_pause_big\", mediaviewPlaybackIconFgOver }};\n\nmediaviewButtonsTop: 6px;\nmediaviewButtonsRight: 8px;\n\nmediaviewPipButtonSkip: 4px;\nmediaviewPipButton: IconButton(mediaviewControlsButton) {\n\twidth: 32px;\n\theight: 32px;\n\trippleAreaSize: 32px;\n\ticon: icon {{ \"player/player_pip\", mediaviewPlaybackIconFg }};\n\ticonOver: icon {{ \"player/player_pip\", mediaviewPlaybackIconFgOver }};\n\ticonPosition: point(4px, 4px);\n}\n\nmediaviewPipMargins: callShadowExtend;\nmediaviewPipShadow: callShadow;\n\nmediaviewFullScreenButtonSkip: 4px;\nmediaviewFullScreenButton: IconButton(mediaviewPipButton) {\n\ticon: icon {{ \"player/player_fullscreen\", mediaviewPlaybackIconFg }};\n\ticonOver: icon {{ \"player/player_fullscreen\", mediaviewPlaybackIconFgOver }};\n}\nmediaviewFullScreenOutIcon: icon {{ \"player/player_minimize\", mediaviewPlaybackIconFg }};\nmediaviewFullScreenOutIconOver: icon {{ \"player/player_minimize\", mediaviewPlaybackIconFgOver }};\n\nmediaviewVolumeWidth: 75px;\nmediaviewControllerRadius: 9px;\n\nmediaviewVolumeIcon0: icon {{ \"player/player_volume_off\", mediaviewPlaybackIconFg }};\nmediaviewVolumeIcon0Over: icon {{ \"player/player_volume_off\", mediaviewPlaybackIconFgOver }};\nmediaviewVolumeIcon1: icon {{ \"player/player_volume_small\", mediaviewPlaybackIconFg }};\nmediaviewVolumeIcon1Over: icon {{ \"player/player_volume_small\", mediaviewPlaybackIconFgOver }};\nmediaviewVolumeIcon2: icon {{ \"player/player_volume_on\", mediaviewPlaybackIconFg }};\nmediaviewVolumeIcon2Over: icon {{ \"player/player_volume_on\", mediaviewPlaybackIconFgOver }};\nmediaviewVolumeTop: 6px;\nmediaviewVolumeToggleLeft: 6px;\nmediaviewVolumeToggle: IconButton(mediaviewControlsButton) {\n\twidth: 32px;\n\theight: 32px;\n\trippleAreaSize: 32px;\n\ticon: mediaviewVolumeIcon0;\n\ticonOver: mediaviewVolumeIcon0Over;\n\ticonPosition: point(4px, 4px);\n}\nmediaviewVolumeSkip: 3px;\n\nmediaviewLeft: icon {\n\t{ \"mediaview/next_shadow-flip_horizontal\", windowShadowFg }\n\t{ \"mediaview/next-flip_horizontal\", mediaviewControlFg }\n};\nmediaviewRight: icon {\n\t{ \"mediaview/next_shadow\", windowShadowFg }\n\t{ \"mediaview/next\", mediaviewControlFg }\n};\nmediaviewSave: icon {{ \"mediaview/download\", mediaviewControlFg }};\nmediaviewSaveLocked: icon {{ \"mediaview/download_locked\", mediaviewControlFg }};\nmediaviewShare: icon {{ \"mediaview/viewer_share\", mediaviewControlFg }};\nmediaviewRotate: icon {{ \"mediaview/rotate\", mediaviewControlFg }};\nmediaviewMore: icon {{ \"title_menu_dots\", mediaviewControlFg }};\nmediaviewRecognize: icon {{ \"mediaview/recognize\", mediaviewControlFg }};\nmediaviewDraw: icon {{ \"mediaview/draw\", mediaviewControlFg }};\n\nmediaviewVoteButton: RoundButton(defaultActiveButton) {\n\theight: 36px;\n\tradius: 18px;\n}\nmediaviewVoteButtonMargin: 8px;\n\nmediaviewPollVotersUserpics: GroupCallUserpics {\n\tsize: 24px;\n\tshift: 8px;\n\tstroke: 2px;\n\talign: align(left);\n}\nmediaviewPollVotersPadding: margins(12px, 0px, 14px, 0px);\nmediaviewPollVotersSpacing: 6px;\n\nmediaviewFileRed: icon {\n\t{ size(25px, 25px), mediaviewFileBg },\n\t{ \"mediaview/file_corner\", mediaviewFileRedCornerFg },\n};\nmediaviewFileYellow: icon {\n\t{ size(25px, 25px), mediaviewFileBg },\n\t{ \"mediaview/file_corner\", mediaviewFileYellowCornerFg },\n};\nmediaviewFileGreen: icon {\n\t{ size(25px, 25px), mediaviewFileBg },\n\t{ \"mediaview/file_corner\", mediaviewFileGreenCornerFg },\n};\nmediaviewFileBlue: icon {\n\t{ size(25px, 25px), mediaviewFileBg },\n\t{ \"mediaview/file_corner\", mediaviewFileBlueCornerFg },\n};\n\nmediaviewFilePadding: 18px;\nmediaviewFileSize: size(340px, 116px);\nmediaviewFileNameTop: 4px;\nmediaviewFileNameFont: font(semibold 14px);\nmediaviewFileSizeTop: 29px;\nmediaviewFileExtTop: 35px;\nmediaviewFileExtFont: font(semibold 18px);\nmediaviewFileExtPadding: 10px;\nmediaviewFileLinksTop: 57px;\nmediaviewFileIconSize: 80px;\n\nmediaviewFileLink: defaultLinkButton;\n\nmediaviewMenuSeparator: MenuSeparator(defaultMenuSeparator) {\n\tfg: groupCallMenuBgOver;\n}\nmediaviewMenu: Menu(menuWithIcons) {\n\titemBg: groupCallMenuBg;\n\titemBgOver: groupCallMenuBgOver;\n\titemFg: groupCallMembersFg;\n\titemFgOver: groupCallMembersFg;\n\titemFgDisabled: groupCallMemberNotJoinedStatus;\n\titemFgShortcut: groupCallMemberNotJoinedStatus;\n\titemFgShortcutOver: groupCallMemberNotJoinedStatus;\n\titemFgShortcutDisabled: groupCallMemberNotJoinedStatus;\n\n\tseparator: mediaviewMenuSeparator;\n\tarrow: icon {{ \"menu/submenu_arrow\", groupCallMemberNotJoinedStatus }};\n\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: groupCallMenuBgRipple;\n\t}\n}\nmediaviewMenuShadow: Shadow(defaultEmptyShadow) {\n\tfallback: groupCallMenuBg;\n}\nmediaviewPanelAnimation: PanelAnimation(defaultPanelAnimation) {\n\tfadeBg: groupCallMenuBg;\n}\nmediaviewPopupMenu: PopupMenu(defaultPopupMenu) {\n\tshadowFallback: groupCallMenuBg;\n\tmenu: mediaviewMenu;\n\tanimation: mediaviewPanelAnimation;\n}\nmediaviewDropdownMenu: DropdownMenu(defaultDropdownMenu) {\n\tmenu: mediaviewMenu;\n\twrap: InnerDropdown(defaultInnerDropdown) {\n\t\tbg: groupCallMenuBg;\n\t\tanimation: mediaviewPanelAnimation;\n\t\tscrollPadding: margins(0px, 8px, 0px, 8px);\n\t\tshadow: mediaviewMenuShadow;\n\t}\n}\n\nmediaviewSaveMsgCheck: icon {{ \"mediaview/save_check\", mediaviewSaveMsgFg }};\nmediaviewSaveMsgPadding: margins(55px, 19px, 29px, 20px);\nmediaviewSaveMsgCheckPos: point(23px, 21px);\nmediaviewSaveMsgShowing: 200;\nmediaviewSaveMsgShown: 2000;\nmediaviewSaveMsgHiding: 2500;\nmediaviewSaveMsgStyle: TextStyle(defaultTextStyle) {\n\tfont: font(16px);\n}\nmediaviewChapterPadding: margins(20px, 12px, 20px, 12px);\nmediaviewChapterFont: font(15px semibold);\nmediaviewChapterShowing: 200;\nmediaviewChapterShown: 1500;\nmediaviewChapterHiding: 400;\nmediaviewChapterArrowSize: 10px;\nmediaviewChapterArrowWidth: 6px;\nmediaviewChapterArrowShift: 16px;\nmediaviewChapterArrowGap: 12px;\nmediaviewChapterArrowSlide: 350;\nmediaviewChapterArrowPause: 1500;\nmediaviewChapterArrowFade: 300;\n\nmediaviewSpeedBoostPadding: margins(12px, 6px, 12px, 6px);\nmediaviewSpeedBoostFont: font(16px semibold);\nmediaviewSpeedBoostShowing: 150;\nmediaviewSpeedBoostHiding: 200;\nmediaviewSpeedBoostArrowHeight: 9px;\nmediaviewSpeedBoostArrowWidth: 7px;\nmediaviewSpeedBoostArrowGap: 2px;\nmediaviewSpeedBoostArrowOverlap: 2px;\nmediaviewSpeedBoostTextRight: 8px;\nmediaviewSpeedBoostTop: 12px;\nmediaviewSpeedBoostHoldDelay: 200;\nmediaviewSpeedBoostDragPerUnit: 300px;\n\nmediaviewTextPalette: TextPalette(defaultTextPalette) {\n\tlinkFg: mediaviewTextLinkFg;\n\tmonoFg: mediaviewCaptionFg;\n\tspoilerFg: mediaviewCaptionFg;\n}\n\nmediaviewCaptionStyle: defaultTextStyle;\n\nmediaviewThickFont: semiboldFont;\nmediaviewFont: normalFont;\nmediaviewTextStyle: defaultTextStyle;\n\nmediaviewTextLeft: 14px;\nmediaviewTextSkip: 10px;\nmediaviewTextSkipHalf: 5px;\nmediaviewHeaderTop: 47px;\nmediaviewTextTop: 26px;\n\nmediaviewControlSize: 90px;\nmediaviewIconSize: size(46px, 54px);\nmediaviewIconOver: 36px;\n\nmediaviewWaitHide: 1100;\nmediaviewHideDuration: 600;\nmediaviewShowDuration: 200;\nmediaviewFadeDuration: 150;\n\nmediaviewDeltaFromLastAction: 5px;\nmediaviewSwipeDistance: 80px;\n\nmediaviewCaptionPadding: margins(11px, 6px, 11px, 6px);\nmediaviewCaptionMargin: size(11px, 11px);\nmediaviewCaptionRadius: 6px;\n\nmediaviewGroupPadding: margins(0px, 14px, 0px, 14px);\nmediaviewGroupHeight: 80px;\nmediaviewGroupWidth: 56px;\nmediaviewGroupWidthMax: 160px;\nmediaviewGroupSkip: 3px;\nmediaviewGroupSkipCurrent: 12px;\n\nmediaviewMinWidth: 480px;\nmediaviewMinHeight: 360px;\nmediaviewDefaultLeft: 160px;\nmediaviewDefaultTop: 120px;\nmediaviewDefaultWidth: 800px;\nmediaviewDefaultHeight: 600px;\n\nmediaviewTitleMinimize: icon{\n\t{ \"mediaview/title_viewer_shadow_minimize\", windowShadowFg },\n\t{ \"mediaview/title_viewer_button_minimize\", mediaviewControlFg },\n};\nmediaviewTitleMaximize: icon{\n\t{ \"mediaview/title_viewer_shadow_maximize\", windowShadowFg },\n\t{ \"mediaview/title_viewer_button_maximize\", mediaviewControlFg },\n};\nmediaviewTitleRestore: icon{\n\t{ \"mediaview/title_viewer_shadow_restore\", windowShadowFg },\n\t{ \"mediaview/title_viewer_button_restore\", mediaviewControlFg },\n};\nmediaviewTitleClose: icon{\n\t{ \"mediaview/title_viewer_shadow_close\", windowShadowFg },\n\t{ \"mediaview/title_viewer_button_close\", mediaviewControlFg },\n};\nmediaviewTitleButton: IconButton(windowTitleButton) {\n\twidth: 44px;\n\theight: 32px;\n}\nmediaviewTitle: WindowTitle(defaultWindowTitle) {\n\theight: 0px;\n\tbg: mediaviewBg;\n\tbgActive: mediaviewBg;\n\tfg: transparent;\n\tfgActive: transparent;\n\tminimize: IconButton(mediaviewTitleButton) {\n\t\ticon: mediaviewTitleMinimize;\n\t\ticonOver: mediaviewTitleMinimize;\n\t}\n\tminimizeIconActive: mediaviewTitleMinimize;\n\tminimizeIconActiveOver: mediaviewTitleMinimize;\n\tmaximize: IconButton(mediaviewTitleButton) {\n\t\ticon: mediaviewTitleMaximize;\n\t\ticonOver: mediaviewTitleMaximize;\n\t}\n\tmaximizeIconActive: mediaviewTitleMaximize;\n\tmaximizeIconActiveOver: mediaviewTitleMaximize;\n\trestoreIcon: mediaviewTitleRestore;\n\trestoreIconOver: mediaviewTitleRestore;\n\trestoreIconActive: mediaviewTitleRestore;\n\trestoreIconActiveOver: mediaviewTitleRestore;\n\tclose: IconButton(mediaviewTitleButton) {\n\t\ticon: mediaviewTitleClose;\n\t\ticonOver: mediaviewTitleClose;\n\t}\n\tcloseIconActive: mediaviewTitleClose;\n\tcloseIconActiveOver: mediaviewTitleClose;\n\tcloseIconActive: mediaviewTitleClose;\n\tcloseIconActiveOver: mediaviewTitleClose;\n\ttop: mediaviewTitleButton;\n\ttopIconActive: mediaviewTitleClose;\n\ttopIconActiveOver: mediaviewTitleClose;\n\ttop2Icon: mediaviewTitleClose;\n\ttop2IconOver: mediaviewTitleClose;\n\ttop2IconActive: mediaviewTitleClose;\n\ttop2IconActiveOver: mediaviewTitleClose;\n}\n\nmediaviewTitleButtonMac: icon{\n\t{ \"mediaview/title_button_shadow_mac\", windowShadowFg },\n\t{ \"mediaview/title_button_mac\", mediaviewControlFg },\n};\nmediaviewTitleMinimizeMac: icon {{ \"mediaview/title_button_minimize_mac\", mediaviewBg }};\nmediaviewTitleMaximizeMac: icon {{ \"mediaview/title_button_maximize_mac\", mediaviewBg }};\nmediaviewTitleRestoreMac: icon {{ \"mediaview/title_button_shrink_mac\", mediaviewBg }};\nmediaviewTitleCloseMac: icon {{ \"mediaview/title_button_close_mac\", mediaviewBg }};\nmediaviewTitleCloseMacPadding: margins(8px, 4px, 0px, 4px);\nmediaviewTitleMinimizeMacPadding: margins(0px, 4px, 0px, 4px);\nmediaviewTitleMaximizeMacPadding: margins(0px, 4px, 8px, 4px);\n\nmediaviewShadowTop: icon{{ \"mediaview/shadow_top\", windowShadowFg }};\nmediaviewShadowBottom: icon{{ \"mediaview/shadow_bottom\", windowShadowFg }};\nmediaviewWideMenuSeparator: MenuSeparator(mediaviewMenuSeparator) {\n\tpadding: margins(0px, 4px, 0px, 4px);\n\twidth: 6px;\n}\nmediaviewSpeedMenuInner: Menu(mediaviewMenu) {\n\tseparator: mediaviewWideMenuSeparator;\n\titemPadding: margins(54px, 7px, 54px, 9px);\n\titemFgDisabled: mediaviewTextLinkFg;\n}\nmediaviewSpeedMenu: MediaSpeedMenu(mediaPlayerSpeedMenu) {\n\tdropdown: DropdownMenu(mediaviewDropdownMenu) {\n\t\tmenu: mediaviewSpeedMenuInner;\n\t}\n\tqualityMenu: Menu(mediaviewSpeedMenuInner) {\n\t\titemPadding: margins(17px, 7px, 54px, 9px);\n\t}\n\tactiveCheck: icon {{ \"player/player_check\", mediaviewTextLinkFg }};\n\tslider: MediaSlider(defaultContinuousSlider) {\n\t\tactiveFg: mediaviewTextLinkFg;\n\t\tinactiveFg: mediaviewMenuBgOver;\n\t\tactiveFgOver: mediaviewTextLinkFg;\n\t\tinactiveFgOver: mediaviewMenuBgOver;\n\t\tactiveFgDisabled: mediaviewMenuBgOver;\n\t\treceivedTillFg: mediaviewMenuBgOver;\n\t\twidth: 6px;\n\t\tseekSize: size(6px, 6px);\n\t}\n\n\tslow: mediaSpeedSlow;\n\tslowActive: mediaSpeedSlowActive;\n\tnormal: mediaSpeedNormal;\n\tnormalActive: mediaSpeedNormalActive;\n\tmedium: mediaSpeedMedium;\n\tmediumActive: mediaSpeedMediumActive;\n\tfast: mediaSpeedFast;\n\tfastActive: mediaSpeedFastActive;\n\tveryFast: mediaSpeedVeryFast;\n\tveryFastActive: mediaSpeedVeryFastActive;\n\tsuperFast: mediaSpeedSuperFast;\n\tsuperFastActive: mediaSpeedSuperFastActive;\n}\nmediaviewSpeedButton: MediaSpeedButton(mediaPlayerSpeedButton) {\n\tsize: size(32px, 32px);\n\tpadding: margins(0px, 0px, 0px, 0px);\n\tfont: font(8px bold);\n\tfg: mediaviewPlaybackIconFg;\n\toverFg: mediaviewPlaybackIconFgOver;\n\tactiveFg: mediaviewTextLinkFg;\n\ticon: icon{{ \"player/player_settings\", mediaviewPlaybackIconFg }};\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: mediaviewPlaybackIconRipple;\n\t}\n\trippleActiveColor: mediaviewPlaybackIconRipple;\n\trippleRadius: 16px;\n\tmenu: mediaviewSpeedMenu;\n\tmenuAlign: align(bottomright);\n\tmenuPosition: point(-2px, 31px);\n}\n\nthemePreviewSize: size(903px, 584px);\nthemePreviewBg: windowBg;\nthemePreviewOverlayOpacity: 0.8;\nthemePreviewMargin: margins(36px, 72px, 36px, 88px);\nthemePreviewTitleTop: 24px;\nthemePreviewTitleFg: windowBoldFg;\nthemePreviewTitleFont: font(17px semibold);\nthemePreviewLoadingFont: font(16px);\nthemePreviewLoadingFg: windowSubTextFg;\nthemePreviewApplyButton: RoundButton(defaultActiveButton) {\n\theight: 38px;\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(15px semibold);\n\t}\n}\nthemePreviewCancelButton: RoundButton(defaultLightButton) {\n\theight: 38px;\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(15px semibold);\n\t}\n}\nthemePreviewButtonsSkip: 20px;\nthemePreviewDialogsWidth: 312px;\n\npipDefaultSize: 320px;\npipMinimalSize: 120px;\npipBorderSkip: 20px;\npipBorderSnapArea: 16px;\npipResizeArea: 10px;\npipControlSkip: 6px;\npipPlaybackWidth: 2px;\npipPlaybackWide: 4px;\npipPlaybackSkip: 4px;\npipPlaybackTextSkip: 6px;\npipPlaybackFont: font(11px);\npipPlayIcon: icon {{ \"player/player_pip_play\", mediaviewPipControlsFg }};\npipPlayIconOver: icon {{ \"player/player_pip_play\", mediaviewPipControlsFgOver }};\npipPauseIcon: icon {{ \"player/player_pip_pause\", mediaviewPipControlsFg }};\npipPauseIconOver: icon {{ \"player/player_pip_pause\", mediaviewPipControlsFgOver }};\npipCloseIcon: icon {{ \"player/player_pip_close\", mediaviewPipControlsFg }};\npipCloseIconOver: icon {{ \"player/player_pip_close\", mediaviewPipControlsFgOver }};\npipEnlargeIcon: icon {{ \"player/player_pip_enlarge\", mediaviewPipControlsFg }};\npipEnlargeIconOver: icon {{ \"player/player_pip_enlarge\", mediaviewPipControlsFgOver }};\npipVolumeIcon0: icon {{ \"player/player_volume_off\", mediaviewPipControlsFg }};\npipVolumeIcon0Over: icon {{ \"player/player_volume_off\", mediaviewPipControlsFgOver }};\npipVolumeIcon1: icon {{ \"player/player_volume_small\", mediaviewPipControlsFg }};\npipVolumeIcon1Over: icon {{ \"player/player_volume_small\", mediaviewPipControlsFgOver }};\npipVolumeIcon2: icon {{ \"player/player_volume_on\", mediaviewPipControlsFg }};\npipVolumeIcon2Over: icon {{ \"player/player_volume_on\", mediaviewPipControlsFgOver }};\n\nspeedSliderDividerSize: size(2px, 8px);\n\nstoriesMaxSize: size(540px, 960px);\nstoriesSiblingWidthMin: 200px; // Try making sibling not less than this.\nstoriesMaxNameFontSize: 17px;\nstoriesRadius: 8px;\nstoriesControlSize: 64px;\nstoriesLeft: icon {{ \"stories/next-flip_horizontal\", mediaviewControlFg }};\nstoriesRight: icon {{ \"stories/next\", mediaviewControlFg }};\nstoriesSliderWidth: 2px;\nstoriesSliderMargin: margins(8px, 7px, 8px, 6px);\nstoriesSliderSkip: 4px;\nstoriesSliderOutsideSkip: 4px;\nstoriesHeaderMargin: margins(12px, 4px, 12px, 8px);\nstoriesHeaderPhoto: UserpicButton(defaultUserpicButton) {\n\tsize: size(28px, 28px);\n\tphotoSize: 28px;\n}\nstoriesHeaderName: FlatLabel(defaultFlatLabel) {\n\ttextFg: mediaviewControlFg;\n\tstyle: semiboldTextStyle;\n\tminWidth: 10px;\n\tmaxHeight: 20px;\n}\nstoriesHeaderNamePosition: point(50px, 0px);\nstoriesHeaderDate: FlatLabel(defaultFlatLabel) {\n\ttextFg: mediaviewControlFg;\n\tpalette: TextPalette(defaultTextPalette) {\n\t\tlinkFg: mediaviewControlFg;\n\t}\n\tminWidth: 10px;\n\tmaxHeight: 20px;\n}\nstoriesHeaderDatePosition: point(50px, 17px);\nstoriesHeaderRepostWidthMin: 40px;\nstoriesShadowTop: icon{{ \"mediaview/shadow_bottom-flip_vertical\", windowShadowFg }};\nstoriesShadowBottom: mediaviewShadowBottom;\nstoriesControlsMinWidth: 280px;\nstoriesControlsExtend: 4px;\nstoriesFieldMargin: margins(0px, 18px, 0px, 12px);\nstoriesSideSkip: 145px;\nstoriesCaptionFull: FlatLabel(defaultFlatLabel) {\n\tstyle: mediaviewCaptionStyle;\n\ttextFg: mediaviewCaptionFg;\n\tpalette: mediaviewTextPalette;\n\tminWidth: 36px;\n}\nstoriesComposeBg: groupCallMembersBg;\nstoriesComposeBgOver: groupCallMembersBgOver;\nstoriesComposeBgRipple: groupCallMembersBgRipple;\nstoriesComposeWhiteText: groupCallMembersFg;\nstoriesComposeGrayText: groupCallMemberNotJoinedStatus;\nstoriesComposeGrayIcon: groupCallMemberInactiveIcon;\nstoriesComposeBlue: groupCallActiveFg;\nstoriesComposeRippleLight: RippleAnimation(defaultRippleAnimation) {\n\tcolor: storiesComposeBgOver;\n}\nstoriesComposeRipple: RippleAnimation(defaultRippleAnimation) {\n\tcolor: groupCallMembersBgRipple;\n}\nstoriesAttach: IconButton(historyAttach) {\n\twidth: 42px;\n\theight: 42px;\n\ticon: icon {{ \"chat/input_attach\", storiesComposeGrayIcon }};\n\ticonOver: icon {{ \"chat/input_attach\", storiesComposeGrayIcon }};\n\trippleAreaPosition: point(1px, 1px);\n\tripple: storiesComposeRippleLight;\n}\nstoriesLike: IconButton(storiesAttach) {\n\ticon: icon {{ \"chat/input_like\", storiesComposeGrayIcon }};\n\ticonOver: icon {{ \"chat/input_like\", storiesComposeGrayIcon }};\n}\nstoriesEditStars: IconButton(storiesAttach) {\n\ticon: icon{{ \"chat/input_paid\", storiesComposeGrayIcon }};\n\ticonOver: icon{{ \"chat/input_paid\", storiesComposeGrayIcon }};\n}\nstoriesRemoveSet: IconButton(stickerPanRemoveSet) {\n\ticon: icon {{ \"simple_close\", storiesComposeGrayIcon }};\n\ticonOver: icon {{ \"simple_close\", storiesComposeGrayIcon }};\n\tripple: storiesComposeRippleLight;\n}\nstoriesColorAll: IconButton(emojiPanColorAll) {\n\ticon: icon {{ \"emoji/emoji_skin\", storiesComposeGrayIcon }};\n\ticonOver: icon {{ \"emoji/emoji_skin\", storiesComposeGrayIcon }};\n\tripple: storiesComposeRippleLight;\n}\nstoriesColorAllLabel: FlatLabel(emojiPanColorAllLabel) {\n\ttextFg: storiesComposeGrayText;\n}\nstoriesMenuSeparator: mediaviewMenuSeparator;\nstoriesMenu: Menu(defaultMenu) {\n\titemBg: groupCallMenuBg;\n\titemBgOver: groupCallMenuBgOver;\n\titemFg: groupCallMembersFg;\n\titemFgOver: groupCallMembersFg;\n\titemFgDisabled: groupCallMemberNotJoinedStatus;\n\titemFgShortcut: groupCallMemberNotJoinedStatus;\n\titemFgShortcutOver: groupCallMemberNotJoinedStatus;\n\titemFgShortcutDisabled: groupCallMemberNotJoinedStatus;\n\n\tseparator: storiesMenuSeparator;\n\tarrow: icon {{ \"menu/submenu_arrow\", groupCallMemberNotJoinedStatus }};\n\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: groupCallMenuBgRipple;\n\t}\n}\nstoriesMenuAnimation: mediaviewPanelAnimation;\nstoriesPopupMenu: PopupMenu(defaultPopupMenu) {\n\tshadowFallback: groupCallMenuBg;\n\tmenu: storiesMenu;\n\tanimation: storiesMenuAnimation;\n}\nstoriesMenuWithIcons: mediaviewMenu;\nstoriesPopupMenuWithIcons: PopupMenu(storiesPopupMenu) {\n\tscrollPadding: margins(0px, 5px, 0px, 5px);\n\tmenu: storiesMenuWithIcons;\n}\n\nstoriesBoxLabel: FlatLabel(boxLabel) {\n\ttextFg: groupCallMembersFg;\n}\nstoriesAttachEmojiInner: IconButton(storiesAttach) {\n\ticon: icon {{ \"chat/input_smile_face\", storiesComposeGrayIcon }};\n\ticonOver: icon {{ \"chat/input_smile_face\", storiesComposeGrayIcon }};\n}\nstoriesAttachEmoji: EmojiButton(historyAttachEmoji) {\n\tinner: storiesAttachEmojiInner;\n\tbg: storiesComposeBg;\n\tlineFg: storiesComposeGrayIcon;\n\tlineFgOver: storiesComposeGrayIcon;\n}\nstoriesComposePremium: PremiumLimits(defaultPremiumLimits) {\n\tboxLabel: storiesBoxLabel;\n\tnonPremiumBg: storiesComposeBgOver;\n\tnonPremiumFg: storiesComposeWhiteText;\n}\nstoriesEmojiTabbedSearch: TabbedSearch(defaultTabbedSearch) {\n\touter: storiesComposeBg;\n\tbg: storiesComposeBgOver;\n\tbgActive: storiesComposeBgRipple;\n\tfg: storiesComposeGrayIcon;\n\tfgActive: storiesComposeWhiteText;\n\tfadeLeft: icon {{ \"fade_horizontal-flip_horizontal\", storiesComposeBgOver }};\n\tfadeRight: icon {{ \"fade_horizontal\", storiesComposeBgOver }};\n\tfield: InputField(defaultTabbedSearchField) {\n\t\ttextFg: storiesComposeWhiteText;\n\t\tplaceholderFg: storiesComposeGrayText;\n\t\tplaceholderFgActive: storiesComposeGrayText;\n\t\tplaceholderFgError: storiesComposeGrayText;\n\t}\n\tsearch: IconButton(defaultTabbedSearchButton) {\n\t\ticon: icon{{ \"emoji/emoji_search_input\", storiesComposeGrayIcon }};\n\t\ticonOver: icon{{ \"emoji/emoji_search_input\", storiesComposeGrayIcon }};\n\t\tripple: storiesComposeRipple;\n\t}\n\tback: IconButton(defaultTabbedSearchBack) {\n\t\ticon: icon{{ \"emoji/emoji_back\", storiesComposeGrayIcon }};\n\t\ticonOver: icon{{ \"emoji/emoji_back\", storiesComposeGrayIcon }};\n\t\tripple: storiesComposeRipple;\n\t}\n\tcancel: CrossButton(defaultTabbedSearchCancel) {\n\t\tcrossFg: storiesComposeGrayIcon;\n\t\tcrossFgOver: storiesComposeGrayIcon;\n\t\tripple: emptyRippleAnimation;\n\t}\n}\nstoriesEmojiPan: EmojiPan(defaultEmojiPan) {\n\tshowAnimation: PanelAnimation(emojiPanAnimation) {\n\t\tfadeBg: storiesComposeBg;\n\t}\n\tbg: storiesComposeBg;\n\theaderFg: storiesComposeGrayText;\n\ttrendingHeaderFg: storiesComposeWhiteText;\n\ttrendingSubheaderFg: storiesComposeGrayText;\n\ttrendingUnreadFg: storiesComposeBlue;\n\toverBg: storiesComposeBgOver;\n\tpathBg: storiesComposeBgRipple;\n\tpathFg: storiesComposeBgOver;\n\ttextFg: storiesComposeWhiteText;\n\tcategoriesBg: storiesComposeBg;\n\tcategoriesBgOver: storiesComposeBgOver;\n\tfadeLeft: icon {{ \"fade_horizontal-flip_horizontal\", storiesComposeBg }};\n\tfadeRight: icon {{ \"fade_horizontal\", storiesComposeBg }};\n\tmenu: storiesPopupMenuWithIcons;\n\texpandedSeparator: mediaviewWideMenuSeparator;\n\ttabs: SettingsSlider(emojiTabs) {\n\t\tbarFgActive: storiesComposeBlue;\n\t\tlabelFg: storiesComposeGrayText;\n\t\tlabelFgActive: storiesComposeBlue;\n\t\trippleBg: storiesComposeBgOver;\n\t\trippleBgActive: storiesComposeBgOver;\n\t}\n\tsearch: storiesEmojiTabbedSearch;\n\tcolorAll: storiesColorAll;\n\tcolorAllLabel: storiesColorAllLabel;\n\tremoveSet: storiesRemoveSet;\n\tboxLabel: storiesBoxLabel;\n\tabout: FlatLabel(defaultEmojiPanAbout) {\n\t\ttextFg: storiesComposeGrayText;\n\t}\n\ticons: ComposeIcons {\n\t\tsettings: icon {{ \"emoji/emoji_settings\", storiesComposeGrayIcon }};\n\t\tcollectibles: icon {{ \"menu/unique\", storiesComposeGrayIcon }};\n\n\t\trecent: icon {{ \"emoji/emoji_recent\", storiesComposeGrayIcon }};\n\t\trecentActive: icon {{ \"emoji/emoji_recent\", storiesComposeWhiteText }};\n\t\tpeople: icon {{ \"emoji/emoji_smile\", storiesComposeGrayIcon }};\n\t\tpeopleActive: icon {{ \"emoji/emoji_smile\", storiesComposeWhiteText }};\n\t\tnature: icon {{ \"emoji/emoji_nature\", storiesComposeGrayIcon }};\n\t\tnatureActive: icon {{ \"emoji/emoji_nature\", storiesComposeWhiteText }};\n\t\tfood: icon {{ \"emoji/emoji_food\", storiesComposeGrayIcon }};\n\t\tfoodActive: icon {{ \"emoji/emoji_food\", storiesComposeWhiteText }};\n\t\tactivity: icon {{ \"emoji/emoji_activities\", storiesComposeGrayIcon }};\n\t\tactivityActive: icon {{ \"emoji/emoji_activities\", storiesComposeWhiteText }};\n\t\ttravel: icon {{ \"emoji/emoji_travel\", storiesComposeGrayIcon }};\n\t\ttravelActive: icon {{ \"emoji/emoji_travel\", storiesComposeWhiteText }};\n\t\tobjects: icon {{ \"emoji/emoji_objects\", storiesComposeGrayIcon }};\n\t\tobjectsActive: icon {{ \"emoji/emoji_objects\", storiesComposeWhiteText }};\n\t\tsymbols: icon {{ \"emoji/emoji_love\", storiesComposeGrayIcon }};\n\t\tsymbolsActive: icon {{ \"emoji/emoji_love\", storiesComposeWhiteText }};\n\n\t\tmenuFave: icon {{ \"menu/favorite\", storiesComposeWhiteText }};\n\t\tmenuUnfave: icon {{ \"menu/unfavorite\", storiesComposeWhiteText }};\n\t\tmenuStickerSet: icon {{ \"menu/stickers\", storiesComposeWhiteText }};\n\t\tmenuRecentRemove: icon {{ \"menu/delete\", storiesComposeWhiteText }};\n\t\tmenuGifAdd: icon {{ \"menu/gif\", storiesComposeWhiteText }};\n\t\tmenuGifRemove: icon {{ \"menu/delete\", storiesComposeWhiteText }};\n\t\tmenuMute: icon {{ \"menu/mute\", storiesComposeWhiteText }};\n\t\tmenuSchedule: icon {{ \"menu/calendar\", storiesComposeWhiteText }};\n\t\tmenuWhenOnline: icon {{ \"menu/send_when_online\", storiesComposeWhiteText }};\n\t\tmenuSpoiler: icon {{ \"menu/spoiler_on\", storiesComposeWhiteText }};\n\t\tmenuBelow: icon {{ \"menu/link_below\", storiesComposeWhiteText }};\n\t\tmenuAbove: icon {{ \"menu/link_above\", storiesComposeWhiteText }};\n\t\tmenuQualityHigh: icon {{ \"menu/quality_hd\", storiesComposeWhiteText }};\n\t\tmenuPrice: icon {{ \"menu/earn\", storiesComposeWhiteText }};\n\t\tmenuEditStars: icon {{ \"chat/input_paid\", storiesComposeWhiteText, point(-8px, -8px) }};\n\n\t\tstripBubble: icon{\n\t\t\t{ \"chat/reactions_bubble_shadow\", windowShadowFg },\n\t\t\t{ \"chat/reactions_bubble\", storiesComposeBg },\n\t\t};\n\t\tstripExpandPanel: icon{\n\t\t\t{ \"chat/reactions_round_big\", storiesComposeBgRipple },\n\t\t\t{ \"chat/reactions_expand_panel\", storiesComposeGrayIcon },\n\t\t};\n\t\tstripExpandDropdown: icon{\n\t\t\t{ \"chat/reactions_round_small\", storiesComposeBgRipple },\n\t\t\t{ \"chat/reactions_expand_panel\", storiesComposeGrayIcon },\n\t\t};\n\t}\n\tautocompleteBottomSkip: 10px;\n}\nstoriesBoxInputField: InputField(defaultComposeFilesField) {\n\ttextFg: storiesComposeWhiteText;\n\ttextBg: storiesComposeBg;\n\tplaceholderFg: storiesComposeGrayText;\n\tplaceholderFgActive: storiesComposeBlue;\n\tborderFg: storiesComposeGrayText;\n\tborderFgActive: storiesComposeBlue;\n\tmenu: storiesPopupMenu;\n}\nstoriesChooseSendAs: ChooseSendAs(defaultChooseSendAs) {\n\tbutton: SendAsButton(defaultSendAsButton) {\n\t\twidth: 42px;\n\t\theight: 42px;\n\t}\n\tlabel: FlatLabel(defaultFlatLabel) {\n\t\tminWidth: 272px;\n\t\ttextFg: storiesComposeWhiteText;\n\t}\n\tlist: groupCallJoinAsList;\n}\nstoriesCommentSentAt: Menu(storiesMenu, whoSentItem) {\n\titemFgDisabled: groupCallMembersFg;\n}\nstoriesComposeControls: ComposeControls(defaultComposeControls) {\n\tbg: storiesComposeBg;\n\tradius: 21px;\n\tpadding: margins(1px, 8px, 1px, 6px);\n\tfield: InputField(historyComposeField) {\n\t\ttextMargins: margins(2px, 0px, 2px, 0px);\n\t\ttextFg: storiesComposeWhiteText;\n\t\ttextBg: storiesComposeBg;\n\t\tplaceholderFg: storiesComposeGrayText;\n\t\tplaceholderFgActive: storiesComposeGrayText;\n\t\tplaceholderFgError: storiesComposeGrayText;\n\t\tmenu: storiesPopupMenu;\n\t}\n\tfieldLeft: 10px;\n\tsend: SendButton(historySend) {\n\t\tsendIconPosition: point(9px, 9px);\n\t\tsendIconFillPadding: 5px;\n\t\tinner: IconButton(storiesAttach) {\n\t\t\twidth: 42px;\n\t\t\theight: 42px;\n\t\t\ticon: icon {{ \"chat/input_send_round\", windowFgActive }};\n\t\t\ticonOver: icon {{ \"chat/input_send_round\", windowFgActive }};\n\t\t\trippleAreaSize: 40px;\n\t\t\trippleAreaPosition: point(1px, 1px);\n\t\t}\n\t\tsendDisabledFg: storiesComposeGrayText;\n\t\tsendIconFg: windowFgActive;\n\t}\n\tattach: storiesAttach;\n\temoji: storiesAttachEmoji;\n\tlike: storiesLike;\n\tliked: icon{};\n\teditStars: storiesEditStars;\n\tcommentsShow: IconButton(storiesAttach) {\n\t\ticon: icon {{ \"chat/input_comments_expand\", storiesComposeGrayIcon }};\n\t\ticonOver: icon {{ \"chat/input_comments_expand\", storiesComposeGrayIcon }};\n\t}\n\tcommentsShown: icon {{ \"chat/input_comments_hide\", storiesComposeGrayIcon }};\n\tcommentsSkip: 8px;\n\tcommentsUnreadSize: 6px;\n\tcommentsUnreadMargin: 2px;\n\tcommentsUnreadPosition: point(24px, 8px);\n\tstarsReactionCounter: RoundButton(defaultActiveButton) {\n\t\tstyle: TextStyle(semiboldTextStyle) {\n\t\t\tfont: font(9px semibold);\n\t\t}\n\t\ttextFg: storiesComposeGrayIcon;\n\t\ttextFgOver: storiesComposeGrayIcon;\n\t\ttextBg: storiesComposeBg;\n\t\ttextBgOver: storiesComposeBg;\n\t\tnumbersSkip: 2px;\n\t\tpadding: margins(4px, 0px, 4px, 1px);\n\t\ttextTop: 1px;\n\t\tripple: storiesComposeRippleLight;\n\t}\n\tstarsSkip: 8px;\n\tsuggestions: EmojiSuggestions(defaultEmojiSuggestions) {\n\t\tdropdown: InnerDropdown(emojiSuggestionsDropdown) {\n\t\t\tanimation: PanelAnimation(defaultPanelAnimation) {\n\t\t\t\tfadeBg: storiesComposeBg;\n\t\t\t}\n\t\t\tbg: storiesComposeBg;\n\t\t}\n\t\tbg: storiesComposeBg;\n\t\toverBg: storiesComposeBgOver;\n\t\ttextFg: storiesComposeWhiteText;\n\t\tfadeLeft: icon {{ \"fade_horizontal-flip_horizontal\", storiesComposeBg }};\n\t\tfadeRight: icon {{ \"fade_horizontal\", storiesComposeBg }};\n\t}\n\ttabbed: storiesEmojiPan;\n\ttabbedHeightMin: 220px;\n\ttabbedHeightMax: 480px;\n\trecord: RecordBar(defaultRecordBar) {\n\t\tbg: storiesComposeBg;\n\t\tdurationFg: storiesComposeWhiteText;\n\t\tradius: storiesRadius;\n\t\tcancel: storiesComposeGrayText;\n\t\tcancelActive: storiesComposeBlue;\n\t\tcancelRipple: storiesComposeRippleLight;\n\t\tlock: RecordBarLock(defaultRecordBarLock) {\n\t\t\tripple: storiesComposeRipple;\n\t\t\toriginTop: icon {{ \"voice_lock/record_lock_top\", storiesComposeBg }};\n\t\t\toriginBottom: icon {{ \"voice_lock/record_lock_bottom\", storiesComposeBg }};\n\t\t\toriginBody: icon {{ \"voice_lock/record_lock_body\", storiesComposeBg }};\n\t\t\tarrow: icon {{ \"voice_lock/voice_arrow\", storiesComposeGrayIcon }};\n\t\t\tfg: storiesComposeGrayIcon;\n\t\t}\n\t\tremove: IconButton(storiesAttach) {\n\t\t\ticon: icon {{ \"info/info_media_delete\", storiesComposeGrayIcon }};\n\t\t\ticonOver: icon {{ \"info/info_media_delete\", storiesComposeGrayIcon }};\n\t\t\ticonPosition: point(10px, 11px);\n\t\t}\n\t}\n\tfiles: ComposeFiles(defaultComposeFiles) {\n\t\tcheck: Check(defaultCheck) {\n\t\t\tuntoggledFg: groupCallMemberNotJoinedStatus;\n\t\t\ttoggledFg: groupCallActiveFg;\n\t\t\ticon: icon {{ \"default_checkbox_check\", groupCallMembersFg, point(4px, 7px) }};\n\t\t}\n\t\tcheckbox: Checkbox(defaultBoxCheckbox) {\n\t\t\ttextFg: groupCallMembersFg;\n\t\t\ttextFgActive: groupCallMembersFg;\n\t\t\trippleBg: groupCallMembersBgRipple;\n\t\t\trippleBgActive: groupCallMembersBgRipple;\n\t\t}\n\t\tmenu: IconButton(defaultComposeFilesMenu) {\n\t\t\ticon: icon {{ \"title_menu_dots\", storiesComposeGrayIcon }};\n\t\t\ticonOver: icon {{ \"title_menu_dots\", storiesComposeGrayIcon }};\n\t\t\tripple: storiesComposeRippleLight;\n\t\t}\n\t\tcaption: storiesBoxInputField;\n\t\temoji: EmojiButton(storiesAttachEmoji) {\n\t\t\tinner: IconButton(storiesAttachEmojiInner) {\n\t\t\t\twidth: 30px;\n\t\t\t\theight: 30px;\n\t\t\t\trippleAreaSize: 0px;\n\t\t\t}\n\t\t}\n\t\tconfirmBg: storiesComposeBgOver;\n\t\tbuttonFile: IconButton(sendBoxAlbumGroupButtonFile) {\n\t\t\tripple: storiesComposeRipple;\n\t\t}\n\t\tbuttonFileEdit: icon {{ \"send_media/send_media_replace\", storiesComposeGrayIcon }};\n\t\tbuttonFileDelete: icon {{ \"send_media/send_media_delete\", storiesComposeGrayIcon }};\n\t\ticonBg: storiesComposeBlue;\n\t\ticonPlay: icon {{ \"history_file_play\", storiesComposeWhiteText }};\n\t\ticonImage: icon {{ \"history_file_image\", storiesComposeWhiteText }};\n\t\ticonDocument: icon {{ \"history_file_document\", storiesComposeWhiteText }};\n\t\tnameFg: storiesComposeWhiteText;\n\t\tstatusFg: storiesComposeGrayText;\n\t}\n\tpremium: storiesComposePremium;\n\tboxField: InputField(defaultInputField) {\n\t\ttextBg: transparent;\n\t\ttextFg: groupCallMembersFg;\n\n\t\tplaceholderFg: groupCallMemberNotJoinedStatus;\n\t\tplaceholderFgActive: groupCallMemberNotJoinedStatus;\n\t\tplaceholderFgError: groupCallMemberNotJoinedStatus;\n\n\t\tborderFg: inputBorderFg;\n\t\tborderFgActive: groupCallMemberInactiveStatus;\n\t\tborderFgError: activeLineFgError;\n\n\t\tmenu: storiesPopupMenu;\n\t}\n\trestrictionLabel: FlatLabel(defaultFlatLabel) {\n\t\tminWidth: 12px;\n\t\ttextFg: storiesComposeGrayText;\n\t\talign: align(top);\n\t}\n\tpremiumRequired: ComposePremiumRequired {\n\t\tlabel: FlatLabel(defaultFlatLabel) {\n\t\t\tminWidth: 12px;\n\t\t\ttextFg: storiesComposeGrayText;\n\t\t}\n\t\tbutton: RoundButton(defaultActiveButton) {\n\t\t\ttextFg: storiesComposeWhiteText;\n\t\t\ttextFgOver: storiesComposeWhiteText;\n\t\t\ttextBg: groupCallMembersBgRipple;\n\t\t\ttextBgOver: groupCallMembersBgRipple;\n\t\t\twidth: -12px;\n\t\t\theight: 18px;\n\t\t\ttextTop: 0px;\n\t\t\tstyle: TextStyle(defaultTextStyle) {\n\t\t\t\tfont: font(12px);\n\t\t\t}\n\t\t\tripple: storiesComposeRipple;\n\t\t}\n\t\tbuttonSkip: 6px;\n\t\tbuttonTop: 14px;\n\t\tposition: point(37px, 14px);\n\t\ticon: icon{{ \"emoji/premium_lock\", storiesComposeGrayIcon, point(13px, 14px) }};\n\t}\n\tchooseSendAs: storiesChooseSendAs;\n}\nstoriesViewsMenu: PopupMenu(storiesPopupMenuWithIcons) {\n\tscrollPadding: margins(0px, 6px, 0px, 4px);\n\tmaxHeight: 320px;\n\tmenu: Menu(storiesMenuWithIcons) {\n\t\titemPadding: margins(54px, 8px, 17px, 8px);\n\t\twidthMin: 240px;\n\t\twidthMax: 240px;\n\t}\n\tradius: 7px;\n}\nstoriesRecentViewsSkip: 8px;\nstoriesWhoViewed: WhoRead(defaultWhoRead) {\n\tuserpics: GroupCallUserpics {\n\t\tsize: 24px;\n\t\tshift: 9px;\n\t\tstroke: 4px;\n\t\talign: align(left);\n\t}\n}\nstoriesReactionsPan: EmojiPan(storiesEmojiPan) {\n\tmargin: margins(reactStripSkip, 0px, reactStripSkip, 0px);\n\tpadding: margins(reactStripSkip, 0px, reactStripSkip, reactStripSkip);\n\tdesiredSize: reactStripSize;\n\tverticalSizeSub: 0px;\n\toverBg: transparent;\n\tsearch: TabbedSearch(storiesEmojiTabbedSearch) {\n\t\tdefaultFieldWidth: 88px;\n\t}\n\tsearchMargin: margins(1px, 10px, 2px, 6px);\n}\n\nstoriesReactionsWidth: 210px;\nstoriesReactionsBottomSkip: 29px;\nstoriesReactionsAddedTop: 200px;\nstoriesLikeReactionsPosition: point(85px, 30px);\n\nstoriesUnsupportedLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: mediaviewControlFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px);\n\t\tlineHeight: 21px;\n\t}\n\talign: align(top);\n}\nstoriesUnsupportedUpdate: RoundButton(defaultActiveButton) {\n\twidth: -102px;\n\theight: 42px;\n\ttextTop: 12px;\n}\nstoriesUnsupportedTop: 24px;\nstoriesUnsupportedSkip: 18px;\nstoriesShortInfoBox: ShortInfoBox(shortInfoBox) {\n\tlabel: FlatLabel(infoLabel) {\n\t\ttextFg: storiesComposeGrayText;\n\t\tpalette: TextPalette(mediaviewTextPalette) {\n\t\t\tlinkFg: mediaviewTextLinkFg;\n\t\t\tmonoFg: storiesComposeGrayText;\n\t\t\tspoilerFg: storiesComposeGrayText;\n\t\t}\n\t}\n\tlabeled: FlatLabel(infoLabeled) {\n\t\ttextFg: mediaviewCaptionFg;\n\t\tpalette: mediaviewTextPalette;\n\t}\n\tlabeledOneLine: FlatLabel(infoLabeledOneLine) {\n\t\ttextFg: mediaviewCaptionFg;\n\t\tpalette: mediaviewTextPalette;\n\t}\n}\nstoriesReportBox: ReportBox(defaultReportBox) {\n\tbutton: SettingsButton(reportReasonButton) {\n\t\ttextFg: storiesComposeWhiteText;\n\t\ttextFgOver: storiesComposeWhiteText;\n\t\ttextBg: storiesComposeBg;\n\t\ttextBgOver: storiesComposeBgOver;\n\t\tripple: storiesComposeRipple;\n\t}\n\tnoIconButton: SettingsButton(reportReasonButton) {\n\t\ttextFg: storiesComposeWhiteText;\n\t\ttextFgOver: storiesComposeWhiteText;\n\t\ttextBg: storiesComposeBg;\n\t\ttextBgOver: storiesComposeBgOver;\n\t\tpadding: margins(22px, 7px, 8px, 7px);\n\t\tripple: storiesComposeRipple;\n\t}\n\tlabel: storiesBoxLabel;\n\tfield: InputField(storiesBoxInputField) {\n\t\ttextMargins: margins(1px, 26px, 1px, 4px);\n\t\theightMax: 116px;\n\t}\n\tdivider: groupCallDividerLabel;\n\tspam: icon {{ \"menu/delete\", storiesComposeWhiteText }};\n\tfake: icon {{ \"menu/fake\", storiesComposeWhiteText }};\n\tviolence: icon {{ \"menu/violence\", storiesComposeWhiteText }};\n\tchildren: icon {{ \"menu/block\", storiesComposeWhiteText }};\n\tpornography: icon {{ \"menu/porn\", storiesComposeWhiteText }};\n\tcopyright: icon {{ \"menu/copyright\", storiesComposeWhiteText }};\n\tdrugs: icon {{ \"menu/drugs\", storiesComposeWhiteText }};\n\tpersonal: icon {{ \"menu/personal\", storiesComposeWhiteText }};\n\tother: icon {{ \"menu/report\", storiesComposeWhiteText }};\n}\nstoriesActionToast: Toast(defaultToast) {\n\tmaxWidth: 320px;\n}\n\nstoriesBadgeCloseFriends: icon{{ \"mediaview/mini_close_friends\", historyPeerUserpicFg }};\nstoriesBadgeContacts: icon{{ \"mediaview/mini_contacts\", historyPeerUserpicFg }};\nstoriesBadgeSelectedContacts: icon{{ \"mediaview/mini_selected_contacts\", historyPeerUserpicFg }};\nstoriesBadgePadding: margins(1px, 1px, 1px, 1px);\nstoriesBadgeOutline: 2px;\nstoriesBadgeShift: point(5px, 4px);\n\nstoriesPlayIcon: icon{{ \"player/player_play\", mediaviewPlaybackIconFgOver }};\nstoriesPauseIcon: icon{{ \"player/player_pause\", mediaviewPlaybackIconFgOver }};\nstoriesPlayButton: IconButton(defaultIconButton) {\n\twidth: 40px;\n\theight: 40px;\n\ticon: storiesPlayIcon;\n\ticonOver: storiesPlayIcon;\n\ticonPosition: point(8px, 8px);\n\trippleAreaPosition: point(4px, 4px);\n\trippleAreaSize: 32px;\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: mediaviewPlaybackIconRipple;\n\t}\n}\nstoriesPlayButtonPosition: point(54px, 0px);\nstoriesVolumeButton: IconButton(storiesPlayButton) {\n\ticon: mediaviewVolumeIcon0Over;\n\ticonOver: mediaviewVolumeIcon0Over;\n\tripple: emptyRippleAnimation;\n}\nstoriesVolumeButtonPosition: point(10px, 0px);\nstoriesVolumeSize: 75px;\nstoriesVolumeBottom: 20px;\nstoriesVolumeSlider: MediaSlider {\n\twidth: 3px;\n\tactiveFg: mediaviewPlaybackActiveOver;\n\tinactiveFg: mediaviewPlaybackActive;\n\tactiveFgOver: mediaviewPlaybackActiveOver;\n\tinactiveFgOver: mediaviewPlaybackActive;\n\tactiveFgDisabled: mediaviewPlaybackActiveOver;\n\tinactiveFgDisabled: mediaviewPlaybackActive;\n\treceivedTillFg: mediaviewPlaybackActive;\n\tseekSize: size(12px, 12px);\n\tduration: mediaviewOverDuration;\n}\nstoriesInfoTooltipLabel: defaultImportantTooltipLabel;\nstoriesInfoTooltip: defaultImportantTooltip;\nstoriesInfoTooltipMaxWidth: 360px;\nstoriesCaptionPullThreshold: 50px;\nstoriesShowMorePadding: margins(6px, 4px, 6px, 4px);\nstoriesShowMoreFont: semiboldFont;\n\nstoriesStealthLogoAdd: 12px;\nstoriesStealthLogoMargin: margins(0px, 28px, 0px, 7px);\nstoriesStealthBox: Box(defaultBox) {\n\tbuttonPadding: margins(10px, 10px, 10px, 10px);\n\tbuttonHeight: 42px;\n\tbutton: RoundButton(defaultBoxButton) {\n\t\theight: 42px;\n\t\ttextTop: 12px;\n\t\tstyle: semiboldTextStyle;\n\n\t\ttextFg: storiesComposeWhiteText;\n\t\ttextFgOver: storiesComposeWhiteText;\n\t\tnumbersTextFg: storiesComposeWhiteText;\n\t\tnumbersTextFgOver: storiesComposeWhiteText;\n\t\ttextBg: storiesComposeBlue;\n\t\ttextBgOver: storiesComposeBlue;\n\n\t\tripple: universalRippleAnimation;\n\t}\n\tmargin: margins(0px, 56px, 0px, 10px);\n\tbg: groupCallMembersBg;\n\ttitle: FlatLabel(boxTitle) {\n\t\ttextFg: groupCallMembersFg;\n\t\talign: align(top);\n\t}\n\ttitleAdditionalFg: groupCallMemberNotJoinedStatus;\n}\nstoriesStealthTitleMargin: margins(0px, 10px, 0px, 0px);\nstoriesStealthBoxClose: IconButton(defaultIconButton) {\n\twidth: boxTitleHeight;\n\theight: boxTitleHeight;\n\n\ticon: icon {{ \"box_button_close\", storiesComposeGrayIcon }};\n\ticonOver: icon {{ \"box_button_close\", storiesComposeGrayIcon }};\n\n\trippleAreaPosition: point(4px, 4px);\n\trippleAreaSize: 40px;\n\tripple: storiesComposeRippleLight;\n}\nstoriesStealthAboutMargin: margins(0px, 5px, 0px, 15px);\nstoriesStealthBoxBottom: 11px;\nstoriesStealthToast: Toast(defaultMultilineToast) {\n\tmaxWidth: 340px;\n}\n\nstoriesStealthStyle: StealthBoxStyle {\n\tbox: storiesStealthBox;\n\tbuttonLabel: FlatLabel(defaultFlatLabel) {\n\t\tstyle: semiboldTextStyle;\n\t\ttextFg: storiesComposeWhiteText;\n\t\talign: align(top);\n\t\tminWidth: 20px;\n\t\tmaxHeight: 20px;\n\t}\n\tlockIcon: icon {{ \"dialogs/dialogs_lock_on\", storiesComposeWhiteText }};\n\tboxClose: storiesStealthBoxClose;\n\tabout: FlatLabel(defaultFlatLabel) {\n\t\ttextFg: storiesComposeGrayText;\n\t\talign: align(top);\n\t\tminWidth: 20px;\n\t}\n\tfeatureTitle: storiesHeaderName;\n\tfeatureAbout: FlatLabel(defaultFlatLabel) {\n\t\ttextFg: storiesComposeGrayText;\n\t\tminWidth: 20px;\n\t}\n\tfeaturePastIcon: icon{{ \"stories/stealth_5m\", storiesComposeBlue }};\n\tfeatureNextIcon: icon{{ \"stories/stealth_25m\", storiesComposeBlue }};\n\tlogoIcon: icon{{ \"stories/stealth_logo\", storiesComposeWhiteText }};\n\tlogoBg: storiesComposeBlue;\n}\n\nstoriesViewsPosition: point(4px, 29px);\nstoriesViewsIcon: icon{{ \"mediaview/views\", storiesComposeGrayText }};\nstoriesViewsIconTop: 11px;\nstoriesViewsText: FlatLabel(defaultFlatLabel) {\n\ttextFg: storiesComposeGrayText;\n}\nstoriesViewsTextPosition: point(26px, 14px);\nstoriesChannelReactionsTextTop: 16px;\n\nstoriesLikesPosition: point(0px, 29px);\nstoriesLikesIcon: icon {{ \"chat/input_like\", storiesComposeWhiteText }};\nstoriesLikesText: FlatLabel(defaultFlatLabel) {\n\ttextFg: storiesComposeWhiteText;\n\tstyle: semiboldTextStyle;\n}\nstoriesLikesTextPosition: point(41px, 14px);\nstoriesLikesTextRightSkip: 8px;\nstoriesLikesEmptyRightSkip: 2px;\n\nstoriesLikeCountStyle: TextStyle(defaultTextStyle) {\n\tfont: font(32px semibold);\n}\nstoriesChangelogFooterWidthMin: 240px;\n\nstoriesRepostSimpleStyle: QuoteStyle(defaultQuoteStyle) {\n\tpadding: margins(8px, 2px, 8px, 2px);\n\tverticalSkip: 4px;\n\toutline: 0px;\n\tradius: 10px;\n}\nstoriesRepostIcon: IconEmoji{\n\ticon: icon {{ \"mediaview/mini_repost\", windowFg }};\n\tpadding: margins(0px, 4px, 2px, 0px);\n}\nstoriesRepostUserpicPadding: margins(0px, 1px, 4px, 0px);\n\nmediaviewSponsoredButton: RoundButton(defaultActiveButton) {\n\ttextFg: mediaviewPlaybackIconFg;\n\ttextFgOver: mediaviewPlaybackIconFg;\n\ttextBg: mediaviewSaveMsgBg;\n\ttextBgOver: mediaviewSaveMsgBg;\n\n\tripple: universalRippleAnimation;\n}\n\nmediaSponsoredSkip: 16px;\nmediaSponsoredShift: 16px;\nmediaSponsoredPadding: margins(12px, 8px, 8px, 8px);\nmediaSponsoredThumb: 48px;\nmediaSponsoredCloseTwice: 3px;\nmediaSponsoredCloseSmall: 3px;\nmediaSponsoredCloseSize: 11px;\nmediaSponsoredCloseCorner: 6px;\nmediaSponsoredCloseFull: 64px;\nmediaSponsoredCloseStroke: 2px;\nmediaSponsoredCloseRipple: 36px;\nmediaSponsoredCloseDiameter: 24px;\nmediaSponsoredCloseFont: font(12px bold);\n\nmediaSponsoredAbout: RoundButton(defaultActiveButton) {\n\ttextFg: windowActiveTextFg;\n\ttextFgOver: windowActiveTextFg;\n\ttextBg: lightButtonBgOver;\n\ttextBgOver: lightButtonBgOver;\n\twidth: -12px;\n\theight: 18px;\n\tradius: 9px;\n\ttextTop: 0px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px);\n\t}\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: lightButtonBgRipple;\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_group_thumbs.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/view/media_view_group_thumbs.h\"\n\n#include \"base/variant.h\"\n#include \"data/data_shared_media.h\"\n#include \"data/data_user_photos.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_session.h\"\n#include \"data/data_web_page.h\"\n#include \"data/data_file_origin.h\"\n#include \"history/history.h\"\n#include \"history/view/media/history_view_media.h\"\n#include \"ui/image/image.h\"\n#include \"ui/ui_utility.h\"\n#include \"main/main_session.h\"\n#include \"core/crash_reports.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Media {\nnamespace View {\nnamespace {\n\nconstexpr auto kThumbDuration = crl::time(150);\n\nint Round(float64 value) {\n\treturn int(base::SafeRound(value));\n}\n\nusing Context = GroupThumbs::Context;\nusing Key = GroupThumbs::Key;\n\n#if 0\n[[nodiscard]] QString DebugSerializeMsgId(FullMsgId itemId) {\n\treturn QString(\"msg%1_%2\").arg(itemId.channel.bare).arg(itemId.msg);\n}\n\n[[nodiscard]] QString DebugSerializePeer(PeerId peerId) {\n\treturn peerIsUser(peerId)\n\t\t? QString(\"user%1\").arg(peerToUser(peerId).bare)\n\t\t: peerIsChat(peerId)\n\t\t? QString(\"chat%1\").arg(peerToChat(peerId).bare)\n\t\t: QString(\"channel%1\").arg(peerToChannel(peerId).bare);\n}\n\n[[nodiscard]] QString DebugSerializeKey(const Key &key) {\n\treturn v::match(key, [&](PhotoId photoId) {\n\t\treturn QString(\"photo%1\").arg(photoId);\n\t}, [](FullMsgId itemId) {\n\t\treturn DebugSerializeMsgId(itemId);\n\t}, [&](GroupThumbs::CollageKey key) {\n\t\treturn QString(\"collage%1\").arg(key.index);\n\t});\n}\n\n[[nodiscard]] QString DebugSerializeContext(const Context &context) {\n\treturn v::match(context, [](PeerId peerId) {\n\t\treturn DebugSerializePeer(peerId);\n\t}, [](MessageGroupId groupId) {\n\t\treturn QString(\"group_%1_%2\"\n\t\t).arg(DebugSerializePeer(groupId.peer)\n\t\t).arg(groupId.value);\n\t}, [](FullMsgId item) {\n\t\treturn DebugSerializeMsgId(item);\n\t}, [](v::null_t) -> QString {\n\t\treturn \"null\";\n\t});\n}\n#endif\n\nData::FileOrigin ComputeFileOrigin(const Key &key, const Context &context) {\n\treturn v::match(key, [&](PhotoId photoId) {\n\t\treturn v::match(context, [&](PeerId peerId) {\n\t\t\treturn peerIsUser(peerId)\n\t\t\t\t? Data::FileOriginUserPhoto(peerToUser(peerId), photoId)\n\t\t\t\t: Data::FileOrigin(Data::FileOriginPeerPhoto(peerId));\n\t\t}, [](auto&&) {\n\t\t\treturn Data::FileOrigin();\n\t\t});\n\t}, [](FullMsgId itemId) {\n\t\treturn Data::FileOrigin(itemId);\n\t}, [&](GroupThumbs::CollageKey) {\n\t\treturn v::match(context, [](const GroupThumbs::CollageSlice &slice) {\n\t\t\treturn Data::FileOrigin(slice.context);\n\t\t}, [](auto&&) {\n\t\t\treturn Data::FileOrigin();\n\t\t});\n\t});\n}\n\nContext ComputeContext(\n\t\tnot_null<Main::Session*> session,\n\t\tconst SharedMediaWithLastSlice &slice,\n\t\tint index) {\n\tExpects(index >= 0 && index < slice.size());\n\n\tconst auto value = slice[index];\n\tif (const auto photo = std::get_if<not_null<PhotoData*>>(&value)) {\n\t\tif (const auto peer = (*photo)->peer) {\n\t\t\treturn peer->id;\n\t\t}\n\t\treturn v::null;\n\t} else if (const auto msgId = std::get_if<FullMsgId>(&value)) {\n\t\tif (const auto item = session->data().message(*msgId)) {\n\t\t\tif (item->isService()) {\n\t\t\t\treturn item->history()->peer->id;\n\t\t\t} else if (const auto groupId = item->groupId()) {\n\t\t\t\treturn groupId;\n\t\t\t}\n\t\t}\n\t\treturn v::null;\n\t}\n\tUnexpected(\"Variant in ComputeContext(SharedMediaWithLastSlice::Value)\");\n}\n\nContext ComputeContext(\n\t\tnot_null<Main::Session*> session,\n\t\tconst UserPhotosSlice &slice,\n\t\tint index) {\n\treturn peerFromUser(slice.key().userId);\n}\n\nContext ComputeContext(\n\t\tnot_null<Main::Session*> session,\n\t\tconst GroupThumbs::CollageSlice &slice,\n\t\tint index) {\n\treturn slice.context;\n}\n\nKey ComputeKey(const SharedMediaWithLastSlice &slice, int index) {\n\tExpects(index >= 0 && index < slice.size());\n\n\tconst auto value = slice[index];\n\tif (const auto photo = std::get_if<not_null<PhotoData*>>(&value)) {\n\t\treturn (*photo)->id;\n\t} else if (const auto msgId = std::get_if<FullMsgId>(&value)) {\n\t\treturn *msgId;\n\t}\n\tUnexpected(\"Variant in ComputeKey(SharedMediaWithLastSlice::Value)\");\n}\n\nKey ComputeKey(const UserPhotosSlice &slice, int index) {\n\treturn slice[index];\n}\n\nKey ComputeKey(const GroupThumbs::CollageSlice &slice, int index) {\n\treturn GroupThumbs::CollageKey{ index };\n}\n\nstd::optional<WebPageCollage::Item> PollAnswerMediaItemByIndex(\n\t\tnot_null<PollData*> poll,\n\t\tint index) {\n\tauto current = 0;\n\tfor (const auto &answer : poll->answers) {\n\t\tconst auto &media = answer.media;\n\t\tauto item = std::optional<WebPageCollage::Item>();\n\t\tif (media.photo) {\n\t\t\titem = WebPageCollage::Item(media.photo);\n\t\t} else if (media.document && !media.document->sticker()) {\n\t\t\titem = WebPageCollage::Item(media.document);\n\t\t}\n\t\tif (item) {\n\t\t\tif (current++ == index) {\n\t\t\t\treturn item;\n\t\t\t}\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nint ComputeThumbsLimit(int availableWidth) {\n\tconst auto singleWidth = st::mediaviewGroupWidth\n\t\t+ 2 * st::mediaviewGroupSkip;\n\tconst auto currentWidth = st::mediaviewGroupWidthMax\n\t\t+ 2 * st::mediaviewGroupSkipCurrent;\n\tconst auto skipForAnimation = 2 * singleWidth;\n\tconst auto leftWidth = availableWidth\n\t\t- currentWidth\n\t\t- skipForAnimation;\n\treturn std::max(leftWidth / (2 * singleWidth), 1);\n}\n\n} // namespace\n\nclass GroupThumbs::Thumb {\npublic:\n\tenum class State {\n\t\tUnknown,\n\t\tCurrent,\n\t\tAlive,\n\t\tDying,\n\t};\n\n\tThumb(Key key, Fn<void()> handler);\n\tThumb(\n\t\tKey key,\n\t\tnot_null<PhotoData*> photo,\n\t\tData::FileOrigin origin,\n\t\tFn<void()> handler);\n\tThumb(\n\t\tKey key,\n\t\tnot_null<DocumentData*> document,\n\t\tData::FileOrigin origin,\n\t\tFn<void()> handler);\n\n\t[[nodiscard]] int leftToUpdate() const;\n\t[[nodiscard]] int rightToUpdate() const;\n\n\tvoid animateToLeft(not_null<Thumb*> next);\n\tvoid animateToRight(not_null<Thumb*> prev);\n\n\tvoid setState(State state);\n\t[[nodiscard]] State state() const;\n\t[[nodiscard]] bool inited() const;\n\t[[nodiscard]] bool removed() const;\n\n\tvoid paint(QPainter &p, int x, int y, int outerWidth, float64 progress);\n\t[[nodiscard]] ClickHandlerPtr getState(QPoint point) const;\n\nprivate:\n\tQSize wantedPixSize() const;\n\tvoid validateImage();\n\tint currentLeft() const;\n\tint currentWidth() const;\n\tint finalLeft() const;\n\tint finalWidth() const;\n\tvoid animateTo(int left, int width);\n\n\tClickHandlerPtr _link;\n\tconst Key _key;\n\tstd::shared_ptr<Data::DocumentMedia> _documentMedia;\n\tstd::shared_ptr<Data::PhotoMedia> _photoMedia;\n\tImage *_image = nullptr;\n\tData::FileOrigin _origin;\n\tState _state = State::Alive;\n\tQPixmap _full;\n\tint _fullWidth = 0;\n\tbool _hiding = false;\n\n\tanim::value _left = { 0. };\n\tanim::value _width = { 0. };\n\tanim::value _opacity = { 0., 1. };\n\n};\n\nGroupThumbs::Thumb::Thumb(Key key, Fn<void()> handler)\n: _key(key) {\n\t_link = std::make_shared<LambdaClickHandler>(std::move(handler));\n\tvalidateImage();\n}\n\nGroupThumbs::Thumb::Thumb(\n\tKey key,\n\tnot_null<PhotoData*> photo,\n\tData::FileOrigin origin,\n\tFn<void()> handler)\n: _key(key)\n, _photoMedia(photo->createMediaView())\n, _origin(origin) {\n\t_link = std::make_shared<LambdaClickHandler>(std::move(handler));\n\t_photoMedia->wanted(Data::PhotoSize::Thumbnail, origin);\n\tvalidateImage();\n}\n\nGroupThumbs::Thumb::Thumb(\n\tKey key,\n\tnot_null<DocumentData*> document,\n\tData::FileOrigin origin,\n\tFn<void()> handler)\n: _key(key)\n, _documentMedia(document->createMediaView())\n, _origin(origin) {\n\t_link = std::make_shared<LambdaClickHandler>(std::move(handler));\n\t_documentMedia->thumbnailWanted(origin);\n\tvalidateImage();\n}\n\nQSize GroupThumbs::Thumb::wantedPixSize() const {\n\tconst auto originalWidth = _image ? std::max(_image->width(), 1) : 1;\n\tconst auto originalHeight = _image ? std::max(_image->height(), 1) : 1;\n\tconst auto pixHeight = st::mediaviewGroupHeight;\n\tconst auto pixWidth = originalWidth * pixHeight / originalHeight;\n\treturn { pixWidth, pixHeight };\n}\n\nvoid GroupThumbs::Thumb::validateImage() {\n\tif (!_image) {\n\t\tif (_photoMedia) {\n\t\t\t_image = _photoMedia->image(Data::PhotoSize::Thumbnail);\n\t\t} else if (_documentMedia) {\n\t\t\t_image = _documentMedia->thumbnail();\n\t\t}\n\t}\n\tif (!_full.isNull() || !_image) {\n\t\treturn;\n\t}\n\n\tconst auto pixSize = wantedPixSize();\n\tif (pixSize.width() > st::mediaviewGroupWidthMax) {\n\t\tconst auto originalWidth = _image->width();\n\t\tconst auto originalHeight = _image->height();\n\t\tconst auto takeWidth = originalWidth * st::mediaviewGroupWidthMax\n\t\t\t/ pixSize.width();\n\t\tauto original = _image->original();\n\t\toriginal.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t_full = Ui::PixmapFromImage(original.copy(\n\t\t\t(originalWidth - takeWidth) / 2,\n\t\t\t0,\n\t\t\ttakeWidth,\n\t\t\toriginalHeight\n\t\t).scaled(\n\t\t\tst::mediaviewGroupWidthMax * style::DevicePixelRatio(),\n\t\t\tpixSize.height() * style::DevicePixelRatio(),\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation));\n\t} else {\n\t\t_full = _image->pixNoCache(pixSize * style::DevicePixelRatio());\n\t}\n\t_fullWidth = std::min(\n\t\twantedPixSize().width(),\n\t\tst::mediaviewGroupWidthMax);\n}\n\nint GroupThumbs::Thumb::leftToUpdate() const {\n\treturn Round(std::min(_left.from(), _left.to()));\n}\n\nint GroupThumbs::Thumb::rightToUpdate() const {\n\treturn Round(std::max(\n\t\t_left.from() + _width.from(),\n\t\t_left.to() + _width.to()));\n}\n\nint GroupThumbs::Thumb::currentLeft() const {\n\treturn Round(_left.current());\n}\n\nint GroupThumbs::Thumb::currentWidth() const {\n\treturn Round(_width.current());\n}\n\nint GroupThumbs::Thumb::finalLeft() const {\n\treturn Round(_left.to());\n}\n\nint GroupThumbs::Thumb::finalWidth() const {\n\treturn Round(_width.to());\n}\n\nvoid GroupThumbs::Thumb::setState(State state) {\n\tconst auto isNewThumb = (_state == State::Alive);\n\t_state = state;\n\tif (_state == State::Current) {\n\t\tif (isNewThumb) {\n\t\t\t_opacity = anim::value(1.);\n\t\t\t_left = anim::value(-_fullWidth / 2);\n\t\t\t_width = anim::value(_fullWidth);\n\t\t} else {\n\t\t\t_opacity.start(1.);\n\t\t}\n\t\t_hiding = false;\n\t\tanimateTo(-_fullWidth / 2, _fullWidth);\n\t} else if (_state == State::Alive) {\n\t\t_opacity.start(0.7);\n\t\t_hiding = false;\n\t} else if (_state == State::Dying) {\n\t\t_opacity.start(0.);\n\t\t_hiding = true;\n\t\t_left.restart();\n\t\t_width.restart();\n\t}\n}\n\nvoid GroupThumbs::Thumb::animateTo(int left, int width) {\n\t_left.start(left);\n\t_width.start(width);\n}\n\nvoid GroupThumbs::Thumb::animateToLeft(not_null<Thumb*> next) {\n\tconst auto width = st::mediaviewGroupWidth;\n\tif (_state == State::Alive) {\n\t\t// New item animation, start exactly from the next, move only.\n\t\t_left = anim::value(next->currentLeft() - width);\n\t\t_width = anim::value(width);\n\t} else if (_state == State::Unknown) {\n\t\t// Existing item animation.\n\t\tsetState(State::Alive);\n\t}\n\tconst auto skip1 = st::mediaviewGroupSkip;\n\tconst auto skip2 = (next->state() == State::Current)\n\t\t? st::mediaviewGroupSkipCurrent\n\t\t: st::mediaviewGroupSkip;\n\tanimateTo(next->finalLeft() - width - skip1 - skip2, width);\n}\n\nvoid GroupThumbs::Thumb::animateToRight(not_null<Thumb*> prev) {\n\tconst auto width = st::mediaviewGroupWidth;\n\tif (_state == State::Alive) {\n\t\t// New item animation, start exactly from the next, move only.\n\t\t_left = anim::value(prev->currentLeft() + prev->currentWidth());\n\t\t_width = anim::value(width);\n\t} else if (_state == State::Unknown) {\n\t\t// Existing item animation.\n\t\tsetState(State::Alive);\n\t}\n\tconst auto skip1 = st::mediaviewGroupSkip;\n\tconst auto skip2 = (prev->state() == State::Current)\n\t\t? st::mediaviewGroupSkipCurrent\n\t\t: st::mediaviewGroupSkip;\n\tanimateTo(prev->finalLeft() + prev->finalWidth() + skip1 + skip2, width);\n}\n\nauto GroupThumbs::Thumb::state() const -> State {\n\treturn _state;\n}\n\nbool GroupThumbs::Thumb::inited() const {\n\treturn _fullWidth != 0;\n}\n\nbool GroupThumbs::Thumb::removed() const {\n\treturn (_state == State::Dying) && _hiding && !_opacity.current();\n}\n\nvoid GroupThumbs::Thumb::paint(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tfloat64 progress) {\n\tvalidateImage();\n\n\t_opacity.update(progress, anim::linear);\n\t_left.update(progress, anim::linear);\n\t_width.update(progress, anim::linear);\n\n\tconst auto left = x + currentLeft();\n\tconst auto width = currentWidth();\n\tconst auto opacity = p.opacity();\n\tp.setOpacity(_opacity.current() * opacity);\n\tif (width == _fullWidth) {\n\t\tp.drawPixmap(left, y, _full);\n\t} else {\n\t\tconst auto takeWidth = width * style::DevicePixelRatio();\n\t\tconst auto from = QRect(\n\t\t\t(_full.width() - takeWidth) / 2,\n\t\t\t0,\n\t\t\ttakeWidth,\n\t\t\t_full.height());\n\t\tconst auto to = QRect(left, y, width, st::mediaviewGroupHeight);\n\t\tp.drawPixmap(to, _full, from);\n\t}\n\tp.setOpacity(opacity);\n}\n\nClickHandlerPtr GroupThumbs::Thumb::getState(QPoint point) const {\n\tif (_state != State::Alive) {\n\t\treturn nullptr;\n\t}\n\tconst auto left = finalLeft();\n\tconst auto width = finalWidth();\n\treturn QRect(left, 0, width, st::mediaviewGroupHeight).contains(point)\n\t\t? _link\n\t\t: nullptr;\n}\n\nint GroupThumbs::CollageSlice::size() const {\n\treturn data->items.size();\n}\n\nGroupThumbs::GroupThumbs(not_null<Main::Session*> session, Context context)\n: _session(session)\n, _context(context) {\n}\n\nvoid GroupThumbs::updateContext(Context context) {\n\tif (_context != context) {\n\t\tclear();\n\t\t_context = context;\n\t}\n}\n\ntemplate <typename Slice>\nvoid GroupThumbs::RefreshFromSlice(\n\t\tnot_null<Main::Session*> session,\n\t\tstd::unique_ptr<GroupThumbs> &instance,\n\t\tconst Slice &slice,\n\t\tint index,\n\t\tint availableWidth) {\n\tconst auto context = ComputeContext(session, slice, index);\n\tif (instance) {\n\t\tinstance->updateContext(context);\n\t}\n\tif (v::is_null(context)) {\n\t\tif (instance) {\n\t\t\tinstance->resizeToWidth(availableWidth);\n\t\t}\n\t\treturn;\n\t}\n\tconst auto limit = ComputeThumbsLimit(availableWidth);\n\tconst auto from = [&] {\n\t\tconst auto edge = std::max(index - limit, 0);\n\t\tfor (auto result = index; result != edge; --result) {\n\t\t\tif (ComputeContext(session, slice, result - 1) != context) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t\treturn edge;\n\t}();\n\tconst auto till = [&] {\n\t\tconst auto edge = std::min(index + limit + 1, slice.size());\n\t\tfor (auto result = index + 1; result != edge; ++result) {\n\t\t\tif (ComputeContext(session, slice, result) != context) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t\treturn edge;\n\t}();\n\tif (from + 1 < till) {\n\t\tif (!instance) {\n\t\t\tinstance = std::make_unique<GroupThumbs>(session, context);\n\t\t}\n\t\tinstance->fillItems(slice, from, index, till);\n\t\tinstance->resizeToWidth(availableWidth);\n\t} else if (instance) {\n\t\tinstance->clear();\n\t\tinstance->resizeToWidth(availableWidth);\n\t}\n}\n\n#if 0\ntemplate <typename Slice>\nvoid ValidateSlice(\n\t\tconst Slice &slice,\n\t\tconst Context &context,\n\t\tint from,\n\t\tint index,\n\t\tint till) {\n\tauto keys = base::flat_set<Key>();\n\tfor (auto i = from; i != till; ++i) {\n\t\tconst auto key = ComputeKey(slice, i);\n\t\tif (keys.contains(key)) {\n\t\t\t// All items should be unique!\n\t\t\tauto strings = QStringList();\n\t\t\tstrings.reserve(till - from);\n\t\t\tfor (auto i = from; i != till; ++i) {\n\t\t\t\tstrings.push_back(DebugSerializeKey(ComputeKey(slice, i)));\n\t\t\t}\n\t\t\tCrashReports::SetAnnotation(\n\t\t\t\t\"keys\",\n\t\t\t\tQString(\"%1:%2-(%3)-%4:\"\n\t\t\t\t).arg(DebugSerializeContext(context)\n\t\t\t\t).arg(from\n\t\t\t\t).arg(index\n\t\t\t\t).arg(till) + strings.join(\",\"));\n\t\t\tif (Logs::DebugEnabled()) {\n\t\t\t\tUnexpected(\"Bad slice in GroupThumbs.\");\n\t\t\t}\n\t\t\tbreak;\n\t\t} else {\n\t\t\tkeys.emplace(key);\n\t\t}\n\t}\n}\n#endif\n\ntemplate <typename Slice>\nvoid GroupThumbs::fillItems(\n\t\tconst Slice &slice,\n\t\tint from,\n\t\tint index,\n\t\tint till) {\n\tExpects(from <= index);\n\tExpects(index < till);\n\tExpects(from + 1 < till);\n\n\tconst auto current = (index - from);\n\tconst auto old = base::take(_items);\n\n\t//ValidateSlice(slice, _context, from, index, till);\n\n\tmarkCacheStale();\n\t_items.reserve(till - from);\n\tfor (auto i = from; i != till; ++i) {\n\t\t_items.push_back(validateCacheEntry(ComputeKey(slice, i)));\n\t}\n\tanimateAliveItems(current);\n\tfillDyingItems(old);\n\tstartDelayedAnimation();\n}\n\nvoid GroupThumbs::animateAliveItems(int current) {\n\tExpects(current >= 0 && current < _items.size());\n\n\t_items[current]->setState(Thumb::State::Current);\n\tfor (auto i = current; i != 0;) {\n\t\tconst auto prev = _items[i];\n\t\tconst auto item = _items[--i];\n\t\titem->animateToLeft(prev);\n\t}\n\tfor (auto i = current + 1; i != _items.size(); ++i) {\n\t\tconst auto prev = _items[i - 1];\n\t\tconst auto item = _items[i];\n\t\titem->animateToRight(prev);\n\t}\n}\n\nvoid GroupThumbs::fillDyingItems(const std::vector<not_null<Thumb*>> &old) {\n\t//Expects(_cache.size() >= _items.size());\n\n\tif (_cache.size() >= _items.size()) {\n\t\t_dying.reserve(_cache.size() - _items.size());\n\t}\n\tanimatePreviouslyAlive(old);\n\tmarkRestAsDying();\n}\n\nvoid GroupThumbs::markRestAsDying() {\n\t//Expects(_cache.size() >= _items.size());\n\n\tif (_cache.size() >= _items.size()) {\n\t\t_dying.reserve(_cache.size() - _items.size());\n\t}\n\tfor (const auto &cacheItem : _cache) {\n\t\tconst auto &thumb = cacheItem.second;\n\t\tconst auto state = thumb->state();\n\t\tif (state == Thumb::State::Unknown) {\n\t\t\tmarkAsDying(thumb.get());\n\t\t}\n\t}\n}\n\nvoid GroupThumbs::markAsDying(not_null<Thumb*> thumb) {\n\tthumb->setState(Thumb::State::Dying);\n\t_dying.push_back(thumb.get());\n}\n\nvoid GroupThumbs::animatePreviouslyAlive(\n\t\tconst std::vector<not_null<Thumb*>> &old) {\n\tauto toRight = false;\n\tfor (auto i = 0; i != old.size(); ++i) {\n\t\tconst auto item = old[i];\n\t\tif (item->state() == Thumb::State::Unknown) {\n\t\t\tif (toRight) {\n\t\t\t\tmarkAsDying(item);\n\t\t\t\titem->animateToRight(old[i - 1]);\n\t\t\t}\n\t\t} else if (!toRight) {\n\t\t\tfor (auto j = i; j != 0;) {\n\t\t\t\tconst auto next = old[j];\n\t\t\t\tconst auto prev = old[--j];\n\t\t\t\tmarkAsDying(prev);\n\t\t\t\tprev->animateToLeft(next);\n\t\t\t}\n\t\t\ttoRight = true;\n\t\t}\n\t}\n}\n\nauto GroupThumbs::createThumb(Key key)\n-> std::unique_ptr<Thumb> {\n\tif (const auto photoId = std::get_if<PhotoId>(&key)) {\n\t\tconst auto photo = _session->data().photo(*photoId);\n\t\treturn createThumb(key, photo);\n\t} else if (const auto msgId = std::get_if<FullMsgId>(&key)) {\n\t\tif (const auto item = _session->data().message(*msgId)) {\n\t\t\tif (const auto media = item->media()) {\n\t\t\t\tif (const auto photo = media->photo()) {\n\t\t\t\t\treturn createThumb(key, photo);\n\t\t\t\t} else if (const auto document = media->document()) {\n\t\t\t\t\treturn createThumb(key, document);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn createThumb(key, nullptr);\n\t} else if (const auto collageKey = std::get_if<CollageKey>(&key)) {\n\t\tif (const auto itemId = std::get_if<FullMsgId>(&_context)) {\n\t\t\tif (const auto item = _session->data().message(*itemId)) {\n\t\t\t\tif (const auto media = item->media()) {\n\t\t\t\t\tif (const auto page = media->webpage()) {\n\t\t\t\t\t\treturn createThumb(\n\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\tpage->collage,\n\t\t\t\t\t\t\tcollageKey->index);\n\t\t\t\t\t} else if (const auto invoice = media->invoice()) {\n\t\t\t\t\t\treturn createThumb(\n\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\t*invoice,\n\t\t\t\t\t\t\tcollageKey->index);\n\t\t\t\t\t} else if (const auto poll = media->poll()) {\n\t\t\t\t\t\tif (const auto item = PollAnswerMediaItemByIndex(\n\t\t\t\t\t\t\t\tpoll,\n\t\t\t\t\t\t\t\tcollageKey->index)) {\n\t\t\t\t\t\t\treturn v::match(*item, [&](PhotoData *photo) {\n\t\t\t\t\t\t\t\treturn createThumb(key, photo);\n\t\t\t\t\t\t\t}, [&](DocumentData *document) {\n\t\t\t\t\t\t\t\treturn createThumb(key, document);\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn createThumb(key, nullptr);\n\t}\n\tUnexpected(\"Value of Key in GroupThumbs::createThumb()\");\n}\n\nauto GroupThumbs::createThumb(\n\tKey key,\n\tconst WebPageCollage &collage,\n\tint index)\n-> std::unique_ptr<Thumb> {\n\tif (index < 0 || index >= collage.items.size()) {\n\t\treturn createThumb(key, nullptr);\n\t}\n\treturn v::match(collage.items[index], [&](PhotoData *photo) {\n\t\treturn createThumb(key, photo);\n\t}, [&](DocumentData *document) {\n\t\treturn createThumb(key, document);\n\t});\n}\n\nauto GroupThumbs::createThumb(\n\tKey key,\n\tconst Data::Invoice &invoice,\n\tint index)\n-> std::unique_ptr<Thumb> {\n\tif (index < 0 || index >= invoice.extendedMedia.size()) {\n\t\treturn createThumb(key, nullptr);\n\t}\n\tconst auto &media = invoice.extendedMedia[index];\n\tif (const auto photo = media->photo()) {\n\t\treturn createThumb(key, photo);\n\t} else if (const auto document = media->document()) {\n\t\treturn createThumb(key, document);\n\t}\n\treturn createThumb(key, nullptr);\n}\n\nauto GroupThumbs::createThumb(Key key, std::nullptr_t)\n-> std::unique_ptr<Thumb> {\n\tconst auto weak = base::make_weak(this);\n\tconst auto origin = ComputeFileOrigin(key, _context);\n\treturn std::make_unique<Thumb>(key, [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->_activateStream.fire_copy(key);\n\t\t}\n\t});\n}\n\nauto GroupThumbs::createThumb(Key key, not_null<PhotoData*> photo)\n-> std::unique_ptr<Thumb> {\n\tconst auto weak = base::make_weak(this);\n\tconst auto origin = ComputeFileOrigin(key, _context);\n\treturn std::make_unique<Thumb>(key, photo, origin, [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->_activateStream.fire_copy(key);\n\t\t}\n\t});\n}\n\nauto GroupThumbs::createThumb(Key key, not_null<DocumentData*> document)\n-> std::unique_ptr<Thumb> {\n\tconst auto weak = base::make_weak(this);\n\tconst auto origin = ComputeFileOrigin(key, _context);\n\treturn std::make_unique<Thumb>(key, document, origin, [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->_activateStream.fire_copy(key);\n\t\t}\n\t});\n}\n\nauto GroupThumbs::validateCacheEntry(Key key) -> not_null<Thumb*> {\n\tconst auto i = _cache.find(key);\n\treturn (i != _cache.end())\n\t\t? i->second.get()\n\t\t: _cache.emplace(key, createThumb(key)).first->second.get();\n}\n\nvoid GroupThumbs::markCacheStale() {\n\t_dying.clear();\n\tfor (const auto &cacheItem : _cache) {\n\t\tconst auto &thumb = cacheItem.second;\n\t\tthumb->setState(Thumb::State::Unknown);\n\t}\n}\n\nvoid GroupThumbs::Refresh(\n\t\tnot_null<Main::Session*> session,\n\t\tstd::unique_ptr<GroupThumbs> &instance,\n\t\tconst SharedMediaWithLastSlice &slice,\n\t\tint index,\n\t\tint availableWidth) {\n\tRefreshFromSlice(session, instance, slice, index, availableWidth);\n}\n\nvoid GroupThumbs::Refresh(\n\t\tnot_null<Main::Session*> session,\n\t\tstd::unique_ptr<GroupThumbs> &instance,\n\t\tconst UserPhotosSlice &slice,\n\t\tint index,\n\t\tint availableWidth) {\n\tRefreshFromSlice(session, instance, slice, index, availableWidth);\n}\n\nvoid GroupThumbs::Refresh(\n\t\tnot_null<Main::Session*> session,\n\t\tstd::unique_ptr<GroupThumbs> &instance,\n\t\tconst CollageSlice &slice,\n\t\tint index,\n\t\tint availableWidth) {\n\tRefreshFromSlice(session, instance, slice, index, availableWidth);\n}\n\nvoid GroupThumbs::clear() {\n\tif (_items.empty()) {\n\t\treturn;\n\t}\n\tbase::take(_items);\n\tmarkCacheStale();\n\tmarkRestAsDying();\n\tstartDelayedAnimation();\n}\n\nvoid GroupThumbs::startDelayedAnimation() {\n\t_animation.stop();\n\t_waitingForAnimationStart = true;\n\tcountUpdatedRect();\n}\n\nvoid GroupThumbs::resizeToWidth(int newWidth) {\n\t_width = newWidth;\n}\n\nint GroupThumbs::height() const {\n\treturn st::mediaviewGroupPadding.top()\n\t\t+ st::mediaviewGroupHeight\n\t\t+ st::mediaviewGroupPadding.bottom();\n}\n\nbool GroupThumbs::hiding() const {\n\treturn _items.empty();\n}\n\nbool GroupThumbs::hidden() const {\n\treturn hiding() && !_waitingForAnimationStart && !_animation.animating();\n}\n\nvoid GroupThumbs::checkForAnimationStart() {\n\tif (_waitingForAnimationStart) {\n\t\t_waitingForAnimationStart = false;\n\t\t_animation.start([=] { update(); }, 0., 1., kThumbDuration);\n\t}\n}\n\nvoid GroupThumbs::update() {\n\tif (_cache.empty()) {\n\t\treturn;\n\t}\n\t_updateRequests.fire_copy(_updatedRect);\n}\n\nvoid GroupThumbs::paint(QPainter &p, int x, int y, int outerWidth) {\n\tconst auto progress = _waitingForAnimationStart\n\t\t? 0.\n\t\t: _animation.value(1.);\n\tx += (_width / 2);\n\ty += st::mediaviewGroupPadding.top();\n\tauto initedCurrentIndex = int(_items.size());\n\tfor (auto i = _cache.begin(); i != _cache.end();) {\n\t\tconst auto thumb = not_null{ i->second.get() };\n\t\tconst auto inited = thumb->inited();\n\t\tthumb->paint(p, x, y, outerWidth, progress);\n\t\tif (thumb->removed()) {\n\t\t\t_dying.erase(ranges::remove(_dying, thumb), _dying.end());\n\t\t\ti = _cache.erase(i);\n\t\t} else {\n\t\t\tif (!inited\n\t\t\t\t&& thumb->inited()\n\t\t\t\t&& thumb->state() == Thumb::State::Current) {\n\t\t\t\tinitedCurrentIndex = ranges::find(_items, thumb)\n\t\t\t\t\t- begin(_items);\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t}\n\tif (initedCurrentIndex < _items.size()) {\n\t\tanimateAliveItems(initedCurrentIndex);\n\t}\n}\n\nClickHandlerPtr GroupThumbs::getState(QPoint point) const {\n\tpoint -= QPoint((_width / 2), st::mediaviewGroupPadding.top());\n\tfor (const auto &cacheItem : _cache) {\n\t\tconst auto &thumb = cacheItem.second;\n\t\tif (auto link = thumb->getState(point)) {\n\t\t\treturn link;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid GroupThumbs::countUpdatedRect() {\n\tif (_cache.empty()) {\n\t\treturn;\n\t}\n\tauto min = _width;\n\tauto max = 0;\n\tconst auto left = [](const auto &cacheItem) {\n\t\tconst auto &[key, thumb] = cacheItem;\n\t\treturn thumb->leftToUpdate();\n\t};\n\tconst auto right = [](const auto &cacheItem) {\n\t\tconst auto &[key, thumb] = cacheItem;\n\t\treturn thumb->rightToUpdate();\n\t};\n\taccumulate_min(min, left(*ranges::max_element(\n\t\t_cache,\n\t\tstd::greater<>(),\n\t\tleft)));\n\taccumulate_max(max, right(*ranges::max_element(\n\t\t_cache,\n\t\tstd::less<>(),\n\t\tright)));\n\t_updatedRect = QRect(\n\t\tmin,\n\t\tst::mediaviewGroupPadding.top(),\n\t\tmax - min,\n\t\tst::mediaviewGroupHeight);\n}\n\nGroupThumbs::~GroupThumbs() = default;\n\n} // namespace View\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_group_thumbs.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"history/history_item_components.h\"\n#include \"ui/effects/animations.h\"\n#include \"base/weak_ptr.h\"\n\nclass SharedMediaWithLastSlice;\nclass UserPhotosSlice;\nstruct WebPageCollage;\n\nnamespace Data {\nstruct Invoice;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Media {\nnamespace View {\n\nclass GroupThumbs : public base::has_weak_ptr {\npublic:\n\tstruct CollageKey {\n\t\tint index = 0;\n\n\t\tinline bool operator<(const CollageKey &other) const {\n\t\t\treturn index < other.index;\n\t\t}\n\t};\n\tstruct CollageSlice {\n\t\tFullMsgId context;\n\t\tnot_null<const WebPageCollage*> data;\n\n\t\tint size() const;\n\t};\n\tusing Key = std::variant<PhotoId, FullMsgId, CollageKey>;\n\n\tstatic void Refresh(\n\t\tnot_null<Main::Session*> session,\n\t\tstd::unique_ptr<GroupThumbs> &instance,\n\t\tconst SharedMediaWithLastSlice &slice,\n\t\tint index,\n\t\tint availableWidth);\n\tstatic void Refresh(\n\t\tnot_null<Main::Session*> session,\n\t\tstd::unique_ptr<GroupThumbs> &instance,\n\t\tconst UserPhotosSlice &slice,\n\t\tint index,\n\t\tint availableWidth);\n\tstatic void Refresh(\n\t\tnot_null<Main::Session*> session,\n\t\tstd::unique_ptr<GroupThumbs> &instance,\n\t\tconst CollageSlice &slice,\n\t\tint index,\n\t\tint availableWidth);\n\tvoid clear();\n\n\tvoid resizeToWidth(int newWidth);\n\tint height() const;\n\tbool hiding() const;\n\tbool hidden() const;\n\tvoid checkForAnimationStart();\n\n\tvoid paint(QPainter &p, int x, int y, int outerWidth);\n\tClickHandlerPtr getState(QPoint point) const;\n\n\trpl::producer<QRect> updateRequests() const {\n\t\treturn _updateRequests.events();\n\t}\n\n\trpl::producer<Key> activateRequests() const {\n\t\treturn _activateStream.events();\n\t}\n\n\trpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\n\tusing Context = std::variant<\n\t\tv::null_t,\n\t\tPeerId,\n\t\tMessageGroupId,\n\t\tFullMsgId>;\n\n\tGroupThumbs(not_null<Main::Session*> session, Context context);\n\t~GroupThumbs();\n\nprivate:\n\tclass Thumb;\n\n\ttemplate <typename Slice>\n\tstatic void RefreshFromSlice(\n\t\tnot_null<Main::Session*> session,\n\t\tstd::unique_ptr<GroupThumbs> &instance,\n\t\tconst Slice &slice,\n\t\tint index,\n\t\tint availableWidth);\n\ttemplate <typename Slice>\n\tvoid fillItems(const Slice &slice, int from, int index, int till);\n\tvoid updateContext(Context context);\n\tvoid markCacheStale();\n\tnot_null<Thumb*> validateCacheEntry(Key key);\n\tstd::unique_ptr<Thumb> createThumb(Key key);\n\tstd::unique_ptr<Thumb> createThumb(\n\t\tKey key,\n\t\tconst WebPageCollage &collage,\n\t\tint index);\n\tstd::unique_ptr<Thumb> createThumb(\n\t\tKey key,\n\t\tconst Data::Invoice &invoice,\n\t\tint index);\n\tstd::unique_ptr<Thumb> createThumb(Key key, not_null<PhotoData*> photo);\n\tstd::unique_ptr<Thumb> createThumb(\n\t\tKey key,\n\t\tnot_null<DocumentData*> document);\n\tstd::unique_ptr<Thumb> createThumb(Key key, std::nullptr_t);\n\n\tvoid update();\n\tvoid countUpdatedRect();\n\tvoid animateAliveItems(int current);\n\tvoid fillDyingItems(const std::vector<not_null<Thumb*>> &old);\n\tvoid markAsDying(not_null<Thumb*> thumb);\n\tvoid markRestAsDying();\n\tvoid animatePreviouslyAlive(const std::vector<not_null<Thumb*>> &old);\n\tvoid startDelayedAnimation();\n\n\tconst not_null<Main::Session*> _session;\n\tContext _context;\n\tbool _waitingForAnimationStart = true;\n\tUi::Animations::Simple _animation;\n\tstd::vector<not_null<Thumb*>> _items;\n\tstd::vector<not_null<Thumb*>> _dying;\n\tbase::flat_map<Key, std::unique_ptr<Thumb>> _cache;\n\tint _width = 0;\n\tQRect _updatedRect;\n\n\trpl::event_stream<QRect> _updateRequests;\n\trpl::event_stream<Key> _activateStream;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace View\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_metal_texture.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#ifdef Q_OS_MAC\n\n#include <QtCore/QSize>\n\nclass QRhi;\nclass QRhiTexture;\n\nnamespace Media::View {\n\nclass MetalTextureCache final {\npublic:\n\tMetalTextureCache();\n\t~MetalTextureCache();\n\n\tbool createTexturesFromPixelBuffer(\n\t\tQRhi *rhi,\n\t\tvoid *cvPixelBuffer,\n\t\tQRhiTexture **yTexture,\n\t\tQRhiTexture **uvTexture,\n\t\tQSize *lumaSize,\n\t\tQSize *chromaSize);\n\n\tvoid flush();\n\nprivate:\n\tstruct Private;\n\tstd::unique_ptr<Private> _private;\n\n};\n\n} // namespace Media::View\n\n#endif // Q_OS_MAC\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_metal_texture.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/view/media_view_metal_texture.h\"\n\n#ifdef Q_OS_MAC\n\n#include <CoreVideo/CoreVideo.h>\n#include <Metal/Metal.h>\n#include <rhi/qrhi.h>\n\nnamespace Media::View {\n\nstruct MetalTextureCache::Private {\n\tCVMetalTextureCacheRef cache = nullptr;\n\tCVMetalTextureRef yTextureRef = nullptr;\n\tCVMetalTextureRef uvTextureRef = nullptr;\n\tid<MTLDevice> device = nil;\n};\n\nMetalTextureCache::MetalTextureCache()\n: _private(std::make_unique<Private>()) {\n}\n\nMetalTextureCache::~MetalTextureCache() {\n\tflush();\n\tif (_private->cache) {\n\t\tCFRelease(_private->cache);\n\t}\n}\n\nbool MetalTextureCache::createTexturesFromPixelBuffer(\n\t\tQRhi *rhi,\n\t\tvoid *cvPixelBuffer,\n\t\tQRhiTexture **yTexture,\n\t\tQRhiTexture **uvTexture,\n\t\tQSize *lumaSize,\n\t\tQSize *chromaSize) {\n\tif (!cvPixelBuffer || !rhi) {\n\t\treturn false;\n\t}\n\n\tauto pixelBuffer = static_cast<CVPixelBufferRef>(cvPixelBuffer);\n\tconst auto format = CVPixelBufferGetPixelFormatType(pixelBuffer);\n\tif (format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange\n\t\t&& format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {\n\t\treturn false;\n\t}\n\n\tconst auto nativeHandles = rhi->nativeHandles();\n\tif (!nativeHandles) {\n\t\treturn false;\n\t}\n\tauto *mtlDevice = static_cast<id<MTLDevice>>(\n\t\tstatic_cast<const QRhiMetalNativeHandles*>(nativeHandles)->dev);\n\tif (!mtlDevice) {\n\t\treturn false;\n\t}\n\n\tif (_private->device != mtlDevice) {\n\t\tif (_private->cache) {\n\t\t\tCFRelease(_private->cache);\n\t\t\t_private->cache = nullptr;\n\t\t}\n\t\t_private->device = mtlDevice;\n\t}\n\tif (!_private->cache) {\n\t\tauto status = CVMetalTextureCacheCreate(\n\t\t\tkCFAllocatorDefault,\n\t\t\tnil,\n\t\t\tmtlDevice,\n\t\t\tnil,\n\t\t\t&_private->cache);\n\t\tif (status != kCVReturnSuccess || !_private->cache) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tflush();\n\n\tconst auto width = CVPixelBufferGetWidth(pixelBuffer);\n\tconst auto height = CVPixelBufferGetHeight(pixelBuffer);\n\n\tCVReturn status = CVMetalTextureCacheCreateTextureFromImage(\n\t\tkCFAllocatorDefault,\n\t\t_private->cache,\n\t\tpixelBuffer,\n\t\tnil,\n\t\tMTLPixelFormatR8Unorm,\n\t\twidth,\n\t\theight,\n\t\t0,\n\t\t&_private->yTextureRef);\n\tif (status != kCVReturnSuccess || !_private->yTextureRef) {\n\t\treturn false;\n\t}\n\n\tconst auto uvWidth = (width + 1) / 2;\n\tconst auto uvHeight = (height + 1) / 2;\n\tstatus = CVMetalTextureCacheCreateTextureFromImage(\n\t\tkCFAllocatorDefault,\n\t\t_private->cache,\n\t\tpixelBuffer,\n\t\tnil,\n\t\tMTLPixelFormatRG8Unorm,\n\t\tuvWidth,\n\t\tuvHeight,\n\t\t1,\n\t\t&_private->uvTextureRef);\n\tif (status != kCVReturnSuccess || !_private->uvTextureRef) {\n\t\tCFRelease(_private->yTextureRef);\n\t\t_private->yTextureRef = nullptr;\n\t\treturn false;\n\t}\n\n\tauto *yMtlTexture = CVMetalTextureGetTexture(_private->yTextureRef);\n\tauto *uvMtlTexture = CVMetalTextureGetTexture(_private->uvTextureRef);\n\tif (!yMtlTexture || !uvMtlTexture) {\n\t\tflush();\n\t\treturn false;\n\t}\n\n\t*lumaSize = QSize(width, height);\n\t*chromaSize = QSize(uvWidth, uvHeight);\n\n\tif (!*yTexture || (*yTexture)->pixelSize() != *lumaSize) {\n\t\tdelete *yTexture;\n\t\t*yTexture = rhi->newTexture(\n\t\t\tQRhiTexture::R8,\n\t\t\t*lumaSize,\n\t\t\t1,\n\t\t\tQRhiTexture::ExternalOES);\n\t\t(*yTexture)->create();\n\t}\n\tif (!*uvTexture || (*uvTexture)->pixelSize() != *chromaSize) {\n\t\tdelete *uvTexture;\n\t\t*uvTexture = rhi->newTexture(\n\t\t\tQRhiTexture::RG8,\n\t\t\t*chromaSize,\n\t\t\t1,\n\t\t\tQRhiTexture::ExternalOES);\n\t\t(*uvTexture)->create();\n\t}\n\n\t(*yTexture)->createFrom({quint64(yMtlTexture), 0});\n\t(*uvTexture)->createFrom({quint64(uvMtlTexture), 0});\n\n\treturn true;\n}\n\nvoid MetalTextureCache::flush() {\n\tif (_private->yTextureRef) {\n\t\tCFRelease(_private->yTextureRef);\n\t\t_private->yTextureRef = nullptr;\n\t}\n\tif (_private->uvTextureRef) {\n\t\tCFRelease(_private->uvTextureRef);\n\t\t_private->uvTextureRef = nullptr;\n\t}\n\tif (_private->cache) {\n\t\tCVMetalTextureCacheFlush(_private->cache, 0);\n\t}\n}\n\n} // namespace Media::View\n\n#endif // Q_OS_MAC\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_open_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/view/media_view_open_common.h\"\n\n#include \"history/history_item.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_web_page.h\"\n\nnamespace Media::View {\n\nTimeId ExtractVideoTimestamp(not_null<HistoryItem*> item) {\n\tconst auto media = item->media();\n\tif (!media) {\n\t\treturn 0;\n\t} else if (const auto timestamp = media->videoTimestamp()) {\n\t\treturn timestamp;\n\t} else if (const auto webpage = media->webpage()) {\n\t\treturn webpage->extractVideoTimestamp();\n\t}\n\treturn 0;\n}\n\nTextWithEntities StripQuoteEntities(TextWithEntities text) {\n\tfor (auto i = text.entities.begin(); i != text.entities.end();) {\n\t\tif (i->type() == EntityType::Blockquote) {\n\t\t\ti = text.entities.erase(i);\n\t\t\tcontinue;\n\t\t} else if (i->type() == EntityType::Pre) {\n\t\t\t*i = EntityInText(EntityType::Code, i->offset(), i->length());\n\t\t}\n\t\t++i;\n\t}\n\treturn text;\n}\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_open_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_cloud_themes.h\"\n#include \"data/data_stories.h\"\n\nclass DocumentData;\nclass PeerData;\nclass PhotoData;\nclass HistoryItem;\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Media::View {\n\nstruct OpenRequest {\npublic:\n\tOpenRequest() {\n\t}\n\n\tOpenRequest(\n\t\tWindow::SessionController *controller,\n\t\tnot_null<PhotoData*> photo,\n\t\tHistoryItem *item,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tbool showDrawButton = false)\n\t: _controller(controller)\n\t, _photo(photo)\n\t, _item(item)\n\t, _topicRootId(topicRootId)\n\t, _monoforumPeerId(monoforumPeerId)\n\t, _showDrawButton(showDrawButton) {\n\t}\n\tOpenRequest(\n\t\tWindow::SessionController *controller,\n\t\tnot_null<PhotoData*> photo,\n\t\tnot_null<PeerData*> peer)\n\t: _controller(controller)\n\t, _photo(photo)\n\t, _peer(peer) {\n\t}\n\n\tOpenRequest(\n\t\tWindow::SessionController *controller,\n\t\tnot_null<DocumentData*> document,\n\t\tHistoryItem *item,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tbool continueStreaming = false,\n\t\tcrl::time startTime = 0,\n\t\tbool showDrawButton = false)\n\t: _controller(controller)\n\t, _document(document)\n\t, _item(item)\n\t, _topicRootId(topicRootId)\n\t, _monoforumPeerId(monoforumPeerId)\n\t, _continueStreaming(continueStreaming)\n\t, _startTime(startTime)\n\t, _showDrawButton(showDrawButton) {\n\t}\n\tOpenRequest(\n\t\tWindow::SessionController *controller,\n\t\tnot_null<DocumentData*> document,\n\t\tconst Data::CloudTheme &cloudTheme)\n\t: _controller(controller)\n\t, _document(document)\n\t, _cloudTheme(cloudTheme) {\n\t}\n\n\tOpenRequest(\n\t\tWindow::SessionController *controller,\n\t\tnot_null<Data::Story*> story,\n\t\tData::StoriesContext context)\n\t: _controller(controller)\n\t, _story(story)\n\t, _storiesContext(context) {\n\t}\n\n\tOpenRequest(\n\t\tWindow::SessionController *controller,\n\t\tstd::shared_ptr<Data::GroupCall> call,\n\t\tQString linkSlug,\n\t\tMsgId joinMessageId)\n\t: _controller(controller)\n\t, _call(std::move(call))\n\t, _callLinkSlug(std::move(linkSlug))\n\t, _callJoinMessageId(joinMessageId) {\n\t}\n\n\t[[nodiscard]] PeerData *peer() const {\n\t\treturn _peer;\n\t}\n\n\t[[nodiscard]] PhotoData *photo() const {\n\t\treturn _photo;\n\t}\n\n\t[[nodiscard]] HistoryItem *item() const {\n\t\treturn _item;\n\t}\n\n\t[[nodiscard]] MsgId topicRootId() const {\n\t\treturn _topicRootId;\n\t}\n\t[[nodiscard]] PeerId monoforumPeerId() const {\n\t\treturn _monoforumPeerId;\n\t}\n\n\t[[nodiscard]] DocumentData *document() const {\n\t\treturn _document;\n\t}\n\n\t[[nodiscard]] Data::Story *story() const {\n\t\treturn _story;\n\t}\n\t[[nodiscard]] Data::StoriesContext storiesContext() const {\n\t\treturn _storiesContext;\n\t}\n\n\t[[nodiscard]] const std::shared_ptr<Data::GroupCall> &call() const {\n\t\treturn _call;\n\t}\n\t[[nodiscard]] const QString &callLinkSlug() const {\n\t\treturn _callLinkSlug;\n\t}\n\t[[nodiscard]] MsgId callJoinMessageId() const {\n\t\treturn _callJoinMessageId;\n\t}\n\n\t[[nodiscard]] std::optional<Data::CloudTheme> cloudTheme() const {\n\t\treturn _cloudTheme;\n\t}\n\n\t[[nodiscard]] Window::SessionController *controller() const {\n\t\treturn _controller;\n\t}\n\n\t[[nodiscard]] bool continueStreaming() const {\n\t\treturn _continueStreaming;\n\t}\n\n\t[[nodiscard]] crl::time startTime() const {\n\t\treturn _startTime;\n\t}\n\n\t[[nodiscard]] bool showDrawButton() const {\n\t\treturn _showDrawButton;\n\t}\n\nprivate:\n\tWindow::SessionController *_controller = nullptr;\n\tDocumentData *_document = nullptr;\n\tPhotoData *_photo = nullptr;\n\tData::Story *_story = nullptr;\n\tData::StoriesContext _storiesContext;\n\tPeerData *_peer = nullptr;\n\tHistoryItem *_item = nullptr;\n\tMsgId _topicRootId = 0;\n\tPeerId _monoforumPeerId = 0;\n\tstd::optional<Data::CloudTheme> _cloudTheme = std::nullopt;\n\tbool _continueStreaming = false;\n\tcrl::time _startTime = 0;\n\tbool _showDrawButton = false;\n\n\tstd::shared_ptr<Data::GroupCall> _call;\n\tQString _callLinkSlug;\n\tMsgId _callJoinMessageId = 0;\n\n};\n\n[[nodiscard]] TimeId ExtractVideoTimestamp(not_null<HistoryItem*> item);\n\n[[nodiscard]] TextWithEntities StripQuoteEntities(TextWithEntities text);\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/view/media_view_overlay_opengl.h\"\n\n#include \"data/data_peer_values.h\" // AmPremiumValue.\n#include \"ui/gl/gl_shader.h\"\n#include \"ui/painter.h\"\n#include \"media/stories/media_stories_view.h\"\n#include \"media/streaming/media_streaming_common.h\"\n#include \"media/view/media_view_video_stream.h\"\n#include \"platform/platform_overlay_widget.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"core/crash_reports.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Media::View {\nnamespace {\n\nusing namespace Ui::GL;\n\nconstexpr auto kNotchOffset = 4;\nconstexpr auto kRadialLoadingOffset = kNotchOffset + 4;\nconstexpr auto kThemePreviewOffset = kRadialLoadingOffset + 4;\nconstexpr auto kDocumentBubbleOffset = kThemePreviewOffset + 4;\nconstexpr auto kSaveMsgOffset = kDocumentBubbleOffset + 4;\nconstexpr auto kChapterOffset = kSaveMsgOffset + 4;\nconstexpr auto kSpeedBoostOffset = kChapterOffset + 4;\nconstexpr auto kFooterOffset = kSpeedBoostOffset + 4;\nconstexpr auto kCaptionOffset = kFooterOffset + 4;\nconstexpr auto kGroupThumbsOffset = kCaptionOffset + 4;\nconstexpr auto kControlsOffset = kGroupThumbsOffset + 4;\nconstexpr auto kControlValues = 4 * 4 + 4 * 4; // over + icon\n\n[[nodiscard]] ShaderPart FragmentApplyControlsFade() {\n\treturn {\n\t\t.header = R\"(\nuniform sampler2D f_texture;\nuniform vec4 shadowTopRect;\nuniform vec4 shadowBottomSkipOpacityFullFade;\n)\",\n\t\t.body = R\"(\n\tfloat topHeight = shadowTopRect.w;\n\tfloat bottomHeight = shadowBottomSkipOpacityFullFade.x;\n\tfloat bottomSkip = shadowBottomSkipOpacityFullFade.y;\n\tfloat opacity = shadowBottomSkipOpacityFullFade.z;\n\tfloat fullFade = shadowBottomSkipOpacityFullFade.w;\n\tfloat viewportHeight = shadowTopRect.y + topHeight;\n\tfloat fullHeight = topHeight + bottomHeight;\n\tfloat topY = min(\n\t\t(viewportHeight - gl_FragCoord.y) / fullHeight,\n\t\ttopHeight / fullHeight);\n\tfloat topX = (gl_FragCoord.x - shadowTopRect.x) / shadowTopRect.z;\n\tvec4 fadeTop = texture2D(f_texture, vec2(topX, topY)) * opacity;\n\tfloat bottomY = max(bottomSkip + fullHeight - gl_FragCoord.y, topHeight)\n\t\t/ fullHeight;\n\tvec4 fadeBottom = texture2D(f_texture, vec2(0.5, bottomY)) * opacity;\n\tfloat fade = min((1. - fadeTop.a) * (1. - fadeBottom.a), fullFade);\n\tresult.rgb = result.rgb * fade;\n)\",\n\t};\n}\n\n[[nodiscard]] ShaderPart FragmentPlaceOnTransparentBackground() {\n\treturn {\n\t\t.header = R\"(\nuniform vec4 transparentBg;\nuniform vec4 transparentFg;\nuniform float transparentSize;\n)\",\n\t\t.body = R\"(\n\tvec2 checkboardLadder = floor(gl_FragCoord.xy / transparentSize);\n\tfloat checkboard = mod(checkboardLadder.x + checkboardLadder.y, 2.0);\n\tvec4 checkboardColor = checkboard * transparentBg\n\t\t+ (1. - checkboard) * transparentFg;\n\tresult += checkboardColor * (1. - result.a);\n)\",\n\t};\n}\n\n[[nodiscard]] ShaderPart FragmentRoundedCorners() {\n\treturn {\n\t\t.header = R\"(\nuniform vec4 roundRect;\nuniform float roundRadius;\n\nfloat roundedCorner() {\n\tvec2 rectHalf = roundRect.zw / 2.;\n\tvec2 rectCenter = roundRect.xy + rectHalf;\n\tvec2 fromRectCenter = abs(gl_FragCoord.xy - rectCenter);\n\tvec2 vectorRadius = vec2(roundRadius + 0.5, roundRadius + 0.5);\n\tvec2 fromCenterWithRadius = fromRectCenter + vectorRadius;\n\tvec2 fromRoundingCenter = max(fromCenterWithRadius, rectHalf)\n\t\t- rectHalf;\n\tfloat rounded = length(fromRoundingCenter) - roundRadius;\n\n\treturn 1. - smoothstep(0., 1., rounded);\n}\n)\",\n\t\t.body = R\"(\n\tresult *= roundedCorner();\n)\",\n\t};\n}\n\n[[nodiscard]] QRectF StoryCropTextureRect(\n\t\tQSizeF imageSize,\n\t\tQSizeF targetSize) {\n\tif (imageSize.isEmpty() || targetSize.isEmpty()) {\n\t\treturn QRectF(0., 0., 1., 1.);\n\t}\n\tconst auto targetAspect = targetSize.width() / targetSize.height();\n\tconst auto imageAspect = imageSize.width() / imageSize.height();\n\tif (imageAspect > targetAspect) {\n\t\tconst auto cropW = imageSize.height() * targetAspect;\n\t\tconst auto offset = (imageSize.width() - cropW) / 2.;\n\t\treturn QRectF(\n\t\t\toffset / imageSize.width(),\n\t\t\t0.,\n\t\t\tcropW / imageSize.width(),\n\t\t\t1.);\n\t} else if (imageAspect < targetAspect) {\n\t\tconst auto cropH = imageSize.width() / targetAspect;\n\t\tconst auto offset = (imageSize.height() - cropH) / 2.;\n\t\treturn QRectF(\n\t\t\t0.,\n\t\t\toffset / imageSize.height(),\n\t\t\t1.,\n\t\t\tcropH / imageSize.height());\n\t}\n\treturn QRectF(0., 0., 1., 1.);\n}\n\n} // namespace\n\nOverlayWidget::RendererGL::RendererGL(not_null<OverlayWidget*> owner)\n: _owner(owner) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_controlsFadeImage.invalidate();\n\t\t_radialImage.invalidate();\n\t\t_documentBubbleImage.invalidate();\n\t\t_themePreviewImage.invalidate();\n\t\t_saveMsgImage.invalidate();\n\t\t_footerImage.invalidate();\n\t\t_captionImage.invalidate();\n\t\tinvalidateControls();\n\t}, _lifetime);\n\n\tcrl::on_main(this, [=] {\n\t\t_owner->_storiesChanged.events(\n\t\t) | rpl::on_next([=] {\n\t\t\tif (_owner->_storiesSession) {\n\t\t\t\tData::AmPremiumValue(\n\t\t\t\t\t_owner->_storiesSession\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\tinvalidateControls();\n\t\t\t\t}, _storiesLifetime);\n\t\t\t} else {\n\t\t\t\t_storiesLifetime.destroy();\n\t\t\t\tinvalidateControls();\n\t\t\t}\n\t\t}, _lifetime);\n\t});\n}\n\nvoid OverlayWidget::RendererGL::init(QOpenGLFunctions &f) {\n\tconstexpr auto kQuads = 9;\n\tconstexpr auto kQuadVertices = kQuads * 4;\n\tconstexpr auto kQuadValues = kQuadVertices * 4;\n\tconstexpr auto kControlsValues = kControlsCount * kControlValues;\n\tconstexpr auto kRoundingQuads = 4;\n\tconstexpr auto kRoundingVertices = kRoundingQuads * 6;\n\tconstexpr auto kRoundingValues = kRoundingVertices * 2;\n\tconstexpr auto kStoriesSiblingValues = kStoriesSiblingPartsCount * 16;\n\tconstexpr auto kValues = kQuadValues\n\t\t+ kControlsValues\n\t\t+ kRoundingValues\n\t\t+ kStoriesSiblingValues;\n\n\t_contentBuffer.emplace();\n\t_contentBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw);\n\t_contentBuffer->create();\n\t_contentBuffer->bind();\n\t_contentBuffer->allocate(kValues * sizeof(GLfloat));\n\n\t_textures.ensureCreated(f);\n\n\t_imageProgram.emplace();\n\t_texturedVertexShader = LinkProgram(\n\t\t&*_imageProgram,\n\t\tVertexShader({\n\t\t\tVertexViewportTransform(),\n\t\t\tVertexPassTextureCoord(),\n\t\t}),\n\t\tFragmentShader({\n\t\t\tFragmentSampleARGB32Texture(),\n\t\t})).vertex;\n\n\t_staticContentProgram.emplace();\n\tLinkProgram(\n\t\t&*_staticContentProgram,\n\t\t_texturedVertexShader,\n\t\tFragmentShader({\n\t\t\tFragmentSampleARGB32Texture(),\n\t\t\tFragmentApplyControlsFade(),\n\t\t\tFragmentRoundedCorners()\n\t\t}));\n\n\t_withTransparencyProgram.emplace();\n\tLinkProgram(\n\t\t&*_withTransparencyProgram,\n\t\t_texturedVertexShader,\n\t\tFragmentShader({\n\t\t\tFragmentSampleARGB32Texture(),\n\t\t\tFragmentPlaceOnTransparentBackground(),\n\t\t\tFragmentApplyControlsFade()\n\t\t}));\n\n\t_yuv420Program.emplace();\n\tLinkProgram(\n\t\t&*_yuv420Program,\n\t\t_texturedVertexShader,\n\t\tFragmentShader({\n\t\t\tFragmentSampleYUV420Texture(),\n\t\t\tFragmentApplyControlsFade(),\n\t\t\tFragmentRoundedCorners()\n\t\t}));\n\n\t_nv12Program.emplace();\n\tLinkProgram(\n\t\t&*_nv12Program,\n\t\t_texturedVertexShader,\n\t\tFragmentShader({\n\t\t\tFragmentSampleNV12Texture(),\n\t\t\tFragmentApplyControlsFade(),\n\t\t\tFragmentRoundedCorners()\n\t\t}));\n\n\t_fillProgram.emplace();\n\tLinkProgram(\n\t\t&*_fillProgram,\n\t\tVertexShader({ VertexViewportTransform() }),\n\t\tFragmentShader({ FragmentStaticColor() }));\n\n\t_controlsProgram.emplace();\n\tLinkProgram(\n\t\t&*_controlsProgram,\n\t\t_texturedVertexShader,\n\t\tFragmentShader({\n\t\t\tFragmentSampleARGB32Texture(),\n\t\t\tFragmentGlobalOpacity(),\n\t\t}));\n\n\t_roundedCornersProgram.emplace();\n\tLinkProgram(\n\t\t&*_roundedCornersProgram,\n\t\tVertexShader({ VertexViewportTransform() }),\n\t\tFragmentShader({\n\t\t\t{ .body = \"result = vec4(1.);\" },\n\t\t\tFragmentRoundedCorners(),\n\t\t}));\n\n\tconst auto renderer = reinterpret_cast<const char*>(\n\t\tf.glGetString(GL_RENDERER));\n\tCrashReports::SetAnnotation(\n\t\t\"OpenGL Renderer\",\n\t\trenderer ? renderer : \"[nullptr]\");\n}\n\nvoid OverlayWidget::RendererGL::deinit(QOpenGLFunctions *f) {\n\t_textures.destroy(f);\n\t_imageProgram = std::nullopt;\n\t_texturedVertexShader = nullptr;\n\t_withTransparencyProgram = std::nullopt;\n\t_yuv420Program = std::nullopt;\n\t_nv12Program = std::nullopt;\n\t_fillProgram = std::nullopt;\n\t_controlsProgram = std::nullopt;\n\t_contentBuffer = std::nullopt;\n\t_controlsFadeImage.destroy(f);\n\t_radialImage.destroy(f);\n\t_documentBubbleImage.destroy(f);\n\t_themePreviewImage.destroy(f);\n\t_saveMsgImage.destroy(f);\n\t_footerImage.destroy(f);\n\t_captionImage.destroy(f);\n\t_groupThumbsImage.destroy(f);\n\t_controlsImage.destroy(f);\n\tfor (auto &part : _storiesSiblingParts) {\n\t\tpart.destroy(f);\n\t}\n}\n\nvoid OverlayWidget::RendererGL::paint(\n\t\tnot_null<QOpenGLWidget*> widget,\n\t\tQOpenGLFunctions &f) {\n\tif (handleHideWorkaround(f)) {\n\t\treturn;\n\t}\n\tif (const auto stream = _owner->_videoStream.get()) {\n\t\tstream->ensureBorrowedRenderer(f);\n\t}\n\tconst auto factor = widget->devicePixelRatioF();\n\tif (_factor != factor) {\n\t\t_factor = factor;\n\t\t_ifactor = int(std::ceil(factor));\n\t\t_controlsImage.invalidate();\n\n\t\t// We use the fact that fade texture atlas\n\t\t// takes exactly full texture size. In case we\n\t\t// just invalidate it we may get larger image\n\t\t// in case of moving from greater _factor to lesser.\n\t\t_controlsFadeImage.destroy(&f);\n\t}\n\t_blendingEnabled = false;\n\t_viewport = widget->size();\n\t_uniformViewport = QVector2D(\n\t\t_viewport.width() * _factor,\n\t\t_viewport.height() * _factor);\n\t_f = &f;\n\t_owner->paint(this);\n\t_f = nullptr;\n}\n\nstd::optional<QColor> OverlayWidget::RendererGL::clearColor() {\n\tif (_owner->_hideWorkaround) {\n\t\treturn QColor(0, 0, 0, 0);\n\t} else if (_owner->_fullScreenVideo) {\n\t\treturn st::mediaviewVideoBg->c;\n\t} else {\n\t\treturn st::mediaviewBg->c;\n\t}\n}\n\nbool OverlayWidget::RendererGL::handleHideWorkaround(QOpenGLFunctions &f) {\n\t// This is needed on Windows or Linux,\n\t// because on reopen it blinks with the last shown content.\n\treturn _owner->_hideWorkaround != nullptr;\n}\n\nvoid OverlayWidget::RendererGL::paintBackground() {\n\t_contentBuffer->bind();\n\tif (const auto notch = _owner->topNotchSkip()) {\n\t\tconst auto top = transformRect(QRect(0, 0, _owner->width(), notch));\n\t\tconst GLfloat coords[] = {\n\t\t\ttop.left(), top.top(),\n\t\t\ttop.right(), top.top(),\n\t\t\ttop.right(), top.bottom(),\n\t\t\ttop.left(), top.bottom(),\n\t\t};\n\t\tconst auto offset = kNotchOffset;\n\t\t_contentBuffer->write(\n\t\t\toffset * 4 * sizeof(GLfloat),\n\t\t\tcoords,\n\t\t\tsizeof(coords));\n\n\t\t_fillProgram->bind();\n\t\t_fillProgram->setUniformValue(\"viewport\", _uniformViewport);\n\t\tFillRectangle(\n\t\t\t*_f,\n\t\t\t&*_fillProgram,\n\t\t\toffset,\n\t\t\tQColor(0, 0, 0));\n\t}\n}\n\nvoid OverlayWidget::RendererGL::paintVideoStream() {\n\t_owner->_videoStream->borrowedPaint(*_f);\n}\n\nvoid OverlayWidget::RendererGL::paintTransformedVideoFrame(\n\t\tContentGeometry geometry) {\n\tconst auto data = _owner->videoFrameWithInfo();\n\tif (data.format == Streaming::FrameFormat::None) {\n\t\treturn;\n\t} else if (data.format == Streaming::FrameFormat::ARGB32) {\n\t\tAssert(!data.image.isNull());\n\t\tpaintTransformedStaticContent(\n\t\t\tdata.image,\n\t\t\tgeometry,\n\t\t\tdata.alpha,\n\t\t\tdata.alpha);\n\t\treturn;\n\t}\n\tAssert(!data.yuv->size.isEmpty());\n\tconst auto program = (data.format == Streaming::FrameFormat::NV12)\n\t\t? &*_nv12Program\n\t\t: &*_yuv420Program;\n\tprogram->bind();\n\tconst auto nv12 = (data.format == Streaming::FrameFormat::NV12);\n\tconst auto yuv = data.yuv;\n\tconst auto nv12changed = (_chromaNV12 != nv12);\n\n\tconst auto upload = (_trackFrameIndex != data.index)\n\t\t|| (_streamedIndex != _owner->streamedIndex());\n\t_trackFrameIndex = data.index;\n\t_streamedIndex = _owner->streamedIndex();\n\n\t_f->glActiveTexture(GL_TEXTURE0);\n\t_textures.bind(*_f, 3);\n\tif (upload) {\n\t\t_f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1);\n\t\tuploadTexture(\n\t\t\tGL_ALPHA,\n\t\t\tGL_ALPHA,\n\t\t\tyuv->size,\n\t\t\t_lumaSize,\n\t\t\tyuv->y.stride,\n\t\t\tyuv->y.data);\n\t\t_lumaSize = yuv->size;\n\t}\n\t_f->glActiveTexture(GL_TEXTURE1);\n\t_textures.bind(*_f, 4);\n\tif (upload) {\n\t\tuploadTexture(\n\t\t\tnv12 ? GL_RG : GL_ALPHA,\n\t\t\tnv12 ? GL_RG : GL_ALPHA,\n\t\t\tyuv->chromaSize,\n\t\t\tnv12changed ? QSize() : _chromaSize,\n\t\t\tyuv->u.stride / (nv12 ? 2 : 1),\n\t\t\tyuv->u.data);\n\t\tif (nv12) {\n\t\t\t_chromaSize = yuv->chromaSize;\n\t\t\t_f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4);\n\t\t}\n\t\t_chromaNV12 = nv12;\n\t}\n\n\tvalidateControlsFade();\n\tif (nv12) {\n\t\t_f->glActiveTexture(GL_TEXTURE2);\n\t\t_controlsFadeImage.bind(*_f);\n\t} else {\n\t\t_f->glActiveTexture(GL_TEXTURE2);\n\t\t_textures.bind(*_f, 5);\n\t\tif (upload) {\n\t\t\tuploadTexture(\n\t\t\t\tGL_ALPHA,\n\t\t\t\tGL_ALPHA,\n\t\t\t\tyuv->chromaSize,\n\t\t\t\t_chromaSize,\n\t\t\t\tyuv->v.stride,\n\t\t\t\tyuv->v.data);\n\t\t\t_chromaSize = yuv->chromaSize;\n\t\t\t_f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4);\n\t\t}\n\n\t\t_f->glActiveTexture(GL_TEXTURE3);\n\t\t_controlsFadeImage.bind(*_f);\n\t}\n\tprogram->setUniformValue(\"y_texture\", GLint(0));\n\tif (nv12) {\n\t\tprogram->setUniformValue(\"uv_texture\", GLint(1));\n\t} else {\n\t\tprogram->setUniformValue(\"u_texture\", GLint(1));\n\t\tprogram->setUniformValue(\"v_texture\", GLint(2));\n\t}\n\tprogram->setUniformValue(\"f_texture\", GLint(nv12 ? 2 : 3));\n\n\ttoggleBlending(geometry.roundRadius > 0.);\n\tconst auto textureRect = _owner->_stories\n\t\t? StoryCropTextureRect(QSizeF(yuv->size), geometry.rect.size())\n\t\t: QRectF(0., 0., 1., 1.);\n\tpaintTransformedContent(program, geometry, false, textureRect);\n\n\tif (_owner->_recognitionResult.success\n\t\t&& !_owner->_recognitionResult.items.empty()) {\n\t\tconst auto opacity = _owner->_recognitionAnimation.value(\n\t\t\t_owner->_showRecognitionResults ? 1. : 0.);\n\t\tif (opacity > 0.) {\n\t\t\tpaintRecognitionOverlay(data.image, geometry, opacity);\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::RendererGL::paintTransformedStaticContent(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry,\n\t\tbool semiTransparent,\n\t\tbool fillTransparentBackground,\n\t\tint index) {\n\tExpects(index >= 0 && index < 3);\n\tExpects(image.isNull()\n\t\t|| image.format() == QImage::Format_RGB32\n\t\t|| image.format() == QImage::Format_ARGB32_Premultiplied);\n\n\tif (geometry.rect.isEmpty()) {\n\t\treturn;\n\t}\n\n\tauto &program = fillTransparentBackground\n\t\t? _withTransparencyProgram\n\t\t: _staticContentProgram;\n\tprogram->bind();\n\tif (fillTransparentBackground) {\n\t\tprogram->setUniformValue(\n\t\t\t\"transparentBg\",\n\t\t\tst::mediaviewTransparentBg->c);\n\t\tprogram->setUniformValue(\n\t\t\t\"transparentFg\",\n\t\t\tst::mediaviewTransparentFg->c);\n\t\tprogram->setUniformValue(\n\t\t\t\"transparentSize\",\n\t\t\tst::transparentPlaceholderSize * _factor);\n\t}\n\n\t_f->glActiveTexture(GL_TEXTURE0);\n\t_textures.bind(*_f, index);\n\tconst auto cacheKey = image.isNull() ? qint64(-1) : image.cacheKey();\n\tconst auto upload = (_cacheKeys[index] != cacheKey);\n\tif (upload) {\n\t\t_cacheKeys[index] = cacheKey;\n\t\tif (image.isNull()) {\n\t\t\t// Upload transparent 2x2 texture.\n\t\t\tconst auto stride = 2;\n\t\t\tconst uint32_t data[4] = { 0 };\n\t\t\tuploadTexture(\n\t\t\t\tUi::GL::kFormatRGBA,\n\t\t\t\tUi::GL::kFormatRGBA,\n\t\t\t\tQSize(2, 2),\n\t\t\t\t_rgbaSize[index],\n\t\t\t\tstride,\n\t\t\t\tdata);\n\t\t\t_rgbaSize[index] = QSize(2, 2);\n\t\t} else {\n\t\t\tconst auto stride = image.bytesPerLine() / 4;\n\t\t\tconst auto data = image.constBits();\n\t\t\tuploadTexture(\n\t\t\t\tUi::GL::kFormatRGBA,\n\t\t\t\tUi::GL::kFormatRGBA,\n\t\t\t\timage.size(),\n\t\t\t\t_rgbaSize[index],\n\t\t\t\tstride,\n\t\t\t\tdata);\n\t\t\t_rgbaSize[index] = image.size();\n\t\t}\n\t}\n\n\tvalidateControlsFade();\n\t_f->glActiveTexture(GL_TEXTURE1);\n\t_controlsFadeImage.bind(*_f);\n\n\tprogram->setUniformValue(\"s_texture\", GLint(0));\n\tprogram->setUniformValue(\"f_texture\", GLint(1));\n\n\ttoggleBlending((geometry.roundRadius > 0.)\n\t\t|| (semiTransparent && !fillTransparentBackground));\n\tconst auto textureRect = _owner->_stories\n\t\t? StoryCropTextureRect(QSizeF(image.size()), geometry.rect.size())\n\t\t: QRectF(0., 0., 1., 1.);\n\tpaintTransformedContent(\n\t\t&*program,\n\t\tgeometry,\n\t\tfillTransparentBackground,\n\t\ttextureRect);\n\n\tif (_owner->_recognitionResult.success\n\t\t&& !_owner->_recognitionResult.items.empty()\n\t\t&& !image.isNull()) {\n\t\tconst auto opacity = _owner->_recognitionAnimation.value(\n\t\t\t_owner->_showRecognitionResults ? 1. : 0.);\n\t\tif (opacity > 0.) {\n\t\t\tpaintRecognitionOverlay(image, geometry, opacity);\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::RendererGL::paintTransformedContent(\n\t\tnot_null<QOpenGLShaderProgram*> program,\n\t\tContentGeometry geometry,\n\t\tbool fillTransparentBackground,\n\t\tQRectF textureRect) {\n\tconst auto rect = scaleRect(\n\t\ttransformRect(geometry.rect),\n\t\tgeometry.scale);\n\tconst auto centerx = rect.x() + rect.width() / 2;\n\tconst auto centery = rect.y() + rect.height() / 2;\n\tconst auto rsin = float(std::sin(geometry.rotation * M_PI / 180.));\n\tconst auto rcos = float(std::cos(geometry.rotation * M_PI / 180.));\n\tconst auto rotated = [&](float x, float y) -> std::array<float, 2> {\n\t\tx -= centerx;\n\t\ty -= centery;\n\t\treturn std::array<float, 2>{\n\t\t\tcenterx + (x * rcos + y * rsin),\n\t\t\tcentery + (y * rcos - x * rsin)\n\t\t};\n\t};\n\tconst auto topleft = rotated(rect.left(), rect.top());\n\tconst auto topright = rotated(rect.right(), rect.top());\n\tconst auto bottomright = rotated(rect.right(), rect.bottom());\n\tconst auto bottomleft = rotated(rect.left(), rect.bottom());\n\tconst auto texLeft = float(textureRect.x());\n\tconst auto texRight = float(textureRect.x() + textureRect.width());\n\tconst auto texTop = 1.f - float(textureRect.y());\n\tconst auto texBottom = 1.f - float(textureRect.y() + textureRect.height());\n\tconst GLfloat coords[] = {\n\t\ttopleft[0], topleft[1],\n\t\ttexLeft, texTop,\n\n\t\ttopright[0], topright[1],\n\t\ttexRight, texTop,\n\n\t\tbottomright[0], bottomright[1],\n\t\ttexRight, texBottom,\n\n\t\tbottomleft[0], bottomleft[1],\n\t\ttexLeft, texBottom,\n\t};\n\n\t_contentBuffer->bind();\n\t_contentBuffer->write(0, coords, sizeof(coords));\n\n\tprogram->setUniformValue(\"viewport\", _uniformViewport);\n\tif (_owner->_stories) {\n\t\tconst auto &top = st::storiesShadowTop.size();\n\t\tconst auto shadowTop = geometry.topShadowShown\n\t\t\t? geometry.rect.y()\n\t\t\t: geometry.rect.y() - top.height();\n\t\tprogram->setUniformValue(\n\t\t\t\"shadowTopRect\",\n\t\t\tUniform(transformRect(\n\t\t\t\tQRect(QPoint(geometry.rect.x(), shadowTop), top))));\n\t} else {\n\t\tconst auto &top = st::mediaviewShadowTop.size();\n\t\tconst auto point = QPoint(\n\t\t\t_shadowTopFlip ? 0 : (_viewport.width() - top.width()),\n\t\t\t0);\n\t\tprogram->setUniformValue(\n\t\t\t\"shadowTopRect\",\n\t\t\tUniform(transformRect(QRect(point, top))));\n\t}\n\tconst auto &bottom = _owner->_stories\n\t\t? st::storiesShadowBottom\n\t\t: st::mediaviewShadowBottom;\n\tprogram->setUniformValue(\"shadowBottomSkipOpacityFullFade\", QVector4D(\n\t\tbottom.height() * _factor,\n\t\tgeometry.bottomShadowSkip * _factor,\n\t\tgeometry.controlsOpacity,\n\t\t1.f - float(geometry.fade)));\n\tif (!fillTransparentBackground) {\n\t\tprogram->setUniformValue(\n\t\t\t\"roundRect\",\n\t\t\tgeometry.roundRadius ? Uniform(rect) : QVector4D(\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\t_uniformViewport.x(),\n\t\t\t\t_uniformViewport.y()));\n\t\tprogram->setUniformValue(\n\t\t\t\"roundRadius\",\n\t\t\tGLfloat(geometry.roundRadius * _factor));\n\t}\n\tFillTexturedRectangle(*_f, &*program);\n}\n\nvoid OverlayWidget::RendererGL::uploadTexture(\n\t\tGLint internalformat,\n\t\tGLint format,\n\t\tQSize size,\n\t\tQSize hasSize,\n\t\tint stride,\n\t\tconst void *data) const {\n\t_f->glPixelStorei(GL_UNPACK_ROW_LENGTH, stride);\n\tif (hasSize != size) {\n\t\t_f->glTexImage2D(\n\t\t\tGL_TEXTURE_2D,\n\t\t\t0,\n\t\t\tinternalformat,\n\t\t\tsize.width(),\n\t\t\tsize.height(),\n\t\t\t0,\n\t\t\tformat,\n\t\t\tGL_UNSIGNED_BYTE,\n\t\t\tdata);\n\t} else {\n\t\t_f->glTexSubImage2D(\n\t\t\tGL_TEXTURE_2D,\n\t\t\t0,\n\t\t\t0,\n\t\t\t0,\n\t\t\tsize.width(),\n\t\t\tsize.height(),\n\t\t\tformat,\n\t\t\tGL_UNSIGNED_BYTE,\n\t\t\tdata);\n\t}\n\t_f->glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);\n}\n\nvoid OverlayWidget::RendererGL::paintRadialLoading(\n\t\tQRect inner,\n\t\tbool radial,\n\t\tfloat64 radialOpacity) {\n\tpaintUsingRaster(_radialImage, inner, [&](Painter &&p) {\n\t\tconst auto newInner = QRect(QPoint(), inner.size());\n\t\t_owner->paintRadialLoadingContent(p, newInner, radial, radialOpacity);\n\t}, kRadialLoadingOffset, true);\n}\n\nvoid OverlayWidget::RendererGL::paintThemePreview(QRect outer) {\n\tpaintUsingRaster(_themePreviewImage, outer, [&](Painter &&p) {\n\t\tp.translate(-outer.topLeft());\n\t\t_owner->paintThemePreviewContent(p, outer, outer);\n\t}, kThemePreviewOffset);\n}\n\nvoid OverlayWidget::RendererGL::paintDocumentBubble(\n\t\tQRect outer,\n\t\tQRect icon) {\n\tpaintUsingRaster(_documentBubbleImage, outer, [&](Painter &&p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\tconst auto newIcon = icon.translated(-outer.topLeft());\n\t\t_owner->paintDocumentBubbleContent(p, newOuter, newIcon, newOuter);\n\t}, kDocumentBubbleOffset);\n\t_owner->paintRadialLoading(this);\n}\n\nvoid OverlayWidget::RendererGL::paintSaveMsg(QRect outer) {\n\tpaintUsingRaster(_saveMsgImage, outer, [&](Painter &&p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintSaveMsgContent(p, newOuter, newOuter);\n\t}, kSaveMsgOffset, true);\n}\n\nvoid OverlayWidget::RendererGL::paintChapter(QRect outer) {\n\tpaintUsingRaster(_chapterImage, outer, [&](Painter &&p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintChapterContent(p, newOuter, newOuter);\n\t}, kChapterOffset, true);\n}\n\nvoid OverlayWidget::RendererGL::paintSpeedBoost(QRect outer) {\n\tpaintUsingRaster(_speedBoostImage, outer, [&](Painter &&p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintSpeedBoostContent(p, newOuter, newOuter);\n\t}, kSpeedBoostOffset, true);\n}\n\nvoid OverlayWidget::RendererGL::paintControlsStart() {\n\tvalidateControls();\n\t_f->glActiveTexture(GL_TEXTURE0);\n\t_controlsImage.bind(*_f);\n\ttoggleBlending(true);\n}\n\nvoid OverlayWidget::RendererGL::paintControl(\n\t\tOver control,\n\t\tQRect over,\n\t\tfloat64 overOpacity,\n\t\tQRect inner,\n\t\tfloat64 innerOpacity,\n\t\tconst style::icon &icon) {\n\tconst auto meta = controlMeta(control);\n\tAssert(meta.icon == &icon);\n\n\tconst auto overAlpha = overOpacity * kOverBackgroundOpacity;\n\tconst auto offset = kControlsOffset + (meta.index * kControlValues) / 4;\n\tconst auto fgOffset = offset + 4;\n\tconst auto overRect = _controlsImage.texturedRect(\n\t\tover,\n\t\t_controlsTextures[kControlsCount]);\n\tconst auto overGeometry = transformRect(over);\n\tconst auto iconRect = _controlsImage.texturedRect(\n\t\tinner,\n\t\t_controlsTextures[meta.index]);\n\tconst auto iconGeometry = transformRect(iconRect.geometry);\n\tconst GLfloat coords[] = {\n\t\toverGeometry.left(), overGeometry.top(),\n\t\toverRect.texture.left(), overRect.texture.bottom(),\n\n\t\toverGeometry.right(), overGeometry.top(),\n\t\toverRect.texture.right(), overRect.texture.bottom(),\n\n\t\toverGeometry.right(), overGeometry.bottom(),\n\t\toverRect.texture.right(), overRect.texture.top(),\n\n\t\toverGeometry.left(), overGeometry.bottom(),\n\t\toverRect.texture.left(), overRect.texture.top(),\n\n\t\ticonGeometry.left(), iconGeometry.top(),\n\t\ticonRect.texture.left(), iconRect.texture.bottom(),\n\n\t\ticonGeometry.right(), iconGeometry.top(),\n\t\ticonRect.texture.right(), iconRect.texture.bottom(),\n\n\t\ticonGeometry.right(), iconGeometry.bottom(),\n\t\ticonRect.texture.right(), iconRect.texture.top(),\n\n\t\ticonGeometry.left(), iconGeometry.bottom(),\n\t\ticonRect.texture.left(), iconRect.texture.top(),\n\t};\n\t_controlsProgram->bind();\n\t_controlsProgram->setUniformValue(\"viewport\", _uniformViewport);\n\t_contentBuffer->bind();\n\tif (!over.isEmpty() && overOpacity > 0) {\n\t\t_contentBuffer->write(\n\t\t\toffset * 4 * sizeof(GLfloat),\n\t\t\tcoords,\n\t\t\tsizeof(coords));\n\t\t_controlsProgram->setUniformValue(\"g_opacity\", GLfloat(overAlpha));\n\t\tFillTexturedRectangle(*_f, &*_controlsProgram, offset);\n\t} else {\n\t\t_contentBuffer->write(\n\t\t\tfgOffset * 4 * sizeof(GLfloat),\n\t\t\tcoords + (fgOffset - offset) * 4,\n\t\t\tsizeof(coords) - (fgOffset - offset) * 4 * sizeof(GLfloat));\n\t}\n\t_controlsProgram->setUniformValue(\"g_opacity\", GLfloat(innerOpacity));\n\tFillTexturedRectangle(*_f, &*_controlsProgram, fgOffset);\n}\n\nauto OverlayWidget::RendererGL::controlMeta(Over control) const -> Control {\n\tconst auto stories = [&] {\n\t\treturn (_owner->_stories != nullptr);\n\t};\n\tswitch (control) {\n\tcase Over::Left: return {\n\t\t0,\n\t\tstories() ? &st::storiesLeft : &st::mediaviewLeft\n\t};\n\tcase Over::Right: return {\n\t\t1,\n\t\tstories() ? &st::storiesRight : &st::mediaviewRight\n\t};\n\tcase Over::Save: return {\n\t\t2,\n\t\t(_owner->saveControlLocked()\n\t\t\t? &st::mediaviewSaveLocked\n\t\t\t: &st::mediaviewSave)\n\t};\n\tcase Over::Share: return { 3, &st::mediaviewShare };\n\tcase Over::Rotate: return { 4, &st::mediaviewRotate };\n\tcase Over::More: return { 5, &st::mediaviewMore };\n\tcase Over::Draw: return { 6, &st::mediaviewDraw };\n\tcase Over::Recognize: return { 7, &st::mediaviewRecognize };\n\t}\n\tUnexpected(\"Control value in OverlayWidget::RendererGL::ControlIndex.\");\n}\n\nvoid OverlayWidget::RendererGL::validateControls() {\n\tif (!_controlsImage.image().isNull()) {\n\t\treturn;\n\t}\n\tconst auto metas = {\n\t\tcontrolMeta(Over::Left),\n\t\tcontrolMeta(Over::Right),\n\t\tcontrolMeta(Over::Save),\n\t\tcontrolMeta(Over::Share),\n\t\tcontrolMeta(Over::Rotate),\n\t\tcontrolMeta(Over::More),\n\t\tcontrolMeta(Over::Draw),\n\t\tcontrolMeta(Over::Recognize),\n\t};\n\tauto maxWidth = 0;\n\tauto fullHeight = 0;\n\tfor (const auto &meta : metas) {\n\t\tmaxWidth = std::max(meta.icon->width(), maxWidth);\n\t\tfullHeight += meta.icon->height();\n\t}\n\tmaxWidth = std::max(st::mediaviewIconOver, maxWidth);\n\tfullHeight += st::mediaviewIconOver;\n\tauto image = QImage(\n\t\tQSize(maxWidth, fullHeight) * _ifactor,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.fill(Qt::transparent);\n\timage.setDevicePixelRatio(_ifactor);\n\t{\n\t\tauto p = QPainter(&image);\n\t\tauto index = 0;\n\t\tauto height = 0;\n\t\tfor (const auto &meta : metas) {\n\t\t\tmeta.icon->paint(p, 0, height, maxWidth);\n\t\t\t_controlsTextures[index++] = QRect(\n\t\t\t\tQPoint(0, height) * _ifactor,\n\t\t\t\tmeta.icon->size() * _ifactor);\n\t\t\theight += meta.icon->height();\n\t\t}\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(OverBackgroundColor());\n\t\tp.drawEllipse(\n\t\t\tQRect(0, height, st::mediaviewIconOver, st::mediaviewIconOver));\n\t\t_controlsTextures[index++] = QRect(\n\t\t\tQPoint(0, height) * _ifactor,\n\t\t\tQSize(st::mediaviewIconOver, st::mediaviewIconOver) * _ifactor);\n\t\theight += st::mediaviewIconOver;\n\t}\n\t_controlsImage.setImage(std::move(image));\n}\n\nvoid OverlayWidget::RendererGL::invalidateControls() {\n\t_controlsImage.invalidate();\n\tranges::fill(_controlsTextures, QRect());\n}\n\nvoid OverlayWidget::RendererGL::validateControlsFade() {\n\tconst auto forStories = (_owner->_stories != nullptr);\n\tconst auto flip = !forStories && !_owner->topShadowOnTheRight();\n\tif (!_controlsFadeImage.image().isNull()\n\t\t&& _shadowTopFlip == flip\n\t\t&& _shadowsForStories == forStories) {\n\t\treturn;\n\t}\n\t_shadowTopFlip = flip;\n\t_shadowsForStories = forStories;\n\tconst auto &top = _shadowsForStories\n\t\t? st::storiesShadowTop\n\t\t: st::mediaviewShadowTop;\n\tconst auto &bottom = _shadowsForStories\n\t\t? st::storiesShadowBottom\n\t\t: st::mediaviewShadowBottom;\n\tconst auto width = top.width();\n\tconst auto bottomTop = top.height();\n\tconst auto height = bottomTop + bottom.height();\n\n\tauto image = QImage(\n\t\tQSize(width, height) * _ifactor,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.fill(Qt::transparent);\n\timage.setDevicePixelRatio(_ifactor);\n\n\tauto p = QPainter(&image);\n\ttop.paint(p, 0, 0, width);\n\tbottom.fill(\n\t\tp,\n\t\tQRect(0, bottomTop, width, bottom.height()));\n\tp.end();\n\n\tif (flip) {\n\t\timage = std::move(image).mirrored(true, false);\n\t}\n\n\t_controlsFadeImage.setImage(std::move(image));\n}\n\nvoid OverlayWidget::RendererGL::paintFooter(QRect outer, float64 opacity) {\n\tpaintUsingRaster(_footerImage, outer, [&](Painter &&p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintFooterContent(p, newOuter, newOuter, opacity);\n\t}, kFooterOffset, true);\n}\n\nvoid OverlayWidget::RendererGL::paintCaption(QRect outer, float64 opacity) {\n\tpaintUsingRaster(_captionImage, outer, [&](Painter &&p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintCaptionContent(p, newOuter, newOuter, opacity);\n\t}, kCaptionOffset, true);\n}\n\nvoid OverlayWidget::RendererGL::paintGroupThumbs(\n\t\tQRect outer,\n\t\tfloat64 opacity) {\n\tpaintUsingRaster(_groupThumbsImage, outer, [&](Painter &&p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintGroupThumbsContent(p, newOuter, newOuter, opacity);\n\t}, kGroupThumbsOffset, true);\n}\n\nvoid OverlayWidget::RendererGL::paintRoundedCorners(int radius) {\n\tconst auto topLeft = transformRect(QRect(0, 0, radius, radius));\n\tconst auto topRight = transformRect(\n\t\tQRect(_viewport.width() - radius, 0, radius, radius));\n\tconst auto bottomRight = transformRect(QRect(\n\t\t_viewport.width() - radius,\n\t\t_viewport.height() - radius,\n\t\tradius,\n\t\tradius));\n\tconst auto bottomLeft = transformRect(\n\t\tQRect(0, _viewport.height() - radius, radius, radius));\n\tconst GLfloat coords[] = {\n\t\ttopLeft.left(), topLeft.top(),\n\t\ttopLeft.right(), topLeft.top(),\n\t\ttopLeft.right(), topLeft.bottom(),\n\t\ttopLeft.right(), topLeft.bottom(),\n\t\ttopLeft.left(), topLeft.bottom(),\n\t\ttopLeft.left(), topLeft.top(),\n\n\t\ttopRight.left(), topRight.top(),\n\t\ttopRight.right(), topRight.top(),\n\t\ttopRight.right(), topRight.bottom(),\n\t\ttopRight.right(), topRight.bottom(),\n\t\ttopRight.left(), topRight.bottom(),\n\t\ttopRight.left(), topRight.top(),\n\n\t\tbottomRight.left(), bottomRight.top(),\n\t\tbottomRight.right(), bottomRight.top(),\n\t\tbottomRight.right(), bottomRight.bottom(),\n\t\tbottomRight.right(), bottomRight.bottom(),\n\t\tbottomRight.left(), bottomRight.bottom(),\n\t\tbottomRight.left(), bottomRight.top(),\n\n\t\tbottomLeft.left(), bottomLeft.top(),\n\t\tbottomLeft.right(), bottomLeft.top(),\n\t\tbottomLeft.right(), bottomLeft.bottom(),\n\t\tbottomLeft.right(), bottomLeft.bottom(),\n\t\tbottomLeft.left(), bottomLeft.bottom(),\n\t\tbottomLeft.left(), bottomLeft.top(),\n\t};\n\tconst auto offset = kControlsOffset\n\t\t+ (kControlsCount * kControlValues) / 4;\n\tconst auto byteOffset = offset * 4 * sizeof(GLfloat);\n\t_contentBuffer->bind();\n\t_contentBuffer->write(byteOffset, coords, sizeof(coords));\n\t_roundedCornersProgram->bind();\n\t_roundedCornersProgram->setUniformValue(\"viewport\", _uniformViewport);\n\tconst auto roundRect = transformRect(QRect(QPoint(), _viewport));\n\t_roundedCornersProgram->setUniformValue(\"roundRect\", Uniform(roundRect));\n\t_roundedCornersProgram->setUniformValue(\n\t\t\"roundRadius\",\n\t\tGLfloat(radius * _factor));\n\n\t_f->glEnable(GL_BLEND);\n\t_f->glBlendFunc(GL_ZERO, GL_SRC_ALPHA);\n\n\tGLint position = _roundedCornersProgram->attributeLocation(\"position\");\n\t_f->glVertexAttribPointer(\n\t\tposition,\n\t\t2,\n\t\tGL_FLOAT,\n\t\tGL_FALSE,\n\t\t2 * sizeof(GLfloat),\n\t\treinterpret_cast<const void*>(byteOffset));\n\t_f->glEnableVertexAttribArray(position);\n\n\t_f->glDrawArrays(GL_TRIANGLES, 0, base::array_size(coords) / 2);\n\n\t_f->glDisableVertexAttribArray(position);\n}\n\nvoid OverlayWidget::RendererGL::paintStoriesSiblingPart(\n\t\tint index,\n\t\tconst QImage &image,\n\t\tQRect rect,\n\t\tfloat64 opacity) {\n\tExpects(index >= 0 && index < kStoriesSiblingPartsCount);\n\n\tif (image.isNull() || rect.isEmpty()) {\n\t\treturn;\n\t}\n\n\t_f->glActiveTexture(GL_TEXTURE0);\n\n\tauto &part = _storiesSiblingParts[index];\n\tpart.setImage(image);\n\tpart.bind(*_f);\n\n\tconst auto textured = part.texturedRect(\n\t\trect,\n\t\tQRect(QPoint(), image.size()));\n\tconst auto geometry = transformRect(textured.geometry);\n\tconst GLfloat coords[] = {\n\t\tgeometry.left(), geometry.top(),\n\t\ttextured.texture.left(), textured.texture.bottom(),\n\n\t\tgeometry.right(), geometry.top(),\n\t\ttextured.texture.right(), textured.texture.bottom(),\n\n\t\tgeometry.right(), geometry.bottom(),\n\t\ttextured.texture.right(), textured.texture.top(),\n\n\t\tgeometry.left(), geometry.bottom(),\n\t\ttextured.texture.left(), textured.texture.top(),\n\t};\n\tconst auto offset = kControlsOffset\n\t\t+ (kControlsCount * kControlValues) / 4\n\t\t+ (6 * 2 * 4) / 4 // rounding\n\t\t+ (index * 4);\n\tconst auto byteOffset = offset * 4 * sizeof(GLfloat);\n\t_contentBuffer->bind();\n\t_contentBuffer->write(byteOffset, coords, sizeof(coords));\n\n\t_controlsProgram->bind();\n\t_controlsProgram->setUniformValue(\"viewport\", _uniformViewport);\n\t_contentBuffer->write(\n\t\toffset * 4 * sizeof(GLfloat),\n\t\tcoords,\n\t\tsizeof(coords));\n\t_controlsProgram->setUniformValue(\"g_opacity\", GLfloat(opacity));\n\tFillTexturedRectangle(*_f, &*_controlsProgram, offset);\n}\n\n//\n//void OverlayWidget::RendererGL::invalidate() {\n//\t_trackFrameIndex = -1;\n//\t_streamedIndex = -1;\n//\tconst auto images = {\n//\t\t&_radialImage,\n//\t\t&_documentBubbleImage,\n//\t\t&_themePreviewImage,\n//\t\t&_saveMsgImage,\n//\t\t&_footerImage,\n//\t\t&_captionImage,\n//\t\t&_groupThumbsImage,\n//\t\t&_controlsImage,\n//\t};\n//\tfor (const auto image : images) {\n//\t\timage->setImage(QImage());\n//\t}\n//\tinvalidateControls();\n//}\n\nvoid OverlayWidget::RendererGL::paintUsingRaster(\n\t\tUi::GL::Image &image,\n\t\tQRect rect,\n\t\tFn<void(Painter&&)> method,\n\t\tint bufferOffset,\n\t\tbool transparent) {\n\tauto raster = image.takeImage();\n\tconst auto size = rect.size() * _ifactor;\n\tif (raster.width() < size.width() || raster.height() < size.height()) {\n\t\traster = QImage(size, QImage::Format_ARGB32_Premultiplied);\n\t\tAssert(!raster.isNull());\n\t\traster.setDevicePixelRatio(_ifactor);\n\t\tif (!transparent\n\t\t\t&& (raster.width() > size.width()\n\t\t\t\t|| raster.height() > size.height())) {\n\t\t\traster.fill(Qt::transparent);\n\t\t}\n\t} else if (raster.devicePixelRatio() != _ifactor) {\n\t\traster.setDevicePixelRatio(_ifactor);\n\t}\n\n\tif (transparent) {\n\t\traster.fill(Qt::transparent);\n\t}\n\tmethod(Painter(&raster));\n\n\t_f->glActiveTexture(GL_TEXTURE0);\n\n\timage.setImage(std::move(raster), size);\n\timage.bind(*_f);\n\n\tconst auto textured = image.texturedRect(rect, QRect(QPoint(), size));\n\tconst auto geometry = transformRect(textured.geometry);\n\tconst GLfloat coords[] = {\n\t\tgeometry.left(), geometry.top(),\n\t\ttextured.texture.left(), textured.texture.bottom(),\n\n\t\tgeometry.right(), geometry.top(),\n\t\ttextured.texture.right(), textured.texture.bottom(),\n\n\t\tgeometry.right(), geometry.bottom(),\n\t\ttextured.texture.right(), textured.texture.top(),\n\n\t\tgeometry.left(), geometry.bottom(),\n\t\ttextured.texture.left(), textured.texture.top(),\n\t};\n\t_contentBuffer->bind();\n\t_contentBuffer->write(\n\t\tbufferOffset * 4 * sizeof(GLfloat),\n\t\tcoords,\n\t\tsizeof(coords));\n\n\t_imageProgram->bind();\n\t_imageProgram->setUniformValue(\"viewport\", _uniformViewport);\n\t_imageProgram->setUniformValue(\"s_texture\", GLint(0));\n\n\ttoggleBlending(transparent);\n\tFillTexturedRectangle(*_f, &*_imageProgram, bufferOffset);\n}\n\nvoid OverlayWidget::RendererGL::toggleBlending(bool enabled) {\n\tif (_blendingEnabled == enabled) {\n\t\treturn;\n\t} else if (enabled) {\n\t\t_f->glEnable(GL_BLEND);\n\t\t_f->glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);\n\t} else {\n\t\t_f->glDisable(GL_BLEND);\n\t}\n\t_blendingEnabled = enabled;\n}\n\nRect OverlayWidget::RendererGL::transformRect(const Rect &raster) const {\n\treturn TransformRect(raster, _viewport, _factor);\n}\n\nRect OverlayWidget::RendererGL::transformRect(const QRectF &raster) const {\n\treturn TransformRect(raster, _viewport, _factor);\n}\n\nRect OverlayWidget::RendererGL::transformRect(const QRect &raster) const {\n\treturn TransformRect(Rect(raster), _viewport, _factor);\n}\n\nRect OverlayWidget::RendererGL::scaleRect(\n\t\tconst Rect &unscaled,\n\t\tfloat64 scale) const {\n\tconst auto added = scale - 1.;\n\tconst auto addw = unscaled.width() * added;\n\tconst auto addh = unscaled.height() * added;\n\treturn Rect(\n\t\tunscaled.x() - addw / 2,\n\t\tunscaled.y() - addh / 2,\n\t\tunscaled.width() + addw,\n\t\tunscaled.height() + addh);\n}\n\nvoid OverlayWidget::RendererGL::paintRecognitionOverlay(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry,\n\t\tfloat64 opacity) {\n\tconst auto rect = scaleRect(\n\t\ttransformRect(geometry.rect),\n\t\tgeometry.scale);\n\tconst auto centerx = rect.x() + rect.width() / 2;\n\tconst auto centery = rect.y() + rect.height() / 2;\n\tconst auto radians = float(geometry.rotation * M_PI / 180.);\n\tconst auto rsin = std::sin(radians);\n\tconst auto rcos = std::cos(radians);\n\tconst auto rotated = [&](float x, float y) -> std::array<float, 2> {\n\t\tx -= centerx;\n\t\ty -= centery;\n\t\treturn std::array<float, 2>{\n\t\t\tcenterx + (x * rcos + y * rsin),\n\t\t\tcentery + (y * rcos - x * rsin)\n\t\t};\n\t};\n\n\tconst auto scale = rect.width() / float64(image.width());\n\tconst auto imageTopLeft = QPointF(rect.left(), rect.top());\n\n\t_fillProgram->bind();\n\t_fillProgram->setUniformValue(\"viewport\", _uniformViewport);\n\t_contentBuffer->bind();\n\n\t_f->glEnable(GL_STENCIL_TEST);\n\t_f->glClear(GL_STENCIL_BUFFER_BIT);\n\t_f->glStencilFunc(GL_ALWAYS, 1, 0xFF);\n\t_f->glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);\n\t_f->glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);\n\n\tfor (const auto &item : _owner->_recognitionResult.items) {\n\t\tconst auto &r = item.rect;\n\t\tconst auto lightRect = QRectF(\n\t\t\timageTopLeft.x() + r.left() * _ifactor * scale,\n\t\t\timageTopLeft.y()\n\t\t\t\t+ (image.height() - (r.y() + r.height()) * _ifactor) * scale,\n\t\t\tr.width() * _ifactor * scale,\n\t\t\tr.height() * _ifactor * scale);\n\t\tconst auto tl = rotated(lightRect.left(), lightRect.top());\n\t\tconst auto tr = rotated(lightRect.right(), lightRect.top());\n\t\tconst auto br = rotated(lightRect.right(), lightRect.bottom());\n\t\tconst auto bl = rotated(lightRect.left(), lightRect.bottom());\n\t\tconst GLfloat coords[] = {\n\t\t\ttl[0], tl[1], tr[0], tr[1], br[0], br[1], bl[0], bl[1],\n\t\t};\n\t\t_contentBuffer->write(0, coords, sizeof(coords));\n\t\tFillRectangle(*_f, &*_fillProgram, 0, QColor(255, 255, 255));\n\t}\n\n\t_f->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);\n\t_f->glStencilFunc(GL_NOTEQUAL, 1, 0xFF);\n\t_f->glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);\n\n\t_f->glEnable(GL_BLEND);\n\t_f->glBlendFuncSeparate(GL_ZERO, GL_SRC_COLOR, GL_ZERO, GL_ONE);\n\n\tconst auto tl = rotated(rect.left(), rect.top());\n\tconst auto tr = rotated(rect.right(), rect.top());\n\tconst auto br = rotated(rect.right(), rect.bottom());\n\tconst auto bl = rotated(rect.left(), rect.bottom());\n\tconst auto dark = int(150 + (255 - 150) * (1. - opacity));\n\tconst GLfloat darkRect[] = {\n\t\ttl[0], tl[1], tr[0], tr[1], br[0], br[1], bl[0], bl[1],\n\t};\n\t_contentBuffer->write(0, darkRect, sizeof(darkRect));\n\tFillRectangle(*_f, &*_fillProgram, 0, QColor(dark, dark, dark, 255));\n\n\t_f->glDisable(GL_BLEND);\n\t_f->glDisable(GL_STENCIL_TEST);\n}\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_overlay_opengl.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/view/media_view_overlay_renderer.h\"\n#include \"ui/gl/gl_image.h\"\n#include \"ui/gl/gl_primitives.h\"\n\n#include <QOpenGLBuffer>\n\nnamespace Media::View {\n\nclass OverlayWidget::RendererGL final\n\t: public OverlayWidget::Renderer\n\t, public base::has_weak_ptr {\npublic:\n\texplicit RendererGL(not_null<OverlayWidget*> owner);\n\n\tvoid init(QOpenGLFunctions &f) override;\n\n\tvoid deinit(QOpenGLFunctions *f) override;\n\n\tvoid paint(\n\t\tnot_null<QOpenGLWidget*> widget,\n\t\tQOpenGLFunctions &f) override;\n\n\tstd::optional<QColor> clearColor() override;\n\nprivate:\n\tstruct Control {\n\t\tint index = -1;\n\t\tnot_null<const style::icon*> icon;\n\t};\n\tbool handleHideWorkaround(QOpenGLFunctions &f);\n\n\tvoid paintBackground() override;\n\tvoid paintVideoStream() override;\n\tvoid paintTransformedVideoFrame(ContentGeometry geometry) override;\n\tvoid paintTransformedStaticContent(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry,\n\t\tbool semiTransparent,\n\t\tbool fillTransparentBackground,\n\t\tint index = 0) override;\n\tvoid paintTransformedContent(\n\t\tnot_null<QOpenGLShaderProgram*> program,\n\t\tContentGeometry geometry,\n\t\tbool fillTransparentBackground,\n\t\tQRectF textureRect = QRectF(0., 0., 1., 1.));\n\tvoid paintRadialLoading(\n\t\tQRect inner,\n\t\tbool radial,\n\t\tfloat64 radialOpacity) override;\n\tvoid paintThemePreview(QRect outer) override;\n\tvoid paintDocumentBubble(QRect outer, QRect icon) override;\n\tvoid paintSaveMsg(QRect outer) override;\n\tvoid paintChapter(QRect outer) override;\n\tvoid paintSpeedBoost(QRect outer) override;\n\tvoid paintControlsStart() override;\n\tvoid paintControl(\n\t\tOver control,\n\t\tQRect over,\n\t\tfloat64 overOpacity,\n\t\tQRect inner,\n\t\tfloat64 innerOpacity,\n\t\tconst style::icon &icon) override;\n\tvoid paintFooter(QRect outer, float64 opacity) override;\n\tvoid paintCaption(QRect outer, float64 opacity) override;\n\tvoid paintGroupThumbs(QRect outer, float64 opacity) override;\n\tvoid paintRoundedCorners(int radius) override;\n\tvoid paintStoriesSiblingPart(\n\t\tint index,\n\t\tconst QImage &image,\n\t\tQRect rect,\n\t\tfloat64 opacity = 1.) override;\n\n\tvoid paintRecognitionOverlay(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry,\n\t\tfloat64 opacity);\n\n\t//void invalidate();\n\n\tvoid paintUsingRaster(\n\t\tUi::GL::Image &image,\n\t\tQRect rect,\n\t\tFn<void(Painter&&)> method,\n\t\tint bufferOffset,\n\t\tbool transparent = false);\n\n\tvoid validateControlsFade();\n\tvoid validateControls();\n\tvoid invalidateControls();\n\tvoid toggleBlending(bool enabled);\n\n\t[[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const;\n\t[[nodiscard]] Ui::GL::Rect transformRect(const QRectF &raster) const;\n\t[[nodiscard]] Ui::GL::Rect transformRect(\n\t\tconst Ui::GL::Rect &raster) const;\n\t[[nodiscard]] Ui::GL::Rect scaleRect(\n\t\tconst Ui::GL::Rect &unscaled,\n\t\tfloat64 scale) const;\n\n\tvoid uploadTexture(\n\t\tGLint internalformat,\n\t\tGLint format,\n\t\tQSize size,\n\t\tQSize hasSize,\n\t\tint stride,\n\t\tconst void *data) const;\n\n\tconst not_null<OverlayWidget*> _owner;\n\n\tQOpenGLFunctions *_f = nullptr;\n\tQSize _viewport;\n\tfloat _factor = 1.;\n\tint _ifactor = 1;\n\tQVector2D _uniformViewport;\n\n\tstd::optional<QOpenGLBuffer> _contentBuffer;\n\tstd::optional<QOpenGLShaderProgram> _imageProgram;\n\tstd::optional<QOpenGLShaderProgram> _staticContentProgram;\n\tQOpenGLShader *_texturedVertexShader = nullptr;\n\tstd::optional<QOpenGLShaderProgram> _withTransparencyProgram;\n\tstd::optional<QOpenGLShaderProgram> _yuv420Program;\n\tstd::optional<QOpenGLShaderProgram> _nv12Program;\n\tstd::optional<QOpenGLShaderProgram> _fillProgram;\n\tstd::optional<QOpenGLShaderProgram> _controlsProgram;\n\tstd::optional<QOpenGLShaderProgram> _roundedCornersProgram;\n\tUi::GL::Textures<6> _textures; // image, sibling, right sibling, y, u, v\n\tQSize _rgbaSize[3];\n\tQSize _lumaSize;\n\tQSize _chromaSize;\n\tqint64 _cacheKeys[3] = { 0 }; // image, sibling, right sibling\n\tint _trackFrameIndex = 0;\n\tint _streamedIndex = 0;\n\tbool _chromaNV12 = false;\n\n\tUi::GL::Image _controlsFadeImage;\n\tUi::GL::Image _radialImage;\n\tUi::GL::Image _documentBubbleImage;\n\tUi::GL::Image _themePreviewImage;\n\tUi::GL::Image _saveMsgImage;\n\tUi::GL::Image _chapterImage;\n\tUi::GL::Image _speedBoostImage;\n\tUi::GL::Image _footerImage;\n\tUi::GL::Image _captionImage;\n\tUi::GL::Image _groupThumbsImage;\n\tUi::GL::Image _controlsImage;\n\n\tstatic constexpr auto kStoriesSiblingPartsCount = 4;\n\tUi::GL::Image _storiesSiblingParts[kStoriesSiblingPartsCount];\n\n\tstatic constexpr auto kControlsCount = 8;\n\t[[nodiscard]] Control controlMeta(Over control) const;\n\n\t// Last one is for the over circle image.\n\tstd::array<QRect, kControlsCount + 1> _controlsTextures;\n\n\tbool _shadowTopFlip = false;\n\tbool _shadowsForStories = false;\n\tbool _blendingEnabled = false;\n\n\trpl::lifetime _storiesLifetime;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/view/media_view_overlay_raster.h\"\n\n#include \"ui/painter.h\"\n#include \"media/stories/media_stories_view.h\"\n#include \"media/view/media_view_pip.h\"\n#include \"media/view/media_view_video_stream.h\"\n#include \"platform/platform_overlay_widget.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Media::View {\nnamespace {\n\n[[nodiscard]] QRectF StoryCropRect(QSizeF imageSize, QSizeF targetSize) {\n\tif (imageSize.isEmpty() || targetSize.isEmpty()) {\n\t\treturn QRectF();\n\t}\n\tconst auto targetAspect = targetSize.width() / targetSize.height();\n\tconst auto imageAspect = imageSize.width() / imageSize.height();\n\tif (imageAspect > targetAspect) {\n\t\tconst auto cropW = imageSize.height() * targetAspect;\n\t\treturn QRectF(\n\t\t\t(imageSize.width() - cropW) / 2.,\n\t\t\t0.,\n\t\t\tcropW,\n\t\t\timageSize.height());\n\t} else if (imageAspect < targetAspect) {\n\t\tconst auto cropH = imageSize.width() / targetAspect;\n\t\treturn QRectF(\n\t\t\t0.,\n\t\t\t(imageSize.height() - cropH) / 2.,\n\t\t\timageSize.width(),\n\t\t\tcropH);\n\t}\n\treturn QRectF();\n}\n\n} // namespace\n\nOverlayWidget::RendererSW::RendererSW(not_null<OverlayWidget*> owner)\n: _owner(owner)\n, _transparentBrush(style::TransparentPlaceholder()) {\n}\n\nbool OverlayWidget::RendererSW::handleHideWorkaround() {\n\t// This is needed on Windows or Linux,\n\t// because on reopen it blinks with the last shown content.\n\treturn _owner->_hideWorkaround != nullptr;\n}\n\nvoid OverlayWidget::RendererSW::paintFallback(\n\t\tPainter &p,\n\t\tconst QRegion &clip,\n\t\tUi::GL::Backend backend) {\n\tif (handleHideWorkaround()) {\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tp.fillRect(clip.boundingRect(), Qt::transparent);\n\t\treturn;\n\t}\n\tif (const auto stream = _owner->_videoStream.get()) {\n\t\tstream->ensureBorrowedRenderer();\n\t}\n\t_p = &p;\n\t_clip = &clip;\n\t_clipOuter = clip.boundingRect();\n\t_owner->paint(this);\n\t_p = nullptr;\n\t_clip = nullptr;\n}\n\nvoid OverlayWidget::RendererSW::paintBackground() {\n\tconst auto region = _owner->opaqueContentShown()\n\t\t? (*_clip - _owner->finalContentRect())\n\t\t: *_clip;\n\n\tconst auto m = _p->compositionMode();\n\t_p->setCompositionMode(QPainter::CompositionMode_Source);\n\tconst auto &bg = _owner->_fullScreenVideo\n\t\t? st::mediaviewVideoBg\n\t\t: st::mediaviewBg;\n\tfor (const auto &rect : region) {\n\t\t_p->fillRect(rect, bg);\n\t}\n\tif (const auto notch = _owner->topNotchSkip()) {\n\t\tconst auto top = QRect(0, 0, _owner->width(), notch);\n\t\tif (const auto black = top.intersected(_clipOuter); !black.isEmpty()) {\n\t\t\t_p->fillRect(black, Qt::black);\n\t\t}\n\t}\n\t_p->setCompositionMode(m);\n}\n\nvoid OverlayWidget::RendererSW::paintVideoStream() {\n\t_owner->_videoStream->borrowedPaint(*_p, *_clip);\n}\n\nQRect OverlayWidget::RendererSW::TransformRect(\n\t\tQRectF geometry,\n\t\tint rotation) {\n\tconst auto center = geometry.center();\n\tconst auto rect = ((rotation % 180) == 90)\n\t\t? QRectF(\n\t\t\tcenter.x() - geometry.height() / 2.,\n\t\t\tcenter.y() - geometry.width() / 2.,\n\t\t\tgeometry.height(),\n\t\t\tgeometry.width())\n\t\t: geometry;\n\treturn QRect(\n\t\tint(rect.x()),\n\t\tint(rect.y()),\n\t\tint(rect.width()),\n\t\tint(rect.height()));\n}\n\nvoid OverlayWidget::RendererSW::paintTransformedVideoFrame(\n\t\tContentGeometry geometry) {\n\tExpects(_owner->_streamed != nullptr);\n\n\tconst auto rotation = int(geometry.rotation);\n\tconst auto rect = TransformRect(geometry.rect, rotation);\n\tif (!rect.intersects(_clipOuter)) {\n\t\treturn;\n\t}\n\tconst auto image = _owner->videoFrame();\n\tconst auto sourceRect = _owner->_stories\n\t\t? StoryCropRect(QSizeF(image.size()), geometry.rect.size())\n\t\t: QRectF();\n\tpaintTransformedImage(image, rect, rotation, sourceRect);\n\tpaintControlsFade(rect, geometry);\n}\n\nvoid OverlayWidget::RendererSW::paintTransformedStaticContent(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry,\n\t\tbool semiTransparent,\n\t\tbool fillTransparentBackground,\n\t\tint index) {\n\tconst auto rotation = int(geometry.rotation);\n\tconst auto rect = TransformRect(geometry.rect, rotation);\n\tif (!rect.intersects(_clipOuter)) {\n\t\treturn;\n\t}\n\n\tif (fillTransparentBackground) {\n\t\t_p->fillRect(rect, _transparentBrush);\n\t}\n\tif (!image.isNull()) {\n\t\tconst auto sourceRect = _owner->_stories\n\t\t\t? StoryCropRect(QSizeF(image.size()), geometry.rect.size())\n\t\t\t: QRectF();\n\t\tpaintTransformedImage(image, rect, rotation, sourceRect);\n\t}\n\tpaintControlsFade(rect, geometry);\n}\n\nvoid OverlayWidget::RendererSW::paintControlsFade(\n\t\tQRect content,\n\t\tconst ContentGeometry &geometry) {\n\tauto opacity = geometry.controlsOpacity;\n\tif (geometry.fade > 0.) {\n\t\t_p->setOpacity(geometry.fade);\n\t\t_p->fillRect(content, Qt::black);\n\t\topacity *= 1. - geometry.fade;\n\t}\n\n\t_p->setOpacity(opacity);\n\t_p->setClipRect(content);\n\tconst auto width = _owner->width();\n\tconst auto stories = (_owner->_stories != nullptr);\n\tif (!stories || geometry.topShadowShown) {\n\t\tconst auto flip = !stories && !_owner->topShadowOnTheRight();\n\t\tconst auto &top = stories\n\t\t\t? st::storiesShadowTop\n\t\t\t: st::mediaviewShadowTop;\n\t\tconst auto topShadow = stories\n\t\t\t? QRect(\n\t\t\t\tcontent.topLeft(),\n\t\t\t\tQSize(content.width(), top.height()))\n\t\t\t: QRect(\n\t\t\t\tQPoint(flip ? 0 : (width - top.width()), 0),\n\t\t\t\ttop.size());\n\t\tif (topShadow.intersected(content).intersects(_clipOuter)) {\n\t\t\tif (stories) {\n\t\t\t\ttop.fill(*_p, topShadow);\n\t\t\t} else if (flip) {\n\t\t\t\tif (_topShadowCache.isNull()\n\t\t\t\t\t|| _topShadowColor != st::windowShadowFg->c) {\n\t\t\t\t\t_topShadowColor = st::windowShadowFg->c;\n\t\t\t\t\t_topShadowCache = top.instance(\n\t\t\t\t\t\t_topShadowColor).mirrored(true, false);\n\t\t\t\t}\n\t\t\t\t_p->drawImage(0, 0, _topShadowCache);\n\t\t\t} else {\n\t\t\t\ttop.paint(*_p, topShadow.topLeft(), width);\n\t\t\t}\n\t\t}\n\t}\n\tconst auto &bottom = stories\n\t\t? st::storiesShadowBottom\n\t\t: st::mediaviewShadowBottom;\n\tconst auto bottomStart = _owner->height() - geometry.bottomShadowSkip;\n\tconst auto bottomShadow = QRect(\n\t\tQPoint(0, bottomStart - bottom.height()),\n\t\tQSize(width, bottom.height()));\n\tif (bottomShadow.intersected(content).intersects(_clipOuter)) {\n\t\tbottom.fill(*_p, bottomShadow);\n\t}\n\t_p->setClipping(false);\n\t_p->setOpacity(1.);\n\tif (bottomStart < content.y() + content.height()) {\n\t\t_p->fillRect(\n\t\t\tcontent.x(),\n\t\t\tbottomStart,\n\t\t\tcontent.width(),\n\t\t\tcontent.y() + content.height() - bottomStart,\n\t\t\tQColor(0, 0, 0, 88));\n\t}\n}\n\nvoid OverlayWidget::RendererSW::paintTransformedImage(\n\t\tconst QImage &image,\n\t\tQRect rect,\n\t\tint rotation,\n\t\tconst QRectF &sourceRect) {\n\tPainterHighQualityEnabler hq(*_p);\n\tif (UsePainterRotation(rotation)) {\n\t\tif (rotation) {\n\t\t\t_p->save();\n\t\t\t_p->rotate(rotation);\n\t\t}\n\t\tif (sourceRect.isValid()) {\n\t\t\t_p->drawImage(QRectF(RotatedRect(rect, rotation)), image, sourceRect);\n\t\t} else {\n\t\t\t_p->drawImage(RotatedRect(rect, rotation), image);\n\t\t}\n\t\tif (rotation) {\n\t\t\t_p->restore();\n\t\t}\n\t} else {\n\t\t_p->drawImage(rect, _owner->transformShownContent(image, rotation));\n\t}\n}\n\nvoid OverlayWidget::RendererSW::paintRadialLoading(\n\t\tQRect inner,\n\t\tbool radial,\n\t\tfloat64 radialOpacity) {\n\t_owner->paintRadialLoadingContent(*_p, inner, radial, radialOpacity);\n}\n\nvoid OverlayWidget::RendererSW::paintThemePreview(QRect outer) {\n\t_owner->paintThemePreviewContent(*_p, outer, _clipOuter);\n}\n\nvoid OverlayWidget::RendererSW::paintDocumentBubble(\n\t\tQRect outer,\n\t\tQRect icon) {\n\tif (outer.intersects(_clipOuter)) {\n\t\t_owner->paintDocumentBubbleContent(*_p, outer, icon, _clipOuter);\n\t\tif (icon.intersects(_clipOuter)) {\n\t\t\t_owner->paintRadialLoading(this);\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::RendererSW::paintSaveMsg(QRect outer) {\n\tif (outer.intersects(_clipOuter)) {\n\t\t_owner->paintSaveMsgContent(*_p, outer, _clipOuter);\n\t}\n}\n\nvoid OverlayWidget::RendererSW::paintChapter(QRect outer) {\n\tif (outer.intersects(_clipOuter)) {\n\t\t_owner->paintChapterContent(*_p, outer, _clipOuter);\n\t}\n}\n\nvoid OverlayWidget::RendererSW::paintSpeedBoost(QRect outer) {\n\tif (outer.intersects(_clipOuter)) {\n\t\t_owner->paintSpeedBoostContent(*_p, outer, _clipOuter);\n\t}\n}\n\nvoid OverlayWidget::RendererSW::paintControlsStart() {\n}\n\nvoid OverlayWidget::RendererSW::paintControl(\n\t\tOver control,\n\t\tQRect over,\n\t\tfloat64 overOpacity,\n\t\tQRect inner,\n\t\tfloat64 innerOpacity,\n\t\tconst style::icon &icon) {\n\tif (!over.isEmpty() && !over.intersects(_clipOuter)) {\n\t\treturn;\n\t}\n\tif (!over.isEmpty() && overOpacity > 0) {\n\t\tif (_overControlImage.isNull()) {\n\t\t\tvalidateOverControlImage();\n\t\t}\n\t\t_p->setOpacity(overOpacity);\n\t\t_p->drawImage(over.topLeft(), _overControlImage);\n\t}\n\tif (inner.intersects(_clipOuter)) {\n\t\t_p->setOpacity(innerOpacity);\n\t\ticon.paintInCenter(*_p, inner);\n\t}\n}\n\nvoid OverlayWidget::RendererSW::paintFooter(QRect outer, float64 opacity) {\n\tif (outer.intersects(_clipOuter)) {\n\t\t_owner->paintFooterContent(*_p, outer, _clipOuter, opacity);\n\t}\n}\n\nvoid OverlayWidget::RendererSW::paintCaption(QRect outer, float64 opacity) {\n\tif (outer.intersects(_clipOuter)) {\n\t\t_owner->paintCaptionContent(*_p, outer, _clipOuter, opacity);\n\t}\n}\n\nvoid OverlayWidget::RendererSW::paintGroupThumbs(\n\t\tQRect outer,\n\t\tfloat64 opacity) {\n\tif (outer.intersects(_clipOuter)) {\n\t\t_owner->paintGroupThumbsContent(*_p, outer, _clipOuter, opacity);\n\t}\n}\n\nvoid OverlayWidget::RendererSW::paintRoundedCorners(int radius) {\n\t// The RpWindow rounding overlay will do the job.\n}\n\nvoid OverlayWidget::RendererSW::paintStoriesSiblingPart(\n\t\tint index,\n\t\tconst QImage &image,\n\t\tQRect rect,\n\t\tfloat64 opacity) {\n\tconst auto changeOpacity = (opacity != 1.);\n\tif (changeOpacity) {\n\t\t_p->setOpacity(opacity);\n\t}\n\t_p->drawImage(rect, image);\n\tif (changeOpacity) {\n\t\t_p->setOpacity(1.);\n\t}\n}\n\nvoid OverlayWidget::RendererSW::validateOverControlImage() {\n\tconst auto size = QSize(st::mediaviewIconOver, st::mediaviewIconOver);\n\tconst auto alpha = base::SafeRound(kOverBackgroundOpacity * 255);\n\t_overControlImage = QImage(\n\t\tsize * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_overControlImage.setDevicePixelRatio(style::DevicePixelRatio());\n\t_overControlImage.fill(Qt::transparent);\n\n\tPainter p(&_overControlImage);\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tauto color = OverBackgroundColor();\n\tcolor.setAlpha(alpha);\n\tp.setBrush(color);\n\tp.drawEllipse(QRect(QPoint(), size));\n}\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_overlay_raster.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/view/media_view_overlay_renderer.h\"\n\nnamespace Media::View {\n\nclass OverlayWidget::RendererSW final : public OverlayWidget::Renderer {\npublic:\n\texplicit RendererSW(not_null<OverlayWidget*> owner);\n\n\tvoid paintFallback(\n\t\tPainter &p,\n\t\tconst QRegion &clip,\n\t\tUi::GL::Backend backend) override;\n\nprivate:\n\tvoid paintBackground() override;\n\tvoid paintVideoStream() override;\n\tvoid paintTransformedVideoFrame(ContentGeometry geometry) override;\n\tvoid paintTransformedStaticContent(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry,\n\t\tbool semiTransparent,\n\t\tbool fillTransparentBackground,\n\t\tint index = 0) override;\n\tvoid paintTransformedImage(\n\t\tconst QImage &image,\n\t\tQRect rect,\n\t\tint rotation,\n\t\tconst QRectF &sourceRect = QRectF());\n\tvoid paintControlsFade(QRect content, const ContentGeometry &geometry);\n\tvoid paintRadialLoading(\n\t\tQRect inner,\n\t\tbool radial,\n\t\tfloat64 radialOpacity) override;\n\tvoid paintThemePreview(QRect outer) override;\n\tvoid paintDocumentBubble(QRect outer, QRect icon) override;\n\tvoid paintSaveMsg(QRect outer) override;\n\tvoid paintChapter(QRect outer) override;\n\tvoid paintSpeedBoost(QRect outer) override;\n\tvoid paintControlsStart() override;\n\tvoid paintControl(\n\t\tOver control,\n\t\tQRect over,\n\t\tfloat64 overOpacity,\n\t\tQRect inner,\n\t\tfloat64 innerOpacity,\n\t\tconst style::icon &icon) override;\n\tvoid paintFooter(QRect outer, float64 opacity) override;\n\tvoid paintCaption(QRect outer, float64 opacity) override;\n\tvoid paintGroupThumbs(QRect outer, float64 opacity) override;\n\tvoid paintRoundedCorners(int radius) override;\n\tvoid paintStoriesSiblingPart(\n\t\tint index,\n\t\tconst QImage &image,\n\t\tQRect rect,\n\t\tfloat64 opacity = 1.) override;\n\n\tbool handleHideWorkaround();\n\tvoid validateOverControlImage();\n\n\t[[nodiscard]] static QRect TransformRect(QRectF geometry, int rotation);\n\n\tconst not_null<OverlayWidget*> _owner;\n\tQBrush _transparentBrush;\n\n\tPainter *_p = nullptr;\n\tconst QRegion *_clip = nullptr;\n\tQRect _clipOuter;\n\n\tQImage _overControlImage;\n\n\tQImage _topShadowCache;\n\tQColor _topShadowColor;\n\n};\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_overlay_renderer.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/view/media_view_overlay_widget.h\"\n\nnamespace Media::Stories {\nstruct SiblingView;\n} // namespace Media::Stories\n\nnamespace Media::View {\n\nclass OverlayWidget::Renderer : public Ui::GL::Renderer {\npublic:\n\tvirtual void paintBackground() = 0;\n\tvirtual void paintVideoStream() = 0;\n\tvirtual void paintTransformedVideoFrame(ContentGeometry geometry) = 0;\n\tvirtual void paintTransformedStaticContent(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry,\n\t\tbool semiTransparent,\n\t\tbool fillTransparentBackground,\n\t\tint index = 0) = 0; // image, left sibling, right sibling\n\tvirtual void paintRadialLoading(\n\t\tQRect inner,\n\t\tbool radial,\n\t\tfloat64 radialOpacity) = 0;\n\tvirtual void paintThemePreview(QRect outer) = 0;\n\tvirtual void paintDocumentBubble(QRect outer, QRect icon) = 0;\n\tvirtual void paintSaveMsg(QRect outer) = 0;\n\tvirtual void paintChapter(QRect outer) = 0;\n\tvirtual void paintSpeedBoost(QRect outer) = 0;\n\tvirtual void paintControlsStart() = 0;\n\tvirtual void paintControl(\n\t\tOver control,\n\t\tQRect over,\n\t\tfloat64 overOpacity,\n\t\tQRect inner,\n\t\tfloat64 innerOpacity,\n\t\tconst style::icon &icon) = 0;\n\tvirtual void paintFooter(QRect outer, float64 opacity) = 0;\n\tvirtual void paintCaption(QRect outer, float64 opacity) = 0;\n\tvirtual void paintGroupThumbs(QRect outer, float64 opacity) = 0;\n\tvirtual void paintRoundedCorners(int radius) = 0;\n\tvirtual void paintStoriesSiblingPart(\n\t\tint index,\n\t\tconst QImage &image,\n\t\tQRect rect,\n\t\tfloat64 opacity = 1.) = 0;\n};\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_overlay_rhi.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/view/media_view_overlay_rhi.h\"\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\n#include \"ui/rhi/rhi_shader.h\"\n#include \"ui/painter.h\"\n#include \"data/data_peer_values.h\"\n#include \"media/streaming/media_streaming_common.h\"\n#include \"media/view/media_view_video_stream.h\"\n#include \"platform/platform_overlay_widget.h\"\n#include \"base/debug_log.h\"\n#include \"styles/style_media_view.h\"\n\n#include <rhi/qrhi.h>\n\nnamespace Media::View {\nnamespace {\n\nusing namespace Ui::GL;\n\nstruct ImageUniforms {\n\tfloat viewport[2];\n\tfloat g_opacity;\n\tfloat _pad0;\n};\nstatic_assert(sizeof(ImageUniforms) % 16 == 0);\n\nstruct ContentUniforms {\n\tfloat viewport[2];\n\tfloat _pad0[2];\n\tfloat shadowTopRect[4];\n\tfloat shadowBottomSkipOpacityFullFade[4];\n\tfloat roundRect[4];\n\tfloat roundRadius;\n\tfloat _pad1[3];\n};\nstatic_assert(sizeof(ContentUniforms) == 80);\n\nstruct TransparentContentUniforms {\n\tfloat viewport[2];\n\tfloat _pad0[2];\n\tfloat shadowTopRect[4];\n\tfloat shadowBottomSkipOpacityFullFade[4];\n\tfloat transparentBg[4];\n\tfloat transparentFg[4];\n\tfloat transparentSize;\n\tfloat _pad1[3];\n};\nstatic_assert(sizeof(TransparentContentUniforms) == 96);\n\nstruct RoundedCornersUniforms {\n\tfloat viewport[2];\n\tfloat _pad0[2];\n\tfloat roundRect[4];\n\tfloat roundRadius;\n\tfloat _pad1[3];\n};\nstatic_assert(sizeof(RoundedCornersUniforms) == 48);\n\n[[nodiscard]] QRectF StoryCropTextureRect(\n\t\tQSizeF imageSize,\n\t\tQSizeF targetSize) {\n\tif (imageSize.isEmpty() || targetSize.isEmpty()) {\n\t\treturn QRectF(0., 0., 1., 1.);\n\t}\n\tconst auto targetAspect = targetSize.width() / targetSize.height();\n\tconst auto imageAspect = imageSize.width() / imageSize.height();\n\tif (imageAspect > targetAspect) {\n\t\tconst auto cropW = imageSize.height() * targetAspect;\n\t\tconst auto offset = (imageSize.width() - cropW) / 2.;\n\t\treturn QRectF(\n\t\t\toffset / imageSize.width(),\n\t\t\t0.,\n\t\t\tcropW / imageSize.width(),\n\t\t\t1.);\n\t} else if (imageAspect < targetAspect) {\n\t\tconst auto cropH = imageSize.width() / targetAspect;\n\t\tconst auto offset = (imageSize.height() - cropH) / 2.;\n\t\treturn QRectF(\n\t\t\t0.,\n\t\t\toffset / imageSize.height(),\n\t\t\t1.,\n\t\t\tcropH / imageSize.height());\n\t}\n\treturn QRectF(0., 0., 1., 1.);\n}\n\n[[nodiscard]] QShader LoadShader(const QString &name) {\n\treturn Ui::Rhi::ShaderFromFile(\n\t\tu\":/shaders/\"_q + name + u\".qsb\"_q);\n}\n\n} // namespace\n\nOverlayWidget::RendererRhi::~RendererRhi() {\n\treleaseResources();\n}\n\nOverlayWidget::RendererRhi::RendererRhi(not_null<OverlayWidget*> owner)\n: _owner(owner) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tranges::fill(_cacheKeys, quint64(0));\n\t\tinvalidateControls();\n\t}, _lifetime);\n\n\tcrl::on_main(this, [=] {\n\t\t_owner->_storiesChanged.events(\n\t\t) | rpl::on_next([=] {\n\t\t\tif (_owner->_storiesSession) {\n\t\t\t\tData::AmPremiumValue(\n\t\t\t\t\t_owner->_storiesSession\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\tranges::fill(_cacheKeys, quint64(0));\n\t\t\t\t\tinvalidateControls();\n\t\t\t\t}, _storiesLifetime);\n\t\t\t} else {\n\t\t\t\t_storiesLifetime.destroy();\n\t\t\t\tranges::fill(_cacheKeys, quint64(0));\n\t\t\t\tinvalidateControls();\n\t\t\t}\n\t\t}, _lifetime);\n\t});\n}\n\nvoid OverlayWidget::RendererRhi::initialize(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\tif (_initialized && _rhi == rhi) {\n\t\treturn;\n\t}\n\treleaseResources();\n\n\t_rhi = rhi;\n\t_rt = rt;\n\t_cb = cb;\n\n\t_vertexBuffer = rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::VertexBuffer,\n\t\tkMaxDraws * kVertexSize);\n\t_vertexBuffer->create();\n\n\t_fillVertexBuffer = rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::VertexBuffer,\n\t\t4 * 4 * 2 * sizeof(float));\n\t_fillVertexBuffer->create();\n\n\t_uniformBuffer = rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::UniformBuffer,\n\t\tkMaxDraws * 256);\n\t_uniformBuffer->create();\n\n\t_sampler = rhi->newSampler(\n\t\tQRhiSampler::Linear,\n\t\tQRhiSampler::Linear,\n\t\tQRhiSampler::None,\n\t\tQRhiSampler::ClampToEdge,\n\t\tQRhiSampler::ClampToEdge);\n\t_sampler->create();\n\n\t_placeholderTexture = rhi->newTexture(QRhiTexture::RGBA8, QSize(1, 1));\n\t_placeholderTexture->create();\n\n\tcreatePipelines();\n\t_initialized = true;\n\n\tLOG((\"[RENDERER_TEST] component=overlay backend=%1 device=%2 status=OK\")\n\t\t.arg(rhi->backendName())\n\t\t.arg(rhi->driverInfo().deviceName));\n}\n\nvoid OverlayWidget::RendererRhi::createPipelines() {\n\tconst auto rpDesc = _rt->renderPassDescriptor();\n\n\tconst auto argb32Vert = LoadShader(u\"argb32.vert\"_q);\n\tconst auto argb32Frag = LoadShader(u\"argb32.frag\"_q);\n\tconst auto controlsFrag = LoadShader(u\"controls.frag\"_q);\n\n\tauto *sampleSrb = _rhi->newShaderResourceBindings();\n\tsampleSrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBufferWithDynamicOffset(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer,\n\t\t\tsizeof(ImageUniforms)),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t});\n\tsampleSrb->create();\n\t_perDrawSrbs.push_back(sampleSrb);\n\n\tauto *imagePipeline = _rhi->newGraphicsPipeline();\n\timagePipeline->setShaderStages({\n\t\t{ QRhiShaderStage::Vertex, argb32Vert },\n\t\t{ QRhiShaderStage::Fragment, argb32Frag },\n\t});\n\tQRhiVertexInputLayout inputLayout;\n\tinputLayout.setBindings({ { 4 * sizeof(float) } });\n\tinputLayout.setAttributes({\n\t\t{ 0, 0, QRhiVertexInputAttribute::Float2, 0 },\n\t\t{ 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) },\n\t});\n\timagePipeline->setVertexInputLayout(inputLayout);\n\timagePipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);\n\timagePipeline->setShaderResourceBindings(sampleSrb);\n\timagePipeline->setRenderPassDescriptor(rpDesc);\n\timagePipeline->create();\n\t_imagePipeline = imagePipeline;\n\n\tauto *imageBlendPipeline = _rhi->newGraphicsPipeline();\n\timageBlendPipeline->setShaderStages({\n\t\t{ QRhiShaderStage::Vertex, argb32Vert },\n\t\t{ QRhiShaderStage::Fragment, controlsFrag },\n\t});\n\timageBlendPipeline->setVertexInputLayout(inputLayout);\n\tQRhiGraphicsPipeline::TargetBlend blend;\n\tblend.enable = true;\n\tblend.srcColor = QRhiGraphicsPipeline::One;\n\tblend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;\n\tblend.srcAlpha = QRhiGraphicsPipeline::One;\n\tblend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;\n\timageBlendPipeline->setTargetBlends({ blend });\n\timageBlendPipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);\n\timageBlendPipeline->setShaderResourceBindings(sampleSrb);\n\timageBlendPipeline->setRenderPassDescriptor(rpDesc);\n\timageBlendPipeline->create();\n\t_imageBlendPipeline = imageBlendPipeline;\n\n\tconst auto staticContentFrag = LoadShader(u\"static_content.frag\"_q);\n\tconst auto transparentContentFrag = LoadShader(\n\t\tu\"transparent_content.frag\"_q);\n\n\tauto *contentSrb = _rhi->newShaderResourceBindings();\n\tcontentSrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer,\n\t\t\t0,\n\t\t\tsizeof(ContentUniforms)),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t2,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t});\n\tcontentSrb->create();\n\t_perDrawSrbs.push_back(contentSrb);\n\n\tauto *staticPipeline = _rhi->newGraphicsPipeline();\n\tstaticPipeline->setShaderStages({\n\t\t{ QRhiShaderStage::Vertex, argb32Vert },\n\t\t{ QRhiShaderStage::Fragment, staticContentFrag },\n\t});\n\tstaticPipeline->setVertexInputLayout(inputLayout);\n\tstaticPipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);\n\tstaticPipeline->setShaderResourceBindings(contentSrb);\n\tstaticPipeline->setRenderPassDescriptor(rpDesc);\n\tstaticPipeline->create();\n\t_staticContentPipeline = staticPipeline;\n\n\tauto *staticBlendPipeline = _rhi->newGraphicsPipeline();\n\tstaticBlendPipeline->setShaderStages({\n\t\t{ QRhiShaderStage::Vertex, argb32Vert },\n\t\t{ QRhiShaderStage::Fragment, staticContentFrag },\n\t});\n\tstaticBlendPipeline->setVertexInputLayout(inputLayout);\n\tstaticBlendPipeline->setTargetBlends({ blend });\n\tstaticBlendPipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);\n\tstaticBlendPipeline->setShaderResourceBindings(contentSrb);\n\tstaticBlendPipeline->setRenderPassDescriptor(rpDesc);\n\tstaticBlendPipeline->create();\n\t_staticContentBlendPipeline = staticBlendPipeline;\n\n\tauto *transparentSrb = _rhi->newShaderResourceBindings();\n\ttransparentSrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer,\n\t\t\t0,\n\t\t\tsizeof(TransparentContentUniforms)),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t2,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t});\n\ttransparentSrb->create();\n\t_perDrawSrbs.push_back(transparentSrb);\n\n\tauto *transparentPipeline = _rhi->newGraphicsPipeline();\n\ttransparentPipeline->setShaderStages({\n\t\t{ QRhiShaderStage::Vertex, argb32Vert },\n\t\t{ QRhiShaderStage::Fragment, transparentContentFrag },\n\t});\n\ttransparentPipeline->setVertexInputLayout(inputLayout);\n\ttransparentPipeline->setTargetBlends({ blend });\n\ttransparentPipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);\n\ttransparentPipeline->setShaderResourceBindings(transparentSrb);\n\ttransparentPipeline->setRenderPassDescriptor(rpDesc);\n\ttransparentPipeline->create();\n\t_transparentContentPipeline = transparentPipeline;\n\n\tconst auto fillVert = LoadShader(u\"fill.vert\"_q);\n\tconst auto roundedCornersFrag = LoadShader(u\"rounded_corners.frag\"_q);\n\n\tauto *roundedSrb = _rhi->newShaderResourceBindings();\n\troundedSrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer,\n\t\t\t0,\n\t\t\tsizeof(RoundedCornersUniforms)),\n\t});\n\troundedSrb->create();\n\t_perDrawSrbs.push_back(roundedSrb);\n\n\tQRhiVertexInputLayout fillInputLayout;\n\tfillInputLayout.setBindings({ { 2 * sizeof(float) } });\n\tfillInputLayout.setAttributes({\n\t\t{ 0, 0, QRhiVertexInputAttribute::Float2, 0 },\n\t});\n\n\tQRhiGraphicsPipeline::TargetBlend cornerBlend;\n\tcornerBlend.enable = true;\n\tcornerBlend.srcColor = QRhiGraphicsPipeline::Zero;\n\tcornerBlend.dstColor = QRhiGraphicsPipeline::SrcAlpha;\n\tcornerBlend.srcAlpha = QRhiGraphicsPipeline::Zero;\n\tcornerBlend.dstAlpha = QRhiGraphicsPipeline::SrcAlpha;\n\n\tauto *roundedPipeline = _rhi->newGraphicsPipeline();\n\troundedPipeline->setShaderStages({\n\t\t{ QRhiShaderStage::Vertex, fillVert },\n\t\t{ QRhiShaderStage::Fragment, roundedCornersFrag },\n\t});\n\troundedPipeline->setVertexInputLayout(fillInputLayout);\n\troundedPipeline->setTargetBlends({ cornerBlend });\n\troundedPipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);\n\troundedPipeline->setShaderResourceBindings(roundedSrb);\n\troundedPipeline->setRenderPassDescriptor(rpDesc);\n\troundedPipeline->create();\n\t_roundedCornersPipeline = roundedPipeline;\n\n\tconst auto yuv420Frag = LoadShader(u\"yuv420_content.frag\"_q);\n\tconst auto nv12Frag = LoadShader(u\"nv12_content.frag\"_q);\n\n\tauto *yuv420Srb = _rhi->newShaderResourceBindings();\n\tyuv420Srb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer,\n\t\t\t0,\n\t\t\tsizeof(ContentUniforms)),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t2,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t3,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t4,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t});\n\tyuv420Srb->create();\n\t_perDrawSrbs.push_back(yuv420Srb);\n\n\tauto createYuvPipeline = [&](const QShader &frag, bool blending,\n\t\t\tQRhiShaderResourceBindings *srb) {\n\t\tauto *pipeline = _rhi->newGraphicsPipeline();\n\t\tpipeline->setShaderStages({\n\t\t\t{ QRhiShaderStage::Vertex, argb32Vert },\n\t\t\t{ QRhiShaderStage::Fragment, frag },\n\t\t});\n\t\tpipeline->setVertexInputLayout(inputLayout);\n\t\tif (blending) {\n\t\t\tpipeline->setTargetBlends({ blend });\n\t\t}\n\t\tpipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);\n\t\tpipeline->setShaderResourceBindings(srb);\n\t\tpipeline->setRenderPassDescriptor(rpDesc);\n\t\tpipeline->create();\n\t\treturn pipeline;\n\t};\n\n\t_yuv420Pipeline = createYuvPipeline(yuv420Frag, false, yuv420Srb);\n\t_yuv420BlendPipeline = createYuvPipeline(yuv420Frag, true, yuv420Srb);\n\n\tauto *nv12Srb = _rhi->newShaderResourceBindings();\n\tnv12Srb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer,\n\t\t\t0,\n\t\t\tsizeof(ContentUniforms)),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t2,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t3,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t});\n\tnv12Srb->create();\n\t_perDrawSrbs.push_back(nv12Srb);\n\n\t_nv12Pipeline = createYuvPipeline(nv12Frag, false, nv12Srb);\n\t_nv12BlendPipeline = createYuvPipeline(nv12Frag, true, nv12Srb);\n}\n\nvoid OverlayWidget::RendererRhi::render(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\tif (_owner->_hideWorkaround) {\n\t\treturn;\n\t}\n\t_rhi = rhi;\n\t_rt = rt;\n\t_cb = cb;\n\t_nextVertexSlot = 0;\n\t_nextPoolIndex = 0;\n\tfor (auto *srb : _perDrawSrbs) {\n\t\tdelete srb;\n\t}\n\t_perDrawSrbs.clear();\n\t_drawCommands.clear();\n\n\tconst auto size = rt->pixelSize();\n\tconst auto factor = _owner->widget()->devicePixelRatioF();\n\tif (_factor != factor) {\n\t\t_factor = factor;\n\t\t_ifactor = int(std::ceil(factor));\n\t\tranges::fill(_cacheKeys, quint64(0));\n\t\tinvalidateControls();\n\t\tdelete _controlsFadeTexture;\n\t\t_controlsFadeTexture = nullptr;\n\t}\n\t_viewport = QSize(\n\t\tint(size.width() / _factor),\n\t\tint(size.height() / _factor));\n\n\t_rub = rhi->nextResourceUpdateBatch();\n\t_pendingVideoStream = nullptr;\n\n\t_owner->paint(this);\n\n\tif (_pendingVideoStream) {\n\t\t_pendingVideoStream->borrowedPaintOffscreen(_rhi, _rt, _cb);\n\t}\n\n\tif (const auto notch = _owner->topNotchSkip()) {\n\t\tauto blackImage = QImage(1, 1, QImage::Format_ARGB32_Premultiplied);\n\t\tblackImage.fill(Qt::black);\n\t\tauto *blackTex = acquirePoolTexture(QSize(1, 1));\n\t\t_rub->uploadTexture(\n\t\t\tblackTex,\n\t\t\tQRhiTextureUploadDescription(\n\t\t\t\tQRhiTextureUploadEntry(0, 0,\n\t\t\t\t\tQRhiTextureSubresourceUploadDescription(blackImage))));\n\t\tconst auto notchRect = transformRect(\n\t\t\tQRect(0, 0, _owner->width(), notch));\n\t\tconst float notchCoords[] = {\n\t\t\tnotchRect.left(), notchRect.bottom(), 0.f, 0.f,\n\t\t\tnotchRect.right(), notchRect.bottom(), 1.f, 0.f,\n\t\t\tnotchRect.left(), notchRect.top(), 0.f, 1.f,\n\t\t\tnotchRect.right(), notchRect.top(), 1.f, 1.f,\n\t\t};\n\t\tdrawTexturedQuad(_imagePipeline, blackTex, notchCoords);\n\t}\n\n\tcb->beginPass(rt, QColor(0, 0, 0, 0), { 1.0f, 0 }, _rub);\n\t_rub = nullptr;\n\n\tfor (const auto &cmd : _drawCommands) {\n\t\tcb->setGraphicsPipeline(cmd.pipeline);\n\t\tcb->setShaderResources(cmd.srb);\n\t\tcb->setViewport({\n\t\t\t0, 0,\n\t\t\tfloat(rt->pixelSize().width()),\n\t\t\tfloat(rt->pixelSize().height()) });\n\t\tif (cmd.fillVertex) {\n\t\t\tconst QRhiCommandBuffer::VertexInput vbufBinding(\n\t\t\t\t_fillVertexBuffer,\n\t\t\t\tcmd.vertexIndex * 4 * 2 * sizeof(float));\n\t\t\tcb->setVertexInput(0, 1, &vbufBinding);\n\t\t} else {\n\t\t\tconst QRhiCommandBuffer::VertexInput vbufBinding(\n\t\t\t\t_vertexBuffer, cmd.vertexIndex * kVertexSize);\n\t\t\tcb->setVertexInput(0, 1, &vbufBinding);\n\t\t}\n\t\tcb->draw(4);\n\t}\n\t_drawCommands.clear();\n\n\tif (_pendingVideoStream) {\n\t\t_pendingVideoStream->borrowedPaintOnscreen(_rhi, _rt, _cb);\n\t\t_pendingVideoStream = nullptr;\n\t}\n\n\tcb->endPass();\n}\n\nQRhiTexture *OverlayWidget::RendererRhi::acquirePoolTexture(QSize size) {\n\tif (_nextPoolIndex < int(_texturePool.size())) {\n\t\tauto &entry = _texturePool[_nextPoolIndex++];\n\t\tif (entry.size == size) {\n\t\t\treturn entry.texture;\n\t\t}\n\t\tdelete entry.texture;\n\t\tentry.texture = _rhi->newTexture(QRhiTexture::BGRA8, size);\n\t\tentry.texture->create();\n\t\tentry.size = size;\n\t\treturn entry.texture;\n\t}\n\tauto *tex = _rhi->newTexture(QRhiTexture::BGRA8, size);\n\ttex->create();\n\t_texturePool.push_back({ tex, size });\n\t_nextPoolIndex++;\n\treturn tex;\n}\n\nvoid OverlayWidget::RendererRhi::releaseResources() {\n\t_drawCommands.clear();\n\n\tfor (auto &entry : _texturePool) {\n\t\tdelete entry.texture;\n\t}\n\t_texturePool.clear();\n\n\tdelete _imagePipeline;\n\t_imagePipeline = nullptr;\n\tdelete _imageBlendPipeline;\n\t_imageBlendPipeline = nullptr;\n\tdelete _staticContentPipeline;\n\t_staticContentPipeline = nullptr;\n\tdelete _staticContentBlendPipeline;\n\t_staticContentBlendPipeline = nullptr;\n\tdelete _transparentContentPipeline;\n\t_transparentContentPipeline = nullptr;\n\tdelete _roundedCornersPipeline;\n\t_roundedCornersPipeline = nullptr;\n\tdelete _yuv420Pipeline;\n\t_yuv420Pipeline = nullptr;\n\tdelete _yuv420BlendPipeline;\n\t_yuv420BlendPipeline = nullptr;\n\tdelete _nv12Pipeline;\n\t_nv12Pipeline = nullptr;\n\tdelete _nv12BlendPipeline;\n\t_nv12BlendPipeline = nullptr;\n\n\tfor (auto &tex : _storiesSiblingTextures) {\n\t\tdelete tex;\n\t\ttex = nullptr;\n\t}\n\tranges::fill(_storiesSiblingSizes, QSize());\n\tranges::fill(_storiesSiblingCacheKeys, quint64(0));\n\n\tdelete _controlsAtlasTexture;\n\t_controlsAtlasTexture = nullptr;\n\t_controlsAtlasSize = QSize();\n\tranges::fill(_controlsTextures, QRect());\n\n\tdelete _controlsFadeTexture;\n\t_controlsFadeTexture = nullptr;\n\t_controlsFadeSize = QSize();\n\n\tfor (auto *srb : _perDrawSrbs) {\n\t\tdelete srb;\n\t}\n\t_perDrawSrbs.clear();\n\n\tfor (auto &tex : _rgbaTextures) {\n\t\tdelete tex;\n\t\ttex = nullptr;\n\t}\n\tranges::fill(_rgbaSizes, QSize());\n\tranges::fill(_cacheKeys, quint64(0));\n\n\tdelete _yTexture;\n\t_yTexture = nullptr;\n\tdelete _uTexture;\n\t_uTexture = nullptr;\n\tdelete _vTexture;\n\t_vTexture = nullptr;\n\tdelete _uvTexture;\n\t_uvTexture = nullptr;\n\t_lumaSize = QSize();\n\t_chromaSize = QSize();\n\t_trackFrameIndex = 0;\n\t_streamedIndex = 0;\n\n\tdelete _placeholderTexture;\n\t_placeholderTexture = nullptr;\n\n\tdelete _vertexBuffer;\n\t_vertexBuffer = nullptr;\n\tdelete _fillVertexBuffer;\n\t_fillVertexBuffer = nullptr;\n\tdelete _uniformBuffer;\n\t_uniformBuffer = nullptr;\n\tdelete _sampler;\n\t_sampler = nullptr;\n\n\t_initialized = false;\n}\n\nQColor OverlayWidget::RendererRhi::rhiClearColor() {\n\tif (_owner->_hideWorkaround) {\n\t\treturn QColor(0, 0, 0, 0);\n\t} else if (_owner->_fullScreenVideo) {\n\t\treturn st::mediaviewVideoBg->c;\n\t} else {\n\t\treturn st::mediaviewBg->c;\n\t}\n}\n\nstd::optional<QColor> OverlayWidget::RendererRhi::clearColor() {\n\tif (_owner->_hideWorkaround) {\n\t\treturn QColor(0, 0, 0, 0);\n\t} else if (_owner->_fullScreenVideo) {\n\t\treturn st::mediaviewVideoBg->c;\n\t} else {\n\t\treturn st::mediaviewBg->c;\n\t}\n}\n\nvoid OverlayWidget::RendererRhi::drawTexturedQuad(\n\t\tQRhiGraphicsPipeline *pipeline,\n\t\tQRhiTexture *texture,\n\t\tconst float *coords,\n\t\tfloat opacity,\n\t\tbool blend) {\n\tif (_nextVertexSlot >= kMaxDraws || !texture) {\n\t\treturn;\n\t}\n\tconst auto slot = _nextVertexSlot++;\n\tconst auto vOffset = slot * kVertexSize;\n\tconst auto uOffset = slot * 256;\n\n\t_rub->updateDynamicBuffer(\n\t\t_vertexBuffer, vOffset, kVertexSize, coords);\n\n\tImageUniforms uniforms{};\n\tuniforms.viewport[0] = _viewport.width() * _factor;\n\tuniforms.viewport[1] = _viewport.height() * _factor;\n\tuniforms.g_opacity = opacity;\n\t_rub->updateDynamicBuffer(\n\t\t_uniformBuffer, uOffset, sizeof(ImageUniforms), &uniforms);\n\n\tauto *srb = _rhi->newShaderResourceBindings();\n\tsrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer,\n\t\t\tuOffset,\n\t\t\tsizeof(ImageUniforms)),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\ttexture,\n\t\t\t_sampler),\n\t});\n\tsrb->create();\n\t_perDrawSrbs.push_back(srb);\n\n\t_drawCommands.push_back({\n\t\t.pipeline = blend ? _imageBlendPipeline : pipeline,\n\t\t.srb = srb,\n\t\t.vertexIndex = slot,\n\t});\n}\n\nvoid OverlayWidget::RendererRhi::paintUsingRaster(\n\t\tQRect rect,\n\t\tFn<void(Painter&)> method,\n\t\tbool transparent,\n\t\tfloat opacity) {\n\tif (!_imagePipeline || rect.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto size = rect.size() * _ifactor;\n\tauto raster = QImage(size, QImage::Format_ARGB32_Premultiplied);\n\traster.setDevicePixelRatio(_factor);\n\traster.fill(Qt::transparent);\n\t{\n\t\tauto painter = Painter(&raster);\n\t\tmethod(painter);\n\t}\n\n\tauto *tex = acquirePoolTexture(size);\n\t_rub->uploadTexture(\n\t\ttex,\n\t\tQRhiTextureUploadDescription(\n\t\t\tQRhiTextureUploadEntry(0, 0,\n\t\t\t\tQRhiTextureSubresourceUploadDescription(raster))));\n\n\tconst auto rRect = transformRect(rect);\n\tconst float coords[] = {\n\t\trRect.left(), rRect.bottom(),\n\t\t0.f, 0.f,\n\n\t\trRect.right(), rRect.bottom(),\n\t\t1.f, 0.f,\n\n\t\trRect.left(), rRect.top(),\n\t\t0.f, 1.f,\n\n\t\trRect.right(), rRect.top(),\n\t\t1.f, 1.f,\n\t};\n\n\tdrawTexturedQuad(\n\t\t_imagePipeline,\n\t\ttex,\n\t\tcoords,\n\t\topacity,\n\t\ttransparent);\n}\n\nvoid OverlayWidget::RendererRhi::validateControlsFade() {\n\tconst auto forStories = (_owner->_stories != nullptr);\n\tconst auto flip = !forStories && !_owner->topShadowOnTheRight();\n\tif (_controlsFadeTexture\n\t\t&& _shadowTopFlip == flip\n\t\t&& _shadowsForStories == forStories) {\n\t\treturn;\n\t}\n\t_shadowTopFlip = flip;\n\t_shadowsForStories = forStories;\n\tconst auto &top = _shadowsForStories\n\t\t? st::storiesShadowTop\n\t\t: st::mediaviewShadowTop;\n\tconst auto &bottom = _shadowsForStories\n\t\t? st::storiesShadowBottom\n\t\t: st::mediaviewShadowBottom;\n\tconst auto width = top.width();\n\tconst auto bottomTop = top.height();\n\tconst auto height = bottomTop + bottom.height();\n\n\tauto image = QImage(\n\t\tQSize(width, height) * _ifactor,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.fill(Qt::transparent);\n\timage.setDevicePixelRatio(_ifactor);\n\t{\n\t\tauto p = QPainter(&image);\n\t\ttop.paint(p, 0, 0, width);\n\t\tbottom.fill(p, QRect(0, bottomTop, width, bottom.height()));\n\t}\n\tif (flip) {\n\t\timage = std::move(image).mirrored(true, false);\n\t}\n\n\tconst auto size = image.size();\n\tif (!_controlsFadeTexture || _controlsFadeSize != size) {\n\t\tdelete _controlsFadeTexture;\n\t\t_controlsFadeTexture = _rhi->newTexture(\n\t\t\tQRhiTexture::BGRA8,\n\t\t\tsize);\n\t\t_controlsFadeTexture->create();\n\t\t_controlsFadeSize = size;\n\t}\n\t_rub->uploadTexture(\n\t\t_controlsFadeTexture,\n\t\tQRhiTextureUploadDescription(\n\t\t\tQRhiTextureUploadEntry(0, 0,\n\t\t\t\tQRhiTextureSubresourceUploadDescription(image))));\n}\n\nvoid OverlayWidget::RendererRhi::fillShadowUniforms(\n\t\tfloat *shadowTopRect,\n\t\tfloat *shadowBottomSkipOpacityFullFade,\n\t\tContentGeometry geometry) const {\n\tif (_owner->_stories) {\n\t\tconst auto &top = st::storiesShadowTop.size();\n\t\tconst auto shadowTop = geometry.topShadowShown\n\t\t\t? geometry.rect.y()\n\t\t\t: geometry.rect.y() - top.height();\n\t\tconst auto tRect = transformRect(\n\t\t\tQRect(QPoint(geometry.rect.x(), shadowTop), top));\n\t\tshadowTopRect[0] = tRect.x();\n\t\tshadowTopRect[1] = tRect.y();\n\t\tshadowTopRect[2] = tRect.width();\n\t\tshadowTopRect[3] = tRect.height();\n\t} else {\n\t\tconst auto &top = st::mediaviewShadowTop.size();\n\t\tconst auto point = QPoint(\n\t\t\t_shadowTopFlip ? 0 : (_viewport.width() - top.width()),\n\t\t\t0);\n\t\tconst auto tRect = transformRect(QRect(point, top));\n\t\tshadowTopRect[0] = tRect.x();\n\t\tshadowTopRect[1] = tRect.y();\n\t\tshadowTopRect[2] = tRect.width();\n\t\tshadowTopRect[3] = tRect.height();\n\t}\n\tconst auto &bottom = _owner->_stories\n\t\t? st::storiesShadowBottom\n\t\t: st::mediaviewShadowBottom;\n\tshadowBottomSkipOpacityFullFade[0] = bottom.height() * _factor;\n\tshadowBottomSkipOpacityFullFade[1] =\n\t\tgeometry.bottomShadowSkip * _factor;\n\tshadowBottomSkipOpacityFullFade[2] = geometry.controlsOpacity;\n\tshadowBottomSkipOpacityFullFade[3] = 1.f - float(geometry.fade);\n}\n\nvoid OverlayWidget::RendererRhi::drawContentQuad(\n\t\tQRhiTexture *contentTexture,\n\t\tconst float *coords,\n\t\tContentGeometry geometry,\n\t\tbool fillTransparentBackground,\n\t\tbool blend) {\n\tif (_nextVertexSlot >= kMaxDraws || !contentTexture) {\n\t\treturn;\n\t}\n\n\tvalidateControlsFade();\n\n\tconst auto slot = _nextVertexSlot++;\n\tconst auto vOffset = slot * kVertexSize;\n\tconst auto uOffset = slot * 256;\n\n\t_rub->updateDynamicBuffer(\n\t\t_vertexBuffer, vOffset, kVertexSize, coords);\n\n\tconst auto vw = _viewport.width() * _factor;\n\tconst auto vh = _viewport.height() * _factor;\n\n\tauto *pipeline = fillTransparentBackground\n\t\t? _transparentContentPipeline\n\t\t: (blend ? _staticContentBlendPipeline : _staticContentPipeline);\n\n\tif (fillTransparentBackground) {\n\t\tconst auto c_bg = st::mediaviewTransparentBg->c;\n\t\tconst auto c_fg = st::mediaviewTransparentFg->c;\n\t\tTransparentContentUniforms uniforms{};\n\t\tuniforms.viewport[0] = vw;\n\t\tuniforms.viewport[1] = vh;\n\t\tfillShadowUniforms(\n\t\t\tuniforms.shadowTopRect,\n\t\t\tuniforms.shadowBottomSkipOpacityFullFade,\n\t\t\tgeometry);\n\t\tuniforms.transparentBg[0] = float(c_bg.redF());\n\t\tuniforms.transparentBg[1] = float(c_bg.greenF());\n\t\tuniforms.transparentBg[2] = float(c_bg.blueF());\n\t\tuniforms.transparentBg[3] = float(c_bg.alphaF());\n\t\tuniforms.transparentFg[0] = float(c_fg.redF());\n\t\tuniforms.transparentFg[1] = float(c_fg.greenF());\n\t\tuniforms.transparentFg[2] = float(c_fg.blueF());\n\t\tuniforms.transparentFg[3] = float(c_fg.alphaF());\n\t\tuniforms.transparentSize =\n\t\t\tst::transparentPlaceholderSize * _factor;\n\t\t_rub->updateDynamicBuffer(\n\t\t\t_uniformBuffer,\n\t\t\tuOffset,\n\t\t\tsizeof(TransparentContentUniforms),\n\t\t\t&uniforms);\n\t} else {\n\t\tContentUniforms uniforms{};\n\t\tuniforms.viewport[0] = vw;\n\t\tuniforms.viewport[1] = vh;\n\t\tfillShadowUniforms(\n\t\t\tuniforms.shadowTopRect,\n\t\t\tuniforms.shadowBottomSkipOpacityFullFade,\n\t\t\tgeometry);\n\t\tconst auto rRect = scaleRect(\n\t\t\ttransformRect(geometry.rect),\n\t\t\tgeometry.scale);\n\t\tif (geometry.roundRadius) {\n\t\t\tuniforms.roundRect[0] = rRect.x();\n\t\t\tuniforms.roundRect[1] = rRect.y();\n\t\t\tuniforms.roundRect[2] = rRect.width();\n\t\t\tuniforms.roundRect[3] = rRect.height();\n\t\t} else {\n\t\t\tuniforms.roundRect[0] = 0;\n\t\t\tuniforms.roundRect[1] = 0;\n\t\t\tuniforms.roundRect[2] = vw;\n\t\t\tuniforms.roundRect[3] = vh;\n\t\t}\n\t\tuniforms.roundRadius = geometry.roundRadius * _factor;\n\t\t_rub->updateDynamicBuffer(\n\t\t\t_uniformBuffer,\n\t\t\tuOffset,\n\t\t\tsizeof(ContentUniforms),\n\t\t\t&uniforms);\n\t}\n\n\tauto *srb = _rhi->newShaderResourceBindings();\n\tsrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer,\n\t\t\tuOffset,\n\t\t\tfillTransparentBackground\n\t\t\t\t? int(sizeof(TransparentContentUniforms))\n\t\t\t\t: int(sizeof(ContentUniforms))),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\tcontentTexture,\n\t\t\t_sampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t2,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_controlsFadeTexture\n\t\t\t\t? _controlsFadeTexture\n\t\t\t\t: _placeholderTexture,\n\t\t\t_sampler),\n\t});\n\tsrb->create();\n\t_perDrawSrbs.push_back(srb);\n\n\t_drawCommands.push_back({\n\t\t.pipeline = pipeline,\n\t\t.srb = srb,\n\t\t.vertexIndex = slot,\n\t});\n}\n\nvoid OverlayWidget::RendererRhi::paintBackground() {\n\tconst auto &bg = _owner->_fullScreenVideo\n\t\t? st::mediaviewVideoBg\n\t\t: st::mediaviewBg;\n\tconst auto c = bg->c;\n\tconst auto vw = float(_viewport.width() * _factor);\n\tconst auto vh = float(_viewport.height() * _factor);\n\n\tauto bgImage = QImage(1, 1, QImage::Format_ARGB32_Premultiplied);\n\tbgImage.fill(c);\n\n\tauto *tex = acquirePoolTexture(QSize(1, 1));\n\t_rub->uploadTexture(\n\t\ttex,\n\t\tQRhiTextureUploadDescription(\n\t\t\tQRhiTextureUploadEntry(0, 0,\n\t\t\t\tQRhiTextureSubresourceUploadDescription(bgImage))));\n\n\tconst float coords[] = {\n\t\t0.f, vh, 0.f, 0.f,\n\t\tvw,  vh, 1.f, 0.f,\n\t\t0.f, 0.f, 0.f, 1.f,\n\t\tvw,  0.f, 1.f, 1.f,\n\t};\n\tdrawTexturedQuad(_imagePipeline, tex, coords);\n}\n\nvoid OverlayWidget::RendererRhi::paintVideoStream() {\n\t_pendingVideoStream = _owner->_videoStream.get();\n}\n\nvoid OverlayWidget::RendererRhi::paintTransformedVideoFrame(\n\t\tContentGeometry geometry) {\n\tconst auto data = _owner->videoFrameWithInfo();\n\tif (data.format == Streaming::FrameFormat::None) {\n\t\treturn;\n\t} else if (data.format == Streaming::FrameFormat::ARGB32) {\n\t\tAssert(!data.image.isNull());\n\t\tpaintTransformedStaticContent(\n\t\t\tdata.image,\n\t\t\tgeometry,\n\t\t\tdata.alpha,\n\t\t\tdata.alpha);\n\t\treturn;\n\t}\n\tconst auto nativeTexture =\n\t\t(data.format == Streaming::FrameFormat::NativeTexture);\n\tconst auto nv12 = nativeTexture\n\t\t|| (data.format == Streaming::FrameFormat::NV12);\n\tconst auto yuv = data.yuv;\n\tconst auto nv12changed = !nativeTexture && (_chromaNV12 != nv12);\n\n\tconst auto upload = (_trackFrameIndex != data.index)\n\t\t|| (_streamedIndex != _owner->streamedIndex());\n\n\t_trackFrameIndex = data.index;\n\t_streamedIndex = _owner->streamedIndex();\n\n\tif (upload) {\n\t\tauto zeroCopied = false;\n#ifdef Q_OS_MAC\n\t\tif (nativeTexture && data.nativeFrame\n\t\t\t&& data.nativeFrame->pixelBuffer) {\n\t\t\tif (_metalTextureCache.createTexturesFromPixelBuffer(\n\t\t\t\t\t_rhi,\n\t\t\t\t\tdata.nativeFrame->pixelBuffer,\n\t\t\t\t\t&_yTexture,\n\t\t\t\t\t&_uvTexture,\n\t\t\t\t\t&_lumaSize,\n\t\t\t\t\t&_chromaSize)) {\n\t\t\t\t_chromaNV12 = true;\n\t\t\t\tzeroCopied = true;\n\t\t\t} else {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n#endif // Q_OS_MAC\n\t\tif (!zeroCopied) {\n\t\tif (_usingExternalVideoTextures) {\n\t\t\tdelete _yTexture;\n\t\t\t_yTexture = nullptr;\n\t\t\tdelete _uTexture;\n\t\t\t_uTexture = nullptr;\n\t\t\tdelete _vTexture;\n\t\t\t_vTexture = nullptr;\n\t\t\tdelete _uvTexture;\n\t\t\t_uvTexture = nullptr;\n\t\t\t_lumaSize = QSize();\n\t\t\t_chromaSize = QSize();\n#ifdef Q_OS_MAC\n\t\t\t_metalTextureCache.flush();\n#endif // Q_OS_MAC\n\t\t}\n\t\tAssert(!yuv->size.isEmpty());\n\t\tif (!_yTexture || _lumaSize != yuv->size) {\n\t\t\tdelete _yTexture;\n\t\t\t_yTexture = _rhi->newTexture(QRhiTexture::R8, yuv->size);\n\t\t\t_yTexture->create();\n\t\t\t_lumaSize = yuv->size;\n\t\t}\n\t\tauto yDesc = QRhiTextureSubresourceUploadDescription(\n\t\t\tyuv->y.data,\n\t\t\tyuv->y.stride * yuv->size.height());\n\t\tyDesc.setDataStride(yuv->y.stride);\n\t\t_rub->uploadTexture(\n\t\t\t_yTexture,\n\t\t\tQRhiTextureUploadDescription(\n\t\t\t\tQRhiTextureUploadEntry(0, 0, yDesc)));\n\n\t\tif (nv12) {\n\t\t\tif (!_uvTexture || nv12changed\n\t\t\t\t|| _chromaSize != yuv->chromaSize) {\n\t\t\t\tdelete _uvTexture;\n\t\t\t\t_uvTexture = _rhi->newTexture(\n\t\t\t\t\tQRhiTexture::RG8,\n\t\t\t\t\tyuv->chromaSize);\n\t\t\t\t_uvTexture->create();\n\t\t\t\t_chromaSize = yuv->chromaSize;\n\t\t\t}\n\t\t\tauto uvDesc = QRhiTextureSubresourceUploadDescription(\n\t\t\t\tyuv->u.data,\n\t\t\t\tyuv->u.stride * yuv->chromaSize.height());\n\t\t\tuvDesc.setDataStride(yuv->u.stride);\n\t\t\t_rub->uploadTexture(\n\t\t\t\t_uvTexture,\n\t\t\t\tQRhiTextureUploadDescription(\n\t\t\t\t\tQRhiTextureUploadEntry(0, 0, uvDesc)));\n\t\t} else {\n\t\t\tif (!_uTexture || nv12changed\n\t\t\t\t|| _chromaSize != yuv->chromaSize) {\n\t\t\t\tdelete _uTexture;\n\t\t\t\t_uTexture = _rhi->newTexture(\n\t\t\t\t\tQRhiTexture::R8,\n\t\t\t\t\tyuv->chromaSize);\n\t\t\t\t_uTexture->create();\n\t\t\t\tdelete _vTexture;\n\t\t\t\t_vTexture = _rhi->newTexture(\n\t\t\t\t\tQRhiTexture::R8,\n\t\t\t\t\tyuv->chromaSize);\n\t\t\t\t_vTexture->create();\n\t\t\t\t_chromaSize = yuv->chromaSize;\n\t\t\t}\n\t\t\tauto uDesc = QRhiTextureSubresourceUploadDescription(\n\t\t\t\tyuv->u.data,\n\t\t\t\tyuv->u.stride * yuv->chromaSize.height());\n\t\t\tuDesc.setDataStride(yuv->u.stride);\n\t\t\t_rub->uploadTexture(\n\t\t\t\t_uTexture,\n\t\t\t\tQRhiTextureUploadDescription(\n\t\t\t\t\tQRhiTextureUploadEntry(0, 0, uDesc)));\n\t\t\tauto vDesc = QRhiTextureSubresourceUploadDescription(\n\t\t\t\tyuv->v.data,\n\t\t\t\tyuv->v.stride * yuv->chromaSize.height());\n\t\t\tvDesc.setDataStride(yuv->v.stride);\n\t\t\t_rub->uploadTexture(\n\t\t\t\t_vTexture,\n\t\t\t\tQRhiTextureUploadDescription(\n\t\t\t\t\tQRhiTextureUploadEntry(0, 0, vDesc)));\n\t\t}\n\t\t_chromaNV12 = nv12;\n\t\t} // if (!zeroCopied)\n\t\t_usingExternalVideoTextures = zeroCopied;\n\t}\n\n\tvalidateControlsFade();\n\n\tif (_nextVertexSlot >= kMaxDraws) {\n\t\treturn;\n\t}\n\n\tconst auto textureRect = _owner->_stories\n\t\t? StoryCropTextureRect(\n\t\t\tQSizeF(nativeTexture ? _lumaSize : yuv->size),\n\t\t\tgeometry.rect.size())\n\t\t: QRectF(0., 0., 1., 1.);\n\tconst auto texLeft = float(textureRect.x());\n\tconst auto texRight = float(textureRect.x() + textureRect.width());\n\tconst auto texTop = float(textureRect.y());\n\tconst auto texBottom = float(textureRect.y() + textureRect.height());\n\n\tconst auto rRect = scaleRect(\n\t\ttransformRect(geometry.rect),\n\t\tgeometry.scale);\n\tconst auto centerx = rRect.x() + rRect.width() / 2;\n\tconst auto centery = rRect.y() + rRect.height() / 2;\n\tconst auto rsin = float(std::sin(geometry.rotation * M_PI / 180.));\n\tconst auto rcos = float(std::cos(geometry.rotation * M_PI / 180.));\n\tconst auto rotated = [&](float x, float y) -> std::array<float, 2> {\n\t\tx -= centerx;\n\t\ty -= centery;\n\t\treturn { centerx + x * rcos + y * rsin,\n\t\t         centery + y * rcos - x * rsin };\n\t};\n\tconst auto tl = rotated(rRect.left(), rRect.bottom());\n\tconst auto tr = rotated(rRect.right(), rRect.bottom());\n\tconst auto bl = rotated(rRect.left(), rRect.top());\n\tconst auto br = rotated(rRect.right(), rRect.top());\n\n\tconst auto slot = _nextVertexSlot++;\n\tconst auto vOffset = slot * kVertexSize;\n\tconst auto uOffset = slot * 256;\n\tconst float coords[] = {\n\t\ttl[0], tl[1], texLeft, texTop,\n\t\ttr[0], tr[1], texRight, texTop,\n\t\tbl[0], bl[1], texLeft, texBottom,\n\t\tbr[0], br[1], texRight, texBottom,\n\t};\n\t_rub->updateDynamicBuffer(\n\t\t_vertexBuffer, vOffset, kVertexSize, coords);\n\n\tconst auto vw = _viewport.width() * _factor;\n\tconst auto vh = _viewport.height() * _factor;\n\tContentUniforms uniforms{};\n\tuniforms.viewport[0] = vw;\n\tuniforms.viewport[1] = vh;\n\tfillShadowUniforms(\n\t\tuniforms.shadowTopRect,\n\t\tuniforms.shadowBottomSkipOpacityFullFade,\n\t\tgeometry);\n\tif (geometry.roundRadius) {\n\t\tuniforms.roundRect[0] = rRect.x();\n\t\tuniforms.roundRect[1] = rRect.y();\n\t\tuniforms.roundRect[2] = rRect.width();\n\t\tuniforms.roundRect[3] = rRect.height();\n\t} else {\n\t\tuniforms.roundRect[0] = 0;\n\t\tuniforms.roundRect[1] = 0;\n\t\tuniforms.roundRect[2] = vw;\n\t\tuniforms.roundRect[3] = vh;\n\t}\n\tuniforms.roundRadius = geometry.roundRadius * _factor;\n\t_rub->updateDynamicBuffer(\n\t\t_uniformBuffer, uOffset, sizeof(ContentUniforms), &uniforms);\n\n\tconst auto blendEnabled = (geometry.roundRadius > 0.);\n\tauto *srb = _rhi->newShaderResourceBindings();\n\tif (nv12) {\n\t\tsrb->setBindings({\n\t\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t\t0,\n\t\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_uniformBuffer,\n\t\t\t\tuOffset,\n\t\t\t\tsizeof(ContentUniforms)),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t1,\n\t\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_yTexture,\n\t\t\t\t_sampler),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t2,\n\t\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_uvTexture,\n\t\t\t\t_sampler),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t3,\n\t\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_controlsFadeTexture\n\t\t\t\t\t? _controlsFadeTexture\n\t\t\t\t\t: _placeholderTexture,\n\t\t\t\t_sampler),\n\t\t});\n\t} else {\n\t\tsrb->setBindings({\n\t\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t\t0,\n\t\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_uniformBuffer,\n\t\t\t\tuOffset,\n\t\t\t\tsizeof(ContentUniforms)),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t1,\n\t\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_yTexture,\n\t\t\t\t_sampler),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t2,\n\t\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_uTexture,\n\t\t\t\t_sampler),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t3,\n\t\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_vTexture,\n\t\t\t\t_sampler),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t4,\n\t\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_controlsFadeTexture\n\t\t\t\t\t? _controlsFadeTexture\n\t\t\t\t\t: _placeholderTexture,\n\t\t\t\t_sampler),\n\t\t});\n\t}\n\tsrb->create();\n\t_perDrawSrbs.push_back(srb);\n\n\tauto *pipeline = nv12\n\t\t? (blendEnabled ? _nv12BlendPipeline : _nv12Pipeline)\n\t\t: (blendEnabled ? _yuv420BlendPipeline : _yuv420Pipeline);\n\t_drawCommands.push_back({\n\t\t.pipeline = pipeline,\n\t\t.srb = srb,\n\t\t.vertexIndex = slot,\n\t});\n\n\tif (_owner->_recognitionResult.success\n\t\t&& !_owner->_recognitionResult.items.empty()) {\n\t\tconst auto opacity = _owner->_recognitionAnimation.value(\n\t\t\t_owner->_showRecognitionResults ? 1. : 0.);\n\t\tif (opacity > 0.) {\n\t\t\tpaintRecognitionOverlay(data.image, geometry);\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::RendererRhi::paintRecognitionOverlay(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry) {\n\tif (!_owner->_recognitionResult.success\n\t\t|| _owner->_recognitionResult.items.empty()) {\n\t\treturn;\n\t}\n\tconst auto opacity = _owner->_recognitionAnimation.value(\n\t\t_owner->_showRecognitionResults ? 1. : 0.);\n\tif (opacity <= 0.) {\n\t\treturn;\n\t}\n\tconst auto rect = geometry.rect;\n\tconst auto overlaySize = rect.size().toSize() * _ifactor;\n\tif (overlaySize.isEmpty()) {\n\t\treturn;\n\t}\n\tauto overlay = QImage(\n\t\toverlaySize,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\toverlay.setDevicePixelRatio(_factor);\n\toverlay.fill(QColor(0, 0, 0, int(77 * opacity)));\n\n\tconst auto scale = rect.width() / float(image.width());\n\t{\n\t\tauto p = QPainter(&overlay);\n\t\tp.setCompositionMode(QPainter::CompositionMode_Clear);\n\t\tfor (const auto &item : _owner->_recognitionResult.items) {\n\t\t\tconst auto &r = item.rect;\n\t\t\tp.fillRect(QRectF(\n\t\t\t\tr.x() * _ifactor * scale,\n\t\t\t\tr.y() * _ifactor * scale,\n\t\t\t\tr.width() * _ifactor * scale,\n\t\t\t\tr.height() * _ifactor * scale), Qt::transparent);\n\t\t}\n\t}\n\n\tauto *tex = acquirePoolTexture(overlaySize);\n\t_rub->uploadTexture(\n\t\ttex,\n\t\tQRhiTextureUploadDescription(\n\t\t\tQRhiTextureUploadEntry(0, 0,\n\t\t\t\tQRhiTextureSubresourceUploadDescription(overlay))));\n\n\tconst auto rRect = scaleRect(transformRect(rect), geometry.scale);\n\tconst float coords[] = {\n\t\trRect.left(), rRect.bottom(), 0.f, 0.f,\n\t\trRect.right(), rRect.bottom(), 1.f, 0.f,\n\t\trRect.left(), rRect.top(), 0.f, 1.f,\n\t\trRect.right(), rRect.top(), 1.f, 1.f,\n\t};\n\tdrawTexturedQuad(_imagePipeline, tex, coords, 1.f, true);\n}\n\nvoid OverlayWidget::RendererRhi::paintTransformedStaticContent(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry,\n\t\tbool semiTransparent,\n\t\tbool fillTransparentBackground,\n\t\tint index) {\n\tExpects(index >= 0 && index < 3);\n\n\tif (image.isNull() || geometry.rect.isEmpty() || !_imagePipeline) {\n\t\treturn;\n\t}\n\n\tconst auto cacheKey = image.cacheKey();\n\tconst auto upload = (_cacheKeys[index] != cacheKey);\n\tif (upload) {\n\t\t_cacheKeys[index] = cacheKey;\n\t\tif (!_rgbaTextures[index]\n\t\t\t|| _rgbaSizes[index] != image.size()) {\n\t\t\tdelete _rgbaTextures[index];\n\t\t\t_rgbaTextures[index] = _rhi->newTexture(\n\t\t\t\tQRhiTexture::BGRA8,\n\t\t\t\timage.size());\n\t\t\t_rgbaTextures[index]->create();\n\t\t\t_rgbaSizes[index] = image.size();\n\t\t}\n\t\t_rub->uploadTexture(\n\t\t\t_rgbaTextures[index],\n\t\t\tQRhiTextureUploadDescription(\n\t\t\t\tQRhiTextureUploadEntry(0, 0,\n\t\t\t\t\tQRhiTextureSubresourceUploadDescription(image))));\n\t}\n\n\tconst auto textureRect = _owner->_stories\n\t\t? StoryCropTextureRect(\n\t\t\tQSizeF(image.size()),\n\t\t\tgeometry.rect.size())\n\t\t: QRectF(0., 0., 1., 1.);\n\tconst auto texLeft = float(textureRect.x());\n\tconst auto texRight = float(textureRect.x() + textureRect.width());\n\tconst auto texTop = float(textureRect.y());\n\tconst auto texBottom = float(textureRect.y() + textureRect.height());\n\n\tconst auto rRect = scaleRect(\n\t\ttransformRect(geometry.rect),\n\t\tgeometry.scale);\n\tconst auto centerx = rRect.x() + rRect.width() / 2;\n\tconst auto centery = rRect.y() + rRect.height() / 2;\n\tconst auto rsin = float(std::sin(geometry.rotation * M_PI / 180.));\n\tconst auto rcos = float(std::cos(geometry.rotation * M_PI / 180.));\n\tconst auto rotated = [&](float x, float y) -> std::array<float, 2> {\n\t\tx -= centerx;\n\t\ty -= centery;\n\t\treturn { centerx + x * rcos + y * rsin,\n\t\t         centery + y * rcos - x * rsin };\n\t};\n\tconst auto tl = rotated(rRect.left(), rRect.bottom());\n\tconst auto tr = rotated(rRect.right(), rRect.bottom());\n\tconst auto bl = rotated(rRect.left(), rRect.top());\n\tconst auto br = rotated(rRect.right(), rRect.top());\n\tconst float coords[] = {\n\t\ttl[0], tl[1], texLeft, texTop,\n\t\ttr[0], tr[1], texRight, texTop,\n\t\tbl[0], bl[1], texLeft, texBottom,\n\t\tbr[0], br[1], texRight, texBottom,\n\t};\n\n\tconst auto blend = (geometry.roundRadius > 0.)\n\t\t|| (semiTransparent && !fillTransparentBackground);\n\tdrawContentQuad(\n\t\t_rgbaTextures[index],\n\t\tcoords,\n\t\tgeometry,\n\t\tfillTransparentBackground,\n\t\tblend);\n\tpaintRecognitionOverlay(image, geometry);\n}\n\nvoid OverlayWidget::RendererRhi::paintRadialLoading(\n\t\tQRect inner,\n\t\tbool radial,\n\t\tfloat64 radialOpacity) {\n\tpaintUsingRaster(inner, [&](Painter &p) {\n\t\tconst auto newInner = QRect(QPoint(), inner.size());\n\t\t_owner->paintRadialLoadingContent(p, newInner, radial, radialOpacity);\n\t}, true);\n}\n\nvoid OverlayWidget::RendererRhi::paintThemePreview(QRect outer) {\n\tpaintUsingRaster(outer, [&](Painter &p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintThemePreviewContent(p, newOuter, newOuter);\n\t});\n}\n\nvoid OverlayWidget::RendererRhi::paintDocumentBubble(\n\t\tQRect outer,\n\t\tQRect icon) {\n\tpaintUsingRaster(outer, [&](Painter &p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\tconst auto newIcon = icon.translated(-outer.topLeft());\n\t\t_owner->paintDocumentBubbleContent(p, newOuter, newIcon, newOuter);\n\t}, true);\n}\n\nvoid OverlayWidget::RendererRhi::paintSaveMsg(QRect outer) {\n\tpaintUsingRaster(outer, [&](Painter &p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintSaveMsgContent(p, newOuter, newOuter);\n\t}, true);\n}\n\nvoid OverlayWidget::RendererRhi::paintChapter(QRect outer) {\n\tpaintUsingRaster(outer, [&](Painter &p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintChapterContent(p, newOuter, newOuter);\n\t}, true);\n}\n\nvoid OverlayWidget::RendererRhi::paintSpeedBoost(QRect outer) {\n\tpaintUsingRaster(outer, [&](Painter &p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintSpeedBoostContent(p, newOuter, newOuter);\n\t}, true);\n}\n\nauto OverlayWidget::RendererRhi::controlMeta(Over control) const\n-> Control {\n\tconst auto stories = [&] {\n\t\treturn (_owner->_stories != nullptr);\n\t};\n\tswitch (control) {\n\tcase Over::Left: return {\n\t\t0,\n\t\tstories() ? &st::storiesLeft : &st::mediaviewLeft\n\t};\n\tcase Over::Right: return {\n\t\t1,\n\t\tstories() ? &st::storiesRight : &st::mediaviewRight\n\t};\n\tcase Over::Save: return {\n\t\t2,\n\t\t(_owner->saveControlLocked()\n\t\t\t? &st::mediaviewSaveLocked\n\t\t\t: &st::mediaviewSave)\n\t};\n\tcase Over::Share: return { 3, &st::mediaviewShare };\n\tcase Over::Rotate: return { 4, &st::mediaviewRotate };\n\tcase Over::More: return { 5, &st::mediaviewMore };\n\tcase Over::Draw: return { 6, &st::mediaviewDraw };\n\tcase Over::Recognize: return { 7, &st::mediaviewRecognize };\n\t}\n\tUnexpected(\"Control value in OverlayWidget::RendererRhi::controlMeta.\");\n}\n\nvoid OverlayWidget::RendererRhi::validateControls() {\n\tif (_controlsAtlasTexture) {\n\t\treturn;\n\t}\n\tconst auto metas = {\n\t\tcontrolMeta(Over::Left),\n\t\tcontrolMeta(Over::Right),\n\t\tcontrolMeta(Over::Save),\n\t\tcontrolMeta(Over::Share),\n\t\tcontrolMeta(Over::Rotate),\n\t\tcontrolMeta(Over::More),\n\t\tcontrolMeta(Over::Draw),\n\t\tcontrolMeta(Over::Recognize),\n\t};\n\tauto maxWidth = 0;\n\tauto fullHeight = 0;\n\tfor (const auto &meta : metas) {\n\t\tmaxWidth = std::max(meta.icon->width(), maxWidth);\n\t\tfullHeight += meta.icon->height();\n\t}\n\tmaxWidth = std::max(st::mediaviewIconOver, maxWidth);\n\tfullHeight += st::mediaviewIconOver;\n\tauto image = QImage(\n\t\tQSize(maxWidth, fullHeight) * _ifactor,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.fill(Qt::transparent);\n\timage.setDevicePixelRatio(_ifactor);\n\t{\n\t\tauto p = QPainter(&image);\n\t\tauto index = 0;\n\t\tauto height = 0;\n\t\tfor (const auto &meta : metas) {\n\t\t\tmeta.icon->paint(p, 0, height, maxWidth);\n\t\t\t_controlsTextures[index++] = QRect(\n\t\t\t\tQPoint(0, height) * _ifactor,\n\t\t\t\tmeta.icon->size() * _ifactor);\n\t\t\theight += meta.icon->height();\n\t\t}\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(OverBackgroundColor());\n\t\tp.drawEllipse(\n\t\t\tQRect(0, height, st::mediaviewIconOver, st::mediaviewIconOver));\n\t\t_controlsTextures[index++] = QRect(\n\t\t\tQPoint(0, height) * _ifactor,\n\t\t\tQSize(st::mediaviewIconOver, st::mediaviewIconOver) * _ifactor);\n\t}\n\n\tconst auto size = image.size();\n\t_controlsAtlasTexture = _rhi->newTexture(QRhiTexture::BGRA8, size);\n\t_controlsAtlasTexture->create();\n\t_controlsAtlasSize = size;\n\t_rub->uploadTexture(\n\t\t_controlsAtlasTexture,\n\t\tQRhiTextureUploadDescription(\n\t\t\tQRhiTextureUploadEntry(0, 0,\n\t\t\t\tQRhiTextureSubresourceUploadDescription(image))));\n}\n\nvoid OverlayWidget::RendererRhi::invalidateControls() {\n\tdelete _controlsAtlasTexture;\n\t_controlsAtlasTexture = nullptr;\n\tranges::fill(_controlsTextures, QRect());\n}\n\nvoid OverlayWidget::RendererRhi::paintControlsStart() {\n\tvalidateControls();\n}\n\nvoid OverlayWidget::RendererRhi::paintControl(\n\t\tOver control,\n\t\tQRect over,\n\t\tfloat64 overOpacity,\n\t\tQRect inner,\n\t\tfloat64 innerOpacity,\n\t\tconst style::icon &icon) {\n\tif (!_controlsAtlasTexture) {\n\t\treturn;\n\t}\n\tconst auto meta = controlMeta(control);\n\tAssert(meta.icon == &icon);\n\n\tconst auto atlasW = float(_controlsAtlasSize.width());\n\tconst auto atlasH = float(_controlsAtlasSize.height());\n\n\tif (!over.isEmpty() && overOpacity > 0.) {\n\t\tconst auto overAlpha = float(overOpacity * kOverBackgroundOpacity);\n\t\tconst auto &overTex = _controlsTextures[kControlsCount];\n\t\tconst auto overGeometry = transformRect(over);\n\t\tconst auto tl = overTex.left() / atlasW;\n\t\tconst auto tr = overTex.right() / atlasW;\n\t\tconst auto tt = overTex.top() / atlasH;\n\t\tconst auto tb = overTex.bottom() / atlasH;\n\t\tconst float overCoords[] = {\n\t\t\toverGeometry.left(), overGeometry.bottom(), tl, tt,\n\t\t\toverGeometry.right(), overGeometry.bottom(), tr, tt,\n\t\t\toverGeometry.left(), overGeometry.top(), tl, tb,\n\t\t\toverGeometry.right(), overGeometry.top(), tr, tb,\n\t\t};\n\t\tdrawTexturedQuad(\n\t\t\t_imagePipeline,\n\t\t\t_controlsAtlasTexture,\n\t\t\toverCoords,\n\t\t\toverAlpha,\n\t\t\ttrue);\n\t}\n\n\tconst auto &iconTex = _controlsTextures[meta.index];\n\tconst auto iconGeometry = transformRect(inner);\n\tconst auto tl = iconTex.left() / atlasW;\n\tconst auto tr = iconTex.right() / atlasW;\n\tconst auto tt = iconTex.top() / atlasH;\n\tconst auto tb = iconTex.bottom() / atlasH;\n\tconst float iconCoords[] = {\n\t\ticonGeometry.left(), iconGeometry.bottom(), tl, tt,\n\t\ticonGeometry.right(), iconGeometry.bottom(), tr, tt,\n\t\ticonGeometry.left(), iconGeometry.top(), tl, tb,\n\t\ticonGeometry.right(), iconGeometry.top(), tr, tb,\n\t};\n\tdrawTexturedQuad(\n\t\t_imagePipeline,\n\t\t_controlsAtlasTexture,\n\t\ticonCoords,\n\t\tfloat(innerOpacity),\n\t\ttrue);\n}\n\nvoid OverlayWidget::RendererRhi::paintFooter(\n\t\tQRect outer,\n\t\tfloat64 opacity) {\n\tif (outer.isEmpty() || opacity <= 0.) {\n\t\treturn;\n\t}\n\tpaintUsingRaster(outer, [&](Painter &p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintFooterContent(p, newOuter, newOuter, opacity);\n\t}, true);\n}\n\nvoid OverlayWidget::RendererRhi::paintCaption(\n\t\tQRect outer,\n\t\tfloat64 opacity) {\n\tif (outer.isEmpty() || opacity <= 0.) {\n\t\treturn;\n\t}\n\tpaintUsingRaster(outer, [&](Painter &p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintCaptionContent(p, newOuter, newOuter, opacity);\n\t}, true);\n}\n\nvoid OverlayWidget::RendererRhi::paintGroupThumbs(\n\t\tQRect outer,\n\t\tfloat64 opacity) {\n\tif (outer.isEmpty() || opacity <= 0.) {\n\t\treturn;\n\t}\n\tpaintUsingRaster(outer, [&](Painter &p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintGroupThumbsContent(p, newOuter, newOuter, opacity);\n\t}, true);\n}\n\nvoid OverlayWidget::RendererRhi::paintRoundedCorners(int radius) {\n\tif (!_roundedCornersPipeline || radius <= 0) {\n\t\treturn;\n\t}\n\n\tconst auto vw = _viewport.width() * _factor;\n\tconst auto vh = _viewport.height() * _factor;\n\n\tRoundedCornersUniforms uniforms{};\n\tuniforms.viewport[0] = vw;\n\tuniforms.viewport[1] = vh;\n\tconst auto roundRect = transformRect(QRect(QPoint(), _viewport));\n\tuniforms.roundRect[0] = roundRect.x();\n\tuniforms.roundRect[1] = roundRect.y();\n\tuniforms.roundRect[2] = roundRect.width();\n\tuniforms.roundRect[3] = roundRect.height();\n\tuniforms.roundRadius = radius * _factor;\n\n\tconst auto topLeft = transformRect(\n\t\tQRect(0, 0, radius, radius));\n\tconst auto topRight = transformRect(\n\t\tQRect(_viewport.width() - radius, 0, radius, radius));\n\tconst auto bottomRight = transformRect(QRect(\n\t\t_viewport.width() - radius,\n\t\t_viewport.height() - radius,\n\t\tradius,\n\t\tradius));\n\tconst auto bottomLeft = transformRect(\n\t\tQRect(0, _viewport.height() - radius, radius, radius));\n\n\tconst auto corners = {\n\t\tstd::ref(topLeft),\n\t\tstd::ref(topRight),\n\t\tstd::ref(bottomRight),\n\t\tstd::ref(bottomLeft),\n\t};\n\n\tauto cornerIndex = 0;\n\tfor (const auto &corner : corners) {\n\t\tif (_nextVertexSlot >= kMaxDraws) {\n\t\t\tbreak;\n\t\t}\n\t\tconst auto slot = _nextVertexSlot++;\n\t\tconst auto uOffset = slot * 256;\n\n\t\tconst float coords[] = {\n\t\t\tcorner.get().left(), corner.get().bottom(),\n\t\t\tcorner.get().right(), corner.get().bottom(),\n\t\t\tcorner.get().left(), corner.get().top(),\n\t\t\tcorner.get().right(), corner.get().top(),\n\t\t};\n\n\t\t_rub->updateDynamicBuffer(\n\t\t\t_fillVertexBuffer,\n\t\t\tcornerIndex * int(sizeof(coords)),\n\t\t\tsizeof(coords),\n\t\t\tcoords);\n\n\t\t_rub->updateDynamicBuffer(\n\t\t\t_uniformBuffer,\n\t\t\tuOffset,\n\t\t\tsizeof(RoundedCornersUniforms),\n\t\t\t&uniforms);\n\n\t\tauto *srb = _rhi->newShaderResourceBindings();\n\t\tsrb->setBindings({\n\t\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t\t0,\n\t\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_uniformBuffer,\n\t\t\t\tuOffset,\n\t\t\t\tsizeof(RoundedCornersUniforms)),\n\t\t});\n\t\tsrb->create();\n\t\t_perDrawSrbs.push_back(srb);\n\n\t\t_drawCommands.push_back({\n\t\t\t.pipeline = _roundedCornersPipeline,\n\t\t\t.srb = srb,\n\t\t\t.vertexIndex = cornerIndex,\n\t\t\t.fillVertex = true,\n\t\t});\n\t\t++cornerIndex;\n\t}\n}\n\nvoid OverlayWidget::RendererRhi::paintStoriesSiblingPart(\n\t\tint index,\n\t\tconst QImage &image,\n\t\tQRect rect,\n\t\tfloat64 opacity) {\n\tExpects(index >= 0 && index < kStoriesSiblingPartsCount);\n\n\tif (image.isNull() || rect.isEmpty()) {\n\t\treturn;\n\t}\n\n\tconst auto cacheKey = image.cacheKey();\n\tif (_storiesSiblingCacheKeys[index] != cacheKey\n\t\t|| !_storiesSiblingTextures[index]\n\t\t|| _storiesSiblingSizes[index] != image.size()) {\n\t\t_storiesSiblingCacheKeys[index] = cacheKey;\n\t\tdelete _storiesSiblingTextures[index];\n\t\t_storiesSiblingTextures[index] = _rhi->newTexture(\n\t\t\tQRhiTexture::BGRA8,\n\t\t\timage.size());\n\t\t_storiesSiblingTextures[index]->create();\n\t\t_storiesSiblingSizes[index] = image.size();\n\t\t_rub->uploadTexture(\n\t\t\t_storiesSiblingTextures[index],\n\t\t\tQRhiTextureUploadDescription(\n\t\t\t\tQRhiTextureUploadEntry(0, 0,\n\t\t\t\t\tQRhiTextureSubresourceUploadDescription(image))));\n\t}\n\n\tconst auto rRect = transformRect(rect);\n\tconst float coords[] = {\n\t\trRect.left(), rRect.bottom(), 0.f, 0.f,\n\t\trRect.right(), rRect.bottom(), 1.f, 0.f,\n\t\trRect.left(), rRect.top(), 0.f, 1.f,\n\t\trRect.right(), rRect.top(), 1.f, 1.f,\n\t};\n\tdrawTexturedQuad(\n\t\t_imagePipeline,\n\t\t_storiesSiblingTextures[index],\n\t\tcoords,\n\t\tfloat(opacity),\n\t\ttrue);\n}\n\nRect OverlayWidget::RendererRhi::transformRect(const Rect &raster) const {\n\treturn TransformRect(raster, _viewport, _factor);\n}\n\nRect OverlayWidget::RendererRhi::transformRect(const QRectF &raster) const {\n\treturn TransformRect(raster, _viewport, _factor);\n}\n\nRect OverlayWidget::RendererRhi::transformRect(const QRect &raster) const {\n\treturn TransformRect(Rect(raster), _viewport, _factor);\n}\n\nRect OverlayWidget::RendererRhi::scaleRect(\n\t\tconst Rect &unscaled,\n\t\tfloat64 scale) const {\n\tconst auto added = scale - 1.;\n\tconst auto addw = unscaled.width() * added;\n\tconst auto addh = unscaled.height() * added;\n\treturn Rect(\n\t\tunscaled.x() - addw / 2,\n\t\tunscaled.y() - addh / 2,\n\t\tunscaled.width() + addw,\n\t\tunscaled.height() + addh);\n}\n\n} // namespace Media::View\n\n#endif // Qt >= 6.7\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_overlay_rhi.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/view/media_view_overlay_renderer.h\"\n#include \"ui/rhi/rhi_renderer.h\"\n#include \"ui/rhi/rhi_image.h\"\n#ifdef Q_OS_MAC\n#include \"media/view/media_view_metal_texture.h\"\n#endif\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\nclass QRhi;\nclass QRhiBuffer;\nclass QRhiTexture;\nclass QRhiSampler;\nclass QRhiGraphicsPipeline;\nclass QRhiShaderResourceBindings;\nclass QRhiResourceUpdateBatch;\n\nnamespace Media::View {\n\nclass VideoStream;\n\nclass OverlayWidget::RendererRhi final\n\t: public OverlayWidget::Renderer\n\t, public Ui::Rhi::Renderer\n\t, public base::has_weak_ptr {\npublic:\n\texplicit RendererRhi(not_null<OverlayWidget*> owner);\n\t~RendererRhi();\n\n\tvoid initialize(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) override;\n\tvoid render(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) override;\n\tvoid releaseResources() override;\n\n\tQColor rhiClearColor() override;\n\tstd::optional<QColor> clearColor() override;\n\nprivate:\n\tvoid paintBackground() override;\n\tvoid paintVideoStream() override;\n\tvoid paintTransformedVideoFrame(ContentGeometry geometry) override;\n\tvoid paintTransformedStaticContent(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry,\n\t\tbool semiTransparent,\n\t\tbool fillTransparentBackground,\n\t\tint index = 0) override;\n\tvoid paintRadialLoading(\n\t\tQRect inner,\n\t\tbool radial,\n\t\tfloat64 radialOpacity) override;\n\tvoid paintThemePreview(QRect outer) override;\n\tvoid paintDocumentBubble(QRect outer, QRect icon) override;\n\tvoid paintSaveMsg(QRect outer) override;\n\tvoid paintChapter(QRect outer) override;\n\tvoid paintSpeedBoost(QRect outer) override;\n\tvoid paintControlsStart() override;\n\tvoid paintControl(\n\t\tOver control,\n\t\tQRect over,\n\t\tfloat64 overOpacity,\n\t\tQRect inner,\n\t\tfloat64 innerOpacity,\n\t\tconst style::icon &icon) override;\n\tvoid paintFooter(QRect outer, float64 opacity) override;\n\tvoid paintCaption(QRect outer, float64 opacity) override;\n\tvoid paintGroupThumbs(QRect outer, float64 opacity) override;\n\tvoid paintRoundedCorners(int radius) override;\n\tvoid paintStoriesSiblingPart(\n\t\tint index,\n\t\tconst QImage &image,\n\t\tQRect rect,\n\t\tfloat64 opacity = 1.) override;\n\n\tvoid paintRecognitionOverlay(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry);\n\n\tstruct Control {\n\t\tint index = -1;\n\t\tnot_null<const style::icon*> icon;\n\t};\n\n\tvoid createPipelines();\n\tvoid validateControlsFade();\n\tvoid validateControls();\n\tvoid invalidateControls();\n\t[[nodiscard]] Control controlMeta(Over control) const;\n\n\tvoid drawTexturedQuad(\n\t\tQRhiGraphicsPipeline *pipeline,\n\t\tQRhiTexture *texture,\n\t\tconst float *coords,\n\t\tfloat opacity = 1.f,\n\t\tbool blend = false);\n\n\tvoid drawContentQuad(\n\t\tQRhiTexture *contentTexture,\n\t\tconst float *coords,\n\t\tContentGeometry geometry,\n\t\tbool fillTransparentBackground,\n\t\tbool blend);\n\n\tvoid fillShadowUniforms(\n\t\tfloat *shadowTopRect,\n\t\tfloat *shadowBottomSkipOpacityFullFade,\n\t\tContentGeometry geometry) const;\n\n\tvoid paintUsingRaster(\n\t\tQRect rect,\n\t\tFn<void(Painter&)> method,\n\t\tbool transparent = false,\n\t\tfloat opacity = 1.f);\n\n\t[[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const;\n\t[[nodiscard]] Ui::GL::Rect transformRect(const QRectF &raster) const;\n\t[[nodiscard]] Ui::GL::Rect transformRect(\n\t\tconst Ui::GL::Rect &raster) const;\n\t[[nodiscard]] Ui::GL::Rect scaleRect(\n\t\tconst Ui::GL::Rect &unscaled,\n\t\tfloat64 scale) const;\n\n\tconst not_null<OverlayWidget*> _owner;\n\n\tQRhi *_rhi = nullptr;\n\tQRhiRenderTarget *_rt = nullptr;\n\tQRhiCommandBuffer *_cb = nullptr;\n\tQRhiResourceUpdateBatch *_rub = nullptr;\n\tQSize _viewport;\n\tfloat _factor = 1.;\n\tint _ifactor = 1;\n\n\tstatic constexpr int kMaxDraws = 32;\n\tstatic constexpr int kVertexSize = 4 * 4 * sizeof(float);\n\n\tQRhiBuffer *_vertexBuffer = nullptr;\n\tQRhiBuffer *_fillVertexBuffer = nullptr;\n\tQRhiBuffer *_uniformBuffer = nullptr;\n\tQRhiSampler *_sampler = nullptr;\n\tQRhiTexture *_placeholderTexture = nullptr;\n\n\tQRhiGraphicsPipeline *_imagePipeline = nullptr;\n\tQRhiGraphicsPipeline *_imageBlendPipeline = nullptr;\n\tQRhiGraphicsPipeline *_staticContentPipeline = nullptr;\n\tQRhiGraphicsPipeline *_staticContentBlendPipeline = nullptr;\n\tQRhiGraphicsPipeline *_transparentContentPipeline = nullptr;\n\tQRhiGraphicsPipeline *_roundedCornersPipeline = nullptr;\n\tQRhiGraphicsPipeline *_yuv420Pipeline = nullptr;\n\tQRhiGraphicsPipeline *_yuv420BlendPipeline = nullptr;\n\tQRhiGraphicsPipeline *_nv12Pipeline = nullptr;\n\tQRhiGraphicsPipeline *_nv12BlendPipeline = nullptr;\n\n\tstruct DrawCommand {\n\t\tQRhiGraphicsPipeline *pipeline = nullptr;\n\t\tQRhiShaderResourceBindings *srb = nullptr;\n\t\tint vertexIndex = 0;\n\t\tbool fillVertex = false;\n\t};\n\tstd::vector<DrawCommand> _drawCommands;\n\tstd::vector<QRhiShaderResourceBindings*> _perDrawSrbs;\n\tint _nextVertexSlot = 0;\n\n\tQRhiTexture *_rgbaTextures[3] = {};\n\tQSize _rgbaSizes[3];\n\tquint64 _cacheKeys[3] = {};\n\n\tQRhiTexture *_yTexture = nullptr;\n\tQRhiTexture *_uTexture = nullptr;\n\tQRhiTexture *_vTexture = nullptr;\n\tQRhiTexture *_uvTexture = nullptr;\n\tQSize _lumaSize;\n\tQSize _chromaSize;\n\tbool _chromaNV12 = false;\n\tbool _usingExternalVideoTextures = false;\n\tint _trackFrameIndex = 0;\n\tint _streamedIndex = 0;\n\n\tstruct PoolTexture {\n\t\tQRhiTexture *texture = nullptr;\n\t\tQSize size;\n\t};\n\tstd::vector<PoolTexture> _texturePool;\n\tint _nextPoolIndex = 0;\n\t[[nodiscard]] QRhiTexture *acquirePoolTexture(QSize size);\n\n\tstatic constexpr auto kControlsCount = 8;\n\tQRhiTexture *_controlsAtlasTexture = nullptr;\n\tQSize _controlsAtlasSize;\n\tstd::array<QRect, kControlsCount + 1> _controlsTextures;\n\n\tstatic constexpr auto kStoriesSiblingPartsCount = 4;\n\tQRhiTexture *_storiesSiblingTextures[kStoriesSiblingPartsCount] = {};\n\tQSize _storiesSiblingSizes[kStoriesSiblingPartsCount];\n\tquint64 _storiesSiblingCacheKeys[kStoriesSiblingPartsCount] = {};\n\n\tQRhiTexture *_controlsFadeTexture = nullptr;\n\tQSize _controlsFadeSize;\n\tbool _shadowTopFlip = false;\n\tbool _shadowsForStories = false;\n\n#ifdef Q_OS_MAC\n\tMetalTextureCache _metalTextureCache;\n#endif\n\n\tVideoStream *_pendingVideoStream = nullptr;\n\tbool _initialized = false;\n\n\trpl::lifetime _storiesLifetime;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Media::View\n\n#endif // Qt >= 6.7\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/view/media_view_overlay_widget.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_attached_stickers.h\"\n#include \"api/api_peer_photo.h\"\n#include \"api/api_polls.h\"\n#include \"base/qt/qt_common_adapters.h\"\n#include \"base/timer_rpl.h\"\n#include \"lang/lang_keys.h\"\n#include \"menu/menu_sponsored.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"calls/calls_instance.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/file_utilities.h\"\n#include \"core/mime_type.h\"\n#include \"core/ui_integration.h\"\n#include \"core/crash_reports.h\"\n#include \"core/sandbox.h\"\n#include \"core/shortcuts.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/layers/layer_manager.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/platform/ui_platform_window_title.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/item_text_options.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/gl/gl_window.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/ui_utility.h\"\n#include \"info/info_memento.h\"\n#include \"info/info_controller.h\"\n#include \"info/statistics/info_statistics_widget.h\"\n#include \"boxes/delete_messages_box.h\"\n#include \"boxes/report_messages_box.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/view/media_view_group_thumbs.h\"\n#include \"media/view/media_view_pip.h\"\n#include \"media/view/media_view_overlay_raster.h\"\n#include \"media/view/media_view_overlay_opengl.h\"\n#include \"media/view/media_view_overlay_rhi.h\"\n#include \"media/view/media_view_playback_sponsored.h\"\n#include \"media/view/media_view_video_stream.h\"\n#include \"media/stories/media_stories_share.h\"\n#include \"media/stories/media_stories_view.h\"\n#include \"media/streaming/media_streaming_document.h\"\n#include \"media/streaming/media_streaming_player.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"history/history.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/view/media/history_view_media.h\"\n#include \"history/view/history_view_group_call_bar.h\"\n#include \"history/view/reactions/history_view_reactions_selector.h\"\n#include \"data/components/sponsored_messages.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_media_rotation.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_document_resolver.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_download_manager.h\"\n#include \"data/data_poll.h\"\n#include \"window/themes/window_theme_preview.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_controller.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/power_save_blocker.h\"\n#include \"base/random.h\"\n#include \"base/unixtime.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"base/event_filter.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\" // Domain::activeSessionValue.\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"layout/layout_document_generic_preview.h\"\n#include \"platform/platform_overlay_widget.h\"\n#include \"storage/file_download.h\"\n#include \"storage/storage_account.h\"\n#include \"styles/style_media_view.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"platform/platform_text_recognition.h\"\n\n#include <QGraphicsOpacityEffect>\n\n#ifdef Q_OS_MAC\n#include \"platform/mac/touchbar/mac_touchbar_media_view.h\"\n#endif // Q_OS_MAC\n\n#include <QtWidgets/QApplication>\n#include <QtCore/QBuffer>\n#include <QtGui/QGuiApplication>\n#include <QtGui/QPainterPathStroker>\n#include <QtGui/QWindow>\n#include <QtGui/QScreen>\n\n#include <kurlmimedata.h>\n\nnamespace Media {\nnamespace View {\nnamespace {\n\nstruct RecognitionId {\n\tuint64 sessionUniqueId = 0;\n\tPhotoId photoId = 0;\n\tDocumentId documentId = 0;\n\n\tfriend inline auto operator<=>(RecognitionId, RecognitionId) = default;\n};\n\nusing RecognitionResult = Platform::TextRecognition::Result;\nusing RecognitionCacheMap = base::flat_map<RecognitionId, RecognitionResult>;\n\n[[nodiscard]] RecognitionCacheMap *RecognitionCache() {\n\tstatic auto cache = Platform::TextRecognition::IsAvailable()\n\t\t? std::make_unique<base::flat_map<RecognitionId, RecognitionResult>>()\n\t\t: nullptr;\n\treturn cache.get();\n}\n\nconstexpr auto kPreloadCount = 3;\nconstexpr auto kMaxZoomLevel = 7; // x8\nconstexpr auto kZoomToScreenLevel = 1024;\nconstexpr auto kOverlayLoaderPriority = 2;\nconstexpr auto kSeekTimeMs = 5 * crl::time(1000);\nconstexpr auto kSeekTimeMsLong = 10 * crl::time(1000);\nconstexpr auto kFrameStepFallbackFps = 30.;\nconstexpr auto kFrameStepThrottleMs = crl::time(150);\n\n// macOS OpenGL renderer fails to render larger texture\n// even though it reports that max texture size is 16384.\nconstexpr auto kMaxDisplayImageSize = 4096;\n\n// Preload X message ids before and after current.\nconstexpr auto kIdsLimit = 48;\n\n// Preload next messages if we went further from current than that.\nconstexpr auto kIdsPreloadAfter = 28;\n\nconstexpr auto kLeftSiblingTextureIndex = 1;\nconstexpr auto kRightSiblingTextureIndex = 2;\nconstexpr auto kStoriesControlsOpacity = 1.;\nconstexpr auto kStorySavePromoDuration = 3 * crl::time(1000);\n\nclass PipDelegate final : public Pip::Delegate {\npublic:\n\tPipDelegate(QWidget *parent, not_null<Main::Session*> session);\n\n\tvoid pipSaveGeometry(QByteArray geometry) override;\n\tQByteArray pipLoadGeometry() override;\n\tfloat64 pipPlaybackSpeed() override;\n\tQWidget *pipParentWidget() override;\n\nprivate:\n\tQWidget *_parent = nullptr;\n\tnot_null<Main::Session*> _session;\n\n};\n\n[[nodiscard]] Core::WindowPosition DefaultPosition() {\n\tconst auto moncrc = [&] {\n\t\tif (const auto active = Core::App().activeWindow()) {\n\t\t\tconst auto widget = active->widget();\n\t\t\tif (const auto screen = widget->screen()) {\n\t\t\t\treturn Platform::ScreenNameChecksum(screen->name());\n\t\t\t}\n\t\t}\n\t\treturn Core::App().settings().windowPosition().moncrc;\n\t}();\n\treturn {\n\t\t.moncrc = moncrc,\n\t\t.scale = cScale(),\n\t\t.x = st::mediaviewDefaultLeft,\n\t\t.y = st::mediaviewDefaultTop,\n\t\t.w = st::mediaviewDefaultWidth,\n\t\t.h = st::mediaviewDefaultHeight,\n\t};\n}\n\nPipDelegate::PipDelegate(QWidget *parent, not_null<Main::Session*> session)\n: _parent(parent)\n, _session(session) {\n}\n\nvoid PipDelegate::pipSaveGeometry(QByteArray geometry) {\n\tCore::App().settings().setVideoPipGeometry(geometry);\n\tCore::App().saveSettingsDelayed();\n}\n\nQByteArray PipDelegate::pipLoadGeometry() {\n\treturn Core::App().settings().videoPipGeometry();\n}\n\nfloat64 PipDelegate::pipPlaybackSpeed() {\n\treturn Core::App().settings().videoPlaybackSpeed();\n}\n\nQWidget *PipDelegate::pipParentWidget() {\n\treturn _parent;\n}\n\n[[nodiscard]] Images::Options VideoThumbOptions(DocumentData *document) {\n\tconst auto result = Images::Option::Blur;\n\treturn (document && document->isVideoMessage())\n\t\t? (result | Images::Option::RoundCircle)\n\t\t: result;\n}\n\n[[nodiscard]] QImage PrepareStaticImage(Images::ReadArgs &&args) {\n\tauto read = Images::Read(std::move(args));\n\treturn (read.image.width() > kMaxDisplayImageSize\n\t\t|| read.image.height() > kMaxDisplayImageSize)\n\t\t? read.image.scaled(\n\t\t\tkMaxDisplayImageSize,\n\t\t\tkMaxDisplayImageSize,\n\t\t\tQt::KeepAspectRatio,\n\t\t\tQt::SmoothTransformation)\n\t\t: read.image;\n}\n\n[[nodiscard]] bool IsSemitransparent(const QImage &image) {\n\tif (image.isNull()) {\n\t\treturn true;\n\t} else if (!image.hasAlphaChannel()) {\n\t\treturn false;\n\t}\n\tAssert(image.format() == QImage::Format_ARGB32_Premultiplied);\n\tconstexpr auto kAlphaMask = 0xFF000000;\n\tauto ints = reinterpret_cast<const uint32*>(image.bits());\n\tconst auto add = (image.bytesPerLine() / 4) - image.width();\n\tfor (auto y = 0; y != image.height(); ++y) {\n\t\tfor (auto till = ints + image.width(); ints != till; ++ints) {\n\t\t\tif ((*ints & kAlphaMask) != kAlphaMask) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\tints += add;\n\t}\n\treturn false;\n}\n\n[[nodiscard]] std::optional<WebPageCollage::Item> PollInputMediaItem(\n\t\tconst PollMedia &media) {\n\tif (media.photo) {\n\t\treturn WebPageCollage::Item(media.photo);\n\t} else if (media.document && !media.document->sticker()) {\n\t\treturn WebPageCollage::Item(media.document);\n\t}\n\treturn std::nullopt;\n}\n\n[[nodiscard]] std::optional<WebPageCollage::Item> PollAnswerMediaItem(\n\t\tconst PollAnswer &answer) {\n\treturn PollInputMediaItem(answer.media);\n}\n\n[[nodiscard]] bool IsPollAnswerMediaItem(\n\t\tnot_null<PollData*> poll,\n\t\tconst WebPageCollage::Item &item) {\n\treturn ranges::any_of(poll->answers, [&](const PollAnswer &answer) {\n\t\tconst auto candidate = PollAnswerMediaItem(answer);\n\t\treturn candidate && (*candidate == item);\n\t});\n}\n\n[[nodiscard]] std::optional<WebPageCollage> PollAnswersCollage(\n\t\tnot_null<PollData*> poll) {\n\tauto result = WebPageCollage();\n\tfor (const auto &answer : poll->answers) {\n\t\tif (const auto item = PollAnswerMediaItem(answer)) {\n\t\t\tresult.items.push_back(*item);\n\t\t}\n\t}\n\treturn result.items.empty()\n\t\t? std::nullopt\n\t\t: std::make_optional(std::move(result));\n}\n\n} // namespace\n\nclass OverlayWidget::SponsoredButton : public Ui::RippleButton {\npublic:\n\tSponsoredButton(QWidget *parent)\n\t: Ui::RippleButton(parent, st::mediaviewSponsoredButton.ripple) {\n\t}\n\n\tvoid setText(QString text) {\n\t\t_text = Ui::Text::String(\n\t\t\tst::mediaviewSponsoredButton.style,\n\t\t\tstd::move(text),\n\t\t\tkDefaultTextOptions,\n\t\t\twidth());\n\t\tresize(width(), _text.minHeight() * 2);\n\t}\n\tvoid setOpacity(float opacity) {\n\t\t_opacity = opacity;\n\t}\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override {\n\t\tauto p = QPainter(this);\n\t\tconst auto &st = st::mediaviewSponsoredButton;\n\n\t\tp.setOpacity(_opacity);\n\n\t\tconst auto over = Ui::AbstractButton::isOver();\n\t\tconst auto down = Ui::AbstractButton::isDown();\n\t\t{\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush((over || down) ? st.textBgOver : st.textBg);\n\t\t\tp.drawRoundedRect(\n\t\t\t\trect(),\n\t\t\t\tst::mediaviewCaptionRadius,\n\t\t\t\tst::mediaviewCaptionRadius);\n\t\t}\n\n\t\tUi::RippleButton::paintRipple(p, 0, 0);\n\n\t\tp.setPen(st.textFg);\n\t\tp.setBrush(Qt::NoBrush);\n\t\t_text.draw(p, {\n\t\t\t.position = QPoint(\n\t\t\t\t(width() - _text.maxWidth()) / 2,\n\t\t\t\t(height() - _text.minHeight()) / 2),\n\t\t\t.outerWidth = width(),\n\t\t\t.availableWidth = width(),\n\t\t});\n\t}\n\n\tQImage prepareRippleMask() const override {\n\t\treturn Ui::RippleAnimation::RoundRectMask(\n\t\t\tsize(),\n\t\t\tst::mediaviewCaptionRadius);\n\t}\n\tQPoint prepareRippleStartPosition() const override {\n\t\treturn mapFromGlobal(QCursor::pos())\n\t\t\t- rect::m::pos::tl(st::mediaviewSponsoredButton.padding);\n\t}\n\nprivate:\n\tUi::Text::String _text;\n\tfloat64 _opacity = 1.;\n\n};\n\n\nstruct OverlayWidget::SharedMedia {\n\tSharedMedia(SharedMediaKey key) : key(key) {\n\t}\n\n\tSharedMediaKey key;\n\trpl::lifetime lifetime;\n};\n\nstruct OverlayWidget::UserPhotos {\n\tUserPhotos(UserPhotosKey key) : key(key) {\n\t}\n\n\tUserPhotosKey key;\n\trpl::lifetime lifetime;\n};\n\nstruct OverlayWidget::Collage {\n\tCollage(CollageKey key) : key(key) {\n\t}\n\n\tCollageKey key;\n};\n\nstruct OverlayWidget::Streamed {\n\tStreamed(\n\t\tnot_null<DocumentData*> quality,\n\t\tnot_null<DocumentData*> original,\n\t\tHistoryItem *context,\n\t\tData::FileOrigin origin,\n\t\tFn<void()> waitingCallback);\n\tStreamed(\n\t\tnot_null<PhotoData*> photo,\n\t\tData::FileOrigin origin,\n\t\tFn<void()> waitingCallback);\n\n\tStreaming::Instance instance;\n\tstd::unique_ptr<PlaybackControls> controls;\n\tstd::unique_ptr<PlaybackSponsored> sponsored;\n\tstd::unique_ptr<base::PowerSaveBlocker> powerSaveBlocker;\n\n\tbool ready = false;\n\tbool withSound = false;\n\tbool pausedBySeek = false;\n\tbool resumeOnCallEnd = false;\n};\n\nstruct OverlayWidget::PipWrap {\n\tPipWrap(\n\t\tQWidget *parent,\n\t\tnot_null<DocumentData*> document,\n\t\tData::FileOrigin origin,\n\t\tnot_null<DocumentData*> chosenQuality,\n\t\tHistoryItem *context,\n\t\tVideoQuality quality,\n\t\tstd::shared_ptr<Streaming::Document> shared,\n\t\tFnMut<void()> closeAndContinue,\n\t\tFnMut<void()> destroy);\n\n\tPipWrap(const PipWrap &other) = delete;\n\tPipWrap &operator=(const PipWrap &other) = delete;\n\n\tPipDelegate delegate;\n\tPip wrapped;\n\trpl::lifetime lifetime;\n};\n\nstruct OverlayWidget::ItemContext {\n\tnot_null<HistoryItem*> item;\n\tMsgId topicRootId = 0;\n\tPeerId monoforumPeerId = 0;\n};\n\nstruct OverlayWidget::StoriesContext {\n\tnot_null<PeerData*> peer;\n\tStoryId id = 0;\n\tData::StoriesContext within;\n};\n\nclass OverlayWidget::Show final : public ChatHelpers::Show {\npublic:\n\texplicit Show(not_null<OverlayWidget*> widget) : _widget(widget) {\n\t}\n\n\tvoid activate() override {\n\t\tif (!_widget->isHidden()) {\n\t\t\t_widget->activate();\n\t\t}\n\t}\n\n\tvoid showOrHideBoxOrLayer(\n\t\t\tstd::variant<\n\t\t\t\tv::null_t,\n\t\t\t\tobject_ptr<Ui::BoxContent>,\n\t\t\t\tstd::unique_ptr<Ui::LayerWidget>> &&layer,\n\t\t\tUi::LayerOptions options,\n\t\t\tanim::type animated) const override {\n\t\t_widget->_layerBg->uiShow()->showOrHideBoxOrLayer(\n\t\t\tstd::move(layer),\n\t\t\toptions,\n\t\t\tanim::type::normal);\n\t}\n\tnot_null<QWidget*> toastParent() const override {\n\t\treturn _widget->_body;\n\t}\n\tbool valid() const override {\n\t\treturn _widget->_session || _widget->_storiesSession;\n\t}\n\toperator bool() const override {\n\t\treturn valid();\n\t}\n\n\tMain::Session &session() const override {\n\t\tExpects(_widget->_session || _widget->_storiesSession);\n\n\t\treturn _widget->_session\n\t\t\t? *_widget->_session\n\t\t\t: *_widget->_storiesSession;\n\t}\n\tbool paused(ChatHelpers::PauseReason reason) const override {\n\t\tif (_widget->isHidden()\n\t\t\t|| (!_widget->_fullscreen\n\t\t\t\t&& !_widget->_window->isActiveWindow())) {\n\t\t\treturn true;\n\t\t} else if (reason < ChatHelpers::PauseReason::Layer\n\t\t\t&& _widget->_layerBg->topShownLayer() != nullptr) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\trpl::producer<> pauseChanged() const override {\n\t\treturn rpl::never<>();\n\t}\n\n\trpl::producer<bool> adjustShadowLeft() const override {\n\t\treturn rpl::single(false);\n\t}\n\tSendMenu::Details sendMenuDetails() const override {\n\t\treturn { SendMenu::Type::SilentOnly };\n\t}\n\n\tbool showMediaPreview(\n\t\t\tData::FileOrigin origin,\n\t\t\tnot_null<DocumentData*> document) const override {\n\t\treturn false; // #TODO stories\n\t}\n\tbool showMediaPreview(\n\t\t\tData::FileOrigin origin,\n\t\t\tnot_null<PhotoData*> photo) const override {\n\t\treturn false; // #TODO stories\n\t}\n\n\tvoid processChosenSticker(\n\t\t\tChatHelpers::FileChosen &&chosen) const override {\n\t\t_widget->_storiesStickerOrEmojiChosen.fire(std::move(chosen));\n\t}\n\nprivate:\n\tnot_null<OverlayWidget*> _widget;\n\n};\n\nOverlayWidget::Streamed::Streamed(\n\tnot_null<DocumentData*> quality,\n\tnot_null<DocumentData*> original,\n\tHistoryItem *context,\n\tData::FileOrigin origin,\n\tFn<void()> waitingCallback)\n: instance(quality, original, context, origin, std::move(waitingCallback)) {\n}\n\nOverlayWidget::Streamed::Streamed(\n\tnot_null<PhotoData*> photo,\n\tData::FileOrigin origin,\n\tFn<void()> waitingCallback)\n: instance(photo, origin, std::move(waitingCallback)) {\n}\n\nOverlayWidget::PipWrap::PipWrap(\n\tQWidget *parent,\n\tnot_null<DocumentData*> document,\n\tData::FileOrigin origin,\n\tnot_null<DocumentData*> chosenQuality,\n\tHistoryItem *context,\n\tVideoQuality quality,\n\tstd::shared_ptr<Streaming::Document> shared,\n\tFnMut<void()> closeAndContinue,\n\tFnMut<void()> destroy)\n: delegate(parent, &document->session())\n, wrapped(\n\t&delegate,\n\tdocument,\n\torigin,\n\tchosenQuality,\n\tcontext,\n\tquality,\n\tstd::move(shared),\n\tstd::move(closeAndContinue),\n\tstd::move(destroy)) {\n}\n\nOverlayWidget::OverlayWidget()\n: _wrap(std::make_unique<Ui::GL::Window>(Ui::GL::Window::Translucent::Yes))\n, _window(_wrap->window())\n, _helper(Platform::CreateOverlayWidgetHelper(_window.get(), [=](bool maximized) {\n\ttoggleFullScreen(maximized);\n}))\n, _body(_wrap->widget())\n, _titleBugWorkaround(std::make_unique<Ui::RpWidget>(_body))\n, _surface(\n\tUi::GL::CreateSurface(_body, chooseRenderer(_wrap->backend())))\n, _widget(_surface->rpWidget())\n, _fullscreen(Core::App().settings().mediaViewPosition().maximized == 2)\n, _windowed(Core::App().settings().mediaViewPosition().maximized == 0)\n, _quality(Core::App().settings().videoQuality())\n, _layerBg(std::make_unique<Ui::LayerManager>(_body))\n, _docDownload(_body, tr::lng_media_download(tr::now), st::mediaviewFileLink)\n, _docSaveAs(_body, tr::lng_mediaview_save_as(tr::now), st::mediaviewFileLink)\n, _docCancel(_body, tr::lng_cancel(tr::now), st::mediaviewFileLink)\n, _radial([=](crl::time now) { return radialAnimationCallback(now); })\n, _lastAction(-st::mediaviewDeltaFromLastAction, -st::mediaviewDeltaFromLastAction)\n, _stateAnimation([=](crl::time now) { return stateAnimationCallback(now); })\n, _dropdown(_body, st::mediaviewDropdownMenu)\n, _speedBoostTicker([=] {\n\tupdateSpeedBoost();\n\treturn isSpeedBoostShown();\n}) {\n\t_layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox);\n\t_layerBg->setHideByBackgroundClick(true);\n\n\tCrashReports::SetAnnotation(\"OpenGL Renderer\", \"[not-initialized]\");\n\n\tLang::Updated(\n\t) | rpl::on_next([=] {\n\t\trefreshLang();\n\t}, lifetime());\n\n\t_lastPositiveVolume = (Core::App().settings().videoVolume() > 0.)\n\t\t? Core::App().settings().videoVolume()\n\t\t: Core::Settings::kDefaultVolume;\n\n\t_saveMsgTimer.setCallback([=, delay = st::mediaviewSaveMsgHiding] {\n\t\t_saveMsgAnimation.start([=] { updateSaveMsg(); }, 1., 0., delay);\n\t});\n\n\t_chapterTimer.setCallback([=, delay = st::mediaviewChapterHiding] {\n\t\t_chapterAnimation.start([=] { updateChapter(); }, 1., 0., delay);\n\t});\n\n\t_speedBoostHoldTimer.setCallback([=] {\n\t\tstartSpeedBoost();\n\t});\n\n\t_frameStepThrottle.setCallback([=] {\n\t\tflushPendingFrameStep();\n\t});\n\n\t_docRectImage = QImage(\n\t\tst::mediaviewFileSize * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_docRectImage.setDevicePixelRatio(style::DevicePixelRatio());\n\n\tShortcuts::Requests(\n\t) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {\n\t\trequest->check(\n\t\t\tShortcuts::Command::MediaViewerFullscreen\n\t\t) && request->handle([=] {\n\t\t\tif (_streamed) {\n\t\t\t\tplaybackToggleFullScreen();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t}, lifetime());\n\n\tsetupWindow();\n\n\tconst auto mousePosition = [](not_null<QEvent*> e) {\n\t\treturn static_cast<QMouseEvent*>(e.get())->pos();\n\t};\n\tconst auto mouseButton = [](not_null<QEvent*> e) {\n\t\treturn static_cast<QMouseEvent*>(e.get())->button();\n\t};\n\tbase::install_event_filter(_window, [=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::Move) {\n\t\t\tconst auto position = static_cast<QMoveEvent*>(e.get())->pos();\n\t\t\tDEBUG_LOG((\"Viewer Pos: Moved to %1, %2\")\n\t\t\t\t.arg(position.x())\n\t\t\t\t.arg(position.y()));\n\t\t\tif (_windowed) {\n\t\t\t\tsavePosition();\n\t\t\t} else {\n\t\t\t\tmoveToScreen(true);\n\t\t\t}\n\t\t} else if (type == QEvent::Resize) {\n\t\t\tconst auto size = static_cast<QResizeEvent*>(e.get())->size();\n\t\t\tDEBUG_LOG((\"Viewer Pos: Resized to %1, %2\")\n\t\t\t\t.arg(size.width())\n\t\t\t\t.arg(size.height()));\n\t\t\tif (_windowed) {\n\t\t\t\tsavePosition();\n\t\t\t}\n\t\t} else if (type == QEvent::Close\n\t\t\t&& !Core::Sandbox::Instance().isSavingSession()\n\t\t\t&& !Core::Quitting()) {\n\t\t\te->ignore();\n\t\t\tclose();\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t} else if (type == QEvent::ThemeChange && Platform::IsLinux()) {\n\t\t\t_window->setWindowIcon(Window::CreateIcon(_session));\n\t\t} else if (type == QEvent::ContextMenu) {\n\t\t\tconst auto event = static_cast<QContextMenuEvent*>(e.get());\n\t\t\tconst auto mouse = (event->reason() == QContextMenuEvent::Mouse);\n\t\t\tconst auto position = mouse\n\t\t\t\t? std::make_optional(event->pos())\n\t\t\t\t: std::nullopt;\n\t\t\tif (handleContextMenu(position)) {\n\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t}\n\t\t} else if (e->type() == QEvent::WindowStateChange) {\n\t\t\tconst auto state = window()->windowState();\n\t\t\tif (state == Qt::WindowMinimized || Platform::IsMac()) {\n\t\t\t} else if (state == Qt::WindowMaximized) {\n\t\t\t\tif (_fullscreen || _windowed) {\n\t\t\t\t\t_fullscreen = _windowed = false;\n\t\t\t\t\tsavePosition();\n\t\t\t\t}\n\t\t\t} else if (_fullscreen || _windowed) {\n\t\t\t} else if (state == Qt::WindowFullScreen) {\n\t\t\t\t_fullscreen = true;\n\t\t\t\tsavePosition();\n\t\t\t} else {\n\t\t\t\t_windowed = true;\n\t\t\t\tsavePosition();\n\t\t\t}\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\tbase::install_event_filter(_body, [=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::Resize) {\n\t\t\tconst auto size = static_cast<QResizeEvent*>(e.get())->size();\n\n\t\t\t// Somehow Windows 11 knows the geometry of first widget below\n\t\t\t// the semi-native title control widgets and it uses\n\t\t\t// it's geometry to show the snap grid popup around it when\n\t\t\t// you put the mouse over the Maximize button. In the 4.6.4 beta\n\t\t\t// the first widget was `_widget`, so the popup was shown\n\t\t\t// either above the window or, if not enough space above, below\n\t\t\t// the whole window, you couldn't even put the mouse on it.\n\t\t\t//\n\t\t\t// So now here is this weird workaround that places our\n\t\t\t// `_titleBugWorkaround` widget as the first one under the title\n\t\t\t// controls and the system shows the popup around its geometry,\n\t\t\t// so we set it's height to the title controls height\n\t\t\t// and everything works as expected.\n\t\t\t//\n\t\t\t// This doesn't make sense. But it works. :shrug:\n\t\t\t_titleBugWorkaround->setGeometry(\n\t\t\t\t{ 0, 0, size.width(), st::mediaviewTitleButton.height });\n\n\t\t\t_widget->setGeometry({ QPoint(), size });\n\t\t\tupdateControlsGeometry();\n\t\t} else if (type == QEvent::KeyPress) {\n\t\t\thandleKeyPress(static_cast<QKeyEvent*>(e.get()));\n\t\t} else if (type == QEvent::KeyRelease) {\n\t\t\thandleKeyRelease(static_cast<QKeyEvent*>(e.get()));\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\tbase::install_event_filter(_widget, [=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::Leave) {\n\t\t\tif (_over != Over::None) {\n\t\t\t\tupdateOverState(Over::None);\n\t\t\t}\n\t\t} else if (type == QEvent::MouseButtonPress) {\n\t\t\thandleMousePress(mousePosition(e), mouseButton(e));\n\t\t} else if (type == QEvent::MouseButtonRelease) {\n\t\t\thandleMouseRelease(mousePosition(e), mouseButton(e));\n\t\t} else if (type == QEvent::MouseMove) {\n\t\t\thandleMouseMove(mousePosition(e));\n\t\t} else if (type == QEvent::MouseButtonDblClick) {\n\t\t\tif (handleDoubleClick(mousePosition(e), mouseButton(e))) {\n\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t} else {\n\t\t\t\thandleMousePress(mousePosition(e), mouseButton(e));\n\t\t\t}\n\t\t} else if (type == QEvent::TouchBegin\n\t\t\t|| type == QEvent::TouchUpdate\n\t\t\t|| type == QEvent::TouchEnd\n\t\t\t|| type == QEvent::TouchCancel) {\n\t\t\tif (handleTouchEvent(static_cast<QTouchEvent*>(e.get()))) {\n\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t}\n\t\t} else if (type == QEvent::Wheel) {\n\t\t\thandleWheelEvent(static_cast<QWheelEvent*>(e.get()));\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\t_helper->mouseEvents(\n\t) | rpl::on_next([=](not_null<QMouseEvent*> e) {\n\t\tif (_helper->skipTitleHitTest(e->windowPos().toPoint())) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto type = e->type();\n\t\tconst auto position = e->pos();\n\t\tif (type == QEvent::MouseButtonPress) {\n\t\t\thandleMousePress(position, e->button());\n\t\t} else if (type == QEvent::MouseButtonRelease) {\n\t\t\thandleMouseRelease(position, e->button());\n\t\t} else if (type == QEvent::MouseMove) {\n\t\t\thandleMouseMove(position);\n\t\t} else if (type == QEvent::MouseButtonDblClick) {\n\t\t\tif (!handleDoubleClick(position, e->button())) {\n\t\t\t\thandleMousePress(position, e->button());\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\t_topShadowRight = _helper->controlsSideRightValue();\n\t_topShadowRight.changes(\n\t) | rpl::on_next([=] {\n\t\tupdateControlsGeometry();\n\t\tupdate();\n\t}, lifetime());\n\n\t_helper->topNotchSkipValue(\n\t) | rpl::on_next([=](int notch) {\n\t\tif (_topNotchSize != notch) {\n\t\t\t_topNotchSize = notch;\n\t\t\tif (_fullscreen) {\n\t\t\t\tupdateControlsGeometry();\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\t_window->setTitle(tr::lng_mediaview_title(tr::now));\n\t_window->setTitleStyle(st::mediaviewTitle);\n\n\tif constexpr (Platform::IsMac()) {\n\t\t// Without Qt::Tool starting with Qt 5.15.1 this widget\n\t\t// when being opened from a fullscreen main window was\n\t\t// opening not as overlay over the main window, but as\n\t\t// a separate fullscreen window with a separate space.\n\t\t_window->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool);\n\t}\n\t_widget->setMouseTracking(true);\n\n\t_window->screenValue(\n\t) | rpl::skip(1) | rpl::on_next([=](not_null<QScreen*> screen) {\n\t\thandleScreenChanged(screen);\n\t}, lifetime());\n\tsubscribeToScreenGeometry();\n\tupdateGeometry();\n\tupdateControlsGeometry();\n\n#ifdef Q_OS_MAC\n\tTouchBar::SetupMediaViewTouchBar(\n\t\t_window->winId(),\n\t\tstatic_cast<PlaybackControls::Delegate*>(this),\n\t\t_touchbarTrackState.events(),\n\t\t_touchbarDisplay.events(),\n\t\t_touchbarFullscreenToggled.events());\n#endif // Q_OS_MAC\n\n\tusing namespace rpl::mappers;\n\trpl::combine(\n\t\tCore::App().calls().currentCallValue(),\n\t\tCore::App().calls().currentGroupCallValue(),\n\t\t_1 || _2\n\t) | rpl::on_next([=](bool call) {\n\t\tif (!_streamed\n\t\t\t|| !_document\n\t\t\t|| (_document->isAnimation() && !_document->isVideoMessage())) {\n\t\t\treturn;\n\t\t} else if (call) {\n\t\t\tplaybackPauseOnCall();\n\t\t} else {\n\t\t\tplaybackResumeOnCall();\n\t\t}\n\t}, lifetime());\n\n\t_widget->setAttribute(Qt::WA_AcceptTouchEvents);\n\t_touchTimer.setCallback([=] { handleTouchTimer(); });\n\n\t_controlsHideTimer.setCallback([=] { hideControls(); });\n\t_helper->controlsActivations(\n\t) | rpl::on_next([=] {\n\t\tactivateControls();\n\t}, lifetime());\n\n\t_docDownload->addClickHandler([=] { downloadMedia(); });\n\t_docSaveAs->addClickHandler([=] { saveAs(); });\n\t_docCancel->addClickHandler([=] { saveCancel(); });\n\n\t_dropdown->setHiddenCallback([this] { dropdownHidden(); });\n\t_dropdownShowTimer.setCallback([=] { showDropdown(); });\n\n\torderWidgets();\n}\n\nvoid OverlayWidget::showSaveMsgToast(const QString &path, auto phrase) {\n\tshowSaveMsgToastWith(\n\t\tpath,\n\t\tphrase(\n\t\t\ttr::now,\n\t\t\tlt_downloads,\n\t\t\ttr::link(\n\t\t\t\ttr::lng_mediaview_downloads(tr::now),\n\t\t\t\t\"internal:show_saved_message\"),\n\t\t\ttr::marked),\n\t\tst::mediaviewSaveMsgShown);\n}\n\nvoid OverlayWidget::showSaveMsgToastWith(\n\t\tconst QString &path,\n\t\tconst TextWithEntities &text,\n\t\tcrl::time duration) {\n\t_saveMsgFilename = path;\n\t_saveMsgText.setMarkedText(st::mediaviewSaveMsgStyle, text);\n\tconst auto w = _saveMsgText.maxWidth()\n\t\t+ st::mediaviewSaveMsgPadding.left()\n\t\t+ st::mediaviewSaveMsgPadding.right();\n\tconst auto h = st::mediaviewSaveMsgStyle.font->height\n\t\t+ st::mediaviewSaveMsgPadding.top()\n\t\t+ st::mediaviewSaveMsgPadding.bottom();\n\t_saveMsg = QRect(\n\t\t(width() - w) / 2,\n\t\t_minUsedTop + (_maxUsedHeight - h) / 2,\n\t\tw,\n\t\th);\n\tconst auto callback = [=](float64 value) {\n\t\tupdateSaveMsg();\n\t\tif (!_saveMsgAnimation.animating()) {\n\t\t\t_saveMsgTimer.callOnce(duration);\n\t\t}\n\t};\n\tconst auto animDuration = st::mediaviewSaveMsgShowing;\n\t_saveMsgAnimation.start(callback, 0., 1., animDuration);\n\tupdateSaveMsg();\n}\n\nvoid OverlayWidget::orderWidgets() {\n\t_helper->orderWidgets();\n}\n\nvoid OverlayWidget::setupWindow() {\n\t_window->setBodyTitleArea([=](QPoint widgetPoint) {\n\t\tusing Flag = Ui::WindowTitleHitTestFlag;\n\t\tUi::WindowTitleHitTestFlags result;\n\t\tif (!_widget->rect().contains(widgetPoint)\n\t\t\t|| _helper->skipTitleHitTest(_widget->mapTo(_window, widgetPoint))) {\n\t\t\treturn result;\n\t\t}\n\t\tif (widgetPoint.y() <= st::mediaviewTitleButton.height) {\n\t\t\tresult |= Flag::Menu | Flag::FullScreen;\n\t\t}\n\t\tconst auto inControls = ((_over != Over::None) && (_over != Over::Video));\n\t\tif (inControls\n\t\t\t|| (_streamed\n\t\t\t\t&& _streamed->controls\n\t\t\t\t&& _streamed->controls->dragging())) {\n\t\t} else if ((_w > _widget->width() || _h > _maxUsedHeight)\n\t\t\t\t&& (widgetPoint.y() > st::mediaviewHeaderTop)\n\t\t\t\t&& QRect(_x, _y, _w, _h).contains(widgetPoint)) {\n\t\t} else if (_stories && _stories->ignoreWindowMove(widgetPoint)) {\n\t\t} else if (_sponsoredButton\n\t\t\t&& _sponsoredButton->geometry().contains(widgetPoint)) {\n\t\t} else if (_windowed) {\n\t\t\tresult |= Flag::Move;\n\t\t}\n\t\treturn result;\n\t});\n\n\t_window->setAttribute(Qt::WA_NoSystemBackground, true);\n\t_window->setAttribute(Qt::WA_TranslucentBackground, true);\n\n\t_window->setMinimumSize(\n\t\t{ st::mediaviewMinWidth, st::mediaviewMinHeight });\n\n\t_window->shownValue(\n\t) | rpl::on_next([=](bool shown) {\n\t\ttoggleApplicationEventFilter(shown);\n\t\tif (!shown) {\n\t\t\tclearAfterHide();\n\t\t} else {\n\t\t\tconst auto geometry = _window->geometry();\n\t\t\tconst auto screenList = QGuiApplication::screens();\n\t\t\tDEBUG_LOG((\"Viewer Pos: Shown, geometry: %1, %2, %3, %4, screen number: %5\")\n\t\t\t\t.arg(geometry.x())\n\t\t\t\t.arg(geometry.y())\n\t\t\t\t.arg(geometry.width())\n\t\t\t\t.arg(geometry.height())\n\t\t\t\t.arg(screenList.indexOf(_window->screen())));\n\t\t\tmoveToScreen();\n\t\t}\n\t}, lifetime());\n}\n\nvoid OverlayWidget::refreshLang() {\n\tInvokeQueued(_widget, [=] { updateThemePreviewGeometry(); });\n}\n\nvoid OverlayWidget::moveToScreen(bool inMove) {\n\tif (!_fullscreen || _wasWindowedMode) {\n\t\treturn;\n\t}\n\tconst auto applicationWindow = Core::App().activeWindow()\n\t\t? Core::App().activeWindow()->widget().get()\n\t\t: nullptr;\n\tconst auto activeWindowScreen = applicationWindow\n\t\t? applicationWindow->screen()\n\t\t: nullptr;\n\tconst auto myScreen = _window->screen();\n\tif (activeWindowScreen && myScreen != activeWindowScreen) {\n\t\tconst auto screenList = QGuiApplication::screens();\n\t\tDEBUG_LOG((\"Viewer Pos: Currently on screen %1, moving to screen %2\")\n\t\t\t.arg(screenList.indexOf(myScreen))\n\t\t\t.arg(screenList.indexOf(activeWindowScreen)));\n#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)\n\t\t_window->setScreen(activeWindowScreen);\n#else // Qt >= 6.0.0\n\t\t_window->createWinId();\n\t\twindow()->setScreen(activeWindowScreen);\n#endif // Qt < 6.0.0\n\t\tDEBUG_LOG((\"Viewer Pos: New actual screen: %1\")\n\t\t\t.arg(screenList.indexOf(_window->screen())));\n\t}\n\tupdateGeometry(inMove);\n}\n\nvoid OverlayWidget::initFullScreen() {\n\tif (_fullscreenInited) {\n\t\treturn;\n\t}\n\t_fullscreenInited = true;\n\tswitch (Core::App().settings().mediaViewPosition().maximized) {\n\tcase 2:\n\t\t_fullscreen = true;\n\t\t_windowed = false;\n\t\tbreak;\n\tcase 1:\n\t\t_fullscreen = Platform::IsMac();\n\t\t_windowed = false;\n\t\tbreak;\n\t}\n}\n\nvoid OverlayWidget::initNormalGeometry() {\n\tif (_normalGeometryInited) {\n\t\treturn;\n\t}\n\t_normalGeometryInited = true;\n\tconst auto saved = Core::App().settings().mediaViewPosition();\n\tconst auto adjusted = Core::AdjustToScale(saved, u\"Viewer\"_q);\n\tconst auto initial = DefaultPosition();\n\t_normalGeometry = Window::CountInitialGeometry(\n\t\t_window,\n\t\tadjusted,\n\t\tinitial,\n\t\t{ st::mediaviewMinWidth, st::mediaviewMinHeight },\n\t\tu\"Viewer\"_q);\n}\n\nvoid OverlayWidget::savePosition() {\n\tif (isHidden() || isMinimized() || !_normalGeometryInited) {\n\t\treturn;\n\t}\n\tconst auto &savedPosition = Core::App().settings().mediaViewPosition();\n\tauto realPosition = savedPosition;\n\tif (_fullscreen) {\n\t\trealPosition.maximized = 2;\n\t\trealPosition.moncrc = 0;\n\t\tDEBUG_LOG((\"Viewer Pos: Saving fullscreen position.\"));\n\t} else if (!_windowed) {\n\t\trealPosition.maximized = 1;\n\t\trealPosition.moncrc = 0;\n\t\tDEBUG_LOG((\"Viewer Pos: Saving maximized position.\"));\n\t} else if (!_wasWindowedMode) {\n\t\treturn;\n\t} else {\n\t\tauto r = _normalGeometry = _window->body()->mapToGlobal(\n\t\t\t_window->body()->rect());\n\t\trealPosition.x = r.x();\n\t\trealPosition.y = r.y();\n\t\trealPosition.w = r.width();\n\t\trealPosition.h = r.height();\n\t\trealPosition.scale = cScale();\n\t\trealPosition.maximized = 0;\n\t\trealPosition.moncrc = 0;\n\t\tDEBUG_LOG((\"Viewer Pos: \"\n\t\t\t\"Saving non-maximized position: %1, %2, %3, %4\"\n\t\t\t).arg(realPosition.x\n\t\t\t).arg(realPosition.y\n\t\t\t).arg(realPosition.w\n\t\t\t).arg(realPosition.h));\n\t}\n\trealPosition = Window::PositionWithScreen(\n\t\trealPosition,\n\t\t_window,\n\t\t{ st::mediaviewMinWidth, st::mediaviewMinHeight },\n\t\tu\"Viewer\"_q);\n\tif (realPosition.w >= st::mediaviewMinWidth\n\t\t&& realPosition.h >= st::mediaviewMinHeight\n\t\t&& realPosition != savedPosition) {\n\t\tDEBUG_LOG((\"Viewer Pos: \"\n\t\t\t\"Writing: %1, %2, %3, %4 (scale %5%, maximized %6)\")\n\t\t\t.arg(realPosition.x)\n\t\t\t.arg(realPosition.y)\n\t\t\t.arg(realPosition.w)\n\t\t\t.arg(realPosition.h)\n\t\t\t.arg(realPosition.scale)\n\t\t\t.arg(Logs::b(realPosition.maximized)));\n\t\tCore::App().settings().setMediaViewPosition(realPosition);\n\t\tCore::App().saveSettingsDelayed();\n\t}\n}\n\nvoid OverlayWidget::updateGeometry(bool inMove) {\n\tinitFullScreen();\n\tif (_fullscreen) {\n\t\tupdateGeometryToScreen(inMove);\n\t} else if (_windowed && _normalGeometryInited) {\n\t\tconst auto setGeometry = [=](QRect geometry) {\n\t\t\tDEBUG_LOG((\"Viewer Pos: Setting %1, %2, %3, %4\")\n\t\t\t\t.arg(geometry.x())\n\t\t\t\t.arg(geometry.y())\n\t\t\t\t.arg(geometry.width())\n\t\t\t\t.arg(geometry.height()));\n\t\t\t_window->setGeometry(geometry);\n\t\t};\n\t\tconst auto geometry = _normalGeometry;\n\t\tsetGeometry(geometry);\n\t\tif constexpr (Platform::IsMac()) {\n\t\t\t// Either macOS or Qt immediately overwrite the geometry for\n\t\t\t// some time (perhaps until the window is really on screen),\n\t\t\t// try to set again later in event loop\n\t\t\tInvokeQueued(_window, [=] {\n\t\t\t\tsetGeometry(geometry);\n\t\t\t});\n\t\t}\n\t}\n\tif constexpr (!Platform::IsMac()) {\n\t\tif (_fullscreen) {\n\t\t\tif (!isHidden() && !isMinimized()) {\n\t\t\t\t_window->showFullScreen();\n\t\t\t}\n\t\t} else if (!_windowed) {\n\t\t\tif (!isHidden() && !isMinimized()) {\n\t\t\t\t_window->showMaximized();\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::updateGeometryToScreen(bool inMove) {\n\tconst auto available = _window->screen()->geometry();\n\tif (_window->geometry() == available) {\n\t\treturn;\n\t}\n\tDEBUG_LOG((\"Viewer Pos: Setting %1, %2, %3, %4\")\n\t\t.arg(available.x())\n\t\t.arg(available.y())\n\t\t.arg(available.width())\n\t\t.arg(available.height()));\n\t_window->Ui::RpWidget::setGeometry(available);\n}\n\nvoid OverlayWidget::updateControlsGeometry() {\n\tupdateNavigationControlsGeometry();\n\n\t_saveMsg.moveTo(\n\t\t(width() - _saveMsg.width()) / 2,\n\t\t_minUsedTop + (_maxUsedHeight - _saveMsg.height()) / 2);\n\t_photoRadialRect = QRect(\n\t\tQPoint(\n\t\t\t(width() - st::radialSize.width()) / 2,\n\t\t\t_minUsedTop + (_maxUsedHeight - st::radialSize.height()) / 2),\n\t\tst::radialSize);\n\n\tconst auto bottom = st::mediaviewShadowBottom.height();\n\tconst auto top = st::mediaviewShadowTop.size();\n\t_bottomShadowRect = QRect(0, height() - bottom, width(), bottom);\n\t_topShadowRect = QRect(\n\t\tQPoint(topShadowOnTheRight() ? (width() - top.width()) : 0, 0),\n\t\ttop);\n\n\tif (_dropdown && !_dropdown->isHidden()) {\n\t\t_dropdown->moveToRight(0, height() - _dropdown->height());\n\t}\n\n\tupdateControls();\n\tresizeContentByScreenSize();\n\tupdate();\n}\n\nvoid OverlayWidget::updateNavigationControlsGeometry() {\n\t_minUsedTop = topNotchSkip();\n\t_maxUsedHeight = height() - _minUsedTop;\n\n\tconst auto overRect = QRect(\n\t\tQPoint(),\n\t\tQSize(st::mediaviewIconOver, st::mediaviewIconOver));\n\tconst auto navSize = _stories\n\t\t? st::storiesControlSize\n\t\t: st::mediaviewControlSize;\n\tconst auto navSkip = st::mediaviewHeaderTop;\n\tconst auto xLeft = _stories ? (_x - navSize) : 0;\n\tconst auto xRight = _stories ? (_x + _w) : (width() - navSize);\n\t_leftNav = QRect(\n\t\txLeft,\n\t\t_minUsedTop + navSkip,\n\t\tnavSize,\n\t\t_maxUsedHeight - 2 * navSkip);\n\t_leftNavOver = _stories\n\t\t? QRect()\n\t\t: style::centerrect(_leftNav, overRect);\n\t_leftNavIcon = style::centerrect(\n\t\t_leftNav,\n\t\t_stories ? st::storiesLeft : st::mediaviewLeft);\n\t_rightNav = QRect(\n\t\txRight,\n\t\t_minUsedTop + navSkip,\n\t\tnavSize,\n\t\t_maxUsedHeight - 2 * navSkip);\n\t_rightNavOver = _stories\n\t\t? QRect()\n\t\t: style::centerrect(_rightNav, overRect);\n\t_rightNavIcon = style::centerrect(\n\t\t_rightNav,\n\t\t_stories ? st::storiesRight : st::mediaviewRight);\n}\n\nbool OverlayWidget::topShadowOnTheRight() const {\n\treturn _topShadowRight.current();\n}\n\nQSize OverlayWidget::flipSizeByRotation(QSize size) const {\n\treturn FlipSizeByRotation(size, _rotation);\n}\n\nbool OverlayWidget::hasCopyMediaRestriction(bool skipPremiumCheck) const {\n\tif (const auto story = _stories ? _stories->story() : nullptr) {\n\t\tif (story->call()) {\n\t\t\treturn true;\n\t\t}\n\t\treturn skipPremiumCheck\n\t\t\t? !story->canDownloadIfPremium()\n\t\t\t: !story->canDownloadChecked();\n\t}\n\treturn (_history && !_history->peer->allowsForwarding())\n\t\t|| (_message && _message->forbidsSaving());\n}\n\nbool OverlayWidget::showCopyMediaRestriction(bool skipPRemiumCheck) {\n\tif (!hasCopyMediaRestriction(skipPRemiumCheck)) {\n\t\treturn false;\n\t} else if (_stories) {\n\t\tuiShow()->showToast(tr::lng_error_nocopy_story(tr::now));\n\t} else if (_history) {\n\t\tuiShow()->showToast(_history->peer->isBroadcast()\n\t\t\t? tr::lng_error_nocopy_channel(tr::now)\n\t\t\t: _history->peer->isUser()\n\t\t\t? tr::lng_error_nocopy_user(tr::now)\n\t\t\t: tr::lng_error_nocopy_group(tr::now));\n\t}\n\treturn true;\n}\n\nbool OverlayWidget::videoShown() const {\n\treturn _streamed\n\t\t&& _streamed->ready\n\t\t&& !_streamed->instance.info().video.cover.isNull();\n}\n\nQSize OverlayWidget::videoSize() const {\n\tExpects(videoShown());\n\n\tconst auto use = (_document && _chosenQuality != _document)\n\t\t? _document->dimensions\n\t\t: _streamed->instance.info().video.size;\n\treturn flipSizeByRotation(use);\n}\n\nbool OverlayWidget::streamingRequiresControls() const {\n\treturn !_stories\n\t\t&& _document\n\t\t&& (!_document->isAnimation() || _document->isVideoMessage());\n}\n\nQImage OverlayWidget::videoFrame() const {\n\tExpects(videoShown());\n\n\tauto request = Streaming::FrameRequest();\n\t//request.radius = (_document && _document->isVideoMessage())\n\t//\t? ImageRoundRadius::Ellipse\n\t//\t: ImageRoundRadius::None;\n\treturn _streamed->instance.player().ready()\n\t\t? _streamed->instance.frame(request)\n\t\t: _streamed->instance.info().video.cover;\n}\n\nStreaming::FrameWithInfo OverlayWidget::videoFrameWithInfo() const {\n\tExpects(videoShown());\n\n\treturn _streamed->instance.player().ready()\n\t\t? _streamed->instance.frameWithInfo()\n\t\t: Streaming::FrameWithInfo{\n\t\t\t.image = _streamed->instance.info().video.cover,\n\t\t\t.format = Streaming::FrameFormat::ARGB32,\n\t\t\t.index = -2,\n\t\t\t.alpha = _streamed->instance.info().video.alpha,\n\t\t};\n}\n\nQImage OverlayWidget::currentVideoFrameImage() const {\n\treturn _streamed->instance.player().ready()\n\t\t? _streamed->instance.player().currentFrameImage()\n\t\t: _streamed->instance.info().video.cover;\n}\n\nint OverlayWidget::streamedIndex() const {\n\treturn _streamedCreated;\n}\n\nbool OverlayWidget::documentContentShown() const {\n\treturn _document && (!_staticContent.isNull() || videoShown());\n}\n\nbool OverlayWidget::documentBubbleShown() const {\n\treturn (!_photo && !_document)\n\t\t|| (_document\n\t\t\t&& !_themePreviewShown\n\t\t\t&& !_streamed\n\t\t\t&& _staticContent.isNull());\n}\n\nvoid OverlayWidget::setStaticContent(QImage image) {\n\tconstexpr auto kGood = QImage::Format_ARGB32_Premultiplied;\n\tif (!image.isNull()\n\t\t&& image.format() != kGood\n\t\t&& image.format() != QImage::Format_RGB32) {\n\t\timage = std::move(image).convertToFormat(kGood);\n\t}\n\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\tif (_flip) {\n\t\timage = image.mirrored(_flip & Qt::Horizontal, _flip & Qt::Vertical);\n\t}\n\t_staticContent = std::move(image);\n\t_staticContentTransparent = IsSemitransparent(_staticContent);\n}\n\nbool OverlayWidget::contentShown() const {\n\treturn _photo || documentContentShown();\n}\n\nbool OverlayWidget::opaqueContentShown() const {\n\treturn contentShown()\n\t\t&& (!_staticContentTransparent\n\t\t\t|| !_document\n\t\t\t|| (!_document->isVideoMessage()\n\t\t\t\t&& !_document->sticker()\n\t\t\t\t&& (!_streamed || !_streamed->instance.info().video.alpha)));\n}\n\nvoid OverlayWidget::clearStreaming(bool savePosition) {\n\tif (_streamed && _document && savePosition) {\n\t\tMedia::Player::SaveLastPlaybackPosition(\n\t\t\t_document,\n\t\t\t_streamed->instance.player().prepareLegacyState());\n\t}\n\t_fullScreenVideo = false;\n\t_streamed = nullptr;\n}\n\nvoid OverlayWidget::documentUpdated(not_null<DocumentData*> document) {\n\tif (_document != document) {\n\t\treturn;\n\t} else if (documentBubbleShown()) {\n\t\tif ((_document->loading() && _docCancel->isHidden())\n\t\t\t|| (!_document->loading() && !_docCancel->isHidden())) {\n\t\t\tupdateControls();\n\t\t} else if (_document->loading()) {\n\t\t\tupdateDocSize();\n\t\t\t_widget->update(_docRect);\n\t\t}\n\t} else if (_streamed && _streamed->controls) {\n\t\tconst auto ready = _documentMedia->loaded()\n\t\t\t? _document->size\n\t\t\t: _document->loading()\n\t\t\t? std::clamp(_document->loadOffset(), int64(), _document->size)\n\t\t\t: 0;\n\t\t_streamed->controls->setLoadingProgress(ready, _document->size);\n\t}\n\tif (_stories\n\t\t&& !_documentLoadingTo.isEmpty()\n\t\t&& _document->location(true).isEmpty()) {\n\t\tshowSaveMsgToast(\n\t\t\tbase::take(_documentLoadingTo),\n\t\t\ttr::lng_mediaview_video_saved_to);\n\t}\n}\n\nvoid OverlayWidget::changingMsgId(FullMsgId newId, MsgId oldId) {\n\tif (_message && _message->fullId() == newId) {\n\t\trefreshMediaViewer();\n\t}\n}\n\nvoid OverlayWidget::updateDocSize() {\n\tif (!_document || !documentBubbleShown()) {\n\t\treturn;\n\t}\n\n\tconst auto size = _document->size;\n\t_docSize = _document->loading()\n\t\t? Ui::FormatProgressText(_document->loadOffset(), size)\n\t\t: Ui::FormatSizeText(size);\n\t_docSizeWidth = st::mediaviewFont->width(_docSize);\n\tint32 maxw = st::mediaviewFileSize.width() - st::mediaviewFileIconSize - st::mediaviewFilePadding * 3;\n\tif (_docSizeWidth > maxw) {\n\t\t_docSize = st::mediaviewFont->elided(_docSize, maxw);\n\t\t_docSizeWidth = st::mediaviewFont->width(_docSize);\n\t}\n}\n\nvoid OverlayWidget::refreshNavVisibility() {\n\tif (_stories) {\n\t\t_leftNavVisible = _stories->subjumpAvailable(-1);\n\t\t_rightNavVisible = _stories->subjumpAvailable(1);\n\t} else if (_sharedMediaData) {\n\t\t_leftNavVisible = _index && (*_index > 0);\n\t\t_rightNavVisible = _index && (*_index + 1 < _sharedMediaData->size());\n\t} else if (_userPhotosData) {\n\t\t_leftNavVisible = _index && (*_index > 0);\n\t\t_rightNavVisible = _index && (*_index + 1 < _userPhotosData->size());\n\t} else if (_collageData) {\n\t\t_leftNavVisible = _index && (*_index > 0);\n\t\t_rightNavVisible = _index && (*_index + 1 < _collageData->items.size());\n\t} else {\n\t\t_leftNavVisible = false;\n\t\t_rightNavVisible = false;\n\t}\n}\n\nbool OverlayWidget::computeSaveButtonVisible() const {\n\tif (hasCopyMediaRestriction(true)) {\n\t\treturn false;\n\t} else if (_photo) {\n\t\treturn _photo->hasVideo() || _photoMedia->loaded();\n\t} else if (_document) {\n\t\treturn _document->filepath(true).isEmpty() && !_document->loading();\n\t} else {\n\t\treturn false;\n\t}\n}\n\nvoid OverlayWidget::checkForSaveLoaded() {\n\tif (_savePhotoVideoWhenLoaded == SavePhotoVideo::None) {\n\t\treturn;\n\t} else if (!_photo\n\t\t|| !_photo->hasVideo()\n\t\t|| _photoMedia->videoContent(Data::PhotoSize::Large).isEmpty()) {\n\t\treturn;\n\t} else if (_savePhotoVideoWhenLoaded == SavePhotoVideo::QuickSave) {\n\t\t_savePhotoVideoWhenLoaded = SavePhotoVideo::None;\n\t\tdownloadMedia();\n\t} else if (_savePhotoVideoWhenLoaded == SavePhotoVideo::SaveAs) {\n\t\t_savePhotoVideoWhenLoaded = SavePhotoVideo::None;\n\t\tsaveAs();\n\t} else {\n\t\tUnexpected(\"SavePhotoVideo in OverlayWidget::checkForSaveLoaded.\");\n\t}\n}\n\nvoid OverlayWidget::showPremiumDownloadPromo() {\n\tconst auto filter = [=](const auto &...) {\n\t\tif (const auto window = uiShow()->resolveWindow()) {\n\t\t\tShowPremiumPreviewBox(window, PremiumFeature::Stories);\n\t\t\twindow->window().activate();\n\t\t}\n\t\treturn false;\n\t};\n\tuiShow()->showToast({\n\t\t.text = tr::lng_stories_save_promo(\n\t\t\ttr::now,\n\t\t\tlt_link,\n\t\t\ttr::link(\n\t\t\t\ttr::bold(\n\t\t\t\t\ttr::lng_send_as_premium_required_link(tr::now))),\n\t\t\ttr::marked),\n\t\t.filter = filter,\n\t\t.adaptive = true,\n\t\t.duration = kStorySavePromoDuration,\n\t});\n}\n\nvoid OverlayWidget::updateControls() {\n\tif (_document && documentBubbleShown()) {\n\t\t_docRect = QRect(\n\t\t\t(width() - st::mediaviewFileSize.width()) / 2,\n\t\t\t_minUsedTop + (_maxUsedHeight - st::mediaviewFileSize.height()) / 2,\n\t\t\tst::mediaviewFileSize.width(),\n\t\t\tst::mediaviewFileSize.height());\n\t\t_docIconRect = QRect(\n\t\t\t_docRect.x() + st::mediaviewFilePadding,\n\t\t\t_docRect.y() + st::mediaviewFilePadding,\n\t\t\tst::mediaviewFileIconSize,\n\t\t\tst::mediaviewFileIconSize);\n\t\tif (_document->loading()) {\n\t\t\t_docDownload->hide();\n\t\t\t_docSaveAs->hide();\n\t\t\t_docCancel->moveToLeft(_docRect.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, _docRect.y() + st::mediaviewFilePadding + st::mediaviewFileLinksTop);\n\t\t\t_docCancel->show();\n\t\t} else {\n\t\t\tif (_documentMedia->loaded(true)) {\n\t\t\t\t_docDownload->hide();\n\t\t\t\t_docSaveAs->moveToLeft(_docRect.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, _docRect.y() + st::mediaviewFilePadding + st::mediaviewFileLinksTop);\n\t\t\t\t_docSaveAs->show();\n\t\t\t\t_docCancel->hide();\n\t\t\t} else {\n\t\t\t\t_docDownload->moveToLeft(_docRect.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, _docRect.y() + st::mediaviewFilePadding + st::mediaviewFileLinksTop);\n\t\t\t\t_docDownload->show();\n\t\t\t\t_docSaveAs->moveToLeft(_docRect.x() + 2.5 * st::mediaviewFilePadding + st::mediaviewFileIconSize + _docDownload->width(), _docRect.y() + st::mediaviewFilePadding + st::mediaviewFileLinksTop);\n\t\t\t\t_docSaveAs->show();\n\t\t\t\t_docCancel->hide();\n\t\t\t}\n\t\t}\n\t\tupdateDocSize();\n\t} else {\n\t\t_docIconRect = QRect(\n\t\t\t(width() - st::mediaviewFileIconSize) / 2,\n\t\t\t_minUsedTop + (_maxUsedHeight - st::mediaviewFileIconSize) / 2,\n\t\t\tst::mediaviewFileIconSize,\n\t\t\tst::mediaviewFileIconSize);\n\t\t_docDownload->hide();\n\t\t_docSaveAs->hide();\n\t\t_docCancel->hide();\n\t}\n\tradialStart();\n\n\tupdateThemePreviewGeometry();\n\n\tconst auto story = _stories ? _stories->story() : nullptr;\n\tconst auto overRect = QRect(\n\t\tQPoint(),\n\t\tQSize(st::mediaviewIconOver, st::mediaviewIconOver));\n\t_saveVisible = computeSaveButtonVisible();\n\t_shareVisible = story && story->canShare();\n\t_rotateVisible = !_themePreviewShown && !story;\n\t_drawVisible = _drawButtonEnabled\n\t\t&& !_themePreviewShown\n\t\t&& !story\n\t\t&& (_photo || (_document && _document->isImage()));\n\t_recognizeVisible = _recognitionResult.success\n\t\t&& !_recognitionResult.items.empty();\n\tconst auto navRect = [&](int i) {\n\t\treturn QRect(\n\t\t\twidth() - st::mediaviewIconSize.width() * i,\n\t\t\theight() - st::mediaviewIconSize.height(),\n\t\t\tst::mediaviewIconSize.width(),\n\t\t\tst::mediaviewIconSize.height());\n\t};\n\tauto index = 1;\n\t_moreNav = navRect(index);\n\t_moreNavOver = style::centerrect(_moreNav, overRect);\n\t_moreNavIcon = style::centerrect(_moreNav, st::mediaviewMore);\n\t++index;\n\t_rotateNav = navRect(index);\n\t_rotateNavOver = style::centerrect(_rotateNav, overRect);\n\t_rotateNavIcon = style::centerrect(_rotateNav, st::mediaviewRotate);\n\tif (_rotateVisible) {\n\t\t++index;\n\t}\n\t_shareNav = navRect(index);\n\t_shareNavOver = style::centerrect(_shareNav, overRect);\n\t_shareNavIcon = style::centerrect(_shareNav, st::mediaviewShare);\n\tif (_shareVisible) {\n\t\t++index;\n\t}\n\t_saveNav = navRect(index);\n\t_saveNavOver = style::centerrect(_saveNav, overRect);\n\t_saveNavIcon = style::centerrect(_saveNav, st::mediaviewSave);\n\tif (_saveVisible) {\n\t\t++index;\n\t}\n\t_drawNav = navRect(index);\n\t_drawNavOver = style::centerrect(_drawNav, overRect);\n\t_drawNavIcon = style::centerrect(_drawNav, st::mediaviewDraw);\n\tif (_drawVisible) {\n\t\t++index;\n\t}\n\t_recognizeNav = navRect(index);\n\t_recognizeNavOver = style::centerrect(_recognizeNav, overRect);\n\t_recognizeNavIcon = style::centerrect(_recognizeNav, st::mediaviewRecognize);\n\tAssert(st::mediaviewSave.size() == st::mediaviewSaveLocked.size());\n\n\tconst auto dNow = QDateTime::currentDateTime();\n\tconst auto d = [&] {\n\t\tif (_message) {\n\t\t\treturn ItemDateTime(_message);\n\t\t} else if (_photo) {\n\t\t\treturn base::unixtime::parse(_photo->date());\n\t\t} else if (_document) {\n\t\t\treturn base::unixtime::parse(_document->date);\n\t\t}\n\t\treturn dNow;\n\t}();\n\t_dateText = d.isValid() ? Ui::FormatDateTime(d) : QString();\n\t//\n\t_sizeText = [&] {\n\t\tif (_document) {\n\t\t\treturn Ui::FormatSizeText(_document->size);\n\t\t} else if (_photo) {\n\t\t\tconst auto size = _photoMedia->size(Data::PhotoSize::Large);\n\t\t\tif (!size.isEmpty()) {\n\t\t\t\treturn QString::number(size.width())\n\t\t\t\t\t+ QChar(0x00D7)\n\t\t\t\t\t+ QString::number(size.height());\n\t\t\t}\n\t\t}\n\t\treturn QString();\n\t}();\n\t//\n\tif (!_fromName.isEmpty()) {\n\t\t_fromNameLabel.setText(\n\t\t\tst::mediaviewTextStyle,\n\t\t\t_fromName,\n\t\t\tUi::NameTextOptions());\n\t\t_nameNav = QRect(\n\t\t\tst::mediaviewTextLeft,\n\t\t\theight() - st::mediaviewTextTop,\n\t\t\tqMin(_fromNameLabel.maxWidth(), width() / 3),\n\t\t\tst::mediaviewFont->height);\n\t\tconst auto separatorWidth = st::mediaviewFont->width(Ui::kQBullet);\n\t\t_separatorNav = QRect(\n\t\t\tst::mediaviewTextLeft\n\t\t\t\t+ _nameNav.width()\n\t\t\t\t+ st::mediaviewTextSkipHalf,\n\t\t\theight() - st::mediaviewTextTop,\n\t\t\tseparatorWidth,\n\t\t\tst::mediaviewFont->height);\n\t\t_dateNav = QRect(\n\t\t\tst::mediaviewTextLeft\n\t\t\t\t+ _nameNav.width()\n\t\t\t\t+ st::mediaviewTextSkipHalf\n\t\t\t\t+ separatorWidth\n\t\t\t\t+ st::mediaviewTextSkipHalf,\n\t\t\theight() - st::mediaviewTextTop,\n\t\t\tst::mediaviewFont->width(_dateText),\n\t\t\tst::mediaviewFont->height);\n\t} else {\n\t\t_nameNav = QRect();\n\t\t_separatorNav = QRect();\n\t\t_dateNav = QRect(\n\t\t\tst::mediaviewTextLeft,\n\t\t\theight() - st::mediaviewTextTop,\n\t\t\tst::mediaviewFont->width(_dateText),\n\t\t\tst::mediaviewFont->height);\n\t}\n\t//\n\tif (!_sizeText.isEmpty()) {\n\t\t_sizeNav = QRect(\n\t\t\t_dateNav.left() + _dateNav.width() + st::mediaviewTextSkip,\n\t\t\theight() - st::mediaviewTextTop,\n\t\t\tst::mediaviewFont->width(_sizeText),\n\t\t\tst::mediaviewFont->height);\n\t} else {\n\t\t_sizeNav = QRect();\n\t}\n\t//\n\tupdateHeader();\n\trefreshNavVisibility();\n\tresizeCenteredControls();\n\n\tupdateOver(_widget->mapFromGlobal(QCursor::pos()));\n\tupdate();\n}\n\nvoid OverlayWidget::resizeCenteredControls() {\n\tconst auto bottomSkip = std::max({\n\t\t_dateNav.left() + _dateNav.width(),\n\t\t//\n\t\t_sizeNav.left() + _sizeNav.width(),\n\t\t//\n\t\t_headerNav.left() + _headerNav.width() })\n\t\t+ st::mediaviewCaptionMargin.width();\n\t_groupThumbsAvailableWidth = std::max(\n\t\twidth() - 2 * bottomSkip,\n\t\tst::msgMinWidth\n\t\t+ st::mediaviewCaptionPadding.left()\n\t\t+ st::mediaviewCaptionPadding.right());\n\t_groupThumbsLeft = (width() - _groupThumbsAvailableWidth) / 2;\n\trefreshGroupThumbs();\n\t_groupThumbsTop = _groupThumbs ? (height() - _groupThumbs->height()) : 0;\n\n\trefreshClipControllerGeometry();\n\trefreshSponsoredButtonGeometry();\n\trefreshVoteButton();\n\trefreshPollVotersWidget();\n\trefreshVoteButtonGeometry();\n\trefreshPollVotersWidgetGeometry();\n\trefreshCaptionGeometry();\n\trefreshSponsoredButtonWidth();\n\n\t_pollUpdateLifetime.destroy();\n\tif (currentPollAnswer()) {\n\t\tconst auto media = _message ? _message->media() : nullptr;\n\t\tconst auto poll = media ? media->poll() : nullptr;\n\t\tif (poll && _session) {\n\t\t\t_session->data().pollUpdates(\n\t\t\t) | rpl::filter([=](not_null<PollData*> updated) {\n\t\t\t\treturn updated == poll;\n\t\t\t}) | rpl::on_next([=] {\n\t\t\t\trefreshVoteButton();\n\t\t\t\trefreshPollVotersWidget();\n\t\t\t\trefreshVoteButtonGeometry();\n\t\t\t\trefreshPollVotersWidgetGeometry();\n\t\t\t\trefreshCaptionGeometry();\n\t\t\t}, _pollUpdateLifetime);\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::refreshCaptionGeometry() {\n\t_caption.updateSkipBlock(0, 0);\n\t_captionShowMoreWidth = 0;\n\t_captionSkipBlockWidth = 0;\n\n\tconst auto storiesCaptionWidth = _w\n\t\t- st::mediaviewCaptionPadding.left()\n\t\t- st::mediaviewCaptionPadding.right();\n\tif (_caption.isEmpty() && (!_stories || !_stories->repost())) {\n\t\t_captionRect = QRect();\n\t\treturn;\n\t} else if (_fullScreenVideo) {\n\t\t_captionRect = QRect();\n\t\treturn;\n\t}\n\n\tif (_groupThumbs && _groupThumbs->hiding()) {\n\t\t_groupThumbs = nullptr;\n\t\t_groupThumbsRect = QRect();\n\t}\n\tconst auto captionBottom = _stories\n\t\t? (_y + _h)\n\t\t: _sponsoredButton\n\t\t? (_sponsoredButton->y() - st::mediaviewCaptionMargin.height())\n\t\t: _voteButton\n\t\t? (_voteButton->y() - st::mediaviewCaptionMargin.height())\n\t\t: _pollVotersWidget\n\t\t? (_pollVotersWidget->y() - st::mediaviewCaptionMargin.height())\n\t\t: (_streamed && _streamed->controls)\n\t\t? (_streamed->controls->y() - st::mediaviewCaptionMargin.height())\n\t\t: _groupThumbs\n\t\t? _groupThumbsTop\n\t\t: height() - st::mediaviewCaptionMargin.height();\n\tconst auto captionWidth = _stories\n\t\t? storiesCaptionWidth\n\t\t: std::min(\n\t\t\t(_groupThumbsAvailableWidth\n\t\t\t\t- st::mediaviewCaptionPadding.left()\n\t\t\t\t- st::mediaviewCaptionPadding.right()),\n\t\t\t_caption.maxWidth());\n\tconst auto lineHeight = st::mediaviewCaptionStyle.font->height;\n\tconst auto wantedHeight = _caption.countHeight(captionWidth);\n\tconst auto maxHeight = !_stories\n\t\t? (_maxUsedHeight / 4)\n\t\t: (wantedHeight > lineHeight * Stories::kMaxShownCaptionLines)\n\t\t? (lineHeight * Stories::kCollapsedCaptionLines)\n\t\t: wantedHeight;\n\tconst auto captionHeight = std::min(\n\t\twantedHeight,\n\t\t(maxHeight / lineHeight) * lineHeight);\n\tif (_stories && captionHeight < wantedHeight) {\n\t\tconst auto padding = st::storiesShowMorePadding;\n\t\t_captionShowMoreWidth = st::storiesShowMoreFont->width(\n\t\t\ttr::lng_stories_show_more(tr::now));\n\t\t_captionSkipBlockWidth = _captionShowMoreWidth\n\t\t\t+ padding.left()\n\t\t\t+ padding.right()\n\t\t\t- st::mediaviewCaptionPadding.right();\n\t\tconst auto skiph = st::storiesShowMoreFont->height\n\t\t\t+ padding.bottom()\n\t\t\t- st::mediaviewCaptionPadding.bottom();\n\t\t_caption.updateSkipBlock(_captionSkipBlockWidth, skiph);\n\t}\n\t_captionRect = QRect(\n\t\t(width() - captionWidth) / 2,\n\t\t(captionBottom\n\t\t\t- captionHeight\n\t\t\t- st::mediaviewCaptionPadding.bottom()),\n\t\tcaptionWidth,\n\t\tcaptionHeight);\n}\n\nvoid OverlayWidget::refreshSponsoredButtonGeometry() {\n\tif (!_sponsoredButton) {\n\t\treturn;\n\t}\n\tconst auto controllerBottom = (_groupThumbs && !_fullScreenVideo)\n\t\t? _groupThumbsTop\n\t\t: height();\n\tconst auto captionRect = captionGeometry();\n\t_sponsoredButton->resize(\n\t\tcaptionRect.width(),\n\t\t_sponsoredButton->height());\n\t_sponsoredButton->move(\n\t\t(width() - captionRect.width()) / 2,\n\t\t(controllerBottom // Duplicated in recountSkipTop().\n\t\t\t- ((_streamed && _streamed->controls)\n\t\t\t\t? (_streamed->controls->height()\n\t\t\t\t\t+ st::mediaviewCaptionPadding.bottom())\n\t\t\t\t: 0)\n\t\t\t- _sponsoredButton->height()\n\t\t\t- st::mediaviewCaptionMargin.height()));\n\tUi::SendPendingMoveResizeEvents(_sponsoredButton.get());\n}\n\nvoid OverlayWidget::refreshSponsoredButtonWidth() {\n\tif (!_sponsoredButton) {\n\t\treturn;\n\t}\n\tconst auto captionWidth = captionGeometry().width();\n\t_sponsoredButton->resize(captionWidth, _sponsoredButton->height());\n\t_sponsoredButton->move(\n\t\t(width() - captionWidth) / 2,\n\t\t_sponsoredButton->y());\n}\n\nconst PollAnswer *OverlayWidget::currentPollAnswer() const {\n\tif (!_message || !_session) {\n\t\treturn nullptr;\n\t}\n\tconst auto media = _message->media();\n\tif (!media) {\n\t\treturn nullptr;\n\t}\n\tconst auto poll = media->poll();\n\tif (!poll || (!_photo && !_document)) {\n\t\treturn nullptr;\n\t}\n\tconst auto current = _photo\n\t\t? WebPageCollage::Item(_photo)\n\t\t: WebPageCollage::Item(_document);\n\tfor (const auto &answer : poll->answers) {\n\t\tconst auto candidate = PollAnswerMediaItem(answer);\n\t\tif (candidate && (*candidate == current)) {\n\t\t\treturn &answer;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid OverlayWidget::refreshVoteButton() {\n\tconst auto answer = currentPollAnswer();\n\tconst auto media = _message ? _message->media() : nullptr;\n\tconst auto poll = media ? media->poll() : nullptr;\n\tconst auto show = answer\n\t\t&& poll\n\t\t&& !answer->chosen\n\t\t&& !poll->closed()\n\t\t&& !poll->voted()\n\t\t&& poll->sendingVotes.empty();\n\tif (!show) {\n\t\tif (_voteButton) {\n\t\t\t_voteButton.destroy();\n\t\t}\n\t\treturn;\n\t}\n\tif (!_voteButton) {\n\t\t_voteButton.create(\n\t\t\t_body,\n\t\t\ttr::lng_polls_submit_votes(),\n\t\t\tst::mediaviewVoteButton);\n\t\tconst auto effect = Ui::CreateChild<QGraphicsOpacityEffect>(\n\t\t\t_voteButton.data());\n\t\teffect->setOpacity(_controlsOpacity.current());\n\t\t_voteButton->setGraphicsEffect(effect);\n\t\t_voteButton->setClickedCallback([=] {\n\t\t\tif (!_session) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto current = currentPollAnswer();\n\t\t\tif (!current) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_session->api().polls().sendVotes(\n\t\t\t\t_message->fullId(),\n\t\t\t\t{ current->option });\n\t\t\t_voteButton.destroy();\n\t\t\trefreshCaptionGeometry();\n\t\t});\n\t}\n\t_voteButton->show();\n}\n\nvoid OverlayWidget::refreshVoteButtonGeometry() {\n\tif (!_voteButton) {\n\t\treturn;\n\t}\n\tconst auto controllerBottom = (_groupThumbs && !_fullScreenVideo)\n\t\t? _groupThumbsTop\n\t\t: height();\n\t_voteButton->moveToLeft(\n\t\t(width() - _voteButton->width()) / 2,\n\t\tcontrollerBottom\n\t\t\t- ((_streamed && _streamed->controls)\n\t\t\t\t? (_streamed->controls->height()\n\t\t\t\t\t+ st::mediaviewCaptionPadding.bottom())\n\t\t\t\t: 0)\n\t\t\t- _voteButton->height()\n\t\t\t- st::mediaviewVoteButtonMargin);\n}\n\nvoid OverlayWidget::refreshPollVotersWidget() {\n\tconst auto answer = currentPollAnswer();\n\tconst auto show = answer\n\t\t&& (answer->votes > 0)\n\t\t&& !_voteButton;\n\tif (!show) {\n\t\t_pollVotersWidget.destroy();\n\t\treturn;\n\t}\n\n\t_pollVotersWidget.destroy();\n\t_pollVotersWidget.create(_body);\n\n\tconst auto effect = Ui::CreateChild<QGraphicsOpacityEffect>(\n\t\t_pollVotersWidget.data());\n\teffect->setOpacity(_controlsOpacity.current());\n\t_pollVotersWidget->setGraphicsEffect(effect);\n\n\tauto prepared = HistoryView::PrepareUserpicsInRow(\n\t\tanswer->recentVoters,\n\t\tst::mediaviewPollVotersUserpics,\n\t\t3);\n\n\tconst auto votesText = tr::lng_polls_votes_count(\n\t\ttr::now,\n\t\tlt_count,\n\t\tanswer->votes);\n\tconst auto &font = st::semiboldFont;\n\tconst auto textWidth = font->width(votesText);\n\n\tconst auto userpicsWidth = prepared.width;\n\tconst auto spacing = (userpicsWidth > 0)\n\t\t? st::mediaviewPollVotersSpacing\n\t\t: 0;\n\tconst auto &padding = st::mediaviewPollVotersPadding;\n\tconst auto widgetWidth = padding.left()\n\t\t+ userpicsWidth\n\t\t+ spacing\n\t\t+ textWidth\n\t\t+ padding.right();\n\tconst auto widgetHeight = st::mediaviewVoteButton.height;\n\n\t_pollVotersWidget->resize(widgetWidth, widgetHeight);\n\n\tconst auto userpicSize = st::mediaviewPollVotersUserpics.size;\n\tconst auto raw = _pollVotersWidget.data();\n\traw->paintRequest() | rpl::on_next([=,\n\t\t\timage = std::move(prepared.image)] {\n\t\tauto p = QPainter(raw);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tconst auto radius = st::mediaviewCaptionRadius;\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::mediaviewCaptionBg);\n\t\tp.drawRoundedRect(raw->rect(), radius, radius);\n\n\t\tauto left = padding.left();\n\n\t\tif (!image.isNull()) {\n\t\t\tconst auto top = (widgetHeight - userpicSize) / 2;\n\t\t\tp.drawImage(left, top, image);\n\t\t\tleft += userpicsWidth + spacing;\n\t\t}\n\n\t\tp.setFont(font);\n\t\tp.setPen(st::mediaviewCaptionFg);\n\t\tconst auto textTop = (widgetHeight + font->ascent - font->descent) / 2;\n\t\tp.drawText(left, textTop, votesText);\n\t}, raw->lifetime());\n\n\t_pollVotersWidget->show();\n}\n\nvoid OverlayWidget::refreshPollVotersWidgetGeometry() {\n\tif (!_pollVotersWidget) {\n\t\treturn;\n\t}\n\tconst auto controllerBottom = (_groupThumbs && !_fullScreenVideo)\n\t\t? _groupThumbsTop\n\t\t: height();\n\t_pollVotersWidget->moveToLeft(\n\t\t(width() - _pollVotersWidget->width()) / 2,\n\t\tcontrollerBottom\n\t\t\t- ((_streamed && _streamed->controls)\n\t\t\t\t? (_streamed->controls->height()\n\t\t\t\t\t+ st::mediaviewCaptionPadding.bottom())\n\t\t\t\t: 0)\n\t\t\t- _pollVotersWidget->height()\n\t\t\t- st::mediaviewVoteButtonMargin);\n}\n\nvoid OverlayWidget::fillContextMenuActions(\n\t\tconst Ui::Menu::MenuCallback &addAction) {\n\tif (_message && _message->isSponsored()) {\n\t\tif (const auto window = findWindow()) {\n\t\t\tconst auto show = window->uiShow();\n\t\t\tconst auto fullId = _message->fullId();\n\t\t\tMenu::FillSponsored(\n\t\t\t\taddAction,\n\t\t\t\tshow,\n\t\t\t\tfullId,\n\t\t\t\t{ .dark = true, .skipInfo = true });\n\t\t}\n\t\treturn;\n\t}\n\tconst auto story = _stories ? _stories->story() : nullptr;\n\tif (!story && _document && _document->loading()) {\n\t\taddAction(\n\t\t\ttr::lng_cancel(tr::now),\n\t\t\t[=] { saveCancel(); },\n\t\t\t&st::mediaMenuIconCancel);\n\t}\n\tif (_message && _message->isRegular()) {\n\t\taddAction(\n\t\t\ttr::lng_context_to_msg(tr::now),\n\t\t\t[=] { toMessage(); },\n\t\t\t&st::mediaMenuIconShowInChat);\n\t}\n\tif (currentPollAnswer()) {\n\t\tconst auto media = _message ? _message->media() : nullptr;\n\t\tconst auto poll = media ? media->poll() : nullptr;\n\t\tif (poll\n\t\t\t&& poll->voted()\n\t\t\t&& !poll->closed()\n\t\t\t&& !poll->quiz()\n\t\t\t&& !poll->revotingDisabled()) {\n\t\t\tconst auto itemId = _message->fullId();\n\t\t\taddAction(tr::lng_polls_retract(tr::now), [=] {\n\t\t\t\tif (_session) {\n\t\t\t\t\t_session->api().polls().sendVotes(itemId, {});\n\t\t\t\t}\n\t\t\t}, &st::mediaMenuIconRetractVote);\n\t\t}\n\t}\n\tif (story && story->peer()->isSelf()) {\n\t\tconst auto inProfile = story->inProfile();\n\t\tconst auto text = inProfile\n\t\t\t? tr::lng_mediaview_archive_story(tr::now)\n\t\t\t: tr::lng_mediaview_save_to_profile(tr::now);\n\t\taddAction(text, [=] {\n\t\t\tif (_stories) {\n\t\t\t\t_stories->toggleInProfileRequested(!inProfile);\n\t\t\t}\n\t\t}, (inProfile\n\t\t\t? &st::mediaMenuIconArchiveStory\n\t\t\t: &st::mediaMenuIconSaveStory));\n\t}\n\tif ((!story || story->canDownloadChecked())\n\t\t&& _document\n\t\t&& !_document->filepath(true).isEmpty()) {\n\t\tconst auto text = Platform::IsMac()\n\t\t\t? tr::lng_context_show_in_finder(tr::now)\n\t\t\t: tr::lng_context_show_in_folder(tr::now);\n\t\taddAction(\n\t\t\ttext,\n\t\t\t[=] { showInFolder(); },\n\t\t\t&st::mediaMenuIconShowInFolder);\n\t}\n\tif (!hasCopyMediaRestriction()) {\n\t\tif ((_document && documentContentShown()) || (_photo && _photoMedia->loaded())) {\n\t\t\taddAction(\n\t\t\t\t((_document && _streamed)\n\t\t\t\t\t? tr::lng_mediaview_copy_frame(tr::now)\n\t\t\t\t\t: tr::lng_mediaview_copy(tr::now)),\n\t\t\t\t[=] { copyMedia(); },\n\t\t\t\t&st::mediaMenuIconCopy);\n\t\t}\n\t}\n\tif ((_photo && _photo->hasAttachedStickers())\n\t\t|| (_document && _document->hasAttachedStickers())) {\n\t\taddAction(\n\t\t\ttr::lng_context_attached_stickers(tr::now),\n\t\t\t[=] { showAttachedStickers(); },\n\t\t\t&st::mediaMenuIconStickers);\n\t}\n\tif (_message && _message->allowsForward()) {\n\t\taddAction(\n\t\t\ttr::lng_mediaview_forward(tr::now),\n\t\t\t[=] { forwardMedia(); },\n\t\t\t&st::mediaMenuIconForward);\n\t\tif (canShareAtTime()) {\n\t\t\tconst auto now = [=] {\n\t\t\t\treturn tr::lng_mediaview_share_at_time(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_time,\n\t\t\t\t\tStories::FormatShareAtTime(shareAtVideoTimestamp()));\n\t\t\t};\n\t\t\tconst auto action = addAction(\n\t\t\t\tnow(),\n\t\t\t\t[=] { shareAtTime(); },\n\t\t\t\t&st::mediaMenuIconShare);\n\t\t\tstruct State {\n\t\t\t\trpl::variable<QString> text;\n\t\t\t\trpl::lifetime lifetime;\n\t\t\t};\n\t\t\tconst auto state = Ui::CreateChild<State>(action);\n\t\t\tstate->text = rpl::single(\n\t\t\t\trpl::empty\n\t\t\t) | rpl::then(\n\t\t\t\tbase::timer_each(120)\n\t\t\t) | rpl::map(now);\n\t\t\tstate->text.changes() | rpl::on_next([=](QString value) {\n\t\t\t\taction->setText(value);\n\t\t\t}, state->lifetime);\n\t\t}\n\t}\n\tif (story && story->canShare()) {\n\t\taddAction(tr::lng_mediaview_forward(tr::now), [=] {\n\t\t\t_stories->shareRequested();\n\t\t}, &st::mediaMenuIconForward);\n\t}\n\tconst auto canDelete = [&] {\n\t\tif (story && story->canDelete()) {\n\t\t\treturn true;\n\t\t} else if (_message && _message->canDelete()) {\n\t\t\treturn true;\n\t\t} else if (!_message\n\t\t\t&& _photo\n\t\t\t&& _user\n\t\t\t&& _user == _user->session().user()) {\n\t\t\treturn _userPhotosData && _fullIndex && _fullCount;\n\t\t} else if (_photo && _photo->peer && _photo->peer->userpicPhotoId() == _photo->id) {\n\t\t\tif (auto chat = _photo->peer->asChat()) {\n\t\t\t\treturn chat->canEditInformation();\n\t\t\t} else if (auto channel = _photo->peer->asChannel()) {\n\t\t\t\treturn channel->canEditInformation();\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}();\n\tif (canDelete) {\n\t\taddAction(\n\t\t\ttr::lng_mediaview_delete(tr::now),\n\t\t\t[=] { deleteMedia(); },\n\t\t\t&st::mediaMenuIconDelete);\n\t}\n\tif (!hasCopyMediaRestriction(true)) {\n\t\taddAction(\n\t\t\ttr::lng_mediaview_save_as(tr::now),\n\t\t\t[=] { saveAs(); },\n\t\t\t(saveControlLocked()\n\t\t\t\t? &st::mediaMenuIconDownloadLocked\n\t\t\t\t: &st::mediaMenuIconDownload));\n\t}\n\n\tif (const auto overviewType = computeOverviewType()) {\n\t\tconst auto text = _document\n\t\t\t? tr::lng_mediaview_files_all(tr::now)\n\t\t\t: tr::lng_mediaview_photos_all(tr::now);\n\t\taddAction(\n\t\t\ttext,\n\t\t\t[=] { showMediaOverview(); },\n\t\t\t&st::mediaMenuIconShowAll);\n\t}\n\t[&] { // Set userpic.\n\t\tif (!_peer || !_photo || (_peer->userpicPhotoId() == _photo->id)) {\n\t\t\treturn;\n\t\t}\n\t\tusing Type = SharedMediaType;\n\t\tif (sharedMediaType().value_or(Type::File) == Type::ChatPhoto) {\n\t\t\tif (const auto chat = _peer->asChat()) {\n\t\t\t\tif (!chat->canEditInformation()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} else if (const auto channel = _peer->asChannel()) {\n\t\t\t\tif (!channel->canEditInformation()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else if (userPhotosKey()) {\n\t\t\tif (_user != _user->session().user()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t\tconst auto photo = _photo;\n\t\tconst auto peer = _peer;\n\t\taddAction(tr::lng_mediaview_set_userpic(tr::now), [=] {\n\t\t\tauto lifetime = std::make_shared<rpl::lifetime>();\n\t\t\tpeer->session().changes().peerFlagsValue(\n\t\t\t\tpeer,\n\t\t\t\tData::PeerUpdate::Flag::Photo\n\t\t\t) | rpl::on_next([=]() mutable {\n\t\t\t\tif (lifetime) {\n\t\t\t\t\tbase::take(lifetime)->destroy();\n\t\t\t\t}\n\t\t\t\tclose();\n\t\t\t}, *lifetime);\n\n\t\t\tpeer->session().api().peerPhoto().set(peer, photo);\n\t\t}, &st::mediaMenuIconProfile);\n\t}();\n\t[&] { // Report userpic.\n\t\tif (!_peer || !_photo) {\n\t\t\treturn;\n\t\t}\n\t\tusing Type = SharedMediaType;\n\t\tif (userPhotosKey()) {\n\t\t\tif (_peer->isSelf() || _peer->isNotificationsUser()) {\n\t\t\t\treturn;\n\t\t\t} else if (const auto user = _peer->asUser()) {\n\t\t\t\tif (user->hasPersonalPhoto()\n\t\t\t\t\t&& user->userpicPhotoId() == _photo->id) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if ((sharedMediaType().value_or(Type::File) == Type::ChatPhoto)\n\t\t\t|| (_peer->userpicPhotoId() == _photo->id)) {\n\t\t\tif (const auto chat = _peer->asChat()) {\n\t\t\t\tif (chat->canEditInformation()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} else if (const auto channel = _peer->asChannel()) {\n\t\t\t\tif (channel->canEditInformation()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t\tconst auto photo = _photo;\n\t\tconst auto peer = _peer;\n\t\taddAction(tr::lng_mediaview_report_profile_photo(tr::now), [=] {\n\t\t\tif (const auto window = findWindow()) {\n\t\t\t\tclose();\n\t\t\t\twindow->show(\n\t\t\t\t\tReportProfilePhotoBox(peer, photo),\n\t\t\t\t\tUi::LayerOption::CloseOther);\n\t\t\t}\n\t\t}, &st::mediaMenuIconReport);\n\t}();\n\t{\n\t\tconst auto channel = story ? story->peer()->asChannel() : nullptr;\n\t\tusing Flag = ChannelDataFlag;\n\t\tif (channel && (channel->flags() & Flag::CanGetStatistics)) {\n\t\t\tconst auto peer = channel;\n\t\t\tconst auto fullId = story->fullId();\n\t\t\taddAction(tr::lng_stats_title(tr::now), [=] {\n\t\t\t\tif (const auto window = findWindow()) {\n\t\t\t\t\tclose();\n\t\t\t\t\tusing namespace Info;\n\t\t\t\t\twindow->showSection(Statistics::Make(peer, {}, fullId));\n\t\t\t\t}\n\t\t\t}, &st::mediaMenuIconStats);\n\t\t}\n\t}\n\tif (_stories\n\t\t&& _stories->allowStealthMode()\n\t\t&& story\n\t\t&& story->peer()->isUser()\n\t\t&& !story->call()) {\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto stealth = _session->data().stories().stealthMode();\n\t\taddAction(tr::lng_stealth_mode_menu_item(tr::now), [=] {\n\t\t\t_stories->setupStealthMode();\n\t\t}, ((_session->premium() || (stealth.enabledTill > now))\n\t\t\t? &st::mediaMenuIconStealth\n\t\t\t: &st::mediaMenuIconStealthLocked));\n\t}\n\tif (story && story->canReport()) {\n\t\taddAction(tr::lng_profile_report(tr::now), [=] {\n\t\t\t_stories->reportRequested();\n\t\t}, &st::mediaMenuIconReport);\n\t}\n}\n\nauto OverlayWidget::computeOverviewType() const\n-> std::optional<SharedMediaType> {\n\tif (const auto mediaType = sharedMediaType()) {\n\t\tif (const auto overviewType = SharedMediaOverviewType(*mediaType)) {\n\t\t\treturn overviewType;\n\t\t} else if (mediaType == SharedMediaType::PhotoVideo) {\n\t\t\tif (_photo) {\n\t\t\t\treturn SharedMediaOverviewType(SharedMediaType::Photo);\n\t\t\t} else if (_document) {\n\t\t\t\treturn SharedMediaOverviewType(SharedMediaType::Video);\n\t\t\t}\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nbool OverlayWidget::stateAnimationCallback(crl::time now) {\n\tif (anim::Disabled()) {\n\t\tnow += st::mediaviewShowDuration + st::mediaviewHideDuration;\n\t}\n\tfor (auto i = begin(_animations); i != end(_animations);) {\n\t\tconst auto &[state, started] = *i;\n\t\tupdateOverRect(state);\n\t\tconst auto dt = float64(now - started) / st::mediaviewFadeDuration;\n\t\tif (dt >= 1) {\n\t\t\t_animationOpacities.erase(state);\n\t\t\ti = _animations.erase(i);\n\t\t} else {\n\t\t\t_animationOpacities[state].update(dt, anim::linear);\n\t\t\t++i;\n\t\t}\n\t}\n\treturn !_animations.empty() || updateControlsAnimation(now);\n}\n\nbool OverlayWidget::updateControlsAnimation(crl::time now) {\n\tif (_controlsState != ControlsShowing\n\t\t&& _controlsState != ControlsHiding) {\n\t\treturn false;\n\t}\n\tconst auto duration = (_controlsState == ControlsShowing)\n\t\t? st::mediaviewShowDuration\n\t\t: st::mediaviewHideDuration;\n\tconst auto dt = float64(now - _controlsAnimStarted)\n\t\t/ duration;\n\tif (dt >= 1) {\n\t\t_controlsOpacity.finish();\n\t\t_controlsState = (_controlsState == ControlsShowing)\n\t\t\t? ControlsShown\n\t\t\t: ControlsHidden;\n\t\tupdateCursor();\n\t} else {\n\t\t_controlsOpacity.update(dt, anim::linear);\n\t}\n\tif (_sponsoredButton) {\n\t\tconst auto value = _controlsOpacity.current();\n\t\t_sponsoredButton->setOpacity(value);\n\t\t_sponsoredButton->setAttribute(\n\t\t\tQt::WA_TransparentForMouseEvents,\n\t\t\tvalue < 1);\n\t}\n\tif (_voteButton) {\n\t\tconst auto value = _controlsOpacity.current();\n\t\tif (const auto e = _voteButton->graphicsEffect()) {\n\t\t\tstatic_cast<QGraphicsOpacityEffect*>(e)->setOpacity(value);\n\t\t}\n\t\t_voteButton->setAttribute(\n\t\t\tQt::WA_TransparentForMouseEvents,\n\t\t\tvalue < 1);\n\t}\n\tif (_pollVotersWidget) {\n\t\tconst auto value = _controlsOpacity.current();\n\t\tif (const auto e = _pollVotersWidget->graphicsEffect()) {\n\t\t\tstatic_cast<QGraphicsOpacityEffect*>(e)->setOpacity(value);\n\t\t}\n\t\t_pollVotersWidget->setAttribute(\n\t\t\tQt::WA_TransparentForMouseEvents,\n\t\t\tvalue < 1);\n\t}\n\t_helper->setControlsOpacity(_controlsOpacity.current());\n\tconst auto content = finalContentRect();\n\tconst auto siblingType = (_over == Over::LeftStories)\n\t\t? Stories::SiblingType::Left\n\t\t: Stories::SiblingType::Right;\n\tconst auto toUpdate = QRegion()\n\t\t+ (_over == Over::Left ? _leftNavOver : _leftNavIcon)\n\t\t+ (_over == Over::Right ? _rightNavOver : _rightNavIcon)\n\t\t+ (_over == Over::Save ? _saveNavOver : _saveNavIcon)\n\t\t+ (_over == Over::Share ? _shareNavOver : _shareNavIcon)\n\t\t+ (_over == Over::Rotate ? _rotateNavOver : _rotateNavIcon)\n\t\t+ (_over == Over::More ? _moreNavOver : _moreNavIcon)\n\t\t+ (_over == Over::Draw ? _drawNavOver : _drawNavIcon)\n\t\t+ (_over == Over::Recognize\n\t\t\t? _recognizeNavOver\n\t\t\t: _recognizeNavIcon)\n\t\t+ ((_stories\n\t\t\t&& (_over == Over::LeftStories || _over == Over::RightStories))\n\t\t\t? _stories->sibling(siblingType).layout.geometry\n\t\t\t: QRect())\n\t\t+ _headerNav\n\t\t+ _nameNav\n\t\t+ _separatorNav\n\t\t+ _dateNav\n\t\t//\n\t\t+ _sizeNav\n\t\t//\n\t\t+ _captionRect.marginsAdded(st::mediaviewCaptionPadding)\n\t\t+ _groupThumbsRect\n\t\t+ content.intersected(_bottomShadowRect)\n\t\t+ content.intersected(_topShadowRect);\n\tupdate(toUpdate);\n\treturn (dt < 1);\n}\n\nvoid OverlayWidget::waitingAnimationCallback() {\n\tif (!anim::Disabled()) {\n\t\tupdate(radialRect());\n\t}\n}\n\nvoid OverlayWidget::updateCursor() {\n\tsetCursor((_clickHandlerActive || _clickHandlerPressed)\n\t\t? style::cur_pointer\n\t\t: (_controlsState == ControlsHidden)\n\t\t? Qt::BlankCursor\n\t\t: (_over == Over::None || (_over == Over::Video && _stories))\n\t\t? style::cur_default\n\t\t: style::cur_pointer);\n}\n\nint OverlayWidget::finalContentRotation() const {\n\treturn _streamed\n\t\t? ((_rotation + (_streamed\n\t\t\t? _streamed->instance.info().video.rotation\n\t\t\t: 0)) % 360)\n\t\t: _rotation;\n}\n\nQRect OverlayWidget::finalContentRect() const {\n\treturn { _x, _y, _w, _h };\n}\n\nOverlayWidget::ContentGeometry OverlayWidget::contentGeometry() const {\n\tif (_stories) {\n\t\tauto result = storiesContentGeometry(_stories->contentLayout());\n\t\tif (!_caption.isEmpty()) {\n\t\t\tresult.bottomShadowSkip = _widget->height()\n\t\t\t\t- _captionRect.y()\n\t\t\t\t+ st::mediaviewCaptionStyle.font->height\n\t\t\t\t- st::storiesShadowBottom.height();\n\t\t}\n\t\treturn result;\n\t}\n\tconst auto controlsOpacity = _controlsOpacity.current();\n\tconst auto toRotation = qreal(finalContentRotation());\n\tconst auto toRectRotated = QRectF(finalContentRect());\n\tconst auto toRectCenter = rect::center(toRectRotated);\n\tconst auto toRect = ((int(toRotation) % 180) == 90)\n\t\t? QRectF(\n\t\t\ttoRectCenter.x() - toRectRotated.height() / 2.,\n\t\t\ttoRectCenter.y() - toRectRotated.width() / 2.,\n\t\t\ttoRectRotated.height(),\n\t\t\ttoRectRotated.width())\n\t\t: toRectRotated;\n\tif (!_geometryAnimation.animating()) {\n\t\treturn { toRect, toRotation, controlsOpacity };\n\t}\n\tconst auto fromRect = _oldGeometry.rect;\n\tconst auto fromRotation = _oldGeometry.rotation;\n\tconst auto progress = _geometryAnimation.value(1.);\n\tconst auto rotationDelta = (toRotation - fromRotation);\n\tconst auto useRotationDelta = (rotationDelta > 180.)\n\t\t? (rotationDelta - 360.)\n\t\t: (rotationDelta <= -180.)\n\t\t? (rotationDelta + 360.)\n\t\t: rotationDelta;\n\tconst auto rotation = fromRotation + useRotationDelta * progress;\n\tconst auto useRotation = (rotation > 360.)\n\t\t? (rotation - 360.)\n\t\t: (rotation < 0.)\n\t\t? (rotation + 360.)\n\t\t: rotation;\n\tconst auto useRect = QRectF(\n\t\tfromRect.x() + (toRect.x() - fromRect.x()) * progress,\n\t\tfromRect.y() + (toRect.y() - fromRect.y()) * progress,\n\t\tfromRect.width() + (toRect.width() - fromRect.width()) * progress,\n\t\tfromRect.height() + (toRect.height() - fromRect.height()) * progress\n\t);\n\treturn { useRect, useRotation, controlsOpacity };\n}\n\nOverlayWidget::ContentGeometry OverlayWidget::storiesContentGeometry(\n\t\tconst Stories::ContentLayout &layout,\n\t\tfloat64 scale) const {\n\treturn {\n\t\t.rect = QRectF(layout.geometry),\n\t\t.controlsOpacity = kStoriesControlsOpacity,\n\t\t.fade = layout.fade,\n\t\t.scale = scale,\n\t\t.roundRadius = layout.radius,\n\t\t.topShadowShown = !layout.headerOutside,\n\t};\n}\n\nvoid OverlayWidget::updateContentRect() {\n\tif (_opengl) {\n\t\tupdate();\n\t} else {\n\t\tupdate(finalContentRect());\n\t}\n}\n\nvoid OverlayWidget::contentSizeChanged() {\n\t_width = _w;\n\t_height = _h;\n\tresizeContentByScreenSize();\n}\n\nvoid OverlayWidget::recountSkipTop() {\n\tconst auto controllerBottomNoFullScreenVideo = _groupThumbs\n\t\t? _groupThumbsTop\n\t\t: height();\n\t// We need the bottom in case of non-full-screen-video mode\n\t// to count correct _availableHeight in non-full-screen-video mode.\n\t//\n\t// Originally this is controls->y() - padding.bottom().\n\tconst auto bottom = (_streamed && _streamed->controls)\n\t\t? (controllerBottomNoFullScreenVideo\n\t\t\t- _streamed->controls->height()\n\t\t\t- 2 * st::mediaviewCaptionPadding.bottom())\n\t\t: height();\n\tconst auto skipHeightBottom = (height() - bottom);\n\t_skipTop = _minUsedTop + std::min(\n\t\tstd::max(\n\t\t\tst::mediaviewCaptionMargin.height(),\n\t\t\theight() - _height - skipHeightBottom),\n\t\tskipHeightBottom);\n\t_availableHeight = height() - skipHeightBottom - _skipTop;\n\tif (_fullScreenVideo && skipHeightBottom > 0 && _width > 0) {\n\t\tconst auto h = width() * _height / _width;\n\t\tconst auto topAllFit = _maxUsedHeight - skipHeightBottom - h;\n\t\tif (_skipTop > topAllFit) {\n\t\t\t_skipTop = std::max(topAllFit, 0);\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::resizeContentByScreenSize() {\n\tif (_stories) {\n\t\tconst auto content = _stories->finalShownGeometry();\n\t\t_x = content.x();\n\t\t_y = content.y();\n\t\t_w = content.width();\n\t\t_h = content.height();\n\t\t_zoom = 0;\n\t\tupdateNavigationControlsGeometry();\n\t\tif (_videoStream) {\n\t\t\t_videoStream->updateGeometry(_x, _y, _w, _h);\n\t\t}\n\t\treturn;\n\t} else if (_videoStream) {\n\t\t_w = _body->width() / 2;\n\t\t_h = _body->height() / 2;\n\t\t_x = (_body->width() - _w) / 2;\n\t\t_y = (_body->height() - _h) / 2;\n\t\t_videoStream->updateGeometry(_x, _y, _w, _h);\n\t\treturn;\n\t}\n\trecountSkipTop();\n\tconst auto availableWidth = width();\n\tconst auto countZoomFor = [&](int outerw, int outerh) {\n\t\tauto result = float64(outerw) / _width;\n\t\tif (_height * result > outerh) {\n\t\t\tresult = float64(outerh) / _height;\n\t\t}\n\t\tif (result >= 1.) {\n\t\t\tresult -= 1.;\n\t\t} else {\n\t\t\tresult = 1. - (1. / result);\n\t\t}\n\t\treturn result;\n\t};\n\tif (_width > 0 && _height > 0) {\n\t\t_zoomToDefault = countZoomFor(availableWidth, _availableHeight);\n\t\t_zoomToScreen = countZoomFor(width(), _maxUsedHeight);\n\t} else {\n\t\t_zoomToDefault = _zoomToScreen = 0;\n\t}\n\tconst auto usew = _fullScreenVideo ? width() : availableWidth;\n\tconst auto useh = _fullScreenVideo ? _maxUsedHeight : _availableHeight;\n\tif ((_width > usew) || (_height > useh) || _fullScreenVideo) {\n\t\tconst auto use = _fullScreenVideo ? _zoomToScreen : _zoomToDefault;\n\t\t_zoom = kZoomToScreenLevel;\n\t\tif (use >= 0) {\n\t\t\t_w = qRound(_width * (use + 1));\n\t\t\t_h = qRound(_height * (use + 1));\n\t\t} else {\n\t\t\t_w = qRound(_width / (-use + 1));\n\t\t\t_h = qRound(_height / (-use + 1));\n\t\t}\n\t} else {\n\t\t_zoom = 0;\n\t\t_w = _width;\n\t\t_h = _height;\n\t}\n\t_x = (width() - _w) / 2;\n\t_y = _fullScreenVideo\n\t\t? (_minUsedTop + (_maxUsedHeight - _h) / 2)\n\t\t: (_skipTop + (useh - _h) / 2);\n\t_geometryAnimation.stop();\n}\n\nfloat64 OverlayWidget::radialProgress() const {\n\tif (_document) {\n\t\treturn _documentMedia->progress();\n\t} else if (_photo) {\n\t\treturn _photoMedia->progress();\n\t}\n\treturn 1.;\n}\n\nbool OverlayWidget::radialLoading() const {\n\tif (_streamed) {\n\t\treturn false;\n\t} else if (_document) {\n\t\treturn _document->loading();\n\t} else if (_photo) {\n\t\treturn _photo->displayLoading();\n\t}\n\treturn false;\n}\n\nQRect OverlayWidget::radialRect() const {\n\tif (_photo) {\n\t\treturn _photoRadialRect;\n\t} else if (_document) {\n\t\treturn QRect(\n\t\t\tQPoint(\n\t\t\t\t_docIconRect.x() + ((_docIconRect.width() - st::radialSize.width()) / 2),\n\t\t\t\t_docIconRect.y() + ((_docIconRect.height() - st::radialSize.height()) / 2)),\n\t\t\tst::radialSize);\n\t}\n\treturn QRect();\n}\n\nvoid OverlayWidget::radialStart() {\n\tif (radialLoading() && !_radial.animating()) {\n\t\t_radial.start(radialProgress());\n\t\tif (auto shift = radialTimeShift()) {\n\t\t\t_radial.update(radialProgress(), !radialLoading(), crl::now() + shift);\n\t\t}\n\t}\n}\n\ncrl::time OverlayWidget::radialTimeShift() const {\n\treturn _photo ? st::radialDuration : 0;\n}\n\nbool OverlayWidget::radialAnimationCallback(crl::time now) {\n\tif ((!_document && !_photo) || _streamed) {\n\t\treturn false;\n\t}\n\tconst auto wasAnimating = _radial.animating();\n\tconst auto updated = _radial.update(\n\t\tradialProgress(),\n\t\t!radialLoading(),\n\t\tnow + radialTimeShift());\n\tif ((wasAnimating || _radial.animating())\n\t\t&& (!anim::Disabled() || updated)) {\n\t\tupdate(radialRect());\n\t}\n\tconst auto ready = _document && _documentMedia->loaded();\n\tconst auto streamVideo = ready && _documentMedia->canBePlayed();\n\tconst auto tryOpenImage = ready\n\t\t&& (_document->size < Images::kReadBytesLimit);\n\tif (ready && ((tryOpenImage && !_radial.animating()) || streamVideo)) {\n\t\t_streamingStartPaused = false;\n\t\tif (streamVideo) {\n\t\t\tredisplayContent();\n\t\t} else {\n\t\t\tauto &location = _document->location(true);\n\t\t\tif (location.accessEnable()) {\n\t\t\t\tif (_document->isTheme()\n\t\t\t\t\t|| QImageReader(location.name()).canRead()) {\n\t\t\t\t\tredisplayContent();\n\t\t\t\t}\n\t\t\t\tlocation.accessDisable();\n\t\t\t}\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid OverlayWidget::zoomIn() {\n\tauto newZoom = _zoom;\n\tconst auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault;\n\tif (newZoom == kZoomToScreenLevel) {\n\t\tif (qCeil(full) <= kMaxZoomLevel) {\n\t\t\tnewZoom = qCeil(full);\n\t\t}\n\t} else {\n\t\tif (newZoom < full && (newZoom + 1 > full || (full > kMaxZoomLevel && newZoom == kMaxZoomLevel))) {\n\t\t\tnewZoom = kZoomToScreenLevel;\n\t\t} else if (newZoom < kMaxZoomLevel) {\n\t\t\t++newZoom;\n\t\t}\n\t}\n\tzoomUpdate(newZoom);\n}\n\nvoid OverlayWidget::zoomOut() {\n\tauto newZoom = _zoom;\n\tconst auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault;\n\tif (newZoom == kZoomToScreenLevel) {\n\t\tif (qFloor(full) >= -kMaxZoomLevel) {\n\t\t\tnewZoom = qFloor(full);\n\t\t}\n\t} else {\n\t\tif (newZoom > full && (newZoom - 1 < full || (full < -kMaxZoomLevel && newZoom == -kMaxZoomLevel))) {\n\t\t\tnewZoom = kZoomToScreenLevel;\n\t\t} else if (newZoom > -kMaxZoomLevel) {\n\t\t\t--newZoom;\n\t\t}\n\t}\n\tzoomUpdate(newZoom);\n}\n\nvoid OverlayWidget::zoomReset() {\n\tif (_stories || _fullScreenVideo) {\n\t\treturn;\n\t}\n\tauto newZoom = _zoom;\n\tconst auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault;\n\tif (_zoom == 0) {\n\t\tif (qFloor(full) == qCeil(full) && qRound(full) >= -kMaxZoomLevel && qRound(full) <= kMaxZoomLevel) {\n\t\t\tnewZoom = qRound(full);\n\t\t} else {\n\t\t\tnewZoom = kZoomToScreenLevel;\n\t\t}\n\t} else {\n\t\tnewZoom = 0;\n\t}\n\t_x = -_width / 2;\n\t_y = _skipTop - (_height / 2);\n\tfloat64 z = (_zoom == kZoomToScreenLevel) ? full : _zoom;\n\tif (z >= 0) {\n\t\t_x = qRound(_x * (z + 1));\n\t\t_y = qRound(_y * (z + 1));\n\t} else {\n\t\t_x = qRound(_x / (-z + 1));\n\t\t_y = qRound(_y / (-z + 1));\n\t}\n\t_x += width() / 2;\n\t_y += _availableHeight / 2;\n\tupdate();\n\tzoomUpdate(newZoom);\n}\n\nvoid OverlayWidget::zoomUpdate(int32 &newZoom) {\n\tif (newZoom != kZoomToScreenLevel) {\n\t\twhile ((newZoom < 0 && (-newZoom + 1) > _w) || (-newZoom + 1) > _h) {\n\t\t\t++newZoom;\n\t\t}\n\t}\n\tsetZoomLevel(newZoom);\n}\n\nvoid OverlayWidget::clearSession() {\n\tif (!isHidden()) {\n\t\thide();\n\t}\n\t_sessionLifetime.destroy();\n\tif (!_animations.empty()) {\n\t\t_animations.clear();\n\t\t_stateAnimation.stop();\n\t}\n\tif (!_animationOpacities.empty()) {\n\t\t_animationOpacities.clear();\n\t}\n\tclearStreaming();\n\tsetContext(v::null);\n\t_from = nullptr;\n\t_fromName = QString();\n\tassignMediaPointer(nullptr);\n\t_fullScreenVideo = false;\n\t_caption.clear();\n\t_sharedMedia = nullptr;\n\t_userPhotos = nullptr;\n\t_collage = nullptr;\n\t_session = nullptr;\n}\n\nOverlayWidget::~OverlayWidget() {\n\tclearSession();\n\n\t// Otherwise dropdownHidden() may be called from the destructor.\n\t_dropdown.destroy();\n}\n\nvoid OverlayWidget::assignMediaPointer(DocumentData *document) {\n\t_savePhotoVideoWhenLoaded = SavePhotoVideo::None;\n\t_flip = {};\n\t_photo = nullptr;\n\t_photoMedia = nullptr;\n\t_videoStream = nullptr;\n\tif (_document != document) {\n\t\t_streamedQualityChangeFrame = QImage();\n\t\t_streamedQualityChangeFinished = false;\n\t\tif ((_document = document)) {\n\t\t\t_quality = Core::App().settings().videoQuality();\n\t\t\t_chosenQuality = _document->chooseQuality(_message, _quality);\n\t\t\t_documentMedia = _document->createMediaView();\n\t\t\t_videoCover = LookupVideoCover(_document, _message);\n\t\t\tif (_videoCover) {\n\t\t\t\t_videoCoverMedia = _videoCover->createMediaView();\n\t\t\t\t_videoCoverMedia->wanted(\n\t\t\t\t\tData::PhotoSize::Large,\n\t\t\t\t\tfileOrigin());\n\t\t\t} else {\n\t\t\t\t_videoCoverMedia = nullptr;\n\t\t\t\t_documentMedia->goodThumbnailWanted();\n\t\t\t\t_documentMedia->thumbnailWanted(fileOrigin());\n\t\t\t}\n\t\t} else {\n\t\t\t_chosenQuality = nullptr;\n\t\t\t_documentMedia = nullptr;\n\t\t\t_videoCover = nullptr;\n\t\t\t_videoCoverMedia = nullptr;\n\t\t}\n\t\t_documentLoadingTo = QString();\n\t}\n}\n\nvoid OverlayWidget::assignMediaPointer(not_null<PhotoData*> photo) {\n\t_savePhotoVideoWhenLoaded = SavePhotoVideo::None;\n\t_chosenQuality = nullptr;\n\t_streamedQualityChangeFrame = QImage();\n\t_streamedQualityChangeFinished = false;\n\t_document = nullptr;\n\t_documentMedia = nullptr;\n\t_documentLoadingTo = QString();\n\t_videoCover = nullptr;\n\t_videoCoverMedia = nullptr;\n\t_videoStream = nullptr;\n\tif (_photo != photo) {\n\t\t_flip = {};\n\t\t_photo = photo;\n\t\t_photoMedia = _photo->createMediaView();\n\t\t_photoMedia->wanted(Data::PhotoSize::Small, fileOrigin());\n\t\tif (!_photo->hasVideo() || _photo->videoPlaybackFailed()) {\n\t\t\t_photo->load(fileOrigin(), LoadFromCloudOrLocal, true);\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::assignMediaPointer(\n\t\tstd::shared_ptr<Data::GroupCall> call) {\n\t_savePhotoVideoWhenLoaded = SavePhotoVideo::None;\n\t_chosenQuality = nullptr;\n\t_streamedQualityChangeFrame = QImage();\n\t_streamedQualityChangeFinished = false;\n\t_document = nullptr;\n\t_documentMedia = nullptr;\n\t_documentLoadingTo = QString();\n\t_videoCover = nullptr;\n\t_videoCoverMedia = nullptr;\n\t_flip = {};\n\t_photo = nullptr;\n\t_photoMedia = nullptr;\n\n\t_videoStream = nullptr;\n\t_videoStream = std::make_unique<VideoStream>(\n\t\t_body,\n\t\t_surface.get(),\n\t\t_opengl,\n\t\t_wrap->backend(),\n\t\tuiShow(),\n\t\tstd::move(call),\n\t\t_callLinkSlug,\n\t\t_callJoinMessageId);\n\t_videoStream->closeRequests() | rpl::on_next([=] {\n\t\tclose();\n\t}, _videoStream->lifetime());\n\tif (const auto stories = _stories.get()) {\n\t\tstories->updateVideoStream(_videoStream->call());\n\t\t_videoStream->toggleCommentsOn(stories->commentsShownValue());\n\t}\n\t_widget->lower();\n}\n\nvoid OverlayWidget::clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool active) {\n\t_clickHandlerActive = active;\n\tupdateCursor();\n\tupdate(QRegion(_saveMsg) + captionGeometry());\n}\n\nvoid OverlayWidget::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &p,\n\t\tbool pressed) {\n\t_clickHandlerPressed = pressed;\n\tupdateCursor();\n\tupdate(QRegion(_saveMsg) + captionGeometry());\n}\n\nrpl::lifetime &OverlayWidget::lifetime() {\n\treturn _surface->lifetime();\n}\n\nvoid OverlayWidget::showSaveMsgFile() {\n\tFile::ShowInFolder(_saveMsgFilename);\n}\n\nvoid OverlayWidget::close() {\n\tif (isHidden()) {\n\t\treturn;\n\t}\n\thide();\n\t_helper->clearState();\n}\n\nvoid OverlayWidget::minimize() {\n\tif (isHidden()) {\n\t\treturn;\n\t}\n\t_helper->minimize(_window);\n}\n\nvoid OverlayWidget::toggleFullScreen() {\n\ttoggleFullScreen(!_fullscreen);\n}\n\nvoid OverlayWidget::toggleFullScreen(bool fullscreen) {\n\t_helper->clearState();\n\t_fullscreen = fullscreen;\n\t_windowed = !fullscreen;\n\tinitNormalGeometry();\n\tif constexpr (Platform::IsMac()) {\n\t\t_helper->beforeShow(_fullscreen);\n\t\tupdateGeometry();\n\t\t_helper->afterShow(_fullscreen);\n\t} else if (_fullscreen) {\n\t\tupdateGeometry();\n\t\t_window->showFullScreen();\n\t} else {\n\t\t_wasWindowedMode = false;\n\t\t_window->showNormal();\n\t\tupdateGeometry();\n\t\t_wasWindowedMode = true;\n\t}\n\tsavePosition();\n\t_helper->clearState();\n}\n\nvoid OverlayWidget::activateControls() {\n\tif (!_menu && !_mousePressed && !_touchMove && !_stories) {\n\t\t_controlsHideTimer.callOnce(st::mediaviewWaitHide);\n\t}\n\tif (_fullScreenVideo) {\n\t\tif (_streamed && _streamed->controls) {\n\t\t\t_streamed->controls->showAnimated();\n\t\t}\n\t}\n\tif (_controlsState == ControlsHiding || _controlsState == ControlsHidden) {\n\t\t_controlsState = ControlsShowing;\n\t\t_controlsAnimStarted = crl::now();\n\t\t_controlsOpacity.start(1);\n\t\tif (!_stateAnimation.animating()) {\n\t\t\t_stateAnimation.start();\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::hideControls(bool force) {\n\tif (_stories) {\n\t\t_controlsState = ControlsShown;\n\t\t_controlsOpacity = anim::value(1);\n\t\t_helper->setControlsOpacity(1.);\n\t\treturn;\n\t} else if (!force) {\n\t\tif (!_dropdown->isHidden()\n\t\t\t|| (_streamed\n\t\t\t\t&& _streamed->controls\n\t\t\t\t&& _streamed->controls->hasMenu())\n\t\t\t|| _menu\n\t\t\t|| _mousePressed\n\t\t\t|| _touchMove) {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (_fullScreenVideo && _streamed && _streamed->controls) {\n\t\t_streamed->controls->hideAnimated();\n\t}\n\tif (_controlsState == ControlsHiding || _controlsState == ControlsHidden) return;\n\n\t_lastMouseMovePos = _widget->mapFromGlobal(QCursor::pos());\n\t_controlsState = ControlsHiding;\n\t_controlsAnimStarted = crl::now();\n\t_controlsOpacity.start(0);\n\tif (!_stateAnimation.animating()) {\n\t\t_stateAnimation.start();\n\t}\n}\n\nvoid OverlayWidget::dropdownHidden() {\n\tsetFocus();\n\tif (_stories) {\n\t\t_stories->menuShown(false);\n\t}\n\t_ignoringDropdown = true;\n\t_lastMouseMovePos = _widget->mapFromGlobal(QCursor::pos());\n\tupdateOver(_lastMouseMovePos);\n\t_ignoringDropdown = false;\n\tif (!_controlsHideTimer.isActive()) {\n\t\thideControls(true);\n\t}\n}\n\nvoid OverlayWidget::handleScreenChanged(not_null<QScreen*> screen) {\n\tsubscribeToScreenGeometry();\n\tif (isHidden()) {\n\t\treturn;\n\t}\n\n\tconst auto screenList = QGuiApplication::screens();\n\tDEBUG_LOG((\"Viewer Pos: Screen changed to: %1\")\n\t\t.arg(screenList.indexOf(screen)));\n\n\tmoveToScreen();\n}\n\nvoid OverlayWidget::subscribeToScreenGeometry() {\n\t_screenGeometryLifetime.destroy();\n\tconst auto screen = _window->screen();\n\tif (!screen) {\n\t\treturn;\n\t}\n\tbase::qt_signal_producer(\n\t\tscreen,\n\t\t&QScreen::geometryChanged\n\t) | rpl::filter([=] {\n\t\treturn !isHidden() && !isMinimized() && _fullscreen;\n\t}) | rpl::on_next([=] {\n\t\tupdateGeometry();\n\t}, _screenGeometryLifetime);\n}\n\nvoid OverlayWidget::toMessage() {\n\tif (const auto item = _message) {\n\t\tclose();\n\t\tif (const auto window = findWindow()) {\n\t\t\twindow->showMessage(item);\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::notifyFileDialogShown(bool shown) {\n\t_helper->notifyFileDialogShown(shown);\n}\n\nvoid OverlayWidget::saveAs() {\n\tif (showCopyMediaRestriction(true)) {\n\t\treturn;\n\t} else if (hasCopyMediaRestriction()) {\n\t\tAssert(_stories != nullptr);\n\t\tshowPremiumDownloadPromo();\n\t\treturn;\n\t}\n\tQString file;\n\tif (_document) {\n\t\tconst auto &location = _document->location(true);\n\t\tconst auto bytes = _documentMedia->bytes();\n\t\tif (!bytes.isEmpty() || location.accessEnable()) {\n\t\t\tQFileInfo alreadyInfo(location.name());\n\t\t\tQDir alreadyDir(alreadyInfo.dir());\n\t\t\tQString name = alreadyInfo.fileName(), filter;\n\t\t\tconst auto mimeType = Core::MimeTypeForName(_document->mimeString());\n\t\t\tQStringList p = mimeType.globPatterns();\n\t\t\tQString pattern = p.isEmpty() ? QString() : p.front();\n\t\t\tif (name.isEmpty()) {\n\t\t\t\tname = pattern.isEmpty() ? u\".unknown\"_q : pattern.replace('*', QString());\n\t\t\t}\n\n\t\t\tif (pattern.isEmpty()) {\n\t\t\t\tfilter = QString();\n\t\t\t} else {\n\t\t\t\tfilter = mimeType.filterString() + u\";;\"_q + FileDialog::AllFilesFilter();\n\t\t\t}\n\n\t\t\tfile = FileNameForSave(\n\t\t\t\t_session,\n\t\t\t\ttr::lng_save_file(tr::now),\n\t\t\t\tfilter,\n\t\t\t\tu\"doc\"_q,\n\t\t\t\tname,\n\t\t\t\ttrue,\n\t\t\t\talreadyDir);\n\t\t\tif (!file.isEmpty() && file != location.name()) {\n\t\t\t\tif (bytes.isEmpty()) {\n\t\t\t\t\tQFile(file).remove();\n\t\t\t\t\tQFile(location.name()).copy(file);\n\t\t\t\t} else {\n\t\t\t\t\tQFile f(file);\n\t\t\t\t\tif (f.open(QIODevice::WriteOnly)) {\n\t\t\t\t\t\tf.write(bytes);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (_message) {\n\t\t\t\t\tauto &manager = Core::App().downloadManager();\n\t\t\t\t\tmanager.addLoaded({\n\t\t\t\t\t\t.item = _message,\n\t\t\t\t\t\t.document = _document,\n\t\t\t\t\t}, file, manager.computeNextStartDate());\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (bytes.isEmpty()) {\n\t\t\t\tlocation.accessDisable();\n\t\t\t}\n\t\t} else {\n\t\t\tDocumentSaveClickHandler::SaveAndTrack(\n\t\t\t\t_message ? _message->fullId() : FullMsgId(),\n\t\t\t\t_document,\n\t\t\t\tDocumentSaveClickHandler::Mode::ToNewFile);\n\t\t\tupdateControls();\n\t\t\tupdateOver(_lastMouseMovePos);\n\t\t}\n\t} else if (_photo && _photo->hasVideo()) {\n\t\tconstexpr auto large = Data::PhotoSize::Large;\n\t\tif (const auto bytes = _photoMedia->videoContent(large); !bytes.isEmpty()) {\n\t\t\tconst auto photo = _photo;\n\t\t\tauto filter = u\"Video Files (*.mp4);;\"_q + FileDialog::AllFilesFilter();\n\t\t\tFileDialog::GetWritePath(\n\t\t\t\t_window.get(),\n\t\t\t\ttr::lng_save_video(tr::now),\n\t\t\t\tfilter,\n\t\t\t\tfiledialogDefaultName(\n\t\t\t\t\tu\"photo\"_q,\n\t\t\t\t\tu\".mp4\"_q,\n\t\t\t\t\tQString(),\n\t\t\t\t\tfalse,\n\t\t\t\t\t_photo->date()),\n\t\t\t\tcrl::guard(_window, [=](const QString &result) {\n\t\t\t\t\tQFile f(result);\n\t\t\t\t\tif (!result.isEmpty()\n\t\t\t\t\t\t&& _photo == photo\n\t\t\t\t\t\t&& f.open(QIODevice::WriteOnly)) {\n\t\t\t\t\t\tf.write(bytes);\n\t\t\t\t\t}\n\t\t\t\t}));\n\t\t} else {\n\t\t\t_photo->loadVideo(large, fileOrigin());\n\t\t\t_savePhotoVideoWhenLoaded = SavePhotoVideo::SaveAs;\n\t\t}\n\t} else {\n\t\tif (!_photo || !_photoMedia->loaded()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto media = _photoMedia;\n\t\tconst auto photo = _photo;\n\t\tconst auto filter = u\"JPEG Image (*.jpg);;\"_q\n\t\t\t+ FileDialog::AllFilesFilter();\n\t\tFileDialog::GetWritePath(\n\t\t\t_window.get(),\n\t\t\ttr::lng_save_photo(tr::now),\n\t\t\tfilter,\n\t\t\tfiledialogDefaultName(\n\t\t\t\tu\"photo\"_q,\n\t\t\t\tu\".jpg\"_q,\n\t\t\t\tQString(),\n\t\t\t\tfalse,\n\t\t\t\t_photo->date()),\n\t\t\tcrl::guard(_window, [=](const QString &result) {\n\t\t\t\tif (!result.isEmpty() && _photo == photo) {\n\t\t\t\t\tmedia->saveToFile(result);\n\t\t\t\t}\n\t\t\t}));\n\t}\n\tactivate();\n}\n\nvoid OverlayWidget::handleDocumentClick() {\n\tif (_document->loading()) {\n\t\tsaveCancel();\n\t} else {\n\t\t_reShow = true;\n\t\tData::ResolveDocument(\n\t\t\tfindWindow(),\n\t\t\t_document,\n\t\t\t_message,\n\t\t\t_topicRootId,\n\t\t\t_monoforumPeerId,\n\t\t\t_drawButtonEnabled);\n\t\tif (_document && _document->loading() && !_radial.animating()) {\n\t\t\t_radial.start(_documentMedia->progress());\n\t\t}\n\t\t_reShow = false;\n\t}\n}\n\nbool OverlayWidget::canShareAtTime() const {\n\tconst auto media = _message ? _message->media() : nullptr;\n\treturn _document\n\t\t&& media\n\t\t&& _streamed\n\t\t&& (_document == media->document())\n\t\t&& _document->isVideoFile()\n\t\t&& !media->webpage();\n}\n\nTimeId OverlayWidget::shareAtVideoTimestamp() const {\n\treturn _streamedPosition / crl::time(1000);\n}\n\nvoid OverlayWidget::shareAtTime() {\n\tif (!canShareAtTime()) {\n\t\treturn;\n\t}\n\tif (!_streamed->instance.player().paused()\n\t\t&& !_streamed->instance.player().finished()) {\n\t\tplaybackPauseResume();\n\t}\n\tconst auto show = uiShow();\n\tconst auto timestamp = shareAtVideoTimestamp();\n\tshow->show(Stories::PrepareShareAtTimeBox(show, _message, timestamp));\n}\n\nvoid OverlayWidget::downloadMedia() {\n\tif (!_photo && !_document) {\n\t\treturn;\n\t} else if (Core::App().settings().askDownloadPath()) {\n\t\treturn saveAs();\n\t} else if (hasCopyMediaRestriction()) {\n\t\tif (_stories && !hasCopyMediaRestriction(true)) {\n\t\t\tshowPremiumDownloadPromo();\n\t\t}\n\t\treturn;\n\t}\n\n\tQString path;\n\tconst auto session = _photo ? &_photo->session() : &_document->session();\n\tif (Core::App().settings().downloadPath().isEmpty()) {\n\t\tpath = File::DefaultDownloadPath(session);\n\t} else if (Core::App().settings().downloadPath() == FileDialog::Tmp()) {\n\t\tpath = session->local().tempDirectory();\n\t} else {\n\t\tpath = Core::App().settings().downloadPath();\n\t}\n\tif (path.isEmpty()) return;\n\tQString toName;\n\tif (_document) {\n\t\tconst auto &location = _document->location(true);\n\t\tif (location.accessEnable()) {\n\t\t\tif (!QDir().exists(path)) QDir().mkpath(path);\n\t\t\ttoName = filedialogNextFilename(\n\t\t\t\t_document->filename(),\n\t\t\t\tlocation.name(),\n\t\t\t\tpath);\n\t\t\tif (!toName.isEmpty() && toName != location.name()) {\n\t\t\t\tQFile(toName).remove();\n\t\t\t\tif (!QFile(location.name()).copy(toName)) {\n\t\t\t\t\ttoName = QString();\n\t\t\t\t} else if (_message) {\n\t\t\t\t\tauto &manager = Core::App().downloadManager();\n\t\t\t\t\tmanager.addLoaded({\n\t\t\t\t\t\t.item = _message,\n\t\t\t\t\t\t.document = _document,\n\t\t\t\t\t}, toName, manager.computeNextStartDate());\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_stories && !toName.isEmpty()) {\n\t\t\t\tshowSaveMsgToast(toName, tr::lng_mediaview_video_saved_to);\n\t\t\t}\n\t\t\tlocation.accessDisable();\n\t\t} else {\n\t\t\tif (_document->filepath(true).isEmpty()\n\t\t\t\t&& !_document->loading()) {\n\t\t\t\tconst auto document = _document;\n\t\t\t\tconst auto checkSaveStarted = [=] {\n\t\t\t\t\tif (isHidden() || _document != document) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t_documentLoadingTo = _document->loadingFilePath();\n\t\t\t\t\tif (_stories && _documentLoadingTo.isEmpty()) {\n\t\t\t\t\t\tconst auto toName = _document->filepath(true);\n\t\t\t\t\t\tif (!toName.isEmpty()) {\n\t\t\t\t\t\t\tshowSaveMsgToast(\n\t\t\t\t\t\t\t\ttoName,\n\t\t\t\t\t\t\t\ttr::lng_mediaview_video_saved_to);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tDocumentSaveClickHandler::SaveAndTrack(\n\t\t\t\t\t_message ? _message->fullId() : FullMsgId(),\n\t\t\t\t\t_document,\n\t\t\t\t\tDocumentSaveClickHandler::Mode::ToFile,\n\t\t\t\t\tcrl::guard(_widget, checkSaveStarted));\n\t\t\t} else {\n\t\t\t\t_saveVisible = computeSaveButtonVisible();\n\t\t\t\tupdate(_saveNavOver);\n\t\t\t}\n\t\t\tupdateOver(_lastMouseMovePos);\n\t\t}\n\t} else if (_photo && _photo->hasVideo()) {\n\t\tif (!_photoMedia->videoContent(Data::PhotoSize::Large).isEmpty()) {\n\t\t\tif (!QDir().exists(path)) {\n\t\t\t\tQDir().mkpath(path);\n\t\t\t}\n\t\t\ttoName = filedialogDefaultName(u\"photo\"_q, u\".mp4\"_q, path);\n\t\t\tif (!_photoMedia->saveToFile(toName)) {\n\t\t\t\ttoName = QString();\n\t\t\t}\n\t\t} else {\n\t\t\t_photo->loadVideo(Data::PhotoSize::Large, fileOrigin());\n\t\t\t_savePhotoVideoWhenLoaded = SavePhotoVideo::QuickSave;\n\t\t}\n\t} else {\n\t\tif (!_photo || !_photoMedia->loaded()) {\n\t\t\t_saveVisible = computeSaveButtonVisible();\n\t\t\tupdate(_saveNavOver);\n\t\t} else {\n\t\t\tif (!QDir().exists(path)) {\n\t\t\t\tQDir().mkpath(path);\n\t\t\t}\n\t\t\ttoName = filedialogDefaultName(u\"photo\"_q, u\".jpg\"_q, path);\n\t\t\tconst auto saved = _photoMedia->saveToFile(toName);\n\t\t\tif (!saved) {\n\t\t\t\ttoName = QString();\n\t\t\t}\n\t\t}\n\t}\n\tif (!toName.isEmpty()) {\n\t\tshowSaveMsgToast(toName, (_stories && _document)\n\t\t\t? tr::lng_mediaview_video_saved_to\n\t\t\t: tr::lng_mediaview_saved_to);\n\t}\n}\n\nvoid OverlayWidget::saveCancel() {\n\tif (_document && _document->loading()) {\n\t\t_document->cancel();\n\t\tif (_documentMedia->canBePlayed()) {\n\t\t\tredisplayContent();\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::showInFolder() {\n\tif (!_document) return;\n\n\tauto filepath = _document->filepath(true);\n\tif (!filepath.isEmpty()) {\n\t\tFile::ShowInFolder(filepath);\n\t\tif (!_windowed) {\n\t\t\tclose();\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::forwardMedia() {\n\tif (!_session) {\n\t\treturn;\n\t}\n\tconst auto &active = _session->windows();\n\tif (active.empty()) {\n\t\treturn;\n\t}\n\tconst auto id = (_message && _message->allowsForward())\n\t\t? _message->fullId()\n\t\t: FullMsgId();\n\tif (id) {\n\t\tif (!_windowed) {\n\t\t\tclose();\n\t\t}\n\t\tWindow::ShowForwardMessagesBox(active.front(), { 1, id });\n\t}\n}\n\nvoid OverlayWidget::deleteMedia() {\n\tif (_stories) {\n\t\t_stories->deleteRequested();\n\t\treturn;\n\t} else if (!_session) {\n\t\treturn;\n\t}\n\n\tconst auto session = _session;\n\tconst auto photo = _photo;\n\tconst auto message = _message;\n\tconst auto deletingPeerPhoto = [&] {\n\t\tif (!_message) {\n\t\t\treturn true;\n\t\t} else if (_photo && _history) {\n\t\t\tif (_history->peer->userpicPhotoId() == _photo->id) {\n\t\t\t\treturn _firstOpenedPeerPhoto;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}();\n\tclose();\n\n\tif (const auto window = findWindow()) {\n\t\tif (deletingPeerPhoto) {\n\t\t\tif (photo) {\n\t\t\t\twindow->show(\n\t\t\t\t\tUi::MakeConfirmBox({\n\t\t\t\t\t\t.text = tr::lng_delete_photo_sure(),\n\t\t\t\t\t\t.confirmed = crl::guard(_widget, [=] {\n\t\t\t\t\t\t\tsession->api().peerPhoto().clear(photo);\n\t\t\t\t\t\t\twindow->hideLayer();\n\t\t\t\t\t\t}),\n\t\t\t\t\t\t.confirmText = tr::lng_box_delete(),\n\t\t\t\t\t}),\n\t\t\t\t\tUi::LayerOption::CloseOther);\n\t\t\t}\n\t\t} else if (message) {\n\t\t\tconst auto suggestModerateActions = true;\n\t\t\twindow->show(\n\t\t\t\tBox<DeleteMessagesBox>(message, suggestModerateActions),\n\t\t\t\tUi::LayerOption::CloseOther);\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::showMediaOverview() {\n\tif (_menu) {\n\t\t_menu->hideMenu(true);\n\t}\n\tupdate();\n\tif (const auto overviewType = computeOverviewType()) {\n\t\tif (!_windowed) {\n\t\t\tclose();\n\t\t}\n\t\tif (SharedMediaOverviewType(*overviewType)) {\n\t\t\tif (const auto window = findWindow()) {\n\t\t\t\tconst auto topic = _topicRootId\n\t\t\t\t\t? _history->peer->forumTopicFor(_topicRootId)\n\t\t\t\t\t: nullptr;\n\t\t\t\tconst auto sublist = _monoforumPeerId\n\t\t\t\t\t? _history->peer->monoforumSublistFor(_monoforumPeerId)\n\t\t\t\t\t: nullptr;\n\t\t\t\tif (_topicRootId && !topic) {\n\t\t\t\t\treturn;\n\t\t\t\t} else if (_monoforumPeerId && !sublist) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\twindow->showSection(_topicRootId\n\t\t\t\t\t? std::make_shared<Info::Memento>(\n\t\t\t\t\t\ttopic,\n\t\t\t\t\t\tInfo::Section(*overviewType))\n\t\t\t\t\t: _monoforumPeerId\n\t\t\t\t\t? std::make_shared<Info::Memento>(\n\t\t\t\t\t\tsublist,\n\t\t\t\t\t\tInfo::Section(*overviewType))\n\t\t\t\t\t: std::make_shared<Info::Memento>(\n\t\t\t\t\t\t_history->peer,\n\t\t\t\t\t\tInfo::Section(*overviewType)));\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::recognize() {\n\t_showRecognitionResults = !_showRecognitionResults;\n\t_recognitionAnimation.start(\n\t\t[=] { update(); },\n\t\t_showRecognitionResults ? 0. : 1.,\n\t\t_showRecognitionResults ? 1. : 0.,\n\t\tst::widgetFadeDuration);\n}\n\nvoid OverlayWidget::draw() {\n\tif (!_session) {\n\t\treturn;\n\t}\n\tconst auto photoId = _photo ? _photo->id : PhotoId();\n\tconst auto documentId = (_document && _document->isImage())\n\t\t? _document->id\n\t\t: DocumentId();\n\tif (!photoId && !documentId) {\n\t\treturn;\n\t}\n\t_session->data().requestDrawToReply({\n\t\t_message ? _message->fullId() : FullMsgId(),\n\t\tphotoId,\n\t\tdocumentId,\n\t});\n\tclose();\n}\n\nvoid OverlayWidget::copyMedia() {\n\tif (showCopyMediaRestriction()) {\n\t\treturn;\n\t}\n\t_dropdown->hideAnimated(Ui::DropdownMenu::HideOption::IgnoreShow);\n\tif (_document) {\n\t\tconst auto filepath = _document->filepath(true);\n\t\tauto image = transformedShownContent();\n\t\tif (!image.isNull() || !filepath.isEmpty()) {\n\t\t\tauto mime = std::make_unique<QMimeData>();\n\t\t\tif (!image.isNull()) {\n\t\t\t\tmime->setImageData(std::move(image));\n\t\t\t}\n\t\t\tif (!filepath.isEmpty() && !videoShown()) {\n\t\t\t\tmime->setUrls({ QUrl::fromLocalFile(filepath) });\n\t\t\t\tKUrlMimeData::exportUrlsToPortal(mime.get());\n\t\t\t}\n\t\t\tQGuiApplication::clipboard()->setMimeData(mime.release());\n\t\t}\n\t} else if (_photo && _photoMedia->loaded()) {\n\t\t_photoMedia->setToClipboard();\n\t}\n}\n\nvoid OverlayWidget::showAttachedStickers() {\n\tif (!_session) {\n\t\treturn;\n\t}\n\tconst auto &active = _session->windows();\n\tif (active.empty()) {\n\t\treturn;\n\t}\n\tconst auto window = active.front();\n\tauto &attachedStickers = _session->api().attachedStickers();\n\tif (_photo) {\n\t\tattachedStickers.requestAttachedStickerSets(window, _photo);\n\t} else if (_document) {\n\t\tattachedStickers.requestAttachedStickerSets(window, _document);\n\t} else {\n\t\treturn;\n\t}\n\tif (!_windowed) {\n\t\tclose();\n\t}\n}\n\nauto OverlayWidget::sharedMediaType() const\n-> std::optional<SharedMediaType> {\n\tusing Type = SharedMediaType;\n\tif (_message) {\n\t\tif (const auto media = _message->media()) {\n\t\t\tif (media->webpage() || media->invoice()) {\n\t\t\t\treturn std::nullopt;\n\t\t\t} else if (const auto poll = media->poll()) {\n\t\t\t\tconst auto isPollMedia = [&](const auto &item) {\n\t\t\t\t\treturn IsPollAnswerMediaItem(poll, item)\n\t\t\t\t\t\t|| (PollInputMediaItem(poll->solutionMedia)\n\t\t\t\t\t\t\t== item);\n\t\t\t\t};\n\t\t\t\tif ((_photo\n\t\t\t\t\t\t&& isPollMedia(WebPageCollage::Item(_photo)))\n\t\t\t\t\t|| (_document\n\t\t\t\t\t\t&& isPollMedia(\n\t\t\t\t\t\t\tWebPageCollage::Item(_document)))) {\n\t\t\t\t\treturn std::nullopt;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (_photo) {\n\t\t\tif (_message->isService()) {\n\t\t\t\treturn Type::ChatPhoto;\n\t\t\t}\n\t\t\treturn Type::PhotoVideo;\n\t\t} else if (_document) {\n\t\t\tif (_document->isGifv()) {\n\t\t\t\treturn Type::GIF;\n\t\t\t} else if (_document->isVideoFile()) {\n\t\t\t\treturn Type::PhotoVideo;\n\t\t\t}\n\t\t\treturn Type::File;\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nauto OverlayWidget::sharedMediaKey() const -> std::optional<SharedMediaKey> {\n\tif (!_message\n\t\t&& _peer\n\t\t&& !_user\n\t\t&& _photo\n\t\t&& _peer->userpicPhotoId() == _photo->id) {\n\t\treturn SharedMediaKey{\n\t\t\t_history->peer->id,\n\t\t\tMsgId(0), // topicRootId\n\t\t\tPeerId(0), // monoforumPeerId\n\t\t\t_migrated ? _migrated->peer->id : 0,\n\t\t\tSharedMediaType::ChatPhoto,\n\t\t\t_photo\n\t\t};\n\t}\n\tif (!_message) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto isScheduled = _message->isScheduled();\n\tconst auto keyForType = [&](SharedMediaType type) -> SharedMediaKey {\n\t\treturn {\n\t\t\t_history->peer->id,\n\t\t\t(isScheduled\n\t\t\t\t? SparseIdsMergedSlice::kScheduledTopicId\n\t\t\t\t: _topicRootId),\n\t\t\t(isScheduled ? PeerId() : _monoforumPeerId),\n\t\t\t_migrated ? _migrated->peer->id : 0,\n\t\t\ttype,\n\t\t\t(_message->history() == _history\n\t\t\t\t? _message->id\n\t\t\t\t: (_message->id - ServerMaxMsgId))\n\t\t};\n\t};\n\tif (!_message->isRegular() && !isScheduled) {\n\t\treturn std::nullopt;\n\t}\n\treturn sharedMediaType() | keyForType;\n}\n\nData::FileOrigin OverlayWidget::fileOrigin() const {\n\tif (_stories) {\n\t\treturn _stories->fileOrigin();\n\t} else if (_message) {\n\t\treturn _message->fullId();\n\t} else if (_photo && _user) {\n\t\tconst auto dominated = (_user->hasPersonalPhoto()\n\t\t\t\t&& _photo->id == _user->userpicPhotoId())\n\t\t\t|| SyncUserFallbackPhotoViewer(_user) == _photo->id;\n\t\tif (dominated) {\n\t\t\treturn Data::FileOriginFullUser(peerToUser(_user->id));\n\t\t}\n\t\treturn Data::FileOriginUserPhoto(peerToUser(_user->id), _photo->id);\n\t} else if (_photo && _peer && _peer->userpicPhotoId() == _photo->id) {\n\t\treturn Data::FileOriginPeerPhoto(_peer->id);\n\t}\n\treturn Data::FileOrigin();\n}\n\nData::FileOrigin OverlayWidget::fileOrigin(const Entity &entity) const {\n\tif (const auto item = entity.item) {\n\t\treturn item->fullId();\n\t} else if (!v::is<not_null<PhotoData*>>(entity.data)) {\n\t\treturn Data::FileOrigin();\n\t}\n\tconst auto photo = v::get<not_null<PhotoData*>>(entity.data);\n\tif (_user) {\n\t\tconst auto dominated = (_user->hasPersonalPhoto()\n\t\t\t\t&& photo->id == _user->userpicPhotoId())\n\t\t\t|| SyncUserFallbackPhotoViewer(_user) == photo->id;\n\t\tif (dominated) {\n\t\t\treturn Data::FileOriginFullUser(peerToUser(_user->id));\n\t\t}\n\t\treturn Data::FileOriginUserPhoto(peerToUser(_user->id), photo->id);\n\t} else if (_peer && _peer->userpicPhotoId() == photo->id) {\n\t\treturn Data::FileOriginPeerPhoto(_peer->id);\n\t}\n\treturn Data::FileOrigin();\n}\n\nbool OverlayWidget::validSharedMedia() const {\n\tif (auto key = sharedMediaKey()) {\n\t\tif (!_sharedMedia) {\n\t\t\treturn false;\n\t\t}\n\t\tusing Key = SharedMediaWithLastSlice::Key;\n\t\tauto inSameDomain = [](const Key &a, const Key &b) {\n\t\t\treturn (a.type == b.type)\n\t\t\t\t&& (a.peerId == b.peerId)\n\t\t\t\t&& (a.topicRootId == b.topicRootId)\n\t\t\t\t&& (a.migratedPeerId == b.migratedPeerId);\n\t\t};\n\t\tauto countDistanceInData = [&](const Key &a, const Key &b) {\n\t\t\treturn [&](const SharedMediaWithLastSlice &data) {\n\t\t\t\treturn inSameDomain(a, b)\n\t\t\t\t\t? data.distance(a, b)\n\t\t\t\t\t: std::optional<int>();\n\t\t\t};\n\t\t};\n\n\t\tif (key == _sharedMedia->key) {\n\t\t\treturn true;\n\t\t} else if (!_sharedMediaDataKey\n\t\t\t|| _sharedMedia->key != *_sharedMediaDataKey) {\n\t\t\treturn false;\n\t\t}\n\t\tauto distance = _sharedMediaData\n\t\t\t| countDistanceInData(*key, _sharedMedia->key)\n\t\t\t| func::abs;\n\t\tif (distance) {\n\t\t\treturn (*distance < kIdsPreloadAfter);\n\t\t}\n\t}\n\treturn (_sharedMedia == nullptr);\n}\n\nvoid OverlayWidget::validateSharedMedia() {\n\tif (const auto key = sharedMediaKey()) {\n\t\tAssert(_history != nullptr);\n\n\t\t_sharedMedia = std::make_unique<SharedMedia>(*key);\n\t\tauto viewer = (key->type == SharedMediaType::ChatPhoto)\n\t\t\t? SharedMediaWithLastReversedViewer\n\t\t\t: SharedMediaWithLastViewer;\n\t\tviewer(\n\t\t\t&_history->session(),\n\t\t\t*key,\n\t\t\tkIdsLimit,\n\t\t\tkIdsLimit\n\t\t) | rpl::on_next([this](\n\t\t\t\tSharedMediaWithLastSlice &&update) {\n\t\t\thandleSharedMediaUpdate(std::move(update));\n\t\t}, _sharedMedia->lifetime);\n\t} else {\n\t\t_sharedMedia = nullptr;\n\t\t_sharedMediaData = std::nullopt;\n\t\t_sharedMediaDataKey = std::nullopt;\n\t}\n}\n\nvoid OverlayWidget::handleSharedMediaUpdate(SharedMediaWithLastSlice &&update) {\n\tif ((!_photo && !_document) || !_sharedMedia) {\n\t\t_sharedMediaData = std::nullopt;\n\t\t_sharedMediaDataKey = std::nullopt;\n\t} else {\n\t\t_sharedMediaData = std::move(update);\n\t\t_sharedMediaDataKey = _sharedMedia->key;\n\t}\n\tfindCurrent();\n\tupdateControls();\n\tpreloadData(0);\n}\n\nstd::optional<OverlayWidget::UserPhotosKey> OverlayWidget::userPhotosKey() const {\n\tif (!_message && _user && _photo) {\n\t\treturn UserPhotosKey{ peerToUser(_user->id), _photo->id };\n\t}\n\treturn std::nullopt;\n}\n\nbool OverlayWidget::validUserPhotos() const {\n\tif (const auto key = userPhotosKey()) {\n\t\tif (!_userPhotos) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto countDistanceInData = [](const auto &a, const auto &b) {\n\t\t\treturn [&](const UserPhotosSlice &data) {\n\t\t\t\treturn data.distance(a, b);\n\t\t\t};\n\t\t};\n\n\t\tconst auto distance = (key == _userPhotos->key) ? 0 :\n\t\t\t_userPhotosData\n\t\t\t| countDistanceInData(*key, _userPhotos->key)\n\t\t\t| func::abs;\n\t\tif (distance) {\n\t\t\treturn (*distance < kIdsPreloadAfter);\n\t\t}\n\t}\n\treturn (_userPhotos == nullptr);\n}\n\nvoid OverlayWidget::validateUserPhotos() {\n\tif (const auto key = userPhotosKey()) {\n\t\tAssert(_user != nullptr);\n\n\t\t_userPhotos = std::make_unique<UserPhotos>(*key);\n\t\tUserPhotosReversedViewer(\n\t\t\t&_user->session(),\n\t\t\t*key,\n\t\t\tkIdsLimit,\n\t\t\tkIdsLimit\n\t\t) | rpl::on_next([this](\n\t\t\t\tUserPhotosSlice &&update) {\n\t\t\thandleUserPhotosUpdate(std::move(update));\n\t\t}, _userPhotos->lifetime);\n\t} else {\n\t\t_userPhotos = nullptr;\n\t\t_userPhotosData = std::nullopt;\n\t}\n}\n\nvoid OverlayWidget::handleUserPhotosUpdate(UserPhotosSlice &&update) {\n\tif (!_photo || !_userPhotos) {\n\t\t_userPhotosData = std::nullopt;\n\t} else {\n\t\t_userPhotosData = std::move(update);\n\t}\n\tfindCurrent();\n\tupdateControls();\n\tpreloadData(0);\n}\n\nstd::optional<OverlayWidget::CollageKey> OverlayWidget::collageKey() const {\n\tif (_message) {\n\t\tif (const auto media = _message->media()) {\n\t\t\tif (const auto page = media->webpage()) {\n\t\t\t\tfor (const auto &item : page->collage.items) {\n\t\t\t\t\tif (item == _photo || item == _document) {\n\t\t\t\t\t\treturn item;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (const auto invoice = media->invoice()) {\n\t\t\t\tfor (const auto &item : invoice->extendedMedia) {\n\t\t\t\t\tif (_photo && item->photo() == _photo) {\n\t\t\t\t\t\treturn _photo;\n\t\t\t\t\t} else if (_document && item->document() == _document) {\n\t\t\t\t\t\treturn _document;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (const auto poll = media->poll()) {\n\t\t\t\tif (_photo\n\t\t\t\t\t&& IsPollAnswerMediaItem(\n\t\t\t\t\t\tpoll,\n\t\t\t\t\t\tWebPageCollage::Item(_photo))) {\n\t\t\t\t\treturn _photo;\n\t\t\t\t} else if (_document\n\t\t\t\t\t&& IsPollAnswerMediaItem(\n\t\t\t\t\t\tpoll,\n\t\t\t\t\t\tWebPageCollage::Item(_document))) {\n\t\t\t\t\treturn _document;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nbool OverlayWidget::validCollage() const {\n\tif (const auto key = collageKey()) {\n\t\tif (!_collage) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (key == _collage->key) {\n\t\t\treturn true;\n\t\t} else if (_collageData) {\n\t\t\tconst auto &items = _collageData->items;\n\t\t\tif (ranges::find(items, *key) != end(items)\n\t\t\t\t&& ranges::find(items, _collage->key) != end(items)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\treturn (_collage == nullptr);\n}\n\nvoid OverlayWidget::validateCollage() {\n\tif (const auto key = collageKey()) {\n\t\t_collage = std::make_unique<Collage>(*key);\n\t\t_collageData = WebPageCollage();\n\t\tif (_message) {\n\t\t\tif (const auto media = _message->media()) {\n\t\t\t\tif (const auto page = media->webpage()) {\n\t\t\t\t\t_collageData = page->collage;\n\t\t\t\t} else if (const auto invoice = media->invoice()) {\n\t\t\t\t\tauto &data = *_collageData;\n\t\t\t\t\tdata.items.reserve(invoice->extendedMedia.size());\n\t\t\t\t\tfor (const auto &item : invoice->extendedMedia) {\n\t\t\t\t\t\tif (const auto photo = item->photo()) {\n\t\t\t\t\t\t\tdata.items.push_back(photo);\n\t\t\t\t\t\t} else if (const auto document = item->document()) {\n\t\t\t\t\t\t\tdata.items.push_back(document);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (const auto poll = media->poll()) {\n\t\t\t\t\t_collageData = PollAnswersCollage(poll);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\t_collage = nullptr;\n\t\t_collageData = std::nullopt;\n\t}\n}\n\nvoid OverlayWidget::refreshMediaViewer() {\n\tif (!validSharedMedia()) {\n\t\tvalidateSharedMedia();\n\t}\n\tif (!validUserPhotos()) {\n\t\tvalidateUserPhotos();\n\t}\n\tif (!validCollage()) {\n\t\tvalidateCollage();\n\t}\n\tfindCurrent();\n\tupdateControls();\n}\n\nvoid OverlayWidget::refreshFromLabel() {\n\tif (_message) {\n\t\t_from = _message->originalSender();\n\t\tif (const auto info = _message->originalHiddenSenderInfo()) {\n\t\t\t_fromName = info->name;\n\t\t} else {\n\t\t\tAssert(_from != nullptr);\n\t\t\tconst auto from = _from->migrateTo()\n\t\t\t\t? _from->migrateTo()\n\t\t\t\t: _from;\n\t\t\t_fromName = from->name();\n\t\t}\n\t} else {\n\t\t_from = _user;\n\t\t_fromName = _user ? _user->name() : QString();\n\t}\n}\n\nvoid OverlayWidget::refreshCaption() {\n\t_caption = Ui::Text::String();\n\tconst auto caption = StripQuoteEntities([&] {\n\t\tif (_stories) {\n\t\t\treturn _stories->captionText();\n\t\t} else if (_message) {\n\t\t\tif (const auto media = _message->media()) {\n\t\t\t\tif (media->webpage()) {\n\t\t\t\t\tif (_message->isSponsored()) {\n\t\t\t\t\t\treturn TextWithEntities()\n\t\t\t\t\t\t\t.append(tr::bold(media->webpage()->title))\n\t\t\t\t\t\t\t.append('\\n')\n\t\t\t\t\t\t\t.append(media->webpage()->description);\n\t\t\t\t\t}\n\t\t\t\t\treturn TextWithEntities();\n\t\t\t\t} else if (const auto poll = media->poll()) {\n\t\t\t\t\tconst auto current = _photo\n\t\t\t\t\t\t? WebPageCollage::Item(_photo)\n\t\t\t\t\t\t: WebPageCollage::Item(_document);\n\t\t\t\t\tfor (const auto &answer : poll->answers) {\n\t\t\t\t\t\tconst auto candidate = PollAnswerMediaItem(answer);\n\t\t\t\t\t\tif (candidate && (*candidate == current)) {\n\t\t\t\t\t\t\treturn answer.text;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tconst auto solution = PollInputMediaItem(\n\t\t\t\t\t\tpoll->solutionMedia);\n\t\t\t\t\tif (solution && (*solution == current)) {\n\t\t\t\t\t\treturn poll->solution;\n\t\t\t\t\t}\n\t\t\t\t\treturn TextWithEntities();\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn _message->translatedText();\n\t\t}\n\t\treturn TextWithEntities();\n\t}());\n\tif (caption.text.isEmpty()) {\n\t\tif (_streamed && _streamed->controls) {\n\t\t\t_streamed->controls->setTimestamps({});\n\t\t\trefreshClipControllerGeometry();\n\t\t}\n\t\treturn;\n\t}\n\n\tusing namespace HistoryView;\n\t_caption = Ui::Text::String(st::msgMinWidth);\n\tconst auto duration = (_streamed && _document && _message)\n\t\t? DurationForTimestampLinks(_document)\n\t\t: 0;\n\tconst auto base = duration\n\t\t? TimestampLinkBase(_document, _message->fullId())\n\t\t: QString();\n\tconst auto captionRepaint = [=] {\n\t\tif (_fullScreenVideo || !_controlsOpacity.current()) {\n\t\t\treturn;\n\t\t}\n\t\tupdate(captionGeometry());\n\t};\n\tconst auto context = Core::TextContext({\n\t\t.session = (_stories\n\t\t\t? _storiesSession\n\t\t\t: &_message->history()->session()),\n\t\t.repaint = captionRepaint,\n\t});\n\tauto captionText = base.isEmpty()\n\t\t? caption\n\t\t: AddTimestampLinks(caption, duration, base);\n\trefreshTimestampDividers(captionText, duration);\n\t_caption.setMarkedText(\n\t\tst::mediaviewCaptionStyle,\n\t\tstd::move(captionText),\n\t\t(_message\n\t\t\t? Ui::ItemTextOptions(_message)\n\t\t\t: Ui::ItemTextDefaultOptions()),\n\t\tcontext);\n\tif (_caption.hasSpoilers()) {\n\t\tconst auto weak = base::make_weak(widget());\n\t\t_caption.setSpoilerLinkFilter([=](const ClickContext &context) {\n\t\t\treturn (weak != nullptr);\n\t\t});\n\t}\n}\n\nvoid OverlayWidget::refreshTimestampDividers(\n\t\tconst TextWithEntities &caption,\n\t\tTimeId duration) {\n\tif (!_streamed || !_streamed->controls || duration <= 0) {\n\t\treturn;\n\t}\n\tusing Data = PlaybackControls::TimestampData;\n\tauto timestamps = std::vector<Data>();\n\tfor (const auto &entity : caption.entities) {\n\t\tif (entity.type() != EntityType::CustomUrl\n\t\t\t|| !entity.data().startsWith(\n\t\t\t\tu\"internal:media_timestamp\"_q)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto tPos = entity.data().lastIndexOf(u\"&t=\"_q);\n\t\tif (tPos < 0) {\n\t\t\tcontinue;\n\t\t}\n\t\tauto ok = false;\n\t\tconst auto time = entity.data().mid(tPos + 3).toInt(&ok);\n\t\tif (!ok || time < 0 || time > duration) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto offset = entity.offset();\n\t\tconst auto atLineStart = (offset == 0)\n\t\t\t|| (caption.text[offset - 1] == '\\n');\n\t\tauto label = QString();\n\t\tif (atLineStart) {\n\t\t\tconst auto entityEnd = offset + entity.length();\n\t\t\tconst auto nl = caption.text.indexOf('\\n', entityEnd);\n\t\t\tconst auto lineEnd = (nl >= 0)\n\t\t\t\t? nl\n\t\t\t\t: caption.text.size();\n\t\t\tlabel = caption.text.mid(\n\t\t\t\tentityEnd,\n\t\t\t\tlineEnd - entityEnd).trimmed();\n\t\t\tif (label.startsWith(u\"- \"_q)) {\n\t\t\t\tlabel = label.mid(2).trimmed();\n\t\t\t} else if (label.startsWith(u\"-\"_q)) {\n\t\t\t\tlabel = label.mid(1).trimmed();\n\t\t\t}\n\t\t}\n\t\ttimestamps.push_back({\n\t\t\t.position = float64(time) / duration,\n\t\t\t.label = std::move(label),\n\t\t});\n\t}\n\t_streamed->controls->setTimestamps(std::move(timestamps));\n\trefreshClipControllerGeometry();\n}\n\nvoid OverlayWidget::refreshGroupThumbs() {\n\tconst auto existed = (_groupThumbs != nullptr);\n\tif (_index && _sharedMediaData) {\n\t\tView::GroupThumbs::Refresh(\n\t\t\t_session,\n\t\t\t_groupThumbs,\n\t\t\t*_sharedMediaData,\n\t\t\t*_index,\n\t\t\t_groupThumbsAvailableWidth);\n\t} else if (_index && _userPhotosData) {\n\t\tView::GroupThumbs::Refresh(\n\t\t\t_session,\n\t\t\t_groupThumbs,\n\t\t\t*_userPhotosData,\n\t\t\t*_index,\n\t\t\t_groupThumbsAvailableWidth);\n\t} else if (_index && _collageData) {\n\t\tconst auto messageId = _message ? _message->fullId() : FullMsgId();\n\t\tView::GroupThumbs::Refresh(\n\t\t\t_session,\n\t\t\t_groupThumbs,\n\t\t\t{ messageId, &*_collageData },\n\t\t\t*_index,\n\t\t\t_groupThumbsAvailableWidth);\n\t} else if (_groupThumbs) {\n\t\t_groupThumbs->clear();\n\t\t_groupThumbs->resizeToWidth(_groupThumbsAvailableWidth);\n\t}\n\tif (_groupThumbs && !existed) {\n\t\tinitGroupThumbs();\n\t}\n}\n\nvoid OverlayWidget::initGroupThumbs() {\n\tExpects(_groupThumbs != nullptr);\n\n\t_groupThumbs->updateRequests(\n\t) | rpl::on_next([this](QRect rect) {\n\t\tconst auto shift = (width() / 2);\n\t\t_groupThumbsRect = QRect(\n\t\t\tshift + rect.x(),\n\t\t\t_groupThumbsTop,\n\t\t\trect.width(),\n\t\t\t_groupThumbs->height());\n\t\tupdate(_groupThumbsRect);\n\t}, _groupThumbs->lifetime());\n\n\t_groupThumbs->activateRequests(\n\t) | rpl::on_next([this](View::GroupThumbs::Key key) {\n\t\tusing CollageKey = View::GroupThumbs::CollageKey;\n\t\tif (const auto photoId = std::get_if<PhotoId>(&key)) {\n\t\t\tconst auto photo = _session->data().photo(*photoId);\n\t\t\tmoveToEntity({ photo, nullptr });\n\t\t} else if (const auto itemId = std::get_if<FullMsgId>(&key)) {\n\t\t\tmoveToEntity(entityForItemId(*itemId));\n\t\t} else if (const auto collageKey = std::get_if<CollageKey>(&key)) {\n\t\t\tif (_collageData) {\n\t\t\t\tmoveToEntity(entityForCollage(collageKey->index));\n\t\t\t}\n\t\t}\n\t}, _groupThumbs->lifetime());\n\n\t_groupThumbsRect = QRect(\n\t\t_groupThumbsLeft,\n\t\t_groupThumbsTop,\n\t\twidth() - 2 * _groupThumbsLeft,\n\t\theight() - _groupThumbsTop);\n}\n\nvoid OverlayWidget::clearControlsState() {\n\t_saveMsgAnimation.stop();\n\t_saveMsgTimer.cancel();\n\t_loadRequest = 0;\n\t_over = _down = Over::None;\n\t_pressed = false;\n\t_dragging = 0;\n\tsetCursor(style::cur_default);\n\tif (!_animations.empty()) {\n\t\t_animations.clear();\n\t\t_stateAnimation.stop();\n\t}\n\tif (!_animationOpacities.empty()) {\n\t\t_animationOpacities.clear();\n\t}\n}\n\nnot_null<QWindow*> OverlayWidget::window() const {\n\treturn _window->windowHandle();\n}\n\nint OverlayWidget::width() const {\n\treturn _widget->width();\n}\n\nint OverlayWidget::height() const {\n\treturn _widget->height();\n}\n\nvoid OverlayWidget::update() {\n\t_widget->update();\n}\n\nvoid OverlayWidget::update(const QRegion &region) {\n\t_widget->update(region);\n}\n\nbool OverlayWidget::isActive() const {\n\treturn !isHidden() && !isMinimized() && _window->isActiveWindow();\n}\n\nbool OverlayWidget::isHidden() const {\n\treturn _window->isHidden();\n}\n\nbool OverlayWidget::isMinimized() const {\n\treturn _window->isMinimized();\n}\n\nbool OverlayWidget::isFullScreen() const {\n\treturn _fullscreen;\n}\n\nnot_null<QWidget*> OverlayWidget::widget() const {\n\treturn _widget;\n}\n\nvoid OverlayWidget::hide() {\n\tclearBeforeHide();\n\tapplyHideWindowWorkaround();\n\t_window->hide();\n}\n\nvoid OverlayWidget::setCursor(style::cursor cursor) {\n\t_widget->setCursor(cursor);\n}\n\nvoid OverlayWidget::setFocus() {\n\t_body->setFocus();\n}\n\nbool OverlayWidget::takeFocusFrom(not_null<QWidget*> window) const {\n\treturn _fullscreen\n\t\t&& !isHidden()\n\t\t&& !isMinimized()\n\t\t&& (_window->screen() == window->screen());\n}\n\nvoid OverlayWidget::activate() {\n\t_window->raise();\n\t_window->activateWindow();\n\tsetFocus();\n\tQApplication::setActiveWindow(_window);\n\tsetFocus();\n}\n\nvoid OverlayWidget::show(OpenRequest request) {\n\tconst auto story = request.story();\n\tconst auto document = story ? story->document() : request.document();\n\tconst auto photo = story ? story->photo() : request.photo();\n\tconst auto call = story ? story->call() : request.call();\n\tconst auto contextItem = request.item();\n\tconst auto contextPeer = request.peer();\n\tconst auto contextTopicRootId = request.topicRootId();\n\t_drawButtonEnabled = request.showDrawButton();\n\tif (!request.continueStreaming() && !request.startTime() && !_reShow) {\n\t\tif (_message && (_message == contextItem)) {\n\t\t\treturn close();\n\t\t} else if (_user && (_user == contextPeer)) {\n\t\t\tif ((_photo && (_photo == photo))\n\t\t\t\t|| (_document && (_document == document))) {\n\t\t\t\treturn close();\n\t\t\t}\n\t\t}\n\t}\n\tif (isHidden() || isMinimized()) {\n\t\t// Count top notch on macOS before counting geometry.\n\t\t_helper->beforeShow(_fullscreen);\n\t}\n\tif (_cachedShow) {\n\t\t_cachedShow->showOrHideBoxOrLayer(\n\t\t\tv::null,\n\t\t\tUi::LayerOption::CloseOther,\n\t\t\tanim::type::instant);\n\t}\n\tif (photo) {\n\t\tif (contextItem && contextPeer) {\n\t\t\treturn;\n\t\t}\n\t\tsetSession(&photo->session());\n\n\t\tif (story) {\n\t\t\tsetContext(StoriesContext{\n\t\t\t\tstory->peer(),\n\t\t\t\tstory->id(),\n\t\t\t\trequest.storiesContext(),\n\t\t\t});\n\t\t} else if (contextPeer) {\n\t\t\tsetContext(contextPeer);\n\t\t} else if (contextItem) {\n\t\t\tsetContext(ItemContext{ contextItem, contextTopicRootId });\n\t\t} else {\n\t\t\tsetContext(v::null);\n\t\t}\n\n\t\tclearControlsState();\n\t\t_firstOpenedPeerPhoto = (contextPeer != nullptr);\n\t\tassignMediaPointer(photo);\n\n\t\tdisplayPhoto(photo);\n\t\tpreloadData(0);\n\t\tactivateControls();\n\t} else if (story || document || call) {\n\t\tsetSession(document\n\t\t\t? &document->session()\n\t\t\t: story\n\t\t\t? &story->session()\n\t\t\t: &call->session());\n\n\t\tif (story) {\n\t\t\tsetContext(StoriesContext{\n\t\t\t\tstory->peer(),\n\t\t\t\tstory->id(),\n\t\t\t\trequest.storiesContext(),\n\t\t\t});\n\t\t} else if (contextItem) {\n\t\t\tsetContext(ItemContext{ contextItem, contextTopicRootId });\n\t\t} else {\n\t\t\tsetContext(v::null);\n\t\t}\n\n\t\tclearControlsState();\n\n\t\t_streamingStartPaused = false;\n\t\tif (call) {\n\t\t\t_callLinkSlug = request.callLinkSlug();\n\t\t\t_callJoinMessageId = request.callJoinMessageId();\n\t\t\tdisplayVideoStream(call, anim::activation::normal);\n\t\t} else {\n\t\t\tdisplayDocument(\n\t\t\t\tdocument,\n\t\t\t\tanim::activation::normal,\n\t\t\t\t(request.cloudTheme()\n\t\t\t\t\t? *request.cloudTheme()\n\t\t\t\t\t: Data::CloudTheme()),\n\t\t\t\t{ request.continueStreaming(), request.startTime() });\n\t\t}\n\t\tif (!isHidden()) {\n\t\t\tpreloadData(0);\n\t\t\tactivateControls();\n\t\t}\n\t}\n\tif (const auto controller = request.controller()) {\n\t\t_openedFrom = base::make_weak(&controller->window());\n\t}\n}\n\nvoid OverlayWidget::displayPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tanim::activation activation) {\n\tif (photo->isNull()) {\n\t\tdisplayDocument(nullptr, activation);\n\t\treturn;\n\t}\n\t_touchbarDisplay.fire(TouchBarItemType::Photo);\n\n\tclearStreaming();\n\tdestroyThemePreview();\n\n\t_fullScreenVideo = false;\n\tconst auto photoChanged = (_photo != photo);\n\tassignMediaPointer(photo);\n\t_rotation = _photo->owner().mediaRotation().get(_photo);\n\t_radial.stop();\n\tif (photoChanged) {\n\t\t_showRecognitionResults = false;\n\t\t_recognitionResult = {};\n\t\t_recognitionPendingSessionUniqueId = 0;\n\t\t_recognitionPendingPhotoId = 0;\n\t\t_recognitionRetryOnLarge = false;\n\t}\n\n\trefreshMediaViewer();\n\n\t_staticContent = QImage();\n\tif (!_stories && _photo->videoCanBePlayed()) {\n\t\tinitStreaming();\n\t}\n\n\ttryStartTextRecognition();\n\n\tinitSponsoredButton();\n\n\trefreshCaption();\n\n\t_blurred = true;\n\t_down = Over::None;\n\tif (!_staticContent.isNull()) {\n\t\t// Video thumbnail.\n\t\tconst auto size = style::ConvertScale(\n\t\t\tflipSizeByRotation(_staticContent.size()));\n\t\t_w = size.width();\n\t\t_h = size.height();\n\t} else {\n\t\tconst auto size = style::ConvertScale(flipSizeByRotation(QSize(\n\t\t\tphoto->width(),\n\t\t\tphoto->height())));\n\t\t_w = size.width();\n\t\t_h = size.height();\n\t}\n\tcontentSizeChanged();\n\trefreshFromLabel();\n\tdisplayFinished(activation);\n}\n\nvoid OverlayWidget::destroyThemePreview() {\n\t_themePreviewId = 0;\n\t_themePreviewShown = false;\n\t_themePreview.reset();\n\t_themeApply.destroy();\n\t_themeCancel.destroy();\n\t_themeShare.destroy();\n}\n\nvoid OverlayWidget::redisplayContent() {\n\tif (isHidden() || !_session) {\n\t\treturn;\n\t} else if (_photo) {\n\t\tdisplayPhoto(_photo, anim::activation::background);\n\t} else {\n\t\tdisplayDocument(_document, anim::activation::background);\n\t}\n}\n\n// Empty messages shown as docs: doc can be nullptr.\nvoid OverlayWidget::displayDocument(\n\t\tDocumentData *doc,\n\t\tanim::activation activation,\n\t\tconst Data::CloudTheme &cloud,\n\t\tconst StartStreaming &startStreaming) {\n\t_fullScreenVideo = false;\n\t_staticContent = QImage();\n\tconst auto documentChanged = (_document != doc);\n\tclearStreaming(_document != doc);\n\tdestroyThemePreview();\n\tassignMediaPointer(doc);\n\n\t_rotation = _document\n\t\t? _document->owner().mediaRotation().get(_document)\n\t\t: 0;\n\t_themeCloudData = cloud;\n\t_radial.stop();\n\tif (documentChanged) {\n\t\t_showRecognitionResults = false;\n\t\t_recognitionResult = {};\n\t\t_recognitionPendingSessionUniqueId = 0;\n\t\t_recognitionPendingPhotoId = 0;\n\t\t_recognitionRetryOnLarge = false;\n\t}\n\n\t_touchbarDisplay.fire(TouchBarItemType::None);\n\n\trefreshMediaViewer();\n\tif (_document) {\n\t\tif (_document->sticker()) {\n\t\t\tif (const auto image = _documentMedia->getStickerLarge()) {\n\t\t\t\tsetStaticContent(image->original());\n\t\t\t} else if (const auto thumbnail = _documentMedia->thumbnail()) {\n\t\t\t\tsetStaticContent(thumbnail->pix(\n\t\t\t\t\t_document->dimensions,\n\t\t\t\t\t{ .options = Images::Option::Blur }\n\t\t\t\t).toImage());\n\t\t\t}\n\t\t} else {\n\t\t\tinitSponsoredButton();\n\t\t\tif (_documentMedia->canBePlayed()\n\t\t\t\t&& initStreaming(startStreaming)) {\n\t\t\t} else if (_document->isVideoFile()) {\n\t\t\t\t_documentMedia->automaticLoad(fileOrigin(), _message);\n\t\t\t\tinitStreamingThumbnail();\n\t\t\t} else if (_document->isTheme()) {\n\t\t\t\t_documentMedia->automaticLoad(fileOrigin(), _message);\n\t\t\t\tinitThemePreview();\n\t\t\t} else {\n\t\t\t\t_documentMedia->automaticLoad(fileOrigin(), _message);\n\t\t\t\t_document->saveFromDataSilent();\n\t\t\t\tauto &location = _document->location(true);\n\t\t\t\tif (location.accessEnable()) {\n\t\t\t\t\tsetStaticContent(PrepareStaticImage({\n\t\t\t\t\t\t.path = location.name(),\n\t\t\t\t\t}));\n\t\t\t\t\tif (!_staticContent.isNull()) {\n\t\t\t\t\t\t_touchbarDisplay.fire(TouchBarItemType::Photo);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tsetStaticContent(PrepareStaticImage({\n\t\t\t\t\t\t.content = _documentMedia->bytes(),\n\t\t\t\t\t}));\n\t\t\t\t\tif (!_staticContent.isNull()) {\n\t\t\t\t\t\t_touchbarDisplay.fire(TouchBarItemType::Photo);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlocation.accessDisable();\n\t\t\t}\n\t\t}\n\t}\n\trefreshCaption();\n\n\tconst auto docGeneric = Layout::DocumentGenericPreview::Create(_document);\n\t_docExt = docGeneric.ext;\n\t_docIconColor = docGeneric.color;\n\t_docIcon = docGeneric.icon();\n\n\tint32 extmaxw = (st::mediaviewFileIconSize - st::mediaviewFileExtPadding * 2);\n\t_docExtWidth = st::mediaviewFileExtFont->width(_docExt);\n\tif (_docExtWidth > extmaxw) {\n\t\t_docExt = st::mediaviewFileExtFont->elided(_docExt, extmaxw, Qt::ElideMiddle);\n\t\t_docExtWidth = st::mediaviewFileExtFont->width(_docExt);\n\t}\n\tif (documentBubbleShown()) {\n\t\tif (_document && _document->hasThumbnail()) {\n\t\t\t_document->loadThumbnail(fileOrigin());\n\t\t\tconst auto tw = _documentMedia->thumbnailSize().width();\n\t\t\tconst auto th = _documentMedia->thumbnailSize().height();\n\t\t\tif (!tw || !th) {\n\t\t\t\t_docThumbx = _docThumby = _docThumbw = 0;\n\t\t\t} else if (tw > th) {\n\t\t\t\t_docThumbw = (tw * st::mediaviewFileIconSize) / th;\n\t\t\t\t_docThumbx = (_docThumbw - st::mediaviewFileIconSize) / 2;\n\t\t\t\t_docThumby = 0;\n\t\t\t} else {\n\t\t\t\t_docThumbw = st::mediaviewFileIconSize;\n\t\t\t\t_docThumbx = 0;\n\t\t\t\t_docThumby = ((th * _docThumbw) / tw - st::mediaviewFileIconSize) / 2;\n\t\t\t}\n\t\t}\n\n\t\tint32 maxw = st::mediaviewFileSize.width() - st::mediaviewFileIconSize - st::mediaviewFilePadding * 3;\n\n\t\tif (_document) {\n\t\t\t_docName = (_document->type == StickerDocument)\n\t\t\t\t? tr::lng_in_dlg_sticker(tr::now)\n\t\t\t\t: (_document->type == AnimatedDocument\n\t\t\t\t\t? u\"GIF\"_q\n\t\t\t\t\t: (_document->filename().isEmpty()\n\t\t\t\t\t\t? tr::lng_mediaview_doc_image(tr::now)\n\t\t\t\t\t\t: _document->filename()));\n\t\t} else {\n\t\t\t_docName = tr::lng_message_empty(tr::now);\n\t\t}\n\t\t_docNameWidth = st::mediaviewFileNameFont->width(_docName);\n\t\tif (_docNameWidth > maxw) {\n\t\t\t_docName = st::mediaviewFileNameFont->elided(_docName, maxw, Qt::ElideMiddle);\n\t\t\t_docNameWidth = st::mediaviewFileNameFont->width(_docName);\n\t\t}\n\t} else if (_themePreviewShown) {\n\t\tupdateThemePreviewGeometry();\n\t} else if (!_staticContent.isNull()) {\n\t\tconst auto size = style::ConvertScale(\n\t\t\tflipSizeByRotation(_staticContent.size()));\n\t\t_w = size.width();\n\t\t_h = size.height();\n\t} else if (videoShown()) {\n\t\tconst auto contentSize = style::ConvertScale(videoSize());\n\t\t_w = contentSize.width();\n\t\t_h = contentSize.height();\n\t}\n\tcontentSizeChanged();\n\tif (videoShown()) {\n\t\tapplyVideoSize();\n\t}\n\trefreshFromLabel();\n\t_blurred = false;\n\tif (_showAsPip && _streamed && _streamed->controls) {\n\t\tswitchToPip();\n\t} else {\n\t\tdisplayFinished(activation);\n\t}\n}\n\n// Empty messages shown as docs: doc can be nullptr.\nvoid OverlayWidget::displayVideoStream(\n\t\tconst std::shared_ptr<Data::GroupCall> &call,\n\t\tanim::activation activation) {\n\t_fullScreenVideo = false;\n\t_staticContent = QImage();\n\tclearStreaming(true);\n\tdestroyThemePreview();\n\tassignMediaPointer(call);\n\n\t_rotation = 0;\n\t_radial.stop();\n\n\t_touchbarDisplay.fire(TouchBarItemType::None);\n\n\tcontentSizeChanged();\n\t_blurred = false;\n\tdisplayFinished(activation);\n}\n\nvoid OverlayWidget::initSponsoredButton() {\n\tconst auto has = _message && _message->isSponsored() && _session;\n\tif (has && _sponsoredButton) {\n\t\treturn;\n\t} else if (!has && _sponsoredButton) {\n\t\t_sponsoredButton = nullptr;\n\t\treturn;\n\t} else if (!has && !_sponsoredButton) {\n\t\treturn;\n\t}\n\tconst auto sponsoredMessages = &_session->sponsoredMessages();\n\tconst auto fullId = _message->fullId();\n\tconst auto details = sponsoredMessages->lookupDetails(fullId);\n\t_sponsoredButton = base::make_unique_q<SponsoredButton>(_body);\n\t_sponsoredButton->setText(details.buttonText);\n\t_sponsoredButton->setOpacity(1.0);\n\n\t_sponsoredButton->setClickedCallback([=, link = details.link] {\n\t\tUrlClickHandler::Open(link);\n\t\tsponsoredMessages->clicked(fullId, false, true);\n\t\thide();\n\t});\n}\n\nvoid OverlayWidget::updateThemePreviewGeometry() {\n\tif (_themePreviewShown) {\n\t\tauto previewRect = QRect((width() - st::themePreviewSize.width()) / 2, (height() - st::themePreviewSize.height()) / 2, st::themePreviewSize.width(), st::themePreviewSize.height());\n\t\t_themePreviewRect = previewRect.marginsAdded(st::themePreviewMargin);\n\t\tif (_themeApply) {\n\t\t\tauto right = qMax(width() - _themePreviewRect.x() - _themePreviewRect.width(), 0) + st::themePreviewMargin.right();\n\t\t\tauto bottom = qMin(height(), _themePreviewRect.y() + _themePreviewRect.height());\n\t\t\t_themeApply->moveToRight(right, bottom - st::themePreviewMargin.bottom() + (st::themePreviewMargin.bottom() - _themeApply->height()) / 2);\n\t\t\tright += _themeApply->width() + st::themePreviewButtonsSkip;\n\t\t\t_themeCancel->moveToRight(right, _themeApply->y());\n\t\t\tif (_themeShare) {\n\t\t\t\t_themeShare->moveToLeft(previewRect.x(), _themeApply->y());\n\t\t\t}\n\t\t}\n\n\t\t// For context menu event.\n\t\t_x = _themePreviewRect.x();\n\t\t_y = _themePreviewRect.y();\n\t\t_w = _themePreviewRect.width();\n\t\t_h = _themePreviewRect.height();\n\t}\n}\n\nvoid OverlayWidget::displayFinished(anim::activation activation) {\n\tupdateControls();\n\tif (isHidden()) {\n\t\t_helper->beforeShow(_fullscreen);\n\t\tmoveToScreen();\n\t\tshowAndActivate();\n\t} else if (activation == anim::activation::background) {\n\t\treturn;\n\t} else if (isMinimized()) {\n\t\t_helper->beforeShow(_fullscreen);\n\t\tshowAndActivate();\n\t} else {\n\t\tactivate();\n\t}\n}\n\nvoid OverlayWidget::showAndActivate() {\n\t_body->show();\n\tinitNormalGeometry();\n\tif (_windowed || Platform::IsMac()) {\n\t\t_wasWindowedMode = false;\n\t}\n\tupdateGeometry();\n\tif (_windowed || Platform::IsMac()) {\n\t\t_window->showNormal();\n\t\t_wasWindowedMode = true;\n\t} else if (_fullscreen) {\n\t\t_window->showFullScreen();\n\t} else {\n\t\t_window->showMaximized();\n\t}\n\t_helper->afterShow(_fullscreen);\n\t_widget->update();\n\tactivate();\n}\n\nbool OverlayWidget::canInitStreaming() const {\n\treturn (_document && _documentMedia->canBePlayed())\n\t\t|| (_photo && _photo->videoCanBePlayed());\n}\n\nbool OverlayWidget::initStreaming(const StartStreaming &startStreaming) {\n\tExpects(canInitStreaming());\n\n\tif (_streamed) {\n\t\treturn true;\n\t}\n\tinitStreamingThumbnail();\n\tif (!createStreamingObjects()) {\n\t\tif (_document) {\n\t\t\t_document->setInappPlaybackFailed();\n\t\t} else {\n\t\t\t_photo->setVideoPlaybackFailed();\n\t\t}\n\t\treturn false;\n\t}\n\n\tCore::App().updateNonIdle();\n\n\t_streamed->instance.player().updates(\n\t) | rpl::on_next_error([=](Streaming::Update &&update) {\n\t\thandleStreamingUpdate(std::move(update));\n\t}, [=](Streaming::Error &&error) {\n\t\thandleStreamingError(std::move(error));\n\t}, _streamed->instance.lifetime());\n\n\t_streamed->instance.switchQualityRequests(\n\t) | rpl::filter([=](int quality) {\n\t\treturn !_quality.manual && _quality.height != quality;\n\t}) | rpl::on_next([=](int quality) {\n\t\tapplyVideoQuality({\n\t\t\t.manual = 0,\n\t\t\t.height = uint32(quality),\n\t\t});\n\t}, _streamed->instance.lifetime());\n\n\tconst auto continuing = startStreaming.continueStreaming\n\t\t&& _pip\n\t\t&& (_pip->wrapped.shared().get()\n\t\t\t== _streamed->instance.shared().get());\n\tif (startStreaming.continueStreaming) {\n\t\t_pip = nullptr;\n\t}\n\tif (!continuing\n\t\t|| (!_streamed->instance.player().active()\n\t\t\t&& !_streamed->instance.player().finished())) {\n\t\tstartStreamingPlayer(startStreaming);\n\t} else {\n\t\tif (_streamed->instance.player().ready()) {\n\t\t\tmarkStreamedReady();\n\t\t} else {\n\t\t\t_streamed->ready = false;\n\t\t}\n\t\tupdatePlaybackState();\n\t}\n\treturn true;\n}\n\nvoid OverlayWidget::startStreamingPlayer(\n\t\tconst StartStreaming &startStreaming) {\n\tExpects(_streamed != nullptr);\n\n\tconst auto &player = _streamed->instance.player();\n\tif (player.playing()) {\n\t\tif (!_streamed->withSound) {\n\t\t\tmarkStreamedReady();\n\t\t\treturn;\n\t\t}\n\t\t_pip = nullptr;\n\t} else if (!player.paused() && !player.finished() && !player.failed()) {\n\t\t_pip = nullptr;\n\t} else if (_pip && _streamed->withSound) {\n\t\treturn;\n\t}\n\n\t_streamedPosition = _document\n\t\t? startStreaming.startTime\n\t\t: _photo\n\t\t? _photo->videoStartPosition()\n\t\t: 0;\n\trestartAtSeekPosition(_streamedPosition);\n}\n\nvoid OverlayWidget::markStreamedReady() {\n\tExpects(_streamed != nullptr);\n\n\tif (_streamed->ready) {\n\t\treturn;\n\t}\n\t_streamed->ready = true;\n\tif (const auto sponsored = _streamed->sponsored.get()) {\n\t\tsponsored->start();\n\t}\n}\n\nvoid OverlayWidget::initStreamingThumbnail() {\n\tExpects(_photo || _document);\n\n\t_touchbarDisplay.fire(TouchBarItemType::Video);\n\n\tauto userpicImage = std::optional<Image>();\n\tconst auto computePhotoThumbnail = [&] {\n\t\tconst auto thumbnail = _photoMedia->image(Data::PhotoSize::Thumbnail);\n\t\tif (thumbnail) {\n\t\t\treturn thumbnail;\n\t\t} else if (_peer && _peer->userpicPhotoId() == _photo->id) {\n\t\t\tif (const auto view = _peer->activeUserpicView(); view.cloud) {\n\t\t\t\tif (!view.cloud->isNull()) {\n\t\t\t\t\tuserpicImage.emplace(base::duplicate(*view.cloud));\n\t\t\t\t\treturn &*userpicImage;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn thumbnail;\n\t};\n\tconst auto good = _videoCover\n\t\t? _videoCoverMedia->image(Data::PhotoSize::Large)\n\t\t: _document\n\t\t? _documentMedia->goodThumbnail()\n\t\t: _photoMedia->image(Data::PhotoSize::Large);\n\tconst auto thumbnail = _videoCover\n\t\t? _videoCoverMedia->image(Data::PhotoSize::Small)\n\t\t: _document\n\t\t? _documentMedia->thumbnail()\n\t\t: computePhotoThumbnail();\n\tconst auto blurred = _videoCover\n\t\t? _videoCoverMedia->thumbnailInline()\n\t\t: _document\n\t\t? _documentMedia->thumbnailInline()\n\t\t: _photoMedia->thumbnailInline();\n\tconst auto size = _photo\n\t\t? QSize(\n\t\t\t_photo->videoLocation(Data::PhotoSize::Large).width(),\n\t\t\t_photo->videoLocation(Data::PhotoSize::Large).height())\n\t\t: good\n\t\t? good->size()\n\t\t: _document->dimensions;\n\tif (size.isEmpty()) {\n\t\treturn;\n\t} else if (!_streamedQualityChangeFrame.isNull()) {\n\t\tsetStaticContent(_streamedQualityChangeFrame.scaled(\n\t\t\tsize,\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation));\n\t\treturn;\n\t} else if (!good && !thumbnail && !blurred) {\n\t\treturn;\n\t}\n\tconst auto options = VideoThumbOptions(_document);\n\tconst auto goodOptions = (options & ~Images::Option::Blur);\n\tsetStaticContent((good\n\t\t? good\n\t\t: thumbnail\n\t\t? thumbnail\n\t\t: blurred\n\t\t? blurred\n\t\t: Image::BlankMedia().get())->pixNoCache(\n\t\t\tsize,\n\t\t\t{\n\t\t\t\t.options = good ? goodOptions : options,\n\t\t\t\t.outer = size / style::DevicePixelRatio(),\n\t\t\t}\n\t\t).toImage());\n}\n\nvoid OverlayWidget::streamingReady(Streaming::Information &&info) {\n\tmarkStreamedReady();\n\tif (videoShown()) {\n\t\tapplyVideoSize();\n\t\t_streamedQualityChangeFrame = QImage();\n\t} else {\n\t\tupdateContentRect();\n\t}\n}\n\nvoid OverlayWidget::applyVideoSize() {\n\tconst auto contentSize = style::ConvertScale(videoSize());\n\tif (contentSize != QSize(_width, _height)) {\n\t\tupdateContentRect();\n\t\t_w = contentSize.width();\n\t\t_h = contentSize.height();\n\t\tcontentSizeChanged();\n\t}\n\tupdateContentRect();\n}\n\nbool OverlayWidget::createStreamingObjects() {\n\tExpects(_photo || _document);\n\tExpects(!_streamed);\n\n\tconst auto origin = fileOrigin();\n\tconst auto callback = [=] { waitingAnimationCallback(); };\n\tconst auto video = _chosenQuality ? _chosenQuality : _document;\n\tif (video) {\n\t\t_streamed = std::make_unique<Streamed>(\n\t\t\tvideo,\n\t\t\t_document,\n\t\t\t_message,\n\t\t\torigin,\n\t\t\tcallback);\n\t} else {\n\t\t_streamed = std::make_unique<Streamed>(_photo, origin, callback);\n\t}\n\tif (!_streamed->instance.valid()) {\n\t\t_streamed = nullptr;\n\t\treturn false;\n\t}\n\t++_streamedCreated;\n\t_streamed->instance.setPriority(kOverlayLoaderPriority);\n\t_streamed->instance.lockPlayer();\n\t_streamed->withSound = video\n\t\t&& !video->isSilentVideo()\n\t\t&& (_document->isAudioFile()\n\t\t\t|| _document->isVideoFile()\n\t\t\t|| _document->isVoiceMessage()\n\t\t\t|| _document->isVideoMessage());\n\tif (streamingRequiresControls()) {\n\t\t_streamed->controls = std::make_unique<PlaybackControls>(\n\t\t\t_body,\n\t\t\tstatic_cast<PlaybackControls::Delegate*>(this));\n\t\t_streamed->controls->show();\n\t\t_streamed->sponsored = PlaybackSponsored::Has(_message)\n\t\t\t? std::make_unique<PlaybackSponsored>(\n\t\t\t\t_streamed->controls.get(),\n\t\t\t\tuiShow(),\n\t\t\t\t_message)\n\t\t\t: nullptr;\n\t\tif (const auto sponsored = _streamed->sponsored.get()) {\n\t\t\t_layerBg->layerShownValue(\n\t\t\t) | rpl::on_next([=](bool shown) {\n\t\t\t\tsponsored->setPaused(shown);\n\t\t\t}, sponsored->lifetime());\n\t\t}\n\t\trefreshClipControllerGeometry();\n\t}\n\treturn true;\n}\n\nvoid OverlayWidget::updatePowerSaveBlocker(\n\t\tconst Player::TrackState &state) {\n\tExpects(_streamed != nullptr);\n\n\tconst auto block = (_document != nullptr)\n\t\t&& _document->isVideoFile()\n\t\t&& !IsPausedOrPausing(state.state)\n\t\t&& !IsStoppedOrStopping(state.state);\n\n\tbase::UpdatePowerSaveBlocker(\n\t\t_streamed->powerSaveBlocker,\n\t\tblock,\n\t\tbase::PowerSaveBlockType::PreventDisplaySleep,\n\t\t[] { return u\"Video playback is active\"_q; },\n\t\t[=] { return _window->windowHandle(); });\n}\n\nQImage OverlayWidget::transformedShownContent() const {\n\treturn transformShownContent(\n\t\tvideoShown() ? currentVideoFrameImage() : _staticContent,\n\t\tfinalContentRotation());\n}\n\nQImage OverlayWidget::transformShownContent(\n\t\tQImage content,\n\t\tint rotation) const {\n\tif (rotation) {\n\t\tcontent = RotateFrameImage(std::move(content), rotation);\n\t}\n\tif (videoShown()) {\n\t\tconst auto requiredSize = videoSize();\n\t\tif (content.size() != requiredSize) {\n\t\t\tcontent = content.scaled(\n\t\t\t\trequiredSize,\n\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\tQt::SmoothTransformation);\n\t\t}\n\t}\n\treturn content;\n}\n\nvoid OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) {\n\tusing namespace Streaming;\n\n\tv::match(update.data, [&](Information &update) {\n\t\tstreamingReady(std::move(update));\n\t}, [&](PreloadedVideo) {\n\t\tupdatePlaybackState();\n\t}, [&](UpdateVideo update) {\n\t\tupdateContentRect();\n\t\tCore::App().updateNonIdle();\n\t\tupdatePlaybackState();\n\t\t_streamedPosition = update.position;\n\t}, [&](PreloadedAudio) {\n\t\tupdatePlaybackState();\n\t}, [&](UpdateAudio) {\n\t\tupdatePlaybackState();\n\t}, [](WaitingForData) {\n\t}, [](SpeedEstimate) {\n\t}, [](MutedByOther) {\n\t}, [&](Finished) {\n\t\tupdatePlaybackState();\n\t});\n}\n\nvoid OverlayWidget::handleStreamingError(Streaming::Error &&error) {\n\tExpects(_document || _photo);\n\n\tif (error == Streaming::Error::NotStreamable) {\n\t\tif (_document) {\n\t\t\t_document->setNotSupportsStreaming();\n\t\t} else {\n\t\t\t_photo->setVideoPlaybackFailed();\n\t\t}\n\t} else if (error == Streaming::Error::OpenFailed) {\n\t\tif (_document) {\n\t\t\t_document->setInappPlaybackFailed();\n\t\t} else {\n\t\t\t_photo->setVideoPlaybackFailed();\n\t\t}\n\t}\n\tif (canInitStreaming()) {\n\t\tupdatePlaybackState();\n\t} else {\n\t\tredisplayContent();\n\t}\n}\n\nvoid OverlayWidget::initThemePreview() {\n\tusing namespace Window::Theme;\n\n\tAssert(_document && _document->isTheme());\n\n\tconst auto bytes = _documentMedia->bytes();\n\tauto &location = _document->location();\n\tif (bytes.isEmpty()\n\t\t&& (location.isEmpty() || !location.accessEnable())) {\n\t\treturn;\n\t}\n\t_themePreviewShown = true;\n\n\tauto current = CurrentData();\n\tcurrent.backgroundId = Background()->id();\n\tcurrent.backgroundImage = Background()->createCurrentImage();\n\tcurrent.backgroundTiled = Background()->tile();\n\n\tconst auto &cloudList = _document->session().data().cloudThemes().list();\n\tconst auto i = ranges::find(\n\t\tcloudList,\n\t\t_document->id,\n\t\t&Data::CloudTheme::documentId);\n\tconst auto cloud = (i != end(cloudList)) ? *i : Data::CloudTheme();\n\tconst auto isTrusted = (cloud.documentId != 0);\n\tconst auto fields = [&] {\n\t\tauto result = _themeCloudData.id ? _themeCloudData : cloud;\n\t\tif (!result.documentId) {\n\t\t\tresult.documentId = _document->id;\n\t\t}\n\t\treturn result;\n\t}();\n\n\tconst auto weakSession = base::make_weak(&_document->session());\n\tconst auto path = _document->location().name();\n\tconst auto id = _themePreviewId = base::RandomValue<uint64>();\n\tconst auto weak = base::make_weak(_widget);\n\tcrl::async([=, data = std::move(current)]() mutable {\n\t\tauto preview = GeneratePreview(\n\t\t\tbytes,\n\t\t\tpath,\n\t\t\tfields,\n\t\t\tstd::move(data),\n\t\t\tWindow::Theme::PreviewType::Extended);\n\t\tcrl::on_main(weak, [=, result = std::move(preview)]() mutable {\n\t\t\tconst auto session = weakSession.get();\n\t\t\tif (id != _themePreviewId || !session) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_themePreviewId = 0;\n\t\t\t_themePreview = std::move(result);\n\t\t\tif (_themePreview) {\n\t\t\t\t_themeApply.create(\n\t\t\t\t\t_body,\n\t\t\t\t\ttr::lng_theme_preview_apply(),\n\t\t\t\t\tst::themePreviewApplyButton);\n\t\t\t\t_themeApply->show();\n\t\t\t\t_themeApply->setClickedCallback([=] {\n\t\t\t\t\tconst auto &object = Background()->themeObject();\n\t\t\t\t\tconst auto currentlyIsCustom = !object.cloud.id\n\t\t\t\t\t\t&& !IsEmbeddedTheme(object.pathAbsolute);\n\t\t\t\t\tauto preview = std::move(_themePreview);\n\t\t\t\t\tclose();\n\t\t\t\t\tApply(std::move(preview));\n\t\t\t\t\tif (isTrusted && !currentlyIsCustom) {\n\t\t\t\t\t\tKeepApplied();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\t_themeCancel.create(\n\t\t\t\t\t_body,\n\t\t\t\t\ttr::lng_cancel(),\n\t\t\t\t\tst::themePreviewCancelButton);\n\t\t\t\t_themeCancel->show();\n\t\t\t\t_themeCancel->setClickedCallback([this] { close(); });\n\t\t\t\tif (const auto slug = _themeCloudData.slug; !slug.isEmpty()) {\n\t\t\t\t\t_themeShare.create(\n\t\t\t\t\t\t_body,\n\t\t\t\t\t\ttr::lng_theme_share(),\n\t\t\t\t\t\tst::themePreviewCancelButton);\n\t\t\t\t\t_themeShare->show();\n\t\t\t\t\t_themeShare->setClickedCallback([=] {\n\t\t\t\t\t\tQGuiApplication::clipboard()->setText(\n\t\t\t\t\t\t\tsession->createInternalLinkFull(\"addtheme/\" + slug));\n\t\t\t\t\t\tuiShow()->showToast(\n\t\t\t\t\t\t\ttr::lng_background_link_copied(tr::now));\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\t_themeShare.destroy();\n\t\t\t\t}\n\t\t\t\tupdateControls();\n\t\t\t}\n\t\t\tupdate();\n\t\t});\n\t});\n\tlocation.accessDisable();\n}\n\nvoid OverlayWidget::refreshClipControllerGeometry() {\n\tif (!_streamed || !_streamed->controls) {\n\t\treturn;\n\t}\n\n\tif (_groupThumbs && _groupThumbs->hiding()) {\n\t\t_groupThumbs = nullptr;\n\t\t_groupThumbsRect = QRect();\n\t}\n\tconst auto controllerBottom = (_groupThumbs && !_fullScreenVideo)\n\t\t? _groupThumbsTop\n\t\t: height();\n\tconst auto skip = st::mediaviewCaptionPadding.bottom();\n\tconst auto controllerWidth = std::min(\n\t\tst::mediaviewControllerSize.width(),\n\t\twidth() - 2 * skip);\n\tconst auto controllerHeight = st::mediaviewControllerSize.height()\n\t\t+ (_streamed->controls->hasTimestamps()\n\t\t\t? st::mediaviewTimestampLabelHeight\n\t\t\t: 0);\n\t_streamed->controls->resize(controllerWidth, controllerHeight);\n\t_streamed->controls->move(\n\t\t(width() - controllerWidth) / 2,\n\t\t(controllerBottom // Duplicated in recountSkipTop().\n\t\t\t- _streamed->controls->height()\n\t\t\t- st::mediaviewCaptionPadding.bottom()));\n\tUi::SendPendingMoveResizeEvents(_streamed->controls.get());\n}\n\nvoid OverlayWidget::playbackControlsPlay() {\n\tplaybackPauseResume();\n\tactivateControls();\n}\n\nvoid OverlayWidget::playbackControlsPause() {\n\tplaybackPauseResume();\n\tactivateControls();\n}\n\nvoid OverlayWidget::playbackControlsToFullScreen() {\n\tplaybackToggleFullScreen();\n\tactivateControls();\n}\n\nvoid OverlayWidget::playbackControlsFromFullScreen() {\n\tplaybackToggleFullScreen();\n\tactivateControls();\n}\n\nvoid OverlayWidget::playbackControlsToPictureInPicture() {\n\tif (_streamed && _streamed->controls) {\n\t\tswitchToPip();\n\t}\n}\n\nvoid OverlayWidget::playbackControlsRotate() {\n\t_oldGeometry = contentGeometry();\n\t_geometryAnimation.stop();\n\tif (_photo) {\n\t\tauto &storage = _photo->owner().mediaRotation();\n\t\tstorage.set(_photo, storage.get(_photo) - 90);\n\t\t_rotation = storage.get(_photo);\n\t\tredisplayContent();\n\t} else if (_document) {\n\t\tauto &storage = _document->owner().mediaRotation();\n\t\tstorage.set(_document, storage.get(_document) - 90);\n\t\t_rotation = storage.get(_document);\n\t\tif (videoShown()) {\n\t\t\tapplyVideoSize();\n\t\t} else {\n\t\t\tredisplayContent();\n\t\t}\n\t}\n\tif (_opengl) {\n\t\t_geometryAnimation.start(\n\t\t\t[=] { update(); },\n\t\t\t0.,\n\t\t\t1.,\n\t\t\tst::widgetFadeDuration/*,\n\t\t\tst::easeOutCirc*/);\n\t}\n}\n\nvoid OverlayWidget::playbackPauseResume() {\n\tExpects(_streamed != nullptr);\n\n\t_streamed->resumeOnCallEnd = false;\n\tif (_streamed->instance.player().failed()) {\n\t\tclearStreaming();\n\t\tif (!canInitStreaming() || !initStreaming()) {\n\t\t\tredisplayContent();\n\t\t}\n\t} else if (_streamed->instance.player().finished()\n\t\t|| !_streamed->instance.player().active()\n\t\t|| _streamedQualityChangeFinished) {\n\t\t_streamedQualityChangeFinished = false;\n\t\t_streamingStartPaused = false;\n\t\trestartAtSeekPosition(0);\n\t} else if (_streamed->instance.player().paused()) {\n\t\t_streamed->instance.resume();\n\t\tupdatePlaybackState();\n\t\tplaybackPauseMusic();\n\t} else {\n\t\t_streamed->instance.pause();\n\t\tupdatePlaybackState();\n\t}\n}\n\nvoid OverlayWidget::flushPendingFrameStep() {\n\tif (!_streamed || !_frameStepPending) {\n\t\t_frameStepThrottle.cancel();\n\t\treturn;\n\t}\n\tconst auto fps = _streamed->instance.info().video.fps;\n\tconst auto stepMs = 1000.\n\t\t/ ((fps > 0.) ? fps : kFrameStepFallbackFps);\n\tconst auto shift = crl::time(std::round(_frameStepPending * stepMs));\n\t_frameStepPending = 0;\n\t_streamingStartPaused = true;\n\tseekRelativeTime(shift);\n\t_frameStepThrottle.callOnce(kFrameStepThrottleMs);\n}\n\nvoid OverlayWidget::seekRelativeTime(crl::time time) {\n\tExpects(_streamed != nullptr);\n\n\tconst auto newTime = std::clamp(\n\t\t_streamed->instance.info().video.state.position + time,\n\t\tcrl::time(0),\n\t\t_streamed->instance.info().video.state.duration);\n\trestartAtSeekPosition(newTime);\n}\n\nvoid OverlayWidget::restartAtProgress(float64 progress) {\n\tExpects(_streamed != nullptr);\n\n\trestartAtSeekPosition(_streamed->instance.info().video.state.duration\n\t\t* std::clamp(progress, 0., 1.));\n}\n\nvoid OverlayWidget::restartAtSeekPosition(crl::time position) {\n\tExpects(_streamed != nullptr);\n\n\tif (videoShown()) {\n\t\t_streamed->instance.saveFrameToCover();\n\t\tconst auto saved = base::take(_rotation);\n\t\tsetStaticContent(transformedShownContent());\n\t\t_rotation = saved;\n\t\tupdateContentRect();\n\t}\n\tconst auto overrideDuration = _stories\n\t\t|| (_chosenQuality && _chosenQuality != _document);\n\tauto options = Streaming::PlaybackOptions{\n\t\t.position = position,\n\t\t.durationOverride = ((overrideDuration\n\t\t\t&& _document\n\t\t\t&& _document->hasDuration())\n\t\t\t? _document->duration()\n\t\t\t: crl::time(0)),\n\t\t.hwAllowed = Core::App().settings().hardwareAcceleratedVideo(),\n\t\t.seekable = !_stories,\n\t};\n\tif (!_streamed->withSound) {\n\t\toptions.mode = Streaming::Mode::Video;\n\t\toptions.loop = !_stories;\n\t} else {\n\t\tAssert(_document != nullptr);\n\t\tconst auto messageId = _message ? _message->fullId() : FullMsgId();\n\t\toptions.audioId = AudioMsgId(_document, messageId);\n\t\toptions.speed = _stories\n\t\t\t? 1.\n\t\t\t: Core::App().settings().videoPlaybackSpeed();\n\t\tif (_pip) {\n\t\t\t_pip = nullptr;\n\t\t}\n\t}\n\t_streamed->instance.play(options);\n\tif (_streamingStartPaused) {\n\t\t_streamed->instance.pause();\n\t} else {\n\t\tplaybackPauseMusic();\n\t\t_streamedQualityChangeFinished = false;\n\t}\n\t_streamed->pausedBySeek = false;\n\n\tupdatePlaybackState();\n}\n\nvoid OverlayWidget::playbackControlsSeekProgress(crl::time position) {\n\tExpects(_streamed != nullptr);\n\n\tif (!_streamed->instance.player().paused()\n\t\t&& !_streamed->instance.player().finished()) {\n\t\t_streamed->pausedBySeek = true;\n\t\tplaybackPauseResume();\n\t}\n}\n\nvoid OverlayWidget::playbackControlsSeekFinished(crl::time position) {\n\tExpects(_streamed != nullptr);\n\n\t_streamingStartPaused = !_streamed->pausedBySeek\n\t\t&& !_streamed->instance.player().finished();\n\trestartAtSeekPosition(position);\n\tactivateControls();\n}\n\nvoid OverlayWidget::playbackControlsVolumeChanged(float64 volume) {\n\tif (_streamed) {\n\t\tPlayer::mixer()->setVideoVolume(volume);\n\t} else if (_videoStream) {\n\t\t_videoStream->setVolume(volume);\n\t}\n\tCore::App().settings().setVideoVolume(volume);\n\tCore::App().saveSettingsDelayed();\n}\n\nfloat64 OverlayWidget::playbackControlsCurrentVolume() {\n\treturn Core::App().settings().videoVolume();\n}\n\nvoid OverlayWidget::playbackControlsVolumeToggled() {\n\tconst auto volume = Core::App().settings().videoVolume();\n\tplaybackControlsVolumeChanged(volume ? 0. : _lastPositiveVolume);\n\tactivateControls();\n}\n\nvoid OverlayWidget::playbackControlsVolumeChangeFinished() {\n\tconst auto volume = Core::App().settings().videoVolume();\n\tif (volume > 0.) {\n\t\t_lastPositiveVolume = volume;\n\t}\n\tactivateControls();\n}\n\nvoid OverlayWidget::playbackControlsSpeedChanged(float64 speed) {\n\tDEBUG_LOG((\"Media playback speed: change to %1.\").arg(speed));\n\tif (_document) {\n\t\tDEBUG_LOG((\"Media playback speed: %1 to settings.\").arg(speed));\n\t\tCore::App().settings().setVideoPlaybackSpeed(speed);\n\t\tCore::App().saveSettingsDelayed();\n\t}\n\tif (_streamed && _streamed->controls && !_stories) {\n\t\tDEBUG_LOG((\"Media playback speed: %1 to _streamed.\").arg(speed));\n\t\t_streamed->instance.setSpeed(speed);\n\t}\n}\n\nfloat64 OverlayWidget::playbackControlsCurrentSpeed(bool lastNonDefault) {\n\treturn Core::App().settings().videoPlaybackSpeed(lastNonDefault);\n}\n\nstd::vector<int> OverlayWidget::playbackControlsQualities() {\n\tif (!_document) {\n\t\treturn {};\n\t}\n\tconst auto &list = _document->resolveQualities(_message);\n\tif (list.empty()) {\n\t\treturn {};\n\t}\n\tauto result = std::vector<int>();\n\tresult.reserve(list.size());\n\tfor (const auto &quality : list) {\n\t\tresult.push_back(quality->resolveVideoQuality());\n\t}\n\treturn result;\n}\n\nVideoQuality OverlayWidget::playbackControlsCurrentQuality() {\n\treturn _chosenQuality\n\t\t? VideoQuality{\n\t\t\t.manual = _quality.manual,\n\t\t\t.height = uint32(_chosenQuality->resolveVideoQuality()),\n\t\t}\n\t\t: _quality;\n}\n\nvoid OverlayWidget::playbackControlsQualityChanged(int quality) {\n\tapplyVideoQuality({\n\t\t.manual = (quality > 0),\n\t\t.height = quality ? uint32(quality) : _quality.height,\n\t});\n}\n\nvoid OverlayWidget::applyVideoQuality(VideoQuality value) {\n\tif (_quality == value) {\n\t\treturn;\n\t}\n\t_quality = value;\n\tCore::App().settings().setVideoQuality(value);\n\tCore::App().saveSettingsDelayed();\n\n\tif (!_document) {\n\t\treturn;\n\t}\n\tconst auto resolved = _document->chooseQuality(_message, _quality);\n\tif (_chosenQuality == resolved) {\n\t\treturn;\n\t}\n\t_chosenQuality = resolved;\n\tif (_streamed && _streamed->instance.ready()) {\n\t\t_streamedQualityChangeFrame = currentVideoFrameImage();\n\t}\n\tif (_streamed\n\t\t&& (!_streamed->instance.player().active()\n\t\t\t|| _streamed->instance.player().finished())) {\n\t\t_streamedQualityChangeFinished = true;\n\t}\n\t_streamingStartPaused = _streamedQualityChangeFinished\n\t\t|| (_streamed && _streamed->instance.player().paused());\n\tconst auto wasFullScreen = _fullScreenVideo;\n\tclearStreaming();\n\tconst auto time = _streamedPosition;\n\tconst auto startStreaming = StartStreaming(false, time);\n\tif (!canInitStreaming() || !initStreaming(startStreaming)) {\n\t\tredisplayContent();\n\t} else {\n\t\tif (_fullScreenVideo != wasFullScreen) {\n\t\t\t_fullScreenVideo = wasFullScreen;\n\t\t\tif (_streamed->controls) {\n\t\t\t\t_streamed->controls->setInFullScreen(_fullScreenVideo);\n\t\t\t}\n\t\t}\n\t\trefreshCaption();\n\t}\n}\n\nvoid OverlayWidget::switchToPip() {\n\tExpects(_streamed != nullptr);\n\tExpects(_document != nullptr);\n\n\tconst auto document = _document;\n\tconst auto messageId = _message ? _message->fullId() : FullMsgId();\n\tconst auto topicRootId = _topicRootId;\n\tconst auto monoforumPeerId = _monoforumPeerId;\n\tconst auto closeAndContinue = [=] {\n\t\t_showAsPip = false;\n\t\tshow(OpenRequest(\n\t\t\tfindWindow(false),\n\t\t\tdocument,\n\t\t\tdocument->owner().message(messageId),\n\t\t\ttopicRootId,\n\t\t\tmonoforumPeerId,\n\t\t\ttrue));\n\t};\n\t_showAsPip = true;\n\t_pip = std::make_unique<PipWrap>(\n\t\t_window,\n\t\tdocument,\n\t\tfileOrigin(),\n\t\t_chosenQuality ? _chosenQuality : document,\n\t\t_message,\n\t\t_quality,\n\t\t_streamed->instance.shared(),\n\t\tcloseAndContinue,\n\t\t[=] { _pip = nullptr; });\n\n\tif (const auto raw = _message) {\n\t\traw->history()->owner().itemRemoved(\n\t\t) | rpl::filter([=](not_null<const HistoryItem*> item) {\n\t\t\treturn (raw == item);\n\t\t}) | rpl::on_next([=] {\n\t\t\t_pip = nullptr;\n\t\t}, _pip->lifetime);\n\n\t\tCore::App().passcodeLockChanges(\n\t\t) | rpl::filter(\n\t\t\trpl::mappers::_1\n\t\t) | rpl::on_next([=] {\n\t\t\t_pip = nullptr;\n\t\t}, _pip->lifetime);\n\t}\n\n\tif (isHidden()) {\n\t\tclearBeforeHide();\n\t\tclearAfterHide();\n\t} else {\n\t\tclose();\n\t\tif (const auto window = Core::App().activeWindow()) {\n\t\t\twindow->activate();\n\t\t}\n\t}\n}\n\nnot_null<Ui::RpWidget*> OverlayWidget::storiesWrap() {\n\treturn _body;\n}\n\nstd::shared_ptr<ChatHelpers::Show> OverlayWidget::storiesShow() {\n\treturn uiShow();\n}\n\nstd::shared_ptr<ChatHelpers::Show> OverlayWidget::uiShow() {\n\tif (!_cachedShow) {\n\t\t_cachedShow = std::make_shared<Show>(this);\n\t}\n\treturn _cachedShow;\n}\n\nauto OverlayWidget::storiesStickerOrEmojiChosen()\n-> rpl::producer<ChatHelpers::FileChosen> {\n\treturn _storiesStickerOrEmojiChosen.events();\n}\n\nvoid OverlayWidget::storiesJumpTo(\n\t\tnot_null<Main::Session*> session,\n\t\tFullStoryId id,\n\t\tData::StoriesContext context) {\n\tExpects(_stories != nullptr);\n\tExpects(id.valid());\n\n\tconst auto maybeStory = session->data().stories().lookup(id);\n\tif (!maybeStory) {\n\t\tclose();\n\t\treturn;\n\t}\n\tconst auto story = *maybeStory;\n\tsetContext(StoriesContext{\n\t\tstory->peer(),\n\t\tstory->id(),\n\t\tcontext,\n\t});\n\tclearStreaming();\n\t_streamingStartPaused = false;\n\tv::match(story->media().data, [&](not_null<PhotoData*> photo) {\n\t\tdisplayPhoto(photo, anim::activation::background);\n\t}, [&](not_null<DocumentData*> document) {\n\t\tdisplayDocument(document, anim::activation::background);\n\t}, [&](const std::shared_ptr<Data::GroupCall> &call) {\n\t\tdisplayVideoStream(call, anim::activation::background);\n\t}, [&](v::null_t) {\n\t\tdisplayDocument(nullptr, anim::activation::background);\n\t});\n}\n\nvoid OverlayWidget::storiesRedisplay(not_null<Data::Story*> story) {\n\tExpects(_stories != nullptr);\n\n\tclearStreaming();\n\t_streamingStartPaused = false;\n\tv::match(story->media().data, [&](not_null<PhotoData*> photo) {\n\t\tdisplayPhoto(photo, anim::activation::background);\n\t}, [&](not_null<DocumentData*> document) {\n\t\tdisplayDocument(document, anim::activation::background);\n\t}, [&](const std::shared_ptr<Data::GroupCall> &call) {\n\t\tdisplayVideoStream(call, anim::activation::background);\n\t}, [&](v::null_t) {\n\t\tdisplayDocument(nullptr, anim::activation::background);\n\t});\n}\n\nvoid OverlayWidget::storiesClose() {\n\tclose();\n}\n\nbool OverlayWidget::storiesPaused() {\n\treturn _streamed\n\t\t&& !_streamed->instance.player().failed()\n\t\t&& !_streamed->instance.player().finished()\n\t\t&& _streamed->instance.player().active()\n\t\t&& _streamed->instance.player().paused();\n}\n\nrpl::producer<bool> OverlayWidget::storiesLayerShown() {\n\treturn _layerBg->layerShownValue();\n}\n\nvoid OverlayWidget::storiesTogglePaused(bool paused) {\n\tif (!_streamed\n\t\t|| _streamed->instance.player().failed()\n\t\t|| _streamed->instance.player().finished()\n\t\t|| !_streamed->instance.player().active()) {\n\t\treturn;\n\t} else if (_streamed->instance.player().paused()) {\n\t\tif (!paused) {\n\t\t\t_streamed->instance.resume();\n\t\t\tupdatePlaybackState();\n\t\t\tplaybackPauseMusic();\n\t\t}\n\t} else if (paused) {\n\t\t_streamed->instance.pause();\n\t\tupdatePlaybackState();\n\t}\n}\n\nfloat64 OverlayWidget::storiesSiblingOver(Stories::SiblingType type) {\n\treturn (type == Stories::SiblingType::Left)\n\t\t? overLevel(Over::LeftStories)\n\t\t: overLevel(Over::RightStories);\n}\n\nvoid OverlayWidget::storiesRepaint() {\n\tupdate();\n}\n\nvoid OverlayWidget::storiesVolumeToggle() {\n\tplaybackControlsVolumeToggled();\n}\n\nvoid OverlayWidget::storiesVolumeChanged(float64 volume) {\n\tplaybackControlsVolumeChanged(volume);\n}\n\nvoid OverlayWidget::storiesVolumeChangeFinished() {\n\tplaybackControlsVolumeChangeFinished();\n}\n\nint OverlayWidget::topNotchSkip() const {\n\treturn _fullscreen ? _topNotchSize : 0;\n}\n\nint OverlayWidget::storiesTopNotchSkip() {\n\treturn topNotchSkip();\n}\n\nvoid OverlayWidget::playbackToggleFullScreen() {\n\tExpects(_streamed != nullptr);\n\n\tif (_stories\n\t\t|| !videoShown()\n\t\t|| (!_streamed->controls && !_fullScreenVideo)) {\n\t\treturn;\n\t}\n\t_fullScreenVideo = !_fullScreenVideo;\n\tif (_fullScreenVideo) {\n\t\t_fullScreenZoomCache = _zoom;\n\t}\n\tresizeCenteredControls();\n\trecountSkipTop();\n\tsetZoomLevel(\n\t\t_fullScreenVideo ? kZoomToScreenLevel : _fullScreenZoomCache,\n\t\ttrue);\n\tif (_streamed->controls) {\n\t\tif (!_fullScreenVideo) {\n\t\t\t_streamed->controls->showAnimated();\n\t\t}\n\t\t_streamed->controls->setInFullScreen(_fullScreenVideo);\n\t}\n\t_touchbarFullscreenToggled.fire_copy(_fullScreenVideo);\n\tupdateControls();\n\tupdate();\n}\n\nvoid OverlayWidget::playbackPauseOnCall() {\n\tExpects(_streamed != nullptr);\n\n\tif (_streamed->instance.player().finished()\n\t\t|| _streamed->instance.player().paused()) {\n\t\treturn;\n\t}\n\t_streamed->resumeOnCallEnd = true;\n\t_streamed->instance.pause();\n\tupdatePlaybackState();\n}\n\nvoid OverlayWidget::playbackResumeOnCall() {\n\tExpects(_streamed != nullptr);\n\n\tif (_streamed->resumeOnCallEnd) {\n\t\t_streamed->resumeOnCallEnd = false;\n\t\t_streamed->instance.resume();\n\t\tupdatePlaybackState();\n\t\tplaybackPauseMusic();\n\t}\n}\n\nvoid OverlayWidget::playbackPauseMusic() {\n\tExpects(_streamed != nullptr);\n\n\tif (!_streamed->withSound) {\n\t\treturn;\n\t}\n\tPlayer::instance()->pause(AudioMsgId::Type::Voice);\n\tPlayer::instance()->pause(AudioMsgId::Type::Song);\n}\n\nvoid OverlayWidget::updatePlaybackState() {\n\tExpects(_streamed != nullptr);\n\n\tif (!_streamed->controls && !_stories) {\n\t\treturn;\n\t}\n\tconst auto state = _streamed->instance.player().prepareLegacyState();\n\tif (state.position != kTimeUnknown && state.length != kTimeUnknown) {\n\t\t_streamedPosition = state.position;\n\t\tif (_streamed->controls) {\n\t\t\t_streamed->controls->updatePlayback(state);\n\t\t\t_touchbarTrackState.fire_copy(state);\n\t\t\tupdatePowerSaveBlocker(state);\n\t\t}\n\t\tif (_stories) {\n\t\t\t_stories->updatePlayback(state);\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::validatePhotoImage(Image *image, bool blurred) {\n\tif (!image) {\n\t\treturn;\n\t} else if (!_staticContent.isNull() && (blurred || !_blurred)) {\n\t\treturn;\n\t}\n\tconst auto use = flipSizeByRotation({ _width, _height })\n\t\t* style::DevicePixelRatio();\n\tsetStaticContent(image->pixNoCache(\n\t\tuse,\n\t\t{ .options = (blurred ? Images::Option::Blur : Images::Option()) }\n\t).toImage());\n\t_blurred = blurred;\n}\n\nvoid OverlayWidget::validatePhotoCurrentImage() {\n\tif (!_photo) {\n\t\treturn;\n\t}\n\tvalidatePhotoImage(_photoMedia->image(Data::PhotoSize::Large), false);\n\tvalidatePhotoImage(_photoMedia->image(Data::PhotoSize::Thumbnail), true);\n\tvalidatePhotoImage(_photoMedia->image(Data::PhotoSize::Small), true);\n\tvalidatePhotoImage(_photoMedia->thumbnailInline(), true);\n\tif (_staticContent.isNull()\n\t\t&& !_message\n\t\t&& _peer\n\t\t&& _peer->hasUserpic()) {\n\t\tif (const auto view = _peer->activeUserpicView(); view.cloud) {\n\t\t\tif (!view.cloud->isNull()) {\n\t\t\t\tauto image = Image(base::duplicate(*view.cloud));\n\t\t\t\tvalidatePhotoImage(&image, true);\n\t\t\t}\n\t\t}\n\t}\n\tif (_staticContent.isNull()) {\n\t\t_photoMedia->wanted(Data::PhotoSize::Small, fileOrigin());\n\t}\n\tif (_recognitionRetryOnLarge) {\n\t\tif (const auto image = _photoMedia->image(Data::PhotoSize::Large)\n\t\t\t; image && !image->original().isNull()) {\n\t\t\ttryStartTextRecognition();\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::tryStartTextRecognition() {\n\tif (_stories\n\t\t|| !_session\n\t\t|| !_photo\n\t\t|| !_photoMedia\n\t\t|| !Platform::TextRecognition::IsAvailable()) {\n\t\treturn;\n\t}\n\tconst auto cache = RecognitionCache();\n\tif (!cache) {\n\t\treturn;\n\t}\n\tconst auto id = RecognitionId{\n\t\t.sessionUniqueId = _session->uniqueId(),\n\t\t.photoId = _photo->id,\n\t};\n\tif (const auto cached = cache->find(id); cached != cache->end()) {\n\t\t_recognitionRetryOnLarge = false;\n\t\tconst auto changed = (_recognitionResult.success != cached->second.success)\n\t\t\t|| (_recognitionResult.items.size() != cached->second.items.size());\n\t\t_recognitionResult = cached->second;\n\t\tif (changed) {\n\t\t\tupdateControls();\n\t\t}\n\t\treturn;\n\t}\n\tif (_recognitionPendingSessionUniqueId == id.sessionUniqueId\n\t\t&& _recognitionPendingPhotoId == id.photoId) {\n\t\t_recognitionRetryOnLarge = false;\n\t\treturn;\n\t}\n\t_photoMedia->wanted(Data::PhotoSize::Large, fileOrigin());\n\tconst auto image = _photoMedia->image(Data::PhotoSize::Large);\n\tif (!image) {\n\t\t_recognitionRetryOnLarge = true;\n\t\treturn;\n\t}\n\tconst auto original = image->original();\n\tif (original.isNull()) {\n\t\t_recognitionRetryOnLarge = true;\n\t\treturn;\n\t}\n\t_recognitionRetryOnLarge = false;\n\t_recognitionPendingSessionUniqueId = id.sessionUniqueId;\n\t_recognitionPendingPhotoId = id.photoId;\n\tconst auto weak = base::make_weak(_widget);\n\tcrl::async([=] {\n\t\tauto result = Platform::TextRecognition::RecognizeText(original);\n\t\tcrl::on_main(weak, [=, result = std::move(result)]() mutable {\n\t\t\tconst auto cache = RecognitionCache();\n\t\t\tif (!cache) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto pendingMatches = (_recognitionPendingSessionUniqueId\n\t\t\t\t== id.sessionUniqueId)\n\t\t\t\t&& (_recognitionPendingPhotoId == id.photoId);\n\t\t\tif (pendingMatches) {\n\t\t\t\t_recognitionPendingSessionUniqueId = 0;\n\t\t\t\t_recognitionPendingPhotoId = 0;\n\t\t\t}\n\t\t\t(*cache)[id] = result;\n\t\t\tif (!_session\n\t\t\t\t|| !_photo\n\t\t\t\t|| (_session->uniqueId() != id.sessionUniqueId)\n\t\t\t\t|| (_photo->id != id.photoId)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_recognitionResult = std::move(result);\n\t\t\tupdateControls();\n\t\t});\n\t});\n}\n\nUi::GL::ChosenRenderer OverlayWidget::chooseRenderer(\n\t\tUi::GL::Backend backend) {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\tif (backend == Ui::GL::Backend::QRhi) {\n\t\t_opengl = true;\n\t\treturn {\n\t\t\t.renderer = std::make_unique<RendererRhi>(this),\n\t\t\t.backend = Ui::GL::Backend::QRhi,\n\t\t};\n\t}\n#endif // Qt >= 6.7\n\t_opengl = (backend == Ui::GL::Backend::OpenGL);\n\treturn {\n\t\t.renderer = (_opengl\n\t\t\t? std::unique_ptr<Ui::GL::Renderer>(\n\t\t\t\tstd::make_unique<RendererGL>(this))\n\t\t\t: std::make_unique<RendererSW>(this)),\n\t\t.backend = backend,\n\t};\n}\n\nvoid OverlayWidget::paint(not_null<Renderer*> renderer) {\n\trenderer->paintBackground();\n\tif (contentShown()) {\n\t\tif (videoShown()) {\n\t\t\trenderer->paintTransformedVideoFrame(contentGeometry());\n\t\t\tif (_streamed->instance.player().ready()) {\n\t\t\t\t_streamed->instance.markFrameShown();\n\t\t\t\tif (_stories) {\n\t\t\t\t\t_stories->ready();\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tvalidatePhotoCurrentImage();\n\t\t\tif (_stories && !_blurred) {\n\t\t\t\t_stories->ready();\n\t\t\t}\n\t\t\tconst auto fillTransparentBackground = (!_document\n\t\t\t\t|| (!_document->sticker() && !_document->isVideoMessage()))\n\t\t\t\t&& _staticContentTransparent;\n\t\t\trenderer->paintTransformedStaticContent(\n\t\t\t\t_staticContent,\n\t\t\t\tcontentGeometry(),\n\t\t\t\t_staticContentTransparent,\n\t\t\t\tfillTransparentBackground);\n\t\t}\n\t\tpaintRadialLoading(renderer);\n\t\tif (_stories) {\n\t\t\tusing namespace Stories;\n\t\t\tconst auto paint = [&](const SiblingView &view, int index) {\n\t\t\t\trenderer->paintTransformedStaticContent(\n\t\t\t\t\tview.image,\n\t\t\t\t\tstoriesContentGeometry(view.layout, view.scale),\n\t\t\t\t\tfalse, // semi-transparent\n\t\t\t\t\tfalse, // fill transparent background\n\t\t\t\t\tindex);\n\t\t\t\tconst auto base = (index - 1) * 2;\n\t\t\t\tconst auto userpicSize = view.userpic.size()\n\t\t\t\t\t/ view.userpic.devicePixelRatio();\n\t\t\t\trenderer->paintStoriesSiblingPart(\n\t\t\t\t\tbase,\n\t\t\t\t\tview.userpic,\n\t\t\t\t\tQRect(view.userpicPosition, userpicSize));\n\t\t\t\tconst auto nameSize = view.name.size()\n\t\t\t\t\t/ view.name.devicePixelRatio();\n\t\t\t\trenderer->paintStoriesSiblingPart(\n\t\t\t\t\tbase + 1,\n\t\t\t\t\tview.name,\n\t\t\t\t\tQRect(view.namePosition, nameSize),\n\t\t\t\t\tview.nameOpacity);\n\t\t\t};\n\t\t\tif (const auto left = _stories->sibling(SiblingType::Left)) {\n\t\t\t\tpaint(left, kLeftSiblingTextureIndex);\n\t\t\t}\n\t\t\tif (const auto right = _stories->sibling(SiblingType::Right)) {\n\t\t\t\tpaint(right, kRightSiblingTextureIndex);\n\t\t\t}\n\t\t}\n\t} else if (_stories) {\n\t\tif (_videoStream) {\n\t\t\trenderer->paintVideoStream();\n\t\t} else {\n\t\t\t// Unsupported story.\n\t\t}\n\t} else if (_themePreviewShown) {\n\t\trenderer->paintThemePreview(_themePreviewRect);\n\t} else if (documentBubbleShown() && !_docRect.isEmpty()) {\n\t\trenderer->paintDocumentBubble(_docRect, _docIconRect);\n\t}\n\tif (isSaveMsgShown()) {\n\t\trenderer->paintSaveMsg(_saveMsg);\n\t}\n\tif (isChapterShown()) {\n\t\trenderer->paintChapter(_chapterRect);\n\t}\n\tif (isSpeedBoostShown()) {\n\t\trenderer->paintSpeedBoost(_speedBoostRect);\n\t}\n\n\tconst auto opacity = _fullScreenVideo ? 0. : _controlsOpacity.current();\n\tif (opacity > 0) {\n\t\tpaintControls(renderer, opacity);\n\t\tif (!_stories) {\n\t\t\trenderer->paintFooter(footerGeometry(), opacity);\n\t\t}\n\t\tif (!(_stories ? _stories->skipCaption() : _caption.isEmpty())) {\n\t\t\trenderer->paintCaption(captionGeometry(), opacity);\n\t\t}\n\t\tif (_groupThumbs) {\n\t\t\trenderer->paintGroupThumbs(\n\t\t\t\tQRect(\n\t\t\t\t\t_groupThumbsLeft,\n\t\t\t\t\t_groupThumbsTop,\n\t\t\t\t\twidth() - 2 * _groupThumbsLeft,\n\t\t\t\t\t_groupThumbs->height()),\n\t\t\t\topacity);\n\t\t}\n\t}\n\tcheckGroupThumbsAnimation();\n\tif (const auto radius = _window->manualRoundingRadius()) {\n\t\trenderer->paintRoundedCorners(radius);\n\t}\n}\n\nvoid OverlayWidget::checkGroupThumbsAnimation() {\n\tif (_groupThumbs\n\t\t&& (!_streamed || _streamed->instance.player().ready())) {\n\t\t_groupThumbs->checkForAnimationStart();\n\t}\n}\n\nvoid OverlayWidget::paintRadialLoading(not_null<Renderer*> renderer) {\n\tconst auto radial = _radial.animating();\n\tif (_streamed) {\n\t\tif (!_streamed->instance.waitingShown()) {\n\t\t\treturn;\n\t\t}\n\t} else if (!radial && (!_document || _documentMedia->loaded())) {\n\t\treturn;\n\t}\n\n\tconst auto radialOpacity = radial ? _radial.opacity() : 0.;\n\tconst auto inner = radialRect();\n\tAssert(!inner.isEmpty());\n\n\trenderer->paintRadialLoading(inner, radial, radialOpacity);\n}\n\nvoid OverlayWidget::paintRadialLoadingContent(\n\t\tPainter &p,\n\t\tQRect inner,\n\t\tbool radial,\n\t\tfloat64 radialOpacity) const {\n\tconst auto arc = inner.marginsRemoved(QMargins(\n\t\tst::radialLine,\n\t\tst::radialLine,\n\t\tst::radialLine,\n\t\tst::radialLine));\n\tconst auto paintBg = [&](float64 opacity, QBrush brush) {\n\t\tp.setOpacity(opacity);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(brush);\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(inner);\n\t\t}\n\t\tp.setOpacity(1.);\n\t};\n\n\tif (_streamed) {\n\t\tpaintBg(\n\t\t\t_streamed->instance.waitingOpacity(),\n\t\t\tst::radialBg);\n\t\tUi::InfiniteRadialAnimation::Draw(\n\t\t\tp,\n\t\t\t_streamed->instance.waitingState(),\n\t\t\tarc.topLeft(),\n\t\t\tarc.size(),\n\t\t\twidth(),\n\t\t\tst::radialFg,\n\t\t\tst::radialLine);\n\t\treturn;\n\t}\n\tif (_photo) {\n\t\tpaintBg(radialOpacity, st::radialBg);\n\t} else {\n\t\tconst auto o = overLevel(Over::Icon);\n\t\tpaintBg(\n\t\t\t_documentMedia->loaded() ? radialOpacity : 1.,\n\t\t\tanim::brush(st::msgDateImgBg, st::msgDateImgBgOver, o));\n\n\t\tconst auto icon = [&]() -> const style::icon * {\n\t\t\tif (radial || _document->loading()) {\n\t\t\t\treturn &st::historyFileThumbCancel;\n\t\t\t}\n\t\t\treturn &st::historyFileThumbDownload;\n\t\t}();\n\t\tif (icon) {\n\t\t\ticon->paintInCenter(p, inner);\n\t\t}\n\t}\n\tif (radial) {\n\t\tp.setOpacity(1);\n\t\t_radial.draw(p, arc, st::radialLine, st::radialFg);\n\t}\n}\n\nvoid OverlayWidget::paintThemePreviewContent(\n\t\tPainter &p,\n\t\tQRect outer,\n\t\tQRect clip) {\n\tconst auto fill = outer.intersected(clip);\n\tif (!fill.isEmpty()) {\n\t\tif (_themePreview) {\n\t\t\tp.drawImage(\n\t\t\t\touter.topLeft(),\n\t\t\t\t_themePreview->preview);\n\t\t} else {\n\t\t\tp.fillRect(fill, st::themePreviewBg);\n\t\t\tp.setFont(st::themePreviewLoadingFont);\n\t\t\tp.setPen(st::themePreviewLoadingFg);\n\t\t\tp.drawText(\n\t\t\t\touter,\n\t\t\t\t(_themePreviewId\n\t\t\t\t\t? tr::lng_theme_preview_generating(tr::now)\n\t\t\t\t\t: tr::lng_theme_preview_invalid(tr::now)),\n\t\t\t\tQTextOption(style::al_center));\n\t\t}\n\t}\n\n\tconst auto fillOverlay = [&](QRect fill) {\n\t\tconst auto clipped = fill.intersected(clip);\n\t\tif (!clipped.isEmpty()) {\n\t\t\tp.setOpacity(st::themePreviewOverlayOpacity);\n\t\t\tp.fillRect(clipped, st::themePreviewBg);\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t};\n\tauto titleRect = QRect(\n\t\touter.x(),\n\t\touter.y(),\n\t\touter.width(),\n\t\tst::themePreviewMargin.top());\n\tif (titleRect.x() < 0) {\n\t\ttitleRect = QRect(\n\t\t\t0,\n\t\t\touter.y(),\n\t\t\twidth(),\n\t\t\tst::themePreviewMargin.top());\n\t}\n\tif (titleRect.y() < 0) {\n\t\ttitleRect.moveTop(0);\n\t\tfillOverlay(titleRect);\n\t}\n\ttitleRect = titleRect.marginsRemoved(QMargins(\n\t\tst::themePreviewMargin.left(),\n\t\tst::themePreviewTitleTop,\n\t\tst::themePreviewMargin.right(),\n\t\t(titleRect.height()\n\t\t\t- st::themePreviewTitleTop\n\t\t\t- st::themePreviewTitleFont->height)));\n\tif (titleRect.intersects(clip)) {\n\t\tp.setFont(st::themePreviewTitleFont);\n\t\tp.setPen(st::themePreviewTitleFg);\n\t\tconst auto title = _themeCloudData.title.isEmpty()\n\t\t\t? tr::lng_theme_preview_title(tr::now)\n\t\t\t: _themeCloudData.title;\n\t\tconst auto elided = st::themePreviewTitleFont->elided(\n\t\t\ttitle,\n\t\t\ttitleRect.width());\n\t\tp.drawTextLeft(titleRect.x(), titleRect.y(), width(), elided);\n\t}\n\n\tauto buttonsRect = QRect(\n\t\touter.x(),\n\t\touter.y() + outer.height() - st::themePreviewMargin.bottom(),\n\t\touter.width(),\n\t\tst::themePreviewMargin.bottom());\n\tif (buttonsRect.y() + buttonsRect.height() > height()) {\n\t\tbuttonsRect.moveTop(height() - buttonsRect.height());\n\t\tfillOverlay(buttonsRect);\n\t}\n\tif (_themeShare && _themeCloudData.usersCount > 0) {\n\t\tp.setFont(st::boxTextFont);\n\t\tp.setPen(st::windowSubTextFg);\n\t\tconst auto left = outer.x()\n\t\t\t+ (_themeShare->x() - _themePreviewRect.x())\n\t\t\t+ _themeShare->width()\n\t\t\t- (st::themePreviewCancelButton.width / 2);\n\t\tconst auto baseline = outer.y()\n\t\t\t+ (_themeShare->y() - _themePreviewRect.y())\n\t\t\t+ st::themePreviewCancelButton.padding.top()\n\t\t\t+ st::themePreviewCancelButton.textTop\n\t\t\t+ st::themePreviewCancelButton.style.font->ascent;\n\t\tp.drawText(\n\t\t\tleft,\n\t\t\tbaseline,\n\t\t\ttr::lng_theme_preview_users(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\t_themeCloudData.usersCount));\n\t}\n}\n\nvoid OverlayWidget::paintDocumentBubbleContent(\n\t\tPainter &p,\n\t\tQRect outer,\n\t\tQRect icon,\n\t\tQRect clip) const {\n\tp.fillRect(outer, st::mediaviewFileBg);\n\tif (icon.intersects(clip)) {\n\t\tif (!_document || !_document->hasThumbnail()) {\n\t\t\tp.fillRect(icon, _docIconColor);\n\t\t\tconst auto radial = _radial.animating();\n\t\t\tconst auto radialOpacity = radial ? _radial.opacity() : 0.;\n\t\t\tif ((!_document || _documentMedia->loaded()) && (!radial || radialOpacity < 1) && _docIcon) {\n\t\t\t\t_docIcon->paint(p, icon.x() + (icon.width() - _docIcon->width()), icon.y(), width());\n\t\t\t\tp.setPen(st::mediaviewFileExtFg);\n\t\t\t\tp.setFont(st::mediaviewFileExtFont);\n\t\t\t\tif (!_docExt.isEmpty()) {\n\t\t\t\t\tp.drawText(icon.x() + (icon.width() - _docExtWidth) / 2, icon.y() + st::mediaviewFileExtTop + st::mediaviewFileExtFont->ascent, _docExt);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (const auto thumbnail = _documentMedia->thumbnail()) {\n\t\t\tint32 rf(style::DevicePixelRatio());\n\t\t\tp.drawPixmap(icon.topLeft(), thumbnail->pix(_docThumbw), QRect(_docThumbx * rf, _docThumby * rf, st::mediaviewFileIconSize * rf, st::mediaviewFileIconSize * rf));\n\t\t}\n\t}\n\tif (!icon.contains(clip)) {\n\t\tp.setPen(st::mediaviewFileNameFg);\n\t\tp.setFont(st::mediaviewFileNameFont);\n\t\tp.drawTextLeft(outer.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, outer.y() + st::mediaviewFilePadding + st::mediaviewFileNameTop, width(), _docName, _docNameWidth);\n\n\t\tp.setPen(st::mediaviewFileSizeFg);\n\t\tp.setFont(st::mediaviewFont);\n\t\tp.drawTextLeft(outer.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, outer.y() + st::mediaviewFilePadding + st::mediaviewFileSizeTop, width(), _docSize, _docSizeWidth);\n\t}\n}\n\nvoid OverlayWidget::paintSaveMsgContent(\n\t\tPainter &p,\n\t\tQRect outer,\n\t\tQRect clip) {\n\tp.setOpacity(_saveMsgAnimation.value(1.));\n\tUi::FillRoundRect(p, outer, st::mediaviewSaveMsgBg, Ui::MediaviewSaveCorners);\n\tst::mediaviewSaveMsgCheck.paint(p, outer.topLeft() + st::mediaviewSaveMsgCheckPos, width());\n\n\tp.setPen(st::mediaviewSaveMsgFg);\n\t_saveMsgText.draw(p, {\n\t\t.position = QPoint(\n\t\t\touter.x() + st::mediaviewSaveMsgPadding.left(),\n\t\t\touter.y() + st::mediaviewSaveMsgPadding.top()),\n\t\t.availableWidth = outer.width() - st::mediaviewSaveMsgPadding.left() - st::mediaviewSaveMsgPadding.right(),\n\t\t.palette = &st::mediaviewTextPalette,\n\t});\n\tp.setOpacity(1);\n}\n\nvoid OverlayWidget::showChapterIndicator(\n\t\tconst QString &name,\n\t\tint direction) {\n\tif (name.isEmpty()) {\n\t\treturn;\n\t}\n\t_chapterText = name;\n\n\tconst auto font = st::mediaviewChapterFont;\n\tconst auto padding = st::mediaviewChapterPadding;\n\tconst auto arrowSpace = st::mediaviewChapterArrowWidth\n\t\t+ st::mediaviewChapterArrowGap;\n\tconst auto textWidth = font->width(_chapterText);\n\tconst auto w = padding.left()\n\t\t+ arrowSpace\n\t\t+ textWidth\n\t\t+ arrowSpace\n\t\t+ padding.right();\n\tconst auto h = rect::m::sum::v(padding) + font->height;\n\t_chapterRect = QRect(\n\t\t(width() - w) / 2,\n\t\t_minUsedTop + (_maxUsedHeight - h) / 2,\n\t\tw,\n\t\th);\n\n\tif (isChapterShown()) {\n\t\t_chapterTimer.callOnce(st::mediaviewChapterShown);\n\t} else {\n\t\tconst auto callback = [=](float64 value) {\n\t\t\tupdateChapter();\n\t\t\tif (!_chapterAnimation.animating()) {\n\t\t\t\t_chapterTimer.callOnce(st::mediaviewChapterShown);\n\t\t\t}\n\t\t};\n\t\t_chapterAnimation.start(\n\t\t\tcallback,\n\t\t\t0.,\n\t\t\t1.,\n\t\t\tst::mediaviewChapterShowing);\n\t}\n\n\t_chapterArrows.erase(\n\t\tranges::remove_if(\n\t\t\t_chapterArrows,\n\t\t\t[&](const auto &a) {\n\t\t\t\treturn !a->animation.animating()\n\t\t\t\t\t|| (a->direction != direction);\n\t\t\t}),\n\t\t_chapterArrows.end());\n\tauto arrow = std::make_unique<ChapterArrow>();\n\tarrow->direction = direction;\n\tarrow->animation.start(\n\t\t[=] { updateChapter(); },\n\t\t0.,\n\t\t1.,\n\t\tst::mediaviewChapterArrowSlide\n\t\t\t+ st::mediaviewChapterArrowPause\n\t\t\t+ st::mediaviewChapterArrowFade);\n\t_chapterArrows.push_back(std::move(arrow));\n\tupdateChapter();\n}\n\nvoid OverlayWidget::paintChapterContent(\n\t\tPainter &p,\n\t\tQRect outer,\n\t\tQRect clip) {\n\tconst auto opacity = _chapterAnimation.value(1.);\n\tp.setOpacity(opacity);\n\tUi::FillRoundRect(\n\t\tp,\n\t\touter,\n\t\tst::mediaviewSaveMsgBg,\n\t\tUi::MediaviewSaveCorners);\n\n\tconst auto font = st::mediaviewChapterFont;\n\tconst auto padding = st::mediaviewChapterPadding;\n\tconst auto arrowSize = st::mediaviewChapterArrowSize;\n\tconst auto arrowWidth = st::mediaviewChapterArrowWidth;\n\tconst auto arrowGap = st::mediaviewChapterArrowGap;\n\tconst auto textX = outer.x()\n\t\t+ padding.left()\n\t\t+ arrowWidth\n\t\t+ arrowGap;\n\tconst auto textY = outer.y() + padding.top();\n\n\tp.setFont(font);\n\tp.setPen(st::mediaviewSaveMsgFg);\n\tp.drawText(textX, textY + font->ascent, _chapterText);\n\n\tconst auto cy = outer.y() + outer.height() / 2.;\n\tconst auto halfH = arrowSize / 2.;\n\tconst auto totalDuration = float64(st::mediaviewChapterArrowSlide\n\t\t+ st::mediaviewChapterArrowPause\n\t\t+ st::mediaviewChapterArrowFade);\n\tconst auto slideEnd = st::mediaviewChapterArrowSlide / totalDuration;\n\tconst auto fadeStart = 1. - st::mediaviewChapterArrowFade / totalDuration;\n\tauto hq = PainterHighQualityEnabler(p);\n\tauto pen = QPen(st::mediaviewSaveMsgFg->c);\n\tpen.setWidthF(st::lineWidth * 1.5);\n\tpen.setCapStyle(Qt::RoundCap);\n\tpen.setJoinStyle(Qt::RoundJoin);\n\tp.setPen(pen);\n\tp.setBrush(Qt::NoBrush);\n\tfor (const auto &arrow : _chapterArrows) {\n\t\tif (!arrow->animation.animating()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto progress = arrow->animation.value(1.);\n\t\tconst auto slideProgress = std::min(progress / slideEnd, 1.);\n\t\tconst auto shift = st::mediaviewChapterArrowShift * slideProgress;\n\t\tconst auto arrowOpacity = (progress <= fadeStart)\n\t\t\t? slideProgress\n\t\t\t: (1. - progress) / (1. - fadeStart);\n\t\tp.setOpacity(opacity * arrowOpacity);\n\t\tauto path = QPainterPath();\n\t\tif (arrow->direction > 0) {\n\t\t\tconst auto ax = outer.x()\n\t\t\t\t+ outer.width()\n\t\t\t\t- padding.right()\n\t\t\t\t- arrowGap\n\t\t\t\t- arrowWidth\n\t\t\t\t+ shift;\n\t\t\tpath.moveTo(ax, cy - halfH);\n\t\t\tpath.lineTo(ax + arrowWidth, cy);\n\t\t\tpath.lineTo(ax, cy + halfH);\n\t\t} else {\n\t\t\tconst auto ax = outer.x()\n\t\t\t\t+ padding.left()\n\t\t\t\t+ arrowGap\n\t\t\t\t+ arrowWidth\n\t\t\t\t- shift;\n\t\t\tpath.moveTo(ax, cy - halfH);\n\t\t\tpath.lineTo(ax - arrowWidth, cy);\n\t\t\tpath.lineTo(ax, cy + halfH);\n\t\t}\n\t\tp.drawPath(path);\n\t}\n\tp.setOpacity(1);\n}\n\nbool OverlayWidget::isChapterShown() const {\n\treturn _chapterAnimation.animating() || _chapterTimer.isActive();\n}\n\nvoid OverlayWidget::updateChapter() {\n\tupdate(_chapterRect);\n}\n\nvoid OverlayWidget::startSpeedBoost() {\n\tif (_speedBoostActive || !_streamed || _stories) {\n\t\treturn;\n\t}\n\tif (_speedBoostFromMouse && _down != Over::Video) {\n\t\treturn;\n\t}\n\t_speedBoostActive = true;\n\t_speedBoostSavedSpeed = _streamed->instance.speed();\n\t_speedBoostSpeed = 2.0;\n\t_speedBoostPhase = 0.;\n\t_speedBoostLastFrame = crl::now();\n\t_streamed->instance.setSpeed(_speedBoostSpeed);\n\t_speedBoostHoldTimer.cancel();\n\n\tupdateSpeedBoostRect();\n\t_speedBoostAnimation.start(\n\t\t[=] { updateSpeedBoost(); },\n\t\t0.,\n\t\t1.,\n\t\tst::mediaviewSpeedBoostShowing);\n\t_speedBoostTicker.start();\n}\n\nvoid OverlayWidget::stopSpeedBoost() {\n\tif (!_speedBoostActive) {\n\t\treturn;\n\t}\n\t_speedBoostActive = false;\n\t_speedBoostFromMouse = false;\n\t_speedBoostHoldTimer.cancel();\n\tif (_streamed) {\n\t\t_streamed->instance.setSpeed(_speedBoostSavedSpeed);\n\t}\n\t_speedBoostAnimation.start(\n\t\t[=] { updateSpeedBoost(); },\n\t\t1.,\n\t\t0.,\n\t\tst::mediaviewSpeedBoostHiding);\n}\n\nvoid OverlayWidget::updateSpeedBoostRect() {\n\tconst auto was = _speedBoostRect;\n\tconst auto font = st::mediaviewSpeedBoostFont;\n\tconst auto padding = st::mediaviewSpeedBoostPadding;\n\tconst auto maxText\n\t\t= QString::fromUtf8(\"%1\\xC3\\x97\").arg(kSpeedMax, 0, 'f', 1);\n\tconst auto arrowStep = std::max(\n\t\t1,\n\t\tst::mediaviewSpeedBoostArrowWidth\n\t\t\t+ st::mediaviewSpeedBoostArrowGap\n\t\t\t- st::mediaviewSpeedBoostArrowOverlap);\n\tconst auto arrowSpace = st::mediaviewSpeedBoostArrowWidth\n\t\t+ arrowStep\n\t\t+ st::mediaviewSpeedBoostTextRight;\n\tconst auto w = padding.left()\n\t\t+ font->width(maxText)\n\t\t+ arrowSpace\n\t\t+ padding.right();\n\tconst auto h = rect::m::sum::v(padding) + font->height;\n\t_speedBoostRect = QRect(\n\t\t(width() - w) / 2,\n\t\t_minUsedTop + st::mediaviewSpeedBoostTop,\n\t\tw,\n\t\th);\n\tif (was != _speedBoostRect && !was.isEmpty()) {\n\t\tupdate(was);\n\t}\n}\n\nvoid OverlayWidget::paintSpeedBoostContent(\n\t\tPainter &p,\n\t\tQRect outer,\n\t\tQRect clip) {\n\tconst auto opacity = _speedBoostAnimation.value(\n\t\t_speedBoostActive ? 1. : 0.);\n\tif (opacity <= 0.) {\n\t\treturn;\n\t}\n\n\tconst auto scale = 0.6 + 0.4 * opacity;\n\tp.save();\n\tp.translate(rect::center(outer));\n\tp.scale(scale, scale);\n\tp.translate(-rect::center(outer));\n\n\tp.setOpacity(opacity);\n\tUi::FillRoundRect(\n\t\tp,\n\t\touter,\n\t\tst::mediaviewSaveMsgBg,\n\t\tUi::MediaviewSaveCorners);\n\n\tconst auto font = st::mediaviewSpeedBoostFont;\n\tconst auto padding = st::mediaviewSpeedBoostPadding;\n\tconst auto text\n\t\t= QString::fromUtf8(\"%1\\xC3\\x97\").arg(_speedBoostSpeed, 0, 'f', 1);\n\tconst auto arrowWidth = st::mediaviewSpeedBoostArrowWidth;\n\tconst auto arrowHeight = st::mediaviewSpeedBoostArrowHeight;\n\tconst auto arrowGap = st::mediaviewSpeedBoostArrowGap;\n\tconst auto arrowRound = style::ConvertFloatScale(1.5);\n\tconst auto arrowStep = std::max(\n\t\t1,\n\t\tarrowWidth + arrowGap - st::mediaviewSpeedBoostArrowOverlap);\n\tconst auto arrowsWidth = arrowWidth + arrowStep;\n\tconst auto innerArrowWidth = arrowWidth - arrowRound * 2.;\n\tconst auto innerArrowHeight = arrowHeight - arrowRound * 2.;\n\tconst auto innerHalfH = innerArrowHeight / 2.;\n\tconst auto textRight = st::mediaviewSpeedBoostTextRight;\n\tconst auto textWidth = font->width(text);\n\tconst auto contentWidth = textWidth + textRight + arrowsWidth;\n\tconst auto contentX = outer.x()\n\t\t+ (outer.width() - contentWidth) / 2.;\n\tconst auto textY = outer.y() + padding.top();\n\tp.setFont(font);\n\tp.setPen(st::mediaviewSaveMsgFg);\n\tp.drawText(int(contentX), textY + font->ascent, text);\n\n\tconst auto cy = outer.y() + outer.height() / 2.;\n\tconst auto arrowsX = contentX + textWidth + textRight;\n\tconst auto now = crl::now();\n\tconst auto dt = std::min(\n\t\t0.016,\n\t\t(now - _speedBoostLastFrame) / 1000.);\n\t_speedBoostLastFrame = now;\n\t_speedBoostPhase += dt * 1.5 * std::min(_speedBoostSpeed, 4.);\n\n\tauto hq = PainterHighQualityEnabler(p);\n\tauto stroker = QPainterPathStroker();\n\tif (arrowRound > 0.) {\n\t\tstroker.setWidth(arrowRound * 2.);\n\t\tstroker.setCapStyle(Qt::RoundCap);\n\t\tstroker.setJoinStyle(Qt::RoundJoin);\n\t}\n\tp.setPen(Qt::NoPen);\n\tfor (auto i = 0; i < 2; ++i) {\n\t\tconst auto phase = _speedBoostPhase + i * 0.17;\n\t\tconst auto pulse = std::sin(phase * M_PI) / 2. + 1.;\n\t\tconst auto alpha = opacity * (0.2 + 0.75 * pulse);\n\t\tp.setBrush(anim::with_alpha(st::mediaviewSaveMsgFg->c, alpha));\n\t\tconst auto ax = arrowsX + i * arrowStep;\n\t\tauto path = QPainterPath();\n\t\tpath.moveTo(ax + arrowRound, cy - innerHalfH);\n\t\tpath.lineTo(ax + arrowRound + innerArrowWidth, cy);\n\t\tpath.lineTo(ax + arrowRound, cy + innerHalfH);\n\t\tpath.closeSubpath();\n\t\tif (arrowRound > 0.) {\n\t\t\tpath = path.united(stroker.createStroke(path));\n\t\t}\n\t\tp.drawPath(path);\n\t}\n\tp.restore();\n\tp.setOpacity(1);\n}\n\nbool OverlayWidget::isSpeedBoostShown() const {\n\treturn _speedBoostActive || _speedBoostAnimation.animating();\n}\n\nvoid OverlayWidget::updateSpeedBoost() {\n\tupdate(_speedBoostRect);\n}\n\nbool OverlayWidget::saveControlLocked() const {\n\tconst auto story = _stories ? _stories->story() : nullptr;\n\treturn story\n\t\t&& story->canDownloadIfPremium()\n\t\t&& !story->canDownloadChecked();\n}\n\nvoid OverlayWidget::paintControls(\n\t\tnot_null<Renderer*> renderer,\n\t\tfloat64 opacity) {\n\tstruct Control {\n\t\tOver state = Over::None;\n\t\tbool visible = false;\n\t\tconst QRect &over;\n\t\tconst QRect &inner;\n\t\tconst style::icon &icon;\n\t\tbool nonbright = false;\n\t};\n\t// When adding / removing controls please update RendererGL.\n\tconst Control controls[] = {\n\t\t{\n\t\t\tOver::Left,\n\t\t\t_leftNavVisible,\n\t\t\t_leftNavOver,\n\t\t\t_leftNavIcon,\n\t\t\t_stories ? st::storiesLeft : st::mediaviewLeft,\n\t\t\ttrue },\n\t\t{\n\t\t\tOver::Right,\n\t\t\t_rightNavVisible,\n\t\t\t_rightNavOver,\n\t\t\t_rightNavIcon,\n\t\t\t_stories ? st::storiesRight : st::mediaviewRight,\n\t\t\ttrue },\n\t\t{\n\t\t\tOver::Save,\n\t\t\t_saveVisible,\n\t\t\t_saveNavOver,\n\t\t\t_saveNavIcon,\n\t\t\t(saveControlLocked()\n\t\t\t\t? st::mediaviewSaveLocked\n\t\t\t\t: st::mediaviewSave) },\n\t\t{\n\t\t\tOver::Share,\n\t\t\t_shareVisible,\n\t\t\t_shareNavOver,\n\t\t\t_shareNavIcon,\n\t\t\tst::mediaviewShare },\n\t\t{\n\t\t\tOver::Rotate,\n\t\t\t_rotateVisible,\n\t\t\t_rotateNavOver,\n\t\t\t_rotateNavIcon,\n\t\t\tst::mediaviewRotate },\n\t\t{\n\t\t\tOver::More,\n\t\t\ttrue,\n\t\t\t_moreNavOver,\n\t\t\t_moreNavIcon,\n\t\t\tst::mediaviewMore },\n\t\t{\n\t\t\tOver::Draw,\n\t\t\t_drawVisible,\n\t\t\t_drawNavOver,\n\t\t\t_drawNavIcon,\n\t\t\tst::mediaviewDraw },\n\t\t{\n\t\t\tOver::Recognize,\n\t\t\t_recognizeVisible,\n\t\t\t_recognizeNavOver,\n\t\t\t_recognizeNavIcon,\n\t\t\tst::mediaviewRecognize },\n\t};\n\n\trenderer->paintControlsStart();\n\tfor (const auto &control : controls) {\n\t\tif (!control.visible) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto progress = overLevel(control.state);\n\t\tconst auto bg = progress;\n\t\tconst auto icon = controlOpacity(progress, control.nonbright);\n\t\trenderer->paintControl(\n\t\t\tcontrol.state,\n\t\t\tcontrol.over,\n\t\t\tbg * opacity,\n\t\t\tcontrol.inner,\n\t\t\ticon * opacity,\n\t\t\tcontrol.icon);\n\t}\n}\n\nfloat64 OverlayWidget::controlOpacity(\n\t\tfloat64 progress,\n\t\tbool nonbright) const {\n\tif (nonbright && _stories) {\n\t\treturn progress * kStoriesNavOverOpacity\n\t\t\t+ (1. - progress) * kStoriesNavOpacity;\n\t}\n\tconst auto normal = _windowed\n\t\t? kNormalIconOpacity\n\t\t: kMaximizedIconOpacity;\n\treturn progress + (1. - progress) * normal;\n}\n\nvoid OverlayWidget::paintFooterContent(\n\t\tPainter &p,\n\t\tQRect outer,\n\t\tQRect clip,\n\t\tfloat64 opacity) {\n\tp.setPen(st::mediaviewControlFg);\n\tp.setFont(st::mediaviewThickFont);\n\n\t// header\n\tconst auto shift = outer.topLeft() - _headerNav.topLeft();\n\tconst auto header = _headerNav.translated(shift);\n\tconst auto name = _nameNav.translated(shift);\n\tconst auto date = _dateNav.translated(shift);\n\tif (header.intersects(clip)) {\n\t\tauto o = _headerHasLink ? overLevel(Over::Header) : 0;\n\t\tp.setOpacity(controlOpacity(o) * opacity);\n\t\tp.drawText(header.left(), header.top() + st::mediaviewThickFont->ascent, _headerText);\n\n\t\tif (o > 0) {\n\t\t\tp.setOpacity(o * opacity);\n\t\t\tp.drawLine(header.left(), header.top() + st::mediaviewThickFont->ascent + 1, header.right(), header.top() + st::mediaviewThickFont->ascent + 1);\n\t\t}\n\t}\n\n\tp.setFont(st::mediaviewFont);\n\n\t// name\n\tif (_nameNav.isValid() && name.intersects(clip)) {\n\t\tfloat64 o = _from ? overLevel(Over::Name) : 0.;\n\t\tp.setOpacity(controlOpacity(o) * opacity);\n\t\t_fromNameLabel.drawElided(p, name.left(), name.top(), name.width());\n\n\t\tif (o > 0) {\n\t\t\tp.setOpacity(o * opacity);\n\t\t\tp.drawLine(name.left(), name.top() + st::mediaviewFont->ascent + 1, name.right(), name.top() + st::mediaviewFont->ascent + 1);\n\t\t}\n\t}\n\n\t// separator\n\tif (_separatorNav.isValid()) {\n\t\tconst auto separator = _separatorNav.translated(shift);\n\t\tif (separator.intersects(clip)) {\n\t\t\tp.setOpacity(controlOpacity(0.) * opacity);\n\t\t\tp.drawText(\n\t\t\t\tseparator.left(),\n\t\t\t\tseparator.top() + st::mediaviewFont->ascent,\n\t\t\t\tUi::kQBullet);\n\t\t}\n\t}\n\n\t// date\n\tif (date.intersects(clip)) {\n\t\tfloat64 o = overLevel(Over::Date);\n\t\tp.setOpacity(controlOpacity(o) * opacity);\n\t\tp.drawText(date.left(), date.top() + st::mediaviewFont->ascent, _dateText);\n\n\t\tif (o > 0) {\n\t\t\tp.setOpacity(o * opacity);\n\t\t\tp.drawLine(date.left(), date.top() + st::mediaviewFont->ascent + 1, date.right(), date.top() + st::mediaviewFont->ascent + 1);\n\t\t}\n\t}\n\n\t// Size.\n\tif (_sizeNav.isValid()) {\n\t\tconst auto size = _sizeNav.translated(shift);\n\t\tif (size.intersects(clip)) {\n\t\t\tp.setOpacity(controlOpacity(0) * opacity);\n\t\t\tp.drawText(size.left(), size.top() + st::mediaviewFont->ascent, _sizeText);\n\t\t}\n\t}\n\t//\n}\n\nQRect OverlayWidget::footerGeometry() const {\n\treturn _headerNav.united(_nameNav).united(_separatorNav).united(_dateNav).united(_sizeNav);\n}\n\nvoid OverlayWidget::paintCaptionContent(\n\t\tPainter &p,\n\t\tQRect outer,\n\t\tQRect clip,\n\t\tfloat64 opacity) {\n\tconst auto full = outer.marginsRemoved(st::mediaviewCaptionPadding);\n\tconst auto inner = full.marginsRemoved(\n\t\t_stories ? _stories->repostCaptionPadding() : QMargins());\n\tif (_stories) {\n\t\tp.setOpacity(1.);\n\t\tif (_stories->repost()) {\n\t\t\t_stories->drawRepostInfo(p, full.x(), full.y(), full.width());\n\t\t}\n\t} else {\n\t\tp.setOpacity(opacity);\n\t\tp.setBrush(st::mediaviewCaptionBg);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawRoundedRect(\n\t\t\touter,\n\t\t\tst::mediaviewCaptionRadius,\n\t\t\tst::mediaviewCaptionRadius);\n\t}\n\tif (inner.intersects(clip)) {\n\t\tp.setPen(st::mediaviewCaptionFg);\n\t\t_caption.draw(p, {\n\t\t\t.position = inner.topLeft(),\n\t\t\t.availableWidth = inner.width(),\n\t\t\t.palette = &st::mediaviewTextPalette,\n\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t.pausedEmoji = On(PowerSaving::kEmojiChat),\n\t\t\t.pausedSpoiler = On(PowerSaving::kChatSpoiler),\n\t\t\t.elisionHeight = inner.height(),\n\t\t\t.elisionRemoveFromEnd = _captionSkipBlockWidth,\n\t\t});\n\n\t\tif (_captionShowMoreWidth > 0) {\n\t\t\tconst auto padding = st::storiesShowMorePadding;\n\t\t\tconst auto showMoreLeft = outer.x()\n\t\t\t\t+ outer.width()\n\t\t\t\t- padding.right()\n\t\t\t\t- _captionShowMoreWidth;\n\t\t\tconst auto showMoreTop = outer.y()\n\t\t\t\t+ outer.height()\n\t\t\t\t- padding.bottom()\n\t\t\t\t- st::storiesShowMoreFont->height;\n\t\t\tconst auto underline = _captionExpandLink\n\t\t\t\t&& ClickHandler::showAsActive(_captionExpandLink);\n\t\t\tp.setFont(underline\n\t\t\t\t? st::storiesShowMoreFont->underline()\n\t\t\t\t: st::storiesShowMoreFont);\n\t\t\tp.drawTextLeft(\n\t\t\t\tshowMoreLeft,\n\t\t\t\tshowMoreTop,\n\t\t\t\twidth(),\n\t\t\t\ttr::lng_stories_show_more(tr::now));\n\t\t}\n\t}\n}\n\nQRect OverlayWidget::captionGeometry() const {\n\treturn _captionRect.marginsAdded(\n\t\tst::mediaviewCaptionPadding\n\t).marginsAdded(\n\t\t_stories ? _stories->repostCaptionPadding() : QMargins());\n}\n\nvoid OverlayWidget::paintGroupThumbsContent(\n\t\tPainter &p,\n\t\tQRect outer,\n\t\tQRect clip,\n\t\tfloat64 opacity) {\n\tp.setOpacity(opacity);\n\t_groupThumbs->paint(p, outer.x(), outer.y(), width());\n\tif (_groupThumbs->hidden()) {\n\t\t_groupThumbs = nullptr;\n\t\t_groupThumbsRect = QRect();\n\t}\n}\n\nbool OverlayWidget::isSaveMsgShown() const {\n\treturn _saveMsgAnimation.animating() || _saveMsgTimer.isActive();\n}\n\nvoid OverlayWidget::handleKeyPress(not_null<QKeyEvent*> e) {\n\tif (_processingKeyPress) {\n\t\treturn;\n\t}\n\t_processingKeyPress = true;\n\tconst auto guard = gsl::finally([&] { _processingKeyPress = false; });\n\tconst auto key = e->key();\n\tconst auto modifiers = e->modifiers();\n\tconst auto ctrl = modifiers.testFlag(Qt::ControlModifier);\n\tif (_stories) {\n\t\tif (key == Qt::Key_Space && _down != Over::Video) {\n\t\t\t_stories->togglePaused(!_stories->paused());\n\t\t\treturn;\n\t\t}\n\t} else if (_streamed) {\n\t\t// Ctrl + F for full screen toggle is in eventFilter().\n\t\tconst auto toggleFull = (modifiers.testFlag(Qt::AltModifier) || ctrl)\n\t\t\t&& (key == Qt::Key_Enter || key == Qt::Key_Return);\n\t\tif (toggleFull) {\n\t\t\tplaybackToggleFullScreen();\n\t\t\treturn;\n\t\t} else if (key == Qt::Key_K) {\n\t\t\tplaybackPauseResume();\n\t\t\treturn;\n\t\t} else if (key == Qt::Key_Space) {\n\t\t\tif (!e->isAutoRepeat()) {\n\t\t\t\t_speedBoostHoldTimer.callOnce(\n\t\t\t\t\tst::mediaviewSpeedBoostHoldDelay);\n\t\t\t}\n\t\t\treturn;\n\t\t} else if (key == Qt::Key_J) {\n\t\t\tactivateControls();\n\t\t\tseekRelativeTime(-kSeekTimeMsLong);\n\t\t\treturn;\n\t\t} else if (key == Qt::Key_L) {\n\t\t\tactivateControls();\n\t\t\tseekRelativeTime(kSeekTimeMsLong);\n\t\t\treturn;\n\t\t} else if ((key == Qt::Key_Period || key == Qt::Key_Comma)\n\t\t\t&& _streamed->instance.player().paused()) {\n\t\t\tactivateControls();\n\t\t\t_frameStepPending += (key == Qt::Key_Period) ? 1 : -1;\n\t\t\tif (!_frameStepThrottle.isActive()) {\n\t\t\t\tflushPendingFrameStep();\n\t\t\t\t_frameStepThrottle.callOnce(kFrameStepThrottleMs);\n\t\t\t}\n\t\t\treturn;\n\t\t} else if (modifiers.testFlag(Qt::AltModifier)\n\t\t\t&& (key == Qt::Key_Left || key == Qt::Key_Right)\n\t\t\t&& _streamed->controls\n\t\t\t&& _streamed->controls->hasTimestamps()) {\n\t\t\tconst auto &state = _streamed->instance.info().video.state;\n\t\t\tconst auto duration = state.duration;\n\t\t\tif (duration > 0) {\n\t\t\t\tconst auto progress = state.position\n\t\t\t\t\t/ float64(duration);\n\t\t\t\tconst auto &controls = _streamed->controls;\n\t\t\t\tif (key == Qt::Key_Right) {\n\t\t\t\t\tif (const auto ts = controls->nextTimestamp(progress)) {\n\t\t\t\t\t\tactivateControls();\n\t\t\t\t\t\trestartAtProgress(ts->position);\n\t\t\t\t\t\tshowChapterIndicator(ts->label, 1);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif (const auto ts = controls->prevTimestamp(progress)) {\n\t\t\t\t\t\tactivateControls();\n\t\t\t\t\t\trestartAtProgress(ts->position);\n\t\t\t\t\t\tshowChapterIndicator(ts->label, -1);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tactivateControls();\n\t\t\t\t\t\trestartAtSeekPosition(0);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t} else if (_fullScreenVideo) {\n\t\t\tif (key == Qt::Key_Escape) {\n\t\t\t\tplaybackToggleFullScreen();\n\t\t\t} else if (ctrl) {\n\t\t\t} else if (key == Qt::Key_0) {\n\t\t\t\tactivateControls();\n\t\t\t\trestartAtSeekPosition(0);\n\t\t\t} else if (key >= Qt::Key_1 && key <= Qt::Key_9) {\n\t\t\t\tactivateControls();\n\t\t\t\tconst auto index = int(key - Qt::Key_0);\n\t\t\t\trestartAtProgress(index / 10.0);\n\t\t\t} else if (key == Qt::Key_Left) {\n\t\t\t\tactivateControls();\n\t\t\t\tseekRelativeTime(-kSeekTimeMs);\n\t\t\t} else if (key == Qt::Key_Right) {\n\t\t\t\tactivateControls();\n\t\t\t\tseekRelativeTime(kSeekTimeMs);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t}\n\tif (!_menu && key == Qt::Key_Escape) {\n\t\tif (_document && _document->loading() && !_streamed) {\n\t\t\thandleDocumentClick();\n\t\t} else {\n\t\t\tclose();\n\t\t}\n\t} else if (e == QKeySequence::Save || e == QKeySequence::SaveAs) {\n\t\tsaveAs();\n\t} else if (key == Qt::Key_Copy || (key == Qt::Key_C && ctrl)) {\n\t\tcopyMedia();\n\t} else if (key == Qt::Key_Enter\n\t\t|| key == Qt::Key_Return\n\t\t|| key == Qt::Key_Space) {\n\t\tif (_streamed) {\n\t\t\tplaybackPauseResume();\n\t\t} else if (_document\n\t\t\t&& !_document->loading()\n\t\t\t&& (documentBubbleShown() || !_documentMedia->loaded())) {\n\t\t\thandleDocumentClick();\n\t\t}\n\t} else if (key == Qt::Key_Left) {\n\t\tif (_controlsHideTimer.isActive()) {\n\t\t\tactivateControls();\n\t\t}\n\t\tmoveToNext(-1);\n\t} else if (key == Qt::Key_H && !_stories) {\n\t\tif (_flip & Qt::Horizontal) {\n\t\t\t_flip &= ~Qt::Horizontal;\n\t\t} else {\n\t\t\t_flip |= Qt::Horizontal;\n\t\t}\n\t\tif (_photo) {\n\t\t\tvalidatePhotoCurrentImage();\n\t\t\tredisplayContent();\n\t\t}\n\t} else if (key == Qt::Key_V && !_stories) {\n\t\tif (_flip & Qt::Vertical) {\n\t\t\t_flip &= ~Qt::Vertical;\n\t\t} else {\n\t\t\t_flip |= Qt::Vertical;\n\t\t}\n\t\tif (_photo) {\n\t\t\tvalidatePhotoCurrentImage();\n\t\t\tredisplayContent();\n\t\t}\n\t} else if (key == Qt::Key_Right) {\n\t\tif (_controlsHideTimer.isActive()) {\n\t\t\tactivateControls();\n\t\t}\n\t\tif (!moveToNext(1) && _stories) {\n\t\t\tstoriesClose();\n\t\t}\n\t} else if (key == Qt::Key_Plus\n\t\t|| key == Qt::Key_Equal\n\t\t|| key == ']'\n\t\t|| key == Qt::Key_Up) {\n\t\tzoomIn();\n\t} else if (key == Qt::Key_Minus\n\t\t|| key == Qt::Key_Underscore\n\t\t|| key == Qt::Key_Down) {\n\t\tzoomOut();\n\t} else if (key == Qt::Key_Asterisk || key == Qt::Key_0) {\n\t\tzoomReset();\n\t} else if (key == Qt::Key_I) {\n\t\tupdate();\n\t} else if (_stories) {\n\t\t_stories->tryProcessKeyInput(e);\n\t}\n}\n\nvoid OverlayWidget::handleKeyRelease(not_null<QKeyEvent*> e) {\n\tif (e->isAutoRepeat()) {\n\t\treturn;\n\t}\n\tif (e->key() == Qt::Key_Space && _streamed && !_stories) {\n\t\tif (_speedBoostHoldTimer.isActive()) {\n\t\t\t_speedBoostHoldTimer.cancel();\n\t\t\tplaybackPauseResume();\n\t\t} else if (_speedBoostActive) {\n\t\t\tstopSpeedBoost();\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::handleWheelEvent(not_null<QWheelEvent*> e) {\n\tconstexpr auto step = int(QWheelEvent::DefaultDeltasPerStep);\n\n\tconst auto acceptForJump = !_stories\n\t\t&& ((e->source() == Qt::MouseEventNotSynthesized)\n\t\t\t|| (e->source() == Qt::MouseEventSynthesizedBySystem));\n\t_verticalWheelDelta += e->angleDelta().y();\n\twhile (qAbs(_verticalWheelDelta) >= step) {\n\t\tif (_verticalWheelDelta < 0) {\n\t\t\t_verticalWheelDelta += step;\n\t\t\tif (!e->modifiers().testFlag(Qt::ControlModifier)) {\n\t\t\t\tzoomOut();\n\t\t\t} else if (acceptForJump) {\n\t\t\t\tmoveToNext(1);\n\t\t\t}\n\t\t} else {\n\t\t\t_verticalWheelDelta -= step;\n\t\t\tif (!e->modifiers().testFlag(Qt::ControlModifier)) {\n\t\t\t\tzoomIn();\n\t\t\t} else if (acceptForJump) {\n\t\t\t\tmoveToNext(-1);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::setZoomLevel(int newZoom, bool force) {\n\tif (_stories\n\t\t|| (!force && _zoom == newZoom)\n\t\t|| (_fullScreenVideo && newZoom != kZoomToScreenLevel)) {\n\t\treturn;\n\t}\n\n\tconst auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault;\n\tfloat64 nx, ny, z = (_zoom == kZoomToScreenLevel) ? full : _zoom;\n\tconst auto contentSize = videoShown()\n\t\t? style::ConvertScale(videoSize())\n\t\t: QSize(_width, _height);\n\t_oldGeometry = contentGeometry();\n\t_geometryAnimation.stop();\n\n\t_w = contentSize.width();\n\t_h = contentSize.height();\n\tif (z >= 0) {\n\t\tnx = (_x - width() / 2.) / (z + 1);\n\t\tny = (_y - _availableHeight / 2.) / (z + 1);\n\t} else {\n\t\tnx = (_x - width() / 2.) * (-z + 1);\n\t\tny = (_y - _availableHeight / 2.) * (-z + 1);\n\t}\n\t_zoom = newZoom;\n\tz = (_zoom == kZoomToScreenLevel) ? full : _zoom;\n\tif (z > 0) {\n\t\t_w = qRound(_w * (z + 1));\n\t\t_h = qRound(_h * (z + 1));\n\t\t_x = qRound(nx * (z + 1) + width() / 2.);\n\t\t_y = qRound(ny * (z + 1) + _availableHeight / 2.);\n\t} else {\n\t\t_w = qRound(_w / (-z + 1));\n\t\t_h = qRound(_h / (-z + 1));\n\t\t_x = qRound(nx / (-z + 1) + width() / 2.);\n\t\t_y = qRound(ny / (-z + 1) + _availableHeight / 2.);\n\t}\n\tsnapXY();\n\tif (_opengl) {\n\t\t_geometryAnimation.start(\n\t\t\t[=] { update(); },\n\t\t\t0.,\n\t\t\t1.,\n\t\t\tst::widgetFadeDuration/*,\n\t\t\tanim::easeOutCirc*/);\n\t}\n\tupdate();\n}\n\nOverlayWidget::Entity OverlayWidget::entityForUserPhotos(int index) const {\n\tExpects(_userPhotosData.has_value());\n\tExpects(_session != nullptr);\n\n\tif (index < 0 || index >= _userPhotosData->size()) {\n\t\treturn { v::null, nullptr };\n\t}\n\tconst auto id = (*_userPhotosData)[index];\n\tif (const auto photo = _session->data().photo(id)) {\n\t\treturn { photo, nullptr };\n\t}\n\treturn { v::null, nullptr };\n}\n\nOverlayWidget::Entity OverlayWidget::entityForSharedMedia(int index) const {\n\tExpects(_sharedMediaData.has_value());\n\n\tif (index < 0 || index >= _sharedMediaData->size()) {\n\t\treturn { v::null, nullptr };\n\t}\n\tauto value = (*_sharedMediaData)[index];\n\tif (const auto photo = std::get_if<not_null<PhotoData*>>(&value)) {\n\t\t// Last peer photo.\n\t\treturn { *photo, nullptr };\n\t} else if (const auto itemId = std::get_if<FullMsgId>(&value)) {\n\t\treturn entityForItemId(*itemId);\n\t}\n\treturn { v::null, nullptr };\n}\n\nOverlayWidget::Entity OverlayWidget::entityForCollage(int index) const {\n\tExpects(_collageData.has_value());\n\tExpects(_session != nullptr);\n\n\tconst auto &items = _collageData->items;\n\tif (!_message || index < 0 || index >= items.size()) {\n\t\treturn { v::null, nullptr };\n\t}\n\tif (const auto document = std::get_if<DocumentData*>(&items[index])) {\n\t\treturn { *document, _message, _topicRootId, _monoforumPeerId };\n\t} else if (const auto photo = std::get_if<PhotoData*>(&items[index])) {\n\t\treturn { *photo, _message, _topicRootId, _monoforumPeerId };\n\t}\n\treturn { v::null, nullptr };\n}\n\nOverlayWidget::Entity OverlayWidget::entityForItemId(const FullMsgId &itemId) const {\n\tExpects(_session != nullptr);\n\n\tif (const auto item = _session->data().message(itemId)) {\n\t\tif (const auto media = item->media()) {\n\t\t\tif (const auto photo = media->photo()) {\n\t\t\t\treturn { photo, item, _topicRootId, _monoforumPeerId };\n\t\t\t} else if (const auto document = media->document()) {\n\t\t\t\treturn { document, item, _topicRootId, _monoforumPeerId };\n\t\t\t}\n\t\t}\n\t\treturn { v::null, item, _topicRootId, _monoforumPeerId };\n\t}\n\treturn { v::null, nullptr };\n}\n\nOverlayWidget::Entity OverlayWidget::entityByIndex(int index) const {\n\tif (_sharedMediaData) {\n\t\treturn entityForSharedMedia(index);\n\t} else if (_userPhotosData) {\n\t\treturn entityForUserPhotos(index);\n\t} else if (_collageData) {\n\t\treturn entityForCollage(index);\n\t}\n\treturn { v::null, nullptr };\n}\n\nvoid OverlayWidget::setContext(\n\tstd::variant<\n\t\tv::null_t,\n\t\tItemContext,\n\t\tnot_null<PeerData*>,\n\t\tStoriesContext> context) {\n\tif (const auto item = std::get_if<ItemContext>(&context)) {\n\t\t_message = item->item;\n\t\t_history = _message->history();\n\t\t_peer = _history->peer;\n\t\t_topicRootId = _peer->isForum() ? item->topicRootId : MsgId();\n\t\t_monoforumPeerId = _peer->amMonoforumAdmin()\n\t\t\t? item->monoforumPeerId\n\t\t\t: PeerId();\n\t\tsetStoriesPeer(nullptr);\n\t} else if (const auto peer = std::get_if<not_null<PeerData*>>(&context)) {\n\t\t_peer = *peer;\n\t\t_history = _peer->owner().history(_peer);\n\t\t_message = nullptr;\n\t\t_topicRootId = MsgId();\n\t\tsetStoriesPeer(nullptr);\n\t} else if (const auto story = std::get_if<StoriesContext>(&context)) {\n\t\t_message = nullptr;\n\t\t_topicRootId = MsgId();\n\t\t_history = nullptr;\n\t\t_peer = nullptr;\n\t\tsetStoriesPeer(story->peer);\n\t\tauto &stories = story->peer->owner().stories();\n\t\tconst auto maybeStory = stories.lookup(\n\t\t\t{ story->peer->id, story->id });\n\t\tif (maybeStory) {\n\t\t\t_stories->show(*maybeStory, story->within);\n\t\t\t_dropdown->raise();\n\t\t}\n\t} else {\n\t\t_message = nullptr;\n\t\t_topicRootId = MsgId();\n\t\t_history = nullptr;\n\t\t_peer = nullptr;\n\t\tsetStoriesPeer(nullptr);\n\t}\n\t_migrated = nullptr;\n\tif (_history) {\n\t\tif (_history->peer->migrateFrom()) {\n\t\t\t_migrated = _history->owner().history(\n\t\t\t\t_history->peer->migrateFrom());\n\t\t} else if (_history->peer->migrateTo()) {\n\t\t\t_migrated = _history;\n\t\t\t_history = _history->owner().history(_history->peer->migrateTo());\n\t\t}\n\t}\n\t_user = _peer ? _peer->asUser() : nullptr;\n}\n\nvoid OverlayWidget::setStoriesPeer(PeerData *peer) {\n\tconst auto session = peer ? &peer->session() : nullptr;\n\tif (!session && !_storiesSession) {\n\t\tAssert(!_stories);\n\t\tAssert(!_videoStream);\n\t} else if (!peer) {\n\t\t_videoStream = nullptr;\n\t\t_stories = nullptr;\n\t\t_storiesSession = nullptr;\n\t\t_storiesChanged.fire({});\n\t\tupdateNavigationControlsGeometry();\n\t} else if (_storiesSession != session) {\n\t\t_videoStream = nullptr;\n\t\t_stories = nullptr;\n\t\t_storiesSession = session;\n\t\tconst auto delegate = static_cast<Stories::Delegate*>(this);\n\t\t_stories = std::make_unique<Stories::View>(delegate);\n\t\t_stories->finalShownGeometryValue(\n\t\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\t\tupdateControlsGeometry();\n\t\t}, _stories->lifetime());\n\t\t_storiesChanged.fire({});\n\t}\n}\n\nvoid OverlayWidget::setSession(not_null<Main::Session*> session) {\n\tif (_session == session) {\n\t\treturn;\n\t}\n\n\tclearSession();\n\t_session = session;\n\t_window->setWindowIcon(Window::CreateIcon(session));\n\n\tsession->downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tif (!isHidden()) {\n\t\t\tupdateControls();\n\t\t\tcheckForSaveLoaded();\n\t\t}\n\t}, _sessionLifetime);\n\n\tsession->data().documentLoadProgress(\n\t) | rpl::filter([=] {\n\t\treturn !isHidden();\n\t}) | rpl::on_next([=](not_null<DocumentData*> document) {\n\t\tdocumentUpdated(document);\n\t}, _sessionLifetime);\n\n\tsession->data().itemIdChanged(\n\t) | rpl::on_next([=](const Data::Session::IdChange &change) {\n\t\tchangingMsgId(change.newId, change.oldId);\n\t}, _sessionLifetime);\n\n\tsession->data().itemRemoved(\n\t) | rpl::filter([=](not_null<const HistoryItem*> item) {\n\t\treturn (_message == item);\n\t}) | rpl::on_next([=] {\n\t\tclose();\n\t\tclearSession();\n\t}, _sessionLifetime);\n\n\tsession->account().sessionChanges(\n\t) | rpl::on_next([=] {\n\t\tclearSession();\n\t}, _sessionLifetime);\n}\n\nbool OverlayWidget::moveToNext(int delta) {\n\tif (_stories) {\n\t\treturn _stories->subjumpFor(delta);\n\t} else if (!_index) {\n\t\treturn false;\n\t}\n\tauto newIndex = *_index + delta;\n\treturn moveToEntity(entityByIndex(newIndex), delta);\n}\n\nbool OverlayWidget::moveToEntity(const Entity &entity, int preloadDelta) {\n\tif (v::is_null(entity.data) && !entity.item) {\n\t\treturn false;\n\t}\n\tif (const auto item = entity.item) {\n\t\tsetContext(ItemContext{ item, entity.topicRootId });\n\t} else if (_peer) {\n\t\tsetContext(_peer);\n\t} else {\n\t\tsetContext(v::null);\n\t}\n\tclearStreaming();\n\t_streamingStartPaused = false;\n\tif (auto photo = std::get_if<not_null<PhotoData*>>(&entity.data)) {\n\t\tdisplayPhoto(*photo);\n\t} else if (auto document = std::get_if<not_null<DocumentData*>>(&entity.data)) {\n\t\tdisplayDocument(*document);\n\t} else {\n\t\tdisplayDocument(nullptr);\n\t}\n\tpreloadData(preloadDelta);\n\treturn true;\n}\n\nvoid OverlayWidget::preloadData(int delta) {\n\tif (!_index) {\n\t\treturn;\n\t}\n\tauto from = *_index + (delta ? -delta : -1);\n\tauto till = *_index + (delta ? delta * kPreloadCount : 1);\n\tif (from > till) std::swap(from, till);\n\n\tauto photos = base::flat_set<std::shared_ptr<Data::PhotoMedia>>();\n\tauto documents = base::flat_set<std::shared_ptr<Data::DocumentMedia>>();\n\tfor (auto index = from; index != till + 1; ++index) {\n\t\tauto entity = entityByIndex(index);\n\t\tif (auto photo = std::get_if<not_null<PhotoData*>>(&entity.data)) {\n\t\t\tconst auto &[i, ok] = photos.emplace((*photo)->createMediaView());\n\t\t\t(*i)->wanted(Data::PhotoSize::Small, fileOrigin(entity));\n\t\t\t(*photo)->load(fileOrigin(entity), LoadFromCloudOrLocal, true);\n\t\t} else if (auto document = std::get_if<not_null<DocumentData*>>(\n\t\t\t\t&entity.data)) {\n\t\t\tconst auto &[i, ok] = documents.emplace(\n\t\t\t\t(*document)->createMediaView());\n\t\t\t(*i)->thumbnailWanted(fileOrigin(entity));\n\t\t\tif (!(*i)->canBePlayed()) {\n\t\t\t\t(*i)->automaticLoad(fileOrigin(entity), entity.item);\n\t\t\t}\n\t\t}\n\t}\n\t_preloadPhotos = std::move(photos);\n\t_preloadDocuments = std::move(documents);\n}\n\nvoid OverlayWidget::handleMousePress(\n\t\tQPoint position,\n\t\tQt::MouseButton button) {\n\tupdateOver(position);\n\tif (_menu || !_receiveMouse) {\n\t\treturn;\n\t}\n\n\tClickHandler::pressed();\n\n\tif (button == Qt::LeftButton) {\n\t\t_down = Over::None;\n\t\tif (!ClickHandler::getPressed()) {\n\t\t\tif ((_over == Over::Left && moveToNext(-1))\n\t\t\t\t|| (_over == Over::Right && moveToNext(1))\n\t\t\t\t|| (_stories\n\t\t\t\t\t&& _over == Over::LeftStories\n\t\t\t\t\t&& _stories->jumpFor(-1))\n\t\t\t\t|| (_stories\n\t\t\t\t\t&& _over == Over::RightStories\n\t\t\t\t\t&& _stories->jumpFor(1))) {\n\t\t\t\t_lastAction = position;\n\t\t\t} else if (_over == Over::Name\n\t\t\t\t|| _over == Over::Date\n\t\t\t\t|| _over == Over::Header\n\t\t\t\t|| _over == Over::Save\n\t\t\t\t|| _over == Over::Share\n\t\t\t\t|| _over == Over::Rotate\n\t\t\t\t|| _over == Over::Draw\n\t\t\t\t|| _over == Over::Recognize\n\t\t\t\t|| _over == Over::Icon\n\t\t\t\t|| _over == Over::More\n\t\t\t\t|| _over == Over::Video) {\n\t\t\t\t_down = _over;\n\t\t\t\tif (_over == Over::Video && _stories) {\n\t\t\t\t\t_stories->contentPressed(true);\n\t\t\t\t} else if (_over == Over::Video\n\t\t\t\t\t&& _streamed\n\t\t\t\t\t&& !_stories) {\n\t\t\t\t\t_speedBoostFromMouse = true;\n\t\t\t\t\t_speedBoostDragAccum = position.x();\n\t\t\t\t\t_speedBoostHoldTimer.callOnce(\n\t\t\t\t\t\tst::mediaviewSpeedBoostHoldDelay);\n\t\t\t\t}\n\t\t\t} else if (!_saveMsg.contains(position) || !isSaveMsgShown()) {\n\t\t\t\t_pressed = true;\n\t\t\t\t_dragging = 0;\n\t\t\t\tupdateCursor();\n\t\t\t\t_mStart = position;\n\t\t\t\t_xStart = _x;\n\t\t\t\t_yStart = _y;\n\t\t\t}\n\t\t}\n\t} else if (button == Qt::MiddleButton) {\n\t\tzoomReset();\n\t}\n\tactivateControls();\n}\n\nbool OverlayWidget::handleDoubleClick(\n\t\tQPoint position,\n\t\tQt::MouseButton button) {\n\tupdateOver(position);\n\n\tif (_over != Over::Video || button != Qt::LeftButton) {\n\t\treturn false;\n\t}\n\t_speedBoostHoldTimer.cancel();\n\t_speedBoostFromMouse = false;\n\tif (_speedBoostActive) {\n\t\tstopSpeedBoost();\n\t}\n\tif (_stories) {\n\t\tif (ClickHandler::getActive()) {\n\t\t\treturn false;\n\t\t}\n\t\ttoggleFullScreen(_windowed);\n\t} else if (!_streamed) {\n\t\treturn false;\n\t} else {\n\t\tplaybackToggleFullScreen();\n\t\tplaybackPauseResume();\n\t}\n\treturn true;\n}\n\nvoid OverlayWidget::snapXY() {\n\tauto xmin = width() - _w, xmax = 0;\n\tauto ymin = height() - _h, ymax = _minUsedTop;\n\taccumulate_min(xmin, (width() - _w) / 2);\n\taccumulate_max(xmax, (width() - _w) / 2);\n\tconst auto centerY = _fullScreenVideo\n\t\t? (_minUsedTop + (_maxUsedHeight - _h) / 2)\n\t\t: (_skipTop + (_availableHeight - _h) / 2);\n\taccumulate_min(ymin, centerY);\n\taccumulate_max(ymax, centerY);\n\taccumulate_max(_x, xmin);\n\taccumulate_min(_x, xmax);\n\taccumulate_max(_y, ymin);\n\taccumulate_min(_y, ymax);\n}\n\nauto OverlayWidget::scaledRecognitionRect(QPoint position)\nconst -> std::optional<Platform::TextRecognition::RectWithText> {\n\tauto contentRect = finalContentRect();\n\tif (_rotation) {\n\t\tauto transform = QTransform();\n\t\tconst auto center = rect::center(contentRect);\n\t\ttransform.translate(center.x(), center.y());\n\t\ttransform.rotate(-_rotation);\n\t\ttransform.translate(-center.x(), -center.y());\n\t\tcontentRect = transform.mapRect(contentRect);\n\t\tposition = transform.map(position);\n\t}\n\tconst auto imageSize = _staticContent.size()\n\t\t/ style::DevicePixelRatio();\n\tif (imageSize.isEmpty() || !contentRect.contains(position)) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto scale = contentRect.width() / float64(imageSize.width());\n\tfor (const auto &item : _recognitionResult.items) {\n\t\tconst auto &rect = item.rect;\n\t\tconst auto scaledRect = QRect(\n\t\t\tcontentRect.x() + int(rect.x() * scale),\n\t\t\tcontentRect.y() + int(rect.y() * scale),\n\t\t\tint(rect.width() * scale),\n\t\t\tint(rect.height() * scale));\n\t\tif (scaledRect.contains(position)) {\n\t\t\treturn Platform::TextRecognition::RectWithText{\n\t\t\t\t.text = item.text,\n\t\t\t\t.rect = scaledRect,\n\t\t\t};\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nvoid OverlayWidget::handleMouseMove(QPoint position) {\n\tif (_speedBoostFromMouse && !_speedBoostActive) {\n\t\tif (_speedBoostHoldTimer.isActive()) {\n\t\t\t_speedBoostDragAccum = position.x();\n\t\t}\n\t} else if (_speedBoostActive && _speedBoostFromMouse) {\n\t\tconst auto delta = position.x() - _speedBoostDragAccum;\n\t\t_speedBoostDragAccum = position.x();\n\t\tconst auto perUnit = float64(st::mediaviewSpeedBoostDragPerUnit);\n\t\tif (perUnit > 0.) {\n\t\t\tconst auto speed = std::clamp(\n\t\t\t\t_speedBoostSpeed + delta / perUnit,\n\t\t\t\tkSpeedMin,\n\t\t\t\tkSpeedMax);\n\t\t\tif (_speedBoostSpeed != speed) {\n\t\t\t\t_speedBoostSpeed = speed;\n\t\t\t\tif (_streamed) {\n\t\t\t\t\t_streamed->instance.setSpeed(speed);\n\t\t\t\t}\n\t\t\t\tupdateSpeedBoost();\n\t\t\t}\n\t\t}\n\t}\n\tupdateOver(position);\n\tif (_lastAction.x() >= 0\n\t\t&& ((position - _lastAction).manhattanLength()\n\t\t\t>= st::mediaviewDeltaFromLastAction)) {\n\t\t_lastAction = QPoint(-st::mediaviewDeltaFromLastAction, -st::mediaviewDeltaFromLastAction);\n\t}\n\tif (_recognitionResult.success\n\t\t&& !_recognitionResult.items.empty()\n\t\t&& _showRecognitionResults) {\n\t\tif (scaledRecognitionRect(position)) {\n\t\t\tsetCursor(style::cur_pointer);\n\t\t} else if (!_pressed) {\n\t\t\tsetCursor(style::cur_default);\n\t\t}\n\t}\n\tif (_pressed) {\n\t\tif (!_dragging\n\t\t\t&& ((position - _mStart).manhattanLength()\n\t\t\t\t>= QApplication::startDragDistance())) {\n\t\t\t_dragging = QRect(_x, _y, _w, _h).contains(_mStart) ? 1 : -1;\n\t\t\tif (_dragging > 0) {\n\t\t\t\tif (_w > width() || _h > _maxUsedHeight) {\n\t\t\t\t\tsetCursor(style::cur_sizeall);\n\t\t\t\t} else {\n\t\t\t\t\tsetCursor(style::cur_default);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (_dragging > 0) {\n\t\t\t_x = _xStart + (position - _mStart).x();\n\t\t\t_y = _yStart + (position - _mStart).y();\n\t\t\tsnapXY();\n\t\t\tupdate();\n\t\t}\n\t}\n}\n\nvoid OverlayWidget::updateOverRect(Over state) {\n\tusing Type = Stories::SiblingType;\n\tswitch (state) {\n\tcase Over::Left:\n\t\tupdate(_stories ? _leftNavIcon : _leftNavOver);\n\t\tbreak;\n\tcase Over::Right:\n\t\tupdate(_stories ? _rightNavIcon : _rightNavOver);\n\t\tbreak;\n\tcase Over::LeftStories:\n\t\tupdate(_stories\n\t\t\t? _stories->sibling(Type::Left).layout.geometry\n\t\t\t: QRect());\n\t\tbreak;\n\tcase Over::RightStories:\n\t\tupdate(_stories\n\t\t\t? _stories->sibling(Type::Right).layout.geometry\n\t\t\t: QRect());\n\t\tbreak;\n\tcase Over::Name: update(_nameNav); break;\n\tcase Over::Date: update(_dateNav); break;\n\tcase Over::Save: update(_saveNavOver); break;\n\tcase Over::Share: update(_shareNavOver); break;\n\tcase Over::Rotate: update(_rotateNavOver); break;\n\tcase Over::Draw: update(_drawNavOver); break;\n\tcase Over::Icon: update(_docIconRect); break;\n\tcase Over::Header: update(_headerNav); break;\n\tcase Over::More: update(_moreNavOver); break;\n\tcase Over::Recognize: update(_recognizeNavOver); break;\n\t}\n}\n\nbool OverlayWidget::updateOverState(Over newState) {\n\tbool result = true;\n\tif (_over != newState) {\n\t\tif (!_stories && newState == Over::More && !_ignoringDropdown) {\n\t\t\t_dropdownShowTimer.callOnce(0);\n\t\t} else {\n\t\t\t_dropdownShowTimer.cancel();\n\t\t}\n\t\tupdateOverRect(_over);\n\t\tupdateOverRect(newState);\n\t\tif (_over != Over::None) {\n\t\t\t_animations[_over] = crl::now();\n\t\t\tconst auto i = _animationOpacities.find(_over);\n\t\t\tif (i != end(_animationOpacities)) {\n\t\t\t\ti->second.start(0);\n\t\t\t} else {\n\t\t\t\t_animationOpacities.emplace(_over, anim::value(1, 0));\n\t\t\t}\n\t\t\tif (!_stateAnimation.animating()) {\n\t\t\t\t_stateAnimation.start();\n\t\t\t}\n\t\t} else {\n\t\t\tresult = false;\n\t\t}\n\t\t_over = newState;\n\t\tif (newState != Over::None) {\n\t\t\t_animations[_over] = crl::now();\n\t\t\tconst auto i = _animationOpacities.find(_over);\n\t\t\tif (i != end(_animationOpacities)) {\n\t\t\t\ti->second.start(1);\n\t\t\t} else {\n\t\t\t\t_animationOpacities.emplace(_over, anim::value(0, 1));\n\t\t\t}\n\t\t\tif (!_stateAnimation.animating()) {\n\t\t\t\t_stateAnimation.start();\n\t\t\t}\n\t\t}\n\t\tupdateCursor();\n\t}\n\treturn result;\n}\n\nvoid OverlayWidget::updateOver(QPoint pos) {\n\tClickHandlerPtr lnk;\n\tClickHandlerHost *lnkhost = nullptr;\n\tif (isSaveMsgShown() && _saveMsg.contains(pos)) {\n\t\tauto textState = _saveMsgText.getState(pos - _saveMsg.topLeft() - QPoint(st::mediaviewSaveMsgPadding.left(), st::mediaviewSaveMsgPadding.top()), _saveMsg.width() - st::mediaviewSaveMsgPadding.left() - st::mediaviewSaveMsgPadding.right());\n\t\tlnk = textState.link;\n\t\tlnkhost = this;\n\t} else if (_captionRect.contains(pos) && !_fullScreenVideo) {\n\t\tauto request = Ui::Text::StateRequestElided();\n\t\tconst auto lineHeight = st::mediaviewCaptionStyle.font->height;\n\t\trequest.lines = _captionRect.height() / lineHeight;\n\t\trequest.removeFromEnd = _captionSkipBlockWidth;\n\t\tauto textState = _caption.getStateElided(pos - _captionRect.topLeft(), _captionRect.width(), request);\n\t\tlnk = textState.link;\n\t\tif (_stories && !lnk) {\n\t\t\tlnk = ensureCaptionExpandLink();\n\t\t}\n\t\tlnkhost = this;\n\t} else if (_stories && captionGeometry().contains(pos)) {\n\t\tconst auto padding = st::mediaviewCaptionPadding;\n\t\tconst auto handler = _stories->lookupRepostHandler(\n\t\t\tpos - captionGeometry().marginsRemoved(padding).topLeft());\n\t\tif (handler) {\n\t\t\tlnk = handler.link;\n\t\t\tlnkhost = handler.host;\n\t\t\tsetCursor(style::cur_pointer);\n\t\t\t_cursorOverriden = true;\n\t\t}\n\t} else if (_groupThumbs && _groupThumbsRect.contains(pos)) {\n\t\tconst auto point = pos - QPoint(_groupThumbsLeft, _groupThumbsTop);\n\t\tlnk = _groupThumbs->getState(point);\n\t\tlnkhost = this;\n\t} else if (_stories) {\n\t\tlnk = _stories->lookupAreaHandler(pos);\n\t\tlnkhost = this;\n\t}\n\n\t// retina\n\tif (pos.x() == width()) {\n\t\tpos.setX(pos.x() - 1);\n\t}\n\tif (pos.y() == height()) {\n\t\tpos.setY(pos.y() - 1);\n\t}\n\n\tif (_cursorOverriden && (!lnkhost || lnkhost == this)) {\n\t\t_cursorOverriden = false;\n\t\tsetCursor(style::cur_default);\n\t}\n\tClickHandler::setActive(lnk, lnkhost);\n\n\tif (_pressed || _dragging) return;\n\n\tusing SiblingType = Stories::SiblingType;\n\tif (_fullScreenVideo) {\n\t\tupdateOverState(Over::Video);\n\t} else if (_leftNavVisible && _leftNav.contains(pos)) {\n\t\tupdateOverState(Over::Left);\n\t} else if (_rightNavVisible && _rightNav.contains(pos)) {\n\t\tupdateOverState(Over::Right);\n\t} else if (_stories\n\t\t&& _stories->sibling(\n\t\t\tSiblingType::Left).layout.geometry.contains(pos)) {\n\t\tupdateOverState(Over::LeftStories);\n\t} else if (_stories\n\t\t&& _stories->sibling(\n\t\t\tSiblingType::Right).layout.geometry.contains(pos)) {\n\t\tupdateOverState(Over::RightStories);\n\t} else if (!_stories && _from && _nameNav.contains(pos)) {\n\t\tupdateOverState(Over::Name);\n\t} else if (!_stories\n\t\t&& _message\n\t\t&& _message->isRegular()\n\t\t&& _dateNav.contains(pos)) {\n\t\tupdateOverState(Over::Date);\n\t} else if (!_stories && _headerHasLink && _headerNav.contains(pos)) {\n\t\tupdateOverState(Over::Header);\n\t} else if (_saveVisible && _saveNav.contains(pos)) {\n\t\tupdateOverState(Over::Save);\n\t} else if (_shareVisible && _shareNav.contains(pos)) {\n\t\tupdateOverState(Over::Share);\n\t} else if (_rotateVisible && _rotateNav.contains(pos)) {\n\t\tupdateOverState(Over::Rotate);\n\t} else if (_document\n\t\t&& documentBubbleShown()\n\t\t&& _docIconRect.contains(pos)) {\n\t\tupdateOverState(Over::Icon);\n\t} else if (_moreNav.contains(pos)) {\n\t\tupdateOverState(Over::More);\n\t} else if (_drawVisible && _drawNav.contains(pos)) {\n\t\tupdateOverState(Over::Draw);\n\t} else if (_recognizeVisible && _recognizeNav.contains(pos)) {\n\t\tupdateOverState(Over::Recognize);\n\t} else if (contentShown() && finalContentRect().contains(pos)) {\n\t\tif (_stories) {\n\t\t\tupdateOverState(Over::Video);\n\t\t} else if (_streamed\n\t\t\t&& _document\n\t\t\t&& (_document->isVideoFile() || _document->isVideoMessage())) {\n\t\t\tupdateOverState(Over::Video);\n\t\t} else if (!_streamed && _document && !_documentMedia->loaded()) {\n\t\t\tupdateOverState(Over::Icon);\n\t\t} else if (_over != Over::None) {\n\t\t\tupdateOverState(Over::None);\n\t\t}\n\t} else if (_over != Over::None) {\n\t\tupdateOverState(Over::None);\n\t}\n}\n\nClickHandlerPtr OverlayWidget::ensureCaptionExpandLink() {\n\tif (!_captionExpandLink) {\n\t\tconst auto toggle = crl::guard(_widget, [=] {\n\t\t\tif (_stories) {\n\t\t\t\t_stories->showFullCaption();\n\t\t\t}\n\t\t});\n\t\t_captionExpandLink = std::make_shared<LambdaClickHandler>(toggle);\n\t}\n\treturn _captionExpandLink;\n}\n\nvoid OverlayWidget::handleMouseRelease(\n\t\tQPoint position,\n\t\tQt::MouseButton button) {\n\tupdateOver(position);\n\n\tif (const auto activated = ClickHandler::unpressed()) {\n\t\tif (activated->url() == u\"internal:show_saved_message\"_q) {\n\t\t\tshowSaveMsgFile();\n\t\t\treturn;\n\t\t}\n\t\t// There may be a mention / hashtag / bot command link.\n\t\t// For now activate account for all activated links.\n\t\t// findWindow() will activate account.\n\t\tActivateClickHandler(_widget, activated, {\n\t\t\tbutton,\n\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.itemId = _message ? _message->fullId() : FullMsgId(),\n\t\t\t\t.sessionWindow = base::make_weak(findWindow()),\n\t\t\t\t.show = _stories ? _stories->uiShow() : uiShow(),\n\t\t\t\t.dark = true,\n\t\t\t})\n\t\t});\n\t\treturn;\n\t}\n\n\tif (_recognitionResult.success\n\t\t&& !_dragging\n\t\t&& !_recognitionResult.items.empty()\n\t\t&& _showRecognitionResults\n\t\t&& button == Qt::LeftButton) {\n\t\tif (const auto result = scaledRecognitionRect(position)) {\n\t\t\tQGuiApplication::clipboard()->setText(result->text);\n\t\t\tshowSaveMsgToastWith(\n\t\t\t\tQString(),\n\t\t\t\t{ tr::lng_text_copied(tr::now) },\n\t\t\t\t1000);\n\t\t\t_over = _down = Over::None;\n\t\t\t_pressed = false;\n\t\t\t_dragging = 0;\n\t\t\treturn;\n\t\t}\n\t}\n\n\tif (_over == Over::Name && _down == Over::Name) {\n\t\tif (_from) {\n\t\t\tif (!_windowed) {\n\t\t\t\tclose();\n\t\t\t}\n\t\t\tif (const auto window = findWindow(true)) {\n\t\t\t\twindow->showPeerInfo(_from);\n\t\t\t\twindow->window().activate();\n\t\t\t}\n\t\t}\n\t} else if (_over == Over::Date && _down == Over::Date) {\n\t\ttoMessage();\n\t} else if (_over == Over::Header && _down == Over::Header) {\n\t\tshowMediaOverview();\n\t} else if (_over == Over::Save && _down == Over::Save) {\n\t\tdownloadMedia();\n\t} else if (_over == Over::Share && _down == Over::Share && _stories) {\n\t\t_stories->shareRequested();\n\t} else if (_over == Over::Rotate && _down == Over::Rotate) {\n\t\tplaybackControlsRotate();\n\t} else if (_over == Over::Icon && _down == Over::Icon) {\n\t\thandleDocumentClick();\n\t} else if (_over == Over::More && _down == Over::More) {\n\t\tInvokeQueued(_widget, [=] { showDropdown(); });\n\t} else if (_over == Over::Draw && _down == Over::Draw) {\n\t\tdraw();\n\t} else if (_over == Over::Recognize && _down == Over::Recognize) {\n\t\trecognize();\n\t} else if (_down == Over::Video) {\n\t\tif (_stories) {\n\t\t\t_stories->contentPressed(false);\n\t\t} else {\n\t\t\t_speedBoostHoldTimer.cancel();\n\t\t\tif (_speedBoostActive && _speedBoostFromMouse) {\n\t\t\t\tstopSpeedBoost();\n\t\t\t} else {\n\t\t\t\t_speedBoostFromMouse = false;\n\t\t\t\tif (_over == Over::Video\n\t\t\t\t\t&& _streamed\n\t\t\t\t\t&& !_window->mousePressCancelled()) {\n\t\t\t\t\tif (_sponsoredButton && _session && _message) {\n\t\t\t\t\t\tconst auto sponsoredMessages\n\t\t\t\t\t\t\t= &_session->sponsoredMessages();\n\t\t\t\t\t\tconst auto fullId = _message->fullId();\n\t\t\t\t\t\tconst auto details = sponsoredMessages->lookupDetails(\n\t\t\t\t\t\t\tfullId);\n\t\t\t\t\t\tif (const auto link = details.link; !link.isEmpty()) {\n\t\t\t\t\t\t\tUrlClickHandler::Open(link);\n\t\t\t\t\t\t\tsponsoredMessages->clicked(fullId, true, true);\n\t\t\t\t\t\t\thide();\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tplaybackPauseResume();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if (_pressed) {\n\t\tif (_dragging) {\n\t\t\tif (_dragging > 0) {\n\t\t\t\t_x = _xStart + (position - _mStart).x();\n\t\t\t\t_y = _yStart + (position - _mStart).y();\n\t\t\t\tsnapXY();\n\t\t\t\tupdate();\n\t\t\t}\n\t\t\t_dragging = 0;\n\t\t\tsetCursor(style::cur_default);\n\t\t} else if (!_windowed\n\t\t\t&& position.y() > st::mediaviewTitleButton.height\n\t\t\t&& (position - _lastAction).manhattanLength()\n\t\t\t\t>= st::mediaviewDeltaFromLastAction) {\n\t\t\tif (_themePreviewShown) {\n\t\t\t\tif (!_themePreviewRect.contains(position)) {\n\t\t\t\t\tclose();\n\t\t\t\t}\n\t\t\t} else if (!_document\n\t\t\t\t|| documentContentShown()\n\t\t\t\t|| !documentBubbleShown()\n\t\t\t\t|| !_docRect.contains(position)) {\n\t\t\t\tif (!_stories || _stories->closeByClickAt(position)) {\n\t\t\t\t\tclose();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t_pressed = false;\n\t}\n\t_down = Over::None;\n\tif (!isHidden()) {\n\t\tactivateControls();\n\t}\n}\n\nbool OverlayWidget::handleContextMenu(std::optional<QPoint> position) {\n\tif (position) {\n\t\tif (!QRect(_x, _y, _w, _h).contains(*position)\n\t\t\t\t|| position->y() <= st::mediaviewTitleButton.height) {\n\t\t\treturn false;\n\t\t}\n\t}\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t_window,\n\t\tst::mediaviewPopupMenu);\n\tfillContextMenuActions(Ui::Menu::CreateAddActionCallback(_menu));\n\n\tif (_menu->empty()) {\n\t\t_menu = nullptr;\n\t\treturn true;\n\t}\n\tif (_stories) {\n\t\t_stories->menuShown(true);\n\t}\n\t_menu->setDestroyedCallback(crl::guard(_widget, [=] {\n\t\tif (_stories) {\n\t\t\t_stories->menuShown(false);\n\t\t}\n\t\tactivateControls();\n\t\t_receiveMouse = false;\n\t\tInvokeQueued(_widget, [=] { receiveMouse(); });\n\t}));\n\n\tusing HistoryView::Reactions::AttachSelectorResult;\n\tconst auto attached = _stories\n\t\t? _stories->attachReactionsToMenu(_menu.get(), QCursor::pos())\n\t\t: AttachSelectorResult::Skipped;\n\tif (attached == AttachSelectorResult::Failed) {\n\t\t_menu = nullptr;\n\t\treturn true;\n\t} else if (attached == AttachSelectorResult::Attached) {\n\t\t_menu->popupPrepared();\n\t} else {\n\t\t_menu->popup(QCursor::pos());\n\t}\n\tactivateControls();\n\treturn true;\n}\n\nbool OverlayWidget::handleTouchEvent(not_null<QTouchEvent*> e) {\n\tif (e->device()->type() != base::TouchDevice::TouchScreen) {\n\t\treturn false;\n\t} else if (e->type() == QEvent::TouchBegin\n\t\t&& !e->touchPoints().isEmpty()\n\t\t&& _widget->childAt(\n\t\t\t_widget->mapFromGlobal(\n\t\t\t\te->touchPoints().cbegin()->screenPos().toPoint()))) {\n\t\treturn false;\n\t}\n\tswitch (e->type()) {\n\tcase QEvent::TouchBegin: {\n\t\tif (_touchPress || e->touchPoints().isEmpty()) {\n\t\t\tbreak;\n\t\t}\n\t\t_touchTimer.callOnce(QApplication::startDragTime());\n\t\t_touchPress = true;\n\t\t_touchMove = _touchRightButton = false;\n\t\t_touchStart = e->touchPoints().cbegin()->screenPos().toPoint();\n\t\tactivateControls();\n\t} break;\n\n\tcase QEvent::TouchUpdate: {\n\t\tif (!_touchPress || e->touchPoints().isEmpty()) {\n\t\t\tbreak;\n\t\t}\n\t\tif (!_touchMove && (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart).manhattanLength() >= QApplication::startDragDistance()) {\n\t\t\t_touchMove = true;\n\t\t}\n\t\tactivateControls();\n\t} break;\n\n\tcase QEvent::TouchEnd: {\n\t\tif (!_touchPress) {\n\t\t\tbreak;\n\t\t}\n\t\tauto weak = base::make_weak(_widget);\n\t\tif (!_touchMove) {\n\t\t\tconst auto button = _touchRightButton\n\t\t\t\t? Qt::RightButton\n\t\t\t\t: Qt::LeftButton;\n\t\t\tconst auto position = _widget->mapFromGlobal(_touchStart);\n\n\t\t\tif (weak) handleMousePress(position, button);\n\t\t\tif (weak) handleMouseRelease(position, button);\n\t\t\tif (weak && _touchRightButton) {\n\t\t\t\thandleContextMenu(position);\n\t\t\t}\n\t\t} else if (_touchMove) {\n\t\t\tif ((!_leftNavVisible || !_leftNav.contains(_widget->mapFromGlobal(_touchStart))) && (!_rightNavVisible || !_rightNav.contains(_widget->mapFromGlobal(_touchStart)))) {\n\t\t\t\tQPoint d = (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart);\n\t\t\t\tif (d.x() * d.x() > d.y() * d.y() && (d.x() > st::mediaviewSwipeDistance || d.x() < -st::mediaviewSwipeDistance)) {\n\t\t\t\t\tmoveToNext(d.x() > 0 ? -1 : 1);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (weak) {\n\t\t\t_touchTimer.cancel();\n\t\t\t_touchPress = _touchMove = _touchRightButton = false;\n\t\t}\n\t} break;\n\n\tcase QEvent::TouchCancel: {\n\t\t_touchPress = false;\n\t\t_touchTimer.cancel();\n\t} break;\n\t}\n\te->accept();\n\treturn true;\n}\n\nvoid OverlayWidget::toggleApplicationEventFilter(bool install) {\n\tif (!install) {\n\t\t_applicationEventFilter = nullptr;\n\t\treturn;\n\t} else if (_applicationEventFilter) {\n\t\treturn;\n\t}\n\tclass Filter final : public QObject {\n\tpublic:\n\t\texplicit Filter(not_null<OverlayWidget*> owner) : _owner(owner) {\n\t\t}\n\n\tprivate:\n\t\tbool eventFilter(QObject *obj, QEvent *e) override {\n\t\t\treturn obj && e && _owner->filterApplicationEvent(obj, e);\n\t\t}\n\n\t\tconst not_null<OverlayWidget*> _owner;\n\n\t};\n\n\t_applicationEventFilter = std::make_unique<Filter>(this);\n\tqApp->installEventFilter(_applicationEventFilter.get());\n}\n\nbool OverlayWidget::filterApplicationEvent(\n\t\tnot_null<QObject*> object,\n\t\tnot_null<QEvent*> e) {\n\tconst auto type = e->type();\n\tif (type == QEvent::ShortcutOverride) {\n\t\tconst auto event = static_cast<QKeyEvent*>(e.get());\n\t\tconst auto key = event->key();\n\t\tconst auto ctrl = event->modifiers().testFlag(Qt::ControlModifier);\n\t\tif (key == Qt::Key_F && ctrl && _streamed) {\n\t\t\tplaybackToggleFullScreen();\n\t\t\treturn true;\n\t\t} else if (key == Qt::Key_0 && ctrl) {\n\t\t\tzoomReset();\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t} else if (type == QEvent::MouseMove\n\t\t|| type == QEvent::MouseButtonPress\n\t\t|| type == QEvent::MouseButtonRelease) {\n\t\tif (object->isWidgetType()\n\t\t\t&& static_cast<QWidget*>(object.get())->window() == _window) {\n\t\t\tconst auto mouseEvent = static_cast<QMouseEvent*>(e.get());\n\t\t\tconst auto mousePosition = _body->mapFromGlobal(\n\t\t\t\tmouseEvent->globalPos());\n\t\t\tconst auto delta = (mousePosition - _lastMouseMovePos);\n\t\t\tauto activate = delta.manhattanLength()\n\t\t\t\t>= st::mediaviewDeltaFromLastAction;\n\t\t\tif (activate) {\n\t\t\t\t_lastMouseMovePos = mousePosition;\n\t\t\t}\n\t\t\tif (type == QEvent::MouseButtonPress) {\n\t\t\t\t_mousePressed = true;\n\t\t\t\tactivate = true;\n\t\t\t} else if (type == QEvent::MouseButtonRelease) {\n\t\t\t\t_mousePressed = false;\n\t\t\t\tactivate = true;\n\t\t\t}\n\t\t\tif (activate) {\n\t\t\t\tactivateControls();\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid OverlayWidget::applyHideWindowWorkaround() {\n\t// QOpenGLWidget can't properly destroy a child widget if it is hidden\n\t// exactly after that, the child is cached in the backing store.\n\t// So on next paint we force full backing store repaint.\n\tif (!isHidden() && !_hideWorkaround) {\n\t\t_hideWorkaround = std::make_unique<Ui::RpWidget>(_window);\n\t\tconst auto raw = _hideWorkaround.get();\n\t\traw->setGeometry(_window->rect());\n\t\traw->show();\n\t\traw->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tif (_hideWorkaround.get() == raw) {\n\t\t\t\t_hideWorkaround.release();\n\t\t\t}\n\t\t\tQPainter(raw).fillRect(raw->rect(), QColor(0, 1, 0, 1));\n\t\t\tcrl::on_main(raw, [=] {\n\t\t\t\tdelete raw;\n\t\t\t});\n\t\t}, raw->lifetime());\n\t\traw->update();\n\t\t_widget->update();\n\n\t\tif (!Platform::IsMac()) {\n\t\t\tUi::ForceFullRepaintSync(_window);\n\t\t}\n\t\t_hideWorkaround = nullptr;\n\t}\n}\n\nWindow::SessionController *OverlayWidget::findWindow(bool switchTo) const {\n\tif (!_session) {\n\t\treturn nullptr;\n\t}\n\n\tconst auto window = _openedFrom.get();\n\tif (window) {\n\t\tif (const auto controller = window->sessionController()) {\n\t\t\tif (&controller->session() == _session) {\n\t\t\t\treturn controller;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (switchTo) {\n\t\tauto controllerPtr = (Window::SessionController*)nullptr;\n\t\tconst auto account = not_null(&_session->account());\n\t\tconst auto sessionWindow = Core::App().windowFor(account);\n\t\tconst auto anyWindow = (sessionWindow\n\t\t\t&& &sessionWindow->account() == account)\n\t\t\t? sessionWindow\n\t\t\t: window\n\t\t\t? window\n\t\t\t: sessionWindow;\n\t\tif (anyWindow) {\n\t\t\tanyWindow->invokeForSessionController(\n\t\t\t\t&_session->account(),\n\t\t\t\t_history ? _history->peer.get() : nullptr,\n\t\t\t\t[&](not_null<Window::SessionController*> newController) {\n\t\t\t\t\tcontrollerPtr = newController;\n\t\t\t\t});\n\t\t}\n\t\treturn controllerPtr;\n\t}\n\n\treturn nullptr;\n}\n\n// #TODO unite and check\nvoid OverlayWidget::clearBeforeHide() {\n\t_message = nullptr;\n\t_sharedMedia = nullptr;\n\t_sharedMediaData = std::nullopt;\n\t_sharedMediaDataKey = std::nullopt;\n\t_userPhotos = nullptr;\n\t_userPhotosData = std::nullopt;\n\t_collage = nullptr;\n\t_collageData = std::nullopt;\n\tclearStreaming();\n\tsetStoriesPeer(nullptr);\n\t_layerBg->hideAll(anim::type::instant);\n\tassignMediaPointer(nullptr);\n\t_preloadPhotos.clear();\n\t_preloadDocuments.clear();\n\tif (_menu) {\n\t\t_menu->hideMenu(true);\n\t}\n\t_chapterText = QString();\n\t_chapterRect = QRect();\n\t_chapterAnimation.stop();\n\t_chapterTimer.cancel();\n\t_chapterArrows.clear();\n\t_speedBoostActive = false;\n\t_speedBoostFromMouse = false;\n\t_speedBoostAnimation.stop();\n\t_speedBoostHoldTimer.cancel();\n\t_speedBoostTicker.stop();\n\t_controlsHideTimer.cancel();\n\t_controlsState = ControlsShown;\n\t_controlsOpacity = anim::value(1);\n\t_helper->setControlsOpacity(1.);\n\t_groupThumbs = nullptr;\n\t_groupThumbsRect = QRect();\n\t_sponsoredButton = nullptr;\n\t_voteButton.destroy();\n\t_pollVotersWidget.destroy();\n\t_pollUpdateLifetime.destroy();\n\t_showRecognitionResults = false;\n}\n\nvoid OverlayWidget::clearAfterHide() {\n\t_recognitionResult = {};\n\t_recognitionPendingSessionUniqueId = 0;\n\t_recognitionPendingPhotoId = 0;\n\t_recognitionRetryOnLarge = false;\n\t_body->hide();\n\tclearStreaming();\n\tdestroyThemePreview();\n\t_radial.stop();\n\t_staticContent = QImage();\n\t_themePreview = nullptr;\n\t_voteButton.destroyDelayed();\n\t_pollVotersWidget.destroyDelayed();\n\t_themeApply.destroyDelayed();\n\t_themeCancel.destroyDelayed();\n\t_themeShare.destroyDelayed();\n}\n\nvoid OverlayWidget::receiveMouse() {\n\t_receiveMouse = true;\n}\n\nvoid OverlayWidget::showDropdown() {\n\t_dropdown->clearActions();\n\tfillContextMenuActions(Ui::Menu::CreateAddActionCallback(_dropdown));\n\t_dropdown->moveToRight(0, height() - _dropdown->height());\n\t_dropdown->showAnimated(Ui::PanelAnimation::Origin::BottomRight);\n\t_dropdown->setFocus();\n\tif (_stories) {\n\t\t_stories->menuShown(true);\n\t}\n}\n\nvoid OverlayWidget::handleTouchTimer() {\n\t_touchRightButton = true;\n}\n\nvoid OverlayWidget::updateSaveMsg() {\n\tupdate(_saveMsg);\n}\n\nvoid OverlayWidget::findCurrent() {\n\tusing namespace rpl::mappers;\n\tif (_sharedMediaData) {\n\t\t_index = _message\n\t\t\t? _sharedMediaData->indexOf(_message->fullId())\n\t\t\t: _photo ? _sharedMediaData->indexOf(_photo) : std::nullopt;\n\t\t_fullIndex = _sharedMediaData->skippedBefore()\n\t\t\t? (_index | func::add(*_sharedMediaData->skippedBefore()))\n\t\t\t: std::nullopt;\n\t\t_fullCount = _sharedMediaData->fullCount();\n\t} else if (_userPhotosData) {\n\t\t_index = _photo ? _userPhotosData->indexOf(_photo->id) : std::nullopt;\n\t\t_fullIndex = _userPhotosData->skippedBefore()\n\t\t\t? (_index | func::add(*_userPhotosData->skippedBefore()))\n\t\t\t: std::nullopt;\n\t\t_fullCount = _userPhotosData->fullCount();\n\t} else if (_collageData) {\n\t\tconst auto item = _photo ? WebPageCollage::Item(_photo) : _document;\n\t\tconst auto &items = _collageData->items;\n\t\tconst auto i = ranges::find(items, item);\n\t\t_index = (i != end(items))\n\t\t\t? std::make_optional(int(i - begin(items)))\n\t\t\t: std::nullopt;\n\t\t_fullIndex = _index;\n\t\t_fullCount = items.size();\n\t} else {\n\t\t_index = _fullIndex = _fullCount = std::nullopt;\n\t}\n}\n\nvoid OverlayWidget::updateHeader() {\n\tauto index = _fullIndex ? *_fullIndex : -1;\n\tauto count = _fullCount ? *_fullCount : -1;\n\tif (index >= 0 && index < count && count > 1) {\n\t\tif (_document) {\n\t\t\t_headerText = tr::lng_mediaview_file_n_of_amount(\n\t\t\t\ttr::now,\n\t\t\t\tlt_file,\n\t\t\t\t(_document->filename().isEmpty()\n\t\t\t\t\t? tr::lng_mediaview_doc_image(tr::now)\n\t\t\t\t\t: _document->filename()),\n\t\t\t\tlt_n,\n\t\t\t\tQString::number(index + 1),\n\t\t\t\tlt_amount,\n\t\t\t\tQString::number(count));\n\t\t} else {\n\t\t\tif (_user\n\t\t\t\t&& (index == count - 1)\n\t\t\t\t&& _photo\n\t\t\t\t&& SyncUserFallbackPhotoViewer(_user) == _photo->id) {\n\t\t\t\t_headerText = tr::lng_mediaview_profile_public_photo(tr::now);\n\t\t\t} else if (_user\n\t\t\t\t&& _user->hasPersonalPhoto()\n\t\t\t\t&& _photo\n\t\t\t\t&& (_photo->id == _user->userpicPhotoId())) {\n\t\t\t\t_headerText = tr::lng_mediaview_profile_photo_by_you(tr::now);\n\t\t\t} else {\n\t\t\t\t_headerText = tr::lng_mediaview_n_of_amount(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_n,\n\t\t\t\t\tQString::number(index + 1),\n\t\t\t\t\tlt_amount,\n\t\t\t\t\tQString::number(count));\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (_document) {\n\t\t\t_headerText = _document->filename().isEmpty()\n\t\t\t\t? tr::lng_mediaview_doc_image(tr::now)\n\t\t\t\t: _document->filename();\n\t\t} else if (_message) {\n\t\t\t_headerText = tr::lng_mediaview_single_photo(tr::now);\n\t\t} else if (_user) {\n\t\t\tif (_user->hasPersonalPhoto()\n\t\t\t\t&& _photo\n\t\t\t\t&& (_photo->id == _user->userpicPhotoId())) {\n\t\t\t\t_headerText = tr::lng_mediaview_profile_photo_by_you(tr::now);\n\t\t\t} else {\n\t\t\t\t_headerText = tr::lng_mediaview_profile_photo(tr::now);\n\t\t\t}\n\t\t} else if ((_history && _history->peer->isBroadcast())\n\t\t\t|| (_peer && _peer->isChannel() && !_peer->isMegagroup())) {\n\t\t\t_headerText = tr::lng_mediaview_channel_photo(tr::now);\n\t\t} else if (_peer) {\n\t\t\t_headerText = tr::lng_mediaview_group_photo(tr::now);\n\t\t} else {\n\t\t\t_headerText = tr::lng_mediaview_single_photo(tr::now);\n\t\t}\n\t}\n\t_headerHasLink = computeOverviewType() != std::nullopt;\n\tauto hwidth = st::mediaviewThickFont->width(_headerText);\n\tif (hwidth > width() / 3) {\n\t\thwidth = width() / 3;\n\t\t_headerText = st::mediaviewThickFont->elided(_headerText, hwidth, Qt::ElideMiddle);\n\t}\n\t_headerNav = QRect(st::mediaviewTextLeft, height() - st::mediaviewHeaderTop, hwidth, st::mediaviewThickFont->height);\n}\n\nfloat64 OverlayWidget::overLevel(Over control) const {\n\tauto i = _animationOpacities.find(control);\n\treturn (i == end(_animationOpacities))\n\t\t? (_over == control ? 1. : 0.)\n\t\t: i->second.current();\n}\n\n} // namespace View\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_overlay_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/gl/gl_surface.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"data/data_shared_media.h\"\n#include \"data/data_user_photos.h\"\n#include \"data/data_web_page.h\"\n#include \"data/data_cloud_themes.h\" // Data::CloudTheme.\n#include \"media/stories/media_stories_delegate.h\"\n#include \"media/view/media_view_playback_controls.h\"\n#include \"media/view/media_view_open_common.h\"\n#include \"media/media_common.h\"\n#include \"platform/platform_text_recognition.h\"\n\nclass History;\nstruct PollAnswer;\n\nnamespace anim {\nenum class activation : uchar;\n} // namespace anim\n\nnamespace Calls {\nclass GroupCall;\n} // namespace Calls\n\nnamespace Data {\nclass GroupCall;\nclass PhotoMedia;\nclass DocumentMedia;\nstruct StoriesContext;\n} // namespace Data\n\nnamespace Ui {\nclass DropdownMenu;\nclass PopupMenu;\nclass LinkButton;\nclass RoundButton;\nclass RpWindow;\nclass LayerManager;\n} // namespace Ui\n\nnamespace Ui::GL {\nclass Window;\nstruct ChosenRenderer;\nenum class Backend;\n} // namespace Ui::GL\n\nnamespace Ui::Menu {\nstruct MenuCallback;\n} // namespace Ui::Menu\n\nnamespace Platform {\nclass OverlayWidgetHelper;\n} // namespace Platform\n\nnamespace Window::Theme {\nstruct Preview;\n} // namespace Window::Theme\n\nnamespace Media::Player {\nstruct TrackState;\n} // namespace Media::Player\n\nnamespace Media::Streaming {\nstruct Information;\nstruct Update;\nstruct FrameWithInfo;\nenum class Error;\n} // namespace Media::Streaming\n\nnamespace Media::Stories {\nclass View;\nstruct ContentLayout;\n} // namespace Media::Stories\n\nnamespace Media::View {\n\nclass VideoStream;\nclass PlaybackSponsored;\nclass GroupThumbs;\nclass Pip;\n\nclass OverlayWidget final\n\t: public ClickHandlerHost\n\t, private PlaybackControls::Delegate\n\t, private Stories::Delegate {\npublic:\n\tOverlayWidget();\n\t~OverlayWidget();\n\n\tenum class TouchBarItemType {\n\t\tPhoto,\n\t\tVideo,\n\t\tNone,\n\t};\n\n\t[[nodiscard]] bool isActive() const;\n\t[[nodiscard]] bool isHidden() const;\n\t[[nodiscard]] bool isMinimized() const;\n\t[[nodiscard]] bool isFullScreen() const;\n\t[[nodiscard]] not_null<QWidget*> widget() const;\n\tvoid hide();\n\tvoid setCursor(style::cursor cursor);\n\tvoid setFocus();\n\t[[nodiscard]] bool takeFocusFrom(not_null<QWidget*> window) const;\n\tvoid activate();\n\n\tvoid show(OpenRequest request);\n\n\tvoid activateControls();\n\tvoid close();\n\tvoid minimize();\n\tvoid toggleFullScreen();\n\tvoid toggleFullScreen(bool fullscreen);\n\n\tvoid notifyFileDialogShown(bool shown);\n\n\tvoid clearSession();\n\n\t// ClickHandlerHost interface\n\tvoid clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;\n\tvoid clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;\n\n\trpl::lifetime &lifetime();\n\nprivate:\n\tclass Show;\n\tstruct Streamed;\n\tstruct PipWrap;\n\tstruct ItemContext;\n\tstruct StoriesContext;\n\tclass Renderer;\n\tclass RendererSW;\n\tclass RendererGL;\n\tclass RendererRhi;\n\tclass SponsoredButton;\n\n\t// If changing, see paintControls()!\n\tenum class Over {\n\t\tNone,\n\t\tLeft,\n\t\tRight,\n\t\tLeftStories,\n\t\tRightStories,\n\t\tSponsoredButton,\n\t\tHeader,\n\t\tName,\n\t\tDate,\n\t\tSave,\n\t\tShare,\n\t\tRotate,\n\t\tMore,\n\t\tDraw,\n\t\tRecognize,\n\t\tIcon,\n\t\tVideo,\n\t\tCaption,\n\t};\n\tstruct Entity {\n\t\tstd::variant<\n\t\t\tv::null_t,\n\t\t\tnot_null<PhotoData*>,\n\t\t\tnot_null<DocumentData*>> data;\n\t\tHistoryItem *item = nullptr;\n\t\tMsgId topicRootId = 0;\n\t\tPeerId monoforumPeerId = 0;\n\t};\n\tenum class SavePhotoVideo {\n\t\tNone,\n\t\tQuickSave,\n\t\tSaveAs,\n\t};\n\tstruct ContentGeometry {\n\t\tQRectF rect;\n\t\tqreal rotation = 0.;\n\t\tqreal controlsOpacity = 0.;\n\n\t\t// Stories.\n\t\tqreal fade = 0.;\n\t\tqreal scale = 1.;\n\t\tint bottomShadowSkip = 0;\n\t\tint roundRadius = 0;\n\t\tbool topShadowShown = false;\n\t};\n\tstruct StartStreaming {\n\t\tStartStreaming() : continueStreaming(false), startTime(0) {\n\t\t}\n\t\tStartStreaming(bool continueStreaming, crl::time startTime)\n\t\t: continueStreaming(continueStreaming)\n\t\t, startTime(startTime) {\n\t\t}\n\t\tconst bool continueStreaming = false;\n\t\tconst crl::time startTime = 0;\n\t};\n\n\t[[nodiscard]] not_null<QWindow*> window() const;\n\t[[nodiscard]] int width() const;\n\t[[nodiscard]] int height() const;\n\tvoid update();\n\tvoid update(const QRegion &region);\n\n\t[[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer(\n\t\tUi::GL::Backend backend);\n\tvoid paint(not_null<Renderer*> renderer);\n\n\tvoid setupWindow();\n\tvoid orderWidgets();\n\tvoid showAndActivate();\n\tvoid handleMousePress(QPoint position, Qt::MouseButton button);\n\tvoid handleMouseRelease(QPoint position, Qt::MouseButton button);\n\tvoid handleMouseMove(QPoint position);\n\tbool handleContextMenu(std::optional<QPoint> position);\n\tbool handleDoubleClick(QPoint position, Qt::MouseButton button);\n\tbool handleTouchEvent(not_null<QTouchEvent*> e);\n\tvoid handleWheelEvent(not_null<QWheelEvent*> e);\n\tvoid handleKeyPress(not_null<QKeyEvent*> e);\n\tvoid handleKeyRelease(not_null<QKeyEvent*> e);\n\n\tvoid toggleApplicationEventFilter(bool install);\n\tbool filterApplicationEvent(\n\t\tnot_null<QObject*> object,\n\t\tnot_null<QEvent*> e);\n\tvoid setSession(not_null<Main::Session*> session);\n\n\tvoid playbackControlsPlay() override;\n\tvoid playbackControlsPause() override;\n\tvoid playbackControlsSeekProgress(crl::time position) override;\n\tvoid playbackControlsSeekFinished(crl::time position) override;\n\tvoid playbackControlsVolumeChanged(float64 volume) override;\n\tfloat64 playbackControlsCurrentVolume() override;\n\tvoid playbackControlsVolumeToggled() override;\n\tvoid playbackControlsVolumeChangeFinished() override;\n\tvoid playbackControlsSpeedChanged(float64 speed) override;\n\tfloat64 playbackControlsCurrentSpeed(bool lastNonDefault) override;\n\tstd::vector<int> playbackControlsQualities() override;\n\tVideoQuality playbackControlsCurrentQuality() override;\n\tvoid playbackControlsQualityChanged(int quality) override;\n\tvoid playbackControlsToFullScreen() override;\n\tvoid playbackControlsFromFullScreen() override;\n\tvoid playbackControlsToPictureInPicture() override;\n\tvoid playbackControlsRotate() override;\n\tvoid playbackPauseResume();\n\tvoid playbackToggleFullScreen();\n\tvoid playbackPauseOnCall();\n\tvoid playbackResumeOnCall();\n\tvoid playbackPauseMusic();\n\tvoid switchToPip();\n\t[[nodiscard]] int topNotchSkip() const;\n\t[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow();\n\n\tnot_null<Ui::RpWidget*> storiesWrap() override;\n\tstd::shared_ptr<ChatHelpers::Show> storiesShow() override;\n\tauto storiesStickerOrEmojiChosen()\n\t\t-> rpl::producer<ChatHelpers::FileChosen> override;\n\tvoid storiesRedisplay(not_null<Data::Story*> story) override;\n\tvoid storiesJumpTo(\n\t\tnot_null<Main::Session*> session,\n\t\tFullStoryId id,\n\t\tData::StoriesContext context) override;\n\tvoid storiesClose() override;\n\tbool storiesPaused() override;\n\trpl::producer<bool> storiesLayerShown() override;\n\tvoid storiesTogglePaused(bool paused) override;\n\tfloat64 storiesSiblingOver(Stories::SiblingType type) override;\n\tvoid storiesRepaint() override;\n\tvoid storiesVolumeToggle() override;\n\tvoid storiesVolumeChanged(float64 volume) override;\n\tvoid storiesVolumeChangeFinished() override;\n\tint storiesTopNotchSkip() override;\n\n\tvoid hideControls(bool force = false);\n\tvoid subscribeToScreenGeometry();\n\n\tvoid toMessage();\n\tvoid saveAs();\n\tvoid downloadMedia();\n\tvoid saveCancel();\n\tvoid showInFolder();\n\tvoid forwardMedia();\n\tvoid deleteMedia();\n\tvoid showMediaOverview();\n\tvoid copyMedia();\n\tvoid recognize();\n\tvoid draw();\n\tvoid receiveMouse();\n\tvoid showAttachedStickers();\n\t[[nodiscard]] auto scaledRecognitionRect(QPoint position)\n\tconst -> std::optional<Platform::TextRecognition::RectWithText>;\n\tvoid showDropdown();\n\tvoid handleTouchTimer();\n\tvoid handleDocumentClick();\n\n\t[[nodiscard]] bool canShareAtTime() const;\n\t[[nodiscard]] TimeId shareAtVideoTimestamp() const;\n\tvoid shareAtTime();\n\n\tvoid showSaveMsgToast(const QString &path, auto phrase);\n\tvoid showSaveMsgToastWith(\n\t\tconst QString &path,\n\t\tconst TextWithEntities &text,\n\t\tcrl::time duration = 0);\n\tvoid updateSaveMsg();\n\n\tvoid clearBeforeHide();\n\tvoid clearAfterHide();\n\n\tvoid assignMediaPointer(DocumentData *document);\n\tvoid assignMediaPointer(not_null<PhotoData*> photo);\n\tvoid assignMediaPointer(std::shared_ptr<Data::GroupCall> call);\n\n\tvoid updateOver(QPoint mpos);\n\tvoid initFullScreen();\n\tvoid initNormalGeometry();\n\tvoid savePosition();\n\tvoid moveToScreen(bool inMove = false);\n\tvoid updateGeometry(bool inMove = false);\n\tvoid updateGeometryToScreen(bool inMove = false);\n\tbool moveToNext(int delta);\n\tvoid preloadData(int delta);\n\n\tvoid handleScreenChanged(not_null<QScreen*> screen);\n\n\t[[nodiscard]] bool computeSaveButtonVisible() const;\n\tvoid checkForSaveLoaded();\n\tvoid showPremiumDownloadPromo();\n\n\t[[nodiscard]] Entity entityForUserPhotos(int index) const;\n\t[[nodiscard]] Entity entityForSharedMedia(int index) const;\n\t[[nodiscard]] Entity entityForCollage(int index) const;\n\t[[nodiscard]] Entity entityByIndex(int index) const;\n\t[[nodiscard]] Entity entityForItemId(const FullMsgId &itemId) const;\n\tbool moveToEntity(const Entity &entity, int preloadDelta = 0);\n\n\tvoid setContext(std::variant<\n\t\tv::null_t,\n\t\tItemContext,\n\t\tnot_null<PeerData*>,\n\t\tStoriesContext> context);\n\tvoid setStoriesPeer(PeerData *peer);\n\n\tvoid refreshLang();\n\tvoid showSaveMsgFile();\n\n\tstruct SharedMedia;\n\tusing SharedMediaType = SharedMediaWithLastSlice::Type;\n\tusing SharedMediaKey = SharedMediaWithLastSlice::Key;\n\t[[nodiscard]] std::optional<SharedMediaType> sharedMediaType() const;\n\t[[nodiscard]] std::optional<SharedMediaKey> sharedMediaKey() const;\n\t[[nodiscard]] std::optional<SharedMediaType> computeOverviewType() const;\n\tbool validSharedMedia() const;\n\tvoid validateSharedMedia();\n\tvoid handleSharedMediaUpdate(SharedMediaWithLastSlice &&update);\n\n\tstruct UserPhotos;\n\tusing UserPhotosKey = UserPhotosSlice::Key;\n\t[[nodiscard]] std::optional<UserPhotosKey> userPhotosKey() const;\n\tbool validUserPhotos() const;\n\tvoid validateUserPhotos();\n\tvoid handleUserPhotosUpdate(UserPhotosSlice &&update);\n\n\tstruct Collage;\n\tusing CollageKey = WebPageCollage::Item;\n\t[[nodiscard]] std::optional<CollageKey> collageKey() const;\n\tbool validCollage() const;\n\tvoid validateCollage();\n\n\t[[nodiscard]] Data::FileOrigin fileOrigin() const;\n\t[[nodiscard]] Data::FileOrigin fileOrigin(const Entity& entity) const;\n\n\tvoid refreshFromLabel();\n\tvoid refreshCaption();\n\tvoid refreshTimestampDividers(\n\t\tconst TextWithEntities &caption,\n\t\tTimeId duration);\n\tvoid refreshMediaViewer();\n\tvoid refreshNavVisibility();\n\tvoid refreshGroupThumbs();\n\n\tvoid dropdownHidden();\n\tvoid updateDocSize();\n\tvoid updateControls();\n\tvoid updateControlsGeometry();\n\tvoid updateNavigationControlsGeometry();\n\n\tvoid fillContextMenuActions(const Ui::Menu::MenuCallback &addAction);\n\n\tvoid resizeCenteredControls();\n\tvoid resizeContentByScreenSize();\n\tvoid recountSkipTop();\n\n\tvoid displayPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tanim::activation activation = anim::activation::normal);\n\tvoid displayDocument(\n\t\tDocumentData *document,\n\t\tanim::activation activation = anim::activation::normal,\n\t\tconst Data::CloudTheme &cloud = Data::CloudTheme(),\n\t\tconst StartStreaming &startStreaming = StartStreaming());\n\tvoid displayVideoStream(\n\t\tconst std::shared_ptr<Data::GroupCall> &call,\n\t\tanim::activation activation = anim::activation::normal);\n\tvoid displayFinished(anim::activation activation);\n\tvoid redisplayContent();\n\tvoid findCurrent();\n\n\tvoid updateCursor();\n\tvoid setZoomLevel(int newZoom, bool force = false);\n\n\tvoid updatePlaybackState();\n\tvoid seekRelativeTime(crl::time time);\n\tvoid restartAtProgress(float64 progress);\n\tvoid restartAtSeekPosition(crl::time position);\n\tvoid flushPendingFrameStep();\n\n\tvoid refreshClipControllerGeometry();\n\tvoid refreshCaptionGeometry();\n\n\tbool initStreaming(\n\t\tconst StartStreaming &startStreaming = StartStreaming());\n\tvoid startStreamingPlayer(const StartStreaming &startStreaming);\n\tvoid initStreamingThumbnail();\n\tvoid markStreamedReady();\n\tvoid streamingReady(Streaming::Information &&info);\n\t[[nodiscard]] bool createStreamingObjects();\n\tvoid handleStreamingUpdate(Streaming::Update &&update);\n\tvoid handleStreamingError(Streaming::Error &&error);\n\tvoid updatePowerSaveBlocker(const Player::TrackState &state);\n\n\tvoid initThemePreview();\n\tvoid destroyThemePreview();\n\tvoid updateThemePreviewGeometry();\n\n\tvoid initSponsoredButton();\n\tvoid refreshSponsoredButtonGeometry();\n\tvoid refreshSponsoredButtonWidth();\n\n\tvoid refreshVoteButton();\n\tvoid refreshVoteButtonGeometry();\n\tvoid refreshPollVotersWidget();\n\tvoid refreshPollVotersWidgetGeometry();\n\t[[nodiscard]] const PollAnswer *currentPollAnswer() const;\n\n\tvoid documentUpdated(not_null<DocumentData*> document);\n\tvoid changingMsgId(FullMsgId newId, MsgId oldId);\n\n\t[[nodiscard]] int finalContentRotation() const;\n\t[[nodiscard]] QRect finalContentRect() const;\n\t[[nodiscard]] ContentGeometry contentGeometry() const;\n\t[[nodiscard]] ContentGeometry storiesContentGeometry(\n\t\tconst Stories::ContentLayout &layout,\n\t\tfloat64 scale = 1.) const;\n\tvoid updateContentRect();\n\tvoid contentSizeChanged();\n\n\t// Radial animation interface.\n\t[[nodiscard]] float64 radialProgress() const;\n\t[[nodiscard]] bool radialLoading() const;\n\t[[nodiscard]] QRect radialRect() const;\n\tvoid radialStart();\n\t[[nodiscard]] crl::time radialTimeShift() const;\n\n\tvoid updateHeader();\n\tvoid snapXY();\n\n\tvoid clearControlsState();\n\tbool stateAnimationCallback(crl::time ms);\n\tbool radialAnimationCallback(crl::time now);\n\tvoid waitingAnimationCallback();\n\tbool updateControlsAnimation(crl::time now);\n\n\tvoid zoomIn();\n\tvoid zoomOut();\n\tvoid zoomReset();\n\tvoid zoomUpdate(int32 &newZoom);\n\n\tvoid paintRadialLoading(not_null<Renderer*> renderer);\n\tvoid paintRadialLoadingContent(\n\t\tPainter &p,\n\t\tQRect inner,\n\t\tbool radial,\n\t\tfloat64 radialOpacity) const;\n\tvoid paintThemePreviewContent(Painter &p, QRect outer, QRect clip);\n\tvoid paintDocumentBubbleContent(\n\t\tPainter &p,\n\t\tQRect outer,\n\t\tQRect icon,\n\t\tQRect clip) const;\n\tvoid paintSaveMsgContent(Painter &p, QRect outer, QRect clip);\n\tvoid paintControls(not_null<Renderer*> renderer, float64 opacity);\n\tvoid paintFooterContent(\n\t\tPainter &p,\n\t\tQRect outer,\n\t\tQRect clip,\n\t\tfloat64 opacity);\n\t[[nodiscard]] QRect footerGeometry() const;\n\tvoid paintCaptionContent(\n\t\tPainter &p,\n\t\tQRect outer,\n\t\tQRect clip,\n\t\tfloat64 opacity);\n\t[[nodiscard]] QRect captionGeometry() const;\n\tvoid paintGroupThumbsContent(\n\t\tPainter &p,\n\t\tQRect outer,\n\t\tQRect clip,\n\t\tfloat64 opacity);\n\n\t[[nodiscard]] float64 controlOpacity(\n\t\tfloat64 progress,\n\t\tbool nonbright = false) const;\n\t[[nodiscard]] bool isSaveMsgShown() const;\n\n\tvoid showChapterIndicator(const QString &name, int direction);\n\tvoid paintChapterContent(Painter &p, QRect outer, QRect clip);\n\t[[nodiscard]] bool isChapterShown() const;\n\tvoid updateChapter();\n\n\tvoid startSpeedBoost();\n\tvoid stopSpeedBoost();\n\tvoid updateSpeedBoostRect();\n\tvoid paintSpeedBoostContent(Painter &p, QRect outer, QRect clip);\n\t[[nodiscard]] bool isSpeedBoostShown() const;\n\tvoid updateSpeedBoost();\n\n\tvoid updateOverRect(Over state);\n\tbool updateOverState(Over newState);\n\tfloat64 overLevel(Over control) const;\n\n\tvoid checkGroupThumbsAnimation();\n\tvoid initGroupThumbs();\n\n\tvoid validatePhotoImage(Image *image, bool blurred);\n\tvoid validatePhotoCurrentImage();\n\tvoid tryStartTextRecognition();\n\n\t[[nodiscard]] bool hasCopyMediaRestriction(\n\t\tbool skipPremiumCheck = false) const;\n\t[[nodiscard]] bool showCopyMediaRestriction(\n\t\tbool skipPRemiumCheck = false);\n\n\t[[nodiscard]] QSize flipSizeByRotation(QSize size) const;\n\n\tvoid applyVideoSize();\n\t[[nodiscard]] bool videoShown() const;\n\t[[nodiscard]] QSize videoSize() const;\n\t[[nodiscard]] bool streamingRequiresControls() const;\n\t[[nodiscard]] QImage videoFrame() const; // ARGB (changes prepare format)\n\t[[nodiscard]] QImage currentVideoFrameImage() const; // RGB (may convert)\n\t[[nodiscard]] Streaming::FrameWithInfo videoFrameWithInfo() const; // YUV\n\t[[nodiscard]] int streamedIndex() const;\n\t[[nodiscard]] QImage transformedShownContent() const;\n\t[[nodiscard]] QImage transformShownContent(\n\t\tQImage content,\n\t\tint rotation) const;\n\t[[nodiscard]] bool documentContentShown() const;\n\t[[nodiscard]] bool documentBubbleShown() const;\n\tvoid setStaticContent(QImage image);\n\t[[nodiscard]] bool contentShown() const;\n\t[[nodiscard]] bool opaqueContentShown() const;\n\tvoid clearStreaming(bool savePosition = true);\n\t[[nodiscard]] bool canInitStreaming() const;\n\t[[nodiscard]] bool saveControlLocked() const;\n\tvoid applyVideoQuality(VideoQuality value);\n\n\t[[nodiscard]] bool topShadowOnTheRight() const;\n\tvoid applyHideWindowWorkaround();\n\t[[nodiscard]] ClickHandlerPtr ensureCaptionExpandLink();\n\n\tWindow::SessionController *findWindow(bool switchTo = true) const;\n\n\tbool _opengl = false;\n\tconst std::unique_ptr<Ui::GL::Window> _wrap;\n\tconst not_null<Ui::RpWindow*> _window;\n\tconst std::unique_ptr<Platform::OverlayWidgetHelper> _helper;\n\tconst not_null<Ui::RpWidget*> _body;\n\tconst std::unique_ptr<Ui::RpWidget> _titleBugWorkaround;\n\tconst std::unique_ptr<Ui::RpWidgetWrap> _surface;\n\tconst not_null<QWidget*> _widget;\n\tQRect _normalGeometry;\n\tbool _wasWindowedMode = false;\n\tbool _fullscreenInited = false;\n\tbool _normalGeometryInited = false;\n\tbool _fullscreen = true;\n\tbool _windowed = false;\n\n\tbase::weak_ptr<Window::Controller> _openedFrom;\n\tMain::Session *_session = nullptr;\n\trpl::lifetime _sessionLifetime;\n\tPhotoData *_photo = nullptr;\n\tDocumentData *_document = nullptr;\n\tDocumentData *_chosenQuality = nullptr;\n\tPhotoData *_videoCover = nullptr;\n\tMedia::VideoQuality _quality;\n\tQString _documentLoadingTo;\n\tstd::shared_ptr<Data::PhotoMedia> _photoMedia;\n\tstd::shared_ptr<Data::DocumentMedia> _documentMedia;\n\tstd::shared_ptr<Data::PhotoMedia> _videoCoverMedia;\n\tbase::flat_set<std::shared_ptr<Data::PhotoMedia>> _preloadPhotos;\n\tbase::flat_set<std::shared_ptr<Data::DocumentMedia>> _preloadDocuments;\n\tint _rotation = 0;\n\tstd::unique_ptr<SharedMedia> _sharedMedia;\n\tstd::optional<SharedMediaWithLastSlice> _sharedMediaData;\n\tstd::optional<SharedMediaWithLastSlice::Key> _sharedMediaDataKey;\n\tstd::unique_ptr<UserPhotos> _userPhotos;\n\tstd::optional<UserPhotosSlice> _userPhotosData;\n\tstd::unique_ptr<Collage> _collage;\n\tstd::optional<WebPageCollage> _collageData;\n\n\tQRect _leftNav, _leftNavOver, _leftNavIcon;\n\tQRect _rightNav, _rightNavOver, _rightNavIcon;\n\tQRect _headerNav, _nameNav, _dateNav, _separatorNav, _sizeNav;\n\tQRect _rotateNav, _rotateNavOver, _rotateNavIcon;\n\tQRect _shareNav, _shareNavOver, _shareNavIcon;\n\tQRect _drawNav, _drawNavOver, _drawNavIcon;\n\tQRect _recognizeNav, _recognizeNavOver, _recognizeNavIcon;\n\tQRect _saveNav, _saveNavOver, _saveNavIcon;\n\tQRect _moreNav, _moreNavOver, _moreNavIcon;\n\tbool _leftNavVisible = false;\n\tbool _rightNavVisible = false;\n\tbool _saveVisible = false;\n\tbool _shareVisible = false;\n\tbool _rotateVisible = false;\n\tbool _drawButtonEnabled = true;\n\tbool _drawVisible = false;\n\tbool _recognizeVisible = false;\n\tbool _headerHasLink = false;\n\tQString _dateText;\n\tQString _sizeText;\n\tQString _headerText;\n\n\tbool _streamingStartPaused = false;\n\tbool _fullScreenVideo = false;\n\tint _fullScreenZoomCache = 0;\n\tfloat64 _lastPositiveVolume = 1.;\n\n\tstd::unique_ptr<GroupThumbs> _groupThumbs;\n\tQRect _groupThumbsRect;\n\tint _groupThumbsAvailableWidth = 0;\n\tint _groupThumbsLeft = 0;\n\tint _groupThumbsTop = 0;\n\tUi::Text::String _caption;\n\tQRect _captionRect;\n\tClickHandlerPtr _captionExpandLink;\n\tint _captionShowMoreWidth = 0;\n\tint _captionSkipBlockWidth = 0;\n\n\tint _topNotchSize = 0;\n\tint _width = 0;\n\tint _height = 0;\n\tint _skipTop = 0;\n\tint _availableHeight = 0;\n\tint _minUsedTop = 0; // Geometry without top notch on macOS.\n\tint _maxUsedHeight = 0;\n\tint _x = 0, _y = 0, _w = 0, _h = 0;\n\tint _xStart = 0, _yStart = 0;\n\tint _zoom = 0; // < 0 - out, 0 - none, > 0 - in\n\tfloat64 _zoomToScreen = 0.; // for documents\n\tfloat64 _zoomToDefault = 0.;\n\tQPoint _mStart;\n\tbool _pressed = false;\n\tbool _cursorOverriden = false;\n\tint32 _dragging = 0;\n\tQImage _staticContent;\n\tbool _staticContentTransparent = false;\n\tbool _blurred = true;\n\tbool _reShow = false;\n\n\tContentGeometry _oldGeometry;\n\tUi::Animations::Simple _geometryAnimation;\n\trpl::lifetime _screenGeometryLifetime;\n\tstd::unique_ptr<QObject> _applicationEventFilter;\n\n\tstd::unique_ptr<Streamed> _streamed;\n\tstd::unique_ptr<PipWrap> _pip;\n\tQImage _streamedQualityChangeFrame;\n\tcrl::time _streamedPosition = 0;\n\tint _streamedCreated = 0;\n\tbool _streamedQualityChangeFinished = false;\n\tbool _showAsPip = false;\n\n\tQt::Orientations _flip;\n\n\tstd::unique_ptr<Stories::View> _stories;\n\tstd::shared_ptr<Show> _cachedShow;\n\trpl::event_stream<> _storiesChanged;\n\tMain::Session *_storiesSession = nullptr;\n\trpl::event_stream<ChatHelpers::FileChosen> _storiesStickerOrEmojiChosen;\n\tstd::unique_ptr<Ui::LayerManager> _layerBg;\n\n\tstd::unique_ptr<VideoStream> _videoStream;\n\tQString _callLinkSlug;\n\tMsgId _callJoinMessageId;\n\n\tconst style::icon *_docIcon = nullptr;\n\tstyle::color _docIconColor;\n\tQString _docName, _docSize, _docExt;\n\tint _docNameWidth = 0, _docSizeWidth = 0, _docExtWidth = 0;\n\tQRect _docRect, _docIconRect;\n\tQImage _docRectImage;\n\tint _docThumbx = 0, _docThumby = 0, _docThumbw = 0;\n\tobject_ptr<Ui::LinkButton> _docDownload;\n\tobject_ptr<Ui::LinkButton> _docSaveAs;\n\tobject_ptr<Ui::LinkButton> _docCancel;\n\n\tQRect _bottomShadowRect;\n\tQRect _topShadowRect;\n\trpl::variable<bool> _topShadowRight = false;\n\n\tQRect _photoRadialRect;\n\tUi::RadialAnimation _radial;\n\n\tHistory *_migrated = nullptr;\n\tHistory *_history = nullptr; // if conversation photos or files overview\n\tMsgId _topicRootId = 0;\n\tPeerId _monoforumPeerId = 0;\n\tPeerData *_peer = nullptr;\n\tUserData *_user = nullptr; // if user profile photos overview\n\n\t// We save the information about the reason of the current mediaview show:\n\t// did we open a peer profile photo or a photo from some message.\n\t// We use it when trying to delete a photo: if we've opened a peer photo,\n\t// then we'll delete group photo instead of the corresponding message.\n\tbool _firstOpenedPeerPhoto = false;\n\n\tPeerData *_from = nullptr;\n\tQString _fromName;\n\tUi::Text::String _fromNameLabel;\n\n\tstd::optional<int> _index; // Index in current _sharedMedia data.\n\tstd::optional<int> _fullIndex; // Index in full shared media.\n\tstd::optional<int> _fullCount;\n\tHistoryItem *_message = nullptr;\n\n\tmtpRequestId _loadRequest = 0;\n\n\tOver _over = Over::None;\n\tOver _down = Over::None;\n\tQPoint _lastAction, _lastMouseMovePos;\n\tbool _ignoringDropdown = false;\n\n\tUi::Animations::Basic _stateAnimation;\n\n\tenum ControlsState {\n\t\tControlsShowing,\n\t\tControlsShown,\n\t\tControlsHiding,\n\t\tControlsHidden,\n\t};\n\tControlsState _controlsState = ControlsShown;\n\tcrl::time _controlsAnimStarted = 0;\n\tbase::Timer _controlsHideTimer;\n\tanim::value _controlsOpacity = { 1. };\n\tbool _mousePressed = false;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\tobject_ptr<Ui::DropdownMenu> _dropdown;\n\tbase::Timer _dropdownShowTimer;\n\n\tbase::unique_qptr<SponsoredButton> _sponsoredButton;\n\tobject_ptr<Ui::RoundButton> _voteButton = { nullptr };\n\tobject_ptr<Ui::RpWidget> _pollVotersWidget = { nullptr };\n\trpl::lifetime _pollUpdateLifetime;\n\n\tbool _receiveMouse = true;\n\tbool _processingKeyPress = false;\n\tbool _clickHandlerActive = false;\n\tbool _clickHandlerPressed = false;\n\n\tbool _touchPress = false;\n\tbool _touchMove = false;\n\tbool _touchRightButton = false;\n\tbase::Timer _touchTimer;\n\tQPoint _touchStart;\n\n\tQString _saveMsgFilename;\n\tQRect _saveMsg;\n\tUi::Text::String _saveMsgText;\n\tSavePhotoVideo _savePhotoVideoWhenLoaded = SavePhotoVideo::None;\n\t// _saveMsgAnimation -> _saveMsgTimer -> _saveMsgAnimation.\n\tUi::Animations::Simple _saveMsgAnimation;\n\tbase::Timer _saveMsgTimer;\n\n\tQString _chapterText;\n\tQRect _chapterRect;\n\tUi::Animations::Simple _chapterAnimation;\n\tbase::Timer _chapterTimer;\n\tstruct ChapterArrow {\n\t\tUi::Animations::Simple animation;\n\t\tint direction = 0;\n\t};\n\tstd::vector<std::unique_ptr<ChapterArrow>> _chapterArrows;\n\n\tbool _speedBoostActive = false;\n\tbool _speedBoostFromMouse = false;\n\tfloat64 _speedBoostSavedSpeed = 1.;\n\tfloat64 _speedBoostSpeed = 2.;\n\tfloat64 _speedBoostDragAccum = 0.;\n\tQRect _speedBoostRect;\n\tUi::Animations::Simple _speedBoostAnimation;\n\tbase::Timer _speedBoostHoldTimer;\n\tbase::Timer _frameStepThrottle;\n\tint _frameStepPending = 0;\n\tUi::Animations::Basic _speedBoostTicker;\n\tfloat64 _speedBoostPhase = 0.;\n\tcrl::time _speedBoostLastFrame = 0;\n\n\tbase::flat_map<Over, crl::time> _animations;\n\tbase::flat_map<Over, anim::value> _animationOpacities;\n\n\trpl::event_stream<Media::Player::TrackState> _touchbarTrackState;\n\trpl::event_stream<TouchBarItemType> _touchbarDisplay;\n\trpl::event_stream<bool> _touchbarFullscreenToggled;\n\n\tint _verticalWheelDelta = 0;\n\n\tPlatform::TextRecognition::Result _recognitionResult;\n\tuint64 _recognitionPendingSessionUniqueId = 0;\n\tPhotoId _recognitionPendingPhotoId = 0;\n\tbool _recognitionRetryOnLarge = false;\n\tbool _showRecognitionResults = false;\n\tUi::Animations::Simple _recognitionAnimation;\n\n\tbool _themePreviewShown = false;\n\tuint64 _themePreviewId = 0;\n\tQRect _themePreviewRect;\n\tstd::unique_ptr<Window::Theme::Preview> _themePreview;\n\tobject_ptr<Ui::RoundButton> _themeApply = { nullptr };\n\tobject_ptr<Ui::RoundButton> _themeCancel = { nullptr };\n\tobject_ptr<Ui::RoundButton> _themeShare = { nullptr };\n\tData::CloudTheme _themeCloudData;\n\n\tstd::unique_ptr<Ui::RpWidget> _hideWorkaround;\n\n};\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_pip.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/view/media_view_pip.h\"\n\n#include \"media/streaming/media_streaming_player.h\"\n#include \"media/streaming/media_streaming_document.h\"\n#include \"media/streaming/media_streaming_utility.h\"\n#include \"media/view/media_view_playback_progress.h\"\n#include \"media/view/media_view_pip_opengl.h\"\n#include \"media/view/media_view_pip_raster.h\"\n#include \"media/view/media_view_pip_rhi.h\"\n#include \"media/audio/media_audio.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_session.h\"\n#include \"data/data_media_rotation.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"core/application.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/power_save_blocker.h\"\n#include \"ui/platform/ui_platform_utility.h\"\n#include \"ui/platform/ui_platform_window_title.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/gl/gl_surface.h\"\n#include \"ui/gl/gl_detection.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_controller.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_media_view.h\"\n\n#include <QtGui/QWindow>\n#include <QtGui/QScreen>\n#include <QtWidgets/QApplication>\n\nnamespace Media {\nnamespace View {\nnamespace {\n\nconstexpr auto kPipLoaderPriority = 2;\nconstexpr auto kMsInSecond = 1000;\n\n[[nodiscard]] QRect ScreenFromPosition(QPoint point) {\n\tconst auto screen = QGuiApplication::screenAt(point);\n\tconst auto use = screen ? screen : QGuiApplication::primaryScreen();\n\treturn use\n\t\t? use->availableGeometry()\n\t\t: QRect(0, 0, st::windowDefaultWidth, st::windowDefaultHeight);\n}\n\n[[nodiscard]] QSize MaxAllowedSizeForScreen(QSize screenSize) {\n\t// Each side should be less than screen side - 3 * st::pipBorderSkip,\n\t// That way it won't try to snap to both opposite sides of the screen.\n\tconst auto skip = 3 * st::pipBorderSkip;\n\treturn { screenSize.width() - skip, screenSize.height() - skip };\n}\n\n[[nodiscard]] QPoint ClampToEdges(QRect screen, QRect inner) {\n\tconst auto skip = st::pipBorderSkip;\n\tconst auto area = st::pipBorderSnapArea;\n\tconst auto sleft = screen.x() + skip;\n\tconst auto stop = screen.y() + skip;\n\tconst auto sright = screen.x() + screen.width() - skip;\n\tconst auto sbottom = screen.y() + screen.height() - skip;\n\tconst auto ileft = inner.x();\n\tconst auto itop = inner.y();\n\tconst auto iright = inner.x() + inner.width();\n\tconst auto ibottom = inner.y() + inner.height();\n\tauto shiftx = 0;\n\tauto shifty = 0;\n\tif (iright + shiftx >= sright - area && iright + shiftx < sright + area) {\n\t\tshiftx += (sright - iright);\n\t}\n\tif (ileft + shiftx >= sleft - area && ileft + shiftx < sleft + area) {\n\t\tshiftx += (sleft - ileft);\n\t}\n\tif (ibottom + shifty >= sbottom - area && ibottom + shifty < sbottom + area) {\n\t\tshifty += (sbottom - ibottom);\n\t}\n\tif (itop + shifty >= stop - area && itop + shifty < stop + area) {\n\t\tshifty += (stop - itop);\n\t}\n\treturn inner.topLeft() + QPoint(shiftx, shifty);\n}\n\n[[nodiscard]] QRect Transformed(\n\t\tQRect original,\n\t\tQSize minimalSize,\n\t\tQSize maximalSize,\n\t\tQPoint delta,\n\t\tRectPart by) {\n\tconst auto width = original.width();\n\tconst auto height = original.height();\n\tconst auto x1 = width - minimalSize.width();\n\tconst auto x2 = maximalSize.width() - width;\n\tconst auto y1 = height - minimalSize.height();\n\tconst auto y2 = maximalSize.height() - height;\n\tswitch (by) {\n\tcase RectPart::Center: return original.translated(delta);\n\tcase RectPart::TopLeft:\n\t\toriginal.setTop(original.y() + std::clamp(delta.y(), -y2, y1));\n\t\toriginal.setLeft(original.x() + std::clamp(delta.x(), -x2, x1));\n\t\treturn original;\n\tcase RectPart::TopRight:\n\t\toriginal.setTop(original.y() + std::clamp(delta.y(), -y2, y1));\n\t\toriginal.setWidth(original.width() + std::clamp(delta.x(), -x1, x2));\n\t\treturn original;\n\tcase RectPart::BottomRight:\n\t\toriginal.setHeight(\n\t\t\toriginal.height() + std::clamp(delta.y(), -y1, y2));\n\t\toriginal.setWidth(original.width() + std::clamp(delta.x(), -x1, x2));\n\t\treturn original;\n\tcase RectPart::BottomLeft:\n\t\toriginal.setHeight(\n\t\t\toriginal.height() + std::clamp(delta.y(), -y1, y2));\n\t\toriginal.setLeft(original.x() + std::clamp(delta.x(), -x2, x1));\n\t\treturn original;\n\tcase RectPart::Left:\n\t\toriginal.setLeft(original.x() + std::clamp(delta.x(), -x2, x1));\n\t\treturn original;\n\tcase RectPart::Top:\n\t\toriginal.setTop(original.y() + std::clamp(delta.y(), -y2, y1));\n\t\treturn original;\n\tcase RectPart::Right:\n\t\toriginal.setWidth(original.width() + std::clamp(delta.x(), -x1, x2));\n\t\treturn original;\n\tcase RectPart::Bottom:\n\t\toriginal.setHeight(\n\t\t\toriginal.height() + std::clamp(delta.y(), -y1, y2));\n\t\treturn original;\n\t}\n\treturn original;\n\tUnexpected(\"RectPart in PiP Transformed.\");\n}\n\n[[nodiscard]] QRect Constrained(\n\t\tQRect original,\n\t\tQSize minimalSize,\n\t\tQSize maximalSize,\n\t\tQSize ratio,\n\t\tRectPart by,\n\t\tRectParts attached) {\n\tif (by == RectPart::Center) {\n\t\treturn original;\n\t} else if (!original.width() && !original.height()) {\n\t\treturn QRect(original.topLeft(), ratio);\n\t}\n\tconst auto widthLarger = (original.width() * ratio.height())\n\t\t> (original.height() * ratio.width());\n\tconst auto desiredSize = ratio.scaled(\n\t\toriginal.size(),\n\t\t(((RectParts(by) & RectPart::AllCorners)\n\t\t\t|| ((by == RectPart::Top || by == RectPart::Bottom)\n\t\t\t\t&& widthLarger)\n\t\t\t|| ((by == RectPart::Left || by == RectPart::Right)\n\t\t\t\t&& !widthLarger))\n\t\t\t? Qt::KeepAspectRatio\n\t\t\t: Qt::KeepAspectRatioByExpanding));\n\tconst auto newSize = QSize(\n\t\tstd::clamp(\n\t\t\tdesiredSize.width(),\n\t\t\tminimalSize.width(),\n\t\t\tmaximalSize.width()),\n\t\tstd::clamp(\n\t\t\tdesiredSize.height(),\n\t\t\tminimalSize.height(),\n\t\t\tmaximalSize.height()));\n\tswitch (by) {\n\tcase RectPart::TopLeft:\n\t\treturn QRect(\n\t\t\toriginal.topLeft() + QPoint(\n\t\t\t\toriginal.width() - newSize.width(),\n\t\t\t\toriginal.height() - newSize.height()),\n\t\t\tnewSize);\n\tcase RectPart::TopRight:\n\t\treturn QRect(\n\t\t\toriginal.topLeft() + QPoint(\n\t\t\t\t0,\n\t\t\t\toriginal.height() - newSize.height()),\n\t\t\tnewSize);\n\tcase RectPart::BottomRight:\n\t\treturn QRect(original.topLeft(), newSize);\n\tcase RectPart::BottomLeft:\n\t\treturn QRect(\n\t\t\toriginal.topLeft() + QPoint(\n\t\t\t\toriginal.width() - newSize.width(),\n\t\t\t\t0),\n\t\t\tnewSize);\n\tcase RectPart::Left:\n\t\treturn QRect(\n\t\t\toriginal.topLeft() + QPoint(\n\t\t\t\t(original.width() - newSize.width()),\n\t\t\t\t((attached & RectPart::Top)\n\t\t\t\t\t? 0\n\t\t\t\t\t: (attached & RectPart::Bottom)\n\t\t\t\t\t? (original.height() - newSize.height())\n\t\t\t\t\t: (original.height() - newSize.height()) / 2)),\n\t\t\tnewSize);\n\tcase RectPart::Top:\n\t\treturn QRect(\n\t\t\toriginal.topLeft() + QPoint(\n\t\t\t\t((attached & RectPart::Left)\n\t\t\t\t\t? 0\n\t\t\t\t\t: (attached & RectPart::Right)\n\t\t\t\t\t? (original.width() - newSize.width())\n\t\t\t\t\t: (original.width() - newSize.width()) / 2),\n\t\t\t\t0),\n\t\t\tnewSize);\n\tcase RectPart::Right:\n\t\treturn QRect(\n\t\t\toriginal.topLeft() + QPoint(\n\t\t\t\t0,\n\t\t\t\t((attached & RectPart::Top)\n\t\t\t\t\t? 0\n\t\t\t\t\t: (attached & RectPart::Bottom)\n\t\t\t\t\t? (original.height() - newSize.height())\n\t\t\t\t\t: (original.height() - newSize.height()) / 2)),\n\t\t\tnewSize);\n\tcase RectPart::Bottom:\n\t\treturn QRect(\n\t\t\toriginal.topLeft() + QPoint(\n\t\t\t\t((attached & RectPart::Left)\n\t\t\t\t\t? 0\n\t\t\t\t\t: (attached & RectPart::Right)\n\t\t\t\t\t? (original.width() - newSize.width())\n\t\t\t\t\t: (original.width() - newSize.width()) / 2),\n\t\t\t\t(original.height() - newSize.height())),\n\t\t\tnewSize);\n\t}\n\tUnexpected(\"RectPart in PiP Constrained.\");\n}\n\n[[nodiscard]] QByteArray Serialize(const PipPanel::Position &position) {\n\tauto result = QByteArray();\n\tauto stream = QDataStream(&result, QIODevice::WriteOnly);\n\tstream.setVersion(QDataStream::Qt_5_3);\n\tstream\n\t\t<< qint32(position.attached.value())\n\t\t<< qint32(position.snapped.value())\n\t\t<< position.screen\n\t\t<< position.geometry;\n\tstream.device()->close();\n\n\treturn result;\n}\n\n[[nodiscard]] PipPanel::Position Deserialize(const QByteArray &data) {\n\tauto stream = QDataStream(data);\n\tauto result = PipPanel::Position();\n\tauto attached = qint32(0);\n\tauto snapped = qint32(0);\n\tstream >> attached >> snapped >> result.screen >> result.geometry;\n\tif (stream.status() != QDataStream::Ok) {\n\t\treturn {};\n\t}\n\tresult.attached = RectParts::from_raw(attached);\n\tresult.snapped = RectParts::from_raw(snapped);\n\treturn result;\n}\n\nQt::Edges RectPartToQtEdges(RectPart rectPart) {\n\tswitch (rectPart) {\n\tcase RectPart::TopLeft:\n\t\treturn Qt::TopEdge | Qt::LeftEdge;\n\tcase RectPart::TopRight:\n\t\treturn Qt::TopEdge | Qt::RightEdge;\n\tcase RectPart::BottomRight:\n\t\treturn Qt::BottomEdge | Qt::RightEdge;\n\tcase RectPart::BottomLeft:\n\t\treturn Qt::BottomEdge | Qt::LeftEdge;\n\tcase RectPart::Left:\n\t\treturn Qt::LeftEdge;\n\tcase RectPart::Top:\n\t\treturn Qt::TopEdge;\n\tcase RectPart::Right:\n\t\treturn Qt::RightEdge;\n\tcase RectPart::Bottom:\n\t\treturn Qt::BottomEdge;\n\t}\n\n\treturn Qt::Edges();\n}\n\n} // namespace\n\nQRect RotatedRect(QRect rect, int rotation) {\n\tswitch (rotation) {\n\tcase 0: return rect;\n\tcase 90: return QRect(\n\t\trect.y(),\n\t\t-rect.x() - rect.width(),\n\t\trect.height(),\n\t\trect.width());\n\tcase 180: return QRect(\n\t\t-rect.x() - rect.width(),\n\t\t-rect.y() - rect.height(),\n\t\trect.width(),\n\t\trect.height());\n\tcase 270: return QRect(\n\t\t-rect.y() - rect.height(),\n\t\trect.x(),\n\t\trect.height(),\n\t\trect.width());\n\t}\n\tUnexpected(\"Rotation in RotatedRect.\");\n}\n\nbool UsePainterRotation(int rotation) {\n\treturn !(rotation % 180);\n}\n\nQSize FlipSizeByRotation(QSize size, int rotation) {\n\treturn (((rotation / 90) % 2) == 1)\n\t\t? QSize(size.height(), size.width())\n\t\t: size;\n}\n\nQImage RotateFrameImage(QImage image, int rotation) {\n\tauto transform = QTransform();\n\ttransform.rotate(rotation);\n\treturn image.transformed(transform);\n}\n\nconst style::Shadow &PipShadow() {\n\treturn st::mediaviewPipShadow;\n}\n\nPipPanel::PipPanel(\n\tQWidget *parent,\n\tFn<Ui::GL::ChosenRenderer(Ui::GL::Capabilities)> renderer)\n: _window(std::make_unique<Ui::RpWidget>(nullptr))\n, _parent(parent) {\n\tauto chosen = std::move(renderer)(\n\t\tUi::GL::CheckCapabilities(nullptr));\n\t_content = Ui::GL::CreateSurface(\n\t\t_window.get(),\n\t\tstd::move(chosen));\n}\n\nvoid PipPanel::init() {\n\twidget()->setWindowFlags(Qt::Tool\n\t\t| Qt::WindowStaysOnTopHint\n\t\t| Qt::FramelessWindowHint\n\t\t| Qt::WindowDoesNotAcceptFocus);\n\twidget()->setAttribute(Qt::WA_ShowWithoutActivating);\n\twidget()->setAttribute(Qt::WA_MacAlwaysShowToolWindow);\n\twidget()->setAttribute(Qt::WA_NoSystemBackground);\n\twidget()->setAttribute(Qt::WA_TranslucentBackground);\n\tUi::Platform::IgnoreAllActivation(widget());\n\tUi::Platform::InitOnTopPanel(widget());\n\twidget()->setMouseTracking(true);\n\twidget()->resize(0, 0);\n\twidget()->hide();\n\n\t// Surface fills the container window, mouse events pass through\n\t// to the container for drag/resize/controls handling.\n\t_content->rpWidget()->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_window->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\t_content->rpWidget()->setGeometry(QRect(QPoint(), size));\n\t}, _window->lifetime());\n\n\trpl::merge(\n\t\trp()->shownValue() | rpl::to_empty,\n\t\trp()->paintRequest() | rpl::to_empty\n\t) | rpl::map([=] {\n\t\treturn widget()->windowHandle()\n\t\t\t&& widget()->windowHandle()->isExposed();\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::filter(rpl::mappers::_1) | rpl::on_next([=] {\n\t\t// Workaround Qt's forced transient parent.\n\t\tUi::Platform::ClearTransientParent(widget());\n\t}, rp()->lifetime());\n\n\trp()->shownValue(\n\t) | rpl::filter(rpl::mappers::_1) | rpl::on_next([=] {\n\t\tUi::Platform::SetWindowMargins(widget(), _padding);\n\t}, rp()->lifetime());\n\n\trp()->screenValue(\n\t) | rpl::skip(1) | rpl::on_next([=](not_null<QScreen*> screen) {\n\t\thandleScreenChanged(screen);\n\t}, rp()->lifetime());\n\n\tif (Platform::IsWayland()) {\n\t\trp()->sizeValue(\n\t\t) | rpl::skip(1) | rpl::on_next([=](QSize size) {\n\t\t\thandleWaylandResize(size);\n\t\t}, rp()->lifetime());\n\t}\n}\n\nnot_null<QWidget*> PipPanel::widget() const {\n\treturn _window.get();\n}\n\nnot_null<Ui::RpWidgetWrap*> PipPanel::rp() const {\n\treturn _window.get();\n}\n\nvoid PipPanel::setAspectRatio(QSize ratio) {\n\tif (_ratio == ratio) {\n\t\treturn;\n\t}\n\t_ratio = ratio;\n\tif (_ratio.isEmpty()) {\n\t\t_ratio = QSize(1, 1);\n\t}\n\tUi::Platform::DisableSystemWindowResize(widget(), _ratio);\n\tif (!widget()->size().isEmpty()) {\n\t\tsetPosition(countPosition());\n\t}\n}\n\nvoid PipPanel::setPosition(Position position) {\n\tif (!position.screen.isEmpty()) {\n\t\tfor (const auto screen : QApplication::screens()) {\n\t\t\tif (screen->geometry() == position.screen) {\n\t\t\t\tsetPositionOnScreen(position, screen->availableGeometry());\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\tsetPositionDefault();\n}\n\nQRect PipPanel::inner() const {\n\treturn widget()->rect().marginsRemoved(_padding);\n}\n\nRectParts PipPanel::attached() const {\n\treturn _attached;\n}\n\nbool PipPanel::useTransparency() const {\n\treturn _useTransparency;\n}\n\nvoid PipPanel::setDragDisabled(bool disabled) {\n\t_dragDisabled = disabled;\n\tif (_dragState) {\n\t\t_dragState = std::nullopt;\n\t}\n}\n\nbool PipPanel::dragging() const {\n\treturn _dragState.has_value();\n}\n\nrpl::producer<> PipPanel::saveGeometryRequests() const {\n\treturn _saveGeometryRequests.events();\n}\n\nQScreen *PipPanel::myScreen() const {\n\treturn widget()->screen();\n}\n\nPipPanel::Position PipPanel::countPosition() const {\n\tconst auto screen = myScreen();\n\tif (!screen) {\n\t\treturn Position();\n\t}\n\tauto result = Position();\n\tresult.screen = screen->geometry();\n\tresult.geometry = widget()->geometry().marginsRemoved(_padding);\n\tconst auto available = screen->availableGeometry();\n\tconst auto skip = st::pipBorderSkip;\n\tconst auto left = result.geometry.x();\n\tconst auto right = left + result.geometry.width();\n\tconst auto top = result.geometry.y();\n\tconst auto bottom = top + result.geometry.height();\n\tif ((!_dragState || *_dragState != RectPart::Center)\n\t\t&& !Platform::IsWayland()) {\n\t\tif (left == available.x()) {\n\t\t\tresult.attached |= RectPart::Left;\n\t\t} else if (right == available.x() + available.width()) {\n\t\t\tresult.attached |= RectPart::Right;\n\t\t} else if (left == available.x() + skip) {\n\t\t\tresult.snapped |= RectPart::Left;\n\t\t} else if (right == available.x() + available.width() - skip) {\n\t\t\tresult.snapped |= RectPart::Right;\n\t\t}\n\t\tif (top == available.y()) {\n\t\t\tresult.attached |= RectPart::Top;\n\t\t} else if (bottom == available.y() + available.height()) {\n\t\t\tresult.attached |= RectPart::Bottom;\n\t\t} else if (top == available.y() + skip) {\n\t\t\tresult.snapped |= RectPart::Top;\n\t\t} else if (bottom == available.y() + available.height() - skip) {\n\t\t\tresult.snapped |= RectPart::Bottom;\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid PipPanel::setPositionDefault() {\n\tconst auto parentScreen = _parent ? _parent->screen() : nullptr;\n\tconst auto myScreen = widget()->screen();\n\tif (parentScreen && myScreen != parentScreen) {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)\n\t\twidget()->setScreen(parentScreen);\n#else // Qt >= 6.0.0\n\t\twidget()->windowHandle()->setScreen(parentScreen);\n#endif // Qt < 6.0.0\n\t}\n\tauto position = Position();\n\tposition.snapped = RectPart::Top | RectPart::Left;\n\tposition.screen = parentScreen->geometry();\n\tposition.geometry = QRect(0, 0, st::pipDefaultSize, st::pipDefaultSize);\n\tsetPositionOnScreen(position, parentScreen->availableGeometry());\n}\n\nvoid PipPanel::setPositionOnScreen(Position position, QRect available) {\n\tconst auto screen = available;\n\tconst auto requestedSize = position.geometry.size();\n\tconst auto max = std::max(requestedSize.width(), requestedSize.height());\n\n\t// Apply aspect ratio.\n\tconst auto scaled = (_ratio.width() > _ratio.height())\n\t\t? QSize(max, max * _ratio.height() / _ratio.width())\n\t\t: QSize(max * _ratio.width() / _ratio.height(), max);\n\n\t// Apply maximum size.\n\tconst auto fit = MaxAllowedSizeForScreen(screen.size());\n\tconst auto byWidth = (scaled.width() * fit.height())\n\t\t> (scaled.height() * fit.width());\n\tconst auto normalized = (byWidth && scaled.width() > fit.width())\n\t\t? QSize(fit.width(), fit.width() * scaled.height() / scaled.width())\n\t\t: (!byWidth && scaled.height() > fit.height())\n\t\t? QSize(\n\t\t\tfit.height() * scaled.width() / scaled.height(),\n\t\t\tfit.height())\n\t\t: scaled;\n\n\t// Apply minimal size.\n\tconst auto min = st::pipMinimalSize;\n\tconst auto minimalSize = (_ratio.width() > _ratio.height())\n\t\t? QSize(min * _ratio.width() / _ratio.height(), min)\n\t\t: QSize(min, min * _ratio.height() / _ratio.width());\n\tconst auto size = QSize(\n\t\tstd::max(normalized.width(), minimalSize.width()),\n\t\tstd::max(normalized.height(), minimalSize.height()));\n\n\t// Apply maximal size.\n\tconst auto maximalSize = byWidth\n\t\t? QSize(fit.width(), fit.width() * _ratio.height() / _ratio.width())\n\t\t: QSize(fit.height() * _ratio.width() / _ratio.height(), fit.height());\n\n\t// Apply left-right screen borders.\n\tconst auto skip = st::pipBorderSkip;\n\tconst auto inner = screen.marginsRemoved({ skip, skip, skip, skip });\n\tauto geometry = QRect(position.geometry.topLeft(), size);\n\tif ((position.attached & RectPart::Left)\n\t\t|| (geometry.x() < screen.x())) {\n\t\tgeometry.moveLeft(screen.x());\n\t} else if ((position.attached & RectPart::Right)\n\t\t|| (geometry.x() + geometry.width() > screen.x() + screen.width())) {\n\t\tgeometry.moveLeft(screen.x() + screen.width() - geometry.width());\n\t} else if (position.snapped & RectPart::Left) {\n\t\tgeometry.moveLeft(inner.x());\n\t} else if (position.snapped & RectPart::Right) {\n\t\tgeometry.moveLeft(inner.x() + inner.width() - geometry.width());\n\t}\n\n\t// Apply top-bottom screen borders.\n\tif ((position.attached & RectPart::Top) || (geometry.y() < screen.y())) {\n\t\tgeometry.moveTop(screen.y());\n\t} else if ((position.attached & RectPart::Bottom)\n\t\t|| (geometry.y() + geometry.height()\n\t\t\t> screen.y() + screen.height())) {\n\t\tgeometry.moveTop(\n\t\t\tscreen.y() + screen.height() - geometry.height());\n\t} else if (position.snapped & RectPart::Top) {\n\t\tgeometry.moveTop(inner.y());\n\t} else if (position.snapped & RectPart::Bottom) {\n\t\tgeometry.moveTop(inner.y() + inner.height() - geometry.height());\n\t}\n\n\tgeometry += _padding;\n\n\tsetGeometry(geometry);\n\twidget()->setMinimumSize(minimalSize);\n\twidget()->setMaximumSize(\n\t\tstd::max(minimalSize.width(), maximalSize.width()),\n\t\tstd::max(minimalSize.height(), maximalSize.height()));\n\tupdateDecorations();\n}\n\nvoid PipPanel::update() {\n\t_content->rpWidget()->update();\n}\n\nvoid PipPanel::setGeometry(QRect geometry) {\n\twidget()->setGeometry(geometry);\n}\n\nvoid PipPanel::handleWaylandResize(QSize size) {\n\tif (_inHandleWaylandResize) {\n\t\treturn;\n\t}\n\t_inHandleWaylandResize = true;\n\n\t// Apply aspect ratio.\n\tconst auto max = std::max(size.width(), size.height());\n\tconst auto scaled = (_ratio.width() > _ratio.height())\n\t\t? QSize(max, max * _ratio.height() / _ratio.width())\n\t\t: QSize(max * _ratio.width() / _ratio.height(), max);\n\n\t// Buffer can't be bigger than the configured\n\t// (suggested by compositor) size.\n\tconst auto byWidth = (scaled.width() * size.height())\n\t\t> (scaled.height() * size.width());\n\tconst auto normalized = (byWidth && scaled.width() > size.width())\n\t\t? QSize(size.width(), size.width() * scaled.height() / scaled.width())\n\t\t: (!byWidth && scaled.height() > size.height())\n\t\t? QSize(\n\t\t\tsize.height() * scaled.width() / scaled.height(),\n\t\t\tsize.height())\n\t\t: scaled;\n\n\twidget()->resize(normalized);\n\tQResizeEvent e(normalized, size);\n\tQCoreApplication::sendEvent(widget()->windowHandle(), &e);\n\t_inHandleWaylandResize = false;\n}\n\nvoid PipPanel::handleScreenChanged(not_null<QScreen*> screen) {\n\tconst auto screenGeometry = screen->availableGeometry();\n\tconst auto minimalSize = _ratio.scaled(\n\t\tst::pipMinimalSize,\n\t\tst::pipMinimalSize,\n\t\tQt::KeepAspectRatioByExpanding);\n\tconst auto maximalSize = _ratio.scaled(\n\t\tMaxAllowedSizeForScreen(screenGeometry.size()),\n\t\tQt::KeepAspectRatio);\n\twidget()->setMinimumSize(minimalSize);\n\twidget()->setMaximumSize(\n\t\tstd::max(minimalSize.width(), maximalSize.width()),\n\t\tstd::max(minimalSize.height(), maximalSize.height()));\n}\n\nvoid PipPanel::handleMousePress(QPoint position, Qt::MouseButton button) {\n\tif (button != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tupdateOverState(position);\n\t_pressState = _overState;\n\t_pressPoint = QCursor::pos();\n}\n\nvoid PipPanel::handleMouseRelease(QPoint position, Qt::MouseButton button) {\n\tif (button != Qt::LeftButton || !base::take(_pressState)) {\n\t\treturn;\n\t} else if (base::take(_dragState)) {\n\t\tfinishDrag(QCursor::pos());\n\t\tupdateOverState(position);\n\t}\n}\n\nvoid PipPanel::updateOverState(QPoint point) {\n\tconst auto size = st::pipResizeArea;\n\tconst auto ignore = _attached | _snapped;\n\tconst auto count = [&](RectPart side, int padding) {\n\t\treturn (ignore & side) ? 0 : padding ? padding : size;\n\t};\n\tconst auto left = count(RectPart::Left, _padding.left());\n\tconst auto top = count(RectPart::Top, _padding.top());\n\tconst auto right = count(RectPart::Right, _padding.right());\n\tconst auto bottom = count(RectPart::Bottom, _padding.bottom());\n\tconst auto width = widget()->width();\n\tconst auto height = widget()->height();\n\tconst auto overState = [&] {\n\t\tif (point.x() < left) {\n\t\t\tif (point.y() < top) {\n\t\t\t\treturn RectPart::TopLeft;\n\t\t\t} else if (point.y() >= height - bottom) {\n\t\t\t\treturn RectPart::BottomLeft;\n\t\t\t} else {\n\t\t\t\treturn RectPart::Left;\n\t\t\t}\n\t\t} else if (point.x() >= width - right) {\n\t\t\tif (point.y() < top) {\n\t\t\t\treturn RectPart::TopRight;\n\t\t\t} else if (point.y() >= height - bottom) {\n\t\t\t\treturn RectPart::BottomRight;\n\t\t\t} else {\n\t\t\t\treturn RectPart::Right;\n\t\t\t}\n\t\t} else if (point.y() < top) {\n\t\t\treturn RectPart::Top;\n\t\t} else if (point.y() >= height - bottom) {\n\t\t\treturn RectPart::Bottom;\n\t\t} else {\n\t\t\treturn RectPart::Center;\n\t\t}\n\t}();\n\tif (_overState != overState) {\n\t\t_overState = overState;\n\t\twidget()->setCursor([&] {\n\t\t\tswitch (_overState) {\n\t\t\tcase RectPart::Center:\n\t\t\t\treturn style::cur_pointer;\n\t\t\tcase RectPart::TopLeft:\n\t\t\tcase RectPart::BottomRight:\n\t\t\t\treturn style::cur_sizefdiag;\n\t\t\tcase RectPart::TopRight:\n\t\t\tcase RectPart::BottomLeft:\n\t\t\t\treturn style::cur_sizebdiag;\n\t\t\tcase RectPart::Left:\n\t\t\tcase RectPart::Right:\n\t\t\t\treturn style::cur_sizehor;\n\t\t\tcase RectPart::Top:\n\t\t\tcase RectPart::Bottom:\n\t\t\t\treturn style::cur_sizever;\n\t\t\t}\n\t\t\tUnexpected(\"State in PipPanel::updateOverState.\");\n\t\t}());\n\t}\n}\n\nvoid PipPanel::handleMouseMove(QPoint position) {\n\tif (!_pressState) {\n\t\tupdateOverState(position);\n\t\treturn;\n\t}\n\tconst auto point = QCursor::pos();\n\tconst auto distance = QApplication::startDragDistance();\n\tif (!_dragState\n\t\t&& (point - _pressPoint).manhattanLength() > distance\n\t\t&& !_dragDisabled) {\n\t\t_dragState = _pressState;\n\t\tupdateDecorations();\n\t\t_dragStartGeometry = widget()->geometry().marginsRemoved(_padding);\n\t}\n\tif (_dragState) {\n\t\tif (Platform::IsWayland()) {\n\t\t\tstartSystemDrag();\n\t\t} else {\n\t\t\tprocessDrag(point);\n\t\t}\n\t}\n}\n\nvoid PipPanel::startSystemDrag() {\n\tExpects(_dragState.has_value());\n\n\tconst auto stateEdges = RectPartToQtEdges(*_dragState);\n\tif (stateEdges) {\n\t\twidget()->windowHandle()->startSystemResize(stateEdges);\n\t} else {\n\t\twidget()->windowHandle()->startSystemMove();\n\t}\n\n\tUi::SendSynteticMouseEvent(\n\t\twidget().get(),\n\t\tQEvent::MouseButtonRelease,\n\t\tQt::LeftButton);\n}\n\nvoid PipPanel::processDrag(QPoint point) {\n\tExpects(_dragState.has_value());\n\n\tconst auto dragPart = *_dragState;\n\tconst auto screen = (dragPart == RectPart::Center)\n\t\t? ScreenFromPosition(point)\n\t\t: myScreen()\n\t\t? myScreen()->availableGeometry()\n\t\t: QRect();\n\tif (screen.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto minimalSize = _ratio.scaled(\n\t\tst::pipMinimalSize,\n\t\tst::pipMinimalSize,\n\t\tQt::KeepAspectRatioByExpanding);\n\tconst auto maximalSize = _ratio.scaled(\n\t\tMaxAllowedSizeForScreen(screen.size()),\n\t\tQt::KeepAspectRatio);\n\tconst auto geometry = Transformed(\n\t\t_dragStartGeometry,\n\t\tminimalSize,\n\t\tmaximalSize,\n\t\tpoint - _pressPoint,\n\t\tdragPart);\n\tconst auto valid = Constrained(\n\t\tgeometry,\n\t\tminimalSize,\n\t\tmaximalSize,\n\t\t_ratio,\n\t\tdragPart,\n\t\t_attached);\n\tconst auto clamped = (dragPart == RectPart::Center)\n\t\t? ClampToEdges(screen, valid)\n\t\t: valid.topLeft();\n\twidget()->setMinimumSize(minimalSize);\n\twidget()->setMaximumSize(\n\t\tstd::max(minimalSize.width(), maximalSize.width()),\n\t\tstd::max(minimalSize.height(), maximalSize.height()));\n\tif (clamped != valid.topLeft()) {\n\t\tmoveAnimated(clamped);\n\t} else {\n\t\tconst auto newGeometry = valid.marginsAdded(_padding);\n\t\t_positionAnimation.stop();\n\t\tsetGeometry(newGeometry);\n\t}\n}\n\nvoid PipPanel::finishDrag(QPoint point) {\n\tconst auto screen = ScreenFromPosition(point);\n\tconst auto inner = widget()->geometry().marginsRemoved(_padding);\n\tconst auto position = widget()->pos();\n\tconst auto clamped = [&] {\n\t\tauto result = position;\n\t\tif (Platform::IsWayland()) {\n\t\t\treturn result;\n\t\t}\n\t\tif (result.x() > screen.x() + screen.width() - inner.width()) {\n\t\t\tresult.setX(screen.x() + screen.width() - inner.width());\n\t\t}\n\t\tif (result.x() < screen.x()) {\n\t\t\tresult.setX(screen.x());\n\t\t}\n\t\tif (result.y() > screen.y() + screen.height() - inner.height()) {\n\t\t\tresult.setY(screen.y() + screen.height() - inner.height());\n\t\t}\n\t\tif (result.y() < screen.y()) {\n\t\t\tresult.setY(screen.y());\n\t\t}\n\t\treturn result;\n\t}();\n\tif (position != clamped) {\n\t\tmoveAnimated(clamped);\n\t} else {\n\t\t_positionAnimation.stop();\n\t\tupdateDecorations();\n\t}\n}\n\nvoid PipPanel::updatePositionAnimated() {\n\tconst auto progress = _positionAnimation.value(1.);\n\tif (!_positionAnimation.animating()) {\n\t\twidget()->move(_positionAnimationTo\n\t\t\t- QPoint(_padding.left(), _padding.top()));\n\t\tif (!_dragState) {\n\t\t\tupdateDecorations();\n\t\t}\n\t\treturn;\n\t}\n\tconst auto from = QPointF(_positionAnimationFrom);\n\tconst auto to = QPointF(_positionAnimationTo);\n\twidget()->move((from + (to - from) * progress).toPoint()\n\t\t- QPoint(_padding.left(), _padding.top()));\n}\n\nvoid PipPanel::moveAnimated(QPoint to) {\n\tif (_positionAnimation.animating() && _positionAnimationTo == to) {\n\t\treturn;\n\t}\n\t_positionAnimationTo = to;\n\t_positionAnimationFrom = widget()->pos()\n\t\t+ QPoint(_padding.left(), _padding.top());\n\t_positionAnimation.stop();\n\t_positionAnimation.start(\n\t\t[=] { updatePositionAnimated(); },\n\t\t0.,\n\t\t1.,\n\t\tst::slideWrapDuration,\n\t\tanim::easeOutCirc);\n}\n\nvoid PipPanel::updateDecorations() {\n\tconst auto guard = gsl::finally([&] {\n\t\tif (!_dragState) {\n\t\t\t_saveGeometryRequests.fire({});\n\t\t}\n\t});\n\tconst auto position = countPosition();\n\tconst auto use = Ui::Platform::TranslucentWindowsSupported();\n\tconst auto full = use ? PipShadow().extend : style::margins();\n\tconst auto padding = style::margins(\n\t\t(position.attached & RectPart::Left) ? 0 : full.left(),\n\t\t(position.attached & RectPart::Top) ? 0 : full.top(),\n\t\t(position.attached & RectPart::Right) ? 0 : full.right(),\n\t\t(position.attached & RectPart::Bottom) ? 0 : full.bottom());\n\t_snapped = position.snapped;\n\tif (_padding == padding && _attached == position.attached) {\n\t\treturn;\n\t}\n\tconst auto newGeometry = position.geometry.marginsAdded(padding);\n\t_attached = position.attached;\n\t_padding = padding;\n\t_useTransparency = use;\n\twidget()->setAttribute(Qt::WA_OpaquePaintEvent, !_useTransparency);\n\tif (widget()->windowHandle()) {\n\t\tUi::Platform::SetWindowMargins(widget(), _padding);\n\t}\n\tsetGeometry(newGeometry);\n\tupdate();\n}\n\nPip::Pip(\n\tnot_null<Delegate*> delegate,\n\tnot_null<DocumentData*> data,\n\tData::FileOrigin origin,\n\tnot_null<DocumentData*> chosenQuality,\n\tHistoryItem *context,\n\tVideoQuality quality,\n\tstd::shared_ptr<Streaming::Document> shared,\n\tFnMut<void()> closeAndContinue,\n\tFnMut<void()> destroy)\n: _delegate(delegate)\n, _data(data)\n, _origin(origin)\n, _chosenQuality(chosenQuality)\n, _context(context)\n, _quality(quality)\n, _instance(\n\tstd::in_place,\n\tstd::move(shared),\n\t[=] { waitingAnimationCallback(); })\n, _panel(\n\t_delegate->pipParentWidget(),\n\t[=](Ui::GL::Capabilities capabilities) {\n\t\treturn chooseRenderer(capabilities);\n\t})\n, _playbackProgress(std::make_unique<PlaybackProgress>())\n, _dataMedia(_data->createMediaView())\n, _rotation(data->owner().mediaRotation().get(data))\n, _lastPositiveVolume((Core::App().settings().videoVolume() > 0.)\n\t? Core::App().settings().videoVolume()\n\t: Core::Settings::kDefaultVolume)\n, _closeAndContinue(std::move(closeAndContinue))\n, _destroy(std::move(destroy)) {\n\tsetupPanel();\n\tsetupButtons();\n\tsetupStreaming();\n\n\t_data->session().account().sessionChanges(\n\t) | rpl::on_next([=] {\n\t\t_destroy();\n\t}, _panel.rp()->lifetime());\n\n\tif (_context) {\n\t\t_data->owner().itemRemoved(\n\t\t) | rpl::on_next([=](not_null<const HistoryItem*> data) {\n\t\t\tif (_context != data) {\n\t\t\t\t_context = nullptr;\n\t\t\t}\n\t\t}, _panel.rp()->lifetime());\n\t}\n}\n\nPip::~Pip() = default;\n\nstd::shared_ptr<Streaming::Document> Pip::shared() const {\n\treturn _instance->shared();\n}\n\nvoid Pip::setupPanel() {\n\t_panel.init();\n\tconst auto size = [&] {\n\t\tif (!_instance->info().video.size.isEmpty()) {\n\t\t\treturn _instance->info().video.size;\n\t\t}\n\t\tconst auto media = _data->activeMediaView();\n\t\tif (media) {\n\t\t\tmedia->goodThumbnailWanted();\n\t\t}\n\t\tconst auto good = media ? media->goodThumbnail() : nullptr;\n\t\tconst auto original = good ? good->size() : _data->dimensions;\n\t\treturn original.isEmpty() ? QSize(1, 1) : original;\n\t}();\n\t_panel.setAspectRatio(FlipSizeByRotation(size, _rotation));\n\t_panel.setPosition(Deserialize(_delegate->pipLoadGeometry()));\n\t_panel.widget()->show();\n\n\t_panel.saveGeometryRequests(\n\t) | rpl::on_next([=] {\n\t\tsaveGeometry();\n\t}, _panel.rp()->lifetime());\n\n\t_panel.rp()->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tconst auto mousePosition = [&] {\n\t\t\treturn static_cast<QMouseEvent*>(e.get())->pos();\n\t\t};\n\t\tconst auto mouseButton = [&] {\n\t\t\treturn static_cast<QMouseEvent*>(e.get())->button();\n\t\t};\n\t\tswitch (e->type()) {\n\t\tcase QEvent::Close: handleClose(); break;\n\t\tcase QEvent::Leave: handleLeave(); break;\n\t\tcase QEvent::MouseMove:\n\t\t\thandleMouseMove(mousePosition());\n\t\t\tbreak;\n\t\tcase QEvent::MouseButtonPress:\n\t\t\thandleMousePress(mousePosition(), mouseButton());\n\t\t\tbreak;\n\t\tcase QEvent::MouseButtonRelease:\n\t\t\thandleMouseRelease(mousePosition(), mouseButton());\n\t\t\tbreak;\n\t\tcase QEvent::MouseButtonDblClick:\n\t\t\thandleDoubleClick(mouseButton());\n\t\t\tbreak;\n\t\t}\n\t}, _panel.rp()->lifetime());\n}\n\nvoid Pip::handleClose() {\n\tcrl::on_main(_panel.widget(), [=] {\n\t\t_destroy();\n\t});\n}\n\nvoid Pip::handleLeave() {\n\tsetOverState(OverState::None);\n}\n\nvoid Pip::handleMouseMove(QPoint position) {\n\tconst auto weak = base::make_weak(_panel.widget());\n\tconst auto guard = gsl::finally([&] {\n\t\tif (weak) {\n\t\t\t_panel.handleMouseMove(position);\n\t\t}\n\t});\n\tsetOverState(computeState(position));\n\tseekUpdate(position);\n\tvolumeControllerUpdate(position);\n}\n\nvoid Pip::setOverState(OverState state) {\n\tif (_over == state) {\n\t\treturn;\n\t}\n\tconst auto wasShown = ResolveShownOver(_over);\n\t_over = state;\n\tconst auto nowAreShown = (ResolveShownOver(_over) != OverState::None);\n\tif ((wasShown != OverState::None) != nowAreShown) {\n\t\t_controlsShown.start(\n\t\t\t[=] { _panel.update(); },\n\t\t\tnowAreShown ? 0. : 1.,\n\t\t\tnowAreShown ? 1. : 0.,\n\t\t\tst::fadeWrapDuration,\n\t\t\tanim::linear);\n\t}\n\tif (!_pressed) {\n\t\tupdateActiveState(wasShown);\n\t}\n\t_panel.update();\n}\n\nvoid Pip::setPressedState(std::optional<OverState> state) {\n\tif (_pressed == state) {\n\t\treturn;\n\t}\n\tconst auto wasShown = shownActiveState();\n\t_pressed = state;\n\tupdateActiveState(wasShown);\n}\n\nPip::OverState Pip::shownActiveState() const {\n\treturn ResolveShownOver(_pressed.value_or(_over));\n}\n\nfloat64 Pip::activeValue(const Button &button) const {\n\tconst auto shownState = ResolveShownOver(button.state);\n\treturn button.active.value((shownActiveState() == shownState) ? 1. : 0.);\n}\n\nvoid Pip::updateActiveState(OverState wasShown) {\n\tconst auto check = [&](Button &button) {\n\t\tconst auto shownState = ResolveShownOver(button.state);\n\t\tconst auto nowIsShown = (shownActiveState() == shownState);\n\t\tif ((wasShown == shownState) != nowIsShown) {\n\t\t\tbutton.active.start(\n\t\t\t\t[=] { _panel.update(); },\n\t\t\t\tnowIsShown ? 0. : 1.,\n\t\t\t\tnowIsShown ? 1. : 0.,\n\t\t\t\tst::fadeWrapDuration,\n\t\t\t\tanim::linear);\n\t\t}\n\t};\n\tcheck(_close);\n\tcheck(_enlarge);\n\tcheck(_play);\n\tcheck(_playback);\n\tcheck(_volumeToggle);\n\tcheck(_volumeController);\n}\n\nPip::OverState Pip::ResolveShownOver(OverState state) {\n\treturn (state == OverState::VolumeController)\n\t\t? OverState::VolumeToggle\n\t\t: state;\n}\n\nvoid Pip::handleMousePress(QPoint position, Qt::MouseButton button) {\n\tconst auto weak = base::make_weak(_panel.widget());\n\tconst auto guard = gsl::finally([&] {\n\t\tif (weak) {\n\t\t\t_panel.handleMousePress(position, button);\n\t\t}\n\t});\n\tif (button != Qt::LeftButton) {\n\t\treturn;\n\t}\n\t_pressed = _over;\n\tif (_over == OverState::Playback || _over == OverState::VolumeController) {\n\t\t_panel.setDragDisabled(true);\n\t}\n\tseekUpdate(position);\n\tvolumeControllerUpdate(position);\n}\n\nvoid Pip::handleMouseRelease(QPoint position, Qt::MouseButton button) {\n\tconst auto weak = base::make_weak(_panel.widget());\n\tconst auto guard = gsl::finally([&] {\n\t\tif (weak) {\n\t\t\t_panel.handleMouseRelease(position, button);\n\t\t}\n\t});\n\tif (button != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tseekUpdate(position);\n\n\tvolumeControllerUpdate(position);\n\n\tconst auto pressed = base::take(_pressed);\n\tif (pressed && *pressed == OverState::Playback) {\n\t\t_panel.setDragDisabled(false);\n\t\tseekFinish(_playbackProgress->value());\n\t} else if (pressed && *pressed == OverState::VolumeController) {\n\t\t_panel.setDragDisabled(false);\n\t\t_panel.update();\n\t} else if (_panel.dragging() || !pressed || *pressed != _over) {\n\t\t_lastHandledPress = std::nullopt;\n\t} else {\n\t\t_lastHandledPress = _over;\n\t\tswitch (_over) {\n\t\tcase OverState::Close: _panel.widget()->close(); break;\n\t\tcase OverState::Enlarge: _closeAndContinue(); break;\n\t\tcase OverState::VolumeToggle: volumeToggled(); break;\n\t\tcase OverState::Other: playbackPauseResume(); break;\n\t\t}\n\t}\n}\n\nvoid Pip::handleDoubleClick(Qt::MouseButton button) {\n\tif (_over != OverState::Other\n\t\t|| !_lastHandledPress\n\t\t|| *_lastHandledPress != _over) {\n\t\treturn;\n\t}\n\tplaybackPauseResume(); // Un-click the first click.\n\t_closeAndContinue();\n}\n\nvoid Pip::seekUpdate(QPoint position) {\n\tif (!_pressed || *_pressed != OverState::Playback) {\n\t\treturn;\n\t}\n\tconst auto unbound = (position.x() - _playback.icon.x())\n\t\t/ float64(_playback.icon.width());\n\tconst auto progress = std::clamp(unbound, 0., 1.);\n\tseekProgress(progress);\n}\n\nvoid Pip::seekProgress(float64 value) {\n\tif (!_lastDurationMs) {\n\t\treturn;\n\t}\n\n\t_playbackProgress->setValue(value, false);\n\n\tconst auto positionMs = std::clamp(\n\t\tstatic_cast<crl::time>(value * _lastDurationMs),\n\t\tcrl::time(0),\n\t\t_lastDurationMs);\n\tif (_seekPositionMs != positionMs) {\n\t\t_seekPositionMs = positionMs;\n\t\tif (!_instance->player().paused()\n\t\t\t&& !_instance->player().finished()) {\n\t\t\t_pausedBySeek = true;\n\t\t\tplaybackPauseResume();\n\t\t}\n\t\tupdatePlaybackTexts(_seekPositionMs, _lastDurationMs, kMsInSecond);\n\t}\n}\n\nvoid Pip::seekFinish(float64 value) {\n\tif (!_lastDurationMs) {\n\t\treturn;\n\t}\n\n\tconst auto positionMs = std::clamp(\n\t\tstatic_cast<crl::time>(value * _lastDurationMs),\n\t\tcrl::time(0),\n\t\t_lastDurationMs);\n\t_seekPositionMs = -1;\n\t_startPaused = !_pausedBySeek && !_instance->player().finished();\n\trestartAtSeekPosition(positionMs);\n}\n\nvoid Pip::volumeChanged(float64 volume) {\n\tif (volume > 0.) {\n\t\t_lastPositiveVolume = volume;\n\t}\n\tPlayer::mixer()->setVideoVolume(volume);\n\tCore::App().settings().setVideoVolume(volume);\n\tCore::App().saveSettingsDelayed();\n}\n\nvoid Pip::volumeToggled() {\n\tconst auto volume = Core::App().settings().videoVolume();\n\tvolumeChanged(volume ? 0. : _lastPositiveVolume);\n\t_panel.update();\n}\n\nvoid Pip::volumeControllerUpdate(QPoint position) {\n\tif (!_pressed || *_pressed != OverState::VolumeController) {\n\t\treturn;\n\t}\n\tconst auto unbound = (position.x() - _volumeController.icon.x())\n\t\t/ float64(_volumeController.icon.width());\n\tconst auto value = std::clamp(unbound, 0., 1.);\n\tvolumeChanged(value);\n\t_panel.update();\n}\n\nvoid Pip::setupButtons() {\n\t_close.state = OverState::Close;\n\t_enlarge.state = OverState::Enlarge;\n\t_playback.state = OverState::Playback;\n\t_volumeToggle.state = OverState::VolumeToggle;\n\t_volumeController.state = OverState::VolumeController;\n\t_play.state = OverState::Other;\n\t_panel.rp()->sizeValue(\n\t) | rpl::map([=] {\n\t\treturn _panel.inner();\n\t}) | rpl::on_next([=](QRect rect) {\n\t\tconst auto skip = st::pipControlSkip;\n\t\t_close.area = QRect(\n\t\t\trect.x(),\n\t\t\trect.y(),\n\t\t\tst::pipCloseIcon.width() + 2 * skip,\n\t\t\tst::pipCloseIcon.height() + 2 * skip);\n\t\t_enlarge.area = QRect(\n\t\t\t_close.area.x() + _close.area.width(),\n\t\t\trect.y(),\n\t\t\tst::pipEnlargeIcon.width() + 2 * skip,\n\t\t\tst::pipEnlargeIcon.height() + 2 * skip);\n\n\t\tconst auto volumeSkip = st::pipPlaybackSkip;\n\t\tconst auto volumeHeight = 2 * volumeSkip + st::pipPlaybackWide;\n\t\tconst auto volumeToggleWidth = st::pipVolumeIcon0.width()\n\t\t\t+ 2 * skip;\n\t\tconst auto volumeToggleHeight = st::pipVolumeIcon0.height()\n\t\t\t+ 2 * skip;\n\t\tconst auto volumeWidth = (((st::mediaviewVolumeWidth + 2 * skip)\n\t\t\t+ _close.area.width()\n\t\t\t+ _enlarge.area.width()\n\t\t\t+ volumeToggleWidth) < rect.width())\n\t\t\t\t? st::mediaviewVolumeWidth\n\t\t\t\t: 0;\n\t\t_volumeController.area = QRect(\n\t\t\trect.x() + rect.width() - volumeWidth - 2 * volumeSkip,\n\t\t\trect.y() + (volumeToggleHeight - volumeHeight) / 2,\n\t\t\tvolumeWidth,\n\t\t\tvolumeHeight);\n\t\t_volumeToggle.area = QRect(\n\t\t\t_volumeController.area.x()\n\t\t\t\t- st::pipVolumeIcon0.width()\n\t\t\t\t- skip,\n\t\t\trect.y(),\n\t\t\tvolumeToggleWidth,\n\t\t\tvolumeToggleHeight);\n\t\tusing Ui::Platform::TitleControlsLayout;\n\t\tif (!TitleControlsLayout::Instance()->current().onLeft()) {\n\t\t\t_close.area.moveLeft(rect.x()\n\t\t\t\t+ rect.width()\n\t\t\t\t- (_close.area.x() - rect.x())\n\t\t\t\t- _close.area.width());\n\t\t\t_enlarge.area.moveLeft(rect.x()\n\t\t\t\t+ rect.width()\n\t\t\t\t- (_enlarge.area.x() - rect.x())\n\t\t\t\t- _enlarge.area.width());\n\t\t\t_volumeToggle.area.moveLeft(rect.x());\n\t\t\t_volumeController.area.moveLeft(_volumeToggle.area.x()\n\t\t\t\t+ _volumeToggle.area.width());\n\t\t}\n\t\t_close.icon = _close.area.marginsRemoved({ skip, skip, skip, skip });\n\t\t_enlarge.icon = _enlarge.area.marginsRemoved(\n\t\t\t{ skip, skip, skip, skip });\n\t\t_volumeToggle.icon = _volumeToggle.area.marginsRemoved(\n\t\t\t{ skip, skip, skip, skip });\n\t\t_play.icon = QRect(\n\t\t\trect.x() + (rect.width() - st::pipPlayIcon.width()) / 2,\n\t\t\trect.y() + (rect.height() - st::pipPlayIcon.height()) / 2,\n\t\t\tst::pipPlayIcon.width(),\n\t\t\tst::pipPlayIcon.height());\n\t\tconst auto volumeArea = _volumeController.area;\n\t\t_volumeController.icon = (volumeArea.width() > 2 * volumeSkip\n\t\t\t&& volumeArea.height() > 2 * volumeSkip)\n\t\t\t? volumeArea.marginsRemoved(\n\t\t\t\t{ volumeSkip, volumeSkip, volumeSkip, volumeSkip })\n\t\t\t: QRect();\n\t\tconst auto playbackSkip = st::pipPlaybackSkip;\n\t\tconst auto playbackHeight = 2 * playbackSkip + st::pipPlaybackWide;\n\t\t_playback.area = QRect(\n\t\t\trect.x(),\n\t\t\trect.y() + rect.height() - playbackHeight,\n\t\t\trect.width(),\n\t\t\tplaybackHeight);\n\t\t_playback.icon = _playback.area.marginsRemoved(\n\t\t\t{ playbackSkip, playbackSkip, playbackSkip, playbackSkip });\n\t}, _panel.rp()->lifetime());\n\n\t_playbackProgress->setValueChangedCallback([=](\n\t\t\tfloat64 value,\n\t\t\tfloat64 receivedTill) {\n\t\t_panel.update();\n\t});\n}\n\nvoid Pip::saveGeometry() {\n\t_delegate->pipSaveGeometry(Serialize(_panel.countPosition()));\n}\n\nvoid Pip::updatePlayPauseResumeState(const Player::TrackState &state) {\n\tauto showPause = Player::ShowPauseIcon(state.state);\n\tif (showPause != _showPause) {\n\t\t_showPause = showPause;\n\t\t_panel.update();\n\t}\n}\n\nvoid Pip::setupStreaming() {\n\t_instance->setPriority(kPipLoaderPriority);\n\t_instance->lockPlayer();\n\n\t_instance->switchQualityRequests(\n\t) | rpl::filter([=](int quality) {\n\t\treturn !_quality.manual && _quality.height != quality;\n\t}) | rpl::on_next([=](int quality) {\n\t\tapplyVideoQuality({\n\t\t\t.manual = 0,\n\t\t\t.height = uint32(quality),\n\t\t});\n\t}, _instance->lifetime());\n\n\t_instance->player().updates(\n\t) | rpl::on_next_error([=](Streaming::Update &&update) {\n\t\thandleStreamingUpdate(std::move(update));\n\t}, [=](Streaming::Error &&error) {\n\t\thandleStreamingError(std::move(error));\n\t}, _instance->lifetime());\n\tupdatePlaybackState();\n}\n\nvoid Pip::applyVideoQuality(VideoQuality value) {\n\tif (_quality == value\n\t\t|| !_dataMedia->canBePlayed()) {\n\t\treturn;\n\t}\n\tconst auto resolved = _data->chooseQuality(_context, value);\n\tif (_chosenQuality == resolved) {\n\t\treturn;\n\t}\n\tauto instance = Streaming::Instance(\n\t\tresolved,\n\t\t_data,\n\t\t_context,\n\t\t_origin,\n\t\t[=] { waitingAnimationCallback(); });\n\tif (!instance.valid()) {\n\t\treturn;\n\t}\n\n\tif (_instance->ready()) {\n\t\t_qualityChangeFrame = currentVideoFrameImage();\n\t}\n\tif (!_instance->player().active()\n\t\t|| _instance->player().finished()) {\n\t\t_qualityChangeFinished = true;\n\t}\n\t_startPaused = _qualityChangeFinished || _instance->player().paused();\n\n\t_quality = value;\n\tCore::App().settings().setVideoQuality(value);\n\tCore::App().saveSettingsDelayed();\n\t_chosenQuality = resolved;\n\t_instance.emplace(std::move(instance));\n\tsetupStreaming();\n\trestartAtSeekPosition(_lastUpdatePosition);\n}\n\nQImage Pip::currentVideoFrameImage() const {\n\treturn _instance->player().ready()\n\t\t? _instance->player().currentFrameImage()\n\t\t: _instance->info().video.cover;\n}\n\nUi::GL::ChosenRenderer Pip::chooseRenderer(\n\t\tUi::GL::Capabilities capabilities) {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\tif (qEnvironmentVariableIsSet(\"QT_WIDGETS_RHI\")) {\n\t\tLOG((\"Renderer: [QRhi] (PipPanel)\"));\n\t\t_opengl = true;\n\t\treturn {\n\t\t\t.renderer = std::make_unique<RendererRhi>(this),\n\t\t\t.backend = Ui::GL::Backend::QRhi,\n\t\t};\n\t}\n#endif // Qt >= 6.8\n\tconst auto use = Platform::IsMac()\n\t\t? true\n\t\t: capabilities.transparency;\n\tLOG((\"OpenGL: %1 (PipPanel)\").arg(Logs::b(use)));\n\tif (use) {\n\t\t_opengl = true;\n\t\treturn {\n\t\t\t.renderer = std::make_unique<RendererGL>(this),\n\t\t\t.backend = Ui::GL::Backend::OpenGL,\n\t\t};\n\t}\n\treturn {\n\t\t.renderer = std::make_unique<RendererSW>(this),\n\t\t.backend = Ui::GL::Backend::Raster,\n\t};\n}\n\nvoid Pip::paint(not_null<Renderer*> renderer) const {\n\tconst auto controlsShown = _controlsShown.value(\n\t\t(_over != OverState::None) ? 1. : 0.);\n\tauto geometry = ContentGeometry{\n\t\t.inner = _panel.inner(),\n\t\t.attached = (_panel.useTransparency()\n\t\t\t? _panel.attached()\n\t\t\t: RectPart::AllSides),\n\t\t.fade = controlsShown,\n\t\t.outer = _panel.widget()->size(),\n\t\t.rotation = _rotation,\n\t\t.videoRotation = _instance->info().video.rotation,\n\t\t.useTransparency = _panel.useTransparency(),\n\t};\n\tif (canUseVideoFrame()) {\n\t\trenderer->paintTransformedVideoFrame(geometry);\n\t\t_instance->markFrameShown();\n\t} else {\n\t\tconst auto content = staticContent();\n\t\tif (_preparedCoverState == ThumbState::Cover) {\n\t\t\tgeometry.rotation += base::take(geometry.videoRotation);\n\t\t}\n\t\trenderer->paintTransformedStaticContent(content, geometry);\n\t}\n\tif (_instance->waitingShown()) {\n\t\trenderer->paintRadialLoading(countRadialRect(), controlsShown);\n\t}\n\tif (controlsShown > 0) {\n\t\tpaintButtons(renderer, controlsShown);\n\t\tpaintPlayback(renderer, controlsShown);\n\t\tpaintVolumeController(renderer, controlsShown);\n\t}\n}\n\nvoid Pip::paintButtons(not_null<Renderer*> renderer, float64 shown) const {\n\tconst auto outer = _panel.widget()->width();\n\tconst auto drawOne = [&](\n\t\t\tconst Button &button,\n\t\t\tconst style::icon &icon,\n\t\t\tconst style::icon &iconOver) {\n\t\trenderer->paintButton(\n\t\t\tbutton,\n\t\t\touter,\n\t\t\tshown,\n\t\t\tactiveValue(button),\n\t\t\ticon,\n\t\t\ticonOver);\n\t};\n\n\trenderer->paintButtonsStart();\n\tdrawOne(\n\t\t_play,\n\t\t_showPause ? st::pipPauseIcon : st::pipPlayIcon,\n\t\t_showPause ? st::pipPauseIconOver : st::pipPlayIconOver);\n\tdrawOne(_close, st::pipCloseIcon, st::pipCloseIconOver);\n\tdrawOne(_enlarge, st::pipEnlargeIcon, st::pipEnlargeIconOver);\n\tconst auto volume = Core::App().settings().videoVolume();\n\tif (volume <= 0.) {\n\t\tdrawOne(\n\t\t\t_volumeToggle,\n\t\t\tst::pipVolumeIcon0,\n\t\t\tst::pipVolumeIcon0Over);\n\t} else if (volume < 1 / 2.) {\n\t\tdrawOne(\n\t\t\t_volumeToggle,\n\t\t\tst::pipVolumeIcon1,\n\t\t\tst::pipVolumeIcon1Over);\n\t} else {\n\t\tdrawOne(\n\t\t\t_volumeToggle,\n\t\t\tst::pipVolumeIcon2,\n\t\t\tst::pipVolumeIcon2Over);\n\t}\n}\n\nvoid Pip::paintPlayback(not_null<Renderer*> renderer, float64 shown) const {\n\tconst auto outer = QRect(\n\t\t_playback.icon.x(),\n\t\t_playback.icon.y() - st::pipPlaybackFont->height,\n\t\t_playback.icon.width(),\n\t\tst::pipPlaybackFont->height + _playback.icon.height());\n\trenderer->paintPlayback(outer, shown);\n}\n\nvoid Pip::paintPlaybackContent(\n\t\tQPainter &p,\n\t\tQRect outer,\n\t\tfloat64 shown) const {\n\tp.setOpacity(shown);\n\tpaintPlaybackProgress(p, outer);\n\tpaintPlaybackTexts(p, outer);\n}\n\nvoid Pip::paintPlaybackProgress(QPainter &p, QRect outer) const {\n\tconst auto radius = _playback.icon.height() / 2;\n\tconst auto progress = _playbackProgress->value();\n\tconst auto active = activeValue(_playback);\n\tconst auto height = anim::interpolate(\n\t\tst::pipPlaybackWidth,\n\t\t_playback.icon.height(),\n\t\tactive);\n\tconst auto rect = QRect(\n\t\touter.x(),\n\t\t(outer.y()\n\t\t\t+ st::pipPlaybackFont->height\n\t\t\t+ _playback.icon.height()\n\t\t\t- height),\n\t\touter.width(),\n\t\theight);\n\n\tpaintProgressBar(p, rect, progress, radius, active);\n}\n\nvoid Pip::paintProgressBar(\n\t\tQPainter &p,\n\t\tconst QRect &rect,\n\t\tfloat64 progress,\n\t\tint radius,\n\t\tfloat64 active) const {\n\tconst auto done = int(base::SafeRound(rect.width() * progress));\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tif (done > 0) {\n\t\tp.setBrush(anim::brush(\n\t\t\tst::mediaviewPipControlsFg,\n\t\t\tst::mediaviewPipPlaybackActive,\n\t\t\tactive));\n\t\tp.setClipRect(rect.x(), rect.y(), done, rect.height());\n\t\tp.drawRoundedRect(\n\t\t\trect.x(),\n\t\t\trect.y(),\n\t\t\tstd::min(done + radius, rect.width()),\n\t\t\trect.height(),\n\t\t\tradius,\n\t\t\tradius);\n\t}\n\tif (done < rect.width()) {\n\t\tconst auto from = std::max(rect.x() + done - radius, rect.x());\n\t\tp.setBrush(st::mediaviewPipPlaybackInactive);\n\t\tp.setClipRect(\n\t\t\trect.x() + done,\n\t\t\trect.y(),\n\t\t\trect.width() - done,\n\t\t\trect.height());\n\t\tp.drawRoundedRect(\n\t\t\tfrom,\n\t\t\trect.y(),\n\t\t\trect.x() + rect.width() - from,\n\t\t\trect.height(),\n\t\t\tradius,\n\t\t\tradius);\n\t}\n\tp.setClipping(false);\n}\n\nvoid Pip::paintPlaybackTexts(QPainter &p, QRect outer) const {\n\tconst auto left = outer.x()\n\t\t- _playback.icon.x()\n\t\t+ _playback.area.x()\n\t\t+ st::pipPlaybackTextSkip;\n\tconst auto right = outer.x()\n\t\t- _playback.icon.x()\n\t\t+ _playback.area.x()\n\t\t+ _playback.area.width()\n\t\t- st::pipPlaybackTextSkip;\n\tconst auto top = outer.y() + st::pipPlaybackFont->ascent;\n\n\tp.setFont(st::pipPlaybackFont);\n\tp.setPen(st::mediaviewPipControlsFgOver);\n\tp.drawText(left, top, _timeAlready);\n\tp.drawText(right - _timeLeftWidth, top, _timeLeft);\n}\n\nvoid Pip::paintVolumeController(\n\t\tnot_null<Renderer*> renderer,\n\t\tfloat64 shown) const {\n\tif (_volumeController.icon.isEmpty()) {\n\t\treturn;\n\t}\n\trenderer->paintVolumeController(_volumeController.icon, shown);\n}\n\nvoid Pip::paintVolumeControllerContent(\n\t\tQPainter &p,\n\t\tQRect outer,\n\t\tfloat64 shown) const {\n\tp.setOpacity(shown);\n\n\tconst auto radius = _volumeController.icon.height() / 2;\n\tconst auto volume = Core::App().settings().videoVolume();\n\tconst auto active = activeValue(_volumeController);\n\tconst auto height = anim::interpolate(\n\t\tst::pipPlaybackWidth,\n\t\t_volumeController.icon.height(),\n\t\tactive);\n\tconst auto rect = QRect(\n\t\touter.x(),\n\t\touter.y() + radius - height / 2,\n\t\touter.width(),\n\t\theight);\n\n\tpaintProgressBar(p, rect, volume, radius, active);\n}\n\nvoid Pip::handleStreamingUpdate(Streaming::Update &&update) {\n\tusing namespace Streaming;\n\n\tv::match(update.data, [&](const Information &update) {\n\t\t_panel.setAspectRatio(\n\t\t\tFlipSizeByRotation(update.video.size, _rotation));\n\t\t_qualityChangeFrame = QImage();\n\t}, [&](PreloadedVideo) {\n\t\tupdatePlaybackState();\n\t}, [&](UpdateVideo update) {\n\t\t_panel.update();\n\t\tCore::App().updateNonIdle();\n\t\tupdatePlaybackState();\n\t\t_lastUpdatePosition = update.position;\n\t}, [&](PreloadedAudio) {\n\t\tupdatePlaybackState();\n\t}, [&](UpdateAudio) {\n\t\tupdatePlaybackState();\n\t}, [](WaitingForData) {\n\t}, [](SpeedEstimate) {\n\t}, [](MutedByOther) {\n\t}, [&](Finished) {\n\t\tupdatePlaybackState();\n\t});\n}\n\nvoid Pip::updatePlaybackState() {\n\tconst auto state = _instance->player().prepareLegacyState();\n\tupdatePlayPauseResumeState(state);\n\tif (state.position == kTimeUnknown\n\t\t|| state.length == kTimeUnknown\n\t\t|| _pausedBySeek) {\n\t\treturn;\n\t}\n\t_playbackProgress->updateState(state);\n\tupdatePowerSaveBlocker(state);\n\n\tqint64 position = 0;\n\tif (Player::IsStoppedAtEnd(state.state)) {\n\t\tposition = state.length;\n\t} else if (!Player::IsStoppedOrStopping(state.state)) {\n\t\tposition = state.position;\n\t} else {\n\t\tposition = 0;\n\t}\n\tconst auto playFrequency = state.frequency;\n\t_lastDurationMs = (state.length * crl::time(1000)) / playFrequency;\n\n\tif (_seekPositionMs < 0) {\n\t\tupdatePlaybackTexts(position, state.length, playFrequency);\n\t}\n}\n\nvoid Pip::updatePowerSaveBlocker(const Player::TrackState &state) {\n\tconst auto block = _data->isVideoFile()\n\t\t&& !IsPausedOrPausing(state.state)\n\t\t&& !IsStoppedOrStopping(state.state);\n\tbase::UpdatePowerSaveBlocker(\n\t\t_powerSaveBlocker,\n\t\tblock,\n\t\tbase::PowerSaveBlockType::PreventDisplaySleep,\n\t\t[] { return u\"Video playback is active\"_q; },\n\t\t[=] { return _panel.widget()->windowHandle(); });\n}\n\nvoid Pip::updatePlaybackTexts(\n\t\tint64 position,\n\t\tint64 length,\n\t\tint64 frequency) {\n\tconst auto playAlready = position / frequency;\n\tconst auto playLeft = (length / frequency) - playAlready;\n\tconst auto already = Ui::FormatDurationText(playAlready);\n\tconst auto minus = QChar(8722);\n\tconst auto left = minus + Ui::FormatDurationText(playLeft);\n\tif (_timeAlready == already && _timeLeft == left) {\n\t\treturn;\n\t}\n\t_timeAlready = already;\n\t_timeLeft = left;\n\t_timeLeftWidth = st::pipPlaybackFont->width(_timeLeft);\n\t_panel.update();\n}\n\nvoid Pip::handleStreamingError(Streaming::Error &&error) {\n\t_panel.widget()->close();\n}\n\nvoid Pip::playbackPauseResume() {\n\tif (_instance->player().failed()) {\n\t\t_panel.widget()->close();\n\t} else if (_instance->player().finished()\n\t\t|| !_instance->player().active()) {\n\t\t_startPaused = false;\n\t\trestartAtSeekPosition(0);\n\t} else if (_instance->player().paused()) {\n\t\t_instance->resume();\n\t\tupdatePlaybackState();\n\t} else {\n\t\t_instance->pause();\n\t\tupdatePlaybackState();\n\t}\n}\n\nvoid Pip::restartAtSeekPosition(crl::time position) {\n\t_lastUpdatePosition = position;\n\n\tif (!_instance->info().video.cover.isNull()) {\n\t\t_preparedCoverStorage = QImage();\n\t\t_preparedCoverState = ThumbState::Empty;\n\t\t_instance->saveFrameToCover();\n\t}\n\n\tauto options = Streaming::PlaybackOptions();\n\toptions.position = position;\n\toptions.hwAllowed = Core::App().settings().hardwareAcceleratedVideo();\n\toptions.audioId = _instance->player().prepareLegacyState().id;\n\toptions.speed = _delegate->pipPlaybackSpeed();\n\n\t_instance->play(options);\n\tif (_startPaused) {\n\t\t_instance->pause();\n\t}\n\t_pausedBySeek = false;\n\tupdatePlaybackState();\n}\n\nbool Pip::canUseVideoFrame() const {\n\treturn _instance->player().ready()\n\t\t&& !_instance->info().video.cover.isNull();\n}\n\nQImage Pip::videoFrame(const FrameRequest &request) const {\n\tExpects(canUseVideoFrame());\n\n\treturn _instance->frame(request);\n}\n\nStreaming::FrameWithInfo Pip::videoFrameWithInfo() const {\n\tExpects(canUseVideoFrame());\n\n\treturn _instance->frameWithInfo();\n}\n\nQImage Pip::staticContent() const {\n\tconst auto &cover = !_qualityChangeFrame.isNull()\n\t\t? _qualityChangeFrame\n\t\t: _instance->info().video.cover;\n\tconst auto media = _data->activeMediaView();\n\tconst auto use = media\n\t\t? media\n\t\t: _data->inlineThumbnailBytes().isEmpty()\n\t\t? nullptr\n\t\t: _data->createMediaView();\n\tif (use) {\n\t\tuse->goodThumbnailWanted();\n\t}\n\tconst auto good = use ? use->goodThumbnail() : nullptr;\n\tconst auto thumb = use ? use->thumbnail() : nullptr;\n\tconst auto blurred = use ? use->thumbnailInline() : nullptr;\n\n\tconst auto state = !cover.isNull()\n\t\t? ThumbState::Cover\n\t\t: good\n\t\t? ThumbState::Good\n\t\t: thumb\n\t\t? ThumbState::Thumb\n\t\t: blurred\n\t\t? ThumbState::Inline\n\t\t: ThumbState::Empty;\n\tif (!_preparedCoverStorage.isNull() && _preparedCoverState >= state) {\n\t\treturn _preparedCoverStorage;\n\t}\n\t_preparedCoverState = state;\n\tif (state == ThumbState::Cover) {\n\t\t_preparedCoverStorage = cover;\n\t} else {\n\t\t_preparedCoverStorage = (good\n\t\t\t? good\n\t\t\t: thumb\n\t\t\t? thumb\n\t\t\t: blurred\n\t\t\t? blurred\n\t\t\t: Image::BlankMedia().get())->original();\n\t\tif (!good) {\n\t\t\t_preparedCoverStorage = Images::Blur(\n\t\t\t\tstd::move(_preparedCoverStorage));\n\t\t}\n\t}\n\treturn _preparedCoverStorage;\n}\n\nvoid Pip::paintRadialLoadingContent(\n\t\tQPainter &p,\n\t\tconst QRect &inner,\n\t\tQColor fg) const {\n\tconst auto arc = inner.marginsRemoved(QMargins(\n\t\tst::radialLine,\n\t\tst::radialLine,\n\t\tst::radialLine,\n\t\tst::radialLine));\n\tp.setOpacity(_instance->waitingOpacity());\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(st::radialBg);\n\t{\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.drawEllipse(inner);\n\t}\n\tp.setOpacity(1.);\n\tUi::InfiniteRadialAnimation::Draw(\n\t\tp,\n\t\t_instance->waitingState(),\n\t\tarc.topLeft(),\n\t\tarc.size(),\n\t\t_panel.widget()->width(),\n\t\tfg,\n\t\tst::radialLine);\n}\n\nQRect Pip::countRadialRect() const {\n\tconst auto outer = _panel.inner();\n\treturn {\n\t\touter.x() + (outer.width() - st::radialSize.width()) / 2,\n\t\touter.y() + (outer.height() - st::radialSize.height()) / 2,\n\t\tst::radialSize.width(),\n\t\tst::radialSize.height()\n\t};\n}\n\nPip::OverState Pip::computeState(QPoint position) const {\n\tif (!_panel.inner().contains(position)) {\n\t\treturn OverState::None;\n\t} else if (_close.area.contains(position)) {\n\t\treturn OverState::Close;\n\t} else if (_enlarge.area.contains(position)) {\n\t\treturn OverState::Enlarge;\n\t} else if (_playback.area.contains(position)) {\n\t\treturn OverState::Playback;\n\t} else if (_volumeToggle.area.contains(position)) {\n\t\treturn OverState::VolumeToggle;\n\t} else if (_volumeController.area.contains(position)) {\n\t\treturn OverState::VolumeController;\n\t} else {\n\t\treturn OverState::Other;\n\t}\n}\n\nvoid Pip::waitingAnimationCallback() {\n\t_panel.update();\n}\n\n} // namespace View\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_pip.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_file_origin.h\"\n#include \"media/streaming/media_streaming_instance.h\"\n#include \"media/media_common.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/round_rect.h\"\n#include \"ui/rp_widget.h\"\n\n#include <QtCore/QPointer>\n\nclass HistoryItem;\n\nnamespace base {\nclass PowerSaveBlocker;\n} // namespace base\n\nnamespace Data {\nclass DocumentMedia;\n} // namespace Data\n\nnamespace style {\nstruct Shadow;\n} // namespace style\n\nnamespace Ui {\nclass IconButton;\ntemplate <typename Widget>\nclass FadeWrap;\nnamespace GL {\nstruct ChosenRenderer;\nstruct Capabilities;\n} // namespace GL\n} // namespace Ui\n\nnamespace Media::Player {\nstruct TrackState;\n} // namespace Media::Player\n\nnamespace Media::Streaming {\nclass Document;\n} // namespace Media::Streaming\n\nnamespace Media::View {\n\nclass PlaybackProgress;\n\n[[nodiscard]] QRect RotatedRect(QRect rect, int rotation);\n[[nodiscard]] bool UsePainterRotation(int rotation);\n[[nodiscard]] QSize FlipSizeByRotation(QSize size, int rotation);\n[[nodiscard]] QImage RotateFrameImage(QImage image, int rotation);\n[[nodiscard]] const style::Shadow &PipShadow();\n\nclass PipPanel final {\npublic:\n\tstruct Position {\n\t\tRectParts attached = RectPart(0);\n\t\tRectParts snapped = RectPart(0);\n\t\tQRect geometry;\n\t\tQRect screen;\n\t};\n\n\tPipPanel(\n\t\tQWidget *parent,\n\t\tFn<Ui::GL::ChosenRenderer(Ui::GL::Capabilities)> renderer);\n\tvoid init();\n\n\t[[nodiscard]] not_null<QWidget*> widget() const;\n\t[[nodiscard]] not_null<Ui::RpWidgetWrap*> rp() const;\n\n\tvoid update();\n\tvoid setGeometry(QRect geometry);\n\n\tvoid setAspectRatio(QSize ratio);\n\t[[nodiscard]] Position countPosition() const;\n\tvoid setPosition(Position position);\n\t[[nodiscard]] QRect inner() const;\n\t[[nodiscard]] RectParts attached() const;\n\t[[nodiscard]] bool useTransparency() const;\n\n\tvoid setDragDisabled(bool disabled);\n\t[[nodiscard]] bool dragging() const;\n\n\tvoid handleWaylandResize(QSize size);\n\tvoid handleScreenChanged(not_null<QScreen*> screen);\n\tvoid handleMousePress(QPoint position, Qt::MouseButton button);\n\tvoid handleMouseRelease(QPoint position, Qt::MouseButton button);\n\tvoid handleMouseMove(QPoint position);\n\n\t[[nodiscard]] rpl::producer<> saveGeometryRequests() const;\n\nprivate:\n\tvoid setPositionDefault();\n\tvoid setPositionOnScreen(Position position, QRect available);\n\n\tQScreen *myScreen() const;\n\tvoid startSystemDrag();\n\tvoid processDrag(QPoint point);\n\tvoid finishDrag(QPoint point);\n\tvoid updatePositionAnimated();\n\tvoid updateOverState(QPoint point);\n\tvoid moveAnimated(QPoint to);\n\tvoid updateDecorations();\n\n\tconst std::unique_ptr<Ui::RpWidget> _window;\n\tstd::unique_ptr<Ui::RpWidgetWrap> _content;\n\tconst QPointer<QWidget> _parent;\n\tRectParts _attached = RectParts();\n\tRectParts _snapped = RectParts();\n\tQSize _ratio;\n\n\tbool _useTransparency = true;\n\tbool _dragDisabled = false;\n\tbool _inHandleWaylandResize = false;\n\tstyle::margins _padding;\n\n\tRectPart _overState = RectPart();\n\tstd::optional<RectPart> _pressState;\n\tQPoint _pressPoint;\n\tQRect _dragStartGeometry;\n\tstd::optional<RectPart> _dragState;\n\trpl::event_stream<> _saveGeometryRequests;\n\n\tQPoint _positionAnimationFrom;\n\tQPoint _positionAnimationTo;\n\tUi::Animations::Simple _positionAnimation;\n\n};\n\nclass Pip final {\npublic:\n\tclass Delegate {\n\tpublic:\n\t\tvirtual void pipSaveGeometry(QByteArray geometry) = 0;\n\t\t[[nodiscard]] virtual QByteArray pipLoadGeometry() = 0;\n\t\t[[nodiscard]] virtual float64 pipPlaybackSpeed() = 0;\n\t\t[[nodiscard]] virtual QWidget *pipParentWidget() = 0;\n\t};\n\n\tPip(\n\t\tnot_null<Delegate*> delegate,\n\t\tnot_null<DocumentData*> data,\n\t\tData::FileOrigin origin,\n\t\tnot_null<DocumentData*> chosenQuality,\n\t\tHistoryItem *context,\n\t\tVideoQuality quality,\n\t\tstd::shared_ptr<Streaming::Document> shared,\n\t\tFnMut<void()> closeAndContinue,\n\t\tFnMut<void()> destroy);\n\t~Pip();\n\n\t[[nodiscard]] std::shared_ptr<Streaming::Document> shared() const;\n\nprivate:\n\tenum class OverState {\n\t\tNone,\n\t\tClose,\n\t\tEnlarge,\n\t\tPlayback,\n\t\tVolumeToggle,\n\t\tVolumeController,\n\t\tOther,\n\t};\n\tenum class ThumbState {\n\t\tEmpty,\n\t\tInline,\n\t\tThumb,\n\t\tGood,\n\t\tCover,\n\t};\n\tstruct Button {\n\t\tQRect area;\n\t\tQRect icon;\n\t\tOverState state = OverState::None;\n\t\tUi::Animations::Simple active;\n\t};\n\tstruct ContentGeometry {\n\t\tQRect inner;\n\t\tRectParts attached = RectParts();\n\t\tfloat64 fade = 0.;\n\t\tQSize outer;\n\t\tint rotation = 0;\n\t\tint videoRotation = 0;\n\t\tbool useTransparency = false;\n\t};\n\tstruct StaticContent {\n\t\tQImage image;\n\t\tbool blurred = false;\n\t};\n\tusing FrameRequest = Streaming::FrameRequest;\n\tclass Renderer;\n\tclass RendererGL;\n\tclass RendererSW;\n\tclass RendererRhi;\n\n\tvoid setupPanel();\n\tvoid setupButtons();\n\tvoid setupStreaming();\n\tvoid playbackPauseResume();\n\tvoid volumeChanged(float64 volume);\n\tvoid volumeToggled();\n\tvoid volumeControllerUpdate(QPoint position);\n\tvoid waitingAnimationCallback();\n\tvoid handleStreamingUpdate(Streaming::Update &&update);\n\tvoid handleStreamingError(Streaming::Error &&error);\n\tvoid saveGeometry();\n\n\tvoid updatePlaybackState();\n\tvoid updatePowerSaveBlocker(const Player::TrackState &state);\n\tvoid updatePlayPauseResumeState(const Player::TrackState &state);\n\tvoid restartAtSeekPosition(crl::time position);\n\n\t[[nodiscard]] bool canUseVideoFrame() const;\n\t[[nodiscard]] QImage videoFrame(const FrameRequest &request) const;\n\t[[nodiscard]] Streaming::FrameWithInfo videoFrameWithInfo() const; // YUV\n\t[[nodiscard]] QImage staticContent() const;\n\t[[nodiscard]] OverState computeState(QPoint position) const;\n\tvoid setOverState(OverState state);\n\tvoid setPressedState(std::optional<OverState> state);\n\t[[nodiscard]] OverState shownActiveState() const;\n\t[[nodiscard]] float64 activeValue(const Button &button) const;\n\tvoid updateActiveState(OverState was);\n\tvoid updatePlaybackTexts(int64 position, int64 length, int64 frequency);\n\n\t[[nodiscard]] static OverState ResolveShownOver(OverState state);\n\n\t[[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer(\n\t\tUi::GL::Capabilities capabilities);\n\tvoid paint(not_null<Renderer*> renderer) const;\n\n\tvoid handleMouseMove(QPoint position);\n\tvoid handleMousePress(QPoint position, Qt::MouseButton button);\n\tvoid handleMouseRelease(QPoint position, Qt::MouseButton button);\n\tvoid handleDoubleClick(Qt::MouseButton button);\n\tvoid handleLeave();\n\tvoid handleClose();\n\n\tvoid paintRadialLoadingContent(\n\t\tQPainter &p,\n\t\tconst QRect &inner,\n\t\tQColor fg) const;\n\tvoid paintButtons(not_null<Renderer*> renderer, float64 shown) const;\n\tvoid paintPlayback(not_null<Renderer*> renderer, float64 shown) const;\n\tvoid paintPlaybackContent(QPainter &p, QRect outer, float64 shown) const;\n\tvoid paintPlaybackProgress(QPainter &p, QRect outer) const;\n\tvoid paintProgressBar(\n\t\tQPainter &p,\n\t\tconst QRect &rect,\n\t\tfloat64 progress,\n\t\tint radius,\n\t\tfloat64 active) const;\n\tvoid paintPlaybackTexts(QPainter &p, QRect outer) const;\n\tvoid paintVolumeController(\n\t\tnot_null<Renderer*> renderer,\n\t\tfloat64 shown) const;\n\tvoid paintVolumeControllerContent(\n\t\tQPainter &p,\n\t\tQRect outer,\n\t\tfloat64 shown) const;\n\t[[nodiscard]] QRect countRadialRect() const;\n\tvoid applyVideoQuality(VideoQuality value);\n\t[[nodiscard]] QImage currentVideoFrameImage() const;\n\n\tvoid seekUpdate(QPoint position);\n\tvoid seekProgress(float64 value);\n\tvoid seekFinish(float64 value);\n\n\tconst not_null<Delegate*> _delegate;\n\tconst not_null<DocumentData*> _data;\n\tconst Data::FileOrigin _origin;\n\tDocumentData *_chosenQuality = nullptr;\n\tHistoryItem *_context = nullptr;\n\tMedia::VideoQuality _quality;\n\tstd::optional<Streaming::Instance> _instance;\n\tbool _opengl = false;\n\tPipPanel _panel;\n\tQSize _size;\n\tstd::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;\n\tstd::unique_ptr<PlaybackProgress> _playbackProgress;\n\tstd::shared_ptr<Data::DocumentMedia> _dataMedia;\n\n\tQImage _qualityChangeFrame;\n\tbool _qualityChangeFinished = false;\n\tcrl::time _lastUpdatePosition = 0;\n\n\tbool _showPause = false;\n\tbool _startPaused = false;\n\tbool _pausedBySeek = false;\n\tQString _timeAlready, _timeLeft;\n\tint _timeLeftWidth = 0;\n\tint _rotation = 0;\n\tfloat64 _lastPositiveVolume = 1.;\n\tcrl::time _seekPositionMs = -1;\n\tcrl::time _lastDurationMs = 0;\n\tOverState _over = OverState::None;\n\tstd::optional<OverState> _pressed;\n\tstd::optional<OverState> _lastHandledPress;\n\tButton _close;\n\tButton _enlarge;\n\tButton _playback;\n\tButton _play;\n\tButton _volumeToggle;\n\tButton _volumeController;\n\tUi::Animations::Simple _controlsShown;\n\n\tFnMut<void()> _closeAndContinue;\n\tFnMut<void()> _destroy;\n\n\tmutable QImage _preparedCoverStorage;\n\tmutable ThumbState _preparedCoverState = ThumbState::Empty;\n\n};\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_pip_opengl.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/view/media_view_pip_opengl.h\"\n\n#include \"ui/gl/gl_shader.h\"\n#include \"ui/gl/gl_primitives.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"media/streaming/media_streaming_common.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"styles/style_media_view.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Media::View {\nnamespace {\n\nusing namespace Ui::GL;\n\nconstexpr auto kRadialLoadingOffset = 4;\nconstexpr auto kPlaybackOffset = kRadialLoadingOffset + 4;\nconstexpr auto kVolumeControllerOffset = kPlaybackOffset + 4;\nconstexpr auto kControlsOffset = kVolumeControllerOffset + 4;\nconstexpr auto kControlValues = 4 * 4 + 2 * 4;\n\n[[nodiscard]] ShaderPart FragmentAddControlOver() {\n\treturn {\n\t\t.header = R\"(\nvarying vec2 o_texcoord;\nuniform float o_opacity;\n)\",\n\t\t.body = R\"(\n\tvec4 over = texture2D(s_texture, o_texcoord);\n\tresult = result * (1. - o_opacity)\n\t\t+ vec4(over.b, over.g, over.r, over.a) * o_opacity;\n)\",\n\t};\n}\n\n[[nodiscard]] ShaderPart FragmentApplyFade() {\n\treturn {\n\t\t.header = R\"(\nuniform vec4 fadeColor; // Premultiplied.\n)\",\n\t\t.body = R\"(\n\tresult = result * (1. - fadeColor.a) + fadeColor;\n)\",\n\t};\n}\n\nShaderPart FragmentSampleShadow() {\n\treturn {\n\t\t.header = R\"(\nuniform sampler2D h_texture;\nuniform vec2 h_size;\nuniform vec4 h_extend;\nuniform vec4 h_components;\n)\",\n\t\t.body = R\"(\n\tvec4 extended = vec4( // Left-Bottom-Width-Height rectangle.\n\t\troundRect.xy - h_extend.xw,\n\t\troundRect.zw + h_extend.xw + h_extend.zy);\n\tvec2 inside = (gl_FragCoord.xy - extended.xy);\n\tvec2 insideOtherCorner = (inside + h_size - extended.zw);\n\tvec4 outsideCorners = step(\n\t\tvec4(h_components.xy, inside),\n\t\tvec4(inside, extended.zw - h_components.xy));\n\tvec4 insideCorners = vec4(1.) - outsideCorners;\n\tvec2 linear = outsideCorners.xy * outsideCorners.zw;\n\tvec2 h_size_half = 0.5 * h_size;\n\n\tvec2 bottomleft = inside * insideCorners.x * insideCorners.y;\n\tvec2 bottomright = vec2(insideOtherCorner.x, inside.y)\n\t\t* insideCorners.z\n\t\t* insideCorners.y;\n\tvec2 topright = insideOtherCorner * insideCorners.z * insideCorners.w;\n\tvec2 topleft = vec2(inside.x, insideOtherCorner.y)\n\t\t* insideCorners.x\n\t\t* insideCorners.w;\n\n\tvec2 left = vec2(inside.x, h_size_half.y)\n\t\t* step(inside.x, h_components.z)\n\t\t* linear.y;\n\tvec2 bottom = vec2(h_size_half.x, inside.y)\n\t\t* step(inside.y, h_components.w)\n\t\t* linear.x;\n\tvec2 right = vec2(insideOtherCorner.x, h_size_half.y)\n\t\t* step(h_size.x - h_components.z, insideOtherCorner.x)\n\t\t* linear.y;\n\tvec2 top = vec2(h_size_half.x, insideOtherCorner.y)\n\t\t* step(h_size.y - h_components.w, insideOtherCorner.y)\n\t\t* linear.x;\n\n\tvec2 uv = bottomleft\n\t\t+ bottomright\n\t\t+ topleft\n\t\t+ topright\n\t\t+ left\n\t\t+ bottom\n\t\t+ right\n\t\t+ top;\n\tresult = texture2D(h_texture, uv / h_size);\n)\",\n\t};\n}\n\nShaderPart FragmentRoundToShadow() {\n\tconst auto shadow = FragmentSampleShadow();\n\treturn {\n\t\t.header = R\"(\nuniform vec4 roundRect;\nuniform float roundRadius;\n)\" + shadow.header + R\"(\n\nfloat roundedCorner() {\n\tvec2 rectHalf = roundRect.zw / 2.;\n\tvec2 rectCenter = roundRect.xy + rectHalf;\n\tvec2 fromRectCenter = abs(gl_FragCoord.xy - rectCenter);\n\tvec2 vectorRadius = vec2(roundRadius + 0.5, roundRadius + 0.5);\n\tvec2 fromCenterWithRadius = fromRectCenter + vectorRadius;\n\tvec2 fromRoundingCenter = max(fromCenterWithRadius, rectHalf)\n\t\t- rectHalf;\n\tfloat rounded = length(fromRoundingCenter) - roundRadius;\n\n\treturn 1. - smoothstep(0., 1., rounded);\n}\n\nvec4 shadow() {\n\tvec4 result;\n\n)\" + shadow.body + R\"(\n\n\treturn result;\n}\n)\",\n\t\t.body = R\"(\n\tfloat round = roundedCorner();\n\tresult = result * round + shadow() * (1. - round);\n)\",\n\t};\n}\n\n} // namespace\n\nPip::RendererGL::RendererGL(not_null<Pip*> owner)\n: _owner(owner) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_radialImage.invalidate();\n\t\t_playbackImage.invalidate();\n\t\t_volumeControllerImage.invalidate();\n\t\tinvalidateControls();\n\t}, _lifetime);\n}\n\nvoid Pip::RendererGL::init(QOpenGLFunctions &f) {\n\tconstexpr auto kQuads = 8;\n\tconstexpr auto kQuadVertices = kQuads * 4;\n\tconstexpr auto kQuadValues = kQuadVertices * 4;\n\tconstexpr auto kControlsValues = kControlsCount * kControlValues;\n\tconstexpr auto kValues = kQuadValues + kControlsValues;\n\n\t_contentBuffer.emplace();\n\t_contentBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw);\n\t_contentBuffer->create();\n\t_contentBuffer->bind();\n\t_contentBuffer->allocate(kValues * sizeof(GLfloat));\n\n\t_textures.ensureCreated(f);\n\n\t_argb32Program.emplace();\n\t_texturedVertexShader = LinkProgram(\n\t\t&*_argb32Program,\n\t\tVertexShader({\n\t\t\tVertexPassTextureCoord(),\n\t\t}),\n\t\tFragmentShader({\n\t\t\tFragmentSampleARGB32Texture(),\n\t\t\tFragmentApplyFade(),\n\t\t\tFragmentRoundToShadow(),\n\t\t})).vertex;\n\n\t_yuv420Program.emplace();\n\tLinkProgram(\n\t\t&*_yuv420Program,\n\t\t_texturedVertexShader,\n\t\tFragmentShader({\n\t\t\tFragmentSampleYUV420Texture(),\n\t\t\tFragmentApplyFade(),\n\t\t\tFragmentRoundToShadow(),\n\t\t}));\n\n\t_nv12Program.emplace();\n\tLinkProgram(\n\t\t&*_nv12Program,\n\t\t_texturedVertexShader,\n\t\tFragmentShader({\n\t\t\tFragmentSampleNV12Texture(),\n\t\t\tFragmentApplyFade(),\n\t\t\tFragmentRoundToShadow(),\n\t\t}));\n\n\t_imageProgram.emplace();\n\tLinkProgram(\n\t\t&*_imageProgram,\n\t\tVertexShader({\n\t\t\tVertexViewportTransform(),\n\t\t\tVertexPassTextureCoord(),\n\t\t}),\n\t\tFragmentShader({\n\t\t\tFragmentSampleARGB32Texture(),\n\t\t}));\n\n\t_controlsProgram.emplace();\n\tLinkProgram(\n\t\t&*_controlsProgram,\n\t\tVertexShader({\n\t\t\tVertexViewportTransform(),\n\t\t\tVertexPassTextureCoord(),\n\t\t\tVertexPassTextureCoord('o'),\n\t\t}),\n\t\tFragmentShader({\n\t\t\tFragmentSampleARGB32Texture(),\n\t\t\tFragmentAddControlOver(),\n\t\t\tFragmentGlobalOpacity(),\n\t\t}));\n\n\tcreateShadowTexture();\n}\n\nvoid Pip::RendererGL::deinit(QOpenGLFunctions *f) {\n\t_radialImage.destroy(f);\n\t_controlsImage.destroy(f);\n\t_playbackImage.destroy(f);\n\t_volumeControllerImage.destroy(f);\n\t_shadowImage.destroy(f);\n\t_textures.destroy(f);\n\t_imageProgram = std::nullopt;\n\t_texturedVertexShader = nullptr;\n\t_argb32Program = std::nullopt;\n\t_yuv420Program = std::nullopt;\n\t_nv12Program = std::nullopt;\n\t_controlsProgram = std::nullopt;\n\t_contentBuffer = std::nullopt;\n}\n\nvoid Pip::RendererGL::createShadowTexture() {\n\tconst auto &shadow = PipShadow();\n\tconst auto size = 2 * PipShadow().topLeft.size()\n\t\t+ QSize(st::roundRadiusLarge, st::roundRadiusLarge);\n\tauto image = QImage(\n\t\tsize * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\timage.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&image);\n\t\tUi::Shadow::paint(\n\t\t\tp,\n\t\t\tQRect(QPoint(), size).marginsRemoved(shadow.extend),\n\t\t\tsize.width(),\n\t\t\tshadow);\n\t}\n\t_shadowImage.setImage(std::move(image));\n}\n\nvoid Pip::RendererGL::paint(\n\t\tnot_null<QOpenGLWidget*> widget,\n\t\tQOpenGLFunctions &f) {\n\tconst auto factor = widget->devicePixelRatioF();\n\tif (_factor != factor) {\n\t\t_factor = factor;\n\t\t_ifactor = int(std::ceil(_factor));\n\t\t_controlsImage.invalidate();\n\t}\n\t_blendingEnabled = false;\n\t_viewport = widget->size();\n\t_uniformViewport = QVector2D(\n\t\t_viewport.width() * _factor,\n\t\t_viewport.height() * _factor);\n\t_f = &f;\n\t_owner->paint(this);\n\t_f = nullptr;\n}\n\nstd::optional<QColor> Pip::RendererGL::clearColor() {\n\treturn QColor(0, 0, 0, 0);\n}\n\nvoid Pip::RendererGL::paintTransformedVideoFrame(\n\t\tContentGeometry geometry) {\n\tconst auto data = _owner->videoFrameWithInfo();\n\tif (data.format == Streaming::FrameFormat::None) {\n\t\treturn;\n\t}\n\tgeometry.rotation = (geometry.rotation + geometry.videoRotation) % 360;\n\tif (data.format == Streaming::FrameFormat::ARGB32) {\n\t\tAssert(!data.image.isNull());\n\t\tpaintTransformedStaticContent(data.image, geometry);\n\t\treturn;\n\t}\n\tAssert(!data.yuv->size.isEmpty());\n\tconst auto program = (data.format == Streaming::FrameFormat::NV12)\n\t\t? &*_nv12Program\n\t\t: &*_yuv420Program;\n\tprogram->bind();\n\tconst auto nv12 = (data.format == Streaming::FrameFormat::NV12);\n\tconst auto yuv = data.yuv;\n\tconst auto nv12changed = (_chromaNV12 != nv12);\n\n\tconst auto upload = (_trackFrameIndex != data.index);\n\t_trackFrameIndex = data.index;\n\n\t_f->glActiveTexture(GL_TEXTURE0);\n\t_textures.bind(*_f, 1);\n\tif (upload) {\n\t\t_f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1);\n\t\tuploadTexture(\n\t\t\tGL_ALPHA,\n\t\t\tGL_ALPHA,\n\t\t\tyuv->size,\n\t\t\t_lumaSize,\n\t\t\tyuv->y.stride,\n\t\t\tyuv->y.data);\n\t\t_lumaSize = yuv->size;\n\t}\n\t_f->glActiveTexture(GL_TEXTURE1);\n\t_textures.bind(*_f, 2);\n\tif (upload) {\n\t\tuploadTexture(\n\t\t\tnv12 ? GL_RG : GL_ALPHA,\n\t\t\tnv12 ? GL_RG : GL_ALPHA,\n\t\t\tyuv->chromaSize,\n\t\t\tnv12changed ? QSize() : _chromaSize,\n\t\t\tyuv->u.stride / (nv12 ? 2 : 1),\n\t\t\tyuv->u.data);\n\t\tif (nv12) {\n\t\t\t_chromaSize = yuv->chromaSize;\n\t\t\t_f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4);\n\t\t}\n\t\t_chromaNV12 = nv12;\n\t}\n\tif (!nv12) {\n\t\t_f->glActiveTexture(GL_TEXTURE2);\n\t\t_textures.bind(*_f, 3);\n\t\tif (upload) {\n\t\t\tuploadTexture(\n\t\t\t\tGL_ALPHA,\n\t\t\t\tGL_ALPHA,\n\t\t\t\tyuv->chromaSize,\n\t\t\t\t_chromaSize,\n\t\t\t\tyuv->v.stride,\n\t\t\t\tyuv->v.data);\n\t\t\t_chromaSize = yuv->chromaSize;\n\t\t\t_f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4);\n\t\t}\n\t}\n\tprogram->setUniformValue(\"y_texture\", GLint(0));\n\tif (nv12) {\n\t\tprogram->setUniformValue(\"uv_texture\", GLint(1));\n\t} else {\n\t\tprogram->setUniformValue(\"u_texture\", GLint(1));\n\t\tprogram->setUniformValue(\"v_texture\", GLint(2));\n\t}\n\n\tpaintTransformedContent(program, geometry);\n}\n\nvoid Pip::RendererGL::paintTransformedStaticContent(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry) {\n\t_argb32Program->bind();\n\n\t_f->glActiveTexture(GL_TEXTURE0);\n\t_textures.bind(*_f, 0);\n\tconst auto cacheKey = image.cacheKey();\n\tconst auto upload = (_cacheKey != cacheKey);\n\tif (upload) {\n\t\t_cacheKey = cacheKey;\n\t\tconst auto stride = image.bytesPerLine() / 4;\n\t\tconst auto data = image.constBits();\n\t\tuploadTexture(\n\t\t\tUi::GL::kFormatRGBA,\n\t\t\tUi::GL::kFormatRGBA,\n\t\t\timage.size(),\n\t\t\t_rgbaSize,\n\t\t\tstride,\n\t\t\tdata);\n\t\t_rgbaSize = image.size();\n\t}\n\t_argb32Program->setUniformValue(\"s_texture\", GLint(0));\n\n\tpaintTransformedContent(&*_argb32Program, geometry);\n}\n\nvoid Pip::RendererGL::paintTransformedContent(\n\t\tnot_null<QOpenGLShaderProgram*> program,\n\t\tContentGeometry geometry) {\n\tstd::array<std::array<GLfloat, 2>, 4> rect = { {\n\t\t{ { -1.f, 1.f } },\n\t\t{ { 1.f, 1.f } },\n\t\t{ { 1.f, -1.f } },\n\t\t{ { -1.f, -1.f } },\n\t} };\n\tif (const auto shift = (geometry.rotation / 90); shift != 0) {\n\t\tstd::rotate(begin(rect), begin(rect) + shift, end(rect));\n\t}\n\tconst auto xscale = 1.f / geometry.inner.width();\n\tconst auto yscale = 1.f / geometry.inner.height();\n\tconst GLfloat coords[] = {\n\t\trect[0][0], rect[0][1],\n\t\t-geometry.inner.x() * xscale,\n\t\t-geometry.inner.y() * yscale,\n\n\t\trect[1][0], rect[1][1],\n\t\t(geometry.outer.width() - geometry.inner.x()) * xscale,\n\t\t-geometry.inner.y() * yscale,\n\n\t\trect[2][0], rect[2][1],\n\t\t(geometry.outer.width() - geometry.inner.x()) * xscale,\n\t\t(geometry.outer.height() - geometry.inner.y()) * yscale,\n\n\t\trect[3][0], rect[3][1],\n\t\t-geometry.inner.x() * xscale,\n\t\t(geometry.outer.height() - geometry.inner.y()) * yscale,\n\t};\n\n\t_contentBuffer->bind();\n\t_contentBuffer->write(0, coords, sizeof(coords));\n\n\tconst auto rgbaFrame = _chromaSize.isEmpty();\n\t_f->glActiveTexture(rgbaFrame ? GL_TEXTURE1 : GL_TEXTURE3);\n\t_shadowImage.bind(*_f);\n\n\tconst auto globalFactor = style::DevicePixelRatio();\n\tconst auto fadeAlpha = st::radialBg->c.alphaF() * geometry.fade;\n\tconst auto roundRect = transformRect(RoundingRect(geometry));\n\tprogram->setUniformValue(\"roundRect\", Uniform(roundRect));\n\tprogram->setUniformValue(\"h_texture\", GLint(rgbaFrame ? 1 : 3));\n\tprogram->setUniformValue(\"h_size\", QSizeF(_shadowImage.image().size()));\n\tprogram->setUniformValue(\"h_extend\", QVector4D(\n\t\tPipShadow().extend.left() * globalFactor,\n\t\tPipShadow().extend.top() * globalFactor,\n\t\tPipShadow().extend.right() * globalFactor,\n\t\tPipShadow().extend.bottom() * globalFactor));\n\tprogram->setUniformValue(\"h_components\", QVector4D(\n\t\tfloat(PipShadow().topLeft.width() * globalFactor),\n\t\tfloat(PipShadow().topLeft.height() * globalFactor),\n\t\tfloat(PipShadow().left.width() * globalFactor),\n\t\tfloat(PipShadow().top.height() * globalFactor)));\n\tprogram->setUniformValue(\n\t\t\"roundRadius\",\n\t\tGLfloat(st::roundRadiusLarge * _factor));\n\tprogram->setUniformValue(\"fadeColor\", QVector4D(\n\t\tfloat(st::radialBg->c.redF() * fadeAlpha),\n\t\tfloat(st::radialBg->c.greenF() * fadeAlpha),\n\t\tfloat(st::radialBg->c.blueF() * fadeAlpha),\n\t\tfloat(fadeAlpha)));\n\n\tFillTexturedRectangle(*_f, &*program);\n}\n\nvoid Pip::RendererGL::uploadTexture(\n\t\tGLint internalformat,\n\t\tGLint format,\n\t\tQSize size,\n\t\tQSize hasSize,\n\t\tint stride,\n\t\tconst void *data) const {\n\t_f->glPixelStorei(GL_UNPACK_ROW_LENGTH, stride);\n\tif (hasSize != size) {\n\t\t_f->glTexImage2D(\n\t\t\tGL_TEXTURE_2D,\n\t\t\t0,\n\t\t\tinternalformat,\n\t\t\tsize.width(),\n\t\t\tsize.height(),\n\t\t\t0,\n\t\t\tformat,\n\t\t\tGL_UNSIGNED_BYTE,\n\t\t\tdata);\n\t} else {\n\t\t_f->glTexSubImage2D(\n\t\t\tGL_TEXTURE_2D,\n\t\t\t0,\n\t\t\t0,\n\t\t\t0,\n\t\t\tsize.width(),\n\t\t\tsize.height(),\n\t\t\tformat,\n\t\t\tGL_UNSIGNED_BYTE,\n\t\t\tdata);\n\t}\n\t_f->glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);\n}\n\nvoid Pip::RendererGL::paintRadialLoading(\n\t\tQRect inner,\n\t\tfloat64 controlsShown) {\n\tpaintUsingRaster(_radialImage, inner, [&](QPainter &&p) {\n\t\t// Raster renderer paints content, then radial loading, then fade.\n\t\t// Here we paint fade together with the content, so we should emulate\n\t\t// radial loading being under the fade.\n\t\t//\n\t\t// The loading background is the same color as the fade (radialBg),\n\t\t// so nothing should be done with it. But the fade should be added\n\t\t// to the radial loading line color (radialFg).\n\t\tconst auto newInner = QRect(QPoint(), inner.size());\n\t\tconst auto fg = st::radialFg->c;\n\t\tconst auto fade = st::radialBg->c;\n\t\tconst auto fadeAlpha = controlsShown * fade.alphaF();\n\t\tconst auto fgAlpha = 1. - fadeAlpha;\n\t\tconst auto color = (fadeAlpha == 0.) ? fg : QColor(\n\t\t\tint(base::SafeRound(fg.red() * fgAlpha + fade.red() * fadeAlpha)),\n\t\t\tint(base::SafeRound(fg.green() * fgAlpha + fade.green() * fadeAlpha)),\n\t\t\tint(base::SafeRound(fg.blue() * fgAlpha + fade.blue() * fadeAlpha)),\n\t\t\tfg.alpha());\n\n\t\t_owner->paintRadialLoadingContent(p, newInner, color);\n\t}, kRadialLoadingOffset, true);\n}\n\nvoid Pip::RendererGL::paintPlayback(QRect outer, float64 shown) {\n\tpaintUsingRaster(_playbackImage, outer, [&](QPainter &&p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintPlaybackContent(p, newOuter, shown);\n\t}, kPlaybackOffset, true);\n}\n\nvoid Pip::RendererGL::paintVolumeController(QRect outer, float64 shown) {\n\tpaintUsingRaster(_volumeControllerImage, outer, [&](QPainter &&p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintVolumeControllerContent(p, newOuter, shown);\n\t}, kVolumeControllerOffset, true);\n}\n\nvoid Pip::RendererGL::paintButtonsStart() {\n\tvalidateControls();\n\t_f->glActiveTexture(GL_TEXTURE0);\n\t_controlsImage.bind(*_f);\n\ttoggleBlending(true);\n}\n\nvoid Pip::RendererGL::paintButton(\n\t\tconst Button &button,\n\t\tint outerWidth,\n\t\tfloat64 shown,\n\t\tfloat64 over,\n\t\tconst style::icon &icon,\n\t\tconst style::icon &iconOver) {\n\tconst auto tryIndex = [&](int stateIndex) -> std::optional<Control> {\n\t\tconst auto result = ControlMeta(button.state, stateIndex);\n\t\treturn (result.icon == &icon && result.iconOver == &iconOver)\n\t\t\t? std::make_optional(result)\n\t\t\t: std::nullopt;\n\t};\n\tconst auto meta = tryIndex(0)\n\t\t? *tryIndex(0)\n\t\t: tryIndex(1)\n\t\t? *tryIndex(1)\n\t\t: *tryIndex(2);\n\tAssert(meta.icon == &icon && meta.iconOver == &iconOver);\n\n\tconst auto offset = kControlsOffset + (meta.index * kControlValues) / 4;\n\tconst auto iconRect = _controlsImage.texturedRect(\n\t\tbutton.icon,\n\t\t_controlsTextures[meta.index * 2 + 0]);\n\tconst auto iconOverRect = _controlsImage.texturedRect(\n\t\tbutton.icon,\n\t\t_controlsTextures[meta.index * 2 + 1]);\n\tconst auto iconGeometry = transformRect(iconRect.geometry);\n\tconst GLfloat coords[] = {\n\t\ticonGeometry.left(), iconGeometry.top(),\n\t\ticonRect.texture.left(), iconRect.texture.bottom(),\n\n\t\ticonGeometry.right(), iconGeometry.top(),\n\t\ticonRect.texture.right(), iconRect.texture.bottom(),\n\n\t\ticonGeometry.right(), iconGeometry.bottom(),\n\t\ticonRect.texture.right(), iconRect.texture.top(),\n\n\t\ticonGeometry.left(), iconGeometry.bottom(),\n\t\ticonRect.texture.left(), iconRect.texture.top(),\n\n\t\ticonOverRect.texture.left(), iconOverRect.texture.bottom(),\n\t\ticonOverRect.texture.right(), iconOverRect.texture.bottom(),\n\t\ticonOverRect.texture.right(), iconOverRect.texture.top(),\n\t\ticonOverRect.texture.left(), iconOverRect.texture.top(),\n\t};\n\t_contentBuffer->bind();\n\t_contentBuffer->write(\n\t\toffset * 4 * sizeof(GLfloat),\n\t\tcoords,\n\t\tsizeof(coords));\n\t_controlsProgram->bind();\n\t_controlsProgram->setUniformValue(\"o_opacity\", GLfloat(over));\n\t_controlsProgram->setUniformValue(\"g_opacity\", GLfloat(shown));\n\t_controlsProgram->setUniformValue(\"viewport\", _uniformViewport);\n\n\tGLint overTexcoord = _controlsProgram->attributeLocation(\"o_texcoordIn\");\n\t_f->glVertexAttribPointer(\n\t\toverTexcoord,\n\t\t2,\n\t\tGL_FLOAT,\n\t\tGL_FALSE,\n\t\t2 * sizeof(GLfloat),\n\t\treinterpret_cast<const void*>((offset + 4) * 4 * sizeof(GLfloat)));\n\t_f->glEnableVertexAttribArray(overTexcoord);\n\tFillTexturedRectangle(*_f, &*_controlsProgram, offset);\n\t_f->glDisableVertexAttribArray(overTexcoord);\n}\n\nauto Pip::RendererGL::ControlMeta(OverState control, int index)\n-> Control {\n\tExpects(index >= 0);\n\n\tswitch (control) {\n\tcase OverState::Close: Assert(index < 1); return {\n\t\t0,\n\t\t&st::pipCloseIcon,\n\t\t&st::pipCloseIconOver,\n\t};\n\tcase OverState::Enlarge: Assert(index < 1); return {\n\t\t1,\n\t\t&st::pipEnlargeIcon,\n\t\t&st::pipEnlargeIconOver,\n\t};\n\tcase OverState::VolumeToggle: Assert(index < 3); return {\n\t\t(2 + index),\n\t\t(index == 0\n\t\t\t? &st::pipVolumeIcon0\n\t\t\t: (index == 1)\n\t\t\t? &st::pipVolumeIcon1\n\t\t\t: &st::pipVolumeIcon2),\n\t\t(index == 0\n\t\t\t? &st::pipVolumeIcon0Over\n\t\t\t: (index == 1)\n\t\t\t? &st::pipVolumeIcon1Over\n\t\t\t: &st::pipVolumeIcon2Over),\n\t};\n\tcase OverState::Other: Assert(index < 2); return {\n\t\t(5 + index),\n\t\t(index ? &st::pipPauseIcon : &st::pipPlayIcon),\n\t\t(index ? &st::pipPauseIconOver : &st::pipPlayIconOver),\n\t};\n\t}\n\tUnexpected(\"Control value in Pip::RendererGL::ControlIndex.\");\n}\n\nvoid Pip::RendererGL::validateControls() {\n\tif (!_controlsImage.image().isNull()) {\n\t\treturn;\n\t}\n\tconst auto metas = {\n\t\tControlMeta(OverState::Close),\n\t\tControlMeta(OverState::Enlarge),\n\t\tControlMeta(OverState::VolumeToggle),\n\t\tControlMeta(OverState::VolumeToggle, 1),\n\t\tControlMeta(OverState::VolumeToggle, 2),\n\t\tControlMeta(OverState::Other),\n\t\tControlMeta(OverState::Other, 1),\n\t};\n\tauto maxWidth = 0;\n\tauto fullHeight = 0;\n\tfor (const auto &meta : metas) {\n\t\tAssert(meta.icon->size() == meta.iconOver->size());\n\t\tmaxWidth = std::max(meta.icon->width(), maxWidth);\n\t\tfullHeight += 2 * meta.icon->height();\n\t}\n\tauto image = QImage(\n\t\tQSize(maxWidth, fullHeight) * _ifactor,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.fill(Qt::transparent);\n\timage.setDevicePixelRatio(_ifactor);\n\t{\n\t\tauto p = QPainter(&image);\n\t\tauto index = 0;\n\t\tauto height = 0;\n\t\tconst auto paint = [&](not_null<const style::icon*> icon) {\n\t\t\ticon->paint(p, 0, height, maxWidth);\n\t\t\t_controlsTextures[index++] = QRect(\n\t\t\t\tQPoint(0, height) * _ifactor,\n\t\t\t\ticon->size() * _ifactor);\n\t\t\theight += icon->height();\n\t\t};\n\t\tfor (const auto &meta : metas) {\n\t\t\tpaint(meta.icon);\n\t\t\tpaint(meta.iconOver);\n\t\t}\n\t}\n\t_controlsImage.setImage(std::move(image));\n}\n\nvoid Pip::RendererGL::invalidateControls() {\n\t_controlsImage.invalidate();\n\tranges::fill(_controlsTextures, QRect());\n}\n\nvoid Pip::RendererGL::paintUsingRaster(\n\t\tUi::GL::Image &image,\n\t\tQRect rect,\n\t\tFn<void(QPainter&&)> method,\n\t\tint bufferOffset,\n\t\tbool transparent) {\n\tauto raster = image.takeImage();\n\tconst auto size = rect.size() * _ifactor;\n\tif (raster.width() < size.width() || raster.height() < size.height()) {\n\t\traster = QImage(size, QImage::Format_ARGB32_Premultiplied);\n\t\traster.setDevicePixelRatio(_factor);\n\t\tif (!transparent\n\t\t\t&& (raster.width() > size.width()\n\t\t\t\t|| raster.height() > size.height())) {\n\t\t\traster.fill(Qt::transparent);\n\t\t}\n\t} else if (raster.devicePixelRatio() != _ifactor) {\n\t\traster.setDevicePixelRatio(_ifactor);\n\t}\n\n\tif (transparent) {\n\t\traster.fill(Qt::transparent);\n\t}\n\tmethod(QPainter(&raster));\n\n\t_f->glActiveTexture(GL_TEXTURE0);\n\n\timage.setImage(std::move(raster), size);\n\timage.bind(*_f);\n\n\tconst auto textured = image.texturedRect(rect, QRect(QPoint(), size));\n\tconst auto geometry = transformRect(textured.geometry);\n\tconst GLfloat coords[] = {\n\t\tgeometry.left(), geometry.top(),\n\t\ttextured.texture.left(), textured.texture.bottom(),\n\n\t\tgeometry.right(), geometry.top(),\n\t\ttextured.texture.right(), textured.texture.bottom(),\n\n\t\tgeometry.right(), geometry.bottom(),\n\t\ttextured.texture.right(), textured.texture.top(),\n\n\t\tgeometry.left(), geometry.bottom(),\n\t\ttextured.texture.left(), textured.texture.top(),\n\t};\n\t_contentBuffer->bind();\n\t_contentBuffer->write(\n\t\tbufferOffset * 4 * sizeof(GLfloat),\n\t\tcoords,\n\t\tsizeof(coords));\n\n\t_imageProgram->bind();\n\t_imageProgram->setUniformValue(\"viewport\", _uniformViewport);\n\t_imageProgram->setUniformValue(\"s_texture\", GLint(0));\n\t_imageProgram->setUniformValue(\"g_opacity\", GLfloat(1));\n\n\ttoggleBlending(transparent);\n\tFillTexturedRectangle(*_f, &*_imageProgram, bufferOffset);\n}\n\nvoid Pip::RendererGL::toggleBlending(bool enabled) {\n\tif (_blendingEnabled == enabled) {\n\t\treturn;\n\t} else if (enabled) {\n\t\t_f->glEnable(GL_BLEND);\n\t\t_f->glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);\n\t} else {\n\t\t_f->glDisable(GL_BLEND);\n\t}\n\t_blendingEnabled = enabled;\n}\n\nQRect Pip::RendererGL::RoundingRect(ContentGeometry geometry) {\n\tconst auto inner = geometry.inner;\n\tconst auto attached = geometry.attached;\n\tconst auto added = std::max({\n\t\tst::roundRadiusLarge,\n\t\tinner.x(),\n\t\tinner.y(),\n\t\tgeometry.outer.width() - inner.x() - inner.width(),\n\t\tgeometry.outer.height() - inner.y() - inner.height(),\n\t\tPipShadow().topLeft.width(),\n\t\tPipShadow().topLeft.height(),\n\t\tPipShadow().topRight.width(),\n\t\tPipShadow().topRight.height(),\n\t\tPipShadow().bottomRight.width(),\n\t\tPipShadow().bottomRight.height(),\n\t\tPipShadow().bottomLeft.width(),\n\t\tPipShadow().bottomLeft.height(),\n\t});\n\treturn geometry.inner.marginsAdded({\n\t\t(attached & RectPart::Left) ? added : 0,\n\t\t(attached & RectPart::Top) ? added : 0,\n\t\t(attached & RectPart::Right) ? added : 0,\n\t\t(attached & RectPart::Bottom) ? added : 0,\n\t});\n}\n\nRect Pip::RendererGL::transformRect(const Rect &raster) const {\n\treturn TransformRect(raster, _viewport, _factor);\n}\n\nRect Pip::RendererGL::transformRect(const QRectF &raster) const {\n\treturn TransformRect(raster, _viewport, _factor);\n}\n\nRect Pip::RendererGL::transformRect(const QRect &raster) const {\n\treturn TransformRect(Rect(raster), _viewport, _factor);\n}\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_pip_opengl.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/view/media_view_pip_renderer.h\"\n#include \"ui/gl/gl_image.h\"\n#include \"ui/gl/gl_primitives.h\"\n\n#include <QOpenGLBuffer>\n\nnamespace Media::View {\n\nclass Pip::RendererGL final : public Pip::Renderer {\npublic:\n\texplicit RendererGL(not_null<Pip*> owner);\n\n\tvoid init(QOpenGLFunctions &f) override;\n\n\tvoid deinit(QOpenGLFunctions *f) override;\n\n\tvoid paint(\n\t\tnot_null<QOpenGLWidget*> widget,\n\t\tQOpenGLFunctions &f) override;\n\n\tstd::optional<QColor> clearColor() override;\n\nprivate:\n\tstruct Control {\n\t\tint index = -1;\n\t\tnot_null<const style::icon*> icon;\n\t\tnot_null<const style::icon*> iconOver;\n\t};\n\tvoid createShadowTexture();\n\n\tvoid paintTransformedVideoFrame(ContentGeometry geometry) override;\n\tvoid paintTransformedStaticContent(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry) override;\n\tvoid paintTransformedContent(\n\t\tnot_null<QOpenGLShaderProgram*> program,\n\t\tContentGeometry geometry);\n\tvoid paintRadialLoading(\n\t\tQRect inner,\n\t\tfloat64 controlsShown) override;\n\tvoid paintButtonsStart() override;\n\tvoid paintButton(\n\t\tconst Button &button,\n\t\tint outerWidth,\n\t\tfloat64 shown,\n\t\tfloat64 over,\n\t\tconst style::icon &icon,\n\t\tconst style::icon &iconOver) override;\n\tvoid paintPlayback(QRect outer, float64 shown) override;\n\tvoid paintVolumeController(QRect outer, float64 shown) override;\n\n\tvoid paintUsingRaster(\n\t\tUi::GL::Image &image,\n\t\tQRect rect,\n\t\tFn<void(QPainter&&)> method,\n\t\tint bufferOffset,\n\t\tbool transparent = false);\n\n\tvoid validateControls();\n\tvoid invalidateControls();\n\tvoid toggleBlending(bool enabled);\n\n\t[[nodiscard]] QRect RoundingRect(ContentGeometry geometry);\n\n\t[[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const;\n\t[[nodiscard]] Ui::GL::Rect transformRect(const QRectF &raster) const;\n\t[[nodiscard]] Ui::GL::Rect transformRect(\n\t\tconst Ui::GL::Rect &raster) const;\n\n\tvoid uploadTexture(\n\t\tGLint internalformat,\n\t\tGLint format,\n\t\tQSize size,\n\t\tQSize hasSize,\n\t\tint stride,\n\t\tconst void *data) const;\n\n\tconst not_null<Pip*> _owner;\n\n\tQOpenGLFunctions *_f = nullptr;\n\tQSize _viewport;\n\tfloat _factor = 1.;\n\tint _ifactor = 1;\n\tQVector2D _uniformViewport;\n\n\tstd::optional<QOpenGLBuffer> _contentBuffer;\n\tstd::optional<QOpenGLShaderProgram> _imageProgram;\n\tstd::optional<QOpenGLShaderProgram> _controlsProgram;\n\tQOpenGLShader *_texturedVertexShader = nullptr;\n\tstd::optional<QOpenGLShaderProgram> _argb32Program;\n\tstd::optional<QOpenGLShaderProgram> _yuv420Program;\n\tstd::optional<QOpenGLShaderProgram> _nv12Program;\n\tUi::GL::Textures<4> _textures;\n\tQSize _rgbaSize;\n\tQSize _lumaSize;\n\tQSize _chromaSize;\n\tquint64 _cacheKey = 0;\n\tint _trackFrameIndex = 0;\n\tbool _chromaNV12 = false;\n\n\tUi::GL::Image _radialImage;\n\tUi::GL::Image _controlsImage;\n\tUi::GL::Image _playbackImage;\n\tUi::GL::Image _volumeControllerImage;\n\tUi::GL::Image _shadowImage;\n\n\tstatic constexpr auto kControlsCount = 7;\n\t[[nodiscard]] static Control ControlMeta(\n\t\tOverState control,\n\t\tint index = 0);\n\tstd::array<QRect, kControlsCount * 2> _controlsTextures;\n\n\tbool _blendingEnabled = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_pip_raster.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/view/media_view_pip_raster.h\"\n\n#include \"ui/image/image_prepare.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_media_view.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Media::View {\nnamespace {\n\n[[nodiscard]] Streaming::FrameRequest UnrotateRequest(\n\t\tconst Streaming::FrameRequest &request,\n\t\tint rotation) {\n\tif (!rotation) {\n\t\treturn request;\n\t}\n\tconst auto unrotatedCorner = [&](int index) {\n\t\tusing namespace Images;\n\t\tswitch (index) {\n\t\tcase kTopLeft:\n\t\t\treturn (rotation == 90)\n\t\t\t\t? kBottomLeft\n\t\t\t\t: (rotation == 180)\n\t\t\t\t? kBottomRight\n\t\t\t\t: kTopRight;\n\t\tcase kTopRight:\n\t\t\treturn (rotation == 90)\n\t\t\t\t? kTopLeft\n\t\t\t\t: (rotation == 180)\n\t\t\t\t? kBottomLeft\n\t\t\t\t: kBottomRight;\n\t\tcase kBottomRight:\n\t\t\treturn (rotation == 90)\n\t\t\t\t? kTopRight\n\t\t\t\t: (rotation == 180)\n\t\t\t\t? kTopLeft\n\t\t\t\t: kBottomLeft;\n\t\tcase kBottomLeft:\n\t\t\treturn (rotation == 90)\n\t\t\t\t? kBottomRight\n\t\t\t\t: (rotation == 180)\n\t\t\t\t? kTopRight\n\t\t\t\t: kTopLeft;\n\t\t}\n\t\tUnexpected(\"Corner in rotateCorner.\");\n\t};\n\tauto result = request;\n\tresult.outer = FlipSizeByRotation(request.outer, rotation);\n\tresult.resize = FlipSizeByRotation(request.resize, rotation);\n\tauto rounding = result.rounding;\n\tfor (auto i = 0; i != 4; ++i) {\n\t\tresult.rounding.p[unrotatedCorner(i)] = rounding.p[i];\n\t}\n\treturn result;\n}\n\n} // namespace\n\nPip::RendererSW::RendererSW(not_null<Pip*> owner)\n: _owner(owner)\n, _roundRect(ImageRoundRadius::Large, st::radialBg) {\n}\n\nvoid Pip::RendererSW::paintFallback(\n\t\tPainter &p,\n\t\tconst QRegion &clip,\n\t\tUi::GL::Backend backend) {\n\t_p = &p;\n\t_clip = &clip;\n\t_clipOuter = clip.boundingRect();\n\t_owner->paint(this);\n\t_p = nullptr;\n\t_clip = nullptr;\n}\n\nvoid Pip::RendererSW::paintTransformedVideoFrame(\n\t\tContentGeometry geometry) {\n\tpaintTransformedImage(\n\t\t_owner->videoFrame(frameRequest(geometry)),\n\t\tgeometry);\n}\n\nvoid Pip::RendererSW::paintTransformedStaticContent(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry) {\n\tpaintTransformedImage(\n\t\tstaticContentByRequest(image, frameRequest(geometry)),\n\t\tgeometry);\n}\n\nvoid Pip::RendererSW::paintFade(ContentGeometry geometry) const {\n\tusing Part = RectPart;\n\tconst auto sides = geometry.attached;\n\tconst auto rounded = RectPart(0)\n\t\t| ((sides & (Part::Top | Part::Left)) ? Part(0) : Part::TopLeft)\n\t\t| ((sides & (Part::Top | Part::Right)) ? Part(0) : Part::TopRight)\n\t\t| ((sides & (Part::Bottom | Part::Right))\n\t\t\t? Part(0)\n\t\t\t: Part::BottomRight)\n\t\t| ((sides & (Part::Bottom | Part::Left))\n\t\t\t? Part(0)\n\t\t\t: Part::BottomLeft);\n\t_roundRect.paintSomeRounded(\n\t\t*_p,\n\t\tgeometry.inner,\n\t\trounded | Part::NoTopBottom | Part::Top | Part::Bottom);\n}\n\nvoid Pip::RendererSW::paintButtonsStart() {\n}\n\nvoid Pip::RendererSW::paintButton(\n\t\tconst Button &button,\n\t\tint outerWidth,\n\t\tfloat64 shown,\n\t\tfloat64 over,\n\t\tconst style::icon &icon,\n\t\tconst style::icon &iconOver) {\n\tif (over < 1.) {\n\t\t_p->setOpacity(shown);\n\t\ticon.paint(*_p, button.icon.x(), button.icon.y(), outerWidth);\n\t}\n\tif (over > 0.) {\n\t\t_p->setOpacity(over * shown);\n\t\ticonOver.paint(*_p, button.icon.x(), button.icon.y(), outerWidth);\n\t}\n}\n\nPip::FrameRequest Pip::RendererSW::frameRequest(\n\t\tContentGeometry geometry) const {\n\tusing namespace Images;\n\tauto result = FrameRequest();\n\tresult.outer = geometry.inner.size() * style::DevicePixelRatio();\n\tresult.resize = result.outer;\n\tresult.rounding = CornersMaskRef(CornersMask(ImageRoundRadius::Large));\n\tif (geometry.attached & (RectPart::Top | RectPart::Left)) {\n\t\tresult.rounding.p[kTopLeft] = nullptr;\n\t}\n\tif (geometry.attached & (RectPart::Top | RectPart::Right)) {\n\t\tresult.rounding.p[kTopRight] = nullptr;\n\t}\n\tif (geometry.attached & (RectPart::Bottom | RectPart::Left)) {\n\t\tresult.rounding.p[kBottomLeft] = nullptr;\n\t}\n\tif (geometry.attached & (RectPart::Bottom | RectPart::Right)) {\n\t\tresult.rounding.p[kBottomRight] = nullptr;\n\t}\n\treturn UnrotateRequest(result, geometry.rotation);\n}\n\nQImage Pip::RendererSW::staticContentByRequest(\n\t\tconst QImage &image,\n\t\tconst FrameRequest &request) {\n\tif (request.resize.isEmpty()) {\n\t\treturn QImage();\n\t} else if (!_preparedStaticContent.isNull()\n\t\t&& _preparedStaticRequest == request\n\t\t&& image.cacheKey() == _preparedStaticKey) {\n\t\treturn _preparedStaticContent;\n\t}\n\t_preparedStaticKey = image.cacheKey();\n\t_preparedStaticRequest = request;\n\t_preparedStaticContent = Images::Round(\n\t\tImages::Prepare(\n\t\t\timage,\n\t\t\trequest.resize,\n\t\t\t{ .outer = request.outer / style::DevicePixelRatio() }),\n\t\trequest.rounding);\n\n\treturn _preparedStaticContent;\n}\n\nvoid Pip::RendererSW::paintTransformedImage(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry) {\n\tconst auto rect = geometry.inner;\n\tconst auto rotation = geometry.rotation;\n\tif (geometry.useTransparency) {\n\t\tUi::Shadow::paint(\n\t\t\t*_p,\n\t\t\trect,\n\t\t\tgeometry.outer.width(),\n\t\t\tPipShadow());\n\t}\n\n\tif (UsePainterRotation(rotation)) {\n\t\tif (rotation) {\n\t\t\t_p->save();\n\t\t\t_p->rotate(rotation);\n\t\t}\n\t\tPainterHighQualityEnabler hq(*_p);\n\t\t_p->drawImage(RotatedRect(rect, rotation), image);\n\t\tif (rotation) {\n\t\t\t_p->restore();\n\t\t}\n\t} else {\n\t\t_p->drawImage(rect, RotateFrameImage(image, rotation));\n\t}\n\n\tif (geometry.fade > 0) {\n\t\t_p->setOpacity(geometry.fade);\n\t\tpaintFade(geometry);\n\t}\n}\n\nvoid Pip::RendererSW::paintRadialLoading(\n\t\tQRect inner,\n\t\tfloat64 controlsShown) {\n\t_owner->paintRadialLoadingContent(*_p, inner, st::radialFg->c);\n}\n\nvoid Pip::RendererSW::paintPlayback(QRect outer, float64 shown) {\n\t_owner->paintPlaybackContent(*_p, outer, shown);\n}\n\nvoid Pip::RendererSW::paintVolumeController(QRect outer, float64 shown) {\n\t_owner->paintVolumeControllerContent(*_p, outer, shown);\n}\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_pip_raster.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/view/media_view_pip_renderer.h\"\n\nnamespace Media::View {\n\nclass Pip::RendererSW final : public Pip::Renderer {\npublic:\n\texplicit RendererSW(not_null<Pip*> owner);\n\n\tvoid paintFallback(\n\t\tPainter &p,\n\t\tconst QRegion &clip,\n\t\tUi::GL::Backend backend) override;\n\nprivate:\n\tvoid paintTransformedVideoFrame(ContentGeometry geometry) override;\n\tvoid paintTransformedStaticContent(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry) override;\n\tvoid paintTransformedImage(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry);\n\tvoid paintRadialLoading(\n\t\tQRect inner,\n\t\tfloat64 controlsShown) override;\n\tvoid paintButtonsStart() override;\n\tvoid paintButton(\n\t\tconst Button &button,\n\t\tint outerWidth,\n\t\tfloat64 shown,\n\t\tfloat64 over,\n\t\tconst style::icon &icon,\n\t\tconst style::icon &iconOver) override;\n\tvoid paintPlayback(QRect outer, float64 shown) override;\n\tvoid paintVolumeController(QRect outer, float64 shown) override;\n\n\tvoid paintFade(ContentGeometry geometry) const;\n\n\t[[nodiscard]] FrameRequest frameRequest(ContentGeometry geometry) const;\n\t[[nodiscard]] QImage staticContentByRequest(\n\t\tconst QImage &image,\n\t\tconst FrameRequest &request);\n\n\tconst not_null<Pip*> _owner;\n\n\tPainter *_p = nullptr;\n\tconst QRegion *_clip = nullptr;\n\tQRect _clipOuter;\n\n\tUi::RoundRect _roundRect;\n\n\tQImage _preparedStaticContent;\n\tFrameRequest _preparedStaticRequest;\n\tqint64 _preparedStaticKey = 0;\n\n};\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_pip_renderer.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/view/media_view_pip.h\"\n#include \"ui/gl/gl_surface.h\"\n\nnamespace Media::View {\n\nclass Pip::Renderer : public Ui::GL::Renderer {\npublic:\n\tvirtual void paintTransformedVideoFrame(ContentGeometry geometry) = 0;\n\tvirtual void paintTransformedStaticContent(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry) = 0;\n\tvirtual void paintRadialLoading(\n\t\tQRect inner,\n\t\tfloat64 controlsShown) = 0;\n\tvirtual void paintButtonsStart() = 0;\n\tvirtual void paintButton(\n\t\tconst Button &button,\n\t\tint outerWidth,\n\t\tfloat64 shown,\n\t\tfloat64 over,\n\t\tconst style::icon &icon,\n\t\tconst style::icon &iconOver) = 0;\n\tvirtual void paintPlayback(QRect outer, float64 shown) = 0;\n\tvirtual void paintVolumeController(QRect outer, float64 shown) = 0;\n\n};\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_pip_rhi.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/view/media_view_pip_rhi.h\"\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\n#include \"ui/rhi/rhi_shader.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"media/streaming/media_streaming_common.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"styles/style_media_view.h\"\n#include \"styles/style_widgets.h\"\n\n#include <rhi/qrhi.h>\n\nnamespace Media::View {\nnamespace {\n\nusing namespace Ui::GL;\n\nstruct PipUniforms {\n\tfloat viewport[2];\n\tfloat _pad0[2];\n\tfloat roundRect[4];\n\tfloat roundRadius;\n\tfloat _pad1[3];\n\tfloat fadeColor[4];\n\tfloat h_size[2];\n\tfloat _pad2[2];\n\tfloat h_extend[4];\n\tfloat h_components[4];\n};\nstatic_assert(sizeof(PipUniforms) % 16 == 0);\n\nstruct ImageUniforms {\n\tfloat viewport[2];\n\tfloat g_opacity;\n\tfloat o_opacity;\n};\nstatic_assert(sizeof(ImageUniforms) % 16 == 0);\n\n[[nodiscard]] QShader LoadShader(const QString &name) {\n\treturn Ui::Rhi::ShaderFromFile(\n\t\tu\":/shaders/\"_q + name + u\".qsb\"_q);\n}\n\n[[nodiscard]] QRhiGraphicsPipeline *CreatePipeline(\n\t\tQRhi *rhi,\n\t\tQRhiRenderPassDescriptor *rpDesc,\n\t\tQRhiShaderResourceBindings *srb,\n\t\tconst QShader &vertexShader,\n\t\tconst QShader &fragmentShader,\n\t\tbool blending,\n\t\tint vertexStride,\n\t\tint attributeCount = 2,\n\t\tQRhiGraphicsPipeline::Topology topology\n\t\t\t= QRhiGraphicsPipeline::TriangleStrip) {\n\tauto pipeline = rhi->newGraphicsPipeline();\n\n\tpipeline->setShaderStages({\n\t\t{ QRhiShaderStage::Vertex, vertexShader },\n\t\t{ QRhiShaderStage::Fragment, fragmentShader },\n\t});\n\n\tQRhiVertexInputLayout inputLayout;\n\tinputLayout.setBindings({\n\t\t{ quint32(vertexStride) },\n\t});\n\tif (attributeCount == 2) {\n\t\tinputLayout.setAttributes({\n\t\t\t{ 0, 0, QRhiVertexInputAttribute::Float2, 0 },\n\t\t\t{ 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) },\n\t\t});\n\t} else if (attributeCount == 3) {\n\t\tinputLayout.setAttributes({\n\t\t\t{ 0, 0, QRhiVertexInputAttribute::Float2, 0 },\n\t\t\t{ 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) },\n\t\t\t{ 0, 2, QRhiVertexInputAttribute::Float2, 4 * sizeof(float) },\n\t\t});\n\t} else {\n\t\tUnexpected(\"Attribute count in PiP::RendererRhi::CreatePipeline.\");\n\t}\n\tpipeline->setVertexInputLayout(inputLayout);\n\n\tif (blending) {\n\t\tQRhiGraphicsPipeline::TargetBlend blend;\n\t\tblend.enable = true;\n\t\tblend.srcColor = QRhiGraphicsPipeline::One;\n\t\tblend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;\n\t\tblend.srcAlpha = QRhiGraphicsPipeline::One;\n\t\tblend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;\n\t\tpipeline->setTargetBlends({ blend });\n\t}\n\n\tpipeline->setTopology(topology);\n\tpipeline->setShaderResourceBindings(srb);\n\tpipeline->setRenderPassDescriptor(rpDesc);\n\tpipeline->create();\n\n\treturn pipeline;\n}\n\n} // namespace\n\nPip::RendererRhi::RendererRhi(not_null<Pip*> owner)\n: _owner(owner) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_radialImage.invalidate();\n\t\t_playbackImage.invalidate();\n\t\t_volumeControllerImage.invalidate();\n\t\tinvalidateControls();\n\t}, _lifetime);\n}\n\nPip::RendererRhi::~RendererRhi() {\n\treleaseResources();\n}\n\nvoid Pip::RendererRhi::initialize(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\tif (_initialized && _rhi == rhi && _rt == rt) {\n\t\treturn;\n\t}\n\treleaseResources();\n\n\t_rhi = rhi;\n\t_rt = rt;\n\t_cb = cb;\n\n\t_vertexBuffer = rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::VertexBuffer,\n\t\tkMaxDraws * kVertexSlotSize);\n\t_vertexBuffer->create();\n\n\t_uniformBuffer = rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::UniformBuffer,\n\t\tkMaxDraws * 256);\n\t_uniformBuffer->create();\n\n\t_sampler = rhi->newSampler(\n\t\tQRhiSampler::Linear,\n\t\tQRhiSampler::Linear,\n\t\tQRhiSampler::None,\n\t\tQRhiSampler::ClampToEdge,\n\t\tQRhiSampler::ClampToEdge);\n\t_sampler->create();\n\n\t_placeholderTexture = rhi->newTexture(QRhiTexture::RGBA8, QSize(1, 1));\n\t_placeholderTexture->create();\n\n\tcreatePipelines();\n\tcreateShadowTexture();\n\t_initialized = true;\n}\n\nvoid Pip::RendererRhi::createPipelines() {\n\tconst auto rpDesc = _rt->renderPassDescriptor();\n\n\tconst auto argb32Vert = LoadShader(u\"argb32.vert\"_q);\n\tconst auto passthroughVert = LoadShader(u\"passthrough.vert\"_q);\n\tconst auto argb32Frag = LoadShader(u\"argb32.frag\"_q);\n\tconst auto pipArgb32Frag = LoadShader(u\"pip_argb32.frag\"_q);\n\tconst auto controlsFrag = LoadShader(u\"pip_controls.frag\"_q);\n\n\t_argb32Srb = _rhi->newShaderResourceBindings();\n\t_argb32Srb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t2,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t});\n\t_argb32Srb->create();\n\n\t_argb32Pipeline = CreatePipeline(\n\t\t_rhi,\n\t\trpDesc,\n\t\t_argb32Srb,\n\t\tpassthroughVert,\n\t\tpipArgb32Frag,\n\t\tfalse,\n\t\t4 * sizeof(float));\n\n\t_imageSrb = _rhi->newShaderResourceBindings();\n\t_imageSrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t});\n\t_imageSrb->create();\n\n\t_imagePipeline = CreatePipeline(\n\t\t_rhi,\n\t\trpDesc,\n\t\t_imageSrb,\n\t\targb32Vert,\n\t\targb32Frag,\n\t\tfalse,\n\t\t4 * sizeof(float));\n\n\t_imageBlendPipeline = CreatePipeline(\n\t\t_rhi,\n\t\trpDesc,\n\t\t_imageSrb,\n\t\targb32Vert,\n\t\targb32Frag,\n\t\ttrue,\n\t\t4 * sizeof(float));\n\n\tconst auto pipControlsVert = LoadShader(u\"pip_controls.vert\"_q);\n\t_controlsSrb = _rhi->newShaderResourceBindings();\n\t_controlsSrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_sampler),\n\t});\n\t_controlsSrb->create();\n\n\t_controlsPipeline = CreatePipeline(\n\t\t_rhi,\n\t\trpDesc,\n\t\t_controlsSrb,\n\t\tpipControlsVert,\n\t\tcontrolsFrag,\n\t\ttrue,\n\t\t6 * sizeof(float),\n\t\t3);\n\n\tconst auto pipYuv420Frag = LoadShader(u\"pip_yuv420.frag\"_q);\n\tconst auto pipNv12Frag = LoadShader(u\"pip_nv12.frag\"_q);\n\n\t_yuv420Srb = _rhi->newShaderResourceBindings();\n\t_yuv420Srb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture, _sampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t2, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture, _sampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t3, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture, _sampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t4, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture, _sampler),\n\t});\n\t_yuv420Srb->create();\n\n\t_yuv420Pipeline = CreatePipeline(\n\t\t_rhi, rpDesc, _yuv420Srb,\n\t\tpassthroughVert, pipYuv420Frag, false, 4 * sizeof(float));\n\n\t_nv12Srb = _rhi->newShaderResourceBindings();\n\t_nv12Srb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture, _sampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t2, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture, _sampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t3, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture, _sampler),\n\t});\n\t_nv12Srb->create();\n\n\t_nv12Pipeline = CreatePipeline(\n\t\t_rhi, rpDesc, _nv12Srb,\n\t\tpassthroughVert, pipNv12Frag, false, 4 * sizeof(float));\n}\n\nvoid Pip::RendererRhi::render(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\t_rhi = rhi;\n\t_rt = rt;\n\t_cb = cb;\n\t_nextSrbIndex = 0;\n\t_drawCommands.clear();\n\t_nextDrawSlot = 0;\n\n\tconst auto size = rt->pixelSize();\n\tconst auto factor = float(\n\t\t_owner->_panel.widget()->devicePixelRatioF());\n\tif (_factor != factor) {\n\t\t_factor = factor;\n\t\t_ifactor = int(std::ceil(_factor));\n\t\tinvalidateControls();\n\t}\n\t_viewport = QSize(\n\t\tint(size.width() / _factor),\n\t\tint(size.height() / _factor));\n\n\t_rub = rhi->nextResourceUpdateBatch();\n\n\t_owner->paint(this);\n\n\tconst auto bg = QColor(0, 0, 0, 0);\n\tcb->beginPass(rt, bg, { 1.0f, 0 }, _rub);\n\t_rub = nullptr;\n\n\tfor (const auto &cmd : _drawCommands) {\n\t\tcb->setGraphicsPipeline(cmd.pipeline);\n\t\tcb->setShaderResources(cmd.srb);\n\t\tcb->setViewport({\n\t\t\t0, 0,\n\t\t\tfloat(rt->pixelSize().width()),\n\t\t\tfloat(rt->pixelSize().height()) });\n\t\tconst QRhiCommandBuffer::VertexInput vbufBinding(\n\t\t\tcmd.vertexBuffer, cmd.vertexOffset);\n\t\tcb->setVertexInput(0, 1, &vbufBinding);\n\t\tcb->draw(4);\n\t}\n\t_drawCommands.clear();\n\n\tcb->endPass();\n}\n\nvoid Pip::RendererRhi::releaseResources() {\n\t_drawCommands.clear();\n\tfor (auto *srb : _srbPool) {\n\t\tdelete srb;\n\t}\n\t_srbPool.clear();\n\t_nextSrbIndex = 0;\n\n\t_shadowImage.destroy();\n\t_radialImage.destroy();\n\t_controlsImage.destroy();\n\t_playbackImage.destroy();\n\t_volumeControllerImage.destroy();\n\n\tdelete _argb32Pipeline;\n\t_argb32Pipeline = nullptr;\n\tdelete _yuv420Pipeline;\n\t_yuv420Pipeline = nullptr;\n\tdelete _nv12Pipeline;\n\t_nv12Pipeline = nullptr;\n\tdelete _imagePipeline;\n\t_imagePipeline = nullptr;\n\tdelete _imageBlendPipeline;\n\t_imageBlendPipeline = nullptr;\n\tdelete _controlsPipeline;\n\t_controlsPipeline = nullptr;\n\n\tdelete _argb32Srb;\n\t_argb32Srb = nullptr;\n\tdelete _yuv420Srb;\n\t_yuv420Srb = nullptr;\n\tdelete _nv12Srb;\n\t_nv12Srb = nullptr;\n\tdelete _imageSrb;\n\t_imageSrb = nullptr;\n\tdelete _controlsSrb;\n\t_controlsSrb = nullptr;\n\n\tdelete _placeholderTexture;\n\t_placeholderTexture = nullptr;\n\tdelete _rgbaTexture;\n\t_rgbaTexture = nullptr;\n\t_rgbaSize = QSize();\n\t_cacheKey = 0;\n\tdelete _yTexture;\n\t_yTexture = nullptr;\n\tdelete _uTexture;\n\t_uTexture = nullptr;\n\tdelete _vTexture;\n\t_vTexture = nullptr;\n\tdelete _uvTexture;\n\t_uvTexture = nullptr;\n\t_lumaSize = QSize();\n\t_chromaSize = QSize();\n\t_trackFrameIndex = -1;\n\t_chromaNV12 = false;\n\t_usingExternalVideoTextures = false;\n\n\tdelete _vertexBuffer;\n\t_vertexBuffer = nullptr;\n\tdelete _uniformBuffer;\n\t_uniformBuffer = nullptr;\n\tdelete _sampler;\n\t_sampler = nullptr;\n\n\t_initialized = false;\n}\n\nint Pip::RendererRhi::allocateDrawSlot() {\n\treturn (_nextDrawSlot < kMaxDraws) ? _nextDrawSlot++ : -1;\n}\n\nQRhiShaderResourceBindings *Pip::RendererRhi::allocateSrb() {\n\tif (_nextSrbIndex < int(_srbPool.size())) {\n\t\treturn _srbPool[_nextSrbIndex++];\n\t}\n\tauto *srb = _rhi->newShaderResourceBindings();\n\t_srbPool.push_back(srb);\n\t_nextSrbIndex++;\n\treturn srb;\n}\n\nvoid Pip::RendererRhi::createShadowTexture() {\n\tconst auto &shadow = PipShadow();\n\tconst auto size = 2 * PipShadow().topLeft.size()\n\t\t+ QSize(st::roundRadiusLarge, st::roundRadiusLarge);\n\tauto image = QImage(\n\t\tsize * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\timage.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&image);\n\t\tUi::Shadow::paint(\n\t\t\tp,\n\t\t\tQRect(QPoint(), size).marginsRemoved(shadow.extend),\n\t\t\tsize.width(),\n\t\t\tshadow);\n\t}\n\t_shadowImage.setImage(std::move(image));\n}\n\nvoid Pip::RendererRhi::paintTransformedVideoFrame(\n\t\tContentGeometry geometry) {\n\tconst auto data = _owner->videoFrameWithInfo();\n\tif (data.format == Streaming::FrameFormat::None) {\n\t\treturn;\n\t}\n\tgeometry.rotation = (geometry.rotation + geometry.videoRotation) % 360;\n\tif (data.format == Streaming::FrameFormat::ARGB32) {\n\t\tif (data.image.isNull()) {\n\t\t\treturn;\n\t\t}\n\t\tpaintTransformedStaticContent(data.image, geometry);\n\t\treturn;\n\t}\n\tconst auto nativeTexture =\n\t\t(data.format == Streaming::FrameFormat::NativeTexture);\n\tconst auto nv12 = nativeTexture\n\t\t|| (data.format == Streaming::FrameFormat::NV12);\n\tconst auto yuv = data.yuv;\n\tconst auto nv12changed = !nativeTexture && (_chromaNV12 != nv12);\n\tconst auto upload = (_trackFrameIndex != data.index);\n\t_trackFrameIndex = data.index;\n\n\tif (upload) {\n\t\tauto zeroCopied = false;\n#ifdef Q_OS_MAC\n\t\tif (nativeTexture && data.nativeFrame\n\t\t\t&& data.nativeFrame->pixelBuffer) {\n\t\t\tconst auto ok = _metalTextureCache.createTexturesFromPixelBuffer(\n\t\t\t\t\t_rhi,\n\t\t\t\t\tdata.nativeFrame->pixelBuffer,\n\t\t\t\t\t&_yTexture,\n\t\t\t\t\t&_uvTexture,\n\t\t\t\t\t&_lumaSize,\n\t\t\t\t\t&_chromaSize);\n\t\t\tif (ok) {\n\t\t\t\t_chromaNV12 = true;\n\t\t\t\tzeroCopied = true;\n\t\t\t} else {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n#endif // Q_OS_MAC\n\t\tif (!zeroCopied) {\n\t\t\tif (_usingExternalVideoTextures) {\n\t\t\t\tdelete _yTexture;\n\t\t\t\t_yTexture = nullptr;\n\t\t\t\tdelete _uTexture;\n\t\t\t\t_uTexture = nullptr;\n\t\t\t\tdelete _vTexture;\n\t\t\t\t_vTexture = nullptr;\n\t\t\t\tdelete _uvTexture;\n\t\t\t\t_uvTexture = nullptr;\n\t\t\t\t_lumaSize = QSize();\n\t\t\t\t_chromaSize = QSize();\n#ifdef Q_OS_MAC\n\t\t\t\t_metalTextureCache.flush();\n#endif // Q_OS_MAC\n\t\t\t}\n\t\t\tif (!yuv || yuv->size.isEmpty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!_yTexture || _lumaSize != yuv->size) {\n\t\t\t\tdelete _yTexture;\n\t\t\t\t_yTexture = _rhi->newTexture(QRhiTexture::R8, yuv->size);\n\t\t\t\t_yTexture->create();\n\t\t\t\t_lumaSize = yuv->size;\n\t\t\t}\n\t\t\tauto yDesc = QRhiTextureSubresourceUploadDescription(\n\t\t\t\tyuv->y.data, yuv->y.stride * yuv->size.height());\n\t\t\tyDesc.setDataStride(yuv->y.stride);\n\t\t\t_rub->uploadTexture(_yTexture,\n\t\t\t\tQRhiTextureUploadDescription(\n\t\t\t\t\tQRhiTextureUploadEntry(0, 0, yDesc)));\n\n\t\t\tif (nv12) {\n\t\t\t\tif (!_uvTexture || nv12changed\n\t\t\t\t\t|| _chromaSize != yuv->chromaSize) {\n\t\t\t\t\tdelete _uvTexture;\n\t\t\t\t\t_uvTexture = _rhi->newTexture(\n\t\t\t\t\t\tQRhiTexture::RG8, yuv->chromaSize);\n\t\t\t\t\t_uvTexture->create();\n\t\t\t\t\t_chromaSize = yuv->chromaSize;\n\t\t\t\t}\n\t\t\t\tauto uvDesc = QRhiTextureSubresourceUploadDescription(\n\t\t\t\t\tyuv->u.data, yuv->u.stride * yuv->chromaSize.height());\n\t\t\t\tuvDesc.setDataStride(yuv->u.stride);\n\t\t\t\t_rub->uploadTexture(_uvTexture,\n\t\t\t\t\tQRhiTextureUploadDescription(\n\t\t\t\t\t\tQRhiTextureUploadEntry(0, 0, uvDesc)));\n\t\t\t} else {\n\t\t\t\tif (!_uTexture || nv12changed\n\t\t\t\t\t|| _chromaSize != yuv->chromaSize) {\n\t\t\t\t\tdelete _uTexture;\n\t\t\t\t\t_uTexture = _rhi->newTexture(\n\t\t\t\t\t\tQRhiTexture::R8, yuv->chromaSize);\n\t\t\t\t\t_uTexture->create();\n\t\t\t\t\tdelete _vTexture;\n\t\t\t\t\t_vTexture = _rhi->newTexture(\n\t\t\t\t\t\tQRhiTexture::R8, yuv->chromaSize);\n\t\t\t\t\t_vTexture->create();\n\t\t\t\t\t_chromaSize = yuv->chromaSize;\n\t\t\t\t}\n\t\t\t\tauto uDesc = QRhiTextureSubresourceUploadDescription(\n\t\t\t\t\tyuv->u.data, yuv->u.stride * yuv->chromaSize.height());\n\t\t\t\tuDesc.setDataStride(yuv->u.stride);\n\t\t\t\t_rub->uploadTexture(_uTexture,\n\t\t\t\t\tQRhiTextureUploadDescription(\n\t\t\t\t\t\tQRhiTextureUploadEntry(0, 0, uDesc)));\n\t\t\t\tauto vDesc = QRhiTextureSubresourceUploadDescription(\n\t\t\t\t\tyuv->v.data, yuv->v.stride * yuv->chromaSize.height());\n\t\t\t\tvDesc.setDataStride(yuv->v.stride);\n\t\t\t\t_rub->uploadTexture(_vTexture,\n\t\t\t\t\tQRhiTextureUploadDescription(\n\t\t\t\t\t\tQRhiTextureUploadEntry(0, 0, vDesc)));\n\t\t\t}\n\t\t} // !zeroCopied\n\t\t_chromaNV12 = nv12;\n\t\t_usingExternalVideoTextures = zeroCopied;\n\t}\n\n\t_shadowImage.upload(_rhi, _rub);\n\n\tif (nv12) {\n\t\tconst auto slot = allocateDrawSlot();\n\t\tif (slot < 0) {\n\t\t\treturn;\n\t\t}\n\t\tauto *srb = allocateSrb();\n\t\tsrb->setBindings({\n\t\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t\t0,\n\t\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_uniformBuffer,\n\t\t\t\tslot * 256,\n\t\t\t\tsizeof(PipUniforms)),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t1, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_yTexture, _sampler),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t2, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_uvTexture, _sampler),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t3, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_shadowImage.texture()\n\t\t\t\t\t? _shadowImage.texture()\n\t\t\t\t\t: _placeholderTexture,\n\t\t\t\t_sampler),\n\t\t});\n\t\tsrb->create();\n\t\tpaintTransformedContent(_nv12Pipeline, srb, geometry, slot);\n\t} else {\n\t\tconst auto slot = allocateDrawSlot();\n\t\tif (slot < 0) {\n\t\t\treturn;\n\t\t}\n\t\tauto *srb = allocateSrb();\n\t\tsrb->setBindings({\n\t\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t\t0,\n\t\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_uniformBuffer,\n\t\t\t\tslot * 256,\n\t\t\t\tsizeof(PipUniforms)),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t1, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_yTexture, _sampler),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t2, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_uTexture, _sampler),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t3, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_vTexture, _sampler),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t4, QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_shadowImage.texture()\n\t\t\t\t\t? _shadowImage.texture()\n\t\t\t\t\t: _placeholderTexture,\n\t\t\t\t_sampler),\n\t\t});\n\t\tsrb->create();\n\t\tpaintTransformedContent(_yuv420Pipeline, srb, geometry, slot);\n\t}\n}\n\nvoid Pip::RendererRhi::paintTransformedStaticContent(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry) {\n\tif (image.isNull() || !_argb32Pipeline) {\n\t\treturn;\n\t}\n\n\tconst auto cacheKey = image.cacheKey();\n\tif (_cacheKey != cacheKey) {\n\t\t_cacheKey = cacheKey;\n\t\tif (!_rgbaTexture || _rgbaSize != image.size()) {\n\t\t\tdelete _rgbaTexture;\n\t\t\t_rgbaTexture = _rhi->newTexture(\n\t\t\t\tQRhiTexture::BGRA8,\n\t\t\t\timage.size());\n\t\t\t_rgbaTexture->create();\n\t\t\t_rgbaSize = image.size();\n\t\t}\n\t\tauto desc = QRhiTextureSubresourceUploadDescription(image);\n\t\t_rub->uploadTexture(\n\t\t\t_rgbaTexture,\n\t\t\tQRhiTextureUploadDescription(\n\t\t\t\tQRhiTextureUploadEntry(0, 0, desc)));\n\t}\n\n\tconst auto slot = allocateDrawSlot();\n\tif (slot < 0) {\n\t\treturn;\n\t}\n\tauto *srb = allocateSrb();\n\tsrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer,\n\t\t\tslot * 256,\n\t\t\tsizeof(PipUniforms)),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_rgbaTexture,\n\t\t\t_sampler),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t2,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_shadowImage.texture()\n\t\t\t\t? _shadowImage.texture()\n\t\t\t\t: _placeholderTexture,\n\t\t\t_sampler),\n\t});\n\tsrb->create();\n\n\t_shadowImage.upload(_rhi, _rub);\n\n\tpaintTransformedContent(_argb32Pipeline, srb, geometry, slot);\n}\n\nvoid Pip::RendererRhi::paintTransformedContent(\n\t\tQRhiGraphicsPipeline *pipeline,\n\t\tQRhiShaderResourceBindings *srb,\n\t\tContentGeometry geometry,\n\t\tint slot) {\n\tstd::array<std::array<float, 2>, 4> rect = { {\n\t\t{ { -1.f, 1.f } },\n\t\t{ { 1.f, 1.f } },\n\t\t{ { 1.f, -1.f } },\n\t\t{ { -1.f, -1.f } },\n\t} };\n\tif (const auto shift = (geometry.rotation / 90); shift != 0) {\n\t\tstd::rotate(begin(rect), begin(rect) + shift, end(rect));\n\t}\n\tconst auto xscale = 1.f / geometry.inner.width();\n\tconst auto yscale = 1.f / geometry.inner.height();\n\tconst float coords[] = {\n\t\trect[0][0], rect[0][1],\n\t\t-geometry.inner.x() * xscale,\n\t\t-geometry.inner.y() * yscale,\n\n\t\trect[1][0], rect[1][1],\n\t\t(geometry.outer.width() - geometry.inner.x()) * xscale,\n\t\t-geometry.inner.y() * yscale,\n\n\t\trect[3][0], rect[3][1],\n\t\t-geometry.inner.x() * xscale,\n\t\t(geometry.outer.height() - geometry.inner.y()) * yscale,\n\n\t\trect[2][0], rect[2][1],\n\t\t(geometry.outer.width() - geometry.inner.x()) * xscale,\n\t\t(geometry.outer.height() - geometry.inner.y()) * yscale,\n\t};\n\n\tconst auto vOffset = slot * kVertexSlotSize;\n\tconst auto uOffset = slot * 256;\n\t_rub->updateDynamicBuffer(\n\t\t_vertexBuffer,\n\t\tvOffset,\n\t\tsizeof(coords),\n\t\tcoords);\n\n\tconst auto globalFactor = _factor;\n\tconst auto fadeAlpha = float(\n\t\tst::radialBg->c.alphaF() * geometry.fade);\n\tconst auto roundRect = transformRect(RoundingRect(geometry));\n\n\tPipUniforms uniforms{};\n\tuniforms.viewport[0] = _viewport.width() * _factor;\n\tuniforms.viewport[1] = _viewport.height() * _factor;\n\tuniforms.roundRect[0] = roundRect.left();\n\tuniforms.roundRect[1] = roundRect.top();\n\tuniforms.roundRect[2] = roundRect.width();\n\tuniforms.roundRect[3] = roundRect.height();\n\tuniforms.roundRadius = st::roundRadiusLarge * _factor;\n\tuniforms.fadeColor[0] = float(st::radialBg->c.redF() * fadeAlpha);\n\tuniforms.fadeColor[1] = float(st::radialBg->c.greenF() * fadeAlpha);\n\tuniforms.fadeColor[2] = float(st::radialBg->c.blueF() * fadeAlpha);\n\tuniforms.fadeColor[3] = fadeAlpha;\n\tconst auto &shadowImg = _shadowImage.image();\n\tuniforms.h_size[0] = shadowImg.width();\n\tuniforms.h_size[1] = shadowImg.height();\n\tuniforms.h_extend[0] = PipShadow().extend.left() * globalFactor;\n\tuniforms.h_extend[1] = PipShadow().extend.top() * globalFactor;\n\tuniforms.h_extend[2] = PipShadow().extend.right() * globalFactor;\n\tuniforms.h_extend[3] = PipShadow().extend.bottom() * globalFactor;\n\tuniforms.h_components[0] = PipShadow().topLeft.width() * globalFactor;\n\tuniforms.h_components[1] = PipShadow().topLeft.height() * globalFactor;\n\tuniforms.h_components[2] = PipShadow().left.width() * globalFactor;\n\tuniforms.h_components[3] = PipShadow().top.height() * globalFactor;\n\n\t_rub->updateDynamicBuffer(\n\t\t_uniformBuffer,\n\t\tuOffset,\n\t\tsizeof(PipUniforms),\n\t\t&uniforms);\n\n\t_drawCommands.push_back({\n\t\t.pipeline = pipeline,\n\t\t.srb = srb,\n\t\t.vertexBuffer = _vertexBuffer,\n\t\t.vertexOffset = quint32(vOffset),\n\t});\n}\n\nvoid Pip::RendererRhi::paintRadialLoading(\n\t\tQRect inner,\n\t\tfloat64 controlsShown) {\n\tpaintUsingRaster(_radialImage, inner, [&](QPainter &&p) {\n\t\tconst auto newInner = QRect(QPoint(), inner.size());\n\t\tconst auto fg = st::radialFg->c;\n\t\tconst auto fade = st::radialBg->c;\n\t\tconst auto fadeAlpha = controlsShown * fade.alphaF();\n\t\tconst auto fgAlpha = 1. - fadeAlpha;\n\t\tconst auto color = (fadeAlpha == 0.) ? fg : QColor(\n\t\t\tint(base::SafeRound(\n\t\t\t\tfg.red() * fgAlpha + fade.red() * fadeAlpha)),\n\t\t\tint(base::SafeRound(\n\t\t\t\tfg.green() * fgAlpha + fade.green() * fadeAlpha)),\n\t\t\tint(base::SafeRound(\n\t\t\t\tfg.blue() * fgAlpha + fade.blue() * fadeAlpha)),\n\t\t\tfg.alpha());\n\n\t\t_owner->paintRadialLoadingContent(p, newInner, color);\n\t}, true);\n}\n\nvoid Pip::RendererRhi::paintPlayback(QRect outer, float64 shown) {\n\tpaintUsingRaster(_playbackImage, outer, [&](QPainter &&p) {\n\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t_owner->paintPlaybackContent(p, newOuter, shown);\n\t}, true);\n}\n\nvoid Pip::RendererRhi::paintVolumeController(QRect outer, float64 shown) {\n\tpaintUsingRaster(\n\t\t_volumeControllerImage,\n\t\touter,\n\t\t[&](QPainter &&p) {\n\t\t\tconst auto newOuter = QRect(QPoint(), outer.size());\n\t\t\t_owner->paintVolumeControllerContent(p, newOuter, shown);\n\t\t},\n\t\ttrue);\n}\n\nvoid Pip::RendererRhi::paintButtonsStart() {\n\tvalidateControls();\n}\n\nvoid Pip::RendererRhi::paintButton(\n\t\tconst Button &button,\n\t\tint outerWidth,\n\t\tfloat64 shown,\n\t\tfloat64 over,\n\t\tconst style::icon &icon,\n\t\tconst style::icon &iconOver) {\n\tconst auto tryIndex = [&](int stateIndex) -> std::optional<Control> {\n\t\tconst auto result = ControlMeta(button.state, stateIndex);\n\t\treturn (result.icon == &icon && result.iconOver == &iconOver)\n\t\t\t? std::make_optional(result)\n\t\t\t: std::nullopt;\n\t};\n\tconst auto meta = tryIndex(0)\n\t\t? *tryIndex(0)\n\t\t: tryIndex(1)\n\t\t? *tryIndex(1)\n\t\t: *tryIndex(2);\n\tAssert(meta.icon == &icon && meta.iconOver == &iconOver);\n\n\t_controlsImage.upload(_rhi, _rub);\n\tif (!_controlsImage.texture()) {\n\t\treturn;\n\t}\n\n\tconst auto iconRect = _controlsImage.texturedRect(\n\t\tbutton.icon,\n\t\t_controlsTextures[meta.index * 2 + 0]);\n\tconst auto overRect = _controlsImage.texturedRect(\n\t\tbutton.icon,\n\t\t_controlsTextures[meta.index * 2 + 1]);\n\tconst auto geo = transformRect(iconRect.geometry);\n\n\tconst float coords[] = {\n\t\tgeo.left(), geo.top(),\n\t\ticonRect.texture.left(), iconRect.texture.bottom(),\n\t\toverRect.texture.left(), overRect.texture.bottom(),\n\n\t\tgeo.right(), geo.top(),\n\t\ticonRect.texture.right(), iconRect.texture.bottom(),\n\t\toverRect.texture.right(), overRect.texture.bottom(),\n\n\t\tgeo.left(), geo.bottom(),\n\t\ticonRect.texture.left(), iconRect.texture.top(),\n\t\toverRect.texture.left(), overRect.texture.top(),\n\n\t\tgeo.right(), geo.bottom(),\n\t\ticonRect.texture.right(), iconRect.texture.top(),\n\t\toverRect.texture.right(), overRect.texture.top(),\n\t};\n\tconst auto slot = allocateDrawSlot();\n\tif (slot < 0) {\n\t\treturn;\n\t}\n\tconst auto vOffset = slot * kVertexSlotSize;\n\tconst auto uOffset = slot * 256;\n\t_rub->updateDynamicBuffer(\n\t\t_vertexBuffer, vOffset, sizeof(coords), coords);\n\n\tImageUniforms uniforms{};\n\tuniforms.viewport[0] = _viewport.width() * _factor;\n\tuniforms.viewport[1] = _viewport.height() * _factor;\n\tuniforms.g_opacity = float(shown);\n\tuniforms.o_opacity = float(over);\n\t_rub->updateDynamicBuffer(\n\t\t_uniformBuffer, uOffset, sizeof(ImageUniforms), &uniforms);\n\n\tauto *srb = allocateSrb();\n\tsrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t_uniformBuffer,\n\t\t\tuOffset,\n\t\t\tsizeof(ImageUniforms)),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_controlsImage.texture(),\n\t\t\t_sampler),\n\t});\n\tsrb->create();\n\n\t_drawCommands.push_back({\n\t\t.pipeline = _controlsPipeline,\n\t\t.srb = srb,\n\t\t.vertexBuffer = _vertexBuffer,\n\t\t.vertexOffset = quint32(vOffset),\n\t});\n}\n\nauto Pip::RendererRhi::ControlMeta(OverState control, int index)\n-> Control {\n\tExpects(index >= 0);\n\n\tswitch (control) {\n\tcase OverState::Close: Assert(index < 1); return {\n\t\t0,\n\t\t&st::pipCloseIcon,\n\t\t&st::pipCloseIconOver,\n\t};\n\tcase OverState::Enlarge: Assert(index < 1); return {\n\t\t1,\n\t\t&st::pipEnlargeIcon,\n\t\t&st::pipEnlargeIconOver,\n\t};\n\tcase OverState::VolumeToggle: Assert(index < 3); return {\n\t\t(2 + index),\n\t\t(index == 0\n\t\t\t? &st::pipVolumeIcon0\n\t\t\t: (index == 1)\n\t\t\t? &st::pipVolumeIcon1\n\t\t\t: &st::pipVolumeIcon2),\n\t\t(index == 0\n\t\t\t? &st::pipVolumeIcon0Over\n\t\t\t: (index == 1)\n\t\t\t? &st::pipVolumeIcon1Over\n\t\t\t: &st::pipVolumeIcon2Over),\n\t};\n\tcase OverState::Other: Assert(index < 2); return {\n\t\t(5 + index),\n\t\t(index ? &st::pipPauseIcon : &st::pipPlayIcon),\n\t\t(index ? &st::pipPauseIconOver : &st::pipPlayIconOver),\n\t};\n\t}\n\tUnexpected(\"Control value in Pip::RendererRhi::ControlIndex.\");\n}\n\nvoid Pip::RendererRhi::validateControls() {\n\tif (!_controlsImage.image().isNull()) {\n\t\treturn;\n\t}\n\tconst auto metas = {\n\t\tControlMeta(OverState::Close),\n\t\tControlMeta(OverState::Enlarge),\n\t\tControlMeta(OverState::VolumeToggle),\n\t\tControlMeta(OverState::VolumeToggle, 1),\n\t\tControlMeta(OverState::VolumeToggle, 2),\n\t\tControlMeta(OverState::Other),\n\t\tControlMeta(OverState::Other, 1),\n\t};\n\tauto maxWidth = 0;\n\tauto fullHeight = 0;\n\tfor (const auto &meta : metas) {\n\t\tAssert(meta.icon->size() == meta.iconOver->size());\n\t\tmaxWidth = std::max(meta.icon->width(), maxWidth);\n\t\tfullHeight += 2 * meta.icon->height();\n\t}\n\tauto image = QImage(\n\t\tQSize(maxWidth, fullHeight) * _ifactor,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.fill(Qt::transparent);\n\timage.setDevicePixelRatio(_ifactor);\n\t{\n\t\tauto p = QPainter(&image);\n\t\tauto index = 0;\n\t\tauto height = 0;\n\t\tconst auto paint = [&](not_null<const style::icon*> icon) {\n\t\t\ticon->paint(p, 0, height, maxWidth);\n\t\t\t_controlsTextures[index++] = QRect(\n\t\t\t\tQPoint(0, height) * _ifactor,\n\t\t\t\ticon->size() * _ifactor);\n\t\t\theight += icon->height();\n\t\t};\n\t\tfor (const auto &meta : metas) {\n\t\t\tpaint(meta.icon);\n\t\t\tpaint(meta.iconOver);\n\t\t}\n\t}\n\t_controlsImage.setImage(std::move(image));\n}\n\nvoid Pip::RendererRhi::invalidateControls() {\n\t_controlsImage.invalidate();\n\tranges::fill(_controlsTextures, QRect());\n}\n\nvoid Pip::RendererRhi::paintUsingRaster(\n\t\tUi::Rhi::Image &image,\n\t\tQRect rect,\n\t\tFn<void(QPainter&&)> method,\n\t\tbool transparent) {\n\tauto raster = image.takeImage();\n\tconst auto size = rect.size() * _ifactor;\n\tif (raster.width() < size.width()\n\t\t|| raster.height() < size.height()) {\n\t\traster = QImage(size, QImage::Format_ARGB32_Premultiplied);\n\t\traster.setDevicePixelRatio(_factor);\n\t\tif (!transparent\n\t\t\t&& (raster.width() > size.width()\n\t\t\t\t|| raster.height() > size.height())) {\n\t\t\traster.fill(Qt::transparent);\n\t\t}\n\t} else if (raster.devicePixelRatio() != _ifactor) {\n\t\traster.setDevicePixelRatio(_ifactor);\n\t}\n\n\tif (transparent) {\n\t\traster.fill(Qt::transparent);\n\t}\n\tmethod(QPainter(&raster));\n\n\timage.setImage(std::move(raster), size);\n\timage.upload(_rhi, _rub);\n\n\tconst auto textured = image.texturedRect(\n\t\trect,\n\t\tQRect(QPoint(), size));\n\tconst auto geometry = transformRect(textured.geometry);\n\tconst float coords[] = {\n\t\tgeometry.left(), geometry.top(),\n\t\ttextured.texture.left(), textured.texture.bottom(),\n\n\t\tgeometry.right(), geometry.top(),\n\t\ttextured.texture.right(), textured.texture.bottom(),\n\n\t\tgeometry.left(), geometry.bottom(),\n\t\ttextured.texture.left(), textured.texture.top(),\n\n\t\tgeometry.right(), geometry.bottom(),\n\t\ttextured.texture.right(), textured.texture.top(),\n\t};\n\tconst auto slot = allocateDrawSlot();\n\tif (slot < 0) {\n\t\treturn;\n\t}\n\tconst auto vOffset = slot * kVertexSlotSize;\n\tconst auto uOffset = slot * 256;\n\t_rub->updateDynamicBuffer(\n\t\t_vertexBuffer,\n\t\tvOffset,\n\t\tsizeof(coords),\n\t\tcoords);\n\n\tImageUniforms uniforms{};\n\tuniforms.viewport[0] = _viewport.width() * _factor;\n\tuniforms.viewport[1] = _viewport.height() * _factor;\n\tuniforms.g_opacity = 1.0f;\n\n\t_rub->updateDynamicBuffer(\n\t\t_uniformBuffer,\n\t\tuOffset,\n\t\tsizeof(ImageUniforms),\n\t\t&uniforms);\n\n\tif (image.texture()) {\n\t\tauto *srb = allocateSrb();\n\t\tsrb->setBindings({\n\t\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t\t0,\n\t\t\t\tQRhiShaderResourceBinding::VertexStage\n\t\t\t\t\t| QRhiShaderResourceBinding::FragmentStage,\n\t\t\t\t_uniformBuffer,\n\t\t\t\tuOffset,\n\t\t\t\tsizeof(ImageUniforms)),\n\t\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t\t1,\n\t\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t\timage.texture(),\n\t\t\t\t_sampler),\n\t\t});\n\t\tsrb->create();\n\n\t\tconst auto pipeline = transparent\n\t\t\t? _imageBlendPipeline\n\t\t\t: _imagePipeline;\n\t\t_drawCommands.push_back({\n\t\t\t.pipeline = pipeline,\n\t\t\t.srb = srb,\n\t\t\t.vertexBuffer = _vertexBuffer,\n\t\t\t.vertexOffset = quint32(vOffset),\n\t\t});\n\t}\n}\n\nQRect Pip::RendererRhi::RoundingRect(ContentGeometry geometry) {\n\tconst auto inner = geometry.inner;\n\tconst auto attached = geometry.attached;\n\tconst auto added = std::max({\n\t\tst::roundRadiusLarge,\n\t\tinner.x(),\n\t\tinner.y(),\n\t\tgeometry.outer.width() - inner.x() - inner.width(),\n\t\tgeometry.outer.height() - inner.y() - inner.height(),\n\t\tPipShadow().topLeft.width(),\n\t\tPipShadow().topLeft.height(),\n\t\tPipShadow().topRight.width(),\n\t\tPipShadow().topRight.height(),\n\t\tPipShadow().bottomRight.width(),\n\t\tPipShadow().bottomRight.height(),\n\t\tPipShadow().bottomLeft.width(),\n\t\tPipShadow().bottomLeft.height(),\n\t});\n\treturn geometry.inner.marginsAdded({\n\t\t(attached & RectPart::Left) ? added : 0,\n\t\t(attached & RectPart::Top) ? added : 0,\n\t\t(attached & RectPart::Right) ? added : 0,\n\t\t(attached & RectPart::Bottom) ? added : 0,\n\t});\n}\n\nRect Pip::RendererRhi::transformRect(const Rect &raster) const {\n\treturn TransformRect(raster, _viewport, _factor);\n}\n\nRect Pip::RendererRhi::transformRect(const QRectF &raster) const {\n\treturn TransformRect(raster, _viewport, _factor);\n}\n\nRect Pip::RendererRhi::transformRect(const QRect &raster) const {\n\treturn TransformRect(Rect(raster), _viewport, _factor);\n}\n\n} // namespace Media::View\n\n#endif // Qt >= 6.7\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_pip_rhi.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/view/media_view_pip_renderer.h\"\n#include \"ui/rhi/rhi_renderer.h\"\n#include \"ui/rhi/rhi_image.h\"\n#include \"ui/gl/gl_math.h\"\n\n#ifdef Q_OS_MAC\n#include \"media/view/media_view_metal_texture.h\"\n#endif\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\nclass QRhi;\nclass QRhiBuffer;\nclass QRhiTexture;\nclass QRhiSampler;\nclass QRhiGraphicsPipeline;\nclass QRhiShaderResourceBindings;\nclass QRhiResourceUpdateBatch;\n\nnamespace Media::View {\n\nclass Pip::RendererRhi final\n\t: public Pip::Renderer\n\t, public Ui::Rhi::Renderer {\npublic:\n\texplicit RendererRhi(not_null<Pip*> owner);\n\t~RendererRhi();\n\n\tvoid initialize(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) override;\n\tvoid render(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) override;\n\tvoid releaseResources() override;\n\n\tQColor rhiClearColor() override {\n\t\treturn QColor(0, 0, 0, 0);\n\t}\n\n\tstd::optional<QColor> clearColor() override {\n\t\treturn QColor(0, 0, 0, 0);\n\t}\n\nprivate:\n\tstruct Control {\n\t\tint index = -1;\n\t\tnot_null<const style::icon*> icon;\n\t\tnot_null<const style::icon*> iconOver;\n\t};\n\n\tvoid paintTransformedVideoFrame(ContentGeometry geometry) override;\n\tvoid paintTransformedStaticContent(\n\t\tconst QImage &image,\n\t\tContentGeometry geometry) override;\n\tvoid paintRadialLoading(\n\t\tQRect inner,\n\t\tfloat64 controlsShown) override;\n\tvoid paintButtonsStart() override;\n\tvoid paintButton(\n\t\tconst Button &button,\n\t\tint outerWidth,\n\t\tfloat64 shown,\n\t\tfloat64 over,\n\t\tconst style::icon &icon,\n\t\tconst style::icon &iconOver) override;\n\tvoid paintPlayback(QRect outer, float64 shown) override;\n\tvoid paintVolumeController(QRect outer, float64 shown) override;\n\n\tvoid paintTransformedContent(\n\t\tQRhiGraphicsPipeline *pipeline,\n\t\tQRhiShaderResourceBindings *srb,\n\t\tContentGeometry geometry,\n\t\tint slot);\n\tvoid paintUsingRaster(\n\t\tUi::Rhi::Image &image,\n\t\tQRect rect,\n\t\tFn<void(QPainter&&)> method,\n\t\tbool transparent = false);\n\t[[nodiscard]] int allocateDrawSlot();\n\t[[nodiscard]] QRhiShaderResourceBindings *allocateSrb();\n\n\tstruct DrawCommand {\n\t\tQRhiGraphicsPipeline *pipeline = nullptr;\n\t\tQRhiShaderResourceBindings *srb = nullptr;\n\t\tQRhiBuffer *vertexBuffer = nullptr;\n\t\tquint32 vertexOffset = 0;\n\t};\n\n\tvoid createShadowTexture();\n\tvoid createPipelines();\n\tvoid validateControls();\n\tvoid invalidateControls();\n\n\t[[nodiscard]] static QRect RoundingRect(ContentGeometry geometry);\n\t[[nodiscard]] static Control ControlMeta(\n\t\tOverState control,\n\t\tint index = 0);\n\n\t[[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const;\n\t[[nodiscard]] Ui::GL::Rect transformRect(const QRectF &raster) const;\n\t[[nodiscard]] Ui::GL::Rect transformRect(\n\t\tconst Ui::GL::Rect &raster) const;\n\n\tconst not_null<Pip*> _owner;\n\n\tQRhi *_rhi = nullptr;\n\tQRhiRenderTarget *_rt = nullptr;\n\tQRhiCommandBuffer *_cb = nullptr;\n\tQRhiResourceUpdateBatch *_rub = nullptr;\n\tQSize _viewport;\n\tfloat _factor = 1.;\n\tint _ifactor = 1;\n\n\tQRhiBuffer *_vertexBuffer = nullptr;\n\tQRhiBuffer *_uniformBuffer = nullptr;\n\tQRhiSampler *_sampler = nullptr;\n\n\tQRhiTexture *_placeholderTexture = nullptr;\n\tQRhiTexture *_rgbaTexture = nullptr;\n\tQSize _rgbaSize;\n\tquint64 _cacheKey = 0;\n\tint _trackFrameIndex = -1;\n\tbool _chromaNV12 = false;\n\tbool _usingExternalVideoTextures = false;\n\n\tQRhiTexture *_yTexture = nullptr;\n\tQRhiTexture *_uTexture = nullptr;\n\tQRhiTexture *_vTexture = nullptr;\n\tQRhiTexture *_uvTexture = nullptr;\n\tQSize _lumaSize;\n\tQSize _chromaSize;\n\n\tQRhiGraphicsPipeline *_argb32Pipeline = nullptr;\n\tQRhiGraphicsPipeline *_yuv420Pipeline = nullptr;\n\tQRhiGraphicsPipeline *_nv12Pipeline = nullptr;\n\tQRhiGraphicsPipeline *_imagePipeline = nullptr;\n\tQRhiGraphicsPipeline *_imageBlendPipeline = nullptr;\n\tQRhiGraphicsPipeline *_controlsPipeline = nullptr;\n\n\t// Layout-only SRBs: used solely as pipeline layout templates,\n\t// not for actual draw calls. Per-draw SRBs come from _srbPool.\n\tQRhiShaderResourceBindings *_argb32Srb = nullptr;\n\tQRhiShaderResourceBindings *_yuv420Srb = nullptr;\n\tQRhiShaderResourceBindings *_nv12Srb = nullptr;\n\tQRhiShaderResourceBindings *_imageSrb = nullptr;\n\tQRhiShaderResourceBindings *_controlsSrb = nullptr;\n\n\tstd::vector<QRhiShaderResourceBindings*> _srbPool;\n\tint _nextSrbIndex = 0;\n\n\tUi::Rhi::Image _shadowImage;\n\tUi::Rhi::Image _radialImage;\n\tUi::Rhi::Image _controlsImage;\n\tUi::Rhi::Image _playbackImage;\n\tUi::Rhi::Image _volumeControllerImage;\n\n\tstatic constexpr auto kControlsCount = 7;\n\tstatic constexpr auto kMaxDraws = 12;\n\tstatic constexpr auto kVertexSlotSize = 4 * 6 * sizeof(float);\n\tstd::array<QRect, kControlsCount * 2> _controlsTextures;\n\tstd::vector<DrawCommand> _drawCommands;\n\tint _nextDrawSlot = 0;\n\n\tbool _initialized = false;\n\n#ifdef Q_OS_MAC\n\tMetalTextureCache _metalTextureCache;\n#endif\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Media::View\n\n#endif // Qt >= 6.7\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_playback_controls.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/view/media_view_playback_controls.h\"\n\n#include \"media/audio/media_audio.h\"\n#include \"media/player/media_player_button.h\"\n#include \"media/player/media_player_dropdown.h\"\n#include \"media/view/media_view_playback_progress.h\"\n#include \"ui/widgets/cross_fade_label.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/effects/fade_animation.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/menu/menu_item_base.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Media {\nnamespace View {\nnamespace  {\n\nconstexpr auto kEps = 0.005;\n\n} // namespace\n\nPlaybackControls::PlaybackControls(\n\tQWidget *parent,\n\tnot_null<Delegate*> delegate)\n: RpWidget(parent)\n, _delegate(delegate)\n, _speedControllable(Media::Audio::SupportsSpeedControl())\n, _qualitiesList(_delegate->playbackControlsQualities())\n, _playPauseResume(this, st::mediaviewPlayButton)\n, _playbackSlider(this, st::mediaviewPlayback)\n, _playbackProgress(std::make_unique<PlaybackProgress>())\n, _volumeToggle(this, st::mediaviewVolumeToggle)\n, _volumeController(this, st::mediaviewPlayback)\n, _speedToggle((_speedControllable || !_qualitiesList.empty())\n\t? object_ptr<Player::SettingsButton>(this, st::mediaviewSpeedButton)\n\t: nullptr)\n, _fullScreenToggle(this, st::mediaviewFullScreenButton)\n, _pictureInPicture(this, st::mediaviewPipButton)\n, _playedAlready(this, st::mediaviewPlayProgressLabel)\n, _toPlayLeft(this, st::mediaviewPlayProgressLabel)\n, _speedController(_speedToggle\n\t? std::make_unique<Player::SpeedController>(\n\t\t_speedToggle.data(),\n\t\t_speedToggle->st(),\n\t\tparent,\n\t\t[=](bool) {},\n\t\t(_speedControllable\n\t\t\t? [=](bool lastNonDefault) {\n\t\t\t\treturn speedLookup(lastNonDefault);\n\t\t\t}\n\t\t\t: Fn<float64(bool)>()),\n\t\t(_speedControllable\n\t\t\t? [=](float64 speed) { saveSpeed(speed); }\n\t\t\t: Fn<void(float64)>()),\n\t\t_qualitiesList,\n\t\t[=] { return _delegate->playbackControlsCurrentQuality(); },\n\t\t[=](int quality) { saveQuality(quality); })\n\t: nullptr)\n, _fadeAnimation(std::make_unique<Ui::FadeAnimation>(this)) {\n\t_fadeAnimation->show();\n\t_fadeAnimation->setFinishedCallback([=] {\n\t\tfadeFinished();\n\t});\n\t_fadeAnimation->setUpdatedCallback([=](float64 opacity) {\n\t\tfadeUpdated(opacity);\n\t});\n\n\t_speedToggle->setSpeed(_speedControllable\n\t\t? _delegate->playbackControlsCurrentSpeed(false)\n\t\t: 1.);\n\tupdateSpeedToggleQuality();\n\n\tif (const auto controller = _speedController.get()) {\n\t\tcontroller->menuToggledValue(\n\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\t_speedToggle->setActive(toggled);\n\t\t}, _speedToggle->lifetime());\n\t}\n\n\t_pictureInPicture->addClickHandler([=] {\n\t\t_delegate->playbackControlsToPictureInPicture();\n\t});\n\n\t_volumeController->setValue(_delegate->playbackControlsCurrentVolume());\n\t_volumeController->setChangeProgressCallback([=](float64 value) {\n\t\t_delegate->playbackControlsVolumeChanged(value);\n\t\tupdateVolumeToggleIcon();\n\t});\n\t_volumeController->setChangeFinishedCallback([=](float64) {\n\t\t_delegate->playbackControlsVolumeChangeFinished();\n\t});\n\tupdateVolumeToggleIcon();\n\t_volumeToggle->setClickedCallback([=] {\n\t\t_delegate->playbackControlsVolumeToggled();\n\t\t_volumeController->setValue(_delegate->playbackControlsCurrentVolume());\n\t\tupdateVolumeToggleIcon();\n\t});\n\n\t_playPauseResume->addClickHandler([=] {\n\t\tif (_showPause) {\n\t\t\t_delegate->playbackControlsPause();\n\t\t} else {\n\t\t\t_delegate->playbackControlsPlay();\n\t\t}\n\t});\n\t_fullScreenToggle->addClickHandler([=] {\n\t\tif (_inFullScreen) {\n\t\t\t_delegate->playbackControlsFromFullScreen();\n\t\t} else {\n\t\t\t_delegate->playbackControlsToFullScreen();\n\t\t}\n\t});\n\n\t_playbackProgress->setValueChangedCallback([=](\n\t\t\tfloat64 value,\n\t\t\tfloat64 receivedTill) {\n\t\t_playbackSlider->setValue(value, receivedTill);\n\t});\n\t_playbackSlider->setChangeProgressCallback([=](float64 value) {\n\t\t_playbackProgress->setValue(value, false);\n\n\t\t// This may destroy PlaybackControls.\n\t\thandleSeekProgress(value);\n\t});\n\t_playbackSlider->setChangeFinishedCallback([=](float64 value) {\n\t\t_playbackProgress->setValue(value, false);\n\t\thandleSeekFinished(value);\n\t});\n}\n\nvoid PlaybackControls::handleSeekProgress(float64 progress) {\n\tif (!_lastDurationMs) return;\n\n\tconst auto positionMs = std::clamp(\n\t\tstatic_cast<crl::time>(progress * _lastDurationMs),\n\t\tcrl::time(0),\n\t\t_lastDurationMs);\n\tif (_seekPositionMs != positionMs) {\n\t\t_seekPositionMs = positionMs;\n\t\trefreshTimeTexts();\n\t\tupdateTimestampLabel();\n\n\t\t// This may destroy PlaybackControls.\n\t\t_delegate->playbackControlsSeekProgress(positionMs);\n\t}\n}\n\nvoid PlaybackControls::handleSeekFinished(float64 progress) {\n\tif (!_lastDurationMs) return;\n\n\tconst auto positionMs = std::clamp(\n\t\tstatic_cast<crl::time>(progress * _lastDurationMs),\n\t\tcrl::time(0),\n\t\t_lastDurationMs);\n\t_seekPositionMs = -1;\n\t_delegate->playbackControlsSeekFinished(positionMs);\n\trefreshTimeTexts();\n}\n\ntemplate <typename Callback>\nvoid PlaybackControls::startFading(Callback start) {\n\tif (!_fadeAnimation->animating()) {\n\t\tshowChildren();\n\t\t_playbackSlider->disablePaint(true);\n\t\t_volumeController->disablePaint(true);\n\t\t_childrenHidden = false;\n\t}\n\tstart();\n\tif (_fadeAnimation->animating()) {\n\t\tfor (const auto child : children()) {\n\t\t\tif (child->isWidgetType()\n\t\t\t\t&& child != _playbackSlider\n\t\t\t\t&& child != _volumeController) {\n\t\t\t\tstatic_cast<QWidget*>(child)->hide();\n\t\t\t}\n\t\t}\n\t\t_childrenHidden = true;\n\t} else {\n\t\tfadeFinished();\n\t}\n\t_playbackSlider->disablePaint(false);\n\t_volumeController->disablePaint(false);\n}\n\nvoid PlaybackControls::showAnimated() {\n\tstartFading([this]() {\n\t\t_fadeAnimation->fadeIn(st::mediaviewShowDuration);\n\t});\n}\n\nvoid PlaybackControls::hideAnimated() {\n\tstartFading([this]() {\n\t\t_fadeAnimation->fadeOut(st::mediaviewHideDuration);\n\t});\n}\n\nvoid PlaybackControls::fadeFinished() {\n\tfadeUpdated(_fadeAnimation->visible() ? 1. : 0.);\n}\n\nvoid PlaybackControls::fadeUpdated(float64 opacity) {\n\t_playbackSlider->setFadeOpacity(opacity);\n\t_volumeController->setFadeOpacity(opacity);\n}\n\n\nfloat64 PlaybackControls::speedLookup(bool lastNonDefault) const {\n\treturn _delegate->playbackControlsCurrentSpeed(lastNonDefault);\n}\n\nvoid PlaybackControls::saveSpeed(float64 speed) {\n\t_speedToggle->setSpeed(speed);\n\t_delegate->playbackControlsSpeedChanged(speed);\n}\n\nvoid PlaybackControls::saveQuality(int quality) {\n\t_speedToggle->setQuality(_qualitiesList.empty() ? 0 : quality);\n\t_delegate->playbackControlsQualityChanged(quality);\n}\n\nvoid PlaybackControls::updateSpeedToggleQuality() {\n\tconst auto quality = _delegate->playbackControlsCurrentQuality();\n\t_speedToggle->setQuality(_qualitiesList.empty() ? 0 : quality.height);\n}\n\nvoid PlaybackControls::updatePlaybackSpeed(float64 speed) {\n\tDEBUG_LOG((\"Media playback speed: update to %1.\").arg(speed));\n\t_delegate->playbackControlsSpeedChanged(speed);\n\tresizeEvent(nullptr);\n}\n\nvoid PlaybackControls::updatePlayback(const Player::TrackState &state) {\n\tupdatePlayPauseResumeState(state);\n\t_playbackProgress->updateState(state, countDownloadedTillPercent(state));\n\tupdateTimeTexts(state);\n\tupdateTimestampLabel();\n}\n\nvoid PlaybackControls::updateVolumeToggleIcon() {\n\tconst auto volume = _delegate->playbackControlsCurrentVolume();\n\t_volumeToggle->setIconOverride([&] {\n\t\treturn (volume <= 0.)\n\t\t\t? nullptr\n\t\t\t: (volume < 1 / 2.)\n\t\t\t? &st::mediaviewVolumeIcon1\n\t\t\t: &st::mediaviewVolumeIcon2;\n\t}(), [&] {\n\t\treturn (volume <= 0.)\n\t\t\t? nullptr\n\t\t\t: (volume < 1 / 2.)\n\t\t\t? &st::mediaviewVolumeIcon1Over\n\t\t\t: &st::mediaviewVolumeIcon2Over;\n\t}());\n}\n\nfloat64 PlaybackControls::countDownloadedTillPercent(\n\t\tconst Player::TrackState &state) const {\n\tif (_loadingReady > 0 && _loadingReady == _loadingTotal) {\n\t\treturn 1.;\n\t}\n\tconst auto header = state.fileHeaderSize;\n\tif (!header || _loadingReady <= header || _loadingTotal <= header) {\n\t\treturn 0.;\n\t}\n\treturn (_loadingReady - header) / float64(_loadingTotal - header);\n}\n\nvoid PlaybackControls::setTimestamps(std::vector<TimestampData> timestamps) {\n\t_playbackSlider->clearDividers();\n\t_playbackSlider->setDividerStyle(\n\t\tUi::MediaSlider::DividerStyle::Gaps);\n\t_timestamps = std::move(timestamps);\n\tfor (const auto &ts : _timestamps) {\n\t\tif (ts.position > 0.) {\n\t\t\t_playbackSlider->addDivider(\n\t\t\t\tts.position,\n\t\t\t\tst::mediaviewPlaybackTimestamp);\n\t\t}\n\t}\n\tconst auto hasLabels = ranges::any_of(\n\t\t_timestamps,\n\t\t[](const auto &ts) { return !ts.label.isEmpty(); });\n\tif (hasLabels && !_timestampLabel) {\n\t\t_timestampLabel.create(\n\t\t\tthis,\n\t\t\tst::mediaviewTimestampLabel);\n\t\t_timestampLabel->show();\n\t} else if (!hasLabels) {\n\t\t_timestampLabel.destroy();\n\t}\n\t_playbackSlider->update();\n\t_currentTimestampIndex = -1;\n\tupdateTimestampLabel();\n}\n\nbool PlaybackControls::hasTimestamps() const {\n\treturn _timestampLabel != nullptr;\n}\n\nauto PlaybackControls::nextTimestamp(float64 progress) const\n-> std::optional<TimestampData> {\n\tfor (const auto &ts : _timestamps) {\n\t\tif (ts.position > progress + kEps) {\n\t\t\treturn ts;\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nauto PlaybackControls::prevTimestamp(float64 progress) const\n-> std::optional<TimestampData> {\n\tfor (auto i = int(_timestamps.size()) - 1; i >= 0; --i) {\n\t\tif (_timestamps[i].position < progress - kEps) {\n\t\t\treturn _timestamps[i];\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nvoid PlaybackControls::updateTimestampLabel() {\n\tif (!_timestampLabel) {\n\t\treturn;\n\t}\n\tconst auto progress = _playbackSlider->value();\n\tauto index = -1;\n\tfor (auto i = int(_timestamps.size()) - 1; i >= 0; --i) {\n\t\tif (_timestamps[i].position <= progress) {\n\t\t\tindex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (index == _currentTimestampIndex) {\n\t\treturn;\n\t}\n\tconst auto previous = _currentTimestampIndex;\n\t_currentTimestampIndex = index;\n\t_timestampLabel->setDirection((index > previous) ? 1 : -1);\n\tauto text = (index >= 0) ? _timestamps[index].label : QString();\n\tconst auto maxWidth = _playbackSlider->width();\n\tif (maxWidth > 0\n\t\t&& st::mediaviewTimestampLabel.font->width(text) > maxWidth) {\n\t\ttext = st::mediaviewTimestampLabel.font->elided(text, maxWidth);\n\t}\n\t_timestampLabel->setText(text);\n\tresizeEvent(nullptr);\n\trefreshFadeCache();\n}\n\nvoid PlaybackControls::setLoadingProgress(int64 ready, int64 total) {\n\tif (_loadingReady == ready && _loadingTotal == total) {\n\t\treturn;\n\t}\n\t_loadingReady = ready;\n\t_loadingTotal = total;\n\tif (_loadingReady != 0 && _loadingReady != _loadingTotal) {\n\t\tif (!_downloadProgress) {\n\t\t\t_downloadProgress.create(this, st::mediaviewPlayProgressLabel);\n\t\t\t_downloadProgress->setVisible(!_fadeAnimation->animating());\n\t\t\t_loadingPercent = -1;\n\t\t}\n\t\tconst auto progress = total ? (ready / float64(total)) : 0.;\n\t\tconst auto percent = int(base::SafeRound(progress * 100));\n\t\tif (_loadingPercent != percent) {\n\t\t\t_loadingPercent = percent;\n\t\t\t_downloadProgress->setText(QString::number(percent) + '%');\n\t\t\tupdateDownloadProgressPosition();\n\t\t\trefreshFadeCache();\n\t\t}\n\t} else {\n\t\t_downloadProgress.destroy();\n\t}\n}\n\nvoid PlaybackControls::refreshFadeCache() {\n\tif (!_fadeAnimation->animating()) {\n\t\treturn;\n\t}\n\tstartFading([&] {\n\t\t_fadeAnimation->refreshCache();\n\t});\n}\n\nvoid PlaybackControls::updatePlayPauseResumeState(const Player::TrackState &state) {\n\tauto showPause = ShowPauseIcon(state.state) || (_seekPositionMs >= 0);\n\tif (showPause != _showPause) {\n\t\t_showPause = showPause;\n\t\t_playPauseResume->setIconOverride(_showPause ? &st::mediaviewPauseIcon : nullptr, _showPause ? &st::mediaviewPauseIconOver : nullptr);\n\t}\n}\n\nvoid PlaybackControls::updateTimeTexts(const Player::TrackState &state) {\n\tqint64 position = 0;\n\n\tif (Player::IsStoppedAtEnd(state.state)) {\n\t\tposition = state.length;\n\t} else if (!Player::IsStoppedOrStopping(state.state)) {\n\t\tposition = state.position;\n\t} else {\n\t\tposition = 0;\n\t}\n\tauto playFrequency = state.frequency;\n\tauto playAlready = position / playFrequency;\n\tauto playLeft = (state.length / playFrequency) - playAlready;\n\n\t_lastDurationMs = (state.length * crl::time(1000)) / playFrequency;\n\n\t_timeAlready = Ui::FormatDurationText(playAlready);\n\tauto minus = QChar(8722);\n\t_timeLeft = minus + Ui::FormatDurationText(playLeft);\n\n\tif (_seekPositionMs < 0) {\n\t\trefreshTimeTexts();\n\t}\n}\n\nvoid PlaybackControls::refreshTimeTexts() {\n\tauto alreadyChanged = false, leftChanged = false;\n\tauto timeAlready = _timeAlready;\n\tauto timeLeft = _timeLeft;\n\tif (_seekPositionMs >= 0) {\n\t\tauto playAlready = _seekPositionMs / crl::time(1000);\n\t\tauto playLeft = (_lastDurationMs / crl::time(1000)) - playAlready;\n\n\t\ttimeAlready = Ui::FormatDurationText(playAlready);\n\t\tauto minus = QChar(8722);\n\t\ttimeLeft = minus + Ui::FormatDurationText(playLeft);\n\t}\n\n\t_playedAlready->setText(timeAlready, &alreadyChanged);\n\t_toPlayLeft->setText(timeLeft, &leftChanged);\n\tif (alreadyChanged || leftChanged) {\n\t\tresizeEvent(nullptr);\n\t\trefreshFadeCache();\n\t}\n}\n\nvoid PlaybackControls::setInFullScreen(bool inFullScreen) {\n\tif (_inFullScreen != inFullScreen) {\n\t\t_inFullScreen = inFullScreen;\n\t\t_fullScreenToggle->setIconOverride(\n\t\t\t_inFullScreen ? &st::mediaviewFullScreenOutIcon : nullptr,\n\t\t\t_inFullScreen ? &st::mediaviewFullScreenOutIconOver : nullptr);\n\t}\n}\n\nvoid PlaybackControls::resizeEvent(QResizeEvent *e) {\n\tconst auto textSkip = st::mediaviewPlayProgressSkip;\n\tconst auto textLeft = st::mediaviewPlayProgressLeft;\n\tconst auto textTop = st::mediaviewPlayProgressTop;\n\t_playedAlready->moveToLeft(textLeft + textSkip, textTop);\n\t_toPlayLeft->moveToRight(textLeft + textSkip, textTop);\n\tconst auto remove = 2 * textLeft + 4 * textSkip + _playedAlready->width() + _toPlayLeft->width();\n\tauto playbackWidth = width() - remove;\n\t_playbackSlider->resize(playbackWidth, st::mediaviewPlayback.seekSize.height());\n\t_playbackSlider->moveToLeft(textLeft + 2 * textSkip + _playedAlready->width(), st::mediaviewPlaybackTop);\n\n\tif (_timestampLabel) {\n\t\t_timestampLabel->resize(\n\t\t\t_playbackSlider->width(),\n\t\t\t_timestampLabel->height());\n\t\t_timestampLabel->moveToLeft(\n\t\t\t_playbackSlider->x(),\n\t\t\tst::mediaviewTimestampLabelTop);\n\t}\n\n\t_playPauseResume->moveToLeft(\n\t\t(width() - _playPauseResume->width()) / 2,\n\t\tst::mediaviewPlayButtonTop);\n\n\tauto right = st::mediaviewButtonsRight;\n\tif (_speedToggle) {\n\t\t_speedToggle->moveToRight(right, st::mediaviewButtonsTop);\n\t\tright += _speedToggle->width() + st::mediaviewPipButtonSkip;\n\t}\n\t_pictureInPicture->moveToRight(right, st::mediaviewButtonsTop);\n\tright += _pictureInPicture->width() + st::mediaviewFullScreenButtonSkip;\n\t_fullScreenToggle->moveToRight(right, st::mediaviewButtonsTop);\n\n\tupdateDownloadProgressPosition();\n\n\tauto left = st::mediaviewVolumeToggleLeft;\n\t_volumeToggle->moveToLeft(left, st::mediaviewVolumeTop);\n\tleft += _volumeToggle->width() + st::mediaviewVolumeSkip;\n\t_volumeController->resize(\n\t\tst::mediaviewVolumeWidth,\n\t\tst::mediaviewPlayback.seekSize.height());\n\t_volumeController->moveToLeft(left, st::mediaviewVolumeTop + (_volumeToggle->height() - _volumeController->height()) / 2);\n}\n\nvoid PlaybackControls::updateDownloadProgressPosition() {\n\tif (!_downloadProgress) {\n\t\treturn;\n\t}\n\tconst auto left = _playPauseResume->x() + _playPauseResume->width();\n\tconst auto right = _fullScreenToggle->x();\n\tconst auto available = right - left;\n\tconst auto x = left + (available - _downloadProgress->width()) / 2;\n\tconst auto y = _playPauseResume->y() + (_playPauseResume->height() - _downloadProgress->height()) / 2;\n\t_downloadProgress->move(x, y);\n}\n\nvoid PlaybackControls::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tif (_fadeAnimation->paint(p)) {\n\t\treturn;\n\t}\n\tif (_childrenHidden) {\n\t\tshowChildren();\n\t\t_playbackSlider->setFadeOpacity(1.);\n\t\t_volumeController->setFadeOpacity(1.);\n\t\t_childrenHidden = false;\n\t}\n\tUi::FillRoundRect(p, rect(), st::mediaviewSaveMsgBg, Ui::MediaviewSaveCorners);\n}\n\nvoid PlaybackControls::mousePressEvent(QMouseEvent *e) {\n\te->accept(); // Don't pass event to the Media::View::OverlayWidget.\n}\n\nbool PlaybackControls::hasMenu() const {\n\treturn _speedController && _speedController->menu();\n}\n\nbool PlaybackControls::dragging() const {\n\treturn _volumeController->isChanging()\n\t\t|| _playbackSlider->isChanging()\n\t\t|| _playPauseResume->isOver()\n\t\t|| _volumeToggle->isOver()\n\t\t|| (_speedToggle && _speedToggle->isOver())\n\t\t|| _fullScreenToggle->isOver()\n\t\t|| _pictureInPicture->isOver()\n\t\t|| hasMenu();\n}\n\nPlaybackControls::~PlaybackControls() = default;\n\n} // namespace View\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_playback_controls.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"base/object_ptr.h\"\n#include \"media/media_common.h\"\n\nnamespace Ui {\nclass CrossFadeLabel;\nclass LabelSimple;\nclass FadeAnimation;\nclass IconButton;\nclass MediaSlider;\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Media::Player {\nstruct TrackState;\nclass SettingsButton;\nclass SpeedController;\n} // namespace Media::Player\n\nnamespace Media::View {\n\nclass PlaybackProgress;\n\nclass PlaybackControls : public Ui::RpWidget {\npublic:\n\tclass Delegate {\n\tpublic:\n\t\tvirtual void playbackControlsPlay() = 0;\n\t\tvirtual void playbackControlsPause() = 0;\n\t\tvirtual void playbackControlsSeekProgress(crl::time position) = 0;\n\t\tvirtual void playbackControlsSeekFinished(crl::time position) = 0;\n\t\tvirtual void playbackControlsVolumeChanged(float64 volume) = 0;\n\t\t[[nodiscard]] virtual float64 playbackControlsCurrentVolume() = 0;\n\t\tvirtual void playbackControlsVolumeToggled() = 0;\n\t\tvirtual void playbackControlsVolumeChangeFinished() = 0;\n\t\tvirtual void playbackControlsSpeedChanged(float64 speed) = 0;\n\t\t[[nodiscard]] virtual float64 playbackControlsCurrentSpeed(\n\t\t\tbool lastNonDefault) = 0;\n\t\t[[nodiscard]] virtual auto playbackControlsQualities()\n\t\t\t-> std::vector<int> = 0;\n\t\t[[nodiscard]] virtual auto playbackControlsCurrentQuality()\n\t\t\t-> VideoQuality = 0;\n\t\tvirtual void playbackControlsQualityChanged(int quality) = 0;\n\t\tvirtual void playbackControlsToFullScreen() = 0;\n\t\tvirtual void playbackControlsFromFullScreen() = 0;\n\t\tvirtual void playbackControlsToPictureInPicture() = 0;\n\t\tvirtual void playbackControlsRotate() = 0;\n\t};\n\n\tstruct TimestampData {\n\t\tfloat64 position = 0.;\n\t\tQString label;\n\t};\n\n\tPlaybackControls(QWidget *parent, not_null<Delegate*> delegate);\n\t~PlaybackControls();\n\n\tvoid showAnimated();\n\tvoid hideAnimated();\n\n\tvoid updatePlayback(const Player::TrackState &state);\n\tvoid setLoadingProgress(int64 ready, int64 total);\n\tvoid setTimestamps(std::vector<TimestampData> timestamps);\n\tvoid setInFullScreen(bool inFullScreen);\n\t[[nodiscard]] bool hasTimestamps() const;\n\t[[nodiscard]] std::optional<TimestampData> nextTimestamp(\n\t\tfloat64 progress) const;\n\t[[nodiscard]] std::optional<TimestampData> prevTimestamp(\n\t\tfloat64 progress) const;\n\t[[nodiscard]] bool hasMenu() const;\n\t[[nodiscard]] bool dragging() const;\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\nprivate:\n\tvoid handleSeekProgress(float64 progress);\n\tvoid handleSeekFinished(float64 progress);\n\n\ttemplate <typename Callback>\n\tvoid startFading(Callback start);\n\tvoid fadeFinished();\n\tvoid fadeUpdated(float64 opacity);\n\tvoid refreshFadeCache();\n\t[[nodiscard]] float64 countDownloadedTillPercent(\n\t\tconst Player::TrackState &state) const;\n\n\tvoid updatePlaybackSpeed(float64 speed);\n\tvoid updateVolumeToggleIcon();\n\tvoid updateDownloadProgressPosition();\n\n\tvoid updatePlayPauseResumeState(const Player::TrackState &state);\n\tvoid updateTimeTexts(const Player::TrackState &state);\n\tvoid refreshTimeTexts();\n\n\t[[nodiscard]] float64 speedLookup(bool lastNonDefault) const;\n\tvoid saveSpeed(float64 speed);\n\n\tvoid saveQuality(int quality);\n\tvoid updateSpeedToggleQuality();\n\tvoid updateTimestampLabel();\n\n\tconst not_null<Delegate*> _delegate;\n\n\tbool _speedControllable = false;\n\tstd::vector<int> _qualitiesList;\n\n\tbool _inFullScreen = false;\n\tbool _showPause = false;\n\tbool _childrenHidden = false;\n\tQString _timeAlready, _timeLeft;\n\tcrl::time _seekPositionMs = -1;\n\tcrl::time _lastDurationMs = 0;\n\tint64 _loadingReady = 0;\n\tint64 _loadingTotal = 0;\n\tint _loadingPercent = 0;\n\n\tobject_ptr<Ui::IconButton> _playPauseResume;\n\tobject_ptr<Ui::MediaSlider> _playbackSlider;\n\tstd::unique_ptr<PlaybackProgress> _playbackProgress;\n\tstd::unique_ptr<PlaybackProgress> _receivedTillProgress;\n\tobject_ptr<Ui::IconButton> _volumeToggle;\n\tobject_ptr<Ui::MediaSlider> _volumeController;\n\tobject_ptr<Player::SettingsButton> _speedToggle;\n\tobject_ptr<Ui::IconButton> _fullScreenToggle;\n\tobject_ptr<Ui::IconButton> _pictureInPicture;\n\tobject_ptr<Ui::LabelSimple> _playedAlready;\n\tobject_ptr<Ui::LabelSimple> _toPlayLeft;\n\tobject_ptr<Ui::LabelSimple> _downloadProgress = { nullptr };\n\tobject_ptr<Ui::CrossFadeLabel> _timestampLabel = { nullptr };\n\tstd::vector<TimestampData> _timestamps;\n\tint _currentTimestampIndex = -1;\n\tstd::unique_ptr<Player::SpeedController> _speedController;\n\tstd::unique_ptr<Ui::FadeAnimation> _fadeAnimation;\n\n};\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_playback_progress.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/view/media_view_playback_progress.h\"\n\n#include \"media/audio/media_audio.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Media {\nnamespace View {\nnamespace {\n\nconstexpr auto kPlaybackAnimationDurationMs = crl::time(200);\n\n} // namespace\n\nPlaybackProgress::PlaybackProgress()\n: _valueAnimation([=](crl::time now) {\n\treturn valueAnimationCallback(now);\n})\n, _availableTillAnimation([=](crl::time now) {\n\treturn availableTillAnimationCallback(now);\n}) {\n}\n\nvoid PlaybackProgress::updateState(\n\t\tconst Player::TrackState &state,\n\t\tfloat64 loadedTillPercent) {\n\t_playing = !Player::IsStopped(state.state);\n\tconst auto length = state.length;\n\tconst auto position = Player::IsStoppedAtEnd(state.state)\n\t\t? state.length\n\t\t: Player::IsStoppedOrStopping(state.state)\n\t\t? 0\n\t\t: state.position;\n\tconst auto receivedTill = (length && state.receivedTill > position)\n\t\t? state.receivedTill\n\t\t: -1;\n\tconst auto loadedTill = (loadedTillPercent != 0.)\n\t\t? int64(std::floor(loadedTillPercent * length))\n\t\t: -1;\n\tconst auto availableTill = (length && loadedTill > position)\n\t\t? std::max(receivedTill, loadedTill)\n\t\t: receivedTill;\n\n\tconst auto wasInLoadingState = _inLoadingState;\n\tif (wasInLoadingState) {\n\t\t_inLoadingState = false;\n\t\tif (_inLoadingStateChanged) {\n\t\t\t_inLoadingStateChanged(false);\n\t\t}\n\t}\n\n\tconst auto progress = (position > length)\n\t\t? 1.\n\t\t: length\n\t\t? std::clamp(float64(position) / length, 0., 1.)\n\t\t: 0.;\n\tconst auto availableTillProgress = (availableTill > position)\n\t\t? std::clamp(float64(availableTill) / length, 0., 1.)\n\t\t: -1.;\n\tconst auto animatedPosition = position + (state.frequency * kPlaybackAnimationDurationMs / 1000);\n\tconst auto animatedProgress = length ? qMax(float64(animatedPosition) / length, 0.) : 0.;\n\tif (length != _length || position != _position || wasInLoadingState) {\n\t\tconst auto animated = length\n\t\t\t&& _length\n\t\t\t&& (animatedProgress > value())\n\t\t\t&& (position > _position)\n\t\t\t&& (position < _position + state.frequency);\n\t\tif (animated) {\n\t\t\tsetValue(animatedProgress, animated);\n\t\t} else {\n\t\t\tsetValue(progress, animated);\n\t\t}\n\t\t_position = position;\n\t\t_length = length;\n\t}\n\tif (availableTill != _availableTill) {\n\t\tsetAvailableTill(availableTillProgress);\n\t\t_availableTill = availableTill;\n\t}\n}\n\nvoid PlaybackProgress::updateLoadingState(float64 progress) {\n\tif (!_inLoadingState) {\n\t\t_inLoadingState = true;\n\t\tif (_inLoadingStateChanged) {\n\t\t\t_inLoadingStateChanged(true);\n\t\t}\n\t}\n\tauto animated = (progress > value());\n\tsetValue(progress, animated);\n}\n\nfloat64 PlaybackProgress::value() const {\n\treturn qMin(a_value.current(), 1.);\n}\n\nvoid PlaybackProgress::setValue(float64 value, bool animated) {\n\tif (animated) {\n\t\tvalueAnimationCallback(crl::now());\n\t\ta_value.start(value);\n\t\t_valueAnimation.start();\n\t} else {\n\t\ta_value = anim::value(value, value);\n\t\t_valueAnimation.stop();\n\t}\n\temitUpdatedValue();\n}\n\nvoid PlaybackProgress::setAvailableTill(float64 value) {\n\tconst auto current = a_availableTill.current();\n\tif (value > current && current > 0.) {\n\t\tavailableTillAnimationCallback(crl::now());\n\t\ta_availableTill.start(value);\n\t\t_availableTillAnimation.start();\n\t} else if (value > a_value.current()) {\n\t\ta_availableTill = anim::value(a_value.current(), value);\n\t\t_availableTillAnimation.start();\n\t} else {\n\t\ta_availableTill = anim::value(-1., -1.);\n\t\t_availableTillAnimation.stop();\n\t}\n\temitUpdatedValue();\n}\n\nbool PlaybackProgress::valueAnimationCallback(float64 now) {\n\tconst auto time = (now - _valueAnimation.started());\n\tconst auto dt = anim::Disabled()\n\t\t? 1.\n\t\t: (time / kPlaybackAnimationDurationMs);\n\tif (dt >= 1.) {\n\t\ta_value.finish();\n\t} else {\n\t\ta_value.update(dt, anim::linear);\n\t}\n\temitUpdatedValue();\n\treturn (dt < 1.);\n}\n\nbool PlaybackProgress::availableTillAnimationCallback(float64 now) {\n\tconst auto time = now - _availableTillAnimation.started();\n\tconst auto dt = anim::Disabled()\n\t\t? 1.\n\t\t: (time / kPlaybackAnimationDurationMs);\n\tif (dt >= 1.) {\n\t\ta_availableTill.finish();\n\t} else {\n\t\ta_availableTill.update(dt, anim::linear);\n\t}\n\temitUpdatedValue();\n\treturn (dt < 1.);\n}\n\nvoid PlaybackProgress::emitUpdatedValue() {\n\tif (_valueChanged) {\n\t\tconst auto value = a_value.current();\n\t\tconst auto availableTill = a_availableTill.current();\n\t\t_valueChanged(value, std::max(value, availableTill));\n\t}\n}\n\n} // namespace View\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_playback_progress.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/continuous_sliders.h\"\n\nnamespace Media {\nnamespace Player {\nstruct TrackState;\n} // namespace Player\n\nnamespace View {\n\nclass PlaybackProgress {\npublic:\n\tPlaybackProgress();\n\n\tvoid setValueChangedCallback(Fn<void(float64,float64)> callback) {\n\t\t_valueChanged = std::move(callback);\n\t}\n\tvoid setInLoadingStateChangedCallback(Fn<void(bool)> callback) {\n\t\t_inLoadingStateChanged = std::move(callback);\n\t}\n\tvoid setValue(float64 value, bool animated);\n\t[[nodiscard]] float64 value() const;\n\n\tvoid updateState(\n\t\tconst Player::TrackState &state,\n\t\tfloat64 loadedTillPercent = 0.);\n\tvoid updateLoadingState(float64 progress);\n\nprivate:\n\tbool valueAnimationCallback(float64 now);\n\tbool availableTillAnimationCallback(float64 now);\n\tvoid setAvailableTill(float64 value);\n\tvoid emitUpdatedValue();\n\n\t// This can animate for a very long time (like in music playing),\n\t// so it should be a Basic, not a Simple animation, because\n\t// Simple-s pauses mtproto responses/updates handling while playing.\n\tanim::value a_value, a_availableTill;\n\tUi::Animations::Basic _valueAnimation, _availableTillAnimation;\n\tFn<void(float64,float64)> _valueChanged;\n\n\tbool _inLoadingState = false;\n\tFn<void(bool)> _inLoadingStateChanged;\n\n\tint64 _position = 0;\n\tint64 _length = 0;\n\tint64 _availableTill = -1;\n\n\tbool _playing = false;\n\n};\n\n} // namespace View\n} // namespace Media\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_playback_sponsored.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/view/media_view_playback_sponsored.h\"\n\n#include \"boxes/premium_preview_box.h\"\n#include \"data/components/sponsored_messages.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_sponsored.h\"\n#include \"ui/effects/numbers_animation.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Media::View {\nnamespace {\n\nconstexpr auto kStartDelayMin = crl::time(1000);\nconstexpr auto kDurationMin = 5 * crl::time(1000);\n\nenum class Action {\n\tClose,\n\tPromotePremium,\n\tPause,\n\tUnpause,\n};\n\nclass Close final : public Ui::RippleButton {\npublic:\n\tClose(\n\t\tnot_null<QWidget*> parent,\n\t\tconst style::RippleAnimation &st,\n\t\trpl::producer<crl::time> allowCloseAt);\n\n\t[[nodiscard]] rpl::producer<Action> actions() const;\n\nprivate:\n\tQPoint prepareRippleStartPosition() const override;\n\tQImage prepareRippleMask() const override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid updateProgress(crl::time now);\n\n\trpl::event_stream<Action> _actions;\n\n\tUi::NumbersAnimation _countdown;\n\tUi::Animations::Basic _progress;\n\tbase::Timer _noAnimationTimer;\n\tcrl::time _allowCloseAt = 0;\n\tcrl::time _startedAt = 0;\n\tcrl::time _pausedAt = 0;\n\tint _secondsTill = 0;\n\tint _rippleSize = 0;\n\tQPoint _rippleOrigin;\n\tbool _allowClose = false;\n\n};\n\nClose::Close(\n\tnot_null<QWidget*> parent,\n\tconst style::RippleAnimation &st,\n\trpl::producer<crl::time> allowCloseAt)\n: RippleButton(parent, st)\n, _countdown(st::mediaSponsoredCloseFont, [=] { update(); })\n, _progress([=](crl::time now) { updateProgress(now); })\n, _noAnimationTimer([=] { updateProgress(crl::now()); })\n, _startedAt(crl::now()) {\n\tresize(st::mediaSponsoredCloseFull, st::mediaSponsoredCloseFull);\n\n\tconst auto size = st::mediaSponsoredCloseRipple;\n\tconst auto cut = int(base::SafeRound((width() - size) / 2.));\n\t_rippleSize = std::min(width() - 2 * cut, height() - 2 * cut);\n\t_rippleOrigin = QPoint(\n\t\t(width() - _rippleSize) / 2,\n\t\t(height() - _rippleSize) / 2);\n\n\tstd::move(\n\t\tallowCloseAt\n\t) | rpl::on_next([=](crl::time at) {\n\t\tconst auto now = crl::now();\n\t\tif (!at) {\n\t\t\tupdateProgress(now);\n\t\t\t_pausedAt = now;\n\t\t\t_progress.stop();\n\t\t} else {\n\t\t\tif (_pausedAt) {\n\t\t\t\t_startedAt += now - base::take(_pausedAt);\n\t\t\t}\n\t\t\t_allowCloseAt = at;\n\t\t\tupdateProgress(now);\n\t\t\tif (!anim::Disabled()) {\n\t\t\t\t_progress.start();\n\t\t\t} else if (!_allowClose) {\n\t\t\t\t_noAnimationTimer.callEach(crl::time(200));\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\tupdateProgress(_startedAt);\n\n\tsetClickedCallback([=] {\n\t\t_actions.fire(_allowClose ? Action::Close : Action::PromotePremium);\n\t});\n}\n\nrpl::producer<Action> Close::actions() const {\n\treturn _actions.events();\n}\n\nvoid Close::updateProgress(crl::time now) {\n\tupdate();\n}\n\nQPoint Close::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos()) - _rippleOrigin;\n}\n\nQImage Close::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::EllipseMask({ _rippleSize, _rippleSize });\n}\n\nvoid Close::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tpaintRipple(p, _rippleOrigin);\n\n\tconst auto now = crl::now();\n\tif (!_pausedAt) {\n\t\t_allowClose = (now >= _allowCloseAt);\n\t}\n\tconst auto msTill = _allowCloseAt - (_pausedAt ? _pausedAt : now);\n\tconst auto msFull = _allowCloseAt - _startedAt;\n\tconst auto secondsTill = (std::max(msTill, crl::time()) + 999) / 1000;\n\tconst auto secondsFull = (std::max(msFull, crl::time()) + 999) / 1000;\n\tconst auto allowCloseLeft = anim::Disabled()\n\t\t? (secondsFull ? (secondsTill / float64(secondsFull)) : 0)\n\t\t: std::max(msFull ? (msTill / float64(msFull)) : 0., 0.);\n\tconst auto duration = crl::time(st::fadeWrapDuration);\n\tconst auto allowedProgress = anim::Disabled()\n\t\t? (secondsTill ? 0. : 1.)\n\t\t: std::clamp(-msTill, crl::time(), duration) / float64(duration);\n\n\tif (_secondsTill != secondsTill) {\n\t\tconst auto initial = !_secondsTill;\n\t\t_secondsTill = secondsTill;\n\t\t_countdown.setText(QString::number(_secondsTill), _secondsTill);\n\t\tif (initial) {\n\t\t\t_countdown.finishAnimating();\n\t\t}\n\t}\n\n\tauto pen = st::mediaviewTextLinkFg->p;\n\tif (allowedProgress < 1.) {\n\t\tif (allowedProgress > 0.) {\n\t\t\tp.setOpacity(1. - allowedProgress);\n\t\t}\n\t\tp.setPen(pen);\n\n\t\tconst auto inner = QRect(\n\t\t\t(width() - st::mediaSponsoredCloseDiameter) / 2,\n\t\t\t(height() - st::mediaSponsoredCloseDiameter) / 2,\n\t\t\tst::mediaSponsoredCloseDiameter,\n\t\t\tst::mediaSponsoredCloseDiameter);\n\t\t_countdown.paint(\n\t\t\tp,\n\t\t\tinner.x() + (inner.width() - _countdown.countWidth()) / 2,\n\t\t\t(inner.y()\n\t\t\t\t+ (inner.height()\n\t\t\t\t\t- st::mediaSponsoredCloseFont->height) / 2),\n\t\t\twidth());\n\n\t\tconst auto skip = 0.23;\n\t\tconst auto len = int(base::SafeRound(\n\t\t\tarc::kFullLength * (1. - skip) * allowCloseLeft));\n\t\tif (len > 0) {\n\t\t\tconst auto from = arc::kFullLength / 4;\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tpen.setWidthF(st::mediaSponsoredCloseStroke);\n\t\t\tpen.setCapStyle(Qt::RoundCap);\n\t\t\tp.setPen(pen);\n\t\t\tp.drawArc(inner, from, len);\n\t\t}\n\n\t\tp.setOpacity(1.);\n\t}\n\n\tconst auto sizeFinal = st::mediaSponsoredCloseSize;\n\tconst auto sizeSmall = st::mediaSponsoredCloseCorner;\n\tconst auto twiceFinal = st::mediaSponsoredCloseTwice;\n\tconst auto twiceSmall = st::mediaSponsoredCloseSmall;\n\tconst auto size = sizeSmall + allowedProgress * (sizeFinal - sizeSmall);\n\tconst auto twice = twiceSmall\n\t\t+ allowedProgress * (twiceFinal - twiceSmall);\n\tconst auto leftFinal = (width() - size) / 2.;\n\tconst auto leftSmall = (width() + st::mediaSponsoredCloseDiameter) / 2.\n\t\t- (st::mediaSponsoredCloseStroke / 2.)\n\t\t- sizeSmall;\n\tconst auto topFinal = (height() - size) / 2.;\n\tconst auto topSmall = (height() - st::mediaSponsoredCloseDiameter) / 2.;\n\tconst auto left = leftSmall + allowedProgress * (leftFinal - leftSmall);\n\tconst auto top = topSmall + allowedProgress * (topFinal - topSmall);\n\n\tauto hq = PainterHighQualityEnabler(p);\n\tpen.setWidthF(twice / 2.);\n\tp.setPen(pen);\n\tp.drawLine(QPointF(left, top), QPointF(left + size, top + size));\n\tp.drawLine(QPointF(left + size, top), QPointF(left, top + size));\n}\n\n[[nodiscard]] style::RoundButton PrepareAboutStyle() {\n\tstatic auto textBg = style::complex_color([] {\n\t\tauto result = st::mediaviewTextLinkFg->c;\n\t\tresult.setAlphaF(result.alphaF() * 0.1);\n\t\treturn result;\n\t});\n\tstatic auto textBgOver = style::complex_color([] {\n\t\tauto result = st::mediaviewTextLinkFg->c;\n\t\tresult.setAlphaF(result.alphaF() * 0.15);\n\t\treturn result;\n\t});\n\tstatic auto rippleColor = style::complex_color([] {\n\t\tauto result = st::mediaviewTextLinkFg->c;\n\t\tresult.setAlphaF(result.alphaF() * 0.2);\n\t\treturn result;\n\t});\n\n\tauto result = st::mediaSponsoredAbout;\n\tresult.textFg = st::mediaviewTextLinkFg;\n\tresult.textFgOver = st::mediaviewTextLinkFg;\n\tresult.textBg = textBg.color();\n\tresult.textBgOver = textBgOver.color();\n\tresult.ripple.color = rippleColor.color();\n\treturn result;\n}\n\n} // namespace\n\nclass PlaybackSponsored::Message final : public Ui::RpWidget {\npublic:\n\tMessage(\n\t\tQWidget *parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::SponsoredMessage &data,\n\t\trpl::producer<crl::time> allowCloseAt);\n\n\t[[nodiscard]] rpl::producer<Action> actions() const;\n\n\tvoid setFinalPosition(int x, int y);\n\n\tvoid fadeIn();\n\tvoid fadeOut(Fn<void()> hidden);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid populate();\n\tvoid startFadeIn();\n\tvoid updateShown(Fn<void()> finished = nullptr);\n\tvoid startFade(Fn<void()> finished);\n\n\tconst not_null<Main::Session*> _session;\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst Data::SponsoredMessage _data;\n\n\tstyle::RoundButton _aboutSt;\n\tstd::unique_ptr<Ui::RoundButton> _about;\n\tstd::unique_ptr<Close> _close;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\trpl::event_stream<Action> _actions;\n\n\tstd::shared_ptr<Data::PhotoMedia> _photo;\n\tUi::Text::String _title;\n\tUi::Text::String _text;\n\n\tQPoint _finalPosition;\n\tint _left = 0;\n\tint _top = 0;\n\tint _titleHeight = 0;\n\tint _textHeight = 0;\n\n\tQImage _cache;\n\tUi::Animations::Simple _showAnimation;\n\tbool _shown = false;\n\tbool _over = false;\n\tbool _pressed = false;\n\n\trpl::lifetime _photoLifetime;\n\n};\n\nPlaybackSponsored::Message::Message(\n\tQWidget *parent,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst Data::SponsoredMessage &data,\n\trpl::producer<crl::time> allowCloseAt)\n: RpWidget(parent)\n, _session(&data.history->session())\n, _show(std::move(show))\n, _data(data)\n, _aboutSt(PrepareAboutStyle())\n, _about(std::make_unique<Ui::RoundButton>(\n\tthis,\n\ttr::lng_search_sponsored_button(),\n\t_aboutSt))\n, _close(\n\tstd::make_unique<Close>(\n\t\tthis,\n\t\t_aboutSt.ripple,\n\t\tstd::move(allowCloseAt))) {\n\tsetMouseTracking(true);\n\tpopulate();\n\thide();\n}\n\nrpl::producer<Action> PlaybackSponsored::Message::actions() const {\n\treturn rpl::merge(_actions.events(), _close->actions());\n}\n\nvoid PlaybackSponsored::Message::setFinalPosition(int x, int y) {\n\t_finalPosition = { x, y };\n\tif (_shown) {\n\t\tupdateShown();\n\t}\n}\n\nvoid PlaybackSponsored::Message::fadeIn() {\n\t_shown = true;\n\tif (!_photo || _photo->loaded()) {\n\t\tstartFadeIn();\n\t\treturn;\n\t}\n\t_photo->owner()->session().downloaderTaskFinished(\n\t) | rpl::filter([=] {\n\t\treturn _photo->loaded();\n\t}) | rpl::on_next([=] {\n\t\t_photoLifetime.destroy();\n\t\tstartFadeIn();\n\t}, _photoLifetime);\n}\n\nvoid PlaybackSponsored::Message::startFadeIn() {\n\tif (!_shown) {\n\t\treturn;\n\t}\n\tstartFade([=] {\n\t\t_session->sponsoredMessages().view(_data.randomId);\n\t});\n\tshow();\n}\n\nvoid PlaybackSponsored::Message::fadeOut(Fn<void()> hidden) {\n\tif (!_shown) {\n\t\tif (const auto onstack = hidden) {\n\t\t\tonstack();\n\t\t}\n\t\treturn;\n\t}\n\t_shown = false;\n\tstartFade(std::move(hidden));\n}\n\nvoid PlaybackSponsored::Message::startFade(Fn<void()> finished) {\n\t_cache = Ui::GrabWidgetToImage(this);\n\t_about->hide();\n\t_close->hide();\n\tconst auto from = _shown ? 0. : 1.;\n\tconst auto till = _shown ? 1. : 0.;\n\t_showAnimation.start([=] {\n\t\tupdateShown(finished);\n\t}, from, till, st::fadeWrapDuration);\n}\n\nvoid PlaybackSponsored::Message::updateShown(Fn<void()> finished) {\n\tconst auto shown = _showAnimation.value(_shown ? 1. : 0.);\n\tconst auto shift = anim::interpolate(st::mediaSponsoredShift, 0, shown);\n\tmove(_finalPosition.x(), _finalPosition.y() + shift);\n\tupdate();\n\tif (!_showAnimation.animating()) {\n\t\t_cache = QImage();\n\t\t_close->show();\n\t\t_about->show();\n\t\tif (const auto onstack = finished) {\n\t\t\tonstack();\n\t\t}\n\t}\n}\n\nvoid PlaybackSponsored::Message::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto shown = _showAnimation.value(_shown ? 1. : 0.);\n\tif (!_cache.isNull()) {\n\t\tp.setOpacity(shown);\n\t\tp.drawImage(0, 0, _cache);\n\t\treturn;\n\t}\n\n\tUi::FillRoundRect(\n\t\tp,\n\t\trect(),\n\t\tst::mediaviewSaveMsgBg,\n\t\tUi::MediaviewSaveCorners);\n\n\tconst auto &padding = st::mediaSponsoredPadding;\n\tif (_photo) {\n\t\tif (const auto image = _photo->image(Data::PhotoSize::Large)) {\n\t\t\tconst auto size = st::mediaSponsoredThumb;\n\t\t\tconst auto x = padding.left();\n\t\t\tconst auto y = (height() - size) / 2;\n\t\t\tp.drawPixmap(\n\t\t\t\tx,\n\t\t\t\ty,\n\t\t\t\timage->pixSingle(\n\t\t\t\t\tsize,\n\t\t\t\t\tsize,\n\t\t\t\t\t{ .options = Images::Option::RoundCircle }));\n\t\t}\n\t}\n\n\tp.setPen(st::mediaviewControlFg);\n\n\t_title.draw(p, {\n\t\t.position = { _left, _top },\n\t\t.availableWidth = _about->x() - _left,\n\t\t.palette = &st::mediaviewTextPalette,\n\t});\n\n\t_text.draw(p, {\n\t\t.position = { _left, _top + _titleHeight },\n\t\t.availableWidth = _close->x() - _left,\n\t\t.palette = &st::mediaviewTextPalette,\n\t});\n}\n\nvoid PlaybackSponsored::Message::mouseMoveEvent(QMouseEvent *e) {\n\tconst auto &padding = st::mediaSponsoredPadding;\n\tconst auto point = e->pos();\n\tconst auto about = _about->geometry();\n\tconst auto close = _close->geometry();\n\tconst auto over = !about.marginsAdded(padding).contains(point)\n\t\t&& !close.marginsAdded(padding).contains(point);\n\tif (_over != over) {\n\t\t_over = over;\n\t\tsetCursor(_over ? style::cur_pointer : style::cur_default);\n\t}\n}\n\nvoid PlaybackSponsored::Message::mousePressEvent(QMouseEvent *e) {\n\tif (_over) {\n\t\t_pressed = true;\n\t}\n}\n\nvoid PlaybackSponsored::Message::mouseReleaseEvent(QMouseEvent *e) {\n\tif (base::take(_pressed) && _over) {\n\t\t_session->sponsoredMessages().clicked(_data.randomId, false, false);\n\t\tUrlClickHandler::Open(_data.link);\n\t}\n}\n\nint PlaybackSponsored::Message::resizeGetHeight(int newWidth) {\n\tconst auto &padding = st::mediaSponsoredPadding;\n\tconst auto userpic = st::mediaSponsoredThumb;\n\t_left = padding.left() + (_photo ? (userpic + padding.left()) : 0);\n\tconst auto innerWidth = newWidth - _left - _close->width();\n\tconst auto titleWidth = innerWidth - _about->width() - padding.right();\n\t_titleHeight = _title.countHeight(titleWidth);\n\t_textHeight = _text.countHeight(innerWidth);\n\n\tconst auto use = std::max(_titleHeight + _textHeight, userpic);\n\n\tconst auto height = padding.top() + use + padding.bottom();\n\t_top = padding.top() + (use - _titleHeight - _textHeight) / 2;\n\n\t_about->move(\n\t\t_left + std::min(titleWidth, _title.maxWidth()) + padding.right(),\n\t\t_top);\n\t_close->move(\n\t\tnewWidth - _close->width(),\n\t\t(height - _close->height()) / 2);\n\n\treturn height;\n}\n\nvoid PlaybackSponsored::Message::populate() {\n\tconst auto &from = _data.from;\n\tconst auto photo = from.photoId\n\t\t? _data.history->owner().photo(from.photoId).get()\n\t\t: nullptr;\n\tif (photo) {\n\t\t_photo = photo->createMediaView();\n\t\tphoto->load({}, LoadFromCloudOrLocal, true);\n\t}\n\t_title = Ui::Text::String(\n\t\tst::semiboldTextStyle,\n\t\tfrom.title,\n\t\tkDefaultTextOptions,\n\t\tst::msgMinWidth);\n\t_text = Ui::Text::String(\n\t\tst::defaultTextStyle,\n\t\t_data.textWithEntities,\n\t\tkMarkupTextOptions,\n\t\tst::msgMinWidth);\n\n\t_about->setClickedCallback([=] {\n\t\t_menu = nullptr;\n\t\tconst auto parent = parentWidget();\n\t\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tparent,\n\t\t\tst::mediaviewPopupMenu);\n\t\tconst auto raw = _menu.get();\n\t\tconst auto addAction = Ui::Menu::CreateAddActionCallback(raw);\n\t\tMenu::FillSponsored(\n\t\t\taddAction,\n\t\t\t_show,\n\t\t\tMenu::SponsoredPhrases::Channel,\n\t\t\t_session->sponsoredMessages().lookupDetails(_data),\n\t\t\t_session->sponsoredMessages().createReportCallback(\n\t\t\t\t_data.randomId,\n\t\t\t\tcrl::guard(this, [=] { _actions.fire(Action::Close); })),\n\t\t\t{ .dark = true });\n\t\t_actions.fire(Action::Pause);\n\t\tUi::Connect(raw, &QObject::destroyed, this, [=] {\n\t\t\t_actions.fire(Action::Unpause);\n\t\t});\n\t\traw->popup(QCursor::pos());\n\t});\n}\n\nPlaybackSponsored::PlaybackSponsored(\n\tnot_null<Ui::RpWidget*> controls,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<HistoryItem*> item)\n: _parent(controls->parentWidget())\n, _session(&item->history()->session())\n, _show(std::move(show))\n, _itemId(item->fullId())\n, _controlsGeometry(controls->geometryValue())\n, _timer([=] { update(); }) {\n\t_session->sponsoredMessages().requestForVideo(item, crl::guard(this, [=](\n\t\t\tData::SponsoredForVideo data) {\n\t\tif (data.list.empty()) {\n\t\t\treturn;\n\t\t}\n\t\t_data = std::move(data);\n\t\tif (_data->state.initial()\n\t\t\t|| (_data->state.itemIndex > _data->list.size())\n\t\t\t|| (_data->state.itemIndex == _data->list.size()\n\t\t\t\t&& _data->state.leftTillShow <= 0)) {\n\t\t\t_data->state.itemIndex = 0;\n\t\t\t_data->state.leftTillShow = std::max(\n\t\t\t\t_data->startDelay,\n\t\t\t\tkStartDelayMin);\n\t\t}\n\t\tupdate();\n\t}));\n}\n\nPlaybackSponsored::~PlaybackSponsored() {\n\tsaveState();\n}\n\nvoid PlaybackSponsored::start() {\n\t_started = true;\n\tif (!_paused) {\n\t\t_start = crl::now();\n\t\tupdate();\n\t}\n}\n\nvoid PlaybackSponsored::setPaused(bool paused) {\n\tsetPausedOutside(paused);\n}\n\nvoid PlaybackSponsored::updatePaused() {\n\tconst auto paused = _pausedInside || _pausedOutside;\n\tif (_paused == paused) {\n\t\treturn;\n\t} else if (_started && paused) {\n\t\tupdate();\n\t}\n\t_paused = paused;\n\tif (!_started) {\n\t\treturn;\n\t} else if (_paused) {\n\t\t_start = 0;\n\t\t_timer.cancel();\n\t\t_allowCloseAt = 0;\n\t} else {\n\t\t_start = crl::now();\n\t\tupdate();\n\t}\n}\n\nvoid PlaybackSponsored::setPausedInside(bool paused) {\n\tif (_pausedInside == paused) {\n\t\treturn;\n\t}\n\t_pausedInside = paused;\n\tupdatePaused();\n}\n\nvoid PlaybackSponsored::setPausedOutside(bool paused) {\n\tif (_pausedOutside == paused) {\n\t\treturn;\n\t}\n\t_pausedOutside = paused;\n\tupdatePaused();\n}\n\nvoid PlaybackSponsored::finish() {\n\t_timer.cancel();\n\tif (_data) {\n\t\tsaveState();\n\t\t_data = std::nullopt;\n\t}\n}\n\nvoid PlaybackSponsored::update() {\n\tif (!_data || !_start) {\n\t\treturn;\n\t}\n\n\tconst auto [now, state] = computeState();\n\tconst auto message = (_data->state.itemIndex < _data->list.size())\n\t\t? &_data->list[state.itemIndex]\n\t\t: nullptr;\n\tconst auto duration = message\n\t\t? std::max(\n\t\t\tmessage->durationMin + kDurationMin,\n\t\t\tmessage->durationMax)\n\t\t: crl::time(0);\n\tif (_data->state.leftTillShow > 0 && state.leftTillShow <= 0) {\n\t\t_data->state.leftTillShow = 0;\n\t\tif (duration) {\n\t\t\t_allowCloseAt = now + message->durationMin;\n\t\t\tshow(*message);\n\n\t\t\t_start = now;\n\t\t\t_timer.callOnce(duration);\n\t\t\tsaveState();\n\t\t} else {\n\t\t\tfinish();\n\t\t}\n\t} else if (_data->state.leftTillShow <= 0\n\t\t&& state.leftTillShow <= -duration) {\n\t\thide(now);\n\t} else {\n\t\tif (state.leftTillShow <= 0 && duration) {\n\t\t\t_allowCloseAt = now + state.leftTillShow + message->durationMin;\n\t\t\tif (!_widget) {\n\t\t\t\tshow(*message);\n\t\t\t}\n\t\t}\n\t\t_data->state = state;\n\t\t_timer.callOnce((state.leftTillShow > 0)\n\t\t\t? state.leftTillShow\n\t\t\t: (state.leftTillShow + duration));\n\t}\n}\n\n\nvoid PlaybackSponsored::show(const Data::SponsoredMessage &data) {\n\t_widget = std::make_unique<Message>(\n\t\t_parent,\n\t\t_show,\n\t\tdata,\n\t\t_allowCloseAt.value());\n\tconst auto raw = _widget.get();\n\n\t_controlsGeometry.value() | rpl::on_next([=](QRect controls) {\n\t\traw->resizeToWidth(controls.width());\n\t\traw->setFinalPosition(\n\t\t\tcontrols.x(),\n\t\t\tcontrols.y() - st::mediaSponsoredSkip - raw->height());\n\t}, raw->lifetime());\n\n\traw->actions() | rpl::on_next([=](Action action) {\n\t\tswitch (action) {\n\t\tcase Action::Close: hide(crl::now()); break;\n\t\tcase Action::PromotePremium: showPremiumPromo(); break;\n\t\tcase Action::Pause: setPausedInside(true); break;\n\t\tcase Action::Unpause: setPausedInside(false); break;\n\t\t}\n\t}, raw->lifetime());\n\n\traw->fadeIn();\n}\n\nvoid PlaybackSponsored::showPremiumPromo() {\n\tShowPremiumPreviewBox(_show, PremiumFeature::NoAds);\n}\n\nvoid PlaybackSponsored::hide(crl::time now) {\n\tExpects(_widget != nullptr);\n\n\t_widget->fadeOut([this, raw = _widget.get()] {\n\t\tif (_widget.get() == raw) {\n\t\t\t_widget = nullptr;\n\t\t}\n\t});\n\n\t++_data->state.itemIndex;\n\t_data->state.leftTillShow = std::max(\n\t\t_data->betweenDelay,\n\t\tkStartDelayMin);\n\t_start = now;\n\t_timer.callOnce(_data->state.leftTillShow);\n\tsaveState();\n}\n\nvoid PlaybackSponsored::saveState() {\n\t_session->sponsoredMessages().updateForVideo(\n\t\t_itemId,\n\t\tcomputeState().data);\n}\n\nPlaybackSponsored::State PlaybackSponsored::computeState() const {\n\tauto result = State{ crl::now() };\n\tif (!_data) {\n\t\treturn result;\n\t}\n\tresult.data = _data->state;\n\tif (!_start) {\n\t\treturn result;\n\t}\n\tconst auto elapsed = result.now - _start;\n\tresult.data.leftTillShow -= elapsed;\n\treturn result;\n}\n\nrpl::lifetime &PlaybackSponsored::lifetime() {\n\treturn _lifetime;\n}\n\nbool PlaybackSponsored::Has(HistoryItem *item) {\n\treturn item\n\t\t&& item->history()->session().sponsoredMessages().canHaveFor(item);\n}\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_playback_sponsored.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n#include \"data/components/sponsored_messages.h\"\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Media::View {\n\nclass PlaybackSponsored final : public base::has_weak_ptr {\npublic:\n\tPlaybackSponsored(\n\t\tnot_null<Ui::RpWidget*> controls,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<HistoryItem*> item);\n\t~PlaybackSponsored();\n\n\tvoid start();\n\tvoid setPaused(bool paused);\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\n\t[[nodiscard]] static bool Has(HistoryItem *item);\n\nprivate:\n\tclass Message;\n\tstruct State {\n\t\tcrl::time now = 0;\n\t\tData::SponsoredForVideoState data;\n\t};\n\n\tvoid update();\n\tvoid finish();\n\tvoid updatePaused();\n\tvoid showPremiumPromo();\n\tvoid setPausedInside(bool paused);\n\tvoid setPausedOutside(bool paused);\n\tvoid show(const Data::SponsoredMessage &data);\n\tvoid hide(crl::time now);\n\t[[nodiscard]] State computeState() const;\n\tvoid saveState();\n\n\tconst not_null<QWidget*> _parent;\n\tconst not_null<Main::Session*> _session;\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst FullMsgId _itemId;\n\n\trpl::variable<QRect> _controlsGeometry;\n\tstd::unique_ptr<Message> _widget;\n\n\trpl::variable<crl::time> _allowCloseAt;\n\tcrl::time _start = 0;\n\tbool _started = false;\n\tbool _paused = false;\n\tbool _pausedInside = false;\n\tbool _pausedOutside = false;\n\tbase::Timer _timer;\n\n\tstd::optional<Data::SponsoredForVideo> _data;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_video_stream.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"media/view/media_view_video_stream.h\"\n\n#include \"data/data_message_reactions.h\"\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/group/calls_group_members.h\"\n#include \"calls/group/calls_group_messages.h\"\n#include \"calls/group/calls_group_messages_ui.h\"\n#include \"calls/group/calls_group_viewport.h\"\n#include \"calls/calls_instance.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"payments/ui/payments_reaction_box.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Media::View {\n\nclass VideoStream::Delegate final : public Calls::GroupCall::Delegate {\npublic:\n\texplicit Delegate(Fn<void()> close);\n\n\tvoid groupCallFinished(not_null<Calls::GroupCall*> call) override;\n\tvoid groupCallFailed(not_null<Calls::GroupCall*> call) override;\n\tvoid groupCallRequestPermissionsOrFail(Fn<void()> onSuccess) override;\n\tvoid groupCallPlaySound(GroupCallSound sound) override;\n\tauto groupCallGetVideoCapture(const QString &deviceId)\n\t\t-> std::shared_ptr<tgcalls::VideoCaptureInterface> override;\n\tFnMut<void()> groupCallAddAsyncWaiter() override;\n\nprivate:\n\tFn<void()> _close;\n\n};\n\nclass VideoStream::Loading final {\npublic:\n\tLoading(not_null<QWidget*> parent, not_null<VideoStream*> stream);\n\n\tvoid lower();\n\tvoid setGeometry(int x, int y, int width, int height);\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tvoid setup(not_null<QWidget*> parent);\n\n\tconst not_null<VideoStream*> _stream;\n\tstd::unique_ptr<Ui::RpWidget> _bg;\n\tstd::unique_ptr<Ui::PathShiftGradient> _gradient;\n\n};\n\nauto TopVideoStreamDonors(not_null<Calls::GroupCall*> call)\n-> rpl::producer<std::vector<Data::MessageReactionsTopPaid>> {\n\tconst auto messages = call->messages();\n\treturn rpl::single(rpl::empty) | rpl::then(\n\t\tmessages->starsValueChanges() | rpl::to_empty\n\t) | rpl::map([=] {\n\t\tconst auto &list = messages->starsTop().topDonors;\n\t\tauto still = Ui::MaxTopPaidDonorsShown();\n\t\tauto result = std::vector<Data::MessageReactionsTopPaid>();\n\t\tresult.reserve(list.size());\n\t\tfor (const auto &item : list) {\n\t\t\tresult.push_back({\n\t\t\t\t.peer = item.peer,\n\t\t\t\t.count = uint32(item.stars),\n\t\t\t\t.my = item.my ? 1U : 0U,\n\t\t\t});\n\t\t\tif (!item.my && !--still) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t});\n}\n\nauto TopDonorPlaces(not_null<Calls::GroupCall*> call)\n-> rpl::producer<std::vector<not_null<PeerData*>>> {\n\treturn TopVideoStreamDonors(\n\t\tcall\n\t) | rpl::map([=](const std::vector<Data::MessageReactionsTopPaid> &lst) {\n\t\tauto result = std::vector<not_null<PeerData*>>();\n\t\tauto left = Ui::MaxTopPaidDonorsShown();\n\t\tresult.reserve(lst.size());\n\t\tfor (const auto &donor : lst) {\n\t\t\tresult.push_back(donor.peer);\n\t\t\tif (!--left) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t});\n}\n\nVideoStream::Delegate::Delegate(Fn<void()> close)\n: _close(std::move(close)) {\n}\n\nvoid VideoStream::Delegate::groupCallFinished(\n\t\tnot_null<Calls::GroupCall*> call) {\n\tcrl::on_main(call, _close);\n}\n\nvoid VideoStream::Delegate::groupCallFailed(not_null<Calls::GroupCall*> call) {\n\tcrl::on_main(call, _close);\n}\n\nvoid VideoStream::Delegate::groupCallRequestPermissionsOrFail(\n\t\tFn<void()> onSuccess) {\n}\n\nvoid VideoStream::Delegate::groupCallPlaySound(GroupCallSound sound) {\n}\n\nauto VideoStream::Delegate::groupCallGetVideoCapture(const QString &deviceId)\n-> std::shared_ptr<tgcalls::VideoCaptureInterface> {\n\treturn nullptr;\n}\n\nFnMut<void()> VideoStream::Delegate::groupCallAddAsyncWaiter() {\n\treturn [] {};\n}\n\nVideoStream::Loading::Loading(\n\tnot_null<QWidget*> parent,\n\tnot_null<VideoStream*> stream)\n: _stream(stream) {\n\tsetup(parent);\n}\n\nvoid VideoStream::Loading::lower() {\n\t_bg->lower();\n}\n\nvoid VideoStream::Loading::setGeometry(int x, int y, int width, int height) {\n\t_bg->setGeometry(x, y, width, height);\n}\n\nrpl::lifetime &VideoStream::Loading::lifetime() {\n\treturn _bg->lifetime();\n}\n\nvoid VideoStream::Loading::setup(not_null<QWidget*> parent) {\n\t_bg = std::make_unique<Ui::RpWidget>(parent);\n\n\t_gradient = std::make_unique<Ui::PathShiftGradient>(\n\t\tst::storiesComposeBg,\n\t\tst::storiesComposeBgRipple,\n\t\t[=] { _bg->update(); });\n\n\t_bg->show();\n\t_bg->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(_bg.get());\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\t_gradient->startFrame(0, _bg->width(), _bg->width() / 3);\n\t\t_gradient->paint([&](const Ui::PathShiftGradient::Background &bg) {\n\t\t\tconst auto stroke = style::ConvertScaleExact(2);\n\t\t\tif (const auto color = std::get_if<style::color>(&bg)) {\n\t\t\t\tauto pen = (*color)->p;\n\t\t\t\tpen.setWidthF(stroke);\n\t\t\t\tp.setPen(pen);\n\t\t\t\tp.setBrush(*color);\n\t\t\t} else {\n\t\t\t\tconst auto gradient = v::get<QLinearGradient*>(bg);\n\n\t\t\t\tauto copy = *gradient;\n\t\t\t\tcopy.setStops({\n\t\t\t\t\t{ 0., st::storiesComposeBg->c },\n\t\t\t\t\t{ 0.5, st::white->c },\n\t\t\t\t\t{ 1., st::storiesComposeBg->c },\n\t\t\t\t});\n\t\t\t\tauto pen = QPen(QBrush(copy), stroke);\n\t\t\t\tp.setPen(pen);\n\t\t\t\tp.setBrush(*gradient);\n\t\t\t}\n\t\t\tconst auto half = stroke / 2.;\n\t\t\tconst auto remove = QMarginsF(half, half, half, half);\n\t\t\tconst auto rect = QRectF(_bg->rect()).marginsRemoved(remove);\n\t\t\tconst auto radius = st::storiesRadius - half;\n\t\t\tp.drawRoundedRect(rect, radius, radius);\n\t\t\treturn true;\n\t\t});\n\t}, _bg->lifetime());\n}\n\nVideoStream::VideoStream(\n\tnot_null<QWidget*> parent,\n\tnot_null<Ui::RpWidgetWrap*> borrowedRp,\n\tbool borrowedOpenGL,\n\tUi::GL::Backend backend,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tstd::shared_ptr<Data::GroupCall> call,\n\tQString callLinkSlug,\n\tMsgId callJoinMessageId)\n: _show(std::move(show))\n, _delegate(std::make_unique<Delegate>([=] { _closeRequests.fire({}); }))\n, _loading(std::make_unique<Loading>(parent, this))\n, _call(std::make_unique<Calls::GroupCall>(\n\t_delegate.get(),\n\tCalls::StartConferenceInfo{\n\t\t.show = _show,\n\t\t.call = std::move(call),\n\t\t.linkSlug = callLinkSlug,\n\t\t.joinMessageId = callJoinMessageId,\n\t}))\n, _members(\n\tstd::make_unique<Calls::Group::Members>(\n\t\tparent,\n\t\t_call.get(),\n\t\tCalls::Group::PanelMode::VideoStream,\n\t\tbackend))\n, _viewport(\n\tstd::make_unique<Calls::Group::Viewport>(\n\t\tparent,\n\t\tCalls::Group::PanelMode::VideoStream,\n\t\tbackend,\n\t\tborrowedRp,\n\t\tborrowedOpenGL))\n, _messages(\n\tstd::make_unique<Calls::Group::MessagesUi>(\n\t\tparent,\n\t\t_show,\n\t\tCalls::Group::MessagesMode::VideoStream,\n\t\t_call->messages()->listValue(),\n\t\tTopDonorPlaces(_call.get()),\n\t\t_call->messages()->idUpdates(),\n\t\t_call->canManageValue(),\n\t\t_commentsShown.value())) {\n\tCore::App().calls().registerVideoStream(_call.get());\n\tsetupMembers();\n\tsetupVideo();\n\tsetupMessages();\n}\n\nVideoStream::~VideoStream() {\n}\n\nnot_null<Calls::GroupCall*> VideoStream::call() const {\n\treturn _call.get();\n}\n\nrpl::producer<> VideoStream::closeRequests() const {\n\treturn _closeRequests.events();\n}\n\nvoid VideoStream::updateGeometry(int x, int y, int width, int height) {\n\tconst auto skip = st::groupCallMessageSkip;\n\t_viewport->setGeometry(false, { x, y, width, height });\n\t_messages->move(x + skip, y + height, width - 2 * skip, height / 2);\n\tif (_loading) {\n\t\t_loading->setGeometry(x, y, width, height);\n\t}\n}\n\nvoid VideoStream::toggleCommentsOn(rpl::producer<bool> shown) {\n\t_commentsShown = std::move(shown);\n}\n\nvoid VideoStream::ensureBorrowedRenderer(QOpenGLFunctions &f) {\n\t_viewport->ensureBorrowedRenderer(f);\n}\n\nvoid VideoStream::borrowedPaint(QOpenGLFunctions &f) {\n\t_viewport->borrowedPaint(f);\n}\n\nvoid VideoStream::ensureBorrowedRenderer() {\n\t_viewport->ensureBorrowedRenderer();\n}\n\nvoid VideoStream::borrowedPaint(Painter &p, const QRegion &clip) {\n\t_viewport->borrowedPaint(p, clip);\n}\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\nvoid VideoStream::borrowedPaintOffscreen(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\t_viewport->borrowedPaintOffscreen(rhi, rt, cb);\n}\n\nvoid VideoStream::borrowedPaintOnscreen(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\t_viewport->borrowedPaintOnscreen(rhi, rt, cb);\n}\n#endif\n\nrpl::lifetime &VideoStream::lifetime() {\n\treturn _lifetime;\n}\n\nvoid VideoStream::setupMembers() {\n}\n\nvoid VideoStream::setupVideo() {\n\tconst auto setupTile = [=](\n\t\t\tconst Calls::VideoEndpoint &endpoint,\n\t\t\tconst std::unique_ptr<Calls::GroupCall::VideoTrack> &track) {\n\t\tusing namespace rpl::mappers;\n\t\tconst auto row = endpoint.rtmp()\n\t\t\t? _members->rtmpFakeRow(Calls::GroupCall::TrackPeer(track)).get()\n\t\t\t: _members->lookupRow(Calls::GroupCall::TrackPeer(track));\n\t\tAssert(row != nullptr);\n\n\t\tauto pinned = rpl::combine(\n\t\t\t_call->videoEndpointLargeValue(),\n\t\t\t_call->videoEndpointPinnedValue()\n\t\t) | rpl::map(_1 == endpoint && _2);\n\t\tconst auto self = (endpoint.peer == _call->joinAs());\n\t\t_viewport->add(\n\t\t\tendpoint,\n\t\t\tCalls::Group::VideoTileTrack{\n\t\t\t\tCalls::GroupCall::TrackPointer(track),\n\t\t\t\trow,\n\t\t\t},\n\t\t\tCalls::GroupCall::TrackSizeValue(track),\n\t\t\tstd::move(pinned),\n\t\t\tself);\n\t};\n\tfor (const auto &[endpoint, track] : _call->activeVideoTracks()) {\n\t\tsetupTile(endpoint, track);\n\t}\n\t_call->videoStreamActiveUpdates(\n\t) | rpl::on_next([=](const Calls::VideoStateToggle &update) {\n\t\tif (update.value) {\n\t\t\t// Add async (=> the participant row is definitely in Members).\n\t\t\tconst auto endpoint = update.endpoint;\n\t\t\tcrl::on_main(_viewport->widget(), [=] {\n\t\t\t\tconst auto &tracks = _call->activeVideoTracks();\n\t\t\t\tconst auto i = tracks.find(endpoint);\n\t\t\t\tif (i != end(tracks)) {\n\t\t\t\t\tsetupTile(endpoint, i->second);\n\t\t\t\t}\n\t\t\t});\n\t\t} else {\n\t\t\t// Remove sync.\n\t\t\t_viewport->remove(update.endpoint);\n\t\t}\n\t}, _viewport->lifetime());\n\n\t_viewport->qualityRequests(\n\t) | rpl::on_next([=](const Calls::VideoQualityRequest &request) {\n\t\t_call->requestVideoQuality(request.endpoint, request.quality);\n\t}, _viewport->lifetime());\n\n\t_loading->lower();\n\t_viewport->widget()->lower();\n\t_viewport->setControlsShown(0.);\n\n\t_call->hasVideoWithFramesValue(\n\t) | rpl::on_next([=](bool has) {\n\t\tif (has) {\n\t\t\t_loading = nullptr;\n\t\t}\n\t}, _loading->lifetime());\n\n\tsetVolume(Core::App().settings().videoVolume());\n}\n\nvoid VideoStream::setupMessages() {\n\t_messages->hiddenShowRequested() | rpl::on_next([=] {\n\t\t_call->messages()->requestHiddenShow();\n\t}, _messages->lifetime());\n\n\n\t_messages->deleteRequests(\n\t) | rpl::on_next([=](Calls::Group::MessageDeleteRequest event) {\n\t\t_call->messages()->deleteConfirmed(event);\n\t}, _messages->lifetime());\n}\n\nvoid VideoStream::setVolume(float64 volume) {\n\tconst auto value = volume * Calls::Group::kDefaultVolume;\n\t_call->changeVolume({\n\t\t.peer = _call->peer(),\n\t\t.volume = int(base::SafeRound(value)),\n\t});\n}\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/media/view/media_view_video_stream.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass Painter;\nclass QOpenGLFunctions;\nclass QRhi;\nclass QRhiRenderTarget;\nclass QRhiCommandBuffer;\n\nnamespace Calls {\nclass GroupCall;\n} // namespace Calls\n\nnamespace Calls::Group {\nclass Members;\nclass Viewport;\nclass MessagesUi;\n} // namespace Calls::Group\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nclass GroupCall;\nstruct MessageReactionsTopPaid;\n} // namespace Data\n\nnamespace Ui {\nclass RpWidgetWrap;\n} // namespace Ui\n\nnamespace Ui::GL {\nenum class Backend;\n} // namespace Ui::GL\n\nnamespace Media::View {\n\n[[nodiscard]] auto TopVideoStreamDonors(not_null<Calls::GroupCall*> call)\n-> rpl::producer<std::vector<Data::MessageReactionsTopPaid>>;\n\nclass VideoStream final {\npublic:\n\tVideoStream(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Ui::RpWidgetWrap*> borrowedRp,\n\t\tbool borrowedOpenGL,\n\t\tUi::GL::Backend backend,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tstd::shared_ptr<Data::GroupCall> call,\n\t\tQString callLinkSlug,\n\t\tMsgId callJoinMessageId);\n\t~VideoStream();\n\n\t[[nodiscard]] not_null<Calls::GroupCall*> call() const;\n\t[[nodiscard]] rpl::producer<> closeRequests() const;\n\n\tvoid setVolume(float64 volume);\n\tvoid updateGeometry(int x, int y, int width, int height);\n\tvoid toggleCommentsOn(rpl::producer<bool> shown);\n\n\tvoid ensureBorrowedRenderer(QOpenGLFunctions &f);\n\tvoid borrowedPaint(QOpenGLFunctions &f);\n\n\tvoid ensureBorrowedRenderer();\n\tvoid borrowedPaint(Painter &p, const QRegion &clip);\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\tvoid borrowedPaintOffscreen(QRhi *rhi, QRhiRenderTarget *rt, QRhiCommandBuffer *cb);\n\tvoid borrowedPaintOnscreen(QRhi *rhi, QRhiRenderTarget *rt, QRhiCommandBuffer *cb);\n#endif\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tclass Delegate;\n\tclass Loading;\n\n\tvoid setupVideo();\n\tvoid setupMembers();\n\tvoid setupMessages();\n\n\trpl::variable<bool> _commentsShown;\n\n\tstd::shared_ptr<ChatHelpers::Show> _show;\n\tstd::unique_ptr<Delegate> _delegate;\n\tstd::unique_ptr<Loading> _loading;\n\tstd::unique_ptr<Calls::GroupCall> _call;\n\tstd::unique_ptr<Calls::Group::Members> _members;\n\tstd::unique_ptr<Calls::Group::Viewport> _viewport;\n\tstd::unique_ptr<Calls::Group::MessagesUi> _messages;\n\n\trpl::event_stream<> _closeRequests;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Media::View\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/gift_resale_filter.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"menu/gift_resale_filter.h\"\n\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_credits.h\" // giftBoxResaleColorSize\n#include \"styles/style_media_player.h\" // mediaPlayerMenuCheck\n\nnamespace Ui {\nnamespace {\n\n[[nodiscard]] Text::MarkedContext WithRepaint(\n\t\tconst Text::MarkedContext &context,\n\t\tFn<void()> repaint) {\n\tauto result = context;\n\tresult.repaint = std::move(repaint);\n\treturn result;\n}\n\n[[nodiscard]] QString SerializeColorData(const QColor &color) {\n\treturn u\"color:%1,%2,%3,%4\"_q\n\t\t.arg(color.red())\n\t\t.arg(color.green())\n\t\t.arg(color.blue())\n\t\t.arg(color.alpha());\n}\n\n[[nodiscard]] bool IsColorData(QStringView data) {\n\treturn data.startsWith(u\"color:\"_q);\n}\n\n[[nodiscard]] QColor ParseColorData(QStringView data) {\n\tExpects(data.size() > 12);\n\n\tconst auto parts = data.mid(6).split(',');\n\tAssert(parts.size() == 4);\n\treturn QColor(\n\t\tparts[0].toInt(),\n\t\tparts[1].toInt(),\n\t\tparts[2].toInt(),\n\t\tparts[3].toInt());\n}\n\n} // namespace\n\nGiftResaleFilterAction::GiftResaleFilterAction(\n\tnot_null<Menu::Menu*> parent,\n\tconst style::Menu &st,\n\tconst TextWithEntities &text,\n\tconst Text::MarkedContext &context,\n\tQString iconEmojiData,\n\tconst style::icon *icon)\n: Action(parent, st, new QAction(parent), icon, icon)\n, _iconEmoji(iconEmojiData.isEmpty()\n\t? nullptr\n\t: context.customEmojiFactory(\n\t\ticonEmojiData,\n\t\tWithRepaint(context, [=] { update(); }))) {\n\tsetMarkedText(text, QString(), context);\n}\n\nvoid GiftResaleFilterAction::paintEvent(QPaintEvent *e) {\n\tAction::paintEvent(e);\n\n\tPainter p(this);\n\n\tconst auto enabled = isEnabled();\n\tconst auto selected = isSelected();\n\tconst auto fg = selected\n\t\t? st().itemFgOver\n\t\t: enabled\n\t\t? st().itemFg\n\t\t: st().itemFgDisabled;\n\tif (const auto emoji = _iconEmoji.get()) {\n\t\tconst auto x = st().itemIconPosition.x();\n\t\tconst auto y = (height() - st::emojiSize) / 2;\n\t\temoji->paint(p, {\n\t\t\t.textColor = fg->c,\n\t\t\t.position = { x, y },\n\t\t});\n\t}\n\tif (_checked) {\n\t\tconst auto &icon = st::mediaPlayerMenuCheck;\n\t\tconst auto skip = st().itemRightSkip;\n\t\tconst auto left = width() - skip - icon.width();\n\t\tconst auto top = (height() - icon.height()) / 2;\n\t\ticon.paint(p, left, top, width());\n\t}\n}\n\nvoid GiftResaleFilterAction::setChecked(bool checked) {\n\tif (_checked != checked) {\n\t\t_checked = checked;\n\t\tupdate();\n\t}\n}\n\nGiftResaleColorEmoji::GiftResaleColorEmoji(QStringView data)\n: _color(ParseColorData(data)) {\n}\n\nbool GiftResaleColorEmoji::Owns(QStringView data) {\n\treturn IsColorData(data);\n}\n\nQString GiftResaleColorEmoji::DataFor(QColor color) {\n\treturn SerializeColorData(color);\n}\n\nint GiftResaleColorEmoji::width() {\n\treturn st::giftBoxResaleColorSize;\n}\n\nQString GiftResaleColorEmoji::entityData() {\n\treturn DataFor(_color);\n}\n\nvoid GiftResaleColorEmoji::paint(QPainter &p, const Context &context) {\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setBrush(_color);\n\tp.setPen(Qt::NoPen);\n\tp.drawEllipse(\n\t\tcontext.position.x(),\n\t\tcontext.position.y() + st::giftBoxResaleColorTop,\n\t\twidth(),\n\t\twidth());\n}\n\nvoid GiftResaleColorEmoji::unload() {\n}\n\nbool GiftResaleColorEmoji::ready() {\n\treturn true;\n}\n\nbool GiftResaleColorEmoji::readyInDefaultState() {\n\treturn true;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/gift_resale_filter.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/text/text_custom_emoji.h\"\n\nnamespace Ui {\n\nclass GiftResaleFilterAction final : public Menu::Action {\npublic:\n\tGiftResaleFilterAction(\n\t\tnot_null<Menu::Menu*> parent,\n\t\tconst style::Menu &st,\n\t\tconst TextWithEntities &text,\n\t\tconst Text::MarkedContext &context,\n\t\tQString iconEmojiData,\n\t\tconst style::icon *icon);\n\n\tvoid setChecked(bool checked);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tconst std::unique_ptr<Text::CustomEmoji> _iconEmoji;\n\tbool _checked = false;\n\n};\n\nclass GiftResaleColorEmoji final : public Text::CustomEmoji {\npublic:\n\tGiftResaleColorEmoji(QStringView data);\n\n\t[[nodiscard]] static bool Owns(QStringView data);\n\t[[nodiscard]] static QString DataFor(QColor color);\n\n\tint width() override;\n\tQString entityData() override;\n\n\tvoid paint(QPainter &p, const Context &context) override;\n\tvoid unload() override;\n\tbool ready() override;\n\tbool readyInDefaultState() override;\n\nprivate:\n\tQColor _color;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_antispam_validator.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"menu/menu_antispam_validator.h\"\n\n#include \"apiwrap.h\"\n#include \"boxes/peers/edit_participants_box.h\"\n#include \"boxes/peers/edit_peer_info_box.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\" // IconDescriptor.\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\nnamespace AntiSpamMenu {\nnamespace {\n\n[[nodiscard]] int EnableAntiSpamMinMembers(not_null<ChannelData*> channel) {\n\treturn channel->session().appConfig().get<int>(\n\t\tu\"telegram_antispam_group_size_min\"_q,\n\t\t100);\n}\n\n[[nodiscard]] UserId AntiSpamUserId(not_null<ChannelData*> channel) {\n\tconst auto id = channel->session().appConfig().get<QString>(\n\t\tu\"telegram_antispam_user_id\"_q,\n\t\tQString());\n\treturn UserId(id.toULongLong());\n}\n\n} // namespace\n\nAntiSpamValidator::AntiSpamValidator(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<ChannelData*> channel)\n: _channel(channel)\n, _controller(controller) {\n}\n\nobject_ptr<Ui::RpWidget> AntiSpamValidator::createButton() const {\n\tconst auto channel = _channel;\n\tauto container = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t(QWidget*)nullptr,\n\t\tobject_ptr<Ui::VerticalLayout>((QWidget*)nullptr));\n\tstruct State {\n\t\trpl::variable<bool> locked;\n\t\trpl::event_stream<bool> toggled;\n\t};\n\tUi::AddSkip(container->entity());\n\tconst auto state = container->lifetime().make_state<State>();\n\tconst auto button = container->entity()->add(\n\t\tEditPeerInfoBox::CreateButton(\n\t\t\tcontainer->entity(),\n\t\t\ttr::lng_manage_peer_antispam(),\n\t\t\trpl::single(QString()),\n\t\t\t[] {},\n\t\t\tst::manageGroupTopicsButton,\n\t\t\t{ &st::menuIconAntispam }\n\t))->toggleOn(rpl::single(\n\t\t_channel->antiSpamMode()\n\t) | rpl::then(state->toggled.events()));\n\tcontainer->show(anim::type::instant);\n\tUi::AddSkip(container->entity());\n\tUi::AddDividerText(\n\t\tcontainer->entity(),\n\t\ttr::lng_manage_peer_antispam_about());\n\n\tconst auto updateLocked = [=] {\n\t\tconst auto min = EnableAntiSpamMinMembers(channel);\n\t\tconst auto locked = (channel->membersCount() < min);\n\t\tstate->locked = locked;\n\t\tbutton->setToggleLocked(locked);\n\t};\n\tusing UpdateFlag = Data::PeerUpdate::Flag;\n\t_channel->session().changes().peerUpdates(\n\t\t_channel,\n\t\tUpdateFlag::Members | UpdateFlag::Admins\n\t) | rpl::on_next(updateLocked, button->lifetime());\n\tupdateLocked();\n\tbutton->toggledValue(\n\t) | rpl::on_next([=, controller = _controller](bool toggled) {\n\t\tif (state->locked.current() && toggled) {\n\t\t\tstate->toggled.fire(false);\n\t\t\tcontroller->showToast(tr::lng_manage_peer_antispam_not_enough(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tEnableAntiSpamMinMembers(channel),\n\t\t\t\ttr::rich));\n\t\t} else {\n\t\t\tchannel->session().api().request(MTPchannels_ToggleAntiSpam(\n\t\t\t\tchannel->inputChannel(),\n\t\t\t\tMTP_bool(toggled)\n\t\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t\tchannel->session().api().applyUpdates(result);\n\t\t\t}).send();\n\t\t}\n\t}, button->lifetime());\n\n\treturn container;\n}\n\nvoid AntiSpamValidator::resolveUser(Fn<void()> finish) const {\n\tif (_channel->antiSpamMode()) {\n\t\tconst auto mtpUserId = peerToBareMTPInt(AntiSpamUserId(_channel));\n\t\t_channel->session().api().request(MTPusers_GetUsers(\n\t\t\tMTP_vector<MTPInputUser>(1, MTP_inputUser(mtpUserId, MTPlong()))\n\t\t)).done([=, channel = _channel](const MTPVector<MTPUser> &result) {\n\t\t\tchannel->owner().processUsers(result);\n\t\t\tfinish();\n\t\t}).fail([=] {\n\t\t\tfinish();\n\t\t}).send();\n\t} else {\n\t\tfinish();\n\t}\n}\n\nUserData *AntiSpamValidator::maybeAppendUser() const {\n\tif (_channel->antiSpamMode()) {\n\t\tconst auto userId = AntiSpamUserId(_channel);\n\t\tif (const auto user = _channel->owner().user(userId)) {\n\t\t\treturn user;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nUserId AntiSpamValidator::userId() const {\n\treturn AntiSpamUserId(_channel);\n}\n\nvoid AntiSpamValidator::addAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tFullMsgId fakeId) const {\n\tif (!fakeId) {\n\t\treturn;\n\t}\n\tconst auto suggestReport = [&](MsgId eventId) {\n\t\tconst auto text = tr::lng_admin_log_antispam_menu_report_toast(\n\t\t\ttr::now,\n\t\t\tlt_link,\n\t\t\ttr::link(\n\t\t\t\ttr::lng_admin_log_antispam_menu_report_toast_link(\n\t\t\t\t\ttr::now),\n\t\t\t\t\"internal:show\"),\n\t\t\ttr::rich);\n\t\tconst auto showToast = [=,\n\t\t\t\twindow = _controller,\n\t\t\t\tchannel = _channel] {\n\t\t\twindow->showToast({\n\t\t\t\t.text = text,\n\t\t\t\t.filter = [=](\n\t\t\t\t\t\tconst ClickHandlerPtr&,\n\t\t\t\t\t\tQt::MouseButton) {\n\t\t\t\t\tParticipantsBoxController::Start(\n\t\t\t\t\t\twindow,\n\t\t\t\t\t\tchannel,\n\t\t\t\t\t\tParticipantsRole::Admins);\n\t\t\t\t\treturn true;\n\t\t\t\t},\n\t\t\t\t.duration = ApiWrap::kJoinErrorDuration,\n\t\t\t});\n\t\t};\n\t\tmenu->addAction(\n\t\t\ttr::lng_admin_log_antispam_menu_report(tr::now),\n\t\t\t[=, channel = _channel] {\n\t\t\t\t_channel->session().api().request(\n\t\t\t\t\tMTPchannels_ReportAntiSpamFalsePositive(\n\t\t\t\t\t\tchannel->inputChannel(),\n\t\t\t\t\t\tMTP_int(eventId)\n\t\t\t\t)).done(showToast).send();\n\t\t\t},\n\t\t\t&st::menuIconReportAntiSpam);\n\t};\n\t{\n\t\tconst auto it = _itemEventMsgIds.find(fakeId);\n\t\tif (it != end(_itemEventMsgIds)) {\n\t\t\tsuggestReport(it->second);\n\t\t\tmenu->addSeparator();\n\t\t}\n\t}\n}\n\nvoid AntiSpamValidator::addEventMsgId(FullMsgId fakeId, MsgId realId) {\n\t_itemEventMsgIds.emplace(fakeId, realId);\n}\n\n} // namespace AntiSpamMenu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_antispam_validator.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\ntemplate <typename Object>\nclass object_ptr;\n\nclass ChannelData;\nclass UserData;\n\nnamespace Ui {\nclass PopupMenu;\nclass RpWidget;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace AntiSpamMenu {\n\nclass AntiSpamValidator final {\npublic:\n\tAntiSpamValidator(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<ChannelData*> channel);\n\n\t[[nodiscard]] object_ptr<Ui::RpWidget> createButton() const;\n\n\tvoid resolveUser(Fn<void()> finish) const;\n\t[[nodiscard]] UserData *maybeAppendUser() const;\n\t[[nodiscard]] UserId userId() const;\n\tvoid addAction(not_null<Ui::PopupMenu*> menu, FullMsgId fakeId) const;\n\tvoid addEventMsgId(FullMsgId fakeId, MsgId realId);\n\nprivate:\n\tconst not_null<ChannelData*> _channel;\n\tconst not_null<Window::SessionController*> _controller;\n\n\tbase::flat_map<FullMsgId, MsgId> _itemEventMsgIds;\n\n};\n\n} // namespace AntiSpamMenu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_check_item.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"menu/menu_check_item.h\"\n\n#include \"ui/widgets/checkbox.h\"\n#include \"styles/style_media_player.h\"\n\nnamespace Menu {\n\nvoid ItemWithCheck::init(bool checked) {\n\tenableMouseSelecting();\n\n\tItemBase::setPreventClose(true);\n\n\t_checkView = std::make_unique<Ui::CheckView>(st::defaultCheck, false);\n\t_checkView->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tsetIcon(checked ? &st::mediaPlayerMenuCheck : nullptr);\n\t}, lifetime());\n\n\t_checkView->setChecked(checked, anim::type::normal);\n\tItemBase::clicks() | rpl::on_next([=] {\n\t\t_checkView->setChecked(!_checkView->checked(), anim::type::normal);\n\t}, lifetime());\n}\n\nnot_null<Ui::CheckView*> ItemWithCheck::checkView() const {\n\treturn _checkView.get();\n}\n\n} // namespace Menu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_check_item.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/menu/menu_action.h\"\n\nnamespace Ui {\nclass CheckView;\n} // namespace Ui\n\nnamespace Menu {\n\nclass ItemWithCheck final : public Ui::Menu::Action {\npublic:\n\tusing Ui::Menu::Action::Action;\n\n\tvoid init(bool checked);\n\n\tnot_null<Ui::CheckView*> checkView() const;\n\nprivate:\n\tstd::unique_ptr<Ui::CheckView> _checkView;\n};\n\n} // namespace Menu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_checked_action.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"menu/menu_checked_action.h\"\n\n#include \"base/unique_qptr.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/menu/menu_common.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/painter.h\"\n\n#include \"styles/style_media_player.h\"\n\nnamespace {\n\nclass CheckedAction final : public Ui::Menu::Action {\npublic:\n\tCheckedAction(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::Menu &st,\n\t\tnot_null<QAction*> action,\n\t\tconst style::icon *icon,\n\t\tbool checked);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tconst bool _checked = false;\n\n};\n\nCheckedAction::CheckedAction(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::Menu &st,\n\tnot_null<QAction*> action,\n\tconst style::icon *icon,\n\tbool checked)\n: Ui::Menu::Action(parent, st, action, icon, icon)\n, _checked(checked) {\n\tsetMinWidth(minWidth() + st.itemRightSkip + st::mediaPlayerMenuCheck.width());\n}\n\nvoid CheckedAction::paintEvent(QPaintEvent *e) {\n\tUi::Menu::Action::paintEvent(e);\n\n\tif (!_checked) {\n\t\treturn;\n\t}\n\n\tPainter p(this);\n\tconst auto &icon = st::mediaPlayerMenuCheck;\n\tconst auto left = width() - st().itemRightSkip - icon.width();\n\tconst auto top = (height() - icon.height()) / 2;\n\ticon.paint(p, left, top, width());\n}\n\n} // namespace\n\nnamespace Menu {\n\nnot_null<QAction*> AddCheckedAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst QString &text,\n\t\tFn<void()> callback,\n\t\tconst style::icon *icon,\n\t\tbool checked) {\n\tauto item = base::make_unique_q<CheckedAction>(\n\t\tmenu->menu(),\n\t\tmenu->st().menu,\n\t\tUi::Menu::CreateAction(\n\t\t\tmenu->menu().get(),\n\t\t\ttext,\n\t\t\tstd::move(callback)),\n\t\ticon,\n\t\tchecked);\n\treturn menu->addAction(std::move(item));\n}\n\n} // namespace Menu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_checked_action.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/basic_types.h\"\n#include \"ui/style/style_core_types.h\"\n\nclass QAction;\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Menu {\n\nnot_null<QAction*> AddCheckedAction(\n\tnot_null<Ui::PopupMenu*> menu,\n\tconst QString &text,\n\tFn<void()> callback,\n\tconst style::icon *icon,\n\tbool checked);\n\n} // namespace Menu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_dock.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"menu/menu_dock.h\"\n\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"ui/text/text_utilities.h\"\n\n#include <QtWidgets/QMenu>\n\nnamespace Menu {\n\nvoid RefreshDockMenu(QMenu *menu) {\n\tmenu->clear();\n\tif (!Core::IsAppLaunched()) {\n\t\treturn;\n\t}\n\n\tconst auto accounts = Core::App().domain().orderedAccounts();\n\tif (accounts.size() > 1) {\n\t\tmenu->addSeparator();\n\t\tconst auto profilesHeader = menu->addAction(\n\t\t\ttr::lng_mac_menu_profiles(tr::now));\n\t\tprofilesHeader->setEnabled(false);\n\t\tconstexpr auto kMaxLength = 30;\n\t\tfor (const auto &account : accounts) {\n\t\t\tif (account->sessionExists()) {\n\t\t\t\tconst auto name = account->session().user()->name();\n\t\t\t\tmenu->addAction(\n\t\t\t\t\t(name.size() > kMaxLength)\n\t\t\t\t\t\t? (name.mid(0, kMaxLength) + Ui::kQEllipsis)\n\t\t\t\t\t\t: name,\n\t\t\t\t\t[account] {\n\t\t\t\t\t\tCore::App().ensureSeparateWindowFor(account);\n\t\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tmenu->addSeparator();\n\t}\n\n\tmenu->addAction(\n\t\tCore::App().settings().desktopNotify()\n\t\t\t? tr::lng_disable_notifications_from_tray(tr::now)\n\t\t\t: tr::lng_enable_notifications_from_tray(tr::now),\n\t\t[] {\n\t\t\tauto &settings = Core::App().settings();\n\t\t\tsettings.setDesktopNotify(!settings.desktopNotify());\n\t\t});\n\n\tusing namespace Media::Player;\n\tconst auto type = instance()->getActiveType();\n\tconst auto state = instance()->getState(type);\n\tif (!IsStoppedOrStopping(state.state)) {\n\t\tmenu->addSeparator();\n\t\tconst auto previous = menu->addAction(\n\t\t\ttr::lng_mac_menu_player_previous(tr::now),\n\t\t\t[] { instance()->previous(); });\n\t\tprevious->setEnabled(instance()->previousAvailable(type));\n\t\tmenu->addAction(\n\t\t\tIsPausedOrPausing(state.state)\n\t\t\t\t? tr::lng_mac_menu_player_resume(tr::now)\n\t\t\t\t: tr::lng_mac_menu_player_pause(tr::now),\n\t\t\t[] { instance()->playPause(); });\n\t\tconst auto next = menu->addAction(\n\t\t\ttr::lng_mac_menu_player_next(tr::now),\n\t\t\t[] { instance()->next(); });\n\t\tnext->setEnabled(instance()->nextAvailable(type));\n\t}\n}\n\n} // namespace Menu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_dock.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass QMenu;\n\nnamespace Menu {\n\nvoid RefreshDockMenu(QMenu *menu);\n\n} // namespace Menu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_item_download_files.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"menu/menu_item_download_files.h\"\n\n#include \"base/base_file_utilities.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/file_utilities.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"history/history_inner_widget.h\"\n#include \"history/history_item.h\"\n#include \"history/view/history_view_list_widget.h\" // HistoryView::SelectedItem.\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mainwindow.h\"\n#include \"storage/storage_account.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Menu {\nnamespace {\n\nusing Documents = std::vector<std::pair<not_null<DocumentData*>, FullMsgId>>;\nusing Photos = std::vector<std::pair<not_null<PhotoData*>, FullMsgId>>;\n\n[[nodiscard]] bool Added(\n\t\tHistoryItem *item,\n\t\tDocuments &documents,\n\t\tPhotos &photos) {\n\tif (item && !item->forbidsForward()) {\n\t\tif (const auto media = item->media()) {\n\t\t\tif (const auto photo = media->photo()) {\n\t\t\t\tphotos.emplace_back(photo, item->fullId());\n\t\t\t\treturn true;\n\t\t\t} else if (const auto document = media->document()) {\n\t\t\t\tdocuments.emplace_back(document, item->fullId());\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid AddAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tDocuments &&documents,\n\t\tPhotos &&photos,\n\t\tFn<void()> callback) {\n\tconst auto text = documents.empty()\n\t\t? tr::lng_context_save_images_selected(tr::now)\n\t\t: tr::lng_context_save_documents_selected(tr::now);\n\tconst auto icon = documents.empty()\n\t\t? &st::menuIconSaveImage\n\t\t: &st::menuIconDownload;\n\tconst auto shouldShowToast = documents.empty();\n\n\tconst auto weak = base::make_weak(controller);\n\tconst auto saveImages = [=](const QString &folderPath) {\n\t\tconst auto controller = weak.get();\n\t\tif (!controller) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto session = &controller->session();\n\t\tconst auto downloadPath = folderPath.isEmpty()\n\t\t\t? Core::App().settings().downloadPath()\n\t\t\t: folderPath;\n\t\tconst auto path = downloadPath.isEmpty()\n\t\t\t? File::DefaultDownloadPath(session)\n\t\t\t: (downloadPath == FileDialog::Tmp())\n\t\t\t? session->local().tempDirectory()\n\t\t\t: downloadPath;\n\t\tif (path.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tQDir().mkpath(path);\n\n\t\tconst auto showToast = !shouldShowToast\n\t\t\t? Fn<void(const QString &)>(nullptr)\n\t\t\t: [=](const QString &lastPath) {\n\t\t\t\tconst auto filter = [lastPath](const auto ...) {\n\t\t\t\t\tFile::ShowInFolder(lastPath);\n\t\t\t\t\treturn false;\n\t\t\t\t};\n\t\t\t\tcontroller->showToast({\n\t\t\t\t\t.text = (photos.size() > 1\n\t\t\t\t\t\t\t? tr::lng_mediaview_saved_images_to\n\t\t\t\t\t\t\t: tr::lng_mediaview_saved_to)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_downloads,\n\t\t\t\t\t\ttr::link(\n\t\t\t\t\t\t\ttr::lng_mediaview_downloads(tr::now),\n\t\t\t\t\t\t\t\"internal:show_saved_message\"),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\t.filter = filter,\n\t\t\t\t\t.st = &st::defaultToast,\n\t\t\t\t});\n\t\t\t};\n\n\t\tauto views = std::vector<std::shared_ptr<Data::PhotoMedia>>();\n\t\tfor (const auto &[photo, fullId] : photos) {\n\t\t\tif (const auto view = photo->createMediaView()) {\n\t\t\t\tview->wanted(Data::PhotoSize::Large, fullId);\n\t\t\t\tviews.push_back(view);\n\t\t\t}\n\t\t}\n\n\t\tconst auto finalCheck = [=] {\n\t\t\tfor (const auto &[photo, _] : photos) {\n\t\t\t\tif (photo->loading()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\n\t\tconst auto saveToFiles = [=] {\n\t\t\tconst auto fullPath = [&](int i) {\n\t\t\t\treturn filedialogDefaultName(\n\t\t\t\t\tu\"photo_\"_q + QString::number(i),\n\t\t\t\t\tu\".jpg\"_q,\n\t\t\t\t\tpath);\n\t\t\t};\n\t\t\tauto lastPath = QString();\n\t\t\tfor (auto i = 0; i < views.size(); i++) {\n\t\t\t\tlastPath = fullPath(i + 1);\n\t\t\t\tviews[i]->saveToFile(lastPath);\n\t\t\t}\n\t\t\tif (showToast) {\n\t\t\t\tshowToast(lastPath);\n\t\t\t}\n\t\t};\n\n\t\tif (finalCheck()) {\n\t\t\tsaveToFiles();\n\t\t} else {\n\t\t\tauto lifetime = std::make_shared<rpl::lifetime>();\n\t\t\tsession->downloaderTaskFinished(\n\t\t\t) | rpl::on_next([=]() mutable {\n\t\t\t\tif (finalCheck()) {\n\t\t\t\t\tsaveToFiles();\n\t\t\t\t\tbase::take(lifetime)->destroy();\n\t\t\t\t}\n\t\t\t}, *lifetime);\n\t\t}\n\t};\n\tconst auto saveDocuments = [=](const QString &folderPath) {\n\t\tfor (const auto &[document, origin] : documents) {\n\t\t\tif (!folderPath.isEmpty()) {\n\t\t\t\tconst auto name =\n\t\t\t\t\tbase::FileNameFromUserString(document->filename());\n\t\t\t\tdocument->save(origin, folderPath + name);\n\t\t\t} else {\n\t\t\t\tDocumentSaveClickHandler::SaveAndTrack(origin, document);\n\t\t\t}\n\t\t}\n\t};\n\n\tmenu->addAction(text, [=] {\n\t\tconst auto save = [=](const QString &folderPath) {\n\t\t\tsaveImages(folderPath);\n\t\t\tsaveDocuments(folderPath);\n\t\t\tcallback();\n\t\t};\n\t\tconst auto controller = weak.get();\n\t\tif (!controller) {\n\t\t\treturn;\n\t\t}\n\t\tif (Core::App().settings().askDownloadPath()) {\n\t\t\tconst auto initialPath = [] {\n\t\t\t\tconst auto path = Core::App().settings().downloadPath();\n\t\t\t\tif (!path.isEmpty() && path != FileDialog::Tmp()) {\n\t\t\t\t\treturn path.left(path.size()\n\t\t\t\t\t\t- (path.endsWith('/') ? 1 : 0));\n\t\t\t\t}\n\t\t\t\treturn QString();\n\t\t\t}();\n\t\t\tconst auto handleFolder = [=](const QString &result) {\n\t\t\t\tif (!result.isEmpty()) {\n\t\t\t\t\tconst auto folderPath = result.endsWith('/')\n\t\t\t\t\t\t? result\n\t\t\t\t\t\t: (result + '/');\n\t\t\t\t\tsave(folderPath);\n\t\t\t\t}\n\t\t\t};\n\t\t\tFileDialog::GetFolder(\n\t\t\t\tcontroller->window().widget().get(),\n\t\t\t\ttr::lng_download_path_choose(tr::now),\n\t\t\t\tinitialPath,\n\t\t\t\thandleFolder);\n\t\t} else {\n\t\t\tsave(QString());\n\t\t}\n\t}, icon);\n}\n\n} // namespace\n\nvoid AddDownloadFilesAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<Window::SessionController*> window,\n\t\tconst std::vector<HistoryView::SelectedItem> &selectedItems,\n\t\tnot_null<HistoryView::ListWidget*> list) {\n\tif (selectedItems.empty()) {\n\t\treturn;\n\t}\n\tauto docs = Documents();\n\tauto photos = Photos();\n\tfor (const auto &selectedItem : selectedItems) {\n\t\tconst auto &id = selectedItem.msgId;\n\t\tconst auto item = window->session().data().message(id);\n\n\t\tif (!Added(item, docs, photos)) {\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto done = [weak = base::make_weak(list)] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->cancelSelection();\n\t\t}\n\t};\n\tAddAction(menu, window, std::move(docs), std::move(photos), done);\n}\n\nvoid AddDownloadFilesAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<Window::SessionController*> window,\n\t\tconst base::flat_map<HistoryItem*, TextSelection, std::less<>> &items,\n\t\tnot_null<HistoryInner*> list) {\n\tif (items.empty()) {\n\t\treturn;\n\t}\n\tauto sortedItems = ranges::views::all(items)\n\t\t| ranges::views::keys\n\t\t| ranges::to<std::vector>();\n\tranges::sort(sortedItems, {}, &HistoryItem::fullId);\n\tauto docs = Documents();\n\tauto photos = Photos();\n\tfor (const auto &item : sortedItems) {\n\t\tif (!Added(item, docs, photos)) {\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto done = [weak = base::make_weak(list)] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->clearSelected();\n\t\t}\n\t};\n\tAddAction(menu, window, std::move(docs), std::move(photos), done);\n}\n\n} // namespace Menu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_item_download_files.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass HistoryInner;\nclass HistoryItem;\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nnamespace HistoryView {\nclass ListWidget;\nstruct SelectedItem;\n} // namespace HistoryView\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Menu {\n\nvoid AddDownloadFilesAction(\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<Window::SessionController*> window,\n\tconst std::vector<HistoryView::SelectedItem> &selectedItems,\n\tnot_null<HistoryView::ListWidget*> list);\n\nvoid AddDownloadFilesAction(\n\tnot_null<Ui::PopupMenu*> menu,\n\tnot_null<Window::SessionController*> window,\n\t// From the legacy history inner widget.\n\tconst base::flat_map<HistoryItem*, TextSelection, std::less<>> &items,\n\tnot_null<HistoryInner*> list);\n\n} // namespace Menu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_item_rate_transcribe.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"menu/menu_item_rate_transcribe.h\"\n\n#include \"base/call_delayed.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/rect.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Menu {\nnamespace {\n\nconstexpr auto kDuration = crl::time(5000);\n\n} // namespace\n\nRateTranscribe::RateTranscribe(\n\tnot_null<Ui::PopupMenu*> popupMenu,\n\tconst style::Menu &st,\n\tFn<void(bool)> rate)\n: Ui::Menu::ItemBase(popupMenu->menu(), st)\n, _dummyAction(Ui::CreateChild<QAction>(this))\n, _leftButton(Ui::CreateSimpleCircleButton(\n\tthis,\n\tst::defaultRippleAnimation))\n, _rightButton(Ui::CreateSimpleCircleButton(\n\tthis,\n\tst::defaultRippleAnimation)) {\n\tsetAcceptBoth(true);\n\n\tfitToMenuWidth();\n\n\tenableMouseSelecting();\n\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tUi::AddSkip(content);\n\n\tconst auto label = content->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_context_rate_transcription(),\n\t\t\tst::boxDividerLabel),\n\t\tstyle::margins(),\n\t\tstyle::al_top);\n\tsetMinWidth(\n\t\tlabel->st().style.font->width(\n\t\t\ttr::lng_context_rate_transcription(tr::now)));\n\twidthValue() | rpl::on_next([=, menu = popupMenu->menu()](int w) {\n\t\tcontent->resizeToWidth(menu->width());\n\t}, content->lifetime());\n\tUi::AddSkip(content);\n\n\t// const auto leftButton = Ui::CreateChild<Ui::IconButton>(\n\t// \tthis,\n\t// \tst::menuTranscribeDummyButton);\n\t// const auto rightButton = Ui::CreateChild<Ui::IconButton>(\n\t// \tthis,\n\t// \tst::menuTranscribeDummyButton);\n\t{\n\t\t_leftButton->resize(Size(st::menuTranscribeDummyButton.width));\n\t\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t_leftButton,\n\t\t\tQString::fromUtf8(\"\\U0001F44D\"));\n\t\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents, true);\n\t\t_leftButton->sizeValue() | rpl::on_next([=](QSize s) {\n\t\t\tlabel->moveToLeft(\n\t\t\t\t(s.width() - label->width()) / 2,\n\t\t\t\t(s.height() - label->height()) / 2);\n\t\t}, label->lifetime());\n\t}\n\t{\n\t\t_rightButton->resize(Size(st::menuTranscribeDummyButton.width));\n\t\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t_rightButton,\n\t\t\tQString::fromUtf8(\"\\U0001F44E\"));\n\t\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents, true);\n\t\t_rightButton->sizeValue() | rpl::on_next([=](QSize s) {\n\t\t\tlabel->moveToLeft(\n\t\t\t\t(s.width() - label->width()) / 2,\n\t\t\t\t(s.height() - label->height()) / 2);\n\t\t}, label->lifetime());\n\t}\n\t{\n\t\tconst auto showToast = [=,\n\t\t\t\tweak = base::make_weak(popupMenu->parentWidget())]{\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tbase::call_delayed(\n\t\t\t\t\tst::universalDuration * 1.1,\n\t\t\t\t\tcrl::guard(strong, [=] {\n\t\t\t\t\t\tUi::Toast::Show(strong->window(), {\n\t\t\t\t\t\t\t.text = tr::lng_toast_sent_rate_transcription(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tTextWithEntities::Simple),\n\t\t\t\t\t\t\t.duration = kDuration,\n\t\t\t\t\t\t});\n\t\t\t\t\t}));\n\t\t\t}\n\t\t};\n\t\tconst auto hideMenu = [=, weak = base::make_weak(popupMenu)] {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tbase::call_delayed(\n\t\t\t\t\tst::universalDuration,\n\t\t\t\t\tcrl::guard(strong, [=] { strong->hideMenu(false); }));\n\t\t\t}\n\t\t};\n\t\t_leftButton->setClickedCallback([=] {\n\t\t\trate(true);\n\t\t\tshowToast();\n\t\t\thideMenu();\n\t\t});\n\t\t_rightButton->setClickedCallback([=] {\n\t\t\trate(false);\n\t\t\tshowToast();\n\t\t\thideMenu();\n\t\t});\n\t}\n\t_desiredHeight = rect::m::sum::v(st::menuTranscribeItemPadding)\n\t\t+ st::menuTranscribeDummyButton.height\n\t\t+ label->st().style.font->height;\n\trpl::combine(\n\t\tcontent->geometryValue(),\n\t\tlabel->geometryValue()\n\t) | rpl::on_next([=](\n\t\t\tconst QRect &contentRect,\n\t\t\tconst QRect &labelRect) {\n\t\t_leftButton->moveToLeft(\n\t\t\tlabelRect.x(),\n\t\t\trect::bottom(contentRect));\n\t\t_rightButton->moveToLeft(\n\t\t\trect::right(labelRect) - _rightButton->width(),\n\t\t\trect::bottom(contentRect));\n\t\t_desiredHeight = rect::m::sum::v(st::menuTranscribeItemPadding)\n\t\t\t+ _leftButton->height()\n\t\t\t+ labelRect.height();\n\t}, _leftButton->lifetime());\n\t_leftButton->show();\n\t_rightButton->show();\n\n\tselects() | rpl::on_next([=](const Ui::Menu::CallbackData &data) {\n\t\tif (data.selected\n\t\t\t&& data.source == Ui::Menu::TriggeredSource::Keyboard) {\n\t\t\t_leftButton->setForceRippled(true);\n\t\t\t_selectedButton = SelectedButton::Left;\n\t\t} else if (!data.selected) {\n\t\t\t_leftButton->setForceRippled(false);\n\t\t\t_rightButton->setForceRippled(false);\n\t\t\t_selectedButton = SelectedButton::None;\n\t\t}\n\t}, lifetime());\n}\n\nnot_null<QAction*> RateTranscribe::action() const {\n\treturn _dummyAction;\n}\n\nbool RateTranscribe::isEnabled() const {\n\treturn true;\n}\n\nint RateTranscribe::contentHeight() const {\n\treturn _desiredHeight;\n}\n\nvoid RateTranscribe::handleKeyPress(not_null<QKeyEvent*> e) {\n\tconst auto key = e->key();\n\tif (key == Qt::Key_Left) {\n\t\tif (_selectedButton == SelectedButton::Right) {\n\t\t\t_rightButton->setForceRippled(false);\n\t\t\t_leftButton->setForceRippled(true);\n\t\t\t_selectedButton = SelectedButton::Left;\n\t\t} else if (_selectedButton == SelectedButton::Left) {\n\t\t\t_leftButton->setForceRippled(false);\n\t\t\t_rightButton->setForceRippled(true);\n\t\t\t_selectedButton = SelectedButton::Right;\n\t\t} else {\n\t\t\t_leftButton->setForceRippled(true);\n\t\t\t_selectedButton = SelectedButton::Left;\n\t\t}\n\t} else if (key == Qt::Key_Right) {\n\t\tif (_selectedButton == SelectedButton::Left) {\n\t\t\t_leftButton->setForceRippled(false);\n\t\t\t_rightButton->setForceRippled(true);\n\t\t\t_selectedButton = SelectedButton::Right;\n\t\t} else if (_selectedButton == SelectedButton::Right) {\n\t\t\t_rightButton->setForceRippled(false);\n\t\t\t_leftButton->setForceRippled(true);\n\t\t\t_selectedButton = SelectedButton::Left;\n\t\t} else {\n\t\t\t_leftButton->setForceRippled(true);\n\t\t\t_selectedButton = SelectedButton::Left;\n\t\t}\n\t} else if (key == Qt::Key_Return\n\t\t\t|| key == Qt::Key_Enter\n\t\t\t|| key == Qt::Key_Space) {\n\t\tif (_selectedButton == SelectedButton::Left) {\n\t\t\t_leftButton->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);\n\t\t} else if (_selectedButton == SelectedButton::Right) {\n\t\t\t_rightButton->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);\n\t\t}\n\t}\n}\n\n} // namespace Menu"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_item_rate_transcribe.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/menu/menu_item_base.h\"\n\nclass HistoryItem;\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Menu {\n\nclass RateTranscribe final : public Ui::Menu::ItemBase {\npublic:\n\tRateTranscribe(\n\t\tnot_null<Ui::PopupMenu*> parent,\n\t\tconst style::Menu &st,\n\t\tFn<void(bool)> rate);\n\n\tnot_null<QAction*> action() const override;\n\tbool isEnabled() const override;\n\nprotected:\n\tint contentHeight() const override;\n\tvoid handleKeyPress(not_null<QKeyEvent*> e) override;\n\nprivate:\n\tenum class SelectedButton {\n\t\tNone,\n\t\tLeft,\n\t\tRight,\n\t};\n\n\tint _desiredHeight = 0;\n\tnot_null<QAction*> _dummyAction;\n\tnot_null<Ui::RippleButton*> _leftButton;\n\tnot_null<Ui::RippleButton*> _rightButton;\n\tSelectedButton _selectedButton = SelectedButton::None;\n\n};\n\nbool HasRateTranscribeItem(not_null<HistoryItem*>);\n\n} // namespace Menu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_item_rate_transcribe_session.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"menu/menu_item_rate_transcribe_session.h\"\n\n#include \"api/api_transcribes.h\"\n#include \"apiwrap.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/rect.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Menu {\n\nFn<void(bool)> RateTranscribeCallbackFactory(\n\t\tnot_null<HistoryItem*> item) {\n\treturn [item](bool good) {\n\t\titem->history()->peer->session().api().transcribes().rate(\n\t\t\titem,\n\t\t\tgood);\n\t};\n}\n\nbool HasRateTranscribeItem(not_null<HistoryItem*> item) {\n\tconst auto &peer = item->history()->peer;\n\tif (!peer->session().api().transcribes().entry(\n\t\t\titem).result.isEmpty()) {\n\t\treturn !peer->session().api().transcribes().isRated(item);\n\t}\n\treturn false;\n}\n\n} // namespace Menu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_item_rate_transcribe_session.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass HistoryItem;\n\nnamespace Menu {\n\n[[nodiscard]] Fn<void(bool)> RateTranscribeCallbackFactory(\n\tnot_null<HistoryItem*>);\n\n[[nodiscard]] bool HasRateTranscribeItem(not_null<HistoryItem*>);\n\n} // namespace Menu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_mute.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"menu/menu_mute.h\"\n\n#include \"boxes/ringtones_box.h\"\n#include \"data/data_session.h\"\n#include \"data/data_thread.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/notify/data_peer_notify_settings.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"ui/boxes/choose_time.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/time_picker_box.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_info.h\" // infoTopBarMenu\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace MuteMenu {\nnamespace {\n\nconstexpr auto kMuteDurSecondsDefault = crl::time(8) * 3600;\nconstexpr auto kMuteForeverValue = std::numeric_limits<TimeId>::max();\n\nclass IconWithText final : public Ui::Menu::Action {\npublic:\n\tusing Ui::Menu::Action::Action;\n\n\tvoid setData(const QString &text, const QPoint &iconPosition);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tQPoint _iconPosition;\n\tQString _text;\n\n};\n\nvoid IconWithText::setData(const QString &text, const QPoint &iconPosition) {\n\t_iconPosition = iconPosition;\n\t_text = text;\n}\n\nvoid IconWithText::paintEvent(QPaintEvent *e) {\n\tUi::Menu::Action::paintEvent(e);\n\n\tauto p = QPainter(this);\n\tp.setFont(st::menuIconMuteForAnyTextFont);\n\tp.setPen(st::menuIconColor);\n\tp.drawText(_iconPosition, _text);\n}\n\nclass MuteItem final : public Ui::Menu::Action {\npublic:\n\tMuteItem(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::Menu &st,\n\t\tDescriptor descriptor);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tconst QPoint _itemIconPosition;\n\tUi::Animations::Simple _animation;\n\tbool _isMuted = false;\n\tbool _inited;\n\n};\n\nMuteItem::MuteItem(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::Menu &st,\n\tDescriptor descriptor)\n: Ui::Menu::Action(\n\tparent,\n\tst,\n\tUi::CreateChild<QAction>(parent.get()),\n\tnullptr,\n\tnullptr)\n, _itemIconPosition(st.itemIconPosition) {\n\tdescriptor.isMutedValue(\n\t) | rpl::on_next([=](bool isMuted) {\n\t\taction()->setText(isMuted\n\t\t\t? tr::lng_mute_menu_duration_unmute(tr::now)\n\t\t\t: tr::lng_mute_menu_duration_forever(tr::now));\n\t\tif (_inited && isMuted == _isMuted) {\n\t\t\treturn;\n\t\t}\n\t\t_inited = true;\n\t\t_isMuted = isMuted;\n\t\t_animation.start(\n\t\t\t[=] { update(); },\n\t\t\tisMuted ? 0. : 1.,\n\t\t\tisMuted ? 1. : 0.,\n\t\t\tst::defaultPopupMenu.showDuration);\n\t}, lifetime());\n\t_animation.stop();\n\n\tsetActionTriggered([=] {\n\t\tdescriptor.updateMutePeriod(_isMuted ? 0 : kMuteForeverValue);\n\t});\n}\n\nvoid MuteItem::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tconst auto progress = _animation.value(_isMuted ? 1. : 0.);\n\tconst auto color = anim::color(\n\t\tst::menuIconAttentionColor,\n\t\tst::boxTextFgGood,\n\t\tprogress);\n\tp.setPen(color);\n\n\tAction::paintBackground(p, Action::isSelected());\n\tRippleButton::paintRipple(p, 0, 0);\n\tAction::paintText(p);\n\n\tconst auto &icon = _isMuted ? st::menuIconUnmute : st::menuIconMute;\n\ticon.paint(p, _itemIconPosition, width(), color);\n}\n\nvoid MuteBox(not_null<Ui::GenericBox*> box, Descriptor descriptor) {\n\tstruct State {\n\t\tint lastSeconds = 0;\n\t};\n\n\tauto chooseTimeResult = ChooseTimeWidget(box, kMuteDurSecondsDefault);\n\tbox->addRow(std::move(chooseTimeResult.widget));\n\n\tconst auto state = box->lifetime().make_state<State>();\n\n\tbox->setTitle(tr::lng_mute_box_title());\n\n\tauto confirmText = std::move(\n\t\tchooseTimeResult.secondsValue\n\t) | rpl::map([=](int seconds) {\n\t\tstate->lastSeconds = seconds;\n\t\treturn !seconds\n\t\t\t? tr::lng_mute_menu_unmute()\n\t\t\t: tr::lng_mute_menu_mute();\n\t}) | rpl::flatten_latest();\n\n\tUi::ConfirmBox(box, {\n\t\t.confirmed = [=] {\n\t\t\tdescriptor.updateMutePeriod(state->lastSeconds);\n\t\t\tbox->getDelegate()->hideLayer();\n\t\t},\n\t\t.confirmText = std::move(confirmText),\n\t\t.cancelText = tr::lng_cancel(),\n\t});\n}\n\nvoid PickMuteBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tDescriptor descriptor) {\n\tstruct State {\n\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t};\n\tconst auto seconds = Ui::DefaultTimePickerValues();\n\tconst auto phrases = ranges::views::all(\n\t\tseconds\n\t) | ranges::views::transform(Ui::FormatMuteFor) | ranges::to_vector;\n\n\tconst auto state = box->lifetime().make_state<State>();\n\n\tconst auto pickerCallback = TimePickerBox(box, seconds, phrases, 0);\n\n\tUi::ConfirmBox(box, {\n\t\t.confirmed = [=] {\n\t\t\tconst auto muteFor = pickerCallback();\n\t\t\tdescriptor.updateMutePeriod(muteFor);\n\t\t\tdescriptor.session->settings().addMutePeriod(muteFor);\n\t\t\tdescriptor.session->saveSettings();\n\t\t\tbox->closeBox();\n\t\t},\n\t\t.confirmText = tr::lng_mute_menu_mute(),\n\t\t.cancelText = tr::lng_cancel(),\n\t});\n\n\tbox->setTitle(tr::lng_mute_box_title());\n\n\tconst auto top = box->addTopButton(st::infoTopBarMenu);\n\ttop->setClickedCallback([=] {\n\t\tif (state->menu) {\n\t\t\treturn;\n\t\t}\n\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\ttop,\n\t\t\tst::popupMenuWithIcons);\n\t\tstate->menu->addAction(\n\t\t\ttr::lng_manage_messages_ttl_after_custom(tr::now),\n\t\t\t[=] { box->getDelegate()->show(Box(MuteBox, descriptor)); },\n\t\t\t&st::menuIconCustomize);\n\t\tstate->menu->setDestroyedCallback(crl::guard(top, [=] {\n\t\t\ttop->setForceRippled(false);\n\t\t}));\n\t\ttop->setForceRippled(true);\n\t\tstate->menu->popup(QCursor::pos());\n\t});\n}\n\n} // namespace\n\nDescriptor ThreadDescriptor(not_null<Data::Thread*> thread) {\n\tconst auto weak = base::make_weak(thread);\n\tconst auto isMutedValue = [=]() -> rpl::producer<bool> {\n\t\tif (const auto strong = weak.get()) {\n\t\t\treturn Info::Profile::NotificationsEnabledValue(\n\t\t\t\tstrong\n\t\t\t) | rpl::map(!rpl::mappers::_1);\n\t\t}\n\t\treturn rpl::single(false);\n\t};\n\tconst auto currentSound = [=] {\n\t\tconst auto strong = weak.get();\n\t\treturn strong\n\t\t\t? strong->owner().notifySettings().sound(strong)\n\t\t\t: std::optional<Data::NotifySound>();\n\t};\n\tconst auto updateSound = crl::guard(weak, [=](Data::NotifySound sound) {\n\t\tthread->owner().notifySettings().update(thread, {}, {}, sound);\n\t});\n\tconst auto updateMutePeriod = crl::guard(weak, [=](TimeId mute) {\n\t\tconst auto settings = &thread->owner().notifySettings();\n\t\tif (!mute) {\n\t\t\tsettings->update(thread, { .unmute = true });\n\t\t} else if (mute == kMuteForeverValue) {\n\t\t\tsettings->update(thread, { .forever = true });\n\t\t} else {\n\t\t\tsettings->update(thread, { .period = mute });\n\t\t}\n\t});\n\treturn {\n\t\t.session = &thread->session(),\n\t\t.isMutedValue = isMutedValue,\n\t\t.currentSound = currentSound,\n\t\t.updateSound = updateSound,\n\t\t.updateMutePeriod = updateMutePeriod,\n\t\t.volumeController = Data::ThreadRingtonesVolumeController(thread),\n\t};\n}\n\nDescriptor DefaultDescriptor(\n\t\tnot_null<Main::Session*> session,\n\t\tData::DefaultNotify type) {\n\tconst auto settings = &session->data().notifySettings();\n\tconst auto isMutedValue = [=]() -> rpl::producer<bool> {\n\t\treturn rpl::single(\n\t\t\trpl::empty\n\t\t) | rpl::then(\n\t\t\tsettings->defaultUpdates(type)\n\t\t) | rpl::map([=] {\n\t\t\treturn settings->isMuted(type);\n\t\t});\n\t};\n\tconst auto currentSound = [=] {\n\t\treturn settings->defaultSettings(type).sound();\n\t};\n\tconst auto updateSound = [=](Data::NotifySound sound) {\n\t\tsettings->defaultUpdate(type, {}, {}, sound);\n\t};\n\tconst auto updateMutePeriod = [=](TimeId mute) {\n\t\tif (!mute) {\n\t\t\tsettings->defaultUpdate(type, { .unmute = true });\n\t\t} else if (mute == kMuteForeverValue) {\n\t\t\tsettings->defaultUpdate(type, { .forever = true });\n\t\t} else {\n\t\t\tsettings->defaultUpdate(type, { .period = mute });\n\t\t}\n\t};\n\treturn {\n\t\t.session = session,\n\t\t.isMutedValue = isMutedValue,\n\t\t.currentSound = currentSound,\n\t\t.updateSound = updateSound,\n\t\t.updateMutePeriod = updateMutePeriod,\n\t\t.volumeController = DefaultRingtonesVolumeController(session, type),\n\t};\n}\n\nvoid FillMuteMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tDescriptor descriptor,\n\t\tstd::shared_ptr<Ui::Show> show) {\n\tconst auto session = descriptor.session;\n\tconst auto soundSelect = [=] {\n\t\tif (const auto currentSound = descriptor.currentSound()) {\n\t\t\tshow->showBox(Box(\n\t\t\t\tRingtonesBox,\n\t\t\t\tsession,\n\t\t\t\t*currentSound,\n\t\t\t\tdescriptor.updateSound,\n\t\t\t\tdescriptor.volumeController));\n\t\t}\n\t};\n\tmenu->addAction(\n\t\ttr::lng_mute_menu_sound_select(tr::now),\n\t\tsoundSelect,\n\t\t&st::menuIconSoundSelect);\n\n\tconst auto soundIsNone = descriptor.currentSound().value_or(\n\t\tData::NotifySound()\n\t).none;\n\tconst auto toggleSound = [=] {\n\t\tif (auto sound = descriptor.currentSound()) {\n\t\t\tsound->none = !soundIsNone;\n\t\t\tdescriptor.updateSound(*sound);\n\t\t}\n\t};\n\tmenu->addAction(\n\t\t(soundIsNone\n\t\t\t? tr::lng_mute_menu_sound_on(tr::now)\n\t\t\t: tr::lng_mute_menu_sound_off(tr::now)),\n\t\ttoggleSound,\n\t\tsoundIsNone ? &st::menuIconSoundOn : &st::menuIconSoundOff);\n\n\tconst auto &st = menu->st().menu;\n\tconst auto iconTextPosition = st.itemIconPosition\n\t\t+ st::menuIconMuteForAnyTextPosition;\n\tfor (const auto muteFor : session->settings().mutePeriods()) {\n\t\tconst auto callback = [=, update = descriptor.updateMutePeriod] {\n\t\t\tupdate(muteFor);\n\t\t};\n\n\t\tauto item = base::make_unique_q<IconWithText>(\n\t\t\tmenu->menu(),\n\t\t\tst,\n\t\t\tUi::Menu::CreateAction(\n\t\t\t\tmenu->menu().get(),\n\t\t\t\ttr::lng_mute_menu_duration_any(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_duration,\n\t\t\t\t\tUi::FormatMuteFor(muteFor)),\n\t\t\t\tcallback),\n\t\t\t&st::menuIconMuteForAny,\n\t\t\t&st::menuIconMuteForAny);\n\t\titem->setData(Ui::FormatMuteForTiny(muteFor), iconTextPosition);\n\t\tmenu->addAction(std::move(item));\n\t}\n\n\tmenu->addAction(\n\t\ttr::lng_mute_menu_duration(tr::now),\n\t\t[=] { show->showBox(Box(PickMuteBox, descriptor)); },\n\t\t&st::menuIconMuteFor);\n\n\tmenu->addAction(\n\t\tbase::make_unique_q<MuteItem>(\n\t\t\tmenu->menu(),\n\t\t\tmenu->st().menu,\n\t\t\tdescriptor));\n}\n\nvoid SetupMuteMenu(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<> triggers,\n\t\tFn<std::optional<Descriptor>()> makeDescriptor,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tFn<QPoint()> positionCallback) {\n\tstruct State {\n\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t};\n\tconst auto state = parent->lifetime().make_state<State>();\n\tstd::move(\n\t\ttriggers\n\t) | rpl::on_next([=] {\n\t\tif (state->menu) {\n\t\t\treturn;\n\t\t} else if (const auto descriptor = makeDescriptor()) {\n\t\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\tparent,\n\t\t\t\tst::popupMenuWithIcons);\n\t\t\tFillMuteMenu(state->menu.get(), *descriptor, show);\n\t\t\tstate->menu->popup(positionCallback\n\t\t\t\t? positionCallback()\n\t\t\t\t: QCursor::pos());\n\t\t}\n\t}, parent->lifetime());\n}\n\n} // namespace MuteMenu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_mute.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/notify/data_peer_notify_volume.h\" // VolumeController\n\nnamespace Data {\nclass Thread;\nstruct NotifySound;\nenum class DefaultNotify : uint8_t;\nstruct VolumeController;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass PopupMenu;\nclass RpWidget;\nclass Show;\n} // namespace Ui\n\nnamespace MuteMenu {\n\nstruct Descriptor {\n\tnot_null<Main::Session*> session;\n\tFn<rpl::producer<bool>()> isMutedValue;\n\tFn<std::optional<Data::NotifySound>()> currentSound;\n\tFn<void(Data::NotifySound)> updateSound;\n\tFn<void(TimeId)> updateMutePeriod;\n\tData::VolumeController volumeController;\n};\n\n[[nodiscard]] Descriptor ThreadDescriptor(not_null<Data::Thread*> thread);\n[[nodiscard]] Descriptor DefaultDescriptor(\n\tnot_null<Main::Session*> session,\n\tData::DefaultNotify type);\n\nvoid FillMuteMenu(\n\tnot_null<Ui::PopupMenu*> menu,\n\tDescriptor descriptor,\n\tstd::shared_ptr<Ui::Show> show);\n\nvoid SetupMuteMenu(\n\tnot_null<Ui::RpWidget*> parent,\n\trpl::producer<> triggers,\n\tFn<std::optional<Descriptor>()> makeDescriptor,\n\tstd::shared_ptr<Ui::Show> show,\n\tFn<QPoint()> positionCallback = nullptr);\n\ninline void FillMuteMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tnot_null<Data::Thread*> thread,\n\t\tstd::shared_ptr<Ui::Show> show) {\n\tFillMuteMenu(menu, ThreadDescriptor(thread), std::move(show));\n}\n\ninline void SetupMuteMenu(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<> triggers,\n\t\tFn<Data::Thread*()> makeThread,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tFn<QPoint()> positionCallback = nullptr) {\n\tSetupMuteMenu(parent, std::move(triggers), [=] {\n\t\tconst auto thread = makeThread();\n\t\treturn thread\n\t\t\t? ThreadDescriptor(thread)\n\t\t\t: std::optional<Descriptor>();\n\t}, std::move(show), positionCallback);\n}\n\n} // namespace MuteMenu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_send.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"menu/menu_send.h\"\n\n#include \"menu/menu_checked_action.h\"\n\n#include \"api/api_common.h\"\n#include \"base/event_filter.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/abstract_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"calls/group/calls_group_stars_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"core/shortcuts.h\"\n#include \"history/admin_log/history_admin_log_item.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"history/view/reactions/history_view_reactions_selector.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/history_view_fake_items.h\"\n#include \"history/view/history_view_schedule_box.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_unread_things.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/section_widget.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_window.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace SendMenu {\nnamespace {\n\nconstexpr auto kToggleDuration = crl::time(400);\n\nclass Delegate final : public HistoryView::DefaultElementDelegate {\npublic:\n\tDelegate(not_null<Ui::PathShiftGradient*> pathGradient)\n\t: _pathGradient(pathGradient) {\n\t}\n\nprivate:\n\tbool elementAnimationsPaused() override {\n\t\treturn false;\n\t}\n\tnot_null<Ui::PathShiftGradient*> elementPathShiftGradient() override {\n\t\treturn _pathGradient;\n\t}\n\tHistoryView::Context elementContext() override {\n\t\treturn HistoryView::Context::ContactPreview;\n\t}\n\n\tconst not_null<Ui::PathShiftGradient*> _pathGradient;\n};\n\nclass EffectPreview final : public Ui::RpWidget {\npublic:\n\tEffectPreview(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tDetails details,\n\t\tQPoint position,\n\t\tconst Data::Reaction &effect,\n\t\tFn<void(Action, Details)> action,\n\t\tFn<void()> done);\n\n\tvoid hideAnimated();\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\n\t[[nodiscard]] bool canSend() const;\n\n\tvoid setupGeometry(QPoint position);\n\tvoid setupBackground();\n\tvoid setupItem();\n\tvoid repaintBackground();\n\tvoid setupLottie();\n\tvoid setupSend(Details details);\n\tvoid createLottie();\n\n\t[[nodiscard]] bool ready() const;\n\tvoid paintLoading(QPainter &p);\n\tvoid paintLottie(QPainter &p);\n\tbool checkIconBecameLoaded();\n\t[[nodiscard]] bool checkLoaded();\n\tvoid toggle(bool shown);\n\n\tconst EffectId _effectId = 0;\n\tconst Data::Reaction _effect;\n\tconst std::shared_ptr<ChatHelpers::Show> _show;\n\tconst std::shared_ptr<Ui::ChatTheme> _theme;\n\tconst std::unique_ptr<Ui::ChatStyle> _chatStyle;\n\tconst std::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\tconst std::unique_ptr<Delegate> _delegate;\n\tconst not_null<History*> _history;\n\tconst AdminLog::OwnedItem _replyTo;\n\tconst AdminLog::OwnedItem _item;\n\tconst std::unique_ptr<Ui::FlatButton> _send;\n\tconst std::unique_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _premiumPromoLabel;\n\tconst not_null<Ui::RpWidget*> _bottom;\n\tconst Fn<void()> _close;\n\tconst Fn<void(Action, Details)> _actionWithEffect;\n\n\tQImage _icon;\n\tstd::shared_ptr<Data::DocumentMedia> _media;\n\tQByteArray _bytes;\n\tQString _filepath;\n\tstd::unique_ptr<Lottie::SinglePlayer> _lottie;\n\n\tQRect _inner;\n\tQImage _bg;\n\tQPoint _itemShift;\n\tQRect _iconRect;\n\tUi::BoxShadow _boxShadow;\n\tstd::unique_ptr<Ui::InfiniteRadialAnimation> _loading;\n\n\tUi::Animations::Simple _shownAnimation;\n\tQPixmap _bottomCache;\n\tbool _hiding = false;\n\n\trpl::lifetime _readyCheckLifetime;\n\n};\n\nclass BottomRounded final : public Ui::FlatButton {\npublic:\n\tusing FlatButton::FlatButton;\n\nprivate:\n\tQImage prepareRippleMask() const override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\n};\n\nQImage BottomRounded::prepareRippleMask() const {\n\tconst auto fill = false;\n\treturn Ui::RippleAnimation::MaskByDrawer(size(), fill, [&](QPainter &p) {\n\t\tconst auto radius = st::previewMenu.radius;\n\t\tconst auto expanded = rect().marginsAdded({ 0, 2 * radius, 0, 0 });\n\t\tp.drawRoundedRect(expanded, radius, radius);\n\t});\n}\n\nvoid BottomRounded::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto radius = st::previewMenu.radius;\n\tconst auto expanded = rect().marginsAdded({ 0, 2 * radius, 0, 0 });\n\tp.setPen(Qt::NoPen);\n\tconst auto &st = st::previewMarkRead;\n\tif (isOver()) {\n\t\tp.setBrush(st.overBgColor);\n\t}\n\tp.drawRoundedRect(expanded, radius, radius);\n\tp.end();\n\n\tUi::FlatButton::paintEvent(e);\n}\n\n[[nodiscard]] Data::PossibleItemReactionsRef LookupPossibleEffects(\n\t\tnot_null<Main::Session*> session) {\n\tauto result = Data::PossibleItemReactionsRef();\n\tconst auto reactions = &session->data().reactions();\n\tconst auto &effects = reactions->list(Data::Reactions::Type::Effects);\n\tconst auto premiumPossible = session->premiumPossible();\n\tauto added = base::flat_set<Data::ReactionId>();\n\tresult.recent.reserve(effects.size());\n\tresult.stickers.reserve(effects.size());\n\tfor (const auto &reaction : effects) {\n\t\tif (premiumPossible || !reaction.premium) {\n\t\t\tif (added.emplace(reaction.id).second) {\n\t\t\t\tif (reaction.aroundAnimation) {\n\t\t\t\t\tresult.recent.push_back(&reaction);\n\t\t\t\t} else {\n\t\t\t\t\tresult.stickers.push_back(&reaction);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] Fn<void(Action, Details)> ComposeActionWithEffect(\n\t\tFn<void(Action, Details)> sendAction,\n\t\tEffectId id,\n\t\tFn<void()> done) {\n\treturn [=](Action action, Details details) {\n\t\taction.options.effectId = id;\n\n\t\tconst auto onstack = done;\n\t\tsendAction(action, details);\n\t\tif (onstack) {\n\t\t\tonstack();\n\t\t}\n\t};\n}\n\nEffectPreview::EffectPreview(\n\tnot_null<QWidget*> parent,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tDetails details,\n\tQPoint position,\n\tconst Data::Reaction &effect,\n\tFn<void(Action, Details)> action,\n\tFn<void()> done)\n: RpWidget(parent)\n, _effectId(effect.id.custom())\n, _effect(effect)\n, _show(show)\n, _theme(Window::Theme::DefaultChatThemeOn(lifetime()))\n, _chatStyle(\n\tstd::make_unique<Ui::ChatStyle>(\n\t\t_show->session().colorIndicesValue()))\n, _pathGradient(\n\tHistoryView::MakePathShiftGradient(_chatStyle.get(), [=] { update(); }))\n, _delegate(std::make_unique<Delegate>(_pathGradient.get()))\n, _history(show->session().data().history(\n\tPeerData::kServiceNotificationsId))\n, _replyTo(HistoryView::GenerateItem(\n\t_delegate.get(),\n\t_history,\n\tHistoryView::GenerateUser(\n\t\t_history,\n\t\ttr::lng_settings_chat_message_reply_from(tr::now)),\n\tFullMsgId(),\n\ttr::lng_settings_chat_message(tr::now)))\n, _item(HistoryView::GenerateItem(\n\t_delegate.get(),\n\t_history,\n\t_history->peer->id,\n\t_replyTo->data()->fullId(),\n\ttr::lng_settings_chat_message_reply(tr::now),\n\tData::Reactions::kFakeEffectId))\n, _send(canSend()\n\t? std::make_unique<BottomRounded>(\n\t\tthis,\n\t\ttr::lng_effect_send(tr::now),\n\t\tst::effectPreviewSend)\n\t: nullptr)\n, _premiumPromoLabel(canSend()\n\t? nullptr\n\t: std::make_unique<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\tthis,\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tthis,\n\t\t\ttr::lng_effect_premium(\n\t\t\t\tlt_link,\n\t\t\t\ttr::lng_effect_premium_link(tr::link),\n\t\t\t\ttr::marked),\n\t\t\tst::effectPreviewPromoLabel),\n\t\tst::effectPreviewPromoPadding))\n, _bottom(_send ? ((Ui::RpWidget*)_send.get()) : _premiumPromoLabel.get())\n, _close(done)\n, _actionWithEffect(ComposeActionWithEffect(action, _effectId, done))\n, _boxShadow(st::previewMenu.animation.shadow) {\n\t_chatStyle->apply(_theme.get());\n\n\tsetupGeometry(position);\n\tsetupItem();\n\tsetupBackground();\n\tsetupLottie();\n\tsetupSend(details);\n\n\ttoggle(true);\n}\n\nvoid EffectPreview::paintEvent(QPaintEvent *e) {\n\tcheckIconBecameLoaded();\n\n\tconst auto progress = _shownAnimation.value(_hiding ? 0. : 1.);\n\tif (!progress) {\n\t\treturn;\n\t}\n\n\tauto p = QPainter(this);\n\tp.setOpacity(progress);\n\tp.drawImage(0, 0, _bg);\n\n\tif (!_bottomCache.isNull()) {\n\t\tp.drawPixmap(_bottom->pos(), _bottomCache);\n\t}\n\n\tif (!ready()) {\n\t\tpaintLoading(p);\n\t} else {\n\t\t_loading = nullptr;\n\t\tp.drawImage(_iconRect, _icon);\n\t\tif (!_hiding) {\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t\tpaintLottie(p);\n\t}\n}\n\nbool EffectPreview::ready() const {\n\treturn !_icon.isNull() && _lottie && _lottie->ready();\n}\n\nvoid EffectPreview::paintLoading(QPainter &p) {\n\tif (!_loading) {\n\t\t_loading = std::make_unique<Ui::InfiniteRadialAnimation>([=] {\n\t\t\tupdate();\n\t\t}, st::effectPreviewLoading);\n\t\t_loading->start(st::defaultInfiniteRadialAnimation.linearPeriod);\n\t}\n\tconst auto loading = _iconRect.marginsRemoved(\n\t\t{ st::lineWidth, st::lineWidth, st::lineWidth, st::lineWidth });\n\tauto hq = PainterHighQualityEnabler(p);\n\tUi::InfiniteRadialAnimation::Draw(\n\t\tp,\n\t\t_loading->computeState(),\n\t\tloading.topLeft(),\n\t\tloading.size(),\n\t\twidth(),\n\t\t_chatStyle->msgInDateFg(),\n\t\tst::effectPreviewLoading.thickness);\n}\n\nvoid EffectPreview::paintLottie(QPainter &p) {\n\tconst auto factor = style::DevicePixelRatio();\n\tauto request = Lottie::FrameRequest();\n\trequest.box = _inner.size() * factor;\n\tconst auto rightAligned = _item->hasRightLayout();\n\tif (!rightAligned) {\n\t\trequest.mirrorHorizontal = true;\n\t}\n\tconst auto frame = _lottie->frameInfo(request);\n\tp.drawImage(\n\t\tQRect(_inner.topLeft(), frame.image.size() / factor),\n\t\tframe.image);\n\t_lottie->markFrameShown();\n}\n\nvoid EffectPreview::hideAnimated() {\n\ttoggle(false);\n}\n\nvoid EffectPreview::mousePressEvent(QMouseEvent *e) {\n\thideAnimated();\n}\n\nvoid EffectPreview::setupGeometry(QPoint position) {\n\tconst auto parent = parentWidget();\n\tconst auto innerSize = HistoryView::Sticker::MessageEffectSize();\n\tconst auto extend = Ui::BoxShadow::ExtendFor(st::previewMenu.shadow);\n\t_inner = QRect(QPoint(extend.left(), extend.top()), innerSize);\n\t_bottom->resizeToWidth(_inner.width());\n\tconst auto size = _inner.marginsAdded(extend).size()\n\t\t+ QSize(0, _bottom->height());\n\tconst auto left = std::max(\n\t\tstd::min(\n\t\t\tposition.x() - size.width() / 2,\n\t\t\tparent->width() - size.width()),\n\t\t0);\n\tconst auto topMin = std::min((parent->height() - size.height()) / 2, 0);\n\tconst auto top = std::max(\n\t\tstd::min(\n\t\t\tposition.y() - size.height() / 2,\n\t\t\tparent->height() - size.height()),\n\t\ttopMin);\n\tsetGeometry(left, top, size.width(), size.height());\n\t_bottom->setGeometry(\n\t\t_inner.x(),\n\t\t_inner.y() + _inner.height(),\n\t\t_inner.width(),\n\t\t_bottom->height());\n}\n\nvoid EffectPreview::setupBackground() {\n\tconst auto ratio = style::DevicePixelRatio();\n\t_bg = QImage(\n\t\tsize() * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_bg.setDevicePixelRatio(ratio);\n\trepaintBackground();\n\t_theme->repaintBackgroundRequests() | rpl::on_next([=] {\n\t\trepaintBackground();\n\t\tupdate();\n\t}, lifetime());\n}\n\nvoid EffectPreview::setupItem() {\n\t_item->resizeGetHeight(st::windowMinWidth);\n\n\tconst auto icon = _item->effectIconGeometry();\n\tAssert(!icon.isEmpty());\n\n\tconst auto size = _inner.size();\n\tconst auto shift = _item->hasRightLayout()\n\t\t? (-size.width() / 3)\n\t\t: (size.width() / 3);\n\tconst auto position = QPoint(\n\t\tshift + icon.x() + (icon.width() - size.width()) / 2,\n\t\ticon.y() + (icon.height() - size.height()) / 2);\n\t_itemShift = _inner.topLeft() - position;\n\t_iconRect = icon.translated(_itemShift);\n}\n\nvoid EffectPreview::repaintBackground() {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto inner = _inner.size() + QSize(0, _bottom->height());\n\tauto bg = QImage(\n\t\tinner * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tbg.setDevicePixelRatio(ratio);\n\n\t{\n\t\tauto p = Painter(&bg);\n\t\tWindow::SectionWidget::PaintBackground(\n\t\t\tp,\n\t\t\t_theme.get(),\n\t\t\tQSize(inner.width(), inner.height() * 5),\n\t\t\tQRect(QPoint(), inner));\n\t\tp.fillRect(\n\t\t\tQRect(0, _inner.height(), _inner.width(), _bottom->height()),\n\t\t\tst::previewMarkRead.bgColor);\n\n\t\tp.translate(_itemShift - _inner.topLeft());\n\t\tauto rect = QRect(0, 0, st::windowMinWidth, _inner.height());\n\t\tauto context = _theme->preparePaintContext(\n\t\t\t_chatStyle.get(),\n\t\t\trect,\n\t\t\trect,\n\t\t\trect,\n\t\t\tfalse);\n\t\tcontext.outbg = _item->hasOutLayout();\n\t\t_item->draw(p, context);\n\t\tp.translate(_inner.topLeft() - _itemShift);\n\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\tauto roundRect = Ui::RoundRect(st::previewMenu.radius, st::menuBg);\n\t\troundRect.paint(p, QRect(QPoint(), inner), RectPart::AllCorners);\n\t}\n\n\t_bg.fill(Qt::transparent);\n\tauto p = QPainter(&_bg);\n\n\tconst auto shadowed = QRect(_inner.topLeft(), inner);\n\t_boxShadow.paint(p, shadowed, st::previewMenu.radius);\n\tp.drawImage(_inner.topLeft(), bg);\n}\n\nvoid EffectPreview::setupLottie() {\n\tconst auto reactions = &_show->session().data().reactions();\n\treactions->preloadEffectImageFor(_effectId);\n\n\tif (const auto document = _effect.aroundAnimation) {\n\t\t_media = document->createMediaView();\n\t} else {\n\t\t_media = _effect.selectAnimation->createMediaView();\n\t}\n\trpl::single(rpl::empty) | rpl::then(\n\t\t_show->session().downloaderTaskFinished()\n\t) | rpl::on_next([=] {\n\t\tif (checkLoaded()) {\n\t\t\t_readyCheckLifetime.destroy();\n\t\t\tcreateLottie();\n\t\t}\n\t}, _readyCheckLifetime);\n}\n\nvoid EffectPreview::createLottie() {\n\t_lottie = _show->session().emojiStickersPack().effectPlayer(\n\t\t_media->owner(),\n\t\t_bytes,\n\t\t_filepath,\n\t\tStickers::EffectType::MessageEffect);\n\tconst auto raw = _lottie.get();\n\traw->updates(\n\t) | rpl::on_next([=](Lottie::Update update) {\n\t\tv::match(update.data, [&](const Lottie::Information &information) {\n\t\t}, [&](const Lottie::DisplayFrameRequest &request) {\n\t\t\tthis->update();\n\t\t});\n\t}, raw->lifetime());\n}\n\nbool EffectPreview::canSend() const {\n\treturn !_effect.premium || _show->session().premium();\n}\n\nvoid EffectPreview::setupSend(Details details) {\n\tif (_send) {\n\t\t_send->setClickedCallback([=] {\n\t\t\t_actionWithEffect({}, details);\n\t\t});\n\t\tconst auto type = details.type;\n\t\tSetupMenuAndShortcuts(_send.get(), _show, [=] {\n\t\t\treturn Details{ .type = type };\n\t\t}, _actionWithEffect);\n\t} else {\n\t\t_premiumPromoLabel->entity()->setClickHandlerFilter([=](auto&&...) {\n\t\t\tconst auto window = _show->resolveWindow();\n\t\t\tif (window) {\n\t\t\t\tif (const auto onstack = _close) {\n\t\t\t\t\tonstack();\n\t\t\t\t}\n\t\t\t\tShowPremiumPreviewBox(window, PremiumFeature::Effects);\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t}\n}\n\nbool EffectPreview::checkIconBecameLoaded() {\n\tif (!_icon.isNull()) {\n\t\treturn false;\n\t}\n\tconst auto reactions = &_show->session().data().reactions();\n\t_icon = reactions->resolveEffectImageFor(_effect.id.custom());\n\tif (_icon.isNull()) {\n\t\treturn false;\n\t}\n\trepaintBackground();\n\treturn true;\n}\n\nbool EffectPreview::checkLoaded() {\n\tif (checkIconBecameLoaded()) {\n\t\tupdate();\n\t}\n\tif (_effect.aroundAnimation) {\n\t\t_bytes = _media->bytes();\n\t\t_filepath = _media->owner()->filepath();\n\t} else {\n\t\t_bytes = _media->videoThumbnailContent();\n\t}\n\treturn !_icon.isNull() && (!_bytes.isEmpty() || !_filepath.isEmpty());\n}\n\nvoid EffectPreview::toggle(bool shown) {\n\tif (!shown && _hiding) {\n\t\treturn;\n\t}\n\t_hiding = !shown;\n\tif (_bottomCache.isNull()) {\n\t\t_bottomCache = Ui::GrabWidget(_bottom);\n\t\t_bottom->hide();\n\t}\n\t_shownAnimation.start([=] {\n\t\tupdate();\n\t\tif (!_shownAnimation.animating()) {\n\t\t\tif (_hiding) {\n\t\t\t\tdelete this;\n\t\t\t} else {\n\t\t\t\t_bottomCache = QPixmap();\n\t\t\t\t_bottom->show();\n\t\t\t}\n\t\t}\n\t}, shown ? 0. : 1., shown ? 1. : 0., kToggleDuration, anim::easeOutCirc);\n\tshow();\n}\n\n} // namespace\n\nFn<void(Action, Details)> DefaultCallback(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tFn<void(Api::SendOptions)> send) {\n\tconst auto guard = base::make_weak(show->toastParent());\n\treturn [=](Action action, Details details) {\n\t\tif (action.type == ActionType::Send) {\n\t\t\tsend(action.options);\n\t\t\treturn;\n\t\t}\n\t\tauto box = HistoryView::PrepareScheduleBox(\n\t\t\tguard,\n\t\t\tshow,\n\t\t\tdetails,\n\t\t\tsend,\n\t\t\taction.options);\n\t\tconst auto weak = base::make_weak(box.data());\n\t\tshow->showBox(std::move(box));\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->setCloseByOutsideClick(false);\n\t\t}\n\t};\n}\n\nFillMenuResult AttachSendMenuEffect(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tDetails details,\n\t\tFn<void(Action, Details)> action,\n\t\tstd::optional<QPoint> desiredPositionOverride) {\n\tExpects(show != nullptr);\n\n\tusing namespace HistoryView::Reactions;\n\tconst auto effect = std::make_shared<base::weak_qptr<EffectPreview>>();\n\tconst auto position = desiredPositionOverride.value_or(QCursor::pos());\n\tconst auto selector = details.effectAllowed\n\t\t? AttachSelectorToMenu(\n\t\t\tmenu,\n\t\t\tposition,\n\t\t\tst::reactPanelEmojiPan,\n\t\t\tshow,\n\t\t\tLookupPossibleEffects(&show->session()),\n\t\t\t{ tr::lng_effect_add_title(tr::now) },\n\t\t\tnullptr, // iconFactory\n\t\t\t[=] { return (*effect) != nullptr; }) // paused\n\t\t: base::make_unexpected(AttachSelectorResult::Skipped);\n\tif (!selector) {\n\t\tif (selector.error() == AttachSelectorResult::Failed) {\n\t\t\treturn FillMenuResult::Failed;\n\t\t}\n\t\tmenu->prepareGeometryFor(position);\n\t\treturn FillMenuResult::Prepared;\n\t}\n\n\t(*selector)->chosen(\n\t) | rpl::on_next([=](ChosenReaction chosen) {\n\t\tconst auto &reactions = show->session().data().reactions();\n\t\tconst auto &effects = reactions.list(Data::Reactions::Type::Effects);\n\t\tconst auto i = ranges::find(effects, chosen.id, &Data::Reaction::id);\n\t\tif (i != end(effects)) {\n\t\t\tif (const auto strong = effect->get()) {\n\t\t\t\tstrong->hideAnimated();\n\t\t\t}\n\t\t\tconst auto weak = base::make_weak(menu);\n\t\t\tconst auto done = [=] {\n\t\t\t\tdelete effect->get();\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->hideMenu(true);\n\t\t\t\t}\n\t\t\t};\n\t\t\t*effect = Ui::CreateChild<EffectPreview>(\n\t\t\t\tmenu,\n\t\t\t\tshow,\n\t\t\t\tdetails,\n\t\t\t\tmenu->mapFromGlobal(chosen.globalGeometry.center()),\n\t\t\t\t*i,\n\t\t\t\taction,\n\t\t\t\tcrl::guard(menu, done));\n\t\t\t(*effect)->show();\n\t\t}\n\t}, menu->lifetime());\n\n\treturn FillMenuResult::Prepared;\n}\n\nFillMenuResult FillEditCommentPriceMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tDetails details,\n\t\tFn<void(Action, Details)> action,\n\t\tconst style::ComposeIcons *iconsOverride,\n\t\tstd::optional<QPoint> desiredPositionOverride) {\n\tExpects(show != nullptr);\n\n\tconst auto &icons = iconsOverride\n\t\t? *iconsOverride\n\t\t: st::defaultComposeIcons;\n\tmenu->addAction(tr::lng_video_stream_edit_stars(tr::now), [=] {\n\t\tshow->show(Calls::Group::MakeVideoStreamStarsBox({\n\t\t\t.show = show,\n\t\t\t.min = int(details.commentPriceMin.value_or(1)),\n\t\t\t.current = int(details.price.value_or(1)),\n\t\t\t.save = [=](int count) {\n\t\t\t\tauto copy = details;\n\t\t\t\tcopy.price = count;\n\t\t\t\taction({ {}, Action::Type::ChangePrice }, copy);\n\t\t\t},\n\t\t\t.name = details.commentStreamerName,\n\t\t\t//.preview = details.commentPreview,\n\t\t}));\n\t}, &icons.menuEditStars);\n\tif (details.price.value_or(0) > details.commentPriceMin.value_or(0)) {\n\t\tauto copy = details;\n\t\tcopy.price = details.commentPriceMin.value_or(0);\n\t\tmenu->addAction(tr::lng_video_stream_remove_stars(tr::now), [=] {\n\t\t\taction({ {}, Action::Type::ChangePrice }, copy);\n\t\t}, &icons.menuGifRemove);\n\t}\n\tconst auto position = desiredPositionOverride.value_or(QCursor::pos());\n\tmenu->prepareGeometryFor(position);\n\treturn FillMenuResult::Prepared;\n}\n\nFillMenuResult FillSendMenu(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tstd::shared_ptr<ChatHelpers::Show> maybeShow,\n\t\tDetails details,\n\t\tFn<void(Action, Details)> action,\n\t\tconst style::ComposeIcons *iconsOverride,\n\t\tstd::optional<QPoint> desiredPositionOverride) {\n\tconst auto type = details.type;\n\tconst auto sending = (type != Type::Disabled);\n\tconst auto empty = !sending\n\t\t&& (details.spoiler == SpoilerState::None)\n\t\t&& (details.caption == CaptionState::None)\n\t\t&& (details.photoQuality == PhotoQualityState::None)\n\t\t&& !details.price.has_value();\n\tif (empty || !action) {\n\t\treturn FillMenuResult::Skipped;\n\t} else if (type == Type::EditCommentPrice) {\n\t\treturn FillEditCommentPriceMenu(\n\t\t\tmenu,\n\t\t\tmaybeShow,\n\t\t\tdetails,\n\t\t\taction,\n\t\t\ticonsOverride,\n\t\t\tdesiredPositionOverride);\n\t}\n\tconst auto &icons = iconsOverride\n\t\t? *iconsOverride\n\t\t: st::defaultComposeIcons;\n\n\tif (sending && type != Type::Reminder) {\n\t\tmenu->addAction(\n\t\t\ttr::lng_send_silent_message(tr::now),\n\t\t\t[=] { action({ Api::SendOptions{ .silent = true } }, details); },\n\t\t\t&icons.menuMute);\n\t}\n\tif (sending && type != Type::SilentOnly) {\n\t\tmenu->addAction(\n\t\t\t((type == Type::Reminder)\n\t\t\t\t? tr::lng_reminder_message(tr::now)\n\t\t\t\t: tr::lng_schedule_message(tr::now)),\n\t\t\t[=] { action({ .type = ActionType::Schedule }, details); },\n\t\t\t&icons.menuSchedule);\n\t}\n\tif (sending && type == Type::ScheduledToUser) {\n\t\tmenu->addAction(\n\t\t\ttr::lng_scheduled_send_until_online(tr::now),\n\t\t\t[=] { action(\n\t\t\t\t{ Api::DefaultSendWhenOnlineOptions() },\n\t\t\t\tdetails); },\n\t\t\t&icons.menuWhenOnline);\n\t}\n\n\tif ((type != Type::Disabled)\n\t\t&& ((details.spoiler != SpoilerState::None)\n\t\t\t|| (details.caption != CaptionState::None)\n\t\t\t|| (details.photoQuality != PhotoQualityState::None)\n\t\t\t|| details.price.has_value())) {\n\t\tmenu->addSeparator(&st::expandedMenuSeparator);\n\t}\n\tif (details.photoQuality != PhotoQualityState::None) {\n\t\tconst auto high = (details.photoQuality == PhotoQualityState::High);\n\t\tMenu::AddCheckedAction(\n\t\t\tmenu,\n\t\t\ttr::lng_send_high_quality(tr::now),\n\t\t\t[=] { action({ .type = high\n\t\t\t\t? ActionType::PhotoQualityOff\n\t\t\t\t: ActionType::PhotoQualityOn\n\t\t\t}, details); },\n\t\t\t&icons.menuQualityHigh,\n\t\t\thigh);\n\t}\n\tif (details.spoiler != SpoilerState::None) {\n\t\tconst auto spoilered = (details.spoiler == SpoilerState::Enabled);\n\t\tMenu::AddCheckedAction(\n\t\t\tmenu,\n\t\t\ttr::lng_context_spoiler_effect(tr::now),\n\t\t\t[=] { action({ .type = spoilered\n\t\t\t\t? ActionType::SpoilerOff\n\t\t\t\t: ActionType::SpoilerOn\n\t\t\t}, details); },\n\t\t\t&icons.menuSpoiler,\n\t\t\tspoilered);\n\t}\n\tif (details.caption != CaptionState::None) {\n\t\tconst auto above = (details.caption == CaptionState::Above);\n\t\tmenu->addAction(\n\t\t\t(above\n\t\t\t\t? tr::lng_caption_move_down(tr::now)\n\t\t\t\t: tr::lng_caption_move_up(tr::now)),\n\t\t\t[=] { action({ .type = above\n\t\t\t\t? ActionType::CaptionDown\n\t\t\t\t: ActionType::CaptionUp\n\t\t\t}, details); },\n\t\t\tabove ? &icons.menuBelow : &icons.menuAbove);\n\t}\n\tif (details.price) {\n\t\tmenu->addAction(\n\t\t\t((*details.price > 0)\n\t\t\t\t? tr::lng_context_change_price(tr::now)\n\t\t\t\t: tr::lng_context_make_paid(tr::now)),\n\t\t\t[=] { action({ .type = ActionType::ChangePrice }, details); },\n\t\t\t&icons.menuPrice);\n\t}\n\n\tif (maybeShow) {\n\t\treturn AttachSendMenuEffect(\n\t\t\tmenu,\n\t\t\tmaybeShow,\n\t\t\tdetails,\n\t\t\taction,\n\t\t\tdesiredPositionOverride);\n\t}\n\tconst auto position = desiredPositionOverride.value_or(QCursor::pos());\n\tmenu->prepareGeometryFor(position);\n\treturn FillMenuResult::Prepared;\n}\n\nvoid SetupMenuAndShortcuts(\n\t\tnot_null<Ui::RpWidget*> button,\n\t\tstd::shared_ptr<ChatHelpers::Show> maybeShow,\n\t\tFn<Details()> details,\n\t\tFn<void(Action, Details)> action,\n\t\tconst style::PopupMenu *stOverride,\n\t\tconst style::ComposeIcons *iconsOverride) {\n\tconst auto menu = std::make_shared<base::unique_qptr<Ui::PopupMenu>>();\n\tconst auto showMenu = [=] {\n\t\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tbutton,\n\t\t\tstOverride ? *stOverride : st::popupMenuWithIcons);\n\t\tconst auto result = FillSendMenu(\n\t\t\t*menu,\n\t\t\tmaybeShow,\n\t\t\tdetails(),\n\t\t\taction,\n\t\t\ticonsOverride);\n\t\tif (result != FillMenuResult::Prepared) {\n\t\t\treturn false;\n\t\t}\n\t\t(*menu)->popupPrepared();\n\t\treturn true;\n\t};\n\tbase::install_event_filter(button, [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::ContextMenu && showMenu()) {\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\tShortcuts::Requests(\n\t) | rpl::filter([=] {\n\t\treturn button->isActiveWindow();\n\t}) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {\n\t\tusing Command = Shortcuts::Command;\n\n\t\tconst auto now = details().type;\n\t\tif (now == Type::Disabled) {\n\t\t\treturn;\n\t\t}\n\t\t((now != Type::Reminder)\n\t\t\t&& request->check(Command::SendSilentMessage)\n\t\t\t&& request->handle([=] {\n\t\t\t\taction({ Api::SendOptions{ .silent = true } }, details());\n\t\t\t\treturn true;\n\t\t\t}))\n\t\t||\n\t\t((now != Type::SilentOnly)\n\t\t\t&& request->check(Command::ScheduleMessage)\n\t\t\t&& request->handle([=] {\n\t\t\t\taction({ .type = ActionType::Schedule }, details());\n\t\t\t\treturn true;\n\t\t\t}))\n\t\t||\n\t\t(request->check(Command::JustSendMessage) && request->handle([=] {\n\t\t\tconst auto post = [&](QEvent::Type type) {\n\t\t\t\tQApplication::postEvent(\n\t\t\t\t\tbutton,\n\t\t\t\t\tnew QMouseEvent(\n\t\t\t\t\t\ttype,\n\t\t\t\t\t\tQPointF(0, 0),\n\t\t\t\t\t\tQt::LeftButton,\n\t\t\t\t\t\tQt::LeftButton,\n\t\t\t\t\t\tQt::NoModifier));\n\t\t\t};\n\t\t\tpost(QEvent::MouseButtonPress);\n\t\t\tpost(QEvent::MouseButtonRelease);\n\t\t\treturn true;\n\t\t}));\n\t}, button->lifetime());\n}\n\nvoid SetupReadAllMenu(\n\t\tnot_null<Ui::RpWidget*> button,\n\t\tFn<Data::Thread*()> currentThread,\n\t\tconst QString &text,\n\t\tFn<void(not_null<Data::Thread*>, Fn<void()>)> sendReadRequest) {\n\tstruct State {\n\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t\tbase::flat_set<base::weak_ptr<Data::Thread>> sentForEntries;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tconst auto showMenu = [=] {\n\t\tconst auto thread = base::make_weak(currentThread());\n\t\tif (!thread) {\n\t\t\treturn;\n\t\t}\n\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tbutton,\n\t\t\tst::popupMenuWithIcons);\n\t\tstate->menu->addAction(text, [=] {\n\t\t\tconst auto strong = thread.get();\n\t\t\tif (!strong || !state->sentForEntries.emplace(thread).second) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsendReadRequest(strong, [=] {\n\t\t\t\tstate->sentForEntries.remove(thread);\n\t\t\t});\n\t\t}, &st::menuIconMarkRead);\n\t\tstate->menu->popup(QCursor::pos());\n\t};\n\n\tbase::install_event_filter(button, [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::ContextMenu) {\n\t\t\tshowMenu();\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n}\n\nvoid SetupUnreadMentionsMenu(\n\t\tnot_null<Ui::RpWidget*> button,\n\t\tFn<Data::Thread*()> currentThread) {\n\tconst auto text = tr::lng_context_mark_read_mentions_all(tr::now);\n\tconst auto sendOne = [=](\n\t\t\tbase::weak_ptr<Data::Thread> weakThread,\n\t\t\tFn<void()> done,\n\t\t\tauto resend) -> void {\n\t\tconst auto thread = weakThread.get();\n\t\tif (!thread) {\n\t\t\tdone();\n\t\t\treturn;\n\t\t}\n\t\tconst auto peer = thread->peer();\n\t\tconst auto topic = thread->asTopic();\n\t\tconst auto rootId = topic ? topic->rootId() : 0;\n\t\tusing Flag = MTPmessages_ReadMentions::Flag;\n\t\tpeer->session().api().request(MTPmessages_ReadMentions(\n\t\t\tMTP_flags(rootId ? Flag::f_top_msg_id : Flag()),\n\t\t\tpeer->input(),\n\t\t\tMTP_int(rootId)\n\t\t)).done([=](const MTPmessages_AffectedHistory &result) {\n\t\t\tconst auto offset = peer->session().api().applyAffectedHistory(\n\t\t\t\tpeer,\n\t\t\t\tresult);\n\t\t\tif (offset > 0) {\n\t\t\t\tresend(weakThread, done, resend);\n\t\t\t} else {\n\t\t\t\tdone();\n\t\t\t\tpeer->owner().history(peer)->clearUnreadMentionsFor(rootId);\n\t\t\t}\n\t\t}).fail(done).send();\n\t};\n\tconst auto sendRequest = [=](\n\t\t\tnot_null<Data::Thread*> thread,\n\t\t\tFn<void()> done) {\n\t\tsendOne(base::make_weak(thread), std::move(done), sendOne);\n\t};\n\tSetupReadAllMenu(button, currentThread, text, sendRequest);\n}\n\nvoid SetupUnreadReactionsMenu(\n\t\tnot_null<Ui::RpWidget*> button,\n\t\tFn<Data::Thread*()> currentThread) {\n\tconst auto text = tr::lng_context_mark_read_reactions_all(tr::now);\n\tconst auto sendOne = [=](\n\t\t\tbase::weak_ptr<Data::Thread> weakThread,\n\t\t\tFn<void()> done,\n\t\t\tauto resend) -> void {\n\t\tconst auto thread = weakThread.get();\n\t\tif (!thread) {\n\t\t\tdone();\n\t\t\treturn;\n\t\t}\n\t\tconst auto topic = thread->asTopic();\n\t\tconst auto sublist = thread->asSublist();\n\t\tconst auto peer = thread->peer();\n\t\tconst auto rootId = topic ? topic->rootId() : 0;\n\t\tusing Flag = MTPmessages_ReadReactions::Flag;\n\t\tpeer->session().api().request(MTPmessages_ReadReactions(\n\t\t\tMTP_flags((rootId ? Flag::f_top_msg_id : Flag(0))\n\t\t\t\t| (sublist ? Flag::f_saved_peer_id : Flag(0))),\n\t\t\tpeer->input(),\n\t\t\tMTP_int(rootId),\n\t\t\tsublist ? sublist->sublistPeer()->input() : MTPInputPeer()\n\t\t)).done([=](const MTPmessages_AffectedHistory &result) {\n\t\t\tconst auto offset = peer->session().api().applyAffectedHistory(\n\t\t\t\tpeer,\n\t\t\t\tresult);\n\t\t\tif (offset > 0) {\n\t\t\t\tresend(weakThread, done, resend);\n\t\t\t} else {\n\t\t\t\tdone();\n\t\t\t\tpeer->owner().history(peer)->clearUnreadReactionsFor(\n\t\t\t\t\trootId,\n\t\t\t\t\tsublist);\n\t\t\t}\n\t\t}).fail(done).send();\n\t};\n\tconst auto sendRequest = [=](\n\t\t\tnot_null<Data::Thread*> thread,\n\t\t\tFn<void()> done) {\n\t\tsendOne(base::make_weak(thread), std::move(done), sendOne);\n\t};\n\tSetupReadAllMenu(button, currentThread, text, sendRequest);\n}\n\nvoid SetupUnreadPollVotesMenu(\n\t\tnot_null<Ui::RpWidget*> button,\n\t\tFn<Data::Thread*()> currentThread) {\n\tconst auto text = tr::lng_context_mark_read_poll_votes_all(tr::now);\n\tconst auto sendOne = [=](\n\t\t\tbase::weak_ptr<Data::Thread> weakThread,\n\t\t\tFn<void()> done,\n\t\t\tauto resend) -> void {\n\t\tconst auto thread = weakThread.get();\n\t\tif (!thread) {\n\t\t\tdone();\n\t\t\treturn;\n\t\t}\n\t\tconst auto topic = thread->asTopic();\n\t\tconst auto peer = thread->peer();\n\t\tconst auto rootId = topic ? topic->rootId() : 0;\n\t\tusing Flag = MTPmessages_ReadPollVotes::Flag;\n\t\tpeer->session().api().request(MTPmessages_ReadPollVotes(\n\t\t\tMTP_flags(rootId ? Flag::f_top_msg_id : Flag(0)),\n\t\t\tpeer->input(),\n\t\t\tMTP_int(rootId)\n\t\t)).done([=](const MTPmessages_AffectedHistory &result) {\n\t\t\tconst auto offset = peer->session().api().applyAffectedHistory(\n\t\t\t\tpeer,\n\t\t\t\tresult);\n\t\t\tif (offset > 0) {\n\t\t\t\tresend(weakThread, done, resend);\n\t\t\t} else {\n\t\t\t\tdone();\n\t\t\t\tpeer->owner().history(peer)->clearUnreadPollVotesFor(\n\t\t\t\t\trootId);\n\t\t\t}\n\t\t}).fail(done).send();\n\t};\n\tconst auto sendRequest = [=](\n\t\t\tnot_null<Data::Thread*> thread,\n\t\t\tFn<void()> done) {\n\t\tsendOne(base::make_weak(thread), std::move(done), sendOne);\n\t};\n\tSetupReadAllMenu(button, currentThread, text, sendRequest);\n}\n\n} // namespace SendMenu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_send.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_common.h\"\n\nnamespace style {\nstruct ComposeIcons;\nstruct PopupMenu;\n} // namespace style\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Ui {\nclass PopupMenu;\nclass RpWidget;\nclass Show;\n} // namespace Ui\n\nnamespace Data {\nclass Thread;\n} // namespace Data\n\nnamespace SendMenu {\n\nenum class Type : uchar {\n\tDisabled,\n\tSilentOnly,\n\tScheduled,\n\tScheduledToUser, // For \"Send when online\".\n\tReminder,\n\tEditCommentPrice,\n};\n\nenum class SpoilerState : uchar {\n\tNone,\n\tEnabled,\n\tPossible,\n};\n\nenum class CaptionState : uchar {\n\tNone,\n\tBelow,\n\tAbove,\n};\n\nenum class PhotoQualityState : uchar {\n\tNone,\n\tStandard,\n\tHigh,\n};\n\nstruct Details {\n\tType type = Type::Disabled;\n\tSpoilerState spoiler = SpoilerState::None;\n\tCaptionState caption = CaptionState::None;\n\tPhotoQualityState photoQuality = PhotoQualityState::None;\n\tTextWithTags commentPreview;\n\tQString commentStreamerName;\n\tstd::optional<uint64> price;\n\tstd::optional<uint64> commentPriceMin;\n\tbool effectAllowed = false;\n};\n\nenum class FillMenuResult : uchar {\n\tPrepared,\n\tSkipped,\n\tFailed,\n};\n\nenum class ActionType : uchar {\n\tSend,\n\tSchedule,\n\tSpoilerOn,\n\tSpoilerOff,\n\tCaptionUp,\n\tCaptionDown,\n\tPhotoQualityOn,\n\tPhotoQualityOff,\n\tChangePrice,\n};\nstruct Action {\n\tusing Type = ActionType;\n\n\tApi::SendOptions options;\n\tType type = Type::Send;\n};\n[[nodiscard]] Fn<void(Action, Details)> DefaultCallback(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tFn<void(Api::SendOptions)> send);\n\nFillMenuResult FillSendMenu(\n\tnot_null<Ui::PopupMenu*> menu,\n\tstd::shared_ptr<ChatHelpers::Show> maybeShow,\n\tDetails details,\n\tFn<void(Action, Details)> action,\n\tconst style::ComposeIcons *iconsOverride = nullptr,\n\tstd::optional<QPoint> desiredPositionOverride = std::nullopt);\n\nFillMenuResult AttachSendMenuEffect(\n\tnot_null<Ui::PopupMenu*> menu,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tDetails details,\n\tFn<void(Action, Details)> action,\n\tstd::optional<QPoint> desiredPositionOverride = std::nullopt);\n\nvoid SetupMenuAndShortcuts(\n\tnot_null<Ui::RpWidget*> button,\n\tstd::shared_ptr<ChatHelpers::Show> maybeShow,\n\tFn<Details()> details,\n\tFn<void(Action, Details)> action,\n\tconst style::PopupMenu *stOverride = nullptr,\n\tconst style::ComposeIcons *iconsOverride = nullptr);\n\nvoid SetupUnreadMentionsMenu(\n\tnot_null<Ui::RpWidget*> button,\n\tFn<Data::Thread*()> currentThread);\n\nvoid SetupUnreadReactionsMenu(\n\tnot_null<Ui::RpWidget*> button,\n\tFn<Data::Thread*()> currentThread);\n\nvoid SetupUnreadPollVotesMenu(\n\tnot_null<Ui::RpWidget*> button,\n\tFn<Data::Thread*()> currentThread);\n\n} // namespace SendMenu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_sponsored.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"menu/menu_sponsored.h\"\n\n#include \"boxes/premium_preview_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"core/ui_integration.h\" // TextContext\n#include \"data/components/sponsored_messages.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/report_box_graphics.h\" // AddReportOptionButton.\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/menu/menu_multiline_action.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"styles/style_channel_earn.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_media_view.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Menu {\nnamespace {\n\n[[nodiscard]] SponsoredPhrases PhrasesForMessage(FullMsgId fullId) {\n\treturn peerIsChannel(fullId.peer)\n\t\t? SponsoredPhrases::Channel\n\t\t: SponsoredPhrases::Bot;\n}\n\nvoid AboutBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tSponsoredPhrases phrases,\n\t\tconst Data::SponsoredMessages::Details &details,\n\t\tData::SponsoredReportAction report) {\n\tconstexpr auto kUrl = \"https://promote.telegram.org\"_cs;\n\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setNoContentMargin(true);\n\n\tconst auto isChannel = (phrases == SponsoredPhrases::Channel);\n\tconst auto isSearch = (phrases == SponsoredPhrases::Search);\n\tconst auto session = &show->session();\n\n\tconst auto content = box->verticalLayout().get();\n\tconst auto levels = Data::LevelLimits(session)\n\t\t.channelRestrictSponsoredLevelMin();\n\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\t{\n\t\tconst auto &icon = st::sponsoredAboutTitleIcon;\n\t\tconst auto rect = Rect(icon.size() * 1.4);\n\t\tauto owned = object_ptr<Ui::RpWidget>(content);\n\t\towned->resize(rect.size());\n\t\towned->setNaturalWidth(rect.width());\n\t\tconst auto widget = box->addRow(std::move(owned), style::al_top);\n\t\twidget->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = Painter(widget);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(st::activeButtonBg);\n\t\t\tp.drawEllipse(rect);\n\t\t\ticon.paintInCenter(p, rect);\n\t\t}, widget->lifetime());\n\t}\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_sponsored_menu_revenued_about(),\n\t\t\tst::boxTitle),\n\t\tstyle::al_top);\n\tUi::AddSkip(content);\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_sponsored_revenued_subtitle(),\n\t\t\tst::channelEarnLearnDescription),\n\t\tstyle::al_top);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\t{\n\t\tconst auto padding = QMargins(\n\t\t\tst::settingsButton.padding.left(),\n\t\t\tst::boxRowPadding.top(),\n\t\t\tst::boxRowPadding.right(),\n\t\t\tst::boxRowPadding.bottom());\n\t\tconst auto addEntry = [&](\n\t\t\t\trpl::producer<QString> title,\n\t\t\t\trpl::producer<TextWithEntities> about,\n\t\t\t\tconst style::icon &icon) {\n\t\t\tconst auto top = content->add(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tcontent,\n\t\t\t\t\tstd::move(title),\n\t\t\t\t\tst::channelEarnSemiboldLabel),\n\t\t\t\tpadding);\n\t\t\tUi::AddSkip(content, st::channelEarnHistoryThreeSkip);\n\t\t\tconst auto label = content->add(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tcontent,\n\t\t\t\t\tstd::move(about),\n\t\t\t\t\tst::channelEarnHistoryRecipientLabel),\n\t\t\t\tpadding);\n\t\t\tconst auto left = Ui::CreateChild<Ui::RpWidget>(\n\t\t\t\tbox->verticalLayout().get());\n\t\t\tleft->paintRequest(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tauto p = Painter(left);\n\t\t\t\ticon.paint(p, 0, 0, left->width());\n\t\t\t}, left->lifetime());\n\t\t\tleft->resize(icon.size());\n\t\t\ttop->geometryValue(\n\t\t\t) | rpl::on_next([=](const QRect &g) {\n\t\t\t\tleft->moveToLeft(\n\t\t\t\t\t(g.left() - left->width()) / 2,\n\t\t\t\t\tg.top() + st::channelEarnHistoryThreeSkip);\n\t\t\t}, left->lifetime());\n\t\t\treturn label;\n\t\t};\n\t\taddEntry(\n\t\t\ttr::lng_sponsored_revenued_info1_title(),\n\t\t\t(isChannel\n\t\t\t\t? tr::lng_sponsored_revenued_info1_description\n\t\t\t\t: isSearch\n\t\t\t\t? tr::lng_sponsored_revenued_info1_search_description\n\t\t\t\t: tr::lng_sponsored_revenued_info1_bot_description)(\n\t\t\t\t\ttr::rich),\n\t\t\tst::sponsoredAboutPrivacyIcon);\n\t\tif (!isSearch) {\n\t\t\tUi::AddSkip(content);\n\t\t\tUi::AddSkip(content);\n\t\t\taddEntry(\n\t\t\t\t(isChannel\n\t\t\t\t\t? tr::lng_sponsored_revenued_info2_title\n\t\t\t\t\t: tr::lng_sponsored_revenued_info2_bot_title)(),\n\t\t\t\t(isChannel\n\t\t\t\t\t? tr::lng_sponsored_revenued_info2_description\n\t\t\t\t\t: tr::lng_sponsored_revenued_info2_bot_description)(\n\t\t\t\t\t\ttr::rich),\n\t\t\t\tst::sponsoredAboutSplitIcon);\n\t\t}\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\t\tauto link = tr::lng_settings_privacy_premium_link(\n\t\t) | rpl::map([](QString t) {\n\t\t\treturn tr::link(std::move(t), u\"internal:\"_q);\n\t\t});\n\t\taddEntry(\n\t\t\ttr::lng_sponsored_revenued_info3_title(),\n\t\t\t(isChannel\n\t\t\t\t? tr::lng_sponsored_revenued_info3_description(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(float64(levels)),\n\t\t\t\t\tlt_link,\n\t\t\t\t\tstd::move(link),\n\t\t\t\t\ttr::rich)\n\t\t\t\t: isSearch\n\t\t\t\t? tr::lng_sponsored_revenued_info3_search_description(\n\t\t\t\t\tlt_link,\n\t\t\t\t\ttr::lng_sponsored_revenued_info3_search_link(\n\t\t\t\t\t\tlt_arrow,\n\t\t\t\t\t\trpl::single(\n\t\t\t\t\t\t\tUi::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\t\t\t\ttr::marked\n\t\t\t\t\t) | rpl::map([](TextWithEntities &&link) {\n\t\t\t\t\t\treturn Ui::Text::Wrapped(\n\t\t\t\t\t\t\tstd::move(link),\n\t\t\t\t\t\t\tEntityType::CustomUrl,\n\t\t\t\t\t\t\tu\"internal:\"_q);\n\t\t\t\t\t}),\n\t\t\t\t\ttr::rich)\n\t\t\t\t: tr::lng_sponsored_revenued_info3_bot_description(\n\t\t\t\t\tlt_link,\n\t\t\t\t\tstd::move(link),\n\t\t\t\t\ttr::rich)),\n\t\t\tst::sponsoredAboutRemoveIcon)->setClickHandlerFilter([=](\n\t\t\t\t\tconst auto &...) {\n\t\t\t\tShowPremiumPreviewBox(show, PremiumFeature::NoAds);\n\t\t\t\treturn true;\n\t\t\t});\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\t}\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\t{\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_sponsored_revenued_footer_title(),\n\t\t\t\tst::boxTitle),\n\t\t\tstyle::al_top);\n\t}\n\tUi::AddSkip(content);\n\t{\n\t\tconst auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji);\n\t\tconst auto available = box->width()\n\t\t\t- rect::m::sum::h(st::boxRowPadding);\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\t(isChannel\n\t\t\t\t\t? tr::lng_sponsored_revenued_footer_description\n\t\t\t\t\t: isSearch\n\t\t\t\t\t? tr::lng_sponsored_revenued_footer_search_description\n\t\t\t\t\t: tr::lng_sponsored_revenued_footer_bot_description)(\n\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\ttr::lng_channel_earn_about_link(\n\t\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\t\trpl::single(arrow),\n\t\t\t\t\t\t\ttr::rich\n\t\t\t\t\t\t) | rpl::map([=](TextWithEntities t) {\n\t\t\t\t\t\t\treturn tr::link(std::move(t), kUrl.utf16());\n\t\t\t\t\t\t}),\n\t\t\t\t\t\ttr::rich),\n\t\t\t\tst::channelEarnLearnDescription))->resizeToWidth(available);\n\t}\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\t{\n\t\tconst auto &st = st::premiumPreviewDoubledLimitsBox;\n\t\tbox->setStyle(st);\n\t\tauto button = object_ptr<Ui::RoundButton>(\n\t\t\tbox,\n\t\t\ttr::lng_box_ok(),\n\t\t\tst::defaultActiveButton);\n\t\tbutton->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\t\tbutton->resizeToWidth(box->width()\n\t\t\t- st.buttonPadding.left()\n\t\t\t- st.buttonPadding.left());\n\t\tbutton->setClickedCallback([=] { box->closeBox(); });\n\t\tbox->addButton(std::move(button));\n\t}\n\n\tif (!isChannel) {\n\t\tconst auto top = Ui::CreateChild<Ui::IconButton>(\n\t\t\tbox,\n\t\t\tst::infoTopBarMenu);\n\t\tbox->widthValue(\n\t\t) | rpl::on_next([=](int width) {\n\t\t\ttop->raise();\n\t\t\ttop->moveToLeft(\n\t\t\t\twidth - top->width() - st::defaultScrollArea.width,\n\t\t\t\t0);\n\t\t}, top->lifetime());\n\t\tusing MenuPtr = base::unique_qptr<Ui::PopupMenu>;\n\t\tconst auto menu = top->lifetime().make_state<MenuPtr>();\n\t\ttop->setClickedCallback([=] {\n\t\t\t*menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\tbox->window(),\n\t\t\t\tst::popupMenuWithIcons);\n\t\t\tconst auto raw = menu->get();\n\t\t\traw->animatePhaseValue(\n\t\t\t) | rpl::on_next([=](Ui::PopupMenu::AnimatePhase phase) {\n\t\t\t\ttop->setForceRippled(false\n\t\t\t\t\t|| phase == Ui::PopupMenu::AnimatePhase::Shown\n\t\t\t\t\t|| phase == Ui::PopupMenu::AnimatePhase::StartShow);\n\t\t\t}, top->lifetime());\n\t\t\traw->setDestroyedCallback([=] {\n\t\t\t\ttop->setForceRippled(false);\n\t\t\t});\n\t\t\tFillSponsored(\n\t\t\t\tUi::Menu::CreateAddActionCallback(menu->get()),\n\t\t\t\tshow,\n\t\t\t\tphrases,\n\t\t\t\tdetails,\n\t\t\t\treport,\n\t\t\t\t{ .skipAbout = true });\n\t\t\tconst auto global = top->mapToGlobal(\n\t\t\t\tQPoint(top->width() / 4 * 3, top->height() / 2));\n\t\t\traw->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);\n\t\t\traw->popup(\n\t\t\t\tQPoint(\n\t\t\t\t\tglobal.x(),\n\t\t\t\t\tstd::max(global.y(), QCursor::pos().y())));\n\t\t\treturn true;\n\t\t});\n\t}\n}\n\nvoid ShowReportSponsoredBox(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tData::SponsoredReportAction report) {\n\tconst auto guideLink = tr::link(\n\t\ttr::lng_report_sponsored_reported_link(tr::now),\n\t\tu\"https://promote.telegram.org/guidelines\"_q);\n\n\tauto performRequest = [=](\n\t\t\tconst auto &repeatRequest,\n\t\t\tData::SponsoredReportResult::Id id) -> void {\n\t\treport.callback(id, [=](const Data::SponsoredReportResult &result) {\n\t\t\tif (!result.error.isEmpty()) {\n\t\t\t\tshow->showToast(result.error);\n\t\t\t}\n\t\t\tif (!result.options.empty()) {\n\t\t\t\tshow->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\t\tbox->setTitle(result.title);\n\n\t\t\t\t\tfor (const auto &option : result.options) {\n\t\t\t\t\t\tconst auto button = Ui::AddReportOptionButton(\n\t\t\t\t\t\t\tbox->verticalLayout(),\n\t\t\t\t\t\t\toption.text,\n\t\t\t\t\t\t\tnullptr);\n\t\t\t\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\t\t\t\trepeatRequest(repeatRequest, option.id);\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (!id.isNull()) {\n\t\t\t\t\t\tbox->addLeftButton(\n\t\t\t\t\t\t\ttr::lng_create_group_back(),\n\t\t\t\t\t\t\t[=] { box->closeBox(); });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst auto container = box->verticalLayout();\n\t\t\t\t\t\tUi::AddSkip(container);\n\t\t\t\t\t\tcontainer->add(object_ptr<Ui::DividerLabel>(\n\t\t\t\t\t\t\tcontainer,\n\t\t\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\t\t\tcontainer,\n\t\t\t\t\t\t\t\ttr::lng_report_sponsored_reported_learn(\n\t\t\t\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\t\t\t\trpl::single(guideLink),\n\t\t\t\t\t\t\t\t\ttr::marked),\n\t\t\t\t\t\t\t\tst::boxDividerLabel),\n\t\t\t\t\t\t\tst::defaultBoxDividerLabelPadding));\n\t\t\t\t\t}\n\t\t\t\t\tbox->addButton(\n\t\t\t\t\t\ttr::lng_close(),\n\t\t\t\t\t\t[=] { show->hideLayer(); });\n\t\t\t\t}));\n\t\t\t} else {\n\t\t\t\tconstexpr auto kToastDuration = crl::time(4000);\n\t\t\t\tswitch (result.result) {\n\t\t\t\tcase Data::SponsoredReportResult::FinalStep::Hidden: {\n\t\t\t\t\tshow->showToast(\n\t\t\t\t\t\ttr::lng_report_sponsored_hidden(tr::now),\n\t\t\t\t\t\tkToastDuration);\n\t\t\t\t} break;\n\t\t\t\tcase Data::SponsoredReportResult::FinalStep::Reported: {\n\t\t\t\t\tauto text = tr::lng_report_sponsored_reported(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\tguideLink,\n\t\t\t\t\t\ttr::marked);\n\t\t\t\t\tshow->showToast({\n\t\t\t\t\t\t.text = std::move(text),\n\t\t\t\t\t\t.duration = kToastDuration,\n\t\t\t\t\t});\n\t\t\t\t} break;\n\t\t\t\tcase Data::SponsoredReportResult::FinalStep::Premium: {\n\t\t\t\t\tShowPremiumPreviewBox(show, PremiumFeature::NoAds);\n\t\t\t\t} break;\n\t\t\t\t}\n\t\t\t\tshow->hideLayer();\n\t\t\t}\n\t\t});\n\t};\n\tperformRequest(performRequest, Data::SponsoredReportResult::Id());\n}\n\n} // namespace\n\nvoid FillSponsored(\n\t\tconst Ui::Menu::MenuCallback &addAction,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tSponsoredPhrases phrases,\n\t\tconst Data::SponsoredMessages::Details &details,\n\t\tData::SponsoredReportAction report,\n\t\tSponsoredMenuSettings settings) {\n\tconst auto session = &show->session();\n\tconst auto &info = details.info;\n\tconst auto dark = settings.dark;\n\n\tif (!settings.skipInfo && !info.empty()) {\n\t\tauto fillSubmenu = [&](not_null<Ui::PopupMenu*> menu) {\n\t\t\tconst auto allText = ranges::accumulate(\n\t\t\t\tinfo,\n\t\t\t\tTextWithEntities(),\n\t\t\t\t[](TextWithEntities a, TextWithEntities b) {\n\t\t\t\t\treturn a.text.isEmpty() ? b : a.append('\\n').append(b);\n\t\t\t\t}).text;\n\t\t\tconst auto callback = [=] {\n\t\t\t\tTextUtilities::SetClipboardText({ allText });\n\t\t\t\tshow->showToast(tr::lng_text_copied(tr::now));\n\t\t\t};\n\t\t\tfor (const auto &i : info) {\n\t\t\t\tauto item = base::make_unique_q<Ui::Menu::MultilineAction>(\n\t\t\t\t\tmenu->menu(),\n\t\t\t\t\tdark ? st::storiesMenu : st::defaultMenu,\n\t\t\t\t\t(dark\n\t\t\t\t\t\t? st::historySponsorInfoItemDark\n\t\t\t\t\t\t: st::historySponsorInfoItem),\n\t\t\t\t\tst::historyHasCustomEmojiPosition,\n\t\t\t\t\tbase::duplicate(i));\n\t\t\t\titem->clicks(\n\t\t\t\t) | rpl::on_next(callback, menu->lifetime());\n\t\t\t\tmenu->addAction(std::move(item));\n\t\t\t\tif (i != details.info.back()) {\n\t\t\t\t\tmenu->addSeparator();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\taddAction({\n\t\t\t.text = tr::lng_sponsored_info_menu(tr::now),\n\t\t\t.handler = nullptr,\n\t\t\t.icon = (dark\n\t\t\t\t? &st::mediaMenuIconChannel\n\t\t\t\t: &st::menuIconChannel),\n\t\t\t.fillSubmenu = std::move(fillSubmenu),\n\t\t});\n\t\taddAction({\n\t\t\t.separatorSt = (dark\n\t\t\t\t? &st::mediaviewMenuSeparator\n\t\t\t\t: &st::expandedMenuSeparator),\n\t\t\t.isSeparator = true,\n\t\t});\n\t}\n\tif (details.canReport) {\n\t\tif (!settings.skipAbout) {\n\t\t\taddAction(tr::lng_sponsored_menu_revenued_about(tr::now), [=] {\n\t\t\t\tshow->show(Box(AboutBox, show, phrases, details, report));\n\t\t\t}, (dark ? &st::mediaMenuIconInfo : &st::menuIconInfo));\n\t\t}\n\n\t\taddAction(tr::lng_sponsored_menu_revenued_report(tr::now), [=] {\n\t\t\tShowReportSponsoredBox(show, report);\n\t\t}, (dark ? &st::mediaMenuIconBlock : &st::menuIconBlock));\n\n\t\taddAction({\n\t\t\t.separatorSt = (dark\n\t\t\t\t? &st::mediaviewMenuSeparator\n\t\t\t\t: &st::expandedMenuSeparator),\n\t\t\t.isSeparator = true,\n\t\t});\n\t}\n\taddAction(tr::lng_sponsored_hide_ads(tr::now), [=] {\n\t\tif (session->premium()) {\n\t\t\tusing Result = Data::SponsoredReportResult;\n\t\t\treport.callback(Result::Id(\"-1\"), [](const auto &) {});\n\t\t} else {\n\t\t\tShowPremiumPreviewBox(show, PremiumFeature::NoAds);\n\t\t}\n\t}, (dark ? &st::mediaMenuIconCancel : &st::menuIconCancel));\n}\n\nvoid FillSponsored(\n\t\tconst Ui::Menu::MenuCallback &addAction,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst FullMsgId &fullId,\n\t\tSponsoredMenuSettings settings) {\n\tconst auto session = &show->session();\n\tFillSponsored(\n\t\taddAction,\n\t\tshow,\n\t\tPhrasesForMessage(fullId),\n\t\tsession->sponsoredMessages().lookupDetails(fullId),\n\t\tsession->sponsoredMessages().createReportCallback(fullId),\n\t\tsettings);\n}\n\nvoid ShowSponsored(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst FullMsgId &fullId) {\n\tconst auto menu = Ui::CreateChild<Ui::PopupMenu>(\n\t\tparent.get(),\n\t\tst::popupMenuWithIcons);\n\n\tFillSponsored(\n\t\tUi::Menu::CreateAddActionCallback(menu),\n\t\tshow,\n\t\tfullId);\n\n\tmenu->popup(QCursor::pos());\n}\n\nvoid ShowSponsoredAbout(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst FullMsgId &fullId) {\n\tconst auto session = &show->session();\n\tshow->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tAboutBox(\n\t\t\tbox,\n\t\t\tshow,\n\t\t\tPhrasesForMessage(fullId),\n\t\t\tsession->sponsoredMessages().lookupDetails(fullId),\n\t\t\tsession->sponsoredMessages().createReportCallback(fullId));\n\t}));\n}\n\n} // namespace Menu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_sponsored.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nstruct SponsoredMessageDetails;\nstruct SponsoredReportAction;\n} // namespace Data\n\nnamespace Ui {\nclass RpWidget;\nnamespace Menu {\nstruct MenuCallback;\n} // namespace Menu\n} // namespace Ui\n\nclass HistoryItem;\n\nnamespace Menu {\n\nenum class SponsoredPhrases {\n\tChannel,\n\tBot,\n\tSearch,\n};\n\nstruct SponsoredMenuSettings {\n\tbool dark = false;\n\tbool skipAbout = false;\n\tbool skipInfo = false;\n};\n\nvoid FillSponsored(\n\tconst Ui::Menu::MenuCallback &addAction,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tSponsoredPhrases phrases,\n\tconst Data::SponsoredMessageDetails &details,\n\tData::SponsoredReportAction report,\n\tSponsoredMenuSettings settings = {});\n\nvoid FillSponsored(\n\tconst Ui::Menu::MenuCallback &addAction,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst FullMsgId &fullId,\n\tSponsoredMenuSettings settings = {});\n\nvoid ShowSponsored(\n\tnot_null<Ui::RpWidget*> parent,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst FullMsgId &fullId);\n\nvoid ShowSponsoredAbout(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst FullMsgId &fullId);\n\n} // namespace Menu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_timecode_action.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"menu/menu_timecode_action.h\"\n\n#include \"base/unique_qptr.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/menu/menu_common.h\"\n#include \"ui/widgets/popup_menu.h\"\n\n#include \"styles/style_chat.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace {\n\nclass TimecodeAction final : public Ui::Menu::Action {\npublic:\n\tTimecodeAction(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst style::Menu &st,\n\t\tnot_null<QAction*> action,\n\t\tconst style::icon *icon,\n\t\tconst QString &timecode,\n\t\trpl::producer<QString> updates);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tconst style::icon *_replyIcon = nullptr;\n\tQString _timecode;\n\n};\n\nTimecodeAction::TimecodeAction(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst style::Menu &st,\n\tnot_null<QAction*> action,\n\tconst style::icon *icon,\n\tconst QString &timecode,\n\trpl::producer<QString> updates)\n: Ui::Menu::Action(parent, st, action, nullptr, nullptr)\n, _replyIcon(icon)\n, _timecode(timecode) {\n\tstd::move(updates) | rpl::on_next([=](const QString &value) {\n\t\t_timecode = value;\n\t\tupdate();\n\t}, lifetime());\n}\n\nvoid TimecodeAction::paintEvent(QPaintEvent *e) {\n\tUi::Menu::Action::paintEvent(e);\n\n\tauto p = Painter(this);\n\tif (_replyIcon) {\n\t\tconst auto pos = st().itemIconPosition;\n\t\tconst auto iconW = _replyIcon->width();\n\t\tconstexpr auto kScale = 0.8;\n\t\tconst auto dx = pos.x() + (iconW * (1. - kScale)) / 2.;\n\t\tconst auto dy = pos.y();\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.translate(dx, dy);\n\t\tp.scale(kScale, kScale);\n\t\t_replyIcon->paint(p, 0, 0, width() / kScale);\n\t\tp.resetTransform();\n\t}\n\tconst auto &font = st::menuTimecodeFont;\n\tp.setFont(font);\n\tp.setPen(st::menuIconColor);\n\tconst auto iconRight = st().itemIconPosition.x()\n\t\t+ st::menuIconReply.width();\n\tconst auto textWidth = font->width(_timecode);\n\tconst auto x = iconRight - textWidth;\n\tconst auto y = st().itemIconPosition.y()\n\t\t+ st::menuIconReply.height()\n\t\t- font->descent;\n\tp.drawText(x, y, _timecode);\n}\n\n} // namespace\n\nnamespace Menu {\n\nvoid InsertTextAtCursor(\n\t\tnot_null<Ui::InputField*> field,\n\t\tconst QString &text) {\n\tauto cursor = field->textCursor();\n\tconst auto pos = cursor.position();\n\tconst auto doc = field->getTextWithTags().text;\n\tconst auto needSpaceBefore = pos > 0\n\t\t&& !doc[pos - 1].isSpace();\n\tauto insert = QString();\n\tif (needSpaceBefore) {\n\t\tinsert += ' ';\n\t}\n\tinsert += text + ' ';\n\tcursor.insertText(insert);\n\tfield->setTextCursor(cursor);\n}\n\nnot_null<QAction*> AddTimecodeAction(\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst QString &timecode,\n\t\trpl::producer<QString> updates,\n\t\tFn<void()> callback) {\n\tauto item = base::make_unique_q<TimecodeAction>(\n\t\tmenu->menu(),\n\t\tmenu->st().menu,\n\t\tUi::Menu::CreateAction(\n\t\t\tmenu->menu().get(),\n\t\t\ttr::lng_context_reply_with_timecode(tr::now),\n\t\t\tstd::move(callback)),\n\t\t&st::menuIconReply,\n\t\ttimecode,\n\t\tstd::move(updates));\n\treturn menu->addAction(std::move(item));\n}\n\n} // namespace Menu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_timecode_action.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/basic_types.h\"\n\nclass QAction;\n\nnamespace Ui {\nclass InputField;\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Menu {\n\nvoid InsertTextAtCursor(\n\tnot_null<Ui::InputField*> field,\n\tconst QString &text);\n\nnot_null<QAction*> AddTimecodeAction(\n\tnot_null<Ui::PopupMenu*> menu,\n\tconst QString &timecode,\n\trpl::producer<QString> updates,\n\tFn<void()> callback);\n\n} // namespace Menu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_ttl.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"menu/menu_ttl.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/time_picker_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/labels.h\"\n#if 0\n#include \"ui/boxes/choose_time.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"styles/style_dialogs.h\" // dialogsScamFont\n#include \"styles/style_menu_icons.h\"\n#endif\n#include \"styles/style_layers.h\"\n\nnamespace TTLMenu {\n\nnamespace {\n\n#if 0\nconstexpr auto kTTLDurHours1 = crl::time(1);\nconstexpr auto kTTLDurSeconds1 = kTTLDurHours1 * 3600;\nconstexpr auto kTTLDurHours2 = crl::time(24);\nconstexpr auto kTTLDurSeconds2 = kTTLDurHours2 * 3600;\nconstexpr auto kTTLDurHours3 = crl::time(24 * 7);\nconstexpr auto kTTLDurSeconds3 = kTTLDurHours3 * 3600;\nconstexpr auto kTTLDurHours4 = crl::time(24 * 30);\nconstexpr auto kTTLDurSeconds4 = kTTLDurHours4 * 3600;\n\n// See menu_mute.cpp.\nclass IconWithText final : public Ui::Menu::Action {\npublic:\n\tusing Ui::Menu::Action::Action;\n\n\tvoid setData(const QString &text, const QPoint &iconPosition);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tQPoint _iconPosition;\n\tQString _text;\n\n};\n\nvoid IconWithText::setData(const QString &text, const QPoint &iconPosition) {\n\t_iconPosition = iconPosition;\n\t_text = text;\n}\n\nvoid IconWithText::paintEvent(QPaintEvent *e) {\n\tUi::Menu::Action::paintEvent(e);\n\n\tPainter p(this);\n\tp.setFont(st::dialogsScamFont);\n\tp.setPen(st::menuIconColor);\n\tp.drawText(_iconPosition, _text);\n}\n\nclass TextItem final : public Ui::Menu::ItemBase {\npublic:\n\tTextItem(\n\t\tnot_null<RpWidget*> parent,\n\t\tconst style::Menu &st,\n\t\trpl::producer<QString> &&text);\n\n\tnot_null<QAction*> action() const override;\n\tbool isEnabled() const override;\n\nprotected:\n\tint contentHeight() const override;\n\nprivate:\n\tconst base::unique_qptr<Ui::FlatLabel> _label;\n\tconst not_null<QAction*> _dummyAction;\n\n};\n\nTextItem::TextItem(\n\tnot_null<RpWidget*> parent,\n\tconst style::Menu &st,\n\trpl::producer<QString> &&text)\n: ItemBase(parent, st)\n, _label(base::make_unique_q<Ui::FlatLabel>(\n\tthis,\n\tstd::move(text),\n\tst::historyMessagesTTLLabel))\n, _dummyAction(Ui::CreateChild<QAction>(parent.get())) {\n\n\tsetAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tsetMinWidth(st::historyMessagesTTLLabel.minWidth\n\t\t+ st.itemIconPosition.x());\n\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\t_label->moveToLeft(\n\t\t\tst.itemIconPosition.x(),\n\t\t\t(s.height() - _label->height()) / 2);\n\t}, lifetime());\n\n\tfitToMenuWidth();\n}\n\nnot_null<QAction*> TextItem::action() const {\n\treturn _dummyAction;\n}\n\nbool TextItem::isEnabled() const {\n\treturn false;\n}\n\nint TextItem::contentHeight() const {\n\treturn _label->height();\n}\n\nvoid TTLBoxOld(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tFn<void(TimeId)> callback,\n\t\tTimeId startTtlPeriod) {\n\tstruct State {\n\t\tint lastSeconds = 0;\n\t};\n\tconst auto startTtl = startTtlPeriod ? startTtlPeriod : kTTLDurSeconds2;\n\tauto chooseTimeResult = ChooseTimeWidget(box, startTtl);\n\tbox->addRow(std::move(chooseTimeResult.widget));\n\n\tconst auto state = box->lifetime().make_state<State>();\n\n\tbox->setTitle(tr::lng_manage_messages_ttl_title());\n\n\tauto confirmText = std::move(\n\t\tchooseTimeResult.secondsValue\n\t) | rpl::map([=](int seconds) {\n\t\tstate->lastSeconds = seconds;\n\t\treturn !seconds\n\t\t\t? tr::lng_manage_messages_ttl_disable()\n\t\t\t: tr::lng_enable_auto_delete();\n\t}) | rpl::flatten_latest();\n\tconst auto confirm = box->addButton(std::move(confirmText), [=] {\n\t\tcallback(state->lastSeconds);\n\t\tbox->closeBox();\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n#endif\n\n} // namespace\n\nvoid TTLBox(not_null<Ui::GenericBox*> box, Args args) {\n\tif (args.about) {\n\t\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(args.about),\n\t\t\tst::boxLabel));\n\t}\n\n\tconst auto ttls = std::vector<TimeId>{\n\t\t(86400 * 1),\n\t\t(86400 * 2),\n\t\t(86400 * 3),\n\t\t(86400 * 4),\n\t\t(86400 * 5),\n\t\t(86400 * 6),\n\t\t(86400 * 7 * 1),\n\t\t(86400 * 7 * 2),\n\t\t(86400 * 7 * 3),\n\t\t(86400 * 31 * 1),\n\t\t(86400 * 31 * 2),\n\t\t(86400 * 31 * 3),\n\t\t(86400 * 31 * 4),\n\t\t(86400 * 31 * 5),\n\t\t(86400 * 31 * 6),\n\t\t(86400 * 365),\n\t};\n\tconst auto phrases = ranges::views::all(\n\t\tttls\n\t) | ranges::views::transform(Ui::FormatTTL) | ranges::to_vector;\n\n\tconst auto pickerTtl = TimePickerBox(box, ttls, phrases, args.startTtl);\n\n\tUi::ConfirmBox(box, {\n\t\t.confirmed = [=](Fn<void()> close) {\n\t\t\targs.callback(pickerTtl(), std::move(close));\n\t\t},\n\t\t.confirmText = tr::lng_settings_save(),\n\t\t.cancelText = tr::lng_cancel(),\n\t});\n\n\tbox->setTitle(tr::lng_manage_messages_ttl_title());\n\n\tif (args.startTtl && !args.hideDisable) {\n\t\tbox->addLeftButton(tr::lng_manage_messages_ttl_disable(), [=] {\n\t\t\targs.callback(0, [=] { box->closeBox(); });\n\t\t});\n\t}\n}\n\n#if 0\nvoid FillTTLMenu(not_null<Ui::PopupMenu*> menu, Args args) {\n\tconst auto &st = menu->st().menu;\n\tconst auto iconTextPosition = st.itemIconPosition\n\t\t+ st::menuIconTTLAnyTextPosition;\n\tconst auto addAction = [&](const QString &text, TimeId ttl) {\n\t\tauto item = base::make_unique_q<IconWithText>(\n\t\t\tmenu,\n\t\t\tst,\n\t\t\tUi::Menu::CreateAction(\n\t\t\t\tmenu->menu().get(),\n\t\t\t\ttext,\n\t\t\t\t[=] { args.callback(ttl); }),\n\t\t\t&st::menuIconTTLAny,\n\t\t\t&st::menuIconTTLAny);\n\t\titem->setData(Ui::FormatTTLTiny(ttl), iconTextPosition);\n\t\tmenu->addAction(std::move(item));\n\t};\n\taddAction(tr::lng_manage_messages_ttl_after1(tr::now), kTTLDurSeconds1);\n\taddAction(tr::lng_manage_messages_ttl_after2(tr::now), kTTLDurSeconds2);\n\taddAction(tr::lng_manage_messages_ttl_after3(tr::now), kTTLDurSeconds3);\n\taddAction(tr::lng_manage_messages_ttl_after4(tr::now), kTTLDurSeconds4);\n\n\tmenu->addAction(\n\t\ttr::lng_manage_messages_ttl_after_custom(tr::now),\n\t\t[a = args] { a.show->showBox(Box(TTLBox, a)); },\n\t\t&st::menuIconCustomize);\n\n\tif (args.startTtl) {\n\t\tmenu->addAction({\n\t\t\t.text = tr::lng_manage_messages_ttl_disable(tr::now),\n\t\t\t.handler = [=] { args.callback(0); },\n\t\t\t.icon = &st::menuIconDisableAttention,\n\t\t\t.isAttention = true,\n\t\t});\n\t}\n\n\tmenu->addSeparator();\n\n\tmenu->addAction(base::make_unique_q<TextItem>(\n\t\tmenu,\n\t\tmenu->st().menu,\n\t\tstd::move(args.about)));\n}\n\nvoid SetupTTLMenu(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<> triggers,\n\t\tArgs args) {\n\tstruct State {\n\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t};\n\tconst auto state = parent->lifetime().make_state<State>();\n\tstd::move(\n\t\ttriggers\n\t) | rpl::on_next([=] {\n\t\tif (state->menu) {\n\t\t\treturn;\n\t\t}\n\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tparent,\n\t\t\tst::popupMenuExpandedSeparator);\n\t\tFillTTLMenu(state->menu.get(), args);\n\t\tstate->menu->popup(QCursor::pos());\n\t}, parent->lifetime());\n}\n#endif\n\n} // namespace TTLMenu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_ttl.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n#if 0\nclass PopupMenu;\nclass RpWidget;\n#endif\nclass Show;\nclass GenericBox;\n} // namespace Ui\n\nnamespace TTLMenu {\n\nstruct Args {\n\tstd::shared_ptr<Ui::Show> show;\n\tTimeId startTtl;\n\trpl::producer<TextWithEntities> about;\n\tFn<void(TimeId, Fn<void()>)> callback;\n\tbool hideDisable = false;\n};\n\nvoid TTLBox(not_null<Ui::GenericBox*> box, Args args);\n#if 0\nvoid FillTTLMenu(not_null<Ui::PopupMenu*> menu, Args args);\n\nvoid SetupTTLMenu(\n\tnot_null<Ui::RpWidget*> parent,\n\trpl::producer<> triggers,\n\tArgs args);\n#endif\n\n} // namespace TTLMenu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_ttl_validator.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"menu/menu_ttl_validator.h\"\n\n#include \"apiwrap.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/session/session_show.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_ttl.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/layers/show.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/text/format_values.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace TTLMenu {\nnamespace {\n\nconstexpr auto kToastDuration = crl::time(3500);\n\nvoid ShowAutoDeleteToast(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer) {\n\tconst auto period = peer->messagesTTL();\n\tif (!period) {\n\t\tshow->showToast(tr::lng_ttl_about_tooltip_off(tr::now));\n\t\treturn;\n\t}\n\n\tconst auto duration = (period == 5)\n\t\t? u\"5 seconds\"_q\n\t\t: Ui::FormatTTL(period);\n\tconst auto text = peer->isBroadcast()\n\t\t? tr::lng_ttl_about_tooltip_channel(tr::now, lt_duration, duration)\n\t\t: tr::lng_ttl_about_tooltip(tr::now, lt_duration, duration);\n\tshow->showToast(text, kToastDuration);\n}\n\n} // namespace\n\nTTLValidator::TTLValidator(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<PeerData*> peer)\n: _peer(peer)\n, _show(std::move(show)) {\n}\n\nArgs TTLValidator::createArgs() const {\n\tconst auto peer = _peer;\n\tconst auto show = _show;\n\tstruct State {\n\t\tTimeId savingPeriod = 0;\n\t\tmtpRequestId savingRequestId = 0;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tauto callback = [=](\n\t\t\tTimeId period,\n\t\t\tFn<void()>) {\n\t\tauto &api = peer->session().api();\n\t\tif (state->savingRequestId) {\n\t\t\tif (period == state->savingPeriod) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tapi.request(state->savingRequestId).cancel();\n\t\t}\n\t\tstate->savingPeriod = period;\n\t\tstate->savingRequestId = api.request(MTPmessages_SetHistoryTTL(\n\t\t\tpeer->input(),\n\t\t\tMTP_int(period)\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\tpeer->session().api().applyUpdates(result);\n\t\t\tShowAutoDeleteToast(show, peer);\n\t\t\tstate->savingRequestId = 0;\n\t\t}).fail([=] {\n\t\t\tstate->savingRequestId = 0;\n\t\t}).send();\n\t\tshow->hideLayer();\n\t};\n\tauto about1 = peer->isUser()\n\t\t? tr::lng_ttl_edit_about(lt_user, rpl::single(peer->shortName()))\n\t\t: peer->isBroadcast()\n\t\t? tr::lng_ttl_edit_about_channel()\n\t\t: tr::lng_ttl_edit_about_group();\n\tauto about2 = tr::lng_ttl_edit_about2(\n\t\tlt_link,\n\t\ttr::lng_ttl_edit_about2_link(\n\t\t) | rpl::map([=](const QString &s) {\n\t\t\treturn tr::link(s, \"tg://settings/auto_delete\");\n\t\t}),\n\t\ttr::marked);\n\tauto about = rpl::combine(\n\t\tstd::move(about1),\n\t\tstd::move(about2)\n\t) | rpl::map([](const QString &s1, TextWithEntities &&s2) {\n\t\treturn TextWithEntities{ s1 }.append(u\"\\n\\n\"_q).append(std::move(s2));\n\t});\n\tconst auto ttl = peer->messagesTTL();\n\treturn { std::move(show), ttl, std::move(about), std::move(callback) };\n}\n\nbool TTLValidator::can() const {\n\treturn (_peer->isUser()\n\t\t\t&& !_peer->isSelf()\n\t\t\t&& !_peer->isNotificationsUser()\n\t\t\t&& !_peer->asUser()->isInaccessible()\n\t\t\t&& !_peer->asUser()->starsPerMessage()\n\t\t\t&& !_peer->asUser()->isVerifyCodes()\n\t\t\t&& (!_peer->asUser()->requiresPremiumToWrite()\n\t\t\t\t|| _peer->session().premium()))\n\t\t|| (_peer->isChat()\n\t\t\t&& _peer->asChat()->canEditInformation()\n\t\t\t&& _peer->asChat()->amIn())\n\t\t|| (_peer->isChannel()\n\t\t\t&& _peer->asChannel()->canEditInformation()\n\t\t\t&& _peer->asChannel()->amIn());\n}\n\nvoid TTLValidator::showToast() const {\n\tShowAutoDeleteToast(_show, _peer);\n}\n\nconst style::icon *TTLValidator::icon() const {\n\treturn &st::menuIconTTL;\n}\n\nvoid TTLValidator::showBox() const {\n\tif (Main::MakeSessionShow(_show, &_peer->session())->showFrozenError()) {\n\t\treturn;\n\t}\n\t_show->showBox(Box(TTLBox, createArgs()));\n}\n\n} // namespace TTLMenu\n"
  },
  {
    "path": "Telegram/SourceFiles/menu/menu_ttl_validator.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"menu/menu_ttl.h\"\n\nclass PeerData;\n\nnamespace Ui {\nclass Show;\n} // namespace Ui\n\nnamespace TTLMenu {\n\nclass TTLValidator final {\npublic:\n\tTTLValidator(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<PeerData*> peer);\n\n\tvoid showBox() const;\n\t[[nodiscard]] bool can() const;\n\t[[nodiscard]] Args createArgs() const;\n\tvoid showToast() const;\n\tconst style::icon *icon() const;\n\nprivate:\n\tconst not_null<PeerData*> _peer;\n\tconst std::shared_ptr<Ui::Show> _show;\n\n};\n\n} // namespace TTLMenu\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/config_loader.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/config_loader.h\"\n\n#include \"base/random.h\"\n#include \"mtproto/special_config_request.h\"\n#include \"mtproto/facade.h\"\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"mtproto/mtp_instance.h\"\n\nnamespace MTP {\nnamespace details {\nnamespace {\n\nconstexpr auto kEnumerateDcTimeout = 8000; // 8 seconds timeout for help_getConfig to work (then move to other dc)\nconstexpr auto kSpecialRequestTimeoutMs = 6000; // 4 seconds timeout for it to work in a specially requested dc.\n\n} // namespace\n\nConfigLoader::ConfigLoader(\n\tnot_null<Instance*> instance,\n\tconst QString &phone,\n\tFn<void(const MTPConfig &result)> onDone,\n\tFailHandler onFail,\n\tbool proxyEnabled)\n: _instance(instance)\n, _phone(phone)\n, _proxyEnabled(proxyEnabled)\n, _doneHandler(onDone)\n, _failHandler(onFail) {\n\t_enumDCTimer.setCallback([this] { enumerate(); });\n\t_specialEnumTimer.setCallback([this] { sendSpecialRequest(); });\n}\n\nvoid ConfigLoader::load() {\n\tif (!_instance->isKeysDestroyer()) {\n\t\tsendRequest(_instance->mainDcId());\n\t\t_enumDCTimer.callOnce(kEnumerateDcTimeout);\n\t} else {\n\t\tauto ids = _instance->dcOptions().configEnumDcIds();\n\t\tAssert(!ids.empty());\n\t\t_enumCurrent = ids.front();\n\t\tenumerate();\n\t}\n}\n\nmtpRequestId ConfigLoader::sendRequest(ShiftedDcId shiftedDcId) {\n\tauto done = [done = _doneHandler](const Response &response) {\n\t\tauto from = response.reply.constData();\n\t\tauto result = MTPConfig();\n\t\tif (!result.read(from, from + response.reply.size())) {\n\t\t\treturn false;\n\t\t}\n\t\tdone(result);\n\t\treturn true;\n\t};\n\treturn _instance->send(\n\t\tMTPhelp_GetConfig(),\n\t\tstd::move(done),\n\t\tbase::duplicate(_failHandler),\n\t\tshiftedDcId);\n}\n\nDcId ConfigLoader::specialToRealDcId(DcId specialDcId) {\n\treturn getTemporaryIdFromRealDcId(specialDcId);\n}\n\nvoid ConfigLoader::terminateRequest() {\n\tif (_enumRequest) {\n\t\t_instance->cancel(base::take(_enumRequest));\n\t}\n\tif (_enumCurrent) {\n\t\t_instance->killSession(MTP::configDcId(_enumCurrent));\n\t}\n}\n\nvoid ConfigLoader::terminateSpecialRequest() {\n\tif (_specialEnumRequest) {\n\t\t_instance->cancel(base::take(_specialEnumRequest));\n\t}\n\tif (_specialEnumCurrent) {\n\t\t_instance->killSession(_specialEnumCurrent);\n\t}\n}\n\nConfigLoader::~ConfigLoader() {\n\tterminateRequest();\n\tterminateSpecialRequest();\n}\n\nvoid ConfigLoader::enumerate() {\n\tterminateRequest();\n\tif (!_enumCurrent) {\n\t\t_enumCurrent = _instance->mainDcId();\n\t}\n\tauto ids = _instance->dcOptions().configEnumDcIds();\n\tAssert(!ids.empty());\n\n\tauto i = std::find(ids.cbegin(), ids.cend(), _enumCurrent);\n\tif (i == ids.cend() || (++i) == ids.cend()) {\n\t\t_enumCurrent = ids.front();\n\t} else {\n\t\t_enumCurrent = *i;\n\t}\n\t_enumRequest = sendRequest(MTP::configDcId(_enumCurrent));\n\n\t_enumDCTimer.callOnce(kEnumerateDcTimeout);\n\n\trefreshSpecialLoader();\n}\n\nvoid ConfigLoader::refreshSpecialLoader() {\n\tif (_proxyEnabled || _instance->isKeysDestroyer()) {\n\t\t_specialLoader.reset();\n\t\treturn;\n\t}\n\tif (!_specialLoader\n\t\t|| (!_specialEnumRequest && _specialEndpoints.empty())) {\n\t\tcreateSpecialLoader();\n\t}\n}\n\nvoid ConfigLoader::setPhone(const QString &phone) {\n\tif (_phone != phone) {\n\t\t_phone = phone;\n\t\tif (_specialLoader) {\n\t\t\tcreateSpecialLoader();\n\t\t}\n\t}\n}\n\nvoid ConfigLoader::createSpecialLoader() {\n\tconst auto testMode = _instance->isTestMode();\n\t_triedSpecialEndpoints.clear();\n\t_specialLoader = std::make_unique<SpecialConfigRequest>([=](\n\t\t\tDcId dcId,\n\t\t\tconst std::string &ip,\n\t\t\tint port,\n\t\t\tbytes::const_span secret) {\n\t\tif (ip.empty()) {\n\t\t\t_specialLoader = nullptr;\n\t\t} else {\n\t\t\taddSpecialEndpoint(dcId, ip, port, secret);\n\t\t}\n\t}, testMode, _instance->configValues().txtDomainString, _phone);\n}\n\nvoid ConfigLoader::addSpecialEndpoint(\n\t\tDcId dcId,\n\t\tconst std::string &ip,\n\t\tint port,\n\t\tbytes::const_span secret) {\n\tconst auto endpoint = SpecialEndpoint {\n\t\tdcId,\n\t\tip,\n\t\tport,\n\t\tbytes::make_vector(secret)\n\t};\n\tif (base::contains(_specialEndpoints, endpoint)\n\t\t|| base::contains(_triedSpecialEndpoints, endpoint)) {\n\t\treturn;\n\t}\n\tDEBUG_LOG((\"MTP Info: Special endpoint received, '%1:%2'\").arg(ip.c_str()).arg(port));\n\t_specialEndpoints.push_back(endpoint);\n\n\tif (!_specialEnumTimer.isActive()) {\n\t\t_specialEnumTimer.callOnce(1);\n\t}\n}\n\nvoid ConfigLoader::sendSpecialRequest() {\n\tterminateSpecialRequest();\n\tif (_proxyEnabled) {\n\t\t_specialLoader.reset();\n\t\treturn;\n\t}\n\tif (_specialEndpoints.empty()) {\n\t\trefreshSpecialLoader();\n\t\treturn;\n\t}\n\n\tconst auto weak = base::make_weak(this);\n\tconst auto index = base::RandomValue<uint32>() % _specialEndpoints.size();\n\tconst auto endpoint = _specialEndpoints.begin() + index;\n\t_specialEnumCurrent = specialToRealDcId(endpoint->dcId);\n\n\tusing Flag = MTPDdcOption::Flag;\n\tconst auto flags = Flag::f_tcpo_only\n\t\t| (endpoint->secret.empty() ? Flag(0) : Flag::f_secret);\n\t_instance->dcOptions().constructAddOne(\n\t\t_specialEnumCurrent,\n\t\tflags,\n\t\tendpoint->ip,\n\t\tendpoint->port,\n\t\tendpoint->secret);\n\t_specialEnumRequest = _instance->send(\n\t\tMTPhelp_GetConfig(),\n\t\t[weak](const Response &response) {\n\t\t\tauto result = MTPConfig();\n\t\t\tauto from = response.reply.constData();\n\t\t\tif (!result.read(from, from + response.reply.size())) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->specialConfigLoaded(result);\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\t\tbase::duplicate(_failHandler),\n\t\t_specialEnumCurrent);\n\t_triedSpecialEndpoints.push_back(*endpoint);\n\t_specialEndpoints.erase(endpoint);\n\n\t_specialEnumTimer.callOnce(kSpecialRequestTimeoutMs);\n}\n\nvoid ConfigLoader::specialConfigLoaded(const MTPConfig &result) {\n\tExpects(result.type() == mtpc_config);\n\n\tconst auto &data = result.c_config();\n\tif (data.vdc_options().v.empty()) {\n\t\tLOG((\"MTP Error: config with empty dc_options received!\"));\n\t\treturn;\n\t}\n\n\t// We use special config only for dc options.\n\t// For everything else we wait for normal config from main dc.\n\t_instance->dcOptions().setFromList(data.vdc_options());\n}\n\nvoid ConfigLoader::setProxyEnabled(bool value) {\n\t_proxyEnabled = value;\n}\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/config_loader.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n#include \"base/bytes.h\"\n#include \"mtproto/mtproto_response.h\"\n\nnamespace MTP {\n\nclass Instance;\n\nnamespace details {\n\nclass SpecialConfigRequest;\n\nclass ConfigLoader : public base::has_weak_ptr {\npublic:\n\tConfigLoader(\n\t\tnot_null<Instance*> instance,\n\t\tconst QString &phone,\n\t\tFn<void(const MTPConfig &result)> onDone,\n\t\tFailHandler onFail,\n\t\tbool proxyEnabled);\n\t~ConfigLoader();\n\n\tvoid load();\n\tvoid setPhone(const QString &phone);\n\tvoid setProxyEnabled(bool value);\n\nprivate:\n\tmtpRequestId sendRequest(ShiftedDcId shiftedDcId);\n\tvoid addSpecialEndpoint(\n\t\tDcId dcId,\n\t\tconst std::string &ip,\n\t\tint port,\n\t\tbytes::const_span secret);\n\tvoid sendSpecialRequest();\n\tvoid enumerate();\n\tvoid refreshSpecialLoader();\n\tvoid createSpecialLoader();\n\tDcId specialToRealDcId(DcId specialDcId);\n\tvoid specialConfigLoaded(const MTPConfig &result);\n\tvoid terminateRequest();\n\tvoid terminateSpecialRequest();\n\n\tnot_null<Instance*> _instance;\n\tbase::Timer _enumDCTimer;\n\tDcId _enumCurrent = 0;\n\tmtpRequestId _enumRequest = 0;\n\n\tstruct SpecialEndpoint {\n\t\tDcId dcId;\n\t\tstd::string ip;\n\t\tint port;\n\t\tbytes::vector secret;\n\t};\n\tfriend bool operator==(const SpecialEndpoint &a, const SpecialEndpoint &b);\n\tstd::unique_ptr<SpecialConfigRequest> _specialLoader;\n\tstd::vector<SpecialEndpoint> _specialEndpoints;\n\tstd::vector<SpecialEndpoint> _triedSpecialEndpoints;\n\tbase::Timer _specialEnumTimer;\n\tDcId _specialEnumCurrent = 0;\n\tmtpRequestId _specialEnumRequest = 0;\n\tQString _phone;\n\tbool _proxyEnabled = false;\n\n\tFn<void(const MTPConfig &result)> _doneHandler;\n\tFailHandler _failHandler;\n\n};\n\ninline bool operator==(const ConfigLoader::SpecialEndpoint &a, const ConfigLoader::SpecialEndpoint &b) {\n\treturn (a.dcId == b.dcId) && (a.ip == b.ip) && (a.port == b.port);\n}\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/connection_abstract.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/connection_abstract.h\"\n\n#include \"mtproto/connection_tcp.h\"\n#include \"mtproto/connection_http.h\"\n#include \"mtproto/connection_resolving.h\"\n#include \"mtproto/session.h\"\n#include \"base/unixtime.h\"\n#include \"base/random.h\"\n\nnamespace MTP {\nnamespace details {\nnamespace {\n\nstd::atomic<int> GlobalConnectionCounter/* = 0*/;\n\n} // namespace\n\nConnectionPointer::ConnectionPointer() = default;\n\nConnectionPointer::ConnectionPointer(std::nullptr_t) {\n}\n\nConnectionPointer::ConnectionPointer(AbstractConnection *value)\n: _value(value) {\n}\n\nConnectionPointer::ConnectionPointer(ConnectionPointer &&other)\n: _value(base::take(other._value)) {\n}\n\nConnectionPointer &ConnectionPointer::operator=(ConnectionPointer &&other) {\n\treset(base::take(other._value));\n\treturn *this;\n}\n\nAbstractConnection *ConnectionPointer::get() const {\n\treturn _value;\n}\n\nvoid ConnectionPointer::reset(AbstractConnection *value) {\n\tif (_value == value) {\n\t\treturn;\n\t} else if (const auto old = base::take(_value)) {\n\t\tconst auto disconnect = [&](auto signal) {\n\t\t\told->disconnect(old, signal, nullptr, nullptr);\n\t\t};\n\t\tdisconnect(&AbstractConnection::receivedData);\n\t\tdisconnect(&AbstractConnection::receivedSome);\n\t\tdisconnect(&AbstractConnection::error);\n\t\tdisconnect(&AbstractConnection::connected);\n\t\tdisconnect(&AbstractConnection::disconnected);\n\t\told->disconnectFromServer();\n\t\told->deleteLater();\n\t}\n\t_value = value;\n}\n\nConnectionPointer::operator AbstractConnection*() const {\n\treturn get();\n}\n\nAbstractConnection *ConnectionPointer::operator->() const {\n\treturn get();\n}\n\nAbstractConnection &ConnectionPointer::operator*() const {\n\treturn *get();\n}\n\nConnectionPointer::operator bool() const {\n\treturn get() != nullptr;\n}\n\nConnectionPointer::~ConnectionPointer() {\n\treset();\n}\n\nmtpBuffer AbstractConnection::prepareSecurePacket(\n\t\tuint64 keyId,\n\t\tMTPint128 msgKey,\n\t\tuint32 size) const {\n\tauto result = mtpBuffer();\n\tconstexpr auto kTcpPrefixInts = 2;\n\tconstexpr auto kAuthKeyIdPosition = kTcpPrefixInts;\n\tconstexpr auto kAuthKeyIdInts = 2;\n\tconstexpr auto kMessageKeyPosition = kAuthKeyIdPosition\n\t\t+ kAuthKeyIdInts;\n\tconstexpr auto kMessageKeyInts = 4;\n\tconstexpr auto kPrefixInts = kTcpPrefixInts\n\t\t+ kAuthKeyIdInts\n\t\t+ kMessageKeyInts;\n\tconstexpr auto kTcpPostfixInts = 4;\n\tresult.reserve(kPrefixInts + size + kTcpPostfixInts);\n\tresult.resize(kPrefixInts);\n\t*reinterpret_cast<uint64*>(&result[kAuthKeyIdPosition]) = keyId;\n\t*reinterpret_cast<MTPint128*>(&result[kMessageKeyPosition]) = msgKey;\n\treturn result;\n}\n\ngsl::span<const mtpPrime> AbstractConnection::parseNotSecureResponse(\n\t\tconst mtpBuffer &buffer) const {\n\tconst auto answer = buffer.data();\n\tconst auto len = buffer.size();\n\tif (len < 6) {\n\t\tLOG((\"Not Secure Error: bad request answer, len = %1\"\n\t\t\t).arg(len * sizeof(mtpPrime)));\n\t\tDEBUG_LOG((\"Not Secure Error: answer bytes %1\"\n\t\t\t).arg(Logs::mb(answer, len * sizeof(mtpPrime)).str()));\n\t\treturn {};\n\t}\n\tif (answer[0] != 0\n\t\t|| answer[1] != 0\n\t\t|| (((uint32)answer[2]) & 0x03) != 1\n\t\t//|| (base::unixtime::now() - answer[3] > 300) // We didn't sync time yet.\n\t\t//|| (answer[3] - base::unixtime::now() > 60)\n\t\t|| false) {\n\t\tLOG((\"Not Secure Error: bad request answer start (%1 %2 %3)\"\n\t\t\t).arg(answer[0]\n\t\t\t).arg(answer[1]\n\t\t\t).arg(answer[2]));\n\t\tDEBUG_LOG((\"Not Secure Error: answer bytes %1\"\n\t\t\t).arg(Logs::mb(answer, len * sizeof(mtpPrime)).str()));\n\t\treturn {};\n\t}\n\tconst auto answerLen = (uint32)answer[4];\n\tif (answerLen < 1 || answerLen > (len - 5) * sizeof(mtpPrime)) {\n\t\tLOG((\"Not Secure Error: bad request answer 1 <= %1 <= %2\"\n\t\t\t).arg(answerLen\n\t\t\t).arg((len - 5) * sizeof(mtpPrime)));\n\t\tDEBUG_LOG((\"Not Secure Error: answer bytes %1\"\n\t\t\t).arg(Logs::mb(answer, len * sizeof(mtpPrime)).str()));\n\t\treturn {};\n\t}\n\treturn gsl::make_span(answer + 5, answerLen);\n}\n\nmtpBuffer AbstractConnection::preparePQFake(const MTPint128 &nonce) const {\n\treturn prepareNotSecurePacket(\n\t\tMTPReq_pq(nonce),\n\t\tbase::unixtime::mtproto_msg_id());\n}\n\nstd::optional<MTPResPQ> AbstractConnection::readPQFakeReply(\n\t\tconst mtpBuffer &buffer) const {\n\tconst auto answer = parseNotSecureResponse(buffer);\n\tif (answer.empty()) {\n\t\treturn std::nullopt;\n\t}\n\tauto from = answer.data();\n\tMTPResPQ response;\n\treturn response.read(from, from + answer.size())\n\t\t? std::make_optional(response)\n\t\t: std::nullopt;\n}\n\nAbstractConnection::AbstractConnection(\n\tQThread *thread,\n\tconst ProxyData &proxy)\n: _proxy(proxy)\n, _debugId(QString::number(++GlobalConnectionCounter)) {\n\tmoveToThread(thread);\n}\n\nConnectionPointer AbstractConnection::Create(\n\t\tnot_null<Instance*> instance,\n\t\tDcOptions::Variants::Protocol protocol,\n\t\tQThread *thread,\n\t\tconst bytes::vector &secret,\n\t\tconst ProxyData &proxy) {\n\tauto result = [&] {\n\t\tif (protocol == DcOptions::Variants::Tcp) {\n\t\t\treturn ConnectionPointer::New<TcpConnection>(\n\t\t\t\tinstance,\n\t\t\t\tthread,\n\t\t\t\tproxy);\n\t\t} else {\n\t\t\treturn ConnectionPointer::New<HttpConnection>(thread, proxy);\n\t\t}\n\t}();\n\tif (proxy.tryCustomResolve()) {\n\t\treturn ConnectionPointer::New<ResolvingConnection>(\n\t\t\tinstance,\n\t\t\tthread,\n\t\t\tproxy,\n\t\t\tstd::move(result));\n\t}\n\treturn result;\n}\n\nQString AbstractConnection::ProtocolDcDebugId(int16 protocolDcId) {\n\tconst auto postfix = (protocolDcId < 0) ? \"_media\" : \"\";\n\tprotocolDcId = (protocolDcId < 0) ? (-protocolDcId) : protocolDcId;\n\tconst auto prefix = (protocolDcId > kTestModeDcIdShift) ? \"test_\" : \"\";\n\tprotocolDcId = (protocolDcId > kTestModeDcIdShift)\n\t\t? (protocolDcId - kTestModeDcIdShift)\n\t\t: protocolDcId;\n\treturn prefix + QString::number(protocolDcId) + postfix;\n}\n\nvoid AbstractConnection::logInfo(const QString &message) {\n\tDEBUG_LOG((\"Connection %1 Info: \").arg(_debugId) + message);\n}\n\nvoid AbstractConnection::logError(const QString &message) {\n\tDEBUG_LOG((\"Connection %1 Error: \").arg(_debugId) + message);\n}\n\nuint32 AbstractConnection::extendedNotSecurePadding() const {\n\treturn uint32(base::RandomValue<uchar>() & 0x3F);\n}\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/connection_abstract.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"mtproto/mtproto_proxy_data.h\"\n#include \"base/bytes.h\"\n\n#include <QtCore/QObject>\n#include <QtCore/QThread>\n\n#include <deque>\n\nnamespace MTP {\n\nclass Instance;\n\nnamespace details {\n\nstruct ConnectionOptions;\n\nclass AbstractConnection;\n\ninline constexpr auto kTestModeDcIdShift = 10000;\n\nclass ConnectionPointer {\npublic:\n\tConnectionPointer();\n\tConnectionPointer(std::nullptr_t);\n\tConnectionPointer(ConnectionPointer &&other);\n\tConnectionPointer &operator=(ConnectionPointer &&other);\n\n\ttemplate <typename ConnectionType, typename ...Args>\n\tstatic ConnectionPointer New(Args &&...args) {\n\t\treturn ConnectionPointer(new ConnectionType(\n\t\t\tstd::forward<Args>(args)...\n\t\t));\n\t}\n\n\tAbstractConnection *get() const;\n\tvoid reset(AbstractConnection *value = nullptr);\n\toperator AbstractConnection*() const;\n\tAbstractConnection *operator->() const;\n\tAbstractConnection &operator*() const;\n\texplicit operator bool() const;\n\n\t~ConnectionPointer();\n\nprivate:\n\texplicit ConnectionPointer(AbstractConnection *value);\n\n\tAbstractConnection *_value = nullptr;\n\n};\n\nclass AbstractConnection : public QObject {\n\tQ_OBJECT\n\npublic:\n\tAbstractConnection(QThread *thread, const ProxyData &proxy);\n\tAbstractConnection(const AbstractConnection &other) = delete;\n\tAbstractConnection &operator=(const AbstractConnection &other) = delete;\n\tvirtual ~AbstractConnection() = default;\n\n\t// virtual constructor\n\t[[nodiscard]] static ConnectionPointer Create(\n\t\tnot_null<Instance*> instance,\n\t\tDcOptions::Variants::Protocol protocol,\n\t\tQThread *thread,\n\t\tconst bytes::vector &secret,\n\t\tconst ProxyData &proxy);\n\n\t[[nodiscard]] virtual ConnectionPointer clone(const ProxyData &proxy) = 0;\n\n\t[[nodiscard]] virtual crl::time pingTime() const = 0;\n\t[[nodiscard]] virtual crl::time fullConnectTimeout() const = 0;\n\tvirtual void sendData(mtpBuffer &&buffer) = 0;\n\tvirtual void disconnectFromServer() = 0;\n\tvirtual void connectToServer(\n\t\tconst QString &ip,\n\t\tint port,\n\t\tconst bytes::vector &protocolSecret,\n\t\tint16 protocolDcId,\n\t\tbool protocolForFiles) = 0;\n\tvirtual void timedOut() {\n\t}\n\t[[nodiscard]] virtual bool isConnected() const = 0;\n\t[[nodiscard]] virtual bool usingHttpWait() {\n\t\treturn false;\n\t}\n\t[[nodiscard]] virtual bool needHttpWait() {\n\t\treturn false;\n\t}\n\n\t[[nodiscard]] virtual int32 debugState() const = 0;\n\n\t[[nodiscard]] virtual QString transport() const = 0;\n\t[[nodiscard]] virtual QString tag() const = 0;\n\n\tvoid setSentEncryptedWithKeyId(uint64 keyId) {\n\t\t_sentEncryptedWithKeyId = keyId;\n\t}\n\t[[nodiscard]] uint64 sentEncryptedWithKeyId() const {\n\t\treturn _sentEncryptedWithKeyId;\n\t}\n\n\tusing BuffersQueue = std::deque<mtpBuffer>;\n\t[[nodiscard]] BuffersQueue &received() {\n\t\treturn _receivedQueue;\n\t}\n\n\ttemplate <typename Request>\n\t[[nodiscard]] mtpBuffer prepareNotSecurePacket(\n\t\tconst Request &request,\n\t\tmtpMsgId newId) const;\n\t[[nodiscard]] mtpBuffer prepareSecurePacket(\n\t\tuint64 keyId,\n\t\tMTPint128 msgKey,\n\t\tuint32 size) const;\n\n\t[[nodiscard]] gsl::span<const mtpPrime> parseNotSecureResponse(\n\t\tconst mtpBuffer &buffer) const;\n\n\t[[nodiscard]] static QString ProtocolDcDebugId(int16 protocolDcId);\n\t[[nodiscard]] QString debugId() const {\n\t\treturn _debugId;\n\t}\n\tvoid logInfo(const QString &message);\n\tvoid logError(const QString &message);\n\n\t// Used to emit error(...) with no real code from the server.\n\tstatic constexpr auto kErrorCodeOther = -499;\n\nQ_SIGNALS:\n\tvoid receivedData();\n\tvoid receivedSome(); // to stop restart timer\n\n\tvoid error(qint32 errorCodebool);\n\n\tvoid connected();\n\tvoid disconnected();\n\n\tvoid syncTimeRequest();\n\nprotected:\n\tBuffersQueue _receivedQueue; // list of received packets, not processed yet\n\tint _pingTime = 0;\n\tProxyData _proxy;\n\n\tQString _debugId;\n\n\t// first we always send fake MTPReq_pq to see if connection works at all\n\t// we send them simultaneously through TCP/HTTP/IPv4/IPv6 to choose the working one\n\t[[nodiscard]] mtpBuffer preparePQFake(const MTPint128 &nonce) const;\n\t[[nodiscard]] std::optional<MTPResPQ> readPQFakeReply(\n\t\tconst mtpBuffer &buffer) const;\n\nprivate:\n\t[[nodiscard]] uint32 extendedNotSecurePadding() const;\n\n\tuint64 _sentEncryptedWithKeyId = 0;\n\n};\n\ntemplate <typename Request>\nmtpBuffer AbstractConnection::prepareNotSecurePacket(\n\t\tconst Request &request,\n\t\tmtpMsgId newId) const {\n\tconst auto intsSize = tl::count_length(request) >> 2;\n\tconst auto intsPadding = extendedNotSecurePadding();\n\n\tauto result = mtpBuffer();\n\tconstexpr auto kTcpPrefixInts = 2;\n\tconstexpr auto kAuthKeyIdInts = 2;\n\tconstexpr auto kMessageIdInts = 2;\n\tconstexpr auto kMessageLengthInts = 1;\n\tconstexpr auto kPrefixInts = kTcpPrefixInts\n\t\t+ kAuthKeyIdInts\n\t\t+ kMessageIdInts\n\t\t+ kMessageLengthInts;\n\tconstexpr auto kTcpPostfixInts = 4;\n\n\tresult.reserve(kPrefixInts + intsSize + intsPadding + kTcpPostfixInts);\n\tresult.resize(kPrefixInts);\n\n\tconst auto messageId = &result[kTcpPrefixInts + kAuthKeyIdInts];\n\t*reinterpret_cast<mtpMsgId*>(messageId) = newId;\n\n\trequest.write(result);\n\n\tconst auto messageLength = messageId + kMessageIdInts;\n\t*messageLength = (result.size() - kPrefixInts + intsPadding) << 2;\n\n\tif (intsPadding > 0) {\n\t\tconst auto skipPrimes = result.size();\n\t\tresult.resize(skipPrimes + intsPadding);\n\t\tconst auto skipBytes = skipPrimes * sizeof(mtpPrime);\n\t\tbytes::set_random(bytes::make_span(result).subspan(skipBytes));\n\t}\n\n\treturn result;\n}\n\n#define CONNECTION_LOG_INFO(x) if (Logs::DebugEnabled()) { logInfo(x); }\n#define CONNECTION_LOG_ERROR(x) if (Logs::DebugEnabled()) { logError(x); }\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/connection_http.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/connection_http.h\"\n\n#include \"base/random.h\"\n#include \"base/qthelp_url.h\"\n\nnamespace MTP {\nnamespace details {\nnamespace {\n\nconstexpr auto kForceHttpPort = 80;\nconstexpr auto kFullConnectionTimeout = crl::time(8000);\n\n} // namespace\n\nHttpConnection::HttpConnection(QThread *thread, const ProxyData &proxy)\n: AbstractConnection(thread, proxy)\n, _checkNonce(base::RandomValue<MTPint128>()) {\n\t_manager.moveToThread(thread);\n\t_manager.setProxy(ToNetworkProxy(proxy));\n}\n\nConnectionPointer HttpConnection::clone(const ProxyData &proxy) {\n\treturn ConnectionPointer::New<HttpConnection>(thread(), proxy);\n}\n\nvoid HttpConnection::sendData(mtpBuffer &&buffer) {\n\tExpects(buffer.size() > 2);\n\n\tif (_status == Status::Finished) {\n\t\treturn;\n\t}\n\n\tint32 requestSize = (buffer.size() - 2) * sizeof(mtpPrime);\n\n\tQNetworkRequest request(url());\n\trequest.setHeader(\n\t\tQNetworkRequest::ContentLengthHeader,\n\t\tQVariant(requestSize));\n\trequest.setHeader(\n\t\tQNetworkRequest::ContentTypeHeader,\n\t\tQVariant(u\"application/x-www-form-urlencoded\"_q));\n\n\tCONNECTION_LOG_INFO(u\"Sending %1 len request.\"_q.arg(requestSize));\n\t_requests.insert(_manager.post(request, QByteArray((const char*)(&buffer[2]), requestSize)));\n}\n\nvoid HttpConnection::disconnectFromServer() {\n\tif (_status == Status::Finished) return;\n\t_status = Status::Finished;\n\n\tconst auto requests = base::take(_requests);\n\tfor (const auto request : requests) {\n\t\trequest->abort();\n\t\trequest->deleteLater();\n\t}\n\n\tdisconnect(\n\t\t&_manager,\n\t\t&QNetworkAccessManager::finished,\n\t\tthis,\n\t\t&HttpConnection::requestFinished);\n}\n\nvoid HttpConnection::connectToServer(\n\t\tconst QString &address,\n\t\tint port,\n\t\tconst bytes::vector &protocolSecret,\n\t\tint16 protocolDcId,\n\t\tbool protocolForFiles) {\n\t_address = address;\n\tconnect(\n\t\t&_manager,\n\t\t&QNetworkAccessManager::finished,\n\t\tthis,\n\t\t&HttpConnection::requestFinished);\n\n\tauto buffer = preparePQFake(_checkNonce);\n\n\tif (Logs::DebugEnabled()) {\n\t\t_debugId = u\"%1(dc:%2,%3)\"_q\n\t\t\t.arg(_debugId.toInt())\n\t\t\t.arg(ProtocolDcDebugId(protocolDcId), url().toDisplayString());\n\t}\n\n\t_pingTime = crl::now();\n\tsendData(std::move(buffer));\n}\n\nmtpBuffer HttpConnection::handleResponse(QNetworkReply *reply) {\n\tQByteArray response = reply->readAll();\n\tCONNECTION_LOG_INFO(u\"Read %1 bytes.\"_q.arg(response.size()));\n\n\tif (response.isEmpty()) return mtpBuffer();\n\n\tif (response.size() & 0x03 || response.size() < 8) {\n\t\tCONNECTION_LOG_ERROR(u\"Bad response size %1.\"_q.arg(response.size()));\n\t\treturn mtpBuffer(1, -500);\n\t}\n\n\tmtpBuffer data(response.size() >> 2);\n\tmemcpy(data.data(), response.constData(), response.size());\n\n\treturn data;\n}\n\n// Returns \"maybe bad key\".\nqint32 HttpConnection::handleError(QNetworkReply *reply) {\n\tauto result = qint32(kErrorCodeOther);\n\n\tQVariant statusCode = reply->attribute(\n\t\tQNetworkRequest::HttpStatusCodeAttribute);\n\tif (statusCode.isValid()) {\n\t\tint status = statusCode.toInt();\n\t\tresult = -status;\n\t}\n\n\tswitch (reply->error()) {\n\tcase QNetworkReply::ConnectionRefusedError:\n\t\tCONNECTION_LOG_ERROR(u\"Connection refused - %1.\"_q\n\t\t\t.arg(reply->errorString()));\n\t\tbreak;\n\tcase QNetworkReply::RemoteHostClosedError:\n\t\tCONNECTION_LOG_ERROR(u\"Remote host closed - %1.\"_q\n\t\t\t.arg(reply->errorString()));\n\t\tbreak;\n\tcase QNetworkReply::HostNotFoundError:\n\t\tCONNECTION_LOG_ERROR(u\"Host not found - %1.\"_q\n\t\t\t.arg(reply->errorString()));\n\t\tbreak;\n\tcase QNetworkReply::TimeoutError:\n\t\tCONNECTION_LOG_ERROR(u\"Timeout - %1.\"_q\n\t\t\t.arg(reply->errorString()));\n\t\tbreak;\n\tcase QNetworkReply::OperationCanceledError:\n\t\tCONNECTION_LOG_ERROR(u\"Cancelled - %1.\"_q\n\t\t\t.arg(reply->errorString()));\n\t\tbreak;\n\tcase QNetworkReply::SslHandshakeFailedError:\n\tcase QNetworkReply::TemporaryNetworkFailureError:\n\tcase QNetworkReply::NetworkSessionFailedError:\n\tcase QNetworkReply::BackgroundRequestNotAllowedError:\n\tcase QNetworkReply::UnknownNetworkError:\n\t\tCONNECTION_LOG_ERROR(u\"Network error %1 - %2.\"_q\n\t\t\t.arg(reply->error())\n\t\t\t.arg(reply->errorString()));\n\t\tbreak;\n\n\t// proxy errors (101-199):\n\tcase QNetworkReply::ProxyConnectionRefusedError:\n\tcase QNetworkReply::ProxyConnectionClosedError:\n\tcase QNetworkReply::ProxyNotFoundError:\n\tcase QNetworkReply::ProxyTimeoutError:\n\tcase QNetworkReply::ProxyAuthenticationRequiredError:\n\tcase QNetworkReply::UnknownProxyError:\n\t\tCONNECTION_LOG_ERROR(u\"Proxy error %1 - %2.\"_q\n\t\t\t.arg(reply->error())\n\t\t\t.arg(reply->errorString()));\n\t\tbreak;\n\n\t// content errors (201-299):\n\tcase QNetworkReply::ContentAccessDenied:\n\tcase QNetworkReply::ContentOperationNotPermittedError:\n\tcase QNetworkReply::ContentNotFoundError:\n\tcase QNetworkReply::AuthenticationRequiredError:\n\tcase QNetworkReply::ContentReSendError:\n\tcase QNetworkReply::UnknownContentError:\n\t\tCONNECTION_LOG_ERROR(u\"Content error %1 - %2.\"_q\n\t\t\t.arg(reply->error())\n\t\t\t.arg(reply->errorString()));\n\t\tbreak;\n\n\t// protocol errors\n\tcase QNetworkReply::ProtocolUnknownError:\n\tcase QNetworkReply::ProtocolInvalidOperationError:\n\tcase QNetworkReply::ProtocolFailure:\n\t\tCONNECTION_LOG_ERROR(u\"Protocol error %1 - %2.\"_q\n\t\t\t.arg(reply->error())\n\t\t\t.arg(reply->errorString()));\n\t\tbreak;\n\t};\n\n\treturn result;\n}\n\nbool HttpConnection::isConnected() const {\n\treturn (_status == Status::Ready);\n}\n\nvoid HttpConnection::requestFinished(QNetworkReply *reply) {\n\tif (_status == Status::Finished) return;\n\n\treply->deleteLater();\n\tif (reply->error() == QNetworkReply::NoError) {\n\t\t_requests.remove(reply);\n\n\t\tmtpBuffer data = handleResponse(reply);\n\t\tif (data.size() == 1) {\n\t\t\terror(data[0]);\n\t\t} else if (!data.isEmpty()) {\n\t\t\tif (_status == Status::Ready) {\n\t\t\t\t_receivedQueue.push_back(data);\n\t\t\t\treceivedData();\n\t\t\t} else if (const auto res_pq = readPQFakeReply(data)) {\n\t\t\t\tconst auto &data = res_pq->c_resPQ();\n\t\t\t\tif (data.vnonce() == _checkNonce) {\n\t\t\t\t\tCONNECTION_LOG_INFO(\n\t\t\t\t\t\t\"HTTP-transport connected by pq-response.\");\n\t\t\t\t\t_status = Status::Ready;\n\t\t\t\t\t_pingTime = crl::now() - _pingTime;\n\t\t\t\t\tconnected();\n\t\t\t\t} else {\n\t\t\t\t\tCONNECTION_LOG_ERROR(\n\t\t\t\t\t\t\"Wrong nonce in HTTP fake pq-response.\");\n\t\t\t\t\terror(kErrorCodeOther);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tCONNECTION_LOG_ERROR(\n\t\t\t\t\t\"Could not parse HTTP fake pq-response.\");\n\t\t\t\terror(kErrorCodeOther);\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (!_requests.remove(reply)) {\n\t\t\treturn;\n\t\t}\n\n\t\terror(handleError(reply));\n\t}\n}\n\ncrl::time HttpConnection::pingTime() const {\n\treturn isConnected() ? _pingTime : crl::time(0);\n}\n\ncrl::time HttpConnection::fullConnectTimeout() const {\n\treturn kFullConnectionTimeout;\n}\n\nbool HttpConnection::usingHttpWait() {\n\treturn true;\n}\n\nbool HttpConnection::needHttpWait() {\n\treturn _requests.isEmpty();\n}\n\nint32 HttpConnection::debugState() const {\n\treturn -1;\n}\n\nQString HttpConnection::transport() const {\n\tif (!isConnected()) {\n\t\treturn QString();\n\t}\n\tauto result = u\"HTTP\"_q;\n\tif (qthelp::is_ipv6(_address)) {\n\t\tresult += u\"/IPv6\"_q;\n\t}\n\treturn result;\n}\n\nQString HttpConnection::tag() const {\n\tauto result = u\"HTTP\"_q;\n\tif (qthelp::is_ipv6(_address)) {\n\t\tresult += u\"/IPv6\"_q;\n\t} else {\n\t\tresult += u\"/IPv4\"_q;\n\t}\n\treturn result;\n}\n\nQUrl HttpConnection::url() const {\n\tconst auto pattern = qthelp::is_ipv6(_address)\n\t\t? u\"http://[%1]:%2/api\"_q\n\t\t: u\"http://%1:%2/api\"_q;\n\n\t// Not endpoint.port - always 80 port for http transport.\n\treturn QUrl(pattern.arg(_address).arg(kForceHttpPort));\n}\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/connection_http.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/connection_abstract.h\"\n\n#include <QtNetwork/QNetworkAccessManager>\n#include <QtNetwork/QNetworkReply>\n\nnamespace MTP {\nnamespace details {\n\nclass HttpConnection : public AbstractConnection {\npublic:\n\tHttpConnection(QThread *thread, const ProxyData &proxy);\n\n\tConnectionPointer clone(const ProxyData &proxy) override;\n\n\tcrl::time pingTime() const override;\n\tcrl::time fullConnectTimeout() const override;\n\tvoid sendData(mtpBuffer &&buffer) override;\n\tvoid disconnectFromServer() override;\n\tvoid connectToServer(\n\t\tconst QString &address,\n\t\tint port,\n\t\tconst bytes::vector &protocolSecret,\n\t\tint16 protocolDcId,\n\t\tbool protocolForFiles) override;\n\tbool isConnected() const override;\n\tbool usingHttpWait() override;\n\tbool needHttpWait() override;\n\n\tint32 debugState() const override;\n\n\tQString transport() const override;\n\tQString tag() const override;\n\n\tmtpBuffer handleResponse(QNetworkReply *reply);\n\tqint32 handleError(QNetworkReply *reply); // Returns error code.\n\nprivate:\n\tQUrl url() const;\n\n\tvoid requestFinished(QNetworkReply *reply);\n\n\tenum class Status {\n\t\tWaiting = 0,\n\t\tReady,\n\t\tFinished,\n\t};\n\tStatus _status = Status::Waiting;\n\tMTPint128 _checkNonce;\n\n\tQNetworkAccessManager _manager;\n\tQString _address;\n\n\tQSet<QNetworkReply*> _requests;\n\n\tcrl::time _pingTime = 0;\n\n};\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/connection_resolving.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/connection_resolving.h\"\n\n#include \"mtproto/mtp_instance.h\"\n\nnamespace MTP {\nnamespace details {\nnamespace {\n\nconstexpr auto kOneConnectionTimeout = 4000;\n\n} // namespace\n\nResolvingConnection::ResolvingConnection(\n\tnot_null<Instance*> instance,\n\tQThread *thread,\n\tconst ProxyData &proxy,\n\tConnectionPointer &&child)\n: AbstractConnection(thread, proxy)\n, _instance(instance)\n, _timeoutTimer([=] { handleError(kErrorCodeOther); }) {\n\tsetChild(std::move(child));\n\tif (proxy.resolvedExpireAt < crl::now()) {\n\t\tconst auto host = proxy.host;\n\t\tconnect(\n\t\t\tinstance,\n\t\t\t&Instance::proxyDomainResolved,\n\t\t\tthis,\n\t\t\t&ResolvingConnection::domainResolved,\n\t\t\tQt::QueuedConnection);\n\t\tInvokeQueued(instance, [=] {\n\t\t\tinstance->resolveProxyDomain(host);\n\t\t});\n\t}\n\tif (!proxy.resolvedIPs.empty()) {\n\t\trefreshChild();\n\t}\n}\n\nConnectionPointer ResolvingConnection::clone(const ProxyData &proxy) {\n\tUnexpected(\"ResolvingConnection::clone call.\");\n}\n\nvoid ResolvingConnection::setChild(ConnectionPointer &&child) {\n\t_child = std::move(child);\n\tconnect(\n\t\t_child,\n\t\t&AbstractConnection::receivedData,\n\t\tthis,\n\t\t&ResolvingConnection::handleReceivedData);\n\tconnect(\n\t\t_child,\n\t\t&AbstractConnection::receivedSome,\n\t\tthis,\n\t\t&ResolvingConnection::receivedSome);\n\tconnect(\n\t\t_child,\n\t\t&AbstractConnection::error,\n\t\tthis,\n\t\t&ResolvingConnection::handleError);\n\tconnect(_child,\n\t\t&AbstractConnection::connected,\n\t\tthis,\n\t\t&ResolvingConnection::handleConnected);\n\tconnect(_child,\n\t\t&AbstractConnection::disconnected,\n\t\tthis,\n\t\t&ResolvingConnection::handleDisconnected);\n\tif (_protocolDcId) {\n\t\t_child->connectToServer(\n\t\t\t_address,\n\t\t\t_port,\n\t\t\t_protocolSecret,\n\t\t\t_protocolDcId,\n\t\t\t_protocolForFiles);\n\t\tCONNECTION_LOG_INFO(\"Resolving connected a new child: \"\n\t\t\t+ _child->debugId());\n\t}\n}\n\nvoid ResolvingConnection::domainResolved(\n\t\tconst QString &host,\n\t\tconst QStringList &ips,\n\t\tqint64 expireAt) {\n\tif (host != _proxy.host || !_child) {\n\t\treturn;\n\t}\n\t_proxy.resolvedExpireAt = expireAt;\n\n\tauto index = 0;\n\tfor (const auto &ip : ips) {\n\t\tif (index >= _proxy.resolvedIPs.size()) {\n\t\t\t_proxy.resolvedIPs.push_back(ip);\n\t\t} else if (_proxy.resolvedIPs[index] != ip) {\n\t\t\t_proxy.resolvedIPs[index] = ip;\n\t\t\tif (_ipIndex >= index) {\n\t\t\t\t_ipIndex = index - 1;\n\t\t\t\trefreshChild();\n\t\t\t}\n\t\t}\n\t\t++index;\n\t}\n\tif (index < _proxy.resolvedIPs.size()) {\n\t\t_proxy.resolvedIPs.resize(index);\n\t\tif (_ipIndex >= index) {\n\t\t\temitError(kErrorCodeOther);\n\t\t}\n\t}\n\tif (_ipIndex < 0) {\n\t\trefreshChild();\n\t}\n}\n\nbool ResolvingConnection::refreshChild() {\n\tif (!_child) {\n\t\treturn true;\n\t} else if (++_ipIndex >= _proxy.resolvedIPs.size()) {\n\t\treturn false;\n\t}\n\tsetChild(_child->clone(ToDirectIpProxy(_proxy, _ipIndex)));\n\t_timeoutTimer.callOnce(kOneConnectionTimeout);\n\treturn true;\n}\n\nvoid ResolvingConnection::emitError(int errorCode) {\n\t_ipIndex = -1;\n\t_child = nullptr;\n\terror(errorCode);\n}\n\nvoid ResolvingConnection::handleError(int errorCode) {\n\tif (_connected) {\n\t\temitError(errorCode);\n\t} else if (!_proxy.resolvedIPs.empty()) {\n\t\tif (!refreshChild()) {\n\t\t\temitError(errorCode);\n\t\t}\n\t} else {\n\t\t// Wait for the domain to be resolved.\n\t}\n}\n\nvoid ResolvingConnection::handleDisconnected() {\n\tif (_connected) {\n\t\tdisconnected();\n\t} else {\n\t\thandleError(kErrorCodeOther);\n\t}\n}\n\nvoid ResolvingConnection::handleReceivedData() {\n\tauto &my = received();\n\tauto &his = _child->received();\n\tfor (auto &item : his) {\n\t\tmy.push_back(std::move(item));\n\t}\n\this.clear();\n\treceivedData();\n}\n\nvoid ResolvingConnection::handleConnected() {\n\t_connected = true;\n\t_timeoutTimer.cancel();\n\tif (_ipIndex >= 0) {\n\t\tconst auto host = _proxy.host;\n\t\tconst auto good = _proxy.resolvedIPs[_ipIndex];\n\t\tconst auto instance = _instance;\n\t\tInvokeQueued(_instance, [=] {\n\t\t\tinstance->setGoodProxyDomain(host, good);\n\t\t});\n\t}\n\tconnected();\n}\n\ncrl::time ResolvingConnection::pingTime() const {\n\tExpects(_child != nullptr);\n\n\treturn _child->pingTime();\n}\n\ncrl::time ResolvingConnection::fullConnectTimeout() const {\n\treturn kOneConnectionTimeout * qMax(int(_proxy.resolvedIPs.size()), 1);\n}\n\nvoid ResolvingConnection::sendData(mtpBuffer &&buffer) {\n\tExpects(_child != nullptr);\n\n\t_child->sendData(std::move(buffer));\n}\n\nvoid ResolvingConnection::disconnectFromServer() {\n\t_address = QString();\n\t_port = 0;\n\t_protocolSecret = bytes::vector();\n\t_protocolDcId = 0;\n\tif (!_child) {\n\t\treturn;\n\t}\n\t_child->disconnectFromServer();\n}\n\nvoid ResolvingConnection::connectToServer(\n\t\tconst QString &address,\n\t\tint port,\n\t\tconst bytes::vector &protocolSecret,\n\t\tint16 protocolDcId,\n\t\tbool protocolForFiles) {\n\tif (!_child) {\n\t\tInvokeQueued(this, [=] { emitError(kErrorCodeOther); });\n\t\treturn;\n\t}\n\t_address = address;\n\t_port = port;\n\t_protocolSecret = protocolSecret;\n\t_protocolDcId = protocolDcId;\n\t_protocolForFiles = protocolForFiles;\n\t_child->connectToServer(\n\t\taddress,\n\t\tport,\n\t\tprotocolSecret,\n\t\tprotocolDcId,\n\t\tprotocolForFiles);\n\tCONNECTION_LOG_INFO(\"Resolving connected a child: \" + _child->debugId());\n}\n\nbool ResolvingConnection::isConnected() const {\n\treturn _child ? _child->isConnected() : false;\n}\n\nint32 ResolvingConnection::debugState() const {\n\treturn _child ? _child->debugState() : -1;\n}\n\nQString ResolvingConnection::transport() const {\n\treturn _child ? _child->transport() : QString();\n}\n\nQString ResolvingConnection::tag() const {\n\treturn _child ? _child->tag() : QString();\n}\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/connection_resolving.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/mtproto_auth_key.h\"\n#include \"mtproto/connection_abstract.h\"\n#include \"base/timer.h\"\n\nnamespace MTP {\nnamespace details {\n\nclass ResolvingConnection : public AbstractConnection {\npublic:\n\tResolvingConnection(\n\t\tnot_null<Instance*> instance,\n\t\tQThread *thread,\n\t\tconst ProxyData &proxy,\n\t\tConnectionPointer &&child);\n\n\tConnectionPointer clone(const ProxyData &proxy) override;\n\n\tcrl::time pingTime() const override;\n\tcrl::time fullConnectTimeout() const override;\n\tvoid sendData(mtpBuffer &&buffer) override;\n\tvoid disconnectFromServer() override;\n\tvoid connectToServer(\n\t\tconst QString &address,\n\t\tint port,\n\t\tconst bytes::vector &protocolSecret,\n\t\tint16 protocolDcId,\n\t\tbool protocolForFiles) override;\n\tbool isConnected() const override;\n\n\tint32 debugState() const override;\n\n\tQString transport() const override;\n\tQString tag() const override;\n\nprivate:\n\tvoid setChild(ConnectionPointer &&child);\n\tbool refreshChild();\n\tvoid emitError(int errorCode);\n\n\tvoid domainResolved(\n\t\tconst QString &host,\n\t\tconst QStringList &ips,\n\t\tqint64 expireAt);\n\tvoid handleError(int errorCode);\n\tvoid handleConnected();\n\tvoid handleDisconnected();\n\tvoid handleReceivedData();\n\n\tnot_null<Instance*> _instance;\n\tConnectionPointer _child;\n\tbool _connected = false;\n\tint _ipIndex = -1;\n\tQString _address;\n\tint _port = 0;\n\tbytes::vector _protocolSecret;\n\tint16 _protocolDcId = 0;\n\tbool _protocolForFiles = false;\n\tbase::Timer _timeoutTimer;\n\n};\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/connection_tcp.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/connection_tcp.h\"\n\n#include \"mtproto/details/mtproto_abstract_socket.h\"\n#include \"base/bytes.h\"\n#include \"base/openssl_help.h\"\n#include \"base/random.h\"\n#include \"base/qthelp_url.h\"\n\nnamespace MTP {\nnamespace details {\nnamespace {\n\nconstexpr auto kPacketSizeMax = int(0x01000000 * sizeof(mtpPrime));\nconstexpr auto kFullConnectionTimeout = 8 * crl::time(1000);\nconstexpr auto kSmallBufferSize = 256 * 1024;\nconstexpr auto kMinPacketBuffer = 256;\nconstexpr auto kConnectionStartPrefixSize = 64;\n\n} // namespace\n\nclass TcpConnection::Protocol {\npublic:\n\tstatic std::unique_ptr<Protocol> Create(bytes::const_span secret);\n\n\tvirtual uint32 id() const = 0;\n\tvirtual bool supportsArbitraryLength() const = 0;\n\n\tvirtual void prepareKey(bytes::span key, bytes::const_span source) = 0;\n\tvirtual bytes::span finalizePacket(mtpBuffer &buffer) = 0;\n\n\tstatic constexpr auto kUnknownSize = -1;\n\tstatic constexpr auto kInvalidSize = -2;\n\tvirtual int readPacketLength(bytes::const_span bytes) const = 0;\n\tvirtual bytes::const_span readPacket(bytes::const_span bytes) const = 0;\n\n\tvirtual QString debugPostfix() const = 0;\n\n\tvirtual ~Protocol() = default;\n\nprivate:\n\tclass Version0;\n\tclass Version1;\n\tclass VersionD;\n\n};\n\nclass TcpConnection::Protocol::Version0 : public Protocol {\npublic:\n\tuint32 id() const override;\n\tbool supportsArbitraryLength() const override;\n\n\tvoid prepareKey(bytes::span key, bytes::const_span source) override;\n\tbytes::span finalizePacket(mtpBuffer &buffer) override;\n\n\tint readPacketLength(bytes::const_span bytes) const override;\n\tbytes::const_span readPacket(bytes::const_span bytes) const override;\n\n\tQString debugPostfix() const override;\n\n};\n\nuint32 TcpConnection::Protocol::Version0::id() const {\n\treturn 0xEFEFEFEFU;\n}\n\nbool TcpConnection::Protocol::Version0::supportsArbitraryLength() const {\n\treturn false;\n}\n\nvoid TcpConnection::Protocol::Version0::prepareKey(\n\t\tbytes::span key,\n\t\tbytes::const_span source) {\n\tbytes::copy(key, source);\n}\n\nbytes::span TcpConnection::Protocol::Version0::finalizePacket(\n\t\tmtpBuffer &buffer) {\n\tExpects(buffer.size() > 2 && buffer.size() < 0x1000003U);\n\n\tconst auto intsSize = uint32(buffer.size() - 2);\n\tconst auto bytesSize = intsSize * sizeof(mtpPrime);\n\tconst auto data = reinterpret_cast<uchar*>(&buffer[0]);\n\tconst auto added = [&] {\n\t\tif (intsSize < 0x7F) {\n\t\t\tdata[7] = uchar(intsSize);\n\t\t\treturn 1;\n\t\t}\n\t\tdata[4] = uchar(0x7F);\n\t\tdata[5] = uchar(intsSize & 0xFF);\n\t\tdata[6] = uchar((intsSize >> 8) & 0xFF);\n\t\tdata[7] = uchar((intsSize >> 16) & 0xFF);\n\t\treturn 4;\n\t}();\n\treturn bytes::make_span(buffer).subspan(8 - added, added + bytesSize);\n}\n\nint TcpConnection::Protocol::Version0::readPacketLength(\n\t\tbytes::const_span bytes) const {\n\tif (bytes.empty()) {\n\t\treturn kUnknownSize;\n\t}\n\n\tconst auto first = static_cast<char>(bytes[0]);\n\tif (first == 0x7F) {\n\t\tif (bytes.size() < 4) {\n\t\t\treturn kUnknownSize;\n\t\t}\n\t\tconst auto ints = static_cast<uint32>(bytes[1])\n\t\t\t| (static_cast<uint32>(bytes[2]) << 8)\n\t\t\t| (static_cast<uint32>(bytes[3]) << 16);\n\t\treturn (ints >= 0x7F) ? (int(ints << 2) + 4) : kInvalidSize;\n\t} else if (first > 0 && first < 0x7F) {\n\t\tconst auto ints = uint32(first);\n\t\treturn int(ints << 2) + 1;\n\t}\n\treturn kInvalidSize;\n}\n\nbytes::const_span TcpConnection::Protocol::Version0::readPacket(\n\t\tbytes::const_span bytes) const {\n\tconst auto size = readPacketLength(bytes);\n\tAssert(size != kUnknownSize\n\t\t&& size != kInvalidSize\n\t\t&& size <= bytes.size());\n\tconst auto sizeLength = (static_cast<char>(bytes[0]) == 0x7F) ? 4 : 1;\n\treturn bytes.subspan(sizeLength, size - sizeLength);\n}\n\nQString TcpConnection::Protocol::Version0::debugPostfix() const {\n\treturn QString();\n}\n\nclass TcpConnection::Protocol::Version1 : public Version0 {\npublic:\n\texplicit Version1(bytes::vector &&secret);\n\n\tvoid prepareKey(bytes::span key, bytes::const_span source) override;\n\n\tQString debugPostfix() const override;\n\nprivate:\n\tbytes::vector _secret;\n\n};\n\nTcpConnection::Protocol::Version1::Version1(bytes::vector &&secret)\n: _secret(std::move(secret)) {\n}\n\nvoid TcpConnection::Protocol::Version1::prepareKey(\n\t\tbytes::span key,\n\t\tbytes::const_span source) {\n\tconst auto payload = bytes::concatenate(source, _secret);\n\tbytes::copy(key, openssl::Sha256(payload));\n}\n\nQString TcpConnection::Protocol::Version1::debugPostfix() const {\n\treturn u\"_obf\"_q;\n}\n\nclass TcpConnection::Protocol::VersionD : public Version1 {\npublic:\n\tusing Version1::Version1;\n\n\tuint32 id() const override;\n\tbool supportsArbitraryLength() const override;\n\n\tbytes::span finalizePacket(mtpBuffer &buffer) override;\n\n\tint readPacketLength(bytes::const_span bytes) const override;\n\tbytes::const_span readPacket(bytes::const_span bytes) const override;\n\n\tQString debugPostfix() const override;\n\n};\n\nuint32 TcpConnection::Protocol::VersionD::id() const {\n\treturn 0xDDDDDDDDU;\n}\n\nbool TcpConnection::Protocol::VersionD::supportsArbitraryLength() const {\n\treturn true;\n}\n\nbytes::span TcpConnection::Protocol::VersionD::finalizePacket(\n\t\tmtpBuffer &buffer) {\n\tExpects(buffer.size() > 2 && buffer.size() < 0x1000003U);\n\n\tconst auto intsSize = uint32(buffer.size() - 2);\n\tconst auto padding = base::RandomValue<uint32>() & 0x0F;\n\tconst auto bytesSize = intsSize * sizeof(mtpPrime) + padding;\n\tbuffer[1] = bytesSize;\n\tfor (auto added = 0; added < padding; added += 4) {\n\t\tbuffer.push_back(base::RandomValue<mtpPrime>());\n\t}\n\n\treturn bytes::make_span(buffer).subspan(4, 4 + bytesSize);\n}\n\nint TcpConnection::Protocol::VersionD::readPacketLength(\n\t\tbytes::const_span bytes) const {\n\tif (bytes.size() < 4) {\n\t\treturn kUnknownSize;\n\t}\n\tconst auto value = *reinterpret_cast<const uint32*>(bytes.data()) + 4;\n\treturn (value >= 8 && value < kPacketSizeMax)\n\t\t? int(value)\n\t\t: kInvalidSize;\n}\n\nbytes::const_span TcpConnection::Protocol::VersionD::readPacket(\n\t\tbytes::const_span bytes) const {\n\tconst auto size = readPacketLength(bytes);\n\tAssert(size != kUnknownSize\n\t\t&& size != kInvalidSize\n\t\t&& size <= bytes.size());\n\tconst auto sizeLength = 4;\n\treturn bytes.subspan(sizeLength, size - sizeLength);\n}\n\nQString TcpConnection::Protocol::VersionD::debugPostfix() const {\n\treturn u\"_dd\"_q;\n}\n\nauto TcpConnection::Protocol::Create(bytes::const_span secret)\n-> std::unique_ptr<Protocol> {\n\t// See also DcOptions::ValidateSecret.\n\tif ((secret.size() >= 21 && secret[0] == bytes::type(0xEE))\n\t\t|| (secret.size() == 17 && secret[0] == bytes::type(0xDD))) {\n\t\treturn std::make_unique<VersionD>(\n\t\t\tbytes::make_vector(secret.subspan(1, 16)));\n\t} else if (secret.size() == 16) {\n\t\treturn std::make_unique<Version1>(bytes::make_vector(secret));\n\t} else if (secret.empty()) {\n\t\treturn std::make_unique<Version0>();\n\t}\n\tUnexpected(\"Secret bytes in TcpConnection::Protocol::Create.\");\n}\n\nTcpConnection::TcpConnection(\n\tnot_null<Instance*> instance,\n\tQThread *thread,\n\tconst ProxyData &proxy)\n: AbstractConnection(thread, proxy)\n, _instance(instance)\n, _checkNonce(base::RandomValue<MTPint128>()) {\n}\n\nConnectionPointer TcpConnection::clone(const ProxyData &proxy) {\n\treturn ConnectionPointer::New<TcpConnection>(_instance, thread(), proxy);\n}\n\nvoid TcpConnection::ensureAvailableInBuffer(int amount) {\n\tauto &buffer = _usingLargeBuffer ? _largeBuffer : _smallBuffer;\n\tconst auto full = bytes::make_span(buffer).subspan(\n\t\t_offsetBytes);\n\tif (full.size() >= amount) {\n\t\treturn;\n\t}\n\tconst auto read = full.subspan(0, _readBytes);\n\tif (amount <= _smallBuffer.size()) {\n\t\tif (_usingLargeBuffer) {\n\t\t\tbytes::copy(_smallBuffer, read);\n\t\t\t_usingLargeBuffer = false;\n\t\t\t_largeBuffer.clear();\n\t\t} else {\n\t\t\tbytes::move(_smallBuffer, read);\n\t\t}\n\t} else if (amount <= _largeBuffer.size()) {\n\t\tAssert(_usingLargeBuffer);\n\t\tbytes::move(_largeBuffer, read);\n\t} else {\n\t\tauto enough = bytes::vector(amount);\n\t\tbytes::copy(enough, read);\n\t\t_largeBuffer = std::move(enough);\n\t\t_usingLargeBuffer = true;\n\t}\n\t_offsetBytes = 0;\n}\n\nvoid TcpConnection::socketRead() {\n\tExpects(_leftBytes > 0 || !_usingLargeBuffer);\n\n\tif (!_socket || !_socket->isConnected()) {\n\t\tCONNECTION_LOG_ERROR(\"Socket not connected in socketRead()\");\n\t\terror(kErrorCodeOther);\n\t\treturn;\n\t}\n\n\tif (_smallBuffer.empty()) {\n\t\t_smallBuffer.resize(kSmallBufferSize);\n\t}\n\tdo {\n\t\tconst auto readLimit = (_leftBytes > 0)\n\t\t\t? _leftBytes\n\t\t\t: (kSmallBufferSize - _offsetBytes - _readBytes);\n\t\tAssert(readLimit > 0);\n\n\t\tauto &buffer = _usingLargeBuffer ? _largeBuffer : _smallBuffer;\n\t\tconst auto full = bytes::make_span(buffer).subspan(_offsetBytes);\n\t\tconst auto free = full.subspan(_readBytes);\n\t\tconst auto readCount = _socket->read(free.subspan(0, readLimit));\n\t\tif (readCount > 0) {\n\t\t\tconst auto read = free.subspan(0, readCount);\n\t\t\taesCtrEncrypt(read, _receiveKey, &_receiveState);\n\t\t\tCONNECTION_LOG_INFO(u\"Read %1 bytes\"_q.arg(readCount));\n\n\t\t\t_readBytes += readCount;\n\t\t\tif (_leftBytes > 0) {\n\t\t\t\tAssert(readCount <= _leftBytes);\n\t\t\t\t_leftBytes -= readCount;\n\t\t\t\tif (!_leftBytes) {\n\t\t\t\t\tsocketPacket(full.subspan(0, _readBytes));\n\t\t\t\t\tif (!_socket || !_socket->isConnected()) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t_usingLargeBuffer = false;\n\t\t\t\t\t_largeBuffer.clear();\n\t\t\t\t\t_offsetBytes = _readBytes = 0;\n\t\t\t\t} else {\n\t\t\t\t\tCONNECTION_LOG_INFO(\n\t\t\t\t\t\tu\"Not enough %1 for packet! read %2\"_q\n\t\t\t\t\t\t.arg(_leftBytes)\n\t\t\t\t\t\t.arg(_readBytes));\n\t\t\t\t\treceivedSome();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tauto available = full.subspan(0, _readBytes);\n\t\t\t\twhile (_readBytes > 0) {\n\t\t\t\t\tconst auto packetSize = _protocol->readPacketLength(\n\t\t\t\t\t\tavailable);\n\t\t\t\t\tif (packetSize == Protocol::kUnknownSize) {\n\t\t\t\t\t\t// Not enough bytes yet.\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else if (packetSize <= 0) {\n\t\t\t\t\t\tCONNECTION_LOG_ERROR(\n\t\t\t\t\t\t\tu\"Bad packet size in 4 bytes: %1\"_q\n\t\t\t\t\t\t\t.arg(packetSize));\n\t\t\t\t\t\terror(kErrorCodeOther);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t} else if (available.size() >= packetSize) {\n\t\t\t\t\t\tsocketPacket(available.subspan(0, packetSize));\n\t\t\t\t\t\tif (!_socket || !_socket->isConnected()) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tavailable = available.subspan(packetSize);\n\t\t\t\t\t\t_offsetBytes += packetSize;\n\t\t\t\t\t\t_readBytes -= packetSize;\n\n\t\t\t\t\t\t// If we have too little space left in the buffer.\n\t\t\t\t\t\tensureAvailableInBuffer(kMinPacketBuffer);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t_leftBytes = packetSize - available.size();\n\n\t\t\t\t\t\t// If the next packet won't fit in the buffer.\n\t\t\t\t\t\tensureAvailableInBuffer(packetSize);\n\n\t\t\t\t\t\tCONNECTION_LOG_INFO(u\"Not enough %1 for packet! \"\n\t\t\t\t\t\t\t\"full size %2 read %3\"_q\n\t\t\t\t\t\t\t.arg(_leftBytes)\n\t\t\t\t\t\t\t.arg(packetSize)\n\t\t\t\t\t\t\t.arg(available.size()));\n\t\t\t\t\t\treceivedSome();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (readCount < 0) {\n\t\t\tCONNECTION_LOG_ERROR(u\"Socket read return %1.\"_q.arg(readCount));\n\t\t\terror(kErrorCodeOther);\n\t\t\treturn;\n\t\t} else {\n\t\t\tCONNECTION_LOG_INFO(\n\t\t\t\t\"No bytes read, but bytes available was true...\");\n\t\t\tbreak;\n\t\t}\n\t} while (_socket\n\t\t&& _socket->isConnected()\n\t\t&& _socket->hasBytesAvailable());\n}\n\nmtpBuffer TcpConnection::parsePacket(bytes::const_span bytes) {\n\tconst auto packet = _protocol->readPacket(bytes);\n\tCONNECTION_LOG_INFO(u\"Packet received, size = %1.\"_q.arg(packet.size()));\n\tconst auto ints = gsl::make_span(\n\t\treinterpret_cast<const mtpPrime*>(packet.data()),\n\t\tpacket.size() / sizeof(mtpPrime));\n\tAssert(!ints.empty());\n\tif (ints.size() < 3) {\n\t\t// nop or error or new quickack, latter is not yet supported.\n\t\tif (ints[0] != 0) {\n\t\t\tCONNECTION_LOG_ERROR(u\"Error packet received, code = %1\"_q\n\t\t\t\t.arg(ints[0]));\n\t\t}\n\t\treturn mtpBuffer(1, ints[0]);\n\t}\n\tauto result = mtpBuffer(ints.size());\n\tmemcpy(result.data(), ints.data(), ints.size() * sizeof(mtpPrime));\n\treturn result;\n}\n\nvoid TcpConnection::socketConnected() {\n\tExpects(_status == Status::Waiting);\n\n\tauto buffer = preparePQFake(_checkNonce);\n\n\tCONNECTION_LOG_INFO(\"Sending fake req_pq.\");\n\n\t_pingTime = crl::now();\n\tsendData(std::move(buffer));\n}\n\nvoid TcpConnection::socketDisconnected() {\n\tif (_status == Status::Waiting || _status == Status::Ready) {\n\t\tdisconnected();\n\t}\n}\n\nvoid TcpConnection::sendData(mtpBuffer &&buffer) {\n\tExpects(buffer.size() > 2);\n\n\tif (!_socket) {\n\t\treturn;\n\t}\n\tchar connectionStartPrefixBytes[kConnectionStartPrefixSize];\n\tconst auto connectionStartPrefix = prepareConnectionStartPrefix(\n\t\tbytes::make_span(connectionStartPrefixBytes));\n\n\t// buffer: 2 available int-s + data + available int.\n\tconst auto bytes = _protocol->finalizePacket(buffer);\n\tCONNECTION_LOG_INFO(u\"TCP Info: write packet %1 bytes.\"_q\n\t\t.arg(bytes.size()));\n\taesCtrEncrypt(bytes, _sendKey, &_sendState);\n\t_socket->write(connectionStartPrefix, bytes);\n}\n\nbytes::const_span TcpConnection::prepareConnectionStartPrefix(\n\t\tbytes::span buffer) {\n\tExpects(_socket != nullptr);\n\tExpects(_protocol != nullptr);\n\n\tif (_connectionStarted) {\n\t\treturn {};\n\t}\n\t_connectionStarted = true;\n\n\t// prepare random part\n\tchar nonceBytes[64];\n\tconst auto nonce = bytes::make_span(nonceBytes);\n\tdo {\n\t\tbytes::set_random(nonce);\n\t} while (!_socket->isGoodStartNonce(nonce));\n\n\t// prepare encryption key/iv\n\t_protocol->prepareKey(\n\t\tbytes::make_span(_sendKey),\n\t\tnonce.subspan(8, CTRState::KeySize));\n\tbytes::copy(\n\t\tbytes::make_span(_sendState.ivec),\n\t\tnonce.subspan(8 + CTRState::KeySize, CTRState::IvecSize));\n\n\t// prepare decryption key/iv\n\tauto reversedBytes = bytes::vector(48);\n\tconst auto reversed = bytes::make_span(reversedBytes);\n\tbytes::copy(reversed, nonce.subspan(8, reversed.size()));\n\tstd::reverse(reversed.begin(), reversed.end());\n\t_protocol->prepareKey(\n\t\tbytes::make_span(_receiveKey),\n\t\treversed.subspan(0, CTRState::KeySize));\n\tbytes::copy(\n\t\tbytes::make_span(_receiveState.ivec),\n\t\treversed.subspan(CTRState::KeySize, CTRState::IvecSize));\n\n\t// write protocol and dc ids\n\tconst auto protocol = reinterpret_cast<uint32*>(nonce.data() + 56);\n\t*protocol = _protocol->id();\n\tconst auto dcId = reinterpret_cast<int16*>(nonce.data() + 60);\n\t*dcId = _protocolDcId;\n\n\tbytes::copy(buffer, nonce.subspan(0, 56));\n\taesCtrEncrypt(nonce, _sendKey, &_sendState);\n\tbytes::copy(buffer.subspan(56), nonce.subspan(56));\n\n\treturn buffer;\n}\n\nvoid TcpConnection::disconnectFromServer() {\n\tif (_status == Status::Finished) {\n\t\treturn;\n\t}\n\t_status = Status::Finished;\n\t_connectedLifetime.destroy();\n\t_lifetime.destroy();\n\t_socket = nullptr;\n}\n\nvoid TcpConnection::connectToServer(\n\t\tconst QString &address,\n\t\tint port,\n\t\tconst bytes::vector &protocolSecret,\n\t\tint16 protocolDcId,\n\t\tbool protocolForFiles) {\n\tExpects(_address.isEmpty());\n\tExpects(_port == 0);\n\tExpects(_protocol == nullptr);\n\tExpects(_protocolDcId == 0);\n\n\tconst auto secret = (_proxy.type == ProxyData::Type::Mtproto)\n\t\t? _proxy.secretFromMtprotoPassword()\n\t\t: protocolSecret;\n\tif (_proxy.type == ProxyData::Type::Mtproto) {\n\t\t_address = _proxy.host;\n\t\t_port = _proxy.port;\n\t\t_protocol = Protocol::Create(secret);\n\t} else {\n\t\t_address = address;\n\t\t_port = port;\n\t\t_protocol = Protocol::Create(secret);\n\t}\n\t_socket = AbstractSocket::Create(\n\t\tthread(),\n\t\tsecret,\n\t\tToNetworkProxy(_proxy),\n\t\tprotocolForFiles);\n\t_protocolDcId = protocolDcId;\n\n\tconst auto postfix = _socket->debugPostfix();\n\t_debugId = u\"%1(dc:%2,%3%4:%5%6)\"_q\n\t\t.arg(_debugId.toInt())\n\t\t.arg(\n\t\t\tProtocolDcDebugId(_protocolDcId),\n\t\t\t(_proxy.type == ProxyData::Type::Mtproto) ? \"mtproxy \" : \"\",\n\t\t\t_address)\n\t\t.arg(_port)\n\t\t.arg(postfix.isEmpty() ? _protocol->debugPostfix() : postfix);\n\t_socket->setDebugId(_debugId);\n\n\tCONNECTION_LOG_INFO(\"Connecting...\");\n\n\t_socket->connected(\n\t) | rpl::on_next([=] {\n\t\tsocketConnected();\n\t}, _connectedLifetime);\n\n\t_socket->disconnected(\n\t) | rpl::on_next([=] {\n\t\tsocketDisconnected();\n\t}, _lifetime);\n\n\t_socket->readyRead(\n\t) | rpl::on_next([=] {\n\t\tsocketRead();\n\t}, _lifetime);\n\n\t_socket->error(\n\t) | rpl::on_next([=] {\n\t\tsocketError();\n\t}, _lifetime);\n\n\t_socket->syncTimeRequests(\n\t) | rpl::on_next([=] {\n\t\tsyncTimeRequest();\n\t}, _lifetime);\n\n\t_socket->connectToHost(_address, _port);\n}\n\ncrl::time TcpConnection::pingTime() const {\n\treturn isConnected() ? _pingTime : crl::time(0);\n}\n\ncrl::time TcpConnection::fullConnectTimeout() const {\n\treturn kFullConnectionTimeout;\n}\n\nvoid TcpConnection::socketPacket(bytes::const_span bytes) {\n\tExpects(_socket != nullptr);\n\n\t// old quickack?..\n\tconst auto data = parsePacket(bytes);\n\tif (data.size() == 1) {\n\t\tif (data[0] != 0) {\n\t\t\terror(data[0]);\n\t\t} else {\n\t\t\t// nop\n\t\t}\n\t//} else if (data.size() == 2) {\n\t\t// new quickack?..\n\t} else if (_status == Status::Ready) {\n\t\t_receivedQueue.push_back(data);\n\t\treceivedData();\n\t} else if (_status == Status::Waiting) {\n\t\tif (const auto res_pq = readPQFakeReply(data)) {\n\t\t\tconst auto &data = res_pq->c_resPQ();\n\t\t\tif (data.vnonce() == _checkNonce) {\n\t\t\t\tCONNECTION_LOG_INFO(\"Valid pq response by TCP.\");\n\t\t\t\t_status = Status::Ready;\n\t\t\t\t_connectedLifetime.destroy();\n\t\t\t\t_pingTime = (crl::now() - _pingTime);\n\t\t\t\tconnected();\n\t\t\t} else {\n\t\t\t\tCONNECTION_LOG_ERROR(\n\t\t\t\t\t\"Wrong nonce received in TCP fake pq-responce\");\n\t\t\t\terror(kErrorCodeOther);\n\t\t\t}\n\t\t} else {\n\t\t\tCONNECTION_LOG_ERROR(\"Could not parse TCP fake pq-responce\");\n\t\t\terror(kErrorCodeOther);\n\t\t}\n\t}\n}\n\nvoid TcpConnection::timedOut() {\n\tif (_socket) {\n\t\t_socket->timedOut();\n\t}\n}\n\nbool TcpConnection::isConnected() const {\n\treturn (_status == Status::Ready);\n}\n\nint32 TcpConnection::debugState() const {\n\treturn _socket ? _socket->debugState() : -1;\n}\n\nQString TcpConnection::transport() const {\n\tif (!isConnected()) {\n\t\treturn QString();\n\t}\n\tauto result = u\"TCP\"_q;\n\tif (qthelp::is_ipv6(_address)) {\n\t\tresult += u\"/IPv6\"_q;\n\t}\n\treturn result;\n}\n\nQString TcpConnection::tag() const {\n\tauto result = u\"TCP\"_q;\n\tif (qthelp::is_ipv6(_address)) {\n\t\tresult += u\"/IPv6\"_q;\n\t} else {\n\t\tresult += u\"/IPv4\"_q;\n\t}\n\treturn result;\n}\n\nvoid TcpConnection::socketError() {\n\tif (!_socket) {\n\t\treturn;\n\t}\n\n\terror(kErrorCodeOther);\n}\n\nTcpConnection::~TcpConnection() = default;\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/connection_tcp.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/connection_abstract.h\"\n#include \"mtproto/mtproto_auth_key.h\"\n\nnamespace MTP {\nnamespace details {\n\nclass AbstractSocket;\n\nclass TcpConnection : public AbstractConnection {\npublic:\n\tTcpConnection(\n\t\tnot_null<Instance*> instance,\n\t\tQThread *thread,\n\t\tconst ProxyData &proxy);\n\n\tConnectionPointer clone(const ProxyData &proxy) override;\n\n\tcrl::time pingTime() const override;\n\tcrl::time fullConnectTimeout() const override;\n\tvoid sendData(mtpBuffer &&buffer) override;\n\tvoid disconnectFromServer() override;\n\tvoid connectToServer(\n\t\tconst QString &address,\n\t\tint port,\n\t\tconst bytes::vector &protocolSecret,\n\t\tint16 protocolDcId,\n\t\tbool protocolForFiles) override;\n\tvoid timedOut() override;\n\tbool isConnected() const override;\n\n\tint32 debugState() const override;\n\n\tQString transport() const override;\n\tQString tag() const override;\n\n\t~TcpConnection();\n\nprivate:\n\tenum class Status {\n\t\tWaiting = 0,\n\t\tReady,\n\t\tFinished,\n\t};\n\n\tvoid socketRead();\n\tbytes::const_span prepareConnectionStartPrefix(bytes::span buffer);\n\n\tvoid socketPacket(bytes::const_span bytes);\n\n\tvoid socketConnected();\n\tvoid socketDisconnected();\n\tvoid socketError();\n\n\tmtpBuffer parsePacket(bytes::const_span bytes);\n\tvoid ensureAvailableInBuffer(int amount);\n\tstatic uint32 fourCharsToUInt(char ch1, char ch2, char ch3, char ch4) {\n\t\tchar ch[4] = { ch1, ch2, ch3, ch4 };\n\t\treturn *reinterpret_cast<uint32*>(ch);\n\t}\n\n\tconst not_null<Instance*> _instance;\n\tstd::unique_ptr<AbstractSocket> _socket;\n\tbool _connectionStarted = false;\n\n\tint _offsetBytes = 0;\n\tint _readBytes = 0;\n\tint _leftBytes = 0;\n\tbytes::vector _smallBuffer;\n\tbytes::vector _largeBuffer;\n\tbool _usingLargeBuffer = false;\n\n\tuchar _sendKey[CTRState::KeySize];\n\tCTRState _sendState;\n\tuchar _receiveKey[CTRState::KeySize];\n\tCTRState _receiveState;\n\tclass Protocol;\n\tstd::unique_ptr<Protocol> _protocol;\n\tint16 _protocolDcId = 0;\n\n\tStatus _status = Status::Waiting;\n\tMTPint128 _checkNonce;\n\n\tQString _address;\n\tint32 _port = 0;\n\tcrl::time _pingTime = 0;\n\n\trpl::lifetime _connectedLifetime;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/core_types.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/basic_types.h\"\n#include \"base/match_method.h\"\n#include \"base/flags.h\"\n#include \"base/bytes.h\"\n#include \"base/algorithm.h\"\n#include \"base/assertion.h\"\n#include \"tl/tl_basic_types.h\"\n#include \"tl/tl_boxed.h\"\n\n#include <QtCore/QVector>\n#include <QtCore/QString>\n#include <QtCore/QByteArray>\n#include <range/v3/range/conversion.hpp>\n#include <gsl/gsl>\n\nusing mtpPrime = int32;\nusing mtpRequestId = int32;\nusing mtpMsgId = uint64;\nusing mtpPingId = uint64;\n\nusing mtpBuffer = QVector<mtpPrime>;\nusing mtpTypeId = uint32;\n\nstruct NotSingleDataTypePlaceholder {\n};\n\nnamespace MTP {\n\n// type DcId represents actual data center id, while in most cases\n// we use some shifted ids, like DcId() + X * DCShift\nusing DcId = int32;\nusing ShiftedDcId = int32;\n\nconstexpr auto kDcShift = ShiftedDcId(10000);\nconstexpr auto kConfigDcShift = 0x01;\nconstexpr auto kLogoutDcShift = 0x02;\nconstexpr auto kUpdaterDcShift = 0x03;\nconstexpr auto kExportDcShift = 0x04;\nconstexpr auto kExportMediaDcShift = 0x05;\nconstexpr auto kGroupCallStreamDcShift = 0x06;\nconstexpr auto kStatsDcShift = 0x07;\nconstexpr auto kMaxMediaDcCount = 0x10;\nconstexpr auto kBaseDownloadDcShift = 0x10;\nconstexpr auto kBaseUploadDcShift = 0x20;\nconstexpr auto kDestroyKeyStartDcShift = 0x100;\n\nconstexpr DcId BareDcId(ShiftedDcId shiftedDcId) {\n\treturn (shiftedDcId % kDcShift);\n}\n\nconstexpr ShiftedDcId ShiftDcId(DcId dcId, int value) {\n\treturn dcId + kDcShift * value;\n}\n\nconstexpr int GetDcIdShift(ShiftedDcId shiftedDcId) {\n\treturn shiftedDcId / kDcShift;\n}\n\n} // namespace MTP\n\nenum {\n\t// core types\n\tmtpc_int = tl::id_int,\n\tmtpc_long = tl::id_long,\n\tmtpc_int128 = tl::id_int128,\n\tmtpc_int256 = tl::id_int256,\n\tmtpc_double = tl::id_double,\n\tmtpc_string = tl::id_string,\n\tmtpc_vector = tl::id_vector,\n\tmtpc_bytes = tl::id_bytes,\n\tmtpc_flags = tl::id_flags,\n\tmtpc_flags64 = tl::id_flags64,\n\n\t// layers\n\tmtpc_invokeWithLayer1 = 0x53835315,\n\tmtpc_invokeWithLayer2 = 0x289dd1f6,\n\tmtpc_invokeWithLayer3 = 0xb7475268,\n\tmtpc_invokeWithLayer4 = 0xdea0d430,\n\tmtpc_invokeWithLayer5 = 0x417a57ae,\n\tmtpc_invokeWithLayer6 = 0x3a64d54d,\n\tmtpc_invokeWithLayer7 = 0xa5be56d3,\n\tmtpc_invokeWithLayer8 = 0xe9abd9fd,\n\tmtpc_invokeWithLayer9 = 0x76715a63,\n\tmtpc_invokeWithLayer10 = 0x39620c41,\n\tmtpc_invokeWithLayer11 = 0xa6b88fdf,\n\tmtpc_invokeWithLayer12 = 0xdda60d3c,\n\tmtpc_invokeWithLayer13 = 0x427c8ea2,\n\tmtpc_invokeWithLayer14 = 0x2b9b08fa,\n\tmtpc_invokeWithLayer15 = 0xb4418b64,\n\tmtpc_invokeWithLayer16 = 0xcf5f0987,\n\tmtpc_invokeWithLayer17 = 0x50858a19,\n\tmtpc_invokeWithLayer18 = 0x1c900537,\n\n\t// manually parsed\n\tmtpc_rpc_result = 0xf35c6d01,\n\tmtpc_msg_container = 0x73f1f8dc,\n//\tmtpc_msg_copy = 0xe06046b2,\n\tmtpc_gzip_packed = 0x3072cfa1\n};\nstatic const mtpTypeId mtpc_core_message = -1; // undefined type, but is used\nstatic const mtpTypeId mtpLayers[] = {\n\tmtpTypeId(mtpc_invokeWithLayer1),\n\tmtpTypeId(mtpc_invokeWithLayer2),\n\tmtpTypeId(mtpc_invokeWithLayer3),\n\tmtpTypeId(mtpc_invokeWithLayer4),\n\tmtpTypeId(mtpc_invokeWithLayer5),\n\tmtpTypeId(mtpc_invokeWithLayer6),\n\tmtpTypeId(mtpc_invokeWithLayer7),\n\tmtpTypeId(mtpc_invokeWithLayer8),\n\tmtpTypeId(mtpc_invokeWithLayer9),\n\tmtpTypeId(mtpc_invokeWithLayer10),\n\tmtpTypeId(mtpc_invokeWithLayer11),\n\tmtpTypeId(mtpc_invokeWithLayer12),\n\tmtpTypeId(mtpc_invokeWithLayer13),\n\tmtpTypeId(mtpc_invokeWithLayer14),\n\tmtpTypeId(mtpc_invokeWithLayer15),\n\tmtpTypeId(mtpc_invokeWithLayer16),\n\tmtpTypeId(mtpc_invokeWithLayer17),\n\tmtpTypeId(mtpc_invokeWithLayer18),\n};\nstatic const uint32 mtpLayerMaxSingle = sizeof(mtpLayers) / sizeof(mtpLayers[0]);\n\nusing MTPint = tl::int_type;\n\ninline MTPint MTP_int(int32 v) {\n\treturn tl::make_int(v);\n}\n\ntemplate <typename Flags>\nusing MTPflags = tl::flags_type<Flags>;\n\ntemplate <typename T>\ninline MTPflags<base::flags<T>> MTP_flags(base::flags<T> v) {\n\treturn tl::make_flags(v);\n}\n\ntemplate <typename T, typename = std::enable_if_t<!std::is_same<T, int>::value>>\ninline MTPflags<base::flags<T>> MTP_flags(T v) {\n\treturn tl::make_flags(v);\n}\n\ninline tl::details::zero_flags_helper MTP_flags(void(tl::details::zero_flags_helper::*)()) {\n\treturn tl::details::zero_flags_helper();\n}\n\nusing MTPlong = tl::long_type;\n\ninline MTPlong MTP_long(uint64 v) {\n\treturn tl::make_long(v);\n}\n\nusing MTPint128 = tl::int128_type;\n\ninline MTPint128 MTP_int128(uint64 l, uint64 h) {\n\treturn tl::make_int128(l, h);\n}\n\nusing MTPint256 = tl::int256_type;\n\ninline MTPint256 MTP_int256(const MTPint128 &l, const MTPint128 &h) {\n\treturn tl::make_int256(l, h);\n}\n\nusing MTPdouble = tl::double_type;\n\ninline MTPdouble MTP_double(float64 v) {\n\treturn tl::make_double(v);\n}\n\nusing MTPstring = tl::string_type;\nusing MTPbytes = tl::bytes_type;\n\ninline MTPstring MTP_string(const std::string &v) {\n\treturn tl::make_string(v);\n}\ninline MTPstring MTP_string(const QString &v) {\n\treturn tl::make_string(v);\n}\ninline MTPstring MTP_string(const char *v) {\n\treturn tl::make_string(v);\n}\ninline MTPstring MTP_string() {\n\treturn tl::make_string();\n}\nMTPstring MTP_string(const QByteArray &v) = delete;\n\ninline MTPbytes MTP_bytes(const QByteArray &v) {\n\treturn tl::make_bytes(v);\n}\ninline MTPbytes MTP_bytes(QByteArray &&v) {\n\treturn tl::make_bytes(std::move(v));\n}\ninline MTPbytes MTP_bytes() {\n\treturn tl::make_bytes();\n}\ninline MTPbytes MTP_bytes(bytes::const_span buffer) {\n\treturn tl::make_bytes(buffer);\n}\ninline MTPbytes MTP_bytes(const bytes::vector &buffer) {\n\treturn tl::make_bytes(buffer);\n}\n\ninline QString qs(const MTPstring &v) {\n\treturn tl::utf16(v);\n}\n\ninline QString qs(const QByteArray &v) {\n\treturn tl::utf16(v);\n}\n\ninline QByteArray qba(const MTPstring &v) {\n\treturn tl::utf8(v);\n}\n\ntemplate <typename T>\nusing MTPvector = tl::vector_type<T>;\n\ntemplate <typename T>\ninline MTPvector<T> MTP_vector(uint32 count) {\n\treturn tl::make_vector<T>(count);\n}\ntemplate <typename T>\ninline MTPvector<T> MTP_vector(uint32 count, const T &value) {\n\treturn tl::make_vector<T>(count, value);\n}\ntemplate <typename T>\ninline MTPvector<T> MTP_vector(const QVector<T> &v) {\n\treturn tl::make_vector<T>(v);\n}\ntemplate <typename T>\ninline MTPvector<T> MTP_vector(QVector<T> &&v) {\n\treturn tl::make_vector<T>(std::move(v));\n}\ntemplate <typename T>\ninline MTPvector<T> MTP_vector() {\n\treturn tl::make_vector<T>();\n}\n\n// ranges::to<QVector> doesn't work with Qt 6 in Clang,\n// because QVector is a type alias for QList there.\ntemplate <typename Rng>\ninline auto MTP_vector_from_range(Rng &&range) {\n\tusing T = std::remove_cvref_t<decltype(*ranges::begin(range))>;\n#if QT_VERSION >= QT_VERSION_CHECK(6, 0 ,0)\n\treturn MTP_vector<T>(std::forward<Rng>(range) | ranges::to<QList>());\n#else // QT_VERSION >= 6.0\n\treturn MTP_vector<T>(std::forward<Rng>(range) | ranges::to<QVector>());\n#endif // QT_VERSION < 6.0\n}\n\nnamespace tl {\n\ntemplate <typename Accumulator>\nstruct Writer;\n\ntemplate <typename Prime>\nstruct Reader;\n\ntemplate <>\nstruct Writer<mtpBuffer> {\n\tstatic void PutBytes(mtpBuffer &to, const void *bytes, uint32 count) {\n\t\tconstexpr auto kPrime = sizeof(uint32);\n\t\tconst auto primes = (count / kPrime) + (count % kPrime ? 1 : 0);\n\t\tconst auto size = to.size();\n\t\tto.resize(size + primes);\n\t\tmemcpy(to.data() + size, bytes, count);\n\t}\n\tstatic void Put(mtpBuffer &to, uint32 value) {\n\t\tto.push_back(mtpPrime(value));\n\t}\n};\n\ntemplate <>\nstruct Reader<mtpPrime> final {\n\t[[nodiscard]] static bool HasBytes(\n\t\t\tuint32 count,\n\t\t\tconst mtpPrime *from,\n\t\t\tconst mtpPrime *end) {\n\t\tconstexpr auto kPrime = sizeof(uint32);\n\t\tconst auto primes = (count / kPrime) + (count % kPrime ? 1 : 0);\n\t\treturn (end - from) >= primes;\n\t}\n\tstatic void GetBytes(\n\t\t\tvoid *bytes,\n\t\t\tuint32 count,\n\t\t\tconst mtpPrime *&from,\n\t\t\tconst mtpPrime *end) {\n\t\tExpects(HasBytes(count, from, end));\n\n\t\tconstexpr auto kPrime = sizeof(uint32);\n\t\tconst auto primes = (count / kPrime) + (count % kPrime ? 1 : 0);\n\t\tmemcpy(bytes, from, count);\n\t\tfrom += primes;\n\t}\n\t[[nodiscard]] static bool Has(\n\t\t\tuint32 primes,\n\t\t\tconst mtpPrime *from,\n\t\t\tconst mtpPrime *end) {\n\t\treturn (end - from) >= primes;\n\t}\n\t[[nodiscard]] static uint32 Get(const mtpPrime *&from, const mtpPrime *end) {\n\t\tExpects(from < end);\n\n\t\treturn uint32(*from++);\n\t}\n};\n\n} // namespace tl\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/dedicated_file_loader.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/dedicated_file_loader.h\"\n\n#include \"mtproto/facade.h\"\n#include \"main/main_account.h\" // Account::sessionChanges.\n#include \"main/main_session.h\" // Session::account.\n#include \"core/application.h\"\n#include \"base/call_delayed.h\"\n\nnamespace MTP {\nnamespace {\n\nstd::optional<MTPInputChannel> ExtractChannel(\n\t\tconst MTPcontacts_ResolvedPeer &result) {\n\tconst auto &data = result.c_contacts_resolvedPeer();\n\tif (const auto peer = peerFromMTP(data.vpeer())) {\n\t\tfor (const auto &chat : data.vchats().v) {\n\t\t\tif (chat.type() == mtpc_channel) {\n\t\t\t\tconst auto &channel = chat.c_channel();\n\t\t\t\tif (peer == peerFromChannel(channel.vid())) {\n\t\t\t\t\treturn MTP_inputChannel(\n\t\t\t\t\t\tchannel.vid(),\n\t\t\t\t\t\tMTP_long(channel.vaccess_hash().value_or_empty()));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nstd::optional<DedicatedLoader::File> ParseFile(\n\t\tconst MTPmessages_Messages &result) {\n\tconst auto message = GetMessagesElement(result);\n\tif (!message || message->type() != mtpc_message) {\n\t\tLOG((\"Update Error: MTP file message not found.\"));\n\t\treturn std::nullopt;\n\t}\n\tconst auto &data = message->c_message();\n\tconst auto media = data.vmedia();\n\tif (!media || media->type() != mtpc_messageMediaDocument) {\n\t\tLOG((\"Update Error: MTP file media not found.\"));\n\t\treturn std::nullopt;\n\t}\n\tconst auto &inner = media->c_messageMediaDocument();\n\tconst auto document = inner.vdocument();\n\tif (!document || document->type() != mtpc_document) {\n\t\tLOG((\"Update Error: MTP file not found.\"));\n\t\treturn std::nullopt;\n\t}\n\tconst auto &fields = document->c_document();\n\tconst auto name = [&] {\n\t\tfor (const auto &attribute : fields.vattributes().v) {\n\t\t\tif (attribute.type() == mtpc_documentAttributeFilename) {\n\t\t\t\tconst auto &data = attribute.c_documentAttributeFilename();\n\t\t\t\treturn qs(data.vfile_name());\n\t\t\t}\n\t\t}\n\t\treturn QString();\n\t}();\n\tif (name.isEmpty()) {\n\t\tLOG((\"Update Error: MTP file name not found.\"));\n\t\treturn std::nullopt;\n\t}\n\tconst auto size = int64(fields.vsize().v);\n\tif (size <= 0) {\n\t\tLOG((\"Update Error: MTP file size is invalid.\"));\n\t\treturn std::nullopt;\n\t}\n\tconst auto location = MTP_inputDocumentFileLocation(\n\t\tfields.vid(),\n\t\tfields.vaccess_hash(),\n\t\tfields.vfile_reference(),\n\t\tMTP_string());\n\treturn DedicatedLoader::File{ name, size, fields.vdc_id().v, location };\n}\n\n} // namespace\n\nWeakInstance::WeakInstance(base::weak_ptr<Main::Session> session)\n: _session(session)\n, _instance(_session ? &_session->account().mtp() : nullptr) {\n\tif (!valid()) {\n\t\treturn;\n\t}\n\n\tconnect(_instance, &QObject::destroyed, this, [=] {\n\t\t_instance = nullptr;\n\t\t_session = nullptr;\n\t\tdie();\n\t});\n\t_session->account().sessionChanges(\n\t) | rpl::filter([](Main::Session *session) {\n\t\treturn !session;\n\t}) | rpl::on_next([=] {\n\t\tdie();\n\t}, _lifetime);\n}\n\nbase::weak_ptr<Main::Session> WeakInstance::session() const {\n\treturn _session;\n}\n\nbool WeakInstance::valid() const {\n\treturn (_session != nullptr);\n}\n\nInstance *WeakInstance::instance() const {\n\treturn _instance;\n}\n\nvoid WeakInstance::die() {\n\tfor (const auto &[requestId, fail] : base::take(_requests)) {\n\t\tif (_instance) {\n\t\t\t_instance->cancel(requestId);\n\t\t}\n\t\tfail(Error::Local(\n\t\t\t\"UNAVAILABLE\",\n\t\t\t\"MTP instance is not available.\"));\n\t}\n}\n\nbool WeakInstance::removeRequest(mtpRequestId requestId) {\n\tif (const auto i = _requests.find(requestId); i != end(_requests)) {\n\t\t_requests.erase(i);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid WeakInstance::reportUnavailable(\n\t\tFn<void(const Error &error)> callback) {\n\tInvokeQueued(this, [=] {\n\t\tcallback(Error::Local(\n\t\t\t\"UNAVAILABLE\",\n\t\t\t\"MTP instance is not available.\"));\n\t});\n}\n\nWeakInstance::~WeakInstance() {\n\tif (_instance) {\n\t\tfor (const auto &[requestId, fail] : base::take(_requests)) {\n\t\t\t_instance->cancel(requestId);\n\t\t}\n\t}\n}\n\nAbstractDedicatedLoader::AbstractDedicatedLoader(\n\tconst QString &filepath,\n\tint chunkSize)\n: _filepath(filepath)\n, _chunkSize(chunkSize) {\n}\n\nvoid AbstractDedicatedLoader::start() {\n\tif (!validateOutput()\n\t\t|| (!_output.isOpen() && !_output.open(QIODevice::Append))) {\n\t\tQFile(_filepath).remove();\n\t\tthreadSafeFailed();\n\t\treturn;\n\t}\n\n\tLOG((\"Update Info: Starting loading '%1' from %2 offset.\"\n\t\t).arg(_filepath\n\t\t).arg(alreadySize()));\n\tstartLoading();\n}\n\nint64 AbstractDedicatedLoader::alreadySize() const {\n\tQMutexLocker lock(&_sizesMutex);\n\treturn _alreadySize;\n}\n\nint64 AbstractDedicatedLoader::totalSize() const {\n\tQMutexLocker lock(&_sizesMutex);\n\treturn _totalSize;\n}\n\nrpl::producer<QString> AbstractDedicatedLoader::ready() const {\n\treturn _ready.events();\n}\n\nauto AbstractDedicatedLoader::progress() const -> rpl::producer<Progress> {\n\treturn _progress.events();\n}\n\nrpl::producer<> AbstractDedicatedLoader::failed() const {\n\treturn _failed.events();\n}\n\nvoid AbstractDedicatedLoader::wipeFolder() {\n\tQFileInfo info(_filepath);\n\tconst auto dir = info.dir();\n\tconst auto all = dir.entryInfoList(QDir::Files);\n\tfor (auto i = all.begin(), e = all.end(); i != e; ++i) {\n\t\tif (i->absoluteFilePath() != info.absoluteFilePath()) {\n\t\t\tQFile::remove(i->absoluteFilePath());\n\t\t}\n\t}\n}\n\nbool AbstractDedicatedLoader::validateOutput() {\n\tif (_filepath.isEmpty()) {\n\t\treturn false;\n\t}\n\n\tQFileInfo info(_filepath);\n\tconst auto dir = info.dir();\n\tif (!dir.exists()) {\n\t\tdir.mkdir(dir.absolutePath());\n\t}\n\t_output.setFileName(_filepath);\n\n\tif (!info.exists()) {\n\t\treturn true;\n\t}\n\tconst auto fullSize = info.size();\n\tif (fullSize < _chunkSize || fullSize > kMaxFileSize) {\n\t\treturn _output.remove();\n\t}\n\tconst auto goodSize = int64((fullSize % _chunkSize)\n\t\t? (fullSize - (fullSize % _chunkSize))\n\t\t: fullSize);\n\tif (_output.resize(goodSize)) {\n\t\t_alreadySize = goodSize;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid AbstractDedicatedLoader::threadSafeProgress(Progress progress) {\n\tcrl::on_main(this, [=] {\n\t\t_progress.fire_copy(progress);\n\t});\n}\n\nvoid AbstractDedicatedLoader::threadSafeReady() {\n\tcrl::on_main(this, [=] {\n\t\t_ready.fire_copy(_filepath);\n\t});\n}\n\nvoid AbstractDedicatedLoader::threadSafeFailed() {\n\tcrl::on_main(this, [=] {\n\t\t_failed.fire({});\n\t});\n}\n\nvoid AbstractDedicatedLoader::writeChunk(bytes::const_span data, int totalSize) {\n\tconst auto size = data.size();\n\tif (size > 0) {\n\t\tconst auto written = _output.write(QByteArray::fromRawData(\n\t\t\treinterpret_cast<const char*>(data.data()),\n\t\t\tsize));\n\t\tif (written != size) {\n\t\t\tthreadSafeFailed();\n\t\t\treturn;\n\t\t}\n\t}\n\n\tconst auto progress = [&] {\n\t\tQMutexLocker lock(&_sizesMutex);\n\t\tif (!_totalSize) {\n\t\t\t_totalSize = totalSize;\n\t\t}\n\t\t_alreadySize += size;\n\t\treturn Progress { _alreadySize, _totalSize };\n\t}();\n\n\tif (progress.size > 0 && progress.already >= progress.size) {\n\t\t_output.close();\n\t\tthreadSafeReady();\n\t} else {\n\t\tthreadSafeProgress(progress);\n\t}\n}\n\nrpl::lifetime &AbstractDedicatedLoader::lifetime() {\n\treturn _lifetime;\n}\n\nDedicatedLoader::DedicatedLoader(\n\tbase::weak_ptr<Main::Session> session,\n\tconst QString &folder,\n\tconst File &file)\n: AbstractDedicatedLoader(folder + '/' + file.name, kChunkSize)\n, _size(file.size)\n, _dcId(file.dcId)\n, _location(file.location)\n, _mtp(session) {\n\tExpects(_size > 0);\n}\n\nvoid DedicatedLoader::startLoading() {\n\tif (!_mtp.valid()) {\n\t\tLOG((\"Update Error: MTP is unavailable.\"));\n\t\tthreadSafeFailed();\n\t\treturn;\n\t}\n\n\tLOG((\"Update Info: Loading using MTP from '%1'.\").arg(_dcId));\n\t_offset = alreadySize();\n\twriteChunk({}, _size);\n\tsendRequest();\n}\n\nvoid DedicatedLoader::sendRequest() {\n\tif (_requests.size() >= kRequestsCount || _offset >= _size) {\n\t\treturn;\n\t}\n\tconst auto offset = _offset;\n\t_requests.push_back({ offset });\n\t_mtp.send(\n\t\tMTPupload_GetFile(\n\t\t\tMTP_flags(0),\n\t\t\t_location,\n\t\t\tMTP_long(offset),\n\t\t\tMTP_int(kChunkSize)),\n\t\t[=](const MTPupload_File &result) { gotPart(offset, result); },\n\t\tfailHandler(),\n\t\tMTP::updaterDcId(_dcId));\n\t_offset += kChunkSize;\n\n\tif (_requests.size() < kRequestsCount) {\n\t\tbase::call_delayed(kNextRequestDelay, this, [=] { sendRequest(); });\n\t}\n}\n\nvoid DedicatedLoader::gotPart(int offset, const MTPupload_File &result) {\n\tExpects(!_requests.empty());\n\n\tif (result.type() == mtpc_upload_fileCdnRedirect) {\n\t\tLOG((\"Update Error: MTP does not support cdn right now.\"));\n\t\tthreadSafeFailed();\n\t\treturn;\n\t}\n\tconst auto &data = result.c_upload_file();\n\tif (data.vbytes().v.isEmpty()) {\n\t\tLOG((\"Update Error: MTP empty part received.\"));\n\t\tthreadSafeFailed();\n\t\treturn;\n\t}\n\n\tconst auto i = ranges::find(\n\t\t_requests,\n\t\toffset,\n\t\t[](const Request &request) { return request.offset; });\n\tAssert(i != end(_requests));\n\n\ti->bytes = data.vbytes().v;\n\twhile (!_requests.empty() && !_requests.front().bytes.isEmpty()) {\n\t\twriteChunk(bytes::make_span(_requests.front().bytes), _size);\n\t\t_requests.pop_front();\n\t}\n\tsendRequest();\n}\n\nFn<void(const Error &)> DedicatedLoader::failHandler() {\n\treturn [=](const Error &error) {\n\t\tLOG((\"Update Error: MTP load failed with '%1'\"\n\t\t\t).arg(QString::number(error.code()) + ':' + error.type()));\n\t\tthreadSafeFailed();\n\t};\n}\n\nvoid ResolveChannel(\n\t\tnot_null<MTP::WeakInstance*> mtp,\n\t\tconst QString &username,\n\t\tFn<void(const MTPInputChannel &channel)> done,\n\t\tFn<void()> fail) {\n\tconst auto failed = [&] {\n\t\tLOG((\"Dedicated MTP Error: Channel '%1' resolve failed.\"\n\t\t\t).arg(username));\n\t\tfail();\n\t};\n\tconst auto session = mtp->session();\n\tif (!mtp->valid()) {\n\t\tfailed();\n\t\treturn;\n\t}\n\n\tstruct ResolveResult {\n\t\tbase::weak_ptr<Main::Session> session;\n\t\tMTPInputChannel channel;\n\t};\n\tstatic std::map<QString, ResolveResult> ResolveCache;\n\n\tconst auto i = ResolveCache.find(username);\n\tif (i != end(ResolveCache)) {\n\t\tif (i->second.session.get() == session.get()) {\n\t\t\tdone(i->second.channel);\n\t\t\treturn;\n\t\t}\n\t\tResolveCache.erase(i);\n\t}\n\n\tconst auto doneHandler = [=](const MTPcontacts_ResolvedPeer &result) {\n\t\tExpects(result.type() == mtpc_contacts_resolvedPeer);\n\n\t\tif (const auto channel = ExtractChannel(result)) {\n\t\t\tResolveCache.emplace(\n\t\t\t\tusername,\n\t\t\t\tResolveResult { session, *channel });\n\t\t\tdone(*channel);\n\t\t} else {\n\t\t\tfailed();\n\t\t}\n\t};\n\tconst auto failHandler = [=](const Error &error) {\n\t\tLOG((\"Dedicated MTP Error: Resolve failed with '%1'\"\n\t\t\t).arg(QString::number(error.code()) + ':' + error.type()));\n\t\tfail();\n\t};\n\tmtp->send(MTPcontacts_ResolveUsername(\n\t\tMTP_flags(0),\n\t\tMTP_string(username),\n\t\tMTP_string()\n\t), doneHandler, failHandler);\n}\n\nstd::optional<MTPMessage> GetMessagesElement(\n\t\tconst MTPmessages_Messages &list) {\n\treturn list.match([&](const MTPDmessages_messagesNotModified &) {\n\t\treturn std::optional<MTPMessage>(std::nullopt);\n\t}, [&](const auto &data) {\n\t\treturn data.vmessages().v.isEmpty()\n\t\t\t? std::nullopt\n\t\t\t: std::make_optional(data.vmessages().v[0]);\n\t});\n}\n\nvoid StartDedicatedLoader(\n\t\tnot_null<MTP::WeakInstance*> mtp,\n\t\tconst DedicatedLoader::Location &location,\n\t\tconst QString &folder,\n\t\tFn<void(std::unique_ptr<DedicatedLoader>)> ready) {\n\tconst auto doneHandler = [=](const MTPmessages_Messages &result) {\n\t\tconst auto file = ParseFile(result);\n\t\tready(file\n\t\t\t? std::make_unique<MTP::DedicatedLoader>(\n\t\t\t\tmtp->session(),\n\t\t\t\tfolder,\n\t\t\t\t*file)\n\t\t\t: nullptr);\n\t};\n\tconst auto failHandler = [=](const Error &error) {\n\t\tLOG((\"Update Error: MTP check failed with '%1'\"\n\t\t\t).arg(QString::number(error.code()) + ':' + error.type()));\n\t\tready(nullptr);\n\t};\n\n\tconst auto &[username, postId] = location;\n\tResolveChannel(mtp, username, [=, postId = postId](\n\t\t\tconst MTPInputChannel &channel) {\n\t\tmtp->send(\n\t\t\tMTPchannels_GetMessages(\n\t\t\t\tchannel,\n\t\t\t\tMTP_vector<MTPInputMessage>(\n\t\t\t\t\t1,\n\t\t\t\t\tMTP_inputMessageID(MTP_int(postId)))),\n\t\t\tdoneHandler,\n\t\t\tfailHandler);\n\t}, [=] { ready(nullptr); });\n}\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/dedicated_file_loader.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/mtp_instance.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace MTP {\n\nclass WeakInstance final : private QObject {\npublic:\n\texplicit WeakInstance(base::weak_ptr<Main::Session> session);\n\n\ttemplate <typename Request>\n\tvoid send(\n\t\tconst Request &request,\n\t\tFn<void(const typename Request::ResponseType &result)> done,\n\t\tFn<void(const Error &error)> fail,\n\t\tShiftedDcId dcId = 0);\n\n\t[[nodiscard]] base::weak_ptr<Main::Session> session() const;\n\t[[nodiscard]] bool valid() const;\n\t[[nodiscard]] Instance *instance() const;\n\n\t~WeakInstance();\n\nprivate:\n\tvoid die();\n\tbool removeRequest(mtpRequestId requestId);\n\tvoid reportUnavailable(Fn<void(const Error &error)> callback);\n\n\tbase::weak_ptr<Main::Session> _session;\n\tInstance *_instance = nullptr;\n\tstd::map<mtpRequestId, Fn<void(const Error &)>> _requests;\n\trpl::lifetime _lifetime;\n\n};\n\nclass AbstractDedicatedLoader : public base::has_weak_ptr {\npublic:\n\tAbstractDedicatedLoader(const QString &filepath, int chunkSize);\n\n\tstatic constexpr auto kChunkSize = 128 * 1024;\n\tstatic constexpr auto kMaxFileSize = 256 * 1024 * 1024;\n\n\tstruct Progress {\n\t\tint64 already = 0;\n\t\tint64 size = 0;\n\n\t\tinline bool operator<(const Progress &other) const {\n\t\t\treturn (already < other.already)\n\t\t\t\t|| (already == other.already && size < other.size);\n\t\t}\n\t\tinline bool operator==(const Progress &other) const {\n\t\t\treturn (already == other.already) && (size == other.size);\n\t\t}\n\t};\n\n\tvoid start();\n\tvoid wipeFolder();\n\tvoid wipeOutput();\n\n\tint64 alreadySize() const;\n\tint64 totalSize() const;\n\n\trpl::producer<Progress> progress() const;\n\trpl::producer<QString> ready() const;\n\trpl::producer<> failed() const;\n\n\trpl::lifetime &lifetime();\n\n\tvirtual ~AbstractDedicatedLoader() = default;\n\nprotected:\n\tvoid threadSafeFailed();\n\n\t// Single threaded.\n\tvoid writeChunk(bytes::const_span data, int totalSize);\n\nprivate:\n\tvirtual void startLoading() = 0;\n\n\tbool validateOutput();\n\tvoid threadSafeProgress(Progress progress);\n\tvoid threadSafeReady();\n\n\tQString _filepath;\n\tint _chunkSize = 0;\n\n\tQFile _output;\n\tint64 _alreadySize = 0;\n\tint64 _totalSize = 0;\n\tmutable QMutex _sizesMutex;\n\trpl::event_stream<Progress> _progress;\n\trpl::event_stream<QString> _ready;\n\trpl::event_stream<> _failed;\n\n\trpl::lifetime _lifetime;\n\n};\n\nclass DedicatedLoader : public AbstractDedicatedLoader {\npublic:\n\tstruct Location {\n\t\tQString username;\n\t\tint32 postId = 0;\n\t};\n\tstruct File {\n\t\tQString name;\n\t\tint64 size = 0;\n\t\tDcId dcId = 0;\n\t\tMTPInputFileLocation location;\n\t};\n\n\tDedicatedLoader(\n\t\tbase::weak_ptr<Main::Session> session,\n\t\tconst QString &folder,\n\t\tconst File &file);\n\nprivate:\n\tstruct Request {\n\t\tint64 offset = 0;\n\t\tQByteArray bytes;\n\t};\n\tvoid startLoading() override;\n\tvoid sendRequest();\n\tvoid gotPart(int offset, const MTPupload_File &result);\n\tFn<void(const Error &)> failHandler();\n\n\tstatic constexpr auto kRequestsCount = 2;\n\tstatic constexpr auto kNextRequestDelay = crl::time(20);\n\n\tstd::deque<Request> _requests;\n\tint64 _size = 0;\n\tint64 _offset = 0;\n\tDcId _dcId = 0;\n\tMTPInputFileLocation _location;\n\tWeakInstance _mtp;\n\n};\n\nvoid ResolveChannel(\n\tnot_null<MTP::WeakInstance*> mtp,\n\tconst QString &username,\n\tFn<void(const MTPInputChannel &channel)> done,\n\tFn<void()> fail);\n\nstd::optional<MTPMessage> GetMessagesElement(\n\tconst MTPmessages_Messages &list);\n\nvoid StartDedicatedLoader(\n\tnot_null<MTP::WeakInstance*> mtp,\n\tconst DedicatedLoader::Location &location,\n\tconst QString &folder,\n\tFn<void(std::unique_ptr<DedicatedLoader>)> ready);\n\ntemplate <typename Request>\nvoid WeakInstance::send(\n\t\tconst Request &request,\n\t\tFn<void(const typename Request::ResponseType &result)> done,\n\t\tFn<void(const Error &error)> fail,\n\t\tMTP::ShiftedDcId dcId) {\n\tusing Result = typename Request::ResponseType;\n\tif (!valid()) {\n\t\treportUnavailable(fail);\n\t\treturn;\n\t}\n\tconst auto onDone = crl::guard((QObject*)this, [=](\n\t\t\tconst Response &response) {\n\t\tauto result = Result();\n\t\tauto from = response.reply.constData();\n\t\tif (!result.read(from, from + response.reply.size())) {\n\t\t\treturn false;\n\t\t}\n\t\tif (removeRequest(response.requestId)) {\n\t\t\tdone(result);\n\t\t}\n\t\treturn true;\n\t});\n\tconst auto onFail = crl::guard((QObject*)this, [=](\n\t\t\tconst Error &error,\n\t\t\tconst Response &response) {\n\t\tif (MTP::IsDefaultHandledError(error)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (removeRequest(response.requestId)) {\n\t\t\tfail(error);\n\t\t}\n\t\treturn true;\n\t});\n\tconst auto requestId = _instance->send(\n\t\trequest,\n\t\tstd::move(onDone),\n\t\tstd::move(onFail),\n\t\tdcId);\n\t_requests.emplace(requestId, fail);\n}\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_abstract_socket.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/details/mtproto_abstract_socket.h\"\n\n#include \"mtproto/details/mtproto_tcp_socket.h\"\n#include \"mtproto/details/mtproto_tls_socket.h\"\n\nnamespace MTP::details {\n\nstd::unique_ptr<AbstractSocket> AbstractSocket::Create(\n\t\tnot_null<QThread*> thread,\n\t\tconst bytes::vector &secret,\n\t\tconst QNetworkProxy &proxy,\n\t\tbool protocolForFiles) {\n\tif (secret.size() >= 21 && secret[0] == bytes::type(0xEE)) {\n\t\treturn std::make_unique<TlsSocket>(\n\t\t\tthread,\n\t\t\tsecret,\n\t\t\tproxy,\n\t\t\tprotocolForFiles);\n\t} else {\n\t\treturn std::make_unique<TcpSocket>(thread, proxy, protocolForFiles);\n\t}\n}\n\nvoid AbstractSocket::logError(int errorCode, const QString &errorText) {\n\tconst auto log = [&](const QString &message) {\n\t\tDEBUG_LOG((\"Socket %1 Error: \").arg(_debugId) + message);\n\t};\n\tswitch (errorCode) {\n\tcase QAbstractSocket::ConnectionRefusedError:\n\t\tlog(u\"Socket connection refused - %1.\"_q.arg(errorText));\n\t\tbreak;\n\n\tcase QAbstractSocket::RemoteHostClosedError:\n\t\tlog(u\"Remote host closed socket connection - %1.\"_q.arg(errorText));\n\t\tbreak;\n\n\tcase QAbstractSocket::HostNotFoundError:\n\t\tlog(u\"Host not found - %1.\"_q.arg(errorText));\n\t\tbreak;\n\n\tcase QAbstractSocket::SocketTimeoutError:\n\t\tlog(u\"Socket timeout - %1.\"_q.arg(errorText));\n\t\tbreak;\n\n\tcase QAbstractSocket::NetworkError: {\n\t\tlog(u\"Network - %1.\"_q.arg(errorText));\n\t} break;\n\n\tcase QAbstractSocket::ProxyAuthenticationRequiredError:\n\tcase QAbstractSocket::ProxyConnectionRefusedError:\n\tcase QAbstractSocket::ProxyConnectionClosedError:\n\tcase QAbstractSocket::ProxyConnectionTimeoutError:\n\tcase QAbstractSocket::ProxyNotFoundError:\n\tcase QAbstractSocket::ProxyProtocolError:\n\t\tlog(u\"Proxy (%1) - %2.\"_q.arg(errorCode).arg(errorText));\n\t\tbreak;\n\n\tdefault:\n\t\tlog(u\"Other (%1) - %2.\"_q.arg(errorCode).arg(errorText));\n\t\tbreak;\n\t}\n}\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_abstract_socket.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/bytes.h\"\n#include \"base/basic_types.h\"\n\nnamespace MTP::details {\n\nclass AbstractSocket : protected QObject {\npublic:\n\tstatic std::unique_ptr<AbstractSocket> Create(\n\t\tnot_null<QThread*> thread,\n\t\tconst bytes::vector &secret,\n\t\tconst QNetworkProxy &proxy,\n\t\tbool protocolForFiles);\n\n\tvoid setDebugId(const QString &id) {\n\t\t_debugId = id;\n\t}\n\n\texplicit AbstractSocket(not_null<QThread*> thread) {\n\t\tmoveToThread(thread);\n\t}\n\tvirtual ~AbstractSocket() = default;\n\n\t[[nodiscard]] rpl::producer<> connected() const {\n\t\treturn _connected.events();\n\t}\n\t[[nodiscard]] rpl::producer<> disconnected() const {\n\t\treturn _disconnected.events();\n\t}\n\t[[nodiscard]] rpl::producer<> readyRead() const {\n\t\treturn _readyRead.events();\n\t}\n\t[[nodiscard]] rpl::producer<> error() const {\n\t\treturn _error.events();\n\t}\n\t[[nodiscard]] rpl::producer<> syncTimeRequests() const {\n\t\treturn _syncTimeRequests.events();\n\t}\n\n\tvirtual void connectToHost(const QString &address, int port) = 0;\n\t[[nodiscard]] virtual bool isGoodStartNonce(bytes::const_span nonce) = 0;\n\tvirtual void timedOut() = 0;\n\t[[nodiscard]] virtual bool isConnected() = 0;\n\t[[nodiscard]] virtual bool hasBytesAvailable() = 0;\n\t[[nodiscard]] virtual int64 read(bytes::span buffer) = 0;\n\tvirtual void write(\n\t\tbytes::const_span prefix,\n\t\tbytes::const_span buffer) = 0;\n\n\tvirtual int32 debugState() = 0;\n\t[[nodiscard]] virtual QString debugPostfix() const = 0;\n\nprotected:\n\tstatic const int kFilesSendBufferSize = 2 * 1024 * 1024;\n\tstatic const int kFilesReceiveBufferSize = 2 * 1024 * 1024;\n\n\tvoid logError(int errorCode, const QString &errorText);\n\n\tQString _debugId;\n\trpl::event_stream<> _connected;\n\trpl::event_stream<> _disconnected;\n\trpl::event_stream<> _readyRead;\n\trpl::event_stream<> _error;\n\trpl::event_stream<> _syncTimeRequests;\n\n};\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_bound_key_creator.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/details/mtproto_bound_key_creator.h\"\n\n#include \"mtproto/details/mtproto_serialized_request.h\"\n\nnamespace MTP::details {\n\nBoundKeyCreator::BoundKeyCreator(DcKeyRequest request, Delegate delegate)\n: _request(request)\n, _delegate(std::move(delegate)) {\n}\n\nvoid BoundKeyCreator::start(\n\t\tDcId dcId,\n\t\tint16 protocolDcId,\n\t\tnot_null<AbstractConnection*> connection,\n\t\tnot_null<DcOptions*> dcOptions) {\n\tExpects(!_creator.has_value());\n\n\tauto delegate = DcKeyCreator::Delegate();\n\tdelegate.done = _delegate.unboundReady;\n\tdelegate.sentSome = _delegate.sentSome;\n\tdelegate.receivedSome = _delegate.receivedSome;\n\n\t_creator.emplace(\n\t\tdcId,\n\t\tprotocolDcId,\n\t\tconnection,\n\t\tdcOptions,\n\t\tstd::move(delegate),\n\t\t_request);\n}\n\nvoid BoundKeyCreator::stop() {\n\t_creator = std::nullopt;\n}\n\nvoid BoundKeyCreator::bind(AuthKeyPtr &&persistentKey) {\n\tstop();\n\t_binder.emplace(std::move(persistentKey));\n}\n\nvoid BoundKeyCreator::restartBinder() {\n\tif (_binder) {\n\t\t_binder.emplace(_binder->persistentKey());\n\t}\n}\n\nbool BoundKeyCreator::readyToBind() const {\n\treturn _binder.has_value();\n}\n\nSerializedRequest BoundKeyCreator::prepareBindRequest(\n\t\tconst AuthKeyPtr &temporaryKey,\n\t\tuint64 sessionId) {\n\tExpects(_binder.has_value());\n\n\treturn _binder->prepareRequest(temporaryKey, sessionId);\n}\n\nDcKeyBindState BoundKeyCreator::handleBindResponse(\n\t\tconst mtpBuffer &response) {\n\tExpects(_binder.has_value());\n\n\treturn _binder->handleResponse(response);\n}\n\nAuthKeyPtr BoundKeyCreator::bindPersistentKey() const {\n\tExpects(_binder.has_value());\n\n\treturn _binder->persistentKey();\n}\n\nbool IsDestroyedTemporaryKeyError(const mtpBuffer &buffer) {\n\tauto from = buffer.data();\n\tauto error = MTPRpcError();\n\tif (!error.read(from, from + buffer.size())) {\n\t\treturn false;\n\t}\n\treturn error.match([&](const MTPDrpc_error &data) {\n\t\treturn (data.verror_code().v == 401)\n\t\t\t&& (data.verror_message().v == \"AUTH_KEY_PERM_EMPTY\");\n\t});\n}\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_bound_key_creator.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/details/mtproto_dc_key_creator.h\"\n#include \"mtproto/details/mtproto_dc_key_binder.h\"\n\nnamespace MTP::details {\n\nclass SerializedRequest;\n\nclass BoundKeyCreator final {\npublic:\n\tstruct Delegate {\n\t\tFn<void(base::expected<DcKeyResult, DcKeyError>)> unboundReady;\n\t\tFn<void(uint64)> sentSome;\n\t\tFn<void()> receivedSome;\n\t};\n\n\tBoundKeyCreator(DcKeyRequest request, Delegate delegate);\n\n\tvoid start(\n\t\tDcId dcId,\n\t\tint16 protocolDcId,\n\t\tnot_null<AbstractConnection*> connection,\n\t\tnot_null<DcOptions*> dcOptions);\n\tvoid stop();\n\n\tvoid bind(AuthKeyPtr &&persistentKey);\n\tvoid restartBinder();\n\t[[nodiscard]] bool readyToBind() const;\n\t[[nodiscard]] SerializedRequest prepareBindRequest(\n\t\tconst AuthKeyPtr &temporaryKey,\n\t\tuint64 sessionId);\n\t[[nodiscard]] DcKeyBindState handleBindResponse(\n\t\tconst mtpBuffer &response);\n\t[[nodiscard]] AuthKeyPtr bindPersistentKey() const;\n\nprivate:\n\tconst DcKeyRequest _request;\n\tDelegate _delegate;\n\n\tstd::optional<DcKeyCreator> _creator;\n\tstd::optional<DcKeyBinder> _binder;\n\n};\n\n\n[[nodiscard]] bool IsDestroyedTemporaryKeyError(const mtpBuffer &buffer);\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_dc_key_binder.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/details/mtproto_dc_key_binder.h\"\n\n#include \"mtproto/details/mtproto_serialized_request.h\"\n#include \"mtproto/mtp_instance.h\"\n#include \"base/unixtime.h\"\n#include \"base/openssl_help.h\"\n#include \"base/random.h\"\n#include \"scheme.h\"\n\n#include <QtCore/QPointer>\n\nnamespace MTP::details {\nnamespace {\n\n[[nodiscard]] QByteArray EncryptBindAuthKeyInner(\n\t\tconst AuthKeyPtr &persistentKey,\n\t\tmtpMsgId realMsgId,\n\t\tconst MTPBindAuthKeyInner &data) {\n\tauto serialized = SerializedRequest::Serialize(data);\n\tserialized.setMsgId(realMsgId);\n\tserialized.setSeqNo(0);\n\tserialized.addPadding(true);\n\n\tconstexpr auto kMsgIdPosition = SerializedRequest::kMessageIdPosition;\n\tconstexpr auto kMinMessageSize = 5;\n\n\tconst auto sizeInPrimes = serialized->size();\n\tconst auto messageSize = serialized.messageSize();\n\tAssert(messageSize >= kMinMessageSize);\n\tAssert(sizeInPrimes >= kMsgIdPosition + messageSize);\n\n\tconst auto sizeInBytes = sizeInPrimes * sizeof(mtpPrime);\n\tconst auto padding = sizeInBytes\n\t\t- (kMsgIdPosition + messageSize) * sizeof(mtpPrime);\n\n\t// session_id, salt - just random here.\n\tbytes::set_random(bytes::make_span(*serialized).subspan(\n\t\t0,\n\t\tkMsgIdPosition * sizeof(mtpPrime)));\n\n\tconst auto hash = openssl::Sha1(bytes::make_span(*serialized).subspan(\n\t\t0,\n\t\tsizeInBytes - padding));\n\tauto msgKey = MTPint128();\n\tbytes::copy(\n\t\tbytes::object_as_span(&msgKey),\n\t\tbytes::make_span(hash).subspan(4));\n\n\tconstexpr auto kAuthKeyIdBytes = 2 * sizeof(mtpPrime);\n\tconstexpr auto kMessageKeyPosition = kAuthKeyIdBytes;\n\tconstexpr auto kMessageKeyBytes = 4 * sizeof(mtpPrime);\n\tconstexpr auto kPrefix = (kAuthKeyIdBytes + kMessageKeyBytes);\n\tauto encrypted = QByteArray(kPrefix + sizeInBytes, Qt::Uninitialized);\n\t*reinterpret_cast<uint64*>(encrypted.data()) = persistentKey->keyId();\n\t*reinterpret_cast<MTPint128*>(encrypted.data() + kMessageKeyPosition)\n\t\t= msgKey;\n\n\taesIgeEncrypt_oldmtp(\n\t\tserialized->constData(),\n\t\tencrypted.data() + kPrefix,\n\t\tsizeInBytes,\n\t\tpersistentKey,\n\t\tmsgKey);\n\n\treturn encrypted;\n}\n\n} // namespace\n\nDcKeyBinder::DcKeyBinder(AuthKeyPtr &&persistentKey)\n: _persistentKey(std::move(persistentKey)) {\n\tExpects(_persistentKey != nullptr);\n}\n\nSerializedRequest DcKeyBinder::prepareRequest(\n\t\tconst AuthKeyPtr &temporaryKey,\n\t\tuint64 sessionId) {\n\tExpects(temporaryKey != nullptr);\n\tExpects(temporaryKey->expiresAt() != 0);\n\n\tconst auto nonce = base::RandomValue<uint64>();\n\tconst auto msgId = base::unixtime::mtproto_msg_id();\n\tauto result = SerializedRequest::Serialize(MTPauth_BindTempAuthKey(\n\t\tMTP_long(_persistentKey->keyId()),\n\t\tMTP_long(nonce),\n\t\tMTP_int(temporaryKey->expiresAt()),\n\t\tMTP_bytes(EncryptBindAuthKeyInner(\n\t\t\t_persistentKey,\n\t\t\tmsgId,\n\t\t\tMTP_bind_auth_key_inner(\n\t\t\t\tMTP_long(nonce),\n\t\t\t\tMTP_long(temporaryKey->keyId()),\n\t\t\t\tMTP_long(_persistentKey->keyId()),\n\t\t\t\tMTP_long(sessionId),\n\t\t\t\tMTP_int(temporaryKey->expiresAt()))))));\n\tresult.setMsgId(msgId);\n\treturn result;\n}\n\nDcKeyBindState DcKeyBinder::handleResponse(const mtpBuffer &response) {\n\tExpects(!response.isEmpty());\n\n\tauto from = response.data();\n\tconst auto end = from + response.size();\n\tauto error = MTPRpcError();\n\tif (response[0] == mtpc_boolTrue) {\n\t\treturn DcKeyBindState::Success;\n\t} else if (response[0] == mtpc_rpc_error && error.read(from, end)) {\n\t\tconst auto destroyed = error.match([&](const MTPDrpc_error &data) {\n\t\t\treturn (data.verror_code().v == 400)\n\t\t\t\t&& (data.verror_message().v == \"ENCRYPTED_MESSAGE_INVALID\");\n\t\t});\n\t\treturn destroyed\n\t\t\t? DcKeyBindState::DefinitelyDestroyed\n\t\t\t: DcKeyBindState::Failed;\n\t} else {\n\t\treturn DcKeyBindState::Failed;\n\t}\n}\n\nAuthKeyPtr DcKeyBinder::persistentKey() const {\n\treturn _persistentKey;\n}\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_dc_key_binder.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/core_types.h\"\n#include \"mtproto/mtproto_auth_key.h\"\n\nnamespace MTP {\nclass Instance;\n} // namespace MTP\n\nnamespace MTP::details {\n\nclass SerializedRequest;\n\nenum class DcKeyBindState {\n\tSuccess,\n\tFailed,\n\tDefinitelyDestroyed,\n};\n\nclass DcKeyBinder final {\npublic:\n\texplicit DcKeyBinder(AuthKeyPtr &&persistentKey);\n\n\t[[nodiscard]] SerializedRequest prepareRequest(\n\t\tconst AuthKeyPtr &temporaryKey,\n\t\tuint64 sessionId);\n\t[[nodiscard]] DcKeyBindState handleResponse(const mtpBuffer &response);\n\t[[nodiscard]] AuthKeyPtr persistentKey() const;\n\nprivate:\n\tAuthKeyPtr _persistentKey;\n\n};\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_dc_key_creator.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/details/mtproto_dc_key_creator.h\"\n\n#include \"mtproto/details/mtproto_rsa_public_key.h\"\n#include \"mtproto/connection_abstract.h\"\n#include \"mtproto/mtproto_dh_utils.h\"\n#include \"base/openssl_help.h\"\n#include \"base/random.h\"\n#include \"base/unixtime.h\"\n#include \"scheme.h\"\n#include \"logs.h\"\n\n#include <cmath>\n\nnamespace MTP::details {\nnamespace {\n\nstruct ParsedPQ {\n\tQByteArray p;\n\tQByteArray q;\n};\n\n// Fast PQ factorization taken from TDLib:\n// https://github.com/tdlib/td/blob/v1.7.0/tdutils/td/utils/crypto.cpp\n[[nodiscard]] uint64 gcd(uint64 a, uint64 b) {\n\tif (a == 0) {\n\t\treturn b;\n\t} else if (b == 0) {\n\t\treturn a;\n\t}\n\n\tint shift = 0;\n\twhile ((a & 1) == 0 && (b & 1) == 0) {\n\t\ta >>= 1;\n\t\tb >>= 1;\n\t\tshift++;\n\t}\n\n\twhile (true) {\n\t\twhile ((a & 1) == 0) {\n\t\t\ta >>= 1;\n\t\t}\n\t\twhile ((b & 1) == 0) {\n\t\t\tb >>= 1;\n\t\t}\n\t\tif (a > b) {\n\t\t\ta -= b;\n\t\t} else if (b > a) {\n\t\t\tb -= a;\n\t\t} else {\n\t\t\treturn a << shift;\n\t\t}\n\t}\n}\n\n[[nodiscard]] uint64 FactorizeSmallPQ(uint64 pq) {\n\tif (pq < 2 || pq >(static_cast<uint64>(1) << 63)) {\n\t\treturn 1;\n\t}\n\tuint64 g = 0;\n\tfor (int i = 0, iter = 0; i < 3 || iter < 1000; i++) {\n\t\tuint64 q = (17 + base::RandomIndex(16)) % (pq - 1);\n\t\tuint64 x = base::RandomValue<uint64>() % (pq - 1) + 1;\n\t\tuint64 y = x;\n\t\tint lim = 1 << (std::min(5, i) + 18);\n\t\tfor (int j = 1; j < lim; j++) {\n\t\t\titer++;\n\t\t\tuint64 a = x;\n\t\t\tuint64 b = x;\n\t\t\tuint64 c = q;\n\n\t\t\t// c += a * b\n\t\t\twhile (b) {\n\t\t\t\tif (b & 1) {\n\t\t\t\t\tc += a;\n\t\t\t\t\tif (c >= pq) {\n\t\t\t\t\t\tc -= pq;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ta += a;\n\t\t\t\tif (a >= pq) {\n\t\t\t\t\ta -= pq;\n\t\t\t\t}\n\t\t\t\tb >>= 1;\n\t\t\t}\n\n\t\t\tx = c;\n\t\t\tuint64 z = x < y ? pq + x - y : x - y;\n\t\t\tg = gcd(z, pq);\n\t\t\tif (g != 1) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (!(j & (j - 1))) {\n\t\t\t\ty = x;\n\t\t\t}\n\t\t}\n\t\tif (g > 1 && g < pq) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (g != 0) {\n\t\tuint64 other = pq / g;\n\t\tif (other < g) {\n\t\t\tg = other;\n\t\t}\n\t}\n\treturn g;\n}\n\nParsedPQ FactorizeBigPQ(const QByteArray &pqStr) {\n\tusing namespace openssl;\n\n\tContext context;\n\tBigNum a;\n\tBigNum b;\n\tBigNum p;\n\tBigNum q;\n\tauto one = BigNum(1);\n\tauto pq = BigNum(bytes::make_span(pqStr));\n\n\tbool found = false;\n\tfor (int i = 0, iter = 0; !found && (i < 3 || iter < 1000); i++) {\n\t\tint32 t = 17 + base::RandomIndex(16);\n\t\ta.setWord(base::RandomValue<uint32>());\n\t\tb = a;\n\n\t\tint32 lim = 1 << (i + 23);\n\t\tfor (int j = 1; j < lim; j++) {\n\t\t\titer++;\n\t\t\ta.setModMul(a, a, pq, context);\n\t\t\ta.setAdd(a, BigNum(uint32(t)));\n\t\t\tif (BigNum::Compare(a, pq) >= 0) {\n\t\t\t\ta = BigNum::Sub(a, pq);\n\t\t\t}\n\t\t\tif (BigNum::Compare(a, b) > 0) {\n\t\t\t\tq.setSub(a, b);\n\t\t\t} else {\n\t\t\t\tq.setSub(b, a);\n\t\t\t}\n\t\t\tp.setGcd(q, pq, context);\n\t\t\tif (BigNum::Compare(p, one) != 0) {\n\t\t\t\tfound = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ((j & (j - 1)) == 0) {\n\t\t\t\tb = a;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!found) {\n\t\treturn ParsedPQ();\n\t}\n\tBigNum::Div(&q, nullptr, pq, p, context);\n\tif (BigNum::Compare(p, q) > 0) {\n\t\tstd::swap(p, q);\n\t}\n\n\tconst auto pb = p.getBytes();\n\tconst auto qb = q.getBytes();\n\n\treturn {\n\t\tQByteArray(reinterpret_cast<const char*>(pb.data()), pb.size()),\n\t\tQByteArray(reinterpret_cast<const char*>(qb.data()), qb.size())\n\t};\n}\n\n[[nodiscard]] ParsedPQ FactorizePQ(const QByteArray &pqStr) {\n\tconst auto size = pqStr.size();\n\tif (size > 8 || (size == 8 && (uchar(pqStr[0]) & 128) != 0)) {\n\t\treturn FactorizeBigPQ(pqStr);\n\t}\n\n\tauto ptr = reinterpret_cast<const uchar*>(pqStr.data());\n\tuint64 pq = 0;\n\tfor (auto i = 0; i != size; ++i) {\n\t\tpq = (pq << 8) | ptr[i];\n\t}\n\n\tauto p = FactorizeSmallPQ(pq);\n\tif (p == 0 || (pq % p) != 0) {\n\t\treturn ParsedPQ();\n\t}\n\tauto q = pq / p;\n\n\tauto pStr = QByteArray(4, Qt::Uninitialized);\n\tuchar *pChars = (uchar*)pStr.data();\n\tfor (auto i = 0; i != 4; ++i) {\n\t\t*(pChars + 3 - i) = (uchar)(p & 0xFF);\n\t\tp >>= 8;\n\t}\n\n\tauto qStr = QByteArray(4, Qt::Uninitialized);\n\tuchar *qChars = (uchar*)qStr.data();\n\tfor (auto i = 0; i != 4; ++i) {\n\t\t*(qChars + 3 - i) = (uchar)(q & 0xFF);\n\t\tq >>= 8;\n\t}\n\treturn { pStr, qStr };\n}\n\n[[nodiscard]] bool IsGoodEncryptedInner(\n\t\tbytes::const_span keyAesEncrypted,\n\t\tconst RSAPublicKey &key) {\n\tExpects(keyAesEncrypted.size() == 256);\n\n\tconst auto modulus = key.getN();\n\tconst auto e = key.getE();\n\tconst auto shift = (256 - int(modulus.size()));\n\tAssert(shift >= 0);\n\tfor (auto i = 0; i != 256; ++i) {\n\t\tconst auto a = keyAesEncrypted[i];\n\t\tconst auto b = (i < shift)\n\t\t\t? bytes::type(0)\n\t\t\t: modulus[i - shift];\n\t\tif (a > b) {\n\t\t\treturn false;\n\t\t} else if (a < b) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\ntemplate <typename PQInnerData>\n[[nodiscard]] bytes::vector EncryptPQInnerRSA(\n\t\tconst PQInnerData &data,\n\t\tconst RSAPublicKey &key) {\n\tDEBUG_LOG((\"AuthKey Info: encrypting pq inner...\"));\n\n\tconstexpr auto kPrime = sizeof(mtpPrime);\n\tconstexpr auto kDataWithPaddingPrimes = 192 / kPrime;\n\tconstexpr auto kMaxSizeInPrimes = 144 / kPrime;\n\tconstexpr auto kDataHashPrimes = (SHA256_DIGEST_LENGTH / kPrime);\n\tconstexpr auto kKeySize = 32;\n\tconstexpr auto kIvSize = 32;\n\n\tusing BoxedPQInnerData = std::conditional_t<\n\t\ttl::is_boxed_v<PQInnerData>,\n\t\tPQInnerData,\n\t\ttl::boxed<PQInnerData>>;\n\tconst auto boxed = BoxedPQInnerData(data);\n\tconst auto p_q_inner_size = tl::count_length(boxed);\n\tconst auto sizeInPrimes = (p_q_inner_size / kPrime);\n\tif (sizeInPrimes > kMaxSizeInPrimes) {\n\t\treturn {};\n\t}\n\n\tauto dataWithPadding = mtpBuffer();\n\tdataWithPadding.reserve(kDataWithPaddingPrimes);\n\tboxed.write(dataWithPadding);\n\n\t// data_with_padding := data + random_padding_bytes;\n\tdataWithPadding.resize(kDataWithPaddingPrimes);\n\tconst auto dataWithPaddingBytes = bytes::make_span(dataWithPadding);\n\tbytes::set_random(dataWithPaddingBytes.subspan(sizeInPrimes * kPrime));\n\n\tDEBUG_LOG((\"AuthKey Info: starting key generation for pq inner...\"));\n\n\twhile (true) {\n\t\tauto dataWithHash = mtpBuffer();\n\t\tdataWithHash.reserve(kDataWithPaddingPrimes + kDataHashPrimes);\n\t\tdataWithHash.append(dataWithPadding);\n\n\t\t// data_pad_reversed := BYTE_REVERSE(data_with_padding);\n\t\tranges::reverse(bytes::make_span(dataWithHash));\n\n\t\t// data_with_hash := data_pad_reversed\n\t\t//\t+ SHA256(temp_key + data_with_padding);\n\t\tconst auto tempKey = base::RandomValue<bytes::array<kKeySize>>();\n\t\tdataWithHash.resize(kDataWithPaddingPrimes + kDataHashPrimes);\n\t\tconst auto dataWithHashBytes = bytes::make_span(dataWithHash);\n\t\tbytes::copy(\n\t\t\tdataWithHashBytes.subspan(kDataWithPaddingPrimes * kPrime),\n\t\t\topenssl::Sha256(tempKey, bytes::make_span(dataWithPadding)));\n\n\t\tauto aesEncrypted = mtpBuffer();\n\t\tauto keyAesEncrypted = mtpBuffer();\n\t\taesEncrypted.resize(dataWithHash.size());\n\t\tconst auto aesEncryptedBytes = bytes::make_span(aesEncrypted);\n\n\t\tDEBUG_LOG((\"AuthKey Info: encrypting ige for pq inner...\"));\n\n\t\t// aes_encrypted := AES256_IGE(data_with_hash, temp_key, 0);\n\t\tconst auto tempIv = bytes::array<kIvSize>{ { bytes::type(0) } };\n\t\taesIgeEncryptRaw(\n\t\t\tdataWithHashBytes.data(),\n\t\t\taesEncryptedBytes.data(),\n\t\t\tdataWithHashBytes.size(),\n\t\t\ttempKey.data(),\n\t\t\ttempIv.data());\n\n\t\tDEBUG_LOG((\"AuthKey Info: counting hash for pq inner...\"));\n\n\t\t// temp_key_xor := temp_key XOR SHA256(aes_encrypted);\n\t\tconst auto fullSize = (kKeySize / kPrime) + dataWithHash.size();\n\t\tkeyAesEncrypted.resize(fullSize);\n\t\tconst auto keyAesEncryptedBytes = bytes::make_span(keyAesEncrypted);\n\t\tconst auto aesHash = openssl::Sha256(aesEncryptedBytes);\n\t\tfor (auto i = 0; i != kKeySize; ++i) {\n\t\t\tkeyAesEncryptedBytes[i] = tempKey[i] ^ aesHash[i];\n\t\t}\n\n\t\tDEBUG_LOG((\"AuthKey Info: checking chosen key for pq inner...\"));\n\n\t\t// key_aes_encrypted := temp_key_xor + aes_encrypted;\n\t\tbytes::copy(\n\t\t\tkeyAesEncryptedBytes.subspan(kKeySize),\n\t\t\taesEncryptedBytes);\n\t\tif (IsGoodEncryptedInner(keyAesEncryptedBytes, key)) {\n\t\t\tDEBUG_LOG((\"AuthKey Info: chosen key for pq inner is good.\"));\n\t\t\treturn key.encrypt(keyAesEncryptedBytes);\n\t\t}\n\n\t\tDEBUG_LOG((\"AuthKey Info: chosen key for pq inner is bad...\"));\n\t}\n}\n\n[[nodiscard]] std::string EncryptClientDHInner(\n\t\tconst MTPClient_DH_Inner_Data &data,\n\t\tconst void *aesKey,\n\t\tconst void *aesIV) {\n\tconstexpr auto kSkipPrimes = openssl::kSha1Size / sizeof(mtpPrime);\n\n\tauto client_dh_inner_size = tl::count_length(data);\n\tauto encSize = (client_dh_inner_size >> 2) + kSkipPrimes;\n\tauto encFullSize = encSize;\n\tif (encSize & 0x03) {\n\t\tencFullSize += 4 - (encSize & 0x03);\n\t}\n\n\tauto encBuffer = mtpBuffer();\n\tencBuffer.reserve(encFullSize);\n\tencBuffer.resize(kSkipPrimes);\n\tdata.write(encBuffer);\n\tencBuffer.resize(encFullSize);\n\n\tconst auto bytes = bytes::make_span(encBuffer);\n\n\tconst auto hash = openssl::Sha1(bytes.subspan(\n\t\tkSkipPrimes * sizeof(mtpPrime),\n\t\tclient_dh_inner_size));\n\tbytes::copy(bytes, hash);\n\tbytes::set_random(bytes.subspan(encSize * sizeof(mtpPrime)));\n\n\tauto sdhEncString = std::string(encFullSize * 4, ' ');\n\n\taesIgeEncryptRaw(&encBuffer[0], &sdhEncString[0], encFullSize * sizeof(mtpPrime), aesKey, aesIV);\n\n\treturn sdhEncString;\n}\n\n// 128 lower-order bits of SHA1.\nMTPint128 NonceDigest(bytes::const_span data) {\n\tconst auto hash = openssl::Sha1(data);\n\treturn *(MTPint128*)(hash.data() + 4);\n}\n\n} // namespace\n\nDcKeyCreator::Attempt::~Attempt() {\n\tconst auto clearBytes = [](bytes::span bytes) {\n\t\tOPENSSL_cleanse(bytes.data(), bytes.size());\n\t};\n\tOPENSSL_cleanse(&data, sizeof(data));\n\tclearBytes(dhPrime);\n\tclearBytes(g_a);\n\tclearBytes(authKey);\n}\n\nDcKeyCreator::DcKeyCreator(\n\tDcId dcId,\n\tint16 protocolDcId,\n\tnot_null<AbstractConnection*> connection,\n\tnot_null<DcOptions*> dcOptions,\n\tDelegate delegate,\n\tDcKeyRequest request)\n: _connection(connection)\n, _dcOptions(dcOptions)\n, _dcId(dcId)\n, _protocolDcId(protocolDcId)\n, _request(request)\n, _delegate(std::move(delegate)) {\n\tExpects(_request.temporaryExpiresIn > 0);\n\tExpects(_delegate.done != nullptr);\n\n\tQObject::connect(_connection, &AbstractConnection::receivedData, [=] {\n\t\tanswered();\n\t});\n\n\tif (_request.persistentNeeded) {\n\t\tpqSend(&_persistent, 0);\n\t} else {\n\t\tpqSend(&_temporary, _request.temporaryExpiresIn);\n\t}\n}\n\nDcKeyCreator::~DcKeyCreator() {\n\tif (_delegate.done) {\n\t\tstopReceiving();\n\t}\n}\n\ntemplate <typename RequestType>\nvoid DcKeyCreator::sendNotSecureRequest(const RequestType &request) {\n\tauto packet = _connection->prepareNotSecurePacket(\n\t\trequest,\n\t\tbase::unixtime::mtproto_msg_id());\n\n\tDEBUG_LOG((\"AuthKey Info: sending request, size: %1, time: %3\"\n\t\t).arg(packet.size() - 8\n\t\t).arg(packet[5]));\n\n\tconst auto bytesSize = packet.size() * sizeof(mtpPrime);\n\n\t_connection->sendData(std::move(packet));\n\n\tif (_delegate.sentSome) {\n\t\t_delegate.sentSome(bytesSize);\n\t}\n}\n\ntemplate <typename RequestType, typename Response>\nstd::optional<Response> DcKeyCreator::readNotSecureResponse(\n\t\tgsl::span<const mtpPrime> answer) {\n\tauto from = answer.data();\n\tauto result = Response();\n\tif (result.read(from, from + answer.size())) {\n\t\treturn result;\n\t}\n\treturn std::nullopt;\n}\n\nvoid DcKeyCreator::answered() {\n\tif (_delegate.receivedSome) {\n\t\t_delegate.receivedSome();\n\t}\n\n\tif (_connection->received().empty()) {\n\t\tLOG((\"AuthKey Error: \"\n\t\t\t\"trying to read response from empty received list\"));\n\t\treturn failed();\n\t}\n\n\tconst auto buffer = std::move(_connection->received().front());\n\t_connection->received().pop_front();\n\n\tconst auto answer = _connection->parseNotSecureResponse(buffer);\n\tif (answer.empty()) {\n\t\treturn failed();\n\t}\n\n\thandleAnswer(answer);\n}\n\nDcKeyCreator::Attempt *DcKeyCreator::attemptByNonce(const MTPint128 &nonce) {\n\tif (_temporary.data.nonce == nonce) {\n\t\tDEBUG_LOG((\"AuthKey Info: receiving answer for temporary...\"));\n\t\treturn &_temporary;\n\t} else if (_persistent.data.nonce == nonce) {\n\t\tDEBUG_LOG((\"AuthKey Info: receiving answer for persistent...\"));\n\t\treturn &_persistent;\n\t}\n\tLOG((\"AuthKey Error: attempt by nonce not found.\"));\n\treturn nullptr;\n}\n\nvoid DcKeyCreator::handleAnswer(gsl::span<const mtpPrime> answer) {\n\tif (const auto resPQ = readNotSecureResponse<MTPReq_pq>(answer)) {\n\t\tconst auto nonce = resPQ->match([](const auto &data) {\n\t\t\treturn data.vnonce();\n\t\t});\n\t\tif (const auto attempt = attemptByNonce(nonce)) {\n\t\t\tDEBUG_LOG((\"AuthKey Info: receiving Req_pq answer...\"));\n\t\t\treturn pqAnswered(attempt, *resPQ);\n\t\t}\n\t} else if (const auto resDH = readNotSecureResponse<MTPReq_DH_params>(answer)) {\n\t\tconst auto nonce = resDH->match([](const auto &data) {\n\t\t\treturn data.vnonce();\n\t\t});\n\t\tif (const auto attempt = attemptByNonce(nonce)) {\n\t\t\tDEBUG_LOG((\"AuthKey Info: receiving Req_DH_params answer...\"));\n\t\t\treturn dhParamsAnswered(attempt, *resDH);\n\t\t}\n\t} else if (const auto result = readNotSecureResponse<MTPSet_client_DH_params>(answer)) {\n\t\tconst auto nonce = result->match([](const auto &data) {\n\t\t\treturn data.vnonce();\n\t\t});\n\t\tif (const auto attempt = attemptByNonce(nonce)) {\n\t\t\tDEBUG_LOG((\"AuthKey Info: receiving Req_client_DH_params answer...\"));\n\t\t\treturn dhClientParamsAnswered(attempt, *result);\n\t\t}\n\t}\n\tLOG((\"AuthKey Error: Unknown answer received.\"));\n\tfailed();\n}\n\nvoid DcKeyCreator::pqSend(not_null<Attempt*> attempt, TimeId expiresIn) {\n\tDEBUG_LOG((\"AuthKey Info: sending Req_pq for %1...\"\n\t\t).arg(expiresIn ? \"temporary\" : \"persistent\"));\n\tattempt->stage = Stage::WaitingPQ;\n\tattempt->expiresIn = expiresIn;\n\tattempt->data.nonce = base::RandomValue<MTPint128>();\n\tsendNotSecureRequest(MTPReq_pq_multi(attempt->data.nonce));\n}\n\nvoid DcKeyCreator::pqAnswered(\n\t\tnot_null<Attempt*> attempt,\n\t\tconst MTPresPQ &data) {\n\tdata.match([&](const MTPDresPQ &data) {\n\t\tExpects(data.vnonce() == attempt->data.nonce);\n\n\t\tif (attempt->stage != Stage::WaitingPQ) {\n\t\t\tLOG((\"AuthKey Error: Unexpected stage %1\").arg(int(attempt->stage)));\n\t\t\treturn failed();\n\t\t}\n\t\tDEBUG_LOG((\"AuthKey Info: getting dc RSA key...\"));\n\t\tconst auto rsaKey = _dcOptions->getDcRSAKey(\n\t\t\t_dcId,\n\t\t\tdata.vserver_public_key_fingerprints().v);\n\t\tif (!rsaKey.valid()) {\n\t\t\tDEBUG_LOG((\"AuthKey Error: unknown public key.\"));\n\t\t\treturn failed(DcKeyError::UnknownPublicKey);\n\t\t}\n\n\t\tattempt->data.server_nonce = data.vserver_nonce();\n\t\tattempt->data.new_nonce = base::RandomValue<MTPint256>();\n\n\t\tDEBUG_LOG((\"AuthKey Info: parsing pq...\"));\n\t\tconst auto &pq = data.vpq().v;\n\t\tconst auto parsed = FactorizePQ(data.vpq().v);\n\t\tif (parsed.p.isEmpty() || parsed.q.isEmpty()) {\n\t\t\tLOG((\"AuthKey Error: could not factor pq!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: problematic pq: %1\").arg(Logs::mb(pq.constData(), pq.length()).str()));\n\t\t\treturn failed();\n\t\t}\n\t\tDEBUG_LOG((\"AuthKey Info: parse pq done.\"));\n\n\t\tconst auto dhEncString = [&] {\n\t\t\treturn (attempt->expiresIn == 0)\n\t\t\t\t? EncryptPQInnerRSA(\n\t\t\t\t\tMTP_p_q_inner_data_dc(\n\t\t\t\t\t\tdata.vpq(),\n\t\t\t\t\t\tMTP_bytes(parsed.p),\n\t\t\t\t\t\tMTP_bytes(parsed.q),\n\t\t\t\t\t\tattempt->data.nonce,\n\t\t\t\t\t\tattempt->data.server_nonce,\n\t\t\t\t\t\tattempt->data.new_nonce,\n\t\t\t\t\t\tMTP_int(_protocolDcId)),\n\t\t\t\t\trsaKey)\n\t\t\t\t: EncryptPQInnerRSA(\n\t\t\t\t\tMTP_p_q_inner_data_temp_dc(\n\t\t\t\t\t\tdata.vpq(),\n\t\t\t\t\t\tMTP_bytes(parsed.p),\n\t\t\t\t\t\tMTP_bytes(parsed.q),\n\t\t\t\t\t\tattempt->data.nonce,\n\t\t\t\t\t\tattempt->data.server_nonce,\n\t\t\t\t\t\tattempt->data.new_nonce,\n\t\t\t\t\t\tMTP_int(_protocolDcId),\n\t\t\t\t\t\tMTP_int(attempt->expiresIn)),\n\t\t\t\t\trsaKey);\n\t\t}();\n\t\tif (dhEncString.empty()) {\n\t\t\tDEBUG_LOG((\"AuthKey Error: could not encrypt pq inner.\"));\n\t\t\treturn failed();\n\t\t}\n\n\t\tattempt->stage = Stage::WaitingDH;\n\t\tDEBUG_LOG((\"AuthKey Info: sending Req_DH_params...\"));\n\t\tsendNotSecureRequest(MTPReq_DH_params(\n\t\t\tattempt->data.nonce,\n\t\t\tattempt->data.server_nonce,\n\t\t\tMTP_bytes(parsed.p),\n\t\t\tMTP_bytes(parsed.q),\n\t\t\tMTP_long(rsaKey.fingerprint()),\n\t\t\tMTP_bytes(dhEncString)));\n\t});\n}\n\nvoid DcKeyCreator::dhParamsAnswered(\n\t\tnot_null<Attempt*> attempt,\n\t\tconst MTPserver_DH_Params &data) {\n\tif (attempt->stage != Stage::WaitingDH) {\n\t\tLOG((\"AuthKey Error: Unexpected stage %1\").arg(int(attempt->stage)));\n\t\treturn failed();\n\t}\n\tdata.match([&](const MTPDserver_DH_params_ok &data) {\n\t\tExpects(data.vnonce() == attempt->data.nonce);\n\n\t\tif (data.vserver_nonce() != attempt->data.server_nonce) {\n\t\t\tLOG((\"AuthKey Error: received server_nonce <> sent server_nonce (in server_DH_params_ok)!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: received server_nonce: %1, sent server_nonce: %2\").arg(Logs::mb(&data.vserver_nonce(), 16).str(), Logs::mb(&attempt->data.server_nonce, 16).str()));\n\t\t\treturn failed();\n\t\t}\n\n\t\tauto &encDHStr = data.vencrypted_answer().v;\n\t\tuint32 encDHLen = encDHStr.length(), encDHBufLen = encDHLen >> 2;\n\t\tif ((encDHLen & 0x03) || encDHBufLen < 6) {\n\t\t\tLOG((\"AuthKey Error: bad encrypted data length %1 (in server_DH_params_ok)!\").arg(encDHLen));\n\t\t\tDEBUG_LOG((\"AuthKey Error: received encrypted data %1\").arg(Logs::mb(encDHStr.constData(), encDHLen).str()));\n\t\t\treturn failed();\n\t\t}\n\n\t\tconst auto nlen = sizeof(attempt->data.new_nonce);\n\t\tconst auto slen = sizeof(attempt->data.server_nonce);\n\t\tauto tmp_aes_buffer = bytes::array<1024>();\n\t\tconst auto tmp_aes = bytes::make_span(tmp_aes_buffer);\n\t\tbytes::copy(tmp_aes, bytes::object_as_span(&attempt->data.new_nonce));\n\t\tbytes::copy(tmp_aes.subspan(nlen), bytes::object_as_span(&attempt->data.server_nonce));\n\t\tbytes::copy(tmp_aes.subspan(nlen + slen), bytes::object_as_span(&attempt->data.new_nonce));\n\t\tbytes::copy(tmp_aes.subspan(nlen + slen + nlen), bytes::object_as_span(&attempt->data.new_nonce));\n\t\tconst auto sha1ns = openssl::Sha1(tmp_aes.subspan(0, nlen + slen));\n\t\tconst auto sha1sn = openssl::Sha1(tmp_aes.subspan(nlen, nlen + slen));\n\t\tconst auto sha1nn = openssl::Sha1(tmp_aes.subspan(nlen + slen, nlen + nlen));\n\n\t\tmtpBuffer decBuffer;\n\t\tdecBuffer.resize(encDHBufLen);\n\n\t\tconst auto aesKey = bytes::make_span(attempt->data.aesKey);\n\t\tconst auto aesIV = bytes::make_span(attempt->data.aesIV);\n\t\tbytes::copy(aesKey, bytes::make_span(sha1ns).subspan(0, 20));\n\t\tbytes::copy(aesKey.subspan(20), bytes::make_span(sha1sn).subspan(0, 12));\n\t\tbytes::copy(aesIV, bytes::make_span(sha1sn).subspan(12, 8));\n\t\tbytes::copy(aesIV.subspan(8), bytes::make_span(sha1nn).subspan(0, 20));\n\t\tbytes::copy(aesIV.subspan(28), bytes::object_as_span(&attempt->data.new_nonce).subspan(0, 4));\n\n\t\taesIgeDecryptRaw(encDHStr.constData(), &decBuffer[0], encDHLen, aesKey.data(), aesIV.data());\n\n\t\tconst mtpPrime *from(&decBuffer[5]), *to(from), *end(from + (encDHBufLen - 5));\n\t\tMTPServer_DH_inner_data dh_inner;\n\t\tif (!dh_inner.read(to, end)) {\n\t\t\tLOG((\"AuthKey Error: could not decrypt server_DH_inner_data!\"));\n\t\t\treturn failed();\n\t\t}\n\t\tconst auto &dh_inner_data(dh_inner.c_server_DH_inner_data());\n\t\tif (dh_inner_data.vnonce() != attempt->data.nonce) {\n\t\t\tLOG((\"AuthKey Error: received nonce <> sent nonce (in server_DH_inner_data)!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: received nonce: %1, sent nonce: %2\").arg(Logs::mb(&dh_inner_data.vnonce(), 16).str(), Logs::mb(&attempt->data.nonce, 16).str()));\n\t\t\treturn failed();\n\t\t}\n\t\tif (dh_inner_data.vserver_nonce() != attempt->data.server_nonce) {\n\t\t\tLOG((\"AuthKey Error: received server_nonce <> sent server_nonce (in server_DH_inner_data)!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: received server_nonce: %1, sent server_nonce: %2\").arg(Logs::mb(&dh_inner_data.vserver_nonce(), 16).str(), Logs::mb(&attempt->data.server_nonce, 16).str()));\n\t\t\treturn failed();\n\t\t}\n\t\tconst auto sha1Buffer = openssl::Sha1(\n\t\t\tbytes::make_span(decBuffer).subspan(\n\t\t\t\t5 * sizeof(mtpPrime),\n\t\t\t\t(to - from) * sizeof(mtpPrime)));\n\t\tconst auto sha1Dec = bytes::make_span(decBuffer).subspan(\n\t\t\t0,\n\t\t\topenssl::kSha1Size);\n\t\tif (bytes::compare(sha1Dec, sha1Buffer)) {\n\t\t\tLOG((\"AuthKey Error: sha1 hash of encrypted part did not match!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: sha1 did not match, server_nonce: %1, new_nonce %2, encrypted data %3\").arg(Logs::mb(&attempt->data.server_nonce, 16).str(), Logs::mb(&attempt->data.new_nonce, 16).str(), Logs::mb(encDHStr.constData(), encDHLen).str()));\n\t\t\treturn failed();\n\t\t}\n\t\tbase::unixtime::update(dh_inner_data.vserver_time().v);\n\n\t\t// check that dhPrime and (dhPrime - 1) / 2 are really prime\n\t\tif (!IsPrimeAndGood(bytes::make_span(dh_inner_data.vdh_prime().v), dh_inner_data.vg().v)) {\n\t\t\tLOG((\"AuthKey Error: bad dh_prime primality!\"));\n\t\t\treturn failed();\n\t\t}\n\n\t\tattempt->dhPrime = bytes::make_vector(\n\t\t\tdh_inner_data.vdh_prime().v);\n\t\tattempt->data.g = dh_inner_data.vg().v;\n\t\tattempt->g_a = bytes::make_vector(dh_inner_data.vg_a().v);\n\t\tattempt->data.retry_id = MTP_long(0);\n\t\tattempt->retries = 0;\n\t\tdhClientParamsSend(attempt);\n\t}, [&](const MTPDserver_DH_params_fail &data) {\n\t\tExpects(data.vnonce() == attempt->data.nonce);\n\n\t\tif (data.vserver_nonce() != attempt->data.server_nonce) {\n\t\t\tLOG((\"AuthKey Error: received server_nonce <> sent server_nonce (in server_DH_params_fail)!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: received server_nonce: %1, sent server_nonce: %2\").arg(Logs::mb(&data.vserver_nonce(), 16).str(), Logs::mb(&attempt->data.server_nonce, 16).str()));\n\t\t\treturn failed();\n\t\t}\n\t\tif (data.vnew_nonce_hash() != NonceDigest(bytes::object_as_span(&attempt->data.new_nonce))) {\n\t\t\tLOG((\"AuthKey Error: received new_nonce_hash did not match!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: received new_nonce_hash: %1, new_nonce: %2\").arg(Logs::mb(&data.vnew_nonce_hash(), 16).str(), Logs::mb(&attempt->data.new_nonce, 32).str()));\n\t\t\treturn failed();\n\t\t}\n\t\tLOG((\"AuthKey Error: server_DH_params_fail received!\"));\n\t\tfailed();\n\t});\n}\n\nvoid DcKeyCreator::dhClientParamsSend(not_null<Attempt*> attempt) {\n\tif (++attempt->retries > 5) {\n\t\tLOG((\"AuthKey Error: could not create auth_key for %1 retries\").arg(attempt->retries - 1));\n\t\treturn failed();\n\t}\n\n\t// gen rand 'b'\n\tauto randomSeed = bytes::vector(ModExpFirst::kRandomPowerSize);\n\tbytes::set_random(randomSeed);\n\tauto g_b_data = CreateModExp(attempt->data.g, attempt->dhPrime, randomSeed);\n\tif (g_b_data.modexp.empty()) {\n\t\tLOG((\"AuthKey Error: could not generate good g_b.\"));\n\t\treturn failed();\n\t}\n\n\tauto computedAuthKey = CreateAuthKey(attempt->g_a, g_b_data.randomPower, attempt->dhPrime);\n\tif (computedAuthKey.empty()) {\n\t\tLOG((\"AuthKey Error: could not generate auth_key.\"));\n\t\treturn failed();\n\t}\n\tAuthKey::FillData(attempt->authKey, computedAuthKey);\n\n\tauto auth_key_sha = openssl::Sha1(attempt->authKey);\n\tmemcpy(&attempt->data.auth_key_aux_hash.v, auth_key_sha.data(), 8);\n\tmemcpy(&attempt->data.auth_key_hash.v, auth_key_sha.data() + 12, 8);\n\n\tconst auto client_dh_inner = MTP_client_DH_inner_data(\n\t\tattempt->data.nonce,\n\t\tattempt->data.server_nonce,\n\t\tattempt->data.retry_id,\n\t\tMTP_bytes(g_b_data.modexp));\n\n\tauto sdhEncString = EncryptClientDHInner(\n\t\tclient_dh_inner,\n\t\tattempt->data.aesKey.data(),\n\t\tattempt->data.aesIV.data());\n\n\tattempt->stage = Stage::WaitingDone;\n\tDEBUG_LOG((\"AuthKey Info: sending Req_client_DH_params...\"));\n\tsendNotSecureRequest(MTPSet_client_DH_params(\n\t\tattempt->data.nonce,\n\t\tattempt->data.server_nonce,\n\t\tMTP_string(std::move(sdhEncString))));\n}\n\nvoid DcKeyCreator::dhClientParamsAnswered(\n\t\tnot_null<Attempt*> attempt,\n\t\tconst MTPset_client_DH_params_answer &data) {\n\tif (attempt->stage != Stage::WaitingDone) {\n\t\tLOG((\"AuthKey Error: Unexpected stage %1\").arg(int(attempt->stage)));\n\t\treturn failed();\n\t}\n\n\tdata.match([&](const MTPDdh_gen_ok &data) {\n\t\tif (data.vnonce() != attempt->data.nonce) {\n\t\t\tLOG((\"AuthKey Error: received nonce <> sent nonce (in dh_gen_ok)!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: received nonce: %1, sent nonce: %2\").arg(Logs::mb(&data.vnonce(), 16).str(), Logs::mb(&attempt->data.nonce, 16).str()));\n\t\t\treturn failed();\n\t\t}\n\t\tif (data.vserver_nonce() != attempt->data.server_nonce) {\n\t\t\tLOG((\"AuthKey Error: received server_nonce <> sent server_nonce (in dh_gen_ok)!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: received server_nonce: %1, sent server_nonce: %2\").arg(Logs::mb(&data.vserver_nonce(), 16).str(), Logs::mb(&attempt->data.server_nonce, 16).str()));\n\t\t\treturn failed();\n\t\t}\n\t\tattempt->data.new_nonce_buf[32] = bytes::type(1);\n\t\tif (data.vnew_nonce_hash1() != NonceDigest(attempt->data.new_nonce_buf)) {\n\t\t\tLOG((\"AuthKey Error: received new_nonce_hash1 did not match!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: received new_nonce_hash1: %1, new_nonce_buf: %2\").arg(Logs::mb(&data.vnew_nonce_hash1(), 16).str(), Logs::mb(attempt->data.new_nonce_buf.data(), 41).str()));\n\t\t\treturn failed();\n\t\t}\n\n\t\tuint64 salt1 = attempt->data.new_nonce.l.l, salt2 = attempt->data.server_nonce.l;\n\t\tattempt->data.doneSalt = salt1 ^ salt2;\n\t\tattempt->stage = Stage::Ready;\n\t\tdone();\n\t}, [&](const MTPDdh_gen_retry &data) {\n\t\tif (data.vnonce() != attempt->data.nonce) {\n\t\t\tLOG((\"AuthKey Error: received nonce <> sent nonce (in dh_gen_retry)!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: received nonce: %1, sent nonce: %2\").arg(Logs::mb(&data.vnonce(), 16).str(), Logs::mb(&attempt->data.nonce, 16).str()));\n\t\t\treturn failed();\n\t\t}\n\t\tif (data.vserver_nonce() != attempt->data.server_nonce) {\n\t\t\tLOG((\"AuthKey Error: received server_nonce <> sent server_nonce (in dh_gen_retry)!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: received server_nonce: %1, sent server_nonce: %2\").arg(Logs::mb(&data.vserver_nonce(), 16).str(), Logs::mb(&attempt->data.server_nonce, 16).str()));\n\t\t\treturn failed();\n\t\t}\n\t\tattempt->data.new_nonce_buf[32] = bytes::type(2);\n\t\tif (data.vnew_nonce_hash2() != NonceDigest(attempt->data.new_nonce_buf)) {\n\t\t\tLOG((\"AuthKey Error: received new_nonce_hash2 did not match!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: received new_nonce_hash2: %1, new_nonce_buf: %2\").arg(Logs::mb(&data.vnew_nonce_hash2(), 16).str(), Logs::mb(attempt->data.new_nonce_buf.data(), 41).str()));\n\t\t\treturn failed();\n\t\t}\n\t\tattempt->data.retry_id = attempt->data.auth_key_aux_hash;\n\t\tdhClientParamsSend(attempt);\n\t}, [&](const MTPDdh_gen_fail &data) {\n\t\tif (data.vnonce() != attempt->data.nonce) {\n\t\t\tLOG((\"AuthKey Error: received nonce <> sent nonce (in dh_gen_fail)!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: received nonce: %1, sent nonce: %2\").arg(Logs::mb(&data.vnonce(), 16).str(), Logs::mb(&attempt->data.nonce, 16).str()));\n\t\t\treturn failed();\n\t\t}\n\t\tif (data.vserver_nonce() != attempt->data.server_nonce) {\n\t\t\tLOG((\"AuthKey Error: received server_nonce <> sent server_nonce (in dh_gen_fail)!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: received server_nonce: %1, sent server_nonce: %2\").arg(Logs::mb(&data.vserver_nonce(), 16).str(), Logs::mb(&attempt->data.server_nonce, 16).str()));\n\t\t\treturn failed();\n\t\t}\n\t\tattempt->data.new_nonce_buf[32] = bytes::type(3);\n\t\tif (data.vnew_nonce_hash3() != NonceDigest(attempt->data.new_nonce_buf)) {\n\t\t\tLOG((\"AuthKey Error: received new_nonce_hash3 did not match!\"));\n\t\t\tDEBUG_LOG((\"AuthKey Error: received new_nonce_hash3: %1, new_nonce_buf: %2\").arg(Logs::mb(&data.vnew_nonce_hash3(), 16).str(), Logs::mb(attempt->data.new_nonce_buf.data(), 41).str()));\n\t\t\treturn failed();\n\t\t}\n\t\tLOG((\"AuthKey Error: dh_gen_fail received!\"));\n\t\tfailed();\n\t});\n}\n\nvoid DcKeyCreator::failed(DcKeyError error) {\n\tstopReceiving();\n\tauto onstack = base::take(_delegate.done);\n\tonstack(tl::unexpected(error));\n}\n\nvoid DcKeyCreator::done() {\n\tif (_temporary.stage == Stage::None) {\n\t\tpqSend(&_temporary, _request.temporaryExpiresIn);\n\t\treturn;\n\t}\n\tAssert(_temporary.stage == Stage::Ready);\n\tAssert(_persistent.stage == Stage::Ready || _persistent.stage == Stage::None);\n\n\tauto result = DcKeyResult();\n\tresult.temporaryKey = std::make_shared<AuthKey>(\n\t\tAuthKey::Type::Temporary,\n\t\t_dcId,\n\t\t_temporary.authKey);\n\tresult.temporaryServerSalt = _temporary.data.doneSalt;\n\tif (_persistent.stage == Stage::Ready) {\n\t\tresult.persistentKey = std::make_shared<AuthKey>(\n\t\t\tAuthKey::Type::Generated,\n\t\t\t_dcId,\n\t\t\t_persistent.authKey);\n\t\tresult.persistentServerSalt = _persistent.data.doneSalt;\n\t}\n\n\tstopReceiving();\n\tauto onstack = base::take(_delegate.done);\n\tonstack(std::move(result));\n}\n\nvoid DcKeyCreator::stopReceiving() {\n\tQObject::disconnect(\n\t\t_connection,\n\t\t&AbstractConnection::receivedData,\n\t\tnullptr,\n\t\tnullptr);\n}\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_dc_key_creator.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/core_types.h\"\n#include \"mtproto/mtproto_auth_key.h\"\n#include \"mtproto/connection_abstract.h\"\n#include \"base/basic_types.h\"\n#include \"base/expected.h\"\n\nnamespace MTP {\nclass DcOptions;\n} // namespace MTP\n\nnamespace MTP::details {\n\nstruct DcKeyRequest {\n\tTimeId temporaryExpiresIn = 0;\n\tbool persistentNeeded = false;\n};\n\nenum class DcKeyError {\n\tUnknownPublicKey,\n\tOther,\n};\n\nstruct DcKeyResult {\n\tAuthKeyPtr persistentKey;\n\tAuthKeyPtr temporaryKey;\n\tuint64 temporaryServerSalt = 0;\n\tuint64 persistentServerSalt = 0;\n};\n\nclass DcKeyCreator final {\npublic:\n\tstruct Delegate {\n\t\tFn<void(base::expected<DcKeyResult, DcKeyError>)> done;\n\t\tFn<void(uint64)> sentSome;\n\t\tFn<void()> receivedSome;\n\t};\n\n\tDcKeyCreator(\n\t\tDcId dcId,\n\t\tint16 protocolDcId,\n\t\tnot_null<AbstractConnection*> connection,\n\t\tnot_null<DcOptions*> dcOptions,\n\t\tDelegate delegate,\n\t\tDcKeyRequest request);\n\t~DcKeyCreator();\n\nprivate:\n\tenum class Stage {\n\t\tNone,\n\t\tWaitingPQ,\n\t\tWaitingDH,\n\t\tWaitingDone,\n\t\tReady,\n\t};\n\tstruct Data {\n\t\tData()\n\t\t: new_nonce(*(MTPint256*)((uchar*)new_nonce_buf.data()))\n\t\t, auth_key_aux_hash(*(MTPlong*)((uchar*)new_nonce_buf.data() + 33)) {\n\t\t}\n\t\tMTPint128 nonce, server_nonce;\n\n\t\t// 32 bytes new_nonce + 1 check byte + 8 bytes of auth_key_aux_hash.\n\t\tbytes::array<41> new_nonce_buf{};\n\n\t\tMTPint256 &new_nonce;\n\t\tMTPlong &auth_key_aux_hash;\n\n\t\tMTPlong retry_id;\n\n\t\tint32 g = 0;\n\n\t\tbytes::array<32> aesKey;\n\t\tbytes::array<32> aesIV;\n\t\tMTPlong auth_key_hash;\n\t\tuint64 doneSalt = 0;\n\t};\n\tstruct Attempt {\n\t\t~Attempt();\n\n\t\tData data;\n\t\tbytes::vector dhPrime;\n\t\tbytes::vector g_a;\n\t\tAuthKey::Data authKey = { { gsl::byte{} } };\n\t\tTimeId expiresIn = 0;\n\t\tuint32 retries = 0;\n\t\tStage stage = Stage::None;\n\t};\n\n\ttemplate <typename RequestType>\n\tvoid sendNotSecureRequest(const RequestType &request);\n\n\ttemplate <\n\t\ttypename RequestType,\n\t\ttypename Response = typename RequestType::ResponseType>\n\t[[nodiscard]] std::optional<Response> readNotSecureResponse(\n\t\t\tgsl::span<const mtpPrime> answer);\n\n\tAttempt *attemptByNonce(const MTPint128 &nonce);\n\n\tvoid answered();\n\tvoid handleAnswer(gsl::span<const mtpPrime> answer);\n\tvoid pqSend(not_null<Attempt*> attempt, TimeId expiresIn);\n\tvoid pqAnswered(\n\t\tnot_null<Attempt*> attempt,\n\t\tconst MTPresPQ &data);\n\tvoid dhParamsAnswered(\n\t\tnot_null<Attempt*> attempt,\n\t\tconst MTPserver_DH_Params &data);\n\tvoid dhClientParamsSend(not_null<Attempt*> attempt);\n\tvoid dhClientParamsAnswered(\n\t\tnot_null<Attempt*> attempt,\n\t\tconst MTPset_client_DH_params_answer &data);\n\n\tvoid stopReceiving();\n\tvoid failed(DcKeyError error = DcKeyError::Other);\n\tvoid done();\n\n\tconst not_null<AbstractConnection*> _connection;\n\tconst not_null<DcOptions*> _dcOptions;\n\tconst DcId _dcId = 0;\n\tconst int16 _protocolDcId = 0;\n\tconst DcKeyRequest _request;\n\tDelegate _delegate;\n\n\tAttempt _temporary;\n\tAttempt _persistent;\n\n};\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_dcenter.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/details/mtproto_dcenter.h\"\n\n#include \"mtproto/facade.h\"\n#include \"mtproto/mtproto_auth_key.h\"\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"mtproto/mtp_instance.h\"\n#include \"mtproto/special_config_request.h\"\n\nnamespace MTP {\nnamespace details {\nnamespace {\n\nint IndexByType(TemporaryKeyType type) {\n\tswitch (type) {\n\tcase TemporaryKeyType::Regular: return 0;\n\tcase TemporaryKeyType::MediaCluster: return 1;\n\t}\n\tUnexpected(\"Type value in IndexByType.\");\n}\n\nint IndexByType(CreatingKeyType type) {\n\tswitch (type) {\n\tcase CreatingKeyType::Persistent:\n\tcase CreatingKeyType::TemporaryRegular: return 0;\n\tcase CreatingKeyType::TemporaryMediaCluster: return 1;\n\t}\n\tUnexpected(\"Creating type value in IndexByType.\");\n}\n\nconst char *NameOfType(CreatingKeyType type) {\n\tswitch (type) {\n\tcase CreatingKeyType::Persistent: return \"persistent\";\n\tcase CreatingKeyType::TemporaryRegular: return \"regular\";\n\tcase CreatingKeyType::TemporaryMediaCluster: return \"media\";\n\t}\n\tUnexpected(\"Type value in NameOfType.\");\n}\n\n} // namespace\n\n\nTemporaryKeyType TemporaryKeyTypeByDcType(DcType type) {\n\treturn (type == DcType::MediaCluster)\n\t\t? TemporaryKeyType::MediaCluster\n\t\t: TemporaryKeyType::Regular;\n}\n\nDcenter::Dcenter(DcId dcId, AuthKeyPtr &&key)\n: _id(dcId)\n, _persistentKey(std::move(key)) {\n}\n\nDcId Dcenter::id() const {\n\treturn _id;\n}\n\nAuthKeyPtr Dcenter::getTemporaryKey(TemporaryKeyType type) const {\n\tQReadLocker lock(&_mutex);\n\treturn _temporaryKeys[IndexByType(type)];\n}\n\nAuthKeyPtr Dcenter::getPersistentKey() const {\n\tQReadLocker lock(&_mutex);\n\treturn _persistentKey;\n}\n\nbool Dcenter::destroyTemporaryKey(uint64 keyId) {\n\tQWriteLocker lock(&_mutex);\n\tfor (auto &key : _temporaryKeys) {\n\t\tif (key && key->keyId() == keyId) {\n\t\t\tkey = nullptr;\n\t\t\t_connectionInited = false;\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool Dcenter::destroyConfirmedForgottenKey(uint64 keyId) {\n\tQWriteLocker lock(&_mutex);\n\tif (!_persistentKey || _persistentKey->keyId() != keyId) {\n\t\treturn false;\n\t}\n\tfor (auto &key : _temporaryKeys) {\n\t\tkey = nullptr;\n\t}\n\t_persistentKey = nullptr;\n\t_connectionInited = false;\n\treturn true;\n}\n\nbool Dcenter::connectionInited() const {\n\tQReadLocker lock(&_mutex);\n\treturn _connectionInited;\n}\n\nvoid Dcenter::setConnectionInited(bool connectionInited) {\n\tQWriteLocker lock(&_mutex);\n\t_connectionInited = connectionInited;\n}\n\nCreatingKeyType Dcenter::acquireKeyCreation(DcType type) {\n\tQReadLocker lock(&_mutex);\n\tconst auto keyType = TemporaryKeyTypeByDcType(type);\n\tconst auto index = IndexByType(keyType);\n\tauto &key = _temporaryKeys[index];\n\tif (key != nullptr) {\n\t\treturn CreatingKeyType::None;\n\t}\n\tauto expected = false;\n\tconst auto regular = IndexByType(TemporaryKeyType::Regular);\n\tif (keyType == TemporaryKeyType::MediaCluster && _temporaryKeys[regular]) {\n\t\treturn !_creatingKeys[index].compare_exchange_strong(expected, true)\n\t\t\t? CreatingKeyType::None\n\t\t\t: CreatingKeyType::TemporaryMediaCluster;\n\t}\n\treturn !_creatingKeys[regular].compare_exchange_strong(expected, true)\n\t\t? CreatingKeyType::None\n\t\t: (type != DcType::Cdn && !_persistentKey)\n\t\t? CreatingKeyType::Persistent\n\t\t: CreatingKeyType::TemporaryRegular;\n}\n\nbool Dcenter::releaseKeyCreationOnDone(\n\t\tCreatingKeyType type,\n\t\tconst AuthKeyPtr &temporaryKey,\n\t\tconst AuthKeyPtr &persistentKeyUsedForBind) {\n\tExpects(_creatingKeys[IndexByType(type)]);\n\tExpects(_temporaryKeys[IndexByType(type)] == nullptr);\n\tExpects(temporaryKey != nullptr);\n\n\tQWriteLocker lock(&_mutex);\n\tif (type == CreatingKeyType::Persistent) {\n\t\t_persistentKey = persistentKeyUsedForBind;\n\t} else if (_persistentKey != persistentKeyUsedForBind) {\n\t\treturn false;\n\t}\n\t_temporaryKeys[IndexByType(type)] = temporaryKey;\n\t_creatingKeys[IndexByType(type)] = false;\n\t_connectionInited = false;\n\n\tDEBUG_LOG((\"AuthKey Info: Dcenter::releaseKeyCreationOnDone(%1, %2, %3).\"\n\t\t).arg(NameOfType(type)\n\t\t).arg(temporaryKey ? temporaryKey->keyId() : 0\n\t\t).arg(persistentKeyUsedForBind\n\t\t\t? persistentKeyUsedForBind->keyId()\n\t\t\t: 0));\n\treturn true;\n}\n\nvoid Dcenter::releaseKeyCreationOnFail(CreatingKeyType type) {\n\tExpects(_creatingKeys[IndexByType(type)]);\n\tExpects(_temporaryKeys[IndexByType(type)] == nullptr);\n\n\t_creatingKeys[IndexByType(type)] = false;\n}\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_dcenter.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QReadWriteLock>\n\nnamespace MTP {\n\nclass Instance;\nclass AuthKey;\nusing AuthKeyPtr = std::shared_ptr<AuthKey>;\nenum class DcType;\n\nnamespace details {\n\nenum class TemporaryKeyType {\n\tRegular,\n\tMediaCluster\n};\n\nenum class CreatingKeyType {\n\tNone,\n\tPersistent,\n\tTemporaryRegular,\n\tTemporaryMediaCluster\n};\n\n[[nodiscard]] TemporaryKeyType TemporaryKeyTypeByDcType(DcType type);\n\nclass Dcenter : public QObject {\npublic:\n\t// Main thread.\n\tDcenter(DcId dcId, AuthKeyPtr &&key);\n\n\t// Thread-safe.\n\t[[nodiscard]] DcId id() const;\n\t[[nodiscard]] AuthKeyPtr getPersistentKey() const;\n\t[[nodiscard]] AuthKeyPtr getTemporaryKey(TemporaryKeyType type) const;\n\t[[nodiscard]] CreatingKeyType acquireKeyCreation(DcType type);\n\tbool releaseKeyCreationOnDone(\n\t\tCreatingKeyType type,\n\t\tconst AuthKeyPtr &temporaryKey,\n\t\tconst AuthKeyPtr &persistentKeyUsedForBind);\n\tvoid releaseKeyCreationOnFail(CreatingKeyType type);\n\tbool destroyTemporaryKey(uint64 keyId);\n\tbool destroyConfirmedForgottenKey(uint64 keyId);\n\n\t[[nodiscard]] bool connectionInited() const;\n\tvoid setConnectionInited(bool connectionInited = true);\n\nprivate:\n\tstatic constexpr auto kTemporaryKeysCount = 2;\n\n\tconst DcId _id = 0;\n\tmutable QReadWriteLock _mutex;\n\n\tAuthKeyPtr _temporaryKeys[kTemporaryKeysCount];\n\tAuthKeyPtr _persistentKey;\n\tbool _connectionInited = false;\n\tstd::atomic<bool> _creatingKeys[kTemporaryKeysCount] = { false };\n\n};\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/details/mtproto_domain_resolver.h\"\n\n#include \"base/random.h\"\n#include \"base/invoke_queued.h\"\n#include \"base/call_delayed.h\"\n\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonArray>\n#include <QtCore/QJsonObject>\n#include <range/v3/algorithm/shuffle.hpp>\n#include <range/v3/algorithm/reverse.hpp>\n#include <range/v3/algorithm/remove.hpp>\n#include <random>\n\nnamespace MTP::details {\nnamespace {\n\nconstexpr auto kSendNextTimeout = crl::time(800);\nconstexpr auto kMinTimeToLive = 10 * crl::time(1000);\nconstexpr auto kMaxTimeToLive = 300 * crl::time(1000);\n\n} // namespace\n\nconst std::vector<QString> &DnsDomains() {\n\tstatic const auto kResult = std::vector<QString>{\n\t\t\"google.com\",\n\t\t\"www.google.com\",\n\t\t\"google.ru\",\n\t\t\"www.google.ru\",\n\t};\n\treturn kResult;\n}\n\nQString GenerateDnsRandomPadding() {\n\tconstexpr char kValid[] = \"abcdefghijklmnopqrstuvwxyz\"\n\t\t\"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n\n\tauto result = QString();\n\tconst auto count = [&] {\n\t\tconstexpr auto kMinPadding = 13;\n\t\tconstexpr auto kMaxPadding = 128;\n\t\twhile (true) {\n\t\t\tconst auto result = 1 + (base::RandomValue<uchar>() / 2);\n\t\t\tAssert(result <= kMaxPadding);\n\t\t\tif (result >= kMinPadding) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}();\n\tresult.resize(count);\n\tfor (auto &ch : result) {\n\t\tch = kValid[base::RandomValue<uchar>() % (sizeof(kValid) - 1)];\n\t}\n\treturn result;\n}\n\nQByteArray DnsUserAgent() {\n\tstatic const auto kResult = QByteArray(\n\t\t\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \"\n\t\t\"AppleWebKit/537.36 (KHTML, like Gecko) \"\n\t\t\"Chrome/142.0.0.0 Safari/537.36\");\n\treturn kResult;\n}\n\nstd::vector<DnsEntry> ParseDnsResponse(\n\t\tconst QByteArray &bytes,\n\t\tstd::optional<int> typeRestriction) {\n\tif (bytes.isEmpty()) {\n\t\treturn {};\n\t}\n\n\t// Read and store to \"result\" all the data bytes from the response:\n\t// { ..,\n\t//   \"Answer\": [\n\t//     { .., \"data\": \"bytes1\", \"TTL\": int, .. },\n\t//     { .., \"data\": \"bytes2\", \"TTL\": int, .. }\n\t//   ],\n\t// .. }\n\tauto error = QJsonParseError{ 0, QJsonParseError::NoError };\n\tconst auto document = QJsonDocument::fromJson(bytes, &error);\n\tif (error.error != QJsonParseError::NoError) {\n\t\tLOG((\"Config Error: Failed to parse dns response JSON, error: %1\"\n\t\t\t).arg(error.errorString()));\n\t\treturn {};\n\t} else if (!document.isObject()) {\n\t\tLOG((\"Config Error: Not an object received in dns response JSON.\"));\n\t\treturn {};\n\t}\n\tconst auto response = document.object();\n\tconst auto answerIt = response.find(\"Answer\");\n\tif (answerIt == response.constEnd()) {\n\t\tLOG((\"Config Error: Could not find Answer in dns response JSON.\"));\n\t\treturn {};\n\t} else if (!(*answerIt).isArray()) {\n\t\tLOG((\"Config Error: Not an array received \"\n\t\t\t\"in Answer in dns response JSON.\"));\n\t\treturn {};\n\t}\n\n\tconst auto array = (*answerIt).toArray();\n\tauto result = std::vector<DnsEntry>();\n\tfor (const auto elem : array) {\n\t\tif (!elem.isObject()) {\n\t\t\tLOG((\"Config Error: Not an object found \"\n\t\t\t\t\"in Answer array in dns response JSON.\"));\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto object = elem.toObject();\n\t\tif (typeRestriction) {\n\t\t\tconst auto typeIt = object.find(\"type\");\n\t\t\tconst auto type = int(base::SafeRound((*typeIt).toDouble()));\n\t\t\tif (!(*typeIt).isDouble()) {\n\t\t\t\tLOG((\"Config Error: Not a number in type field \"\n\t\t\t\t\t\"in Answer array in dns response JSON.\"));\n\t\t\t\tcontinue;\n\t\t\t} else if (type != *typeRestriction) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tconst auto dataIt = object.find(\"data\");\n\t\tif (dataIt == object.constEnd()) {\n\t\t\tLOG((\"Config Error: Could not find data \"\n\t\t\t\t\"in Answer array entry in dns response JSON.\"));\n\t\t\tcontinue;\n\t\t} else if (!(*dataIt).isString()) {\n\t\t\tLOG((\"Config Error: Not a string data found \"\n\t\t\t\t\"in Answer array entry in dns response JSON.\"));\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto ttlIt = object.find(\"TTL\");\n\t\tconst auto ttl = (ttlIt != object.constEnd())\n\t\t\t? crl::time(base::SafeRound((*ttlIt).toDouble()))\n\t\t\t: crl::time(0);\n\t\tresult.push_back({ (*dataIt).toString(), ttl });\n\t}\n\treturn result;\n}\n\nServiceWebRequest::ServiceWebRequest(not_null<QNetworkReply*> reply)\n: reply(reply.get()) {\n}\n\nServiceWebRequest::ServiceWebRequest(ServiceWebRequest &&other)\n: reply(base::take(other.reply)) {\n}\n\nServiceWebRequest &ServiceWebRequest::operator=(ServiceWebRequest &&other) {\n\tif (reply != other.reply) {\n\t\tdestroy();\n\t\treply = base::take(other.reply);\n\t}\n\treturn *this;\n}\n\nvoid ServiceWebRequest::destroy() {\n\tif (const auto value = base::take(reply)) {\n\t\tvalue->disconnect(\n\t\t\tvalue,\n\t\t\t&QNetworkReply::finished,\n\t\t\tnullptr,\n\t\t\tnullptr);\n\t\tvalue->abort();\n\t\tvalue->deleteLater();\n\t}\n}\n\nServiceWebRequest::~ServiceWebRequest() {\n\tif (reply) {\n\t\treply->deleteLater();\n\t}\n}\n\nDomainResolver::DomainResolver(Fn<void(\n\tconst QString &host,\n\tconst QStringList &ips,\n\tcrl::time expireAt)> callback)\n: _callback(std::move(callback)) {\n\t_manager.setProxy(QNetworkProxy::NoProxy);\n}\n\nvoid DomainResolver::resolve(const QString &domain) {\n\tresolve({ domain, false });\n\tresolve({ domain, true });\n}\n\nvoid DomainResolver::resolve(const AttemptKey &key) {\n\tif (_attempts.find(key) != end(_attempts)) {\n\t\treturn;\n\t} else if (_requests.find(key) != end(_requests)) {\n\t\treturn;\n\t}\n\tconst auto i = _cache.find(key);\n\t_lastTimestamp = crl::now();\n\tif (i != end(_cache) && i->second.expireAt > _lastTimestamp) {\n\t\tcheckExpireAndPushResult(key.domain);\n\t\treturn;\n\t}\n\n\tauto attempts = std::vector<Attempt>();\n\tauto domains = DnsDomains();\n\tstd::random_device rd;\n\tranges::shuffle(domains, std::mt19937(rd()));\n\tconst auto takeDomain = [&] {\n\t\tconst auto result = domains.back();\n\t\tdomains.pop_back();\n\t\treturn result;\n\t};\n\tconst auto shuffle = [&](int from, int till) {\n\t\tExpects(till > from);\n\n\t\tranges::shuffle(\n\t\t\tbegin(attempts) + from,\n\t\t\tbegin(attempts) + till,\n\t\t\tstd::mt19937(rd()));\n\t};\n\n\tattempts.push_back({ Type::Google, \"dns.google.com\" });\n\tattempts.push_back({ Type::Google, takeDomain(), \"dns\" });\n\tattempts.push_back({ Type::Mozilla, \"mozilla.cloudflare-dns.com\" });\n\twhile (!domains.empty()) {\n\t\tattempts.push_back({ Type::Google, takeDomain(), \"dns\" });\n\t}\n\n\tshuffle(0, 2);\n\n\tranges::reverse(attempts); // We go from last to first.\n\n\t_attempts.emplace(key, Attempts{ std::move(attempts) });\n\tsendNextRequest(key);\n}\n\nvoid DomainResolver::checkExpireAndPushResult(const QString &domain) {\n\tconst auto ipv4 = _cache.find({ domain, false });\n\tif (ipv4 == end(_cache) || ipv4->second.expireAt <= _lastTimestamp) {\n\t\treturn;\n\t}\n\tauto result = ipv4->second;\n\tconst auto ipv6 = _cache.find({ domain, true });\n\tif (ipv6 != end(_cache) && ipv6->second.expireAt > _lastTimestamp) {\n\t\tresult.ips.append(ipv6->second.ips);\n\t\taccumulate_min(result.expireAt, ipv6->second.expireAt);\n\t}\n\tInvokeQueued(this, [=] {\n\t\t_callback(domain, result.ips, result.expireAt);\n\t});\n}\n\nvoid DomainResolver::sendNextRequest(const AttemptKey &key) {\n\tauto i = _attempts.find(key);\n\tif (i == end(_attempts)) {\n\t\treturn;\n\t}\n\tauto &attempts = i->second;\n\tauto &list = attempts.list;\n\tconst auto attempt = list.back();\n\tlist.pop_back();\n\n\tif (!list.empty()) {\n\t\tbase::call_delayed(kSendNextTimeout, &attempts.guard, [=] {\n\t\t\tsendNextRequest(key);\n\t\t});\n\t}\n\tperformRequest(key, attempt);\n}\n\nvoid DomainResolver::performRequest(\n\t\tconst AttemptKey &key,\n\t\tconst Attempt &attempt) {\n\tauto url = QUrl();\n\turl.setScheme(\"https\");\n\tauto request = QNetworkRequest();\n\tswitch (attempt.type) {\n\tcase Type::Mozilla: {\n\t\turl.setHost(attempt.data);\n\t\turl.setPath(\"/dns-query\");\n\t\turl.setQuery(QStringLiteral(\"name=%1&type=%2&random_padding=%3\"\n\t\t).arg(key.domain\n\t\t).arg(key.ipv6 ? 28 : 1\n\t\t).arg(GenerateDnsRandomPadding()));\n\t\trequest.setRawHeader(\"accept\", \"application/dns-json\");\n\t} break;\n\tcase Type::Google: {\n\t\turl.setHost(attempt.data);\n\t\turl.setPath(\"/resolve\");\n\t\turl.setQuery(QStringLiteral(\"name=%1&type=%2&random_padding=%3\"\n\t\t).arg(key.domain\n\t\t).arg(key.ipv6 ? 28 : 1\n\t\t).arg(GenerateDnsRandomPadding()));\n\t\tif (!attempt.host.isEmpty()) {\n\t\t\tconst auto host = attempt.host + \".google.com\";\n\t\t\trequest.setRawHeader(\"Host\", host.toLatin1());\n\t\t}\n\t} break;\n\tdefault: Unexpected(\"Type in DomainResolver::performRequest.\");\n\t}\n\trequest.setUrl(url);\n\trequest.setRawHeader(\"User-Agent\", DnsUserAgent());\n\tconst auto i = _requests.emplace(\n\t\tkey,\n\t\tstd::vector<ServiceWebRequest>()).first;\n\tconst auto reply = i->second.emplace_back(\n\t\t_manager.get(request)\n\t).reply;\n\tconnect(reply, &QNetworkReply::finished, this, [=] {\n\t\trequestFinished(key, reply);\n\t});\n}\n\nvoid DomainResolver::requestFinished(\n\t\tconst AttemptKey &key,\n\t\tnot_null<QNetworkReply*> reply) {\n\tconst auto result = finalizeRequest(key, reply);\n\tconst auto response = ParseDnsResponse(result);\n\tif (response.empty()) {\n\t\treturn;\n\t}\n\t_requests.erase(key);\n\t_attempts.erase(key);\n\n\tauto entry = CacheEntry();\n\tauto ttl = kMaxTimeToLive;\n\tfor (const auto &item : response) {\n\t\tentry.ips.push_back(item.data);\n\t\tttl = std::min(\n\t\t\tttl,\n\t\t\tstd::max(item.TTL * crl::time(1000), kMinTimeToLive));\n\t}\n\t_lastTimestamp = crl::now();\n\tentry.expireAt = _lastTimestamp + ttl;\n\t_cache[key] = std::move(entry);\n\n\tcheckExpireAndPushResult(key.domain);\n}\n\nQByteArray DomainResolver::finalizeRequest(\n\t\tconst AttemptKey &key,\n\t\tnot_null<QNetworkReply*> reply) {\n\tif (reply->error() != QNetworkReply::NoError) {\n\t\tDEBUG_LOG((\"Resolve Error: Failed to get response, error: %2 (%3)\"\n\t\t\t).arg(reply->errorString()\n\t\t\t).arg(reply->error()));\n\t}\n\tconst auto result = reply->readAll();\n\tconst auto i = _requests.find(key);\n\tif (i != end(_requests)) {\n\t\tauto &requests = i->second;\n\t\tconst auto from = ranges::remove(\n\t\t\trequests,\n\t\t\treply,\n\t\t\t[](const ServiceWebRequest &request) { return request.reply; });\n\t\trequests.erase(from, end(requests));\n\t\tif (requests.empty()) {\n\t\t\t_requests.erase(i);\n\t\t}\n\t}\n\treturn result;\n}\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n\n#include <QtCore/QPointer>\n#include <QtNetwork/QNetworkReply>\n#include <optional>\n\nnamespace MTP::details {\n\n[[nodiscard]] const std::vector<QString> &DnsDomains();\n[[nodiscard]] QString GenerateDnsRandomPadding();\n[[nodiscard]] QByteArray DnsUserAgent();\n\nstruct DnsEntry {\n\tQString data;\n\tcrl::time TTL = 0;\n};\n\n[[nodiscard]] std::vector<DnsEntry> ParseDnsResponse(\n\tconst QByteArray &bytes,\n\tstd::optional<int> typeRestriction = std::nullopt);\n\nstruct ServiceWebRequest {\n\tServiceWebRequest(not_null<QNetworkReply*> reply);\n\tServiceWebRequest(ServiceWebRequest &&other);\n\tServiceWebRequest &operator=(ServiceWebRequest &&other);\n\t~ServiceWebRequest();\n\n\tvoid destroy();\n\n\tQPointer<QNetworkReply> reply;\n};\n\nclass DomainResolver : public QObject {\npublic:\n\tDomainResolver(Fn<void(\n\t\tconst QString &domain,\n\t\tconst QStringList &ips,\n\t\tcrl::time expireAt)> callback);\n\n\tvoid resolve(const QString &domain);\n\nprivate:\n\tenum class Type {\n\t\tMozilla,\n\t\tGoogle,\n\t};\n\tstruct Attempt {\n\t\tType type;\n\t\tQString data;\n\t\tQString host;\n\t};\n\tstruct AttemptKey {\n\t\tQString domain;\n\t\tbool ipv6 = false;\n\n\t\tinline bool operator<(const AttemptKey &other) const {\n\t\t\treturn (domain < other.domain)\n\t\t\t\t|| (domain == other.domain && !ipv6 && other.ipv6);\n\t\t}\n\t\tinline bool operator==(const AttemptKey &other) const {\n\t\t\treturn (domain == other.domain) && (ipv6 == other.ipv6);\n\t\t}\n\t};\n\tstruct CacheEntry {\n\t\tQStringList ips;\n\t\tcrl::time expireAt = 0;\n\t};\n\tstruct Attempts {\n\t\tstd::vector<Attempt> list;\n\t\tbase::has_weak_ptr guard;\n\t};\n\n\tvoid resolve(const AttemptKey &key);\n\tvoid sendNextRequest(const AttemptKey &key);\n\tvoid performRequest(const AttemptKey &key, const Attempt &attempt);\n\tvoid checkExpireAndPushResult(const QString &domain);\n\tvoid requestFinished(\n\t\tconst AttemptKey &key,\n\t\tnot_null<QNetworkReply*> reply);\n\tQByteArray finalizeRequest(\n\t\tconst AttemptKey &key,\n\t\tnot_null<QNetworkReply*> reply);\n\n\tFn<void(\n\t\tconst QString &domain,\n\t\tconst QStringList &ips,\n\t\tcrl::time expireAt)> _callback;\n\n\tQNetworkAccessManager _manager;\n\tstd::map<AttemptKey, Attempts> _attempts;\n\tstd::map<AttemptKey, std::vector<ServiceWebRequest>> _requests;\n\tstd::map<AttemptKey, CacheEntry> _cache;\n\tcrl::time _lastTimestamp = 0;\n\n};\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_dump_to_text.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/details/mtproto_dump_to_text.h\"\n\n#include \"scheme-dump_to_text.h\"\n#include \"scheme.h\"\n\n#include <zlib.h>\n\nnamespace MTP::details {\n\nbool DumpToTextCore(DumpToTextBuffer &to, const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons, uint32 level, mtpPrime vcons) {\n\tswitch (mtpTypeId(cons)) {\n\tcase mtpc_int: {\n\t\tMTPint value;\n\t\tif (value.read(from, end, cons)) {\n\t\t\tto.add(QString::number(value.v)).add(\" [INT]\");\n\t\t\treturn true;\n\t\t}\n\t} break;\n\n\tcase mtpc_long: {\n\t\tMTPlong value;\n\t\tif (value.read(from, end, cons)) {\n\t\t\tto.add(QString::number(value.v)).add(\" [LONG]\");\n\t\t\treturn true;\n\t\t}\n\t} break;\n\n\tcase mtpc_int128: {\n\t\tMTPint128 value;\n\t\tif (value.read(from, end, cons)) {\n\t\t\tto.add(QString::number(value.h)).add(\" * 2^64 + \").add(QString::number(value.l)).add(\" [INT128]\");\n\t\t\treturn true;\n\t\t}\n\t} break;\n\n\tcase mtpc_int256: {\n\t\tMTPint256 value;\n\t\tif (value.read(from, end, cons)) {\n\t\t\tto.add(QString::number(value.h.h)).add(\" * 2^192 + \").add(QString::number(value.h.l)).add(\" * 2^128 + \").add(QString::number(value.l.h)).add(\" * 2 ^ 64 + \").add(QString::number(value.l.l)).add(\" [INT256]\");\n\t\t\treturn true;\n\t\t}\n\t} break;\n\n\tcase mtpc_double: {\n\t\tMTPdouble value;\n\t\tif (value.read(from, end, cons)) {\n\t\t\tto.add(QString::number(value.v)).add(\" [DOUBLE]\");\n\t\t\treturn true;\n\t\t}\n\t} break;\n\n\tcase mtpc_string: {\n\t\tMTPstring value;\n\t\tif (value.read(from, end, cons)) {\n\t\t\tauto strUtf8 = value.v;\n\t\t\tauto str = QString::fromUtf8(strUtf8);\n\t\t\tif (str.toUtf8() == strUtf8) {\n\t\t\t\tto.add(\"\\\"\").add(str.replace('\\\\', \"\\\\\\\\\").replace('\"', \"\\\\\\\"\").replace('\\n', \"\\\\n\")).add(\"\\\" [STRING]\");\n\t\t\t} else if (strUtf8.size() < 64) {\n\t\t\t\tto.add(Logs::mb(strUtf8.constData(), strUtf8.size()).str()).add(\" [\").add(QString::number(strUtf8.size())).add(\" BYTES]\");\n\t\t\t} else {\n\t\t\t\tto.add(Logs::mb(strUtf8.constData(), 16).str()).add(\"... [\").add(QString::number(strUtf8.size())).add(\" BYTES]\");\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t} break;\n\n\tcase mtpc_vector: {\n\t\tif (from < end) {\n\t\t\tint32 cnt = *(from++);\n\t\t\tto.add(\"[ vector<0x\").add(QString::number(vcons, 16)).add(\"> (\").add(QString::number(cnt)).add(\")\");\n\t\t\tif (cnt) {\n\t\t\t\tto.add(\"\\n\").addSpaces(level);\n\t\t\t\tfor (int32 i = 0; i < cnt; ++i) {\n\t\t\t\t\tto.add(\"  \");\n\t\t\t\t\tif (!DumpToTextType(to, from, end, vcons, level + 1)) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\tto.add(\",\\n\").addSpaces(level);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tto.add(\" \");\n\t\t\t}\n\t\t\tto.add(\"]\");\n\t\t\treturn true;\n\t\t}\n\t} break;\n\n\tcase mtpc_gzip_packed: {\n\t\tMTPstring packed;\n\t\t// read packed string as serialized mtp string type\n\t\tif (!packed.read(from, end)) {\n\t\t\treturn false;\n\t\t}\n\t\tuint32 packedLen = packed.v.size(), unpackedChunk = packedLen;\n\t\tmtpBuffer result; // * 4 because of mtpPrime type\n\t\tresult.resize(0);\n\n\t\tz_stream stream;\n\t\tstream.zalloc = nullptr;\n\t\tstream.zfree = nullptr;\n\t\tstream.opaque = nullptr;\n\t\tstream.avail_in = 0;\n\t\tstream.next_in = nullptr;\n\t\tint res = inflateInit2(&stream, 16 + MAX_WBITS);\n\t\tif (res != Z_OK) {\n\t\t\treturn false;\n\t\t}\n\t\tstream.avail_in = packedLen;\n\t\tstream.next_in = reinterpret_cast<Bytef*>(packed.v.data());\n\t\tstream.avail_out = 0;\n\t\twhile (!stream.avail_out) {\n\t\t\tresult.resize(result.size() + unpackedChunk);\n\t\t\tstream.avail_out = unpackedChunk * sizeof(mtpPrime);\n\t\t\tstream.next_out = (Bytef*)&result[result.size() - unpackedChunk];\n\t\t\tint res = inflate(&stream, Z_NO_FLUSH);\n\t\t\tif (res != Z_OK && res != Z_STREAM_END) {\n\t\t\t\tinflateEnd(&stream);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\tif (stream.avail_out & 0x03) {\n\t\t\treturn false;\n\t\t}\n\t\tresult.resize(result.size() - (stream.avail_out >> 2));\n\t\tinflateEnd(&stream);\n\n\t\tif (result.empty()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst mtpPrime *newFrom = result.constData(), *newEnd = result.constData() + result.size();\n\t\tto.add(\"[GZIPPED] \");\n\t\treturn DumpToTextType(to, newFrom, newEnd, 0, level);\n\t} break;\n\n\tdefault: {\n\t\tfor (uint32 i = 1; i < mtpLayerMaxSingle; ++i) {\n\t\t\tif (cons == mtpLayers[i]) {\n\t\t\t\tto.add(\"[LAYER\").add(QString::number(i + 1)).add(\"] \");\n\t\t\t\treturn DumpToTextType(to, from, end, 0, level);\n\t\t\t}\n\t\t}\n\t\tif (cons == mtpc_invokeWithLayer) {\n\t\t\tif (from >= end) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tint32 layer = *(from++);\n\t\t\tto.add(\"[LAYER\").add(QString::number(layer)).add(\"] \");\n\t\t\treturn DumpToTextType(to, from, end, 0, level);\n\t\t}\n\t} break;\n\t}\n\treturn false;\n}\n\nQString DumpToText(const mtpPrime *&from, const mtpPrime *end) {\n\tDumpToTextBuffer to;\n\t[[maybe_unused]] bool result = DumpToTextType(to, from, end, mtpc_core_message);\n\treturn QString::fromUtf8(to.p, to.size);\n}\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_dump_to_text.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/core_types.h\"\n#include \"mtproto/details/mtproto_serialized_request.h\"\n\nnamespace MTP::details {\n\n// Human-readable text serialization\nQString DumpToText(const mtpPrime *&from, const mtpPrime *end);\n\nstruct DumpToTextBuffer {\n\tstatic constexpr auto kBufferSize = 1024 * 1024; // 1 mb start size\n\n\tDumpToTextBuffer()\n\t\t: p(new char[kBufferSize])\n\t\t, alloced(kBufferSize) {\n\t}\n\t~DumpToTextBuffer() {\n\t\tdelete[] p;\n\t}\n\n\tDumpToTextBuffer &add(const QString &data) {\n\t\tauto d = data.toUtf8();\n\t\treturn add(d.constData(), d.size());\n\t}\n\n\tDumpToTextBuffer &add(const char *data, int32 len = -1) {\n\t\tif (len < 0) len = strlen(data);\n\t\tif (!len) return (*this);\n\n\t\tensureLength(len);\n\t\tmemcpy(p + size, data, len);\n\t\tsize += len;\n\t\treturn (*this);\n\t}\n\n\tDumpToTextBuffer &addSpaces(int32 level) {\n\t\tint32 len = level * 2;\n\t\tif (!len) return (*this);\n\n\t\tensureLength(len);\n\t\tfor (char *ptr = p + size, *end = ptr + len; ptr != end; ++ptr) {\n\t\t\t*ptr = ' ';\n\t\t}\n\t\tsize += len;\n\t\treturn (*this);\n\t}\n\n\tDumpToTextBuffer &error(const char *problem = \"could not decode type\") {\n\t\treturn add(\"[ERROR] (\").add(problem).add(\")\");\n\t}\n\n\tvoid ensureLength(int32 add) {\n\t\tif (size + add <= alloced) return;\n\n\t\tint32 newsize = size + add;\n\t\tif (newsize % kBufferSize) {\n\t\t\tnewsize += kBufferSize - (newsize % kBufferSize);\n\t\t}\n\t\tchar *b = new char[newsize];\n\t\tmemcpy(b, p, size);\n\t\talloced = newsize;\n\t\tdelete[] p;\n\t\tp = b;\n\t}\n\n\tchar *p = nullptr;\n\tint size = 0;\n\tint alloced = 0;\n\n};\n\n[[nodiscard]] bool DumpToTextCore(DumpToTextBuffer &to, const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons, uint32 level, mtpPrime vcons = 0);\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_received_ids_manager.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/details/mtproto_received_ids_manager.h\"\n\nnamespace MTP::details {\n\nReceivedIdsManager::Result ReceivedIdsManager::registerMsgId(\n\t\tmtpMsgId msgId,\n\t\tbool needAck) {\n\tconst auto i = _idsNeedAck.find(msgId);\n\tif (i != _idsNeedAck.end()) {\n\t\tMTP_LOG(-1, (\"No need to handle - %1 already is in map\").arg(msgId));\n\t\treturn Result::Duplicate;\n\t} else if (_idsNeedAck.size() < kIdsBufferSize || msgId > min()) {\n\t\t_idsNeedAck.emplace(msgId, needAck);\n\t\treturn Result::Success;\n\t}\n\tMTP_LOG(-1, (\"Reset on too old - %1 < min = %2\").arg(msgId).arg(min()));\n\treturn Result::TooOld;\n}\n\nmtpMsgId ReceivedIdsManager::min() const {\n\treturn _idsNeedAck.empty() ? 0 : _idsNeedAck.begin()->first;\n}\n\nmtpMsgId ReceivedIdsManager::max() const {\n\tauto end = _idsNeedAck.end();\n\treturn _idsNeedAck.empty() ? 0 : (--end)->first;\n}\n\nReceivedIdsManager::State ReceivedIdsManager::lookup(mtpMsgId msgId) const {\n\tauto i = _idsNeedAck.find(msgId);\n\tif (i == _idsNeedAck.end()) {\n\t\treturn State::NotFound;\n\t}\n\treturn i->second ? State::NeedsAck : State::NoAckNeeded;\n}\n\nvoid ReceivedIdsManager::shrink() {\n\tauto size = _idsNeedAck.size();\n\twhile (size-- > kIdsBufferSize) {\n\t\t_idsNeedAck.erase(_idsNeedAck.begin());\n\t}\n}\n\nvoid ReceivedIdsManager::clear() {\n\t_idsNeedAck.clear();\n}\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_received_ids_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flat_map.h\"\n\nnamespace MTP::details {\n\n// Received msgIds and wereAcked msgIds count stored.\ninline constexpr auto kIdsBufferSize = 400;\n\nclass ReceivedIdsManager final {\npublic:\n\tenum class State {\n\t\tNotFound,\n\t\tNeedsAck,\n\t\tNoAckNeeded,\n\t};\n\tenum class Result {\n\t\tSuccess,\n\t\tDuplicate,\n\t\tTooOld,\n\t};\n\n\t[[nodiscard]] Result registerMsgId(mtpMsgId msgId, bool needAck);\n\t[[nodiscard]] mtpMsgId min() const;\n\t[[nodiscard]] mtpMsgId max() const;\n\t[[nodiscard]] State lookup(mtpMsgId msgId) const;\n\n\tvoid shrink();\n\tvoid clear();\n\nprivate:\n\tbase::flat_map<mtpMsgId, bool> _idsNeedAck;\n\n};\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_rsa_public_key.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/details/mtproto_rsa_public_key.h\"\n\n#include \"base/openssl_help.h\"\n\nnamespace MTP::details {\nnamespace {\n\nenum class Format {\n\tRSAPublicKey,\n\tRSA_PUBKEY,\n\tUnknown,\n};\n\nstruct BIODeleter {\n\tvoid operator()(BIO *value) {\n\t\tBIO_free(value);\n\t}\n};\n\nFormat GuessFormat(bytes::const_span key) {\n\tconst auto array = QByteArray::fromRawData(\n\t\treinterpret_cast<const char*>(key.data()),\n\t\tkey.size());\n\tif (array.indexOf(\"BEGIN RSA PUBLIC KEY\") >= 0) {\n\t\treturn Format::RSAPublicKey;\n\t} else if (array.indexOf(\"BEGIN PUBLIC KEY\") >= 0) {\n\t\treturn Format::RSA_PUBKEY;\n\t}\n\treturn Format::Unknown;\n}\n\nRSA *CreateRaw(bytes::const_span key) {\n\tconst auto format = GuessFormat(key);\n\tconst auto bio = std::unique_ptr<BIO, BIODeleter>{\n\t\tBIO_new_mem_buf(\n\t\t\tconst_cast<gsl::byte*>(key.data()),\n\t\t\tkey.size()),\n\t};\n\tswitch (format) {\n\tcase Format::RSAPublicKey:\n\t\treturn PEM_read_bio_RSAPublicKey(bio.get(), nullptr, nullptr, nullptr);\n\tcase Format::RSA_PUBKEY:\n\t\treturn PEM_read_bio_RSA_PUBKEY(bio.get(), nullptr, nullptr, nullptr);\n\t}\n\tUnexpected(\"format in RSAPublicKey::Private::Create.\");\n}\n\n} // namespace\n\nclass RSAPublicKey::Private {\npublic:\n\texplicit Private(bytes::const_span key);\n\tPrivate(bytes::const_span nBytes, bytes::const_span eBytes);\n\t~Private();\n\n\t[[nodiscard]] bool valid() const;\n\t[[nodiscard]] uint64 fingerprint() const;\n\t[[nodiscard]] bytes::vector getN() const;\n\t[[nodiscard]] bytes::vector getE() const;\n\t[[nodiscard]] bytes::vector encrypt(bytes::const_span data) const;\n\t[[nodiscard]] bytes::vector decrypt(bytes::const_span data) const;\n\t[[nodiscard]] bytes::vector encryptOAEPpadding(\n\t\tbytes::const_span data) const;\n\nprivate:\n\tvoid computeFingerprint();\n\t[[nodiscard]] static bytes::vector ToBytes(const BIGNUM *number);\n\n\tRSA *_rsa = nullptr;\n\tuint64 _fingerprint = 0;\n\n};\n\nRSAPublicKey::Private::Private(bytes::const_span key)\n\t: _rsa(CreateRaw(key)) {\n\tif (_rsa) {\n\t\tcomputeFingerprint();\n\t}\n}\n\nRSAPublicKey::Private::Private(bytes::const_span nBytes, bytes::const_span eBytes)\n\t: _rsa(RSA_new()) {\n\tif (_rsa) {\n\t\tconst auto n = openssl::BigNum(nBytes).takeRaw();\n\t\tconst auto e = openssl::BigNum(eBytes).takeRaw();\n\t\tconst auto valid = (n != nullptr) && (e != nullptr);\n\t\t// We still pass both values to RSA_set0_key() so that even\n\t\t// if only one of them is valid RSA would take ownership of it.\n\t\tif (!RSA_set0_key(_rsa, n, e, nullptr) || !valid) {\n\t\t\tRSA_free(base::take(_rsa));\n\t\t} else {\n\t\t\tcomputeFingerprint();\n\t\t}\n\t}\n}\n\nbool RSAPublicKey::Private::valid() const {\n\treturn _rsa != nullptr;\n}\n\nuint64 RSAPublicKey::Private::fingerprint() const {\n\treturn _fingerprint;\n}\n\nbytes::vector RSAPublicKey::Private::getN() const {\n\tExpects(valid());\n\n\tconst BIGNUM *n;\n\tRSA_get0_key(_rsa, &n, nullptr, nullptr);\n\treturn ToBytes(n);\n}\n\nbytes::vector RSAPublicKey::Private::getE() const {\n\tExpects(valid());\n\n\tconst BIGNUM *e;\n\tRSA_get0_key(_rsa, nullptr, &e, nullptr);\n\treturn ToBytes(e);\n}\n\nbytes::vector RSAPublicKey::Private::encrypt(bytes::const_span data) const {\n\tExpects(valid());\n\n\tconstexpr auto kEncryptSize = 256;\n\tauto result = bytes::vector(kEncryptSize, gsl::byte{});\n\tauto res = RSA_public_encrypt(kEncryptSize, reinterpret_cast<const unsigned char*>(data.data()), reinterpret_cast<unsigned char*>(result.data()), _rsa, RSA_NO_PADDING);\n\tif (res < 0 || res > kEncryptSize) {\n\t\tOPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, nullptr);\n\t\tLOG((\"RSA Error: RSA_public_encrypt failed, key fp: %1, result: %2, error: %3\").arg(fingerprint()).arg(res).arg(ERR_error_string(ERR_get_error(), 0)));\n\t\treturn {};\n\t} else if (auto zeroBytes = kEncryptSize - res) {\n\t\tauto resultBytes = gsl::make_span(result);\n\t\tbytes::move(resultBytes.subspan(zeroBytes, res), resultBytes.subspan(0, res));\n\t\tbytes::set_with_const(resultBytes.subspan(0, zeroBytes), gsl::byte{});\n\t}\n\treturn result;\n}\n\nbytes::vector RSAPublicKey::Private::decrypt(bytes::const_span data) const {\n\tExpects(valid());\n\n\tconstexpr auto kDecryptSize = 256;\n\tauto result = bytes::vector(kDecryptSize, gsl::byte{});\n\tauto res = RSA_public_decrypt(kDecryptSize, reinterpret_cast<const unsigned char*>(data.data()), reinterpret_cast<unsigned char*>(result.data()), _rsa, RSA_NO_PADDING);\n\tif (res < 0 || res > kDecryptSize) {\n\t\tOPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, nullptr);\n\t\tLOG((\"RSA Error: RSA_public_encrypt failed, key fp: %1, result: %2, error: %3\").arg(fingerprint()).arg(res).arg(ERR_error_string(ERR_get_error(), 0)));\n\t\treturn {};\n\t} else if (auto zeroBytes = kDecryptSize - res) {\n\t\tauto resultBytes = gsl::make_span(result);\n\t\tbytes::move(resultBytes.subspan(zeroBytes - res, res), resultBytes.subspan(0, res));\n\t\tbytes::set_with_const(resultBytes.subspan(0, zeroBytes - res), gsl::byte{});\n\t}\n\treturn result;\n}\n\nbytes::vector RSAPublicKey::Private::encryptOAEPpadding(bytes::const_span data) const {\n\tExpects(valid());\n\n\tconst auto resultSize = RSA_size(_rsa);\n\tauto result = bytes::vector(resultSize, gsl::byte{});\n\tconst auto encryptedSize = RSA_public_encrypt(\n\t\tdata.size(),\n\t\treinterpret_cast<const unsigned char*>(data.data()),\n\t\treinterpret_cast<unsigned char*>(result.data()),\n\t\t_rsa,\n\t\tRSA_PKCS1_OAEP_PADDING);\n\tif (encryptedSize != resultSize) {\n\t\tOPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, nullptr);\n\t\tLOG((\"RSA Error: RSA_public_encrypt failed, \"\n\t\t\t\"key fp: %1, result: %2, error: %3\"\n\t\t\t).arg(fingerprint()\n\t\t\t).arg(encryptedSize\n\t\t\t).arg(ERR_error_string(ERR_get_error(), 0)\n\t\t\t));\n\t\treturn {};\n\t}\n\treturn result;\n}\n\nRSAPublicKey::Private::~Private() {\n\tRSA_free(_rsa);\n}\n\nvoid RSAPublicKey::Private::computeFingerprint() {\n\tExpects(valid());\n\n\tconst BIGNUM *n, *e;\n\tmtpBuffer string;\n\tRSA_get0_key(_rsa, &n, &e, nullptr);\n\tMTP_bytes(ToBytes(n)).write(string);\n\tMTP_bytes(ToBytes(e)).write(string);\n\n\tbytes::array<20> sha1Buffer;\n\topenssl::Sha1To(sha1Buffer, bytes::make_span(string));\n\t_fingerprint = *(uint64*)(sha1Buffer.data() + 12);\n}\n\nbytes::vector RSAPublicKey::Private::ToBytes(const BIGNUM *number) {\n\tauto size = BN_num_bytes(number);\n\tauto result = bytes::vector(size, gsl::byte{});\n\tBN_bn2bin(number, reinterpret_cast<unsigned char*>(result.data()));\n\treturn result;\n}\n\nRSAPublicKey::RSAPublicKey(bytes::const_span key)\n: _private(std::make_shared<Private>(key)) {\n}\n\nRSAPublicKey::RSAPublicKey(\n\tbytes::const_span nBytes,\n\tbytes::const_span eBytes)\n: _private(std::make_shared<Private>(nBytes, eBytes)) {\n}\n\nbool RSAPublicKey::empty() const {\n\treturn !_private;\n}\n\nbool RSAPublicKey::valid() const {\n\treturn !empty() && _private->valid();\n}\n\nuint64 RSAPublicKey::fingerprint() const {\n\tExpects(valid());\n\n\treturn _private->fingerprint();\n}\n\nbytes::vector RSAPublicKey::getN() const {\n\tExpects(valid());\n\n\treturn _private->getN();\n}\n\nbytes::vector RSAPublicKey::getE() const {\n\tExpects(valid());\n\n\treturn _private->getE();\n}\n\nbytes::vector RSAPublicKey::encrypt(bytes::const_span data) const {\n\tExpects(valid());\n\n\treturn _private->encrypt(data);\n}\n\nbytes::vector RSAPublicKey::decrypt(bytes::const_span data) const {\n\tExpects(valid());\n\n\treturn _private->decrypt(data);\n}\n\nbytes::vector RSAPublicKey::encryptOAEPpadding(\n\t\tbytes::const_span data) const {\n\treturn _private->encryptOAEPpadding(data);\n}\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_rsa_public_key.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/bytes.h\"\n\nnamespace MTP::details {\n\n// this class holds an RSA public key and can encrypt fixed-size messages with it\nclass RSAPublicKey final {\npublic:\n\tRSAPublicKey() = default;\n\tRSAPublicKey(bytes::const_span nBytes, bytes::const_span eBytes);\n\tRSAPublicKey(RSAPublicKey &&other) = default;\n\tRSAPublicKey(const RSAPublicKey &other) = default;\n\tRSAPublicKey &operator=(RSAPublicKey &&other) = default;\n\tRSAPublicKey &operator=(const RSAPublicKey &other) = default;\n\n\t// key in \"-----BEGIN RSA PUBLIC KEY----- ...\" format\n\t// or in \"-----BEGIN PUBLIC KEY----- ...\" format\n\texplicit RSAPublicKey(bytes::const_span key);\n\n\t[[nodiscard]] bool empty() const;\n\t[[nodiscard]] bool valid() const;\n\t[[nodiscard]] uint64 fingerprint() const;\n\t[[nodiscard]] bytes::vector getN() const;\n\t[[nodiscard]] bytes::vector getE() const;\n\n\t// data has exactly 256 chars to be encrypted\n\t[[nodiscard]] bytes::vector encrypt(bytes::const_span data) const;\n\n\t// data has exactly 256 chars to be decrypted\n\t[[nodiscard]] bytes::vector decrypt(bytes::const_span data) const;\n\n\t// data has lequal than 215 chars to be decrypted\n\t[[nodiscard]] bytes::vector encryptOAEPpadding(bytes::const_span data) const;\n\nprivate:\n\tclass Private;\n\tstd::shared_ptr<Private> _private;\n\n};\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_serialized_request.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/details/mtproto_serialized_request.h\"\n\n#include \"base/random.h\"\n\nnamespace MTP::details {\nnamespace {\n\nuint32 CountPaddingPrimesCount(\n\t\tuint32 requestSize,\n\t\tbool forAuthKeyInner) {\n\tif (forAuthKeyInner) {\n\t\treturn ((8 + requestSize) & 0x03)\n\t\t\t? (4 - ((8 + requestSize) & 0x03))\n\t\t\t: 0;\n\t}\n\tauto result = ((8 + requestSize) & 0x03)\n\t\t? (4 - ((8 + requestSize) & 0x03))\n\t\t: 0;\n\n\t// At least 12 bytes of random padding.\n\tif (result < 3) {\n\t\tresult += 4;\n\t}\n\n\t// Some more random padding.\n\treturn result + ((base::RandomValue<uchar>() & 0x0F) << 2);\n}\n\n} // namespace\n\nSerializedRequest::SerializedRequest(const RequestConstructHider::Tag &tag)\n: _data(std::make_shared<RequestData>(tag)) {\n}\n\nSerializedRequest SerializedRequest::Prepare(\n\t\tuint32 size,\n\t\tuint32 reserveSize) {\n\tExpects(size > 0);\n\n\tconst auto finalSize = std::max(size, reserveSize);\n\n\tauto result = SerializedRequest(RequestConstructHider::Tag{});\n\tresult->reserve(kMessageBodyPosition + finalSize);\n\tresult->resize(kMessageBodyPosition);\n\tresult->back() = (size << 2);\n\tresult->lastSentTime = crl::now();\n\treturn result;\n}\n\nRequestData *SerializedRequest::operator->() const {\n\tExpects(_data != nullptr);\n\n\treturn _data.get();\n}\n\nRequestData &SerializedRequest::operator*() const {\n\tExpects(_data != nullptr);\n\n\treturn *_data;\n}\n\nSerializedRequest::operator bool() const {\n\treturn (_data != nullptr);\n}\n\nvoid SerializedRequest::setMsgId(mtpMsgId msgId) {\n\tExpects(_data != nullptr);\n\tExpects(_data->size() > kMessageBodyPosition);\n\n\tmemcpy(_data->data() + kMessageIdPosition, &msgId, sizeof(mtpMsgId));\n}\n\nmtpMsgId SerializedRequest::getMsgId() const {\n\tExpects(_data != nullptr);\n\tExpects(_data->size() > kMessageBodyPosition);\n\n\treturn *(mtpMsgId*)(_data->constData() + kMessageIdPosition);\n}\n\nvoid SerializedRequest::setSeqNo(uint32 seqNo) {\n\tExpects(_data != nullptr);\n\tExpects(_data->size() > kMessageBodyPosition);\n\n\t(*_data)[kSeqNoPosition] = mtpPrime(seqNo);\n}\n\nuint32 SerializedRequest::getSeqNo() const {\n\tExpects(_data != nullptr);\n\tExpects(_data->size() > kMessageBodyPosition);\n\n\treturn uint32((*_data)[kSeqNoPosition]);\n}\n\nvoid SerializedRequest::addPadding(bool forAuthKeyInner) {\n\tExpects(_data != nullptr);\n\tExpects(_data->size() > kMessageBodyPosition);\n\n\tconst auto requestSize = (tl::count_length(*this) >> 2);\n\tconst auto padding = CountPaddingPrimesCount(\n\t\trequestSize,\n\t\tforAuthKeyInner);\n\tconst auto fullSize = kMessageBodyPosition + requestSize + padding;\n\tif (uint32(_data->size()) != fullSize) {\n\t\t_data->resize(fullSize);\n\t\tif (padding > 0) {\n\t\t\tbytes::set_random(bytes::make_span(*_data).subspan(\n\t\t\t\t(fullSize - padding) * sizeof(mtpPrime)));\n\t\t}\n\t}\n}\n\nuint32 SerializedRequest::messageSize() const {\n\tExpects(_data != nullptr);\n\tExpects(_data->size() > kMessageBodyPosition);\n\n\tconst auto ints = (tl::count_length(*this) >> 2);\n\treturn kMessageIdInts + kSeqNoInts + kMessageLengthInts + ints;\n}\n\nbool SerializedRequest::needAck() const {\n\tExpects(_data != nullptr);\n\tExpects(_data->size() > kMessageBodyPosition);\n\n\tconst auto type = mtpTypeId((*_data)[kMessageBodyPosition]);\n\tswitch (type) {\n\tcase mtpc_msg_container:\n\tcase mtpc_msgs_ack:\n\tcase mtpc_http_wait:\n\tcase mtpc_bad_msg_notification:\n\tcase mtpc_msgs_all_info:\n\tcase mtpc_msgs_state_info:\n\tcase mtpc_msg_detailed_info:\n\tcase mtpc_msg_new_detailed_info:\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nsize_t SerializedRequest::sizeInBytes() const {\n\tExpects(!_data || _data->size() > kMessageBodyPosition);\n\treturn _data ? (*_data)[kMessageLengthPosition] : 0;\n}\n\nconst void *SerializedRequest::dataInBytes() const {\n\tExpects(!_data || _data->size() > kMessageBodyPosition);\n\treturn _data ? (_data->constData() + kMessageBodyPosition) : nullptr;\n}\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_serialized_request.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/core_types.h\"\n\n#include <crl/crl_time.h>\n\nnamespace MTP {\nnamespace details {\n\nclass RequestData;\nclass SerializedRequest;\n\nclass RequestConstructHider {\n\tstruct Tag {};\n\tfriend class RequestData;\n\tfriend class SerializedRequest;\n};\n\nclass SerializedRequest {\npublic:\n\tSerializedRequest() = default;\n\n\tstatic constexpr auto kSaltInts = 2;\n\tstatic constexpr auto kSessionIdInts = 2;\n\tstatic constexpr auto kMessageIdPosition = kSaltInts + kSessionIdInts;\n\tstatic constexpr auto kMessageIdInts = 2;\n\tstatic constexpr auto kSeqNoPosition = kMessageIdPosition\n\t\t+ kMessageIdInts;\n\tstatic constexpr auto kSeqNoInts = 1;\n\tstatic constexpr auto kMessageLengthPosition = kSeqNoPosition\n\t\t+ kSeqNoInts;\n\tstatic constexpr auto kMessageLengthInts = 1;\n\tstatic constexpr auto kMessageBodyPosition = kMessageLengthPosition\n\t\t+ kMessageLengthInts;\n\n\tstatic SerializedRequest Prepare(uint32 size, uint32 reserveSize = 0);\n\n\ttemplate <\n\t\ttypename Request,\n\t\ttypename = std::enable_if_t<tl::is_boxed_v<Request>>>\n\t\tstatic SerializedRequest Serialize(const Request &request);\n\n\t// For template MTP requests and MTPBoxed instantiation.\n\ttemplate <typename Accumulator>\n\tvoid write(Accumulator &to) const {\n\t\tif (const auto size = sizeInBytes()) {\n\t\t\ttl::Writer<Accumulator>::PutBytes(to, dataInBytes(), size);\n\t\t}\n\t}\n\n\tRequestData *operator->() const;\n\tRequestData &operator*() const;\n\texplicit operator bool() const;\n\n\tvoid setMsgId(mtpMsgId msgId);\n\t[[nodiscard]] mtpMsgId getMsgId() const;\n\n\tvoid setSeqNo(uint32 seqNo);\n\t[[nodiscard]] uint32 getSeqNo() const;\n\n\tvoid addPadding(bool forAuthKeyInner);\n\t[[nodiscard]] uint32 messageSize() const;\n\n\t[[nodiscard]] bool needAck() const;\n\n\tusing ResponseType = void; // don't know real response type =(\n\nprivate:\n\texplicit SerializedRequest(const RequestConstructHider::Tag &);\n\n\t[[nodiscard]] size_t sizeInBytes() const;\n\t[[nodiscard]] const void *dataInBytes() const;\n\n\tstd::shared_ptr<RequestData> _data;\n\n};\n\nclass RequestData : public mtpBuffer {\npublic:\n\texplicit RequestData(const RequestConstructHider::Tag &) {\n\t}\n\n\tSerializedRequest after;\n\tcrl::time lastSentTime = 0;\n\tmtpRequestId requestId = 0;\n\tbool needsLayer = false;\n\tbool forceSendInContainer = false;\n\n};\n\ntemplate <typename Request, typename>\nSerializedRequest SerializedRequest::Serialize(const Request &request) {\n\tconst auto requestSize = tl::count_length(request) >> 2;\n\tauto serialized = Prepare(requestSize);\n\trequest.template write<mtpBuffer>(*serialized);\n\treturn serialized;\n}\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_tcp_socket.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/details/mtproto_tcp_socket.h\"\n\n#include \"base/invoke_queued.h\"\n\nnamespace MTP::details {\n\nTcpSocket::TcpSocket(\n\tnot_null<QThread*> thread,\n\tconst QNetworkProxy &proxy,\n\tbool protocolForFiles)\n: AbstractSocket(thread) {\n\t_socket.moveToThread(thread);\n\t_socket.setProxy(proxy);\n\tif (protocolForFiles) {\n\t\t_socket.setSocketOption(\n\t\t\tQAbstractSocket::SendBufferSizeSocketOption,\n\t\t\tkFilesSendBufferSize);\n\t\t_socket.setSocketOption(\n\t\t\tQAbstractSocket::ReceiveBufferSizeSocketOption,\n\t\t\tkFilesReceiveBufferSize);\n\t}\n\tconst auto wrap = [&](auto handler) {\n\t\treturn [=](auto &&...args) {\n\t\t\tInvokeQueued(this, [=] { handler(args...); });\n\t\t};\n\t};\n\tusing Error = QAbstractSocket::SocketError;\n\tconnect(\n\t\t&_socket,\n\t\t&QTcpSocket::connected,\n\t\twrap([=] { _connected.fire({}); }));\n\tconnect(\n\t\t&_socket,\n\t\t&QTcpSocket::disconnected,\n\t\twrap([=] { _disconnected.fire({}); }));\n\tconnect(\n\t\t&_socket,\n\t\t&QTcpSocket::readyRead,\n\t\twrap([=] { _readyRead.fire({}); }));\n\tconnect(\n\t\t&_socket,\n\t\t&QAbstractSocket::errorOccurred,\n\t\twrap([=](Error e) { handleError(e); }));\n}\n\nvoid TcpSocket::connectToHost(const QString &address, int port) {\n\t_socket.connectToHost(address, port);\n}\n\nbool TcpSocket::isGoodStartNonce(bytes::const_span nonce) {\n\tExpects(nonce.size() >= 2 * sizeof(uint32));\n\n\tconst auto bytes = nonce.data();\n\tconst auto zero = *reinterpret_cast<const uchar*>(bytes);\n\tconst auto first = *reinterpret_cast<const uint32*>(bytes);\n\tconst auto second = *(reinterpret_cast<const uint32*>(bytes) + 1);\n\tconst auto reserved01 = 0x000000EFU;\n\tconst auto reserved11 = 0x44414548U;\n\tconst auto reserved12 = 0x54534F50U;\n\tconst auto reserved13 = 0x20544547U;\n\tconst auto reserved14 = 0xEEEEEEEEU;\n\tconst auto reserved15 = 0xDDDDDDDDU;\n\tconst auto reserved16 = 0x02010316U;\n\tconst auto reserved21 = 0x00000000U;\n\treturn (zero != reserved01)\n\t\t&& (first != reserved11)\n\t\t&& (first != reserved12)\n\t\t&& (first != reserved13)\n\t\t&& (first != reserved14)\n\t\t&& (first != reserved15)\n\t\t&& (first != reserved16)\n\t\t&& (second != reserved21);\n}\n\nvoid TcpSocket::timedOut() {\n}\n\nbool TcpSocket::isConnected() {\n\treturn (_socket.state() == QAbstractSocket::ConnectedState);\n}\n\nbool TcpSocket::hasBytesAvailable() {\n\treturn _socket.bytesAvailable() > 0;\n}\n\nint64 TcpSocket::read(bytes::span buffer) {\n\treturn _socket.read(\n\t\treinterpret_cast<char*>(buffer.data()),\n\t\tbuffer.size());\n}\n\nvoid TcpSocket::write(bytes::const_span prefix, bytes::const_span buffer) {\n\tExpects(!buffer.empty());\n\n\tif (!prefix.empty()) {\n\t\t_socket.write(\n\t\t\treinterpret_cast<const char*>(prefix.data()),\n\t\t\tprefix.size());\n\t}\n\t_socket.write(\n\t\treinterpret_cast<const char*>(buffer.data()),\n\t\tbuffer.size());\n}\n\nint32 TcpSocket::debugState() {\n\treturn _socket.state();\n}\n\nQString TcpSocket::debugPostfix() const {\n\treturn QString();\n}\n\nvoid TcpSocket::handleError(int errorCode) {\n\tlogError(errorCode, _socket.errorString());\n\t_error.fire({});\n}\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_tcp_socket.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/details/mtproto_abstract_socket.h\"\n\nnamespace MTP::details {\n\nclass TcpSocket final : public AbstractSocket {\npublic:\n\tTcpSocket(\n\t\tnot_null<QThread*> thread,\n\t\tconst QNetworkProxy &proxy,\n\t\tbool protocolForFiles);\n\n\tvoid connectToHost(const QString &address, int port) override;\n\tbool isGoodStartNonce(bytes::const_span nonce) override;\n\tvoid timedOut() override;\n\tbool isConnected() override;\n\tbool hasBytesAvailable() override;\n\tint64 read(bytes::span buffer) override;\n\tvoid write(bytes::const_span prefix, bytes::const_span buffer) override;\n\n\tint32 debugState() override;\n\tQString debugPostfix() const override;\n\nprivate:\n\tvoid handleError(int errorCode);\n\n\tQTcpSocket _socket;\n\n};\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_tls_socket.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/details/mtproto_tls_socket.h\"\n\n#include \"mtproto/details/mtproto_tcp_socket.h\"\n#include \"base/openssl_help.h\"\n#include \"base/bytes.h\"\n#include \"base/invoke_queued.h\"\n#include \"base/random.h\"\n#include \"base/unixtime.h\"\n\n#include <QtCore/QtEndian>\n#include <range/v3/algorithm/reverse.hpp>\n\nnamespace MTP::details {\nnamespace {\n\nconstexpr auto kMaxGrease = 8;\nconstexpr auto kClientHelloLimit = 2048;\nconstexpr auto kHelloDigestLength = 32;\nconstexpr auto kLengthSize = sizeof(uint16);\nconst auto kServerHelloPart1 = qstr(\"\\x16\\x03\\x03\");\nconst auto kServerHelloPart3 = qstr(\"\\x14\\x03\\x03\\x00\\x01\\x01\\x17\\x03\\x03\");\nconstexpr auto kServerHelloDigestPosition = 11;\nconst auto kServerHeader = qstr(\"\\x17\\x03\\x03\");\nconstexpr auto kClientPartSize = 2878;\nconst auto kClientPrefix = qstr(\"\\x14\\x03\\x03\\x00\\x01\\x01\");\nconst auto kClientHeader = qstr(\"\\x17\\x03\\x03\");\n\nusing BigNum = openssl::BigNum;\nusing BigNumContext = openssl::Context;\n\n[[nodiscard]] MTPTlsClientHello PrepareClientHelloRules() {\n\tusing Scope = QVector<MTPTlsBlock>;\n\tusing Permutation = std::vector<Scope>;\n\tusing StackElement = std::variant<Scope, Permutation>;\n\tauto stack = std::vector<StackElement>();\n\tconst auto pushToBack = [&](MTPTlsBlock &&block) {\n\t\tExpects(!stack.empty());\n\n\t\tif (const auto scope = std::get_if<Scope>(&stack.back())) {\n\t\t\tscope->push_back(std::move(block));\n\t\t} else {\n\t\t\tauto &permutation = v::get<Permutation>(stack.back());\n\t\t\tAssert(!permutation.empty());\n\t\t\tpermutation.back().push_back(std::move(block));\n\t\t}\n\t};\n\tconst auto S = [&](QByteArray data) {\n\t\tpushToBack(MTP_tlsBlockString(MTP_bytes(data)));\n\t};\n\tconst auto Z = [&](int length) {\n\t\tpushToBack(MTP_tlsBlockZero(MTP_int(length)));\n\t};\n\tconst auto G = [&](int seed) {\n\t\tpushToBack(MTP_tlsBlockGrease(MTP_int(seed)));\n\t};\n\tconst auto R = [&](int length) {\n\t\tpushToBack(MTP_tlsBlockRandom(MTP_int(length)));\n\t};\n\tconst auto D = [&] {\n\t\tpushToBack(MTP_tlsBlockDomain());\n\t};\n\tconst auto K = [&] {\n\t\tpushToBack(MTP_tlsBlockPublicKey());\n\t};\n\tconst auto M = [&] {\n\t\tpushToBack(MTP_tlsBlockM());\n\t};\n\tconst auto E = [&] {\n\t\tpushToBack(MTP_tlsBlockE());\n\t};\n\tconst auto P = [&] {\n\t\tpushToBack(MTP_tlsBlockPadding());\n\t};\n\tconst auto OpenScope = [&] {\n\t\tstack.emplace_back(Scope());\n\t};\n\tconst auto CloseScope = [&] {\n\t\tExpects(stack.size() > 1);\n\t\tExpects(v::is<Scope>(stack.back()));\n\n\t\tconst auto blocks = std::move(v::get<Scope>(stack.back()));\n\t\tstack.pop_back();\n\t\tpushToBack(MTP_tlsBlockScope(MTP_vector<MTPTlsBlock>(blocks)));\n\t};\n\tconst auto OpenPermutation = [&] {\n\t\tstack.emplace_back(Permutation());\n\t};\n\tconst auto ClosePermutation = [&] {\n\t\tExpects(stack.size() > 1);\n\t\tExpects(v::is<Permutation>(stack.back()));\n\n\t\tconst auto list = std::move(v::get<Permutation>(stack.back()));\n\t\tstack.pop_back();\n\n\t\tconst auto wrapped = list | ranges::views::transform([](\n\t\t\t\tconst QVector<MTPTlsBlock> &elements) {\n\t\t\treturn MTP_vector<MTPTlsBlock>(elements);\n\t\t}) | ranges::to<QVector<MTPVector<MTPTlsBlock>>>();\n\n\t\tpushToBack(MTP_tlsBlockPermutation(\n\t\t\tMTP_vector<MTPVector<MTPTlsBlock>>(wrapped)));\n\t};\n\tconst auto StartPermutationElement = [&] {\n\t\tExpects(stack.size() > 1);\n\t\tExpects(v::is<Permutation>(stack.back()));\n\n\t\tv::get<Permutation>(stack.back()).emplace_back();\n\t};\n\tconst auto Finish = [&] {\n\t\tExpects(stack.size() == 1);\n\t\tExpects(v::is<Scope>(stack.back()));\n\n\t\treturn v::get<Scope>(stack.back());\n\t};\n\n\tstack.emplace_back(Scope());\n\n\tS(\"\\x16\\x03\\x01\"_q);\n\tOpenScope();\n\tS(\"\\x01\\x00\"_q);\n\tOpenScope();\n\tS(\"\\x03\\x03\"_q);\n\tZ(32);\n\tS(\"\\x20\"_q);\n\tR(32);\n\tS(\"\\x00\\x20\"_q);\n\tG(0);\n\tS(\"\"\n\t\t\"\\x13\\x01\\x13\\x02\\x13\\x03\\xc0\\x2b\\xc0\\x2f\\xc0\\x2c\\xc0\\x30\\xcc\\xa9\"\n\t\t\"\\xcc\\xa8\\xc0\\x13\\xc0\\x14\\x00\\x9c\\x00\\x9d\\x00\\x2f\\x00\\x35\\x01\\x00\"\n\t\t\"\"_q);\n\tOpenScope();\n\tG(2);\n\tS(\"\\x00\\x00\"_q);\n\tOpenPermutation(); {\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\\x00\\x00\"_q);\n\t\t\tOpenScope();\n\t\t\tOpenScope();\n\t\t\tS(\"\\x00\"_q);\n\t\t\tOpenScope();\n\t\t\tD();\n\t\t\tCloseScope();\n\t\t\tCloseScope();\n\t\t\tCloseScope();\n\t\t}\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\\x00\\x05\\x00\\x05\\x01\\x00\\x00\\x00\\x00\"_q);\n\t\t}\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\\x00\\x0a\\x00\\x0c\\x00\\x0a\"_q);\n\t\t\tG(4);\n\t\t\tS(\"\\x11\\xec\\x00\\x1d\\x00\\x17\\x00\\x18\"_q);\n\t\t}\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\\x00\\x0b\\x00\\x02\\x01\\x00\"_q);\n\t\t}\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\"\n\t\t\t\t\"\\x00\\x0d\\x00\\x12\\x00\\x10\\x04\\x03\\x08\\x04\\x04\\x01\\x05\\x03\"\n\t\t\t\t\"\\x08\\x05\\x05\\x01\\x08\\x06\\x06\\x01\"_q);\n\t\t}\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\"\n\t\t\t\t\"\\x00\\x10\\x00\\x0e\\x00\\x0c\\x02\\x68\\x32\\x08\\x68\\x74\\x74\\x70\"\n\t\t\t\t\"\\x2f\\x31\\x2e\\x31\"_q);\n\t\t}\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\\x00\\x12\\x00\\x00\"_q);\n\t\t}\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\\x00\\x17\\x00\\x00\"_q);\n\t\t}\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\\x00\\x1b\\x00\\x03\\x02\\x00\\x02\"_q);\n\t\t}\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\\x00\\x23\\x00\\x00\"_q);\n\t\t}\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\\x00\\x2b\\x00\\x07\\x06\"_q);\n\t\t\tG(6);\n\t\t\tS(\"\\x03\\x04\\x03\\x03\"_q);\n\t\t}\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\\x00\\x2d\\x00\\x02\\x01\\x01\"_q);\n\t\t}\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\\x00\\x33\\x04\\xef\\x04\\xed\"_q);\n\t\t\tG(4);\n\t\t\tS(\"\\x00\\x01\\x00\\x11\\xec\\x04\\xc0\"_q);\n\t\t\tM();\n\t\t\tK();\n\t\t\tS(\"\\x00\\x1d\\x00\\x20\"_q);\n\t\t\tK();\n\t\t}\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\\x44\\xcd\\x00\\x05\\x00\\x03\\x02\\x68\\x32\"_q);\n\t\t}\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\\xfe\\x0d\"_q);\n\t\t\tOpenScope();\n\t\t\tS(\"\\x00\\x00\\x01\\x00\\x01\"_q);\n\t\t\tR(1);\n\t\t\tS(\"\\x00\\x20\"_q);\n\t\t\tR(32);\n\t\t\tOpenScope();\n\t\t\tE();\n\t\t\tCloseScope();\n\t\t\tCloseScope();\n\t\t}\n\t\tStartPermutationElement(); {\n\t\t\tS(\"\\xff\\x01\\x00\\x01\\x00\"_q);\n\t\t}\n\t} ClosePermutation();\n\tG(3);\n\tS(\"\\x00\\x01\\x00\"_q);\n\tP();\n\tCloseScope();\n\tCloseScope();\n\tCloseScope();\n\n\treturn MTP_tlsClientHello(MTP_vector<MTPTlsBlock>(Finish()));\n}\n\n[[nodiscard]] bytes::vector PrepareGreases() {\n\tauto result = bytes::vector(kMaxGrease);\n\tbytes::set_random(result);\n\tfor (auto &byte : result) {\n\t\tbyte = bytes::type((uchar(byte) & 0xF0) + 0x0A);\n\t}\n\tstatic_assert(kMaxGrease % 2 == 0);\n\tfor (auto i = 0; i != kMaxGrease; i += 2) {\n\t\tif (result[i] == result[i + 1]) {\n\t\t\tresult[i + 1] = bytes::type(uchar(result[i + 1]) ^ 0x10);\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] bytes::vector GeneratePublicKey() {\n\tconst auto context = EVP_PKEY_CTX_new_id(NID_ED25519, nullptr);\n\tif (!context) {\n\t\treturn {};\n\t}\n\tconst auto guardContext = gsl::finally([&] {\n\t\tEVP_PKEY_CTX_free(context);\n\t});\n\n\tif (EVP_PKEY_keygen_init(context) <= 0) {\n\t\treturn {};\n\t}\n\n\tauto key = (EVP_PKEY*)nullptr;\n\tif (EVP_PKEY_keygen(context, &key) <= 0) {\n\t\treturn {};\n\t}\n\tconst auto guardKey = gsl::finally([&] {\n\t\tEVP_PKEY_free(key);\n\t});\n\n\tauto length = size_t(0);\n\tif (!EVP_PKEY_get_raw_public_key(key, nullptr, &length)) {\n\t\treturn {};\n\t}\n\tAssert(length == 32);\n\n\tauto result = bytes::vector(length);\n\tconst auto code = EVP_PKEY_get_raw_public_key(\n\t\tkey,\n\t\treinterpret_cast<unsigned char *>(result.data()),\n\t\t&length);\n\tif (!code) {\n\t\treturn {};\n\t}\n\treturn result;\n}\n\nstruct ClientHello {\n\tQByteArray data;\n\tQByteArray digest;\n};\n\nclass Generator {\npublic:\n\tGenerator(\n\t\tconst MTPTlsClientHello &rules,\n\t\tbytes::const_span domain,\n\t\tbytes::const_span key);\n\t[[nodiscard]] ClientHello take();\n\nprivate:\n\tclass Part final {\n\tpublic:\n\t\texplicit Part(\n\t\t\tbytes::const_span domain,\n\t\t\tconst bytes::vector &greases);\n\n\t\t[[nodiscard]] bytes::span grow(int size);\n\t\tvoid writeBlocks(const QVector<MTPTlsBlock> &blocks);\n\t\tvoid writeBlock(const MTPTlsBlock &data);\n\t\tvoid writeBlock(const MTPDtlsBlockString &data);\n\t\tvoid writeBlock(const MTPDtlsBlockZero &data);\n\t\tvoid writeBlock(const MTPDtlsBlockGrease &data);\n\t\tvoid writeBlock(const MTPDtlsBlockRandom &data);\n\t\tvoid writeBlock(const MTPDtlsBlockDomain &data);\n\t\tvoid writeBlock(const MTPDtlsBlockPublicKey &data);\n\t\tvoid writeBlock(const MTPDtlsBlockScope &data);\n\t\tvoid writeBlock(const MTPDtlsBlockPermutation &data);\n\t\tvoid writeBlock(const MTPDtlsBlockM &data);\n\t\tvoid writeBlock(const MTPDtlsBlockE &data);\n\t\tvoid writeBlock(const MTPDtlsBlockPadding &data);\n\t\tvoid finalize(bytes::const_span key);\n\t\t[[nodiscard]] QByteArray extractDigest() const;\n\n\t\t[[nodiscard]] bool error() const;\n\t\t[[nodiscard]] QByteArray take();\n\n\tprivate:\n\t\tvoid writeDigest(bytes::const_span key);\n\t\tvoid injectTimestamp();\n\n\t\tbytes::const_span _domain;\n\t\tconst bytes::vector &_greases;\n\t\tQByteArray _result;\n\t\tconst char *_data = nullptr;\n\t\tint _digestPosition = -1;\n\t\tbool _error = false;\n\n\t};\n\n\tbytes::vector _greases;\n\tPart _result;\n\tQByteArray _digest;\n\n};\n\nGenerator::Part::Part(\n\tbytes::const_span domain,\n\tconst bytes::vector &greases)\n: _domain(domain)\n, _greases(greases) {\n\t_result.reserve(kClientHelloLimit);\n\t_data = _result.constData();\n}\n\nbool Generator::Part::error() const {\n\treturn _error;\n}\n\nQByteArray Generator::Part::take() {\n\tExpects(_error || _result.constData() == _data);\n\n\treturn _error ? QByteArray() : std::move(_result);\n}\n\nbytes::span Generator::Part::grow(int size) {\n\tif (_error\n\t\t|| size <= 0\n\t\t|| _result.size() + size > kClientHelloLimit) {\n\t\t_error = true;\n\t\treturn bytes::span();\n\t}\n\n\tconst auto offset = _result.size();\n\t_result.resize(offset + size);\n\treturn bytes::make_detached_span(_result).subspan(offset);\n}\n\nvoid Generator::Part::writeBlocks(const QVector<MTPTlsBlock> &blocks) {\n\tfor (const auto &block : blocks) {\n\t\twriteBlock(block);\n\t}\n}\n\nvoid Generator::Part::writeBlock(const MTPTlsBlock &data) {\n\tdata.match([&](const auto &data) {\n\t\twriteBlock(data);\n\t});\n}\n\nvoid Generator::Part::writeBlock(const MTPDtlsBlockString &data) {\n\tconst auto &bytes = data.vdata().v;\n\tconst auto storage = grow(bytes.size());\n\tif (storage.empty()) {\n\t\treturn;\n\t}\n\tbytes::copy(storage, bytes::make_span(bytes));\n}\n\nvoid Generator::Part::writeBlock(const MTPDtlsBlockZero &data) {\n\tconst auto length = data.vlength().v;\n\tconst auto already = _result.size();\n\tconst auto storage = grow(length);\n\tif (storage.empty()) {\n\t\treturn;\n\t}\n\tif (length == kHelloDigestLength && _digestPosition < 0) {\n\t\t_digestPosition = already;\n\t}\n\tbytes::set_with_const(storage, bytes::type(0));\n}\n\nvoid Generator::Part::writeBlock(const MTPDtlsBlockGrease &data) {\n\tconst auto seed = data.vseed().v;\n\tif (seed < 0 || seed >= _greases.size()) {\n\t\t_error = true;\n\t\treturn;\n\t}\n\tconst auto storage = grow(2);\n\tif (storage.empty()) {\n\t\treturn;\n\t}\n\tbytes::set_with_const(storage, _greases[seed]);\n}\n\nvoid Generator::Part::writeBlock(const MTPDtlsBlockRandom &data) {\n\tconst auto length = data.vlength().v;\n\tconst auto storage = grow(length);\n\tif (storage.empty()) {\n\t\treturn;\n\t}\n\tbytes::set_random(storage);\n}\n\nvoid Generator::Part::writeBlock(const MTPDtlsBlockDomain &data) {\n\tconst auto storage = grow(_domain.size());\n\tif (storage.empty()) {\n\t\treturn;\n\t}\n\tbytes::copy(storage, _domain);\n}\n\nvoid Generator::Part::writeBlock(const MTPDtlsBlockPublicKey &data) {\n\tconst auto key = GeneratePublicKey();\n\tconst auto storage = grow(key.size());\n\tif (storage.empty()) {\n\t\treturn;\n\t}\n\tbytes::copy(storage, key);\n}\n\nvoid Generator::Part::writeBlock(const MTPDtlsBlockScope &data) {\n\tconst auto storage = grow(kLengthSize);\n\tif (storage.empty()) {\n\t\treturn;\n\t}\n\tconst auto already = _result.size();\n\twriteBlocks(data.ventries().v);\n\tconst auto length = qToBigEndian(uint16(_result.size() - already));\n\tbytes::copy(storage, bytes::object_as_span(&length));\n}\n\nvoid Generator::Part::writeBlock(const MTPDtlsBlockPermutation &data) {\n\tauto list = std::vector<QByteArray>();\n\tlist.reserve(data.ventries().v.size());\n\tfor (const auto &inner : data.ventries().v) {\n\t\tauto part = Part(_domain, _greases);\n\t\tpart.writeBlocks(inner.v);\n\t\tif (part.error()) {\n\t\t\t_error = true;\n\t\t\treturn;\n\t\t}\n\t\tlist.push_back(part.take());\n\t}\n\tranges::shuffle(list);\n\tfor (const auto &element : list) {\n\t\tconst auto storage = grow(element.size());\n\t\tif (storage.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tbytes::copy(storage, bytes::make_span(element));\n\t}\n}\n\nvoid Generator::Part::writeBlock(const MTPDtlsBlockM &data) {\n\tconstexpr auto kElements = 384;\n\tconstexpr auto kAdded = 32;\n\n\tconst auto storage = grow(kElements * 3 + kAdded);\n\tif (storage.empty()) {\n\t\treturn;\n\t}\n\n\tauto random = bytes::vector(kElements * 8 + kAdded);\n\tbytes::set_random(random);\n\n\tauto chars = reinterpret_cast<char*>(storage.data());\n\tconst auto ints = reinterpret_cast<const uint32*>(random.data());\n\tfor (auto i = 0; i < kElements; ++i) {\n\t\tconst auto a = int(ints[i * 2] % 3329);\n\t\tconst auto b = int(ints[i * 2 + 1] % 3329);\n\t\t*chars++ = (char)(a & 255);\n\t\t*chars++ = (char)((a >> 8) + ((b & 15) << 4));\n\t\t*chars++ = (char)(b >> 4);\n\t}\n\tbytes::set_random(storage.subspan(kElements * 3));\n}\n\nvoid Generator::Part::writeBlock(const MTPDtlsBlockE &data) {\n\tconst auto lengths = std::array{ 144, 176, 208, 240 };\n\tconst auto length = lengths[base::RandomIndex(lengths.size())];\n\twriteBlock(MTP_tlsBlockRandom(MTP_int(length)));\n}\n\nvoid Generator::Part::writeBlock(const MTPDtlsBlockPadding &data) {\n\tconst auto length = int(_result.size());\n\tif (length < 513) {\n\t\tconst auto zero = MTP_tlsBlockZero(MTP_int(513 - length));\n\t\twriteBlock(MTP_tlsBlockString(MTP_bytes(\"\\x00\\x15\"_q)));\n\t\twriteBlock(MTP_tlsBlockScope(MTP_vector<MTPTlsBlock>(1, zero)));\n\t}\n}\n\nvoid Generator::Part::finalize(bytes::const_span key) {\n\tif (_error) {\n\t\treturn;\n\t} else if (_digestPosition < 0) {\n\t\t_error = true;\n\t\treturn;\n\t}\n\twriteDigest(key);\n\tinjectTimestamp();\n}\n\nQByteArray Generator::Part::extractDigest() const {\n\tif (_digestPosition < 0) {\n\t\treturn {};\n\t}\n\treturn _result.mid(_digestPosition, kHelloDigestLength);\n}\n\nvoid Generator::Part::writeDigest(bytes::const_span key) {\n\tExpects(_digestPosition >= 0);\n\n\tbytes::copy(\n\t\tbytes::make_detached_span(_result).subspan(_digestPosition),\n\t\topenssl::HmacSha256(key, bytes::make_span(_result)));\n}\n\nvoid Generator::Part::injectTimestamp() {\n\tExpects(_digestPosition >= 0);\n\n\tconst auto storage = bytes::make_detached_span(_result).subspan(\n\t\t_digestPosition + kHelloDigestLength - sizeof(int32),\n\t\tsizeof(int32));\n\tauto already = int32();\n\tbytes::copy(bytes::object_as_span(&already), storage);\n\talready ^= qToLittleEndian(int32(base::unixtime::http_now()));\n\tbytes::copy(storage, bytes::object_as_span(&already));\n}\n\nGenerator::Generator(\n\tconst MTPTlsClientHello &rules,\n\tbytes::const_span domain,\n\tbytes::const_span key)\n: _greases(PrepareGreases())\n, _result(domain, _greases) {\n\t_result.writeBlocks(rules.data().vblocks().v);\n\t_result.finalize(key);\n}\n\nClientHello Generator::take() {\n\tauto digest = _result.extractDigest();\n\treturn { _result.take(), std::move(digest) };\n}\n\n[[nodiscard]] ClientHello PrepareClientHello(\n\t\tconst MTPTlsClientHello &rules,\n\t\tbytes::const_span domain,\n\t\tbytes::const_span key) {\n\treturn Generator(rules, domain, key).take();\n}\n\n[[nodiscard]] bool CheckPart(bytes::const_span data, QLatin1String check) {\n\tif (data.size() < check.size()) {\n\t\treturn false;\n\t}\n\treturn !bytes::compare(\n\t\tdata.subspan(0, check.size()),\n\t\tbytes::make_span(check.data(), check.size()));\n}\n\n[[nodiscard]] int ReadPartLength(bytes::const_span data, int offset) {\n\tconst auto storage = data.subspan(offset, kLengthSize);\n\treturn qFromBigEndian(\n\t\t*reinterpret_cast<const uint16*>(storage.data()));\n}\n\n} // namespace\n\nTlsSocket::TlsSocket(\n\tnot_null<QThread*> thread,\n\tconst bytes::vector &secret,\n\tconst QNetworkProxy &proxy,\n\tbool protocolForFiles)\n: AbstractSocket(thread)\n, _secret(secret) {\n\tExpects(_secret.size() >= 21 && _secret[0] == bytes::type(0xEE));\n\n\t_socket.moveToThread(thread);\n\t_socket.setProxy(proxy);\n\tif (protocolForFiles) {\n\t\t_socket.setSocketOption(\n\t\t\tQAbstractSocket::SendBufferSizeSocketOption,\n\t\t\tkFilesSendBufferSize);\n\t\t_socket.setSocketOption(\n\t\t\tQAbstractSocket::ReceiveBufferSizeSocketOption,\n\t\t\tkFilesReceiveBufferSize);\n\t}\n\tconst auto wrap = [&](auto handler) {\n\t\treturn [=](auto &&...args) {\n\t\t\tInvokeQueued(this, [=] { handler(args...); });\n\t\t};\n\t};\n\tusing Error = QAbstractSocket::SocketError;\n\tconnect(\n\t\t&_socket,\n\t\t&QTcpSocket::connected,\n\t\twrap([=] { plainConnected(); }));\n\tconnect(\n\t\t&_socket,\n\t\t&QTcpSocket::disconnected,\n\t\twrap([=] { plainDisconnected(); }));\n\tconnect(\n\t\t&_socket,\n\t\t&QTcpSocket::readyRead,\n\t\twrap([=] { plainReadyRead(); }));\n\tconnect(\n\t\t&_socket,\n\t\t&QAbstractSocket::errorOccurred,\n\t\twrap([=](Error e) { handleError(e); }));\n}\n\nbytes::const_span TlsSocket::domainFromSecret() const {\n\treturn bytes::make_span(_secret).subspan(17);\n}\n\nbytes::const_span TlsSocket::keyFromSecret() const {\n\treturn bytes::make_span(_secret).subspan(1, 16);\n}\n\nvoid TlsSocket::plainConnected() {\n\tif (_state != State::Connecting) {\n\t\treturn;\n\t}\n\n\tstatic const auto kClientHelloRules = PrepareClientHelloRules();\n\tconst auto hello = PrepareClientHello(\n\t\tkClientHelloRules,\n\t\tdomainFromSecret(),\n\t\tkeyFromSecret());\n\tif (hello.data.isEmpty()) {\n\t\tlogError(888, \"Could not generate Client Hello.\");\n\t\t_state = State::Error;\n\t\t_error.fire({});\n\t} else {\n\t\t_state = State::WaitingHello;\n\t\t_incoming = hello.digest;\n\t\t_socket.write(hello.data);\n\t}\n}\n\nvoid TlsSocket::plainDisconnected() {\n\t_state = State::NotConnected;\n\t_incoming = QByteArray();\n\t_serverHelloLength = 0;\n\t_incomingGoodDataOffset = 0;\n\t_incomingGoodDataLimit = 0;\n\t_disconnected.fire({});\n}\n\nvoid TlsSocket::plainReadyRead() {\n\tswitch (_state) {\n\tcase State::WaitingHello: return readHello();\n\tcase State::Connected: return readData();\n\t}\n}\n\nbool TlsSocket::requiredHelloPartReady() const {\n\treturn _incoming.size() >= kHelloDigestLength + _serverHelloLength;\n}\n\nvoid TlsSocket::readHello() {\n\tconst auto parts1Size = kServerHelloPart1.size() + kLengthSize;\n\tif (!_serverHelloLength) {\n\t\t_serverHelloLength = parts1Size;\n\t}\n\twhile (!requiredHelloPartReady()) {\n\t\tif (!_socket.bytesAvailable()) {\n\t\t\treturn;\n\t\t}\n\t\t_incoming.append(_socket.readAll());\n\t}\n\tcheckHelloParts12(parts1Size);\n}\n\nvoid TlsSocket::checkHelloParts12(int parts1Size) {\n\tconst auto data = bytes::make_span(_incoming).subspan(\n\t\tkHelloDigestLength,\n\t\tparts1Size);\n\tconst auto part2Size = ReadPartLength(data, parts1Size - kLengthSize);\n\tconst auto parts123Size = parts1Size\n\t\t+ part2Size\n\t\t+ kServerHelloPart3.size()\n\t\t+ kLengthSize;\n\tif (_serverHelloLength == parts1Size) {\n\t\tconst auto part1Offset = parts1Size\n\t\t\t- kLengthSize\n\t\t\t- kServerHelloPart1.size();\n\t\tif (!CheckPart(data.subspan(part1Offset), kServerHelloPart1)) {\n\t\t\tlogError(888, \"Bad Server Hello part1.\");\n\t\t\thandleError();\n\t\t\treturn;\n\t\t}\n\t\t_serverHelloLength = parts123Size;\n\t\tif (!requiredHelloPartReady()) {\n\t\t\treadHello();\n\t\t\treturn;\n\t\t}\n\t}\n\tcheckHelloParts34(parts123Size);\n}\n\nvoid TlsSocket::checkHelloParts34(int parts123Size) {\n\tconst auto data = bytes::make_span(_incoming).subspan(\n\t\tkHelloDigestLength,\n\t\tparts123Size);\n\tconst auto part4Size = ReadPartLength(data, parts123Size - kLengthSize);\n\tconst auto full = parts123Size + part4Size;\n\tif (_serverHelloLength == parts123Size) {\n\t\tconst auto part3Offset = parts123Size\n\t\t\t- kLengthSize\n\t\t\t- kServerHelloPart3.size();\n\t\tif (!CheckPart(data.subspan(part3Offset), kServerHelloPart3)) {\n\t\t\tlogError(888, \"Bad Server Hello part.\");\n\t\t\thandleError();\n\t\t\treturn;\n\t\t}\n\t\t_serverHelloLength = full;\n\t\tif (!requiredHelloPartReady()) {\n\t\t\treadHello();\n\t\t\treturn;\n\t\t}\n\t}\n\tcheckHelloDigest();\n}\n\nvoid TlsSocket::checkHelloDigest() {\n\tconst auto fulldata = bytes::make_detached_span(_incoming).subspan(\n\t\t0,\n\t\tkHelloDigestLength + _serverHelloLength);\n\tconst auto digest = fulldata.subspan(\n\t\tkHelloDigestLength + kServerHelloDigestPosition,\n\t\tkHelloDigestLength);\n\tconst auto digestCopy = bytes::make_vector(digest);\n\tbytes::set_with_const(digest, bytes::type(0));\n\tconst auto check = openssl::HmacSha256(keyFromSecret(), fulldata);\n\tif (bytes::compare(digestCopy, check) != 0) {\n\t\tlogError(888, \"Bad Server Hello digest.\");\n\t\thandleError();\n\t\treturn;\n\t}\n\tshiftIncomingBy(fulldata.size());\n\tif (!_incoming.isEmpty()) {\n\t\tInvokeQueued(this, [=] {\n\t\t\tif (!checkNextPacket()) {\n\t\t\t\thandleError();\n\t\t\t}\n\t\t});\n\t}\n\t_incomingGoodDataOffset = _incomingGoodDataLimit = 0;\n\t_state = State::Connected;\n\t_connected.fire({});\n}\n\nvoid TlsSocket::readData() {\n\tif (!isConnected()) {\n\t\treturn;\n\t}\n\t_incoming.append(_socket.readAll());\n\tif (!checkNextPacket()) {\n\t\thandleError();\n\t} else if (hasBytesAvailable()) {\n\t\t_readyRead.fire({});\n\t}\n}\n\nbool TlsSocket::checkNextPacket() {\n\tauto offset = 0;\n\tconst auto incoming = bytes::make_span(_incoming);\n\twhile (!_incomingGoodDataLimit) {\n\t\tconst auto fullHeader = kServerHeader.size() + kLengthSize;\n\t\tif (incoming.size() <= offset + fullHeader) {\n\t\t\treturn true;\n\t\t}\n\t\tif (!CheckPart(incoming.subspan(offset), kServerHeader)) {\n\t\t\tlogError(888, \"Bad packet header.\");\n\t\t\treturn false;\n\t\t}\n\t\tconst auto length = ReadPartLength(\n\t\t\tincoming,\n\t\t\toffset + kServerHeader.size());\n\t\tif (length > 0) {\n\t\t\tif (offset > 0) {\n\t\t\t\tshiftIncomingBy(offset);\n\t\t\t}\n\t\t\t_incomingGoodDataOffset = fullHeader;\n\t\t\t_incomingGoodDataLimit = length;\n\t\t} else {\n\t\t\toffset += kServerHeader.size() + kLengthSize + length;\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid TlsSocket::shiftIncomingBy(int amount) {\n\tExpects(_incomingGoodDataOffset == 0);\n\tExpects(_incomingGoodDataLimit == 0);\n\n\tconst auto incoming = bytes::make_detached_span(_incoming);\n\tif (incoming.size() > amount) {\n\t\tbytes::move(incoming, incoming.subspan(amount));\n\t\t_incoming.chop(amount);\n\t} else {\n\t\t_incoming.clear();\n\t}\n}\n\nvoid TlsSocket::connectToHost(const QString &address, int port) {\n\tExpects(_state == State::NotConnected);\n\n\t_state = State::Connecting;\n\t_socket.connectToHost(address, port);\n}\n\nbool TlsSocket::isGoodStartNonce(bytes::const_span nonce) {\n\treturn true;\n}\n\nvoid TlsSocket::timedOut() {\n\t_syncTimeRequests.fire({});\n}\n\nbool TlsSocket::isConnected() {\n\treturn (_state == State::Connected);\n}\n\nbool TlsSocket::hasBytesAvailable() {\n\treturn (_incomingGoodDataLimit > 0)\n\t\t&& (_incomingGoodDataOffset < _incoming.size());\n}\n\nint64 TlsSocket::read(bytes::span buffer) {\n\tauto written = int64(0);\n\twhile (_incomingGoodDataLimit) {\n\t\tconst auto available = std::min(\n\t\t\t_incomingGoodDataLimit,\n\t\t\tint(_incoming.size()) - _incomingGoodDataOffset);\n\t\tif (available <= 0) {\n\t\t\treturn written;\n\t\t}\n\t\tconst auto write = std::min(std::size_t(available), buffer.size());\n\t\tif (write <= 0) {\n\t\t\treturn written;\n\t\t}\n\t\tbytes::copy(\n\t\t\tbuffer,\n\t\t\tbytes::make_span(_incoming).subspan(\n\t\t\t\t_incomingGoodDataOffset,\n\t\t\t\twrite));\n\t\twritten += write;\n\t\tbuffer = buffer.subspan(write);\n\t\t_incomingGoodDataLimit -= write;\n\t\t_incomingGoodDataOffset += write;\n\t\tif (_incomingGoodDataLimit) {\n\t\t\treturn written;\n\t\t}\n\t\tshiftIncomingBy(base::take(_incomingGoodDataOffset));\n\t\tif (!checkNextPacket()) {\n\t\t\t_state = State::Error;\n\t\t\tInvokeQueued(this, [=] { handleError(); });\n\t\t\treturn written;\n\t\t}\n\t}\n\treturn written;\n}\n\nvoid TlsSocket::write(bytes::const_span prefix, bytes::const_span buffer) {\n\tExpects(!buffer.empty());\n\n\tif (!isConnected()) {\n\t\treturn;\n\t}\n\tif (!prefix.empty()) {\n\t\t_socket.write(kClientPrefix.data(), kClientPrefix.size());\n\t}\n\twhile (!buffer.empty()) {\n\t\tconst auto write = std::min(\n\t\t\tkClientPartSize - prefix.size(),\n\t\t\tbuffer.size());\n\t\t_socket.write(kClientHeader.data(), kClientHeader.size());\n\t\tconst auto size = qToBigEndian(uint16(prefix.size() + write));\n\t\t_socket.write(reinterpret_cast<const char*>(&size), sizeof(size));\n\t\tif (!prefix.empty()) {\n\t\t\t_socket.write(\n\t\t\t\treinterpret_cast<const char*>(prefix.data()),\n\t\t\t\tprefix.size());\n\t\t\tprefix = bytes::const_span();\n\t\t}\n\t\t_socket.write(\n\t\t\treinterpret_cast<const char*>(buffer.data()),\n\t\t\twrite);\n\t\tbuffer = buffer.subspan(write);\n\t}\n}\n\nint32 TlsSocket::debugState() {\n\treturn _socket.state();\n}\n\nQString TlsSocket::debugPostfix() const {\n\treturn u\"_ee\"_q;\n}\n\nvoid TlsSocket::handleError(int errorCode) {\n\tif (_state != State::Connected) {\n\t\t_syncTimeRequests.fire({});\n\t}\n\tif (errorCode) {\n\t\tlogError(errorCode, _socket.errorString());\n\t}\n\t_state = State::Error;\n\t_error.fire({});\n}\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/details/mtproto_tls_socket.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/details/mtproto_abstract_socket.h\"\n\nnamespace MTP::details {\n\nclass TlsSocket final : public AbstractSocket {\npublic:\n\tTlsSocket(\n\t\tnot_null<QThread*> thread,\n\t\tconst bytes::vector &secret,\n\t\tconst QNetworkProxy &proxy,\n\t\tbool protocolForFiles);\n\n\tvoid connectToHost(const QString &address, int port) override;\n\tbool isGoodStartNonce(bytes::const_span nonce) override;\n\tvoid timedOut() override;\n\tbool isConnected() override;\n\tbool hasBytesAvailable() override;\n\tint64 read(bytes::span buffer) override;\n\tvoid write(bytes::const_span prefix, bytes::const_span buffer) override;\n\n\tint32 debugState() override;\n\tQString debugPostfix() const override;\n\nprivate:\n\tenum class State {\n\t\tNotConnected,\n\t\tConnecting,\n\t\tWaitingHello,\n\t\tConnected,\n\t\tError,\n\t};\n\n\t[[nodiscard]] bytes::const_span domainFromSecret() const;\n\t[[nodiscard]] bytes::const_span keyFromSecret() const;\n\n\tvoid plainConnected();\n\tvoid plainDisconnected();\n\tvoid plainReadyRead();\n\tvoid handleError(int errorCode = 0);\n\t[[nodiscard]] bool requiredHelloPartReady() const;\n\tvoid readHello();\n\tvoid checkHelloParts12(int parts1Size);\n\tvoid checkHelloParts34(int parts123Size);\n\tvoid checkHelloDigest();\n\tvoid readData();\n\t[[nodiscard]] bool checkNextPacket();\n\tvoid shiftIncomingBy(int amount);\n\n\tconst bytes::vector _secret;\n\tQTcpSocket _socket;\n\tState _state = State::NotConnected;\n\tQByteArray _incoming;\n\tint _incomingGoodDataOffset = 0;\n\tint _incomingGoodDataLimit = 0;\n\tint16 _serverHelloLength = 0;\n\n};\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/facade.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/facade.h\"\n\n#include \"storage/localstorage.h\"\n#include \"core/application.h\"\n#include \"main/main_account.h\"\n\nnamespace MTP {\nnamespace details {\nnamespace {\n\nint PauseLevel = 0;\nrpl::event_stream<> Unpaused;\n\n} // namespace\n\nbool paused() {\n\treturn PauseLevel > 0;\n}\n\nvoid pause() {\n\t++PauseLevel;\n}\n\nvoid unpause() {\n\t--PauseLevel;\n\tif (!PauseLevel) {\n\t\tUnpaused.fire({});\n\t}\n}\n\nrpl::producer<> unpaused() {\n\treturn Unpaused.events();\n}\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/facade.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/type_utils.h\"\n#include \"mtproto/mtp_instance.h\"\n\nnamespace MTP {\nnamespace details {\n\n[[nodiscard]] bool paused();\nvoid pause();\nvoid unpause();\n[[nodiscard]] rpl::producer<> unpaused();\n\n} // namespace details\n\n// send(MTPhelp_GetConfig(), MTP::configDcId(dc)) - for dc enumeration\nconstexpr ShiftedDcId configDcId(DcId dcId) {\n\treturn ShiftDcId(dcId, kConfigDcShift);\n}\n\n// send(MTPauth_LogOut(), MTP::logoutDcId(dc)) - for logout of guest dcs enumeration\nconstexpr ShiftedDcId logoutDcId(DcId dcId) {\n\treturn ShiftDcId(dcId, kLogoutDcShift);\n}\n\n// send(MTPupload_GetFile(), MTP::updaterDcId(dc)) - for autoupdater\nconstexpr ShiftedDcId updaterDcId(DcId dcId) {\n\treturn ShiftDcId(dcId, kUpdaterDcShift);\n}\n\n// send(MTPupload_GetFile(), MTP::groupCallStreamDcId(dc)) - for group call stream\nconstexpr ShiftedDcId groupCallStreamDcId(DcId dcId) {\n\treturn ShiftDcId(dcId, kGroupCallStreamDcShift);\n}\n\nnamespace details {\n\nconstexpr ShiftedDcId downloadDcId(DcId dcId, int index) {\n\tExpects(index < kMaxMediaDcCount);\n\n\treturn ShiftDcId(dcId, kBaseDownloadDcShift + index);\n};\n\n} // namespace details\n\n// send(req, callbacks, MTP::downloadDcId(dc, index)) - for download shifted dc id\ninline ShiftedDcId downloadDcId(DcId dcId, int index) {\n\treturn details::downloadDcId(dcId, index);\n}\n\ninline constexpr bool isDownloadDcId(ShiftedDcId shiftedDcId) {\n\treturn (shiftedDcId >= details::downloadDcId(0, 0))\n\t\t&& (shiftedDcId < details::downloadDcId(0, kMaxMediaDcCount - 1) + kDcShift);\n}\n\ninline constexpr bool isMediaClusterDcId(ShiftedDcId shiftedDcId) {\n\tconst auto shift = GetDcIdShift(shiftedDcId);\n\treturn isDownloadDcId(shiftedDcId)\n\t\t|| (shift == kGroupCallStreamDcShift)\n\t\t|| (shift == kExportMediaDcShift)\n\t\t|| (shift == kUpdaterDcShift);\n}\n\ninline bool isCdnDc(MTPDdcOption::Flags flags) {\n\treturn (flags & MTPDdcOption::Flag::f_cdn);\n}\n\ninline bool isTemporaryDcId(ShiftedDcId shiftedDcId) {\n\tauto dcId = BareDcId(shiftedDcId);\n\treturn (dcId >= Instance::Fields::kTemporaryMainDc);\n}\n\ninline DcId getRealIdFromTemporaryDcId(ShiftedDcId shiftedDcId) {\n\tauto dcId = BareDcId(shiftedDcId);\n\treturn (dcId >= Instance::Fields::kTemporaryMainDc) ? (dcId - Instance::Fields::kTemporaryMainDc) : 0;\n}\n\ninline DcId getTemporaryIdFromRealDcId(ShiftedDcId shiftedDcId) {\n\tauto dcId = BareDcId(shiftedDcId);\n\treturn (dcId < Instance::Fields::kTemporaryMainDc) ? (dcId + Instance::Fields::kTemporaryMainDc) : 0;\n}\n\nnamespace details {\n\nconstexpr ShiftedDcId uploadDcId(DcId dcId, int index) {\n\treturn ShiftDcId(dcId, kBaseUploadDcShift + index);\n};\n\n} // namespace details\n\n// send(req, callbacks, MTP::uploadDcId(index)) - for upload shifted dc id\n// uploading always to the main dc so BareDcId(result) == 0\ninline ShiftedDcId uploadDcId(int index) {\n\tExpects(index >= 0 && index < kMaxMediaDcCount);\n\n\treturn details::uploadDcId(0, index);\n};\n\nconstexpr bool isUploadDcId(ShiftedDcId shiftedDcId) {\n\treturn (shiftedDcId >= details::uploadDcId(0, 0))\n\t\t&& (shiftedDcId < details::uploadDcId(0, kMaxMediaDcCount - 1) + kDcShift);\n}\n\ninline ShiftedDcId destroyKeyNextDcId(ShiftedDcId shiftedDcId) {\n\tconst auto shift = GetDcIdShift(shiftedDcId);\n\treturn ShiftDcId(BareDcId(shiftedDcId), shift ? (shift + 1) : kDestroyKeyStartDcShift);\n}\n\nenum {\n\tDisconnectedState = 0,\n\tConnectingState = 1,\n\tConnectedState = 2,\n};\n\nenum {\n\tRequestSent = 0,\n\tRequestConnecting = 1,\n\tRequestSending = 2\n};\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtp_instance.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/mtp_instance.h\"\n\n#include \"mtproto/details/mtproto_dcenter.h\"\n#include \"mtproto/details/mtproto_rsa_public_key.h\"\n#include \"mtproto/special_config_request.h\"\n#include \"mtproto/session.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"mtproto/config_loader.h\"\n#include \"mtproto/sender.h\"\n#include \"storage/localstorage.h\"\n#include \"calls/calls_instance.h\"\n#include \"main/main_account.h\" // Account::configUpdated.\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_instance.h\"\n#include \"lang/lang_cloud_manager.h\"\n#include \"base/unixtime.h\"\n#include \"base/call_delayed.h\"\n#include \"base/timer.h\"\n#include \"base/network_reachability.h\"\n\nnamespace MTP {\nnamespace {\n\nconstexpr auto kConfigBecomesOldIn = 2 * 60 * crl::time(1000);\nconstexpr auto kConfigBecomesOldForBlockedIn = 8 * crl::time(1000);\n\nusing namespace details;\n\nstd::atomic<int> GlobalAtomicRequestId = 0;\n\n} // namespace\n\nnamespace details {\n\nint GetNextRequestId() {\n\tconst auto result = ++GlobalAtomicRequestId;\n\tif (result == std::numeric_limits<int>::max() / 2) {\n\t\tGlobalAtomicRequestId = 0;\n\t}\n\treturn result;\n}\n\n} // namespace details\n\nclass Instance::Private : private Sender {\npublic:\n\tPrivate(\n\t\tnot_null<Instance*> instance,\n\t\tInstance::Mode mode,\n\t\tFields &&fields);\n\n\tvoid start();\n\n\t[[nodiscard]] Config &config() const;\n\t[[nodiscard]] const ConfigFields &configValues() const;\n\t[[nodiscard]] DcOptions &dcOptions() const;\n\t[[nodiscard]] Environment environment() const;\n\t[[nodiscard]] bool isTestMode() const;\n\n\tvoid resolveProxyDomain(const QString &host);\n\tvoid setGoodProxyDomain(const QString &host, const QString &ip);\n\tvoid suggestMainDcId(DcId mainDcId);\n\tvoid setMainDcId(DcId mainDcId);\n\t[[nodiscard]] bool hasMainDcId() const;\n\t[[nodiscard]] DcId mainDcId() const;\n\t[[nodiscard]] rpl::producer<DcId> mainDcIdValue() const;\n\n\t[[nodiscard]] rpl::producer<> writeKeysRequests() const;\n\n\tvoid dcPersistentKeyChanged(DcId dcId, const AuthKeyPtr &persistentKey);\n\tvoid dcTemporaryKeyChanged(DcId dcId);\n\t[[nodiscard]] rpl::producer<DcId> dcTemporaryKeyChanged() const;\n\t[[nodiscard]] AuthKeysList getKeysForWrite() const;\n\tvoid addKeysForDestroy(AuthKeysList &&keys);\n\t[[nodiscard]] rpl::producer<> allKeysDestroyed() const;\n\n\t// Thread safe.\n\t[[nodiscard]] QString deviceModel() const;\n\t[[nodiscard]] QString systemVersion() const;\n\n\t// Main thread.\n\tvoid requestConfig();\n\tvoid requestConfigIfOld();\n\tvoid requestCDNConfig();\n\tvoid setUserPhone(const QString &phone);\n\tvoid badConfigurationError();\n\tvoid syncHttpUnixtime();\n\n\tvoid restartedByTimeout(ShiftedDcId shiftedDcId);\n\t[[nodiscard]] rpl::producer<ShiftedDcId> restartsByTimeout() const;\n\n\t[[nodiscard]] auto nonPremiumDelayedRequests() const\n\t-> rpl::producer<mtpRequestId>;\n\t[[nodiscard]] rpl::producer<> frozenErrorReceived() const;\n\n\tvoid restart();\n\tvoid restart(ShiftedDcId shiftedDcId);\n\t[[nodiscard]] int32 dcstate(ShiftedDcId shiftedDcId = 0);\n\t[[nodiscard]] QString dctransport(ShiftedDcId shiftedDcId = 0);\n\tvoid ping();\n\tvoid cancel(mtpRequestId requestId);\n\t[[nodiscard]] int32 state(mtpRequestId requestId); // < 0 means waiting for such count of ms\n\tvoid killSession(ShiftedDcId shiftedDcId);\n\tvoid stopSession(ShiftedDcId shiftedDcId);\n\tvoid reInitConnection(DcId dcId);\n\tvoid logout(Fn<void()> done);\n\n\tnot_null<Dcenter*> getDcById(ShiftedDcId shiftedDcId);\n\tDcenter *findDc(ShiftedDcId shiftedDcId);\n\tnot_null<Dcenter*> addDc(\n\t\tShiftedDcId shiftedDcId,\n\t\tAuthKeyPtr &&key = nullptr);\n\tvoid removeDc(ShiftedDcId shiftedDcId);\n\n\tvoid sendRequest(\n\t\tmtpRequestId requestId,\n\t\tSerializedRequest &&request,\n\t\tResponseHandler &&callbacks,\n\t\tShiftedDcId shiftedDcId,\n\t\tcrl::time msCanWait,\n\t\tbool needsLayer,\n\t\tmtpRequestId afterRequestId);\n\tvoid registerRequest(mtpRequestId requestId, ShiftedDcId shiftedDcId);\n\tvoid unregisterRequest(mtpRequestId requestId);\n\tvoid storeRequest(\n\t\tmtpRequestId requestId,\n\t\tconst SerializedRequest &request,\n\t\tResponseHandler &&callbacks);\n\tSerializedRequest getRequest(mtpRequestId requestId);\n\t[[nodiscard]] bool hasCallback(mtpRequestId requestId) const;\n\tvoid processCallback(const Response &response);\n\tvoid processUpdate(const Response &message);\n\n\tvoid onStateChange(ShiftedDcId shiftedDcId, int32 state);\n\tvoid onSessionReset(ShiftedDcId shiftedDcId);\n\n\t// return true if need to clean request data\n\tbool rpcErrorOccured(\n\t\tconst Response &response,\n\t\tconst FailHandler &onFail,\n\t\tconst Error &error);\n\tinline bool rpcErrorOccured(\n\t\t\tconst Response &response,\n\t\t\tconst ResponseHandler &handler,\n\t\t\tconst Error &error) {\n\t\treturn rpcErrorOccured(response, handler.fail, error);\n\t}\n\n\tvoid setUpdatesHandler(Fn<void(const Response&)> handler);\n\tvoid setGlobalFailHandler(\n\t\tFn<void(const Error&, const Response&)> handler);\n\tvoid setStateChangedHandler(Fn<void(ShiftedDcId shiftedDcId, int32 state)> handler);\n\tvoid setSessionResetHandler(Fn<void(ShiftedDcId shiftedDcId)> handler);\n\tvoid clearGlobalHandlers();\n\n\t[[nodiscard]] not_null<Session*> getSession(ShiftedDcId shiftedDcId);\n\n\tbool isNormal() const {\n\t\treturn (_mode == Instance::Mode::Normal);\n\t}\n\tbool isKeysDestroyer() const {\n\t\treturn (_mode == Instance::Mode::KeysDestroyer);\n\t}\n\n\tvoid scheduleKeyDestroy(ShiftedDcId shiftedDcId);\n\tvoid keyWasPossiblyDestroyed(ShiftedDcId shiftedDcId);\n\tvoid performKeyDestroy(ShiftedDcId shiftedDcId);\n\tvoid completedKeyDestroy(ShiftedDcId shiftedDcId);\n\tvoid keyDestroyedOnServer(ShiftedDcId shiftedDcId, uint64 keyId);\n\n\tvoid prepareToDestroy();\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tvoid importDone(\n\t\tconst MTPauth_Authorization &result,\n\t\tconst Response &response);\n\tbool importFail(const Error &error, const Response &response);\n\tvoid exportDone(\n\t\tconst MTPauth_ExportedAuthorization &result,\n\t\tconst Response &response);\n\tbool exportFail(const Error &error, const Response &response);\n\tbool onErrorDefault(const Error &error, const Response &response);\n\n\tvoid unpaused();\n\n\tSession *findSession(ShiftedDcId shiftedDcId);\n\tnot_null<Session*> startSession(ShiftedDcId shiftedDcId);\n\tvoid scheduleSessionDestroy(ShiftedDcId shiftedDcId);\n\t[[nodiscard]] not_null<QThread*> getThreadForDc(ShiftedDcId shiftedDcId);\n\n\tvoid applyDomainIps(\n\t\tconst QString &host,\n\t\tconst QStringList &ips,\n\t\tcrl::time expireAt);\n\n\tvoid logoutGuestDcs();\n\tbool logoutGuestDone(mtpRequestId requestId);\n\n\tvoid requestConfigIfExpired();\n\tvoid configLoadDone(const MTPConfig &result);\n\tbool configLoadFail(const Error &error);\n\n\tstd::optional<ShiftedDcId> queryRequestByDc(\n\t\tmtpRequestId requestId) const;\n\tstd::optional<ShiftedDcId> changeRequestByDc(\n\t\tmtpRequestId requestId, DcId newdc);\n\n\tvoid checkDelayedRequests();\n\n\tconst not_null<Instance*> _instance;\n\tconst Instance::Mode _mode = Instance::Mode::Normal;\n\tconst std::unique_ptr<Config> _config;\n\tconst std::shared_ptr<base::NetworkReachability> _networkReachability;\n\n\tstd::unique_ptr<QThread> _mainSessionThread;\n\tstd::unique_ptr<QThread> _otherSessionsThread;\n\tstd::vector<std::unique_ptr<QThread>> _fileSessionThreads;\n\n\tQString _deviceModelDefault;\n\tQString _systemVersion;\n\n\tmutable QMutex _deviceModelMutex;\n\tQString _customDeviceModel;\n\n\trpl::variable<DcId> _mainDcId = Fields::kDefaultMainDc;\n\tbool _mainDcIdForced = false;\n\tbase::flat_map<DcId, std::unique_ptr<Dcenter>> _dcenters;\n\tstd::vector<std::unique_ptr<Dcenter>> _dcentersToDestroy;\n\trpl::event_stream<DcId> _dcTemporaryKeyChanged;\n\n\tSession *_mainSession = nullptr;\n\tbase::flat_map<ShiftedDcId, std::unique_ptr<Session>> _sessions;\n\tstd::vector<std::unique_ptr<Session>> _sessionsToDestroy;\n\trpl::event_stream<ShiftedDcId> _restartsByTimeout;\n\n\tstd::unique_ptr<ConfigLoader> _configLoader;\n\tstd::unique_ptr<DomainResolver> _domainResolver;\n\tstd::unique_ptr<SpecialConfigRequest> _httpUnixtimeLoader;\n\tQString _userPhone;\n\tmtpRequestId _cdnConfigLoadRequestId = 0;\n\tcrl::time _lastConfigLoadedTime = 0;\n\tcrl::time _configExpiresAt = 0;\n\n\tbase::flat_map<DcId, AuthKeyPtr> _keysForWrite;\n\tbase::flat_map<ShiftedDcId, mtpRequestId> _logoutGuestRequestIds;\n\n\trpl::event_stream<> _writeKeysRequests;\n\trpl::event_stream<> _allKeysDestroyed;\n\n\t// holds dcWithShift for request to this dc or -dc for request to main dc\n\tstd::map<mtpRequestId, ShiftedDcId> _requestsByDc;\n\tmutable QMutex _requestByDcLock;\n\n\t// holds target dcWithShift for auth export request\n\tstd::map<mtpRequestId, ShiftedDcId> _authExportRequests;\n\n\tstd::map<mtpRequestId, ResponseHandler> _parserMap;\n\tmutable QMutex _parserMapLock;\n\n\tstd::map<mtpRequestId, SerializedRequest> _requestMap;\n\tQReadWriteLock _requestMapLock;\n\n\tstd::deque<std::pair<mtpRequestId, crl::time>> _delayedRequests;\n\tbase::flat_map<mtpRequestId, mtpRequestId> _dependentRequests;\n\tmutable QMutex _dependentRequestsLock;\n\n\tstd::map<mtpRequestId, int> _requestsDelays;\n\n\tstd::set<mtpRequestId> _badGuestDcRequests;\n\n\tstd::map<DcId, std::vector<mtpRequestId>> _authWaiters;\n\n\tFn<void(const Response&)> _updatesHandler;\n\tFn<void(const Error&, const Response&)> _globalFailHandler;\n\tFn<void(ShiftedDcId shiftedDcId, int32 state)> _stateChangedHandler;\n\tFn<void(ShiftedDcId shiftedDcId)> _sessionResetHandler;\n\n\trpl::event_stream<mtpRequestId> _nonPremiumDelayedRequests;\n\trpl::event_stream<> _frozenErrorReceived;\n\n\tbase::Timer _checkDelayedTimer;\n\n\tCore::SettingsProxy &_proxySettings;\n\n\trpl::lifetime _lifetime;\n\n};\n\nInstance::Fields::Fields() = default;\n\nInstance::Fields::Fields(Fields &&other) = default;\n\nauto Instance::Fields::operator=(Fields &&other) -> Fields & = default;\n\nInstance::Fields::~Fields() = default;\n\nInstance::Private::Private(\n\tnot_null<Instance*> instance,\n\tInstance::Mode mode,\n\tFields &&fields)\n: Sender(instance)\n, _instance(instance)\n, _mode(mode)\n, _config(std::move(fields.config))\n, _networkReachability(base::NetworkReachability::Instance())\n, _proxySettings(Core::App().settings().proxy()) {\n\tExpects(_config != nullptr);\n\n\tconst auto idealThreadPoolSize = QThread::idealThreadCount();\n\t_fileSessionThreads.resize(2 * std::max(idealThreadPoolSize / 2, 1));\n\n\tdetails::unpaused(\n\t) | rpl::on_next([=] {\n\t\tunpaused();\n\t}, _lifetime);\n\n\t_networkReachability->availableChanges(\n\t) | rpl::on_next([=](bool available) {\n\t\trestart();\n\t}, _lifetime);\n\n\t_deviceModelDefault = std::move(fields.deviceModel);\n\t_systemVersion = std::move(fields.systemVersion);\n\n\t_customDeviceModel = Core::App().settings().customDeviceModel();\n\tCore::App().settings().customDeviceModelChanges(\n\t) | rpl::on_next([=](const QString &value) {\n\t\tQMutexLocker lock(&_deviceModelMutex);\n\t\t_customDeviceModel = value;\n\t\tlock.unlock();\n\n\t\treInitConnection(mainDcId());\n\t}, _lifetime);\n\n\tfor (auto &key : fields.keys) {\n\t\tauto dcId = key->dcId();\n\t\tauto shiftedDcId = dcId;\n\t\tif (isKeysDestroyer()) {\n\t\t\tshiftedDcId = MTP::destroyKeyNextDcId(shiftedDcId);\n\n\t\t\t// There could be several keys for one dc if we're destroying them.\n\t\t\t// Place them all in separate shiftedDcId so that they won't conflict.\n\t\t\twhile (_keysForWrite.find(shiftedDcId) != _keysForWrite.cend()) {\n\t\t\t\tshiftedDcId = MTP::destroyKeyNextDcId(shiftedDcId);\n\t\t\t}\n\t\t}\n\t\t_keysForWrite[shiftedDcId] = key;\n\t\taddDc(shiftedDcId, std::move(key));\n\t}\n\n\tif (fields.mainDcId != Fields::kNotSetMainDc) {\n\t\t_mainDcId = fields.mainDcId;\n\t\t_mainDcIdForced = true;\n\t}\n\n\t_proxySettings.connectionTypeChanges(\n\t) | rpl::on_next([=] {\n\t\tif (_configLoader) {\n\t\t\t_configLoader->setProxyEnabled(_proxySettings.isEnabled());\n\t\t}\n\t}, _lifetime);\n}\n\nvoid Instance::Private::start() {\n\tif (isKeysDestroyer()) {\n\t\tfor (const auto &[shiftedDcId, dc] : _dcenters) {\n\t\t\tstartSession(shiftedDcId);\n\t\t}\n\t} else if (hasMainDcId()) {\n\t\t_mainSession = startSession(mainDcId());\n\t}\n\n\t_checkDelayedTimer.setCallback([this] { checkDelayedRequests(); });\n\n\tAssert(!hasMainDcId() == isKeysDestroyer());\n\trequestConfig();\n}\n\nvoid Instance::Private::resolveProxyDomain(const QString &host) {\n\tif (!_domainResolver) {\n\t\t_domainResolver = std::make_unique<DomainResolver>([=](\n\t\t\t\tconst QString &host,\n\t\t\t\tconst QStringList &ips,\n\t\t\t\tcrl::time expireAt) {\n\t\t\tapplyDomainIps(host, ips, expireAt);\n\t\t});\n\t}\n\t_domainResolver->resolve(host);\n}\n\nvoid Instance::Private::applyDomainIps(\n\t\tconst QString &host,\n\t\tconst QStringList &ips,\n\t\tcrl::time expireAt) {\n\tconst auto applyToProxy = [&](ProxyData &proxy) {\n\t\tif (!proxy.tryCustomResolve() || proxy.host != host) {\n\t\t\treturn false;\n\t\t}\n\t\tproxy.resolvedExpireAt = expireAt;\n\t\tauto copy = ips;\n\t\tauto &current = proxy.resolvedIPs;\n\t\tconst auto i = ranges::remove_if(current, [&](const QString &ip) {\n\t\t\tconst auto index = copy.indexOf(ip);\n\t\t\tif (index < 0) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tcopy.removeAt(index);\n\t\t\treturn false;\n\t\t});\n\t\tif (i == end(current) && copy.isEmpty()) {\n\t\t\t// Even if the proxy was changed already, we still want\n\t\t\t// to refreshOptions in all sessions across all instances.\n\t\t\treturn true;\n\t\t}\n\t\tcurrent.erase(i, end(current));\n\t\tfor (const auto &ip : std::as_const(copy)) {\n\t\t\tproxy.resolvedIPs.push_back(ip);\n\t\t}\n\t\treturn true;\n\t};\n\tfor (auto &proxy : _proxySettings.list()) {\n\t\tapplyToProxy(proxy);\n\t}\n\tauto selected = _proxySettings.selected();\n\tif (applyToProxy(selected) && _proxySettings.isEnabled()) {\n\t\t_proxySettings.setSelected(selected);\n\t\tfor (const auto &[shiftedDcId, session] : _sessions) {\n\t\t\tsession->refreshOptions();\n\t\t}\n\t}\n\t_instance->proxyDomainResolved(host, ips, expireAt);\n}\n\nvoid Instance::Private::setGoodProxyDomain(\n\t\tconst QString &host,\n\t\tconst QString &ip) {\n\tconst auto applyToProxy = [&](ProxyData &proxy) {\n\t\tif (!proxy.tryCustomResolve() || proxy.host != host) {\n\t\t\treturn false;\n\t\t}\n\t\tauto &current = proxy.resolvedIPs;\n\t\tauto i = ranges::find(current, ip);\n\t\tif (i == end(current) || i == begin(current)) {\n\t\t\treturn false;\n\t\t}\n\t\twhile (i != begin(current)) {\n\t\t\tconst auto j = i--;\n\t\t\tstd::swap(*i, *j);\n\t\t}\n\t\treturn true;\n\t};\n\tfor (auto &proxy : _proxySettings.list()) {\n\t\tapplyToProxy(proxy);\n\t}\n\n\tauto selected = _proxySettings.selected();\n\tif (applyToProxy(selected) && _proxySettings.isEnabled()) {\n\t\t_proxySettings.setSelected(selected);\n\t\tCore::App().refreshGlobalProxy();\n\t}\n}\n\nvoid Instance::Private::suggestMainDcId(DcId mainDcId) {\n\tif (!_mainDcIdForced) {\n\t\tsetMainDcId(mainDcId);\n\t}\n}\n\nvoid Instance::Private::setMainDcId(DcId mainDcId) {\n\tif (!_mainSession) {\n\t\tLOG((\"MTP Error: attempting to change mainDcId in an MTP instance without main session.\"));\n\t\treturn;\n\t}\n\n\t_mainDcIdForced = true;\n\tconst auto oldMainDcId = _mainSession->getDcWithShift();\n\tif (oldMainDcId != mainDcId) {\n\t\tscheduleSessionDestroy(oldMainDcId);\n\t\tscheduleSessionDestroy(mainDcId);\n\t\t_mainSession = startSession(mainDcId);\n\t}\n\t_mainDcId = mainDcId;\n\t_writeKeysRequests.fire({});\n}\n\nbool Instance::Private::hasMainDcId() const {\n\treturn (_mainDcId.current() != Fields::kNoneMainDc);\n}\n\nDcId Instance::Private::mainDcId() const {\n\tExpects(hasMainDcId());\n\n\treturn _mainDcId.current();\n}\n\nrpl::producer<DcId> Instance::Private::mainDcIdValue() const {\n\tExpects(_mainDcId.current() != Fields::kNoneMainDc);\n\n\treturn _mainDcId.value();\n}\n\nvoid Instance::Private::requestConfig() {\n\tif (_configLoader || isKeysDestroyer()) {\n\t\treturn;\n\t}\n\t_configLoader = std::make_unique<ConfigLoader>(\n\t\t_instance,\n\t\t_userPhone,\n\t\t[=](const MTPConfig &result) { configLoadDone(result); },\n\t\t[=](const Error &error, const Response &) {\n\t\t\treturn configLoadFail(error);\n\t\t},\n\t\t_proxySettings.isEnabled());\n\t_configLoader->load();\n}\n\nvoid Instance::Private::setUserPhone(const QString &phone) {\n\tif (_userPhone != phone) {\n\t\t_userPhone = phone;\n\t\tif (_configLoader) {\n\t\t\t_configLoader->setPhone(_userPhone);\n\t\t}\n\t}\n}\n\nvoid Instance::Private::badConfigurationError() {\n\tif (_mode == Mode::Normal) {\n\t\tCore::App().badMtprotoConfigurationError();\n\t}\n}\n\nvoid Instance::Private::syncHttpUnixtime() {\n\tif (base::unixtime::http_valid() || _httpUnixtimeLoader) {\n\t\treturn;\n\t}\n\t_httpUnixtimeLoader = std::make_unique<SpecialConfigRequest>([=] {\n\t\tInvokeQueued(_instance, [=] {\n\t\t\t_httpUnixtimeLoader = nullptr;\n\t\t});\n\t}, isTestMode(), configValues().txtDomainString);\n}\n\nvoid Instance::Private::restartedByTimeout(ShiftedDcId shiftedDcId) {\n\t_restartsByTimeout.fire_copy(shiftedDcId);\n}\n\nrpl::producer<ShiftedDcId> Instance::Private::restartsByTimeout() const {\n\treturn _restartsByTimeout.events();\n}\n\nauto Instance::Private::nonPremiumDelayedRequests() const\n-> rpl::producer<mtpRequestId> {\n\treturn _nonPremiumDelayedRequests.events();\n}\n\nrpl::producer<> Instance::Private::frozenErrorReceived() const {\n\treturn _frozenErrorReceived.events();\n}\n\nvoid Instance::Private::requestConfigIfOld() {\n\tconst auto timeout = _config->values().blockedMode\n\t\t? kConfigBecomesOldForBlockedIn\n\t\t: kConfigBecomesOldIn;\n\tif (crl::now() - _lastConfigLoadedTime >= timeout) {\n\t\trequestConfig();\n\t}\n}\n\nvoid Instance::Private::requestConfigIfExpired() {\n\tconst auto requestIn = (_configExpiresAt - crl::now());\n\tif (requestIn > 0) {\n\t\tbase::call_delayed(\n\t\t\tstd::min(requestIn, 3600 * crl::time(1000)),\n\t\t\t_instance,\n\t\t\t[=] { requestConfigIfExpired(); });\n\t} else {\n\t\trequestConfig();\n\t}\n}\n\nvoid Instance::Private::requestCDNConfig() {\n\tif (_cdnConfigLoadRequestId || !hasMainDcId()) {\n\t\treturn;\n\t}\n\t_cdnConfigLoadRequestId = request(\n\t\tMTPhelp_GetCdnConfig()\n\t).done([this](const MTPCdnConfig &result) {\n\t\t_cdnConfigLoadRequestId = 0;\n\t\tresult.match([&](const MTPDcdnConfig &data) {\n\t\t\tdcOptions().setCDNConfig(data);\n\t\t});\n\t\tLocal::writeSettings();\n\t}).send();\n}\n\nvoid Instance::Private::restart() {\n\tfor (const auto &[shiftedDcId, session] : _sessions) {\n\t\tsession->restart();\n\t}\n}\n\nvoid Instance::Private::restart(ShiftedDcId shiftedDcId) {\n\tconst auto dcId = BareDcId(shiftedDcId);\n\tfor (const auto &[shiftedDcId, session] : _sessions) {\n\t\tif (BareDcId(shiftedDcId) == dcId) {\n\t\t\tsession->restart();\n\t\t}\n\t}\n}\n\nint32 Instance::Private::dcstate(ShiftedDcId shiftedDcId) {\n\tif (!shiftedDcId) {\n\t\tAssert(_mainSession != nullptr);\n\t\treturn _mainSession->getState();\n\t}\n\n\tif (!BareDcId(shiftedDcId)) {\n\t\tAssert(_mainSession != nullptr);\n\t\tshiftedDcId += BareDcId(_mainSession->getDcWithShift());\n\t}\n\n\tif (const auto session = findSession(shiftedDcId)) {\n\t\treturn session->getState();\n\t}\n\treturn DisconnectedState;\n}\n\nQString Instance::Private::dctransport(ShiftedDcId shiftedDcId) {\n\tif (!shiftedDcId) {\n\t\tAssert(_mainSession != nullptr);\n\t\treturn _mainSession->transport();\n\t}\n\tif (!BareDcId(shiftedDcId)) {\n\t\tAssert(_mainSession != nullptr);\n\t\tshiftedDcId += BareDcId(_mainSession->getDcWithShift());\n\t}\n\n\tif (const auto session = findSession(shiftedDcId)) {\n\t\treturn session->transport();\n\t}\n\treturn QString();\n}\n\nvoid Instance::Private::ping() {\n\tgetSession(0)->ping();\n}\n\nvoid Instance::Private::cancel(mtpRequestId requestId) {\n\tif (!requestId) return;\n\n\tDEBUG_LOG((\"MTP Info: Cancel request %1.\").arg(requestId));\n\tconst auto shiftedDcId = queryRequestByDc(requestId);\n\tauto msgId = mtpMsgId(0);\n\t{\n\t\tQWriteLocker locker(&_requestMapLock);\n\t\tauto it = _requestMap.find(requestId);\n\t\tif (it != _requestMap.end()) {\n\t\t\tmsgId = *(mtpMsgId*)(it->second->constData() + 4);\n\t\t\t_requestMap.erase(it);\n\t\t}\n\t}\n\tunregisterRequest(requestId);\n\tif (shiftedDcId) {\n\t\tconst auto session = getSession(qAbs(*shiftedDcId));\n\t\tsession->cancel(requestId, msgId);\n\t}\n\n\tQMutexLocker locker(&_parserMapLock);\n\t_parserMap.erase(requestId);\n}\n\n// result < 0 means waiting for such count of ms.\nint32 Instance::Private::state(mtpRequestId requestId) {\n\tif (requestId > 0) {\n\t\tif (const auto shiftedDcId = queryRequestByDc(requestId)) {\n\t\t\tconst auto session = getSession(qAbs(*shiftedDcId));\n\t\t\treturn session->requestState(requestId);\n\t\t}\n\t\treturn MTP::RequestSent;\n\t}\n\tconst auto session = getSession(-requestId);\n\treturn session->requestState(0);\n}\n\nvoid Instance::Private::killSession(ShiftedDcId shiftedDcId) {\n\tconst auto i = _sessions.find(shiftedDcId);\n\tif (i != _sessions.cend()) {\n\t\tAssert(i->second.get() != _mainSession);\n\t}\n\tscheduleSessionDestroy(shiftedDcId);\n}\n\nvoid Instance::Private::stopSession(ShiftedDcId shiftedDcId) {\n\tif (const auto session = findSession(shiftedDcId)) {\n\t\tif (session != _mainSession) { // don't stop main session\n\t\t\tsession->stop();\n\t\t}\n\t}\n}\n\nvoid Instance::Private::reInitConnection(DcId dcId) {\n\tfor (const auto &[shiftedDcId, session] : _sessions) {\n\t\tif (BareDcId(shiftedDcId) == dcId) {\n\t\t\tsession->reInitConnection();\n\t\t}\n\t}\n}\n\nvoid Instance::Private::logout(Fn<void()> done) {\n\t_instance->send(MTPauth_LogOut(), [=](Response) {\n\t\tdone();\n\t\treturn true;\n\t}, [=](const Error&, Response) {\n\t\tdone();\n\t\treturn true;\n\t});\n\tlogoutGuestDcs();\n}\n\nvoid Instance::Private::logoutGuestDcs() {\n\tExpects(!isKeysDestroyer());\n\n\tauto dcIds = std::vector<DcId>();\n\tdcIds.reserve(_keysForWrite.size());\n\tfor (const auto &key : _keysForWrite) {\n\t\tdcIds.push_back(key.first);\n\t}\n\tfor (const auto dcId : dcIds) {\n\t\tif (dcId == mainDcId() || dcOptions().dcType(dcId) == DcType::Cdn) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto shiftedDcId = MTP::logoutDcId(dcId);\n\t\tconst auto requestId = _instance->send(MTPauth_LogOut(), [=](\n\t\t\t\tconst Response &response) {\n\t\t\tlogoutGuestDone(response.requestId);\n\t\t\treturn true;\n\t\t}, [=](const Error &, const Response &response) {\n\t\t\tlogoutGuestDone(response.requestId);\n\t\t\treturn true;\n\t\t}, shiftedDcId);\n\t\t_logoutGuestRequestIds.emplace(shiftedDcId, requestId);\n\t}\n}\n\nbool Instance::Private::logoutGuestDone(mtpRequestId requestId) {\n\tfor (auto i = _logoutGuestRequestIds.begin(), e = _logoutGuestRequestIds.end(); i != e; ++i) {\n\t\tif (i->second == requestId) {\n\t\t\tkillSession(i->first);\n\t\t\t_logoutGuestRequestIds.erase(i);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nDcenter *Instance::Private::findDc(ShiftedDcId shiftedDcId) {\n\tconst auto i = _dcenters.find(shiftedDcId);\n\treturn (i != _dcenters.end()) ? i->second.get() : nullptr;\n}\n\nnot_null<Dcenter*> Instance::Private::addDc(\n\t\tShiftedDcId shiftedDcId,\n\t\tAuthKeyPtr &&key) {\n\tconst auto dcId = BareDcId(shiftedDcId);\n\treturn _dcenters.emplace(\n\t\tshiftedDcId,\n\t\tstd::make_unique<Dcenter>(dcId, std::move(key))\n\t).first->second.get();\n}\n\nvoid Instance::Private::removeDc(ShiftedDcId shiftedDcId) {\n\tconst auto i = _dcenters.find(shiftedDcId);\n\tif (i != _dcenters.end()) {\n\t\t_dcentersToDestroy.push_back(std::move(i->second));\n\t\t_dcenters.erase(i);\n\t}\n}\n\nnot_null<Dcenter*> Instance::Private::getDcById(\n\t\tShiftedDcId shiftedDcId) {\n\tif (const auto result = findDc(shiftedDcId)) {\n\t\treturn result;\n\t}\n\tconst auto dcId = [&] {\n\t\tconst auto result = BareDcId(shiftedDcId);\n\t\tif (isTemporaryDcId(result)) {\n\t\t\tif (const auto realDcId = getRealIdFromTemporaryDcId(result)) {\n\t\t\t\treturn realDcId;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}();\n\tif (dcId != shiftedDcId) {\n\t\tif (const auto result = findDc(dcId)) {\n\t\t\treturn result;\n\t\t}\n\t}\n\treturn addDc(dcId);\n}\n\nvoid Instance::Private::dcPersistentKeyChanged(\n\t\tDcId dcId,\n\t\tconst AuthKeyPtr &persistentKey) {\n\tdcTemporaryKeyChanged(dcId);\n\n\tif (isTemporaryDcId(dcId)) {\n\t\treturn;\n\t}\n\n\tconst auto i = _keysForWrite.find(dcId);\n\tif (i != _keysForWrite.end() && i->second == persistentKey) {\n\t\treturn;\n\t} else if (i == _keysForWrite.end() && !persistentKey) {\n\t\treturn;\n\t}\n\tif (!persistentKey) {\n\t\t_keysForWrite.erase(i);\n\t} else if (i != _keysForWrite.end()) {\n\t\ti->second = persistentKey;\n\t} else {\n\t\t_keysForWrite.emplace(dcId, persistentKey);\n\t}\n\tDEBUG_LOG((\"AuthKey Info: writing auth keys, called by dc %1\"\n\t\t).arg(dcId));\n\t_writeKeysRequests.fire({});\n}\n\nvoid Instance::Private::dcTemporaryKeyChanged(DcId dcId) {\n\t_dcTemporaryKeyChanged.fire_copy(dcId);\n}\n\nrpl::producer<DcId> Instance::Private::dcTemporaryKeyChanged() const {\n\treturn _dcTemporaryKeyChanged.events();\n}\n\nAuthKeysList Instance::Private::getKeysForWrite() const {\n\tauto result = AuthKeysList();\n\n\tresult.reserve(_keysForWrite.size());\n\tfor (const auto &key : _keysForWrite) {\n\t\tresult.push_back(key.second);\n\t}\n\treturn result;\n}\n\nvoid Instance::Private::addKeysForDestroy(AuthKeysList &&keys) {\n\tExpects(isKeysDestroyer());\n\n\tfor (auto &key : keys) {\n\t\tconst auto dcId = key->dcId();\n\t\tauto shiftedDcId = MTP::destroyKeyNextDcId(dcId);\n\n\t\t// There could be several keys for one dc if we're destroying them.\n\t\t// Place them all in separate shiftedDcId so that they won't conflict.\n\t\twhile (_keysForWrite.find(shiftedDcId) != _keysForWrite.cend()) {\n\t\t\tshiftedDcId = MTP::destroyKeyNextDcId(shiftedDcId);\n\t\t}\n\t\t_keysForWrite[shiftedDcId] = key;\n\n\t\taddDc(shiftedDcId, std::move(key));\n\t\tstartSession(shiftedDcId);\n\t}\n}\n\nrpl::producer<> Instance::Private::allKeysDestroyed() const {\n\treturn _allKeysDestroyed.events();\n}\n\nrpl::producer<> Instance::Private::writeKeysRequests() const {\n\treturn _writeKeysRequests.events();\n}\n\nConfig &Instance::Private::config() const {\n\treturn *_config;\n}\n\nconst ConfigFields &Instance::Private::configValues() const {\n\treturn _config->values();\n}\n\nDcOptions &Instance::Private::dcOptions() const {\n\treturn _config->dcOptions();\n}\n\nEnvironment Instance::Private::environment() const {\n\treturn _config->environment();\n}\n\nbool Instance::Private::isTestMode() const {\n\treturn _config->isTestMode();\n}\n\nQString Instance::Private::deviceModel() const {\n\tQMutexLocker lock(&_deviceModelMutex);\n\treturn _customDeviceModel.isEmpty()\n\t\t? _deviceModelDefault\n\t\t: _customDeviceModel;\n}\n\nQString Instance::Private::systemVersion() const {\n\treturn _systemVersion;\n}\n\nvoid Instance::Private::unpaused() {\n\tfor (const auto &[shiftedDcId, session] : _sessions) {\n\t\tsession->unpaused();\n\t}\n}\n\nvoid Instance::Private::configLoadDone(const MTPConfig &result) {\n\tExpects(result.type() == mtpc_config);\n\n\t_configLoader.reset();\n\t_lastConfigLoadedTime = crl::now();\n\n\tconst auto &data = result.c_config();\n\t_config->apply(data);\n\n\tconst auto lang = qs(data.vsuggested_lang_code().value_or_empty());\n\tLang::CurrentCloudManager().setSuggestedLanguage(lang);\n\tLang::CurrentCloudManager().setCurrentVersions(\n\t\tdata.vlang_pack_version().value_or_empty(),\n\t\tdata.vbase_lang_pack_version().value_or_empty());\n\tif (const auto prefix = data.vautoupdate_url_prefix()) {\n\t\tLocal::writeAutoupdatePrefix(qs(*prefix));\n\t}\n\n\t_configExpiresAt = crl::now()\n\t\t+ (data.vexpires().v - base::unixtime::now()) * crl::time(1000);\n\trequestConfigIfExpired();\n}\n\nbool Instance::Private::configLoadFail(const Error &error) {\n\tif (IsDefaultHandledError(error)) return false;\n\n\t//\tloadingConfig = false;\n\tLOG((\"MTP Error: failed to get config!\"));\n\treturn false;\n}\n\nstd::optional<ShiftedDcId> Instance::Private::queryRequestByDc(\n\t\tmtpRequestId requestId) const {\n\tQMutexLocker locker(&_requestByDcLock);\n\tauto it = _requestsByDc.find(requestId);\n\tif (it != _requestsByDc.cend()) {\n\t\treturn it->second;\n\t}\n\treturn std::nullopt;\n}\n\nstd::optional<ShiftedDcId> Instance::Private::changeRequestByDc(\n\t\tmtpRequestId requestId,\n\t\tDcId newdc) {\n\tQMutexLocker locker(&_requestByDcLock);\n\tauto it = _requestsByDc.find(requestId);\n\tif (it != _requestsByDc.cend()) {\n\t\tif (it->second < 0) {\n\t\t\tit->second = -newdc;\n\t\t} else {\n\t\t\tit->second = ShiftDcId(newdc, GetDcIdShift(it->second));\n\t\t}\n\t\treturn it->second;\n\t}\n\treturn std::nullopt;\n}\n\nvoid Instance::Private::checkDelayedRequests() {\n\tauto now = crl::now();\n\twhile (!_delayedRequests.empty() && now >= _delayedRequests.front().second) {\n\t\tauto requestId = _delayedRequests.front().first;\n\t\t_delayedRequests.pop_front();\n\n\t\tauto dcWithShift = ShiftedDcId(0);\n\t\tif (const auto shiftedDcId = queryRequestByDc(requestId)) {\n\t\t\tdcWithShift = *shiftedDcId;\n\t\t} else {\n\t\t\tLOG((\"MTP Error: could not find request dc for delayed resend, requestId %1\").arg(requestId));\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto request = SerializedRequest();\n\t\t{\n\t\t\tQReadLocker locker(&_requestMapLock);\n\t\t\tauto it = _requestMap.find(requestId);\n\t\t\tif (it == _requestMap.cend()) {\n\t\t\t\tDEBUG_LOG((\"MTP Error: could not find request %1\").arg(requestId));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\trequest = it->second;\n\t\t}\n\t\tconst auto session = getSession(qAbs(dcWithShift));\n\t\tsession->sendPrepared(request);\n\t}\n\n\tif (!_delayedRequests.empty()) {\n\t\t_checkDelayedTimer.callOnce(_delayedRequests.front().second - now);\n\t}\n}\n\nvoid Instance::Private::sendRequest(\n\t\tmtpRequestId requestId,\n\t\tSerializedRequest &&request,\n\t\tResponseHandler &&callbacks,\n\t\tShiftedDcId shiftedDcId,\n\t\tcrl::time msCanWait,\n\t\tbool needsLayer,\n\t\tmtpRequestId afterRequestId) {\n\tconst auto session = getSession(shiftedDcId);\n\n\trequest->requestId = requestId;\n\tstoreRequest(requestId, request, std::move(callbacks));\n\n\tconst auto toMainDc = (shiftedDcId == 0);\n\tconst auto realShiftedDcId = session->getDcWithShift();\n\tconst auto signedDcId = toMainDc ? -realShiftedDcId : realShiftedDcId;\n\tregisterRequest(requestId, signedDcId);\n\n\trequest->lastSentTime = crl::now();\n\trequest->needsLayer = needsLayer;\n\n\tif (afterRequestId) {\n\t\trequest->after = getRequest(afterRequestId);\n\n\t\tif (request->after) {\n\t\t\t// Check if this after request is waiting in _dependentRequests.\n\t\t\t// This happens if it was after some other request and failed\n\t\t\t// to wait for it, but that other request is still processed.\n\t\t\tQMutexLocker locker(&_dependentRequestsLock);\n\t\t\tconst auto i = _dependentRequests.find(afterRequestId);\n\t\t\tif (i != end(_dependentRequests)) {\n\t\t\t\t_dependentRequests.emplace(requestId, afterRequestId);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tsession->sendPrepared(request, msCanWait);\n}\n\nvoid Instance::Private::registerRequest(\n\t\tmtpRequestId requestId,\n\t\tShiftedDcId shiftedDcId) {\n\tQMutexLocker locker(&_requestByDcLock);\n\t_requestsByDc[requestId] = shiftedDcId;\n}\n\nvoid Instance::Private::unregisterRequest(mtpRequestId requestId) {\n\tDEBUG_LOG((\"MTP Info: unregistering request %1.\").arg(requestId));\n\n\t_requestsDelays.erase(requestId);\n\n\t{\n\t\tQWriteLocker locker(&_requestMapLock);\n\t\t_requestMap.erase(requestId);\n\t}\n\t{\n\t\tQMutexLocker locker(&_requestByDcLock);\n\t\t_requestsByDc.erase(requestId);\n\t}\n\t{\n\t\tauto toRemove = base::flat_set<mtpRequestId>();\n\t\tauto toResend = base::flat_set<mtpRequestId>();\n\n\t\ttoRemove.emplace(requestId);\n\n\t\tQMutexLocker locker(&_dependentRequestsLock);\n\n\t\tauto handling = 0;\n\t\tdo {\n\t\t\thandling = toResend.size();\n\t\t\tfor (const auto &[resendingId, afterId] : _dependentRequests) {\n\t\t\t\tif (toRemove.contains(afterId)) {\n\t\t\t\t\ttoRemove.emplace(resendingId);\n\t\t\t\t\ttoResend.emplace(resendingId);\n\t\t\t\t}\n\t\t\t}\n\t\t} while (handling != toResend.size());\n\n\t\tfor (const auto removingId : toRemove) {\n\t\t\t_dependentRequests.remove(removingId);\n\t\t}\n\t\tlocker.unlock();\n\n\t\tfor (const auto resendingId : toResend) {\n\t\t\tif (const auto shiftedDcId = queryRequestByDc(resendingId)) {\n\t\t\t\tSerializedRequest request;\n\t\t\t\t{\n\t\t\t\t\tQReadLocker locker(&_requestMapLock);\n\t\t\t\t\tauto it = _requestMap.find(resendingId);\n\t\t\t\t\tif (it == _requestMap.cend()) {\n\t\t\t\t\t\tLOG((\"MTP Error: could not find dependent request %1\").arg(resendingId));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\trequest = it->second;\n\t\t\t\t}\n\t\t\t\tgetSession(qAbs(*shiftedDcId))->sendPrepared(request);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Instance::Private::storeRequest(\n\t\tmtpRequestId requestId,\n\t\tconst SerializedRequest &request,\n\t\tResponseHandler &&callbacks) {\n\tif (callbacks.done || callbacks.fail) {\n\t\tQMutexLocker locker(&_parserMapLock);\n\t\t_parserMap.emplace(requestId, std::move(callbacks));\n\t}\n\t{\n\t\tQWriteLocker locker(&_requestMapLock);\n\t\t_requestMap.emplace(requestId, request);\n\t}\n}\n\nSerializedRequest Instance::Private::getRequest(mtpRequestId requestId) {\n\tauto result = SerializedRequest();\n\t{\n\t\tQReadLocker locker(&_requestMapLock);\n\t\tauto it = _requestMap.find(requestId);\n\t\tif (it != _requestMap.cend()) {\n\t\t\tresult = it->second;\n\t\t}\n\t}\n\treturn result;\n}\n\nbool Instance::Private::hasCallback(mtpRequestId requestId) const {\n\tQMutexLocker locker(&_parserMapLock);\n\tauto it = _parserMap.find(requestId);\n\treturn (it != _parserMap.cend());\n}\n\nvoid Instance::Private::processCallback(const Response &response) {\n\tconst auto requestId = response.requestId;\n\tResponseHandler handler;\n\t{\n\t\tQMutexLocker locker(&_parserMapLock);\n\t\tauto it = _parserMap.find(requestId);\n\t\tif (it != _parserMap.cend()) {\n\t\t\thandler = std::move(it->second);\n\t\t\t_parserMap.erase(it);\n\n\t\t\tDEBUG_LOG((\"RPC Info: found parser for request %1, trying to parse response...\").arg(requestId));\n\t\t}\n\t}\n\tif (handler.done || handler.fail) {\n\t\tconst auto handleError = [&](const Error &error) {\n\t\t\tDEBUG_LOG((\"RPC Info: \"\n\t\t\t\t\"error received, code %1, type %2, description: %3\").arg(\n\t\t\t\t\tQString::number(error.code()),\n\t\t\t\t\terror.type(),\n\t\t\t\t\terror.description()));\n\t\t\tconst auto guard = QPointer<Instance>(_instance);\n\t\t\tif (rpcErrorOccured(response, handler, error) && guard) {\n\t\t\t\tunregisterRequest(requestId);\n\t\t\t} else if (guard) {\n\t\t\t\tQMutexLocker locker(&_parserMapLock);\n\t\t\t\t_parserMap.emplace(requestId, std::move(handler));\n\t\t\t}\n\t\t};\n\n\t\tauto from = response.reply.constData();\n\t\tif (response.reply.isEmpty()) {\n\t\t\thandleError(Error::Local(\n\t\t\t\t\"RESPONSE_PARSE_FAILED\",\n\t\t\t\t\"Empty response.\"));\n\t\t} else if (*from == mtpc_rpc_error) {\n\t\t\tauto error = MTPRpcError();\n\t\t\thandleError(\n\t\t\t\tError(error.read(from, from + response.reply.size())\n\t\t\t\t\t? error\n\t\t\t\t\t: Error::MTPLocal(\n\t\t\t\t\t\t\"RESPONSE_PARSE_FAILED\",\n\t\t\t\t\t\t\"Error parse failed.\")));\n\t\t} else {\n\t\t\tconst auto guard = QPointer<Instance>(_instance);\n\t\t\tif (handler.done && !handler.done(response) && guard) {\n\t\t\t\thandleError(Error::Local(\n\t\t\t\t\t\"RESPONSE_PARSE_FAILED\",\n\t\t\t\t\t\"Response parse failed.\"));\n\t\t\t}\n\t\t\tif (guard) {\n\t\t\t\tunregisterRequest(requestId);\n\t\t\t}\n\t\t}\n\t} else {\n\t\tDEBUG_LOG((\"RPC Info: parser not found for %1\").arg(requestId));\n\t\tunregisterRequest(requestId);\n\t}\n}\n\nvoid Instance::Private::processUpdate(const Response &message) {\n\tif (_updatesHandler) {\n\t\t_updatesHandler(message);\n\t}\n}\n\nvoid Instance::Private::onStateChange(ShiftedDcId dcWithShift, int32 state) {\n\tif (_stateChangedHandler) {\n\t\t_stateChangedHandler(dcWithShift, state);\n\t}\n}\n\nvoid Instance::Private::onSessionReset(ShiftedDcId dcWithShift) {\n\tif (_sessionResetHandler) {\n\t\t_sessionResetHandler(dcWithShift);\n\t}\n}\n\nbool Instance::Private::rpcErrorOccured(\n\t\tconst Response &response,\n\t\tconst FailHandler &onFail,\n\t\tconst Error &error) { // return true if need to clean request data\n\tif (IsDefaultHandledError(error)) {\n\t\tconst auto guard = QPointer<Instance>(_instance);\n\t\tif (onFail && onFail(error, response)) {\n\t\t\treturn true;\n\t\t} else if (!guard) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tif (onErrorDefault(error, response)) {\n\t\treturn false;\n\t}\n\tLOG((\"RPC Error: request %1 got fail with code %2, error %3%4\").arg(\n\t\tQString::number(response.requestId),\n\t\tQString::number(error.code()),\n\t\terror.type(),\n\t\terror.description().isEmpty()\n\t\t\t? QString()\n\t\t\t: QString(\": %1\").arg(error.description())));\n\tif (onFail) {\n\t\tconst auto guard = QPointer<Instance>(_instance);\n\t\tonFail(error, response);\n\t\tif (!guard) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid Instance::Private::importDone(\n\t\tconst MTPauth_Authorization &result,\n\t\tconst Response &response) {\n\tconst auto shiftedDcId = queryRequestByDc(response.requestId);\n\tif (!shiftedDcId) {\n\t\tLOG((\"MTP Error: \"\n\t\t\t\"auth import request not found in requestsByDC, requestId: %1\"\n\t\t\t).arg(response.requestId));\n\t\t//\n\t\t// Don't log out on export/import problems, perhaps this is a server side error.\n\t\t//\n\t\t//const auto error = Error::Local(\n\t\t//\t\"AUTH_IMPORT_FAIL\",\n\t\t//\tQString(\"did not find import request in requestsByDC, \"\n\t\t//\t\t\"request %1\").arg(requestId));\n\t\t//if (_globalFailHandler && hasAuthorization()) {\n\t\t//\t_globalFailHandler(error, response); // auth failed in main dc\n\t\t//}\n\t\treturn;\n\t}\n\tauto newdc = BareDcId(*shiftedDcId);\n\n\tDEBUG_LOG((\"MTP Info: auth import to dc %1 succeeded\").arg(newdc));\n\n\tauto &waiters = _authWaiters[newdc];\n\tif (waiters.size()) {\n\t\tQReadLocker locker(&_requestMapLock);\n\t\tfor (auto waitedRequestId : waiters) {\n\t\t\tauto it = _requestMap.find(waitedRequestId);\n\t\t\tif (it == _requestMap.cend()) {\n\t\t\t\tLOG((\"MTP Error: could not find request %1 for resending\").arg(waitedRequestId));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto shiftedDcId = changeRequestByDc(waitedRequestId, newdc);\n\t\t\tif (!shiftedDcId) {\n\t\t\t\tLOG((\"MTP Error: could not find request %1 by dc for resending\").arg(waitedRequestId));\n\t\t\t\tcontinue;\n\t\t\t} else if (*shiftedDcId < 0) {\n\t\t\t\t_instance->setMainDcId(newdc);\n\t\t\t}\n\t\t\tDEBUG_LOG((\"MTP Info: resending request %1 to dc %2 after import auth\").arg(waitedRequestId).arg(*shiftedDcId));\n\t\t\tconst auto session = getSession(*shiftedDcId);\n\t\t\tsession->sendPrepared(it->second);\n\t\t}\n\t\twaiters.clear();\n\t}\n}\n\nbool Instance::Private::importFail(\n\t\tconst Error &error,\n\t\tconst Response &response) {\n\tif (IsDefaultHandledError(error)) {\n\t\treturn false;\n\t}\n\n\t//\n\t// Don't log out on export/import problems, perhaps this is a server side error.\n\t//\n\t//if (_globalFailHandler && hasAuthorization()) {\n\t//\t_globalFailHandler(error, response); // auth import failed\n\t//}\n\treturn true;\n}\n\nvoid Instance::Private::exportDone(\n\t\tconst MTPauth_ExportedAuthorization &result,\n\t\tconst Response &response) {\n\tauto it = _authExportRequests.find(response.requestId);\n\tif (it == _authExportRequests.cend()) {\n\t\tLOG((\"MTP Error: \"\n\t\t\t\"auth export request target dcWithShift not found, requestId: %1\"\n\t\t\t).arg(response.requestId));\n\t\t//\n\t\t// Don't log out on export/import problems, perhaps this is a server side error.\n\t\t//\n\t\t//const auto error = Error::Local(\n\t\t//\t\"AUTH_IMPORT_FAIL\",\n\t\t//\tQString(\"did not find target dcWithShift, request %1\"\n\t\t//\t).arg(requestId));\n\t\t//if (_globalFailHandler && hasAuthorization()) {\n\t\t//\t_globalFailHandler(error, response); // auth failed in main dc\n\t\t//}\n\t\treturn;\n\t}\n\n\tauto &data = result.c_auth_exportedAuthorization();\n\t_instance->send(MTPauth_ImportAuthorization(\n\t\tdata.vid(),\n\t\tdata.vbytes()\n\t), [this](const Response &response) {\n\t\tauto result = MTPauth_Authorization();\n\t\tauto from = response.reply.constData();\n\t\tif (!result.read(from, from + response.reply.size())) {\n\t\t\treturn false;\n\t\t}\n\t\timportDone(result, response);\n\t\treturn true;\n\t}, [this](const Error &error, const Response &response) {\n\t\treturn importFail(error, response);\n\t}, it->second);\n\t_authExportRequests.erase(response.requestId);\n}\n\nbool Instance::Private::exportFail(\n\t\tconst Error &error,\n\t\tconst Response &response) {\n\tif (IsDefaultHandledError(error)) {\n\t\treturn false;\n\t}\n\n\tauto it = _authExportRequests.find(response.requestId);\n\tif (it != _authExportRequests.cend()) {\n\t\t_authWaiters[BareDcId(it->second)].clear();\n\t}\n\t//\n\t// Don't log out on export/import problems, perhaps this is a server side error.\n\t//\n\t//if (_globalFailHandler && hasAuthorization()) {\n\t//\t_globalFailHandler(error, response); // auth failed in main dc\n\t//}\n\treturn true;\n}\n\nbool Instance::Private::onErrorDefault(\n\t\tconst Error &error,\n\t\tconst Response &response) {\n\tconst auto requestId = response.requestId;\n\tconst auto &type = error.type();\n\tconst auto code = error.code();\n\tauto badGuestDc = (code == 400) && (type == u\"FILE_ID_INVALID\"_q);\n\tstatic const auto MigrateRegExp = QRegularExpression(\"^(FILE|PHONE|NETWORK|USER)_MIGRATE_(\\\\d+)$\");\n\tstatic const auto FloodWaitRegExp = QRegularExpression(\"^FLOOD_WAIT_(\\\\d+)$\");\n\tstatic const auto FloodPremiumWaitRegExp = QRegularExpression(\"^FLOOD_PREMIUM_WAIT_(\\\\d+)$\");\n\tstatic const auto SlowmodeWaitRegExp = QRegularExpression(\"^SLOWMODE_WAIT_(\\\\d+)$\");\n\tQRegularExpressionMatch m1, m2, m3;\n\tif ((m1 = MigrateRegExp.match(type)).hasMatch()) {\n\t\tif (!requestId) return false;\n\n\t\tauto dcWithShift = ShiftedDcId(0);\n\t\tauto newdcWithShift = ShiftedDcId(m1.captured(2).toInt());\n\t\tif (const auto shiftedDcId = queryRequestByDc(requestId)) {\n\t\t\tdcWithShift = *shiftedDcId;\n\t\t} else {\n\t\t\tLOG((\"MTP Error: could not find request %1 for migrating to %2\").arg(requestId).arg(newdcWithShift));\n\t\t}\n\t\tif (!dcWithShift || !newdcWithShift) return false;\n\n\t\tDEBUG_LOG((\"MTP Info: changing request %1 from dcWithShift%2 to dc%3\").arg(requestId).arg(dcWithShift).arg(newdcWithShift));\n\t\tif (dcWithShift < 0) { // newdc shift = 0\n\t\t\tif (false/* && hasAuthorization() && _authExportRequests.find(requestId) == _authExportRequests.cend()*/) {\n\t\t\t\t//\n\t\t\t\t// migrate not supported at this moment\n\t\t\t\t// this was not tested even once\n\t\t\t\t//\n\t\t\t\t//DEBUG_LOG((\"MTP Info: importing auth to dc %1\").arg(newdcWithShift));\n\t\t\t\t//auto &waiters(_authWaiters[newdcWithShift]);\n\t\t\t\t//if (waiters.empty()) {\n\t\t\t\t//\tauto exportRequestId = _instance->send(MTPauth_ExportAuthorization(\n\t\t\t\t//\t\tMTP_int(newdcWithShift)\n\t\t\t\t//\t), [this](const Response &response) {\n\t\t\t\t//\t\tauto result = MTPauth_ExportedAuthorization();\n\t\t\t\t//\t\tauto from = response.reply.constData();\n\t\t\t\t//\t\tif (!result.read(from, from + response.reply.size())) {\n\t\t\t\t//\t\t\treturn false;\n\t\t\t\t//\t\t}\n\t\t\t\t//\t\texportDone(result, response);\n\t\t\t\t//\t\treturn true;\n\t\t\t\t//\t}, [this](const Error &error, const Response &response) {\n\t\t\t\t//\t\treturn exportFail(error, response);\n\t\t\t\t//\t});\n\t\t\t\t//\t_authExportRequests.emplace(exportRequestId, newdcWithShift);\n\t\t\t\t//}\n\t\t\t\t//waiters.push_back(requestId);\n\t\t\t\t//return true;\n\t\t\t} else {\n\t\t\t\t_instance->setMainDcId(newdcWithShift);\n\t\t\t}\n\t\t} else {\n\t\t\tnewdcWithShift = ShiftDcId(newdcWithShift, GetDcIdShift(dcWithShift));\n\t\t}\n\n\t\tauto request = SerializedRequest();\n\t\t{\n\t\t\tQReadLocker locker(&_requestMapLock);\n\t\t\tauto it = _requestMap.find(requestId);\n\t\t\tif (it == _requestMap.cend()) {\n\t\t\t\tLOG((\"MTP Error: could not find request %1\").arg(requestId));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\trequest = it->second;\n\t\t}\n\t\tconst auto session = getSession(newdcWithShift);\n\t\tregisterRequest(\n\t\t\trequestId,\n\t\t\t(dcWithShift < 0) ? -newdcWithShift : newdcWithShift);\n\t\tsession->sendPrepared(request);\n\t\treturn true;\n\t} else if (type == u\"MSG_WAIT_TIMEOUT\"_q || type == u\"MSG_WAIT_FAILED\"_q) {\n\t\tSerializedRequest request;\n\t\t{\n\t\t\tQReadLocker locker(&_requestMapLock);\n\t\t\tauto it = _requestMap.find(requestId);\n\t\t\tif (it == _requestMap.cend()) {\n\t\t\t\tLOG((\"MTP Error: could not find MSG_WAIT_* request %1\").arg(requestId));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\trequest = it->second;\n\t\t}\n\t\tif (!request->after) {\n\t\t\tLOG((\"MTP Error: MSG_WAIT_* for not dependent request %1\").arg(requestId));\n\t\t\treturn false;\n\t\t}\n\t\tauto dcWithShift = ShiftedDcId(0);\n\t\tif (const auto shiftedDcId = queryRequestByDc(requestId)) {\n\t\t\tdcWithShift = *shiftedDcId;\n\t\t\tif (const auto afterDcId = queryRequestByDc(request->after->requestId)) {\n\t\t\t\tif (*shiftedDcId != *afterDcId) {\n\t\t\t\t\trequest->after = SerializedRequest();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trequest->after = SerializedRequest();\n\t\t\t}\n\t\t} else {\n\t\t\tLOG((\"MTP Error: could not find MSG_WAIT_* request %1 by dc\").arg(requestId));\n\t\t}\n\t\tif (!dcWithShift) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (!request->after) {\n\t\t\tgetSession(qAbs(dcWithShift))->sendPrepared(request);\n\t\t} else {\n\t\t\tQMutexLocker locker(&_dependentRequestsLock);\n\t\t\t_dependentRequests.emplace(requestId, request->after->requestId);\n\t\t}\n\t\treturn true;\n\t} else if (code < 0\n\t\t|| code >= 500\n\t\t|| (m1 = FloodWaitRegExp.match(type)).hasMatch()\n\t\t|| (m2 = FloodPremiumWaitRegExp.match(type)).hasMatch()\n\t\t|| ((m3 = SlowmodeWaitRegExp.match(type)).hasMatch()\n\t\t\t&& m3.captured(1).toInt() < 3)) {\n\t\tif (!requestId) {\n\t\t\treturn false;\n\t\t}\n\n\t\tauto secs = 1;\n\t\tauto nonPremiumDelay = false;\n\t\tif (code < 0 || code >= 500) {\n\t\t\tconst auto it = _requestsDelays.find(requestId);\n\t\t\tif (it != _requestsDelays.cend()) {\n\t\t\t\tsecs = (it->second > 60) ? it->second : (it->second *= 2);\n\t\t\t} else {\n\t\t\t\t_requestsDelays.emplace(requestId, secs);\n\t\t\t}\n\t\t} else if (m1.hasMatch()) {\n\t\t\tsecs = m1.captured(1).toInt();\n//\t\t\tif (secs >= 60) return false;\n\t\t} else if (m2.hasMatch()) {\n\t\t\tsecs = m2.captured(1).toInt();\n\t\t\tnonPremiumDelay = true;\n\t\t} else if (m3.hasMatch()) {\n\t\t\tsecs = m3.captured(1).toInt();\n\t\t}\n\t\tauto sendAt = crl::now() + secs * 1000 + 10;\n\t\tauto it = _delayedRequests.begin(), e = _delayedRequests.end();\n\t\tfor (; it != e; ++it) {\n\t\t\tif (it->first == requestId) {\n\t\t\t\treturn true;\n\t\t\t} else if (it->second > sendAt) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t_delayedRequests.insert(it, std::make_pair(requestId, sendAt));\n\n\t\tcheckDelayedRequests();\n\n\t\tif (nonPremiumDelay) {\n\t\t\t_nonPremiumDelayedRequests.fire_copy(requestId);\n\t\t}\n\n\t\treturn true;\n\t} else if ((code == 401 && type != u\"AUTH_KEY_PERM_EMPTY\"_q)\n\t\t|| (badGuestDc && _badGuestDcRequests.find(requestId) == _badGuestDcRequests.cend())) {\n\t\tauto dcWithShift = ShiftedDcId(0);\n\t\tif (const auto shiftedDcId = queryRequestByDc(requestId)) {\n\t\t\tdcWithShift = *shiftedDcId;\n\t\t} else {\n\t\t\tLOG((\"MTP Error: unauthorized request without dc info, requestId %1\").arg(requestId));\n\t\t}\n\t\tauto newdc = BareDcId(qAbs(dcWithShift));\n\t\tif (!newdc || !hasMainDcId() || newdc == mainDcId()) {\n\t\t\tif (!badGuestDc && _globalFailHandler) {\n\t\t\t\t_globalFailHandler(error, response); // auth failed in main dc\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\tDEBUG_LOG((\"MTP Info: importing auth to dcWithShift %1\"\n\t\t\t).arg(dcWithShift));\n\t\tauto &waiters(_authWaiters[newdc]);\n\t\tif (!waiters.size()) {\n\t\t\tauto exportRequestId = _instance->send(MTPauth_ExportAuthorization(\n\t\t\t\tMTP_int(newdc)\n\t\t\t), [this](const Response &response) {\n\t\t\t\tauto result = MTPauth_ExportedAuthorization();\n\t\t\t\tauto from = response.reply.constData();\n\t\t\t\tif (!result.read(from, from + response.reply.size())) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\texportDone(result, response);\n\t\t\t\treturn true;\n\t\t\t}, [this](const Error &error, const Response &response) {\n\t\t\t\treturn exportFail(error, response);\n\t\t\t});\n\t\t\t_authExportRequests.emplace(exportRequestId, abs(dcWithShift));\n\t\t}\n\t\twaiters.push_back(requestId);\n\t\tif (badGuestDc) _badGuestDcRequests.insert(requestId);\n\t\treturn true;\n\t} else if (type == u\"CONNECTION_NOT_INITED\"_q\n\t\t|| type == u\"CONNECTION_LAYER_INVALID\"_q) {\n\t\tSerializedRequest request;\n\t\t{\n\t\t\tQReadLocker locker(&_requestMapLock);\n\t\t\tauto it = _requestMap.find(requestId);\n\t\t\tif (it == _requestMap.cend()) {\n\t\t\t\tLOG((\"MTP Error: could not find request %1\").arg(requestId));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\trequest = it->second;\n\t\t}\n\t\tauto dcWithShift = ShiftedDcId(0);\n\t\tif (const auto shiftedDcId = queryRequestByDc(requestId)) {\n\t\t\tdcWithShift = *shiftedDcId;\n\t\t} else {\n\t\t\tLOG((\"MTP Error: could not find request %1 for resending with init connection\").arg(requestId));\n\t\t}\n\t\tif (!dcWithShift) return false;\n\n\t\tconst auto session = getSession(qAbs(dcWithShift));\n\t\trequest->needsLayer = true;\n\t\tsession->setConnectionNotInited();\n\t\tsession->sendPrepared(request);\n\t\treturn true;\n\t} else if (type == u\"CONNECTION_LANG_CODE_INVALID\"_q) {\n\t\tLang::CurrentCloudManager().resetToDefault();\n\t} else if (type == u\"FROZEN_METHOD_INVALID\"_q) {\n\t\t_frozenErrorReceived.fire({});\n\t}\n\tif (badGuestDc) _badGuestDcRequests.erase(requestId);\n\treturn false;\n}\n\nnot_null<Session*> Instance::Private::getSession(\n\t\tShiftedDcId shiftedDcId) {\n\tif (!shiftedDcId) {\n\t\tAssert(_mainSession != nullptr);\n\t\treturn _mainSession;\n\t} else if (!BareDcId(shiftedDcId)) {\n\t\tAssert(_mainSession != nullptr);\n\t\tshiftedDcId += BareDcId(_mainSession->getDcWithShift());\n\t}\n\n\tif (const auto session = findSession(shiftedDcId)) {\n\t\treturn session;\n\t}\n\treturn startSession(shiftedDcId);\n}\n\nrpl::lifetime &Instance::Private::lifetime() {\n\treturn _lifetime;\n}\n\nSession *Instance::Private::findSession(ShiftedDcId shiftedDcId) {\n\tconst auto i = _sessions.find(shiftedDcId);\n\treturn (i != _sessions.end()) ? i->second.get() : nullptr;\n}\n\nnot_null<Session*> Instance::Private::startSession(ShiftedDcId shiftedDcId) {\n\tExpects(BareDcId(shiftedDcId) != 0);\n\n\tconst auto dc = getDcById(shiftedDcId);\n\tconst auto thread = getThreadForDc(shiftedDcId);\n\tconst auto result = _sessions.emplace(\n\t\tshiftedDcId,\n\t\tstd::make_unique<Session>(_instance, thread, shiftedDcId, dc)\n\t).first->second.get();\n\tif (isKeysDestroyer()) {\n\t\tscheduleKeyDestroy(shiftedDcId);\n\t}\n\n\treturn result;\n}\n\nvoid Instance::Private::scheduleSessionDestroy(ShiftedDcId shiftedDcId) {\n\tconst auto i = _sessions.find(shiftedDcId);\n\tif (i == _sessions.cend()) {\n\t\treturn;\n\t}\n\ti->second->kill();\n\t_sessionsToDestroy.push_back(std::move(i->second));\n\t_sessions.erase(i);\n\tInvokeQueued(_instance, [=] {\n\t\t_sessionsToDestroy.clear();\n\t});\n}\n\n\nnot_null<QThread*> Instance::Private::getThreadForDc(\n\t\tShiftedDcId shiftedDcId) {\n\tstatic const auto EnsureStarted = [](\n\t\t\tstd::unique_ptr<QThread> &thread,\n\t\t\tauto name) {\n\t\tif (!thread) {\n\t\t\tthread = std::make_unique<QThread>();\n\t\t\tthread->setObjectName(name());\n\t\t\tthread->start();\n\t\t}\n\t\treturn thread.get();\n\t};\n\tstatic const auto FindOne = [](\n\t\t\tstd::vector<std::unique_ptr<QThread>> &threads,\n\t\t\tconst char *prefix,\n\t\t\tint index,\n\t\t\tbool shift) {\n\t\tExpects(!threads.empty());\n\t\tExpects(!(threads.size() % 2));\n\n\t\tconst auto count = int(threads.size());\n\t\tindex %= count;\n\t\tif (index >= count / 2) {\n\t\t\tindex = (count - 1) - (index - count / 2);\n\t\t}\n\t\tif (shift) {\n\t\t\tindex = (index + count / 2) % count;\n\t\t}\n\t\treturn EnsureStarted(threads[index], [=] {\n\t\t\treturn QString(\"MTP %1 Session (%2)\").arg(prefix).arg(index);\n\t\t});\n\t};\n\tif (shiftedDcId == BareDcId(shiftedDcId)) {\n\t\treturn EnsureStarted(_mainSessionThread, [] {\n\t\t\treturn QString(\"MTP Main Session\");\n\t\t});\n\t} else if (isDownloadDcId(shiftedDcId)) {\n\t\tconst auto index = GetDcIdShift(shiftedDcId) - kBaseDownloadDcShift;\n\t\tconst auto composed = index + BareDcId(shiftedDcId);\n\t\treturn FindOne(_fileSessionThreads, \"Download\", composed, false);\n\t} else if (isUploadDcId(shiftedDcId)) {\n\t\tconst auto index = GetDcIdShift(shiftedDcId) - kBaseUploadDcShift;\n\t\tconst auto composed = index + BareDcId(shiftedDcId);\n\t\treturn FindOne(_fileSessionThreads, \"Upload\", composed, true);\n\t}\n\treturn EnsureStarted(_otherSessionsThread, [] {\n\t\treturn QString(\"MTP Other Session\");\n\t});\n}\n\nvoid Instance::Private::scheduleKeyDestroy(ShiftedDcId shiftedDcId) {\n\tExpects(isKeysDestroyer());\n\n\tif (dcOptions().dcType(shiftedDcId) == DcType::Cdn) {\n\t\tperformKeyDestroy(shiftedDcId);\n\t} else {\n\t\t_instance->send(MTPauth_LogOut(), [=](const Response &) {\n\t\t\tperformKeyDestroy(shiftedDcId);\n\t\t\treturn true;\n\t\t}, [=](const Error &error, const Response &) {\n\t\t\tif (IsDefaultHandledError(error)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tperformKeyDestroy(shiftedDcId);\n\t\t\treturn true;\n\t\t}, shiftedDcId);\n\t}\n}\n\nvoid Instance::Private::keyWasPossiblyDestroyed(ShiftedDcId shiftedDcId) {\n\tExpects(isKeysDestroyer());\n\n\tInvokeQueued(_instance, [=] {\n\t\tLOG((\"MTP Info: checkIfKeyWasDestroyed on destroying key %1, \"\n\t\t\t\"assuming it is destroyed.\").arg(shiftedDcId));\n\t\tcompletedKeyDestroy(shiftedDcId);\n\t});\n}\n\nvoid Instance::Private::performKeyDestroy(ShiftedDcId shiftedDcId) {\n\tExpects(isKeysDestroyer());\n\n\t_instance->send(MTPDestroy_auth_key(), [=](const Response &response) {\n\t\tauto result = MTPDestroyAuthKeyRes();\n\t\tauto from = response.reply.constData();\n\t\tif (!result.read(from, from + response.reply.size())) {\n\t\t\treturn false;\n\t\t}\n\t\tresult.match([&](const MTPDdestroy_auth_key_ok &) {\n\t\t\tLOG((\"MTP Info: key %1 destroyed.\").arg(shiftedDcId));\n\t\t}, [&](const MTPDdestroy_auth_key_fail &) {\n\t\t\tLOG((\"MTP Error: key %1 destruction fail, leave it for now.\"\n\t\t\t\t).arg(shiftedDcId));\n\t\t\tkillSession(shiftedDcId);\n\t\t}, [&](const MTPDdestroy_auth_key_none &) {\n\t\t\tLOG((\"MTP Info: key %1 already destroyed.\").arg(shiftedDcId));\n\t\t});\n\t\t_instance->keyWasPossiblyDestroyed(shiftedDcId);\n\t\treturn true;\n\t}, [=](const Error &error, const Response &response) {\n\t\tLOG((\"MTP Error: key %1 destruction resulted in error: %2\"\n\t\t\t).arg(shiftedDcId).arg(error.type()));\n\t\t_instance->keyWasPossiblyDestroyed(shiftedDcId);\n\t\treturn true;\n\t}, shiftedDcId);\n}\n\nvoid Instance::Private::completedKeyDestroy(ShiftedDcId shiftedDcId) {\n\tExpects(isKeysDestroyer());\n\n\tremoveDc(shiftedDcId);\n\t_keysForWrite.erase(shiftedDcId);\n\tkillSession(shiftedDcId);\n\tif (_dcenters.empty()) {\n\t\t_allKeysDestroyed.fire({});\n\t}\n}\n\nvoid Instance::Private::keyDestroyedOnServer(\n\t\tShiftedDcId shiftedDcId,\n\t\tuint64 keyId) {\n\tLOG((\"Destroying key for dc: %1\").arg(shiftedDcId));\n\tif (const auto dc = findDc(BareDcId(shiftedDcId))) {\n\t\tif (dc->destroyConfirmedForgottenKey(keyId)) {\n\t\t\tLOG((\"Key destroyed!\"));\n\t\t\tdcPersistentKeyChanged(BareDcId(shiftedDcId), nullptr);\n\t\t} else {\n\t\t\tLOG((\"Key already is different.\"));\n\t\t}\n\t}\n\trestart(shiftedDcId);\n}\n\nvoid Instance::Private::setUpdatesHandler(\n\t\tFn<void(const Response&)> handler) {\n\t_updatesHandler = std::move(handler);\n}\n\nvoid Instance::Private::setGlobalFailHandler(\n\t\tFn<void(const Error&, const Response&)> handler) {\n\t_globalFailHandler = std::move(handler);\n}\n\nvoid Instance::Private::setStateChangedHandler(\n\t\tFn<void(ShiftedDcId shiftedDcId, int32 state)> handler) {\n\t_stateChangedHandler = std::move(handler);\n}\n\nvoid Instance::Private::setSessionResetHandler(\n\t\tFn<void(ShiftedDcId shiftedDcId)> handler) {\n\t_sessionResetHandler = std::move(handler);\n}\n\nvoid Instance::Private::clearGlobalHandlers() {\n\tsetUpdatesHandler(nullptr);\n\tsetGlobalFailHandler(nullptr);\n\tsetStateChangedHandler(nullptr);\n\tsetSessionResetHandler(nullptr);\n}\n\nvoid Instance::Private::prepareToDestroy() {\n\t// It accesses Instance in destructor, so it should be destroyed first.\n\t_configLoader.reset();\n\n\trequestCancellingDiscard();\n\n\tfor (const auto &[shiftedDcId, session] : base::take(_sessions)) {\n\t\tsession->kill();\n\t}\n\t_mainSession = nullptr;\n\n\tauto threads = std::vector<std::unique_ptr<QThread>>();\n\tthreads.push_back(base::take(_mainSessionThread));\n\tthreads.push_back(base::take(_otherSessionsThread));\n\tfor (auto &thread : base::take(_fileSessionThreads)) {\n\t\tthreads.push_back(std::move(thread));\n\t}\n\tfor (const auto &thread : threads) {\n\t\tif (thread) {\n\t\t\tthread->quit();\n\t\t}\n\t}\n\tfor (const auto &thread : threads) {\n\t\tif (thread) {\n\t\t\tthread->wait();\n\t\t}\n\t}\n}\n\nInstance::Instance(Mode mode, Fields &&fields)\n: QObject()\n, _private(std::make_unique<Private>(this, mode, std::move(fields))) {\n\t_private->start();\n}\n\nvoid Instance::resolveProxyDomain(const QString &host) {\n\t_private->resolveProxyDomain(host);\n}\n\nvoid Instance::setGoodProxyDomain(const QString &host, const QString &ip) {\n\t_private->setGoodProxyDomain(host, ip);\n}\n\nvoid Instance::suggestMainDcId(DcId mainDcId) {\n\t_private->suggestMainDcId(mainDcId);\n}\n\nvoid Instance::setMainDcId(DcId mainDcId) {\n\t_private->setMainDcId(mainDcId);\n}\n\nDcId Instance::mainDcId() const {\n\treturn _private->mainDcId();\n}\n\nrpl::producer<DcId> Instance::mainDcIdValue() const {\n\treturn _private->mainDcIdValue();\n}\n\nQString Instance::systemLangCode() const {\n\treturn Lang::GetInstance().systemLangCode();\n}\n\nQString Instance::cloudLangCode() const {\n\treturn Lang::GetInstance().cloudLangCode(Lang::Pack::Current);\n}\n\nQString Instance::langPackName() const {\n\treturn Lang::GetInstance().langPackName();\n}\n\nrpl::producer<> Instance::writeKeysRequests() const {\n\treturn _private->writeKeysRequests();\n}\n\nrpl::producer<> Instance::allKeysDestroyed() const {\n\treturn _private->allKeysDestroyed();\n}\n\nvoid Instance::requestConfig() {\n\t_private->requestConfig();\n}\n\nvoid Instance::setUserPhone(const QString &phone) {\n\t_private->setUserPhone(phone);\n}\n\nvoid Instance::badConfigurationError() {\n\t_private->badConfigurationError();\n}\n\nvoid Instance::syncHttpUnixtime() {\n\t_private->syncHttpUnixtime();\n}\n\nvoid Instance::restartedByTimeout(ShiftedDcId shiftedDcId) {\n\t_private->restartedByTimeout(shiftedDcId);\n}\n\nrpl::producer<ShiftedDcId> Instance::restartsByTimeout() const {\n\treturn _private->restartsByTimeout();\n}\n\nrpl::producer<mtpRequestId> Instance::nonPremiumDelayedRequests() const {\n\treturn _private->nonPremiumDelayedRequests();\n}\n\nrpl::producer<> Instance::frozenErrorReceived() const {\n\treturn _private->frozenErrorReceived();\n}\n\nvoid Instance::requestConfigIfOld() {\n\t_private->requestConfigIfOld();\n}\n\nvoid Instance::requestCDNConfig() {\n\t_private->requestCDNConfig();\n}\n\nvoid Instance::restart() {\n\t_private->restart();\n}\n\nvoid Instance::restart(ShiftedDcId shiftedDcId) {\n\t_private->restart(shiftedDcId);\n}\n\nint32 Instance::dcstate(ShiftedDcId shiftedDcId) {\n\treturn _private->dcstate(shiftedDcId);\n}\n\nQString Instance::dctransport(ShiftedDcId shiftedDcId) {\n\treturn _private->dctransport(shiftedDcId);\n}\n\nvoid Instance::ping() {\n\t_private->ping();\n}\n\nvoid Instance::cancel(mtpRequestId requestId) {\n\t_private->cancel(requestId);\n}\n\nint32 Instance::state(mtpRequestId requestId) { // < 0 means waiting for such count of ms\n\treturn _private->state(requestId);\n}\n\nvoid Instance::killSession(ShiftedDcId shiftedDcId) {\n\t_private->killSession(shiftedDcId);\n}\n\nvoid Instance::stopSession(ShiftedDcId shiftedDcId) {\n\t_private->stopSession(shiftedDcId);\n}\n\nvoid Instance::reInitConnection(DcId dcId) {\n\t_private->reInitConnection(dcId);\n}\n\nvoid Instance::logout(Fn<void()> done) {\n\t_private->logout(std::move(done));\n}\n\nvoid Instance::dcPersistentKeyChanged(\n\t\tDcId dcId,\n\t\tconst AuthKeyPtr &persistentKey) {\n\t_private->dcPersistentKeyChanged(dcId, persistentKey);\n}\n\nvoid Instance::dcTemporaryKeyChanged(DcId dcId) {\n\t_private->dcTemporaryKeyChanged(dcId);\n}\n\nrpl::producer<DcId> Instance::dcTemporaryKeyChanged() const {\n\treturn _private->dcTemporaryKeyChanged();\n}\n\nAuthKeysList Instance::getKeysForWrite() const {\n\treturn _private->getKeysForWrite();\n}\n\nvoid Instance::addKeysForDestroy(AuthKeysList &&keys) {\n\t_private->addKeysForDestroy(std::move(keys));\n}\n\nConfig &Instance::config() const {\n\treturn _private->config();\n}\n\nconst ConfigFields &Instance::configValues() const {\n\treturn _private->configValues();\n}\n\nDcOptions &Instance::dcOptions() const {\n\treturn _private->dcOptions();\n}\n\nEnvironment Instance::environment() const {\n\treturn _private->environment();\n}\n\nbool Instance::isTestMode() const {\n\treturn _private->isTestMode();\n}\n\nQString Instance::deviceModel() const {\n\treturn _private->deviceModel();\n}\n\nQString Instance::systemVersion() const {\n\treturn _private->systemVersion();\n}\n\nvoid Instance::setUpdatesHandler(Fn<void(const Response&)> handler) {\n\t_private->setUpdatesHandler(std::move(handler));\n}\n\nvoid Instance::setGlobalFailHandler(\n\t\tFn<void(const Error&, const Response&)> handler) {\n\t_private->setGlobalFailHandler(std::move(handler));\n}\n\nvoid Instance::setStateChangedHandler(\n\t\tFn<void(ShiftedDcId shiftedDcId, int32 state)> handler) {\n\t_private->setStateChangedHandler(std::move(handler));\n}\n\nvoid Instance::setSessionResetHandler(\n\t\tFn<void(ShiftedDcId shiftedDcId)> handler) {\n\t_private->setSessionResetHandler(std::move(handler));\n}\n\nvoid Instance::clearGlobalHandlers() {\n\t_private->clearGlobalHandlers();\n}\n\nvoid Instance::onStateChange(ShiftedDcId shiftedDcId, int32 state) {\n\t_private->onStateChange(shiftedDcId, state);\n}\n\nvoid Instance::onSessionReset(ShiftedDcId shiftedDcId) {\n\t_private->onSessionReset(shiftedDcId);\n}\n\nbool Instance::hasCallback(mtpRequestId requestId) const {\n\treturn _private->hasCallback(requestId);\n}\n\nvoid Instance::processCallback(const Response &response) {\n\t_private->processCallback(response);\n}\n\nvoid Instance::processUpdate(const Response &message) {\n\t_private->processUpdate(message);\n}\n\nbool Instance::rpcErrorOccured(\n\t\tconst Response &response,\n\t\tconst FailHandler &onFail,\n\t\tconst Error &error) {\n\treturn _private->rpcErrorOccured(response, onFail, error);\n}\n\nbool Instance::isKeysDestroyer() const {\n\treturn _private->isKeysDestroyer();\n}\n\nvoid Instance::keyWasPossiblyDestroyed(ShiftedDcId shiftedDcId) {\n\t_private->keyWasPossiblyDestroyed(shiftedDcId);\n}\n\nvoid Instance::keyDestroyedOnServer(ShiftedDcId shiftedDcId, uint64 keyId) {\n\t_private->keyDestroyedOnServer(shiftedDcId, keyId);\n}\n\nvoid Instance::sendRequest(\n\t\tmtpRequestId requestId,\n\t\tSerializedRequest &&request,\n\t\tResponseHandler &&callbacks,\n\t\tShiftedDcId shiftedDcId,\n\t\tcrl::time msCanWait,\n\t\tbool needsLayer,\n\t\tmtpRequestId afterRequestId) {\n\treturn _private->sendRequest(\n\t\trequestId,\n\t\tstd::move(request),\n\t\tstd::move(callbacks),\n\t\tshiftedDcId,\n\t\tmsCanWait,\n\t\tneedsLayer,\n\t\tafterRequestId);\n}\n\nvoid Instance::sendAnything(ShiftedDcId shiftedDcId, crl::time msCanWait) {\n\t_private->getSession(shiftedDcId)->sendAnything(msCanWait);\n}\n\nrpl::lifetime &Instance::lifetime() {\n\treturn _private->lifetime();\n}\n\nInstance::~Instance() {\n\t_private->prepareToDestroy();\n}\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtp_instance.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/details/mtproto_serialized_request.h\"\n#include \"mtproto/mtproto_response.h\"\n\nnamespace MTP {\nnamespace details {\n\nclass Dcenter;\nclass Session;\n\n[[nodiscard]] int GetNextRequestId();\n\n} // namespace details\n\nclass DcOptions;\nclass Config;\nstruct ConfigFields;\nclass AuthKey;\nusing AuthKeyPtr = std::shared_ptr<AuthKey>;\nusing AuthKeysList = std::vector<AuthKeyPtr>;\nenum class Environment : uchar;\n\nclass Instance : public QObject {\n\tQ_OBJECT\n\npublic:\n\tstruct Fields {\n\t\tFields();\n\t\tFields(Fields &&other);\n\t\tFields &operator=(Fields &&other);\n\t\t~Fields();\n\n\t\tstatic constexpr auto kNoneMainDc = -1;\n\t\tstatic constexpr auto kNotSetMainDc = 0;\n\t\tstatic constexpr auto kDefaultMainDc = 2;\n\t\tstatic constexpr auto kTemporaryMainDc = 1000;\n\n\t\tstd::unique_ptr<Config> config;\n\t\tDcId mainDcId = kNotSetMainDc;\n\t\tAuthKeysList keys;\n\t\tQString deviceModel;\n\t\tQString systemVersion;\n\t};\n\n\tenum class Mode {\n\t\tNormal,\n\t\tKeysDestroyer,\n\t};\n\n\tInstance(Mode mode, Fields &&fields);\n\tInstance(const Instance &other) = delete;\n\tInstance &operator=(const Instance &other) = delete;\n\t~Instance();\n\n\tvoid resolveProxyDomain(const QString &host);\n\tvoid setGoodProxyDomain(const QString &host, const QString &ip);\n\tvoid suggestMainDcId(DcId mainDcId);\n\tvoid setMainDcId(DcId mainDcId);\n\t[[nodiscard]] DcId mainDcId() const;\n\t[[nodiscard]] rpl::producer<DcId> mainDcIdValue() const;\n\t[[nodiscard]] QString systemLangCode() const;\n\t[[nodiscard]] QString cloudLangCode() const;\n\t[[nodiscard]] QString langPackName() const;\n\n\t[[nodiscard]] rpl::producer<> writeKeysRequests() const;\n\t[[nodiscard]] rpl::producer<> allKeysDestroyed() const;\n\n\t// Thread-safe.\n\t[[nodiscard]] Config &config() const;\n\t[[nodiscard]] const ConfigFields &configValues() const;\n\t[[nodiscard]] DcOptions &dcOptions() const;\n\t[[nodiscard]] Environment environment() const;\n\t[[nodiscard]] bool isTestMode() const;\n\t[[nodiscard]] QString deviceModel() const;\n\t[[nodiscard]] QString systemVersion() const;\n\n\t// Main thread.\n\tvoid dcPersistentKeyChanged(DcId dcId, const AuthKeyPtr &persistentKey);\n\tvoid dcTemporaryKeyChanged(DcId dcId);\n\t[[nodiscard]] rpl::producer<DcId> dcTemporaryKeyChanged() const;\n\t[[nodiscard]] AuthKeysList getKeysForWrite() const;\n\tvoid addKeysForDestroy(AuthKeysList &&keys);\n\n\tvoid restart();\n\tvoid restart(ShiftedDcId shiftedDcId);\n\tint32 dcstate(ShiftedDcId shiftedDcId = 0);\n\tQString dctransport(ShiftedDcId shiftedDcId = 0);\n\tvoid ping();\n\tvoid cancel(mtpRequestId requestId);\n\tint32 state(mtpRequestId requestId); // < 0 means waiting for such count of ms\n\n\t// Main thread.\n\tvoid killSession(ShiftedDcId shiftedDcId);\n\tvoid stopSession(ShiftedDcId shiftedDcId);\n\tvoid reInitConnection(DcId dcId);\n\tvoid logout(Fn<void()> done);\n\n\tvoid setUpdatesHandler(Fn<void(const Response&)> handler);\n\tvoid setGlobalFailHandler(\n\t\tFn<void(const Error&, const Response&)> handler);\n\tvoid setStateChangedHandler(\n\t\tFn<void(ShiftedDcId shiftedDcId, int32 state)> handler);\n\tvoid setSessionResetHandler(Fn<void(ShiftedDcId shiftedDcId)> handler);\n\tvoid clearGlobalHandlers();\n\n\tvoid onStateChange(ShiftedDcId shiftedDcId, int32 state);\n\tvoid onSessionReset(ShiftedDcId shiftedDcId);\n\n\t[[nodiscard]] bool hasCallback(mtpRequestId requestId) const;\n\tvoid processCallback(const Response &response);\n\tvoid processUpdate(const Response &message);\n\n\t// return true if need to clean request data\n\tbool rpcErrorOccured(\n\t\tconst Response &response,\n\t\tconst FailHandler &onFail,\n\t\tconst Error &err);\n\n\t// Thread-safe.\n\tbool isKeysDestroyer() const;\n\tvoid keyWasPossiblyDestroyed(ShiftedDcId shiftedDcId);\n\n\t// Main thread.\n\tvoid keyDestroyedOnServer(ShiftedDcId shiftedDcId, uint64 keyId);\n\n\tvoid requestConfig();\n\tvoid requestConfigIfOld();\n\tvoid requestCDNConfig();\n\tvoid setUserPhone(const QString &phone);\n\tvoid badConfigurationError();\n\n\tvoid restartedByTimeout(ShiftedDcId shiftedDcId);\n\t[[nodiscard]] rpl::producer<ShiftedDcId> restartsByTimeout() const;\n\n\t[[nodiscard]] auto nonPremiumDelayedRequests() const\n\t\t-> rpl::producer<mtpRequestId>;\n\t[[nodiscard]] rpl::producer<> frozenErrorReceived() const;\n\n\tvoid syncHttpUnixtime();\n\n\tvoid sendAnything(ShiftedDcId shiftedDcId = 0, crl::time msCanWait = 0);\n\n\ttemplate <typename Request>\n\tmtpRequestId send(\n\t\t\tconst Request &request,\n\t\t\tResponseHandler &&callbacks = {},\n\t\t\tShiftedDcId shiftedDcId = 0,\n\t\t\tcrl::time msCanWait = 0,\n\t\t\tmtpRequestId afterRequestId = 0,\n\t\t\tmtpRequestId overrideRequestId = 0) {\n\t\tconst auto requestId = overrideRequestId\n\t\t\t? overrideRequestId\n\t\t\t: details::GetNextRequestId();\n\t\tsendSerialized(\n\t\t\trequestId,\n\t\t\tdetails::SerializedRequest::Serialize(request),\n\t\t\tstd::move(callbacks),\n\t\t\tshiftedDcId,\n\t\t\tmsCanWait,\n\t\t\tafterRequestId);\n\t\treturn requestId;\n\t}\n\n\ttemplate <typename Request>\n\tmtpRequestId send(\n\t\t\tconst Request &request,\n\t\t\tDoneHandler &&onDone,\n\t\t\tFailHandler &&onFail = nullptr,\n\t\t\tShiftedDcId shiftedDcId = 0,\n\t\t\tcrl::time msCanWait = 0,\n\t\t\tmtpRequestId afterRequestId = 0,\n\t\t\tmtpRequestId overrideRequestId = 0) {\n\t\treturn send(\n\t\t\trequest,\n\t\t\tResponseHandler{ std::move(onDone), std::move(onFail) },\n\t\t\tshiftedDcId,\n\t\t\tmsCanWait,\n\t\t\tafterRequestId,\n\t\t\toverrideRequestId);\n\t}\n\n\ttemplate <typename Request>\n\tmtpRequestId sendProtocolMessage(\n\t\t\tShiftedDcId shiftedDcId,\n\t\t\tconst Request &request) {\n\t\tconst auto requestId = details::GetNextRequestId();\n\t\tsendRequest(\n\t\t\trequestId,\n\t\t\tdetails::SerializedRequest::Serialize(request),\n\t\t\t{},\n\t\t\tshiftedDcId,\n\t\t\t0,\n\t\t\tfalse,\n\t\t\t0);\n\t\treturn requestId;\n\t}\n\n\tvoid sendSerialized(\n\t\t\tmtpRequestId requestId,\n\t\t\tdetails::SerializedRequest &&request,\n\t\t\tResponseHandler &&callbacks,\n\t\t\tShiftedDcId shiftedDcId,\n\t\t\tcrl::time msCanWait,\n\t\t\tmtpRequestId afterRequestId) {\n\t\tconst auto needsLayer = true;\n\t\tsendRequest(\n\t\t\trequestId,\n\t\t\tstd::move(request),\n\t\t\tstd::move(callbacks),\n\t\t\tshiftedDcId,\n\t\t\tmsCanWait,\n\t\t\tneedsLayer,\n\t\t\tafterRequestId);\n\t}\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nQ_SIGNALS:\n\tvoid proxyDomainResolved(\n\t\tQString host,\n\t\tQStringList ips,\n\t\tqint64 expireAt);\n\nprivate:\n\tvoid sendRequest(\n\t\tmtpRequestId requestId,\n\t\tdetails::SerializedRequest &&request,\n\t\tResponseHandler &&callbacks,\n\t\tShiftedDcId shiftedDcId,\n\t\tcrl::time msCanWait,\n\t\tbool needsLayer,\n\t\tmtpRequestId afterRequestId);\n\n\tclass Private;\n\tconst std::unique_ptr<Private> _private;\n\n};\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtproto_auth_key.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/mtproto_auth_key.h\"\n\n#include \"base/openssl_help.h\"\n\n#include <QtCore/QDataStream>\n\nnamespace MTP {\n\nAuthKey::AuthKey(Type type, DcId dcId, const Data &data)\n: _type(type)\n, _dcId(dcId)\n, _key(data) {\n\tcountKeyId();\n\tif (type == Type::Generated || type == Type::Temporary) {\n\t\t_creationTime = crl::now();\n\t}\n}\n\nAuthKey::AuthKey(const Data &data) : _type(Type::Local), _key(data) {\n\tcountKeyId();\n}\n\nAuthKey::Type AuthKey::type() const {\n\treturn _type;\n}\n\nint AuthKey::dcId() const {\n\treturn _dcId;\n}\n\nAuthKey::KeyId AuthKey::keyId() const {\n\treturn _keyId;\n}\n\nvoid AuthKey::prepareAES_oldmtp(const MTPint128 &msgKey, MTPint256 &aesKey, MTPint256 &aesIV, bool send) const {\n\tuint32 x = send ? 0 : 8;\n\n\tbytes::array<20> sha1_a, sha1_b, sha1_c, sha1_d;\n\tbytes::array<16 + 32> data_a;\n\tmemcpy(data_a.data(), &msgKey, 16);\n\tmemcpy(data_a.data() + 16, _key.data() + x, 32);\n\topenssl::Sha1To(sha1_a, data_a);\n\n\tbytes::array<16 + 16 + 16> data_b;\n\tmemcpy(data_b.data(), _key.data() + 32 + x, 16);\n\tmemcpy(data_b.data() + 16, &msgKey, 16);\n\tmemcpy(data_b.data() + 32, _key.data() + 48 + x, 16);\n\topenssl::Sha1To(sha1_b, data_b);\n\n\tbytes::array<32 + 16> data_c;\n\tmemcpy(data_c.data(), _key.data() + 64 + x, 32);\n\tmemcpy(data_c.data() + 32, &msgKey, 16);\n\topenssl::Sha1To(sha1_c, data_c);\n\n\tbytes::array<16 + 32> data_d;\n\tmemcpy(data_d.data(), &msgKey, 16);\n\tmemcpy(data_d.data() + 16, _key.data() + 96 + x, 32);\n\topenssl::Sha1To(sha1_d, data_d);\n\n\tauto key = reinterpret_cast<bytes::type*>(&aesKey);\n\tauto iv = reinterpret_cast<bytes::type*>(&aesIV);\n\tmemcpy(key, sha1_a.data(), 8);\n\tmemcpy(key + 8, sha1_b.data() + 8, 12);\n\tmemcpy(key + 8 + 12, sha1_c.data() + 4, 12);\n\tmemcpy(iv, sha1_a.data() + 8, 12);\n\tmemcpy(iv + 12, sha1_b.data(), 8);\n\tmemcpy(iv + 12 + 8, sha1_c.data() + 16, 4);\n\tmemcpy(iv + 12 + 8 + 4, sha1_d.data(), 8);\n}\n\nvoid AuthKey::prepareAES(const MTPint128 &msgKey, MTPint256 &aesKey, MTPint256 &aesIV, bool send) const {\n\tuint32 x = send ? 0 : 8;\n\n\tbytes::array<32> sha256_a, sha256_b;\n\tbytes::array<16 + 36> data_a;\n\tmemcpy(data_a.data(), &msgKey, 16);\n\tmemcpy(data_a.data() + 16, _key.data() + x, 36);\n\topenssl::Sha256To(sha256_a, data_a);\n\n\tbytes::array<36 + 16> data_b;\n\tmemcpy(data_b.data(), _key.data() + 40 + x, 36);\n\tmemcpy(data_b.data() + 36, &msgKey, 16);\n\topenssl::Sha256To(sha256_b, data_b);\n\n\tauto key = reinterpret_cast<uchar*>(&aesKey);\n\tauto iv = reinterpret_cast<uchar*>(&aesIV);\n\tmemcpy(key, sha256_a.data(), 8);\n\tmemcpy(key + 8, sha256_b.data() + 8, 16);\n\tmemcpy(key + 8 + 16, sha256_a.data() + 24, 8);\n\tmemcpy(iv, sha256_b.data(), 8);\n\tmemcpy(iv + 8, sha256_a.data() + 8, 16);\n\tmemcpy(iv + 8 + 16, sha256_b.data() + 24, 8);\n}\n\nconst void *AuthKey::partForMsgKey(bool send) const {\n\treturn _key.data() + 88 + (send ? 0 : 8);\n}\n\nvoid AuthKey::write(QDataStream &to) const {\n\tto.writeRawData(reinterpret_cast<const char*>(_key.data()), _key.size());\n}\n\nbytes::const_span AuthKey::data() const {\n\treturn _key;\n}\n\nbool AuthKey::equals(const std::shared_ptr<AuthKey> &other) const {\n\treturn other ? (_key == other->_key) : false;\n}\n\ncrl::time AuthKey::creationTime() const {\n\treturn _creationTime;\n}\n\nTimeId AuthKey::expiresAt() const {\n\treturn _expiresAt;\n}\n\nvoid AuthKey::setExpiresAt(TimeId expiresAt) {\n\tExpects(_type == Type::Temporary);\n\n\t_expiresAt = expiresAt;\n}\n\nvoid AuthKey::FillData(Data &authKey, bytes::const_span computedAuthKey) {\n\tauto computedAuthKeySize = computedAuthKey.size();\n\tAssert(computedAuthKeySize <= kSize);\n\tauto authKeyBytes = gsl::make_span(authKey);\n\tif (computedAuthKeySize < kSize) {\n\t\tbytes::set_with_const(authKeyBytes.subspan(0, kSize - computedAuthKeySize), gsl::byte());\n\t\tbytes::copy(authKeyBytes.subspan(kSize - computedAuthKeySize), computedAuthKey);\n\t} else {\n\t\tbytes::copy(authKeyBytes, computedAuthKey);\n\t}\n}\n\nvoid AuthKey::countKeyId() {\n\tconst auto hash = openssl::Sha1(_key);\n\n\t// Lower 64 bits = 8 bytes of 20 byte SHA1 hash.\n\t_keyId = *reinterpret_cast<const KeyId*>(hash.data() + 12);\n}\n\nvoid aesIgeEncryptRaw(const void *src, void *dst, uint32 len, const void *key, const void *iv) {\n\tuchar aes_key[32], aes_iv[32];\n\tmemcpy(aes_key, key, 32);\n\tmemcpy(aes_iv, iv, 32);\n\n\tAES_KEY aes;\n\tAES_set_encrypt_key(aes_key, 256, &aes);\n\tAES_ige_encrypt(static_cast<const uchar*>(src), static_cast<uchar*>(dst), len, &aes, aes_iv, AES_ENCRYPT);\n}\n\nvoid aesIgeDecryptRaw(const void *src, void *dst, uint32 len, const void *key, const void *iv) {\n\tuchar aes_key[32], aes_iv[32];\n\tmemcpy(aes_key, key, 32);\n\tmemcpy(aes_iv, iv, 32);\n\n\tAES_KEY aes;\n\tAES_set_decrypt_key(aes_key, 256, &aes);\n\tAES_ige_encrypt(static_cast<const uchar*>(src), static_cast<uchar*>(dst), len, &aes, aes_iv, AES_DECRYPT);\n}\n\nvoid aesCtrEncrypt(bytes::span data, const void *key, CTRState *state) {\n\tAES_KEY aes;\n\tAES_set_encrypt_key(static_cast<const uchar*>(key), 256, &aes);\n\n\tstatic_assert(CTRState::IvecSize == AES_BLOCK_SIZE, \"Wrong size of ctr ivec!\");\n\tstatic_assert(CTRState::EcountSize == AES_BLOCK_SIZE, \"Wrong size of ctr ecount!\");\n\n\tCRYPTO_ctr128_encrypt(\n\t\treinterpret_cast<const uchar*>(data.data()),\n\t\treinterpret_cast<uchar*>(data.data()),\n\t\tdata.size(),\n\t\t&aes,\n\t\tstate->ivec,\n\t\tstate->ecount,\n\t\t&state->num,\n\t\t(block128_f)AES_encrypt);\n}\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtproto_auth_key.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/bytes.h\"\n#include <array>\n#include <memory>\n\nnamespace MTP {\n\nclass AuthKey {\npublic:\n\tstatic constexpr auto kSize = 256; // 2048 bits.\n\tusing Data = std::array<gsl::byte, kSize>;\n\tusing KeyId = uint64;\n\n\tenum class Type {\n\t\tGenerated,\n\t\tTemporary,\n\t\tReadFromFile,\n\t\tLocal,\n\t};\n\tAuthKey(Type type, DcId dcId, const Data &data);\n\texplicit AuthKey(const Data &data);\n\n\tAuthKey(const AuthKey &other) = delete;\n\tAuthKey &operator=(const AuthKey &other) = delete;\n\n\t[[nodiscard]] Type type() const;\n\t[[nodiscard]] int dcId() const;\n\t[[nodiscard]] KeyId keyId() const;\n\n\tvoid prepareAES_oldmtp(const MTPint128 &msgKey, MTPint256 &aesKey, MTPint256 &aesIV, bool send) const;\n\tvoid prepareAES(const MTPint128 &msgKey, MTPint256 &aesKey, MTPint256 &aesIV, bool send) const;\n\n\t[[nodiscard]] const void *partForMsgKey(bool send) const;\n\n\tvoid write(QDataStream &to) const;\n\t[[nodiscard]] bytes::const_span data() const;\n\t[[nodiscard]] bool equals(const std::shared_ptr<AuthKey> &other) const;\n\n\t[[nodiscard]] crl::time creationTime() const; // > 0 if known.\n\t[[nodiscard]] TimeId expiresAt() const;\n\tvoid setExpiresAt(TimeId expiresAt);\n\n\tstatic void FillData(Data &authKey, bytes::const_span computedAuthKey);\n\nprivate:\n\tvoid countKeyId();\n\n\tType _type = Type::Generated;\n\tDcId _dcId = 0;\n\tData _key = { { gsl::byte{} } };\n\tKeyId _keyId = 0;\n\tcrl::time _creationTime = 0;\n\tTimeId _expiresAt = 0;\n\n};\n\nusing AuthKeyPtr = std::shared_ptr<AuthKey>;\nusing AuthKeysList = std::vector<AuthKeyPtr>;\n\nvoid aesIgeEncryptRaw(const void *src, void *dst, uint32 len, const void *key, const void *iv);\nvoid aesIgeDecryptRaw(const void *src, void *dst, uint32 len, const void *key, const void *iv);\n\ninline void aesIgeEncrypt_oldmtp(const void *src, void *dst, uint32 len, const AuthKeyPtr &authKey, const MTPint128 &msgKey) {\n\tMTPint256 aesKey, aesIV;\n\tauthKey->prepareAES_oldmtp(msgKey, aesKey, aesIV, true);\n\n\treturn aesIgeEncryptRaw(src, dst, len, static_cast<const void*>(&aesKey), static_cast<const void*>(&aesIV));\n}\n\ninline void aesIgeEncrypt(const void *src, void *dst, uint32 len, const AuthKeyPtr &authKey, const MTPint128 &msgKey) {\n\tMTPint256 aesKey, aesIV;\n\tauthKey->prepareAES(msgKey, aesKey, aesIV, true);\n\n\treturn aesIgeEncryptRaw(src, dst, len, static_cast<const void*>(&aesKey), static_cast<const void*>(&aesIV));\n}\n\ninline void aesEncryptLocal(const void *src, void *dst, uint32 len, const AuthKeyPtr &authKey, const void *key128) {\n\tMTPint256 aesKey, aesIV;\n\tauthKey->prepareAES_oldmtp(*(const MTPint128*)key128, aesKey, aesIV, false);\n\n\treturn aesIgeEncryptRaw(src, dst, len, static_cast<const void*>(&aesKey), static_cast<const void*>(&aesIV));\n}\n\ninline void aesIgeDecrypt_oldmtp(const void *src, void *dst, uint32 len, const AuthKeyPtr &authKey, const MTPint128 &msgKey) {\n\tMTPint256 aesKey, aesIV;\n\tauthKey->prepareAES_oldmtp(msgKey, aesKey, aesIV, false);\n\n\treturn aesIgeDecryptRaw(src, dst, len, static_cast<const void*>(&aesKey), static_cast<const void*>(&aesIV));\n}\n\ninline void aesIgeDecrypt(const void *src, void *dst, uint32 len, const AuthKeyPtr &authKey, const MTPint128 &msgKey) {\n\tMTPint256 aesKey, aesIV;\n\tauthKey->prepareAES(msgKey, aesKey, aesIV, false);\n\n\treturn aesIgeDecryptRaw(src, dst, len, static_cast<const void*>(&aesKey), static_cast<const void*>(&aesIV));\n}\n\ninline void aesDecryptLocal(const void *src, void *dst, uint32 len, const AuthKeyPtr &authKey, const void *key128) {\n\tMTPint256 aesKey, aesIV;\n\tauthKey->prepareAES_oldmtp(*(const MTPint128*)key128, aesKey, aesIV, false);\n\n\treturn aesIgeDecryptRaw(src, dst, len, static_cast<const void*>(&aesKey), static_cast<const void*>(&aesIV));\n}\n\n// ctr used inplace, encrypt the data and leave it at the same place\nstruct CTRState {\n\tstatic constexpr int KeySize = 32;\n\tstatic constexpr int IvecSize = 16;\n\tstatic constexpr int EcountSize = 16;\n\n\tuchar ivec[IvecSize] = { 0 };\n\tuint32 num = 0;\n\tuchar ecount[EcountSize] = { 0 };\n};\nvoid aesCtrEncrypt(bytes::span data, const void *key, CTRState *state);\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtproto_concurrent_sender.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/mtproto_concurrent_sender.h\"\n\n#include \"mtproto/mtp_instance.h\"\n#include \"mtproto/mtproto_response.h\"\n#include \"mtproto/facade.h\"\n\nnamespace MTP {\n\nclass ConcurrentSender::HandlerMaker final {\npublic:\n\tstatic DoneHandler MakeDone(\n\t\tnot_null<ConcurrentSender*> sender,\n\t\tFn<void(FnMut<void()>)> runner);\n\tstatic FailHandler MakeFail(\n\t\tnot_null<ConcurrentSender*> sender,\n\t\tFn<void(FnMut<void()>)> runner,\n\t\tFailSkipPolicy skipPolicy);\n};\n\nDoneHandler ConcurrentSender::HandlerMaker::MakeDone(\n\t\tnot_null<ConcurrentSender*> sender,\n\t\tFn<void(FnMut<void()>)> runner) {\n\treturn [\n\t\tweak = base::make_weak(sender),\n\t\trunner = std::move(runner)\n\t](const Response &response) mutable {\n\t\trunner([=]() mutable {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->senderRequestDone(\n\t\t\t\t\tresponse.requestId,\n\t\t\t\t\tbytes::make_span(response.reply));\n\t\t\t}\n\t\t});\n\t\treturn true;\n\t};\n}\n\nFailHandler ConcurrentSender::HandlerMaker::MakeFail(\n\t\tnot_null<ConcurrentSender*> sender,\n\t\tFn<void(FnMut<void()>)> runner,\n\t\tFailSkipPolicy skipPolicy) {\n\treturn [\n\t\tweak = base::make_weak(sender),\n\t\trunner = std::move(runner),\n\t\tskipPolicy\n\t](const Error &error, const Response &response) mutable {\n\t\tif (skipPolicy == FailSkipPolicy::Simple) {\n\t\t\tif (IsDefaultHandledError(error)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else if (skipPolicy == FailSkipPolicy::HandleFlood) {\n\t\t\tif (IsDefaultHandledError(error) && !IsFloodError(error)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\trunner([=, requestId = response.requestId]() mutable {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->senderRequestFail(requestId, error);\n\t\t\t}\n\t\t});\n\t\treturn true;\n\t};\n}\n\ntemplate <typename Method>\nauto ConcurrentSender::with_instance(Method &&method)\n-> std::enable_if_t<is_callable_v<Method, not_null<Instance*>>> {\n\tcrl::on_main([\n\t\tweak = _weak,\n\t\tmethod = std::forward<Method>(method)\n\t]() mutable {\n\t\tif (const auto instance = weak.get()) {\n\t\t\tstd::move(method)(instance);\n\t\t}\n\t});\n}\n\nConcurrentSender::RequestBuilder::RequestBuilder(\n\tnot_null<ConcurrentSender*> sender,\n\tdetails::SerializedRequest &&serialized) noexcept\n: _sender(sender)\n, _serialized(std::move(serialized)) {\n}\n\nvoid ConcurrentSender::RequestBuilder::setToDC(ShiftedDcId dcId) noexcept {\n\t_dcId = dcId;\n}\n\nvoid ConcurrentSender::RequestBuilder::setCanWait(crl::time ms) noexcept {\n\t_canWait = ms;\n}\n\nvoid ConcurrentSender::RequestBuilder::setFailSkipPolicy(\n\t\tFailSkipPolicy policy) noexcept {\n\t_failSkipPolicy = policy;\n}\n\nvoid ConcurrentSender::RequestBuilder::setAfter(\n\t\tmtpRequestId requestId) noexcept {\n\t_afterRequestId = requestId;\n}\n\nmtpRequestId ConcurrentSender::RequestBuilder::send() {\n\tconst auto requestId = details::GetNextRequestId();\n\tconst auto dcId = _dcId;\n\tconst auto msCanWait = _canWait;\n\tconst auto afterRequestId = _afterRequestId;\n\n\t_sender->senderRequestRegister(requestId, std::move(_handlers));\n\t_sender->with_instance([\n\t\t=,\n\t\trequest = std::move(_serialized),\n\t\tdone = HandlerMaker::MakeDone(_sender, _sender->_runner),\n\t\tfail = HandlerMaker::MakeFail(\n\t\t\t_sender,\n\t\t\t_sender->_runner,\n\t\t\t_failSkipPolicy)\n\t](not_null<Instance*> instance) mutable {\n\t\tinstance->sendSerialized(\n\t\t\trequestId,\n\t\t\tstd::move(request),\n\t\t\tResponseHandler{ std::move(done), std::move(fail) },\n\t\t\tdcId,\n\t\t\tmsCanWait,\n\t\t\tafterRequestId);\n\t});\n\n\treturn requestId;\n}\n\nConcurrentSender::ConcurrentSender(\n\tbase::weak_qptr<Instance> weak,\n\tFn<void(FnMut<void()>)> runner)\n: _weak(weak)\n, _runner(runner) {\n}\n\nConcurrentSender::~ConcurrentSender() {\n\tsenderRequestCancelAll();\n}\n\nvoid ConcurrentSender::senderRequestRegister(\n\t\tmtpRequestId requestId,\n\t\tHandlers &&handlers) {\n\t_requests.emplace(requestId, std::move(handlers));\n}\n\nvoid ConcurrentSender::senderRequestDone(\n\t\tmtpRequestId requestId,\n\t\tbytes::const_span result) {\n\tif (auto handlers = _requests.take(requestId)) {\n\t\tif (!handlers->done(requestId, result)) {\n\t\t\thandlers->fail(\n\t\t\t\trequestId,\n\t\t\t\tError::Local(\n\t\t\t\t\t\"RESPONSE_PARSE_FAILED\",\n\t\t\t\t\t\"ConcurrentSender::senderRequestDone\"));\n\t\t}\n\t}\n}\n\nvoid ConcurrentSender::senderRequestFail(\n\t\tmtpRequestId requestId,\n\t\tconst Error &error) {\n\tif (auto handlers = _requests.take(requestId)) {\n\t\thandlers->fail(requestId, error);\n\t}\n}\n\nvoid ConcurrentSender::senderRequestCancel(mtpRequestId requestId) {\n\tsenderRequestDetach(requestId);\n\twith_instance([=](not_null<Instance*> instance) {\n\t\tinstance->cancel(requestId);\n\t});\n}\n\nvoid ConcurrentSender::senderRequestCancelAll() {\n\tauto list = std::vector<mtpRequestId>(_requests.size());\n\tfor (const auto &pair : base::take(_requests)) {\n\t\tlist.push_back(pair.first);\n\t}\n\twith_instance([list = std::move(list)](not_null<Instance*> instance) {\n\t\tfor (const auto requestId : list) {\n\t\t\tinstance->cancel(requestId);\n\t\t}\n\t});\n}\n\nvoid ConcurrentSender::senderRequestDetach(mtpRequestId requestId) {\n\t_requests.erase(requestId);\n}\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtproto_concurrent_sender.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/bytes.h\"\n#include \"base/weak_ptr.h\"\n#include \"base/weak_qptr.h\"\n#include \"base/flat_map.h\"\n#include \"mtproto/core_types.h\"\n#include \"mtproto/details/mtproto_serialized_request.h\"\n\n#include <QtCore/QPointer>\n#include <rpl/details/callable.h>\n\n#ifndef _DEBUG\n#define MTP_SENDER_USE_GENERIC_HANDLERS\n#endif // !_DEBUG\n\n\nnamespace MTP {\n\nclass Error;\nclass Instance;\n\nclass ConcurrentSender : public base::has_weak_ptr {\n\ttemplate <typename ...Args>\n\tstatic constexpr bool is_callable_v\n\t\t= rpl::details::is_callable_v<Args...>;\n\n\ttemplate <typename Method>\n\tauto with_instance(Method &&method)\n\t-> std::enable_if_t<is_callable_v<Method, not_null<Instance*>>>;\n\n\tstruct Handlers {\n\t\tFnMut<bool(mtpRequestId requestId, bytes::const_span result)> done;\n\t\tFnMut<void(mtpRequestId requestId, const Error &error)> fail;\n\t};\n\n\tenum class FailSkipPolicy {\n\t\tSimple,\n\t\tHandleFlood,\n\t\tHandleAll,\n\t};\n\n\tclass RequestBuilder {\n\tpublic:\n\t\tRequestBuilder(const RequestBuilder &other) = delete;\n\t\tRequestBuilder(RequestBuilder &&other) = default;\n\t\tRequestBuilder &operator=(const RequestBuilder &other) = delete;\n\t\tRequestBuilder &operator=(RequestBuilder &&other) = delete;\n\n\t\tmtpRequestId send();\n\n\tprotected:\n\t\tRequestBuilder(\n\t\t\tnot_null<ConcurrentSender*> sender,\n\t\t\tdetails::SerializedRequest &&serialized) noexcept;\n\n\t\tvoid setToDC(ShiftedDcId dcId) noexcept;\n\t\tvoid setCanWait(crl::time ms) noexcept;\n\t\ttemplate <typename Response, typename InvokeFullDone>\n\t\tvoid setDoneHandler(InvokeFullDone &&invoke) noexcept;\n\t\ttemplate <typename InvokeFullFail>\n\t\tvoid setFailHandler(InvokeFullFail &&invoke) noexcept;\n\t\tvoid setFailSkipPolicy(FailSkipPolicy policy) noexcept;\n\t\tvoid setAfter(mtpRequestId requestId) noexcept;\n\n\tprivate:\n\t\tnot_null<ConcurrentSender*> _sender;\n\t\tdetails::SerializedRequest _serialized;\n\t\tShiftedDcId _dcId = 0;\n\t\tcrl::time _canWait = 0;\n\n\t\tHandlers _handlers;\n\t\tFailSkipPolicy _failSkipPolicy = FailSkipPolicy::Simple;\n\t\tmtpRequestId _afterRequestId = 0;\n\n\t};\n\npublic:\n\tConcurrentSender(\n\t\tbase::weak_qptr<Instance> weak,\n\t\tFn<void(FnMut<void()>)> runner);\n\n\ttemplate <typename Request>\n\tclass SpecificRequestBuilder : public RequestBuilder {\n\tpublic:\n\t\tusing Result = typename Request::ResponseType;\n\n\t\tSpecificRequestBuilder(\n\t\t\tconst SpecificRequestBuilder &other) = delete;\n\t\tSpecificRequestBuilder(\n\t\t\tSpecificRequestBuilder &&other) = default;\n\t\tSpecificRequestBuilder &operator=(\n\t\t\tconst SpecificRequestBuilder &other) = delete;\n\t\tSpecificRequestBuilder &operator=(\n\t\t\tSpecificRequestBuilder &&other) = delete;\n\n\t\t[[nodiscard]] SpecificRequestBuilder &toDC(\n\t\t\tShiftedDcId dcId) noexcept;\n\t\t[[nodiscard]] SpecificRequestBuilder &afterDelay(\n\t\t\tcrl::time ms) noexcept;\n\n#ifndef MTP_SENDER_USE_GENERIC_HANDLERS\n\t\t// Allow code completion to show response type.\n\t\t[[nodiscard]] SpecificRequestBuilder &done(FnMut<void()> &&handler);\n\t\t[[nodiscard]] SpecificRequestBuilder &done(\n\t\t\tFnMut<void(mtpRequestId, Result &&)> &&handler);\n\t\t[[nodiscard]] SpecificRequestBuilder &done(\n\t\t\tFnMut<void(Result &&)> &&handler);\n\t\t[[nodiscard]] SpecificRequestBuilder &fail(Fn<void()> &&handler);\n\t\t[[nodiscard]] SpecificRequestBuilder &fail(\n\t\t\tFn<void(mtpRequestId, const Error &)> &&handler);\n\t\t[[nodiscard]] SpecificRequestBuilder &fail(\n\t\t\tFn<void(const Error &)> &&handler);\n#else // !MTP_SENDER_USE_GENERIC_HANDLERS\n\t\ttemplate <typename Handler>\n\t\t[[nodiscard]] SpecificRequestBuilder &done(Handler &&handler);\n\t\ttemplate <typename Handler>\n\t\t[[nodiscard]] SpecificRequestBuilder &fail(Handler &&handler);\n#endif // MTP_SENDER_USE_GENERIC_HANDLERS\n\n\t\t[[nodiscard]] SpecificRequestBuilder &handleFloodErrors() noexcept;\n\t\t[[nodiscard]] SpecificRequestBuilder &handleAllErrors() noexcept;\n\t\t[[nodiscard]] SpecificRequestBuilder &afterRequest(\n\t\t\tmtpRequestId requestId) noexcept;\n\n\tprivate:\n\t\tSpecificRequestBuilder(\n\t\t\tnot_null<ConcurrentSender*> sender,\n\t\t\tRequest &&request) noexcept;\n\n\t\tfriend class ConcurrentSender;\n\n\t};\n\n\tclass SentRequestWrap {\n\tpublic:\n\t\tvoid cancel();\n\t\tvoid detach();\n\n\tprivate:\n\t\tfriend class ConcurrentSender;\n\t\tSentRequestWrap(\n\t\t\tnot_null<ConcurrentSender*> sender,\n\t\t\tmtpRequestId requestId);\n\n\t\tnot_null<ConcurrentSender*> _sender;\n\t\tmtpRequestId _requestId = 0;\n\n\t};\n\n\ttemplate <\n\t\ttypename Request,\n\t\ttypename = std::enable_if_t<!std::is_reference_v<Request>>,\n\t\ttypename = typename Request::Unboxed>\n\t[[nodiscard]] SpecificRequestBuilder<Request> request(\n\t\tRequest &&request) noexcept;\n\n\t[[nodiscard]] SentRequestWrap request(mtpRequestId requestId) noexcept;\n\n\t[[nodiscard]] auto requestCanceller() noexcept;\n\n\t~ConcurrentSender();\n\nprivate:\n\tclass HandlerMaker;\n\tfriend class HandlerMaker;\n\tfriend class RequestBuilder;\n\tfriend class SentRequestWrap;\n\n\tvoid senderRequestRegister(mtpRequestId requestId, Handlers &&handlers);\n\tvoid senderRequestDone(\n\t\tmtpRequestId requestId,\n\t\tbytes::const_span result);\n\tvoid senderRequestFail(\n\t\tmtpRequestId requestId,\n\t\tconst Error &error);\n\tvoid senderRequestCancel(mtpRequestId requestId);\n\tvoid senderRequestCancelAll();\n\tvoid senderRequestDetach(mtpRequestId requestId);\n\n\tconst base::weak_qptr<Instance> _weak;\n\tconst Fn<void(FnMut<void()>)> _runner;\n\tbase::flat_map<mtpRequestId, Handlers> _requests;\n\n};\n\ntemplate <typename Result, typename InvokeFullDone>\nvoid ConcurrentSender::RequestBuilder::setDoneHandler(\n\tInvokeFullDone &&invoke\n) noexcept {\n\t_handlers.done = [handler = std::move(invoke)](\n\t\t\tmtpRequestId requestId,\n\t\t\tbytes::const_span result) mutable {\n\t\tauto from = reinterpret_cast<const mtpPrime*>(result.data());\n\t\tconst auto end = from + result.size() / sizeof(mtpPrime);\n\t\tResult data;\n\t\tif (!data.read(from, end)) {\n\t\t\treturn false;\n\t\t}\n\t\thandler(requestId, std::move(data));\n\t\treturn true;\n\t};\n}\n\ntemplate <typename InvokeFullFail>\nvoid ConcurrentSender::RequestBuilder::setFailHandler(\n\tInvokeFullFail &&invoke\n) noexcept {\n\t_handlers.fail = std::move(invoke);\n}\n\ntemplate <typename Request>\nConcurrentSender::SpecificRequestBuilder<Request>::SpecificRequestBuilder(\n\tnot_null<ConcurrentSender*> sender,\n\tRequest &&request) noexcept\n: RequestBuilder(sender, details::SerializedRequest::Serialize(request)) {\n}\n\ntemplate <typename Request>\nauto ConcurrentSender::SpecificRequestBuilder<Request>::toDC(\n\tShiftedDcId dcId\n) noexcept -> SpecificRequestBuilder & {\n\tsetToDC(dcId);\n\treturn *this;\n}\n\ntemplate <typename Request>\nauto ConcurrentSender::SpecificRequestBuilder<Request>::afterDelay(\n\tcrl::time ms\n) noexcept -> SpecificRequestBuilder & {\n\tsetCanWait(ms);\n\treturn *this;\n}\n\n#ifndef MTP_SENDER_USE_GENERIC_HANDLERS\n// Allow code completion to show response type.\ntemplate <typename Request>\nauto ConcurrentSender::SpecificRequestBuilder<Request>::done(\n\tFnMut<void(Result &&)> &&handler\n) -> SpecificRequestBuilder & {\n\tsetDoneHandler<Result>([handler = std::move(handler)](\n\t\t\tmtpRequestId requestId,\n\t\t\tResult &&result) mutable {\n\t\thandler(std::move(result));\n\t});\n\treturn *this;\n}\n\ntemplate <typename Request>\nauto ConcurrentSender::SpecificRequestBuilder<Request>::done(\n\tFnMut<void(mtpRequestId, Result &&)> &&handler\n) -> SpecificRequestBuilder & {\n\tsetDoneHandler<Result>(std::move(handler));\n\treturn *this;\n}\n\ntemplate <typename Request>\nauto ConcurrentSender::SpecificRequestBuilder<Request>::done(\n\tFnMut<void()> &&handler\n) -> SpecificRequestBuilder & {\n\tsetDoneHandler<Result>([handler = std::move(handler)](\n\t\t\tmtpRequestId requestId,\n\t\t\tResult &&result) mutable {\n\t\tstd::move(handler)();\n\t});\n\treturn *this;\n}\n\ntemplate <typename Request>\nauto ConcurrentSender::SpecificRequestBuilder<Request>::fail(\n\tFn<void(const Error &)> &&handler\n) -> SpecificRequestBuilder & {\n\tsetFailHandler([handler = std::move(handler)](\n\t\t\tmtpRequestId requestId,\n\t\t\tconst Error &error) {\n\t\thandler(error);\n\t});\n\treturn *this;\n}\n\ntemplate <typename Request>\nauto ConcurrentSender::SpecificRequestBuilder<Request>::fail(\n\tFn<void(mtpRequestId, const Error &)> &&handler\n) -> SpecificRequestBuilder & {\n\tsetFailHandler(std::move(handler));\n\treturn *this;\n}\n\ntemplate <typename Request>\nauto ConcurrentSender::SpecificRequestBuilder<Request>::fail(\n\tFn<void()> &&handler\n) -> SpecificRequestBuilder & {\n\tsetFailHandler([handler = std::move(handler)](\n\t\t\tmtpRequestId requestId,\n\t\t\tconst Error &error) {\n\t\thandler();\n\t});\n\treturn *this;\n}\n#else // !MTP_SENDER_USE_GENERIC_HANDLERS\ntemplate <typename Request>\ntemplate <typename Handler>\nauto ConcurrentSender::SpecificRequestBuilder<Request>::done(\n\tHandler &&handler\n) -> SpecificRequestBuilder & {\n\tusing Result = typename Request::ResponseType;\n\tconstexpr auto takesFull = rpl::details::is_callable_plain_v<\n\t\tHandler,\n\t\tmtpRequestId,\n\t\tResult>;\n\t[[maybe_unused]] constexpr auto takesResponse = rpl::details::is_callable_plain_v<\n\t\tHandler,\n\t\tResult>;\n\t[[maybe_unused]] constexpr auto takesNone = rpl::details::is_callable_plain_v<Handler>;\n\n\tif constexpr (takesFull) {\n\t\tsetDoneHandler<Result>(std::forward<Handler>(handler));\n\t} else if constexpr (takesResponse) {\n\t\tsetDoneHandler<Result>([handler = std::forward<Handler>(handler)](\n\t\t\t\tmtpRequestId requestId,\n\t\t\t\tResult &&result) mutable {\n\t\t\thandler(std::move(result));\n\t\t});\n\t} else if constexpr (takesNone) {\n\t\tsetDoneHandler<Result>([handler = std::forward<Handler>(handler)](\n\t\t\t\tmtpRequestId requestId,\n\t\t\t\tResult &&result) mutable {\n\t\t\thandler();\n\t\t});\n\t} else {\n\t\tstatic_assert(false_t(Handler{}), \"Bad done handler.\");\n\t}\n\treturn *this;\n}\n\ntemplate <typename Request>\ntemplate <typename Handler>\nauto ConcurrentSender::SpecificRequestBuilder<Request>::fail(\n\tHandler &&handler\n) -> SpecificRequestBuilder & {\n\tconstexpr auto takesFull = rpl::details::is_callable_plain_v<\n\t\tHandler,\n\t\tmtpRequestId,\n\t\tError>;\n\t[[maybe_unused]] constexpr auto takesError = rpl::details::is_callable_plain_v<\n\t\tHandler,\n\t\tError>;\n\t[[maybe_unused]] constexpr auto takesNone = rpl::details::is_callable_plain_v<Handler>;\n\n\tif constexpr (takesFull) {\n\t\tsetFailHandler(std::forward<Handler>(handler));\n\t} else if constexpr (takesError) {\n\t\tsetFailHandler([handler = std::forward<Handler>(handler)](\n\t\t\t\tmtpRequestId requestId,\n\t\t\t\tconst Error &error) {\n\t\t\thandler(error);\n\t\t});\n\t} else if constexpr (takesNone) {\n\t\tsetFailHandler([handler = std::forward<Handler>(handler)](\n\t\t\t\tmtpRequestId requestId,\n\t\t\t\tconst Error &error) {\n\t\t\thandler();\n\t\t});\n\t} else {\n\t\tstatic_assert(false_t(Handler{}), \"Bad fail handler.\");\n\t}\n\treturn *this;\n}\n\n#endif // MTP_SENDER_USE_GENERIC_HANDLERS\n\ntemplate <typename Request>\nauto ConcurrentSender::SpecificRequestBuilder<Request>::handleFloodErrors(\n) noexcept -> SpecificRequestBuilder & {\n\tsetFailSkipPolicy(FailSkipPolicy::HandleFlood);\n\treturn *this;\n}\n\ntemplate <typename Request>\nauto ConcurrentSender::SpecificRequestBuilder<Request>::handleAllErrors(\n) noexcept -> SpecificRequestBuilder & {\n\tsetFailSkipPolicy(FailSkipPolicy::HandleAll);\n\treturn *this;\n}\n\ntemplate <typename Request>\nauto ConcurrentSender::SpecificRequestBuilder<Request>::afterRequest(\n\tmtpRequestId requestId\n) noexcept -> SpecificRequestBuilder & {\n\tsetAfter(requestId);\n\treturn *this;\n}\n\ninline void ConcurrentSender::SentRequestWrap::cancel() {\n\t_sender->senderRequestCancel(_requestId);\n}\n\ninline void ConcurrentSender::SentRequestWrap::detach() {\n\t_sender->senderRequestDetach(_requestId);\n}\n\ninline ConcurrentSender::SentRequestWrap::SentRequestWrap(\n\tnot_null<ConcurrentSender*> sender,\n\tmtpRequestId requestId\n) : _sender(sender)\n, _requestId(requestId) {\n}\n\ntemplate <typename Request, typename, typename>\ninline auto ConcurrentSender::request(Request &&request) noexcept\n-> SpecificRequestBuilder<Request> {\n\treturn SpecificRequestBuilder<Request>(this, std::move(request));\n}\n\ninline auto ConcurrentSender::requestCanceller() noexcept {\n\treturn [=](mtpRequestId requestId) {\n\t\trequest(requestId).cancel();\n\t};\n}\n\ninline auto ConcurrentSender::request(mtpRequestId requestId) noexcept\n-> SentRequestWrap {\n\treturn SentRequestWrap(this, requestId);\n}\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtproto_config.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/mtproto_config.h\"\n\n#include \"storage/serialize_common.h\"\n#include \"mtproto/type_utils.h\"\n#include \"logs.h\"\n\nnamespace MTP {\nnamespace {\n\nconstexpr auto kVersion = 1;\n\n[[nodiscard]] QString ConfigDefaultReactionEmoji() {\n\tstatic const auto result = QString::fromUtf8(\"\\xf0\\x9f\\x91\\x8d\");\n\treturn result;\n}\n\n} // namespace\n\nConfigFields::ConfigFields(Environment environment)\n: webFileDcId(environment == Environment::Test ? 2 : 4)\n, txtDomainString(environment == Environment::Test\n\t? u\"tapv3.stel.com\"_q\n\t: u\"apv3.stel.com\"_q)\n, reactionDefaultEmoji(ConfigDefaultReactionEmoji())\n, gifSearchUsername(environment == Environment::Test\n\t? u\"izgifbot\"_q\n\t: u\"gif\"_q)\n, venueSearchUsername(environment == Environment::Test\n\t? u\"foursquarebot\"_q\n\t: u\"foursquare\"_q) {\n}\n\nConfig::Config(Environment environment)\n: _dcOptions(environment)\n, _fields(environment) {\n}\n\nConfig::Config(const Config &other)\n: _dcOptions(other.dcOptions())\n, _fields(other._fields) {\n}\n\nQByteArray Config::serialize() const {\n\tauto options = _dcOptions.serialize();\n\tauto size = sizeof(qint32) * 2 // version + environment\n\t\t+ Serialize::bytearraySize(options)\n\t\t+ 19 * sizeof(qint32)\n\t\t+ Serialize::stringSize(_fields.internalLinksDomain)\n\t\t+ 6 * sizeof(qint32)\n\t\t+ Serialize::stringSize(_fields.txtDomainString)\n\t\t+ 3 * sizeof(qint32)\n\t\t+ Serialize::stringSize(_fields.reactionDefaultEmoji)\n\t\t+ sizeof(quint64)\n\t\t+ sizeof(qint32)\n\t\t+ Serialize::stringSize(_fields.gifSearchUsername)\n\t\t+ Serialize::stringSize(_fields.venueSearchUsername);\n\n\tauto result = QByteArray();\n\tresult.reserve(size);\n\t{\n\t\tQDataStream stream(&result, QIODevice::WriteOnly);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\t\tstream\n\t\t\t<< qint32(kVersion)\n\t\t\t<< qint32(_dcOptions.isTestMode()\n\t\t\t\t? Environment::Test\n\t\t\t\t: Environment::Production)\n\t\t\t<< options\n\t\t\t<< qint32(_fields.chatSizeMax)\n\t\t\t<< qint32(_fields.megagroupSizeMax)\n\t\t\t<< qint32(_fields.forwardedCountMax)\n\t\t\t<< qint32(_fields.onlineUpdatePeriod)\n\t\t\t<< qint32(_fields.offlineBlurTimeout)\n\t\t\t<< qint32(_fields.offlineIdleTimeout)\n\t\t\t<< qint32(_fields.onlineFocusTimeout)\n\t\t\t<< qint32(_fields.onlineCloudTimeout)\n\t\t\t<< qint32(_fields.notifyCloudDelay)\n\t\t\t<< qint32(_fields.notifyDefaultDelay)\n\t\t\t<< qint32(0) // legacy savedGifsLimit\n\t\t\t<< qint32(_fields.editTimeLimit)\n\t\t\t<< qint32(_fields.revokeTimeLimit)\n\t\t\t<< qint32(_fields.revokePrivateTimeLimit)\n\t\t\t<< qint32(_fields.revokePrivateInbox ? 1 : 0)\n\t\t\t<< qint32(_fields.stickersRecentLimit)\n\t\t\t<< qint32(0) // legacy stickersFavedLimit\n\t\t\t<< qint32(0) // legacy pinnedDialogsCountMax\n\t\t\t<< qint32(0) // legacy pinnedDialogsInFolderMax\n\t\t\t<< _fields.internalLinksDomain\n\t\t\t<< qint32(_fields.channelsReadMediaPeriod)\n\t\t\t<< qint32(_fields.callReceiveTimeoutMs)\n\t\t\t<< qint32(_fields.callRingTimeoutMs)\n\t\t\t<< qint32(_fields.callConnectTimeoutMs)\n\t\t\t<< qint32(_fields.callPacketTimeoutMs)\n\t\t\t<< qint32(_fields.webFileDcId)\n\t\t\t<< _fields.txtDomainString\n\t\t\t<< qint32(1) // legacy phoneCallsEnabled\n\t\t\t<< qint32(_fields.blockedMode ? 1 : 0)\n\t\t\t<< qint32(_fields.captionLengthMax)\n\t\t\t<< _fields.reactionDefaultEmoji\n\t\t\t<< quint64(_fields.reactionDefaultCustom)\n\t\t\t<< qint32(_fields.ratingDecay)\n\t\t\t<< _fields.gifSearchUsername\n\t\t\t<< _fields.venueSearchUsername;\n\t}\n\treturn result;\n}\n\nstd::unique_ptr<Config> Config::FromSerialized(const QByteArray &serialized) {\n\tauto result = std::unique_ptr<Config>();\n\tauto raw = result.get();\n\n\tQDataStream stream(serialized);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\n\tauto version = qint32();\n\tstream >> version;\n\tif (version != kVersion) {\n\t\treturn result;\n\t}\n\tauto environment = qint32();\n\tstream >> environment;\n\tswitch (environment) {\n\tcase qint32(Environment::Test):\n\t\tresult = std::make_unique<Config>(Environment::Test);\n\t\tbreak;\n\tcase qint32(Environment::Production):\n\t\tresult = std::make_unique<Config>(Environment::Production);\n\t\tbreak;\n\t}\n\tif (!(raw = result.get())) {\n\t\treturn result;\n\t}\n\n\tauto dcOptionsSerialized = QByteArray();\n\tauto legacySavedGifsLimit = int();\n\tauto legacyStickersFavedLimit = int();\n\tauto legacyPinnedDialogsCountMax = 0;\n\tauto legacyPinnedDialogsInFolderMax = 0;\n\tauto legacyPhoneCallsEnabled = rpl::variable<bool>();\n\tconst auto read = [&](auto &field) {\n\t\tusing Type = std::remove_reference_t<decltype(field)>;\n\t\tif constexpr (std::is_same_v<Type, int>\n\t\t\t|| std::is_same_v<Type, rpl::variable<int>>) {\n\t\t\tauto value = qint32();\n\t\t\tstream >> value;\n\t\t\tfield = value;\n\t\t} else if constexpr (std::is_same_v<Type, uint64>) {\n\t\t\tauto value = quint64();\n\t\t\tstream >> value;\n\t\t\tfield = value;\n\t\t} else if constexpr (std::is_same_v<Type, bool>\n\t\t\t|| std::is_same_v<Type, rpl::variable<bool>>) {\n\t\t\tauto value = qint32();\n\t\t\tstream >> value;\n\t\t\tfield = (value == 1);\n\t\t} else if constexpr (std::is_same_v<Type, QByteArray>\n\t\t\t|| std::is_same_v<Type, QString>) {\n\t\t\tstream >> field;\n\t\t} else {\n\t\t\tstatic_assert(false_(field), \"Bad read() call.\");\n\t\t}\n\t};\n\n\tread(dcOptionsSerialized);\n\tread(raw->_fields.chatSizeMax);\n\tread(raw->_fields.megagroupSizeMax);\n\tread(raw->_fields.forwardedCountMax);\n\tread(raw->_fields.onlineUpdatePeriod);\n\tread(raw->_fields.offlineBlurTimeout);\n\tread(raw->_fields.offlineIdleTimeout);\n\tread(raw->_fields.onlineFocusTimeout);\n\tread(raw->_fields.onlineCloudTimeout);\n\tread(raw->_fields.notifyCloudDelay);\n\tread(raw->_fields.notifyDefaultDelay);\n\tread(legacySavedGifsLimit);\n\tread(raw->_fields.editTimeLimit);\n\tread(raw->_fields.revokeTimeLimit);\n\tread(raw->_fields.revokePrivateTimeLimit);\n\tread(raw->_fields.revokePrivateInbox);\n\tread(raw->_fields.stickersRecentLimit);\n\tread(legacyStickersFavedLimit);\n\tread(legacyPinnedDialogsCountMax);\n\tread(legacyPinnedDialogsInFolderMax);\n\tread(raw->_fields.internalLinksDomain);\n\tread(raw->_fields.channelsReadMediaPeriod);\n\tread(raw->_fields.callReceiveTimeoutMs);\n\tread(raw->_fields.callRingTimeoutMs);\n\tread(raw->_fields.callConnectTimeoutMs);\n\tread(raw->_fields.callPacketTimeoutMs);\n\tread(raw->_fields.webFileDcId);\n\tread(raw->_fields.txtDomainString);\n\tread(legacyPhoneCallsEnabled);\n\tread(raw->_fields.blockedMode);\n\tread(raw->_fields.captionLengthMax);\n\tif (!stream.atEnd()) {\n\t\tread(raw->_fields.reactionDefaultEmoji);\n\t\tread(raw->_fields.reactionDefaultCustom);\n\t}\n\tif (!stream.atEnd()) {\n\t\tread(raw->_fields.ratingDecay);\n\t}\n\tif (!stream.atEnd()) {\n\t\tread(raw->_fields.gifSearchUsername);\n\t\tread(raw->_fields.venueSearchUsername);\n\t}\n\n\tif (stream.status() != QDataStream::Ok\n\t\t|| !raw->_dcOptions.constructFromSerialized(dcOptionsSerialized)) {\n\t\treturn nullptr;\n\t}\n\treturn result;\n}\n\nconst ConfigFields &Config::values() const {\n\treturn _fields;\n}\n\nvoid Config::apply(const MTPDconfig &data) {\n\tif (mtpIsTrue(data.vtest_mode()) != _dcOptions.isTestMode()) {\n\t\tLOG((\"MTP Error: config with wrong test mode field received!\"));\n\t\treturn;\n\t}\n\n\tDEBUG_LOG((\"MTP Info: got config, \"\n\t\t\"chat_size_max: %1, \"\n\t\t\"date: %2, \"\n\t\t\"test_mode: %3, \"\n\t\t\"this_dc: %4, \"\n\t\t\"dc_options.length: %5\"\n\t\t).arg(data.vchat_size_max().v\n\t\t).arg(data.vdate().v\n\t\t).arg(mtpIsTrue(data.vtest_mode())\n\t\t).arg(data.vthis_dc().v\n\t\t).arg(data.vdc_options().v.size()));\n\n\t_fields.chatSizeMax = data.vchat_size_max().v;\n\t_fields.megagroupSizeMax = data.vmegagroup_size_max().v;\n\t_fields.forwardedCountMax = data.vforwarded_count_max().v;\n\t_fields.onlineUpdatePeriod = data.vonline_update_period_ms().v;\n\t_fields.offlineBlurTimeout = data.voffline_blur_timeout_ms().v;\n\t_fields.offlineIdleTimeout = data.voffline_idle_timeout_ms().v;\n\t_fields.onlineCloudTimeout = data.vonline_cloud_timeout_ms().v;\n\t_fields.notifyCloudDelay = data.vnotify_cloud_delay_ms().v;\n\t_fields.notifyDefaultDelay = data.vnotify_default_delay_ms().v;\n\t_fields.editTimeLimit = data.vedit_time_limit().v;\n\t_fields.revokeTimeLimit = data.vrevoke_time_limit().v;\n\t_fields.revokePrivateTimeLimit = data.vrevoke_pm_time_limit().v;\n\t_fields.revokePrivateInbox = data.is_revoke_pm_inbox();\n\t_fields.stickersRecentLimit = data.vstickers_recent_limit().v;\n\t_fields.internalLinksDomain = qs(data.vme_url_prefix());\n\t_fields.channelsReadMediaPeriod = data.vchannels_read_media_period().v;\n\t_fields.webFileDcId = data.vwebfile_dc_id().v;\n\t_fields.callReceiveTimeoutMs = data.vcall_receive_timeout_ms().v;\n\t_fields.callRingTimeoutMs = data.vcall_ring_timeout_ms().v;\n\t_fields.callConnectTimeoutMs = data.vcall_connect_timeout_ms().v;\n\t_fields.callPacketTimeoutMs = data.vcall_packet_timeout_ms().v;\n\t_fields.blockedMode = data.is_blocked_mode();\n\t_fields.captionLengthMax = data.vcaption_length_max().v;\n\t_fields.reactionDefaultEmoji = ConfigDefaultReactionEmoji();\n\t_fields.reactionDefaultCustom = 0;\n\tif (const auto reaction = data.vreactions_default()) {\n\t\treaction->match([&](const MTPDreactionEmpty &) {\n\t\t}, [&](const MTPDreactionEmoji &data) {\n\t\t\t_fields.reactionDefaultEmoji = qs(data.vemoticon());\n\t\t}, [&](const MTPDreactionCustomEmoji &data) {\n\t\t\t_fields.reactionDefaultCustom = data.vdocument_id().v;\n\t\t}, [&](const MTPDreactionPaid &data) {\n\t\t\t_fields.reactionDefaultEmoji = QString(QChar('*'));\n\t\t});\n\t}\n\t_fields.autologinToken = qs(data.vautologin_token().value_or_empty());\n\t_fields.ratingDecay = data.vrating_e_decay().v;\n\tif (_fields.ratingDecay <= 0) {\n\t\t_fields.ratingDecay = ConfigFields(\n\t\t\t_dcOptions.environment()\n\t\t).ratingDecay;\n\t}\n\t_fields.gifSearchUsername = qs(data.vgif_search_username().value_or_empty());\n\t_fields.venueSearchUsername = qs(data.vvenue_search_username().value_or_empty());\n\n\tif (data.vdc_options().v.empty()) {\n\t\tLOG((\"MTP Error: config with empty dc_options received!\"));\n\t} else {\n\t\tdcOptions().setFromList(data.vdc_options());\n\t}\n\n\t_updates.fire({});\n}\n\nrpl::producer<> Config::updates() const {\n\treturn _updates.events();\n}\n\nvoid Config::setChatSizeMax(int value) {\n\t_fields.chatSizeMax = value;\n}\n\nvoid Config::setStickersRecentLimit(int value) {\n\t_fields.stickersRecentLimit = value;\n}\n\nvoid Config::setMegagroupSizeMax(int value) {\n\t_fields.megagroupSizeMax = value;\n}\n\nvoid Config::setTxtDomainString(const QString &value) {\n\t_fields.txtDomainString = value;\n}\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtproto_config.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/mtproto_dc_options.h\"\n\nnamespace MTP {\n\nstruct ConfigFields {\n\texplicit ConfigFields(Environment environment);\n\n\tint chatSizeMax = 200;\n\tint megagroupSizeMax = 10000;\n\tint forwardedCountMax = 100;\n\tint onlineUpdatePeriod = 120000;\n\tint offlineBlurTimeout = 5000;\n\tint offlineIdleTimeout = 30000;\n\tint onlineFocusTimeout = 1000; // Not from the server config.\n\tint onlineCloudTimeout = 300000;\n\tint notifyCloudDelay = 30000;\n\tint notifyDefaultDelay = 1500;\n\tint editTimeLimit = 172800;\n\tint revokeTimeLimit = 172800;\n\tint revokePrivateTimeLimit = 172800;\n\tbool revokePrivateInbox = false;\n\tint stickersRecentLimit = 30;\n\tQString internalLinksDomain = u\"https://t.me/\"_q;\n\tint channelsReadMediaPeriod = 86400 * 7;\n\tint callReceiveTimeoutMs = 20000;\n\tint callRingTimeoutMs = 90000;\n\tint callConnectTimeoutMs = 30000;\n\tint callPacketTimeoutMs = 10000;\n\tint webFileDcId = 4;\n\tQString txtDomainString;\n\tbool blockedMode = false;\n\tint captionLengthMax = 1024;\n\tint ratingDecay = 2419200;\n\tQString reactionDefaultEmoji;\n\tuint64 reactionDefaultCustom = 0;\n\tQString autologinToken;\n\n\tQString gifSearchUsername;\n\tQString venueSearchUsername;\n};\n\nclass Config final {\n\tstruct PrivateTag {\n\t};\n\npublic:\n\texplicit Config(Environment environment);\n\tConfig(const Config &other);\n\n\t[[nodiscard]] QByteArray serialize() const;\n\t[[nodiscard]] static std::unique_ptr<Config> FromSerialized(\n\t\tconst QByteArray &serialized);\n\n\t[[nodiscard]] DcOptions &dcOptions() {\n\t\treturn _dcOptions;\n\t}\n\t[[nodiscard]] const DcOptions &dcOptions() const {\n\t\treturn _dcOptions;\n\t}\n\t[[nodiscard]] MTP::Environment environment() const {\n\t\treturn _dcOptions.environment();\n\t}\n\t[[nodiscard]] bool isTestMode() const {\n\t\treturn _dcOptions.isTestMode();\n\t}\n\n\tvoid apply(const MTPDconfig &data);\n\n\t[[nodiscard]] const ConfigFields &values() const;\n\t[[nodiscard]] rpl::producer<> updates() const;\n\n\t// Set from legacy local stored values.\n\tvoid setChatSizeMax(int value);\n\tvoid setStickersRecentLimit(int value);\n\tvoid setMegagroupSizeMax(int value);\n\tvoid setTxtDomainString(const QString &value);\n\nprivate:\n\tDcOptions _dcOptions;\n\tConfigFields _fields;\n\n\trpl::event_stream<> _updates;\n\n};\n\n} // namespace MTP"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/mtproto_dc_options.h\"\n\n#include \"mtproto/details/mtproto_rsa_public_key.h\"\n#include \"mtproto/facade.h\"\n#include \"mtproto/connection_tcp.h\"\n#include \"storage/serialize_common.h\"\n\n#include <QtCore/QFile>\n#include <QtCore/QRegularExpression>\n\nnamespace MTP {\nnamespace {\n\nconstexpr auto kVersion = 2;\n\nusing namespace details;\n\nstruct BuiltInDc {\n\tint id;\n\tconst char *ip;\n\tint port;\n};\n\nconst BuiltInDc kBuiltInDcs[] = {\n\t{ 1, \"149.154.175.50\" , 443 },\n\t{ 2, \"149.154.167.51\" , 443 },\n\t{ 2, \"95.161.76.100\"  , 443 },\n\t{ 3, \"149.154.175.100\", 443 },\n\t{ 4, \"149.154.167.91\" , 443 },\n\t{ 5, \"149.154.171.5\"  , 443 },\n};\n\nconst BuiltInDc kBuiltInDcsIPv6[] = {\n\t{ 1, \"2001:0b28:f23d:f001:0000:0000:0000:000a\", 443 },\n\t{ 2, \"2001:067c:04e8:f002:0000:0000:0000:000a\", 443 },\n\t{ 3, \"2001:0b28:f23d:f003:0000:0000:0000:000a\", 443 },\n\t{ 4, \"2001:067c:04e8:f004:0000:0000:0000:000a\", 443 },\n\t{ 5, \"2001:0b28:f23f:f005:0000:0000:0000:000a\", 443 },\n};\n\nconst BuiltInDc kBuiltInDcsTest[] = {\n\t{ 1, \"149.154.175.10\" , 443 },\n\t{ 2, \"149.154.167.40\" , 443 },\n\t{ 3, \"149.154.175.117\", 443 }\n};\n\nconst BuiltInDc kBuiltInDcsIPv6Test[] = {\n\t{ 1, \"2001:0b28:f23d:f001:0000:0000:0000:000e\", 443 },\n\t{ 2, \"2001:067c:04e8:f002:0000:0000:0000:000e\", 443 },\n\t{ 3, \"2001:0b28:f23d:f003:0000:0000:0000:000e\", 443 }\n};\n\nconst char *kTestPublicRSAKeys[] = { \"\\\n-----BEGIN RSA PUBLIC KEY-----\\n\\\nMIIBCgKCAQEAyMEdY1aR+sCR3ZSJrtztKTKqigvO/vBfqACJLZtS7QMgCGXJ6XIR\\n\\\nyy7mx66W0/sOFa7/1mAZtEoIokDP3ShoqF4fVNb6XeqgQfaUHd8wJpDWHcR2OFwv\\n\\\nplUUI1PLTktZ9uW2WE23b+ixNwJjJGwBDJPQEQFBE+vfmH0JP503wr5INS1poWg/\\n\\\nj25sIWeYPHYeOrFp/eXaqhISP6G+q2IeTaWTXpwZj4LzXq5YOpk4bYEQ6mvRq7D1\\n\\\naHWfYmlEGepfaYR8Q0YqvvhYtMte3ITnuSJs171+GDqpdKcSwHnd6FudwGO4pcCO\\n\\\nj4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB\\n\\\n-----END RSA PUBLIC KEY-----\" };\n\nconst char *kPublicRSAKeys[] = { \"\\\n-----BEGIN RSA PUBLIC KEY-----\\n\\\nMIIBCgKCAQEA6LszBcC1LGzyr992NzE0ieY+BSaOW622Aa9Bd4ZHLl+TuFQ4lo4g\\n\\\n5nKaMBwK/BIb9xUfg0Q29/2mgIR6Zr9krM7HjuIcCzFvDtr+L0GQjae9H0pRB2OO\\n\\\n62cECs5HKhT5DZ98K33vmWiLowc621dQuwKWSQKjWf50XYFw42h21P2KXUGyp2y/\\n\\\n+aEyZ+uVgLLQbRA1dEjSDZ2iGRy12Mk5gpYc397aYp438fsJoHIgJ2lgMv5h7WY9\\n\\\nt6N/byY9Nw9p21Og3AoXSL2q/2IJ1WRUhebgAdGVMlV1fkuOQoEzR7EdpqtQD9Cs\\n\\\n5+bfo3Nhmcyvk5ftB0WkJ9z6bNZ7yxrP8wIDAQAB\\n\\\n-----END RSA PUBLIC KEY-----\" };\n\n} // namespace\n\nclass DcOptions::WriteLocker {\npublic:\n\tWriteLocker(not_null<DcOptions*> that)\n\t: _that(that)\n\t, _lock(&_that->_useThroughLockers) {\n\t}\n\n\tvoid unlock() {\n\t\t_lock.unlock();\n\t}\n\n\t~WriteLocker() {\n\t\t_that->computeCdnDcIds();\n\t}\n\nprivate:\n\tnot_null<DcOptions*> _that;\n\tQWriteLocker _lock;\n\n};\n\nclass DcOptions::ReadLocker {\npublic:\n\tReadLocker(not_null<const DcOptions*> that)\n\t: _lock(&that->_useThroughLockers) {\n\t}\n\n\tvoid unlock() {\n\t\t_lock.unlock();\n\t}\n\nprivate:\n\tQReadLocker _lock;\n\n};\n\nDcOptions::DcOptions(Environment environment)\n: _environment(environment) {\n\tconstructFromBuiltIn();\n}\n\nDcOptions::DcOptions(const DcOptions &other)\n: _environment(other._environment)\n, _data(other._data)\n, _cdnDcIds(other._cdnDcIds)\n, _publicKeys(other._publicKeys)\n, _cdnPublicKeys(other._cdnPublicKeys)\n, _immutable(other._immutable) {\n}\n\nDcOptions::~DcOptions() = default;\n\nbool DcOptions::ValidateSecret(bytes::const_span secret) {\n\t// See also TcpConnection::Protocol::Create.\n\treturn (secret.size() >= 21 && secret[0] == bytes::type(0xEE))\n\t\t|| (secret.size() == 17 && secret[0] == bytes::type(0xDD))\n\t\t|| (secret.size() == 16)\n\t\t|| secret.empty();\n}\n\nvoid DcOptions::readBuiltInPublicKeys() {\n\tconst auto builtin = (_environment == Environment::Test)\n\t\t? gsl::make_span(kTestPublicRSAKeys)\n\t\t: gsl::make_span(kPublicRSAKeys);\n\tfor (const auto key : builtin) {\n\t\tconst auto keyBytes = bytes::make_span(key, strlen(key));\n\t\tauto parsed = RSAPublicKey(keyBytes);\n\t\tif (parsed.valid()) {\n\t\t\t_publicKeys.emplace(parsed.fingerprint(), std::move(parsed));\n\t\t} else {\n\t\t\tLOG((\"MTP Error: could not read this public RSA key:\"));\n\t\t\tLOG((key));\n\t\t}\n\t}\n}\n\nEnvironment DcOptions::environment() const {\n\treturn _environment;\n}\n\nbool DcOptions::isTestMode() const {\n\treturn (_environment != Environment::Production);\n}\n\nvoid DcOptions::constructFromBuiltIn() {\n\tWriteLocker lock(this);\n\t_data.clear();\n\n\treadBuiltInPublicKeys();\n\n\tconst auto list = isTestMode()\n\t\t? gsl::make_span(kBuiltInDcsTest)\n\t\t: gsl::make_span(kBuiltInDcs).subspan(0);\n\tfor (const auto &entry : list) {\n\t\tconst auto flags = Flag::f_static | 0;\n\t\tapplyOneGuarded(entry.id, flags, entry.ip, entry.port, {});\n\t\tDEBUG_LOG((\"MTP Info: adding built in DC %1 connect option: %2:%3\"\n\t\t\t).arg(entry.id\n\t\t\t).arg(entry.ip\n\t\t\t).arg(entry.port));\n\t}\n\n\tconst auto listv6 = isTestMode()\n\t\t? gsl::make_span(kBuiltInDcsIPv6Test)\n\t\t: gsl::make_span(kBuiltInDcsIPv6).subspan(0);\n\tfor (const auto &entry : listv6) {\n\t\tconst auto flags = Flag::f_static | Flag::f_ipv6;\n\t\tapplyOneGuarded(entry.id, flags, entry.ip, entry.port, {});\n\t\tDEBUG_LOG((\"MTP Info: adding built in DC %1 IPv6 connect option: \"\n\t\t\t\"%2:%3\"\n\t\t\t).arg(entry.id\n\t\t\t).arg(entry.ip\n\t\t\t).arg(entry.port));\n\t}\n}\n\nvoid DcOptions::processFromList(\n\t\tconst QVector<MTPDcOption> &options,\n\t\tbool overwrite) {\n\tif (options.empty() || _immutable) {\n\t\treturn;\n\t}\n\n\tauto data = [&] {\n\t\tif (overwrite) {\n\t\t\treturn base::flat_map<DcId, std::vector<Endpoint>>();\n\t\t}\n\t\tReadLocker lock(this);\n\t\treturn _data;\n\t}();\n\tfor (auto &mtpOption : options) {\n\t\tif (mtpOption.type() != mtpc_dcOption) {\n\t\t\tLOG((\"Wrong type in DcOptions: %1\").arg(mtpOption.type()));\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto &option = mtpOption.c_dcOption();\n\t\tauto dcId = option.vid().v;\n\t\tauto flags = option.vflags().v;\n\t\tauto ip = std::string(\n\t\t\toption.vip_address().v.constData(),\n\t\t\toption.vip_address().v.size());\n\t\tauto port = option.vport().v;\n\t\tauto secret = bytes::make_vector(option.vsecret().value_or_empty());\n\t\tApplyOneOption(data, dcId, flags, ip, port, secret);\n\t}\n\n\tconst auto difference = [&] {\n\t\tWriteLocker lock(this);\n\t\tauto result = CountOptionsDifference(_data, data);\n\t\tif (!result.empty()) {\n\t\t\t_data = std::move(data);\n\t\t}\n\t\treturn result;\n\t}();\n\tfor (const auto dcId : difference) {\n\t\t_changed.fire_copy(dcId);\n\t}\n}\n\nvoid DcOptions::setFromList(const MTPVector<MTPDcOption> &options) {\n\tprocessFromList(options.v, true);\n}\n\nvoid DcOptions::addFromList(const MTPVector<MTPDcOption> &options) {\n\tprocessFromList(options.v, false);\n}\n\nvoid DcOptions::addFromOther(DcOptions &&options) {\n\tif (this == &options || _immutable) {\n\t\treturn;\n\t}\n\n\tauto idsChanged = std::vector<DcId>();\n\t{\n\t\tReadLocker lock(&options);\n\t\tif (options._data.empty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tidsChanged.reserve(options._data.size());\n\t\t{\n\t\t\tWriteLocker lock(this);\n\t\t\tconst auto changed = [&](const std::vector<Endpoint> &list) {\n\t\t\t\tauto result = false;\n\t\t\t\tfor (const auto &endpoint : list) {\n\t\t\t\t\tconst auto dcId = endpoint.id;\n\t\t\t\t\tconst auto flags = endpoint.flags;\n\t\t\t\t\tconst auto &ip = endpoint.ip;\n\t\t\t\t\tconst auto port = endpoint.port;\n\t\t\t\t\tconst auto &secret = endpoint.secret;\n\t\t\t\t\tif (applyOneGuarded(dcId, flags, ip, port, secret)) {\n\t\t\t\t\t\tresult = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t};\n\t\t\tfor (const auto &item : base::take(options._data)) {\n\t\t\t\tif (changed(item.second)) {\n\t\t\t\t\tidsChanged.push_back(item.first);\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (auto &item : options._cdnPublicKeys) {\n\t\t\t\tfor (auto &entry : item.second) {\n\t\t\t\t\t_cdnPublicKeys[item.first].insert(std::move(entry));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tfor (const auto dcId : idsChanged) {\n\t\t_changed.fire_copy(dcId);\n\t}\n}\n\nvoid DcOptions::constructAddOne(\n\t\tint id,\n\t\tFlags flags,\n\t\tconst std::string &ip,\n\t\tint port,\n\t\tconst bytes::vector &secret) {\n\tWriteLocker lock(this);\n\tapplyOneGuarded(BareDcId(id), flags, ip, port, secret);\n}\n\nbool DcOptions::applyOneGuarded(\n\t\tDcId dcId,\n\t\tFlags flags,\n\t\tconst std::string &ip,\n\t\tint port,\n\t\tconst bytes::vector &secret) {\n\treturn ApplyOneOption(_data, dcId, flags, ip, port, secret);\n}\n\nbool DcOptions::ApplyOneOption(\n\t\tbase::flat_map<DcId, std::vector<Endpoint>> &data,\n\t\tDcId dcId,\n\t\tFlags flags,\n\t\tconst std::string &ip,\n\t\tint port,\n\t\tconst bytes::vector &secret) {\n\tauto i = data.find(dcId);\n\tif (i != data.cend()) {\n\t\tfor (auto &endpoint : i->second) {\n\t\t\tif (endpoint.ip == ip && endpoint.port == port) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\ti->second.emplace_back(dcId, flags, ip, port, secret);\n\t} else {\n\t\tdata.emplace(dcId, std::vector<Endpoint>(\n\t\t\t1,\n\t\t\tEndpoint(dcId, flags, ip, port, secret)));\n\t}\n\treturn true;\n}\n\nstd::vector<DcId> DcOptions::CountOptionsDifference(\n\t\tconst base::flat_map<DcId, std::vector<Endpoint>> &a,\n\t\tconst base::flat_map<DcId, std::vector<Endpoint>> &b) {\n\tauto result = std::vector<DcId>();\n\tconst auto find = [](\n\t\t\tconst std::vector<Endpoint> &where,\n\t\t\tconst Endpoint &what) {\n\t\tfor (const auto &endpoint : where) {\n\t\t\tif (endpoint.ip == what.ip && endpoint.port == what.port) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\tconst auto equal = [&](\n\t\t\tconst std::vector<Endpoint> &m,\n\t\t\tconst std::vector<Endpoint> &n) {\n\t\tif (m.size() != n.size()) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (const auto &endpoint : m) {\n\t\t\tif (!find(n, endpoint)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t};\n\n\tauto i = begin(a);\n\tauto j = begin(b);\n\tconst auto max = std::numeric_limits<DcId>::max();\n\twhile (i != end(a) || j != end(b)) {\n\t\tconst auto aId = (i == end(a)) ? max : i->first;\n\t\tconst auto bId = (j == end(b)) ? max : j->first;\n\t\tif (aId < bId) {\n\t\t\tresult.push_back(aId);\n\t\t\t++i;\n\t\t} else if (bId < aId) {\n\t\t\tresult.push_back(bId);\n\t\t\t++j;\n\t\t} else {\n\t\t\tif (!equal(i->second, j->second)) {\n\t\t\t\tresult.push_back(aId);\n\t\t\t}\n\t\t\t++i;\n\t\t\t++j;\n\t\t}\n\t}\n\treturn result;\n}\n\nQByteArray DcOptions::serialize() const {\n\tif (_immutable) {\n\t\t// Don't write the overriden options to our settings.\n\t\treturn DcOptions(_environment).serialize();\n\t}\n\n\tReadLocker lock(this);\n\n\tauto size = sizeof(qint32);\n\n\t// Dc options.\n\tauto optionsCount = 0;\n\tsize += sizeof(qint32);\n\tfor (const auto &item : _data) {\n\t\tif (isTemporaryDcId(item.first)) {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const auto &endpoint : item.second) {\n\t\t\t++optionsCount;\n\t\t\t// id + flags + port\n\t\t\tsize += sizeof(qint32) + sizeof(qint32) + sizeof(qint32);\n\t\t\tsize += sizeof(qint32) + endpoint.ip.size();\n\t\t\tsize += sizeof(qint32) + endpoint.secret.size();\n\t\t}\n\t}\n\n\t// CDN public keys.\n\tauto count = 0;\n\tfor (auto &keysInDc : _cdnPublicKeys) {\n\t\tcount += keysInDc.second.size();\n\t}\n\tstruct SerializedPublicKey {\n\t\tDcId dcId;\n\t\tbytes::vector n;\n\t\tbytes::vector e;\n\t};\n\tstd::vector<SerializedPublicKey> publicKeys;\n\tpublicKeys.reserve(count);\n\tsize += sizeof(qint32);\n\tfor (const auto &keysInDc : _cdnPublicKeys) {\n\t\tfor (const auto &entry : keysInDc.second) {\n\t\t\tpublicKeys.push_back({\n\t\t\t\tkeysInDc.first,\n\t\t\t\tentry.second.getN(),\n\t\t\t\tentry.second.getE()\n\t\t\t});\n\t\t\tsize += sizeof(qint32)\n\t\t\t\t+ Serialize::bytesSize(publicKeys.back().n)\n\t\t\t\t+ Serialize::bytesSize(publicKeys.back().e);\n\t\t}\n\t}\n\n\tauto result = QByteArray();\n\tresult.reserve(size);\n\t{\n\t\tQDataStream stream(&result, QIODevice::WriteOnly);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\t\tstream << qint32(-kVersion);\n\n\t\t// Dc options.\n\t\tstream << qint32(optionsCount);\n\t\tfor (const auto &item : _data) {\n\t\t\tif (isTemporaryDcId(item.first)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const auto &endpoint : item.second) {\n\t\t\t\tstream << qint32(endpoint.id)\n\t\t\t\t\t<< qint32(endpoint.flags)\n\t\t\t\t\t<< qint32(endpoint.port)\n\t\t\t\t\t<< qint32(endpoint.ip.size());\n\t\t\t\tstream.writeRawData(endpoint.ip.data(), endpoint.ip.size());\n\t\t\t\tstream << qint32(endpoint.secret.size());\n\t\t\t\tstream.writeRawData(\n\t\t\t\t\treinterpret_cast<const char*>(endpoint.secret.data()),\n\t\t\t\t\tendpoint.secret.size());\n\t\t\t}\n\t\t}\n\n\t\t// CDN public keys.\n\t\tstream << qint32(publicKeys.size());\n\t\tfor (auto &key : publicKeys) {\n\t\t\tstream << qint32(key.dcId)\n\t\t\t\t<< Serialize::bytes(key.n)\n\t\t\t\t<< Serialize::bytes(key.e);\n\t\t}\n\t}\n\treturn result;\n}\n\nbool DcOptions::constructFromSerialized(const QByteArray &serialized) {\n\tQDataStream stream(serialized);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\n\tauto minusVersion = qint32(0);\n\tstream >> minusVersion;\n\tconst auto version = (minusVersion < 0) ? (-minusVersion) : 0;\n\n\tauto count = qint32(0);\n\tif (version > 0) {\n\t\tstream >> count;\n\t} else {\n\t\tcount = minusVersion;\n\t}\n\tif (stream.status() != QDataStream::Ok) {\n\t\tLOG((\"MTP Error: Bad data for DcOptions::constructFromSerialized()\"));\n\t\treturn false;\n\t}\n\n\tWriteLocker lock(this);\n\t_data.clear();\n\tfor (auto i = 0; i != count; ++i) {\n\t\tqint32 id = 0, flags = 0, port = 0, ipSize = 0;\n\t\tstream >> id >> flags >> port >> ipSize;\n\n\t\t// https://stackoverflow.com/questions/1076714/max-length-for-client-ip-address\n\t\tconstexpr auto kMaxIpSize = 45;\n\t\tif (ipSize <= 0 || ipSize > kMaxIpSize) {\n\t\t\tLOG((\"MTP Error: Bad data inside DcOptions::constructFromSerialized()\"));\n\t\t\treturn false;\n\t\t}\n\n\t\tauto ip = std::string(ipSize, ' ');\n\t\tstream.readRawData(ip.data(), ipSize);\n\n\t\tconstexpr auto kMaxSecretSize = 32;\n\t\tauto secret = bytes::vector();\n\t\tif (version > 0) {\n\t\t\tauto secretSize = qint32(0);\n\t\t\tstream >> secretSize;\n\t\t\tif (secretSize < 0 || secretSize > kMaxSecretSize) {\n\t\t\t\tLOG((\"MTP Error: Bad data inside DcOptions::constructFromSerialized()\"));\n\t\t\t\treturn false;\n\t\t\t} else if (secretSize > 0) {\n\t\t\t\tsecret.resize(secretSize);\n\t\t\t\tstream.readRawData(\n\t\t\t\t\treinterpret_cast<char*>(secret.data()),\n\t\t\t\t\tsecretSize);\n\t\t\t}\n\t\t}\n\n\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\tLOG((\"MTP Error: Bad data inside DcOptions::constructFromSerialized()\"));\n\t\t\treturn false;\n\t\t}\n\n\t\tapplyOneGuarded(\n\t\t\tDcId(id),\n\t\t\tFlags::from_raw(flags),\n\t\t\tip,\n\t\t\tport,\n\t\t\tsecret);\n\t}\n\n\t// Read CDN config\n\tif (!stream.atEnd() && version > 1) {\n\t\tauto count = qint32(0);\n\t\tstream >> count;\n\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\tLOG((\"MTP Error: Bad data for CDN config in DcOptions::constructFromSerialized()\"));\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tqint32 dcId = 0;\n\t\t\tbytes::vector n, e;\n\t\t\tstream >> dcId >> Serialize::bytes(n) >> Serialize::bytes(e);\n\t\t\tif (stream.status() != QDataStream::Ok) {\n\t\t\t\tLOG((\"MTP Error: Bad data for CDN config inside DcOptions::constructFromSerialized()\"));\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tauto key = RSAPublicKey(n, e);\n\t\t\tif (key.valid()) {\n\t\t\t\t_cdnPublicKeys[dcId].emplace(key.fingerprint(), std::move(key));\n\t\t\t} else {\n\t\t\t\tLOG((\"MTP Error: Could not read valid CDN public key.\"));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\treturn true;\n}\n\nrpl::producer<DcId> DcOptions::changed() const {\n\treturn _changed.events();\n}\n\nrpl::producer<> DcOptions::cdnConfigChanged() const {\n\treturn _cdnConfigChanged.events();\n}\n\nstd::vector<DcId> DcOptions::configEnumDcIds() const {\n\tauto result = std::vector<DcId>();\n\t{\n\t\tReadLocker lock(this);\n\t\tresult.reserve(_data.size());\n\t\tfor (auto &item : _data) {\n\t\t\tconst auto dcId = item.first;\n\t\t\tAssert(!item.second.empty());\n\t\t\tif (!isCdnDc(item.second.front().flags)\n\t\t\t\t&& !isTemporaryDcId(dcId)) {\n\t\t\t\tresult.push_back(dcId);\n\t\t\t}\n\t\t}\n\t}\n\tranges::sort(result);\n\treturn result;\n}\n\nDcType DcOptions::dcType(ShiftedDcId shiftedDcId) const {\n\tif (isTemporaryDcId(shiftedDcId)) {\n\t\treturn DcType::Temporary;\n\t}\n\tReadLocker lock(this);\n\tif (_cdnDcIds.find(BareDcId(shiftedDcId)) != _cdnDcIds.cend()) {\n\t\treturn DcType::Cdn;\n\t}\n\tconst auto dcId = BareDcId(shiftedDcId);\n\tif (isMediaClusterDcId(shiftedDcId) && hasMediaOnlyOptionsFor(dcId)) {\n\t\treturn DcType::MediaCluster;\n\t}\n\treturn DcType::Regular;\n}\n\nvoid DcOptions::setCDNConfig(const MTPDcdnConfig &config) {\n\tWriteLocker lock(this);\n\t_cdnPublicKeys.clear();\n\tfor (const auto &key : config.vpublic_keys().v) {\n\t\tkey.match([&](const MTPDcdnPublicKey &data) {\n\t\t\tconst auto keyBytes = bytes::make_span(data.vpublic_key().v);\n\t\t\tauto key = RSAPublicKey(keyBytes);\n\t\t\tif (key.valid()) {\n\t\t\t\t_cdnPublicKeys[data.vdc_id().v].emplace(\n\t\t\t\t\tkey.fingerprint(),\n\t\t\t\t\tstd::move(key));\n\t\t\t} else {\n\t\t\t\tLOG((\"MTP Error: could not read this public RSA key:\"));\n\t\t\t\tLOG((qs(data.vpublic_key())));\n\t\t\t}\n\t\t});\n\t}\n\tlock.unlock();\n\n\t_cdnConfigChanged.fire({});\n}\n\nbool DcOptions::hasCDNKeysForDc(DcId dcId) const {\n\tReadLocker lock(this);\n\treturn _cdnPublicKeys.find(dcId) != _cdnPublicKeys.cend();\n}\n\nRSAPublicKey DcOptions::getDcRSAKey(\n\t\tDcId dcId,\n\t\tconst QVector<MTPlong> &fingerprints) const {\n\tconst auto findKey = [&](\n\t\t\tconst base::flat_map<uint64, RSAPublicKey> &keys) {\n\t\tfor (const auto &fingerprint : fingerprints) {\n\t\t\tconst auto it = keys.find(static_cast<uint64>(fingerprint.v));\n\t\t\tif (it != keys.cend()) {\n\t\t\t\treturn it->second;\n\t\t\t}\n\t\t}\n\t\treturn RSAPublicKey();\n\t};\n\t{\n\t\tReadLocker lock(this);\n\t\tconst auto it = _cdnPublicKeys.find(dcId);\n\t\tif (it != _cdnPublicKeys.cend()) {\n\t\t\treturn findKey(it->second);\n\t\t}\n\t}\n\treturn findKey(_publicKeys);\n}\n\nauto DcOptions::lookup(\n\t\tDcId dcId,\n\t\tDcType type,\n\t\tbool throughProxy) const -> Variants {\n\tusing Flag = Flag;\n\tauto result = Variants();\n\n\tReadLocker lock(this);\n\tconst auto i = _data.find(dcId);\n\tif (i == end(_data)) {\n\t\treturn result;\n\t}\n\tfor (const auto &endpoint : i->second) {\n\t\tconst auto flags = endpoint.flags;\n\t\tif (type == DcType::Cdn && !(flags & Flag::f_cdn)) {\n\t\t\tcontinue;\n\t\t} else if (type != DcType::MediaCluster\n\t\t\t&& (flags & Flag::f_media_only)) {\n\t\t\tcontinue;\n\t\t} else if (!ValidateSecret(endpoint.secret)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto address = (flags & Flag::f_ipv6)\n\t\t\t? Variants::IPv6\n\t\t\t: Variants::IPv4;\n\t\tresult.data[address][Variants::Tcp].push_back(endpoint);\n\t\tif (!(flags & (Flag::f_tcpo_only | Flag::f_secret))) {\n\t\t\tresult.data[address][Variants::Http].push_back(endpoint);\n\t\t}\n\t}\n\tif (type == DcType::MediaCluster) {\n\t\tFilterIfHasWithFlag(result, Flag::f_media_only);\n\t}\n\tif (throughProxy) {\n\t\tFilterIfHasWithFlag(result, Flag::f_static);\n\t}\n\treturn result;\n}\n\nbool DcOptions::hasMediaOnlyOptionsFor(DcId dcId) const {\n\tReadLocker lock(this);\n\tconst auto i = _data.find(dcId);\n\tif (i == end(_data)) {\n\t\treturn false;\n\t}\n\tfor (const auto &endpoint : i->second) {\n\t\tconst auto flags = endpoint.flags;\n\t\tif (flags & Flag::f_media_only) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid DcOptions::FilterIfHasWithFlag(Variants &variants, Flag flag) {\n\tconst auto is = [&](const Endpoint &endpoint) {\n\t\treturn (endpoint.flags & flag) != 0;\n\t};\n\tconst auto has = [&](const std::vector<Endpoint> &list) {\n\t\treturn ranges::any_of(list, is);\n\t};\n\tfor (auto &byAddress : variants.data) {\n\t\tfor (auto &list : byAddress) {\n\t\t\tif (has(list)) {\n\t\t\t\tlist = ranges::views::all(\n\t\t\t\t\tlist\n\t\t\t\t) | ranges::views::filter(\n\t\t\t\t\tis\n\t\t\t\t) | ranges::to_vector;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid DcOptions::computeCdnDcIds() {\n\t_cdnDcIds.clear();\n\tfor (auto &item : _data) {\n\t\tAssert(!item.second.empty());\n\t\tif (item.second.front().flags & Flag::f_cdn) {\n\t\t\t_cdnDcIds.insert(BareDcId(item.first));\n\t\t}\n\t}\n}\n\nbool DcOptions::loadFromFile(const QString &path) {\n\tQVector<MTPDcOption> options;\n\n\tQFile f(path);\n\tif (!f.open(QIODevice::ReadOnly)) {\n\t\tLOG((\"MTP Error: could not read '%1'\").arg(path));\n\t\treturn false;\n\t}\n\tQTextStream stream(&f);\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n\tstream.setCodec(\"UTF-8\");\n#endif // Qt < 6.0.0\n\twhile (!stream.atEnd()) {\n\t\tstatic const auto RegExp = QRegularExpression(R\"(\\s)\");\n\t\tauto line = stream.readLine();\n\t\tauto components = line.split(RegExp, Qt::SkipEmptyParts);\n\t\tif (components.isEmpty() || components[0].startsWith('#')) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto error = [line] {\n\t\t\tLOG((\"MTP Error: in .tdesktop-endpoints expected 'dcId host port [tcpo_only] [media_only]', got '%1'\").arg(line));\n\t\t\treturn false;\n\t\t};\n\t\tif (components.size() < 3) {\n\t\t\treturn error();\n\t\t}\n\t\tauto dcId = components[0].toInt();\n\t\tauto ip = components[1];\n\t\tauto port = components[2].toInt();\n\t\tauto host = QHostAddress();\n\t\tif (dcId <= 0 || dcId >= kDcShift || !host.setAddress(ip) || port <= 0) {\n\t\t\treturn error();\n\t\t}\n\t\tauto flags = Flags(0);\n\t\tif (host.protocol() == QAbstractSocket::IPv6Protocol) {\n\t\t\tflags |= Flag::f_ipv6;\n\t\t}\n\t\tfor (auto &option : components.mid(3)) {\n\t\t\tif (option.startsWith('#')) {\n\t\t\t\tbreak;\n\t\t\t} else if (option == u\"tcpo_only\"_q) {\n\t\t\t\tflags |= Flag::f_tcpo_only;\n\t\t\t} else if (option == u\"media_only\"_q) {\n\t\t\t\tflags |= Flag::f_media_only;\n\t\t\t} else {\n\t\t\t\treturn error();\n\t\t\t}\n\t\t}\n\t\toptions.push_back(MTP_dcOption(\n\t\t\tMTP_flags(flags),\n\t\t\tMTP_int(dcId),\n\t\t\tMTP_string(ip),\n\t\t\tMTP_int(port),\n\t\t\tMTPbytes()));\n\t}\n\tif (options.isEmpty()) {\n\t\tLOG((\"MTP Error: in .tdesktop-endpoints expected at least one endpoint being provided.\"));\n\t\treturn false;\n\t}\n\n\t_immutable = false;\n\tsetFromList(MTP_vector<MTPDcOption>(options));\n\t_immutable = true;\n\n\treturn true;\n}\n\nbool DcOptions::writeToFile(const QString &path) const {\n\tQFile f(path);\n\tif (!f.open(QIODevice::WriteOnly)) {\n\t\treturn false;\n\t}\n\tQTextStream stream(&f);\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n\tstream.setCodec(\"UTF-8\");\n#endif // Qt < 6.0.0\n\n\tReadLocker lock(this);\n\tfor (const auto &item : _data) {\n\t\tfor (const auto &option : item.second) {\n\t\t\tstream\n\t\t\t\t<< option.id\n\t\t\t\t<< ' '\n\t\t\t\t<< QString::fromStdString(option.ip)\n\t\t\t\t<< ' ' << option.port;\n\t\t\tif (option.flags & Flag::f_tcpo_only) {\n\t\t\t\tstream << \" tcpo_only\";\n\t\t\t}\n\t\t\tif (option.flags & Flag::f_media_only) {\n\t\t\t\tstream << \" media_only\";\n\t\t\t}\n\t\t\tstream << '\\n';\n\t\t}\n\t}\n\treturn true;\n}\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtproto_dc_options.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/bytes.h\"\n\n#include <QtCore/QReadWriteLock>\n#include <string>\n#include <vector>\n#include <map>\n#include <set>\n\nnamespace MTP {\nnamespace details {\nclass RSAPublicKey;\n} // namespace details\n\nenum class DcType {\n\tRegular,\n\tTemporary,\n\tMediaCluster,\n\tCdn,\n};\n\nenum class Environment : uchar {\n\tProduction,\n\tTest,\n};\n\nclass DcOptions {\npublic:\n\tusing Flag = MTPDdcOption::Flag;\n\tusing Flags = MTPDdcOption::Flags;\n\tstruct Endpoint {\n\t\tEndpoint(\n\t\t\tDcId id,\n\t\t\tFlags flags,\n\t\t\tconst std::string &ip,\n\t\t\tint port,\n\t\t\tconst bytes::vector &secret)\n\t\t\t: id(id)\n\t\t\t, flags(flags)\n\t\t\t, ip(ip)\n\t\t\t, port(port)\n\t\t\t, secret(secret) {\n\t\t}\n\n\t\tDcId id;\n\t\tFlags flags;\n\t\tstd::string ip;\n\t\tint port;\n\t\tbytes::vector secret;\n\n\t};\n\n\texplicit DcOptions(Environment environment);\n\tDcOptions(const DcOptions &other);\n\t~DcOptions();\n\n\t[[nodiscard]] static bool ValidateSecret(bytes::const_span secret);\n\n\t[[nodiscard]] Environment environment() const;\n\t[[nodiscard]] bool isTestMode() const;\n\n\t// construct methods don't notify \"changed\" subscribers.\n\tbool constructFromSerialized(const QByteArray &serialized);\n\tvoid constructFromBuiltIn();\n\tvoid constructAddOne(\n\t\tint id,\n\t\tFlags flags,\n\t\tconst std::string &ip,\n\t\tint port,\n\t\tconst bytes::vector &secret);\n\tQByteArray serialize() const;\n\n\t[[nodiscard]] rpl::producer<DcId> changed() const;\n\t[[nodiscard]] rpl::producer<> cdnConfigChanged() const;\n\tvoid setFromList(const MTPVector<MTPDcOption> &options);\n\tvoid addFromList(const MTPVector<MTPDcOption> &options);\n\tvoid addFromOther(DcOptions &&options);\n\n\t[[nodiscard]] std::vector<DcId> configEnumDcIds() const;\n\n\tstruct Variants {\n\t\tenum Address {\n\t\t\tIPv4 = 0,\n\t\t\tIPv6 = 1,\n\t\t\tAddressTypeCount = 2,\n\t\t};\n\t\tenum Protocol {\n\t\t\tTcp = 0,\n\t\t\tHttp = 1,\n\t\t\tProtocolCount = 2,\n\t\t};\n\t\tstd::vector<Endpoint> data[AddressTypeCount][ProtocolCount];\n\t};\n\t[[nodiscard]] Variants lookup(\n\t\tDcId dcId,\n\t\tDcType type,\n\t\tbool throughProxy) const;\n\t[[nodiscard]] DcType dcType(ShiftedDcId shiftedDcId) const;\n\n\tvoid setCDNConfig(const MTPDcdnConfig &config);\n\t[[nodiscard]] bool hasCDNKeysForDc(DcId dcId) const;\n\t[[nodiscard]] details::RSAPublicKey getDcRSAKey(\n\t\tDcId dcId,\n\t\tconst QVector<MTPlong> &fingerprints) const;\n\n\t// Debug feature for now.\n\tbool loadFromFile(const QString &path);\n\tbool writeToFile(const QString &path) const;\n\nprivate:\n\tbool applyOneGuarded(\n\t\tDcId dcId,\n\t\tFlags flags,\n\t\tconst std::string &ip,\n\t\tint port,\n\t\tconst bytes::vector &secret);\n\tstatic bool ApplyOneOption(\n\t\tbase::flat_map<DcId, std::vector<Endpoint>> &data,\n\t\tDcId dcId,\n\t\tFlags flags,\n\t\tconst std::string &ip,\n\t\tint port,\n\t\tconst bytes::vector &secret);\n\tstatic std::vector<DcId> CountOptionsDifference(\n\t\tconst base::flat_map<DcId, std::vector<Endpoint>> &a,\n\t\tconst base::flat_map<DcId, std::vector<Endpoint>> &b);\n\tstatic void FilterIfHasWithFlag(Variants &variants, Flag flag);\n\n\t[[nodiscard]] bool hasMediaOnlyOptionsFor(DcId dcId) const;\n\n\tvoid processFromList(const QVector<MTPDcOption> &options, bool overwrite);\n\tvoid computeCdnDcIds();\n\n\tvoid readBuiltInPublicKeys();\n\n\tclass WriteLocker;\n\tfriend class WriteLocker;\n\n\tclass ReadLocker;\n\tfriend class ReadLocker;\n\n\tconst Environment _environment = Environment();\n\tbase::flat_map<DcId, std::vector<Endpoint>> _data;\n\tbase::flat_set<DcId> _cdnDcIds;\n\tbase::flat_map<uint64, details::RSAPublicKey> _publicKeys;\n\tbase::flat_map<\n\t\tDcId,\n\t\tbase::flat_map<uint64, details::RSAPublicKey>> _cdnPublicKeys;\n\tmutable QReadWriteLock _useThroughLockers;\n\n\trpl::event_stream<DcId> _changed;\n\trpl::event_stream<> _cdnConfigChanged;\n\n\t// True when we have overriden options from a .tdesktop-endpoints file.\n\tbool _immutable = false;\n\n};\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtproto_dh_utils.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/mtproto_dh_utils.h\"\n\n#include \"base/openssl_help.h\"\n\nnamespace MTP {\nnamespace {\n\nconstexpr auto kMaxModExpSize = 256;\n\nbool IsPrimeAndGoodCheck(const openssl::BigNum &prime, int g) {\n\tconstexpr auto kGoodPrimeBitsCount = 2048;\n\n\tif (prime.failed()\n\t\t|| prime.isNegative()\n\t\t|| prime.bitsSize() != kGoodPrimeBitsCount) {\n\t\tLOG((\"MTP Error: Bad prime bits count %1, expected %2.\"\n\t\t\t).arg(prime.bitsSize()\n\t\t\t).arg(kGoodPrimeBitsCount));\n\t\treturn false;\n\t}\n\n\tconst auto context = openssl::Context();\n\tif (!prime.isPrime(context)) {\n\t\tLOG((\"MTP Error: Bad prime.\"));\n\t\treturn false;\n\t}\n\n\tswitch (g) {\n\tcase 2: {\n\t\tconst auto mod8 = prime.countModWord(8);\n\t\tif (mod8 != 7) {\n\t\t\tLOG((\"BigNum PT Error: bad g value: %1, mod8: %2\").arg(g).arg(mod8));\n\t\t\treturn false;\n\t\t}\n\t} break;\n\tcase 3: {\n\t\tconst auto mod3 = prime.countModWord(3);\n\t\tif (mod3 != 2) {\n\t\t\tLOG((\"BigNum PT Error: bad g value: %1, mod3: %2\").arg(g).arg(mod3));\n\t\t\treturn false;\n\t\t}\n\t} break;\n\tcase 4: break;\n\tcase 5: {\n\t\tconst auto mod5 = prime.countModWord(5);\n\t\tif (mod5 != 1 && mod5 != 4) {\n\t\t\tLOG((\"BigNum PT Error: bad g value: %1, mod5: %2\").arg(g).arg(mod5));\n\t\t\treturn false;\n\t\t}\n\t} break;\n\tcase 6: {\n\t\tconst auto mod24 = prime.countModWord(24);\n\t\tif (mod24 != 19 && mod24 != 23) {\n\t\t\tLOG((\"BigNum PT Error: bad g value: %1, mod24: %2\").arg(g).arg(mod24));\n\t\t\treturn false;\n\t\t}\n\t} break;\n\tcase 7: {\n\t\tconst auto mod7 = prime.countModWord(7);\n\t\tif (mod7 != 3 && mod7 != 5 && mod7 != 6) {\n\t\t\tLOG((\"BigNum PT Error: bad g value: %1, mod7: %2\").arg(g).arg(mod7));\n\t\t\treturn false;\n\t\t}\n\t} break;\n\tdefault: {\n\t\tLOG((\"BigNum PT Error: bad g value: %1\").arg(g));\n\t\treturn false;\n\t} break;\n\t}\n\n\tif (!openssl::BigNum(prime).subWord(1).divWord(2).isPrime(context)) {\n\t\tLOG((\"MTP Error: Bad (prime - 1) / 2.\"));\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\n} // namespace\n\nbool IsGoodModExpFirst(\n\t\tconst openssl::BigNum &modexp,\n\t\tconst openssl::BigNum &prime) {\n\tconst auto diff = openssl::BigNum::Sub(prime, modexp);\n\tif (modexp.failed() || prime.failed() || diff.failed()) {\n\t\treturn false;\n\t}\n\tconstexpr auto kMinDiffBitsCount = 2048 - 64;\n\tif (diff.isNegative()\n\t\t|| diff.bitsSize() < kMinDiffBitsCount\n\t\t|| modexp.bitsSize() < kMinDiffBitsCount\n\t\t|| modexp.bytesSize() > kMaxModExpSize) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nbool IsPrimeAndGood(bytes::const_span primeBytes, int g) {\n\tstatic constexpr unsigned char GoodPrime[] = {\n\t\t0xC7, 0x1C, 0xAE, 0xB9, 0xC6, 0xB1, 0xC9, 0x04, 0x8E, 0x6C, 0x52, 0x2F, 0x70, 0xF1, 0x3F, 0x73,\n\t\t0x98, 0x0D, 0x40, 0x23, 0x8E, 0x3E, 0x21, 0xC1, 0x49, 0x34, 0xD0, 0x37, 0x56, 0x3D, 0x93, 0x0F,\n\t\t0x48, 0x19, 0x8A, 0x0A, 0xA7, 0xC1, 0x40, 0x58, 0x22, 0x94, 0x93, 0xD2, 0x25, 0x30, 0xF4, 0xDB,\n\t\t0xFA, 0x33, 0x6F, 0x6E, 0x0A, 0xC9, 0x25, 0x13, 0x95, 0x43, 0xAE, 0xD4, 0x4C, 0xCE, 0x7C, 0x37,\n\t\t0x20, 0xFD, 0x51, 0xF6, 0x94, 0x58, 0x70, 0x5A, 0xC6, 0x8C, 0xD4, 0xFE, 0x6B, 0x6B, 0x13, 0xAB,\n\t\t0xDC, 0x97, 0x46, 0x51, 0x29, 0x69, 0x32, 0x84, 0x54, 0xF1, 0x8F, 0xAF, 0x8C, 0x59, 0x5F, 0x64,\n\t\t0x24, 0x77, 0xFE, 0x96, 0xBB, 0x2A, 0x94, 0x1D, 0x5B, 0xCD, 0x1D, 0x4A, 0xC8, 0xCC, 0x49, 0x88,\n\t\t0x07, 0x08, 0xFA, 0x9B, 0x37, 0x8E, 0x3C, 0x4F, 0x3A, 0x90, 0x60, 0xBE, 0xE6, 0x7C, 0xF9, 0xA4,\n\t\t0xA4, 0xA6, 0x95, 0x81, 0x10, 0x51, 0x90, 0x7E, 0x16, 0x27, 0x53, 0xB5, 0x6B, 0x0F, 0x6B, 0x41,\n\t\t0x0D, 0xBA, 0x74, 0xD8, 0xA8, 0x4B, 0x2A, 0x14, 0xB3, 0x14, 0x4E, 0x0E, 0xF1, 0x28, 0x47, 0x54,\n\t\t0xFD, 0x17, 0xED, 0x95, 0x0D, 0x59, 0x65, 0xB4, 0xB9, 0xDD, 0x46, 0x58, 0x2D, 0xB1, 0x17, 0x8D,\n\t\t0x16, 0x9C, 0x6B, 0xC4, 0x65, 0xB0, 0xD6, 0xFF, 0x9C, 0xA3, 0x92, 0x8F, 0xEF, 0x5B, 0x9A, 0xE4,\n\t\t0xE4, 0x18, 0xFC, 0x15, 0xE8, 0x3E, 0xBE, 0xA0, 0xF8, 0x7F, 0xA9, 0xFF, 0x5E, 0xED, 0x70, 0x05,\n\t\t0x0D, 0xED, 0x28, 0x49, 0xF4, 0x7B, 0xF9, 0x59, 0xD9, 0x56, 0x85, 0x0C, 0xE9, 0x29, 0x85, 0x1F,\n\t\t0x0D, 0x81, 0x15, 0xF6, 0x35, 0xB1, 0x05, 0xEE, 0x2E, 0x4E, 0x15, 0xD0, 0x4B, 0x24, 0x54, 0xBF,\n\t\t0x6F, 0x4F, 0xAD, 0xF0, 0x34, 0xB1, 0x04, 0x03, 0x11, 0x9C, 0xD8, 0xE3, 0xB9, 0x2F, 0xCC, 0x5B };\n\n\tif (!bytes::compare(bytes::make_span(GoodPrime), primeBytes)) {\n\t\tif (g == 3 || g == 4 || g == 5 || g == 7) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\treturn IsPrimeAndGoodCheck(openssl::BigNum(primeBytes), g);\n}\n\nModExpFirst CreateModExp(\n\t\tint g,\n\t\tbytes::const_span primeBytes,\n\t\tbytes::const_span randomSeed) {\n\tExpects(randomSeed.size() == ModExpFirst::kRandomPowerSize);\n\n\tusing namespace openssl;\n\n\tBigNum prime(primeBytes);\n\tauto result = ModExpFirst();\n\tresult.randomPower.resize(ModExpFirst::kRandomPowerSize);\n\twhile (true) {\n\t\tbytes::set_random(result.randomPower);\n\t\tfor (auto i = 0; i != ModExpFirst::kRandomPowerSize; ++i) {\n\t\t\tresult.randomPower[i] ^= randomSeed[i];\n\t\t}\n\t\tconst auto modexp = BigNum::ModExp(\n\t\t\tBigNum(g),\n\t\t\tBigNum(result.randomPower),\n\t\t\tprime);\n\t\tif (IsGoodModExpFirst(modexp, prime)) {\n\t\t\tresult.modexp = modexp.getBytes();\n\t\t\treturn result;\n\t\t}\n\t}\n}\n\nbytes::vector CreateAuthKey(\n\t\tbytes::const_span firstBytes,\n\t\tbytes::const_span randomBytes,\n\t\tbytes::const_span primeBytes) {\n\tusing openssl::BigNum;\n\n\tconst auto first = BigNum(firstBytes);\n\tconst auto prime = BigNum(primeBytes);\n\tif (!IsGoodModExpFirst(first, prime)) {\n\t\tLOG((\"AuthKey Error: Bad first prime in CreateAuthKey().\"));\n\t\treturn {};\n\t}\n\treturn BigNum::ModExp(first, BigNum(randomBytes), prime).getBytes();\n}\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtproto_dh_utils.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/bytes.h\"\n\nnamespace openssl {\nclass BigNum;\n} // namespace openssl\n\nnamespace MTP {\n\nstruct ModExpFirst {\n\tstatic constexpr auto kRandomPowerSize = 256;\n\n\tbytes::vector modexp;\n\tbytes::vector randomPower;\n};\n\n[[nodiscard]] bool IsPrimeAndGood(bytes::const_span primeBytes, int g);\n[[nodiscard]] bool IsGoodModExpFirst(\n\tconst openssl::BigNum &modexp,\n\tconst openssl::BigNum &prime);\n[[nodiscard]] ModExpFirst CreateModExp(\n\tint g,\n\tbytes::const_span primeBytes,\n\tbytes::const_span randomSeed);\n[[nodiscard]] bytes::vector CreateAuthKey(\n\tbytes::const_span firstBytes,\n\tbytes::const_span randomBytes,\n\tbytes::const_span primeBytes);\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtproto_pch.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include <QtCore/QObject>\n#include <QtCore/QThread>\n#include <QtNetwork/QNetworkProxy>\n#include <QtNetwork/QTcpSocket>\n\n#include <range/v3/all.hpp>\n\n#include <rpl/rpl.h>\n#include <crl/crl.h>\n\n#include \"base/bytes.h\"\n#include \"base/flat_map.h\"\n#include \"base/flat_set.h\"\n\n#include \"logs.h\"\n#include \"scheme.h\"\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/mtproto_proxy_data.h\"\n\n#include \"base/qthelp_url.h\"\n#include \"base/qt/qt_string_view.h\"\n\nnamespace MTP {\nnamespace {\n\n[[nodiscard]] bool IsHexMtprotoPassword(const QString &password) {\n\tconst auto size = password.size();\n\tif (size < 32 || size % 2 == 1) {\n\t\treturn false;\n\t}\n\tconst auto bad = [](QChar ch) {\n\t\tconst auto code = ch.unicode();\n\t\treturn (code < 'a' || code > 'f')\n\t\t\t&& (code < 'A' || code > 'F')\n\t\t\t&& (code < '0' || code > '9');\n\t};\n\tconst auto i = std::find_if(password.begin(), password.end(), bad);\n\treturn (i == password.end());\n}\n\n[[nodiscard]] ProxyData::Status HexMtprotoPasswordStatus(\n\t\tconst QString &password) {\n\tconst auto size = password.size() / 2;\n\tconst auto type1 = password[0].toLower();\n\tconst auto type2 = password[1].toLower();\n\tconst auto valid = (size == 16)\n\t\t|| (size == 17 && (type1 == 'd') && (type2 == 'd'))\n\t\t|| (size >= 21 && (type1 == 'e') && (type2 == 'e'));\n\tif (valid) {\n\t\treturn ProxyData::Status::Valid;\n\t} else if (size < 16) {\n\t\treturn ProxyData::Status::Invalid;\n\t}\n\treturn ProxyData::Status::Unsupported;\n}\n\n[[nodiscard]] bytes::vector SecretFromHexMtprotoPassword(\n\t\tconst QString &password) {\n\tExpects(password.size() % 2 == 0);\n\n\tconst auto size = password.size() / 2;\n\tconst auto fromHex = [](QChar ch) -> int {\n\t\tconst auto code = int(ch.unicode());\n\t\tif (code >= '0' && code <= '9') {\n\t\t\treturn (code - '0');\n\t\t} else if (code >= 'A' && code <= 'F') {\n\t\t\treturn 10 + (code - 'A');\n\t\t} else if (ch >= 'a' && ch <= 'f') {\n\t\t\treturn 10 + (code - 'a');\n\t\t}\n\t\tUnexpected(\"Code in ProxyData fromHex.\");\n\t};\n\tauto result = bytes::vector(size);\n\tfor (auto i = 0; i != size; ++i) {\n\t\tconst auto high = fromHex(password[2 * i]);\n\t\tconst auto low = fromHex(password[2 * i + 1]);\n\t\tif (high < 0 || low < 0) {\n\t\t\treturn {};\n\t\t}\n\t\tresult[i] = static_cast<bytes::type>(high * 16 + low);\n\t}\n\treturn result;\n}\n\n[[nodiscard]] QStringView Base64UrlInner(const QString &password) {\n\tExpects(password.size() > 2);\n\n\t// Skip one or two '=' at the end of the string.\n\treturn base::StringViewMid(password, 0, [&] {\n\t\tauto result = password.size();\n\t\tfor (auto i = 0; i != 2; ++i) {\n\t\t\tconst auto prev = result - 1;\n\t\t\tif (password[prev] != '=') {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tresult = prev;\n\t\t}\n\t\treturn result;\n\t}());\n}\n\n[[nodiscard]] bool IsBase64UrlMtprotoPassword(const QString &password) {\n\tconst auto size = password.size();\n\tif (size < 22 || size % 4 == 1) {\n\t\treturn false;\n\t}\n\tconst auto bad = [](QChar ch) {\n\t\tconst auto code = ch.unicode();\n\t\treturn (code < 'a' || code > 'z')\n\t\t\t&& (code < 'A' || code > 'Z')\n\t\t\t&& (code < '0' || code > '9')\n\t\t\t&& (code != '_')\n\t\t\t&& (code != '-');\n\t};\n\tconst auto inner = Base64UrlInner(password);\n\tconst auto begin = inner.data();\n\tconst auto end = begin + inner.size();\n\treturn (std::find_if(begin, end, bad) == end);\n}\n\n[[nodiscard]] ProxyData::Status Base64UrlMtprotoPasswordStatus(\n\t\tconst QString &password) {\n\t// IncorrectSecret\n\tconst auto inner = Base64UrlInner(password);\n\tconst auto size = (inner.size() * 3) / 4;\n\tconst auto valid = (size == 16)\n\t\t|| (size == 17\n\t\t\t&& (password[0] == '3')\n\t\t\t&& ((password[1] >= 'Q' && password[1] <= 'Z')\n\t\t\t\t|| (password[1] >= 'a' && password[1] <= 'f')))\n\t\t|| (size >= 21\n\t\t\t&& (password[0] == '7')\n\t\t\t&& (password[1] >= 'g')\n\t\t\t&& (password[1] <= 'v'));\n\tconst auto incorrect = (size >= 21\n\t\t&& password[0].toLower() == 'e'\n\t\t&& password[1].toLower() == 'e');\n\tif (size < 16) {\n\t\treturn ProxyData::Status::Invalid;\n\t} else if (valid) {\n\t\treturn ProxyData::Status::Valid;\n\t} else if (incorrect) {\n\t\treturn ProxyData::Status::IncorrectSecret;\n\t}\n\treturn ProxyData::Status::Unsupported;\n}\n\n[[nodiscard]] bytes::vector SecretFromBase64UrlMtprotoPassword(\n\t\tconst QString &password) {\n\tconst auto result = QByteArray::fromBase64(\n\t\tpassword.toLatin1(),\n\t\tQByteArray::Base64UrlEncoding);\n\treturn bytes::make_vector(bytes::make_span(result));\n}\n\n} // namespace\n\nbool ProxyData::valid() const {\n\treturn status() == Status::Valid;\n}\n\nProxyData::Status ProxyData::status() const {\n\tif (type == Type::None || host.isEmpty() || !port) {\n\t\treturn Status::Invalid;\n\t} else if (type == Type::Mtproto) {\n\t\treturn MtprotoPasswordStatus(password);\n\t}\n\treturn Status::Valid;\n}\n\nbool ProxyData::supportsCalls() const {\n\treturn false;// (type == Type::Socks5);\n}\n\nbool ProxyData::tryCustomResolve() const {\n\tstatic const auto RegExp = QRegularExpression(\n\t\tQStringLiteral(\"^\\\\d+\\\\.\\\\d+\\\\.\\\\d+\\\\.\\\\d+$\")\n\t);\n\treturn (type == Type::Socks5 || type == Type::Mtproto)\n\t\t&& !qthelp::is_ipv6(host)\n\t\t&& !RegExp.match(host).hasMatch();\n}\n\nbytes::vector ProxyData::secretFromMtprotoPassword() const {\n\tExpects(type == Type::Mtproto);\n\n\tif (IsHexMtprotoPassword(password)) {\n\t\treturn SecretFromHexMtprotoPassword(password);\n\t} else if (IsBase64UrlMtprotoPassword(password)) {\n\t\treturn SecretFromBase64UrlMtprotoPassword(password);\n\t}\n\treturn {};\n}\n\nProxyData::operator bool() const {\n\treturn valid();\n}\n\nbool ProxyData::operator==(const ProxyData &other) const {\n\tif (!valid()) {\n\t\treturn !other.valid();\n\t}\n\treturn (type == other.type)\n\t\t&& (host == other.host)\n\t\t&& (port == other.port)\n\t\t&& (user == other.user)\n\t\t&& (password == other.password);\n}\n\nbool ProxyData::operator!=(const ProxyData &other) const {\n\treturn !(*this == other);\n}\n\nbool ProxyData::ValidMtprotoPassword(const QString &password) {\n\treturn MtprotoPasswordStatus(password) == Status::Valid;\n}\n\nProxyData::Status ProxyData::MtprotoPasswordStatus(const QString &password) {\n\tif (IsHexMtprotoPassword(password)) {\n\t\treturn HexMtprotoPasswordStatus(password);\n\t} else if (IsBase64UrlMtprotoPassword(password)) {\n\t\treturn Base64UrlMtprotoPasswordStatus(password);\n\t}\n\treturn Status::Invalid;\n}\n\nProxyData ToDirectIpProxy(const ProxyData &proxy, int ipIndex) {\n\tif (!proxy.tryCustomResolve()\n\t\t|| ipIndex < 0\n\t\t|| ipIndex >= proxy.resolvedIPs.size()) {\n\t\treturn proxy;\n\t}\n\treturn {\n\t\tproxy.type,\n\t\tproxy.resolvedIPs[ipIndex],\n\t\tproxy.port,\n\t\tproxy.user,\n\t\tproxy.password\n\t};\n}\n\nQNetworkProxy ToNetworkProxy(const ProxyData &proxy) {\n\tif (proxy.type == ProxyData::Type::None) {\n\t\treturn QNetworkProxy::DefaultProxy;\n\t} else if (proxy.type == ProxyData::Type::Mtproto) {\n\t\treturn QNetworkProxy::NoProxy;\n\t}\n\treturn QNetworkProxy(\n\t\t(proxy.type == ProxyData::Type::Socks5\n\t\t\t? QNetworkProxy::Socks5Proxy\n\t\t\t: QNetworkProxy::HttpProxy),\n\t\tproxy.host,\n\t\tproxy.port,\n\t\tproxy.user,\n\t\tproxy.password);\n}\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtproto_proxy_data.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace MTP {\n\nstruct ProxyData {\n\tenum class Settings {\n\t\tSystem,\n\t\tEnabled,\n\t\tDisabled,\n\t};\n\tenum class Type {\n\t\tNone,\n\t\tSocks5,\n\t\tHttp,\n\t\tMtproto,\n\t};\n\tenum class Status {\n\t\tValid,\n\t\tUnsupported,\n\t\tIncorrectSecret,\n\t\tInvalid,\n\t};\n\n\tType type = Type::None;\n\tQString host;\n\tuint32 port = 0;\n\tQString user, password;\n\n\tstd::vector<QString> resolvedIPs;\n\tcrl::time resolvedExpireAt = 0;\n\n\t[[nodiscard]] bool valid() const;\n\t[[nodiscard]] Status status() const;\n\t[[nodiscard]] bool supportsCalls() const;\n\t[[nodiscard]] bool tryCustomResolve() const;\n\t[[nodiscard]] bytes::vector secretFromMtprotoPassword() const;\n\t[[nodiscard]] explicit operator bool() const;\n\t[[nodiscard]] bool operator==(const ProxyData &other) const;\n\t[[nodiscard]] bool operator!=(const ProxyData &other) const;\n\n\t[[nodiscard]] static bool ValidMtprotoPassword(const QString &password);\n\t[[nodiscard]] static Status MtprotoPasswordStatus(\n\t\tconst QString &password);\n\n};\n\n[[nodiscard]] ProxyData ToDirectIpProxy(\n\tconst ProxyData &proxy,\n\tint ipIndex = 0);\n[[nodiscard]] QNetworkProxy ToNetworkProxy(const ProxyData &proxy);\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtproto_response.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/mtproto_response.h\"\n\n#include <QtCore/QRegularExpression>\n#include <QtCore/QDebug>\n\nnamespace MTP {\nnamespace {\n\n[[nodiscard]] MTPrpcError ParseError(const mtpBuffer &reply) {\n\tauto result = MTPRpcError();\n\tauto from = reply.constData();\n\treturn result.read(from, from + reply.size())\n\t\t? result\n\t\t: Error::MTPLocal(\"RESPONSE_PARSE_FAILED\", \"Error parse failed.\");\n}\n\n} // namespace\n\nError::Error(const MTPrpcError &error)\n: _code(error.c_rpc_error().verror_code().v) {\n\tQString text = qs(error.c_rpc_error().verror_message());\n\tstatic const auto Expression = QRegularExpression(\n\t\t\"^([A-Z0-9_]+)(: .*)?$\",\n\t\t(QRegularExpression::DotMatchesEverythingOption\n\t\t\t| QRegularExpression::MultilineOption));\n\tconst auto match = Expression.match(text);\n\tif (match.hasMatch()) {\n\t\t_type = match.captured(1);\n\t\t_description = match.captured(2).mid(2);\n\t} else if (_code < 0 || _code >= 500) {\n\t\t_type = \"INTERNAL_SERVER_ERROR\";\n\t\t_description = text;\n\t} else {\n\t\t_type = \"CLIENT_BAD_RPC_ERROR\";\n\t\t_description = \"Bad rpc error received, text = '\" + text + '\\'';\n\t}\n}\n\nError::Error(const mtpBuffer &reply) : Error(ParseError(reply)) {\n}\n\nint32 Error::code() const {\n\treturn _code;\n}\n\nconst QString &Error::type() const {\n\treturn _type;\n}\n\nconst QString &Error::description() const {\n\treturn _description;\n}\n\nMTPrpcError Error::MTPLocal(\n\t\tconst QString &type,\n\t\tconst QString &description) {\n\treturn MTP_rpc_error(\n\t\tMTP_int(0),\n\t\tMTP_bytes(\n\t\t\t(\"CLIENT_\"\n\t\t\t\t+ type\n\t\t\t\t+ (description.length()\n\t\t\t\t\t? (\": \" + description)\n\t\t\t\t\t: QString())).toUtf8()));\n}\n\nError Error::Local(\n\t\tconst QString &type,\n\t\tconst QString &description) {\n\treturn Error(MTPLocal(type, description));\n}\n\nQDebug operator<<(QDebug debug, const Error &error) {\n\treturn debug.nospace()\n\t\t<< \"MTP::Error(\"\n\t\t<< error.code()\n\t\t<< \", \"\n\t\t<< error.type()\n\t\t<< \", \"\n\t\t<< error.description()\n\t\t<< \")\";\n}\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/mtproto_response.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flat_set.h\"\n\nclass QDebug;\n\nnamespace MTP {\n\nclass Error {\npublic:\n\texplicit Error(const MTPrpcError &error);\n\texplicit Error(const mtpBuffer &reply);\n\n\tenum {\n\t\tNoError,\n\t\tTimeoutError\n\t};\n\n\t[[nodiscard]] int32 code() const;\n\t[[nodiscard]] const QString &type() const;\n\t[[nodiscard]] const QString &description() const;\n\n\t[[nodiscard]] static Error Local(\n\t\tconst QString &type,\n\t\tconst QString &description);\n\t[[nodiscard]] static MTPrpcError MTPLocal(\n\t\tconst QString &type,\n\t\tconst QString &description);\n\nprivate:\n\tint32 _code = 0;\n\tQString _type, _description;\n\n};\n\ninline bool IsFloodError(const QString &type) {\n\treturn type.startsWith(u\"FLOOD_WAIT_\"_q)\n\t\t|| type.startsWith(u\"FLOOD_PREMIUM_WAIT_\"_q);\n}\n\ninline bool IsFloodError(const Error &error) {\n\treturn IsFloodError(error.type());\n}\n\ninline bool IsTemporaryError(const Error &error) {\n\treturn error.code() < 0 || error.code() >= 500 || IsFloodError(error);\n}\n\ninline bool IsDefaultHandledError(const Error &error) {\n\treturn IsTemporaryError(error);\n}\n\nstruct Response {\n\tmtpBuffer reply;\n\tmtpMsgId outerMsgId = 0;\n\tmtpRequestId requestId = 0;\n};\n\nusing DoneHandler = FnMut<bool(const Response&)>;\nusing FailHandler = Fn<bool(const Error&, const Response&)>;\n\nstruct ResponseHandler {\n\tDoneHandler done;\n\tFailHandler fail;\n};\n\n[[nodiscard]] QDebug operator<<(QDebug debug, const Error &error);\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/scheme/api.tl",
    "content": "boolFalse#bc799737 = Bool;\nboolTrue#997275b5 = Bool;\n\ntrue#3fedd339 = True;\n\nvector#1cb5c415 {t:Type} # [ t ] = Vector t;\n\nerror#c4b9f9bb code:int text:string = Error;\n\nnull#56730bcc = Null;\n\ninputPeerEmpty#7f3b18ea = InputPeer;\ninputPeerSelf#7da07ec9 = InputPeer;\ninputPeerChat#35a95cb9 chat_id:long = InputPeer;\ninputPeerUser#dde8a54c user_id:long access_hash:long = InputPeer;\ninputPeerChannel#27bcbbfc channel_id:long access_hash:long = InputPeer;\ninputPeerUserFromMessage#a87b0a1c peer:InputPeer msg_id:int user_id:long = InputPeer;\ninputPeerChannelFromMessage#bd2a0840 peer:InputPeer msg_id:int channel_id:long = InputPeer;\n\ninputUserEmpty#b98886cf = InputUser;\ninputUserSelf#f7c1b13f = InputUser;\ninputUser#f21158c6 user_id:long access_hash:long = InputUser;\ninputUserFromMessage#1da448e2 peer:InputPeer msg_id:int user_id:long = InputUser;\n\ninputPhoneContact#6a1dc4be flags:# client_id:long phone:string first_name:string last_name:string note:flags.0?TextWithEntities = InputContact;\n\ninputFile#f52ff27f id:long parts:int name:string md5_checksum:string = InputFile;\ninputFileBig#fa4f0bb5 id:long parts:int name:string = InputFile;\ninputFileStoryDocument#62dc8b48 id:InputDocument = InputFile;\n\ninputMediaEmpty#9664f57f = InputMedia;\ninputMediaUploadedPhoto#7d8375da flags:# spoiler:flags.2?true live_photo:flags.3?true file:InputFile stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int video:flags.3?InputDocument = InputMedia;\ninputMediaPhoto#e3af4434 flags:# spoiler:flags.1?true live_photo:flags.2?true id:InputPhoto ttl_seconds:flags.0?int video:flags.2?InputDocument = InputMedia;\ninputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia;\ninputMediaContact#f8ab7dfb phone_number:string first_name:string last_name:string vcard:string = InputMedia;\ninputMediaUploadedDocument#37c9330 flags:# nosound_video:flags.3?true force_file:flags.4?true spoiler:flags.5?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector<DocumentAttribute> stickers:flags.0?Vector<InputDocument> video_cover:flags.6?InputPhoto video_timestamp:flags.7?int ttl_seconds:flags.1?int = InputMedia;\ninputMediaDocument#a8763ab5 flags:# spoiler:flags.2?true id:InputDocument video_cover:flags.3?InputPhoto video_timestamp:flags.4?int ttl_seconds:flags.0?int query:flags.1?string = InputMedia;\ninputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia;\ninputMediaPhotoExternal#e5bbfe1a flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int = InputMedia;\ninputMediaDocumentExternal#779600f9 flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int video_cover:flags.2?InputPhoto video_timestamp:flags.3?int = InputMedia;\ninputMediaGame#d33f43f3 id:InputGame = InputMedia;\ninputMediaInvoice#405fef0d flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:flags.3?string provider_data:DataJSON start_param:flags.1?string extended_media:flags.2?InputMedia = InputMedia;\ninputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia;\ninputMediaPoll#883a4108 flags:# poll:Poll correct_answers:flags.0?Vector<int> attached_media:flags.3?InputMedia solution:flags.1?string solution_entities:flags.1?Vector<MessageEntity> solution_media:flags.2?InputMedia = InputMedia;\ninputMediaDice#e66fbf7b emoticon:string = InputMedia;\ninputMediaStory#89fdd778 peer:InputPeer id:int = InputMedia;\ninputMediaWebPage#c21b8849 flags:# force_large_media:flags.0?true force_small_media:flags.1?true optional:flags.2?true url:string = InputMedia;\ninputMediaPaidMedia#c4103386 flags:# stars_amount:long extended_media:Vector<InputMedia> payload:flags.0?string = InputMedia;\ninputMediaTodo#9fc55fde todo:TodoList = InputMedia;\ninputMediaStakeDice#f3a9244a game_hash:string ton_amount:long client_seed:bytes = InputMedia;\n\ninputChatPhotoEmpty#1ca48f57 = InputChatPhoto;\ninputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto;\ninputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto;\n\ninputGeoPointEmpty#e4c123d6 = InputGeoPoint;\ninputGeoPoint#48222faf flags:# lat:double long:double accuracy_radius:flags.0?int = InputGeoPoint;\n\ninputPhotoEmpty#1cd7bf0d = InputPhoto;\ninputPhoto#3bb3b94a id:long access_hash:long file_reference:bytes = InputPhoto;\n\ninputFileLocation#dfdaabe1 volume_id:long local_id:int secret:long file_reference:bytes = InputFileLocation;\ninputEncryptedFileLocation#f5235d55 id:long access_hash:long = InputFileLocation;\ninputDocumentFileLocation#bad07584 id:long access_hash:long file_reference:bytes thumb_size:string = InputFileLocation;\ninputSecureFileLocation#cbc7ee28 id:long access_hash:long = InputFileLocation;\ninputTakeoutFileLocation#29be5899 = InputFileLocation;\ninputPhotoFileLocation#40181ffe id:long access_hash:long file_reference:bytes thumb_size:string = InputFileLocation;\ninputPhotoLegacyFileLocation#d83466f3 id:long access_hash:long file_reference:bytes volume_id:long local_id:int secret:long = InputFileLocation;\ninputPeerPhotoFileLocation#37257e99 flags:# big:flags.0?true peer:InputPeer photo_id:long = InputFileLocation;\ninputStickerSetThumb#9d84f3db stickerset:InputStickerSet thumb_version:int = InputFileLocation;\ninputGroupCallStream#598a92a flags:# call:InputGroupCall time_ms:long scale:int video_channel:flags.0?int video_quality:flags.0?int = InputFileLocation;\n\npeerUser#59511722 user_id:long = Peer;\npeerChat#36c6019a chat_id:long = Peer;\npeerChannel#a2a5371e channel_id:long = Peer;\n\nstorage.fileUnknown#aa963b05 = storage.FileType;\nstorage.filePartial#40bc6f52 = storage.FileType;\nstorage.fileJpeg#7efe0e = storage.FileType;\nstorage.fileGif#cae1aadf = storage.FileType;\nstorage.filePng#a4f63c0 = storage.FileType;\nstorage.filePdf#ae1e508d = storage.FileType;\nstorage.fileMp3#528a0677 = storage.FileType;\nstorage.fileMov#4b09ebbc = storage.FileType;\nstorage.fileMp4#b3cea0e4 = storage.FileType;\nstorage.fileWebp#1081464c = storage.FileType;\n\nuserEmpty#d3bc4b7a id:long = User;\nuser#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 bot_can_manage_bots:flags2.18?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<RestrictionReason> bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector<Username> 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;\n\nuserProfilePhotoEmpty#4f11bae1 = UserProfilePhoto;\nuserProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto;\n\nuserStatusEmpty#9d05049 = UserStatus;\nuserStatusOnline#edb93949 expires:int = UserStatus;\nuserStatusOffline#8c703f was_online:int = UserStatus;\nuserStatusRecently#7b197dc8 flags:# by_me:flags.0?true = UserStatus;\nuserStatusLastWeek#541a1d1a flags:# by_me:flags.0?true = UserStatus;\nuserStatusLastMonth#65899777 flags:# by_me:flags.0?true = UserStatus;\n\nchatEmpty#29562865 id:long = Chat;\nchat#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;\nchatForbidden#6592a1a7 id:long title:string = Chat;\nchannel#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<RestrictionReason> 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<Username> 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;\nchannelForbidden#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;\n\nchatFull#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<BotInfo> 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<long> available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull;\nchannelFull#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<BotInfo> 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<string> groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector<long> 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;\n\nchatParticipant#38e79fde flags:# user_id:long inviter_id:long date:int rank:flags.0?string = ChatParticipant;\nchatParticipantCreator#e1f867b8 flags:# user_id:long rank:flags.0?string = ChatParticipant;\nchatParticipantAdmin#360d5d2 flags:# user_id:long inviter_id:long date:int rank:flags.0?string = ChatParticipant;\n\nchatParticipantsForbidden#8763d3e1 flags:# chat_id:long self_participant:flags.0?ChatParticipant = ChatParticipants;\nchatParticipants#3cbc93f8 chat_id:long participants:Vector<ChatParticipant> version:int = ChatParticipants;\n\nchatPhotoEmpty#37c1011c = ChatPhoto;\nchatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto;\n\nmessageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message;\nmessage#3ae56482 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true paid_suggested_post_stars:flags2.8?true paid_suggested_post_ton:flags2.9?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int from_rank:flags2.12?string peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long suggested_post:flags2.7?SuggestedPost schedule_repeat_period:flags2.10?int summary_from_language:flags2.11?string = Message;\nmessageService#7a800e0a flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer saved_peer_id:flags.28?Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message;\n\nmessageMediaEmpty#3ded6320 = MessageMedia;\nmessageMediaPhoto#e216eb63 flags:# spoiler:flags.3?true live_photo:flags.4?true photo:flags.0?Photo ttl_seconds:flags.2?int video:flags.4?Document = MessageMedia;\nmessageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia;\nmessageMediaContact#70322949 phone_number:string first_name:string last_name:string vcard:string user_id:long = MessageMedia;\nmessageMediaUnsupported#9f84f49e = MessageMedia;\nmessageMediaDocument#52d8ccd9 flags:# nopremium:flags.3?true spoiler:flags.4?true video:flags.6?true round:flags.7?true voice:flags.8?true document:flags.0?Document alt_documents:flags.5?Vector<Document> video_cover:flags.9?Photo video_timestamp:flags.10?int ttl_seconds:flags.2?int = MessageMedia;\nmessageMediaWebPage#ddf10c3b flags:# force_large_media:flags.0?true force_small_media:flags.1?true manual:flags.3?true safe:flags.4?true webpage:WebPage = MessageMedia;\nmessageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia;\nmessageMediaGame#fdb19008 game:Game = MessageMedia;\nmessageMediaInvoice#f6a548d3 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string extended_media:flags.4?MessageExtendedMedia = MessageMedia;\nmessageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia;\nmessageMediaPoll#773f4e66 flags:# poll:Poll results:PollResults attached_media:flags.0?MessageMedia = MessageMedia;\nmessageMediaDice#8cbec07 flags:# value:int emoticon:string game_outcome:flags.0?messages.EmojiGameOutcome = MessageMedia;\nmessageMediaStory#68cb6283 flags:# via_mention:flags.1?true peer:Peer id:int story:flags.0?StoryItem = MessageMedia;\nmessageMediaGiveaway#aa073beb flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.2?true channels:Vector<long> countries_iso2:flags.1?Vector<string> prize_description:flags.3?string quantity:int months:flags.4?int stars:flags.5?long until_date:int = MessageMedia;\nmessageMediaGiveawayResults#ceaa3ea1 flags:# only_new_subscribers:flags.0?true refunded:flags.2?true channel_id:long additional_peers_count:flags.3?int launch_msg_id:int winners_count:int unclaimed_count:int winners:Vector<long> months:flags.4?int stars:flags.5?long prize_description:flags.1?string until_date:int = MessageMedia;\nmessageMediaPaidMedia#a8852491 stars_amount:long extended_media:Vector<MessageExtendedMedia> = MessageMedia;\nmessageMediaToDo#8a53b014 flags:# todo:TodoList completions:flags.0?Vector<TodoCompletion> = MessageMedia;\nmessageMediaVideoStream#ca5cab89 flags:# rtmp_stream:flags.0?true call:InputGroupCall = MessageMedia;\n\nmessageActionEmpty#b6aef7b0 = MessageAction;\nmessageActionChatCreate#bd47cbad title:string users:Vector<long> = MessageAction;\nmessageActionChatEditTitle#b5a1ce5a title:string = MessageAction;\nmessageActionChatEditPhoto#7fcb13a8 photo:Photo = MessageAction;\nmessageActionChatDeletePhoto#95e3fbef = MessageAction;\nmessageActionChatAddUser#15cefd00 users:Vector<long> = MessageAction;\nmessageActionChatDeleteUser#a43f30cc user_id:long = MessageAction;\nmessageActionChatJoinedByLink#31224c3 inviter_id:long = MessageAction;\nmessageActionChannelCreate#95d2ac92 title:string = MessageAction;\nmessageActionChatMigrateTo#e1037f92 channel_id:long = MessageAction;\nmessageActionChannelMigrateFrom#ea3948e9 title:string chat_id:long = MessageAction;\nmessageActionPinMessage#94bd38ed = MessageAction;\nmessageActionHistoryClear#9fbab604 = MessageAction;\nmessageActionGameScore#92a72876 game_id:long score:int = MessageAction;\nmessageActionPaymentSentMe#ffa00ccc flags:# recurring_init:flags.2?true recurring_used:flags.3?true currency:string total_amount:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string charge:PaymentCharge subscription_until_date:flags.4?int = MessageAction;\nmessageActionPaymentSent#c624b16e flags:# recurring_init:flags.2?true recurring_used:flags.3?true currency:string total_amount:long invoice_slug:flags.0?string subscription_until_date:flags.4?int = MessageAction;\nmessageActionPhoneCall#80e11a7f flags:# video:flags.2?true call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction;\nmessageActionScreenshotTaken#4792929b = MessageAction;\nmessageActionCustomAction#fae69f56 message:string = MessageAction;\nmessageActionBotAllowed#c516d679 flags:# attach_menu:flags.1?true from_request:flags.3?true domain:flags.0?string app:flags.2?BotApp = MessageAction;\nmessageActionSecureValuesSentMe#1b287353 values:Vector<SecureValue> credentials:SecureCredentialsEncrypted = MessageAction;\nmessageActionSecureValuesSent#d95c6154 types:Vector<SecureValueType> = MessageAction;\nmessageActionContactSignUp#f3f25f76 = MessageAction;\nmessageActionGeoProximityReached#98e0d697 from_id:Peer to_id:Peer distance:int = MessageAction;\nmessageActionGroupCall#7a0d7f42 flags:# call:InputGroupCall duration:flags.0?int = MessageAction;\nmessageActionInviteToGroupCall#502f92f7 call:InputGroupCall users:Vector<long> = MessageAction;\nmessageActionSetMessagesTTL#3c134d7b flags:# period:int auto_setting_from:flags.0?long = MessageAction;\nmessageActionGroupCallScheduled#b3a07661 call:InputGroupCall schedule_date:int = MessageAction;\nmessageActionSetChatTheme#b91bbd3a theme:ChatTheme = MessageAction;\nmessageActionChatJoinedByRequest#ebbca3cb = MessageAction;\nmessageActionWebViewDataSentMe#47dd8079 text:string data:string = MessageAction;\nmessageActionWebViewDataSent#b4c38cb5 text:string = MessageAction;\nmessageActionGiftPremium#48e91302 flags:# currency:string amount:long days:int crypto_currency:flags.0?string crypto_amount:flags.0?long message:flags.1?TextWithEntities = MessageAction;\nmessageActionTopicCreate#d999256 flags:# title_missing:flags.1?true title:string icon_color:int icon_emoji_id:flags.0?long = MessageAction;\nmessageActionTopicEdit#c0944820 flags:# title:flags.0?string icon_emoji_id:flags.1?long closed:flags.2?Bool hidden:flags.3?Bool = MessageAction;\nmessageActionSuggestProfilePhoto#57de635e photo:Photo = MessageAction;\nmessageActionRequestedPeer#31518e9b button_id:int peers:Vector<Peer> = MessageAction;\nmessageActionSetChatWallPaper#5060a3f4 flags:# same:flags.0?true for_both:flags.1?true wallpaper:WallPaper = MessageAction;\nmessageActionGiftCode#31c48347 flags:# via_giveaway:flags.0?true unclaimed:flags.5?true boost_peer:flags.1?Peer days:int slug:string currency:flags.2?string amount:flags.2?long crypto_currency:flags.3?string crypto_amount:flags.3?long message:flags.4?TextWithEntities = MessageAction;\nmessageActionGiveawayLaunch#a80f51e4 flags:# stars:flags.0?long = MessageAction;\nmessageActionGiveawayResults#87e2f155 flags:# stars:flags.0?true winners_count:int unclaimed_count:int = MessageAction;\nmessageActionBoostApply#cc02aa6d boosts:int = MessageAction;\nmessageActionRequestedPeerSentMe#93b31848 button_id:int peers:Vector<RequestedPeer> = MessageAction;\nmessageActionPaymentRefunded#41b3e202 flags:# peer:Peer currency:string total_amount:long payload:flags.0?bytes charge:PaymentCharge = MessageAction;\nmessageActionGiftStars#45d5b021 flags:# currency:string amount:long stars:long crypto_currency:flags.0?string crypto_amount:flags.0?long transaction_id:flags.1?string = MessageAction;\nmessageActionPrizeStars#b00c47a2 flags:# unclaimed:flags.0?true stars:long transaction_id:string boost_peer:Peer giveaway_msg_id:int = MessageAction;\nmessageActionStarGift#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;\nmessageActionStarGiftUnique#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;\nmessageActionPaidMessagesRefunded#ac1f1fcd count:int stars:long = MessageAction;\nmessageActionPaidMessagesPrice#84b88578 flags:# broadcast_messages_allowed:flags.0?true stars:long = MessageAction;\nmessageActionConferenceCall#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<Peer> = MessageAction;\nmessageActionTodoCompletions#cc7c5c89 completed:Vector<int> incompleted:Vector<int> = MessageAction;\nmessageActionTodoAppendTasks#c7edbc83 list:Vector<TodoItem> = MessageAction;\nmessageActionSuggestedPostApproval#ee7a1596 flags:# rejected:flags.0?true balance_too_low:flags.1?true reject_comment:flags.2?string schedule_date:flags.3?int price:flags.4?StarsAmount = MessageAction;\nmessageActionSuggestedPostSuccess#95ddcf69 price:StarsAmount = MessageAction;\nmessageActionSuggestedPostRefund#69f916f8 flags:# payer_initiated:flags.0?true = MessageAction;\nmessageActionGiftTon#a8a3c699 flags:# currency:string amount:long crypto_currency:string crypto_amount:long transaction_id:flags.0?string = MessageAction;\nmessageActionSuggestBirthday#2c8f2a25 birthday:Birthday = MessageAction;\nmessageActionStarGiftPurchaseOffer#774278d4 flags:# accepted:flags.0?true declined:flags.1?true gift:StarGift price:StarsAmount expires_at:int = MessageAction;\nmessageActionStarGiftPurchaseOfferDeclined#73ada76b flags:# expired:flags.0?true gift:StarGift price:StarsAmount = MessageAction;\nmessageActionNewCreatorPending#b07ed085 new_creator_id:long = MessageAction;\nmessageActionChangeCreator#e188503b new_creator_id:long = MessageAction;\nmessageActionNoForwardsToggle#bf7d6572 prev_value:Bool new_value:Bool = MessageAction;\nmessageActionNoForwardsRequest#3e2793ba flags:# expired:flags.0?true prev_value:Bool new_value:Bool = MessageAction;\nmessageActionPollAppendAnswer#9da1cd6c answer:PollAnswer = MessageAction;\nmessageActionPollDeleteAnswer#399674dc answer:PollAnswer = MessageAction;\nmessageActionManagedBotCreated#16605e3e bot_id:long = MessageAction;\n\ndialog#fc89f7f3 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 unread_poll_votes_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog;\ndialogFolder#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;\n\nphotoEmpty#2331b22d id:long = Photo;\nphoto#fb197a65 flags:# has_stickers:flags.0?true id:long access_hash:long file_reference:bytes date:int sizes:Vector<PhotoSize> video_sizes:flags.1?Vector<VideoSize> dc_id:int = Photo;\n\nphotoSizeEmpty#e17e23c type:string = PhotoSize;\nphotoSize#75c78e60 type:string w:int h:int size:int = PhotoSize;\nphotoCachedSize#21e1ad6 type:string w:int h:int bytes:bytes = PhotoSize;\nphotoStrippedSize#e0b0bc2e type:string bytes:bytes = PhotoSize;\nphotoSizeProgressive#fa3efb95 type:string w:int h:int sizes:Vector<int> = PhotoSize;\nphotoPathSize#d8214d41 type:string bytes:bytes = PhotoSize;\n\ngeoPointEmpty#1117dd5f = GeoPoint;\ngeoPoint#b2a2f663 flags:# long:double lat:double access_hash:long accuracy_radius:flags.0?int = GeoPoint;\n\nauth.sentCode#5e002502 flags:# type:auth.SentCodeType phone_code_hash:string next_type:flags.1?auth.CodeType timeout:flags.2?int = auth.SentCode;\nauth.sentCodeSuccess#2390fe44 authorization:auth.Authorization = auth.SentCode;\nauth.sentCodePaymentRequired#e0955a3c store_product:string phone_code_hash:string support_email_address:string support_email_subject:string currency:string amount:long = auth.SentCode;\n\nauth.authorization#2ea2c0d4 flags:# setup_password_required:flags.1?true otherwise_relogin_days:flags.1?int tmp_sessions:flags.0?int future_auth_token:flags.2?bytes user:User = auth.Authorization;\nauth.authorizationSignUpRequired#44747e9a flags:# terms_of_service:flags.0?help.TermsOfService = auth.Authorization;\n\nauth.exportedAuthorization#b434e2b8 id:long bytes:bytes = auth.ExportedAuthorization;\n\ninputNotifyPeer#b8bc5b0c peer:InputPeer = InputNotifyPeer;\ninputNotifyUsers#193b4417 = InputNotifyPeer;\ninputNotifyChats#4a95e84e = InputNotifyPeer;\ninputNotifyBroadcasts#b1db7c7e = InputNotifyPeer;\ninputNotifyForumTopic#5c467992 peer:InputPeer top_msg_id:int = InputNotifyPeer;\n\ninputPeerNotifySettings#cacb6ae2 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?NotificationSound stories_muted:flags.6?Bool stories_hide_sender:flags.7?Bool stories_sound:flags.8?NotificationSound = InputPeerNotifySettings;\n\npeerNotifySettings#99622c0c flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int ios_sound:flags.3?NotificationSound android_sound:flags.4?NotificationSound other_sound:flags.5?NotificationSound stories_muted:flags.6?Bool stories_hide_sender:flags.7?Bool stories_ios_sound:flags.8?NotificationSound stories_android_sound:flags.9?NotificationSound stories_other_sound:flags.10?NotificationSound = PeerNotifySettings;\n\npeerSettings#f47741f7 flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true invite_members:flags.8?true request_chat_broadcast:flags.10?true business_bot_paused:flags.11?true business_bot_can_reply:flags.12?true geo_distance:flags.6?int request_chat_title:flags.9?string request_chat_date:flags.9?int business_bot_id:flags.13?long business_bot_manage_url:flags.13?string charge_paid_message_stars:flags.14?long registration_month:flags.15?string phone_country:flags.16?string name_change_date:flags.17?int photo_change_date:flags.18?int = PeerSettings;\n\nwallPaper#a437c3ed id:long flags:# creator:flags.0?true default:flags.1?true pattern:flags.3?true dark:flags.4?true access_hash:long slug:string document:Document settings:flags.2?WallPaperSettings = WallPaper;\nwallPaperNoFile#e0804116 id:long flags:# default:flags.1?true dark:flags.4?true settings:flags.2?WallPaperSettings = WallPaper;\n\ninputReportReasonSpam#58dbcab8 = ReportReason;\ninputReportReasonViolence#1e22c78d = ReportReason;\ninputReportReasonPornography#2e59d922 = ReportReason;\ninputReportReasonChildAbuse#adf44ee3 = ReportReason;\ninputReportReasonOther#c1e4a2b1 = ReportReason;\ninputReportReasonCopyright#9b89f93a = ReportReason;\ninputReportReasonGeoIrrelevant#dbd4feed = ReportReason;\ninputReportReasonFake#f5ddd6e7 = ReportReason;\ninputReportReasonIllegalDrugs#a8eb2be = ReportReason;\ninputReportReasonPersonalDetails#9ec7863d = ReportReason;\n\nuserFull#6cbe645 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true display_gifts_button:flags2.16?true noforwards_my_enabled:flags2.23?true noforwards_peer_enabled:flags2.24?true unofficial_security_risk:flags2.26?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme:flags.15?ChatTheme private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int starref_program:flags2.11?StarRefProgram bot_verification:flags2.12?BotVerification send_paid_messages_stars:flags2.14?long disallowed_gifts:flags2.15?DisallowedGiftsSettings stars_rating:flags2.17?StarsRating stars_my_pending_rating:flags2.18?StarsRating stars_my_pending_rating_date:flags2.18?int main_tab:flags2.20?ProfileTab saved_music:flags2.21?Document note:flags2.22?TextWithEntities bot_manager_id:flags2.25?long = UserFull;\n\ncontact#145ade0b user_id:long mutual:Bool = Contact;\n\nimportedContact#c13e3c50 user_id:long client_id:long = ImportedContact;\n\ncontactStatus#16d9703b user_id:long status:UserStatus = ContactStatus;\n\ncontacts.contactsNotModified#b74ba9d2 = contacts.Contacts;\ncontacts.contacts#eae87e42 contacts:Vector<Contact> saved_count:int users:Vector<User> = contacts.Contacts;\n\ncontacts.importedContacts#77d01c3b imported:Vector<ImportedContact> popular_invites:Vector<PopularContact> retry_contacts:Vector<long> users:Vector<User> = contacts.ImportedContacts;\n\ncontacts.blocked#ade1591 blocked:Vector<PeerBlocked> chats:Vector<Chat> users:Vector<User> = contacts.Blocked;\ncontacts.blockedSlice#e1664194 count:int blocked:Vector<PeerBlocked> chats:Vector<Chat> users:Vector<User> = contacts.Blocked;\n\nmessages.dialogs#15ba6c40 dialogs:Vector<Dialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Dialogs;\nmessages.dialogsSlice#71e094f3 count:int dialogs:Vector<Dialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Dialogs;\nmessages.dialogsNotModified#f0e3e596 count:int = messages.Dialogs;\n\nmessages.messages#1d73e7ea messages:Vector<Message> topics:Vector<ForumTopic> chats:Vector<Chat> users:Vector<User> = messages.Messages;\nmessages.messagesSlice#5f206716 flags:# inexact:flags.1?true count:int next_rate:flags.0?int offset_id_offset:flags.2?int search_flood:flags.3?SearchPostsFlood messages:Vector<Message> topics:Vector<ForumTopic> chats:Vector<Chat> users:Vector<User> = messages.Messages;\nmessages.channelMessages#c776ba4e flags:# inexact:flags.1?true pts:int count:int offset_id_offset:flags.2?int messages:Vector<Message> topics:Vector<ForumTopic> chats:Vector<Chat> users:Vector<User> = messages.Messages;\nmessages.messagesNotModified#74535f21 count:int = messages.Messages;\n\nmessages.chats#64ff9fd5 chats:Vector<Chat> = messages.Chats;\nmessages.chatsSlice#9cd81144 count:int chats:Vector<Chat> = messages.Chats;\n\nmessages.chatFull#e5d7d19c full_chat:ChatFull chats:Vector<Chat> users:Vector<User> = messages.ChatFull;\n\nmessages.affectedHistory#b45c69d1 pts:int pts_count:int offset:int = messages.AffectedHistory;\n\ninputMessagesFilterEmpty#57e2f66c = MessagesFilter;\ninputMessagesFilterPhotos#9609a51c = MessagesFilter;\ninputMessagesFilterVideo#9fc00e65 = MessagesFilter;\ninputMessagesFilterPhotoVideo#56e9f0e4 = MessagesFilter;\ninputMessagesFilterDocument#9eddf188 = MessagesFilter;\ninputMessagesFilterUrl#7ef0dd87 = MessagesFilter;\ninputMessagesFilterGif#ffc86587 = MessagesFilter;\ninputMessagesFilterVoice#50f5c392 = MessagesFilter;\ninputMessagesFilterMusic#3751b49e = MessagesFilter;\ninputMessagesFilterChatPhotos#3a20ecb8 = MessagesFilter;\ninputMessagesFilterPhoneCalls#80c99768 flags:# missed:flags.0?true = MessagesFilter;\ninputMessagesFilterRoundVoice#7a7c17a4 = MessagesFilter;\ninputMessagesFilterRoundVideo#b549da53 = MessagesFilter;\ninputMessagesFilterMyMentions#c1f8e69a = MessagesFilter;\ninputMessagesFilterGeo#e7026d0d = MessagesFilter;\ninputMessagesFilterContacts#e062db83 = MessagesFilter;\ninputMessagesFilterPinned#1bb00451 = MessagesFilter;\ninputMessagesFilterPoll#fa2bc90a = MessagesFilter;\n\nupdateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update;\nupdateMessageID#4e90bfd6 id:int random_id:long = Update;\nupdateDeleteMessages#a20db0e5 messages:Vector<int> pts:int pts_count:int = Update;\nupdateUserTyping#2a17bf5c flags:# user_id:long top_msg_id:flags.0?int action:SendMessageAction = Update;\nupdateChatUserTyping#83487af0 chat_id:long from_id:Peer action:SendMessageAction = Update;\nupdateChatParticipants#7761198 participants:ChatParticipants = Update;\nupdateUserStatus#e5bdf8de user_id:long status:UserStatus = Update;\nupdateUserName#a7848924 user_id:long first_name:string last_name:string usernames:Vector<Username> = Update;\nupdateNewAuthorization#8951abef flags:# unconfirmed:flags.0?true hash:long date:flags.0?int device:flags.0?string location:flags.0?string = Update;\nupdateNewEncryptedMessage#12bcbd9a message:EncryptedMessage qts:int = Update;\nupdateEncryptedChatTyping#1710f156 chat_id:int = Update;\nupdateEncryption#b4a2e88d chat:EncryptedChat date:int = Update;\nupdateEncryptedMessagesRead#38fe25b7 chat_id:int max_date:int date:int = Update;\nupdateChatParticipantAdd#3dda5451 chat_id:long user_id:long inviter_id:long date:int version:int = Update;\nupdateChatParticipantDelete#e32f3d77 chat_id:long user_id:long version:int = Update;\nupdateDcOptions#8e5e9873 dc_options:Vector<DcOption> = Update;\nupdateNotifySettings#bec268ef peer:NotifyPeer notify_settings:PeerNotifySettings = Update;\nupdateServiceNotification#ebe46819 flags:# popup:flags.0?true invert_media:flags.2?true inbox_date:flags.1?int type:string message:string media:MessageMedia entities:Vector<MessageEntity> = Update;\nupdatePrivacy#ee3b272a key:PrivacyKey rules:Vector<PrivacyRule> = Update;\nupdateUserPhone#5492a13 user_id:long phone:string = Update;\nupdateReadHistoryInbox#9e84bc99 flags:# folder_id:flags.0?int peer:Peer top_msg_id:flags.1?int max_id:int still_unread_count:int pts:int pts_count:int = Update;\nupdateReadHistoryOutbox#2f2f21bf peer:Peer max_id:int pts:int pts_count:int = Update;\nupdateWebPage#7f891213 webpage:WebPage pts:int pts_count:int = Update;\nupdateReadMessagesContents#f8227181 flags:# messages:Vector<int> pts:int pts_count:int date:flags.0?int = Update;\nupdateChannelTooLong#108d941f flags:# channel_id:long pts:flags.0?int = Update;\nupdateChannel#635b4c09 channel_id:long = Update;\nupdateNewChannelMessage#62ba04d9 message:Message pts:int pts_count:int = Update;\nupdateReadChannelInbox#922e6e10 flags:# folder_id:flags.0?int channel_id:long max_id:int still_unread_count:int pts:int = Update;\nupdateDeleteChannelMessages#c32d5b12 channel_id:long messages:Vector<int> pts:int pts_count:int = Update;\nupdateChannelMessageViews#f226ac08 channel_id:long id:int views:int = Update;\nupdateChatParticipantAdmin#d7ca61a2 chat_id:long user_id:long is_admin:Bool version:int = Update;\nupdateNewStickerSet#688a30aa stickerset:messages.StickerSet = Update;\nupdateStickerSetsOrder#bb2d201 flags:# masks:flags.0?true emojis:flags.1?true order:Vector<long> = Update;\nupdateStickerSets#31c24808 flags:# masks:flags.0?true emojis:flags.1?true = Update;\nupdateSavedGifs#9375341e = Update;\nupdateBotInlineQuery#496f379c flags:# query_id:long user_id:long query:string geo:flags.0?GeoPoint peer_type:flags.1?InlineQueryPeerType offset:string = Update;\nupdateBotInlineSend#12f12a07 flags:# user_id:long query:string geo:flags.0?GeoPoint id:string msg_id:flags.1?InputBotInlineMessageID = Update;\nupdateEditChannelMessage#1b3f4df7 message:Message pts:int pts_count:int = Update;\nupdateBotCallbackQuery#b9cfc48d flags:# query_id:long user_id:long peer:Peer msg_id:int chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update;\nupdateEditMessage#e40370a3 message:Message pts:int pts_count:int = Update;\nupdateInlineBotCallbackQuery#691e9052 flags:# query_id:long user_id:long msg_id:InputBotInlineMessageID chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update;\nupdateReadChannelOutbox#b75f99a9 channel_id:long max_id:int = Update;\nupdateDraftMessage#edfc111e flags:# peer:Peer top_msg_id:flags.0?int saved_peer_id:flags.1?Peer draft:DraftMessage = Update;\nupdateReadFeaturedStickers#571d2742 = Update;\nupdateRecentStickers#9a422c20 = Update;\nupdateConfig#a229dd06 = Update;\nupdatePtsChanged#3354678f = Update;\nupdateChannelWebPage#2f2ba99f channel_id:long webpage:WebPage pts:int pts_count:int = Update;\nupdateDialogPinned#6e6fe51c flags:# pinned:flags.0?true folder_id:flags.1?int peer:DialogPeer = Update;\nupdatePinnedDialogs#fa0f3ca2 flags:# folder_id:flags.1?int order:flags.0?Vector<DialogPeer> = Update;\nupdateBotWebhookJSON#8317c0c3 data:DataJSON = Update;\nupdateBotWebhookJSONQuery#9b9240a6 query_id:long data:DataJSON timeout:int = Update;\nupdateBotShippingQuery#b5aefd7d query_id:long user_id:long payload:bytes shipping_address:PostAddress = Update;\nupdateBotPrecheckoutQuery#8caa9a96 flags:# query_id:long user_id:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string currency:string total_amount:long = Update;\nupdatePhoneCall#ab0f6b1e phone_call:PhoneCall = Update;\nupdateLangPackTooLong#46560264 lang_code:string = Update;\nupdateLangPack#56022f4d difference:LangPackDifference = Update;\nupdateFavedStickers#e511996d = Update;\nupdateChannelReadMessagesContents#25f324f7 flags:# channel_id:long top_msg_id:flags.0?int saved_peer_id:flags.1?Peer messages:Vector<int> = Update;\nupdateContactsReset#7084a7be = Update;\nupdateChannelAvailableMessages#b23fc698 channel_id:long available_min_id:int = Update;\nupdateDialogUnreadMark#b658f23e flags:# unread:flags.0?true peer:DialogPeer saved_peer_id:flags.1?Peer = Update;\nupdateMessagePoll#d64c522b flags:# peer:flags.1?Peer msg_id:flags.1?int top_msg_id:flags.2?int poll_id:long poll:flags.0?Poll results:PollResults = Update;\nupdateChatDefaultBannedRights#54c01850 peer:Peer default_banned_rights:ChatBannedRights version:int = Update;\nupdateFolderPeers#19360dc0 folder_peers:Vector<FolderPeer> pts:int pts_count:int = Update;\nupdatePeerSettings#6a7e7366 peer:Peer settings:PeerSettings = Update;\nupdatePeerLocated#b4afcfb0 peers:Vector<PeerLocated> = Update;\nupdateNewScheduledMessage#39a51dfb message:Message = Update;\nupdateDeleteScheduledMessages#f2a71983 flags:# peer:Peer messages:Vector<int> sent_messages:flags.0?Vector<int> = Update;\nupdateTheme#8216fba3 theme:Theme = Update;\nupdateGeoLiveViewed#871fb939 peer:Peer msg_id:int = Update;\nupdateLoginToken#564fe691 = Update;\nupdateMessagePollVote#7699f014 poll_id:long peer:Peer options:Vector<bytes> positions:Vector<int> qts:int = Update;\nupdateDialogFilter#26ffde7d flags:# id:int filter:flags.0?DialogFilter = Update;\nupdateDialogFilterOrder#a5d72105 order:Vector<int> = Update;\nupdateDialogFilters#3504914f = Update;\nupdatePhoneCallSignalingData#2661bf09 phone_call_id:long data:bytes = Update;\nupdateChannelMessageForwards#d29a27f4 channel_id:long id:int forwards:int = Update;\nupdateReadChannelDiscussionInbox#d6b19546 flags:# channel_id:long top_msg_id:int read_max_id:int broadcast_id:flags.0?long broadcast_post:flags.0?int = Update;\nupdateReadChannelDiscussionOutbox#695c9e7c channel_id:long top_msg_id:int read_max_id:int = Update;\nupdatePeerBlocked#ebe07752 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true peer_id:Peer = Update;\nupdateChannelUserTyping#8c88c923 flags:# channel_id:long top_msg_id:flags.0?int from_id:Peer action:SendMessageAction = Update;\nupdatePinnedMessages#ed85eab5 flags:# pinned:flags.0?true peer:Peer messages:Vector<int> pts:int pts_count:int = Update;\nupdatePinnedChannelMessages#5bb98608 flags:# pinned:flags.0?true channel_id:long messages:Vector<int> pts:int pts_count:int = Update;\nupdateChat#f89a6a4e chat_id:long = Update;\nupdateGroupCallParticipants#f2ebdb4e call:InputGroupCall participants:Vector<GroupCallParticipant> version:int = Update;\nupdateGroupCall#9d2216e0 flags:# live_story:flags.2?true peer:flags.1?Peer call:GroupCall = Update;\nupdatePeerHistoryTTL#bb9bb9a5 flags:# peer:Peer ttl_period:flags.0?int = Update;\nupdateChatParticipant#d087663a flags:# chat_id:long date:int actor_id:long user_id:long prev_participant:flags.0?ChatParticipant new_participant:flags.1?ChatParticipant invite:flags.2?ExportedChatInvite qts:int = Update;\nupdateChannelParticipant#985d3abb flags:# via_chatlist:flags.3?true channel_id:long date:int actor_id:long user_id:long prev_participant:flags.0?ChannelParticipant new_participant:flags.1?ChannelParticipant invite:flags.2?ExportedChatInvite qts:int = Update;\nupdateBotStopped#c4870a49 user_id:long date:int stopped:Bool qts:int = Update;\nupdateGroupCallConnection#b783982 flags:# presentation:flags.0?true params:DataJSON = Update;\nupdateBotCommands#4d712f2e peer:Peer bot_id:long commands:Vector<BotCommand> = Update;\nupdatePendingJoinRequests#7063c3db peer:Peer requests_pending:int recent_requesters:Vector<long> = Update;\nupdateBotChatInviteRequester#11dfa986 peer:Peer date:int user_id:long about:string invite:ExportedChatInvite qts:int = Update;\nupdateMessageReactions#1e297bfa flags:# peer:Peer msg_id:int top_msg_id:flags.0?int saved_peer_id:flags.1?Peer reactions:MessageReactions = Update;\nupdateAttachMenuBots#17b7a20b = Update;\nupdateWebViewResultSent#1592b79d query_id:long = Update;\nupdateBotMenuButton#14b85813 bot_id:long button:BotMenuButton = Update;\nupdateSavedRingtones#74d8be99 = Update;\nupdateTranscribedAudio#84cd5a flags:# pending:flags.0?true peer:Peer msg_id:int transcription_id:long text:string = Update;\nupdateReadFeaturedEmojiStickers#fb4c496c = Update;\nupdateUserEmojiStatus#28373599 user_id:long emoji_status:EmojiStatus = Update;\nupdateRecentEmojiStatuses#30f443db = Update;\nupdateRecentReactions#6f7863f4 = Update;\nupdateMoveStickerSetToTop#86fccf85 flags:# masks:flags.0?true emojis:flags.1?true stickerset:long = Update;\nupdateMessageExtendedMedia#d5a41724 peer:Peer msg_id:int extended_media:Vector<MessageExtendedMedia> = Update;\nupdateUser#20529438 user_id:long = Update;\nupdateAutoSaveSettings#ec05b097 = Update;\nupdateStory#75b3b798 peer:Peer story:StoryItem = Update;\nupdateReadStories#f74e932b peer:Peer max_id:int = Update;\nupdateStoryID#1bf335b9 id:int random_id:long = Update;\nupdateStoriesStealthMode#2c084dc1 stealth_mode:StoriesStealthMode = Update;\nupdateSentStoryReaction#7d627683 peer:Peer story_id:int reaction:Reaction = Update;\nupdateBotChatBoost#904dd49c peer:Peer boost:Boost qts:int = Update;\nupdateChannelViewForumAsMessages#7b68920 channel_id:long enabled:Bool = Update;\nupdatePeerWallpaper#ae3f101d flags:# wallpaper_overridden:flags.1?true peer:Peer wallpaper:flags.0?WallPaper = Update;\nupdateBotMessageReaction#ac21d3ce peer:Peer msg_id:int date:int actor:Peer old_reactions:Vector<Reaction> new_reactions:Vector<Reaction> qts:int = Update;\nupdateBotMessageReactions#9cb7759 peer:Peer msg_id:int date:int reactions:Vector<ReactionCount> qts:int = Update;\nupdateSavedDialogPinned#aeaf9e74 flags:# pinned:flags.0?true peer:DialogPeer = Update;\nupdatePinnedSavedDialogs#686c85a6 flags:# order:flags.0?Vector<DialogPeer> = Update;\nupdateSavedReactionTags#39c67432 = Update;\nupdateSmsJob#f16269d4 job_id:string = Update;\nupdateQuickReplies#f9470ab2 quick_replies:Vector<QuickReply> = Update;\nupdateNewQuickReply#f53da717 quick_reply:QuickReply = Update;\nupdateDeleteQuickReply#53e6f1ec shortcut_id:int = Update;\nupdateQuickReplyMessage#3e050d0f message:Message = Update;\nupdateDeleteQuickReplyMessages#566fe7cd shortcut_id:int messages:Vector<int> = Update;\nupdateBotBusinessConnect#8ae5c97a connection:BotBusinessConnection qts:int = Update;\nupdateBotNewBusinessMessage#9ddb347c flags:# connection_id:string message:Message reply_to_message:flags.0?Message qts:int = Update;\nupdateBotEditBusinessMessage#7df587c flags:# connection_id:string message:Message reply_to_message:flags.0?Message qts:int = Update;\nupdateBotDeleteBusinessMessage#a02a982e connection_id:string peer:Peer messages:Vector<int> qts:int = Update;\nupdateNewStoryReaction#1824e40b story_id:int peer:Peer reaction:Reaction = Update;\nupdateStarsBalance#4e80a379 balance:StarsAmount = Update;\nupdateBusinessBotCallbackQuery#1ea2fda7 flags:# query_id:long user_id:long connection_id:string message:Message reply_to_message:flags.2?Message chat_instance:long data:flags.0?bytes = Update;\nupdateStarsRevenueStatus#a584b019 peer:Peer status:StarsRevenueStatus = Update;\nupdateBotPurchasedPaidMedia#283bd312 user_id:long payload:string qts:int = Update;\nupdatePaidReactionPrivacy#8b725fce private:PaidReactionPrivacy = Update;\nupdateSentPhoneCode#504aa18f sent_code:auth.SentCode = Update;\nupdateGroupCallChainBlocks#a477288f call:InputGroupCall sub_chain_id:int blocks:Vector<bytes> next_offset:int = Update;\nupdateReadMonoForumInbox#77b0e372 channel_id:long saved_peer_id:Peer read_max_id:int = Update;\nupdateReadMonoForumOutbox#a4a79376 channel_id:long saved_peer_id:Peer read_max_id:int = Update;\nupdateMonoForumNoPaidException#9f812b08 flags:# exception:flags.0?true channel_id:long saved_peer_id:Peer = Update;\nupdateGroupCallMessage#d8326f0d call:InputGroupCall message:GroupCallMessage = Update;\nupdateGroupCallEncryptedMessage#c957a766 call:InputGroupCall from_id:Peer encrypted_message:bytes = Update;\nupdatePinnedForumTopic#683b2c52 flags:# pinned:flags.0?true peer:Peer topic_id:int = Update;\nupdatePinnedForumTopics#def143d0 flags:# peer:Peer order:flags.0?Vector<int> = Update;\nupdateDeleteGroupCallMessages#3e85e92c call:InputGroupCall messages:Vector<int> = Update;\nupdateStarGiftAuctionState#48e246c2 gift_id:long state:StarGiftAuctionState = Update;\nupdateStarGiftAuctionUserState#dc58f31e gift_id:long user_state:StarGiftAuctionUserState = Update;\nupdateEmojiGameInfo#fb9c547a info:messages.EmojiGameInfo = Update;\nupdateStarGiftCraftFail#ac072444 = Update;\nupdateChatParticipantRank#bd8367b9 chat_id:long user_id:long rank:string version:int = Update;\nupdateManagedBot#4880ed9a user_id:long bot_id:long qts:int = Update;\n\nupdates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;\n\nupdates.differenceEmpty#5d75a138 date:int seq:int = updates.Difference;\nupdates.difference#f49ca0 new_messages:Vector<Message> new_encrypted_messages:Vector<EncryptedMessage> other_updates:Vector<Update> chats:Vector<Chat> users:Vector<User> state:updates.State = updates.Difference;\nupdates.differenceSlice#a8fb1981 new_messages:Vector<Message> new_encrypted_messages:Vector<EncryptedMessage> other_updates:Vector<Update> chats:Vector<Chat> users:Vector<User> intermediate_state:updates.State = updates.Difference;\nupdates.differenceTooLong#4afe8f6d pts:int = updates.Difference;\n\nupdatesTooLong#e317af7e = Updates;\nupdateShortMessage#313bc7f8 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true id:int user_id:long message:string pts:int pts_count:int date:int fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader entities:flags.7?Vector<MessageEntity> ttl_period:flags.25?int = Updates;\nupdateShortChatMessage#4d6deea5 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true id:int from_id:long chat_id:long message:string pts:int pts_count:int date:int fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader entities:flags.7?Vector<MessageEntity> ttl_period:flags.25?int = Updates;\nupdateShort#78d4dec1 update:Update date:int = Updates;\nupdatesCombined#725b04c3 updates:Vector<Update> users:Vector<User> chats:Vector<Chat> date:int seq_start:int seq:int = Updates;\nupdates#74ae4240 updates:Vector<Update> users:Vector<User> chats:Vector<Chat> date:int seq:int = Updates;\nupdateShortSentMessage#9015e101 flags:# out:flags.1?true id:int pts:int pts_count:int date:int media:flags.9?MessageMedia entities:flags.7?Vector<MessageEntity> ttl_period:flags.25?int = Updates;\n\nphotos.photos#8dca6aa5 photos:Vector<Photo> users:Vector<User> = photos.Photos;\nphotos.photosSlice#15051f54 count:int photos:Vector<Photo> users:Vector<User> = photos.Photos;\n\nphotos.photo#20212ca8 photo:Photo users:Vector<User> = photos.Photo;\n\nupload.file#96a18d5 type:storage.FileType mtime:int bytes:bytes = upload.File;\nupload.fileCdnRedirect#f18cda44 dc_id:int file_token:bytes encryption_key:bytes encryption_iv:bytes file_hashes:Vector<FileHash> = upload.File;\n\ndcOption#18b7a10d flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true cdn:flags.3?true static:flags.4?true this_port_only:flags.5?true id:int ip_address:string port:int secret:flags.10?bytes = DcOption;\n\nconfig#cc1a241e flags:# default_p2p_contacts:flags.3?true preload_featured_stickers:flags.4?true revoke_pm_inbox:flags.6?true blocked_mode:flags.8?true force_try_ipv6:flags.14?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector<DcOption> dc_txt_domain_name:string chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int push_chat_period_ms:int push_chat_limit:int edit_time_limit:int revoke_time_limit:int revoke_pm_time_limit:int rating_e_decay:int stickers_recent_limit:int channels_read_media_period:int tmp_sessions:flags.0?int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string autoupdate_url_prefix:flags.7?string gif_search_username:flags.9?string venue_search_username:flags.10?string img_search_username:flags.11?string static_maps_provider:flags.12?string caption_length_max:int message_length_max:int webfile_dc_id:int suggested_lang_code:flags.2?string lang_pack_version:flags.2?int base_lang_pack_version:flags.2?int reactions_default:flags.15?Reaction autologin_token:flags.16?string = Config;\n\nnearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc;\n\nhelp.appUpdate#ccbbce30 flags:# can_not_skip:flags.0?true id:int version:string text:string entities:Vector<MessageEntity> document:flags.1?Document url:flags.2?string sticker:flags.3?Document = help.AppUpdate;\nhelp.noAppUpdate#c45a6536 = help.AppUpdate;\n\nhelp.inviteText#18cb9f78 message:string = help.InviteText;\n\nencryptedChatEmpty#ab7ec0a0 id:int = EncryptedChat;\nencryptedChatWaiting#66b25953 id:int access_hash:long date:int admin_id:long participant_id:long = EncryptedChat;\nencryptedChatRequested#48f1d94c flags:# folder_id:flags.0?int id:int access_hash:long date:int admin_id:long participant_id:long g_a:bytes = EncryptedChat;\nencryptedChat#61f0d4c7 id:int access_hash:long date:int admin_id:long participant_id:long g_a_or_b:bytes key_fingerprint:long = EncryptedChat;\nencryptedChatDiscarded#1e1c7c45 flags:# history_deleted:flags.0?true id:int = EncryptedChat;\n\ninputEncryptedChat#f141b5e1 chat_id:int access_hash:long = InputEncryptedChat;\n\nencryptedFileEmpty#c21f497e = EncryptedFile;\nencryptedFile#a8008cd8 id:long access_hash:long size:long dc_id:int key_fingerprint:int = EncryptedFile;\n\ninputEncryptedFileEmpty#1837c364 = InputEncryptedFile;\ninputEncryptedFileUploaded#64bd0306 id:long parts:int md5_checksum:string key_fingerprint:int = InputEncryptedFile;\ninputEncryptedFile#5a17b5e5 id:long access_hash:long = InputEncryptedFile;\ninputEncryptedFileBigUploaded#2dc173c8 id:long parts:int key_fingerprint:int = InputEncryptedFile;\n\nencryptedMessage#ed18c118 random_id:long chat_id:int date:int bytes:bytes file:EncryptedFile = EncryptedMessage;\nencryptedMessageService#23734b06 random_id:long chat_id:int date:int bytes:bytes = EncryptedMessage;\n\nmessages.dhConfigNotModified#c0e24635 random:bytes = messages.DhConfig;\nmessages.dhConfig#2c221edd g:int p:bytes version:int random:bytes = messages.DhConfig;\n\nmessages.sentEncryptedMessage#560f8935 date:int = messages.SentEncryptedMessage;\nmessages.sentEncryptedFile#9493ff32 date:int file:EncryptedFile = messages.SentEncryptedMessage;\n\ninputDocumentEmpty#72f0eaae = InputDocument;\ninputDocument#1abfb575 id:long access_hash:long file_reference:bytes = InputDocument;\n\ndocumentEmpty#36f8c871 id:long = Document;\ndocument#8fd4c4d8 flags:# id:long access_hash:long file_reference:bytes date:int mime_type:string size:long thumbs:flags.0?Vector<PhotoSize> video_thumbs:flags.1?Vector<VideoSize> dc_id:int attributes:Vector<DocumentAttribute> = Document;\n\nhelp.support#17c6b5f6 phone_number:string user:User = help.Support;\n\nnotifyPeer#9fd40bd8 peer:Peer = NotifyPeer;\nnotifyUsers#b4c83b4c = NotifyPeer;\nnotifyChats#c007cec3 = NotifyPeer;\nnotifyBroadcasts#d612e8ef = NotifyPeer;\nnotifyForumTopic#226e6308 peer:Peer top_msg_id:int = NotifyPeer;\n\nsendMessageTypingAction#16bf744e = SendMessageAction;\nsendMessageCancelAction#fd5ec8f5 = SendMessageAction;\nsendMessageRecordVideoAction#a187d66f = SendMessageAction;\nsendMessageUploadVideoAction#e9763aec progress:int = SendMessageAction;\nsendMessageRecordAudioAction#d52f73f7 = SendMessageAction;\nsendMessageUploadAudioAction#f351d7ab progress:int = SendMessageAction;\nsendMessageUploadPhotoAction#d1d34a26 progress:int = SendMessageAction;\nsendMessageUploadDocumentAction#aa0cd9e4 progress:int = SendMessageAction;\nsendMessageGeoLocationAction#176f8ba1 = SendMessageAction;\nsendMessageChooseContactAction#628cbc6f = SendMessageAction;\nsendMessageGamePlayAction#dd6a8f48 = SendMessageAction;\nsendMessageRecordRoundAction#88f27fbc = SendMessageAction;\nsendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction;\nspeakingInGroupCallAction#d92c2285 = SendMessageAction;\nsendMessageHistoryImportAction#dbda9246 progress:int = SendMessageAction;\nsendMessageChooseStickerAction#b05ac6b1 = SendMessageAction;\nsendMessageEmojiInteraction#25972bcb emoticon:string msg_id:int interaction:DataJSON = SendMessageAction;\nsendMessageEmojiInteractionSeen#b665902e emoticon:string = SendMessageAction;\nsendMessageTextDraftAction#376d975c random_id:long text:TextWithEntities = SendMessageAction;\n\ncontacts.found#b3134d9d my_results:Vector<Peer> results:Vector<Peer> chats:Vector<Chat> users:Vector<User> = contacts.Found;\n\ninputPrivacyKeyStatusTimestamp#4f96cb18 = InputPrivacyKey;\ninputPrivacyKeyChatInvite#bdfb0426 = InputPrivacyKey;\ninputPrivacyKeyPhoneCall#fabadc5f = InputPrivacyKey;\ninputPrivacyKeyPhoneP2P#db9e70d2 = InputPrivacyKey;\ninputPrivacyKeyForwards#a4dd4c08 = InputPrivacyKey;\ninputPrivacyKeyProfilePhoto#5719bacc = InputPrivacyKey;\ninputPrivacyKeyPhoneNumber#352dafa = InputPrivacyKey;\ninputPrivacyKeyAddedByPhone#d1219bdd = InputPrivacyKey;\ninputPrivacyKeyVoiceMessages#aee69d68 = InputPrivacyKey;\ninputPrivacyKeyAbout#3823cc40 = InputPrivacyKey;\ninputPrivacyKeyBirthday#d65a11cc = InputPrivacyKey;\ninputPrivacyKeyStarGiftsAutoSave#e1732341 = InputPrivacyKey;\ninputPrivacyKeyNoPaidMessages#bdc597b4 = InputPrivacyKey;\ninputPrivacyKeySavedMusic#4dbe9226 = InputPrivacyKey;\n\nprivacyKeyStatusTimestamp#bc2eab30 = PrivacyKey;\nprivacyKeyChatInvite#500e6dfa = PrivacyKey;\nprivacyKeyPhoneCall#3d662b7b = PrivacyKey;\nprivacyKeyPhoneP2P#39491cc8 = PrivacyKey;\nprivacyKeyForwards#69ec56a3 = PrivacyKey;\nprivacyKeyProfilePhoto#96151fed = PrivacyKey;\nprivacyKeyPhoneNumber#d19ae46d = PrivacyKey;\nprivacyKeyAddedByPhone#42ffd42b = PrivacyKey;\nprivacyKeyVoiceMessages#697f414 = PrivacyKey;\nprivacyKeyAbout#a486b761 = PrivacyKey;\nprivacyKeyBirthday#2000a518 = PrivacyKey;\nprivacyKeyStarGiftsAutoSave#2ca4fdf8 = PrivacyKey;\nprivacyKeyNoPaidMessages#17d348d2 = PrivacyKey;\nprivacyKeySavedMusic#ff7a571b = PrivacyKey;\n\ninputPrivacyValueAllowContacts#d09e07b = InputPrivacyRule;\ninputPrivacyValueAllowAll#184b35ce = InputPrivacyRule;\ninputPrivacyValueAllowUsers#131cc67f users:Vector<InputUser> = InputPrivacyRule;\ninputPrivacyValueDisallowContacts#ba52007 = InputPrivacyRule;\ninputPrivacyValueDisallowAll#d66b66c9 = InputPrivacyRule;\ninputPrivacyValueDisallowUsers#90110467 users:Vector<InputUser> = InputPrivacyRule;\ninputPrivacyValueAllowChatParticipants#840649cf chats:Vector<long> = InputPrivacyRule;\ninputPrivacyValueDisallowChatParticipants#e94f0f86 chats:Vector<long> = InputPrivacyRule;\ninputPrivacyValueAllowCloseFriends#2f453e49 = InputPrivacyRule;\ninputPrivacyValueAllowPremium#77cdc9f1 = InputPrivacyRule;\ninputPrivacyValueAllowBots#5a4fcce5 = InputPrivacyRule;\ninputPrivacyValueDisallowBots#c4e57915 = InputPrivacyRule;\n\nprivacyValueAllowContacts#fffe1bac = PrivacyRule;\nprivacyValueAllowAll#65427b82 = PrivacyRule;\nprivacyValueAllowUsers#b8905fb2 users:Vector<long> = PrivacyRule;\nprivacyValueDisallowContacts#f888fa1a = PrivacyRule;\nprivacyValueDisallowAll#8b73e763 = PrivacyRule;\nprivacyValueDisallowUsers#e4621141 users:Vector<long> = PrivacyRule;\nprivacyValueAllowChatParticipants#6b134e8e chats:Vector<long> = PrivacyRule;\nprivacyValueDisallowChatParticipants#41c87565 chats:Vector<long> = PrivacyRule;\nprivacyValueAllowCloseFriends#f7e8d89b = PrivacyRule;\nprivacyValueAllowPremium#ece9814b = PrivacyRule;\nprivacyValueAllowBots#21461b5d = PrivacyRule;\nprivacyValueDisallowBots#f6a5f82f = PrivacyRule;\n\naccount.privacyRules#50a04e45 rules:Vector<PrivacyRule> chats:Vector<Chat> users:Vector<User> = account.PrivacyRules;\n\naccountDaysTTL#b8d0afdf days:int = AccountDaysTTL;\n\ndocumentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute;\ndocumentAttributeAnimated#11b58939 = DocumentAttribute;\ndocumentAttributeSticker#6319d612 flags:# mask:flags.1?true alt:string stickerset:InputStickerSet mask_coords:flags.0?MaskCoords = DocumentAttribute;\ndocumentAttributeVideo#43c57c48 flags:# round_message:flags.0?true supports_streaming:flags.1?true nosound:flags.3?true duration:double w:int h:int preload_prefix_size:flags.2?int video_start_ts:flags.4?double video_codec:flags.5?string = DocumentAttribute;\ndocumentAttributeAudio#9852f9c6 flags:# voice:flags.10?true duration:int title:flags.0?string performer:flags.1?string waveform:flags.2?bytes = DocumentAttribute;\ndocumentAttributeFilename#15590068 file_name:string = DocumentAttribute;\ndocumentAttributeHasStickers#9801d2f7 = DocumentAttribute;\ndocumentAttributeCustomEmoji#fd149899 flags:# free:flags.0?true text_color:flags.1?true alt:string stickerset:InputStickerSet = DocumentAttribute;\n\nmessages.stickersNotModified#f1749a22 = messages.Stickers;\nmessages.stickers#30a6ec7e hash:long stickers:Vector<Document> = messages.Stickers;\n\nstickerPack#12b299d4 emoticon:string documents:Vector<long> = StickerPack;\n\nmessages.allStickersNotModified#e86602c3 = messages.AllStickers;\nmessages.allStickers#cdbbcebb hash:long sets:Vector<StickerSet> = messages.AllStickers;\n\nmessages.affectedMessages#84d19185 pts:int pts_count:int = messages.AffectedMessages;\n\nwebPageEmpty#211a1788 flags:# id:long url:flags.0?string = WebPage;\nwebPagePending#b0d13e47 flags:# id:long url:flags.0?string date:int = WebPage;\nwebPage#e89c45b2 flags:# has_large_media:flags.13?true video_cover_photo:flags.14?true id:long url:string display_url:string hash:int type:flags.0?string site_name:flags.1?string title:flags.2?string description:flags.3?string photo:flags.4?Photo embed_url:flags.5?string embed_type:flags.5?string embed_width:flags.6?int embed_height:flags.6?int duration:flags.7?int author:flags.8?string document:flags.9?Document cached_page:flags.10?Page attributes:flags.12?Vector<WebPageAttribute> = WebPage;\nwebPageNotModified#7311ca11 flags:# cached_page_views:flags.0?int = WebPage;\n\nauthorization#ad01d61d flags:# current:flags.0?true official_app:flags.1?true password_pending:flags.2?true encrypted_requests_disabled:flags.3?true call_requests_disabled:flags.4?true unconfirmed:flags.5?true hash:long device_model:string platform:string system_version:string api_id:int app_name:string app_version:string date_created:int date_active:int ip:string country:string region:string = Authorization;\n\naccount.authorizations#4bff8ea0 authorization_ttl_days:int authorizations:Vector<Authorization> = account.Authorizations;\n\naccount.password#957b50fb flags:# has_recovery:flags.0?true has_secure_values:flags.1?true has_password:flags.2?true current_algo:flags.2?PasswordKdfAlgo srp_B:flags.2?bytes srp_id:flags.2?long hint:flags.3?string email_unconfirmed_pattern:flags.4?string new_algo:PasswordKdfAlgo new_secure_algo:SecurePasswordKdfAlgo secure_random:bytes pending_reset_date:flags.5?int login_email_pattern:flags.6?string = account.Password;\n\naccount.passwordSettings#9a5c33e5 flags:# email:flags.0?string secure_settings:flags.1?SecureSecretSettings = account.PasswordSettings;\n\naccount.passwordInputSettings#c23727c9 flags:# new_algo:flags.0?PasswordKdfAlgo new_password_hash:flags.0?bytes hint:flags.0?string email:flags.1?string new_secure_settings:flags.2?SecureSecretSettings = account.PasswordInputSettings;\n\nauth.passwordRecovery#137948a5 email_pattern:string = auth.PasswordRecovery;\n\nreceivedNotifyMessage#a384b779 id:int flags:int = ReceivedNotifyMessage;\n\nchatInviteExported#a22cbd96 flags:# revoked:flags.0?true permanent:flags.5?true request_needed:flags.6?true link:string admin_id:long date:int start_date:flags.4?int expire_date:flags.1?int usage_limit:flags.2?int usage:flags.3?int requested:flags.7?int subscription_expired:flags.10?int title:flags.8?string subscription_pricing:flags.9?StarsSubscriptionPricing = ExportedChatInvite;\nchatInvitePublicJoinRequests#ed107ab7 = ExportedChatInvite;\n\nchatInviteAlready#5a686d7c chat:Chat = ChatInvite;\nchatInvite#5c9d3702 flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true request_needed:flags.6?true verified:flags.7?true scam:flags.8?true fake:flags.9?true can_refulfill_subscription:flags.11?true title:string about:flags.5?string photo:Photo participants_count:int participants:flags.4?Vector<User> color:int subscription_pricing:flags.10?StarsSubscriptionPricing subscription_form_id:flags.12?long bot_verification:flags.13?BotVerification = ChatInvite;\nchatInvitePeek#61695cb0 chat:Chat expires:int = ChatInvite;\n\ninputStickerSetEmpty#ffb62b95 = InputStickerSet;\ninputStickerSetID#9de7a269 id:long access_hash:long = InputStickerSet;\ninputStickerSetShortName#861cc8a0 short_name:string = InputStickerSet;\ninputStickerSetAnimatedEmoji#28703c8 = InputStickerSet;\ninputStickerSetDice#e67f520e emoticon:string = InputStickerSet;\ninputStickerSetAnimatedEmojiAnimations#cde3739 = InputStickerSet;\ninputStickerSetPremiumGifts#c88b3b02 = InputStickerSet;\ninputStickerSetEmojiGenericAnimations#4c4d4ce = InputStickerSet;\ninputStickerSetEmojiDefaultStatuses#29d0f5ee = InputStickerSet;\ninputStickerSetEmojiDefaultTopicIcons#44c1f8e9 = InputStickerSet;\ninputStickerSetEmojiChannelDefaultStatuses#49748553 = InputStickerSet;\ninputStickerSetTonGifts#1cf671a0 = InputStickerSet;\n\nstickerSet#2dd14edc flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true emojis:flags.7?true text_color:flags.9?true channel_emoji_status:flags.10?true creator:flags.11?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector<PhotoSize> thumb_dc_id:flags.4?int thumb_version:flags.4?int thumb_document_id:flags.8?long count:int hash:int = StickerSet;\n\nmessages.stickerSet#6e153f16 set:StickerSet packs:Vector<StickerPack> keywords:Vector<StickerKeyword> documents:Vector<Document> = messages.StickerSet;\nmessages.stickerSetNotModified#d3f924eb = messages.StickerSet;\n\nbotCommand#c27ac8c7 command:string description:string = BotCommand;\n\nbotInfo#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<BotCommand> menu_button:flags.3?BotMenuButton privacy_policy_url:flags.7?string app_settings:flags.8?BotAppSettings verifier_settings:flags.9?BotVerifierSettings = BotInfo;\n\nkeyboardButton#7d170cff flags:# style:flags.10?KeyboardButtonStyle text:string = KeyboardButton;\nkeyboardButtonUrl#d80c25ec flags:# style:flags.10?KeyboardButtonStyle text:string url:string = KeyboardButton;\nkeyboardButtonCallback#e62bc960 flags:# requires_password:flags.0?true style:flags.10?KeyboardButtonStyle text:string data:bytes = KeyboardButton;\nkeyboardButtonRequestPhone#417efd8f flags:# style:flags.10?KeyboardButtonStyle text:string = KeyboardButton;\nkeyboardButtonRequestGeoLocation#aa40f94d flags:# style:flags.10?KeyboardButtonStyle text:string = KeyboardButton;\nkeyboardButtonSwitchInline#991399fc flags:# same_peer:flags.0?true style:flags.10?KeyboardButtonStyle text:string query:string peer_types:flags.1?Vector<InlineQueryPeerType> = KeyboardButton;\nkeyboardButtonGame#89c590f9 flags:# style:flags.10?KeyboardButtonStyle text:string = KeyboardButton;\nkeyboardButtonBuy#3fa53905 flags:# style:flags.10?KeyboardButtonStyle text:string = KeyboardButton;\nkeyboardButtonUrlAuth#f51006f9 flags:# style:flags.10?KeyboardButtonStyle text:string fwd_text:flags.0?string url:string button_id:int = KeyboardButton;\ninputKeyboardButtonUrlAuth#68013e72 flags:# request_write_access:flags.0?true style:flags.10?KeyboardButtonStyle text:string fwd_text:flags.1?string url:string bot:InputUser = KeyboardButton;\nkeyboardButtonRequestPoll#7a11d782 flags:# style:flags.10?KeyboardButtonStyle quiz:flags.0?Bool text:string = KeyboardButton;\ninputKeyboardButtonUserProfile#7d5e07c7 flags:# style:flags.10?KeyboardButtonStyle text:string user_id:InputUser = KeyboardButton;\nkeyboardButtonUserProfile#c0fd5d09 flags:# style:flags.10?KeyboardButtonStyle text:string user_id:long = KeyboardButton;\nkeyboardButtonWebView#e846b1a0 flags:# style:flags.10?KeyboardButtonStyle text:string url:string = KeyboardButton;\nkeyboardButtonSimpleWebView#e15c4370 flags:# style:flags.10?KeyboardButtonStyle text:string url:string = KeyboardButton;\nkeyboardButtonRequestPeer#5b0f15f5 flags:# style:flags.10?KeyboardButtonStyle text:string button_id:int peer_type:RequestPeerType max_quantity:int = KeyboardButton;\ninputKeyboardButtonRequestPeer#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;\nkeyboardButtonCopy#bcc4af10 flags:# style:flags.10?KeyboardButtonStyle text:string copy_text:string = KeyboardButton;\n\nkeyboardButtonRow#77608b83 buttons:Vector<KeyboardButton> = KeyboardButtonRow;\n\nreplyKeyboardHide#a03e5b85 flags:# selective:flags.2?true = ReplyMarkup;\nreplyKeyboardForceReply#86b40b08 flags:# single_use:flags.1?true selective:flags.2?true placeholder:flags.3?string = ReplyMarkup;\nreplyKeyboardMarkup#85dd99d1 flags:# resize:flags.0?true single_use:flags.1?true selective:flags.2?true persistent:flags.4?true rows:Vector<KeyboardButtonRow> placeholder:flags.3?string = ReplyMarkup;\nreplyInlineMarkup#48a30254 rows:Vector<KeyboardButtonRow> = ReplyMarkup;\n\nmessageEntityUnknown#bb92ba95 offset:int length:int = MessageEntity;\nmessageEntityMention#fa04579d offset:int length:int = MessageEntity;\nmessageEntityHashtag#6f635b0d offset:int length:int = MessageEntity;\nmessageEntityBotCommand#6cef8ac7 offset:int length:int = MessageEntity;\nmessageEntityUrl#6ed02538 offset:int length:int = MessageEntity;\nmessageEntityEmail#64e475c2 offset:int length:int = MessageEntity;\nmessageEntityBold#bd610bc9 offset:int length:int = MessageEntity;\nmessageEntityItalic#826f8b60 offset:int length:int = MessageEntity;\nmessageEntityCode#28a20571 offset:int length:int = MessageEntity;\nmessageEntityPre#73924be0 offset:int length:int language:string = MessageEntity;\nmessageEntityTextUrl#76a6d327 offset:int length:int url:string = MessageEntity;\nmessageEntityMentionName#dc7b1140 offset:int length:int user_id:long = MessageEntity;\ninputMessageEntityMentionName#208e68c9 offset:int length:int user_id:InputUser = MessageEntity;\nmessageEntityPhone#9b69e34b offset:int length:int = MessageEntity;\nmessageEntityCashtag#4c4e743f offset:int length:int = MessageEntity;\nmessageEntityUnderline#9c4e7e8b offset:int length:int = MessageEntity;\nmessageEntityStrike#bf0693d4 offset:int length:int = MessageEntity;\nmessageEntityBankCard#761e6af4 offset:int length:int = MessageEntity;\nmessageEntitySpoiler#32ca960f offset:int length:int = MessageEntity;\nmessageEntityCustomEmoji#c8cf05f8 offset:int length:int document_id:long = MessageEntity;\nmessageEntityBlockquote#f1ccaaac flags:# collapsed:flags.0?true offset:int length:int = MessageEntity;\nmessageEntityFormattedDate#904ac7c7 flags:# relative:flags.0?true short_time:flags.1?true long_time:flags.2?true short_date:flags.3?true long_date:flags.4?true day_of_week:flags.5?true offset:int length:int date:int = MessageEntity;\nmessageEntityDiffInsert#71777116 offset:int length:int = MessageEntity;\nmessageEntityDiffReplace#c6c1e5a7 offset:int length:int old_text:string = MessageEntity;\nmessageEntityDiffDelete#652c1c5 offset:int length:int = MessageEntity;\n\ninputChannelEmpty#ee8c1e86 = InputChannel;\ninputChannel#f35aec28 channel_id:long access_hash:long = InputChannel;\ninputChannelFromMessage#5b934f9d peer:InputPeer msg_id:int channel_id:long = InputChannel;\n\ncontacts.resolvedPeer#7f077ad9 peer:Peer chats:Vector<Chat> users:Vector<User> = contacts.ResolvedPeer;\n\nmessageRange#ae30253 min_id:int max_id:int = MessageRange;\n\nupdates.channelDifferenceEmpty#3e11affb flags:# final:flags.0?true pts:int timeout:flags.1?int = updates.ChannelDifference;\nupdates.channelDifferenceTooLong#a4bcc6fe flags:# final:flags.0?true timeout:flags.1?int dialog:Dialog messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = updates.ChannelDifference;\nupdates.channelDifference#2064674e flags:# final:flags.0?true pts:int timeout:flags.1?int new_messages:Vector<Message> other_updates:Vector<Update> chats:Vector<Chat> users:Vector<User> = updates.ChannelDifference;\n\nchannelMessagesFilterEmpty#94d42ee7 = ChannelMessagesFilter;\nchannelMessagesFilter#cd77d957 flags:# exclude_new_messages:flags.1?true ranges:Vector<MessageRange> = ChannelMessagesFilter;\n\nchannelParticipant#1bd54456 flags:# user_id:long date:int subscription_until_date:flags.0?int rank:flags.2?string = ChannelParticipant;\nchannelParticipantSelf#a9478a1a flags:# via_request:flags.0?true user_id:long inviter_id:long date:int subscription_until_date:flags.1?int rank:flags.2?string = ChannelParticipant;\nchannelParticipantCreator#2fe601d3 flags:# user_id:long admin_rights:ChatAdminRights rank:flags.0?string = ChannelParticipant;\nchannelParticipantAdmin#34c3bb53 flags:# can_edit:flags.0?true self:flags.1?true user_id:long inviter_id:flags.1?long promoted_by:long date:int admin_rights:ChatAdminRights rank:flags.2?string = ChannelParticipant;\nchannelParticipantBanned#d5f0ad91 flags:# left:flags.0?true peer:Peer kicked_by:long date:int banned_rights:ChatBannedRights rank:flags.2?string = ChannelParticipant;\nchannelParticipantLeft#1b03f006 peer:Peer = ChannelParticipant;\n\nchannelParticipantsRecent#de3f3c79 = ChannelParticipantsFilter;\nchannelParticipantsAdmins#b4608969 = ChannelParticipantsFilter;\nchannelParticipantsKicked#a3b54985 q:string = ChannelParticipantsFilter;\nchannelParticipantsBots#b0d1865b = ChannelParticipantsFilter;\nchannelParticipantsBanned#1427a5e1 q:string = ChannelParticipantsFilter;\nchannelParticipantsSearch#656ac4b q:string = ChannelParticipantsFilter;\nchannelParticipantsContacts#bb6ae88d q:string = ChannelParticipantsFilter;\nchannelParticipantsMentions#e04b5ceb flags:# q:flags.0?string top_msg_id:flags.1?int = ChannelParticipantsFilter;\n\nchannels.channelParticipants#9ab0feaf count:int participants:Vector<ChannelParticipant> chats:Vector<Chat> users:Vector<User> = channels.ChannelParticipants;\nchannels.channelParticipantsNotModified#f0173fe9 = channels.ChannelParticipants;\n\nchannels.channelParticipant#dfb80317 participant:ChannelParticipant chats:Vector<Chat> users:Vector<User> = channels.ChannelParticipant;\n\nhelp.termsOfService#780a0310 flags:# popup:flags.0?true id:DataJSON text:string entities:Vector<MessageEntity> min_age_confirm:flags.1?int = help.TermsOfService;\n\nmessages.savedGifsNotModified#e8025ca2 = messages.SavedGifs;\nmessages.savedGifs#84a02a0d hash:long gifs:Vector<Document> = messages.SavedGifs;\n\ninputBotInlineMessageMediaAuto#3380c786 flags:# invert_media:flags.3?true message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;\ninputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true invert_media:flags.3?true message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;\ninputBotInlineMessageMediaGeo#96929a85 flags:# geo_point:InputGeoPoint heading:flags.0?int period:flags.1?int proximity_notification_radius:flags.3?int reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;\ninputBotInlineMessageMediaVenue#417bbf11 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;\ninputBotInlineMessageMediaContact#a6edbffd flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;\ninputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;\ninputBotInlineMessageMediaInvoice#d7e78225 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;\ninputBotInlineMessageMediaWebPage#bddcc510 flags:# invert_media:flags.3?true force_large_media:flags.4?true force_small_media:flags.5?true optional:flags.6?true message:string entities:flags.1?Vector<MessageEntity> url:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;\n\ninputBotInlineResult#88bf9319 flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?InputWebDocument content:flags.5?InputWebDocument send_message:InputBotInlineMessage = InputBotInlineResult;\ninputBotInlineResultPhoto#a8d864a7 id:string type:string photo:InputPhoto send_message:InputBotInlineMessage = InputBotInlineResult;\ninputBotInlineResultDocument#fff8fdc4 flags:# id:string type:string title:flags.1?string description:flags.2?string document:InputDocument send_message:InputBotInlineMessage = InputBotInlineResult;\ninputBotInlineResultGame#4fa417f2 id:string short_name:string send_message:InputBotInlineMessage = InputBotInlineResult;\n\nbotInlineMessageMediaAuto#764cf810 flags:# invert_media:flags.3?true message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = BotInlineMessage;\nbotInlineMessageText#8c7f65e2 flags:# no_webpage:flags.0?true invert_media:flags.3?true message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = BotInlineMessage;\nbotInlineMessageMediaGeo#51846fd flags:# geo:GeoPoint heading:flags.0?int period:flags.1?int proximity_notification_radius:flags.3?int reply_markup:flags.2?ReplyMarkup = BotInlineMessage;\nbotInlineMessageMediaVenue#8a86659c flags:# geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage;\nbotInlineMessageMediaContact#18d1cdc2 flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage;\nbotInlineMessageMediaInvoice#354a9b09 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument currency:string total_amount:long reply_markup:flags.2?ReplyMarkup = BotInlineMessage;\nbotInlineMessageMediaWebPage#809ad9a6 flags:# invert_media:flags.3?true force_large_media:flags.4?true force_small_media:flags.5?true manual:flags.7?true safe:flags.8?true message:string entities:flags.1?Vector<MessageEntity> url:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage;\n\nbotInlineResult#11965f3a flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?WebDocument content:flags.5?WebDocument send_message:BotInlineMessage = BotInlineResult;\nbotInlineMediaResult#17db940b flags:# id:string type:string photo:flags.0?Photo document:flags.1?Document title:flags.2?string description:flags.3?string send_message:BotInlineMessage = BotInlineResult;\n\nmessages.botResults#e021f2f6 flags:# gallery:flags.0?true query_id:long next_offset:flags.1?string switch_pm:flags.2?InlineBotSwitchPM switch_webview:flags.3?InlineBotWebView results:Vector<BotInlineResult> cache_time:int users:Vector<User> = messages.BotResults;\n\nexportedMessageLink#5dab1af4 link:string html:string = ExportedMessageLink;\n\nmessageFwdHeader#4e4df4bb flags:# imported:flags.7?true saved_out:flags.11?true from_id:flags.0?Peer from_name:flags.5?string date:int channel_post:flags.2?int post_author:flags.3?string saved_from_peer:flags.4?Peer saved_from_msg_id:flags.4?int saved_from_id:flags.8?Peer saved_from_name:flags.9?string saved_date:flags.10?int psa_type:flags.6?string = MessageFwdHeader;\n\nauth.codeTypeSms#72a3158c = auth.CodeType;\nauth.codeTypeCall#741cd3e3 = auth.CodeType;\nauth.codeTypeFlashCall#226ccefb = auth.CodeType;\nauth.codeTypeMissedCall#d61ad6ee = auth.CodeType;\nauth.codeTypeFragmentSms#6ed998c = auth.CodeType;\n\nauth.sentCodeTypeApp#3dbb5986 length:int = auth.SentCodeType;\nauth.sentCodeTypeSms#c000bba2 length:int = auth.SentCodeType;\nauth.sentCodeTypeCall#5353e5a7 length:int = auth.SentCodeType;\nauth.sentCodeTypeFlashCall#ab03c6d9 pattern:string = auth.SentCodeType;\nauth.sentCodeTypeMissedCall#82006484 prefix:string length:int = auth.SentCodeType;\nauth.sentCodeTypeEmailCode#f450f59b flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true email_pattern:string length:int reset_available_period:flags.3?int reset_pending_date:flags.4?int = auth.SentCodeType;\nauth.sentCodeTypeSetUpEmailRequired#a5491dea flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true = auth.SentCodeType;\nauth.sentCodeTypeFragmentSms#d9565c39 url:string length:int = auth.SentCodeType;\nauth.sentCodeTypeFirebaseSms#9fd736 flags:# nonce:flags.0?bytes play_integrity_project_id:flags.2?long play_integrity_nonce:flags.2?bytes receipt:flags.1?string push_timeout:flags.1?int length:int = auth.SentCodeType;\nauth.sentCodeTypeSmsWord#a416ac81 flags:# beginning:flags.0?string = auth.SentCodeType;\nauth.sentCodeTypeSmsPhrase#b37794af flags:# beginning:flags.0?string = auth.SentCodeType;\n\nmessages.botCallbackAnswer#36585ea4 flags:# alert:flags.1?true has_url:flags.3?true native_ui:flags.4?true message:flags.0?string url:flags.2?string cache_time:int = messages.BotCallbackAnswer;\n\nmessages.messageEditData#26b5dde6 flags:# caption:flags.0?true = messages.MessageEditData;\n\ninputBotInlineMessageID#890c3d89 dc_id:int id:long access_hash:long = InputBotInlineMessageID;\ninputBotInlineMessageID64#b6d915d7 dc_id:int owner_id:long id:int access_hash:long = InputBotInlineMessageID;\n\ninlineBotSwitchPM#3c20629f text:string start_param:string = InlineBotSwitchPM;\n\nmessages.peerDialogs#3371c354 dialogs:Vector<Dialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> state:updates.State = messages.PeerDialogs;\n\ntopPeer#edcdc05b peer:Peer rating:double = TopPeer;\n\ntopPeerCategoryBotsPM#ab661b5b = TopPeerCategory;\ntopPeerCategoryBotsInline#148677e2 = TopPeerCategory;\ntopPeerCategoryCorrespondents#637b7ed = TopPeerCategory;\ntopPeerCategoryGroups#bd17a14a = TopPeerCategory;\ntopPeerCategoryChannels#161d9628 = TopPeerCategory;\ntopPeerCategoryPhoneCalls#1e76a78c = TopPeerCategory;\ntopPeerCategoryForwardUsers#a8406ca9 = TopPeerCategory;\ntopPeerCategoryForwardChats#fbeec0f0 = TopPeerCategory;\ntopPeerCategoryBotsApp#fd9e7bec = TopPeerCategory;\n\ntopPeerCategoryPeers#fb834291 category:TopPeerCategory count:int peers:Vector<TopPeer> = TopPeerCategoryPeers;\n\ncontacts.topPeersNotModified#de266ef5 = contacts.TopPeers;\ncontacts.topPeers#70b772a8 categories:Vector<TopPeerCategoryPeers> chats:Vector<Chat> users:Vector<User> = contacts.TopPeers;\ncontacts.topPeersDisabled#b52c939d = contacts.TopPeers;\n\ndraftMessageEmpty#1b0c841a flags:# date:flags.0?int = DraftMessage;\ndraftMessage#96eaa5eb flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia date:int effect:flags.7?long suggested_post:flags.8?SuggestedPost = DraftMessage;\n\nmessages.featuredStickersNotModified#c6dc0c66 count:int = messages.FeaturedStickers;\nmessages.featuredStickers#be382906 flags:# premium:flags.0?true hash:long count:int sets:Vector<StickerSetCovered> unread:Vector<long> = messages.FeaturedStickers;\n\nmessages.recentStickersNotModified#b17f890 = messages.RecentStickers;\nmessages.recentStickers#88d37c56 hash:long packs:Vector<StickerPack> stickers:Vector<Document> dates:Vector<int> = messages.RecentStickers;\n\nmessages.archivedStickers#4fcba9c8 count:int sets:Vector<StickerSetCovered> = messages.ArchivedStickers;\n\nmessages.stickerSetInstallResultSuccess#38641628 = messages.StickerSetInstallResult;\nmessages.stickerSetInstallResultArchive#35e410a8 sets:Vector<StickerSetCovered> = messages.StickerSetInstallResult;\n\nstickerSetCovered#6410a5d2 set:StickerSet cover:Document = StickerSetCovered;\nstickerSetMultiCovered#3407e51b set:StickerSet covers:Vector<Document> = StickerSetCovered;\nstickerSetFullCovered#40d13c0e set:StickerSet packs:Vector<StickerPack> keywords:Vector<StickerKeyword> documents:Vector<Document> = StickerSetCovered;\nstickerSetNoCovered#77b15d1c set:StickerSet = StickerSetCovered;\n\nmaskCoords#aed6dbb2 n:int x:double y:double zoom:double = MaskCoords;\n\ninputStickeredMediaPhoto#4a992157 id:InputPhoto = InputStickeredMedia;\ninputStickeredMediaDocument#438865b id:InputDocument = InputStickeredMedia;\n\ngame#bdf9653b flags:# id:long access_hash:long short_name:string title:string description:string photo:Photo document:flags.0?Document = Game;\n\ninputGameID#32c3e77 id:long access_hash:long = InputGame;\ninputGameShortName#c331e80a bot_id:InputUser short_name:string = InputGame;\n\nhighScore#73a379eb pos:int user_id:long score:int = HighScore;\n\nmessages.highScores#9a3bfd99 scores:Vector<HighScore> users:Vector<User> = messages.HighScores;\n\ntextEmpty#dc3d824f = RichText;\ntextPlain#744694e0 text:string = RichText;\ntextBold#6724abc4 text:RichText = RichText;\ntextItalic#d912a59c text:RichText = RichText;\ntextUnderline#c12622c4 text:RichText = RichText;\ntextStrike#9bf8bb95 text:RichText = RichText;\ntextFixed#6c3f19b9 text:RichText = RichText;\ntextUrl#3c2884c1 text:RichText url:string webpage_id:long = RichText;\ntextEmail#de5a0dd6 text:RichText email:string = RichText;\ntextConcat#7e6260d7 texts:Vector<RichText> = RichText;\ntextSubscript#ed6a8504 text:RichText = RichText;\ntextSuperscript#c7fb5e01 text:RichText = RichText;\ntextMarked#34b8621 text:RichText = RichText;\ntextPhone#1ccb966a text:RichText phone:string = RichText;\ntextImage#81ccf4f document_id:long w:int h:int = RichText;\ntextAnchor#35553762 text:RichText name:string = RichText;\n\npageBlockUnsupported#13567e8a = PageBlock;\npageBlockTitle#70abc3fd text:RichText = PageBlock;\npageBlockSubtitle#8ffa9a1f text:RichText = PageBlock;\npageBlockAuthorDate#baafe5e0 author:RichText published_date:int = PageBlock;\npageBlockHeader#bfd064ec text:RichText = PageBlock;\npageBlockSubheader#f12bb6e1 text:RichText = PageBlock;\npageBlockParagraph#467a0766 text:RichText = PageBlock;\npageBlockPreformatted#c070d93e text:RichText language:string = PageBlock;\npageBlockFooter#48870999 text:RichText = PageBlock;\npageBlockDivider#db20b188 = PageBlock;\npageBlockAnchor#ce0d37b0 name:string = PageBlock;\npageBlockList#e4e88011 items:Vector<PageListItem> = PageBlock;\npageBlockBlockquote#263d7c26 text:RichText caption:RichText = PageBlock;\npageBlockPullquote#4f4456d3 text:RichText caption:RichText = PageBlock;\npageBlockPhoto#1759c560 flags:# photo_id:long caption:PageCaption url:flags.0?string webpage_id:flags.0?long = PageBlock;\npageBlockVideo#7c8fe7b6 flags:# autoplay:flags.0?true loop:flags.1?true video_id:long caption:PageCaption = PageBlock;\npageBlockCover#39f23300 cover:PageBlock = PageBlock;\npageBlockEmbed#a8718dc5 flags:# full_width:flags.0?true allow_scrolling:flags.3?true url:flags.1?string html:flags.2?string poster_photo_id:flags.4?long w:flags.5?int h:flags.5?int caption:PageCaption = PageBlock;\npageBlockEmbedPost#f259a80b url:string webpage_id:long author_photo_id:long author:string date:int blocks:Vector<PageBlock> caption:PageCaption = PageBlock;\npageBlockCollage#65a0fa4d items:Vector<PageBlock> caption:PageCaption = PageBlock;\npageBlockSlideshow#31f9590 items:Vector<PageBlock> caption:PageCaption = PageBlock;\npageBlockChannel#ef1751b5 channel:Chat = PageBlock;\npageBlockAudio#804361ea audio_id:long caption:PageCaption = PageBlock;\npageBlockKicker#1e148390 text:RichText = PageBlock;\npageBlockTable#bf4dea82 flags:# bordered:flags.0?true striped:flags.1?true title:RichText rows:Vector<PageTableRow> = PageBlock;\npageBlockOrderedList#9a8ae1e1 items:Vector<PageListOrderedItem> = PageBlock;\npageBlockDetails#76768bed flags:# open:flags.0?true blocks:Vector<PageBlock> title:RichText = PageBlock;\npageBlockRelatedArticles#16115a96 title:RichText articles:Vector<PageRelatedArticle> = PageBlock;\npageBlockMap#a44f3ef6 geo:GeoPoint zoom:int w:int h:int caption:PageCaption = PageBlock;\n\nphoneCallDiscardReasonMissed#85e42301 = PhoneCallDiscardReason;\nphoneCallDiscardReasonDisconnect#e095c1a0 = PhoneCallDiscardReason;\nphoneCallDiscardReasonHangup#57adc690 = PhoneCallDiscardReason;\nphoneCallDiscardReasonBusy#faf7e8c9 = PhoneCallDiscardReason;\nphoneCallDiscardReasonMigrateConferenceCall#9fbbf1f7 slug:string = PhoneCallDiscardReason;\n\ndataJSON#7d748d04 data:string = DataJSON;\n\nlabeledPrice#cb296bf8 label:string amount:long = LabeledPrice;\n\ninvoice#49ee584 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true recurring:flags.9?true currency:string prices:Vector<LabeledPrice> max_tip_amount:flags.8?long suggested_tip_amounts:flags.8?Vector<long> terms_url:flags.10?string subscription_period:flags.11?int = Invoice;\n\npaymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge;\n\npostAddress#1e8caaeb street_line1:string street_line2:string city:string state:string country_iso2:string post_code:string = PostAddress;\n\npaymentRequestedInfo#909c3f94 flags:# name:flags.0?string phone:flags.1?string email:flags.2?string shipping_address:flags.3?PostAddress = PaymentRequestedInfo;\n\npaymentSavedCredentialsCard#cdc27a1f id:string title:string = PaymentSavedCredentials;\n\nwebDocument#1c570ed1 url:string access_hash:long size:int mime_type:string attributes:Vector<DocumentAttribute> = WebDocument;\nwebDocumentNoProxy#f9c8bcc6 url:string size:int mime_type:string attributes:Vector<DocumentAttribute> = WebDocument;\n\ninputWebDocument#9bed434d url:string size:int mime_type:string attributes:Vector<DocumentAttribute> = InputWebDocument;\n\ninputWebFileLocation#c239d686 url:string access_hash:long = InputWebFileLocation;\ninputWebFileGeoPointLocation#9f2221c9 geo_point:InputGeoPoint access_hash:long w:int h:int zoom:int scale:int = InputWebFileLocation;\ninputWebFileAudioAlbumThumbLocation#f46fe924 flags:# small:flags.2?true document:flags.0?InputDocument title:flags.1?string performer:flags.1?string = InputWebFileLocation;\n\nupload.webFile#21e753bc size:int mime_type:string file_type:storage.FileType mtime:int bytes:bytes = upload.WebFile;\n\npayments.paymentForm#a0058751 flags:# can_save_credentials:flags.2?true password_missing:flags.3?true form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice provider_id:long url:string native_provider:flags.4?string native_params:flags.4?DataJSON additional_methods:flags.6?Vector<PaymentFormMethod> saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?Vector<PaymentSavedCredentials> users:Vector<User> = payments.PaymentForm;\npayments.paymentFormStars#7bf6b15c flags:# form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice users:Vector<User> = payments.PaymentForm;\npayments.paymentFormStarGift#b425cfe1 form_id:long invoice:Invoice = payments.PaymentForm;\n\npayments.validatedRequestedInfo#d1451883 flags:# id:flags.0?string shipping_options:flags.1?Vector<ShippingOption> = payments.ValidatedRequestedInfo;\n\npayments.paymentResult#4e5f810d updates:Updates = payments.PaymentResult;\npayments.paymentVerificationNeeded#d8411139 url:string = payments.PaymentResult;\n\npayments.paymentReceipt#70c4fe03 flags:# date:int bot_id:long provider_id:long title:string description:string photo:flags.2?WebDocument invoice:Invoice info:flags.0?PaymentRequestedInfo shipping:flags.1?ShippingOption tip_amount:flags.3?long currency:string total_amount:long credentials_title:string users:Vector<User> = payments.PaymentReceipt;\npayments.paymentReceiptStars#dabbf83a flags:# date:int bot_id:long title:string description:string photo:flags.2?WebDocument invoice:Invoice currency:string total_amount:long transaction_id:string users:Vector<User> = payments.PaymentReceipt;\n\npayments.savedInfo#fb8fe43c flags:# has_saved_credentials:flags.1?true saved_info:flags.0?PaymentRequestedInfo = payments.SavedInfo;\n\ninputPaymentCredentialsSaved#c10eb2cf id:string tmp_password:bytes = InputPaymentCredentials;\ninputPaymentCredentials#3417d728 flags:# save:flags.0?true data:DataJSON = InputPaymentCredentials;\ninputPaymentCredentialsApplePay#aa1c39f payment_data:DataJSON = InputPaymentCredentials;\ninputPaymentCredentialsGooglePay#8ac32801 payment_token:DataJSON = InputPaymentCredentials;\n\naccount.tmpPassword#db64fd34 tmp_password:bytes valid_until:int = account.TmpPassword;\n\nshippingOption#b6213cdf id:string title:string prices:Vector<LabeledPrice> = ShippingOption;\n\ninputStickerSetItem#32da9e9c flags:# document:InputDocument emoji:string mask_coords:flags.0?MaskCoords keywords:flags.1?string = InputStickerSetItem;\n\ninputPhoneCall#1e36fded id:long access_hash:long = InputPhoneCall;\n\nphoneCallEmpty#5366c915 id:long = PhoneCall;\nphoneCallWaiting#c5226f17 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long protocol:PhoneCallProtocol receive_date:flags.0?int = PhoneCall;\nphoneCallRequested#14b0ed0c flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_a_hash:bytes protocol:PhoneCallProtocol = PhoneCall;\nphoneCallAccepted#3660c311 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_b:bytes protocol:PhoneCallProtocol = PhoneCall;\nphoneCall#30535af5 flags:# p2p_allowed:flags.5?true video:flags.6?true conference_supported:flags.8?true id:long access_hash:long date:int admin_id:long participant_id:long g_a_or_b:bytes key_fingerprint:long protocol:PhoneCallProtocol connections:Vector<PhoneConnection> start_date:int custom_parameters:flags.7?DataJSON = PhoneCall;\nphoneCallDiscarded#50ca4de1 flags:# need_rating:flags.2?true need_debug:flags.3?true video:flags.6?true id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = PhoneCall;\n\nphoneConnection#9cc123c7 flags:# tcp:flags.0?true id:long ip:string ipv6:string port:int peer_tag:bytes = PhoneConnection;\nphoneConnectionWebrtc#635fe375 flags:# turn:flags.0?true stun:flags.1?true id:long ip:string ipv6:string port:int username:string password:string = PhoneConnection;\n\nphoneCallProtocol#fc878fc8 flags:# udp_p2p:flags.0?true udp_reflector:flags.1?true min_layer:int max_layer:int library_versions:Vector<string> = PhoneCallProtocol;\n\nphone.phoneCall#ec82e140 phone_call:PhoneCall users:Vector<User> = phone.PhoneCall;\n\nupload.cdnFileReuploadNeeded#eea8e46e request_token:bytes = upload.CdnFile;\nupload.cdnFile#a99fca4f bytes:bytes = upload.CdnFile;\n\ncdnPublicKey#c982eaba dc_id:int public_key:string = CdnPublicKey;\n\ncdnConfig#5725e40a public_keys:Vector<CdnPublicKey> = CdnConfig;\n\nlangPackString#cad181f6 key:string value:string = LangPackString;\nlangPackStringPluralized#6c47ac9f flags:# key:string zero_value:flags.0?string one_value:flags.1?string two_value:flags.2?string few_value:flags.3?string many_value:flags.4?string other_value:string = LangPackString;\nlangPackStringDeleted#2979eeb2 key:string = LangPackString;\n\nlangPackDifference#f385c1f6 lang_code:string from_version:int version:int strings:Vector<LangPackString> = LangPackDifference;\n\nlangPackLanguage#eeca5ce3 flags:# official:flags.0?true rtl:flags.2?true beta:flags.3?true name:string native_name:string lang_code:string base_lang_code:flags.1?string plural_code:string strings_count:int translated_count:int translations_url:string = LangPackLanguage;\n\nchannelAdminLogEventActionChangeTitle#e6dfb825 prev_value:string new_value:string = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeAbout#55188a2e prev_value:string new_value:string = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeUsername#6a4afc38 prev_value:string new_value:string = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangePhoto#434bd2af prev_photo:Photo new_photo:Photo = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleInvites#1b7907ae new_value:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleSignatures#26ae0971 new_value:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionUpdatePinned#e9e82c18 message:Message = ChannelAdminLogEventAction;\nchannelAdminLogEventActionEditMessage#709b2405 prev_message:Message new_message:Message = ChannelAdminLogEventAction;\nchannelAdminLogEventActionDeleteMessage#42e047bb message:Message = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantJoin#183040d3 = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantLeave#f89777f2 = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantInvite#e31c34d8 participant:ChannelParticipant = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantToggleBan#e6d83d7e prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantToggleAdmin#d5676710 prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeStickerSet#b1c3caa7 prev_stickerset:InputStickerSet new_stickerset:InputStickerSet = ChannelAdminLogEventAction;\nchannelAdminLogEventActionTogglePreHistoryHidden#5f5c95f1 new_value:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionDefaultBannedRights#2df5fc0a prev_banned_rights:ChatBannedRights new_banned_rights:ChatBannedRights = ChannelAdminLogEventAction;\nchannelAdminLogEventActionStopPoll#8f079643 message:Message = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeLinkedChat#50c7ac8 prev_value:long new_value:long = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeLocation#e6b76ae prev_value:ChannelLocation new_value:ChannelLocation = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleSlowMode#53909779 prev_value:int new_value:int = ChannelAdminLogEventAction;\nchannelAdminLogEventActionStartGroupCall#23209745 call:InputGroupCall = ChannelAdminLogEventAction;\nchannelAdminLogEventActionDiscardGroupCall#db9f9140 call:InputGroupCall = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantMute#f92424d2 participant:GroupCallParticipant = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantUnmute#e64429c0 participant:GroupCallParticipant = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleGroupCallSetting#56d6a247 join_muted:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantJoinByInvite#fe9fc158 flags:# via_chatlist:flags.0?true invite:ExportedChatInvite = ChannelAdminLogEventAction;\nchannelAdminLogEventActionExportedInviteDelete#5a50fca4 invite:ExportedChatInvite = ChannelAdminLogEventAction;\nchannelAdminLogEventActionExportedInviteRevoke#410a134e invite:ExportedChatInvite = ChannelAdminLogEventAction;\nchannelAdminLogEventActionExportedInviteEdit#e90ebb59 prev_invite:ExportedChatInvite new_invite:ExportedChatInvite = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantVolume#3e7f6847 participant:GroupCallParticipant = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeHistoryTTL#6e941a38 prev_value:int new_value:int = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantJoinByRequest#afb6144a invite:ExportedChatInvite approved_by:long = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleNoForwards#cb2ac766 new_value:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionSendMessage#278f2868 message:Message = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeAvailableReactions#be4e0ef8 prev_value:ChatReactions new_value:ChatReactions = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeUsernames#f04fb3a9 prev_value:Vector<string> new_value:Vector<string> = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleForum#2cc6383 new_value:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionCreateTopic#58707d28 topic:ForumTopic = ChannelAdminLogEventAction;\nchannelAdminLogEventActionEditTopic#f06fe208 prev_topic:ForumTopic new_topic:ForumTopic = ChannelAdminLogEventAction;\nchannelAdminLogEventActionDeleteTopic#ae168909 topic:ForumTopic = ChannelAdminLogEventAction;\nchannelAdminLogEventActionPinTopic#5d8d353b flags:# prev_topic:flags.0?ForumTopic new_topic:flags.1?ForumTopic = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleAntiSpam#64f36dfc new_value:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangePeerColor#5796e780 prev_value:PeerColor new_value:PeerColor = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeProfilePeerColor#5e477b25 prev_value:PeerColor new_value:PeerColor = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeWallpaper#31bb5d52 prev_value:WallPaper new_value:WallPaper = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeEmojiStatus#3ea9feb1 prev_value:EmojiStatus new_value:EmojiStatus = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeEmojiStickerSet#46d840ab prev_stickerset:InputStickerSet new_stickerset:InputStickerSet = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleSignatureProfiles#60a79c79 new_value:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantSubExtend#64642db3 prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleAutotranslation#c517f77e new_value:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantEditRank#5806b4ec user_id:long prev_rank:string new_rank:string = ChannelAdminLogEventAction;\n\nchannelAdminLogEvent#1fad68cd id:long date:int user_id:long action:ChannelAdminLogEventAction = ChannelAdminLogEvent;\n\nchannels.adminLogResults#ed8af74d events:Vector<ChannelAdminLogEvent> chats:Vector<Chat> users:Vector<User> = channels.AdminLogResults;\n\nchannelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?true invite:flags.2?true ban:flags.3?true unban:flags.4?true kick:flags.5?true unkick:flags.6?true promote:flags.7?true demote:flags.8?true info:flags.9?true settings:flags.10?true pinned:flags.11?true edit:flags.12?true delete:flags.13?true group_call:flags.14?true invites:flags.15?true send:flags.16?true forums:flags.17?true sub_extend:flags.18?true edit_rank:flags.19?true = ChannelAdminLogEventsFilter;\n\npopularContact#5ce14175 client_id:long importers:int = PopularContact;\n\nmessages.favedStickersNotModified#9e8fa6d3 = messages.FavedStickers;\nmessages.favedStickers#2cb51097 hash:long packs:Vector<StickerPack> stickers:Vector<Document> = messages.FavedStickers;\n\nrecentMeUrlUnknown#46e1d13d url:string = RecentMeUrl;\nrecentMeUrlUser#b92c09e2 url:string user_id:long = RecentMeUrl;\nrecentMeUrlChat#b2da71d2 url:string chat_id:long = RecentMeUrl;\nrecentMeUrlChatInvite#eb49081d url:string chat_invite:ChatInvite = RecentMeUrl;\nrecentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl;\n\nhelp.recentMeUrls#e0310d7 urls:Vector<RecentMeUrl> chats:Vector<Chat> users:Vector<User> = help.RecentMeUrls;\n\ninputSingleMedia#1cc6e91f flags:# media:InputMedia random_id:long message:string entities:flags.0?Vector<MessageEntity> = InputSingleMedia;\n\nwebAuthorization#a6f8f452 hash:long bot_id:long domain:string browser:string platform:string date_created:int date_active:int ip:string region:string = WebAuthorization;\n\naccount.webAuthorizations#ed56c9fc authorizations:Vector<WebAuthorization> users:Vector<User> = account.WebAuthorizations;\n\ninputMessageID#a676a322 id:int = InputMessage;\ninputMessageReplyTo#bad88395 id:int = InputMessage;\ninputMessagePinned#86872538 = InputMessage;\ninputMessageCallbackQuery#acfa1a7e id:int query_id:long = InputMessage;\n\ninputDialogPeer#fcaafeb7 peer:InputPeer = InputDialogPeer;\ninputDialogPeerFolder#64600527 folder_id:int = InputDialogPeer;\n\ndialogPeer#e56dbf05 peer:Peer = DialogPeer;\ndialogPeerFolder#514519e2 folder_id:int = DialogPeer;\n\nmessages.foundStickerSetsNotModified#d54b65d = messages.FoundStickerSets;\nmessages.foundStickerSets#8af09dd2 hash:long sets:Vector<StickerSetCovered> = messages.FoundStickerSets;\n\nfileHash#f39b035c offset:long limit:int hash:bytes = FileHash;\n\ninputClientProxy#75588b3f address:string port:int = InputClientProxy;\n\nhelp.termsOfServiceUpdateEmpty#e3309f7f expires:int = help.TermsOfServiceUpdate;\nhelp.termsOfServiceUpdate#28ecf961 expires:int terms_of_service:help.TermsOfService = help.TermsOfServiceUpdate;\n\ninputSecureFileUploaded#3334b0f0 id:long parts:int md5_checksum:string file_hash:bytes secret:bytes = InputSecureFile;\ninputSecureFile#5367e5be id:long access_hash:long = InputSecureFile;\n\nsecureFileEmpty#64199744 = SecureFile;\nsecureFile#7d09c27e id:long access_hash:long size:long dc_id:int date:int file_hash:bytes secret:bytes = SecureFile;\n\nsecureData#8aeabec3 data:bytes data_hash:bytes secret:bytes = SecureData;\n\nsecurePlainPhone#7d6099dd phone:string = SecurePlainData;\nsecurePlainEmail#21ec5a5f email:string = SecurePlainData;\n\nsecureValueTypePersonalDetails#9d2a81e3 = SecureValueType;\nsecureValueTypePassport#3dac6a00 = SecureValueType;\nsecureValueTypeDriverLicense#6e425c4 = SecureValueType;\nsecureValueTypeIdentityCard#a0d0744b = SecureValueType;\nsecureValueTypeInternalPassport#99a48f23 = SecureValueType;\nsecureValueTypeAddress#cbe31e26 = SecureValueType;\nsecureValueTypeUtilityBill#fc36954e = SecureValueType;\nsecureValueTypeBankStatement#89137c0d = SecureValueType;\nsecureValueTypeRentalAgreement#8b883488 = SecureValueType;\nsecureValueTypePassportRegistration#99e3806a = SecureValueType;\nsecureValueTypeTemporaryRegistration#ea02ec33 = SecureValueType;\nsecureValueTypePhone#b320aadb = SecureValueType;\nsecureValueTypeEmail#8e3ca7ee = SecureValueType;\n\nsecureValue#187fa0ca flags:# type:SecureValueType data:flags.0?SecureData front_side:flags.1?SecureFile reverse_side:flags.2?SecureFile selfie:flags.3?SecureFile translation:flags.6?Vector<SecureFile> files:flags.4?Vector<SecureFile> plain_data:flags.5?SecurePlainData hash:bytes = SecureValue;\n\ninputSecureValue#db21d0a7 flags:# type:SecureValueType data:flags.0?SecureData front_side:flags.1?InputSecureFile reverse_side:flags.2?InputSecureFile selfie:flags.3?InputSecureFile translation:flags.6?Vector<InputSecureFile> files:flags.4?Vector<InputSecureFile> plain_data:flags.5?SecurePlainData = InputSecureValue;\n\nsecureValueHash#ed1ecdb0 type:SecureValueType hash:bytes = SecureValueHash;\n\nsecureValueErrorData#e8a40bd9 type:SecureValueType data_hash:bytes field:string text:string = SecureValueError;\nsecureValueErrorFrontSide#be3dfa type:SecureValueType file_hash:bytes text:string = SecureValueError;\nsecureValueErrorReverseSide#868a2aa5 type:SecureValueType file_hash:bytes text:string = SecureValueError;\nsecureValueErrorSelfie#e537ced6 type:SecureValueType file_hash:bytes text:string = SecureValueError;\nsecureValueErrorFile#7a700873 type:SecureValueType file_hash:bytes text:string = SecureValueError;\nsecureValueErrorFiles#666220e9 type:SecureValueType file_hash:Vector<bytes> text:string = SecureValueError;\nsecureValueError#869d758f type:SecureValueType hash:bytes text:string = SecureValueError;\nsecureValueErrorTranslationFile#a1144770 type:SecureValueType file_hash:bytes text:string = SecureValueError;\nsecureValueErrorTranslationFiles#34636dd8 type:SecureValueType file_hash:Vector<bytes> text:string = SecureValueError;\n\nsecureCredentialsEncrypted#33f0ea47 data:bytes hash:bytes secret:bytes = SecureCredentialsEncrypted;\n\naccount.authorizationForm#ad2e1cd8 flags:# required_types:Vector<SecureRequiredType> values:Vector<SecureValue> errors:Vector<SecureValueError> users:Vector<User> privacy_policy_url:flags.0?string = account.AuthorizationForm;\n\naccount.sentEmailCode#811f854f email_pattern:string length:int = account.SentEmailCode;\n\nhelp.deepLinkInfoEmpty#66afa166 = help.DeepLinkInfo;\nhelp.deepLinkInfo#6a4ee832 flags:# update_app:flags.0?true message:string entities:flags.1?Vector<MessageEntity> = help.DeepLinkInfo;\n\nsavedPhoneContact#1142bd56 phone:string first_name:string last_name:string date:int = SavedContact;\n\naccount.takeout#4dba4501 id:long = account.Takeout;\n\npasswordKdfAlgoUnknown#d45ab096 = PasswordKdfAlgo;\npasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow#3a912d4a salt1:bytes salt2:bytes g:int p:bytes = PasswordKdfAlgo;\n\nsecurePasswordKdfAlgoUnknown#4a8537 = SecurePasswordKdfAlgo;\nsecurePasswordKdfAlgoPBKDF2HMACSHA512iter100000#bbf2dda0 salt:bytes = SecurePasswordKdfAlgo;\nsecurePasswordKdfAlgoSHA512#86471d92 salt:bytes = SecurePasswordKdfAlgo;\n\nsecureSecretSettings#1527bcac secure_algo:SecurePasswordKdfAlgo secure_secret:bytes secure_secret_id:long = SecureSecretSettings;\n\ninputCheckPasswordEmpty#9880f658 = InputCheckPasswordSRP;\ninputCheckPasswordSRP#d27ff082 srp_id:long A:bytes M1:bytes = InputCheckPasswordSRP;\n\nsecureRequiredType#829d99da flags:# native_names:flags.0?true selfie_required:flags.1?true translation_required:flags.2?true type:SecureValueType = SecureRequiredType;\nsecureRequiredTypeOneOf#27477b4 types:Vector<SecureRequiredType> = SecureRequiredType;\n\nhelp.passportConfigNotModified#bfb9f457 = help.PassportConfig;\nhelp.passportConfig#a098d6af hash:int countries_langs:DataJSON = help.PassportConfig;\n\ninputAppEvent#1d1b1245 time:double type:string peer:long data:JSONValue = InputAppEvent;\n\njsonObjectValue#c0de1bd9 key:string value:JSONValue = JSONObjectValue;\n\njsonNull#3f6d7b68 = JSONValue;\njsonBool#c7345e6a value:Bool = JSONValue;\njsonNumber#2be0dfa4 value:double = JSONValue;\njsonString#b71e767a value:string = JSONValue;\njsonArray#f7444763 value:Vector<JSONValue> = JSONValue;\njsonObject#99c1d49d value:Vector<JSONObjectValue> = JSONValue;\n\npageTableCell#34566b6a flags:# header:flags.0?true align_center:flags.3?true align_right:flags.4?true valign_middle:flags.5?true valign_bottom:flags.6?true text:flags.7?RichText colspan:flags.1?int rowspan:flags.2?int = PageTableCell;\n\npageTableRow#e0c0c5e5 cells:Vector<PageTableCell> = PageTableRow;\n\npageCaption#6f747657 text:RichText credit:RichText = PageCaption;\n\npageListItemText#b92fb6cd text:RichText = PageListItem;\npageListItemBlocks#25e073fc blocks:Vector<PageBlock> = PageListItem;\n\npageListOrderedItemText#5e068047 num:string text:RichText = PageListOrderedItem;\npageListOrderedItemBlocks#98dd8936 num:string blocks:Vector<PageBlock> = PageListOrderedItem;\n\npageRelatedArticle#b390dc08 flags:# url:string webpage_id:long title:flags.0?string description:flags.1?string photo_id:flags.2?long author:flags.3?string published_date:flags.4?int = PageRelatedArticle;\n\npage#98657f0d flags:# part:flags.0?true rtl:flags.1?true v2:flags.2?true url:string blocks:Vector<PageBlock> photos:Vector<Photo> documents:Vector<Document> views:flags.3?int = Page;\n\nhelp.supportName#8c05f1c9 name:string = help.SupportName;\n\nhelp.userInfoEmpty#f3ae2eed = help.UserInfo;\nhelp.userInfo#1eb3758 message:string entities:Vector<MessageEntity> author:string date:int = help.UserInfo;\n\npollAnswer#4b7d786a flags:# text:TextWithEntities option:bytes media:flags.0?MessageMedia added_by:flags.1?Peer date:flags.1?int = PollAnswer;\ninputPollAnswer#199fed96 flags:# text:TextWithEntities media:flags.0?InputMedia = PollAnswer;\n\npoll#b8425be9 id:long flags:# closed:flags.0?true public_voters:flags.1?true multiple_choice:flags.2?true quiz:flags.3?true open_answers:flags.6?true revoting_disabled:flags.7?true shuffle_answers:flags.8?true hide_results_until_close:flags.9?true creator:flags.10?true question:TextWithEntities answers:Vector<PollAnswer> close_period:flags.4?int close_date:flags.5?int hash:long = Poll;\n\npollAnswerVoters#3645230a flags:# chosen:flags.0?true correct:flags.1?true option:bytes voters:flags.2?int recent_voters:flags.2?Vector<Peer> = PollAnswerVoters;\n\npollResults#ba7bb15e flags:# min:flags.0?true has_unread_votes:flags.6?true results:flags.1?Vector<PollAnswerVoters> total_voters:flags.2?int recent_voters:flags.3?Vector<Peer> solution:flags.4?string solution_entities:flags.4?Vector<MessageEntity> solution_media:flags.5?MessageMedia = PollResults;\n\nchatOnlines#f041e250 onlines:int = ChatOnlines;\n\nstatsURL#47a971e0 url:string = StatsURL;\n\nchatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true manage_direct_messages:flags.17?true manage_ranks:flags.18?true = ChatAdminRights;\n\nchatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true edit_rank:flags.26?true until_date:int = ChatBannedRights;\n\ninputWallPaper#e630b979 id:long access_hash:long = InputWallPaper;\ninputWallPaperSlug#72091c80 slug:string = InputWallPaper;\ninputWallPaperNoFile#967a462e id:long = InputWallPaper;\n\naccount.wallPapersNotModified#1c199183 = account.WallPapers;\naccount.wallPapers#cdc3858c hash:long wallpapers:Vector<WallPaper> = account.WallPapers;\n\ncodeSettings#ad253d78 flags:# allow_flashcall:flags.0?true current_number:flags.1?true allow_app_hash:flags.4?true allow_missed_call:flags.5?true allow_firebase:flags.7?true unknown_number:flags.9?true logout_tokens:flags.6?Vector<bytes> token:flags.8?string app_sandbox:flags.8?Bool = CodeSettings;\n\nwallPaperSettings#372efcd0 flags:# blur:flags.1?true motion:flags.2?true background_color:flags.0?int second_background_color:flags.4?int third_background_color:flags.5?int fourth_background_color:flags.6?int intensity:flags.3?int rotation:flags.4?int emoticon:flags.7?string = WallPaperSettings;\n\nautoDownloadSettings#baa57628 flags:# disabled:flags.0?true video_preload_large:flags.1?true audio_preload_next:flags.2?true phonecalls_less_data:flags.3?true stories_preload:flags.4?true photo_size_max:int video_size_max:long file_size_max:long video_upload_maxbitrate:int small_queue_active_operations_max:int large_queue_active_operations_max:int = AutoDownloadSettings;\n\naccount.autoDownloadSettings#63cacf26 low:AutoDownloadSettings medium:AutoDownloadSettings high:AutoDownloadSettings = account.AutoDownloadSettings;\n\nemojiKeyword#d5b3b9f9 keyword:string emoticons:Vector<string> = EmojiKeyword;\nemojiKeywordDeleted#236df622 keyword:string emoticons:Vector<string> = EmojiKeyword;\n\nemojiKeywordsDifference#5cc761bd lang_code:string from_version:int version:int keywords:Vector<EmojiKeyword> = EmojiKeywordsDifference;\n\nemojiURL#a575739d url:string = EmojiURL;\n\nemojiLanguage#b3fb5361 lang_code:string = EmojiLanguage;\n\nfolder#ff544e65 flags:# autofill_new_broadcasts:flags.0?true autofill_public_groups:flags.1?true autofill_new_correspondents:flags.2?true id:int title:string photo:flags.3?ChatPhoto = Folder;\n\ninputFolderPeer#fbd2c296 peer:InputPeer folder_id:int = InputFolderPeer;\n\nfolderPeer#e9baa668 peer:Peer folder_id:int = FolderPeer;\n\nmessages.searchCounter#e844ebff flags:# inexact:flags.1?true filter:MessagesFilter count:int = messages.SearchCounter;\n\nurlAuthResultRequest#3cd623ec flags:# request_write_access:flags.0?true request_phone_number:flags.1?true match_codes_first:flags.5?true is_app:flags.6?true bot:User domain:string browser:flags.2?string platform:flags.2?string ip:flags.2?string region:flags.2?string match_codes:flags.3?Vector<string> user_id_hint:flags.4?long verified_app_name:flags.7?string = UrlAuthResult;\nurlAuthResultAccepted#623a8fa0 flags:# url:flags.0?string = UrlAuthResult;\nurlAuthResultDefault#a9d6db1f = UrlAuthResult;\n\nchannelLocationEmpty#bfb5ad8b = ChannelLocation;\nchannelLocation#209b82db geo_point:GeoPoint address:string = ChannelLocation;\n\npeerLocated#ca461b5d peer:Peer expires:int distance:int = PeerLocated;\npeerSelfLocated#f8ec284b expires:int = PeerLocated;\n\nrestrictionReason#d072acb4 platform:string reason:string text:string = RestrictionReason;\n\ninputTheme#3c5693e9 id:long access_hash:long = InputTheme;\ninputThemeSlug#f5890df1 slug:string = InputTheme;\n\ntheme#a00e67d6 flags:# creator:flags.0?true default:flags.1?true for_chat:flags.5?true id:long access_hash:long slug:string title:string document:flags.2?Document settings:flags.3?Vector<ThemeSettings> emoticon:flags.6?string installs_count:flags.4?int = Theme;\n\naccount.themesNotModified#f41eb622 = account.Themes;\naccount.themes#9a3d8c6d hash:long themes:Vector<Theme> = account.Themes;\n\nauth.loginToken#629f1980 expires:int token:bytes = auth.LoginToken;\nauth.loginTokenMigrateTo#68e9916 dc_id:int token:bytes = auth.LoginToken;\nauth.loginTokenSuccess#390d5c5e authorization:auth.Authorization = auth.LoginToken;\n\naccount.contentSettings#57e28221 flags:# sensitive_enabled:flags.0?true sensitive_can_change:flags.1?true = account.ContentSettings;\n\nmessages.inactiveChats#a927fec5 dates:Vector<int> chats:Vector<Chat> users:Vector<User> = messages.InactiveChats;\n\nbaseThemeClassic#c3a12462 = BaseTheme;\nbaseThemeDay#fbd81688 = BaseTheme;\nbaseThemeNight#b7b31ea8 = BaseTheme;\nbaseThemeTinted#6d5f77ee = BaseTheme;\nbaseThemeArctic#5b11125a = BaseTheme;\n\ninputThemeSettings#8fde504f flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int outbox_accent_color:flags.3?int message_colors:flags.0?Vector<int> wallpaper:flags.1?InputWallPaper wallpaper_settings:flags.1?WallPaperSettings = InputThemeSettings;\n\nthemeSettings#fa58b6d4 flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int outbox_accent_color:flags.3?int message_colors:flags.0?Vector<int> wallpaper:flags.1?WallPaper = ThemeSettings;\n\nwebPageAttributeTheme#54b56617 flags:# documents:flags.0?Vector<Document> settings:flags.1?ThemeSettings = WebPageAttribute;\nwebPageAttributeStory#2e94c3e7 flags:# peer:Peer id:int story:flags.0?StoryItem = WebPageAttribute;\nwebPageAttributeStickerSet#50cc03d3 flags:# emojis:flags.0?true text_color:flags.1?true stickers:Vector<Document> = WebPageAttribute;\nwebPageAttributeUniqueStarGift#cf6f6db8 gift:StarGift = WebPageAttribute;\nwebPageAttributeStarGiftCollection#31cad303 icons:Vector<Document> = WebPageAttribute;\nwebPageAttributeStarGiftAuction#1c641c2 gift:StarGift end_date:int = WebPageAttribute;\n\nmessages.votesList#4899484e flags:# count:int votes:Vector<MessagePeerVote> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = messages.VotesList;\n\nbankCardOpenUrl#f568028a url:string name:string = BankCardOpenUrl;\n\npayments.bankCardData#3e24e573 title:string open_urls:Vector<BankCardOpenUrl> = payments.BankCardData;\n\ndialogFilter#aa472651 flags:# contacts:flags.0?true non_contacts:flags.1?true groups:flags.2?true broadcasts:flags.3?true bots:flags.4?true exclude_muted:flags.11?true exclude_read:flags.12?true exclude_archived:flags.13?true title_noanimate:flags.28?true id:int title:TextWithEntities emoticon:flags.25?string color:flags.27?int pinned_peers:Vector<InputPeer> include_peers:Vector<InputPeer> exclude_peers:Vector<InputPeer> = DialogFilter;\ndialogFilterDefault#363293ae = DialogFilter;\ndialogFilterChatlist#96537bd7 flags:# has_my_invites:flags.26?true title_noanimate:flags.28?true id:int title:TextWithEntities emoticon:flags.25?string color:flags.27?int pinned_peers:Vector<InputPeer> include_peers:Vector<InputPeer> = DialogFilter;\n\ndialogFilterSuggested#77744d4a filter:DialogFilter description:string = DialogFilterSuggested;\n\nstatsDateRangeDays#b637edaf min_date:int max_date:int = StatsDateRangeDays;\n\nstatsAbsValueAndPrev#cb43acde current:double previous:double = StatsAbsValueAndPrev;\n\nstatsPercentValue#cbce2fe0 part:double total:double = StatsPercentValue;\n\nstatsGraphAsync#4a27eb2d token:string = StatsGraph;\nstatsGraphError#bedc9822 error:string = StatsGraph;\nstatsGraph#8ea464b6 flags:# json:DataJSON zoom_token:flags.0?string = StatsGraph;\n\nstats.broadcastStats#396ca5fc period:StatsDateRangeDays followers:StatsAbsValueAndPrev views_per_post:StatsAbsValueAndPrev shares_per_post:StatsAbsValueAndPrev reactions_per_post:StatsAbsValueAndPrev views_per_story:StatsAbsValueAndPrev shares_per_story:StatsAbsValueAndPrev reactions_per_story:StatsAbsValueAndPrev enabled_notifications:StatsPercentValue growth_graph:StatsGraph followers_graph:StatsGraph mute_graph:StatsGraph top_hours_graph:StatsGraph interactions_graph:StatsGraph iv_interactions_graph:StatsGraph views_by_source_graph:StatsGraph new_followers_by_source_graph:StatsGraph languages_graph:StatsGraph reactions_by_emotion_graph:StatsGraph story_interactions_graph:StatsGraph story_reactions_by_emotion_graph:StatsGraph recent_posts_interactions:Vector<PostInteractionCounters> = stats.BroadcastStats;\n\nhelp.promoDataEmpty#98f6ac75 expires:int = help.PromoData;\nhelp.promoData#8a4d87a flags:# proxy:flags.0?true expires:int peer:flags.3?Peer psa_type:flags.1?string psa_message:flags.2?string pending_suggestions:Vector<string> dismissed_suggestions:Vector<string> custom_pending_suggestion:flags.4?PendingSuggestion chats:Vector<Chat> users:Vector<User> = help.PromoData;\n\nvideoSize#de33b094 flags:# type:string w:int h:int size:int video_start_ts:flags.0?double = VideoSize;\nvideoSizeEmojiMarkup#f85c413c emoji_id:long background_colors:Vector<int> = VideoSize;\nvideoSizeStickerMarkup#da082fe stickerset:InputStickerSet sticker_id:long background_colors:Vector<int> = VideoSize;\n\nstatsGroupTopPoster#9d04af9b user_id:long messages:int avg_chars:int = StatsGroupTopPoster;\n\nstatsGroupTopAdmin#d7584c87 user_id:long deleted:int kicked:int banned:int = StatsGroupTopAdmin;\n\nstatsGroupTopInviter#535f779d user_id:long invitations:int = StatsGroupTopInviter;\n\nstats.megagroupStats#ef7ff916 period:StatsDateRangeDays members:StatsAbsValueAndPrev messages:StatsAbsValueAndPrev viewers:StatsAbsValueAndPrev posters:StatsAbsValueAndPrev growth_graph:StatsGraph members_graph:StatsGraph new_members_by_source_graph:StatsGraph languages_graph:StatsGraph messages_graph:StatsGraph actions_graph:StatsGraph top_hours_graph:StatsGraph weekdays_graph:StatsGraph top_posters:Vector<StatsGroupTopPoster> top_admins:Vector<StatsGroupTopAdmin> top_inviters:Vector<StatsGroupTopInviter> users:Vector<User> = stats.MegagroupStats;\n\nglobalPrivacySettings#fe41b34f flags:# archive_and_mute_new_noncontact_peers:flags.0?true keep_archived_unmuted:flags.1?true keep_archived_folders:flags.2?true hide_read_marks:flags.3?true new_noncontact_peers_require_premium:flags.4?true display_gifts_button:flags.7?true noncontact_peers_paid_stars:flags.5?long disallowed_gifts:flags.6?DisallowedGiftsSettings = GlobalPrivacySettings;\n\nhelp.countryCode#4203c5ef flags:# country_code:string prefixes:flags.0?Vector<string> patterns:flags.1?Vector<string> = help.CountryCode;\n\nhelp.country#c3878e23 flags:# hidden:flags.0?true iso2:string default_name:string name:flags.1?string country_codes:Vector<help.CountryCode> = help.Country;\n\nhelp.countriesListNotModified#93cc1f32 = help.CountriesList;\nhelp.countriesList#87d0759e countries:Vector<help.Country> hash:int = help.CountriesList;\n\nmessageViews#455b853d flags:# views:flags.0?int forwards:flags.1?int replies:flags.2?MessageReplies = MessageViews;\n\nmessages.messageViews#b6c4f543 views:Vector<MessageViews> chats:Vector<Chat> users:Vector<User> = messages.MessageViews;\n\nmessages.discussionMessage#a6341782 flags:# messages:Vector<Message> max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector<Chat> users:Vector<User> = messages.DiscussionMessage;\n\nmessageReplyHeader#1b97dd66 flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true quote:flags.9?true reply_to_msg_id:flags.4?int reply_to_peer_id:flags.0?Peer reply_from:flags.5?MessageFwdHeader reply_media:flags.8?MessageMedia reply_to_top_id:flags.1?int quote_text:flags.6?string quote_entities:flags.7?Vector<MessageEntity> quote_offset:flags.10?int todo_item_id:flags.11?int poll_option:flags.12?bytes = MessageReplyHeader;\nmessageReplyStoryHeader#e5af939 peer:Peer story_id:int = MessageReplyHeader;\n\nmessageReplies#83d60fc2 flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector<Peer> channel_id:flags.0?long max_id:flags.2?int read_max_id:flags.3?int = MessageReplies;\n\npeerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked;\n\nstats.messageStats#7fe91c14 views_graph:StatsGraph reactions_by_emotion_graph:StatsGraph = stats.MessageStats;\n\ngroupCallDiscarded#7780bcb4 id:long access_hash:long duration:int = GroupCall;\ngroupCall#efb2b617 flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true can_start_video:flags.9?true record_video_active:flags.11?true rtmp_stream:flags.12?true listeners_hidden:flags.13?true conference:flags.14?true creator:flags.15?true messages_enabled:flags.17?true can_change_messages_enabled:flags.18?true min:flags.19?true id:long access_hash:long participants_count:int title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int unmuted_video_count:flags.10?int unmuted_video_limit:int version:int invite_link:flags.16?string send_paid_messages_stars:flags.20?long default_send_as:flags.21?Peer = GroupCall;\n\ninputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall;\ninputGroupCallSlug#fe06823f slug:string = InputGroupCall;\ninputGroupCallInviteMessage#8c10603f msg_id:int = InputGroupCall;\n\ngroupCallParticipant#2a3dc7ac flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true min:flags.8?true muted_by_you:flags.9?true volume_by_admin:flags.10?true self:flags.12?true video_joined:flags.15?true peer:Peer date:int active_date:flags.3?int source:int volume:flags.7?int about:flags.11?string raise_hand_rating:flags.13?long video:flags.6?GroupCallParticipantVideo presentation:flags.14?GroupCallParticipantVideo paid_stars_total:flags.16?long = GroupCallParticipant;\n\nphone.groupCall#9e727aad call:GroupCall participants:Vector<GroupCallParticipant> participants_next_offset:string chats:Vector<Chat> users:Vector<User> = phone.GroupCall;\n\nphone.groupParticipants#f47751b6 count:int participants:Vector<GroupCallParticipant> next_offset:string chats:Vector<Chat> users:Vector<User> version:int = phone.GroupParticipants;\n\ninlineQueryPeerTypeSameBotPM#3081ed9d = InlineQueryPeerType;\ninlineQueryPeerTypePM#833c0fac = InlineQueryPeerType;\ninlineQueryPeerTypeChat#d766c50a = InlineQueryPeerType;\ninlineQueryPeerTypeMegagroup#5ec4be43 = InlineQueryPeerType;\ninlineQueryPeerTypeBroadcast#6334ee9a = InlineQueryPeerType;\ninlineQueryPeerTypeBotPM#e3b2d0c = InlineQueryPeerType;\n\nmessages.historyImport#1662af0b id:long = messages.HistoryImport;\n\nmessages.historyImportParsed#5e0fb7b9 flags:# pm:flags.0?true group:flags.1?true title:flags.2?string = messages.HistoryImportParsed;\n\nmessages.affectedFoundMessages#ef8d3e6c pts:int pts_count:int offset:int messages:Vector<int> = messages.AffectedFoundMessages;\n\nchatInviteImporter#8c5adfd9 flags:# requested:flags.0?true via_chatlist:flags.3?true user_id:long date:int about:flags.2?string approved_by:flags.1?long = ChatInviteImporter;\n\nmessages.exportedChatInvites#bdc62dcc count:int invites:Vector<ExportedChatInvite> users:Vector<User> = messages.ExportedChatInvites;\n\nmessages.exportedChatInvite#1871be50 invite:ExportedChatInvite users:Vector<User> = messages.ExportedChatInvite;\nmessages.exportedChatInviteReplaced#222600ef invite:ExportedChatInvite new_invite:ExportedChatInvite users:Vector<User> = messages.ExportedChatInvite;\n\nmessages.chatInviteImporters#81b6b00a count:int importers:Vector<ChatInviteImporter> users:Vector<User> = messages.ChatInviteImporters;\n\nchatAdminWithInvites#f2ecef23 admin_id:long invites_count:int revoked_invites_count:int = ChatAdminWithInvites;\n\nmessages.chatAdminsWithInvites#b69b72d7 admins:Vector<ChatAdminWithInvites> users:Vector<User> = messages.ChatAdminsWithInvites;\n\nmessages.checkedHistoryImportPeer#a24de717 confirm_text:string = messages.CheckedHistoryImportPeer;\n\nphone.joinAsPeers#afe5623f peers:Vector<Peer> chats:Vector<Chat> users:Vector<User> = phone.JoinAsPeers;\n\nphone.exportedGroupCallInvite#204bd158 link:string = phone.ExportedGroupCallInvite;\n\ngroupCallParticipantVideoSourceGroup#dcb118b7 semantics:string sources:Vector<int> = GroupCallParticipantVideoSourceGroup;\n\ngroupCallParticipantVideo#67753ac8 flags:# paused:flags.0?true endpoint:string source_groups:Vector<GroupCallParticipantVideoSourceGroup> audio_source:flags.1?int = GroupCallParticipantVideo;\n\nstickers.suggestedShortName#85fea03f short_name:string = stickers.SuggestedShortName;\n\nbotCommandScopeDefault#2f6cb2ab = BotCommandScope;\nbotCommandScopeUsers#3c4f04d8 = BotCommandScope;\nbotCommandScopeChats#6fe1a881 = BotCommandScope;\nbotCommandScopeChatAdmins#b9aa606a = BotCommandScope;\nbotCommandScopePeer#db9d897d peer:InputPeer = BotCommandScope;\nbotCommandScopePeerAdmins#3fd863d1 peer:InputPeer = BotCommandScope;\nbotCommandScopePeerUser#a1321f3 peer:InputPeer user_id:InputUser = BotCommandScope;\n\naccount.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordResult;\naccount.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult;\naccount.resetPasswordOk#e926d63e = account.ResetPasswordResult;\n\nchatTheme#c3dffc04 emoticon:string = ChatTheme;\nchatThemeUniqueGift#3458f9c8 gift:StarGift theme_settings:Vector<ThemeSettings> = ChatTheme;\n\naccount.chatThemesNotModified#e011e1c4 = account.ChatThemes;\naccount.chatThemes#be098173 flags:# hash:long themes:Vector<ChatTheme> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = account.ChatThemes;\n\nsponsoredMessage#7dbf8673 flags:# recommended:flags.5?true can_report:flags.12?true random_id:bytes url:string title:string message:string entities:flags.1?Vector<MessageEntity> photo:flags.6?Photo media:flags.14?MessageMedia color:flags.13?PeerColor button_text:string sponsor_info:flags.7?string additional_info:flags.8?string min_display_duration:flags.15?int max_display_duration:flags.15?int = SponsoredMessage;\n\nmessages.sponsoredMessages#ffda656d flags:# posts_between:flags.0?int start_delay:flags.1?int between_delay:flags.2?int messages:Vector<SponsoredMessage> chats:Vector<Chat> users:Vector<User> = messages.SponsoredMessages;\nmessages.sponsoredMessagesEmpty#1839490f = messages.SponsoredMessages;\n\nsearchResultsCalendarPeriod#c9b0539f date:int min_msg_id:int max_msg_id:int count:int = SearchResultsCalendarPeriod;\n\nmessages.searchResultsCalendar#147ee23c flags:# inexact:flags.0?true count:int min_date:int min_msg_id:int offset_id_offset:flags.1?int periods:Vector<SearchResultsCalendarPeriod> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SearchResultsCalendar;\n\nsearchResultPosition#7f648b67 msg_id:int date:int offset:int = SearchResultsPosition;\n\nmessages.searchResultsPositions#53b22baf count:int positions:Vector<SearchResultsPosition> = messages.SearchResultsPositions;\n\nchannels.sendAsPeers#f496b0c6 peers:Vector<SendAsPeer> chats:Vector<Chat> users:Vector<User> = channels.SendAsPeers;\n\nusers.userFull#3b6d152e full_user:UserFull chats:Vector<Chat> users:Vector<User> = users.UserFull;\n\nmessages.peerSettings#6880b94d settings:PeerSettings chats:Vector<Chat> users:Vector<User> = messages.PeerSettings;\n\nauth.loggedOut#c3a2835f flags:# future_auth_token:flags.0?bytes = auth.LoggedOut;\n\nreactionCount#a3d1cb80 flags:# chosen_order:flags.0?int reaction:Reaction count:int = ReactionCount;\n\nmessageReactions#a339f0b flags:# min:flags.0?true can_see_list:flags.2?true reactions_as_tags:flags.3?true results:Vector<ReactionCount> recent_reactions:flags.1?Vector<MessagePeerReaction> top_reactors:flags.4?Vector<MessageReactor> = MessageReactions;\n\nmessages.messageReactionsList#31bd492d flags:# count:int reactions:Vector<MessagePeerReaction> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = messages.MessageReactionsList;\n\navailableReaction#c077ec01 flags:# inactive:flags.0?true premium:flags.2?true reaction:string title:string static_icon:Document appear_animation:Document select_animation:Document activate_animation:Document effect_animation:Document around_animation:flags.1?Document center_icon:flags.1?Document = AvailableReaction;\n\nmessages.availableReactionsNotModified#9f071957 = messages.AvailableReactions;\nmessages.availableReactions#768e3aad hash:int reactions:Vector<AvailableReaction> = messages.AvailableReactions;\n\nmessagePeerReaction#8c79b63c flags:# big:flags.0?true unread:flags.1?true my:flags.2?true peer_id:Peer date:int reaction:Reaction = MessagePeerReaction;\n\ngroupCallStreamChannel#80eb48af channel:int scale:int last_timestamp_ms:long = GroupCallStreamChannel;\n\nphone.groupCallStreamChannels#d0e482b2 channels:Vector<GroupCallStreamChannel> = phone.GroupCallStreamChannels;\n\nphone.groupCallStreamRtmpUrl#2dbf3432 url:string key:string = phone.GroupCallStreamRtmpUrl;\n\nattachMenuBotIconColor#4576f3f0 name:string color:int = AttachMenuBotIconColor;\n\nattachMenuBotIcon#b2a7386b flags:# name:string icon:Document colors:flags.0?Vector<AttachMenuBotIconColor> = AttachMenuBotIcon;\n\nattachMenuBot#d90d8dfe flags:# inactive:flags.0?true has_settings:flags.1?true request_write_access:flags.2?true show_in_attach_menu:flags.3?true show_in_side_menu:flags.4?true side_menu_disclaimer_needed:flags.5?true bot_id:long short_name:string peer_types:flags.3?Vector<AttachMenuPeerType> icons:Vector<AttachMenuBotIcon> = AttachMenuBot;\n\nattachMenuBotsNotModified#f1d88a5c = AttachMenuBots;\nattachMenuBots#3c4301c0 hash:long bots:Vector<AttachMenuBot> users:Vector<User> = AttachMenuBots;\n\nattachMenuBotsBot#93bf667f bot:AttachMenuBot users:Vector<User> = AttachMenuBotsBot;\n\nwebViewResultUrl#4d22ff98 flags:# fullsize:flags.1?true fullscreen:flags.2?true query_id:flags.0?long url:string = WebViewResult;\n\nwebViewMessageSent#c94511c flags:# msg_id:flags.0?InputBotInlineMessageID = WebViewMessageSent;\n\nbotMenuButtonDefault#7533a588 = BotMenuButton;\nbotMenuButtonCommands#4258c205 = BotMenuButton;\nbotMenuButton#c7b57ce6 text:string url:string = BotMenuButton;\n\naccount.savedRingtonesNotModified#fbf6e8b1 = account.SavedRingtones;\naccount.savedRingtones#c1e92cc5 hash:long ringtones:Vector<Document> = account.SavedRingtones;\n\nnotificationSoundDefault#97e8bebe = NotificationSound;\nnotificationSoundNone#6f0c34df = NotificationSound;\nnotificationSoundLocal#830b9ae4 title:string data:string = NotificationSound;\nnotificationSoundRingtone#ff6c8049 id:long = NotificationSound;\n\naccount.savedRingtone#b7263f6d = account.SavedRingtone;\naccount.savedRingtoneConverted#1f307eb7 document:Document = account.SavedRingtone;\n\nattachMenuPeerTypeSameBotPM#7d6be90e = AttachMenuPeerType;\nattachMenuPeerTypeBotPM#c32bfa1a = AttachMenuPeerType;\nattachMenuPeerTypePM#f146d31f = AttachMenuPeerType;\nattachMenuPeerTypeChat#509113f = AttachMenuPeerType;\nattachMenuPeerTypeBroadcast#7bfbdefc = AttachMenuPeerType;\n\ninputInvoiceMessage#c5b56859 peer:InputPeer msg_id:int = InputInvoice;\ninputInvoiceSlug#c326caef slug:string = InputInvoice;\ninputInvoicePremiumGiftCode#98986c0d purpose:InputStorePaymentPurpose option:PremiumGiftCodeOption = InputInvoice;\ninputInvoiceStars#65f00ce3 purpose:InputStorePaymentPurpose = InputInvoice;\ninputInvoiceChatInviteSubscription#34e793f1 hash:string = InputInvoice;\ninputInvoiceStarGift#e8625e92 flags:# hide_name:flags.0?true include_upgrade:flags.2?true peer:InputPeer gift_id:long message:flags.1?TextWithEntities = InputInvoice;\ninputInvoiceStarGiftUpgrade#4d818d5d flags:# keep_original_details:flags.0?true stargift:InputSavedStarGift = InputInvoice;\ninputInvoiceStarGiftTransfer#4a5f5bd9 stargift:InputSavedStarGift to_id:InputPeer = InputInvoice;\ninputInvoicePremiumGiftStars#dabab2ef flags:# user_id:InputUser months:int message:flags.0?TextWithEntities = InputInvoice;\ninputInvoiceBusinessBotTransferStars#f4997e42 bot:InputUser stars:long = InputInvoice;\ninputInvoiceStarGiftResale#c39f5324 flags:# ton:flags.0?true slug:string to_id:InputPeer = InputInvoice;\ninputInvoiceStarGiftPrepaidUpgrade#9a0b48b8 peer:InputPeer hash:string = InputInvoice;\ninputInvoicePremiumAuthCode#3e77f614 purpose:InputStorePaymentPurpose = InputInvoice;\ninputInvoiceStarGiftDropOriginalDetails#923d8d1 stargift:InputSavedStarGift = InputInvoice;\ninputInvoiceStarGiftAuctionBid#1ecafa10 flags:# hide_name:flags.0?true update_bid:flags.2?true peer:flags.3?InputPeer gift_id:long bid_amount:long message:flags.1?TextWithEntities = InputInvoice;\n\npayments.exportedInvoice#aed0cbd9 url:string = payments.ExportedInvoice;\n\nmessages.transcribedAudio#cfb9d957 flags:# pending:flags.0?true transcription_id:long text:string trial_remains_num:flags.1?int trial_remains_until_date:flags.1?int = messages.TranscribedAudio;\n\nhelp.premiumPromo#5334759c status_text:string status_entities:Vector<MessageEntity> video_sections:Vector<string> videos:Vector<Document> period_options:Vector<PremiumSubscriptionOption> users:Vector<User> = help.PremiumPromo;\n\ninputStorePaymentPremiumSubscription#a6751e66 flags:# restore:flags.0?true upgrade:flags.1?true = InputStorePaymentPurpose;\ninputStorePaymentGiftPremium#616f7fe8 user_id:InputUser currency:string amount:long = InputStorePaymentPurpose;\ninputStorePaymentPremiumGiftCode#fb790393 flags:# users:Vector<InputUser> boost_peer:flags.0?InputPeer currency:string amount:long message:flags.1?TextWithEntities = InputStorePaymentPurpose;\ninputStorePaymentPremiumGiveaway#160544ca flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true boost_peer:InputPeer additional_peers:flags.1?Vector<InputPeer> countries_iso2:flags.2?Vector<string> prize_description:flags.4?string random_id:long until_date:int currency:string amount:long = InputStorePaymentPurpose;\ninputStorePaymentStarsTopup#f9a2a6cb flags:# stars:long currency:string amount:long spend_purpose_peer:flags.0?InputPeer = InputStorePaymentPurpose;\ninputStorePaymentStarsGift#1d741ef7 user_id:InputUser stars:long currency:string amount:long = InputStorePaymentPurpose;\ninputStorePaymentStarsGiveaway#751f08fa flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true stars:long boost_peer:InputPeer additional_peers:flags.1?Vector<InputPeer> countries_iso2:flags.2?Vector<string> prize_description:flags.4?string random_id:long until_date:int currency:string amount:long users:int = InputStorePaymentPurpose;\ninputStorePaymentAuthCode#9bb2636d flags:# restore:flags.0?true phone_number:string phone_code_hash:string currency:string amount:long = InputStorePaymentPurpose;\n\npaymentFormMethod#88f8f21b url:string title:string = PaymentFormMethod;\n\nemojiStatusEmpty#2de11aae = EmojiStatus;\nemojiStatus#e7ff068a flags:# document_id:long until:flags.0?int = EmojiStatus;\nemojiStatusCollectible#7184603b flags:# collectible_id:long document_id:long title:string slug:string pattern_document_id:long center_color:int edge_color:int pattern_color:int text_color:int until:flags.0?int = EmojiStatus;\ninputEmojiStatusCollectible#7141dbf flags:# collectible_id:long until:flags.0?int = EmojiStatus;\n\naccount.emojiStatusesNotModified#d08ce645 = account.EmojiStatuses;\naccount.emojiStatuses#90c467d1 hash:long statuses:Vector<EmojiStatus> = account.EmojiStatuses;\n\nreactionEmpty#79f5d419 = Reaction;\nreactionEmoji#1b2286b8 emoticon:string = Reaction;\nreactionCustomEmoji#8935fc73 document_id:long = Reaction;\nreactionPaid#523da4eb = Reaction;\n\nchatReactionsNone#eafc32bc = ChatReactions;\nchatReactionsAll#52928bca flags:# allow_custom:flags.0?true = ChatReactions;\nchatReactionsSome#661d4037 reactions:Vector<Reaction> = ChatReactions;\n\nmessages.reactionsNotModified#b06fdbdf = messages.Reactions;\nmessages.reactions#eafdf716 hash:long reactions:Vector<Reaction> = messages.Reactions;\n\nemailVerifyPurposeLoginSetup#4345be73 phone_number:string phone_code_hash:string = EmailVerifyPurpose;\nemailVerifyPurposeLoginChange#527d22eb = EmailVerifyPurpose;\nemailVerifyPurposePassport#bbf51685 = EmailVerifyPurpose;\n\nemailVerificationCode#922e55a9 code:string = EmailVerification;\nemailVerificationGoogle#db909ec2 token:string = EmailVerification;\nemailVerificationApple#96d074fd token:string = EmailVerification;\n\naccount.emailVerified#2b96cd1b email:string = account.EmailVerified;\naccount.emailVerifiedLogin#e1bb0d61 email:string sent_code:auth.SentCode = account.EmailVerified;\n\npremiumSubscriptionOption#5f2d1df2 flags:# current:flags.1?true can_purchase_upgrade:flags.2?true transaction:flags.3?string months:int currency:string amount:long bot_url:string store_product:flags.0?string = PremiumSubscriptionOption;\n\nsendAsPeer#b81c7034 flags:# premium_required:flags.0?true peer:Peer = SendAsPeer;\n\nmessageExtendedMediaPreview#ad628cc8 flags:# w:flags.0?int h:flags.0?int thumb:flags.1?PhotoSize video_duration:flags.2?int = MessageExtendedMedia;\nmessageExtendedMedia#ee479c64 media:MessageMedia = MessageExtendedMedia;\n\nstickerKeyword#fcfeb29c document_id:long keyword:Vector<string> = StickerKeyword;\n\nusername#b4073647 flags:# editable:flags.0?true active:flags.1?true username:string = Username;\n\nforumTopicDeleted#23f109b id:int = ForumTopic;\nforumTopic#fcdad815 flags:# my:flags.1?true closed:flags.2?true pinned:flags.3?true short:flags.5?true hidden:flags.6?true title_missing:flags.7?true id:int date:int peer:Peer title:string icon_color:int icon_emoji_id:flags.0?long top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int unread_poll_votes_count:int from_id:Peer notify_settings:PeerNotifySettings draft:flags.4?DraftMessage = ForumTopic;\n\nmessages.forumTopics#367617d3 flags:# order_by_create_date:flags.0?true count:int topics:Vector<ForumTopic> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> pts:int = messages.ForumTopics;\n\ndefaultHistoryTTL#43b46b20 period:int = DefaultHistoryTTL;\n\nexportedContactToken#41bf109b url:string expires:int = ExportedContactToken;\n\nrequestPeerTypeUser#5f3b8a00 flags:# bot:flags.0?Bool premium:flags.1?Bool = RequestPeerType;\nrequestPeerTypeChat#c9f06e1b flags:# creator:flags.0?true bot_participant:flags.5?true has_username:flags.3?Bool forum:flags.4?Bool user_admin_rights:flags.1?ChatAdminRights bot_admin_rights:flags.2?ChatAdminRights = RequestPeerType;\nrequestPeerTypeBroadcast#339bef6c flags:# creator:flags.0?true has_username:flags.3?Bool user_admin_rights:flags.1?ChatAdminRights bot_admin_rights:flags.2?ChatAdminRights = RequestPeerType;\nrequestPeerTypeCreateBot#3e81e078 flags:# bot_managed:flags.0?true suggested_name:flags.1?string suggested_username:flags.2?string = RequestPeerType;\n\nemojiListNotModified#481eadfa = EmojiList;\nemojiList#7a1e11d1 hash:long document_id:Vector<long> = EmojiList;\n\nemojiGroup#7a9abda9 title:string icon_emoji_id:long emoticons:Vector<string> = EmojiGroup;\nemojiGroupGreeting#80d26cc7 title:string icon_emoji_id:long emoticons:Vector<string> = EmojiGroup;\nemojiGroupPremium#93bcf34 title:string icon_emoji_id:long = EmojiGroup;\n\nmessages.emojiGroupsNotModified#6fb4ad87 = messages.EmojiGroups;\nmessages.emojiGroups#881fb94b hash:int groups:Vector<EmojiGroup> = messages.EmojiGroups;\n\ntextWithEntities#751f3146 text:string entities:Vector<MessageEntity> = TextWithEntities;\n\nmessages.translateResult#33db32f8 result:Vector<TextWithEntities> = messages.TranslatedText;\n\nautoSaveSettings#c84834ce flags:# photos:flags.0?true videos:flags.1?true video_max_size:flags.2?long = AutoSaveSettings;\n\nautoSaveException#81602d47 peer:Peer settings:AutoSaveSettings = AutoSaveException;\n\naccount.autoSaveSettings#4c3e069d users_settings:AutoSaveSettings chats_settings:AutoSaveSettings broadcasts_settings:AutoSaveSettings exceptions:Vector<AutoSaveException> chats:Vector<Chat> users:Vector<User> = account.AutoSaveSettings;\n\nhelp.appConfigNotModified#7cde641d = help.AppConfig;\nhelp.appConfig#dd18782e hash:int config:JSONValue = help.AppConfig;\n\ninputBotAppID#a920bd7a id:long access_hash:long = InputBotApp;\ninputBotAppShortName#908c0407 bot_id:InputUser short_name:string = InputBotApp;\n\nbotAppNotModified#5da674b7 = BotApp;\nbotApp#95fcd1d6 flags:# id:long access_hash:long short_name:string title:string description:string photo:Photo document:flags.0?Document hash:long = BotApp;\n\nmessages.botApp#eb50adf5 flags:# inactive:flags.0?true request_write_access:flags.1?true has_settings:flags.2?true app:BotApp = messages.BotApp;\n\ninlineBotWebView#b57295d5 text:string url:string = InlineBotWebView;\n\nreadParticipantDate#4a4ff172 user_id:long date:int = ReadParticipantDate;\n\ninputChatlistDialogFilter#f3e0da33 filter_id:int = InputChatlist;\n\nexportedChatlistInvite#c5181ac flags:# title:string url:string peers:Vector<Peer> = ExportedChatlistInvite;\n\nchatlists.exportedChatlistInvite#10e6e3a6 filter:DialogFilter invite:ExportedChatlistInvite = chatlists.ExportedChatlistInvite;\n\nchatlists.exportedInvites#10ab6dc7 invites:Vector<ExportedChatlistInvite> chats:Vector<Chat> users:Vector<User> = chatlists.ExportedInvites;\n\nchatlists.chatlistInviteAlready#fa87f659 filter_id:int missing_peers:Vector<Peer> already_peers:Vector<Peer> chats:Vector<Chat> users:Vector<User> = chatlists.ChatlistInvite;\nchatlists.chatlistInvite#f10ece2f flags:# title_noanimate:flags.1?true title:TextWithEntities emoticon:flags.0?string peers:Vector<Peer> chats:Vector<Chat> users:Vector<User> = chatlists.ChatlistInvite;\n\nchatlists.chatlistUpdates#93bd878d missing_peers:Vector<Peer> chats:Vector<Chat> users:Vector<User> = chatlists.ChatlistUpdates;\n\nbots.botInfo#e8a775b0 name:string about:string description:string = bots.BotInfo;\n\nmessagePeerVote#b6cc2d5c peer:Peer option:bytes date:int = MessagePeerVote;\nmessagePeerVoteInputOption#74cda504 peer:Peer date:int = MessagePeerVote;\nmessagePeerVoteMultiple#4628f6e6 peer:Peer options:Vector<bytes> date:int = MessagePeerVote;\n\nstoryViews#8d595cd6 flags:# has_viewers:flags.1?true views_count:int forwards_count:flags.2?int reactions:flags.3?Vector<ReactionCount> reactions_count:flags.4?int recent_viewers:flags.0?Vector<long> = StoryViews;\n\nstoryItemDeleted#51e6ee4f id:int = StoryItem;\nstoryItemSkipped#ffadc913 flags:# close_friends:flags.8?true live:flags.9?true id:int date:int expire_date:int = StoryItem;\nstoryItem#16a4b93c flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true out:flags.16?true id:int date:int from_id:flags.18?Peer fwd_from:flags.17?StoryFwdHeader expire_date:int caption:flags.0?string entities:flags.1?Vector<MessageEntity> media:MessageMedia media_areas:flags.14?Vector<MediaArea> privacy:flags.2?Vector<PrivacyRule> views:flags.3?StoryViews sent_reaction:flags.15?Reaction albums:flags.19?Vector<int> music:flags.20?Document = StoryItem;\n\nstories.allStoriesNotModified#1158fe3e flags:# state:string stealth_mode:StoriesStealthMode = stories.AllStories;\nstories.allStories#6efc5e81 flags:# has_more:flags.0?true count:int state:string peer_stories:Vector<PeerStories> chats:Vector<Chat> users:Vector<User> stealth_mode:StoriesStealthMode = stories.AllStories;\n\nstories.stories#63c3dd0a flags:# count:int stories:Vector<StoryItem> pinned_to_top:flags.0?Vector<int> chats:Vector<Chat> users:Vector<User> = stories.Stories;\n\nstoryView#b0bdeac5 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true user_id:long date:int reaction:flags.2?Reaction = StoryView;\nstoryViewPublicForward#9083670b flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true message:Message = StoryView;\nstoryViewPublicRepost#bd74cf49 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true peer_id:Peer story:StoryItem = StoryView;\n\nstories.storyViewsList#59d78fc5 flags:# count:int views_count:int forwards_count:int reactions_count:int views:Vector<StoryView> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = stories.StoryViewsList;\n\nstories.storyViews#de9eed1d views:Vector<StoryViews> users:Vector<User> = stories.StoryViews;\n\ninputReplyToMessage#3bd4b7c2 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector<MessageEntity> quote_offset:flags.4?int monoforum_peer_id:flags.5?InputPeer todo_item_id:flags.6?int poll_option:flags.7?bytes = InputReplyTo;\ninputReplyToStory#5881323a peer:InputPeer story_id:int = InputReplyTo;\ninputReplyToMonoForum#69d66c45 monoforum_peer_id:InputPeer = InputReplyTo;\n\nexportedStoryLink#3fc9053b link:string = ExportedStoryLink;\n\nstoriesStealthMode#712e27fd flags:# active_until_date:flags.0?int cooldown_until_date:flags.1?int = StoriesStealthMode;\n\nmediaAreaCoordinates#cfc9e002 flags:# x:double y:double w:double h:double rotation:double radius:flags.0?double = MediaAreaCoordinates;\n\nmediaAreaVenue#be82db9c coordinates:MediaAreaCoordinates geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MediaArea;\ninputMediaAreaVenue#b282217f coordinates:MediaAreaCoordinates query_id:long result_id:string = MediaArea;\nmediaAreaGeoPoint#cad5452d flags:# coordinates:MediaAreaCoordinates geo:GeoPoint address:flags.0?GeoPointAddress = MediaArea;\nmediaAreaSuggestedReaction#14455871 flags:# dark:flags.0?true flipped:flags.1?true coordinates:MediaAreaCoordinates reaction:Reaction = MediaArea;\nmediaAreaChannelPost#770416af coordinates:MediaAreaCoordinates channel_id:long msg_id:int = MediaArea;\ninputMediaAreaChannelPost#2271f2bf coordinates:MediaAreaCoordinates channel:InputChannel msg_id:int = MediaArea;\nmediaAreaUrl#37381085 coordinates:MediaAreaCoordinates url:string = MediaArea;\nmediaAreaWeather#49a6549c coordinates:MediaAreaCoordinates emoji:string temperature_c:double color:int = MediaArea;\nmediaAreaStarGift#5787686d coordinates:MediaAreaCoordinates slug:string = MediaArea;\n\npeerStories#9a35e999 flags:# peer:Peer max_read_id:flags.0?int stories:Vector<StoryItem> = PeerStories;\n\nstories.peerStories#cae68768 stories:PeerStories chats:Vector<Chat> users:Vector<User> = stories.PeerStories;\n\nmessages.webPage#fd5e12bd webpage:WebPage chats:Vector<Chat> users:Vector<User> = messages.WebPage;\n\npremiumGiftCodeOption#257e962b flags:# users:int months:int store_product:flags.0?string store_quantity:flags.1?int currency:string amount:long = PremiumGiftCodeOption;\n\npayments.checkedGiftCode#eb983f8f flags:# via_giveaway:flags.2?true from_id:flags.4?Peer giveaway_msg_id:flags.3?int to_id:flags.0?long date:int days:int used_date:flags.1?int chats:Vector<Chat> users:Vector<User> = payments.CheckedGiftCode;\n\npayments.giveawayInfo#4367daa0 flags:# participating:flags.0?true preparing_results:flags.3?true start_date:int joined_too_early_date:flags.1?int admin_disallowed_chat_id:flags.2?long disallowed_country:flags.4?string = payments.GiveawayInfo;\npayments.giveawayInfoResults#e175e66f flags:# winner:flags.0?true refunded:flags.1?true start_date:int gift_code_slug:flags.3?string stars_prize:flags.4?long finish_date:int winners_count:int activated_count:flags.2?int = payments.GiveawayInfo;\n\nprepaidGiveaway#b2539d54 id:long months:int quantity:int date:int = PrepaidGiveaway;\nprepaidStarsGiveaway#9a9d77e0 id:long stars:long quantity:int boosts:int date:int = PrepaidGiveaway;\n\nboost#4b3e14d6 flags:# gift:flags.1?true giveaway:flags.2?true unclaimed:flags.3?true id:string user_id:flags.0?long giveaway_msg_id:flags.2?int date:int expires:int used_gift_slug:flags.4?string multiplier:flags.5?int stars:flags.6?long = Boost;\n\npremium.boostsList#86f8613c flags:# count:int boosts:Vector<Boost> next_offset:flags.0?string users:Vector<User> = premium.BoostsList;\n\nmyBoost#c448415c flags:# slot:int peer:flags.0?Peer date:int expires:int cooldown_until_date:flags.1?int = MyBoost;\n\npremium.myBoosts#9ae228e2 my_boosts:Vector<MyBoost> chats:Vector<Chat> users:Vector<User> = premium.MyBoosts;\n\npremium.boostsStatus#4959427a flags:# my_boost:flags.2?true level:int current_level_boosts:int boosts:int gift_boosts:flags.4?int next_level_boosts:flags.0?int premium_audience:flags.1?StatsPercentValue boost_url:string prepaid_giveaways:flags.3?Vector<PrepaidGiveaway> my_boost_slots:flags.2?Vector<int> = premium.BoostsStatus;\n\nstoryFwdHeader#b826e150 flags:# modified:flags.3?true from:flags.0?Peer from_name:flags.1?string story_id:flags.2?int = StoryFwdHeader;\n\npostInteractionCountersMessage#e7058e7f msg_id:int views:int forwards:int reactions:int = PostInteractionCounters;\npostInteractionCountersStory#8a480e27 story_id:int views:int forwards:int reactions:int = PostInteractionCounters;\n\nstats.storyStats#50cd067c views_graph:StatsGraph reactions_by_emotion_graph:StatsGraph = stats.StoryStats;\n\npublicForwardMessage#1f2bf4a message:Message = PublicForward;\npublicForwardStory#edf3add0 peer:Peer story:StoryItem = PublicForward;\n\nstats.publicForwards#93037e20 flags:# count:int forwards:Vector<PublicForward> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = stats.PublicForwards;\n\npeerColor#b54b5acf flags:# color:flags.0?int background_emoji_id:flags.1?long = PeerColor;\npeerColorCollectible#b9c0639a flags:# collectible_id:long gift_emoji_id:long background_emoji_id:long accent_color:int colors:Vector<int> dark_accent_color:flags.0?int dark_colors:flags.1?Vector<int> = PeerColor;\ninputPeerColorCollectible#b8ea86a9 collectible_id:long = PeerColor;\n\nhelp.peerColorSet#26219a58 colors:Vector<int> = help.PeerColorSet;\nhelp.peerColorProfileSet#767d61eb palette_colors:Vector<int> bg_colors:Vector<int> story_colors:Vector<int> = help.PeerColorSet;\n\nhelp.peerColorOption#adec6ebe flags:# hidden:flags.0?true color_id:int colors:flags.1?help.PeerColorSet dark_colors:flags.2?help.PeerColorSet channel_min_level:flags.3?int group_min_level:flags.4?int = help.PeerColorOption;\n\nhelp.peerColorsNotModified#2ba1f5ce = help.PeerColors;\nhelp.peerColors#f8ed08 hash:int colors:Vector<help.PeerColorOption> = help.PeerColors;\n\nstoryReaction#6090d6d5 peer_id:Peer date:int reaction:Reaction = StoryReaction;\nstoryReactionPublicForward#bbab2643 message:Message = StoryReaction;\nstoryReactionPublicRepost#cfcd0f13 peer_id:Peer story:StoryItem = StoryReaction;\n\nstories.storyReactionsList#aa5f789c flags:# count:int reactions:Vector<StoryReaction> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = stories.StoryReactionsList;\n\nsavedDialog#bd87cb6c flags:# pinned:flags.2?true peer:Peer top_message:int = SavedDialog;\nmonoForumDialog#64407ea7 flags:# unread_mark:flags.3?true nopaid_messages_exception:flags.4?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_reactions_count:int draft:flags.1?DraftMessage = SavedDialog;\n\nmessages.savedDialogs#f83ae221 dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs;\nmessages.savedDialogsSlice#44ba9dd9 count:int dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs;\nmessages.savedDialogsNotModified#c01f6fe8 count:int = messages.SavedDialogs;\n\nsavedReactionTag#cb6ff828 flags:# reaction:Reaction title:flags.0?string count:int = SavedReactionTag;\n\nmessages.savedReactionTagsNotModified#889b59ef = messages.SavedReactionTags;\nmessages.savedReactionTags#3259950a tags:Vector<SavedReactionTag> hash:long = messages.SavedReactionTags;\n\noutboxReadDate#3bb842ac date:int = OutboxReadDate;\n\nsmsjobs.eligibleToJoin#dc8b44cf terms_url:string monthly_sent_sms:int = smsjobs.EligibilityToJoin;\n\nsmsjobs.status#2aee9191 flags:# allow_international:flags.0?true recent_sent:int recent_since:int recent_remains:int total_sent:int total_since:int last_gift_slug:flags.1?string terms_url:string = smsjobs.Status;\n\nsmsJob#e6a1eeb8 job_id:string phone_number:string text:string = SmsJob;\n\nbusinessWeeklyOpen#120b1ab9 start_minute:int end_minute:int = BusinessWeeklyOpen;\n\nbusinessWorkHours#8c92b098 flags:# open_now:flags.0?true timezone_id:string weekly_open:Vector<BusinessWeeklyOpen> = BusinessWorkHours;\n\nbusinessLocation#ac5c1af7 flags:# geo_point:flags.0?GeoPoint address:string = BusinessLocation;\n\ninputBusinessRecipients#6f8b32aa flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector<InputUser> = InputBusinessRecipients;\n\nbusinessRecipients#21108ff7 flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector<long> = BusinessRecipients;\n\nbusinessAwayMessageScheduleAlways#c9b9e2b9 = BusinessAwayMessageSchedule;\nbusinessAwayMessageScheduleOutsideWorkHours#c3f2f501 = BusinessAwayMessageSchedule;\nbusinessAwayMessageScheduleCustom#cc4d9ecc start_date:int end_date:int = BusinessAwayMessageSchedule;\n\ninputBusinessGreetingMessage#194cb3b shortcut_id:int recipients:InputBusinessRecipients no_activity_days:int = InputBusinessGreetingMessage;\n\nbusinessGreetingMessage#e519abab shortcut_id:int recipients:BusinessRecipients no_activity_days:int = BusinessGreetingMessage;\n\ninputBusinessAwayMessage#832175e0 flags:# offline_only:flags.0?true shortcut_id:int schedule:BusinessAwayMessageSchedule recipients:InputBusinessRecipients = InputBusinessAwayMessage;\n\nbusinessAwayMessage#ef156a5c flags:# offline_only:flags.0?true shortcut_id:int schedule:BusinessAwayMessageSchedule recipients:BusinessRecipients = BusinessAwayMessage;\n\ntimezone#ff9289f5 id:string name:string utc_offset:int = Timezone;\n\nhelp.timezonesListNotModified#970708cc = help.TimezonesList;\nhelp.timezonesList#7b74ed71 timezones:Vector<Timezone> hash:int = help.TimezonesList;\n\nquickReply#697102b shortcut_id:int shortcut:string top_message:int count:int = QuickReply;\n\ninputQuickReplyShortcut#24596d41 shortcut:string = InputQuickReplyShortcut;\ninputQuickReplyShortcutId#1190cf1 shortcut_id:int = InputQuickReplyShortcut;\n\nmessages.quickReplies#c68d6695 quick_replies:Vector<QuickReply> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.QuickReplies;\nmessages.quickRepliesNotModified#5f91eb5b = messages.QuickReplies;\n\nconnectedBot#cd64636c flags:# bot_id:long recipients:BusinessBotRecipients rights:BusinessBotRights = ConnectedBot;\n\naccount.connectedBots#17d7f87b connected_bots:Vector<ConnectedBot> users:Vector<User> = account.ConnectedBots;\n\nmessages.dialogFilters#2ad93719 flags:# tags_enabled:flags.0?true filters:Vector<DialogFilter> = messages.DialogFilters;\n\nbirthday#6c8e1e06 flags:# day:int month:int year:flags.0?int = Birthday;\n\nbotBusinessConnection#8f34b2f5 flags:# disabled:flags.1?true connection_id:string user_id:long dc_id:int date:int rights:flags.2?BusinessBotRights = BotBusinessConnection;\n\ninputBusinessIntro#9c469cd flags:# title:string description:string sticker:flags.0?InputDocument = InputBusinessIntro;\n\nbusinessIntro#5a0a066d flags:# title:string description:string sticker:flags.0?Document = BusinessIntro;\n\nmessages.myStickers#faff629d count:int sets:Vector<StickerSetCovered> = messages.MyStickers;\n\ninputCollectibleUsername#e39460a9 username:string = InputCollectible;\ninputCollectiblePhone#a2e214a4 phone:string = InputCollectible;\n\nfragment.collectibleInfo#6ebdff91 purchase_date:int currency:string amount:long crypto_currency:string crypto_amount:long url:string = fragment.CollectibleInfo;\n\ninputBusinessBotRecipients#c4e5921e flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector<InputUser> exclude_users:flags.6?Vector<InputUser> = InputBusinessBotRecipients;\n\nbusinessBotRecipients#b88cf373 flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector<long> exclude_users:flags.6?Vector<long> = BusinessBotRecipients;\n\ncontactBirthday#1d998733 contact_id:long birthday:Birthday = ContactBirthday;\n\ncontacts.contactBirthdays#114ff30d contacts:Vector<ContactBirthday> users:Vector<User> = contacts.ContactBirthdays;\n\nmissingInvitee#628c9224 flags:# premium_would_allow_invite:flags.0?true premium_required_for_pm:flags.1?true user_id:long = MissingInvitee;\n\nmessages.invitedUsers#7f5defa6 updates:Updates missing_invitees:Vector<MissingInvitee> = messages.InvitedUsers;\n\ninputBusinessChatLink#11679fa7 flags:# message:string entities:flags.0?Vector<MessageEntity> title:flags.1?string = InputBusinessChatLink;\n\nbusinessChatLink#b4ae666f flags:# link:string message:string entities:flags.0?Vector<MessageEntity> title:flags.1?string views:int = BusinessChatLink;\n\naccount.businessChatLinks#ec43a2d1 links:Vector<BusinessChatLink> chats:Vector<Chat> users:Vector<User> = account.BusinessChatLinks;\n\naccount.resolvedBusinessChatLinks#9a23af21 flags:# peer:Peer message:string entities:flags.0?Vector<MessageEntity> chats:Vector<Chat> users:Vector<User> = account.ResolvedBusinessChatLinks;\n\nrequestedPeerUser#d62ff46a flags:# user_id:long first_name:flags.0?string last_name:flags.0?string username:flags.1?string photo:flags.2?Photo = RequestedPeer;\nrequestedPeerChat#7307544f flags:# chat_id:long title:flags.0?string photo:flags.2?Photo = RequestedPeer;\nrequestedPeerChannel#8ba403e4 flags:# channel_id:long title:flags.0?string username:flags.1?string photo:flags.2?Photo = RequestedPeer;\n\nsponsoredMessageReportOption#430d3150 text:string option:bytes = SponsoredMessageReportOption;\n\nchannels.sponsoredMessageReportResultChooseOption#846f9e42 title:string options:Vector<SponsoredMessageReportOption> = channels.SponsoredMessageReportResult;\nchannels.sponsoredMessageReportResultAdsHidden#3e3bcf2f = channels.SponsoredMessageReportResult;\nchannels.sponsoredMessageReportResultReported#ad798849 = channels.SponsoredMessageReportResult;\n\nreactionNotificationsFromContacts#bac3a61a = ReactionNotificationsFrom;\nreactionNotificationsFromAll#4b9e22a0 = ReactionNotificationsFrom;\n\nreactionsNotifySettings#71e4ea58 flags:# messages_notify_from:flags.0?ReactionNotificationsFrom stories_notify_from:flags.1?ReactionNotificationsFrom poll_votes_notify_from:flags.2?ReactionNotificationsFrom sound:NotificationSound show_previews:Bool = ReactionsNotifySettings;\n\navailableEffect#93c3e27e flags:# premium_required:flags.2?true id:long emoticon:string static_icon_id:flags.0?long effect_sticker_id:long effect_animation_id:flags.1?long = AvailableEffect;\n\nmessages.availableEffectsNotModified#d1ed9a5b = messages.AvailableEffects;\nmessages.availableEffects#bddb616e hash:int effects:Vector<AvailableEffect> documents:Vector<Document> = messages.AvailableEffects;\n\nfactCheck#b89bfccf flags:# need_check:flags.0?true country:flags.1?string text:flags.1?TextWithEntities hash:long = FactCheck;\n\nstarsTransactionPeerUnsupported#95f2bfe4 = StarsTransactionPeer;\nstarsTransactionPeerAppStore#b457b375 = StarsTransactionPeer;\nstarsTransactionPeerPlayMarket#7b560a0b = StarsTransactionPeer;\nstarsTransactionPeerPremiumBot#250dbaf8 = StarsTransactionPeer;\nstarsTransactionPeerFragment#e92fd902 = StarsTransactionPeer;\nstarsTransactionPeer#d80da15d peer:Peer = StarsTransactionPeer;\nstarsTransactionPeerAds#60682812 = StarsTransactionPeer;\nstarsTransactionPeerAPI#f9677aad = StarsTransactionPeer;\n\nstarsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption;\n\nstarsTransaction#13659eb0 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true business_transfer:flags.21?true stargift_resale:flags.22?true posts_search:flags.24?true stargift_prepaid_upgrade:flags.25?true stargift_drop_original_details:flags.26?true phonegroup_message:flags.27?true stargift_auction_bid:flags.28?true offer:flags.29?true id:string amount:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector<MessageMedia> subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount paid_messages:flags.19?int premium_gift_months:flags.20?int ads_proceeds_from_date:flags.23?int ads_proceeds_to_date:flags.23?int = StarsTransaction;\n\npayments.starsStatus#6c9ce8ed flags:# balance:StarsAmount subscriptions:flags.1?Vector<StarsSubscription> subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector<StarsTransaction> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = payments.StarsStatus;\n\nfoundStory#e87acbc0 peer:Peer story:StoryItem = FoundStory;\n\nstories.foundStories#e2de7737 flags:# count:int stories:Vector<FoundStory> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = stories.FoundStories;\n\ngeoPointAddress#de4c5d93 flags:# country_iso2:string state:flags.0?string city:flags.1?string street:flags.2?string = GeoPointAddress;\n\nstarsRevenueStatus#febe5491 flags:# withdrawal_enabled:flags.0?true current_balance:StarsAmount available_balance:StarsAmount overall_revenue:StarsAmount next_withdrawal_at:flags.1?int = StarsRevenueStatus;\n\npayments.starsRevenueStats#6c207376 flags:# top_hours_graph:flags.0?StatsGraph revenue_graph:StatsGraph status:StarsRevenueStatus usd_rate:double = payments.StarsRevenueStats;\n\npayments.starsRevenueWithdrawalUrl#1dab80b7 url:string = payments.StarsRevenueWithdrawalUrl;\n\npayments.starsRevenueAdsAccountUrl#394e7f21 url:string = payments.StarsRevenueAdsAccountUrl;\n\ninputStarsTransaction#206ae6d1 flags:# refund:flags.0?true id:string = InputStarsTransaction;\n\nstarsGiftOption#5e0589f1 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsGiftOption;\n\nbots.popularAppBots#1991b13b flags:# next_offset:flags.0?string users:Vector<User> = bots.PopularAppBots;\n\nbotPreviewMedia#23e91ba3 date:int media:MessageMedia = BotPreviewMedia;\n\nbots.previewInfo#ca71d64 media:Vector<BotPreviewMedia> lang_codes:Vector<string> = bots.PreviewInfo;\n\nstarsSubscriptionPricing#5416d58 period:int amount:long = StarsSubscriptionPricing;\n\nstarsSubscription#2e6eab1a flags:# canceled:flags.0?true can_refulfill:flags.1?true missing_balance:flags.2?true bot_canceled:flags.7?true id:string peer:Peer until_date:int pricing:StarsSubscriptionPricing chat_invite_hash:flags.3?string title:flags.4?string photo:flags.5?WebDocument invoice_slug:flags.6?string = StarsSubscription;\n\nmessageReactor#4ba3a95a flags:# top:flags.0?true my:flags.1?true anonymous:flags.2?true peer_id:flags.3?Peer count:int = MessageReactor;\n\nstarsGiveawayOption#94ce852a flags:# extended:flags.0?true default:flags.1?true stars:long yearly_boosts:int store_product:flags.2?string currency:string amount:long winners:Vector<StarsGiveawayWinnersOption> = StarsGiveawayOption;\n\nstarsGiveawayWinnersOption#54236209 flags:# default:flags.0?true users:int per_user_stars:long = StarsGiveawayWinnersOption;\n\nstarGift#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;\nstarGiftUnique#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<StarGiftAttribute> availability_issued:int availability_total:int gift_address:flags.3?string resell_amount:flags.4?Vector<StarsAmount> 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;\n\npayments.starGiftsNotModified#a388a368 = payments.StarGifts;\npayments.starGifts#2ed82995 hash:int gifts:Vector<StarGift> chats:Vector<Chat> users:Vector<User> = payments.StarGifts;\n\nmessageReportOption#7903e3d9 text:string option:bytes = MessageReportOption;\n\nreportResultChooseOption#f0e4e0b6 title:string options:Vector<MessageReportOption> = ReportResult;\nreportResultAddComment#6f09ac31 flags:# optional:flags.0?true option:bytes = ReportResult;\nreportResultReported#8db33c4b = ReportResult;\n\nmessages.botPreparedInlineMessage#8ecf0511 id:string expire_date:int = messages.BotPreparedInlineMessage;\n\nmessages.preparedInlineMessage#ff57708d query_id:long result:BotInlineResult peer_types:Vector<InlineQueryPeerType> cache_time:int users:Vector<User> = messages.PreparedInlineMessage;\n\nbotAppSettings#c99b1950 flags:# placeholder_path:flags.0?bytes background_color:flags.1?int background_dark_color:flags.2?int header_color:flags.3?int header_dark_color:flags.4?int = BotAppSettings;\n\nstarRefProgram#dd0c66f2 flags:# bot_id:long commission_permille:int duration_months:flags.0?int end_date:flags.1?int daily_revenue_per_user:flags.2?StarsAmount = StarRefProgram;\n\nconnectedBotStarRef#19a13f71 flags:# revoked:flags.1?true url:string date:int bot_id:long commission_permille:int duration_months:flags.0?int participants:long revenue:long = ConnectedBotStarRef;\n\npayments.connectedStarRefBots#98d5ea1d count:int connected_bots:Vector<ConnectedBotStarRef> users:Vector<User> = payments.ConnectedStarRefBots;\n\npayments.suggestedStarRefBots#b4d5d859 flags:# count:int suggested_bots:Vector<StarRefProgram> users:Vector<User> next_offset:flags.0?string = payments.SuggestedStarRefBots;\n\nstarsAmount#bbb6b4a3 amount:long nanos:int = StarsAmount;\nstarsTonAmount#74aee3e0 amount:long = StarsAmount;\n\nmessages.foundStickersNotModified#6010c534 flags:# next_offset:flags.0?int = messages.FoundStickers;\nmessages.foundStickers#82c9e290 flags:# next_offset:flags.0?int hash:long stickers:Vector<Document> = messages.FoundStickers;\n\nbotVerifierSettings#b0cd6617 flags:# can_modify_custom_description:flags.1?true icon:long company:string custom_description:flags.0?string = BotVerifierSettings;\n\nbotVerification#f93cd45c bot_id:long icon:long description:string = BotVerification;\n\nstarGiftAttributeModel#565251e2 flags:# crafted:flags.0?true name:string document:Document rarity:StarGiftAttributeRarity = StarGiftAttribute;\nstarGiftAttributePattern#4e7085ea name:string document:Document rarity:StarGiftAttributeRarity = StarGiftAttribute;\nstarGiftAttributeBackdrop#9f2504e4 name:string backdrop_id:int center_color:int edge_color:int pattern_color:int text_color:int rarity:StarGiftAttributeRarity = StarGiftAttribute;\nstarGiftAttributeOriginalDetails#e0bff26c flags:# sender_id:flags.0?Peer recipient_id:Peer date:int message:flags.1?TextWithEntities = StarGiftAttribute;\n\npayments.starGiftUpgradePreview#3de1dfed sample_attributes:Vector<StarGiftAttribute> prices:Vector<StarGiftUpgradePrice> next_prices:Vector<StarGiftUpgradePrice> = payments.StarGiftUpgradePreview;\n\nusers.users#62d706b8 users:Vector<User> = users.Users;\nusers.usersSlice#315a4974 count:int users:Vector<User> = users.Users;\n\npayments.uniqueStarGift#416c56e8 gift:StarGift chats:Vector<Chat> users:Vector<User> = payments.UniqueStarGift;\n\nmessages.webPagePreview#8c9a88ac media:MessageMedia chats:Vector<Chat> users:Vector<User> = messages.WebPagePreview;\n\nsavedStarGift#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<int> 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;\n\npayments.savedStarGifts#95f389b1 flags:# count:int chat_notifications_enabled:flags.1?Bool gifts:Vector<SavedStarGift> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = payments.SavedStarGifts;\n\ninputSavedStarGiftUser#69279795 msg_id:int = InputSavedStarGift;\ninputSavedStarGiftChat#f101aa7f peer:InputPeer saved_id:long = InputSavedStarGift;\ninputSavedStarGiftSlug#2085c238 slug:string = InputSavedStarGift;\n\npayments.starGiftWithdrawalUrl#84aa3a9c url:string = payments.StarGiftWithdrawalUrl;\n\npaidReactionPrivacyDefault#206ad49e = PaidReactionPrivacy;\npaidReactionPrivacyAnonymous#1f0c1ad9 = PaidReactionPrivacy;\npaidReactionPrivacyPeer#dc6cfcf0 peer:InputPeer = PaidReactionPrivacy;\n\naccount.paidMessagesRevenue#1e109708 stars_amount:long = account.PaidMessagesRevenue;\n\nrequirementToContactEmpty#50a9839 = RequirementToContact;\nrequirementToContactPremium#e581e4e9 = RequirementToContact;\nrequirementToContactPaidMessages#b4f67e93 stars_amount:long = RequirementToContact;\n\nbusinessBotRights#a0624cf7 flags:# reply:flags.0?true read_messages:flags.1?true delete_sent_messages:flags.2?true delete_received_messages:flags.3?true edit_name:flags.4?true edit_bio:flags.5?true edit_profile_photo:flags.6?true edit_username:flags.7?true view_gifts:flags.8?true sell_gifts:flags.9?true change_gift_settings:flags.10?true transfer_and_upgrade_gifts:flags.11?true transfer_stars:flags.12?true manage_stories:flags.13?true = BusinessBotRights;\n\ndisallowedGiftsSettings#71f276c4 flags:# disallow_unlimited_stargifts:flags.0?true disallow_limited_stargifts:flags.1?true disallow_unique_stargifts:flags.2?true disallow_premium_gifts:flags.3?true disallow_stargifts_from_channels:flags.4?true = DisallowedGiftsSettings;\n\nsponsoredPeer#c69708d3 flags:# random_id:bytes peer:Peer sponsor_info:flags.0?string additional_info:flags.1?string = SponsoredPeer;\n\ncontacts.sponsoredPeersEmpty#ea32b4b1 = contacts.SponsoredPeers;\ncontacts.sponsoredPeers#eb032884 peers:Vector<SponsoredPeer> chats:Vector<Chat> users:Vector<User> = contacts.SponsoredPeers;\n\nstarGiftAttributeIdModel#48aaae3c document_id:long = StarGiftAttributeId;\nstarGiftAttributeIdPattern#4a162433 document_id:long = StarGiftAttributeId;\nstarGiftAttributeIdBackdrop#1f01c757 backdrop_id:int = StarGiftAttributeId;\n\nstarGiftAttributeCounter#2eb1b658 attribute:StarGiftAttributeId count:int = StarGiftAttributeCounter;\n\npayments.resaleStarGifts#947a12df flags:# count:int gifts:Vector<StarGift> next_offset:flags.0?string attributes:flags.1?Vector<StarGiftAttribute> attributes_hash:flags.1?long chats:Vector<Chat> counters:flags.2?Vector<StarGiftAttributeCounter> users:Vector<User> = payments.ResaleStarGifts;\n\nstories.canSendStoryCount#c387c04e count_remains:int = stories.CanSendStoryCount;\n\npendingSuggestion#e7e82e12 suggestion:string title:TextWithEntities description:TextWithEntities url:string = PendingSuggestion;\n\ntodoItem#cba9a52f id:int title:TextWithEntities = TodoItem;\n\ntodoList#49b92a26 flags:# others_can_append:flags.0?true others_can_complete:flags.1?true title:TextWithEntities list:Vector<TodoItem> = TodoList;\n\ntodoCompletion#221bb5e4 id:int completed_by:Peer date:int = TodoCompletion;\n\nsuggestedPost#e8e37e5 flags:# accepted:flags.1?true rejected:flags.2?true price:flags.3?StarsAmount schedule_date:flags.0?int = SuggestedPost;\n\nstarsRating#1b0e4f07 flags:# level:int current_level_stars:long stars:long next_level_stars:flags.0?long = StarsRating;\n\nstarGiftCollection#9d6b13b0 flags:# collection_id:int title:string icon:flags.0?Document gifts_count:int hash:long = StarGiftCollection;\n\npayments.starGiftCollectionsNotModified#a0ba4f17 = payments.StarGiftCollections;\npayments.starGiftCollections#8a2932f3 collections:Vector<StarGiftCollection> = payments.StarGiftCollections;\n\nstoryAlbum#9325705a flags:# album_id:int title:string icon_photo:flags.0?Photo icon_video:flags.1?Document = StoryAlbum;\n\nstories.albumsNotModified#564edaeb = stories.Albums;\nstories.albums#c3987a3a hash:long albums:Vector<StoryAlbum> = stories.Albums;\n\nsearchPostsFlood#3e0b5b6a flags:# query_is_free:flags.0?true total_daily:int remains:int wait_till:flags.1?int stars_amount:long = SearchPostsFlood;\n\npayments.uniqueStarGiftValueInfo#512fe446 flags:# last_sale_on_fragment:flags.1?true value_is_average:flags.6?true currency:string value:long initial_sale_date:int initial_sale_stars:long initial_sale_price:long last_sale_date:flags.0?int last_sale_price:flags.0?long floor_price:flags.2?long average_price:flags.3?long listed_count:flags.4?int fragment_listed_count:flags.5?int fragment_listed_url:flags.5?string = payments.UniqueStarGiftValueInfo;\n\nprofileTabPosts#b98cd696 = ProfileTab;\nprofileTabGifts#4d4bd46a = ProfileTab;\nprofileTabMedia#72c64955 = ProfileTab;\nprofileTabFiles#ab339c00 = ProfileTab;\nprofileTabMusic#9f27d26e = ProfileTab;\nprofileTabVoice#e477092e = ProfileTab;\nprofileTabLinks#d3656499 = ProfileTab;\nprofileTabGifs#a2c0f695 = ProfileTab;\n\nusers.savedMusicNotModified#e3878aa4 count:int = users.SavedMusic;\nusers.savedMusic#34a2f297 count:int documents:Vector<Document> = users.SavedMusic;\n\naccount.savedMusicIdsNotModified#4fc81d6e = account.SavedMusicIds;\naccount.savedMusicIds#998d6636 ids:Vector<long> = account.SavedMusicIds;\n\npayments.checkCanSendGiftResultOk#374fa7ad = payments.CheckCanSendGiftResult;\npayments.checkCanSendGiftResultFail#d5e58274 reason:TextWithEntities = payments.CheckCanSendGiftResult;\n\ninputChatThemeEmpty#83268483 = InputChatTheme;\ninputChatTheme#c93de95c emoticon:string = InputChatTheme;\ninputChatThemeUniqueGift#87e5dfe4 slug:string = InputChatTheme;\n\nstarGiftUpgradePrice#99ea331d date:int upgrade_stars:long = StarGiftUpgradePrice;\n\ngroupCallMessage#1a8afc7e flags:# from_admin:flags.1?true id:int from_id:Peer date:int message:TextWithEntities paid_message_stars:flags.0?long = GroupCallMessage;\n\ngroupCallDonor#ee430c85 flags:# top:flags.0?true my:flags.1?true peer_id:flags.3?Peer stars:long = GroupCallDonor;\n\nphone.groupCallStars#9d1dbd26 total_stars:long top_donors:Vector<GroupCallDonor> chats:Vector<Chat> users:Vector<User> = phone.GroupCallStars;\n\nrecentStory#711d692d flags:# live:flags.0?true max_id:flags.1?int = RecentStory;\n\nauctionBidLevel#310240cc pos:int amount:long date:int = AuctionBidLevel;\n\nstarGiftAuctionStateNotModified#fe333952 = StarGiftAuctionState;\nstarGiftAuctionState#771a4e66 version:int start_date:int end_date:int min_bid_amount:long bid_levels:Vector<AuctionBidLevel> top_bidders:Vector<long> next_round_at:int last_gift_num:int gifts_left:int current_round:int total_rounds:int rounds:Vector<StarGiftAuctionRound> = StarGiftAuctionState;\nstarGiftAuctionStateFinished#972dabbf flags:# start_date:int end_date:int average_price:long listed_count:flags.0?int fragment_listed_count:flags.1?int fragment_listed_url:flags.1?string = StarGiftAuctionState;\n\nstarGiftAuctionUserState#2eeed1c4 flags:# returned:flags.1?true bid_amount:flags.0?long bid_date:flags.0?int min_bid_amount:flags.0?long bid_peer:flags.0?Peer acquired_count:int = StarGiftAuctionUserState;\n\npayments.starGiftAuctionState#6b39f4ec gift:StarGift state:StarGiftAuctionState user_state:StarGiftAuctionUserState timeout:int users:Vector<User> chats:Vector<Chat> = payments.StarGiftAuctionState;\n\nstarGiftAuctionAcquiredGift#42b00348 flags:# name_hidden:flags.0?true peer:Peer date:int bid_amount:long round:int pos:int message:flags.1?TextWithEntities gift_num:flags.2?int = StarGiftAuctionAcquiredGift;\n\npayments.starGiftAuctionAcquiredGifts#7d5bd1f0 gifts:Vector<StarGiftAuctionAcquiredGift> users:Vector<User> chats:Vector<Chat> = payments.StarGiftAuctionAcquiredGifts;\n\nstarGiftActiveAuctionState#d31bc45d gift:StarGift state:StarGiftAuctionState user_state:StarGiftAuctionUserState = StarGiftActiveAuctionState;\n\npayments.starGiftActiveAuctionsNotModified#db33dad0 = payments.StarGiftActiveAuctions;\npayments.starGiftActiveAuctions#aef6abbc auctions:Vector<StarGiftActiveAuctionState> users:Vector<User> chats:Vector<Chat> = payments.StarGiftActiveAuctions;\n\ninputStarGiftAuction#2e16c98 gift_id:long = InputStarGiftAuction;\ninputStarGiftAuctionSlug#7ab58308 slug:string = InputStarGiftAuction;\n\npasskey#98613ebf flags:# id:string name:string date:int software_emoji_id:flags.0?long last_usage_date:flags.1?int = Passkey;\n\naccount.passkeys#f8e0aa1c passkeys:Vector<Passkey> = account.Passkeys;\n\naccount.passkeyRegistrationOptions#e16b5ce1 options:DataJSON = account.PasskeyRegistrationOptions;\n\nauth.passkeyLoginOptions#e2037789 options:DataJSON = auth.PasskeyLoginOptions;\n\ninputPasskeyResponseRegister#3e63935c client_data:DataJSON attestation_data:bytes = InputPasskeyResponse;\ninputPasskeyResponseLogin#c31fc14a client_data:DataJSON authenticator_data:bytes signature:bytes user_handle:string = InputPasskeyResponse;\n\ninputPasskeyCredentialPublicKey#3c27b78f id:string raw_id:string response:InputPasskeyResponse = InputPasskeyCredential;\ninputPasskeyCredentialFirebasePNV#5b1ccb28 pnv_token:string = InputPasskeyCredential;\n\nstarGiftBackground#aff56398 center_color:int edge_color:int text_color:int = StarGiftBackground;\n\nstarGiftAuctionRound#3aae0528 num:int duration:int = StarGiftAuctionRound;\nstarGiftAuctionRoundExtendable#aa021e5 num:int duration:int extend_top:int extend_window:int = StarGiftAuctionRound;\n\npayments.starGiftUpgradeAttributes#46c6e36f attributes:Vector<StarGiftAttribute> = payments.StarGiftUpgradeAttributes;\n\nmessages.emojiGameOutcome#da2ad647 seed:bytes stake_ton_amount:long ton_amount:long = messages.EmojiGameOutcome;\n\nmessages.emojiGameUnavailable#59e65335 = messages.EmojiGameInfo;\nmessages.emojiGameDiceInfo#44e56023 flags:# game_hash:string prev_stake:long current_streak:int params:Vector<int> plays_left:flags.0?int = messages.EmojiGameInfo;\n\nstarGiftAttributeRarity#36437737 permille:int = StarGiftAttributeRarity;\nstarGiftAttributeRarityUncommon#dbce6389 = StarGiftAttributeRarity;\nstarGiftAttributeRarityRare#f08d516b = StarGiftAttributeRarity;\nstarGiftAttributeRarityEpic#78fbf3a8 = StarGiftAttributeRarity;\nstarGiftAttributeRarityLegendary#cef7e7a8 = StarGiftAttributeRarity;\n\nkeyboardButtonStyle#4fdd3430 flags:# bg_primary:flags.0?true bg_danger:flags.1?true bg_success:flags.2?true icon:flags.3?long = KeyboardButtonStyle;\n\ninputMessageReadMetric#402b4495 msg_id:int view_id:long time_in_view_ms:int active_time_in_view_ms:int height_to_viewport_ratio_permille:int seen_range_ratio_permille:int = InputMessageReadMetric;\n\nbots.exportedBotToken#3c60b621 token:string = bots.ExportedBotToken;\n\nbots.requestedButton#f13bbcd7 webapp_req_id:string = bots.RequestedButton;\n\nmessages.composedMessageWithAI#90d7adfa flags:# result_text:TextWithEntities diff_text:flags.0?TextWithEntities = messages.ComposedMessageWithAI;\n\n---functions---\n\ninvokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;\ninvokeAfterMsgs#3dc4b4f0 {X:Type} msg_ids:Vector<long> query:!X = X;\ninitConnection#c1cd5ea9 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy params:flags.1?JSONValue query:!X = X;\ninvokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;\ninvokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X;\ninvokeWithMessagesRange#365275f2 {X:Type} range:MessageRange query:!X = X;\ninvokeWithTakeout#aca9fd2e {X:Type} takeout_id:long query:!X = X;\ninvokeWithBusinessConnection#dd289f8e {X:Type} connection_id:string query:!X = X;\ninvokeWithGooglePlayIntegrity#1df92984 {X:Type} nonce:string token:string query:!X = X;\ninvokeWithApnsSecret#0dae54f8 {X:Type} nonce:string secret:string query:!X = X;\ninvokeWithReCaptcha#adbb0f94 {X:Type} token:string query:!X = X;\n\nauth.sendCode#a677244f phone_number:string api_id:int api_hash:string settings:CodeSettings = auth.SentCode;\nauth.signUp#aac7b717 flags:# no_joined_notifications:flags.0?true phone_number:string phone_code_hash:string first_name:string last_name:string = auth.Authorization;\nauth.signIn#8d52a951 flags:# phone_number:string phone_code_hash:string phone_code:flags.0?string email_verification:flags.1?EmailVerification = auth.Authorization;\nauth.logOut#3e72ba19 = auth.LoggedOut;\nauth.resetAuthorizations#9fab0d1a = Bool;\nauth.exportAuthorization#e5bfffcd dc_id:int = auth.ExportedAuthorization;\nauth.importAuthorization#a57a7dad id:long bytes:bytes = auth.Authorization;\nauth.bindTempAuthKey#cdd42a05 perm_auth_key_id:long nonce:long expires_at:int encrypted_message:bytes = Bool;\nauth.importBotAuthorization#67a3ff2c flags:int api_id:int api_hash:string bot_auth_token:string = auth.Authorization;\nauth.checkPassword#d18b4d16 password:InputCheckPasswordSRP = auth.Authorization;\nauth.requestPasswordRecovery#d897bc66 = auth.PasswordRecovery;\nauth.recoverPassword#37096c70 flags:# code:string new_settings:flags.0?account.PasswordInputSettings = auth.Authorization;\nauth.resendCode#cae47523 flags:# phone_number:string phone_code_hash:string reason:flags.0?string = auth.SentCode;\nauth.cancelCode#1f040578 phone_number:string phone_code_hash:string = Bool;\nauth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector<long> = Bool;\nauth.exportLoginToken#b7e085fe api_id:int api_hash:string except_ids:Vector<long> = auth.LoginToken;\nauth.importLoginToken#95ac5ce4 token:bytes = auth.LoginToken;\nauth.acceptLoginToken#e894ad4d token:bytes = Authorization;\nauth.checkRecoveryPassword#d36bf79 code:string = Bool;\nauth.importWebTokenAuthorization#2db873a9 api_id:int api_hash:string web_auth_token:string = auth.Authorization;\nauth.requestFirebaseSms#8e39261e flags:# phone_number:string phone_code_hash:string safety_net_token:flags.0?string play_integrity_token:flags.2?string ios_push_secret:flags.1?string = Bool;\nauth.resetLoginEmail#7e960193 phone_number:string phone_code_hash:string = auth.SentCode;\nauth.reportMissingCode#cb9deff6 phone_number:string phone_code_hash:string mnc:string = Bool;\nauth.checkPaidAuth#56e59f9c phone_number:string phone_code_hash:string form_id:long = auth.SentCode;\nauth.initPasskeyLogin#518ad0b7 api_id:int api_hash:string = auth.PasskeyLoginOptions;\nauth.finishPasskeyLogin#9857ad07 flags:# credential:InputPasskeyCredential from_dc_id:flags.0?int from_auth_key_id:flags.0?long = auth.Authorization;\n\naccount.registerDevice#ec86017a flags:# no_muted:flags.0?true token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector<long> = Bool;\naccount.unregisterDevice#6a0d3206 token_type:int token:string other_uids:Vector<long> = Bool;\naccount.updateNotifySettings#84be5b93 peer:InputNotifyPeer settings:InputPeerNotifySettings = Bool;\naccount.getNotifySettings#12b3ad31 peer:InputNotifyPeer = PeerNotifySettings;\naccount.resetNotifySettings#db7e1747 = Bool;\naccount.updateProfile#78515775 flags:# first_name:flags.0?string last_name:flags.1?string about:flags.2?string = User;\naccount.updateStatus#6628562c offline:Bool = Bool;\naccount.getWallPapers#7967d36 hash:long = account.WallPapers;\naccount.reportPeer#c5ba3d86 peer:InputPeer reason:ReportReason message:string = Bool;\naccount.checkUsername#2714d86c username:string = Bool;\naccount.updateUsername#3e0bdd7c username:string = User;\naccount.getPrivacy#dadbc950 key:InputPrivacyKey = account.PrivacyRules;\naccount.setPrivacy#c9f81ce8 key:InputPrivacyKey rules:Vector<InputPrivacyRule> = account.PrivacyRules;\naccount.deleteAccount#a2c0cf74 flags:# reason:string password:flags.0?InputCheckPasswordSRP = Bool;\naccount.getAccountTTL#8fc711d = AccountDaysTTL;\naccount.setAccountTTL#2442485e ttl:AccountDaysTTL = Bool;\naccount.sendChangePhoneCode#82574ae5 phone_number:string settings:CodeSettings = auth.SentCode;\naccount.changePhone#70c32edb phone_number:string phone_code_hash:string phone_code:string = User;\naccount.updateDeviceLocked#38df3532 period:int = Bool;\naccount.getAuthorizations#e320c158 = account.Authorizations;\naccount.resetAuthorization#df77f3bc hash:long = Bool;\naccount.getPassword#548a30f5 = account.Password;\naccount.getPasswordSettings#9cd4eaf9 password:InputCheckPasswordSRP = account.PasswordSettings;\naccount.updatePasswordSettings#a59b102f password:InputCheckPasswordSRP new_settings:account.PasswordInputSettings = Bool;\naccount.sendConfirmPhoneCode#1b3faa88 hash:string settings:CodeSettings = auth.SentCode;\naccount.confirmPhone#5f2178c3 phone_code_hash:string phone_code:string = Bool;\naccount.getTmpPassword#449e0b51 password:InputCheckPasswordSRP period:int = account.TmpPassword;\naccount.getWebAuthorizations#182e6d6f = account.WebAuthorizations;\naccount.resetWebAuthorization#2d01b9ef hash:long = Bool;\naccount.resetWebAuthorizations#682d2594 = Bool;\naccount.getAllSecureValues#b288bc7d = Vector<SecureValue>;\naccount.getSecureValue#73665bc2 types:Vector<SecureValueType> = Vector<SecureValue>;\naccount.saveSecureValue#899fe31d value:InputSecureValue secure_secret_id:long = SecureValue;\naccount.deleteSecureValue#b880bc4b types:Vector<SecureValueType> = Bool;\naccount.getAuthorizationForm#a929597a bot_id:long scope:string public_key:string = account.AuthorizationForm;\naccount.acceptAuthorization#f3ed4c73 bot_id:long scope:string public_key:string value_hashes:Vector<SecureValueHash> credentials:SecureCredentialsEncrypted = Bool;\naccount.sendVerifyPhoneCode#a5a356f9 phone_number:string settings:CodeSettings = auth.SentCode;\naccount.verifyPhone#4dd3a7f6 phone_number:string phone_code_hash:string phone_code:string = Bool;\naccount.sendVerifyEmailCode#98e037bb purpose:EmailVerifyPurpose email:string = account.SentEmailCode;\naccount.verifyEmail#32da4cf purpose:EmailVerifyPurpose verification:EmailVerification = account.EmailVerified;\naccount.initTakeoutSession#8ef3eab0 flags:# contacts:flags.0?true message_users:flags.1?true message_chats:flags.2?true message_megagroups:flags.3?true message_channels:flags.4?true files:flags.5?true file_max_size:flags.5?long = account.Takeout;\naccount.finishTakeoutSession#1d2652ee flags:# success:flags.0?true = Bool;\naccount.confirmPasswordEmail#8fdf1920 code:string = Bool;\naccount.resendPasswordEmail#7a7f2a15 = Bool;\naccount.cancelPasswordEmail#c1cbd5b6 = Bool;\naccount.getContactSignUpNotification#9f07c728 = Bool;\naccount.setContactSignUpNotification#cff43f61 silent:Bool = Bool;\naccount.getNotifyExceptions#53577479 flags:# compare_sound:flags.1?true compare_stories:flags.2?true peer:flags.0?InputNotifyPeer = Updates;\naccount.getWallPaper#fc8ddbea wallpaper:InputWallPaper = WallPaper;\naccount.uploadWallPaper#e39a8f03 flags:# for_chat:flags.0?true file:InputFile mime_type:string settings:WallPaperSettings = WallPaper;\naccount.saveWallPaper#6c5a5b37 wallpaper:InputWallPaper unsave:Bool settings:WallPaperSettings = Bool;\naccount.installWallPaper#feed5769 wallpaper:InputWallPaper settings:WallPaperSettings = Bool;\naccount.resetWallPapers#bb3b9804 = Bool;\naccount.getAutoDownloadSettings#56da0b3f = account.AutoDownloadSettings;\naccount.saveAutoDownloadSettings#76f36233 flags:# low:flags.0?true high:flags.1?true settings:AutoDownloadSettings = Bool;\naccount.uploadTheme#1c3db333 flags:# file:InputFile thumb:flags.0?InputFile file_name:string mime_type:string = Document;\naccount.createTheme#652e4400 flags:# slug:string title:string document:flags.2?InputDocument settings:flags.3?Vector<InputThemeSettings> = Theme;\naccount.updateTheme#2bf40ccc flags:# format:string theme:InputTheme slug:flags.0?string title:flags.1?string document:flags.2?InputDocument settings:flags.3?Vector<InputThemeSettings> = Theme;\naccount.saveTheme#f257106c theme:InputTheme unsave:Bool = Bool;\naccount.installTheme#c727bb3b flags:# dark:flags.0?true theme:flags.1?InputTheme format:flags.2?string base_theme:flags.3?BaseTheme = Bool;\naccount.getTheme#3a5869ec format:string theme:InputTheme = Theme;\naccount.getThemes#7206e458 format:string hash:long = account.Themes;\naccount.setContentSettings#b574b16b flags:# sensitive_enabled:flags.0?true = Bool;\naccount.getContentSettings#8b9b4dae = account.ContentSettings;\naccount.getMultiWallPapers#65ad71dc wallpapers:Vector<InputWallPaper> = Vector<WallPaper>;\naccount.getGlobalPrivacySettings#eb2b4cf6 = GlobalPrivacySettings;\naccount.setGlobalPrivacySettings#1edaaac2 settings:GlobalPrivacySettings = GlobalPrivacySettings;\naccount.reportProfilePhoto#fa8cc6f5 peer:InputPeer photo_id:InputPhoto reason:ReportReason message:string = Bool;\naccount.resetPassword#9308ce1b = account.ResetPasswordResult;\naccount.declinePasswordReset#4c9409f6 = Bool;\naccount.getChatThemes#d638de89 hash:long = account.Themes;\naccount.setAuthorizationTTL#bf899aa0 authorization_ttl_days:int = Bool;\naccount.changeAuthorizationSettings#40f48462 flags:# confirmed:flags.3?true hash:long encrypted_requests_disabled:flags.0?Bool call_requests_disabled:flags.1?Bool = Bool;\naccount.getSavedRingtones#e1902288 hash:long = account.SavedRingtones;\naccount.saveRingtone#3dea5b03 id:InputDocument unsave:Bool = account.SavedRingtone;\naccount.uploadRingtone#831a83a2 file:InputFile file_name:string mime_type:string = Document;\naccount.updateEmojiStatus#fbd3de6b emoji_status:EmojiStatus = Bool;\naccount.getDefaultEmojiStatuses#d6753386 hash:long = account.EmojiStatuses;\naccount.getRecentEmojiStatuses#f578105 hash:long = account.EmojiStatuses;\naccount.clearRecentEmojiStatuses#18201aae = Bool;\naccount.reorderUsernames#ef500eab order:Vector<string> = Bool;\naccount.toggleUsername#58d6b376 username:string active:Bool = Bool;\naccount.getDefaultProfilePhotoEmojis#e2750328 hash:long = EmojiList;\naccount.getDefaultGroupPhotoEmojis#915860ae hash:long = EmojiList;\naccount.getAutoSaveSettings#adcbbcda = account.AutoSaveSettings;\naccount.saveAutoSaveSettings#d69b8361 flags:# users:flags.0?true chats:flags.1?true broadcasts:flags.2?true peer:flags.3?InputPeer settings:AutoSaveSettings = Bool;\naccount.deleteAutoSaveExceptions#53bc0020 = Bool;\naccount.invalidateSignInCodes#ca8ae8ba codes:Vector<string> = Bool;\naccount.updateColor#684d214e flags:# for_profile:flags.1?true color:flags.2?PeerColor = Bool;\naccount.getDefaultBackgroundEmojis#a60ab9ce hash:long = EmojiList;\naccount.getChannelDefaultEmojiStatuses#7727a7d5 hash:long = account.EmojiStatuses;\naccount.getChannelRestrictedStatusEmojis#35a9e0d5 hash:long = EmojiList;\naccount.updateBusinessWorkHours#4b00e066 flags:# business_work_hours:flags.0?BusinessWorkHours = Bool;\naccount.updateBusinessLocation#9e6b131a flags:# geo_point:flags.1?InputGeoPoint address:flags.0?string = Bool;\naccount.updateBusinessGreetingMessage#66cdafc4 flags:# message:flags.0?InputBusinessGreetingMessage = Bool;\naccount.updateBusinessAwayMessage#a26a7fa5 flags:# message:flags.0?InputBusinessAwayMessage = Bool;\naccount.updateConnectedBot#66a08c7e flags:# deleted:flags.1?true rights:flags.0?BusinessBotRights bot:InputUser recipients:InputBusinessBotRecipients = Updates;\naccount.getConnectedBots#4ea4c80f = account.ConnectedBots;\naccount.getBotBusinessConnection#76a86270 connection_id:string = Updates;\naccount.updateBusinessIntro#a614d034 flags:# intro:flags.0?InputBusinessIntro = Bool;\naccount.toggleConnectedBotPaused#646e1097 peer:InputPeer paused:Bool = Bool;\naccount.disablePeerConnectedBot#5e437ed9 peer:InputPeer = Bool;\naccount.updateBirthday#cc6e0c11 flags:# birthday:flags.0?Birthday = Bool;\naccount.createBusinessChatLink#8851e68e link:InputBusinessChatLink = BusinessChatLink;\naccount.editBusinessChatLink#8c3410af slug:string link:InputBusinessChatLink = BusinessChatLink;\naccount.deleteBusinessChatLink#60073674 slug:string = Bool;\naccount.getBusinessChatLinks#6f70dde1 = account.BusinessChatLinks;\naccount.resolveBusinessChatLink#5492e5ee slug:string = account.ResolvedBusinessChatLinks;\naccount.updatePersonalChannel#d94305e0 channel:InputChannel = Bool;\naccount.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool;\naccount.getReactionsNotifySettings#6dd654c = ReactionsNotifySettings;\naccount.setReactionsNotifySettings#316ce548 settings:ReactionsNotifySettings = ReactionsNotifySettings;\naccount.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses;\naccount.getPaidMessagesRevenue#19ba4a67 flags:# parent_peer:flags.0?InputPeer user_id:InputUser = account.PaidMessagesRevenue;\naccount.toggleNoPaidMessagesException#fe2eda76 flags:# refund_charged:flags.0?true require_payment:flags.2?true parent_peer:flags.1?InputPeer user_id:InputUser = Bool;\naccount.setMainProfileTab#5dee78b0 tab:ProfileTab = Bool;\naccount.saveMusic#b26732a9 flags:# unsave:flags.0?true id:InputDocument after_id:flags.1?InputDocument = Bool;\naccount.getSavedMusicIds#e09d5faf hash:long = account.SavedMusicIds;\naccount.getUniqueGiftChatThemes#e42ce9c9 offset:string limit:int hash:long = account.ChatThemes;\naccount.initPasskeyRegistration#429547e8 = account.PasskeyRegistrationOptions;\naccount.registerPasskey#55b41fd6 credential:InputPasskeyCredential = Passkey;\naccount.getPasskeys#ea1f0c52 = account.Passkeys;\naccount.deletePasskey#f5b5563f id:string = Bool;\n\nusers.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;\nusers.getFullUser#b60f5918 id:InputUser = users.UserFull;\nusers.setSecureValueErrors#90c894b5 id:InputUser errors:Vector<SecureValueError> = Bool;\nusers.getRequirementsToContact#d89a83a3 id:Vector<InputUser> = Vector<RequirementToContact>;\nusers.getSavedMusic#788d7fe3 id:InputUser offset:int limit:int hash:long = users.SavedMusic;\nusers.getSavedMusicByID#7573a4e9 id:InputUser documents:Vector<InputDocument> = users.SavedMusic;\nusers.suggestBirthday#fc533372 id:InputUser birthday:Birthday = Updates;\n\ncontacts.getContactIDs#7adc669d hash:long = Vector<int>;\ncontacts.getStatuses#c4a353ee = Vector<ContactStatus>;\ncontacts.getContacts#5dd69e12 hash:long = contacts.Contacts;\ncontacts.importContacts#2c800be5 contacts:Vector<InputContact> = contacts.ImportedContacts;\ncontacts.deleteContacts#96a0e00 id:Vector<InputUser> = Updates;\ncontacts.deleteByPhones#1013fd9e phones:Vector<string> = Bool;\ncontacts.block#2e2e8734 flags:# my_stories_from:flags.0?true id:InputPeer = Bool;\ncontacts.unblock#b550d328 flags:# my_stories_from:flags.0?true id:InputPeer = Bool;\ncontacts.getBlocked#9a868f80 flags:# my_stories_from:flags.0?true offset:int limit:int = contacts.Blocked;\ncontacts.search#11f812d8 q:string limit:int = contacts.Found;\ncontacts.resolveUsername#725afbbc flags:# username:string referer:flags.0?string = contacts.ResolvedPeer;\ncontacts.getTopPeers#973478b6 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true bots_app:flags.16?true offset:int limit:int hash:long = contacts.TopPeers;\ncontacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool;\ncontacts.resetSaved#879537f1 = Bool;\ncontacts.getSaved#82f1e39f = Vector<SavedContact>;\ncontacts.toggleTopPeers#8514bdda enabled:Bool = Bool;\ncontacts.addContact#d9ba2e54 flags:# add_phone_privacy_exception:flags.0?true id:InputUser first_name:string last_name:string phone:string note:flags.1?TextWithEntities = Updates;\ncontacts.acceptContact#f831a20f id:InputUser = Updates;\ncontacts.getLocated#d348bc44 flags:# background:flags.1?true geo_point:InputGeoPoint self_expires:flags.0?int = Updates;\ncontacts.blockFromReplies#29a8962c flags:# delete_message:flags.0?true delete_history:flags.1?true report_spam:flags.2?true msg_id:int = Updates;\ncontacts.resolvePhone#8af94344 phone:string = contacts.ResolvedPeer;\ncontacts.exportContactToken#f8654027 = ExportedContactToken;\ncontacts.importContactToken#13005788 token:string = User;\ncontacts.editCloseFriends#ba6705f0 id:Vector<long> = Bool;\ncontacts.setBlocked#94c65c76 flags:# my_stories_from:flags.0?true id:Vector<InputPeer> limit:int = Bool;\ncontacts.getBirthdays#daeda864 = contacts.ContactBirthdays;\ncontacts.getSponsoredPeers#b6c8c393 q:string = contacts.SponsoredPeers;\ncontacts.updateContactNote#139f63fb id:InputUser note:TextWithEntities = Bool;\n\nmessages.getMessages#63c66506 id:Vector<InputMessage> = messages.Messages;\nmessages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs;\nmessages.getHistory#4423e6c5 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages;\nmessages.search#29ee847a flags:# peer:InputPeer q:string from_id:flags.0?InputPeer saved_peer_id:flags.2?InputPeer saved_reaction:flags.3?Vector<Reaction> top_msg_id:flags.1?int filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages;\nmessages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages;\nmessages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?true peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory;\nmessages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = messages.AffectedMessages;\nmessages.receivedMessages#5a954c0 max_id:int = Vector<ReceivedNotifyMessage>;\nmessages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool;\nmessages.sendMessage#545cd15a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int schedule_repeat_period:flags.24?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost = Updates;\nmessages.sendMedia#330e77f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int schedule_repeat_period:flags.24?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost = Updates;\nmessages.forwardMessages#13704a7c flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int reply_to:flags.22?InputReplyTo schedule_date:flags.10?int schedule_repeat_period:flags.24?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long video_timestamp:flags.20?int allow_paid_stars:flags.21?long suggested_post:flags.23?SuggestedPost = Updates;\nmessages.reportSpam#cf1592db peer:InputPeer = Bool;\nmessages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings;\nmessages.report#fc78af9b peer:InputPeer id:Vector<int> option:bytes message:string = ReportResult;\nmessages.getChats#49e9528f id:Vector<long> = messages.Chats;\nmessages.getFullChat#aeb00b34 chat_id:long = messages.ChatFull;\nmessages.editChatTitle#73783ffd chat_id:long title:string = Updates;\nmessages.editChatPhoto#35ddd674 chat_id:long photo:InputChatPhoto = Updates;\nmessages.addChatUser#cbc6d107 chat_id:long user_id:InputUser fwd_limit:int = messages.InvitedUsers;\nmessages.deleteChatUser#a2185cab flags:# revoke_history:flags.0?true chat_id:long user_id:InputUser = Updates;\nmessages.createChat#92ceddd4 flags:# users:Vector<InputUser> title:string ttl_period:flags.0?int = messages.InvitedUsers;\nmessages.getDhConfig#26cf8950 version:int random_length:int = messages.DhConfig;\nmessages.requestEncryption#f64daf43 user_id:InputUser random_id:int g_a:bytes = EncryptedChat;\nmessages.acceptEncryption#3dbc0415 peer:InputEncryptedChat g_b:bytes key_fingerprint:long = EncryptedChat;\nmessages.discardEncryption#f393aea0 flags:# delete_history:flags.0?true chat_id:int = Bool;\nmessages.setEncryptedTyping#791451ed peer:InputEncryptedChat typing:Bool = Bool;\nmessages.readEncryptedHistory#7f4b690a peer:InputEncryptedChat max_date:int = Bool;\nmessages.sendEncrypted#44fa7a15 flags:# silent:flags.0?true peer:InputEncryptedChat random_id:long data:bytes = messages.SentEncryptedMessage;\nmessages.sendEncryptedFile#5559481d flags:# silent:flags.0?true peer:InputEncryptedChat random_id:long data:bytes file:InputEncryptedFile = messages.SentEncryptedMessage;\nmessages.sendEncryptedService#32d439a4 peer:InputEncryptedChat random_id:long data:bytes = messages.SentEncryptedMessage;\nmessages.receivedQueue#55a5bb66 max_qts:int = Vector<long>;\nmessages.reportEncryptedSpam#4b0c8c0f peer:InputEncryptedChat = Bool;\nmessages.readMessageContents#36a73f77 id:Vector<int> = messages.AffectedMessages;\nmessages.getStickers#d5a5d3a1 emoticon:string hash:long = messages.Stickers;\nmessages.getAllStickers#b8a0a1a8 hash:long = messages.AllStickers;\nmessages.getWebPagePreview#570d6f6f flags:# message:string entities:flags.3?Vector<MessageEntity> = messages.WebPagePreview;\nmessages.exportChatInvite#a455de90 flags:# legacy_revoke_permanent:flags.2?true request_needed:flags.3?true peer:InputPeer expire_date:flags.0?int usage_limit:flags.1?int title:flags.4?string subscription_pricing:flags.5?StarsSubscriptionPricing = ExportedChatInvite;\nmessages.checkChatInvite#3eadb1bb hash:string = ChatInvite;\nmessages.importChatInvite#6c50051c hash:string = Updates;\nmessages.getStickerSet#c8a0ec74 stickerset:InputStickerSet hash:int = messages.StickerSet;\nmessages.installStickerSet#c78fe460 stickerset:InputStickerSet archived:Bool = messages.StickerSetInstallResult;\nmessages.uninstallStickerSet#f96e55de stickerset:InputStickerSet = Bool;\nmessages.startBot#e6df7378 bot:InputUser peer:InputPeer random_id:long start_param:string = Updates;\nmessages.getMessagesViews#5784d3e1 peer:InputPeer id:Vector<int> increment:Bool = messages.MessageViews;\nmessages.editChatAdmin#a85bd1c2 chat_id:long user_id:InputUser is_admin:Bool = Bool;\nmessages.migrateChat#a2875319 chat_id:long = Updates;\nmessages.searchGlobal#4bc6589a flags:# broadcasts_only:flags.1?true groups_only:flags.2?true users_only:flags.3?true folder_id:flags.0?int q:string filter:MessagesFilter min_date:int max_date:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;\nmessages.reorderStickerSets#78337739 flags:# masks:flags.0?true emojis:flags.1?true order:Vector<long> = Bool;\nmessages.getDocumentByHash#b1f2061f sha256:bytes size:long mime_type:string = Document;\nmessages.getSavedGifs#5cf09635 hash:long = messages.SavedGifs;\nmessages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool;\nmessages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults;\nmessages.setInlineBotResults#bb12a419 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector<InputBotInlineResult> cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM switch_webview:flags.4?InlineBotWebView = Bool;\nmessages.sendInlineBotResult#c0cf7646 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to:flags.0?InputReplyTo random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut allow_paid_stars:flags.21?long = Updates;\nmessages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData;\nmessages.editMessage#51e842e1 flags:# no_webpage:flags.1?true invert_media:flags.16?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.15?int schedule_repeat_period:flags.18?int quick_reply_shortcut_id:flags.17?int = Updates;\nmessages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true invert_media:flags.16?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> = Bool;\nmessages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer;\nmessages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool;\nmessages.getPeerDialogs#e470bcfd peers:Vector<InputDialogPeer> = messages.PeerDialogs;\nmessages.saveDraft#54ae308e flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia effect:flags.7?long suggested_post:flags.8?SuggestedPost = Bool;\nmessages.getAllDrafts#6a3f8d65 = Updates;\nmessages.getFeaturedStickers#64780b14 hash:long = messages.FeaturedStickers;\nmessages.readFeaturedStickers#5b118126 id:Vector<long> = Bool;\nmessages.getRecentStickers#9da9403b flags:# attached:flags.0?true hash:long = messages.RecentStickers;\nmessages.saveRecentSticker#392718f8 flags:# attached:flags.0?true id:InputDocument unsave:Bool = Bool;\nmessages.clearRecentStickers#8999602d flags:# attached:flags.0?true = Bool;\nmessages.getArchivedStickers#57f17692 flags:# masks:flags.0?true emojis:flags.1?true offset_id:long limit:int = messages.ArchivedStickers;\nmessages.getMaskStickers#640f82b8 hash:long = messages.AllStickers;\nmessages.getAttachedStickers#cc5b67cc media:InputStickeredMedia = Vector<StickerSetCovered>;\nmessages.setGameScore#8ef8ecc0 flags:# edit_message:flags.0?true force:flags.1?true peer:InputPeer id:int user_id:InputUser score:int = Updates;\nmessages.setInlineGameScore#15ad9f64 flags:# edit_message:flags.0?true force:flags.1?true id:InputBotInlineMessageID user_id:InputUser score:int = Bool;\nmessages.getGameHighScores#e822649d peer:InputPeer id:int user_id:InputUser = messages.HighScores;\nmessages.getInlineGameHighScores#f635e1b id:InputBotInlineMessageID user_id:InputUser = messages.HighScores;\nmessages.getCommonChats#e40ca104 user_id:InputUser max_id:long limit:int = messages.Chats;\nmessages.getWebPage#8d9692a3 url:string hash:int = messages.WebPage;\nmessages.toggleDialogPin#a731e257 flags:# pinned:flags.0?true peer:InputDialogPeer = Bool;\nmessages.reorderPinnedDialogs#3b1adf37 flags:# force:flags.0?true folder_id:int order:Vector<InputDialogPeer> = Bool;\nmessages.getPinnedDialogs#d6b94df2 folder_id:int = messages.PeerDialogs;\nmessages.setBotShippingResults#e5f672fa flags:# query_id:long error:flags.0?string shipping_options:flags.1?Vector<ShippingOption> = Bool;\nmessages.setBotPrecheckoutResults#9c2dd95 flags:# success:flags.1?true query_id:long error:flags.0?string = Bool;\nmessages.uploadMedia#14967978 flags:# business_connection_id:flags.0?string peer:InputPeer media:InputMedia = MessageMedia;\nmessages.sendScreenshotNotification#a1405817 peer:InputPeer reply_to:InputReplyTo random_id:long = Updates;\nmessages.getFavedStickers#4f1aaa9 hash:long = messages.FavedStickers;\nmessages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool;\nmessages.getUnreadMentions#f107e790 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;\nmessages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory;\nmessages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages;\nmessages.sendMultiMedia#1bf89d74 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector<InputSingleMedia> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates;\nmessages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile;\nmessages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets;\nmessages.getSplitRanges#1cff7e08 = Vector<MessageRange>;\nmessages.markDialogUnread#8c5006f8 flags:# unread:flags.0?true parent_peer:flags.1?InputPeer peer:InputDialogPeer = Bool;\nmessages.getDialogUnreadMarks#21202222 flags:# parent_peer:flags.0?InputPeer = Vector<DialogPeer>;\nmessages.clearAllDrafts#7e58ee9c = Bool;\nmessages.updatePinnedMessage#d2aaf7ec flags:# silent:flags.0?true unpin:flags.1?true pm_oneside:flags.2?true peer:InputPeer id:int = Updates;\nmessages.sendVote#10ea6184 peer:InputPeer msg_id:int options:Vector<bytes> = Updates;\nmessages.getPollResults#eda3e33b peer:InputPeer msg_id:int poll_hash:long = Updates;\nmessages.getOnlines#6e2be050 peer:InputPeer = ChatOnlines;\nmessages.editChatAbout#def60797 peer:InputPeer about:string = Bool;\nmessages.editChatDefaultBannedRights#a5866b41 peer:InputPeer banned_rights:ChatBannedRights = Updates;\nmessages.getEmojiKeywords#35a0e062 lang_code:string = EmojiKeywordsDifference;\nmessages.getEmojiKeywordsDifference#1508b6af lang_code:string from_version:int = EmojiKeywordsDifference;\nmessages.getEmojiKeywordsLanguages#4e9963b2 lang_codes:Vector<string> = Vector<EmojiLanguage>;\nmessages.getEmojiURL#d5b10c26 lang_code:string = EmojiURL;\nmessages.getSearchCounters#1bbcf300 flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer top_msg_id:flags.0?int filters:Vector<MessagesFilter> = Vector<messages.SearchCounter>;\nmessages.requestUrlAuth#894cc99c flags:# peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string in_app_origin:flags.3?string = UrlAuthResult;\nmessages.acceptUrlAuth#67a3f0de 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 match_code:flags.4?string = UrlAuthResult;\nmessages.hidePeerSettingsBar#4facb138 peer:InputPeer = Bool;\nmessages.getScheduledHistory#f516760b peer:InputPeer hash:long = messages.Messages;\nmessages.getScheduledMessages#bdbb0464 peer:InputPeer id:Vector<int> = messages.Messages;\nmessages.sendScheduledMessages#bd38850a peer:InputPeer id:Vector<int> = Updates;\nmessages.deleteScheduledMessages#59ae2b16 peer:InputPeer id:Vector<int> = Updates;\nmessages.getPollVotes#b86e380e flags:# peer:InputPeer id:int option:flags.0?bytes offset:flags.1?string limit:int = messages.VotesList;\nmessages.toggleStickerSets#b5052fea flags:# uninstall:flags.0?true archive:flags.1?true unarchive:flags.2?true stickersets:Vector<InputStickerSet> = Bool;\nmessages.getDialogFilters#efd48c89 = messages.DialogFilters;\nmessages.getSuggestedDialogFilters#a29cd42c = Vector<DialogFilterSuggested>;\nmessages.updateDialogFilter#1ad4a04a flags:# id:int filter:flags.0?DialogFilter = Bool;\nmessages.updateDialogFiltersOrder#c563c1e4 order:Vector<int> = Bool;\nmessages.getOldFeaturedStickers#7ed094a1 offset:int limit:int hash:long = messages.FeaturedStickers;\nmessages.getReplies#22ddd30c peer:InputPeer msg_id:int offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages;\nmessages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage;\nmessages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool;\nmessages.unpinAllMessages#62dd747 flags:# peer:InputPeer top_msg_id:flags.0?int saved_peer_id:flags.1?InputPeer = messages.AffectedHistory;\nmessages.deleteChat#5bd0ee50 chat_id:long = Bool;\nmessages.deletePhoneCallHistory#f9cbe409 flags:# revoke:flags.0?true = messages.AffectedFoundMessages;\nmessages.checkHistoryImport#43fe19f3 import_head:string = messages.HistoryImportParsed;\nmessages.initHistoryImport#34090c3b peer:InputPeer file:InputFile media_count:int = messages.HistoryImport;\nmessages.uploadImportedMedia#2a862092 peer:InputPeer import_id:long file_name:string media:InputMedia = MessageMedia;\nmessages.startHistoryImport#b43df344 peer:InputPeer import_id:long = Bool;\nmessages.getExportedChatInvites#a2b5a3f6 flags:# revoked:flags.3?true peer:InputPeer admin_id:InputUser offset_date:flags.2?int offset_link:flags.2?string limit:int = messages.ExportedChatInvites;\nmessages.getExportedChatInvite#73746f5c peer:InputPeer link:string = messages.ExportedChatInvite;\nmessages.editExportedChatInvite#bdca2f75 flags:# revoked:flags.2?true peer:InputPeer link:string expire_date:flags.0?int usage_limit:flags.1?int request_needed:flags.3?Bool title:flags.4?string = messages.ExportedChatInvite;\nmessages.deleteRevokedExportedChatInvites#56987bd5 peer:InputPeer admin_id:InputUser = Bool;\nmessages.deleteExportedChatInvite#d464a42b peer:InputPeer link:string = Bool;\nmessages.getAdminsWithInvites#3920e6ef peer:InputPeer = messages.ChatAdminsWithInvites;\nmessages.getChatInviteImporters#df04dd4e flags:# requested:flags.0?true subscription_expired:flags.3?true peer:InputPeer link:flags.1?string q:flags.2?string offset_date:int offset_user:InputUser limit:int = messages.ChatInviteImporters;\nmessages.setHistoryTTL#b80e5fe4 peer:InputPeer period:int = Updates;\nmessages.checkHistoryImportPeer#5dc60f03 peer:InputPeer = messages.CheckedHistoryImportPeer;\nmessages.setChatTheme#81202c9 peer:InputPeer theme:InputChatTheme = Updates;\nmessages.getMessageReadParticipants#31c1c44f peer:InputPeer msg_id:int = Vector<ReadParticipantDate>;\nmessages.getSearchResultsCalendar#6aa3f6bd flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer filter:MessagesFilter offset_id:int offset_date:int = messages.SearchResultsCalendar;\nmessages.getSearchResultsPositions#9c7f2f10 flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer filter:MessagesFilter offset_id:int limit:int = messages.SearchResultsPositions;\nmessages.hideChatJoinRequest#7fe7e815 flags:# approved:flags.0?true peer:InputPeer user_id:InputUser = Updates;\nmessages.hideAllChatJoinRequests#e085f4ea flags:# approved:flags.0?true peer:InputPeer link:flags.1?string = Updates;\nmessages.toggleNoForwards#b2081a35 flags:# peer:InputPeer enabled:Bool request_msg_id:flags.0?int = Updates;\nmessages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool;\nmessages.sendReaction#d30d78d4 flags:# big:flags.1?true add_to_recent:flags.2?true peer:InputPeer msg_id:int reaction:flags.0?Vector<Reaction> = Updates;\nmessages.getMessagesReactions#8bba90e6 peer:InputPeer id:Vector<int> = Updates;\nmessages.getMessageReactionsList#461b3f48 flags:# peer:InputPeer id:int reaction:flags.0?Reaction offset:flags.1?string limit:int = messages.MessageReactionsList;\nmessages.setChatAvailableReactions#864b2581 flags:# peer:InputPeer available_reactions:ChatReactions reactions_limit:flags.0?int paid_enabled:flags.1?Bool = Updates;\nmessages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions;\nmessages.setDefaultReaction#4f47a016 reaction:Reaction = Bool;\nmessages.translateText#a5eec345 flags:# peer:flags.0?InputPeer id:flags.0?Vector<int> text:flags.1?Vector<TextWithEntities> to_lang:string tone:flags.2?string = messages.TranslatedText;\nmessages.getUnreadReactions#bd7f90ac flags:# peer:InputPeer top_msg_id:flags.0?int saved_peer_id:flags.1?InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;\nmessages.readReactions#9ec44f93 flags:# peer:InputPeer top_msg_id:flags.0?int saved_peer_id:flags.1?InputPeer = messages.AffectedHistory;\nmessages.searchSentMedia#107e31a0 q:string filter:MessagesFilter limit:int = messages.Messages;\nmessages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots;\nmessages.getAttachMenuBot#77216192 bot:InputUser = AttachMenuBotsBot;\nmessages.toggleBotInAttachMenu#69f59d69 flags:# write_allowed:flags.0?true bot:InputUser enabled:Bool = Bool;\nmessages.requestWebView#269dc2c1 flags:# from_bot_menu:flags.4?true silent:flags.5?true compact:flags.7?true fullscreen:flags.8?true peer:InputPeer bot:InputUser url:flags.1?string start_param:flags.3?string theme_params:flags.2?DataJSON platform:string reply_to:flags.0?InputReplyTo send_as:flags.13?InputPeer = WebViewResult;\nmessages.prolongWebView#b0d81a83 flags:# silent:flags.5?true peer:InputPeer bot:InputUser query_id:long reply_to:flags.0?InputReplyTo send_as:flags.13?InputPeer = Bool;\nmessages.requestSimpleWebView#413a3e73 flags:# from_switch_webview:flags.1?true from_side_menu:flags.2?true compact:flags.7?true fullscreen:flags.8?true bot:InputUser url:flags.3?string start_param:flags.4?string theme_params:flags.0?DataJSON platform:string = WebViewResult;\nmessages.sendWebViewResultMessage#a4314f5 bot_query_id:string result:InputBotInlineResult = WebViewMessageSent;\nmessages.sendWebViewData#dc0242c8 bot:InputUser random_id:long button_text:string data:string = Updates;\nmessages.transcribeAudio#269e9a49 peer:InputPeer msg_id:int = messages.TranscribedAudio;\nmessages.rateTranscribedAudio#7f1d072f peer:InputPeer msg_id:int transcription_id:long good:Bool = Bool;\nmessages.getCustomEmojiDocuments#d9ab0f54 document_id:Vector<long> = Vector<Document>;\nmessages.getEmojiStickers#fbfca18f hash:long = messages.AllStickers;\nmessages.getFeaturedEmojiStickers#ecf6736 hash:long = messages.FeaturedStickers;\nmessages.reportReaction#3f64c076 peer:InputPeer id:int reaction_peer:InputPeer = Bool;\nmessages.getTopReactions#bb8125ba limit:int hash:long = messages.Reactions;\nmessages.getRecentReactions#39461db2 limit:int hash:long = messages.Reactions;\nmessages.clearRecentReactions#9dfeefb4 = Bool;\nmessages.getExtendedMedia#84f80814 peer:InputPeer id:Vector<int> = Updates;\nmessages.setDefaultHistoryTTL#9eb51445 period:int = Bool;\nmessages.getDefaultHistoryTTL#658b7188 = DefaultHistoryTTL;\nmessages.sendBotRequestedPeer#6c5cf2a7 flags:# peer:InputPeer msg_id:flags.0?int webapp_req_id:flags.1?string button_id:int requested_peers:Vector<InputPeer> = Updates;\nmessages.getEmojiGroups#7488ce5b hash:int = messages.EmojiGroups;\nmessages.getEmojiStatusGroups#2ecd56cd hash:int = messages.EmojiGroups;\nmessages.getEmojiProfilePhotoGroups#21a548f3 hash:int = messages.EmojiGroups;\nmessages.searchCustomEmoji#2c11c0d7 emoticon:string hash:long = EmojiList;\nmessages.togglePeerTranslations#e47cb579 flags:# disabled:flags.0?true peer:InputPeer = Bool;\nmessages.getBotApp#34fdc5c3 app:InputBotApp hash:long = messages.BotApp;\nmessages.requestAppWebView#53618bce flags:# write_allowed:flags.0?true compact:flags.7?true fullscreen:flags.8?true peer:InputPeer app:InputBotApp start_param:flags.1?string theme_params:flags.2?DataJSON platform:string = WebViewResult;\nmessages.setChatWallPaper#8ffacae1 flags:# for_both:flags.3?true revert:flags.4?true peer:InputPeer wallpaper:flags.0?InputWallPaper settings:flags.2?WallPaperSettings id:flags.1?int = Updates;\nmessages.searchEmojiStickerSets#92b4494c flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets;\nmessages.getSavedDialogs#1e91fc99 flags:# exclude_pinned:flags.0?true parent_peer:flags.1?InputPeer offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.SavedDialogs;\nmessages.getSavedHistory#998ab009 flags:# parent_peer:flags.0?InputPeer peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages;\nmessages.deleteSavedHistory#4dc5085f flags:# parent_peer:flags.0?InputPeer peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory;\nmessages.getPinnedSavedDialogs#d63d94e0 = messages.SavedDialogs;\nmessages.toggleSavedDialogPin#ac81bbde flags:# pinned:flags.0?true peer:InputDialogPeer = Bool;\nmessages.reorderPinnedSavedDialogs#8b716587 flags:# force:flags.0?true order:Vector<InputDialogPeer> = Bool;\nmessages.getSavedReactionTags#3637e05b flags:# peer:flags.0?InputPeer hash:long = messages.SavedReactionTags;\nmessages.updateSavedReactionTag#60297dec flags:# reaction:Reaction title:flags.0?string = Bool;\nmessages.getDefaultTagReactions#bdf93428 hash:long = messages.Reactions;\nmessages.getOutboxReadDate#8c4bfe5d peer:InputPeer msg_id:int = OutboxReadDate;\nmessages.getQuickReplies#d483f2a8 hash:long = messages.QuickReplies;\nmessages.reorderQuickReplies#60331907 order:Vector<int> = Bool;\nmessages.checkQuickReplyShortcut#f1d0fbd3 shortcut:string = Bool;\nmessages.editQuickReplyShortcut#5c003cef shortcut_id:int shortcut:string = Bool;\nmessages.deleteQuickReplyShortcut#3cc04740 shortcut_id:int = Bool;\nmessages.getQuickReplyMessages#94a495c3 flags:# shortcut_id:int id:flags.0?Vector<int> hash:long = messages.Messages;\nmessages.sendQuickReplyMessages#6c750de1 peer:InputPeer shortcut_id:int id:Vector<int> random_id:Vector<long> = Updates;\nmessages.deleteQuickReplyMessages#e105e910 shortcut_id:int id:Vector<int> = Updates;\nmessages.toggleDialogFilterTags#fd2dda49 enabled:Bool = Bool;\nmessages.getMyStickers#d0b5e1fc offset_id:long limit:int = messages.MyStickers;\nmessages.getEmojiStickerGroups#1dd840f5 hash:int = messages.EmojiGroups;\nmessages.getAvailableEffects#dea20a39 hash:int = messages.AvailableEffects;\nmessages.editFactCheck#589ee75 peer:InputPeer msg_id:int text:TextWithEntities = Updates;\nmessages.deleteFactCheck#d1da940c peer:InputPeer msg_id:int = Updates;\nmessages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector<int> = Vector<FactCheck>;\nmessages.requestMainWebView#c9e01e7b flags:# compact:flags.7?true fullscreen:flags.8?true peer:InputPeer bot:InputUser start_param:flags.1?string theme_params:flags.0?DataJSON platform:string = WebViewResult;\nmessages.sendPaidReaction#58bbcb50 flags:# peer:InputPeer msg_id:int count:int random_id:long private:flags.0?PaidReactionPrivacy = Updates;\nmessages.togglePaidReactionPrivacy#435885b5 peer:InputPeer msg_id:int private:PaidReactionPrivacy = Bool;\nmessages.getPaidReactionPrivacy#472455aa = Updates;\nmessages.viewSponsoredMessage#269e3643 random_id:bytes = Bool;\nmessages.clickSponsoredMessage#8235057e flags:# media:flags.0?true fullscreen:flags.1?true random_id:bytes = Bool;\nmessages.reportSponsoredMessage#12cbf0c4 random_id:bytes option:bytes = channels.SponsoredMessageReportResult;\nmessages.getSponsoredMessages#3d6ce850 flags:# peer:InputPeer msg_id:flags.0?int = messages.SponsoredMessages;\nmessages.savePreparedInlineMessage#f21f7f2f flags:# result:InputBotInlineResult user_id:InputUser peer_types:flags.0?Vector<InlineQueryPeerType> = messages.BotPreparedInlineMessage;\nmessages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.PreparedInlineMessage;\nmessages.searchStickers#29b1c66a flags:# emojis:flags.0?true q:string emoticon:string lang_code:Vector<string> offset:int limit:int hash:long = messages.FoundStickers;\nmessages.reportMessagesDelivery#5a6d7395 flags:# push:flags.0?true peer:InputPeer id:Vector<int> = Bool;\nmessages.getSavedDialogsByID#6f6f9c96 flags:# parent_peer:flags.1?InputPeer ids:Vector<InputPeer> = messages.SavedDialogs;\nmessages.readSavedHistory#ba4a3b5b parent_peer:InputPeer peer:InputPeer max_id:int = Bool;\nmessages.toggleTodoCompleted#d3e03124 peer:InputPeer msg_id:int completed:Vector<int> incompleted:Vector<int> = Updates;\nmessages.appendTodoList#21a61057 peer:InputPeer msg_id:int list:Vector<TodoItem> = Updates;\nmessages.toggleSuggestedPostApproval#8107455c flags:# reject:flags.1?true peer:InputPeer msg_id:int schedule_date:flags.0?int reject_comment:flags.2?string = Updates;\nmessages.getForumTopics#3ba47bff flags:# peer:InputPeer q:flags.0?string offset_date:int offset_id:int offset_topic:int limit:int = messages.ForumTopics;\nmessages.getForumTopicsByID#af0a4a08 peer:InputPeer topics:Vector<int> = messages.ForumTopics;\nmessages.editForumTopic#cecc1134 flags:# peer:InputPeer topic_id:int title:flags.0?string icon_emoji_id:flags.1?long closed:flags.2?Bool hidden:flags.3?Bool = Updates;\nmessages.updatePinnedForumTopic#175df251 peer:InputPeer topic_id:int pinned:Bool = Updates;\nmessages.reorderPinnedForumTopics#e7841f0 flags:# force:flags.0?true peer:InputPeer order:Vector<int> = Updates;\nmessages.createForumTopic#2f98c3d5 flags:# title_missing:flags.4?true peer:InputPeer title:string icon_color:flags.0?int icon_emoji_id:flags.3?long random_id:long send_as:flags.2?InputPeer = Updates;\nmessages.deleteTopicHistory#d2816f10 peer:InputPeer top_msg_id:int = messages.AffectedHistory;\nmessages.getEmojiGameInfo#fb7e8ca7 = messages.EmojiGameInfo;\nmessages.summarizeText#abbbd346 flags:# peer:InputPeer id:int to_lang:flags.0?string tone:flags.2?string = TextWithEntities;\nmessages.editChatCreator#f743b857 peer:InputPeer user_id:InputUser password:InputCheckPasswordSRP = Updates;\nmessages.getFutureChatCreatorAfterLeave#3b7d0ea6 peer:InputPeer = User;\nmessages.editChatParticipantRank#a00f32b0 peer:InputPeer participant:InputPeer rank:string = Updates;\nmessages.declineUrlAuth#35436bbc url:string = Bool;\nmessages.checkUrlAuthMatchCode#c9a47b0b url:string match_code:string = Bool;\nmessages.composeMessageWithAI#fd426afe flags:# proofread:flags.0?true emojify:flags.3?true text:TextWithEntities translate_to_lang:flags.1?string change_tone:flags.2?string = messages.ComposedMessageWithAI;\nmessages.reportReadMetrics#4067c5e6 peer:InputPeer metrics:Vector<InputMessageReadMetric> = Bool;\nmessages.reportMusicListen#ddbcd819 id:InputDocument listened_duration:int = Bool;\nmessages.addPollAnswer#19bc4b6d peer:InputPeer msg_id:int answer:PollAnswer = Updates;\nmessages.deletePollAnswer#ac8505a5 peer:InputPeer msg_id:int option:bytes = Updates;\nmessages.getUnreadPollVotes#43286cf2 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;\nmessages.readPollVotes#1720b4d8 flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory;\n\nupdates.getState#edd4882a = updates.State;\nupdates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference;\nupdates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;\n\nphotos.updateProfilePhoto#9e82039 flags:# fallback:flags.0?true bot:flags.1?InputUser id:InputPhoto = photos.Photo;\nphotos.uploadProfilePhoto#388a3b5 flags:# fallback:flags.3?true bot:flags.5?InputUser file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.4?VideoSize = photos.Photo;\nphotos.deletePhotos#87cf7f2f id:Vector<InputPhoto> = Vector<long>;\nphotos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos;\nphotos.uploadContactProfilePhoto#e14c4a71 flags:# suggest:flags.3?true save:flags.4?true user_id:InputUser file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.5?VideoSize = photos.Photo;\n\nupload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool;\nupload.getFile#be5335be flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:long limit:int = upload.File;\nupload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool;\nupload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile;\nupload.getCdnFile#395f69da file_token:bytes offset:long limit:int = upload.CdnFile;\nupload.reuploadCdnFile#9b2754a8 file_token:bytes request_token:bytes = Vector<FileHash>;\nupload.getCdnFileHashes#91dc3f31 file_token:bytes offset:long = Vector<FileHash>;\nupload.getFileHashes#9156982a location:InputFileLocation offset:long = Vector<FileHash>;\n\nhelp.getConfig#c4f9186b = Config;\nhelp.getNearestDc#1fb33026 = NearestDc;\nhelp.getAppUpdate#522d5a7d source:string = help.AppUpdate;\nhelp.getInviteText#4d392343 = help.InviteText;\nhelp.getSupport#9cdf08cd = help.Support;\nhelp.setBotUpdatesStatus#ec22cfcd pending_updates_count:int message:string = Bool;\nhelp.getCdnConfig#52029342 = CdnConfig;\nhelp.getRecentMeUrls#3dc0f114 referer:string = help.RecentMeUrls;\nhelp.getTermsOfServiceUpdate#2ca51fd1 = help.TermsOfServiceUpdate;\nhelp.acceptTermsOfService#ee72f79a id:DataJSON = Bool;\nhelp.getDeepLinkInfo#3fedc75f path:string = help.DeepLinkInfo;\nhelp.getAppConfig#61e3f854 hash:int = help.AppConfig;\nhelp.saveAppLog#6f02f748 events:Vector<InputAppEvent> = Bool;\nhelp.getPassportConfig#c661ad08 hash:int = help.PassportConfig;\nhelp.getSupportName#d360e72c = help.SupportName;\nhelp.getUserInfo#38a08d3 user_id:InputUser = help.UserInfo;\nhelp.editUserInfo#66b91b70 user_id:InputUser message:string entities:Vector<MessageEntity> = help.UserInfo;\nhelp.getPromoData#c0977421 = help.PromoData;\nhelp.hidePromoData#1e251c95 peer:InputPeer = Bool;\nhelp.dismissSuggestion#f50dbaa1 peer:InputPeer suggestion:string = Bool;\nhelp.getCountriesList#735787a8 lang_code:string hash:int = help.CountriesList;\nhelp.getPremiumPromo#b81b93d4 = help.PremiumPromo;\nhelp.getPeerColors#da80f42f hash:int = help.PeerColors;\nhelp.getPeerProfileColors#abcfa9fd hash:int = help.PeerColors;\nhelp.getTimezonesList#49b30240 hash:int = help.TimezonesList;\n\nchannels.readHistory#cc104937 channel:InputChannel max_id:int = Bool;\nchannels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages;\nchannels.reportSpam#f44a8315 channel:InputChannel participant:InputPeer id:Vector<int> = Bool;\nchannels.getMessages#ad8c9a23 channel:InputChannel id:Vector<InputMessage> = messages.Messages;\nchannels.getParticipants#77ced9d0 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:long = channels.ChannelParticipants;\nchannels.getParticipant#a0ab6cc6 channel:InputChannel participant:InputPeer = channels.ChannelParticipant;\nchannels.getChannels#a7f6bbb id:Vector<InputChannel> = messages.Chats;\nchannels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull;\nchannels.createChannel#91006707 flags:# broadcast:flags.0?true megagroup:flags.1?true for_import:flags.3?true forum:flags.5?true title:string about:string geo_point:flags.2?InputGeoPoint address:flags.2?string ttl_period:flags.4?int = Updates;\nchannels.editAdmin#9a98ad68 flags:# channel:InputChannel user_id:InputUser admin_rights:ChatAdminRights rank:flags.0?string = Updates;\nchannels.editTitle#566decd0 channel:InputChannel title:string = Updates;\nchannels.editPhoto#f12e57c9 channel:InputChannel photo:InputChatPhoto = Updates;\nchannels.checkUsername#10e6bd2c channel:InputChannel username:string = Bool;\nchannels.updateUsername#3514b3de channel:InputChannel username:string = Bool;\nchannels.joinChannel#24b524c5 channel:InputChannel = Updates;\nchannels.leaveChannel#f836aa95 channel:InputChannel = Updates;\nchannels.inviteToChannel#c9e33d54 channel:InputChannel users:Vector<InputUser> = messages.InvitedUsers;\nchannels.deleteChannel#c0111fe3 channel:InputChannel = Updates;\nchannels.exportMessageLink#e63fadeb flags:# grouped:flags.0?true thread:flags.1?true channel:InputChannel id:int = ExportedMessageLink;\nchannels.toggleSignatures#418d549c flags:# signatures_enabled:flags.0?true profiles_enabled:flags.1?true channel:InputChannel = Updates;\nchannels.getAdminedPublicChannels#f8b036af flags:# by_location:flags.0?true check_limit:flags.1?true for_personal:flags.2?true = messages.Chats;\nchannels.editBanned#96e6cd81 channel:InputChannel participant:InputPeer banned_rights:ChatBannedRights = Updates;\nchannels.getAdminLog#33ddf480 flags:# channel:InputChannel q:string events_filter:flags.0?ChannelAdminLogEventsFilter admins:flags.1?Vector<InputUser> max_id:long min_id:long limit:int = channels.AdminLogResults;\nchannels.setStickers#ea8ca4f9 channel:InputChannel stickerset:InputStickerSet = Bool;\nchannels.readMessageContents#eab5dc38 channel:InputChannel id:Vector<int> = Bool;\nchannels.deleteHistory#9baa9647 flags:# for_everyone:flags.0?true channel:InputChannel max_id:int = Updates;\nchannels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates;\nchannels.getLeftChannels#8341ecc0 offset:int = messages.Chats;\nchannels.getGroupsForDiscussion#f5dad378 = messages.Chats;\nchannels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool;\nchannels.editLocation#58e63f6d channel:InputChannel geo_point:InputGeoPoint address:string = Bool;\nchannels.toggleSlowMode#edd49ef0 channel:InputChannel seconds:int = Updates;\nchannels.getInactiveChannels#11e831ee = messages.InactiveChats;\nchannels.convertToGigagroup#b290c69 channel:InputChannel = Updates;\nchannels.getSendAs#e785a43f flags:# for_paid_reactions:flags.0?true for_live_stories:flags.1?true peer:InputPeer = channels.SendAsPeers;\nchannels.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory;\nchannels.toggleJoinToSend#e4cb9580 channel:InputChannel enabled:Bool = Updates;\nchannels.toggleJoinRequest#4c2985b6 channel:InputChannel enabled:Bool = Updates;\nchannels.reorderUsernames#b45ced1d channel:InputChannel order:Vector<string> = Bool;\nchannels.toggleUsername#50f24105 channel:InputChannel username:string active:Bool = Bool;\nchannels.deactivateAllUsernames#a245dd3 channel:InputChannel = Bool;\nchannels.toggleForum#3ff75734 channel:InputChannel enabled:Bool tabs:Bool = Updates;\nchannels.toggleAntiSpam#68f3e4eb channel:InputChannel enabled:Bool = Updates;\nchannels.reportAntiSpamFalsePositive#a850a693 channel:InputChannel msg_id:int = Bool;\nchannels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = Updates;\nchannels.updateColor#d8aa3671 flags:# for_profile:flags.1?true channel:InputChannel color:flags.2?int background_emoji_id:flags.0?long = Updates;\nchannels.toggleViewForumAsMessages#9738bb15 channel:InputChannel enabled:Bool = Updates;\nchannels.getChannelRecommendations#25a71742 flags:# channel:flags.0?InputChannel = messages.Chats;\nchannels.updateEmojiStatus#f0d3e6a8 channel:InputChannel emoji_status:EmojiStatus = Updates;\nchannels.setBoostsToUnblockRestrictions#ad399cee channel:InputChannel boosts:int = Updates;\nchannels.setEmojiStickers#3cd930b7 channel:InputChannel stickerset:InputStickerSet = Bool;\nchannels.restrictSponsoredMessages#9ae91519 channel:InputChannel restricted:Bool = Updates;\nchannels.searchPosts#f2c4f24d flags:# hashtag:flags.0?string query:flags.1?string offset_rate:int offset_peer:InputPeer offset_id:int limit:int allow_paid_stars:flags.2?long = messages.Messages;\nchannels.updatePaidMessagesPrice#4b12327b flags:# broadcast_messages_allowed:flags.0?true channel:InputChannel send_paid_messages_stars:long = Updates;\nchannels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Updates;\nchannels.getMessageAuthor#ece2a0e6 channel:InputChannel id:int = User;\nchannels.checkSearchPostsFlood#22567115 flags:# query:flags.0?string = SearchPostsFlood;\nchannels.setMainProfileTab#3583fcb1 channel:InputChannel tab:ProfileTab = Bool;\n\nbots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON;\nbots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool;\nbots.setBotCommands#517165a scope:BotCommandScope lang_code:string commands:Vector<BotCommand> = Bool;\nbots.resetBotCommands#3d8de0f9 scope:BotCommandScope lang_code:string = Bool;\nbots.getBotCommands#e34c0dd6 scope:BotCommandScope lang_code:string = Vector<BotCommand>;\nbots.setBotMenuButton#4504d54f user_id:InputUser button:BotMenuButton = Bool;\nbots.getBotMenuButton#9c60eb28 user_id:InputUser = BotMenuButton;\nbots.setBotBroadcastDefaultAdminRights#788464e1 admin_rights:ChatAdminRights = Bool;\nbots.setBotGroupDefaultAdminRights#925ec9ea admin_rights:ChatAdminRights = Bool;\nbots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool;\nbots.getBotInfo#dcd914fd flags:# bot:flags.0?InputUser lang_code:string = bots.BotInfo;\nbots.reorderUsernames#9709b1c2 bot:InputUser order:Vector<string> = Bool;\nbots.toggleUsername#53ca973 bot:InputUser username:string active:Bool = Bool;\nbots.canSendMessage#1359f4e6 bot:InputUser = Bool;\nbots.allowSendMessage#f132e3ef bot:InputUser = Updates;\nbots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON;\nbots.getPopularAppBots#c2510192 offset:string limit:int = bots.PopularAppBots;\nbots.addPreviewMedia#17aeb75a bot:InputUser lang_code:string media:InputMedia = BotPreviewMedia;\nbots.editPreviewMedia#8525606f bot:InputUser lang_code:string media:InputMedia new_media:InputMedia = BotPreviewMedia;\nbots.deletePreviewMedia#2d0135b3 bot:InputUser lang_code:string media:Vector<InputMedia> = Bool;\nbots.reorderPreviewMedias#b627f3aa bot:InputUser lang_code:string order:Vector<InputMedia> = Bool;\nbots.getPreviewInfo#423ab3ad bot:InputUser lang_code:string = bots.PreviewInfo;\nbots.getPreviewMedias#a2a5594d bot:InputUser = Vector<BotPreviewMedia>;\nbots.updateUserEmojiStatus#ed9f30c5 user_id:InputUser emoji_status:EmojiStatus = Bool;\nbots.toggleUserEmojiStatusPermission#6de6392 bot:InputUser enabled:Bool = Bool;\nbots.checkDownloadFileParams#50077589 bot:InputUser file_name:string url:string = Bool;\nbots.getAdminedBots#b0711d83 = Vector<User>;\nbots.updateStarRefProgram#778b5ab3 flags:# bot:InputUser commission_permille:int duration_months:flags.0?int = StarRefProgram;\nbots.setCustomVerification#8b89dfbd flags:# enabled:flags.1?true bot:flags.0?InputUser peer:InputPeer custom_description:flags.2?string = Bool;\nbots.getBotRecommendations#a1b70815 bot:InputUser = users.Users;\nbots.checkUsername#87f2219b username:string = Bool;\nbots.createBot#e5b17f2b flags:# via_deeplink:flags.0?true name:string username:string manager_id:InputUser = User;\nbots.exportBotToken#bd0d99eb bot:InputUser revoke:Bool = bots.ExportedBotToken;\nbots.requestWebViewButton#31a2a35e user_id:InputUser button:KeyboardButton = bots.RequestedButton;\nbots.getRequestedWebViewButton#bf25b7f3 bot:InputUser webapp_req_id:string = KeyboardButton;\n\npayments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm;\npayments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt;\npayments.validateRequestedInfo#b6c8f12b flags:# save:flags.0?true invoice:InputInvoice info:PaymentRequestedInfo = payments.ValidatedRequestedInfo;\npayments.sendPaymentForm#2d03522f flags:# form_id:long invoice:InputInvoice requested_info_id:flags.0?string shipping_option_id:flags.1?string credentials:InputPaymentCredentials tip_amount:flags.2?long = payments.PaymentResult;\npayments.getSavedInfo#227d824b = payments.SavedInfo;\npayments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool;\npayments.getBankCardData#2e79d779 number:string = payments.BankCardData;\npayments.exportInvoice#f91b065 invoice_media:InputMedia = payments.ExportedInvoice;\npayments.assignAppStoreTransaction#80ed747d receipt:bytes purpose:InputStorePaymentPurpose = Updates;\npayments.assignPlayMarketTransaction#dffd50d3 receipt:DataJSON purpose:InputStorePaymentPurpose = Updates;\npayments.getPremiumGiftCodeOptions#2757ba54 flags:# boost_peer:flags.0?InputPeer = Vector<PremiumGiftCodeOption>;\npayments.checkGiftCode#8e51b4c1 slug:string = payments.CheckedGiftCode;\npayments.applyGiftCode#f6e26854 slug:string = Updates;\npayments.getGiveawayInfo#f4239425 peer:InputPeer msg_id:int = payments.GiveawayInfo;\npayments.launchPrepaidGiveaway#5ff58f20 peer:InputPeer giveaway_id:long purpose:InputStorePaymentPurpose = Updates;\npayments.getStarsTopupOptions#c00ec7d3 = Vector<StarsTopupOption>;\npayments.getStarsStatus#4ea9b3bf flags:# ton:flags.0?true peer:InputPeer = payments.StarsStatus;\npayments.getStarsTransactions#69da4557 flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true ton:flags.4?true subscription_id:flags.3?string peer:InputPeer offset:string limit:int = payments.StarsStatus;\npayments.sendStarsForm#7998c914 form_id:long invoice:InputInvoice = payments.PaymentResult;\npayments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates;\npayments.getStarsRevenueStats#d91ffad6 flags:# dark:flags.0?true ton:flags.1?true peer:InputPeer = payments.StarsRevenueStats;\npayments.getStarsRevenueWithdrawalUrl#2433dc92 flags:# ton:flags.0?true peer:InputPeer amount:flags.1?long password:InputCheckPasswordSRP = payments.StarsRevenueWithdrawalUrl;\npayments.getStarsRevenueAdsAccountUrl#d1d7efc5 peer:InputPeer = payments.StarsRevenueAdsAccountUrl;\npayments.getStarsTransactionsByID#2dca16b8 flags:# ton:flags.0?true peer:InputPeer id:Vector<InputStarsTransaction> = payments.StarsStatus;\npayments.getStarsGiftOptions#d3c96bc8 flags:# user_id:flags.0?InputUser = Vector<StarsGiftOption>;\npayments.getStarsSubscriptions#32512c5 flags:# missing_balance:flags.0?true peer:InputPeer offset:string = payments.StarsStatus;\npayments.changeStarsSubscription#c7770878 flags:# peer:InputPeer subscription_id:string canceled:flags.0?Bool = Bool;\npayments.fulfillStarsSubscription#cc5bebb3 peer:InputPeer subscription_id:string = Bool;\npayments.getStarsGiveawayOptions#bd1efd3e = Vector<StarsGiveawayOption>;\npayments.getStarGifts#c4563590 hash:int = payments.StarGifts;\npayments.saveStarGift#2a2a697c flags:# unsave:flags.0?true stargift:InputSavedStarGift = Bool;\npayments.convertStarGift#74bf076b stargift:InputSavedStarGift = Bool;\npayments.botCancelStarsSubscription#6dfa0622 flags:# restore:flags.0?true user_id:InputUser charge_id:string = Bool;\npayments.getConnectedStarRefBots#5869a553 flags:# peer:InputPeer offset_date:flags.2?int offset_link:flags.2?string limit:int = payments.ConnectedStarRefBots;\npayments.getConnectedStarRefBot#b7d998f0 peer:InputPeer bot:InputUser = payments.ConnectedStarRefBots;\npayments.getSuggestedStarRefBots#d6b48f7 flags:# order_by_revenue:flags.0?true order_by_date:flags.1?true peer:InputPeer offset:string limit:int = payments.SuggestedStarRefBots;\npayments.connectStarRefBot#7ed5348a peer:InputPeer bot:InputUser = payments.ConnectedStarRefBots;\npayments.editConnectedStarRefBot#e4fca4a3 flags:# revoked:flags.0?true peer:InputPeer link:string = payments.ConnectedStarRefBots;\npayments.getStarGiftUpgradePreview#9c9abcb1 gift_id:long = payments.StarGiftUpgradePreview;\npayments.upgradeStarGift#aed6e4f5 flags:# keep_original_details:flags.0?true stargift:InputSavedStarGift = Updates;\npayments.transferStarGift#7f18176a stargift:InputSavedStarGift to_id:InputPeer = Updates;\npayments.getUniqueStarGift#a1974d72 slug:string = payments.UniqueStarGift;\npayments.getSavedStarGifts#a319e569 flags:# exclude_unsaved:flags.0?true exclude_saved:flags.1?true exclude_unlimited:flags.2?true exclude_unique:flags.4?true sort_by_value:flags.5?true exclude_upgradable:flags.7?true exclude_unupgradable:flags.8?true peer_color_available:flags.9?true exclude_hosted:flags.10?true peer:InputPeer collection_id:flags.6?int offset:string limit:int = payments.SavedStarGifts;\npayments.getSavedStarGift#b455a106 stargift:Vector<InputSavedStarGift> = payments.SavedStarGifts;\npayments.getStarGiftWithdrawalUrl#d06e93a8 stargift:InputSavedStarGift password:InputCheckPasswordSRP = payments.StarGiftWithdrawalUrl;\npayments.toggleChatStarGiftNotifications#60eaefa1 flags:# enabled:flags.0?true peer:InputPeer = Bool;\npayments.toggleStarGiftsPinnedToTop#1513e7b0 peer:InputPeer stargift:Vector<InputSavedStarGift> = Bool;\npayments.canPurchaseStore#4fdc5ea7 purpose:InputStorePaymentPurpose = Bool;\npayments.getResaleStarGifts#7a5fa236 flags:# sort_by_price:flags.1?true sort_by_num:flags.2?true for_craft:flags.4?true stars_only:flags.5?true attributes_hash:flags.0?long gift_id:long attributes:flags.3?Vector<StarGiftAttributeId> offset:string limit:int = payments.ResaleStarGifts;\npayments.updateStarGiftPrice#edbe6ccb stargift:InputSavedStarGift resell_amount:StarsAmount = Updates;\npayments.createStarGiftCollection#1f4a0e87 peer:InputPeer title:string stargift:Vector<InputSavedStarGift> = StarGiftCollection;\npayments.updateStarGiftCollection#4fddbee7 flags:# peer:InputPeer collection_id:int title:flags.0?string delete_stargift:flags.1?Vector<InputSavedStarGift> add_stargift:flags.2?Vector<InputSavedStarGift> order:flags.3?Vector<InputSavedStarGift> = StarGiftCollection;\npayments.reorderStarGiftCollections#c32af4cc peer:InputPeer order:Vector<int> = Bool;\npayments.deleteStarGiftCollection#ad5648e8 peer:InputPeer collection_id:int = Bool;\npayments.getStarGiftCollections#981b91dd peer:InputPeer hash:long = payments.StarGiftCollections;\npayments.getUniqueStarGiftValueInfo#4365af6b slug:string = payments.UniqueStarGiftValueInfo;\npayments.checkCanSendGift#c0c4edc9 gift_id:long = payments.CheckCanSendGiftResult;\npayments.getStarGiftAuctionState#5c9ff4d6 auction:InputStarGiftAuction version:int = payments.StarGiftAuctionState;\npayments.getStarGiftAuctionAcquiredGifts#6ba2cbec gift_id:long = payments.StarGiftAuctionAcquiredGifts;\npayments.getStarGiftActiveAuctions#a5d0514d hash:long = payments.StarGiftActiveAuctions;\npayments.resolveStarGiftOffer#e9ce781c flags:# decline:flags.0?true offer_msg_id:int = Updates;\npayments.sendStarGiftOffer#8fb86b41 flags:# peer:InputPeer slug:string price:StarsAmount duration:int random_id:long allow_paid_stars:flags.0?long = Updates;\npayments.getStarGiftUpgradeAttributes#6d038b58 gift_id:long = payments.StarGiftUpgradeAttributes;\npayments.getCraftStarGifts#fd05dd00 gift_id:long offset:string limit:int = payments.SavedStarGifts;\npayments.craftStarGift#b0f9684f stargift:Vector<InputSavedStarGift> = Updates;\n\nstickers.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<InputStickerSetItem> software:flags.3?string = messages.StickerSet;\nstickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet;\nstickers.changeStickerPosition#ffb6d4ca sticker:InputDocument position:int = messages.StickerSet;\nstickers.addStickerToSet#8653febe stickerset:InputStickerSet sticker:InputStickerSetItem = messages.StickerSet;\nstickers.setStickerSetThumb#a76a5392 flags:# stickerset:InputStickerSet thumb:flags.0?InputDocument thumb_document_id:flags.1?long = messages.StickerSet;\nstickers.checkShortName#284b3639 short_name:string = Bool;\nstickers.suggestShortName#4dafc503 title:string = stickers.SuggestedShortName;\nstickers.changeSticker#f5537ebc flags:# sticker:InputDocument emoji:flags.0?string mask_coords:flags.1?MaskCoords keywords:flags.2?string = messages.StickerSet;\nstickers.renameStickerSet#124b1c00 stickerset:InputStickerSet title:string = messages.StickerSet;\nstickers.deleteStickerSet#87704394 stickerset:InputStickerSet = Bool;\nstickers.replaceSticker#4696459a sticker:InputDocument new_sticker:InputStickerSetItem = messages.StickerSet;\n\nphone.getCallConfig#55451fa9 = DataJSON;\nphone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall;\nphone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall;\nphone.confirmCall#2efe1722 peer:InputPhoneCall g_a:bytes key_fingerprint:long protocol:PhoneCallProtocol = phone.PhoneCall;\nphone.receivedCall#17d54f61 peer:InputPhoneCall = Bool;\nphone.discardCall#b2cbc1c0 flags:# video:flags.0?true peer:InputPhoneCall duration:int reason:PhoneCallDiscardReason connection_id:long = Updates;\nphone.setCallRating#59ead627 flags:# user_initiative:flags.0?true peer:InputPhoneCall rating:int comment:string = Updates;\nphone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool;\nphone.sendSignalingData#ff7a9383 peer:InputPhoneCall data:bytes = Bool;\nphone.createGroupCall#48cdc6d8 flags:# rtmp_stream:flags.2?true peer:InputPeer random_id:int title:flags.0?string schedule_date:flags.1?int = Updates;\nphone.joinGroupCall#8fb53057 flags:# muted:flags.0?true video_stopped:flags.2?true call:InputGroupCall join_as:InputPeer invite_hash:flags.1?string public_key:flags.3?int256 block:flags.3?bytes params:DataJSON = Updates;\nphone.leaveGroupCall#500377f9 call:InputGroupCall source:int = Updates;\nphone.inviteToGroupCall#7b393160 call:InputGroupCall users:Vector<InputUser> = Updates;\nphone.discardGroupCall#7a777135 call:InputGroupCall = Updates;\nphone.toggleGroupCallSettings#974392f2 flags:# reset_invite_hash:flags.1?true call:InputGroupCall join_muted:flags.0?Bool messages_enabled:flags.2?Bool send_paid_messages_stars:flags.3?long = Updates;\nphone.getGroupCall#41845db call:InputGroupCall limit:int = phone.GroupCall;\nphone.getGroupParticipants#c558d8ab call:InputGroupCall ids:Vector<InputPeer> sources:Vector<int> offset:string limit:int = phone.GroupParticipants;\nphone.checkGroupCall#b59cf977 call:InputGroupCall sources:Vector<int> = Vector<int>;\nphone.toggleGroupCallRecord#f128c708 flags:# start:flags.0?true video:flags.2?true call:InputGroupCall title:flags.1?string video_portrait:flags.2?Bool = Updates;\nphone.editGroupCallParticipant#a5273abf flags:# call:InputGroupCall participant:InputPeer muted:flags.0?Bool volume:flags.1?int raise_hand:flags.2?Bool video_stopped:flags.3?Bool video_paused:flags.4?Bool presentation_paused:flags.5?Bool = Updates;\nphone.editGroupCallTitle#1ca6ac0a call:InputGroupCall title:string = Updates;\nphone.getGroupCallJoinAs#ef7c213a peer:InputPeer = phone.JoinAsPeers;\nphone.exportGroupCallInvite#e6aa647f flags:# can_self_unmute:flags.0?true call:InputGroupCall = phone.ExportedGroupCallInvite;\nphone.toggleGroupCallStartSubscription#219c34e6 call:InputGroupCall subscribed:Bool = Updates;\nphone.startScheduledGroupCall#5680e342 call:InputGroupCall = Updates;\nphone.saveDefaultGroupCallJoinAs#575e1f8c peer:InputPeer join_as:InputPeer = Bool;\nphone.joinGroupCallPresentation#cbea6bc4 call:InputGroupCall params:DataJSON = Updates;\nphone.leaveGroupCallPresentation#1c50d144 call:InputGroupCall = Updates;\nphone.getGroupCallStreamChannels#1ab21940 call:InputGroupCall = phone.GroupCallStreamChannels;\nphone.getGroupCallStreamRtmpUrl#5af4c73a flags:# live_story:flags.0?true peer:InputPeer revoke:Bool = phone.GroupCallStreamRtmpUrl;\nphone.saveCallLog#41248786 peer:InputPhoneCall file:InputFile = Bool;\nphone.createConferenceCall#7d0444bb flags:# muted:flags.0?true video_stopped:flags.2?true join:flags.3?true random_id:int public_key:flags.3?int256 block:flags.3?bytes params:flags.3?DataJSON = Updates;\nphone.deleteConferenceCallParticipants#8ca60525 flags:# only_left:flags.0?true kick:flags.1?true call:InputGroupCall ids:Vector<long> block:bytes = Updates;\nphone.sendConferenceCallBroadcast#c6701900 call:InputGroupCall block:bytes = Updates;\nphone.inviteConferenceCallParticipant#bcf22685 flags:# video:flags.0?true call:InputGroupCall user_id:InputUser = Updates;\nphone.declineConferenceCallInvite#3c479971 msg_id:int = Updates;\nphone.getGroupCallChainBlocks#ee9f88a6 call:InputGroupCall sub_chain_id:int offset:int limit:int = Updates;\nphone.sendGroupCallMessage#b1d11410 flags:# call:InputGroupCall random_id:long message:TextWithEntities allow_paid_stars:flags.0?long send_as:flags.1?InputPeer = Updates;\nphone.sendGroupCallEncryptedMessage#e5afa56d call:InputGroupCall encrypted_message:bytes = Bool;\nphone.deleteGroupCallMessages#f64f54f7 flags:# report_spam:flags.0?true call:InputGroupCall messages:Vector<int> = Updates;\nphone.deleteGroupCallParticipantMessages#1dbfeca0 flags:# report_spam:flags.0?true call:InputGroupCall participant:InputPeer = Updates;\nphone.getGroupCallStars#6f636302 call:InputGroupCall = phone.GroupCallStars;\nphone.saveDefaultSendAs#4167add1 call:InputGroupCall send_as:InputPeer = Bool;\n\nlangpack.getLangPack#f2f2330a lang_pack:string lang_code:string = LangPackDifference;\nlangpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector<string> = Vector<LangPackString>;\nlangpack.getDifference#cd984aa5 lang_pack:string lang_code:string from_version:int = LangPackDifference;\nlangpack.getLanguages#42c6978f lang_pack:string = Vector<LangPackLanguage>;\nlangpack.getLanguage#6a596502 lang_pack:string lang_code:string = LangPackLanguage;\n\nfolders.editPeerFolders#6847d0ab folder_peers:Vector<InputFolderPeer> = Updates;\n\nstats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastStats;\nstats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph;\nstats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel = stats.MegagroupStats;\nstats.getMessagePublicForwards#5f150144 channel:InputChannel msg_id:int offset:string limit:int = stats.PublicForwards;\nstats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats;\nstats.getStoryStats#374fef40 flags:# dark:flags.0?true peer:InputPeer id:int = stats.StoryStats;\nstats.getStoryPublicForwards#a6437ef6 peer:InputPeer id:int offset:string limit:int = stats.PublicForwards;\n\nchatlists.exportChatlistInvite#8472478e chatlist:InputChatlist title:string peers:Vector<InputPeer> = chatlists.ExportedChatlistInvite;\nchatlists.deleteExportedInvite#719c5c5e chatlist:InputChatlist slug:string = Bool;\nchatlists.editExportedInvite#653db63d flags:# chatlist:InputChatlist slug:string title:flags.1?string peers:flags.2?Vector<InputPeer> = ExportedChatlistInvite;\nchatlists.getExportedInvites#ce03da83 chatlist:InputChatlist = chatlists.ExportedInvites;\nchatlists.checkChatlistInvite#41c10fff slug:string = chatlists.ChatlistInvite;\nchatlists.joinChatlistInvite#a6b1e39a slug:string peers:Vector<InputPeer> = Updates;\nchatlists.getChatlistUpdates#89419521 chatlist:InputChatlist = chatlists.ChatlistUpdates;\nchatlists.joinChatlistUpdates#e089f8f5 chatlist:InputChatlist peers:Vector<InputPeer> = Updates;\nchatlists.hideChatlistUpdates#66e486fb chatlist:InputChatlist = Bool;\nchatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector<Peer>;\nchatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector<InputPeer> = Updates;\n\nstories.canSendStory#30eb63f0 peer:InputPeer = stories.CanSendStoryCount;\nstories.sendStory#8f9e6898 flags:# pinned:flags.2?true noforwards:flags.4?true fwd_modified:flags.7?true peer:InputPeer media:InputMedia media_areas:flags.5?Vector<MediaArea> caption:flags.0?string entities:flags.1?Vector<MessageEntity> privacy_rules:Vector<InputPrivacyRule> random_id:long period:flags.3?int fwd_from_id:flags.6?InputPeer fwd_from_story:flags.6?int albums:flags.8?Vector<int> music:flags.9?InputDocument = Updates;\nstories.editStory#2c63a72b flags:# peer:InputPeer id:int media:flags.0?InputMedia media_areas:flags.3?Vector<MediaArea> caption:flags.1?string entities:flags.1?Vector<MessageEntity> privacy_rules:flags.2?Vector<InputPrivacyRule> music:flags.4?InputDocument = Updates;\nstories.deleteStories#ae59db5f peer:InputPeer id:Vector<int> = Vector<int>;\nstories.togglePinned#9a75a1ef peer:InputPeer id:Vector<int> pinned:Bool = Vector<int>;\nstories.getAllStories#eeb0d625 flags:# next:flags.1?true hidden:flags.2?true state:flags.0?string = stories.AllStories;\nstories.getPinnedStories#5821a5dc peer:InputPeer offset_id:int limit:int = stories.Stories;\nstories.getStoriesArchive#b4352016 peer:InputPeer offset_id:int limit:int = stories.Stories;\nstories.getStoriesByID#5774ca74 peer:InputPeer id:Vector<int> = stories.Stories;\nstories.toggleAllStoriesHidden#7c2557c4 hidden:Bool = Bool;\nstories.readStories#a556dac8 peer:InputPeer max_id:int = Vector<int>;\nstories.incrementStoryViews#b2028afb peer:InputPeer id:Vector<int> = Bool;\nstories.getStoryViewsList#7ed23c57 flags:# just_contacts:flags.0?true reactions_first:flags.2?true forwards_first:flags.3?true peer:InputPeer q:flags.1?string id:int offset:string limit:int = stories.StoryViewsList;\nstories.getStoriesViews#28e16cc8 peer:InputPeer id:Vector<int> = stories.StoryViews;\nstories.exportStoryLink#7b8def20 peer:InputPeer id:int = ExportedStoryLink;\nstories.report#19d8eb45 peer:InputPeer id:Vector<int> option:bytes message:string = ReportResult;\nstories.activateStealthMode#57bbd166 flags:# past:flags.0?true future:flags.1?true = Updates;\nstories.sendReaction#7fd736b2 flags:# add_to_recent:flags.0?true peer:InputPeer story_id:int reaction:Reaction = Updates;\nstories.getPeerStories#2c4ada50 peer:InputPeer = stories.PeerStories;\nstories.getAllReadPeerStories#9b5ae7f9 = Updates;\nstories.getPeerMaxIDs#78499170 id:Vector<InputPeer> = Vector<RecentStory>;\nstories.getChatsToSend#a56a8b60 = messages.Chats;\nstories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool;\nstories.getStoryReactionsList#b9b2881f flags:# forwards_first:flags.2?true peer:InputPeer id:int reaction:flags.0?Reaction offset:flags.1?string limit:int = stories.StoryReactionsList;\nstories.togglePinnedToTop#b297e9b peer:InputPeer id:Vector<int> = Bool;\nstories.searchPosts#d1810907 flags:# hashtag:flags.0?string area:flags.1?MediaArea peer:flags.2?InputPeer offset:string limit:int = stories.FoundStories;\nstories.createAlbum#a36396e5 peer:InputPeer title:string stories:Vector<int> = StoryAlbum;\nstories.updateAlbum#5e5259b6 flags:# peer:InputPeer album_id:int title:flags.0?string delete_stories:flags.1?Vector<int> add_stories:flags.2?Vector<int> order:flags.3?Vector<int> = StoryAlbum;\nstories.reorderAlbums#8535fbd9 peer:InputPeer order:Vector<int> = Bool;\nstories.deleteAlbum#8d3456d0 peer:InputPeer album_id:int = Bool;\nstories.getAlbums#25b3eac7 peer:InputPeer hash:long = stories.Albums;\nstories.getAlbumStories#ac806d61 peer:InputPeer album_id:int offset:int limit:int = stories.Stories;\nstories.startLive#d069ccde flags:# pinned:flags.2?true noforwards:flags.4?true rtmp_stream:flags.5?true peer:InputPeer caption:flags.0?string entities:flags.1?Vector<MessageEntity> privacy_rules:Vector<InputPrivacyRule> random_id:long messages_enabled:flags.6?Bool send_paid_messages_stars:flags.7?long = Updates;\n\npremium.getBoostsList#60f67660 flags:# gifts:flags.0?true peer:InputPeer offset:string limit:int = premium.BoostsList;\npremium.getMyBoosts#be77b4a = premium.MyBoosts;\npremium.applyBoost#6b7da746 flags:# slots:flags.0?Vector<int> peer:InputPeer = premium.MyBoosts;\npremium.getBoostsStatus#42f1f61 peer:InputPeer = premium.BoostsStatus;\npremium.getUserBoosts#39854d1f peer:InputPeer user_id:InputUser = premium.BoostsList;\n\nsmsjobs.isEligibleToJoin#edc39d0 = smsjobs.EligibilityToJoin;\nsmsjobs.join#a74ece2d = Bool;\nsmsjobs.leave#9898ad73 = Bool;\nsmsjobs.updateSettings#93fa0bf flags:# allow_international:flags.0?true = Bool;\nsmsjobs.getStatus#10a698e8 = smsjobs.Status;\nsmsjobs.getSmsJob#778d902f job_id:string = SmsJob;\nsmsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool;\n\nfragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo;\n\n// LAYER 224\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/scheme/mtproto.tl",
    "content": "// Core types (no need to gen)\n\nint ? = Int;\nlong ? = Long;\ndouble ? = Double;\nstring ? = String;\n\nvector {t:Type} # [ t ] = Vector t;\n\nint128 4*[ int ] = Int128;\nint256 8*[ int ] = Int256;\n\n///////////////////////////////\n/// Authorization key creation\n///////////////////////////////\n\nresPQ#05162463 nonce:int128 server_nonce:int128 pq:string server_public_key_fingerprints:Vector<long> = ResPQ;\n\np_q_inner_data#83c95aec pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data;\np_q_inner_data_dc#a9f55f95 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int = P_Q_inner_data;\np_q_inner_data_temp#3c6a84d4 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 expires_in:int = P_Q_inner_data;\np_q_inner_data_temp_dc#56fddf88 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int expires_in:int = P_Q_inner_data;\n\nbind_auth_key_inner#75a3f765 nonce:long temp_auth_key_id:long perm_auth_key_id:long temp_session_id:long expires_at:int = BindAuthKeyInner;\n\nserver_DH_params_fail#79cb045d nonce:int128 server_nonce:int128 new_nonce_hash:int128 = Server_DH_Params;\nserver_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:string = Server_DH_Params;\n\nserver_DH_inner_data#b5890dba nonce:int128 server_nonce:int128 g:int dh_prime:string g_a:string server_time:int = Server_DH_inner_data;\n\nclient_DH_inner_data#6643b654 nonce:int128 server_nonce:int128 retry_id:long g_b:string = Client_DH_Inner_Data;\n\ndh_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hash1:int128 = Set_client_DH_params_answer;\ndh_gen_retry#46dc1fb9 nonce:int128 server_nonce:int128 new_nonce_hash2:int128 = Set_client_DH_params_answer;\ndh_gen_fail#a69dae02 nonce:int128 server_nonce:int128 new_nonce_hash3:int128 = Set_client_DH_params_answer;\n\ndestroy_auth_key_ok#f660e1d4 = DestroyAuthKeyRes;\ndestroy_auth_key_none#0a9f2259 = DestroyAuthKeyRes;\ndestroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes;\n\n---functions---\n\nreq_pq#60469778 nonce:int128 = ResPQ;\nreq_pq_multi#be7e8ef1 nonce:int128 = ResPQ;\n\nreq_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string = Server_DH_Params;\n\nset_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:string = Set_client_DH_params_answer;\n\ndestroy_auth_key#d1435160 = DestroyAuthKeyRes;\n\n///////////////////////////////\n////////////// System messages\n///////////////////////////////\n\n---types---\n\nmsgs_ack#62d6b459 msg_ids:Vector<long> = MsgsAck;\n\nbad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification;\nbad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification;\n\nmsgs_state_req#da69fb52 msg_ids:Vector<long> = MsgsStateReq;\nmsgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;\nmsgs_all_info#8cc0d131 msg_ids:Vector<long> info:string = MsgsAllInfo;\n\nmsg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo;\nmsg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo;\n\nmsg_resend_req#7d861a08 msg_ids:Vector<long> = MsgResendReq;\n\n//rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult; // parsed manually\n\nrpc_error#2144ca19 error_code:int error_message:string = RpcError;\n\nrpc_answer_unknown#5e2ad36e = RpcDropAnswer;\nrpc_answer_dropped_running#cd78e586 = RpcDropAnswer;\nrpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer;\n\nfuture_salt#0949d9dc valid_since:int valid_until:int salt:long = FutureSalt;\nfuture_salts#ae500895 req_msg_id:long now:int salts:vector<future_salt> = FutureSalts;\n\npong#347773c5 msg_id:long ping_id:long = Pong;\n\ndestroy_session_ok#e22045fc session_id:long = DestroySessionRes;\ndestroy_session_none#62d350c9 session_id:long = DestroySessionRes;\n\nnew_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long = NewSession;\n\n//message msg_id:long seqno:int bytes:int body:Object = Message; // parsed manually\n//msg_container#73f1f8dc messages:vector<message> = MessageContainer; // parsed manually\n//msg_copy#e06046b2 orig_message:Message = MessageCopy; // parsed manually, not used - use msg_container\n//gzip_packed#3072cfa1 packed_data:string = Object; // parsed manually\n\nhttp_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait;\n\n//ipPort ipv4:int port:int = IpPort;\n//help.configSimple#d997c3c5 date:int expires:int dc_id:int ip_port_list:Vector<ipPort> = help.ConfigSimple;\n\nipPort#d433ad73 ipv4:int port:int = IpPort;\nipPortSecret#37982646 ipv4:int port:int secret:bytes = IpPort;\naccessPointRule#4679b65f phone_prefix_rules:string dc_id:int ips:vector<IpPort> = AccessPointRule;\nhelp.configSimple#5a592a6c date:int expires:int rules:vector<AccessPointRule> = help.ConfigSimple;\n\ntlsClientHello blocks:vector<TlsBlock> = TlsClientHello;\n\ntlsBlockString data:string = TlsBlock;\ntlsBlockRandom length:int = TlsBlock;\ntlsBlockZero length:int = TlsBlock;\ntlsBlockDomain = TlsBlock;\ntlsBlockGrease seed:int = TlsBlock;\ntlsBlockPublicKey = TlsBlock;\ntlsBlockScope entries:Vector<TlsBlock> = TlsBlock;\ntlsBlockPermutation entries:Vector<Vector<TlsBlock>> = TlsBlock;\ntlsBlockM = TlsBlock;\ntlsBlockE = TlsBlock;\ntlsBlockPadding = TlsBlock;\n\n---functions---\n\nrpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;\n\nget_future_salts#b921bd04 num:int = FutureSalts;\n\nping#7abe77ec ping_id:long = Pong;\nping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;\n\ndestroy_session#e7512126 session_id:long = DestroySessionRes;\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/sender.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/variant.h\"\n#include \"mtproto/mtproto_response.h\"\n#include \"mtproto/mtp_instance.h\"\n#include \"mtproto/facade.h\"\n\nnamespace MTP {\n\nclass Sender {\n\tclass RequestBuilder {\n\tpublic:\n\t\tRequestBuilder(const RequestBuilder &other) = delete;\n\t\tRequestBuilder &operator=(const RequestBuilder &other) = delete;\n\t\tRequestBuilder &operator=(RequestBuilder &&other) = delete;\n\n\tprotected:\n\t\tenum class FailSkipPolicy {\n\t\t\tSimple,\n\t\t\tHandleFlood,\n\t\t\tHandleAll,\n\t\t};\n\t\tusing FailPlainHandler = Fn<void()>;\n\t\tusing FailErrorHandler = Fn<void(const Error&)>;\n\t\tusing FailRequestIdHandler = Fn<void(const Error&, mtpRequestId)>;\n\t\tusing FailFullHandler = Fn<void(const Error&, const Response&)>;\n\n\t\ttemplate <typename ...Args>\n\t\tstatic constexpr bool IsCallable\n\t\t\t= rpl::details::is_callable_plain_v<Args...>;\n\n\t\ttemplate <typename Result, typename Handler>\n\t\t[[nodiscard]] DoneHandler MakeDoneHandler(\n\t\t\t\tnot_null<Sender*> sender,\n\t\t\t\tHandler &&handler) {\n\t\t\treturn [sender, handler = std::forward<Handler>(handler)](\n\t\t\t\t\tconst Response &response) mutable {\n\t\t\t\tauto onstack = std::move(handler);\n\t\t\t\tsender->senderRequestHandled(response.requestId);\n\n\t\t\t\tauto result = Result();\n\t\t\t\tauto from = response.reply.constData();\n\t\t\t\tif (!result.read(from, from + response.reply.size())) {\n\t\t\t\t\treturn false;\n\t\t\t\t} else if (!onstack) {\n\t\t\t\t\treturn true;\n\t\t\t\t} else if constexpr (IsCallable<\n\t\t\t\t\t\tHandler,\n\t\t\t\t\t\tconst Result&,\n\t\t\t\t\t\tconst Response&>) {\n\t\t\t\t\tonstack(result, response);\n\t\t\t\t} else if constexpr (IsCallable<\n\t\t\t\t\t\tHandler,\n\t\t\t\t\t\tconst Result&,\n\t\t\t\t\t\tmtpRequestId>) {\n\t\t\t\t\tonstack(result, response.requestId);\n\t\t\t\t} else if constexpr (IsCallable<\n\t\t\t\t\t\tHandler,\n\t\t\t\t\t\tconst Result&>) {\n\t\t\t\t\tonstack(result);\n\t\t\t\t} else if constexpr (IsCallable<Handler>) {\n\t\t\t\t\tonstack();\n\t\t\t\t} else {\n\t\t\t\t\tstatic_assert(false_t(Handler{}), \"Bad done handler.\");\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t};\n\t\t}\n\n\t\ttemplate <typename Handler>\n\t\t[[nodiscard]] FailHandler MakeFailHandler(\n\t\t\t\tnot_null<Sender*> sender,\n\t\t\t\tHandler &&handler,\n\t\t\t\tFailSkipPolicy skipPolicy) {\n\t\t\treturn [\n\t\t\t\tsender,\n\t\t\t\thandler = std::forward<Handler>(handler),\n\t\t\t\tskipPolicy\n\t\t\t](const Error &error, const Response &response) {\n\t\t\t\tif (skipPolicy == FailSkipPolicy::Simple) {\n\t\t\t\t\tif (IsDefaultHandledError(error)) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t} else if (skipPolicy == FailSkipPolicy::HandleFlood) {\n\t\t\t\t\tif (IsDefaultHandledError(error) && !IsFloodError(error)) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tauto onstack = handler;\n\t\t\t\tsender->senderRequestHandled(response.requestId);\n\n\t\t\t\tif (!onstack) {\n\t\t\t\t\treturn true;\n\t\t\t\t} else if constexpr (IsCallable<\n\t\t\t\t\t\tHandler,\n\t\t\t\t\t\tconst Error&,\n\t\t\t\t\t\tconst Response&>) {\n\t\t\t\t\tonstack(error, response);\n\t\t\t\t} else if constexpr (IsCallable<\n\t\t\t\t\t\tHandler,\n\t\t\t\t\t\tconst Error&,\n\t\t\t\t\t\tmtpRequestId>) {\n\t\t\t\t\tonstack(error, response.requestId);\n\t\t\t\t} else if constexpr (IsCallable<\n\t\t\t\t\t\tHandler,\n\t\t\t\t\t\tconst Error&>) {\n\t\t\t\t\tonstack(error);\n\t\t\t\t} else if constexpr (IsCallable<Handler>) {\n\t\t\t\t\tonstack();\n\t\t\t\t} else {\n\t\t\t\t\tstatic_assert(false_t(Handler{}), \"Bad fail handler.\");\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t};\n\t\t}\n\n\t\texplicit RequestBuilder(not_null<Sender*> sender) noexcept\n\t\t: _sender(sender) {\n\t\t}\n\t\tRequestBuilder(RequestBuilder &&other) = default;\n\n\t\tvoid setToDC(ShiftedDcId dcId) noexcept {\n\t\t\t_dcId = dcId;\n\t\t}\n\t\tvoid setOverrideRequestId(mtpRequestId id) noexcept {\n\t\t\t_overrideRequestId = id;\n\t\t}\n\t\tvoid setCanWait(crl::time ms) noexcept {\n\t\t\t_canWait = ms;\n\t\t}\n\t\tvoid setDoneHandler(DoneHandler &&handler) noexcept {\n\t\t\t_done = std::move(handler);\n\t\t}\n\t\ttemplate <typename Handler>\n\t\tvoid setFailHandler(Handler &&handler) noexcept {\n\t\t\t_fail = std::forward<Handler>(handler);\n\t\t}\n\t\tvoid setFailSkipPolicy(FailSkipPolicy policy) noexcept {\n\t\t\t_failSkipPolicy = policy;\n\t\t}\n\t\tvoid setAfter(mtpRequestId requestId) noexcept {\n\t\t\t_afterRequestId = requestId;\n\t\t}\n\n\t\t[[nodiscard]] ShiftedDcId takeDcId() const noexcept {\n\t\t\treturn _dcId;\n\t\t}\n\t\t[[nodiscard]] crl::time takeCanWait() const noexcept {\n\t\t\treturn _canWait;\n\t\t}\n\t\t[[nodiscard]] DoneHandler takeOnDone() noexcept {\n\t\t\treturn std::move(_done);\n\t\t}\n\t\t[[nodiscard]] FailHandler takeOnFail() {\n\t\t\treturn v::match(_fail, [&](auto &value) {\n\t\t\t\treturn MakeFailHandler(\n\t\t\t\t\t_sender,\n\t\t\t\t\tstd::move(value),\n\t\t\t\t\t_failSkipPolicy);\n\t\t\t});\n\t\t}\n\t\t[[nodiscard]] mtpRequestId takeAfter() const noexcept {\n\t\t\treturn _afterRequestId;\n\t\t}\n\t\t[[nodiscard]] mtpRequestId takeOverrideRequestId() const noexcept {\n\t\t\treturn _overrideRequestId;\n\t\t}\n\n\t\t[[nodiscard]] not_null<Sender*> sender() const noexcept {\n\t\t\treturn _sender;\n\t\t}\n\t\tvoid registerRequest(mtpRequestId requestId) {\n\t\t\t_sender->senderRequestRegister(requestId);\n\t\t}\n\n\tprivate:\n\t\tnot_null<Sender*> _sender;\n\t\tShiftedDcId _dcId = 0;\n\t\tcrl::time _canWait = 0;\n\t\tDoneHandler _done;\n\t\tstd::variant<\n\t\t\tFailPlainHandler,\n\t\t\tFailErrorHandler,\n\t\t\tFailRequestIdHandler,\n\t\t\tFailFullHandler> _fail;\n\t\tFailSkipPolicy _failSkipPolicy = FailSkipPolicy::Simple;\n\t\tmtpRequestId _afterRequestId = 0;\n\t\tmtpRequestId _overrideRequestId = 0;\n\n\t};\n\npublic:\n\texplicit Sender(not_null<Instance*> instance) noexcept\n\t: _instance(instance) {\n\t}\n\n\t[[nodiscard]] Instance &instance() const {\n\t\treturn *_instance;\n\t}\n\n\ttemplate <typename Request>\n\tclass SpecificRequestBuilder : public RequestBuilder {\n\tprivate:\n\t\tfriend class Sender;\n\t\tSpecificRequestBuilder(not_null<Sender*> sender, Request &&request) noexcept\n\t\t: RequestBuilder(sender)\n\t\t, _request(std::move(request)) {\n\t\t}\n\n\tpublic:\n\t\tSpecificRequestBuilder(SpecificRequestBuilder &&other) = default;\n\n\t\t[[nodiscard]] SpecificRequestBuilder &toDC(ShiftedDcId dcId) noexcept {\n\t\t\tsetToDC(dcId);\n\t\t\treturn *this;\n\t\t}\n\t\t[[nodiscard]] SpecificRequestBuilder &afterDelay(crl::time ms) noexcept {\n\t\t\tsetCanWait(ms);\n\t\t\treturn *this;\n\t\t}\n\t\t[[nodiscard]] SpecificRequestBuilder &overrideId(mtpRequestId id) noexcept {\n\t\t\tsetOverrideRequestId(id);\n\t\t\treturn *this;\n\t\t}\n\n\t\tusing Result = typename Request::ResponseType;\n\t\t[[nodiscard]] SpecificRequestBuilder &done(\n\t\t\tFnMut<void(\n\t\t\t\tconst Result &result,\n\t\t\t\tmtpRequestId requestId)> callback) {\n\t\t\tsetDoneHandler(\n\t\t\t\tMakeDoneHandler<Result>(sender(), std::move(callback)));\n\t\t\treturn *this;\n\t\t}\n\t\t[[nodiscard]] SpecificRequestBuilder &done(\n\t\t\tFnMut<void(\n\t\t\t\tconst Result &result,\n\t\t\t\tconst Response &response)> callback) {\n\t\t\tsetDoneHandler(\n\t\t\t\tMakeDoneHandler<Result>(sender(), std::move(callback)));\n\t\t\treturn *this;\n\t\t}\n\t\t[[nodiscard]] SpecificRequestBuilder &done(\n\t\t\t\tFnMut<void()> callback) {\n\t\t\tsetDoneHandler(\n\t\t\t\tMakeDoneHandler<Result>(sender(), std::move(callback)));\n\t\t\treturn *this;\n\t\t}\n\t\t[[nodiscard]] SpecificRequestBuilder &done(\n\t\t\tFnMut<void(\n\t\t\t\tconst typename Request::ResponseType &result)> callback) {\n\t\t\tsetDoneHandler(\n\t\t\t\tMakeDoneHandler<Result>(sender(), std::move(callback)));\n\t\t\treturn *this;\n\t\t}\n\n\t\t[[nodiscard]] SpecificRequestBuilder &fail(\n\t\t\tFn<void(\n\t\t\t\tconst Error &error,\n\t\t\t\tmtpRequestId requestId)> callback) noexcept {\n\t\t\tsetFailHandler(std::move(callback));\n\t\t\treturn *this;\n\t\t}\n\t\t[[nodiscard]] SpecificRequestBuilder &fail(\n\t\t\tFn<void(\n\t\t\t\tconst Error &error,\n\t\t\t\tconst Response &response)> callback) noexcept {\n\t\t\tsetFailHandler(std::move(callback));\n\t\t\treturn *this;\n\t\t}\n\t\t[[nodiscard]] SpecificRequestBuilder &fail(\n\t\t\t\tFn<void()> callback) noexcept {\n\t\t\tsetFailHandler(std::move(callback));\n\t\t\treturn *this;\n\t\t}\n\t\t[[nodiscard]] SpecificRequestBuilder &fail(\n\t\t\t\tFn<void(const Error &error)> callback) noexcept {\n\t\t\tsetFailHandler(std::move(callback));\n\t\t\treturn *this;\n\t\t}\n\n\t\t[[nodiscard]] SpecificRequestBuilder &handleFloodErrors() noexcept {\n\t\t\tsetFailSkipPolicy(FailSkipPolicy::HandleFlood);\n\t\t\treturn *this;\n\t\t}\n\t\t[[nodiscard]] SpecificRequestBuilder &handleAllErrors() noexcept {\n\t\t\tsetFailSkipPolicy(FailSkipPolicy::HandleAll);\n\t\t\treturn *this;\n\t\t}\n\t\t[[nodiscard]] SpecificRequestBuilder &afterRequest(mtpRequestId requestId) noexcept {\n\t\t\tsetAfter(requestId);\n\t\t\treturn *this;\n\t\t}\n\n\t\tmtpRequestId send() {\n\t\t\tconst auto id = sender()->_instance->send(\n\t\t\t\t_request,\n\t\t\t\ttakeOnDone(),\n\t\t\t\ttakeOnFail(),\n\t\t\t\ttakeDcId(),\n\t\t\t\ttakeCanWait(),\n\t\t\t\ttakeAfter(),\n\t\t\t\ttakeOverrideRequestId());\n\t\t\tregisterRequest(id);\n\t\t\treturn id;\n\t\t}\n\n\tprivate:\n\t\tRequest _request;\n\n\t};\n\n\tclass SentRequestWrap {\n\tprivate:\n\t\tfriend class Sender;\n\t\tSentRequestWrap(not_null<Sender*> sender, mtpRequestId requestId) : _sender(sender), _requestId(requestId) {\n\t\t}\n\n\tpublic:\n\t\tvoid cancel() {\n\t\t\tif (_requestId) {\n\t\t\t\t_sender->senderRequestCancel(_requestId);\n\t\t\t}\n\t\t}\n\n\tprivate:\n\t\tnot_null<Sender*> _sender;\n\t\tmtpRequestId _requestId = 0;\n\n\t};\n\n\ttemplate <\n\t\ttypename Request,\n\t\ttypename = std::enable_if_t<!std::is_reference_v<Request>>,\n\t\ttypename = typename Request::Unboxed>\n\t[[nodiscard]] SpecificRequestBuilder<Request> request(Request &&request) noexcept;\n\n\t[[nodiscard]] SentRequestWrap request(mtpRequestId requestId) noexcept;\n\n\t[[nodiscard]] auto requestCanceller() noexcept {\n\t\treturn [this](mtpRequestId requestId) {\n\t\t\trequest(requestId).cancel();\n\t\t};\n\t}\n\n\tvoid requestSendDelayed() {\n\t\t_instance->sendAnything();\n\t}\n\tvoid requestCancellingDiscard() {\n\t\tfor (auto &request : base::take(_requests)) {\n\t\t\trequest.handled();\n\t\t}\n\t}\n\n\t[[nodiscard]] mtpRequestId allocateRequestId() noexcept {\n\t\treturn details::GetNextRequestId();\n\t}\n\t[[nodiscard]] bool pending(mtpRequestId requestId) noexcept {\n\t\treturn _requests.contains(requestId);\n\t}\n\nprivate:\n\tclass RequestWrap {\n\tpublic:\n\t\tRequestWrap(\n\t\t\tnot_null<Instance*> instance,\n\t\t\tmtpRequestId requestId) noexcept\n\t\t: _instance(instance)\n\t\t, _id(requestId) {\n\t\t}\n\n\t\tRequestWrap(const RequestWrap &other) = delete;\n\t\tRequestWrap &operator=(const RequestWrap &other) = delete;\n\t\tRequestWrap(RequestWrap &&other)\n\t\t: _instance(other._instance)\n\t\t, _id(base::take(other._id)) {\n\t\t}\n\t\tRequestWrap &operator=(RequestWrap &&other) {\n\t\t\tExpects(_instance == other._instance);\n\n\t\t\tif (_id != other._id) {\n\t\t\t\tcancelRequest();\n\t\t\t\t_id = base::take(other._id);\n\t\t\t}\n\t\t\treturn *this;\n\t\t}\n\n\t\tmtpRequestId id() const noexcept {\n\t\t\treturn _id;\n\t\t}\n\t\tvoid handled() const noexcept {\n\t\t\t_id = 0;\n\t\t}\n\n\t\t~RequestWrap() {\n\t\t\tcancelRequest();\n\t\t}\n\n\tprivate:\n\t\tvoid cancelRequest() {\n\t\t\tif (_id) {\n\t\t\t\t_instance->cancel(_id);\n\t\t\t}\n\t\t}\n\t\tconst not_null<Instance*> _instance;\n\t\tmutable mtpRequestId _id = 0;\n\n\t};\n\n\tstruct RequestWrapComparator {\n\t\tusing is_transparent = std::true_type;\n\n\t\tstruct helper {\n\t\t\tmtpRequestId requestId = 0;\n\n\t\t\thelper() = default;\n\t\t\thelper(const helper &other) = default;\n\t\t\thelper(mtpRequestId requestId) noexcept : requestId(requestId) {\n\t\t\t}\n\t\t\thelper(const RequestWrap &request) noexcept : requestId(request.id()) {\n\t\t\t}\n\t\t\tbool operator<(helper other) const {\n\t\t\t\treturn requestId < other.requestId;\n\t\t\t}\n\t\t};\n\t\tbool operator()(const helper &&lhs, const helper &&rhs) const {\n\t\t\treturn lhs < rhs;\n\t\t}\n\n\t};\n\n\ttemplate <typename Request>\n\tfriend class SpecificRequestBuilder;\n\tfriend class RequestBuilder;\n\tfriend class RequestWrap;\n\tfriend class SentRequestWrap;\n\n\tvoid senderRequestRegister(mtpRequestId requestId) {\n\t\t_requests.emplace(_instance, requestId);\n\t}\n\tvoid senderRequestHandled(mtpRequestId requestId) {\n\t\tauto it = _requests.find(requestId);\n\t\tif (it != _requests.cend()) {\n\t\t\tit->handled();\n\t\t\t_requests.erase(it);\n\t\t}\n\t}\n\tvoid senderRequestCancel(mtpRequestId requestId) {\n\t\tauto it = _requests.find(requestId);\n\t\tif (it != _requests.cend()) {\n\t\t\t_requests.erase(it);\n\t\t}\n\t}\n\n\tconst not_null<Instance*> _instance;\n\tbase::flat_set<RequestWrap, RequestWrapComparator> _requests;\n\n};\n\ntemplate <typename Request, typename, typename>\nSender::SpecificRequestBuilder<Request> Sender::request(Request &&request) noexcept {\n\treturn SpecificRequestBuilder<Request>(this, std::move(request));\n}\n\ninline Sender::SentRequestWrap Sender::request(mtpRequestId requestId) noexcept {\n\treturn SentRequestWrap(this, requestId);\n}\n\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/session.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/session.h\"\n\n#include \"mtproto/details/mtproto_dcenter.h\"\n#include \"mtproto/session_private.h\"\n#include \"mtproto/mtproto_auth_key.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"base/unixtime.h\"\n\nnamespace MTP {\nnamespace details {\n\nSessionOptions::SessionOptions(\n\tconst QString &systemLangCode,\n\tconst QString &cloudLangCode,\n\tconst QString &langPackName,\n\tconst ProxyData &proxy,\n\tbool useIPv4,\n\tbool useIPv6,\n\tbool useHttp,\n\tbool useTcp)\n: systemLangCode(systemLangCode)\n, cloudLangCode(cloudLangCode)\n, langPackName(langPackName)\n, proxy(proxy)\n, useIPv4(useIPv4)\n, useIPv6(useIPv6)\n, useHttp(useHttp)\n, useTcp(useTcp) {\n}\n\ntemplate <typename Callback>\nvoid SessionData::withSession(Callback &&callback) {\n\tQMutexLocker lock(&_ownerMutex);\n\tif (const auto session = _owner) {\n\t\tInvokeQueued(session, [\n\t\t\tsession,\n\t\t\tcallback = std::forward<Callback>(callback)\n\t\t] {\n\t\t\tcallback(session);\n\t\t});\n\t}\n}\n\nvoid SessionData::notifyConnectionInited(const SessionOptions &options) {\n\t// #TODO race\n\tconst auto current = this->options();\n\tif (current.cloudLangCode == _options.cloudLangCode\n\t\t&& current.systemLangCode == _options.systemLangCode\n\t\t&& current.langPackName == _options.langPackName\n\t\t&& current.proxy == _options.proxy) {\n\t\tQMutexLocker lock(&_ownerMutex);\n\t\tif (_owner) {\n\t\t\t_owner->notifyDcConnectionInited();\n\t\t}\n\t}\n}\n\nvoid SessionData::queueTryToReceive() {\n\twithSession([](not_null<Session*> session) {\n\t\tsession->tryToReceive();\n\t});\n}\n\nvoid SessionData::queueNeedToResumeAndSend() {\n\twithSession([](not_null<Session*> session) {\n\t\tsession->needToResumeAndSend();\n\t});\n}\n\nvoid SessionData::queueConnectionStateChange(int newState) {\n\twithSession([=](not_null<Session*> session) {\n\t\tsession->connectionStateChange(newState);\n\t});\n}\n\nvoid SessionData::queueResetDone() {\n\twithSession([](not_null<Session*> session) {\n\t\tsession->resetDone();\n\t});\n}\n\nvoid SessionData::queueSendAnything(crl::time msCanWait) {\n\twithSession([=](not_null<Session*> session) {\n\t\tsession->sendAnything(msCanWait);\n\t});\n}\n\nbool SessionData::connectionInited() const {\n\tQMutexLocker lock(&_ownerMutex);\n\treturn _owner ? _owner->connectionInited() : false;\n}\n\nAuthKeyPtr SessionData::getTemporaryKey(TemporaryKeyType type) const {\n\tQMutexLocker lock(&_ownerMutex);\n\treturn _owner ? _owner->getTemporaryKey(type) : nullptr;\n}\n\nAuthKeyPtr SessionData::getPersistentKey() const {\n\tQMutexLocker lock(&_ownerMutex);\n\treturn _owner ? _owner->getPersistentKey() : nullptr;\n}\n\nCreatingKeyType SessionData::acquireKeyCreation(DcType type) {\n\tQMutexLocker lock(&_ownerMutex);\n\treturn _owner ? _owner->acquireKeyCreation(type) : CreatingKeyType::None;\n}\n\nbool SessionData::releaseKeyCreationOnDone(\n\t\tconst AuthKeyPtr &temporaryKey,\n\t\tconst AuthKeyPtr &persistentKeyUsedForBind) {\n\tQMutexLocker lock(&_ownerMutex);\n\treturn _owner\n\t\t? _owner->releaseKeyCreationOnDone(\n\t\t\ttemporaryKey,\n\t\t\tpersistentKeyUsedForBind)\n\t\t: false;\n}\n\nbool SessionData::releaseCdnKeyCreationOnDone(\n\t\tconst AuthKeyPtr &temporaryKey) {\n\tQMutexLocker lock(&_ownerMutex);\n\treturn _owner\n\t\t? _owner->releaseCdnKeyCreationOnDone(temporaryKey)\n\t\t: false;\n}\n\nvoid SessionData::releaseKeyCreationOnFail() {\n\tQMutexLocker lock(&_ownerMutex);\n\tif (_owner) {\n\t\t_owner->releaseKeyCreationOnFail();\n\t}\n}\n\nvoid SessionData::destroyTemporaryKey(uint64 keyId) {\n\tQMutexLocker lock(&_ownerMutex);\n\tif (_owner) {\n\t\t_owner->destroyTemporaryKey(keyId);\n\t}\n}\n\nvoid SessionData::detach() {\n\tQMutexLocker lock(&_ownerMutex);\n\t_owner = nullptr;\n}\n\nSession::Session(\n\tnot_null<Instance*> instance,\n\tnot_null<QThread*> thread,\n\tShiftedDcId shiftedDcId,\n\tnot_null<Dcenter*> dc)\n: _instance(instance)\n, _shiftedDcId(shiftedDcId)\n, _dc(dc)\n, _data(std::make_shared<SessionData>(this))\n, _thread(thread)\n, _sender([=] { needToResumeAndSend(); }) {\n\trefreshOptions();\n\twatchDcKeyChanges();\n\twatchDcOptionsChanges();\n\tstart();\n}\n\nSession::~Session() {\n\tExpects(!_private);\n\n\tif (_myKeyCreation != CreatingKeyType::None) {\n\t\treleaseKeyCreationOnFail();\n\t}\n}\n\nvoid Session::watchDcKeyChanges() {\n\t_instance->dcTemporaryKeyChanged(\n\t) | rpl::filter([=](DcId dcId) {\n\t\treturn (dcId == _shiftedDcId) || (dcId == BareDcId(_shiftedDcId));\n\t}) | rpl::on_next([=] {\n\t\tDEBUG_LOG((\"AuthKey Info: dcTemporaryKeyChanged in Session %1\"\n\t\t\t).arg(_shiftedDcId));\n\t\tif (const auto captured = _private) {\n\t\t\tInvokeQueued(captured, [=] {\n\t\t\t\tDEBUG_LOG((\"AuthKey Info: calling Connection::updateAuthKey in Session %1\"\n\t\t\t\t\t).arg(_shiftedDcId));\n\t\t\t\tcaptured->updateAuthKey();\n\t\t\t});\n\t\t}\n\t}, _lifetime);\n}\n\nvoid Session::watchDcOptionsChanges() {\n\t_instance->dcOptions().changed(\n\t) | rpl::filter([=](DcId dcId) {\n\t\treturn (BareDcId(_shiftedDcId) == dcId) && (_private != nullptr);\n\t}) | rpl::on_next([=] {\n\t\tInvokeQueued(_private, [captured = _private] {\n\t\t\tcaptured->dcOptionsChanged();\n\t\t});\n\t}, _lifetime);\n\n\t_instance->dcOptions().cdnConfigChanged(\n\t) | rpl::filter([=] {\n\t\treturn (_private != nullptr)\n\t\t\t&& (_instance->dcOptions().dcType(_shiftedDcId) == DcType::Cdn);\n\t}) | rpl::on_next([=] {\n\t\tInvokeQueued(_private, [captured = _private] {\n\t\t\tcaptured->cdnConfigChanged();\n\t\t});\n\t}, _lifetime);\n}\n\nvoid Session::start() {\n\tkillConnection();\n\t_private = new SessionPrivate(\n\t\t_instance,\n\t\t_thread.get(),\n\t\t_data,\n\t\t_shiftedDcId);\n}\n\nvoid Session::restart() {\n\tif (_killed) {\n\t\tDEBUG_LOG((\"Session Error: can't restart a killed session\"));\n\t\treturn;\n\t}\n\trefreshOptions();\n\tif (const auto captured = _private) {\n\t\tInvokeQueued(captured, [=] {\n\t\t\tcaptured->restartNow();\n\t\t});\n\t}\n}\n\nvoid Session::refreshOptions() {\n\tauto &settings = Core::App().settings().proxy();\n\tconst auto &proxy = settings.selected();\n\tconst auto isEnabled = settings.isEnabled();\n\tconst auto proxyType = (isEnabled ? proxy.type : ProxyData::Type::None);\n\tconst auto useTcp = (proxyType != ProxyData::Type::Http);\n\tconst auto useHttp = (proxyType != ProxyData::Type::Mtproto);\n\tconst auto useIPv4 = true;\n\tconst auto useIPv6 = settings.tryIPv6();\n\t_data->setOptions(SessionOptions(\n\t\t_instance->systemLangCode(),\n\t\t_instance->cloudLangCode(),\n\t\t_instance->langPackName(),\n\t\t(isEnabled ? proxy : ProxyData()),\n\t\tuseIPv4,\n\t\tuseIPv6,\n\t\tuseHttp,\n\t\tuseTcp));\n}\n\nvoid Session::reInitConnection() {\n\tsetConnectionNotInited();\n\trestart();\n}\n\nvoid Session::setConnectionNotInited() {\n\t_dc->setConnectionInited(false);\n}\n\nvoid Session::stop() {\n\tif (_killed) {\n\t\tDEBUG_LOG((\"Session Error: can't stop a killed session\"));\n\t\treturn;\n\t}\n\tDEBUG_LOG((\"Session Info: stopping session dcWithShift %1\").arg(_shiftedDcId));\n\tkillConnection();\n}\n\nvoid Session::kill() {\n\tstop();\n\t_killed = true;\n\t_data->detach();\n\tDEBUG_LOG((\"Session Info: marked session dcWithShift %1 as killed\").arg(_shiftedDcId));\n}\n\nvoid Session::unpaused() {\n\tif (_needToReceive) {\n\t\t_needToReceive = false;\n\t\tInvokeQueued(this, [=] {\n\t\t\ttryToReceive();\n\t\t});\n\t}\n}\n\nvoid Session::sendAnything(crl::time msCanWait) {\n\tif (_killed) {\n\t\tDEBUG_LOG((\"Session Error: can't send anything in a killed session\"));\n\t\treturn;\n\t}\n\tconst auto ms = crl::now();\n\tif (_msSendCall) {\n\t\tif (ms > _msSendCall + _msWait) {\n\t\t\t_msWait = 0;\n\t\t} else {\n\t\t\t_msWait = (_msSendCall + _msWait) - ms;\n\t\t\tif (_msWait > msCanWait) {\n\t\t\t\t_msWait = msCanWait;\n\t\t\t}\n\t\t}\n\t} else {\n\t\t_msWait = msCanWait;\n\t}\n\tif (_msWait) {\n\t\tDEBUG_LOG((\"MTP Info: dcWithShift %1 can wait for %2ms from current %3\").arg(_shiftedDcId).arg(_msWait).arg(_msSendCall));\n\t\t_msSendCall = ms;\n\t\t_sender.callOnce(_msWait);\n\t} else {\n\t\tDEBUG_LOG((\"MTP Info: dcWithShift %1 stopped send timer, can wait for %2ms from current %3\").arg(_shiftedDcId).arg(_msWait).arg(_msSendCall));\n\t\t_sender.cancel();\n\t\t_msSendCall = 0;\n\t\tneedToResumeAndSend();\n\t}\n}\n\nvoid Session::needToResumeAndSend() {\n\tif (_killed) {\n\t\tDEBUG_LOG((\"Session Info: can't resume a killed session\"));\n\t\treturn;\n\t}\n\tif (!_private) {\n\t\tDEBUG_LOG((\"Session Info: resuming session dcWithShift %1\").arg(_shiftedDcId));\n\t\tstart();\n\t}\n\tconst auto captured = _private;\n\tconst auto ping = base::take(_ping);\n\tInvokeQueued(captured, [=] {\n\t\tif (ping) {\n\t\t\tcaptured->sendPingForce();\n\t\t} else {\n\t\t\tcaptured->tryToSend();\n\t\t}\n\t});\n}\n\nvoid Session::connectionStateChange(int newState) {\n\t_instance->onStateChange(_shiftedDcId, newState);\n}\n\nvoid Session::resetDone() {\n\t_instance->onSessionReset(_shiftedDcId);\n}\n\nvoid Session::cancel(mtpRequestId requestId, mtpMsgId msgId) {\n\tif (requestId) {\n\t\tQWriteLocker locker(_data->toSendMutex());\n\t\t_data->toSendMap().remove(requestId);\n\t}\n\tif (msgId) {\n\t\tQWriteLocker locker(_data->haveSentMutex());\n\t\t_data->haveSentMap().remove(msgId);\n\t}\n}\n\nvoid Session::ping() {\n\t_ping = true;\n\tsendAnything();\n}\n\nint32 Session::requestState(mtpRequestId requestId) const {\n\tint32 result = MTP::RequestSent;\n\n\tbool connected = false;\n\tif (_private) {\n\t\tconst auto s = _private->getState();\n\t\tif (s == ConnectedState) {\n\t\t\tconnected = true;\n\t\t} else if (s == ConnectingState || s == DisconnectedState) {\n\t\t\tif (result < 0 || result == MTP::RequestSent) {\n\t\t\t\tresult = MTP::RequestConnecting;\n\t\t\t}\n\t\t} else if (s < 0) {\n\t\t\tif ((result < 0 && s > result) || result == MTP::RequestSent) {\n\t\t\t\tresult = s;\n\t\t\t}\n\t\t}\n\t}\n\tif (!connected) {\n\t\treturn result;\n\t} else if (!requestId) {\n\t\treturn MTP::RequestSent;\n\t}\n\n\tQWriteLocker locker(_data->toSendMutex());\n\treturn _data->toSendMap().contains(requestId)\n\t\t? MTP::RequestSending\n\t\t: MTP::RequestSent;\n}\n\nint32 Session::getState() const {\n\tint32 result = -86400000;\n\n\tif (_private) {\n\t\tconst auto s = _private->getState();\n\t\tif (s == ConnectedState\n\t\t\t|| s == ConnectingState\n\t\t\t|| s == DisconnectedState) {\n\t\t\treturn s;\n\t\t} else if (s < 0) {\n\t\t\tif (result < 0 && s > result) {\n\t\t\t\tresult = s;\n\t\t\t}\n\t\t}\n\t}\n\tif (result == -86400000) {\n\t\tresult = DisconnectedState;\n\t}\n\treturn result;\n}\n\nQString Session::transport() const {\n\treturn _private ? _private->transport() : QString();\n}\n\nvoid Session::sendPrepared(\n\t\tconst SerializedRequest &request,\n\t\tcrl::time msCanWait) {\n\tDEBUG_LOG((\"MTP Info: adding request to toSendMap, msCanWait %1\"\n\t\t).arg(msCanWait));\n\t{\n\t\tQWriteLocker locker(_data->toSendMutex());\n\t\t_data->toSendMap().emplace(request->requestId, request);\n\t\t*(mtpMsgId*)(request->data() + 4) = 0;\n\t\t*(request->data() + 6) = 0;\n\t}\n\n\tDEBUG_LOG((\"MTP Info: added, requestId %1\").arg(request->requestId));\n\tif (msCanWait >= 0) {\n\t\tInvokeQueued(this, [=] {\n\t\t\tsendAnything(msCanWait);\n\t\t});\n\t}\n}\n\nCreatingKeyType Session::acquireKeyCreation(DcType type) {\n\tExpects(_myKeyCreation == CreatingKeyType::None);\n\n\t_myKeyCreation = _dc->acquireKeyCreation(type);\n\treturn _myKeyCreation;\n}\n\nbool Session::releaseKeyCreationOnDone(\n\t\tconst AuthKeyPtr &temporaryKey,\n\t\tconst AuthKeyPtr &persistentKeyUsedForBind) {\n\tExpects(_myKeyCreation != CreatingKeyType::None);\n\tExpects(persistentKeyUsedForBind != nullptr);\n\n\treturn releaseGenericKeyCreationOnDone(\n\t\ttemporaryKey,\n\t\tpersistentKeyUsedForBind);\n}\n\nbool Session::releaseCdnKeyCreationOnDone(\n\t\tconst AuthKeyPtr &temporaryKey) {\n\tExpects(_myKeyCreation == CreatingKeyType::TemporaryRegular);\n\n\treturn releaseGenericKeyCreationOnDone(temporaryKey, nullptr);\n}\n\nbool Session::releaseGenericKeyCreationOnDone(\n\t\tconst AuthKeyPtr &temporaryKey,\n\t\tconst AuthKeyPtr &persistentKeyUsedForBind) {\n\tconst auto wasKeyCreation = std::exchange(\n\t\t_myKeyCreation,\n\t\tCreatingKeyType::None);\n\tconst auto result = _dc->releaseKeyCreationOnDone(\n\t\twasKeyCreation,\n\t\ttemporaryKey,\n\t\tpersistentKeyUsedForBind);\n\n\tif (!result) {\n\t\tDEBUG_LOG((\"AuthKey Info: Persistent key changed \"\n\t\t\t\"while binding temporary, dcWithShift %1\"\n\t\t\t).arg(_shiftedDcId));\n\t\treturn false;\n\t}\n\n\tDEBUG_LOG((\"AuthKey Info: Session key bound, setting, dcWithShift %1\"\n\t\t).arg(_shiftedDcId));\n\n\tconst auto dcId = _dc->id();\n\tconst auto instance = _instance;\n\tInvokeQueued(instance, [=] {\n\t\tif (wasKeyCreation == CreatingKeyType::Persistent) {\n\t\t\tinstance->dcPersistentKeyChanged(dcId, persistentKeyUsedForBind);\n\t\t} else {\n\t\t\tinstance->dcTemporaryKeyChanged(dcId);\n\t\t}\n\t});\n\treturn true;\n}\n\nvoid Session::releaseKeyCreationOnFail() {\n\tExpects(_myKeyCreation != CreatingKeyType::None);\n\n\tconst auto wasKeyCreation = std::exchange(\n\t\t_myKeyCreation,\n\t\tCreatingKeyType::None);\n\t_dc->releaseKeyCreationOnFail(wasKeyCreation);\n}\n\nvoid Session::notifyDcConnectionInited() {\n\tDEBUG_LOG((\"MTP Info: MTProtoDC::connectionWasInited(), dcWithShift %1\"\n\t\t).arg(_shiftedDcId));\n\t_dc->setConnectionInited();\n}\n\nvoid Session::destroyTemporaryKey(uint64 keyId) {\n\tif (!_dc->destroyTemporaryKey(keyId)) {\n\t\treturn;\n\t}\n\tconst auto dcId = _dc->id();\n\tconst auto instance = _instance;\n\tInvokeQueued(instance, [=] {\n\t\tinstance->dcTemporaryKeyChanged(dcId);\n\t});\n}\n\nint32 Session::getDcWithShift() const {\n\treturn _shiftedDcId;\n}\n\nAuthKeyPtr Session::getTemporaryKey(TemporaryKeyType type) const {\n\treturn _dc->getTemporaryKey(type);\n}\n\nAuthKeyPtr Session::getPersistentKey() const {\n\treturn _dc->getPersistentKey();\n}\n\nbool Session::connectionInited() const {\n\treturn _dc->connectionInited();\n}\n\nvoid Session::tryToReceive() {\n\tif (_killed) {\n\t\tDEBUG_LOG((\"Session Error: can't receive in a killed session\"));\n\t\treturn;\n\t}\n\tif (paused()) {\n\t\t_needToReceive = true;\n\t\treturn;\n\t}\n\twhile (true) {\n\t\tauto lock = QWriteLocker(_data->haveReceivedMutex());\n\t\tconst auto messages = base::take(_data->haveReceivedMessages());\n\t\tlock.unlock();\n\t\tif (messages.empty()) {\n\t\t\tbreak;\n\t\t}\n\t\tconst auto guard = QPointer<Session>(this);\n\t\tconst auto instance = QPointer<Instance>(_instance);\n\t\tconst auto main = (_shiftedDcId == BareDcId(_shiftedDcId));\n\t\tfor (const auto &message : messages) {\n\t\t\tif (message.requestId) {\n\t\t\t\tinstance->processCallback(message);\n\t\t\t} else if (main) {\n\t\t\t\t// Process updates only in main session.\n\t\t\t\tinstance->processUpdate(message);\n\t\t\t}\n\t\t\tif (!instance) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (!guard) {\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nvoid Session::killConnection() {\n\tif (!_private) {\n\t\treturn;\n\t}\n\n\tbase::take(_private)->deleteLater();\n\n\tEnsures(_private == nullptr);\n}\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/session.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"mtproto/mtproto_response.h\"\n#include \"mtproto/mtproto_proxy_data.h\"\n#include \"mtproto/details/mtproto_serialized_request.h\"\n\n#include <QtCore/QTimer>\n\nnamespace MTP {\n\nclass Instance;\nclass AuthKey;\nusing AuthKeyPtr = std::shared_ptr<AuthKey>;\nenum class DcType;\n\nnamespace details {\n\nclass Dcenter;\nclass SessionPrivate;\n\nenum class TemporaryKeyType;\nenum class CreatingKeyType;\n\nstruct SessionOptions {\n\tSessionOptions() = default;\n\tSessionOptions(\n\t\tconst QString &systemLangCode,\n\t\tconst QString &cloudLangCode,\n\t\tconst QString &langPackName,\n\t\tconst ProxyData &proxy,\n\t\tbool useIPv4,\n\t\tbool useIPv6,\n\t\tbool useHttp,\n\t\tbool useTcp);\n\n\tQString systemLangCode;\n\tQString cloudLangCode;\n\tQString langPackName;\n\tProxyData proxy;\n\tbool useIPv4 = true;\n\tbool useIPv6 = true;\n\tbool useHttp = true;\n\tbool useTcp = true;\n\n};\n\nclass Session;\nclass SessionData final {\npublic:\n\texplicit SessionData(not_null<Session*> creator) : _owner(creator) {\n\t}\n\n\tvoid notifyConnectionInited(const SessionOptions &options);\n\tvoid setOptions(SessionOptions options) {\n\t\tQWriteLocker locker(&_optionsLock);\n\t\t_options = options;\n\t}\n\t[[nodiscard]] SessionOptions options() const {\n\t\tQReadLocker locker(&_optionsLock);\n\t\treturn _options;\n\t}\n\n\tnot_null<QReadWriteLock*> toSendMutex() {\n\t\treturn &_toSendLock;\n\t}\n\tnot_null<QReadWriteLock*> haveSentMutex() {\n\t\treturn &_haveSentLock;\n\t}\n\tnot_null<QReadWriteLock*> haveReceivedMutex() {\n\t\treturn &_haveReceivedLock;\n\t}\n\n\tbase::flat_map<mtpRequestId, SerializedRequest> &toSendMap() {\n\t\treturn _toSend;\n\t}\n\tbase::flat_map<mtpMsgId, SerializedRequest> &haveSentMap() {\n\t\treturn _haveSent;\n\t}\n\tstd::vector<Response> &haveReceivedMessages() {\n\t\treturn _receivedMessages;\n\t}\n\n\t// SessionPrivate -> Session interface.\n\tvoid queueTryToReceive();\n\tvoid queueNeedToResumeAndSend();\n\tvoid queueConnectionStateChange(int newState);\n\tvoid queueResetDone();\n\tvoid queueSendAnything(crl::time msCanWait = 0);\n\n\t[[nodiscard]] bool connectionInited() const;\n\t[[nodiscard]] AuthKeyPtr getPersistentKey() const;\n\t[[nodiscard]] AuthKeyPtr getTemporaryKey(TemporaryKeyType type) const;\n\t[[nodiscard]] CreatingKeyType acquireKeyCreation(DcType type);\n\t[[nodiscard]] bool releaseKeyCreationOnDone(\n\t\tconst AuthKeyPtr &temporaryKey,\n\t\tconst AuthKeyPtr &persistentKeyUsedForBind);\n\t[[nodiscard]] bool releaseCdnKeyCreationOnDone(\n\t\tconst AuthKeyPtr &temporaryKey);\n\tvoid releaseKeyCreationOnFail();\n\tvoid destroyTemporaryKey(uint64 keyId);\n\n\tvoid detach();\n\nprivate:\n\ttemplate <typename Callback>\n\tvoid withSession(Callback &&callback);\n\n\tSession *_owner = nullptr;\n\tmutable QMutex _ownerMutex;\n\n\tSessionOptions _options;\n\tmutable QReadWriteLock _optionsLock;\n\n\tbase::flat_map<mtpRequestId, SerializedRequest> _toSend; // map of request_id -> request, that is waiting to be sent\n\tQReadWriteLock _toSendLock;\n\n\tbase::flat_map<mtpMsgId, SerializedRequest> _haveSent; // map of msg_id -> request, that was sent\n\tQReadWriteLock _haveSentLock;\n\n\tstd::vector<Response> _receivedMessages; // list of responses / updates that should be processed in the main thread\n\tQReadWriteLock _haveReceivedLock;\n\n};\n\nclass Session final : public QObject {\npublic:\n\t// Main thread.\n\tSession(\n\t\tnot_null<Instance*> instance,\n\t\tnot_null<QThread*> thread,\n\t\tShiftedDcId shiftedDcId,\n\t\tnot_null<Dcenter*> dc);\n\t~Session();\n\n\tvoid start();\n\tvoid reInitConnection();\n\tvoid setConnectionNotInited();\n\n\tvoid restart();\n\tvoid refreshOptions();\n\tvoid stop();\n\tvoid kill();\n\n\tvoid unpaused();\n\n\t// Thread-safe.\n\t[[nodiscard]] ShiftedDcId getDcWithShift() const;\n\t[[nodiscard]] AuthKeyPtr getPersistentKey() const;\n\t[[nodiscard]] AuthKeyPtr getTemporaryKey(TemporaryKeyType type) const;\n\t[[nodiscard]] bool connectionInited() const;\n\tvoid sendPrepared(\n\t\tconst SerializedRequest &request,\n\t\tcrl::time msCanWait = 0);\n\n\t// SessionPrivate thread.\n\t[[nodiscard]] CreatingKeyType acquireKeyCreation(DcType type);\n\t[[nodiscard]] bool releaseKeyCreationOnDone(\n\t\tconst AuthKeyPtr &temporaryKey,\n\t\tconst AuthKeyPtr &persistentKeyUsedForBind);\n\t[[nodiscard]] bool releaseCdnKeyCreationOnDone(const AuthKeyPtr &temporaryKey);\n\tvoid releaseKeyCreationOnFail();\n\tvoid destroyTemporaryKey(uint64 keyId);\n\n\tvoid notifyDcConnectionInited();\n\n\tvoid ping();\n\tvoid cancel(mtpRequestId requestId, mtpMsgId msgId);\n\tint requestState(mtpRequestId requestId) const;\n\tint getState() const;\n\tQString transport() const;\n\n\tvoid tryToReceive();\n\tvoid needToResumeAndSend();\n\tvoid connectionStateChange(int newState);\n\tvoid resetDone();\n\tvoid sendAnything(crl::time msCanWait = 0);\n\nprivate:\n\tvoid watchDcKeyChanges();\n\tvoid watchDcOptionsChanges();\n\n\tvoid killConnection();\n\n\t[[nodiscard]] bool releaseGenericKeyCreationOnDone(\n\t\tconst AuthKeyPtr &temporaryKey,\n\t\tconst AuthKeyPtr &persistentKeyUsedForBind);\n\n\tconst not_null<Instance*> _instance;\n\tconst ShiftedDcId _shiftedDcId = 0;\n\tconst not_null<Dcenter*> _dc;\n\tconst std::shared_ptr<SessionData> _data;\n\tconst not_null<QThread*> _thread;\n\n\tSessionPrivate *_private = nullptr;\n\n\tbool _killed = false;\n\tbool _needToReceive = false;\n\n\tAuthKeyPtr _dcKeyForCheck;\n\tCreatingKeyType _myKeyCreation = CreatingKeyType();\n\n\tcrl::time _msSendCall = 0;\n\tcrl::time _msWait = 0;\n\n\tbool _ping = false;\n\n\tbase::Timer _sender;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/session_private.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"base/options.h\"\n#include \"mtproto/session_private.h\"\n\n#include \"mtproto/details/mtproto_bound_key_creator.h\"\n#include \"mtproto/details/mtproto_dcenter.h\"\n#include \"mtproto/details/mtproto_dump_to_text.h\"\n#include \"mtproto/details/mtproto_rsa_public_key.h\"\n#include \"mtproto/session.h\"\n#include \"mtproto/mtproto_response.h\"\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"mtproto/connection_abstract.h\"\n#include \"base/random.h\"\n#include \"base/qthelp_url.h\"\n#include \"base/openssl_help.h\"\n#include \"base/unixtime.h\"\n#include \"base/platform/base_platform_info.h\"\n\n#include <ksandbox.h>\n#include <zlib.h>\n\nnamespace MTP {\nnamespace details {\nnamespace {\n\nconstexpr auto kIntSize = static_cast<int>(sizeof(mtpPrime));\nconstexpr auto kWaitForBetterTimeout = crl::time(2000);\nconstexpr auto kMinConnectedTimeout = crl::time(1000);\nconstexpr auto kMaxConnectedTimeout = crl::time(8000);\nconstexpr auto kMinReceiveTimeout = crl::time(4000);\nconstexpr auto kMaxReceiveTimeout = crl::time(64000);\nconstexpr auto kMarkConnectionOldTimeout = crl::time(192000);\nconstexpr auto kPingDelayDisconnect = 60;\nconstexpr auto kPingSendAfter = 30 * crl::time(1000);\nconstexpr auto kPingSendAfterForce = 45 * crl::time(1000);\nconstexpr auto kTemporaryExpiresIn = TimeId(86400);\nconstexpr auto kBindKeyAdditionalExpiresTimeout = TimeId(30);\nconstexpr auto kKeyOldEnoughForDestroy = 60 * crl::time(1000);\nconstexpr auto kSentContainerLives = 600 * crl::time(1000);\nconstexpr auto kFastRequestDuration = crl::time(500);\n\n// If we can't connect for this time we will ask _instance to update config.\nconstexpr auto kRequestConfigTimeout = 8 * crl::time(1000);\n\n// Don't try to handle messages larger than this size.\nconstexpr auto kMaxMessageLength = 16 * 1024 * 1024;\n\n// How much time passed from send till we resend request or check its state.\nconstexpr auto kCheckSentRequestTimeout = 10 * crl::time(1000);\n\n// How much time to wait for some more requests,\n// when resending request or checking its state.\nconstexpr auto kSendStateRequestWaiting = crl::time(1000);\n\n// How much time to wait for some more requests, when sending msg acks.\nconstexpr auto kAckSendWaiting = 10 * crl::time(1000);\n\nconstexpr auto kCutContainerOnSize = 16 * 1024;\n\nauto SyncTimeRequestDuration = kFastRequestDuration;\n\nusing namespace details;\n\n[[nodiscard]] QString LogIdsVector(const QVector<MTPlong> &ids) {\n\tif (!ids.size()) return \"[]\";\n\tauto idsStr = QString(\"[%1\").arg(ids.cbegin()->v);\n\tfor (const auto &id : ids) {\n\t\tidsStr += QString(\", %2\").arg(id.v);\n\t}\n\treturn idsStr + \"]\";\n}\n\n[[nodiscard]] QString ComputeAppVersion() {\n#if defined Q_OS_WIN && defined Q_PROCESSOR_X86_64\n\tconst auto arch = u\" x64\"_q;\n#elif (defined Q_OS_WIN && defined Q_PROCESSOR_X86_32) || defined Q_PROCESSOR_X86_64\n\tconst auto arch = QString();\n#else\n\tconst auto arch = ' ' + QSysInfo::buildCpuArchitecture();\n#endif\n\treturn QString::fromLatin1(AppVersionStr) + arch + ([] {\n#if defined OS_MAC_STORE\n\t\treturn u\" Mac App Store\"_q;\n#elif defined OS_WIN_STORE // OS_MAC_STORE\n\t\treturn u\" Microsoft Store\"_q;\n#else // OS_MAC_STORE || OS_WIN_STORE\n\t\treturn KSandbox::isFlatpak()\n\t\t\t? u\" Flatpak\"_q\n\t\t\t: KSandbox::isSnap()\n\t\t\t? u\" Snap\"_q\n\t\t\t: QString();\n#endif // OS_MAC_STORE || OS_WIN_STORE\n\t})();\n}\n\nvoid WrapInvokeAfter(\n\t\tSerializedRequest &to,\n\t\tconst SerializedRequest &from,\n\t\tconst base::flat_map<mtpMsgId, SerializedRequest> &haveSent,\n\t\tint32 skipBeforeRequest = 0) {\n\tconst auto afterId = *(mtpMsgId*)(from->after->data() + 4);\n\tconst auto i = afterId ? haveSent.find(afterId) : haveSent.end();\n\tint32 size = to->size(), lenInInts = (tl::count_length(from) >> 2), headlen = 4, fulllen = headlen + lenInInts;\n\tif (i == haveSent.end()) { // no invoke after or such msg was not sent or was completed recently\n\t\tto->resize(size + fulllen + skipBeforeRequest);\n\t\tif (skipBeforeRequest) {\n\t\t\tmemcpy(to->data() + size, from->constData() + 4, headlen * sizeof(mtpPrime));\n\t\t\tmemcpy(to->data() + size + headlen + skipBeforeRequest, from->constData() + 4 + headlen, lenInInts * sizeof(mtpPrime));\n\t\t} else {\n\t\t\tmemcpy(to->data() + size, from->constData() + 4, fulllen * sizeof(mtpPrime));\n\t\t}\n\t} else {\n\t\tto->resize(size + fulllen + skipBeforeRequest + 3);\n\t\tmemcpy(to->data() + size, from->constData() + 4, headlen * sizeof(mtpPrime));\n\t\t(*to)[size + 3] += 3 * sizeof(mtpPrime);\n\t\t*((mtpTypeId*)&((*to)[size + headlen + skipBeforeRequest])) = mtpc_invokeAfterMsg;\n\t\tmemcpy(to->data() + size + headlen + skipBeforeRequest + 1, &afterId, 2 * sizeof(mtpPrime));\n\t\tmemcpy(to->data() + size + headlen + skipBeforeRequest + 3, from->constData() + 4 + headlen, lenInInts * sizeof(mtpPrime));\n\t\tif (size + 3 != 7) (*to)[7] += 3 * sizeof(mtpPrime);\n\t}\n}\n\n[[nodiscard]] bool ConstTimeIsDifferent(\n\t\tconst void *a,\n\t\tconst void *b,\n\t\tsize_t size) {\n\tauto ca = reinterpret_cast<const char*>(a);\n\tauto cb = reinterpret_cast<const char*>(b);\n\tvolatile auto different = false;\n\tfor (const auto ce = ca + size; ca != ce; ++ca, ++cb) {\n\t\tdifferent = different | (*ca != *cb);\n\t}\n\treturn different;\n}\n\nbase::options::toggle OptionPreferIPv6({\n\t.id = kOptionPreferIPv6,\n\t.name = \"Prefer IPv6\",\n\t.description = \"Prefer IPv6 if it is available. Require \\\"Try connecting through IPv6\\\" to be enabled\",\n});\n\n} // namespace\n\nconst char kOptionPreferIPv6[] = \"prefer-ipv6\";\n\nSessionPrivate::SessionPrivate(\n\tnot_null<Instance*> instance,\n\tnot_null<QThread*> thread,\n\tstd::shared_ptr<SessionData> data,\n\tShiftedDcId shiftedDcId)\n: QObject(nullptr)\n, _instance(instance)\n, _shiftedDcId(shiftedDcId)\n, _realDcType(_instance->dcOptions().dcType(_shiftedDcId))\n, _currentDcType(_realDcType)\n, _state(DisconnectedState)\n, _retryTimer(thread, [=] { retryByTimer(); })\n, _oldConnectionTimer(thread, [=] { markConnectionOld(); })\n, _waitForConnectedTimer(thread, [=] { waitConnectedFailed(); })\n, _waitForReceivedTimer(thread, [=] { waitReceivedFailed(); })\n, _waitForBetterTimer(thread, [=] { waitBetterFailed(); })\n, _waitForReceived(kMinReceiveTimeout)\n, _waitForConnected(kMinConnectedTimeout)\n, _pingSender(thread, [=] { sendPingByTimer(); })\n, _checkSentRequestsTimer(thread, [=] { checkSentRequests(); })\n, _clearOldContainersTimer(thread, [=] { clearOldContainers(); })\n, _sessionData(std::move(data)) {\n\tExpects(_shiftedDcId != 0);\n\n\tmoveToThread(thread);\n\n\tInvokeQueued(this, [=] {\n\t\t_clearOldContainersTimer.callEach(kSentContainerLives);\n\t\tconnectToServer();\n\t});\n}\n\nSessionPrivate::~SessionPrivate() {\n\treleaseKeyCreationOnFail();\n\tdoDisconnect();\n\n\tExpects(!_connection);\n\tExpects(_testConnections.empty());\n}\n\nvoid SessionPrivate::appendTestConnection(\n\t\tDcOptions::Variants::Protocol protocol,\n\t\tconst QString &ip,\n\t\tint port,\n\t\tconst bytes::vector &protocolSecret) {\n\tQWriteLocker lock(&_stateMutex);\n\n\tconst auto priority = (qthelp::is_ipv6(ip) ? (OptionPreferIPv6.value() ? 2 : 0) : 1)\n\t\t+ (protocol == DcOptions::Variants::Tcp ? 1 : 0)\n\t\t+ (protocolSecret.empty() ? 0 : 1);\n\t_testConnections.push_back({\n\t\tAbstractConnection::Create(\n\t\t\t_instance,\n\t\t\tprotocol,\n\t\t\tthread(),\n\t\t\tprotocolSecret,\n\t\t\t_options->proxy),\n\t\tpriority\n\t});\n\tconst auto weak = _testConnections.back().data.get();\n\tconnect(weak, &AbstractConnection::error, [=](int errorCode) {\n\t\tonError(weak, errorCode);\n\t});\n\tconnect(weak, &AbstractConnection::receivedSome, [=] {\n\t\tonReceivedSome();\n\t});\n\t_firstSentAt = 0;\n\tif (_oldConnection) {\n\t\t_oldConnection = false;\n\t\tDEBUG_LOG((\"This connection marked as not old!\"));\n\t}\n\t_oldConnectionTimer.callOnce(kMarkConnectionOldTimeout);\n\tconnect(weak, &AbstractConnection::connected, [=] {\n\t\tonConnected(weak);\n\t});\n\tconnect(weak, &AbstractConnection::disconnected, [=] {\n\t\tonDisconnected(weak);\n\t});\n\tconnect(weak, &AbstractConnection::syncTimeRequest, [=] {\n\t\tInvokeQueued(_instance, [instance = _instance] {\n\t\t\tinstance->syncHttpUnixtime();\n\t\t});\n\t});\n\n\tconst auto protocolForFiles = isMediaClusterDcId(_shiftedDcId)\n\t\t//|| isUploadDcId(_shiftedDcId)\n\t\t|| (_realDcType == DcType::Cdn);\n\tconst auto protocolDcId = getProtocolDcId();\n\tInvokeQueued(_testConnections.back().data, [=] {\n\t\tweak->connectToServer(\n\t\t\tip,\n\t\t\tport,\n\t\t\tprotocolSecret,\n\t\t\tprotocolDcId,\n\t\t\tprotocolForFiles);\n\t});\n}\n\nint16 SessionPrivate::getProtocolDcId() const {\n\tconst auto dcId = BareDcId(_shiftedDcId);\n\tconst auto simpleDcId = isTemporaryDcId(dcId)\n\t\t? getRealIdFromTemporaryDcId(dcId)\n\t\t: dcId;\n\tconst auto testedDcId = _instance->isTestMode()\n\t\t? (kTestModeDcIdShift + simpleDcId)\n\t\t: simpleDcId;\n\treturn (_currentDcType == DcType::MediaCluster)\n\t\t? -testedDcId\n\t\t: testedDcId;\n}\n\nvoid SessionPrivate::checkSentRequests() {\n\tconst auto now = crl::now();\n\tconst auto checkTime = now - kCheckSentRequestTimeout;\n\tif (_bindMsgId && _bindMessageSent < checkTime) {\n\t\tDEBUG_LOG((\"MTP Info: \"\n\t\t\t\"Request state while key is not bound, restarting.\"));\n\t\trestart();\n\t\t_checkSentRequestsTimer.callOnce(kCheckSentRequestTimeout);\n\t\treturn;\n\t}\n\tauto requesting = false;\n\tauto nextTimeout = kCheckSentRequestTimeout;\n\t{\n\t\tQReadLocker locker(_sessionData->haveSentMutex());\n\t\tauto &haveSent = _sessionData->haveSentMap();\n\t\tfor (const auto &[msgId, request] : haveSent) {\n\t\t\tif (request->lastSentTime <= checkTime) {\n\t\t\t\t// Need to check state.\n\t\t\t\trequest->lastSentTime = now;\n\t\t\t\tif (_stateRequestData.emplace(msgId).second) {\n\t\t\t\t\trequesting = true;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tnextTimeout = std::min(request->lastSentTime - checkTime, nextTimeout);\n\t\t\t}\n\t\t}\n\t}\n\tif (requesting) {\n\t\t_sessionData->queueSendAnything(kSendStateRequestWaiting);\n\t}\n\tif (nextTimeout < kCheckSentRequestTimeout) {\n\t\t_checkSentRequestsTimer.callOnce(nextTimeout);\n\t}\n}\n\nvoid SessionPrivate::clearOldContainers() {\n\tauto resent = false;\n\tauto nextTimeout = kSentContainerLives;\n\tconst auto now = crl::now();\n\tconst auto checkTime = now - kSentContainerLives;\n\tfor (auto i = _sentContainers.begin(); i != _sentContainers.end();) {\n\t\tif (i->second.sent <= checkTime) {\n\t\t\tDEBUG_LOG((\"MTP Info: Removing old container with resending %1, \"\n\t\t\t\t\"sent: %2, now: %3, current unixtime: %4\"\n\t\t\t\t).arg(i->first\n\t\t\t\t).arg(i->second.sent\n\t\t\t\t).arg(now\n\t\t\t\t).arg(base::unixtime::now()));\n\n\t\t\tconst auto ids = std::move(i->second.messages);\n\t\t\ti = _sentContainers.erase(i);\n\n\t\t\tresent = resent || !ids.empty();\n\t\t\tfor (const auto innerMsgId : ids) {\n\t\t\t\tresend(innerMsgId, -1);\n\t\t\t}\n\t\t} else {\n\t\t\tnextTimeout = std::min(i->second.sent - checkTime, nextTimeout);\n\t\t\t++i;\n\t\t}\n\t}\n\tif (resent) {\n\t\t_sessionData->queueNeedToResumeAndSend();\n\t}\n\tif (nextTimeout < kSentContainerLives) {\n\t\t_clearOldContainersTimer.callOnce(nextTimeout);\n\t} else if (!_clearOldContainersTimer.isActive()) {\n\t\t_clearOldContainersTimer.callEach(nextTimeout);\n\t}\n}\n\nvoid SessionPrivate::destroyAllConnections() {\n\tclearUnboundKeyCreator();\n\t_waitForBetterTimer.cancel();\n\t_waitForReceivedTimer.cancel();\n\t_waitForConnectedTimer.cancel();\n\t_testConnections.clear();\n\t_connection = nullptr;\n}\n\nvoid SessionPrivate::cdnConfigChanged() {\n\tconnectToServer(true);\n}\n\nint32 SessionPrivate::getShiftedDcId() const {\n\treturn _shiftedDcId;\n}\n\nvoid SessionPrivate::dcOptionsChanged() {\n\t_retryTimeout = 1;\n\tconnectToServer(true);\n}\n\nint32 SessionPrivate::getState() const {\n\tQReadLocker lock(&_stateMutex);\n\tint32 result = _state;\n\tif (_state < 0) {\n\t\tif (_retryTimer.isActive()) {\n\t\t\tresult = int32(crl::now() - _retryWillFinish);\n\t\t\tif (result >= 0) {\n\t\t\t\tresult = -1;\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nQString SessionPrivate::transport() const {\n\tQReadLocker lock(&_stateMutex);\n\tif (!_connection || (_state < 0)) {\n\t\treturn QString();\n\t}\n\n\tAssert(_options != nullptr);\n\treturn _connection->transport();\n}\n\nbool SessionPrivate::setState(int state, int ifState) {\n\tif (ifState != kUpdateStateAlways) {\n\t\tQReadLocker lock(&_stateMutex);\n\t\tif (_state != ifState) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tQWriteLocker lock(&_stateMutex);\n\tif (_state == state) {\n\t\treturn false;\n\t}\n\t_state = state;\n\tif (state < 0) {\n\t\t_retryTimeout = -state;\n\t\t_retryTimer.callOnce(_retryTimeout);\n\t\t_retryWillFinish = crl::now() + _retryTimeout;\n\t}\n\tlock.unlock();\n\n\t_sessionData->queueConnectionStateChange(state);\n\treturn true;\n}\n\nvoid SessionPrivate::resetSession() {\n\tMTP_LOG(_shiftedDcId, (\"Resetting session!\"));\n\t_needSessionReset = false;\n\n\tDEBUG_LOG((\"MTP Info: creating new session in resetSession.\"));\n\tchangeSessionId();\n\n\t_sessionData->queueResetDone();\n}\n\nvoid SessionPrivate::changeSessionId() {\n\tauto sessionId = _sessionId;\n\tdo {\n\t\tsessionId = base::RandomValue<uint64>();\n\t} while (_sessionId == sessionId);\n\n\tDEBUG_LOG((\"MTP Info: setting server_session: %1\").arg(sessionId));\n\n\t_sessionId = sessionId;\n\t_messagesCounter = 0;\n\t_sessionMarkedAsStarted = false;\n\t_ackRequestData.clear();\n\t_resendRequestData.clear();\n\t_stateRequestData.clear();\n\t_receivedMessageIds.clear();\n}\n\nuint32 SessionPrivate::nextRequestSeqNumber(bool needAck) {\n\tconst auto result = _messagesCounter;\n\t_messagesCounter += (needAck ? 1 : 0);\n\treturn result * 2 + (needAck ? 1 : 0);\n}\n\nbool SessionPrivate::realDcTypeChanged() {\n\tconst auto now = _instance->dcOptions().dcType(_shiftedDcId);\n\tif (_realDcType == now) {\n\t\treturn false;\n\t}\n\t_realDcType = now;\n\treturn true;\n}\n\nbool SessionPrivate::markSessionAsStarted() {\n\tif (_sessionMarkedAsStarted) {\n\t\treturn false;\n\t}\n\t_sessionMarkedAsStarted = true;\n\treturn true;\n}\n\nmtpMsgId SessionPrivate::prepareToSend(\n\t\tSerializedRequest &request,\n\t\tmtpMsgId currentLastId,\n\t\tbool forceNewMsgId) {\n\tExpects(request->size() > 8);\n\n\tif (const auto msgId = request.getMsgId()) {\n\t\t// resending this request\n\t\tconst auto i = _resendingIds.find(msgId);\n\t\tif (i != _resendingIds.cend()) {\n\t\t\t_resendingIds.erase(i);\n\t\t}\n\n\t\treturn (forceNewMsgId || msgId > currentLastId)\n\t\t\t? replaceMsgId(request, currentLastId)\n\t\t\t: msgId;\n\t}\n\trequest.setMsgId(currentLastId);\n\trequest.setSeqNo(nextRequestSeqNumber(request.needAck()));\n\tif (request->requestId) {\n\t\tMTP_LOG(_shiftedDcId, (\"[r%1] msg_id 0 -> %2\").arg(request->requestId).arg(currentLastId));\n\t}\n\treturn currentLastId;\n}\n\nmtpMsgId SessionPrivate::replaceMsgId(SerializedRequest &request, mtpMsgId newId) {\n\tExpects(request->size() > 8);\n\n\tconst auto oldMsgId = request.getMsgId();\n\tif (oldMsgId == newId) {\n\t\treturn newId;\n\t}\n\t// haveSentMutex() was locked in tryToSend()\n\tauto &haveSent = _sessionData->haveSentMap();\n\n\twhile (_resendingIds.contains(newId)\n\t\t|| _ackedIds.contains(newId)\n\t\t|| haveSent.contains(newId)) {\n\t\tnewId = base::unixtime::mtproto_msg_id();\n\t}\n\n\tMTP_LOG(_shiftedDcId, (\"[r%1] msg_id %2 -> %3\"\n\t\t).arg(request->requestId\n\t\t).arg(oldMsgId\n\t\t).arg(newId));\n\n\tconst auto i = _resendingIds.find(oldMsgId);\n\tif (i != _resendingIds.end()) {\n\t\tconst auto requestId = i->second;\n\t\t_resendingIds.erase(i);\n\t\t_resendingIds.emplace(newId, requestId);\n\t}\n\n\tconst auto j = _ackedIds.find(oldMsgId);\n\tif (j != _ackedIds.end()) {\n\t\tconst auto requestId = j->second;\n\t\t_ackedIds.erase(j);\n\t\t_ackedIds.emplace(newId, requestId);\n\t}\n\n\tconst auto k = haveSent.find(oldMsgId);\n\tif (k != haveSent.end()) {\n\t\tconst auto request = k->second;\n\t\thaveSent.erase(k);\n\t\thaveSent.emplace(newId, request);\n\t}\n\tfor (auto &[msgId, container] : _sentContainers) {\n\t\tfor (auto &innerMsgId : container.messages) {\n\t\t\tif (innerMsgId == oldMsgId) {\n\t\t\t\tinnerMsgId = newId;\n\t\t\t}\n\t\t}\n\t}\n\trequest.setMsgId(newId);\n\trequest.setSeqNo(nextRequestSeqNumber(request.needAck()));\n\treturn newId;\n}\n\nmtpMsgId SessionPrivate::placeToContainer(\n\t\tSerializedRequest &toSendRequest,\n\t\tmtpMsgId &bigMsgId,\n\t\tbool forceNewMsgId,\n\t\tSerializedRequest &req) {\n\tconst auto msgId = prepareToSend(req, bigMsgId, forceNewMsgId);\n\tif (msgId >= bigMsgId) {\n\t\tbigMsgId = base::unixtime::mtproto_msg_id();\n\t}\n\n\tuint32 from = toSendRequest->size(), len = req.messageSize();\n\ttoSendRequest->resize(from + len);\n\tmemcpy(toSendRequest->data() + from, req->constData() + 4, len * sizeof(mtpPrime));\n\n\treturn msgId;\n}\n\nMTPVector<MTPJSONObjectValue> SessionPrivate::prepareInitParams() {\n\tconst auto local = QDateTime::currentDateTime();\n\tconst auto utc = QDateTime(local.date(), local.time(), Qt::UTC);\n\tconst auto shift = base::unixtime::now() - (TimeId)::time(nullptr);\n\tconst auto delta = int(utc.toSecsSinceEpoch()) - int(local.toSecsSinceEpoch()) - shift;\n\tauto sliced = delta;\n\twhile (sliced < -12 * 3600) {\n\t\tsliced += 24 * 3600;\n\t}\n\twhile (sliced > 14 * 3600) {\n\t\tsliced -= 24 * 3600;\n\t}\n\tconst auto sign = (sliced < 0) ? -1 : 1;\n\tconst auto rounded = base::SafeRound(std::abs(sliced) / 900.)\n\t\t* 900\n\t\t* sign;\n\treturn MTP_vector<MTPJSONObjectValue>(\n\t\t1,\n\t\tMTP_jsonObjectValue(\n\t\t\tMTP_string(\"tz_offset\"),\n\t\t\tMTP_jsonNumber(MTP_double(rounded))));\n}\n\nvoid SessionPrivate::tryToSend() {\n\tDEBUG_LOG((\"MTP Info: tryToSend for dc %1.\").arg(_shiftedDcId));\n\tif (!_connection) {\n\t\tDEBUG_LOG((\"MTP Info: not yet connected in dc %1.\").arg(_shiftedDcId));\n\t\treturn;\n\t} else if (!_keyId) {\n\t\tDEBUG_LOG((\"MTP Info: not yet with auth key in dc %1.\").arg(_shiftedDcId));\n\t\treturn;\n\t}\n\n\tconst auto needsLayer = !_sessionData->connectionInited();\n\tconst auto state = getState();\n\tconst auto sendOnlyFirstPing = (state != ConnectedState);\n\tconst auto sendAll = !sendOnlyFirstPing && !_keyCreator;\n\tconst auto isMainSession = (GetDcIdShift(_shiftedDcId) == 0);\n\tif (sendOnlyFirstPing && !_pingIdToSend) {\n\t\tDEBUG_LOG((\"MTP Info: dc %1 not sending, waiting for Connected state, state: %2\").arg(_shiftedDcId).arg(state));\n\t\treturn; // just do nothing, if is not connected yet\n\t} else if (isMainSession\n\t\t&& !sendOnlyFirstPing\n\t\t&& !_pingIdToSend\n\t\t&& !_pingId\n\t\t&& _pingSendAt <= crl::now()) {\n\t\t_pingIdToSend = base::RandomValue<mtpPingId>();\n\t}\n\tconst auto forceNewMsgId = sendAll && markSessionAsStarted();\n\tif (forceNewMsgId && _keyCreator) {\n\t\t_keyCreator->restartBinder();\n\t}\n\n\tauto pingRequest = SerializedRequest();\n\tauto ackRequest = SerializedRequest();\n\tauto resendRequest = SerializedRequest();\n\tauto stateRequest = SerializedRequest();\n\tauto httpWaitRequest = SerializedRequest();\n\tauto bindDcKeyRequest = SerializedRequest();\n\tif (_pingIdToSend) {\n\t\tif (sendOnlyFirstPing || !isMainSession) {\n\t\t\tDEBUG_LOG((\"MTP Info: sending ping, ping_id: %1\"\n\t\t\t\t).arg(_pingIdToSend));\n\t\t\tpingRequest = SerializedRequest::Serialize(MTPPing(\n\t\t\t\tMTP_long(_pingIdToSend)\n\t\t\t));\n\t\t} else {\n\t\t\tDEBUG_LOG((\"MTP Info: sending ping_delay_disconnect, \"\n\t\t\t\t\"ping_id: %1\").arg(_pingIdToSend));\n\t\t\tpingRequest = SerializedRequest::Serialize(MTPPing_delay_disconnect(\n\t\t\t\tMTP_long(_pingIdToSend),\n\t\t\t\tMTP_int(kPingDelayDisconnect)));\n\t\t\t_pingSender.callOnce(kPingSendAfterForce);\n\t\t}\n\t\t_pingSendAt = pingRequest->lastSentTime + kPingSendAfter;\n\t\t_pingId = base::take(_pingIdToSend);\n\t} else if (!sendAll) {\n\t\tDEBUG_LOG((\"MTP Info: dc %1 sending only service or bind.\"\n\t\t\t).arg(_shiftedDcId));\n\t} else {\n\t\tDEBUG_LOG((\"MTP Info: dc %1 trying to send after ping, state: %2\"\n\t\t\t).arg(_shiftedDcId\n\t\t\t).arg(state));\n\t}\n\n\tif (!sendOnlyFirstPing) {\n\t\tif (!_ackRequestData.isEmpty()) {\n\t\t\tackRequest = SerializedRequest::Serialize(MTPMsgsAck(\n\t\t\t\tMTP_msgs_ack(MTP_vector<MTPlong>(\n\t\t\t\t\tbase::take(_ackRequestData)))));\n\t\t}\n\t\tif (!_resendRequestData.isEmpty()) {\n\t\t\tresendRequest = SerializedRequest::Serialize(MTPMsgResendReq(\n\t\t\t\tMTP_msg_resend_req(MTP_vector<MTPlong>(\n\t\t\t\t\tbase::take(_resendRequestData)))));\n\t\t}\n\t\tif (!_stateRequestData.empty()) {\n\t\t\tauto ids = QVector<MTPlong>();\n\t\t\tids.reserve(_stateRequestData.size());\n\t\t\tfor (const auto id : base::take(_stateRequestData)) {\n\t\t\t\tids.push_back(MTP_long(id));\n\t\t\t}\n\t\t\tstateRequest = SerializedRequest::Serialize(MTPMsgsStateReq(\n\t\t\t\tMTP_msgs_state_req(MTP_vector<MTPlong>(ids))));\n\t\t}\n\t\tif (_connection->usingHttpWait()) {\n\t\t\thttpWaitRequest = SerializedRequest::Serialize(MTPHttpWait(\n\t\t\t\tMTP_http_wait(MTP_int(100), MTP_int(30), MTP_int(25000))));\n\t\t}\n\t\tif (!_bindMsgId && _keyCreator && _keyCreator->readyToBind()) {\n\t\t\tbindDcKeyRequest = _keyCreator->prepareBindRequest(\n\t\t\t\t_encryptionKey,\n\t\t\t\t_sessionId);\n\n\t\t\t// This is a special request with msgId used inside the message\n\t\t\t// body, so it is prepared already with a msgId and we place\n\t\t\t// seqNo for it manually here.\n\t\t\tbindDcKeyRequest.setSeqNo(\n\t\t\t\tnextRequestSeqNumber(bindDcKeyRequest.needAck()));\n\t\t}\n\t}\n\n\tMTPInitConnection<SerializedRequest> initWrapper;\n\tint32 initSize = 0, initSizeInInts = 0;\n\tif (needsLayer) {\n\t\tAssert(_options != nullptr);\n\t\tconst auto systemLangCode = _options->systemLangCode;\n\t\tconst auto cloudLangCode = _options->cloudLangCode;\n\t\tconst auto langPackName = _options->langPackName;\n\t\tconst auto deviceModel = (_currentDcType == DcType::Cdn)\n\t\t\t? \"n/a\"\n\t\t\t: _instance->deviceModel();\n\t\tconst auto systemVersion = (_currentDcType == DcType::Cdn)\n\t\t\t? \"n/a\"\n\t\t\t: _instance->systemVersion();\n\t\tconst auto appVersion = ComputeAppVersion();\n\t\tconst auto proxyType = _options->proxy.type;\n\t\tconst auto mtprotoProxy = (proxyType == ProxyData::Type::Mtproto);\n\t\tconst auto clientProxyFields = mtprotoProxy\n\t\t\t? MTP_inputClientProxy(\n\t\t\t\tMTP_string(_options->proxy.host),\n\t\t\t\tMTP_int(_options->proxy.port))\n\t\t\t: MTPInputClientProxy();\n\t\tusing Flag = MTPInitConnection<SerializedRequest>::Flag;\n\t\tinitWrapper = MTPInitConnection<SerializedRequest>(\n\t\t\tMTP_flags(Flag::f_params\n\t\t\t\t| (mtprotoProxy ? Flag::f_proxy : Flag(0))),\n\t\t\tMTP_int(ApiId),\n\t\t\tMTP_string(deviceModel),\n\t\t\tMTP_string(systemVersion),\n\t\t\tMTP_string(appVersion),\n\t\t\tMTP_string(systemLangCode),\n\t\t\tMTP_string(langPackName),\n\t\t\tMTP_string(cloudLangCode),\n\t\t\tclientProxyFields,\n\t\t\tMTP_jsonObject(prepareInitParams()),\n\t\t\tSerializedRequest());\n\t\tinitSizeInInts = (tl::count_length(initWrapper) >> 2) + 2;\n\t\tinitSize = initSizeInInts * sizeof(mtpPrime);\n\t}\n\n\tauto needAnyResponse = false;\n\tauto someSkipped = false;\n\tSerializedRequest toSendRequest;\n\t{\n\t\tQWriteLocker locker1(_sessionData->toSendMutex());\n\n\t\tauto scheduleCheckSentRequests = false;\n\n\t\tauto toSendDummy = base::flat_map<mtpRequestId, SerializedRequest>();\n\t\tauto &toSend = sendAll\n\t\t\t? _sessionData->toSendMap()\n\t\t\t: toSendDummy;\n\t\tif (!sendAll) {\n\t\t\tlocker1.unlock();\n\t\t}\n\n\t\tauto totalSending = int(toSend.size());\n\t\tauto sendingFrom = begin(toSend);\n\t\tauto sendingTill = end(toSend);\n\t\tauto combinedLength = 0;\n\t\tfor (auto i = sendingFrom; i != sendingTill; ++i) {\n\t\t\tcombinedLength += i->second->size();\n\t\t\tif (combinedLength >= kCutContainerOnSize) {\n\t\t\t\t++i;\n\t\t\t\tif (const auto skipping = int(sendingTill - i)) {\n\t\t\t\t\tsendingTill = i;\n\t\t\t\t\ttotalSending -= skipping;\n\t\t\t\t\tAssert(totalSending > 0);\n\t\t\t\t\tsomeSkipped = true;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tauto sendingRange = ranges::make_subrange(sendingFrom, sendingTill);\n\t\tconst auto sendingCount = totalSending;\n\t\tif (pingRequest) ++totalSending;\n\t\tif (ackRequest) ++totalSending;\n\t\tif (resendRequest) ++totalSending;\n\t\tif (stateRequest) ++totalSending;\n\t\tif (httpWaitRequest) ++totalSending;\n\t\tif (bindDcKeyRequest) ++totalSending;\n\n\t\tif (!totalSending) {\n\t\t\treturn; // nothing to send\n\t\t}\n\n\t\tconst auto first = pingRequest\n\t\t\t? pingRequest\n\t\t\t: ackRequest\n\t\t\t? ackRequest\n\t\t\t: resendRequest\n\t\t\t? resendRequest\n\t\t\t: stateRequest\n\t\t\t? stateRequest\n\t\t\t: httpWaitRequest\n\t\t\t? httpWaitRequest\n\t\t\t: bindDcKeyRequest\n\t\t\t? bindDcKeyRequest\n\t\t\t: sendingRange.begin()->second;\n\t\tif (totalSending == 1 && !first->forceSendInContainer) {\n\t\t\ttoSendRequest = first;\n\t\t\tif (sendAll) {\n\t\t\t\ttoSend.erase(sendingFrom, sendingTill);\n\t\t\t\tlocker1.unlock();\n\t\t\t}\n\n\t\t\tconst auto msgId = prepareToSend(\n\t\t\t\ttoSendRequest,\n\t\t\t\tbase::unixtime::mtproto_msg_id(),\n\t\t\t\tforceNewMsgId && !bindDcKeyRequest);\n\t\t\tif (bindDcKeyRequest) {\n\t\t\t\t_bindMsgId = msgId;\n\t\t\t\t_bindMessageSent = crl::now();\n\t\t\t\tneedAnyResponse = true;\n\t\t\t} else if (pingRequest) {\n\t\t\t\t_pingMsgId = msgId;\n\t\t\t\tneedAnyResponse = true;\n\t\t\t} else if (stateRequest || resendRequest) {\n\t\t\t\t_stateAndResendRequests.emplace(\n\t\t\t\t\tmsgId,\n\t\t\t\t\tstateRequest ? stateRequest : resendRequest);\n\t\t\t\tneedAnyResponse = true;\n\t\t\t}\n\n\t\t\tif (toSendRequest->requestId) {\n\t\t\t\tif (toSendRequest.needAck()) {\n\t\t\t\t\ttoSendRequest->lastSentTime = crl::now();\n\n\t\t\t\t\tQWriteLocker locker2(_sessionData->haveSentMutex());\n\t\t\t\t\tauto &haveSent = _sessionData->haveSentMap();\n\t\t\t\t\thaveSent.emplace(msgId, toSendRequest);\n\t\t\t\t\tscheduleCheckSentRequests = true;\n\n\t\t\t\t\tconst auto wrapLayer = needsLayer && toSendRequest->needsLayer;\n\t\t\t\t\tif (toSendRequest->after) {\n\t\t\t\t\t\tconst auto toSendSize = tl::count_length(toSendRequest) >> 2;\n\t\t\t\t\t\tauto wrappedRequest = SerializedRequest::Prepare(\n\t\t\t\t\t\t\ttoSendSize,\n\t\t\t\t\t\t\ttoSendSize + 3);\n\t\t\t\t\t\twrappedRequest->resize(4);\n\t\t\t\t\t\tmemcpy(wrappedRequest->data(), toSendRequest->constData(), 4 * sizeof(mtpPrime));\n\t\t\t\t\t\tWrapInvokeAfter(wrappedRequest, toSendRequest, haveSent);\n\t\t\t\t\t\ttoSendRequest = std::move(wrappedRequest);\n\t\t\t\t\t}\n\t\t\t\t\tif (wrapLayer) {\n\t\t\t\t\t\tconst auto noWrapSize = (tl::count_length(toSendRequest) >> 2);\n\t\t\t\t\t\tconst auto toSendSize = noWrapSize + initSizeInInts;\n\t\t\t\t\t\tauto wrappedRequest = SerializedRequest::Prepare(toSendSize);\n\t\t\t\t\t\tmemcpy(wrappedRequest->data(), toSendRequest->constData(), 7 * sizeof(mtpPrime)); // all except length\n\t\t\t\t\t\twrappedRequest->push_back(mtpc_invokeWithLayer);\n\t\t\t\t\t\twrappedRequest->push_back(kCurrentLayer);\n\t\t\t\t\t\tinitWrapper.write<mtpBuffer>(*wrappedRequest);\n\t\t\t\t\t\twrappedRequest->resize(wrappedRequest->size() + noWrapSize);\n\t\t\t\t\t\tmemcpy(wrappedRequest->data() + wrappedRequest->size() - noWrapSize, toSendRequest->constData() + 8, noWrapSize * sizeof(mtpPrime));\n\t\t\t\t\t\ttoSendRequest = std::move(wrappedRequest);\n\t\t\t\t\t}\n\n\t\t\t\t\tneedAnyResponse = true;\n\t\t\t\t} else {\n\t\t\t\t\t_ackedIds.emplace(msgId, toSendRequest->requestId);\n\t\t\t\t}\n\t\t\t}\n\t\t} else { // send in container\n\t\t\tbool willNeedInit = false;\n\t\t\tuint32 containerSize = 1 + 1; // cons + vector size\n\t\t\tif (pingRequest) containerSize += pingRequest.messageSize();\n\t\t\tif (ackRequest) containerSize += ackRequest.messageSize();\n\t\t\tif (resendRequest) containerSize += resendRequest.messageSize();\n\t\t\tif (stateRequest) containerSize += stateRequest.messageSize();\n\t\t\tif (httpWaitRequest) containerSize += httpWaitRequest.messageSize();\n\t\t\tif (bindDcKeyRequest) containerSize += bindDcKeyRequest.messageSize();\n\t\t\tfor (const auto &[requestId, request] : sendingRange) {\n\t\t\t\tcontainerSize += request.messageSize();\n\t\t\t\tif (needsLayer && request->needsLayer) {\n\t\t\t\t\tcontainerSize += initSizeInInts;\n\t\t\t\t\twillNeedInit = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tmtpBuffer initSerialized;\n\t\t\tif (willNeedInit) {\n\t\t\t\tinitSerialized.reserve(initSizeInInts);\n\t\t\t\tinitSerialized.push_back(mtpc_invokeWithLayer);\n\t\t\t\tinitSerialized.push_back(kCurrentLayer);\n\t\t\t\tinitWrapper.write<mtpBuffer>(initSerialized);\n\t\t\t}\n\t\t\t// prepare container + each in invoke after\n\t\t\ttoSendRequest = SerializedRequest::Prepare(\n\t\t\t\tcontainerSize,\n\t\t\t\tcontainerSize + 3 * sendingCount);\n\t\t\ttoSendRequest->push_back(mtpc_msg_container);\n\t\t\ttoSendRequest->push_back(totalSending);\n\n\t\t\t// check for a valid container\n\t\t\tauto bigMsgId = base::unixtime::mtproto_msg_id();\n\n\t\t\t// the fact of this lock is used in replaceMsgId()\n\t\t\tQWriteLocker locker2(_sessionData->haveSentMutex());\n\t\t\tauto &haveSent = _sessionData->haveSentMap();\n\n\t\t\t// prepare sent container\n\t\t\tauto sentIdsWrap = SentContainer();\n\t\t\tsentIdsWrap.sent = crl::now();\n\t\t\tsentIdsWrap.messages.reserve(totalSending);\n\n\t\t\tif (bindDcKeyRequest) {\n\t\t\t\t_bindMsgId = placeToContainer(\n\t\t\t\t\ttoSendRequest,\n\t\t\t\t\tbigMsgId,\n\t\t\t\t\tfalse,\n\t\t\t\t\tbindDcKeyRequest);\n\t\t\t\t_bindMessageSent = crl::now();\n\t\t\t\tsentIdsWrap.messages.push_back(_bindMsgId);\n\t\t\t\tneedAnyResponse = true;\n\t\t\t}\n\t\t\tif (pingRequest) {\n\t\t\t\t_pingMsgId = placeToContainer(\n\t\t\t\t\ttoSendRequest,\n\t\t\t\t\tbigMsgId,\n\t\t\t\t\tforceNewMsgId,\n\t\t\t\t\tpingRequest);\n\t\t\t\tsentIdsWrap.messages.push_back(_pingMsgId);\n\t\t\t\tneedAnyResponse = true;\n\t\t\t}\n\n\t\t\tfor (auto &[requestId, request] : sendingRange) {\n\t\t\t\tconst auto msgId = prepareToSend(\n\t\t\t\t\trequest,\n\t\t\t\t\tbigMsgId,\n\t\t\t\t\tforceNewMsgId);\n\t\t\t\tif (msgId >= bigMsgId) {\n\t\t\t\t\tbigMsgId = base::unixtime::mtproto_msg_id();\n\t\t\t\t}\n\t\t\t\tbool added = false;\n\t\t\t\tif (request->requestId) {\n\t\t\t\t\tif (request.needAck()) {\n\t\t\t\t\t\trequest->lastSentTime = crl::now();\n\t\t\t\t\t\tint32 reqNeedsLayer = (needsLayer && request->needsLayer) ? toSendRequest->size() : 0;\n\t\t\t\t\t\tif (request->after) {\n\t\t\t\t\t\t\tWrapInvokeAfter(toSendRequest, request, haveSent, reqNeedsLayer ? initSizeInInts : 0);\n\t\t\t\t\t\t\tif (reqNeedsLayer) {\n\t\t\t\t\t\t\t\tmemcpy(toSendRequest->data() + reqNeedsLayer + 4, initSerialized.constData(), initSize);\n\t\t\t\t\t\t\t\t*(toSendRequest->data() + reqNeedsLayer + 3) += initSize;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tadded = true;\n\t\t\t\t\t\t} else if (reqNeedsLayer) {\n\t\t\t\t\t\t\ttoSendRequest->resize(reqNeedsLayer + initSizeInInts + request.messageSize());\n\t\t\t\t\t\t\tmemcpy(toSendRequest->data() + reqNeedsLayer, request->constData() + 4, 4 * sizeof(mtpPrime));\n\t\t\t\t\t\t\tmemcpy(toSendRequest->data() + reqNeedsLayer + 4, initSerialized.constData(), initSize);\n\t\t\t\t\t\t\tmemcpy(toSendRequest->data() + reqNeedsLayer + 4 + initSizeInInts, request->constData() + 8, tl::count_length(request));\n\t\t\t\t\t\t\t*(toSendRequest->data() + reqNeedsLayer + 3) += initSize;\n\t\t\t\t\t\t\tadded = true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// #TODO rewrite so that it will always hold.\n\t\t\t\t\t\t//Assert(!haveSent.contains(msgId));\n\t\t\t\t\t\thaveSent.emplace(msgId, request);\n\t\t\t\t\t\tsentIdsWrap.messages.push_back(msgId);\n\t\t\t\t\t\tscheduleCheckSentRequests = true;\n\t\t\t\t\t\tneedAnyResponse = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t_ackedIds.emplace(msgId, request->requestId);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!added) {\n\t\t\t\t\tuint32 from = toSendRequest->size(), len = request.messageSize();\n\t\t\t\t\ttoSendRequest->resize(from + len);\n\t\t\t\t\tmemcpy(toSendRequest->data() + from, request->constData() + 4, len * sizeof(mtpPrime));\n\t\t\t\t}\n\t\t\t}\n\t\t\ttoSend.erase(sendingFrom, sendingTill);\n\n\t\t\tif (stateRequest) {\n\t\t\t\tconst auto msgId = placeToContainer(\n\t\t\t\t\ttoSendRequest,\n\t\t\t\t\tbigMsgId,\n\t\t\t\t\tforceNewMsgId,\n\t\t\t\t\tstateRequest);\n\t\t\t\t_stateAndResendRequests.emplace(msgId, stateRequest);\n\t\t\t\tneedAnyResponse = true;\n\t\t\t}\n\t\t\tif (resendRequest) {\n\t\t\t\tconst auto msgId = placeToContainer(\n\t\t\t\t\ttoSendRequest,\n\t\t\t\t\tbigMsgId,\n\t\t\t\t\tforceNewMsgId,\n\t\t\t\t\tresendRequest);\n\t\t\t\t_stateAndResendRequests.emplace(msgId, resendRequest);\n\t\t\t\tneedAnyResponse = true;\n\t\t\t}\n\t\t\tif (ackRequest) {\n\t\t\t\tplaceToContainer(\n\t\t\t\t\ttoSendRequest,\n\t\t\t\t\tbigMsgId,\n\t\t\t\t\tforceNewMsgId,\n\t\t\t\t\tackRequest);\n\t\t\t}\n\t\t\tif (httpWaitRequest) {\n\t\t\t\tplaceToContainer(\n\t\t\t\t\ttoSendRequest,\n\t\t\t\t\tbigMsgId,\n\t\t\t\t\tforceNewMsgId,\n\t\t\t\t\thttpWaitRequest);\n\t\t\t}\n\n\t\t\tconst auto containerMsgId = prepareToSend(\n\t\t\t\ttoSendRequest,\n\t\t\t\tbigMsgId,\n\t\t\t\tforceNewMsgId);\n\t\t\t_sentContainers.emplace(containerMsgId, std::move(sentIdsWrap));\n\n\t\t\tif (scheduleCheckSentRequests && !_checkSentRequestsTimer.isActive()) {\n\t\t\t\t_checkSentRequestsTimer.callOnce(kCheckSentRequestTimeout);\n\t\t\t}\n\t\t}\n\t}\n\tsendSecureRequest(std::move(toSendRequest), needAnyResponse);\n\tif (someSkipped) {\n\t\tInvokeQueued(this, [=] {\n\t\t\ttryToSend();\n\t\t});\n\t}\n}\n\nvoid SessionPrivate::retryByTimer() {\n\tif (_retryTimeout < 3) {\n\t\t++_retryTimeout;\n\t} else if (_retryTimeout == 3) {\n\t\t_retryTimeout = 1000;\n\t} else if (_retryTimeout < 64000) {\n\t\t_retryTimeout *= 2;\n\t}\n\tconnectToServer();\n}\n\nvoid SessionPrivate::restartNow() {\n\t_retryTimeout = 1;\n\t_retryTimer.cancel();\n\trestart();\n}\n\nvoid SessionPrivate::connectToServer(bool afterConfig) {\n\tif (afterConfig && (!_testConnections.empty() || _connection)) {\n\t\treturn;\n\t}\n\n\tdestroyAllConnections();\n\n\tif (realDcTypeChanged() && _keyCreator) {\n\t\tdestroyTemporaryKey();\n\t\treturn;\n\t}\n\n\t_options = std::make_unique<SessionOptions>(_sessionData->options());\n\n\tconst auto bareDc = BareDcId(_shiftedDcId);\n\n\t_currentDcType = tryAcquireKeyCreation();\n\tif (_currentDcType == DcType::Cdn && !_instance->isKeysDestroyer()) {\n\t\tif (!_instance->dcOptions().hasCDNKeysForDc(bareDc)) {\n\t\t\trequestCDNConfig();\n\t\t\treturn;\n\t\t}\n\t}\n\tif (_options->proxy.type == ProxyData::Type::Mtproto) {\n\t\t// host, port, secret for mtproto proxy are taken from proxy.\n\t\tappendTestConnection(DcOptions::Variants::Tcp, {}, 0, {});\n\t} else {\n\t\tusing Variants = DcOptions::Variants;\n\t\tconst auto special = (_currentDcType == DcType::Temporary);\n\t\tconst auto variants = _instance->dcOptions().lookup(\n\t\t\tbareDc,\n\t\t\t_currentDcType,\n\t\t\t_options->proxy.type != ProxyData::Type::None);\n\t\tconst auto useIPv4 = special ? true : _options->useIPv4;\n\t\tconst auto useIPv6 = special ? false : _options->useIPv6;\n\t\tconst auto useTcp = special ? true : _options->useTcp;\n\t\tconst auto useHttp = special ? false : _options->useHttp;\n\t\tconst auto skipAddress = !useIPv4\n\t\t\t? Variants::IPv4\n\t\t\t: !useIPv6\n\t\t\t? Variants::IPv6\n\t\t\t: Variants::AddressTypeCount;\n\t\tconst auto skipProtocol = !useTcp\n\t\t\t? Variants::Tcp\n\t\t\t: !useHttp\n\t\t\t? Variants::Http\n\t\t\t: Variants::ProtocolCount;\n\t\tfor (auto address = 0; address != Variants::AddressTypeCount; ++address) {\n\t\t\tif (address == skipAddress) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (auto protocol = 0; protocol != Variants::ProtocolCount; ++protocol) {\n\t\t\t\tif (protocol == skipProtocol) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tfor (const auto &endpoint : variants.data[address][protocol]) {\n\t\t\t\t\tappendTestConnection(\n\t\t\t\t\t\tstatic_cast<Variants::Protocol>(protocol),\n\t\t\t\t\t\tQString::fromStdString(endpoint.ip),\n\t\t\t\t\t\tendpoint.port,\n\t\t\t\t\t\tendpoint.secret);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif (_testConnections.empty()) {\n\t\tif (_instance->isKeysDestroyer()) {\n\t\t\tLOG((\"MTP Error: DC %1 options for not found for auth key destruction!\").arg(_shiftedDcId));\n\t\t\t_instance->keyWasPossiblyDestroyed(_shiftedDcId);\n\t\t\treturn;\n\t\t} else if (afterConfig) {\n\t\t\tLOG((\"MTP Error: DC %1 options for not found right after config load!\").arg(_shiftedDcId));\n\t\t\treturn restart();\n\t\t}\n\t\tDEBUG_LOG((\"MTP Info: DC %1 options not found, waiting for config\").arg(_shiftedDcId));\n\t\tInvokeQueued(_instance, [instance = _instance] {\n\t\t\tinstance->requestConfig();\n\t\t});\n\t\treturn;\n\t}\n\tDEBUG_LOG((\"Connection Info: Connecting to %1 with %2 test connections.\"\n\t\t).arg(_shiftedDcId\n\t\t).arg(_testConnections.size()));\n\n\tif (!_startedConnectingAt) {\n\t\t_startedConnectingAt = crl::now();\n\t} else if (crl::now() - _startedConnectingAt > kRequestConfigTimeout) {\n\t\tInvokeQueued(_instance, [instance = _instance] {\n\t\t\tinstance->requestConfigIfOld();\n\t\t});\n\t}\n\n\t_retryTimer.cancel();\n\t_waitForConnectedTimer.cancel();\n\n\tsetState(ConnectingState);\n\n\t_bindMsgId = 0;\n\t_pingId = _pingMsgId = _pingIdToSend = _pingSendAt = 0;\n\t_pingSender.cancel();\n\n\t_waitForConnectedTimer.callOnce(_waitForConnected);\n}\n\nvoid SessionPrivate::restart() {\n\tDEBUG_LOG((\"MTP Info: restarting Connection\"));\n\n\t_waitForReceivedTimer.cancel();\n\t_waitForConnectedTimer.cancel();\n\n\tdoDisconnect();\n\n\tif (_needSessionReset) {\n\t\tresetSession();\n\t}\n\tif (_retryTimer.isActive()) {\n\t\treturn;\n\t}\n\n\tDEBUG_LOG((\"MTP Info: restart timeout: %1ms\").arg(_retryTimeout));\n\n\tsetState(-_retryTimeout);\n}\n\nvoid SessionPrivate::onSentSome(uint64 size) {\n\tif (!_waitForReceivedTimer.isActive()) {\n\t\tauto remain = static_cast<uint64>(_waitForReceived);\n\t\tif (!_oldConnection) {\n\t\t\tAssert(remain <= kMaxReceiveTimeout);\n\n\t\t\t// 8kb / sec, so 512 kb give 64 sec\n\t\t\tauto remainBySize = size * _waitForReceived / 8192;\n\t\t\tremain = std::clamp(\n\t\t\t\tremainBySize,\n\t\t\t\tremain,\n\t\t\t\tuint64(kMaxReceiveTimeout));\n\t\t\tif (remain != _waitForReceived) {\n\t\t\t\tDEBUG_LOG((\"Checking connect for request with size %1 bytes, delay will be %2\").arg(size).arg(remain));\n\t\t\t}\n\t\t}\n\t\t_waitForReceivedTimer.callOnce(remain);\n\t}\n\tif (!_firstSentAt) {\n\t\t_firstSentAt = crl::now();\n\t}\n}\n\nvoid SessionPrivate::onReceivedSome() {\n\tif (_oldConnection) {\n\t\t_oldConnection = false;\n\t\tDEBUG_LOG((\"This connection marked as not old!\"));\n\t}\n\t_oldConnectionTimer.callOnce(kMarkConnectionOldTimeout);\n\t_waitForReceivedTimer.cancel();\n\tif (_firstSentAt > 0) {\n\t\tconst auto ms = crl::now() - _firstSentAt;\n\t\tDEBUG_LOG((\"MTP Info: response in %1ms, _waitForReceived: %2ms\"\n\t\t\t).arg(ms\n\t\t\t).arg(_waitForReceived));\n\n\t\tif (ms > 0 && ms * 2 < _waitForReceived) {\n\t\t\t_waitForReceived = qMax(ms * 2, kMinReceiveTimeout);\n\t\t}\n\t\t_firstSentAt = -1;\n\t}\n}\n\nvoid SessionPrivate::markConnectionOld() {\n\t_oldConnection = true;\n\t_waitForReceived = kMinReceiveTimeout;\n\tDEBUG_LOG((\"This connection marked as old! _waitForReceived now %1ms\"\n\t\t).arg(_waitForReceived));\n}\n\nvoid SessionPrivate::sendPingByTimer() {\n\tif (_pingId) {\n\t\t// _pingSendAt: when to send next ping (lastPingAt + kPingSendAfter)\n\t\t// could be equal to zero.\n\t\tconst auto now = crl::now();\n\t\tconst auto mustSendTill = _pingSendAt\n\t\t\t+ kPingSendAfterForce\n\t\t\t- kPingSendAfter;\n\t\tif (mustSendTill < now + 1000) {\n\t\t\tLOG((\"Could not send ping for some seconds, restarting...\"));\n\t\t\treturn restart();\n\t\t} else {\n\t\t\t_pingSender.callOnce(mustSendTill - now);\n\t\t}\n\t} else {\n\t\t_sessionData->queueNeedToResumeAndSend();\n\t}\n}\n\nvoid SessionPrivate::sendPingForce() {\n\tDEBUG_LOG((\"MTP Info: send ping force for dcWithShift %1.\").arg(_shiftedDcId));\n\tif (!_pingId) {\n\t\t_pingSendAt = 0;\n\t\tDEBUG_LOG((\"Will send ping!\"));\n\t\ttryToSend();\n\t}\n}\n\nvoid SessionPrivate::waitReceivedFailed() {\n\tExpects(_options != nullptr);\n\n\tDEBUG_LOG((\"MTP Info: bad connection, _waitForReceived: %1ms\").arg(_waitForReceived));\n\tif (_waitForReceived < kMaxReceiveTimeout) {\n\t\t_waitForReceived = std::min(\n\t\t\t_waitForReceived * 2,\n\t\t\tkMaxReceiveTimeout);\n\t}\n\tdoDisconnect();\n\tif (_retryTimer.isActive()) {\n\t\treturn;\n\t}\n\n\tDEBUG_LOG((\"MTP Info: immediate restart!\"));\n\tInvokeQueued(this, [=] { connectToServer(); });\n\n\tconst auto instance = _instance;\n\tconst auto shiftedDcId = _shiftedDcId;\n\tInvokeQueued(instance, [=] {\n\t\tinstance->restartedByTimeout(shiftedDcId);\n\t});\n}\n\nvoid SessionPrivate::waitConnectedFailed() {\n\tDEBUG_LOG((\"MTP Info: can't connect in %1ms\").arg(_waitForConnected));\n\tauto maxTimeout = kMaxConnectedTimeout;\n\tfor (const auto &connection : _testConnections) {\n\t\taccumulate_max(maxTimeout, connection.data->fullConnectTimeout());\n\t}\n\tif (_waitForConnected < maxTimeout) {\n\t\t_waitForConnected = std::min(maxTimeout, 2 * _waitForConnected);\n\t}\n\n\tconnectingTimedOut();\n\n\tDEBUG_LOG((\"MTP Info: immediate restart!\"));\n\tInvokeQueued(this, [=] { connectToServer(); });\n}\n\nvoid SessionPrivate::waitBetterFailed() {\n\tconfirmBestConnection();\n}\n\nvoid SessionPrivate::connectingTimedOut() {\n\tfor (const auto &connection : _testConnections) {\n\t\tconnection.data->timedOut();\n\t}\n\tdoDisconnect();\n}\n\nvoid SessionPrivate::doDisconnect() {\n\tdestroyAllConnections();\n\tsetState(DisconnectedState);\n}\n\nvoid SessionPrivate::requestCDNConfig() {\n\tInvokeQueued(_instance, [instance = _instance] {\n\t\tinstance->requestCDNConfig();\n\t});\n}\n\nvoid SessionPrivate::handleReceived() {\n\tExpects(_encryptionKey != nullptr);\n\n\tonReceivedSome();\n\n\twhile (!_connection->received().empty()) {\n\t\tauto intsBuffer = std::move(_connection->received().front());\n\t\t_connection->received().pop_front();\n\n\t\tconstexpr auto kExternalHeaderIntsCount = 6U; // 2 auth_key_id, 4 msg_key\n\t\tconstexpr auto kEncryptedHeaderIntsCount = 8U; // 2 salt, 2 session, 2 msg_id, 1 seq_no, 1 length\n\t\tconstexpr auto kMinimalEncryptedIntsCount = kEncryptedHeaderIntsCount + 4U; // + 1 data + 3 padding\n\t\tconstexpr auto kMinimalIntsCount = kExternalHeaderIntsCount + kMinimalEncryptedIntsCount;\n\t\tauto intsCount = uint32(intsBuffer.size());\n\t\tauto ints = intsBuffer.constData();\n\t\tif ((intsCount < kMinimalIntsCount) || (intsCount > kMaxMessageLength / kIntSize)) {\n\t\t\tLOG((\"TCP Error: bad message received, len %1\").arg(intsCount * kIntSize));\n\t\t\treturn restart();\n\t\t}\n\t\tif (_keyId != *(uint64*)ints) {\n\t\t\tLOG((\"TCP Error: bad auth_key_id %1 instead of %2 received\").arg(_keyId).arg(*(uint64*)ints));\n\t\t\treturn restart();\n\t\t}\n\n\t\tconstexpr auto kMinPaddingSize = 12U;\n\t\tconstexpr auto kMaxPaddingSize = 1024U;\n\n\t\tauto encryptedInts = ints + kExternalHeaderIntsCount;\n\t\tauto encryptedIntsCount = (intsCount - kExternalHeaderIntsCount) & ~0x03U;\n\t\tauto encryptedBytesCount = encryptedIntsCount * kIntSize;\n\t\tauto decryptedBuffer = QByteArray(encryptedBytesCount, Qt::Uninitialized);\n\t\tauto msgKey = *(MTPint128*)(ints + 2);\n\n\t\taesIgeDecrypt(encryptedInts, decryptedBuffer.data(), encryptedBytesCount, _encryptionKey, msgKey);\n\n\t\tauto decryptedInts = reinterpret_cast<const mtpPrime*>(decryptedBuffer.constData());\n\t\tauto serverSalt = *(uint64*)&decryptedInts[0];\n\t\tauto session = *(uint64*)&decryptedInts[2];\n\t\tauto msgId = *(uint64*)&decryptedInts[4];\n\t\tauto seqNo = *(uint32*)&decryptedInts[6];\n\t\tauto needAck = ((seqNo & 0x01) != 0);\n\t\tauto messageLength = *(uint32*)&decryptedInts[7];\n\t\tauto fullDataLength = kEncryptedHeaderIntsCount * kIntSize + messageLength; // Without padding.\n\n\t\t// Can underflow, but it is an unsigned type, so we just check the range later.\n\t\tauto paddingSize = static_cast<uint32>(encryptedBytesCount) - static_cast<uint32>(fullDataLength);\n\n\t\tstd::array<uchar, 32> sha256Buffer = { { 0 } };\n\n\t\tSHA256_CTX msgKeyLargeContext;\n\t\tSHA256_Init(&msgKeyLargeContext);\n\t\tSHA256_Update(&msgKeyLargeContext, _encryptionKey->partForMsgKey(false), 32);\n\t\tSHA256_Update(&msgKeyLargeContext, decryptedInts, encryptedBytesCount);\n\t\tSHA256_Final(sha256Buffer.data(), &msgKeyLargeContext);\n\n\t\tconstexpr auto kMsgKeyShift = 8U;\n\t\tif (ConstTimeIsDifferent(&msgKey, sha256Buffer.data() + kMsgKeyShift, sizeof(msgKey))) {\n\t\t\tLOG((\"TCP Error: bad SHA256 hash after aesDecrypt in message\"));\n\t\t\treturn restart();\n\t\t}\n\n\t\tif ((messageLength > kMaxMessageLength)\n\t\t\t|| (messageLength & 0x03)\n\t\t\t|| (paddingSize < kMinPaddingSize)\n\t\t\t|| (paddingSize > kMaxPaddingSize)) {\n\t\t\tLOG((\"TCP Error: bad msg_len received %1, data size: %2\").arg(messageLength).arg(encryptedBytesCount));\n\t\t\treturn restart();\n\t\t}\n\n\t\tif (Logs::DebugEnabled()) {\n\t\t\t_connection->logInfo(u\"Decrypted message %1,%2,%3 is %4 len\"_q\n\t\t\t\t.arg(msgId)\n\t\t\t\t.arg(seqNo)\n\t\t\t\t.arg(Logs::b(needAck))\n\t\t\t\t.arg(fullDataLength));\n\t\t}\n\n\t\tif (session != _sessionId) {\n\t\t\tLOG((\"MTP Error: bad server session received\"));\n\t\t\treturn restart();\n\t\t}\n\n\t\tconst auto serverTime = int32(msgId >> 32);\n\t\tconst auto isReply = ((msgId & 0x03) == 1);\n\t\tif (!isReply && ((msgId & 0x03) != 3)) {\n\t\t\tLOG((\"MTP Error: bad msg_id %1 in message received\").arg(msgId));\n\n\t\t\treturn restart();\n\t\t}\n\n\t\tconst auto clientTime = base::unixtime::now();\n\t\tconst auto badTime = (serverTime > clientTime + 60)\n\t\t\t|| (serverTime + 300 < clientTime);\n\t\tif (badTime) {\n\t\t\tDEBUG_LOG((\"MTP Info: bad server time from msg_id: %1, my time: %2\").arg(serverTime).arg(clientTime));\n\t\t}\n\n\t\tbool wasConnected = (getState() == ConnectedState);\n\t\tif (serverSalt != _sessionSalt) {\n\t\t\tif (!badTime) {\n\t\t\t\tDEBUG_LOG((\"MTP Info: other salt received... received: %1, my salt: %2, updating...\").arg(serverSalt).arg(_sessionSalt));\n\t\t\t\t_sessionSalt = serverSalt;\n\n\t\t\t\tif (setState(ConnectedState, ConnectingState)) {\n\t\t\t\t\tresendAll();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tDEBUG_LOG((\"MTP Info: other salt received... received: %1, my salt: %2\").arg(serverSalt).arg(_sessionSalt));\n\t\t\t}\n\t\t} else {\n\t\t\tserverSalt = 0; // dont pass to handle method, so not to lock in setSalt()\n\t\t}\n\n\t\tif (needAck) _ackRequestData.push_back(MTP_long(msgId));\n\n\t\tauto res = HandleResult::Success; // if no need to handle, then succeed\n\t\tauto from = decryptedInts + kEncryptedHeaderIntsCount;\n\t\tauto end = from + (messageLength / kIntSize);\n\t\tauto sfrom = decryptedInts + 4U; // msg_id + seq_no + length + message\n\t\tMTP_LOG(_shiftedDcId, (\"Recv: \")\n\t\t\t+ DumpToText(sfrom, end)\n\t\t\t+ QString(\" (dc:%1,key:%2,session:%3)\"\n\t\t\t).arg(AbstractConnection::ProtocolDcDebugId(getProtocolDcId())\n\t\t\t).arg(_encryptionKey->keyId()\n\t\t\t).arg(_sessionId));\n\n\t\tconst auto registered = _receivedMessageIds.registerMsgId(\n\t\t\tmsgId,\n\t\t\tneedAck);\n\t\tif (registered == ReceivedIdsManager::Result::Success) {\n\t\t\tres = handleOneReceived(from, end, msgId, {\n\t\t\t\t.outerMsgId = msgId,\n\t\t\t\t.serverSalt = serverSalt,\n\t\t\t\t.serverTime = serverTime,\n\t\t\t\t.badTime = badTime,\n\t\t\t});\n\t\t} else if (registered == ReceivedIdsManager::Result::TooOld) {\n\t\t\tres = HandleResult::ResetSession;\n\t\t}\n\t\t_receivedMessageIds.shrink();\n\n\t\t// send acks\n\t\tif (const auto toAckSize = _ackRequestData.size()) {\n\t\t\tDEBUG_LOG((\"MTP Info: will send %1 acks, ids: %2\").arg(toAckSize).arg(LogIdsVector(_ackRequestData)));\n\t\t\t_sessionData->queueSendAnything(kAckSendWaiting);\n\t\t}\n\n\t\tauto lock = QReadLocker(_sessionData->haveReceivedMutex());\n\t\tconst auto tryToReceive = !_sessionData->haveReceivedMessages().empty();\n\t\tlock.unlock();\n\n\t\tif (tryToReceive) {\n\t\t\tDEBUG_LOG((\"MTP Info: queueTryToReceive() - need to parse in another thread, %1 messages.\").arg(_sessionData->haveReceivedMessages().size()));\n\t\t\t_sessionData->queueTryToReceive();\n\t\t}\n\n\t\tif (res != HandleResult::Success && res != HandleResult::Ignored) {\n\t\t\tif (res == HandleResult::DestroyTemporaryKey) {\n\t\t\t\tdestroyTemporaryKey();\n\t\t\t} else if (res == HandleResult::ResetSession) {\n\t\t\t\t_needSessionReset = true;\n\t\t\t}\n\t\t\treturn restart();\n\t\t}\n\t\t_retryTimeout = 1; // reset restart() timer\n\n\t\t_startedConnectingAt = crl::time(0);\n\n\t\tif (!wasConnected) {\n\t\t\tif (getState() == ConnectedState) {\n\t\t\t\t_sessionData->queueNeedToResumeAndSend();\n\t\t\t}\n\t\t}\n\t}\n\tif (_connection->needHttpWait()) {\n\t\t_sessionData->queueSendAnything();\n\t}\n}\n\nSessionPrivate::HandleResult SessionPrivate::handleOneReceived(\n\t\tconst mtpPrime *from,\n\t\tconst mtpPrime *end,\n\t\tuint64 msgId,\n\t\tOuterInfo info) {\n\tExpects(from < end);\n\n\tswitch (mtpTypeId(*from)) {\n\n\tcase mtpc_gzip_packed: {\n\t\tDEBUG_LOG((\"Message Info: gzip container\"));\n\t\tmtpBuffer response = ungzip(++from, end);\n\t\tif (response.empty()) {\n\t\t\treturn HandleResult::RestartConnection;\n\t\t}\n\t\treturn handleOneReceived(response.data(), response.data() + response.size(), msgId, info);\n\t}\n\n\tcase mtpc_msg_container: {\n\t\tif (++from >= end) {\n\t\t\treturn HandleResult::ParseError;\n\t\t}\n\n\t\tconst mtpPrime *otherEnd;\n\t\tconst auto msgsCount = (uint32)*(from++);\n\t\tDEBUG_LOG((\"Message Info: container received, count: %1\").arg(msgsCount));\n\t\tfor (uint32 i = 0; i < msgsCount; ++i) {\n\t\t\tif (from + 4 >= end) {\n\t\t\t\treturn HandleResult::ParseError;\n\t\t\t}\n\t\t\totherEnd = from + 4;\n\n\t\t\tMTPlong inMsgId;\n\t\t\tif (!inMsgId.read(from, otherEnd)) {\n\t\t\t\treturn HandleResult::ParseError;\n\t\t\t}\n\t\t\tbool isReply = ((inMsgId.v & 0x03) == 1);\n\t\t\tif (!isReply && ((inMsgId.v & 0x03) != 3)) {\n\t\t\t\tLOG((\"Message Error: bad msg_id %1 in contained message received\").arg(inMsgId.v));\n\t\t\t\treturn HandleResult::RestartConnection;\n\t\t\t}\n\n\t\t\tMTPint inSeqNo;\n\t\t\tif (!inSeqNo.read(from, otherEnd)) {\n\t\t\t\treturn HandleResult::ParseError;\n\t\t\t}\n\t\t\tMTPint bytes;\n\t\t\tif (!bytes.read(from, otherEnd)) {\n\t\t\t\treturn HandleResult::ParseError;\n\t\t\t}\n\t\t\tif ((bytes.v & 0x03) || bytes.v < 4) {\n\t\t\t\tLOG((\"Message Error: bad length %1 of contained message received\").arg(bytes.v));\n\t\t\t\treturn HandleResult::RestartConnection;\n\t\t\t}\n\n\t\t\tbool needAck = (inSeqNo.v & 0x01);\n\t\t\tif (needAck) _ackRequestData.push_back(inMsgId);\n\n\t\t\tDEBUG_LOG((\"Message Info: message from container, msg_id: %1, needAck: %2\").arg(inMsgId.v).arg(Logs::b(needAck)));\n\n\t\t\totherEnd = from + (bytes.v >> 2);\n\t\t\tif (otherEnd > end) {\n\t\t\t\treturn HandleResult::ParseError;\n\t\t\t}\n\n\t\t\tauto res = HandleResult::Success; // if no need to handle, then succeed\n\t\t\tconst auto registered = _receivedMessageIds.registerMsgId(\n\t\t\t\tinMsgId.v,\n\t\t\t\tneedAck);\n\t\t\tif (registered == ReceivedIdsManager::Result::Success) {\n\t\t\t\tres = handleOneReceived(from, otherEnd, inMsgId.v, info);\n\t\t\t\tinfo.badTime = false;\n\t\t\t} else if (registered == ReceivedIdsManager::Result::TooOld) {\n\t\t\t\tres = HandleResult::ResetSession;\n\t\t\t}\n\t\t\tif (res != HandleResult::Success) {\n\t\t\t\treturn res;\n\t\t\t}\n\n\t\t\tfrom = otherEnd;\n\t\t}\n\t} return HandleResult::Success;\n\n\tcase mtpc_msgs_ack: {\n\t\tMTPMsgsAck msg;\n\t\tif (!msg.read(from, end)) {\n\t\t\treturn HandleResult::ParseError;\n\t\t}\n\t\tconst auto &ids = msg.c_msgs_ack().vmsg_ids().v;\n\t\tDEBUG_LOG((\"Message Info: acks received, ids: %1\"\n\t\t\t).arg(LogIdsVector(ids)));\n\t\tif (ids.isEmpty()) {\n\t\t\treturn info.badTime ? HandleResult::Ignored : HandleResult::Success;\n\t\t}\n\n\t\tif (info.badTime) {\n\t\t\tif (!requestsFixTimeSalt(ids, info)) {\n\t\t\t\treturn HandleResult::Ignored;\n\t\t\t}\n\t\t} else {\n\t\t\tcorrectUnixtimeByFastRequest(ids, info.serverTime);\n\t\t}\n\t\trequestsAcked(ids);\n\t} return HandleResult::Success;\n\n\tcase mtpc_bad_msg_notification: {\n\t\tMTPBadMsgNotification msg;\n\t\tif (!msg.read(from, end)) {\n\t\t\treturn HandleResult::ParseError;\n\t\t}\n\t\tconst auto &data(msg.c_bad_msg_notification());\n\t\tLOG((\"Message Info: bad message notification received (error_code %3) for msg_id = %1, seq_no = %2\").arg(data.vbad_msg_id().v).arg(data.vbad_msg_seqno().v).arg(data.verror_code().v));\n\n\t\tconst auto resendId = data.vbad_msg_id().v;\n\t\tconst auto errorCode = data.verror_code().v;\n\t\tif (false\n\t\t\t|| errorCode == 16\n\t\t\t|| errorCode == 17\n\t\t\t|| errorCode == 32\n\t\t\t|| errorCode == 33\n\t\t\t|| errorCode == 64) { // can handle\n\t\t\tconst auto needResend = false\n\t\t\t\t|| (errorCode == 16) // bad msg_id\n\t\t\t\t|| (errorCode == 17) // bad msg_id\n\t\t\t\t|| (errorCode == 64); // bad container\n\t\t\tif (errorCode == 64) { // bad container!\n\t\t\t\tif (Logs::DebugEnabled()) {\n\t\t\t\t\tconst auto i = _sentContainers.find(resendId);\n\t\t\t\t\tif (i == _sentContainers.end()) {\n\t\t\t\t\t\tLOG((\"Message Error: Container not found!\"));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tauto idsList = QStringList();\n\t\t\t\t\t\tfor (const auto innerMsgId : i->second.messages) {\n\t\t\t\t\t\t\tidsList.push_back(QString::number(innerMsgId));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tLOG((\"Message Info: bad container received! messages: %1\").arg(idsList.join(',')));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!wasSent(resendId)) {\n\t\t\t\tDEBUG_LOG((\"Message Error: \"\n\t\t\t\t\t\"such message was not sent recently %1\").arg(resendId));\n\t\t\t\treturn info.badTime\n\t\t\t\t\t? HandleResult::Ignored\n\t\t\t\t\t: HandleResult::Success;\n\t\t\t}\n\n\t\t\tif (needResend) { // bad msg_id or bad container\n\t\t\t\tif (info.serverSalt) {\n\t\t\t\t\t_sessionSalt = info.serverSalt;\n\t\t\t\t}\n\n\t\t\t\tcorrectUnixtimeWithBadLocal(info.serverTime);\n\n\t\t\t\tDEBUG_LOG((\"Message Info: unixtime updated, now %1, resending in container...\").arg(info.serverTime));\n\n\t\t\t\tresend(resendId);\n\t\t\t} else { // must create new session, because msg_id and msg_seqno are inconsistent\n\t\t\t\tif (info.badTime) {\n\t\t\t\t\tif (info.serverSalt) {\n\t\t\t\t\t\t_sessionSalt = info.serverSalt;\n\t\t\t\t\t}\n\t\t\t\t\tcorrectUnixtimeWithBadLocal(info.serverTime);\n\t\t\t\t\tinfo.badTime = false;\n\t\t\t\t}\n\t\t\t\tif (_bindMsgId) {\n\t\t\t\t\tLOG((\"Message Info: bad message notification received\"\n\t\t\t\t\t\t\" while binding temp key, restarting.\"));\n\t\t\t\t\treturn HandleResult::RestartConnection;\n\t\t\t\t}\n\t\t\t\tLOG((\"Message Info: bad message notification received, msgId %1, error_code %2\").arg(data.vbad_msg_id().v).arg(errorCode));\n\t\t\t\treturn HandleResult::ResetSession;\n\t\t\t}\n\t\t} else { // fatal (except 48, but it must not get here)\n\t\t\tconst auto badMsgId = mtpMsgId(data.vbad_msg_id().v);\n\t\t\tconst auto requestId = wasSent(resendId);\n\t\t\tif (_bindMsgId) {\n\t\t\t\tLOG((\"Message Error: fatal bad message notification received\"\n\t\t\t\t\t\" while binding temp key, restarting.\"));\n\t\t\t\treturn HandleResult::RestartConnection;\n\t\t\t} else if (requestId) {\n\t\t\t\tLOG((\"Message Error: \"\n\t\t\t\t\t\"fatal bad message notification received, \"\n\t\t\t\t\t\"msgId %1, error_code %2, requestId: %3\"\n\t\t\t\t\t).arg(badMsgId\n\t\t\t\t\t).arg(errorCode\n\t\t\t\t\t).arg(requestId));\n\t\t\t\tauto reply = mtpBuffer();\n\t\t\t\tMTPRpcError(MTP_rpc_error(\n\t\t\t\t\tMTP_int(500),\n\t\t\t\t\tMTP_string(\"PROTOCOL_ERROR\")\n\t\t\t\t)).write(reply);\n\n\t\t\t\t// Save rpc_error for processing in the main thread.\n\t\t\t\tQWriteLocker locker(_sessionData->haveReceivedMutex());\n\t\t\t\t_sessionData->haveReceivedMessages().push_back({\n\t\t\t\t\t.reply = std::move(reply),\n\t\t\t\t\t.outerMsgId = info.outerMsgId,\n\t\t\t\t\t.requestId = requestId,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tDEBUG_LOG((\"Message Error: \"\n\t\t\t\t\t\"such message was not sent recently %1\").arg(badMsgId));\n\t\t\t}\n\t\t\treturn info.badTime\n\t\t\t\t? HandleResult::Ignored\n\t\t\t\t: HandleResult::Success;\n\t\t}\n\t} return HandleResult::Success;\n\n\tcase mtpc_bad_server_salt: {\n\t\tMTPBadMsgNotification msg;\n\t\tif (!msg.read(from, end)) {\n\t\t\treturn HandleResult::ParseError;\n\t\t}\n\t\tconst auto &data = msg.c_bad_server_salt();\n\t\tDEBUG_LOG((\"Message Info: bad server salt received (error_code %4) for msg_id = %1, seq_no = %2, new salt: %3\").arg(data.vbad_msg_id().v).arg(data.vbad_msg_seqno().v).arg(data.vnew_server_salt().v).arg(data.verror_code().v));\n\n\t\tconst auto resendId = data.vbad_msg_id().v;\n\t\tif (!wasSent(resendId)) {\n\t\t\tDEBUG_LOG((\"Message Error: such message was not sent recently %1\").arg(resendId));\n\t\t\treturn (info.badTime ? HandleResult::Ignored : HandleResult::Success);\n\t\t}\n\n\t\t_sessionSalt = data.vnew_server_salt().v;\n\n\t\t// Don't force time update here.\n\t\tbase::unixtime::update(info.serverTime);\n\n\t\tif (_bindMsgId) {\n\t\t\tLOG((\"Message Info: bad_server_salt received while binding temp key, restarting.\"));\n\t\t\treturn HandleResult::RestartConnection;\n\t\t}\n\n\t\tif (setState(ConnectedState, ConnectingState)) {\n\t\t\tresendAll();\n\t\t}\n\n\t\tinfo.badTime = false;\n\n\t\tDEBUG_LOG((\"Message Info: unixtime updated, now %1, server_salt updated, now %2, resending...\").arg(info.serverTime).arg(info.serverSalt));\n\t\tresend(resendId);\n\t} return HandleResult::Success;\n\n\tcase mtpc_msgs_state_info: {\n\t\tMTPMsgsStateInfo msg;\n\t\tif (!msg.read(from, end)) {\n\t\t\treturn HandleResult::ParseError;\n\t\t}\n\t\tauto &data = msg.c_msgs_state_info();\n\n\t\tauto reqMsgId = data.vreq_msg_id().v;\n\t\tauto &states = data.vinfo().v;\n\n\t\tDEBUG_LOG((\"Message Info: msg state received, msgId %1, reqMsgId: %2, HEX states %3\").arg(msgId).arg(reqMsgId).arg(Logs::mb(states.data(), states.length()).str()));\n\t\tconst auto i = _stateAndResendRequests.find(reqMsgId);\n\t\tif (i == _stateAndResendRequests.end()) {\n\t\t\tDEBUG_LOG((\"Message Error: such message was not sent recently %1\").arg(reqMsgId));\n\t\t\treturn info.badTime\n\t\t\t\t? HandleResult::Ignored\n\t\t\t\t: HandleResult::Success;\n\t\t}\n\t\tif (info.badTime) {\n\t\t\tif (info.serverSalt) {\n\t\t\t\t_sessionSalt = info.serverSalt; // requestsFixTimeSalt with no lookup\n\t\t\t}\n\t\t\tcorrectUnixtimeWithBadLocal(info.serverTime);\n\n\t\t\tDEBUG_LOG((\"Message Info: unixtime updated from mtpc_msgs_state_info, now %1\").arg(info.serverTime));\n\n\t\t\tinfo.badTime = false;\n\t\t}\n\t\tconst auto originalRequest = i->second;\n\t\tAssert(originalRequest->size() > 8);\n\n\t\trequestsAcked(QVector<MTPlong>(1, MTP_long(reqMsgId)), true);\n\n\t\tauto rFrom = originalRequest->constData() + 8;\n\t\tconst auto rEnd = originalRequest->constData() + originalRequest->size();\n\t\tif (mtpTypeId(*rFrom) == mtpc_msgs_state_req) {\n\t\t\tMTPMsgsStateReq request;\n\t\t\tif (!request.read(rFrom, rEnd)) {\n\t\t\t\tLOG((\"Message Error: could not parse sent msgs_state_req\"));\n\t\t\t\treturn HandleResult::ParseError;\n\t\t\t}\n\t\t\thandleMsgsStates(request.c_msgs_state_req().vmsg_ids().v, states);\n\t\t} else {\n\t\t\tMTPMsgResendReq request;\n\t\t\tif (!request.read(rFrom, rEnd)) {\n\t\t\t\tLOG((\"Message Error: could not parse sent msgs_resend_req\"));\n\t\t\t\treturn HandleResult::ParseError;\n\t\t\t}\n\t\t\thandleMsgsStates(request.c_msg_resend_req().vmsg_ids().v, states);\n\t\t}\n\t} return HandleResult::Success;\n\n\tcase mtpc_msgs_all_info: {\n\t\tif (info.badTime) {\n\t\t\tDEBUG_LOG((\"Message Info: skipping with bad time...\"));\n\t\t\treturn HandleResult::Ignored;\n\t\t}\n\n\t\tMTPMsgsAllInfo msg;\n\t\tif (!msg.read(from, end)) {\n\t\t\treturn HandleResult::ParseError;\n\t\t}\n\t\tauto &data = msg.c_msgs_all_info();\n\t\tauto &ids = data.vmsg_ids().v;\n\t\tauto &states = data.vinfo().v;\n\n\t\tDEBUG_LOG((\"Message Info: msgs all info received, msgId %1, reqMsgIds: %2, states %3\").arg(\n\t\t\tQString::number(msgId),\n\t\t\tLogIdsVector(ids),\n\t\t\tLogs::mb(states.data(), states.length()).str()));\n\n\t\thandleMsgsStates(ids, states);\n\t} return HandleResult::Success;\n\n\tcase mtpc_msg_detailed_info: {\n\t\tMTPMsgDetailedInfo msg;\n\t\tif (!msg.read(from, end)) {\n\t\t\treturn HandleResult::ParseError;\n\t\t}\n\t\tconst auto &data(msg.c_msg_detailed_info());\n\n\t\tDEBUG_LOG((\"Message Info: msg detailed info, sent msgId %1, answerId %2, status %3, bytes %4\").arg(data.vmsg_id().v).arg(data.vanswer_msg_id().v).arg(data.vstatus().v).arg(data.vbytes().v));\n\n\t\tQVector<MTPlong> ids(1, data.vmsg_id());\n\t\tif (info.badTime) {\n\t\t\tif (requestsFixTimeSalt(ids, info)) {\n\t\t\t\tinfo.badTime = false;\n\t\t\t} else {\n\t\t\t\tDEBUG_LOG((\"Message Info: error, such message was not sent recently %1\").arg(data.vmsg_id().v));\n\t\t\t\treturn HandleResult::Ignored;\n\t\t\t}\n\t\t}\n\t\trequestsAcked(ids);\n\n\t\tconst auto resMsgId = data.vanswer_msg_id();\n\t\tif (_receivedMessageIds.lookup(resMsgId.v) != ReceivedIdsManager::State::NotFound) {\n\t\t\t_ackRequestData.push_back(resMsgId);\n\t\t} else {\n\t\t\tDEBUG_LOG((\"Message Info: answer message %1 was not received, requesting...\").arg(resMsgId.v));\n\t\t\t_resendRequestData.push_back(resMsgId);\n\t\t}\n\t} return HandleResult::Success;\n\n\tcase mtpc_msg_new_detailed_info: {\n\t\tif (info.badTime) {\n\t\t\tDEBUG_LOG((\"Message Info: skipping msg_new_detailed_info with bad time...\"));\n\t\t\treturn HandleResult::Ignored;\n\t\t}\n\t\tMTPMsgDetailedInfo msg;\n\t\tif (!msg.read(from, end)) {\n\t\t\treturn HandleResult::ParseError;\n\t\t}\n\t\tconst auto &data(msg.c_msg_new_detailed_info());\n\n\t\tDEBUG_LOG((\"Message Info: msg new detailed info, answerId %2, status %3, bytes %4\").arg(data.vanswer_msg_id().v).arg(data.vstatus().v).arg(data.vbytes().v));\n\n\t\tconst auto resMsgId = data.vanswer_msg_id();\n\t\tif (_receivedMessageIds.lookup(resMsgId.v) != ReceivedIdsManager::State::NotFound) {\n\t\t\t_ackRequestData.push_back(resMsgId);\n\t\t} else {\n\t\t\tDEBUG_LOG((\"Message Info: answer message %1 was not received, requesting...\").arg(resMsgId.v));\n\t\t\t_resendRequestData.push_back(resMsgId);\n\t\t}\n\t} return HandleResult::Success;\n\n\tcase mtpc_rpc_result: {\n\t\tif (from + 3 > end) {\n\t\t\treturn HandleResult::ParseError;\n\t\t}\n\t\tauto response = mtpBuffer();\n\n\t\tMTPlong reqMsgId;\n\t\tif (!reqMsgId.read(++from, end)) {\n\t\t\treturn HandleResult::ParseError;\n\t\t}\n\t\tconst auto requestMsgId = reqMsgId.v;\n\n\t\tDEBUG_LOG((\"RPC Info: response received for %1, queueing...\").arg(requestMsgId));\n\n\t\tQVector<MTPlong> ids(1, reqMsgId);\n\t\tif (info.badTime) {\n\t\t\tif (requestsFixTimeSalt(ids, info)) {\n\t\t\t\tinfo.badTime = false;\n\t\t\t} else {\n\t\t\t\tDEBUG_LOG((\"Message Info: error, such message was not sent recently %1\").arg(requestMsgId));\n\t\t\t\treturn HandleResult::Ignored;\n\t\t\t}\n\t\t}\n\n\t\tmtpTypeId typeId = from[0];\n\t\tif (typeId == mtpc_gzip_packed) {\n\t\t\tDEBUG_LOG((\"RPC Info: gzip container\"));\n\t\t\tresponse = ungzip(++from, end);\n\t\t\tif (response.empty()) {\n\t\t\t\treturn HandleResult::RestartConnection;\n\t\t\t}\n\t\t\ttypeId = response[0];\n\t\t} else {\n\t\t\tresponse.resize(end - from);\n\t\t\tmemcpy(response.data(), from, (end - from) * sizeof(mtpPrime));\n\t\t}\n\t\tif (typeId == mtpc_rpc_error) {\n\t\t\tif (IsDestroyedTemporaryKeyError(response)) {\n\t\t\t\treturn HandleResult::DestroyTemporaryKey;\n\t\t\t}\n\t\t\t// An error could be some RPC_CALL_FAIL or other error inside\n\t\t\t// the initConnection, so we're not sure yet that it was inited.\n\t\t\t// Wait till a good response is received.\n\t\t} else {\n\t\t\t_sessionData->notifyConnectionInited(*_options);\n\t\t}\n\t\trequestsAcked(ids, true);\n\n\t\tconst auto bindResult = handleBindResponse(requestMsgId, response);\n\t\tif (bindResult != HandleResult::Ignored) {\n\t\t\treturn bindResult;\n\t\t}\n\t\tconst auto requestId = wasSent(requestMsgId);\n\t\tif (requestId && requestId != mtpRequestId(0xFFFFFFFF)) {\n\t\t\t// Save rpc_result for processing in the main thread.\n\t\t\tQWriteLocker locker(_sessionData->haveReceivedMutex());\n\t\t\t_sessionData->haveReceivedMessages().push_back({\n\t\t\t\t.reply = std::move(response),\n\t\t\t\t.outerMsgId = info.outerMsgId,\n\t\t\t\t.requestId = requestId,\n\t\t\t});\n\t\t} else {\n\t\t\tDEBUG_LOG((\"RPC Info: requestId not found for msgId %1\").arg(requestMsgId));\n\t\t}\n\t} return HandleResult::Success;\n\n\tcase mtpc_new_session_created: {\n\t\tconst mtpPrime *start = from;\n\t\tMTPNewSession msg;\n\t\tif (!msg.read(from, end)) {\n\t\t\treturn HandleResult::ParseError;\n\t\t}\n\t\tconst auto &data(msg.c_new_session_created());\n\n\t\tif (info.badTime) {\n\t\t\tif (requestsFixTimeSalt(QVector<MTPlong>(1, data.vfirst_msg_id()), info)) {\n\t\t\t\tinfo.badTime = false;\n\t\t\t} else {\n\t\t\t\tDEBUG_LOG((\"Message Info: error, such message was not sent recently %1\").arg(data.vfirst_msg_id().v));\n\t\t\t\treturn HandleResult::Ignored;\n\t\t\t}\n\t\t}\n\n\t\tDEBUG_LOG((\"Message Info: new server session created, unique_id %1, first_msg_id %2, server_salt %3\").arg(data.vunique_id().v).arg(data.vfirst_msg_id().v).arg(data.vserver_salt().v));\n\t\t_sessionSalt = data.vserver_salt().v;\n\n\t\tmtpMsgId firstMsgId = data.vfirst_msg_id().v;\n\t\tQVector<quint64> toResend;\n\t\t{\n\t\t\tQReadLocker locker(_sessionData->haveSentMutex());\n\t\t\tconst auto &haveSent = _sessionData->haveSentMap();\n\t\t\ttoResend.reserve(haveSent.size());\n\t\t\tfor (const auto &[msgId, request] : haveSent) {\n\t\t\t\tif (msgId >= firstMsgId) {\n\t\t\t\t\tbreak;\n\t\t\t\t} else if (request->requestId) {\n\t\t\t\t\ttoResend.push_back(msgId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor (const auto msgId : toResend) {\n\t\t\tresend(msgId, 10);\n\t\t}\n\n\t\tmtpBuffer update(from - start);\n\t\tif (from > start) memcpy(update.data(), start, (from - start) * sizeof(mtpPrime));\n\n\t\t// Notify main process about new session - need to get difference.\n\t\tQWriteLocker locker(_sessionData->haveReceivedMutex());\n\t\t_sessionData->haveReceivedMessages().push_back({\n\t\t\t.reply = update,\n\t\t\t.outerMsgId = info.outerMsgId,\n\t\t});\n\t} return HandleResult::Success;\n\n\tcase mtpc_pong: {\n\t\tMTPPong msg;\n\t\tif (!msg.read(from, end)) {\n\t\t\treturn HandleResult::ParseError;\n\t\t}\n\t\tconst auto &data(msg.c_pong());\n\t\tDEBUG_LOG((\"Message Info: pong received, msg_id: %1, ping_id: %2\").arg(data.vmsg_id().v).arg(data.vping_id().v));\n\n\t\tif (!wasSent(data.vmsg_id().v)) {\n\t\t\tDEBUG_LOG((\"Message Error: such msg_id %1 ping_id %2 was not sent recently\").arg(data.vmsg_id().v).arg(data.vping_id().v));\n\t\t\treturn HandleResult::Ignored;\n\t\t}\n\t\tif (data.vping_id().v == _pingId) {\n\t\t\t_pingId = 0;\n\t\t} else {\n\t\t\tDEBUG_LOG((\"Message Info: just pong...\"));\n\t\t}\n\n\t\tQVector<MTPlong> ids(1, data.vmsg_id());\n\t\tif (info.badTime) {\n\t\t\tif (requestsFixTimeSalt(ids, info)) {\n\t\t\t\tinfo.badTime = false;\n\t\t\t} else {\n\t\t\t\treturn HandleResult::Ignored;\n\t\t\t}\n\t\t}\n\t\trequestsAcked(ids, true);\n\t} return HandleResult::Success;\n\n\t}\n\n\tif (info.badTime) {\n\t\tDEBUG_LOG((\"Message Error: bad time in updates cons, must create new session\"));\n\t\treturn HandleResult::ResetSession;\n\t}\n\n\tif (_currentDcType == DcType::Regular) {\n\t\tmtpBuffer update(end - from);\n\t\tif (end > from) {\n\t\t\tmemcpy(update.data(), from, (end - from) * sizeof(mtpPrime));\n\t\t}\n\n\t\t// Notify main process about the new updates.\n\t\tQWriteLocker locker(_sessionData->haveReceivedMutex());\n\t\t_sessionData->haveReceivedMessages().push_back({\n\t\t\t.reply = update,\n\t\t\t.outerMsgId = info.outerMsgId,\n\t\t});\n\t} else {\n\t\tLOG((\"Message Error: unexpected updates in dcType: %1\"\n\t\t\t).arg(static_cast<int>(_currentDcType)));\n\t}\n\n\treturn HandleResult::Success;\n}\n\nSessionPrivate::HandleResult SessionPrivate::handleBindResponse(\n\t\tmtpMsgId requestMsgId,\n\t\tconst mtpBuffer &response) {\n\tif (!_keyCreator || !_bindMsgId || _bindMsgId != requestMsgId) {\n\t\treturn HandleResult::Ignored;\n\t}\n\t_bindMsgId = 0;\n\n\tconst auto result = _keyCreator->handleBindResponse(response);\n\tswitch (result) {\n\tcase DcKeyBindState::Success:\n\t\tif (!_sessionData->releaseKeyCreationOnDone(\n\t\t\t_encryptionKey,\n\t\t\tbase::take(_keyCreator)->bindPersistentKey())) {\n\t\t\treturn HandleResult::DestroyTemporaryKey;\n\t\t}\n\t\t_sessionData->queueNeedToResumeAndSend();\n\t\treturn HandleResult::Success;\n\tcase DcKeyBindState::DefinitelyDestroyed:\n\t\tif (destroyOldEnoughPersistentKey()) {\n\t\t\treturn HandleResult::DestroyTemporaryKey;\n\t\t}\n\t\t[[fallthrough]];\n\tcase DcKeyBindState::Failed:\n\t\t_sessionData->queueNeedToResumeAndSend();\n\t\treturn HandleResult::Success;\n\t}\n\tUnexpected(\"Result of BoundKeyCreator::handleBindResponse.\");\n}\n\nmtpBuffer SessionPrivate::ungzip(const mtpPrime *from, const mtpPrime *end) const {\n\tmtpBuffer result; // * 4 because of mtpPrime type\n\tresult.resize(0);\n\n\tMTPstring packed;\n\tif (!packed.read(from, end)) { // read packed string as serialized mtp string type\n\t\tLOG((\"RPC Error: could not read gziped bytes.\"));\n\t\treturn result;\n\t}\n\tuint32 packedLen = packed.v.size(), unpackedChunk = packedLen;\n\n\tz_stream stream;\n\tstream.zalloc = 0;\n\tstream.zfree = 0;\n\tstream.opaque = 0;\n\tstream.avail_in = 0;\n\tstream.next_in = 0;\n\tint res = inflateInit2(&stream, 16 + MAX_WBITS);\n\tif (res != Z_OK) {\n\t\tLOG((\"RPC Error: could not init zlib stream, code: %1\").arg(res));\n\t\treturn result;\n\t}\n\tstream.avail_in = packedLen;\n\tstream.next_in = reinterpret_cast<Bytef*>(packed.v.data());\n\n\tstream.avail_out = 0;\n\twhile (!stream.avail_out) {\n\t\tresult.resize(result.size() + unpackedChunk);\n\t\tstream.avail_out = unpackedChunk * sizeof(mtpPrime);\n\t\tstream.next_out = (Bytef*)&result[result.size() - unpackedChunk];\n\t\tint res = inflate(&stream, Z_NO_FLUSH);\n\t\tif (res != Z_OK && res != Z_STREAM_END) {\n\t\t\tinflateEnd(&stream);\n\t\t\tLOG((\"RPC Error: could not unpack gziped data, code: %1\").arg(res));\n\t\t\tDEBUG_LOG((\"RPC Error: bad gzip: %1\").arg(Logs::mb(packed.v.constData(), packedLen).str()));\n\t\t\treturn mtpBuffer();\n\t\t}\n\t}\n\tif (stream.avail_out & 0x03) {\n\t\tuint32 badSize = result.size() * sizeof(mtpPrime) - stream.avail_out;\n\t\tLOG((\"RPC Error: bad length of unpacked data %1\").arg(badSize));\n\t\tDEBUG_LOG((\"RPC Error: bad unpacked data %1\").arg(Logs::mb(result.data(), badSize).str()));\n\t\treturn mtpBuffer();\n\t}\n\tresult.resize(result.size() - (stream.avail_out >> 2));\n\tinflateEnd(&stream);\n\tif (!result.size()) {\n\t\tLOG((\"RPC Error: bad length of unpacked data 0\"));\n\t}\n\treturn result;\n}\n\nbool SessionPrivate::requestsFixTimeSalt(const QVector<MTPlong> &ids, const OuterInfo &info) {\n\tfor (const auto &id : ids) {\n\t\tif (wasSent(id.v)) {\n\t\t\t// Found such msg_id in recent acked or in recent sent requests.\n\t\t\tif (info.serverSalt) {\n\t\t\t\t_sessionSalt = info.serverSalt;\n\t\t\t}\n\t\t\tcorrectUnixtimeWithBadLocal(info.serverTime);\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid SessionPrivate::correctUnixtimeByFastRequest(\n\t\tconst QVector<MTPlong> &ids,\n\t\tTimeId serverTime) {\n\tconst auto now = crl::now();\n\n\tQReadLocker locker(_sessionData->haveSentMutex());\n\tconst auto &haveSent = _sessionData->haveSentMap();\n\tfor (const auto &id : ids) {\n\t\tconst auto i = haveSent.find(id.v);\n\t\tif (i == haveSent.end()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto duration = (now - i->second->lastSentTime);\n\t\tif (duration < 0 || duration > SyncTimeRequestDuration) {\n\t\t\tcontinue;\n\t\t}\n\t\tlocker.unlock();\n\n\t\tSyncTimeRequestDuration = duration;\n\t\tbase::unixtime::update(serverTime);\n\t\treturn;\n\t}\n}\n\nvoid SessionPrivate::correctUnixtimeWithBadLocal(TimeId serverTime) {\n\tSyncTimeRequestDuration = kFastRequestDuration;\n\tbase::unixtime::update(serverTime, true);\n}\n\nvoid SessionPrivate::requestsAcked(const QVector<MTPlong> &ids, bool byResponse) {\n\tDEBUG_LOG((\"Message Info: requests acked, ids %1\").arg(LogIdsVector(ids)));\n\n\tQVector<MTPlong> toAckMore;\n\t{\n\t\tQWriteLocker locker2(_sessionData->haveSentMutex());\n\t\tauto &haveSent = _sessionData->haveSentMap();\n\n\t\tfor (const auto &wrappedMsgId : ids) {\n\t\t\tconst auto msgId = wrappedMsgId.v;\n\t\t\tif (const auto i = _sentContainers.find(msgId); i != end(_sentContainers)) {\n\t\t\t\tDEBUG_LOG((\"Message Info: container ack received, msgId %1\").arg(msgId));\n\t\t\t\tconst auto &list = i->second.messages;\n\t\t\t\ttoAckMore.reserve(toAckMore.size() + list.size());\n\t\t\t\tfor (const auto msgId : list) {\n\t\t\t\t\ttoAckMore.push_back(MTP_long(msgId));\n\t\t\t\t}\n\t\t\t\t_sentContainers.erase(i);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (const auto i = _stateAndResendRequests.find(msgId); i != end(_stateAndResendRequests)) {\n\t\t\t\t_stateAndResendRequests.erase(i);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (const auto i = haveSent.find(msgId); i != end(haveSent)) {\n\t\t\t\tconst auto requestId = i->second->requestId;\n\n\t\t\t\tif (!byResponse && _instance->hasCallback(requestId)) {\n\t\t\t\t\tDEBUG_LOG((\"Message Info: ignoring ACK for msgId %1 because request %2 requires a response\").arg(msgId).arg(requestId));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\thaveSent.erase(i);\n\n\t\t\t\t_ackedIds.emplace(msgId, requestId);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tDEBUG_LOG((\"Message Info: msgId %1 was not found in recent sent, while acking requests, searching in resend...\").arg(msgId));\n\t\t\tif (const auto i = _resendingIds.find(msgId); i != end(_resendingIds)) {\n\t\t\t\tconst auto requestId = i->second;\n\n\t\t\t\tif (!byResponse && _instance->hasCallback(requestId)) {\n\t\t\t\t\tDEBUG_LOG((\"Message Info: ignoring ACK for msgId %1 because request %2 requires a response\").arg(msgId).arg(requestId));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t_resendingIds.erase(i);\n\n\t\t\t\tQWriteLocker locker4(_sessionData->toSendMutex());\n\t\t\t\tauto &toSend = _sessionData->toSendMap();\n\t\t\t\tconst auto j = toSend.find(requestId);\n\t\t\t\tif (j == end(toSend)) {\n\t\t\t\t\tDEBUG_LOG((\"Message Info: msgId %1 was found in recent resent, requestId %2 was not found in prepared to send\").arg(msgId).arg(requestId));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (j->second->requestId != requestId) {\n\t\t\t\t\tDEBUG_LOG((\"Message Error: for msgId %1 found resent request, requestId %2, contains requestId %3\").arg(msgId).arg(requestId).arg(j->second->requestId));\n\t\t\t\t} else {\n\t\t\t\t\tDEBUG_LOG((\"Message Info: acked msgId %1 that was prepared to resend, requestId %2\").arg(msgId).arg(requestId));\n\t\t\t\t}\n\n\t\t\t\t_ackedIds.emplace(msgId, j->second->requestId);\n\n\t\t\t\ttoSend.erase(j);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tDEBUG_LOG((\"Message Info: msgId %1 was not found in recent resent either\").arg(msgId));\n\t\t}\n\t}\n\n\tauto ackedCount = _ackedIds.size();\n\tif (ackedCount > kIdsBufferSize) {\n\t\tDEBUG_LOG((\"Message Info: removing some old acked sent msgIds %1\").arg(ackedCount - kIdsBufferSize));\n\t\twhile (ackedCount-- > kIdsBufferSize) {\n\t\t\t_ackedIds.erase(_ackedIds.begin());\n\t\t}\n\t}\n\n\tif (toAckMore.size()) {\n\t\trequestsAcked(toAckMore);\n\t}\n}\n\nvoid SessionPrivate::handleMsgsStates(const QVector<MTPlong> &ids, const QByteArray &states) {\n\tconst auto idsCount = ids.size();\n\tif (!idsCount) {\n\t\tDEBUG_LOG((\"Message Info: void ids vector in handleMsgsStates()\"));\n\t\treturn;\n\t}\n\tif (states.size() != idsCount) {\n\t\tLOG((\"Message Error: got less states than required ids count.\"));\n\t\treturn;\n\t}\n\n\tauto acked = QVector<MTPlong>();\n\tacked.reserve(idsCount);\n\tfor (auto i = 0; i != idsCount; ++i) {\n\t\tconst auto state = states[i];\n\t\tconst auto requestMsgId = ids[i].v;\n\t\t{\n\t\t\tQReadLocker locker(_sessionData->haveSentMutex());\n\t\t\tif (!_sessionData->haveSentMap().contains(requestMsgId)) {\n\t\t\t\tDEBUG_LOG((\"Message Info: state was received for msgId %1, but request is not found, looking in resent requests...\").arg(requestMsgId));\n\t\t\t\tconst auto reqIt = _resendingIds.find(requestMsgId);\n\t\t\t\tif (reqIt != _resendingIds.cend()) {\n\t\t\t\t\tif ((state & 0x07) != 0x04) { // was received\n\t\t\t\t\t\tDEBUG_LOG((\"Message Info: state was received for msgId %1, state %2, already resending in container\").arg(requestMsgId).arg((int32)state));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tDEBUG_LOG((\"Message Info: state was received for msgId %1, state %2, ack, cancelling resend\").arg(requestMsgId).arg((int32)state));\n\t\t\t\t\t\tacked.push_back(MTP_long(requestMsgId)); // will remove from resend in requestsAcked\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tDEBUG_LOG((\"Message Info: msgId %1 was not found in recent resent either\").arg(requestMsgId));\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif ((state & 0x07) != 0x04) { // was received\n\t\t\tDEBUG_LOG((\"Message Info: state was received for msgId %1, state %2, resending in container\").arg(requestMsgId).arg((int32)state));\n\t\t\tresend(requestMsgId, 10);\n\t\t} else {\n\t\t\tDEBUG_LOG((\"Message Info: state was received for msgId %1, state %2, ack\").arg(requestMsgId).arg((int32)state));\n\t\t\tacked.push_back(MTP_long(requestMsgId));\n\t\t}\n\t}\n\trequestsAcked(acked);\n}\n\nvoid SessionPrivate::clearSpecialMsgId(mtpMsgId msgId) {\n\tif (msgId == _pingMsgId) {\n\t\t_pingMsgId = 0;\n\t\t_pingId = 0;\n\t} else if (msgId == _bindMsgId) {\n\t\t_bindMsgId = 0;\n\t}\n}\n\nvoid SessionPrivate::resend(mtpMsgId msgId, crl::time msCanWait) {\n\tconst auto guard = gsl::finally([&] {\n\t\tclearSpecialMsgId(msgId);\n\t\tif (msCanWait >= 0) {\n\t\t\t_sessionData->queueSendAnything(msCanWait);\n\t\t}\n\t});\n\n\tif (const auto i = _sentContainers.find(msgId); i != end(_sentContainers)) {\n\t\tDEBUG_LOG((\"Message Info: resending container, msgId %1\").arg(msgId));\n\t\tconst auto ids = std::move(i->second.messages);\n\t\t_sentContainers.erase(i);\n\n\t\tfor (const auto innerMsgId : ids) {\n\t\t\tresend(innerMsgId, -1);\n\t\t}\n\t\treturn;\n\t}\n\tauto lock = QWriteLocker(_sessionData->haveSentMutex());\n\tauto &haveSent = _sessionData->haveSentMap();\n\tauto i = haveSent.find(msgId);\n\tif (i == haveSent.end()) {\n\t\treturn;\n\t}\n\tauto request = i->second;\n\thaveSent.erase(i);\n\tlock.unlock();\n\n\trequest->lastSentTime = crl::now();\n\trequest->forceSendInContainer = true;\n\t_resendingIds.emplace(msgId, request->requestId);\n\t{\n\t\tQWriteLocker locker(_sessionData->toSendMutex());\n\t\t_sessionData->toSendMap().emplace(request->requestId, request);\n\t}\n}\n\nvoid SessionPrivate::resendAll() {\n\tauto lock = QWriteLocker(_sessionData->haveSentMutex());\n\tauto haveSent = base::take(_sessionData->haveSentMap());\n\tlock.unlock();\n\t{\n\t\tauto lock = QWriteLocker(_sessionData->toSendMutex());\n\t\tauto &toSend = _sessionData->toSendMap();\n\t\tconst auto now = crl::now();\n\t\tfor (auto &[msgId, request] : haveSent) {\n\t\t\tconst auto requestId = request->requestId;\n\t\t\trequest->lastSentTime = now;\n\t\t\trequest->forceSendInContainer = true;\n\t\t\t_resendingIds.emplace(msgId, requestId);\n\t\t\ttoSend.emplace(requestId, std::move(request));\n\t\t}\n\t}\n\n\t_sessionData->queueSendAnything();\n}\n\nvoid SessionPrivate::onConnected(\n\t\tnot_null<AbstractConnection*> connection) {\n\tdisconnect(connection, &AbstractConnection::connected, nullptr, nullptr);\n\tif (!connection->isConnected()) {\n\t\tLOG((\"Connection Error: not connected in onConnected(), \"\n\t\t\t\"state: %1\").arg(connection->debugState()));\n\t\treturn restart();\n\t}\n\n\t_waitForConnected = kMinConnectedTimeout;\n\t_waitForConnectedTimer.cancel();\n\n\tconst auto i = ranges::find(\n\t\t_testConnections,\n\t\tconnection.get(),\n\t\t[](const TestConnection &test) { return test.data.get(); });\n\tAssert(i != end(_testConnections));\n\tconst auto my = i->priority;\n\tconst auto j = ranges::find_if(\n\t\t_testConnections,\n\t\t[&](const TestConnection &test) { return test.priority > my; });\n\tif (j != end(_testConnections)) {\n\t\tDEBUG_LOG((\"MTP Info: connection %1 succeed, waiting for %2.\").arg(\n\t\t\ti->data->tag(),\n\t\t\tj->data->tag()));\n\t\t_waitForBetterTimer.callOnce(kWaitForBetterTimeout);\n\t} else {\n\t\tDEBUG_LOG((\"MTP Info: connection through IPv4 succeed.\"));\n\t\t_waitForBetterTimer.cancel();\n\t\t_connection = std::move(i->data);\n\t\t_testConnections.clear();\n\t\tcheckAuthKey();\n\t}\n}\n\nvoid SessionPrivate::onDisconnected(\n\t\tnot_null<AbstractConnection*> connection) {\n\tremoveTestConnection(connection);\n\n\tif (_testConnections.empty()) {\n\t\tdestroyAllConnections();\n\t\trestart();\n\t} else {\n\t\tconfirmBestConnection();\n\t}\n}\n\nvoid SessionPrivate::confirmBestConnection() {\n\tif (_waitForBetterTimer.isActive()) {\n\t\treturn;\n\t}\n\tconst auto i = ranges::max_element(\n\t\t_testConnections,\n\t\tstd::less<>(),\n\t\t[](const TestConnection &test) {\n\t\t\treturn test.data->isConnected() ? test.priority : -1;\n\t\t});\n\tAssert(i != end(_testConnections));\n\tif (!i->data->isConnected()) {\n\t\treturn;\n\t}\n\n\tDEBUG_LOG((\"MTP Info: can't connect through better, using %1.\"\n\t\t).arg(i->data->tag()));\n\n\t_connection = std::move(i->data);\n\t_testConnections.clear();\n\n\tcheckAuthKey();\n}\n\nvoid SessionPrivate::removeTestConnection(\n\t\tnot_null<AbstractConnection*> connection) {\n\t_testConnections.erase(\n\t\tranges::remove(\n\t\t\t_testConnections,\n\t\t\tconnection.get(),\n\t\t\t[](const TestConnection &test) { return test.data.get(); }),\n\t\tend(_testConnections));\n}\n\nvoid SessionPrivate::checkAuthKey() {\n\tif (_keyId) {\n\t\tauthKeyChecked();\n\t} else if (_instance->isKeysDestroyer()) {\n\t\tapplyAuthKey(_sessionData->getPersistentKey());\n\t} else {\n\t\tapplyAuthKey(_sessionData->getTemporaryKey(\n\t\t\tTemporaryKeyTypeByDcType(_currentDcType)));\n\t}\n}\n\nvoid SessionPrivate::updateAuthKey() {\n\tif (_instance->isKeysDestroyer() || _keyCreator || !_connection) {\n\t\treturn;\n\t}\n\n\tDEBUG_LOG((\"AuthKey Info: Connection updating key from Session, dc %1\"\n\t\t).arg(_shiftedDcId));\n\tapplyAuthKey(_sessionData->getTemporaryKey(\n\t\tTemporaryKeyTypeByDcType(_currentDcType)));\n}\n\nvoid SessionPrivate::setCurrentKeyId(uint64 newKeyId) {\n\tif (_keyId == newKeyId) {\n\t\treturn;\n\t}\n\t_keyId = newKeyId;\n\n\tDEBUG_LOG((\"MTP Info: auth key id set to id %1\").arg(newKeyId));\n\tchangeSessionId();\n}\n\nvoid SessionPrivate::applyAuthKey(AuthKeyPtr &&encryptionKey) {\n\t_encryptionKey = std::move(encryptionKey);\n\tconst auto newKeyId = _encryptionKey ? _encryptionKey->keyId() : 0;\n\tif (_keyId) {\n\t\tif (_keyId == newKeyId) {\n\t\t\treturn;\n\t\t}\n\t\tsetCurrentKeyId(0);\n\t\tDEBUG_LOG((\"MTP Info: auth_key id for dc %1 changed, restarting...\"\n\t\t\t).arg(_shiftedDcId));\n\t\tif (_connection) {\n\t\t\trestart();\n\t\t}\n\t\treturn;\n\t}\n\tif (!_connection) {\n\t\treturn;\n\t}\n\tsetCurrentKeyId(newKeyId);\n\tAssert(!_connection->sentEncryptedWithKeyId());\n\n\tDEBUG_LOG((\"AuthKey Info: Connection update key from Session, \"\n\t\t\"dc %1 result: %2\"\n\t\t).arg(_shiftedDcId\n\t\t).arg(Logs::mb(&_keyId, sizeof(_keyId)).str()));\n\tif (_keyId) {\n\t\treturn authKeyChecked();\n\t}\n\n\tif (_instance->isKeysDestroyer()) {\n\t\t// We are here to destroy an old key, so we're done.\n\t\tLOG((\"MTP Error: No key %1 in updateAuthKey() for destroying.\"\n\t\t\t).arg(_shiftedDcId));\n\t\t_instance->keyWasPossiblyDestroyed(_shiftedDcId);\n\t} else if (noMediaKeyWithExistingRegularKey()) {\n\t\tDEBUG_LOG((\"AuthKey Info: No key in updateAuthKey() for media, \"\n\t\t\t\"but someone has created regular, trying to acquire.\"));\n\t\tconst auto dcType = tryAcquireKeyCreation();\n\t\tif (_keyCreator && dcType != _currentDcType) {\n\t\t\tDEBUG_LOG((\"AuthKey Info: \"\n\t\t\t\t\"Dc type changed for creation, restarting.\"));\n\t\t\trestart();\n\t\t\treturn;\n\t\t}\n\t}\n\tif (_keyCreator) {\n\t\tDEBUG_LOG((\"AuthKey Info: No key in updateAuthKey(), creating.\"));\n\t\t_keyCreator->start(\n\t\t\tBareDcId(_shiftedDcId),\n\t\t\tgetProtocolDcId(),\n\t\t\t_connection.get(),\n\t\t\t&_instance->dcOptions());\n\t} else {\n\t\tDEBUG_LOG((\"AuthKey Info: No key in updateAuthKey(), \"\n\t\t\t\"but someone is creating already, waiting.\"));\n\t}\n}\n\nbool SessionPrivate::noMediaKeyWithExistingRegularKey() const {\n\treturn (TemporaryKeyTypeByDcType(_currentDcType)\n\t\t\t== TemporaryKeyType::MediaCluster)\n\t\t&& _sessionData->getTemporaryKey(TemporaryKeyType::Regular);\n}\n\nbool SessionPrivate::destroyOldEnoughPersistentKey() {\n\tExpects(_keyCreator != nullptr);\n\n\tconst auto key = _keyCreator->bindPersistentKey();\n\tAssert(key != nullptr);\n\n\tconst auto created = key->creationTime();\n\tif (created > 0 && crl::now() - created < kKeyOldEnoughForDestroy) {\n\t\treturn false;\n\t}\n\tconst auto instance = _instance;\n\tconst auto shiftedDcId = _shiftedDcId;\n\tconst auto keyId = key->keyId();\n\tInvokeQueued(instance, [=] {\n\t\tinstance->keyDestroyedOnServer(shiftedDcId, keyId);\n\t});\n\treturn true;\n}\n\nDcType SessionPrivate::tryAcquireKeyCreation() {\n\tif (_keyCreator) {\n\t\treturn _currentDcType;\n\t} else if (_instance->isKeysDestroyer()) {\n\t\treturn _realDcType;\n\t}\n\n\tconst auto acquired = _sessionData->acquireKeyCreation(_realDcType);\n\tif (acquired == CreatingKeyType::None) {\n\t\treturn _realDcType;\n\t}\n\n\tusing Result = DcKeyResult;\n\tusing Error = DcKeyError;\n\tauto delegate = BoundKeyCreator::Delegate();\n\tdelegate.unboundReady = [=](base::expected<Result, Error> result) {\n\t\tif (!result) {\n\t\t\treleaseKeyCreationOnFail();\n\t\t\tif (result.error() == Error::UnknownPublicKey) {\n\t\t\t\tif (_realDcType == DcType::Cdn) {\n\t\t\t\t\tLOG((\"Warning: CDN public RSA key not found\"));\n\t\t\t\t\trequestCDNConfig();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tLOG((\"AuthKey Error: could not choose public RSA key\"));\n\t\t\t}\n\t\t\trestart();\n\t\t\treturn;\n\t\t}\n\t\tDEBUG_LOG((\"AuthKey Info: unbound key creation succeed, \"\n\t\t\t\"ids: (%1, %2) server salts: (%3, %4)\"\n\t\t\t).arg(result->temporaryKey\n\t\t\t\t? result->temporaryKey->keyId()\n\t\t\t\t: 0\n\t\t\t).arg(result->persistentKey\n\t\t\t\t? result->persistentKey->keyId()\n\t\t\t\t: 0\n\t\t\t).arg(result->temporaryServerSalt\n\t\t\t).arg(result->persistentServerSalt));\n\n\t\t_sessionSalt = result->temporaryServerSalt;\n\t\tresult->temporaryKey->setExpiresAt(base::unixtime::now()\n\t\t\t+ kTemporaryExpiresIn\n\t\t\t+ kBindKeyAdditionalExpiresTimeout);\n\t\tif (_realDcType != DcType::Cdn) {\n\t\t\tauto key = result->persistentKey\n\t\t\t\t? std::move(result->persistentKey)\n\t\t\t\t: _sessionData->getPersistentKey();\n\t\t\tif (!key) {\n\t\t\t\treleaseKeyCreationOnFail();\n\t\t\t\trestart();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_keyCreator->bind(std::move(key));\n\t\t}\n\t\tapplyAuthKey(std::move(result->temporaryKey));\n\t\tif (_realDcType == DcType::Cdn) {\n\t\t\t_keyCreator = nullptr;\n\t\t\tif (!_sessionData->releaseCdnKeyCreationOnDone(_encryptionKey)) {\n\t\t\t\trestart();\n\t\t\t} else {\n\t\t\t\t_sessionData->queueNeedToResumeAndSend();\n\t\t\t}\n\t\t}\n\t};\n\tdelegate.sentSome = [=](uint64 size) {\n\t\tonSentSome(size);\n\t};\n\tdelegate.receivedSome = [=] {\n\t\tonReceivedSome();\n\t};\n\n\tauto request = DcKeyRequest();\n\trequest.persistentNeeded = (acquired == CreatingKeyType::Persistent);\n\trequest.temporaryExpiresIn = kTemporaryExpiresIn;\n\t_keyCreator = std::make_unique<BoundKeyCreator>(\n\t\trequest,\n\t\tstd::move(delegate));\n\tconst auto forceUseRegular = (_realDcType == DcType::MediaCluster)\n\t\t&& (acquired != CreatingKeyType::TemporaryMediaCluster);\n\treturn forceUseRegular ? DcType::Regular : _realDcType;\n}\n\nvoid SessionPrivate::authKeyChecked() {\n\tconnect(_connection, &AbstractConnection::receivedData, [=] {\n\t\thandleReceived();\n\t});\n\n\tif (_sessionSalt && setState(ConnectedState)) {\n\t\tresendAll();\n\t} // else receive salt in bad_server_salt first, then try to send all the requests\n\n\t_pingIdToSend = base::RandomValue<uint64>(); // get server_salt\n\t_sessionData->queueNeedToResumeAndSend();\n}\n\nvoid SessionPrivate::onError(\n\t\tnot_null<AbstractConnection*> connection,\n\t\tqint32 errorCode) {\n\tif (errorCode == -429) {\n\t\tLOG((\"Protocol Error: -429 flood code returned!\"));\n\t} else if (errorCode == -444) {\n\t\tLOG((\"Protocol Error: -444 bad dc_id code returned!\"));\n\t\tInvokeQueued(_instance, [instance = _instance] {\n\t\t\tinstance->badConfigurationError();\n\t\t});\n\t}\n\tremoveTestConnection(connection);\n\n\tif (_testConnections.empty()) {\n\t\thandleError(errorCode);\n\t} else {\n\t\tconfirmBestConnection();\n\t}\n}\n\nvoid SessionPrivate::handleError(int errorCode) {\n\tdestroyAllConnections();\n\t_waitForConnectedTimer.cancel();\n\n\tif (errorCode == -404) {\n\t\tdestroyTemporaryKey();\n\t} else {\n\t\tMTP_LOG(_shiftedDcId, (\"Restarting after error in connection, error code: %1...\").arg(errorCode));\n\t\treturn restart();\n\t}\n}\n\nvoid SessionPrivate::destroyTemporaryKey() {\n\tif (_instance->isKeysDestroyer()) {\n\t\tLOG((\"MTP Info: -404 error received in destroyer %1, assuming key was destroyed.\").arg(_shiftedDcId));\n\t\t_instance->keyWasPossiblyDestroyed(_shiftedDcId);\n\t\treturn;\n\t}\n\tLOG((\"MTP Info: -404 error received in %1 with temporary key, assuming it was destroyed.\").arg(_shiftedDcId));\n\treleaseKeyCreationOnFail();\n\tif (_encryptionKey) {\n\t\t_sessionData->destroyTemporaryKey(_encryptionKey->keyId());\n\t}\n\tapplyAuthKey(nullptr);\n\trestart();\n}\n\nbool SessionPrivate::sendSecureRequest(\n\t\tSerializedRequest &&request,\n\t\tbool needAnyResponse) {\n\trequest.addPadding(false);\n\n\tuint32 fullSize = request->size();\n\tif (fullSize < 9) {\n\t\treturn false;\n\t}\n\n\tauto messageSize = request.messageSize();\n\tif (messageSize < 5 || fullSize < messageSize + 4) {\n\t\treturn false;\n\t}\n\n\tmemcpy(request->data() + 0, &_sessionSalt, 2 * sizeof(mtpPrime));\n\tmemcpy(request->data() + 2, &_sessionId, 2 * sizeof(mtpPrime));\n\n\tauto from = request->constData() + 4;\n\tMTP_LOG(_shiftedDcId, (\"Send: \")\n\t\t+ DumpToText(from, from + messageSize)\n\t\t+ QString(\" (dc:%1,key:%2,session:%3)\"\n\t\t).arg(AbstractConnection::ProtocolDcDebugId(getProtocolDcId())\n\t\t).arg(_encryptionKey->keyId()\n\t\t).arg(_sessionId));\n\n\tuchar encryptedSHA256[32];\n\tMTPint128 &msgKey(*(MTPint128*)(encryptedSHA256 + 8));\n\n\tSHA256_CTX msgKeyLargeContext;\n\tSHA256_Init(&msgKeyLargeContext);\n\tSHA256_Update(&msgKeyLargeContext, _encryptionKey->partForMsgKey(true), 32);\n\tSHA256_Update(&msgKeyLargeContext, request->constData(), fullSize * sizeof(mtpPrime));\n\tSHA256_Final(encryptedSHA256, &msgKeyLargeContext);\n\n\tauto packet = _connection->prepareSecurePacket(_keyId, msgKey, fullSize);\n\tconst auto prefix = packet.size();\n\tpacket.resize(prefix + fullSize);\n\n\taesIgeEncrypt(\n\t\trequest->constData(),\n\t\t&packet[prefix],\n\t\tfullSize * sizeof(mtpPrime),\n\t\t_encryptionKey,\n\t\tmsgKey);\n\n\tDEBUG_LOG((\"MTP Info: sending request, size: %1, num: %2, time: %3\").arg(fullSize + 6).arg((*request)[4]).arg((*request)[5]));\n\n\t_connection->setSentEncryptedWithKeyId(_keyId);\n\t_connection->sendData(std::move(packet));\n\n\tif (needAnyResponse) {\n\t\tonSentSome((prefix + fullSize) * sizeof(mtpPrime));\n\t}\n\n\treturn true;\n}\n\nmtpRequestId SessionPrivate::wasSent(mtpMsgId msgId) const {\n\tif (msgId == _pingMsgId || msgId == _bindMsgId) {\n\t\treturn mtpRequestId(0xFFFFFFFF);\n\t}\n\tif (const auto i = _resendingIds.find(msgId); i != end(_resendingIds)) {\n\t\treturn i->second;\n\t}\n\tif (const auto i = _ackedIds.find(msgId); i != end(_ackedIds)) {\n\t\treturn i->second;\n\t}\n\tif (const auto i = _sentContainers.find(msgId); i != end(_sentContainers)) {\n\t\treturn mtpRequestId(0xFFFFFFFF);\n\t}\n\n\t{\n\t\tQReadLocker locker(_sessionData->haveSentMutex());\n\t\tconst auto &haveSent = _sessionData->haveSentMap();\n\t\tconst auto i = haveSent.find(msgId);\n\t\tif (i != haveSent.end()) {\n\t\t\treturn i->second->requestId\n\t\t\t\t? i->second->requestId\n\t\t\t\t: mtpRequestId(0xFFFFFFFF);\n\t\t}\n\t}\n\treturn 0;\n}\n\nvoid SessionPrivate::clearUnboundKeyCreator() {\n\tif (_keyCreator) {\n\t\t_keyCreator->stop();\n\t}\n}\n\nvoid SessionPrivate::releaseKeyCreationOnFail() {\n\tif (!_keyCreator) {\n\t\treturn;\n\t}\n\t_keyCreator = nullptr;\n\t_sessionData->releaseKeyCreationOnFail();\n}\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/session_private.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/details/mtproto_received_ids_manager.h\"\n#include \"mtproto/details/mtproto_serialized_request.h\"\n#include \"mtproto/mtproto_auth_key.h\"\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"mtproto/connection_abstract.h\"\n#include \"mtproto/facade.h\"\n#include \"base/timer.h\"\n\nnamespace MTP {\nnamespace details {\nclass BoundKeyCreator;\n} // namespace details\n\nclass Instance;\n\nnamespace details {\n\nclass AbstractConnection;\nclass SessionData;\nclass RSAPublicKey;\nstruct SessionOptions;\n\nclass SessionPrivate final : public QObject {\npublic:\n\tSessionPrivate(\n\t\tnot_null<Instance*> instance,\n\t\tnot_null<QThread*> thread,\n\t\tstd::shared_ptr<SessionData> data,\n\t\tShiftedDcId shiftedDcId);\n\t~SessionPrivate();\n\n\t[[nodiscard]] int32 getShiftedDcId() const;\n\tvoid dcOptionsChanged();\n\tvoid cdnConfigChanged();\n\n\t[[nodiscard]] int32 getState() const;\n\t[[nodiscard]] QString transport() const;\n\n\tvoid updateAuthKey();\n\tvoid restartNow();\n\tvoid sendPingForce();\n\tvoid tryToSend();\n\nprivate:\n\tstatic constexpr auto kUpdateStateAlways = 666;\n\n\tstruct TestConnection {\n\t\tConnectionPointer data;\n\t\tint priority = 0;\n\t};\n\tstruct SentContainer {\n\t\tcrl::time sent = 0;\n\t\tstd::vector<mtpMsgId> messages;\n\t};\n\tenum class HandleResult {\n\t\tSuccess,\n\t\tIgnored,\n\t\tRestartConnection,\n\t\tResetSession,\n\t\tDestroyTemporaryKey,\n\t\tParseError,\n\t};\n\n\tvoid connectToServer(bool afterConfig = false);\n\tvoid connectingTimedOut();\n\tvoid doDisconnect();\n\tvoid restart();\n\tvoid requestCDNConfig();\n\tvoid handleError(int errorCode);\n\tvoid onError(\n\t\tnot_null<AbstractConnection*> connection,\n\t\tqint32 errorCode);\n\tvoid onConnected(not_null<AbstractConnection*> connection);\n\tvoid onDisconnected(not_null<AbstractConnection*> connection);\n\tvoid onSentSome(uint64 size);\n\tvoid onReceivedSome();\n\n\tvoid handleReceived();\n\n\tvoid retryByTimer();\n\tvoid waitConnectedFailed();\n\tvoid waitReceivedFailed();\n\tvoid waitBetterFailed();\n\tvoid markConnectionOld();\n\tvoid sendPingByTimer();\n\tvoid destroyAllConnections();\n\n\tvoid confirmBestConnection();\n\tvoid removeTestConnection(not_null<AbstractConnection*> connection);\n\t[[nodiscard]] int16 getProtocolDcId() const;\n\n\tvoid checkSentRequests();\n\tvoid clearOldContainers();\n\n\tmtpMsgId placeToContainer(\n\t\tSerializedRequest &toSendRequest,\n\t\tmtpMsgId &bigMsgId,\n\t\tbool forceNewMsgId,\n\t\tSerializedRequest &req);\n\tmtpMsgId prepareToSend(\n\t\tSerializedRequest &request,\n\t\tmtpMsgId currentLastId,\n\t\tbool forceNewMsgId);\n\tmtpMsgId replaceMsgId(\n\t\tSerializedRequest &request,\n\t\tmtpMsgId newId);\n\n\tbool sendSecureRequest(\n\t\tSerializedRequest &&request,\n\t\tbool needAnyResponse);\n\tmtpRequestId wasSent(mtpMsgId msgId) const;\n\n\tstruct OuterInfo {\n\t\tmtpMsgId outerMsgId = 0;\n\t\tuint64 serverSalt = 0;\n\t\tint32 serverTime = 0;\n\t\tbool badTime = false;\n\t};\n\t[[nodiscard]] HandleResult handleOneReceived(\n\t\tconst mtpPrime *from,\n\t\tconst mtpPrime *end,\n\t\tuint64 msgId,\n\t\tOuterInfo info);\n\t[[nodiscard]] HandleResult handleBindResponse(\n\t\tmtpMsgId requestMsgId,\n\t\tconst mtpBuffer &response);\n\tmtpBuffer ungzip(const mtpPrime *from, const mtpPrime *end) const;\n\tvoid handleMsgsStates(const QVector<MTPlong> &ids, const QByteArray &states);\n\n\t// _sessionDataMutex must be locked for read.\n\tbool setState(int state, int ifState = kUpdateStateAlways);\n\n\tvoid appendTestConnection(\n\t\tDcOptions::Variants::Protocol protocol,\n\t\tconst QString &ip,\n\t\tint port,\n\t\tconst bytes::vector &protocolSecret);\n\n\t// if badTime received - search for ids in sessionData->haveSent and sessionData->wereAcked and sync time/salt, return true if found\n\tbool requestsFixTimeSalt(const QVector<MTPlong> &ids, const OuterInfo &info);\n\n\t// if we had a confirmed fast request use its unixtime as a correct one.\n\tvoid correctUnixtimeByFastRequest(\n\t\tconst QVector<MTPlong> &ids,\n\t\tTimeId serverTime);\n\tvoid correctUnixtimeWithBadLocal(TimeId serverTime);\n\n\t// remove msgs with such ids from sessionData->haveSent, add to sessionData->wereAcked\n\tvoid requestsAcked(const QVector<MTPlong> &ids, bool byResponse = false);\n\n\tvoid resend(mtpMsgId msgId, crl::time msCanWait = 0);\n\tvoid resendAll();\n\tvoid clearSpecialMsgId(mtpMsgId msgId);\n\n\t[[nodiscard]] DcType tryAcquireKeyCreation();\n\tvoid resetSession();\n\tvoid checkAuthKey();\n\tvoid authKeyChecked();\n\tvoid destroyTemporaryKey();\n\tvoid clearUnboundKeyCreator();\n\tvoid releaseKeyCreationOnFail();\n\tvoid applyAuthKey(AuthKeyPtr &&encryptionKey);\n\t[[nodiscard]] bool noMediaKeyWithExistingRegularKey() const;\n\tbool destroyOldEnoughPersistentKey();\n\n\tvoid setCurrentKeyId(uint64 newKeyId);\n\tvoid changeSessionId();\n\t[[nodiscard]] bool markSessionAsStarted();\n\t[[nodiscard]] uint32 nextRequestSeqNumber(bool needAck);\n\n\t[[nodiscard]] bool realDcTypeChanged();\n\t[[nodiscard]] MTPVector<MTPJSONObjectValue> prepareInitParams();\n\n\tconst not_null<Instance*> _instance;\n\tconst ShiftedDcId _shiftedDcId = 0;\n\tDcType _realDcType = DcType();\n\tDcType _currentDcType = DcType();\n\n\tmutable QReadWriteLock _stateMutex;\n\tint _state = DisconnectedState;\n\n\tbool _needSessionReset = false;\n\n\tConnectionPointer _connection;\n\tstd::vector<TestConnection> _testConnections;\n\tcrl::time _startedConnectingAt = 0;\n\n\tbase::Timer _retryTimer; // exp retry timer\n\tint _retryTimeout = 1;\n\tqint64 _retryWillFinish = 0;\n\n\tbase::Timer _oldConnectionTimer;\n\tbool _oldConnection = true;\n\n\tbase::Timer _waitForConnectedTimer;\n\tbase::Timer _waitForReceivedTimer;\n\tbase::Timer _waitForBetterTimer;\n\tcrl::time _waitForReceived = 0;\n\tcrl::time _waitForConnected = 0;\n\tcrl::time _firstSentAt = -1;\n\n\tmtpPingId _pingId = 0;\n\tmtpPingId _pingIdToSend = 0;\n\tcrl::time _pingSendAt = 0;\n\tmtpMsgId _pingMsgId = 0;\n\tbase::Timer _pingSender;\n\tbase::Timer _checkSentRequestsTimer;\n\tbase::Timer _clearOldContainersTimer;\n\n\tstd::shared_ptr<SessionData> _sessionData;\n\tstd::unique_ptr<SessionOptions> _options;\n\tAuthKeyPtr _encryptionKey;\n\tuint64 _keyId = 0;\n\tuint64 _sessionId = 0;\n\tuint64 _sessionSalt = 0;\n\tuint32 _messagesCounter = 0;\n\tbool _sessionMarkedAsStarted = false;\n\n\tQVector<MTPlong> _ackRequestData;\n\tQVector<MTPlong> _resendRequestData;\n\tbase::flat_set<mtpMsgId> _stateRequestData;\n\tReceivedIdsManager _receivedMessageIds;\n\tbase::flat_map<mtpMsgId, mtpRequestId> _resendingIds;\n\tbase::flat_map<mtpMsgId, mtpRequestId> _ackedIds;\n\tbase::flat_map<mtpMsgId, SerializedRequest> _stateAndResendRequests;\n\tbase::flat_map<mtpMsgId, SentContainer> _sentContainers;\n\n\tstd::unique_ptr<BoundKeyCreator> _keyCreator;\n\tmtpMsgId _bindMsgId = 0;\n\tcrl::time _bindMessageSent = 0;\n\n};\n\nextern const char kOptionPreferIPv6[];\n\n} // namespace details\n} // namespace MTP\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/special_config_request.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"mtproto/special_config_request.h\"\n\n#include \"mtproto/details/mtproto_rsa_public_key.h\"\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"mtproto/mtproto_auth_key.h\"\n#include \"base/unixtime.h\"\n#include \"base/openssl_help.h\"\n#include \"base/call_delayed.h\"\n\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonArray>\n#include <QtCore/QJsonObject>\n\nnamespace MTP::details {\nnamespace {\n\nconstexpr auto kSendNextTimeout = crl::time(800);\n\nconstexpr auto kPublicKey = \"\\\n-----BEGIN RSA PUBLIC KEY-----\\n\\\nMIIBCgKCAQEAyr+18Rex2ohtVy8sroGPBwXD3DOoKCSpjDqYoXgCqB7ioln4eDCF\\n\\\nfOBUlfXUEvM/fnKCpF46VkAftlb4VuPDeQSS/ZxZYEGqHaywlroVnXHIjgqoxiAd\\n\\\n192xRGreuXIaUKmkwlM9JID9WS2jUsTpzQ91L8MEPLJ/4zrBwZua8W5fECwCCh2c\\n\\\n9G5IzzBm+otMS/YKwmR1olzRCyEkyAEjXWqBI9Ftv5eG8m0VkBzOG655WIYdyV0H\\n\\\nfDK/NWcvGqa0w/nriMD6mDjKOryamw0OP9QuYgMN0C9xMW9y8SmP4h92OAWodTYg\\n\\\nY1hZCxdv6cs5UnW9+PWvS+WIbkh+GaWYxwIDAQAB\\n\\\n-----END RSA PUBLIC KEY-----\\\n\"_cs;\n\nconst auto kRemoteProject = \"peak-vista-421\";\nconst auto kFireProject = \"reserve-5a846\";\nconst auto kConfigKey = \"ipconfig\";\nconst auto kConfigSubKey = \"v3\";\nconst auto kApiKey = \"AIzaSyC2-kAkpDsroixRXw-sTw-Wfqo4NxjMwwM\";\nconst auto kAppId = \"1:560508485281:web:4ee13a6af4e84d49e67ae0\";\n\nQString ApiDomain(const QString &service) {\n\treturn service + \".googleapis.com\";\n}\n\nQString GenerateInstanceId() {\n\tauto fid = bytes::array<17>();\n\tbytes::set_random(fid);\n\tfid[0] = (bytes::type(0xF0) & fid[0]) | bytes::type(0x07);\n\treturn QString::fromLatin1(\n\t\tQByteArray::fromRawData(\n\t\t\treinterpret_cast<const char*>(fid.data()),\n\t\t\tfid.size()\n\t\t).toBase64(QByteArray::Base64UrlEncoding).mid(0, 22));\n}\n\nQString InstanceId() {\n\tstatic const auto result = GenerateInstanceId();\n\treturn result;\n}\n\nbool CheckPhoneByPrefixesRules(const QString &phone, const QString &rules) {\n\tstatic const auto RegExp = QRegularExpression(\"[^0-9]\");\n\tconst auto check = QString(phone).replace(\n\t\tRegExp,\n\t\tQString());\n\tauto result = false;\n\tfor (const auto &prefix : rules.split(',')) {\n\t\tif (prefix.isEmpty()) {\n\t\t\tresult = true;\n\t\t} else if (prefix[0] == '+' && check.startsWith(prefix.mid(1))) {\n\t\t\tresult = true;\n\t\t} else if (prefix[0] == '-' && check.startsWith(prefix.mid(1))) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn result;\n}\n\nQByteArray ConcatenateDnsTxtFields(const std::vector<DnsEntry> &response) {\n\tauto entries = QMultiMap<int, QString>();\n\tfor (const auto &entry : response) {\n\t\tentries.insert(INT_MAX - entry.data.size(), entry.data);\n\t}\n\treturn QStringList(entries.values()).join(QString()).toLatin1();\n}\n\nQByteArray ParseRemoteConfigResponse(const QByteArray &bytes) {\n\tauto error = QJsonParseError{ 0, QJsonParseError::NoError };\n\tconst auto document = QJsonDocument::fromJson(bytes, &error);\n\tif (error.error != QJsonParseError::NoError) {\n\t\tLOG((\"Config Error: Failed to parse fire response JSON, error: %1\"\n\t\t\t).arg(error.errorString()));\n\t\treturn {};\n\t} else if (!document.isObject()) {\n\t\tLOG((\"Config Error: Not an object received in fire response JSON.\"));\n\t\treturn {};\n\t}\n\treturn document.object().value(\n\t\t\"entries\"\n\t).toObject().value(\n\t\tu\"%1%2\"_q.arg(kConfigKey, kConfigSubKey)\n\t).toString().toLatin1();\n}\n\nQByteArray ParseFireStoreResponse(const QByteArray &bytes) {\n\tauto error = QJsonParseError{ 0, QJsonParseError::NoError };\n\tconst auto document = QJsonDocument::fromJson(bytes, &error);\n\tif (error.error != QJsonParseError::NoError) {\n\t\tLOG((\"Config Error: Failed to parse fire response JSON, error: %1\"\n\t\t\t).arg(error.errorString()));\n\t\treturn {};\n\t} else if (!document.isObject()) {\n\t\tLOG((\"Config Error: Not an object received in fire response JSON.\"));\n\t\treturn {};\n\t}\n\treturn document.object().value(\n\t\t\"fields\"\n\t).toObject().value(\n\t\t\"data\"\n\t).toObject().value(\n\t\t\"stringValue\"\n\t).toString().toLatin1();\n}\n\nQByteArray ParseRealtimeResponse(const QByteArray &bytes) {\n\tif (bytes.size() < 2\n\t\t|| bytes[0] != '\"'\n\t\t|| bytes[bytes.size() - 1] != '\"') {\n\t\treturn QByteArray();\n\t}\n\treturn bytes.mid(1, bytes.size() - 2);\n}\n\n[[nodiscard]] QDateTime ParseHttpDate(const QString &date) {\n\t// Wed, 10 Jul 2019 14:33:38 GMT\n\tstatic const auto expression = QRegularExpression(\n\t\tR\"(\\w\\w\\w, (\\d\\d) (\\w\\w\\w) (\\d\\d\\d\\d) (\\d\\d):(\\d\\d):(\\d\\d) GMT)\");\n\tconst auto match = expression.match(date);\n\tif (!match.hasMatch()) {\n\t\treturn QDateTime();\n\t}\n\n\tconst auto number = [&](int index) {\n\t\treturn match.capturedView(index).toInt();\n\t};\n\tconst auto day = number(1);\n\tconst auto month = [&] {\n\t\tstatic const auto months = {\n\t\t\t\"Jan\",\n\t\t\t\"Feb\",\n\t\t\t\"Mar\",\n\t\t\t\"Apr\",\n\t\t\t\"May\",\n\t\t\t\"Jun\",\n\t\t\t\"Jul\",\n\t\t\t\"Aug\",\n\t\t\t\"Sep\",\n\t\t\t\"Oct\",\n\t\t\t\"Nov\",\n\t\t\t\"Dec\"\n\t\t};\n\t\tconst auto captured = match.capturedView(2);\n\t\tfor (auto i = begin(months); i != end(months); ++i) {\n\t\t\tif (captured == QString(*i)) {\n\t\t\t\treturn 1 + int(i - begin(months));\n\t\t\t}\n\t\t}\n\t\treturn 0;\n\t}();\n\tconst auto year = number(3);\n\tconst auto hour = number(4);\n\tconst auto minute = number(5);\n\tconst auto second = number(6);\n\treturn QDateTime(\n\t\tQDate(year, month, day),\n\t\tQTime(hour, minute, second),\n\t\tQt::UTC);\n}\n\n} // namespace\n\nSpecialConfigRequest::SpecialConfigRequest(\n\tFn<void(\n\t\tDcId dcId,\n\t\tconst std::string &ip,\n\t\tint port,\n\t\tbytes::const_span secret)> callback,\n\tFn<void()> timeDoneCallback,\n\tbool isTestMode,\n\tconst QString &domainString,\n\tconst QString &phone)\n: _callback(std::move(callback))\n, _timeDoneCallback(std::move(timeDoneCallback))\n, _domainString(domainString)\n, _phone(phone) {\n\tExpects((_callback == nullptr) != (_timeDoneCallback == nullptr));\n\n\t_manager.setProxy(QNetworkProxy::NoProxy);\n\n\tstd::random_device rd;\n\tconst auto shuffle = [&](int from, int till) {\n\t\tExpects(till > from);\n\n\t\tranges::shuffle(\n\t\t\tbegin(_attempts) + from,\n\t\t\tbegin(_attempts) + till,\n\t\t\tstd::mt19937(rd()));\n\t};\n\n\t_attempts = {};\n\t_attempts.push_back({ Type::Google, \"dns.google.com\" });\n\t_attempts.push_back({ Type::Mozilla, \"mozilla.cloudflare-dns.com\" });\n\t_attempts.push_back({ Type::RemoteConfig, \"firebaseremoteconfig\" });\n\tif (!_timeDoneCallback) {\n\t\t_attempts.push_back({ Type::FireStore, \"firestore\" });\n\t\tfor (const auto &domain : DnsDomains()) {\n\t\t\t_attempts.push_back({ Type::FireStore, domain, \"firestore\" });\n\t\t}\n\t}\n\n\tshuffle(0, 2);\n\tif (!_timeDoneCallback) {\n\t\tshuffle(_attempts.size() - (int(DnsDomains().size()) + 1), _attempts.size());\n\t}\n\tif (isTestMode) {\n\t\t_attempts.erase(ranges::remove_if(_attempts, [](\n\t\t\t\tconst Attempt &attempt) {\n\t\t\treturn (attempt.type != Type::Google)\n\t\t\t\t&& (attempt.type != Type::Mozilla);\n\t\t}), _attempts.end());\n\t}\n\tranges::reverse(_attempts); // We go from last to first.\n\n\tsendNextRequest();\n}\n\nSpecialConfigRequest::SpecialConfigRequest(\n\tFn<void(\n\t\tDcId dcId,\n\t\tconst std::string &ip,\n\t\tint port,\n\t\tbytes::const_span secret)> callback,\n\tbool isTestMode,\n\tconst QString &domainString,\n\tconst QString &phone)\n: SpecialConfigRequest(\n\tstd::move(callback),\n\tnullptr,\n\tisTestMode,\n\tdomainString,\n\tphone) {\n}\n\nSpecialConfigRequest::SpecialConfigRequest(\n\tFn<void()> timeDoneCallback,\n\tbool isTestMode,\n\tconst QString &domainString)\n: SpecialConfigRequest(\n\tnullptr,\n\tstd::move(timeDoneCallback),\n\tisTestMode,\n\tdomainString,\n\tQString()) {\n}\n\nvoid SpecialConfigRequest::sendNextRequest() {\n\tExpects(!_attempts.empty());\n\n\tconst auto attempt = _attempts.back();\n\t_attempts.pop_back();\n\tif (!_attempts.empty()) {\n\t\tbase::call_delayed(kSendNextTimeout, this, [=] {\n\t\t\tsendNextRequest();\n\t\t});\n\t}\n\tperformRequest(attempt);\n}\n\nvoid SpecialConfigRequest::performRequest(const Attempt &attempt) {\n\tconst auto type = attempt.type;\n\tauto url = QUrl();\n\turl.setScheme(u\"https\"_q);\n\tauto request = QNetworkRequest();\n\tauto payload = QByteArray();\n\tswitch (type) {\n\tcase Type::Mozilla: {\n\t\turl.setHost(attempt.data);\n\t\turl.setPath(u\"/dns-query\"_q);\n\t\turl.setQuery(u\"name=%1&type=16&random_padding=%2\"_q.arg(\n\t\t\t_domainString,\n\t\t\tGenerateDnsRandomPadding()));\n\t\trequest.setRawHeader(\"accept\", \"application/dns-json\");\n\t} break;\n\tcase Type::Google: {\n\t\turl.setHost(attempt.data);\n\t\turl.setPath(u\"/resolve\"_q);\n\t\turl.setQuery(u\"name=%1&type=ANY&random_padding=%2\"_q.arg(\n\t\t\t_domainString,\n\t\t\tGenerateDnsRandomPadding()));\n\t\tif (!attempt.host.isEmpty()) {\n\t\t\tconst auto host = attempt.host + \".google.com\";\n\t\t\trequest.setRawHeader(\"Host\", host.toLatin1());\n\t\t}\n\t} break;\n\tcase Type::RemoteConfig: {\n\t\turl.setHost(ApiDomain(attempt.data));\n\t\turl.setPath((u\"/v1/projects/%1/namespaces/firebase:fetch\"_q\n\t\t).arg(kRemoteProject));\n\t\turl.setQuery(u\"key=%1\"_q.arg(kApiKey));\n\t\tpayload = u\"{\\\"app_id\\\":\\\"%1\\\",\\\"app_instance_id\\\":\\\"%2\\\"}\"_q.arg(\n\t\t\tkAppId,\n\t\t\tInstanceId()).toLatin1();\n\t\trequest.setRawHeader(\"Content-Type\", \"application/json\");\n\t} break;\n\tcase Type::Realtime: {\n\t\turl.setHost(kFireProject + u\".%1\"_q.arg(attempt.data));\n\t\turl.setPath(u\"/%1%2.json\"_q.arg(kConfigKey, kConfigSubKey));\n\t} break;\n\tcase Type::FireStore: {\n\t\turl.setHost(attempt.host.isEmpty()\n\t\t\t? ApiDomain(attempt.data)\n\t\t\t: attempt.data);\n\t\turl.setPath((u\"/v1/projects/%1/databases/(default)/documents/%2/%3\"_q\n\t\t).arg(\n\t\t\tkFireProject,\n\t\t\tkConfigKey,\n\t\t\tkConfigSubKey));\n\t\tif (!attempt.host.isEmpty()) {\n\t\t\tconst auto host = ApiDomain(attempt.host);\n\t\t\trequest.setRawHeader(\"Host\", host.toLatin1());\n\t\t}\n\t} break;\n\tdefault: Unexpected(\"Type in SpecialConfigRequest::performRequest.\");\n\t}\n\trequest.setUrl(url);\n\trequest.setRawHeader(\"User-Agent\", DnsUserAgent());\n\tconst auto reply = _requests.emplace_back(payload.isEmpty()\n\t\t? _manager.get(request)\n\t\t: _manager.post(request, payload)\n\t).reply;\n\tconnect(reply, &QNetworkReply::finished, this, [=] {\n\t\trequestFinished(type, reply);\n\t});\n}\n\nvoid SpecialConfigRequest::handleHeaderUnixtime(\n\t\tnot_null<QNetworkReply*> reply) {\n\tif (reply->error() != QNetworkReply::NoError) {\n\t\treturn;\n\t}\n\tconst auto date = QString::fromLatin1([&] {\n\t\tfor (const auto &pair : reply->rawHeaderPairs()) {\n\t\t\tif (pair.first == \"Date\") {\n\t\t\t\treturn pair.second;\n\t\t\t}\n\t\t}\n\t\treturn QByteArray();\n\t}());\n\tif (date.isEmpty()) {\n\t\tLOG((\"Config Error: No 'Date' header received.\"));\n\t\treturn;\n\t}\n\tconst auto parsed = ParseHttpDate(date);\n\tif (!parsed.isValid()) {\n\t\tLOG((\"Config Error: Bad 'Date' header received: %1\").arg(date));\n\t\treturn;\n\t}\n\tbase::unixtime::http_update(parsed.toSecsSinceEpoch());\n\tif (_timeDoneCallback) {\n\t\t_timeDoneCallback();\n\t}\n}\n\nvoid SpecialConfigRequest::requestFinished(\n\t\tType type,\n\t\tnot_null<QNetworkReply*> reply) {\n\thandleHeaderUnixtime(reply);\n\tconst auto result = finalizeRequest(reply);\n\tif (!_callback || result.isEmpty()) {\n\t\treturn;\n\t}\n\n\tswitch (type) {\n\tcase Type::Mozilla:\n\tcase Type::Google: {\n\t\tconstexpr auto kTypeRestriction = 16; // TXT\n\t\thandleResponse(ConcatenateDnsTxtFields(\n\t\t\tParseDnsResponse(result, kTypeRestriction)));\n\t} break;\n\tcase Type::RemoteConfig: {\n\t\thandleResponse(ParseRemoteConfigResponse(result));\n\t} break;\n\tcase Type::Realtime: {\n\t\thandleResponse(ParseRealtimeResponse(result));\n\t} break;\n\tcase Type::FireStore: {\n\t\thandleResponse(ParseFireStoreResponse(result));\n\t} break;\n\tdefault: Unexpected(\"Type in SpecialConfigRequest::requestFinished.\");\n\t}\n}\n\nQByteArray SpecialConfigRequest::finalizeRequest(\n\t\tnot_null<QNetworkReply*> reply) {\n\tif (reply->error() != QNetworkReply::NoError) {\n\t\tDEBUG_LOG((\"Config Error: Failed to get response, error: %2 (%3)\"\n\t\t\t).arg(reply->errorString()\n\t\t\t).arg(reply->error()));\n\t}\n\tconst auto result = reply->readAll();\n\tconst auto from = ranges::remove(\n\t\t_requests,\n\t\treply,\n\t\t[](const ServiceWebRequest &request) { return request.reply; });\n\t_requests.erase(from, end(_requests));\n\treturn result;\n}\n\nbool SpecialConfigRequest::decryptSimpleConfig(const QByteArray &bytes) {\n\tauto cleanBytes = bytes;\n\tauto removeFrom = std::remove_if(cleanBytes.begin(), cleanBytes.end(), [](char ch) {\n\t\tauto isGoodBase64 = (ch == '+') || (ch == '=') || (ch == '/')\n\t\t\t|| (ch >= 'a' && ch <= 'z')\n\t\t\t|| (ch >= 'A' && ch <= 'Z')\n\t\t\t|| (ch >= '0' && ch <= '9');\n\t\treturn !isGoodBase64;\n\t});\n\tif (removeFrom != cleanBytes.end()) {\n\t\tcleanBytes.remove(removeFrom - cleanBytes.begin(), cleanBytes.end() - removeFrom);\n\t}\n\n\tconstexpr auto kGoodSizeBase64 = 344;\n\tif (cleanBytes.size() != kGoodSizeBase64) {\n\t\tLOG((\"Config Error: Bad data size %1 required %2\").arg(cleanBytes.size()).arg(kGoodSizeBase64));\n\t\treturn false;\n\t}\n\tconstexpr auto kGoodSizeData = 256;\n\tauto decodedBytes = QByteArray::fromBase64(cleanBytes, QByteArray::Base64Encoding);\n\tif (decodedBytes.size() != kGoodSizeData) {\n\t\tLOG((\"Config Error: Bad data size %1 required %2\").arg(decodedBytes.size()).arg(kGoodSizeData));\n\t\treturn false;\n\t}\n\n\tauto publicKey = details::RSAPublicKey(bytes::make_span(kPublicKey));\n\tauto decrypted = publicKey.decrypt(bytes::make_span(decodedBytes));\n\tauto decryptedBytes = gsl::make_span(decrypted);\n\n\tauto aesEncryptedBytes = decryptedBytes.subspan(CTRState::KeySize);\n\tauto aesivec = bytes::make_vector(decryptedBytes.subspan(CTRState::KeySize - CTRState::IvecSize, CTRState::IvecSize));\n\tAES_KEY aeskey;\n\tAES_set_decrypt_key(reinterpret_cast<const unsigned char*>(decryptedBytes.data()), CTRState::KeySize * CHAR_BIT, &aeskey);\n\tAES_cbc_encrypt(reinterpret_cast<const unsigned char*>(aesEncryptedBytes.data()), reinterpret_cast<unsigned char*>(aesEncryptedBytes.data()), aesEncryptedBytes.size(), &aeskey, reinterpret_cast<unsigned char*>(aesivec.data()), AES_DECRYPT);\n\n\tconstexpr auto kDigestSize = 16;\n\tauto dataSize = aesEncryptedBytes.size() - kDigestSize;\n\tauto data = aesEncryptedBytes.subspan(0, dataSize);\n\tauto hash = openssl::Sha256(data);\n\tif (bytes::compare(gsl::make_span(hash).subspan(0, kDigestSize), aesEncryptedBytes.subspan(dataSize)) != 0) {\n\t\tLOG((\"Config Error: Bad digest.\"));\n\t\treturn false;\n\t}\n\n\tmtpBuffer buffer;\n\tbuffer.resize(data.size() / sizeof(mtpPrime));\n\tbytes::copy(bytes::make_span(buffer), data);\n\tauto from = &*buffer.cbegin();\n\tauto end = from + buffer.size();\n\tauto realLength = *from++;\n\tif (realLength <= 0 || realLength > dataSize || (realLength & 0x03)) {\n\t\tLOG((\"Config Error: Bad length %1.\").arg(realLength));\n\t\treturn false;\n\t}\n\n\tif (!_simpleConfig.read(from, end)) {\n\t\tLOG((\"Config Error: Could not read configSimple.\"));\n\t\treturn false;\n\t}\n\tif ((end - from) * sizeof(mtpPrime) != (dataSize - realLength)) {\n\t\tLOG((\"Config Error: Bad read length %1, should be %2.\").arg((end - from) * sizeof(mtpPrime)).arg(dataSize - realLength));\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nvoid SpecialConfigRequest::handleResponse(const QByteArray &bytes) {\n\tif (!decryptSimpleConfig(bytes)) {\n\t\treturn;\n\t}\n\tAssert(_simpleConfig.type() == mtpc_help_configSimple);\n\tconst auto &config = _simpleConfig.c_help_configSimple();\n\tconst auto now = base::unixtime::http_now();\n\tif (now > config.vexpires().v) {\n\t\tLOG((\"Config Error: \"\n\t\t\t\"Bad date frame for simple config: %1-%2, our time is %3.\"\n\t\t\t).arg(config.vdate().v\n\t\t\t).arg(config.vexpires().v\n\t\t\t).arg(now));\n\t\treturn;\n\t}\n\tif (config.vrules().v.empty()) {\n\t\tLOG((\"Config Error: Empty simple config received.\"));\n\t\treturn;\n\t}\n\tfor (const auto &rule : config.vrules().v) {\n\t\tAssert(rule.type() == mtpc_accessPointRule);\n\t\tconst auto &data = rule.c_accessPointRule();\n\t\tconst auto phoneRules = qs(data.vphone_prefix_rules());\n\t\tif (!CheckPhoneByPrefixesRules(_phone, phoneRules)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto dcId = data.vdc_id().v;\n\t\tfor (const auto &address : data.vips().v) {\n\t\t\tconst auto parseIp = [](const MTPint &ipv4) {\n\t\t\t\tconst auto ip = *reinterpret_cast<const uint32*>(&ipv4.v);\n\t\t\t\treturn (u\"%1.%2.%3.%4\"_q\n\t\t\t\t).arg((ip >> 24) & 0xFF\n\t\t\t\t).arg((ip >> 16) & 0xFF\n\t\t\t\t).arg((ip >> 8) & 0xFF\n\t\t\t\t).arg(ip & 0xFF).toStdString();\n\t\t\t};\n\t\t\tswitch (address.type()) {\n\t\t\tcase mtpc_ipPort: {\n\t\t\t\tconst auto &fields = address.c_ipPort();\n\t\t\t\tconst auto ip = parseIp(fields.vipv4());\n\t\t\t\tif (!ip.empty()) {\n\t\t\t\t\t_callback(dcId, ip, fields.vport().v, {});\n\t\t\t\t}\n\t\t\t} break;\n\t\t\tcase mtpc_ipPortSecret: {\n\t\t\t\tconst auto &fields = address.c_ipPortSecret();\n\t\t\t\tconst auto ip = parseIp(fields.vipv4());\n\t\t\t\tif (!ip.empty()) {\n\t\t\t\t\t_callback(\n\t\t\t\t\t\tdcId,\n\t\t\t\t\t\tip,\n\t\t\t\t\t\tfields.vport().v,\n\t\t\t\t\t\tbytes::make_span(fields.vsecret().v));\n\t\t\t\t}\n\t\t\t} break;\n\t\t\tdefault: Unexpected(\"Type in simpleConfig ips.\");\n\t\t\t}\n\t\t}\n\t}\n\t_callback(0, std::string(), 0, {});\n}\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/special_config_request.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/details/mtproto_domain_resolver.h\"\n#include \"base/bytes.h\"\n#include \"base/weak_ptr.h\"\n\n#include <QtCore/QPointer>\n#include <QtNetwork/QNetworkReply>\n#include <QtNetwork/QNetworkAccessManager>\n\nnamespace MTP::details {\n\nclass SpecialConfigRequest : public QObject {\npublic:\n\tSpecialConfigRequest(\n\t\tFn<void(\n\t\t\tDcId dcId,\n\t\t\tconst std::string &ip,\n\t\t\tint port,\n\t\t\tbytes::const_span secret)> callback,\n\t\tbool isTestMode,\n\t\tconst QString &domainString,\n\t\tconst QString &phone);\n\tSpecialConfigRequest(\n\t\tFn<void()> timeDoneCallback,\n\t\tbool isTestMode,\n\t\tconst QString &domainString);\n\nprivate:\n\tenum class Type {\n\t\tMozilla,\n\t\tGoogle,\n\t\tRemoteConfig,\n\t\tRealtime,\n\t\tFireStore,\n\t};\n\tstruct Attempt {\n\t\tType type;\n\t\tQString data;\n\t\tQString host;\n\t};\n\n\tSpecialConfigRequest(\n\t\tFn<void(\n\t\t\tDcId dcId,\n\t\t\tconst std::string &ip,\n\t\t\tint port,\n\t\t\tbytes::const_span secret)> callback,\n\t\tFn<void()> timeDoneCallback,\n\t\tbool isTestMode,\n\t\tconst QString &domainString,\n\t\tconst QString &phone);\n\n\tvoid sendNextRequest();\n\tvoid performRequest(const Attempt &attempt);\n\tvoid requestFinished(Type type, not_null<QNetworkReply*> reply);\n\tvoid handleHeaderUnixtime(not_null<QNetworkReply*> reply);\n\tQByteArray finalizeRequest(not_null<QNetworkReply*> reply);\n\tvoid handleResponse(const QByteArray &bytes);\n\tbool decryptSimpleConfig(const QByteArray &bytes);\n\n\tFn<void(\n\t\tDcId dcId,\n\t\tconst std::string &ip,\n\t\tint port,\n\t\tbytes::const_span secret)> _callback;\n\tFn<void()> _timeDoneCallback;\n\tQString _domainString;\n\tQString _phone;\n\tMTPhelp_ConfigSimple _simpleConfig;\n\n\tQNetworkAccessManager _manager;\n\tstd::vector<Attempt> _attempts;\n\tstd::vector<ServiceWebRequest> _requests;\n\n};\n\n} // namespace MTP::details\n"
  },
  {
    "path": "Telegram/SourceFiles/mtproto/type_utils.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\ninline MTPbool MTP_bool(bool v) {\n\treturn v ? MTP_boolTrue() : MTP_boolFalse();\n}\n\ninline bool mtpIsTrue(const MTPBool &v) {\n\treturn v.type() == mtpc_boolTrue;\n}\ninline bool mtpIsFalse(const MTPBool &v) {\n\treturn !mtpIsTrue(v);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/old_settings/settings_chat_settings_widget.cpp",
    "content": ""
  },
  {
    "path": "Telegram/SourceFiles/old_settings/settings_chat_settings_widget.h",
    "content": ""
  },
  {
    "path": "Telegram/SourceFiles/overview/overview.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\nusing \"ui/chat/chat.style\";\nusing \"ui/widgets/widgets.style\";\nusing \"boxes/boxes.style\";\n\nOverviewFileLayout {\n\tmaxWidth: pixels;\n\tsongPadding: margins;\n\tsongThumbSize: pixels;\n\tsongNameTop: pixels;\n\tsongStatusTop: pixels;\n\tsongIconBg: color;\n\tsongOverBg: color;\n\tsongPause: icon;\n\tsongPauseSelected: icon;\n\tsongPlay: icon;\n\tsongPlaySelected: icon;\n\tsongCancel: icon;\n\tsongCancelSelected: icon;\n\tsongDownload: icon;\n\tsongDownloadSelected: icon;\n\n\tvoicePause: icon;\n\tvoicePauseSelected: icon;\n\tvoicePlay: icon;\n\tvoicePlaySelected: icon;\n\tvoiceCancel: icon;\n\tvoiceCancelSelected: icon;\n\tvoiceDownload: icon;\n\tvoiceDownloadSelected: icon;\n\n\tfilePadding: margins;\n\tfileThumbSize: pixels;\n\tfileNameTop: pixels;\n\tfileStatusTop: pixels;\n\tfileDateTop: pixels;\n}\n\noverviewCheckPressedSize: 0.8;\noverviewCheck: RoundCheckbox(defaultRoundCheckbox) {\n\tbgInactive: overviewCheckBg;\n\tbgActive: overviewCheckBgActive;\n\tborder: overviewCheckBorder;\n\tsize: 29px;\n\tsizeSmall: 0.3;\n\tcheck: icon {{ \"overview_photo_check\", overviewCheckFgActive, point(4px, 8px) }};\n}\noverviewSmallCheck: RoundCheckbox(defaultPeerListCheck) {\n\tborder: overviewCheckBorder;\n}\noverviewCheckSkip: 5px;\n\noverviewPhotoBg: windowBgOver;\noverviewPhotoMinSize: 90px;\noverviewVideoBg: imageBg;\n\noverviewFileThumbBg: imageBg;\noverviewFileExtPadding: 5px;\noverviewFileExtTop: 24px;\noverviewFileExtFg: windowFgActive;\noverviewFileExtFont: font(18px semibold);\n\noverviewVoicePause: icon {{ \"player/playlist_pause\", historyFileInIconFg }};\noverviewVoicePauseSelected: icon {{ \"player/playlist_pause\", historyFileInIconFgSelected }};\noverviewVoicePlay: icon {{ \"player/playlist_play\", historyFileInIconFg }};\noverviewVoicePlaySelected: icon {{ \"player/playlist_play\", historyFileInIconFgSelected }};\noverviewVoiceCancel: icon {{ \"player/playlist_cancel\", historyFileInIconFg }};\noverviewVoiceCancelSelected: icon {{ \"player/playlist_cancel\", historyFileInIconFgSelected }};\noverviewVoiceDownload: icon {{ \"player/playlist_download\", historyFileInIconFg }};\noverviewVoiceDownloadSelected: icon {{ \"player/playlist_download\", historyFileInIconFgSelected }};\n\noverviewSongPause: icon {{ \"player/playlist_pause\", historyFileThumbIconFg }};\noverviewSongPauseSelected: icon {{ \"player/playlist_pause\", historyFileThumbIconFgSelected }};\noverviewSongPlay: icon {{ \"player/playlist_play\", historyFileThumbIconFg }};\noverviewSongPlaySelected: icon {{ \"player/playlist_play\", historyFileThumbIconFgSelected }};\noverviewSongCancel: icon {{ \"player/playlist_cancel\", historyFileThumbIconFg }};\noverviewSongCancelSelected: icon {{ \"player/playlist_cancel\", historyFileThumbIconFgSelected }};\noverviewSongDownload: icon {{ \"player/playlist_download\", historyFileThumbIconFg }};\noverviewSongDownloadSelected: icon {{ \"player/playlist_download\", historyFileThumbIconFgSelected }};\noverviewSmallCancel: icon {{ \"history_audio_cancel\", historyFileInIconFg }};\noverviewSmallCancelSelected: icon {{ \"history_audio_cancel\", historyFileInIconFgSelected }};\noverviewSmallDownload: icon {{ \"history_audio_download\", historyFileInIconFg }};\noverviewSmallDownloadSelected: icon {{ \"history_audio_download\", historyFileInIconFgSelected }};\noverviewFileLayout: OverviewFileLayout {\n\tmaxWidth: 520px;\n\tsongPadding: margins(17px, 7px, 10px, 6px);\n\tsongThumbSize: 36px;\n\tsongNameTop: 7px;\n\tsongStatusTop: 25px;\n\tsongIconBg: msgFileInBg;\n\tsongOverBg: msgFileInBgOver;\n\tsongPause: overviewSongPause;\n\tsongPauseSelected: overviewSongPauseSelected;\n\tsongPlay: overviewSongPlay;\n\tsongPlaySelected: overviewSongPlaySelected;\n\tsongCancel: overviewSongCancel;\n\tsongCancelSelected: overviewSongCancelSelected;\n\tsongDownload: overviewSongDownload;\n\tsongDownloadSelected: overviewSongDownloadSelected;\n\n\tvoicePause: overviewVoicePause;\n\tvoicePauseSelected: overviewVoicePauseSelected;\n\tvoicePlay: overviewVoicePlay;\n\tvoicePlaySelected: overviewVoicePlaySelected;\n\tvoiceCancel: overviewVoiceCancel;\n\tvoiceCancelSelected: overviewVoiceCancelSelected;\n\tvoiceDownload: overviewVoiceDownload;\n\tvoiceDownloadSelected: overviewVoiceDownloadSelected;\n\n\tfilePadding: margins(0px, 3px, 16px, 3px);\n\tfileThumbSize: 70px;\n\tfileNameTop: 7px;\n\tfileStatusTop: 24px;\n\tfileDateTop: 49px;\n}\n\nlinksMaxWidth: 520px;\nlinksLetterFg: windowFgActive;\nlinksLetterFont: font(24px);\nlinksMargin: margins(0px, 7px, 0px, 5px);\nlinksTextFg: windowFg;\nlinksTextTop: 6px;\nlinksBorder: 1px;\nlinksBorderFg: shadowFg;\nlinksPhotoSize: 46px;\nlinksPhotoPadding: 12px;\n\noverviewVideoStatusMargin: 3px;\noverviewVideoStatusPadding: point(6px, 0px);\noverviewVideoStatusRadius: 4px;\noverviewVideoPlay: icon {{ \"overview_video_play\", historyFileThumbIconFg }};\noverviewVideoPlaySelected: icon {{ \"overview_video_play\", historyFileThumbIconFgSelected }};\noverviewVideoDownload: icon {{ \"overview_video_download\", historyFileThumbIconFg }};\noverviewVideoDownloadSelected: icon {{ \"overview_video_download\", historyFileThumbIconFgSelected }};\noverviewVideoRadialSize: 36px;\n\nstoryPinnedIcon: icon{\n\t{ \"dialogs/dialogs_pinned_shadow\", windowShadowFg },\n\t{ \"dialogs/dialogs_pinned\", historyFileThumbIconFg }\n};\nstoryPinnedIconSelected: icon{\n\t{ \"dialogs/dialogs_pinned_shadow\", windowShadowFg },\n\t{ \"dialogs/dialogs_pinned\", historyFileThumbIconFgSelected }\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/overview/overview_checkbox.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"overview/overview_checkbox.h\"\n\n#include \"styles/style_overview.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Overview::Layout {\n\nvoid Checkbox::paint(\n\t\tQPainter &p,\n\t\tQPoint position,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool selecting) {\n\t_check.setDisplayInactive(selecting);\n\t_check.setChecked(selected);\n\tconst auto pression = _pression.value((_active && _pressed) ? 1. : 0.);\n\tconst auto scale = 1. - (1. - st::overviewCheckPressedSize) * pression;\n\t_check.paint(p, position.x(), position.y(), outerWidth, scale);\n}\n\nvoid Checkbox::setActive(bool active) {\n\t_active = active;\n\tif (_pressed) {\n\t\tstartAnimation();\n\t}\n}\n\nvoid Checkbox::setPressed(bool pressed) {\n\t_pressed = pressed;\n\tif (_active) {\n\t\tstartAnimation();\n\t}\n}\n\nvoid Checkbox::setChecked(bool checked, anim::type animated) {\n\t_check.setChecked(checked, animated);\n}\n\nvoid Checkbox::startAnimation() {\n\tconst auto showPressed = (_pressed && _active);\n\t_pression.start(\n\t\t_updateCallback,\n\t\tshowPressed ? 0. : 1.,\n\t\tshowPressed ? 1. : 0.,\n\t\tst::overviewCheck.duration);\n}\n\nvoid Checkbox::finishAnimating() {\n\t_pression.stop();\n\t_check.finishAnimating();\n}\n\n} // namespace Overview::Layout\n"
  },
  {
    "path": "Telegram/SourceFiles/overview/overview_checkbox.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/round_checkbox.h\"\n\nnamespace style {\nstruct RoundCheckbox;\n} // namespace style\n\nnamespace Overview::Layout {\n\nclass Checkbox {\npublic:\n\ttemplate <typename UpdateCallback>\n\tCheckbox(UpdateCallback callback, const style::RoundCheckbox &st)\n\t: _updateCallback(callback)\n\t, _check(st, _updateCallback) {\n\t}\n\n\tvoid paint(\n\t\tQPainter &p,\n\t\tQPoint position,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool selecting);\n\n\tvoid setActive(bool active);\n\tvoid setPressed(bool pressed);\n\tvoid setChecked(bool checked, anim::type animated = anim::type::normal);\n\tvoid finishAnimating();\n\n\tvoid invalidateCache() {\n\t\t_check.invalidateCache();\n\t}\n\nprivate:\n\tvoid startAnimation();\n\n\tFn<void()> _updateCallback;\n\tUi::RoundCheckbox _check;\n\n\tUi::Animations::Simple _pression;\n\tbool _active = false;\n\tbool _pressed = false;\n\n};\n\n} // namespace Overview::Layout\n"
  },
  {
    "path": "Telegram/SourceFiles/overview/overview_layout.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"overview/overview_layout.h\"\n\n#include \"overview/overview_checkbox.h\"\n#include \"overview/overview_layout_delegate.h\"\n#include \"core/ui_integration.h\" // TextContext\n#include \"data/data_document.h\"\n#include \"data/data_document_resolver.h\"\n#include \"data/data_session.h\"\n#include \"data/data_web_page.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"layout/layout_selection.h\"\n#include \"storage/file_upload.h\"\n#include \"main/main_session.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"storage/localstorage.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/view/history_view_cursor_state.h\"\n#include \"history/view/media/history_view_media_common.h\"\n#include \"history/view/media/history_view_document.h\" // DrawThumbnailAsSongCover\n#include \"base/unixtime.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"ui/effects/round_checkbox.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/image/image.h\"\n#include \"ui/text/format_song_document_name.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_overview.h\"\n\nnamespace Overview::Layout {\nnamespace {\n\nusing TextState = HistoryView::TextState;\n\nTextParseOptions _documentNameOptions = {\n\tTextParseMultiline | TextParseLinks | TextParseMarkdown, // flags\n\t0, // maxw\n\t0, // maxh\n\tQt::LayoutDirectionAuto, // dir\n};\n\nconstexpr auto kMaxInlineArea = 1280 * 720;\nconstexpr auto kStoryRatio = 1.46;\n\n[[nodiscard]] bool CanPlayInline(not_null<DocumentData*> document) {\n\tconst auto dimensions = document->dimensions;\n\treturn dimensions.width() * dimensions.height() <= kMaxInlineArea;\n}\n\n[[nodiscard]] QImage CropMediaFrame(QImage image, int width, int height) {\n\tconst auto ratio = style::DevicePixelRatio();\n\twidth *= ratio;\n\theight *= ratio;\n\tconst auto finalize = [&](QImage result) {\n\t\tresult = result.scaled(\n\t\t\twidth,\n\t\t\theight,\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t\tresult.setDevicePixelRatio(ratio);\n\t\treturn result;\n\t};\n\tif (image.width() * height == image.height() * width) {\n\t\tif (image.width() != width) {\n\t\t\treturn finalize(std::move(image));\n\t\t}\n\t\timage.setDevicePixelRatio(ratio);\n\t\treturn image;\n\t} else if (image.width() * height > image.height() * width) {\n\t\tconst auto use = (image.height() * width) / height;\n\t\tconst auto skip = (image.width() - use) / 2;\n\t\treturn finalize(image.copy(skip, 0, use, image.height()));\n\t} else {\n\t\tconst auto use = (image.width() * height) / width;\n\t\tconst auto skip = (image.height() - use) / 2;\n\t\treturn finalize(image.copy(0, skip, image.width(), use));\n\t}\n}\n\nvoid PaintSensitiveTag(Painter &p, QRect r) {\n\tauto text = Ui::Text::String();\n\ttext.setText(\n\t\tst::semiboldTextStyle,\n\t\ttr::lng_sensitive_tag(tr::now));\n\tconst auto width = text.maxWidth();\n\tconst auto inner = QRect(0, 0, width, text.minHeight());\n\tconst auto outer = style::centerrect(r, inner.marginsAdded(st::paidTagPadding));\n\tconst auto size = outer.size();\n\tconst auto radius = std::min(size.width(), size.height()) / 2;\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(st::radialBg);\n\tp.drawRoundedRect(outer, radius, radius);\n\tp.setPen(st::radialFg);\n\ttext.draw(p, {\n\t\t.position = outer.marginsRemoved(st::paidTagPadding).topLeft(),\n\t});\n}\n\n} // namespace\n\nItemBase::ItemBase(\n\tnot_null<Delegate*> delegate,\n\tnot_null<HistoryItem*> parent)\n: _delegate(delegate)\n, _parent(parent)\n, _dateTime(ItemDateTime(parent)) {\n}\n\nItemBase::~ItemBase() = default;\n\nQDateTime ItemBase::dateTime() const {\n\treturn _dateTime;\n}\n\nvoid ItemBase::clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &action,\n\t\tbool active) {\n\t_parent->history()->session().data().requestItemRepaint(_parent);\n\tif (_check) {\n\t\t_check->setActive(active);\n\t}\n}\n\nvoid ItemBase::clickHandlerPressedChanged(\n\t\tconst ClickHandlerPtr &action,\n\t\tbool pressed) {\n\t_parent->history()->session().data().requestItemRepaint(_parent);\n\tif (_check) {\n\t\t_check->setPressed(pressed);\n\t}\n}\n\nvoid ItemBase::invalidateCache() {\n\tif (_check) {\n\t\t_check->invalidateCache();\n\t}\n}\n\nvoid ItemBase::paintCheckbox(\n\t\tPainter &p,\n\t\tQPoint position,\n\t\tbool selected,\n\t\tconst PaintContext *context) {\n\tif (selected || context->selecting) {\n\t\tensureCheckboxCreated();\n\t}\n\tif (_check) {\n\t\t_check->paint(p, position, _width, selected, context->selecting);\n\t}\n}\n\nconst style::RoundCheckbox &ItemBase::checkboxStyle() const {\n\treturn st::overviewCheck;\n}\n\nvoid ItemBase::ensureCheckboxCreated() {\n\tif (_check) {\n\t\treturn;\n\t}\n\tconst auto repaint = [=] {\n\t\t_parent->history()->session().data().requestItemRepaint(_parent);\n\t};\n\t_check = std::make_unique<Checkbox>(repaint, checkboxStyle());\n}\n\nvoid RadialProgressItem::setDocumentLinks(\n\t\tnot_null<DocumentData*> document,\n\t\tbool forceOpen) {\n\tconst auto context = parent()->fullId();\n\tsetLinks(\n\t\tstd::make_shared<DocumentOpenClickHandler>(\n\t\t\tdocument,\n\t\t\tcrl::guard(this, [=](FullMsgId id) {\n\t\t\t\tclearSpoiler();\n\t\t\t\tdelegate()->openDocument(document, id, forceOpen);\n\t\t\t}),\n\t\t\tcontext),\n\t\tstd::make_shared<DocumentSaveClickHandler>(document, context),\n\t\tstd::make_shared<DocumentCancelClickHandler>(\n\t\t\tdocument,\n\t\t\tnullptr,\n\t\t\tcontext));\n}\n\nvoid RadialProgressItem::clickHandlerActiveChanged(\n\t\tconst ClickHandlerPtr &action,\n\t\tbool active) {\n\tItemBase::clickHandlerActiveChanged(action, active);\n\tif (action == _openl || action == _savel || action == _cancell) {\n\t\tif (iconAnimated()) {\n\t\t\tconst auto repaint = [=] {\n\t\t\t\tparent()->history()->session().data().requestItemRepaint(\n\t\t\t\t\tparent());\n\t\t\t};\n\t\t\t_a_iconOver.start(\n\t\t\t\trepaint,\n\t\t\t\tactive ? 0. : 1.,\n\t\t\t\tactive ? 1. : 0.,\n\t\t\t\tst::msgFileOverDuration);\n\t\t}\n\t}\n}\n\nvoid RadialProgressItem::setLinks(\n\t\tClickHandlerPtr &&openl,\n\t\tClickHandlerPtr &&savel,\n\t\tClickHandlerPtr &&cancell) {\n\t_openl = std::move(openl);\n\t_savel = std::move(savel);\n\t_cancell = std::move(cancell);\n}\n\nvoid RadialProgressItem::radialAnimationCallback(crl::time now) const {\n\tconst auto updated = [&] {\n\t\treturn _radial->update(dataProgress(), dataFinished(), now);\n\t}();\n\tif (!anim::Disabled() || updated) {\n\t\tparent()->history()->session().data().requestItemRepaint(parent());\n\t}\n\tif (!_radial->animating()) {\n\t\tcheckRadialFinished();\n\t}\n}\n\nvoid RadialProgressItem::ensureRadial() {\n\tif (_radial) {\n\t\treturn;\n\t}\n\t_radial = std::make_unique<Ui::RadialAnimation>([=](crl::time now) {\n\t\tradialAnimationCallback(now);\n\t});\n}\n\nvoid RadialProgressItem::checkRadialFinished() const {\n\tif (_radial && !_radial->animating() && dataLoaded()) {\n\t\t_radial.reset();\n\t}\n}\n\nRadialProgressItem::~RadialProgressItem() = default;\n\nvoid StatusText::update(\n\t\tint64 newSize,\n\t\tint64 fullSize,\n\t\tTimeId duration,\n\t\tTimeId realDuration) {\n\tsetSize(newSize);\n\tif (_size == Ui::FileStatusSizeReady) {\n\t\t_text = (duration >= 0) ? Ui::FormatDurationAndSizeText(duration, fullSize) : (duration < -1 ? Ui::FormatGifAndSizeText(fullSize) : Ui::FormatSizeText(fullSize));\n\t} else if (_size == Ui::FileStatusSizeLoaded) {\n\t\t_text = (duration >= 0) ? Ui::FormatDurationText(duration) : (duration < -1 ? u\"GIF\"_q : Ui::FormatSizeText(fullSize));\n\t} else if (_size == Ui::FileStatusSizeFailed) {\n\t\t_text = tr::lng_attach_failed(tr::now);\n\t} else if (_size >= 0) {\n\t\t_text = Ui::FormatDownloadText(_size, fullSize);\n\t} else {\n\t\t_text = Ui::FormatPlayedText(-_size - 1, realDuration);\n\t}\n}\n\nvoid StatusText::setSize(int64 newSize) {\n\t_size = newSize;\n}\n\nPhoto::Photo(\n\tnot_null<Delegate*> delegate,\n\tnot_null<HistoryItem*> parent,\n\tnot_null<PhotoData*> photo,\n\tMediaOptions options)\n: ItemBase(delegate, parent)\n, _data(photo)\n, _spoiler((options.spoiler || parent->isMediaSensitive())\n\t? std::make_unique<Ui::SpoilerAnimation>([=] {\n\t\tdelegate->repaintItem(this);\n\t})\n\t: nullptr)\n, _sensitiveSpoiler(parent->isMediaSensitive() ? 1 : 0)\n, _story(options.story)\n, _storyPinned(options.storyPinned)\n, _storyShowPinned(options.storyShowPinned)\n, _storyHidden(options.storyHidden)\n, _storyShowHidden(options.storyShowHidden)\n, _link(_sensitiveSpoiler\n\t? HistoryView::MakeSensitiveMediaLink(\n\t\tstd::make_shared<LambdaClickHandler>(crl::guard(this, [=] {\n\t\t\tmaybeClearSensitiveSpoiler();\n\t\t})),\n\t\tparent)\n\t: makeOpenPhotoHandler()) {\n\tif (_data->inlineThumbnailBytes().isEmpty()\n\t\t&& (_data->hasExact(Data::PhotoSize::Small)\n\t\t\t|| _data->hasExact(Data::PhotoSize::Thumbnail))) {\n\t\t_data->load(Data::PhotoSize::Small, parent->fullId());\n\t}\n}\n\nPhoto::~Photo() = default;\n\nClickHandlerPtr Photo::makeOpenPhotoHandler() {\n\treturn std::make_shared<PhotoOpenClickHandler>(\n\t\t_data,\n\t\tcrl::guard(this, [=](FullMsgId id) {\n\t\t\tclearSpoiler();\n\t\t\tdelegate()->openPhoto(_data, id);\n\t\t}),\n\t\tparent()->fullId());\n}\n\nvoid Photo::initDimensions() {\n\t_maxw = 2 * st::overviewPhotoMinSize;\n\t_minh = _story ? qRound(_maxw * kStoryRatio) : _maxw;\n}\n\nint32 Photo::resizeGetHeight(int32 width) {\n\twidth = qMin(width, _maxw);\n\tif (_width != width) {\n\t\t_width = width;\n\t\t_height = _story ? qRound(_width * kStoryRatio) : _width;\n\t}\n\treturn _height;\n}\n\nvoid Photo::paint(Painter &p, const QRect &clip, TextSelection selection, const PaintContext *context) {\n\tconst auto selected = (selection == FullSelection);\n\tconst auto widthChanged = (_pix.width()\n\t\t!= (_width * style::DevicePixelRatio()));\n\tif (!_goodLoaded || widthChanged) {\n\t\tensureDataMediaCreated();\n\t\tconst auto good = !_spoiler\n\t\t\t&& (_dataMedia->loaded()\n\t\t\t\t|| _dataMedia->image(Data::PhotoSize::Thumbnail));\n\t\tif ((good && !_goodLoaded) || widthChanged) {\n\t\t\t_goodLoaded = good;\n\t\t\t_pix = QImage();\n\t\t\tif (_goodLoaded) {\n\t\t\t\tsetPixFrom(_dataMedia->image(Data::PhotoSize::Large)\n\t\t\t\t\t? _dataMedia->image(Data::PhotoSize::Large)\n\t\t\t\t\t: _dataMedia->image(Data::PhotoSize::Thumbnail));\n\t\t\t} else if (const auto small = _spoiler\n\t\t\t\t? nullptr\n\t\t\t\t: _dataMedia->image(Data::PhotoSize::Small)) {\n\t\t\t\tsetPixFrom(small);\n\t\t\t} else if (const auto blurred = _dataMedia->thumbnailInline()) {\n\t\t\t\tsetPixFrom(blurred);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (_pix.isNull()) {\n\t\tp.fillRect(0, 0, _width, _height, st::overviewPhotoBg);\n\t} else {\n\t\tp.drawImage(0, 0, _pix);\n\t}\n\n\tif (_spoiler) {\n\t\tconst auto paused = context->paused || On(PowerSaving::kChatSpoiler);\n\t\tUi::FillSpoilerRect(\n\t\t\tp,\n\t\t\tQRect(0, 0, _width, _height),\n\t\t\tUi::DefaultImageSpoiler().frame(\n\t\t\t\t_spoiler->index(context->ms, paused)));\n\n\t\tif (_sensitiveSpoiler) {\n\t\t\tPaintSensitiveTag(p, QRect(0, 0, _width, _height));\n\t\t}\n\t}\n\n\tif (_storyHidden) {\n\t\tdelegate()->hiddenMark()->paint(\n\t\t\tp,\n\t\t\t_pix,\n\t\t\t_hiddenBgCache,\n\t\t\tQPoint(),\n\t\t\tQSize(_width, _height),\n\t\t\t_width);\n\t}\n\n\tif (selected) {\n\t\tp.fillRect(0, 0, _width, _height, st::overviewPhotoSelectOverlay);\n\t}\n\n\tif (_storyPinned) {\n\t\tconst auto &icon = selected\n\t\t\t? st::storyPinnedIconSelected\n\t\t\t: st::storyPinnedIcon;\n\t\ticon.paint(p, _width - icon.width(), 0, _width);\n\t}\n\n\tconst auto checkDelta = st::overviewCheckSkip + st::overviewCheck.size;\n\tconst auto checkLeft = _width - checkDelta;\n\tconst auto checkTop = _height - checkDelta;\n\tpaintCheckbox(p, { checkLeft, checkTop }, selected, context);\n}\n\nvoid Photo::setPixFrom(not_null<Image*> image) {\n\tExpects(_width > 0 && _height > 0);\n\n\tauto img = image->original();\n\tif (!_goodLoaded) {\n\t\timg = Images::Blur(std::move(img));\n\t}\n\t_pix = CropMediaFrame(std::move(img), _width, _height);\n\n\t// In case we have inline thumbnail we can unload all images and we still\n\t// won't get a blank image in the media viewer when the photo is opened.\n\tif (!_data->inlineThumbnailBytes().isEmpty()) {\n\t\t_dataMedia = nullptr;\n\t\tdelegate()->unregisterHeavyItem(this);\n\t}\n}\n\nvoid Photo::ensureDataMediaCreated() const {\n\tif (_dataMedia) {\n\t\treturn;\n\t}\n\t_dataMedia = _data->createMediaView();\n\tif (_data->inlineThumbnailBytes().isEmpty()) {\n\t\t_dataMedia->wanted(Data::PhotoSize::Small, parent()->fullId());\n\t}\n\t_dataMedia->wanted(Data::PhotoSize::Thumbnail, parent()->fullId());\n\tdelegate()->registerHeavyItem(this);\n}\n\nvoid Photo::clearSpoiler() {\n\tif (_spoiler) {\n\t\t_spoiler = nullptr;\n\t\t_sensitiveSpoiler = false;\n\t\t_pix = QImage();\n\t\tdelegate()->repaintItem(this);\n\t}\n}\n\nvoid Photo::maybeClearSensitiveSpoiler() {\n\tif (_sensitiveSpoiler) {\n\t\tclearSpoiler();\n\t\t_link = makeOpenPhotoHandler();\n\t}\n}\n\nvoid Photo::itemDataChanged() {\n\tconst auto pinned = _storyShowPinned && parent()->isPinned();\n\tconst auto hidden = _storyShowHidden && !parent()->storyInProfile();\n\tif (_storyPinned != pinned || _storyHidden != hidden) {\n\t\t_storyPinned = pinned;\n\t\t_storyHidden = hidden;\n\t\tdelegate()->repaintItem(this);\n\t}\n}\n\nvoid Photo::clearHeavyPart() {\n\t_dataMedia = nullptr;\n}\n\nTextState Photo::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tif (hasPoint(point)) {\n\t\treturn { parent(), _link };\n\t}\n\treturn {};\n}\n\nVideo::Video(\n\tnot_null<Delegate*> delegate,\n\tnot_null<HistoryItem*> parent,\n\tnot_null<DocumentData*> video,\n\tMediaOptions options)\n: RadialProgressItem(delegate, parent)\n, _data(video)\n, _videoCover(LookupVideoCover(video, parent))\n, _duration(Ui::FormatDurationText(_data->duration() / 1000))\n, _spoiler((options.spoiler || parent->isMediaSensitive())\n\t? std::make_unique<Ui::SpoilerAnimation>([=] {\n\t\tdelegate->repaintItem(this);\n\t})\n\t: nullptr)\n, _sensitiveSpoiler(parent->isMediaSensitive() ? 1 : 0)\n, _story(options.story)\n, _storyPinned(options.storyPinned)\n, _storyShowPinned(options.storyShowPinned)\n, _storyHidden(options.storyHidden)\n, _storyShowHidden(options.storyShowHidden) {\n\tsetDocumentLinks(_data);\n\tif (_sensitiveSpoiler) {\n\t\t_openl = HistoryView::MakeSensitiveMediaLink(\n\t\t\tstd::make_shared<LambdaClickHandler>(crl::guard(this, [=] {\n\t\t\t\tclearSpoiler();\n\t\t\t\tsetDocumentLinks(_data);\n\t\t\t})),\n\t\t\tparent);\n\t}\n\tif (!_videoCover) {\n\t\t_data->loadThumbnail(parent->fullId());\n\t} else if (_videoCover->inlineThumbnailBytes().isEmpty()\n\t\t&& (_videoCover->hasExact(Data::PhotoSize::Small)\n\t\t\t|| _videoCover->hasExact(Data::PhotoSize::Thumbnail))) {\n\t\t_videoCover->load(Data::PhotoSize::Small, parent->fullId());\n\t}\n}\n\nVideo::~Video() = default;\n\nvoid Video::initDimensions() {\n\t_maxw = 2 * st::overviewPhotoMinSize;\n\t_minh = _story ? qRound(_maxw * kStoryRatio) : _maxw;\n}\n\nint32 Video::resizeGetHeight(int32 width) {\n\twidth = qMin(width, _maxw);\n\tif (_width != width) {\n\t\t_width = width;\n\t\t_height = _story ? qRound(_width * kStoryRatio) : _width;\n\t}\n\treturn _height;\n}\n\nvoid Video::paint(\n\t\tPainter &p,\n\t\tconst QRect &clip,\n\t\tTextSelection selection,\n\t\tconst PaintContext *context) {\n\tensureDataMediaCreated();\n\n\tconst auto selected = (selection == FullSelection);\n\tconst auto blurred = _videoCover\n\t\t? _videoCoverMedia->thumbnailInline()\n\t\t: _dataMedia->thumbnailInline();\n\tconst auto thumbnail = _spoiler\n\t\t? nullptr\n\t\t: _videoCover\n\t\t? _videoCoverMedia->image(Data::PhotoSize::Small)\n\t\t: _dataMedia->thumbnail();\n\tconst auto good = _spoiler\n\t\t? nullptr\n\t\t: _videoCover\n\t\t? _videoCoverMedia->image(Data::PhotoSize::Large)\n\t\t: _dataMedia->goodThumbnail();\n\n\tbool loaded = dataLoaded(), displayLoading = _data->displayLoading();\n\tif (displayLoading) {\n\t\tensureRadial();\n\t\tif (!_radial->animating()) {\n\t\t\t_radial->start(dataProgress());\n\t\t}\n\t}\n\tupdateStatusText();\n\tconst auto radial = isRadialAnimation();\n\tconst auto radialOpacity = radial ? _radial->opacity() : 0.;\n\n\tif ((blurred || thumbnail || good)\n\t\t&& ((_pix.width() != _width * style::DevicePixelRatio())\n\t\t\t|| (_pixBlurred && (thumbnail || good)))) {\n\t\tauto img = good\n\t\t\t? good->original()\n\t\t\t: thumbnail\n\t\t\t? thumbnail->original()\n\t\t\t: Images::Blur(blurred->original());\n\t\t_pix = CropMediaFrame(std::move(img), _width, _height);\n\t\t_pixBlurred = !(thumbnail || good);\n\t}\n\n\tif (_pix.isNull()) {\n\t\tp.fillRect(0, 0, _width, _height, st::overviewPhotoBg);\n\t} else {\n\t\tp.drawImage(0, 0, _pix);\n\t}\n\n\tif (_spoiler) {\n\t\tconst auto paused = context->paused || On(PowerSaving::kChatSpoiler);\n\t\tUi::FillSpoilerRect(\n\t\t\tp,\n\t\t\tQRect(0, 0, _width, _height),\n\t\t\tUi::DefaultImageSpoiler().frame(\n\t\t\t\t_spoiler->index(context->ms, paused)));\n\n\t\tif (_sensitiveSpoiler) {\n\t\t\tPaintSensitiveTag(p, QRect(0, 0, _width, _height));\n\t\t}\n\t}\n\n\tif (_storyHidden) {\n\t\tdelegate()->hiddenMark()->paint(\n\t\t\tp,\n\t\t\t_pix,\n\t\t\t_hiddenBgCache,\n\t\t\tQPoint(),\n\t\t\tQSize(_width, _height),\n\t\t\t_width);\n\t}\n\n\tif (selected) {\n\t\tp.fillRect(QRect(0, 0, _width, _height), st::overviewPhotoSelectOverlay);\n\t}\n\n\tif (_storyPinned) {\n\t\tconst auto &icon = selected\n\t\t\t? st::storyPinnedIconSelected\n\t\t\t: st::storyPinnedIcon;\n\t\ticon.paint(p, _width - icon.width(), 0, _width);\n\t}\n\n\tif (!selected && !context->selecting && radialOpacity < 1.) {\n\t\tif (clip.intersects(QRect(0, _height - st::normalFont->height, _width, st::normalFont->height))) {\n\t\t\tconst auto download = !loaded && !_dataMedia->canBePlayed();\n\t\t\tconst auto &icon = download\n\t\t\t\t? (selected ? st::overviewVideoDownloadSelected : st::overviewVideoDownload)\n\t\t\t\t: (selected ? st::overviewVideoPlaySelected : st::overviewVideoPlay);\n\t\t\tconst auto text = download ? _status.text() : _duration;\n\t\t\tconst auto margin = st::overviewVideoStatusMargin;\n\t\t\tconst auto padding = st::overviewVideoStatusPadding;\n\t\t\tconst auto statusX = margin + padding.x(), statusY = _height - margin - padding.y() - st::normalFont->height;\n\t\t\tconst auto statusW = icon.width() + padding.x() + st::normalFont->width(text) + 2 * padding.x();\n\t\t\tconst auto statusH = st::normalFont->height + 2 * padding.y();\n\t\t\tp.setOpacity(1. - radialOpacity);\n\t\t\tUi::FillRoundRect(p, statusX - padding.x(), statusY - padding.y(), statusW, statusH, selected ? st::msgDateImgBgSelected : st::msgDateImgBg, selected ? Ui::OverviewVideoSelectedCorners : Ui::OverviewVideoCorners);\n\t\t\tp.setFont(st::normalFont);\n\t\t\tp.setPen(st::msgDateImgFg);\n\t\t\ticon.paint(p, statusX, statusY + (st::normalFont->height - icon.height()) / 2, _width);\n\t\t\tp.drawTextLeft(statusX + icon.width() + padding.x(), statusY, _width, text, statusW - 2 * padding.x());\n\t\t}\n\t}\n\n\tQRect inner((_width - st::overviewVideoRadialSize) / 2, (_height - st::overviewVideoRadialSize) / 2, st::overviewVideoRadialSize, st::overviewVideoRadialSize);\n\tif (radial && clip.intersects(inner)) {\n\t\tp.setOpacity(radialOpacity);\n\t\tp.setPen(Qt::NoPen);\n\t\tif (selected) {\n\t\t\tp.setBrush(st::msgDateImgBgSelected);\n\t\t} else {\n\t\t\tauto over = ClickHandler::showAsActive((_data->loading() || _data->uploading()) ? _cancell : (loaded || _dataMedia->canBePlayed()) ? _openl : _savel);\n\t\t\tp.setBrush(anim::brush(st::msgDateImgBg, st::msgDateImgBgOver, _a_iconOver.value(over ? 1. : 0.)));\n\t\t}\n\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(inner);\n\t\t}\n\n\t\tconst auto icon = [&] {\n\t\t\treturn &(selected ? st::historyFileThumbCancelSelected : st::historyFileThumbCancel);\n\t\t}();\n\t\ticon->paintInCenter(p, inner);\n\t\tif (radial) {\n\t\t\tp.setOpacity(1);\n\t\t\tQRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));\n\t\t\t_radial->draw(p, rinner, st::msgFileRadialLine, selected ? st::historyFileThumbRadialFgSelected : st::historyFileThumbRadialFg);\n\t\t}\n\t}\n\tp.setOpacity(1);\n\n\tconst auto checkDelta = st::overviewCheckSkip + st::overviewCheck.size;\n\tconst auto checkLeft = _width - checkDelta;\n\tconst auto checkTop = _height - checkDelta;\n\tpaintCheckbox(p, { checkLeft, checkTop }, selected, context);\n}\n\nvoid Video::ensureDataMediaCreated() const {\n\tif (_dataMedia && (!_videoCover || _videoCoverMedia)) {\n\t\treturn;\n\t}\n\t_dataMedia = _data->createMediaView();\n\tif (_videoCover) {\n\t\t_videoCoverMedia = _videoCover->createMediaView();\n\t\t_videoCover->load(Data::PhotoSize::Large, parent()->fullId());\n\t} else {\n\t\t_dataMedia->goodThumbnailWanted();\n\t\t_dataMedia->thumbnailWanted(parent()->fullId());\n\t}\n\tdelegate()->registerHeavyItem(this);\n}\n\nvoid Video::clearSpoiler() {\n\tif (_spoiler) {\n\t\t_spoiler = nullptr;\n\t\t_sensitiveSpoiler = false;\n\t\t_pix = QImage();\n\t\tdelegate()->repaintItem(this);\n\t}\n}\n\nvoid Video::maybeClearSensitiveSpoiler() {\n\tif (_sensitiveSpoiler) {\n\t\tclearSpoiler();\n\t\tsetDocumentLinks(_data);\n\t}\n}\n\nvoid Video::itemDataChanged() {\n\tconst auto pinned = _storyShowPinned && parent()->isPinned();\n\tconst auto hidden = _storyShowHidden && !parent()->storyInProfile();\n\tif (_storyPinned != pinned || _storyHidden != hidden) {\n\t\t_storyPinned = pinned;\n\t\t_storyHidden = hidden;\n\t\tdelegate()->repaintItem(this);\n\t}\n}\n\nvoid Video::clearHeavyPart() {\n\t_dataMedia = nullptr;\n}\n\nfloat64 Video::dataProgress() const {\n\tensureDataMediaCreated();\n\treturn _dataMedia->progress();\n}\n\nbool Video::dataFinished() const {\n\treturn !_data->loading();\n}\n\nbool Video::dataLoaded() const {\n\tensureDataMediaCreated();\n\treturn _dataMedia->loaded();\n}\n\nbool Video::iconAnimated() const {\n\treturn true;\n}\n\nTextState Video::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tif (hasPoint(point)) {\n\t\tensureDataMediaCreated();\n\t\tconst auto link = _sensitiveSpoiler\n\t\t\t? _openl\n\t\t\t: (_data->loading() || _data->uploading())\n\t\t\t? _cancell\n\t\t\t: (dataLoaded() || _dataMedia->canBePlayed())\n\t\t\t? _openl\n\t\t\t: _savel;\n\t\treturn { parent(), link };\n\t}\n\treturn {};\n}\n\nvoid Video::updateStatusText() {\n\tauto statusSize = int64();\n\tif (_data->status == FileDownloadFailed || _data->status == FileUploadFailed) {\n\t\tstatusSize = Ui::FileStatusSizeFailed;\n\t} else if (_data->uploading()) {\n\t\tstatusSize = _data->uploadingData->offset;\n\t} else if (dataLoaded()) {\n\t\tstatusSize = Ui::FileStatusSizeLoaded;\n\t} else {\n\t\tstatusSize = Ui::FileStatusSizeReady;\n\t}\n\tif (statusSize != _status.size()) {\n\t\tauto status = statusSize;\n\t\tauto size = _data->size;\n\t\tif (statusSize >= 0 && statusSize < 0xFF000000LL) {\n\t\t\tsize = status;\n\t\t\tstatus = Ui::FileStatusSizeReady;\n\t\t}\n\t\t_status.update(status, size, -1, 0);\n\t\t_status.setSize(statusSize);\n\t}\n}\n\nVoice::Voice(\n\tnot_null<Delegate*> delegate,\n\tnot_null<HistoryItem*> parent,\n\tnot_null<DocumentData*> voice,\n\tconst style::OverviewFileLayout &st)\n: RadialProgressItem(delegate, parent)\n, _data(voice)\n, _namel(std::make_shared<DocumentOpenClickHandler>(\n\t_data,\n\tcrl::guard(this, [=](FullMsgId id) {\n\t\tdelegate->openDocument(_data, id);\n\t}),\n\tparent->fullId()))\n, _st(st) {\n\tAddComponents(Info::Bit());\n\n\tsetDocumentLinks(_data);\n\t_data->loadThumbnail(parent->fullId());\n\n\tupdateName();\n\tconst auto dateText = tr::link(\n\t\tlangDateTime(base::unixtime::parse(parent->date()))); // Link 1.\n\t_details.setMarkedText(\n\t\tst::defaultTextStyle,\n\t\ttr::lng_date_and_duration(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\tdateText,\n\t\t\tlt_duration,\n\t\t\t{ .text = Ui::FormatDurationText(_data->duration() / 1000) },\n\t\t\ttr::marked));\n\t_details.setLink(1, JumpToMessageClickHandler(parent));\n}\n\nvoid Voice::initDimensions() {\n\t_maxw = _st.maxWidth;\n\t_minh = _st.songPadding.top() + _st.songThumbSize + _st.songPadding.bottom() + st::lineWidth;\n}\n\nvoid Voice::paint(Painter &p, const QRect &clip, TextSelection selection, const PaintContext *context) {\n\tensureDataMediaCreated();\n\tbool selected = (selection == FullSelection);\n\tbool loaded = dataLoaded(), displayLoading = _data->displayLoading();\n\n\tif (displayLoading) {\n\t\tensureRadial();\n\t\tif (!_radial->animating()) {\n\t\t\t_radial->start(dataProgress());\n\t\t}\n\t}\n\tconst auto showPause = updateStatusText();\n\tconst auto nameVersion = parent()->fromOriginal()->nameVersion();\n\tif (_nameVersion < nameVersion) {\n\t\tupdateName();\n\t}\n\tconst auto radial = isRadialAnimation();\n\n\tconst auto nameleft = _st.songPadding.left()\n\t\t+ _st.songThumbSize\n\t\t+ _st.songPadding.right();\n\tconst auto nameright = _st.songPadding.left();\n\tconst auto nametop = _st.songNameTop;\n\tconst auto statustop = _st.songStatusTop;\n\tconst auto namewidth = _width - nameleft - nameright;\n\n\tconst auto inner = style::rtlrect(\n\t\t_st.songPadding.left(),\n\t\t_st.songPadding.top(),\n\t\t_st.songThumbSize,\n\t\t_st.songThumbSize,\n\t\t_width);\n\tif (clip.intersects(inner)) {\n\t\tif (_data->hasThumbnail()) {\n\t\t\tensureDataMediaCreated();\n\t\t}\n\t\tconst auto thumbnail = _dataMedia\n\t\t\t? _dataMedia->thumbnail()\n\t\t\t: nullptr;\n\t\tconst auto blurred = _dataMedia\n\t\t\t? _dataMedia->thumbnailInline()\n\t\t\t: nullptr;\n\n\t\tp.setPen(Qt::NoPen);\n\t\tif (thumbnail || blurred) {\n\t\t\tconst auto options = Images::Option::RoundCircle\n\t\t\t\t| (blurred ? Images::Option::Blur : Images::Option());\n\t\t\tconst auto thumb = (thumbnail ? thumbnail : blurred)->pix(\n\t\t\t\tinner.size(),\n\t\t\t\t{ .options = options });\n\t\t\tp.drawPixmap(inner.topLeft(), thumb);\n\t\t} else if (_data->hasThumbnail()) {\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.setBrush(st::imageBg);\n\t\t\tp.drawEllipse(inner);\n\t\t}\n\t\tconst auto &checkLink = (_data->loading() || _data->uploading())\n\t\t\t? _cancell\n\t\t\t: (_dataMedia->canBePlayed() || loaded)\n\t\t\t? _openl\n\t\t\t: _savel;\n\t\tif (selected) {\n\t\t\tp.setBrush((thumbnail || blurred) ? st::msgDateImgBgSelected : st::msgFileInBgSelected);\n\t\t} else if (_data->hasThumbnail()) {\n\t\t\tauto over = ClickHandler::showAsActive(checkLink);\n\t\t\tp.setBrush(anim::brush(st::msgDateImgBg, st::msgDateImgBgOver, _a_iconOver.value(over ? 1. : 0.)));\n\t\t} else {\n\t\t\tauto over = ClickHandler::showAsActive(checkLink);\n\t\t\tp.setBrush(anim::brush(st::msgFileInBg, st::msgFileInBgOver, _a_iconOver.value(over ? 1. : 0.)));\n\t\t}\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(inner);\n\t\t}\n\n\t\tif (radial) {\n\t\t\tQRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));\n\t\t\tauto &bg = selected ? st::historyFileInRadialFgSelected : st::historyFileInRadialFg;\n\t\t\t_radial->draw(p, rinner, st::msgFileRadialLine, bg);\n\t\t}\n\n\t\tconst auto icon = [&] {\n\t\t\tif (_data->loading() || _data->uploading()) {\n\t\t\t\treturn &(selected ? _st.voiceCancelSelected : _st.voiceCancel);\n\t\t\t} else if (showPause) {\n\t\t\t\treturn &(selected ? _st.voicePauseSelected : _st.voicePause);\n\t\t\t} else if (_dataMedia->canBePlayed()) {\n\t\t\t\treturn &(selected ? _st.voicePlaySelected : _st.voicePlay);\n\t\t\t}\n\t\t\treturn &(selected\n\t\t\t\t? _st.voiceDownloadSelected\n\t\t\t\t: _st.voiceDownload);\n\t\t}();\n\t\ticon->paintInCenter(p, inner);\n\t}\n\n\tif (clip.intersects(style::rtlrect(nameleft, nametop, namewidth, st::semiboldFont->height, _width))) {\n\t\tp.setPen(st::historyFileNameInFg);\n\t\t_name.drawLeftElided(p, nameleft, nametop, namewidth, _width);\n\t}\n\n\tif (clip.intersects(style::rtlrect(nameleft, statustop, namewidth, st::normalFont->height, _width))) {\n\t\tp.setFont(st::normalFont);\n\t\tp.setPen(selected ? st::mediaInFgSelected : st::mediaInFg);\n\t\tint32 unreadx = nameleft;\n\t\tif (_status.size() == Ui::FileStatusSizeLoaded || _status.size() == Ui::FileStatusSizeReady) {\n\t\t\tp.setTextPalette(selected ? st::mediaInPaletteSelected : st::mediaInPalette);\n\t\t\t_details.drawLeftElided(p, nameleft, statustop, namewidth, _width);\n\t\t\tp.restoreTextPalette();\n\t\t\tunreadx += _details.maxWidth();\n\t\t} else {\n\t\t\tint32 statusw = st::normalFont->width(_status.text());\n\t\t\tp.drawTextLeft(nameleft, statustop, _width, _status.text(), statusw);\n\t\t\tunreadx += statusw;\n\t\t}\n\t\tauto captionLeft = unreadx + st::mediaUnreadSkip;\n\t\tif (parent()->hasUnreadMediaFlag() && unreadx + st::mediaUnreadSkip + st::mediaUnreadSize <= _width) {\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(selected ? st::msgFileInBgSelected : st::msgFileInBg);\n\n\t\t\t{\n\t\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\t\tp.drawEllipse(style::rtlrect(unreadx + st::mediaUnreadSkip, statustop + st::mediaUnreadTop, st::mediaUnreadSize, st::mediaUnreadSize, _width));\n\t\t\t}\n\t\t\tcaptionLeft += st::mediaUnreadSkip + st::mediaUnreadSize;\n\t\t}\n\t\tif (!_caption.isEmpty()) {\n\t\t\tp.setPen(st::historyFileNameInFg);\n\t\t\tconst auto w = _width - captionLeft - st::defaultScrollArea.width;\n\t\t\t_caption.draw(p, Ui::Text::PaintContext{\n\t\t\t\t.position = QPoint(captionLeft, statustop),\n\t\t\t\t.availableWidth = w,\n\t\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t\t.paused = context\n\t\t\t\t\t? context->paused\n\t\t\t\t\t: On(PowerSaving::kEmojiChat),\n\t\t\t\t.pausedEmoji = On(PowerSaving::kEmojiChat),\n\t\t\t\t.pausedSpoiler = On(PowerSaving::kChatSpoiler),\n\t\t\t\t.elisionLines = 1,\n\t\t\t});\n\t\t}\n\t}\n\n\tconst auto checkDelta = _st.songThumbSize\n\t\t+ st::overviewCheckSkip\n\t\t- st::overviewSmallCheck.size;\n\tconst auto checkLeft = _st.songPadding.left() + checkDelta;\n\tconst auto checkTop = _st.songPadding.top() + checkDelta;\n\tpaintCheckbox(p, { checkLeft, checkTop }, selected, context);\n}\n\nTextState Voice::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tensureDataMediaCreated();\n\tconst auto loaded = dataLoaded();\n\n\tconst auto nameleft = _st.songPadding.left()\n\t\t+ _st.songThumbSize\n\t\t+ _st.songPadding.right();\n\tconst auto nameright = _st.songPadding.left();\n\tconst auto nametop = _st.songNameTop;\n\tconst auto statustop = _st.songStatusTop;\n\n\tconst auto inner = style::rtlrect(\n\t\t_st.songPadding.left(),\n\t\t_st.songPadding.top(),\n\t\t_st.songThumbSize,\n\t\t_st.songThumbSize,\n\t\t_width);\n\tif (inner.contains(point)) {\n\t\tconst auto link = (_data->loading() || _data->uploading())\n\t\t\t? _cancell\n\t\t\t: (_dataMedia->canBePlayed() || loaded)\n\t\t\t? _openl\n\t\t\t: _savel;\n\t\treturn { parent(), link };\n\t}\n\tauto result = TextState(parent());\n\tconst auto statusmaxwidth = _width - nameleft - nameright;\n\tconst auto statusrect = style::rtlrect(\n\t\tnameleft,\n\t\tstatustop,\n\t\tstatusmaxwidth,\n\t\tst::normalFont->height,\n\t\t_width);\n\tif (statusrect.contains(point)) {\n\t\tif (_status.size() == Ui::FileStatusSizeLoaded || _status.size() == Ui::FileStatusSizeReady) {\n\t\t\tauto textState = _details.getStateLeft(point - QPoint(nameleft, statustop), _width, _width);\n\t\t\tresult.link = textState.link;\n\t\t\tresult.cursor = textState.uponSymbol\n\t\t\t\t? HistoryView::CursorState::Text\n\t\t\t\t: HistoryView::CursorState::None;\n\t\t}\n\t}\n\tconst auto namewidth = std::min(\n\t\t_width - nameleft - nameright,\n\t\t_name.maxWidth());\n\tconst auto namerect = style::rtlrect(\n\t\tnameleft,\n\t\tnametop,\n\t\tnamewidth,\n\t\tst::normalFont->height,\n\t\t_width);\n\tif (namerect.contains(point) && !result.link && !_data->loading()) {\n\t\treturn { parent(), _namel };\n\t}\n\treturn result;\n}\n\nvoid Voice::ensureDataMediaCreated() const {\n\tif (_dataMedia) {\n\t\treturn;\n\t}\n\t_dataMedia = _data->createMediaView();\n\tdelegate()->registerHeavyItem(this);\n}\n\nvoid Voice::clearHeavyPart() {\n\t_dataMedia = nullptr;\n}\n\nfloat64 Voice::dataProgress() const {\n\tensureDataMediaCreated();\n\treturn _dataMedia->progress();\n}\n\nbool Voice::dataFinished() const {\n\treturn !_data->loading();\n}\n\nbool Voice::dataLoaded() const {\n\tensureDataMediaCreated();\n\treturn _dataMedia->loaded();\n}\n\nbool Voice::iconAnimated() const {\n\treturn true;\n}\n\nconst style::RoundCheckbox &Voice::checkboxStyle() const {\n\treturn st::overviewSmallCheck;\n}\n\nvoid Voice::updateName() {\n\tif (parent()->Has<HistoryMessageForwarded>()) {\n\t\tconst auto info = parent()->originalHiddenSenderInfo();\n\t\tconst auto name = info\n\t\t\t? tr::lng_forwarded(tr::now, lt_user, info->nameText().toString())\n\t\t\t: parent()->fromOriginal()->isChannel()\n\t\t\t? tr::lng_forwarded_channel(\n\t\t\t\ttr::now,\n\t\t\t\tlt_channel,\n\t\t\t\tparent()->fromOriginal()->name())\n\t\t\t: tr::lng_forwarded(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tparent()->fromOriginal()->name());\n\t\t_name.setText(st::semiboldTextStyle, name, Ui::NameTextOptions());\n\t} else {\n\t\t_name.setText(\n\t\t\tst::semiboldTextStyle,\n\t\t\tparent()->from()->name(),\n\t\t\tUi::NameTextOptions());\n\t}\n\t_nameVersion = parent()->fromOriginal()->nameVersion();\n\t_caption.setMarkedText(\n\t\tst::defaultTextStyle,\n\t\tparent()->originalText(),\n\t\tUi::DialogTextOptions(),\n\t\tCore::TextContext({\n\t\t\t.session = &parent()->history()->session(),\n\t\t\t.repaint = [=] { delegate()->repaintItem(this); },\n\t\t}));\n}\n\nbool Voice::updateStatusText() {\n\tauto showPause = false;\n\tauto statusSize = int64();\n\tauto realDuration = TimeId();\n\tif (_data->status == FileDownloadFailed || _data->status == FileUploadFailed) {\n\t\tstatusSize = Ui::FileStatusSizeFailed;\n\t} else if (dataLoaded()) {\n\t\tstatusSize = Ui::FileStatusSizeLoaded;\n\t} else {\n\t\tstatusSize = Ui::FileStatusSizeReady;\n\t}\n\n\tconst auto state = Media::Player::instance()->getState(AudioMsgId::Type::Voice);\n\tif (state.id == AudioMsgId(_data, parent()->fullId(), state.id.externalPlayId())\n\t\t&& !Media::Player::IsStoppedOrStopping(state.state)) {\n\t\tstatusSize = -1 - (state.position / state.frequency);\n\t\trealDuration = (state.length / state.frequency);\n\t\tshowPause = Media::Player::ShowPauseIcon(state.state);\n\t}\n\n\tif (statusSize != _status.size()) {\n\t\t_status.update(statusSize, _data->size, _data->duration() / 1000, realDuration);\n\t}\n\treturn showPause;\n}\n\nDocument::Document(\n\tnot_null<Delegate*> delegate,\n\tnot_null<HistoryItem*> parent,\n\tDocumentFields fields,\n\tconst style::OverviewFileLayout &st)\n: RadialProgressItem(delegate, parent)\n, _data(fields.document)\n, _msgl(parent->isHistoryEntry()\n\t? JumpToMessageClickHandler(parent)\n\t: nullptr)\n, _namel(std::make_shared<DocumentOpenClickHandler>(\n\t_data,\n\tcrl::guard(this, [=](FullMsgId id) {\n\t\tdelegate->openDocument(_data, id);\n\t}),\n\tparent->fullId()))\n, _st(st)\n, _generic(::Layout::DocumentGenericPreview::Create(_data))\n, _forceFileLayout(fields.forceFileLayout)\n, _date(langDateTime(base::unixtime::parse(fields.dateOverride\n\t? fields.dateOverride\n\t: parent->date())))\n, _ext(_generic.ext)\n, _datew(st::normalFont->width(_date)) {\n\t_name.setMarkedText(\n\t\tst::defaultTextStyle,\n\t\t(!_forceFileLayout\n\t\t\t? Ui::Text::FormatSongNameFor(_data).textWithEntities()\n\t\t\t: Ui::Text::FormatDownloadsName(_data)),\n\t\t_documentNameOptions);\n\n\tAddComponents(Info::Bit());\n\n\tsetDocumentLinks(_data);\n\n\t_status.update(\n\t\tUi::FileStatusSizeReady,\n\t\t_data->size,\n\t\tsongLayout() ? (_data->duration() / 1000) : -1,\n\t\t0);\n\n\tif (withThumb()) {\n\t\tif (_data->hasThumbnail()) {\n\t\t\t_data->loadThumbnail(parent->fullId());\n\t\t\tauto tw = style::ConvertScale(\n\t\t\t\t_data->thumbnailLocation().width());\n\t\t\tauto th = style::ConvertScale(\n\t\t\t\t_data->thumbnailLocation().height());\n\t\t\tif (tw > th) {\n\t\t\t\t_thumbw = (tw * _st.fileThumbSize) / th;\n\t\t\t} else {\n\t\t\t\t_thumbw = _st.fileThumbSize;\n\t\t\t}\n\t\t} else {\n\t\t\t_thumbw = _st.fileThumbSize;\n\t\t}\n\t} else {\n\t\t_thumbw = 0;\n\t}\n\n\t_extw = st::overviewFileExtFont->width(_ext);\n\tif (_extw > _st.fileThumbSize - st::overviewFileExtPadding * 2) {\n\t\t_ext = st::overviewFileExtFont->elided(_ext, _st.fileThumbSize - st::overviewFileExtPadding * 2, Qt::ElideMiddle);\n\t\t_extw = st::overviewFileExtFont->width(_ext);\n\t}\n}\n\nbool Document::downloadInCorner() const {\n\treturn _data->isAudioFile()\n\t\t&& parent()->allowsForward()\n\t\t&& _data->canBeStreamed()\n\t\t&& !_data->inappPlaybackFailed();\n}\n\nvoid Document::initDimensions() {\n\t_maxw = _st.maxWidth;\n\tif (songLayout()) {\n\t\t_minh = _st.songPadding.top() + _st.songThumbSize + _st.songPadding.bottom();\n\t} else {\n\t\t_minh = _st.filePadding.top() + _st.fileThumbSize + _st.filePadding.bottom() + st::lineWidth;\n\t}\n}\n\nvoid Document::paint(Painter &p, const QRect &clip, TextSelection selection, const PaintContext *context) {\n\tensureDataMediaCreated();\n\n\tconst auto selected = (selection == FullSelection);\n\n\tconst auto cornerDownload = downloadInCorner();\n\n\t_dataMedia->automaticLoad(parent()->fullId(), parent());\n\tconst auto loaded = dataLoaded();\n\tconst auto displayLoading = _data->displayLoading();\n\n\tif (displayLoading) {\n\t\tensureRadial();\n\t\tif (!_radial->animating()) {\n\t\t\t_radial->start(dataProgress());\n\t\t}\n\t}\n\tconst auto showPause = updateStatusText();\n\tconst auto radial = isRadialAnimation();\n\n\tint32 nameleft = 0, nametop = 0, nameright = 0, statustop = 0, datetop = -1;\n\tconst auto wthumb = withThumb();\n\n\tconst auto isSong = songLayout();\n\tif (isSong) {\n\t\tnameleft = _st.songPadding.left() + _st.songThumbSize + _st.songPadding.right();\n\t\tnameright = _st.songPadding.left();\n\t\tnametop = _st.songNameTop;\n\t\tstatustop = _st.songStatusTop;\n\n\t\tauto inner = style::rtlrect(_st.songPadding.left(), _st.songPadding.top(), _st.songThumbSize, _st.songThumbSize, _width);\n\t\tif (clip.intersects(inner)) {\n\t\t\tconst auto isLoading = (!cornerDownload\n\t\t\t\t&& (_data->loading() || _data->uploading()));\n\t\t\tp.setPen(Qt::NoPen);\n\n\t\t\tusing namespace HistoryView;\n\t\t\tconst auto coverDrawn = _data->isSongWithCover()\n\t\t\t\t&& DrawThumbnailAsSongCover(\n\t\t\t\t\tp,\n\t\t\t\t\tst::songCoverOverlayFg,\n\t\t\t\t\t_dataMedia,\n\t\t\t\t\tinner,\n\t\t\t\t\tselected);\n\t\t\tif (!coverDrawn) {\n\t\t\t\tif (selected) {\n\t\t\t\t\tp.setBrush(st::msgFileInBgSelected);\n\t\t\t\t} else {\n\t\t\t\t\tconst auto over = ClickHandler::showAsActive(isLoading\n\t\t\t\t\t\t? _cancell\n\t\t\t\t\t\t: (loaded || _dataMedia->canBePlayed())\n\t\t\t\t\t\t? _openl\n\t\t\t\t\t\t: _savel);\n\t\t\t\t\tp.setBrush(anim::brush(\n\t\t\t\t\t\t_st.songIconBg,\n\t\t\t\t\t\t_st.songOverBg,\n\t\t\t\t\t\t_a_iconOver.value(over ? 1. : 0.)));\n\t\t\t\t}\n\t\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\t\tp.drawEllipse(inner);\n\t\t\t}\n\n\t\t\tconst auto icon = [&] {\n\t\t\t\tif (!coverDrawn) {\n\t\t\t\t\tif (isLoading) {\n\t\t\t\t\t\treturn &(selected\n\t\t\t\t\t\t\t? _st.voiceCancelSelected\n\t\t\t\t\t\t\t: _st.voiceCancel);\n\t\t\t\t\t} else if (showPause) {\n\t\t\t\t\t\treturn &(selected\n\t\t\t\t\t\t\t? _st.voicePauseSelected\n\t\t\t\t\t\t\t: _st.voicePause);\n\t\t\t\t\t} else if (loaded || _dataMedia->canBePlayed()) {\n\t\t\t\t\t\treturn &(selected\n\t\t\t\t\t\t\t? _st.voicePlaySelected\n\t\t\t\t\t\t\t: _st.voicePlay);\n\t\t\t\t\t}\n\t\t\t\t\treturn &(selected\n\t\t\t\t\t\t? _st.voiceDownloadSelected\n\t\t\t\t\t\t: _st.voiceDownload);\n\t\t\t\t}\n\t\t\t\tif (isLoading) {\n\t\t\t\t\treturn &(selected ? _st.songCancelSelected : _st.songCancel);\n\t\t\t\t} else if (showPause) {\n\t\t\t\t\treturn &(selected ? _st.songPauseSelected : _st.songPause);\n\t\t\t\t} else if (loaded || _dataMedia->canBePlayed()) {\n\t\t\t\t\treturn &(selected ? _st.songPlaySelected : _st.songPlay);\n\t\t\t\t}\n\t\t\t\treturn &(selected ? _st.songDownloadSelected : _st.songDownload);\n\t\t\t}();\n\t\t\ticon->paintInCenter(p, inner);\n\n\t\t\tif (radial && !cornerDownload) {\n\t\t\t\tauto rinner = inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine));\n\t\t\t\tauto &bg = selected ? st::historyFileInRadialFgSelected : st::historyFileInRadialFg;\n\t\t\t\t_radial->draw(p, rinner, st::msgFileRadialLine, bg);\n\t\t\t}\n\n\t\t\tdrawCornerDownload(p, selected, context);\n\t\t}\n\t} else {\n\t\tnameleft = _st.fileThumbSize + _st.filePadding.right();\n\t\tnametop = st::linksBorder + _st.fileNameTop;\n\t\tstatustop = st::linksBorder + _st.fileStatusTop;\n\t\tdatetop = st::linksBorder + _st.fileDateTop;\n\n\t\tQRect border(style::rtlrect(nameleft, 0, _width - nameleft, st::linksBorder, _width));\n\t\tif (!context->skipBorder && clip.intersects(border)) {\n\t\t\tp.fillRect(clip.intersected(border), st::linksBorderFg);\n\t\t}\n\n\t\tQRect rthumb(style::rtlrect(0, st::linksBorder + _st.filePadding.top(), _st.fileThumbSize, _st.fileThumbSize, _width));\n\t\tif (clip.intersects(rthumb)) {\n\t\t\tif (wthumb) {\n\t\t\t\tensureDataMediaCreated();\n\t\t\t\tconst auto good = _data->isSvgImage()\n\t\t\t\t\t? _dataMedia->goodThumbnail()\n\t\t\t\t\t: nullptr;\n\t\t\t\tconst auto thumbnail = good\n\t\t\t\t\t? good\n\t\t\t\t\t: _dataMedia->thumbnail();\n\t\t\t\tconst auto blurred = _dataMedia->thumbnailInline();\n\t\t\t\tif (thumbnail || blurred) {\n\t\t\t\t\tif (_thumb.isNull() || (thumbnail && !_thumbLoaded)) {\n\t\t\t\t\t\t_thumbLoaded = (thumbnail != nullptr);\n\t\t\t\t\t\tconst auto options = Images::Option::RoundSmall\n\t\t\t\t\t\t\t| (_thumbLoaded\n\t\t\t\t\t\t\t\t? Images::Option()\n\t\t\t\t\t\t\t\t: Images::Option::Blur);\n\t\t\t\t\t\tconst auto image = thumbnail ? thumbnail : blurred;\n\t\t\t\t\t\t_thumb = image->pixNoCache(\n\t\t\t\t\t\t\t_thumbw * style::DevicePixelRatio(),\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t.options = options,\n\t\t\t\t\t\t\t\t.outer = QSize(\n\t\t\t\t\t\t\t\t\t_st.fileThumbSize,\n\t\t\t\t\t\t\t\t\t_st.fileThumbSize),\n\t\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tp.drawPixmap(rthumb.topLeft(), _thumb);\n\t\t\t\t} else {\n\t\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\t\tp.setBrush(st::overviewFileThumbBg);\n\t\t\t\t\tp.drawRoundedRect(\n\t\t\t\t\t\trthumb,\n\t\t\t\t\t\tst::roundRadiusSmall,\n\t\t\t\t\t\tst::roundRadiusSmall);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(_generic.color);\n\t\t\t\tp.drawRoundedRect(\n\t\t\t\t\trthumb,\n\t\t\t\t\tst::roundRadiusSmall,\n\t\t\t\t\tst::roundRadiusSmall);\n\t\t\t\tif (!radial && loaded && !_ext.isEmpty()) {\n\t\t\t\t\tp.setFont(st::overviewFileExtFont);\n\t\t\t\t\tp.setPen(st::overviewFileExtFg);\n\t\t\t\t\tp.drawText(rthumb.left() + (rthumb.width() - _extw) / 2, rthumb.top() + st::overviewFileExtTop + st::overviewFileExtFont->ascent, _ext);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (selected) {\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(st::defaultTextPalette.selectOverlay);\n\t\t\t\tp.drawRoundedRect(\n\t\t\t\t\trthumb,\n\t\t\t\t\tst::roundRadiusSmall,\n\t\t\t\t\tst::roundRadiusSmall);\n\t\t\t}\n\n\t\t\tif (radial || (!loaded && !_data->loading())) {\n\t\t\t\tQRect inner(rthumb.x() + (rthumb.width() - _st.songThumbSize) / 2, rthumb.y() + (rthumb.height() - _st.songThumbSize) / 2, _st.songThumbSize, _st.songThumbSize);\n\t\t\t\tif (clip.intersects(inner)) {\n\t\t\t\t\tauto radialOpacity = (radial && loaded && !_data->uploading()) ? _radial->opacity() : 1;\n\t\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\t\tif (selected) {\n\t\t\t\t\t\tp.setBrush(wthumb\n\t\t\t\t\t\t\t? st::msgDateImgBgSelected\n\t\t\t\t\t\t\t: _generic.selected);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tauto over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel);\n\t\t\t\t\t\tp.setBrush(anim::brush(\n\t\t\t\t\t\t\twthumb ? st::msgDateImgBg : _generic.dark,\n\t\t\t\t\t\t\twthumb ? st::msgDateImgBgOver : _generic.over,\n\t\t\t\t\t\t\t_a_iconOver.value(over ? 1. : 0.)));\n\t\t\t\t\t}\n\t\t\t\t\tp.setOpacity(radialOpacity * p.opacity());\n\n\t\t\t\t\t{\n\t\t\t\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\t\t\t\tp.drawEllipse(inner);\n\t\t\t\t\t}\n\n\t\t\t\t\tp.setOpacity(radialOpacity);\n\t\t\t\t\tauto icon = ([loaded, this, selected] {\n\t\t\t\t\t\tif (loaded || _data->loading()) {\n\t\t\t\t\t\t\treturn &(selected ? st::historyFileThumbCancelSelected : st::historyFileThumbCancel);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn &(selected ? st::historyFileThumbDownloadSelected : st::historyFileThumbDownload);\n\t\t\t\t\t})();\n\t\t\t\t\ticon->paintInCenter(p, inner);\n\t\t\t\t\tif (radial) {\n\t\t\t\t\t\tp.setOpacity(1);\n\n\t\t\t\t\t\tQRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));\n\t\t\t\t\t\t_radial->draw(p, rinner, st::msgFileRadialLine, selected ? st::historyFileThumbRadialFgSelected : st::historyFileThumbRadialFg);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tconst auto availwidth = _width - nameleft - nameright;\n\tconst auto namewidth = std::min(availwidth, _name.maxWidth());\n\tif (clip.intersects(style::rtlrect(nameleft, nametop, namewidth, st::semiboldFont->height, _width))) {\n\t\tp.setPen(st::historyFileNameInFg);\n\t\t_name.drawLeftElided(p, nameleft, nametop, namewidth, _width);\n\t}\n\n\tif (clip.intersects(style::rtlrect(nameleft, statustop, availwidth, st::normalFont->height, _width))) {\n\t\tp.setFont(st::normalFont);\n\t\tp.setPen((isSong && selected) ? st::mediaInFgSelected : st::mediaInFg);\n\t\tp.drawTextLeft(nameleft, statustop, _width, _status.text());\n\t}\n\tif (datetop >= 0 && clip.intersects(style::rtlrect(nameleft, datetop, _datew, st::normalFont->height, _width))) {\n\t\tp.setFont((_msgl && ClickHandler::showAsActive(_msgl))\n\t\t\t? st::normalFont->underline()\n\t\t\t: st::normalFont);\n\t\tp.setPen(st::mediaInFg);\n\t\tp.drawTextLeft(nameleft, datetop, _width, _date, _datew);\n\t}\n\n\tconst auto checkDelta = (isSong ? _st.songThumbSize : _st.fileThumbSize)\n\t\t+ (isSong ? st::overviewCheckSkip : -st::overviewCheckSkip)\n\t\t- st::overviewSmallCheck.size;\n\tconst auto checkLeft = (isSong\n\t\t? _st.songPadding.left()\n\t\t: 0) + checkDelta;\n\tconst auto checkTop = (isSong\n\t\t? _st.songPadding.top()\n\t\t: (st::linksBorder + _st.filePadding.top())) + checkDelta;\n\tpaintCheckbox(p, { checkLeft, checkTop }, selected, context);\n}\n\nvoid Document::drawCornerDownload(QPainter &p, bool selected, const PaintContext *context) const {\n\tif (dataLoaded()\n\t\t|| _data->loadedInMediaCache()\n\t\t|| !downloadInCorner()) {\n\t\treturn;\n\t}\n\tconst auto size = st::overviewSmallCheck.size;\n\tconst auto shift = _st.songThumbSize + st::overviewCheckSkip - size;\n\tconst auto inner = style::rtlrect(_st.songPadding.left() + shift, _st.songPadding.top() + shift, size, size, _width);\n\tauto pen = st::windowBg->p;\n\tpen.setWidth(st::lineWidth);\n\tp.setPen(pen);\n\tif (selected) {\n\t\tp.setBrush(st::msgFileInBgSelected);\n\t} else {\n\t\tp.setBrush(_st.songIconBg);\n\t}\n\t{\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.drawEllipse(inner);\n\t}\n\tconst auto icon = [&] {\n\t\tif (_data->loading()) {\n\t\t\treturn &(selected ? st::overviewSmallCancelSelected : st::overviewSmallCancel);\n\t\t}\n\t\treturn &(selected ? st::overviewSmallDownloadSelected : st::overviewSmallDownload);\n\t}();\n\ticon->paintInCenter(p, inner);\n\tif (_radial && _radial->animating()) {\n\t\tconst auto rinner = inner.marginsRemoved(QMargins(st::historyAudioRadialLine, st::historyAudioRadialLine, st::historyAudioRadialLine, st::historyAudioRadialLine));\n\t\tconst auto &fg = selected\n\t\t\t? st::historyFileInIconFgSelected\n\t\t\t: st::historyFileInIconFg;\n\t\t_radial->draw(p, rinner, st::historyAudioRadialLine, fg);\n\t}\n}\n\nTextState Document::cornerDownloadTextState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tauto result = TextState(parent());\n\tif (!downloadInCorner()\n\t\t|| dataLoaded()\n\t\t|| _data->loadedInMediaCache()) {\n\t\treturn result;\n\t}\n\tconst auto size = st::overviewSmallCheck.size;\n\tconst auto shift = _st.songThumbSize + st::overviewCheckSkip - size;\n\tconst auto inner = style::rtlrect(_st.songPadding.left() + shift, _st.songPadding.top() + shift, size, size, _width);\n\tif (inner.contains(point)) {\n\t\tresult.link = _data->loading() ? _cancell : _savel;\n\t}\n\treturn result;\n\n}\n\nTextState Document::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tensureDataMediaCreated();\n\tconst auto loaded = dataLoaded();\n\n\tif (songLayout()) {\n\t\tconst auto nameleft = _st.songPadding.left() + _st.songThumbSize + _st.songPadding.right();\n\t\tconst auto nameright = _st.songPadding.left();\n\t\tconst auto namewidth = std::min(\n\t\t\t_width - nameleft - nameright,\n\t\t\t_name.maxWidth());\n\t\tconst auto nametop = _st.songNameTop;\n\n\t\tif (const auto state = cornerDownloadTextState(point, request); state.link) {\n\t\t\treturn state;\n\t\t}\n\n\t\tconst auto inner = style::rtlrect(\n\t\t\t_st.songPadding.left(),\n\t\t\t_st.songPadding.top(),\n\t\t\t_st.songThumbSize,\n\t\t\t_st.songThumbSize,\n\t\t\t_width);\n\t\tif (inner.contains(point)) {\n\t\t\tconst auto link = (!downloadInCorner()\n\t\t\t\t&& (_data->loading() || _data->uploading()))\n\t\t\t\t? _cancell\n\t\t\t\t: (loaded || _dataMedia->canBePlayed())\n\t\t\t\t? _openl\n\t\t\t\t: _savel;\n\t\t\treturn { parent(), link };\n\t\t}\n\t\tconst auto namerect = style::rtlrect(\n\t\t\tnameleft,\n\t\t\tnametop,\n\t\t\tnamewidth,\n\t\t\tst::semiboldFont->height,\n\t\t\t_width);\n\t\tif (namerect.contains(point) && !_data->loading()) {\n\t\t\treturn { parent(), _namel };\n\t\t}\n\t} else {\n\t\tconst auto nameleft = _st.fileThumbSize + _st.filePadding.right();\n\t\tconst auto nameright = 0;\n\t\tconst auto nametop = st::linksBorder + _st.fileNameTop;\n\t\tconst auto namewidth = std::min(\n\t\t\t_width - nameleft - nameright,\n\t\t\t_name.maxWidth());\n\t\tconst auto datetop = st::linksBorder + _st.fileDateTop;\n\n\t\tconst auto rthumb = style::rtlrect(\n\t\t\t0,\n\t\t\tst::linksBorder + _st.filePadding.top(),\n\t\t\t_st.fileThumbSize,\n\t\t\t_st.fileThumbSize,\n\t\t\t_width);\n\n\t\tif (rthumb.contains(point)) {\n\t\t\tconst auto link = (_data->loading() || _data->uploading())\n\t\t\t\t? _cancell\n\t\t\t\t: loaded\n\t\t\t\t? _openl\n\t\t\t\t: _savel;\n\t\t\treturn { parent(), link };\n\t\t}\n\n\t\tif (_data->status != FileUploadFailed) {\n\t\t\tauto daterect = style::rtlrect(\n\t\t\t\tnameleft,\n\t\t\t\tdatetop,\n\t\t\t\t_datew,\n\t\t\t\tst::normalFont->height,\n\t\t\t\t_width);\n\t\t\tif (daterect.contains(point)) {\n\t\t\t\treturn { parent(), _msgl };\n\t\t\t}\n\t\t}\n\t\tif (!_data->loading() && !_data->isNull()) {\n\t\t\tauto leftofnamerect = style::rtlrect(\n\t\t\t\t0,\n\t\t\t\tst::linksBorder,\n\t\t\t\tnameleft,\n\t\t\t\t_height - st::linksBorder,\n\t\t\t\t_width);\n\t\t\tif (loaded && leftofnamerect.contains(point)) {\n\t\t\t\treturn { parent(), _namel };\n\t\t\t}\n\t\t\tconst auto namerect = style::rtlrect(\n\t\t\t\tnameleft,\n\t\t\t\tnametop,\n\t\t\t\tnamewidth,\n\t\t\t\tst::semiboldFont->height,\n\t\t\t\t_width);\n\t\t\tif (namerect.contains(point)) {\n\t\t\t\treturn { parent(), _namel };\n\t\t\t}\n\t\t}\n\t}\n\treturn {};\n}\n\nconst style::RoundCheckbox &Document::checkboxStyle() const {\n\treturn st::overviewSmallCheck;\n}\n\nbool Document::songLayout() const {\n\treturn !_forceFileLayout && _data->isSong();\n}\n\nvoid Document::ensureDataMediaCreated() const {\n\tif (_dataMedia) {\n\t\treturn;\n\t}\n\t_dataMedia = _data->createMediaView();\n\t_dataMedia->thumbnailWanted(parent()->fullId());\n\tif (_data->isSvgImage()) {\n\t\t_dataMedia->goodThumbnailWanted();\n\t\tData::DocumentMedia::CheckGoodThumbnail(_data);\n\t}\n\tdelegate()->registerHeavyItem(this);\n}\n\nvoid Document::clearHeavyPart() {\n\t_dataMedia = nullptr;\n}\n\nfloat64 Document::dataProgress() const {\n\tensureDataMediaCreated();\n\treturn _dataMedia->progress();\n}\n\nbool Document::dataFinished() const {\n\treturn !_data->loading();\n}\n\nbool Document::dataLoaded() const {\n\tensureDataMediaCreated();\n\treturn _dataMedia->loaded();\n}\n\nbool Document::iconAnimated() const {\n\treturn songLayout()\n\t\t|| !dataLoaded()\n\t\t|| (_radial && _radial->animating());\n}\n\nbool Document::withThumb() const {\n\treturn !songLayout()\n\t\t&& (_data->hasThumbnail() || _data->isSvgImage());\n}\n\nbool Document::updateStatusText() {\n\tauto showPause = false;\n\tauto statusSize = int64();\n\tauto realDuration = TimeId();\n\tif (_data->status == FileDownloadFailed\n\t\t|| _data->status == FileUploadFailed) {\n\t\tstatusSize = Ui::FileStatusSizeFailed;\n\t} else if (_data->uploading()) {\n\t\tstatusSize = _data->uploadingData->offset;\n\t} else if (_data->loading()) {\n\t\tstatusSize = _data->loadOffset();\n\t} else if (dataLoaded()) {\n\t\tstatusSize = Ui::FileStatusSizeLoaded;\n\t} else {\n\t\tstatusSize = Ui::FileStatusSizeReady;\n\t}\n\n\tconst auto isSong = songLayout();\n\tif (isSong) {\n\t\tconst auto state = Media::Player::instance()->getState(AudioMsgId::Type::Song);\n\t\tif (state.id == AudioMsgId(_data, parent()->fullId(), state.id.externalPlayId()) && !Media::Player::IsStoppedOrStopping(state.state)) {\n\t\t\tstatusSize = -1 - (state.position / state.frequency);\n\t\t\trealDuration = (state.length / state.frequency);\n\t\t\tshowPause = Media::Player::ShowPauseIcon(state.state);\n\t\t}\n\t\tif (!showPause && (state.id == AudioMsgId(_data, parent()->fullId(), state.id.externalPlayId())) && Media::Player::instance()->isSeeking(AudioMsgId::Type::Song)) {\n\t\t\tshowPause = true;\n\t\t}\n\t}\n\n\tif (statusSize != _status.size()) {\n\t\t_status.update(\n\t\t\tstatusSize,\n\t\t\t_data->size,\n\t\t\tisSong ? (_data->duration() / 1000) : -1,\n\t\t\trealDuration);\n\t}\n\treturn showPause;\n}\n\nLink::Link(\n\tnot_null<Delegate*> delegate,\n\tnot_null<HistoryItem*> parent,\n\tData::Media *media)\n: ItemBase(delegate, parent)\n, _text(st::msgMinWidth) {\n\tAddComponents(Info::Bit());\n\n\tauto textWithEntities = parent->originalText();\n\tQString mainUrl;\n\n\tauto text = textWithEntities.text;\n\tconst auto &entities = textWithEntities.entities;\n\tint32 from = 0, till = text.size(), lnk = entities.size();\n\tfor (const auto &entity : entities) {\n\t\tauto type = entity.type();\n\t\tif (type != EntityType::Url && type != EntityType::CustomUrl && type != EntityType::Email) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto customUrl = entity.data();\n\t\tconst auto entityText = text.mid(entity.offset(), entity.length());\n\t\tconst auto url = customUrl.isEmpty() ? entityText : customUrl;\n\t\tif (_links.isEmpty()) {\n\t\t\tmainUrl = url;\n\t\t}\n\t\t_links.push_back(LinkEntry(url, entityText));\n\t}\n\tif (_links.empty()) {\n\t\tif (const auto media = parent->media()) {\n\t\t\tif (const auto webpage = media->webpage()) {\n\t\t\t\tif (!webpage->displayUrl.isEmpty()\n\t\t\t\t\t&& !webpage->url.isEmpty()) {\n\t\t\t\t\t_links.push_back(\n\t\t\t\t\t\tLinkEntry(webpage->displayUrl, webpage->url));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\twhile (lnk > 0 && till > from) {\n\t\t--lnk;\n\t\tauto &entity = entities.at(lnk);\n\t\tauto type = entity.type();\n\t\tif (type != EntityType::Url && type != EntityType::CustomUrl && type != EntityType::Email) {\n\t\t\t++lnk;\n\t\t\tbreak;\n\t\t}\n\t\tint32 afterLinkStart = entity.offset() + entity.length();\n\t\tif (till > afterLinkStart) {\n\t\t\tif (!QRegularExpression(u\"^[,.\\\\s_=+\\\\-;:`'\\\"\\\\(\\\\)\\\\[\\\\]\\\\{\\\\}<>*&^%\\\\$#@!\\\\\\\\/]+$\"_q).match(text.mid(afterLinkStart, till - afterLinkStart)).hasMatch()) {\n\t\t\t\t++lnk;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\ttill = entity.offset();\n\t}\n\tif (!lnk) {\n\t\tif (QRegularExpression(u\"^[,.\\\\s\\\\-;:`'\\\"\\\\(\\\\)\\\\[\\\\]\\\\{\\\\}<>*&^%\\\\$#@!\\\\\\\\/]+$\"_q).match(text.mid(from, till - from)).hasMatch()) {\n\t\t\ttill = from;\n\t\t}\n\t}\n\n\tconst auto createHandler = [](const QString &url) {\n\t\treturn UrlClickHandler::IsSuspicious(url)\n\t\t\t? std::make_shared<HiddenUrlClickHandler>(url)\n\t\t\t: std::make_shared<UrlClickHandler>(url, false);\n\t};\n\t_page = media ? media->webpage() : nullptr;\n\tif (_page) {\n\t\tmainUrl = _page->url;\n\t\tif (_page->document) {\n\t\t\t_photol = std::make_shared<DocumentOpenClickHandler>(\n\t\t\t\t_page->document,\n\t\t\t\tcrl::guard(this, [=](FullMsgId id) {\n\t\t\t\t\tdelegate->openDocument(_page->document, id);\n\t\t\t\t}),\n\t\t\t\tparent->fullId());\n\t\t} else if (_page->photo) {\n\t\t\tif (_page->type == WebPageType::Profile\n\t\t\t\t|| _page->type == WebPageType::Video) {\n\t\t\t\t_photol = createHandler(_page->url);\n\t\t\t} else if (_page->type == WebPageType::Photo\n\t\t\t\t|| _page->type == WebPageType::Document\n\t\t\t\t|| _page->siteName == u\"Twitter\"_q\n\t\t\t\t|| _page->siteName == u\"Facebook\"_q) {\n\t\t\t\t_photol = std::make_shared<PhotoOpenClickHandler>(\n\t\t\t\t\t_page->photo,\n\t\t\t\t\tcrl::guard(this, [=](FullMsgId id) {\n\t\t\t\t\t\tdelegate->openPhoto(_page->photo, id);\n\t\t\t\t\t}),\n\t\t\t\t\tparent->fullId());\n\t\t\t} else {\n\t\t\t\t_photol = createHandler(_page->url);\n\t\t\t}\n\t\t} else {\n\t\t\t_photol = createHandler(_page->url);\n\t\t}\n\t} else if (!mainUrl.isEmpty()) {\n\t\t_photol = createHandler(mainUrl);\n\t}\n\tif (from >= till && _page) {\n\t\ttext = _page->description.text;\n\t\tfrom = 0;\n\t\ttill = text.size();\n\t}\n\tif (till > from) {\n\t\tTextParseOptions opts = { TextParseMultiline, int32(st::linksMaxWidth), 3 * st::normalFont->height, Qt::LayoutDirectionAuto };\n\t\t_text.setText(st::defaultTextStyle, text.mid(from, till - from), opts);\n\t}\n\tint32 tw = 0, th = 0;\n\tif (_page && _page->photo) {\n\t\tconst auto photo = _page->photo;\n\t\tif (photo->hasExact(Data::PhotoSize::Small)\n\t\t\t|| photo->hasExact(Data::PhotoSize::Thumbnail)) {\n\t\t\tphoto->load(Data::PhotoSize::Small, parent->fullId());\n\t\t}\n\t\ttw = style::ConvertScale(photo->width());\n\t\tth = style::ConvertScale(photo->height());\n\t} else if (_page && _page->document && _page->document->hasThumbnail()) {\n\t\t_page->document->loadThumbnail(parent->fullId());\n\t\tconst auto &location = _page->document->thumbnailLocation();\n\t\ttw = style::ConvertScale(location.width());\n\t\tth = style::ConvertScale(location.height());\n\t}\n\tif (tw > st::linksPhotoSize) {\n\t\tif (th > tw) {\n\t\t\tth = th * st::linksPhotoSize / tw;\n\t\t\ttw = st::linksPhotoSize;\n\t\t} else if (th > st::linksPhotoSize) {\n\t\t\ttw = tw * st::linksPhotoSize / th;\n\t\t\tth = st::linksPhotoSize;\n\t\t}\n\t}\n\t_pixw = qMax(tw, 1);\n\t_pixh = qMax(th, 1);\n\n\tif (_page) {\n\t\t_title = _page->title;\n\t}\n\n\tauto parts = QStringView(mainUrl).split('/');\n\tif (!parts.isEmpty()) {\n\t\tauto domain = parts.at(0);\n\t\tif (parts.size() > 2 && domain.endsWith(':') && parts.at(1).isEmpty()) { // http:// and others\n\t\t\tdomain = parts.at(2);\n\t\t}\n\n\t\tparts = domain.split('@').constLast().split('.', Qt::SkipEmptyParts);\n\t\tif (parts.size() > 1) {\n\t\t\t_letter = parts.at(parts.size() - 2).at(0).toUpper();\n\t\t\tif (_title.isEmpty()) {\n\t\t\t\t_title.reserve(parts.at(parts.size() - 2).size());\n\t\t\t\t_title.append(_letter).append(parts.at(parts.size() - 2).mid(1));\n\t\t\t}\n\t\t}\n\t}\n\t_titlew = st::semiboldFont->width(_title);\n}\n\nvoid Link::initDimensions() {\n\t_maxw = st::linksMaxWidth;\n\t_minh = 0;\n\tif (!_title.isEmpty()) {\n\t\t_minh += st::semiboldFont->height;\n\t}\n\tif (!_text.isEmpty()) {\n\t\t_minh += qMin(3 * st::normalFont->height, _text.countHeight(_maxw - st::linksPhotoSize - st::linksPhotoPadding));\n\t}\n\t_minh += _links.size() * st::normalFont->height;\n\t_minh = qMax(_minh, int32(st::linksPhotoSize)) + st::linksMargin.top() + st::linksMargin.bottom() + st::linksBorder;\n}\n\nint32 Link::resizeGetHeight(int32 width) {\n\t_width = qMin(width, _maxw);\n\tint32 w = _width - st::linksPhotoSize - st::linksPhotoPadding;\n\tfor (const auto &link : std::as_const(_links)) {\n\t\tlink.lnk->setFullDisplayed(w >= link.width);\n\t}\n\n\t_height = 0;\n\tif (!_title.isEmpty()) {\n\t\t_height += st::semiboldFont->height;\n\t}\n\tif (!_text.isEmpty()) {\n\t\t_height += qMin(3 * st::normalFont->height, _text.countHeight(_width - st::linksPhotoSize - st::linksPhotoPadding));\n\t}\n\t_height += _links.size() * st::normalFont->height;\n\t_height = qMax(_height, int32(st::linksPhotoSize)) + st::linksMargin.top() + st::linksMargin.bottom() + st::linksBorder;\n\treturn _height;\n}\n\nvoid Link::paint(Painter &p, const QRect &clip, TextSelection selection, const PaintContext *context) {\n\tauto selected = (selection == FullSelection);\n\n\tconst auto pixLeft = 0;\n\tconst auto pixTop = st::linksMargin.top() + st::linksBorder;\n\tif (clip.intersects(style::rtlrect(0, pixTop, st::linksPhotoSize, st::linksPhotoSize, _width))) {\n\t\tvalidateThumbnail();\n\t\tif (!_thumbnail.isNull()) {\n\t\t\tp.drawPixmap(pixLeft, pixTop, _thumbnail);\n\t\t}\n\t}\n\n\tconst auto left = st::linksPhotoSize + st::linksPhotoPadding;\n\tconst auto w = _width - left;\n\tauto top = [&] {\n\t\tif (!_title.isEmpty() && _text.isEmpty() && _links.size() == 1) {\n\t\t\treturn pixTop + (st::linksPhotoSize - st::semiboldFont->height - st::normalFont->height) / 2;\n\t\t}\n\t\treturn st::linksTextTop;\n\t}();\n\n\tp.setPen(st::linksTextFg);\n\tp.setFont(st::semiboldFont);\n\tif (!_title.isEmpty()) {\n\t\tif (clip.intersects(style::rtlrect(left, top, qMin(w, _titlew), st::semiboldFont->height, _width))) {\n\t\t\tp.drawTextLeft(left, top, _width, (w < _titlew) ? st::semiboldFont->elided(_title, w) : _title);\n\t\t}\n\t\ttop += st::semiboldFont->height;\n\t}\n\tp.setFont(st::msgFont);\n\tif (!_text.isEmpty()) {\n\t\tint32 h = qMin(st::normalFont->height * 3, _text.countHeight(w));\n\t\tif (clip.intersects(style::rtlrect(left, top, w, h, _width))) {\n\t\t\t_text.drawLeftElided(p, left, top, w, _width, 3);\n\t\t}\n\t\ttop += h;\n\t}\n\n\tp.setPen(st::windowActiveTextFg);\n\tfor (const auto &link : std::as_const(_links)) {\n\t\tif (clip.intersects(style::rtlrect(left, top, qMin(w, link.width), st::normalFont->height, _width))) {\n\t\t\tp.setFont(ClickHandler::showAsActive(link.lnk) ? st::normalFont->underline() : st::normalFont);\n\t\t\tp.drawTextLeft(left, top, _width, (w < link.width) ? st::normalFont->elided(link.text, w) : link.text);\n\t\t}\n\t\ttop += st::normalFont->height;\n\t}\n\n\tQRect border(style::rtlrect(left, 0, w, st::linksBorder, _width));\n\tif (!context->skipBorder && clip.intersects(border)) {\n\t\tp.fillRect(clip.intersected(border), st::linksBorderFg);\n\t}\n\n\tconst auto checkDelta = st::linksPhotoSize + st::overviewCheckSkip\n\t\t- st::overviewSmallCheck.size;\n\tconst auto checkLeft = pixLeft + checkDelta;\n\tconst auto checkTop = pixTop + checkDelta;\n\tpaintCheckbox(p, { checkLeft, checkTop }, selected, context);\n}\n\nvoid Link::validateThumbnail() {\n\tif (!_thumbnail.isNull() && !_thumbnailBlurred) {\n\t\treturn;\n\t}\n\tconst auto size = QSize(_pixw, _pixh);\n\tconst auto outer = QSize(st::linksPhotoSize, st::linksPhotoSize);\n\tif (_page && _page->photo) {\n\t\tusing Data::PhotoSize;\n\t\tensurePhotoMediaCreated();\n\t\tconst auto args = Images::PrepareArgs{\n\t\t\t.options = Images::Option::RoundSmall,\n\t\t\t.outer = outer,\n\t\t};\n\t\tif (const auto thumbnail = _photoMedia->image(PhotoSize::Thumbnail)) {\n\t\t\t_thumbnail = thumbnail->pixSingle(size, args);\n\t\t\t_thumbnailBlurred = false;\n\t\t} else if (const auto large = _photoMedia->image(PhotoSize::Large)) {\n\t\t\t_thumbnail = large->pixSingle(size, args);\n\t\t\t_thumbnailBlurred = false;\n\t\t} else if (const auto small = _photoMedia->image(PhotoSize::Small)) {\n\t\t\t_thumbnail = small->pixSingle(size, args);\n\t\t\t_thumbnailBlurred = false;\n\t\t} else if (const auto blurred = _photoMedia->thumbnailInline()) {\n\t\t\t_thumbnail = blurred->pixSingle(size, args.blurred());\n\t\t\treturn;\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t\t_photoMedia = nullptr;\n\t\tdelegate()->unregisterHeavyItem(this);\n\t} else if (_page && _page->document && _page->document->hasThumbnail()) {\n\t\tensureDocumentMediaCreated();\n\t\tconst auto args = Images::PrepareArgs{\n\t\t\t.options = (_page->document->isVideoMessage()\n\t\t\t\t? Images::Option::RoundCircle\n\t\t\t\t: Images::Option::RoundSmall),\n\t\t\t.outer = outer,\n\t\t};\n\t\tif (const auto thumbnail = _documentMedia->thumbnail()) {\n\t\t\t_thumbnail = thumbnail->pixSingle(size, args);\n\t\t\t_thumbnailBlurred = false;\n\t\t} else if (const auto blurred = _documentMedia->thumbnailInline()) {\n\t\t\t_thumbnail = blurred->pixSingle(size, args.blurred());\n\t\t\treturn;\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t\t_documentMedia = nullptr;\n\t\tdelegate()->unregisterHeavyItem(this);\n\t} else {\n\t\tconst auto size = QSize(st::linksPhotoSize, st::linksPhotoSize);\n\t\t_thumbnail = QPixmap(size * style::DevicePixelRatio());\n\t\t_thumbnail.fill(Qt::transparent);\n\t\tauto p = Painter(&_thumbnail);\n\t\tconst auto index = _letter.isEmpty()\n\t\t\t? 0\n\t\t\t: (_letter[0].unicode() % 4);\n\t\tconst auto fill = [&](style::color color, Ui::CachedRoundCorners corners) {\n\t\t\tauto pixRect = QRect(\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\tst::linksPhotoSize,\n\t\t\t\tst::linksPhotoSize);\n\t\t\tUi::FillRoundRect(p, pixRect, color, corners);\n\t\t};\n\t\tswitch (index) {\n\t\tcase 0: fill(st::msgFile1Bg, Ui::Doc1Corners); break;\n\t\tcase 1: fill(st::msgFile2Bg, Ui::Doc2Corners); break;\n\t\tcase 2: fill(st::msgFile3Bg, Ui::Doc3Corners); break;\n\t\tcase 3: fill(st::msgFile4Bg, Ui::Doc4Corners); break;\n\t\t}\n\n\t\tif (!_letter.isEmpty()) {\n\t\t\tp.setFont(st::linksLetterFont);\n\t\t\tp.setPen(st::linksLetterFg);\n\t\t\tp.drawText(\n\t\t\t\tQRect(0, 0, st::linksPhotoSize, st::linksPhotoSize),\n\t\t\t\t_letter,\n\t\t\t\tstyle::al_center);\n\t\t}\n\t\t_thumbnailBlurred = false;\n\t}\n}\n\nvoid Link::ensurePhotoMediaCreated() {\n\tif (_photoMedia) {\n\t\treturn;\n\t}\n\t_photoMedia = _page->photo->createMediaView();\n\t_photoMedia->wanted(Data::PhotoSize::Small, parent()->fullId());\n\tdelegate()->registerHeavyItem(this);\n}\n\nvoid Link::ensureDocumentMediaCreated() {\n\tif (_documentMedia) {\n\t\treturn;\n\t}\n\t_documentMedia = _page->document->createMediaView();\n\t_documentMedia->thumbnailWanted(parent()->fullId());\n\tdelegate()->registerHeavyItem(this);\n}\n\nvoid Link::clearHeavyPart() {\n\t_photoMedia = nullptr;\n\t_documentMedia = nullptr;\n}\n\nTextState Link::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tint32 left = st::linksPhotoSize + st::linksPhotoPadding, top = st::linksMargin.top() + st::linksBorder, w = _width - left;\n\tif (style::rtlrect(0, top, st::linksPhotoSize, st::linksPhotoSize, _width).contains(point)) {\n\t\treturn { parent(), _photol };\n\t}\n\n\tif (!_title.isEmpty() && _text.isEmpty() && _links.size() == 1) {\n\t\ttop += (st::linksPhotoSize - st::semiboldFont->height - st::normalFont->height) / 2;\n\t}\n\tif (!_title.isEmpty()) {\n\t\tif (style::rtlrect(left, top, qMin(w, _titlew), st::semiboldFont->height, _width).contains(point)) {\n\t\t\treturn { parent(), _photol };\n\t\t}\n\t\ttop += st::webPageTitleFont->height;\n\t}\n\tif (!_text.isEmpty()) {\n\t\ttop += qMin(st::normalFont->height * 3, _text.countHeight(w));\n\t}\n\tfor (const auto &link : _links) {\n\t\tif (style::rtlrect(left, top, qMin(w, link.width), st::normalFont->height, _width).contains(point)) {\n\t\t\treturn { parent(), ClickHandlerPtr(link.lnk) };\n\t\t}\n\t\ttop += st::normalFont->height;\n\t}\n\treturn {};\n}\n\nconst style::RoundCheckbox &Link::checkboxStyle() const {\n\treturn st::overviewSmallCheck;\n}\n\nLink::LinkEntry::LinkEntry(const QString &url, const QString &text)\n: text(text)\n, width(st::normalFont->width(text))\n, lnk(UrlClickHandler::IsSuspicious(url)\n\t? std::make_shared<HiddenUrlClickHandler>(url)\n\t: std::make_shared<UrlClickHandler>(url)) {\n}\n\n// Copied from inline_bot_layout_internal.\nGif::Gif(\n\tnot_null<Delegate*> delegate,\n\tnot_null<HistoryItem*> parent,\n\tnot_null<DocumentData*> gif)\n: RadialProgressItem(delegate, parent)\n, _data(gif)\n, _spoiler(parent->isMediaSensitive()\n\t? std::make_unique<Ui::SpoilerAnimation>([=] {\n\t\tdelegate->repaintItem(this);\n\t})\n\t: nullptr)\n, _sensitiveSpoiler(parent->isMediaSensitive() ? 1 : 0) {\n\tsetDocumentLinks(_data, true);\n\tif (_sensitiveSpoiler) {\n\t\t_openl = HistoryView::MakeSensitiveMediaLink(\n\t\t\tstd::make_shared<LambdaClickHandler>(crl::guard(this, [=] {\n\t\t\t\tclearSpoiler();\n\t\t\t\tsetDocumentLinks(_data, true);\n\t\t\t})),\n\t\t\tparent);\n\t}\n\t_data->loadThumbnail(parent->fullId());\n}\n\nGif::~Gif() = default;\n\nint Gif::contentWidth() const {\n\tif (_data->dimensions.width() > 0) {\n\t\treturn _data->dimensions.width();\n\t}\n\treturn style::ConvertScale(_data->thumbnailLocation().width());\n}\n\nint Gif::contentHeight() const {\n\tif (_data->dimensions.height() > 0) {\n\t\treturn _data->dimensions.height();\n\t}\n\treturn style::ConvertScale(_data->thumbnailLocation().height());\n}\n\nvoid Gif::initDimensions() {\n\tint32 w = contentWidth(), h = contentHeight();\n\tif (w <= 0 || h <= 0) {\n\t\t_maxw = 0;\n\t} else {\n\t\tw = w * st::inlineMediaHeight / h;\n\t\t_maxw = qMax(w, int32(st::inlineResultsMinWidth));\n\t}\n\t_minh = st::inlineMediaHeight + st::inlineResultsSkip;\n}\n\nint32 Gif::resizeGetHeight(int32 width) {\n\t_width = width;\n\t_height = _minh;\n\treturn _height;\n}\n\nQSize Gif::countFrameSize() const {\n\tconst auto animating = (_gif && _gif->ready());\n\tauto framew = animating ? _gif->width() : contentWidth();\n\tauto frameh = animating ? _gif->height() : contentHeight();\n\tconst auto height = st::inlineMediaHeight;\n\tconst auto maxSize = st::maxStickerSize;\n\tif (framew * height > frameh * _width) {\n\t\tif (framew < maxSize || frameh > height) {\n\t\t\tif (frameh > height || (framew * height / frameh) <= maxSize) {\n\t\t\t\tframew = framew * height / frameh;\n\t\t\t\tframeh = height;\n\t\t\t} else {\n\t\t\t\tframeh = int32(frameh * maxSize) / framew;\n\t\t\t\tframew = maxSize;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (frameh < maxSize || framew > _width) {\n\t\t\tif (framew > _width || (frameh * _width / framew) <= maxSize) {\n\t\t\t\tframeh = frameh * _width / framew;\n\t\t\t\tframew = _width;\n\t\t\t} else {\n\t\t\t\tframew = int32(framew * maxSize) / frameh;\n\t\t\t\tframeh = maxSize;\n\t\t\t}\n\t\t}\n\t}\n\treturn QSize(framew, frameh);\n}\n\nvoid Gif::clipCallback(Media::Clip::Notification notification) {\n\tusing namespace Media::Clip;\n\tswitch (notification) {\n\tcase Notification::Reinit: {\n\t\tif (_gif) {\n\t\t\tif (_gif->state() == State::Error) {\n\t\t\t\t_gif.setBad();\n\t\t\t} else if (_gif->ready() && !_gif->started()) {\n\t\t\t\tif (_gif->width() * _gif->height() > kMaxInlineArea) {\n\t\t\t\t\t_data->dimensions = QSize(\n\t\t\t\t\t\t_gif->width(),\n\t\t\t\t\t\t_gif->height());\n\t\t\t\t\t_gif.reset();\n\t\t\t\t} else {\n\t\t\t\t\t_gif->start({\n\t\t\t\t\t\t.frame = countFrameSize(),\n\t\t\t\t\t\t.outer = { _width, st::inlineMediaHeight },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else if (_gif->autoPausedGif()\n\t\t\t\t\t&& !delegate()->itemVisible(this)) {\n\t\t\t\tclearHeavyPart();\n\t\t\t}\n\t\t}\n\n\t\tupdate();\n\t} break;\n\n\tcase Notification::Repaint: {\n\t\tif (_gif && !_gif->currentDisplayed()) {\n\t\t\tupdate();\n\t\t}\n\t} break;\n\t}\n}\n\nvoid Gif::clearSpoiler() {\n\tif (_spoiler) {\n\t\t_spoiler = nullptr;\n\t\t_sensitiveSpoiler = false;\n\t\t_thumb = QImage();\n\t\t_thumbGood = false;\n\t\tdelegate()->repaintItem(this);\n\t}\n}\n\nvoid Gif::maybeClearSensitiveSpoiler() {\n\tif (_sensitiveSpoiler) {\n\t\tclearSpoiler();\n\t\tsetDocumentLinks(_data);\n\t}\n}\n\nvoid Gif::validateThumbnail(\n\t\tImage *image,\n\t\tQSize size,\n\t\tQSize frame,\n\t\tbool good) {\n\tif (!image || (_thumbGood && !good)) {\n\t\treturn;\n\t} else if ((_thumb.size() == size * style::DevicePixelRatio())\n\t\t&& (_thumbGood || !good)) {\n\t\treturn;\n\t}\n\t_thumbGood = good;\n\t_thumb = image->pixNoCache(\n\t\tframe * style::DevicePixelRatio(),\n\t\t{\n\t\t\t.options = (good ? Images::Option() : Images::Option::Blur),\n\t\t\t.outer = size,\n\t\t}).toImage();\n}\n\nvoid Gif::prepareThumbnail(QSize size, QSize frame) {\n\tconst auto document = _data;\n\tAssert(document != nullptr);\n\n\tensureDataMediaCreated();\n\tif (!_spoiler) {\n\t\tvalidateThumbnail(_dataMedia->thumbnail(), size, frame, true);\n\t}\n\tvalidateThumbnail(_dataMedia->thumbnailInline(), size, frame, false);\n}\n\nvoid Gif::paint(\n\t\tPainter &p,\n\t\tconst QRect &clip,\n\t\tTextSelection selection,\n\t\tconst PaintContext *context) {\n\tconst auto document = _data;\n\tensureDataMediaCreated();\n\tconst auto preview = Data::VideoPreviewState(_dataMedia.get());\n\tpreview.automaticLoad(getItem()->fullId());\n\n\tconst auto displayLoading = !preview.usingThumbnail()\n\t\t&& document->displayLoading();\n\tconst auto loaded = preview.loaded();\n\tconst auto loading = preview.loading();\n\tif (loaded\n\t\t&& !_gif\n\t\t&& !_gif.isBad()\n\t\t&& CanPlayInline(document)) {\n\t\tauto that = const_cast<Gif*>(this);\n\t\tthat->_gif = preview.makeAnimation([=](\n\t\t\t\tMedia::Clip::Notification notification) {\n\t\t\tthat->clipCallback(notification);\n\t\t});\n\t}\n\n\tconst auto animating = !_spoiler && (_gif && _gif->started());\n\tif (displayLoading) {\n\t\tensureRadial();\n\t\tif (!_radial->animating()) {\n\t\t\t_radial->start(dataProgress());\n\t\t}\n\t}\n\tconst auto radial = isRadialAnimation();\n\n\tconst auto frame = countFrameSize();\n\tconst auto r = QRect(0, 0, _width, st::inlineMediaHeight);\n\tif (animating) {\n\t\tconst auto pixmap = _gif->current({\n\t\t\t.frame = frame,\n\t\t\t.outer = r.size(),\n\t\t}, context->paused ? 0 : context->ms);\n\t\tif (_thumb.isNull()) {\n\t\t\t_thumb = pixmap;\n\t\t\t_thumbGood = true;\n\t\t}\n\t\tp.drawImage(r.topLeft(), pixmap);\n\t} else {\n\t\tprepareThumbnail(r.size(), frame);\n\t\tif (_thumb.isNull()) {\n\t\t\tp.fillRect(r, st::overviewPhotoBg);\n\t\t} else {\n\t\t\tp.drawImage(r.topLeft(), _thumb);\n\t\t}\n\t}\n\n\tif (_spoiler) {\n\t\tconst auto paused = context->paused || On(PowerSaving::kChatSpoiler);\n\t\tUi::FillSpoilerRect(\n\t\t\tp,\n\t\t\tr,\n\t\t\tUi::DefaultImageSpoiler().frame(\n\t\t\t\t_spoiler->index(context->ms, paused)));\n\n\t\tif (_sensitiveSpoiler) {\n\t\t\tPaintSensitiveTag(p, r);\n\t\t}\n\t}\n\n\tconst auto selected = (selection == FullSelection);\n\n\tif (radial\n\t\t|| _gif.isBad()\n\t\t|| (!_gif && !loaded && !loading && !preview.usingThumbnail())) {\n\t\tconst auto radialOpacity = (radial && loaded)\n\t\t\t? _radial->opacity()\n\t\t\t: 1.;\n\t\tp.fillRect(r, st::msgDateImgBg);\n\n\t\tp.setOpacity(radialOpacity);\n\t\tauto icon = [&] {\n\t\t\tif (radial || loading) {\n\t\t\t\treturn &st::historyFileInCancel;\n\t\t\t} else if (loaded) {\n\t\t\t\treturn &st::historyFileInPlay;\n\t\t\t}\n\t\t\treturn &st::historyFileInDownload;\n\t\t}();\n\t\tconst auto size = st::overviewVideoRadialSize;\n\t\tQRect inner(\n\t\t\t(r.width() - size) / 2,\n\t\t\t(r.height() - size) / 2,\n\t\t\tsize,\n\t\t\tsize);\n\t\ticon->paintInCenter(p, inner);\n\t\tif (radial) {\n\t\t\tp.setOpacity(1);\n\t\t\tconst auto margin = st::msgFileRadialLine;\n\t\t\tconst auto rinner = inner\n\t\t\t\t- QMargins(margin, margin, margin, margin);\n\t\t\tauto &bg = selected\n\t\t\t\t? st::historyFileInRadialFgSelected\n\t\t\t\t: st::historyFileInRadialFg;\n\t\t\t_radial->draw(p, rinner, st::msgFileRadialLine, bg);\n\t\t}\n\t}\n\n\tconst auto checkDelta = st::overviewCheckSkip + st::overviewCheck.size;\n\tconst auto checkLeft = _width - checkDelta;\n\tconst auto checkTop = st::overviewCheckSkip;\n\tpaintCheckbox(p, { checkLeft, checkTop }, selected, context);\n}\n\nvoid Gif::update() {\n\tdelegate()->repaintItem(this);\n}\n\nvoid Gif::ensureDataMediaCreated() const {\n\tif (_dataMedia) {\n\t\treturn;\n\t}\n\t_dataMedia = _data->createMediaView();\n\t_dataMedia->goodThumbnailWanted();\n\t_dataMedia->thumbnailWanted(parent()->fullId());\n\tdelegate()->registerHeavyItem(this);\n}\n\nvoid Gif::clearHeavyPart() {\n\t_gif.reset();\n\t_dataMedia = nullptr;\n}\n\nvoid Gif::setPosition(int32 position) {\n\tAbstractLayoutItem::setPosition(position);\n\tif (position < 0) {\n\t\t_gif.reset();\n\t}\n}\n\nfloat64 Gif::dataProgress() const {\n\tensureDataMediaCreated();\n\treturn _dataMedia->progress();\n}\n\nbool Gif::dataFinished() const {\n\treturn !_data->loading();\n}\n\nbool Gif::dataLoaded() const {\n\tensureDataMediaCreated();\n\tconst auto preview = Data::VideoPreviewState(_dataMedia.get());\n\treturn preview.loaded();\n}\n\nbool Gif::iconAnimated() const {\n\treturn true;\n}\n\nTextState Gif::getState(\n\t\tQPoint point,\n\t\tStateRequest request) const {\n\tif (hasPoint(point)) {\n\t\tconst auto link = (_data->loading() || _data->uploading())\n\t\t\t? _cancell\n\t\t\t: dataLoaded()\n\t\t\t? _openl\n\t\t\t: _savel;\n\t\treturn { parent(), link };\n\t}\n\treturn {};\n}\n\nvoid Gif::updateStatusText() {\n\tauto statusSize = int64();\n\tif (_data->status == FileDownloadFailed || _data->status == FileUploadFailed) {\n\t\tstatusSize = Ui::FileStatusSizeFailed;\n\t} else if (_data->uploading()) {\n\t\tstatusSize = _data->uploadingData->offset;\n\t} else if (dataLoaded()) {\n\t\tstatusSize = Ui::FileStatusSizeLoaded;\n\t} else {\n\t\tstatusSize = Ui::FileStatusSizeReady;\n\t}\n\tif (statusSize != _status.size()) {\n\t\tauto status = statusSize;\n\t\tauto size = _data->size;\n\t\tif (statusSize >= 0 && statusSize < 0xFF000000LL) {\n\t\t\tsize = status;\n\t\t\tstatus = Ui::FileStatusSizeReady;\n\t\t}\n\t\t_status.update(status, size, -1, 0);\n\t\t_status.setSize(statusSize);\n\t}\n}\n\n} // namespace Overview::Layout\n"
  },
  {
    "path": "Telegram/SourceFiles/overview/overview_layout.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"layout/layout_item_base.h\"\n#include \"layout/layout_document_generic_preview.h\"\n#include \"media/clip/media_clip_reader.h\"\n#include \"core/click_handler_types.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/radial_animation.h\"\n\nclass Image;\n\nnamespace style {\nstruct RoundCheckbox;\nstruct OverviewFileLayout;\n} // namespace style\n\nnamespace Data {\nclass Media;\nclass PhotoMedia;\nclass DocumentMedia;\n} // namespace Data\n\nnamespace Ui {\nclass SpoilerAnimation;\n} // namespace Ui\n\nnamespace Overview::Layout {\n\nclass Checkbox;\nclass ItemBase;\nclass Delegate;\n\nclass PaintContext : public PaintContextBase {\npublic:\n\tPaintContext(crl::time ms, bool selecting, bool paused)\n\t: PaintContextBase(ms, selecting)\n\t, paused(paused) {\n\t}\n\tbool skipBorder = false;\n\tbool paused = false;\n\n};\n\nclass ItemBase : public LayoutItemBase, public base::has_weak_ptr {\npublic:\n\tItemBase(not_null<Delegate*> delegate, not_null<HistoryItem*> parent);\n\t~ItemBase();\n\n\tvirtual void paint(\n\t\tPainter &p,\n\t\tconst QRect &clip,\n\t\tTextSelection selection,\n\t\tconst PaintContext *context) = 0;\n\n\t[[nodiscard]] QDateTime dateTime() const;\n\n\t[[nodiscard]] not_null<HistoryItem*> getItem() const {\n\t\treturn _parent;\n\t}\n\n\tvoid clickHandlerActiveChanged(const ClickHandlerPtr &action, bool active) override;\n\tvoid clickHandlerPressedChanged(const ClickHandlerPtr &action, bool pressed) override;\n\n\tvoid invalidateCache();\n\n\tvirtual void itemDataChanged() {\n\t}\n\tvirtual void clearHeavyPart() {\n\t}\n\n\tvirtual void maybeClearSensitiveSpoiler() {\n\t}\n\nprotected:\n\t[[nodiscard]] not_null<HistoryItem*> parent() const {\n\t\treturn _parent;\n\t}\n\t[[nodiscard]] not_null<Delegate*> delegate() const {\n\t\treturn _delegate;\n\t}\n\tvoid paintCheckbox(\n\t\tPainter &p,\n\t\tQPoint position,\n\t\tbool selected,\n\t\tconst PaintContext *context);\n\t[[nodiscard]] virtual const style::RoundCheckbox &checkboxStyle() const;\n\nprivate:\n\tvoid ensureCheckboxCreated();\n\n\tconst not_null<Delegate*> _delegate;\n\tconst not_null<HistoryItem*> _parent;\n\tconst QDateTime _dateTime;\n\tstd::unique_ptr<Checkbox> _check;\n\n};\n\nclass RadialProgressItem : public ItemBase {\npublic:\n\tRadialProgressItem(\n\t\tnot_null<Delegate*> delegate,\n\t\tnot_null<HistoryItem*> parent)\n\t: ItemBase(delegate, parent) {\n\t}\n\tRadialProgressItem(const RadialProgressItem &other) = delete;\n\n\tvoid clickHandlerActiveChanged(const ClickHandlerPtr &action, bool active) override;\n\n\tvirtual void clearSpoiler() {\n\t}\n\n\t~RadialProgressItem();\n\nprotected:\n\tClickHandlerPtr _openl, _savel, _cancell;\n\tvoid setLinks(\n\t\tClickHandlerPtr &&openl,\n\t\tClickHandlerPtr &&savel,\n\t\tClickHandlerPtr &&cancell);\n\tvoid setDocumentLinks(\n\t\tnot_null<DocumentData*> document,\n\t\tbool forceOpen = false);\n\n\tvoid radialAnimationCallback(crl::time now) const;\n\n\tvoid ensureRadial();\n\tvoid checkRadialFinished() const;\n\n\tbool isRadialAnimation() const {\n\t\tif (_radial) {\n\t\t\tif (_radial->animating()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tcheckRadialFinished();\n\t\t}\n\t\treturn false;\n\t}\n\n\tvirtual float64 dataProgress() const = 0;\n\tvirtual bool dataFinished() const = 0;\n\tvirtual bool dataLoaded() const = 0;\n\tvirtual bool iconAnimated() const {\n\t\treturn false;\n\t}\n\n\tmutable std::unique_ptr<Ui::RadialAnimation> _radial;\n\tUi::Animations::Simple _a_iconOver;\n\n};\n\nclass StatusText {\npublic:\n\t// duration = -1 - no duration, duration = -2 - \"GIF\" duration\n\tvoid update(\n\t\tint64 newSize,\n\t\tint64 fullSize,\n\t\tTimeId duration,\n\t\tTimeId realDuration);\n\tvoid setSize(int64 newSize);\n\n\t[[nodiscard]] int64 size() const {\n\t\treturn _size;\n\t}\n\t[[nodiscard]] QString text() const {\n\t\treturn _text;\n\t}\n\nprivate:\n\t// >= 0 will contain download / upload string, _size = loaded bytes\n\t// < 0 will contain played string, _size = -(seconds + 1) played\n\t// 0xFFFFFFF0LL will contain status for not yet downloaded file\n\t// 0xFFFFFFF1LL will contain status for already downloaded file\n\t// 0xFFFFFFF2LL will contain status for failed to download / upload file\n\tint64 _size = 0;\n\tQString _text;\n\n};\n\nstruct Info : RuntimeComponent<Info, LayoutItemBase> {\n\tint top = 0;\n};\n\nstruct MediaOptions {\n\tbool spoiler = false;\n\tbool story = false;\n\tbool storyPinned = false;\n\tbool storyShowPinned = false;\n\tbool storyHidden = false;\n\tbool storyShowHidden = false;\n};\n\nclass Photo final : public ItemBase {\npublic:\n\tPhoto(\n\t\tnot_null<Delegate*> delegate,\n\t\tnot_null<HistoryItem*> parent,\n\t\tnot_null<PhotoData*> photo,\n\t\tMediaOptions options);\n\t~Photo();\n\n\tvoid initDimensions() override;\n\tint32 resizeGetHeight(int32 width) override;\n\tvoid paint(Painter &p, const QRect &clip, TextSelection selection, const PaintContext *context) override;\n\tTextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\tvoid itemDataChanged() override;\n\tvoid clearHeavyPart() override;\n\n\tvoid maybeClearSensitiveSpoiler() override;\n\nprivate:\n\tvoid ensureDataMediaCreated() const;\n\tvoid setPixFrom(not_null<Image*> image);\n\t[[nodiscard]] ClickHandlerPtr makeOpenPhotoHandler();\n\tvoid clearSpoiler();\n\n\tconst not_null<PhotoData*> _data;\n\tmutable std::shared_ptr<Data::PhotoMedia> _dataMedia;\n\tstd::unique_ptr<Ui::SpoilerAnimation> _spoiler;\n\n\tQImage _pix;\n\tQImage _hiddenBgCache;\n\tbool _goodLoaded : 1 = false;\n\tbool _sensitiveSpoiler : 1 = false;\n\tbool _story : 1 = false;\n\tbool _storyPinned : 1 = false;\n\tbool _storyShowPinned : 1 = false;\n\tbool _storyHidden : 1 = false;\n\tbool _storyShowHidden : 1 = false;\n\n\tClickHandlerPtr _link;\n\n};\n\nclass Gif final : public RadialProgressItem {\npublic:\n\tGif(\n\t\tnot_null<Delegate*> delegate,\n\t\tnot_null<HistoryItem*> parent,\n\t\tnot_null<DocumentData*> gif);\n\t~Gif();\n\n\tvoid initDimensions() override;\n\tint32 resizeGetHeight(int32 width) override;\n\tvoid paint(\n\t\tPainter &p,\n\t\tconst QRect &clip,\n\t\tTextSelection selection,\n\t\tconst PaintContext *context) override;\n\tTextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\tvoid clearHeavyPart() override;\n\tvoid setPosition(int32 position) override;\n\n\tvoid clearSpoiler() override;\n\tvoid maybeClearSensitiveSpoiler() override;\n\nprotected:\n\tfloat64 dataProgress() const override;\n\tbool dataFinished() const override;\n\tbool dataLoaded() const override;\n\tbool iconAnimated() const override;\n\nprivate:\n\tQSize countFrameSize() const;\n\tint contentWidth() const;\n\tint contentHeight() const;\n\n\tvoid validateThumbnail(\n\t\tImage *image,\n\t\tQSize size,\n\t\tQSize frame,\n\t\tbool good);\n\tvoid prepareThumbnail(QSize size, QSize frame);\n\n\tvoid update();\n\n\tvoid ensureDataMediaCreated() const;\n\tvoid updateStatusText();\n\n\tvoid clipCallback(Media::Clip::Notification notification);\n\n\tMedia::Clip::ReaderPointer _gif;\n\n\tconst not_null<DocumentData*> _data;\n\tmutable std::shared_ptr<Data::DocumentMedia> _dataMedia;\n\tStatusText _status;\n\tstd::unique_ptr<Ui::SpoilerAnimation> _spoiler;\n\n\tQImage _thumb;\n\tbool _thumbGood = false;\n\tbool _sensitiveSpoiler = false;\n\n};\n\nclass Video final : public RadialProgressItem {\npublic:\n\tVideo(\n\t\tnot_null<Delegate*> delegate,\n\t\tnot_null<HistoryItem*> parent,\n\t\tnot_null<DocumentData*> video,\n\t\tMediaOptions options);\n\t~Video();\n\n\tvoid initDimensions() override;\n\tint32 resizeGetHeight(int32 width) override;\n\tvoid paint(Painter &p, const QRect &clip, TextSelection selection, const PaintContext *context) override;\n\tTextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\tvoid itemDataChanged() override;\n\tvoid clearHeavyPart() override;\n\tvoid clearSpoiler() override;\n\n\tvoid maybeClearSensitiveSpoiler() override;\n\nprotected:\n\tfloat64 dataProgress() const override;\n\tbool dataFinished() const override;\n\tbool dataLoaded() const override;\n\tbool iconAnimated() const override;\n\nprivate:\n\tvoid ensureDataMediaCreated() const;\n\tvoid updateStatusText();\n\n\tconst not_null<DocumentData*> _data;\n\tPhotoData *_videoCover = nullptr;\n\tmutable std::shared_ptr<Data::DocumentMedia> _dataMedia;\n\tmutable std::shared_ptr<Data::PhotoMedia> _videoCoverMedia;\n\tStatusText _status;\n\n\tQString _duration;\n\tstd::unique_ptr<Ui::SpoilerAnimation> _spoiler;\n\n\tQImage _pix;\n\tQImage _hiddenBgCache;\n\tbool _pixBlurred : 1 = true;\n\tbool _sensitiveSpoiler : 1 = false;\n\tbool _story : 1 = false;\n\tbool _storyPinned : 1 = false;\n\tbool _storyShowPinned : 1 = false;\n\tbool _storyHidden : 1 = false;\n\tbool _storyShowHidden : 1 = false;\n\n};\n\nclass Voice final : public RadialProgressItem {\npublic:\n\tVoice(\n\t\tnot_null<Delegate*> delegate,\n\t\tnot_null<HistoryItem*> parent,\n\t\tnot_null<DocumentData*> voice,\n\t\tconst style::OverviewFileLayout &st);\n\n\tvoid initDimensions() override;\n\tvoid paint(Painter &p, const QRect &clip, TextSelection selection, const PaintContext *context) override;\n\tTextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\tvoid clearHeavyPart() override;\n\nprotected:\n\tfloat64 dataProgress() const override;\n\tbool dataFinished() const override;\n\tbool dataLoaded() const override;\n\tbool iconAnimated() const override;\n\tconst style::RoundCheckbox &checkboxStyle() const override;\n\nprivate:\n\tvoid ensureDataMediaCreated() const;\n\n\tconst not_null<DocumentData*> _data;\n\tmutable std::shared_ptr<Data::DocumentMedia> _dataMedia;\n\tStatusText _status;\n\tClickHandlerPtr _namel;\n\n\tconst style::OverviewFileLayout &_st;\n\n\tUi::Text::String _name;\n\tUi::Text::String _details;\n\tUi::Text::String _caption;\n\tint _nameVersion = 0;\n\n\tvoid updateName();\n\tbool updateStatusText();\n\n};\n\nstruct DocumentFields {\n\tnot_null<DocumentData*> document;\n\tTimeId dateOverride = 0;\n\tbool forceFileLayout = false;\n};\n\nclass Document final : public RadialProgressItem {\npublic:\n\tDocument(\n\t\tnot_null<Delegate*> delegate,\n\t\tnot_null<HistoryItem*> parent,\n\t\tDocumentFields fields,\n\t\tconst style::OverviewFileLayout &st);\n\n\tvoid initDimensions() override;\n\tvoid paint(Painter &p, const QRect &clip, TextSelection selection, const PaintContext *context) override;\n\tTextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\tvoid clearHeavyPart() override;\n\nprotected:\n\tfloat64 dataProgress() const override;\n\tbool dataFinished() const override;\n\tbool dataLoaded() const override;\n\tbool iconAnimated() const override;\n\tconst style::RoundCheckbox &checkboxStyle() const override;\n\nprivate:\n\t[[nodiscard]] bool downloadInCorner() const;\n\tvoid drawCornerDownload(QPainter &p, bool selected, const PaintContext *context) const;\n\t[[nodiscard]] TextState cornerDownloadTextState(\n\t\tQPoint point,\n\t\tStateRequest request) const;\n\n\t[[nodiscard]] bool songLayout() const;\n\tvoid ensureDataMediaCreated() const;\n\n\tnot_null<DocumentData*> _data;\n\tmutable std::shared_ptr<Data::DocumentMedia> _dataMedia;\n\tStatusText _status;\n\tClickHandlerPtr _msgl, _namel;\n\n\tconst style::OverviewFileLayout &_st;\n\tconst ::Layout::DocumentGenericPreview _generic;\n\n\tbool _thumbLoaded = false;\n\tbool _forceFileLayout = false;\n\tQPixmap _thumb;\n\n\tUi::Text::String _name;\n\tQString _date, _ext;\n\tint _datew = 0;\n\tint _extw = 0;\n\tint _thumbw = 0;\n\n\tbool withThumb() const;\n\tbool updateStatusText();\n\n};\n\nclass Link final : public ItemBase {\npublic:\n\tLink(\n\t\tnot_null<Delegate*> delegate,\n\t\tnot_null<HistoryItem*> parent,\n\t\tData::Media *media);\n\n\tvoid initDimensions() override;\n\tint32 resizeGetHeight(int32 width) override;\n\tvoid paint(Painter &p, const QRect &clip, TextSelection selection, const PaintContext *context) override;\n\tTextState getState(\n\t\tQPoint point,\n\t\tStateRequest request) const override;\n\n\tvoid clearHeavyPart() override;\n\nprotected:\n\tconst style::RoundCheckbox &checkboxStyle() const override;\n\nprivate:\n\tvoid ensurePhotoMediaCreated();\n\tvoid ensureDocumentMediaCreated();\n\tvoid validateThumbnail();\n\n\tClickHandlerPtr _photol;\n\n\tQString _title, _letter;\n\tint _titlew = 0;\n\tWebPageData *_page = nullptr;\n\tstd::shared_ptr<Data::PhotoMedia> _photoMedia;\n\tstd::shared_ptr<Data::DocumentMedia> _documentMedia;\n\tint _pixw = 0;\n\tint _pixh = 0;\n\tUi::Text::String _text;\n\tQPixmap _thumbnail;\n\tbool _thumbnailBlurred = true;\n\n\tstruct LinkEntry {\n\t\tLinkEntry() = default;\n\t\tLinkEntry(const QString &url, const QString &text);\n\n\t\tQString text;\n\t\tint width = 0;\n\t\tstd::shared_ptr<TextClickHandler> lnk;\n\t};\n\tQVector<LinkEntry> _links;\n\n};\n\n} // namespace Overview::Layout\n"
  },
  {
    "path": "Telegram/SourceFiles/overview/overview_layout_delegate.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass StickerPremiumMark;\n\nnamespace Overview::Layout {\n\nclass ItemBase;\n\nclass Delegate {\npublic:\n\tvirtual void registerHeavyItem(not_null<const ItemBase*> item) = 0;\n\tvirtual void unregisterHeavyItem(not_null<const ItemBase*> item) = 0;\n\tvirtual void repaintItem(not_null<const ItemBase*> item) = 0;\n\tvirtual bool itemVisible(not_null<const ItemBase*> item) = 0;\n\n\t[[nodiscard]] virtual not_null<StickerPremiumMark*> hiddenMark() = 0;\n\n\tvirtual void openPhoto(not_null<PhotoData*> photo, FullMsgId id) = 0;\n\tvirtual void openDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId id,\n\t\tbool showInMediaView = false) = 0;\n\n};\n\n} // namespace Overview::Layout\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\n\nusing \"ui/widgets/widgets.style\";\nusing \"boxes/boxes.style\";\nusing \"info/info.style\";\nusing \"chat_helpers/chat_helpers.style\";\n\npassportPasswordPadding: margins(20px, 30px, 20px, 40px);\npassportPasswordLabel: FlatLabel(boxLabel) {\n\tminWidth: 275px;\n\talign: align(top);\n}\npassportPasswordLabelBold: FlatLabel(passportPasswordLabel) {\n\tstyle: TextStyle(boxLabelStyle) {\n\t\tfont: font(boxFontSize semibold);\n\t}\n}\npassportPasswordSetupLabel: FlatLabel(passportPasswordLabel) {\n\tminWidth: 0px;\n}\npassportPasswordHintLabel: passportPasswordLabel;\npassportErrorLabel: FlatLabel(passportPasswordLabel) {\n\ttextFg: boxTextFgError;\n}\npassportVerifyErrorLabel: FlatLabel(passportErrorLabel) {\n\tminWidth: 128px;\n\talign: align(topleft);\n}\n\npassportPanelSize: size(392px, 600px);\n\npassportPasswordFieldBottom: 306px;\npassportPasswordFieldSkip: 29px;\npassportPasswordHintSkip: 10px;\npassportPasswordUserpicSkip: 14px;\npassportPasswordUserpic: UserpicButton(defaultUserpicButton) {\n\tsize: size(80px, 80px);\n\tphotoSize: 80px;\n\tphotoPosition: point(0px, 0px);\n}\npassportPasswordSubmit: RoundButton(defaultActiveButton) {\n\twidth: 200px;\n\theight: 44px;\n\ttextTop: 12px;\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(semibold 15px);\n\t}\n}\npassportPasswordSubmitBottom: 72px;\npassportPasswordForgotBottom: 36px;\n\npassportPanelScroll: ScrollArea(defaultScrollArea) {\n\tdeltat: 6px;\n\tdeltab: 6px;\n}\n\npassportPanelAuthorize: RoundButton(passportPasswordSubmit) {\n\twidth: 0px;\n\theight: 49px;\n\tpadding: margins(0px, -3px, 0px, 0px);\n\ttextTop: 16px;\n\ticon: icon {{ \"passport_authorize\", activeButtonFg }};\n\ticonPosition: point(-8px, 9px);\n}\npassportPanelSaveValue: RoundButton(passportPanelAuthorize) {\n\ttextFg: windowActiveTextFg;\n\ttextFgOver: windowActiveTextFg;\n\ttextBg: windowBg;\n\ttextBgOver: windowBgOver;\n\tripple: defaultRippleAnimation;\n\ticon: icon {};\n}\npassportFormAbout1Padding: margins(10px, 4px, 10px, 0px);\npassportFormAbout2Padding: margins(10px, 0px, 10px, 22px);\npassportFormHeader: FlatLabel(boxLabel) {\n\ttextFg: windowActiveTextFg;\n\tstyle: semiboldTextStyle;\n}\npassportFormHeaderPadding: margins(22px, 20px, 22px, 9px);\npassportFormUserpic: UserpicButton(passportPasswordUserpic) {\n\tsize: size(60px, 60px);\n\tphotoSize: 60px;\n}\npassportFormUserpicPadding: margins(0px, 5px, 0px, 10px);\npassportFormDividerHeight: 13px;\npassportFormLabelPadding: margins(22px, 7px, 22px, 14px);\npassportFormPolicy: FlatLabel(boxDividerLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tlinkUnderline: kLinkUnderlineAlways;\n\t}\n\tpalette: TextPalette(defaultTextPalette) {\n\t\tlinkFg: windowSubTextFg;\n\t}\n}\npassportFormPolicyPadding: margins(22px, 7px, 22px, 28px);\npassportContactNewFieldPadding: margins(22px, 0px, 22px, 14px);\npassportContactFieldPadding: margins(22px, 14px, 22px, 14px);\npassportContactErrorPadding: margins(22px, 0px, 22px, 0px);\npassportContactErrorMargin: margins(0px, 0px, 0px, 14px);\n\npassportRowPadding: margins(22px, 8px, 25px, 8px);\npassportRowIconSkip: 10px;\npassportRowSkip: 2px;\npassportRowRipple: defaultRippleAnimationBgOver;\npassportRowReadyIcon: icon {{ \"passport_ready\", windowActiveTextFg }};\npassportRowEmptyIcon: icon {{ \"passport_empty\", menuIconFgOver }};\npassportRowTitleFg: windowFg;\npassportRowDescriptionFg: windowSubTextFg;\n\npassportUploadButton: SettingsButton(defaultSettingsButton) {\n\ttextFg: windowActiveTextFg;\n\ttextFgOver: windowActiveTextFg;\n\ttextBg: windowBg;\n\ttextBgOver: windowBgOver;\n\n\tstyle: semiboldTextStyle;\n\n\theight: 18px;\n\tpadding: margins(22px, 14px, 22px, 12px);\n\n\tripple: defaultRippleAnimation;\n}\npassportUploadButtonPadding: margins(0px, 10px, 0px, 10px);\npassportUploadHeaderPadding: margins(22px, 14px, 22px, 3px);\npassportUploadErrorPadding: margins(22px, 5px, 22px, 5px);\npassportValueErrorPadding: passportUploadHeaderPadding;\npassportDeleteButton: SettingsButton(passportUploadButton) {\n\ttextFg: attentionButtonFg;\n\ttextFgOver: attentionButtonFgOver;\n}\n\npassportScanNameStyle: TextStyle(defaultTextStyle) {\n\tfont: font(boxFontSize semibold);\n}\npassportScanRow: PassportScanRow {\n\tpadding: margins(22px, 10px, 10px, 10px);\n\tsize: 40px;\n\ttextLeft: 53px;\n\tnameTop: 1px;\n\tstatusTop: 22px;\n\tborder: 1px;\n\tborderFg: inputBorderFg;\n\tremove: stickersRemove;\n\trestore: stickersUndoRemove;\n}\npassportScanDeletedOpacity: stickersRowDisabledOpacity;\n\npassportDetailsHeaderPadding: margins(22px, 20px, 33px, 10px);\npassportDetailsPadding: margins(22px, 10px, 28px, 10px);\npassportDetailsField: InputField(defaultInputField) {\n\ttextMargins: margins(2px, 8px, 2px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n\theightMin: 32px;\n\tstyle: defaultTextStyle;\n}\npassportDetailsDateField: InputField(passportDetailsField) {\n\ttextMargins: margins(2px, 8px, 2px, 0px);\n\tborder: 0px;\n\tborderActive: 0px;\n\theightMin: 30px;\n\tplaceholderFont: semiboldFont;\n\tplaceholderFgActive: placeholderFgActive;\n}\npassportDetailsSeparator: FlatLabel(passportPasswordLabelBold) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(semibold 14px);\n\t}\n\ttextFg: windowSubTextFg;\n\talign: align(topleft);\n}\npassportDetailsSeparatorPadding: margins(5px, 8px, 5px, 0px);\npassportContactField: InputField(defaultInputField) {\n\tstyle: defaultTextStyle;\n}\npassportDetailsFieldLeft: 116px;\npassportDetailsFieldTop: 2px;\npassportDetailsFieldSkipMin: 12px;\npassportDetailsSkip: 30px;\npassportDetailsGenderSkip: 20px;\n\npassportRequestTypeSkip: 16px;\n\npassportPasswordAbout1Padding: margins(10px, 28px, 10px, 0px);\npassportPasswordAbout2Padding: margins(10px, 0px, 10px, 28px);\npassportPasswordIconHeight: 224px;\npassportPasswordIcon: icon {{ \"passport_password_setup\", windowSubTextFg }};\n\npassportNativeNameAboutMargin: margins(0px, 16px, 0px, 0px);\npassportNativeNameHeaderPadding: margins(22px, 28px, 33px, 10px);\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_edit_identity_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"passport/passport_edit_identity_box.h\"\n\n#include \"passport/passport_panel_controller.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/text_options.h\"\n#include \"lang/lang_keys.h\"\n#include \"core/file_utilities.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_passport.h\"\n\nnamespace Passport {\n\nclass ScanButton : public Ui::RippleButton {\npublic:\n\tScanButton(\n\t\tQWidget *parent,\n\t\tconst QString &title,\n\t\tconst QString &description);\n\n\tvoid setImage(const QImage &image);\n\tvoid setDescription(const QString &description);\n\n\trpl::producer<> deleteClicks() const {\n\t\treturn _delete->clicks();\n\t}\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tint countAvailableWidth() const;\n\tint countAvailableWidth(int newWidth) const;\n\n\tText _title;\n\tText _description;\n\tint _titleHeight = 0;\n\tint _descriptionHeight = 0;\n\tQImage _image;\n\tobject_ptr<Ui::IconButton> _delete = { nullptr };\n\n};\n\nScanButton::ScanButton(\n\tQWidget *parent,\n\tconst QString &title,\n\tconst QString &description)\n: RippleButton(parent, st::passportRowRipple)\n, _title(\n\tst::semiboldTextStyle,\n\ttitle,\n\tUi::NameTextOptions())\n, _description(\n\tst::defaultTextStyle,\n\tdescription,\n\tUi::NameTextOptions())\n, _delete(this, st::passportScanDelete) {\n}\n\nvoid ScanButton::setImage(const QImage &image) {\n\t_image = image;\n\tupdate();\n}\n\nvoid ScanButton::setDescription(const QString &description) {\n\t_description.setText(\n\t\tst::defaultTextStyle,\n\t\tdescription,\n\t\tUi::NameTextOptions());\n\tupdate();\n}\n\nint ScanButton::resizeGetHeight(int newWidth) {\n\tconst auto availableWidth = countAvailableWidth(newWidth);\n\t_titleHeight = st::semiboldFont->height;\n\t_descriptionHeight = st::normalFont->height;\n\tconst auto result = st::passportRowPadding.top()\n\t\t+ _titleHeight\n\t\t+ st::passportRowSkip\n\t\t+ _descriptionHeight\n\t\t+ st::passportRowPadding.bottom();\n\tconst auto right = st::passportRowPadding.right();\n\t_delete->moveToRight(\n\t\tright,\n\t\t(result - _delete->height()) / 2,\n\t\tnewWidth);\n\treturn result;\n}\n\nint ScanButton::countAvailableWidth(int newWidth) const {\n\treturn newWidth\n\t\t- st::passportRowPadding.left()\n\t\t- st::passportRowPadding.right()\n\t\t- _delete->width();\n}\n\nint ScanButton::countAvailableWidth() const {\n\treturn countAvailableWidth(width());\n}\n\nvoid ScanButton::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tconst auto ms = getms();\n\tpaintRipple(p, 0, 0, ms);\n\n\tauto left = st::passportRowPadding.left();\n\tauto availableWidth = countAvailableWidth();\n\tauto top = st::passportRowPadding.top();\n\tconst auto size = height() - top - st::passportRowPadding.bottom();\n\tif (_image.isNull()) {\n\t\tp.fillRect(left, top, size, size, Qt::black);\n\t} else {\n\t\tPainterHighQualityEnabler hq(p);\n\t\tif (_image.width() > _image.height()) {\n\t\t\tauto newheight = size * _image.height() / _image.width();\n\t\t\tp.drawImage(QRect(left, top + (size - newheight) / 2, size, newheight), _image);\n\t\t} else {\n\t\t\tauto newwidth = size * _image.width() / _image.height();\n\t\t\tp.drawImage(QRect(left + (size - newwidth) / 2, top, newwidth, size), _image);\n\t\t}\n\t}\n\tleft += size + st::passportRowPadding.left();\n\tavailableWidth -= size + st::passportRowPadding.left();\n\n\t_title.drawLeftElided(p, left, top, availableWidth, width());\n\ttop += _titleHeight + st::passportRowSkip;\n\n\t_description.drawLeftElided(p, left, top, availableWidth, width());\n\ttop += _descriptionHeight + st::passportRowPadding.bottom();\n}\n\nIdentityBox::IdentityBox(\n\tQWidget*,\n\tnot_null<PanelController*> controller,\n\tint valueIndex,\n\tconst IdentityData &data,\n\tstd::vector<ScanInfo> &&files)\n: _controller(controller)\n, _valueIndex(valueIndex)\n, _files(std::move(files))\n, _uploadScan(this, \"Upload scans\") // #TODO langs\n, _name(\n\tthis,\n\tst::defaultInputField,\n\tlangFactory(lng_signup_firstname),\n\tdata.name)\n, _surname(\n\tthis,\n\tst::defaultInputField,\n\tlangFactory(lng_signup_lastname),\n\tdata.surname) {\n}\n\nvoid IdentityBox::prepare() {\n\tsetTitle(langFactory(lng_passport_identity_title));\n\n\tauto index = 0;\n\tfor (const auto &scan : _files) {\n\t\t_scans.push_back(object_ptr<ScanButton>(\n\t\t\tthis,\n\t\t\tQString(\"Scan %1\").arg(++index), // #TODO langs\n\t\t\tscan.status));\n\t\t_scans.back()->setImage(scan.thumb);\n\t\t_scans.back()->resizeToWidth(st::boxWideWidth);\n\t\t_scans.back()->deleteClicks(\n\t\t) | rpl::on_next([=] {\n\t\t\t_controller->deleteScan(_valueIndex, index - 1);\n\t\t}, lifetime());\n\t}\n\n\taddButton(langFactory(lng_settings_save), [=] {\n\t\tsave();\n\t});\n\taddButton(langFactory(lng_cancel), [=] {\n\t\tcloseBox();\n\t});\n\t_controller->scanUpdated(\n\t) | rpl::on_next([=](ScanInfo &&info) {\n\t\tupdateScan(std::move(info));\n\t}, lifetime());\n\n\t_uploadScan->addClickHandler([=] {\n\t\tchooseScan();\n\t});\n\tsetDimensions(st::boxWideWidth, countHeight());\n}\n\nint IdentityBox::countHeight() const {\n\tauto height = st::contactPadding.top();\n\tfor (const auto &scan : _scans) {\n\t\theight += scan->height();\n\t}\n\theight += st::contactPadding.top()\n\t\t+ _uploadScan->height()\n\t\t+ st::contactSkip\n\t\t+ _name->height()\n\t\t+ st::contactSkip\n\t\t+ _surname->height()\n\t\t+ st::contactPadding.bottom()\n\t\t+ st::boxPadding.bottom();\n\treturn height;\n}\n\nvoid IdentityBox::updateScan(ScanInfo &&info) {\n\tconst auto i = ranges::find(_files, info.key, [](const ScanInfo &file) {\n\t\treturn file.key;\n\t});\n\tif (i != _files.end()) {\n\t\t*i = info;\n\t\t_scans[i - _files.begin()]->setDescription(i->status);\n\t\t_scans[i - _files.begin()]->setImage(i->thumb);\n\t} else {\n\t\t_files.push_back(std::move(info));\n\t\t_scans.push_back(object_ptr<ScanButton>(\n\t\t\tthis,\n\t\t\tQString(\"Scan %1\").arg(_files.size()),\n\t\t\t_files.back().status));\n\t\t_scans.back()->setImage(_files.back().thumb);\n\t\t_scans.back()->resizeToWidth(st::boxWideWidth);\n\t\t_scans.back()->show();\n\t\tupdateControlsPosition();\n\t\tsetDimensions(st::boxWideWidth, countHeight());\n\t}\n\tupdate();\n}\n\nvoid IdentityBox::setInnerFocus() {\n\t_name->setFocusFast();\n}\n\nvoid IdentityBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\t_name->resize((width()\n\t\t\t- st::boxPadding.left()\n\t\t\t- st::boxPadding.right()),\n\t\t_name->height());\n\t_surname->resize(_name->width(), _surname->height());\n\n\tupdateControlsPosition();\n}\n\nvoid IdentityBox::updateControlsPosition() {\n\tauto top = st::contactPadding.top();\n\tfor (const auto &scan : _scans) {\n\t\tscan->moveToLeft(0, top);\n\t\ttop += scan->height();\n\t}\n\ttop += st::contactPadding.top();\n\t_uploadScan->moveToLeft(st::boxPadding.left(), top);\n\ttop += _uploadScan->height() + st::contactSkip;\n\t_name->moveToLeft(st::boxPadding.left(), top);\n\ttop += _name->height() + st::contactSkip;\n\t_surname->moveToLeft(st::boxPadding.left(), top);\n}\n\nvoid IdentityBox::chooseScan() {\n\tconst auto filter = FileDialog::AllFilesFilter()\n\t\t+ u\";;Image files (*\"_q\n\t\t+ cImgExtensions().join(u\" *\"_q)\n\t\t+ u\")\"_q;\n\tconst auto callback = [=](FileDialog::OpenResult &&result) {\n\t\tif (result.paths.size() == 1) {\n\t\t\tencryptScan(result.paths.front());\n\t\t} else if (!result.remoteContent.isEmpty()) {\n\t\t\tencryptScanContent(std::move(result.remoteContent));\n\t\t}\n\t};\n\tFileDialog::GetOpenPath(\n\t\t\"Choose scan image\",\n\t\tfilter,\n\t\tbase::lambda_guarded(this, callback));\n}\n\nvoid IdentityBox::encryptScan(const QString &path) {\n\tencryptScanContent([&] {\n\t\tQFile f(path);\n\t\tif (!f.open(QIODevice::ReadOnly)) {\n\t\t\treturn QByteArray();\n\t\t}\n\t\treturn f.readAll();\n\t}());\n}\n\nvoid IdentityBox::encryptScanContent(QByteArray &&content) {\n\t_controller->uploadScan(_valueIndex, std::move(content));\n}\n\nvoid IdentityBox::save() {\n\tauto data = IdentityData();\n\tdata.name = _name->getLastText();\n\tdata.surname = _surname->getLastText();\n\t_controller->saveValueIdentity(_valueIndex, data);\n}\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_edit_identity_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n\nnamespace Ui {\nclass LinkButton;\nclass InputField;\n} // namespace Ui\n\nnamespace Passport {\n\nclass PanelController;\nstruct ScanInfo;\nclass ScanButton;\n\nstruct IdentityData {\n\tQString name;\n\tQString surname;\n};\n\nclass IdentityBox : public BoxContent {\npublic:\n\tIdentityBox(\n\t\tQWidget*,\n\t\tnot_null<PanelController*> controller,\n\t\tint valueIndex,\n\t\tconst IdentityData &data,\n\t\tstd::vector<ScanInfo> &&files);\n\nprotected:\n\tvoid prepare() override;\n\tvoid setInnerFocus() override;\n\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid chooseScan();\n\tvoid encryptScan(const QString &path);\n\tvoid encryptScanContent(QByteArray &&content);\n\tvoid updateScan(ScanInfo &&info);\n\tint countHeight() const;\n\tvoid updateControlsPosition();\n\tvoid save();\n\n\tnot_null<PanelController*> _controller;\n\tint _valueIndex = -1;\n\n\tstd::vector<ScanInfo> _files;\n\n\tstd::vector<object_ptr<ScanButton>> _scans;\n\tobject_ptr<Ui::LinkButton> _uploadScan;\n\tobject_ptr<Ui::InputField> _name;\n\tobject_ptr<Ui::InputField> _surname;\n\n};\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_encryption.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"passport/passport_encryption.h\"\n\n#include \"base/openssl_help.h\"\n#include \"base/random.h\"\n#include \"mtproto/details/mtproto_rsa_public_key.h\"\n\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonArray>\n#include <QtCore/QJsonObject>\n\nnamespace Passport {\nnamespace {\n\nconstexpr auto kAesKeyLength = 32;\nconstexpr auto kAesIvLength = 16;\nconstexpr auto kSecretSize = 32;\nconstexpr auto kAesParamsHashSize = 64;\nconstexpr auto kMinPadding = 32;\nconstexpr auto kMaxPadding = 255;\nconstexpr auto kAlignTo = 16;\n\n} // namespace\n\nstruct AesParams {\n\tbytes::vector key;\n\tbytes::vector iv;\n};\n\nAesParams PrepareAesParamsWithHash(bytes::const_span hashForEncryptionKey) {\n\tExpects(hashForEncryptionKey.size() == kAesParamsHashSize);\n\n\tauto result = AesParams();\n\tresult.key = bytes::make_vector(\n\t\thashForEncryptionKey.subspan(0, kAesKeyLength));\n\tresult.iv = bytes::make_vector(\n\t\thashForEncryptionKey.subspan(kAesKeyLength, kAesIvLength));\n\treturn result;\n}\n\nAesParams PrepareAesParams(bytes::const_span bytesForEncryptionKey) {\n\treturn PrepareAesParamsWithHash(openssl::Sha512(bytesForEncryptionKey));\n}\n\nbytes::vector EncryptOrDecrypt(\n\t\tbytes::const_span initial,\n\t\tAesParams &&params,\n\t\tint encryptOrDecrypt) {\n\tExpects((initial.size() & 0x0F) == 0);\n\tExpects(params.key.size() == kAesKeyLength);\n\tExpects(params.iv.size() == kAesIvLength);\n\n\tauto aesKey = AES_KEY();\n\tconst auto error = (encryptOrDecrypt == AES_ENCRYPT)\n\t\t? AES_set_encrypt_key(\n\t\t\treinterpret_cast<const uchar*>(params.key.data()),\n\t\t\tparams.key.size() * CHAR_BIT,\n\t\t\t&aesKey)\n\t\t: AES_set_decrypt_key(\n\t\t\treinterpret_cast<const uchar*>(params.key.data()),\n\t\t\tparams.key.size() * CHAR_BIT,\n\t\t\t&aesKey);\n\tif (error != 0) {\n\t\tLOG((\"App Error: Could not AES_set_encrypt_key, result %1\"\n\t\t\t).arg(error));\n\t\treturn {};\n\t}\n\tauto result = bytes::vector(initial.size());\n\tAES_cbc_encrypt(\n\t\treinterpret_cast<const uchar*>(initial.data()),\n\t\treinterpret_cast<uchar*>(result.data()),\n\t\tinitial.size(),\n\t\t&aesKey,\n\t\treinterpret_cast<uchar*>(params.iv.data()),\n\t\tencryptOrDecrypt);\n\treturn result;\n}\n\nbytes::vector Encrypt(\n\t\tbytes::const_span decrypted,\n\t\tAesParams &&params) {\n\treturn EncryptOrDecrypt(decrypted, std::move(params), AES_ENCRYPT);\n}\n\nbytes::vector Decrypt(\n\t\tbytes::const_span encrypted,\n\t\tAesParams &&params) {\n\treturn EncryptOrDecrypt(encrypted, std::move(params), AES_DECRYPT);\n}\n\nbool CheckBytesMod255(bytes::const_span bytes) {\n\tconst auto full = ranges::accumulate(\n\t\tbytes,\n\t\t0ULL,\n\t\t[](uint64 sum, gsl::byte value) { return sum + uchar(value); });\n\tconst auto mod = (full % 255ULL);\n\treturn (mod == 239);\n}\n\nbool CheckSecretBytes(bytes::const_span secret) {\n\treturn CheckBytesMod255(secret);\n}\n\nbytes::vector GenerateSecretBytes() {\n\tauto result = bytes::vector(kSecretSize);\n\tbytes::set_random(result);\n\tconst auto full = ranges::accumulate(\n\t\tresult,\n\t\t0ULL,\n\t\t[](uint64 sum, gsl::byte value) { return sum + uchar(value); });\n\tconst auto mod = (full % 255ULL);\n\tconst auto add = 255ULL + 239 - mod;\n\tauto first = (static_cast<uchar>(result[0]) + add) % 255ULL;\n\tresult[0] = static_cast<gsl::byte>(first);\n\treturn result;\n}\n\nbytes::vector DecryptSecretBytesWithHash(\n\t\tbytes::const_span encryptedSecret,\n\t\tbytes::const_span hashForEncryptionKey) {\n\tif (encryptedSecret.empty()) {\n\t\treturn {};\n\t} else if (encryptedSecret.size() != kSecretSize) {\n\t\tLOG((\"API Error: Wrong secret size %1\"\n\t\t\t).arg(encryptedSecret.size()));\n\t\treturn {};\n\t}\n\tauto params = PrepareAesParamsWithHash(hashForEncryptionKey);\n\tauto result = Decrypt(encryptedSecret, std::move(params));\n\tif (!CheckSecretBytes(result)) {\n\t\tLOG((\"API Error: Bad secret bytes.\"));\n\t\treturn {};\n\t}\n\treturn result;\n}\n\nbytes::vector DecryptSecretBytes(\n\t\tbytes::const_span encryptedSecret,\n\t\tbytes::const_span bytesForEncryptionKey) {\n\treturn DecryptSecretBytesWithHash(\n\t\tencryptedSecret,\n\t\topenssl::Sha512(bytesForEncryptionKey));\n}\n\nbytes::vector EncryptSecretBytesWithHash(\n\t\tbytes::const_span secret,\n\t\tbytes::const_span hashForEncryptionKey) {\n\tExpects(secret.size() == kSecretSize);\n\tExpects(CheckSecretBytes(secret) == true);\n\n\tauto params = PrepareAesParamsWithHash(hashForEncryptionKey);\n\treturn Encrypt(secret, std::move(params));\n}\n\nbytes::vector EncryptSecretBytes(\n\t\tbytes::const_span secret,\n\t\tbytes::const_span bytesForEncryptionKey) {\n\tExpects(secret.size() == kSecretSize);\n\tExpects(CheckSecretBytes(secret) == true);\n\n\tauto params = PrepareAesParams(bytesForEncryptionKey);\n\treturn Encrypt(secret, std::move(params));\n}\n\nbytes::vector DecryptSecureSecret(\n\t\tbytes::const_span encryptedSecret,\n\t\tbytes::const_span passwordHashForSecret) {\n\tExpects(!encryptedSecret.empty());\n\n\treturn DecryptSecretBytesWithHash(\n\t\tencryptedSecret,\n\t\tpasswordHashForSecret);\n}\n\nbytes::vector EncryptSecureSecret(\n\t\tbytes::const_span secret,\n\t\tbytes::const_span passwordHashForSecret) {\n\tExpects(secret.size() == kSecretSize);\n\n\treturn EncryptSecretBytesWithHash(secret, passwordHashForSecret);\n}\n\nbytes::vector SerializeData(const std::map<QString, QString> &data) {\n\tauto root = QJsonObject();\n\tfor (const auto &[key, value] : data) {\n\t\troot.insert(key, value);\n\t}\n\tauto document = QJsonDocument(root);\n\tconst auto result = document.toJson(QJsonDocument::Compact);\n\treturn bytes::make_vector(result);\n}\n\nstd::map<QString, QString> DeserializeData(bytes::const_span bytes) {\n\tconst auto serialized = QByteArray::fromRawData(\n\t\treinterpret_cast<const char*>(bytes.data()),\n\t\tbytes.size());\n\tauto error = QJsonParseError();\n\tauto document = QJsonDocument::fromJson(serialized, &error);\n\tif (error.error != QJsonParseError::NoError) {\n\t\tLOG((\"API Error: Could not deserialize decrypted JSON, error %1\"\n\t\t\t).arg(error.errorString()));\n\t\treturn {};\n\t} else if (!document.isObject()) {\n\t\tLOG((\"API Error: decrypted JSON root is not an object.\"));\n\t\treturn {};\n\t}\n\tauto object = document.object();\n\tauto result = std::map<QString, QString>();\n\tfor (auto i = object.constBegin(), e = object.constEnd(); i != e; ++i) {\n\t\tconst auto key = i.key();\n\t\tswitch ((*i).type()) {\n\t\tcase QJsonValue::Null: {\n\t\t\tLOG((\"API Error: null found inside decrypted JSON root. \"\n\t\t\t\t\"Defaulting to empty string value.\"));\n\t\t\tresult[key] = QString();\n\t\t} break;\n\t\tcase QJsonValue::Undefined: {\n\t\t\tLOG((\"API Error: undefined found inside decrypted JSON root. \"\n\t\t\t\t\"Defaulting to empty string value.\"));\n\t\t\tresult[key] = QString();\n\t\t} break;\n\t\tcase QJsonValue::Bool: {\n\t\t\tLOG((\"API Error: bool found inside decrypted JSON root. \"\n\t\t\t\t\"Aborting.\"));\n\t\t\treturn {};\n\t\t} break;\n\t\tcase QJsonValue::Double: {\n\t\t\tLOG((\"API Error: double found inside decrypted JSON root. \"\n\t\t\t\t\"Converting to string.\"));\n\t\t\tresult[key] = QString::number((*i).toDouble());\n\t\t} break;\n\t\tcase QJsonValue::String: {\n\t\t\tresult[key] = (*i).toString();\n\t\t} break;\n\t\tcase QJsonValue::Array: {\n\t\t\tLOG((\"API Error: array found inside decrypted JSON root. \"\n\t\t\t\t\"Aborting.\"));\n\t\t\treturn {};\n\t\t} break;\n\t\tcase QJsonValue::Object: {\n\t\t\tLOG((\"API Error: object found inside decrypted JSON root. \"\n\t\t\t\t\"Aborting.\"));\n\t\t\treturn {};\n\t\t} break;\n\t\t}\n\t}\n\treturn result;\n}\n\nstd::vector<DataError> DeserializeErrors(bytes::const_span json) {\n\tconst auto serialized = QByteArray::fromRawData(\n\t\treinterpret_cast<const char*>(json.data()),\n\t\tjson.size());\n\tauto error = QJsonParseError();\n\tauto document = QJsonDocument::fromJson(serialized, &error);\n\tif (error.error != QJsonParseError::NoError) {\n\t\tLOG((\"API Error: Could not deserialize errors JSON, error %1\"\n\t\t\t).arg(error.errorString()));\n\t\treturn {};\n\t} else if (!document.isArray()) {\n\t\tLOG((\"API Error: Errors JSON root is not an array.\"));\n\t\treturn {};\n\t}\n\tauto array = document.array();\n\tauto result = std::vector<DataError>();\n\tfor (const auto error : array) {\n\t\tif (!error.isObject()) {\n\t\t\tLOG((\"API Error: Not an object inside errors JSON.\"));\n\t\t\tcontinue;\n\t\t}\n\t\tauto fields = error.toObject();\n\t\tconst auto typeIt = fields.constFind(\"type\");\n\t\tif (typeIt == fields.constEnd()) {\n\t\t\tLOG((\"API Error: type was not found in an error.\"));\n\t\t\tcontinue;\n\t\t} else if (!(*typeIt).isString()) {\n\t\t\tLOG((\"API Error: type was not a string in an error.\"));\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto descriptionIt = fields.constFind(\"description\");\n\t\tif (descriptionIt == fields.constEnd()) {\n\t\t\tLOG((\"API Error: description was not found in an error.\"));\n\t\t\tcontinue;\n\t\t} else if (!(*typeIt).isString()) {\n\t\t\tLOG((\"API Error: description was not a string in an error.\"));\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto targetIt = fields.constFind(\"target\");\n\t\tif (targetIt == fields.constEnd()) {\n\t\t\tLOG((\"API Error: target aws not found in an error.\"));\n\t\t\tcontinue;\n\t\t} else if (!(*targetIt).isString()) {\n\t\t\tLOG((\"API Error: target was not as string in an error.\"));\n\t\t\tcontinue;\n\t\t}\n\t\tauto next = DataError();\n\t\tnext.type = (*typeIt).toString();\n\t\tnext.text = (*descriptionIt).toString();\n\t\tconst auto fieldIt = fields.constFind(\"field\");\n\t\tconst auto fileHashIt = fields.constFind(\"file_hash\");\n\t\tif (fieldIt != fields.constEnd()) {\n\t\t\tif (!(*fieldIt).isString()) {\n\t\t\t\tLOG((\"API Error: field was not a string in an error.\"));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tnext.key = (*fieldIt).toString();\n\t\t} else if (fileHashIt != fields.constEnd()) {\n\t\t\tif (!(*fileHashIt).isString()) {\n\t\t\t\tLOG((\"API Error: file_hash was not a string in an error.\"));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tnext.key = QByteArray::fromBase64(\n\t\t\t\t(*fileHashIt).toString().toUtf8());\n\t\t} else if ((*targetIt).toString() == \"selfie\") {\n\t\t\tnext.key = QByteArray();\n\t\t}\n\t\tresult.push_back(std::move(next));\n\t}\n\treturn result;\n}\n\nEncryptedData EncryptData(bytes::const_span bytes) {\n\treturn EncryptData(bytes, GenerateSecretBytes());\n}\n\nEncryptedData EncryptData(\n\t\tbytes::const_span bytes,\n\t\tbytes::const_span dataSecret) {\n\tconstexpr auto kFromPadding = kMinPadding + kAlignTo - 1;\n\tconstexpr auto kPaddingDelta = kMaxPadding - kFromPadding;\n\tconst auto randomPadding = kFromPadding\n\t\t+ (base::RandomValue<uint32>() % kPaddingDelta);\n\tconst auto padding = randomPadding\n\t\t- ((bytes.size() + randomPadding) % kAlignTo);\n\tAssert(padding >= kMinPadding && padding <= kMaxPadding);\n\n\tauto unencrypted = bytes::vector(padding + bytes.size());\n\tAssert(unencrypted.size() % kAlignTo == 0);\n\n\tunencrypted[0] = static_cast<gsl::byte>(padding);\n\tbase::RandomFill(unencrypted.data() + 1, padding - 1);\n\tbytes::copy(\n\t\tgsl::make_span(unencrypted).subspan(padding),\n\t\tbytes);\n\tconst auto dataHash = openssl::Sha256(unencrypted);\n\tconst auto bytesForEncryptionKey = bytes::concatenate(\n\t\tdataSecret,\n\t\tdataHash);\n\n\tauto params = PrepareAesParams(bytesForEncryptionKey);\n\treturn {\n\t\t{ dataSecret.begin(), dataSecret.end() },\n\t\t{ dataHash.begin(), dataHash.end() },\n\t\tEncrypt(unencrypted, std::move(params))\n\t};\n}\n\nbytes::vector DecryptData(\n\t\tbytes::const_span encrypted,\n\t\tbytes::const_span dataHash,\n\t\tbytes::const_span dataSecret) {\n\tconstexpr auto kDataHashSize = 32;\n\tif (encrypted.empty()) {\n\t\treturn {};\n\t} else if (dataHash.size() != kDataHashSize) {\n\t\tLOG((\"API Error: Bad data hash size %1\").arg(dataHash.size()));\n\t\treturn {};\n\t} else if (dataSecret.size() != kSecretSize) {\n\t\tLOG((\"API Error: Bad data secret size %1\").arg(dataSecret.size()));\n\t\treturn {};\n\t}\n\n\tconst auto bytesForEncryptionKey = bytes::concatenate(\n\t\tdataSecret,\n\t\tdataHash);\n\tauto params = PrepareAesParams(bytesForEncryptionKey);\n\tconst auto decrypted = Decrypt(encrypted, std::move(params));\n\tif (bytes::compare(openssl::Sha256(decrypted), dataHash) != 0) {\n\t\tLOG((\"API Error: Bad data hash.\"));\n\t\treturn {};\n\t}\n\tconst auto padding = static_cast<uint32>(decrypted[0]);\n\tif (padding < kMinPadding\n\t\t|| padding > kMaxPadding\n\t\t|| padding > decrypted.size()) {\n\t\tLOG((\"API Error: Bad padding value %1\").arg(padding));\n\t\treturn {};\n\t}\n\tconst auto bytes = gsl::make_span(decrypted).subspan(padding);\n\treturn { bytes.begin(), bytes.end() };\n}\n\nbytes::vector PrepareValueHash(\n\t\tbytes::const_span dataHash,\n\t\tbytes::const_span valueSecret) {\n\treturn openssl::Sha256(dataHash, valueSecret);\n}\n\nbytes::vector EncryptValueSecret(\n\t\tbytes::const_span valueSecret,\n\t\tbytes::const_span secret,\n\t\tbytes::const_span valueHash) {\n\tconst auto bytesForEncryptionKey = bytes::concatenate(\n\t\tsecret,\n\t\tvalueHash);\n\treturn EncryptSecretBytes(valueSecret, bytesForEncryptionKey);\n}\n\nbytes::vector DecryptValueSecret(\n\t\tbytes::const_span encrypted,\n\t\tbytes::const_span secret,\n\t\tbytes::const_span valueHash) {\n\tconst auto bytesForEncryptionKey = bytes::concatenate(\n\t\tsecret,\n\t\tvalueHash);\n\treturn DecryptSecretBytes(encrypted, bytesForEncryptionKey);\n}\n\nuint64 CountSecureSecretId(bytes::const_span secret) {\n\tconst auto full = openssl::Sha256(secret);\n\treturn *reinterpret_cast<const uint64*>(full.data());\n}\n\nbytes::vector EncryptCredentialsSecret(\n\t\tbytes::const_span secret,\n\t\tbytes::const_span publicKey) {\n\tconst auto key = MTP::details::RSAPublicKey(publicKey);\n\treturn key.encryptOAEPpadding(secret);\n}\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_encryption.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Passport {\n\nbytes::vector GenerateSecretBytes();\n\nbytes::vector EncryptSecureSecret(\n\tbytes::const_span secret,\n\tbytes::const_span passwordHashForSecret);\nbytes::vector DecryptSecureSecret(\n\tbytes::const_span encryptedSecret,\n\tbytes::const_span passwordHashForSecret);\n\nbytes::vector SerializeData(const std::map<QString, QString> &data);\nstd::map<QString, QString> DeserializeData(bytes::const_span bytes);\n\nstruct DataError {\n\t// QByteArray - bad existing scan with such file_hash\n\t// QString - bad data field value with such key\n\t// std::nullopt - additional scan required\n\tstd::variant<v::null_t, QByteArray, QString> key;\n\tQString type; // personal_details, passport, etc.\n\tQString text;\n\n};\nstd::vector<DataError> DeserializeErrors(bytes::const_span json);\n\nstruct EncryptedData {\n\tbytes::vector secret;\n\tbytes::vector hash;\n\tbytes::vector bytes;\n};\n\nEncryptedData EncryptData(bytes::const_span bytes);\n\nEncryptedData EncryptData(\n\tbytes::const_span bytes,\n\tbytes::const_span dataSecret);\n\nbytes::vector DecryptData(\n\tbytes::const_span encrypted,\n\tbytes::const_span dataHash,\n\tbytes::const_span dataSecret);\n\nbytes::vector PrepareValueHash(\n\tbytes::const_span dataHash,\n\tbytes::const_span valueSecret);\n\nbytes::vector EncryptValueSecret(\n\tbytes::const_span valueSecret,\n\tbytes::const_span secret,\n\tbytes::const_span valueHash);\n\nbytes::vector DecryptValueSecret(\n\tbytes::const_span encrypted,\n\tbytes::const_span secret,\n\tbytes::const_span valueHash);\n\nuint64 CountSecureSecretId(bytes::const_span secret);\n\nbytes::vector EncryptCredentialsSecret(\n\tbytes::const_span secret,\n\tbytes::const_span publicKey);\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_form_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"passport/passport_form_controller.h\"\n\n#include \"passport/passport_encryption.h\"\n#include \"passport/passport_panel_controller.h\"\n#include \"passport/passport_panel_edit_document.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/passcode_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"lang/lang_hardcoded.h\"\n#include \"base/random.h\"\n#include \"base/qthelp_url.h\"\n#include \"base/unixtime.h\"\n#include \"base/call_delayed.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"mainwindow.h\"\n#include \"window/window_session_controller.h\"\n#include \"core/click_handler_types.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/sent_code_field.h\"\n#include \"main/main_session.h\"\n#include \"storage/localimageloader.h\"\n#include \"storage/localstorage.h\"\n#include \"storage/file_upload.h\"\n#include \"storage/file_download_mtproto.h\"\n\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonArray>\n#include <QtCore/QJsonObject>\n\nnamespace Passport {\nnamespace {\n\nconstexpr auto kDocumentScansLimit = 20;\nconstexpr auto kTranslationScansLimit = 20;\nconstexpr auto kShortPollTimeout = crl::time(3000);\nconstexpr auto kRememberCredentialsDelay = crl::time(1800 * 1000);\n\nbool ForwardServiceErrorRequired(const QString &error) {\n\treturn (error == u\"BOT_INVALID\"_q)\n\t\t|| (error == u\"PUBLIC_KEY_REQUIRED\"_q)\n\t\t|| (error == u\"PUBLIC_KEY_INVALID\"_q)\n\t\t|| (error == u\"SCOPE_EMPTY\"_q)\n\t\t|| (error == u\"PAYLOAD_EMPTY\"_q);\n}\n\nbool SaveErrorRequiresRestart(const QString &error) {\n\treturn (error == u\"PASSWORD_REQUIRED\"_q)\n\t\t|| (error == u\"SECURE_SECRET_REQUIRED\"_q)\n\t\t|| (error == u\"SECURE_SECRET_INVALID\"_q);\n}\n\nbool AcceptErrorRequiresRestart(const QString &error) {\n\treturn (error == u\"PASSWORD_REQUIRED\"_q)\n\t\t|| (error == u\"SECURE_SECRET_REQUIRED\"_q)\n\t\t|| (error == u\"SECURE_VALUE_EMPTY\"_q)\n\t\t|| (error == u\"SECURE_VALUE_HASH_INVALID\"_q);\n}\n\nstd::map<QString, QString> GetTexts(const ValueMap &map) {\n\tauto result = std::map<QString, QString>();\n\tfor (const auto &[key, value] : map.fields) {\n\t\tresult[key] = value.text;\n\t}\n\treturn result;\n}\n\nQImage ReadImage(bytes::const_span buffer) {\n\treturn Images::Read({\n\t\t.content = QByteArray::fromRawData(\n\t\t\treinterpret_cast<const char*>(buffer.data()),\n\t\t\tbuffer.size()),\n\t\t.forceOpaque = true,\n\t}).image;\n}\n\nValue::Type ConvertType(const MTPSecureValueType &type) {\n\tusing Type = Value::Type;\n\tswitch (type.type()) {\n\tcase mtpc_secureValueTypePersonalDetails:\n\t\treturn Type::PersonalDetails;\n\tcase mtpc_secureValueTypePassport:\n\t\treturn Type::Passport;\n\tcase mtpc_secureValueTypeDriverLicense:\n\t\treturn Type::DriverLicense;\n\tcase mtpc_secureValueTypeIdentityCard:\n\t\treturn Type::IdentityCard;\n\tcase mtpc_secureValueTypeInternalPassport:\n\t\treturn Type::InternalPassport;\n\tcase mtpc_secureValueTypeAddress:\n\t\treturn Type::Address;\n\tcase mtpc_secureValueTypeUtilityBill:\n\t\treturn Type::UtilityBill;\n\tcase mtpc_secureValueTypeBankStatement:\n\t\treturn Type::BankStatement;\n\tcase mtpc_secureValueTypeRentalAgreement:\n\t\treturn Type::RentalAgreement;\n\tcase mtpc_secureValueTypePassportRegistration:\n\t\treturn Type::PassportRegistration;\n\tcase mtpc_secureValueTypeTemporaryRegistration:\n\t\treturn Type::TemporaryRegistration;\n\tcase mtpc_secureValueTypePhone:\n\t\treturn Type::Phone;\n\tcase mtpc_secureValueTypeEmail:\n\t\treturn Type::Email;\n\t}\n\tUnexpected(\"Type in secureValueType type.\");\n};\n\nMTPSecureValueType ConvertType(Value::Type type) {\n\tusing Type = Value::Type;\n\tswitch (type) {\n\tcase Type::PersonalDetails:\n\t\treturn MTP_secureValueTypePersonalDetails();\n\tcase Type::Passport:\n\t\treturn MTP_secureValueTypePassport();\n\tcase Type::DriverLicense:\n\t\treturn MTP_secureValueTypeDriverLicense();\n\tcase Type::IdentityCard:\n\t\treturn MTP_secureValueTypeIdentityCard();\n\tcase Type::InternalPassport:\n\t\treturn MTP_secureValueTypeInternalPassport();\n\tcase Type::Address:\n\t\treturn MTP_secureValueTypeAddress();\n\tcase Type::UtilityBill:\n\t\treturn MTP_secureValueTypeUtilityBill();\n\tcase Type::BankStatement:\n\t\treturn MTP_secureValueTypeBankStatement();\n\tcase Type::RentalAgreement:\n\t\treturn MTP_secureValueTypeRentalAgreement();\n\tcase Type::PassportRegistration:\n\t\treturn MTP_secureValueTypePassportRegistration();\n\tcase Type::TemporaryRegistration:\n\t\treturn MTP_secureValueTypeTemporaryRegistration();\n\tcase Type::Phone:\n\t\treturn MTP_secureValueTypePhone();\n\tcase Type::Email:\n\t\treturn MTP_secureValueTypeEmail();\n\t}\n\tUnexpected(\"Type in FormController::submit.\");\n}\n\nvoid CollectToRequestedRow(\n\t\tRequestedRow &row,\n\t\tconst MTPSecureRequiredType &data) {\n\tdata.match([&](const MTPDsecureRequiredType &data) {\n\t\trow.values.emplace_back(ConvertType(data.vtype()));\n\t\tauto &value = row.values.back();\n\t\tvalue.selfieRequired = data.is_selfie_required();\n\t\tvalue.translationRequired = data.is_translation_required();\n\t\tvalue.nativeNames = data.is_native_names();\n\t}, [&](const MTPDsecureRequiredTypeOneOf &data) {\n\t\trow.values.reserve(row.values.size() + data.vtypes().v.size());\n\t\tfor (const auto &one : data.vtypes().v) {\n\t\t\tCollectToRequestedRow(row, one);\n\t\t}\n\t});\n}\n\nvoid ApplyDataChanges(ValueData &data, ValueMap &&changes) {\n\tdata.parsedInEdit = data.parsed;\n\tfor (auto &[key, value] : changes.fields) {\n\t\tdata.parsedInEdit.fields[key] = std::move(value);\n\t}\n}\n\nRequestedRow CollectRequestedRow(const MTPSecureRequiredType &data) {\n\tauto result = RequestedRow();\n\tCollectToRequestedRow(result, data);\n\treturn result;\n}\n\nQJsonObject GetJSONFromMap(\n\tconst std::map<QString, bytes::const_span> &map) {\n\tauto result = QJsonObject();\n\tfor (const auto &[key, value] : map) {\n\t\tconst auto raw = QByteArray::fromRawData(\n\t\t\treinterpret_cast<const char*>(value.data()),\n\t\t\tvalue.size());\n\t\tresult.insert(key, QString::fromUtf8(raw.toBase64()));\n\t}\n\treturn result;\n}\n\nQJsonObject GetJSONFromFile(const File &file) {\n\treturn GetJSONFromMap({\n\t\t{ \"file_hash\", file.hash },\n\t\t{ \"secret\", file.secret }\n\t\t});\n}\n\nFormRequest PreprocessRequest(const FormRequest &request) {\n\tauto result = request;\n\tresult.publicKey.replace(\"\\r\\n\", \"\\n\");\n\treturn result;\n}\n\nQString ValueCredentialsKey(Value::Type type) {\n\tusing Type = Value::Type;\n\tswitch (type) {\n\tcase Type::PersonalDetails: return \"personal_details\";\n\tcase Type::Passport: return \"passport\";\n\tcase Type::DriverLicense: return \"driver_license\";\n\tcase Type::IdentityCard: return \"identity_card\";\n\tcase Type::InternalPassport: return \"internal_passport\";\n\tcase Type::Address: return \"address\";\n\tcase Type::UtilityBill: return \"utility_bill\";\n\tcase Type::BankStatement: return \"bank_statement\";\n\tcase Type::RentalAgreement: return \"rental_agreement\";\n\tcase Type::PassportRegistration: return \"passport_registration\";\n\tcase Type::TemporaryRegistration: return \"temporary_registration\";\n\tcase Type::Phone:\n\tcase Type::Email: return QString();\n\t}\n\tUnexpected(\"Type in ValueCredentialsKey.\");\n}\n\nQString SpecialScanCredentialsKey(FileType type) {\n\tswitch (type) {\n\tcase FileType::FrontSide: return \"front_side\";\n\tcase FileType::ReverseSide: return \"reverse_side\";\n\tcase FileType::Selfie: return \"selfie\";\n\t}\n\tUnexpected(\"Type in SpecialScanCredentialsKey.\");\n}\n\nQString ValidateUrl(const QString &url) {\n\tconst auto result = qthelp::validate_url(url);\n\treturn (result.startsWith(\"http://\", Qt::CaseInsensitive)\n\t\t|| result.startsWith(\"https://\", Qt::CaseInsensitive))\n\t\t? result\n\t\t: QString();\n}\n\nauto ParseConfig(const QByteArray &json) {\n\tauto languagesByCountryCode = std::map<QString, QString>();\n\tauto error = QJsonParseError{ 0, QJsonParseError::NoError };\n\tconst auto document = QJsonDocument::fromJson(json, &error);\n\tif (error.error != QJsonParseError::NoError) {\n\t\tLOG((\"API Error: Failed to parse passport config, error: %1.\"\n\t\t\t).arg(error.errorString()));\n\t\treturn languagesByCountryCode;\n\t} else if (!document.isObject()) {\n\t\tLOG((\"API Error: Not an object received in passport config.\"));\n\t\treturn languagesByCountryCode;\n\t}\n\tconst auto object = document.object();\n\tfor (auto i = object.constBegin(); i != object.constEnd(); ++i) {\n\t\tconst auto countryCode = i.key();\n\t\tconst auto language = i.value();\n\t\tif (!language.isString()) {\n\t\t\tLOG((\"API Error: Not a string in passport config item.\"));\n\t\t\tcontinue;\n\t\t}\n\t\tlanguagesByCountryCode.emplace(\n\t\t\tcountryCode,\n\t\t\tlanguage.toString());\n\t}\n\treturn languagesByCountryCode;\n}\n\n} // namespace\n\nQString NonceNameByScope(const QString &scope) {\n\treturn (scope.startsWith('{') && scope.endsWith('}'))\n\t\t? u\"nonce\"_q\n\t\t: u\"payload\"_q;\n}\n\nbool ValueChanged(not_null<const Value*> value, const ValueMap &data) {\n\tconst auto FileChanged = [](const EditFile &file) {\n\t\tif (file.uploadData) {\n\t\t\treturn !file.deleted;\n\t\t}\n\t\treturn file.deleted;\n\t};\n\n\tfor (const auto &scan : value->filesInEdit(FileType::Scan)) {\n\t\tif (FileChanged(scan)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\tfor (const auto &scan : value->filesInEdit(FileType::Translation)) {\n\t\tif (FileChanged(scan)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\tfor (const auto &[type, scan] : value->specialScansInEdit) {\n\t\tif (FileChanged(scan)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\tconst auto &existing = value->data.parsed.fields;\n\tfor (const auto &[key, value] : data.fields) {\n\t\tconst auto i = existing.find(key);\n\t\tif (i != existing.end()) {\n\t\t\tif (i->second.text != value.text) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (!value.text.isEmpty()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nFormRequest::FormRequest(\n\tUserId botId,\n\tconst QString &scope,\n\tconst QString &callbackUrl,\n\tconst QString &publicKey,\n\tconst QString &nonce)\n: botId(botId)\n, scope(scope)\n, callbackUrl(ValidateUrl(callbackUrl))\n, publicKey(publicKey)\n, nonce(nonce) {\n}\n\nEditFile::EditFile(\n\tnot_null<Main::Session*> session,\n\tnot_null<const Value*> value,\n\tFileType type,\n\tconst File &fields,\n\tstd::unique_ptr<UploadScanData> &&uploadData)\n: value(value)\n, type(type)\n, fields(std::move(fields))\n, uploadData(session, std::move(uploadData))\n, guard(std::make_shared<bool>(true)) {\n}\n\nUploadScanDataPointer::UploadScanDataPointer(\n\tnot_null<Main::Session*> session,\n\tstd::unique_ptr<UploadScanData> &&value)\n: _session(session)\n, _value(std::move(value)) {\n}\n\nUploadScanDataPointer::UploadScanDataPointer(\n\tUploadScanDataPointer &&other) = default;\n\nUploadScanDataPointer &UploadScanDataPointer::operator=(\n\tUploadScanDataPointer &&other) = default;\n\nUploadScanDataPointer::~UploadScanDataPointer() {\n\tif (const auto value = _value.get()) {\n\t\tif (const auto fullId = value->fullId) {\n\t\t\t_session->uploader().cancel(fullId);\n\t\t}\n\t}\n}\n\nUploadScanData *UploadScanDataPointer::get() const {\n\treturn _value.get();\n}\n\nUploadScanDataPointer::operator UploadScanData*() const {\n\treturn _value.get();\n}\n\nUploadScanDataPointer::operator bool() const {\n\treturn _value.get();\n}\n\nUploadScanData *UploadScanDataPointer::operator->() const {\n\treturn _value.get();\n}\n\nRequestedValue::RequestedValue(Value::Type type) : type(type) {\n}\n\nValue::Value(Type type) : type(type) {\n}\n\nbool Value::requiresScan(FileType type) const {\n\tif (type == FileType::Scan) {\n\t\treturn (this->type == Type::UtilityBill)\n\t\t\t|| (this->type == Type::BankStatement)\n\t\t\t|| (this->type == Type::RentalAgreement)\n\t\t\t|| (this->type == Type::PassportRegistration)\n\t\t\t|| (this->type == Type::TemporaryRegistration);\n\t} else if (type == FileType::Translation) {\n\t\treturn translationRequired;\n\t} else {\n\t\treturn requiresSpecialScan(type);\n\t}\n}\n\nbool Value::requiresSpecialScan(FileType type) const {\n\tswitch (type) {\n\tcase FileType::FrontSide:\n\t\treturn (this->type == Type::Passport)\n\t\t\t|| (this->type == Type::DriverLicense)\n\t\t\t|| (this->type == Type::IdentityCard)\n\t\t\t|| (this->type == Type::InternalPassport);\n\tcase FileType::ReverseSide:\n\t\treturn (this->type == Type::DriverLicense)\n\t\t\t|| (this->type == Type::IdentityCard);\n\tcase FileType::Selfie:\n\t\treturn selfieRequired;\n\t}\n\tUnexpected(\"Special scan type in requiresSpecialScan.\");\n}\n\nvoid Value::fillDataFrom(Value &&other) {\n\tconst auto savedSelfieRequired = selfieRequired;\n\tconst auto savedTranslationRequired = translationRequired;\n\tconst auto savedNativeNames = nativeNames;\n\tconst auto savedEditScreens = editScreens;\n\n\t*this = std::move(other);\n\n\tselfieRequired = savedSelfieRequired;\n\ttranslationRequired = savedTranslationRequired;\n\tnativeNames = savedNativeNames;\n\teditScreens = savedEditScreens;\n}\n\nbool Value::scansAreFilled() const {\n\treturn (whatNotFilled() == 0);\n}\n\nint Value::whatNotFilled() const {\n\tconst auto noRequiredSpecialScan = [&](FileType type) {\n\t\treturn requiresSpecialScan(type)\n\t\t\t&& (specialScans.find(type) == end(specialScans));\n\t};\n\tif (requiresScan(FileType::Scan) && _scans.empty()) {\n\t\treturn kNothingFilled;\n\t} else if (noRequiredSpecialScan(FileType::FrontSide)) {\n\t\treturn kNothingFilled;\n\t}\n\tauto result = 0;\n\tif (requiresScan(FileType::Translation) && _translations.empty()) {\n\t\tresult |= kNoTranslationFilled;\n\t}\n\tif (noRequiredSpecialScan(FileType::ReverseSide)\n\t\t|| noRequiredSpecialScan(FileType::Selfie)) {\n\t\tresult |= kNoSelfieFilled;\n\t}\n\treturn result;\n}\n\nvoid Value::saveInEdit(not_null<Main::Session*> session) {\n\tconst auto saveList = [&](FileType type) {\n\t\tfilesInEdit(type) = ranges::views::all(\n\t\t\tfiles(type)\n\t\t) | ranges::views::transform([=](const File &file) {\n\t\t\treturn EditFile(session, this, type, file, nullptr);\n\t\t}) | ranges::to_vector;\n\t};\n\tsaveList(FileType::Scan);\n\tsaveList(FileType::Translation);\n\n\tspecialScansInEdit.clear();\n\tfor (const auto &[type, scan] : specialScans) {\n\t\tspecialScansInEdit.emplace(type, EditFile(\n\t\t\tsession,\n\t\t\tthis,\n\t\t\ttype,\n\t\t\tscan,\n\t\t\tnullptr));\n\t}\n\tdata.parsedInEdit = data.parsed;\n}\n\nvoid Value::clearEditData() {\n\tfilesInEdit(FileType::Scan).clear();\n\tfilesInEdit(FileType::Translation).clear();\n\tspecialScansInEdit.clear();\n\tdata.encryptedSecretInEdit.clear();\n\tdata.hashInEdit.clear();\n\tdata.parsedInEdit = ValueMap();\n}\n\nbool Value::uploadingScan() const {\n\tconst auto uploading = [](const EditFile &file) {\n\t\treturn file.uploadData\n\t\t\t&& file.uploadData->fullId\n\t\t\t&& !file.deleted;\n\t};\n\tconst auto uploadingInList = [&](FileType type) {\n\t\tconst auto &list = filesInEdit(type);\n\t\treturn ranges::any_of(list, uploading);\n\t};\n\tif (uploadingInList(FileType::Scan)\n\t\t|| uploadingInList(FileType::Translation)) {\n\t\treturn true;\n\t}\n\tif (ranges::any_of(specialScansInEdit, [&](const auto &pair) {\n\t\treturn uploading(pair.second);\n\t})) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool Value::saving() const {\n\treturn (saveRequestId != 0)\n\t\t|| (verification.requestId != 0)\n\t\t|| (verification.codeLength != 0)\n\t\t|| uploadingScan();\n}\n\nstd::vector<File> &Value::files(FileType type) {\n\tswitch (type) {\n\tcase FileType::Scan: return _scans;\n\tcase FileType::Translation: return _translations;\n\t}\n\tUnexpected(\"Type in Value::files().\");\n}\n\nconst std::vector<File> &Value::files(FileType type) const {\n\tswitch (type) {\n\tcase FileType::Scan: return _scans;\n\tcase FileType::Translation: return _translations;\n\t}\n\tUnexpected(\"Type in Value::files() const.\");\n}\n\nQString &Value::fileMissingError(FileType type) {\n\tswitch (type) {\n\tcase FileType::Scan: return _scanMissingError;\n\tcase FileType::Translation: return _translationMissingError;\n\t}\n\tUnexpected(\"Type in Value::fileMissingError().\");\n}\n\nconst QString &Value::fileMissingError(FileType type) const {\n\tswitch (type) {\n\tcase FileType::Scan: return _scanMissingError;\n\tcase FileType::Translation: return _translationMissingError;\n\t}\n\tUnexpected(\"Type in Value::fileMissingError() const.\");\n}\n\nstd::vector<EditFile> &Value::filesInEdit(FileType type) {\n\tswitch (type) {\n\tcase FileType::Scan: return _scansInEdit;\n\tcase FileType::Translation: return _translationsInEdit;\n\t}\n\tUnexpected(\"Type in Value::filesInEdit().\");\n}\n\nconst std::vector<EditFile> &Value::filesInEdit(FileType type) const {\n\tswitch (type) {\n\tcase FileType::Scan: return _scansInEdit;\n\tcase FileType::Translation: return _translationsInEdit;\n\t}\n\tUnexpected(\"Type in Value::filesInEdit() const.\");\n}\n\nEditFile &Value::fileInEdit(FileType type, std::optional<int> fileIndex) {\n\tswitch (type) {\n\tcase FileType::Scan:\n\tcase FileType::Translation: {\n\t\tauto &list = filesInEdit(type);\n\t\tAssert(fileIndex.has_value());\n\t\tAssert(*fileIndex >= 0 && *fileIndex < list.size());\n\t\treturn list[*fileIndex];\n\t} break;\n\t}\n\tconst auto i = specialScansInEdit.find(type);\n\tAssert(!fileIndex.has_value());\n\tAssert(i != end(specialScansInEdit));\n\treturn i->second;\n}\n\nconst EditFile &Value::fileInEdit(\n\t\tFileType type,\n\t\tstd::optional<int> fileIndex) const {\n\tswitch (type) {\n\tcase FileType::Scan:\n\tcase FileType::Translation: {\n\t\tauto &list = filesInEdit(type);\n\t\tAssert(fileIndex.has_value());\n\t\tAssert(*fileIndex >= 0 && *fileIndex < list.size());\n\t\treturn list[*fileIndex];\n\t} break;\n\t}\n\tconst auto i = specialScansInEdit.find(type);\n\tAssert(!fileIndex.has_value());\n\tAssert(i != end(specialScansInEdit));\n\treturn i->second;\n}\n\nstd::vector<EditFile> Value::takeAllFilesInEdit() {\n\tauto result = base::take(filesInEdit(FileType::Scan));\n\tauto &translation = filesInEdit(FileType::Translation);\n\tauto &special = specialScansInEdit;\n\tresult.reserve(result.size() + translation.size() + special.size());\n\n\tfor (auto &scan : base::take(translation)) {\n\t\tresult.push_back(std::move(scan));\n\t}\n\tfor (auto &[type, scan] : base::take(special)) {\n\t\tresult.push_back(std::move(scan));\n\t}\n\treturn result;\n}\n\nFormController::FormController(\n\tnot_null<Window::SessionController*> controller,\n\tconst FormRequest &request)\n: _controller(controller)\n, _api(&_controller->session().mtp())\n, _request(PreprocessRequest(request))\n, _shortPollTimer([=] { reloadPassword(); })\n, _view(std::make_unique<PanelController>(this)) {\n}\n\nMain::Session &FormController::session() const {\n\treturn _controller->session();\n}\n\nvoid FormController::show() {\n\trequestForm();\n\trequestPassword();\n}\n\nUserData *FormController::bot() const {\n\treturn _bot;\n}\n\nQString FormController::privacyPolicyUrl() const {\n\treturn _form.privacyPolicyUrl;\n}\n\nbytes::vector FormController::passwordHashForAuth(\n\t\tbytes::const_span password) const {\n\treturn Core::ComputeCloudPasswordHash(_password.request.algo, password);\n}\n\nauto FormController::prepareFinalData() -> FinalData {\n\tauto errors = std::vector<not_null<const Value*>>();\n\tauto hashes = QVector<MTPSecureValueHash>();\n\tauto secureData = QJsonObject();\n\tconst auto addValueToJSON = [&](\n\t\t\tconst QString &key,\n\t\t\tnot_null<const Value*> value) {\n\t\tauto object = QJsonObject();\n\t\tif (!value->data.parsed.fields.empty()) {\n\t\t\tobject.insert(\"data\", GetJSONFromMap({\n\t\t\t\t{ \"data_hash\", value->data.hash },\n\t\t\t\t{ \"secret\", value->data.secret }\n\t\t\t}));\n\t\t}\n\t\tconst auto addList = [&](\n\t\t\t\tconst QString &key,\n\t\t\t\tconst std::vector<File> &list) {\n\t\t\tif (!list.empty()) {\n\t\t\t\tauto files = QJsonArray();\n\t\t\t\tfor (const auto &scan : list) {\n\t\t\t\t\tfiles.append(GetJSONFromFile(scan));\n\t\t\t\t}\n\t\t\t\tobject.insert(key, files);\n\t\t\t}\n\t\t};\n\t\taddList(\"files\", value->files(FileType::Scan));\n\t\tif (value->translationRequired) {\n\t\t\taddList(\"translation\", value->files(FileType::Translation));\n\t\t}\n\t\tfor (const auto &[type, scan] : value->specialScans) {\n\t\t\tif (value->requiresSpecialScan(type)) {\n\t\t\t\tobject.insert(\n\t\t\t\t\tSpecialScanCredentialsKey(type),\n\t\t\t\t\tGetJSONFromFile(scan));\n\t\t\t}\n\t\t}\n\t\tsecureData.insert(key, object);\n\t};\n\tconst auto addValue = [&](not_null<const Value*> value) {\n\t\thashes.push_back(MTP_secureValueHash(\n\t\t\tConvertType(value->type),\n\t\t\tMTP_bytes(value->submitHash)));\n\t\tconst auto key = ValueCredentialsKey(value->type);\n\t\tif (!key.isEmpty()) {\n\t\t\taddValueToJSON(key, value);\n\t\t}\n\t};\n\tconst auto scopes = ComputeScopes(_form);\n\tfor (const auto &scope : scopes) {\n\t\tconst auto row = ComputeScopeRow(scope);\n\t\tif (row.ready.isEmpty() || !row.error.isEmpty()) {\n\t\t\terrors.push_back(scope.details\n\t\t\t\t? scope.details\n\t\t\t\t: scope.documents[0].get());\n\t\t\tcontinue;\n\t\t}\n\t\tif (scope.details) {\n\t\t\taddValue(scope.details);\n\t\t}\n\t\tif (!scope.documents.empty()) {\n\t\t\tfor (const auto &document : scope.documents) {\n\t\t\t\tif (document->scansAreFilled()) {\n\t\t\t\t\taddValue(document);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tauto json = QJsonObject();\n\tif (errors.empty()) {\n\t\tjson.insert(\"secure_data\", secureData);\n\t\tjson.insert(NonceNameByScope(_request.scope), _request.nonce);\n\t}\n\n\treturn {\n\t\thashes,\n\t\tQJsonDocument(json).toJson(QJsonDocument::Compact),\n\t\terrors\n\t};\n}\n\nstd::vector<not_null<const Value*>> FormController::submitGetErrors() {\n\tif (_submitRequestId || _submitSuccess|| _cancelled) {\n\t\treturn {};\n\t}\n\n\tconst auto prepared = prepareFinalData();\n\tif (!prepared.errors.empty()) {\n\t\treturn prepared.errors;\n\t}\n\tconst auto credentialsEncryptedData = EncryptData(\n\t\tbytes::make_span(prepared.credentials));\n\tconst auto credentialsEncryptedSecret = EncryptCredentialsSecret(\n\t\tcredentialsEncryptedData.secret,\n\t\tbytes::make_span(_request.publicKey.toUtf8()));\n\n\t_submitRequestId = _api.request(MTPaccount_AcceptAuthorization(\n\t\tMTP_long(_request.botId.bare),\n\t\tMTP_string(_request.scope),\n\t\tMTP_string(_request.publicKey),\n\t\tMTP_vector<MTPSecureValueHash>(prepared.hashes),\n\t\tMTP_secureCredentialsEncrypted(\n\t\t\tMTP_bytes(credentialsEncryptedData.bytes),\n\t\t\tMTP_bytes(credentialsEncryptedData.hash),\n\t\t\tMTP_bytes(credentialsEncryptedSecret))\n\t)).done([=] {\n\t\t_submitRequestId = 0;\n\t\t_submitSuccess = true;\n\n\t\t_view->showToast(tr::lng_passport_success(tr::now));\n\n\t\tbase::call_delayed(\n\t\t\t(st::defaultToast.durationFadeIn\n\t\t\t\t+ Ui::Toast::kDefaultDuration\n\t\t\t\t+ st::defaultToast.durationFadeOut),\n\t\t\tthis,\n\t\t\t[=] { cancel(); });\n\t}).fail([=](const MTP::Error &error) {\n\t\t_submitRequestId = 0;\n\t\tif (handleAppUpdateError(error.type())) {\n\t\t} else if (AcceptErrorRequiresRestart(error.type())) {\n\t\t\tsuggestRestart();\n\t\t} else {\n\t\t\t_view->show(Ui::MakeInformBox(\n\t\t\t\tLang::Hard::SecureAcceptError() + \"\\n\" + error.type()));\n\t\t}\n\t}).send();\n\n\treturn {};\n}\n\nvoid FormController::checkPasswordHash(\n\t\tmtpRequestId &guard,\n\t\tbytes::vector hash,\n\t\tPasswordCheckCallback callback) {\n\t_passwordCheckHash = std::move(hash);\n\t_passwordCheckCallback = std::move(callback);\n\tif (_password.request.id) {\n\t\tpasswordChecked();\n\t} else {\n\t\trequestPasswordData(guard);\n\t}\n}\n\nvoid FormController::passwordChecked() {\n\tif (!_password.request || !_password.request.id) {\n\t\treturn passwordServerError();\n\t}\n\tconst auto check = Core::ComputeCloudPasswordCheck(\n\t\t_password.request,\n\t\t_passwordCheckHash);\n\tif (!check) {\n\t\treturn passwordServerError();\n\t}\n\t_password.request.id = 0;\n\t_passwordCheckCallback(check);\n}\n\nvoid FormController::requestPasswordData(mtpRequestId &guard) {\n\tif (!_passwordCheckCallback) {\n\t\treturn passwordServerError();\n\t}\n\n\t_api.request(base::take(guard)).cancel();\n\tguard = _api.request(\n\t\tMTPaccount_GetPassword()\n\t).done([=, &guard](const MTPaccount_Password &result) {\n\t\tguard = 0;\n\t\tresult.match([&](const MTPDaccount_password &data) {\n\t\t\t_password.request = Core::ParseCloudPasswordCheckRequest(data);\n\t\t\tpasswordChecked();\n\t\t});\n\t}).send();\n}\n\nvoid FormController::submitPassword(const QByteArray &password) {\n\tExpects(!!_password.request);\n\n\tconst auto submitSaved = !base::take(_savedPasswordValue).isEmpty();\n\tif (_passwordCheckRequestId) {\n\t\treturn;\n\t} else if (password.isEmpty()) {\n\t\t_passwordError.fire(QString());\n\t\treturn;\n\t}\n\tconst auto callback = [=](const Core::CloudPasswordResult &check) {\n\t\tsubmitPassword(check, password, submitSaved);\n\t};\n\tcheckPasswordHash(\n\t\t_passwordCheckRequestId,\n\t\tpasswordHashForAuth(bytes::make_span(password)),\n\t\tcallback);\n}\n\nvoid FormController::submitPassword(\n\t\tconst Core::CloudPasswordResult &check,\n\t\tconst QByteArray &password,\n\t\tbool submitSaved) {\n\t_passwordCheckRequestId = _api.request(MTPaccount_GetPasswordSettings(\n\t\tcheck.result\n\t)).handleFloodErrors(\n\t).done([=](const MTPaccount_PasswordSettings &result) {\n\t\tExpects(result.type() == mtpc_account_passwordSettings);\n\n\t\t_passwordCheckRequestId = 0;\n\t\t_savedPasswordValue = QByteArray();\n\t\tconst auto &data = result.c_account_passwordSettings();\n\t\t_password.confirmedEmail = qs(data.vemail().value_or_empty());\n\t\tif (const auto wrapped = data.vsecure_settings()) {\n\t\t\tconst auto &settings = wrapped->c_secureSecretSettings();\n\t\t\tconst auto algo = Core::ParseSecureSecretAlgo(\n\t\t\t\tsettings.vsecure_algo());\n\t\t\tif (v::is_null(algo)) {\n\t\t\t\t_view->showUpdateAppBox();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto hashForSecret = Core::ComputeSecureSecretHash(\n\t\t\t\talgo,\n\t\t\t\tbytes::make_span(password));\n\t\t\tvalidateSecureSecret(\n\t\t\t\tbytes::make_span(settings.vsecure_secret().v),\n\t\t\t\thashForSecret,\n\t\t\t\tbytes::make_span(password),\n\t\t\t\tsettings.vsecure_secret_id().v);\n\t\t\tif (!_secret.empty()) {\n\t\t\t\tauto saved = SavedCredentials();\n\t\t\t\tsaved.hashForAuth = base::take(_passwordCheckHash);\n\t\t\t\tsaved.hashForSecret = hashForSecret;\n\t\t\t\tsaved.secretId = _secretId;\n\t\t\t\tsession().data().rememberPassportCredentials(\n\t\t\t\t\tstd::move(saved),\n\t\t\t\t\tkRememberCredentialsDelay);\n\t\t\t}\n\t\t} else {\n\t\t\tvalidateSecureSecret(\n\t\t\t\tbytes::const_span(), // secure_secret\n\t\t\t\tbytes::const_span(), // hash for secret\n\t\t\t\tbytes::make_span(password),\n\t\t\t\t0); // secure_secret_id\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\t_passwordCheckRequestId = 0;\n\t\tif (error.type() == u\"SRP_ID_INVALID\"_q) {\n\t\t\thandleSrpIdInvalid(_passwordCheckRequestId);\n\t\t} else if (submitSaved) {\n\t\t\t// Force reload and show form.\n\t\t\t_password = PasswordSettings();\n\t\t\treloadPassword();\n\t\t} else if (MTP::IsFloodError(error)) {\n\t\t\t_passwordError.fire(tr::lng_flood_error(tr::now));\n\t\t} else if (error.type() == u\"PASSWORD_HASH_INVALID\"_q\n\t\t\t|| error.type() == u\"SRP_PASSWORD_CHANGED\"_q) {\n\t\t\t_passwordError.fire(tr::lng_passport_password_wrong(tr::now));\n\t\t} else {\n\t\t\t_passwordError.fire_copy(error.type());\n\t\t}\n\t}).send();\n}\n\nbool FormController::handleSrpIdInvalid(mtpRequestId &guard) {\n\tconst auto now = crl::now();\n\tif (_lastSrpIdInvalidTime > 0\n\t\t&& now - _lastSrpIdInvalidTime < Core::kHandleSrpIdInvalidTimeout) {\n\t\t_password.request.id = 0;\n\t\t_passwordError.fire(Lang::Hard::ServerError());\n\t\treturn false;\n\t} else {\n\t\t_lastSrpIdInvalidTime = now;\n\t\trequestPasswordData(guard);\n\t\treturn true;\n\t}\n}\n\nvoid FormController::passwordServerError() {\n\t_view->showCriticalError(Lang::Hard::ServerError());\n}\n\nvoid FormController::checkSavedPasswordSettings(\n\t\tconst SavedCredentials &credentials) {\n\tconst auto callback = [=](const Core::CloudPasswordResult &check) {\n\t\tcheckSavedPasswordSettings(check, credentials);\n\t};\n\tcheckPasswordHash(\n\t\t_passwordCheckRequestId,\n\t\tcredentials.hashForAuth,\n\t\tcallback);\n}\n\nvoid FormController::checkSavedPasswordSettings(\n\t\tconst Core::CloudPasswordResult &check,\n\t\tconst SavedCredentials &credentials) {\n\t_passwordCheckRequestId = _api.request(MTPaccount_GetPasswordSettings(\n\t\tcheck.result\n\t)).done([=](const MTPaccount_PasswordSettings &result) {\n\t\tExpects(result.type() == mtpc_account_passwordSettings);\n\n\t\t_passwordCheckRequestId = 0;\n\t\tconst auto &data = result.c_account_passwordSettings();\n\t\tif (const auto wrapped = data.vsecure_settings()) {\n\t\t\tconst auto &settings = wrapped->c_secureSecretSettings();\n\t\t\tconst auto algo = Core::ParseSecureSecretAlgo(\n\t\t\t\tsettings.vsecure_algo());\n\t\t\tif (v::is_null(algo)) {\n\t\t\t\t_view->showUpdateAppBox();\n\t\t\t\treturn;\n\t\t\t} else if (!settings.vsecure_secret().v.isEmpty()\n\t\t\t\t&& settings.vsecure_secret_id().v == credentials.secretId) {\n\t\t\t\t_password.confirmedEmail = qs(data.vemail().value_or_empty());\n\t\t\t\tvalidateSecureSecret(\n\t\t\t\t\tbytes::make_span(settings.vsecure_secret().v),\n\t\t\t\t\tcredentials.hashForSecret,\n\t\t\t\t\t{},\n\t\t\t\t\tsettings.vsecure_secret_id().v);\n\t\t\t}\n\t\t}\n\t\tif (_secret.empty()) {\n\t\t\tsession().data().forgetPassportCredentials();\n\t\t\tshowForm();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\t_passwordCheckRequestId = 0;\n\t\tif (error.type() != u\"SRP_ID_INVALID\"_q\n\t\t\t|| !handleSrpIdInvalid(_passwordCheckRequestId)) {\n\t\t} else {\n\t\t\tsession().data().forgetPassportCredentials();\n\t\t\tshowForm();\n\t\t}\n\t}).send();\n}\n\nvoid FormController::recoverPassword() {\n\tif (!_password.hasRecovery) {\n\t\t_view->show(Ui::MakeInformBox(tr::lng_signin_no_email_forgot()));\n\t\treturn;\n\t} else if (_recoverRequestId) {\n\t\treturn;\n\t}\n\t_recoverRequestId = _api.request(MTPauth_RequestPasswordRecovery(\n\t)).done([=](const MTPauth_PasswordRecovery &result) {\n\t\tExpects(result.type() == mtpc_auth_passwordRecovery);\n\n\t\t_recoverRequestId = 0;\n\n\t\tconst auto &data = result.c_auth_passwordRecovery();\n\t\tconst auto pattern = qs(data.vemail_pattern());\n\t\tauto fields = PasscodeBox::CloudFields{\n\t\t\t.mtp = PasscodeBox::CloudFields::Mtp {\n\t\t\t\t.newAlgo = _password.newAlgo,\n\t\t\t\t.newSecureSecretAlgo = _password.newSecureAlgo,\n\t\t\t},\n\t\t\t.hasRecovery = _password.hasRecovery,\n\t\t\t.pendingResetDate = _password.pendingResetDate,\n\t\t};\n\n\t\t// MSVC x64 (non-LTO) Release build fails with a linker error:\n\t\t// - unresolved external variant::variant(variant const &)\n\t\t// It looks like a MSVC bug and this works like a workaround.\n\t\tconst auto force = fields.mtp.newSecureSecretAlgo;\n\n\t\tconst auto box = _view->show(Box<RecoverBox>(\n\t\t\t&_controller->session().mtp(),\n\t\t\t&_controller->session(),\n\t\t\tpattern,\n\t\t\tfields));\n\t\tbox->newPasswordSet(\n\t\t) | rpl::on_next([=](const QByteArray &password) {\n\t\t\tif (password.isEmpty()) {\n\t\t\t\treloadPassword();\n\t\t\t} else {\n\t\t\t\treloadAndSubmitPassword(password);\n\t\t\t}\n\t\t}, box->lifetime());\n\n\t\tbox->recoveryExpired(\n\t\t) | rpl::on_next([=] {\n\t\t\tbox->closeBox();\n\t\t}, box->lifetime());\n\t}).fail([=](const MTP::Error &error) {\n\t\t_recoverRequestId = 0;\n\t\t_view->show(Ui::MakeInformBox(Lang::Hard::ServerError()\n\t\t\t+ '\\n'\n\t\t\t+ error.type()));\n\t}).send();\n}\n\nvoid FormController::reloadPassword() {\n\trequestPassword();\n}\n\nvoid FormController::reloadAndSubmitPassword(const QByteArray &password) {\n\t_savedPasswordValue = password;\n\trequestPassword();\n}\n\nvoid FormController::cancelPassword() {\n\tif (_passwordRequestId) {\n\t\treturn;\n\t}\n\t_passwordRequestId = _api.request(MTPaccount_CancelPasswordEmail(\n\t)).done([=] {\n\t\t_passwordRequestId = 0;\n\t\treloadPassword();\n\t}).fail([=] {\n\t\t_passwordRequestId = 0;\n\t\treloadPassword();\n\t}).send();\n}\n\nvoid FormController::validateSecureSecret(\n\t\tbytes::const_span encryptedSecret,\n\t\tbytes::const_span passwordHashForSecret,\n\t\tbytes::const_span passwordBytes,\n\t\tuint64 serverSecretId) {\n\tExpects(!passwordBytes.empty() || !passwordHashForSecret.empty());\n\n\tif (!passwordHashForSecret.empty() && !encryptedSecret.empty()) {\n\t\t_secret = DecryptSecureSecret(\n\t\t\tencryptedSecret,\n\t\t\tpasswordHashForSecret);\n\t\tif (_secret.empty()) {\n\t\t\t_secretId = 0;\n\t\t\tLOG((\"API Error: Failed to decrypt secure secret.\"));\n\t\t\tif (!passwordBytes.empty()) {\n\t\t\t\tsuggestReset(bytes::make_vector(passwordBytes));\n\t\t\t}\n\t\t\treturn;\n\t\t} else if (CountSecureSecretId(_secret) != serverSecretId) {\n\t\t\t_secret.clear();\n\t\t\t_secretId = 0;\n\t\t\tLOG((\"API Error: Wrong secure secret id.\"));\n\t\t\tif (!passwordBytes.empty()) {\n\t\t\t\tsuggestReset(bytes::make_vector(passwordBytes));\n\t\t\t}\n\t\t\treturn;\n\t\t} else {\n\t\t\t_secretId = serverSecretId;\n\t\t\tdecryptValues();\n\t\t}\n\t}\n\tif (_secret.empty()) {\n\t\tgenerateSecret(passwordBytes);\n\t}\n\t_secretReady.fire({});\n}\n\nvoid FormController::suggestReset(bytes::vector password) {\n\tfor (auto &[type, value] : _form.values) {\n//\t\tif (!value.data.original.isEmpty()) {\n\t\tresetValue(value);\n//\t\t}\n\t}\n\t_view->suggestReset([=] {\n\t\tconst auto callback = [=](const Core::CloudPasswordResult &check) {\n\t\t\tresetSecret(check, password);\n\t\t};\n\t\tcheckPasswordHash(\n\t\t\t_saveSecretRequestId,\n\t\t\tpasswordHashForAuth(bytes::make_span(password)),\n\t\t\tcallback);\n\t\t_secretReady.fire({});\n\t});\n}\n\nvoid FormController::resetSecret(\n\t\tconst Core::CloudPasswordResult &check,\n\t\tconst bytes::vector &password) {\n\tusing Flag = MTPDaccount_passwordInputSettings::Flag;\n\t_saveSecretRequestId = _api.request(MTPaccount_UpdatePasswordSettings(\n\t\tcheck.result,\n\t\tMTP_account_passwordInputSettings(\n\t\t\tMTP_flags(Flag::f_new_secure_settings),\n\t\t\tMTPPasswordKdfAlgo(), // new_algo\n\t\t\tMTPbytes(), // new_password_hash\n\t\t\tMTPstring(), // hint\n\t\t\tMTPstring(), // email\n\t\t\tMTP_secureSecretSettings(\n\t\t\t\tMTP_securePasswordKdfAlgoUnknown(), // secure_algo\n\t\t\t\tMTP_bytes(), // secure_secret\n\t\t\t\tMTP_long(0))) // secure_secret_id\n\t)).done([=] {\n\t\t_saveSecretRequestId = 0;\n\t\tgenerateSecret(password);\n\t}).fail([=](const MTP::Error &error) {\n\t\t_saveSecretRequestId = 0;\n\t\tif (error.type() != u\"SRP_ID_INVALID\"_q\n\t\t\t|| !handleSrpIdInvalid(_saveSecretRequestId)) {\n\t\t\tformFail(error.type());\n\t\t}\n\t}).send();\n}\n\nvoid FormController::decryptValues() {\n\tExpects(!_secret.empty());\n\n\tfor (auto &[type, value] : _form.values) {\n\t\tdecryptValue(value);\n\t}\n\tfillErrors();\n\tfillNativeFromFallback();\n}\n\nvoid FormController::fillErrors() {\n\tconst auto find = [&](const MTPSecureValueType &type) -> Value* {\n\t\tconst auto i = _form.values.find(ConvertType(type));\n\t\tif (i != end(_form.values)) {\n\t\t\treturn &i->second;\n\t\t}\n\t\tLOG((\"API Error: Value not found for error type.\"));\n\t\treturn nullptr;\n\t};\n\tconst auto scan = [&](\n\t\t\tValue &value,\n\t\t\tFileType type,\n\t\t\tbytes::const_span hash) -> File* {\n\t\tauto &list = value.files(type);\n\t\tconst auto i = ranges::find_if(list, [&](const File &scan) {\n\t\t\treturn !bytes::compare(hash, scan.hash);\n\t\t});\n\t\tif (i != end(list)) {\n\t\t\treturn &*i;\n\t\t}\n\t\tLOG((\"API Error: File not found for error value.\"));\n\t\treturn nullptr;\n\t};\n\tconst auto setSpecialScanError = [&](FileType type, auto &&data) {\n\t\tif (const auto value = find(data.vtype())) {\n\t\t\tif (value->requiresSpecialScan(type)) {\n\t\t\t\tconst auto i = value->specialScans.find(type);\n\t\t\t\tif (i != value->specialScans.end()) {\n\t\t\t\t\ti->second.error = qs(data.vtext());\n\t\t\t\t} else {\n\t\t\t\t\tLOG((\"API Error: \"\n\t\t\t\t\t\t\"Special scan %1 not found for error value.\"\n\t\t\t\t\t\t).arg(int(type)));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\tfor (const auto &error : std::as_const(_form.pendingErrors)) {\n\t\terror.match([&](const MTPDsecureValueError &data) {\n\t\t\tif (const auto value = find(data.vtype())) {\n\t\t\t\tif (CanHaveErrors(value->type)) {\n\t\t\t\t\tvalue->error = qs(data.vtext());\n\t\t\t\t}\n\t\t\t}\n\t\t}, [&](const MTPDsecureValueErrorData &data) {\n\t\t\tif (const auto value = find(data.vtype())) {\n\t\t\t\tconst auto key = qs(data.vfield());\n\t\t\t\tif (CanHaveErrors(value->type)\n\t\t\t\t\t&& !SkipFieldCheck(value, key)) {\n\t\t\t\t\tvalue->data.parsed.fields[key].error = qs(data.vtext());\n\t\t\t\t}\n\t\t\t}\n\t\t}, [&](const MTPDsecureValueErrorFile &data) {\n\t\t\tconst auto hash = bytes::make_span(data.vfile_hash().v);\n\t\t\tif (const auto value = find(data.vtype())) {\n\t\t\t\tif (const auto file = scan(*value, FileType::Scan, hash)) {\n\t\t\t\t\tif (value->requiresScan(FileType::Scan)) {\n\t\t\t\t\t\tfile->error = qs(data.vtext());\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}, [&](const MTPDsecureValueErrorFiles &data) {\n\t\t\tif (const auto value = find(data.vtype())) {\n\t\t\t\tif (value->requiresScan(FileType::Scan)) {\n\t\t\t\t\tvalue->fileMissingError(FileType::Scan)\n\t\t\t\t\t\t= qs(data.vtext());\n\t\t\t\t}\n\t\t\t}\n\t\t}, [&](const MTPDsecureValueErrorTranslationFile &data) {\n\t\t\tconst auto hash = bytes::make_span(data.vfile_hash().v);\n\t\t\tif (const auto value = find(data.vtype())) {\n\t\t\t\tconst auto file = scan(*value, FileType::Translation, hash);\n\t\t\t\tif (file && value->requiresScan(FileType::Translation)) {\n\t\t\t\t\tfile->error = qs(data.vtext());\n\t\t\t\t}\n\t\t\t}\n\t\t}, [&](const MTPDsecureValueErrorTranslationFiles &data) {\n\t\t\tif (const auto value = find(data.vtype())) {\n\t\t\t\tif (value->requiresScan(FileType::Translation)) {\n\t\t\t\t\tvalue->fileMissingError(FileType::Translation)\n\t\t\t\t\t\t= qs(data.vtext());\n\t\t\t\t}\n\t\t\t}\n\t\t}, [&](const MTPDsecureValueErrorFrontSide &data) {\n\t\t\tsetSpecialScanError(FileType::FrontSide, data);\n\t\t}, [&](const MTPDsecureValueErrorReverseSide &data) {\n\t\t\tsetSpecialScanError(FileType::ReverseSide, data);\n\t\t}, [&](const MTPDsecureValueErrorSelfie &data) {\n\t\t\tsetSpecialScanError(FileType::Selfie, data);\n\t\t});\n\t}\n}\n\nrpl::producer<EditDocumentCountry> FormController::preferredLanguage(\n\t\tconst QString &countryCode) {\n\tconst auto findLang = [=] {\n\t\tif (countryCode.isEmpty()) {\n\t\t\treturn QString();\n\t\t}\n\t\tauto &langs = _passportConfig.languagesByCountryCode;\n\t\tconst auto i = langs.find(countryCode);\n\t\treturn (i == end(langs)) ? QString() : i->second;\n\t};\n\treturn [=](auto consumer) {\n\t\tconst auto hash = _passportConfig.hash;\n\t\tif (hash) {\n\t\t\tconsumer.put_next({ countryCode, findLang() });\n\t\t\tconsumer.put_done();\n\t\t\treturn rpl::lifetime() ;\n\t\t}\n\n\t\t_api.request(MTPhelp_GetPassportConfig(\n\t\t\tMTP_int(hash)\n\t\t)).done([=](const MTPhelp_PassportConfig &result) {\n\t\t\tresult.match([&](const MTPDhelp_passportConfig &data) {\n\t\t\t\t_passportConfig.hash = data.vhash().v;\n\t\t\t\t_passportConfig.languagesByCountryCode = ParseConfig(\n\t\t\t\t\tdata.vcountries_langs().c_dataJSON().vdata().v);\n\t\t\t}, [](const MTPDhelp_passportConfigNotModified &data) {\n\t\t\t});\n\t\t\tconsumer.put_next({ countryCode, findLang() });\n\t\t\tconsumer.put_done();\n\t\t}).fail([=] {\n\t\t\tconsumer.put_next({ countryCode, QString() });\n\t\t\tconsumer.put_done();\n\t\t}).send();\n\n\t\treturn rpl::lifetime();\n\t};\n}\n\nvoid FormController::fillNativeFromFallback() {\n\t// Check if additional values (*_name_native) were requested.\n\tconst auto i = _form.values.find(Value::Type::PersonalDetails);\n\tif (i == end(_form.values) || !i->second.nativeNames) {\n\t\treturn;\n\t}\n\tauto values = i->second.data.parsed;\n\n\t// Check if additional values should be copied from fallback values.\n\tconst auto scheme = GetDocumentScheme(\n\t\tScope::Type::PersonalDetails,\n\t\tstd::nullopt,\n\t\ttrue,\n\t\t[=](const QString &code) { return preferredLanguage(code); });\n\tconst auto dependencyIt = values.fields.find(\n\t\tscheme.additionalDependencyKey);\n\tconst auto dependency = (dependencyIt == end(values.fields))\n\t\t? QString()\n\t\t: dependencyIt->second.text;\n\n\t// Copy additional values from fallback if they're not filled yet.\n\tusing Scheme = EditDocumentScheme;\n\tscheme.preferredLanguage(\n\t\tdependency\n\t) | rpl::map(\n\t\tscheme.additionalShown\n\t) | rpl::take(\n\t\t1\n\t) | rpl::on_next([=](Scheme::AdditionalVisibility v) {\n\t\tif (v != Scheme::AdditionalVisibility::OnlyIfError) {\n\t\t\treturn;\n\t\t}\n\t\tauto values = i->second.data.parsed;\n\t\tauto changed = false;\n\n\t\tfor (const auto &row : scheme.rows) {\n\t\t\tif (row.valueClass == Scheme::ValueClass::Additional) {\n\t\t\t\tconst auto nativeIt = values.fields.find(row.key);\n\t\t\t\tconst auto native = (nativeIt == end(values.fields))\n\t\t\t\t\t? QString()\n\t\t\t\t\t: nativeIt->second.text;\n\t\t\t\tif (!native.isEmpty()\n\t\t\t\t\t|| (nativeIt != end(values.fields)\n\t\t\t\t\t\t&& !nativeIt->second.error.isEmpty())) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto latinIt = values.fields.find(\n\t\t\t\t\trow.additionalFallbackKey);\n\t\t\t\tconst auto latin = (latinIt == end(values.fields))\n\t\t\t\t\t? QString()\n\t\t\t\t\t: latinIt->second.text;\n\t\t\t\tif (row.error(latin).has_value()) {\n\t\t\t\t\treturn;\n\t\t\t\t} else if (native != latin) {\n\t\t\t\t\tvalues.fields[row.key].text = latin;\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (changed) {\n\t\t\tstartValueEdit(&i->second);\n\t\t\tsaveValueEdit(&i->second, std::move(values));\n\t\t}\n\t}, _lifetime);\n}\n\nvoid FormController::decryptValue(Value &value) const {\n\tExpects(!_secret.empty());\n\n\tif (!validateValueSecrets(value)) {\n\t\tresetValue(value);\n\t\treturn;\n\t}\n\tif (!value.data.original.isEmpty()) {\n\t\tconst auto decrypted = DecryptData(\n\t\t\tbytes::make_span(value.data.original),\n\t\t\tvalue.data.hash,\n\t\t\tvalue.data.secret);\n\t\tif (decrypted.empty()) {\n\t\t\tLOG((\"API Error: Could not decrypt value fields.\"));\n\t\t\tresetValue(value);\n\t\t\treturn;\n\t\t}\n\t\tconst auto fields = DeserializeData(decrypted);\n\t\tvalue.data.parsed.fields.clear();\n\t\tfor (const auto &[key, text] : fields) {\n\t\t\tvalue.data.parsed.fields[key] = { text };\n\t\t}\n\t}\n}\n\nbool FormController::validateValueSecrets(Value &value) const {\n\tif (!value.data.original.isEmpty()) {\n\t\tvalue.data.secret = DecryptValueSecret(\n\t\t\tvalue.data.encryptedSecret,\n\t\t\t_secret,\n\t\t\tvalue.data.hash);\n\t\tif (value.data.secret.empty()) {\n\t\t\tLOG((\"API Error: Could not decrypt data secret.\"));\n\t\t\treturn false;\n\t\t}\n\t}\n\tconst auto validateFileSecret = [&](File &file) {\n\t\tfile.secret = DecryptValueSecret(\n\t\t\tfile.encryptedSecret,\n\t\t\t_secret,\n\t\t\tfile.hash);\n\t\tif (file.secret.empty()) {\n\t\t\tLOG((\"API Error: Could not decrypt file secret.\"));\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t};\n\tfor (auto &scan : value.files(FileType::Scan)) {\n\t\tif (!validateFileSecret(scan)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\tfor (auto &scan : value.files(FileType::Translation)) {\n\t\tif (!validateFileSecret(scan)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\tfor (auto &[type, scan] : value.specialScans) {\n\t\tif (!validateFileSecret(scan)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid FormController::resetValue(Value &value) const {\n\tvalue.fillDataFrom(Value(value.type));\n}\n\nrpl::producer<QString> FormController::passwordError() const {\n\treturn _passwordError.events();\n}\n\nconst PasswordSettings &FormController::passwordSettings() const {\n\treturn _password;\n}\n\nvoid FormController::uploadScan(\n\t\tnot_null<const Value*> value,\n\t\tFileType type,\n\t\tQByteArray &&content) {\n\tif (!canAddScan(value, type)) {\n\t\t_view->showToast(tr::lng_passport_scans_limit_reached(tr::now));\n\t\treturn;\n\t}\n\tconst auto nonconst = findValue(value);\n\tconst auto fileIndex = [&]() -> std::optional<int> {\n\t\tauto scanInEdit = EditFile(\n\t\t\t&session(),\n\t\t\tnonconst,\n\t\t\ttype,\n\t\t\tFile(),\n\t\t\tnullptr);\n\t\tif (type == FileType::Scan || type == FileType::Translation) {\n\t\t\tauto &list = nonconst->filesInEdit(type);\n\t\t\tlist.push_back(std::move(scanInEdit));\n\t\t\treturn list.size() - 1;\n\t\t}\n\t\tauto i = nonconst->specialScansInEdit.find(type);\n\t\tif (i != nonconst->specialScansInEdit.end()) {\n\t\t\ti->second = std::move(scanInEdit);\n\t\t} else {\n\t\t\ti = nonconst->specialScansInEdit.emplace(\n\t\t\t\ttype,\n\t\t\t\tstd::move(scanInEdit)).first;\n\t\t}\n\t\treturn std::nullopt;\n\t}();\n\tauto &scan = nonconst->fileInEdit(type, fileIndex);\n\tencryptFile(scan, std::move(content), [=](UploadScanData &&result) {\n\t\tuploadEncryptedFile(\n\t\t\tnonconst->fileInEdit(type, fileIndex),\n\t\t\tstd::move(result));\n\t});\n}\n\nvoid FormController::deleteScan(\n\t\tnot_null<const Value*> value,\n\t\tFileType type,\n\t\tstd::optional<int> fileIndex) {\n\tscanDeleteRestore(value, type, fileIndex, true);\n}\n\nvoid FormController::restoreScan(\n\t\tnot_null<const Value*> value,\n\t\tFileType type,\n\t\tstd::optional<int> fileIndex) {\n\tscanDeleteRestore(value, type, fileIndex, false);\n}\n\nvoid FormController::prepareFile(\n\t\tEditFile &file,\n\t\tconst QByteArray &content) {\n\tconst auto fileId = base::RandomValue<uint64>();\n\tfile.fields.size = content.size();\n\tfile.fields.id = fileId;\n\tfile.fields.dcId = _controller->session().mainDcId();\n\tfile.fields.secret = GenerateSecretBytes();\n\tfile.fields.date = base::unixtime::now();\n\tfile.fields.image = ReadImage(bytes::make_span(content));\n\tfile.fields.downloadStatus.set(LoadStatus::Status::Done);\n\n\t_scanUpdated.fire(&file);\n}\n\nvoid FormController::encryptFile(\n\t\tEditFile &file,\n\t\tQByteArray &&content,\n\t\tFn<void(UploadScanData &&result)> callback) {\n\tprepareFile(file, content);\n\n\tconst auto weak = std::weak_ptr<bool>(file.guard);\n\tcrl::async([\n\t\t=,\n\t\tfileId = file.fields.id,\n\t\tbytes = std::move(content),\n\t\tfileSecret = file.fields.secret\n\t] {\n\t\tauto data = EncryptData(\n\t\t\tbytes::make_span(bytes),\n\t\t\tfileSecret);\n\t\tauto result = UploadScanData();\n\t\tresult.fileId = fileId;\n\t\tresult.hash = std::move(data.hash);\n\t\tresult.bytes = std::move(data.bytes);\n\t\tresult.md5checksum.resize(32);\n\t\thashMd5Hex(\n\t\t\tresult.bytes.data(),\n\t\t\tresult.bytes.size(),\n\t\t\tresult.md5checksum.data());\n\t\tcrl::on_main([=, encrypted = std::move(result)]() mutable {\n\t\t\tif (weak.lock()) {\n\t\t\t\tcallback(std::move(encrypted));\n\t\t\t}\n\t\t});\n\t});\n}\n\nvoid FormController::scanDeleteRestore(\n\t\tnot_null<const Value*> value,\n\t\tFileType type,\n\t\tstd::optional<int> fileIndex,\n\t\tbool deleted) {\n\tconst auto nonconst = findValue(value);\n\tauto &scan = nonconst->fileInEdit(type, fileIndex);\n\tif (scan.deleted && !deleted) {\n\t\tif (!canAddScan(value, type)) {\n\t\t\t_view->showToast(tr::lng_passport_scans_limit_reached(tr::now));\n\t\t\treturn;\n\t\t}\n\t}\n\tscan.deleted = deleted;\n\t_scanUpdated.fire(&scan);\n}\n\nbool FormController::canAddScan(\n\t\tnot_null<const Value*> value,\n\t\tFileType type) const {\n\tconst auto limit = (type == FileType::Scan)\n\t\t? kDocumentScansLimit\n\t\t: (type == FileType::Translation)\n\t\t? kTranslationScansLimit\n\t\t: -1;\n\tif (limit < 0) {\n\t\treturn true;\n\t}\n\tconst auto scansCount = ranges::count_if(\n\t\tvalue->filesInEdit(type),\n\t\t[](const EditFile &scan) { return !scan.deleted; });\n\treturn (scansCount < limit);\n}\n\nvoid FormController::subscribeToUploader() {\n\tif (_uploaderSubscriptions) {\n\t\treturn;\n\t}\n\n\tusing namespace Storage;\n\n\tsession().uploader().secureReady(\n\t) | rpl::on_next([=](const UploadSecureDone &data) {\n\t\tscanUploadDone(data);\n\t}, _uploaderSubscriptions);\n\n\tsession().uploader().secureProgress(\n\t) | rpl::on_next([=](const UploadSecureProgress &data) {\n\t\tscanUploadProgress(data);\n\t}, _uploaderSubscriptions);\n\n\tsession().uploader().secureFailed(\n\t) | rpl::on_next([=](const FullMsgId &fullId) {\n\t\tscanUploadFail(fullId);\n\t}, _uploaderSubscriptions);\n}\n\nvoid FormController::uploadEncryptedFile(\n\t\tEditFile &file,\n\t\tUploadScanData &&data) {\n\tsubscribeToUploader();\n\n\tfile.uploadData = UploadScanDataPointer(\n\t\t&session(),\n\t\tstd::make_unique<UploadScanData>(std::move(data)));\n\n\tauto prepared = MakePreparedFile({\n\t\t.id = file.uploadData->fileId,\n\t\t.type = SendMediaType::Secure,\n\t});\n\tprepared->content = QByteArray::fromRawData(\n\t\treinterpret_cast<char*>(file.uploadData->bytes.data()),\n\t\tfile.uploadData->bytes.size());\n\tprepared->setFileData(prepared->content);\n\tprepared->filemd5 = file.uploadData->md5checksum;\n\n\tfile.uploadData->fullId = FullMsgId(\n\t\tsession().userPeerId(),\n\t\tsession().data().nextLocalMessageId());\n\tfile.uploadData->status.set(LoadStatus::Status::InProgress, 0);\n\tsession().uploader().upload(\n\t\tfile.uploadData->fullId,\n\t\tstd::move(prepared));\n}\n\nvoid FormController::scanUploadDone(const Storage::UploadSecureDone &data) {\n\tif (const auto file = findEditFile(data.fullId)) {\n\t\tAssert(file->uploadData != nullptr);\n\t\tAssert(file->uploadData->fileId == data.fileId);\n\n\t\tfile->uploadData->partsCount = data.partsCount;\n\t\tfile->fields.hash = std::move(file->uploadData->hash);\n\t\tfile->fields.encryptedSecret = EncryptValueSecret(\n\t\t\tfile->fields.secret,\n\t\t\t_secret,\n\t\t\tfile->fields.hash);\n\t\tfile->uploadData->fullId = FullMsgId();\n\t\tfile->uploadData->status.set(LoadStatus::Status::Done);\n\n\t\t_scanUpdated.fire(file);\n\t}\n}\n\nvoid FormController::scanUploadProgress(\n\t\tconst Storage::UploadSecureProgress &data) {\n\tif (const auto file = findEditFile(data.fullId)) {\n\t\tAssert(file->uploadData != nullptr);\n\n\t\tfile->uploadData->status.set(\n\t\t\tLoadStatus::Status::InProgress,\n\t\t\tdata.offset);\n\n\t\t_scanUpdated.fire(file);\n\t}\n}\n\nvoid FormController::scanUploadFail(const FullMsgId &fullId) {\n\tif (const auto file = findEditFile(fullId)) {\n\t\tAssert(file->uploadData != nullptr);\n\n\t\tfile->uploadData->status.set(LoadStatus::Status::Failed);\n\n\t\t_scanUpdated.fire(file);\n\t}\n}\n\nrpl::producer<> FormController::secretReadyEvents() const {\n\treturn _secretReady.events();\n}\n\nQString FormController::defaultEmail() const {\n\treturn _password.confirmedEmail;\n}\n\nQString FormController::defaultPhoneNumber() const {\n\treturn session().user()->phone();\n}\n\nauto FormController::scanUpdated() const\n-> rpl::producer<not_null<const EditFile*>> {\n\treturn _scanUpdated.events();\n}\n\nauto FormController::valueSaveFinished() const\n-> rpl::producer<not_null<const Value*>> {\n\treturn _valueSaveFinished.events();\n}\n\nauto FormController::verificationNeeded() const\n-> rpl::producer<not_null<const Value*>> {\n\treturn _verificationNeeded.events();\n}\n\nauto FormController::verificationUpdate() const\n-> rpl::producer<not_null<const Value*>> {\n\treturn _verificationUpdate.events();\n}\n\nvoid FormController::verify(\n\t\tnot_null<const Value*> value,\n\t\tconst QString &code) {\n\tif (value->verification.requestId) {\n\t\treturn;\n\t}\n\tconst auto nonconst = findValue(value);\n\tconst auto prepared = code.trimmed();\n\tAssert(nonconst->verification.codeLength != 0);\n\tverificationError(nonconst, QString());\n\tif (nonconst->verification.codeLength > 0\n\t\t&& nonconst->verification.codeLength != prepared.size()) {\n\t\tverificationError(nonconst, tr::lng_signin_wrong_code(tr::now));\n\t\treturn;\n\t} else if (prepared.isEmpty()) {\n\t\tverificationError(nonconst, tr::lng_signin_wrong_code(tr::now));\n\t\treturn;\n\t}\n\tnonconst->verification.requestId = [&] {\n\t\tswitch (nonconst->type) {\n\t\tcase Value::Type::Phone:\n\t\t\treturn _api.request(MTPaccount_VerifyPhone(\n\t\t\t\tMTP_string(getPhoneFromValue(nonconst)),\n\t\t\t\tMTP_string(nonconst->verification.phoneCodeHash),\n\t\t\t\tMTP_string(prepared)\n\t\t\t)).done([=](const MTPBool &result) {\n\t\t\t\tsavePlainTextValue(nonconst);\n\t\t\t\tclearValueVerification(nonconst);\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tnonconst->verification.requestId = 0;\n\t\t\t\tif (error.type() == u\"PHONE_CODE_INVALID\"_q) {\n\t\t\t\t\tverificationError(\n\t\t\t\t\t\tnonconst,\n\t\t\t\t\t\ttr::lng_signin_wrong_code(tr::now));\n\t\t\t\t} else {\n\t\t\t\t\tverificationError(nonconst, error.type());\n\t\t\t\t}\n\t\t\t}).send();\n\t\tcase Value::Type::Email:\n\t\t\treturn _api.request(MTPaccount_VerifyEmail(\n\t\t\t\tMTP_emailVerifyPurposePassport(),\n\t\t\t\tMTP_emailVerificationCode(MTP_string(prepared))\n\t\t\t)).done([=](const MTPaccount_EmailVerified &result) {\n\t\t\t\tsavePlainTextValue(nonconst);\n\t\t\t\tclearValueVerification(nonconst);\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\tnonconst->verification.requestId = 0;\n\t\t\t\tif (error.type() == u\"CODE_INVALID\"_q) {\n\t\t\t\t\tverificationError(\n\t\t\t\t\t\tnonconst,\n\t\t\t\t\t\ttr::lng_signin_wrong_code(tr::now));\n\t\t\t\t} else {\n\t\t\t\t\tverificationError(nonconst, error.type());\n\t\t\t\t}\n\t\t\t}).send();\n\t\t}\n\t\tUnexpected(\"Type in FormController::verify().\");\n\t}();\n}\n\nvoid FormController::verificationError(\n\t\tnot_null<Value*> value,\n\t\tconst QString &text) {\n\tvalue->verification.error = text;\n\t_verificationUpdate.fire_copy(value);\n}\n\nconst Form &FormController::form() const {\n\treturn _form;\n}\n\nnot_null<Value*> FormController::findValue(not_null<const Value*> value) {\n\tconst auto i = _form.values.find(value->type);\n\tAssert(i != end(_form.values));\n\tconst auto result = &i->second;\n\n\tEnsures(result == value);\n\treturn result;\n}\n\nvoid FormController::startValueEdit(not_null<const Value*> value) {\n\tconst auto nonconst = findValue(value);\n\t++nonconst->editScreens;\n\tif (nonconst->saving()) {\n\t\treturn;\n\t}\n\tfor (auto &scan : nonconst->files(FileType::Scan)) {\n\t\tloadFile(scan);\n\t}\n\tif (nonconst->translationRequired) {\n\t\tfor (auto &scan : nonconst->files(FileType::Translation)) {\n\t\t\tloadFile(scan);\n\t\t}\n\t}\n\tfor (auto &[type, scan] : nonconst->specialScans) {\n\t\tif (nonconst->requiresSpecialScan(type)) {\n\t\t\tloadFile(scan);\n\t\t}\n\t}\n\tnonconst->saveInEdit(&session());\n}\n\nvoid FormController::loadFile(File &file) {\n\tif (!file.image.isNull()) {\n\t\tfile.downloadStatus.set(LoadStatus::Status::Done);\n\t\treturn;\n\t}\n\n\tconst auto key = FileKey{ file.id };\n\tconst auto i = _fileLoaders.find(key);\n\tif (i != _fileLoaders.end()) {\n\t\treturn;\n\t}\n\tfile.downloadStatus.set(LoadStatus::Status::InProgress, 0);\n\tconst auto &[j, ok] = _fileLoaders.emplace(\n\t\tkey,\n\t\tstd::make_unique<mtpFileLoader>(\n\t\t\t&_controller->session(),\n\t\t\tStorageFileLocation(\n\t\t\t\tfile.dcId,\n\t\t\t\tsession().userId(),\n\t\t\t\tMTP_inputSecureFileLocation(\n\t\t\t\t\tMTP_long(file.id),\n\t\t\t\t\tMTP_long(file.accessHash))),\n\t\t\tData::FileOrigin(),\n\t\t\tSecureFileLocation,\n\t\t\tQString(),\n\t\t\tfile.size,\n\t\t\tfile.size,\n\t\t\tLoadToCacheAsWell,\n\t\t\tLoadFromCloudOrLocal,\n\t\t\tfalse,\n\t\t\tData::kImageCacheTag));\n\tconst auto loader = j->second.get();\n\tloader->updates(\n\t) | rpl::on_next_error_done([=] {\n\t\tfileLoadProgress(key, loader->currentOffset());\n\t}, [=](FileLoader::Error error) {\n\t\tfileLoadFail(key);\n\t}, [=] {\n\t\tfileLoadDone(key, loader->bytes());\n\t}, loader->lifetime());\n\tloader->start();\n}\n\nvoid FormController::fileLoadDone(FileKey key, const QByteArray &bytes) {\n\tif (const auto &[value, file] = findFile(key); file != nullptr) {\n\t\tconst auto decrypted = DecryptData(\n\t\t\tbytes::make_span(bytes),\n\t\t\tfile->hash,\n\t\t\tfile->secret);\n\t\tif (decrypted.empty()) {\n\t\t\tfileLoadFail(key);\n\t\t\treturn;\n\t\t}\n\t\tfile->downloadStatus.set(LoadStatus::Status::Done);\n\t\tfile->image = ReadImage(gsl::make_span(decrypted));\n\t\tif (const auto fileInEdit = findEditFile(key)) {\n\t\t\tfileInEdit->fields.image = file->image;\n\t\t\tfileInEdit->fields.downloadStatus = file->downloadStatus;\n\t\t\t_scanUpdated.fire(fileInEdit);\n\t\t}\n\t}\n}\n\nvoid FormController::fileLoadProgress(FileKey key, int offset) {\n\tif (const auto &[value, file] = findFile(key); file != nullptr) {\n\t\tfile->downloadStatus.set(LoadStatus::Status::InProgress, offset);\n\t\tif (const auto fileInEdit = findEditFile(key)) {\n\t\t\tfileInEdit->fields.downloadStatus = file->downloadStatus;\n\t\t\t_scanUpdated.fire(fileInEdit);\n\t\t}\n\t}\n}\n\nvoid FormController::fileLoadFail(FileKey key) {\n\tif (const auto &[value, file] = findFile(key); file != nullptr) {\n\t\tfile->downloadStatus.set(LoadStatus::Status::Failed);\n\t\tif (const auto fileInEdit = findEditFile(key)) {\n\t\t\tfileInEdit->fields.downloadStatus = file->downloadStatus;\n\t\t\t_scanUpdated.fire(fileInEdit);\n\t\t}\n\t}\n}\n\nvoid FormController::cancelValueEdit(not_null<const Value*> value) {\n\tExpects(value->editScreens > 0);\n\n\tconst auto nonconst = findValue(value);\n\t--nonconst->editScreens;\n\tclearValueEdit(nonconst);\n}\n\nvoid FormController::valueEditFailed(not_null<Value*> value) {\n\tExpects(!value->saving());\n\n\tif (value->editScreens == 0) {\n\t\tclearValueEdit(value);\n\t}\n}\n\nvoid FormController::clearValueEdit(not_null<Value*> value) {\n\tif (value->saving()) {\n\t\treturn;\n\t}\n\tvalue->clearEditData();\n}\n\nvoid FormController::cancelValueVerification(not_null<const Value*> value) {\n\tconst auto nonconst = findValue(value);\n\tclearValueVerification(nonconst);\n\tif (!nonconst->saving()) {\n\t\tvalueEditFailed(nonconst);\n\t}\n}\n\nvoid FormController::clearValueVerification(not_null<Value*> value) {\n\tconst auto was = (value->verification.codeLength != 0);\n\tif (const auto requestId = base::take(value->verification.requestId)) {\n\t\t_api.request(requestId).cancel();\n\t}\n\tvalue->verification = Verification();\n\tif (was) {\n\t\t_verificationUpdate.fire_copy(value);\n\t}\n}\n\nbool FormController::isEncryptedValue(Value::Type type) const {\n\treturn (type != Value::Type::Phone && type != Value::Type::Email);\n}\n\nvoid FormController::saveValueEdit(\n\t\tnot_null<const Value*> value,\n\t\tValueMap &&data) {\n\tif (value->saving() || _submitRequestId) {\n\t\treturn;\n\t}\n\n\t// If we didn't change anything, we don't send save request\n\t// and we don't reset value->error/[scan|translation]MissingError.\n\t// Otherwise we reset them after save by re-parsing the value.\n\tconst auto nonconst = findValue(value);\n\tif (!ValueChanged(nonconst, data)) {\n\t\tnonconst->saveRequestId = -1;\n\t\tcrl::on_main(this, [=] {\n\t\t\tnonconst->clearEditData();\n\t\t\tnonconst->saveRequestId = 0;\n\t\t\t_valueSaveFinished.fire_copy(nonconst);\n\t\t});\n\t\treturn;\n\t}\n\tApplyDataChanges(nonconst->data, std::move(data));\n\n\tif (isEncryptedValue(nonconst->type)) {\n\t\tsaveEncryptedValue(nonconst);\n\t} else {\n\t\tsavePlainTextValue(nonconst);\n\t}\n}\n\nvoid FormController::deleteValueEdit(not_null<const Value*> value) {\n\tif (value->saving() || _submitRequestId) {\n\t\treturn;\n\t}\n\n\tconst auto nonconst = findValue(value);\n\tnonconst->saveRequestId = _api.request(MTPaccount_DeleteSecureValue(\n\t\tMTP_vector<MTPSecureValueType>(1, ConvertType(nonconst->type))\n\t)).done([=] {\n\t\tresetValue(*nonconst);\n\t\t_valueSaveFinished.fire_copy(value);\n\t}).fail([=](const MTP::Error &error) {\n\t\tnonconst->saveRequestId = 0;\n\t\tvalueSaveShowError(nonconst, error);\n\t}).send();\n}\n\nvoid FormController::saveEncryptedValue(not_null<Value*> value) {\n\tExpects(isEncryptedValue(value->type));\n\n\tif (_secret.empty()) {\n\t\t_secretCallbacks.push_back([=] {\n\t\t\tsaveEncryptedValue(value);\n\t\t});\n\t\treturn;\n\t}\n\n\tconst auto wrapFile = [](const EditFile &file) {\n\t\tif (const auto uploadData = file.uploadData.get()) {\n\t\t\treturn MTP_inputSecureFileUploaded(\n\t\t\t\tMTP_long(file.fields.id),\n\t\t\t\tMTP_int(uploadData->partsCount),\n\t\t\t\tMTP_bytes(uploadData->md5checksum),\n\t\t\t\tMTP_bytes(file.fields.hash),\n\t\t\t\tMTP_bytes(file.fields.encryptedSecret));\n\t\t}\n\t\treturn MTP_inputSecureFile(\n\t\t\tMTP_long(file.fields.id),\n\t\t\tMTP_long(file.fields.accessHash));\n\t};\n\tconst auto wrapList = [&](not_null<const Value*> value, FileType type) {\n\t\tconst auto &list = value->filesInEdit(type);\n\t\tauto result = QVector<MTPInputSecureFile>();\n\t\tresult.reserve(list.size());\n\t\tfor (const auto &scan : value->filesInEdit(type)) {\n\t\t\tif (scan.deleted) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tresult.push_back(wrapFile(scan));\n\t\t}\n\t\treturn result;\n\t};\n\n\tconst auto files = wrapList(value, FileType::Scan);\n\tconst auto translations = wrapList(value, FileType::Translation);\n\n\tif (value->data.secret.empty()) {\n\t\tvalue->data.secret = GenerateSecretBytes();\n\t}\n\tconst auto encryptedData = EncryptData(\n\t\tSerializeData(GetTexts(value->data.parsedInEdit)),\n\t\tvalue->data.secret);\n\tvalue->data.hashInEdit = encryptedData.hash;\n\tvalue->data.encryptedSecretInEdit = EncryptValueSecret(\n\t\tvalue->data.secret,\n\t\t_secret,\n\t\tvalue->data.hashInEdit);\n\n\tconst auto hasSpecialFile = [&](FileType type) {\n\t\tconst auto i = value->specialScansInEdit.find(type);\n\t\treturn (i != end(value->specialScansInEdit) && !i->second.deleted);\n\t};\n\tconst auto specialFile = [&](FileType type) {\n\t\tconst auto i = value->specialScansInEdit.find(type);\n\t\treturn (i != end(value->specialScansInEdit) && !i->second.deleted)\n\t\t\t? wrapFile(i->second)\n\t\t\t: MTPInputSecureFile();\n\t};\n\tconst auto frontSide = specialFile(FileType::FrontSide);\n\tconst auto reverseSide = specialFile(FileType::ReverseSide);\n\tconst auto selfie = specialFile(FileType::Selfie);\n\n\tconst auto type = ConvertType(value->type);\n\tconst auto flags = (value->data.parsedInEdit.fields.empty()\n\t\t\t? MTPDinputSecureValue::Flag(0)\n\t\t\t: MTPDinputSecureValue::Flag::f_data)\n\t\t| (hasSpecialFile(FileType::FrontSide)\n\t\t\t? MTPDinputSecureValue::Flag::f_front_side\n\t\t\t: MTPDinputSecureValue::Flag(0))\n\t\t| (hasSpecialFile(FileType::ReverseSide)\n\t\t\t? MTPDinputSecureValue::Flag::f_reverse_side\n\t\t\t: MTPDinputSecureValue::Flag(0))\n\t\t| (hasSpecialFile(FileType::Selfie)\n\t\t\t? MTPDinputSecureValue::Flag::f_selfie\n\t\t\t: MTPDinputSecureValue::Flag(0))\n\t\t| (translations.empty()\n\t\t\t? MTPDinputSecureValue::Flag(0)\n\t\t\t: MTPDinputSecureValue::Flag::f_translation)\n\t\t| (files.empty()\n\t\t\t? MTPDinputSecureValue::Flag(0)\n\t\t\t: MTPDinputSecureValue::Flag::f_files);\n\tAssert(flags != MTPDinputSecureValue::Flags(0));\n\n\tsendSaveRequest(value, MTP_inputSecureValue(\n\t\tMTP_flags(flags),\n\t\ttype,\n\t\tMTP_secureData(\n\t\t\tMTP_bytes(encryptedData.bytes),\n\t\t\tMTP_bytes(value->data.hashInEdit),\n\t\t\tMTP_bytes(value->data.encryptedSecretInEdit)),\n\t\tfrontSide,\n\t\treverseSide,\n\t\tselfie,\n\t\tMTP_vector<MTPInputSecureFile>(translations),\n\t\tMTP_vector<MTPInputSecureFile>(files),\n\t\tMTPSecurePlainData()));\n}\n\nvoid FormController::savePlainTextValue(not_null<Value*> value) {\n\tExpects(!isEncryptedValue(value->type));\n\n\tconst auto text = getPlainTextFromValue(value);\n\tconst auto type = [&] {\n\t\tswitch (value->type) {\n\t\tcase Value::Type::Phone: return MTP_secureValueTypePhone();\n\t\tcase Value::Type::Email: return MTP_secureValueTypeEmail();\n\t\t}\n\t\tUnexpected(\"Value type in savePlainTextValue().\");\n\t}();\n\tconst auto plain = [&] {\n\t\tswitch (value->type) {\n\t\tcase Value::Type::Phone: return MTP_securePlainPhone;\n\t\tcase Value::Type::Email: return MTP_securePlainEmail;\n\t\t}\n\t\tUnexpected(\"Value type in savePlainTextValue().\");\n\t}();\n\tsendSaveRequest(value, MTP_inputSecureValue(\n\t\tMTP_flags(MTPDinputSecureValue::Flag::f_plain_data),\n\t\ttype,\n\t\tMTPSecureData(),\n\t\tMTPInputSecureFile(),\n\t\tMTPInputSecureFile(),\n\t\tMTPInputSecureFile(),\n\t\tMTPVector<MTPInputSecureFile>(),\n\t\tMTPVector<MTPInputSecureFile>(),\n\t\tplain(MTP_string(text))));\n}\n\nvoid FormController::sendSaveRequest(\n\t\tnot_null<Value*> value,\n\t\tconst MTPInputSecureValue &data) {\n\tExpects(value->saveRequestId == 0);\n\n\tvalue->saveRequestId = _api.request(MTPaccount_SaveSecureValue(\n\t\tdata,\n\t\tMTP_long(_secretId)\n\t)).done([=](const MTPSecureValue &result) {\n\t\tauto scansInEdit = value->takeAllFilesInEdit();\n\n\t\tauto refreshed = parseValue(result, scansInEdit);\n\t\tdecryptValue(refreshed);\n\t\tvalue->fillDataFrom(std::move(refreshed));\n\n\t\t_valueSaveFinished.fire_copy(value);\n\t}).fail([=](const MTP::Error &error) {\n\t\tvalue->saveRequestId = 0;\n\t\tconst auto code = error.type();\n\t\tif (handleAppUpdateError(code)) {\n\t\t} else if (code == u\"PHONE_VERIFICATION_NEEDED\"_q) {\n\t\t\tif (value->type == Value::Type::Phone) {\n\t\t\t\tstartPhoneVerification(value);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else if (code == u\"PHONE_NUMBER_INVALID\"_q) {\n\t\t\tif (value->type == Value::Type::Phone) {\n\t\t\t\tvalue->data.parsedInEdit.fields[\"value\"].error\n\t\t\t\t\t= tr::lng_bad_phone(tr::now);\n\t\t\t\tvalueSaveFailed(value);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else if (code == u\"EMAIL_VERIFICATION_NEEDED\"_q) {\n\t\t\tif (value->type == Value::Type::Email) {\n\t\t\t\tstartEmailVerification(value);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else if (code == u\"EMAIL_INVALID\"_q) {\n\t\t\tif (value->type == Value::Type::Email) {\n\t\t\t\tvalue->data.parsedInEdit.fields[\"value\"].error\n\t\t\t\t\t= tr::lng_cloud_password_bad_email(tr::now);\n\t\t\t\tvalueSaveFailed(value);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (SaveErrorRequiresRestart(code)) {\n\t\t\tsuggestRestart();\n\t\t} else {\n\t\t\tvalueSaveShowError(value, error);\n\t\t}\n\t}).send();\n}\n\nQString FormController::getPhoneFromValue(\n\t\tnot_null<const Value*> value) const {\n\tExpects(value->type == Value::Type::Phone);\n\n\treturn getPlainTextFromValue(value);\n}\n\nQString FormController::getEmailFromValue(\n\t\tnot_null<const Value*> value) const {\n\tExpects(value->type == Value::Type::Email);\n\n\treturn getPlainTextFromValue(value);\n}\n\nQString FormController::getPlainTextFromValue(\n\t\tnot_null<const Value*> value) const {\n\tExpects(value->type == Value::Type::Phone\n\t\t|| value->type == Value::Type::Email);\n\n\tconst auto i = value->data.parsedInEdit.fields.find(\"value\");\n\tAssert(i != end(value->data.parsedInEdit.fields));\n\treturn i->second.text;\n}\n\nvoid FormController::startPhoneVerification(not_null<Value*> value) {\n\tvalue->verification.requestId = _api.request(MTPaccount_SendVerifyPhoneCode(\n\t\tMTP_string(getPhoneFromValue(value)),\n\t\tMTP_codeSettings(\n\t\t\tMTP_flags(0),\n\t\t\tMTPVector<MTPbytes>(),\n\t\t\tMTPstring(),\n\t\t\tMTPBool())\n\t)).done([=](const MTPauth_SentCode &result) {\n\t\tresult.match([&](const MTPDauth_sentCode &data) {\n\t\t\tconst auto next = data.vnext_type();\n\t\t\tconst auto timeout = data.vtimeout();\n\t\t\tvalue->verification.requestId = 0;\n\t\t\tvalue->verification.phoneCodeHash = qs(data.vphone_code_hash());\n\t\t\tvalue->verification.fragmentUrl = QString();\n\t\t\tconst auto bad = [](const char *type) {\n\t\t\t\tLOG((\"API Error: Should not be '%1' \"\n\t\t\t\t\t\"in FormController::startPhoneVerification.\").arg(type));\n\t\t\t};\n\t\t\tdata.vtype().match([&](const MTPDauth_sentCodeTypeApp &) {\n\t\t\t\tLOG((\"API Error: sentCodeTypeApp not expected \"\n\t\t\t\t\t\"in FormController::startPhoneVerification.\"));\n\t\t\t}, [&](const MTPDauth_sentCodeTypeCall &data) {\n\t\t\t\tvalue->verification.codeLength = (data.vlength().v > 0)\n\t\t\t\t\t? data.vlength().v\n\t\t\t\t\t: -1;\n\t\t\t\tvalue->verification.call = std::make_unique<Ui::SentCodeCall>(\n\t\t\t\t\t[=] { requestPhoneCall(value); },\n\t\t\t\t\t[=] { _verificationUpdate.fire_copy(value); });\n\t\t\t\tvalue->verification.call->setStatus(\n\t\t\t\t\t{ Ui::SentCodeCall::State::Called, 0 });\n\t\t\t\tif (next) {\n\t\t\t\t\tLOG((\"API Error: next_type is not supported for calls.\"));\n\t\t\t\t}\n\t\t\t}, [&](const MTPDauth_sentCodeTypeSms &data) {\n\t\t\t\tvalue->verification.codeLength = (data.vlength().v > 0)\n\t\t\t\t\t? data.vlength().v\n\t\t\t\t\t: -1;\n\t\t\t\tif (next && next->type() == mtpc_auth_codeTypeCall) {\n\t\t\t\t\tvalue->verification.call = std::make_unique<Ui::SentCodeCall>(\n\t\t\t\t\t\t[=] { requestPhoneCall(value); },\n\t\t\t\t\t\t[=] { _verificationUpdate.fire_copy(value); });\n\t\t\t\t\tvalue->verification.call->setStatus({\n\t\t\t\t\t\tUi::SentCodeCall::State::Waiting,\n\t\t\t\t\t\ttimeout.value_or(60),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}, [&](const MTPDauth_sentCodeTypeFragmentSms &data) {\n\t\t\t\tvalue->verification.codeLength = data.vlength().v;\n\t\t\t\tvalue->verification.fragmentUrl = qs(data.vurl());\n\t\t\t\tvalue->verification.call = nullptr;\n\t\t\t}, [&](const MTPDauth_sentCodeTypeFlashCall &) {\n\t\t\t\tbad(\"FlashCall\");\n\t\t\t}, [&](const MTPDauth_sentCodeTypeMissedCall &) {\n\t\t\t\tbad(\"MissedCall\");\n\t\t\t}, [&](const MTPDauth_sentCodeTypeFirebaseSms &) {\n\t\t\t\tbad(\"FirebaseSms\");\n\t\t\t}, [&](const MTPDauth_sentCodeTypeEmailCode &) {\n\t\t\t\tbad(\"EmailCode\");\n\t\t\t}, [&](const MTPDauth_sentCodeTypeSmsWord &) {\n\t\t\t\tbad(\"SmsWord\");\n\t\t\t}, [&](const MTPDauth_sentCodeTypeSmsPhrase &) {\n\t\t\t\tbad(\"SmsPhrase\");\n\t\t\t}, [&](const MTPDauth_sentCodeTypeSetUpEmailRequired &) {\n\t\t\t\tbad(\"SetUpEmailRequired\");\n\t\t\t});\n\t\t\t_verificationNeeded.fire_copy(value);\n\t\t}, [](const MTPDauth_sentCodeSuccess &) {\n\t\t\tLOG((\"API Error: Unexpected auth.sentCodeSuccess \"\n\t\t\t\t\"(FormController::startPhoneVerification).\"));\n\t\t}, [](const MTPDauth_sentCodePaymentRequired &) {\n\t\t\tLOG((\"API Error: Unexpected auth.sentCodePaymentRequired \"\n\t\t\t\t\"(FormController::startPhoneVerification).\"));\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\tvalue->verification.requestId = 0;\n\t\tvalueSaveShowError(value, error);\n\t}).send();\n}\n\nvoid FormController::startEmailVerification(not_null<Value*> value) {\n\tvalue->verification.requestId = _api.request(\n\t\tMTPaccount_SendVerifyEmailCode(\n\t\t\tMTP_emailVerifyPurposePassport(),\n\t\t\tMTP_string(getEmailFromValue(value)))\n\t).done([=](const MTPaccount_SentEmailCode &result) {\n\t\tExpects(result.type() == mtpc_account_sentEmailCode);\n\n\t\tvalue->verification.requestId = 0;\n\t\tconst auto &data = result.c_account_sentEmailCode();\n\t\tvalue->verification.codeLength = (data.vlength().v > 0)\n\t\t\t? data.vlength().v\n\t\t\t: -1;\n\t\t_verificationNeeded.fire_copy(value);\n\t}).fail([=](const MTP::Error &error) {\n\t\tvalueSaveShowError(value, error);\n\t}).send();\n}\n\n\nvoid FormController::requestPhoneCall(not_null<Value*> value) {\n\tExpects(value->verification.call != nullptr);\n\n\tvalue->verification.call->setStatus(\n\t\t{ Ui::SentCodeCall::State::Calling, 0 });\n\t_api.request(MTPauth_ResendCode(\n\t\tMTP_flags(0),\n\t\tMTP_string(getPhoneFromValue(value)),\n\t\tMTP_string(value->verification.phoneCodeHash),\n\t\tMTPstring() // reason\n\t)).done([=] {\n\t\tvalue->verification.call->callDone();\n\t}).send();\n}\n\nvoid FormController::valueSaveShowError(\n\t\tnot_null<Value*> value,\n\t\tconst MTP::Error &error) {\n\t_view->show(Ui::MakeInformBox(\n\t\tLang::Hard::SecureSaveError() + \"\\n\" + error.type()));\n\tvalueSaveFailed(value);\n}\n\nvoid FormController::valueSaveFailed(not_null<Value*> value) {\n\tvalueEditFailed(value);\n\t_valueSaveFinished.fire_copy(value);\n}\n\nvoid FormController::generateSecret(bytes::const_span password) {\n\tExpects(!password.empty());\n\n\tif (_saveSecretRequestId) {\n\t\treturn;\n\t}\n\tauto secret = GenerateSecretBytes();\n\n\tauto saved = SavedCredentials();\n\tsaved.hashForAuth = _passwordCheckHash;\n\tsaved.hashForSecret = Core::ComputeSecureSecretHash(\n\t\t_password.newSecureAlgo,\n\t\tpassword);\n\tsaved.secretId = CountSecureSecretId(secret);\n\n\tconst auto callback = [=](const Core::CloudPasswordResult &check) {\n\t\tsaveSecret(check, saved, secret);\n\t};\n\tcheckPasswordHash(_saveSecretRequestId, saved.hashForAuth, callback);\n}\n\nvoid FormController::saveSecret(\n\t\tconst Core::CloudPasswordResult &check,\n\t\tconst SavedCredentials &saved,\n\t\tconst bytes::vector &secret) {\n\tconst auto encryptedSecret = EncryptSecureSecret(\n\t\tsecret,\n\t\tsaved.hashForSecret);\n\n\tusing Flag = MTPDaccount_passwordInputSettings::Flag;\n\t_saveSecretRequestId = _api.request(MTPaccount_UpdatePasswordSettings(\n\t\tcheck.result,\n\t\tMTP_account_passwordInputSettings(\n\t\t\tMTP_flags(Flag::f_new_secure_settings),\n\t\t\tMTPPasswordKdfAlgo(), // new_algo\n\t\t\tMTPbytes(), // new_password_hash\n\t\t\tMTPstring(), // hint\n\t\t\tMTPstring(), // email\n\t\t\tMTP_secureSecretSettings(\n\t\t\t\tCore::PrepareSecureSecretAlgo(_password.newSecureAlgo),\n\t\t\t\tMTP_bytes(encryptedSecret),\n\t\t\t\tMTP_long(saved.secretId)))\n\t)).done([=] {\n\t\tsession().data().rememberPassportCredentials(\n\t\t\tstd::move(saved),\n\t\t\tkRememberCredentialsDelay);\n\n\t\t_saveSecretRequestId = 0;\n\t\t_secret = secret;\n\t\t_secretId = saved.secretId;\n\t\t//_password.salt = newPasswordSaltFull;\n\t\tfor (const auto &callback : base::take(_secretCallbacks)) {\n\t\t\tcallback();\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\t_saveSecretRequestId = 0;\n\t\tif (error.type() != u\"SRP_ID_INVALID\"_q\n\t\t\t|| !handleSrpIdInvalid(_saveSecretRequestId)) {\n\t\t\tsuggestRestart();\n\t\t}\n\t}).send();\n}\n\nvoid FormController::suggestRestart() {\n\t_suggestingRestart = true;\n\t_view->show(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_passport_restart_sure(),\n\t\t.confirmed = [=] { _controller->showPassportForm(_request); },\n\t\t.cancelled = [=] { cancel(); },\n\t\t.confirmText = tr::lng_passport_restart(),\n\t}));\n}\n\nvoid FormController::requestForm() {\n\tif (_request.nonce.isEmpty()) {\n\t\t_formRequestId = -1;\n\t\tformFail(NonceNameByScope(_request.scope).toUpper() + \"_EMPTY\");\n\t\treturn;\n\t}\n\t_formRequestId = _api.request(MTPaccount_GetAuthorizationForm(\n\t\tMTP_long(_request.botId.bare),\n\t\tMTP_string(_request.scope),\n\t\tMTP_string(_request.publicKey)\n\t)).done([=](const MTPaccount_AuthorizationForm &result) {\n\t\t_formRequestId = 0;\n\t\tformDone(result);\n\t}).fail([=](const MTP::Error &error) {\n\t\tformFail(error.type());\n\t}).send();\n}\n\nauto FormController::parseFiles(\n\tconst QVector<MTPSecureFile> &data,\n\tconst std::vector<EditFile> &editData) const\n-> std::vector<File> {\n\tauto result = std::vector<File>();\n\tresult.reserve(data.size());\n\n\tfor (const auto &file : data) {\n\t\tif (auto normal = parseFile(file, editData)) {\n\t\t\tresult.push_back(std::move(*normal));\n\t\t}\n\t}\n\n\treturn result;\n}\n\nauto FormController::parseFile(\n\tconst MTPSecureFile &data,\n\tconst std::vector<EditFile> &editData) const\n-> std::optional<File> {\n\tswitch (data.type()) {\n\tcase mtpc_secureFileEmpty:\n\t\treturn std::nullopt;\n\n\tcase mtpc_secureFile: {\n\t\tconst auto &fields = data.c_secureFile();\n\t\tauto result = File();\n\t\tresult.id = fields.vid().v;\n\t\tresult.accessHash = fields.vaccess_hash().v;\n\t\tresult.size = fields.vsize().v;\n\t\tresult.date = fields.vdate().v;\n\t\tresult.dcId = fields.vdc_id().v;\n\t\tresult.hash = bytes::make_vector(fields.vfile_hash().v);\n\t\tresult.encryptedSecret = bytes::make_vector(fields.vsecret().v);\n\t\tfillDownloadedFile(result, editData);\n\t\treturn result;\n\t} break;\n\t}\n\tUnexpected(\"Type in FormController::parseFile.\");\n}\n\nvoid FormController::fillDownloadedFile(\n\t\tFile &destination,\n\t\tconst std::vector<EditFile> &source) const {\n\tconst auto i = ranges::find(\n\t\tsource,\n\t\tdestination.hash,\n\t\t[](const EditFile &file) { return file.fields.hash; });\n\tif (i == source.end()) {\n\t\treturn;\n\t}\n\tdestination.image = i->fields.image;\n\tdestination.downloadStatus = i->fields.downloadStatus;\n\tif (!i->uploadData) {\n\t\treturn;\n\t}\n\tconst auto &bytes = i->uploadData->bytes;\n\tif (bytes.size() > Storage::kMaxFileInMemory) {\n\t\treturn;\n\t}\n\tsession().data().cache().put(\n\t\tData::DocumentCacheKey(destination.dcId, destination.id),\n\t\tStorage::Cache::Database::TaggedValue(\n\t\t\tQByteArray(\n\t\t\t\treinterpret_cast<const char*>(bytes.data()),\n\t\t\t\tbytes.size()),\n\t\t\tData::kImageCacheTag));\n}\n\nauto FormController::parseValue(\n\t\tconst MTPSecureValue &value,\n\t\tconst std::vector<EditFile> &editData) const -> Value {\n\tExpects(value.type() == mtpc_secureValue);\n\n\tconst auto &data = value.c_secureValue();\n\tconst auto type = ConvertType(data.vtype());\n\tauto result = Value(type);\n\tresult.submitHash = bytes::make_vector(data.vhash().v);\n\tif (const auto secureData = data.vdata()) {\n\t\tsecureData->match([&](const MTPDsecureData &data) {\n\t\t\tresult.data.original = data.vdata().v;\n\t\t\tresult.data.hash = bytes::make_vector(data.vdata_hash().v);\n\t\t\tresult.data.encryptedSecret = bytes::make_vector(data.vsecret().v);\n\t\t});\n\t}\n\tif (const auto files = data.vfiles()) {\n\t\tresult.files(FileType::Scan) = parseFiles(files->v, editData);\n\t}\n\tif (const auto translation = data.vtranslation()) {\n\t\tresult.files(FileType::Translation) = parseFiles(\n\t\t\ttranslation->v,\n\t\t\teditData);\n\t}\n\tconst auto parseSpecialScan = [&](\n\t\t\tFileType type,\n\t\t\tconst MTPSecureFile &file) {\n\t\tif (auto parsed = parseFile(file, editData)) {\n\t\t\tresult.specialScans.emplace(type, std::move(*parsed));\n\t\t}\n\t};\n\tif (const auto side = data.vfront_side()) {\n\t\tparseSpecialScan(FileType::FrontSide, *side);\n\t}\n\tif (const auto side = data.vreverse_side()) {\n\t\tparseSpecialScan(FileType::ReverseSide, *side);\n\t}\n\tif (const auto selfie = data.vselfie()) {\n\t\tparseSpecialScan(FileType::Selfie, *selfie);\n\t}\n\tif (const auto plain = data.vplain_data()) {\n\t\tplain->match([&](const MTPDsecurePlainPhone &data) {\n\t\t\tresult.data.parsed.fields[\"value\"].text = qs(data.vphone());\n\t\t}, [&](const MTPDsecurePlainEmail &data) {\n\t\t\tresult.data.parsed.fields[\"value\"].text = qs(data.vemail());\n\t\t});\n\t}\n\treturn result;\n}\n\ntemplate <typename Condition>\nEditFile *FormController::findEditFileByCondition(Condition &&condition) {\n\tfor (auto &pair : _form.values) {\n\t\tauto &value = pair.second;\n\t\tconst auto foundInList = [&](FileType type) -> EditFile* {\n\t\t\tfor (auto &scan : value.filesInEdit(type)) {\n\t\t\t\tif (condition(scan)) {\n\t\t\t\t\treturn &scan;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nullptr;\n\t\t};\n\t\tif (const auto result = foundInList(FileType::Scan)) {\n\t\t\treturn result;\n\t\t} else if (const auto other = foundInList(FileType::Translation)) {\n\t\t\treturn other;\n\t\t}\n\t\tfor (auto &[special, scan] : value.specialScansInEdit) {\n\t\t\tif (condition(scan)) {\n\t\t\t\treturn &scan;\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nEditFile *FormController::findEditFile(const FullMsgId &fullId) {\n\treturn findEditFileByCondition([&](const EditFile &file) {\n\t\treturn (file.uploadData && file.uploadData->fullId == fullId);\n\t});\n}\n\nEditFile *FormController::findEditFile(const FileKey &key) {\n\treturn findEditFileByCondition([&](const EditFile &file) {\n\t\treturn (file.fields.id == key.id);\n\t});\n}\n\nauto FormController::findFile(const FileKey &key)\n-> std::pair<Value*, File*> {\n\tconst auto found = [&](const File &file) {\n\t\treturn (file.id == key.id);\n\t};\n\tfor (auto &pair : _form.values) {\n\t\tauto &value = pair.second;\n\t\tconst auto foundInList = [&](FileType type) -> File* {\n\t\t\tfor (auto &scan : value.files(type)) {\n\t\t\t\tif (found(scan)) {\n\t\t\t\t\treturn &scan;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nullptr;\n\t\t};\n\t\tif (const auto result = foundInList(FileType::Scan)) {\n\t\t\treturn { &value, result };\n\t\t} else if (const auto other = foundInList(FileType::Translation)) {\n\t\t\treturn { &value, other };\n\t\t}\n\t\tfor (auto &[special, scan] : value.specialScans) {\n\t\t\tif (found(scan)) {\n\t\t\t\treturn { &value, &scan };\n\t\t\t}\n\t\t}\n\t}\n\treturn { nullptr, nullptr };\n}\n\nvoid FormController::formDone(const MTPaccount_AuthorizationForm &result) {\n\tif (!parseForm(result)) {\n\t\t_view->showCriticalError(tr::lng_passport_form_error(tr::now));\n\t} else {\n\t\tshowForm();\n\t}\n}\n\nbool FormController::parseForm(const MTPaccount_AuthorizationForm &result) {\n\tExpects(result.type() == mtpc_account_authorizationForm);\n\n\tconst auto &data = result.c_account_authorizationForm();\n\n\tsession().data().processUsers(data.vusers());\n\n\tfor (const auto &value : data.vvalues().v) {\n\t\tauto parsed = parseValue(value);\n\t\tconst auto type = parsed.type;\n\t\tconst auto alreadyIt = _form.values.find(type);\n\t\tif (alreadyIt != _form.values.end()) {\n\t\t\tLOG((\"API Error: Two values for type %1 in authorization form\"\n\t\t\t\t\"%1\").arg(int(type)));\n\t\t\treturn false;\n\t\t}\n\t\t_form.values.emplace(type, std::move(parsed));\n\t}\n\tif (const auto url = data.vprivacy_policy_url()) {\n\t\t_form.privacyPolicyUrl = qs(*url);\n\t}\n\tfor (const auto &required : data.vrequired_types().v) {\n\t\tconst auto row = CollectRequestedRow(required);\n\t\tfor (const auto &requested : row.values) {\n\t\t\tconst auto type = requested.type;\n\t\t\tconst auto &[i, ok] = _form.values.emplace(type, Value(type));\n\t\t\tauto &value = i->second;\n\t\t\tvalue.translationRequired = requested.translationRequired;\n\t\t\tvalue.selfieRequired = requested.selfieRequired;\n\t\t\tvalue.nativeNames = requested.nativeNames;\n\t\t}\n\t\t_form.request.push_back(row.values\n\t\t\t| ranges::views::transform([](const RequestedValue &value) {\n\t\t\t\treturn value.type;\n\t\t\t}) | ranges::to_vector);\n\t}\n\tif (!ValidateForm(_form)) {\n\t\treturn false;\n\t}\n\t_bot = session().data().userLoaded(_request.botId);\n\t_form.pendingErrors = data.verrors().v;\n\treturn true;\n}\n\nvoid FormController::formFail(const QString &error) {\n\t_savedPasswordValue = QByteArray();\n\t_serviceErrorText = error;\n\tif (!handleAppUpdateError(error)) {\n\t\t_view->showCriticalError(\n\t\t\ttr::lng_passport_form_error(tr::now) + \"\\n\" + error);\n\t}\n}\n\nbool FormController::handleAppUpdateError(const QString &error) {\n\tif (error == u\"APP_VERSION_OUTDATED\"_q) {\n\t\t_view->showUpdateAppBox();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid FormController::requestPassword() {\n\tif (_passwordRequestId) {\n\t\treturn;\n\t}\n\t_passwordRequestId = _api.request(MTPaccount_GetPassword(\n\t)).done([=](const MTPaccount_Password &result) {\n\t\t_passwordRequestId = 0;\n\t\tpasswordDone(result);\n\t}).fail([=](const MTP::Error &error) {\n\t\tformFail(error.type());\n\t}).send();\n}\n\nvoid FormController::passwordDone(const MTPaccount_Password &result) {\n\tExpects(result.type() == mtpc_account_password);\n\n\tconst auto changed = applyPassword(result.c_account_password());\n\tif (changed) {\n\t\tshowForm();\n\t}\n\tshortPollEmailConfirmation();\n}\n\nvoid FormController::shortPollEmailConfirmation() {\n\tif (_password.unconfirmedPattern.isEmpty()) {\n\t\t_shortPollTimer.cancel();\n\t\treturn;\n\t}\n\t_shortPollTimer.callOnce(kShortPollTimeout);\n}\n\nvoid FormController::showForm() {\n\tif (_formRequestId || _passwordRequestId) {\n\t\treturn;\n\t} else if (!_bot) {\n\t\tformFail(Lang::Hard::NoAuthorizationBot());\n\t\treturn;\n\t}\n\tif (_password.unknownAlgo\n\t\t|| v::is_null(_password.newAlgo)\n\t\t|| v::is_null(_password.newSecureAlgo)) {\n\t\t_view->showUpdateAppBox();\n\t\treturn;\n\t} else if (_password.request) {\n\t\tif (!_savedPasswordValue.isEmpty()) {\n\t\t\tsubmitPassword(base::duplicate(_savedPasswordValue));\n\t\t} else if (const auto saved = session().data().passportCredentials()) {\n\t\t\tcheckSavedPasswordSettings(*saved);\n\t\t} else {\n\t\t\t_view->showAskPassword();\n\t\t}\n\t} else {\n\t\t_view->showNoPassword();\n\t}\n}\n\nbool FormController::applyPassword(const MTPDaccount_password &result) {\n\tauto settings = PasswordSettings();\n\tsettings.hint = qs(result.vhint().value_or_empty());\n\tsettings.hasRecovery = result.is_has_recovery();\n\tsettings.notEmptyPassport = result.is_has_secure_values();\n\tsettings.request = Core::ParseCloudPasswordCheckRequest(result);\n\tsettings.unknownAlgo = result.vcurrent_algo() && !settings.request;\n\tsettings.unconfirmedPattern = qs(\n\t\tresult.vemail_unconfirmed_pattern().value_or_empty());\n\tsettings.newAlgo = Core::ValidateNewCloudPasswordAlgo(\n\t\tCore::ParseCloudPasswordAlgo(result.vnew_algo()));\n\tsettings.newSecureAlgo = Core::ValidateNewSecureSecretAlgo(\n\t\tCore::ParseSecureSecretAlgo(result.vnew_secure_algo()));\n\tsettings.pendingResetDate = result.vpending_reset_date().value_or_empty();\n\tbase::RandomAddSeed(bytes::make_span(result.vsecure_random().v));\n\treturn applyPassword(std::move(settings));\n}\n\nbool FormController::applyPassword(PasswordSettings &&settings) {\n\tif (_password != settings) {\n\t\t_password = std::move(settings);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid FormController::cancel() {\n\tif (!_submitSuccess && _serviceErrorText.isEmpty()) {\n\t\t_view->show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_passport_stop_sure(),\n\t\t\t.confirmed = [=] { cancelSure(); },\n\t\t\t.cancelled = [=](Fn<void()> close) { cancelAbort(); close(); },\n\t\t\t.confirmText = tr::lng_passport_stop(),\n\t\t}));\n\t} else {\n\t\tcancelSure();\n\t}\n}\n\nvoid FormController::cancelAbort() {\n\tif (_cancelled || _submitSuccess) {\n\t\treturn;\n\t} else if (_suggestingRestart) {\n\t\tsuggestRestart();\n\t}\n}\n\nvoid FormController::cancelSure() {\n\tif (!_cancelled) {\n\t\t_cancelled = true;\n\n\t\tif (!_request.callbackUrl.isEmpty()\n\t\t\t&& (_serviceErrorText.isEmpty()\n\t\t\t\t|| ForwardServiceErrorRequired(_serviceErrorText))) {\n\t\t\tconst auto url = qthelp::url_append_query_or_hash(\n\t\t\t\t_request.callbackUrl,\n\t\t\t\t(_submitSuccess\n\t\t\t\t\t? \"tg_passport=success\"\n\t\t\t\t\t: (_serviceErrorText.isEmpty()\n\t\t\t\t\t\t? \"tg_passport=cancel\"\n\t\t\t\t\t\t: \"tg_passport=error&error=\" + _serviceErrorText)));\n\t\t\tUrlClickHandler::Open(url);\n\t\t}\n\t\tconst auto timeout = _view->closeGetDuration();\n\t\tbase::call_delayed(timeout, this, [=] {\n\t\t\t_controller->clearPassportForm();\n\t\t});\n\t}\n}\n\nrpl::lifetime &FormController::lifetime() {\n\treturn _lifetime;\n}\n\nFormController::~FormController() = default;\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_form_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n#include \"core/core_cloud_password.h\"\n\nclass mtpFileLoader;\n\nnamespace Storage {\nstruct UploadSecureDone;\nstruct UploadSecureProgress;\n} // namespace Storage\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass SentCodeCall;\n} // namespace Ui\n\nnamespace Passport {\n\nstruct EditDocumentCountry;\n\nstruct SavedCredentials {\n\tbytes::vector hashForAuth;\n\tbytes::vector hashForSecret;\n\tuint64 secretId = 0;\n};\n\nQString NonceNameByScope(const QString &scope);\n\nclass ViewController;\n\nstruct FormRequest {\n\tFormRequest(\n\t\tUserId botId,\n\t\tconst QString &scope,\n\t\tconst QString &callbackUrl,\n\t\tconst QString &publicKey,\n\t\tconst QString &nonce);\n\n\tUserId botId;\n\tQString scope;\n\tQString callbackUrl;\n\tQString publicKey;\n\tQString nonce;\n\n};\n\nclass LoadStatus final {\npublic:\n\tenum class Status {\n\t\tDone,\n\t\tInProgress,\n\t\tFailed,\n\t};\n\n\tLoadStatus() = default;\n\n\tvoid set(Status status, int offset = 0) {\n\t\tif (!offset) {\n\t\t\toffset = _offset;\n\t\t}\n\t\t_offset = (status == Status::InProgress) ? offset : 0;\n\t\t_status = status;\n\t}\n\n\tint offset() const {\n\t\treturn _offset;\n\t}\n\tStatus status() const {\n\t\treturn _status;\n\t}\nprivate:\n\tint _offset = 0;\n\tStatus _status = Status::Done;\n};\n\nstruct UploadScanData {\n\tFullMsgId fullId;\n\tuint64 fileId = 0;\n\tint partsCount = 0;\n\tQByteArray md5checksum;\n\tbytes::vector hash;\n\tbytes::vector bytes;\n\n\tLoadStatus status;\n};\n\nclass UploadScanDataPointer {\npublic:\n\tUploadScanDataPointer(\n\t\tnot_null<Main::Session*> session,\n\t\tstd::unique_ptr<UploadScanData> &&value);\n\tUploadScanDataPointer(UploadScanDataPointer &&other);\n\tUploadScanDataPointer &operator=(UploadScanDataPointer &&other);\n\t~UploadScanDataPointer();\n\n\tUploadScanData *get() const;\n\toperator UploadScanData*() const;\n\texplicit operator bool() const;\n\tUploadScanData *operator->() const;\n\nprivate:\n\tnot_null<Main::Session*> _session;\n\tstd::unique_ptr<UploadScanData> _value;\n\n};\n\nstruct Value;\n\nenum class FileType {\n\tScan,\n\tTranslation,\n\tFrontSide,\n\tReverseSide,\n\tSelfie,\n};\n\nstruct File {\n\tuint64 id = 0;\n\tuint64 accessHash = 0;\n\tint32 size = 0;\n\tint32 dcId = 0;\n\tTimeId date = 0;\n\tbytes::vector hash;\n\tbytes::vector secret;\n\tbytes::vector encryptedSecret;\n\n\tLoadStatus downloadStatus;\n\tQImage image;\n\tQString error;\n};\n\nstruct EditFile {\n\tEditFile(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<const Value*> value,\n\t\tFileType type,\n\t\tconst File &fields,\n\t\tstd::unique_ptr<UploadScanData> &&uploadData);\n\n\tnot_null<const Value*> value;\n\tFileType type;\n\tFile fields;\n\tUploadScanDataPointer uploadData;\n\tstd::shared_ptr<bool> guard;\n\tbool deleted = false;\n};\n\nstruct ValueField {\n\tQString text;\n\tQString error;\n};\n\nstruct ValueMap {\n\tstd::map<QString, ValueField> fields;\n};\n\nstruct ValueData {\n\tQByteArray original;\n\tbytes::vector secret;\n\tValueMap parsed;\n\tbytes::vector hash;\n\tbytes::vector encryptedSecret;\n\tValueMap parsedInEdit;\n\tbytes::vector hashInEdit;\n\tbytes::vector encryptedSecretInEdit;\n};\n\nstruct Verification {\n\tmtpRequestId requestId = 0;\n\tQString phoneCodeHash;\n\tint codeLength = 0;\n\tQString fragmentUrl;\n\tstd::unique_ptr<Ui::SentCodeCall> call;\n\n\tQString error;\n\n};\n\nstruct Form;\n\nstruct Value {\n\tenum class Type {\n\t\tPersonalDetails,\n\t\tPassport,\n\t\tDriverLicense,\n\t\tIdentityCard,\n\t\tInternalPassport,\n\t\tAddress,\n\t\tUtilityBill,\n\t\tBankStatement,\n\t\tRentalAgreement,\n\t\tPassportRegistration,\n\t\tTemporaryRegistration,\n\t\tPhone,\n\t\tEmail,\n\t};\n\n\n\texplicit Value(Type type);\n\tValue(Value &&other) = default;\n\n\t// Some data is not parsed from server-provided values.\n\t// It should be preserved through re-parsing (for example when saving).\n\t// So we hide \"operator=(Value&&)\" in private and instead provide this.\n\tvoid fillDataFrom(Value &&other);\n\tbool requiresSpecialScan(FileType type) const;\n\tbool requiresScan(FileType type) const;\n\tbool scansAreFilled() const;\n\tvoid saveInEdit(not_null<Main::Session*> session);\n\tvoid clearEditData();\n\tbool uploadingScan() const;\n\tbool saving() const;\n\n\tstatic constexpr auto kNothingFilled = 0x100;\n\tstatic constexpr auto kNoTranslationFilled = 0x10;\n\tstatic constexpr auto kNoSelfieFilled = 0x001;\n\tint whatNotFilled() const;\n\n\tstd::vector<File> &files(FileType type);\n\tconst std::vector<File> &files(FileType type) const;\n\tQString &fileMissingError(FileType type);\n\tconst QString &fileMissingError(FileType type) const;\n\tstd::vector<EditFile> &filesInEdit(FileType type);\n\tconst std::vector<EditFile> &filesInEdit(FileType type) const;\n\tEditFile &fileInEdit(FileType type, std::optional<int> fileIndex);\n\tconst EditFile &fileInEdit(\n\t\tFileType type,\n\t\tstd::optional<int> fileIndex) const;\n\n\tstd::vector<EditFile> takeAllFilesInEdit();\n\n\tType type;\n\tValueData data;\n\tstd::map<FileType, File> specialScans;\n\tQString error;\n\tstd::map<FileType, EditFile> specialScansInEdit;\n\tVerification verification;\n\tbytes::vector submitHash;\n\n\tbool selfieRequired = false;\n\tbool translationRequired = false;\n\tbool nativeNames = false;\n\tint editScreens = 0;\n\n\tmtpRequestId saveRequestId = 0;\n\nprivate:\n\tValue &operator=(Value &&other) = default;\n\n\tstd::vector<File> _scans;\n\tstd::vector<File> _translations;\n\tstd::vector<EditFile> _scansInEdit;\n\tstd::vector<EditFile> _translationsInEdit;\n\tQString _scanMissingError;\n\tQString _translationMissingError;\n\n};\n\nbool ValueChanged(not_null<const Value*> value, const ValueMap &data);\n\nstruct RequestedValue {\n\texplicit RequestedValue(Value::Type type);\n\n\tValue::Type type;\n\tbool selfieRequired = false;\n\tbool translationRequired = false;\n\tbool nativeNames = false;\n};\n\nstruct RequestedRow {\n\tstd::vector<RequestedValue> values;\n};\n\nstruct Form {\n\tusing Request = std::vector<std::vector<Value::Type>>;\n\n\tstd::map<Value::Type, Value> values;\n\tRequest request;\n\tQString privacyPolicyUrl;\n\tQVector<MTPSecureValueError> pendingErrors;\n};\n\nstruct PasswordSettings {\n\tCore::CloudPasswordCheckRequest request;\n\tCore::CloudPasswordAlgo newAlgo;\n\tCore::SecureSecretAlgo newSecureAlgo;\n\tQString hint;\n\tQString unconfirmedPattern;\n\tQString confirmedEmail;\n\tbool hasRecovery = false;\n\tbool notEmptyPassport = false;\n\tbool unknownAlgo = false;\n\tTimeId pendingResetDate = 0;\n\n\tbool operator==(const PasswordSettings &other) const {\n\t\treturn (request == other.request)\n// newAlgo and newSecureAlgo are always different, because they have\n// different random parts added on the client to the server salts.\n//\t\t\t&& (newAlgo == other.newAlgo)\n//\t\t\t&& (newSecureAlgo == other.newSecureAlgo)\n\t\t\t&& ((v::is_null(newAlgo) && v::is_null(other.newAlgo))\n\t\t\t\t|| (!v::is_null(newAlgo) && !v::is_null(other.newAlgo)))\n\t\t\t&& ((v::is_null(newSecureAlgo) && v::is_null(other.newSecureAlgo))\n\t\t\t\t|| (!v::is_null(newSecureAlgo)\n\t\t\t\t\t&& !v::is_null(other.newSecureAlgo)))\n\t\t\t&& (hint == other.hint)\n\t\t\t&& (unconfirmedPattern == other.unconfirmedPattern)\n\t\t\t&& (confirmedEmail == other.confirmedEmail)\n\t\t\t&& (hasRecovery == other.hasRecovery)\n\t\t\t&& (unknownAlgo == other.unknownAlgo)\n\t\t\t&& (pendingResetDate == other.pendingResetDate);\n\t}\n\tbool operator!=(const PasswordSettings &other) const {\n\t\treturn !(*this == other);\n\t}\n};\n\nstruct FileKey {\n\tuint64 id = 0;\n\n\tinline bool operator==(const FileKey &other) const {\n\t\treturn (id == other.id);\n\t}\n\tinline bool operator!=(const FileKey &other) const {\n\t\treturn !(*this == other);\n\t}\n\tinline bool operator<(const FileKey &other) const {\n\t\treturn (id < other.id);\n\t}\n\tinline bool operator>(const FileKey &other) const {\n\t\treturn (other < *this);\n\t}\n\tinline bool operator<=(const FileKey &other) const {\n\t\treturn !(other < *this);\n\t}\n\tinline bool operator>=(const FileKey &other) const {\n\t\treturn !(*this < other);\n\t}\n\n};\n\nclass FormController : public base::has_weak_ptr {\npublic:\n\tFormController(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst FormRequest &request);\n\n\t[[nodiscard]] not_null<Window::SessionController*> window() const {\n\t\treturn _controller;\n\t}\n\t[[nodiscard]] Main::Session &session() const;\n\n\tvoid show();\n\tUserData *bot() const;\n\tQString privacyPolicyUrl() const;\n\tstd::vector<not_null<const Value*>> submitGetErrors();\n\tvoid submitPassword(const QByteArray &password);\n\tvoid recoverPassword();\n\trpl::producer<QString> passwordError() const;\n\tconst PasswordSettings &passwordSettings() const;\n\tvoid reloadPassword();\n\tvoid reloadAndSubmitPassword(const QByteArray &password);\n\tvoid cancelPassword();\n\n\tbool canAddScan(not_null<const Value*> value, FileType type) const;\n\tvoid uploadScan(\n\t\tnot_null<const Value*> value,\n\t\tFileType type,\n\t\tQByteArray &&content);\n\tvoid deleteScan(\n\t\tnot_null<const Value*> value,\n\t\tFileType type,\n\t\tstd::optional<int> fileIndex);\n\tvoid restoreScan(\n\t\tnot_null<const Value*> value,\n\t\tFileType type,\n\t\tstd::optional<int> fileIndex);\n\n\trpl::producer<> secretReadyEvents() const;\n\n\tQString defaultEmail() const;\n\tQString defaultPhoneNumber() const;\n\n\trpl::producer<not_null<const EditFile*>> scanUpdated() const;\n\trpl::producer<not_null<const Value*>> valueSaveFinished() const;\n\trpl::producer<not_null<const Value*>> verificationNeeded() const;\n\trpl::producer<not_null<const Value*>> verificationUpdate() const;\n\tvoid verify(not_null<const Value*> value, const QString &code);\n\n\tconst Form &form() const;\n\tvoid startValueEdit(not_null<const Value*> value);\n\tvoid cancelValueEdit(not_null<const Value*> value);\n\tvoid cancelValueVerification(not_null<const Value*> value);\n\tvoid saveValueEdit(not_null<const Value*> value, ValueMap &&data);\n\tvoid deleteValueEdit(not_null<const Value*> value);\n\n\tvoid cancel();\n\tvoid cancelSure();\n\n\t[[nodiscard]] rpl::producer<EditDocumentCountry> preferredLanguage(\n\t\tconst QString &countryCode);\n\n\trpl::lifetime &lifetime();\n\n\t~FormController();\n\nprivate:\n\tusing PasswordCheckCallback = Fn<void(\n\t\tconst Core::CloudPasswordResult &check)>;\n\n\tstruct FinalData {\n\t\tQVector<MTPSecureValueHash> hashes;\n\t\tQByteArray credentials;\n\t\tstd::vector<not_null<const Value*>> errors;\n\t};\n\n\ttemplate <typename Condition>\n\tEditFile *findEditFileByCondition(Condition &&condition);\n\tEditFile *findEditFile(const FullMsgId &fullId);\n\tEditFile *findEditFile(const FileKey &key);\n\tstd::pair<Value*, File*> findFile(const FileKey &key);\n\tnot_null<Value*> findValue(not_null<const Value*> value);\n\n\tvoid requestForm();\n\tvoid requestPassword();\n\n\tvoid formDone(const MTPaccount_AuthorizationForm &result);\n\tvoid formFail(const QString &error);\n\tbool parseForm(const MTPaccount_AuthorizationForm &result);\n\tvoid showForm();\n\tValue parseValue(\n\t\tconst MTPSecureValue &value,\n\t\tconst std::vector<EditFile> &editData = {}) const;\n\tstd::vector<File> parseFiles(\n\t\tconst QVector<MTPSecureFile> &data,\n\t\tconst std::vector<EditFile> &editData) const;\n\tstd::optional<File> parseFile(\n\t\tconst MTPSecureFile &data,\n\t\tconst std::vector<EditFile> &editData) const;\n\tvoid fillDownloadedFile(\n\t\tFile &destination,\n\t\tconst std::vector<EditFile> &source) const;\n\tbool handleAppUpdateError(const QString &error);\n\n\tvoid submitPassword(\n\t\tconst Core::CloudPasswordResult &check,\n\t\tconst QByteArray &password,\n\t\tbool submitSaved);\n\tvoid checkPasswordHash(\n\t\tmtpRequestId &guard,\n\t\tbytes::vector hash,\n\t\tPasswordCheckCallback callback);\n\tbool handleSrpIdInvalid(mtpRequestId &guard);\n\tvoid requestPasswordData(mtpRequestId &guard);\n\tvoid passwordChecked();\n\tvoid passwordServerError();\n\tvoid passwordDone(const MTPaccount_Password &result);\n\tbool applyPassword(const MTPDaccount_password &settings);\n\tbool applyPassword(PasswordSettings &&settings);\n\tbytes::vector passwordHashForAuth(bytes::const_span password) const;\n\tvoid checkSavedPasswordSettings(const SavedCredentials &credentials);\n\tvoid checkSavedPasswordSettings(\n\t\tconst Core::CloudPasswordResult &check,\n\t\tconst SavedCredentials &credentials);\n\tvoid validateSecureSecret(\n\t\tbytes::const_span encryptedSecret,\n\t\tbytes::const_span passwordHashForSecret,\n\t\tbytes::const_span passwordBytes,\n\t\tuint64 serverSecretId);\n\tvoid decryptValues();\n\tvoid decryptValue(Value &value) const;\n\tbool validateValueSecrets(Value &value) const;\n\tvoid resetValue(Value &value) const;\n\tvoid fillErrors();\n\tvoid fillNativeFromFallback();\n\n\tvoid loadFile(File &file);\n\tvoid fileLoadDone(FileKey key, const QByteArray &bytes);\n\tvoid fileLoadProgress(FileKey key, int offset);\n\tvoid fileLoadFail(FileKey key);\n\tvoid generateSecret(bytes::const_span password);\n\tvoid saveSecret(\n\t\tconst Core::CloudPasswordResult &check,\n\t\tconst SavedCredentials &saved,\n\t\tconst bytes::vector &secret);\n\n\tvoid subscribeToUploader();\n\tvoid encryptFile(\n\t\tEditFile &file,\n\t\tQByteArray &&content,\n\t\tFn<void(UploadScanData &&result)> callback);\n\tvoid prepareFile(\n\t\tEditFile &file,\n\t\tconst QByteArray &content);\n\tvoid uploadEncryptedFile(\n\t\tEditFile &file,\n\t\tUploadScanData &&data);\n\tvoid scanUploadDone(const Storage::UploadSecureDone &data);\n\tvoid scanUploadProgress(const Storage::UploadSecureProgress &data);\n\tvoid scanUploadFail(const FullMsgId &fullId);\n\tvoid scanDeleteRestore(\n\t\tnot_null<const Value*> value,\n\t\tFileType type,\n\t\tstd::optional<int> fileIndex,\n\t\tbool deleted);\n\n\tQString getPhoneFromValue(not_null<const Value*> value) const;\n\tQString getEmailFromValue(not_null<const Value*> value) const;\n\tQString getPlainTextFromValue(not_null<const Value*> value) const;\n\tvoid startPhoneVerification(not_null<Value*> value);\n\tvoid startEmailVerification(not_null<Value*> value);\n\tvoid valueSaveShowError(not_null<Value*> value, const MTP::Error &error);\n\tvoid valueSaveFailed(not_null<Value*> value);\n\tvoid requestPhoneCall(not_null<Value*> value);\n\tvoid verificationError(\n\t\tnot_null<Value*> value,\n\t\tconst QString &text);\n\tvoid valueEditFailed(not_null<Value*> value);\n\tvoid clearValueEdit(not_null<Value*> value);\n\tvoid clearValueVerification(not_null<Value*> value);\n\n\tbool isEncryptedValue(Value::Type type) const;\n\tvoid saveEncryptedValue(not_null<Value*> value);\n\tvoid savePlainTextValue(not_null<Value*> value);\n\tvoid sendSaveRequest(\n\t\tnot_null<Value*> value,\n\t\tconst MTPInputSecureValue &data);\n\tFinalData prepareFinalData();\n\n\tvoid suggestReset(bytes::vector password);\n\tvoid resetSecret(\n\t\tconst Core::CloudPasswordResult &check,\n\t\tconst bytes::vector &password);\n\tvoid suggestRestart();\n\tvoid cancelAbort();\n\tvoid shortPollEmailConfirmation();\n\n\tnot_null<Window::SessionController*> _controller;\n\tMTP::Sender _api;\n\tFormRequest _request;\n\tUserData *_bot = nullptr;\n\n\tmtpRequestId _formRequestId = 0;\n\tmtpRequestId _passwordRequestId = 0;\n\tmtpRequestId _passwordCheckRequestId = 0;\n\n\tPasswordSettings _password;\n\tcrl::time _lastSrpIdInvalidTime = 0;\n\tbytes::vector _passwordCheckHash;\n\tPasswordCheckCallback _passwordCheckCallback;\n\tQByteArray _savedPasswordValue;\n\tForm _form;\n\tbool _cancelled = false;\n\tmtpRequestId _recoverRequestId = 0;\n\tbase::flat_map<FileKey, std::unique_ptr<mtpFileLoader>> _fileLoaders;\n\n\tstruct {\n\t\tint32 hash = 0;\n\t\tstd::map<QString, QString> languagesByCountryCode;\n\t} _passportConfig;\n\n\trpl::event_stream<not_null<const EditFile*>> _scanUpdated;\n\trpl::event_stream<not_null<const Value*>> _valueSaveFinished;\n\trpl::event_stream<not_null<const Value*>> _verificationNeeded;\n\trpl::event_stream<not_null<const Value*>> _verificationUpdate;\n\n\tbytes::vector _secret;\n\tuint64 _secretId = 0;\n\tstd::vector<Fn<void()>> _secretCallbacks;\n\tmtpRequestId _saveSecretRequestId = 0;\n\trpl::event_stream<> _secretReady;\n\trpl::event_stream<QString> _passwordError;\n\tmtpRequestId _submitRequestId = 0;\n\tbool _submitSuccess = false;\n\tbool _suggestingRestart = false;\n\tQString _serviceErrorText;\n\tbase::Timer _shortPollTimer;\n\n\trpl::lifetime _uploaderSubscriptions;\n\trpl::lifetime _lifetime;\n\n\tstd::unique_ptr<ViewController> _view;\n\n};\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_form_row.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"passport/passport_form_row.h\"\n\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/text_options.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_passport.h\"\n\nnamespace Passport {\n\nFormRow::FormRow(\n\tQWidget *parent,\n\tconst QString &title,\n\tconst QString &description)\n: RippleButton(parent, st::passportRowRipple)\n, _title(\n\tst::semiboldTextStyle,\n\ttitle,\n\tUi::NameTextOptions(),\n\tst::boxWideWidth / 2)\n, _description(\n\tst::defaultTextStyle,\n\tdescription,\n\tUi::NameTextOptions(),\n\tst::boxWideWidth / 2) {\n}\n\nvoid FormRow::setReady(bool ready) {\n\t_ready = ready;\n\tresizeToWidth(width());\n\tupdate();\n}\n\nint FormRow::resizeGetHeight(int newWidth) {\n\tconst auto availableWidth = countAvailableWidth(newWidth);\n\t_titleHeight = _title.countHeight(availableWidth);\n\t_descriptionHeight = _description.countHeight(availableWidth);\n\tconst auto result = st::passportRowPadding.top()\n\t\t+ _titleHeight\n\t\t+ st::passportRowSkip\n\t\t+ _descriptionHeight\n\t\t+ st::passportRowPadding.bottom();\n\treturn result;\n}\n\nint FormRow::countAvailableWidth(int newWidth) const {\n\treturn newWidth\n\t\t- st::passportRowPadding.left()\n\t\t- st::passportRowPadding.right()\n\t\t- (_ready ? st::passportRowReadyIcon : st::passportRowEmptyIcon).width();\n}\n\nint FormRow::countAvailableWidth() const {\n\treturn countAvailableWidth(width());\n}\n\nvoid FormRow::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tconst auto ms = getms();\n\tpaintRipple(p, 0, 0, ms);\n\n\tconst auto left = st::passportRowPadding.left();\n\tconst auto availableWidth = countAvailableWidth();\n\tauto top = st::passportRowPadding.top();\n\n\tp.setPen(st::passportRowTitleFg);\n\t_title.drawLeft(p, left, top, availableWidth, width());\n\ttop += _titleHeight + st::passportRowSkip;\n\n\tp.setPen(st::passportRowDescriptionFg);\n\t_description.drawLeft(p, left, top, availableWidth, width());\n\ttop += _descriptionHeight + st::passportRowPadding.bottom();\n\n\tconst auto &icon = _ready\n\t\t? st::passportRowReadyIcon\n\t\t: st::passportRowEmptyIcon;\n\ticon.paint(\n\t\tp,\n\t\twidth() - st::passportRowPadding.right() - icon.width(),\n\t\t(height() - icon.height()) / 2,\n\t\twidth());\n}\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_form_row.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/buttons.h\"\n\nnamespace Ui {\ntemplate <typename Widget>\nclass FadeWrapScaled;\n} // namespace Ui\n\nnamespace Passport {\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_form_view_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"passport/passport_form_view_controller.h\"\n\n#include \"passport/passport_form_controller.h\"\n#include \"passport/passport_panel_edit_document.h\"\n#include \"passport/passport_panel_edit_contact.h\"\n#include \"passport/passport_panel_controller.h\"\n#include \"lang/lang_keys.h\"\n\nnamespace Passport {\nnamespace {\n\nstd::map<Value::Type, Scope::Type> ScopeTypesMap() {\n\treturn {\n\t\t{ Value::Type::PersonalDetails, Scope::Type::PersonalDetails },\n\t\t{ Value::Type::Passport, Scope::Type::Identity },\n\t\t{ Value::Type::DriverLicense, Scope::Type::Identity },\n\t\t{ Value::Type::IdentityCard, Scope::Type::Identity },\n\t\t{ Value::Type::InternalPassport, Scope::Type::Identity },\n\t\t{ Value::Type::Address, Scope::Type::AddressDetails },\n\t\t{ Value::Type::UtilityBill, Scope::Type::Address },\n\t\t{ Value::Type::BankStatement, Scope::Type::Address },\n\t\t{ Value::Type::RentalAgreement, Scope::Type::Address },\n\t\t{ Value::Type::PassportRegistration, Scope::Type::Address },\n\t\t{ Value::Type::TemporaryRegistration, Scope::Type::Address },\n\t\t{ Value::Type::Phone, Scope::Type::Phone },\n\t\t{ Value::Type::Email, Scope::Type::Email },\n\t};\n}\n\nScope::Type ScopeTypeForValueType(Value::Type type) {\n\tstatic const auto map = ScopeTypesMap();\n\tconst auto i = map.find(type);\n\tAssert(i != map.end());\n\treturn i->second;\n}\n\nstd::map<Scope::Type, Value::Type> ScopeDetailsMap() {\n\treturn {\n\t\t{ Scope::Type::PersonalDetails, Value::Type::PersonalDetails },\n\t\t{ Scope::Type::Identity, Value::Type::PersonalDetails },\n\t\t{ Scope::Type::AddressDetails, Value::Type::Address },\n\t\t{ Scope::Type::Address, Value::Type::Address },\n\t\t{ Scope::Type::Phone, Value::Type::Phone },\n\t\t{ Scope::Type::Email, Value::Type::Email },\n\t};\n}\n\nValue::Type DetailsTypeForScopeType(Scope::Type type) {\n\tstatic const auto map = ScopeDetailsMap();\n\tconst auto i = map.find(type);\n\tAssert(i != map.end());\n\treturn i->second;\n}\n\nbool InlineDetails(\n\t\tconst Form::Request &request,\n\t\tScope::Type into,\n\t\tValue::Type details) {\n\tconst auto count = ranges::count_if(\n\t\trequest,\n\t\t[&](const std::vector<Value::Type> &types) {\n\t\t\tExpects(!types.empty());\n\n\t\t\treturn ScopeTypeForValueType(types[0]) == into;\n\t\t});\n\tif (count != 1) {\n\t\treturn false;\n\t}\n\treturn ranges::any_of(\n\t\trequest,\n\t\t[&](const std::vector<Value::Type> &types) {\n\t\t\tExpects(!types.empty());\n\n\t\t\treturn (types[0] == details);\n\t\t}\n\t);\n}\n\nbool InlineDetails(const Form::Request &request, Value::Type details) {\n\tif (details == Value::Type::PersonalDetails) {\n\t\treturn InlineDetails(request, Scope::Type::Identity, details);\n\t} else if (details == Value::Type::Address) {\n\t\treturn InlineDetails(request, Scope::Type::Address, details);\n\t}\n\treturn false;\n}\n\n} // namespace\n\nScope::Scope(Type type) : type(type) {\n}\n\nbool CanRequireSelfie(Value::Type type) {\n\tconst auto scope = ScopeTypeForValueType(type);\n\treturn (scope == Scope::Type::Address)\n\t\t|| (scope == Scope::Type::Identity);\n}\n\nbool CanRequireScans(Value::Type type) {\n\tconst auto scope = ScopeTypeForValueType(type);\n\treturn (scope == Scope::Type::Address);\n}\n\nbool CanRequireTranslation(Value::Type type) {\n\tconst auto scope = ScopeTypeForValueType(type);\n\treturn (scope == Scope::Type::Address)\n\t\t|| (scope == Scope::Type::Identity);\n}\n\nbool CanRequireNativeNames(Value::Type type) {\n\treturn (type == Value::Type::PersonalDetails);\n}\n\nbool CanHaveErrors(Value::Type type) {\n\treturn (type != Value::Type::Phone) && (type != Value::Type::Email);\n}\n\nbool ValidateForm(const Form &form) {\n\tbase::flat_set<Value::Type> values;\n\tfor (const auto &requested : form.request) {\n\t\tif (requested.empty()) {\n\t\t\tLOG((\"API Error: Empty types list in authorization form row.\"));\n\t\t\treturn false;\n\t\t}\n\t\tconst auto scopeType = ScopeTypeForValueType(requested[0]);\n\t\tconst auto ownsDetails = (scopeType != Scope::Type::Identity\n\t\t\t&& scopeType != Scope::Type::Address);\n\t\tif (ownsDetails && requested.size() != 1) {\n\t\t\tLOG((\"API Error: Large types list in authorization form row.\"));\n\t\t\treturn false;\n\t\t}\n\t\tfor (const auto type : requested) {\n\t\t\tif (values.contains(type)) {\n\t\t\t\tLOG((\"API Error: Value twice in authorization form row.\"));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tvalues.emplace(type);\n\t\t}\n\t}\n\n\t// Invalid errors should be skipped while parsing the form.\n\tfor (const auto &[type, value] : form.values) {\n\t\tif (value.selfieRequired && !CanRequireSelfie(type)) {\n\t\t\tLOG((\"API Error: Bad value requiring selfie.\"));\n\t\t\treturn false;\n\t\t} else if (value.translationRequired\n\t\t\t&& !CanRequireTranslation(type)) {\n\t\t\tLOG((\"API Error: Bad value requiring translation.\"));\n\t\t\treturn false;\n\t\t} else if (value.nativeNames && !CanRequireNativeNames(type)) {\n\t\t\tLOG((\"API Error: Bad value requiring native names.\"));\n\t\t\treturn false;\n\t\t}\n\t\tif (!value.requiresScan(FileType::Scan)) {\n\t\t\tfor (const auto &scan : value.files(FileType::Scan)) {\n\t\t\t\tAssert(scan.error.isEmpty());\n\t\t\t}\n\t\t\tAssert(value.fileMissingError(FileType::Scan).isEmpty());\n\t\t}\n\t\tif (!value.requiresScan(FileType::Translation)) {\n\t\t\tfor (const auto &scan : value.files(FileType::Translation)) {\n\t\t\t\tAssert(scan.error.isEmpty());\n\t\t\t}\n\t\t\tAssert(value.fileMissingError(FileType::Translation).isEmpty());\n\t\t}\n\t\tfor (const auto &[type, specialScan] : value.specialScans) {\n\t\t\tif (!value.requiresSpecialScan(type)) {\n\t\t\t\tAssert(specialScan.error.isEmpty());\n\t\t\t}\n\t\t}\n\t}\n\n\treturn true;\n}\n\nstd::vector<Scope> ComputeScopes(const Form &form) {\n\tauto result = std::vector<Scope>();\n\tconst auto findValue = [&](const Value::Type type) {\n\t\tconst auto i = form.values.find(type);\n\t\tAssert(i != form.values.end());\n\t\treturn &i->second;\n\t};\n\tfor (const auto &requested : form.request) {\n\t\tAssert(!requested.empty());\n\t\tconst auto scopeType = ScopeTypeForValueType(requested[0]);\n\t\tconst auto detailsType = DetailsTypeForScopeType(scopeType);\n\t\tconst auto ownsDetails = (scopeType != Scope::Type::Identity\n\t\t\t&& scopeType != Scope::Type::Address);\n\t\tconst auto inlineDetails = InlineDetails(form.request, detailsType);\n\t\tif (ownsDetails && inlineDetails) {\n\t\t\tcontinue;\n\t\t}\n\t\tresult.push_back(Scope(scopeType));\n\t\tauto &scope = result.back();\n\t\tscope.details = (ownsDetails || inlineDetails)\n\t\t\t? findValue(detailsType)\n\t\t\t: nullptr;\n\t\tif (ownsDetails) {\n\t\t\tAssert(requested.size() == 1);\n\t\t} else {\n\t\t\tfor (const auto type : requested) {\n\t\t\t\tscope.documents.push_back(findValue(type));\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nQString JoinScopeRowReadyString(\n\t\tstd::vector<std::pair<QString, QString>> &&values) {\n\tusing Pair = std::pair<QString, QString>;\n\n\tif (values.empty()) {\n\t\treturn QString();\n\t}\n\tauto result = QString();\n\tauto size = ranges::accumulate(\n\t\tvalues,\n\t\t0,\n\t\tranges::plus(),\n\t\t[](const Pair &v) { return v.second.size(); });\n\tresult.reserve(size + (values.size() - 1) * 2);\n\tfor (const auto &pair : values) {\n\t\tif (pair.second.isEmpty()) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!result.isEmpty()) {\n\t\t\tresult.append(\", \");\n\t\t}\n\t\tresult.append(pair.second);\n\t}\n\treturn result;\n}\n\nScopeRow DocumentRowByType(Value::Type type) {\n\tusing Type = Value::Type;\n\tswitch (type) {\n\tcase Type::Passport:\n\t\treturn {\n\t\t\ttr::lng_passport_identity_passport(tr::now),\n\t\t\ttr::lng_passport_identity_passport_upload(tr::now),\n\t\t};\n\tcase Type::DriverLicense:\n\t\treturn {\n\t\t\ttr::lng_passport_identity_license(tr::now),\n\t\t\ttr::lng_passport_identity_license_upload(tr::now),\n\t\t};\n\tcase Type::IdentityCard:\n\t\treturn {\n\t\t\ttr::lng_passport_identity_card(tr::now),\n\t\t\ttr::lng_passport_identity_card_upload(tr::now),\n\t\t};\n\tcase Type::InternalPassport:\n\t\treturn {\n\t\t\ttr::lng_passport_identity_internal(tr::now),\n\t\t\ttr::lng_passport_identity_internal_upload(tr::now),\n\t\t};\n\tcase Type::BankStatement:\n\t\treturn {\n\t\t\ttr::lng_passport_address_statement(tr::now),\n\t\t\ttr::lng_passport_address_statement_upload(tr::now),\n\t\t};\n\tcase Type::UtilityBill:\n\t\treturn {\n\t\t\ttr::lng_passport_address_bill(tr::now),\n\t\t\ttr::lng_passport_address_bill_upload(tr::now),\n\t\t};\n\tcase Type::RentalAgreement:\n\t\treturn {\n\t\t\ttr::lng_passport_address_agreement(tr::now),\n\t\t\ttr::lng_passport_address_agreement_upload(tr::now),\n\t\t};\n\tcase Type::PassportRegistration:\n\t\treturn {\n\t\t\ttr::lng_passport_address_registration(tr::now),\n\t\t\ttr::lng_passport_address_registration_upload(tr::now),\n\t\t};\n\tcase Type::TemporaryRegistration:\n\t\treturn {\n\t\t\ttr::lng_passport_address_temporary(tr::now),\n\t\t\ttr::lng_passport_address_temporary_upload(tr::now),\n\t\t};\n\tdefault: Unexpected(\"Value type in DocumentRowByType.\");\n\t}\n}\n\nQString DocumentName(Value::Type type) {\n\treturn DocumentRowByType(type).title;\n}\n\nScopeRow DocumentsOneOfRow(\n\t\tconst Scope &scope,\n\t\tconst QString &severalTitle,\n\t\tconst QString &severalDescription) {\n\tExpects(!scope.documents.empty());\n\n\tconst auto &documents = scope.documents;\n\tif (documents.size() == 1) {\n\t\tconst auto type = documents.front()->type;\n\t\treturn DocumentRowByType(type);\n\t} else if (documents.size() == 2) {\n\t\tconst auto type1 = documents.front()->type;\n\t\tconst auto type2 = documents.back()->type;\n\t\treturn {\n\t\t\ttr::lng_passport_or_title(\n\t\t\t\ttr::now,\n\t\t\t\tlt_document,\n\t\t\t\tDocumentName(type1),\n\t\t\t\tlt_second_document,\n\t\t\t\tDocumentName(type2)),\n\t\t\tseveralDescription,\n\t\t};\n\t}\n\treturn {\n\t\tseveralTitle,\n\t\tseveralDescription,\n\t};\n}\n\nQString ComputeScopeRowReadyString(const Scope &scope) {\n\tswitch (scope.type) {\n\tcase Scope::Type::PersonalDetails:\n\tcase Scope::Type::Identity:\n\tcase Scope::Type::AddressDetails:\n\tcase Scope::Type::Address: {\n\t\tauto list = std::vector<std::pair<QString, QString>>();\n\t\tconst auto pushListValue = [&](\n\t\t\t\tconst QString &key,\n\t\t\t\tconst QString &value,\n\t\t\t\tconst QString &keyForAttachmentTo = QString()) {\n\t\t\tif (keyForAttachmentTo.isEmpty()) {\n\t\t\t\tlist.push_back({ key, value.trimmed() });\n\t\t\t} else {\n\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\tlist,\n\t\t\t\t\tkeyForAttachmentTo,\n\t\t\t\t\t[](const std::pair<QString, QString> &value) {\n\t\t\t\t\t\treturn value.first;\n\t\t\t\t\t});\n\t\t\t\tAssert(i != end(list));\n\t\t\t\tif (const auto data = value.trimmed(); !data.isEmpty()) {\n\t\t\t\t\tif (i->second.isEmpty()) {\n\t\t\t\t\t\ti->second = data;\n\t\t\t\t\t} else {\n\t\t\t\t\t\ti->second += ' ' + data;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tconst auto fields = scope.details\n\t\t\t? &scope.details->data.parsed.fields\n\t\t\t: nullptr;\n\t\tconst auto document = [&]() -> const Value* {\n\t\t\tfor (const auto &document : scope.documents) {\n\t\t\t\tif (document->scansAreFilled()) {\n\t\t\t\t\treturn document;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nullptr;\n\t\t}();\n\t\tif (!scope.documents.empty() && !document) {\n\t\t\treturn QString();\n\t\t}\n\t\tif ((document && scope.documents.size() > 1)\n\t\t\t|| (!scope.details\n\t\t\t\t&& (ScopeTypeForValueType(document->type)\n\t\t\t\t\t== Scope::Type::Address))) {\n\t\t\tpushListValue(\"_type\", DocumentName(document->type));\n\t\t}\n\t\tconst auto scheme = GetDocumentScheme(\n\t\t\tscope.type,\n\t\t\tdocument ? base::make_optional(document->type) : std::nullopt,\n\t\t\tscope.details ? scope.details->nativeNames : false,\n\t\t\tnullptr);\n\t\tusing ValueClass = EditDocumentScheme::ValueClass;\n\t\tconst auto skipAdditional = [&] {\n\t\t\tif (!fields) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfor (const auto &row : scheme.rows) {\n\t\t\t\tif (row.valueClass == ValueClass::Additional) {\n\t\t\t\t\tconst auto i = fields->find(row.key);\n\t\t\t\t\tconst auto native = (i == end(*fields))\n\t\t\t\t\t\t? QString()\n\t\t\t\t\t\t: i->second.text;\n\t\t\t\t\tconst auto j = fields->find(row.additionalFallbackKey);\n\t\t\t\t\tconst auto latin = (j == end(*fields))\n\t\t\t\t\t\t? QString()\n\t\t\t\t\t\t: j->second.text;\n\t\t\t\t\tif (latin != native) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}();\n\t\tfor (const auto &row : scheme.rows) {\n\t\t\tconst auto format = row.format;\n\t\t\tif (row.valueClass != ValueClass::Scans) {\n\t\t\t\tif (!fields) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (row.valueClass == ValueClass::Additional\n\t\t\t\t\t&& skipAdditional) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst auto i = fields->find(row.key);\n\t\t\t\tconst auto text = (i == end(*fields))\n\t\t\t\t\t? QString()\n\t\t\t\t\t: i->second.text;\n\t\t\t\tif (row.error && row.error(text).has_value()) {\n\t\t\t\t\treturn QString();\n\t\t\t\t}\n\t\t\t\tpushListValue(\n\t\t\t\t\trow.key,\n\t\t\t\t\tformat ? format(text) : text,\n\t\t\t\t\trow.keyForAttachmentTo);\n\t\t\t} else if (scope.documents.empty()) {\n\t\t\t\tcontinue;\n\t\t\t} else {\n\t\t\t\tconst auto i = document->data.parsed.fields.find(row.key);\n\t\t\t\tconst auto text = (i == end(document->data.parsed.fields))\n\t\t\t\t\t? QString()\n\t\t\t\t\t: i->second.text;\n\t\t\t\tif (row.error && row.error(text).has_value()) {\n\t\t\t\t\treturn QString();\n\t\t\t\t}\n\t\t\t\tpushListValue(row.key, text, row.keyForAttachmentTo);\n\t\t\t}\n\t\t}\n\t\treturn JoinScopeRowReadyString(std::move(list));\n\t} break;\n\tcase Scope::Type::Phone:\n\tcase Scope::Type::Email: {\n\t\tAssert(scope.details != nullptr);\n\t\tconst auto format = GetContactScheme(scope.type).format;\n\t\tconst auto &fields = scope.details->data.parsed.fields;\n\t\tconst auto i = fields.find(\"value\");\n\t\treturn (i != end(fields))\n\t\t\t? (format ? format(i->second.text) : i->second.text)\n\t\t\t: QString();\n\t} break;\n\t}\n\tUnexpected(\"Scope type in ComputeScopeRowReadyString.\");\n}\n\nScopeRow ComputeScopeRow(const Scope &scope) {\n\tconst auto addReadyError = [&](\n\t\t\tScopeRow &&row,\n\t\t\tQString titleFallback = QString()) {\n\t\trow.ready = ComputeScopeRowReadyString(scope);\n\t\tauto errors = QStringList();\n\t\tconst auto addValueErrors = [&](not_null<const Value*> value) {\n\t\t\tif (!value->error.isEmpty()) {\n\t\t\t\terrors.push_back(value->error);\n\t\t\t}\n\t\t\tconst auto addTypeErrors = [&](FileType type) {\n\t\t\t\tif (!value->fileMissingError(type).isEmpty()) {\n\t\t\t\t\terrors.push_back(value->fileMissingError(type));\n\t\t\t\t}\n\t\t\t\tfor (const auto &scan : value->files(type)) {\n\t\t\t\t\tif (!scan.error.isEmpty()) {\n\t\t\t\t\t\terrors.push_back(scan.error);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\taddTypeErrors(FileType::Scan);\n\t\t\taddTypeErrors(FileType::Translation);\n\t\t\tfor (const auto &[type, scan] : value->specialScans) {\n\t\t\t\tif (!scan.error.isEmpty()) {\n\t\t\t\t\terrors.push_back(scan.error);\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const auto &[key, value] : value->data.parsed.fields) {\n\t\t\t\tif (!value.error.isEmpty()) {\n\t\t\t\t\terrors.push_back(value.error);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tconst auto document = [&]() -> const Value* {\n\t\t\tfor (const auto &document : scope.documents) {\n\t\t\t\tif (document->scansAreFilled()) {\n\t\t\t\t\treturn document;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nullptr;\n\t\t}();\n\t\tif (document) {\n\t\t\taddValueErrors(document);\n\t\t}\n\t\tif (scope.details) {\n\t\t\taddValueErrors(scope.details);\n\t\t}\n\t\tif (!errors.isEmpty()) {\n\t\t\trow.error = errors[0];// errors.join('\\n');\n\t\t} else if (row.title == row.ready && !titleFallback.isEmpty()) {\n\t\t\trow.title = titleFallback;\n\t\t}\n\n\t\tif (row.error.isEmpty()\n\t\t\t&& row.ready.isEmpty()\n\t\t\t&& !scope.documents.empty()) {\n\t\t\tif (document) {\n\t\t\t\trow.description = (scope.type == Scope::Type::Identity)\n\t\t\t\t\t? tr::lng_passport_personal_details_enter(tr::now)\n\t\t\t\t\t: tr::lng_passport_address_enter(tr::now);\n\t\t\t} else {\n\t\t\t\tconst auto best = ranges::min(\n\t\t\t\t\tscope.documents,\n\t\t\t\t\tstd::less<>(),\n\t\t\t\t\t[](not_null<const Value*> document) {\n\t\t\t\t\t\treturn document->whatNotFilled();\n\t\t\t\t\t});\n\t\t\t\tconst auto notFilled = best->whatNotFilled();\n\t\t\t\tif (notFilled & Value::kNoTranslationFilled) {\n\t\t\t\t\trow.description = tr::lng_passport_translation_needed(tr::now);\n\t\t\t\t} else if (notFilled & Value::kNoSelfieFilled) {\n\t\t\t\t\trow.description = tr::lng_passport_identity_selfie(tr::now);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn std::move(row);\n\t};\n\tswitch (scope.type) {\n\tcase Scope::Type::PersonalDetails:\n\t\treturn addReadyError({\n\t\t\ttr::lng_passport_personal_details(tr::now),\n\t\t\ttr::lng_passport_personal_details_enter(tr::now),\n\t\t});\n\tcase Scope::Type::Identity:\n\t\treturn addReadyError(DocumentsOneOfRow(\n\t\t\tscope,\n\t\t\ttr::lng_passport_identity_title(tr::now),\n\t\t\ttr::lng_passport_identity_description(tr::now)));\n\tcase Scope::Type::AddressDetails:\n\t\treturn addReadyError({\n\t\t\ttr::lng_passport_address(tr::now),\n\t\t\ttr::lng_passport_address_enter(tr::now),\n\t\t});\n\tcase Scope::Type::Address:\n\t\treturn addReadyError(DocumentsOneOfRow(\n\t\t\tscope,\n\t\t\ttr::lng_passport_address_title(tr::now),\n\t\t\ttr::lng_passport_address_description(tr::now)));\n\tcase Scope::Type::Phone:\n\t\treturn addReadyError({\n\t\t\ttr::lng_passport_phone_title(tr::now),\n\t\t\ttr::lng_passport_phone_description(tr::now),\n\t\t});\n\tcase Scope::Type::Email:\n\t\treturn addReadyError({\n\t\t\ttr::lng_passport_email_title(tr::now),\n\t\t\ttr::lng_passport_email_description(tr::now),\n\t\t});\n\tdefault: Unexpected(\"Scope type in ComputeScopeRow.\");\n\t}\n}\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_form_view_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"passport/passport_form_controller.h\"\n#include \"base/object_ptr.h\"\n#include \"ui/layers/box_content.h\"\n\nnamespace Passport {\n\nstruct Scope {\n\tenum class Type {\n\t\tPersonalDetails,\n\t\tIdentity,\n\t\tAddressDetails,\n\t\tAddress,\n\t\tPhone,\n\t\tEmail,\n\t};\n\texplicit Scope(Type type);\n\n\tType type;\n\tconst Value *details = nullptr;\n\tstd::vector<not_null<const Value*>> documents;\n};\n\nstruct ScopeRow {\n\tQString title;\n\tQString description;\n\tQString ready;\n\tQString error;\n};\n\nbool CanHaveErrors(Value::Type type);\nbool ValidateForm(const Form &form);\nstd::vector<Scope> ComputeScopes(const Form &form);\nQString ComputeScopeRowReadyString(const Scope &scope);\nScopeRow ComputeScopeRow(const Scope &scope);\n\nclass ViewController {\npublic:\n\tvirtual void showAskPassword() = 0;\n\tvirtual void showNoPassword() = 0;\n\tvirtual void showCriticalError(const QString &error) = 0;\n\tvirtual void showUpdateAppBox() = 0;\n\tvirtual void editScope(int index) = 0;\n\n\tvirtual void showBox(\n\t\tobject_ptr<Ui::BoxContent> box,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) = 0;\n\tvirtual void showToast(const QString &text) = 0;\n\tvirtual void suggestReset(Fn<void()> callback) = 0;\n\n\tvirtual int closeGetDuration() = 0;\n\n\tvirtual ~ViewController() {\n\t}\n\n\ttemplate <typename BoxType>\n\tbase::weak_qptr<BoxType> show(\n\t\t\tobject_ptr<BoxType> box,\n\t\t\tUi::LayerOptions options = Ui::LayerOption::KeepOther,\n\t\t\tanim::type animated = anim::type::normal) {\n\t\tauto result = base::weak_qptr<BoxType>(box.data());\n\t\tshowBox(std::move(box), options, animated);\n\t\treturn result;\n\t}\n\n};\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_panel.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"passport/passport_panel.h\"\n\n#include \"passport/passport_panel_controller.h\"\n#include \"passport/passport_panel_form.h\"\n#include \"passport/passport_panel_password.h\"\n#include \"ui/widgets/separate_panel.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_passport.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_calls.h\"\n\nnamespace Passport {\n\nPanel::Panel(not_null<PanelController*> controller)\n: _controller(controller)\n, _widget(std::make_unique<Ui::SeparatePanel>(Ui::SeparatePanelArgs{\n\t.onAllSpaces = true,\n})) {\n\t_widget->setTitle(tr::lng_passport_title());\n\t_widget->setInnerSize(st::passportPanelSize);\n\n\t_widget->closeRequests(\n\t) | rpl::on_next([=] {\n\t\t_controller->cancelAuth();\n\t}, _widget->lifetime());\n\n\t_widget->closeEvents(\n\t) | rpl::on_next([=] {\n\t\t_controller->cancelAuthSure();\n\t}, _widget->lifetime());\n}\n\nrpl::producer<> Panel::backRequests() const {\n\treturn _widget->backRequests();\n}\n\nvoid Panel::setBackAllowed(bool allowed) {\n\t_widget->setBackAllowed(allowed);\n}\n\nnot_null<Ui::RpWidget*> Panel::widget() const {\n\treturn _widget.get();\n}\n\nint Panel::hideAndDestroyGetDuration() {\n\treturn _widget->hideGetDuration();\n}\n\nvoid Panel::showAskPassword() {\n\t_widget->showInner(\n\t\tbase::make_unique_q<PanelAskPassword>(_widget.get(), _controller));\n\tsetBackAllowed(false);\n}\n\nvoid Panel::showNoPassword() {\n\t_widget->showInner(\n\t\tbase::make_unique_q<PanelNoPassword>(_widget.get(), _controller));\n\tsetBackAllowed(false);\n}\n\nvoid Panel::showCriticalError(const QString &error) {\n\tauto container = base::make_unique_q<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\t_widget.get(),\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_widget.get(),\n\t\t\terror,\n\t\t\tst::passportErrorLabel),\n\t\tstyle::margins(0, st::passportPanelSize.height() / 3, 0, 0));\n\tcontainer->widthValue(\n\t) | rpl::on_next([label = container->entity()](int width) {\n\t\tlabel->resize(width, label->height());\n\t}, container->lifetime());\n\n\t_widget->showInner(std::move(container));\n\tsetBackAllowed(false);\n}\n\nvoid Panel::showForm() {\n\t_widget->showInner(\n\t\tbase::make_unique_q<PanelForm>(_widget.get(), _controller));\n\tsetBackAllowed(false);\n}\n\nvoid Panel::showEditValue(object_ptr<Ui::RpWidget> from) {\n\t_widget->showInner(base::unique_qptr<Ui::RpWidget>(from.data()));\n}\n\nvoid Panel::showBox(\n\t\tobject_ptr<Ui::BoxContent> box,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) {\n\t_widget->showBox(std::move(box), options, animated);\n\t_widget->showAndActivate();\n}\n\nvoid Panel::showToast(const QString &text) {\n\t_widget->showToast({ text });\n}\n\nPanel::~Panel() = default;\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_panel.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/layer_widget.h\"\n\nnamespace Ui {\nclass RpWidget;\nclass SeparatePanel;\nclass BoxContent;\n} // namespace Ui\n\nnamespace Passport {\n\nclass PanelController;\n\nclass Panel {\npublic:\n\tPanel(not_null<PanelController*> controller);\n\n\tint hideAndDestroyGetDuration();\n\n\tvoid showAskPassword();\n\tvoid showNoPassword();\n\tvoid showForm();\n\tvoid showCriticalError(const QString &error);\n\tvoid showEditValue(object_ptr<Ui::RpWidget> form);\n\tvoid showBox(\n\t\tobject_ptr<Ui::BoxContent> box,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated);\n\tvoid showToast(const QString &text);\n\n\trpl::producer<> backRequests() const;\n\tvoid setBackAllowed(bool allowed);\n\n\tnot_null<Ui::RpWidget*> widget() const;\n\n\t~Panel();\n\nprivate:\n\tnot_null<PanelController*> _controller;\n\tstd::unique_ptr<Ui::SeparatePanel> _widget;\n\n};\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_panel_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"passport/passport_panel_controller.h\"\n\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"passport/passport_panel_edit_document.h\"\n#include \"passport/passport_panel_edit_contact.h\"\n#include \"passport/passport_panel_edit_scans.h\"\n#include \"passport/passport_panel.h\"\n#include \"passport/ui/passport_details_row.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/passcode_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"window/window_session_controller.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/countryinput.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/widgets/sent_code_field.h\"\n#include \"core/update_checker.h\"\n#include \"countries/countries_instance.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Passport {\nnamespace {\n\nconstexpr auto kMaxNameSize = 255;\nconstexpr auto kMaxDocumentSize = 24;\nconstexpr auto kMaxStreetSize = 64;\nconstexpr auto kMinCitySize = 2;\nconstexpr auto kMaxCitySize = 64;\nconstexpr auto kMaxPostcodeSize = 10;\nconst auto kLanguageNamePrefix = \"cloud_lng_passport_in_\";\n\nScanInfo CollectScanInfo(const EditFile &file) {\n\tconst auto status = [&] {\n\t\tif (file.fields.accessHash) {\n\t\t\tswitch (file.fields.downloadStatus.status()) {\n\t\t\tcase LoadStatus::Status::Failed:\n\t\t\t\treturn tr::lng_attach_failed(tr::now);\n\t\t\tcase LoadStatus::Status::InProgress:\n\t\t\t\treturn Ui::FormatDownloadText(\n\t\t\t\t\tfile.fields.downloadStatus.offset(),\n\t\t\t\t\tfile.fields.size);\n\t\t\tcase LoadStatus::Status::Done:\n\t\t\t\treturn tr::lng_passport_scan_uploaded(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_date,\n\t\t\t\t\tlangDateTimeFull(\n\t\t\t\t\t\tbase::unixtime::parse(file.fields.date)));\n\t\t\t}\n\t\t\tUnexpected(\"LoadStatus value in CollectScanInfo.\");\n\t\t} else if (file.uploadData) {\n\t\t\tswitch (file.uploadData->status.status()) {\n\t\t\tcase LoadStatus::Status::Failed:\n\t\t\t\treturn tr::lng_attach_failed(tr::now);\n\t\t\tcase LoadStatus::Status::InProgress:\n\t\t\t\treturn Ui::FormatDownloadText(\n\t\t\t\t\tfile.uploadData->status.offset(),\n\t\t\t\t\tfile.uploadData->bytes.size());\n\t\t\tcase LoadStatus::Status::Done:\n\t\t\t\treturn tr::lng_passport_scan_uploaded(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_date,\n\t\t\t\t\tlangDateTimeFull(\n\t\t\t\t\t\tbase::unixtime::parse(file.fields.date)));\n\t\t\t}\n\t\t\tUnexpected(\"LoadStatus value in CollectScanInfo.\");\n\t\t} else {\n\t\t\treturn Ui::FormatDownloadText(0, file.fields.size);\n\t\t}\n\t}();\n\treturn {\n\t\tfile.type,\n\t\tFileKey{ file.fields.id },\n\t\t!file.fields.error.isEmpty() ? file.fields.error : status,\n\t\tfile.fields.image,\n\t\tfile.deleted,\n\t\tfile.fields.error };\n}\n\nScanListData PrepareScanListData(const Value &value, FileType type) {\n\tauto result = ScanListData();\n\tfor (const auto &scan : value.filesInEdit(type)) {\n\t\tresult.files.push_back(CollectScanInfo(scan));\n\t}\n\tresult.errorMissing = value.fileMissingError(type);\n\treturn result;\n}\n\nstd::map<FileType, ScanInfo> PrepareSpecialFiles(const Value &value) {\n\tauto result = std::map<FileType, ScanInfo>();\n\tconst auto types = {\n\t\tFileType::FrontSide,\n\t\tFileType::ReverseSide,\n\t\tFileType::Selfie\n\t};\n\tfor (const auto type : types) {\n\t\tif (value.requiresSpecialScan(type)) {\n\t\t\tconst auto i = value.specialScansInEdit.find(type);\n\t\t\tresult.emplace(\n\t\t\t\ttype,\n\t\t\t\t(i != end(value.specialScansInEdit)\n\t\t\t\t\t? CollectScanInfo(i->second)\n\t\t\t\t\t: ScanInfo(type)));\n\t\t}\n\t}\n\treturn result;\n}\n\n} // namespace\n\nEditDocumentScheme GetDocumentScheme(\n\t\tScope::Type type,\n\t\tstd::optional<Value::Type> scansType,\n\t\tbool nativeNames,\n\t\tpreferredLangCallback &&preferredLanguage) {\n\tusing Scheme = EditDocumentScheme;\n\tusing ValueClass = Scheme::ValueClass;\n\tconst auto DontFormat = nullptr;\n\tconst auto CountryFormat = [](const QString &value) {\n\t\tconst auto result = Countries::Instance().countryNameByISO2(value);\n\t\treturn result.isEmpty() ? value : result;\n\t};\n\tconst auto GenderFormat = [](const QString &value) {\n\t\tif (value == u\"male\"_q) {\n\t\t\treturn tr::lng_passport_gender_male(tr::now);\n\t\t} else if (value == u\"female\"_q) {\n\t\t\treturn tr::lng_passport_gender_female(tr::now);\n\t\t}\n\t\treturn value;\n\t};\n\tconst auto DontValidate = nullptr;\n\tconst auto FromBoolean = [](auto validation) {\n\t\treturn [=](const QString &value) {\n\t\t\treturn validation(value)\n\t\t\t\t? std::nullopt\n\t\t\t\t: base::make_optional(QString());\n\t\t};\n\t};\n\tconst auto LimitedValidate = [=](int max, int min = 1) {\n\t\treturn FromBoolean([=](const QString &value) {\n\t\t\treturn (value.size() >= min) && (value.size() <= max);\n\t\t});\n\t};\n\tusing Result = std::optional<QString>;\n\tconst auto NameValidate = [](const QString &value) -> Result {\n\t\tstatic const auto RegExp = QRegularExpression(\n\t\t\t\"^[a-zA-Z0-9\\\\.,/&\\\\-' ]+$\"\n\t\t);\n\t\tif (value.isEmpty() || value.size() > kMaxNameSize) {\n\t\t\treturn QString();\n\t\t} else if (!RegExp.match(value).hasMatch()) {\n\t\t\treturn tr::lng_passport_bad_name(tr::now);\n\t\t}\n\t\treturn std::nullopt;\n\t};\n\tconst auto NativeNameValidate = LimitedValidate(kMaxNameSize);\n\tconst auto NativeNameOrEmptyValidate = LimitedValidate(kMaxNameSize, 0);\n\tconst auto DocumentValidate = LimitedValidate(kMaxDocumentSize);\n\tconst auto StreetValidate = LimitedValidate(kMaxStreetSize);\n\tconst auto CityValidate = LimitedValidate(kMaxCitySize, kMinCitySize);\n\tconst auto PostcodeValidate = FromBoolean([](const QString &value) {\n\t\tstatic const auto RegExp = QRegularExpression(\n\t\t\tQString(\"^[a-zA-Z0-9\\\\-]{2,%1}$\").arg(kMaxPostcodeSize)\n\t\t);\n\t\treturn RegExp.match(value).hasMatch();\n\t});\n\tconst auto DateValidateBoolean = [](const QString &value) {\n\t\tstatic const auto RegExp = QRegularExpression(\n\t\t\t\"^\\\\d{2}\\\\.\\\\d{2}\\\\.\\\\d{4}$\"\n\t\t);\n\t\treturn RegExp.match(value).hasMatch();\n\t};\n\tconst auto DateValidate = FromBoolean(DateValidateBoolean);\n\tconst auto DateOrEmptyValidate = FromBoolean([=](const QString &value) {\n\t\treturn value.isEmpty() || DateValidateBoolean(value);\n\t});\n\tconst auto GenderValidate = FromBoolean([](const QString &value) {\n\t\treturn value == u\"male\"_q || value == u\"female\"_q;\n\t});\n\tconst auto CountryValidate = FromBoolean([=](const QString &value) {\n\t\treturn !CountryFormat(value).isEmpty();\n\t});\n\tconst auto NameOrEmptyValidate = [=](const QString &value) -> Result {\n\t\tif (value.isEmpty()) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\treturn NameValidate(value);\n\t};\n\n\tswitch (type) {\n\tcase Scope::Type::PersonalDetails:\n\tcase Scope::Type::Identity: {\n\t\tauto result = Scheme();\n\t\tresult.detailsHeader = tr::lng_passport_personal_details(tr::now);\n\t\tresult.fieldsHeader = tr::lng_passport_document_details(tr::now);\n\t\tif (scansType) {\n\t\t\tresult.scansHeader = [&] {\n\t\t\t\tswitch (*scansType) {\n\t\t\t\tcase Value::Type::Passport:\n\t\t\t\t\treturn tr::lng_passport_identity_passport(tr::now);\n\t\t\t\tcase Value::Type::DriverLicense:\n\t\t\t\t\treturn tr::lng_passport_identity_license(tr::now);\n\t\t\t\tcase Value::Type::IdentityCard:\n\t\t\t\t\treturn tr::lng_passport_identity_card(tr::now);\n\t\t\t\tcase Value::Type::InternalPassport:\n\t\t\t\t\treturn tr::lng_passport_identity_internal(tr::now);\n\t\t\t\tdefault:\n\t\t\t\t\tUnexpected(\"scansType in GetDocumentScheme:Identity.\");\n\t\t\t\t}\n\t\t\t}();\n\t\t}\n\t\tresult.rows = {\n\t\t\t{\n\t\t\t\tValueClass::Fields,\n\t\t\t\tUi::PanelDetailsType::Text,\n\t\t\t\t\"first_name\"_q,\n\t\t\t\ttr::lng_passport_first_name(tr::now),\n\t\t\t\tNameValidate,\n\t\t\t\tDontFormat,\n\t\t\t\tkMaxNameSize,\n\t\t\t},\n\t\t\t{\n\t\t\t\tValueClass::Fields,\n\t\t\t\tUi::PanelDetailsType::Text,\n\t\t\t\t\"middle_name\"_q,\n\t\t\t\ttr::lng_passport_middle_name(tr::now),\n\t\t\t\tNameOrEmptyValidate,\n\t\t\t\tDontFormat,\n\t\t\t\tkMaxNameSize,\n\t\t\t\t\"first_name\"_q,\n\t\t\t},\n\t\t\t{\n\t\t\t\tValueClass::Fields,\n\t\t\t\tUi::PanelDetailsType::Text,\n\t\t\t\t\"last_name\"_q,\n\t\t\t\ttr::lng_passport_last_name(tr::now),\n\t\t\t\tNameValidate,\n\t\t\t\tDontFormat,\n\t\t\t\tkMaxNameSize,\n\t\t\t\t\"first_name\"_q,\n\t\t\t},\n\t\t\t{\n\t\t\t\tValueClass::Fields,\n\t\t\t\tUi::PanelDetailsType::Date,\n\t\t\t\t\"birth_date\"_q,\n\t\t\t\ttr::lng_passport_birth_date(tr::now),\n\t\t\t\tDateValidate,\n\t\t\t\tDontFormat,\n\t\t\t},\n\t\t\t{\n\t\t\t\tValueClass::Fields,\n\t\t\t\tUi::PanelDetailsType::Gender,\n\t\t\t\t\"gender\"_q,\n\t\t\t\ttr::lng_passport_gender(tr::now),\n\t\t\t\tGenderValidate,\n\t\t\t\tGenderFormat,\n\t\t\t},\n\t\t\t{\n\t\t\t\tValueClass::Fields,\n\t\t\t\tUi::PanelDetailsType::Country,\n\t\t\t\t\"country_code\"_q,\n\t\t\t\ttr::lng_passport_country(tr::now),\n\t\t\t\tCountryValidate,\n\t\t\t\tCountryFormat,\n\t\t\t},\n\t\t\t{\n\t\t\t\tValueClass::Fields,\n\t\t\t\tUi::PanelDetailsType::Country,\n\t\t\t\t\"residence_country_code\"_q,\n\t\t\t\ttr::lng_passport_residence_country(tr::now),\n\t\t\t\tCountryValidate,\n\t\t\t\tCountryFormat,\n\t\t\t},\n\t\t\t{\n\t\t\t\tValueClass::Scans,\n\t\t\t\tUi::PanelDetailsType::Text,\n\t\t\t\t\"document_no\"_q,\n\t\t\t\ttr::lng_passport_document_number(tr::now),\n\t\t\t\tDocumentValidate,\n\t\t\t\tDontFormat,\n\t\t\t\tkMaxDocumentSize,\n\t\t\t},\n\t\t\t{\n\t\t\t\tValueClass::Scans,\n\t\t\t\tUi::PanelDetailsType::Date,\n\t\t\t\t\"expiry_date\"_q,\n\t\t\t\ttr::lng_passport_expiry_date(tr::now),\n\t\t\t\tDateOrEmptyValidate,\n\t\t\t\tDontFormat,\n\t\t\t},\n\t\t};\n\t\tif (nativeNames) {\n\t\t\tresult.additionalDependencyKey = \"residence_country_code\"_q;\n\n\t\t\tresult.preferredLanguage = preferredLanguage\n\t\t\t\t? std::move(preferredLanguage)\n\t\t\t\t: [](const QString &) {\n\t\t\t\t\treturn rpl::single(EditDocumentCountry());\n\t\t\t\t};\n\t\t\tconst auto languageValue = [](const QString &langCode) {\n\t\t\t\treturn Lang::GetNonDefaultValue(kLanguageNamePrefix\n\t\t\t\t\t+ langCode.toUtf8());\n\t\t\t};\n\t\t\tresult.additionalHeader = [=](const EditDocumentCountry &info) {\n\t\t\t\tconst auto language = languageValue(info.languageCode);\n\t\t\t\treturn language.isEmpty()\n\t\t\t\t\t? tr::lng_passport_native_name_title(tr::now)\n\t\t\t\t\t: tr::lng_passport_native_name_language(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_language,\n\t\t\t\t\t\tlanguage);\n\t\t\t};\n\t\t\tresult.additionalDescription = [=](\n\t\t\t\t\tconst EditDocumentCountry &info) {\n\t\t\t\tconst auto language = languageValue(info.languageCode);\n\t\t\t\tif (!language.isEmpty()) {\n\t\t\t\t\treturn tr::lng_passport_native_name_language_about(\n\t\t\t\t\t\ttr::now);\n\t\t\t\t}\n\t\t\t\tconst auto name = Countries::Instance().countryNameByISO2(\n\t\t\t\t\tinfo.countryCode);\n\t\t\t\tAssert(!name.isEmpty());\n\t\t\t\treturn tr::lng_passport_native_name_about(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_country,\n\t\t\t\t\tname);\n\t\t\t};\n\t\t\tresult.additionalShown = [](const EditDocumentCountry &info) {\n\t\t\t\tusing Result = EditDocumentScheme::AdditionalVisibility;\n\t\t\t\treturn (info.countryCode.isEmpty())\n\t\t\t\t\t? Result::Hidden\n\t\t\t\t\t: (info.languageCode == \"en\")\n\t\t\t\t\t? Result::OnlyIfError\n\t\t\t\t\t: Result::Shown;\n\t\t\t};\n\t\t\tusing Row = EditDocumentScheme::Row;\n\t\t\tauto additional = std::initializer_list<Row>{\n\t\t\t\t{\n\t\t\t\t\tValueClass::Additional,\n\t\t\t\t\tUi::PanelDetailsType::Text,\n\t\t\t\t\t\"first_name_native\"_q,\n\t\t\t\t\ttr::lng_passport_first_name(tr::now),\n\t\t\t\t\tNativeNameValidate,\n\t\t\t\t\tDontFormat,\n\t\t\t\t\tkMaxNameSize,\n\t\t\t\t\tQString(),\n\t\t\t\t\t\"first_name\"_q,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tValueClass::Additional,\n\t\t\t\t\tUi::PanelDetailsType::Text,\n\t\t\t\t\t\"middle_name_native\"_q,\n\t\t\t\t\ttr::lng_passport_middle_name(tr::now),\n\t\t\t\t\tNativeNameOrEmptyValidate,\n\t\t\t\t\tDontFormat,\n\t\t\t\t\tkMaxNameSize,\n\t\t\t\t\t\"first_name_native\"_q,\n\t\t\t\t\t\"middle_name\"_q,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tValueClass::Additional,\n\t\t\t\t\tUi::PanelDetailsType::Text,\n\t\t\t\t\t\"last_name_native\"_q,\n\t\t\t\t\ttr::lng_passport_last_name(tr::now),\n\t\t\t\t\tNativeNameValidate,\n\t\t\t\t\tDontFormat,\n\t\t\t\t\tkMaxNameSize,\n\t\t\t\t\t\"first_name_native\"_q,\n\t\t\t\t\t\"last_name\"_q,\n\t\t\t\t},\n\t\t\t};\n\t\t\tfor (auto &row : additional) {\n\t\t\t\tresult.rows.push_back(std::move(row));\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t} break;\n\n\tcase Scope::Type::AddressDetails:\n\tcase Scope::Type::Address: {\n\t\tauto result = Scheme();\n\t\tresult.detailsHeader = tr::lng_passport_address(tr::now);\n\t\tif (scansType) {\n\t\t\tswitch (*scansType) {\n\t\t\tcase Value::Type::UtilityBill:\n\t\t\t\tresult.scansHeader = tr::lng_passport_address_bill(tr::now);\n\t\t\t\tbreak;\n\t\t\tcase Value::Type::BankStatement:\n\t\t\t\tresult.scansHeader = tr::lng_passport_address_statement(tr::now);\n\t\t\t\tbreak;\n\t\t\tcase Value::Type::RentalAgreement:\n\t\t\t\tresult.scansHeader = tr::lng_passport_address_agreement(tr::now);\n\t\t\t\tbreak;\n\t\t\tcase Value::Type::PassportRegistration:\n\t\t\t\tresult.scansHeader = tr::lng_passport_address_registration(tr::now);\n\t\t\t\tbreak;\n\t\t\tcase Value::Type::TemporaryRegistration:\n\t\t\t\tresult.scansHeader = tr::lng_passport_address_temporary(tr::now);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tUnexpected(\"scansType in GetDocumentScheme:Address.\");\n\t\t\t}\n\t\t}\n\t\tresult.rows = {\n\t\t\t{\n\t\t\t\tValueClass::Fields,\n\t\t\t\tUi::PanelDetailsType::Text,\n\t\t\t\t\"street_line1\"_q,\n\t\t\t\ttr::lng_passport_street(tr::now),\n\t\t\t\tStreetValidate,\n\t\t\t\tDontFormat,\n\t\t\t\tkMaxStreetSize,\n\t\t\t},\n\t\t\t{\n\t\t\t\tValueClass::Fields,\n\t\t\t\tUi::PanelDetailsType::Text,\n\t\t\t\t\"street_line2\"_q,\n\t\t\t\ttr::lng_passport_street(tr::now),\n\t\t\t\tDontValidate,\n\t\t\t\tDontFormat,\n\t\t\t\tkMaxStreetSize,\n\t\t\t},\n\t\t\t{\n\t\t\t\tValueClass::Fields,\n\t\t\t\tUi::PanelDetailsType::Text,\n\t\t\t\t\"city\"_q,\n\t\t\t\ttr::lng_passport_city(tr::now),\n\t\t\t\tCityValidate,\n\t\t\t\tDontFormat,\n\t\t\t\tkMaxStreetSize,\n\t\t\t},\n\t\t\t{\n\t\t\t\tValueClass::Fields,\n\t\t\t\tUi::PanelDetailsType::Text,\n\t\t\t\t\"state\"_q,\n\t\t\t\ttr::lng_passport_state(tr::now),\n\t\t\t\tDontValidate,\n\t\t\t\tDontFormat,\n\t\t\t\tkMaxStreetSize,\n\t\t\t},\n\t\t\t{\n\t\t\t\tValueClass::Fields,\n\t\t\t\tUi::PanelDetailsType::Country,\n\t\t\t\t\"country_code\"_q,\n\t\t\t\ttr::lng_passport_residence_country(tr::now),\n\t\t\t\tCountryValidate,\n\t\t\t\tCountryFormat,\n\t\t\t},\n\t\t\t{\n\t\t\t\tValueClass::Fields,\n\t\t\t\tUi::PanelDetailsType::Postcode,\n\t\t\t\t\"post_code\"_q,\n\t\t\t\ttr::lng_passport_postcode(tr::now),\n\t\t\t\tPostcodeValidate,\n\t\t\t\tDontFormat,\n\t\t\t\tkMaxPostcodeSize,\n\t\t\t},\n\t\t};\n\t\treturn result;\n\t} break;\n\t}\n\tUnexpected(\"Type in GetDocumentScheme().\");\n}\n\nEditContactScheme GetContactScheme(Scope::Type type) {\n\tusing Scheme = EditContactScheme;\n\tusing ValueType = Scheme::ValueType;\n\n\tswitch (type) {\n\tcase Scope::Type::Phone: {\n\t\tauto result = Scheme(ValueType::Phone);\n\t\tresult.aboutExisting = tr::lng_passport_use_existing_phone(tr::now);\n\t\tresult.newHeader = tr::lng_passport_new_phone(tr::now);\n\t\tresult.aboutNew = tr::lng_passport_new_phone_code(tr::now);\n\t\tresult.validate = [](const QString &value) {\n\t\t\tstatic const auto RegExp = QRegularExpression(\"^\\\\d{2,12}$\");\n\t\t\treturn RegExp.match(value).hasMatch();\n\t\t};\n\t\tresult.format = [](const QString &value) {\n\t\t\treturn Ui::FormatPhone(value);\n\t\t};\n\t\tresult.postprocess = [](QString value) {\n\t\t\treturn value.replace(\n\t\t\t\tTextUtilities::RegExpDigitsExclude(),\n\t\t\t\tQString());\n\t\t};\n\t\treturn result;\n\t} break;\n\n\tcase Scope::Type::Email: {\n\t\tauto result = Scheme(ValueType::Text);\n\t\tresult.aboutExisting = tr::lng_passport_use_existing_email(tr::now);\n\t\tresult.newHeader = tr::lng_passport_new_email(tr::now);\n\t\tresult.newPlaceholder = tr::lng_passport_email_title();\n\t\tresult.aboutNew = tr::lng_passport_new_email_code(tr::now);\n\t\tresult.validate = [](const QString &value) {\n\t\t\tconst auto at = value.indexOf('@');\n\t\t\tconst auto dot = value.lastIndexOf('.');\n\t\t\treturn (at > 0) && (dot > at);\n\t\t};\n\t\tresult.format = result.postprocess = [](const QString &value) {\n\t\t\treturn value.trimmed();\n\t\t};\n\t\treturn result;\n\t} break;\n\t}\n\tUnexpected(\"Type in GetContactScheme().\");\n}\n\nconst std::map<QString, QString> &LatinToNativeMap() {\n\tstatic const auto result = std::map<QString, QString> {\n\t\t{ \"first_name\"_q, \"first_name_native\"_q },\n\t\t{ \"last_name\"_q, \"last_name_native\"_q },\n\t\t{ \"middle_name\"_q, \"middle_name_native\"_q },\n\t};\n\treturn result;\n}\n\nconst std::map<QString, QString> &NativeToLatinMap() {\n\tstatic const auto result = std::map<QString, QString> {\n\t\t{ \"first_name_native\"_q, \"first_name\"_q },\n\t\t{ \"last_name_native\"_q, \"last_name\"_q },\n\t\t{ \"middle_name_native\"_q, \"middle_name\"_q },\n\t};\n\treturn result;\n}\n\nQString AdjustKeyName(not_null<const Value*> value, const QString &key) {\n\tif (!value->nativeNames) {\n\t\treturn key;\n\t}\n\tconst auto &map = LatinToNativeMap();\n\tconst auto i = map.find(key);\n\treturn (i == end(map)) ? key : i->second;\n}\n\nbool SkipFieldCheck(not_null<const Value*> value, const QString &key) {\n\tif (value->type != Value::Type::PersonalDetails) {\n\t\treturn false;\n\t}\n\tconst auto &dontCheckNames = value->nativeNames\n\t\t? LatinToNativeMap()\n\t\t: NativeToLatinMap();\n\treturn dontCheckNames.find(key) != end(dontCheckNames);\n}\n\nScanInfo::ScanInfo(FileType type) : type(type) {\n}\n\nScanInfo::ScanInfo(\n\tFileType type,\n\tconst FileKey &key,\n\tconst QString &status,\n\tconst QImage &thumb,\n\tbool deleted,\n\tconst QString &error)\n: type(type)\n, key(key)\n, status(status)\n, thumb(thumb)\n, deleted(deleted)\n, error(error) {\n}\n\nPanelController::PanelController(not_null<FormController*> form)\n: _form(form)\n, _scopes(ComputeScopes(_form->form())) {\n\t_form->secretReadyEvents(\n\t) | rpl::on_next([=] {\n\t\tensurePanelCreated();\n\t\t_panel->showForm();\n\t}, lifetime());\n\n\t_form->verificationNeeded(\n\t) | rpl::on_next([=](not_null<const Value*> value) {\n\t\tprocessVerificationNeeded(value);\n\t}, lifetime());\n\n\t_form->verificationUpdate(\n\t) | rpl::filter([=](not_null<const Value*> field) {\n\t\treturn (field->verification.codeLength == 0);\n\t}) | rpl::on_next([=](not_null<const Value*> field) {\n\t\t_verificationBoxes.erase(field);\n\t}, lifetime());\n}\n\nnot_null<UserData*> PanelController::bot() const {\n\treturn _form->bot();\n}\n\nQString PanelController::privacyPolicyUrl() const {\n\treturn _form->privacyPolicyUrl();\n}\n\nvoid PanelController::fillRows(\n\tFn<void(\n\t\tQString title,\n\t\tQString description,\n\t\tbool ready,\n\t\tbool error)> callback) {\n\tif (_scopes.empty()) {\n\t\t_scopes = ComputeScopes(_form->form());\n\t}\n\tfor (const auto &scope : _scopes) {\n\t\tconst auto row = ComputeScopeRow(scope);\n\t\tconst auto main = scope.details\n\t\t\t? not_null<const Value*>(scope.details)\n\t\t\t: scope.documents[0];\n\t\tif (main && !row.ready.isEmpty()) {\n\t\t\t_submitErrors.erase(\n\t\t\t\tranges::remove(_submitErrors, main),\n\t\t\t\t_submitErrors.end());\n\t\t}\n\t\tconst auto submitError = base::contains(_submitErrors, main);\n\t\tcallback(\n\t\t\trow.title,\n\t\t\t(!row.error.isEmpty()\n\t\t\t\t? row.error\n\t\t\t\t: !row.ready.isEmpty()\n\t\t\t\t? row.ready\n\t\t\t\t: row.description),\n\t\t\t!row.ready.isEmpty(),\n\t\t\t!row.error.isEmpty() || submitError);\n\t}\n}\n\nrpl::producer<> PanelController::refillRows() const {\n\treturn rpl::merge(\n\t\t_submitFailed.events(),\n\t\t_form->valueSaveFinished() | rpl::to_empty);\n}\n\nvoid PanelController::submitForm() {\n\t_submitErrors = _form->submitGetErrors();\n\tif (!_submitErrors.empty()) {\n\t\t_submitFailed.fire({});\n\t}\n}\n\nvoid PanelController::submitPassword(const QByteArray &password) {\n\t_form->submitPassword(password);\n}\n\nvoid PanelController::recoverPassword() {\n\t_form->recoverPassword();\n}\n\nrpl::producer<QString> PanelController::passwordError() const {\n\treturn _form->passwordError();\n}\n\nQString PanelController::passwordHint() const {\n\treturn _form->passwordSettings().hint;\n}\n\nQString PanelController::unconfirmedEmailPattern() const {\n\treturn _form->passwordSettings().unconfirmedPattern;\n}\n\nQString PanelController::defaultEmail() const {\n\treturn _form->defaultEmail();\n}\n\nQString PanelController::defaultPhoneNumber() const {\n\treturn _form->defaultPhoneNumber();\n}\n\nvoid PanelController::setupPassword() {\n\tExpects(_panel != nullptr);\n\n\tconst auto &settings = _form->passwordSettings();\n\tif (settings.unknownAlgo\n\t\t|| v::is_null(settings.newAlgo)\n\t\t|| v::is_null(settings.newSecureAlgo)) {\n\t\tshowUpdateAppBox();\n\t\treturn;\n\t} else if (settings.request) {\n\t\tshowAskPassword();\n\t\treturn;\n\t}\n\n\tauto fields = PasscodeBox::CloudFields{\n\t\t.mtp = PasscodeBox::CloudFields::Mtp{\n\t\t\t.newAlgo = settings.newAlgo,\n\t\t\t.newSecureSecretAlgo = settings.newSecureAlgo,\n\t\t},\n\t\t.hasRecovery = settings.hasRecovery,\n\t\t.pendingResetDate = settings.pendingResetDate,\n\t};\n\n\t// MSVC x64 (non-LTO) Release build fails with a linker error:\n\t// - unresolved external variant::variant(variant const &)\n\t// It looks like a MSVC bug and this works like a workaround.\n\tconst auto force = fields.mtp.newSecureSecretAlgo;\n\n\tauto box = show(Box<PasscodeBox>(&_form->window()->session(), fields));\n\tbox->newPasswordSet(\n\t) | rpl::on_next([=](const QByteArray &password) {\n\t\tif (password.isEmpty()) {\n\t\t\t_form->reloadPassword();\n\t\t} else {\n\t\t\t_form->reloadAndSubmitPassword(password);\n\t\t}\n\t}, box->lifetime());\n\n\tbox->passwordReloadNeeded(\n\t) | rpl::on_next([=] {\n\t\t_form->reloadPassword();\n\t}, box->lifetime());\n\n\tbox->clearUnconfirmedPassword(\n\t) | rpl::on_next([=] {\n\t\t_form->cancelPassword();\n\t}, box->lifetime());\n}\n\nvoid PanelController::cancelPasswordSubmit() {\n\tshow(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_passport_stop_password_sure(),\n\t\t.confirmed = [=](Fn<void()> &&close) {\n\t\t\tclose();\n\t\t\t_form->cancelPassword();\n\t\t},\n\t\t.confirmText = tr::lng_passport_stop(),\n\t}));\n}\n\nvoid PanelController::validateRecoveryEmail() {\n\tauto validation = ConfirmRecoveryEmail(\n\t\t&_form->session(),\n\t\tunconfirmedEmailPattern());\n\n\tstd::move(\n\t\tvalidation.reloadRequests\n\t) | rpl::on_next([=] {\n\t\t_form->reloadPassword();\n\t}, validation.box->lifetime());\n\n\tstd::move(\n\t\tvalidation.cancelRequests\n\t) | rpl::on_next([=] {\n\t\t_form->cancelPassword();\n\t}, validation.box->lifetime());\n\n\tshow(std::move(validation.box));\n}\n\nbool PanelController::canAddScan(FileType type) const {\n\tExpects(_editScope != nullptr);\n\tExpects(_editDocument != nullptr);\n\n\treturn _form->canAddScan(_editDocument, type);\n}\n\nvoid PanelController::uploadScan(FileType type, QByteArray &&content) {\n\tExpects(_editScope != nullptr);\n\tExpects(_editDocument != nullptr);\n\tExpects(_editDocument->requiresScan(type));\n\n\t_form->uploadScan(_editDocument, type, std::move(content));\n}\n\nvoid PanelController::deleteScan(\n\t\tFileType type,\n\t\tstd::optional<int> fileIndex) {\n\tExpects(_editScope != nullptr);\n\tExpects(_editDocument != nullptr);\n\tExpects(_editDocument->requiresScan(type));\n\n\t_form->deleteScan(_editDocument, type, fileIndex);\n}\n\nvoid PanelController::restoreScan(\n\t\tFileType type,\n\t\tstd::optional<int> fileIndex) {\n\tExpects(_editScope != nullptr);\n\tExpects(_editDocument != nullptr);\n\tExpects(_editDocument->requiresScan(type));\n\n\t_form->restoreScan(_editDocument, type, fileIndex);\n}\n\nrpl::producer<ScanInfo> PanelController::scanUpdated() const {\n\treturn _form->scanUpdated(\n\t) | rpl::filter([=](not_null<const EditFile*> file) {\n\t\treturn (file->value == _editDocument);\n\t}) | rpl::map([](not_null<const EditFile*> file) {\n\t\treturn CollectScanInfo(*file);\n\t});\n}\n\nrpl::producer<ScopeError> PanelController::saveErrors() const {\n\treturn _saveErrors.events();\n}\n\nstd::vector<ScopeError> PanelController::collectSaveErrors(\n\t\tnot_null<const Value*> value) const {\n\tauto result = std::vector<ScopeError>();\n\tfor (const auto &[key, value] : value->data.parsedInEdit.fields) {\n\t\tif (!value.error.isEmpty()) {\n\t\t\tresult.push_back({ key, value.error });\n\t\t}\n\t}\n\treturn result;\n}\n\nauto PanelController::deleteValueLabel() const\n-> std::optional<rpl::producer<QString>> {\n\tExpects(_editScope != nullptr);\n\n\tif (hasValueDocument()) {\n\t\treturn tr::lng_passport_delete_document();\n\t} else if (!hasValueFields()) {\n\t\treturn std::nullopt;\n\t}\n\tswitch (_editScope->type) {\n\tcase Scope::Type::PersonalDetails:\n\tcase Scope::Type::Identity:\n\t\treturn tr::lng_passport_delete_details();\n\tcase Scope::Type::AddressDetails:\n\tcase Scope::Type::Address:\n\t\treturn tr::lng_passport_delete_address();\n\tcase Scope::Type::Email:\n\t\treturn tr::lng_passport_delete_email();\n\tcase Scope::Type::Phone:\n\t\treturn tr::lng_passport_delete_phone();\n\t}\n\tUnexpected(\"Type in PanelController::deleteValueLabel.\");\n}\n\nbool PanelController::hasValueDocument() const {\n\tExpects(_editScope != nullptr);\n\n\tif (!_editDocument) {\n\t\treturn false;\n\t}\n\treturn !_editDocument->data.parsed.fields.empty()\n\t\t|| !_editDocument->files(FileType::Scan).empty()\n\t\t|| !_editDocument->files(FileType::Translation).empty()\n\t\t|| !_editDocument->specialScans.empty();\n}\n\nbool PanelController::hasValueFields() const {\n\treturn _editValue && !_editValue->data.parsed.fields.empty();\n}\n\nvoid PanelController::deleteValue() {\n\tExpects(_editScope != nullptr);\n\tExpects(hasValueDocument() || hasValueFields());\n\n\tif (savingScope()) {\n\t\treturn;\n\t}\n\tconst auto text = [&] {\n\t\tswitch (_editScope->type) {\n\t\tcase Scope::Type::PersonalDetails:\n\t\t\treturn tr::lng_passport_delete_details_sure(tr::now);\n\t\tcase Scope::Type::Identity:\n\t\t\treturn tr::lng_passport_delete_document_sure(tr::now);\n\t\tcase Scope::Type::AddressDetails:\n\t\t\treturn tr::lng_passport_delete_address_sure(tr::now);\n\t\tcase Scope::Type::Address:\n\t\t\treturn tr::lng_passport_delete_document_sure(tr::now);\n\t\tcase Scope::Type::Phone:\n\t\t\treturn tr::lng_passport_delete_phone_sure(tr::now);\n\t\tcase Scope::Type::Email:\n\t\t\treturn tr::lng_passport_delete_email_sure(tr::now);\n\t\t}\n\t\tUnexpected(\"Type in deleteValue.\");\n\t}();\n\tconst auto checkbox = (hasValueDocument() && hasValueFields()) ? [&] {\n\t\tswitch (_editScope->type) {\n\t\tcase Scope::Type::Identity:\n\t\t\treturn tr::lng_passport_delete_details(tr::now);\n\t\tcase Scope::Type::Address:\n\t\t\treturn tr::lng_passport_delete_address(tr::now);\n\t\t}\n\t\tUnexpected(\"Type in deleteValue.\");\n\t}() : QString();\n\n\t_editScopeBoxes.emplace_back(show(ConfirmDeleteDocument(\n\t\t[=](bool withDetails) { deleteValueSure(withDetails); },\n\t\ttext,\n\t\tcheckbox)));\n}\n\nvoid PanelController::deleteValueSure(bool withDetails) {\n\tExpects(!withDetails || _editValue != nullptr);\n\n\tif (hasValueDocument()) {\n\t\t_form->deleteValueEdit(_editDocument);\n\t}\n\tif (withDetails || !hasValueDocument()) {\n\t\t_form->deleteValueEdit(_editValue);\n\t}\n}\n\nvoid PanelController::suggestReset(Fn<void()> callback) {\n\t_resetBox = Ui::BoxPointer(show(Ui::MakeConfirmBox({\n\t\t.text = Lang::Hard::PassportCorrupted(),\n\t\t.confirmed = [=] { resetPassport(callback); },\n\t\t.cancelled = [=] { cancelReset(); },\n\t\t.confirmText = Lang::Hard::PassportCorruptedReset(),\n\t})).get());\n}\n\nvoid PanelController::resetPassport(Fn<void()> callback) {\n\tconst auto box = show(Ui::MakeConfirmBox({\n\t\t.text = Lang::Hard::PassportCorruptedResetSure(),\n\t\t.confirmed = [=] { base::take(_resetBox); callback(); },\n\t\t.cancelled = [=] { suggestReset(callback); },\n\t\t.confirmText = Lang::Hard::PassportCorruptedReset(),\n\t\t.confirmStyle = &st::attentionBoxButton,\n\t}));\n\t_resetBox = Ui::BoxPointer(box.get());\n}\n\nvoid PanelController::cancelReset() {\n\tconst auto weak = base::take(_resetBox);\n\t_form->cancelSure();\n}\n\nQString PanelController::getDefaultContactValue(Scope::Type type) const {\n\tswitch (type) {\n\tcase Scope::Type::Phone:\n\t\treturn _form->defaultPhoneNumber();\n\tcase Scope::Type::Email:\n\t\treturn _form->defaultEmail();\n\t}\n\tUnexpected(\"Type in PanelController::getDefaultContactValue().\");\n}\n\nvoid PanelController::showAskPassword() {\n\tensurePanelCreated();\n\t_panel->showAskPassword();\n}\n\nvoid PanelController::showNoPassword() {\n\tensurePanelCreated();\n\t_panel->showNoPassword();\n}\n\nvoid PanelController::showCriticalError(const QString &error) {\n\tensurePanelCreated();\n\t_panel->showCriticalError(error);\n}\n\nvoid PanelController::showUpdateAppBox() {\n\tensurePanelCreated();\n\n\tconst auto callback = [=] {\n\t\t_form->cancelSure();\n\t\tCore::UpdateApplication();\n\t};\n\tshow(\n\t\tUi::MakeConfirmBox({\n\t\t\t.text = tr::lng_passport_app_out_of_date(),\n\t\t\t.confirmed = callback,\n\t\t\t.cancelled = [=] { _form->cancelSure(); },\n\t\t\t.confirmText = tr::lng_menu_update(),\n\t\t}),\n\t\tUi::LayerOption::KeepOther,\n\t\tanim::type::instant);\n}\n\nvoid PanelController::ensurePanelCreated() {\n\tif (!_panel) {\n\t\t_panel = std::make_unique<Panel>(this);\n\t}\n}\n\nstd::optional<int> PanelController::findBestDocumentIndex(\n\t\tconst Scope &scope) const {\n\tExpects(!scope.documents.empty());\n\n\tconst auto &documents = scope.documents;\n\tconst auto i = ranges::min_element(\n\t\tdocuments,\n\t\tstd::less<>(),\n\t\t[](not_null<const Value*> document) {\n\t\t\treturn document->whatNotFilled();\n\t\t});\n\treturn ((*i)->whatNotFilled() == Value::kNothingFilled)\n\t\t? std::nullopt\n\t\t: base::make_optional(int(i - begin(documents)));\n\treturn -1;\n}\n\nvoid PanelController::editScope(int index) {\n\tExpects(_panel != nullptr);\n\tExpects(index >= 0 && index < _scopes.size());\n\n\tconst auto &scope = _scopes[index];\n\tif (scope.documents.empty()) {\n\t\teditScope(index, std::nullopt);\n\t} else {\n\t\tconst auto documentIndex = findBestDocumentIndex(scope);\n\t\tif (documentIndex || scope.documents.size() == 1) {\n\t\t\teditScope(index, documentIndex ? *documentIndex : 0);\n\t\t} else {\n\t\t\trequestScopeFilesType(index);\n\t\t}\n\t}\n}\n\nvoid PanelController::requestScopeFilesType(int index) {\n\tExpects(_panel != nullptr);\n\tExpects(index >= 0 && index < _scopes.size());\n\n\tconst auto type = _scopes[index].type;\n\t_scopeDocumentTypeBox = [&] {\n\t\tif (type == Scope::Type::Identity) {\n\t\t\treturn show(RequestIdentityType(\n\t\t\t\t[=](int documentIndex) {\n\t\t\t\t\teditWithUpload(index, documentIndex);\n\t\t\t\t},\n\t\t\t\tranges::views::all(\n\t\t\t\t\t_scopes[index].documents\n\t\t\t\t) | ranges::views::transform([](auto value) {\n\t\t\t\t\treturn value->type;\n\t\t\t\t}) | ranges::views::transform([](Value::Type type) {\n\t\t\t\t\tswitch (type) {\n\t\t\t\t\tcase Value::Type::Passport:\n\t\t\t\t\t\treturn tr::lng_passport_identity_passport(tr::now);\n\t\t\t\t\tcase Value::Type::IdentityCard:\n\t\t\t\t\t\treturn tr::lng_passport_identity_card(tr::now);\n\t\t\t\t\tcase Value::Type::DriverLicense:\n\t\t\t\t\t\treturn tr::lng_passport_identity_license(tr::now);\n\t\t\t\t\tcase Value::Type::InternalPassport:\n\t\t\t\t\t\treturn tr::lng_passport_identity_internal(tr::now);\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tUnexpected(\"IdentityType in requestScopeFilesType\");\n\t\t\t\t\t}\n\t\t\t\t}) | ranges::to_vector));\n\t\t} else if (type == Scope::Type::Address) {\n\t\t\treturn show(RequestAddressType(\n\t\t\t\t[=](int documentIndex) {\n\t\t\t\t\teditWithUpload(index, documentIndex);\n\t\t\t\t},\n\t\t\t\tranges::views::all(\n\t\t\t\t\t_scopes[index].documents\n\t\t\t\t) | ranges::views::transform([](auto value) {\n\t\t\t\t\treturn value->type;\n\t\t\t\t}) | ranges::views::transform([](Value::Type type) {\n\t\t\t\t\tswitch (type) {\n\t\t\t\t\tcase Value::Type::UtilityBill:\n\t\t\t\t\t\treturn tr::lng_passport_address_bill(tr::now);\n\t\t\t\t\tcase Value::Type::BankStatement:\n\t\t\t\t\t\treturn tr::lng_passport_address_statement(tr::now);\n\t\t\t\t\tcase Value::Type::RentalAgreement:\n\t\t\t\t\t\treturn tr::lng_passport_address_agreement(tr::now);\n\t\t\t\t\tcase Value::Type::PassportRegistration:\n\t\t\t\t\t\treturn tr::lng_passport_address_registration(tr::now);\n\t\t\t\t\tcase Value::Type::TemporaryRegistration:\n\t\t\t\t\t\treturn tr::lng_passport_address_temporary(tr::now);\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tUnexpected(\"AddressType in requestScopeFilesType\");\n\t\t\t\t\t}\n\t\t\t\t}) | ranges::to_vector));\n\t\t} else {\n\t\t\tUnexpected(\"Type in processVerificationNeeded.\");\n\t\t}\n\t}();\n}\n\nvoid PanelController::editWithUpload(int index, int documentIndex) {\n\tExpects(_panel != nullptr);\n\tExpects(index >= 0 && index < _scopes.size());\n\tExpects(documentIndex >= 0\n\t\t&& documentIndex < _scopes[index].documents.size());\n\n\tconst auto document = _scopes[index].documents[documentIndex];\n\tconst auto type = document->requiresSpecialScan(FileType::FrontSide)\n\t\t? FileType::FrontSide\n\t\t: FileType::Scan;\n\tconst auto widget = _panel->widget();\n\tEditScans::ChooseScan(widget.get(), type, [=](QByteArray &&content) {\n\t\tif (_scopeDocumentTypeBox) {\n\t\t\t_scopeDocumentTypeBox = Ui::BoxPointer();\n\t\t}\n\t\tif (!_editScope || !_editDocument) {\n\t\t\tstartScopeEdit(index, documentIndex);\n\t\t}\n\t\tuploadScan(type, std::move(content));\n\t}, [=](ReadScanError error) {\n\t\treadScanError(error);\n\t});\n}\n\nvoid PanelController::readScanError(ReadScanError error) {\n\tshow(Ui::MakeInformBox([&]() -> rpl::producer<QString> {\n\t\tswitch (error) {\n\t\tcase ReadScanError::FileTooLarge:\n\t\t\treturn tr::lng_passport_error_too_large();\n\t\tcase ReadScanError::BadImageSize:\n\t\t\treturn tr::lng_passport_error_bad_size();\n\t\tcase ReadScanError::CantReadImage:\n\t\t\treturn tr::lng_passport_error_cant_read();\n\t\tcase ReadScanError::Unknown:\n\t\t\treturn rpl::single(Lang::Hard::UnknownSecureScanError());\n\t\t}\n\t\tUnexpected(\"Error type in PanelController::readScanError.\");\n\t}()));\n}\n\nbool PanelController::editRequiresScanUpload(\n\t\tint index,\n\t\tstd::optional<int> documentIndex) const {\n\tExpects(index >= 0 && index < _scopes.size());\n\tExpects(!documentIndex\n\t\t|| (*documentIndex >= 0\n\t\t\t&& *documentIndex < _scopes[index].documents.size()));\n\n\tif (!documentIndex) {\n\t\treturn false;\n\t}\n\tconst auto document = _scopes[index].documents[*documentIndex];\n\tif (document->requiresSpecialScan(FileType::FrontSide)) {\n\t\tconst auto &scans = document->specialScans;\n\t\treturn (scans.find(FileType::FrontSide) == end(scans));\n\t}\n\treturn document->files(FileType::Scan).empty();\n}\n\nvoid PanelController::editScope(\n\t\tint index,\n\t\tstd::optional<int> documentIndex) {\n\tif (editRequiresScanUpload(index, documentIndex)) {\n\t\teditWithUpload(index, *documentIndex);\n\t} else {\n\t\tstartScopeEdit(index, documentIndex);\n\t}\n}\n\nvoid PanelController::startScopeEdit(\n\t\tint index,\n\t\tstd::optional<int> documentIndex) {\n\tExpects(_panel != nullptr);\n\tExpects(index >= 0 && index < _scopes.size());\n\tExpects(_scopes[index].details != 0 || documentIndex.has_value());\n\tExpects(!documentIndex.has_value()\n\t\t|| (*documentIndex >= 0\n\t\t\t&& *documentIndex < _scopes[index].documents.size()));\n\n\t_editScope = &_scopes[index];\n\t_editValue = _editScope->details;\n\t_editDocument = documentIndex\n\t\t? _scopes[index].documents[*documentIndex].get()\n\t\t: nullptr;\n\n\tif (_editValue) {\n\t\t_form->startValueEdit(_editValue);\n\t}\n\tif (_editDocument) {\n\t\t_form->startValueEdit(_editDocument);\n\t}\n\n\tauto preferredLanguage = [=](const QString &countryCode) {\n\t\treturn _form->preferredLanguage(countryCode);\n\t};\n\n\tauto content = [&]() -> object_ptr<Ui::RpWidget> {\n\t\tswitch (_editScope->type) {\n\t\tcase Scope::Type::Identity:\n\t\tcase Scope::Type::Address: {\n\t\t\tAssert(_editDocument != nullptr);\n\t\t\tauto scans = PrepareScanListData(\n\t\t\t\t*_editDocument,\n\t\t\t\tFileType::Scan);\n\t\t\tauto translations = _editDocument->translationRequired\n\t\t\t\t? base::make_optional(PrepareScanListData(\n\t\t\t\t\t*_editDocument,\n\t\t\t\t\tFileType::Translation))\n\t\t\t\t: std::nullopt;\n\t\t\tauto result = _editValue\n\t\t\t\t? object_ptr<PanelEditDocument>(\n\t\t\t\t\t_panel->widget(),\n\t\t\t\t\tthis,\n\t\t\t\t\tGetDocumentScheme(\n\t\t\t\t\t\t_editScope->type,\n\t\t\t\t\t\t_editDocument->type,\n\t\t\t\t\t\t_editValue->nativeNames,\n\t\t\t\t\t\tstd::move(preferredLanguage)),\n\t\t\t\t\t_editValue->error,\n\t\t\t\t\t_editValue->data.parsedInEdit,\n\t\t\t\t\t_editDocument->error,\n\t\t\t\t\t_editDocument->data.parsedInEdit,\n\t\t\t\t\tstd::move(scans),\n\t\t\t\t\tstd::move(translations),\n\t\t\t\t\tPrepareSpecialFiles(*_editDocument))\n\t\t\t\t: object_ptr<PanelEditDocument>(\n\t\t\t\t\t_panel->widget(),\n\t\t\t\t\tthis,\n\t\t\t\t\tGetDocumentScheme(\n\t\t\t\t\t\t_editScope->type,\n\t\t\t\t\t\t_editDocument->type,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\tstd::move(preferredLanguage)),\n\t\t\t\t\t_editDocument->error,\n\t\t\t\t\t_editDocument->data.parsedInEdit,\n\t\t\t\t\tstd::move(scans),\n\t\t\t\t\tstd::move(translations),\n\t\t\t\t\tPrepareSpecialFiles(*_editDocument));\n\t\t\tconst auto weak = base::make_weak(result.data());\n\t\t\t_panelHasUnsavedChanges = [=] {\n\t\t\t\treturn weak ? weak->hasUnsavedChanges() : false;\n\t\t\t};\n\t\t\treturn result;\n\t\t} break;\n\t\tcase Scope::Type::PersonalDetails:\n\t\tcase Scope::Type::AddressDetails: {\n\t\t\tAssert(_editValue != nullptr);\n\t\t\tauto result = object_ptr<PanelEditDocument>(\n\t\t\t\t_panel->widget(),\n\t\t\t\tthis,\n\t\t\t\tGetDocumentScheme(\n\t\t\t\t\t_editScope->type,\n\t\t\t\t\tstd::nullopt,\n\t\t\t\t\t_editValue->nativeNames,\n\t\t\t\t\tstd::move(preferredLanguage)),\n\t\t\t\t_editValue->error,\n\t\t\t\t_editValue->data.parsedInEdit);\n\t\t\tconst auto weak = base::make_weak(result.data());\n\t\t\t_panelHasUnsavedChanges = [=] {\n\t\t\t\treturn weak ? weak->hasUnsavedChanges() : false;\n\t\t\t};\n\t\t\treturn result;\n\t\t} break;\n\t\tcase Scope::Type::Phone:\n\t\tcase Scope::Type::Email: {\n\t\t\tAssert(_editValue != nullptr);\n\t\t\tconst auto &parsed = _editValue->data.parsedInEdit;\n\t\t\tconst auto valueIt = parsed.fields.find(\"value\");\n\t\t\tconst auto value = (valueIt == end(parsed.fields)\n\t\t\t\t? QString()\n\t\t\t\t: valueIt->second.text);\n\t\t\tconst auto existing = getDefaultContactValue(_editScope->type);\n\t\t\t_panelHasUnsavedChanges = nullptr;\n\t\t\treturn object_ptr<PanelEditContact>(\n\t\t\t\t_panel->widget(),\n\t\t\t\tthis,\n\t\t\t\tGetContactScheme(_editScope->type),\n\t\t\t\tvalue,\n\t\t\t\t(existing.toLower().trimmed() != value.toLower().trimmed()\n\t\t\t\t\t? existing\n\t\t\t\t\t: QString()));\n\t\t} break;\n\t\t}\n\t\tUnexpected(\"Type in PanelController::editScope().\");\n\t}();\n\n\tcontent->lifetime().add([=] {\n\t\tcancelValueEdit();\n\t});\n\n\t_panel->setBackAllowed(true);\n\n\t_panel->backRequests(\n\t) | rpl::on_next([=] {\n\t\tcancelEditScope();\n\t}, content->lifetime());\n\n\t_form->valueSaveFinished(\n\t) | rpl::on_next([=](not_null<const Value*> value) {\n\t\tprocessValueSaveFinished(value);\n\t}, content->lifetime());\n\n\t_panel->showEditValue(std::move(content));\n}\n\nvoid PanelController::processValueSaveFinished(\n\t\tnot_null<const Value*> value) {\n\tExpects(_editScope != nullptr);\n\n\tconst auto boxIt = _verificationBoxes.find(value);\n\tif (boxIt != end(_verificationBoxes)) {\n\t\tconst auto saved = std::move(boxIt->second);\n\t\t_verificationBoxes.erase(boxIt);\n\t}\n\n\tif ((_editValue == value || _editDocument == value) && !savingScope()) {\n\t\tif (auto errors = collectSaveErrors(value); !errors.empty()) {\n\t\t\tfor (auto &&error : errors) {\n\t\t\t\t_saveErrors.fire(std::move(error));\n\t\t\t}\n\t\t} else {\n\t\t\t_panel->showForm();\n\t\t}\n\t}\n}\n\nbool PanelController::uploadingScopeScan() const {\n\treturn (_editValue && _editValue->uploadingScan())\n\t\t|| (_editDocument && _editDocument->uploadingScan());\n}\n\nbool PanelController::savingScope() const {\n\treturn (_editValue && _editValue->saving())\n\t\t|| (_editDocument && _editDocument->saving());\n}\n\nvoid PanelController::processVerificationNeeded(\n\t\tnot_null<const Value*> value) {\n\tconst auto i = _verificationBoxes.find(value);\n\tif (i != _verificationBoxes.end()) {\n\t\tLOG((\"API Error: Requesting for verification repeatedly.\"));\n\t\treturn;\n\t}\n\tconst auto textIt = value->data.parsedInEdit.fields.find(\"value\");\n\tAssert(textIt != end(value->data.parsedInEdit.fields));\n\tconst auto text = textIt->second.text;\n\tconst auto type = value->type;\n\tconst auto update = _form->verificationUpdate(\n\t) | rpl::filter([=](not_null<const Value*> field) {\n\t\treturn (field == value);\n\t});\n\tconst auto box = [&] {\n\t\tif (type == Value::Type::Phone) {\n\t\t\tconst auto submit = [=](const QString &code) {\n\t\t\t\t_form->verify(value, code);\n\t\t\t};\n\t\t\tconst auto account = &_form->window()->session().account();\n\t\t\taccount->setHandleLoginCode(submit);\n\t\t\tconst auto box = show(VerifyPhoneBox(\n\t\t\t\ttext,\n\t\t\t\tvalue->verification.codeLength,\n\t\t\t\tvalue->verification.fragmentUrl,\n\t\t\t\tsubmit,\n\t\t\t\tvalue->verification.call ? rpl::single(\n\t\t\t\t\tvalue->verification.call->getText()\n\t\t\t\t) | rpl::then(rpl::duplicate(\n\t\t\t\t\tupdate\n\t\t\t\t) | rpl::filter([=](not_null<const Value*> field) {\n\t\t\t\t\treturn field->verification.call != nullptr;\n\t\t\t\t}) | rpl::map([=](not_null<const Value*> field) {\n\t\t\t\t\treturn field->verification.call->getText();\n\t\t\t\t})) : (rpl::single(QString()) | rpl::type_erased),\n\n\t\t\t\trpl::duplicate(\n\t\t\t\t\tupdate\n\t\t\t\t) | rpl::map([=](not_null<const Value*> field) {\n\t\t\t\t\treturn field->verification.error;\n\t\t\t\t}) | rpl::distinct_until_changed()));\n\t\t\tbox->boxClosing(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\taccount->setHandleLoginCode(nullptr);\n\t\t\t}, box->lifetime());\n\t\t\treturn box;\n\t\t} else if (type == Value::Type::Email) {\n\t\t\treturn show(VerifyEmailBox(\n\t\t\t\ttext,\n\t\t\t\tvalue->verification.codeLength,\n\t\t\t\t[=](const QString &code) { _form->verify(value, code); },\n\t\t\t\tnullptr, // resend\n\n\t\t\t\trpl::duplicate(\n\t\t\t\t\tupdate\n\t\t\t\t) | rpl::map([=](not_null<const Value*> field) {\n\t\t\t\t\treturn field->verification.error;\n\t\t\t\t}) | rpl::distinct_until_changed(),\n\n\t\t\t\tnullptr));\n\t\t} else {\n\t\t\tUnexpected(\"Type in processVerificationNeeded.\");\n\t\t}\n\t}();\n\n\tbox->boxClosing(\n\t) | rpl::on_next([=] {\n\t\t_form->cancelValueVerification(value);\n\t}, lifetime());\n\n\t_verificationBoxes.emplace(value, box);\n}\n\nvoid PanelController::cancelValueEdit() {\n\tExpects(_editScope != nullptr);\n\n\t_editScopeBoxes.clear();\n\tif (const auto value = base::take(_editValue)) {\n\t\t_form->cancelValueEdit(value);\n\t}\n\tif (const auto document = base::take(_editDocument)) {\n\t\t_form->cancelValueEdit(document);\n\t}\n\t_editScope = nullptr;\n}\n\nvoid PanelController::saveScope(ValueMap &&data, ValueMap &&filesData) {\n\tExpects(_panel != nullptr);\n\n\tif (uploadingScopeScan()) {\n\t\tshowToast(tr::lng_passport_wait_upload(tr::now));\n\t\treturn;\n\t} else if (savingScope()) {\n\t\treturn;\n\t}\n\n\tif (_editValue) {\n\t\t_form->saveValueEdit(_editValue, std::move(data));\n\t} else {\n\t\tAssert(data.fields.empty());\n\t}\n\tif (_editDocument) {\n\t\t_form->saveValueEdit(_editDocument, std::move(filesData));\n\t} else {\n\t\tAssert(filesData.fields.empty());\n\t}\n}\n\nbool PanelController::editScopeChanged(\n\t\tconst ValueMap &data,\n\t\tconst ValueMap &filesData) const {\n\tif (_editValue && ValueChanged(_editValue, data)) {\n\t\treturn true;\n\t} else if (_editDocument && ValueChanged(_editDocument, filesData)) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid PanelController::cancelEditScope() {\n\tExpects(_editScope != nullptr);\n\n\tif (_panelHasUnsavedChanges && _panelHasUnsavedChanges()) {\n\t\tif (!_confirmForgetChangesBox) {\n\t\t\t_confirmForgetChangesBox = show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_passport_sure_cancel(),\n\t\t\t\t.confirmed = [=] { _panel->showForm(); },\n\t\t\t\t.confirmText = tr::lng_continue(),\n\t\t\t}));\n\t\t\t_editScopeBoxes.emplace_back(_confirmForgetChangesBox);\n\t\t}\n\t} else {\n\t\t_panel->showForm();\n\t}\n}\n\nint PanelController::closeGetDuration() {\n\tif (_panel) {\n\t\treturn _panel->hideAndDestroyGetDuration();\n\t}\n\treturn 0;\n}\n\nvoid PanelController::cancelAuth() {\n\t_form->cancel();\n}\n\nvoid PanelController::cancelAuthSure() {\n\t_form->cancelSure();\n}\n\nvoid PanelController::showBox(\n\t\tobject_ptr<Ui::BoxContent> box,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) {\n\t_panel->showBox(std::move(box), options, animated);\n}\n\nvoid PanelController::showToast(const QString &text) {\n\t_panel->showToast(text);\n}\n\nrpl::lifetime &PanelController::lifetime() {\n\treturn _lifetime;\n}\n\nPanelController::~PanelController() = default;\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_panel_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"passport/passport_form_view_controller.h\"\n#include \"passport/passport_form_controller.h\"\n#include \"ui/layers/layer_widget.h\"\n\nnamespace Ui {\nclass BoxContent;\n} // namespace Ui\n\nnamespace Passport {\n\nclass FormController;\nclass Panel;\n\nstruct EditDocumentCountry;\nstruct EditDocumentScheme;\nstruct EditContactScheme;\n\nenum class ReadScanError;\n\nusing preferredLangCallback\n\t= Fn<rpl::producer<EditDocumentCountry>(const QString &)>;\nEditDocumentScheme GetDocumentScheme(\n\tScope::Type type,\n\tstd::optional<Value::Type> scansType,\n\tbool nativeNames,\n\tpreferredLangCallback &&preferredLanguage);\nEditContactScheme GetContactScheme(Scope::Type type);\n\nconst std::map<QString, QString> &LatinToNativeMap();\nconst std::map<QString, QString> &NativeToLatinMap();\nQString AdjustKeyName(not_null<const Value*> value, const QString &key);\nbool SkipFieldCheck(not_null<const Value*> value, const QString &key);\n\nstruct ScanInfo {\n\texplicit ScanInfo(FileType type);\n\tScanInfo(\n\t\tFileType type,\n\t\tconst FileKey &key,\n\t\tconst QString &status,\n\t\tconst QImage &thumb,\n\t\tbool deleted,\n\t\tconst QString &error);\n\n\tFileType type;\n\tFileKey key;\n\tQString status;\n\tQImage thumb;\n\tbool deleted = false;\n\tQString error;\n};\n\nstruct ScopeError {\n\tenum class General {\n\t\tWholeValue,\n\t\tScanMissing,\n\t\tTranslationMissing,\n\t};\n\n\t// FileKey - file_hash error (bad scan / selfie / translation)\n\t// General - general value error (or scan / translation missing)\n\t// QString - data_hash with such key error (bad value)\n\tstd::variant<FileKey, General, QString> key;\n\tQString text;\n};\n\nclass PanelController : public ViewController {\npublic:\n\tPanelController(not_null<FormController*> form);\n\n\tnot_null<UserData*> bot() const;\n\tQString privacyPolicyUrl() const;\n\tvoid submitForm();\n\tvoid submitPassword(const QByteArray &password);\n\tvoid recoverPassword();\n\trpl::producer<QString> passwordError() const;\n\tQString passwordHint() const;\n\tQString unconfirmedEmailPattern() const;\n\n\tvoid setupPassword();\n\tvoid cancelPasswordSubmit();\n\tvoid validateRecoveryEmail();\n\n\tbool canAddScan(FileType type) const;\n\tvoid uploadScan(FileType type, QByteArray &&content);\n\tvoid deleteScan(FileType type, std::optional<int> fileIndex);\n\tvoid restoreScan(FileType type, std::optional<int> fileIndex);\n\trpl::producer<ScanInfo> scanUpdated() const;\n\trpl::producer<ScopeError> saveErrors() const;\n\tvoid readScanError(ReadScanError error);\n\n\tstd::optional<rpl::producer<QString>> deleteValueLabel() const;\n\tvoid deleteValue();\n\n\tQString defaultEmail() const;\n\tQString defaultPhoneNumber() const;\n\n\tvoid showAskPassword() override;\n\tvoid showNoPassword() override;\n\tvoid showCriticalError(const QString &error) override;\n\tvoid showUpdateAppBox() override;\n\n\tvoid fillRows(\n\t\tFn<void(\n\t\t\tQString title,\n\t\t\tQString description,\n\t\t\tbool ready,\n\t\t\tbool error)> callback);\n\trpl::producer<> refillRows() const;\n\n\tvoid editScope(int index) override;\n\tvoid saveScope(ValueMap &&data, ValueMap &&filesData);\n\tbool editScopeChanged(\n\t\tconst ValueMap &data,\n\t\tconst ValueMap &filesData) const;\n\tvoid cancelEditScope();\n\n\tvoid showBox(\n\t\tobject_ptr<Ui::BoxContent> box,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) override;\n\tvoid showToast(const QString &text) override;\n\tvoid suggestReset(Fn<void()> callback) override;\n\n\tint closeGetDuration() override;\n\n\tvoid cancelAuth();\n\tvoid cancelAuthSure();\n\n\trpl::lifetime &lifetime();\n\n\t~PanelController();\n\nprivate:\n\tvoid ensurePanelCreated();\n\n\tvoid editScope(int index, std::optional<int> documentIndex);\n\tvoid editWithUpload(int index, int documentIndex);\n\tbool editRequiresScanUpload(\n\t\tint index,\n\t\tstd::optional<int> documentIndex) const;\n\tvoid startScopeEdit(int index, std::optional<int> documentIndex);\n\tstd::optional<int> findBestDocumentIndex(const Scope &scope) const;\n\tvoid requestScopeFilesType(int index);\n\tvoid cancelValueEdit();\n\tvoid processValueSaveFinished(not_null<const Value*> value);\n\tvoid processVerificationNeeded(not_null<const Value*> value);\n\n\tbool savingScope() const;\n\tbool uploadingScopeScan() const;\n\tbool hasValueDocument() const;\n\tbool hasValueFields() const;\n\tstd::vector<ScopeError> collectSaveErrors(\n\t\tnot_null<const Value*> value) const;\n\tQString getDefaultContactValue(Scope::Type type) const;\n\tvoid deleteValueSure(bool withDetails);\n\n\tvoid resetPassport(Fn<void()> callback);\n\tvoid cancelReset();\n\n\tnot_null<FormController*> _form;\n\tstd::vector<Scope> _scopes;\n\trpl::event_stream<> _submitFailed;\n\tstd::vector<not_null<const Value*>> _submitErrors;\n\trpl::event_stream<ScopeError> _saveErrors;\n\n\tstd::unique_ptr<Panel> _panel;\n\tFn<bool()> _panelHasUnsavedChanges;\n\tbase::weak_qptr<Ui::BoxContent> _confirmForgetChangesBox;\n\tstd::vector<Ui::BoxPointer> _editScopeBoxes;\n\tScope *_editScope = nullptr;\n\tconst Value *_editValue = nullptr;\n\tconst Value *_editDocument = nullptr;\n\tUi::BoxPointer _scopeDocumentTypeBox;\n\tstd::map<not_null<const Value*>, Ui::BoxPointer> _verificationBoxes;\n\n\tUi::BoxPointer _resetBox;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"passport/passport_panel_edit_contact.h\"\n\n#include \"core/file_utilities.h\"\n#include \"passport/passport_panel_controller.h\"\n#include \"passport/ui/passport_details_row.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/box_content_divider.h\"\n#include \"ui/widgets/sent_code_field.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/text/format_values.h\" // Ui::FormatPhone\n#include \"ui/widgets/fields/special_fields.h\"\n#include \"boxes/abstract_box.h\"\n#include \"data/data_user.h\"\n#include \"countries/countries_instance.h\" // Countries::ExtractPhoneCode.\n#include \"main/main_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_passport.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Passport {\nnamespace {\n\nclass VerifyBox : public Ui::BoxContent {\npublic:\n\tVerifyBox(\n\t\tQWidget*,\n\t\trpl::producer<QString> title,\n\t\tconst QString &text,\n\t\tint codeLength,\n\t\tconst QString &openUrl,\n\t\tFn<void(QString code)> submit,\n\t\tFn<void()> resend,\n\t\trpl::producer<QString> call,\n\t\trpl::producer<QString> error,\n\t\trpl::producer<QString> resent);\n\n\tvoid setInnerFocus() override;\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\tvoid setupControls(\n\t\tconst QString &text,\n\t\tint codeLength,\n\t\tconst QString &openUrl,\n\t\tFn<void(QString code)> submit,\n\t\tFn<void()> resend,\n\t\trpl::producer<QString> call,\n\t\trpl::producer<QString> error,\n\t\trpl::producer<QString> resent);\n\n\trpl::producer<QString> _title;\n\tFn<void()> _submit;\n\tQPointer<Ui::SentCodeField> _code;\n\tQPointer<Ui::VerticalLayout> _content;\n\n};\n\nVerifyBox::VerifyBox(\n\tQWidget*,\n\trpl::producer<QString> title,\n\tconst QString &text,\n\tint codeLength,\n\tconst QString &openUrl,\n\tFn<void(QString code)> submit,\n\tFn<void()> resend,\n\trpl::producer<QString> call,\n\trpl::producer<QString> error,\n\trpl::producer<QString> resent)\n: _title(std::move(title)) {\n\tsetupControls(\n\t\ttext,\n\t\tcodeLength,\n\t\topenUrl,\n\t\tsubmit,\n\t\tresend,\n\t\tstd::move(call),\n\t\tstd::move(error),\n\t\tstd::move(resent));\n}\n\nvoid VerifyBox::setupControls(\n\t\tconst QString &text,\n\t\tint codeLength,\n\t\tconst QString &openUrl,\n\t\tFn<void(QString code)> submit,\n\t\tFn<void()> resend,\n\t\trpl::producer<QString> call,\n\t\trpl::producer<QString> error,\n\t\trpl::producer<QString> resent) {\n\t_content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tconst auto small = style::margins(\n\t\tst::boxPadding.left(),\n\t\t0,\n\t\tst::boxPadding.right(),\n\t\tst::boxPadding.bottom());\n\t_content->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_content,\n\t\t\ttext,\n\t\t\tst::boxLabel),\n\t\tsmall);\n\t_code = _content->add(\n\t\tobject_ptr<Ui::SentCodeField>(\n\t\t\t_content,\n\t\t\tst::defaultInputField,\n\t\t\ttr::lng_change_phone_code_title()),\n\t\tsmall);\n\n\tconst auto problem = _content->add(\n\t\tobject_ptr<Ui::FadeWrap<Ui::FlatLabel>>(\n\t\t\t_content,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t_content,\n\t\t\t\tQString(),\n\t\t\t\tst::passportVerifyErrorLabel)),\n\t\tsmall);\n\t_content->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_content,\n\t\t\tstd::move(call),\n\t\t\tst::boxDividerLabel),\n\t\tsmall);\n\tif (!openUrl.isEmpty()) {\n\t\tconst auto button = _content->add(\n\t\t\tobject_ptr<Ui::RoundButton>(\n\t\t\t\t_content,\n\t\t\t\ttr::lng_intro_fragment_button(),\n\t\t\t\tst::fragmentBoxButton),\n\t\t\tsmall);\n\t\t_content->widthValue(\n\t\t) | rpl::on_next([=](int w) {\n\t\t\tbutton->setFullWidth(w - small.left() - small.right());\n\t\t}, button->lifetime());\n\t\tbutton->setClickedCallback([=] { ::File::OpenUrl(openUrl); });\n\t}\n\tif (resend) {\n\t\tauto link = TextWithEntities{ tr::lng_cloud_password_resend(tr::now) };\n\t\tlink.entities.push_back({\n\t\t\tEntityType::CustomUrl,\n\t\t\t0,\n\t\t\tint(link.text.size()),\n\t\t\tQString(\"internal:resend\") });\n\t\tconst auto label = _content->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t_content,\n\t\t\t\trpl::single(\n\t\t\t\t\tlink\n\t\t\t\t) | rpl::then(rpl::duplicate(\n\t\t\t\t\tresent\n\t\t\t\t) | rpl::map(TextWithEntities::Simple)),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tsmall);\n\t\tstd::move(\n\t\t\tresent\n\t\t) | rpl::on_next([=] {\n\t\t\t_content->resizeToWidth(st::boxWidth);\n\t\t}, _content->lifetime());\n\t\tlabel->overrideLinkClickHandler(resend);\n\t}\n\tstd::move(\n\t\terror\n\t) | rpl::on_next([=](const QString &error) {\n\t\tif (error.isEmpty()) {\n\t\t\tproblem->hide(anim::type::normal);\n\t\t} else {\n\t\t\tproblem->entity()->setText(error);\n\t\t\t_content->resizeToWidth(st::boxWidth);\n\t\t\tproblem->show(anim::type::normal);\n\t\t\t_code->showError();\n\t\t}\n\t}, lifetime());\n\n\t_submit = [=] {\n\t\tsubmit(_code->getDigitsOnly());\n\t};\n\tif (codeLength > 0) {\n\t\t_code->setAutoSubmit(codeLength, _submit);\n\t} else {\n\t\t_code->submits() | rpl::on_next(_submit, _code->lifetime());\n\t}\n\t_code->changes(\n\t) | rpl::on_next([=] {\n\t\tproblem->hide(anim::type::normal);\n\t}, _code->lifetime());\n}\n\nvoid VerifyBox::setInnerFocus() {\n\t_code->setFocusFast();\n}\n\nvoid VerifyBox::prepare() {\n\tsetTitle(std::move(_title));\n\n\taddButton(tr::lng_change_phone_new_submit(), _submit);\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\n\t_content->resizeToWidth(st::boxWidth);\n\t_content->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tsetDimensions(st::boxWidth, height);\n\t}, _content->lifetime());\n}\n\n} // namespace\n\nEditContactScheme::EditContactScheme(ValueType type) : type(type) {\n}\n\nPanelEditContact::PanelEditContact(\n\tQWidget*,\n\tnot_null<PanelController*> controller,\n\tScheme scheme,\n\tconst QString &data,\n\tconst QString &existing)\n: _controller(controller)\n, _scheme(std::move(scheme))\n, _content(this)\n, _bottomShadow(this)\n, _done(\n\t\tthis,\n\t\ttr::lng_passport_save_value(),\n\t\tst::passportPanelSaveValue) {\n\t_done->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\tsetupControls(data, existing);\n}\n\nvoid PanelEditContact::setupControls(\n\t\tconst QString &data,\n\t\tconst QString &existing) {\n\twidthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_content->resizeToWidth(width);\n\t}, _content->lifetime());\n\n\t_content->add(object_ptr<Ui::BoxContentDivider>(\n\t\t_content,\n\t\tst::passportFormDividerHeight));\n\tif (!existing.isEmpty()) {\n\t\t_content->add(\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\t_content,\n\t\t\t\ttr::lng_passport_use_existing(\n\t\t\t\t\tlt_existing,\n\t\t\t\t\trpl::single(_scheme.format\n\t\t\t\t\t\t? _scheme.format(existing)\n\t\t\t\t\t\t: existing)),\n\t\t\t\tst::passportUploadButton),\n\t\t\tst::passportUploadButtonPadding\n\t\t)->addClickHandler([=] {\n\t\t\tsave(existing);\n\t\t});\n\t\t_content->add(\n\t\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\t\t_content,\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t_content,\n\t\t\t\t\t_scheme.aboutExisting,\n\t\t\t\t\tst::boxDividerLabel),\n\t\t\t\tst::passportFormLabelPadding));\n\t\t_content->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t_content,\n\t\t\t\t_scheme.newHeader,\n\t\t\t\tst::passportFormHeader),\n\t\t\tst::passportDetailsHeaderPadding);\n\t}\n\tconst auto &fieldStyle = existing.isEmpty()\n\t\t? st::passportContactField\n\t\t: st::passportDetailsField;\n\tconst auto fieldPadding = existing.isEmpty()\n\t\t? st::passportContactFieldPadding\n\t\t: st::passportContactNewFieldPadding;\n\tauto fieldPlaceholder = existing.isEmpty()\n\t\t? rpl::duplicate(_scheme.newPlaceholder)\n\t\t: nullptr;\n\tauto wrap = object_ptr<Ui::RpWidget>(_content);\n\tif (_scheme.type == Scheme::ValueType::Phone) {\n\t\t_field = Ui::CreateChild<Ui::PhoneInput>(\n\t\t\twrap.data(),\n\t\t\tfieldStyle,\n\t\t\tstd::move(fieldPlaceholder),\n\t\t\tCountries::ExtractPhoneCode(\n\t\t\t\t_controller->bot()->session().user()->phone()),\n\t\t\tdata,\n\t\t\t[](const QString &s) { return Countries::Groups(s); });\n\t} else {\n\t\t_field = Ui::CreateChild<Ui::MaskedInputField>(\n\t\t\twrap.data(),\n\t\t\tfieldStyle,\n\t\t\tstd::move(fieldPlaceholder),\n\t\t\tdata);\n\t}\n\n\t_field->move(0, 0);\n\t_field->heightValue(\n\t) | rpl::on_next([=, pointer = wrap.data()](int height) {\n\t\tpointer->resize(pointer->width(), height);\n\t}, _field->lifetime());\n\twrap->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_field->resize(width, _field->height());\n\t}, _field->lifetime());\n\n\t_content->add(std::move(wrap), fieldPadding);\n\tconst auto errorWrap = _content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::FlatLabel>>(\n\t\t\t_content,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t_content,\n\t\t\t\tQString(),\n\t\t\t\tst::passportVerifyErrorLabel),\n\t\t\tst::passportContactErrorPadding),\n\t\tst::passportContactErrorMargin);\n\terrorWrap->hide(anim::type::instant);\n\n\t_content->add(\n\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\t_content,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t_content,\n\t\t\t\t_scheme.aboutNew,\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::passportFormLabelPadding));\n\n\tif (auto text = _controller->deleteValueLabel()) {\n\t\t_content->add(\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\t_content,\n\t\t\t\tstd::move(*text) | rpl::map(tr::upper),\n\t\t\t\tst::passportDeleteButton),\n\t\t\tst::passportUploadButtonPadding\n\t\t)->addClickHandler([=] {\n\t\t\t_controller->deleteValue();\n\t\t});\n\t}\n\n\t_controller->saveErrors(\n\t) | rpl::on_next([=](const ScopeError &error) {\n\t\tif (error.key == QString(\"value\")) {\n\t\t\t_field->showError();\n\t\t\terrorWrap->entity()->setText(error.text);\n\t\t\t_content->resizeToWidth(width());\n\t\t\terrorWrap->show(anim::type::normal);\n\t\t}\n\t}, lifetime());\n\n\tconst auto submit = [=] {\n\t\tcrl::on_main(this, [=] {\n\t\t\tsave();\n\t\t});\n\t};\n\tconnect(_field, &Ui::MaskedInputField::submitted, submit);\n\tconnect(_field, &Ui::MaskedInputField::changed, [=] {\n\t\terrorWrap->hide(anim::type::normal);\n\t});\n\t_done->addClickHandler(submit);\n}\n\nvoid PanelEditContact::focusInEvent(QFocusEvent *e) {\n\tcrl::on_main(this, [=] {\n\t\t_field->setFocusFast();\n\t});\n}\n\nvoid PanelEditContact::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid PanelEditContact::updateControlsGeometry() {\n\tconst auto submitTop = height() - _done->height();\n\t_bottomShadow->resizeToWidth(width());\n\t_bottomShadow->moveToLeft(0, submitTop - st::lineWidth);\n\t_done->resizeToWidth(width());\n\t_done->moveToLeft(0, submitTop);\n}\n\nvoid PanelEditContact::save() {\n\tconst auto result = _field->getLastText();\n\tconst auto processed = _scheme.postprocess\n\t\t? _scheme.postprocess(result)\n\t\t: result;\n\tif (_scheme.validate && !_scheme.validate(processed)) {\n\t\t_field->showError();\n\t\treturn;\n\t}\n\tsave(processed);\n}\n\nvoid PanelEditContact::save(const QString &value) {\n\tauto data = ValueMap();\n\tdata.fields[\"value\"].text = value;\n\t_controller->saveScope(std::move(data), {});\n}\n\nobject_ptr<Ui::BoxContent> VerifyPhoneBox(\n\t\tconst QString &phone,\n\t\tint codeLength,\n\t\tconst QString &openUrl,\n\t\tFn<void(QString code)> submit,\n\t\trpl::producer<QString> call,\n\t\trpl::producer<QString> error) {\n\treturn Box<VerifyBox>(\n\t\ttr::lng_passport_phone_title(),\n\t\ttr::lng_passport_confirm_phone(\n\t\t\ttr::now,\n\t\t\tlt_phone,\n\t\t\tUi::FormatPhone(phone)),\n\t\tcodeLength,\n\t\topenUrl,\n\t\tsubmit,\n\t\tnullptr,\n\t\tstd::move(call),\n\t\tstd::move(error),\n\t\tnullptr);\n}\n\nobject_ptr<Ui::BoxContent> VerifyEmailBox(\n\t\tconst QString &email,\n\t\tint codeLength,\n\t\tFn<void(QString code)> submit,\n\t\tFn<void()> resend,\n\t\trpl::producer<QString> error,\n\t\trpl::producer<QString> resent) {\n\treturn Box<VerifyBox>(\n\t\ttr::lng_passport_email_title(),\n\t\ttr::lng_passport_confirm_email(tr::now, lt_email, email),\n\t\tcodeLength,\n\t\tQString(),\n\t\tsubmit,\n\t\tresend,\n\t\trpl::single(QString()),\n\t\tstd::move(error),\n\t\tstd::move(resent));\n}\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_panel_edit_contact.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\nclass MaskedInputField;\nclass PlainShadow;\nclass RoundButton;\nclass VerticalLayout;\nclass BoxContent;\n} // namespace Ui\n\nnamespace Passport {\n\nclass PanelController;\n\nstruct EditContactScheme {\n\tenum class ValueType {\n\t\tPhone,\n\t\tText,\n\t};\n\texplicit EditContactScheme(ValueType type);\n\n\tValueType type;\n\n\tQString aboutExisting;\n\tQString newHeader;\n\trpl::producer<QString> newPlaceholder;\n\tQString aboutNew;\n\tFn<bool(const QString &value)> validate;\n\tFn<QString(const QString &value)> format;\n\tFn<QString(const QString &value)> postprocess;\n\n};\n\nclass PanelEditContact : public Ui::RpWidget {\npublic:\n\tusing Scheme = EditContactScheme;\n\n\tPanelEditContact(\n\t\tQWidget *parent,\n\t\tnot_null<PanelController*> controller,\n\t\tScheme scheme,\n\t\tconst QString &data,\n\t\tconst QString &existing);\n\nprotected:\n\tvoid focusInEvent(QFocusEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid setupControls(\n\t\tconst QString &data,\n\t\tconst QString &existing);\n\tvoid updateControlsGeometry();\n\n\tvoid save();\n\tvoid save(const QString &value);\n\n\tnot_null<PanelController*> _controller;\n\tScheme _scheme;\n\n\tobject_ptr<Ui::VerticalLayout> _content;\n\tQPointer<Ui::MaskedInputField> _field;\n\tobject_ptr<Ui::PlainShadow> _bottomShadow;\n\tobject_ptr<Ui::RoundButton> _done;\n\n};\n\nobject_ptr<Ui::BoxContent> VerifyPhoneBox(\n\tconst QString &phone,\n\tint codeLength,\n\tconst QString &openUrl,\n\tFn<void(QString code)> submit,\n\trpl::producer<QString> call,\n\trpl::producer<QString> error);\nobject_ptr<Ui::BoxContent> VerifyEmailBox(\n\tconst QString &email,\n\tint codeLength,\n\tFn<void(QString code)> submit,\n\tFn<void()> resend,\n\trpl::producer<QString> error,\n\trpl::producer<QString> resent);\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_panel_edit_document.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"passport/passport_panel_edit_document.h\"\n\n#include \"passport/passport_panel_controller.h\"\n#include \"passport/passport_panel_edit_scans.h\"\n#include \"passport/ui/passport_details_row.h\"\n#include \"ui/effects/scroll_content_shadow.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"countries/countries_instance.h\"\n#include \"data/data_user.h\" // ->bot()->session()\n#include \"main/main_session.h\" // ->session().user()\n#include \"boxes/abstract_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_passport.h\"\n\nnamespace Passport {\nnamespace {\n\nclass RequestTypeBox : public Ui::BoxContent {\npublic:\n\tRequestTypeBox(\n\t\tQWidget*,\n\t\trpl::producer<QString> title,\n\t\tconst QString &about,\n\t\tstd::vector<QString> labels,\n\t\tFn<void(int index)> submit);\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\tvoid setupControls(\n\t\tconst QString &about,\n\t\tstd::vector<QString> labels,\n\t\tFn<void(int index)> submit);\n\n\trpl::producer<QString> _title;\n\tFn<void()> _submit;\n\tint _height = 0;\n\n};\n\nclass DeleteDocumentBox : public Ui::BoxContent {\npublic:\n\tDeleteDocumentBox(\n\t\tQWidget*,\n\t\tconst QString &text,\n\t\tconst QString &detailsCheckbox,\n\t\tFn<void(bool withDetails)> submit);\n\nprotected:\n\tvoid prepare() override;\n\nprivate:\n\tvoid setupControls(\n\t\tconst QString &text,\n\t\tconst QString &detailsCheckbox,\n\t\tFn<void(bool withDetails)> submit);\n\n\tFn<void()> _submit;\n\tint _height = 0;\n\n};\n\nRequestTypeBox::RequestTypeBox(\n\tQWidget*,\n\trpl::producer<QString> title,\n\tconst QString &about,\n\tstd::vector<QString> labels,\n\tFn<void(int index)> submit)\n: _title(std::move(title)) {\n\tsetupControls(about, std::move(labels), submit);\n}\n\nvoid RequestTypeBox::prepare() {\n\tsetTitle(std::move(_title));\n\taddButton(tr::lng_passport_upload_document(), _submit);\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\tsetDimensions(st::boxWidth, _height);\n}\n\nvoid RequestTypeBox::setupControls(\n\t\tconst QString &about,\n\t\tstd::vector<QString> labels,\n\t\tFn<void(int index)> submit) {\n\tconst auto header = Ui::CreateChild<Ui::FlatLabel>(\n\t\tthis,\n\t\ttr::lng_passport_document_type(tr::now),\n\t\tst::boxDividerLabel);\n\n\tconst auto group = std::make_shared<Ui::RadiobuttonGroup>(0);\n\tauto buttons = std::vector<QPointer<Ui::Radiobutton>>();\n\tauto index = 0;\n\tfor (const auto &label : labels) {\n\t\tbuttons.emplace_back(Ui::CreateChild<Ui::Radiobutton>(\n\t\t\tthis,\n\t\t\tgroup,\n\t\t\tindex++,\n\t\t\tlabel,\n\t\t\tst::defaultBoxCheckbox));\n\t}\n\n\tconst auto description = Ui::CreateChild<Ui::FlatLabel>(\n\t\tthis,\n\t\tabout,\n\t\tst::boxDividerLabel);\n\n\tauto y = 0;\n\tconst auto innerWidth = st::boxWidth\n\t\t- st::boxPadding.left()\n\t\t- st::boxPadding.right();\n\theader->resizeToWidth(innerWidth);\n\theader->moveToLeft(st::boxPadding.left(), y);\n\ty += header->height() + st::passportRequestTypeSkip;\n\tfor (const auto &button : buttons) {\n\t\tbutton->resizeToNaturalWidth(innerWidth);\n\t\tbutton->moveToLeft(st::boxPadding.left(), y);\n\t\ty += button->heightNoMargins() + st::passportRequestTypeSkip;\n\t}\n\tdescription->resizeToWidth(innerWidth);\n\tdescription->moveToLeft(st::boxPadding.left(), y);\n\ty += description->height() + st::passportRequestTypeSkip;\n\t_height = y;\n\n\t_submit = [=] {\n\t\tconst auto value = group->hasValue() ? group->current() : -1;\n\t\tif (value >= 0) {\n\t\t\tsubmit(value);\n\t\t}\n\t};\n}\n\nDeleteDocumentBox::DeleteDocumentBox(\n\t\tQWidget*,\n\t\tconst QString &text,\n\t\tconst QString &detailsCheckbox,\n\t\tFn<void(bool withDetails)> submit) {\n\tsetupControls(text, detailsCheckbox, submit);\n}\n\nvoid DeleteDocumentBox::prepare() {\n\taddButton(tr::lng_box_delete(), _submit);\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\n\tsetDimensions(st::boxWidth, _height);\n}\n\nvoid DeleteDocumentBox::setupControls(\n\t\tconst QString &text,\n\t\tconst QString &detailsCheckbox,\n\t\tFn<void(bool withDetails)> submit) {\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tthis,\n\t\ttext,\n\t\tst::boxLabel);\n\tconst auto details = !detailsCheckbox.isEmpty()\n\t\t? Ui::CreateChild<Ui::Checkbox>(\n\t\t\tthis,\n\t\t\tdetailsCheckbox,\n\t\t\tfalse,\n\t\t\tst::defaultBoxCheckbox)\n\t\t: nullptr;\n\n\t_height = st::boxPadding.top();\n\tconst auto availableWidth = st::boxWidth\n\t\t- st::boxPadding.left()\n\t\t- st::boxPadding.right();\n\tlabel->resizeToWidth(availableWidth);\n\tlabel->moveToLeft(st::boxPadding.left(), _height);\n\t_height += label->height();\n\n\tif (details) {\n\t\t_height += st::boxPadding.bottom();\n\t\tdetails->moveToLeft(st::boxPadding.left(), _height);\n\t\t_height += details->heightNoMargins();\n\t}\n\t_height += st::boxPadding.bottom();\n\n\t_submit = [=] {\n\t\tsubmit(details ? details->checked() : false);\n\t};\n}\n\n} // namespace\n\nstruct PanelEditDocument::Result {\n\tValueMap data;\n\tValueMap filesData;\n};\n\nPanelEditDocument::PanelEditDocument(\n\tQWidget*,\n\tnot_null<PanelController*> controller,\n\tScheme scheme,\n\tconst QString &error,\n\tconst ValueMap &data,\n\tconst QString &scansError,\n\tconst ValueMap &scansData,\n\tScanListData &&scans,\n\tstd::optional<ScanListData> &&translations,\n\tstd::map<FileType, ScanInfo> &&specialFiles)\n: _controller(controller)\n, _scheme(std::move(scheme))\n, _scroll(this, st::passportPanelScroll)\n, _done(\n\t\tthis,\n\t\ttr::lng_passport_save_value(),\n\t\tst::passportPanelSaveValue) {\n\t_done->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\tsetupControls(\n\t\t&error,\n\t\t&data,\n\t\t&scansError,\n\t\t&scansData,\n\t\tstd::move(scans),\n\t\tstd::move(translations),\n\t\tstd::move(specialFiles));\n}\n\nPanelEditDocument::PanelEditDocument(\n\tQWidget*,\n\tnot_null<PanelController*> controller,\n\tScheme scheme,\n\tconst QString &scansError,\n\tconst ValueMap &scansData,\n\tScanListData &&scans,\n\tstd::optional<ScanListData> &&translations,\n\tstd::map<FileType, ScanInfo> &&specialFiles)\n: _controller(controller)\n, _scheme(std::move(scheme))\n, _scroll(this, st::passportPanelScroll)\n, _done(\n\t\tthis,\n\t\ttr::lng_passport_save_value(),\n\t\tst::passportPanelSaveValue) {\n\t_done->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\tsetupControls(\n\t\tnullptr,\n\t\tnullptr,\n\t\t&scansError,\n\t\t&scansData,\n\t\tstd::move(scans),\n\t\tstd::move(translations),\n\t\tstd::move(specialFiles));\n}\n\nPanelEditDocument::PanelEditDocument(\n\tQWidget*,\n\tnot_null<PanelController*> controller,\n\tScheme scheme,\n\tconst QString &error,\n\tconst ValueMap &data)\n: _controller(controller)\n, _scheme(std::move(scheme))\n, _scroll(this, st::passportPanelScroll)\n, _done(\n\t\tthis,\n\t\ttr::lng_passport_save_value(),\n\t\tst::passportPanelSaveValue) {\n\t_done->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\tsetupControls(&error, &data, nullptr, nullptr, {}, {}, {});\n}\n\nvoid PanelEditDocument::setupControls(\n\t\tconst QString *error,\n\t\tconst ValueMap *data,\n\t\tconst QString *scansError,\n\t\tconst ValueMap *scansData,\n\t\tScanListData &&scans,\n\t\tstd::optional<ScanListData> &&translations,\n\t\tstd::map<FileType, ScanInfo> &&specialFiles) {\n\tconst auto inner = setupContent(\n\t\terror,\n\t\tdata,\n\t\tscansError,\n\t\tscansData,\n\t\tstd::move(scans),\n\t\tstd::move(translations),\n\t\tstd::move(specialFiles));\n\n\tUi::SetupShadowsToScrollContent(this, _scroll, inner->heightValue());\n\n\t_done->addClickHandler([=] {\n\t\tcrl::on_main(this, [=] {\n\t\t\tsave();\n\t\t});\n\t});\n}\n\nnot_null<Ui::RpWidget*> PanelEditDocument::setupContent(\n\t\tconst QString *error,\n\t\tconst ValueMap *data,\n\t\tconst QString *scansError,\n\t\tconst ValueMap *scansData,\n\t\tScanListData &&scans,\n\t\tstd::optional<ScanListData> &&translations,\n\t\tstd::map<FileType, ScanInfo> &&specialFiles) {\n\tconst auto inner = _scroll->setOwnedWidget(\n\t\tobject_ptr<Ui::VerticalLayout>(this));\n\t_scroll->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tinner->resizeToWidth(width);\n\t}, inner->lifetime());\n\n\tif (!specialFiles.empty()) {\n\t\t_editScans = inner->add(\n\t\t\tobject_ptr<EditScans>(\n\t\t\t\tinner,\n\t\t\t\t_controller,\n\t\t\t\t_scheme.scansHeader,\n\t\t\t\t*scansError,\n\t\t\t\tstd::move(specialFiles),\n\t\t\t\tstd::move(translations)));\n\t} else if (scansData) {\n\t\t_editScans = inner->add(\n\t\t\tobject_ptr<EditScans>(\n\t\t\t\tinner,\n\t\t\t\t_controller,\n\t\t\t\t_scheme.scansHeader,\n\t\t\t\t*scansError,\n\t\t\t\tstd::move(scans),\n\t\t\t\tstd::move(translations)));\n\t}\n\n\tconst auto enumerateRows = [&](auto &&callback) {\n\t\tfor (auto i = 0, count = int(_scheme.rows.size()); i != count; ++i) {\n\t\t\tconst auto &row = _scheme.rows[i];\n\n\t\t\tAssert(row.valueClass != Scheme::ValueClass::Additional\n\t\t\t\t|| !_scheme.additionalDependencyKey.isEmpty());\n\t\t\tauto fields = (row.valueClass == Scheme::ValueClass::Scans)\n\t\t\t\t? scansData\n\t\t\t\t: data;\n\t\t\tif (!fields) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tcallback(i, row, *fields);\n\t\t}\n\t};\n\tauto maxLabelWidth = 0;\n\tenumerateRows([&](\n\t\t\tint i,\n\t\t\tconst EditDocumentScheme::Row &row,\n\t\t\tconst ValueMap &fields) {\n\t\taccumulate_max(\n\t\t\tmaxLabelWidth,\n\t\t\tUi::PanelDetailsRow::LabelWidth(row.label));\n\t});\n\tif (maxLabelWidth > 0) {\n\t\tif (error && !error->isEmpty()) {\n\t\t\t_commonError = inner->add(\n\t\t\t\tobject_ptr<Ui::SlideWrap<Ui::FlatLabel>>(\n\t\t\t\t\tinner,\n\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\tinner,\n\t\t\t\t\t\t*error,\n\t\t\t\t\t\tst::passportVerifyErrorLabel),\n\t\t\t\t\tst::passportValueErrorPadding));\n\t\t\t_commonError->toggle(true, anim::type::instant);\n\t\t}\n\t\tinner->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tinner,\n\t\t\t\tdata ? _scheme.detailsHeader : _scheme.fieldsHeader,\n\t\t\t\tst::passportFormHeader),\n\t\t\tst::passportDetailsHeaderPadding);\n\t\tenumerateRows([&](\n\t\t\t\tint i,\n\t\t\t\tconst Scheme::Row &row,\n\t\t\t\tconst ValueMap &fields) {\n\t\t\tif (row.valueClass != Scheme::ValueClass::Additional) {\n\t\t\t\tcreateDetailsRow(inner, i, row, fields, maxLabelWidth);\n\t\t\t}\n\t\t});\n\t\tif (data && !_scheme.additionalDependencyKey.isEmpty()) {\n\t\t\tconst auto row = findRow(_scheme.additionalDependencyKey);\n\t\t\tconst auto wrap = inner->add(\n\t\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\t\tinner,\n\t\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\t\t\tconst auto added = wrap->entity();\n\n\t\t\tauto showIfError = false;\n\t\t\tenumerateRows([&](\n\t\t\t\t\tint i,\n\t\t\t\t\tconst Scheme::Row &row,\n\t\t\t\t\tconst ValueMap &fields) {\n\t\t\t\tif (row.valueClass != Scheme::ValueClass::Additional) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto it = fields.fields.find(row.key);\n\t\t\t\tif (it == end(fields.fields)) {\n\t\t\t\t\treturn;\n\t\t\t\t} else if (!it->second.error.isEmpty()) {\n\t\t\t\t\tshowIfError = true;\n\t\t\t\t} else if (it->second.text.isEmpty()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto fallbackIt = fields.fields.find(\n\t\t\t\t\trow.additionalFallbackKey);\n\t\t\t\tif (fallbackIt != end(fields.fields)\n\t\t\t\t\t&& fallbackIt->second.text != it->second.text) {\n\t\t\t\t\tshowIfError = true;\n\t\t\t\t}\n\t\t\t});\n\t\t\tconst auto shown = [=](const Scheme::CountryInfo &info) {\n\t\t\t\tusing Result = Scheme::AdditionalVisibility;\n\t\t\t\tconst auto value = _scheme.additionalShown(info);\n\t\t\t\treturn (value == Result::Shown)\n\t\t\t\t\t|| (value == Result::OnlyIfError && showIfError);\n\t\t\t};\n\n\t\t\tauto langValue = row->value(\n\t\t\t) | rpl::map(\n\t\t\t\t_scheme.preferredLanguage\n\t\t\t) | rpl::flatten_latest();\n\n\t\t\tauto title = rpl::duplicate(langValue) | rpl::filter(\n\t\t\t\tshown\n\t\t\t) | rpl::map([=](const Scheme::CountryInfo &info) {\n\t\t\t\treturn _scheme.additionalHeader(info);\n\t\t\t});\n\t\t\tconst auto headerLabel = added->add(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tadded,\n\t\t\t\t\trpl::duplicate(title),\n\t\t\t\t\tst::passportFormHeader),\n\t\t\t\tst::passportNativeNameHeaderPadding);\n\t\t\tstd::move(\n\t\t\t\ttitle\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tconst auto &padding = st::passportNativeNameHeaderPadding;\n\t\t\t\tconst auto available = added->width()\n\t\t\t\t\t- padding.left()\n\t\t\t\t\t- padding.right();\n\t\t\t\theaderLabel->resizeToNaturalWidth(available);\n\t\t\t\theaderLabel->moveToLeft(\n\t\t\t\t\tpadding.left(),\n\t\t\t\t\tpadding.top(),\n\t\t\t\t\tavailable);\n\t\t\t}, headerLabel->lifetime());\n\n\t\t\tenumerateRows([&](\n\t\t\t\t\tint i,\n\t\t\t\t\tconst Scheme::Row &row,\n\t\t\t\t\tconst ValueMap &fields) {\n\t\t\t\tif (row.valueClass == Scheme::ValueClass::Additional) {\n\t\t\t\t\tcreateDetailsRow(added, i, row, fields, maxLabelWidth);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tauto description = rpl::duplicate(langValue) | rpl::filter(\n\t\t\t\tshown\n\t\t\t) | rpl::map([=](const Scheme::CountryInfo &info) {\n\t\t\t\treturn _scheme.additionalDescription(info);\n\t\t\t});\n\t\t\tadded->add(\n\t\t\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\t\t\tadded,\n\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\tadded,\n\t\t\t\t\t\tstd::move(description),\n\t\t\t\t\t\tst::boxDividerLabel),\n\t\t\t\t\tst::passportFormLabelPadding),\n\t\t\t\tst::passportNativeNameAboutMargin);\n\n\t\t\twrap->toggleOn(rpl::duplicate(langValue) | rpl::map(shown));\n\t\t\twrap->finishAnimating();\n\n\t\t\tstd::move(langValue) | rpl::map(\n\t\t\t\tshown\n\t\t\t) | rpl::on_next([=](bool visible) {\n\t\t\t\t_additionalShown = visible;\n\t\t\t}, lifetime());\n\t\t}\n\n\t\tinner->add(object_ptr<Ui::FixedHeightWidget>(\n\t\t\tinner,\n\t\t\tst::passportDetailsSkip));\n\t}\n\tif (auto text = _controller->deleteValueLabel()) {\n\t\tinner->add(\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tinner,\n\t\t\t\tstd::move(*text) | rpl::map(tr::upper),\n\t\t\t\tst::passportDeleteButton),\n\t\t\tst::passportUploadButtonPadding\n\t\t)->addClickHandler([=] {\n\t\t\t_controller->deleteValue();\n\t\t});\n\t}\n\n\treturn inner;\n}\n\nvoid PanelEditDocument::createDetailsRow(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tint i,\n\t\tconst Scheme::Row &row,\n\t\tconst ValueMap &fields,\n\t\tint maxLabelWidth) {\n\tconst auto valueOrEmpty = [&](\n\t\t\tconst ValueMap &values,\n\t\t\tconst QString &key) {\n\t\tconst auto &fields = values.fields;\n\t\tif (const auto i = fields.find(key); i != fields.end()) {\n\t\t\treturn i->second;\n\t\t}\n\t\treturn ValueField();\n\t};\n\n\tconst auto current = valueOrEmpty(fields, row.key);\n\tconst auto showBox = [controller = _controller](\n\t\t\tobject_ptr<Ui::BoxContent> box) {\n\t\tcontroller->show(std::move(box));\n\t};\n\tconst auto isoByPhone = Countries::Instance().countryISO2ByPhone(\n\t\t_controller->bot()->session().user()->phone());\n\n\tconst auto &[it, ok] = _details.emplace(\n\t\ti,\n\t\tcontainer->add(Ui::PanelDetailsRow::Create(\n\t\t\tcontainer,\n\t\t\tshowBox,\n\t\t\tisoByPhone,\n\t\t\trow.inputType,\n\t\t\trow.label,\n\t\t\tmaxLabelWidth,\n\t\t\tcurrent.text,\n\t\t\tcurrent.error,\n\t\t\trow.lengthLimit)));\n\tconst bool details = (row.valueClass != Scheme::ValueClass::Scans);\n\tit->second->value(\n\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\tif (details) {\n\t\t\t_fieldsChanged = true;\n\t\t\tupdateCommonError();\n\t\t} else {\n\t\t\tAssert(_editScans != nullptr);\n\t\t\t_editScans->scanFieldsChanged(true);\n\t\t}\n\t}, it->second->lifetime());\n}\n\nnot_null<Ui::PanelDetailsRow*> PanelEditDocument::findRow(\n\t\tconst QString &key) const {\n\tfor (auto i = 0, count = int(_scheme.rows.size()); i != count; ++i) {\n\t\tconst auto &row = _scheme.rows[i];\n\t\tif (row.key == key) {\n\t\t\tconst auto it = _details.find(i);\n\t\t\tAssert(it != end(_details));\n\t\t\treturn it->second.data();\n\t\t}\n\t}\n\tUnexpected(\"Row not found in PanelEditDocument::findRow.\");\n}\n\nvoid PanelEditDocument::updateCommonError() {\n\tif (_commonError) {\n\t\t_commonError->toggle(!_fieldsChanged, anim::type::normal);\n\t}\n}\n\nvoid PanelEditDocument::focusInEvent(QFocusEvent *e) {\n\tcrl::on_main(this, [=] {\n\t\tfor (const auto &[index, row] : _details) {\n\t\t\tif (row->setFocusFast()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t});\n}\n\nvoid PanelEditDocument::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nbool PanelEditDocument::hasUnsavedChanges() const {\n\tconst auto result = collect();\n\treturn _controller->editScopeChanged(result.data, result.filesData);\n}\n\nvoid PanelEditDocument::updateControlsGeometry() {\n\tconst auto submitTop = height() - _done->height();\n\t_scroll->setGeometry(0, 0, width(), submitTop);\n\t_done->resizeToWidth(width());\n\t_done->moveToLeft(0, submitTop);\n\n\t_scroll->updateBars();\n}\n\nPanelEditDocument::Result PanelEditDocument::collect() const {\n\tauto result = Result();\n\tfor (const auto &[i, field] : _details) {\n\t\tconst auto &row = _scheme.rows[i];\n\t\tauto &fields = (row.valueClass == Scheme::ValueClass::Scans)\n\t\t\t? result.filesData\n\t\t\t: result.data;\n\t\tif (row.valueClass == Scheme::ValueClass::Additional\n\t\t\t&& !_additionalShown) {\n\t\t\tcontinue;\n\t\t}\n\t\tfields.fields[row.key].text = field->valueCurrent();\n\t}\n\tif (!_additionalShown) {\n\t\tfillAdditionalFromFallbacks(result);\n\t}\n\treturn result;\n}\n\nvoid PanelEditDocument::fillAdditionalFromFallbacks(Result &result) const {\n\tfor (const auto &row : _scheme.rows) {\n\t\tif (row.valueClass != Scheme::ValueClass::Additional) {\n\t\t\tcontinue;\n\t\t}\n\t\tAssert(!row.additionalFallbackKey.isEmpty());\n\t\tauto &fields = result.data;\n\t\tconst auto j = fields.fields.find(row.additionalFallbackKey);\n\t\tAssert(j != end(fields.fields));\n\t\tfields.fields[row.key] = j->second;\n\t}\n}\n\nbool PanelEditDocument::validate() {\n\tauto error = _editScans\n\t\t? _editScans->validateGetErrorTop()\n\t\t: std::nullopt;\n\tif (error) {\n\t\tconst auto errortop = _editScans->mapToGlobal(QPoint(0, *error));\n\t\tconst auto scrolltop = _scroll->mapToGlobal(QPoint(0, 0));\n\t\tconst auto scrolldelta = errortop.y() - scrolltop.y();\n\t\t_scroll->scrollToY(_scroll->scrollTop() + scrolldelta);\n\t} else if (_commonError && !_fieldsChanged) {\n\t\tconst auto firsttop = _commonError->mapToGlobal(QPoint(0, 0));\n\t\tconst auto scrolltop = _scroll->mapToGlobal(QPoint(0, 0));\n\t\tconst auto scrolldelta = firsttop.y() - scrolltop.y();\n\t\t_scroll->scrollToY(_scroll->scrollTop() + scrolldelta);\n\t\terror = firsttop.y();\n\t}\n\tauto first = QPointer<Ui::PanelDetailsRow>();\n\tfor (const auto &[i, field] : ranges::views::reverse(_details)) {\n\t\tconst auto &row = _scheme.rows[i];\n\t\tif (row.valueClass == Scheme::ValueClass::Additional\n\t\t\t&& !_additionalShown) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (field->errorShown()) {\n\t\t\tfield->showError();\n\t\t\tfirst = field;\n\t\t} else if (row.error) {\n\t\t\tif (const auto error = row.error(field->valueCurrent())) {\n\t\t\t\tfield->showError(error);\n\t\t\t\tfirst = field;\n\t\t\t}\n\t\t}\n\t}\n\tif (error) {\n\t\treturn false;\n\t} else if (!first) {\n\t\treturn true;\n\t}\n\tconst auto firsttop = first->mapToGlobal(QPoint(0, 0));\n\tconst auto scrolltop = _scroll->mapToGlobal(QPoint(0, 0));\n\tconst auto scrolldelta = firsttop.y() - scrolltop.y();\n\t_scroll->scrollToY(_scroll->scrollTop() + scrolldelta);\n\treturn false;\n}\n\nvoid PanelEditDocument::save() {\n\tif (!validate()) {\n\t\treturn;\n\t}\n\tauto result = collect();\n\t_controller->saveScope(\n\t\tstd::move(result.data),\n\t\tstd::move(result.filesData));\n}\n\nobject_ptr<Ui::BoxContent> RequestIdentityType(\n\t\tFn<void(int index)> submit,\n\t\tstd::vector<QString> labels) {\n\treturn Box<RequestTypeBox>(\n\t\ttr::lng_passport_identity_title(),\n\t\ttr::lng_passport_identity_about(tr::now),\n\t\tstd::move(labels),\n\t\tsubmit);\n}\n\nobject_ptr<Ui::BoxContent> RequestAddressType(\n\t\tFn<void(int index)> submit,\n\t\tstd::vector<QString> labels) {\n\treturn Box<RequestTypeBox>(\n\t\ttr::lng_passport_address_title(),\n\t\ttr::lng_passport_address_about(tr::now),\n\t\tstd::move(labels),\n\t\tsubmit);\n}\n\nobject_ptr<Ui::BoxContent> ConfirmDeleteDocument(\n\t\tFn<void(bool withDetails)> submit,\n\t\tconst QString &text,\n\t\tconst QString &detailsCheckbox) {\n\treturn Box<DeleteDocumentBox>(text, detailsCheckbox, submit);\n}\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_panel_edit_document.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\nclass InputField;\nclass ScrollArea;\nclass FlatLabel;\nclass RoundButton;\nclass VerticalLayout;\nclass SettingsButton;\nclass BoxContent;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Passport::Ui {\nusing namespace ::Ui;\nenum class PanelDetailsType;\nclass PanelDetailsRow;\n} // namespace Passport::Ui\n\nnamespace Passport {\n\nclass PanelController;\nstruct ValueMap;\nstruct ScanInfo;\nclass EditScans;\nenum class FileType;\nstruct ScanListData;\n\nstruct EditDocumentCountry {\n\tQString countryCode;\n\tQString languageCode;\n};\n\nstruct EditDocumentScheme {\n\tenum class ValueClass {\n\t\tFields,\n\t\tAdditional,\n\t\tScans,\n\t};\n\tenum class AdditionalVisibility {\n\t\tHidden,\n\t\tOnlyIfError,\n\t\tShown,\n\t};\n\tusing CountryInfo = EditDocumentCountry;\n\tstruct Row {\n\t\tusing Validator = Fn<std::optional<QString>(const QString &value)>;\n\t\tusing Formatter = Fn<QString(const QString &value)>;\n\t\tValueClass valueClass = ValueClass::Fields;\n\t\tUi::PanelDetailsType inputType = Ui::PanelDetailsType();\n\t\tQString key;\n\t\tQString label;\n\t\tValidator error;\n\t\tFormatter format;\n\t\tint lengthLimit = 0;\n\t\tQString keyForAttachmentTo; // Attach [last|middle]_name to first_*.\n\t\tQString additionalFallbackKey; // *_name_native from *_name.\n\t};\n\tstd::vector<Row> rows;\n\tQString fieldsHeader;\n\tQString detailsHeader;\n\tQString scansHeader;\n\n\tQString additionalDependencyKey;\n\tFn<AdditionalVisibility(const CountryInfo &dependency)> additionalShown;\n\tFn<QString(const CountryInfo &dependency)> additionalHeader;\n\tFn<QString(const CountryInfo &dependency)> additionalDescription;\n\tFn<rpl::producer<CountryInfo>(const QString &)> preferredLanguage;\n\n};\n\nclass PanelEditDocument : public Ui::RpWidget {\npublic:\n\tusing Scheme = EditDocumentScheme;\n\n\tPanelEditDocument(\n\t\tQWidget *parent,\n\t\tnot_null<PanelController*> controller,\n\t\tScheme scheme,\n\t\tconst QString &error,\n\t\tconst ValueMap &data,\n\t\tconst QString &scansError,\n\t\tconst ValueMap &scansData,\n\t\tScanListData &&scans,\n\t\tstd::optional<ScanListData> &&translations,\n\t\tstd::map<FileType, ScanInfo> &&specialFiles);\n\tPanelEditDocument(\n\t\tQWidget *parent,\n\t\tnot_null<PanelController*> controller,\n\t\tScheme scheme,\n\t\tconst QString &scansError,\n\t\tconst ValueMap &scansData,\n\t\tScanListData &&scans,\n\t\tstd::optional<ScanListData> &&translations,\n\t\tstd::map<FileType, ScanInfo> &&specialFiles);\n\tPanelEditDocument(\n\t\tQWidget *parent,\n\t\tnot_null<PanelController*> controller,\n\t\tScheme scheme,\n\t\tconst QString &error,\n\t\tconst ValueMap &data);\n\n\tbool hasUnsavedChanges() const;\n\nprotected:\n\tvoid focusInEvent(QFocusEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tstruct Result;\n\tvoid setupControls(\n\t\tconst QString *error,\n\t\tconst ValueMap *data,\n\t\tconst QString *scansError,\n\t\tconst ValueMap *scansData,\n\t\tScanListData &&scans,\n\t\tstd::optional<ScanListData> &&translations,\n\t\tstd::map<FileType, ScanInfo> &&specialFiles);\n\tnot_null<Ui::RpWidget*> setupContent(\n\t\tconst QString *error,\n\t\tconst ValueMap *data,\n\t\tconst QString *scansError,\n\t\tconst ValueMap *scansData,\n\t\tScanListData &&scans,\n\t\tstd::optional<ScanListData> &&translations,\n\t\tstd::map<FileType, ScanInfo> &&specialFiles);\n\tvoid updateControlsGeometry();\n\tvoid updateCommonError();\n\n\tResult collect() const;\n\tvoid fillAdditionalFromFallbacks(Result &result) const;\n\tbool validate();\n\tvoid save();\n\n\tvoid createDetailsRow(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tint i,\n\t\tconst Scheme::Row &row,\n\t\tconst ValueMap &fields,\n\t\tint maxLabelWidth);\n\tnot_null<Ui::PanelDetailsRow*> findRow(const QString &key) const;\n\n\tnot_null<PanelController*> _controller;\n\tScheme _scheme;\n\n\tobject_ptr<Ui::ScrollArea> _scroll;\n\n\tQPointer<EditScans> _editScans;\n\tQPointer<Ui::SlideWrap<Ui::FlatLabel>> _commonError;\n\tstd::map<int, QPointer<Ui::PanelDetailsRow>> _details;\n\tbool _fieldsChanged = false;\n\tbool _additionalShown = false;\n\n\tQPointer<Ui::SettingsButton> _delete;\n\n\tobject_ptr<Ui::RoundButton> _done;\n\n};\n\nobject_ptr<Ui::BoxContent> RequestIdentityType(\n\tFn<void(int index)> submit,\n\tstd::vector<QString> labels);\nobject_ptr<Ui::BoxContent> RequestAddressType(\n\tFn<void(int index)> submit,\n\tstd::vector<QString> labels);\n\nobject_ptr<Ui::BoxContent> ConfirmDeleteDocument(\n\tFn<void(bool withDetails)> submit,\n\tconst QString &text,\n\tconst QString &detailsCheckbox = QString());\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"passport/passport_panel_edit_scans.h\"\n\n#include \"passport/passport_panel_controller.h\"\n#include \"passport/ui/passport_details_row.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/box_content_divider.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"core/file_utilities.h\"\n#include \"lang/lang_keys.h\"\n#include \"boxes/abstract_box.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"storage/file_upload.h\" // For Storage::kUseBigFilesFrom.\n#include \"styles/style_layers.h\"\n#include \"styles/style_passport.h\"\n\n#include <QtCore/QBuffer>\n\nnamespace Passport {\nnamespace {\n\nconstexpr auto kMaxDimensions = 2048;\nconstexpr auto kMaxSize = 10 * 1024 * 1024;\nconstexpr auto kJpegQuality = 89;\n\nstatic_assert(kMaxSize <= Storage::kUseBigFilesFrom);\n\nstd::variant<ReadScanError, QByteArray> ProcessImage(QByteArray &&bytes) {\n\tauto read = Images::Read({\n\t\t.content = base::take(bytes),\n\t\t.forceOpaque = true,\n\t});\n\n\tauto &image = read.image;\n\tif (image.isNull()) {\n\t\treturn ReadScanError::CantReadImage;\n\t} else if (!Ui::ValidateThumbDimensions(image.width(), image.height())) {\n\t\treturn ReadScanError::BadImageSize;\n\t}\n\tif (std::max(image.width(), image.height()) > kMaxDimensions) {\n\t\timage = std::move(image).scaled(\n\t\t\tkMaxDimensions,\n\t\t\tkMaxDimensions,\n\t\t\tQt::KeepAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t}\n\tauto result = QByteArray();\n\t{\n\t\tQBuffer buffer(&result);\n\t\tif (!image.save(&buffer, \"JPG\", kJpegQuality)) {\n\t\t\treturn ReadScanError::Unknown;\n\t\t}\n\t\tbase::take(image);\n\t}\n\tif (result.isEmpty()) {\n\t\treturn ReadScanError::Unknown;\n\t} else if (result.size() > kMaxSize) {\n\t\treturn ReadScanError::FileTooLarge;\n\t}\n\treturn result;\n}\n\n} // namespace\n\nclass ScanButton : public Ui::AbstractButton {\npublic:\n\tScanButton(\n\t\tQWidget *parent,\n\t\tconst style::PassportScanRow &st,\n\t\tconst QString &name,\n\t\tconst QString &status,\n\t\tbool deleted,\n\t\tbool error);\n\n\tvoid setImage(const QImage &image);\n\tvoid setStatus(const QString &status);\n\tvoid setDeleted(bool deleted);\n\tvoid setError(bool error);\n\n\trpl::producer<> deleteClicks() const {\n\t\treturn _delete->entity()->clicks() | rpl::to_empty;\n\t}\n\trpl::producer<> restoreClicks() const {\n\t\treturn _restore->entity()->clicks() | rpl::to_empty;\n\t}\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tint countAvailableWidth() const;\n\n\tconst style::PassportScanRow &_st;\n\tUi::Text::String _name;\n\tUi::Text::String _status;\n\tint _nameHeight = 0;\n\tint _statusHeight = 0;\n\tbool _error = false;\n\tQImage _image;\n\tobject_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _delete;\n\tobject_ptr<Ui::FadeWrapScaled<Ui::RoundButton>> _restore;\n\n};\n\nstruct EditScans::SpecialScan {\n\tSpecialScan(ScanInfo &&file);\n\n\tScanInfo file;\n\tQPointer<Ui::SlideWrap<Ui::FlatLabel>> header;\n\tQPointer<Ui::VerticalLayout> wrap;\n\tbase::unique_qptr<Ui::SlideWrap<ScanButton>> row;\n\tQPointer<Ui::SettingsButton> upload;\n\tbool errorShown = false;\n\tUi::Animations::Simple errorAnimation;\n\trpl::variable<bool> rowCreated;\n};\n\nvoid UpdateFileRow(\n\t\tnot_null<ScanButton*> button,\n\t\tconst ScanInfo &info) {\n\tbutton->setStatus(info.status);\n\tbutton->setImage(info.thumb);\n\tbutton->setDeleted(info.deleted);\n\tbutton->setError(!info.error.isEmpty());\n}\n\nbase::unique_qptr<Ui::SlideWrap<ScanButton>> CreateScan(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tconst ScanInfo &info,\n\t\tconst QString &name) {\n\tauto result = base::unique_qptr<Ui::SlideWrap<ScanButton>>(\n\t\tparent->add(object_ptr<Ui::SlideWrap<ScanButton>>(\n\t\t\tparent,\n\t\t\tobject_ptr<ScanButton>(\n\t\t\t\tparent,\n\t\t\t\tst::passportScanRow,\n\t\t\t\tname,\n\t\t\t\tinfo.status,\n\t\t\t\tinfo.deleted,\n\t\t\t\t!info.error.isEmpty()))));\n\tresult->entity()->setImage(info.thumb);\n\treturn result;\n}\n\nEditScans::List::List(\n\tnot_null<PanelController*> controller,\n\tScanListData &&data)\n: controller(controller)\n, files(std::move(data.files))\n, initialCount(int(files.size()))\n, errorMissing(data.errorMissing) {\n}\n\nEditScans::List::List(\n\tnot_null<PanelController*> controller)\n: List(controller, std::nullopt)\n{\n}\n\nEditScans::List::List(\n\tnot_null<PanelController*> controller,\n\tstd::optional<ScanListData> &&data)\n: controller(controller)\n, files(data ? std::move(data->files) : std::vector<ScanInfo>())\n, initialCount(data ? base::make_optional(int(files.size())) : std::nullopt)\n, errorMissing(data ? std::move(data->errorMissing) : QString()) {\n}\n\nbool EditScans::List::uploadedSomeMore() const {\n\tif (!initialCount) {\n\t\treturn false;\n\t}\n\tconst auto from = begin(files) + *initialCount;\n\tconst auto till = end(files);\n\treturn std::find_if(from, till, [](const ScanInfo &file) {\n\t\treturn !file.deleted;\n\t}) != till;\n}\n\nbool EditScans::List::uploadMoreRequired() const {\n\tif (!upload) {\n\t\treturn false;\n\t}\n\tconst auto exists = ranges::any_of(\n\t\tfiles,\n\t\t[](const ScanInfo &file) { return !file.deleted; });\n\tif (!exists) {\n\t\treturn true;\n\t}\n\tconst auto errorExists = ranges::any_of(\n\t\tfiles,\n\t\t[](const ScanInfo &file) { return !file.error.isEmpty(); });\n\treturn (errorExists || uploadMoreError) && !uploadedSomeMore();\n}\n\nUi::SlideWrap<ScanButton> *EditScans::List::nonDeletedErrorRow() const {\n\tconst auto nonDeletedErrorIt = ranges::find_if(\n\t\tfiles,\n\t\t[](const ScanInfo &file) {\n\t\t\treturn !file.error.isEmpty() && !file.deleted;\n\t\t});\n\tif (nonDeletedErrorIt == end(files)) {\n\t\treturn nullptr;\n\t}\n\tconst auto index = (nonDeletedErrorIt - begin(files));\n\treturn rows[index].get();\n}\n\nrpl::producer<QString> EditScans::List::uploadButtonText() const {\n\treturn (files.empty()\n\t\t? tr::lng_passport_upload_scans\n\t\t: tr::lng_passport_upload_more)(tr::upper);\n}\n\nvoid EditScans::List::hideError() {\n\ttoggleError(false);\n}\n\nvoid EditScans::List::toggleError(bool shown) {\n\tif (errorShown != shown) {\n\t\terrorShown = shown;\n\t\terrorAnimation.start(\n\t\t\t[=] { errorAnimationCallback(); },\n\t\t\terrorShown ? 0. : 1.,\n\t\t\terrorShown ? 1. : 0.,\n\t\t\tst::passportDetailsField.duration);\n\t}\n}\n\nvoid EditScans::List::errorAnimationCallback() {\n\tconst auto error = errorAnimation.value(errorShown ? 1. : 0.);\n\tif (error == 0.) {\n\t\tupload->setColorOverride(std::nullopt);\n\t} else {\n\t\tupload->setColorOverride(anim::color(\n\t\t\tst::passportUploadButton.textFg,\n\t\t\tst::boxTextFgError,\n\t\t\terror));\n\t}\n}\n\nvoid EditScans::List::updateScan(ScanInfo &&info, int width) {\n\tconst auto i = ranges::find(files, info.key, [](const ScanInfo &file) {\n\t\treturn file.key;\n\t});\n\tif (i != files.end()) {\n\t\t*i = std::move(info);\n\t\tconst auto scan = rows[i - files.begin()]->entity();\n\t\tUpdateFileRow(scan, *i);\n\t\tif (!i->deleted) {\n\t\t\thideError();\n\t\t}\n\t} else {\n\t\tfiles.push_back(std::move(info));\n\t\tpushScan(files.back());\n\t\twrap->resizeToWidth(width);\n\t\trows.back()->show(anim::type::normal);\n\t\tif (divider) {\n\t\t\tdivider->hide(anim::type::normal);\n\t\t}\n\t\theader->show(anim::type::normal);\n\t\tuploadTexts.fire(uploadButtonText());\n\t}\n}\n\nvoid EditScans::List::pushScan(const ScanInfo &info) {\n\tconst auto index = rows.size();\n\tconst auto type = info.type;\n\trows.push_back(CreateScan(\n\t\twrap,\n\t\tinfo,\n\t\ttr::lng_passport_scan_index(tr::now, lt_index, QString::number(index + 1))));\n\trows.back()->hide(anim::type::instant);\n\n\tconst auto scan = rows.back()->entity();\n\n\tscan->deleteClicks(\n\t) | rpl::on_next([=] {\n\t\tcontroller->deleteScan(type, index);\n\t}, scan->lifetime());\n\n\tscan->restoreClicks(\n\t) | rpl::on_next([=] {\n\t\tcontroller->restoreScan(type, index);\n\t}, scan->lifetime());\n\n\thideError();\n}\n\nScanButton::ScanButton(\n\tQWidget *parent,\n\tconst style::PassportScanRow &st,\n\tconst QString &name,\n\tconst QString &status,\n\tbool deleted,\n\tbool error)\n: AbstractButton(parent)\n, _st(st)\n, _name(\n\tst::passportScanNameStyle,\n\tname,\n\tUi::NameTextOptions())\n, _status(\n\tst::defaultTextStyle,\n\tstatus,\n\tUi::NameTextOptions())\n, _error(error)\n, _delete(this, object_ptr<Ui::IconButton>(this, _st.remove))\n, _restore(\n\tthis,\n\tobject_ptr<Ui::RoundButton>(\n\t\tthis,\n\t\ttr::lng_passport_delete_scan_undo(),\n\t\t_st.restore)) {\n\t_restore->entity()->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\t_delete->toggle(!deleted, anim::type::instant);\n\t_restore->toggle(deleted, anim::type::instant);\n}\n\nvoid ScanButton::setImage(const QImage &image) {\n\t_image = image;\n\tupdate();\n}\n\nvoid ScanButton::setStatus(const QString &status) {\n\t_status.setText(\n\t\tst::defaultTextStyle,\n\t\tstatus,\n\t\tUi::NameTextOptions());\n\tupdate();\n}\n\nvoid ScanButton::setDeleted(bool deleted) {\n\t_delete->toggle(!deleted, anim::type::instant);\n\t_restore->toggle(deleted, anim::type::instant);\n\tupdate();\n}\n\nvoid ScanButton::setError(bool error) {\n\t_error = error;\n\tupdate();\n}\n\nint ScanButton::resizeGetHeight(int newWidth) {\n\t_nameHeight = st::semiboldFont->height;\n\t_statusHeight = st::normalFont->height;\n\tconst auto result = _st.padding.top() + _st.size + _st.padding.bottom();\n\tconst auto right = _st.padding.right();\n\t_delete->moveToRight(\n\t\tright,\n\t\t(result - _delete->height()) / 2,\n\t\tnewWidth);\n\t_restore->moveToRight(\n\t\tright,\n\t\t(result - _restore->height()) / 2,\n\t\tnewWidth);\n\treturn result + st::lineWidth;\n}\n\nint ScanButton::countAvailableWidth() const {\n\treturn width()\n\t\t- _st.padding.left()\n\t\t- _st.textLeft\n\t\t- _st.padding.right()\n\t\t- std::max(_delete->width(), _restore->width());\n}\n\nvoid ScanButton::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tconst auto left = _st.padding.left();\n\tconst auto top = _st.padding.top();\n\tp.fillRect(\n\t\tleft,\n\t\theight() - _st.border,\n\t\twidth() - left,\n\t\t_st.border,\n\t\t_st.borderFg);\n\n\tconst auto deleted = _restore->toggled();\n\tif (deleted) {\n\t\tp.setOpacity(st::passportScanDeletedOpacity);\n\t}\n\n\tif (_image.isNull()) {\n\t\tp.fillRect(left, top, _st.size, _st.size, Qt::black);\n\t} else {\n\t\tPainterHighQualityEnabler hq(p);\n\t\tconst auto fromRect = [&] {\n\t\t\tif (_image.width() > _image.height()) {\n\t\t\t\tconst auto shift = (_image.width() - _image.height()) / 2;\n\t\t\t\treturn QRect(shift, 0, _image.height(), _image.height());\n\t\t\t} else {\n\t\t\t\tconst auto shift = (_image.height() - _image.width()) / 2;\n\t\t\t\treturn QRect(0, shift, _image.width(), _image.width());\n\t\t\t}\n\t\t}();\n\t\tp.drawImage(QRect(left, top, _st.size, _st.size), _image, fromRect);\n\t}\n\tconst auto availableWidth = countAvailableWidth();\n\n\tp.setPen(st::windowFg);\n\t_name.drawLeftElided(\n\t\tp,\n\t\tleft + _st.textLeft,\n\t\ttop + _st.nameTop,\n\t\tavailableWidth,\n\t\twidth());\n\tp.setPen((_error && !deleted)\n\t\t? st::boxTextFgError\n\t\t: st::windowSubTextFg);\n\t_status.drawLeftElided(\n\t\tp,\n\t\tleft + _st.textLeft,\n\t\ttop + _st.statusTop,\n\t\tavailableWidth,\n\t\twidth());\n}\n\nEditScans::SpecialScan::SpecialScan(ScanInfo &&file)\n: file(std::move(file)) {\n}\n\nEditScans::EditScans(\n\tQWidget *parent,\n\tnot_null<PanelController*> controller,\n\tconst QString &header,\n\tconst QString &error,\n\tScanListData &&scans,\n\tstd::optional<ScanListData> &&translations)\n: RpWidget(parent)\n, _controller(controller)\n, _error(error)\n, _content(this)\n, _scansList(_controller, std::move(scans))\n, _translationsList(_controller, std::move(translations)) {\n\tsetupScans(header);\n}\n\nEditScans::EditScans(\n\tQWidget *parent,\n\tnot_null<PanelController*> controller,\n\tconst QString &header,\n\tconst QString &error,\n\tstd::map<FileType, ScanInfo> &&specialFiles,\n\tstd::optional<ScanListData> &&translations)\n: RpWidget(parent)\n, _controller(controller)\n, _error(error)\n, _content(this)\n, _scansList(_controller)\n, _translationsList(_controller, std::move(translations)) {\n\tsetupSpecialScans(header, std::move(specialFiles));\n}\n\nstd::optional<int> EditScans::validateGetErrorTop() {\n\tauto result = std::optional<int>();\n\tconst auto suggestResult = [&](int value) {\n\t\tif (!result || *result > value) {\n\t\t\tresult = value;\n\t\t}\n\t};\n\n\tif (_commonError && !somethingChanged()) {\n\t\tsuggestResult(_commonError->y());\n\t}\n\tconst auto suggestList = [&](FileType type) {\n\t\tauto &list = this->list(type);\n\t\tif (list.uploadMoreRequired()) {\n\t\t\tlist.toggleError(true);\n\t\t\tsuggestResult((list.files.size() > 5)\n\t\t\t\t? list.upload->y()\n\t\t\t\t: list.header->y());\n\t\t}\n\t\tif (const auto row = list.nonDeletedErrorRow()) {\n\t\t\t//toggleError(true);\n\t\t\tsuggestResult(row->y());\n\t\t}\n\t};\n\tsuggestList(FileType::Scan);\n\tfor (const auto &[type, scan] : _specialScans) {\n\t\tif (!scan.file.key.id\n\t\t\t|| scan.file.deleted\n\t\t\t|| !scan.file.error.isEmpty()) {\n\t\t\ttoggleSpecialScanError(type, true);\n\t\t\tsuggestResult(scan.header ? scan.header->y() : scan.wrap->y());\n\t\t}\n\t}\n\tsuggestList(FileType::Translation);\n\treturn result;\n}\n\nEditScans::List &EditScans::list(FileType type) {\n\tswitch (type) {\n\tcase FileType::Scan: return _scansList;\n\tcase FileType::Translation: return _translationsList;\n\t}\n\tUnexpected(\"Type in EditScans::list().\");\n}\n\nconst EditScans::List &EditScans::list(FileType type) const {\n\tswitch (type) {\n\tcase FileType::Scan: return _scansList;\n\tcase FileType::Translation: return _translationsList;\n\t}\n\tUnexpected(\"Type in EditScans::list() const.\");\n}\n\nvoid EditScans::setupScans(const QString &header) {\n\tconst auto inner = _content.data();\n\tinner->move(0, 0);\n\n\tif (!_error.isEmpty()) {\n\t\t_commonError = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::FlatLabel>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tinner,\n\t\t\t\t\t_error,\n\t\t\t\t\tst::passportVerifyErrorLabel),\n\t\t\t\tst::passportValueErrorPadding));\n\t\t_commonError->toggle(true, anim::type::instant);\n\t}\n\n\tsetupList(inner, FileType::Scan, header);\n\tsetupList(inner, FileType::Translation, tr::lng_passport_translation(tr::now));\n\n\tinit();\n}\n\nvoid EditScans::setupList(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tFileType type,\n\t\tconst QString &header) {\n\tauto &list = this->list(type);\n\tif (!list.initialCount) {\n\t\treturn;\n\t}\n\n\tif (type == FileType::Scan) {\n\t\tlist.divider = container->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::BoxContentDivider>(\n\t\t\t\t\tcontainer,\n\t\t\t\t\tst::passportFormDividerHeight)));\n\t\tlist.divider->toggle(list.files.empty(), anim::type::instant);\n\t}\n\tlist.header = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::FlatLabel>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\theader,\n\t\t\t\tst::passportFormHeader),\n\t\t\tst::passportUploadHeaderPadding));\n\tlist.header->toggle(\n\t\t!list.divider || !list.files.empty(),\n\t\tanim::type::instant);\n\tif (!list.errorMissing.isEmpty()) {\n\t\tlist.uploadMoreError = container->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::FlatLabel>>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tcontainer,\n\t\t\t\t\tlist.errorMissing,\n\t\t\t\t\tst::passportVerifyErrorLabel),\n\t\t\t\tst::passportUploadErrorPadding));\n\t\tlist.uploadMoreError->toggle(true, anim::type::instant);\n\t}\n\tlist.wrap = container->add(object_ptr<Ui::VerticalLayout>(container));\n\tfor (const auto &scan : list.files) {\n\t\tlist.pushScan(scan);\n\t\tlist.rows.back()->show(anim::type::instant);\n\t}\n\n\tlist.upload = container->add(\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\tcontainer,\n\t\t\tlist.uploadTexts.events_starting_with(\n\t\t\t\tlist.uploadButtonText()\n\t\t\t) | rpl::flatten_latest(),\n\t\t\tst::passportUploadButton),\n\t\tst::passportUploadButtonPadding);\n\tlist.upload->addClickHandler([=] {\n\t\tchooseScan(type);\n\t});\n\n\tcontainer->add(object_ptr<Ui::BoxContentDivider>(\n\t\tcontainer,\n\t\tst::passportFormDividerHeight));\n}\n\nvoid EditScans::setupSpecialScans(\n\t\tconst QString &header,\n\t\tstd::map<FileType, ScanInfo> &&files) {\n\tconst auto requiresBothSides = files.find(FileType::ReverseSide)\n\t\t!= end(files);\n\tconst auto uploadText = [=](FileType type, bool hasScan) {\n\t\tswitch (type) {\n\t\tcase FileType::FrontSide:\n\t\t\treturn requiresBothSides\n\t\t\t\t? (hasScan\n\t\t\t\t\t? tr::lng_passport_reupload_front_side\n\t\t\t\t\t: tr::lng_passport_upload_front_side)\n\t\t\t\t: (hasScan\n\t\t\t\t\t? tr::lng_passport_reupload_main_page\n\t\t\t\t\t: tr::lng_passport_upload_main_page);\n\t\tcase FileType::ReverseSide:\n\t\t\treturn hasScan\n\t\t\t\t? tr::lng_passport_reupload_reverse_side\n\t\t\t\t: tr::lng_passport_upload_reverse_side;\n\t\tcase FileType::Selfie:\n\t\t\treturn hasScan\n\t\t\t\t? tr::lng_passport_reupload_selfie\n\t\t\t\t: tr::lng_passport_upload_selfie;\n\t\t}\n\t\tUnexpected(\"Type in special row upload key.\");\n\t};\n\tconst auto description = [&](FileType type) {\n\t\tswitch (type) {\n\t\tcase FileType::FrontSide:\n\t\t\treturn requiresBothSides\n\t\t\t\t? tr::lng_passport_front_side_description\n\t\t\t\t: tr::lng_passport_main_page_description;\n\t\tcase FileType::ReverseSide:\n\t\t\treturn tr::lng_passport_reverse_side_description;\n\t\tcase FileType::Selfie:\n\t\t\treturn tr::lng_passport_selfie_description;\n\t\t}\n\t\tUnexpected(\"Type in special row upload key.\");\n\t};\n\n\tconst auto inner = _content.data();\n\tinner->move(0, 0);\n\n\tif (!_error.isEmpty()) {\n\t\t_commonError = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::FlatLabel>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tinner,\n\t\t\t\t\t_error,\n\t\t\t\t\tst::passportVerifyErrorLabel),\n\t\t\t\tst::passportValueErrorPadding));\n\t\t_commonError->toggle(true, anim::type::instant);\n\t}\n\n\tfor (auto &[type, info] : files) {\n\t\tconst auto i = _specialScans.emplace(\n\t\t\ttype,\n\t\t\tSpecialScan(std::move(info))).first;\n\t\tauto &scan = i->second;\n\n\t\tif (_specialScans.size() == 1) {\n\t\t\tscan.header = inner->add(\n\t\t\t\tobject_ptr<Ui::SlideWrap<Ui::FlatLabel>>(\n\t\t\t\t\tinner,\n\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\tinner,\n\t\t\t\t\t\theader,\n\t\t\t\t\t\tst::passportFormHeader),\n\t\t\t\t\tst::passportUploadHeaderPadding));\n\t\t\tscan.header->toggle(scan.file.key.id != 0, anim::type::instant);\n\t\t}\n\t\tscan.wrap = inner->add(object_ptr<Ui::VerticalLayout>(inner));\n\t\tif (scan.file.key.id) {\n\t\t\tcreateSpecialScanRow(scan, scan.file, requiresBothSides);\n\t\t}\n\t\tauto label = scan.rowCreated.value(\n\t\t) | rpl::map([=, type = type](bool created) {\n\t\t\treturn uploadText(type, created)(tr::upper);\n\t\t}) | rpl::flatten_latest();\n\t\tscan.upload = inner->add(\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tinner,\n\t\t\t\tstd::move(label),\n\t\t\t\tst::passportUploadButton),\n\t\t\tst::passportUploadButtonPadding);\n\t\tscan.upload->addClickHandler([=, type = type] {\n\t\t\tchooseScan(type);\n\t\t});\n\n\t\tinner->add(object_ptr<Ui::DividerLabel>(\n\t\t\tinner,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tinner,\n\t\t\t\tdescription(type)(tr::now),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::passportFormLabelPadding));\n\t}\n\n\tsetupList(inner, FileType::Translation, tr::lng_passport_translation(tr::now));\n\n\tinit();\n}\n\nvoid EditScans::init() {\n\t_controller->scanUpdated(\n\t) | rpl::on_next([=](ScanInfo &&info) {\n\t\tupdateScan(std::move(info));\n\t}, lifetime());\n\n\twidthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_content->resizeToWidth(width);\n\t}, _content->lifetime());\n\n\t_content->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tresize(width(), height);\n\t}, _content->lifetime());\n}\n\nvoid EditScans::updateScan(ScanInfo &&info) {\n\tif (info.type != FileType::Scan && info.type != FileType::Translation) {\n\t\tupdateSpecialScan(std::move(info));\n\t\treturn;\n\t}\n\tlist(info.type).updateScan(std::move(info), width());\n\tupdateErrorLabels();\n}\n\nvoid EditScans::scanFieldsChanged(bool changed) {\n\tif (_scanFieldsChanged != changed) {\n\t\t_scanFieldsChanged = changed;\n\t\tupdateErrorLabels();\n\t}\n}\n\nvoid EditScans::updateErrorLabels() {\n\tconst auto updateList = [&](FileType type) {\n\t\tauto &list = this->list(type);\n\t\tif (list.uploadMoreError) {\n\t\t\tlist.uploadMoreError->toggle(\n\t\t\t\t!list.uploadedSomeMore(),\n\t\t\t\tanim::type::normal);\n\t\t}\n\t};\n\tupdateList(FileType::Scan);\n\tupdateList(FileType::Translation);\n\tif (_commonError) {\n\t\t_commonError->toggle(!somethingChanged(), anim::type::normal);\n\t}\n}\n\nbool EditScans::somethingChanged() const {\n\treturn list(FileType::Scan).uploadedSomeMore()\n\t\t|| list(FileType::Translation).uploadedSomeMore()\n\t\t|| _scanFieldsChanged\n\t\t|| _specialScanChanged;\n}\n\nvoid EditScans::updateSpecialScan(ScanInfo &&info) {\n\tExpects(info.key.id != 0);\n\n\tconst auto type = info.type;\n\tconst auto i = _specialScans.find(type);\n\tif (i == end(_specialScans)) {\n\t\treturn;\n\t}\n\tauto &scan = i->second;\n\tif (scan.file.key.id) {\n\t\tUpdateFileRow(scan.row->entity(), info);\n\t\tscan.rowCreated = !info.deleted;\n\t\tif (scan.file.key.id != info.key.id) {\n\t\t\tspecialScanChanged(type, true);\n\t\t}\n\t} else {\n\t\tconst auto requiresBothSides\n\t\t\t= (_specialScans.find(FileType::ReverseSide)\n\t\t\t\t!= end(_specialScans));\n\t\tcreateSpecialScanRow(scan, info, requiresBothSides);\n\t\tscan.wrap->resizeToWidth(width());\n\t\tscan.row->show(anim::type::normal);\n\t\tif (scan.header) {\n\t\t\tscan.header->show(anim::type::normal);\n\t\t}\n\t\tspecialScanChanged(type, true);\n\t}\n\tscan.file = std::move(info);\n}\n\nvoid EditScans::createSpecialScanRow(\n\t\tSpecialScan &scan,\n\t\tconst ScanInfo &info,\n\t\tbool requiresBothSides) {\n\tExpects(scan.file.type != FileType::Scan\n\t\t&& scan.file.type != FileType::Translation);\n\n\tconst auto type = scan.file.type;\n\tconst auto name = [&] {\n\t\tswitch (type) {\n\t\tcase FileType::FrontSide:\n\t\t\treturn requiresBothSides\n\t\t\t\t? tr::lng_passport_front_side_title(tr::now)\n\t\t\t\t: tr::lng_passport_main_page_title(tr::now);\n\t\tcase FileType::ReverseSide:\n\t\t\treturn tr::lng_passport_reverse_side_title(tr::now);\n\t\tcase FileType::Selfie:\n\t\t\treturn tr::lng_passport_selfie_title(tr::now);\n\t\t}\n\t\tUnexpected(\"Type in special file name.\");\n\t}();\n\tscan.row = CreateScan(scan.wrap, info, name);\n\tconst auto row = scan.row->entity();\n\n\trow->deleteClicks(\n\t) | rpl::on_next([=] {\n\t\t_controller->deleteScan(type, std::nullopt);\n\t}, row->lifetime());\n\n\trow->restoreClicks(\n\t) | rpl::on_next([=] {\n\t\t_controller->restoreScan(type, std::nullopt);\n\t}, row->lifetime());\n\n\tscan.rowCreated = !info.deleted;\n}\n\nvoid EditScans::chooseScan(FileType type) {\n\tif (!_controller->canAddScan(type)) {\n\t\t_controller->showToast(tr::lng_passport_scans_limit_reached(tr::now));\n\t\treturn;\n\t}\n\tChooseScan(this, type, [=](QByteArray &&content) {\n\t\t_controller->uploadScan(type, std::move(content));\n\t}, [=](ReadScanError error) {\n\t\t_controller->readScanError(error);\n\t});\n}\n\nvoid EditScans::ChooseScan(\n\t\tQPointer<QWidget> parent,\n\t\tFileType type,\n\t\tFn<void(QByteArray&&)> doneCallback,\n\t\tFn<void(ReadScanError)> errorCallback) {\n\tExpects(parent != nullptr);\n\n\tconst auto filter = FileDialog::AllOrImagesFilter();\n\tconst auto guardedCallback = crl::guard(parent, doneCallback);\n\tconst auto guardedError = crl::guard(parent, errorCallback);\n\tconst auto onMainError = [=](ReadScanError error) {\n\t\tcrl::on_main([=] {\n\t\t\tguardedError(error);\n\t\t});\n\t};\n\tconst auto processFiles = [=](\n\t\t\tQStringList &&files,\n\t\t\tconst auto &handleImage) -> void {\n\t\twhile (!files.isEmpty()) {\n\t\t\tauto file = files.front();\n\t\t\tfiles.removeAt(0);\n\n\t\t\tauto content = [&] {\n\t\t\t\tQFile f(file);\n\t\t\t\tif (f.size() > Images::kReadBytesLimit) {\n\t\t\t\t\tguardedError(ReadScanError::FileTooLarge);\n\t\t\t\t\treturn QByteArray();\n\t\t\t\t} else if (!f.open(QIODevice::ReadOnly)) {\n\t\t\t\t\tguardedError(ReadScanError::CantReadImage);\n\t\t\t\t\treturn QByteArray();\n\t\t\t\t}\n\t\t\t\treturn f.readAll();\n\t\t\t}();\n\t\t\tif (!content.isEmpty()) {\n\t\t\t\thandleImage(\n\t\t\t\t\tstd::move(content),\n\t\t\t\t\tstd::move(files),\n\t\t\t\t\thandleImage);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t};\n\tconst auto processImage = [=](\n\t\t\tQByteArray &&content,\n\t\t\tQStringList &&remainingFiles,\n\t\t\tconst auto &repeatProcessImage) -> void {\n\t\tcrl::async([\n\t\t\t=,\n\t\t\tbytes = std::move(content),\n\t\t\tremainingFiles = std::move(remainingFiles)\n\t\t]() mutable {\n\t\t\tauto result = ProcessImage(std::move(bytes));\n\t\t\tif (const auto error = std::get_if<ReadScanError>(&result)) {\n\t\t\t\tonMainError(*error);\n\t\t\t} else {\n\t\t\t\tauto content = std::get_if<QByteArray>(&result);\n\t\t\t\tAssert(content != nullptr);\n\t\t\t\tcrl::on_main([\n\t\t\t\t\t=,\n\t\t\t\t\tbytes = std::move(*content),\n\t\t\t\t\tremainingFiles = std::move(remainingFiles)\n\t\t\t\t]() mutable {\n\t\t\t\t\tguardedCallback(std::move(bytes));\n\t\t\t\t\tprocessFiles(\n\t\t\t\t\t\tstd::move(remainingFiles),\n\t\t\t\t\t\trepeatProcessImage);\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t};\n\tconst auto processOpened = [=](FileDialog::OpenResult &&result) {\n\t\tif (result.paths.size() > 0) {\n\t\t\tprocessFiles(std::move(result.paths), processImage);\n\t\t} else if (!result.remoteContent.isEmpty()) {\n\t\t\tprocessImage(std::move(result.remoteContent), {}, processImage);\n\t\t}\n\t};\n\tconst auto allowMany = (type == FileType::Scan)\n\t\t|| (type == FileType::Translation);\n\t(allowMany ? FileDialog::GetOpenPaths : FileDialog::GetOpenPath)(\n\t\tparent,\n\t\ttr::lng_passport_choose_image(tr::now),\n\t\tfilter,\n\t\tprocessOpened,\n\t\tnullptr);\n}\n\nvoid EditScans::hideSpecialScanError(FileType type) {\n\ttoggleSpecialScanError(type, false);\n}\n\nvoid EditScans::specialScanChanged(FileType type, bool changed) {\n\thideSpecialScanError(type);\n\tif (_specialScanChanged != changed) {\n\t\t_specialScanChanged = changed;\n\t\tupdateErrorLabels();\n\t}\n}\n\nauto EditScans::findSpecialScan(FileType type) -> SpecialScan& {\n\tconst auto i = _specialScans.find(type);\n\tAssert(i != end(_specialScans));\n\treturn i->second;\n}\n\nvoid EditScans::toggleSpecialScanError(FileType type, bool shown) {\n\tauto &scan = findSpecialScan(type);\n\tif (scan.errorShown != shown) {\n\t\tscan.errorShown = shown;\n\t\tscan.errorAnimation.start(\n\t\t\t[=] { specialScanErrorAnimationCallback(type); },\n\t\t\tscan.errorShown ? 0. : 1.,\n\t\t\tscan.errorShown ? 1. : 0.,\n\t\t\tst::passportDetailsField.duration);\n\t}\n}\n\nvoid EditScans::specialScanErrorAnimationCallback(FileType type) {\n\tauto &scan = findSpecialScan(type);\n\tconst auto error = scan.errorAnimation.value(\n\t\tscan.errorShown ? 1. : 0.);\n\tif (error == 0.) {\n\t\tscan.upload->setColorOverride(std::nullopt);\n\t} else {\n\t\tscan.upload->setColorOverride(anim::color(\n\t\t\tst::passportUploadButton.textFg,\n\t\t\tst::boxTextFgError,\n\t\t\terror));\n\t}\n}\n\nEditScans::~EditScans() = default;\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_panel_edit_scans.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\nclass BoxContentDivider;\nclass VerticalLayout;\nclass SettingsButton;\nclass FlatLabel;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Passport {\n\nenum class FileType;\nclass PanelController;\nclass ScanButton;\nstruct ScanInfo;\n\nenum class ReadScanError {\n\tFileTooLarge,\n\tCantReadImage,\n\tBadImageSize,\n\tUnknown,\n};\n\nstruct ScanListData {\n\tstd::vector<ScanInfo> files;\n\tQString errorMissing;\n};\n\nclass EditScans : public Ui::RpWidget {\npublic:\n\tEditScans(\n\t\tQWidget *parent,\n\t\tnot_null<PanelController*> controller,\n\t\tconst QString &header,\n\t\tconst QString &error,\n\t\tScanListData &&scans,\n\t\tstd::optional<ScanListData> &&translations);\n\tEditScans(\n\t\tQWidget *parent,\n\t\tnot_null<PanelController*> controller,\n\t\tconst QString &header,\n\t\tconst QString &error,\n\t\tstd::map<FileType, ScanInfo> &&specialFiles,\n\t\tstd::optional<ScanListData> &&translations);\n\n\tstd::optional<int> validateGetErrorTop();\n\n\tvoid scanFieldsChanged(bool changed);\n\n\tstatic void ChooseScan(\n\t\tQPointer<QWidget> parent,\n\t\tFileType type,\n\t\tFn<void(QByteArray&&)> doneCallback,\n\t\tFn<void(ReadScanError)> errorCallback);\n\n\t~EditScans();\n\nprivate:\n\tstruct SpecialScan;\n\tstruct List {\n\t\tList(not_null<PanelController*> controller, ScanListData &&data);\n\t\tList(not_null<PanelController*> controller);\n\t\tList(\n\t\t\tnot_null<PanelController*> controller,\n\t\t\tstd::optional<ScanListData> &&data);\n\n\t\tbool uploadedSomeMore() const;\n\t\tbool uploadMoreRequired() const;\n\t\tUi::SlideWrap<ScanButton> *nonDeletedErrorRow() const;\n\t\trpl::producer<QString> uploadButtonText() const;\n\t\tvoid toggleError(bool shown);\n\t\tvoid hideError();\n\t\tvoid errorAnimationCallback();\n\t\tvoid updateScan(ScanInfo &&info, int width);\n\t\tvoid pushScan(const ScanInfo &info);\n\n\t\tnot_null<PanelController*> controller;\n\t\tstd::vector<ScanInfo> files;\n\t\tstd::optional<int> initialCount;\n\t\tQString errorMissing;\n\t\tQPointer<Ui::SlideWrap<Ui::BoxContentDivider>> divider;\n\t\tQPointer<Ui::SlideWrap<Ui::FlatLabel>> header;\n\t\tQPointer<Ui::SlideWrap<Ui::FlatLabel>> uploadMoreError;\n\t\tQPointer<Ui::VerticalLayout> wrap;\n\t\tstd::vector<base::unique_qptr<Ui::SlideWrap<ScanButton>>> rows;\n\t\tQPointer<Ui::SettingsButton> upload;\n\t\trpl::event_stream<rpl::producer<QString>> uploadTexts;\n\t\tbool errorShown = false;\n\t\tUi::Animations::Simple errorAnimation;\n\t};\n\n\tList &list(FileType type);\n\tconst List &list(FileType type) const;\n\n\tvoid setupScans(const QString &header);\n\tvoid setupList(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tFileType type,\n\t\tconst QString &header);\n\tvoid setupSpecialScans(\n\t\tconst QString &header,\n\t\tstd::map<FileType, ScanInfo> &&files);\n\tvoid init();\n\n\tvoid chooseScan(FileType type);\n\tvoid updateScan(ScanInfo &&info);\n\tvoid updateSpecialScan(ScanInfo &&info);\n\tvoid createSpecialScanRow(\n\t\tSpecialScan &scan,\n\t\tconst ScanInfo &info,\n\t\tbool requiresBothSides);\n\tbase::unique_qptr<Ui::SlideWrap<ScanButton>> createScan(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tconst ScanInfo &info,\n\t\tconst QString &name);\n\tSpecialScan &findSpecialScan(FileType type);\n\n\tvoid updateErrorLabels();\n\tbool somethingChanged() const;\n\n\tvoid toggleSpecialScanError(FileType type, bool shown);\n\tvoid hideSpecialScanError(FileType type);\n\tvoid specialScanErrorAnimationCallback(FileType type);\n\tvoid specialScanChanged(FileType type, bool changed);\n\n\tnot_null<PanelController*> _controller;\n\tQString _error;\n\tobject_ptr<Ui::VerticalLayout> _content;\n\tQPointer<Ui::SlideWrap<Ui::FlatLabel>> _commonError;\n\tbool _scanFieldsChanged = false;\n\tbool _specialScanChanged = false;\n\n\tList _scansList;\n\tstd::map<FileType, SpecialScan> _specialScans;\n\tList _translationsList;\n\n\n};\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_panel_form.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"passport/passport_panel_form.h\"\n\n#include \"passport/passport_panel_controller.h\"\n#include \"passport/ui/passport_form_row.h\"\n#include \"lang/lang_keys.h\"\n#include \"boxes/abstract_box.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/data_user.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/scroll_content_shadow.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/box_content_divider.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_passport.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace Passport {\n\nPanelForm::PanelForm(\n\tQWidget *parent,\n\tnot_null<PanelController*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _scroll(this, st::passportPanelScroll)\n, _submit(\n\t\tthis,\n\t\ttr::lng_passport_authorize(),\n\t\tst::passportPanelAuthorize) {\n\t_submit->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\tsetupControls();\n}\n\nvoid PanelForm::setupControls() {\n\tconst auto inner = setupContent();\n\n\t_submit->addClickHandler([=] {\n\t\t_controller->submitForm();\n\t});\n\n\tSetupShadowsToScrollContent(this, _scroll, inner->heightValue());\n}\n\nnot_null<Ui::RpWidget*> PanelForm::setupContent() {\n\tconst auto bot = _controller->bot();\n\n\tconst auto inner = _scroll->setOwnedWidget(\n\t\tobject_ptr<Ui::VerticalLayout>(this));\n\t_scroll->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tinner->resizeToWidth(width);\n\t}, inner->lifetime());\n\n\t_userpic = inner->add(\n\t\tobject_ptr<Ui::UserpicButton>(\n\t\t\tinner,\n\t\t\tbot,\n\t\t\tst::passportFormUserpic),\n\t\tst::passportFormUserpicPadding,\n\t\tstyle::al_top);\n\n\t_about1 = inner->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tinner,\n\t\t\ttr::lng_passport_request1(tr::now, lt_bot, bot->name()),\n\t\t\tst::passportPasswordLabelBold),\n\t\tst::passportFormAbout1Padding,\n\t\tstyle::al_top);\n\n\t_about2 = inner->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tinner,\n\t\t\ttr::lng_passport_request2(tr::now),\n\t\t\tst::passportPasswordLabel),\n\t\tst::passportFormAbout2Padding,\n\t\tstyle::al_top);\n\n\tinner->add(object_ptr<Ui::BoxContentDivider>(\n\t\tinner,\n\t\tst::passportFormDividerHeight));\n\tinner->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tinner,\n\t\t\ttr::lng_passport_header(tr::now),\n\t\t\tst::passportFormHeader),\n\t\tst::passportFormHeaderPadding);\n\n\tauto index = 0;\n\t_controller->fillRows([&](\n\t\t\tQString title,\n\t\t\tQString description,\n\t\t\tbool ready,\n\t\t\tbool error) {\n\t\t_rows.push_back(inner->add(object_ptr<Row>(this)));\n\t\t_rows.back()->addClickHandler([=] {\n\t\t\t_controller->editScope(index);\n\t\t});\n\t\t_rows.back()->updateContent(\n\t\t\ttitle,\n\t\t\tdescription,\n\t\t\tready,\n\t\t\terror,\n\t\t\tanim::type::instant);\n\t\t++index;\n\t});\n\t_controller->refillRows(\n\t) | rpl::on_next([=] {\n\t\tauto index = 0;\n\t\t_controller->fillRows([&](\n\t\t\t\tQString title,\n\t\t\t\tQString description,\n\t\t\t\tbool ready,\n\t\t\t\tbool error) {\n\t\t\tExpects(index < _rows.size());\n\n\t\t\t_rows[index++]->updateContent(\n\t\t\t\ttitle,\n\t\t\t\tdescription,\n\t\t\t\tready,\n\t\t\t\terror,\n\t\t\t\tanim::type::normal);\n\t\t});\n\t}, lifetime());\n\tconst auto policyUrl = _controller->privacyPolicyUrl();\n\tauto policyLink = tr::lng_passport_policy(\n\t\tlt_bot,\n\t\trpl::single(bot->name())\n\t) | rpl::map(\n\t\ttr::url(policyUrl)\n\t) | rpl::map([=](TextWithEntities &&text) {\n\t\treturn Ui::Text::Wrapped(std::move(text), EntityType::Bold);\n\t});\n\tauto text = policyUrl.isEmpty()\n\t\t? tr::lng_passport_allow(\n\t\t\tlt_bot,\n\t\t\trpl::single(tr::marked('@' + bot->username())),\n\t\t\ttr::marked)\n\t\t: tr::lng_passport_accept_allow(\n\t\t\tlt_policy,\n\t\t\tstd::move(policyLink),\n\t\t\tlt_bot,\n\t\t\trpl::single(tr::marked('@' + bot->username())),\n\t\t\ttr::marked);\n\tconst auto policy = inner->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tinner,\n\t\t\tstd::move(text),\n\t\t\tst::passportFormPolicy),\n\t\tst::passportFormPolicyPadding);\n\tpolicy->setLinksTrusted();\n\n\treturn inner;\n}\n\nvoid PanelForm::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid PanelForm::updateControlsGeometry() {\n\tconst auto submitTop = height() - _submit->height();\n\t_scroll->setGeometry(0, 0, width(), submitTop);\n\t_submit->setFullWidth(width());\n\t_submit->moveToLeft(0, submitTop);\n\n\t_scroll->updateBars();\n}\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_panel_form.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\nclass BoxContentDivider;\nclass ScrollArea;\nclass RoundButton;\nclass FlatLabel;\nclass UserpicButton;\n} // namespace Ui\n\nnamespace Passport::Ui {\nusing namespace ::Ui;\nclass FormRow;\n} // namespace Passport::Ui\n\nnamespace Passport {\n\nclass PanelController;\n\nclass PanelForm : public Ui::RpWidget {\npublic:\n\tPanelForm(\n\t\tQWidget *parent,\n\t\tnot_null<PanelController*> controller);\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tusing Row = Ui::FormRow;\n\n\tvoid setupControls();\n\tnot_null<Ui::RpWidget*> setupContent();\n\tvoid updateControlsGeometry();\n\n\tnot_null<PanelController*> _controller;\n\n\tobject_ptr<Ui::ScrollArea> _scroll;\n\tobject_ptr<Ui::RoundButton> _submit;\n\n\tQPointer<Ui::UserpicButton> _userpic;\n\tQPointer<Ui::FlatLabel> _about1;\n\tQPointer<Ui::FlatLabel> _about2;\n\tstd::vector<QPointer<Row>> _rows;\n\n};\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_panel_password.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"passport/passport_panel_password.h\"\n\n#include \"passport/passport_panel_controller.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/password_input.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"boxes/passcode_box.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"styles/style_passport.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Passport {\n\nPanelAskPassword::PanelAskPassword(\n\tQWidget *parent,\n\tnot_null<PanelController*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _userpic(\n\tthis,\n\t_controller->bot(),\n\tst::passportPasswordUserpic)\n, _about1(\n\tthis,\n\ttr::lng_passport_request1(\n\t\ttr::now,\n\t\tlt_bot,\n\t\t_controller->bot()->name()),\n\tst::passportPasswordLabelBold)\n, _about2(\n\tthis,\n\ttr::lng_passport_request2(tr::now),\n\tst::passportPasswordLabel)\n, _password(\n\tthis,\n\tst::defaultInputField,\n\ttr::lng_passport_password_placeholder())\n, _submit(this, tr::lng_passport_next(), st::passportPasswordSubmit)\n, _forgot(this, tr::lng_signin_recover(tr::now), st::defaultLinkButton) {\n\t_submit->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\tconnect(_password, &Ui::PasswordInput::submitted, this, [=] {\n\t\tsubmit();\n\t});\n\tconnect(_password, &Ui::PasswordInput::changed, this, [=] {\n\t\thideError();\n\t});\n\tif (const auto hint = _controller->passwordHint(); !hint.isEmpty()) {\n\t\t_hint.create(\n\t\t\tthis,\n\t\t\thint,\n\t\t\tst::passportPasswordHintLabel);\n\t}\n\t_controller->passwordError(\n\t) | rpl::on_next([=](const QString &error) {\n\t\tshowError(error);\n\t}, lifetime());\n\n\t_forgot->addClickHandler([=] {\n\t\trecover();\n\t});\n\n\t_password->setFocusFast();\n\t_userpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t_submit->addClickHandler([=] {\n\t\tsubmit();\n\t});\n}\n\nvoid PanelAskPassword::showError(const QString &error) {\n\t_password->showError();\n\t_error.create(\n\t\tthis,\n\t\terror,\n\t\tst::passportErrorLabel);\n\t_error->show();\n\tupdateControlsGeometry();\n}\n\nvoid PanelAskPassword::hideError() {\n\t_error.destroy();\n}\n\nvoid PanelAskPassword::submit() {\n\t_controller->submitPassword(_password->getLastText().toUtf8());\n}\n\nvoid PanelAskPassword::recover() {\n\t_controller->recoverPassword();\n}\n\nvoid PanelAskPassword::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid PanelAskPassword::focusInEvent(QFocusEvent *e) {\n\tcrl::on_main(this, [=] {\n\t\t_password->setFocusFast();\n\t});\n}\n\nvoid PanelAskPassword::updateControlsGeometry() {\n\tconst auto padding = st::passportPasswordPadding;\n\tconst auto availableWidth = width()\n\t\t- st::boxPadding.left()\n\t\t- st::boxPadding.right();\n\n\tauto top = st::passportPasswordFieldBottom;\n\ttop -= _password->height();\n\t_password->resize(\n\t\tst::passportPasswordSubmit.width,\n\t\t_password->height());\n\t_password->moveToLeft((width() - _password->width()) / 2, top);\n\n\ttop -= st::passportPasswordFieldSkip + _about2->height();\n\t_about2->resizeToWidth(availableWidth);\n\t_about2->moveToLeft(padding.left(), top);\n\n\ttop -= _about1->height();\n\t_about1->resizeToWidth(availableWidth);\n\t_about1->moveToLeft(padding.left(), top);\n\n\ttop -= st::passportPasswordUserpicSkip + _userpic->height();\n\t_userpic->moveToLeft((width() - _userpic->width()) / 2, top);\n\n\ttop = st::passportPasswordFieldBottom;\n\tif (_hint) {\n\t\ttop += st::passportPasswordHintSkip;\n\t\t_hint->resizeToWidth(availableWidth);\n\t\t_hint->moveToLeft(padding.left(), top);\n\t\ttop += _hint->height();\n\t}\n\tif (_error) {\n\t\ttop += st::passportPasswordHintSkip;\n\t\t_error->resizeToWidth(availableWidth);\n\t\t_error->moveToLeft(padding.left(), top);\n\t\ttop += _error->height();\n\t}\n\n\ttop = height() - st::passportPasswordSubmitBottom - _submit->height();\n\t_submit->moveToLeft((width() - _submit->width()) / 2, top);\n\n\ttop = height() - st::passportPasswordForgotBottom - _forgot->height();\n\t_forgot->moveToLeft((width() - _forgot->width()) / 2, top);\n}\n\nPanelNoPassword::PanelNoPassword(\n\tQWidget *parent,\n\tnot_null<PanelController*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _inner(Ui::CreateChild<Ui::VerticalLayout>(this)) {\n\tsetupContent();\n}\n\nvoid PanelNoPassword::setupContent() {\n\twidthValue(\n\t) | rpl::on_next([=](int newWidth) {\n\t\t_inner->resizeToWidth(newWidth);\n\t}, _inner->lifetime());\n\n\t_inner->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_inner,\n\t\t\ttr::lng_passport_request1(\n\t\t\t\ttr::now,\n\t\t\t\tlt_bot,\n\t\t\t\t_controller->bot()->name()),\n\t\t\tst::passportPasswordLabelBold),\n\t\tst::passportPasswordAbout1Padding,\n\t\tstyle::al_top);\n\n\t_inner->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_inner,\n\t\t\ttr::lng_passport_request2(tr::now),\n\t\t\tst::passportPasswordLabel),\n\t\tst::passportPasswordAbout2Padding,\n\t\tstyle::al_top);\n\n\tconst auto iconWrap = _inner->add(\n\t\tobject_ptr<Ui::FixedHeightWidget>(\n\t\t\t_inner,\n\t\t\tst::passportPasswordIconHeight),\n\t\tstyle::al_top);\n\ticonWrap->setNaturalWidth(st::passportPasswordIcon.width());\n\tUi::CreateChild<Info::Profile::FloatingIcon>(\n\t\ticonWrap,\n\t\tst::passportPasswordIcon,\n\t\tQPoint(0, 0));\n\n\t_inner->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_inner,\n\t\t\ttr::lng_passport_create_password(tr::now),\n\t\t\tst::passportPasswordSetupLabel),\n\t\tst::passportFormAbout2Padding,\n\t\tstyle::al_top);\n\n\trefreshBottom();\n}\n\nvoid PanelNoPassword::refreshBottom() {\n\tconst auto pattern = _controller->unconfirmedEmailPattern();\n\t_about.reset(_inner->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_inner,\n\t\t\t(pattern.isEmpty()\n\t\t\t\t? tr::lng_passport_about_password(tr::now)\n\t\t\t\t: tr::lng_passport_code_sent(tr::now, lt_email, pattern)),\n\t\t\tst::passportPasswordSetupLabel),\n\t\tst::passportFormAbout2Padding,\n\t\tstyle::al_top));\n\tif (pattern.isEmpty()) {\n\t\tconst auto button = _inner->add(\n\t\t\tobject_ptr<Ui::RoundButton>(\n\t\t\t\t_inner,\n\t\t\t\ttr::lng_passport_password_create(),\n\t\t\t\tst::defaultBoxButton),\n\t\t\tstyle::al_top);\n\t\tbutton->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\t\tbutton->addClickHandler([=] {\n\t\t\t_controller->setupPassword();\n\t\t});\n\t} else {\n\t\tconst auto container = _inner->add(\n\t\t\tobject_ptr<Ui::FixedHeightWidget>(\n\t\t\t\t_inner,\n\t\t\t\tst::defaultBoxButton.height));\n\t\tconst auto cancel = Ui::CreateChild<Ui::RoundButton>(\n\t\t\tcontainer,\n\t\t\ttr::lng_cancel(),\n\t\t\tst::defaultBoxButton);\n\t\tcancel->addClickHandler([=] {\n\t\t\t_controller->cancelPasswordSubmit();\n\t\t});\n\t\tconst auto validate = Ui::CreateChild<Ui::RoundButton>(\n\t\t\tcontainer,\n\t\t\ttr::lng_passport_email_validate(),\n\t\t\tst::defaultBoxButton);\n\t\tvalidate->addClickHandler([=] {\n\t\t\t_controller->validateRecoveryEmail();\n\t\t});\n\t\tcontainer->widthValue(\n\t\t) | rpl::on_next([=](int width) {\n\t\t\tconst auto both = cancel->width()\n\t\t\t\t+ validate->width()\n\t\t\t\t+ st::boxLittleSkip;\n\t\t\tcancel->moveToLeft((width - both) / 2, 0, width);\n\t\t\tvalidate->moveToRight((width - both) / 2, 0, width);\n\t\t}, container->lifetime());\n\t}\n}\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/passport_panel_password.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\nclass PasswordInput;\nclass FlatLabel;\nclass LinkButton;\nclass RoundButton;\nclass UserpicButton;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Passport {\n\nclass PanelController;\n\nclass PanelAskPassword : public Ui::RpWidget {\npublic:\n\tPanelAskPassword(\n\t\tQWidget *parent,\n\t\tnot_null<PanelController*> controller);\n\n\tvoid submit();\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid focusInEvent(QFocusEvent *e) override;\n\nprivate:\n\tvoid updateControlsGeometry();\n\tvoid showError(const QString &error);\n\tvoid hideError();\n\tvoid recover();\n\n\tnot_null<PanelController*> _controller;\n\n\tobject_ptr<Ui::UserpicButton> _userpic;\n\tobject_ptr<Ui::FlatLabel> _about1;\n\tobject_ptr<Ui::FlatLabel> _about2;\n\tobject_ptr<Ui::PasswordInput> _password;\n\tobject_ptr<Ui::FlatLabel> _hint = { nullptr };\n\tobject_ptr<Ui::FlatLabel> _error = { nullptr };\n\tobject_ptr<Ui::RoundButton> _submit;\n\tobject_ptr<Ui::LinkButton> _forgot;\n\n};\n\nclass PanelNoPassword : public Ui::RpWidget {\npublic:\n\tPanelNoPassword(\n\t\tQWidget *parent,\n\t\tnot_null<PanelController*> controller);\n\nprivate:\n\tvoid setupContent();\n\tvoid refreshBottom();\n\n\tnot_null<PanelController*> _controller;\n\n\tnot_null<Ui::VerticalLayout*> _inner;\n\tbase::unique_qptr<Ui::RpWidget> _about;\n\n};\n\n} // namespace Passport\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/ui/passport_details_row.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"passport/ui/passport_details_row.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/fields/masked_input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/layers/box_content.h\"\n#include \"ui/boxes/country_select_box.h\"\n#include \"ui/painter.h\"\n#include \"countries/countries_instance.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_passport.h\"\n#include \"base/qt/qt_common_adapters.h\"\n\n#include <QtCore/QRegularExpression>\n\nnamespace Passport::Ui {\nnamespace {\n\nclass PostcodeInput : public MaskedInputField {\npublic:\n\tPostcodeInput(\n\t\tQWidget *parent,\n\t\tconst style::InputField &st,\n\t\trpl::producer<QString> placeholder,\n\t\tconst QString &val);\n\nprotected:\n\tvoid correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) override;\n\n};\n\nPostcodeInput::PostcodeInput(\n\tQWidget *parent,\n\tconst style::InputField &st,\n\trpl::producer<QString> placeholder,\n\tconst QString &val)\n: MaskedInputField(parent, st, std::move(placeholder), val) {\n\tstatic const auto RegExp = QRegularExpression(\"^[a-zA-Z0-9\\\\-]+$\");\n\tif (!RegExp.match(val).hasMatch()) {\n\t\tsetText(QString());\n\t}\n}\n\nvoid PostcodeInput::correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) {\n\tQString newText;\n\tnewText.reserve(now.size());\n\tauto newPos = nowCursor;\n\tfor (auto i = 0, l = int(now.size()); i < l; ++i) {\n\t\tconst auto ch = now[i];\n\t\tif ((ch >= '0' && ch <= '9')\n\t\t\t|| (ch >= 'a' && ch <= 'z')\n\t\t\t|| (ch >= 'A' && ch <= 'Z')\n\t\t\t|| (ch == '-')) {\n\t\t\tnewText.append(ch);\n\t\t} else if (i < nowCursor) {\n\t\t\t--newPos;\n\t\t}\n\t}\n\tsetCorrectedText(now, nowCursor, newText, newPos);\n}\n\ntemplate <typename Input>\nclass AbstractTextRow : public PanelDetailsRow {\npublic:\n\tAbstractTextRow(\n\t\tQWidget *parent,\n\t\tconst QString &label,\n\t\tint maxLabelWidth,\n\t\tconst QString &value,\n\t\tint limit);\n\n\tbool setFocusFast() override;\n\trpl::producer<QString> value() const override;\n\tQString valueCurrent() const override;\n\nprivate:\n\tint resizeInner(int left, int top, int width) override;\n\tvoid showInnerError() override;\n\tvoid finishInnerAnimating() override;\n\n\tobject_ptr<Input> _field;\n\trpl::variable<QString> _value;\n\n};\n\nclass CountryRow : public PanelDetailsRow {\npublic:\n\tCountryRow(\n\t\tQWidget *parent,\n\t\tFn<void(object_ptr<BoxContent>)> showBox,\n\t\tconst QString &defaultCountry,\n\t\tconst QString &label,\n\t\tint maxLabelWidth,\n\t\tconst QString &value);\n\n\trpl::producer<QString> value() const override;\n\tQString valueCurrent() const override;\n\nprivate:\n\tint resizeInner(int left, int top, int width) override;\n\tvoid showInnerError() override;\n\tvoid finishInnerAnimating() override;\n\n\tvoid chooseCountry();\n\tvoid hideCountryError();\n\tvoid toggleError(bool shown);\n\tvoid errorAnimationCallback();\n\n\tQString _defaultCountry;\n\tFn<void(object_ptr<BoxContent>)> _showBox;\n\tobject_ptr<LinkButton> _link;\n\trpl::variable<QString> _value;\n\tbool _errorShown = false;\n\tAnimations::Simple _errorAnimation;\n\n};\n\nclass DateInput final : public MaskedInputField {\npublic:\n\tusing MaskedInputField::MaskedInputField;\n\n\tvoid setMaxValue(int value);\n\n\trpl::producer<> erasePrevious() const;\n\trpl::producer<QChar> putNext() const;\n\nprotected:\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\n\tvoid correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) override;\n\nprivate:\n\tint _maxValue = 0;\n\tint _maxDigits = 0;\n\trpl::event_stream<> _erasePrevious;\n\trpl::event_stream<QChar> _putNext;\n\n};\n\nclass DateRow : public PanelDetailsRow {\npublic:\n\tDateRow(\n\t\tQWidget *parent,\n\t\tconst QString &label,\n\t\tint maxLabelWidth,\n\t\tconst QString &value);\n\n\tbool setFocusFast() override;\n\trpl::producer<QString> value() const override;\n\tQString valueCurrent() const override;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\nprivate:\n\tvoid setInnerFocus();\n\tvoid putNext(const object_ptr<DateInput> &field, QChar ch);\n\tvoid erasePrevious(const object_ptr<DateInput> &field);\n\tint resizeInner(int left, int top, int width) override;\n\tvoid showInnerError() override;\n\tvoid finishInnerAnimating() override;\n\tvoid setErrorShown(bool error);\n\tvoid setFocused(bool focused);\n\tvoid startBorderAnimation();\n\ttemplate <typename Widget>\n\tbool insideSeparator(QPoint position, const Widget &widget) const;\n\n\tint day() const;\n\tint month() const;\n\tint year() const;\n\tint number(const object_ptr<DateInput> &field) const;\n\n\tobject_ptr<DateInput> _day;\n\tobject_ptr<PaddingWrap<FlatLabel>> _separator1;\n\tobject_ptr<DateInput> _month;\n\tobject_ptr<PaddingWrap<FlatLabel>> _separator2;\n\tobject_ptr<DateInput> _year;\n\trpl::variable<QString> _value;\n\n\tstyle::cursor _cursor = style::cur_default;\n\tAnimations::Simple _a_borderShown;\n\tint _borderAnimationStart = 0;\n\tAnimations::Simple _a_borderOpacity;\n\tbool _borderVisible = false;\n\n\tAnimations::Simple _a_error;\n\tbool _error = false;\n\tAnimations::Simple _a_focused;\n\tbool _focused = false;\n\n};\n\nclass GenderRow : public PanelDetailsRow {\npublic:\n\tGenderRow(\n\t\tQWidget *parent,\n\t\tconst QString &label,\n\t\tint maxLabelWidth,\n\t\tconst QString &value);\n\n\trpl::producer<QString> value() const override;\n\tQString valueCurrent() const override;\n\nprivate:\n\tenum class Gender {\n\t\tMale,\n\t\tFemale,\n\t};\n\n\tstatic std::optional<Gender> StringToGender(const QString &value);\n\tstatic QString GenderToString(Gender gender);\n\n\tint resizeInner(int left, int top, int width) override;\n\n\tvoid showInnerError() override;\n\tvoid finishInnerAnimating() override;\n\tvoid toggleError(bool shown);\n\tvoid hideGenderError();\n\tvoid errorAnimationCallback();\n\n\tstd::unique_ptr<AbstractCheckView> createRadioView(\n\t\tRadioView* &weak) const;\n\n\tstd::shared_ptr<RadioenumGroup<Gender>> _group;\n\tRadioView *_maleRadio = nullptr;\n\tRadioView *_femaleRadio = nullptr;\n\tobject_ptr<Radioenum<Gender>> _male;\n\tobject_ptr<Radioenum<Gender>> _female;\n\trpl::variable<QString> _value;\n\n\tbool _errorShown = false;\n\tAnimations::Simple _errorAnimation;\n\n};\n\ntemplate <typename Input>\nAbstractTextRow<Input>::AbstractTextRow(\n\tQWidget *parent,\n\tconst QString &label,\n\tint maxLabelWidth,\n\tconst QString &value,\n\tint limit)\n: PanelDetailsRow(parent, label, maxLabelWidth)\n, _field(this, st::passportDetailsField, nullptr, value)\n, _value(value) {\n\t_field->setMaxLength(limit);\n\tif constexpr (std::is_same<Input, Ui::InputField>::value) {\n\t\t_field->changes(\n\t\t) | rpl::on_next([=] {\n\t\t\t_value = valueCurrent();\n\t\t}, _field->lifetime());\n\t} else {\n\t\tconnect(_field, &Input::changed, [=] {\n\t\t\t_value = valueCurrent();\n\t\t});\n\t}\n}\n\ntemplate <typename Input>\nbool AbstractTextRow<Input>::setFocusFast() {\n\t_field->setFocusFast();\n\treturn true;\n}\n\ntemplate <typename Input>\nQString AbstractTextRow<Input>::valueCurrent() const {\n\treturn _field->getLastText();\n}\n\ntemplate <typename Input>\nrpl::producer<QString> AbstractTextRow<Input>::value() const {\n\treturn _value.value();\n}\n\ntemplate <typename Input>\nint AbstractTextRow<Input>::resizeInner(int left, int top, int width) {\n\t_field->setGeometry(left, top, width, _field->height());\n\treturn st::semiboldFont->height;\n}\n\ntemplate <typename Input>\nvoid AbstractTextRow<Input>::showInnerError() {\n\t_field->showError();\n}\n\ntemplate <typename Input>\nvoid AbstractTextRow<Input>::finishInnerAnimating() {\n\t_field->finishAnimating();\n}\n\nQString CountryString(const QString &code) {\n\tconst auto name = Countries::Instance().countryNameByISO2(code);\n\treturn name.isEmpty() ? tr::lng_passport_country_choose(tr::now) : name;\n}\n\nCountryRow::CountryRow(\n\tQWidget *parent,\n\tFn<void(object_ptr<BoxContent>)> showBox,\n\tconst QString &defaultCountry,\n\tconst QString &label,\n\tint maxLabelWidth,\n\tconst QString &value)\n: PanelDetailsRow(parent, label, maxLabelWidth)\n, _defaultCountry(defaultCountry)\n, _showBox(std::move(showBox))\n, _link(this, CountryString(value), st::boxLinkButton)\n, _value(value) {\n\t_value.changes(\n\t) | rpl::on_next([=] {\n\t\thideCountryError();\n\t}, lifetime());\n\n\t_link->addClickHandler([=] {\n\t\tchooseCountry();\n\t});\n}\n\nQString CountryRow::valueCurrent() const {\n\treturn _value.current();\n}\n\nrpl::producer<QString> CountryRow::value() const {\n\treturn _value.value();\n}\n\nint CountryRow::resizeInner(int left, int top, int width) {\n\t_link->move(left, st::passportDetailsField.textMargins.top() + top);\n\treturn st::semiboldFont->height;\n}\n\nvoid CountryRow::showInnerError() {\n\ttoggleError(true);\n}\n\nvoid CountryRow::finishInnerAnimating() {\n\tif (_errorAnimation.animating()) {\n\t\t_errorAnimation.stop();\n\t\terrorAnimationCallback();\n\t}\n}\n\nvoid CountryRow::hideCountryError() {\n\ttoggleError(false);\n}\n\nvoid CountryRow::toggleError(bool shown) {\n\tif (_errorShown != shown) {\n\t\t_errorShown = shown;\n\t\t_errorAnimation.start(\n\t\t\t[=] { errorAnimationCallback(); },\n\t\t\t_errorShown ? 0. : 1.,\n\t\t\t_errorShown ? 1. : 0.,\n\t\t\tst::passportDetailsField.duration);\n\t}\n}\n\nvoid CountryRow::errorAnimationCallback() {\n\tconst auto error = _errorAnimation.value(_errorShown ? 1. : 0.);\n\tif (error == 0.) {\n\t\t_link->setColorOverride(std::nullopt);\n\t} else {\n\t\t_link->setColorOverride(anim::color(\n\t\t\tst::boxLinkButton.color,\n\t\t\tst::boxTextFgError,\n\t\t\terror));\n\t}\n}\n\nvoid CountryRow::chooseCountry() {\n\tconst auto top = _value.current();\n\tconst auto name = Countries::Instance().countryNameByISO2(top);\n\tconst auto country = !name.isEmpty()\n\t\t? top\n\t\t: !_defaultCountry.isEmpty()\n\t\t? _defaultCountry\n\t\t: Platform::SystemCountry();\n\tauto box = Box<CountrySelectBox>(\n\t\tcountry,\n\t\tCountrySelectBox::Type::Countries);\n\tconst auto raw = box.data();\n\traw->countryChosen(\n\t) | rpl::on_next([=](QString iso) {\n\t\t_value = iso;\n\t\t_link->setText(CountryString(iso));\n\t\thideCountryError();\n\t\traw->closeBox();\n\t}, lifetime());\n\t_showBox(std::move(box));\n}\n\nQDate ValidateDate(const QString &value) {\n\tstatic const auto RegExp = QRegularExpression(\n\t\t\"^([0-9]{2})\\\\.([0-9]{2})\\\\.([0-9]{4})$\");\n\tconst auto match = RegExp.match(value);\n\tif (!match.hasMatch()) {\n\t\treturn QDate();\n\t}\n\tauto result = QDate();\n\tconst auto readInt = [](const QString &value) {\n\t\tauto view = QStringView(value);\n\t\twhile (!view.isEmpty() && view.at(0) == '0') {\n\t\t\tview = base::StringViewMid(view, 1);\n\t\t}\n\t\treturn view.toInt();\n\t};\n\tresult.setDate(\n\t\treadInt(match.captured(3)),\n\t\treadInt(match.captured(2)),\n\t\treadInt(match.captured(1)));\n\treturn result;\n}\n\nQString GetDay(const QString &value) {\n\tif (const auto date = ValidateDate(value); date.isValid()) {\n\t\treturn QString(\"%1\").arg(date.day(), 2, 10, QChar('0'));\n\t}\n\treturn QString();\n}\n\nQString GetMonth(const QString &value) {\n\tif (const auto date = ValidateDate(value); date.isValid()) {\n\t\treturn QString(\"%1\").arg(date.month(), 2, 10, QChar('0'));\n\t}\n\treturn QString();\n}\n\nQString GetYear(const QString &value) {\n\tif (const auto date = ValidateDate(value); date.isValid()) {\n\t\treturn QString(\"%1\").arg(date.year(), 4, 10, QChar('0'));\n\t}\n\treturn QString();\n}\n\nvoid DateInput::setMaxValue(int value) {\n\t_maxValue = value;\n\t_maxDigits = 0;\n\twhile (value > 0) {\n\t\t++_maxDigits;\n\t\tvalue /= 10;\n\t}\n}\n\nrpl::producer<> DateInput::erasePrevious() const {\n\treturn _erasePrevious.events();\n}\n\nrpl::producer<QChar> DateInput::putNext() const {\n\treturn _putNext.events();\n}\n\nvoid DateInput::keyPressEvent(QKeyEvent *e) {\n\tconst auto isBackspace = (e->key() == Qt::Key_Backspace);\n\tconst auto isBeginning = (cursorPosition() == 0);\n\tif (isBackspace && isBeginning && !hasSelectedText()) {\n\t\t_erasePrevious.fire({});\n\t} else {\n\t\tMaskedInputField::keyPressEvent(e);\n\t}\n}\n\nvoid DateInput::correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) {\n\tauto newText = QString();\n\tauto newCursor = -1;\n\tconst auto oldCursor = nowCursor;\n\tconst auto oldLength = now.size();\n\tauto accumulated = 0;\n\tauto limit = 0;\n\tfor (; limit != oldLength; ++limit) {\n\t\tif (now[limit].isDigit()) {\n\t\t\taccumulated *= 10;\n\t\t\taccumulated += (now[limit].unicode() - '0');\n\t\t\tif (accumulated > _maxValue || limit == _maxDigits) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\tfor (auto i = 0; i != limit;) {\n\t\tif (now[i].isDigit()) {\n\t\t\tnewText += now[i];\n\t\t}\n\t\tif (++i == oldCursor) {\n\t\t\tnewCursor = newText.size();\n\t\t}\n\t}\n\tif (newCursor < 0) {\n\t\tnewCursor = newText.size();\n\t}\n\tif (newText != now) {\n\t\tnow = newText;\n\t\tsetText(now);\n\t\tstartPlaceholderAnimation();\n\t}\n\tif (newCursor != nowCursor) {\n\t\tnowCursor = newCursor;\n\t\tsetCursorPosition(nowCursor);\n\t}\n\tif (accumulated > _maxValue\n\t\t|| (limit == _maxDigits && oldLength > _maxDigits)) {\n\t\tif (oldCursor > limit) {\n\t\t\t_putNext.fire(QChar('0' + (accumulated % 10)));\n\t\t} else {\n\t\t\t_putNext.fire(QChar(0));\n\t\t}\n\t}\n}\n\nDateRow::DateRow(\n\tQWidget *parent,\n\tconst QString &label,\n\tint maxLabelWidth,\n\tconst QString &value)\n: PanelDetailsRow(parent, label, maxLabelWidth)\n, _day(\n\tthis,\n\tst::passportDetailsDateField,\n\ttr::lng_date_input_day(),\n\tGetDay(value))\n, _separator1(\n\tthis,\n\tobject_ptr<FlatLabel>(\n\t\tthis,\n\t\tQString(\" / \"),\n\t\tst::passportDetailsSeparator),\n\tst::passportDetailsSeparatorPadding)\n, _month(\n\tthis,\n\tst::passportDetailsDateField,\n\ttr::lng_date_input_month(),\n\tGetMonth(value))\n, _separator2(\n\tthis,\n\tobject_ptr<FlatLabel>(\n\t\tthis,\n\t\tQString(\" / \"),\n\t\tst::passportDetailsSeparator),\n\tst::passportDetailsSeparatorPadding)\n, _year(\n\tthis,\n\tst::passportDetailsDateField,\n\ttr::lng_date_input_year(),\n\tGetYear(value))\n, _value(valueCurrent()) {\n\tconst auto focused = [=](const object_ptr<DateInput> &field) {\n\t\treturn [this, pointer = base::make_weak(field.data())]{\n\t\t\t_borderAnimationStart = pointer->borderAnimationStart()\n\t\t\t\t+ pointer->x()\n\t\t\t\t- _day->x();\n\t\t\tsetFocused(true);\n\t\t};\n\t};\n\tconst auto blurred = [=] {\n\t\tsetFocused(false);\n\t};\n\tconst auto changed = [=] {\n\t\t_value = valueCurrent();\n\t};\n\tconnect(_day, &MaskedInputField::focused, focused(_day));\n\tconnect(_month, &MaskedInputField::focused, focused(_month));\n\tconnect(_year, &MaskedInputField::focused, focused(_year));\n\tconnect(_day, &MaskedInputField::blurred, blurred);\n\tconnect(_month, &MaskedInputField::blurred, blurred);\n\tconnect(_year, &MaskedInputField::blurred, blurred);\n\tconnect(_day, &MaskedInputField::changed, changed);\n\tconnect(_month, &MaskedInputField::changed, changed);\n\tconnect(_year, &MaskedInputField::changed, changed);\n\t_day->setMaxValue(31);\n\t_day->putNext() | rpl::on_next([=](QChar ch) {\n\t\tputNext(_month, ch);\n\t}, lifetime());\n\t_month->setMaxValue(12);\n\t_month->putNext() | rpl::on_next([=](QChar ch) {\n\t\tputNext(_year, ch);\n\t}, lifetime());\n\t_month->erasePrevious() | rpl::on_next([=] {\n\t\terasePrevious(_day);\n\t}, lifetime());\n\t_year->setMaxValue(2999);\n\t_year->erasePrevious() | rpl::on_next([=] {\n\t\terasePrevious(_month);\n\t}, lifetime());\n\t_separator1->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_separator2->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tsetMouseTracking(true);\n\n\t_value.changes(\n\t) | rpl::on_next([=] {\n\t\tsetErrorShown(false);\n\t}, lifetime());\n}\n\nvoid DateRow::putNext(const object_ptr<DateInput> &field, QChar ch) {\n\tfield->setCursorPosition(0);\n\tif (ch.unicode()) {\n\t\tfield->setText(ch + field->getLastText());\n\t\tfield->setCursorPosition(1);\n\t}\n\tfield->setFocus();\n}\n\nvoid DateRow::erasePrevious(const object_ptr<DateInput> &field) {\n\tconst auto text = field->getLastText();\n\tif (!text.isEmpty()) {\n\t\tfield->setCursorPosition(text.size() - 1);\n\t\tfield->setText(text.mid(0, text.size() - 1));\n\t}\n\tfield->setFocus();\n}\n\nbool DateRow::setFocusFast() {\n\tif (day()) {\n\t\tif (month()) {\n\t\t\t_year->setFocusFast();\n\t\t} else {\n\t\t\t_month->setFocusFast();\n\t\t}\n\t} else {\n\t\t_day->setFocusFast();\n\t}\n\treturn true;\n}\n\nint DateRow::number(const object_ptr<DateInput> &field) const {\n\tconst auto text = field->getLastText();\n\tauto view = QStringView(text);\n\twhile (!view.isEmpty() && view.at(0) == '0') {\n\t\tview = base::StringViewMid(view, 1);\n\t}\n\treturn view.toInt();\n}\n\nint DateRow::day() const {\n\treturn number(_day);\n}\n\nint DateRow::month() const {\n\treturn number(_month);\n}\n\nint DateRow::year() const {\n\treturn number(_year);\n}\n\nQString DateRow::valueCurrent() const {\n\tconst auto result = QString(\"%1.%2.%3\"\n\t\t).arg(day(), 2, 10, QChar('0')\n\t\t).arg(month(), 2, 10, QChar('0')\n\t\t).arg(year(), 4, 10, QChar('0'));\n\treturn ValidateDate(result).isValid() ? result : QString();\n}\n\nrpl::producer<QString> DateRow::value() const {\n\treturn _value.value();\n}\n\nvoid DateRow::paintEvent(QPaintEvent *e) {\n\tPanelDetailsRow::paintEvent(e);\n\n\tPainter p(this);\n\n\tconst auto &_st = st::passportDetailsField;\n\tconst auto height = _st.heightMin;\n\tconst auto width = _year->x() + _year->width() - _day->x();\n\tp.translate(_day->x(), _day->y());\n\tif (_st.border) {\n\t\tp.fillRect(0, height - _st.border, width, _st.border, _st.borderFg);\n\t}\n\tauto errorDegree = _a_error.value(_error ? 1. : 0.);\n\tauto borderShownDegree = _a_borderShown.value(1.);\n\tauto borderOpacity = _a_borderOpacity.value(_borderVisible ? 1. : 0.);\n\tif (_st.borderActive && (borderOpacity > 0.)) {\n\t\tauto borderStart = std::clamp(_borderAnimationStart, 0, width);\n\t\tauto borderFrom = qRound(borderStart * (1. - borderShownDegree));\n\t\tauto borderTo = borderStart + qRound((width - borderStart) * borderShownDegree);\n\t\tif (borderTo > borderFrom) {\n\t\t\tauto borderFg = anim::brush(_st.borderFgActive, _st.borderFgError, errorDegree);\n\t\t\tp.setOpacity(borderOpacity);\n\t\t\tp.fillRect(borderFrom, height - _st.borderActive, borderTo - borderFrom, _st.borderActive, borderFg);\n\t\t\tp.setOpacity(1);\n\t\t}\n\t}\n}\n\ntemplate <typename Widget>\nbool DateRow::insideSeparator(QPoint position, const Widget &widget) const {\n\tconst auto x = position.x();\n\tconst auto y = position.y();\n\treturn (x >= widget->x() && x < widget->x() + widget->width())\n\t\t&& (y >= _day->y() && y < _day->y() + _day->height());\n}\n\nvoid DateRow::mouseMoveEvent(QMouseEvent *e) {\n\tconst auto cursor = (insideSeparator(e->pos(), _separator1)\n\t\t|| insideSeparator(e->pos(), _separator2))\n\t\t? style::cur_text\n\t\t: style::cur_default;\n\tif (_cursor != cursor) {\n\t\t_cursor = cursor;\n\t\tsetCursor(_cursor);\n\t}\n}\n\nvoid DateRow::mousePressEvent(QMouseEvent *e) {\n\tconst auto x = e->pos().x();\n\tconst auto focus1 = [&] {\n\t\tif (_day->getLastText().size() > 1) {\n\t\t\t_month->setFocus();\n\t\t} else {\n\t\t\t_day->setFocus();\n\t\t}\n\t};\n\tif (insideSeparator(e->pos(), _separator1)) {\n\t\tfocus1();\n\t\t_borderAnimationStart = x - _day->x();\n\t} else if (insideSeparator(e->pos(), _separator2)) {\n\t\tif (_month->getLastText().size() > 1) {\n\t\t\t_year->setFocus();\n\t\t} else {\n\t\t\tfocus1();\n\t\t}\n\t\t_borderAnimationStart = x - _day->x();\n\t}\n}\n\nint DateRow::resizeInner(int left, int top, int width) {\n\tconst auto right = left + width;\n\tconst auto &_st = st::passportDetailsDateField;\n\tconst auto &font = _st.placeholderFont;\n\tconst auto addToWidth = st::passportDetailsSeparatorPadding.left();\n\tconst auto dayWidth = _st.textMargins.left()\n\t\t+ _st.placeholderMargins.left()\n\t\t+ font->width(tr::lng_date_input_day(tr::now))\n\t\t+ _st.placeholderMargins.right()\n\t\t+ _st.textMargins.right()\n\t\t+ addToWidth;\n\tconst auto monthWidth = _st.textMargins.left()\n\t\t+ _st.placeholderMargins.left()\n\t\t+ font->width(tr::lng_date_input_month(tr::now))\n\t\t+ _st.placeholderMargins.right()\n\t\t+ _st.textMargins.right()\n\t\t+ addToWidth;\n\t_day->setGeometry(left, top, dayWidth, _day->height());\n\tleft += dayWidth - addToWidth;\n\t_separator1->resizeToNaturalWidth(width);\n\t_separator1->move(left, top);\n\tleft += _separator1->width();\n\t_month->setGeometry(left, top, monthWidth, _month->height());\n\tleft += monthWidth - addToWidth;\n\t_separator2->resizeToNaturalWidth(width);\n\t_separator2->move(left, top);\n\tleft += _separator2->width();\n\t_year->setGeometry(left, top, right - left, _year->height());\n\treturn st::semiboldFont->height;\n}\n\nvoid DateRow::showInnerError() {\n\tsetErrorShown(true);\n\tif (_year->getLastText().size() == 2) {\n\t\t// We don't support year 95 for 1995 or 03 for 2003.\n\t\t// Let's give a hint to our user what is wrong.\n\t\t_year->setFocus();\n\t\t_year->selectAll();\n\t} else if (!_focused) {\n\t\tsetInnerFocus();\n\t}\n}\n\nvoid DateRow::setInnerFocus() {\n\tif (day()) {\n\t\tif (month()) {\n\t\t\t_year->setFocus();\n\t\t} else {\n\t\t\t_month->setFocus();\n\t\t}\n\t} else {\n\t\t_day->setFocus();\n\t}\n}\n\nvoid DateRow::setErrorShown(bool error) {\n\tif (_error != error) {\n\t\t_error = error;\n\t\t_a_error.start(\n\t\t\t[=] { update(); },\n\t\t\t_error ? 0. : 1.,\n\t\t\t_error ? 1. : 0.,\n\t\t\tst::passportDetailsField.duration);\n\t\tstartBorderAnimation();\n\t}\n}\n\nvoid DateRow::setFocused(bool focused) {\n\tif (_focused != focused) {\n\t\t_focused = focused;\n\t\t_a_focused.start(\n\t\t\t[=] { update(); },\n\t\t\t_focused ? 0. : 1.,\n\t\t\t_focused ? 1. : 0.,\n\t\t\tst::passportDetailsField.duration);\n\t\tstartBorderAnimation();\n\t}\n}\n\nvoid DateRow::finishInnerAnimating() {\n\t_day->finishAnimating();\n\t_month->finishAnimating();\n\t_year->finishAnimating();\n\t_a_borderOpacity.stop();\n\t_a_borderShown.stop();\n\t_a_error.stop();\n}\n\nvoid DateRow::startBorderAnimation() {\n\tauto borderVisible = (_error || _focused);\n\tif (_borderVisible != borderVisible) {\n\t\t_borderVisible = borderVisible;\n\t\tconst auto duration = st::passportDetailsField.duration;\n\t\tif (_borderVisible) {\n\t\t\tif (_a_borderOpacity.animating()) {\n\t\t\t\t_a_borderOpacity.start([=] { update(); }, 0., 1., duration);\n\t\t\t} else {\n\t\t\t\t_a_borderShown.start([=] { update(); }, 0., 1., duration);\n\t\t\t}\n\t\t} else {\n\t\t\t_a_borderOpacity.start([=] { update(); }, 1., 0., duration);\n\t\t}\n\t}\n}\n\nGenderRow::GenderRow(\n\tQWidget *parent,\n\tconst QString &label,\n\tint maxLabelWidth,\n\tconst QString &value)\n: PanelDetailsRow(parent, label, maxLabelWidth)\n, _group(StringToGender(value).has_value()\n\t? std::make_shared<RadioenumGroup<Gender>>(*StringToGender(value))\n\t: std::make_shared<RadioenumGroup<Gender>>())\n, _male(\n\tthis,\n\t_group,\n\tGender::Male,\n\ttr::lng_passport_gender_male(tr::now),\n\tst::defaultCheckbox,\n\tcreateRadioView(_maleRadio))\n, _female(\n\tthis,\n\t_group,\n\tGender::Female,\n\ttr::lng_passport_gender_female(tr::now),\n\tst::defaultCheckbox,\n\tcreateRadioView(_femaleRadio))\n, _value(StringToGender(value) ? value : QString()) {\n\t_group->setChangedCallback([=](Gender gender) {\n\t\t_value = GenderToString(gender);\n\t\thideGenderError();\n\t});\n}\n\nstd::unique_ptr<AbstractCheckView> GenderRow::createRadioView(\n\t\tRadioView* &weak) const {\n\tauto result = std::make_unique<RadioView>(st::defaultRadio, false);\n\tweak = result.get();\n\treturn result;\n}\n\nauto GenderRow::StringToGender(const QString &value)\n-> std::optional<Gender> {\n\tif (value == u\"male\"_q) {\n\t\treturn Gender::Male;\n\t} else if (value == u\"female\"_q) {\n\t\treturn Gender::Female;\n\t}\n\treturn std::nullopt;\n}\n\nQString GenderRow::GenderToString(Gender gender) {\n\treturn (gender == Gender::Male) ? \"male\" : \"female\";\n}\n\nQString GenderRow::valueCurrent() const {\n\treturn _value.current();\n}\n\nrpl::producer<QString> GenderRow::value() const {\n\treturn _value.value();\n}\n\nint GenderRow::resizeInner(int left, int top, int width) {\n\ttop += st::passportDetailsField.textMargins.top();\n\ttop -= st::defaultCheckbox.textPosition.y();\n\t_male->moveToLeft(left, top);\n\tleft += _male->checkRect().width()\n\t\t+ st::defaultCheckbox.style.font->width(\n\t\t\ttr::lng_passport_gender_male(tr::now))\n\t\t+ st::passportDetailsGenderSkip;\n\t_female->moveToLeft(left, top);\n\treturn st::semiboldFont->height;\n}\n\nvoid GenderRow::showInnerError() {\n\ttoggleError(true);\n}\n\nvoid GenderRow::finishInnerAnimating() {\n\tif (_errorAnimation.animating()) {\n\t\t_errorAnimation.stop();\n\t\terrorAnimationCallback();\n\t}\n}\n\nvoid GenderRow::hideGenderError() {\n\ttoggleError(false);\n}\n\nvoid GenderRow::toggleError(bool shown) {\n\tif (_errorShown != shown) {\n\t\t_errorShown = shown;\n\t\t_errorAnimation.start(\n\t\t\t[=] { errorAnimationCallback(); },\n\t\t\t_errorShown ? 0. : 1.,\n\t\t\t_errorShown ? 1. : 0.,\n\t\t\tst::passportDetailsField.duration);\n\t}\n}\n\nvoid GenderRow::errorAnimationCallback() {\n\tconst auto error = _errorAnimation.value(_errorShown ? 1. : 0.);\n\tif (error == 0.) {\n\t\t_maleRadio->setUntoggledOverride(std::nullopt);\n\t\t_femaleRadio->setUntoggledOverride(std::nullopt);\n\t} else {\n\t\tconst auto color = anim::color(\n\t\t\tst::defaultRadio.untoggledFg,\n\t\t\tst::boxTextFgError,\n\t\t\terror);\n\t\t_maleRadio->setUntoggledOverride(color);\n\t\t_femaleRadio->setUntoggledOverride(color);\n\t}\n}\n\n} // namespace\n\nPanelDetailsRow::PanelDetailsRow(\n\tQWidget *parent,\n\tconst QString &label,\n\tint maxLabelWidth)\n: _label(label)\n, _maxLabelWidth(maxLabelWidth) {\n}\n\nobject_ptr<PanelDetailsRow> PanelDetailsRow::Create(\n\t\tQWidget *parent,\n\t\tFn<void(object_ptr<BoxContent>)> showBox,\n\t\tconst QString &defaultCountry,\n\t\tType type,\n\t\tconst QString &label,\n\t\tint maxLabelWidth,\n\t\tconst QString &value,\n\t\tconst QString &error,\n\t\tint limit) {\n\tauto result = [&]() -> object_ptr<PanelDetailsRow> {\n\t\tswitch (type) {\n\t\tcase Type::Text:\n\t\t\treturn object_ptr<AbstractTextRow<InputField>>(\n\t\t\t\tparent,\n\t\t\t\tlabel,\n\t\t\t\tmaxLabelWidth,\n\t\t\t\tvalue,\n\t\t\t\tlimit);\n\t\tcase Type::Postcode:\n\t\t\treturn object_ptr<AbstractTextRow<PostcodeInput>>(\n\t\t\t\tparent,\n\t\t\t\tlabel,\n\t\t\t\tmaxLabelWidth,\n\t\t\t\tvalue,\n\t\t\t\tlimit);\n\t\tcase Type::Country:\n\t\t\treturn object_ptr<CountryRow>(\n\t\t\t\tparent,\n\t\t\t\tshowBox,\n\t\t\t\tdefaultCountry,\n\t\t\t\tlabel,\n\t\t\t\tmaxLabelWidth,\n\t\t\t\tvalue);\n\t\tcase Type::Gender:\n\t\t\treturn object_ptr<GenderRow>(\n\t\t\t\tparent,\n\t\t\t\tlabel,\n\t\t\t\tmaxLabelWidth,\n\t\t\t\tvalue);\n\t\tcase Type::Date:\n\t\t\treturn object_ptr<DateRow>(\n\t\t\t\tparent,\n\t\t\t\tlabel,\n\t\t\t\tmaxLabelWidth,\n\t\t\t\tvalue);\n\t\tdefault:\n\t\t\tUnexpected(\"Type in PanelDetailsRow::Create.\");\n\t\t}\n\t}();\n\tif (!error.isEmpty()) {\n\t\tresult->showError(error);\n\t\tresult->finishAnimating();\n\t}\n\treturn result;\n}\n\nint PanelDetailsRow::LabelWidth(const QString &label) {\n\treturn st::semiboldFont->width(label);\n}\n\nbool PanelDetailsRow::setFocusFast() {\n\treturn false;\n}\n\nint PanelDetailsRow::resizeGetHeight(int newWidth) {\n\tconst auto padding = st::passportDetailsPadding;\n\tconst auto inputLeft = padding.left() + std::max(\n\t\tst::passportDetailsFieldLeft,\n\t\t_maxLabelWidth + st::passportDetailsFieldSkipMin);\n\tconst auto inputTop = st::passportDetailsFieldTop;\n\tconst auto inputRight = padding.right();\n\tconst auto inputWidth = std::max(newWidth - inputLeft - inputRight, 0);\n\tconst auto innerHeight = resizeInner(inputLeft, inputTop, inputWidth);\n\tconst auto result = padding.top()\n\t\t+ innerHeight\n\t\t+ (_error ? _error->height() : 0)\n\t\t+ padding.bottom();\n\tif (_error) {\n\t\t_error->resizeToWidth(inputWidth);\n\t\t_error->moveToLeft(inputLeft, result - _error->height());\n\t}\n\treturn result;\n}\n\nvoid PanelDetailsRow::showError(std::optional<QString> error) {\n\tif (!_errorHideSubscription) {\n\t\t_errorHideSubscription = true;\n\n\t\tvalue(\n\t\t) | rpl::on_next([=] {\n\t\t\thideError();\n\t\t}, lifetime());\n\t}\n\tshowInnerError();\n\tstartErrorAnimation(true);\n\tif (!error.has_value()) {\n\t\treturn;\n\t}\n\tif (error->isEmpty()) {\n\t\tif (_error) {\n\t\t\t_error->hide(anim::type::normal);\n\t\t}\n\t} else {\n\t\tif (!_error) {\n\t\t\t_error.create(\n\t\t\t\tthis,\n\t\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\t\tthis,\n\t\t\t\t\t*error,\n\t\t\t\t\tst::passportVerifyErrorLabel));\n\t\t} else {\n\t\t\t_error->entity()->setText(*error);\n\t\t}\n\t\t_error->heightValue(\n\t\t) | rpl::on_next([=] {\n\t\t\tresizeToWidth(width());\n\t\t}, _error->lifetime());\n\t\t_error->show(anim::type::normal);\n\t}\n}\n\nbool PanelDetailsRow::errorShown() const {\n\treturn _errorShown;\n}\n\nvoid PanelDetailsRow::hideError() {\n\tstartErrorAnimation(false);\n\tif (_error) {\n\t\t_error->hide(anim::type::normal);\n\t}\n}\n\nvoid PanelDetailsRow::startErrorAnimation(bool shown) {\n\tif (_errorShown != shown) {\n\t\t_errorShown = shown;\n\t\t_errorAnimation.start(\n\t\t\t[=] { update(); },\n\t\t\t_errorShown ? 0. : 1.,\n\t\t\t_errorShown ? 1. : 0.,\n\t\t\tst::passportDetailsField.duration);\n\t}\n}\n\nvoid PanelDetailsRow::finishAnimating() {\n\tif (_error) {\n\t\t_error->finishAnimating();\n\t}\n\tif (_errorAnimation.animating()) {\n\t\t_errorAnimation.stop();\n\t\tupdate();\n\t}\n}\n\nvoid PanelDetailsRow::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tconst auto error = _errorAnimation.value(_errorShown ? 1. : 0.);\n\tp.setFont(st::semiboldFont);\n\tp.setPen(anim::pen(\n\t\tst::passportDetailsField.placeholderFg,\n\t\tst::passportDetailsField.placeholderFgError,\n\t\terror));\n\tconst auto padding = st::passportDetailsPadding;\n\tp.drawTextLeft(padding.left(), padding.top(), width(), _label);\n}\n\n} // namespace Passport::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/ui/passport_details_row.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/widgets/labels.h\"\n\nnamespace Ui {\nclass BoxContent;\nclass InputField;\nclass FlatLabel;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Passport::Ui {\n\nusing namespace ::Ui;\n\nenum class PanelDetailsType {\n\tText,\n\tPostcode,\n\tCountry,\n\tDate,\n\tGender,\n};\n\nclass PanelDetailsRow : public RpWidget {\npublic:\n\tusing Type = PanelDetailsType;\n\n\tPanelDetailsRow(\n\t\tQWidget *parent,\n\t\tconst QString &label,\n\t\tint maxLabelWidth);\n\n\tstatic object_ptr<PanelDetailsRow> Create(\n\t\tQWidget *parent,\n\t\tFn<void(object_ptr<BoxContent>)> showBox,\n\t\tconst QString &defaultCountry,\n\t\tType type,\n\t\tconst QString &label,\n\t\tint maxLabelWidth,\n\t\tconst QString &value,\n\t\tconst QString &error,\n\t\tint limit = 0);\n\tstatic int LabelWidth(const QString &label);\n\n\tvirtual bool setFocusFast();\n\tvirtual rpl::producer<QString> value() const = 0;\n\tvirtual QString valueCurrent() const = 0;\n\tvoid showError(std::optional<QString> error = std::nullopt);\n\tbool errorShown() const;\n\tvoid hideError();\n\tvoid finishAnimating();\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvirtual int resizeInner(int left, int top, int width) = 0;\n\tvirtual void showInnerError() = 0;\n\tvirtual void finishInnerAnimating() = 0;\n\n\tvoid startErrorAnimation(bool shown);\n\n\tQString _label;\n\tint _maxLabelWidth = 0;\n\tobject_ptr<SlideWrap<FlatLabel>> _error = { nullptr };\n\tbool _errorShown = false;\n\tbool _errorHideSubscription = false;\n\tAnimations::Simple _errorAnimation;\n\n};\n\n} // namespace Passport::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/ui/passport_form_row.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"passport/ui/passport_form_row.h\"\n\n#include \"ui/text/text_options.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_passport.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Passport::Ui {\n\nFormRow::FormRow(QWidget *parent)\n: RippleButton(parent, st::passportRowRipple)\n, _title(st::boxWideWidth / 2)\n, _description(st::boxWideWidth / 2) {\n}\n\nvoid FormRow::updateContent(\n\t\tconst QString &title,\n\t\tconst QString &description,\n\t\tbool ready,\n\t\tbool error,\n\t\tanim::type animated) {\n\t_title.setText(\n\t\tst::semiboldTextStyle,\n\t\ttitle,\n\t\tNameTextOptions());\n\t_description.setText(\n\t\tst::defaultTextStyle,\n\t\tdescription,\n\t\tTextParseOptions {\n\t\t\tTextParseMultiline,\n\t\t\t0,\n\t\t\t0,\n\t\t\tQt::LayoutDirectionAuto\n\t\t});\n\t_ready = ready && !error;\n\tif (_error != error) {\n\t\t_error = error;\n\t\tif (animated == anim::type::instant) {\n\t\t\t_errorAnimation.stop();\n\t\t} else {\n\t\t\t_errorAnimation.start(\n\t\t\t\t[=] { update(); },\n\t\t\t\t_error ? 0. : 1.,\n\t\t\t\t_error ? 1. : 0.,\n\t\t\t\tst::fadeWrapDuration);\n\t\t}\n\t}\n\tresizeToWidth(width());\n\tupdate();\n}\n\nint FormRow::resizeGetHeight(int newWidth) {\n\tconst auto availableWidth = countAvailableWidth(newWidth);\n\t_titleHeight = _title.countHeight(availableWidth);\n\t_descriptionHeight = _description.countHeight(availableWidth);\n\tconst auto result = st::passportRowPadding.top()\n\t\t+ _titleHeight\n\t\t+ st::passportRowSkip\n\t\t+ _descriptionHeight\n\t\t+ st::passportRowPadding.bottom();\n\treturn result;\n}\n\nint FormRow::countAvailableWidth(int newWidth) const {\n\treturn newWidth\n\t\t- st::passportRowPadding.left()\n\t\t- st::passportRowPadding.right()\n\t\t- (_ready\n\t\t\t? st::passportRowReadyIcon\n\t\t\t: st::passportRowEmptyIcon).width()\n\t\t- st::passportRowIconSkip;\n}\n\nint FormRow::countAvailableWidth() const {\n\treturn countAvailableWidth(width());\n}\n\nvoid FormRow::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tpaintRipple(p, 0, 0);\n\n\tconst auto left = st::passportRowPadding.left();\n\tconst auto availableWidth = countAvailableWidth();\n\tauto top = st::passportRowPadding.top();\n\n\tconst auto error = _errorAnimation.value(_error ? 1. : 0.);\n\n\tp.setPen(st::passportRowTitleFg);\n\t_title.drawLeft(p, left, top, availableWidth, width());\n\ttop += _titleHeight + st::passportRowSkip;\n\n\tp.setPen(anim::pen(\n\t\tst::passportRowDescriptionFg,\n\t\tst::boxTextFgError,\n\t\terror));\n\t_description.drawLeft(p, left, top, availableWidth, width());\n\ttop += _descriptionHeight + st::passportRowPadding.bottom();\n\n\tconst auto &icon = _ready\n\t\t? st::passportRowReadyIcon\n\t\t: st::passportRowEmptyIcon;\n\tif (error > 0. && !_ready) {\n\t\ticon.paint(\n\t\t\tp,\n\t\t\twidth() - st::passportRowPadding.right() - icon.width(),\n\t\t\t(height() - icon.height()) / 2,\n\t\t\twidth(),\n\t\t\tanim::color(st::menuIconFgOver, st::boxTextFgError, error));\n\t} else {\n\t\ticon.paint(\n\t\t\tp,\n\t\t\twidth() - st::passportRowPadding.right() - icon.width(),\n\t\t\t(height() - icon.height()) / 2,\n\t\t\twidth());\n\t}\n}\n\n} // namespace Passport::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/passport/ui/passport_form_row.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/text/text.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/widgets/buttons.h\"\n\nnamespace Passport::Ui {\n\nusing namespace ::Ui;\n\nclass FormRow : public RippleButton {\npublic:\n\texplicit FormRow(QWidget *parent);\n\n\tvoid updateContent(\n\t\tconst QString &title,\n\t\tconst QString &description,\n\t\tbool ready,\n\t\tbool error,\n\t\tanim::type animated);\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tint countAvailableWidth() const;\n\tint countAvailableWidth(int newWidth) const;\n\n\tText::String _title;\n\tText::String _description;\n\tint _titleHeight = 0;\n\tint _descriptionHeight = 0;\n\tbool _ready = false;\n\tbool _error = false;\n\tAnimations::Simple _errorAnimation;\n\n};\n\n} // namespace Passport::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/payments_checkout_process.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"payments/payments_checkout_process.h\"\n\n#include \"payments/payments_form.h\"\n#include \"payments/ui/payments_panel.h\"\n#include \"main/main_session.h\"\n#include \"main/main_account.h\"\n#include \"storage/storage_account.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"data/data_user.h\" // UserData::isBot.\n#include \"boxes/passcode_box.h\"\n#include \"core/local_url_handlers.h\" // TryConvertUrlToLocal.\n#include \"core/file_utilities.h\" // File::OpenUrl.\n#include \"core/core_cloud_password.h\" // Core::CloudPasswordState\n#include \"core/click_handler_types.h\"\n#include \"lang/lang_keys.h\"\n#include \"apiwrap.h\"\n#include \"api/api_cloud_password.h\"\n#include \"window/themes/window_theme.h\"\n\n#include <QJsonDocument>\n#include <QJsonObject>\n#include <QJsonArray>\n#include <QJsonValue>\n\nnamespace Payments {\nnamespace {\n\nstruct SessionProcesses {\n\tbase::flat_map<FullMsgId, std::unique_ptr<CheckoutProcess>> byItem;\n\tbase::flat_map<QString, std::unique_ptr<CheckoutProcess>> bySlug;\n\tbase::flat_map<uint64, std::unique_ptr<CheckoutProcess>> byRandomId;\n\tbase::flat_map<FullMsgId, PaidInvoice> paymentStartedByItem;\n\tbase::flat_map<QString, PaidInvoice> paymentStartedBySlug;\n\trpl::lifetime lifetime;\n};\n\nbase::flat_map<not_null<Main::Session*>, SessionProcesses> Processes;\n\n[[nodiscard]] SessionProcesses &LookupSessionProcesses(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto i = Processes.find(session);\n\tif (i != end(Processes)) {\n\t\treturn i->second;\n\t}\n\tconst auto j = Processes.emplace(session).first;\n\tauto &result = j->second;\n\tsession->account().sessionChanges(\n\t) | rpl::on_next([=] {\n\t\tProcesses.erase(session);\n\t}, result.lifetime);\n\treturn result;\n}\n\n} // namespace\n\nvoid CheckoutProcess::Start(\n\t\tnot_null<const HistoryItem*> item,\n\t\tMode mode,\n\t\tFn<void(CheckoutResult)> reactivate,\n\t\tFn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess) {\n\tauto &processes = LookupSessionProcesses(&item->history()->session());\n\tconst auto media = item->media();\n\tconst auto invoice = media ? media->invoice() : nullptr;\n\tif (mode == Mode::Payment && !invoice) {\n\t\treturn;\n\t}\n\tconst auto id = (invoice && invoice->receiptMsgId)\n\t\t? FullMsgId(item->history()->peer->id, invoice->receiptMsgId)\n\t\t: item->fullId();\n\tif (invoice) {\n\t\tmode = invoice->receiptMsgId ? Mode::Receipt : Mode::Payment;\n\t} else if (mode == Mode::Payment) {\n\t\tLOG((\"API Error: CheckoutProcess Payment start without invoice.\"));\n\t\treturn;\n\t}\n\tconst auto i = processes.byItem.find(id);\n\tif (i != end(processes.byItem)) {\n\t\ti->second->setReactivateCallback(std::move(reactivate));\n\t\ti->second->setNonPanelPaymentFormProcess(\n\t\t\tstd::move(nonPanelPaymentFormProcess));\n\t\ti->second->requestActivate();\n\t\treturn;\n\t}\n\tconst auto j = processes.byItem.emplace(\n\t\tid,\n\t\tstd::make_unique<CheckoutProcess>(\n\t\t\tInvoiceId{ InvoiceMessage{ item->history()->peer, id.msg } },\n\t\t\tmode,\n\t\t\tstd::move(reactivate),\n\t\t\tstd::move(nonPanelPaymentFormProcess),\n\t\t\tPrivateTag{})).first;\n\tj->second->requestActivate();\n}\n\nvoid CheckoutProcess::Start(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &slug,\n\t\tFn<void(CheckoutResult)> reactivate,\n\t\tFn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess) {\n\tauto &processes = LookupSessionProcesses(session);\n\tconst auto i = processes.bySlug.find(slug);\n\tif (i != end(processes.bySlug)) {\n\t\ti->second->setReactivateCallback(std::move(reactivate));\n\t\ti->second->setNonPanelPaymentFormProcess(\n\t\t\tstd::move(nonPanelPaymentFormProcess));\n\t\ti->second->requestActivate();\n\t\treturn;\n\t}\n\tconst auto j = processes.bySlug.emplace(\n\t\tslug,\n\t\tstd::make_unique<CheckoutProcess>(\n\t\t\tInvoiceId{ InvoiceSlug{ session, slug } },\n\t\t\tMode::Payment,\n\t\t\tstd::move(reactivate),\n\t\t\tstd::move(nonPanelPaymentFormProcess),\n\t\t\tPrivateTag{})).first;\n\tj->second->requestActivate();\n}\n\nvoid CheckoutProcess::Start(\n\t\tInvoicePremiumGiftCode giftCodeInvoice,\n\t\tFn<void(CheckoutResult)> reactivate,\n\t\tFn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess) {\n\tconst auto randomId = giftCodeInvoice.randomId;\n\tauto id = InvoiceId{ std::move(giftCodeInvoice) };\n\tauto &processes = LookupSessionProcesses(SessionFromId(id));\n\tconst auto i = processes.byRandomId.find(randomId);\n\tif (i != end(processes.byRandomId)) {\n\t\ti->second->setReactivateCallback(std::move(reactivate));\n\t\ti->second->setNonPanelPaymentFormProcess(\n\t\t\tstd::move(nonPanelPaymentFormProcess));\n\t\ti->second->requestActivate();\n\t\treturn;\n\t}\n\tconst auto j = processes.byRandomId.emplace(\n\t\trandomId,\n\t\tstd::make_unique<CheckoutProcess>(\n\t\t\tstd::move(id),\n\t\t\tMode::Payment,\n\t\t\tstd::move(reactivate),\n\t\t\tstd::move(nonPanelPaymentFormProcess),\n\t\t\tPrivateTag{})).first;\n\tj->second->requestActivate();\n}\n\nvoid CheckoutProcess::Start(\n\t\tInvoiceCredits creditsInvoice,\n\t\tFn<void(CheckoutResult)> reactivate) {\n\tconst auto randomId = creditsInvoice.randomId;\n\tauto id = InvoiceId{ std::move(creditsInvoice) };\n\tauto &processes = LookupSessionProcesses(SessionFromId(id));\n\tconst auto i = processes.byRandomId.find(randomId);\n\tif (i != end(processes.byRandomId)) {\n\t\ti->second->setReactivateCallback(std::move(reactivate));\n\t\ti->second->requestActivate();\n\t\treturn;\n\t}\n\tconst auto j = processes.byRandomId.emplace(\n\t\trandomId,\n\t\tstd::make_unique<CheckoutProcess>(\n\t\t\tstd::move(id),\n\t\t\tMode::Payment,\n\t\t\tstd::move(reactivate),\n\t\t\tnullptr,\n\t\t\tPrivateTag{})).first;\n\tj->second->requestActivate();\n}\n\nvoid CheckoutProcess::Start(\n\t\tInvoiceStarGift giftInvoice,\n\t\tFn<void(CheckoutResult)> reactivate,\n\t\tFn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess) {\n\tconst auto randomId = giftInvoice.randomId;\n\tauto id = InvoiceId{ std::move(giftInvoice) };\n\tauto &processes = LookupSessionProcesses(SessionFromId(id));\n\tconst auto i = processes.byRandomId.find(randomId);\n\tif (i != end(processes.byRandomId)) {\n\t\ti->second->setReactivateCallback(std::move(reactivate));\n\t\ti->second->setNonPanelPaymentFormProcess(\n\t\t\tstd::move(nonPanelPaymentFormProcess));\n\t\treturn;\n\t}\n\tprocesses.byRandomId.emplace(\n\t\trandomId,\n\t\tstd::make_unique<CheckoutProcess>(\n\t\t\tstd::move(id),\n\t\t\tMode::Payment,\n\t\t\tstd::move(reactivate),\n\t\t\tstd::move(nonPanelPaymentFormProcess),\n\t\t\tPrivateTag{}));\n}\n\nstd::optional<PaidInvoice> CheckoutProcess::InvoicePaid(\n\t\tnot_null<const HistoryItem*> item) {\n\tconst auto session = &item->history()->session();\n\tconst auto itemId = item->fullId();\n\tconst auto i = Processes.find(session);\n\tif (i == end(Processes)) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto k = i->second.paymentStartedByItem.find(itemId);\n\tif (k == end(i->second.paymentStartedByItem)) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto result = k->second;\n\ti->second.paymentStartedByItem.erase(k);\n\n\tconst auto j = i->second.byItem.find(itemId);\n\tif (j != end(i->second.byItem)) {\n\t\tj->second->closeAndReactivate(CheckoutResult::Paid);\n\t} else if (i->second.paymentStartedByItem.empty()\n\t\t&& i->second.byItem.empty()\n\t\t&& i->second.paymentStartedBySlug.empty()\n\t\t&& i->second.bySlug.empty()\n\t\t&& i->second.byRandomId.empty()) {\n\t\tProcesses.erase(i);\n\t}\n\treturn result;\n}\n\nstd::optional<PaidInvoice> CheckoutProcess::InvoicePaid(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &slug) {\n\tconst auto i = Processes.find(session);\n\tif (i == end(Processes)) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto k = i->second.paymentStartedBySlug.find(slug);\n\tif (k == end(i->second.paymentStartedBySlug)) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto result = k->second;\n\ti->second.paymentStartedBySlug.erase(k);\n\n\tconst auto j = i->second.bySlug.find(slug);\n\tif (j != end(i->second.bySlug)) {\n\t\tj->second->closeAndReactivate(CheckoutResult::Paid);\n\t} else if (i->second.paymentStartedByItem.empty()\n\t\t&& i->second.byItem.empty()\n\t\t&& i->second.paymentStartedBySlug.empty()\n\t\t&& i->second.bySlug.empty()\n\t\t&& i->second.byRandomId.empty()) {\n\t\tProcesses.erase(i);\n\t}\n\treturn result;\n}\n\nvoid CheckoutProcess::ClearAll() {\n\tProcesses.clear();\n}\n\nvoid CheckoutProcess::RegisterPaymentStart(\n\t\tnot_null<CheckoutProcess*> process,\n\t\tPaidInvoice info) {\n\tconst auto i = Processes.find(process->_session);\n\tAssert(i != end(Processes));\n\tfor (const auto &[itemId, itemProcess] : i->second.byItem) {\n\t\tif (itemProcess.get() == process) {\n\t\t\ti->second.paymentStartedByItem.emplace(itemId, info);\n\t\t\treturn;\n\t\t}\n\t}\n\tfor (const auto &[slug, itemProcess] : i->second.bySlug) {\n\t\tif (itemProcess.get() == process) {\n\t\t\ti->second.paymentStartedBySlug.emplace(slug, info);\n\t\t\treturn;\n\t\t}\n\t}\n\tfor (const auto &[randomId, itemProcess] : i->second.byRandomId) {\n\t\tif (itemProcess.get() == process) {\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nvoid CheckoutProcess::UnregisterPaymentStart(\n\t\tnot_null<CheckoutProcess*> process) {\n\tconst auto i = Processes.find(process->_session);\n\tif (i == end(Processes)) {\n\t\treturn;\n\t}\n\tfor (const auto &[itemId, itemProcess] : i->second.byItem) {\n\t\tif (itemProcess.get() == process) {\n\t\t\ti->second.paymentStartedByItem.remove(itemId);\n\t\t\tbreak;\n\t\t}\n\t}\n\tfor (const auto &[slug, itemProcess] : i->second.bySlug) {\n\t\tif (itemProcess.get() == process) {\n\t\t\ti->second.paymentStartedBySlug.remove(slug);\n\t\t\tbreak;\n\t\t}\n\t}\n\tfor (const auto &[randomId, itemProcess] : i->second.byRandomId) {\n\t\tif (itemProcess.get() == process) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (i->second.paymentStartedByItem.empty()\n\t\t&& i->second.byItem.empty()\n\t\t&& i->second.paymentStartedBySlug.empty()\n\t\t&& i->second.bySlug.empty()\n\t\t&& i->second.byRandomId.empty()) {\n\t\tProcesses.erase(i);\n\t}\n}\n\nCheckoutProcess::CheckoutProcess(\n\tInvoiceId id,\n\tMode mode,\n\tFn<void(CheckoutResult)> reactivate,\n\tFn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess,\n\tPrivateTag)\n: _session(SessionFromId(id))\n, _form(std::make_unique<Form>(id, (mode == Mode::Receipt)))\n, _panel(std::make_unique<Ui::Panel>(panelDelegate()))\n, _reactivate(std::move(reactivate))\n, _nonPanelPaymentFormProcess(std::move(nonPanelPaymentFormProcess)) {\n\t_form->updates(\n\t) | rpl::on_next([=](const FormUpdate &update) {\n\t\thandleFormUpdate(update);\n\t}, _lifetime);\n\n\t_panel->savedMethodChosen(\n\t) | rpl::on_next([=](QString id) {\n\t\t_form->chooseSavedMethod(id);\n\t}, _panel->lifetime());\n\n\t_panel->backRequests(\n\t) | rpl::on_next([=] {\n\t\tpanelCancelEdit();\n\t}, _panel->lifetime());\n\tif (!_nonPanelPaymentFormProcess) {\n\t\tshowForm();\n\t}\n\t_panel->toggleProgress(true);\n\n\tif (mode == Mode::Payment) {\n\t\t_session->api().cloudPassword().state(\n\t\t) | rpl::on_next([=](const Core::CloudPasswordState &state) {\n\t\t\t_form->setHasPassword(state.hasPassword);\n\t\t}, _lifetime);\n\t}\n}\n\nCheckoutProcess::~CheckoutProcess() {\n}\n\nvoid CheckoutProcess::setReactivateCallback(\n\t\tFn<void(CheckoutResult)> reactivate) {\n\t_reactivate = std::move(reactivate);\n}\n\nvoid CheckoutProcess::setNonPanelPaymentFormProcess(\n\t\tFn<void(NonPanelPaymentForm)> callback) {\n\t_nonPanelPaymentFormProcess = std::move(callback);\n}\n\nvoid CheckoutProcess::requestActivate() {\n\tif (!_nonPanelPaymentFormProcess) {\n\t\t_panel->requestActivate();\n\t}\n}\n\nnot_null<Ui::PanelDelegate*> CheckoutProcess::panelDelegate() {\n\treturn static_cast<PanelDelegate*>(this);\n}\n\nvoid CheckoutProcess::handleFormUpdate(const FormUpdate &update) {\n\tv::match(update, [&](const ToggleProgress &data) {\n\t\t_panel->toggleProgress(data.shown);\n\t}, [&](const FormReady &) {\n\t\tperformInitialSilentValidation();\n\t\tif (!_initialSilentValidation) {\n\t\t\tshowForm();\n\t\t}\n\t\tif (!_form->paymentMethod().savedCredentials.empty()) {\n\t\t\t_session->api().cloudPassword().reload();\n\t\t}\n\t}, [&](const ThumbnailUpdated &data) {\n\t\t_panel->updateFormThumbnail(data.thumbnail);\n\t}, [&](const ValidateFinished &) {\n\t\tif (_initialSilentValidation) {\n\t\t\t_initialSilentValidation = false;\n\t\t}\n\t\tshowForm();\n\t\tconst auto submitted = (_submitState == SubmitState::Validating);\n\t\t_submitState = SubmitState::Validated;\n\t\tif (submitted) {\n\t\t\tpanelSubmit();\n\t\t}\n\t}, [&](const PaymentMethodUpdate &data) {\n\t\tshowForm();\n\t\tif (data.requestNewPassword) {\n\t\t\trequestSetPassword();\n\t\t}\n\t}, [&](const TmpPasswordRequired &) {\n\t\tUnregisterPaymentStart(this);\n\t\t_submitState = SubmitState::Validated;\n\t\trequestPassword();\n\t}, [&](const BotTrustRequired &data) {\n\t\tUnregisterPaymentStart(this);\n\t\t_submitState = SubmitState::Validated;\n\t\t_panel->showWarning(data.bot->name(), data.provider->name());\n\t\tif (const auto box = _enterPasswordBox.get()) {\n\t\t\tbox->closeBox();\n\t\t}\n\t}, [&](const VerificationNeeded &data) {\n\t\tauto bottomText = tr::lng_payments_processed_by(\n\t\t\tlt_provider,\n\t\t\trpl::single(_form->invoice().provider));\n\t\t_sendFormFailed = false;\n\t\t_sendFormPending = true;\n\t\tif (!_panel->showWebview(data.url, false, std::move(bottomText))) {\n\t\t\tFile::OpenUrl(data.url);\n\t\t\tclose();\n\t\t}\n\t}, [&](const PaymentFinished &data) {\n\t\tconst auto weak = base::make_weak(this);\n\t\t_session->api().applyUpdates(data.updates);\n\t\tif (weak) {\n\t\t\tcloseAndReactivate(CheckoutResult::Paid);\n\t\t}\n\t}, [&](const CreditsPaymentStarted &data) {\n\t\tif (_nonPanelPaymentFormProcess) {\n\t\t\t_nonPanelPaymentFormProcess(\n\t\t\t\tstd::make_shared<CreditsFormData>(data.data));\n\t\t\tclose();\n\t\t}\n\t}, [&](const CreditsReceiptReady &data) {\n\t\tif (_nonPanelPaymentFormProcess) {\n\t\t\t_nonPanelPaymentFormProcess(\n\t\t\t\tstd::make_shared<CreditsReceiptData>(data.data));\n\t\t\tclose();\n\t\t}\n\t}, [&](const Error &error) {\n\t\thandleError(error);\n\t});\n}\n\nvoid CheckoutProcess::handleError(const Error &error) {\n\tconst auto showToast = [&](TextWithEntities &&text) {\n\t\t_panel->requestActivate();\n\t\t_panel->showToast(std::move(text));\n\t};\n\tconst auto &id = error.id;\n\tswitch (error.type) {\n\tcase Error::Type::Form:\n\t\tif (id == u\"INVOICE_ALREADY_PAID\"_q) {\n\t\t\t_panel->showCriticalError({\n\t\t\t\ttr::lng_payments_already_paid(tr::now)\n\t\t\t});\n\t\t} else if (true\n\t\t\t|| id == u\"PROVIDER_ACCOUNT_INVALID\"_q\n\t\t\t|| id == u\"PROVIDER_ACCOUNT_TIMEOUT\"_q) {\n\t\t\t_panel->showCriticalError({ \"Error: \" + id });\n\t\t}\n\t\tbreak;\n\tcase Error::Type::Validate: {\n\t\tif (_submitState == SubmitState::Validating\n\t\t\t|| _submitState == SubmitState::Validated) {\n\t\t\t_submitState = SubmitState::None;\n\t\t}\n\t\tif (_initialSilentValidation) {\n\t\t\t_initialSilentValidation = false;\n\t\t\tshowForm();\n\t\t\treturn;\n\t\t}\n\t\tusing InfoField = Ui::InformationField;\n\t\tusing CardField = Ui::CardField;\n\t\tif (id == u\"REQ_INFO_NAME_INVALID\"_q) {\n\t\t\tshowInformationError(InfoField::Name);\n\t\t} else if (id == u\"REQ_INFO_EMAIL_INVALID\"_q) {\n\t\t\tshowInformationError(InfoField::Email);\n\t\t} else if (id == u\"REQ_INFO_PHONE_INVALID\"_q) {\n\t\t\tshowInformationError(InfoField::Phone);\n\t\t} else if (id == u\"ADDRESS_STREET_LINE1_INVALID\"_q) {\n\t\t\tshowInformationError(InfoField::ShippingStreet);\n\t\t} else if (id == u\"ADDRESS_CITY_INVALID\"_q) {\n\t\t\tshowInformationError(InfoField::ShippingCity);\n\t\t} else if (id == u\"ADDRESS_STATE_INVALID\"_q) {\n\t\t\tshowInformationError(InfoField::ShippingState);\n\t\t} else if (id == u\"ADDRESS_COUNTRY_INVALID\"_q) {\n\t\t\tshowInformationError(InfoField::ShippingCountry);\n\t\t} else if (id == u\"ADDRESS_POSTCODE_INVALID\"_q) {\n\t\t\tshowInformationError(InfoField::ShippingPostcode);\n\t\t} else if (id == u\"LOCAL_CARD_NUMBER_INVALID\"_q) {\n\t\t\tshowCardError(CardField::Number);\n\t\t} else if (id == u\"LOCAL_CARD_EXPIRE_DATE_INVALID\"_q) {\n\t\t\tshowCardError(CardField::ExpireDate);\n\t\t} else if (id == u\"LOCAL_CARD_CVC_INVALID\"_q) {\n\t\t\tshowCardError(CardField::Cvc);\n\t\t} else if (id == u\"LOCAL_CARD_HOLDER_NAME_INVALID\"_q) {\n\t\t\tshowCardError(CardField::Name);\n\t\t} else if (id == u\"LOCAL_CARD_BILLING_COUNTRY_INVALID\"_q) {\n\t\t\tshowCardError(CardField::AddressCountry);\n\t\t} else if (id == u\"LOCAL_CARD_BILLING_ZIP_INVALID\"_q) {\n\t\t\tshowCardError(CardField::AddressZip);\n\t\t} else if (id == u\"SHIPPING_BOT_TIMEOUT\"_q) {\n\t\t\tshowToast({ \"Error: Bot Timeout!\" });\n\t\t} else if (id == u\"SHIPPING_NOT_AVAILABLE\"_q) {\n\t\t\tshowToast({ tr::lng_payments_shipping_not_available(tr::now) });\n\t\t} else {\n\t\t\tshowToast({ \"Error: \" + id });\n\t\t}\n\t} break;\n\tcase Error::Type::Stripe: {\n\t\tusing Field = Ui::CardField;\n\t\tif (id == u\"InvalidNumber\"_q || id == u\"IncorrectNumber\"_q) {\n\t\t\tshowCardError(Field::Number);\n\t\t} else if (id == u\"InvalidCVC\"_q || id == u\"IncorrectCVC\"_q) {\n\t\t\tshowCardError(Field::Cvc);\n\t\t} else if (id == u\"InvalidExpiryMonth\"_q\n\t\t\t|| id == u\"InvalidExpiryYear\"_q\n\t\t\t|| id == u\"ExpiredCard\"_q) {\n\t\t\tshowCardError(Field::ExpireDate);\n\t\t} else if (id == u\"CardDeclined\"_q) {\n\t\t\tshowToast({ tr::lng_payments_card_declined(tr::now) });\n\t\t} else if (id == u\"ProcessingError\"_q) {\n\t\t\tshowToast({ \"Sorry, a processing error occurred.\" });\n\t\t} else {\n\t\t\tshowToast({ \"Stripe Error: \" + id });\n\t\t}\n\t} break;\n\tcase Error::Type::SmartGlocal: {\n\t\tshowToast({ \"SmartGlocal Error: \" + id });\n\t} break;\n\tcase Error::Type::TmpPassword:\n\t\tif (const auto box = _enterPasswordBox.get()) {\n\t\t\tif (!box->handleCustomCheckError(id)) {\n\t\t\t\tshowToast({ \"Error: Could not generate tmp password.\" });\n\t\t\t}\n\t\t}\n\t\tbreak;\n\tcase Error::Type::Send:\n\t\t_sendFormFailed = true;\n\t\tif (const auto box = _enterPasswordBox.get()) {\n\t\t\tbox->closeBox();\n\t\t}\n\t\tif (_submitState == SubmitState::Finishing) {\n\t\t\tUnregisterPaymentStart(this);\n\t\t\t_submitState = SubmitState::Validated;\n\t\t}\n\t\tif (id == u\"INVOICE_ALREADY_PAID\"_q) {\n\t\t\tshowToast({ tr::lng_payments_already_paid(tr::now) });\n\t\t} else if (id == u\"PAYMENT_FAILED\"_q) {\n\t\t\tshowToast({ tr::lng_payments_payment_failed(tr::now) });\n\t\t} else if (id == u\"BOT_PRECHECKOUT_FAILED\"_q) {\n\t\t\tshowToast({ tr::lng_payments_precheckout_failed(tr::now) });\n\t\t} else if (id == u\"BOT_PRECHECKOUT_TIMEOUT\"_q) {\n\t\t\tshowToast({ tr::lng_payments_precheckout_timeout(tr::now) });\n\t\t} else if (id == u\"REQUESTED_INFO_INVALID\"_q\n\t\t\t|| id == u\"SHIPPING_OPTION_INVALID\"_q\n\t\t\t|| id == u\"PAYMENT_CREDENTIALS_INVALID\"_q\n\t\t\t|| id == u\"PAYMENT_CREDENTIALS_ID_INVALID\"_q) {\n\t\t\tshowToast({ tr::lng_payments_payment_failed(tr::now) });\n\t\t\tshowToast({ \"Error: \" + id + \". Your card has not been billed.\" });\n\t\t} else if (id == u\"TMP_PASSWORD_INVALID\"_q) {\n\t\t\trequestPassword();\n\t\t} else {\n\t\t\tshowToast({ \"Error: \" + id });\n\t\t}\n\t\tbreak;\n\tdefault: Unexpected(\"Error type in CheckoutProcess::handleError.\");\n\t}\n}\n\nvoid CheckoutProcess::panelRequestClose() {\n\tif (_form->hasChanges()) {\n\t\t_panel->showCloseConfirm();\n\t} else {\n\t\tpanelCloseSure();\n\t}\n}\n\nvoid CheckoutProcess::panelCloseSure() {\n\tcloseAndReactivate(_sendFormFailed\n\t\t? CheckoutResult::Failed\n\t\t: _sendFormPending\n\t\t? CheckoutResult::Pending\n\t\t: CheckoutResult::Cancelled);\n}\n\nvoid CheckoutProcess::closeAndReactivate(CheckoutResult result) {\n\tconst auto reactivate = std::move(_reactivate);\n\tclose();\n\tif (reactivate) {\n\t\treactivate(result);\n\t}\n}\n\nvoid CheckoutProcess::close() {\n\tconst auto i = Processes.find(_session);\n\tif (i == end(Processes)) {\n\t\treturn;\n\t}\n\tauto &entry = i->second;\n\tconst auto j = ranges::find(entry.byItem, this, [](const auto &pair) {\n\t\treturn pair.second.get();\n\t});\n\tif (j != end(entry.byItem)) {\n\t\tentry.byItem.erase(j);\n\t}\n\tconst auto k = ranges::find(entry.bySlug, this, [](const auto &pair) {\n\t\treturn pair.second.get();\n\t});\n\tif (k != end(entry.bySlug)) {\n\t\tentry.bySlug.erase(k);\n\t}\n\tconst auto l = ranges::find(\n\t\tentry.byRandomId,\n\t\tthis,\n\t\t[](const auto &pair) { return pair.second.get(); });\n\tif (l != end(entry.byRandomId)) {\n\t\tentry.byRandomId.erase(l);\n\t}\n\tif (entry.byItem.empty()\n\t\t&& entry.bySlug.empty()\n\t\t&& i->second.byRandomId.empty()\n\t\t&& entry.paymentStartedByItem.empty()\n\t\t&& entry.paymentStartedBySlug.empty()) {\n\t\tProcesses.erase(i);\n\t}\n}\n\nvoid CheckoutProcess::panelSubmit() {\n\tif (_form->invoice().receipt.paid) {\n\t\tcloseAndReactivate(CheckoutResult::Paid);\n\t\treturn;\n\t} else if (_submitState == SubmitState::Validating\n\t\t|| _submitState == SubmitState::Finishing) {\n\t\treturn;\n\t}\n\tconst auto &method = _form->paymentMethod();\n\tconst auto &invoice = _form->invoice();\n\tconst auto &options = _form->shippingOptions();\n\tif (!options.list.empty() && options.selectedId.isEmpty()) {\n\t\tchooseShippingOption();\n\t} else if (_submitState != SubmitState::Validated\n\t\t&& options.list.empty()\n\t\t&& (invoice.isShippingAddressRequested\n\t\t\t|| invoice.isNameRequested\n\t\t\t|| invoice.isEmailRequested\n\t\t\t|| invoice.isPhoneRequested)) {\n\t\t_submitState = SubmitState::Validating;\n\t\t_form->validateInformation(_form->information());\n\t} else if (!method.newCredentials\n\t\t&& method.savedCredentialsIndex >= method.savedCredentials.size()) {\n\t\teditPaymentMethod();\n\t} else if (!invoice.termsUrl.isEmpty()\n\t\t&& !_form->details().termsAccepted) {\n\t\t_panel->requestTermsAcceptance(\n\t\t\t_form->details().termsBotUsername,\n\t\t\tinvoice.termsUrl,\n\t\t\tinvoice.isRecurring);\n\t} else {\n\t\tRegisterPaymentStart(this, { _form->invoice().cover.title });\n\t\t_submitState = SubmitState::Finishing;\n\t\t_form->submit();\n\t}\n}\n\nvoid CheckoutProcess::panelTrustAndSubmit() {\n\t_form->trustBot();\n\tpanelSubmit();\n}\n\nvoid CheckoutProcess::panelAcceptTermsAndSubmit() {\n\t_form->acceptTerms();\n\tpanelSubmit();\n}\n\nvoid CheckoutProcess::panelWebviewMessage(\n\t\tconst QJsonDocument &message,\n\t\tbool saveInformation) {\n\tif (!message.isArray()) {\n\t\tLOG((\"Payments Error: \"\n\t\t\t\"Not an array received in buy_callback arguments.\"));\n\t\treturn;\n\t}\n\tconst auto list = message.array();\n\tif (list.at(0).toString() != \"payment_form_submit\") {\n\t\treturn;\n\t} else if (!list.at(1).isString()) {\n\t\tLOG((\"Payments Error: \"\n\t\t\t\"Not a string received in buy_callback result.\"));\n\t\treturn;\n\t}\n\n\tauto error = QJsonParseError();\n\tconst auto document = QJsonDocument::fromJson(\n\t\tlist.at(1).toString().toUtf8(),\n\t\t&error);\n\tif (error.error != QJsonParseError::NoError) {\n\t\tLOG((\"Payments Error: \"\n\t\t\t\"Failed to parse buy_callback arguments, error: %1.\"\n\t\t\t).arg(error.errorString()));\n\t\treturn;\n\t} else if (!document.isObject()) {\n\t\tLOG((\"Payments Error: \"\n\t\t\t\"Not an object decoded in buy_callback result.\"));\n\t\treturn;\n\t}\n\tconst auto root = document.object();\n\tconst auto title = root.value(\"title\").toString();\n\tconst auto credentials = root.value(\"credentials\");\n\tif (!credentials.isObject()) {\n\t\tLOG((\"Payments Error: \"\n\t\t\t\"Not an object received in payment credentials.\"));\n\t\treturn;\n\t}\n\tcrl::on_main(this, [=] {\n\t\t_form->setPaymentCredentials(NewCredentials{\n\t\t\t.title = title,\n\t\t\t.data = QJsonDocument(\n\t\t\t\tcredentials.toObject()\n\t\t\t).toJson(QJsonDocument::Compact),\n\t\t\t.saveOnServer = saveInformation,\n\t\t});\n\t});\n}\n\nstd::optional<QDate> CheckoutProcess::panelOverrideExpireDateThreshold() {\n\treturn _form->overrideExpireDateThreshold();\n}\n\nbool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) {\n\tif (Core::TryConvertUrlToLocal(uri) == uri) {\n\t\treturn true;\n\t}\n\t// #TODO payments\n\tcrl::on_main(this, [=] { closeAndReactivate(CheckoutResult::Paid); });\n\treturn false;\n}\n\nvoid CheckoutProcess::panelCancelEdit() {\n\tif (_submitState != SubmitState::None\n\t\t&& _submitState != SubmitState::Validated) {\n\t\treturn;\n\t}\n\tshowForm();\n}\n\nvoid CheckoutProcess::panelEditPaymentMethod() {\n\tif (_submitState != SubmitState::None\n\t\t&& _submitState != SubmitState::Validated) {\n\t\treturn;\n\t}\n\teditPaymentMethod();\n}\n\nvoid CheckoutProcess::panelValidateCard(\n\t\tUi::UncheckedCardDetails data,\n\t\tbool saveInformation) {\n\t_form->validateCard(data, saveInformation);\n}\n\nvoid CheckoutProcess::panelEditShippingInformation() {\n\tshowEditInformation(Ui::InformationField::ShippingStreet);\n}\n\nvoid CheckoutProcess::panelEditName() {\n\tshowEditInformation(Ui::InformationField::Name);\n}\n\nvoid CheckoutProcess::panelEditEmail() {\n\tshowEditInformation(Ui::InformationField::Email);\n}\n\nvoid CheckoutProcess::panelEditPhone() {\n\tshowEditInformation(Ui::InformationField::Phone);\n}\n\nvoid CheckoutProcess::showForm() {\n\t_panel->showForm(\n\t\t_form->invoice(),\n\t\t_form->information(),\n\t\t_form->paymentMethod().ui,\n\t\t_form->shippingOptions());\n\tif (_nonPanelPaymentFormProcess && !_realFormNotified) {\n\t\t_realFormNotified = true;\n\t\tconst auto weak = base::make_weak(_panel.get());\n\t\t_nonPanelPaymentFormProcess(RealFormPresentedNotification());\n\t\tif (weak) {\n\t\t\trequestActivate();\n\t\t}\n\t}\n}\n\nvoid CheckoutProcess::showEditInformation(Ui::InformationField field) {\n\tif (_submitState != SubmitState::None\n\t\t&& _submitState != SubmitState::Validated) {\n\t\treturn;\n\t}\n\t_panel->showEditInformation(\n\t\t_form->invoice(),\n\t\t_form->information(),\n\t\tfield);\n}\n\nvoid CheckoutProcess::showInformationError(Ui::InformationField field) {\n\tExpects(_submitState != SubmitState::Validated);\n\n\tif (_submitState != SubmitState::None) {\n\t\treturn;\n\t}\n\t_panel->showInformationError(\n\t\t_form->invoice(),\n\t\t_form->information(),\n\t\tfield);\n}\n\nvoid CheckoutProcess::showCardError(Ui::CardField field) {\n\tif (_submitState != SubmitState::None\n\t\t&& _submitState != SubmitState::Validated) {\n\t\treturn;\n\t}\n\t_panel->showCardError(_form->paymentMethod().ui.native, field);\n}\n\nvoid CheckoutProcess::chooseShippingOption() {\n\t_panel->chooseShippingOption(_form->shippingOptions());\n}\n\nvoid CheckoutProcess::chooseTips() {\n\t_panel->chooseTips(_form->invoice());\n}\n\nvoid CheckoutProcess::editPaymentMethod() {\n\t_panel->choosePaymentMethod(_form->paymentMethod().ui);\n}\n\nvoid CheckoutProcess::requestSetPassword() {\n\t_session->api().cloudPassword().reload();\n\t_panel->askSetPassword();\n}\n\nvoid CheckoutProcess::requestPassword() {\n\tgetPasswordState([=](const Core::CloudPasswordState &state) {\n\t\tauto fields = PasscodeBox::CloudFields::From(state);\n\t\tfields.customTitle = tr::lng_payments_password_title();\n\t\tconst auto &method = _form->paymentMethod();\n\t\tconst auto &list = method.savedCredentials;\n\t\tconst auto index = method.savedCredentialsIndex;\n\t\tfields.customDescription = tr::lng_payments_password_description(\n\t\t\ttr::now,\n\t\t\tlt_card,\n\t\t\t(index < list.size()) ? list[index].title : QString());\n\t\tfields.customSubmitButton = tr::lng_payments_password_submit();\n\t\tfields.customCheckCallback = [=](\n\t\t\t\tconst Core::CloudPasswordResult &result,\n\t\t\t\tbase::weak_qptr<PasscodeBox> box) {\n\t\t\t_enterPasswordBox = box;\n\t\t\t_form->submit(result);\n\t\t};\n\t\t_panel->showBox(Box<PasscodeBox>(_session, fields));\n\t});\n}\n\nvoid CheckoutProcess::panelSetPassword() {\n\tgetPasswordState([=](const Core::CloudPasswordState &state) {\n\t\tif (state.hasPassword) {\n\t\t\treturn;\n\t\t}\n\t\tauto owned = Box<PasscodeBox>(\n\t\t\t_session,\n\t\t\tPasscodeBox::CloudFields::From(state));\n\t\tconst auto box = owned.data();\n\n\t\trpl::merge(\n\t\t\tbox->newPasswordSet() | rpl::to_empty,\n\t\t\tbox->passwordReloadNeeded()\n\t\t) | rpl::on_next([=] {\n\t\t\t_session->api().cloudPassword().reload();\n\t\t}, box->lifetime());\n\n\t\tbox->clearUnconfirmedPassword(\n\t\t) | rpl::on_next([=] {\n\t\t\t_session->api().cloudPassword().clearUnconfirmedPassword();\n\t\t}, box->lifetime());\n\n\t\t_panel->showBox(std::move(owned));\n\t});\n}\n\nvoid CheckoutProcess::panelOpenUrl(const QString &url) {\n\tFile::OpenUrl(url);\n}\n\nvoid CheckoutProcess::getPasswordState(\n\t\tFn<void(const Core::CloudPasswordState&)> callback) {\n\tExpects(callback != nullptr);\n\n\tif (_gettingPasswordState) {\n\t\treturn;\n\t}\n\t_session->api().cloudPassword().state(\n\t) | rpl::on_next([=](const Core::CloudPasswordState &state) {\n\t\t_gettingPasswordState.destroy();\n\t\tcallback(state);\n\t}, _gettingPasswordState);\n}\n\nvoid CheckoutProcess::panelChooseShippingOption() {\n\tif (_submitState != SubmitState::None\n\t\t&& _submitState != SubmitState::Validated) {\n\t\treturn;\n\t}\n\tchooseShippingOption();\n}\n\nvoid CheckoutProcess::panelChangeShippingOption(const QString &id) {\n\t_form->setShippingOption(id);\n\tshowForm();\n}\n\nvoid CheckoutProcess::panelChooseTips() {\n\tif (_submitState != SubmitState::None\n\t\t&& _submitState != SubmitState::Validated) {\n\t\treturn;\n\t}\n\tchooseTips();\n}\n\nvoid CheckoutProcess::panelChangeTips(int64 value) {\n\t_form->setTips(value);\n\tshowForm();\n}\n\nvoid CheckoutProcess::panelValidateInformation(\n\t\tUi::RequestedInformation data) {\n\tif (_submitState == SubmitState::Validated) {\n\t\t_submitState = SubmitState::None;\n\t}\n\t_form->validateInformation(data);\n}\n\nvoid CheckoutProcess::panelShowBox(object_ptr<Ui::BoxContent> box) {\n\t_panel->showBox(std::move(box));\n}\n\nQVariant CheckoutProcess::panelClickHandlerContext() {\n\treturn QVariant::fromValue(ClickHandlerContext{\n\t\t.show = _panel->uiShow(),\n\t});\n}\n\nvoid CheckoutProcess::performInitialSilentValidation() {\n\tconst auto &invoice = _form->invoice();\n\tconst auto &saved = _form->information();\n\tif (invoice.receipt\n\t\t|| (invoice.isNameRequested && saved.name.isEmpty())\n\t\t|| (invoice.isEmailRequested && saved.email.isEmpty())\n\t\t|| (invoice.isPhoneRequested && saved.phone.isEmpty())\n\t\t|| (invoice.isShippingAddressRequested && !saved.shippingAddress)) {\n\t\treturn;\n\t}\n\t_initialSilentValidation = true;\n\t_form->validateInformation(saved);\n}\n\nWebview::StorageId CheckoutProcess::panelWebviewStorageId() {\n\treturn _session->local().resolveStorageIdOther();\n}\n\nWebview::ThemeParams CheckoutProcess::panelWebviewThemeParams() {\n\treturn Window::Theme::WebViewParams();\n}\n\n} // namespace Payments\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/payments_checkout_process.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"payments/ui/payments_panel_delegate.h\"\n#include \"webview/webview_common.h\"\n\nclass HistoryItem;\nclass PasscodeBox;\n\nnamespace Core {\nstruct CloudPasswordState;\n} // namespace Core\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nnamespace Payments::Ui {\nclass Panel;\nenum class InformationField;\nenum class CardField;\n} // namespace Payments::Ui\n\nnamespace Payments {\n\nclass Form;\nstruct FormUpdate;\nstruct Error;\nstruct InvoiceCredits;\nstruct InvoiceStarGift;\nstruct InvoiceId;\nstruct InvoicePremiumGiftCode;\nstruct CreditsFormData;\nstruct CreditsReceiptData;\n\nenum class Mode {\n\tPayment,\n\tReceipt,\n};\n\nenum class CheckoutResult {\n\tPaid,\n\tPending,\n\tCancelled,\n\tFailed,\n\tFree, // Gift transfer attempt that doesn't need any payment.\n};\n\nstruct RealFormPresentedNotification {\n};\nstruct NonPanelPaymentForm\n\t: std::variant<\n\t\tstd::shared_ptr<CreditsFormData>,\n\t\tstd::shared_ptr<CreditsReceiptData>,\n\t\tRealFormPresentedNotification> {\n\tusing variant::variant;\n};\n\nstruct PaidInvoice {\n\tQString title;\n};\n\nclass CheckoutProcess final\n\t: public base::has_weak_ptr\n\t, private Ui::PanelDelegate {\n\tstruct PrivateTag {};\n\npublic:\n\tstatic void Start(\n\t\tnot_null<const HistoryItem*> item,\n\t\tMode mode,\n\t\tFn<void(CheckoutResult)> reactivate,\n\t\tFn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess);\n\tstatic void Start(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &slug,\n\t\tFn<void(CheckoutResult)> reactivate,\n\t\tFn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess);\n\tstatic void Start(\n\t\tInvoicePremiumGiftCode giftCodeInvoice,\n\t\tFn<void(CheckoutResult)> reactivate,\n\t\tFn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess = nullptr);\n\tstatic void Start(\n\t\tInvoiceCredits creditsInvoice,\n\t\tFn<void(CheckoutResult)> reactivate);\n\tstatic void Start(\n\t\tInvoiceStarGift giftInvoice,\n\t\tFn<void(CheckoutResult)> reactivate,\n\t\tFn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess);\n\t[[nodiscard]] static std::optional<PaidInvoice> InvoicePaid(\n\t\tnot_null<const HistoryItem*> item);\n\t[[nodiscard]] static std::optional<PaidInvoice> InvoicePaid(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &slug);\n\tstatic void ClearAll();\n\n\tCheckoutProcess(\n\t\tInvoiceId id,\n\t\tMode mode,\n\t\tFn<void(CheckoutResult)> reactivate,\n\t\tFn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess,\n\t\tPrivateTag);\n\t~CheckoutProcess();\n\nprivate:\n\tenum class SubmitState {\n\t\tNone,\n\t\tValidating,\n\t\tValidated,\n\t\tFinishing,\n\t};\n\t[[nodiscard]] not_null<PanelDelegate*> panelDelegate();\n\n\tstatic void RegisterPaymentStart(\n\t\tnot_null<CheckoutProcess*> process,\n\t\tPaidInvoice info);\n\tstatic void UnregisterPaymentStart(not_null<CheckoutProcess*> process);\n\n\tvoid setReactivateCallback(Fn<void(CheckoutResult)> reactivate);\n\tvoid setNonPanelPaymentFormProcess(Fn<void(NonPanelPaymentForm)>);\n\tvoid requestActivate();\n\tvoid closeAndReactivate(CheckoutResult result);\n\tvoid close();\n\n\tvoid handleFormUpdate(const FormUpdate &update);\n\tvoid handleError(const Error &error);\n\n\tvoid showForm();\n\tvoid showEditInformation(Ui::InformationField field);\n\tvoid showInformationError(Ui::InformationField field);\n\tvoid showCardError(Ui::CardField field);\n\tvoid chooseShippingOption();\n\tvoid chooseTips();\n\tvoid editPaymentMethod();\n\n\tvoid requestSetPassword();\n\tvoid requestPassword();\n\tvoid getPasswordState(\n\t\tFn<void(const Core::CloudPasswordState&)> callback);\n\n\tvoid performInitialSilentValidation();\n\n\tvoid panelRequestClose() override;\n\tvoid panelCloseSure() override;\n\tvoid panelSubmit() override;\n\tvoid panelTrustAndSubmit() override;\n\tvoid panelAcceptTermsAndSubmit() override;\n\tvoid panelWebviewMessage(\n\t\tconst QJsonDocument &message,\n\t\tbool saveInformation) override;\n\tbool panelWebviewNavigationAttempt(const QString &uri) override;\n\tvoid panelSetPassword() override;\n\tvoid panelOpenUrl(const QString &url) override;\n\n\tvoid panelCancelEdit() override;\n\tvoid panelEditPaymentMethod() override;\n\tvoid panelEditShippingInformation() override;\n\tvoid panelEditName() override;\n\tvoid panelEditEmail() override;\n\tvoid panelEditPhone() override;\n\tvoid panelChooseShippingOption() override;\n\tvoid panelChangeShippingOption(const QString &id) override;\n\tvoid panelChooseTips() override;\n\tvoid panelChangeTips(int64 value) override;\n\n\tvoid panelValidateInformation(Ui::RequestedInformation data) override;\n\tvoid panelValidateCard(\n\t\tUi::UncheckedCardDetails data,\n\t\tbool saveInformation) override;\n\tvoid panelShowBox(object_ptr<Ui::BoxContent> box) override;\n\tQVariant panelClickHandlerContext() override;\n\n\tWebview::StorageId panelWebviewStorageId() override;\n\tWebview::ThemeParams panelWebviewThemeParams() override;\n\n\tstd::optional<QDate> panelOverrideExpireDateThreshold() override;\n\n\tconst not_null<Main::Session*> _session;\n\tconst std::unique_ptr<Form> _form;\n\tconst std::unique_ptr<Ui::Panel> _panel;\n\tbase::weak_qptr<PasscodeBox> _enterPasswordBox;\n\tFn<void(CheckoutResult)> _reactivate;\n\tFn<void(NonPanelPaymentForm)> _nonPanelPaymentFormProcess;\n\tSubmitState _submitState = SubmitState::None;\n\tbool _initialSilentValidation = false;\n\tbool _realFormNotified = false;\n\tbool _sendFormPending = false;\n\tbool _sendFormFailed = false;\n\n\trpl::lifetime _gettingPasswordState;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Payments\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/payments_form.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"payments/payments_form.h\"\n\n#include \"main/main_session.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_session.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_user.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"countries/countries_instance.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"stripe/stripe_api_client.h\"\n#include \"stripe/stripe_error.h\"\n#include \"stripe/stripe_token.h\"\n#include \"stripe/stripe_card_validator.h\"\n#include \"smartglocal/smartglocal_api_client.h\"\n#include \"smartglocal/smartglocal_error.h\"\n#include \"smartglocal/smartglocal_token.h\"\n#include \"storage/storage_account.h\"\n#include \"ui/image/image.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_entity.h\"\n#include \"apiwrap.h\"\n#include \"api/api_text_entities.h\"\n#include \"core/core_cloud_password.h\"\n#include \"window/themes/window_theme.h\"\n#include \"webview/webview_interface.h\"\n#include \"styles/style_payments.h\" // paymentsThumbnailSize.\n\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonObject>\n#include <QtCore/QJsonValue>\n\nnamespace Payments {\nnamespace {\n\nconstexpr auto kPasswordPeriod = 15 * TimeId(60);\n\n[[nodiscard]] Ui::Address ParseAddress(const MTPPostAddress &address) {\n\treturn address.match([](const MTPDpostAddress &data) {\n\t\treturn Ui::Address{\n\t\t\t.address1 = qs(data.vstreet_line1()),\n\t\t\t.address2 = qs(data.vstreet_line2()),\n\t\t\t.city = qs(data.vcity()),\n\t\t\t.state = qs(data.vstate()),\n\t\t\t.countryIso2 = qs(data.vcountry_iso2()),\n\t\t\t.postcode = qs(data.vpost_code()),\n\t\t};\n\t});\n}\n\n[[nodiscard]] int64 ParsePriceAmount(uint64 value) {\n\treturn *reinterpret_cast<const int64*>(&value);\n}\n\n[[nodiscard]] std::vector<Ui::LabeledPrice> ParsePrices(\n\t\tconst MTPVector<MTPLabeledPrice> &data) {\n\treturn ranges::views::all(\n\t\tdata.v\n\t) | ranges::views::transform([](const MTPLabeledPrice &price) {\n\t\treturn price.match([&](const MTPDlabeledPrice &data) {\n\t\t\treturn Ui::LabeledPrice{\n\t\t\t\t.label = qs(data.vlabel()),\n\t\t\t\t.price = ParsePriceAmount(data.vamount().v),\n\t\t\t};\n\t\t});\n\t}) | ranges::to_vector;\n}\n\n[[nodiscard]] MTPPaymentRequestedInfo Serialize(\n\t\tconst Ui::RequestedInformation &information) {\n\tusing Flag = MTPDpaymentRequestedInfo::Flag;\n\treturn MTP_paymentRequestedInfo(\n\t\tMTP_flags((information.name.isEmpty() ? Flag(0) : Flag::f_name)\n\t\t\t| (information.email.isEmpty() ? Flag(0) : Flag::f_email)\n\t\t\t| (information.phone.isEmpty() ? Flag(0) : Flag::f_phone)\n\t\t\t| (information.shippingAddress\n\t\t\t\t? Flag::f_shipping_address\n\t\t\t\t: Flag(0))),\n\t\tMTP_string(information.name),\n\t\tMTP_string(information.phone),\n\t\tMTP_string(information.email),\n\t\tMTP_postAddress(\n\t\t\tMTP_string(information.shippingAddress.address1),\n\t\t\tMTP_string(information.shippingAddress.address2),\n\t\t\tMTP_string(information.shippingAddress.city),\n\t\t\tMTP_string(information.shippingAddress.state),\n\t\t\tMTP_string(information.shippingAddress.countryIso2),\n\t\t\tMTP_string(information.shippingAddress.postcode)));\n}\n\n[[nodiscard]] QString CardTitle(const Stripe::Card &card) {\n\t// Like server stores saved_credentials title.\n\treturn Stripe::CardBrandToString(card.brand()).toLower()\n\t\t+ \" *\"\n\t\t+ card.last4();\n}\n\n[[nodiscard]] QString CardTitle(const SmartGlocal::Card &card) {\n\t// Like server stores saved_credentials title.\n\treturn card.type().toLower()\n\t\t+ \" *\"\n\t\t+ SmartGlocal::Last4(card);\n}\n\n} // namespace\n\nnot_null<Main::Session*> SessionFromId(const InvoiceId &id) {\n\tif (const auto message = std::get_if<InvoiceMessage>(&id.value)) {\n\t\treturn &message->peer->session();\n\t} else if (const auto slug = std::get_if<InvoiceSlug>(&id.value)) {\n\t\treturn slug->session;\n\t} else if (const auto slug = std::get_if<InvoiceCredits>(&id.value)) {\n\t\treturn slug->session;\n\t} else if (const auto gift = std::get_if<InvoiceStarGift>(&id.value)) {\n\t\treturn &gift->recipient->session();\n\t}\n\tconst auto &giftCode = v::get<InvoicePremiumGiftCode>(id.value);\n\tconst auto users = std::get_if<InvoicePremiumGiftCodeUsers>(\n\t\t&giftCode.purpose);\n\tif (users) {\n\t\tAssert(!users->users.empty());\n\t\treturn &users->users.front()->session();\n\t}\n\tconst auto &giveaway = v::get<InvoicePremiumGiftCodeGiveaway>(\n\t\tgiftCode.purpose);\n\treturn &giveaway.boostPeer->session();\n}\n\nMTPinputStorePaymentPurpose InvoicePremiumGiftCodeGiveawayToTL(\n\t\tconst InvoicePremiumGiftCode &invoice) {\n\tconst auto &giveaway = v::get<InvoicePremiumGiftCodeGiveaway>(\n\t\tinvoice.purpose);\n\tusing Flag = MTPDinputStorePaymentPremiumGiveaway::Flag;\n\treturn MTP_inputStorePaymentPremiumGiveaway(\n\t\tMTP_flags(Flag()\n\t\t\t| (giveaway.onlyNewSubscribers\n\t\t\t\t? Flag::f_only_new_subscribers\n\t\t\t\t: Flag())\n\t\t\t| (giveaway.additionalChannels.empty()\n\t\t\t\t? Flag()\n\t\t\t\t: Flag::f_additional_peers)\n\t\t\t| (giveaway.countries.empty()\n\t\t\t\t? Flag()\n\t\t\t\t: Flag::f_countries_iso2)\n\t\t\t| (giveaway.showWinners\n\t\t\t\t? Flag::f_winners_are_visible\n\t\t\t\t: Flag())\n\t\t\t| (giveaway.additionalPrize.isEmpty()\n\t\t\t\t? Flag()\n\t\t\t\t: Flag::f_prize_description)),\n\t\tgiveaway.boostPeer->input(),\n\t\tMTP_vector_from_range(ranges::views::all(\n\t\t\tgiveaway.additionalChannels\n\t\t) | ranges::views::transform([](not_null<ChannelData*> c) {\n\t\t\treturn MTPInputPeer(c->input());\n\t\t})),\n\t\tMTP_vector_from_range(ranges::views::all(\n\t\t\tgiveaway.countries\n\t\t) | ranges::views::transform([](QString value) {\n\t\t\treturn MTP_string(value);\n\t\t})),\n\t\tMTP_string(giveaway.additionalPrize),\n\t\tMTP_long(invoice.randomId),\n\t\tMTP_int(giveaway.untilDate),\n\t\tMTP_string(invoice.currency),\n\t\tMTP_long(invoice.amount));\n}\n\nMTPinputStorePaymentPurpose InvoiceCreditsGiveawayToTL(\n\t\tconst InvoicePremiumGiftCode &invoice) {\n\tExpects(invoice.giveawayCredits.has_value());\n\tconst auto &giveaway = v::get<InvoicePremiumGiftCodeGiveaway>(\n\t\tinvoice.purpose);\n\tusing Flag = MTPDinputStorePaymentStarsGiveaway::Flag;\n\treturn MTP_inputStorePaymentStarsGiveaway(\n\t\tMTP_flags(Flag()\n\t\t\t| (giveaway.onlyNewSubscribers\n\t\t\t\t? Flag::f_only_new_subscribers\n\t\t\t\t: Flag())\n\t\t\t| (giveaway.additionalChannels.empty()\n\t\t\t\t? Flag()\n\t\t\t\t: Flag::f_additional_peers)\n\t\t\t| (giveaway.countries.empty()\n\t\t\t\t? Flag()\n\t\t\t\t: Flag::f_countries_iso2)\n\t\t\t| (giveaway.showWinners\n\t\t\t\t? Flag::f_winners_are_visible\n\t\t\t\t: Flag())\n\t\t\t| (giveaway.additionalPrize.isEmpty()\n\t\t\t\t? Flag()\n\t\t\t\t: Flag::f_prize_description)),\n\t\tMTP_long(*invoice.giveawayCredits),\n\t\tgiveaway.boostPeer->input(),\n\t\tMTP_vector_from_range(ranges::views::all(\n\t\t\tgiveaway.additionalChannels\n\t\t) | ranges::views::transform([](not_null<ChannelData*> c) {\n\t\t\treturn MTPInputPeer(c->input());\n\t\t})),\n\t\tMTP_vector_from_range(ranges::views::all(\n\t\t\tgiveaway.countries\n\t\t) | ranges::views::transform([](QString value) {\n\t\t\treturn MTP_string(value);\n\t\t})),\n\t\tMTP_string(giveaway.additionalPrize),\n\t\tMTP_long(invoice.randomId),\n\t\tMTP_int(giveaway.untilDate),\n\t\tMTP_string(invoice.currency),\n\t\tMTP_long(invoice.amount),\n\t\tMTP_int(invoice.users));\n}\n\nbool IsPremiumForStarsInvoice(const InvoiceId &id) {\n\tconst auto giftCode = std::get_if<InvoicePremiumGiftCode>(&id.value);\n\treturn giftCode\n\t\t&& !giftCode->giveawayCredits\n\t\t&& (giftCode->currency == ::Ui::kCreditsCurrency);\n}\n\nForm::Form(InvoiceId id, bool receipt)\n: _id(id)\n, _session(SessionFromId(id))\n, _api(&_session->mtp())\n, _receiptMode(receipt) {\n\tfillInvoiceFromMessage();\n\tif (_receiptMode) {\n\t\t_invoice.receipt.paid = true;\n\t\trequestReceipt();\n\t} else {\n\t\trequestForm();\n\t}\n}\n\nForm::~Form() = default;\n\nvoid Form::fillInvoiceFromMessage() {\n\tconst auto message = std::get_if<InvoiceMessage>(&_id.value);\n\tif (!message) {\n\t\treturn;\n\t}\n\tconst auto id = FullMsgId(message->peer->id, message->itemId);\n\tif (const auto item = _session->data().message(id)) {\n\t\tconst auto media = [&] {\n\t\t\tif (const auto payment = item->Get<HistoryServicePayment>()) {\n\t\t\t\tif (const auto invoice = payment->msg) {\n\t\t\t\t\treturn invoice->media();\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn item->media();\n\t\t}();\n\t\tif (const auto invoice = media ? media->invoice() : nullptr) {\n\t\t\t_invoice.isTest = invoice->isTest;\n\t\t\t_invoice.cover = Ui::Cover{\n\t\t\t\t.title = invoice->title,\n\t\t\t\t.description = invoice->description,\n\t\t\t};\n\t\t\tif (const auto photo = invoice->photo) {\n\t\t\t\tloadThumbnail(photo);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Form::showProgress() {\n\t_updates.fire(ToggleProgress{ true });\n}\n\nvoid Form::hideProgress() {\n\t_updates.fire(ToggleProgress{ false });\n}\n\nvoid Form::loadThumbnail(not_null<PhotoData*> photo) {\n\tExpects(!_thumbnailLoadProcess);\n\n\tauto view = photo->createMediaView();\n\tif (auto good = prepareGoodThumbnail(view); !good.isNull()) {\n\t\t_invoice.cover.thumbnail = std::move(good);\n\t\treturn;\n\t}\n\t_thumbnailLoadProcess = std::make_unique<ThumbnailLoadProcess>();\n\tif (auto blurred = prepareBlurredThumbnail(view); !blurred.isNull()) {\n\t\t_invoice.cover.thumbnail = std::move(blurred);\n\t\t_thumbnailLoadProcess->blurredSet = true;\n\t} else {\n\t\t_invoice.cover.thumbnail = prepareEmptyThumbnail();\n\t}\n\t_thumbnailLoadProcess->view = std::move(view);\n\tphoto->load(Data::PhotoSize::Thumbnail, thumbnailFileOrigin());\n\t_session->downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tconst auto &view = _thumbnailLoadProcess->view;\n\t\tif (auto good = prepareGoodThumbnail(view); !good.isNull()) {\n\t\t\t_invoice.cover.thumbnail = std::move(good);\n\t\t\t_thumbnailLoadProcess = nullptr;\n\t\t} else if (_thumbnailLoadProcess->blurredSet) {\n\t\t\treturn;\n\t\t} else if (auto blurred = prepareBlurredThumbnail(view)\n\t\t\t; !blurred.isNull()) {\n\t\t\t_invoice.cover.thumbnail = std::move(blurred);\n\t\t\t_thumbnailLoadProcess->blurredSet = true;\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t\t_updates.fire(ThumbnailUpdated{ _invoice.cover.thumbnail });\n\t}, _thumbnailLoadProcess->lifetime);\n}\n\nData::FileOrigin Form::thumbnailFileOrigin() const {\n\tif (const auto message = std::get_if<InvoiceMessage>(&_id.value)) {\n\t\treturn FullMsgId(message->peer->id, message->itemId);\n\t}\n\treturn Data::FileOrigin();\n}\n\nQImage Form::prepareGoodThumbnail(\n\t\tconst std::shared_ptr<Data::PhotoMedia> &view) const {\n\tusing Size = Data::PhotoSize;\n\tif (const auto large = view->image(Size::Large)) {\n\t\treturn prepareThumbnail(large);\n\t} else if (const auto thumbnail = view->image(Size::Thumbnail)) {\n\t\treturn prepareThumbnail(thumbnail);\n\t}\n\treturn QImage();\n}\n\nQImage Form::prepareBlurredThumbnail(\n\t\tconst std::shared_ptr<Data::PhotoMedia> &view) const {\n\tif (const auto small = view->image(Data::PhotoSize::Small)) {\n\t\treturn prepareThumbnail(small, true);\n\t} else if (const auto blurred = view->thumbnailInline()) {\n\t\treturn prepareThumbnail(blurred, true);\n\t}\n\treturn QImage();\n}\n\nQImage Form::prepareThumbnail(\n\t\tnot_null<const Image*> image,\n\t\tbool blurred) const {\n\tauto result = image->original().scaled(\n\t\tst::paymentsThumbnailSize * style::DevicePixelRatio(),\n\t\tQt::KeepAspectRatio,\n\t\tQt::SmoothTransformation);\n\tresult = Images::Round(std::move(result), ImageRoundRadius::Large);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\treturn result;\n}\n\nQImage Form::prepareEmptyThumbnail() const {\n\tauto result = QImage(\n\t\tst::paymentsThumbnailSize * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult.fill(Qt::transparent);\n\treturn result;\n}\n\nMTPInputInvoice Form::inputInvoice() const {\n\tif (const auto message = std::get_if<InvoiceMessage>(&_id.value)) {\n\t\treturn MTP_inputInvoiceMessage(\n\t\t\tmessage->peer->input(),\n\t\t\tMTP_int(message->itemId.bare));\n\t} else if (const auto slug = std::get_if<InvoiceSlug>(&_id.value)) {\n\t\treturn MTP_inputInvoiceSlug(MTP_string(slug->slug));\n\t} else if (const auto credits = std::get_if<InvoiceCredits>(&_id.value)) {\n\t\tif (const auto userId = peerToUser(credits->giftPeerId)) {\n\t\t\tif (const auto user = _session->data().user(userId)) {\n\t\t\t\treturn MTP_inputInvoiceStars(\n\t\t\t\t\tMTP_inputStorePaymentStarsGift(\n\t\t\t\t\t\tuser->inputUser(),\n\t\t\t\t\t\tMTP_long(credits->credits),\n\t\t\t\t\t\tMTP_string(credits->currency),\n\t\t\t\t\t\tMTP_long(credits->amount)));\n\t\t\t}\n\t\t}\n\t\tconst auto spendPeer = credits->spendPurposePeerId\n\t\t\t? _session->data().peerLoaded(credits->spendPurposePeerId)\n\t\t\t: nullptr;\n\t\tusing Flag = MTPDinputStorePaymentStarsTopup::Flag;\n\t\treturn MTP_inputInvoiceStars(\n\t\t\tMTP_inputStorePaymentStarsTopup(\n\t\t\t\tMTP_flags(spendPeer\n\t\t\t\t\t? Flag::f_spend_purpose_peer\n\t\t\t\t\t: Flag()),\n\t\t\t\tMTP_long(credits->credits),\n\t\t\t\tMTP_string(credits->currency),\n\t\t\t\tMTP_long(credits->amount),\n\t\t\t\tspendPeer ? spendPeer->input() : MTPInputPeer()));\n\t} else if (const auto gift = std::get_if<InvoiceStarGift>(&_id.value)) {\n\t\tusing Flag = MTPDinputInvoiceStarGift::Flag;\n\t\treturn MTP_inputInvoiceStarGift(\n\t\t\tMTP_flags((gift->anonymous ? Flag::f_hide_name : Flag(0))\n\t\t\t\t| (gift->message.empty() ? Flag(0) : Flag::f_message)\n\t\t\t\t| (gift->upgraded ? Flag::f_include_upgrade : Flag(0))),\n\t\t\tgift->recipient->input(),\n\t\t\tMTP_long(gift->giftId),\n\t\t\tMTP_textWithEntities(\n\t\t\t\tMTP_string(gift->message.text),\n\t\t\t\tApi::EntitiesToMTP(\n\t\t\t\t\t&gift->recipient->session(),\n\t\t\t\t\tgift->message.entities,\n\t\t\t\t\tApi::ConvertOption::SkipLocal)));\n\t}\n\tconst auto &giftCode = v::get<InvoicePremiumGiftCode>(_id.value);\n\tif (giftCode.giveawayCredits) {\n\t\treturn MTP_inputInvoiceStars(InvoiceCreditsGiveawayToTL(giftCode));\n\t}\n\tusing Flag = MTPDpremiumGiftCodeOption::Flag;\n\tconst auto option = MTP_premiumGiftCodeOption(\n\t\tMTP_flags((giftCode.storeQuantity ? Flag::f_store_quantity : Flag())\n\t\t\t| (giftCode.storeProduct.isEmpty()\n\t\t\t\t? Flag()\n\t\t\t\t: Flag::f_store_product)),\n\t\tMTP_int(giftCode.users),\n\t\tMTP_int(giftCode.months),\n\t\tMTP_string(giftCode.storeProduct),\n\t\tMTP_int(giftCode.storeQuantity),\n\t\tMTP_string(giftCode.currency),\n\t\tMTP_long(giftCode.amount));\n\tconst auto users = std::get_if<InvoicePremiumGiftCodeUsers>(\n\t\t&giftCode.purpose);\n\tauto message = (users && !users->message.empty())\n\t\t? MTP_textWithEntities(\n\t\t\tMTP_string(users->message.text),\n\t\t\tApi::EntitiesToMTP(\n\t\t\t\t&users->users.front()->session(),\n\t\t\t\tusers->message.entities,\n\t\t\t\tApi::ConvertOption::SkipLocal))\n\t\t: std::optional<MTPTextWithEntities>();\n\tif (users\n\t\t&& users->users.size() == 1\n\t\t&& giftCode.currency == ::Ui::kCreditsCurrency) {\n\t\tusing Flag = MTPDinputInvoicePremiumGiftStars::Flag;\n\t\treturn MTP_inputInvoicePremiumGiftStars(\n\t\t\tMTP_flags(message ? Flag::f_message : Flag()),\n\t\t\tusers->users.front()->inputUser(),\n\t\t\tMTP_int(giftCode.months),\n\t\t\tmessage.value_or(MTPTextWithEntities()));\n\t} else if (users) {\n\t\tusing Flag = MTPDinputStorePaymentPremiumGiftCode::Flag;\n\t\treturn MTP_inputInvoicePremiumGiftCode(\n\t\t\tMTP_inputStorePaymentPremiumGiftCode(\n\t\t\t\tMTP_flags((users->boostPeer ? Flag::f_boost_peer : Flag())\n\t\t\t\t\t| (message ? Flag::f_message : Flag())),\n\t\t\t\tMTP_vector_from_range(ranges::views::all(\n\t\t\t\t\tusers->users\n\t\t\t\t) | ranges::views::transform([](not_null<UserData*> user) {\n\t\t\t\t\treturn MTPInputUser(user->inputUser());\n\t\t\t\t})),\n\t\t\t\tusers->boostPeer ? users->boostPeer->input() : MTPInputPeer(),\n\t\t\t\tMTP_string(giftCode.currency),\n\t\t\t\tMTP_long(giftCode.amount),\n\t\t\t\tmessage.value_or(MTPTextWithEntities())),\n\t\t\toption);\n\t} else {\n\t\treturn MTP_inputInvoicePremiumGiftCode(\n\t\t\tInvoicePremiumGiftCodeGiveawayToTL(giftCode),\n\t\t\toption);\n\t}\n}\n\nvoid Form::requestForm() {\n\tshowProgress();\n\t_api.request(MTPpayments_GetPaymentForm(\n\t\tMTP_flags(MTPpayments_GetPaymentForm::Flag::f_theme_params),\n\t\tinputInvoice(),\n\t\tMTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json))\n\t)).done([=](const MTPpayments_PaymentForm &result) {\n\t\thideProgress();\n\t\tresult.match([&](const MTPDpayments_paymentForm &data) {\n\t\t\tprocessForm(data);\n\t\t}, [&](const MTPDpayments_paymentFormStars &data) {\n\t\t\t_session->data().processUsers(data.vusers());\n\t\t\tconst auto currency = qs(data.vinvoice().data().vcurrency());\n\t\t\tconst auto &tlPrices = data.vinvoice().data().vprices().v;\n\t\t\tconst auto amount = tlPrices.empty()\n\t\t\t\t? 0\n\t\t\t\t: tlPrices.front().data().vamount().v;\n\t\t\tconst auto subscriptionPeriod\n\t\t\t\t= data.vinvoice().data().vsubscription_period().value_or(0);\n\t\t\tif (currency != ::Ui::kCreditsCurrency || !amount) {\n\t\t\t\tusing Type = Error::Type;\n\t\t\t\t_updates.fire(Error{ Type::Form, u\"Bad Stars Form.\"_q });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto invoice = InvoiceCredits{\n\t\t\t\t.session = _session,\n\t\t\t\t.randomId = 0,\n\t\t\t\t.credits = amount,\n\t\t\t\t.currency = currency,\n\t\t\t\t.amount = amount,\n\t\t\t\t.subscriptionPeriod = subscriptionPeriod,\n\t\t\t};\n\t\t\tconst auto formData = CreditsFormData{\n\t\t\t\t.id = _id,\n\t\t\t\t.formId = data.vform_id().v,\n\t\t\t\t.botId = data.vbot_id().v,\n\t\t\t\t.title = qs(data.vtitle()),\n\t\t\t\t.description = qs(data.vdescription()),\n\t\t\t\t.photo = data.vphoto()\n\t\t\t\t\t? _session->data().photoFromWeb(\n\t\t\t\t\t\t*data.vphoto(),\n\t\t\t\t\t\tImageLocation())\n\t\t\t\t\t: nullptr,\n\t\t\t\t.invoice = invoice,\n\t\t\t\t.inputInvoice = inputInvoice(),\n\t\t\t};\n\t\t\t_updates.fire(CreditsPaymentStarted{ .data = formData });\n\t\t}, [&](const MTPDpayments_paymentFormStarGift &data) {\n\t\t\tconst auto currency = qs(data.vinvoice().data().vcurrency());\n\t\t\tconst auto &tlPrices = data.vinvoice().data().vprices().v;\n\t\t\tconst auto amount = tlPrices.empty()\n\t\t\t\t? 0\n\t\t\t\t: tlPrices.front().data().vamount().v;\n\t\t\tif (currency != ::Ui::kCreditsCurrency || !amount) {\n\t\t\t\tusing Type = Error::Type;\n\t\t\t\t_updates.fire(Error{ Type::Form, u\"Bad Stars Form.\"_q });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto invoice = InvoiceCredits{\n\t\t\t\t.session = _session,\n\t\t\t\t.randomId = 0,\n\t\t\t\t.credits = amount,\n\t\t\t\t.currency = currency,\n\t\t\t\t.amount = amount,\n\t\t\t};\n\t\t\tconst auto gift = std::get_if<InvoiceStarGift>(&_id.value);\n\t\t\tconst auto formData = CreditsFormData{\n\t\t\t\t.id = _id,\n\t\t\t\t.formId = data.vform_id().v,\n\t\t\t\t.invoice = invoice,\n\t\t\t\t.inputInvoice = inputInvoice(),\n\t\t\t\t.starGiftLimitedCount = gift ? gift->limitedCount : 0,\n\t\t\t\t.starGiftPerUserLimit = gift ? gift->perUserLimit : 0,\n\t\t\t\t.starGiftForm = true,\n\t\t\t};\n\t\t\t_updates.fire(CreditsPaymentStarted{ .data = formData });\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\thideProgress();\n\t\t_updates.fire(Error{ Error::Type::Form, error.type() });\n\t}).send();\n}\n\nvoid Form::requestReceipt() {\n\tExpects(v::is<InvoiceMessage>(_id.value));\n\n\tconst auto message = v::get<InvoiceMessage>(_id.value);\n\tshowProgress();\n\t_api.request(MTPpayments_GetPaymentReceipt(\n\t\tmessage.peer->input(),\n\t\tMTP_int(message.itemId.bare)\n\t)).done([=](const MTPpayments_PaymentReceipt &result) {\n\t\thideProgress();\n\t\tresult.match([&](const auto &data) {\n\t\t\tprocessReceipt(data);\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\thideProgress();\n\t\t_updates.fire(Error{ Error::Type::Form, error.type() });\n\t}).send();\n}\n\nvoid Form::processForm(const MTPDpayments_paymentForm &data) {\n\t_session->data().processUsers(data.vusers());\n\n\tdata.vinvoice().match([&](const auto &data) {\n\t\tprocessInvoice(data);\n\t});\n\tprocessDetails(data);\n\tif (const auto info = data.vsaved_info()) {\n\t\tinfo->match([&](const auto &data) {\n\t\t\tprocessSavedInformation(data);\n\t\t});\n\t}\n\t_paymentMethod.savedCredentials.clear();\n\t_paymentMethod.savedCredentialsIndex = 0;\n\tif (const auto credentials = data.vsaved_credentials()) {\n\t\t_paymentMethod.savedCredentials.reserve(credentials->v.size());\n\t\tfor (const auto &saved : credentials->v) {\n\t\t\t_paymentMethod.savedCredentials.push_back({\n\t\t\t\t.id = qs(saved.data().vid()),\n\t\t\t\t.title = qs(saved.data().vtitle()),\n\t\t\t});\n\t\t}\n\t\trefreshPaymentMethodDetails();\n\t}\n\tif (const auto additional = data.vadditional_methods()) {\n\t\tprocessAdditionalPaymentMethods(additional->v);\n\t}\n\tfillPaymentMethodInformation();\n\t_updates.fire(FormReady{});\n}\n\nvoid Form::processReceipt(const MTPDpayments_paymentReceipt &data) {\n\t_session->data().processUsers(data.vusers());\n\n\tdata.vinvoice().match([&](const auto &data) {\n\t\tprocessInvoice(data);\n\t});\n\tprocessDetails(data);\n\tif (const auto info = data.vinfo()) {\n\t\tinfo->match([&](const auto &data) {\n\t\t\tprocessSavedInformation(data);\n\t\t});\n\t}\n\tif (const auto shipping = data.vshipping()) {\n\t\tprocessShippingOptions({ *shipping });\n\t\tif (!_shippingOptions.list.empty()) {\n\t\t\t_shippingOptions.selectedId = _shippingOptions.list.front().id;\n\t\t}\n\t}\n\t_paymentMethod.savedCredentials = { {\n\t\t.id = \"(used)\",\n\t\t.title = qs(data.vcredentials_title()),\n\t} };\n\t_paymentMethod.savedCredentialsIndex = 0;\n\tfillPaymentMethodInformation();\n\t_updates.fire(FormReady{});\n}\n\nvoid Form::processReceipt(const MTPDpayments_paymentReceiptStars &data) {\n\t_session->data().processUsers(data.vusers());\n\n\tconst auto receiptData = CreditsReceiptData{\n\t\t.id = qs(data.vtransaction_id()),\n\t\t.title = qs(data.vtitle()),\n\t\t.description = qs(data.vdescription()),\n\t\t.photo = data.vphoto()\n\t\t\t? _session->data().photoFromWeb(\n\t\t\t\t*data.vphoto(),\n\t\t\t\tImageLocation())\n\t\t\t: nullptr,\n\t\t.peerId = peerFromUser(data.vbot_id().v),\n\t\t.credits = CreditsAmount(data.vtotal_amount().v),\n\t\t.date = data.vdate().v,\n\t};\n\t_updates.fire(CreditsReceiptReady{ .data = receiptData });\n}\n\nvoid Form::processInvoice(const MTPDinvoice &data) {\n\tconst auto suggested = data.vsuggested_tip_amounts().value_or_empty();\n\t_invoice = Ui::Invoice{\n\t\t.cover = std::move(_invoice.cover),\n\n\t\t.prices = ParsePrices(data.vprices()),\n\t\t.suggestedTips = ranges::views::all(\n\t\t\tsuggested\n\t\t) | ranges::views::transform(\n\t\t\t&MTPlong::v\n\t\t) | ranges::views::transform(\n\t\t\tParsePriceAmount\n\t\t) | ranges::to_vector,\n\t\t.tipsMax = ParsePriceAmount(data.vmax_tip_amount().value_or_empty()),\n\t\t.currency = qs(data.vcurrency()),\n\n\t\t.isNameRequested = data.is_name_requested(),\n\t\t.isPhoneRequested = data.is_phone_requested(),\n\t\t.isEmailRequested = data.is_email_requested(),\n\t\t.isShippingAddressRequested = data.is_shipping_address_requested(),\n\t\t.isRecurring = data.is_recurring(),\n\t\t.isFlexible = data.is_flexible(),\n\t\t.isTest = data.is_test(),\n\n\t\t.termsUrl = qs(data.vterms_url().value_or_empty()),\n\n\t\t.phoneSentToProvider = data.is_phone_to_provider(),\n\t\t.emailSentToProvider = data.is_email_to_provider(),\n\t};\n}\n\nvoid Form::processDetails(const MTPDpayments_paymentForm &data) {\n\tconst auto nativeParams = data.vnative_params();\n\tauto nativeParamsJson = nativeParams\n\t\t? nativeParams->match(\n\t\t\t[&](const MTPDdataJSON &data) { return data.vdata().v; })\n\t\t: QByteArray();\n\t_details = FormDetails{\n\t\t.formId = data.vform_id().v,\n\t\t.url = qs(data.vurl()),\n\t\t.nativeProvider = qs(data.vnative_provider().value_or_empty()),\n\t\t.nativeParamsJson = std::move(nativeParamsJson),\n\t\t.botId = data.vbot_id().v,\n\t\t.providerId = data.vprovider_id().v,\n\t\t.canSaveCredentials = data.is_can_save_credentials(),\n\t\t.passwordMissing = data.is_password_missing(),\n\t};\n\t_invoice.cover.title = qs(data.vtitle());\n\t_invoice.cover.description = TextUtilities::ParseEntities(\n\t\tqs(data.vdescription()),\n\t\tTextParseLinks | TextParseMultiline);\n\tif (_invoice.cover.thumbnail.isNull() && !_thumbnailLoadProcess) {\n\t\tif (const auto photo = data.vphoto()) {\n\t\t\tloadThumbnail(\n\t\t\t\t_session->data().photoFromWeb(*photo, ImageLocation()));\n\t\t}\n\t}\n\tif (const auto botId = _details.botId) {\n\t\tif (const auto bot = _session->data().userLoaded(botId)) {\n\t\t\t_invoice.cover.seller = bot->name();\n\t\t\t_details.termsBotUsername = bot->username();\n\t\t}\n\t}\n\tif (const auto providerId = _details.providerId) {\n\t\tif (const auto bot = _session->data().userLoaded(providerId)) {\n\t\t\t_invoice.provider = bot->name();\n\t\t}\n\t}\n}\n\nvoid Form::processDetails(const MTPDpayments_paymentReceipt &data) {\n\t_invoice.receipt = Ui::Receipt{\n\t\t.date = data.vdate().v,\n\t\t.totalAmount = ParsePriceAmount(data.vtotal_amount().v),\n\t\t.currency = qs(data.vcurrency()),\n\t\t.paid = true,\n\t};\n\t_details = FormDetails{\n\t\t.botId = data.vbot_id().v,\n\t\t.providerId = data.vprovider_id().v,\n\t};\n\tif (_invoice.cover.title.isEmpty()\n\t\t&& _invoice.cover.description.empty()\n\t\t&& _invoice.cover.thumbnail.isNull()\n\t\t&& !_thumbnailLoadProcess) {\n\t\t_invoice.cover = Ui::Cover{\n\t\t\t.title = qs(data.vtitle()),\n\t\t\t.description = { qs(data.vdescription()) },\n\t\t};\n\t\tif (const auto web = data.vphoto()) {\n\t\t\tif (const auto photo = _session->data().photoFromWeb(*web, {})) {\n\t\t\t\tloadThumbnail(photo);\n\t\t\t}\n\t\t}\n\t}\n\tif (_details.botId) {\n\t\tif (const auto bot = _session->data().userLoaded(_details.botId)) {\n\t\t\t_invoice.cover.seller = bot->name();\n\t\t}\n\t}\n}\n\nvoid Form::processDetails(const MTPDpayments_paymentReceiptStars &data) {\n\t_invoice.receipt = Ui::Receipt{\n\t\t.date = data.vdate().v,\n\t\t.totalAmount = ParsePriceAmount(data.vtotal_amount().v),\n\t\t.currency = qs(data.vcurrency()),\n\t\t.paid = true,\n\t};\n\t_details = FormDetails{\n\t\t.botId = data.vbot_id().v,\n\t};\n\tif (_invoice.cover.title.isEmpty()\n\t\t&& _invoice.cover.description.empty()\n\t\t&& _invoice.cover.thumbnail.isNull()\n\t\t&& !_thumbnailLoadProcess) {\n\t\t_invoice.cover = Ui::Cover{\n\t\t\t.title = qs(data.vtitle()),\n\t\t\t.description = { qs(data.vdescription()) },\n\t\t};\n\t\tif (const auto web = data.vphoto()) {\n\t\t\tif (const auto photo = _session->data().photoFromWeb(*web, {})) {\n\t\t\t\tloadThumbnail(photo);\n\t\t\t}\n\t\t}\n\t}\n\tif (_details.botId) {\n\t\tif (const auto bot = _session->data().userLoaded(_details.botId)) {\n\t\t\t_invoice.cover.seller = bot->name();\n\t\t}\n\t}\n}\n\nvoid Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) {\n\tconst auto address = data.vshipping_address();\n\t_savedInformation = _information = Ui::RequestedInformation{\n\t\t.defaultPhone = defaultPhone(),\n\t\t.defaultCountry = defaultCountry(),\n\t\t.name = qs(data.vname().value_or_empty()),\n\t\t.phone = qs(data.vphone().value_or_empty()),\n\t\t.email = qs(data.vemail().value_or_empty()),\n\t\t.shippingAddress = address ? ParseAddress(*address) : Ui::Address(),\n\t};\n}\n\nvoid Form::processAdditionalPaymentMethods(\n\t\tconst QVector<MTPPaymentFormMethod> &list) {\n\t_paymentMethod.ui.additionalMethods = ranges::views::all(\n\t\tlist\n\t) | ranges::views::transform([](const MTPPaymentFormMethod &method) {\n\t\treturn Ui::PaymentMethodAdditional{\n\t\t\t.title = qs(method.data().vtitle()),\n\t\t\t.url = qs(method.data().vurl()),\n\t\t};\n\t}) | ranges::to_vector;\n}\n\nvoid Form::refreshPaymentMethodDetails() {\n\trefreshSavedPaymentMethodDetails();\n\t_paymentMethod.ui.provider = _invoice.provider;\n\t_paymentMethod.ui.native.defaultCountry = defaultCountry();\n\t_paymentMethod.ui.canSaveInformation\n\t\t= _paymentMethod.ui.native.canSaveInformation\n\t\t= _details.canSaveCredentials || _details.passwordMissing;\n}\n\nvoid Form::refreshSavedPaymentMethodDetails() {\n\tconst auto &list = _paymentMethod.savedCredentials;\n\tconst auto index = _paymentMethod.savedCredentialsIndex;\n\tconst auto &entered = _paymentMethod.newCredentials;\n\t_paymentMethod.ui.savedMethods.clear();\n\tif (entered) {\n\t\t_paymentMethod.ui.savedMethods.push_back({ .title = entered.title });\n\t}\n\tfor (const auto &item : list) {\n\t\t_paymentMethod.ui.savedMethods.push_back({\n\t\t\t.id = item.id,\n\t\t\t.title = item.title,\n\t\t});\n\t}\n\t_paymentMethod.ui.savedMethodIndex = (index < list.size())\n\t\t? (index + (entered ? 1 : 0))\n\t\t: 0;\n}\n\nQString Form::defaultPhone() const {\n\treturn _session->user()->phone();\n}\n\nQString Form::defaultCountry() const {\n\treturn Countries::Instance().countryISO2ByPhone(defaultPhone());\n}\n\nvoid Form::fillPaymentMethodInformation() {\n\t_paymentMethod.native = NativePaymentMethod();\n\t_paymentMethod.ui.native = Ui::NativeMethodDetails();\n\t_paymentMethod.ui.url = _details.url;\n\n\t//AssertIsDebug();\n\t//static auto counter = 0; // #TODO payments test both native and webview.\n\tif (!_details.nativeProvider.isEmpty()/* && ((++counter) % 2)*/) {\n\t\tauto error = QJsonParseError();\n\t\tauto document = QJsonDocument::fromJson(\n\t\t\t_details.nativeParamsJson,\n\t\t\t&error);\n\t\tif (error.error != QJsonParseError::NoError) {\n\t\t\tLOG((\"Payment Error: Could not decode native_params, error %1: %2\"\n\t\t\t\t).arg(error.error\n\t\t\t\t).arg(error.errorString()));\n\t\t} else if (!document.isObject()) {\n\t\t\tLOG((\"Payment Error: Not an object in native_params.\"));\n\t\t} else {\n\t\t\tconst auto object = document.object();\n\t\t\tif (_details.nativeProvider == \"stripe\") {\n\t\t\t\tfillStripeNativeMethod(object);\n\t\t\t} else if (_details.nativeProvider == \"smartglocal\") {\n\t\t\t\tfillSmartGlocalNativeMethod(object);\n\t\t\t} else {\n\t\t\t\tLOG((\"Payment Error: Unknown native provider '%1'.\"\n\t\t\t\t\t).arg(_details.nativeProvider));\n\t\t\t}\n\t\t}\n\t}\n\trefreshPaymentMethodDetails();\n}\n\nvoid Form::fillStripeNativeMethod(QJsonObject object) {\n\tconst auto value = [&](QStringView key) {\n\t\treturn object.value(key);\n\t};\n\tconst auto key = value(u\"publishable_key\").toString();\n\tif (key.isEmpty()) {\n\t\tLOG((\"Payment Error: No publishable_key in stripe native_params.\"));\n\t\treturn;\n\t}\n\t_paymentMethod.native = NativePaymentMethod{\n\t\t.data = StripePaymentMethod{\n\t\t\t.publishableKey = key,\n\t\t},\n\t};\n\t_paymentMethod.ui.native = Ui::NativeMethodDetails{\n\t\t.supported = true,\n\t\t.needCountry = value(u\"need_country\").toBool(),\n\t\t.needZip = value(u\"need_zip\").toBool(),\n\t\t.needCardholderName = value(u\"need_cardholder_name\").toBool(),\n\t};\n}\n\nvoid Form::fillSmartGlocalNativeMethod(QJsonObject object) {\n\tconst auto value = [&](QStringView key) {\n\t\treturn object.value(key);\n\t};\n\tconst auto key = value(u\"public_token\").toString();\n\tif (key.isEmpty()) {\n\t\tLOG((\"Payment Error: \"\n\t\t\t\"No public_token in smartglocal native_params.\"));\n\t\treturn;\n\t}\n\t_paymentMethod.native = NativePaymentMethod{\n\t\t.data = SmartGlocalPaymentMethod{\n\t\t\t.publicToken = key,\n\t\t\t.tokenizeUrl = value(u\"tokenize_url\").toString(),\n\t\t},\n\t};\n\t_paymentMethod.ui.native = Ui::NativeMethodDetails{\n\t\t.supported = true,\n\t\t.needCountry = false,\n\t\t.needZip = false,\n\t\t.needCardholderName = false,\n\t};\n}\n\nvoid Form::submit() {\n\tExpects(_paymentMethod.newCredentials\n\t\t|| (_paymentMethod.savedCredentialsIndex\n\t\t\t< _paymentMethod.savedCredentials.size()));\n\n\tconst auto index = _paymentMethod.savedCredentialsIndex;\n\tconst auto &list = _paymentMethod.savedCredentials;\n\n\tconst auto password = (index < list.size())\n\t\t? _session->validTmpPassword()\n\t\t: QByteArray();\n\tif (index < list.size() && password.isEmpty()) {\n\t\t_updates.fire(TmpPasswordRequired{});\n\t\treturn;\n\t} else if (!_session->local().isPeerTrustedPayment(_details.botId)) {\n\t\t_updates.fire(BotTrustRequired{\n\t\t\t.bot = _session->data().user(_details.botId),\n\t\t\t.provider = _session->data().user(_details.providerId),\n\t\t});\n\t\treturn;\n\t}\n\n\tusing Flag = MTPpayments_SendPaymentForm::Flag;\n\tshowProgress();\n\t_api.request(MTPpayments_SendPaymentForm(\n\t\tMTP_flags((_requestedInformationId.isEmpty()\n\t\t\t? Flag(0)\n\t\t\t: Flag::f_requested_info_id)\n\t\t\t| (_shippingOptions.selectedId.isEmpty()\n\t\t\t\t? Flag(0)\n\t\t\t\t: Flag::f_shipping_option_id)\n\t\t\t| (_invoice.tipsMax > 0 ? Flag::f_tip_amount : Flag(0))),\n\t\tMTP_long(_details.formId),\n\t\tinputInvoice(),\n\t\tMTP_string(_requestedInformationId),\n\t\tMTP_string(_shippingOptions.selectedId),\n\t\t(index < list.size()\n\t\t\t? MTP_inputPaymentCredentialsSaved(\n\t\t\t\tMTP_string(list[index].id),\n\t\t\t\tMTP_bytes(password))\n\t\t\t: MTP_inputPaymentCredentials(\n\t\t\t\tMTP_flags((_paymentMethod.newCredentials.saveOnServer\n\t\t\t\t\t&& _details.canSaveCredentials)\n\t\t\t\t\t? MTPDinputPaymentCredentials::Flag::f_save\n\t\t\t\t\t: MTPDinputPaymentCredentials::Flag(0)),\n\t\t\t\tMTP_dataJSON(MTP_bytes(_paymentMethod.newCredentials.data)))),\n\t\tMTP_long(_invoice.tipsSelected)\n\t)).done([=](const MTPpayments_PaymentResult &result) {\n\t\thideProgress();\n\t\tresult.match([&](const MTPDpayments_paymentResult &data) {\n\t\t\t_updates.fire(PaymentFinished{ data.vupdates() });\n\t\t}, [&](const MTPDpayments_paymentVerificationNeeded &data) {\n\t\t\t_updates.fire(VerificationNeeded{ qs(data.vurl()) });\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\thideProgress();\n\t\t_updates.fire(Error{ Error::Type::Send, error.type() });\n\t}).send();\n}\n\nvoid Form::submit(const Core::CloudPasswordResult &result) {\n\tif (_passwordRequestId) {\n\t\treturn;\n\t}\n\t_passwordRequestId = _api.request(MTPaccount_GetTmpPassword(\n\t\tresult.result,\n\t\tMTP_int(kPasswordPeriod)\n\t)).done([=](const MTPaccount_TmpPassword &result) {\n\t\t_passwordRequestId = 0;\n\t\tresult.match([&](const MTPDaccount_tmpPassword &data) {\n\t\t\t_session->setTmpPassword(\n\t\t\t\tdata.vtmp_password().v,\n\t\t\t\tdata.vvalid_until().v);\n\t\t\tsubmit();\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\t_passwordRequestId = 0;\n\t\t_updates.fire(Error{ Error::Type::TmpPassword, error.type() });\n\t}).send();\n}\n\nstd::optional<QDate> Form::overrideExpireDateThreshold() const {\n\tconst auto phone = _session->user()->phone();\n\treturn phone.startsWith('7')\n\t\t? QDate(2022, 2, 1)\n\t\t: std::optional<QDate>();\n}\n\nvoid Form::validateInformation(const Ui::RequestedInformation &information) {\n\tif (_validateRequestId) {\n\t\tif (_validatedInformation == information) {\n\t\t\treturn;\n\t\t}\n\t\thideProgress();\n\t\t_api.request(base::take(_validateRequestId)).cancel();\n\t}\n\t_validatedInformation = information;\n\tif (!validateInformationLocal(information)) {\n\t\treturn;\n\t}\n\n\tAssert(!_invoice.isShippingAddressRequested\n\t\t|| information.shippingAddress);\n\tAssert(!_invoice.isNameRequested || !information.name.isEmpty());\n\tAssert(!_invoice.isEmailRequested || !information.email.isEmpty());\n\tAssert(!_invoice.isPhoneRequested || !information.phone.isEmpty());\n\n\tshowProgress();\n\tusing Flag = MTPpayments_ValidateRequestedInfo::Flag;\n\t_validateRequestId = _api.request(MTPpayments_ValidateRequestedInfo(\n\t\tMTP_flags(information.save ? Flag::f_save : Flag(0)),\n\t\tinputInvoice(),\n\t\tSerialize(information)\n\t)).done([=](const MTPpayments_ValidatedRequestedInfo &result) {\n\t\thideProgress();\n\t\t_validateRequestId = 0;\n\t\tconst auto oldSelectedId = _shippingOptions.selectedId;\n\t\tresult.match([&](const MTPDpayments_validatedRequestedInfo &data) {\n\t\t\t_requestedInformationId = data.vid().value_or_empty();\n\t\t\tprocessShippingOptions(\n\t\t\t\tdata.vshipping_options().value_or_empty());\n\t\t});\n\t\t_shippingOptions.selectedId = ranges::contains(\n\t\t\t_shippingOptions.list,\n\t\t\toldSelectedId,\n\t\t\t&Ui::ShippingOption::id\n\t\t) ? oldSelectedId : QString();\n\t\tif (_shippingOptions.selectedId.isEmpty()\n\t\t\t&& _shippingOptions.list.size() == 1) {\n\t\t\t_shippingOptions.selectedId = _shippingOptions.list.front().id;\n\t\t}\n\t\t_information = _validatedInformation;\n\t\tif (_information.save) {\n\t\t\t_savedInformation = _information;\n\t\t}\n\t\t_updates.fire(ValidateFinished{});\n\t}).fail([=](const MTP::Error &error) {\n\t\thideProgress();\n\t\t_validateRequestId = 0;\n\t\t_updates.fire(Error{ Error::Type::Validate, error.type() });\n\t}).send();\n}\n\nbool Form::hasChanges() const {\n\tconst auto &information = _validateRequestId\n\t\t? _validatedInformation\n\t\t: _information;\n\treturn (information != _savedInformation)\n\t\t|| (_stripe != nullptr)\n\t\t|| (_smartglocal != nullptr)\n\t\t|| (!_paymentMethod.newCredentials.empty()\n\t\t\t&& (_paymentMethod.savedCredentialsIndex\n\t\t\t\t>= _paymentMethod.savedCredentials.size()));\n}\n\nbool Form::validateInformationLocal(\n\t\tconst Ui::RequestedInformation &information) const {\n\tif (const auto error = informationErrorLocal(information)) {\n\t\t_updates.fire_copy(error);\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nError Form::informationErrorLocal(\n\t\tconst Ui::RequestedInformation &information) const {\n\tauto errors = QStringList();\n\tconst auto push = [&](const QString &id) {\n\t\terrors.push_back(id);\n\t};\n\tif (_invoice.isShippingAddressRequested) {\n\t\tif (information.shippingAddress.address1.isEmpty()) {\n\t\t\tpush(u\"ADDRESS_STREET_LINE1_INVALID\"_q);\n\t\t}\n\t\tif (information.shippingAddress.city.isEmpty()) {\n\t\t\tpush(u\"ADDRESS_CITY_INVALID\"_q);\n\t\t}\n\t\tif (information.shippingAddress.countryIso2.isEmpty()) {\n\t\t\tpush(u\"ADDRESS_COUNTRY_INVALID\"_q);\n\t\t}\n\t}\n\tif (_invoice.isNameRequested && information.name.isEmpty()) {\n\t\tpush(u\"REQ_INFO_NAME_INVALID\"_q);\n\t}\n\tif (_invoice.isEmailRequested && information.email.isEmpty()) {\n\t\tpush(u\"REQ_INFO_EMAIL_INVALID\"_q);\n\t}\n\tif (_invoice.isPhoneRequested && information.phone.isEmpty()) {\n\t\tpush(u\"REQ_INFO_PHONE_INVALID\"_q);\n\t}\n\tif (!errors.isEmpty()) {\n\t\treturn Error{ Error::Type::Validate, errors.front() };\n\t}\n\treturn Error();\n}\n\nvoid Form::validateCard(\n\t\tconst Ui::UncheckedCardDetails &details,\n\t\tbool saveInformation) {\n\tExpects(!v::is_null(_paymentMethod.native.data));\n\n\tif (!validateCardLocal(details, overrideExpireDateThreshold())) {\n\t\treturn;\n\t}\n\tconst auto &native = _paymentMethod.native.data;\n\tif (const auto smartglocal = std::get_if<SmartGlocalPaymentMethod>(\n\t\t\t&native)) {\n\t\tvalidateCard(*smartglocal, details, saveInformation);\n\t} else if (const auto stripe = std::get_if<StripePaymentMethod>(&native)) {\n\t\tvalidateCard(*stripe, details, saveInformation);\n\t} else {\n\t\tUnexpected(\"Native payment provider in Form::validateCard.\");\n\t}\n}\n\nbool Form::validateCardLocal(\n\t\tconst Ui::UncheckedCardDetails &details,\n\t\tconst std::optional<QDate> &overrideExpireDateThreshold) const {\n\tif (auto error = cardErrorLocal(details, overrideExpireDateThreshold)) {\n\t\t_updates.fire(std::move(error));\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nError Form::cardErrorLocal(\n\t\tconst Ui::UncheckedCardDetails &details,\n\t\tconst std::optional<QDate> &overrideExpireDateThreshold) const {\n\tusing namespace Stripe;\n\n\tauto errors = QStringList();\n\tconst auto push = [&](const QString &id) {\n\t\terrors.push_back(id);\n\t};\n\tconst auto kValid = ValidationState::Valid;\n\tif (ValidateCard(details.number).state != kValid) {\n\t\tpush(u\"LOCAL_CARD_NUMBER_INVALID\"_q);\n\t}\n\tif (ValidateParsedExpireDate(\n\t\tdetails.expireMonth,\n\t\tdetails.expireYear,\n\t\toverrideExpireDateThreshold\n\t) != kValid) {\n\t\tpush(u\"LOCAL_CARD_EXPIRE_DATE_INVALID\"_q);\n\t}\n\tif (ValidateCvc(details.number, details.cvc).state != kValid) {\n\t\tpush(u\"LOCAL_CARD_CVC_INVALID\"_q);\n\t}\n\tif (_paymentMethod.ui.native.needCardholderName\n\t\t&& details.cardholderName.isEmpty()) {\n\t\tpush(u\"LOCAL_CARD_HOLDER_NAME_INVALID\"_q);\n\t}\n\tif (_paymentMethod.ui.native.needCountry\n\t\t&& details.addressCountry.isEmpty()) {\n\t\tpush(u\"LOCAL_CARD_BILLING_COUNTRY_INVALID\"_q);\n\t}\n\tif (_paymentMethod.ui.native.needZip\n\t\t&& details.addressZip.isEmpty()) {\n\t\tpush(u\"LOCAL_CARD_BILLING_ZIP_INVALID\"_q);\n\t}\n\tif (!errors.isEmpty()) {\n\t\treturn Error{ Error::Type::Validate, errors.front() };\n\t}\n\treturn Error();\n}\n\nvoid Form::validateCard(\n\t\tconst StripePaymentMethod &method,\n\t\tconst Ui::UncheckedCardDetails &details,\n\t\tbool saveInformation) {\n\tExpects(!method.publishableKey.isEmpty());\n\n\tif (_stripe) {\n\t\treturn;\n\t}\n\tauto configuration = Stripe::PaymentConfiguration{\n\t\t.publishableKey = method.publishableKey,\n\t\t.companyName = \"Telegram\",\n\t};\n\t_stripe = std::make_unique<Stripe::APIClient>(std::move(configuration));\n\tauto card = Stripe::CardParams{\n\t\t.number = details.number,\n\t\t.expMonth = details.expireMonth,\n\t\t.expYear = details.expireYear,\n\t\t.cvc = details.cvc,\n\t\t.name = details.cardholderName,\n\t\t.addressZip = details.addressZip,\n\t\t.addressCountry = details.addressCountry,\n\t};\n\tshowProgress();\n\t_stripe->createTokenWithCard(std::move(card), crl::guard(this, [=](\n\t\t\tStripe::Token token,\n\t\t\tStripe::Error error) {\n\t\thideProgress();\n\t\t_stripe = nullptr;\n\n\t\tif (error) {\n\t\t\tLOG((\"Stripe Error %1: %2 (%3)\"\n\t\t\t\t).arg(int(error.code())\n\t\t\t\t).arg(error.description(), error.message()));\n\t\t\t_updates.fire(Error{ Error::Type::Stripe, error.description() });\n\t\t} else {\n\t\t\tsetPaymentCredentials({\n\t\t\t\t.title = CardTitle(token.card()),\n\t\t\t\t.data = QJsonDocument(QJsonObject{\n\t\t\t\t\t{ \"type\", \"card\" },\n\t\t\t\t\t{ \"id\", token.tokenId() },\n\t\t\t\t}).toJson(QJsonDocument::Compact),\n\t\t\t\t.saveOnServer = saveInformation,\n\t\t\t});\n\t\t}\n\t}));\n}\n\nvoid Form::validateCard(\n\t\tconst SmartGlocalPaymentMethod &method,\n\t\tconst Ui::UncheckedCardDetails &details,\n\t\tbool saveInformation) {\n\tExpects(!method.publicToken.isEmpty());\n\n\tif (_smartglocal) {\n\t\treturn;\n\t}\n\tauto configuration = SmartGlocal::PaymentConfiguration{\n\t\t.publicToken = method.publicToken,\n\t\t.tokenizeUrl = method.tokenizeUrl,\n\t\t.isTest = _invoice.isTest,\n\t};\n\t_smartglocal = std::make_unique<SmartGlocal::APIClient>(\n\t\tstd::move(configuration));\n\tauto card = Stripe::CardParams{\n\t\t.number = details.number,\n\t\t.expMonth = details.expireMonth,\n\t\t.expYear = details.expireYear,\n\t\t.cvc = details.cvc,\n\t\t.name = details.cardholderName,\n\t\t.addressZip = details.addressZip,\n\t\t.addressCountry = details.addressCountry,\n\t};\n\tshowProgress();\n\t_smartglocal->createTokenWithCard(std::move(card), crl::guard(this, [=](\n\t\t\tSmartGlocal::Token token,\n\t\t\tSmartGlocal::Error error) {\n\t\thideProgress();\n\t\t_smartglocal = nullptr;\n\n\t\tif (error) {\n\t\t\tLOG((\"SmartGlocal Error %1: %2 (%3)\"\n\t\t\t\t).arg(int(error.code())\n\t\t\t\t).arg(error.description(), error.message()));\n\t\t\t_updates.fire(Error{\n\t\t\t\tError::Type::SmartGlocal,\n\t\t\t\terror.description(),\n\t\t\t});\n\t\t} else {\n\t\t\tsetPaymentCredentials({\n\t\t\t\t.title = CardTitle(token.card()),\n\t\t\t\t.data = QJsonDocument(QJsonObject{\n\t\t\t\t\t{ \"token\", token.tokenId() },\n\t\t\t\t\t{ \"type\", \"card\" },\n\t\t\t\t}).toJson(QJsonDocument::Compact),\n\t\t\t\t.saveOnServer = saveInformation,\n\t\t\t});\n\t\t}\n\t}));\n}\n\nvoid Form::setPaymentCredentials(const NewCredentials &credentials) {\n\tExpects(!credentials.empty());\n\n\t_paymentMethod.newCredentials = credentials;\n\t_paymentMethod.savedCredentialsIndex\n\t\t= _paymentMethod.savedCredentials.size();\n\trefreshSavedPaymentMethodDetails();\n\tconst auto requestNewPassword = credentials.saveOnServer\n\t\t&& !_details.canSaveCredentials\n\t\t&& _details.passwordMissing;\n\t_updates.fire(PaymentMethodUpdate{ requestNewPassword });\n}\n\nvoid Form::chooseSavedMethod(const QString &id) {\n\tauto &index = _paymentMethod.savedCredentialsIndex;\n\tconst auto &list = _paymentMethod.savedCredentials;\n\tif (id.isEmpty() && _paymentMethod.newCredentials) {\n\t\tindex = list.size();\n\t} else {\n\t\tconst auto i = ranges::find(list, id, &SavedCredentials::id);\n\t\tindex = (i != end(list)) ? (i - begin(list)) : 0;\n\t}\n\trefreshSavedPaymentMethodDetails();\n\tconst auto requestNewPassword = (index == list.size())\n\t\t&& _paymentMethod.newCredentials\n\t\t&& _paymentMethod.newCredentials.saveOnServer\n\t\t&& !_details.canSaveCredentials\n\t\t&& _details.passwordMissing;\n\t_updates.fire(PaymentMethodUpdate{ requestNewPassword });\n}\n\nvoid Form::setHasPassword(bool has) {\n\tif (_details.passwordMissing) {\n\t\t_details.canSaveCredentials = has;\n\t}\n}\n\nvoid Form::setShippingOption(const QString &id) {\n\t_shippingOptions.selectedId = id;\n}\n\nvoid Form::setTips(int64 value) {\n\t_invoice.tipsSelected = std::min(value, _invoice.tipsMax);\n}\n\nvoid Form::acceptTerms() {\n\t_details.termsAccepted = true;\n}\n\nvoid Form::trustBot() {\n\t_session->local().markPeerTrustedPayment(_details.botId);\n}\n\nvoid Form::processShippingOptions(const QVector<MTPShippingOption> &data) {\n\tconst auto currency = _invoice.currency;\n\t_shippingOptions = Ui::ShippingOptions{ currency, ranges::views::all(\n\t\tdata\n\t) | ranges::views::transform([](const MTPShippingOption &option) {\n\t\treturn option.match([](const MTPDshippingOption &data) {\n\t\t\treturn Ui::ShippingOption{\n\t\t\t\t.id = qs(data.vid()),\n\t\t\t\t.title = qs(data.vtitle()),\n\t\t\t\t.prices = ParsePrices(data.vprices()),\n\t\t\t};\n\t\t});\n\t}) | ranges::to_vector };\n\t_shippingOptions.currency = _invoice.currency;\n}\n\n} // namespace Payments\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/payments_form.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"payments/ui/payments_panel_data.h\"\n#include \"base/weak_ptr.h\"\n#include \"mtproto/sender.h\"\n\nclass Image;\nclass QJsonObject;\n\nnamespace Core {\nstruct CloudPasswordResult;\n} // namespace Core\n\nnamespace Stripe {\nclass APIClient;\n} // namespace Stripe\n\nnamespace SmartGlocal {\nclass APIClient;\n} // namespace SmartGlocal\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\nclass PhotoMedia;\n} // namespace Data\n\nnamespace Payments {\n\nenum class Mode;\n\nstruct FormDetails {\n\tuint64 formId = 0;\n\tQString url;\n\tQString nativeProvider;\n\tQString termsBotUsername;\n\tQByteArray nativeParamsJson;\n\tUserId botId = 0;\n\tUserId providerId = 0;\n\tbool canSaveCredentials = false;\n\tbool passwordMissing = false;\n\tbool termsAccepted = false;\n\n\t[[nodiscard]] bool valid() const {\n\t\treturn !url.isEmpty();\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn valid();\n\t}\n};\n\nstruct ThumbnailLoadProcess {\n\tstd::shared_ptr<Data::PhotoMedia> view;\n\tbool blurredSet = false;\n\trpl::lifetime lifetime;\n};\n\nstruct SavedCredentials {\n\tQString id;\n\tQString title;\n\n\t[[nodiscard]] bool valid() const {\n\t\treturn !id.isEmpty();\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn valid();\n\t}\n};\n\nstruct NewCredentials {\n\tQString title;\n\tQByteArray data;\n\tbool saveOnServer = false;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn data.isEmpty();\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n};\n\nstruct StripePaymentMethod {\n\tQString publishableKey;\n};\n\nstruct SmartGlocalPaymentMethod {\n\tQString publicToken;\n\tQString tokenizeUrl;\n};\n\nstruct NativePaymentMethod {\n\tstd::variant<\n\t\tv::null_t,\n\t\tStripePaymentMethod,\n\t\tSmartGlocalPaymentMethod> data;\n\n\t[[nodiscard]] bool valid() const {\n\t\treturn !v::is_null(data);\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn valid();\n\t}\n};\n\nstruct PaymentMethod {\n\tNativePaymentMethod native;\n\tstd::vector<SavedCredentials> savedCredentials;\n\tint savedCredentialsIndex = 0;\n\tNewCredentials newCredentials;\n\tUi::PaymentMethodDetails ui;\n};\n\nstruct InvoiceMessage {\n\tnot_null<PeerData*> peer;\n\tMsgId itemId = 0;\n};\n\nstruct InvoiceSlug {\n\tnot_null<Main::Session*> session;\n\tQString slug;\n};\n\nstruct InvoicePremiumGiftCodeGiveaway {\n\tnot_null<ChannelData*> boostPeer;\n\tstd::vector<not_null<ChannelData*>> additionalChannels;\n\tstd::vector<QString> countries;\n\tQString additionalPrize;\n\tTimeId untilDate = 0;\n\tbool onlyNewSubscribers = false;\n\tbool showWinners = false;\n};\n\nstruct InvoicePremiumGiftCodeUsers {\n\tstd::vector<not_null<UserData*>> users;\n\tChannelData *boostPeer = nullptr;\n\tTextWithEntities message;\n};\n\nstruct InvoicePremiumGiftCode {\n\tstd::variant<\n\t\tInvoicePremiumGiftCodeUsers,\n\t\tInvoicePremiumGiftCodeGiveaway> purpose;\n\n\tQString currency;\n\tQString storeProduct;\n\tstd::optional<uint64> giveawayCredits;\n\tuint64 randomId = 0;\n\tuint64 amount = 0;\n\tint storeQuantity = 0;\n\tint users = 0;\n\tint months = 0;\n};\n\nstruct InvoiceCredits {\n\tnot_null<Main::Session*> session;\n\tuint64 randomId = 0;\n\tuint64 credits = 0;\n\tQString product;\n\tQString currency;\n\tuint64 amount = 0;\n\tbool extended = false;\n\tPeerId giftPeerId = PeerId(0);\n\tint subscriptionPeriod = 0;\n\tPeerId spendPurposePeerId = PeerId(0);\n};\n\nstruct InvoiceStarGift {\n\tuint64 giftId = 0;\n\tuint64 randomId = 0;\n\tTextWithEntities message;\n\tnot_null<PeerData*> recipient;\n\tint limitedCount = 0;\n\tint perUserLimit = 0;\n\tbool anonymous = false;\n\tbool upgraded = false;\n};\n\nstruct InvoiceId {\n\tstd::variant<\n\t\tInvoiceMessage,\n\t\tInvoiceSlug,\n\t\tInvoicePremiumGiftCode,\n\t\tInvoiceCredits,\n\t\tInvoiceStarGift> value;\n};\n\nstruct CreditsFormData {\n\tInvoiceId id;\n\tuint64 formId = 0;\n\tuint64 botId = 0;\n\tQString title;\n\tQString description;\n\tPhotoData *photo = nullptr;\n\tInvoiceCredits invoice;\n\tMTPInputInvoice inputInvoice;\n\tint starGiftLimitedCount = 0;\n\tint starGiftPerUserLimit = 0;\n\tbool starGiftForm = false;\n};\n\nstruct CreditsReceiptData {\n\tQString id;\n\tQString title;\n\tQString description;\n\tPhotoData *photo = nullptr;\n\tPeerId peerId = PeerId(0);\n\tCreditsAmount credits;\n\tTimeId date = 0;\n};\n\nstruct ToggleProgress {\n\tbool shown = true;\n};\nstruct FormReady {};\nstruct ThumbnailUpdated {\n\tQImage thumbnail;\n};\nstruct ValidateFinished {};\nstruct PaymentMethodUpdate {\n\tbool requestNewPassword = false;\n};\nstruct VerificationNeeded {\n\tQString url;\n};\nstruct TmpPasswordRequired {};\nstruct BotTrustRequired {\n\tnot_null<UserData*> bot;\n\tnot_null<UserData*> provider;\n};\nstruct PaymentFinished {\n\tMTPUpdates updates;\n};\nstruct CreditsPaymentStarted {\n\tCreditsFormData data;\n};\nstruct CreditsReceiptReady {\n\tCreditsReceiptData data;\n};\nstruct Error {\n\tenum class Type {\n\t\tNone,\n\t\tForm,\n\t\tValidate,\n\t\tStripe,\n\t\tSmartGlocal,\n\t\tTmpPassword,\n\t\tSend,\n\t};\n\tType type = Type::None;\n\tQString id;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn (type == Type::None);\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n};\n\nstruct FormUpdate : std::variant<\n\tToggleProgress,\n\tFormReady,\n\tThumbnailUpdated,\n\tValidateFinished,\n\tPaymentMethodUpdate,\n\tVerificationNeeded,\n\tTmpPasswordRequired,\n\tBotTrustRequired,\n\tPaymentFinished,\n\tCreditsPaymentStarted,\n\tCreditsReceiptReady,\n\tError> {\n\tusing variant::variant;\n};\n\n[[nodiscard]] not_null<Main::Session*> SessionFromId(const InvoiceId &id);\n\n[[nodiscard]] MTPinputStorePaymentPurpose InvoicePremiumGiftCodeGiveawayToTL(\n\tconst InvoicePremiumGiftCode &invoice);\n[[nodiscard]] MTPinputStorePaymentPurpose InvoiceCreditsGiveawayToTL(\n\tconst InvoicePremiumGiftCode &invoice);\n\n[[nodiscard]] bool IsPremiumForStarsInvoice(const InvoiceId &id);\n\nclass Form final : public base::has_weak_ptr {\npublic:\n\tForm(InvoiceId id, bool receipt);\n\t~Form();\n\n\t[[nodiscard]] const Ui::Invoice &invoice() const {\n\t\treturn _invoice;\n\t}\n\t[[nodiscard]] const FormDetails &details() const {\n\t\treturn _details;\n\t}\n\t[[nodiscard]] const Ui::RequestedInformation &information() const {\n\t\treturn _information;\n\t}\n\t[[nodiscard]] const PaymentMethod &paymentMethod() const {\n\t\treturn _paymentMethod;\n\t}\n\t[[nodiscard]] const Ui::ShippingOptions &shippingOptions() const {\n\t\treturn _shippingOptions;\n\t}\n\t[[nodiscard]] bool hasChanges() const;\n\n\t[[nodiscard]] rpl::producer<FormUpdate> updates() const {\n\t\treturn _updates.events();\n\t}\n\n\t[[nodiscard]] std::optional<QDate> overrideExpireDateThreshold() const;\n\n\tvoid validateInformation(const Ui::RequestedInformation &information);\n\tvoid validateCard(\n\t\tconst Ui::UncheckedCardDetails &details,\n\t\tbool saveInformation);\n\tvoid setPaymentCredentials(const NewCredentials &credentials);\n\tvoid chooseSavedMethod(const QString &id);\n\tvoid setHasPassword(bool has);\n\tvoid setShippingOption(const QString &id);\n\tvoid setTips(int64 value);\n\tvoid acceptTerms();\n\tvoid trustBot();\n\tvoid submit();\n\tvoid submit(const Core::CloudPasswordResult &result);\n\nprivate:\n\tvoid fillInvoiceFromMessage();\n\tvoid showProgress();\n\tvoid hideProgress();\n\n\t[[nodiscard]] Data::FileOrigin thumbnailFileOrigin() const;\n\tvoid loadThumbnail(not_null<PhotoData*> photo);\n\t[[nodiscard]] QImage prepareGoodThumbnail(\n\t\tconst std::shared_ptr<Data::PhotoMedia> &view) const;\n\t[[nodiscard]] QImage prepareBlurredThumbnail(\n\t\tconst std::shared_ptr<Data::PhotoMedia> &view) const;\n\t[[nodiscard]] QImage prepareThumbnail(\n\t\tnot_null<const Image*> image,\n\t\tbool blurred = false) const;\n\t[[nodiscard]] QImage prepareEmptyThumbnail() const;\n\n\tvoid requestForm();\n\tvoid requestReceipt();\n\tvoid processForm(const MTPDpayments_paymentForm &data);\n\tvoid processReceipt(const MTPDpayments_paymentReceipt &data);\n\tvoid processReceipt(const MTPDpayments_paymentReceiptStars &data);\n\tvoid processInvoice(const MTPDinvoice &data);\n\tvoid processDetails(const MTPDpayments_paymentForm &data);\n\tvoid processDetails(const MTPDpayments_paymentReceipt &data);\n\tvoid processDetails(const MTPDpayments_paymentReceiptStars &data);\n\tvoid processSavedInformation(const MTPDpaymentRequestedInfo &data);\n\tvoid processAdditionalPaymentMethods(\n\t\tconst QVector<MTPPaymentFormMethod> &list);\n\tvoid processShippingOptions(const QVector<MTPShippingOption> &data);\n\tvoid fillPaymentMethodInformation();\n\tvoid fillStripeNativeMethod(QJsonObject object);\n\tvoid fillSmartGlocalNativeMethod(QJsonObject object);\n\tvoid refreshPaymentMethodDetails();\n\tvoid refreshSavedPaymentMethodDetails();\n\t[[nodiscard]] QString defaultPhone() const;\n\t[[nodiscard]] QString defaultCountry() const;\n\n\t[[nodiscard]] MTPInputInvoice inputInvoice() const;\n\n\tvoid validateCard(\n\t\tconst StripePaymentMethod &method,\n\t\tconst Ui::UncheckedCardDetails &details,\n\t\tbool saveInformation);\n\tvoid validateCard(\n\t\tconst SmartGlocalPaymentMethod &method,\n\t\tconst Ui::UncheckedCardDetails &details,\n\t\tbool saveInformation);\n\n\tbool validateInformationLocal(\n\t\tconst Ui::RequestedInformation &information) const;\n\t[[nodiscard]] Error informationErrorLocal(\n\t\tconst Ui::RequestedInformation &information) const;\n\n\tbool validateCardLocal(\n\t\tconst Ui::UncheckedCardDetails &details,\n\t\tconst std::optional<QDate> &overrideExpireDateThreshold) const;\n\t[[nodiscard]] Error cardErrorLocal(\n\t\tconst Ui::UncheckedCardDetails &details,\n\t\tconst std::optional<QDate> &overrideExpireDateThreshold) const;\n\n\tconst InvoiceId _id;\n\tconst not_null<Main::Session*> _session;\n\n\tMTP::Sender _api;\n\tbool _receiptMode = false;\n\n\tUi::Invoice _invoice;\n\tstd::unique_ptr<ThumbnailLoadProcess> _thumbnailLoadProcess;\n\tFormDetails _details;\n\tUi::RequestedInformation _savedInformation;\n\tUi::RequestedInformation _information;\n\tPaymentMethod _paymentMethod;\n\n\tUi::RequestedInformation _validatedInformation;\n\tmtpRequestId _validateRequestId = 0;\n\tmtpRequestId _passwordRequestId = 0;\n\n\tstd::unique_ptr<Stripe::APIClient> _stripe;\n\tstd::unique_ptr<SmartGlocal::APIClient> _smartglocal;\n\n\tUi::ShippingOptions _shippingOptions;\n\tQString _requestedInformationId;\n\n\trpl::event_stream<FormUpdate> _updates;\n\n};\n\n} // namespace Payments\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/payments_non_panel_process.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"payments/payments_non_panel_process.h\"\n\n#include \"api/api_credits.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/send_credits_box.h\"\n#include \"data/components/credits.h\"\n#include \"data/data_credits.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_user.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"payments/payments_checkout_process.h\" // NonPanelPaymentForm.\n#include \"payments/payments_form.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"ui/boxes/boost_box.h\" // Ui::StartFireworks.\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace Payments {\n\nbool IsCreditsInvoice(not_null<HistoryItem*> item) {\n\tif (const auto payment = item->Get<HistoryServicePayment>()) {\n\t\treturn payment->isCreditsCurrency;\n\t}\n\tconst auto media = item->media();\n\tconst auto invoice = media ? media->invoice() : nullptr;\n\treturn invoice && (invoice->currency == Ui::kCreditsCurrency);\n}\n\nvoid ProcessCreditsPayment(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tQPointer<QWidget> fireworks,\n\t\tstd::shared_ptr<CreditsFormData> form,\n\t\tFn<void(CheckoutResult)> maybeReturnToBot) {\n\tconst auto hasBadResult = [=](Settings::SmallBalanceResult result) {\n\t\tif (result == Settings::SmallBalanceResult::Blocked) {\n\t\t\tif (const auto onstack = maybeReturnToBot) {\n\t\t\t\tonstack(CheckoutResult::Failed);\n\t\t\t}\n\t\t\treturn true;\n\t\t} else if (result == Settings::SmallBalanceResult::Cancelled) {\n\t\t\tif (const auto onstack = maybeReturnToBot) {\n\t\t\t\tonstack(CheckoutResult::Cancelled);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\tconst auto done = [=](Settings::SmallBalanceResult result) {\n\t\tif (hasBadResult(result)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto done = [=](std::optional<QString> error) {\n\t\t\tconst auto onstack = maybeReturnToBot;\n\t\t\tif (error) {\n\t\t\t\tif (*error == u\"STARGIFT_USAGE_LIMITED\"_q) {\n\t\t\t\t\tif (form->starGiftLimitedCount) {\n\t\t\t\t\t\tshow->showToast({\n\t\t\t\t\t\t\t.title = tr::lng_gift_sold_out_title(\n\t\t\t\t\t\t\t\ttr::now),\n\t\t\t\t\t\t\t.text = tr::lng_gift_sold_out_text(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\t\t\tform->starGiftLimitedCount,\n\t\t\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\tshow->showToast(\n\t\t\t\t\t\t\ttr::lng_gift_sold_out_title(tr::now));\n\t\t\t\t\t}\n\t\t\t\t} else if (*error == u\"STARGIFT_USER_USAGE_LIMITED\"_q) {\n\t\t\t\t\tshow->showToast({\n\t\t\t\t\t\t.text = tr::lng_gift_sent_finished(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\tstd::max(form->starGiftPerUserLimit, 1),\n\t\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tshow->showToast(*error);\n\t\t\t\t}\n\t\t\t\tif (onstack) {\n\t\t\t\t\tonstack(CheckoutResult::Failed);\n\t\t\t\t}\n\t\t\t} else if (onstack) {\n\t\t\t\tonstack(CheckoutResult::Paid);\n\t\t\t}\n\t\t};\n\t\tUi::SendStarsForm(&show->session(), form, done);\n\t};\n\tusing namespace Settings;\n\tauto source = Ui::SmallBalanceSourceFromForm(form);\n\tif (form->starGiftForm || IsPremiumForStarsInvoice(form->id)) {\n\t\tconst auto credits = form->invoice.credits;\n\t\tMaybeRequestBalanceIncrease(show, credits, source, done);\n\t} else {\n\t\tconst auto unsuccessful = std::make_shared<bool>(true);\n\t\tconst auto box = show->show(Box(\n\t\t\tUi::SendCreditsBox,\n\t\t\tform,\n\t\t\t[=](Settings::SmallBalanceResult result) {\n\t\t\t\t*unsuccessful = false;\n\t\t\t\tif (hasBadResult(result)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (const auto widget = fireworks.data()) {\n\t\t\t\t\tUi::StartFireworks(widget);\n\t\t\t\t}\n\t\t\t\tif (const auto onstack = maybeReturnToBot) {\n\t\t\t\t\tonstack(CheckoutResult::Paid);\n\t\t\t\t}\n\t\t\t}));\n\t\tbox->boxClosing() | rpl::on_next([=] {\n\t\t\tcrl::on_main([=] {\n\t\t\t\tif (*unsuccessful) {\n\t\t\t\t\tif (const auto onstack = maybeReturnToBot) {\n\t\t\t\t\t\tonstack(CheckoutResult::Cancelled);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}, box->lifetime());\n\t}\n}\n\nvoid ProcessCreditsReceipt(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tstd::shared_ptr<CreditsReceiptData> receipt,\n\t\tFn<void(CheckoutResult)> maybeReturnToBot) {\n\tconst auto entry = Data::CreditsHistoryEntry{\n\t\t.id = receipt->id,\n\t\t.title = receipt->title,\n\t\t.description = { receipt->description },\n\t\t.date = base::unixtime::parse(receipt->date),\n\t\t.photoId = receipt->photo ? receipt->photo->id : 0,\n\t\t.credits = receipt->credits,\n\t\t.bareMsgId = uint64(),\n\t\t.barePeerId = receipt->peerId.value,\n\t\t.peerType = Data::CreditsHistoryEntry::PeerType::Peer,\n\t};\n\tcontroller->uiShow()->show(Box(\n\t\tSettings::ReceiptCreditsBox,\n\t\tcontroller,\n\t\tentry,\n\t\tData::SubscriptionEntry{}));\n\tcontroller->window().activate();\n}\n\nFn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFn<void(CheckoutResult)> maybeReturnToBot) {\n\treturn [=](NonPanelPaymentForm form) {\n\t\tusing CreditsFormDataPtr = std::shared_ptr<CreditsFormData>;\n\t\tusing CreditsReceiptPtr = std::shared_ptr<CreditsReceiptData>;\n\t\tv::match(form, [&](const CreditsFormDataPtr &form) {\n\t\t\tProcessCreditsPayment(\n\t\t\t\tcontroller->uiShow(),\n\t\t\t\tcontroller->content().get(),\n\t\t\t\tform,\n\t\t\t\tmaybeReturnToBot);\n\t\t\tcontroller->window().activate();\n\t\t}, [&](const CreditsReceiptPtr &receipt) {\n\t\t\tProcessCreditsReceipt(controller, receipt, maybeReturnToBot);\n\t\t}, [](RealFormPresentedNotification) {});\n\t};\n}\n\nFn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item) {\n\treturn IsCreditsInvoice(item)\n\t\t? ProcessNonPanelPaymentFormFactory(controller)\n\t\t: nullptr;\n}\n\n} // namespace Payments\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/payments_non_panel_process.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass HistoryItem;\n\nnamespace Main {\nclass SessionShow;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Payments {\n\nenum class CheckoutResult;\nstruct CreditsFormData;\nstruct CreditsReceiptData;\nstruct NonPanelPaymentForm;\n\n[[nodiscard]] bool IsCreditsInvoice(not_null<HistoryItem*> item);\n\nvoid ProcessCreditsPayment(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tQPointer<QWidget> fireworks,\n\tstd::shared_ptr<CreditsFormData> form,\n\tFn<void(CheckoutResult)> maybeReturnToBot = nullptr);\n\nvoid ProcessCreditsReceipt(\n\tnot_null<Window::SessionController*> controller,\n\tstd::shared_ptr<CreditsReceiptData> receipt,\n\tFn<void(CheckoutResult)> maybeReturnToBot = nullptr);\n\nFn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory(\n\tnot_null<Window::SessionController*> controller,\n\tFn<void(Payments::CheckoutResult)> maybeReturnToBot = nullptr);\n\nFn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<HistoryItem*> item);\n\n} // namespace Payments\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/payments_reaction_process.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"payments/payments_reaction_process.h\"\n\n#include \"api/api_credits.h\"\n#include \"api/api_global_privacy.h\"\n#include \"apiwrap.h\"\n#include \"boxes/peers/prepare_short_info_box.h\"\n#include \"boxes/send_credits_box.h\" // CreditsEmojiSmall.\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_messages.h\"\n#include \"core/ui_integration.h\" // TextContext.\n#include \"data/components/credits.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/session/session_show.h\"\n#include \"main/session/send_as_peers.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"payments/ui/payments_reaction_box.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"ui/effects/reaction_fly_animation.h\"\n#include \"ui/layers/box_content.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/layers/show.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace Payments {\nnamespace {\n\nconstexpr auto kMaxPerReactionFallback = 10'000;\nconstexpr auto kDefaultPerReaction = 50;\n\nvoid TryAddingPaidReaction(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tFullMsgId itemId,\n\t\tbase::weak_ptr<HistoryView::Element> weakView,\n\t\tint count,\n\t\tstd::optional<PeerId> shownPeer,\n\t\tFn<void(bool)> finished) {\n\tconst auto owner = &show->session().data();\n\tconst auto checkItem = [=] {\n\t\tconst auto item = owner->message(itemId);\n\t\tif (!item) {\n\t\t\tif (const auto onstack = finished) {\n\t\t\t\tonstack(false);\n\t\t\t}\n\t\t}\n\t\treturn item;\n\t};\n\n\tconst auto item = checkItem();\n\tif (!item) {\n\t\treturn;\n\t}\n\tconst auto done = [=](Settings::SmallBalanceResult result) {\n\t\tif (result == Settings::SmallBalanceResult::Success\n\t\t\t|| result == Settings::SmallBalanceResult::Already) {\n\t\t\tif (const auto item = checkItem()) {\n\t\t\t\titem->addPaidReaction(count, shownPeer);\n\t\t\t\tif (const auto view = count ? weakView.get() : nullptr) {\n\t\t\t\t\tconst auto history = view->history();\n\t\t\t\t\thistory->owner().notifyViewPaidReactionSent(view);\n\t\t\t\t\tview->animateReaction({\n\t\t\t\t\t\t.id = Data::ReactionId::Paid(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (const auto onstack = finished) {\n\t\t\t\t\tonstack(true);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (const auto onstack = finished) {\n\t\t\tonstack(false);\n\t\t}\n\t};\n\tconst auto channelId = peerToChannel(itemId.peer);\n\tSettings::MaybeRequestBalanceIncrease(\n\t\tshow,\n\t\tcount,\n\t\tSettings::SmallBalanceReaction{ .channelId = channelId },\n\t\tdone);\n}\n\n} // namespace\n\nbool LookupMyPaidAnonymous(not_null<HistoryItem*> item) {\n\tfor (const auto &entry : item->topPaidReactionsWithLocal()) {\n\t\tif (entry.my) {\n\t\t\treturn !entry.peer;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid TryAddingPaidReaction(\n\t\tnot_null<HistoryItem*> item,\n\t\tHistoryView::Element *view,\n\t\tint count,\n\t\tstd::optional<PeerId> shownPeer,\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tFn<void(bool)> finished) {\n\tTryAddingPaidReaction(\n\t\tstd::move(show),\n\t\titem->fullId(),\n\t\tview,\n\t\tcount,\n\t\tshownPeer,\n\t\tstd::move(finished));\n}\n\nvoid TryAddingPaidReaction(\n\t\tnot_null<Calls::GroupCall*> call,\n\t\tint count,\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tFn<void(bool)> finished) {\n\tconst auto checkCall = [weak = base::make_weak(call), finished] {\n\t\tconst auto strong = weak.get();\n\t\tif (!strong) {\n\t\t\tif (const auto onstack = finished) {\n\t\t\t\tonstack(false);\n\t\t\t}\n\t\t}\n\t\treturn strong;\n\t};\n\n\tconst auto done = [=](Settings::SmallBalanceResult result) {\n\t\tif (result == Settings::SmallBalanceResult::Success\n\t\t\t|| result == Settings::SmallBalanceResult::Already) {\n\t\t\tif (const auto call = checkCall()) {\n\t\t\t\tcall->messages()->reactionsPaidAdd(count);\n\t\t\t\tcall->peer()->owner().notifyCallPaidReactionSent(call);\n\t\t\t\tif (const auto onstack = finished) {\n\t\t\t\t\tonstack(true);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (const auto onstack = finished) {\n\t\t\tonstack(false);\n\t\t}\n\t};\n\tSettings::MaybeRequestBalanceIncrease(\n\t\tshow,\n\t\tcount,\n\t\tSettings::SmallBalanceVideoStream{ .streamerId = call->peer()->id },\n\t\tdone);\n}\n\nvoid ShowPaidReactionDetails(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item,\n\t\tHistoryView::Element *view,\n\t\tHistoryReactionSource source) {\n\tExpects(item->history()->peer->isBroadcast()\n\t\t|| item->isDiscussionPost());\n\n\tconst auto show = controller->uiShow();\n\tconst auto itemId = item->fullId();\n\tconst auto session = &item->history()->session();\n\tconst auto appConfig = &session->appConfig();\n\n\tconst auto max = std::max(\n\t\tappConfig->get<int>(\n\t\t\tu\"stars_paid_reaction_amount_max\"_q,\n\t\t\tkMaxPerReactionFallback),\n\t\t2);\n\tconst auto chosen = std::clamp(kDefaultPerReaction, 1, max);\n\n\tstruct State {\n\t\tbase::weak_qptr<Ui::BoxContent> selectBox;\n\t\tbool ignoreShownPeerSwitch = false;\n\t\tbool sending = false;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tsession->credits().load(true);\n\n\tconst auto weakView = base::make_weak(view);\n\tconst auto send = [=](int count, PeerId shownPeer, auto resend) -> void {\n\t\tExpects(count >= 0);\n\n\t\tconst auto finish = [=](bool success) {\n\t\t\tstate->sending = false;\n\t\t\tif (success && count > 0) {\n\t\t\t\tstate->ignoreShownPeerSwitch = true;\n\t\t\t\tif (const auto strong = state->selectBox.get()) {\n\t\t\t\t\tstrong->closeBox();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tif (state->sending || (!count && state->ignoreShownPeerSwitch)) {\n\t\t\treturn;\n\t\t} else if (const auto item = session->data().message(itemId)) {\n\t\t\tstate->sending = true;\n\t\t\tTryAddingPaidReaction(\n\t\t\t\titem,\n\t\t\t\tweakView.get(),\n\t\t\t\tcount,\n\t\t\t\tshownPeer,\n\t\t\t\tshow,\n\t\t\t\tfinish);\n\t\t}\n\t};\n\n\tauto submitText = [=](rpl::producer<int> amount) {\n\t\tauto nice = std::move(amount) | rpl::map([=](int count) {\n\t\t\treturn Ui::CreditsEmojiSmall().append(\n\t\t\t\tLang::FormatCountDecimal(count));\n\t\t});\n\t\treturn tr::lng_paid_react_send(\n\t\t\tlt_price,\n\t\t\tstd::move(nice),\n\t\t\ttr::rich);\n\t};\n\tauto top = std::vector<Ui::PaidReactionTop>();\n\tconst auto add = [&](const Data::MessageReactionsTopPaid &entry) {\n\t\tconst auto peer = entry.peer;\n\t\tconst auto name = peer\n\t\t\t? peer->shortName()\n\t\t\t: tr::lng_paid_react_anonymous(tr::now);\n\t\tconst auto open = [=] {\n\t\t\tcontroller->uiShow()->show(PrepareShortInfoBox(peer, controller));\n\t\t};\n\t\ttop.push_back({\n\t\t\t.name = name,\n\t\t\t.photo = (peer\n\t\t\t\t? Ui::MakeUserpicThumbnail(peer)\n\t\t\t\t: Ui::MakeHiddenAuthorThumbnail()),\n\t\t\t.barePeerId = peer ? uint64(peer->id.value) : 0,\n\t\t\t.count = int(entry.count),\n\t\t\t.click = peer ? open : Fn<void()>(),\n\t\t\t.my = (entry.my == 1),\n\t\t});\n\t};\n\tconst auto linked = item->discussionPostOriginalSender();\n\tconst auto channel = (linked ? linked : item->history()->peer.get());\n\tconst auto channels = session->sendAsPeers().list(\n\t\t{ channel, Main::SendAsType::PaidReaction }\n\t) | ranges::views::transform(\n\t\t&Main::SendAsPeer::peer\n\t) | ranges::to_vector;\n\tconst auto topPaid = item->topPaidReactionsWithLocal();\n\ttop.reserve(topPaid.size() + 2 + channels.size());\n\tfor (const auto &entry : topPaid) {\n\t\tadd(entry);\n\t}\n\tauto myAdded = base::flat_set<uint64>();\n\tconst auto i = ranges::find(top, true, &Ui::PaidReactionTop::my);\n\tif (i != end(top)) {\n\t\tmyAdded.emplace(i->barePeerId);\n\t}\n\tconst auto myCount = uint32((i != end(top)) ? i->count : 0);\n\tconst auto myAdd = [&](PeerData *peer) {\n\t\tconst auto barePeerId = peer ? uint64(peer->id.value) : 0;\n\t\tif (!myAdded.emplace(barePeerId).second) {\n\t\t\treturn;\n\t\t}\n\t\tadd(Data::MessageReactionsTopPaid{\n\t\t\t.peer = peer,\n\t\t\t.count = myCount,\n\t\t\t.my = true,\n\t\t});\n\t};\n\tconst auto globalPrivacy = &session->api().globalPrivacy();\n\tconst auto shown = globalPrivacy->paidReactionShownPeerCurrent();\n\tconst auto owner = &session->data();\n\tconst auto shownPeer = shown ? owner->peer(shown).get() : nullptr;\n\tmyAdd(shownPeer);\n\tmyAdd(session->user());\n\tmyAdd(nullptr);\n\tfor (const auto &channel : channels) {\n\t\tmyAdd(channel);\n\t}\n\tranges::stable_sort(top, ranges::greater(), &Ui::PaidReactionTop::count);\n\n\tstate->selectBox = show->show(Ui::MakePaidReactionBox({\n\t\t.chosen = chosen,\n\t\t.max = max,\n\t\t.top = std::move(top),\n\t\t.session = &channel->session(),\n\t\t.name = channel->name(),\n\t\t.submit = std::move(submitText),\n\t\t.balanceValue = session->credits().balanceValue(),\n\t\t.send = [=](int count, uint64 barePeerId) {\n\t\t\tsend(count, PeerId(barePeerId), send);\n\t\t},\n\t}));\n\n\tif (const auto strong = state->selectBox.get()) {\n\t\tsession->data().itemRemoved(\n\t\t) | rpl::on_next([=](not_null<const HistoryItem*> removed) {\n\t\t\tif (removed == item) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t}, strong->lifetime());\n\t}\n}\n\n} // namespace Payments\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/payments_reaction_process.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nenum class HistoryReactionSource : char;\n\nclass HistoryItem;\n\nnamespace Calls {\nclass GroupCall;\n} // namespace Calls\n\nnamespace HistoryView {\nclass Element;\n} // namespace HistoryView\n\nnamespace Main {\nclass SessionShow;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Payments {\n\n[[nodiscard]] bool LookupMyPaidAnonymous(not_null<HistoryItem*> item);\n\nvoid TryAddingPaidReaction(\n\tnot_null<HistoryItem*> item,\n\tHistoryView::Element *view,\n\tint count,\n\tstd::optional<PeerId> shownPeer,\n\tstd::shared_ptr<Main::SessionShow> show,\n\tFn<void(bool)> finished = nullptr);\n\nvoid TryAddingPaidReaction(\n\tnot_null<Calls::GroupCall*> call,\n\tint count,\n\tstd::shared_ptr<Main::SessionShow> show,\n\tFn<void(bool)> finished = nullptr);\n\nvoid ShowPaidReactionDetails(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<HistoryItem*> item,\n\tHistoryView::Element *view,\n\tHistoryReactionSource source);\n\n} // namespace Payments\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/smartglocal/smartglocal_api_client.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"smartglocal/smartglocal_api_client.h\"\n\n#include \"smartglocal/smartglocal_error.h\"\n#include \"smartglocal/smartglocal_token.h\"\n\n#include <QtCore/QJsonObject>\n#include <QtCore/QJsonDocument>\n#include <QtNetwork/QNetworkRequest>\n#include <QtNetwork/QNetworkReply>\n#include <crl/crl_on_main.h>\n\nnamespace SmartGlocal {\nnamespace {\n\n[[nodiscard]] QString APIURLBase(bool isTest) {\n\treturn isTest\n\t\t? \"tgb-playground.smart-glocal.com/cds/v1\"\n\t\t: \"tgb.smart-glocal.com/cds/v1\";\n}\n\n[[nodiscard]] QString TokenEndpoint() {\n\treturn \"tokenize/card\";\n}\n\n[[nodiscard]] QByteArray ToJson(const Stripe::CardParams &card) {\n\tconst auto zero = QChar('0');\n\tconst auto month = QString(\"%1\").arg(card.expMonth, 2, 10, zero);\n\tconst auto year = QString(\"%1\").arg(card.expYear % 100, 2, 10, zero);\n\n\treturn QJsonDocument(QJsonObject{\n\t\t{ \"card\", QJsonObject{\n\t\t\t{ \"number\", card.number },\n\t\t\t{ \"expiration_month\", month },\n\t\t\t{ \"expiration_year\", year },\n\t\t\t{ \"security_code\", card.cvc },\n\t\t} },\n\t}).toJson(QJsonDocument::Compact);\n}\n\n[[nodiscard]] QString ComputeApiUrl(PaymentConfiguration configuration) {\n\tconst auto url = configuration.tokenizeUrl;\n\tif (url.startsWith(\"https://\")\n\t\t&& url.endsWith(\".smart-glocal.com/cds/v1/tokenize/card\")) {\n\t\treturn url;\n\t}\n\treturn QString(\"https://%1/%2\")\n\t\t.arg(APIURLBase(configuration.isTest))\n\t\t.arg(TokenEndpoint());\n}\n\n} // namespace\n\nAPIClient::APIClient(PaymentConfiguration configuration)\n: _apiUrl(ComputeApiUrl(configuration))\n, _configuration(configuration) {\n\t_additionalHttpHeaders = {\n\t\t{ \"X-PUBLIC-TOKEN\", _configuration.publicToken },\n\t};\n}\n\nAPIClient::~APIClient() {\n\tconst auto destroy = std::move(_old);\n}\n\nvoid APIClient::createTokenWithCard(\n\t\tStripe::CardParams card,\n\t\tTokenCompletionCallback completion) {\n\tcreateTokenWithData(ToJson(card), std::move(completion));\n}\n\nvoid APIClient::createTokenWithData(\n\t\tQByteArray data,\n\t\tTokenCompletionCallback completion) {\n\tconst auto url = QUrl(_apiUrl);\n\tauto request = QNetworkRequest(url);\n\trequest.setHeader(\n\t\tQNetworkRequest::ContentTypeHeader,\n\t\t\"application/json\");\n\tfor (const auto &[name, value] : _additionalHttpHeaders) {\n\t\trequest.setRawHeader(name.toUtf8(), value.toUtf8());\n\t}\n\tdestroyReplyDelayed(std::move(_reply));\n\t_reply.reset(_manager.post(request, data));\n\tconst auto finish = [=](Token token, Error error) {\n\t\tcrl::on_main([\n\t\t\tcompletion,\n\t\t\ttoken = std::move(token),\n\t\t\terror = std::move(error)\n\t\t] {\n\t\t\tcompletion(std::move(token), std::move(error));\n\t\t});\n\t};\n\tconst auto finishWithError = [=](Error error) {\n\t\tfinish(Token::Empty(), std::move(error));\n\t};\n\tconst auto finishWithToken = [=](Token token) {\n\t\tfinish(std::move(token), Error::None());\n\t};\n\tQObject::connect(_reply.get(), &QNetworkReply::finished, [=] {\n\t\tconst auto replyError = int(_reply->error());\n\t\tconst auto replyErrorString = _reply->errorString();\n\t\tconst auto bytes = _reply->readAll();\n\t\tdestroyReplyDelayed(std::move(_reply));\n\n\t\tauto parseError = QJsonParseError();\n\t\tconst auto document = QJsonDocument::fromJson(bytes, &parseError);\n\t\tif (!bytes.isEmpty()) {\n\t\t\tif (parseError.error != QJsonParseError::NoError) {\n\t\t\t\tconst auto code = int(parseError.error);\n\t\t\t\tfinishWithError({\n\t\t\t\t\tError::Code::JsonParse,\n\t\t\t\t\tQString(\"InvalidJson%1\").arg(code),\n\t\t\t\t\tparseError.errorString(),\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t} else if (!document.isObject()) {\n\t\t\t\tfinishWithError({\n\t\t\t\t\tError::Code::JsonFormat,\n\t\t\t\t\t\"InvalidJsonRoot\",\n\t\t\t\t\t\"Not an object in JSON reply.\",\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto object = document.object();\n\t\t\tif (auto error = Error::DecodedObjectFromResponse(object)) {\n\t\t\t\tfinishWithError(std::move(error));\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (replyError != QNetworkReply::NoError) {\n\t\t\tfinishWithError({\n\t\t\t\tError::Code::Network,\n\t\t\t\tQString(\"RequestError%1\").arg(replyError),\n\t\t\t\treplyErrorString,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tauto token = Token::DecodedObjectFromAPIResponse(\n\t\t\tdocument.object().value(\"data\").toObject());\n\t\tif (!token) {\n\t\t\tfinishWithError({\n\t\t\t\tError::Code::JsonFormat,\n\t\t\t\t\"InvalidTokenJson\",\n\t\t\t\t\"Could not parse token.\",\n\t\t\t});\n\t\t}\n\t\tfinishWithToken(std::move(token));\n\t});\n}\n\nvoid APIClient::destroyReplyDelayed(std::unique_ptr<QNetworkReply> reply) {\n\tif (!reply) {\n\t\treturn;\n\t}\n\tconst auto raw = reply.get();\n\t_old.push_back(std::move(reply));\n\tQObject::disconnect(raw, &QNetworkReply::finished, nullptr, nullptr);\n\traw->deleteLater();\n\tQObject::connect(raw, &QObject::destroyed, [=] {\n\t\tfor (auto i = begin(_old); i != end(_old); ++i) {\n\t\t\tif (i->get() == raw) {\n\t\t\t\ti->release();\n\t\t\t\t_old.erase(i);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t});\n}\n\n} // namespace SmartGlocal\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/smartglocal/smartglocal_api_client.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"stripe/stripe_card_params.h\"\n#include \"smartglocal/smartglocal_callbacks.h\"\n\n#include <QtNetwork/QNetworkAccessManager>\n#include <QtCore/QString>\n#include <map>\n#include <memory>\n\nnamespace SmartGlocal {\n\nstruct PaymentConfiguration {\n\tQString publicToken;\n\tQString tokenizeUrl;\n\tbool isTest = false;\n};\n\nclass APIClient final {\npublic:\n\texplicit APIClient(PaymentConfiguration configuration);\n\t~APIClient();\n\n\tvoid createTokenWithCard(\n\t\tStripe::CardParams card,\n\t\tTokenCompletionCallback completion);\n\tvoid createTokenWithData(\n\t\tQByteArray data,\n\t\tTokenCompletionCallback completion);\n\nprivate:\n\tvoid destroyReplyDelayed(std::unique_ptr<QNetworkReply> reply);\n\n\tQString _apiUrl;\n\tPaymentConfiguration _configuration;\n\tstd::map<QString, QString> _additionalHttpHeaders;\n\tQNetworkAccessManager _manager;\n\tstd::unique_ptr<QNetworkReply> _reply;\n\tstd::vector<std::unique_ptr<QNetworkReply>> _old;\n\n};\n\n} // namespace SmartGlocal\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/smartglocal/smartglocal_callbacks.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <functional>\n\nnamespace SmartGlocal {\n\nclass Token;\nclass Error;\n\nusing TokenCompletionCallback = std::function<void(Token, Error)>;\n\n} // namespace SmartGlocal\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/smartglocal/smartglocal_card.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"smartglocal/smartglocal_card.h\"\n\n#include <QtCore/QRegularExpression>\n\nnamespace SmartGlocal {\n\nCard::Card(\n\tQString type,\n\tQString network,\n\tQString maskedNumber)\n: _type(type)\n, _network(network)\n, _maskedNumber(maskedNumber) {\n}\n\nCard Card::Empty() {\n\treturn Card(QString(), QString(), QString());\n}\n\nCard Card::DecodedObjectFromAPIResponse(QJsonObject object) {\n\tconst auto string = [&](QStringView key) {\n\t\treturn object.value(key).toString();\n\t};\n\tconst auto type = string(u\"card_type\");\n\tconst auto network = string(u\"card_network\");\n\tconst auto maskedNumber = string(u\"masked_card_number\");\n\tif (type.isEmpty() || maskedNumber.isEmpty()) {\n\t\treturn Card::Empty();\n\t}\n\treturn Card(type, network, maskedNumber);\n}\n\nQString Card::type() const {\n\treturn _type;\n}\n\nQString Card::network() const {\n\treturn _network;\n}\n\nQString Card::maskedNumber() const {\n\treturn _maskedNumber;\n}\n\nbool Card::empty() const {\n\treturn _type.isEmpty() || _maskedNumber.isEmpty();\n}\n\nQString Last4(const Card &card) {\n\tstatic const auto RegExp = QRegularExpression(\"[^\\\\d]\\\\d*(\\\\d{4})$\");\n\tconst auto masked = card.maskedNumber();\n\tconst auto m = RegExp.match(masked);\n\treturn m.hasMatch() ? m.captured(1) : QString();\n}\n\n} // namespace SmartGlocal\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/smartglocal/smartglocal_card.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QString>\n\nclass QJsonObject;\n\nnamespace SmartGlocal {\n\nclass Card final {\npublic:\n\tCard(const Card &other) = default;\n\tCard &operator=(const Card &other) = default;\n\tCard(Card &&other) = default;\n\tCard &operator=(Card &&other) = default;\n\t~Card() = default;\n\n\t[[nodiscard]] static Card Empty();\n\t[[nodiscard]] static Card DecodedObjectFromAPIResponse(\n\t\tQJsonObject object);\n\n\t[[nodiscard]] QString type() const;\n\t[[nodiscard]] QString network() const;\n\t[[nodiscard]] QString maskedNumber() const;\n\n\t[[nodiscard]] bool empty() const;\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n\nprivate:\n\tCard(\n\t\tQString type,\n\t\tQString network,\n\t\tQString maskedNumber);\n\n\tQString _type;\n\tQString _network;\n\tQString _maskedNumber;\n\n};\n\n[[nodiscard]] QString Last4(const Card &card);\n\n} // namespace SmartGlocal\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/smartglocal/smartglocal_error.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"smartglocal/smartglocal_error.h\"\n\nnamespace SmartGlocal {\n\nError::Code Error::code() const {\n\treturn _code;\n}\n\nQString Error::description() const {\n\treturn _description;\n}\n\nQString Error::message() const {\n\treturn _message;\n}\n\nQString Error::parameter() const {\n\treturn _parameter;\n}\n\nError Error::None() {\n\treturn Error(Code::None, {}, {}, {});\n}\n\nError Error::DecodedObjectFromResponse(QJsonObject object) {\n\tif (object.value(\"status\").toString() == \"ok\") {\n\t\treturn Error::None();\n\t}\n\tconst auto entry = object.value(\"error\");\n\tif (!entry.isObject()) {\n\t\treturn {\n\t\t\tCode::Unknown,\n\t\t\t\"GenericError\",\n\t\t\t\"Could not read the error response \"\n\t\t\t\"that was returned from SmartGlocal.\"\n\t\t};\n\t}\n\tconst auto error = entry.toObject();\n\tconst auto string = [&](QStringView key) {\n\t\treturn error.value(key).toString();\n\t};\n\tconst auto code = string(u\"code\");\n\tconst auto description = string(u\"description\");\n\n\t// There should always be a message and type for the error\n\tif (code.isEmpty() || description.isEmpty()) {\n\t\treturn {\n\t\t\tCode::Unknown,\n\t\t\t\"GenericError\",\n\t\t\t\"Could not interpret the error response \"\n\t\t\t\"that was returned from SmartGlocal.\"\n\t\t};\n\t}\n\n\treturn { Code::Unknown, code, description };\n}\n\nbool Error::empty() const {\n\treturn (_code == Code::None);\n}\n\n} // namespace SmartGlocal\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/smartglocal/smartglocal_error.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QString>\n\nclass QJsonObject;\n\nnamespace SmartGlocal {\n\nclass Error {\npublic:\n\tenum class Code {\n\t\tNone = 0, // Non-SmartGlocal errors.\n\t\tJsonParse = -1,\n\t\tJsonFormat = -2,\n\t\tNetwork = -3,\n\n\t\tUnknown = 8,\n\t};\n\n\tError(\n\t\tCode code,\n\t\tconst QString &description,\n\t\tconst QString &message,\n\t\tconst QString &parameter = QString())\n\t: _code(code)\n\t, _description(description)\n\t, _message(message)\n\t, _parameter(parameter) {\n\t}\n\n\t[[nodiscard]] Code code() const;\n\t[[nodiscard]] QString description() const;\n\t[[nodiscard]] QString message() const;\n\t[[nodiscard]] QString parameter() const;\n\n\t[[nodiscard]] static Error None();\n\t[[nodiscard]] static Error DecodedObjectFromResponse(QJsonObject object);\n\n\t[[nodiscard]] bool empty() const;\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n\nprivate:\n\tCode _code = Code::None;\n\tQString _description;\n\tQString _message;\n\tQString _parameter;\n\n};\n\n} // namespace SmartGlocal\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/smartglocal/smartglocal_token.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"smartglocal/smartglocal_token.h\"\n\nnamespace SmartGlocal {\n\nQString Token::tokenId() const {\n\treturn _tokenId;\n}\n\nCard Token::card() const {\n\treturn _card;\n}\n\nToken Token::Empty() {\n\treturn Token(QString());\n}\n\nToken Token::DecodedObjectFromAPIResponse(QJsonObject object) {\n\tconst auto tokenId = object.value(\"token\").toString();\n\tif (tokenId.isEmpty()) {\n\t\treturn Token::Empty();\n\t}\n\tauto result = Token(tokenId);\n\tconst auto card = object.value(\"info\");\n\tif (card.isObject()) {\n\t\tresult._card = Card::DecodedObjectFromAPIResponse(card.toObject());\n\t}\n\treturn result;\n}\n\nbool Token::empty() const {\n\treturn _tokenId.isEmpty();\n}\n\nToken::Token(QString tokenId)\n: _tokenId(std::move(tokenId)) {\n}\n\n} // namespace SmartGlocal\n\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/smartglocal/smartglocal_token.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"smartglocal/smartglocal_card.h\"\n\n#include <QtCore/QDateTime>\n\nclass QJsonObject;\n\nnamespace SmartGlocal {\n\nclass Token {\npublic:\n\tToken(const Token &other) = default;\n\tToken &operator=(const Token &other) = default;\n\tToken(Token &&other) = default;\n\tToken &operator=(Token &&other) = default;\n\t~Token() = default;\n\n\t[[nodiscard]] QString tokenId() const;\n\t[[nodiscard]] bool livemode() const;\n\t[[nodiscard]] Card card() const;\n\n\t[[nodiscard]] static Token Empty();\n\t[[nodiscard]] static Token DecodedObjectFromAPIResponse(\n\t\tQJsonObject object);\n\n\t[[nodiscard]] bool empty() const;\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n\nprivate:\n\texplicit Token(QString tokenId);\n\n\tQString _tokenId;\n\tCard _card = Card::Empty();\n\n};\n\n} // namespace SmartGlocal\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_address.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Stripe {\n\nenum class BillingAddressFields {\n\tNone,\n\tZip,\n\tFull,\n};\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_api_client.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"stripe/stripe_api_client.h\"\n\n#include \"stripe/stripe_error.h\"\n#include \"stripe/stripe_token.h\"\n#include \"stripe/stripe_form_encoder.h\"\n\n#include <QtCore/QJsonObject>\n#include <QtCore/QJsonDocument>\n#include <QtNetwork/QNetworkRequest>\n#include <QtNetwork/QNetworkReply>\n#include <crl/crl_on_main.h>\n\nnamespace Stripe {\nnamespace {\n\n[[nodiscard]] QString APIURLBase() {\n\treturn \"api.stripe.com/v1\";\n}\n\n[[nodiscard]] QString TokenEndpoint() {\n\treturn \"tokens\";\n}\n\n[[nodiscard]] QString StripeAPIVersion() {\n\treturn \"2015-10-12\";\n}\n\n[[nodiscard]] QString SDKVersion() {\n\treturn \"9.1.0\";\n}\n\n[[nodiscard]] QString StripeUserAgentDetails() {\n\tconst auto details = QJsonObject{\n\t\t{ \"lang\", \"objective-c\" },\n\t\t{ \"bindings_version\", SDKVersion() },\n\t};\n\treturn QString::fromUtf8(\n\t\tQJsonDocument(details).toJson(QJsonDocument::Compact));\n}\n\n} // namespace\n\nAPIClient::APIClient(PaymentConfiguration configuration)\n: _apiUrl(\"https://\" + APIURLBase())\n, _configuration(configuration) {\n\t_additionalHttpHeaders = {\n\t\t{ \"X-Stripe-User-Agent\", StripeUserAgentDetails() },\n\t\t{ \"Stripe-Version\", StripeAPIVersion() },\n\t\t{ \"Authorization\", \"Bearer \" + _configuration.publishableKey },\n\t};\n}\n\nAPIClient::~APIClient() {\n\tconst auto destroy = std::move(_old);\n}\n\nvoid APIClient::createTokenWithCard(\n\t\tCardParams card,\n\t\tTokenCompletionCallback completion) {\n\tcreateTokenWithData(\n\t\tFormEncoder::formEncodedDataForObject(MakeEncodable(card)),\n\t\tstd::move(completion));\n}\n\nvoid APIClient::createTokenWithData(\n\t\tQByteArray data,\n\t\tTokenCompletionCallback completion) {\n\tconst auto url = QUrl(_apiUrl + '/' + TokenEndpoint());\n\tauto request = QNetworkRequest(url);\n\tfor (const auto &[name, value] : _additionalHttpHeaders) {\n\t\trequest.setRawHeader(name.toUtf8(), value.toUtf8());\n\t}\n\tdestroyReplyDelayed(std::move(_reply));\n\t_reply.reset(_manager.post(request, data));\n\tconst auto finish = [=](Token token, Error error) {\n\t\tcrl::on_main([\n\t\t\tcompletion,\n\t\t\ttoken = std::move(token),\n\t\t\terror = std::move(error)\n\t\t] {\n\t\t\tcompletion(std::move(token), std::move(error));\n\t\t});\n\t};\n\tconst auto finishWithError = [=](Error error) {\n\t\tfinish(Token::Empty(), std::move(error));\n\t};\n\tconst auto finishWithToken = [=](Token token) {\n\t\tfinish(std::move(token), Error::None());\n\t};\n\tQObject::connect(_reply.get(), &QNetworkReply::finished, [=] {\n\t\tconst auto replyError = int(_reply->error());\n\t\tconst auto replyErrorString = _reply->errorString();\n\t\tconst auto bytes = _reply->readAll();\n\t\tdestroyReplyDelayed(std::move(_reply));\n\n\t\tauto parseError = QJsonParseError();\n\t\tconst auto document = QJsonDocument::fromJson(bytes, &parseError);\n\t\tif (!bytes.isEmpty()) {\n\t\t\tif (parseError.error != QJsonParseError::NoError) {\n\t\t\t\tconst auto code = int(parseError.error);\n\t\t\t\tfinishWithError({\n\t\t\t\t\tError::Code::JsonParse,\n\t\t\t\t\tQString(\"InvalidJson%1\").arg(code),\n\t\t\t\t\tparseError.errorString(),\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t} else if (!document.isObject()) {\n\t\t\t\tfinishWithError({\n\t\t\t\t\tError::Code::JsonFormat,\n\t\t\t\t\t\"InvalidJsonRoot\",\n\t\t\t\t\t\"Not an object in JSON reply.\",\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto object = document.object();\n\t\t\tif (auto error = Error::DecodedObjectFromResponse(object)) {\n\t\t\t\tfinishWithError(std::move(error));\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (replyError != QNetworkReply::NoError) {\n\t\t\tfinishWithError({\n\t\t\t\tError::Code::Network,\n\t\t\t\tQString(\"RequestError%1\").arg(replyError),\n\t\t\t\treplyErrorString,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tauto token = Token::DecodedObjectFromAPIResponse(document.object());\n\t\tif (!token) {\n\t\t\tfinishWithError({\n\t\t\t\tError::Code::JsonFormat,\n\t\t\t\t\"InvalidTokenJson\",\n\t\t\t\t\"Could not parse token.\",\n\t\t\t});\n\t\t}\n\t\tfinishWithToken(std::move(token));\n\t});\n}\n\nvoid APIClient::destroyReplyDelayed(std::unique_ptr<QNetworkReply> reply) {\n\tif (!reply) {\n\t\treturn;\n\t}\n\tconst auto raw = reply.get();\n\t_old.push_back(std::move(reply));\n\tQObject::disconnect(raw, &QNetworkReply::finished, nullptr, nullptr);\n\traw->deleteLater();\n\tQObject::connect(raw, &QObject::destroyed, [=] {\n\t\tfor (auto i = begin(_old); i != end(_old); ++i) {\n\t\t\tif (i->get() == raw) {\n\t\t\t\ti->release();\n\t\t\t\t_old.erase(i);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t});\n}\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_api_client.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"stripe/stripe_payment_configuration.h\"\n#include \"stripe/stripe_card_params.h\"\n#include \"stripe/stripe_callbacks.h\"\n\n#include <QtNetwork/QNetworkAccessManager>\n#include <QtCore/QString>\n#include <map>\n#include <memory>\n\nnamespace Stripe {\n\nclass APIClient final {\npublic:\n\texplicit APIClient(PaymentConfiguration configuration);\n\t~APIClient();\n\n\tvoid createTokenWithCard(\n\t\tCardParams card,\n\t\tTokenCompletionCallback completion);\n\tvoid createTokenWithData(\n\t\tQByteArray data,\n\t\tTokenCompletionCallback completion);\n\nprivate:\n\tvoid destroyReplyDelayed(std::unique_ptr<QNetworkReply> reply);\n\n\tQString _apiUrl;\n\tPaymentConfiguration _configuration;\n\tstd::map<QString, QString> _additionalHttpHeaders;\n\tQNetworkAccessManager _manager;\n\tstd::unique_ptr<QNetworkReply> _reply;\n\tstd::vector<std::unique_ptr<QNetworkReply>> _old;\n\n};\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_callbacks.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <functional>\n\nnamespace Stripe {\n\nclass Error;\nclass Token;\n\nusing TokenCompletionCallback = std::function<void(Token, Error)>;\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_card.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"stripe/stripe_card.h\"\n\n#include \"stripe/stripe_decode.h\"\n\nnamespace Stripe {\nnamespace {\n\nCardBrand BrandFromString(const QString &brand) {\n\tif (brand == \"visa\") {\n\t\treturn CardBrand::Visa;\n\t} else if (brand == \"american express\") {\n\t\treturn CardBrand::Amex;\n\t} else if (brand == \"mastercard\") {\n\t\treturn CardBrand::MasterCard;\n\t} else if (brand == \"discover\") {\n\t\treturn CardBrand::Discover;\n\t} else if (brand == \"jcb\") {\n\t\treturn CardBrand::JCB;\n\t} else if (brand == \"diners club\") {\n\t\treturn CardBrand::DinersClub;\n\t} else {\n\t\treturn CardBrand::Unknown;\n\t}\n}\n\nCardFundingType FundingFromString(const QString &funding) {\n\tif (funding == \"credit\") {\n\t\treturn CardFundingType::Credit;\n\t} else if (funding == \"debit\") {\n\t\treturn CardFundingType::Debit;\n\t} else if (funding == \"prepaid\") {\n\t\treturn CardFundingType::Prepaid;\n\t} else {\n\t\treturn CardFundingType::Other;\n\t}\n}\n\n} // namespace\n\nCard::Card(\n\tQString id,\n\tQString last4,\n\tCardBrand brand,\n\tquint32 expMonth,\n\tquint32 expYear)\n: _cardId(id)\n, _last4(last4)\n, _brand(brand)\n, _expMonth(expMonth)\n, _expYear(expYear) {\n}\n\nCard Card::Empty() {\n\treturn Card(QString(), QString(), CardBrand::Unknown, 0, 0);\n}\n\nCard Card::DecodedObjectFromAPIResponse(QJsonObject object) {\n\tif (!ContainsFields(object, {\n\t\tu\"id\",\n\t\tu\"last4\",\n\t\tu\"brand\",\n\t\tu\"exp_month\",\n\t\tu\"exp_year\"\n\t})) {\n\t\treturn Card::Empty();\n\t}\n\n\tconst auto string = [&](QStringView key) {\n\t\treturn object.value(key).toString();\n\t};\n\tconst auto cardId = string(u\"id\");\n\tconst auto last4 = string(u\"last4\");\n\tconst auto brand = BrandFromString(string(u\"brand\").toLower());\n\tconst auto expMonth = object.value(\"exp_month\").toInt();\n\tconst auto expYear = object.value(\"exp_year\").toInt();\n\tauto result = Card(cardId, last4, brand, expMonth, expYear);\n\tresult._name = string(u\"name\");\n\tresult._dynamicLast4 = string(u\"dynamic_last4\");\n\tresult._funding = FundingFromString(string(u\"funding\").toLower());\n\tresult._fingerprint = string(u\"fingerprint\");\n\tresult._country = string(u\"country\");\n\tresult._currency = string(u\"currency\");\n\tresult._addressLine1 = string(u\"address_line1\");\n\tresult._addressLine2 = string(u\"address_line2\");\n\tresult._addressCity = string(u\"address_city\");\n\tresult._addressState = string(u\"address_state\");\n\tresult._addressZip = string(u\"address_zip\");\n\tresult._addressCountry = string(u\"address_country\");\n\n\t// TODO incomplete, not used.\n\t//result._allResponseFields = object;\n\n\treturn result;\n}\n\nQString Card::cardId() const {\n\treturn _cardId;\n}\n\nQString Card::name() const {\n\treturn _name;\n}\n\nQString Card::last4() const {\n\treturn _last4;\n}\n\nQString Card::dynamicLast4() const {\n\treturn _dynamicLast4;\n}\n\nCardBrand Card::brand() const {\n\treturn _brand;\n}\n\nCardFundingType Card::funding() const {\n\treturn _funding;\n}\n\nQString Card::fingerprint() const {\n\treturn _fingerprint;\n}\n\nQString Card::country() const {\n\treturn _country;\n}\n\nQString Card::currency() const {\n\treturn _currency;\n}\n\nquint32 Card::expMonth() const {\n\treturn _expMonth;\n}\n\nquint32 Card::expYear() const {\n\treturn _expYear;\n}\n\nQString Card::addressLine1() const {\n\treturn _addressLine1;\n}\n\nQString Card::addressLine2() const {\n\treturn _addressLine2;\n}\n\nQString Card::addressCity() const {\n\treturn _addressCity;\n}\n\nQString Card::addressState() const {\n\treturn _addressState;\n}\n\nQString Card::addressZip() const {\n\treturn _addressZip;\n}\n\nQString Card::addressCountry() const {\n\treturn _addressCountry;\n}\n\nbool Card::empty() const {\n\treturn _cardId.isEmpty();\n}\n\nQString CardBrandToString(CardBrand brand) {\n\tswitch (brand) {\n\tcase CardBrand::Amex: return \"American Express\";\n\tcase CardBrand::DinersClub: return \"Diners Club\";\n\tcase CardBrand::Discover: return \"Discover\";\n\tcase CardBrand::JCB: return \"JCB\";\n\tcase CardBrand::MasterCard: return \"MasterCard\";\n\tcase CardBrand::Unknown: return \"Unknown\";\n\tcase CardBrand::Visa: return \"Visa\";\n\t}\n\tstd::abort();\n}\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_card.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QString>\n\nclass QJsonObject;\n\nnamespace Stripe {\n\nenum class CardBrand {\n\tVisa,\n\tAmex,\n\tMasterCard,\n\tDiscover,\n\tJCB,\n\tDinersClub,\n\tUnionPay,\n\tUnknown,\n};\n\nenum class CardFundingType {\n\tDebit,\n\tCredit,\n\tPrepaid,\n\tOther,\n};\n\nclass Card final {\npublic:\n\tCard(const Card &other) = default;\n\tCard &operator=(const Card &other) = default;\n\tCard(Card &&other) = default;\n\tCard &operator=(Card &&other) = default;\n\t~Card() = default;\n\n\t[[nodiscard]] static Card Empty();\n\t[[nodiscard]] static Card DecodedObjectFromAPIResponse(\n\t\tQJsonObject object);\n\n\t[[nodiscard]] QString cardId() const;\n\t[[nodiscard]] QString name() const;\n\t[[nodiscard]] QString last4() const;\n\t[[nodiscard]] QString dynamicLast4() const;\n\t[[nodiscard]] CardBrand brand() const;\n\t[[nodiscard]] CardFundingType funding() const;\n\t[[nodiscard]] QString fingerprint() const;\n\t[[nodiscard]] QString country() const;\n\t[[nodiscard]] QString currency() const;\n\t[[nodiscard]] quint32 expMonth() const;\n\t[[nodiscard]] quint32 expYear() const;\n\t[[nodiscard]] QString addressLine1() const;\n\t[[nodiscard]] QString addressLine2() const;\n\t[[nodiscard]] QString addressCity() const;\n\t[[nodiscard]] QString addressState() const;\n\t[[nodiscard]] QString addressZip() const;\n\t[[nodiscard]] QString addressCountry() const;\n\n\t[[nodiscard]] bool empty() const;\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n\nprivate:\n\tCard(\n\t\tQString id,\n\t\tQString last4,\n\t\tCardBrand brand,\n\t\tquint32 expMonth,\n\t\tquint32 expYear);\n\n\tQString _cardId;\n\tQString _name;\n\tQString _last4;\n\tQString _dynamicLast4;\n\tCardBrand _brand = CardBrand::Unknown;\n\tCardFundingType _funding = CardFundingType::Other;\n\tQString _fingerprint;\n\tQString _country;\n\tQString _currency;\n\tquint32 _expMonth = 0;\n\tquint32 _expYear = 0;\n\tQString _addressLine1;\n\tQString _addressLine2;\n\tQString _addressCity;\n\tQString _addressState;\n\tQString _addressZip;\n\tQString _addressCountry;\n\n};\n\n[[nodiscard]] QString CardBrandToString(CardBrand brand);\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_card_params.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"stripe/stripe_card_params.h\"\n\nnamespace Stripe {\n\nQString CardParams::rootObjectName() {\n\treturn \"card\";\n}\n\nstd::map<QString, QString> CardParams::formFieldValues() const {\n\treturn {\n\t\t{ \"number\", number },\n\t\t{ \"cvc\", cvc },\n\t\t{ \"name\", name },\n\t\t{ \"address_line1\", addressLine1 },\n\t\t{ \"address_line2\", addressLine2 },\n\t\t{ \"address_city\", addressCity },\n\t\t{ \"address_state\", addressState },\n\t\t{ \"address_zip\", addressZip },\n\t\t{ \"address_country\", addressCountry },\n\t\t{ \"exp_month\", QString::number(expMonth) },\n\t\t{ \"exp_year\", QString::number(expYear) },\n\t\t{ \"currency\", currency },\n\t};\n}\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_card_params.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"stripe/stripe_form_encodable.h\"\n\nnamespace Stripe {\n\nstruct CardParams {\n\tQString number;\n\tquint32 expMonth = 0;\n\tquint32 expYear = 0;\n\tQString cvc;\n\tQString name;\n\tQString addressLine1;\n\tQString addressLine2;\n\tQString addressCity;\n\tQString addressState;\n\tQString addressZip;\n\tQString addressCountry;\n\tQString currency;\n\n\t[[nodiscard]] static QString rootObjectName();\n\t[[nodiscard]] std::map<QString, QString> formFieldValues() const;\n};\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_card_validator.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"stripe/stripe_card_validator.h\"\n\n#include \"base/qt/qt_string_view.h\"\n\n#include <QtCore/QDate>\n#include <QtCore/QRegularExpression>\n\nnamespace Stripe {\nnamespace {\n\nconstexpr auto kMinCvcLength = 3;\n\nstruct BinRange {\n\tQString low;\n\tQString high;\n\tint length = 0;\n\tCardBrand brand = CardBrand::Unknown;\n};\n\n[[nodiscard]] const std::vector<BinRange> &AllRanges() {\n\tstatic auto kResult = std::vector<BinRange>{\n\t\t// Unknown\n\t\t{ \"\", \"\", 19, CardBrand::Unknown },\n\t\t// American Express\n\t\t{ \"34\", \"34\", 15, CardBrand::Amex },\n\t\t{ \"37\", \"37\", 15, CardBrand::Amex },\n\t\t// Diners Club\n\t\t{ \"30\", \"30\", 16, CardBrand::DinersClub },\n\t\t{ \"36\", \"36\", 14, CardBrand::DinersClub },\n\t\t{ \"38\", \"39\", 16, CardBrand::DinersClub },\n\t\t// Discover\n\t\t{ \"60\", \"60\", 16, CardBrand::Discover },\n\t\t{ \"64\", \"65\", 16, CardBrand::Discover },\n\t\t// JCB\n\t\t{ \"35\", \"35\", 16, CardBrand::JCB },\n\t\t// Mastercard\n\t\t{ \"50\", \"59\", 16, CardBrand::MasterCard },\n\t\t{ \"22\", \"27\", 16, CardBrand::MasterCard },\n\t\t{ \"67\", \"67\", 16, CardBrand::MasterCard }, // Maestro\n\t\t// UnionPay\n\t\t{ \"62\", \"62\", 16, CardBrand::UnionPay },\n\t\t{ \"81\", \"81\", 16, CardBrand::UnionPay },\n\t\t// Visa\n\t\t{ \"40\", \"49\", 16, CardBrand::Visa },\n\t\t{ \"413600\", \"413600\", 13, CardBrand::Visa },\n\t\t{ \"444509\", \"444509\", 13, CardBrand::Visa },\n\t\t{ \"444509\", \"444509\", 13, CardBrand::Visa },\n\t\t{ \"444550\", \"444550\", 13, CardBrand::Visa },\n\t\t{ \"450603\", \"450603\", 13, CardBrand::Visa },\n\t\t{ \"450617\", \"450617\", 13, CardBrand::Visa },\n\t\t{ \"450628\", \"450629\", 13, CardBrand::Visa },\n\t\t{ \"450636\", \"450636\", 13, CardBrand::Visa },\n\t\t{ \"450640\", \"450641\", 13, CardBrand::Visa },\n\t\t{ \"450662\", \"450662\", 13, CardBrand::Visa },\n\t\t{ \"463100\", \"463100\", 13, CardBrand::Visa },\n\t\t{ \"476142\", \"476142\", 13, CardBrand::Visa },\n\t\t{ \"476143\", \"476143\", 13, CardBrand::Visa },\n\t\t{ \"492901\", \"492902\", 13, CardBrand::Visa },\n\t\t{ \"492920\", \"492920\", 13, CardBrand::Visa },\n\t\t{ \"492923\", \"492923\", 13, CardBrand::Visa },\n\t\t{ \"492928\", \"492930\", 13, CardBrand::Visa },\n\t\t{ \"492937\", \"492937\", 13, CardBrand::Visa },\n\t\t{ \"492939\", \"492939\", 13, CardBrand::Visa },\n\t\t{ \"492960\", \"492960\", 13, CardBrand::Visa },\n\t};\n\treturn kResult;\n}\n\n[[nodiscard]] bool BinRangeMatchesNumber(\n\t\tconst BinRange &range,\n\t\tconst QString &sanitized) {\n\tconst auto minWithLow = std::min(sanitized.size(), range.low.size());\n\tif (base::StringViewMid(sanitized, 0, minWithLow).toInt()\n\t\t< base::StringViewMid(range.low, 0, minWithLow).toInt()) {\n\t\treturn false;\n\t}\n\tconst auto minWithHigh = std::min(sanitized.size(), range.high.size());\n\tif (base::StringViewMid(sanitized, 0, minWithHigh).toInt()\n\t\t> base::StringViewMid(range.high, 0, minWithHigh).toInt()) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\n[[nodiscard]] bool IsNumeric(const QString &value) {\n\tstatic const auto RegExp = QRegularExpression(\"^[0-9]*$\");\n\treturn RegExp.match(value).hasMatch();\n}\n\n[[nodiscard]] QString RemoveWhitespaces(QString value) {\n\tstatic const auto RegExp = QRegularExpression(\"\\\\s\");\n\treturn value.replace(RegExp, QString());\n}\n\n[[nodiscard]] std::vector<BinRange> BinRangesForNumber(\n\t\tconst QString &sanitized) {\n\tconst auto &all = AllRanges();\n\tauto result = std::vector<BinRange>();\n\tresult.reserve(all.size());\n\tfor (const auto &range : all) {\n\t\tif (BinRangeMatchesNumber(range, sanitized)) {\n\t\t\tresult.push_back(range);\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] BinRange MostSpecificBinRangeForNumber(\n\t\tconst QString &sanitized) {\n\tauto possible = BinRangesForNumber(sanitized);\n\tconst auto compare = [&](const BinRange &a, const BinRange &b) {\n\t\tif (sanitized.isEmpty()) {\n\t\t\tconst auto aUnknown = (a.brand == CardBrand::Unknown);\n\t\t\tconst auto bUnknown = (b.brand == CardBrand::Unknown);\n\t\t\tif (aUnknown && !bUnknown) {\n\t\t\t\treturn true;\n\t\t\t} else if (!aUnknown && bUnknown) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn a.low.size() < b.low.size();\n\t};\n\tstd::sort(begin(possible), end(possible), compare);\n\treturn possible.back();\n}\n\n[[nodiscard]] int MaxCvcLengthForBranch(CardBrand brand) {\n\tswitch (brand) {\n\tcase CardBrand::Amex:\n\tcase CardBrand::Unknown:\n\t\treturn 4;\n\tdefault:\n\t\treturn 3;\n\t}\n}\n\n[[nodiscard]] std::vector<CardBrand> PossibleBrandsForNumber(\n\t\tconst QString &sanitized) {\n\tconst auto ranges = BinRangesForNumber(sanitized);\n\tauto result = std::vector<CardBrand>();\n\tfor (const auto &range : ranges) {\n\t\tconst auto brand = range.brand;\n\t\tif (brand == CardBrand::Unknown\n\t\t\t|| (std::find(begin(result), end(result), brand)\n\t\t\t\t!= end(result))) {\n\t\t\tcontinue;\n\t\t}\n\t\tresult.push_back(brand);\n\t}\n\treturn result;\n}\n\n[[nodiscard]] CardBrand BrandForNumber(const QString &number) {\n\tconst auto sanitized = RemoveWhitespaces(number);\n\tif (!IsNumeric(sanitized)) {\n\t\treturn CardBrand::Unknown;\n\t}\n\tconst auto possible = PossibleBrandsForNumber(sanitized);\n\treturn (possible.size() == 1) ? possible.front() : CardBrand::Unknown;\n}\n\n[[nodiscard]] bool IsValidLuhn(const QString &sanitized) {\n\tauto odd = true;\n\tauto sum = 0;\n\tfor (auto i = sanitized.end(); i != sanitized.begin();) {\n\t\t--i;\n\t\tauto digit = int(i->unicode() - '0');\n\t\todd = !odd;\n\t\tif (odd) {\n\t\t\tdigit *= 2;\n\t\t}\n\t\tif (digit > 9) {\n\t\t\tdigit -= 9;\n\t\t}\n\t\tsum += digit;\n\t}\n\treturn (sum % 10) == 0;\n}\n\n} // namespace\n\nCardValidationResult ValidateCard(const QString &number) {\n\tconst auto sanitized = RemoveWhitespaces(number);\n\tif (!IsNumeric(sanitized)) {\n\t\treturn { .state = ValidationState::Invalid };\n\t} else if (sanitized.isEmpty()) {\n\t\treturn { .state = ValidationState::Incomplete };\n\t}\n\tconst auto range = MostSpecificBinRangeForNumber(sanitized);\n\tconst auto brand = range.brand;\n\n\tstatic const auto &all = AllRanges();\n\tstatic const auto compare = [](const BinRange &a, const BinRange &b) {\n\t\treturn a.length < b.length;\n\t};\n\tstatic const auto kMinLength = std::min_element(\n\t\tbegin(all),\n\t\tend(all),\n\t\tcompare)->length;\n\tstatic const auto kMaxLength = std::max_element(\n\t\tbegin(all),\n\t\tend(all),\n\t\tcompare)->length;\n\n\tif (sanitized.size() > kMaxLength) {\n\t\treturn { .state = ValidationState::Invalid, .brand = brand };\n\t} else if (sanitized.size() < kMinLength) {\n\t\treturn { .state = ValidationState::Incomplete, .brand = brand };\n\t} else if (!IsValidLuhn(sanitized)) {\n\t\treturn { .state = ValidationState::Invalid, .brand = brand };\n\t} else if (sanitized.size() < kMaxLength) {\n\t\treturn { .state = ValidationState::Valid, .brand = brand };\n\t}\n\treturn {\n\t\t.state = ValidationState::Valid,\n\t\t.brand = brand,\n\t\t.finished = true,\n\t};\n}\n\nExpireDateValidationResult ValidateExpireDate(\n\t\tconst QString &date,\n\t\tconst std::optional<QDate> &overrideExpireDateThreshold) {\n\tconst auto sanitized = RemoveWhitespaces(date).replace('/', QString());\n\tif (!IsNumeric(sanitized)) {\n\t\treturn { ValidationState::Invalid };\n\t} else if (sanitized.size() < 2) {\n\t\treturn { ValidationState::Incomplete };\n\t}\n\tconst auto normalized = (sanitized[0] > '1' ? \"0\" : \"\") + sanitized;\n\tconst auto month = base::StringViewMid(normalized, 0, 2).toInt();\n\tif (month < 1 || month > 12) {\n\t\treturn { ValidationState::Invalid };\n\t} else if (normalized.size() < 4) {\n\t\treturn { ValidationState::Incomplete };\n\t} else if (normalized.size() > 4) {\n\t\treturn { ValidationState::Invalid };\n\t}\n\tconst auto year = 2000 + base::StringViewMid(normalized, 2).toInt();\n\n\tconst auto thresholdDate = overrideExpireDateThreshold.value_or(\n\t\tQDate::currentDate());\n\tconst auto thresholdMonth = thresholdDate.month();\n\tconst auto thresholdYear = thresholdDate.year();\n\tif (year < thresholdYear) {\n\t\treturn { ValidationState::Invalid };\n\t} else if (year == thresholdYear && month < thresholdMonth) {\n\t\treturn { ValidationState::Invalid };\n\t}\n\treturn { ValidationState::Valid, true };\n}\n\nValidationState ValidateParsedExpireDate(\n\t\tquint32 month,\n\t\tquint32 year,\n\t\tconst std::optional<QDate> &overrideExpireDateThreshold) {\n\tif ((year / 100) != 20) {\n\t\treturn ValidationState::Invalid;\n\t}\n\tconst auto date = QString(\"%1%2\"\n\t).arg(month, 2, 10, QChar('0')\n\t).arg(year % 100, 2, 10, QChar('0'));\n\n\treturn ValidateExpireDate(date, overrideExpireDateThreshold).state;\n}\n\nCvcValidationResult ValidateCvc(\n\t\tconst QString &number,\n\t\tconst QString &cvc) {\n\tif (!IsNumeric(cvc)) {\n\t\treturn { ValidationState::Invalid };\n\t} else if (cvc.size() < kMinCvcLength) {\n\t\treturn { ValidationState::Incomplete };\n\t}\n\tconst auto maxLength = MaxCvcLengthForBranch(BrandForNumber(number));\n\tif (cvc.size() > maxLength) {\n\t\treturn { ValidationState::Invalid };\n\t}\n\treturn { ValidationState::Valid, (cvc.size() == maxLength) };\n}\n\nstd::vector<int> CardNumberFormat(const QString &number) {\n\tstatic const auto kDefault = std::vector{ 4, 4, 4, 4 };\n\tconst auto sanitized = RemoveWhitespaces(number);\n\tif (!IsNumeric(sanitized)) {\n\t\treturn kDefault;\n\t}\n\tconst auto range = MostSpecificBinRangeForNumber(sanitized);\n\tif (range.brand == CardBrand::DinersClub && range.length == 14) {\n\t\treturn { 4, 6, 4 };\n\t} else if (range.brand == CardBrand::Amex) {\n\t\treturn { 4, 6, 5 };\n\t}\n\treturn kDefault;\n}\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_card_validator.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"stripe/stripe_card.h\"\n#include <vector>\n#include <optional>\n\nclass QDate;\n\nnamespace Stripe {\n\nenum class ValidationState {\n\tInvalid,\n\tIncomplete,\n\tValid,\n};\n\nstruct CardValidationResult {\n\tValidationState state = ValidationState::Invalid;\n\tCardBrand brand = CardBrand::Unknown;\n\tbool finished = false;\n};\n\n[[nodiscard]] CardValidationResult ValidateCard(const QString &number);\n\nstruct ExpireDateValidationResult {\n\tValidationState state = ValidationState::Invalid;\n\tbool finished = false;\n};\n\n[[nodiscard]] ExpireDateValidationResult ValidateExpireDate(\n\tconst QString &date,\n\tconst std::optional<QDate> &overrideExpireDateThreshold);\n\n[[nodiscard]] ValidationState ValidateParsedExpireDate(\n\tquint32 month,\n\tquint32 year,\n\tconst std::optional<QDate> &overrideExpireDateThreshold);\n\nstruct CvcValidationResult {\n\tValidationState state = ValidationState::Invalid;\n\tbool finished = false;\n};\n\n[[nodiscard]] CvcValidationResult ValidateCvc(\n\tconst QString &number,\n\tconst QString &cvc);\n\n[[nodiscard]] std::vector<int> CardNumberFormat(const QString &number);\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_decode.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"stripe/stripe_decode.h\"\n\nnamespace Stripe {\n\n[[nodiscard]] bool ContainsFields(\n\t\tconst QJsonObject &object,\n\t\tstd::vector<QStringView> keys) {\n\tfor (const auto &key : keys) {\n\t\tif (object.value(key).isUndefined()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_decode.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QJsonObject>\n#include <vector>\n\nnamespace Stripe {\n\n[[nodiscard]] bool ContainsFields(\n\tconst QJsonObject &object,\n\tstd::vector<QStringView> keys);\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_error.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"stripe/stripe_error.h\"\n\n#include \"stripe/stripe_decode.h\"\n\nnamespace Stripe {\n\nError::Code Error::code() const {\n\treturn _code;\n}\n\nQString Error::description() const {\n\treturn _description;\n}\n\nQString Error::message() const {\n\treturn _message;\n}\n\nQString Error::parameter() const {\n\treturn _parameter;\n}\n\nError Error::None() {\n\treturn Error(Code::None, {}, {}, {});\n}\n\nError Error::DecodedObjectFromResponse(QJsonObject object) {\n\tconst auto entry = object.value(\"error\");\n\tif (!entry.isObject()) {\n\t\treturn Error::None();\n\t}\n\tconst auto error = entry.toObject();\n\tconst auto string = [&](QStringView key) {\n\t\treturn error.value(key).toString();\n\t};\n\tconst auto type = string(u\"type\");\n\tconst auto message = string(u\"message\");\n\tconst auto parameterSnakeCase = string(u\"param\");\n\n\t// There should always be a message and type for the error\n\tif (message.isEmpty() || type.isEmpty()) {\n\t\treturn {\n\t\t\tCode::API,\n\t\t\t\"GenericError\",\n\t\t\t\"Could not interpret the error response \"\n\t\t\t\"that was returned from Stripe.\"\n\t\t};\n\t}\n\n\tauto parameterWords = parameterSnakeCase.isEmpty()\n\t\t? QStringList()\n\t\t: parameterSnakeCase.split('_', Qt::SkipEmptyParts);\n\tauto first = true;\n\tfor (auto &word : parameterWords) {\n\t\tif (first) {\n\t\t\tfirst = false;\n\t\t} else {\n\t\t\tword = word[0].toUpper() + word.mid(1);\n\t\t}\n\t}\n\tconst auto parameter = parameterWords.join(QString());\n\tif (type == \"api_error\") {\n\t\treturn { Code::API, \"GenericError\", message, parameter };\n\t} else if (type == \"invalid_request_error\") {\n\t\treturn { Code::InvalidRequest, \"GenericError\", message, parameter };\n\t} else if (type != \"card_error\") {\n\t\treturn { Code::Unknown, type, message, parameter };\n\t}\n\tconst auto code = string(u\"code\");\n\tconst auto cardError = [&](const QString &description) {\n\t\treturn Error{ Code::Card, description, message, parameter };\n\t};\n\tif (code == \"incorrect_number\") {\n\t\treturn cardError(\"IncorrectNumber\");\n\t} else if (code == \"invalid_number\") {\n\t\treturn cardError(\"InvalidNumber\");\n\t} else if (code == \"invalid_expiry_month\") {\n\t\treturn cardError(\"InvalidExpiryMonth\");\n\t} else if (code == \"invalid_expiry_year\") {\n\t\treturn cardError(\"InvalidExpiryYear\");\n\t} else if (code == \"invalid_cvc\") {\n\t\treturn cardError(\"InvalidCVC\");\n\t} else if (code == \"expired_card\") {\n\t\treturn cardError(\"ExpiredCard\");\n\t} else if (code == \"incorrect_cvc\") {\n\t\treturn cardError(\"IncorrectCVC\");\n\t} else if (code == \"card_declined\") {\n\t\treturn cardError(\"CardDeclined\");\n\t} else if (code == \"processing_error\") {\n\t\treturn cardError(\"ProcessingError\");\n\t} else {\n\t\treturn cardError(code);\n\t}\n}\n\nbool Error::empty() const {\n\treturn (_code == Code::None);\n}\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_error.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QString>\n\nclass QJsonObject;\n\nnamespace Stripe {\n\nclass Error {\npublic:\n\tenum class Code {\n\t\tNone = 0, // Non-Stripe errors.\n\t\tJsonParse = -1,\n\t\tJsonFormat = -2,\n\t\tNetwork = -3,\n\n\t\tUnknown = 8,\n\t\tConnection = 40, // Trouble connecting to Stripe.\n\t\tInvalidRequest = 50, // Your request had invalid parameters.\n\t\tAPI = 60, // General-purpose API error (should be rare).\n\t\tCard = 70, // Something was wrong with the given card (most common).\n\t\tCancellation = 80, // The operation was cancelled.\n\t\tCheckoutUnknown = 5000, // Checkout failed\n\t\tCheckoutTooManyAttempts = 5001, // Too many incorrect code attempts\n\t};\n\n\tError(\n\t\tCode code,\n\t\tconst QString &description,\n\t\tconst QString &message,\n\t\tconst QString &parameter = QString())\n\t: _code(code)\n\t, _description(description)\n\t, _message(message)\n\t, _parameter(parameter) {\n\t}\n\n\t[[nodiscard]] Code code() const;\n\t[[nodiscard]] QString description() const;\n\t[[nodiscard]] QString message() const;\n\t[[nodiscard]] QString parameter() const;\n\n\t[[nodiscard]] static Error None();\n\t[[nodiscard]] static Error DecodedObjectFromResponse(QJsonObject object);\n\n\t[[nodiscard]] bool empty() const;\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n\nprivate:\n\tCode _code = Code::None;\n\tQString _description;\n\tQString _message;\n\tQString _parameter;\n\n};\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_form_encodable.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QString>\n#include <map>\n\nnamespace Stripe {\n\nclass FormEncodable {\npublic:\n\t[[nodiscard]] virtual QString rootObjectName() = 0;\n\t[[nodiscard]] virtual std::map<QString, QString> formFieldValues() = 0;\n};\n\ntemplate <typename T>\nstruct MakeEncodable final : FormEncodable {\npublic:\n\tMakeEncodable(const T &value) : _value(value) {\n\t}\n\n\tQString rootObjectName() override {\n\t\treturn _value.rootObjectName();\n\t}\n\tstd::map<QString, QString> formFieldValues() override {\n\t\treturn _value.formFieldValues();\n\t}\n\nprivate:\n\tconst T &_value;\n\n};\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_form_encoder.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"stripe/stripe_form_encoder.h\"\n\n#include <QStringList>\n#include <QUrl>\n#include <vector>\n\nnamespace Stripe {\n\nQByteArray FormEncoder::formEncodedDataForObject(\n\t\tFormEncodable &&object) {\n\tconst auto root = object.rootObjectName();\n\tconst auto values = object.formFieldValues();\n\tauto result = QByteArray();\n\tauto keys = std::vector<QString>();\n\tfor (const auto &[key, value] : values) {\n\t\tif (!value.isEmpty()) {\n\t\t\tkeys.push_back(key);\n\t\t}\n\t}\n\tstd::sort(begin(keys), end(keys));\n\tconst auto encode = [](const QString &value) {\n\t\treturn QUrl::toPercentEncoding(value);\n\t};\n\tfor (const auto &key : keys) {\n\t\tconst auto fullKey = root.isEmpty() ? key : (root + '[' + key + ']');\n\t\tif (!result.isEmpty()) {\n\t\t\tresult += '&';\n\t\t}\n\t\tresult += encode(fullKey) + '=' + encode(values.at(key));\n\t}\n\treturn result;\n}\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_form_encoder.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"stripe/stripe_form_encodable.h\"\n\nnamespace Stripe {\n\nclass FormEncoder {\npublic:\n\t[[nodiscard]] static QByteArray formEncodedDataForObject(\n\t\tFormEncodable &&object);\n\n};\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_payment_configuration.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"stripe/stripe_address.h\"\n\n#include <QtCore/QString>\n\nnamespace Stripe {\n\nstruct PaymentConfiguration {\n\tQString publishableKey;\n\t// PaymentMethodType additionalPaymentMethods; // Apply Pay\n\n\t// TODO incomplete, not used.\n\t//BillingAddressFields requiredBillingAddressFields\n\t//\t= BillingAddressFields::None;\n\n\tQString companyName;\n\t// QString appleMerchantIdentifier; // Apple Pay\n\t// bool smsAutofillDisabled = true; // Mobile only\n};\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_pch.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\n#include <QtCore/QString>\n#include <QtCore/QDateTime>\n#include <QtCore/QJsonObject>\n#include <QtCore/QJsonValue>\n#include <QtCore/QJsonDocument>\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_token.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"stripe/stripe_token.h\"\n\n#include \"stripe/stripe_decode.h\"\n\nnamespace Stripe {\n\nQString Token::tokenId() const {\n\treturn _tokenId;\n}\n\nbool Token::livemode() const {\n\treturn _livemode;\n}\n\nCard Token::card() const {\n\treturn _card;\n}\n\nToken Token::Empty() {\n\treturn Token(QString(), false, QDateTime());\n}\n\nToken Token::DecodedObjectFromAPIResponse(QJsonObject object) {\n\tif (!ContainsFields(object, { u\"id\", u\"livemode\", u\"created\" })) {\n\t\treturn Token::Empty();\n\t}\n\tconst auto tokenId = object.value(\"id\").toString();\n\tconst auto livemode = object.value(\"livemode\").toBool();\n\tconst auto created = QDateTime::fromSecsSinceEpoch(\n\t\tobject.value(\"created\").toDouble());\n\tauto result = Token(tokenId, livemode, created);\n\tconst auto card = object.value(\"card\");\n\tif (card.isObject()) {\n\t\tresult._card = Card::DecodedObjectFromAPIResponse(card.toObject());\n\t}\n\n\t// TODO incomplete, not used.\n\t//const auto bankAccount = object.value(\"bank_account\");\n\t//if (bankAccount.isObject()) {\n\t//\tresult._bankAccount = bankAccount::DecodedObjectFromAPIResponse(\n\t//\t\tbankAccount.toObject());\n\t//}\n\t//result._allResponseFields = object;\n\n\treturn result;\n}\n\nbool Token::empty() const {\n\treturn _tokenId.isEmpty();\n}\n\nToken::Token(QString tokenId, bool livemode, QDateTime created)\n: _tokenId(std::move(tokenId))\n, _livemode(livemode)\n, _created(std::move(created)) {\n}\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/stripe/stripe_token.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"stripe/stripe_card.h\"\n\n#include <QtCore/QDateTime>\n\nclass QJsonObject;\n\nnamespace Stripe {\n\nclass Token {\npublic:\n\tToken(const Token &other) = default;\n\tToken &operator=(const Token &other) = default;\n\tToken(Token &&other) = default;\n\tToken &operator=(Token &&other) = default;\n\t~Token() = default;\n\n\t[[nodiscard]] QString tokenId() const;\n\t[[nodiscard]] bool livemode() const;\n\t[[nodiscard]] Card card() const;\n\n\t[[nodiscard]] static Token Empty();\n\t[[nodiscard]] static Token DecodedObjectFromAPIResponse(\n\t\tQJsonObject object);\n\n\t[[nodiscard]] bool empty() const;\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n\nprivate:\n\tToken(QString tokenId, bool livemode, QDateTime created);\n\n\tQString _tokenId;\n\tbool _livemode = false;\n\tQDateTime _created;\n\tCard _card = Card::Empty();\n\n};\n\n} // namespace Stripe\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/ui/payments.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\n\nusing \"info/info.style\";\n\npaymentsPanelSize: size(392px, 600px);\n\npaymentsPanelButton: RoundButton(defaultBoxButton) {\n\twidth: -36px;\n\theight: 36px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: boxButtonFont;\n\t}\n}\npaymentsPanelSubmit: RoundButton(defaultActiveButton) {\n\twidth: -36px;\n\theight: 36px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: boxButtonFont;\n\t}\n}\npaymentsPanelPadding: margins(8px, 12px, 15px, 12px);\n\npaymentsCoverPadding: margins(26px, 0px, 26px, 13px);\npaymentsDescription: FlatLabel(defaultFlatLabel) {\n\tminWidth: 160px;\n\ttextFg: windowFg;\n}\npaymentsTitle: FlatLabel(paymentsDescription) {\n\tstyle: semiboldTextStyle;\n}\npaymentsSeller: FlatLabel(paymentsDescription) {\n\ttextFg: windowSubTextFg;\n}\npaymentsWebviewBottom: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n}\npaymentsPriceLabel: paymentsDescription;\npaymentsPriceAmount: defaultFlatLabel;\npaymentsFullPriceLabel: paymentsTitle;\npaymentsFullPriceAmount: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n}\n\npaymentsTitleTop: 0px;\npaymentsDescriptionTop: 3px;\npaymentsSellerTop: 4px;\n\npaymentsThumbnailSize: size(80px, 80px);\npaymentsThumbnailSkip: 18px;\n\npaymentsPricesTopSkip: 12px;\npaymentsPricesBottomSkip: 13px;\npaymentsPricePadding: margins(28px, 6px, 28px, 5px);\n\npaymentsTipSkip: 8px;\npaymentsTipButton: RoundButton(defaultLightButton) {\n\ttextFg: paymentsTipActive;\n\ttextFgOver: paymentsTipActive;\n\ttextBgOver: transparent;\n\n\twidth: -16px;\n\theight: 28px;\n\ttextTop: 5px;\n}\npaymentsTipChosen: RoundButton(paymentsTipButton) {\n\ttextFg: windowFgActive;\n\ttextFgOver: windowFgActive;\n\ttextBgOver: transparent;\n}\npaymentsTipButtonsPadding: margins(26px, 6px, 26px, 6px);\n\npaymentsSectionsTopSkip: 11px;\npaymentsSectionButton: SettingsButton(infoProfileButton) {\n\tpadding: margins(68px, 11px, 14px, 9px);\n}\n\npaymentsIconPaymentMethod: icon {{ \"payments/payment_card\", windowBoldFg }};\npaymentsIconShippingAddress: icon {{ \"payments/payment_address\", windowBoldFg }};\npaymentsIconName: icon {{ \"payments/payment_name\", windowBoldFg }};\npaymentsIconEmail: icon {{ \"payments/payment_email\", windowBoldFg }};\npaymentsIconPhone: icon {{ \"payments/payment_phone\", windowBoldFg }};\npaymentsIconShippingMethod: icon {{ \"payments/payment_shipping\", windowBoldFg }};\n\npaymentsField: defaultInputField;\npaymentsMoneyField: InputField(paymentsField) {\n\ttextMargins: margins(0px, 4px, 0px, 4px);\n\theightMin: 30px;\n}\npaymentsFieldAdditional: FlatLabel(defaultFlatLabel) {\n\tstyle: boxTextStyle;\n}\n\npaymentsFieldPadding: margins(28px, 0px, 28px, 2px);\npaymentsSaveCheckboxPadding: margins(28px, 20px, 28px, 8px);\npaymentsExpireCvcSkip: 34px;\n\npaymentsBillingInformationTitle: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n\ttextFg: windowActiveTextFg;\n\tminWidth: 240px;\n}\npaymentsBillingInformationTitlePadding: margins(28px, 26px, 28px, 1px);\n\npaymentsShippingMargin: margins(27px, 11px, 27px, 20px);\npaymentsShippingLabel: FlatLabel(defaultFlatLabel) {\n\tstyle: boxTextStyle;\n}\npaymentsShippingPrice: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n}\npaymentsShippingLabelPosition: point(43px, 8px);\npaymentsShippingPricePosition: point(43px, 29px);\n\npaymentTipsErrorLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 275px;\n\ttextFg: boxTextFgError;\n}\npaymentTipsErrorPadding: margins(22px, 6px, 22px, 0px);\n\npaymentsToProviderLabel: paymentsShippingPrice;\npaymentsToProviderPadding: margins(28px, 6px, 28px, 6px);\n\npaymentsCriticalError: FlatLabel(boxLabel) {\n\tminWidth: 340px;\n\talign: align(top);\n\ttextFg: windowSubTextFg;\n}\npaymentsCriticalErrorPadding: margins(10px, 40px, 10px, 0px);\n\npaymentsLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {\n\tsize: size(24px, 24px);\n\tcolor: windowSubTextFg;\n\tthickness: 4px;\n}\n\nbotWebViewPanelSize: size(384px, 694px);\nbotWebViewBottomPadding: margins(12px, 12px, 12px, 12px);\nbotWebViewBottomSkip: point(12px, 8px);\nbotWebViewBottomButton: RoundButton(paymentsPanelSubmit) {\n\theight: 40px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: boxButtonFont;\n\t}\n\ttextTop: 11px;\n}\nbotWebViewRadialStroke: 3px;\nbotWebViewMenu: PopupMenu(popupMenuWithIcons) {\n\tmaxHeight: 360px;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/ui/payments_edit_card.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"payments/ui/payments_edit_card.h\"\n\n#include \"payments/ui/payments_panel_delegate.h\"\n#include \"payments/ui/payments_field.h\"\n#include \"stripe/stripe_card_validator.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_payments.h\"\n#include \"styles/style_passport.h\"\n\n#include <QtCore/QRegularExpression>\n\nnamespace Payments::Ui {\nnamespace {\n\nstruct SimpleFieldState {\n\tQString value;\n\tint position = 0;\n};\n\n[[nodiscard]] uint32 ExtractYear(const QString &value) {\n\treturn value.split('/').value(1).toInt() + 2000;\n}\n\n[[nodiscard]] uint32 ExtractMonth(const QString &value) {\n\treturn value.split('/').value(0).toInt();\n}\n\n[[nodiscard]] QString RemoveNonNumbers(QString value) {\n\tstatic const auto RegExp = QRegularExpression(\"[^0-9]\");\n\treturn value.replace(RegExp, QString());\n}\n\n[[nodiscard]] SimpleFieldState NumbersOnlyState(SimpleFieldState state) {\n\treturn {\n\t\t.value = RemoveNonNumbers(state.value),\n\t\t.position = int(RemoveNonNumbers(\n\t\t\tstate.value.mid(0, state.position)).size()),\n\t};\n}\n\n[[nodiscard]] SimpleFieldState PostprocessCardValidateResult(\n\t\tSimpleFieldState result) {\n\tconst auto groups = Stripe::CardNumberFormat(result.value);\n\tauto position = 0;\n\tfor (const auto length : groups) {\n\t\tposition += length;\n\t\tif (position >= result.value.size()) {\n\t\t\tbreak;\n\t\t}\n\t\tresult.value.insert(position, QChar(' '));\n\t\tif (result.position >= position) {\n\t\t\t++result.position;\n\t\t}\n\t\t++position;\n\t}\n\treturn result;\n}\n\n[[nodiscard]] SimpleFieldState PostprocessExpireDateValidateResult(\n\t\tSimpleFieldState result) {\n\tif (result.value.isEmpty()) {\n\t\treturn result;\n\t} else if (result.value[0] == '1'\n\t\t&& (result.value.size() > 1)\n\t\t&& result.value[1] > '2') {\n\t\tresult.value = result.value.mid(0, 2);\n\t\treturn result;\n\t} else if (result.value[0] > '1') {\n\t\tresult.value = '0' + result.value;\n\t\t++result.position;\n\t}\n\tif (result.value.size() > 1) {\n\t\tif (result.value.size() > 4) {\n\t\t\tresult.value = result.value.mid(0, 4);\n\t\t}\n\t\tresult.value.insert(2, '/');\n\t\tif (result.position >= 2) {\n\t\t\t++result.position;\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] bool IsBackspace(const FieldValidateRequest &request) {\n\treturn (request.wasAnchor == request.wasPosition)\n\t\t&& (request.wasPosition == request.nowPosition + 1)\n\t\t&& (request.wasValue.mid(0, request.wasPosition - 1)\n\t\t\t== request.nowValue.mid(0, request.nowPosition))\n\t\t&& (request.wasValue.mid(request.wasPosition)\n\t\t\t== request.nowValue.mid(request.nowPosition));\n}\n\n[[nodiscard]] bool IsDelete(const FieldValidateRequest &request) {\n\treturn (request.wasAnchor == request.wasPosition)\n\t\t&& (request.wasPosition == request.nowPosition)\n\t\t&& (request.wasValue.mid(0, request.wasPosition)\n\t\t\t== request.nowValue.mid(0, request.nowPosition))\n\t\t&& (request.wasValue.mid(request.wasPosition + 1)\n\t\t\t== request.nowValue.mid(request.nowPosition));\n}\n\ntemplate <\n\ttypename ValueValidator,\n\ttypename ValueValidateResult = decltype(\n\t\tstd::declval<ValueValidator>()(QString()))>\n[[nodiscard]] auto ComplexNumberValidator(\n\t\tValueValidator valueValidator,\n\t\tFn<SimpleFieldState(SimpleFieldState)> postprocess) {\n\tusing namespace Stripe;\n\treturn [=](FieldValidateRequest request) {\n\t\tconst auto realNowState = [&] {\n\t\t\tconst auto backspaced = IsBackspace(request);\n\t\t\tconst auto deleted = IsDelete(request);\n\t\t\tif (!backspaced && !deleted) {\n\t\t\t\treturn NumbersOnlyState({\n\t\t\t\t\t.value = request.nowValue,\n\t\t\t\t\t.position = request.nowPosition,\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst auto realWasState = NumbersOnlyState({\n\t\t\t\t.value = request.wasValue,\n\t\t\t\t.position = request.wasPosition,\n\t\t\t});\n\t\t\tconst auto changedValue = deleted\n\t\t\t\t? (realWasState.value.mid(0, realWasState.position)\n\t\t\t\t\t+ realWasState.value.mid(realWasState.position + 1))\n\t\t\t\t: (realWasState.position > 1)\n\t\t\t\t? (realWasState.value.mid(0, realWasState.position - 1)\n\t\t\t\t\t+ realWasState.value.mid(realWasState.position))\n\t\t\t\t: realWasState.value.mid(realWasState.position);\n\t\t\treturn SimpleFieldState{\n\t\t\t\t.value = changedValue,\n\t\t\t\t.position = (deleted\n\t\t\t\t\t? realWasState.position\n\t\t\t\t\t: std::max(realWasState.position - 1, 0))\n\t\t\t};\n\t\t}();\n\t\tconst auto result = valueValidator(realNowState.value);\n\t\tconst auto postprocessed = postprocess(realNowState);\n\t\treturn FieldValidateResult{\n\t\t\t.value = postprocessed.value,\n\t\t\t.position = postprocessed.position,\n\t\t\t.invalid = (result.state == ValidationState::Invalid),\n\t\t\t.finished = result.finished,\n\t\t};\n\t};\n\n}\n\n[[nodiscard]] auto CardNumberValidator() {\n\treturn ComplexNumberValidator(\n\t\tStripe::ValidateCard,\n\t\tPostprocessCardValidateResult);\n}\n\n[[nodiscard]] auto ExpireDateValidator(\n\t\tconst std::optional<QDate> &overrideExpireDateThreshold) {\n\treturn ComplexNumberValidator([=](const QString &date) {\n\t\treturn Stripe::ValidateExpireDate(date, overrideExpireDateThreshold);\n\t}, PostprocessExpireDateValidateResult);\n}\n\n[[nodiscard]] auto CvcValidator(Fn<QString()> number) {\n\tusing namespace Stripe;\n\treturn [=](FieldValidateRequest request) {\n\t\tconst auto realNowState = NumbersOnlyState({\n\t\t\t.value = request.nowValue,\n\t\t\t.position = request.nowPosition,\n\t\t});\n\t\tconst auto result = ValidateCvc(number(), realNowState.value);\n\n\t\treturn FieldValidateResult{\n\t\t\t.value = realNowState.value,\n\t\t\t.position = realNowState.position,\n\t\t\t.invalid = (result.state == ValidationState::Invalid),\n\t\t\t.finished = result.finished,\n\t\t};\n\t};\n}\n\n[[nodiscard]] auto CardHolderNameValidator() {\n\treturn [=](FieldValidateRequest request) {\n\t\treturn FieldValidateResult{\n\t\t\t.value = request.nowValue.toUpper(),\n\t\t\t.position = request.nowPosition,\n\t\t\t.invalid = request.nowValue.isEmpty(),\n\t\t};\n\t};\n}\n\n} // namespace\n\nEditCard::EditCard(\n\tQWidget *parent,\n\tconst NativeMethodDetails &native,\n\tCardField field,\n\tnot_null<PanelDelegate*> delegate)\n: _delegate(delegate)\n, _native(native)\n, _scroll(this, st::passportPanelScroll)\n, _topShadow(this)\n, _bottomShadow(this)\n, _submit(\n\tthis,\n\ttr::lng_about_done(),\n\tst::paymentsPanelButton)\n, _cancel(\n\t\tthis,\n\t\ttr::lng_cancel(),\n\t\tst::paymentsPanelButton) {\n\t_submit->setTextTransform(RoundButtonTextTransform::ToUpper);\n\t_cancel->setTextTransform(RoundButtonTextTransform::ToUpper);\n\tsetupControls();\n}\n\nvoid EditCard::setFocus(CardField field) {\n\t_focusField = field;\n\tif (const auto control = lookupField(field)) {\n\t\t_scroll->ensureWidgetVisible(control->widget());\n\t\tcontrol->setFocus();\n\t}\n}\n\nvoid EditCard::setFocusFast(CardField field) {\n\t_focusField = field;\n\tif (const auto control = lookupField(field)) {\n\t\t_scroll->ensureWidgetVisible(control->widget());\n\t\tcontrol->setFocusFast();\n\t}\n}\n\nvoid EditCard::showError(CardField field) {\n\tif (const auto control = lookupField(field)) {\n\t\t_scroll->ensureWidgetVisible(control->widget());\n\t\tcontrol->showError();\n\t}\n}\n\nvoid EditCard::setupControls() {\n\tconst auto inner = setupContent();\n\n\t_submit->addClickHandler([=] {\n\t\t_delegate->panelValidateCard(collect(), (_save && _save->checked()));\n\t});\n\t_cancel->addClickHandler([=] {\n\t\t_delegate->panelCancelEdit();\n\t});\n\n\tusing namespace rpl::mappers;\n\n\t_topShadow->toggleOn(\n\t\t_scroll->scrollTopValue() | rpl::map(_1 > 0));\n\t_bottomShadow->toggleOn(rpl::combine(\n\t\t_scroll->scrollTopValue(),\n\t\t_scroll->heightValue(),\n\t\tinner->heightValue(),\n\t\t_1 + _2 < _3));\n}\n\nnot_null<RpWidget*> EditCard::setupContent() {\n\tconst auto inner = _scroll->setOwnedWidget(\n\t\tobject_ptr<VerticalLayout>(this));\n\n\t_scroll->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tinner->resizeToWidth(width);\n\t}, inner->lifetime());\n\n\tconst auto showBox = [=](object_ptr<BoxContent> box) {\n\t\t_delegate->panelShowBox(std::move(box));\n\t};\n\tauto last = (Field*)nullptr;\n\tconst auto make = [&](QWidget *parent, FieldConfig &&config) {\n\t\tauto result = std::make_unique<Field>(parent, std::move(config));\n\t\tif (last) {\n\t\t\tlast->setNextField(result.get());\n\t\t\tresult->setPreviousField(last);\n\t\t}\n\t\tlast = result.get();\n\t\treturn result;\n\t};\n\tconst auto add = [&](FieldConfig &&config) {\n\t\tauto result = make(inner, std::move(config));\n\t\tinner->add(result->ownedWidget(), st::paymentsFieldPadding);\n\t\treturn result;\n\t};\n\t_number = add({\n\t\t.type = FieldType::CardNumber,\n\t\t.placeholder = tr::lng_payments_card_number(),\n\t\t.validator = CardNumberValidator(),\n\t});\n\tauto container = inner->add(\n\t\tobject_ptr<FixedHeightWidget>(\n\t\t\tinner,\n\t\t\t_number->widget()->height()),\n\t\tst::paymentsFieldPadding);\n\t_expire = make(container, {\n\t\t.type = FieldType::CardExpireDate,\n\t\t.placeholder = tr::lng_payments_card_expire_date(),\n\t\t.validator = ExpireDateValidator(\n\t\t\t_delegate->panelOverrideExpireDateThreshold()),\n\t});\n\t_cvc = make(container, {\n\t\t.type = FieldType::CardCVC,\n\t\t.placeholder = tr::lng_payments_card_cvc(),\n\t\t.validator = CvcValidator([=] { return _number->value(); }),\n\t});\n\tcontainer->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto left = (width - st::paymentsExpireCvcSkip) / 2;\n\t\tconst auto right = width - st::paymentsExpireCvcSkip - left;\n\t\t_expire->widget()->resizeToWidth(left);\n\t\t_cvc->widget()->resizeToWidth(right);\n\t\t_expire->widget()->moveToLeft(0, 0, width);\n\t\t_cvc->widget()->moveToRight(0, 0, width);\n\t}, container->lifetime());\n\n\tif (_native.needCardholderName) {\n\t\t_name = add({\n\t\t\t.type = FieldType::Text,\n\t\t\t.placeholder = tr::lng_payments_card_holder(),\n\t\t\t.validator = CardHolderNameValidator(),\n\t\t});\n\t}\n\n\tif (_native.needCountry || _native.needZip) {\n\t\tinner->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tinner,\n\t\t\t\ttr::lng_payments_billing_address(),\n\t\t\t\tst::paymentsBillingInformationTitle),\n\t\t\tst::paymentsBillingInformationTitlePadding);\n\t}\n\tif (_native.needCountry) {\n\t\t_country = add({\n\t\t\t.type = FieldType::Country,\n\t\t\t.placeholder = tr::lng_payments_billing_country(),\n\t\t\t.validator = RequiredFinishedValidator(),\n\t\t\t.showBox = showBox,\n\t\t\t.defaultCountry = _native.defaultCountry,\n\t\t});\n\t}\n\tif (_native.needZip) {\n\t\t_zip = add({\n\t\t\t.type = FieldType::Text,\n\t\t\t.placeholder = tr::lng_payments_billing_zip_code(),\n\t\t\t.validator = RequiredValidator(),\n\t\t});\n\t\tif (_country) {\n\t\t\t_country->finished(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\t_zip->setFocus();\n\t\t\t}, lifetime());\n\t\t}\n\t}\n\tif (_native.canSaveInformation) {\n\t\t_save = inner->add(\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tinner,\n\t\t\t\ttr::lng_payments_save_information(tr::now),\n\t\t\t\tfalse),\n\t\t\tst::paymentsSaveCheckboxPadding);\n\t}\n\n\tlast->submitted(\n\t) | rpl::on_next([=] {\n\t\t_delegate->panelValidateCard(collect(), _save && _save->checked());\n\t}, lifetime());\n\n\treturn inner;\n}\n\nvoid EditCard::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid EditCard::focusInEvent(QFocusEvent *e) {\n\tif (const auto control = lookupField(_focusField)) {\n\t\tcontrol->setFocusFast();\n\t}\n}\n\nvoid EditCard::updateControlsGeometry() {\n\tconst auto &padding = st::paymentsPanelPadding;\n\tconst auto buttonsHeight = padding.top()\n\t\t+ _cancel->height()\n\t\t+ padding.bottom();\n\tconst auto buttonsTop = height() - buttonsHeight;\n\t_scroll->setGeometry(0, 0, width(), buttonsTop);\n\t_topShadow->resizeToWidth(width());\n\t_topShadow->moveToLeft(0, 0);\n\t_bottomShadow->resizeToWidth(width());\n\t_bottomShadow->moveToLeft(0, buttonsTop - st::lineWidth);\n\tauto right = padding.right();\n\t_submit->moveToRight(right, buttonsTop + padding.top());\n\tright += _submit->width() + padding.left();\n\t_cancel->moveToRight(right, buttonsTop + padding.top());\n\n\t_scroll->updateBars();\n}\n\nauto EditCard::lookupField(CardField field) const -> Field* {\n\tswitch (field) {\n\tcase CardField::Number: return _number.get();\n\tcase CardField::Cvc: return _cvc.get();\n\tcase CardField::ExpireDate: return _expire.get();\n\tcase CardField::Name: return _name.get();\n\tcase CardField::AddressCountry: return _country.get();\n\tcase CardField::AddressZip: return _zip.get();\n\t}\n\tUnexpected(\"Unknown field in EditCard::controlForField.\");\n}\n\nUncheckedCardDetails EditCard::collect() const {\n\treturn {\n\t\t.number = _number ? _number->value() : QString(),\n\t\t.cvc = _cvc ? _cvc->value() : QString(),\n\t\t.expireYear = _expire ? ExtractYear(_expire->value()) : 0,\n\t\t.expireMonth = _expire ? ExtractMonth(_expire->value()) : 0,\n\t\t.cardholderName = _name ? _name->value() : QString(),\n\t\t.addressCountry = _country ? _country->value() : QString(),\n\t\t.addressZip = _zip ? _zip->value() : QString(),\n\t};\n}\n\n} // namespace Payments::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/ui/payments_edit_card.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"payments/ui/payments_panel_data.h\"\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\nclass ScrollArea;\nclass FadeShadow;\nclass RoundButton;\nclass Checkbox;\n} // namespace Ui\n\nnamespace Payments::Ui {\n\nusing namespace ::Ui;\n\nclass PanelDelegate;\nclass Field;\n\nclass EditCard final : public RpWidget {\npublic:\n\tEditCard(\n\t\tQWidget *parent,\n\t\tconst NativeMethodDetails &native,\n\t\tCardField field,\n\t\tnot_null<PanelDelegate*> delegate);\n\n\tvoid setFocus(CardField field);\n\tvoid setFocusFast(CardField field);\n\tvoid showError(CardField field);\n\nprivate:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid focusInEvent(QFocusEvent *e) override;\n\n\tvoid setupControls();\n\t[[nodiscard]] not_null<Ui::RpWidget*> setupContent();\n\tvoid updateControlsGeometry();\n\t[[nodiscard]] Field *lookupField(CardField field) const;\n\n\t[[nodiscard]] UncheckedCardDetails collect() const;\n\n\tconst not_null<PanelDelegate*> _delegate;\n\tNativeMethodDetails _native;\n\n\tobject_ptr<ScrollArea> _scroll;\n\tobject_ptr<FadeShadow> _topShadow;\n\tobject_ptr<FadeShadow> _bottomShadow;\n\tobject_ptr<RoundButton> _submit;\n\tobject_ptr<RoundButton> _cancel;\n\n\tstd::unique_ptr<Field> _number;\n\tstd::unique_ptr<Field> _cvc;\n\tstd::unique_ptr<Field> _expire;\n\tstd::unique_ptr<Field> _name;\n\tstd::unique_ptr<Field> _country;\n\tstd::unique_ptr<Field> _zip;\n\tCheckbox *_save = nullptr;\n\n\tCardField _focusField = CardField::Number;\n\n};\n\n} // namespace Payments::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/ui/payments_edit_information.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"payments/ui/payments_edit_information.h\"\n\n#include \"payments/ui/payments_panel_delegate.h\"\n#include \"payments/ui/payments_field.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_payments.h\"\n#include \"styles/style_passport.h\"\n\nnamespace Payments::Ui {\nnamespace {\n\nconstexpr auto kMaxStreetSize = 64;\nconstexpr auto kMaxPostcodeSize = 10;\nconstexpr auto kMaxNameSize = 64;\nconstexpr auto kMaxEmailSize = 128;\nconstexpr auto kMaxPhoneSize = 16;\nconstexpr auto kMinCitySize = 2;\nconstexpr auto kMaxCitySize = 64;\n\n} // namespace\n\nEditInformation::EditInformation(\n\tQWidget *parent,\n\tconst Invoice &invoice,\n\tconst RequestedInformation &current,\n\tInformationField field,\n\tnot_null<PanelDelegate*> delegate)\n: _delegate(delegate)\n, _invoice(invoice)\n, _information(current)\n, _scroll(this, st::passportPanelScroll)\n, _topShadow(this)\n, _bottomShadow(this)\n, _submit(\n\tthis,\n\ttr::lng_settings_save(),\n\tst::paymentsPanelButton)\n, _cancel(\n\t\tthis,\n\t\ttr::lng_cancel(),\n\t\tst::paymentsPanelButton) {\n\t_submit->setTextTransform(RoundButtonTextTransform::ToUpper);\n\t_cancel->setTextTransform(RoundButtonTextTransform::ToUpper);\n\tsetupControls();\n}\n\nEditInformation::~EditInformation() = default;\n\nvoid EditInformation::setFocus(InformationField field) {\n\t_focusField = field;\n\tif (const auto control = lookupField(field)) {\n\t\t_scroll->ensureWidgetVisible(control->widget());\n\t\tcontrol->setFocus();\n\t}\n}\n\nvoid EditInformation::setFocusFast(InformationField field) {\n\t_focusField = field;\n\tif (const auto control = lookupField(field)) {\n\t\t_scroll->ensureWidgetVisible(control->widget());\n\t\tcontrol->setFocusFast();\n\t}\n}\n\nvoid EditInformation::showError(InformationField field) {\n\tif (const auto control = lookupField(field)) {\n\t\t_scroll->ensureWidgetVisible(control->widget());\n\t\tcontrol->showError();\n\t}\n}\n\nvoid EditInformation::setupControls() {\n\tconst auto inner = setupContent();\n\n\t_submit->addClickHandler([=] {\n\t\t_delegate->panelValidateInformation(collect());\n\t});\n\t_cancel->addClickHandler([=] {\n\t\t_delegate->panelCancelEdit();\n\t});\n\n\tusing namespace rpl::mappers;\n\n\t_topShadow->toggleOn(\n\t\t_scroll->scrollTopValue() | rpl::map(_1 > 0));\n\t_bottomShadow->toggleOn(rpl::combine(\n\t\t_scroll->scrollTopValue(),\n\t\t_scroll->heightValue(),\n\t\tinner->heightValue(),\n\t\t_1 + _2 < _3));\n}\n\nnot_null<RpWidget*> EditInformation::setupContent() {\n\tconst auto inner = _scroll->setOwnedWidget(\n\t\tobject_ptr<VerticalLayout>(this));\n\n\t_scroll->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tinner->resizeToWidth(width);\n\t}, inner->lifetime());\n\n\tconst auto showBox = [=](object_ptr<BoxContent> box) {\n\t\t_delegate->panelShowBox(std::move(box));\n\t};\n\tauto last = (Field*)nullptr;\n\tconst auto add = [&](FieldConfig &&config) {\n\t\tauto result = std::make_unique<Field>(inner, std::move(config));\n\t\tinner->add(result->ownedWidget(), st::paymentsFieldPadding);\n\t\tif (last) {\n\t\t\tlast->setNextField(result.get());\n\t\t\tresult->setPreviousField(last);\n\t\t}\n\t\tlast = result.get();\n\t\treturn result;\n\t};\n\tif (_invoice.isShippingAddressRequested) {\n\t\t_street1 = add({\n\t\t\t.placeholder = tr::lng_payments_address_street1(),\n\t\t\t.value = _information.shippingAddress.address1,\n\t\t\t.validator = RangeLengthValidator(1, kMaxStreetSize),\n\t\t});\n\t\t_street2 = add({\n\t\t\t.placeholder = tr::lng_payments_address_street2(),\n\t\t\t.value = _information.shippingAddress.address2,\n\t\t\t.validator = MaxLengthValidator(kMaxStreetSize),\n\t\t});\n\t\t_city = add({\n\t\t\t.placeholder = tr::lng_payments_address_city(),\n\t\t\t.value = _information.shippingAddress.city,\n\t\t\t.validator = RangeLengthValidator(kMinCitySize, kMaxCitySize),\n\t\t});\n\t\t_state = add({\n\t\t\t.placeholder = tr::lng_payments_address_state(),\n\t\t\t.value = _information.shippingAddress.state,\n\t\t});\n\t\t_country = add({\n\t\t\t.type = FieldType::Country,\n\t\t\t.placeholder = tr::lng_payments_address_country(),\n\t\t\t.value = _information.shippingAddress.countryIso2,\n\t\t\t.validator = RequiredFinishedValidator(),\n\t\t\t.showBox = showBox,\n\t\t\t.defaultCountry = _information.defaultCountry,\n\t\t});\n\t\t_postcode = add({\n\t\t\t.placeholder = tr::lng_payments_address_postcode(),\n\t\t\t.value = _information.shippingAddress.postcode,\n\t\t\t.validator = RangeLengthValidator(1, kMaxPostcodeSize),\n\t\t});\n\t}\n\tif (_invoice.isNameRequested) {\n\t\t_name = add({\n\t\t\t.placeholder = tr::lng_payments_info_name(),\n\t\t\t.value = _information.name,\n\t\t\t.validator = RangeLengthValidator(1, kMaxNameSize),\n\t\t});\n\t}\n\tif (_invoice.isEmailRequested) {\n\t\t_email = add({\n\t\t\t.type = FieldType::Email,\n\t\t\t.placeholder = tr::lng_payments_info_email(),\n\t\t\t.value = _information.email,\n\t\t\t.validator = RangeLengthValidator(1, kMaxEmailSize),\n\t\t});\n\t}\n\tif (_invoice.isPhoneRequested) {\n\t\t_phone = add({\n\t\t\t.type = FieldType::Phone,\n\t\t\t.placeholder = tr::lng_payments_info_phone(),\n\t\t\t.value = _information.phone,\n\t\t\t.validator = RangeLengthValidator(1, kMaxPhoneSize),\n\t\t\t.defaultPhone = _information.defaultPhone,\n\t\t});\n\t}\n\tconst auto emailToProvider = _invoice.isEmailRequested\n\t\t&& _invoice.emailSentToProvider;\n\tconst auto phoneToProvider = _invoice.isPhoneRequested\n\t\t&& _invoice.phoneSentToProvider;\n\tif (emailToProvider || phoneToProvider) {\n\t\tinner->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tinner,\n\t\t\t\t((emailToProvider && phoneToProvider)\n\t\t\t\t\t? tr::lng_payments_to_provider_phone_email\n\t\t\t\t\t: emailToProvider\n\t\t\t\t\t? tr::lng_payments_to_provider_email\n\t\t\t\t\t: tr::lng_payments_to_provider_phone)(\n\t\t\t\t\t\tlt_provider,\n\t\t\t\t\t\trpl::single(_invoice.provider)),\n\t\t\t\tst::paymentsToProviderLabel),\n\t\t\tst::paymentsToProviderPadding);\n\t}\n\t_save = inner->add(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tinner,\n\t\t\ttr::lng_payments_save_information(tr::now),\n\t\t\ttrue),\n\t\tst::paymentsSaveCheckboxPadding);\n\n\tif (last) {\n\t\tlast->submitted(\n\t\t) | rpl::on_next([=] {\n\t\t\t_delegate->panelValidateInformation(collect());\n\t\t}, lifetime());\n\t}\n\n\treturn inner;\n}\n\nvoid EditInformation::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid EditInformation::focusInEvent(QFocusEvent *e) {\n\tif (const auto control = lookupField(_focusField)) {\n\t\tcontrol->setFocus();\n\t}\n}\n\nvoid EditInformation::updateControlsGeometry() {\n\tconst auto &padding = st::paymentsPanelPadding;\n\tconst auto buttonsHeight = padding.top()\n\t\t+ _cancel->height()\n\t\t+ padding.bottom();\n\tconst auto buttonsTop = height() - buttonsHeight;\n\t_scroll->setGeometry(0, 0, width(), buttonsTop);\n\t_topShadow->resizeToWidth(width());\n\t_topShadow->moveToLeft(0, 0);\n\t_bottomShadow->resizeToWidth(width());\n\t_bottomShadow->moveToLeft(0, buttonsTop - st::lineWidth);\n\tauto right = padding.right();\n\t_submit->moveToRight(right, buttonsTop + padding.top());\n\tright += _submit->width() + padding.left();\n\t_cancel->moveToRight(right, buttonsTop + padding.top());\n\n\t_scroll->updateBars();\n}\n\nauto EditInformation::lookupField(InformationField field) const -> Field* {\n\tswitch (field) {\n\tcase InformationField::ShippingStreet: return _street1.get();\n\tcase InformationField::ShippingCity: return _city.get();\n\tcase InformationField::ShippingState: return _state.get();\n\tcase InformationField::ShippingCountry: return _country.get();\n\tcase InformationField::ShippingPostcode: return _postcode.get();\n\tcase InformationField::Name: return _name.get();\n\tcase InformationField::Email: return _email.get();\n\tcase InformationField::Phone: return _phone.get();\n\t}\n\tUnexpected(\"Unknown field in EditInformation::lookupField.\");\n}\n\nRequestedInformation EditInformation::collect() const {\n\treturn {\n\t\t.defaultPhone = _information.defaultPhone,\n\t\t.defaultCountry = _information.defaultCountry,\n\t\t.save = _save->checked(),\n\t\t.name = _name ? _name->value() : QString(),\n\t\t.phone = _phone ? _phone->value() : QString(),\n\t\t.email = _email ? _email->value() : QString(),\n\t\t.shippingAddress = {\n\t\t\t.address1 = _street1 ? _street1->value() : QString(),\n\t\t\t.address2 = _street2 ? _street2->value() : QString(),\n\t\t\t.city = _city ? _city->value() : QString(),\n\t\t\t.state = _state ? _state->value() : QString(),\n\t\t\t.countryIso2 = _country ? _country->value() : QString(),\n\t\t\t.postcode = _postcode ? _postcode->value() : QString(),\n\t\t},\n\t};\n}\n\n} // namespace Payments::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/ui/payments_edit_information.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"payments/ui/payments_panel_data.h\"\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\nclass ScrollArea;\nclass FadeShadow;\nclass RoundButton;\nclass InputField;\nclass MaskedInputField;\nclass Checkbox;\n} // namespace Ui\n\nnamespace Payments::Ui {\n\nusing namespace ::Ui;\n\nclass PanelDelegate;\nclass Field;\n\nclass EditInformation final : public RpWidget {\npublic:\n\tEditInformation(\n\t\tQWidget *parent,\n\t\tconst Invoice &invoice,\n\t\tconst RequestedInformation &current,\n\t\tInformationField field,\n\t\tnot_null<PanelDelegate*> delegate);\n\t~EditInformation();\n\n\tvoid setFocus(InformationField field);\n\tvoid setFocusFast(InformationField field);\n\tvoid showError(InformationField field);\n\nprivate:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid focusInEvent(QFocusEvent *e) override;\n\n\tvoid setupControls();\n\t[[nodiscard]] not_null<Ui::RpWidget*> setupContent();\n\tvoid updateControlsGeometry();\n\t[[nodiscard]] Field *lookupField(InformationField field) const;\n\n\t[[nodiscard]] RequestedInformation collect() const;\n\n\tconst not_null<PanelDelegate*> _delegate;\n\tInvoice _invoice;\n\tRequestedInformation _information;\n\n\tobject_ptr<ScrollArea> _scroll;\n\tobject_ptr<FadeShadow> _topShadow;\n\tobject_ptr<FadeShadow> _bottomShadow;\n\tobject_ptr<RoundButton> _submit;\n\tobject_ptr<RoundButton> _cancel;\n\n\tstd::unique_ptr<Field> _street1;\n\tstd::unique_ptr<Field> _street2;\n\tstd::unique_ptr<Field> _city;\n\tstd::unique_ptr<Field> _state;\n\tstd::unique_ptr<Field> _country;\n\tstd::unique_ptr<Field> _postcode;\n\tstd::unique_ptr<Field> _name;\n\tstd::unique_ptr<Field> _email;\n\tstd::unique_ptr<Field> _phone;\n\tCheckbox *_save = nullptr;\n\n\tInformationField _focusField = InformationField::ShippingStreet;\n\n};\n\n} // namespace Payments::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/ui/payments_field.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"payments/ui/payments_field.h\"\n\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/boxes/country_select_box.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/fields/special_fields.h\"\n#include \"countries/countries_instance.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/event_filter.h\"\n#include \"base/qt/qt_common_adapters.h\"\n#include \"styles/style_payments.h\"\n\n#include <QtCore/QRegularExpression>\n#include <QtWidgets/QTextEdit>\n\nnamespace Payments::Ui {\nnamespace {\n\nstruct SimpleFieldState {\n\tQString value;\n\tint position = 0;\n};\n\n[[nodiscard]] char FieldThousandsSeparator(const CurrencyRule &rule) {\n\treturn (rule.thousands == '.' || rule.thousands == ',')\n\t\t? ' '\n\t\t: rule.thousands;\n}\n\n[[nodiscard]] QString RemoveNonNumbers(QString value) {\n\tstatic const auto RegExp = QRegularExpression(\"[^0-9]\");\n\treturn value.replace(RegExp, QString());\n}\n\n[[nodiscard]] SimpleFieldState CleanMoneyState(\n\t\tconst CurrencyRule &rule,\n\t\tSimpleFieldState state) {\n\tconst auto withDecimal = state.value.replace(\n\t\tQChar('.'),\n\t\trule.decimal\n\t).replace(\n\t\tQChar(','),\n\t\trule.decimal\n\t);\n\tconst auto digitsLimit = 16 - rule.exponent;\n\tconst auto beforePosition = state.value.mid(0, state.position);\n\tauto decimalPosition = int(withDecimal.lastIndexOf(rule.decimal));\n\tif (decimalPosition < 0) {\n\t\tstate = {\n\t\t\t.value = RemoveNonNumbers(state.value),\n\t\t\t.position = int(RemoveNonNumbers(beforePosition).size()),\n\t\t};\n\t} else {\n\t\tconst auto onlyNumbersBeforeDecimal = RemoveNonNumbers(\n\t\t\tstate.value.mid(0, decimalPosition));\n\t\tstate = {\n\t\t\t.value = (onlyNumbersBeforeDecimal\n\t\t\t\t+ QChar(rule.decimal)\n\t\t\t\t+ RemoveNonNumbers(state.value.mid(decimalPosition + 1))),\n\t\t\t.position = int(RemoveNonNumbers(beforePosition).size()\n\t\t\t\t+ (state.position > decimalPosition ? 1 : 0)),\n\t\t};\n\t\tdecimalPosition = onlyNumbersBeforeDecimal.size();\n\t\tconst auto maxLength = decimalPosition + 1 + rule.exponent;\n\t\tif (state.value.size() > maxLength) {\n\t\t\tstate = {\n\t\t\t\t.value = state.value.mid(0, maxLength),\n\t\t\t\t.position = std::min(state.position, maxLength),\n\t\t\t};\n\t\t}\n\t}\n\tif (!state.value.isEmpty() && state.value[0] == QChar(rule.decimal)) {\n\t\tstate = {\n\t\t\t.value = QChar('0') + state.value,\n\t\t\t.position = state.position + 1,\n\t\t};\n\t\tif (decimalPosition >= 0) {\n\t\t\t++decimalPosition;\n\t\t}\n\t}\n\tauto skip = 0;\n\twhile (state.value.size() > skip + 1\n\t\t&& state.value[skip] == QChar('0')\n\t\t&& state.value[skip + 1] != QChar(rule.decimal)) {\n\t\t++skip;\n\t}\n\tstate = {\n\t\t.value = state.value.mid(skip),\n\t\t.position = std::max(state.position - skip, 0),\n\t};\n\tif (decimalPosition >= 0) {\n\t\tAssert(decimalPosition >= skip);\n\t\tdecimalPosition -= skip;\n\t\tif (decimalPosition > digitsLimit) {\n\t\t\tstate = {\n\t\t\t\t.value = (state.value.mid(0, digitsLimit)\n\t\t\t\t\t+ state.value.mid(decimalPosition)),\n\t\t\t\t.position = (state.position > digitsLimit\n\t\t\t\t\t? std::max(\n\t\t\t\t\t\tstate.position - (decimalPosition - digitsLimit),\n\t\t\t\t\t\tdigitsLimit)\n\t\t\t\t\t: state.position),\n\t\t\t};\n\t\t}\n\t} else if (state.value.size() > digitsLimit) {\n\t\tstate = {\n\t\t\t.value = state.value.mid(0, digitsLimit),\n\t\t\t.position = std::min(state.position, digitsLimit),\n\t\t};\n\t}\n\treturn state;\n}\n\n[[nodiscard]] SimpleFieldState PostprocessMoneyResult(\n\t\tconst CurrencyRule &rule,\n\t\tSimpleFieldState result) {\n\tconst auto position = result.value.indexOf(rule.decimal);\n\tconst auto from = (position >= 0) ? position : result.value.size();\n\tfor (auto insertAt = from - 3; insertAt > 0; insertAt -= 3) {\n\t\tresult.value.insert(insertAt, QChar(FieldThousandsSeparator(rule)));\n\t\tif (result.position >= insertAt) {\n\t\t\t++result.position;\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] bool IsBackspace(const FieldValidateRequest &request) {\n\treturn (request.wasAnchor == request.wasPosition)\n\t\t&& (request.wasPosition == request.nowPosition + 1)\n\t\t&& (base::StringViewMid(request.wasValue, 0, request.wasPosition - 1)\n\t\t\t== base::StringViewMid(request.nowValue, 0, request.nowPosition))\n\t\t&& (base::StringViewMid(request.wasValue, request.wasPosition)\n\t\t\t== base::StringViewMid(request.nowValue, request.nowPosition));\n}\n\n[[nodiscard]] bool IsDelete(const FieldValidateRequest &request) {\n\treturn (request.wasAnchor == request.wasPosition)\n\t\t&& (request.wasPosition == request.nowPosition)\n\t\t&& (base::StringViewMid(request.wasValue, 0, request.wasPosition)\n\t\t\t== base::StringViewMid(request.nowValue, 0, request.nowPosition))\n\t\t&& (base::StringViewMid(request.wasValue, request.wasPosition + 1)\n\t\t\t== base::StringViewMid(request.nowValue, request.nowPosition));\n}\n\n[[nodiscard]] auto MoneyValidator(const CurrencyRule &rule) {\n\treturn [=](FieldValidateRequest request) {\n\t\tconst auto realNowState = [&] {\n\t\t\tconst auto backspaced = IsBackspace(request);\n\t\t\tconst auto deleted = IsDelete(request);\n\t\t\tif (!backspaced && !deleted) {\n\t\t\t\treturn CleanMoneyState(rule, {\n\t\t\t\t\t.value = request.nowValue,\n\t\t\t\t\t.position = request.nowPosition,\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst auto realWasState = CleanMoneyState(rule, {\n\t\t\t\t.value = request.wasValue,\n\t\t\t\t.position = request.wasPosition,\n\t\t\t});\n\t\t\tconst auto changedValue = deleted\n\t\t\t\t? (realWasState.value.mid(0, realWasState.position)\n\t\t\t\t\t+ realWasState.value.mid(realWasState.position + 1))\n\t\t\t\t: (realWasState.position > 1)\n\t\t\t\t? (realWasState.value.mid(0, realWasState.position - 1)\n\t\t\t\t\t+ realWasState.value.mid(realWasState.position))\n\t\t\t\t: realWasState.value.mid(realWasState.position);\n\t\t\treturn SimpleFieldState{\n\t\t\t\t.value = changedValue,\n\t\t\t\t.position = (deleted\n\t\t\t\t\t? realWasState.position\n\t\t\t\t\t: std::max(realWasState.position - 1, 0))\n\t\t\t};\n\t\t}();\n\t\tconst auto postprocessed = PostprocessMoneyResult(\n\t\t\trule,\n\t\t\trealNowState);\n\t\treturn FieldValidateResult{\n\t\t\t.value = postprocessed.value,\n\t\t\t.position = postprocessed.position,\n\t\t};\n\t};\n}\n\n[[nodiscard]] QString Parse(const FieldConfig &config) {\n\tif (config.type == FieldType::Country) {\n\t\treturn Countries::Instance().countryNameByISO2(config.value);\n\t} else if (config.type == FieldType::Money) {\n\t\tconst auto amount = config.value.toLongLong();\n\t\tif (!amount) {\n\t\t\treturn QString();\n\t\t}\n\t\tconst auto rule = LookupCurrencyRule(config.currency);\n\t\tconst auto value = std::abs(amount) / std::pow(10., rule.exponent);\n\t\tconst auto precision = (!rule.stripDotZero\n\t\t\t|| std::floor(value) != value)\n\t\t\t? rule.exponent\n\t\t\t: 0;\n\t\treturn FormatWithSeparators(\n\t\t\tvalue,\n\t\t\tprecision,\n\t\t\trule.decimal,\n\t\t\tFieldThousandsSeparator(rule));\n\t}\n\treturn config.value;\n}\n\n[[nodiscard]] QString Format(\n\t\tconst FieldConfig &config,\n\t\tconst QString &parsed,\n\t\tconst QString &countryIso2) {\n\tif (config.type == FieldType::Country) {\n\t\treturn countryIso2;\n\t} else if (config.type == FieldType::Money) {\n\t\tstatic const auto RegExp = QRegularExpression(\"[^0-9\\\\.]\");\n\t\tconst auto rule = LookupCurrencyRule(config.currency);\n\t\tconst auto real = QString(parsed).replace(\n\t\t\tQChar(rule.decimal),\n\t\t\tQChar('.')\n\t\t).replace(\n\t\t\tQChar(','),\n\t\t\tQChar('.')\n\t\t).replace(\n\t\t\tRegExp,\n\t\t\tQString()\n\t\t).toDouble();\n\t\treturn QString::number(\n\t\t\tint64(base::SafeRound(real * std::pow(10., rule.exponent))));\n\t} else if (config.type == FieldType::CardNumber\n\t\t|| config.type == FieldType::CardCVC) {\n\t\tstatic const auto RegExp = QRegularExpression(\"[^0-9]\");\n\t\treturn QString(parsed).replace(RegExp, QString());\n\t}\n\treturn parsed;\n}\n\n[[nodiscard]] bool UseMaskedField(FieldType type) {\n\tswitch (type) {\n\tcase FieldType::Text:\n\tcase FieldType::Email:\n\t\treturn false;\n\tcase FieldType::CardNumber:\n\tcase FieldType::CardExpireDate:\n\tcase FieldType::CardCVC:\n\tcase FieldType::Country:\n\tcase FieldType::Phone:\n\tcase FieldType::Money:\n\t\treturn true;\n\t}\n\tUnexpected(\"FieldType in Payments::Ui::UseMaskedField.\");\n}\n\n[[nodiscard]] base::unique_qptr<RpWidget> CreateWrap(\n\t\tQWidget *parent,\n\t\tFieldConfig &config) {\n\tswitch (config.type) {\n\tcase FieldType::Text:\n\tcase FieldType::Email:\n\t\treturn base::make_unique_q<InputField>(\n\t\t\tparent,\n\t\t\tst::paymentsField,\n\t\t\tstd::move(config.placeholder),\n\t\t\tParse(config));\n\tcase FieldType::CardNumber:\n\tcase FieldType::CardExpireDate:\n\tcase FieldType::CardCVC:\n\tcase FieldType::Country:\n\tcase FieldType::Phone:\n\tcase FieldType::Money:\n\t\treturn base::make_unique_q<RpWidget>(parent);\n\t}\n\tUnexpected(\"FieldType in Payments::Ui::CreateWrap.\");\n}\n\n[[nodiscard]] InputField *LookupInputField(\n\t\tnot_null<RpWidget*> wrap,\n\t\tFieldConfig &config) {\n\treturn UseMaskedField(config.type)\n\t\t? nullptr\n\t\t: static_cast<InputField*>(wrap.get());\n}\n\n[[nodiscard]] MaskedInputField *CreateMoneyField(\n\t\tnot_null<RpWidget*> wrap,\n\t\tFieldConfig &config,\n\t\trpl::producer<> textPossiblyChanged) {\n\tstruct State {\n\t\tCurrencyRule rule;\n\t\tstyle::InputField st;\n\t\tQString currencyText;\n\t\tint currencySkip = 0;\n\t\tFlatLabel *left = nullptr;\n\t\tFlatLabel *right = nullptr;\n\t};\n\tconst auto state = wrap->lifetime().make_state<State>(State{\n\t\t.rule = LookupCurrencyRule(config.currency),\n\t\t.st = st::paymentsMoneyField,\n\t});\n\tconst auto &rule = state->rule;\n\tstate->currencySkip = rule.space ? state->st.style.font->spacew : 0;\n\tstate->currencyText = ((!rule.left && rule.space)\n\t\t? QString(QChar(' '))\n\t\t: QString()) + (*rule.international\n\t\t\t? QString(rule.international)\n\t\t\t: config.currency) + ((rule.left && rule.space)\n\t\t\t\t? QString(QChar(' '))\n\t\t\t\t: QString());\n\tif (rule.left) {\n\t\tstate->left = CreateChild<FlatLabel>(\n\t\t\twrap.get(),\n\t\t\tstate->currencyText,\n\t\t\tst::paymentsFieldAdditional);\n\t}\n\tstate->right = CreateChild<FlatLabel>(\n\t\twrap.get(),\n\t\tQString(),\n\t\tst::paymentsFieldAdditional);\n\tconst auto leftSkip = state->left\n\t\t? (state->left->textMaxWidth() + state->currencySkip)\n\t\t: 0;\n\tconst auto rightSkip = st::paymentsFieldAdditional.style.font->width(\n\t\tQString(QChar(rule.decimal))\n\t\t+ QString(QChar('0')).repeated(rule.exponent)\n\t\t+ (rule.left ? QString() : state->currencyText));\n\tstate->st.textMargins += QMargins(leftSkip, 0, rightSkip, 0);\n\tstate->st.placeholderMargins -= QMargins(leftSkip, 0, rightSkip, 0);\n\tconst auto result = CreateChild<MaskedInputField>(\n\t\twrap.get(),\n\t\tstate->st,\n\t\tstd::move(config.placeholder),\n\t\tParse(config));\n\tresult->setPlaceholderHidden(true);\n\tif (state->left) {\n\t\tstate->left->move(0, state->st.textMargins.top());\n\t}\n\tconst auto updateRight = [=] {\n\t\tconst auto text = result->getLastText();\n\t\tconst auto width = state->st.style.font->width(text);\n\t\tconst auto &rule = state->rule;\n\t\tconst auto symbol = QChar(rule.decimal);\n\t\tconst auto decimal = text.indexOf(symbol);\n\t\tconst auto zeros = (decimal >= 0)\n\t\t\t? std::max(rule.exponent - int(text.size() - decimal - 1), 0)\n\t\t\t: rule.stripDotZero\n\t\t\t? 0\n\t\t\t: rule.exponent;\n\t\tconst auto valueDecimalSeparator = (decimal >= 0 || !zeros)\n\t\t\t? QString()\n\t\t\t: QString(symbol);\n\t\tconst auto zeroString = QString(QChar('0'));\n\t\tconst auto valueRightPart = (text.isEmpty() ? zeroString : QString())\n\t\t\t+ valueDecimalSeparator\n\t\t\t+ zeroString.repeated(zeros);\n\t\tconst auto right = valueRightPart\n\t\t\t+ (rule.left ? QString() : state->currencyText);\n\t\tstate->right->setText(right);\n\t\tstate->right->setTextColorOverride(valueRightPart.isEmpty()\n\t\t\t? std::nullopt\n\t\t\t: std::make_optional(st::windowSubTextFg->c));\n\t\tstate->right->move(\n\t\t\t(state->st.textMargins.left()\n\t\t\t\t+ width\n\t\t\t\t+ ((rule.left || !valueRightPart.isEmpty())\n\t\t\t\t\t? 0\n\t\t\t\t\t: state->currencySkip)),\n\t\t\tstate->st.textMargins.top());\n\t};\n\tstd::move(\n\t\ttextPossiblyChanged\n\t) | rpl::on_next(updateRight, result->lifetime());\n\tif (state->left) {\n\t\tstate->left->raise();\n\t}\n\tstate->right->raise();\n\treturn result;\n}\n\n[[nodiscard]] MaskedInputField *LookupMaskedField(\n\t\tnot_null<RpWidget*> wrap,\n\t\tFieldConfig &config,\n\t\trpl::producer<> textPossiblyChanged) {\n\tif (!UseMaskedField(config.type)) {\n\t\treturn nullptr;\n\t}\n\tswitch (config.type) {\n\tcase FieldType::Text:\n\tcase FieldType::Email:\n\t\treturn nullptr;\n\tcase FieldType::CardNumber:\n\tcase FieldType::CardExpireDate:\n\tcase FieldType::CardCVC:\n\tcase FieldType::Country:\n\t\treturn CreateChild<MaskedInputField>(\n\t\t\twrap.get(),\n\t\t\tst::paymentsField,\n\t\t\tstd::move(config.placeholder),\n\t\t\tParse(config));\n\tcase FieldType::Phone:\n\t\treturn CreateChild<PhoneInput>(\n\t\t\twrap.get(),\n\t\t\tst::paymentsField,\n\t\t\tstd::move(config.placeholder),\n\t\t\tCountries::ExtractPhoneCode(config.defaultPhone),\n\t\t\tParse(config),\n\t\t\t[](const QString &s) { return Countries::Groups(s); });\n\tcase FieldType::Money:\n\t\treturn CreateMoneyField(\n\t\t\twrap,\n\t\t\tconfig,\n\t\t\tstd::move(textPossiblyChanged));\n\t}\n\tUnexpected(\"FieldType in Payments::Ui::LookupMaskedField.\");\n}\n\n} // namespace\n\nField::Field(QWidget *parent, FieldConfig &&config)\n: _config(config)\n, _wrap(CreateWrap(parent, config))\n, _input(LookupInputField(_wrap.get(), config))\n, _masked(LookupMaskedField(\n\t_wrap.get(),\n\tconfig,\n\t_textPossiblyChanged.events_starting_with({})))\n, _countryIso2(config.value) {\n\tif (_masked) {\n\t\tsetupMaskedGeometry();\n\t}\n\tif (_config.type == FieldType::Country) {\n\t\tsetupCountry();\n\t}\n\tif (const auto &validator = config.validator) {\n\t\tsetupValidator(validator);\n\t} else if (config.type == FieldType::Money) {\n\t\tsetupValidator(MoneyValidator(LookupCurrencyRule(config.currency)));\n\t}\n\tsetupFrontBackspace();\n\tsetupSubmit();\n}\n\nRpWidget *Field::widget() const {\n\treturn _wrap.get();\n}\n\nobject_ptr<RpWidget> Field::ownedWidget() const {\n\treturn object_ptr<RpWidget>::fromRaw(_wrap.get());\n}\n\nQString Field::value() const {\n\treturn Format(\n\t\t_config,\n\t\t_input ? _input->getLastText() : _masked->getLastText(),\n\t\t_countryIso2);\n}\n\nrpl::producer<> Field::frontBackspace() const {\n\treturn _frontBackspace.events();\n}\n\nrpl::producer<> Field::finished() const {\n\treturn _finished.events();\n}\n\nrpl::producer<> Field::submitted() const {\n\treturn _submitted.events();\n}\n\nvoid Field::setupMaskedGeometry() {\n\tExpects(_masked != nullptr);\n\n\t_wrap->resize(_masked->size());\n\t_wrap->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_masked->resize(width, _masked->height());\n\t}, _masked->lifetime());\n\t_masked->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\t_wrap->resize(_wrap->width(), height);\n\t}, _masked->lifetime());\n}\n\nvoid Field::setupCountry() {\n\tExpects(_config.type == FieldType::Country);\n\tExpects(_masked != nullptr);\n\n\tQObject::connect(_masked, &MaskedInputField::focused, [=] {\n\t\tsetFocus();\n\n\t\tconst auto name = Countries::Instance().countryNameByISO2(\n\t\t\t_countryIso2);\n\t\tconst auto country = !name.isEmpty()\n\t\t\t? _countryIso2\n\t\t\t: !_config.defaultCountry.isEmpty()\n\t\t\t? _config.defaultCountry\n\t\t\t: Platform::SystemCountry();\n\t\tauto box = Box<CountrySelectBox>(\n\t\t\tcountry,\n\t\t\tCountrySelectBox::Type::Countries);\n\t\tconst auto raw = box.data();\n\t\traw->countryChosen(\n\t\t) | rpl::on_next([=](QString iso2) {\n\t\t\t_countryIso2 = iso2;\n\t\t\t_masked->setText(Countries::Instance().countryNameByISO2(iso2));\n\t\t\t_masked->hideError();\n\t\t\traw->closeBox();\n\t\t\tif (!iso2.isEmpty()) {\n\t\t\t\tif (_nextField) {\n\t\t\t\t\t_nextField->activate();\n\t\t\t\t} else {\n\t\t\t\t\t_submitted.fire({});\n\t\t\t\t}\n\t\t\t}\n\t\t}, _masked->lifetime());\n\t\traw->boxClosing() | rpl::on_next([=] {\n\t\t\tsetFocus();\n\t\t}, _masked->lifetime());\n\t\t_config.showBox(std::move(box));\n\t});\n}\n\nvoid Field::setupValidator(Fn<ValidateResult(ValidateRequest)> validator) {\n\tExpects(validator != nullptr);\n\n\tconst auto state = [=]() -> State {\n\t\tif (_masked) {\n\t\t\tconst auto position = _masked->cursorPosition();\n\t\t\tconst auto selectionStart = _masked->selectionStart();\n\t\t\tconst auto selectionEnd = _masked->selectionEnd();\n\t\t\treturn {\n\t\t\t\t.value = _masked->getLastText(),\n\t\t\t\t.position = position,\n\t\t\t\t.anchor = (selectionStart == selectionEnd\n\t\t\t\t\t? position\n\t\t\t\t\t: (selectionStart == position)\n\t\t\t\t\t? selectionEnd\n\t\t\t\t\t: selectionStart),\n\t\t\t};\n\t\t}\n\t\tconst auto cursor = _input->textCursor();\n\t\treturn {\n\t\t\t.value = _input->getLastText(),\n\t\t\t.position = cursor.position(),\n\t\t\t.anchor = cursor.anchor(),\n\t\t};\n\t};\n\tconst auto save = [=] {\n\t\t_was = state();\n\t};\n\tconst auto setText = [=](const QString &text) {\n\t\tif (_masked) {\n\t\t\t_masked->setText(text);\n\t\t} else {\n\t\t\t_input->setText(text);\n\t\t}\n\t};\n\tconst auto setPosition = [=](int position) {\n\t\tif (_masked) {\n\t\t\t_masked->setCursorPosition(position);\n\t\t} else {\n\t\t\tauto cursor = _input->textCursor();\n\t\t\tcursor.setPosition(position);\n\t\t\t_input->setTextCursor(cursor);\n\t\t}\n\t};\n\tconst auto validate = [=] {\n\t\tif (_validating) {\n\t\t\treturn;\n\t\t}\n\t\t_validating = true;\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\t_validating = false;\n\t\t\tsave();\n\t\t\t_textPossiblyChanged.fire({});\n\t\t});\n\n\t\tconst auto now = state();\n\t\tconst auto result = validator(ValidateRequest{\n\t\t\t.wasValue = _was.value,\n\t\t\t.wasPosition = _was.position,\n\t\t\t.wasAnchor = _was.anchor,\n\t\t\t.nowValue = now.value,\n\t\t\t.nowPosition = now.position,\n\t\t});\n\t\t_valid = result.finished || !result.invalid;\n\n\t\tconst auto changed = (result.value != now.value);\n\t\tif (changed) {\n\t\t\tsetText(result.value);\n\t\t}\n\t\tif (changed || result.position != now.position) {\n\t\t\tsetPosition(result.position);\n\t\t}\n\t\tif (result.finished) {\n\t\t\t_finished.fire({});\n\t\t} else if (result.invalid) {\n\t\t\tUi::PostponeCall(\n\t\t\t\t_masked ? (QWidget*)_masked : _input,\n\t\t\t\t[=] { showErrorNoFocus(); });\n\t\t}\n\t};\n\tif (_masked) {\n\t\tQObject::connect(_masked, &QLineEdit::cursorPositionChanged, save);\n\t\tQObject::connect(_masked, &MaskedInputField::changed, validate);\n\t} else {\n\t\tconst auto raw = _input->rawTextEdit();\n\t\tQObject::connect(raw, &QTextEdit::cursorPositionChanged, save);\n\t\t_input->changes(\n\t\t) | rpl::on_next(validate, _input->lifetime());\n\t}\n}\n\nvoid Field::setupFrontBackspace() {\n\tconst auto filter = [=](not_null<QEvent*> e) {\n\t\tconst auto frontBackspace = (e->type() == QEvent::KeyPress)\n\t\t\t&& (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Backspace)\n\t\t\t&& (_masked\n\t\t\t\t? (_masked->cursorPosition() == 0\n\t\t\t\t\t&& _masked->selectionLength() == 0)\n\t\t\t\t: (_input->textCursor().position() == 0\n\t\t\t\t\t&& _input->textCursor().anchor() == 0));\n\t\tif (frontBackspace) {\n\t\t\t_frontBackspace.fire({});\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t};\n\tif (_masked) {\n\t\tbase::install_event_filter(_masked, filter);\n\t} else {\n\t\tbase::install_event_filter(_input->rawTextEdit(), filter);\n\t}\n}\n\nvoid Field::setupSubmit() {\n\tconst auto submitted = [=] {\n\t\tif (!_valid) {\n\t\t\tshowError();\n\t\t} else if (_nextField) {\n\t\t\t_nextField->activate();\n\t\t} else {\n\t\t\t_submitted.fire({});\n\t\t}\n\t};\n\tif (_masked) {\n\t\tQObject::connect(_masked, &MaskedInputField::submitted, submitted);\n\t} else {\n\t\t_input->submits(\n\t\t) | rpl::on_next(submitted, _input->lifetime());\n\t}\n}\n\nvoid Field::setNextField(not_null<Field*> field) {\n\t_nextField = field;\n\n\tfinished() | rpl::on_next([=] {\n\t\tfield->setFocus();\n\t}, _masked ? _masked->lifetime() : _input->lifetime());\n}\n\nvoid Field::setPreviousField(not_null<Field*> field) {\n\tfrontBackspace(\n\t) | rpl::on_next([=] {\n\t\tfield->setFocus();\n\t}, _masked ? _masked->lifetime() : _input->lifetime());\n}\n\nvoid Field::activate() {\n\tif (_input) {\n\t\t_input->setFocus();\n\t} else {\n\t\t_masked->setFocus();\n\t}\n}\n\nvoid Field::setFocus() {\n\tif (_config.type == FieldType::Country) {\n\t\t_wrap->setFocus();\n\t} else {\n\t\tactivate();\n\t}\n}\n\nvoid Field::setFocusFast() {\n\tif (_config.type == FieldType::Country) {\n\t\tsetFocus();\n\t} else if (_input) {\n\t\t_input->setFocusFast();\n\t} else {\n\t\t_masked->setFocusFast();\n\t}\n}\n\nvoid Field::showError() {\n\tif (_config.type == FieldType::Country) {\n\t\tsetFocus();\n\t\t_masked->showErrorNoFocus();\n\t} else if (_input) {\n\t\t_input->showError();\n\t} else {\n\t\t_masked->showError();\n\t}\n}\n\nvoid Field::showErrorNoFocus() {\n\tif (_input) {\n\t\t_input->showErrorNoFocus();\n\t} else {\n\t\t_masked->showErrorNoFocus();\n\t}\n}\n\n} // namespace Payments::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/ui/payments_field.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"base/unique_qptr.h\"\n\nnamespace Ui {\nclass RpWidget;\nclass InputField;\nclass MaskedInputField;\nclass BoxContent;\n} // namespace Ui\n\nnamespace Payments::Ui {\n\nusing namespace ::Ui;\n\nenum class FieldType {\n\tText,\n\tCardNumber,\n\tCardExpireDate,\n\tCardCVC,\n\tCountry,\n\tPhone,\n\tEmail,\n\tMoney,\n};\n\nstruct FieldValidateRequest {\n\tQString wasValue;\n\tint wasPosition = 0;\n\tint wasAnchor = 0;\n\tQString nowValue;\n\tint nowPosition = 0;\n};\n\nstruct FieldValidateResult {\n\tQString value;\n\tint position = 0;\n\tbool invalid = false;\n\tbool finished = false;\n};\n\n[[nodiscard]] inline auto RangeLengthValidator(int minLength, int maxLength) {\n\treturn [=](FieldValidateRequest request) {\n\t\treturn FieldValidateResult{\n\t\t\t.value = request.nowValue,\n\t\t\t.position = request.nowPosition,\n\t\t\t.invalid = (request.nowValue.size() < minLength\n\t\t\t\t|| request.nowValue.size() > maxLength),\n\t\t};\n\t};\n}\n\n[[nodiscard]] inline auto MaxLengthValidator(int maxLength) {\n\treturn RangeLengthValidator(0, maxLength);\n}\n\n[[nodiscard]] inline auto RequiredValidator() {\n\treturn RangeLengthValidator(1, std::numeric_limits<int>::max());\n}\n\n[[nodiscard]] inline auto RequiredFinishedValidator() {\n\treturn [=](FieldValidateRequest request) {\n\t\treturn FieldValidateResult{\n\t\t\t.value = request.nowValue,\n\t\t\t.position = request.nowPosition,\n\t\t\t.invalid = request.nowValue.isEmpty(),\n\t\t\t.finished = !request.nowValue.isEmpty(),\n\t\t};\n\t};\n}\n\nstruct FieldConfig {\n\tFieldType type = FieldType::Text;\n\trpl::producer<QString> placeholder;\n\tQString value;\n\tFn<FieldValidateResult(FieldValidateRequest)> validator;\n\tFn<void(object_ptr<BoxContent>)> showBox;\n\tQString currency;\n\tQString defaultPhone;\n\tQString defaultCountry;\n};\n\nclass Field final {\npublic:\n\tField(QWidget *parent, FieldConfig &&config);\n\n\t[[nodiscard]] RpWidget *widget() const;\n\t[[nodiscard]] object_ptr<RpWidget> ownedWidget() const;\n\n\t[[nodiscard]] QString value() const;\n\t[[nodiscard]] rpl::producer<> frontBackspace() const;\n\t[[nodiscard]] rpl::producer<> finished() const;\n\t[[nodiscard]] rpl::producer<> submitted() const;\n\n\tvoid activate();\n\tvoid setFocus();\n\tvoid setFocusFast();\n\tvoid showError();\n\tvoid showErrorNoFocus();\n\n\tvoid setNextField(not_null<Field*> field);\n\tvoid setPreviousField(not_null<Field*> field);\n\nprivate:\n\tstruct State {\n\t\tQString value;\n\t\tint position = 0;\n\t\tint anchor = 0;\n\t};\n\tusing ValidateRequest = FieldValidateRequest;\n\tusing ValidateResult = FieldValidateResult;\n\n\tvoid setupMaskedGeometry();\n\tvoid setupCountry();\n\tvoid setupValidator(Fn<ValidateResult(ValidateRequest)> validator);\n\tvoid setupFrontBackspace();\n\tvoid setupSubmit();\n\n\tconst FieldConfig _config;\n\tconst base::unique_qptr<RpWidget> _wrap;\n\trpl::event_stream<> _frontBackspace;\n\trpl::event_stream<> _finished;\n\trpl::event_stream<> _submitted;\n\trpl::event_stream<> _textPossiblyChanged; // Must be above _masked.\n\tInputField *_input = nullptr;\n\tMaskedInputField *_masked = nullptr;\n\tField *_nextField = nullptr;\n\tQString _countryIso2;\n\tState _was;\n\tbool _validating = false;\n\tbool _valid = true;\n\n};\n\n} // namespace Payments::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/ui/payments_form_summary.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"payments/ui/payments_form_summary.h\"\n\n#include \"payments/ui/payments_panel_delegate.h\"\n#include \"settings/settings_common.h\" // AddButtonWithLabel.\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"countries/countries_instance.h\"\n#include \"lang/lang_keys.h\"\n#include \"base/unixtime.h\"\n#include \"styles/style_payments.h\"\n#include \"styles/style_passport.h\"\n\nnamespace Payments::Ui {\nnamespace {\n\nconstexpr auto kLightOpacity = 0.1;\nconstexpr auto kLightRippleOpacity = 0.11;\nconstexpr auto kChosenOpacity = 0.8;\nconstexpr auto kChosenRippleOpacity = 0.5;\n\n[[nodiscard]] Fn<QColor()> TransparentColor(\n\t\tconst style::color &c,\n\t\tfloat64 opacity) {\n\treturn [&c, opacity] {\n\t\treturn QColor(\n\t\t\tc->c.red(),\n\t\t\tc->c.green(),\n\t\t\tc->c.blue(),\n\t\t\tc->c.alpha() * opacity);\n\t};\n}\n\n[[nodiscard]] style::RoundButton TipButtonStyle(\n\t\tconst style::RoundButton &original,\n\t\tconst style::color &light,\n\t\tconst style::color &ripple) {\n\tauto result = original;\n\tresult.textBg = light;\n\tresult.ripple.color = ripple;\n\treturn result;\n}\n\n} // namespace\n\nusing namespace ::Ui;\n\nclass PanelDelegate;\n\nFormSummary::FormSummary(\n\tQWidget *parent,\n\tconst Invoice &invoice,\n\tconst RequestedInformation &current,\n\tconst PaymentMethodDetails &method,\n\tconst ShippingOptions &options,\n\tnot_null<PanelDelegate*> delegate,\n\tint scrollTop)\n: _delegate(delegate)\n, _invoice(invoice)\n, _method(method)\n, _options(options)\n, _information(current)\n, _scroll(this, st::passportPanelScroll)\n, _layout(_scroll->setOwnedWidget(object_ptr<VerticalLayout>(this)))\n, _topShadow(this)\n, _bottomShadow(this)\n, _submit(_invoice.receipt.paid\n\t? object_ptr<RoundButton>(nullptr)\n\t: object_ptr<RoundButton>(\n\t\tthis,\n\t\ttr::lng_payments_pay_amount(\n\t\t\tlt_amount,\n\t\t\trpl::single(formatAmount(computeTotalAmount()))),\n\t\tst::paymentsPanelSubmit))\n, _cancel(\n\tthis,\n\t(_invoice.receipt.paid\n\t\t? tr::lng_about_done()\n\t\t: tr::lng_cancel()),\n\tst::paymentsPanelButton)\n, _tipLightBg(TransparentColor(st::paymentsTipActive, kLightOpacity))\n, _tipLightRipple(\n\tTransparentColor(st::paymentsTipActive, kLightRippleOpacity))\n, _tipChosenBg(TransparentColor(st::paymentsTipActive, kChosenOpacity))\n, _tipChosenRipple(\n\tTransparentColor(st::paymentsTipActive, kChosenRippleOpacity))\n, _tipButton(TipButtonStyle(\n\tst::paymentsTipButton,\n\t_tipLightBg.color(),\n\t_tipLightRipple.color()))\n, _tipChosen(TipButtonStyle(\n\tst::paymentsTipChosen,\n\t_tipChosenBg.color(),\n\t_tipChosenRipple.color()))\n, _initialScrollTop(scrollTop) {\n\tsetupControls();\n}\n\nrpl::producer<int> FormSummary::scrollTopValue() const {\n\treturn _scroll->scrollTopValue();\n}\n\nbool FormSummary::showCriticalError(const TextWithEntities &text) {\n\tif (_invoice\n\t\t|| (_scroll->height() - _layout->height()\n\t\t\t< st::paymentsPanelSize.height() / 2)) {\n\t\treturn false;\n\t}\n\tUi::AddSkip(_layout.get(), st::paymentsPricesTopSkip);\n\t_layout->add(\n\t\tobject_ptr<FlatLabel>(\n\t\t\t_layout.get(),\n\t\t\trpl::single(text),\n\t\t\tst::paymentsCriticalError),\n\t\tstyle::al_top);\n\treturn true;\n}\n\nint FormSummary::contentHeight() const {\n\treturn _invoice ? _scroll->height() : _layout->height();\n}\n\nvoid FormSummary::updateThumbnail(const QImage &thumbnail) {\n\t_invoice.cover.thumbnail = thumbnail;\n\t_thumbnails.fire_copy(thumbnail);\n}\n\nQString FormSummary::formatAmount(\n\t\tint64 amount,\n\t\tbool forceStripDotZero) const {\n\treturn FillAmountAndCurrency(\n\t\tamount,\n\t\t_invoice.currency,\n\t\tforceStripDotZero);\n}\n\nint64 FormSummary::computeTotalAmount() const {\n\tconst auto total = ranges::accumulate(\n\t\t_invoice.prices,\n\t\tint64(0),\n\t\tstd::plus<>(),\n\t\t&LabeledPrice::price);\n\tconst auto selected = ranges::find(\n\t\t_options.list,\n\t\t_options.selectedId,\n\t\t&ShippingOption::id);\n\tconst auto shipping = (selected != end(_options.list))\n\t\t? ranges::accumulate(\n\t\t\tselected->prices,\n\t\t\tint64(0),\n\t\t\tstd::plus<>(),\n\t\t\t&LabeledPrice::price)\n\t\t: int64(0);\n\treturn total + shipping + _invoice.tipsSelected;\n}\n\nvoid FormSummary::setupControls() {\n\tsetupContent(_layout.get());\n\n\tif (_submit) {\n\t\t_submit->addClickHandler([=] {\n\t\t\t_delegate->panelSubmit();\n\t\t});\n\t}\n\t_cancel->addClickHandler([=] {\n\t\t_delegate->panelRequestClose();\n\t});\n\tif (!_invoice) {\n\t\tif (_submit) {\n\t\t\t_submit->hide();\n\t\t}\n\t\t_cancel->hide();\n\t}\n\n\tusing namespace rpl::mappers;\n\n\t_topShadow->toggleOn(\n\t\t_scroll->scrollTopValue() | rpl::map(_1 > 0));\n\t_bottomShadow->toggleOn(rpl::combine(\n\t\t_scroll->scrollTopValue(),\n\t\t_scroll->heightValue(),\n\t\t_layout->heightValue(),\n\t\t_1 + _2 < _3));\n\n\trpl::merge(\n\t\t(_submit ? _submit->widthValue() : rpl::single(0)),\n\t\t_cancel->widthValue()\n\t) | rpl::skip(2) | rpl::on_next([=] {\n\t\tupdateControlsGeometry();\n\t}, lifetime());\n}\n\nvoid FormSummary::setupCover(not_null<VerticalLayout*> layout) {\n\tstruct State {\n\t\tQImage thumbnail;\n\t\tFlatLabel *title = nullptr;\n\t\tFlatLabel *description = nullptr;\n\t\tFlatLabel *seller = nullptr;\n\t};\n\tconst auto cover = layout->add(object_ptr<RpWidget>(layout));\n\tconst auto state = cover->lifetime().make_state<State>();\n\tstate->title = CreateChild<FlatLabel>(\n\t\tcover,\n\t\t_invoice.cover.title,\n\t\tst::paymentsTitle);\n\tstate->description = CreateChild<FlatLabel>(\n\t\tcover,\n\t\trpl::single(_invoice.cover.description),\n\t\tst::paymentsDescription);\n\tstate->seller = CreateChild<FlatLabel>(\n\t\tcover,\n\t\t_invoice.cover.seller,\n\t\tst::paymentsSeller);\n\tcover->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tif (state->thumbnail.isNull()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto &padding = st::paymentsCoverPadding;\n\t\tconst auto left = padding.left();\n\t\tconst auto top = padding.top();\n\t\tconst auto rect = QRect(\n\t\t\tQPoint(left, top),\n\t\t\tstate->thumbnail.size() / state->thumbnail.devicePixelRatio());\n\t\tif (rect.intersects(clip)) {\n\t\t\tQPainter(cover).drawImage(rect, state->thumbnail);\n\t\t}\n\t}, cover->lifetime());\n\trpl::combine(\n\t\tcover->widthValue(),\n\t\t_thumbnails.events_starting_with_copy(_invoice.cover.thumbnail)\n\t) | rpl::on_next([=](int width, QImage &&thumbnail) {\n\t\tconst auto &padding = st::paymentsCoverPadding;\n\t\tconst auto thumbnailSkip = st::paymentsThumbnailSize.width()\n\t\t\t+ st::paymentsThumbnailSkip;\n\t\tconst auto left = padding.left()\n\t\t\t+ (thumbnail.isNull() ? 0 : thumbnailSkip);\n\t\tconst auto available = width\n\t\t\t- padding.left()\n\t\t\t- padding.right()\n\t\t\t- (thumbnail.isNull() ? 0 : thumbnailSkip);\n\t\tstate->title->resizeToNaturalWidth(available);\n\t\tstate->title->moveToLeft(\n\t\t\tleft,\n\t\t\tpadding.top() + st::paymentsTitleTop);\n\t\tstate->description->resizeToNaturalWidth(available);\n\t\tstate->description->moveToLeft(\n\t\t\tleft,\n\t\t\t(state->title->y()\n\t\t\t\t+ state->title->height()\n\t\t\t\t+ st::paymentsDescriptionTop));\n\t\tstate->seller->resizeToNaturalWidth(available);\n\t\tstate->seller->moveToLeft(\n\t\t\tleft,\n\t\t\t(state->description->y()\n\t\t\t\t+ state->description->height()\n\t\t\t\t+ st::paymentsSellerTop));\n\t\tconst auto thumbnailHeight = padding.top()\n\t\t\t+ (thumbnail.isNull()\n\t\t\t\t? 0\n\t\t\t\t: int(thumbnail.height() / thumbnail.devicePixelRatio()))\n\t\t\t+ padding.bottom();\n\t\tconst auto height = state->seller->y()\n\t\t\t+ state->seller->height()\n\t\t\t+ padding.bottom();\n\t\tcover->resize(width, std::max(thumbnailHeight, height));\n\t\tstate->thumbnail = std::move(thumbnail);\n\t\tcover->update();\n\t}, cover->lifetime());\n}\n\nvoid FormSummary::setupPrices(not_null<VerticalLayout*> layout) {\n\tconst auto addRow = [&](\n\t\t\tconst QString &label,\n\t\t\tconst TextWithEntities &value,\n\t\t\tbool full = false) {\n\t\tconst auto &st = full\n\t\t\t? st::paymentsFullPriceAmount\n\t\t\t: st::paymentsPriceAmount;\n\t\tconst auto right = CreateChild<FlatLabel>(\n\t\t\tlayout.get(),\n\t\t\trpl::single(value),\n\t\t\tst);\n\t\tconst auto &padding = st::paymentsPricePadding;\n\t\tconst auto left = layout->add(\n\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\tlayout,\n\t\t\t\tlabel,\n\t\t\t\t(full\n\t\t\t\t\t? st::paymentsFullPriceLabel\n\t\t\t\t\t: st::paymentsPriceLabel)),\n\t\t\tstyle::margins(\n\t\t\t\tpadding.left(),\n\t\t\t\tpadding.top(),\n\t\t\t\t(padding.right()\n\t\t\t\t\t+ right->textMaxWidth()\n\t\t\t\t\t+ 2 * st.style.font->spacew),\n\t\t\t\tpadding.bottom()));\n\t\trpl::combine(\n\t\t\tleft->topValue(),\n\t\t\tlayout->widthValue()\n\t\t) | rpl::on_next([=](int top, int width) {\n\t\t\tright->moveToRight(st::paymentsPricePadding.right(), top, width);\n\t\t}, right->lifetime());\n\t\treturn right;\n\t};\n\n\tUi::AddSkip(layout, st::paymentsPricesTopSkip);\n\tif (_invoice.receipt) {\n\t\taddRow(\n\t\t\ttr::lng_payments_date_label(tr::now),\n\t\t\t{ langDateTime(base::unixtime::parse(_invoice.receipt.date)) },\n\t\t\ttrue);\n\t\tUi::AddSkip(layout, st::paymentsPricesBottomSkip);\n\t\tUi::AddDivider(layout);\n\t\tUi::AddSkip(layout, st::paymentsPricesBottomSkip);\n\t}\n\n\tconst auto add = [&](\n\t\t\tconst QString &label,\n\t\t\tint64 amount,\n\t\t\tbool full = false) {\n\t\taddRow(label, { formatAmount(amount) }, full);\n\t};\n\tfor (const auto &price : _invoice.prices) {\n\t\tadd(price.label, price.price);\n\t}\n\tconst auto selected = ranges::find(\n\t\t_options.list,\n\t\t_options.selectedId,\n\t\t&ShippingOption::id);\n\tif (selected != end(_options.list)) {\n\t\tfor (const auto &price : selected->prices) {\n\t\t\tadd(price.label, price.price);\n\t\t}\n\t}\n\n\tconst auto computedTotal = computeTotalAmount();\n\tconst auto total = _invoice.receipt.paid\n\t\t? _invoice.receipt.totalAmount\n\t\t: computedTotal;\n\tif (_invoice.receipt.paid) {\n\t\tif (const auto tips = total - computedTotal) {\n\t\t\tadd(tr::lng_payments_tips_label(tr::now), tips);\n\t\t}\n\t} else if (_invoice.tipsMax > 0) {\n\t\tconst auto text = formatAmount(_invoice.tipsSelected);\n\t\tconst auto label = addRow(\n\t\t\ttr::lng_payments_tips_label(tr::now),\n\t\t\ttr::link(text));\n\t\tlabel->overrideLinkClickHandler([=] {\n\t\t\t_delegate->panelChooseTips();\n\t\t});\n\t\tsetupSuggestedTips(layout);\n\t}\n\n\tadd(tr::lng_payments_total_label(tr::now), total, true);\n\tUi::AddSkip(layout, st::paymentsPricesBottomSkip);\n}\n\nvoid FormSummary::setupSuggestedTips(not_null<VerticalLayout*> layout) {\n\tif (_invoice.suggestedTips.empty()) {\n\t\treturn;\n\t}\n\tstruct Button {\n\t\tRoundButton *widget = nullptr;\n\t\tint minWidth = 0;\n\t};\n\tstruct State {\n\t\tstd::vector<Button> buttons;\n\t\tint maxWidth = 0;\n\t};\n\tconst auto outer = layout->add(\n\t\tobject_ptr<RpWidget>(layout),\n\t\tst::paymentsTipButtonsPadding);\n\tconst auto state = outer->lifetime().make_state<State>();\n\tfor (const auto amount : _invoice.suggestedTips) {\n\t\tconst auto selected = (amount == _invoice.tipsSelected);\n\t\tconst auto &st = selected\n\t\t\t? _tipChosen\n\t\t\t: _tipButton;\n\t\tstate->buttons.push_back(Button{\n\t\t\t.widget = CreateChild<RoundButton>(\n\t\t\t\touter,\n\t\t\t\trpl::single(formatAmount(amount, true)),\n\t\t\t\tst),\n\t\t});\n\t\tauto &button = state->buttons.back();\n\t\tbutton.widget->show();\n\t\tbutton.widget->setClickedCallback([=] {\n\t\t\t_delegate->panelChangeTips(selected ? 0 : amount);\n\t\t});\n\t\tbutton.minWidth = button.widget->width();\n\t\tstate->maxWidth = std::max(state->maxWidth, button.minWidth);\n\t}\n\touter->widthValue(\n\t) | rpl::filter([=](int outerWidth) {\n\t\treturn outerWidth >= state->maxWidth;\n\t}) | rpl::on_next([=](int outerWidth) {\n\t\tconst auto skip = st::paymentsTipSkip;\n\t\tconst auto &buttons = state->buttons;\n\t\tauto left = outerWidth;\n\t\tauto height = 0;\n\t\tauto rowStart = 0;\n\t\tauto rowEnd = 0;\n\t\tauto buttonWidths = std::vector<float64>();\n\t\tconst auto layoutRow = [&] {\n\t\t\tconst auto count = rowEnd - rowStart;\n\t\t\tif (!count) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tbuttonWidths.resize(count);\n\t\t\tranges::fill(buttonWidths, 0.);\n\t\t\tauto available = float64(outerWidth - (count - 1) * skip);\n\t\t\tauto zeros = count;\n\t\t\tdo {\n\t\t\t\tconst auto started = zeros;\n\t\t\t\tconst auto average = available / zeros;\n\t\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\t\tif (buttonWidths[i] > 0.) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst auto min = buttons[rowStart + i].minWidth;\n\t\t\t\t\tif (min > average) {\n\t\t\t\t\t\tbuttonWidths[i] = min;\n\t\t\t\t\t\tavailable -= min;\n\t\t\t\t\t\t--zeros;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (started == zeros) {\n\t\t\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\t\t\tif (!buttonWidths[i]) {\n\t\t\t\t\t\t\tbuttonWidths[i] = average;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t} while (zeros > 0);\n\t\t\tauto x = 0.;\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tconst auto button = buttons[rowStart + i].widget;\n\t\t\t\tauto right = x + buttonWidths[i];\n\t\t\t\tbutton->setFullWidth(\n\t\t\t\t\tint(base::SafeRound(right) - base::SafeRound(x)));\n\t\t\t\tbutton->moveToLeft(\n\t\t\t\t\tint(base::SafeRound(x)),\n\t\t\t\t\theight,\n\t\t\t\t\touterWidth);\n\t\t\t\tx = right + skip;\n\t\t\t}\n\t\t\theight += buttons[0].widget->height() + skip;\n\t\t};\n\t\tfor (const auto &button : buttons) {\n\t\t\tif (button.minWidth <= left) {\n\t\t\t\tleft -= button.minWidth + skip;\n\t\t\t\t++rowEnd;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tlayoutRow();\n\t\t\trowStart = rowEnd++;\n\t\t\tleft = outerWidth - button.minWidth - skip;\n\t\t}\n\t\tlayoutRow();\n\t\touter->resize(outerWidth, height - skip);\n\t}, outer->lifetime());\n}\n\nvoid FormSummary::setupSections(not_null<VerticalLayout*> layout) {\n\tUi::AddSkip(layout, st::paymentsSectionsTopSkip);\n\n\tconst auto add = [&](\n\t\t\trpl::producer<QString> title,\n\t\t\tconst QString &label,\n\t\t\tconst style::icon *icon,\n\t\t\tFn<void()> handler) {\n\t\tconst auto button = Settings::AddButtonWithLabel(\n\t\t\tlayout,\n\t\t\tstd::move(title),\n\t\t\trpl::single(label),\n\t\t\tst::paymentsSectionButton,\n\t\t\t{ .icon = icon });\n\t\tbutton->addClickHandler(std::move(handler));\n\t\tif (_invoice.receipt) {\n\t\t\tbutton->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t}\n\t};\n\tadd(\n\t\ttr::lng_payments_payment_method(),\n\t\t(_method.savedMethods.empty()\n\t\t\t? QString()\n\t\t\t: _method.savedMethods[_method.savedMethodIndex].title),\n\t\t&st::paymentsIconPaymentMethod,\n\t\t[=] { _delegate->panelEditPaymentMethod(); });\n\tif (_invoice.isShippingAddressRequested) {\n\t\tauto list = QStringList();\n\t\tconst auto push = [&](const QString &value) {\n\t\t\tif (!value.isEmpty()) {\n\t\t\t\tlist.push_back(value);\n\t\t\t}\n\t\t};\n\t\tpush(_information.shippingAddress.address1);\n\t\tpush(_information.shippingAddress.address2);\n\t\tpush(_information.shippingAddress.city);\n\t\tpush(_information.shippingAddress.state);\n\t\tpush(Countries::Instance().countryNameByISO2(\n\t\t\t_information.shippingAddress.countryIso2));\n\t\tpush(_information.shippingAddress.postcode);\n\t\tadd(\n\t\t\ttr::lng_payments_shipping_address(),\n\t\t\tlist.join(\", \"),\n\t\t\t&st::paymentsIconShippingAddress,\n\t\t\t[=] { _delegate->panelEditShippingInformation(); });\n\t}\n\tif (!_options.list.empty()) {\n\t\tconst auto selected = ranges::find(\n\t\t\t_options.list,\n\t\t\t_options.selectedId,\n\t\t\t&ShippingOption::id);\n\t\tadd(\n\t\t\ttr::lng_payments_shipping_method(),\n\t\t\t(selected != end(_options.list)) ? selected->title : QString(),\n\t\t\t&st::paymentsIconShippingMethod,\n\t\t\t[=] { _delegate->panelChooseShippingOption(); });\n\t}\n\tif (_invoice.isNameRequested) {\n\t\tadd(\n\t\t\ttr::lng_payments_info_name(),\n\t\t\t_information.name,\n\t\t\t&st::paymentsIconName,\n\t\t\t[=] { _delegate->panelEditName(); });\n\t}\n\tif (_invoice.isEmailRequested) {\n\t\tadd(\n\t\t\ttr::lng_payments_info_email(),\n\t\t\t_information.email,\n\t\t\t&st::paymentsIconEmail,\n\t\t\t[=] { _delegate->panelEditEmail(); });\n\t}\n\tif (_invoice.isPhoneRequested) {\n\t\tadd(\n\t\t\ttr::lng_payments_info_phone(),\n\t\t\t(_information.phone.isEmpty()\n\t\t\t\t? QString()\n\t\t\t\t: Ui::FormatPhone(_information.phone)),\n\t\t\t&st::paymentsIconPhone,\n\t\t\t[=] { _delegate->panelEditPhone(); });\n\t}\n\tUi::AddSkip(layout, st::paymentsSectionsTopSkip);\n}\n\nvoid FormSummary::setupContent(not_null<VerticalLayout*> layout) {\n\t_scroll->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tlayout->resizeToWidth(width);\n\t}, layout->lifetime());\n\n\tsetupCover(layout);\n\tif (_invoice) {\n\t\tUi::AddDivider(layout);\n\t\tsetupPrices(layout);\n\t\tUi::AddDivider(layout);\n\t\tsetupSections(layout);\n\t}\n}\n\nvoid FormSummary::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid FormSummary::updateControlsGeometry() {\n\tconst auto &padding = st::paymentsPanelPadding;\n\tconst auto buttonsHeight = padding.top()\n\t\t+ _cancel->height()\n\t\t+ padding.bottom();\n\tconst auto buttonsTop = height() - buttonsHeight;\n\t_scroll->setGeometry(0, 0, width(), buttonsTop);\n\t_topShadow->resizeToWidth(width());\n\t_topShadow->moveToLeft(0, 0);\n\t_bottomShadow->resizeToWidth(width());\n\t_bottomShadow->moveToLeft(0, buttonsTop - st::lineWidth);\n\tauto right = padding.right();\n\tif (_submit) {\n\t\t_submit->moveToRight(right, buttonsTop + padding.top());\n\t\tright += _submit->width() + padding.left();\n\t}\n\t_cancel->moveToRight(right, buttonsTop + padding.top());\n\n\t_scroll->updateBars();\n\n\tif (buttonsTop > 0 && width() > 0) {\n\t\tif (const auto top = base::take(_initialScrollTop)) {\n\t\t\t_scroll->scrollToY(top);\n\t\t}\n\t}\n}\n\n} // namespace Payments::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/ui/payments_form_summary.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"payments/ui/payments_panel_data.h\"\n#include \"base/object_ptr.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Ui {\nclass ScrollArea;\nclass FadeShadow;\nclass RoundButton;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Payments::Ui {\n\nusing namespace ::Ui;\n\nclass PanelDelegate;\n\nclass FormSummary final : public RpWidget {\npublic:\n\tFormSummary(\n\t\tQWidget *parent,\n\t\tconst Invoice &invoice,\n\t\tconst RequestedInformation &current,\n\t\tconst PaymentMethodDetails &method,\n\t\tconst ShippingOptions &options,\n\t\tnot_null<PanelDelegate*> delegate,\n\t\tint scrollTop);\n\n\tvoid updateThumbnail(const QImage &thumbnail);\n\t[[nodiscard]] rpl::producer<int> scrollTopValue() const;\n\n\tbool showCriticalError(const TextWithEntities &text);\n\t[[nodiscard]] int contentHeight() const;\n\nprivate:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\tvoid setupControls();\n\tvoid setupContent(not_null<VerticalLayout*> layout);\n\tvoid setupCover(not_null<VerticalLayout*> layout);\n\tvoid setupPrices(not_null<VerticalLayout*> layout);\n\tvoid setupSuggestedTips(not_null<VerticalLayout*> layout);\n\tvoid setupSections(not_null<VerticalLayout*> layout);\n\tvoid updateControlsGeometry();\n\n\t[[nodiscard]] QString formatAmount(\n\t\tint64 amount,\n\t\tbool forceStripDotZero = false) const;\n\t[[nodiscard]] int64 computeTotalAmount() const;\n\n\tconst not_null<PanelDelegate*> _delegate;\n\tInvoice _invoice;\n\tPaymentMethodDetails _method;\n\tShippingOptions _options;\n\tRequestedInformation _information;\n\tobject_ptr<ScrollArea> _scroll;\n\tnot_null<VerticalLayout*> _layout;\n\tobject_ptr<FadeShadow> _topShadow;\n\tobject_ptr<FadeShadow> _bottomShadow;\n\tobject_ptr<RoundButton> _submit;\n\tobject_ptr<RoundButton> _cancel;\n\trpl::event_stream<QImage> _thumbnails;\n\n\tstyle::complex_color _tipLightBg;\n\tstyle::complex_color _tipLightRipple;\n\tstyle::complex_color _tipChosenBg;\n\tstyle::complex_color _tipChosenRipple;\n\tstyle::RoundButton _tipButton;\n\tstyle::RoundButton _tipChosen;\n\tint _initialScrollTop = 0;\n\n};\n\n} // namespace Payments::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/ui/payments_panel.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"payments/ui/payments_panel.h\"\n\n#include \"payments/ui/payments_form_summary.h\"\n#include \"payments/ui/payments_edit_information.h\"\n#include \"payments/ui/payments_edit_card.h\"\n#include \"payments/ui/payments_panel_delegate.h\"\n#include \"payments/ui/payments_field.h\"\n#include \"ui/widgets/separate_panel.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/boxes/single_choice_box.h\"\n#include \"ui/chat/attach/attach_bot_webview.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/click_handler.h\"\n#include \"lang/lang_keys.h\"\n#include \"webview/webview_embed.h\"\n#include \"webview/webview_interface.h\"\n#include \"styles/style_payments.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Payments::Ui {\nnamespace {\n\nconstexpr auto kProgressDuration = crl::time(200);\nconstexpr auto kProgressOpacity = 0.3;\n\n} // namespace\n\nstruct Panel::Progress {\n\tProgress(QWidget *parent, Fn<QRect()> rect);\n\n\tRpWidget widget;\n\tInfiniteRadialAnimation animation;\n\tAnimations::Simple shownAnimation;\n\tbool shown = true;\n\trpl::lifetime geometryLifetime;\n};\n\nstruct Panel::WebviewWithLifetime {\n\tWebviewWithLifetime(\n\t\tQWidget *parent = nullptr,\n\t\tWebview::WindowConfig config = Webview::WindowConfig());\n\n\tWebview::Window window;\n\tQPointer<RpWidget> lastHidingBox;\n\trpl::lifetime lifetime;\n};\n\nPanel::WebviewWithLifetime::WebviewWithLifetime(\n\tQWidget *parent,\n\tWebview::WindowConfig config)\n: window(parent, std::move(config)) {\n}\n\nPanel::Progress::Progress(QWidget *parent, Fn<QRect()> rect)\n: widget(parent)\n, animation(\n\t[=] { if (!anim::Disabled()) widget.update(rect()); },\n\tst::paymentsLoading) {\n}\n\nPanel::Panel(not_null<PanelDelegate*> delegate)\n: _delegate(delegate)\n, _widget(std::make_unique<SeparatePanel>()) {\n\t_widget->setWindowFlag(Qt::WindowStaysOnTopHint, false);\n\t_widget->setInnerSize(st::paymentsPanelSize);\n\n\t_widget->closeRequests(\n\t) | rpl::on_next([=] {\n\t\t_delegate->panelRequestClose();\n\t}, _widget->lifetime());\n\n\t_widget->closeEvents(\n\t) | rpl::on_next([=] {\n\t\t_delegate->panelCloseSure();\n\t}, _widget->lifetime());\n\n\tstyle::PaletteChanged(\n\t) | rpl::filter([=] {\n\t\treturn !_themeUpdateScheduled;\n\t}) | rpl::on_next([=] {\n\t\t_themeUpdateScheduled = true;\n\t\tcrl::on_main(_widget.get(), [=] {\n\t\t\t_themeUpdateScheduled = false;\n\t\t\tupdateThemeParams(_delegate->panelWebviewThemeParams());\n\t\t});\n\t}, lifetime());\n}\n\nPanel::~Panel() {\n\tbase::take(_webview);\n\t_progress = nullptr;\n\t_widget = nullptr;\n}\n\nvoid Panel::requestActivate() {\n\t_widget->showAndActivate();\n}\n\nvoid Panel::toggleProgress(bool shown) {\n\tif (!_progress) {\n\t\tif (!shown) {\n\t\t\treturn;\n\t\t}\n\t\t_progress = std::make_unique<Progress>(\n\t\t\t_widget.get(),\n\t\t\t[=] { return progressRect(); });\n\t\t_progress->widget.paintRequest(\n\t\t) | rpl::on_next([=](QRect clip) {\n\t\t\tauto p = QPainter(&_progress->widget);\n\t\t\tp.setOpacity(\n\t\t\t\t_progress->shownAnimation.value(_progress->shown ? 1. : 0.));\n\t\t\tauto thickness = st::paymentsLoading.thickness;\n\t\t\tif (progressWithBackground()) {\n\t\t\t\tauto color = st::windowBg->c;\n\t\t\t\tcolor.setAlphaF(kProgressOpacity);\n\t\t\t\tp.fillRect(clip, color);\n\t\t\t}\n\t\t\tconst auto rect = progressRect().marginsRemoved(\n\t\t\t\t{ thickness, thickness, thickness, thickness });\n\t\t\tInfiniteRadialAnimation::Draw(\n\t\t\t\tp,\n\t\t\t\t_progress->animation.computeState(),\n\t\t\t\trect.topLeft(),\n\t\t\t\trect.size() - QSize(),\n\t\t\t\t_progress->widget.width(),\n\t\t\t\tst::paymentsLoading.color,\n\t\t\t\tthickness);\n\t\t}, _progress->widget.lifetime());\n\t\t_progress->widget.show();\n\t\t_progress->animation.start();\n\t} else if (_progress->shown == shown) {\n\t\treturn;\n\t}\n\tconst auto callback = [=] {\n\t\tif (!_progress->shownAnimation.animating() && !_progress->shown) {\n\t\t\t_progress = nullptr;\n\t\t} else {\n\t\t\t_progress->widget.update();\n\t\t}\n\t};\n\t_progress->shown = shown;\n\t_progress->shownAnimation.start(\n\t\tcallback,\n\t\tshown ? 0. : 1.,\n\t\tshown ? 1. : 0.,\n\t\tkProgressDuration);\n\tif (shown) {\n\t\tsetupProgressGeometry();\n\t}\n}\n\nbool Panel::progressWithBackground() const {\n\treturn (_progress->widget.width() == _widget->innerGeometry().width());\n}\n\nQRect Panel::progressRect() const {\n\tconst auto rect = _progress->widget.rect();\n\tif (!progressWithBackground()) {\n\t\treturn rect;\n\t}\n\tconst auto size = st::defaultBoxButton.height;\n\treturn QRect(\n\t\trect.x() + (rect.width() - size) / 2,\n\t\trect.y() + (rect.height() - size) / 2,\n\t\tsize,\n\t\tsize);\n}\n\nvoid Panel::setupProgressGeometry() {\n\tif (!_progress || !_progress->shown) {\n\t\treturn;\n\t}\n\t_progress->geometryLifetime.destroy();\n\tif (_webviewBottom) {\n\t\t_webviewBottom->geometryValue(\n\t\t) | rpl::on_next([=](QRect bottom) {\n\t\t\tconst auto height = bottom.height();\n\t\t\tconst auto size = st::paymentsLoading.size;\n\t\t\tconst auto skip = (height - size.height()) / 2;\n\t\t\tconst auto inner = _widget->innerGeometry();\n\t\t\tconst auto right = inner.x() + inner.width();\n\t\t\tconst auto top = inner.y() + inner.height() - height;\n\t\t\t// This doesn't work, because first we get the correct bottom\n\t\t\t// geometry and after that we get the previous event (which\n\t\t\t// triggered the 'fire' of correct geometry before getting here).\n\t\t\t//const auto right = bottom.x() + bottom.width();\n\t\t\t//const auto top = bottom.y();\n\t\t\t_progress->widget.setGeometry(QRect{\n\t\t\t\tQPoint(right - skip - size.width(), top + skip),\n\t\t\t\tsize });\n\t\t}, _progress->geometryLifetime);\n\t} else if (_weakFormSummary) {\n\t\t_weakFormSummary->sizeValue(\n\t\t) | rpl::on_next([=](QSize form) {\n\t\t\tconst auto full = _widget->innerGeometry();\n\t\t\tconst auto size = st::defaultBoxButton.height;\n\t\t\tconst auto inner = _weakFormSummary->contentHeight();\n\t\t\tconst auto left = full.height() - inner;\n\t\t\tif (left >= 2 * size) {\n\t\t\t\t_progress->widget.setGeometry(\n\t\t\t\t\tfull.x() + (full.width() - size) / 2,\n\t\t\t\t\tfull.y() + inner + (left - size) / 2,\n\t\t\t\t\tsize,\n\t\t\t\t\tsize);\n\t\t\t} else {\n\t\t\t\t_progress->widget.setGeometry(full);\n\t\t\t}\n\t\t}, _progress->geometryLifetime);\n\t} else if (_weakEditInformation) {\n\t\t_weakEditInformation->geometryValue(\n\t\t) | rpl::on_next([=] {\n\t\t\t_progress->widget.setGeometry(_widget->innerGeometry());\n\t\t}, _progress->geometryLifetime);\n\t} else if (_weakEditCard) {\n\t\t_weakEditCard->geometryValue(\n\t\t) | rpl::on_next([=] {\n\t\t\t_progress->widget.setGeometry(_widget->innerGeometry());\n\t\t}, _progress->geometryLifetime);\n\t}\n\t_progress->widget.show();\n\t_progress->widget.raise();\n\tif (_progress->shown) {\n\t\t_progress->widget.setFocus();\n\t}\n}\n\nvoid Panel::showForm(\n\t\tconst Invoice &invoice,\n\t\tconst RequestedInformation &current,\n\t\tconst PaymentMethodDetails &method,\n\t\tconst ShippingOptions &options) {\n\tif (invoice\n\t\t&& method.savedMethods.empty()\n\t\t&& !method.native.supported) {\n\t\tconst auto available = Webview::Availability();\n\t\tif (available.error != Webview::Available::Error::None) {\n\t\t\tshowWebviewError(\n\t\t\t\ttr::lng_payments_webview_no_use(tr::now),\n\t\t\t\tavailable);\n\t\t\treturn;\n\t\t}\n\t}\n\n\t_testMode = invoice.isTest;\n\tsetTitle(invoice.receipt\n\t\t? tr::lng_payments_receipt_title()\n\t\t: tr::lng_payments_checkout_title());\n\tauto form = base::make_unique_q<FormSummary>(\n\t\t_widget.get(),\n\t\tinvoice,\n\t\tcurrent,\n\t\tmethod,\n\t\toptions,\n\t\t_delegate,\n\t\t_formScrollTop.current());\n\t_weakFormSummary = form.get();\n\t_widget->showInner(std::move(form));\n\t_widget->setBackAllowed(false);\n\t_formScrollTop = _weakFormSummary->scrollTopValue();\n\tsetupProgressGeometry();\n}\n\nvoid Panel::updateFormThumbnail(const QImage &thumbnail) {\n\tif (_weakFormSummary) {\n\t\t_weakFormSummary->updateThumbnail(thumbnail);\n\t}\n}\n\nvoid Panel::showEditInformation(\n\t\tconst Invoice &invoice,\n\t\tconst RequestedInformation &current,\n\t\tInformationField field) {\n\tsetTitle(tr::lng_payments_shipping_address_title());\n\tauto edit = base::make_unique_q<EditInformation>(\n\t\t_widget.get(),\n\t\tinvoice,\n\t\tcurrent,\n\t\tfield,\n\t\t_delegate);\n\t_weakEditInformation = edit.get();\n\t_widget->showInner(std::move(edit));\n\t_widget->setBackAllowed(true);\n\t_weakEditInformation->setFocusFast(field);\n\tsetupProgressGeometry();\n}\n\nvoid Panel::showInformationError(\n\t\tconst Invoice &invoice,\n\t\tconst RequestedInformation &current,\n\t\tInformationField field) {\n\tif (_weakEditInformation) {\n\t\t_weakEditInformation->showError(field);\n\t} else {\n\t\tshowEditInformation(invoice, current, field);\n\t\tif (_weakEditInformation\n\t\t\t&& field == InformationField::ShippingCountry) {\n\t\t\t_weakEditInformation->showError(field);\n\t\t}\n\t}\n}\n\nvoid Panel::chooseShippingOption(const ShippingOptions &options) {\n\tshowBox(Box([=](not_null<GenericBox*> box) {\n\t\tconst auto i = ranges::find(\n\t\t\toptions.list,\n\t\t\toptions.selectedId,\n\t\t\t&ShippingOption::id);\n\t\tconst auto index = (i != end(options.list))\n\t\t\t? int(i - begin(options.list))\n\t\t\t: -1;\n\t\tconst auto group = std::make_shared<RadiobuttonGroup>(index);\n\n\t\tconst auto layout = box->verticalLayout();\n\t\tauto counter = 0;\n\t\tfor (const auto &option : options.list) {\n\t\t\tconst auto index = counter++;\n\t\t\tconst auto button = layout->add(\n\t\t\t\tobject_ptr<Radiobutton>(\n\t\t\t\t\tlayout,\n\t\t\t\t\tgroup,\n\t\t\t\t\tindex,\n\t\t\t\t\tQString(),\n\t\t\t\t\tst::defaultBoxCheckbox,\n\t\t\t\t\tst::defaultRadio),\n\t\t\t\tst::paymentsShippingMargin);\n\t\t\tconst auto label = CreateChild<FlatLabel>(\n\t\t\t\tlayout.get(),\n\t\t\t\toption.title,\n\t\t\t\tst::paymentsShippingLabel);\n\t\t\tconst auto total = ranges::accumulate(\n\t\t\t\toption.prices,\n\t\t\t\tint64(0),\n\t\t\t\tstd::plus<>(),\n\t\t\t\t&LabeledPrice::price);\n\t\t\tconst auto price = CreateChild<FlatLabel>(\n\t\t\t\tlayout.get(),\n\t\t\t\tFillAmountAndCurrency(total, options.currency),\n\t\t\t\tst::paymentsShippingPrice);\n\t\t\tconst auto area = CreateChild<AbstractButton>(layout.get());\n\t\t\tarea->setClickedCallback([=] { group->setValue(index); });\n\t\t\tbutton->geometryValue(\n\t\t\t) | rpl::on_next([=](QRect geometry) {\n\t\t\t\tlabel->move(\n\t\t\t\t\tgeometry.topLeft() + st::paymentsShippingLabelPosition);\n\t\t\t\tprice->move(\n\t\t\t\t\tgeometry.topLeft() + st::paymentsShippingPricePosition);\n\t\t\t\tconst auto right = geometry.x()\n\t\t\t\t\t+ st::paymentsShippingLabelPosition.x();\n\t\t\t\tarea->setGeometry(\n\t\t\t\t\tright,\n\t\t\t\t\tgeometry.y(),\n\t\t\t\t\tstd::max(\n\t\t\t\t\t\tlabel->x() + label->width() - right,\n\t\t\t\t\t\tprice->x() + price->width() - right),\n\t\t\t\t\tprice->y() + price->height() - geometry.y());\n\t\t\t}, button->lifetime());\n\t\t}\n\n\t\tbox->setTitle(tr::lng_payments_shipping_method());\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t\tgroup->setChangedCallback([=](int index) {\n\t\t\tif (index >= 0) {\n\t\t\t\t_delegate->panelChangeShippingOption(\n\t\t\t\t\toptions.list[index].id);\n\t\t\t\tbox->closeBox();\n\t\t\t}\n\t\t});\n\t}));\n}\n\nvoid Panel::chooseTips(const Invoice &invoice) {\n\tconst auto max = invoice.tipsMax;\n\tconst auto now = invoice.tipsSelected;\n\tconst auto currency = invoice.currency;\n\tshowBox(Box([=](not_null<GenericBox*> box) {\n\t\tbox->setTitle(tr::lng_payments_tips_box_title());\n\t\tconst auto row = box->lifetime().make_state<Field>(\n\t\t\tbox,\n\t\t\tFieldConfig{\n\t\t\t\t.type = FieldType::Money,\n\t\t\t\t.value = QString::number(now),\n\t\t\t\t.currency = currency,\n\t\t\t});\n\t\tbox->setFocusCallback([=] {\n\t\t\trow->setFocusFast();\n\t\t});\n\t\tbox->addRow(row->ownedWidget());\n\t\tconst auto errorWrap = box->addRow(\n\t\t\tobject_ptr<FadeWrap<FlatLabel>>(\n\t\t\t\tbox,\n\t\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\t\tbox,\n\t\t\t\t\ttr::lng_payments_tips_max(\n\t\t\t\t\t\tlt_amount,\n\t\t\t\t\t\trpl::single(FillAmountAndCurrency(max, currency))),\n\t\t\t\t\tst::paymentTipsErrorLabel)),\n\t\t\tst::paymentTipsErrorPadding);\n\t\terrorWrap->hide(anim::type::instant);\n\t\tconst auto submit = [=] {\n\t\t\tconst auto value = row->value().toLongLong();\n\t\t\tif (value > max) {\n\t\t\t\trow->showError();\n\t\t\t\terrorWrap->show(anim::type::normal);\n\t\t\t} else {\n\t\t\t\t_delegate->panelChangeTips(value);\n\t\t\t\tbox->closeBox();\n\t\t\t}\n\t\t};\n\t\trow->submitted(\n\t\t) | rpl::on_next(submit, box->lifetime());\n\t\tbox->addButton(tr::lng_settings_save(), submit);\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t}));\n}\n\nvoid Panel::showEditPaymentMethod(const PaymentMethodDetails &method) {\n\tsetTitle(tr::lng_payments_card_title());\n\tif (method.native.supported) {\n\t\tshowEditCard(method.native, CardField::Number);\n\t} else {\n\t\tshowEditCardByUrl(\n\t\t\tmethod.url,\n\t\t\tmethod.provider,\n\t\t\tmethod.canSaveInformation);\n\t}\n}\n\nvoid Panel::showEditCardByUrl(\n\t\tconst QString &url,\n\t\tconst QString &provider,\n\t\tbool canSaveInformation) {\n\tauto bottomText = canSaveInformation\n\t\t? rpl::producer<QString>()\n\t\t: tr::lng_payments_processed_by(lt_provider, rpl::single(provider));\n\tif (!showWebview(url, true, std::move(bottomText))) {\n\t\tconst auto available = Webview::Availability();\n\t\tif (available.error != Webview::Available::Error::None) {\n\t\t\tshowWebviewError(\n\t\t\t\ttr::lng_payments_webview_no_use(tr::now),\n\t\t\t\tavailable);\n\t\t} else {\n\t\t\tshowCriticalError({ \"Error: Could not initialize WebView.\" });\n\t\t}\n\t\t_widget->setBackAllowed(true);\n\t} else if (canSaveInformation) {\n\t\tconst auto &padding = st::paymentsPanelPadding;\n\t\t_saveWebviewInformation = CreateChild<Checkbox>(\n\t\t\t_webviewBottom.get(),\n\t\t\ttr::lng_payments_save_information(tr::now),\n\t\t\tfalse);\n\t\tconst auto height = padding.top()\n\t\t\t+ _saveWebviewInformation->heightNoMargins()\n\t\t\t+ padding.bottom();\n\t\t_saveWebviewInformation->moveToLeft(padding.right(), padding.top());\n\t\t_saveWebviewInformation->show();\n\t\t_webviewBottom->resize(_webviewBottom->width(), height);\n\t}\n}\n\nvoid Panel::showAdditionalMethod(\n\t\tconst PaymentMethodAdditional &method,\n\t\tconst QString &provider,\n\t\tbool canSaveInformation) {\n\tsetTitle(rpl::single(method.title));\n\tshowEditCardByUrl(method.url, provider, canSaveInformation);\n}\n\nvoid Panel::showWebviewProgress() {\n\tif (_webviewProgress && _progress && _progress->shown) {\n\t\treturn;\n\t}\n\t_webviewProgress = true;\n\ttoggleProgress(true);\n}\n\nvoid Panel::hideWebviewProgress() {\n\tif (!_webviewProgress) {\n\t\treturn;\n\t}\n\t_webviewProgress = false;\n\ttoggleProgress(false);\n}\n\nbool Panel::showWebview(\n\t\tconst QString &url,\n\t\tbool allowBack,\n\t\trpl::producer<QString> bottomText) {\n\tconst auto params = _delegate->panelWebviewThemeParams();\n\tif (!_webview && !createWebview(params)) {\n\t\treturn false;\n\t}\n\tshowWebviewProgress();\n\t_widget->hideLayer(anim::type::instant);\n\tupdateThemeParams(params);\n\t_webview->window.navigate(url);\n\t_widget->setBackAllowed(allowBack);\n\tif (bottomText) {\n\t\tconst auto &padding = st::paymentsPanelPadding;\n\t\tconst auto label = CreateChild<FlatLabel>(\n\t\t\t_webviewBottom.get(),\n\t\t\tstd::move(bottomText),\n\t\t\tst::paymentsWebviewBottom);\n\t\tconst auto height = padding.top()\n\t\t\t+ label->heightNoMargins()\n\t\t\t+ padding.bottom();\n\t\trpl::combine(\n\t\t\t_webviewBottom->widthValue(),\n\t\t\tlabel->widthValue()\n\t\t) | rpl::on_next([=](int outerWidth, int width) {\n\t\t\tlabel->move((outerWidth - width) / 2, padding.top());\n\t\t}, label->lifetime());\n\t\tlabel->show();\n\t\t_webviewBottom->resize(_webviewBottom->width(), height);\n\t}\n\treturn true;\n}\n\nbool Panel::createWebview(const Webview::ThemeParams &params) {\n\tauto outer = base::make_unique_q<RpWidget>(_widget.get());\n\tconst auto container = outer.get();\n\t_widget->showInner(std::move(outer));\n\tconst auto webviewParent = QPointer<RpWidget>(container);\n\n\t_webviewBottom = std::make_unique<RpWidget>(_widget.get());\n\tconst auto bottom = _webviewBottom.get();\n\tbottom->show();\n\n\trpl::combine(\n\t\tcontainer->geometryValue() | rpl::map([=] {\n\t\t\treturn _widget->innerGeometry();\n\t\t}),\n\t\tbottom->heightValue()\n\t) | rpl::on_next([=](QRect inner, int height) {\n\t\tbottom->move(inner.x(), inner.y() + inner.height() - height);\n\t\tbottom->resizeToWidth(inner.width());\n\t\t_footerHeight = bottom->height();\n\t}, bottom->lifetime());\n\tcontainer->show();\n\n\t_webview = std::make_unique<WebviewWithLifetime>(\n\t\tcontainer,\n\t\tWebview::WindowConfig{\n\t\t\t.opaqueBg = params.bodyBg,\n\t\t\t.storageId = _delegate->panelWebviewStorageId(),\n\t\t});\n\n\tconst auto raw = &_webview->window;\n\tQObject::connect(container, &QObject::destroyed, [=] {\n\t\tif (_webview && &_webview->window == raw) {\n\t\t\tbase::take(_webview);\n\t\t\tif (_webviewProgress) {\n\t\t\t\thideWebviewProgress();\n\t\t\t\tif (_progress && !_progress->shown) {\n\t\t\t\t\t_progress = nullptr;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (_webviewBottom.get() == bottom) {\n\t\t\t_webviewBottom = nullptr;\n\t\t}\n\t});\n\tif (!raw->widget()) {\n\t\treturn false;\n\t}\n\tQObject::connect(raw->widget(), &QObject::destroyed, [=] {\n\t\tconst auto parent = webviewParent.data();\n\t\tif (!_webview\n\t\t\t|| &_webview->window != raw\n\t\t\t|| !parent\n\t\t\t|| _widget->inner() != parent) {\n\t\t\t// If we destroyed _webview ourselves,\n\t\t\t// or if we changed _widget->inner ourselves,\n\t\t\t// we don't show any message, nothing crashed.\n\t\t\treturn;\n\t\t}\n\t\tcrl::on_main(this, [=] {\n\t\t\tshowCriticalError({ \"Error: WebView has crashed.\" });\n\t\t});\n\t});\n\n\trpl::combine(\n\t\tcontainer->geometryValue(),\n\t\t_footerHeight.value()\n\t) | rpl::on_next([=](QRect geometry, int footer) {\n\t\tif (const auto view = raw->widget()) {\n\t\t\tview->setGeometry(geometry.marginsRemoved({ 0, 0, 0, footer }));\n\t\t}\n\t}, _webview->lifetime);\n\n\traw->setMessageHandler([=](const QJsonDocument &message) {\n\t\tconst auto save = _saveWebviewInformation\n\t\t\t&& _saveWebviewInformation->checked();\n\t\t_delegate->panelWebviewMessage(message, save);\n\t});\n\n\traw->setNavigationStartHandler([=](const QString &uri, bool newWindow) {\n\t\tif (!_delegate->panelWebviewNavigationAttempt(uri)) {\n\t\t\treturn false;\n\t\t} else if (newWindow) {\n\t\t\treturn false;\n\t\t}\n\t\tshowWebviewProgress();\n\t\treturn true;\n\t});\n\traw->setNavigationDoneHandler([=](bool success) {\n\t\thideWebviewProgress();\n\t});\n\n\traw->init(R\"(\nwindow.TelegramWebviewProxy = {\npostEvent: function(eventType, eventData) {\n\tif (window.external && window.external.invoke) {\n\t\twindow.external.invoke(JSON.stringify([eventType, eventData]));\n\t}\n}\n};)\");\n\n\tif (!_webview) {\n\t\treturn false;\n\t}\n\n\tsetupProgressGeometry();\n\n\treturn true;\n}\n\nvoid Panel::choosePaymentMethod(const PaymentMethodDetails &method) {\n\tif (method.savedMethods.empty() && method.additionalMethods.empty()) {\n\t\tshowEditPaymentMethod(method);\n\t\treturn;\n\t}\n\tshowBox(Box([=](not_null<GenericBox*> box) {\n\t\tconst auto save = [=](int option) {\n\t\t\tconst auto saved = int(method.savedMethods.size());\n\t\t\tif (!option) {\n\t\t\t\tshowEditPaymentMethod(method);\n\t\t\t} else if (option > saved) {\n\t\t\t\tconst auto index = option - saved - 1;\n\t\t\t\tAssert(index < method.additionalMethods.size());\n\t\t\t\tshowAdditionalMethod(\n\t\t\t\t\tmethod.additionalMethods[index],\n\t\t\t\t\tmethod.provider,\n\t\t\t\t\tmethod.canSaveInformation);\n\t\t\t} else {\n\t\t\t\tconst auto index = option - 1;\n\t\t\t\t_savedMethodChosen.fire_copy(method.savedMethods[index].id);\n\t\t\t}\n\t\t};\n\t\tauto options = std::vector{\n\t\t\ttr::lng_payments_new_card(tr::now),\n\t\t};\n\t\tfor (const auto &saved : method.savedMethods) {\n\t\t\toptions.push_back(saved.title);\n\t\t}\n\t\tfor (const auto &additional : method.additionalMethods) {\n\t\t\toptions.push_back(additional.title);\n\t\t}\n\t\tSingleChoiceBox(box, {\n\t\t\t.title = tr::lng_payments_payment_method(),\n\t\t\t.options = std::move(options),\n\t\t\t.initialSelection = (method.savedMethods.empty()\n\t\t\t\t? -1\n\t\t\t\t: (method.savedMethodIndex + 1)),\n\t\t\t.callback = save,\n\t\t});\n\t}));\n}\n\nvoid Panel::askSetPassword() {\n\tshowBox(Box([=](not_null<GenericBox*> box) {\n\t\tbox->addRow(\n\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\tbox.get(),\n\t\t\t\ttr::lng_payments_need_password(),\n\t\t\t\tst::boxLabel),\n\t\t\tst::boxPadding);\n\t\tbox->addButton(tr::lng_continue(), [=] {\n\t\t\t_delegate->panelSetPassword();\n\t\t\tbox->closeBox();\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t}));\n}\n\nvoid Panel::showCloseConfirm() {\n\tshowBox(Box([=](not_null<GenericBox*> box) {\n\t\tbox->addRow(\n\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\tbox.get(),\n\t\t\t\ttr::lng_payments_sure_close(),\n\t\t\t\tst::boxLabel),\n\t\t\tst::boxPadding);\n\t\tbox->addButton(tr::lng_close(), [=] {\n\t\t\t_delegate->panelCloseSure();\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t}));\n}\n\nvoid Panel::showWarning(const QString &bot, const QString &provider) {\n\tshowBox(Box([=](not_null<GenericBox*> box) {\n\t\tbox->setTitle(tr::lng_payments_warning_title());\n\t\tbox->addRow(object_ptr<FlatLabel>(\n\t\t\tbox.get(),\n\t\t\ttr::lng_payments_warning_body(\n\t\t\t\tlt_bot1,\n\t\t\t\trpl::single(bot),\n\t\t\t\tlt_provider,\n\t\t\t\trpl::single(provider),\n\t\t\t\tlt_bot2,\n\t\t\t\trpl::single(bot),\n\t\t\t\tlt_bot3,\n\t\t\t\trpl::single(bot)),\n\t\t\tst::boxLabel));\n\t\tbox->addButton(tr::lng_continue(), [=] {\n\t\t\t_delegate->panelTrustAndSubmit();\n\t\t\tbox->closeBox();\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t}));\n}\n\nvoid Panel::requestTermsAcceptance(\n\t\tconst QString &username,\n\t\tconst QString &url,\n\t\tbool recurring) {\n\tshowBox(Box([=](not_null<GenericBox*> box) {\n\t\tbox->setTitle(tr::lng_payments_terms_title());\n\t\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\t\tbox.get(),\n\t\t\t(recurring\n\t\t\t\t? tr::lng_payments_terms_text\n\t\t\t\t: tr::lng_payments_terms_text_once)(\n\t\t\t\t\tlt_bot,\n\t\t\t\t\trpl::single(tr::bold('@' + username)),\n\t\t\t\t\ttr::marked),\n\t\t\tst::boxLabel));\n\t\tconst auto update = std::make_shared<Fn<void()>>();\n\t\tauto checkView = std::make_unique<Ui::CheckView>(\n\t\t\tst::defaultCheck,\n\t\t\tfalse,\n\t\t\t[=] { if (*update) { (*update)(); } });\n\t\tconst auto check = checkView.get();\n\t\tconst auto row = box->addRow(\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tbox.get(),\n\t\t\t\ttr::lng_payments_terms_agree(\n\t\t\t\t\tlt_link,\n\t\t\t\t\trpl::single(tr::link(\n\t\t\t\t\t\ttr::lng_payments_terms_link(tr::now),\n\t\t\t\t\t\turl)),\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::defaultBoxCheckbox,\n\t\t\t\tstd::move(checkView)),\n\t\t\t{\n\t\t\t\tst::boxRowPadding.left(),\n\t\t\t\tst::boxRowPadding.left(),\n\t\t\t\tst::boxRowPadding.right(),\n\t\t\t\tst::defaultBoxCheckbox.margin.bottom(),\n\t\t\t});\n\t\trow->setAllowTextLines(5);\n\t\trow->setClickHandlerFilter([=](\n\t\t\t\tconst ClickHandlerPtr &link,\n\t\t\t\tQt::MouseButton button) {\n\t\t\tActivateClickHandler(_widget.get(), link, ClickContext{\n\t\t\t\t.button = button,\n\t\t\t\t.other = _delegate->panelClickHandlerContext(),\n\t\t\t});\n\t\t\treturn false;\n\t\t});\n\n\t\t(*update) = [=] { row->update(); };\n\n\t\tconst auto showError = Ui::CheckView::PrepareNonToggledError(\n\t\t\tcheck,\n\t\t\tbox->lifetime());\n\n\t\tbox->addButton(tr::lng_payments_terms_accept(), [=] {\n\t\t\tif (check->checked()) {\n\t\t\t\t_delegate->panelAcceptTermsAndSubmit();\n\t\t\t\tbox->closeBox();\n\t\t\t} else {\n\t\t\t\tshowError();\n\t\t\t}\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t}));\n}\n\nvoid Panel::showEditCard(\n\t\tconst NativeMethodDetails &native,\n\t\tCardField field) {\n\tExpects(native.supported);\n\n\tauto edit = base::make_unique_q<EditCard>(\n\t\t_widget.get(),\n\t\tnative,\n\t\tfield,\n\t\t_delegate);\n\t_weakEditCard = edit.get();\n\t_widget->showInner(std::move(edit));\n\t_widget->setBackAllowed(true);\n\t_weakEditCard->setFocusFast(field);\n\tsetupProgressGeometry();\n}\n\nvoid Panel::showCardError(\n\t\tconst NativeMethodDetails &native,\n\t\tCardField field) {\n\tif (_weakEditCard) {\n\t\t_weakEditCard->showError(field);\n\t} else {\n\t\t// We cancelled card edit already.\n\t\t//showEditCard(native, field);\n\t\t//if (_weakEditCard\n\t\t//\t&& field == CardField::AddressCountry) {\n\t\t//\t_weakEditCard->showError(field);\n\t\t//}\n\t}\n}\n\nvoid Panel::setTitle(rpl::producer<QString> title) {\n\tusing namespace rpl::mappers;\n\tif (_testMode) {\n\t\t_widget->setTitle(std::move(title) | rpl::map(_1 + \" (Test)\"));\n\t} else {\n\t\t_widget->setTitle(std::move(title));\n\t}\n}\n\nrpl::producer<> Panel::backRequests() const {\n\treturn _widget->backRequests();\n}\n\nrpl::producer<QString> Panel::savedMethodChosen() const {\n\treturn _savedMethodChosen.events();\n}\n\nvoid Panel::showBox(object_ptr<BoxContent> box) {\n\tif (const auto widget = _webview ? _webview->window.widget() : nullptr) {\n\t\tconst auto hideNow = !widget->isHidden();\n\t\tif (hideNow || _webview->lastHidingBox) {\n\t\t\tconst auto raw = _webview->lastHidingBox = box.data();\n\t\t\tbox->boxClosing(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tconst auto widget = _webview\n\t\t\t\t\t? _webview->window.widget()\n\t\t\t\t\t: nullptr;\n\t\t\t\tif (widget\n\t\t\t\t\t&& widget->isHidden()\n\t\t\t\t\t&& _webview->lastHidingBox == raw) {\n\t\t\t\t\twidget->show();\n\t\t\t\t}\n\t\t\t}, _webview->lifetime);\n\t\t\tif (hideNow) {\n\t\t\t\twidget->hide();\n\t\t\t}\n\t\t}\n\t}\n\t_widget->showBox(\n\t\tstd::move(box),\n\t\tLayerOption::KeepOther,\n\t\tanim::type::normal);\n}\n\nvoid Panel::showToast(TextWithEntities &&text) {\n\t_widget->showToast(std::move(text));\n}\n\nvoid Panel::showCriticalError(const TextWithEntities &text) {\n\t_progress = nullptr;\n\t_webviewProgress = false;\n\tif (!_weakFormSummary || !_weakFormSummary->showCriticalError(text)) {\n\t\tauto wrap = base::make_unique_q<RpWidget>(_widget.get());\n\t\tconst auto raw = wrap.get();\n\n\t\tconst auto error = CreateChild<PaddingWrap<FlatLabel>>(\n\t\t\traw,\n\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\traw,\n\t\t\t\trpl::single(text),\n\t\t\t\tst::paymentsCriticalError),\n\t\t\tst::paymentsCriticalErrorPadding);\n\t\terror->entity()->setClickHandlerFilter([=](\n\t\t\t\tconst ClickHandlerPtr &handler,\n\t\t\t\tQt::MouseButton) {\n\t\t\tconst auto entity = handler->getTextEntity();\n\t\t\tif (entity.type != EntityType::CustomUrl) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\t_delegate->panelOpenUrl(entity.data);\n\t\t\treturn false;\n\t\t});\n\n\t\traw->widthValue() | rpl::on_next([=](int width) {\n\t\t\terror->resizeToWidth(width);\n\t\t\traw->resize(width, error->height());\n\t\t}, raw->lifetime());\n\n\t\t_widget->showInner(std::move(wrap));\n\t}\n}\n\nstd::shared_ptr<Show> Panel::uiShow() {\n\treturn _widget->uiShow();\n}\n\nvoid Panel::showWebviewError(\n\t\tconst QString &text,\n\t\tconst Webview::Available &information) {\n\tshowCriticalError(TextWithEntities{ text }.append(\n\t\t\"\\n\\n\"\n\t).append(BotWebView::ErrorText(information)));\n}\n\nvoid Panel::updateThemeParams(const Webview::ThemeParams &params) {\n\tif (!_webview || !_webview->window.widget()) {\n\t\treturn;\n\t}\n\t_webview->window.updateTheme(\n\t\tparams.bodyBg,\n\t\tparams.scrollBg,\n\t\tparams.scrollBgOver,\n\t\tparams.scrollBarBg,\n\t\tparams.scrollBarBgOver);\n\t_webview->window.eval(R\"(\nif (window.TelegramGameProxy) {\n\twindow.TelegramGameProxy.receiveEvent(\n\t\t\"theme_changed\",\n\t\t{ \"theme_params\": )\" + params.json + R\"( });\n}\n)\");\n}\n\nrpl::lifetime &Panel::lifetime() {\n\treturn _widget->lifetime();\n}\n\n} // namespace Payments::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/ui/payments_panel.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"base/weak_ptr.h\"\n\nnamespace Ui {\nclass Show;\nclass RpWidget;\nclass SeparatePanel;\nclass BoxContent;\nclass Checkbox;\n} // namespace Ui\n\nnamespace Webview {\nstruct Available;\nstruct ThemeParams;\n} // namespace Webview\n\nnamespace Payments::Ui {\n\nusing namespace ::Ui;\n\nclass PanelDelegate;\nstruct Invoice;\nstruct RequestedInformation;\nstruct ShippingOptions;\nenum class InformationField;\nenum class CardField;\nclass FormSummary;\nclass EditInformation;\nclass EditCard;\nstruct PaymentMethodDetails;\nstruct PaymentMethodAdditional;\nstruct NativeMethodDetails;\n\nclass Panel final : public base::has_weak_ptr {\npublic:\n\texplicit Panel(not_null<PanelDelegate*> delegate);\n\t~Panel();\n\n\tvoid requestActivate();\n\tvoid toggleProgress(bool shown);\n\n\tvoid showForm(\n\t\tconst Invoice &invoice,\n\t\tconst RequestedInformation &current,\n\t\tconst PaymentMethodDetails &method,\n\t\tconst ShippingOptions &options);\n\tvoid updateFormThumbnail(const QImage &thumbnail);\n\tvoid showEditInformation(\n\t\tconst Invoice &invoice,\n\t\tconst RequestedInformation &current,\n\t\tInformationField field);\n\tvoid showInformationError(\n\t\tconst Invoice &invoice,\n\t\tconst RequestedInformation &current,\n\t\tInformationField field);\n\tvoid showEditPaymentMethod(const PaymentMethodDetails &method);\n\tvoid showAdditionalMethod(\n\t\tconst PaymentMethodAdditional &method,\n\t\tconst QString &provider,\n\t\tbool canSaveInformation);\n\tvoid showEditCard(const NativeMethodDetails &native, CardField field);\n\tvoid showEditCardByUrl(\n\t\tconst QString &url,\n\t\tconst QString &provider,\n\t\tbool canSaveInformation);\n\tvoid showCardError(const NativeMethodDetails &native, CardField field);\n\tvoid chooseShippingOption(const ShippingOptions &options);\n\tvoid chooseTips(const Invoice &invoice);\n\tvoid choosePaymentMethod(const PaymentMethodDetails &method);\n\tvoid askSetPassword();\n\tvoid showCloseConfirm();\n\tvoid showWarning(const QString &bot, const QString &provider);\n\tvoid requestTermsAcceptance(\n\t\tconst QString &username,\n\t\tconst QString &url,\n\t\tbool recurring);\n\n\tbool showWebview(\n\t\tconst QString &url,\n\t\tbool allowBack,\n\t\trpl::producer<QString> bottomText);\n\tvoid updateThemeParams(const Webview::ThemeParams &params);\n\n\t[[nodiscard]] rpl::producer<> backRequests() const;\n\t[[nodiscard]] rpl::producer<QString> savedMethodChosen() const;\n\n\tvoid showBox(object_ptr<Ui::BoxContent> box);\n\tvoid showToast(TextWithEntities &&text);\n\tvoid showCriticalError(const TextWithEntities &text);\n\t[[nodiscard]] std::shared_ptr<Show> uiShow();\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tstruct Progress;\n\tstruct WebviewWithLifetime;\n\n\tbool createWebview(const Webview::ThemeParams &params);\n\tvoid showWebviewProgress();\n\tvoid hideWebviewProgress();\n\tvoid showWebviewError(\n\t\tconst QString &text,\n\t\tconst Webview::Available &information);\n\tvoid setTitle(rpl::producer<QString> title);\n\n\t[[nodiscard]] bool progressWithBackground() const;\n\t[[nodiscard]] QRect progressRect() const;\n\tvoid setupProgressGeometry();\n\tvoid updateFooterHeight();\n\n\tconst not_null<PanelDelegate*> _delegate;\n\tstd::unique_ptr<SeparatePanel> _widget;\n\tstd::unique_ptr<WebviewWithLifetime> _webview;\n\tstd::unique_ptr<RpWidget> _webviewBottom;\n\trpl::variable<int> _footerHeight;\n\tstd::unique_ptr<Progress> _progress;\n\tQPointer<Checkbox> _saveWebviewInformation;\n\tQPointer<FormSummary> _weakFormSummary;\n\trpl::variable<int> _formScrollTop;\n\tQPointer<EditInformation> _weakEditInformation;\n\tQPointer<EditCard> _weakEditCard;\n\trpl::event_stream<QString> _savedMethodChosen;\n\tbool _themeUpdateScheduled = false;\n\tbool _webviewProgress = false;\n\tbool _testMode = false;\n\n};\n\n} // namespace Payments::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/ui/payments_panel_data.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/text/text_entity.h\"\n\nnamespace Payments::Ui {\n\nstruct LabeledPrice {\n\tQString label;\n\tint64 price = 0;\n};\n\nstruct Cover {\n\tQString title;\n\tTextWithEntities description;\n\tQString seller;\n\tQImage thumbnail;\n};\n\nstruct Receipt {\n\tTimeId date = 0;\n\tint64 totalAmount = 0;\n\tQString currency;\n\tbool paid = false;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn !paid;\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n};\n\nstruct Invoice {\n\tCover cover;\n\n\tstd::vector<LabeledPrice> prices;\n\tstd::vector<int64> suggestedTips;\n\tint64 tipsMax = 0;\n\tint64 tipsSelected = 0;\n\tQString currency;\n\tReceipt receipt;\n\n\tbool isNameRequested = false;\n\tbool isPhoneRequested = false;\n\tbool isEmailRequested = false;\n\tbool isShippingAddressRequested = false;\n\tbool isRecurring = false;\n\tbool isFlexible = false;\n\tbool isTest = false;\n\n\tQString provider;\n\tQString termsUrl;\n\tbool phoneSentToProvider = false;\n\tbool emailSentToProvider = false;\n\n\t[[nodiscard]] bool valid() const {\n\t\treturn !currency.isEmpty() && (!prices.empty() || tipsMax);\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn valid();\n\t}\n};\n\nstruct ShippingOption {\n\tQString id;\n\tQString title;\n\tstd::vector<LabeledPrice> prices;\n};\n\nstruct ShippingOptions {\n\tQString currency;\n\tstd::vector<ShippingOption> list;\n\tQString selectedId;\n};\n\nstruct Address {\n\tQString address1;\n\tQString address2;\n\tQString city;\n\tQString state;\n\tQString countryIso2;\n\tQString postcode;\n\n\t[[nodiscard]] bool valid() const {\n\t\treturn !address1.isEmpty()\n\t\t\t&& !city.isEmpty()\n\t\t\t&& !countryIso2.isEmpty();\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn valid();\n\t}\n\n\tinline bool operator==(const Address &other) const {\n\t\treturn (address1 == other.address1)\n\t\t\t&& (address2 == other.address2)\n\t\t\t&& (city == other.city)\n\t\t\t&& (state == other.state)\n\t\t\t&& (countryIso2 == other.countryIso2)\n\t\t\t&& (postcode == other.postcode);\n\t}\n\tinline bool operator!=(const Address &other) const {\n\t\treturn !(*this == other);\n\t}\n};\n\nstruct RequestedInformation {\n\tQString defaultPhone;\n\tQString defaultCountry;\n\tbool save = true;\n\n\tQString name;\n\tQString phone;\n\tQString email;\n\tAddress shippingAddress;\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn name.isEmpty()\n\t\t\t&& phone.isEmpty()\n\t\t\t&& email.isEmpty()\n\t\t\t&& !shippingAddress;\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n\n\tinline bool operator==(const RequestedInformation &other) const {\n\t\treturn (name == other.name)\n\t\t\t&& (phone == other.phone)\n\t\t\t&& (email == other.email)\n\t\t\t&& (shippingAddress == other.shippingAddress);\n\t}\n\tinline bool operator!=(const RequestedInformation &other) const {\n\t\treturn !(*this == other);\n\t}\n};\n\nenum class InformationField {\n\tShippingStreet,\n\tShippingCity,\n\tShippingState,\n\tShippingCountry,\n\tShippingPostcode,\n\tName,\n\tEmail,\n\tPhone,\n};\n\nstruct NativeMethodDetails {\n\tQString defaultCountry;\n\n\tbool supported = false;\n\tbool needCountry = false;\n\tbool needZip = false;\n\tbool needCardholderName = false;\n\tbool canSaveInformation = false;\n};\n\nstruct PaymentMethodAdditional {\n\tQString title;\n\tQString url;\n};\n\nstruct PaymentMethodSaved {\n\tQString id;\n\tQString title;\n};\n\nstruct PaymentMethodDetails {\n\tNativeMethodDetails native;\n\tstd::vector<PaymentMethodSaved> savedMethods;\n\tstd::vector<PaymentMethodAdditional> additionalMethods;\n\tQString url;\n\tQString provider;\n\tint savedMethodIndex = 0;\n\tbool canSaveInformation = false;\n};\n\nenum class CardField {\n\tNumber,\n\tCvc,\n\tExpireDate,\n\tName,\n\tAddressCountry,\n\tAddressZip,\n};\n\nstruct UncheckedCardDetails {\n\tQString number;\n\tQString cvc;\n\tuint32 expireYear = 0;\n\tuint32 expireMonth = 0;\n\tQString cardholderName;\n\tQString addressCountry;\n\tQString addressZip;\n};\n\n} // namespace Payments::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/ui/payments_panel_delegate.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nclass QJsonDocument;\nclass QString;\n\nnamespace Ui {\nclass BoxContent;\n} // namespace Ui\n\nnamespace Webview {\nstruct ThemeParams;\nstruct StorageId;\n} // namespace Webview\n\nnamespace Payments::Ui {\n\nusing namespace ::Ui;\n\nstruct RequestedInformation;\nstruct UncheckedCardDetails;\n\nclass PanelDelegate {\npublic:\n\tvirtual void panelRequestClose() = 0;\n\tvirtual void panelCloseSure() = 0;\n\tvirtual void panelSubmit() = 0;\n\tvirtual void panelTrustAndSubmit() = 0;\n\tvirtual void panelAcceptTermsAndSubmit() = 0;\n\tvirtual void panelWebviewMessage(\n\t\tconst QJsonDocument &message,\n\t\tbool saveInformation) = 0;\n\tvirtual bool panelWebviewNavigationAttempt(const QString &uri) = 0;\n\tvirtual void panelSetPassword() = 0;\n\tvirtual void panelOpenUrl(const QString &url) = 0;\n\n\tvirtual void panelCancelEdit() = 0;\n\tvirtual void panelEditPaymentMethod() = 0;\n\tvirtual void panelEditShippingInformation() = 0;\n\tvirtual void panelEditName() = 0;\n\tvirtual void panelEditEmail() = 0;\n\tvirtual void panelEditPhone() = 0;\n\tvirtual void panelChooseShippingOption() = 0;\n\tvirtual void panelChangeShippingOption(const QString &id) = 0;\n\tvirtual void panelChooseTips() = 0;\n\tvirtual void panelChangeTips(int64 value) = 0;\n\n\tvirtual void panelValidateInformation(RequestedInformation data) = 0;\n\tvirtual void panelValidateCard(\n\t\tUi::UncheckedCardDetails data,\n\t\tbool saveInformation) = 0;\n\tvirtual void panelShowBox(object_ptr<BoxContent> box) = 0;\n\tvirtual QVariant panelClickHandlerContext() = 0;\n\n\tvirtual Webview::StorageId panelWebviewStorageId() = 0;\n\tvirtual Webview::ThemeParams panelWebviewThemeParams() = 0;\n\n\tvirtual std::optional<QDate> panelOverrideExpireDateThreshold() = 0;\n};\n\n} // namespace Payments::Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"payments/ui/payments_reaction_box.h\"\n\n#include \"base/qt/qt_compare.h\"\n#include \"calls/group/ui/calls_group_stars_coloring.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/boxes/boost_box.h\" // MakeBoostFeaturesBadge.\n#include \"ui/controls/who_reacted_context_action.h\"\n#include \"ui/effects/premium_bubble.h\"\n#include \"ui/effects/ministar_particles.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/color_int_conversion.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_info_levels.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_media_player.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\n[[nodiscard]] not_null<Ui::RpWidget*> AddBalanceWidget(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Main::Session*> session,\n\trpl::producer<CreditsAmount> balanceValue,\n\tbool rightAlign,\n\trpl::producer<float64> opacityValue = nullptr,\n\tbool dark = false);\n} // namespace Settings\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kMaxTopPaidShown = 3;\n\nstruct TopReactorKey {\n\tstd::shared_ptr<DynamicImage> photo;\n\tint count = 0;\n\tQString name;\n\n\tfriend inline auto operator<=>(\n\t\tconst TopReactorKey &,\n\t\tconst TopReactorKey &) = default;\n\tfriend inline bool operator==(\n\t\tconst TopReactorKey &,\n\t\tconst TopReactorKey &) = default;\n};\n\n[[nodiscard]] QImage GenerateBadgeImage(\n\t\tconst std::vector<Calls::Group::Ui::StarsColoring> &colorings,\n\t\tint count,\n\t\tbool videoStream) {\n\treturn GenerateSmallBadgeImage(\n\t\tLang::FormatCountDecimal(count),\n\t\tst::paidReactTopStarIcon,\n\t\t(videoStream\n\t\t\t? Ui::ColorFromSerialized(\n\t\t\t\tCalls::Group::Ui::StarsColoringForCount(\n\t\t\t\t\tcolorings,\n\t\t\t\t\tcount\n\t\t\t\t).bgLight)\n\t\t\t: st::creditsBg3->c),\n\t\tvideoStream ? st::white->c : st::premiumButtonFg->c,\n\t\tvideoStream ? &st::groupCallTopReactorBadge : nullptr);\n}\n\nvoid AddArrowDown(not_null<RpWidget*> widget) {\n\tconst auto arrow = CreateChild<RpWidget>(widget);\n\tconst auto icon = &st::paidReactChannelArrow;\n\tconst auto skip = st::lineWidth * 4;\n\tconst auto size = icon->width() + skip * 2;\n\tarrow->resize(size, size);\n\twidget->widthValue() | rpl::on_next([=](int width) {\n\t\tconst auto left = (width - st::paidReactTopUserpic) / 2;\n\t\tarrow->moveToRight(left - skip, -st::lineWidth, width);\n\t}, widget->lifetime());\n\tarrow->paintRequest() | rpl::on_next([=] {\n\t\tPainter p(arrow);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setBrush(st::activeButtonBg);\n\t\tp.setPen(st::activeButtonFg);\n\t\tconst auto rect = arrow->rect();\n\t\tconst auto line = st::lineWidth;\n\t\tp.drawEllipse(rect.marginsRemoved({ line, line, line, line }));\n\t\ticon->paint(p, skip, (size - icon->height()) / 2 + line, size);\n\t}, widget->lifetime());\n\tarrow->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tarrow->show();\n}\n\n[[nodiscard]] not_null<RpWidget*> MakeTopReactor(\n\t\tnot_null<QWidget*> parent,\n\t\tconst PaidReactionTop &data,\n\t\tint place,\n\t\tconst std::vector<Calls::Group::Ui::StarsColoring> &colorings,\n\t\tFn<void()> selectShownPeer,\n\t\tbool videoStream) {\n\tauto top = 0;\n\tauto height = st::paidReactTopNameSkip + st::normalFont->height;\n\tif (videoStream) {\n\t\ttop += st::paidReactCrownSkip;\n\t\theight += top;\n\t}\n\n\tconst auto result = CreateChild<AbstractButton>(parent);\n\tresult->resize(0, height);\n\tresult->show();\n\tif (data.click && !data.my) {\n\t\tresult->setClickedCallback(data.click);\n\t} else if (data.click && selectShownPeer) {\n\t\tresult->setClickedCallback(selectShownPeer);\n\t\tAddArrowDown(result);\n\t} else {\n\t\tresult->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t}\n\n\tstruct State {\n\t\tQImage badge;\n\t\tText::String name;\n\t};\n\tconst auto state = result->lifetime().make_state<State>();\n\tstate->name.setText(st::defaultTextStyle, data.name);\n\n\tconst auto count = data.count;\n\tconst auto photo = data.photo->clone();\n\tphoto->subscribeToUpdates([=] {\n\t\tresult->update();\n\t});\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tstate->badge = QImage();\n\t}, result->lifetime());\n\tresult->paintRequest() | rpl::on_next([=] {\n\t\tauto p = Painter(result);\n\t\tconst auto left = (result->width() - st::paidReactTopUserpic) / 2;\n\t\tp.drawImage(left, top, photo->image(st::paidReactTopUserpic));\n\n\t\tif (state->badge.isNull()) {\n\t\t\tstate->badge = GenerateBadgeImage(colorings, count, videoStream);\n\t\t}\n\t\tconst auto bwidth = state->badge.width()\n\t\t\t/ state->badge.devicePixelRatio();\n\t\tp.drawImage(\n\t\t\t(result->width() - bwidth) / 2,\n\t\t\ttop + st::paidReactTopBadgeSkip,\n\t\t\tstate->badge);\n\n\t\tif (videoStream) {\n\t\t\tconst auto bg = Calls::Group::Ui::StarsColoringForCount(\n\t\t\t\tcolorings,\n\t\t\t\tcount\n\t\t\t).bgLight;\n\t\t\tconst auto &icon = st::paidReactCrown;\n\t\t\tconst auto left = (result->width() - icon.width()) / 2;\n\t\t\tconst auto shift = st::paidReactCrownOutline;\n\t\t\tconst auto outline = st::groupCallMembersBg->c;\n\t\t\ticon.paint(p, left - shift, shift, result->width(), outline);\n\t\t\ticon.paint(p, left + shift, shift, result->width(), outline);\n\t\t\ticon.paint(p, left, 0, result->width(), bg);\n\n\t\t\tconst auto top = st::paidReactCrownTop;\n\t\t\tp.setPen(st::white);\n\t\t\tp.setFont(st::levelStyle.font);\n\t\t\tp.drawText(\n\t\t\t\tQRect(left, top, icon.width(), icon.height()),\n\t\t\t\tQString::number(place),\n\t\t\t\tstyle::al_top);\n\n\t\t\tp.setPen(st::groupCallMembersFg);\n\t\t} else {\n\t\t\tp.setPen(st::windowFg);\n\t\t}\n\t\tconst auto skip = st::normalFont->spacew;\n\t\tconst auto nameTop = top + st::paidReactTopNameSkip;\n\t\tconst auto available = result->width() - skip * 2;\n\t\tstate->name.draw(p, skip, nameTop, available, style::al_top);\n\t}, result->lifetime());\n\n\treturn result;\n}\n\nvoid SelectShownPeer(\n\t\tstd::shared_ptr<base::weak_qptr<PopupMenu>> menu,\n\t\tnot_null<QWidget*> parent,\n\t\tconst std::vector<PaidReactionTop> &mine,\n\t\tuint64 selected,\n\t\tFn<void(uint64)> callback) {\n\tif (*menu) {\n\t\t(*menu)->hideMenu();\n\t}\n\t(*menu) = CreateChild<PopupMenu>(\n\t\tparent,\n\t\tst::paidReactChannelMenu);\n\n\tstruct Entry {\n\t\tnot_null<Ui::WhoReactedEntryAction*> action;\n\t\tstd::shared_ptr<Ui::DynamicImage> userpic;\n\t};\n\tauto actions = std::make_shared<std::vector<Entry>>();\n\tactions->reserve(mine.size());\n\tfor (const auto &entry : mine) {\n\t\tauto action = base::make_unique_q<WhoReactedEntryAction>(\n\t\t\t(*menu)->menu(),\n\t\t\tnullptr,\n\t\t\t(*menu)->menu()->st(),\n\t\t\tUi::WhoReactedEntryData());\n\t\tconst auto index = int(actions->size());\n\t\tactions->push_back({ action.get(), entry.photo->clone() });\n\t\tconst auto id = entry.barePeerId;\n\t\tconst auto updateUserpic = [=] {\n\t\t\tconst auto size = st::defaultWhoRead.photoSize;\n\t\t\tactions->at(index).action->setData({\n\t\t\t\t.text = entry.name,\n\t\t\t\t.type = ((id == selected)\n\t\t\t\t\t? Ui::WhoReactedType::RefRecipientNow\n\t\t\t\t\t: Ui::WhoReactedType::RefRecipient),\n\t\t\t\t.userpic = actions->at(index).userpic->image(size),\n\t\t\t\t.callback = [=] { callback(id); },\n\t\t\t});\n\t\t};\n\t\tactions->back().userpic->subscribeToUpdates(updateUserpic);\n\n\t\t(*menu)->addAction(std::move(action));\n\t\tupdateUserpic();\n\t}\n\t(*menu)->popup(QCursor::pos());\n}\n\nvoid FillTopReactors(\n\t\tnot_null<VerticalLayout*> container,\n\t\tstd::vector<PaidReactionTop> top,\n\t\tconst std::vector<Calls::Group::Ui::StarsColoring> &colorings,\n\t\trpl::producer<int> chosen,\n\t\trpl::producer<uint64> shownPeer,\n\t\tFn<void(uint64)> changeShownPeer,\n\t\tbool videoStream) {\n\tconst auto badge = videoStream\n\t\t? nullptr\n\t\t: container->add(\n\t\t\tobject_ptr<SlideWrap<RpWidget>>(\n\t\t\t\tcontainer,\n\t\t\t\tMakeBoostFeaturesBadge(\n\t\t\t\t\tcontainer,\n\t\t\t\t\ttr::lng_paid_react_top_title(),\n\t\t\t\t\t[](QRect) { return st::creditsBg3->b; }),\n\t\t\t\tst::paidReactTopTitleMargin),\n\t\t\tst::boxRowPadding,\n\t\t\tstyle::al_top);\n\tconst auto wrap = container->add(\n\t\tobject_ptr<SlideWrap<FixedHeightWidget>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<FixedHeightWidget>(container, 0),\n\t\t\tst::paidReactTopMargin));\n\tconst auto parent = wrap->entity();\n\tusing Key = TopReactorKey;\n\tstruct State {\n\t\tbase::flat_map<Key, not_null<RpWidget*>> cache;\n\t\tstd::vector<not_null<RpWidget*>> widgets;\n\t\trpl::event_stream<> updated;\n\t\tstd::optional<int> initialChosen;\n\t\tbool chosenChanged = false;\n\t};\n\tconst auto state = wrap->lifetime().make_state<State>();\n\tconst auto menu = std::make_shared<base::weak_qptr<Ui::PopupMenu>>();\n\n\trpl::combine(\n\t\tstd::move(chosen),\n\t\tstd::move(shownPeer)\n\t) | rpl::on_next([=](int chosen, uint64 barePeerId) {\n\t\tif (!state->initialChosen) {\n\t\t\tstate->initialChosen = chosen;\n\t\t} else if (*state->initialChosen != chosen) {\n\t\t\tstate->chosenChanged = true;\n\t\t}\n\t\tauto mine = std::vector<PaidReactionTop>();\n\t\tauto list = std::vector<PaidReactionTop>();\n\t\tlist.reserve(kMaxTopPaidShown + 1);\n\t\tfor (const auto &entry : top) {\n\t\t\tif (!entry.my) {\n\t\t\t\tlist.push_back(entry);\n\t\t\t} else if (entry.barePeerId == barePeerId) {\n\t\t\t\tauto copy = entry;\n\t\t\t\tif (state->chosenChanged) {\n\t\t\t\t\tcopy.count += chosen;\n\t\t\t\t}\n\t\t\t\tlist.push_back(copy);\n\t\t\t}\n\t\t\tif (entry.my && entry.barePeerId) {\n\t\t\t\tmine.push_back(entry);\n\t\t\t}\n\t\t}\n\t\tranges::stable_sort(\n\t\t\tlist,\n\t\t\tranges::greater(),\n\t\t\t&PaidReactionTop::count);\n\t\twhile (list.size() > kMaxTopPaidShown\n\t\t\t|| (!list.empty() && !list.back().count)) {\n\t\t\tlist.pop_back();\n\t\t}\n\t\tauto selectShownPeer = (mine.size() < 2)\n\t\t\t? Fn<void()>()\n\t\t\t: [=] { SelectShownPeer(\n\t\t\t\tmenu,\n\t\t\t\tparent,\n\t\t\t\tmine,\n\t\t\t\tbarePeerId,\n\t\t\t\tchangeShownPeer); };\n\t\tif (list.empty()) {\n\t\t\tif (badge) {\n\t\t\t\tbadge->hide(anim::type::normal);\n\t\t\t}\n\t\t\twrap->hide(anim::type::normal);\n\t\t} else {\n\t\t\tif (badge) {\n\t\t\t\tbadge->show(anim::type::normal);\n\t\t\t}\n\t\t\tfor (const auto &widget : state->widgets) {\n\t\t\t\twidget->hide();\n\t\t\t}\n\t\t\tstate->widgets.clear();\n\t\t\tauto index = 0;\n\t\t\tfor (const auto &entry : list) {\n\t\t\t\tconst auto key = Key{\n\t\t\t\t\t.photo = entry.photo,\n\t\t\t\t\t.count = entry.count,\n\t\t\t\t\t.name = entry.name,\n\t\t\t\t};\n\t\t\t\tconst auto i = state->cache.find(key);\n\t\t\t\tconst auto widget = (i != end(state->cache))\n\t\t\t\t\t? i->second\n\t\t\t\t\t: MakeTopReactor(\n\t\t\t\t\t\tparent,\n\t\t\t\t\t\tentry,\n\t\t\t\t\t\t++index,\n\t\t\t\t\t\tcolorings,\n\t\t\t\t\t\tselectShownPeer,\n\t\t\t\t\t\tvideoStream);\n\t\t\t\tstate->widgets.push_back(widget);\n\t\t\t\twidget->show();\n\t\t\t}\n\t\t\tfor (const auto &[k, widget] : state->cache) {\n\t\t\t\tif (widget->isHidden()) {\n\t\t\t\t\tdelete widget;\n\t\t\t\t}\n\t\t\t}\n\t\t\twrap->show(anim::type::normal);\n\t\t}\n\n\t\tstate->updated.fire({});\n\t}, wrap->lifetime());\n\tif (badge) {\n\t\tbadge->finishAnimating();\n\t}\n\twrap->finishAnimating();\n\n\trpl::combine(\n\t\tstate->updated.events_starting_with({}),\n\t\twrap->widthValue()\n\t) | rpl::on_next([=](auto, int width) {\n\t\tif (!state->widgets.empty()) {\n\t\t\tparent->resize(parent->width(), state->widgets.back()->height());\n\t\t}\n\t\tconst auto single = width / 4;\n\t\tif (single <= st::paidReactTopUserpic) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto count = int(state->widgets.size());\n\t\tauto left = (width - single * count) / 2;\n\t\tfor (const auto &widget : state->widgets) {\n\t\t\twidget->setGeometry(left, 0, single, widget->height());\n\t\t\tleft += single;\n\t\t}\n\t}, wrap->lifetime());\n}\n\n[[nodiscard]] not_null<RpWidget*> MakeStarSelectInfoBlock(\n\t\tnot_null<RpWidget*> parent,\n\t\trpl::producer<TextWithEntities> title,\n\t\trpl::producer<QString> subtext,\n\t\tFn<void()> click,\n\t\tText::MarkedContext context,\n\t\tbool dark) {\n\tconst auto result = CreateChild<AbstractButton>(parent);\n\n\tconst auto titleHeight = st::starSelectInfoTitle.style.font->height;\n\tconst auto subtextHeight = st::starSelectInfoSubtext.style.font->height;\n\tconst auto height = titleHeight + subtextHeight;\n\n\tresult->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(result);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(dark ? st::groupCallMembersBgOver : st::windowBgOver);\n\t\tconst auto radius = st::boxRadius;\n\t\tp.drawRoundedRect(result->rect(), radius, radius);\n\t}, result->lifetime());\n\n\tresult->resize(\n\t\tresult->width(),\n\t\tQSize(height, height).grownBy(st::starSelectInfoPadding).height());\n\n\tconst auto titleLabel = CreateChild<FlatLabel>(\n\t\tresult,\n\t\tstd::move(title),\n\t\tdark ? st::videoStreamInfoTitle : st::starSelectInfoTitle,\n\t\tst::defaultPopupMenu,\n\t\tcontext);\n\tconst auto subtextLabel = CreateChild<FlatLabel>(\n\t\tresult,\n\t\tstd::move(subtext),\n\t\tdark ? st::videoStreamInfoSubtext : st::starSelectInfoSubtext);\n\n\tif (click) {\n\t\tresult->setClickedCallback(std::move(click));\n\t\ttitleLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tsubtextLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t} else {\n\t\tresult->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t}\n\n\trpl::combine(\n\t\tresult->widthValue(),\n\t\ttitleLabel->widthValue(),\n\t\tsubtextLabel->widthValue()\n\t) | rpl::on_next([=](int width, int titlew, int subtextw) {\n\t\tconst auto padding = st::starSelectInfoPadding;\n\t\ttitleLabel->moveToLeft((width - titlew) / 2, padding.top(), width);\n\t\tsubtextLabel->moveToLeft(\n\t\t\t(width - subtextw) / 2,\n\t\t\tpadding.top() + titleHeight,\n\t\t\twidth);\n\t}, result->lifetime());\n\n\treturn result;\n}\n\n} // namespace\n\nvoid PaidReactionsBox(\n\t\tnot_null<GenericBox*> box,\n\t\tPaidReactionBoxArgs &&args) {\n\tExpects(!args.top.empty());\n\n\tconst auto dark = args.dark;\n\targs.min = std::max(args.min, 1);\n\targs.max = std::max({\n\t\targs.min + 1,\n\t\targs.max,\n\t\targs.explicitlyAllowed,\n\t\targs.chosen,\n\t});\n\n\tconst auto allowed = args.explicitlyAllowed;\n\targs.chosen = (allowed && args.chosen == allowed)\n\t\t? allowed\n\t\t: std::clamp(args.chosen, args.min, args.max);\n\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setStyle(dark ? st::darkEditStarsBox : st::paidReactBox);\n\tbox->setNoContentMargin(true);\n\n\tstruct State {\n\t\trpl::variable<int> chosen;\n\t\trpl::variable<uint64> shownPeer;\n\t\tuint64 savedShownPeer = 0;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\n\tstate->chosen = args.chosen;\n\tconst auto changed = [=](int count) {\n\t\tstate->chosen = count;\n\t};\n\n\tconst auto colorings = args.colorings;\n\tconst auto videoStreamChoosing = args.videoStreamChoosing;\n\tconst auto videoStreamSending = args.videoStreamSending;\n\tconst auto videoStreamAdmin = args.videoStreamAdmin;\n\tconst auto videoStream = videoStreamChoosing || videoStreamSending;\n\tconst auto initialShownPeer = ranges::find(\n\t\targs.top,\n\t\ttrue,\n\t\t&PaidReactionTop::my\n\t)->barePeerId;\n\tstate->shownPeer = initialShownPeer;\n\tstate->savedShownPeer = ranges::find_if(args.top, [](\n\t\t\tconst PaidReactionTop &entry) {\n\t\treturn entry.my && entry.barePeerId != 0;\n\t})->barePeerId;\n\n\tconst auto content = box->verticalLayout();\n\tAddSkip(content, st::boxTitleClose.height + st::paidReactBubbleTop);\n\n\tconst auto activeFgOverride = [=](int count) {\n\t\tconst auto coloring = Calls::Group::Ui::StarsColoringForCount(\n\t\t\tcolorings,\n\t\t\tcount);\n\t\treturn Ui::ColorFromSerialized(coloring.bgLight);\n\t};\n\tAddStarSelectBubble(\n\t\tcontent,\n\t\tBoxShowFinishes(box),\n\t\tstate->chosen.value(),\n\t\targs.max,\n\t\tvideoStream ? activeFgOverride : Fn<QColor(int)>());\n\n\tconst auto already = ranges::find(\n\t\targs.top,\n\t\ttrue,\n\t\t&PaidReactionTop::my)->count;\n\tPaidReactionSlider(\n\t\tcontent,\n\t\t(dark ? st::darkEditStarsSlider : st::paidReactSlider),\n\t\targs.min,\n\t\targs.explicitlyAllowed,\n\t\trpl::single(args.chosen),\n\t\targs.max,\n\t\tchanged,\n\t\tvideoStream ? activeFgOverride : Fn<QColor(int)>());\n\n\tbox->addTopButton(\n\t\tdark ? st::darkEditStarsClose : st::boxTitleClose,\n\t\t[=] { box->closeBox(); });\n\n\tconst auto addTopReactors = [&] {\n\t\tFillTopReactors(\n\t\t\tcontent,\n\t\t\tstd::move(args.top),\n\t\t\tcolorings,\n\t\t\t(videoStreamAdmin\n\t\t\t\t? rpl::single(state->chosen.current())\n\t\t\t\t: state->chosen.value() | rpl::type_erased),\n\t\t\t(videoStreamAdmin\n\t\t\t\t? rpl::single(state->shownPeer.current())\n\t\t\t\t: state->shownPeer.value() | rpl::type_erased),\n\t\t\t[=](uint64 barePeerId) {\n\t\t\t\tstate->shownPeer = state->savedShownPeer = barePeerId;\n\t\t\t},\n\t\t\tvideoStream);\n\t};\n\n\tif (videoStreamChoosing) {\n\t\tusing namespace Calls::Group::Ui;\n\t\tbox->addRow(\n\t\t\tVideoStreamStarsLevel(box, colorings, state->chosen.value()),\n\t\t\tst::boxRowPadding + QMargins(0, st::paidReactTitleSkip, 0, 0));\n\t} else if (videoStreamSending) {\n\t\taddTopReactors();\n\t}\n\n\tbox->addRow(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tbox,\n\t\t\t(videoStreamAdmin\n\t\t\t\t? tr::lng_paid_admin_title()\n\t\t\t\t: videoStreamChoosing\n\t\t\t\t? tr::lng_paid_comment_title()\n\t\t\t\t: videoStreamSending\n\t\t\t\t? tr::lng_paid_reaction_title()\n\t\t\t\t: tr::lng_paid_react_title()),\n\t\t\tdark ? st::darkEditStarsCenteredTitle : st::boostCenteredTitle),\n\t\tst::boxRowPadding + QMargins(0, st::paidReactTitleSkip, 0, 0),\n\t\tstyle::al_top);\n\n\tconst auto labelWrap = box->addRow(\n\t\tobject_ptr<RpWidget>(box),\n\t\t(st::boxRowPadding\n\t\t\t+ QMargins(0, st::lineWidth, 0, st::boostBottomSkip)));\n\tconst auto label = CreateChild<FlatLabel>(\n\t\tlabelWrap,\n\t\t(videoStreamAdmin\n\t\t\t? tr::lng_paid_admin_about(tr::marked)\n\t\t\t: videoStream\n\t\t\t? (videoStreamChoosing\n\t\t\t\t? tr::lng_paid_comment_about\n\t\t\t\t: tr::lng_paid_reaction_about)(\n\t\t\t\t\tlt_name,\n\t\t\t\t\trpl::single(tr::bold(args.name)),\n\t\t\t\t\ttr::rich)\n\t\t\t: already\n\t\t\t? tr::lng_paid_react_already(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(already) | tr::to_count(),\n\t\t\t\ttr::rich)\n\t\t\t: tr::lng_paid_react_about(\n\t\t\t\tlt_channel,\n\t\t\t\trpl::single(tr::bold(args.name)),\n\t\t\t\ttr::rich)),\n\t\tdark ? st::darkEditStarsText : st::boostText);\n\tlabel->setTryMakeSimilarLines(true);\n\tlabelWrap->widthValue() | rpl::on_next([=](int width) {\n\t\tlabel->resizeToWidth(width);\n\t}, label->lifetime());\n\tlabel->heightValue() | rpl::on_next([=](int height) {\n\t\tconst auto min = 2 * st::normalFont->height;\n\t\tconst auto skip = std::max((min - height) / 2, 0);\n\t\tlabelWrap->resize(labelWrap->width(), 2 * skip + height);\n\t\tlabel->moveToLeft(0, skip);\n\t}, label->lifetime());\n\n\tif (!videoStream) {\n\t\taddTopReactors();\n\n\t\tconst auto skip = st::defaultCheckbox.margin.bottom();\n\t\tconst auto named = box->addRow(\n\t\t\tobject_ptr<Checkbox>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_paid_react_show_in_top(tr::now),\n\t\t\t\tstate->shownPeer.current() != 0,\n\t\t\t\tst::paidReactBoxCheckbox),\n\t\t\tst::boxRowPadding + QMargins(0, 0, 0, skip),\n\t\t\tstyle::al_top);\n\t\tnamed->checkedValue(\n\t\t) | rpl::on_next([=](bool show) {\n\t\t\tstate->shownPeer = show ? state->savedShownPeer : 0;\n\t\t}, named->lifetime());\n\t}\n\n\tAddSkip(content);\n\tAddSkip(content);\n\n\tAddDividerText(\n\t\tcontent,\n\t\t(videoStreamAdmin\n\t\t\t? tr::lng_paid_react_admin_cant(tr::marked)\n\t\t\t: tr::lng_paid_react_agree(\n\t\t\t\tlt_link,\n\t\t\t\trpl::combine(\n\t\t\t\t\ttr::lng_paid_react_agree_link(),\n\t\t\t\t\ttr::lng_group_invite_subscription_about_url()\n\t\t\t\t) | rpl::map([](const QString &text, const QString &url) {\n\t\t\t\t\treturn tr::link(text, url);\n\t\t\t\t}),\n\t\t\t\ttr::rich)),\n\t\tst::defaultBoxDividerLabelPadding,\n\t\tdark ? st::groupCallDividerLabel : st::defaultDividerLabel);\n\n\tconst auto button = box->addButton(rpl::single(QString()), [=] {\n\t\targs.send(state->chosen.current(), state->shownPeer.current());\n\t});\n\n\tbox->boxClosing() | rpl::filter([=] {\n\t\treturn state->shownPeer.current() != initialShownPeer;\n\t}) | rpl::on_next([=] {\n\t\targs.send(0, state->shownPeer.current());\n\t}, box->lifetime());\n\n\tbutton->setText(args.submit(state->chosen.value()));\n\n\tAddStarSelectBalance(\n\t\tbox,\n\t\targs.session,\n\t\tstd::move(args.balanceValue),\n\t\tdark);\n}\n\nobject_ptr<BoxContent> MakePaidReactionBox(PaidReactionBoxArgs &&args) {\n\treturn Box(PaidReactionsBox, std::move(args));\n}\n\nint MaxTopPaidDonorsShown() {\n\treturn kMaxTopPaidShown;\n}\n\nQImage GenerateSmallBadgeImage(\n\t\tQString text,\n\t\tconst style::icon &icon,\n\t\tQColor bg,\n\t\tQColor fg,\n\t\tconst style::RoundCheckbox *borderSt) {\n\tconst auto length = st::chatSimilarBadgeFont->width(text);\n\tconst auto contents = st::chatSimilarLockedIconPosition.x()\n\t\t+ icon.width()\n\t\t+ st::paidReactTopStarSkip\n\t\t+ length;\n\tconst auto badge = QRect(\n\t\tst::chatSimilarBadgePadding.left(),\n\t\tst::chatSimilarBadgePadding.top(),\n\t\tcontents,\n\t\tst::chatSimilarBadgeFont->height);\n\tconst auto rect = badge.marginsAdded(st::chatSimilarBadgePadding);\n\tconst auto add = borderSt ? borderSt->width : 0;\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto result = QImage(\n\t\t(rect + QMargins(add, add, add, add)).size() * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\tresult.fill(Qt::transparent);\n\tauto q = QPainter(&result);\n\n\tconst auto &font = st::chatSimilarBadgeFont;\n\tconst auto textTop = badge.y() + font->ascent;\n\tconst auto position = st::chatSimilarLockedIconPosition;\n\n\tauto hq = PainterHighQualityEnabler(q);\n\tq.translate(add, add);\n\tq.setBrush(bg);\n\tif (borderSt) {\n\t\tq.setPen(QPen(borderSt->border->c, borderSt->width));\n\t} else {\n\t\tq.setPen(Qt::NoPen);\n\t}\n\tconst auto radius = rect.height() / 2.;\n\tconst auto shift = add / 2.;\n\tq.drawRoundedRect(\n\t\tQRectF(rect) + QMarginsF(shift, shift, shift, shift),\n\t\tradius,\n\t\tradius);\n\n\tauto textLeft = 0;\n\ticon.paint(\n\t\tq,\n\t\tbadge.x() + position.x(),\n\t\tbadge.y() + position.y(),\n\t\trect.width());\n\ttextLeft += position.x() + icon.width() + st::paidReactTopStarSkip;\n\n\tq.setFont(font);\n\tq.setPen(fg);\n\tq.drawText(textLeft, textTop, text);\n\tq.end();\n\n\treturn result;\n}\n\nStarSelectDiscreter StarSelectDiscreterForMax(int max) {\n\tExpects(max >= 2);\n\n\t// 1/8 of width is 1..10\n\t// 1/3 of width is 1..100\n\t// 2/3 of width is 1..1000\n\n\tauto thresholds = base::flat_map<float64, int>();\n\tthresholds.emplace(0., 1);\n\tif (max <= 40) {\n\t\tthresholds.emplace(1., max);\n\t} else if (max <= 300) {\n\t\tthresholds.emplace(1. / 4, 10);\n\t\tthresholds.emplace(1., max);\n\t} else if (max <= 600) {\n\t\tthresholds.emplace(1. / 8, 10);\n\t\tthresholds.emplace(1. / 2, 100);\n\t\tthresholds.emplace(1., max);\n\t} else if (max <= 1900) {\n\t\tthresholds.emplace(1. / 8, 10);\n\t\tthresholds.emplace(1. / 3, 100);\n\t\tthresholds.emplace(1., max);\n\t} else if (max <= 10000) {\n\t\tthresholds.emplace(1. / 8, 10);\n\t\tthresholds.emplace(1. / 3, 100);\n\t\tthresholds.emplace(2. / 3, 1000);\n\t\tthresholds.emplace(1., max);\n\t} else {\n\t\tthresholds.emplace(1. / 10, 10);\n\t\tthresholds.emplace(1. / 6, 100);\n\t\tthresholds.emplace(1. / 3, 1000);\n\t\tthresholds.emplace(1., max);\n\t}\n\n\tconst auto ratioToValue = [=](float64 ratio) {\n\t\tratio = std::clamp(ratio, 0., 1.);\n\t\tconst auto j = thresholds.lower_bound(ratio);\n\t\tif (j == begin(thresholds)) {\n\t\t\treturn 1;\n\t\t}\n\t\tconst auto i = j - 1;\n\t\tconst auto progress = (ratio - i->first) / (j->first - i->first);\n\t\tconst auto value = i->second + (j->second - i->second) * progress;\n\t\treturn int(base::SafeRound(value));\n\t};\n\tconst auto valueToRatio = [=](int value) {\n\t\tvalue = std::clamp(value, 1, max);\n\t\tauto i = begin(thresholds);\n\t\tauto j = i + 1;\n\t\twhile (j->second < value) {\n\t\t\ti = j++;\n\t\t}\n\t\tconst auto progress = (value - i->second)\n\t\t\t/ float64(j->second - i->second);\n\t\treturn i->first + (j->first - i->first) * progress;\n\t};\n\treturn {\n\t\t.ratioToValue = ratioToValue,\n\t\t.valueToRatio = valueToRatio,\n\t};\n}\n\nvoid PaidReactionSlider(\n\t\tnot_null<VerticalLayout*> container,\n\t\tconst style::MediaSlider &st,\n\t\tint min,\n\t\tint explicitlyAllowed,\n\t\trpl::producer<int> current,\n\t\tint max,\n\t\tFn<void(int)> changed,\n\t\tFn<QColor(int)> activeFgOverride) {\n\tExpects(explicitlyAllowed <= max);\n\n\tif (!explicitlyAllowed) {\n\t\texplicitlyAllowed = min;\n\t}\n\tconst auto slider = container->add(\n\t\tobject_ptr<MediaSlider>(container, st),\n\t\tst::boxRowPadding + QMargins(0, st::paidReactSliderTop, 0, 0));\n\tslider->resize(slider->width(), st::paidReactSlider.seekSize.height());\n\n\tconst auto update = [=](int count) {\n\t\tif (activeFgOverride) {\n\t\t\tconst auto color = activeFgOverride(count);\n\t\t\tslider->setColorOverrides({\n\t\t\t\t.activeBg = color,\n\t\t\t\t.activeBorder = color,\n\t\t\t\t.seekFg = st::groupCallMembersFg->c,\n\t\t\t\t.seekBorder = color,\n\t\t\t\t.inactiveBorder = Qt::transparent,\n\t\t\t});\n\t\t}\n\t};\n\n\tconst auto discreter = StarSelectDiscreterForMax(max);\n\tslider->setAlwaysDisplayMarker(true);\n\tslider->setDirection(ContinuousSlider::Direction::Horizontal);\n\n\tconst auto ratioToValue = [=](float64 ratio) {\n\t\tconst auto value = discreter.ratioToValue(ratio);\n\t\treturn (value <= explicitlyAllowed && explicitlyAllowed < min)\n\t\t\t? explicitlyAllowed\n\t\t\t: std::max(value, min);\n\t};\n\n\tstd::move(current) | rpl::on_next([=](int value) {\n\t\tvalue = std::clamp(value, 1, max);\n\t\tif (discreter.ratioToValue(slider->value()) != value) {\n\t\t\tslider->setValue(discreter.valueToRatio(value));\n\t\t\tupdate(value);\n\t\t}\n\t}, slider->lifetime());\n\n\tslider->setAdjustCallback([=](float64 ratio) {\n\t\treturn discreter.valueToRatio(ratioToValue(ratio));\n\t});\n\tconst auto callback = [=](float64 ratio) {\n\t\tconst auto value = ratioToValue(ratio);\n\t\tupdate(value);\n\t\tchanged(value);\n\t};\n\tslider->setChangeProgressCallback(callback);\n\tslider->setChangeFinishedCallback(callback);\n\n\n\n\tstruct State {\n\t\tStarParticles particles = StarParticles(\n\t\t\tStarParticles::Type::Right,\n\t\t\t200,\n\t\t\tst::lineWidth * 7);\n\t\tUi::Animations::Basic animation;\n\t};\n\tconst auto state = slider->lifetime().make_state<State>();\n\n\tconst auto stars = Ui::CreateChild<Ui::RpWidget>(slider->parentWidget());\n\tstars->show();\n\tstars->raise();\n\tslider->geometryValue() | rpl::on_next([=](QRect rect) {\n\t\tstars->setGeometry(rect);\n\t}, stars->lifetime());\n\n\tstate->animation.init([=] { stars->update(); });\n\tstars->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto seekSize = st::paidReactSlider.seekSize.width();\n\tconst auto seekRadius = seekSize / 2.;\n\tstars->paintRequest() | rpl::on_next([=] {\n\t\tif (!state->animation.animating()) {\n\t\t\tstate->animation.start();\n\t\t}\n\t\tauto p = QPainter(stars);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto progress = slider->value();\n\t\tconst auto rect = stars->rect();\n\t\tconst auto availableWidth = rect.width() - seekSize;\n\t\tconst auto seekCenter = seekRadius + availableWidth * progress;\n\n\t\tstate->particles.setSpeed(.1 + progress * .3);\n\t\tstate->particles.setVisible(.25 + .65 * progress);\n\n\t\tauto fullPath = QPainterPath();\n\t\tfullPath.addRoundedRect(QRectF(rect), seekRadius, seekRadius);\n\t\tauto circlePath = QPainterPath();\n\t\tcirclePath.addEllipse(\n\t\t\tQPointF(seekCenter, rect.height() / 2.),\n\t\t\tseekRadius,\n\t\t\tseekRadius);\n\t\tauto rightRect = QPainterPath();\n\t\trightRect.addRect(\n\t\t\tQRectF(seekCenter, 0, rect.width() - seekCenter, rect.height()));\n\n\t\tp.setClipPath(fullPath.subtracted(circlePath));\n\t\tstate->particles.setColor(Qt::white);\n\t\tstate->particles.paint(p, rect, crl::now(), false);\n\t\tp.setClipping(false);\n\n\t\tp.setClipPath(fullPath.intersected(circlePath.united(rightRect)));\n\t\tstate->particles.setColor(activeFgOverride\n\t\t\t? st::groupCallMemberInactiveIcon->c\n\t\t\t: st::creditsBg3->c);\n\t\tstate->particles.paint(p, rect, crl::now(), false);\n\t}, stars->lifetime());\n}\n\nvoid AddStarSelectBalance(\n\t\tnot_null<GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<CreditsAmount> balanceValue,\n\t\tbool dark) {\n\tconst auto balance = Settings::AddBalanceWidget(\n\t\tbox->verticalLayout(),\n\t\tsession,\n\t\tstd::move(balanceValue),\n\t\tfalse,\n\t\tnullptr,\n\t\tdark);\n\trpl::combine(\n\t\tbalance->sizeValue(),\n\t\tbox->widthValue()\n\t) | rpl::on_next([=] {\n\t\tbalance->moveToLeft(\n\t\t\tst::creditsHistoryRightSkip * 2,\n\t\t\tst::creditsHistoryRightSkip);\n\t\tbalance->update();\n\t}, balance->lifetime());\n}\n\nnot_null<Premium::BubbleWidget*> AddStarSelectBubble(\n\t\tnot_null<VerticalLayout*> container,\n\t\trpl::producer<> showFinishes,\n\t\trpl::producer<int> value,\n\t\tint max,\n\t\tFn<QColor(int)> activeFgOverride) {\n\tconst auto valueToRatio = StarSelectDiscreterForMax(max).valueToRatio;\n\tauto bubbleRowState = rpl::duplicate(value) | rpl::map([=](int value) {\n\t\tconst auto full = st::boxWideWidth\n\t\t\t- st::boxRowPadding.left()\n\t\t\t- st::boxRowPadding.right();\n\t\tconst auto marker = st::paidReactSlider.seekSize.width();\n\t\tconst auto start = marker / 2;\n\t\tconst auto inner = full - marker;\n\t\tconst auto correct = start + inner * valueToRatio(value);\n\t\treturn Premium::BubbleRowState{\n\t\t\t.counter = value,\n\t\t\t.ratio = correct / full,\n\t\t};\n\t});\n\n\tconst auto bubble = Premium::AddBubbleRow(\n\t\tcontainer,\n\t\tst::boostBubble,\n\t\tstd::move(showFinishes),\n\t\tstd::move(bubbleRowState),\n\t\tPremium::BubbleType::Credits,\n\t\tnullptr,\n\t\t&st::paidReactBubbleIcon,\n\t\tst::boxRowPadding);\n\tbubble->show();\n\tif (activeFgOverride) {\n\t\tstd::move(value) | rpl::on_next([=](int count) {\n\t\t\tbubble->setBrushOverride(activeFgOverride(count));\n\t\t}, bubble->lifetime());\n\t}\n\treturn bubble;\n}\n\nobject_ptr<RpWidget> MakeStarSelectInfoBlocks(\n\t\tnot_null<RpWidget*> parent,\n\t\tstd::vector<StarSelectInfoBlock> blocks,\n\t\tText::MarkedContext context,\n\t\tbool dark) {\n\tExpects(!blocks.empty());\n\n\tauto result = object_ptr<RpWidget>(parent.get());\n\tconst auto raw = result.data();\n\n\tstruct State {\n\t\tstd::vector<not_null<RpWidget*>> blocks;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>();\n\n\tfor (auto &info : blocks) {\n\t\tstate->blocks.push_back(MakeStarSelectInfoBlock(\n\t\t\traw,\n\t\t\tstd::move(info.title),\n\t\t\tstd::move(info.subtext),\n\t\t\tstd::move(info.click),\n\t\t\tcontext,\n\t\t\tdark));\n\t}\n\traw->resize(raw->width(), state->blocks.front()->height());\n\traw->widthValue() | rpl::on_next([=](int width) {\n\t\tconst auto count = int(state->blocks.size());\n\t\tconst auto skip = (st::boxRowPadding.left() / 2);\n\t\tconst auto single = (width - skip * (count - 1)) / float64(count);\n\t\tif (single < 1.) {\n\t\t\treturn;\n\t\t}\n\t\tauto x = 0.;\n\t\tconst auto w = int(base::SafeRound(single));\n\t\tfor (const auto &block : state->blocks) {\n\t\t\tblock->resizeToWidth(w);\n\t\t\tblock->moveToLeft(int(base::SafeRound(x)), 0);\n\t\t\tx += single + skip;\n\t\t}\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/payments/ui/payments_reaction_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"calls/group/ui/calls_group_stars_coloring.h\"\n\nnamespace style {\nstruct RoundCheckbox;\nstruct MediaSlider;\n} // namespace style\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui::Premium {\nclass BubbleWidget;\n} // namespace Ui::Premium\n\nnamespace Ui {\n\nclass AbstractButton;\nclass BoxContent;\nclass GenericBox;\nclass DynamicImage;\nclass VerticalLayout;\n\nstruct PaidReactionTop {\n\tQString name;\n\tstd::shared_ptr<DynamicImage> photo;\n\tuint64 barePeerId = 0;\n\tint count = 0;\n\tFn<void()> click;\n\tbool my = false;\n};\n\nstruct PaidReactionBoxArgs {\n\tint min = 0;\n\tint explicitlyAllowed = 0;\n\tint chosen = 0;\n\tint max = 0;\n\n\tstd::vector<PaidReactionTop> top;\n\n\tnot_null<Main::Session*> session;\n\tQString name;\n\tFn<rpl::producer<TextWithEntities>(rpl::producer<int> amount)> submit;\n\tstd::vector<Calls::Group::Ui::StarsColoring> colorings;\n\trpl::producer<CreditsAmount> balanceValue;\n\tFn<void(int, uint64)> send;\n\tbool videoStreamChoosing = false;\n\tbool videoStreamSending = false;\n\tbool videoStreamAdmin = false;\n\tbool dark = false;\n};\n\nvoid PaidReactionsBox(\n\tnot_null<GenericBox*> box,\n\tPaidReactionBoxArgs &&args);\n\n[[nodiscard]] object_ptr<BoxContent> MakePaidReactionBox(\n\tPaidReactionBoxArgs &&args);\n\n[[nodiscard]] int MaxTopPaidDonorsShown();\n\n[[nodiscard]] QImage GenerateSmallBadgeImage(\n\tQString text,\n\tconst style::icon &icon,\n\tQColor bg,\n\tQColor fg,\n\tconst style::RoundCheckbox *borderSt = nullptr);\n\nstruct StarSelectDiscreter {\n\tFn<int(float64)> ratioToValue;\n\tFn<float64(int)> valueToRatio;\n};\n\n[[nodiscard]] StarSelectDiscreter StarSelectDiscreterForMax(int max);\n\nvoid PaidReactionSlider(\n\tnot_null<VerticalLayout*> container,\n\tconst style::MediaSlider &st,\n\tint min,\n\tint explicitlyAllowed,\n\trpl::producer<int> current,\n\tint max,\n\tFn<void(int)> changed,\n\tFn<QColor(int)> activeFgOverride = nullptr);\n\nvoid AddStarSelectBalance(\n\tnot_null<GenericBox*> box,\n\tnot_null<Main::Session*> session,\n\trpl::producer<CreditsAmount> balanceValue,\n\tbool dark = false);\n\nnot_null<Premium::BubbleWidget*> AddStarSelectBubble(\n\tnot_null<VerticalLayout*> container,\n\trpl::producer<> showFinishes,\n\trpl::producer<int> value,\n\tint max,\n\tFn<QColor(int)> activeFgOverride = nullptr);\n\nstruct StarSelectInfoBlock {\n\trpl::producer<TextWithEntities> title;\n\trpl::producer<QString> subtext;\n\tFn<void()> click;\n};\n[[nodiscard]] object_ptr<RpWidget> MakeStarSelectInfoBlocks(\n\tnot_null<RpWidget*> parent,\n\tstd::vector<StarSelectInfoBlock> blocks,\n\tText::MarkedContext context,\n\tbool dark = false);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/linux/current_geo_location_linux.h\"\n\n#include \"core/current_geo_location.h\"\n#include \"base/platform/linux/base_linux_library.h\"\n\n#include <gio/gio.h>\n\nnamespace Platform {\nnamespace {\n\ntypedef struct _GClueSimple GClueSimple;\ntypedef struct _GClueLocation GClueLocation;\n\ntypedef enum {\n\tGCLUE_ACCURACY_LEVEL_NONE = 0,\n\tGCLUE_ACCURACY_LEVEL_COUNTRY = 1,\n\tGCLUE_ACCURACY_LEVEL_CITY = 4,\n\tGCLUE_ACCURACY_LEVEL_NEIGHBORHOOD = 5,\n\tGCLUE_ACCURACY_LEVEL_STREET = 6,\n\tGCLUE_ACCURACY_LEVEL_EXACT = 8,\n} GClueAccuracyLevel;\n\nvoid (*gclue_simple_new)(\n\tconst char *desktop_id,\n\tGClueAccuracyLevel accuracy_level,\n\tGCancellable *cancellable,\n\tGAsyncReadyCallback callback,\n\tgpointer user_data);\n\nGClueSimple *(*gclue_simple_new_finish)(GAsyncResult *result, GError **error);\nGClueLocation *(*gclue_simple_get_location)(GClueSimple *simple);\n\ngdouble (*gclue_location_get_latitude)(GClueLocation *loc);\ngdouble (*gclue_location_get_longitude)(GClueLocation *loc);\n\n} // namespace\n\nvoid ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {\n\tstatic const auto Inited = [] {\n\t\tconst auto lib = base::Platform::LoadLibrary(\n\t\t\t\"libgeoclue-2.so.0\",\n\t\t\tRTLD_NODELETE);\n\t\treturn lib\n\t\t\t&& LOAD_LIBRARY_SYMBOL(lib, gclue_simple_new)\n\t\t\t&& LOAD_LIBRARY_SYMBOL(lib, gclue_simple_new_finish)\n\t\t\t&& LOAD_LIBRARY_SYMBOL(lib, gclue_simple_get_location)\n\t\t\t&& LOAD_LIBRARY_SYMBOL(lib, gclue_location_get_latitude)\n\t\t\t&& LOAD_LIBRARY_SYMBOL(lib, gclue_location_get_longitude);\n\t}();\n\n\tif (!Inited) {\n\t\tcallback({});\n\t\treturn;\n\t}\n\n\tgclue_simple_new(\n\t\tQGuiApplication::desktopFileName().toUtf8().constData(),\n\t\tGCLUE_ACCURACY_LEVEL_EXACT,\n\t\tnullptr,\n\t\tGAsyncReadyCallback(+[](\n\t\t\t\tGObject *object,\n\t\t\t\tGAsyncResult* res,\n\t\t\t\tFn<void(Core::GeoLocation)> *callback) {\n\t\t\tconst auto callbackGuard = gsl::finally([&] {\n\t\t\t\tdelete callback;\n\t\t\t});\n\n\t\t\tconst auto simple = gclue_simple_new_finish(res, nullptr);\n\t\t\tif (!simple) {\n\t\t\t\t(*callback)({});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst auto simpleGuard = gsl::finally([&] {\n\t\t\t\tg_object_unref(simple);\n\t\t\t});\n\n\t\t\tconst auto location = gclue_simple_get_location(simple);\n\n\t\t\t(*callback)({\n\t\t\t\t.point = {\n\t\t\t\t\tgclue_location_get_latitude(location),\n\t\t\t\t\tgclue_location_get_longitude(location),\n\t\t\t\t},\n\t\t\t\t.accuracy = Core::GeoLocationAccuracy::Exact,\n\t\t\t});\n\t\t}),\n\t\tnew Fn(callback));\n}\n\nvoid ResolveLocationAddress(\n\t\tconst Core::GeoLocation &location,\n\t\tconst QString &language,\n\t\tFn<void(Core::GeoAddress)> callback) {\n\tcallback({});\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/current_geo_location_linux.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_current_geo_location.h\"\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/file_utilities_linux.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/linux/file_utilities_linux.h\"\n\n#include \"base/platform/base_platform_info.h\"\n#include \"base/platform/linux/base_linux_xdp_utilities.h\"\n#include \"base/platform/linux/base_linux_xdg_activation_token.h\"\n#include \"base/random.h\"\n\n#include <fcntl.h>\n#include <xdpopenuri/xdpopenuri.hpp>\n#include <xdprequest/xdprequest.hpp>\n\nnamespace Platform {\nnamespace File {\nnamespace {\n\nusing namespace gi::repository;\nusing base::Platform::XdgActivationToken;\n\n} // namespace\n\nbool UnsafeShowOpenWith(const QString &filepath) {\n\tauto proxy = XdpOpenURI::OpenURIProxy::new_for_bus_sync(\n\t\tGio::BusType::SESSION_,\n\t\tGio::DBusProxyFlags::NONE_,\n\t\tbase::Platform::XDP::kService,\n\t\tbase::Platform::XDP::kObjectPath,\n\t\tnullptr);\n\n\tif (!proxy) {\n\t\treturn false;\n\t}\n\n\tauto interface = XdpOpenURI::OpenURI(proxy);\n\tif (interface.get_version() < 3) {\n\t\treturn false;\n\t}\n\n\tconst auto fd = open(\n\t\tQFile::encodeName(filepath).constData(),\n\t\tO_RDONLY | O_CLOEXEC);\n\n\tif (fd == -1) {\n\t\treturn false;\n\t}\n\n\tconst auto handleToken = \"tdesktop\"\n\t\t+ std::to_string(base::RandomValue<uint>());\n\n\tstd::string uniqueName = proxy.get_connection().get_unique_name();\n\tuniqueName.erase(0, 1);\n\tuniqueName.replace(uniqueName.find('.'), 1, 1, '_');\n\n\tauto request = XdpRequest::Request(\n\t\tXdpRequest::RequestProxy::new_sync(\n\t\t\tproxy.get_connection(),\n\t\t\tGio::DBusProxyFlags::NONE_,\n\t\t\tbase::Platform::XDP::kService,\n\t\t\tbase::Platform::XDP::kObjectPath\n\t\t\t\t+ std::string(\"/request/\")\n\t\t\t\t+ uniqueName\n\t\t\t\t+ '/'\n\t\t\t\t+ handleToken,\n\t\t\tnullptr,\n\t\t\tnullptr));\n\n\tif (!request) {\n\t\tclose(fd);\n\t\treturn false;\n\t}\n\n\tauto loop = GLib::MainLoop::new_();\n\n\tconst auto signalId = request.signal_response().connect([=](\n\t\t\tXdpRequest::Request,\n\t\t\tguint,\n\t\t\tGLib::Variant) mutable {\n\t\tloop.quit();\n\t});\n\n\tconst auto signalGuard = gsl::finally([&] {\n\t\trequest.disconnect(signalId);\n\t});\n\n\tauto result = interface.call_open_file_sync(\n\t\tbase::Platform::XDP::ParentWindowID(),\n\t\tGLib::Variant::new_handle(0),\n\t\tGLib::Variant::new_array({\n\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\tGLib::Variant::new_string(\"handle_token\"),\n\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\tGLib::Variant::new_string(handleToken))),\n\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\tGLib::Variant::new_string(\"activation_token\"),\n\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\tGLib::Variant::new_string(\n\t\t\t\t\t\tXdgActivationToken().toStdString()))),\n\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\tGLib::Variant::new_string(\"ask\"),\n\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\tGLib::Variant::new_boolean(true))),\n\t\t}),\n\t\tGio::UnixFDList::new_from_array(&fd, 1),\n\t\tnullptr);\n\n\tif (!result) {\n\t\treturn false;\n\t}\n\n\tQWidget window;\n\twindow.setAttribute(Qt::WA_DontShowOnScreen);\n\twindow.setWindowModality(Qt::ApplicationModal);\n\twindow.show();\n\tloop.run();\n\n\treturn true;\n}\n\n} // namespace File\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/file_utilities_linux.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_file_utilities.h\"\n\nnamespace Platform {\nnamespace File {\n\ninline QString UrlToLocal(const QUrl &url) {\n\treturn ::File::internal::UrlToLocalDefault(url);\n}\n\ninline void UnsafeOpenUrl(const QString &url) {\n\treturn ::File::internal::UnsafeOpenUrlDefault(url);\n}\n\ninline void UnsafeOpenEmailLink(const QString &email) {\n\treturn ::File::internal::UnsafeOpenEmailLinkDefault(email);\n}\n\ninline bool UnsafeShowOpenWithDropdown(const QString &filepath) {\n\treturn false;\n}\n\ninline void UnsafeLaunch(const QString &filepath) {\n\treturn ::File::internal::UnsafeLaunchDefault(filepath);\n}\n\ninline void PostprocessDownloaded(const QString &filepath) {\n}\n\n} // namespace File\n\nnamespace FileDialog {\n\ninline void InitLastPath() {\n\t::FileDialog::internal::InitLastPathDefault();\n}\n\ninline bool Get(\n\t\tQPointer<QWidget> parent,\n\t\tQStringList &files,\n\t\tQByteArray &remoteContent,\n\t\tconst QString &caption,\n\t\tconst QString &filter,\n\t\t::FileDialog::internal::Type type,\n\t\tQString startFile) {\n\treturn ::FileDialog::internal::GetDefault(\n\t\tparent,\n\t\tfiles,\n\t\tremoteContent,\n\t\tcaption,\n\t\tfilter,\n\t\ttype,\n\t\tstartFile);\n}\n\n} // namespace FileDialog\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/integration_linux.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/linux/integration_linux.h\"\n\n#include \"platform/platform_integration.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/platform/linux/base_linux_xdp_utilities.h\"\n#include \"core/sandbox.h\"\n#include \"core/application.h\"\n#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)\n#include \"core/core_settings.h\"\n#endif\n#include \"base/random.h\"\n#include \"base/qt_connection.h\"\n\n#include <QtCore/QAbstractEventDispatcher>\n\n#include <gio/gio.hpp>\n#include <xdpinhibit/xdpinhibit.hpp>\n\n#ifdef __GLIBC__\n#include <malloc.h>\n#endif // __GLIBC__\n\nnamespace Platform {\nnamespace {\n\nusing namespace gi::repository;\nnamespace GObject = gi::repository::GObject;\n\nclass Application : public Gio::impl::ApplicationImpl {\npublic:\n\t[[maybe_unused]] Application();\n\n\tvoid before_emit_(GLib::Variant platformData) noexcept override {\n\t\tif (Platform::IsWayland()) {\n\t\t\tstatic const auto keys = {\n\t\t\t\t\"activation-token\",\n\t\t\t\t\"desktop-startup-id\",\n\t\t\t};\n\t\t\tfor (const auto &key : keys) {\n\t\t\t\tif (auto token = platformData.lookup_value(key)) {\n\t\t\t\t\tqputenv(\n\t\t\t\t\t\t\"XDG_ACTIVATION_TOKEN\",\n\t\t\t\t\t\ttoken.get_string(nullptr).c_str());\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid activate_() noexcept override {\n\t\tCore::Sandbox::Instance().customEnterFromEventLoop([] {\n\t\t\tCore::App().activate();\n\t\t});\n\t}\n\n\tvoid open_(\n\t\t\tgi::Collection<gi::DSpan, ::GFile*, gi::transfer_none_t> files,\n\t\t\tconst gi::cstring_v hint) noexcept override {\n\t\tfor (auto file : files) {\n\t\t\tQFileOpenEvent e(QUrl(QString::fromStdString(file.get_uri())));\n\t\t\tQGuiApplication::sendEvent(qApp, &e);\n\t\t}\n\t}\n\n\tvoid add_platform_data_(\n\t\t\tGLib::VariantBuilder_Ref builder) noexcept override {\n\t\tif (Platform::IsWayland()) {\n\t\t\tconst auto token = qgetenv(\"XDG_ACTIVATION_TOKEN\");\n\t\t\tif (!token.isEmpty()) {\n\t\t\t\tbuilder.add_value(\n\t\t\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\t\t\tGLib::Variant::new_string(\"activation-token\"),\n\t\t\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\t\t\tGLib::Variant::new_string(token.toStdString()))));\n\t\t\t\tqunsetenv(\"XDG_ACTIVATION_TOKEN\");\n\t\t\t}\n\t\t}\n\t}\n};\n\nApplication::Application()\n: Gio::impl::ApplicationImpl(this) {\n\tconst auto appId = QGuiApplication::desktopFileName().toStdString();\n\tif (Gio::Application::id_is_valid(appId)) {\n\t\tset_application_id(appId);\n\t}\n\tset_flags(Gio::ApplicationFlags::HANDLES_OPEN_);\n\n\tauto actionMap = Gio::ActionMap(*this);\n\n\tauto quitAction = Gio::SimpleAction::new_(\"quit\");\n\tquitAction.signal_activate().connect([](\n\t\t\tGio::SimpleAction,\n\t\t\tGLib::Variant parameter) {\n\t\tCore::Sandbox::Instance().customEnterFromEventLoop([] {\n\t\t\tCore::Quit();\n\t\t});\n\t});\n\tactionMap.add_action(quitAction);\n\n\tconst auto notificationIdVariantType = GLib::VariantType::new_(\"a{sv}\");\n\n\tauto notificationActivateAction = Gio::SimpleAction::new_(\n\t\t\"notification-activate\",\n\t\tnotificationIdVariantType);\n\n\tactionMap.add_action(notificationActivateAction);\n\n\tauto notificationMarkAsReadAction = Gio::SimpleAction::new_(\n\t\t\"notification-mark-as-read\",\n\t\tnotificationIdVariantType);\n\n\tactionMap.add_action(notificationMarkAsReadAction);\n}\n\ngi::ref_ptr<Application> MakeApplication() {\n\tconst auto result = gi::make_ref<Application>();\n\tif (const auto registered = result->register_(); !registered) {\n\t\tLOG((\"App Error: Failed to register: %1\").arg(\n\t\t\tregistered.error().message_().c_str()));\n\t\treturn nullptr;\n\t}\n\treturn result;\n}\n\nclass LinuxIntegration final : public Integration, public base::has_weak_ptr {\npublic:\n\tLinuxIntegration();\n\n\tvoid init() override;\n\nprivate:\n\t[[nodiscard]] XdpInhibit::Inhibit inhibit() {\n\t\treturn _inhibitProxy;\n\t}\n\n\tvoid initInhibit();\n\n\tconst gi::ref_ptr<Application> _application;\n\tXdpInhibit::InhibitProxy _inhibitProxy;\n#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)\n\tbase::Platform::XDP::SettingWatcher _darkModeWatcher;\n#endif // Qt < 6.5.0\n#ifdef __GLIBC__\n\tbase::qt_connection _memoryTrim;\n\tcrl::time _memoryTrimmed = 0;\n#endif // __GLIBC__\n};\n\nLinuxIntegration::LinuxIntegration()\n: _application(MakeApplication())\n#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)\n, _darkModeWatcher(\n\t\"org.freedesktop.appearance\",\n\t\"color-scheme\",\n\t[](GLib::Variant value) {\n\t\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t\tCore::App().settings().setSystemDarkMode(value.get_uint32() == 1);\n\t\t});\n})\n#endif // Qt < 6.5.0\n{\n\tLOG((\"Icon theme: %1\").arg(QIcon::themeName()));\n\tLOG((\"Fallback icon theme: %1\").arg(QIcon::fallbackThemeName()));\n\n\tif (!QCoreApplication::eventDispatcher()->inherits(\n\t\t\"QEventDispatcherGlib\")) {\n\t\tg_warning(\"Qt is running without GLib event loop integration, \"\n\t\t\t\"expect various functionality to not to work.\");\n\t}\n\n#ifdef __GLIBC__\n\t_memoryTrim = QObject::connect(\n\t\tQCoreApplication::eventDispatcher(),\n\t\t&QAbstractEventDispatcher::aboutToBlock,\n\t\t[=] {\n\t\t\tif (crl::now() - _memoryTrimmed >= 10000) {\n\t\t\t\tmalloc_trim(0);\n\t\t\t\t_memoryTrimmed = crl::now();\n\t\t\t}\n\t\t});\n#endif // __GLIBC__\n}\n\nvoid LinuxIntegration::init() {\n\tXdpInhibit::InhibitProxy::new_for_bus(\n\t\tGio::BusType::SESSION_,\n\t\tGio::DBusProxyFlags::NONE_,\n\t\tbase::Platform::XDP::kService,\n\t\tbase::Platform::XDP::kObjectPath,\n\t\tcrl::guard(this, [=](GObject::Object, Gio::AsyncResult res) {\n\t\t\t_inhibitProxy = XdpInhibit::InhibitProxy::new_for_bus_finish(\n\t\t\t\tres,\n\t\t\t\tnullptr);\n\n\t\t\tinitInhibit();\n\t\t}));\n}\n\nvoid LinuxIntegration::initInhibit() {\n\tif (!_inhibitProxy) {\n\t\treturn;\n\t}\n\n\tstd::string uniqueName = _inhibitProxy.get_connection().get_unique_name();\n\tuniqueName.erase(0, 1);\n\tuniqueName.replace(uniqueName.find('.'), 1, 1, '_');\n\n\tconst auto handleToken = \"tdesktop\"\n\t\t+ std::to_string(base::RandomValue<uint>());\n\n\tconst auto sessionHandleToken = \"tdesktop\"\n\t\t+ std::to_string(base::RandomValue<uint>());\n\n\tconst auto sessionHandle = base::Platform::XDP::kObjectPath\n\t\t+ std::string(\"/session/\")\n\t\t+ uniqueName\n\t\t+ '/'\n\t\t+ sessionHandleToken;\n\n\tinhibit().signal_state_changed().connect([\n\t\tmySessionHandle = sessionHandle\n\t](\n\t\t\tXdpInhibit::Inhibit,\n\t\t\tconst std::string &sessionHandle,\n\t\t\tGLib::Variant state) {\n\t\tif (sessionHandle != mySessionHandle) {\n\t\t\treturn;\n\t\t}\n\n\t\tCore::App().setScreenIsLocked(\n\t\t\tGLib::VariantDict::new_(\n\t\t\t\tstate\n\t\t\t).lookup_value(\n\t\t\t\t\"screensaver-active\"\n\t\t\t).get_boolean()\n\t\t);\n\t});\n\n\tinhibit().call_create_monitor(\n\t\t\"\",\n\t\tGLib::Variant::new_array({\n\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\tGLib::Variant::new_string(\"handle_token\"),\n\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\tGLib::Variant::new_string(handleToken))),\n\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\tGLib::Variant::new_string(\"session_handle_token\"),\n\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\tGLib::Variant::new_string(sessionHandleToken))),\n\t\t}),\n\t\tnullptr);\n}\n\n} // namespace\n\nstd::unique_ptr<Integration> CreateIntegration() {\n\treturn std::make_unique<LinuxIntegration>();\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/integration_linux.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Platform {\n\nclass Integration;\n\n[[nodiscard]] std::unique_ptr<Integration> CreateIntegration();\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/launcher_linux.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/linux/launcher_linux.h\"\n\n#include \"core/crash_reports.h\"\n#include \"core/update_checker.h\"\n#include \"webview/platform/linux/webview_linux_webkitgtk.h\"\n\n#include <QtWidgets/QApplication>\n#include <glib/glib.hpp>\n\n#ifdef __GLIBC__\n#include <malloc.h>\n#endif // __GLIBC__\n\nusing namespace gi::repository;\n\nnamespace Platform {\n\nLauncher::Launcher(int argc, char *argv[])\n: Core::Launcher(argc, argv) {\n#ifdef __GLIBC__\n\tmallopt(M_ARENA_MAX, 1);\n#endif // __GLIBC__\n}\n\nint Launcher::exec() {\n\tfor (auto i = arguments().begin(), e = arguments().end(); i != e; ++i) {\n\t\tif (*i == u\"-webviewhelper\"_q && std::distance(i, e) > 1) {\n\t\t\tWebview::WebKitGTK::SetSocketPath((i + 1)->toStdString());\n\t\t\treturn Webview::WebKitGTK::Exec();\n\t\t}\n\t}\n\n\treturn Core::Launcher::exec();\n}\n\nbool Launcher::launchUpdater(UpdaterLaunch action) {\n\tif (cExeName().isEmpty()) {\n\t\treturn false;\n\t}\n\n\tconst auto justRelaunch = action == UpdaterLaunch::JustRelaunch;\n\tif (action == UpdaterLaunch::PerformUpdate) {\n\t\t_updating = true;\n\t}\n\n\tstd::vector<std::string> argumentsList;\n\n\t// What we are launching.\n\tconst auto launching = justRelaunch\n\t\t? (cExeDir() + cExeName())\n\t\t: cWriteProtected()\n\t\t? GLib::find_program_in_path(\"run0\")\n\t\t? u\"run0\"_q\n\t\t: u\"pkexec\"_q\n\t\t: (cExeDir() + u\"Updater\"_q);\n\targumentsList.push_back(launching.toStdString());\n\n\tif (justRelaunch) {\n\t\t// argv[0] that is passed to what we are launching.\n\t\t// It should be added explicitly in case of FILE_AND_ARGV_ZERO_.\n\t\tconst auto argv0 = !arguments().isEmpty()\n\t\t\t? arguments().first()\n\t\t\t: launching;\n\t\targumentsList.push_back(argv0.toStdString());\n\t} else if (cWriteProtected()) {\n\t\t// Elevated process that run0/pkexec should launch.\n\t\tconst auto elevated = cWorkingDir() + u\"tupdates/temp/Updater\"_q;\n\t\targumentsList.push_back(elevated.toStdString());\n\t}\n\n\tif (Logs::DebugEnabled()) {\n\t\targumentsList.push_back(\"-debug\");\n\t}\n\n\tif (justRelaunch) {\n\t\tif (cLaunchMode() == LaunchModeAutoStart) {\n\t\t\targumentsList.push_back(\"-autostart\");\n\t\t}\n\t\tif (cStartInTray()) {\n\t\t\targumentsList.push_back(\"-startintray\");\n\t\t}\n\t\tif (cDataFile() != u\"data\"_q) {\n\t\t\targumentsList.push_back(\"-key\");\n\t\t\targumentsList.push_back(cDataFile().toStdString());\n\t\t}\n\t\tif (!_updating) {\n\t\t\targumentsList.push_back(\"-noupdate\");\n\t\t\targumentsList.push_back(\"-tosettings\");\n\t\t}\n\t\tif (customWorkingDir()) {\n\t\t\targumentsList.push_back(\"-workdir\");\n\t\t\targumentsList.push_back(cWorkingDir().toStdString());\n\t\t}\n\t} else {\n\t\t// Don't relaunch Telegram.\n\t\targumentsList.push_back(\"-justupdate\");\n\n\t\targumentsList.push_back(\"-workpath\");\n\t\targumentsList.push_back(cWorkingDir().toStdString());\n\t\targumentsList.push_back(\"-exename\");\n\t\targumentsList.push_back(cExeName().toStdString());\n\t\targumentsList.push_back(\"-exepath\");\n\t\targumentsList.push_back(cExeDir().toStdString());\n\t\tif (cWriteProtected()) {\n\t\t\targumentsList.push_back(\"-writeprotected\");\n\t\t}\n\t}\n\n\tLogs::closeMain();\n\tCrashReports::Finish();\n\n\tint waitStatus = 0;\n\tif (justRelaunch) {\n\t\treturn GLib::spawn_async(\n\t\t\tinitialWorkingDir().toStdString(),\n\t\t\targumentsList,\n\t\t\t{},\n\t\t\tGLib::SpawnFlags::FILE_AND_ARGV_ZERO_,\n\t\t\tnullptr,\n\t\t\tnullptr,\n\t\t\tnullptr);\n\t} else if (!GLib::spawn_sync(\n\t\t\targumentsList,\n\t\t\t{},\n\t\t\t// if the spawn is sync, working directory is not set\n\t\t\t// and GLib::SpawnFlags::LEAVE_DESCRIPTORS_OPEN_ is set,\n\t\t\t// it goes through an optimized code path\n\t\t\tGLib::SpawnFlags::SEARCH_PATH_\n\t\t\t\t| GLib::SpawnFlags::LEAVE_DESCRIPTORS_OPEN_,\n\t\t\tnullptr,\n\t\t\tnullptr,\n\t\t\tnullptr,\n\t\t\t&waitStatus,\n\t\t\tnullptr) || !g_spawn_check_exit_status(waitStatus, nullptr)) {\n\t\treturn false;\n\t}\n\treturn launchUpdater(UpdaterLaunch::JustRelaunch);\n}\n\n} // namespace\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/launcher_linux.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"core/launcher.h\"\n\nnamespace Platform {\n\nclass Launcher : public Core::Launcher {\npublic:\n\tLauncher(int argc, char *argv[]);\n\n\tint exec() override;\n\nprivate:\n\tbool launchUpdater(UpdaterLaunch action) override;\n\n\tbool _updating = false;\n\n};\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/main_window_linux.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/linux/main_window_linux.h\"\n\n#include \"styles/style_window.h\"\n#include \"platform/linux/specific_linux.h\"\n#include \"history/history.h\"\n#include \"history/history_widget.h\"\n#include \"history/history_inner_widget.h\"\n#include \"main/main_account.h\" // Account::sessionChanges.\n#include \"main/main_session.h\"\n#include \"mainwindow.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/sandbox.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/about_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"storage/localstorage.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/event_filter.h\"\n#include \"ui/platform/ui_platform_window_title.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/ui_utility.h\"\n\n#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION\n#include \"base/platform/linux/base_linux_xcb_utilities.h\"\n#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION\n\n#include <QtCore/QSize>\n#include <QtCore/QMimeData>\n#include <QtGui/QWindow>\n#include <QtWidgets/QMenuBar>\n#include <QtWidgets/QLineEdit>\n#include <QtWidgets/QTextEdit>\n\n#include <gio/gio.hpp>\n\nnamespace Platform {\nnamespace {\n\nusing WorkMode = Core::Settings::WorkMode;\n\n#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION\nvoid XCBSkipTaskbar(QWindow *window, bool skip) {\n\tconst base::Platform::XCB::Connection connection;\n\tif (!connection || xcb_connection_has_error(connection)) {\n\t\treturn;\n\t}\n\n\tconst auto root = base::Platform::XCB::GetRootWindow(connection);\n\tif (!root) {\n\t\treturn;\n\t}\n\n\tconst auto stateAtom = base::Platform::XCB::GetAtom(\n\t\tconnection,\n\t\t\"_NET_WM_STATE\");\n\n\tif (!stateAtom) {\n\t\treturn;\n\t}\n\n\tconst auto skipTaskbarAtom = base::Platform::XCB::GetAtom(\n\t\tconnection,\n\t\t\"_NET_WM_STATE_SKIP_TASKBAR\");\n\n\tif (!skipTaskbarAtom) {\n\t\treturn;\n\t}\n\n\txcb_client_message_event_t xev;\n\txev.response_type = XCB_CLIENT_MESSAGE;\n\txev.type = stateAtom;\n\txev.sequence = 0;\n\txev.window = window->winId();\n\txev.format = 32;\n\txev.data.data32[0] = skip ? 1 : 0;\n\txev.data.data32[1] = skipTaskbarAtom;\n\txev.data.data32[2] = 0;\n\txev.data.data32[3] = 0;\n\txev.data.data32[4] = 0;\n\n\tfree(\n\t\txcb_request_check(\n\t\t\tconnection,\n\t\t\txcb_send_event_checked(\n\t\t\t\tconnection,\n\t\t\t\tfalse,\n\t\t\t\troot,\n\t\t\t\tXCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT\n\t\t\t\t\t| XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,\n\t\t\t\treinterpret_cast<const char*>(&xev))));\n}\n#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION\n\nvoid SkipTaskbar(QWindow *window, bool skip) {\n#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION\n\tif (IsX11()) {\n\t\tXCBSkipTaskbar(window, skip);\n\t\treturn;\n\t}\n#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION\n}\n\nvoid SendKeySequence(\n\t\tQt::Key key,\n\t\tQt::KeyboardModifiers modifiers = Qt::NoModifier) {\n\tconst auto focused = QApplication::focusWidget();\n\tif (qobject_cast<QLineEdit*>(focused)\n\t\t|| qobject_cast<QTextEdit*>(focused)\n\t\t|| dynamic_cast<HistoryInner*>(focused)) {\n\t\tQApplication::postEvent(\n\t\t\tfocused,\n\t\t\tnew QKeyEvent(QEvent::KeyPress, key, modifiers));\n\t\tQApplication::postEvent(\n\t\t\tfocused,\n\t\t\tnew QKeyEvent(QEvent::KeyRelease, key, modifiers));\n\t}\n}\n\nvoid ForceDisabled(QAction *action, bool disabled) {\n\tif (action->isEnabled()) {\n\t\tif (disabled) action->setDisabled(true);\n\t} else if (!disabled) {\n\t\taction->setDisabled(false);\n\t}\n}\n\n} // namespace\n\nMainWindow::MainWindow(not_null<Window::Controller*> controller)\n: Window::MainWindow(controller) {\n}\n\nvoid MainWindow::workmodeUpdated(Core::Settings::WorkMode mode) {\n\tif (!TrayIconSupported()) {\n\t\treturn;\n\t}\n\n\tSkipTaskbar(windowHandle(), mode == WorkMode::TrayOnly);\n}\n\nvoid MainWindow::unreadCounterChangedHook() {\n\tupdateUnityCounter();\n}\n\nvoid MainWindow::updateWindowIcon() {\n\tconst auto session = sessionController()\n\t\t? &sessionController()->session()\n\t\t: nullptr;\n\tsetWindowIcon(Window::CreateIcon(session));\n}\n\nvoid MainWindow::updateUnityCounter() {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)\n\tqApp->setBadgeNumber(Core::App().unreadBadge());\n#else // Qt >= 6.6.0\n\tusing namespace gi::repository;\n\n\tstatic const auto djbStringHash = [](const std::string &string) {\n\t\tuint hash = 5381;\n\t\tfor (const auto &curChar : string) {\n\t\t\thash = (hash << 5) + hash + curChar;\n\t\t}\n\t\treturn hash;\n\t};\n\n\tconst auto launcherUrl = \"application://\"\n\t\t+ QGuiApplication::desktopFileName().toStdString()\n\t\t+ \".desktop\";\n\n\tconst auto counterSlice = std::min(Core::App().unreadBadge(), 9999);\n\n\tauto connection = Gio::bus_get_sync(Gio::BusType::SESSION_, nullptr);\n\tif (!connection) {\n\t\treturn;\n\t}\n\n\tconnection.emit_signal(\n\t\t{},\n\t\t\"/com/canonical/unity/launcherentry/\"\n\t\t\t+ std::to_string(djbStringHash(launcherUrl)),\n\t\t\"com.canonical.Unity.LauncherEntry\",\n\t\t\"Update\",\n\t\tGLib::Variant::new_tuple({\n\t\t\tGLib::Variant::new_string(launcherUrl),\n\t\t\tGLib::Variant::new_array({\n\t\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\t\tGLib::Variant::new_string(\"count\"),\n\t\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\t\tGLib::Variant::new_int64(counterSlice))),\n\t\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\t\tGLib::Variant::new_string(\"count-visible\"),\n\t\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\t\tGLib::Variant::new_boolean(counterSlice))),\n\t\t\t}),\n\t\t}));\n#endif // Qt < 6.6.0\n}\n\nvoid MainWindow::createGlobalMenu() {\n\tconst auto ensureWindowShown = [=] {\n\t\tif (isHidden()) {\n\t\t\tshowFromTray();\n\t\t}\n\t};\n\n\tpsMainMenu = new QMenuBar(this);\n\tpsMainMenu->hide();\n\n\tauto file = psMainMenu->addMenu(tr::lng_mac_menu_file(tr::now));\n\n\tpsLogout = file->addAction(\n\t\ttr::lng_mac_menu_logout(tr::now),\n\t\tthis,\n\t\t[=] {\n\t\t\tensureWindowShown();\n\t\t\tcontroller().showLogoutConfirmation();\n\t\t});\n\n\tauto quit = file->addAction(\n\t\ttr::lng_mac_menu_quit_telegram(tr::now, lt_telegram, u\"Telegram\"_q),\n\t\tthis,\n\t\t[=] { quitFromTray(); },\n\t\tQKeySequence::Quit);\n\n\tquit->setMenuRole(QAction::QuitRole);\n\tquit->setShortcutContext(Qt::WidgetShortcut);\n\n\tauto edit = psMainMenu->addMenu(tr::lng_mac_menu_edit(tr::now));\n\n\tpsUndo = edit->addAction(\n\t\ttr::lng_linux_menu_undo(tr::now),\n\t\t[] { SendKeySequence(Qt::Key_Z, Qt::ControlModifier); },\n\t\tQKeySequence::Undo);\n\n\tpsUndo->setShortcutContext(Qt::WidgetShortcut);\n\n\tpsRedo = edit->addAction(\n\t\ttr::lng_linux_menu_redo(tr::now),\n\t\t[] {\n\t\t\tSendKeySequence(\n\t\t\t\tQt::Key_Z,\n\t\t\t\tQt::ControlModifier | Qt::ShiftModifier);\n\t\t},\n\t\tQKeySequence::Redo);\n\n\tpsRedo->setShortcutContext(Qt::WidgetShortcut);\n\n\tedit->addSeparator();\n\n\tpsCut = edit->addAction(\n\t\ttr::lng_mac_menu_cut(tr::now),\n\t\t[] { SendKeySequence(Qt::Key_X, Qt::ControlModifier); },\n\t\tQKeySequence::Cut);\n\n\tpsCut->setShortcutContext(Qt::WidgetShortcut);\n\n\tpsCopy = edit->addAction(\n\t\ttr::lng_mac_menu_copy(tr::now),\n\t\t[] { SendKeySequence(Qt::Key_C, Qt::ControlModifier); },\n\t\tQKeySequence::Copy);\n\n\tpsCopy->setShortcutContext(Qt::WidgetShortcut);\n\n\tpsPaste = edit->addAction(\n\t\ttr::lng_mac_menu_paste(tr::now),\n\t\t[] { SendKeySequence(Qt::Key_V, Qt::ControlModifier); },\n\t\tQKeySequence::Paste);\n\n\tpsPaste->setShortcutContext(Qt::WidgetShortcut);\n\n\tpsDelete = edit->addAction(\n\t\ttr::lng_mac_menu_delete(tr::now),\n\t\t[] { SendKeySequence(Qt::Key_Delete); },\n\t\tQKeySequence(Qt::ControlModifier | Qt::Key_Backspace));\n\n\tpsDelete->setShortcutContext(Qt::WidgetShortcut);\n\n\tedit->addSeparator();\n\n\tpsBold = edit->addAction(\n\t\ttr::lng_menu_formatting_bold(tr::now),\n\t\t[] { SendKeySequence(Qt::Key_B, Qt::ControlModifier); },\n\t\tQKeySequence::Bold);\n\n\tpsBold->setShortcutContext(Qt::WidgetShortcut);\n\n\tpsItalic = edit->addAction(\n\t\ttr::lng_menu_formatting_italic(tr::now),\n\t\t[] { SendKeySequence(Qt::Key_I, Qt::ControlModifier); },\n\t\tQKeySequence::Italic);\n\n\tpsItalic->setShortcutContext(Qt::WidgetShortcut);\n\n\tpsUnderline = edit->addAction(\n\t\ttr::lng_menu_formatting_underline(tr::now),\n\t\t[] { SendKeySequence(Qt::Key_U, Qt::ControlModifier); },\n\t\tQKeySequence::Underline);\n\n\tpsUnderline->setShortcutContext(Qt::WidgetShortcut);\n\n\tpsStrikeOut = edit->addAction(\n\t\ttr::lng_menu_formatting_strike_out(tr::now),\n\t\t[] {\n\t\t\tSendKeySequence(\n\t\t\t\tQt::Key_X,\n\t\t\t\tQt::ControlModifier | Qt::ShiftModifier);\n\t\t},\n\t\tUi::kStrikeOutSequence);\n\n\tpsStrikeOut->setShortcutContext(Qt::WidgetShortcut);\n\n\tpsBlockquote = edit->addAction(\n\t\ttr::lng_menu_formatting_blockquote(tr::now),\n\t\t[] {\n\t\t\tSendKeySequence(\n\t\t\t\tQt::Key_Period,\n\t\t\t\tQt::ControlModifier | Qt::ShiftModifier);\n\t\t},\n\t\tUi::kBlockquoteSequence);\n\n\tpsBlockquote->setShortcutContext(Qt::WidgetShortcut);\n\n\tpsMonospace = edit->addAction(\n\t\ttr::lng_menu_formatting_monospace(tr::now),\n\t\t[] {\n\t\t\tSendKeySequence(\n\t\t\t\tQt::Key_M,\n\t\t\t\tQt::ControlModifier | Qt::ShiftModifier);\n\t\t},\n\t\tUi::kMonospaceSequence);\n\n\tpsMonospace->setShortcutContext(Qt::WidgetShortcut);\n\n\tpsClearFormat = edit->addAction(\n\t\ttr::lng_menu_formatting_clear(tr::now),\n\t\t[] {\n\t\t\tSendKeySequence(\n\t\t\t\tQt::Key_N,\n\t\t\t\tQt::ControlModifier | Qt::ShiftModifier);\n\t\t},\n\t\tUi::kClearFormatSequence);\n\n\tpsClearFormat->setShortcutContext(Qt::WidgetShortcut);\n\n\tedit->addSeparator();\n\n\tpsSelectAll = edit->addAction(\n\t\ttr::lng_mac_menu_select_all(tr::now),\n\t\t[] { SendKeySequence(Qt::Key_A, Qt::ControlModifier); },\n\t\tQKeySequence::SelectAll);\n\n\tpsSelectAll->setShortcutContext(Qt::WidgetShortcut);\n\n\tedit->addSeparator();\n\n\tauto prefs = edit->addAction(\n\t\ttr::lng_mac_menu_preferences(tr::now),\n\t\tthis,\n\t\t[=] {\n\t\t\tensureWindowShown();\n\t\t\tcontroller().showSettings();\n\t\t},\n\t\tQKeySequence(Qt::ControlModifier | Qt::Key_Comma));\n\n\tprefs->setMenuRole(QAction::PreferencesRole);\n\n\tauto tools = psMainMenu->addMenu(tr::lng_linux_menu_tools(tr::now));\n\n\tpsContacts = tools->addAction(\n\t\ttr::lng_mac_menu_contacts(tr::now),\n\t\tcrl::guard(this, [=] {\n\t\t\tif (isHidden()) {\n\t\t\t\tshowFromTray();\n\t\t\t}\n\n\t\t\tif (!sessionController()) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsessionController()->show(\n\t\t\t\tPrepareContactsBox(sessionController()));\n\t\t}));\n\n\tpsAddContact = tools->addAction(\n\t\ttr::lng_mac_menu_add_contact(tr::now),\n\t\tthis,\n\t\t[=] {\n\t\t\tExpects(sessionController() != nullptr);\n\t\t\tensureWindowShown();\n\t\t\tsessionController()->showAddContact();\n\t\t});\n\n\ttools->addSeparator();\n\n\tpsNewGroup = tools->addAction(\n\t\ttr::lng_mac_menu_new_group(tr::now),\n\t\tthis,\n\t\t[=] {\n\t\t\tExpects(sessionController() != nullptr);\n\t\t\tensureWindowShown();\n\t\t\tsessionController()->showNewGroup();\n\t\t});\n\n\tpsNewChannel = tools->addAction(\n\t\ttr::lng_mac_menu_new_channel(tr::now),\n\t\tthis,\n\t\t[=] {\n\t\t\tExpects(sessionController() != nullptr);\n\t\t\tensureWindowShown();\n\t\t\tsessionController()->showNewChannel();\n\t\t});\n\n\tauto help = psMainMenu->addMenu(tr::lng_linux_menu_help(tr::now));\n\n\tauto about = help->addAction(\n\t\ttr::lng_mac_menu_about_telegram(\n\t\t\ttr::now,\n\t\t\tlt_telegram,\n\t\t\tu\"Telegram\"_q),\n\t\t[=] {\n\t\t\tensureWindowShown();\n\t\t\tcontroller().show(Box(AboutBox));\n\t\t});\n\n\tabout->setMenuRole(QAction::AboutQtRole);\n\n\tupdateGlobalMenu();\n}\n\nvoid MainWindow::updateGlobalMenuHook() {\n\tif (!positionInited()) {\n\t\treturn;\n\t}\n\n\tconst auto focused = QApplication::focusWidget();\n\tauto canUndo = false;\n\tauto canRedo = false;\n\tauto canCut = false;\n\tauto canCopy = false;\n\tauto canPaste = false;\n\tauto canDelete = false;\n\tauto canSelectAll = false;\n\tconst auto mimeData = QGuiApplication::clipboard()->mimeData();\n\tconst auto clipboardHasText = mimeData ? mimeData->hasText() : false;\n\tauto markdownState = Ui::MarkdownEnabledState();\n\tif (const auto edit = qobject_cast<QLineEdit*>(focused)) {\n\t\tcanCut = canCopy = canDelete = edit->hasSelectedText();\n\t\tcanSelectAll = !edit->text().isEmpty();\n\t\tcanUndo = edit->isUndoAvailable();\n\t\tcanRedo = edit->isRedoAvailable();\n\t\tcanPaste = clipboardHasText;\n\t} else if (const auto edit = qobject_cast<QTextEdit*>(focused)) {\n\t\tcanCut = canCopy = canDelete = edit->textCursor().hasSelection();\n\t\tcanSelectAll = !edit->document()->isEmpty();\n\t\tcanUndo = edit->document()->isUndoAvailable();\n\t\tcanRedo = edit->document()->isRedoAvailable();\n\t\tcanPaste = clipboardHasText;\n\t\tif (canCopy) {\n\t\t\tif (const auto inputField = dynamic_cast<Ui::InputField*>(\n\t\t\t\tfocused->parentWidget())) {\n\t\t\t\tmarkdownState = inputField->markdownEnabledState();\n\t\t\t}\n\t\t}\n\t} else if (const auto list = dynamic_cast<HistoryInner*>(focused)) {\n\t\tcanCopy = list->canCopySelected();\n\t\tcanDelete = list->canDeleteSelected();\n\t}\n\tupdateIsActive();\n\tconst auto logged = (sessionController() != nullptr);\n\tconst auto inactive = !logged || controller().locked();\n\tconst auto support = logged\n\t\t&& sessionController()->session().supportMode();\n\tForceDisabled(psLogout, !logged && !Core::App().passcodeLocked());\n\tForceDisabled(psUndo, !canUndo);\n\tForceDisabled(psRedo, !canRedo);\n\tForceDisabled(psCut, !canCut);\n\tForceDisabled(psCopy, !canCopy);\n\tForceDisabled(psPaste, !canPaste);\n\tForceDisabled(psDelete, !canDelete);\n\tForceDisabled(psSelectAll, !canSelectAll);\n\tForceDisabled(psContacts, inactive || support);\n\tForceDisabled(psAddContact, inactive);\n\tForceDisabled(psNewGroup, inactive || support);\n\tForceDisabled(psNewChannel, inactive || support);\n\n\tconst auto diabled = [=](const QString &tag) {\n\t\treturn !markdownState.enabledForTag(tag);\n\t};\n\tusing Field = Ui::InputField;\n\tForceDisabled(psBold, diabled(Field::kTagBold));\n\tForceDisabled(psItalic, diabled(Field::kTagItalic));\n\tForceDisabled(psUnderline, diabled(Field::kTagUnderline));\n\tForceDisabled(psStrikeOut, diabled(Field::kTagStrikeOut));\n\tForceDisabled(psBlockquote, diabled(Field::kTagBlockquote));\n\tForceDisabled(\n\t\tpsMonospace,\n\t\tdiabled(Field::kTagPre) || diabled(Field::kTagCode));\n\tForceDisabled(psClearFormat, markdownState.disabled());\n}\n\nbool MainWindow::eventFilter(QObject *obj, QEvent *evt) {\n\tconst auto t = evt->type();\n\tif (t == QEvent::FocusIn || t == QEvent::FocusOut) {\n\t\tif (qobject_cast<QLineEdit*>(obj)\n\t\t\t|| qobject_cast<QTextEdit*>(obj)\n\t\t\t|| dynamic_cast<HistoryInner*>(obj)) {\n\t\t\tif (QApplication::focusWidget()) {\n\t\t\t\tupdateGlobalMenu();\n\t\t\t}\n\t\t}\n\t} else if (obj == this && t == QEvent::Paint) {\n\t\tif (!_exposed) {\n\t\t\t_exposed = true;\n\t\t\tSkipTaskbar(\n\t\t\t\twindowHandle(),\n\t\t\t\t(Core::App().settings().workMode() == WorkMode::TrayOnly)\n\t\t\t\t\t&& TrayIconSupported());\n\t\t}\n\t} else if (obj == this && t == QEvent::Hide) {\n\t\t_exposed = false;\n\t} else if (obj == this && t == QEvent::ThemeChange) {\n\t\tupdateWindowIcon();\n\t}\n\treturn Window::MainWindow::eventFilter(obj, evt);\n}\n\nMainWindow::~MainWindow() {\n}\n\nint32 ScreenNameChecksum(const QString &name) {\n\treturn Window::DefaultScreenNameChecksum(name);\n}\n\nint32 ScreenNameChecksum(const QScreen *screen) {\n\treturn ScreenNameChecksum(screen->name());\n}\n\nQString ScreenDisplayLabel(const QScreen *screen) {\n\tif (!screen) {\n\t\treturn QString();\n\t}\n\n\tconst auto model = (screen->manufacturer()\n\t\t+ ' '\n\t\t+ screen->model()).simplified();\n\n\tif (!model.isEmpty()) {\n\t\tif (!screen->name().isEmpty()) {\n\t\t\treturn (model\n\t\t\t\t+ ' '\n\t\t\t\t+ QChar(8212)\n\t\t\t\t+ ' '\n\t\t\t\t+ screen->name()).simplified();\n\t\t}\n\n\t\treturn model;\n\t}\n\n\treturn screen->name();\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/main_window_linux.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_main_window.h\"\n#include \"base/unique_qptr.h\"\n\nclass QMenuBar;\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Platform {\n\nclass MainWindow : public Window::MainWindow {\npublic:\n\texplicit MainWindow(not_null<Window::Controller*> controller);\n\t~MainWindow();\n\n\tvoid updateWindowIcon() override;\n\nprotected:\n\tbool eventFilter(QObject *obj, QEvent *evt) override;\n\n\tvoid unreadCounterChangedHook() override;\n\tvoid updateGlobalMenuHook() override;\n\n\tvoid workmodeUpdated(Core::Settings::WorkMode mode) override;\n\tvoid createGlobalMenu() override;\n\nprivate:\n\tvoid updateUnityCounter();\n\n\tQMenuBar *psMainMenu = nullptr;\n\tQAction *psLogout = nullptr;\n\tQAction *psUndo = nullptr;\n\tQAction *psRedo = nullptr;\n\tQAction *psCut = nullptr;\n\tQAction *psCopy = nullptr;\n\tQAction *psPaste = nullptr;\n\tQAction *psDelete = nullptr;\n\tQAction *psSelectAll = nullptr;\n\tQAction *psContacts = nullptr;\n\tQAction *psAddContact = nullptr;\n\tQAction *psNewGroup = nullptr;\n\tQAction *psNewChannel = nullptr;\n\n\tQAction *psBold = nullptr;\n\tQAction *psItalic = nullptr;\n\tQAction *psUnderline = nullptr;\n\tQAction *psStrikeOut = nullptr;\n\tQAction *psBlockquote = nullptr;\n\tQAction *psMonospace = nullptr;\n\tQAction *psClearFormat = nullptr;\n\n\tbool _exposed = false;\n\n};\n\n[[nodiscard]] int32 ScreenNameChecksum(const QString &name);\n[[nodiscard]] int32 ScreenNameChecksum(const QScreen *screen);\n\n[[nodiscard]] QString ScreenDisplayLabel(const QScreen *screen);\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp",
    "content": "\n/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/linux/notifications_manager_linux.h\"\n\n#include \"base/options.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/platform/linux/base_linux_dbus_utilities.h\"\n#include \"platform/platform_specific.h\"\n#include \"core/application.h\"\n#include \"core/sandbox.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_peer.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"main/main_session.h\"\n#include \"media/audio/media_audio_local_cache.h\"\n#include \"lang/lang_keys.h\"\n#include \"base/weak_ptr.h\"\n#include \"window/notifications_utilities.h\"\n\n#include <QtCore/QBuffer>\n#include <QtCore/QVersionNumber>\n#include <QtGui/QGuiApplication>\n\n#include <ksandbox.h>\n\n#include <xdgnotifications/xdgnotifications.hpp>\n\n#include <dlfcn.h>\n\nnamespace Platform {\nnamespace Notifications {\nnamespace {\n\nusing namespace gi::repository;\nnamespace GObject = gi::repository::GObject;\n\nconstexpr auto kService = \"org.freedesktop.Notifications\";\nconstexpr auto kObjectPath = \"/org/freedesktop/Notifications\";\n\nstruct ServerInformation {\n\tstd::string name;\n\tstd::string vendor;\n\tQVersionNumber version;\n\tQVersionNumber specVersion;\n};\n\nbool ServiceRegistered = false;\nServerInformation CurrentServerInformation;\nstd::vector<std::string> CurrentCapabilities;\n\n[[nodiscard]] bool HasCapability(const char *value) {\n\treturn ranges::contains(CurrentCapabilities, value);\n}\n\nstd::optional<base::Platform::DBus::ServiceWatcher> CreateServiceWatcher() {\n\tauto connection = Gio::bus_get_sync(Gio::BusType::SESSION_, nullptr);\n\tif (!connection) {\n\t\treturn {};\n\t}\n\n\tconst auto activatable = [&] {\n\t\tconst auto names = base::Platform::DBus::ListActivatableNames(\n\t\t\tconnection.gobj_());\n\n\t\tif (!names) {\n\t\t\t// avoid service restart loop in sandboxed environments\n\t\t\treturn true;\n\t\t}\n\n\t\treturn ranges::contains(*names, kService);\n\t}();\n\n\treturn std::make_optional<base::Platform::DBus::ServiceWatcher>(\n\t\tconnection.gobj_(),\n\t\tkService,\n\t\t[=](\n\t\t\tconst std::string &service,\n\t\t\tconst std::string &oldOwner,\n\t\t\tconst std::string &newOwner) {\n\t\t\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t\t\tif (activatable && newOwner.empty()) {\n\t\t\t\t\tCore::App().notifications().clearAll();\n\t\t\t\t} else {\n\t\t\t\t\tCore::App().notifications().createManager();\n\t\t\t\t}\n\t\t\t});\n\t\t});\n}\n\nvoid StartServiceAsync(Gio::DBusConnection connection, Fn<void()> callback) {\n\tnamespace DBus = base::Platform::DBus;\n\tDBus::StartServiceByNameAsync(\n\t\tconnection.gobj_(),\n\t\tkService,\n\t\t[=](Fn<DBus::Result<DBus::StartReply>()> result) {\n\t\t\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t\t\t// get the error if any\n\t\t\t\tif (const auto ret = result(); !ret) {\n\t\t\t\t\tconst auto &error = *static_cast<GLib::Error*>(\n\t\t\t\t\t\tret.error().get());\n\n\t\t\t\t\tif (error.gobj_()->domain != G_DBUS_ERROR\n\t\t\t\t\t\t\t|| error.code_()\n\t\t\t\t\t\t\t\t!= G_DBUS_ERROR_SERVICE_UNKNOWN) {\n\t\t\t\t\t\tGio::DBusErrorNS_::strip_remote_error(error);\n\t\t\t\t\t\tLOG((\"Native Notification Error: %1\").arg(\n\t\t\t\t\t\t\terror.message_().c_str()));\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcallback();\n\t\t\t});\n\t\t});\n}\n\nstd::string GetImageKey() {\n\tconst auto &specVersion = CurrentServerInformation.specVersion;\n\tif (specVersion >= QVersionNumber(1, 2)) {\n\t\treturn \"image-data\";\n\t} else if (specVersion == QVersionNumber(1, 1)) {\n\t\treturn \"image_data\";\n\t}\n\treturn \"icon_data\";\n}\n\nbool UseGNotification() {\n\tif (!Gio::Application::get_default()) {\n\t\treturn false;\n\t}\n\n\tif (Window::Notifications::OptionGNotification.value()) {\n\t\treturn true;\n\t}\n\n\treturn KSandbox::isFlatpak() && !ServiceRegistered;\n}\n\n} // namespace\n\nclass Manager::Private : public base::has_weak_ptr {\npublic:\n\texplicit Private(not_null<Manager*> manager);\n\n\tvoid init(XdgNotifications::NotificationsProxy proxy);\n\n\tvoid showNotification(\n\t\tNotificationInfo &&info,\n\t\tUi::PeerUserpicView &userpicView);\n\tvoid clearAll();\n\tvoid clearFromItem(not_null<HistoryItem*> item);\n\tvoid clearFromTopic(not_null<Data::ForumTopic*> topic);\n\tvoid clearFromSublist(not_null<Data::SavedSublist*> sublist);\n\tvoid clearFromHistory(not_null<History*> history);\n\tvoid clearFromSession(not_null<Main::Session*> session);\n\tvoid clearNotification(NotificationId id);\n\tvoid invokeIfNotInhibited(Fn<void()> callback);\n\nprivate:\n\tstruct NotificationData : public base::has_weak_ptr {\n\t\tstd::variant<v::null_t, uint, std::string> id;\n\t\trpl::lifetime lifetime;\n\t};\n\tusing Notification = std::unique_ptr<NotificationData>;\n\n\tconst not_null<Manager*> _manager;\n\tGio::Application _application;\n\tXdgNotifications::NotificationsProxy _proxy;\n\tXdgNotifications::Notifications _interface;\n\tMedia::Audio::LocalDiskCache _sounds;\n\tbase::flat_map<\n\t\tContextId,\n\t\tbase::flat_map<MsgId, Notification>> _notifications;\n\trpl::lifetime _lifetime;\n\n};\n\nbool SkipToastForCustom() {\n\treturn false;\n}\n\nvoid MaybePlaySoundForCustom(Fn<void()> playSound) {\n\tplaySound();\n}\n\nvoid MaybeFlashBounceForCustom(Fn<void()> flashBounce) {\n\tflashBounce();\n}\n\nbool WaitForInputForCustom() {\n\treturn true;\n}\n\nbool Supported() {\n\treturn ServiceRegistered || UseGNotification();\n}\n\nbool Enforced() {\n\t// Wayland doesn't support positioning\n\t// and custom notifications don't work here\n\treturn IsWayland()\n\t\t|| (Gio::Application::get_default()\n\t\t\t&& Window::Notifications::OptionGNotification.value());\n}\n\nbool ByDefault() {\n\t// The capabilities are static, equivalent to 'body' and 'actions' only\n\tif (UseGNotification()) {\n\t\treturn false;\n\t}\n\n\t// A list of capabilities that offer feature parity\n\t// with custom notifications\n\treturn ranges::all_of(std::array{\n\t\t// To show message content\n\t\t\"body\",\n\t\t// To have buttons on notifications\n\t\t\"actions\",\n\t\t// To have quick reply\n\t\t\"inline-reply\",\n\t}, HasCapability) && ranges::any_of(std::array{\n\t\t// To not to play sound with Don't Disturb activated\n\t\t\"sound\",\n\t\t\"inhibitions\",\n\t}, HasCapability);\n}\n\nbool VolumeSupported() {\n\treturn UseGNotification() || !HasCapability(\"sound\");\n}\n\nvoid Create(Window::Notifications::System *system) {\n\tstatic const auto ServiceWatcher = CreateServiceWatcher();\n\n\tconst auto managerSetter = [=](\n\t\t\tXdgNotifications::NotificationsProxy proxy) {\n\t\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t\tsystem->setManager([=] {\n\t\t\t\tauto manager = std::make_unique<Manager>(system);\n\t\t\t\tmanager->_private->init(proxy);\n\t\t\t\treturn manager;\n\t\t\t});\n\t\t});\n\t};\n\n\tconst auto counter = std::make_shared<int>(2);\n\tconst auto oneReady = [=](XdgNotifications::NotificationsProxy proxy) {\n\t\tif (!--*counter) {\n\t\t\tmanagerSetter(proxy);\n\t\t}\n\t};\n\n\tXdgNotifications::NotificationsProxy::new_for_bus(\n\t\tGio::BusType::SESSION_,\n\t\tGio::DBusProxyFlags::NONE_,\n\t\tkService,\n\t\tkObjectPath,\n\t\t[=](GObject::Object, Gio::AsyncResult res) {\n\t\t\tauto result =\n\t\t\t\tXdgNotifications::NotificationsProxy::new_for_bus_finish(res);\n\n\t\t\tif (result) {\n\t\t\t\tServiceRegistered = bool(result->get_name_owner());\n\t\t\t} else {\n\t\t\t\tGio::DBusErrorNS_::strip_remote_error(result.error());\n\t\t\t\tLOG((\"Native Notification Error: %1\").arg(\n\t\t\t\t\tresult.error().message_().c_str()));\n\t\t\t\tServiceRegistered = false;\n\t\t\t}\n\n\t\t\tif (!ServiceRegistered) {\n\t\t\t\tCurrentServerInformation = {};\n\t\t\t\tCurrentCapabilities = {};\n\t\t\t\tmanagerSetter({});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tauto proxy = *result;\n\t\t\tauto interface = XdgNotifications::Notifications(proxy);\n\n\t\t\tinterface.call_get_server_information([=](\n\t\t\t\t\tGObject::Object,\n\t\t\t\t\tGio::AsyncResult res) mutable {\n\t\t\t\tconst auto result =\n\t\t\t\t\tinterface.call_get_server_information_finish(res);\n\t\t\t\tif (result) {\n\t\t\t\t\tCurrentServerInformation = {\n\t\t\t\t\t\tstd::get<1>(*result),\n\t\t\t\t\t\tstd::get<2>(*result),\n\t\t\t\t\t\tQVersionNumber::fromString(\n\t\t\t\t\t\t\tQString::fromStdString(std::get<3>(*result))\n\t\t\t\t\t\t).normalized(),\n\t\t\t\t\t\tQVersionNumber::fromString(\n\t\t\t\t\t\t\tQString::fromStdString(std::get<4>(*result))\n\t\t\t\t\t\t).normalized(),\n\t\t\t\t\t};\n\t\t\t\t} else {\n\t\t\t\t\tGio::DBusErrorNS_::strip_remote_error(result.error());\n\t\t\t\t\tLOG((\"Native Notification Error: %1\").arg(\n\t\t\t\t\t\tresult.error().message_().c_str()));\n\t\t\t\t\tCurrentServerInformation = {};\n\t\t\t\t}\n\t\t\t\toneReady(proxy);\n\t\t\t});\n\n\t\t\tinterface.call_get_capabilities([=](\n\t\t\t\t\tGObject::Object,\n\t\t\t\t\tGio::AsyncResult res) mutable {\n\t\t\t\tconst auto result = interface.call_get_capabilities_finish(\n\t\t\t\t\tres);\n\t\t\t\tif (result) {\n\t\t\t\t\tCurrentCapabilities = std::get<1>(*result)\n\t\t\t\t\t\t| ranges::to<std::vector<std::string>>;\n\t\t\t\t} else {\n\t\t\t\t\tGio::DBusErrorNS_::strip_remote_error(result.error());\n\t\t\t\t\tLOG((\"Native Notification Error: %1\").arg(\n\t\t\t\t\t\tresult.error().message_().c_str()));\n\t\t\t\t\tCurrentCapabilities = {};\n\t\t\t\t}\n\t\t\t\toneReady(proxy);\n\t\t\t});\n\t\t});\n}\n\nManager::Private::Private(not_null<Manager*> manager)\n: _manager(manager)\n, _application(UseGNotification()\n\t\t? Gio::Application::get_default()\n\t\t: nullptr)\n, _sounds(cWorkingDir() + u\"tdata/audio_cache\"_q) {\n\tconst auto &serverInformation = CurrentServerInformation;\n\n\tif (!serverInformation.name.empty()) {\n\t\tLOG((\"Notification daemon product name: %1\")\n\t\t\t.arg(serverInformation.name.c_str()));\n\t}\n\n\tif (!serverInformation.vendor.empty()) {\n\t\tLOG((\"Notification daemon vendor name: %1\")\n\t\t\t.arg(serverInformation.vendor.c_str()));\n\t}\n\n\tif (!serverInformation.version.isNull()) {\n\t\tLOG((\"Notification daemon version: %1\")\n\t\t\t.arg(serverInformation.version.toString()));\n\t}\n\n\tif (!serverInformation.specVersion.isNull()) {\n\t\tLOG((\"Notification daemon specification version: %1\")\n\t\t\t.arg(serverInformation.specVersion.toString()));\n\t}\n\n\tif (!CurrentCapabilities.empty()) {\n\t\tLOG((\"Notification daemon capabilities: %1\").arg(\n\t\t\tranges::fold_left(\n\t\t\t\tCurrentCapabilities,\n\t\t\t\t\"\",\n\t\t\t\t[](const std::string &a, const std::string &b) {\n\t\t\t\t\treturn a + (a.empty() ? \"\" : \", \") + b;\n\t\t\t\t}).c_str()));\n\t}\n\n\tif (auto actionMap = Gio::ActionMap(_application)) {\n\t\tconst auto dictToNotificationId = [](GLib::VariantDict dict) {\n\t\t\treturn NotificationId{\n\t\t\t\t.contextId = ContextId{\n\t\t\t\t\t.sessionId = dict.lookup_value(\"session\").get_uint64(),\n\t\t\t\t\t.peerId = PeerId(dict.lookup_value(\"peer\").get_uint64()),\n\t\t\t\t\t.topicRootId = MsgId(\n\t\t\t\t\t\tdict.lookup_value(\"topic\").get_int64()),\n\t\t\t\t\t.monoforumPeerId = PeerId(dict.lookup_value(\n\t\t\t\t\t\t\"monoforumpeer\").get_uint64()),\n\t\t\t\t},\n\t\t\t\t.msgId = dict.lookup_value(\"msgid\").get_int64(),\n\t\t\t};\n\t\t};\n\n\t\tauto activate = gi::object_cast<Gio::SimpleAction>(\n\t\t\tactionMap.lookup_action(\"notification-activate\"));\n\n\t\tconst auto activateSig = activate.signal_activate().connect([=](\n\t\t\t\tGio::SimpleAction,\n\t\t\t\tGLib::Variant parameter) {\n\t\t\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t\t\t_manager->notificationActivated(\n\t\t\t\t\tdictToNotificationId(GLib::VariantDict::new_(parameter)));\n\t\t\t});\n\t\t});\n\n\t\t_lifetime.add([=]() mutable {\n\t\t\tactivate.disconnect(activateSig);\n\t\t});\n\n\t\tauto markAsRead = gi::object_cast<Gio::SimpleAction>(\n\t\t\tactionMap.lookup_action(\"notification-mark-as-read\"));\n\n\t\tconst auto markAsReadSig = markAsRead.signal_activate().connect([=](\n\t\t\t\tGio::SimpleAction,\n\t\t\t\tGLib::Variant parameter) {\n\t\t\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t\t\t_manager->notificationReplied(\n\t\t\t\t\tdictToNotificationId(GLib::VariantDict::new_(parameter)),\n\t\t\t\t\t{});\n\t\t\t});\n\t\t});\n\n\t\t_lifetime.add([=]() mutable {\n\t\t\tmarkAsRead.disconnect(markAsReadSig);\n\t\t});\n\t}\n}\n\nvoid Manager::Private::init(XdgNotifications::NotificationsProxy proxy) {\n\t_proxy = proxy;\n\t_interface = proxy;\n\n\tif (_application || !_interface) {\n\t\treturn;\n\t}\n\n\tconst auto actionInvoked = _interface.signal_action_invoked().connect([=](\n\t\t\tXdgNotifications::Notifications,\n\t\t\tuint id,\n\t\t\tstd::string actionName) {\n\t\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t\tfor (const auto &[key, notifications] : _notifications) {\n\t\t\t\tfor (const auto &[msgId, notification] : notifications) {\n\t\t\t\t\tconst auto &nid = notification->id;\n\t\t\t\t\tif (v::is<uint>(nid) && v::get<uint>(nid) == id) {\n\t\t\t\t\t\tif (actionName == \"default\") {\n\t\t\t\t\t\t\t_manager->notificationActivated({ key, msgId });\n\t\t\t\t\t\t} else if (actionName == \"mail-mark-read\") {\n\t\t\t\t\t\t\t_manager->notificationReplied({ key, msgId }, {});\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n\n\t_lifetime.add([=] {\n\t\t_interface.disconnect(actionInvoked);\n\t});\n\n\tconst auto replied = _interface.signal_notification_replied().connect([=](\n\t\t\tXdgNotifications::Notifications,\n\t\t\tuint id,\n\t\t\tstd::string text) {\n\t\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t\tfor (const auto &[key, notifications] : _notifications) {\n\t\t\t\tfor (const auto &[msgId, notification] : notifications) {\n\t\t\t\t\tconst auto &nid = notification->id;\n\t\t\t\t\tif (v::is<uint>(nid) && v::get<uint>(nid) == id) {\n\t\t\t\t\t\t_manager->notificationReplied(\n\t\t\t\t\t\t\t{ key, msgId },\n\t\t\t\t\t\t\t{ QString::fromStdString(text), {} });\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n\n\t_lifetime.add([=] {\n\t\t_interface.disconnect(replied);\n\t});\n\n\tconst auto tokenSignal = _interface.signal_activation_token().connect([=](\n\t\t\tXdgNotifications::Notifications,\n\t\t\tuint id,\n\t\t\tstd::string token) {\n\t\tfor (const auto &[key, notifications] : _notifications) {\n\t\t\tfor (const auto &[msgId, notification] : notifications) {\n\t\t\t\tconst auto &nid = notification->id;\n\t\t\t\tif (v::is<uint>(nid) && v::get<uint>(nid) == id) {\n\t\t\t\t\tGLib::setenv(\"XDG_ACTIVATION_TOKEN\", token, true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\n\t_lifetime.add([=] {\n\t\t_interface.disconnect(tokenSignal);\n\t});\n\n\tconst auto closed = _interface.signal_notification_closed().connect([=](\n\t\t\tXdgNotifications::Notifications,\n\t\t\tuint id,\n\t\t\tuint reason) {\n\t\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t\tfor (const auto &[key, notifications] : _notifications) {\n\t\t\t\tfor (const auto &[msgId, notification] : notifications) {\n\t\t\t\t\t/*\n\t\t\t\t\t* From: https://specifications.freedesktop.org/notification-spec/latest/ar01s09.html\n\t\t\t\t\t* The reason the notification was closed\n\t\t\t\t\t* 1 - The notification expired.\n\t\t\t\t\t* 2 - The notification was dismissed by the user.\n\t\t\t\t\t* 3 - The notification was closed by a call to CloseNotification.\n\t\t\t\t\t* 4 - Undefined/reserved reasons.\n\t\t\t\t\t*\n\t\t\t\t\t* If the notification was dismissed by the user (reason == 2), the notification is not kept in notification history.\n\t\t\t\t\t* We do not need to send a \"CloseNotification\" call later to clear it from history.\n\t\t\t\t\t* Therefore we can drop the notification reference now.\n\t\t\t\t\t* In all other cases we keep the notification reference so that we may clear the notification later from history,\n\t\t\t\t\t* if the message for that notification is read (e.g. chat is opened or read from another device).\n\t\t\t\t\t*/\n\t\t\t\t\tconst auto &nid = notification->id;\n\t\t\t\t\tif (v::is<uint>(nid) && v::get<uint>(nid) == id && reason == 2) {\n\t\t\t\t\t\tclearNotification({ key, msgId });\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n\n\t_lifetime.add([=] {\n\t\t_interface.disconnect(closed);\n\t});\n}\n\nvoid Manager::Private::showNotification(\n\t\tNotificationInfo &&info,\n\t\tUi::PeerUserpicView &userpicView) {\n\tconst auto peer = info.peer;\n\tconst auto options = info.options;\n\tconst auto key = ContextId{\n\t\t.sessionId = peer->session().uniqueId(),\n\t\t.peerId = peer->id,\n\t\t.topicRootId = info.topicRootId,\n\t\t.monoforumPeerId = info.monoforumPeerId,\n\t};\n\tconst auto notificationId = NotificationId{\n\t\t.contextId = key,\n\t\t.msgId = info.itemId,\n\t};\n\tauto notification = _application\n\t\t? Gio::Notification::new_(info.title.toStdString())\n\t\t: Gio::Notification();\n\n\tstd::vector<gi::cstring> actions;\n\tauto hints = GLib::VariantDict::new_();\n\tif (notification) {\n\t\tnotification.set_body(info.subtitle.isEmpty()\n\t\t\t? info.message.toStdString()\n\t\t\t: tr::lng_dialogs_text_with_from(\n\t\t\t\ttr::now,\n\t\t\t\tlt_from_part,\n\t\t\t\ttr::lng_dialogs_text_from_wrapped(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_from,\n\t\t\t\t\tinfo.subtitle),\n\t\t\t\tlt_message,\n\t\t\t\tinfo.message).toStdString());\n\n\t\tnotification.set_icon(\n\t\t\tGio::ThemedIcon::new_(ApplicationIconName().toStdString()));\n\n\t\t// for chat messages, according to\n\t\t// https://docs.gtk.org/gio/enum.NotificationPriority.html\n\t\tnotification.set_priority(Gio::NotificationPriority::HIGH_);\n\n\t\t// glib 2.70+, we keep glib 2.56+ compatibility\n\t\tstatic const auto set_category = [] {\n\t\t\t// reset dlerror after dlsym call\n\t\t\tconst auto guard = gsl::finally([] { dlerror(); });\n\t\t\treturn reinterpret_cast<void(*)(GNotification*, const gchar*)>(\n\t\t\t\tdlsym(RTLD_DEFAULT, \"g_notification_set_category\"));\n\t\t}();\n\n\t\tif (set_category) {\n\t\t\tset_category(notification.gobj_(), \"im.received\");\n\t\t}\n\n\t\tconst auto notificationVariant = GLib::Variant::new_array({\n\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\tGLib::Variant::new_string(\"session\"),\n\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\tGLib::Variant::new_uint64(peer->session().uniqueId()))),\n\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\tGLib::Variant::new_string(\"peer\"),\n\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\tGLib::Variant::new_uint64(peer->id.value))),\n\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\tGLib::Variant::new_string(\"peer\"),\n\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\tGLib::Variant::new_uint64(peer->id.value))),\n\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\tGLib::Variant::new_string(\"topic\"),\n\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\tGLib::Variant::new_int64(info.topicRootId.bare))),\n\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\tGLib::Variant::new_string(\"monoforumpeer\"),\n\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\tGLib::Variant::new_uint64(info.monoforumPeerId.value))),\n\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\tGLib::Variant::new_string(\"msgid\"),\n\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\tGLib::Variant::new_int64(info.itemId.bare))),\n\t\t});\n\n\t\tnotification.set_default_action_and_target(\n\t\t\t\"app.notification-activate\",\n\t\t\tnotificationVariant);\n\n\t\tif (!options.hideMarkAsRead) {\n\t\t\tnotification.add_button_with_target(\n\t\t\t\ttr::lng_context_mark_read(tr::now).toStdString(),\n\t\t\t\t\"app.notification-mark-as-read\",\n\t\t\t\tnotificationVariant);\n\t\t}\n\t} else {\n\t\tif (HasCapability(\"actions\")) {\n\t\t\tactions.push_back(\"default\");\n\t\t\tactions.push_back(tr::lng_open_link(tr::now).toStdString());\n\n\t\t\tif (!options.hideMarkAsRead) {\n\t\t\t\t// icon name according to https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html\n\t\t\t\tactions.push_back(\"mail-mark-read\");\n\t\t\t\tactions.push_back(\n\t\t\t\t\ttr::lng_context_mark_read(tr::now).toStdString());\n\t\t\t}\n\n\t\t\tif (HasCapability(\"inline-reply\")\n\t\t\t\t\t&& !options.hideReplyButton) {\n\t\t\t\tactions.push_back(\"inline-reply\");\n\t\t\t\tactions.push_back(\n\t\t\t\t\ttr::lng_notification_reply(tr::now).toStdString());\n\t\t\t}\n\t\t}\n\n\t\tactions.push_back({});\n\n\t\tif (HasCapability(\"action-icons\")) {\n\t\t\thints.insert_value(\n\t\t\t\t\"action-icons\",\n\t\t\t\tGLib::Variant::new_boolean(true));\n\t\t}\n\n\t\tif (HasCapability(\"sound\")) {\n\t\t\tconst auto sound = info.sound\n\t\t\t\t? info.sound()\n\t\t\t\t: Media::Audio::LocalSound();\n\n\t\t\tconst auto path = sound\n\t\t\t\t? _sounds.path(sound).toStdString()\n\t\t\t\t: std::string();\n\n\t\t\tif (!path.empty()) {\n\t\t\t\thints.insert_value(\n\t\t\t\t\t\"sound-file\",\n\t\t\t\t\tGLib::Variant::new_string(path));\n\t\t\t} else {\n\t\t\t\thints.insert_value(\n\t\t\t\t\t\"suppress-sound\",\n\t\t\t\t\tGLib::Variant::new_boolean(true));\n\t\t\t}\n\t\t}\n\n\t\tif (HasCapability(\"x-canonical-append\")) {\n\t\t\thints.insert_value(\n\t\t\t\t\"x-canonical-append\",\n\t\t\t\tGLib::Variant::new_string(\"true\"));\n\t\t}\n\n\t\thints.insert_value(\n\t\t\t\"category\",\n\t\t\tGLib::Variant::new_string(\"im.received\"));\n\n\t\thints.insert_value(\"desktop-entry\", GLib::Variant::new_string(\n\t\t\tQGuiApplication::desktopFileName().toStdString()));\n\t}\n\n\tconst auto imageKey = GetImageKey();\n\tif (!options.hideNameAndPhoto) {\n\t\tif (notification) {\n\t\t\tQByteArray imageData;\n\t\t\tQBuffer buffer(&imageData);\n\t\t\tbuffer.open(QIODevice::WriteOnly);\n\t\t\tWindow::Notifications::GenerateUserpic(peer, userpicView).save(\n\t\t\t\t&buffer,\n\t\t\t\t\"PNG\");\n\n\t\t\tnotification.set_icon(\n\t\t\t\tGio::BytesIcon::new_(\n\t\t\t\t\tGLib::Bytes::new_with_free_func(\n\t\t\t\t\t\treinterpret_cast<const uchar*>(imageData.constData()),\n\t\t\t\t\t\timageData.size(),\n\t\t\t\t\t\t[imageData] {})));\n\t\t} else if (!imageKey.empty()) {\n\t\t\tconst auto image = Window::Notifications::GenerateUserpic(\n\t\t\t\tpeer,\n\t\t\t\tuserpicView\n\t\t\t).convertToFormat(QImage::Format_RGBA8888);\n\n\t\t\thints.insert_value(imageKey, GLib::Variant::new_tuple({\n\t\t\t\tGLib::Variant::new_int32(image.width()),\n\t\t\t\tGLib::Variant::new_int32(image.height()),\n\t\t\t\tGLib::Variant::new_int32(image.bytesPerLine()),\n\t\t\t\tGLib::Variant::new_boolean(true),\n\t\t\t\tGLib::Variant::new_int32(8),\n\t\t\t\tGLib::Variant::new_int32(4),\n\t\t\t\tGLib::Variant::new_from_data(\n\t\t\t\t\tGLib::VariantType::new_(\"ay\"),\n\t\t\t\t\treinterpret_cast<const uchar*>(image.constBits()),\n\t\t\t\t\timage.sizeInBytes(),\n\t\t\t\t\ttrue,\n\t\t\t\t\t[image] {}),\n\t\t\t}));\n\t\t}\n\t}\n\n\tconst auto &data\n\t\t= _notifications[key][info.itemId]\n\t\t\t= std::make_unique<NotificationData>();\n\tdata->lifetime.add([=, notification = data.get()] {\n\t\tv::match(notification->id, [&](const std::string &id) {\n\t\t\t_application.withdraw_notification(id);\n\t\t}, [&](uint id) {\n\t\t\t_interface.call_close_notification(id, nullptr);\n\t\t}, [](v::null_t) {});\n\t});\n\n\tif (notification) {\n\t\tconst auto id = Gio::dbus_generate_guid();\n\t\tdata->id = id;\n\t\t_application.send_notification(id, notification);\n\t} else {\n\t\t// work around snap's activation restriction\n\t\tconst auto weak = base::make_weak(data);\n\t\tStartServiceAsync(\n\t\t\t_proxy.get_connection(),\n\t\t\tcrl::guard(weak, [=]() mutable {\n\t\t\t\tconst auto hasImage = !imageKey.empty()\n\t\t\t\t\t&& hints.lookup_value(imageKey);\n\n\t\t\t\tconst auto callbackWrap = gi::unwrap(\n\t\t\t\t\tGio::AsyncReadyCallback(\n\t\t\t\t\t\tcrl::guard(this, [=](\n\t\t\t\t\t\t\t\tGObject::Object,\n\t\t\t\t\t\t\t\tGio::AsyncResult res) {\n\t\t\t\t\t\t\tauto &sandbox = Core::Sandbox::Instance();\n\t\t\t\t\t\t\tsandbox.customEnterFromEventLoop([&] {\n\t\t\t\t\t\t\t\tconst auto result\n\t\t\t\t\t\t\t\t\t= _interface.call_notify_finish(res);\n\n\t\t\t\t\t\t\t\tif (!result) {\n\t\t\t\t\t\t\t\t\tGio::DBusErrorNS_::strip_remote_error(\n\t\t\t\t\t\t\t\t\t\tresult.error());\n\t\t\t\t\t\t\t\t\tLOG((\"Native Notification Error: %1\").arg(\n\t\t\t\t\t\t\t\t\t\tresult.error().message_().c_str()));\n\t\t\t\t\t\t\t\t\tclearNotification(notificationId);\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif (!weak) {\n\t\t\t\t\t\t\t\t\t_interface.call_close_notification(\n\t\t\t\t\t\t\t\t\t\tstd::get<1>(*result),\n\t\t\t\t\t\t\t\t\t\tnullptr);\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tweak->id = std::get<1>(*result);\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t})),\n\t\t\t\t\tgi::scope_async);\n\n\t\t\t\txdg_notifications_notifications_call_notify(\n\t\t\t\t\t_interface.gobj_(),\n\t\t\t\t\tAppNameF.data(),\n\t\t\t\t\t0,\n\t\t\t\t\t(!hasImage\n\t\t\t\t\t\t? ApplicationIconName().toStdString()\n\t\t\t\t\t\t: std::string()).c_str(),\n\t\t\t\t\tinfo.title.toStdString().c_str(),\n\t\t\t\t\t(HasCapability(\"body-markup\")\n\t\t\t\t\t\t? info.subtitle.isEmpty()\n\t\t\t\t\t\t\t? info.message.toHtmlEscaped().toStdString()\n\t\t\t\t\t\t\t: u\"<b>%1</b>\\n%2\"_q.arg(\n\t\t\t\t\t\t\t\tinfo.subtitle.toHtmlEscaped(),\n\t\t\t\t\t\t\t\tinfo.message.toHtmlEscaped()).toStdString()\n\t\t\t\t\t\t: info.subtitle.isEmpty()\n\t\t\t\t\t\t\t? info.message.toStdString()\n\t\t\t\t\t\t\t: tr::lng_dialogs_text_with_from(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_from_part,\n\t\t\t\t\t\t\t\ttr::lng_dialogs_text_from_wrapped(\n\t\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\t\tlt_from,\n\t\t\t\t\t\t\t\t\tinfo.subtitle),\n\t\t\t\t\t\t\t\tlt_message,\n\t\t\t\t\t\t\t\tinfo.message).toStdString()).c_str(),\n\t\t\t\t\t(actions\n\t\t\t\t\t\t| ranges::views::transform(&gi::cstring::c_str)\n\t\t\t\t\t\t| ranges::to_vector).data(),\n\t\t\t\t\thints.end().gobj_(),\n\t\t\t\t\t-1,\n\t\t\t\t\tnullptr,\n\t\t\t\t\t&callbackWrap->wrapper,\n\t\t\t\t\tcallbackWrap);\n\t\t\t}));\n\t}\n}\n\nvoid Manager::Private::clearAll() {\n\t_notifications.clear();\n}\n\nvoid Manager::Private::clearFromItem(not_null<HistoryItem*> item) {\n\tconst auto i = _notifications.find(ContextId{\n\t\t.sessionId = item->history()->session().uniqueId(),\n\t\t.peerId = item->history()->peer->id,\n\t\t.topicRootId = item->topicRootId(),\n\t\t.monoforumPeerId = item->sublistPeerId(),\n\t});\n\tif (i != _notifications.cend()\n\t\t\t&& i->second.remove(item->id)\n\t\t\t&& i->second.empty()) {\n\t\t_notifications.erase(i);\n\t}\n}\n\nvoid Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) {\n\t_notifications.remove(ContextId{\n\t\t.sessionId = topic->session().uniqueId(),\n\t\t.peerId = topic->history()->peer->id,\n\t\t.topicRootId = topic->rootId(),\n\t});\n}\n\nvoid Manager::Private::clearFromSublist(\n\t\tnot_null<Data::SavedSublist*> sublist) {\n\t_notifications.remove(ContextId{\n\t\t.sessionId = sublist->session().uniqueId(),\n\t\t.peerId = sublist->owningHistory()->peer->id,\n\t\t.monoforumPeerId = sublist->sublistPeer()->id,\n\t});\n}\n\nvoid Manager::Private::clearFromHistory(not_null<History*> history) {\n\tconst auto sessionId = history->session().uniqueId();\n\tconst auto peerId = history->peer->id;\n\tauto i = _notifications.lower_bound(ContextId{\n\t\t.sessionId = sessionId,\n\t\t.peerId = peerId,\n\t});\n\twhile (i != _notifications.cend()\n\t\t&& i->first.sessionId == sessionId\n\t\t&& i->first.peerId == peerId) {\n\t\ti = _notifications.erase(i);\n\t}\n}\n\nvoid Manager::Private::clearFromSession(not_null<Main::Session*> session) {\n\tconst auto sessionId = session->uniqueId();\n\tauto i = _notifications.lower_bound(ContextId{\n\t\t.sessionId = sessionId,\n\t});\n\twhile (i != _notifications.cend() && i->first.sessionId == sessionId) {\n\t\ti = _notifications.erase(i);\n\t}\n}\n\nvoid Manager::Private::clearNotification(NotificationId id) {\n\tauto i = _notifications.find(id.contextId);\n\tif (i != _notifications.cend()\n\t\t\t&& i->second.remove(id.msgId)\n\t\t\t&& i->second.empty()) {\n\t\t_notifications.erase(i);\n\t}\n}\n\nvoid Manager::Private::invokeIfNotInhibited(Fn<void()> callback) {\n\tif (!_interface.get_inhibited()) {\n\t\tcallback();\n\t}\n}\n\nManager::Manager(not_null<Window::Notifications::System*> system)\n: NativeManager(system)\n, _private(std::make_unique<Private>(this)) {\n}\n\nManager::~Manager() = default;\n\nvoid Manager::doShowNativeNotification(\n\t\tNotificationInfo &&info,\n\t\tUi::PeerUserpicView &userpicView) {\n\t_private->showNotification(std::move(info), userpicView);\n}\n\nvoid Manager::doClearAllFast() {\n\t_private->clearAll();\n}\n\nvoid Manager::doClearFromItem(not_null<HistoryItem*> item) {\n\t_private->clearFromItem(item);\n}\n\nvoid Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) {\n\t_private->clearFromTopic(topic);\n}\n\nvoid Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) {\n\t_private->clearFromSublist(sublist);\n}\n\nvoid Manager::doClearFromHistory(not_null<History*> history) {\n\t_private->clearFromHistory(history);\n}\n\nvoid Manager::doClearFromSession(not_null<Main::Session*> session) {\n\t_private->clearFromSession(session);\n}\n\nbool Manager::doSkipToast() const {\n\treturn false;\n}\n\nvoid Manager::doMaybePlaySound(Fn<void()> playSound) {\n\t_private->invokeIfNotInhibited(std::move(playSound));\n}\n\nvoid Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {\n\t_private->invokeIfNotInhibited(std::move(flashBounce));\n}\n\n} // namespace Notifications\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/notifications_manager_linux.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_notifications_manager.h\"\n\nnamespace Platform {\nnamespace Notifications {\n\nclass Manager : public Window::Notifications::NativeManager {\npublic:\n\tManager(not_null<Window::Notifications::System*> system);\n\t~Manager();\n\nprotected:\n\tvoid doShowNativeNotification(\n\t\tNotificationInfo &&info,\n\t\tUi::PeerUserpicView &userpicView) override;\n\tvoid doClearAllFast() override;\n\tvoid doClearFromItem(not_null<HistoryItem*> item) override;\n\tvoid doClearFromTopic(not_null<Data::ForumTopic*> topic) override;\n\tvoid doClearFromSublist(not_null<Data::SavedSublist*> sublist) override;\n\tvoid doClearFromHistory(not_null<History*> history) override;\n\tvoid doClearFromSession(not_null<Main::Session*> session) override;\n\tbool doSkipToast() const override;\n\tvoid doMaybePlaySound(Fn<void()> playSound) override;\n\tvoid doMaybeFlashBounce(Fn<void()> flashBounce) override;\n\nprivate:\n\tfriend void Create(Window::Notifications::System *system);\n\tclass Private;\n\tconst std::unique_ptr<Private> _private;\n\n};\n\n} // namespace Notifications\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/org.freedesktop.Notifications.xml",
    "content": "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n<node>\n  <interface name=\"org.freedesktop.Notifications\">\n    <signal name=\"NotificationClosed\">\n      <arg direction=\"out\" type=\"u\" name=\"id\"/>\n      <arg direction=\"out\" type=\"u\" name=\"reason\"/>\n    </signal>\n    <signal name=\"ActionInvoked\">\n      <arg direction=\"out\" type=\"u\" name=\"id\"/>\n      <arg direction=\"out\" type=\"s\" name=\"action_key\"/>\n    </signal>\n    <signal name=\"NotificationReplied\">\n      <arg direction=\"out\" type=\"u\" name=\"id\"/>\n      <arg direction=\"out\" type=\"s\" name=\"text\"/>\n    </signal>\n    <signal name=\"ActivationToken\">\n      <arg direction=\"out\" type=\"u\" name=\"id\"/>\n      <arg direction=\"out\" type=\"s\" name=\"activation_token\"/>\n    </signal>\n    <method name=\"Notify\">\n      <annotation value=\"QVariantMap\" name=\"org.qtproject.QtDBus.QtTypeName.In6\"/>\n      <arg direction=\"out\" type=\"u\"/>\n      <arg direction=\"in\" type=\"s\" name=\"app_name\"/>\n      <arg direction=\"in\" type=\"u\" name=\"replaces_id\"/>\n      <arg direction=\"in\" type=\"s\" name=\"app_icon\"/>\n      <arg direction=\"in\" type=\"s\" name=\"summary\"/>\n      <arg direction=\"in\" type=\"s\" name=\"body\"/>\n      <arg direction=\"in\" type=\"as\" name=\"actions\"/>\n      <arg direction=\"in\" type=\"a{sv}\" name=\"hints\"/>\n      <arg direction=\"in\" type=\"i\" name=\"timeout\"/>\n    </method>\n    <method name=\"CloseNotification\">\n      <arg direction=\"in\" type=\"u\" name=\"id\"/>\n    </method>\n    <method name=\"GetCapabilities\">\n      <arg direction=\"out\" type=\"as\" name=\"caps\"/>\n    </method>\n    <method name=\"GetServerInformation\">\n      <arg direction=\"out\" type=\"s\" name=\"name\"/>\n      <arg direction=\"out\" type=\"s\" name=\"vendor\"/>\n      <arg direction=\"out\" type=\"s\" name=\"version\"/>\n      <arg direction=\"out\" type=\"s\" name=\"spec_version\"/>\n    </method>\n    <method name=\"Inhibit\">\n      <annotation value=\"QVariantMap\" name=\"org.qtproject.QtDBus.QtTypeName.In2\"/>\n      <arg direction=\"out\" type=\"u\"/>\n      <arg direction=\"in\" type=\"s\" name=\"desktop_entry\"/>\n      <arg direction=\"in\" type=\"s\" name=\"reason\"/>\n      <arg direction=\"in\" type=\"a{sv}\" name=\"hints\"/>\n    </method>\n    <method name=\"UnInhibit\">\n      <arg direction=\"in\" type=\"u\"/>\n    </method>\n    <property access=\"read\" type=\"b\" name=\"Inhibited\">\n      <annotation value=\"true\" name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\"/>\n    </property>\n  </interface>\n</node>\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/overlay_widget_linux.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Platform {\n\ninline std::unique_ptr<OverlayWidgetHelper> CreateOverlayWidgetHelper(\n\t\tnot_null<Ui::RpWindow*> window,\n\t\tFn<void(bool)> maximize) {\n\treturn std::make_unique<DefaultOverlayWidgetHelper>(\n\t\twindow,\n\t\tstd::move(maximize));\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/specific_linux.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/linux/specific_linux.h\"\n\n#include \"base/openssl_help.h\"\n#include \"base/random.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/platform/linux/base_linux_dbus_utilities.h\"\n#include \"base/platform/linux/base_linux_xdp_utilities.h\"\n#include \"base/platform/linux/base_linux_app_launch_context.h\"\n#include \"lang/lang_keys.h\"\n#include \"core/launcher.h\"\n#include \"core/sandbox.h\"\n#include \"core/application.h\"\n#include \"core/update_checker.h\"\n#include \"data/data_location.h\"\n#include \"window/window_controller.h\"\n#include \"webview/platform/linux/webview_linux_webkitgtk.h\"\n\n#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION\n#include \"base/platform/linux/base_linux_xcb_utilities.h\"\n#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION\n\n#include <QtWidgets/QApplication>\n#include <QtWidgets/QSystemTrayIcon>\n#include <QtGui/QDesktopServices>\n#include <QtCore/QStandardPaths>\n#include <QtCore/QProcess>\n\n#include <kshell.h>\n#include <ksandbox.h>\n\n#include <xdgdbus/xdgdbus.hpp>\n#include <xdpbackground/xdpbackground.hpp>\n#include <xdpopenuri/xdpopenuri.hpp>\n#include <xdprequest/xdprequest.hpp>\n\n#include <sys/stat.h>\n#include <sys/types.h>\n#include <sys/un.h>\n#include <cstdlib>\n#include <unistd.h>\n#include <dirent.h>\n#include <pwd.h>\n\nnamespace {\n\nusing namespace gi::repository;\nnamespace GObject = gi::repository::GObject;\nusing namespace Platform;\n\nvoid PortalAutostart(bool enabled, Fn<void(bool)> done) {\n\tconst auto executable = ExecutablePathForShortcuts();\n\tif (executable.isEmpty()) {\n\t\tif (done) {\n\t\t\tdone(false);\n\t\t}\n\t\treturn;\n\t}\n\n\tXdpBackground::BackgroundProxy::new_for_bus(\n\t\tGio::BusType::SESSION_,\n\t\tGio::DBusProxyFlags::NONE_,\n\t\tbase::Platform::XDP::kService,\n\t\tbase::Platform::XDP::kObjectPath,\n\t\t[=](GObject::Object, Gio::AsyncResult res) {\n\t\t\tauto proxy = XdpBackground::BackgroundProxy::new_for_bus_finish(\n\t\t\t\tres);\n\n\t\t\tif (!proxy) {\n\t\t\t\tif (done) {\n\t\t\t\t\tGio::DBusErrorNS_::strip_remote_error(proxy.error());\n\t\t\t\t\tLOG((\"Portal Autostart Error: %1\").arg(\n\t\t\t\t\t\tproxy.error().message_().c_str()));\n\t\t\t\t\tdone(false);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tauto interface = XdpBackground::Background(*proxy);\n\n\t\t\tconst auto handleToken = \"tdesktop\"\n\t\t\t\t+ std::to_string(base::RandomValue<uint>());\n\n\t\t\tauto uniqueName = std::string(\n\t\t\t\tproxy->get_connection().get_unique_name());\n\t\t\tuniqueName.erase(0, 1);\n\t\t\tuniqueName.replace(uniqueName.find('.'), 1, 1, '_');\n\n\t\t\tconst auto parent = []() -> QPointer<QWidget> {\n\t\t\t\tconst auto active = Core::App().activeWindow();\n\t\t\t\tif (!active) {\n\t\t\t\t\treturn nullptr;\n\t\t\t\t}\n\n\t\t\t\treturn active->widget().get();\n\t\t\t}();\n\n\t\t\tconst auto window = std::make_shared<base::unique_qptr<QWidget>>(\n\t\t\t\tstd::in_place,\n\t\t\t\tparent);\n\n\t\t\tauto &raw = **window;\n\t\t\traw.setAttribute(Qt::WA_DontShowOnScreen);\n\t\t\traw.setWindowFlag(Qt::Window);\n\t\t\traw.setWindowModality(Qt::WindowModal);\n\t\t\traw.show();\n\n\t\t\tXdpRequest::RequestProxy::new_(\n\t\t\t\tproxy->get_connection(),\n\t\t\t\tGio::DBusProxyFlags::NONE_,\n\t\t\t\tbase::Platform::XDP::kService,\n\t\t\t\tbase::Platform::XDP::kObjectPath\n\t\t\t\t\t+ std::string(\"/request/\")\n\t\t\t\t\t+ uniqueName\n\t\t\t\t\t+ '/'\n\t\t\t\t\t+ handleToken,\n\t\t\t\tnullptr,\n\t\t\t\t[=](GObject::Object, Gio::AsyncResult res) mutable {\n\t\t\t\t\tauto requestProxy = XdpRequest::RequestProxy::new_finish(\n\t\t\t\t\t\tres);\n\n\t\t\t\t\tif (!requestProxy) {\n\t\t\t\t\t\tif (done) {\n\t\t\t\t\t\t\tGio::DBusErrorNS_::strip_remote_error(\n\t\t\t\t\t\t\t\trequestProxy.error());\n\t\t\t\t\t\t\tLOG((\"Portal Autostart Error: %1\").arg(\n\t\t\t\t\t\t\t\trequestProxy.error().message_().c_str()));\n\t\t\t\t\t\t\tdone(false);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tauto request = XdpRequest::Request(*requestProxy);\n\t\t\t\t\tconst auto signalId = std::make_shared<ulong>();\n\t\t\t\t\t*signalId = request.signal_response().connect([=](\n\t\t\t\t\t\t\tXdpRequest::Request,\n\t\t\t\t\t\t\tguint response,\n\t\t\t\t\t\t\tGLib::Variant) mutable {\n\t\t\t\t\t\tauto &sandbox = Core::Sandbox::Instance();\n\t\t\t\t\t\tsandbox.customEnterFromEventLoop([&] {\n\t\t\t\t\t\t\t(void)window; // don't destroy until finish\n\n\t\t\t\t\t\t\tif (response) {\n\t\t\t\t\t\t\t\tif (done) {\n\t\t\t\t\t\t\t\t\tLOG((\"Portal Autostart Error: \"\n\t\t\t\t\t\t\t\t\t\t\"Request denied\"));\n\t\t\t\t\t\t\t\t\tdone(false);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if (done) {\n\t\t\t\t\t\t\t\tdone(enabled);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\trequest.disconnect(*signalId);\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\n\t\t\t\t\tstd::vector<std::string> commandline;\n\t\t\t\t\tcommandline.push_back(executable.toStdString());\n\t\t\t\t\tif (Core::Launcher::Instance().customWorkingDir()) {\n\t\t\t\t\t\tcommandline.push_back(\"-workdir\");\n\t\t\t\t\t\tcommandline.push_back(cWorkingDir().toStdString());\n\t\t\t\t\t}\n\t\t\t\t\tcommandline.push_back(\"-autostart\");\n\n\t\t\t\t\tinterface.call_request_background(\n\t\t\t\t\t\tbase::Platform::XDP::ParentWindowID(parent\n\t\t\t\t\t\t\t? parent->windowHandle()\n\t\t\t\t\t\t\t: nullptr),\n\t\t\t\t\t\tGLib::Variant::new_array({\n\t\t\t\t\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\t\t\t\t\tGLib::Variant::new_string(\"handle_token\"),\n\t\t\t\t\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\t\t\t\t\tGLib::Variant::new_string(handleToken))),\n\t\t\t\t\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\t\t\t\t\tGLib::Variant::new_string(\"reason\"),\n\t\t\t\t\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\t\t\t\t\tGLib::Variant::new_string(\n\t\t\t\t\t\t\t\t\t\ttr::lng_settings_auto_start(tr::now)\n\t\t\t\t\t\t\t\t\t\t\t.toStdString()))),\n\t\t\t\t\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\t\t\t\t\tGLib::Variant::new_string(\"autostart\"),\n\t\t\t\t\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\t\t\t\t\tGLib::Variant::new_boolean(enabled))),\n\t\t\t\t\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\t\t\t\t\tGLib::Variant::new_string(\"commandline\"),\n\t\t\t\t\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\t\t\t\t\tGLib::Variant::new_strv(commandline))),\n\t\t\t\t\t\t\tGLib::Variant::new_dict_entry(\n\t\t\t\t\t\t\t\tGLib::Variant::new_string(\"dbus-activatable\"),\n\t\t\t\t\t\t\t\tGLib::Variant::new_variant(\n\t\t\t\t\t\t\t\t\tGLib::Variant::new_boolean(false))),\n\t\t\t\t\t\t}),\n\t\t\t\t\t\t[=](GObject::Object, Gio::AsyncResult res) mutable {\n\t\t\t\t\t\t\tauto &sandbox = Core::Sandbox::Instance();\n\t\t\t\t\t\t\tsandbox.customEnterFromEventLoop([&] {\n\t\t\t\t\t\t\t\tconst auto result =\n\t\t\t\t\t\t\t\t\tinterface.call_request_background_finish(\n\t\t\t\t\t\t\t\t\t\tres);\n\n\t\t\t\t\t\t\t\tif (!result) {\n\t\t\t\t\t\t\t\t\tif (done) {\n\t\t\t\t\t\t\t\t\t\tconst auto &error = result.error();\n\t\t\t\t\t\t\t\t\t\tGio::DBusErrorNS_::strip_remote_error(\n\t\t\t\t\t\t\t\t\t\t\terror);\n\t\t\t\t\t\t\t\t\t\tLOG((\"Portal Autostart Error: %1\").arg(\n\t\t\t\t\t\t\t\t\t\t\terror.message_().c_str()));\n\t\t\t\t\t\t\t\t\t\tdone(false);\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\trequest.disconnect(*signalId);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t});\n\t\t\t\t});\n\t\t});\n}\n\nbool GenerateDesktopFile(\n\t\tconst QString &targetPath,\n\t\tconst QStringList &args = {},\n\t\tbool onlyMainGroup = false,\n\t\tbool silent = false) {\n\tconst auto executable = ExecutablePathForShortcuts();\n\tif (targetPath.isEmpty() || executable.isEmpty()) {\n\t\treturn false;\n\t}\n\n\tDEBUG_LOG((\"App Info: placing .desktop file to %1\").arg(targetPath));\n\tif (!QDir(targetPath).exists()) QDir().mkpath(targetPath);\n\n\tconst auto sourceFile = u\":/misc/org.telegram.desktop.desktop\"_q;\n\tconst auto targetFile = targetPath\n\t\t+ QGuiApplication::desktopFileName()\n\t\t+ u\".desktop\"_q;\n\n\tconst auto sourceText = [&] {\n\t\tQFile source(sourceFile);\n\t\tif (source.open(QIODevice::ReadOnly)) {\n\t\t\treturn source.readAll().toStdString();\n\t\t}\n\t\treturn std::string();\n\t}();\n\n\tif (sourceText.empty()) {\n\t\tif (!silent) {\n\t\t\tLOG((\"App Error: Could not open '%1' for read\").arg(sourceFile));\n\t\t}\n\t\treturn false;\n\t}\n\n\tauto target = GLib::KeyFile::new_();\n\tconst auto loaded = target.load_from_data(\n\t\tsourceText,\n\t\t-1,\n\t\tGLib::KeyFileFlags::KEEP_COMMENTS_\n\t\t\t| GLib::KeyFileFlags::KEEP_TRANSLATIONS_);\n\n\tif (!loaded) {\n\t\tif (!silent) {\n\t\t\tLOG((\"App Error: %1\").arg(loaded.error().message_().c_str()));\n\t\t}\n\t\treturn false;\n\t}\n\n\tfor (const auto &group : target.get_groups(nullptr)) {\n\t\tif (onlyMainGroup && group != \"Desktop Entry\") {\n\t\t\tconst auto removed = target.remove_group(group);\n\t\t\tif (!removed) {\n\t\t\t\tif (!silent) {\n\t\t\t\t\tLOG((\"App Error: %1\").arg(\n\t\t\t\t\t\tremoved.error().message_().c_str()));\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (target.has_key(group, \"TryExec\", nullptr)) {\n\t\t\ttarget.set_string(\n\t\t\t\tgroup,\n\t\t\t\t\"TryExec\",\n\t\t\t\tKShell::joinArgs({ executable }).replace(\n\t\t\t\t\t'\\\\',\n\t\t\t\t\tqstr(\"\\\\\\\\\")).toStdString());\n\t\t}\n\n\t\tif (target.has_key(group, \"Exec\", nullptr)) {\n\t\t\tif (group == \"Desktop Entry\" && !args.isEmpty()) {\n\t\t\t\tQStringList exec;\n\t\t\t\texec.append(executable);\n\t\t\t\tif (Core::Launcher::Instance().customWorkingDir()) {\n\t\t\t\t\texec.append(u\"-workdir\"_q);\n\t\t\t\t\texec.append(cWorkingDir());\n\t\t\t\t}\n\t\t\t\texec.append(args);\n\t\t\t\ttarget.set_string(\n\t\t\t\t\tgroup,\n\t\t\t\t\t\"Exec\",\n\t\t\t\t\tKShell::joinArgs(exec).replace(\n\t\t\t\t\t\t'\\\\',\n\t\t\t\t\t\tqstr(\"\\\\\\\\\")).toStdString());\n\t\t\t} else {\n\t\t\t\tauto exec = KShell::splitArgs(\n\t\t\t\t\tQString::fromStdString(\n\t\t\t\t\t\ttarget.get_string(group, \"Exec\", nullptr)\n\t\t\t\t\t).replace(\n\t\t\t\t\t\tqstr(\"\\\\\\\\\"),\n\t\t\t\t\t\tqstr(\"\\\\\")));\n\n\t\t\t\tif (!exec.isEmpty()) {\n\t\t\t\t\texec[0] = executable;\n\t\t\t\t\tif (Core::Launcher::Instance().customWorkingDir()) {\n\t\t\t\t\t\texec.insert(1, u\"-workdir\"_q);\n\t\t\t\t\t\texec.insert(2, cWorkingDir());\n\t\t\t\t\t}\n\t\t\t\t\ttarget.set_string(\n\t\t\t\t\t\tgroup,\n\t\t\t\t\t\t\"Exec\",\n\t\t\t\t\t\tKShell::joinArgs(exec).replace(\n\t\t\t\t\t\t\t'\\\\',\n\t\t\t\t\t\t\tqstr(\"\\\\\\\\\")).toStdString());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!args.isEmpty()) {\n\t\ttarget.remove_key(\"Desktop Entry\", \"DBusActivatable\");\n\t}\n\n\tconst auto saved = target.save_to_file(targetFile.toStdString());\n\tif (!saved) {\n\t\tif (!silent) {\n\t\t\tLOG((\"App Error: %1\").arg(saved.error().message_().c_str()));\n\t\t}\n\t\treturn false;\n\t}\n\n\tQFile::setPermissions(\n\t\ttargetFile,\n\t\tQFile::permissions(targetFile)\n\t\t\t| QFileDevice::ExeOwner\n\t\t\t| QFileDevice::ExeGroup\n\t\t\t| QFileDevice::ExeOther);\n\n\tif (!Core::UpdaterDisabled()) {\n\t\tDEBUG_LOG((\"App Info: removing old .desktop files\"));\n\t\tQFile::remove(u\"%1telegram.desktop\"_q.arg(targetPath));\n\t\tQFile::remove(u\"%1telegramdesktop.desktop\"_q.arg(targetPath));\n\n\t\tconst auto appimagePath = u\"file://%1%2\"_q.arg(\n\t\t\tcExeDir(),\n\t\t\tcExeName()).toUtf8();\n\n\t\tchar md5Hash[33] = { 0 };\n\t\thashMd5Hex(\n\t\t\tappimagePath.constData(),\n\t\t\tappimagePath.size(),\n\t\t\tmd5Hash);\n\n\t\tQFile::remove(u\"%1appimagekit_%2-%3.desktop\"_q.arg(\n\t\t\ttargetPath,\n\t\t\tmd5Hash,\n\t\t\tAppName.utf16().replace(' ', '_')));\n\n\t\tconst auto d = QFile::encodeName(QDir(cWorkingDir()).absolutePath());\n\t\thashMd5Hex(d.constData(), d.size(), md5Hash);\n\n\t\tif (!Core::Launcher::Instance().customWorkingDir()) {\n\t\t\tQFile::remove(u\"%1org.telegram.desktop._%2.desktop\"_q.arg(\n\t\t\t\ttargetPath,\n\t\t\t\tmd5Hash));\n\n\t\t\tconst auto exePath = QFile::encodeName(\n\t\t\t\tcExeDir() + cExeName());\n\t\t\thashMd5Hex(exePath.constData(), exePath.size(), md5Hash);\n\t\t}\n\n\t\tQFile::remove(u\"%1org.telegram.desktop.%2.desktop\"_q.arg(\n\t\t\ttargetPath,\n\t\t\tmd5Hash));\n\t}\n\n\treturn true;\n}\n\nbool GenerateServiceFile(bool silent = false) {\n\tconst auto executable = ExecutablePathForShortcuts();\n\tif (executable.isEmpty()) {\n\t\treturn false;\n\t}\n\n\tconst auto targetPath = QStandardPaths::writableLocation(\n\t\tQStandardPaths::GenericDataLocation) + u\"/dbus-1/services/\"_q;\n\n\tconst auto targetFile = targetPath\n\t\t+ QGuiApplication::desktopFileName()\n\t\t+ u\".service\"_q;\n\n\tDEBUG_LOG((\"App Info: placing D-Bus service file to %1\").arg(targetPath));\n\tif (!QDir(targetPath).exists()) QDir().mkpath(targetPath);\n\n\tauto target = GLib::KeyFile::new_();\n\tconstexpr auto group = \"D-BUS Service\";\n\n\ttarget.set_string(\n\t\tgroup,\n\t\t\"Name\",\n\t\tQGuiApplication::desktopFileName().toStdString());\n\n\tQStringList exec;\n\texec.append(executable);\n\tif (Core::Launcher::Instance().customWorkingDir()) {\n\t\texec.append(u\"-workdir\"_q);\n\t\texec.append(cWorkingDir());\n\t}\n\ttarget.set_string(\n\t\tgroup,\n\t\t\"Exec\",\n\t\tKShell::joinArgs(exec).toStdString());\n\n\tconst auto saved = target.save_to_file(targetFile.toStdString());\n\tif (!saved) {\n\t\tif (!silent) {\n\t\t\tLOG((\"App Error: %1\").arg(saved.error().message_().c_str()));\n\t\t}\n\t\treturn false;\n\t}\n\n\tif (!Core::UpdaterDisabled()\n\t\t\t&& !Core::Launcher::Instance().customWorkingDir()) {\n\t\tDEBUG_LOG((\"App Info: removing old D-Bus service files\"));\n\n\t\tchar md5Hash[33] = { 0 };\n\t\tconst auto d = QFile::encodeName(QDir(cWorkingDir()).absolutePath());\n\t\thashMd5Hex(d.constData(), d.size(), md5Hash);\n\n\t\tQFile::remove(u\"%1org.telegram.desktop._%2.service\"_q.arg(\n\t\t\ttargetPath,\n\t\t\tmd5Hash));\n\t}\n\n\tXdgDBus::DBusProxy::new_for_bus(\n\t\tGio::BusType::SESSION_,\n\t\tGio::DBusProxyFlags::NONE_,\n\t\tbase::Platform::DBus::kService,\n\t\tbase::Platform::DBus::kObjectPath,\n\t\t[=](GObject::Object, Gio::AsyncResult res) {\n\t\t\tauto interface = XdgDBus::DBus(\n\t\t\t\tXdgDBus::DBusProxy::new_for_bus_finish(res, nullptr));\n\n\t\t\tif (!interface) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tinterface.call_reload_config(nullptr);\n\t\t});\n\n\treturn true;\n}\n\nvoid InstallLauncher() {\n\tstatic const auto DisabledByEnv = !qEnvironmentVariableIsEmpty(\n\t\t\"DESKTOPINTEGRATION\");\n\n\t// don't update desktop file for alpha version or if updater is disabled\n\tif (cAlphaVersion() || Core::UpdaterDisabled() || DisabledByEnv) {\n\t\treturn;\n\t}\n\n\tconst auto applicationsPath = QStandardPaths::writableLocation(\n\t\tQStandardPaths::ApplicationsLocation) + '/';\n\n\tGenerateDesktopFile(applicationsPath);\n\tGenerateServiceFile();\n\n\tconst auto icons = QStandardPaths::writableLocation(\n\t\tQStandardPaths::GenericDataLocation) + u\"/icons/\"_q;\n\n\tconst auto appIcons = icons + u\"/hicolor/256x256/apps/\"_q;\n\tif (!QDir(appIcons).exists()) QDir().mkpath(appIcons);\n\n\tconst auto icon = appIcons + ApplicationIconName() + u\".png\"_q;\n\tQFile::remove(icon);\n\tQFile::remove(icons + u\"telegram.png\"_q);\n\tif (QFile::copy(u\":/gui/art/forkgram/logo_256_no_margin.png\"_q, icon)) {\n\t\tDEBUG_LOG((\"App Info: Icon copied to '%1'\").arg(icon));\n\t}\n\n\tconst auto symbolicIcons = icons + u\"/hicolor/symbolic/apps/\"_q;\n\tif (!QDir().exists(symbolicIcons)) QDir().mkpath(symbolicIcons);\n\n\tconst auto monochromeIcons = {\n\t\tQString(),\n\t\tu\"attention\"_q,\n\t\tu\"mute\"_q,\n\t};\n\n\tfor (const auto &icon : monochromeIcons) {\n\t\tQFile::copy(\n\t\t\tu\":/gui/icons/tray/monochrome%1.svg\"_q.arg(\n\t\t\t\t!icon.isEmpty() ? u\"_\"_q + icon : QString()),\n\t\t\tsymbolicIcons\n\t\t\t\t+ ApplicationIconName()\n\t\t\t\t+ (!icon.isEmpty() ? u\"-\"_q + icon : QString())\n\t\t\t\t+ u\"-symbolic.svg\"_q);\n\t}\n\n\tQProcess::execute(\"update-desktop-database\", {\n\t\tapplicationsPath\n\t});\n}\n\n[[nodiscard]] QByteArray HashForSocketPath() {\n\tconstexpr auto kHashForSocketPathLength = 24;\n\n\tconst auto binary = openssl::Sha256(\n\t\tbytes::make_span(Core::Launcher::Instance().instanceHash()));\n\tconst auto base64 = QByteArray(\n\t\treinterpret_cast<const char*>(binary.data()),\n\t\tbinary.size()).toBase64(QByteArray::Base64UrlEncoding);\n\treturn base64.mid(0, kHashForSocketPathLength);\n}\n\nvoid AppInfoCheckScheme(\n\t\tconst std::string &scheme,\n\t\tFn<void(Gio::AppInfo, Fn<void()>)> callback,\n\t\tFn<void()> fail) {\n\t// TODO: use get_default_for_uri_scheme_async once we can use GLib 2.74\n\tif (auto appInfo = Gio::AppInfo::get_default_for_uri_scheme(scheme)) {\n\t\tcallback(appInfo, fail);\n\t\treturn;\n\t}\n\tfail();\n}\n\nvoid PortalCheckScheme(\n\t\tconst std::string &scheme,\n\t\tFn<void(Fn<void()>)> callback,\n\t\tFn<void()> fail) {\n\tXdpOpenURI::OpenURIProxy::new_for_bus(\n\t\tGio::BusType::SESSION_,\n\t\tGio::DBusProxyFlags::NONE_,\n\t\tbase::Platform::XDP::kService,\n\t\tbase::Platform::XDP::kObjectPath,\n\t\t[=](GObject::Object, Gio::AsyncResult res) {\n\t\t\tauto interface = XdpOpenURI::OpenURI(\n\t\t\t\tXdpOpenURI::OpenURIProxy::new_for_bus_finish(res, nullptr));\n\n\t\t\tif (!interface) {\n\t\t\t\tfail();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tinterface.call_scheme_supported(\n\t\t\t\tscheme,\n\t\t\t\tGLib::Variant::new_array(\n\t\t\t\t\tGLib::VariantType::new_(\"{sv}\"),\n\t\t\t\t\t{}),\n\t\t\t\t[=](GObject::Object, Gio::AsyncResult res) mutable {\n\t\t\t\t\tconst auto result\n\t\t\t\t\t\t= interface.call_scheme_supported_finish(res);\n\n\t\t\t\t\tif (!result || !std::get<1>(*result)) {\n\t\t\t\t\t\tfail();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tcallback(fail);\n\t\t\t\t});\n\t\t});\n}\n\n} // namespace\n\nnamespace Platform {\n\nvoid SetApplicationIcon(const QIcon &icon) {\n\tQApplication::setWindowIcon(icon);\n}\n\nQString SingleInstanceLocalServerName(const QString &hash) {\n#if defined Q_OS_LINUX && QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)\n\tif (KSandbox::isSnap()) {\n\t\treturn u\"snap.\"_q\n\t\t\t+ qEnvironmentVariable(\"SNAP_INSTANCE_NAME\")\n\t\t\t+ '.'\n\t\t\t+ hash;\n\t}\n\treturn hash + '-' + QCoreApplication::applicationName();\n#else // Q_OS_LINUX && Qt >= 6.2.0\n\treturn QDir::tempPath()\n\t\t+ '/'\n\t\t+ hash\n\t\t+ '-'\n\t\t+ QCoreApplication::applicationName();\n#endif // !Q_OS_LINUX || Qt < 6.2.0\n}\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)\nstd::optional<bool> IsDarkMode() {\n\tauto result = base::Platform::XDP::ReadSetting(\n\t\t\"org.freedesktop.appearance\",\n\t\t\"color-scheme\");\n\n\treturn result.has_value()\n\t\t? std::make_optional(result->get_uint32() == 1)\n\t\t: std::nullopt;\n}\n#endif // Qt < 6.5.0\n\nbool AutostartSupported() {\n\treturn true;\n}\n\nvoid AutostartToggle(bool enabled, Fn<void(bool)> done) {\n\tif (KSandbox::isFlatpak()) {\n\t\tPortalAutostart(enabled, done);\n\t\treturn;\n\t}\n\n\tconst auto success = [&] {\n\t\tconst auto autostart = QStandardPaths::writableLocation(\n\t\t\tQStandardPaths::GenericConfigLocation)\n\t\t\t+ u\"/autostart/\"_q;\n\n\t\tif (!enabled) {\n\t\t\treturn QFile::remove(\n\t\t\t\tautostart\n\t\t\t\t\t+ QGuiApplication::desktopFileName()\n\t\t\t\t\t+ u\".desktop\"_q);\n\t\t}\n\n\t\treturn GenerateDesktopFile(\n\t\t\tautostart,\n\t\t\t{ u\"-autostart\"_q },\n\t\t\ttrue,\n\t\t\t!done);\n\t}();\n\n\tif (done) {\n\t\tdone(enabled && success);\n\t}\n}\n\nbool AutostartSkip() {\n\treturn !cAutoStart();\n}\n\nbool TrayIconSupported() {\n\treturn QSystemTrayIcon::isSystemTrayAvailable();\n}\n\nbool SkipTaskbarSupported() {\n#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION\n\tif (IsX11()) {\n\t\treturn base::Platform::XCB::IsSupportedByWM(\n\t\t\tbase::Platform::XCB::Connection(),\n\t\t\t\"_NET_WM_STATE_SKIP_TASKBAR\");\n\t}\n#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION\n\n\treturn false;\n}\n\nQString ExecutablePathForShortcuts() {\n\tif (Core::UpdaterDisabled()) {\n\t\tconst auto &arguments = Core::Launcher::Instance().arguments();\n\t\tif (!arguments.isEmpty()) {\n\t\t\tconst auto result = QFileInfo(arguments.first()).fileName();\n\t\t\tif (!result.isEmpty()) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t\treturn cExeName();\n\t}\n\treturn cExeDir() + cExeName();\n}\n\n} // namespace Platform\n\nQString psAppDataPath() {\n\t// Previously we used ~/.TelegramDesktop, so look there first.\n\t// If we find data there, we should still use it.\n\tauto home = QDir::homePath();\n\tif (!home.isEmpty()) {\n\t\tauto oldPath = home + u\"/.TelegramDesktop/\"_q;\n\t\tauto oldSettingsBase = oldPath + u\"tdata/settings\"_q;\n\t\tif (QFile::exists(oldSettingsBase + '0')\n\t\t\t|| QFile::exists(oldSettingsBase + '1')\n\t\t\t|| QFile::exists(oldSettingsBase + 's')) {\n\t\t\treturn oldPath;\n\t\t}\n\t}\n\n\treturn QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + '/';\n}\n\nvoid psDoCleanup() {\n\ttry {\n\t\tPlatform::AutostartToggle(false);\n\t\tpsSendToMenu(false, true);\n\t} catch (...) {\n\t}\n}\n\nint psCleanup() {\n\tpsDoCleanup();\n\treturn 0;\n}\n\nvoid psDoFixPrevious() {\n}\n\nint psFixPrevious() {\n\tpsDoFixPrevious();\n\treturn 0;\n}\n\nnamespace Platform {\n\nvoid start() {\n\tQGuiApplication::setDesktopFileName([&] {\n\t\tif (KSandbox::isFlatpak()) {\n\t\t\treturn qEnvironmentVariable(\"FLATPAK_ID\");\n\t\t}\n\n\t\tif (KSandbox::isSnap()) {\n\t\t\treturn qEnvironmentVariable(\"SNAP_INSTANCE_NAME\")\n\t\t\t\t+ '_'\n\t\t\t\t+ cExeName();\n\t\t}\n\n\t\tif (!Core::UpdaterDisabled()) {\n\t\t\treturn u\"io.github.forkgram.tdesktop._%1\"_q.arg(\n\t\t\t\tCore::Launcher::Instance().instanceHash().constData());\n\t\t}\n\n\t\treturn u\"io.github.forkgram.tdesktop\"_q;\n\t}());\n\n\tLOG((\"App ID: %1\").arg(QGuiApplication::desktopFileName()));\n\n\tif (!qEnvironmentVariableIsSet(\"XDG_ACTIVATION_TOKEN\")\n\t\t&& qEnvironmentVariableIsSet(\"DESKTOP_STARTUP_ID\")) {\n\t\tqputenv(\"XDG_ACTIVATION_TOKEN\", qgetenv(\"DESKTOP_STARTUP_ID\"));\n\t}\n\n\tqputenv(\"PULSE_PROP_application.name\", AppName.utf8());\n\tqputenv(\n\t\t\"PULSE_PROP_application.icon_name\",\n\t\tApplicationIconName().toUtf8());\n\n\tGLib::set_prgname(cExeName().toStdString());\n\tGLib::set_application_name(AppNameF.data());\n\n\tWebview::WebKitGTK::SetSocketPath(u\"%1/%2-%3-webview-{}\"_q.arg(\n\t\tQDir::tempPath(),\n\t\tHashForSocketPath(),\n\t\tu\"TD\"_q).toStdString());\n\n\tInstallLauncher();\n}\n\nvoid finish() {\n}\n\nPermissionStatus GetPermissionStatus(PermissionType type) {\n\treturn PermissionStatus::Granted;\n}\n\nvoid RequestPermission(PermissionType type, Fn<void(PermissionStatus)> resultCallback) {\n\tresultCallback(PermissionStatus::Granted);\n}\n\nvoid OpenSystemSettingsForPermission(PermissionType type) {\n}\n\nbool OpenSystemSettings(SystemSettingsType type) {\n\tif (type == SystemSettingsType::Audio) {\n\t\tstruct Command {\n\t\t\tQString command;\n\t\t\tQStringList arguments;\n\t\t};\n\t\tauto options = std::vector<Command>();\n\t\tconst auto add = [&](const char *option, const char *arg = nullptr) {\n\t\t\tauto command = Command{ .command = option };\n\t\t\tif (arg) {\n\t\t\t\tcommand.arguments.push_back(arg);\n\t\t\t}\n\t\t\toptions.push_back(std::move(command));\n\t\t};\n\t\tadd(\"unity-control-center\", \"sound\");\n\t\tadd(\"kcmshell6\", \"kcm_pulseaudio\");\n\t\tadd(\"kcmshell5\", \"kcm_pulseaudio\");\n\t\tadd(\"kcmshell4\", \"phonon\");\n\t\tadd(\"gnome-control-center\", \"sound\");\n\t\tadd(\"cinnamon-settings\", \"sound\");\n\t\tadd(\"mate-volume-control\");\n\t\tadd(\"pavucontrol-qt\");\n\t\tadd(\"pavucontrol\");\n\t\tadd(\"alsamixergui\");\n\t\treturn ranges::any_of(options, [](const Command &command) {\n\t\t\treturn QProcess::startDetached(\n\t\t\t\tcommand.command,\n\t\t\t\tcommand.arguments);\n\t\t});\n\t}\n\treturn true;\n}\n\nvoid NewVersionLaunched(int oldVersion) {\n\tif (oldVersion <= 5014003 && cAutoStart()) {\n\t\tAutostartToggle(true);\n\t}\n}\n\nQImage DefaultApplicationIcon() {\n\treturn Window::Logo();\n}\n\nQString ApplicationIconName() {\n\tstatic const auto Result = KSandbox::isSnap()\n\t\t? u\"snap.%1.\"_q.arg(qEnvironmentVariable(\"SNAP_INSTANCE_NAME\"))\n\t\t: QGuiApplication::desktopFileName().remove(\n\t\tu\"._\"_q + Core::Launcher::Instance().instanceHash());\n\treturn Result;\n}\n\nvoid LaunchMaps(const Data::LocationPoint &point, Fn<void()> fail) {\n\tconst auto url = QUrl(\n\t\tu\"geo:%1,%2\"_q.arg(point.latAsString(), point.lonAsString()));\n\n\tAppInfoCheckScheme(url.scheme().toStdString(), [=](\n\t\t\tGio::AppInfo appInfo,\n\t\t\tFn<void()> fail) {\n\t\t// TODO: use launch_uris_async once we can use GLib 2.60\n\t\tif (!appInfo.launch_uris(\n\t\t\t\t{ url.toString().toStdString() },\n\t\t\t\tbase::Platform::AppLaunchContext(),\n\t\t\t\tnullptr)) {\n\t\t\tfail();\n\t\t}\n\t}, [=] {\n\t\tPortalCheckScheme(url.scheme().toStdString(), [=](Fn<void()> fail) {\n\t\t\tif (!QDesktopServices::openUrl(url)) {\n\t\t\t\tfail();\n\t\t\t}\n\t\t}, fail);\n\t});\n}\n\nnamespace ThirdParty {\n\nvoid start() {\n}\n\n} // namespace ThirdParty\n\n} // namespace Platform\n\nvoid psSendToMenu(bool send, bool silent) {\n}\n\nbool linuxMoveFile(const char *from, const char *to) {\n\tFILE *ffrom = fopen(from, \"rb\"), *fto = fopen(to, \"wb\");\n\tif (!ffrom) {\n\t\tif (fto) fclose(fto);\n\t\treturn false;\n\t}\n\tif (!fto) {\n\t\tfclose(ffrom);\n\t\treturn false;\n\t}\n\tstatic const int BufSize = 65536;\n\tchar buf[BufSize];\n\twhile (size_t size = fread(buf, 1, BufSize, ffrom)) {\n\t\tfwrite(buf, 1, size, fto);\n\t}\n\n\tstruct stat fst; // from http://stackoverflow.com/questions/5486774/keeping-fileowner-and-permissions-after-copying-file-in-c\n\t//let's say this wont fail since you already worked OK on that fp\n\tif (fstat(fileno(ffrom), &fst) != 0) {\n\t\tfclose(ffrom);\n\t\tfclose(fto);\n\t\treturn false;\n\t}\n\t//update to the same uid/gid\n\tif (fchown(fileno(fto), fst.st_uid, fst.st_gid) != 0) {\n\t\tfclose(ffrom);\n\t\tfclose(fto);\n\t\treturn false;\n\t}\n\t//update the permissions\n\tif (fchmod(fileno(fto), fst.st_mode) != 0) {\n\t\tfclose(ffrom);\n\t\tfclose(fto);\n\t\treturn false;\n\t}\n\n\tfclose(ffrom);\n\tfclose(fto);\n\n\tif (unlink(from)) {\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/specific_linux.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_specific.h\"\n\nnamespace Platform {\n\ninline void IgnoreApplicationActivationRightNow() {\n}\n\ninline void WriteCrashDumpDetails() {\n}\n\ninline void AutostartRequestStateFromSystem(Fn<void(bool)> callback) {\n}\n\ninline bool PreventsQuit(Core::QuitReason reason) {\n\treturn false;\n}\n\ninline void ActivateThisProcess() {\n}\n\ninline uint64 ActivationWindowId(not_null<QWidget*> window) {\n\treturn 1;\n}\n\ninline void ActivateOtherProcess(uint64 processId, uint64 windowId) {\n}\n\n} // namespace Platform\n\ninline void psCheckLocalSocket(const QString &serverName) {\n\tQFile address(serverName);\n\tif (address.exists()) {\n\t\taddress.remove();\n\t}\n}\n\nQString psAppDataPath();\nvoid psSendToMenu(bool send, bool silent = false);\n\nint psCleanup();\nint psFixPrevious();\n\ninline QByteArray psDownloadPathBookmark(const QString &path) {\n\treturn QByteArray();\n}\ninline void psDownloadPathEnableAccess() {\n}\n\nbool linuxMoveFile(const char *from, const char *to);\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/text_recognition_linux.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_text_recognition.h\"\n\nnamespace Platform {\nnamespace TextRecognition {\n\ninline bool IsAvailable() {\n\treturn false;\n}\n\ninline Result RecognizeText(const QImage &image) {\n\treturn {};\n}\n\n} // namespace TextRecognition\n} // namespace Platform"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/translate_provider_linux.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/linux/translate_provider_linux.h\"\n\n#include \"spellcheck/platform/platform_language.h\"\n\n#include <QtCore/QStandardPaths>\n#include <QtCore/QProcess>\n#include <QtGui/QTextDocument>\n\nnamespace Platform {\nnamespace {\n\n[[nodiscard]] QString Command() {\n\tconst auto commands = {\n\t\tu\"crow\"_q,\n\t\tu\"org.kde.CrowTranslate\"_q,\n\t};\n\tconst auto it = ranges::find_if(commands, [](const auto &command) {\n\t\treturn !QStandardPaths::findExecutable(command).isEmpty();\n\t});\n\treturn it != end(commands) ? *it : QString();\n}\n\nclass TranslateProvider final : public QObject, public Ui::TranslateProvider {\npublic:\n\t[[nodiscard]] bool supportsMessageId() const override {\n\t\treturn false;\n\t}\n\n\tvoid request(\n\t\t\tUi::TranslateProviderRequest request,\n\t\t\tLanguageId to,\n\t\t\tFn<void(Ui::TranslateProviderResult)> done) override {\n\t\tconst auto from = Platform::Language::Recognize(request.text.text);\n\t\tconst auto process = QPointer(new QProcess(this));\n\t\tprocess->setProgram(Command());\n\t\tprocess->setArguments(\n\t\t\tQStringList{ u\"-i\"_q, u\"-b\"_q, u\"-t\"_q, to.twoLetterCode() }\n\t\t\t\t+ (from.known()\n\t\t\t\t\t? QStringList{ u\"-s\"_q, from.twoLetterCode() }\n\t\t\t\t\t: QStringList()));\n\t\tconnect(process, &QProcess::finished, this, [=] {\n\t\t\t_document.setHtml(process->readAllStandardOutput());\n\t\t\tconst auto text = _document.toPlainText();\n\t\t\tdone(!text.isEmpty()\n\t\t\t\t? Ui::TranslateProviderResult{\n\t\t\t\t\t.text = TextWithEntities{ .text = text },\n\t\t\t\t}\n\t\t\t\t: Ui::TranslateProviderResult{\n\t\t\t\t\t.error = Ui::TranslateProviderError::Unknown,\n\t\t\t\t}\n\t\t\t);\n\t\t\tdelete process;\n\t\t});\n\t\tprocess->start();\n\t\tprocess->write(request.text.text.toUtf8());\n\t\tprocess->closeWriteChannel();\n\t}\n\nprivate:\n\tQTextDocument _document;\n};\n\n} // namespace\n\nstd::unique_ptr<Ui::TranslateProvider> CreateTranslateProvider() {\n\treturn std::make_unique<TranslateProvider>();\n}\n\nbool IsTranslateProviderAvailable() {\n\treturn !Command().isEmpty();\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/translate_provider_linux.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_translate_provider.h\"\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/tray_linux.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/linux/tray_linux.h\"\n\n#include \"base/invoke_queued.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"base/platform/linux/base_linux_dbus_utilities.h\"\n#include \"core/application.h\"\n#include \"core/sandbox.h\"\n#include \"platform/platform_specific.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"window/window_controller.h\"\n#include \"styles/style_window.h\"\n\n#include <QtCore/QCoreApplication>\n#include <QtWidgets/QMenu>\n#include <QtWidgets/QSystemTrayIcon>\n\n#include <gio/gio.hpp>\n\nnamespace Platform {\nnamespace {\n\nusing namespace gi::repository;\n\n[[nodiscard]] QString PanelIconName(int counter, bool muted) {\n\treturn ApplicationIconName() + ((counter > 0)\n\t\t? (muted\n\t\t\t? u\"-mute\"_q\n\t\t\t: u\"-attention\"_q)\n\t\t: QString()) + u\"-symbolic\"_q;\n}\n\n} // namespace\n\nclass IconGraphic final {\npublic:\n\texplicit IconGraphic();\n\t~IconGraphic();\n\n\tvoid updateState();\n\t[[nodiscard]] bool isRefreshNeeded() const;\n\t[[nodiscard]] QIcon trayIcon();\n\nprivate:\n\tstruct State {\n\t\tQIcon systemIcon;\n\t\tQString iconThemeName;\n\t\tbool monochrome = false;\n\t\tint32 counter = 0;\n\t\tbool muted = false;\n\t};\n\n\t[[nodiscard]] QIcon systemIcon() const;\n\t[[nodiscard]] bool isCounterNeeded(const State &state) const;\n\t[[nodiscard]] int counterSlice(int counter) const;\n\t[[nodiscard]] QSize dprSize(const QImage &image) const;\n\n\tconst int _iconSizes[7];\n\n\tbase::flat_map<int, QImage> _imageBack;\n\tQIcon _trayIcon;\n\tState _current;\n\tState _new;\n\n};\n\nIconGraphic::IconGraphic()\n: _iconSizes{ 16, 22, 32, 48, 64, 128, 256 } {\n\tupdateState();\n}\n\nIconGraphic::~IconGraphic() = default;\n\nQIcon IconGraphic::systemIcon() const {\n\tif (_new.iconThemeName == _current.iconThemeName\n\t\t&& _new.monochrome == _current.monochrome\n\t\t&& (_new.counter > 0) == (_current.counter > 0)\n\t\t&& _new.muted == _current.muted) {\n\t\treturn _current.systemIcon;\n\t}\n\n\tconst auto candidates = {\n\t\t_new.monochrome ? PanelIconName(_new.counter, _new.muted) : QString(),\n\t\tApplicationIconName(),\n\t};\n\n\tfor (const auto &candidate : candidates) {\n\t\tif (candidate.isEmpty()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto icon = QIcon::fromTheme(candidate, QIcon());\n\t\tif (icon.name() == candidate) {\n\t\t\treturn icon;\n\t\t}\n\t}\n\n\treturn QIcon();\n}\n\nbool IconGraphic::isCounterNeeded(const State &state) const {\n\treturn state.systemIcon.name() != PanelIconName(\n\t\tstate.counter,\n\t\tstate.muted);\n}\n\nint IconGraphic::counterSlice(int counter) const {\n\treturn (counter >= 100)\n\t\t? (100 + (counter % 10))\n\t\t: counter;\n}\n\nQSize IconGraphic::dprSize(const QImage &image) const {\n\treturn image.size() / image.devicePixelRatio();\n}\n\nvoid IconGraphic::updateState() {\n\t_new.iconThemeName = QIcon::themeName();\n\t_new.monochrome = Core::App().settings().trayIconMonochrome();\n\t_new.counter = Core::App().unreadBadge();\n\t_new.muted = Core::App().unreadBadgeMuted();\n\t_new.systemIcon = systemIcon();\n}\n\nbool IconGraphic::isRefreshNeeded() const {\n\treturn _trayIcon.isNull()\n\t\t|| _new.iconThemeName != _current.iconThemeName\n\t\t|| _new.systemIcon.name() != _current.systemIcon.name()\n\t\t|| (isCounterNeeded(_new)\n\t\t\t? _new.muted != _current.muted\n\t\t\t\t|| counterSlice(_new.counter) != counterSlice(\n\t\t\t\t\t\t_current.counter)\n\t\t\t: false);\n}\n\nQIcon IconGraphic::trayIcon() {\n\tif (!isRefreshNeeded()) {\n\t\treturn _trayIcon;\n\t}\n\n\tconst auto guard = gsl::finally([&] {\n\t\t_current = _new;\n\t});\n\n\tif (!isCounterNeeded(_new)) {\n\t\t_trayIcon = _new.systemIcon;\n\t\treturn _trayIcon;\n\t}\n\n\tQIcon result;\n\tfor (const auto iconSize : _iconSizes) {\n\t\tauto &currentImageBack = _imageBack[iconSize];\n\t\tconst auto desiredSize = QSize(iconSize, iconSize);\n\n\t\tif (currentImageBack.isNull()\n\t\t\t|| _new.iconThemeName != _current.iconThemeName\n\t\t\t|| _new.systemIcon.name() != _current.systemIcon.name()) {\n\t\t\tcurrentImageBack = {};\n\n\t\t\tif (!_new.systemIcon.isNull()) {\n\t\t\t\t// We can't use QIcon::actualSize here\n\t\t\t\t// since it works incorrectly with svg icon themes\n\t\t\t\tcurrentImageBack = _new.systemIcon\n\t\t\t\t\t.pixmap(desiredSize)\n\t\t\t\t\t.toImage();\n\n\t\t\t\tconst auto firstAttemptSize = dprSize(currentImageBack);\n\n\t\t\t\t// if current icon theme is not a svg one, Qt can return\n\t\t\t\t// a pixmap that less in size even if there are a bigger one\n\t\t\t\tif (firstAttemptSize.width() < desiredSize.width()) {\n\t\t\t\t\tconst auto availableSizes\n\t\t\t\t\t\t= _new.systemIcon.availableSizes();\n\n\t\t\t\t\tconst auto biggestSize = ranges::max_element(\n\t\t\t\t\t\tavailableSizes,\n\t\t\t\t\t\tstd::less<>(),\n\t\t\t\t\t\t&QSize::width);\n\n\t\t\t\t\tif (biggestSize->width() > firstAttemptSize.width()) {\n\t\t\t\t\t\tcurrentImageBack = _new.systemIcon\n\t\t\t\t\t\t\t.pixmap(*biggestSize)\n\t\t\t\t\t\t\t.toImage();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (currentImageBack.isNull()) {\n\t\t\t\tcurrentImageBack = Window::Logo();\n\t\t\t}\n\n\t\t\tif (dprSize(currentImageBack) != desiredSize) {\n\t\t\t\tcurrentImageBack = currentImageBack.scaled(\n\t\t\t\t\tdesiredSize * currentImageBack.devicePixelRatio(),\n\t\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\t\tQt::SmoothTransformation);\n\t\t\t}\n\t\t}\n\n\t\tresult.addPixmap(Ui::PixmapFromImage(_new.counter > 0\n\t\t\t? Window::WithSmallCounter(std::move(currentImageBack), {\n\t\t\t\t.size = iconSize,\n\t\t\t\t.count = _new.counter,\n\t\t\t\t.bg = _new.muted ? st::trayCounterBgMute : st::trayCounterBg,\n\t\t\t\t.fg = st::trayCounterFg,\n\t\t\t}) : std::move(currentImageBack)));\n\t}\n\n\t_trayIcon = result;\n\treturn _trayIcon;\n}\n\nclass TrayEventFilter final : public QObject {\npublic:\n\tTrayEventFilter(not_null<QObject*> parent);\n\n\t[[nodiscard]] rpl::producer<> contextMenuFilters() const;\n\nprotected:\n\tbool eventFilter(QObject *watched, QEvent *event) override;\n\nprivate:\n\tconst QString _iconObjectName;\n\trpl::event_stream<> _contextMenuFilters;\n\n};\n\nTrayEventFilter::TrayEventFilter(not_null<QObject*> parent)\n: QObject(parent)\n, _iconObjectName(\"QSystemTrayIconSys\") {\n\tparent->installEventFilter(this);\n}\n\nbool TrayEventFilter::eventFilter(QObject *obj, QEvent *event) {\n\tif (event->type() == QEvent::MouseButtonPress\n\t\t&& obj->objectName() == _iconObjectName) {\n\t\tconst auto m = static_cast<QMouseEvent*>(event);\n\t\tif (m->button() == Qt::RightButton) {\n\t\t\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t\t\t_contextMenuFilters.fire({});\n\t\t\t});\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nrpl::producer<> TrayEventFilter::contextMenuFilters() const {\n\treturn _contextMenuFilters.events();\n}\n\nTray::Tray() {\n\tauto connection = Gio::bus_get_sync(Gio::BusType::SESSION_, nullptr);\n\tif (connection) {\n\t\t_sniWatcher = std::make_unique<base::Platform::DBus::ServiceWatcher>(\n\t\t\tconnection.gobj_(),\n\t\t\t\"org.kde.StatusNotifierWatcher\",\n\t\t\t[=](\n\t\t\t\t\tconst std::string &service,\n\t\t\t\t\tconst std::string &oldOwner,\n\t\t\t\t\tconst std::string &newOwner) {\n\t\t\t\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t\t\t\tif (hasIcon()) {\n\t\t\t\t\t\tdestroyIcon();\n\t\t\t\t\t\tcreateIcon();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t});\n\t}\n}\n\nvoid Tray::createIcon() {\n\tif (!_icon) {\n\t\tLOG((\"System tray available: %1\").arg(Logs::b(TrayIconSupported())));\n\n\t\tif (!_iconGraphic) {\n\t\t\t_iconGraphic = std::make_unique<IconGraphic>();\n\t\t}\n\n\t\tconst auto showCustom = [=] {\n\t\t\t_aboutToShowRequests.fire({});\n\t\t\tInvokeQueued(_menuCustom.get(), [=] {\n\t\t\t\t_menuCustom->popup(QCursor::pos());\n\t\t\t});\n\t\t};\n\n\t\t_icon = base::make_unique_q<QSystemTrayIcon>(nullptr);\n\t\t_icon->setIcon(_iconGraphic->trayIcon());\n\t\t_icon->setToolTip(AppNameF.utf16());\n\n\t\tusing Reason = QSystemTrayIcon::ActivationReason;\n\t\tbase::qt_signal_producer(\n\t\t\t_icon.get(),\n\t\t\t&QSystemTrayIcon::activated\n\t\t) | rpl::on_next([=](Reason reason) {\n\t\t\tif (reason == QSystemTrayIcon::Context) {\n\t\t\t\tshowCustom();\n\t\t\t} else {\n\t\t\t\t_iconClicks.fire({});\n\t\t\t}\n\t\t}, _lifetime);\n\n\t\t_icon->setContextMenu(_menu.get());\n\n\t\tif (!_eventFilter) {\n\t\t\t_eventFilter = base::make_unique_q<TrayEventFilter>(\n\t\t\t\tQCoreApplication::instance());\n\t\t\t_eventFilter->contextMenuFilters(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tshowCustom();\n\t\t\t}, _lifetime);\n\t\t}\n\t}\n\tupdateIcon();\n\n\t_icon->show();\n}\n\nvoid Tray::destroyIcon() {\n\t_icon = nullptr;\n}\n\nvoid Tray::updateIcon() {\n\tif (!_icon || !_iconGraphic) {\n\t\treturn;\n\t}\n\n\t_iconGraphic->updateState();\n\tif (_iconGraphic->isRefreshNeeded()) {\n\t\t_icon->setIcon(_iconGraphic->trayIcon());\n\t}\n}\n\nvoid Tray::createMenu() {\n\tif (!_menu) {\n\t\t_menu = base::make_unique_q<QMenu>(nullptr);\n\t}\n\tif (!_menuCustom) {\n\t\t_menuCustom = base::make_unique_q<Ui::PopupMenu>(nullptr);\n\t\t_menuCustom->deleteOnHide(false);\n\t}\n}\n\nvoid Tray::destroyMenu() {\n\t_menuCustom = nullptr;\n\tif (_menu) {\n\t\t_menu->clear();\n\t}\n\t_actionsLifetime.destroy();\n}\n\nvoid Tray::addAction(rpl::producer<QString> text, Fn<void()> &&callback) {\n\tif (_menuCustom) {\n\t\tconst auto action = _menuCustom->addAction(QString(), callback);\n\t\trpl::duplicate(\n\t\t\ttext\n\t\t) | rpl::on_next([=](const QString &text) {\n\t\t\taction->setText(text);\n\t\t}, _actionsLifetime);\n\t}\n\n\tif (_menu) {\n\t\tconst auto action = _menu->addAction(QString(), std::move(callback));\n\t\tstd::move(\n\t\t\ttext\n\t\t) | rpl::on_next([=](const QString &text) {\n\t\t\taction->setText(text);\n\t\t}, _actionsLifetime);\n\t}\n}\n\nvoid Tray::showTrayMessage() const {\n}\n\nbool Tray::hasTrayMessageSupport() const {\n\treturn false;\n}\n\nrpl::producer<> Tray::aboutToShowRequests() const {\n\treturn rpl::merge(\n\t\t_aboutToShowRequests.events(),\n\t\t_menu\n\t\t\t? base::qt_signal_producer(_menu.get(), &QMenu::aboutToShow)\n\t\t\t: rpl::never<>() | rpl::type_erased);\n}\n\nrpl::producer<> Tray::showFromTrayRequests() const {\n\treturn rpl::never<>();\n}\n\nrpl::producer<> Tray::hideToTrayRequests() const {\n\treturn rpl::never<>();\n}\n\nrpl::producer<> Tray::iconClicks() const {\n\treturn _iconClicks.events();\n}\n\nbool Tray::hasIcon() const {\n\treturn _icon;\n}\n\nrpl::lifetime &Tray::lifetime() {\n\treturn _lifetime;\n}\n\nTray::~Tray() = default;\n\nbool HasMonochromeSetting() {\n\treturn QIcon::hasThemeIcon(\n\t\tPanelIconName(\n\t\t\tCore::App().unreadBadge(),\n\t\t\tCore::App().unreadBadgeMuted()));\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/tray_linux.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_tray.h\"\n\n#include \"base/unique_qptr.h\"\n\nnamespace base::Platform::DBus {\nclass ServiceWatcher;\n} // namespace base::Platform::DBus\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nclass QMenu;\nclass QSystemTrayIcon;\n\nnamespace Platform {\n\nclass IconGraphic;\nclass TrayEventFilter;\n\nclass Tray final {\npublic:\n\tTray();\n\t~Tray();\n\n\t[[nodiscard]] rpl::producer<> aboutToShowRequests() const;\n\t[[nodiscard]] rpl::producer<> showFromTrayRequests() const;\n\t[[nodiscard]] rpl::producer<> hideToTrayRequests() const;\n\t[[nodiscard]] rpl::producer<> iconClicks() const;\n\n\t[[nodiscard]] bool hasIcon() const;\n\n\tvoid createIcon();\n\tvoid destroyIcon();\n\n\tvoid updateIcon();\n\n\tvoid createMenu();\n\tvoid destroyMenu();\n\n\tvoid addAction(rpl::producer<QString> text, Fn<void()> &&callback);\n\n\tvoid showTrayMessage() const;\n\t[[nodiscard]] bool hasTrayMessageSupport() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tstd::unique_ptr<base::Platform::DBus::ServiceWatcher> _sniWatcher;\n\tstd::unique_ptr<IconGraphic> _iconGraphic;\n\n\tbase::unique_qptr<QSystemTrayIcon> _icon;\n\tbase::unique_qptr<QMenu> _menu;\n\tbase::unique_qptr<Ui::PopupMenu> _menuCustom;\n\n\tbase::unique_qptr<TrayEventFilter> _eventFilter;\n\n\trpl::event_stream<> _iconClicks;\n\trpl::event_stream<> _aboutToShowRequests;\n\n\trpl::lifetime _actionsLifetime;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/linux/webauthn_linux.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\n#include \"platform/platform_webauthn.h\"\n\nnamespace Platform::WebAuthn {\n\nbool IsSupported() {\n\treturn false;\n}\n\nvoid RegisterKey(\n\t\tconst Data::Passkey::RegisterData &data,\n\t\tFn<void(RegisterResult result)> callback) {\n\tcallback({});\n}\n\nvoid Login(\n\t\tconst Data::Passkey::LoginData &data,\n\t\tFn<void(LoginResult result)> callback) {\n\tcallback({});\n}\n\n} // namespace Platform::WebAuthn\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/current_geo_location_mac.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_current_geo_location.h\"\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/current_geo_location_mac.h\"\n\n#include \"base/platform/mac/base_utilities_mac.h\"\n#include \"core/current_geo_location.h\"\n\n#include <CoreLocation/CoreLocation.h>\n\n@interface LocationDelegate : NSObject<CLLocationManagerDelegate>\n\n- (id) initWithCallback:(Fn<void(Core::GeoLocation)>)callback;\n- (void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations;\n- (void) locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error;\n- (void) locationManager:(CLLocationManager *) manager didChangeAuthorizationStatus:(CLAuthorizationStatus) status;\n- (void) dealloc;\n\n@end\n\n@implementation LocationDelegate {\nCLLocationManager *_manager;\nFn<void(Core::GeoLocation)> _callback;\n}\n\n- (void) fail {\n\t[_manager stopUpdatingLocation];\n\n\tconst auto onstack = _callback;\n\t[self release];\n\n\tonstack({});\n}\n\n- (void) processWithStatus:(CLAuthorizationStatus)status {\n\tswitch (status) {\n\tcase kCLAuthorizationStatusNotDetermined:\n\t\tif (@available(macOS 10.15, *)) {\n\t\t\t[_manager requestWhenInUseAuthorization];\n\t\t} else {\n\t\t\t[_manager startUpdatingLocation];\n\t\t}\n\t\tbreak;\n\tcase kCLAuthorizationStatusAuthorizedAlways:\n\t\t[_manager startUpdatingLocation];\n\t\treturn;\n\tcase kCLAuthorizationStatusRestricted:\n\tcase kCLAuthorizationStatusDenied:\n\tdefault:\n\t\t[self fail];\n\t\treturn;\n\t}\n}\n\n- (id) initWithCallback:(Fn<void(Core::GeoLocation)>)callback {\n\tif (self = [super init]) {\n\t\t_callback = std::move(callback);\n\t\t_manager = [[CLLocationManager alloc] init];\n\t\t_manager.desiredAccuracy = kCLLocationAccuracyThreeKilometers;\n\t\t_manager.delegate = self;\n\t\tif ([CLLocationManager locationServicesEnabled]) {\n\t\t\tif (@available(macOS 11, *)) {\n\t\t\t\t[self processWithStatus:[_manager authorizationStatus]];\n\t\t\t} else {\n\t\t\t\t[self processWithStatus:[CLLocationManager authorizationStatus]];\n\t\t\t}\n\t\t} else {\n\t\t\t[self fail];\n\t\t}\n\t}\n\treturn self;\n}\n\n- (void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation*>*)locations {\n\t[_manager stopUpdatingLocation];\n\n\tauto result = Core::GeoLocation();\n\tif ([locations count] > 0) {\n\t\tconst auto coordinate = [locations lastObject].coordinate;\n\t\tresult.accuracy = Core::GeoLocationAccuracy::Exact;\n\t\tresult.point = QPointF(coordinate.latitude, coordinate.longitude);\n\t}\n\n\tconst auto onstack = _callback;\n\t[self release];\n\n\tonstack(result);\n}\n\n- (void) locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {\n\tif (error.code != kCLErrorLocationUnknown) {\n\t\t[self fail];\n\t}\n}\n\n- (void) locationManager:(CLLocationManager *) manager didChangeAuthorizationStatus:(CLAuthorizationStatus) status {\n\t[self processWithStatus:status];\n}\n\n- (void) dealloc {\n\tif (_manager) {\n\t\t_manager.delegate = nil;\n\t\t[_manager release];\n\t}\n\t[super dealloc];\n}\n\n@end\n\nnamespace Platform {\n\nvoid ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {\n\t[[LocationDelegate alloc] initWithCallback:std::move(callback)];\n}\n\nvoid ResolveLocationAddress(\n\t\tconst Core::GeoLocation &location,\n\t\tconst QString &language,\n\t\tFn<void(Core::GeoAddress)> callback) {\n\tCLGeocoder *geocoder = [[CLGeocoder alloc] init];\n\tCLLocation *request = [[CLLocation alloc]\n\t\tinitWithLatitude:location.point.x()\n\t\tlongitude:location.point.y()];\n\t[geocoder reverseGeocodeLocation:request completionHandler:^(\n\t\t\tNSArray<CLPlacemark*> * __nullable placemarks,\n\t\t\tNSError * __nullable error) {\n\t\tif (placemarks && [placemarks count] > 0) {\n\t\t\tCLPlacemark *placemark = [placemarks firstObject];\n\t\t\tauto list = QStringList();\n\t\t\tconst auto push = [&](NSString *text) {\n\t\t\t\tif (text) {\n\t\t\t\t\tconst auto qt = NS2QString(text);\n\t\t\t\t\tif (!qt.isEmpty()) {\n\t\t\t\t\t\tlist.push_back(qt);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\tpush([placemark thoroughfare]);\n\t\t\tpush([placemark locality]);\n\t\t\tpush([placemark country]);\n\t\t\tcallback({ .name = list.join(u\", \"_q) });\n\t\t} else {\n\t\t\tcallback({});\n\t\t}\n\t\t[geocoder release];\n\t}];\n\t[request release];\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/file_bookmark_mac.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Platform {\n\nclass FileBookmark final {\npublic:\n\tFileBookmark(const QByteArray &bookmark);\n\t~FileBookmark();\n\n\t[[nodiscard]] bool check() const;\n\tbool enable() const;\n\tvoid disable() const;\n\t[[nodiscard]] const QString &name(const QString &original) const;\n\t[[nodiscard]] QByteArray bookmark() const;\n\nprivate:\n#ifdef OS_MAC_STORE\n\tstruct Data;\n\tData *data = nullptr;\n#endif // OS_MAC_STORE\n\n};\n\n[[nodiscard]] QByteArray PathBookmark(const QString &path);\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/file_bookmark_mac.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/file_bookmark_mac.h\"\n\n#include \"base/platform/mac/base_utilities_mac.h\"\n#include \"logs.h\"\n\n#include <QtCore/QMutex>\n\n#include <Cocoa/Cocoa.h>\n#include <CoreFoundation/CFURL.h>\n\nnamespace Platform {\nnamespace {\n\n#ifdef OS_MAC_STORE\nQMutex BookmarksMutex;\n#endif // OS_MAC_STORE\n\n} // namespace\n\n#ifdef OS_MAC_STORE\nstruct FileBookmark::Data {\n\t~Data() {\n\t\tif (url) [url release];\n\t}\n\tNSURL *url = nil;\n\tQString name;\n\tQByteArray bookmark;\n\tint counter = 0;\n};\n#endif // OS_MAC_STORE\n\nFileBookmark::FileBookmark(const QByteArray &bookmark) {\n#ifdef OS_MAC_STORE\n\tif (bookmark.isEmpty()) return;\n\n\tBOOL isStale = NO;\n\tNSError *error = nil;\n\tNSURL *url = [NSURL URLByResolvingBookmarkData:bookmark.toNSData() options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error];\n\tif (!url) return;\n\n\tif ([url startAccessingSecurityScopedResource]) {\n\t\tdata = new Data();\n\t\tdata->url = [url retain];\n\t\tdata->name = NS2QString([url path]);\n\t\tdata->bookmark = bookmark;\n\t\t[url stopAccessingSecurityScopedResource];\n\t}\n#endif // OS_MAC_STORE\n}\n\nbool FileBookmark::check() const {\n\tif (enable()) {\n\t\tdisable();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool FileBookmark::enable() const {\n#ifndef OS_MAC_STORE\n\treturn true;\n#else // OS_MAC_STORE\n\tif (!data) return false;\n\n\tQMutexLocker lock(&BookmarksMutex);\n\tif (data->counter > 0 || [data->url startAccessingSecurityScopedResource] == YES) {\n\t\t++data->counter;\n\t\treturn true;\n\t}\n\treturn false;\n#endif // OS_MAC_STORE\n}\n\nvoid FileBookmark::disable() const {\n#ifdef OS_MAC_STORE\n\tif (!data) return;\n\n\tQMutexLocker lock(&BookmarksMutex);\n\tif (data->counter > 0) {\n\t\t--data->counter;\n\t\tif (!data->counter) {\n\t\t\t[data->url stopAccessingSecurityScopedResource];\n\t\t}\n\t}\n#endif // OS_MAC_STORE\n}\n\nconst QString &FileBookmark::name(const QString &original) const {\n#ifndef OS_MAC_STORE\n\treturn original;\n#else // OS_MAC_STORE\n\treturn (data && !data->name.isEmpty()) ? data->name : original;\n#endif // OS_MAC_STORE\n}\n\nQByteArray FileBookmark::bookmark() const {\n#ifndef OS_MAC_STORE\n\treturn QByteArray();\n#else // OS_MAC_STORE\n\treturn data ? data->bookmark : QByteArray();\n#endif // OS_MAC_STORE\n}\n\nFileBookmark::~FileBookmark() {\n#ifdef OS_MAC_STORE\n\tif (data && data->counter > 0) {\n\t\tLOG((\"Did not disable() bookmark, counter: %1\").arg(data->counter));\n\t\t[data->url stopAccessingSecurityScopedResource];\n\t}\n#endif // OS_MAC_STORE\n}\n\nQByteArray PathBookmark(const QString &path) {\n#ifndef OS_MAC_STORE\n\treturn QByteArray();\n#else // OS_MAC_STORE\n\tNSURL *url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:path.toUtf8().constData()]];\n\tif (!url) return QByteArray();\n\n\tNSError *error = nil;\n\tNSData *data = [url bookmarkDataWithOptions:(NSURLBookmarkCreationWithSecurityScope | NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess) includingResourceValuesForKeys:nil relativeToURL:nil error:&error];\n\treturn data ? QByteArray::fromNSData(data) : QByteArray();\n#endif // OS_MAC_STORE\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/file_utilities_mac.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_file_utilities.h\"\n\nnamespace Platform {\nnamespace File {\n\ninline void UnsafeOpenUrl(const QString &url) {\n\treturn ::File::internal::UnsafeOpenUrlDefault(url);\n}\n\ninline void UnsafeOpenEmailLink(const QString &email) {\n\treturn ::File::internal::UnsafeOpenEmailLinkDefault(email);\n}\n\ninline void PostprocessDownloaded(const QString &filepath) {\n}\n\n} // namespace File\n\nnamespace FileDialog {\n\ninline void InitLastPath() {\n\t::FileDialog::internal::InitLastPathDefault();\n}\n\ninline bool Get(\n\t\tQPointer<QWidget> parent,\n\t\tQStringList &files,\n\t\tQByteArray &remoteContent,\n\t\tconst QString &caption,\n\t\tconst QString &filter,\n\t\t::FileDialog::internal::Type type,\n\t\tQString startFile) {\n\treturn ::FileDialog::internal::GetDefault(\n\t\tparent,\n\t\tfiles,\n\t\tremoteContent,\n\t\tcaption,\n\t\tfilter,\n\t\ttype,\n\t\tstartFile);\n}\n\n} // namespace FileDialog\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/file_utilities_mac.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/file_utilities_mac.h\"\n\n#include \"base/platform/mac/base_utilities_mac.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_window.h\"\n\n#include <Cocoa/Cocoa.h>\n#include <CoreFoundation/CFURL.h>\n\nnamespace {\n\nusing namespace Platform;\n\nQString strNeedToReload() {\n\tconst uint32 letters[] = { 0xAD92C02B, 0xA2217C97, 0x5E55F4F5, 0x2207DAAC, 0xD18BA536, 0x03E41869, 0xB96D2BFD, 0x810C7284, 0xE412099E, 0x5AAD0837, 0xE6637AEE, 0x8E5E2FF5, 0xE3BDA123, 0x94A5CE38, 0x4A42F7D1, 0xCE4677DC, 0x40A81701, 0x9C5B38CD, 0x61801E1A, 0x6FF16179 };\n\treturn MakeFromLetters(letters);\n}\n\nQString strNeedToRefresh1() {\n\tconst uint32 letters[] = { 0xEDDFCD66, 0x434DF1FB, 0x820B76AB, 0x48CE7965, 0x3609C0BA, 0xFC9A990C, 0x3EDD1C51, 0xE2BDA036, 0x7140CEE9, 0x65DB414D, 0x88592EC3, 0x2CB2613A };\n\treturn MakeFromLetters(letters);\n}\n\nQString strNeedToRefresh2() {\n\tconst uint32 letters[] = { 0x8AE4915D, 0x7159D7EF, 0x79C74167, 0x29B7611C, 0x0E6B9ADD, 0x0D93610F, 0xEBEAFE7A, 0x5BD17540, 0x121EF3B7, 0x61B02E26, 0x2174AAEE, 0x61AD3325 };\n\treturn MakeFromLetters(letters);\n}\n\n} // namespace\n\n@interface OpenWithApp : NSObject {\n\tNSString *fullname;\n\tNSURL *app;\n\tNSImage *icon;\n\n}\n\n@property (nonatomic, retain) NSString *fullname;\n@property (nonatomic, retain) NSURL *app;\n@property (nonatomic, retain) NSImage *icon;\n\n@end // @interface OpenWithApp\n\n@implementation OpenWithApp\n\n@synthesize fullname, app, icon;\n\n- (void) dealloc {\n\t[fullname release];\n\t[app release];\n\t[icon release];\n\t[super dealloc];\n}\n\n@end // @implementation OpenWithApp\n\n@interface OpenFileWithInterface : NSObject {\n}\n\n- (id) init:(NSString *)file;\n- (BOOL) popupAt:(NSPoint)point;\n- (void) itemChosen:(id)sender;\n- (void) dealloc;\n\n@end // @interface OpenFileWithInterface\n\n@implementation OpenFileWithInterface {\n\tNSString *toOpen;\n\n\tNSURL *defUrl;\n\tNSString *defBundle, *defName, *defVersion;\n\tNSImage *defIcon;\n\n\tNSMutableArray *apps;\n\n\tNSMenu *menu;\n\n}\n\n- (void) fillAppByUrl:(NSURL*)url bundle:(NSString**)bundle name:(NSString**)name version:(NSString**)version icon:(NSImage**)icon {\n\tNSBundle *b = [NSBundle bundleWithURL:url];\n\tif (b) {\n\t\tNSString *path = [url path];\n\t\t*name = [[NSFileManager defaultManager] displayNameAtPath: path];\n\t\tif (!*name) *name = (NSString*)[b objectForInfoDictionaryKey:@\"CFBundleDisplayName\"];\n\t\tif (!*name) *name = (NSString*)[b objectForInfoDictionaryKey:@\"CFBundleName\"];\n\t\tif (*name) {\n\t\t\t*bundle = [b bundleIdentifier];\n\t\t\tif (bundle) {\n\t\t\t\t*version = (NSString*)[b objectForInfoDictionaryKey:@\"CFBundleShortVersionString\"];\n\t\t\t\t*icon = [[NSWorkspace sharedWorkspace] iconForFile: path];\n\t\t\t\tif (*icon && [*icon isValid]) [*icon setSize: CGSizeMake(16., 16.)];\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\t*bundle = *name = *version = nil;\n\t*icon = nil;\n}\n\n- (id) init:(NSString*)file {\n\ttoOpen = [file retain];\n\tif (self = [super init]) {\n\t\tNSURL *url = [NSURL fileURLWithPath:file];\n\t\tdefUrl = [[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:url];\n\t\tif (defUrl) {\n\t\t\t[self fillAppByUrl:defUrl bundle:&defBundle name:&defName version:&defVersion icon:&defIcon];\n\t\t\tif (!defBundle || !defName) {\n\t\t\t\tdefUrl = nil;\n\t\t\t}\n\t\t}\n\t\tNSArray *appsList = (NSArray*)LSCopyApplicationURLsForURL(CFURLRef(url), kLSRolesAll);\n\t\tNSMutableDictionary *data = [NSMutableDictionary dictionaryWithCapacity:16];\n\t\tint fullcount = 0;\n\t\tfor (id app in appsList) {\n\t\t\tif (fullcount > 15) break;\n\n\t\t\tNSString *bundle = nil, *name = nil, *version = nil;\n\t\t\tNSImage *icon = nil;\n\t\t\t[self fillAppByUrl:(NSURL*)app bundle:&bundle name:&name version:&version icon:&icon];\n\t\t\tif (bundle && name) {\n\t\t\t\tif ([bundle isEqualToString:defBundle] && [version isEqualToString:defVersion]) continue;\n\t\t\t\tNSString *key = [[NSArray arrayWithObjects:bundle, name, nil] componentsJoinedByString:@\"|\"];\n\t\t\t\tif (!version) version = @\"\";\n\n\t\t\t\tNSMutableDictionary *versions = (NSMutableDictionary*)[data objectForKey:key];\n\t\t\t\tif (!versions) {\n\t\t\t\t\tversions = [NSMutableDictionary dictionaryWithCapacity:2];\n\t\t\t\t\t[data setValue:versions forKey:key];\n\t\t\t\t}\n\t\t\t\tif (![versions objectForKey:version]) {\n\t\t\t\t\t[versions setValue:[NSArray arrayWithObjects:name, icon, app, nil] forKey:version];\n\t\t\t\t\t++fullcount;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (fullcount || defUrl) {\n\t\t\tapps = [NSMutableArray arrayWithCapacity:fullcount];\n\t\t\tfor (id key in data) {\n\t\t\t\tNSMutableDictionary *val = (NSMutableDictionary*)[data objectForKey:key];\n\t\t\t\tfor (id ver in val) {\n\t\t\t\t\tNSArray *app = (NSArray*)[val objectForKey:ver];\n\t\t\t\t\tOpenWithApp *a = [[OpenWithApp alloc] init];\n\t\t\t\t\tNSString *fullname = (NSString*)[app objectAtIndex:0], *version = (NSString*)ver;\n\t\t\t\t\tBOOL showVersion = ([val count] > 1);\n\t\t\t\t\tif (!showVersion) {\n\t\t\t\t\t\tNSError *error = NULL;\n\t\t\t\t\t\tNSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@\"^\\\\d+\\\\.\\\\d+\\\\.\\\\d+(\\\\.\\\\d+)?$\" options:NSRegularExpressionCaseInsensitive error:&error];\n\t\t\t\t\t\tshowVersion = ![regex numberOfMatchesInString:version options:NSMatchingWithoutAnchoringBounds range:{0,[version length]}];\n\t\t\t\t\t}\n\t\t\t\t\tif (showVersion) fullname = [[NSArray arrayWithObjects:fullname, @\" (\", version, @\")\", nil] componentsJoinedByString:@\"\"];\n\t\t\t\t\t[a setFullname:fullname];\n\t\t\t\t\t[a setIcon:(NSImage*)[app objectAtIndex:1]];\n\t\t\t\t\t[a setApp:(NSURL*)[app objectAtIndex:2]];\n\t\t\t\t\t[apps addObject:a];\n\t\t\t\t\t[a release];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t[apps sortUsingDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@\"fullname\" ascending:YES]]];\n\t\t[appsList release];\n\t\tmenu = nil;\n\t}\n\treturn self;\n}\n\n- (BOOL) popupAt:(NSPoint)point {\n\tif (![apps count] && !defName) return NO;\n\tmenu = [[NSMenu alloc] initWithTitle:@\"Open With\"];\n\n\tint index = 0;\n\tif (defName) {\n\t\tNSMenuItem *item = [menu insertItemWithTitle:[[NSArray arrayWithObjects:defName, @\" (default)\", nil] componentsJoinedByString:@\"\"] action:@selector(itemChosen:) keyEquivalent:@\"\" atIndex:index++];\n\t\tif (defIcon) [item setImage:defIcon];\n\t\t[item setTarget:self];\n\t\t[menu insertItem:[NSMenuItem separatorItem] atIndex:index++];\n\t}\n\tif ([apps count]) {\n\t\tfor (id a in apps) {\n\t\t\tOpenWithApp *app = (OpenWithApp*)a;\n\t\t\tNSMenuItem *item = [menu insertItemWithTitle:[a fullname] action:@selector(itemChosen:) keyEquivalent:@\"\" atIndex:index++];\n\t\t\tif ([app icon]) [item setImage:[app icon]];\n\t\t\t[item setTarget:self];\n\t\t}\n\t\t[menu insertItem:[NSMenuItem separatorItem] atIndex:index++];\n\t}\n\tNSMenuItem *item = [menu insertItemWithTitle:Q2NSString(tr::lng_mac_choose_program_menu(tr::now)) action:@selector(itemChosen:) keyEquivalent:@\"\" atIndex:index++];\n\t[item setTarget:self];\n\n\t[menu popUpMenuPositioningItem:nil atLocation:point inView:nil];\n\n\treturn YES;\n}\n\n- (void) itemChosen:(id)sender {\n\tNSArray *items = [menu itemArray];\n\tNSURL *url = nil;\n\tfor (int i = 0, l = [items count]; i < l; ++i) {\n\t\tif ([items objectAtIndex:i] == sender) {\n\t\t\tif (defName) i -= 2;\n\t\t\tif (i < 0) {\n\t\t\t\turl = defUrl;\n\t\t\t} else if (i < int([apps count])) {\n\t\t\t\turl = [(OpenWithApp*)[apps objectAtIndex:i] app];\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (url) {\n\t\t[[NSWorkspace sharedWorkspace] openFile:toOpen withApplication:[url path]];\n\t} else if (!Platform::File::UnsafeShowOpenWith(NS2QString(toOpen))) {\n\t\tPlatform::File::UnsafeLaunch(NS2QString(toOpen));\n\t}\n}\n\n- (void) dealloc {\n\t[toOpen release];\n\tif (menu) [menu release];\n\t[super dealloc];\n}\n\n@end // @implementation OpenFileWithInterface\n\n@interface NSURL(CompareUrls)\n\n- (BOOL) isEquivalent:(NSURL *)aURL;\n\n@end // @interface NSURL(CompareUrls)\n\n@implementation NSURL(CompareUrls)\n\n- (BOOL) isEquivalent:(NSURL *)aURL {\n\tif ([self isEqual:aURL]) return YES;\n\tif ([[self scheme] caseInsensitiveCompare:[aURL scheme]] != NSOrderedSame) return NO;\n\tif ([[self host] caseInsensitiveCompare:[aURL host]] != NSOrderedSame) return NO;\n\tif ([[self path] compare:[aURL path]] != NSOrderedSame) return NO;\n\tif ([[self port] compare:[aURL port]] != NSOrderedSame) return NO;\n\tif ([[self query] compare:[aURL query]] != NSOrderedSame) return NO;\n\treturn YES;\n}\n\n@end // @implementation NSURL(CompareUrls)\n\n@interface ChooseApplicationDelegate : NSObject<NSOpenSavePanelDelegate> {\n}\n\n- (id) init:(NSArray *)recommendedApps withPanel:(NSOpenPanel *)creator withSelector:(NSPopUpButton *)menu withGood:(NSTextField *)goodLabel withBad:(NSTextField *)badLabel withIcon:(NSImageView *)badIcon withAccessory:(NSView *)acc;\n- (BOOL) panel:(id)sender shouldEnableURL:(NSURL *)url;\n- (void) panelSelectionDidChange:(id)sender;\n- (void) menuDidClose;\n- (void) dealloc;\n\n@end // @interface ChooseApplicationDelegate\n\n@implementation ChooseApplicationDelegate {\n\tBOOL onlyRecommended;\n\tNSArray *apps;\n\tNSOpenPanel *panel;\n\tNSPopUpButton *selector;\n\tNSTextField *good, *bad;\n\tNSImageView *icon;\n\tNSString *recom;\n\tNSView *accessory;\n\n}\n\n- (id) init:(NSArray *)recommendedApps withPanel:(NSOpenPanel *)creator withSelector:(NSPopUpButton *)menu withGood:(NSTextField *)goodLabel withBad:(NSTextField *)badLabel withIcon:(NSImageView *)badIcon withAccessory:(NSView *)acc {\n\tif (self = [super init]) {\n\t\tonlyRecommended = YES;\n\t\trecom = [Q2NSString(tr::lng_mac_recommended_apps(tr::now)) copy];\n\t\tapps = recommendedApps;\n\t\tpanel = creator;\n\t\tselector = menu;\n\t\tgood = goodLabel;\n\t\tbad = badLabel;\n\t\ticon = badIcon;\n\t\taccessory = acc;\n\t\t[selector setAction:@selector(menuDidClose)];\n\t}\n\treturn self;\n}\n\n- (BOOL) isRecommended:(NSURL *)url {\n\tif (apps) {\n\t\tfor (id app in apps) {\n\t\t\tif ([(NSURL*)app isEquivalent:url]) {\n\t\t\t\treturn YES;\n\t\t\t}\n\t\t}\n\t}\n\treturn NO;\n}\n\n- (BOOL) panel:(id)sender shouldEnableURL:(NSURL *)url {\n\tNSNumber *isDirectory;\n\tif ([url getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil] && isDirectory != nil && [isDirectory boolValue]) {\n\t\tif (onlyRecommended) {\n\t\t\tNSNumber *isPackage;\n\t\t\tif ([url getResourceValue:&isPackage forKey:NSURLIsPackageKey error:nil] && isPackage != nil && [isPackage boolValue]) {\n\t\t\t\treturn [self isRecommended:url];\n\t\t\t}\n\t\t}\n\t\treturn YES;\n\t}\n\treturn NO;\n}\n\n- (void) panelSelectionDidChange:(id)sender {\n\tNSArray *urls = [panel URLs];\n\tif ([urls count]) {\n\t\tif ([self isRecommended:[urls firstObject]]) {\n\t\t\t[bad removeFromSuperview];\n\t\t\t[icon removeFromSuperview];\n\t\t\t[accessory addSubview:good];\n\t\t} else {\n\t\t\t[good removeFromSuperview];\n\t\t\t[accessory addSubview:bad];\n\t\t\t[accessory addSubview:icon];\n\t\t}\n\t} else {\n\t\t[good removeFromSuperview];\n\t\t[bad removeFromSuperview];\n\t\t[icon removeFromSuperview];\n\t}\n}\n\n- (void) menuDidClose {\n\tonlyRecommended = [[[selector selectedItem] title] isEqualToString:recom];\n\t[self refreshPanelTable];\n}\n\n- (BOOL) refreshDataInViews: (NSArray*)subviews {\n\tfor (id view in subviews) {\n\t\tNSString *cls = [view className];\n\t\tif ([cls isEqualToString:Q2NSString(strNeedToReload())]) {\n\t\t\t[view reloadData];\n\t\t} else if ([cls isEqualToString:Q2NSString(strNeedToRefresh1())] || [cls isEqualToString:Q2NSString(strNeedToRefresh2())]) {\n\t\t\t[view reloadData];\n\t\t\treturn YES;\n\t\t} else {\n\t\t\tNSArray *next = [view subviews];\n\t\t\tif ([next count] && [self refreshDataInViews:next]) {\n\t\t\t\treturn YES;\n\t\t\t}\n\t\t}\n\t}\n\treturn NO;\n}\n\n\n- (void) refreshPanelTable {\n\t@autoreleasepool {\n\n\t[self refreshDataInViews:[[panel contentView] subviews]];\n\t[panel validateVisibleColumns];\n\n\t}\n}\n\n- (void) dealloc {\n\tif (apps) {\n\t\t[apps release];\n\t\t[recom release];\n\t}\n\t[super dealloc];\n}\n\n@end // @implementation ChooseApplicationDelegate\n\nnamespace Platform {\nnamespace File {\n\nQString UrlToLocal(const QUrl &url) {\n\tauto result = url.toLocalFile();\n\tif (result.startsWith(u\"/.file/id=\"_q)) {\n\t\tNSString *nsurl = [[[NSURL URLWithString: [NSString stringWithUTF8String: (u\"file://\"_q + result).toUtf8().constData()]] filePathURL] path];\n\t\tif (!nsurl) return QString();\n\n\t\treturn NS2QString(nsurl);\n\t}\n\treturn result;\n}\n\nbool UnsafeShowOpenWithDropdown(const QString &filepath) {\n\t@autoreleasepool {\n\n\tNSString *file = Q2NSString(filepath);\n\t@try {\n\t\tOpenFileWithInterface *menu = [[[OpenFileWithInterface alloc] init:file] autorelease];\n\t\treturn !![menu popupAt:[NSEvent mouseLocation]];\n\t}\n\t@catch (NSException *exception) {\n\t}\n\t@finally {\n\t}\n\n\t}\n\treturn false;\n}\n\nbool UnsafeShowOpenWith(const QString &filepath) {\n\t@autoreleasepool {\n\n\tNSString *file = Q2NSString(filepath);\n\t@try {\n\t\tNSURL *url = [NSURL fileURLWithPath:file];\n\t\tNSString *ext = [url pathExtension];\n\t\tNSArray *names = [url pathComponents];\n\t\tNSString *name = [names count] ? [names lastObject] : @\"\";\n\t\tNSArray *apps = (NSArray*)LSCopyApplicationURLsForURL(CFURLRef(url), kLSRolesAll);\n\n\t\tNSOpenPanel *openPanel = [NSOpenPanel openPanel];\n\n\t\tNSRect fullRect = { { 0., 0. }, { st::macAccessoryWidth, st::macAccessoryHeight } };\n\t\tNSView *accessory = [[NSView alloc] initWithFrame:fullRect];\n\n\t\t[accessory setAutoresizesSubviews:YES];\n\n\t\tNSPopUpButton *selector = [[NSPopUpButton alloc] init];\n\t\t[accessory addSubview:selector];\n\t\t[selector addItemWithTitle:Q2NSString(tr::lng_mac_recommended_apps(tr::now))];\n\t\t[selector addItemWithTitle:Q2NSString(tr::lng_mac_all_apps(tr::now))];\n\t\t[selector sizeToFit];\n\n\t\tNSTextField *enableLabel = [[NSTextField alloc] init];\n\t\t[accessory addSubview:enableLabel];\n\t\t[enableLabel setStringValue:Q2NSString(tr::lng_mac_enable_filter(tr::now))];\n\t\t[enableLabel setFont:[selector font]];\n\t\t[enableLabel setBezeled:NO];\n\t\t[enableLabel setDrawsBackground:NO];\n\t\t[enableLabel setEditable:NO];\n\t\t[enableLabel setSelectable:NO];\n\t\t[enableLabel sizeToFit];\n\n\t\tNSRect selectorFrame = [selector frame], enableFrame = [enableLabel frame];\n\t\tenableFrame.size.width += st::macEnableFilterAdd;\n\t\tenableFrame.origin.x = (fullRect.size.width - selectorFrame.size.width - enableFrame.size.width) / 2.;\n\t\tselectorFrame.origin.x = (fullRect.size.width - selectorFrame.size.width + enableFrame.size.width) / 2.;\n\t\tenableFrame.origin.y = fullRect.size.height - selectorFrame.size.height - st::macEnableFilterTop + (selectorFrame.size.height - enableFrame.size.height) / 2.;\n\t\tselectorFrame.origin.y = fullRect.size.height - selectorFrame.size.height - st::macSelectorTop;\n\t\t[enableLabel setFrame:enableFrame];\n\t\t[enableLabel setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];\n\t\t[selector setFrame:selectorFrame];\n\t\t[selector setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];\n\n\t\tNSButton *button = [[NSButton alloc] init];\n\t\t[accessory addSubview:button];\n\t\t[button setButtonType:NSSwitchButton];\n\t\t[button setFont:[selector font]];\n\t\t[button setTitle:Q2NSString(tr::lng_mac_always_open_with(tr::now))];\n\t\t[button sizeToFit];\n\t\tNSRect alwaysRect = [button frame];\n\t\talwaysRect.origin.x = (fullRect.size.width - alwaysRect.size.width) / 2;\n\t\talwaysRect.origin.y = selectorFrame.origin.y - alwaysRect.size.height - st::macAlwaysThisAppTop;\n\t\t[button setFrame:alwaysRect];\n\t\t[button setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];\n#ifdef OS_MAC_STORE\n\t\t[button setHidden:YES];\n#endif // OS_MAC_STORE\n\t\tNSTextField *goodLabel = [[NSTextField alloc] init];\n\t\t[goodLabel setStringValue:Q2NSString(tr::lng_mac_this_app_can_open(tr::now, lt_file, NS2QString(name)))];\n\t\t[goodLabel setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];\n\t\t[goodLabel setBezeled:NO];\n\t\t[goodLabel setDrawsBackground:NO];\n\t\t[goodLabel setEditable:NO];\n\t\t[goodLabel setSelectable:NO];\n\t\t[goodLabel sizeToFit];\n\t\tNSRect goodFrame = [goodLabel frame];\n\t\tgoodFrame.origin.x = (fullRect.size.width - goodFrame.size.width) / 2.;\n\t\tgoodFrame.origin.y = alwaysRect.origin.y - goodFrame.size.height - st::macAppHintTop;\n\t\t[goodLabel setFrame:goodFrame];\n\n\t\tNSTextField *badLabel = [[NSTextField alloc] init];\n\t\t[badLabel setStringValue:Q2NSString(tr::lng_mac_not_known_app(tr::now, lt_file, NS2QString(name)))];\n\t\t[badLabel setFont:[goodLabel font]];\n\t\t[badLabel setBezeled:NO];\n\t\t[badLabel setDrawsBackground:NO];\n\t\t[badLabel setEditable:NO];\n\t\t[badLabel setSelectable:NO];\n\t\t[badLabel sizeToFit];\n\t\tNSImageView *badIcon = [[NSImageView alloc] init];\n\t\tNSImage *badImage = [NSImage imageNamed:NSImageNameCaution];\n\t\t[badIcon setImage:badImage];\n\t\t[badIcon setFrame:NSMakeRect(0, 0, st::macCautionIconSize, st::macCautionIconSize)];\n\n\t\tNSRect badFrame = [badLabel frame], badIconFrame = [badIcon frame];\n\t\tbadFrame.origin.x = (fullRect.size.width - badFrame.size.width + badIconFrame.size.width) / 2.;\n\t\tbadIconFrame.origin.x = (fullRect.size.width - badFrame.size.width - badIconFrame.size.width) / 2.;\n\t\tbadFrame.origin.y = alwaysRect.origin.y - badFrame.size.height - st::macAppHintTop;\n\t\tbadIconFrame.origin.y = badFrame.origin.y;\n\t\t[badLabel setFrame:badFrame];\n\t\t[badIcon setFrame:badIconFrame];\n\n\t\t[openPanel setAccessoryView:accessory];\n\n\t\tChooseApplicationDelegate *delegate = [[ChooseApplicationDelegate alloc] init:apps withPanel:openPanel withSelector:selector withGood:goodLabel withBad:badLabel withIcon:badIcon withAccessory:accessory];\n\t\t[openPanel setDelegate:delegate];\n\n\t\t[openPanel setCanChooseDirectories:NO];\n\t\t[openPanel setCanChooseFiles:YES];\n\t\t[openPanel setAllowsMultipleSelection:NO];\n\t\t[openPanel setResolvesAliases:YES];\n\t\t[openPanel setTitle:Q2NSString(tr::lng_mac_choose_app(tr::now))];\n\t\t[openPanel setMessage:Q2NSString(tr::lng_mac_choose_text(tr::now, lt_file, NS2QString(name)))];\n\n\t\tNSArray *appsPaths = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationDirectory inDomains:NSLocalDomainMask];\n\t\tif ([appsPaths count]) [openPanel setDirectoryURL:[appsPaths firstObject]];\n\t\t[openPanel beginWithCompletionHandler:^(NSInteger result){\n\t\t\tif (result == NSModalResponseOK) {\n\t\t\t\tif ([[openPanel URLs] count] > 0) {\n\t\t\t\t\tNSURL *app = [[openPanel URLs] objectAtIndex:0];\n\t\t\t\t\tNSString *path = [app path];\n\t\t\t\t\tif ([button state] == NSOnState) {\n\t\t\t\t\t\tNSArray *UTIs = (NSArray *)UTTypeCreateAllIdentifiersForTag(kUTTagClassFilenameExtension,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t(CFStringRef)ext,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tnil);\n\t\t\t\t\t\tfor (NSString *UTI in UTIs) {\n\t\t\t\t\t\t\tOSStatus result = LSSetDefaultRoleHandlerForContentType((CFStringRef)UTI,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tkLSRolesAll,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t(CFStringRef)[[NSBundle bundleWithPath:path] bundleIdentifier]);\n\t\t\t\t\t\t\tDEBUG_LOG((\"App Info: set default handler for '%1' UTI result: %2\").arg(NS2QString(UTI)).arg(result));\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t[UTIs release];\n\t\t\t\t\t}\n\t\t\t\t\t[[NSWorkspace sharedWorkspace] openFile:file withApplication:[app path]];\n\t\t\t\t}\n\t\t\t}\n\t\t\t[selector release];\n\t\t\t[button release];\n\t\t\t[enableLabel release];\n\t\t\t[goodLabel release];\n\t\t\t[badLabel release];\n\t\t\t[badIcon release];\n\t\t\t[accessory release];\n\t\t\t[delegate release];\n\t\t}];\n\t}\n\t@catch (NSException *exception) {\n\t\t[[NSWorkspace sharedWorkspace] openFile:file];\n\t}\n\t@finally {\n\t}\n\n\t}\n\n\treturn YES;\n}\n\nvoid UnsafeLaunch(const QString &filepath) {\n\t@autoreleasepool {\n\n\tNSString *file = Q2NSString(filepath);\n\tif ([[NSWorkspace sharedWorkspace] openFile:file] == NO) {\n\t\tUnsafeShowOpenWith(filepath);\n\t}\n\n\t}\n}\n\n} // namespace File\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/integration_mac.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Platform {\n\nclass Integration;\n\n[[nodiscard]] std::unique_ptr<Integration> CreateIntegration();\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/integration_mac.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/integration_mac.h\"\n\n#include \"platform/platform_integration.h\"\n\nnamespace Platform {\nnamespace {\n\nclass MacIntegration final : public Integration {\n};\n\n} // namespace\n\nstd::unique_ptr<Integration> CreateIntegration() {\n\treturn std::make_unique<MacIntegration>();\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/launcher_mac.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"core/launcher.h\"\n\nnamespace Platform {\n\nclass Launcher : public Core::Launcher {\npublic:\n\tLauncher(int argc, char *argv[]);\n\nprivate:\n\tvoid initHook() override;\n\n\tbool launchUpdater(UpdaterLaunch action) override;\n\n};\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/launcher_mac.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/launcher_mac.h\"\n\n#include \"core/crash_reports.h\"\n#include \"core/update_checker.h\"\n#include \"base/base_file_utilities.h\"\n#include \"base/platform/base_platform_file_utilities.h\"\n#include \"base/platform/mac/base_utilities_mac.h\"\n\n#include <Cocoa/Cocoa.h>\n#include <CoreFoundation/CFURL.h>\n#include <sys/sysctl.h>\n\nnamespace Platform {\n\nLauncher::Launcher(int argc, char *argv[])\n: Core::Launcher(argc, argv) {\n}\n\nvoid Launcher::initHook() {\n\tbase::RegisterBundledResources(u\"Telegram.rcc\"_q);\n}\n\nbool Launcher::launchUpdater(UpdaterLaunch action) {\n\tif (cExeName().isEmpty()) {\n\t\treturn false;\n\t}\n\t@autoreleasepool {\n\n#ifdef OS_MAC_STORE\n\t// In AppStore version we don't have Updater.\n\t// We just relaunch our app.\n\tif (action == UpdaterLaunch::JustRelaunch) {\n\t\tNSDictionary *conf = [NSDictionary dictionaryWithObject:[NSArray array] forKey:NSWorkspaceLaunchConfigurationArguments];\n\t\t[[NSWorkspace sharedWorkspace] launchApplicationAtURL:[NSURL fileURLWithPath:Q2NSString(cExeDir() + cExeName())] options:NSWorkspaceLaunchAsync | NSWorkspaceLaunchNewInstance configuration:conf error:0];\n\t\treturn true;\n\t}\n#endif // OS_MAC_STORE\n\n\tNSString *path = @\"\", *args = @\"\";\n\t@try {\n\t\tpath = [[NSBundle mainBundle] bundlePath];\n\t\tif (!path) {\n\t\t\tLOG((\"Could not get bundle path!!\"));\n\t\t\treturn false;\n\t\t}\n\t\tpath = [path stringByAppendingString:@\"/Contents/Frameworks/Updater\"];\n\t\tbase::Platform::RemoveQuarantine(QFile::decodeName([path fileSystemRepresentation]));\n\n\t\tNSMutableArray *args = [[NSMutableArray alloc] initWithObjects:@\"-workpath\", Q2NSString(cWorkingDir()), @\"-procid\", nil];\n\t\t[args addObject:[NSString stringWithFormat:@\"%d\", [[NSProcessInfo processInfo] processIdentifier]]];\n\t\tif (cRestartingToSettings()) [args addObject:@\"-tosettings\"];\n\t\tif (action == UpdaterLaunch::JustRelaunch) [args addObject:@\"-noupdate\"];\n\t\tif (cLaunchMode() == LaunchModeAutoStart) [args addObject:@\"-autostart\"];\n\t\tif (Logs::DebugEnabled()) [args addObject:@\"-debug\"];\n\t\tif (cStartInTray()) [args addObject:@\"-startintray\"];\n\t\tif (cDataFile() != u\"data\"_q) {\n\t\t\t[args addObject:@\"-key\"];\n\t\t\t[args addObject:Q2NSString(cDataFile())];\n\t\t}\n\t\tif (customWorkingDir()) {\n\t\t\t[args addObject:@\"-workdir_custom\"];\n\t\t}\n\n\t\tDEBUG_LOG((\"Application Info: executing %1 %2\").arg(NS2QString(path)).arg(NS2QString([args componentsJoinedByString:@\" \"])));\n\t\tLogs::closeMain();\n\t\tCrashReports::Finish();\n\t\tif (![NSTask launchedTaskWithLaunchPath:path arguments:args]) {\n\t\t\tDEBUG_LOG((\"Task not launched while executing %1 %2\").arg(NS2QString(path)).arg(NS2QString([args componentsJoinedByString:@\" \"])));\n\t\t\treturn false;\n\t\t}\n\t}\n\t@catch (NSException *exception) {\n\t\tLOG((\"Exception caught while executing %1 %2\").arg(NS2QString(path)).arg(NS2QString(args)));\n\t\treturn false;\n\t}\n\t@finally {\n\t}\n\n\t}\n\treturn true;\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/mac_iconv_helper.c",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include <iconv.h>\n\n#ifdef iconv_open\n#undef iconv_open\n#endif // iconv_open\n\n#ifdef iconv\n#undef iconv\n#endif // iconv\n\n#ifdef iconv_close\n#undef iconv_close\n#endif // iconv_close\n\niconv_t iconv_open(const char* tocode, const char* fromcode) {\n\treturn libiconv_open(tocode, fromcode);\n}\n\nsize_t iconv(iconv_t cd, char** inbuf, size_t *inbytesleft, char** outbuf, size_t *outbytesleft) {\n\treturn libiconv(cd, inbuf, inbytesleft, outbuf, outbytesleft);\n}\n\nint iconv_close(iconv_t cd) {\n\treturn libiconv_close(cd);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/main_window_mac.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_main_window.h\"\n#include \"platform/mac/specific_mac_p.h\"\n#include \"base/timer.h\"\n\n#include <QtWidgets/QMenuBar>\n#include <QtCore/QTimer>\n\nnamespace Platform {\n\nclass MainWindow : public Window::MainWindow {\npublic:\n\texplicit MainWindow(not_null<Window::Controller*> controller);\n\n\tint getCustomTitleHeight() const {\n\t\treturn _customTitleHeight;\n\t}\n\n\t~MainWindow();\n\n\tvoid updateWindowIcon() override;\n\n\trpl::producer<QPoint> globalForceClicks() override {\n\t\treturn _forceClicks.events();\n\t}\n\n\tclass Private;\n\nprotected:\n\tbool eventFilter(QObject *obj, QEvent *evt) override;\n\n\tvoid stateChangedHook(Qt::WindowState state) override;\n\tvoid initHook() override;\n\tvoid unreadCounterChangedHook() override;\n\n\tvoid updateGlobalMenuHook() override;\n\n\tvoid closeWithoutDestroy() override;\n\tvoid createGlobalMenu() override;\n\nprivate:\n\tfriend class Private;\n\n\tbool nativeEvent(\n\t\tconst QByteArray &eventType,\n\t\tvoid *message,\n\t\tqintptr *result) override;\n\n\tvoid hideAndDeactivate();\n\tvoid updateDockCounter();\n\n\tstd::unique_ptr<Private> _private;\n\n\tmutable QTimer psIdleTimer;\n\n\tbase::Timer _hideAfterFullScreenTimer;\n\n\tQMenuBar psMainMenu;\n\tQAction *psLogout = nullptr;\n\tQAction *psUndo = nullptr;\n\tQAction *psRedo = nullptr;\n\tQAction *psCut = nullptr;\n\tQAction *psCopy = nullptr;\n\tQAction *psPaste = nullptr;\n\tQAction *psDelete = nullptr;\n\tQAction *psSelectAll = nullptr;\n\tQAction *psContacts = nullptr;\n\tQAction *psAddContact = nullptr;\n\tQAction *psNewGroup = nullptr;\n\tQAction *psNewChannel = nullptr;\n\tQAction *psShowTelegram = nullptr;\n\n\tQAction *psBold = nullptr;\n\tQAction *psItalic = nullptr;\n\tQAction *psUnderline = nullptr;\n\tQAction *psStrikeOut = nullptr;\n\tQAction *psBlockquote = nullptr;\n\tQAction *psMonospace = nullptr;\n\tQAction *psClearFormat = nullptr;\n\n\trpl::event_stream<QPoint> _forceClicks;\n\tint _customTitleHeight = 0;\n\tint _lastPressureStage = 0;\n\n};\n\n[[nodiscard]] int32 ScreenNameChecksum(const QString &name);\n[[nodiscard]] int32 ScreenNameChecksum(const QScreen *screen);\n\n[[nodiscard]] QString ScreenDisplayLabel(const QScreen *screen);\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/main_window_mac.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/main_window_mac.h\"\n\n#include \"data/data_session.h\"\n#include \"core/application.h\"\n#include \"core/sandbox.h\"\n#include \"main/main_session.h\"\n#include \"history/history_widget.h\"\n#include \"history/history_inner_widget.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\" // Domain::activeSessionValue\n#include \"media/player/media_player_instance.h\"\n#include \"media/audio/media_audio.h\"\n#include \"storage/localstorage.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"platform/mac/touchbar/mac_touchbar_manager.h\"\n#include \"platform/platform_specific.h\"\n#include \"platform/platform_notifications_manager.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/options.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/about_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"base/platform/mac/base_utilities_mac.h\"\n\n#include <QtWidgets/QLineEdit>\n#include <QtWidgets/QTextEdit>\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)\n#include <qpa/qwindowsysteminterface.h>\n#endif // Qt < 6.6.0\n\n#include <Cocoa/Cocoa.h>\n#include <CoreFoundation/CFURL.h>\n#include <IOKit/IOKitLib.h>\n#include <IOKit/hidsystem/ev_keymap.h>\n\n@interface MainWindowObserver : NSObject {\n}\n\n- (id) init:(MainWindow::Private*)window;\n- (void) activeSpaceDidChange:(NSNotification *)aNotification;\n#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)\n- (void) darkModeChanged:(NSNotification *)aNotification;\n#endif // Qt < 6.6.0\n- (void) screenIsLocked:(NSNotification *)aNotification;\n- (void) screenIsUnlocked:(NSNotification *)aNotification;\n\n@end // @interface MainWindowObserver\n\nnamespace Platform {\nnamespace {\n\n// When we close a window that is fullscreen we first leave the fullscreen\n// mode and after that hide the window. This is a timeout for elaving the\n// fullscreen mode, after that we'll hide the window no matter what.\nconstexpr auto kHideAfterFullscreenTimeoutMs = 3000;\n\n[[nodiscard]] bool PossiblyTextTypingEvent(NSEvent *e) {\n\tif ([e type] != NSEventTypeKeyDown) {\n\t\treturn false;\n\t}\n\tNSEventModifierFlags flags = [e modifierFlags]\n\t\t& NSEventModifierFlagDeviceIndependentFlagsMask;\n\tif ((flags & ~NSEventModifierFlagShift) != 0) {\n\t\treturn false;\n\t}\n\tNSString *text = [e characters];\n\tconst auto length = int([text length]);\n\tfor (auto i = 0; i != length; ++i) {\n\t\tconst auto utf16 = [text characterAtIndex:i];\n\t\tif (utf16 >= 32) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n} // namespace\n\nclass MainWindow::Private {\npublic:\n\texplicit Private(not_null<MainWindow*> window);\n\n\tvoid setNativeWindow(NSWindow *window, NSView *view);\n\tvoid initTouchBar(\n\t\tNSWindow *window,\n\t\tnot_null<Window::Controller*> controller);\n\tvoid setWindowBadge(const QString &str);\n\n\tvoid setMarkdownEnabledState(Ui::MarkdownEnabledState state);\n\n\tbool clipboardHasText();\n\t~Private();\n\nprivate:\n\tnot_null<MainWindow*> _public;\n\tfriend class MainWindow;\n\n\trpl::variable<Ui::MarkdownEnabledState> _markdownState;\n\n\tNSWindow * __weak _nativeWindow = nil;\n\tNSView * __weak _nativeView = nil;\n\n\tMainWindowObserver *_observer = nullptr;\n\tNSPasteboard *_generalPasteboard = nullptr;\n\tint _generalPasteboardChangeCount = -1;\n\tbool _generalPasteboardHasText = false;\n\n};\n\n} // namespace Platform\n\n@implementation MainWindowObserver {\n\tMainWindow::Private *_private;\n\n}\n\n- (id) init:(MainWindow::Private*)window {\n\tif (self = [super init]) {\n\t\t_private = window;\n\t}\n\treturn self;\n}\n\n- (void) activeSpaceDidChange:(NSNotification *)aNotification {\n}\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)\n- (void) darkModeChanged:(NSNotification *)aNotification {\n\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n\t\tQWindowSystemInterface::handleThemeChange();\n#else // Qt >= 6.5.0\n\t\tCore::App().settings().setSystemDarkMode(Platform::IsDarkMode());\n#endif // Qt < 6.5.0\n\t});\n}\n#endif // Qt < 6.6.0\n\n- (void) screenIsLocked:(NSNotification *)aNotification {\n\tCore::App().setScreenIsLocked(true);\n}\n\n- (void) screenIsUnlocked:(NSNotification *)aNotification {\n\tCore::App().setScreenIsLocked(false);\n}\n\n@end // @implementation MainWindowObserver\n\nnamespace Platform {\nnamespace {\n\nvoid SendKeySequence(\n\t\tQt::Key key,\n\t\tQt::KeyboardModifiers modifiers = Qt::NoModifier) {\n\tconst auto focused = QApplication::focusWidget();\n\tif (qobject_cast<QLineEdit*>(focused)\n\t\t|| qobject_cast<QTextEdit*>(focused)\n\t\t|| dynamic_cast<HistoryInner*>(focused)) {\n\t\tQApplication::postEvent(\n\t\t\tfocused,\n\t\t\tnew QKeyEvent(QEvent::KeyPress, key, modifiers));\n\t\tQApplication::postEvent(\n\t\t\tfocused,\n\t\t\tnew QKeyEvent(QEvent::KeyRelease, key, modifiers));\n\t}\n}\n\nvoid ForceDisabled(QAction *action, bool disabled) {\n\tif (action->isEnabled()) {\n\t\tif (disabled) action->setDisabled(true);\n\t} else if (!disabled) {\n\t\taction->setDisabled(false);\n\t}\n}\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)\nQString strNotificationAboutThemeChange() {\n\tconst uint32 letters[] = { 0x75E86256, 0xD03E11B1, 0x4D92201D, 0xA2144987, 0x99D5B34F, 0x037589C3, 0x38ED2A7C, 0xD2371ABC, 0xDC98BB02, 0x27964E1B, 0x01748AED, 0xE06679F8, 0x761C9580, 0x4F2595BF, 0x6B5FCBF4, 0xE4D9C24E, 0xBA2F6AB5, 0xE6E3FA71, 0xF2CFC255, 0x56A50C19, 0x43AE1239, 0x77CA4254, 0x7D189A89, 0xEA7663EE, 0x84CEB554, 0xA0ADF236, 0x886512D4, 0x7D3FBDAF, 0x85C4BE4F, 0x12C8255E, 0x9AD8BD41, 0xAC154683, 0xB117598B, 0xDFD9F947, 0x63F06C7B, 0x6340DCD6, 0x3AAE6B3E, 0x26CB125A };\n\treturn Platform::MakeFromLetters(letters);\n}\n#endif // Qt < 6.6.0\n\nQString strNotificationAboutScreenLocked() {\n\tconst uint32 letters[] = { 0x34B47F28, 0x47E95179, 0x73D05C42, 0xB4E2A933, 0x924F22D1, 0x4265D8EA, 0x9E4D2CC2, 0x02E8157B, 0x35BF7525, 0x75901A41, 0xB0400FCC, 0xE801169D, 0x4E04B589, 0xC1CEF054, 0xAB2A7EB0, 0x5C67C4F6, 0xA4E2B954, 0xB35E12D2, 0xD598B22B, 0x4E3B8AAB, 0xBEA5E439, 0xFDA8AA3C, 0x1632DBA8, 0x88FE8965 };\n\treturn Platform::MakeFromLetters(letters);\n}\n\nQString strNotificationAboutScreenUnlocked() {\n\tconst uint32 letters[] = { 0xF897900B, 0x19A04630, 0x144DA6DF, 0x643CA7ED, 0x81DDA343, 0x88C6B149, 0x5F9A3A15, 0x31804E13, 0xDF2202B8, 0x9BD1B500, 0x61B92735, 0x7DDF5D43, 0xB74E06C3, 0x16FF1665, 0x9098F702, 0x4461DAF0, 0xA3134FA5, 0x52B01D3C, 0x6BC35769, 0xA7CC945D, 0x8B5327C0, 0x7630B9A0, 0x4E52E3CE, 0xED7765E3, 0xCEB7862D, 0xA06B34F0 };\n\treturn Platform::MakeFromLetters(letters);\n}\n\n} // namespace\n\nMainWindow::Private::Private(not_null<MainWindow*> window)\n: _public(window)\n, _observer([[MainWindowObserver alloc] init:this]) {\n\t_generalPasteboard = [NSPasteboard generalPasteboard];\n\n\t@autoreleasepool {\n\n\t[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:_observer selector:@selector(activeSpaceDidChange:) name:NSWorkspaceActiveSpaceDidChangeNotification object:nil];\n#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)\n\t[[NSDistributedNotificationCenter defaultCenter] addObserver:_observer selector:@selector(darkModeChanged:) name:Q2NSString(strNotificationAboutThemeChange()) object:nil];\n#endif // Qt < 6.6.0\n\t[[NSDistributedNotificationCenter defaultCenter] addObserver:_observer selector:@selector(screenIsLocked:) name:Q2NSString(strNotificationAboutScreenLocked()) object:nil];\n\t[[NSDistributedNotificationCenter defaultCenter] addObserver:_observer selector:@selector(screenIsUnlocked:) name:Q2NSString(strNotificationAboutScreenUnlocked()) object:nil];\n\n\t}\n}\n\nvoid MainWindow::Private::setWindowBadge(const QString &str) {\n\t@autoreleasepool {\n\n\t[[NSApp dockTile] setBadgeLabel:Q2NSString(str)];\n\n\t}\n}\n\nvoid MainWindow::Private::setNativeWindow(NSWindow *window, NSView *view) {\n\t_nativeWindow = window;\n\t_nativeView = view;\n\tauto inner = [_nativeWindow contentLayoutRect];\n\tauto full = [_nativeView frame];\n\t_public->_customTitleHeight = qMax(qRound(full.size.height - inner.size.height), 0);\n}\n\nvoid MainWindow::Private::initTouchBar(\n\t\tNSWindow *window,\n\t\tnot_null<Window::Controller*> controller) {\n\tif (!IsMac10_13OrGreater()) {\n\t\treturn;\n\t}\n\t[NSApplication sharedApplication]\n\t\t.automaticCustomizeTouchBarMenuItemEnabled = true;\n\n\t[window\n\t\tperformSelectorOnMainThread:@selector(setTouchBar:)\n\t\twithObject:[[[RootTouchBar alloc]\n\t\t\tinit:_markdownState.value()\n\t\t\tcontroller:controller\n\t\t\tdomain:(&Core::App().domain())] autorelease]\n\t\twaitUntilDone:true];\n}\n\nvoid MainWindow::Private::setMarkdownEnabledState(\n\t\tUi::MarkdownEnabledState state) {\n\t_markdownState = state;\n}\n\nbool MainWindow::Private::clipboardHasText() {\n\tauto currentChangeCount = static_cast<int>([_generalPasteboard changeCount]);\n\tif (_generalPasteboardChangeCount != currentChangeCount) {\n\t\t_generalPasteboardChangeCount = currentChangeCount;\n\t\t_generalPasteboardHasText = !QGuiApplication::clipboard()->text().isEmpty();\n\t}\n\treturn _generalPasteboardHasText;\n}\n\nMainWindow::Private::~Private() {\n\t[_observer release];\n}\n\nMainWindow::MainWindow(not_null<Window::Controller*> controller)\n: Window::MainWindow(controller)\n, _private(std::make_unique<Private>(this))\n, psMainMenu(this) {\n\t_hideAfterFullScreenTimer.setCallback([this] { hideAndDeactivate(); });\n}\n\nvoid MainWindow::closeWithoutDestroy() {\n\tNSWindow *nsWindow = [reinterpret_cast<NSView*>(winId()) window];\n\n\tauto isFullScreen = (([nsWindow styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen);\n\tif (isFullScreen) {\n\t\t_hideAfterFullScreenTimer.callOnce(kHideAfterFullscreenTimeoutMs);\n\t\t[nsWindow toggleFullScreen:nsWindow];\n\t} else {\n\t\thideAndDeactivate();\n\t}\n}\n\nvoid MainWindow::stateChangedHook(Qt::WindowState state) {\n\tif (_hideAfterFullScreenTimer.isActive()) {\n\t\t_hideAfterFullScreenTimer.callOnce(0);\n\t}\n}\n\nvoid MainWindow::initHook() {\n\t_customTitleHeight = 0;\n\tif (auto view = reinterpret_cast<NSView*>(winId())) {\n\t\tif (auto window = [view window]) {\n\t\t\t_private->setNativeWindow(window, view);\n\t\t\tif (!base::options::lookup<bool>(\n\t\t\t\t\tWindow::kOptionDisableTouchbar).value()) {\n\t\t\t\t_private->initTouchBar(window, &controller());\n\t\t\t} else {\n\t\t\t\tLOG((\"Touch Bar was disabled from Experimental Settings.\"));\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid MainWindow::updateWindowIcon() {\n}\n\nbool MainWindow::nativeEvent(\n\t\tconst QByteArray &eventType,\n\t\tvoid *message,\n\t\tqintptr *result) {\n\tif (message && eventType == \"NSEvent\") {\n\t\tconst auto event = static_cast<NSEvent*>(message);\n\t\tif (PossiblyTextTypingEvent(event)) {\n\t\t\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t\t\timeCompositionStartReceived();\n\t\t\t});\n\t\t} else if ([event type] == NSEventTypePressure) {\n\t\t\tconst auto stage = [event stage];\n\t\t\tif (_lastPressureStage != stage) {\n\t\t\t\t_lastPressureStage = stage;\n\t\t\t\tif (stage == 2) {\n\t\t\t\t\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t\t\t\t\t_forceClicks.fire(QCursor::pos());\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid MainWindow::hideAndDeactivate() {\n\thide();\n}\n\nvoid MainWindow::unreadCounterChangedHook() {\n\tupdateDockCounter();\n}\n\nvoid MainWindow::updateDockCounter() {\n\tconst auto counter = Core::App().unreadBadge();\n\n\tconst auto string = !counter\n\t\t? QString()\n\t\t: (counter < 1000)\n\t\t? QString(\"%1\").arg(counter)\n\t\t: QString(\"..%1\").arg(counter % 100, 2, 10, QChar('0'));\n\t_private->setWindowBadge(string);\n}\n\nvoid MainWindow::createGlobalMenu() {\n\tconst auto ensureWindowShown = [=] {\n\t\tif (isHidden()) {\n\t\t\tshowFromTray();\n\t\t}\n\t};\n\n\tauto main = psMainMenu.addMenu(u\"Telegram\"_q);\n\t{\n\t\tauto callback = [=] {\n\t\t\tensureWindowShown();\n\t\t\tcontroller().show(Box(AboutBox));\n\t\t};\n\t\tmain->addAction(\n\t\t\ttr::lng_mac_menu_about_telegram(\n\t\t\t\ttr::now,\n\t\t\t\tlt_telegram,\n\t\t\t\tu\"Telegram\"_q),\n\t\t\tstd::move(callback))\n\t\t->setMenuRole(QAction::AboutQtRole);\n\t}\n\n\tmain->addSeparator();\n\t{\n\t\tauto callback = [=] {\n\t\t\tensureWindowShown();\n\t\t\tcontroller().showSettings();\n\t\t};\n\t\tauto prefs = main->addAction(\n\t\t\ttr::lng_mac_menu_preferences(tr::now),\n\t\t\tthis,\n\t\t\tstd::move(callback),\n\t\t\tQKeySequence(Qt::ControlModifier | Qt::Key_Comma));\n\t\tprefs->setMenuRole(QAction::PreferencesRole);\n\t\tprefs->setShortcutContext(Qt::WidgetShortcut);\n\t}\n\n\tQMenu *file = psMainMenu.addMenu(tr::lng_mac_menu_file(tr::now));\n\t{\n\t\tauto callback = [=] {\n\t\t\tensureWindowShown();\n\t\t\tcontroller().showLogoutConfirmation();\n\t\t};\n\t\tpsLogout = file->addAction(\n\t\t\ttr::lng_mac_menu_logout(tr::now),\n\t\t\tthis,\n\t\t\tstd::move(callback));\n\t}\n\n\tQMenu *edit = psMainMenu.addMenu(tr::lng_mac_menu_edit(tr::now));\n\tpsUndo = edit->addAction(\n\t\ttr::lng_mac_menu_undo(tr::now),\n\t\tthis,\n\t\t[] { SendKeySequence(Qt::Key_Z, Qt::ControlModifier); },\n\t\tQKeySequence::Undo);\n\tpsUndo->setShortcutContext(Qt::WidgetShortcut);\n\tpsRedo = edit->addAction(\n\t\ttr::lng_mac_menu_redo(tr::now),\n\t\tthis,\n\t\t[] {\n\t\t\tSendKeySequence(\n\t\t\t\tQt::Key_Z,\n\t\t\t\tQt::ControlModifier | Qt::ShiftModifier);\n\t\t},\n\t\tQKeySequence::Redo);\n\tpsRedo->setShortcutContext(Qt::WidgetShortcut);\n\tedit->addSeparator();\n\tpsCut = edit->addAction(\n\t\ttr::lng_mac_menu_cut(tr::now),\n\t\tthis,\n\t\t[] { SendKeySequence(Qt::Key_X, Qt::ControlModifier); },\n\t\tQKeySequence::Cut);\n\tpsCut->setShortcutContext(Qt::WidgetShortcut);\n\tpsCopy = edit->addAction(\n\t\ttr::lng_mac_menu_copy(tr::now),\n\t\tthis,\n\t\t[] { SendKeySequence(Qt::Key_C, Qt::ControlModifier); },\n\t\tQKeySequence::Copy);\n\tpsCopy->setShortcutContext(Qt::WidgetShortcut);\n\tpsPaste = edit->addAction(\n\t\ttr::lng_mac_menu_paste(tr::now),\n\t\tthis,\n\t\t[] { SendKeySequence(Qt::Key_V, Qt::ControlModifier); },\n\t\tQKeySequence::Paste);\n\tpsPaste->setShortcutContext(Qt::WidgetShortcut);\n\tpsDelete = edit->addAction(\n\t\ttr::lng_mac_menu_delete(tr::now),\n\t\tthis,\n\t\t[] { SendKeySequence(Qt::Key_Delete); },\n\t\tQKeySequence(Qt::ControlModifier | Qt::Key_Backspace));\n\tpsDelete->setShortcutContext(Qt::WidgetShortcut);\n\n\tedit->addSeparator();\n\tpsBold = edit->addAction(\n\t\ttr::lng_menu_formatting_bold(tr::now),\n\t\tthis,\n\t\t[] { SendKeySequence(Qt::Key_B, Qt::ControlModifier); },\n\t\tQKeySequence::Bold);\n\tpsBold->setShortcutContext(Qt::WidgetShortcut);\n\tpsItalic = edit->addAction(\n\t\ttr::lng_menu_formatting_italic(tr::now),\n\t\tthis,\n\t\t[] { SendKeySequence(Qt::Key_I, Qt::ControlModifier); },\n\t\tQKeySequence::Italic);\n\tpsItalic->setShortcutContext(Qt::WidgetShortcut);\n\tpsUnderline = edit->addAction(\n\t\ttr::lng_menu_formatting_underline(tr::now),\n\t\tthis,\n\t\t[] { SendKeySequence(Qt::Key_U, Qt::ControlModifier); },\n\t\tQKeySequence::Underline);\n\tpsUnderline->setShortcutContext(Qt::WidgetShortcut);\n\tpsStrikeOut = edit->addAction(\n\t\ttr::lng_menu_formatting_strike_out(tr::now),\n\t\tthis,\n\t\t[] {\n\t\t\tSendKeySequence(\n\t\t\t\tQt::Key_X,\n\t\t\t\tQt::ControlModifier | Qt::ShiftModifier);\n\t\t},\n\t\tUi::kStrikeOutSequence);\n\tpsStrikeOut->setShortcutContext(Qt::WidgetShortcut);\n\tpsBlockquote = edit->addAction(\n\t\ttr::lng_menu_formatting_blockquote(tr::now),\n\t\tthis,\n\t\t[] {\n\t\t\tSendKeySequence(\n\t\t\t\tQt::Key_Period,\n\t\t\t\tQt::ControlModifier | Qt::ShiftModifier);\n\t\t},\n\t\tUi::kBlockquoteSequence);\n\tpsBlockquote->setShortcutContext(Qt::WidgetShortcut);\n\tpsMonospace = edit->addAction(\n\t\ttr::lng_menu_formatting_monospace(tr::now),\n\t\tthis,\n\t\t[] {\n\t\t\tSendKeySequence(\n\t\t\t\tQt::Key_M,\n\t\t\t\tQt::ControlModifier | Qt::ShiftModifier);\n\t\t},\n\t\tUi::kMonospaceSequence);\n\tpsMonospace->setShortcutContext(Qt::WidgetShortcut);\n\tpsClearFormat = edit->addAction(\n\t\ttr::lng_menu_formatting_clear(tr::now),\n\t\tthis,\n\t\t[] {\n\t\t\tSendKeySequence(\n\t\t\t\tQt::Key_N,\n\t\t\t\tQt::ControlModifier | Qt::ShiftModifier);\n\t\t},\n\t\tUi::kClearFormatSequence);\n\tpsClearFormat->setShortcutContext(Qt::WidgetShortcut);\n\n\tedit->addSeparator();\n\tpsSelectAll = edit->addAction(\n\t\ttr::lng_mac_menu_select_all(tr::now),\n\t\tthis,\n\t\t[] { SendKeySequence(Qt::Key_A, Qt::ControlModifier); },\n\t\tQKeySequence::SelectAll);\n\tpsSelectAll->setShortcutContext(Qt::WidgetShortcut);\n\n\tif (!Platform::IsMac26_0OrGreater()) {\n\t\tedit->addSeparator();\n\t\tedit->addAction(\n\t\t\ttr::lng_mac_menu_emoji_and_symbols(\n\t\t\t\ttr::now,\n\t\t\t\tUi::Text::FixAmpersandInAction),\n\t\t\tthis,\n\t\t\t[] { [NSApp orderFrontCharacterPalette:nil]; },\n\t\t\tQKeySequence(Qt::MetaModifier\n\t\t\t\t| Qt::ControlModifier\n\t\t\t\t| Qt::Key_Space)\n\t\t)->setShortcutContext(Qt::WidgetShortcut);\n\t}\n\n\tQMenu *window = psMainMenu.addMenu(tr::lng_mac_menu_window(tr::now));\n\n\twindow->addAction(\n\t\ttr::lng_mac_menu_fullscreen(tr::now),\n\t\tthis,\n\t\t[=] {\n\t\t\tNSWindow *nsWindow = [reinterpret_cast<NSView*>(winId()) window];\n\t\t\t[nsWindow toggleFullScreen:nsWindow];\n\t\t},\n\t\tQKeySequence(Qt::MetaModifier | Qt::ControlModifier | Qt::Key_F)\n\t)->setShortcutContext(Qt::WidgetShortcut);\n\twindow->addSeparator();\n\n\tpsContacts = window->addAction(tr::lng_mac_menu_contacts(tr::now));\n\tconnect(psContacts, &QAction::triggered, psContacts, crl::guard(this, [=] {\n\t\tExpects(sessionController() != nullptr && !controller().locked());\n\n\t\tensureWindowShown();\n\t\tsessionController()->show(PrepareContactsBox(sessionController()));\n\t}));\n\t{\n\t\tauto callback = [=] {\n\t\t\tExpects(sessionController() != nullptr && !controller().locked());\n\n\t\t\tensureWindowShown();\n\t\t\tsessionController()->showAddContact();\n\t\t};\n\t\tpsAddContact = window->addAction(\n\t\t\ttr::lng_mac_menu_add_contact(tr::now),\n\t\t\tthis,\n\t\t\tstd::move(callback));\n\t}\n\twindow->addSeparator();\n\t{\n\t\tauto callback = [=] {\n\t\t\tExpects(sessionController() != nullptr && !controller().locked());\n\n\t\t\tensureWindowShown();\n\t\t\tsessionController()->showNewGroup();\n\t\t};\n\t\tpsNewGroup = window->addAction(\n\t\t\ttr::lng_mac_menu_new_group(tr::now),\n\t\t\tthis,\n\t\t\tstd::move(callback));\n\t}\n\t{\n\t\tauto callback = [=] {\n\t\t\tExpects(sessionController() != nullptr && !controller().locked());\n\n\t\t\tensureWindowShown();\n\t\t\tsessionController()->showNewChannel();\n\t\t};\n\t\tpsNewChannel = window->addAction(\n\t\t\ttr::lng_mac_menu_new_channel(tr::now),\n\t\t\tthis,\n\t\t\tstd::move(callback));\n\t}\n\twindow->addSeparator();\n\tpsShowTelegram = window->addAction(\n\t\ttr::lng_mac_menu_show(tr::now),\n\t\tthis,\n\t\t[=] { showFromTray(); });\n\n\tupdateGlobalMenu();\n}\n\nvoid MainWindow::updateGlobalMenuHook() {\n\tif (!positionInited()) {\n\t\treturn;\n\t}\n\n\tauto focused = QApplication::focusWidget();\n\tbool canUndo = false, canRedo = false, canCut = false, canCopy = false, canPaste = false, canDelete = false, canSelectAll = false;\n\tauto clipboardHasText = _private->clipboardHasText();\n\tauto markdownState = Ui::MarkdownEnabledState();\n\tif (auto edit = qobject_cast<QLineEdit*>(focused)) {\n\t\tcanCut = canCopy = canDelete = edit->hasSelectedText();\n\t\tcanSelectAll = !edit->text().isEmpty();\n\t\tcanUndo = edit->isUndoAvailable();\n\t\tcanRedo = edit->isRedoAvailable();\n\t\tcanPaste = clipboardHasText;\n\t} else if (auto edit = qobject_cast<QTextEdit*>(focused)) {\n\t\tcanCut = canCopy = canDelete = edit->textCursor().hasSelection();\n\t\tcanSelectAll = !edit->document()->isEmpty();\n\t\tcanUndo = edit->document()->isUndoAvailable();\n\t\tcanRedo = edit->document()->isRedoAvailable();\n\t\tcanPaste = clipboardHasText;\n\t\tif (canCopy) {\n\t\t\tif (const auto inputField = dynamic_cast<Ui::InputField*>(\n\t\t\t\t\tfocused->parentWidget())) {\n\t\t\t\tmarkdownState = inputField->markdownEnabledState();\n\t\t\t}\n\t\t}\n\t} else if (auto list = dynamic_cast<HistoryInner*>(focused)) {\n\t\tcanCopy = list->canCopySelected();\n\t\tcanDelete = list->canDeleteSelected();\n\t}\n\n\t_private->setMarkdownEnabledState(markdownState);\n\n\tupdateIsActive();\n\tconst auto logged = (sessionController() != nullptr);\n\tconst auto inactive = !logged || controller().locked();\n\tconst auto support = logged\n\t\t&& sessionController()->session().supportMode();\n\tForceDisabled(psLogout, !logged && !Core::App().passcodeLocked());\n\tForceDisabled(psUndo, !canUndo);\n\tForceDisabled(psRedo, !canRedo);\n\tForceDisabled(psCut, !canCut);\n\tForceDisabled(psCopy, !canCopy);\n\tForceDisabled(psPaste, !canPaste);\n\tForceDisabled(psDelete, !canDelete);\n\tForceDisabled(psSelectAll, !canSelectAll);\n\tForceDisabled(psContacts, inactive || support);\n\tForceDisabled(psAddContact, inactive);\n\tForceDisabled(psNewGroup, inactive || support);\n\tForceDisabled(psNewChannel, inactive || support);\n\tForceDisabled(psShowTelegram, isActive());\n\n\tconst auto diabled = [=](const QString &tag) {\n\t\treturn !markdownState.enabledForTag(tag);\n\t};\n\tusing Field = Ui::InputField;\n\tForceDisabled(psBold, diabled(Field::kTagBold));\n\tForceDisabled(psItalic, diabled(Field::kTagItalic));\n\tForceDisabled(psUnderline, diabled(Field::kTagUnderline));\n\tForceDisabled(psStrikeOut, diabled(Field::kTagStrikeOut));\n\tForceDisabled(psBlockquote, diabled(Field::kTagBlockquote));\n\tForceDisabled(\n\t\tpsMonospace,\n\t\tdiabled(Field::kTagPre) || diabled(Field::kTagCode));\n\tForceDisabled(psClearFormat, markdownState.disabled());\n}\n\nbool MainWindow::eventFilter(QObject *obj, QEvent *evt) {\n\tQEvent::Type t = evt->type();\n\tif (t == QEvent::FocusIn || t == QEvent::FocusOut) {\n\t\tif (qobject_cast<QLineEdit*>(obj) || qobject_cast<QTextEdit*>(obj) || dynamic_cast<HistoryInner*>(obj)) {\n\t\t\tupdateGlobalMenu();\n\t\t}\n\t}\n\treturn Window::MainWindow::eventFilter(obj, evt);\n}\n\nMainWindow::~MainWindow() {\n}\n\nint32 ScreenNameChecksum(const QString &name) {\n\treturn Window::DefaultScreenNameChecksum(name);\n}\n\nint32 ScreenNameChecksum(const QScreen *screen) {\n\treturn ScreenNameChecksum(screen->name());\n}\n\nQString ScreenDisplayLabel(const QScreen *screen) {\n\treturn screen ? screen->name() : QString();\n}\n\n} // namespace\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/notifications_manager_mac.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_notifications_manager.h\"\n#include \"base/weak_ptr.h\"\n\nnamespace Platform {\nnamespace Notifications {\n\nclass Manager : public Window::Notifications::NativeManager, public base::has_weak_ptr {\npublic:\n\tManager(Window::Notifications::System *system);\n\t~Manager();\n\nprotected:\n\tvoid doShowNativeNotification(\n\t\tNotificationInfo &&info,\n\t\tUi::PeerUserpicView &userpicView) override;\n\tvoid doClearAllFast() override;\n\tvoid doClearFromItem(not_null<HistoryItem*> item) override;\n\tvoid doClearFromTopic(not_null<Data::ForumTopic*> topic) override;\n\tvoid doClearFromSublist(not_null<Data::SavedSublist*> sublist) override;\n\tvoid doClearFromHistory(not_null<History*> history) override;\n\tvoid doClearFromSession(not_null<Main::Session*> session) override;\n\tQString accountNameSeparator() override;\n\tbool doSkipToast() const override;\n\tvoid doMaybePlaySound(Fn<void()> playSound) override;\n\tvoid doMaybeFlashBounce(Fn<void()> flashBounce) override;\n\nprivate:\n\tclass Private;\n\tconst std::unique_ptr<Private> _private;\n\n};\n\n} // namespace Notifications\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/notifications_manager_mac.h\"\n\n#include \"base/platform/base_platform_info.h\"\n#include \"base/options.h\"\n#include \"base/platform/mac/base_utilities_mac.h\"\n#include \"base/random.h\"\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"lang/lang_cloud_manager.h\"\n#include \"lang/lang_instance.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mainwindow.h\"\n#include \"platform/platform_specific.h\"\n#include \"ui/empty_userpic.h\"\n#include \"window/notifications_utilities.h\"\n#include \"styles/style_window.h\"\n\n#include <thread>\n#include <Cocoa/Cocoa.h>\n\nnamespace {\n\nconstexpr auto kQuerySettingsEachMs = crl::time(1000);\nconstexpr auto kCacheExpirationWeeks = 5;\nconstexpr auto kCacheExpirationSeconds = kCacheExpirationWeeks * 7 * 24 * 60 * 60;\n\nNSString *const kTelegramMarkAsReadText = @\"TelegramMarkAsReadText\";\nNSString *const kTelegramMarkAsReadTimestamp = @\"TelegramMarkAsReadTimestamp\";\nNSString *const kTelegramMarkAsReadLanguageCode = @\"TelegramMarkAsReadLanguageCode\";\n\ncrl::time LastSettingsQueryMs/* = 0*/;\nbool DoNotDisturbEnabled/* = false*/;\n\n[[nodiscard]] bool ShouldQuerySettings() {\n\tconst auto now = crl::now();\n\tif (LastSettingsQueryMs > 0 && now <= LastSettingsQueryMs + kQuerySettingsEachMs) {\n\t\treturn false;\n\t}\n\tLastSettingsQueryMs = now;\n\treturn true;\n}\n\n[[nodiscard]] QString LibraryPath() {\n\tstatic const auto result = [] {\n\t\tNSURL *url = [[NSFileManager defaultManager] URLForDirectory:NSLibraryDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];\n\t\treturn url\n\t\t\t? QString::fromUtf8([[url path] fileSystemRepresentation])\n\t\t\t: QString();\n\t}();\n\treturn result;\n}\n\nvoid queryDoNotDisturbState() {\n\tif (!ShouldQuerySettings()) {\n\t\treturn;\n\t}\n\tBoolean isKeyValid;\n\tconst auto doNotDisturb = CFPreferencesGetAppBooleanValue(\n\t\tCFSTR(\"doNotDisturb\"),\n\t\tCFSTR(\"com.apple.notificationcenterui\"),\n\t\t&isKeyValid);\n\tDoNotDisturbEnabled = isKeyValid\n\t\t? doNotDisturb\n\t\t: false;\n}\n\nusing Manager = Platform::Notifications::Manager;\n\n} // namespace\n\n@interface NotificationDelegate : NSObject<NSUserNotificationCenterDelegate> {\n}\n\n- (id) initWithManager:(base::weak_ptr<Manager>)manager managerId:(uint64)managerId;\n- (void) userNotificationCenter:(NSUserNotificationCenter*)center didActivateNotification:(NSUserNotification*)notification;\n- (BOOL) userNotificationCenter:(NSUserNotificationCenter*)center shouldPresentNotification:(NSUserNotification*)notification;\n\n@end // @interface NotificationDelegate\n\n@implementation NotificationDelegate {\n\tbase::weak_ptr<Manager> _manager;\n\tuint64 _managerId;\n\n}\n\n- (id) initWithManager:(base::weak_ptr<Manager>)manager managerId:(uint64)managerId {\n\tif (self = [super init]) {\n\t\t_manager = manager;\n\t\t_managerId = managerId;\n\t}\n\treturn self;\n}\n\n- (void) userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification {\n\tNSDictionary *notificationUserInfo = [notification userInfo];\n\tNSNumber *managerIdObject = [notificationUserInfo objectForKey:@\"manager\"];\n\tauto notificationManagerId = managerIdObject ? [managerIdObject unsignedLongLongValue] : 0ULL;\n\tDEBUG_LOG((\"Received notification with instance %1, mine: %2\").arg(notificationManagerId).arg(_managerId));\n\tif (notificationManagerId != _managerId) { // other app instance notification\n\t\tcrl::on_main([] {\n\t\t\t// Usually we show and activate main window when the application\n\t\t\t// is activated (receives applicationDidBecomeActive: notification).\n\t\t\t//\n\t\t\t// This is used for window show in Cmd+Tab switching to the application.\n\t\t\t//\n\t\t\t// But when a notification arrives sometimes macOS still activates the app\n\t\t\t// and we receive applicationDidBecomeActive: notification even if the\n\t\t\t// notification was sent by another instance of the application. In that case\n\t\t\t// we set a flag for a couple of seconds to ignore this app activation.\n\t\t\tobjc_ignoreApplicationActivationRightNow();\n\t\t});\n\t\treturn;\n\t}\n\n\tNSNumber *sessionObject = [notificationUserInfo objectForKey:@\"session\"];\n\tconst auto notificationSessionId = sessionObject ? [sessionObject unsignedLongLongValue] : 0;\n\tif (!notificationSessionId) {\n\t\tLOG((\"App Error: A notification with unknown session was received\"));\n\t\treturn;\n\t}\n\tNSNumber *peerObject = [notificationUserInfo objectForKey:@\"peer\"];\n\tconst auto notificationPeerId = peerObject ? [peerObject unsignedLongLongValue] : 0ULL;\n\tif (!notificationPeerId) {\n\t\tLOG((\"App Error: A notification with unknown peer was received\"));\n\t\treturn;\n\t}\n\tNSNumber *topicObject = [notificationUserInfo objectForKey:@\"topic\"];\n\tif (!topicObject) {\n\t\tLOG((\"App Error: A notification with unknown topic was received\"));\n\t\treturn;\n\t}\n\tconst auto notificationTopicRootId = [topicObject longLongValue];\n\tNSNumber *monoforumPeerObject = [notificationUserInfo objectForKey:@\"monoforumpeer\"];\n\tif (!monoforumPeerObject) {\n\t\tLOG((\"App Error: A notification with unknown monoforum peer was received\"));\n\t\treturn;\n\t}\n\tconst auto notificationMonoforumPeerId = [monoforumPeerObject unsignedLongLongValue];\n\n\tNSNumber *msgObject = [notificationUserInfo objectForKey:@\"msgid\"];\n\tconst auto notificationMsgId = msgObject ? [msgObject longLongValue] : 0LL;\n\n\tconst auto my = Window::Notifications::Manager::NotificationId{\n\t\t.contextId = Manager::ContextId{\n\t\t\t.sessionId = notificationSessionId,\n\t\t\t.peerId = PeerId(notificationPeerId),\n\t\t\t.topicRootId = MsgId(notificationTopicRootId),\n\t\t\t.monoforumPeerId = PeerId(notificationMonoforumPeerId),\n\t\t},\n\t\t.msgId = notificationMsgId,\n\t};\n\tif (notification.activationType == NSUserNotificationActivationTypeReplied) {\n\t\tconst auto notificationReply = QString::fromUtf8([[[notification response] string] UTF8String]);\n\t\tconst auto manager = _manager;\n\t\tcrl::on_main(manager, [=] {\n\t\t\tmanager->notificationReplied(my, { notificationReply, {} });\n\t\t});\n\t} else if (notification.activationType == NSUserNotificationActivationTypeContentsClicked) {\n\t\tconst auto manager = _manager;\n\t\tcrl::on_main(manager, [=] {\n\t\t\tmanager->notificationActivated(my);\n\t\t});\n\t}\n\tif (notification.activationType == NSUserNotificationActivationTypeAdditionalActionClicked\n\t\t|| notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) {\n\t\tconst auto manager = _manager;\n\t\tNSString *actionId = nil;\n\t\tif (notification.activationType == NSUserNotificationActivationTypeAdditionalActionClicked\n\t\t\t&& [notification respondsToSelector:@selector(additionalActivationAction)]) {\n\t\t\tactionId = notification.additionalActivationAction.identifier;\n\t\t}\n\t\tif (!actionId) {\n\t\t\tactionId = [notificationUserInfo objectForKey:@\"actionId\"];\n\t\t}\n\t\tif (actionId) {\n\t\t\tconst auto actionStr = QString::fromNSString(actionId);\n\t\t\tcrl::on_main(manager, [=] {\n\t\t\t\tmanager->notificationActionActivated(my, actionStr);\n\t\t\t});\n\t\t}\n\t}\n\n\t[center removeDeliveredNotification: notification];\n}\n\n- (BOOL) userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification {\n\treturn YES;\n}\n\n@end // @implementation NotificationDelegate\n\nnamespace Platform {\nnamespace Notifications {\n\nbool SkipToastForCustom() {\n\treturn false;\n}\n\nvoid MaybePlaySoundForCustom(Fn<void()> playSound) {\n\tplaySound();\n}\n\nvoid MaybeFlashBounceForCustom(Fn<void()> flashBounce) {\n\tflashBounce();\n}\n\nbool WaitForInputForCustom() {\n\treturn true;\n}\n\nbool Supported() {\n\treturn true;\n}\n\nbool Enforced() {\n\treturn Supported();\n}\n\nbool ByDefault() {\n\treturn Supported();\n}\n\nbool VolumeSupported() {\n\treturn false;\n}\n\nvoid Create(Window::Notifications::System *system) {\n\tsystem->setManager([=] { return std::make_unique<Manager>(system); });\n}\n\nclass Manager::Private : public QObject {\npublic:\n\tPrivate(Manager *manager);\n\n\tvoid showNotification(\n\t\tNotificationInfo &&info,\n\t\tUi::PeerUserpicView &userpicView);\n\tvoid clearAll();\n\tvoid clearFromItem(not_null<HistoryItem*> item);\n\tvoid clearFromTopic(not_null<Data::ForumTopic*> topic);\n\tvoid clearFromSublist(not_null<Data::SavedSublist*> sublist);\n\tvoid clearFromHistory(not_null<History*> history);\n\tvoid clearFromSession(not_null<Main::Session*> session);\n\tvoid updateDelegate();\n\n\t~Private();\n\nprivate:\n\ttemplate <typename Task>\n\tvoid putClearTask(Task task);\n\n\tvoid clearingThreadLoop();\n\tvoid initCachedMarkAsReadText();\n\n\tconst uint64 _managerId = 0;\n\tQString _managerIdString;\n\n\tNotificationDelegate *_delegate = nullptr;\n\n\tstd::thread _clearingThread;\n\tstd::mutex _clearingMutex;\n\tstd::condition_variable _clearingCondition;\n\n\tstruct ClearFromItem {\n\t\tNotificationId id;\n\t};\n\tstruct ClearFromTopic {\n\t\tContextId contextId;\n\t};\n\tstruct ClearFromSublist {\n\t\tContextId contextId;\n\t};\n\tstruct ClearFromHistory {\n\t\tContextId partialContextId;\n\t};\n\tstruct ClearFromSession {\n\t\tuint64 sessionId = 0;\n\t};\n\tstruct ClearAll {\n\t};\n\tstruct ClearFinish {\n\t};\n\tusing ClearTask = std::variant<\n\t\tClearFromItem,\n\t\tClearFromTopic,\n\t\tClearFromSublist,\n\t\tClearFromHistory,\n\t\tClearFromSession,\n\t\tClearAll,\n\t\tClearFinish>;\n\tstd::vector<ClearTask> _clearingTasks;\n\n\tMedia::Audio::LocalDiskCache _sounds;\n\tNSString *_cachedMarkAsReadText = nil;\n\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] QString ResolveSoundsFolder() {\n\tNSArray *paths = NSSearchPathForDirectoriesInDomains(\n\t\tNSLibraryDirectory,\n\t\tNSUserDomainMask,\n\t\tYES);\n\tNSString *library = [paths firstObject];\n\tNSString *sounds = [library stringByAppendingPathComponent : @\"Sounds\"];\n\treturn NS2QString(sounds);\n}\n\nvoid AddActionIdToNotification(\n\t\tNSUserNotification *notification,\n\t\tNSString *actionId) {\n\tNSMutableDictionary *mutableUserInfo\n\t\t= [[notification userInfo] mutableCopy];\n\t[mutableUserInfo setObject:actionId forKey:@\"actionId\"];\n\t[notification setUserInfo:mutableUserInfo];\n\t[mutableUserInfo release];\n}\n\nManager::Private::Private(Manager *manager)\n: _managerId(base::RandomValue<uint64>())\n, _managerIdString(QString::number(_managerId))\n, _delegate([[NotificationDelegate alloc] initWithManager:manager managerId:_managerId])\n, _sounds(ResolveSoundsFolder()) {\n\tCore::App().settings().workModeValue(\n\t) | rpl::on_next([=](Core::Settings::WorkMode mode) {\n\t\t// We need to update the delegate _after_ the tray icon change was done in Qt.\n\t\t// Because Qt resets the delegate.\n\t\tcrl::on_main(this, [=] {\n\t\t\tupdateDelegate();\n\t\t});\n\t}, _lifetime);\n\n\tinitCachedMarkAsReadText();\n}\n\nvoid Manager::Private::showNotification(\n\t\tNotificationInfo &&info,\n\t\tUi::PeerUserpicView &userpicView) {\n\t@autoreleasepool {\n\n\tconst auto peer = info.peer;\n\tNSUserNotification *notification = [[[NSUserNotification alloc] init] autorelease];\n\tif ([notification respondsToSelector:@selector(setIdentifier:)]) {\n\t\tauto identifier = _managerIdString\n\t\t\t+ '_'\n\t\t\t+ QString::number(peer->id.value)\n\t\t\t+ '_'\n\t\t\t+ QString::number(info.itemId.bare);\n\t\tauto identifierValue = Q2NSString(identifier);\n\t\t[notification setIdentifier:identifierValue];\n\t}\n\t[notification setUserInfo:\n\t\t[NSDictionary dictionaryWithObjectsAndKeys:\n\t\t\t[NSNumber numberWithUnsignedLongLong:peer->session().uniqueId()],\n\t\t\t@\"session\",\n\t\t\t[NSNumber numberWithUnsignedLongLong:peer->id.value],\n\t\t\t@\"peer\",\n\t\t\t[NSNumber numberWithLongLong:info.topicRootId.bare],\n\t\t\t@\"topic\",\n\t\t\t[NSNumber numberWithUnsignedLongLong:info.monoforumPeerId.value],\n\t\t\t@\"monoforumpeer\",\n\t\t\t[NSNumber numberWithLongLong:info.itemId.bare],\n\t\t\t@\"msgid\",\n\t\t\t[NSNumber numberWithUnsignedLongLong:_managerId],\n\t\t\t@\"manager\",\n\t\t\tnil]];\n\n\t[notification setTitle:Q2NSString(info.title)];\n\t[notification setSubtitle:Q2NSString(info.subtitle)];\n\t[notification setInformativeText:Q2NSString(info.message)];\n\tif (!info.options.hideNameAndPhoto\n\t\t&& [notification respondsToSelector:@selector(setContentImage:)]) {\n\t\tNSImage *img = Q2NSImage(\n\t\t\tWindow::Notifications::GenerateUserpic(peer, userpicView));\n\t\t[notification setContentImage:img];\n\t}\n\n\tif (!info.actions.empty()\n\t\t&& [notification respondsToSelector:@selector(setAdditionalActions:)]) {\n\t\tNSMutableArray *actions = [NSMutableArray array];\n\t\tif (!info.options.hideMarkAsRead) {\n\t\t\t[actions addObject:[NSUserNotificationAction\n\t\t\t\tactionWithIdentifier:@\"markAsRead\"\n\t\t\t\ttitle:_cachedMarkAsReadText]];\n\t\t}\n\t\tfor (const auto &action : info.actions) {\n\t\t\t[actions addObject:[NSUserNotificationAction\n\t\t\t\tactionWithIdentifier:Q2NSString(action.id)\n\t\t\t\ttitle:Q2NSString(action.text)]];\n\t\t}\n\t\tif (!info.options.hideReplyButton\n\t\t\t&& [notification respondsToSelector:@selector(setHasReplyButton:)]) {\n\t\t\t[notification setHasReplyButton:YES];\n\t\t}\n\t\t[notification setAdditionalActions:actions];\n\t\tAddActionIdToNotification(\n\t\t\tnotification,\n\t\t\t[[actions firstObject] identifier]);\n\t} else if (!info.options.hideReplyButton\n\t\t&& !info.options.hideMarkAsRead\n\t\t&& [notification respondsToSelector:@selector(setHasReplyButton:)]\n\t\t&& [notification respondsToSelector:@selector(setAdditionalActions:)]) {\n\t\t[notification setHasReplyButton:YES];\n\n\t\tAddActionIdToNotification(notification, @\"markAsRead\");\n\n\t\t[notification setAdditionalActions:@[\n\t\t\t[NSUserNotificationAction\n\t\t\t\tactionWithIdentifier:@\"markAsRead\"\n\t\t\t\ttitle:_cachedMarkAsReadText]\n\t\t]];\n\t} else if (!info.options.hideReplyButton\n\t\t&& [notification respondsToSelector:@selector(setHasReplyButton:)]) {\n\t\t[notification setHasReplyButton:YES];\n\t} else if (!info.options.hideMarkAsRead\n\t\t&& [notification respondsToSelector:@selector(setHasActionButton:)]) {\n\t\t[notification setHasActionButton:YES];\n\t\t[notification\n\t\t\tsetActionButtonTitle:_cachedMarkAsReadText];\n\n\t\tAddActionIdToNotification(notification, @\"markAsRead\");\n\t}\n\n\tconst auto sound = info.sound ? info.sound() : Media::Audio::LocalSound();\n\tif (sound) {\n\t\t[notification setSoundName:Q2NSString(_sounds.name(sound))];\n\t} else {\n\t\t[notification setSoundName:nil];\n\t}\n\n\tNSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];\n\t[center deliverNotification:notification];\n\n\t}\n}\n\nvoid Manager::Private::clearingThreadLoop() {\n\tauto finished = false;\n\twhile (!finished) {\n\t\tauto clearAll = false;\n\t\tauto clearFromItems = base::flat_set<NotificationId>();\n\t\tauto clearFromTopics = base::flat_set<ContextId>();\n\t\tauto clearFromSublists = base::flat_set<ContextId>();\n\t\tauto clearFromHistories = base::flat_set<ContextId>();\n\t\tauto clearFromSessions = base::flat_set<uint64>();\n\t\t{\n\t\t\tstd::unique_lock<std::mutex> lock(_clearingMutex);\n\t\t\twhile (_clearingTasks.empty()) {\n\t\t\t\t_clearingCondition.wait(lock);\n\t\t\t}\n\t\t\tfor (auto &task : _clearingTasks) {\n\t\t\t\tv::match(task, [&](ClearFinish) {\n\t\t\t\t\tfinished = true;\n\t\t\t\t\tclearAll = true;\n\t\t\t\t}, [&](ClearAll) {\n\t\t\t\t\tclearAll = true;\n\t\t\t\t}, [&](const ClearFromItem &value) {\n\t\t\t\t\tclearFromItems.emplace(value.id);\n\t\t\t\t}, [&](const ClearFromTopic &value) {\n\t\t\t\t\tclearFromTopics.emplace(value.contextId);\n\t\t\t\t}, [&](const ClearFromSublist &value) {\n\t\t\t\t\tclearFromSublists.emplace(value.contextId);\n\t\t\t\t}, [&](const ClearFromHistory &value) {\n\t\t\t\t\tclearFromHistories.emplace(value.partialContextId);\n\t\t\t\t}, [&](const ClearFromSession &value) {\n\t\t\t\t\tclearFromSessions.emplace(value.sessionId);\n\t\t\t\t});\n\t\t\t}\n\t\t\t_clearingTasks.clear();\n\t\t}\n\n\t\t@autoreleasepool {\n\n\t\tauto clearBySpecial = [&](NSDictionary *notificationUserInfo) {\n\t\t\tNSNumber *sessionObject = [notificationUserInfo objectForKey:@\"session\"];\n\t\t\tconst auto notificationSessionId = sessionObject ? [sessionObject unsignedLongLongValue] : 0;\n\t\t\tif (!notificationSessionId) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tNSNumber *peerObject = [notificationUserInfo objectForKey:@\"peer\"];\n\t\t\tconst auto notificationPeerId = peerObject ? [peerObject unsignedLongLongValue] : 0;\n\t\t\tif (!notificationPeerId) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tNSNumber *topicObject = [notificationUserInfo objectForKey:@\"topic\"];\n\t\t\tif (!topicObject) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst auto notificationTopicRootId = [topicObject longLongValue];\n\t\t\tNSNumber *monoforumPeerObject = [notificationUserInfo objectForKey:@\"monoforumpeer\"];\n\t\t\tif (!monoforumPeerObject) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst auto notificationMonoforumPeerId = [monoforumPeerObject unsignedLongLongValue];\n\t\t\tNSNumber *msgObject = [notificationUserInfo objectForKey:@\"msgid\"];\n\t\t\tconst auto msgId = msgObject ? [msgObject longLongValue] : 0LL;\n\t\t\tconst auto partialContextId = ContextId{\n\t\t\t\t.sessionId = notificationSessionId,\n\t\t\t\t.peerId = PeerId(notificationPeerId),\n\t\t\t};\n\t\t\tconst auto contextId = notificationTopicRootId\n\t\t\t? ContextId{\n\t\t\t\t.sessionId = notificationSessionId,\n\t\t\t\t.peerId = PeerId(notificationPeerId),\n\t\t\t\t.topicRootId = MsgId(notificationTopicRootId),\n\t\t\t}\n\t\t\t: notificationMonoforumPeerId\n\t\t\t? ContextId{\n\t\t\t\t.sessionId = notificationSessionId,\n\t\t\t\t.peerId = PeerId(notificationPeerId),\n\t\t\t\t.monoforumPeerId = PeerId(notificationMonoforumPeerId),\n\t\t\t}\n\t\t\t: partialContextId;\n\t\t\tconst auto id = NotificationId{ contextId, MsgId(msgId) };\n\t\t\treturn clearFromSessions.contains(notificationSessionId)\n\t\t\t\t|| clearFromHistories.contains(partialContextId)\n\t\t\t\t|| clearFromTopics.contains(contextId)\n\t\t\t\t|| clearFromSublists.contains(contextId)\n\t\t\t\t|| (msgId && clearFromItems.contains(id));\n\t\t};\n\n\t\tNSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];\n\t\tNSArray *notificationsList = [center deliveredNotifications];\n\t\tfor (id notification in notificationsList) {\n\t\t\tNSDictionary *notificationUserInfo = [notification userInfo];\n\t\t\tNSNumber *managerIdObject = [notificationUserInfo objectForKey:@\"manager\"];\n\t\t\tauto notificationManagerId = managerIdObject ? [managerIdObject unsignedLongLongValue] : 0ULL;\n\t\t\tif (notificationManagerId == _managerId) {\n\t\t\t\tif (clearAll || clearBySpecial(notificationUserInfo)) {\n\t\t\t\t\t[center removeDeliveredNotification:notification];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t}\n\t}\n}\n\ntemplate <typename Task>\nvoid Manager::Private::putClearTask(Task task) {\n\tif (!_clearingThread.joinable()) {\n\t\t_clearingThread = std::thread([this] { clearingThreadLoop(); });\n\t}\n\n\tstd::unique_lock<std::mutex> lock(_clearingMutex);\n\t_clearingTasks.push_back(task);\n\t_clearingCondition.notify_one();\n}\n\nvoid Manager::Private::clearAll() {\n\tputClearTask(ClearAll());\n}\n\nvoid Manager::Private::clearFromItem(not_null<HistoryItem*> item) {\n\tputClearTask(ClearFromItem{ ContextId{\n\t\t.sessionId = item->history()->session().uniqueId(),\n\t\t.peerId = item->history()->peer->id,\n\t\t.topicRootId = item->topicRootId(),\n\t\t.monoforumPeerId = item->sublistPeerId(),\n\t}, item->id });\n}\n\nvoid Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) {\n\tputClearTask(ClearFromTopic{ ContextId{\n\t\t.sessionId = topic->session().uniqueId(),\n\t\t.peerId = topic->history()->peer->id,\n\t\t.topicRootId = topic->rootId(),\n\t} });\n}\n\nvoid Manager::Private::clearFromSublist(\n\t\tnot_null<Data::SavedSublist*> sublist) {\n\tputClearTask(ClearFromSublist{ ContextId{\n\t\t.sessionId = sublist->session().uniqueId(),\n\t\t.peerId = sublist->owningHistory()->peer->id,\n\t\t.monoforumPeerId = sublist->sublistPeer()->id,\n\t} });\n}\n\nvoid Manager::Private::clearFromHistory(not_null<History*> history) {\n\tputClearTask(ClearFromHistory{ ContextId{\n\t\t.sessionId = history->session().uniqueId(),\n\t\t.peerId = history->peer->id,\n\t} });\n}\n\nvoid Manager::Private::clearFromSession(not_null<Main::Session*> session) {\n\tputClearTask(ClearFromSession{ session->uniqueId() });\n}\n\nvoid Manager::Private::updateDelegate() {\n\tNSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];\n\t[center setDelegate:_delegate];\n}\n\nvoid Manager::Private::initCachedMarkAsReadText() {\n\tconst auto langId = Lang::GetInstance().id();\n\n\tconst auto preferredLang = [[NSLocale preferredLanguages] firstObject];\n\tconst auto languageCode\n\t\t= [[NSLocale localeWithLocaleIdentifier:preferredLang] languageCode];\n\n\tconst auto defaults = [NSUserDefaults standardUserDefaults];\n\tconst auto cachedText = static_cast<NSString*>([defaults\n\t\tstringForKey:kTelegramMarkAsReadText]);\n\tconst auto cachedTimestamp = static_cast<NSNumber*>([defaults\n\t\tobjectForKey:kTelegramMarkAsReadTimestamp]);\n\tconst auto cachedLanguageCode = static_cast<NSString*>([defaults\n\t\tstringForKey:kTelegramMarkAsReadLanguageCode]);\n\n\tconst auto now = base::unixtime::now();\n\tconst auto shouldRefresh = !cachedTimestamp\n\t\t|| (now - [cachedTimestamp longLongValue]) > kCacheExpirationSeconds\n\t\t|| ![cachedLanguageCode isEqualToString:languageCode];\n\n\tif (cachedText && !shouldRefresh) {\n\t\t_cachedMarkAsReadText = [cachedText retain];\n\t} else {\n\t\t_cachedMarkAsReadText\n\t\t\t= [Q2NSString(tr::lng_context_mark_read(tr::now)) retain];\n\t}\n\n\tif (langId == NS2QString(languageCode)) {\n\t\t[defaults\n\t\t\tsetObject:Q2NSString(tr::lng_context_mark_read(tr::now))\n\t\t\tforKey:kTelegramMarkAsReadText];\n\t\t[defaults\n\t\t\tsetObject:@(base::unixtime::now())\n\t\t\tforKey:kTelegramMarkAsReadTimestamp];\n\t\t[defaults\n\t\t\tsetObject:languageCode\n\t\t\tforKey:kTelegramMarkAsReadLanguageCode];\n\t} else if (shouldRefresh) {\n\t\tLang::CurrentCloudManager().getValueForLang(\n\t\t\tu\"lng_context_mark_read\"_q,\n\t\t\tNS2QString(languageCode),\n\t\t\t[=](const QString &r) {\n\t\t\t\tif (r != NS2QString(_cachedMarkAsReadText)) {\n\t\t\t\t\t[_cachedMarkAsReadText release];\n\t\t\t\t\t_cachedMarkAsReadText = [Q2NSString(r) retain];\n\t\t\t\t}\n\t\t\t\t[defaults\n\t\t\t\t\tsetObject:Q2NSString(r)\n\t\t\t\t\tforKey:kTelegramMarkAsReadText];\n\t\t\t\t[defaults\n\t\t\t\t\tsetObject:@(base::unixtime::now())\n\t\t\t\t\tforKey:kTelegramMarkAsReadTimestamp];\n\t\t\t\t[defaults\n\t\t\t\t\tsetObject:languageCode\n\t\t\t\t\tforKey:kTelegramMarkAsReadLanguageCode];\n\t\t\t});\n\t}\n}\n\nManager::Private::~Private() {\n\tif (_clearingThread.joinable()) {\n\t\tputClearTask(ClearFinish());\n\t\t_clearingThread.join();\n\t}\n\tNSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];\n\t[center setDelegate:nil];\n\t[_delegate release];\n\t[_cachedMarkAsReadText release];\n}\n\nManager::Manager(Window::Notifications::System *system) : NativeManager(system)\n, _private(std::make_unique<Private>(this)) {\n}\n\nManager::~Manager() = default;\n\nvoid Manager::doShowNativeNotification(\n\t\tNotificationInfo &&info,\n\t\tUi::PeerUserpicView &userpicView) {\n\t_private->showNotification(std::move(info), userpicView);\n}\n\nvoid Manager::doClearAllFast() {\n\t_private->clearAll();\n}\n\nvoid Manager::doClearFromItem(not_null<HistoryItem*> item) {\n\t_private->clearFromItem(item);\n}\n\nvoid Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) {\n\t_private->clearFromTopic(topic);\n}\n\nvoid Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) {\n\t_private->clearFromSublist(sublist);\n}\n\nvoid Manager::doClearFromHistory(not_null<History*> history) {\n\t_private->clearFromHistory(history);\n}\n\nvoid Manager::doClearFromSession(not_null<Main::Session*> session) {\n\t_private->clearFromSession(session);\n}\n\nQString Manager::accountNameSeparator() {\n\treturn QString::fromUtf8(\" \\xE2\\x86\\x92 \");\n}\n\nbool Manager::doSkipToast() const {\n\treturn false;\n}\n\nvoid Manager::doMaybePlaySound(Fn<void()> playSound) {\n\tplaySound();\n}\n\nvoid Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {\n\tflashBounce();\n}\n\n} // namespace Notifications\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/overlay_widget_mac.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_overlay_widget.h\"\n\ntemplate <typename Object>\nclass object_ptr;\n\nnamespace Ui {\nclass AbstractButton;\n} // namespace Ui\n\nnamespace Ui::Platform {\nenum class TitleControl;\n} // namespace Ui::Platform\n\nnamespace Platform {\n\nclass MacOverlayWidgetHelper final : public OverlayWidgetHelper {\npublic:\n\tMacOverlayWidgetHelper(\n\t\tnot_null<Ui::RpWindow*> window,\n\t\tFn<void(bool)> maximize);\n\t~MacOverlayWidgetHelper();\n\n\tvoid beforeShow(bool fullscreen) override;\n\tvoid afterShow(bool fullscreen) override;\n\tvoid notifyFileDialogShown(bool shown) override;\n\tvoid minimize(not_null<Ui::RpWindow*> window) override;\n\tvoid clearState() override;\n\tvoid setControlsOpacity(float64 opacity) override;\n\trpl::producer<bool> controlsSideRightValue() override;\n\trpl::producer<int> topNotchSkipValue() override;\n\nprivate:\n\tusing Control = Ui::Platform::TitleControl;\n\tstruct Data;\n\n\tvoid activate(Control control);\n\tvoid resolveNative();\n\tvoid updateStyles(bool fullscreen);\n\tvoid refreshButtons(bool fullscreen);\n\n\tobject_ptr<Ui::AbstractButton> create(\n\t\tnot_null<QWidget*> parent,\n\t\tControl control);\n\n\tstd::unique_ptr<Data> _data;\n\n};\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/overlay_widget_mac.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/overlay_widget_mac.h\"\n\n#include \"base/object_ptr.h\"\n#include \"ui/platform/ui_platform_window_title.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/rp_window.h\"\n#include \"styles/style_media_view.h\"\n\n#include <QtGui/QWindow>\n#include <Cocoa/Cocoa.h>\n\nnamespace Platform {\nnamespace {\n\nusing namespace Media::View;\n\n} // namespace\n\nstruct MacOverlayWidgetHelper::Data {\n\tconst not_null<Ui::RpWindow*> window;\n\tconst Fn<void(bool)> maximize;\n\tobject_ptr<Ui::AbstractButton> buttonClose = { nullptr };\n\tobject_ptr<Ui::AbstractButton> buttonMinimize = { nullptr };\n\tobject_ptr<Ui::AbstractButton> buttonMaximize = { nullptr };\n\trpl::event_stream<> activations;\n\trpl::variable<float64> masterOpacity = 1.;\n\trpl::variable<bool> maximized = false;\n\trpl::event_stream<> clearStateRequests;\n\tbool anyOver = false;\n\tNSWindow * __weak native = nil;\n\trpl::variable<int> topNotchSkip;\n};\n\nMacOverlayWidgetHelper::MacOverlayWidgetHelper(\n\tnot_null<Ui::RpWindow*> window,\n\tFn<void(bool)> maximize)\n: _data(std::make_unique<Data>(Data{\n\t.window = window,\n\t.maximize = std::move(maximize),\n})) {\n\t_data->buttonClose = create(window, Control::Close);\n\t_data->buttonMinimize = create(window, Control::Minimize);\n\t_data->buttonMaximize = create(window, Control::Maximize);\n}\n\nMacOverlayWidgetHelper::~MacOverlayWidgetHelper() = default;\n\nvoid MacOverlayWidgetHelper::activate(Control control) {\n\tconst auto fullscreen = (_data->window->windowHandle()->flags() & Qt::FramelessWindowHint);\n\tswitch (control) {\n\tcase Control::Close: _data->window->close(); return;\n\tcase Control::Minimize: [_data->native miniaturize:_data->native]; return;\n\tcase Control::Maximize: _data->maximize(!fullscreen); return;\n\t}\n}\n\nvoid MacOverlayWidgetHelper::beforeShow(bool fullscreen) {\n\t_data->window->setAttribute(Qt::WA_MacAlwaysShowToolWindow, !fullscreen);\n\t_data->window->windowHandle()->setFlag(Qt::FramelessWindowHint, fullscreen);\n\tupdateStyles(fullscreen);\n\tclearState();\n}\n\nvoid MacOverlayWidgetHelper::afterShow(bool fullscreen) {\n\tupdateStyles(fullscreen);\n\trefreshButtons(fullscreen);\n\t_data->window->activateWindow();\n}\n\nvoid MacOverlayWidgetHelper::resolveNative() {\n\tif (const auto handle = _data->window->winId()) {\n\t\t_data->native = [reinterpret_cast<NSView*>(handle) window];\n\t}\n}\n\nvoid MacOverlayWidgetHelper::updateStyles(bool fullscreen) {\n\t_data->maximized = fullscreen;\n\n\tresolveNative();\n\tif (!_data->native) {\n\t\treturn;\n\t}\n\n\tconst auto window = _data->native;\n\tconst auto level = !fullscreen\n\t\t? NSNormalWindowLevel\n\t\t: NSPopUpMenuWindowLevel;\n\t[window setLevel:level];\n\n\t// Fullscreen overlay: follow the currently active Space on activation.\n\t// Windowed overlay: behave like a normal tool panel, so the user can move\n\t// it to another desktop and it stays there.\n\tauto behavior = [window collectionBehavior];\n\tif (fullscreen) {\n\t\tbehavior |= NSWindowCollectionBehaviorMoveToActiveSpace;\n\t} else {\n\t\tbehavior &= ~NSWindowCollectionBehaviorMoveToActiveSpace;\n\t}\n\t[window setCollectionBehavior:behavior];\n\n\t[window setHidesOnDeactivate:!_data->window->testAttribute(Qt::WA_MacAlwaysShowToolWindow)];\n\t[window setTitleVisibility:NSWindowTitleHidden];\n\t[window setTitlebarAppearsTransparent:YES];\n\t[window setStyleMask:[window styleMask] | NSWindowStyleMaskFullSizeContentView];\n\tif (@available(macOS 12.0, *)) {\n\t\t_data->topNotchSkip = [[window screen] safeAreaInsets].top;\n\t}\n}\n\nvoid MacOverlayWidgetHelper::refreshButtons(bool fullscreen) {\n\tExpects(_data->native != nullptr);\n\n\tconst auto window = _data->native;\n\tconst auto process = [&](NSWindowButton type) {\n\t\tif (const auto button = [window standardWindowButton:type]) {\n\t\t\t[button setHidden:YES];\n\t\t}\n\t};\n\tprocess(NSWindowCloseButton);\n\tprocess(NSWindowMiniaturizeButton);\n\tprocess(NSWindowZoomButton);\n\t_data->buttonClose->moveToLeft(0, 0);\n\t_data->buttonClose->raise();\n\t_data->buttonClose->show();\n\t_data->buttonMinimize->moveToLeft(_data->buttonClose->width(), 0);\n\t_data->buttonMinimize->raise();\n\t_data->buttonMinimize->show();\n\t_data->buttonMaximize->moveToLeft(_data->buttonClose->width() + _data->buttonMinimize->width(), 0);\n\t_data->buttonMaximize->raise();\n\t_data->buttonMaximize->show();\n}\n\nvoid MacOverlayWidgetHelper::notifyFileDialogShown(bool shown) {\n\tresolveNative();\n\tif (_data->native && !_data->window->isHidden()) {\n\t\tconst auto level = [_data->native level];\n\t\tif (level != NSNormalWindowLevel) {\n\t\t\tconst auto level = shown\n\t\t\t\t? NSModalPanelWindowLevel\n\t\t\t\t: NSPopUpMenuWindowLevel;\n\t\t\t[_data->native setLevel:level];\n\t\t}\n\t}\n}\n\nvoid MacOverlayWidgetHelper::minimize(not_null<Ui::RpWindow*> window) {\n\tresolveNative();\n\tif (_data->native) {\n\t\t[_data->native miniaturize:_data->native];\n\t}\n}\n\nvoid MacOverlayWidgetHelper::clearState() {\n\t_data->clearStateRequests.fire({});\n}\n\nvoid MacOverlayWidgetHelper::setControlsOpacity(float64 opacity) {\n\t_data->masterOpacity = opacity;\n}\n\nrpl::producer<bool> MacOverlayWidgetHelper::controlsSideRightValue() {\n\treturn rpl::single(false);\n}\n\nrpl::producer<int> MacOverlayWidgetHelper::topNotchSkipValue() {\n\treturn _data->topNotchSkip.value();\n}\n\nobject_ptr<Ui::AbstractButton> MacOverlayWidgetHelper::create(\n\t\tnot_null<QWidget*> parent,\n\t\tControl control) {\n\tauto result = object_ptr<Ui::AbstractButton>(parent);\n\tconst auto raw = result.data();\n\n\traw->setClickedCallback([=] { activate(control); });\n\n\tstruct State {\n\t\tUi::Animations::Simple animation;\n\t\tfloat64 progress = -1.;\n\t\tQImage frame;\n\t\tbool maximized = false;\n\t\tbool anyOver = false;\n\t\tbool over = false;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>();\n\n\trpl::merge(\n\t\t_data->masterOpacity.changes() | rpl::to_empty,\n\t\t_data->maximized.changes() | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\traw->update();\n\t}, raw->lifetime());\n\n\t_data->clearStateRequests.events(\n\t) | rpl::on_next([=] {\n\t\traw->clearState();\n\t\traw->update();\n\t\tstate->over = raw->isOver();\n\t\t_data->anyOver = false;\n\t\tstate->animation.stop();\n\t}, raw->lifetime());\n\n\tstruct Info {\n\t\tconst style::icon *icon = nullptr;\n\t\tstyle::margins padding;\n\t};\n\tconst auto info = [&]() -> Info {\n\t\tswitch (control) {\n\t\tcase Control::Minimize:\n\t\t\treturn { &st::mediaviewTitleMinimizeMac, st::mediaviewTitleMinimizeMacPadding };\n\t\tcase Control::Maximize:\n\t\t\treturn { &st::mediaviewTitleMaximizeMac, st::mediaviewTitleMaximizeMacPadding };\n\t\tcase Control::Close:\n\t\t\treturn { &st::mediaviewTitleCloseMac, st::mediaviewTitleCloseMacPadding };\n\t\t}\n\t\tUnexpected(\"Value in DefaultOverlayWidgetHelper::Buttons::create.\");\n\t}();\n\tconst auto icon = info.icon;\n\n\traw->resize(QRect(QPoint(), icon->size()).marginsAdded(info.padding).size());\n\tstate->frame = QImage(\n\t\ticon->size() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tstate->frame.setDevicePixelRatio(style::DevicePixelRatio());\n\n\tconst auto updateOver = [=] {\n\t\tconst auto over = raw->isOver();\n\t\tif (state->over == over) {\n\t\t\treturn;\n\t\t}\n\t\tstate->over = over;\n\t\tconst auto anyOver = over\n\t\t\t|| _data->buttonClose->isOver()\n\t\t\t|| _data->buttonMinimize->isOver()\n\t\t\t|| _data->buttonMaximize->isOver();\n\t\tif (_data->anyOver != anyOver) {\n\t\t\t_data->anyOver = anyOver;\n\t\t\t_data->buttonClose->update();\n\t\t\t_data->buttonMinimize->update();\n\t\t\t_data->buttonMaximize->update();\n\t\t}\n\t\tstate->animation.start(\n\t\t\t[=] { raw->update(); },\n\t\t\tstate->over ? 0. : 1.,\n\t\t\tstate->over ? 1. : 0.,\n\t\t\tst::mediaviewFadeDuration);\n\t};\n\n\tconst auto prepareFrame = [=] {\n\t\tconst auto progress = state->animation.value(state->over ? 1. : 0.);\n\t\tconst auto maximized = _data->maximized.current();\n\t\tconst auto anyOver = _data->anyOver;\n\t\tif (state->progress == progress\n\t\t\t&& state->maximized == maximized\n\t\t\t&& state->anyOver == anyOver) {\n\t\t\treturn;\n\t\t}\n\t\tstate->progress = progress;\n\t\tstate->maximized = maximized;\n\t\tstate->anyOver = anyOver;\n\t\tauto current = icon;\n\t\tif (control == Control::Maximize) {\n\t\t\tcurrent = maximized ? &st::mediaviewTitleRestoreMac : icon;\n\t\t}\n\t\tstate->frame.fill(Qt::transparent);\n\n\t\tauto q = QPainter(&state->frame);\n\t\tconst auto normal = maximized\n\t\t\t? kMaximizedIconOpacity\n\t\t\t: kNormalIconOpacity;\n\t\tq.setOpacity(progress + (1 - progress) * normal);\n\t\tst::mediaviewTitleButtonMac.paint(q, 0, 0, raw->width());\n\t\tif (anyOver) {\n\t\t\tq.setOpacity(1.);\n\t\t\tcurrent->paint(q, 0, 0, raw->width());\n\t\t}\n\t\tq.end();\n\t};\n\n\traw->paintRequest(\n\t) | rpl::on_next([=, padding = info.padding] {\n\t\tupdateOver();\n\t\tprepareFrame();\n\n\t\tauto p = QPainter(raw);\n\t\tp.setOpacity(_data->masterOpacity.current());\n\t\tp.drawImage(padding.left(), padding.top(), state->frame);\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\nstd::unique_ptr<OverlayWidgetHelper> CreateOverlayWidgetHelper(\n\t\tnot_null<Ui::RpWindow*> window,\n\t\tFn<void(bool)> maximize) {\n\treturn std::make_unique<MacOverlayWidgetHelper>(\n\t\twindow,\n\t\tstd::move(maximize));\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/specific_mac.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_specific.h\"\n#include \"platform/mac/specific_mac_p.h\"\n\nnamespace Platform {\n\ninline bool AutostartSupported() {\n\treturn false;\n}\n\ninline void AutostartRequestStateFromSystem(Fn<void(bool)> callback) {\n}\n\ninline bool TrayIconSupported() {\n\treturn true;\n}\n\ninline bool SkipTaskbarSupported() {\n\treturn false;\n}\n\nvoid ActivateThisProcess();\n\ninline uint64 ActivationWindowId(not_null<QWidget*> window) {\n\treturn 1;\n}\n\ninline void ActivateOtherProcess(uint64 processId, uint64 windowId) {\n}\n\ninline QString ApplicationIconName() {\n\treturn {};\n}\n\ninline QString ExecutablePathForShortcuts() {\n\treturn cExeDir() + cExeName();\n}\n\nnamespace ThirdParty {\n\ninline void start() {\n}\n\n} // namespace ThirdParty\n} // namespace Platform\n\ninline void psCheckLocalSocket(const QString &serverName) {\n\tQFile address(serverName);\n\tif (address.exists()) {\n\t\taddress.remove();\n\t}\n}\n\nQString psAppDataPath();\nvoid psSendToMenu(bool send, bool silent = false);\n\nint psCleanup();\nint psFixPrevious();\n\nvoid psDownloadPathEnableAccess();\nQByteArray psDownloadPathBookmark(const QString &path);\nQByteArray psPathBookmark(const QString &path);\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/specific_mac.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/specific_mac.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"mainwidget.h\"\n#include \"history/history_widget.h\"\n#include \"core/crash_reports.h\"\n#include \"core/sandbox.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"storage/localstorage.h\"\n#include \"window/window_controller.h\"\n#include \"mainwindow.h\"\n#include \"history/history_location_manager.h\"\n#include \"base/platform/mac/base_confirm_quit.h\"\n#include \"base/platform/mac/base_utilities_mac.h\"\n#include \"base/platform/base_platform_info.h\"\n\n#include <QtGui/QDesktopServices>\n#include <QtWidgets/QApplication>\n\n#include <cstdlib>\n#include <execinfo.h>\n#include <sys/xattr.h>\n\n#include <Cocoa/Cocoa.h>\n#include <CoreFoundation/CFURL.h>\n#include <IOKit/IOKitLib.h>\n#include <IOKit/hidsystem/ev_keymap.h>\n#include <mach-o/dyld.h>\n#include <AVFoundation/AVFoundation.h>\n\nnamespace {\n\n[[nodiscard]] QImage ImageFromNS(NSImage *icon) {\n\tCGImageRef image = [icon CGImageForProposedRect:NULL context:nil hints:nil];\n\n\tconst int width = CGImageGetWidth(image);\n\tconst int height = CGImageGetHeight(image);\n\tauto result = QImage(width, height, QImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::transparent);\n\n\tCGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);\n\tCGBitmapInfo info = CGBitmapInfo(kCGImageAlphaPremultipliedFirst) | kCGBitmapByteOrder32Host;\n\tCGContextRef context = CGBitmapContextCreate(\n\t\tresult.bits(),\n\t\twidth,\n\t\theight,\n\t\t8,\n\t\tresult.bytesPerLine(),\n\t\tspace,\n\t\tinfo);\n\n\tCGRect rect = CGRectMake(0, 0, width, height);\n\tCGContextDrawImage(context, rect, image);\n\n\tCFRelease(space);\n\tCFRelease(context);\n\n\treturn result;\n}\n\n[[nodiscard]] QImage ResolveBundleIconDefault() {\n\tNSString *path = [[NSBundle mainBundle] bundlePath];\n\tNSString *icon = [path stringByAppendingString:@\"/Contents/Resources/Icon.icns\"];\n\tNSImage *image = [[NSImage alloc] initWithContentsOfFile:icon];\n\tif (!image) {\n\t\treturn Window::Logo();\n\t}\n\n\tauto result = ImageFromNS(image);\n\t[image release];\n\treturn result;\n}\n\n} // namespace\n\nQString psAppDataPath() {\n\treturn objc_appDataPath();\n}\n\nvoid psDoCleanup() {\n\ttry {\n\t\tPlatform::AutostartToggle(false);\n\t\tpsSendToMenu(false, true);\n\t} catch (...) {\n\t}\n}\n\nint psCleanup() {\n\tpsDoCleanup();\n\treturn 0;\n}\n\nvoid psDoFixPrevious() {\n}\n\nint psFixPrevious() {\n\tpsDoFixPrevious();\n\treturn 0;\n}\n\nnamespace Platform {\n\nvoid start() {\n\tobjc_start();\n}\n\nvoid finish() {\n\tobjc_finish();\n}\n\nQString SingleInstanceLocalServerName(const QString &hash) {\n#ifndef OS_MAC_STORE\n\treturn u\"/tmp/\"_q + hash + '-' + cGUIDStr();\n#else // OS_MAC_STORE\n\treturn objc_documentsPath() + hash.left(4);\n#endif // OS_MAC_STORE\n}\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)\nnamespace {\n\nQString strStyleOfInterface() {\n\tconst uint32 letters[] = { 0x3BBB7F05, 0xED4C5EC3, 0xC62C15A3, 0x5D10B283, 0x1BB35729, 0x63FB674D, 0xDBE5C174, 0x401EA195, 0x87B0C82A, 0x311BD596, 0x7063ECFA, 0x4AB90C27, 0xDA587DC4, 0x0B6296F8, 0xAA5603FA, 0xE1140A9F, 0x3D12D094, 0x339B5708, 0x712BA5B1 };\n\treturn Platform::MakeFromLetters(letters);\n}\n\nbool IsDarkMenuBar() {\n\tbool result = false;\n\t@autoreleasepool {\n\n\tNSDictionary *dict = [[NSUserDefaults standardUserDefaults] persistentDomainForName:NSGlobalDomain];\n\tid style = [dict objectForKey:Q2NSString(strStyleOfInterface())];\n\tBOOL darkModeOn = (style && [style isKindOfClass:[NSString class]] && NSOrderedSame == [style caseInsensitiveCompare:@\"dark\"]);\n\tresult = darkModeOn ? true : false;\n\n\t}\n\treturn result;\n}\n\n} // namespace\n\nstd::optional<bool> IsDarkMode() {\n\treturn IsMac10_14OrGreater()\n\t\t? std::make_optional(IsDarkMenuBar())\n\t\t: std::nullopt;\n}\n#endif // Qt < 6.5.0\n\nvoid WriteCrashDumpDetails() {\n#ifndef TDESKTOP_DISABLE_CRASH_REPORTS\n\tdouble v = objc_appkitVersion();\n\tCrashReports::dump() << \"OS-Version: \" << v;\n#endif // TDESKTOP_DISABLE_CRASH_REPORTS\n}\n\n// I do check for availability, just not in the exact way clang is content with\n#pragma clang diagnostic push\n#pragma clang diagnostic ignored \"-Wunguarded-availability\"\nPermissionStatus GetPermissionStatus(PermissionType type) {\n\tswitch (type) {\n\tcase PermissionType::Microphone:\n\tcase PermissionType::Camera:\n\t\tconst auto nativeType = (type == PermissionType::Microphone)\n\t\t\t? AVMediaTypeAudio\n\t\t\t: AVMediaTypeVideo;\n\t\tif ([AVCaptureDevice respondsToSelector: @selector(authorizationStatusForMediaType:)]) { // Available starting with 10.14\n\t\t\tswitch ([AVCaptureDevice authorizationStatusForMediaType:nativeType]) {\n\t\t\t\tcase AVAuthorizationStatusNotDetermined:\n\t\t\t\t\treturn PermissionStatus::CanRequest;\n\t\t\t\tcase AVAuthorizationStatusAuthorized:\n\t\t\t\t\treturn PermissionStatus::Granted;\n\t\t\t\tcase AVAuthorizationStatusDenied:\n\t\t\t\tcase AVAuthorizationStatusRestricted:\n\t\t\t\t\treturn PermissionStatus::Denied;\n\t\t\t}\n\t\t}\n\t\tbreak;\n\t}\n\treturn PermissionStatus::Granted;\n}\n\nvoid RequestPermission(PermissionType type, Fn<void(PermissionStatus)> resultCallback) {\n\tswitch (type) {\n\tcase PermissionType::Microphone:\n\tcase PermissionType::Camera:\n\t\tconst auto nativeType = (type == PermissionType::Microphone)\n\t\t\t? AVMediaTypeAudio\n\t\t\t: AVMediaTypeVideo;\n\t\tif ([AVCaptureDevice respondsToSelector: @selector(requestAccessForMediaType:completionHandler:)]) { // Available starting with 10.14\n\t\t\t[AVCaptureDevice requestAccessForMediaType:nativeType completionHandler:^(BOOL granted) {\n\t\t\t\tcrl::on_main([=] {\n\t\t\t\t\tresultCallback(granted ? PermissionStatus::Granted : PermissionStatus::Denied);\n\t\t\t\t});\n\t\t\t}];\n\t\t}\n\t\tbreak;\n\t}\n\tresultCallback(PermissionStatus::Granted);\n}\n#pragma clang diagnostic pop // -Wunguarded-availability\n\nvoid OpenSystemSettingsForPermission(PermissionType type) {\n\tswitch (type) {\n\tcase PermissionType::Microphone:\n\t\t[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@\"x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone\"]];\n\t\tbreak;\n\tcase PermissionType::Camera:\n\t\t[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@\"x-apple.systempreferences:com.apple.preference.security?Privacy_Camera\"]];\n\t\tbreak;\n\t}\n}\n\nbool OpenSystemSettings(SystemSettingsType type) {\n\tswitch (type) {\n\tcase SystemSettingsType::Audio:\n\t\t[[NSWorkspace sharedWorkspace] openFile:@\"/System/Library/PreferencePanes/Sound.prefPane\"];\n\t\tbreak;\n\t}\n\treturn true;\n}\n\nvoid IgnoreApplicationActivationRightNow() {\n\tobjc_ignoreApplicationActivationRightNow();\n}\n\nvoid AutostartToggle(bool enabled, Fn<void(bool)> done) {\n\tif (done) {\n\t\tdone(false);\n\t}\n}\n\nbool AutostartSkip() {\n\treturn !cAutoStart();\n}\n\nvoid NewVersionLaunched(int oldVersion) {\n}\n\nQImage DefaultApplicationIcon() {\n\tstatic auto result = ResolveBundleIconDefault();\n\treturn result;\n}\n\nbool PreventsQuit(Core::QuitReason reason) {\n\t// Thanks Chromium, see\n\t// chromium.org/developers/design-documents/confirm-to-quit-experiment\n\treturn (reason == Core::QuitReason::QtQuitEvent)\n\t\t&& Core::App().settings().macWarnBeforeQuit()\n\t\t&& ([[NSApp currentEvent] type] == NSEventTypeKeyDown)\n\t\t&& !ConfirmQuit::RunModal(\n\t\t\ttr::lng_mac_hold_to_quit(\n\t\t\t\ttr::now,\n\t\t\t\tlt_text,\n\t\t\t\tConfirmQuit::QuitKeysString()));\n}\n\nvoid ActivateThisProcess() {\n\tconst auto window = Core::App().activeWindow();\n\tobjc_activateProgram(window ? window->widget()->winId() : 0);\n}\n\nvoid LaunchMaps(const Data::LocationPoint &point, Fn<void()> fail) {\n\tif (!QDesktopServices::openUrl(\n\t\tu\"https://maps.apple.com/?q=Point&z=16&ll=%1,%2\"_q.arg(\n\t\t\tpoint.latAsString(),\n\t\t\tpoint.lonAsString()))) {\n\t\tfail();\n\t}\n}\n\n} // namespace Platform\n\nvoid psSendToMenu(bool send, bool silent) {\n}\n\nvoid psDownloadPathEnableAccess() {\n\tobjc_downloadPathEnableAccess(Core::App().settings().downloadPathBookmark());\n}\n\nQByteArray psDownloadPathBookmark(const QString &path) {\n\treturn objc_downloadPathBookmark(path);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/specific_mac_p.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n// e is NSEvent*\nbool objc_handleMediaKeyEvent(void *e);\n\nvoid objc_debugShowAlert(const QString &str);\nvoid objc_outputDebugString(const QString &str);\n\nvoid objc_start();\nvoid objc_ignoreApplicationActivationRightNow();\nvoid objc_finish();\n\nvoid objc_activateProgram(WId winId);\nbool objc_moveFile(const QString &from, const QString &to);\n\ndouble objc_appkitVersion();\n\nQString objc_documentsPath();\nQString objc_appDataPath();\nQByteArray objc_downloadPathBookmark(const QString &path);\nvoid objc_downloadPathEnableAccess(const QByteArray &bookmark);\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/specific_mac_p.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/specific_mac_p.h\"\n\n#include \"mainwindow.h\"\n#include \"mainwidget.h\"\n#include \"calls/calls_instance.h\"\n#include \"core/sandbox.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/crash_reports.h\"\n#include \"menu/menu_dock.h\"\n#include \"storage/localstorage.h\"\n#include \"media/audio/media_audio.h\"\n#include \"window/window_controller.h\"\n#include \"base/platform/mac/base_utilities_mac.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/timer.h\"\n#include \"styles/style_window.h\"\n#include \"platform/platform_specific.h\"\n\n#include <QtGui/QWindow>\n#include <QtWidgets/QApplication>\n#include <QtWidgets/QMenu>\n#if __has_include(<QtCore/QOperatingSystemVersion>)\n#include <QtCore/QOperatingSystemVersion>\n#endif // __has_include(<QtCore/QOperatingSystemVersion>)\n#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)\n#include <qpa/qwindowsysteminterface.h>\n#endif // Qt < 6.6.0\n#include <Cocoa/Cocoa.h>\n#include <CoreFoundation/CFURL.h>\n#include <IOKit/IOKitLib.h>\n#include <IOKit/hidsystem/ev_keymap.h>\n\nusing Platform::Q2NSString;\nusing Platform::NS2QString;\n\nnamespace {\n\nconstexpr auto kIgnoreActivationTimeoutMs = 500;\n\nvoid SetupDockMenu() {\n\tstatic const auto DockMenu = std::make_unique<QMenu>();\n\tQObject::connect(DockMenu.get(), &QMenu::aboutToShow, [] {\n\t\tMenu::RefreshDockMenu(DockMenu.get());\n\t});\n\tDockMenu->setAsDockMenu();\n}\n\n} // namespace\n\n@interface qVisualize : NSObject {\n}\n\n+ (id)str:(const QString &)str;\n- (id)initWithString:(const QString &)str;\n\n+ (id)bytearr:(const QByteArray &)arr;\n- (id)initWithByteArray:(const QByteArray &)arr;\n\n- (id)debugQuickLookObject;\n\n@end // @interface qVisualize\n\n@implementation qVisualize {\n\tNSString *value;\n\n}\n\n+ (id)bytearr:(const QByteArray &)arr {\n\treturn [[qVisualize alloc] initWithByteArray:arr];\n}\n- (id)initWithByteArray:(const QByteArray &)arr {\n\tif (self = [super init]) {\n\t\tvalue = [NSString stringWithUTF8String:arr.constData()];\n\t}\n\treturn self;\n}\n\n+ (id)str:(const QString &)str {\n\treturn [[qVisualize alloc] initWithString:str];\n}\n- (id)initWithString:(const QString &)str {\n\tif (self = [super init]) {\n\t\tvalue = [NSString stringWithUTF8String:str.toUtf8().constData()];\n\t}\n\treturn self;\n}\n\n- (id)debugQuickLookObject {\n\treturn value;\n}\n\n@end // @implementation qVisualize\n\n@interface ApplicationDelegate : NSObject<NSApplicationDelegate> {\n}\n\n- (BOOL) applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag;\n- (void) applicationDidBecomeActive:(NSNotification *)aNotification;\n- (void) applicationDidResignActive:(NSNotification *)aNotification;\n- (void) receiveWakeNote:(NSNotification*)note;\n\n- (void) ignoreApplicationActivationRightNow;\n\n@end // @interface ApplicationDelegate\n\nApplicationDelegate *_sharedDelegate = nil;\n\n@implementation ApplicationDelegate {\n\tbool _ignoreActivation;\n\tbase::Timer _ignoreActivationStop;\n}\n\n- (instancetype) init {\n\t_ignoreActivation = false;\n\t_ignoreActivationStop.setCallback([self] {\n\t\t_ignoreActivation = false;\n\t});\n\treturn [super init];\n}\n\n- (BOOL) applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag {\n\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\tif (Core::IsAppLaunched()) {\n\t\t\tif (const auto window = Core::App().activeWindow()) {\n\t\t\t\tif (window->widget()->isHidden()) {\n\t\t\t\t\twindow->widget()->showFromTray();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\treturn YES;\n}\n\n- (void) applicationDidBecomeActive:(NSNotification *)aNotification {\n\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\tif (Core::IsAppLaunched() && !_ignoreActivation) {\n\t\t\tCore::App().handleAppActivated();\n\t\t\tif (const auto window = Core::App().activeWindow()) {\n\t\t\t\tif (window->widget()->isHidden()) {\n\t\t\t\t\tif (Core::App().calls().hasVisiblePanel()) {\n\t\t\t\t\t\tCore::App().calls().activateCurrentCall();\n\t\t\t\t\t} else {\n\t\t\t\t\t\twindow->widget()->showFromTray();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n}\n\n- (void) applicationDidResignActive:(NSNotification *)aNotification {\n}\n\n- (void) receiveWakeNote:(NSNotification*)aNotification {\n\tif (!Core::IsAppLaunched()) {\n\t\treturn;\n\t}\n\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\tCore::App().checkLocalTime();\n\n\t\tLOG((\"Audio Info: \"\n\t\t\t\"-receiveWakeNote: received, scheduling detach from audio device\"));\n\t\tMedia::Audio::ScheduleDetachFromDeviceSafe();\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)\n\t\tCore::App().settings().setSystemDarkMode(Platform::IsDarkMode());\n#elif QT_VERSION < QT_VERSION_CHECK(6, 6, 0) // Qt < 6.5.0\n\t\tQWindowSystemInterface::handleThemeChange();\n#endif // Qt < 6.6.0\n\t});\n}\n\n- (void) ignoreApplicationActivationRightNow {\n\t_ignoreActivation = true;\n\t_ignoreActivationStop.callOnce(kIgnoreActivationTimeoutMs);\n}\n\n@end // @implementation ApplicationDelegate\n\nnamespace Platform {\n\nvoid SetApplicationIcon(const QIcon &icon) {\n\tNSImage *image = nil;\n\tif (!icon.isNull()) {\n\t\tauto pixmap = icon.pixmap(1024, 1024);\n\t\tpixmap.setDevicePixelRatio(style::DevicePixelRatio());\n\t\timage = Q2NSImage(pixmap.toImage());\n\t}\n\t[[NSApplication sharedApplication] setApplicationIconImage:image];\n}\n\n} // namespace Platform\n\nvoid objc_debugShowAlert(const QString &str) {\n\t@autoreleasepool {\n\n\tNSAlert *alert = [[NSAlert alloc] init];\n\talert.messageText = @\"Debug Message\";\n\talert.informativeText = Q2NSString(str);\n\t[alert runModal];\n\n\t}\n}\n\nvoid objc_outputDebugString(const QString &str) {\n\t@autoreleasepool {\n\n\tNSLog(@\"%@\", Q2NSString(str));\n\n\t}\n}\n\nvoid objc_start() {\n\t// Patch: Fix macOS regression. On 10.14.4, it crashes on GPU switches.\n\t// See https://bugreports.qt.io/browse/QTCREATORBUG-22215\n\tconst auto version = QOperatingSystemVersion::current();\n\tif (version.majorVersion() == 10\n\t\t&& version.minorVersion() == 14\n\t\t&& version.microVersion() == 4) {\n\t\tqputenv(\"QT_MAC_PRO_WEBENGINE_WORKAROUND\", \"1\");\n\t}\n\n\t_sharedDelegate = [[ApplicationDelegate alloc] init];\n\t[[NSApplication sharedApplication] setDelegate:_sharedDelegate];\n\t[[[NSWorkspace sharedWorkspace] notificationCenter]\n\t\taddObserver: _sharedDelegate\n\t\tselector: @selector(receiveWakeNote:)\n\t\tname: NSWorkspaceDidWakeNotification object: NULL];\n\n\tcrl::on_main([=] { SetupDockMenu(); });\n}\n\nvoid objc_ignoreApplicationActivationRightNow() {\n\tif (_sharedDelegate) {\n\t\t[_sharedDelegate ignoreApplicationActivationRightNow];\n\t}\n}\n\nnamespace {\n\tNSURL *_downloadPathUrl = nil;\n}\n\nvoid objc_finish() {\n\t[_sharedDelegate release];\n\t_sharedDelegate = nil;\n\tif (_downloadPathUrl) {\n\t\t[_downloadPathUrl stopAccessingSecurityScopedResource];\n\t\t_downloadPathUrl = nil;\n\t}\n}\n\nvoid objc_activateProgram(WId winId) {\n\t[NSApp activateIgnoringOtherApps:YES];\n\tif (winId) {\n\t\tNSWindow *w = [reinterpret_cast<NSView*>(winId) window];\n\t\t[w makeKeyAndOrderFront:NSApp];\n\t}\n}\n\nbool objc_moveFile(const QString &from, const QString &to) {\n\t@autoreleasepool {\n\n\tNSString *f = Q2NSString(from), *t = Q2NSString(to);\n\tif ([[NSFileManager defaultManager] fileExistsAtPath:t]) {\n\t\tNSData *data = [NSData dataWithContentsOfFile:f];\n\t\tif (data) {\n\t\t\tif ([data writeToFile:t atomically:YES]) {\n\t\t\t\tif ([[NSFileManager defaultManager] removeItemAtPath:f error:nil]) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif ([[NSFileManager defaultManager] moveItemAtPath:f toPath:t error:nil]) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\t}\n\treturn false;\n}\n\ndouble objc_appkitVersion() {\n\treturn NSAppKitVersionNumber;\n}\n\nQString objc_documentsPath() {\n\tNSURL *url = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:nil];\n\tif (url) {\n\t\treturn QString::fromUtf8([[url path] fileSystemRepresentation]) + '/';\n\t}\n\treturn QString();\n}\n\nQString objc_appDataPath() {\n\tNSURL *url = [[NSFileManager defaultManager] URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:nil];\n\tif (url) {\n\t\treturn QString::fromUtf8([[url path] fileSystemRepresentation]) + '/' + AppNameF.utf16() + '/';\n\t}\n\treturn QString();\n}\n\nQByteArray objc_downloadPathBookmark(const QString &path) {\n#ifndef OS_MAC_STORE\n\treturn QByteArray();\n#else // OS_MAC_STORE\n\tNSURL *url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:path.toUtf8().constData()] isDirectory:YES];\n\tif (!url) return QByteArray();\n\n\tNSError *error = nil;\n\tNSData *data = [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error];\n\treturn data ? QByteArray::fromNSData(data) : QByteArray();\n#endif // OS_MAC_STORE\n}\n\nvoid objc_downloadPathEnableAccess(const QByteArray &bookmark) {\n#ifdef OS_MAC_STORE\n\tif (bookmark.isEmpty()) return;\n\n\tBOOL isStale = NO;\n\tNSError *error = nil;\n\tNSURL *url = [NSURL URLByResolvingBookmarkData:bookmark.toNSData() options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error];\n\tif (!url) return;\n\n\tif ([url startAccessingSecurityScopedResource]) {\n\t\tif (_downloadPathUrl) {\n\t\t\t[_downloadPathUrl stopAccessingSecurityScopedResource];\n\t\t}\n\t\t_downloadPathUrl = [url retain];\n\n\t\tCore::App().settings().setDownloadPath(NS2QString([_downloadPathUrl path]) + '/');\n\t\tif (isStale) {\n\t\t\tNSData *data = [_downloadPathUrl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error];\n\t\t\tif (data) {\n\t\t\t\tCore::App().settings().setDownloadPathBookmark(QByteArray::fromNSData(data));\n\t\t\t\tLocal::writeSettings();\n\t\t\t}\n\t\t}\n\t}\n#endif // OS_MAC_STORE\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/text_recognition_mac.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/text_recognition_mac.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/platform_text_recognition.h\"\n\n#include \"base/platform/mac/base_utilities_mac.h\"\n\n#import <Foundation/Foundation.h>\n#import <Vision/Vision.h>\n#import <CoreImage/CoreImage.h>\n\nnamespace Platform {\nnamespace TextRecognition {\n\nbool IsAvailable() {\n\tif (@available(macOS 10.15, *)) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nResult RecognizeText(const QImage &image) {\n\tauto result = Result();\n\n\tif (!IsAvailable()) {\n\t\treturn result;\n\t}\n\n\t@autoreleasepool {\n\t\tCGImageRef cgImage = image.toCGImage();\n\t\tif (!cgImage) {\n\t\t\treturn result;\n\t\t}\n\t\tCIImage *image = [CIImage imageWithCGImage:cgImage];\n\t\tCFRelease(cgImage);\n\n\t\tif (!image) {\n\t\t\treturn result;\n\t\t}\n\n\t\tif (@available(macOS 10.15, *)) {\n\t\t\tVNRecognizeTextRequest *request\n\t\t\t\t= [[VNRecognizeTextRequest alloc] init];\n\t\t\trequest.recognitionLevel = VNRequestTextRecognitionLevelAccurate;\n\n\t\t\tVNImageRequestHandler *handler = [[VNImageRequestHandler alloc]\n\t\t\t\tinitWithCIImage:image options:@{}];\n\n\t\t\tNSError *error = nil;\n\t\t\tconst auto success\n\t\t\t\t= [handler performRequests:@[request] error:&error];\n\n\t\t\tif (success && !error) {\n\t\t\t\tconst auto imageSize = image.extent.size;\n\t\t\t\tfor (VNRecognizedTextObservation *obs in request.results) {\n\t\t\t\t\tVNRecognizedText *recognizedText = [obs\n\t\t\t\t\t\ttopCandidates:1].firstObject;\n\t\t\t\t\tif (recognizedText) {\n\t\t\t\t\t\tconst auto text = recognizedText.string;\n\t\t\t\t\t\tconst auto boundingBox = obs.boundingBox;\n\t\t\t\t\t\tconst auto x = boundingBox.origin.x * imageSize.width;\n\t\t\t\t\t\tconst auto y = (1.0 - boundingBox.origin.y\n\t\t\t\t\t\t\t- boundingBox.size.height) * imageSize.height;\n\t\t\t\t\t\tconst auto width = boundingBox.size.width\n\t\t\t\t\t\t\t* imageSize.width;\n\t\t\t\t\t\tconst auto height = boundingBox.size.height\n\t\t\t\t\t\t\t* imageSize.height;\n\t\t\t\t\t\tresult.items.push_back({\n\t\t\t\t\t\t\tNS2QString(text),\n\t\t\t\t\t\t\tQRect(\n\t\t\t\t\t\t\t\tstyle::ConvertScale(x),\n\t\t\t\t\t\t\t\tstyle::ConvertScale(y),\n\t\t\t\t\t\t\t\tstyle::ConvertScale(width),\n\t\t\t\t\t\t\t\tstyle::ConvertScale(height))\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tresult.success = true;\n\t\t\t}\n\n\t\t\t[request release];\n\t\t\t[handler release];\n\t\t}\n\t}\n\n\treturn result;\n}\n\n} // namespace TextRecognition\n} // namespace Platform"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/items/mac_formatter_item.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#import <AppKit/NSPopoverTouchBarItem.h>\n#import <AppKit/NSTouchBar.h>\n\nAPI_AVAILABLE(macos(10.12.2))\n@interface TextFormatPopover : NSPopoverTouchBarItem\n- (id)init:(NSTouchBarItemIdentifier)identifier;\n@end\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/items/mac_formatter_item.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/touchbar/items/mac_formatter_item.h\"\n\n#include \"base/platform/mac/base_utilities_mac.h\"\n#include \"lang/lang_keys.h\"\n#include \"platform/mac/touchbar/mac_touchbar_common.h\"\n\n#import <AppKit/NSCustomTouchBarItem.h>\n#import <AppKit/NSScrollView.h>\n#import <AppKit/NSSegmentedControl.h>\n\n#include <QtWidgets/QApplication>\n#include <QtWidgets/QTextEdit>\n\nnamespace {\n\nconstexpr auto kCommandBold = 0x010;\nconstexpr auto kCommandItalic = 0x011;\nconstexpr auto kCommandUnderline = 0x012;\nconstexpr auto kCommandStrikeOut = 0x013;\nconstexpr auto kCommandBlockquote = 0x014;\nconstexpr auto kCommandMonospace = 0x015;\nconstexpr auto kCommandClear = 0x016;\nconstexpr auto kCommandLink = 0x017;\n\nconst auto kPopoverFormatter = @\"popoverInputFormatter\";\n\nvoid SendKeyEvent(int command) {\n\tauto *focused = qobject_cast<QTextEdit*>(QApplication::focusWidget());\n\tif (!focused) {\n\t\treturn;\n\t}\n\tauto key = 0;\n\tauto modifier = Qt::KeyboardModifiers(0) | Qt::ControlModifier;\n\tswitch (command) {\n\tcase kCommandBold:\n\t\tkey = Qt::Key_B;\n\t\tbreak;\n\tcase kCommandItalic:\n\t\tkey = Qt::Key_I;\n\t\tbreak;\n\tcase kCommandBlockquote:\n\t\tkey = Qt::Key_Period;\n\t\tmodifier |= Qt::ShiftModifier;\n\t\tbreak;\n\tcase kCommandMonospace:\n\t\tkey = Qt::Key_M;\n\t\tmodifier |= Qt::ShiftModifier;\n\t\tbreak;\n\tcase kCommandClear:\n\t\tkey = Qt::Key_N;\n\t\tmodifier |= Qt::ShiftModifier;\n\t\tbreak;\n\tcase kCommandLink:\n\t\tkey = Qt::Key_K;\n\t\tbreak;\n\tcase kCommandUnderline:\n\t\tkey = Qt::Key_U;\n\t\tbreak;\n\tcase kCommandStrikeOut:\n\t\tkey = Qt::Key_X;\n\t\tmodifier |= Qt::ShiftModifier;\n\t\tbreak;\n\t}\n\tQApplication::postEvent(\n\t\tfocused,\n\t\tnew QKeyEvent(QEvent::KeyPress, key, modifier));\n\tQApplication::postEvent(\n\t\tfocused,\n\t\tnew QKeyEvent(QEvent::KeyRelease, key, modifier));\n}\n\n} // namespace\n\n#pragma mark - TextFormatPopover\n\n@implementation TextFormatPopover {\n\trpl::lifetime _lifetime;\n}\n\n- (id)init:(NSTouchBarItemIdentifier)identifier {\n\tself = [super initWithIdentifier:identifier];\n\tif (!self) {\n\t\treturn nil;\n\t}\n\n\tself.collapsedRepresentationImage = [NSImage\n\t\timageNamed:NSImageNameTouchBarTextItalicTemplate]; // autorelease];\n\tauto *secondaryTouchBar = [[[NSTouchBar alloc] init] autorelease];\n\n\tauto *popover = [[[NSCustomTouchBarItem alloc]\n\t\tinitWithIdentifier:kPopoverFormatter] autorelease];\n\t{\n\t\tauto *scroll = [[[NSScrollView alloc] init] autorelease];\n\t\tauto *segment = [[[NSSegmentedControl alloc] init] autorelease];\n\t\tsegment.segmentStyle = NSSegmentStyleRounded;\n\t\tsegment.target = self;\n\t\tsegment.action = @selector(segmentClicked:);\n\n\t\tstatic const auto strings = {\n\t\t\ttr::lng_menu_formatting_bold,\n\t\t\ttr::lng_menu_formatting_italic,\n\t\t\ttr::lng_menu_formatting_underline,\n\t\t\ttr::lng_menu_formatting_strike_out,\n\t\t\ttr::lng_menu_formatting_blockquote,\n\t\t\ttr::lng_menu_formatting_monospace,\n\t\t\ttr::lng_menu_formatting_clear,\n\t\t\ttr::lng_info_link_label,\n\t\t};\n\t\tsegment.segmentCount = strings.size();\n\t\tauto width = 0;\n\t\tauto count = 0;\n\t\tfor (const auto &s : strings) {\n\t\t\tconst auto string = Platform::Q2NSString(s(tr::now));\n\t\t\twidth += TouchBar::WidthFromString(string) * 1.4;\n\t\t\t[segment setLabel:string forSegment:count++];\n\t\t}\n\t\tsegment.frame = NSMakeRect(0, 0, width, TouchBar::kCircleDiameter);\n\t\t[scroll setDocumentView:segment];\n\t\tpopover.view = scroll;\n\t}\n\n\tsecondaryTouchBar.templateItems = [NSSet setWithArray:@[popover]];\n\tsecondaryTouchBar.defaultItemIdentifiers = @[kPopoverFormatter];\n\n\tself.popoverTouchBar = secondaryTouchBar;\n\treturn self;\n}\n\n- (void)segmentClicked:(NSSegmentedControl*)sender {\n\tconst auto command = int(sender.selectedSegment) + kCommandBold;\n\tsender.selectedSegment = -1;\n\tSendKeyEvent(command);\n\n\t[self dismissPopover:nil];\n}\n\n@end // @implementation TextFormatPopover\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <AppKit/NSImageView.h>\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nAPI_AVAILABLE(macos(10.12.2))\n@interface PinnedDialogsPanel : NSImageView\n- (id)init:(not_null<Main::Session*>)session\n\tdestroyEvent:(rpl::producer<>)touchBarSwitches;\n@end\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/touchbar/items/mac_pinned_chats_item.h\"\n\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"base/timer.h\"\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"core/sandbox.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_cloud_file.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"platform/mac/touchbar/mac_touchbar_common.h\"\n#include \"styles/style_dialogs.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/userpic_view.h\"\n#include \"ui/unread_badge_paint.h\"\n#include \"ui/painter.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n\n#import <AppKit/NSColor.h>\n#import <AppKit/NSGraphicsContext.h>\n#import <AppKit/NSPressGestureRecognizer.h>\n\nusing TouchBar::kCircleDiameter;\n\nnamespace {\n\nconstexpr auto kPinnedButtonsSpace = 30;\nconstexpr auto kPinnedButtonsLeftSkip = kPinnedButtonsSpace / 2;\n\nconstexpr auto kOnlineCircleSize = 8;\nconstexpr auto kOnlineCircleStrokeWidth = 1.5;\nconstexpr auto kUnreadBadgeSize = 15;\n\ninline bool IsSelfPeer(PeerData *peer) {\n\treturn peer && peer->isSelf();\n}\n\ninline bool IsRepliesPeer(PeerData *peer) {\n\treturn peer && peer->isRepliesChat();\n}\n\nQImage PrepareImage() {\n\tconst auto s = kCircleDiameter * style::DevicePixelRatio();\n\tauto result = QImage(QSize(s, s), QImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::transparent);\n\treturn result;\n}\n\nQImage SavedMessagesUserpic() {\n\tauto result = PrepareImage();\n\tPainter paint(&result);\n\n\tconst auto s = result.width();\n\tUi::EmptyUserpic::PaintSavedMessages(paint, 0, 0, s, s);\n\treturn result;\n}\n\nQImage RepliesMessagesUserpic() {\n\tauto result = PrepareImage();\n\tPainter paint(&result);\n\n\tconst auto s = result.width();\n\tUi::EmptyUserpic::PaintRepliesMessages(paint, 0, 0, s, s);\n\treturn result;\n}\n\nQImage ArchiveUserpic(not_null<Data::Folder*> folder) {\n\tauto result = PrepareImage();\n\tPainter paint(&result);\n\n\tfolder->paintUserpic(paint, 0, 0, result.width());\n\treturn result;\n}\n\nQImage UnreadBadge(not_null<PeerData*> peer) {\n\tconst auto history = peer->owner().history(peer->id);\n\tconst auto state = history->chatListBadgesState();\n\tif (!state.unread) {\n\t\treturn QImage();\n\t}\n\tconst auto counter = (state.unreadCounter > 0)\n\t\t? QString::number(state.unreadCounter)\n\t\t: QString();\n\tUi::UnreadBadgeStyle unreadSt;\n\tunreadSt.sizeId = Ui::UnreadBadgeSize::TouchBar;\n\tunreadSt.muted = state.unreadMuted;\n\t// Use constant values to draw badge regardless of cConfigScale().\n\tunreadSt.size = kUnreadBadgeSize * float64(style::DevicePixelRatio());\n\tunreadSt.padding = 4 * float64(style::DevicePixelRatio());\n\tunreadSt.font = style::font(\n\t\t9.5 * float64(style::DevicePixelRatio()),\n\t\tunreadSt.font->flags(),\n\t\tunreadSt.font->family());\n\n\tauto result = QImage(\n\t\tQSize(kCircleDiameter, kUnreadBadgeSize) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::transparent);\n\tPainter p(&result);\n\n\tUi::PaintUnreadBadge(\n\t\tp,\n\t\tcounter,\n\t\tresult.width(),\n\t\tresult.height() - unreadSt.size,\n\t\tunreadSt,\n\t\t2);\n\treturn result;\n}\n\nNSRect PeerRectByIndex(int index) {\n\treturn NSMakeRect(\n\t\tindex * (kCircleDiameter + kPinnedButtonsSpace)\n\t\t\t+ kPinnedButtonsLeftSkip,\n\t\t0,\n\t\tkCircleDiameter,\n\t\tkCircleDiameter);\n}\n\n[[nodiscard]] Data::LastseenStatus CalculateLastseenStatus(\n\t\tnot_null<PeerData*> peer) {\n\tif (const auto user = peer->asUser()) {\n\t\treturn user->lastseen();\n\t}\n\treturn Data::LastseenStatus();\n}\n\n} // namespace\n\n#pragma mark - PinnedDialogsPanel\n\n@interface PinnedDialogsPanel()\n@end // @interface PinnedDialogsPanel\n\n@implementation PinnedDialogsPanel {\n\tstruct Pin {\n\t\tPeerData *peer = nullptr;\n\t\tUi::PeerUserpicView userpicView;\n\t\tint index = -1;\n\t\tQImage userpic;\n\t\tQImage unreadBadge;\n\n\t\tUi::Animations::Simple shiftAnimation;\n\t\tint shift = 0;\n\t\tint finalShift = 0;\n\t\tint deltaShift = 0;\n\t\tint x = 0;\n\t\tint horizontalShift = 0;\n\t\tbool onTop = false;\n\n\t\tUi::Animations::Simple onlineAnimation;\n\t\tData::LastseenStatus lastseen;\n\t};\n\trpl::lifetime _lifetime;\n\tMain::Session *_session;\n\n\tstd::vector<std::unique_ptr<Pin>> _pins;\n\tQImage _savedMessages;\n\tQImage _repliesMessages;\n\tQImage _archive;\n\n\tbool _hasArchive;\n\tbool _selfUnpinned;\n\tbool _repliesUnpinned;\n\n\trpl::event_stream<not_null<NSEvent*>> _touches;\n\trpl::event_stream<not_null<NSPressGestureRecognizer*>> _gestures;\n\n\tCGFloat _r, _g, _b, _a; // The online circle color.\n}\n\n- (void)processHorizontalReorder {\n\t// This method is a simplified version of the VerticalLayoutReorder class\n\t// and is adapatized for horizontal use.\n\tenum class State : uchar {\n\t\tStarted,\n\t\tApplied,\n\t\tCancelled,\n\t};\n\n\tconst auto currentStart = _lifetime.make_state<int>(0);\n\tconst auto currentPeer = _lifetime.make_state<PeerData*>(nullptr);\n\tconst auto currentState = _lifetime.make_state<State>(State::Cancelled);\n\tconst auto currentDesiredIndex = _lifetime.make_state<int>(-1);\n\tconst auto waitForFinish = _lifetime.make_state<bool>(false);\n\tconst auto isDragging = _lifetime.make_state<bool>(false);\n\n\tconst auto indexOf = [=](PeerData *p) {\n\t\tconst auto i = ranges::find(_pins, p, &Pin::peer);\n\t\tAssert(i != end(_pins));\n\t\treturn i - begin(_pins);\n\t};\n\n\tconst auto setHorizontalShift = [=](const auto &pin, int shift) {\n\t\tif (const auto delta = shift - pin->horizontalShift) {\n\t\t\tpin->horizontalShift = shift;\n\t\t\tpin->x += delta;\n\n\t\t\t// Redraw a rectangle\n\t\t\t// from the beginning point of the pin movement to the end point.\n\t\t\tauto rect = PeerRectByIndex(indexOf(pin->peer) + [self shift]);\n\t\t\tconst auto absDelta = std::abs(delta);\n\t\t\trect.origin.x = pin->x - absDelta;\n\t\t\trect.size.width += absDelta * 2;\n\t\t\t[self setNeedsDisplayInRect:rect];\n\t\t}\n\t};\n\n\tconst auto updateShift = [=](not_null<PeerData*> peer, int indexHint) {\n\t\tExpects(indexHint >= 0 && indexHint < _pins.size());\n\n\t\tconst auto index = (_pins[indexHint]->peer->id == peer->id)\n\t\t\t? indexHint\n\t\t\t: indexOf(peer);\n\t\tconst auto &entry = _pins[index];\n\t\tentry->shift = entry->deltaShift\n\t\t\t+ base::SafeRound(\n\t\t\t\tentry->shiftAnimation.value(entry->finalShift));\n\t\tif (entry->deltaShift && !entry->shiftAnimation.animating()) {\n\t\t\tentry->finalShift += entry->deltaShift;\n\t\t\tentry->deltaShift = 0;\n\t\t}\n\t\tsetHorizontalShift(entry, entry->shift);\n\t};\n\n\tconst auto moveToShift = [=](int index, int shift) {\n\t\tCore::Sandbox::Instance().customEnterFromEventLoop([=] {\n\t\t\tauto &entry = _pins[index];\n\t\t\tif (entry->finalShift + entry->deltaShift == shift) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto peer = entry->peer;\n\t\t\tentry->shiftAnimation.start(\n\t\t\t\t[=] { updateShift(peer, index); },\n\t\t\t\tentry->finalShift,\n\t\t\t\tshift - entry->deltaShift,\n\t\t\t\tst::slideWrapDuration);\n\t\t\tentry->finalShift = shift - entry->deltaShift;\n\t\t});\n\t};\n\n\tconst auto cancelCurrentPeer = [=] {\n\t\tExpects(*currentPeer != nullptr);\n\n\t\tif (*currentState == State::Started) {\n\t\t\t*currentState = State::Cancelled;\n\t\t}\n\t\t*currentPeer = nullptr;\n\t\tfor (auto i = 0, count = int(_pins.size()); i != count; ++i) {\n\t\t\tmoveToShift(i, 0);\n\t\t}\n\t};\n\n\tconst auto cancelCurrent = [=] {\n\t\tif (*currentPeer) {\n\t\t\tcancelCurrentPeer();\n\t\t}\n\t};\n\n\tconst auto updateOrder = [=](int index, int positionX) {\n\t\tconst auto shift = positionX - *currentStart;\n\t\tconst auto &current = _pins[index];\n\t\tcurrent->shiftAnimation.stop();\n\t\tcurrent->shift = current->finalShift = shift;\n\t\tsetHorizontalShift(current, shift);\n\n\t\tconst auto count = _pins.size();\n\t\tconst auto currentWidth = current->userpic.width();\n\t\tconst auto currentMiddle = current->x + currentWidth / 2;\n\t\t*currentDesiredIndex = index;\n\t\tif (shift > 0) {\n\t\t\tauto top = current->x - shift;\n\t\t\tfor (auto next = index + 1; next != count; ++next) {\n\t\t\t\tconst auto &entry = _pins[next];\n\t\t\t\ttop += entry->userpic.width();\n\t\t\t\tif (currentMiddle < top) {\n\t\t\t\t\tmoveToShift(next, 0);\n\t\t\t\t} else {\n\t\t\t\t\t*currentDesiredIndex = next;\n\t\t\t\t\tmoveToShift(next, -currentWidth);\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (auto prev = index - 1; prev >= 0; --prev) {\n\t\t\t\tmoveToShift(prev, 0);\n\t\t\t}\n\t\t} else {\n\t\t\tfor (auto next = index + 1; next != count; ++next) {\n\t\t\t\tmoveToShift(next, 0);\n\t\t\t}\n\t\t\tfor (auto prev = index - 1; prev >= 0; --prev) {\n\t\t\t\tconst auto &entry = _pins[prev];\n\t\t\t\tif (currentMiddle >= entry->x - entry->shift + currentWidth) {\n\t\t\t\t\tmoveToShift(prev, 0);\n\t\t\t\t} else {\n\t\t\t\t\t*currentDesiredIndex = prev;\n\t\t\t\t\tmoveToShift(prev, currentWidth);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\tconst auto checkForStart = [=](int positionX) {\n\t\tconst auto shift = positionX - *currentStart;\n\t\tconst auto delta = QApplication::startDragDistance();\n\t\t*isDragging = (std::abs(shift) > delta);\n\t\tif (!*isDragging) {\n\t\t\treturn;\n\t\t}\n\n\t\t*currentState = State::Started;\n\t\t*currentStart += (shift > 0) ? delta : -delta;\n\n\t\tconst auto index = indexOf(*currentPeer);\n\t\t*currentDesiredIndex = index;\n\n\t\t// Raise the pin.\n\t\tranges::for_each(_pins, [=](const auto &pin) {\n\t\t\tpin->onTop = false;\n\t\t});\n\t\t_pins[index]->onTop = true;\n\n\t\tupdateOrder(index, positionX);\n\t};\n\n\tconst auto localGuard = _lifetime.make_state<base::has_weak_ptr>();\n\n\tconst auto finishCurrent = [=] {\n\t\tif (!*currentPeer) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto index = indexOf(*currentPeer);\n\t\tif (*currentDesiredIndex == index\n\t\t\t|| *currentState != State::Started) {\n\t\t\tcancelCurrentPeer();\n\t\t\treturn;\n\t\t}\n\t\tconst auto result = *currentDesiredIndex;\n\t\t*currentState = State::Cancelled;\n\t\t*currentPeer = nullptr;\n\n\t\tconst auto &current = _pins[index];\n\t\t// Since the width of all elements is the same\n\t\t// we can use a single value.\n\t\tcurrent->finalShift += (index - result) * current->userpic.width();\n\n\t\tif (!(current->finalShift + current->deltaShift)) {\n\t\t\tcurrent->shift = 0;\n\t\t\tsetHorizontalShift(current, 0);\n\t\t}\n\t\tcurrent->horizontalShift = current->finalShift;\n\t\tbase::reorder(_pins, index, result);\n\n\t\t*waitForFinish = true;\n\t\t// Call on end of an animation.\n\t\tbase::call_delayed(st::slideWrapDuration, &(*localGuard), [=] {\n\t\t\tconst auto guard = gsl::finally([=] {\n\t\t\t\t_session->data().notifyPinnedDialogsOrderUpdated();\n\t\t\t\t*waitForFinish = false;\n\t\t\t});\n\t\t\tif (index == result) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto &order = _session->data().pinnedChatsOrder(nullptr);\n\t\t\tconst auto d = (index < result) ? 1 : -1; // Direction.\n\t\t\tfor (auto i = index; i != result; i += d) {\n\t\t\t\t_session->data().chatsList()->pinned()->reorder(\n\t\t\t\t\torder.at(i).history(),\n\t\t\t\t\torder.at(i + d).history());\n\t\t\t}\n\t\t\t_session->api().savePinnedOrder(nullptr);\n\t\t});\n\n\t\tmoveToShift(result, 0);\n\t};\n\n\tconst auto touchBegan = [=](int touchX) {\n\t\t*isDragging = false;\n\t\tcancelCurrent();\n\t\t*currentStart = touchX;\n\t\tif (_pins.size() < 2) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto index = [self indexFromX:*currentStart];\n\t\tif (index < 0) {\n\t\t\treturn;\n\t\t}\n\t\t*currentPeer = _pins[index]->peer;\n\t};\n\n\tconst auto touchMoved = [=](int touchX) {\n\t\tif (!*currentPeer) {\n\t\t\treturn;\n\t\t} else if (*currentState != State::Started) {\n\t\t\tcheckForStart(touchX);\n\t\t} else {\n\t\t\tupdateOrder(indexOf(*currentPeer), touchX);\n\t\t}\n\t};\n\n\tconst auto touchEnded = [=](int touchX) {\n\t\tif (*isDragging) {\n\t\t\tfinishCurrent();\n\t\t\treturn;\n\t\t}\n\t\tconst auto step = QApplication::startDragDistance();\n\t\tif (std::abs(*currentStart - touchX) < step) {\n\t\t\t[self performAction:touchX];\n\t\t}\n\t};\n\n\t_gestures.events(\n\t) | rpl::filter([=] {\n\t\treturn !(*waitForFinish);\n\t}) | rpl::on_next([=](\n\t\t\tnot_null<NSPressGestureRecognizer*> gesture) {\n\t\tconst auto currentPosition = [gesture locationInView:self].x;\n\n\t\tswitch ([gesture state]) {\n\t\tcase NSGestureRecognizerStateBegan:\n\t\t\treturn touchBegan(currentPosition);\n\t\tcase NSGestureRecognizerStateChanged:\n\t\t\treturn touchMoved(currentPosition);\n\t\tcase NSGestureRecognizerStateCancelled:\n\t\tcase NSGestureRecognizerStateEnded:\n\t\t\treturn touchEnded(currentPosition);\n\t\t}\n\t}, _lifetime);\n\n\t_session->data().pinnedDialogsOrderUpdated(\n\t) | rpl::on_next(cancelCurrent, _lifetime);\n\n\t_lifetime.add([=] {\n\t\tfor (const auto &pin : _pins) {\n\t\t\tpin->shiftAnimation.stop();\n\t\t\tpin->onlineAnimation.stop();\n\t\t}\n\t});\n\n}\n\n- (id)init:(not_null<Main::Session*>)session\n\t\tdestroyEvent:(rpl::producer<>)touchBarSwitches {\n\tself = [super init];\n\t_session = session;\n\t_hasArchive = _selfUnpinned = false;\n\t_savedMessages = SavedMessagesUserpic();\n\t_repliesMessages = RepliesMessagesUserpic();\n\n\tauto *gesture = [[[NSPressGestureRecognizer alloc]\n\t\tinitWithTarget:self\n\t\taction:@selector(gestureHandler:)] autorelease];\n\tgesture.allowedTouchTypes = NSTouchTypeMaskDirect;\n\tgesture.minimumPressDuration = 0;\n\tgesture.allowableMovement = 0;\n\t[self addGestureRecognizer:gesture];\n\n\t// For some reason, sometimes a parent deallocates not immediately,\n\t// but only after the user's input (mouse movement, key pressing, etc.).\n\t// So we have to use a custom event to destroy the current lifetime\n\t// manually, before it leads to crashes.\n\tstd::move(\n\t\ttouchBarSwitches\n\t) | rpl::on_next([=] {\n\t\t_lifetime.destroy();\n\t}, _lifetime);\n\n\tusing UpdateFlag = Data::PeerUpdate::Flag;\n\n\tconst auto downloadLifetime = _lifetime.make_state<rpl::lifetime>();\n\tconst auto peerChangedLifetime = _lifetime.make_state<rpl::lifetime>();\n\tconst auto lastDialogsCount = _lifetime.make_state<rpl::variable<int>>(0);\n\tauto &&peers = ranges::views::all(\n\t\t_pins\n\t) | ranges::views::transform(&Pin::peer);\n\n\tconst auto updatePanelSize = [=] {\n\t\tconst auto size = lastDialogsCount->current();\n\t\tif (self.image) {\n\t\t\t[self.image release];\n\t\t}\n\t\t// TODO: replace it with NSLayoutConstraint.\n\t\tself.image = [[NSImage alloc] initWithSize:NSMakeSize(\n\t\t\tsize * (kCircleDiameter + kPinnedButtonsSpace)\n\t\t\t\t+ kPinnedButtonsLeftSkip\n\t\t\t\t- kPinnedButtonsSpace / 2,\n\t\t\tkCircleDiameter)];\n\t};\n\tlastDialogsCount->changes(\n\t) | rpl::on_next(updatePanelSize, _lifetime);\n\tconst auto singleUserpic = [=](const auto &pin) {\n\t\tif (IsSelfPeer(pin->peer)) {\n\t\t\tpin->userpic = _savedMessages;\n\t\t\treturn;\n\t\t} else if (IsRepliesPeer(pin->peer)) {\n\t\t\tpin->userpic = _repliesMessages;\n\t\t\treturn;\n\t\t}\n\t\tauto userpic = PrepareImage();\n\t\tPainter p(&userpic);\n\n\t\tpin->peer->paintUserpic(p, pin->userpicView, 0, 0, userpic.width());\n\t\tuserpic.setDevicePixelRatio(style::DevicePixelRatio());\n\t\tpin->userpic = std::move(userpic);\n\t\tconst auto userpicIndex = pin->index + [self shift];\n\t\t[self setNeedsDisplayInRect:PeerRectByIndex(userpicIndex)];\n\t};\n\tconst auto updateUserpics = [=] {\n\t\tranges::for_each(_pins, singleUserpic);\n\t\t*lastDialogsCount = [self shift] + int(std::size(_pins));\n\t};\n\tconst auto updateBadge = [=](const auto &pin) {\n\t\tconst auto peer = pin->peer;\n\t\tif (IsSelfPeer(peer)) {\n\t\t\treturn;\n\t\t}\n\t\tpin->unreadBadge = UnreadBadge(peer);\n\n\t\tconst auto userpicIndex = pin->index + [self shift];\n\t\t[self setNeedsDisplayInRect:PeerRectByIndex(userpicIndex)];\n\t};\n\tconst auto listenToDownloaderFinished = [=] {\n\t\t_session->downloaderTaskFinished(\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto all = ranges::all_of(_pins, [=](const auto &pin) {\n\t\t\t\treturn (!pin->peer->hasUserpic())\n\t\t\t\t\t|| (!Ui::PeerUserpicLoading(pin->userpicView));\n\t\t\t});\n\t\t\tif (all) {\n\t\t\t\tdownloadLifetime->destroy();\n\t\t\t}\n\t\t\tupdateUserpics();\n\t\t}, *downloadLifetime);\n\t};\n\tconst auto processOnline = [=](const auto &pin) {\n\t\t// TODO: this should be replaced\n\t\t// with the global application timer for online statuses.\n\t\tconst auto onlineChanges\n\t\t\t= peerChangedLifetime->make_state<rpl::event_stream<PeerData*>>();\n\t\tconst auto peer = pin->peer;\n\t\tconst auto onlineTimer = peerChangedLifetime->make_state<base::Timer>(\n\t\t\t[=] { onlineChanges->fire_copy({ peer }); });\n\n\t\tconst auto callTimer = [=](const auto &pin) {\n\t\t\tonlineTimer->cancel();\n\t\t\tif (const auto till = pin->lastseen.onlineTill()) {\n\t\t\t\tconst auto left = till - base::unixtime::now();\n\t\t\t\tif (left > 0) {\n\t\t\t\t\tonlineTimer->callOnce(std::min(86400, left)\n\t\t\t\t\t\t* crl::time(1000));\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tcallTimer(pin);\n\n\t\tusing PeerUpdate = Data::PeerUpdate;\n\t\tauto to_peer = rpl::map([=](const PeerUpdate &update) -> PeerData* {\n\t\t\treturn update.peer;\n\t\t});\n\t\trpl::merge(\n\t\t\t_session->changes().peerUpdates(\n\t\t\t\tpin->peer,\n\t\t\t\tUpdateFlag::OnlineStatus) | to_peer,\n\t\t\tonlineChanges->events()\n\t\t) | rpl::on_next([=](PeerData *peer) {\n\t\t\tconst auto it = ranges::find(_pins, peer, &Pin::peer);\n\t\t\tif (it == end(_pins)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto &pin = *it;\n\t\t\tpin->lastseen = CalculateLastseenStatus(pin->peer);\n\n\t\t\tcallTimer(pin);\n\n\t\t\tif (![NSApplication sharedApplication].active) {\n\t\t\t\tpin->onlineAnimation.stop();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto now = base::unixtime::now();\n\t\t\tconst auto online = pin->lastseen.isOnline(now);\n\t\t\tif (pin->onlineAnimation.animating()) {\n\t\t\t\tpin->onlineAnimation.change(\n\t\t\t\t\tonline ? 1. : 0.,\n\t\t\t\t\tst::dialogsOnlineBadgeDuration);\n\t\t\t} else {\n\t\t\t\tconst auto s = kOnlineCircleSize + kOnlineCircleStrokeWidth;\n\t\t\t\tconst auto index = pin->index;\n\t\t\t\tCore::Sandbox::Instance().customEnterFromEventLoop([=] {\n\t\t\t\t\t_pins[index]->onlineAnimation.start(\n\t\t\t\t\t\t[=] {\n\t\t\t\t\t\t\t[self setNeedsDisplayInRect:NSMakeRect(\n\t\t\t\t\t\t\t\t_pins[index]->x + kCircleDiameter - s,\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\ts,\n\t\t\t\t\t\t\t\ts)];\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonline ? 0. : 1.,\n\t\t\t\t\t\tonline ? 1. : 0.,\n\t\t\t\t\t\tst::dialogsOnlineBadgeDuration);\n\t\t\t\t});\n\t\t\t}\n\t\t}, *peerChangedLifetime);\n\t};\n\n\tconst auto updatePinnedChats = [=] {\n\t\t_pins = ranges::views::zip(\n\t\t\t_session->data().pinnedChatsOrder(nullptr),\n\t\t\tranges::views::ints(0, ranges::unreachable)\n\t\t) | ranges::views::transform([=](const auto &pair) {\n\t\t\tconst auto index = pair.second;\n\t\t\tauto peer = pair.first.history()->peer;\n\t\t\tauto view = peer->createUserpicView();\n\t\t\treturn std::make_unique<Pin>(Pin{\n\t\t\t\t.peer = std::move(peer),\n\t\t\t\t.userpicView = std::move(view),\n\t\t\t\t.index = index,\n\t\t\t\t.lastseen = CalculateLastseenStatus(peer),\n\t\t\t});\n\t\t}) | ranges::to_vector;\n\t\t_selfUnpinned = ranges::none_of(peers, &PeerData::isSelf);\n\t\t_repliesUnpinned = ranges::none_of(peers, &PeerData::isRepliesChat);\n\n\t\tpeerChangedLifetime->destroy();\n\t\tfor (const auto &pin : _pins) {\n\t\t\tconst auto peer = pin->peer;\n\t\t\tconst auto index = pin->index;\n\n\t\t\t_session->changes().peerUpdates(\n\t\t\t\tpeer,\n\t\t\t\tUpdateFlag::Photo\n\t\t\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\t\t\t_pins[index]->userpicView = update.peer->createUserpicView();\n\t\t\t\tlistenToDownloaderFinished();\n\t\t\t}, *peerChangedLifetime);\n\n\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\tif (!user->isServiceUser()\n\t\t\t\t\t&& !user->isBot()\n\t\t\t\t\t&& !peer->isSelf()) {\n\t\t\t\t\tprocessOnline(pin);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trpl::merge(\n\t\t\t\t_session->changes().historyUpdates(\n\t\t\t\t\t_session->data().history(peer),\n\t\t\t\t\tData::HistoryUpdate::Flag::UnreadView\n\t\t\t\t) | rpl::to_empty,\n\t\t\t\t_session->changes().peerFlagsValue(\n\t\t\t\t\tpeer,\n\t\t\t\t\tUpdateFlag::Notifications\n\t\t\t\t) | rpl::to_empty\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tupdateBadge(_pins[index]);\n\t\t\t}, *peerChangedLifetime);\n\t\t}\n\n\t\tupdateUserpics();\n\t};\n\n\trpl::single(rpl::empty) | rpl::then(\n\t\t_session->data().pinnedDialogsOrderUpdated()\n\t) | rpl::on_next(updatePinnedChats, _lifetime);\n\n\tconst auto ArchiveId = Data::Folder::kId;\n\trpl::single(\n\t\t_session->data().folderLoaded(ArchiveId)\n\t) | rpl::then(\n\t\t_session->data().chatsListChanges()\n\t) | rpl::filter([](Data::Folder *folder) {\n\t\treturn folder && (folder->id() == ArchiveId);\n\t}) | rpl::on_next([=](Data::Folder *folder) {\n\t\t_hasArchive = !folder->chatsList()->empty();\n\t\tif (_archive.isNull()) {\n\t\t\t_archive = ArchiveUserpic(folder);\n\t\t}\n\t\tupdateUserpics();\n\t}, _lifetime);\n\n\tconst auto updateOnlineColor = [=] {\n\t\tauto r = 0, g = 0, b = 0, a = 0;\n\t\tst::dialogsOnlineBadgeFg->c.getRgb(&r, &g, &b, &a);\n\t\t_r = r / 255.;\n\t\t_g = g / 255.;\n\t\t_b = b / 255.;\n\t\t_a = a / 255.;\n\t};\n\tupdateOnlineColor();\n\n\tconst auto localGuard = _lifetime.make_state<base::has_weak_ptr>();\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tcrl::on_main(&(*localGuard), [=] {\n\t\t\tupdateOnlineColor();\n\t\t\tif (const auto f = _session->data().folderLoaded(ArchiveId)) {\n\t\t\t\t_archive = ArchiveUserpic(f);\n\t\t\t}\n\t\t\t_savedMessages = SavedMessagesUserpic();\n\t\t\t_repliesMessages = RepliesMessagesUserpic();\n\t\t\tupdateUserpics();\n\t\t});\n\t}, _lifetime);\n\n\tlistenToDownloaderFinished();\n\t[self processHorizontalReorder];\n\treturn self;\n}\n\n- (void)dealloc {\n\tif (self.image) {\n\t\t[self.image release];\n\t}\n\t[super dealloc];\n}\n\n- (int)shift {\n\treturn (_hasArchive ? 1 : 0) + (_selfUnpinned ? 1 : 0);\n}\n\n- (void)gestureHandler:(NSPressGestureRecognizer*)gesture {\n\t_gestures.fire(std::move(gesture));\n}\n\n- (int)indexFromX:(int)position {\n\tconst auto x = position\n\t\t- kPinnedButtonsLeftSkip\n\t\t+ kPinnedButtonsSpace / 2;\n\treturn x / (kCircleDiameter + kPinnedButtonsSpace) - [self shift];\n}\n\n- (void)performAction:(int)xPosition {\n\tconst auto index = [self indexFromX:xPosition];\n\tconst auto peer = (index < 0 || index >= int(std::size(_pins)))\n\t\t? nullptr\n\t\t: _pins[index]->peer;\n\tif (!peer && !_hasArchive && !_selfUnpinned) {\n\t\treturn;\n\t}\n\n\tconst auto active = Core::App().activePrimaryWindow();\n\tconst auto controller = active ? active->sessionController() : nullptr;\n\tconst auto openFolder = [=] {\n\t\tconst auto folder = _session->data().folderLoaded(Data::Folder::kId);\n\t\tif (folder && controller) {\n\t\t\tcontroller->openFolder(folder);\n\t\t}\n\t};\n\tCore::Sandbox::Instance().customEnterFromEventLoop([=] {\n\t\tif (_hasArchive && (index == (_selfUnpinned ? -2 : -1))) {\n\t\t\topenFolder();\n\t\t} else {\n\t\t\tcontroller->showPeerHistory((_selfUnpinned && index == -1)\n\t\t\t\t? _session->user()\n\t\t\t\t: peer);\n\t\t}\n\t});\n}\n\n- (QImage)imageToDraw:(int)i {\n\tExpects(i < int(std::size(_pins)));\n\tif (i < 0) {\n\t\tif (_hasArchive && (i == -[self shift])) {\n\t\t\treturn _archive;\n\t\t} else if (_selfUnpinned) {\n\t\t\treturn _savedMessages;\n\t\t} else if (_repliesUnpinned) {\n\t\t\treturn _repliesMessages;\n\t\t}\n\t}\n\treturn _pins[i]->userpic;\n}\n\n- (void)drawSinglePin:(int)i rect:(NSRect)dirtyRect {\n\tconst auto rect = [&] {\n\t\tauto rect = PeerRectByIndex(i + [self shift]);\n\t\tif (i < 0) {\n\t\t\treturn rect;\n\t\t}\n\t\tauto &pin = _pins[i];\n\t\t// We can have x = 0 when the pin is dragged.\n\t\trect.origin.x = ((!pin->x && !pin->onTop) ? rect.origin.x : pin->x);\n\t\tpin->x = rect.origin.x;\n\t\treturn rect;\n\t}();\n\tif (!NSIntersectsRect(rect, dirtyRect)) {\n\t\treturn;\n\t}\n\tCGContextRef context = [[NSGraphicsContext currentContext] CGContext];\n\t{\n\t\tCGImageRef image = ([self imageToDraw:i]).toCGImage();\n\t\tCGContextDrawImage(context, rect, image);\n\t\tCGImageRelease(image);\n\t}\n\n\tif (i >= 0) {\n\t\tconst auto &pin = _pins[i];\n\t\tconst auto rectRight = NSMaxX(rect);\n\t\tif (!pin->unreadBadge.isNull()) {\n\t\t\tCGImageRef image = pin->unreadBadge.toCGImage();\n\t\t\tconst auto w = CGImageGetWidth(image)\n\t\t\t\t/ float64(style::DevicePixelRatio());\n\t\t\tconst auto borderRect = CGRectMake(\n\t\t\t\trectRight - w,\n\t\t\t\t0,\n\t\t\t\tw,\n\t\t\t\tCGImageGetHeight(image)\n\t\t\t\t\t/ float64(style::DevicePixelRatio()));\n\t\t\tCGContextDrawImage(context, borderRect, image);\n\t\t\tCGImageRelease(image);\n\t\t\treturn;\n\t\t}\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto online = pin->lastseen.isOnline(now);\n\t\tconst auto value = pin->onlineAnimation.value(online ? 1. : 0.);\n\t\tif (value < 0.05) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto lineWidth = kOnlineCircleStrokeWidth;\n\t\tconst auto circleSize = kOnlineCircleSize;\n\t\tconst auto progress = value * circleSize;\n\t\tconst auto diff = (circleSize - progress) / 2;\n\t\tconst auto borderRect = CGRectMake(\n\t\t\trectRight - circleSize + diff - lineWidth / 2,\n\t\t\tdiff,\n\t\t\tprogress,\n\t\t\tprogress);\n\n\t\tCGContextSetRGBStrokeColor(context, 0, 0, 0, 1.0);\n\t\tCGContextSetRGBFillColor(context, _r, _g, _b, _a);\n\t\tCGContextSetLineWidth(context, lineWidth);\n\t\tCGContextFillEllipseInRect(context, borderRect);\n\t\tCGContextStrokeEllipseInRect(context, borderRect);\n\t}\n}\n\n- (void)drawRect:(NSRect)dirtyRect {\n\tconst auto shift = [self shift];\n\tif (_pins.empty() && !shift) {\n\t\treturn;\n\t}\n\tauto indexToTop = -1;\n\tconst auto guard = gsl::finally([&] {\n\t\tif (indexToTop >= 0) {\n\t\t\t[self drawSinglePin:indexToTop rect:dirtyRect];\n\t\t}\n\t});\n\tfor (auto i = -shift; i < int(std::size(_pins)); i++) {\n\t\tif (i >= 0 && _pins[i]->onTop && (indexToTop < 0)) {\n\t\t\tindexToTop = i;\n\t\t\tcontinue;\n\t\t}\n\t\t[self drawSinglePin:i rect:dirtyRect];\n\t}\n}\n\n@end // @@implementation PinnedDialogsPanel\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/items/mac_scrubber_item.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#import <AppKit/NSPopoverTouchBarItem.h>\n#import <AppKit/NSTouchBar.h>\n\nnamespace Window {\nclass Controller;\n} // namespace Window\n\nAPI_AVAILABLE(macos(10.12.2))\n@interface StickerEmojiPopover : NSPopoverTouchBarItem<NSTouchBarDelegate>\n- (id)init:(not_null<Window::Controller*>)controller\n\tidentifier:(NSTouchBarItemIdentifier)identifier;\n@end\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/items/mac_scrubber_item.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/touchbar/items/mac_scrubber_item.h\"\n\n#include \"api/api_common.h\"\n#include \"api/api_sending.h\"\n#include \"base/call_delayed.h\"\n#include \"base/platform/mac/base_utilities_mac.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/painter.h\"\n#include \"chat_helpers/emoji_list_widget.h\"\n#include \"core/sandbox.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"platform/mac/touchbar/mac_touchbar_common.h\"\n#include \"styles/style_basic.h\"\n#include \"styles/style_settings.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n\n#import <AppKit/NSCustomTouchBarItem.h>\n#import <AppKit/NSGestureRecognizer.h>\n#import <AppKit/NSImage.h>\n#import <AppKit/NSImageView.h>\n#import <AppKit/NSPressGestureRecognizer.h>\n#import <AppKit/NSScrollView.h>\n#import <AppKit/NSScrubber.h>\n#import <AppKit/NSScrubberItemView.h>\n#import <AppKit/NSScrubberLayout.h>\n#import <AppKit/NSSegmentedControl.h>\n#import <AppKit/NSTextField.h>\n\n#include <QtWidgets/QTextEdit>\n\nusing TouchBar::kCircleDiameter;\nusing TouchBar::CreateNSImageFromStyleIcon;\n\nnamespace {\n\n//https://developer.apple.com/design/human-interface-guidelines/macos/touch-bar/touch-bar-icons-and-images/\nconstexpr auto kIdealIconSize = 36;\nconstexpr auto kSegmentIconSize = 25;\nconstexpr auto kSegmentSize = 92;\n\nconstexpr auto kMaxStickerSets = 5;\n\nconstexpr auto kGestureStateProcessed = {\n\tNSGestureRecognizerStateChanged,\n\tNSGestureRecognizerStateBegan,\n};\n\nconstexpr auto kGestureStateFinished = {\n\tNSGestureRecognizerStateEnded,\n\tNSGestureRecognizerStateCancelled,\n\tNSGestureRecognizerStateFailed,\n};\n\nconst auto kStickersScrubber = @\"scrubberStickers\";\nconst auto kEmojiScrubber = @\"scrubberEmoji\";\n\nconst auto kStickerItemIdentifier = @\"stickerItem\";\nconst auto kEmojiItemIdentifier = @\"emojiItem\";\nconst auto kPickerTitleItemIdentifier = @\"pickerTitleItem\";\n\nenum ScrubberItemType {\n\tEmoji,\n\tSticker,\n\tNone,\n};\n\ninline bool IsSticker(ScrubberItemType type) {\n\treturn type == ScrubberItemType::Sticker;\n}\n\nstruct PickerScrubberItem {\n\tPickerScrubberItem(QString title) : title(title) {\n\t}\n\tPickerScrubberItem(DocumentData *document) : document(document) {\n\t\tmediaView = document->createMediaView();\n\t\tmediaView->checkStickerSmall();\n\t\tupdateThumbnail();\n\t}\n\tPickerScrubberItem(EmojiPtr emoji) : emoji(emoji) {\n\t}\n\n\tvoid updateThumbnail() {\n\t\tif (!document || !image.isNull()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto sticker = mediaView->getStickerSmall();\n\t\tif (!sticker) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto size = sticker->size()\n\t\t\t.scaled(kCircleDiameter, kCircleDiameter, Qt::KeepAspectRatio);\n\t\timage = sticker->pixSingle(\n\t\t\tsize,\n\t\t\t{ .outer = { kCircleDiameter, kCircleDiameter } }).toImage();\n\t}\n\n\tbool isStickerLoaded() const {\n\t\treturn !image.isNull();\n\t}\n\n\tQString title = QString();\n\n\tDocumentData *document = nullptr;\n\tstd::shared_ptr<Data::DocumentMedia> mediaView = nullptr;\n\tQImage image;\n\n\tEmojiPtr emoji = nullptr;\n};\n\nstruct PickerScrubberItemsHolder {\n\tstd::vector<PickerScrubberItem> stickers;\n\tstd::vector<PickerScrubberItem> emoji;\n\n\tint size(ScrubberItemType type) {\n\t\treturn IsSticker(type) ? stickers.size() : emoji.size();\n\t}\n\n\tauto at(int index, ScrubberItemType type) {\n\t\treturn IsSticker(type) ? stickers[index] : emoji[index];\n\t}\n};\n\nusing Platform::Q2NSString;\nusing Platform::Q2NSImage;\n\nNSImage *CreateNSImageFromEmoji(EmojiPtr emoji) {\n\tauto image = QImage(\n\t\tQSize(kIdealIconSize, kIdealIconSize) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\timage.fill(Qt::black);\n\t{\n\t\tPainter paint(&image);\n\t\tPainterHighQualityEnabler hq(paint);\n\t\tUi::Emoji::Draw(\n\t\t\tpaint,\n\t\t\temoji,\n\t\t\tUi::Emoji::GetSizeTouchbar(),\n\t\t\t0,\n\t\t\t0);\n\t}\n\treturn Q2NSImage(image);\n}\n\nauto ActiveChat(not_null<Window::Controller*> controller) {\n\tif (const auto sessionController = controller->sessionController()) {\n\t\treturn sessionController->activeChatCurrent();\n\t}\n\treturn Dialogs::Key();\n}\n\nbool CanSendToActiveChat(\n\t\tnot_null<Window::Controller*> controller,\n\t\t\tChatRestriction right) {\n\tif (const auto topic = ActiveChat(controller).topic()) {\n\t\treturn Data::CanSend(topic, right);\n\t} else if (const auto history = ActiveChat(controller).history()) {\n\t\treturn Data::CanSend(history->peer, right);\n\t}\n\treturn false;\n}\n\nstd::optional<QString> RestrictionToSend(\n\t\tnot_null<Window::Controller*> controller,\n\t\tChatRestriction right) {\n\tif (const auto peer = ActiveChat(controller).peer()) {\n\t\tif (const auto error = Data::RestrictionError(peer, right)) {\n\t\t\treturn *error;\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nQString TitleRecentlyUsed(const Data::StickersSets &sets) {\n\tconst auto it = sets.find(Data::Stickers::CloudRecentSetId);\n\treturn (it != sets.cend())\n\t\t? it->second->title\n\t\t: tr::lng_recent_stickers(tr::now);\n}\n\nvoid AppendStickerSet(\n\t\tconst Data::StickersSets &sets,\n\t\tstd::vector<PickerScrubberItem> &to,\n\t\tuint64 setId) {\n\tconst auto it = sets.find(setId);\n\tif (it == sets.cend() || it->second->stickers.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto set = it->second.get();\n\tif (set->flags & Data::StickersSetFlag::Archived) {\n\t\treturn;\n\t}\n\tif (!(set->flags & Data::StickersSetFlag::Installed)) {\n\t\treturn;\n\t}\n\n\tto.emplace_back(PickerScrubberItem(set->title.isEmpty()\n\t\t? set->shortName\n\t\t: set->title));\n\tfor (const auto sticker : set->stickers) {\n\t\tto.emplace_back(PickerScrubberItem(sticker));\n\t}\n}\n\nvoid AppendRecentStickers(\n\t\tconst Data::StickersSets &sets,\n\t\tRecentStickerPack &recentPack,\n\t\tstd::vector<PickerScrubberItem> &to) {\n\tconst auto cloudIt = sets.find(Data::Stickers::CloudRecentSetId);\n\tconst auto cloudCount = (cloudIt != sets.cend())\n\t\t? cloudIt->second->stickers.size()\n\t\t: 0;\n\tif (cloudCount > 0) {\n\t\tto.emplace_back(PickerScrubberItem(cloudIt->second->title));\n\t\tfor (const auto document : cloudIt->second->stickers) {\n\t\t\tif (document->owner().stickers().isFaved(document)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tto.emplace_back(PickerScrubberItem(document));\n\t\t}\n\t}\n\tfor (const auto &recent : recentPack) {\n\t\tto.emplace_back(PickerScrubberItem(recent.first));\n\t}\n}\n\nvoid AppendFavedStickers(\n\t\tconst Data::StickersSets &sets,\n\t\tstd::vector<PickerScrubberItem> &to) {\n\tconst auto it = sets.find(Data::Stickers::FavedSetId);\n\tconst auto count = (it != sets.cend())\n\t\t? it->second->stickers.size()\n\t\t: 0;\n\tif (!count) {\n\t\treturn;\n\t}\n\tto.emplace_back(PickerScrubberItem(\n\t\ttr::lng_mac_touchbar_favorite_stickers(tr::now)));\n\tfor (const auto document : it->second->stickers) {\n\t\tto.emplace_back(PickerScrubberItem(document));\n\t}\n}\n\n[[nodiscard]] EmojiPack RecentEmojiSection() {\n\tconst auto list = Core::App().settings().recentEmoji();\n\tauto result = EmojiPack();\n\tresult.reserve(list.size());\n\tfor (const auto &emoji : list) {\n\t\tif (const auto one = std::get_if<EmojiPtr>(&emoji.id.data)) {\n\t\t\tresult.push_back(*one);\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid AppendEmojiPacks(\n\t\tconst Data::StickersSets &sets,\n\t\tstd::vector<PickerScrubberItem> &to) {\n\tfor (auto i = 0; i != ChatHelpers::kEmojiSectionCount; ++i) {\n\t\tconst auto section = static_cast<Ui::Emoji::Section>(i);\n\t\tconst auto list = (section == Ui::Emoji::Section::Recent)\n\t\t\t? RecentEmojiSection()\n\t\t\t: Ui::Emoji::GetSection(section);\n\t\tconst auto title = (section == Ui::Emoji::Section::Recent)\n\t\t\t? TitleRecentlyUsed(sets)\n\t\t\t: ChatHelpers::EmojiCategoryTitle(i)(tr::now);\n\t\tto.emplace_back(title);\n\t\tfor (const auto &emoji : list) {\n\t\t\tto.emplace_back(PickerScrubberItem(emoji));\n\t\t}\n\t}\n}\n\n} // namespace\n\n@interface PickerScrubberItemView : NSScrubberImageItemView {\n\t@public\n\tDocumentId documentId;\n}\n@end // @interface PickerScrubberItemView\n@implementation PickerScrubberItemView\n@end // @implementation PickerScrubberItemView\n\n#pragma mark - PickerCustomTouchBarItem\n\n@interface PickerCustomTouchBarItem : NSCustomTouchBarItem\n\t<NSScrubberDelegate,\n\tNSScrubberDataSource,\n\tNSScrubberFlowLayoutDelegate>\n@end // @interface PickerCustomTouchBarItem\n\n@implementation PickerCustomTouchBarItem {\n\tScrubberItemType _type;\n\tstd::shared_ptr<PickerScrubberItemsHolder> _itemsDataSource;\n\tstd::unique_ptr<PickerScrubberItem> _error;\n\tDocumentId _lastPreviewedSticker;\n\tWindow::Controller *_controller;\n\tHistory *_history;\n\n\trpl::event_stream<> _closeRequests;\n\trpl::lifetime _lifetime;\n}\n\n- (id)init:(ScrubberItemType)type\n\t\tcontroller:(not_null<Window::Controller*>)controller\n\t\titems:(std::shared_ptr<PickerScrubberItemsHolder>)items {\n\tExpects(controller->sessionController() != nullptr);\n\tself = [super initWithIdentifier:IsSticker(type)\n\t\t? kStickersScrubber\n\t\t: kEmojiScrubber];\n\tif (!self) {\n\t\treturn self;\n\t}\n\t_type = type;\n\t_controller = controller;\n\t_itemsDataSource = items;\n\n\tauto *scrubber = [[[NSScrubber alloc] initWithFrame:NSZeroRect]\n\t\tautorelease];\n\tauto *layout = [[[NSScrubberFlowLayout alloc] init] autorelease];\n\tlayout.itemSpacing = 10;\n\tscrubber.scrubberLayout = layout;\n\tscrubber.mode = NSScrubberModeFree;\n\tscrubber.delegate = self;\n\tscrubber.dataSource = self;\n\tscrubber.floatsSelectionViews = true;\n\tscrubber.showsAdditionalContentIndicators = true;\n\tscrubber.itemAlignment = NSScrubberAlignmentCenter;\n\n\t[scrubber registerClass:[PickerScrubberItemView class]\n\t\tforItemIdentifier:kStickerItemIdentifier];\n\t[scrubber registerClass:[NSScrubberTextItemView class]\n\t\tforItemIdentifier:kPickerTitleItemIdentifier];\n\t[scrubber registerClass:[NSScrubberImageItemView class]\n\t\tforItemIdentifier:kEmojiItemIdentifier];\n\n\tif (IsSticker(type)) {\n\t\tauto *gesture = [[[NSPressGestureRecognizer alloc]\n\t\t\tinitWithTarget:self\n\t\t\taction:@selector(gesturePreviewHandler:)] autorelease];\n\t\tgesture.allowedTouchTypes = NSTouchTypeMaskDirect;\n\t\tgesture.minimumPressDuration = QApplication::startDragTime() / 1000.;\n\t\tgesture.allowableMovement = 0;\n\t\t[scrubber addGestureRecognizer:gesture];\n\n\t\tconst auto kRight = ChatRestriction::SendStickers;\n\t\tif (const auto error = RestrictionToSend(_controller, kRight)) {\n\t\t\t_error = std::make_unique<PickerScrubberItem>(\n\t\t\t\ttr::lng_restricted_send_stickers_all(tr::now));\n\t\t}\n\t} else {\n\t\tconst auto kRight = ChatRestriction::SendOther;\n\t\tif (const auto error = RestrictionToSend(_controller, kRight)) {\n\t\t\t_error = std::make_unique<PickerScrubberItem>(\n\t\t\t\ttr::lng_restricted_send_message_all(tr::now));\n\t\t}\n\t}\n\t_lastPreviewedSticker = 0;\n\n\tself.view = scrubber;\n\treturn self;\n}\n\n- (PickerScrubberItem)itemAt:(int)index {\n\treturn _error ? *_error : _itemsDataSource->at(index, _type);\n}\n\n- (void)gesturePreviewHandler:(NSPressGestureRecognizer*)gesture {\n\tconst auto customEnter = [=](auto &&callback) {\n\t\tCore::Sandbox::Instance().customEnterFromEventLoop([=] {\n\t\t\tif (_controller) {\n\t\t\t\tcallback();\n\t\t\t}\n\t\t});\n\t};\n\n\tconst auto checkState = [&](const auto &states) {\n\t\treturn ranges::contains(states, gesture.state);\n\t};\n\n\tif (checkState(kGestureStateProcessed)) {\n\t\tNSScrollView *scrollView = self.view;\n\t\tauto *container = scrollView.documentView.subviews.firstObject;\n\t\tif (!container) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto point = [gesture locationInView:container];\n\n\t\tfor (PickerScrubberItemView *item in container.subviews) {\n\t\t\tif (![item isMemberOfClass:[PickerScrubberItemView class]]\n\t\t\t\t|| (item->documentId == _lastPreviewedSticker)\n\t\t\t\t|| !NSPointInRect(point, item.frame)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t_lastPreviewedSticker = item->documentId;\n\t\t\tauto &owner = _controller->sessionController()->session().data();\n\t\t\tconst auto doc = owner.document(item->documentId);\n\t\t\tcustomEnter([=] {\n\t\t\t\t_controller->widget()->showMediaPreview(\n\t\t\t\t\tData::FileOrigin(),\n\t\t\t\t\tdoc);\n\t\t\t});\n\t\t\tbreak;\n\t\t}\n\t} else if (checkState(kGestureStateFinished)) {\n\t\tcustomEnter([=] { _controller->widget()->hideMediaPreview(); });\n\t\t_lastPreviewedSticker = 0;\n\t}\n}\n\n- (void)encodeWithCoder:(nonnull NSCoder*)aCoder {\n\t// Has not been implemented.\n}\n\n#pragma mark - NSScrubberDelegate\n\n- (NSInteger)numberOfItemsForScrubber:(NSScrubber*)scrubber {\n\treturn _error ? 1 : _itemsDataSource->size(_type);\n}\n\n- (NSScrubberItemView*)scrubber:(NSScrubber*)scrubber\n\t\tviewForItemAtIndex:(NSInteger)index {\n\tconst auto item = [self itemAt:index];\n\tif (const auto document = item.document) {\n\t\tPickerScrubberItemView *itemView = [scrubber\n\t\t\tmakeItemWithIdentifier:kStickerItemIdentifier\n\t\t\towner:self];\n\t\titemView.imageView.image = Q2NSImage(item.image);\n\t\titemView->documentId = document->id;\n\t\treturn itemView;\n\t} else if (const auto emoji = item.emoji) {\n\t\tNSScrubberImageItemView *itemView = [scrubber\n\t\t\tmakeItemWithIdentifier:kEmojiItemIdentifier\n\t\t\towner:self];\n\t\titemView.imageView.image = CreateNSImageFromEmoji(emoji);\n\t\treturn itemView;\n\t} else {\n\t\tNSScrubberTextItemView *itemView = [scrubber\n\t\t\tmakeItemWithIdentifier:kPickerTitleItemIdentifier\n\t\t\towner:self];\n\t\titemView.textField.stringValue = Q2NSString(item.title);\n\t\treturn itemView;\n\t}\n}\n\n- (NSSize)scrubber:(NSScrubber*)scrubber\n\t\tlayout:(NSScrubberFlowLayout*)layout\n\t\tsizeForItemAtIndex:(NSInteger)index {\n\tconst auto t = [self itemAt:index].title;\n\tconst auto w = t.isEmpty() ? 0 : TouchBar::WidthFromString(Q2NSString(t));\n\treturn NSMakeSize(kCircleDiameter + w, kCircleDiameter);\n}\n\n- (void)scrubber:(NSScrubber*)scrubber\n\t\tdidSelectItemAtIndex:(NSInteger)index {\n\tscrubber.selectedIndex = -1;\n\tconst auto sticker = _itemsDataSource->at(index, _type);\n\tconst auto document = sticker.document;\n\tconst auto emoji = sticker.emoji;\n\tconst auto kRight = document\n\t\t? ChatRestriction::SendStickers\n\t\t: ChatRestriction::SendOther;\n\tif (!CanSendToActiveChat(_controller, kRight) || _error) {\n\t\treturn;\n\t}\n\tauto callback = [=] {\n\t\tif (document) {\n\t\t\tif (const auto error = RestrictionToSend(_controller, kRight)) {\n\t\t\t\t_controller->show(Ui::MakeInformBox(*error));\n\t\t\t\treturn true;\n\t\t\t} else if (Window::ShowSendPremiumError(_controller->sessionController(), document)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tApi::SendExistingDocument(\n\t\t\t\tApi::MessageToSend(\n\t\t\t\t\tApi::SendAction(ActiveChat(_controller).history())),\n\t\t\t\tdocument);\n\t\t\treturn true;\n\t\t} else if (emoji) {\n\t\t\tif (const auto error = RestrictionToSend(_controller, kRight)) {\n\t\t\t\t_controller->show(Ui::MakeInformBox(*error));\n\t\t\t\treturn true;\n\t\t\t} else if (const auto inputField = qobject_cast<QTextEdit*>(\n\t\t\t\t\tQApplication::focusWidget())) {\n\t\t\t\tUi::InsertEmojiAtCursor(inputField->textCursor(), emoji);\n\t\t\t\tCore::App().settings().incrementRecentEmoji({ emoji });\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\n\tif (!Core::Sandbox::Instance().customEnterFromEventLoop(\n\t\t\tstd::move(callback))) {\n\t\treturn;\n\t}\n\n\t_closeRequests.fire({});\n}\n\n- (rpl::producer<>)closeRequests {\n\treturn _closeRequests.events();\n}\n\n- (rpl::lifetime &)lifetime {\n\treturn _lifetime;\n}\n\n@end // @implementation PickerCustomTouchBarItem\n\n#pragma mark - StickerEmojiPopover\n\n@implementation StickerEmojiPopover {\n\tWindow::Controller *_controller;\n\tMain::Session *_session;\n\tstd::shared_ptr<PickerScrubberItemsHolder> _itemsDataSource;\n\tScrubberItemType _waitingForUpdate;\n\n\trpl::lifetime _lifetime;\n}\n\n- (id)init:(not_null<Window::Controller*>)controller\n\t\tidentifier:(NSTouchBarItemIdentifier)identifier {\n\tself = [super initWithIdentifier:identifier];\n\tif (!self) {\n\t\treturn nil;\n\t}\n\t_controller = controller;\n\t_session = &controller->sessionController()->session();\n\t_waitingForUpdate = ScrubberItemType::None;\n\n\tauto *segment = [[[NSSegmentedControl alloc] init] autorelease];\n\tconst auto size = kSegmentIconSize;\n\tsegment.segmentStyle = NSSegmentStyleSeparated;\n\tsegment.segmentCount = 2;\n\t[segment\n\t\tsetImage:CreateNSImageFromStyleIcon(st::settingsIconStickers, size)\n\t\tforSegment:0];\n\t[segment\n\t\tsetImage:CreateNSImageFromStyleIcon(st::settingsIconEmoji, size)\n\t\tforSegment:1];\n\t[segment setWidth:kSegmentSize forSegment:0];\n\t[segment setWidth:kSegmentSize forSegment:1];\n\tsegment.target = self;\n\tsegment.action = @selector(segmentClicked:);\n\tsegment.trackingMode = NSSegmentSwitchTrackingMomentary;\n\tself.visibilityPriority = NSTouchBarItemPriorityHigh;\n\tself.collapsedRepresentation = segment;\n\n\tself.popoverTouchBar = [[[NSTouchBar alloc] init] autorelease];\n\tself.popoverTouchBar.delegate = self;\n\n\tcontroller->sessionController()->activeChatValue(\n\t) | rpl::map([](Dialogs::Key k) {\n\t\tconst auto topic = k.topic();\n\t\tconst auto peer = k.peer();\n\t\tconst auto right = ChatRestriction::SendStickers;\n\t\treturn peer\n\t\t\t&& (topic\n\t\t\t\t? Data::CanSend(topic, right)\n\t\t\t\t: Data::CanSend(peer, right));\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](bool value) {\n\t\t[self dismissPopover:nil];\n\t}, _lifetime);\n\n\n\t_itemsDataSource = std::make_shared<PickerScrubberItemsHolder>();\n\tconst auto localGuard = _lifetime.make_state<base::has_weak_ptr>();\n\t// Workaround.\n\t// A little waiting for the sticker sets and the ending animation.\n\tbase::call_delayed(st::slideDuration, &(*localGuard), [=] {\n\t\t[self updateStickers];\n\t\t[self updateEmoji];\n\t});\n\n\trpl::merge(\n\t\trpl::merge(\n\t\t\t_session->data().stickers().updated(\n\t\t\t\tData::StickersType::Stickers),\n\t\t\t_session->data().stickers().recentUpdated(\n\t\t\t\tData::StickersType::Stickers)\n\t\t) | rpl::map_to(ScrubberItemType::Sticker),\n\t\trpl::merge(\n\t\t\tCore::App().settings().recentEmojiUpdated(),\n\t\t\tUi::Emoji::Updated()\n\t\t) | rpl::map_to(ScrubberItemType::Emoji)\n\t) | rpl::on_next([=](ScrubberItemType type) {\n\t\t_waitingForUpdate = type;\n\t}, _lifetime);\n\n\treturn self;\n}\n\n- (NSTouchBarItem*)touchBar:(NSTouchBar*)touchBar\n\t\tmakeItemForIdentifier:(NSTouchBarItemIdentifier)identifier {\n\tif (!touchBar) {\n\t\treturn nil;\n\t}\n\tconst auto isEqual = [&](NSString *string) {\n\t\treturn [identifier isEqualToString:string];\n\t};\n\n\tif (isEqual(kStickersScrubber)) {\n\t\tauto *item = [[[PickerCustomTouchBarItem alloc]\n\t\t\tinit:(ScrubberItemType::Sticker)\n\t\t\tcontroller:_controller\n\t\t\titems:_itemsDataSource] autorelease];\n\t\tauto &lifetime = [item lifetime];\n\t\t[item closeRequests] | rpl::on_next([=] {\n\t\t\t[self dismissPopover:nil];\n\t\t\t[self updateStickers];\n\t\t}, lifetime);\n\t\treturn item;\n\t} else if (isEqual(kEmojiScrubber)) {\n\t\treturn [[[PickerCustomTouchBarItem alloc]\n\t\t\tinit:(ScrubberItemType::Emoji)\n\t\t\tcontroller:_controller\n\t\t\titems:_itemsDataSource] autorelease];\n\t}\n\treturn nil;\n}\n\n- (void)segmentClicked:(NSSegmentedControl*)sender {\n\tself.popoverTouchBar.defaultItemIdentifiers = @[];\n\tconst auto identifier = sender.selectedSegment\n\t\t? kEmojiScrubber\n\t\t: kStickersScrubber;\n\n\tif (sender.selectedSegment\n\t\t\t&& _waitingForUpdate == ScrubberItemType::Emoji) {\n\t\t[self updateEmoji];\n\t} else if (!sender.selectedSegment\n\t\t\t&& _waitingForUpdate == ScrubberItemType::Sticker) {\n\t\t[self updateStickers];\n\t}\n\n\tself.popoverTouchBar.defaultItemIdentifiers = @[identifier];\n\t[self showPopover:nil];\n}\n\n- (void)addDownloadHandler {\n\tconst auto loadingLifetime = _lifetime.make_state<rpl::lifetime>();\n\tconst auto checkLoaded = [=](const auto &sticker) {\n\t\treturn !sticker.document || sticker.isStickerLoaded();\n\t};\n\tconst auto isPerformedOnMain = loadingLifetime->make_state<bool>(true);\n\tconst auto localGuard = loadingLifetime->make_state<base::has_weak_ptr>();\n\t_session->downloaderTaskFinished(\n\t) | rpl::on_next(crl::guard(&(*localGuard), [=] {\n\t\tif (*isPerformedOnMain) {\n\t\t\tcrl::on_main(&(*localGuard), [=] {\n\t\t\t\tfor (auto &sticker : _itemsDataSource->stickers) {\n\t\t\t\t\tsticker.updateThumbnail();\n\t\t\t\t}\n\t\t\t\tif (ranges::all_of(_itemsDataSource->stickers, checkLoaded)) {\n\t\t\t\t\tloadingLifetime->destroy();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t*isPerformedOnMain = true;\n\t\t\t});\n\t\t}\n\t\t*isPerformedOnMain = false;\n\t}), *loadingLifetime);\n}\n\n- (void)updateStickers {\n\tauto &stickers = _session->data().stickers();\n\tstd::vector<PickerScrubberItem> temp;\n\tAppendFavedStickers(stickers.sets(), temp);\n\tAppendRecentStickers(stickers.sets(), stickers.getRecentPack(), temp);\n\tauto count = 0;\n\tfor (const auto setId : stickers.setsOrderRef()) {\n\t\tAppendStickerSet(stickers.sets(), temp, setId);\n\t\tif (++count == kMaxStickerSets) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (!temp.size()) {\n\t\ttemp.emplace_back(PickerScrubberItem(\n\t\t\ttr::lng_stickers_nothing_found(tr::now)));\n\t}\n\t_itemsDataSource->stickers = std::move(temp);\n\t_waitingForUpdate = ScrubberItemType::None;\n\t[self addDownloadHandler];\n}\n\n- (void)updateEmoji {\n\tstd::vector<PickerScrubberItem> temp;\n\tAppendEmojiPacks(_session->data().stickers().sets(), temp);\n\t_itemsDataSource->emoji = std::move(temp);\n\t_waitingForUpdate = ScrubberItemType::None;\n}\n\n@end // @implementation StickerEmojiPopover\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_audio.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#import <AppKit/NSTouchBar.h>\n\nAPI_AVAILABLE(macos(10.12.2))\n@interface TouchBarAudioPlayer : NSTouchBar<NSTouchBarDelegate>\n- (rpl::producer<>)closeRequests;\n@end\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_audio.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/touchbar/mac_touchbar_audio.h\"\n\n#include \"media/audio/media_audio.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"platform/mac/touchbar/mac_touchbar_common.h\"\n#include \"platform/mac/touchbar/mac_touchbar_controls.h\"\n#include \"styles/style_media_player.h\"\n\n#import <AppKit/NSButton.h>\n#import <AppKit/NSCustomTouchBarItem.h>\n#import <AppKit/NSSlider.h>\n#import <AppKit/NSSliderTouchBarItem.h>\n\nusing TouchBar::kCircleDiameter;\nusing TouchBar::CreateNSImageFromStyleIcon;\n\nnamespace {\n\nconstexpr auto kSongType = AudioMsgId::Type::Song;\n\nconst auto *kCustomizationIdPlayer = @\"telegram.touchbar\";\n\ninline NSTouchBarItemIdentifier Format(NSString *s) {\n\treturn [NSString stringWithFormat:@\"%@.%@\", kCustomizationIdPlayer, s];\n}\nconst auto kSeekBarItemIdentifier = Format(@\"seekbar\");\nconst auto kPlayItemIdentifier = Format(@\"play\");\nconst auto kNextItemIdentifier = Format(@\"nextItem\");\nconst auto kPreviousItemIdentifier = Format(@\"previousItem\");\nconst auto kClosePlayerItemIdentifier = Format(@\"closePlayer\");\nconst auto kCurrentPositionItemIdentifier = Format(@\"currentPosition\");\n\n} // namespace\n\n#pragma mark - TouchBarAudioPlayer\n\n@interface TouchBarAudioPlayer()\n@end // @interface TouchBarAudioPlayer\n\n@implementation TouchBarAudioPlayer {\n\trpl::event_stream<> _closeRequests;\n\trpl::producer< Media::Player::TrackState> _trackState;\n\n\trpl::lifetime _lifetime;\n}\n\n- (id)init {\n\tself = [super init];\n\tif (!self) {\n\t\treturn self;\n\t}\n\tself.delegate = self;\n\tself.customizationIdentifier = kCustomizationIdPlayer.lowercaseString;\n\tself.defaultItemIdentifiers = @[\n\t\tkPlayItemIdentifier,\n\t\tkPreviousItemIdentifier,\n\t\tkNextItemIdentifier,\n\t\tkSeekBarItemIdentifier,\n\t\tkClosePlayerItemIdentifier];\n\tself.customizationAllowedItemIdentifiers = @[\n\t\tkPlayItemIdentifier,\n\t\tkPreviousItemIdentifier,\n\t\tkNextItemIdentifier,\n\t\tkCurrentPositionItemIdentifier,\n\t\tkSeekBarItemIdentifier,\n\t\tkClosePlayerItemIdentifier];\n\n\t_trackState = Media::Player::instance()->updatedNotifier(\n\t) | rpl::filter([=](const Media::Player::TrackState &state) {\n\t\treturn state.id.type() == kSongType;\n\t});\n\n\treturn self;\n}\n\n- (NSTouchBarItem*)touchBar:(NSTouchBar*)touchBar\n\t\tmakeItemForIdentifier:(NSTouchBarItemIdentifier)itemId {\n\tif (!touchBar) {\n\t\treturn nil;\n\t}\n\tconst auto mediaPlayer = Media::Player::instance();\n\tconst auto isEqual = [&](NSString *string) {\n\t\treturn [itemId isEqualToString:string];\n\t};\n\n\tif (isEqual(kSeekBarItemIdentifier)) {\n\t\tauto *item = TouchBar::CreateTouchBarSlider(\n\t\t\titemId,\n\t\t\t_lifetime,\n\t\t\t[=](bool touchUp, double value, double duration) {\n\t\t\t\tif (touchUp) {\n\t\t\t\t\tmediaPlayer->finishSeeking(kSongType, value);\n\t\t\t\t} else {\n\t\t\t\t\tmediaPlayer->startSeeking(kSongType);\n\t\t\t\t}\n\t\t\t},\n\t\t\trpl::duplicate(_trackState));\n\t\treturn [item autorelease];\n\t} else if (isEqual(kNextItemIdentifier)\n\t\t\t|| isEqual(kPreviousItemIdentifier)) {\n\t\tconst auto isNext = isEqual(kNextItemIdentifier);\n\t\tauto *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:itemId];\n\n\t\tauto *button = TouchBar::CreateTouchBarButton(\n\t\t\tisNext\n\t\t\t\t? st::touchBarIconPlayerNext\n\t\t\t\t: st::touchBarIconPlayerPrevious,\n\t\t\t_lifetime,\n\t\t\t[=] { isNext // TODO\n\t\t\t\t? mediaPlayer->next(kSongType)\n\t\t\t\t: mediaPlayer->previous(kSongType); });\n\t\trpl::duplicate(\n\t\t\t_trackState\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto newValue = isNext\n\t\t\t\t? mediaPlayer->nextAvailable(kSongType)\n\t\t\t\t: mediaPlayer->previousAvailable(kSongType);\n\t\t\tif (button.enabled != newValue) {\n\t\t\t\tbutton.enabled = newValue;\n\t\t\t}\n\t\t}, _lifetime);\n\n\t\titem.view = button;\n\t\titem.customizationLabel = [NSString\n\t\t\tstringWithFormat:@\"%@ Playlist Item\",\n\t\t\tisNext ? @\"Next\" : @\"Previous\"];\n\t\treturn [item autorelease];\n\t} else if (isEqual(kPlayItemIdentifier)) {\n\t\tauto *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:itemId];\n\n\t\tauto *button = TouchBar::CreateTouchBarButtonWithTwoStates(\n\t\t\tst::touchBarIconPlayerPause,\n\t\t\tst::touchBarIconPlayerPlay,\n\t\t\t_lifetime,\n\t\t\t[=](bool value) { mediaPlayer->playPause(kSongType); },\n\t\t\tfalse,\n\t\t\trpl::duplicate(\n\t\t\t\t_trackState\n\t\t\t) | rpl::map([](const auto &state) {\n\t\t\t\treturn (state.state == Media::Player::State::Playing);\n\t\t\t}) | rpl::distinct_until_changed());\n\n\t\titem.view = button;\n\t\titem.customizationLabel = @\"Play/Pause\";\n\t\treturn [item autorelease];\n\t} else if (isEqual(kClosePlayerItemIdentifier)) {\n\t\tauto *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:itemId];\n\t\tauto *button = TouchBar::CreateTouchBarButton(\n\t\t\tst::touchBarIconPlayerClose,\n\t\t\t_lifetime,\n\t\t\t[=] { _closeRequests.fire({}); });\n\n\t\titem.view = button;\n\t\titem.customizationLabel = @\"Close Player\";\n\t\treturn [item autorelease];\n\t} else if (isEqual(kCurrentPositionItemIdentifier)) {\n\t\tauto *item = TouchBar::CreateTouchBarTrackPosition(\n\t\t\titemId,\n\t\t\trpl::duplicate(_trackState));\n\t\treturn [item autorelease];\n\t}\n\treturn nil;\n}\n\n- (rpl::producer<>)closeRequests {\n\treturn _closeRequests.events();\n}\n\n@end // @implementation TouchBarAudioPlayer\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#import <AppKit/NSImage.h>\n#import <Foundation/Foundation.h>\n\nnamespace TouchBar {\n\nconstexpr auto kCircleDiameter = 30;\n\ntemplate <typename Callable>\nvoid CustomEnterToCocoaEventLoop(Callable callable) {\n\tid block = [^{ callable(); } copy]; // Don't forget to -release.\n\t[block\n\t\tperformSelectorOnMainThread:@selector(invoke)\n\t\twithObject:nil\n\t\twaitUntilDone:true];\n\t// [block performSelector:@selector(invoke) withObject:nil afterDelay:d];\n\t[block release];\n}\n\nint WidthFromString(NSString *s);\n\nNSImage *CreateNSImageFromStyleIcon(const style::icon &icon, int size);\n\n} // namespace TouchBar\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_common.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/touchbar/mac_touchbar_common.h\"\n\n#include \"base/platform/mac/base_utilities_mac.h\"\n\n#import <AppKit/NSTextField.h>\n\nnamespace TouchBar {\n\nint WidthFromString(NSString *s) {\n\treturn (int)ceil(\n\t\t[[NSTextField labelWithString:s] frame].size.width) * 1.2;\n}\n\nNSImage *CreateNSImageFromStyleIcon(const style::icon &icon, int size) {\n\tauto instance = icon.instance(QColor(255, 255, 255, 255), 100);\n\tinstance.setDevicePixelRatio(style::DevicePixelRatio());\n\tNSImage *image = Platform::Q2NSImage(instance);\n\t[image setSize:NSMakeSize(size, size)];\n\treturn image;\n}\n\n} // namespace TouchBar\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_controls.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Media {\nnamespace Player {\nstruct TrackState;\n} // namespace Player\n} // namespace Media\n\n@class NSButton;\n@class NSCustomTouchBarItem;\n@class NSImage;\n@class NSSliderTouchBarItem;\n\nnamespace TouchBar {\n\n[[nodiscard]] API_AVAILABLE(macos(10.12.2))\nNSButton *CreateTouchBarButton(\n\tNSImage *image,\n\trpl::lifetime &lifetime,\n\tFn<void()> callback);\n\n[[nodiscard]] API_AVAILABLE(macos(10.12.2))\nNSButton *CreateTouchBarButton(\n\tconst style::icon &icon,\n\trpl::lifetime &lifetime,\n\tFn<void()> callback);\n\n[[nodiscard]] API_AVAILABLE(macos(10.12.2))\nNSButton *CreateTouchBarButtonWithTwoStates(\n\tNSImage *icon1,\n\tNSImage *icon2,\n\trpl::lifetime &lifetime,\n\tFn<void(bool)> callback,\n\tbool firstState,\n\trpl::producer<bool> stateChanged = rpl::never<bool>());\n\n[[nodiscard]] API_AVAILABLE(macos(10.12.2))\nNSButton *CreateTouchBarButtonWithTwoStates(\n\tconst style::icon &icon1,\n\tconst style::icon &icon2,\n\trpl::lifetime &lifetime,\n\tFn<void(bool)> callback,\n\tbool firstState,\n\trpl::producer<bool> stateChanged = rpl::never<bool>());\n\n[[nodiscard]] API_AVAILABLE(macos(10.12.2))\nNSSliderTouchBarItem *CreateTouchBarSlider(\n\tNSString *itemId,\n\trpl::lifetime &lifetime,\n\tFn<void(bool, double, double)> callback,\n\trpl::producer<Media::Player::TrackState> stateChanged);\n\n[[nodiscard]] API_AVAILABLE(macos(10.12.2))\nNSCustomTouchBarItem *CreateTouchBarTrackPosition(\n\tNSString *itemId,\n\trpl::producer<Media::Player::TrackState> stateChanged);\n\n} // namespace TouchBar\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_controls.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/touchbar/mac_touchbar_controls.h\"\n\n#include \"base/platform/mac/base_utilities_mac.h\" // Q2NSString()\n#include \"core/sandbox.h\" // Sandbox::customEnterFromEventLoop()\n#include \"ui/text/format_values.h\" // Ui::FormatDurationText()\n#include \"media/audio/media_audio.h\"\n#include \"platform/mac/touchbar/mac_touchbar_common.h\"\n\n#import <AppKit/NSButton.h>\n#import <AppKit/NSCustomTouchBarItem.h>\n#import <AppKit/NSImage.h>\n#import <AppKit/NSImageView.h>\n#import <AppKit/NSSlider.h>\n#import <AppKit/NSSliderTouchBarItem.h>\n\nusing namespace TouchBar;\n\nnamespace {\n\nconstexpr auto kPadding = 7;\n\ninline NSImage *Icon(const style::icon &icon) {\n\treturn CreateNSImageFromStyleIcon(icon, kCircleDiameter / 2);\n}\n\ninline NSDictionary *Attributes() {\n\treturn @{\n\t\tNSFontAttributeName: [NSFont systemFontOfSize:14],\n\t\tNSParagraphStyleAttributeName:\n\t\t\t[NSMutableParagraphStyle defaultParagraphStyle],\n\t\tNSForegroundColorAttributeName: [NSColor whiteColor]\n\t};\n}\n\ninline NSString *FormatTime(TimeId time) {\n\treturn Platform::Q2NSString(Ui::FormatDurationText(time));\n}\n\n} // namespace\n\n#pragma mark - TrackPosition\n\n@interface TrackPosition : NSImageView\n@end // @interface TrackPosition\n\n@implementation TrackPosition {\n\tNSMutableString *_text;\n\n\tdouble _width;\n\tdouble _height;\n\n\trpl::lifetime _lifetime;\n}\n\n- (id)init:(rpl::producer< Media::Player::TrackState>)trackState {\n\tself = [super init];\n\tconst auto textLength = _lifetime.make_state<rpl::variable<int>>(0);\n\t_width = _height = 0;\n\t_text = [[NSMutableString alloc] initWithCapacity:13];\n\n\trpl::combine(\n\t\trpl::duplicate(\n\t\t\ttrackState\n\t\t) | rpl::map([](const auto &state) {\n\t\t\treturn state.position / 1000;\n\t\t}) | rpl::distinct_until_changed(),\n\t\tstd::move(\n\t\t\ttrackState\n\t\t) | rpl::map([](const auto &state) {\n\t\t\treturn state.length / 1000;\n\t\t}) | rpl::distinct_until_changed()\n\t) | rpl::on_next([=](int position, int length) {\n\t\t[_text setString:[NSString stringWithFormat:@\"%@ / %@\",\n\t\t\tFormatTime(position),\n\t\t\tFormatTime(length)]];\n\t\t*textLength = _text.length;\n\n\t\t[self display];\n\t}, _lifetime);\n\n\ttextLength->changes(\n\t) | rpl::on_next([=] {\n\t\tconst auto size = [_text sizeWithAttributes:Attributes()];\n\t\t_width = size.width + kPadding * 2;\n\t\t_height = size.height;\n\n\t\tif (self.image) {\n\t\t\t[self.image release];\n\t\t}\n\t\tself.image = [[NSImage alloc] initWithSize:NSMakeSize(\n\t\t\t_width,\n\t\t\tkCircleDiameter)];\n\t}, _lifetime);\n\n\treturn self;\n}\n\n- (void)drawRect:(NSRect)dirtyRect {\n\tif (!(_text && _text.length && _width && _height)) {\n\t\treturn;\n\t}\n\tconst auto size = [_text sizeWithAttributes:Attributes()];\n\tconst auto rect = CGRectMake(\n\t\t(_width - size.width) / 2,\n\t\t-(kCircleDiameter - _height) / 2,\n\t\t_width,\n\t\tkCircleDiameter);\n\t[_text drawInRect:rect withAttributes:Attributes()];\n}\n\n- (void)dealloc {\n\tif (self.image) {\n\t\t[self.image release];\n\t}\n\tif (_text) {\n\t\t[_text release];\n\t}\n\t[super dealloc];\n}\n\n@end // @implementation TrackPosition\n\nnamespace TouchBar {\n\nNSButton *CreateTouchBarButton(\n\t\t// const style::icon &icon,\n\t\tNSImage *image,\n\t\trpl::lifetime &lifetime,\n\t\tFn<void()> callback) {\n\tid block = [^{\n\t\tCore::Sandbox::Instance().customEnterFromEventLoop(callback);\n\t} copy];\n\n\tNSButton* button = [NSButton\n\t\tbuttonWithImage:image\n\t\ttarget:block\n\t\taction:@selector(invoke)];\n\tlifetime.add([=] {\n\t\t[block release];\n\t});\n\treturn button;\n}\n\nNSButton *CreateTouchBarButton(\n\tconst style::icon &icon,\n\trpl::lifetime &lifetime,\n\tFn<void()> callback) {\n\treturn CreateTouchBarButton(Icon(icon), lifetime, std::move(callback));\n}\n\nNSButton *CreateTouchBarButtonWithTwoStates(\n\t\tNSImage *icon1,\n\t\tNSImage *icon2,\n\t\trpl::lifetime &lifetime,\n\t\tFn<void(bool)> callback,\n\t\tbool firstState,\n\t\trpl::producer<bool> stateChanged) {\n\tNSButton* button = [NSButton\n\t\tbuttonWithImage:(firstState ? icon2 : icon1)\n\t\ttarget:nil\n\t\taction:nil];\n\n\tconst auto isFirstState = lifetime.make_state<bool>(firstState);\n\tid block = [^{\n\t\tconst auto state = *isFirstState;\n\t\tbutton.image = state ? icon1 : icon2;\n\t\t*isFirstState = !state;\n\t\tCore::Sandbox::Instance().customEnterFromEventLoop([=] {\n\t\t\tcallback(state);\n\t\t});\n\t} copy];\n\n\tbutton.target = block;\n\tbutton.action = @selector(invoke);\n\n\tstd::move(\n\t\tstateChanged\n\t) | rpl::on_next([=](bool isChangedToFirstState) {\n\t\tbutton.image = isChangedToFirstState ? icon1 : icon2;\n\t}, lifetime);\n\n\tlifetime.add([=] {\n\t\t[block release];\n\t});\n\treturn button;\n}\n\nNSButton *CreateTouchBarButtonWithTwoStates(\n\t\tconst style::icon &icon1,\n\t\tconst style::icon &icon2,\n\t\trpl::lifetime &lifetime,\n\t\tFn<void(bool)> callback,\n\t\tbool firstState,\n\t\trpl::producer<bool> stateChanged) {\n\treturn CreateTouchBarButtonWithTwoStates(\n\t\tIcon(icon1),\n\t\tIcon(icon2),\n\t\tlifetime,\n\t\tstd::move(callback),\n\t\tfirstState,\n\t\tstd::move(stateChanged));\n}\n\nNSSliderTouchBarItem *CreateTouchBarSlider(\n\t\tNSString *itemId,\n\t\trpl::lifetime &lifetime,\n\t\tFn<void(bool, double, double)> callback,\n\t\trpl::producer<Media::Player::TrackState> stateChanged) {\n\tconst auto lastDurationMs = lifetime.make_state<crl::time>(0);\n\n\tauto *seekBar = [[NSSliderTouchBarItem alloc] initWithIdentifier:itemId];\n\tseekBar.slider.minValue = 0.0f;\n\tseekBar.slider.maxValue = 1.0f;\n\tseekBar.customizationLabel = @\"Seek Bar\";\n\n\tid block = [^{\n\t\t// https://stackoverflow.com/a/45891017\n\t\tauto *event = [[NSApplication sharedApplication] currentEvent];\n\t\tconst auto touchUp = [event\n\t\t\ttouchesMatchingPhase:NSTouchPhaseEnded\n\t\t\tinView:nil].count > 0;\n\t\tCore::Sandbox::Instance().customEnterFromEventLoop([=] {\n\t\t\tcallback(touchUp, seekBar.slider.doubleValue, *lastDurationMs);\n\t\t});\n\t} copy];\n\n\tstd::move(\n\t\tstateChanged\n\t) | rpl::on_next([=](const Media::Player::TrackState &state) {\n\t\tconst auto stop = Media::Player::IsStoppedOrStopping(state.state);\n\t\tconst auto duration = double(stop ? 0 : state.length);\n\t\tauto slider = seekBar.slider;\n\t\tif (duration <= 0) {\n\t\t\tslider.enabled = false;\n\t\t\tslider.doubleValue = 0;\n\t\t} else {\n\t\t\tslider.enabled = true;\n\t\t\tif (!slider.highlighted) {\n\t\t\t\tconst auto pos = stop\n\t\t\t\t\t? 0\n\t\t\t\t\t: std::max(state.position, int64(0));\n\t\t\t\tslider.doubleValue = (pos / duration) * slider.maxValue;\n\t\t\t\t*lastDurationMs = duration;\n\t\t\t}\n\t\t}\n\t}, lifetime);\n\n\tseekBar.target = block;\n\tseekBar.action = @selector(invoke);\n\tlifetime.add([=] {\n\t\t[block release];\n\t});\n\n\treturn seekBar;\n}\n\nNSCustomTouchBarItem *CreateTouchBarTrackPosition(\n\t\tNSString *itemId,\n\t\trpl::producer<Media::Player::TrackState> stateChanged) {\n\tauto *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:itemId];\n\tauto *trackPosition = [[[TrackPosition alloc]\n\t\tinit:std::move(stateChanged)] autorelease];\n\n\titem.view = trackPosition;\n\titem.customizationLabel = @\"Track Position\";\n\treturn item;\n}\n\n} // namespace TouchBar\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_main.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#import <AppKit/NSTouchBar.h>\n\nnamespace Window {\nclass Controller;\n} // namespace Window\n\nnamespace TouchBar::Main {\n\nconst auto kPinnedPanelItemIdentifier = @\"pinnedPanel\";\nconst auto kPopoverInputItemIdentifier = @\"popoverInput\";\nconst auto kPopoverPickerItemIdentifier = @\"pickerButtons\";\n\n} // namespace TouchBar::Main\n\nAPI_AVAILABLE(macos(10.12.2))\n@interface TouchBarMain : NSTouchBar\n- (id)init:(not_null<Window::Controller*>)controller\n\ttouchBarSwitches:(rpl::producer<>)touchBarSwitches;\n@end\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_main.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/touchbar/mac_touchbar_main.h\"\n\n#include \"platform/mac/touchbar/items/mac_formatter_item.h\"\n#include \"platform/mac/touchbar/items/mac_pinned_chats_item.h\"\n#include \"platform/mac/touchbar/items/mac_scrubber_item.h\"\n#include \"platform/mac/touchbar/mac_touchbar_common.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n\n#import <AppKit/NSCustomTouchBarItem.h>\n\nusing namespace TouchBar::Main;\n\n#pragma mark - TouchBarMain\n\n@interface TouchBarMain()\n@end // @interface TouchBarMain\n\n@implementation TouchBarMain\n\n- (id)init:(not_null<Window::Controller*>)controller\n\t\ttouchBarSwitches:(rpl::producer<>)touchBarSwitches {\n\tself = [super init];\n\tif (!self) {\n\t\treturn self;\n\t}\n\n\tauto *pin = [[[NSCustomTouchBarItem alloc]\n\t\tinitWithIdentifier:kPinnedPanelItemIdentifier] autorelease];\n\tpin.view = [[[PinnedDialogsPanel alloc]\n\t\tinit:(&controller->sessionController()->session())\n\t\tdestroyEvent:std::move(touchBarSwitches)] autorelease];\n\n\tauto *sticker = [[[StickerEmojiPopover alloc]\n\t\tinit:controller\n\t\tidentifier:kPopoverPickerItemIdentifier] autorelease];\n\n\tauto *format = [[[TextFormatPopover alloc]\n\t\tinit:kPopoverInputItemIdentifier] autorelease];\n\n\tself.templateItems = [NSSet setWithArray:@[pin, sticker, format]];\n\n\treturn self;\n}\n\n@end // @implementation TouchBarMain\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#import <AppKit/NSTouchBar.h>\n\nnamespace Main {\nclass Domain;\n} // namespace Main\n\nnamespace Window {\nclass Controller;\n} // namespace Window\n\nnamespace Ui {\nstruct MarkdownEnabledState;\n} // namespace Ui\n\nAPI_AVAILABLE(macos(10.12.2))\n@interface RootTouchBar : NSTouchBar<NSTouchBarDelegate>\n- (id)init:(rpl::producer<Ui::MarkdownEnabledState>)markdownState\n\tcontroller:(not_null<Window::Controller*>)controller\n\tdomain:(not_null<Main::Domain*>)domain;\n@end\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_manager.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/touchbar/mac_touchbar_manager.h\"\n\n#include \"apiwrap.h\" // ApiWrap::updateStickers()\n#include \"core/application.h\"\n#include \"data/data_chat_participant_status.h\" // Data::CanSendAnyOf.\n#include \"data/data_forum_topic.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_stickers.h\" // Stickers::setsRef()\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"media/audio/media_audio_capture.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"platform/mac/touchbar/mac_touchbar_audio.h\"\n#include \"platform/mac/touchbar/mac_touchbar_common.h\"\n#include \"platform/mac/touchbar/mac_touchbar_main.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n\n#import <AppKit/NSGroupTouchBarItem.h>\n\nusing namespace TouchBar::Main;\n\nnamespace {\n\nconst auto kMainItemIdentifier = @\"touchbarMain\";\nconst auto kAudioItemIdentifier = @\"touchbarAudio\";\n\n} // namespace\n\n@interface GroupTouchBarItem : NSGroupTouchBarItem\n- (rpl::lifetime &)lifetime;\n@end // @interface GroupTouchBarItem\n\n@implementation GroupTouchBarItem {\n\trpl::lifetime _lifetime;\n}\n\n- (rpl::lifetime &)lifetime {\n\treturn _lifetime;\n}\n\n@end // GroupTouchBarItem\n\n#pragma mark - RootTouchBar\n\n@interface RootTouchBar()\n@end // @interface RootTouchBar\n\n@implementation RootTouchBar {\n\tMain::Session *_session;\n\tWindow::Controller *_controller;\n\n\trpl::variable<Ui::MarkdownEnabledState> _markdownState;\n\trpl::event_stream<> _touchBarSwitches;\n\trpl::lifetime _lifetime;\n}\n\n- (id)init:(rpl::producer<Ui::MarkdownEnabledState>)markdownState\n\t\tcontroller:(not_null<Window::Controller*>)controller\n\t\tdomain:(not_null<Main::Domain*>)domain {\n\tself = [super init];\n\tif (!self) {\n\t\treturn self;\n\t}\n\tself.delegate = self;\n\tTouchBar::CustomEnterToCocoaEventLoop([=] {\n\t\tself.defaultItemIdentifiers = @[];\n\t});\n\t_controller = controller;\n\t_markdownState = std::move(markdownState);\n\n\tauto sessionChanges = domain->activeSessionChanges(\n\t) | rpl::map([=](Main::Session *session) {\n\t\tif (session && session->data().stickers().setsRef().empty()) {\n\t\t\tsession->api().updateStickers();\n\t\t}\n\t\treturn session;\n\t});\n\n\tconst auto type = AudioMsgId::Type::Song;\n\tauto audioPlayer = rpl::merge(\n\t\tMedia::Player::instance()->stops(type) | rpl::map_to(false),\n\t\tMedia::Player::instance()->startsPlay(type) | rpl::map_to(true)\n\t);\n\n\tauto voiceRecording = ::Media::Capture::instance()->startedChanges();\n\n\trpl::combine(\n\t\tstd::move(sessionChanges),\n\t\trpl::single(false) | rpl::then(Core::App().passcodeLockChanges()),\n\t\trpl::single(false) | rpl::then(std::move(audioPlayer)),\n\t\trpl::single(false) | rpl::then(std::move(voiceRecording))\n\t) | rpl::on_next([=](\n\t\t\tMain::Session *session,\n\t\t\tbool lock,\n\t\t\tbool audio,\n\t\t\tbool recording) {\n\t\tTouchBar::CustomEnterToCocoaEventLoop([=] {\n\t\t\t_touchBarSwitches.fire({});\n\t\t\tif (!audio) {\n\t\t\t\tself.defaultItemIdentifiers = @[];\n\t\t\t}\n\t\t\tself.defaultItemIdentifiers = (lock || recording)\n\t\t\t\t? @[]\n\t\t\t\t: audio\n\t\t\t\t? @[kAudioItemIdentifier]\n\t\t\t\t: session\n\t\t\t\t? @[kMainItemIdentifier]\n\t\t\t\t: @[];\n\t\t});\n\t}, _lifetime);\n\n\treturn self;\n}\n\n- (NSTouchBarItem*)touchBar:(NSTouchBar*)touchBar\n\t\tmakeItemForIdentifier:(NSTouchBarItemIdentifier)itemId {\n\tif (!touchBar || !_controller->sessionController()) {\n\t\treturn nil;\n\t}\n\tconst auto isEqual = [&](NSString *string) {\n\t\treturn [itemId isEqualToString:string];\n\t};\n\n\tif (isEqual(kMainItemIdentifier)) {\n\t\tauto *item = [[GroupTouchBarItem alloc] initWithIdentifier:itemId];\n\t\titem.groupTouchBar\n\t\t\t= [[[TouchBarMain alloc]\n\t\t\t\tinit:_controller\n\t\t\t\ttouchBarSwitches:_touchBarSwitches.events()] autorelease];\n\t\trpl::combine(\n\t\t\t_markdownState.value(),\n\t\t\t_controller->sessionController()->activeChatValue(\n\t\t\t) | rpl::map([](Dialogs::Key k) {\n\t\t\t\tconst auto topic = k.topic();\n\t\t\t\tconst auto peer = k.peer();\n\t\t\t\tconst auto rights = ChatRestriction::SendStickers\n\t\t\t\t\t| ChatRestriction::SendOther;\n\t\t\t\treturn topic\n\t\t\t\t\t? Data::CanSendAnyOf(topic, rights)\n\t\t\t\t\t: (peer && Data::CanSendAnyOf(peer, rights));\n\t\t\t}) | rpl::distinct_until_changed()\n\t\t) | rpl::on_next([=](\n\t\t\t\tUi::MarkdownEnabledState state,\n\t\t\t\tbool hasActiveChat) {\n\t\t\titem.groupTouchBar.defaultItemIdentifiers = @[\n\t\t\t\tkPinnedPanelItemIdentifier,\n\t\t\t\t(!state.disabled()\n\t\t\t\t\t? kPopoverInputItemIdentifier\n\t\t\t\t\t: hasActiveChat\n\t\t\t\t\t? kPopoverPickerItemIdentifier\n\t\t\t\t\t: @\"\")];\n\t\t}, [item lifetime]);\n\n\t\treturn [item autorelease];\n\t} else if (isEqual(kAudioItemIdentifier)) {\n\t\tauto *item = [[GroupTouchBarItem alloc] initWithIdentifier:itemId];\n\t\tauto *touchBar = [[[TouchBarAudioPlayer alloc] init]\n\t\t\tautorelease];\n\t\titem.groupTouchBar = touchBar;\n\t\t[touchBar closeRequests] | rpl::on_next([=] {\n\t\t\tMedia::Player::instance()->stopAndClose();\n\t\t}, [item lifetime]);\n\t\treturn [item autorelease];\n\t}\n\n\treturn nil;\n}\n\n@end // @implementation RootTouchBar\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_media_view.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/view/media_view_playback_controls.h\"\n#include \"media/view/media_view_overlay_widget.h\"\n\nnamespace TouchBar {\n\nvoid SetupMediaViewTouchBar(\n\tWId winId,\n\tnot_null<Media::View::PlaybackControls::Delegate*> controlsDelegate,\n\trpl::producer<Media::Player::TrackState> trackState,\n\trpl::producer<Media::View::OverlayWidget::TouchBarItemType> display,\n\trpl::producer<bool> fullscreenToggled);\n\n} // namespace TouchBar\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_media_view.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/touchbar/mac_touchbar_media_view.h\"\n\n#include \"media/audio/media_audio.h\"\n#include \"platform/mac/touchbar/mac_touchbar_common.h\"\n#include \"platform/mac/touchbar/mac_touchbar_controls.h\"\n#include \"styles/style_media_player.h\"\n#include \"styles/style_media_view.h\"\n\n#import <AppKit/NSButton.h>\n#import <AppKit/NSCustomTouchBarItem.h>\n#import <AppKit/NSTouchBar.h>\n\nusing namespace TouchBar;\nusing Delegate = Media::View::PlaybackControls::Delegate;\nusing ItemType = Media::View::OverlayWidget::TouchBarItemType;\n\nnamespace {\n\ninline NSTouchBarItemIdentifier Format(NSString *s) {\n\treturn [NSString stringWithFormat:@\"button.%@\", s];\n}\n\nconst auto kPlayItemIdentifier = Format(@\"playPause\");\nconst auto kRotateItemIdentifier = Format(@\"rotate\");\nconst auto kFullscreenItemIdentifier = Format(@\"fullscreen\");\nconst auto kPipItemIdentifier = Format(@\"pip\");\nconst auto kTrackItemIdentifier = @\"trackPosition\";\nconst auto kSeekItemIdentifier = @\"seekBar\";\n\n}\n\n#pragma mark - MediaViewTouchBar\n\n@interface MediaViewTouchBar : NSTouchBar\n- (id)init:(not_null<Delegate*>)controlsDelegate\n\ttrackState:(rpl::producer<Media::Player::TrackState>)trackState\n\tdisplay:(rpl::producer<ItemType>)display\n\tfullscreenToggled:(rpl::producer<bool>)fullscreenToggled;\n@end\n\n@implementation MediaViewTouchBar {\n\trpl::lifetime _lifetime;\n}\n\n- (id)init:(not_null<Delegate*>)controlsDelegate\n\t\ttrackState:(rpl::producer<Media::Player::TrackState>)trackState\n\t\tdisplay:(rpl::producer<ItemType>)display\n\t\tfullscreenToggled:(rpl::producer<bool>)fullscreenToggled {\n\tself = [super init];\n\tif (!self) {\n\t\treturn self;\n\t}\n\tconst auto allocate = [](NSTouchBarItemIdentifier i) {\n\t\treturn [[NSCustomTouchBarItem alloc] initWithIdentifier:i];\n\t};\n\n\tauto *playPause = allocate(kPlayItemIdentifier);\n\t{\n\t\tauto *button = CreateTouchBarButtonWithTwoStates(\n\t\t\tst::touchBarIconPlayerPause,\n\t\t\tst::touchBarIconPlayerPlay,\n\t\t\t_lifetime,\n\t\t\t[=](bool value) {\n\t\t\t\tvalue\n\t\t\t\t\t? controlsDelegate->playbackControlsPlay()\n\t\t\t\t\t: controlsDelegate->playbackControlsPause();\n\t\t\t},\n\t\t\tfalse,\n\t\t\trpl::duplicate(\n\t\t\t\ttrackState\n\t\t\t) | rpl::map([](const auto &state) {\n\t\t\t\treturn (state.state == Media::Player::State::Playing);\n\t\t\t}) | rpl::distinct_until_changed());\n\t\tplayPause.view = button;\n\t\tplayPause.customizationLabel = @\"Play/Pause\";\n\t}\n\n\tauto *rotate = allocate(kRotateItemIdentifier);\n\t{\n\t\tauto *button = CreateTouchBarButton(\n\t\t\t[NSImage imageNamed:NSImageNameTouchBarRotateLeftTemplate],\n\t\t\t_lifetime,\n\t\t\t[=] { controlsDelegate->playbackControlsRotate(); });\n\t\trotate.view = button;\n\t\trotate.customizationLabel = @\"Rotate\";\n\t}\n\n\tauto *fullscreen = allocate(kFullscreenItemIdentifier);\n\t{\n\t\tauto *button = CreateTouchBarButtonWithTwoStates(\n\t\t\t[NSImage imageNamed:NSImageNameTouchBarExitFullScreenTemplate],\n\t\t\t[NSImage imageNamed:NSImageNameTouchBarEnterFullScreenTemplate],\n\t\t\t_lifetime,\n\t\t\t[=](bool value) {\n\t\t\t\tvalue\n\t\t\t\t\t? controlsDelegate->playbackControlsFromFullScreen()\n\t\t\t\t\t: controlsDelegate->playbackControlsToFullScreen();\n\t\t\t},\n\t\t\ttrue,\n\t\t\tstd::move(fullscreenToggled));\n\t\tfullscreen.view = button;\n\t\tfullscreen.customizationLabel = @\"Fullscreen\";\n\t}\n\n\tauto *pip = allocate(kPipItemIdentifier);\n\t{\n\t\tauto *button = TouchBar::CreateTouchBarButton(\n\t\t\tCreateNSImageFromStyleIcon(\n\t\t\t\tst::mediaviewPipButton.icon,\n\t\t\t\tkCircleDiameter / 4 * 3),\n\t\t\t_lifetime,\n\t\t\t[=] { controlsDelegate->playbackControlsToPictureInPicture(); });\n\t\tpip.view = button;\n\t\tpip.customizationLabel = @\"Picture-in-Picture\";\n\t}\n\n\tauto *trackPosition = CreateTouchBarTrackPosition(\n\t\tkTrackItemIdentifier,\n\t\trpl::duplicate(trackState));\n\n\tauto *seekBar = TouchBar::CreateTouchBarSlider(\n\t\tkSeekItemIdentifier,\n\t\t_lifetime,\n\t\t[=](bool touchUp, double value, double duration) {\n\t\t\tconst auto progress = value * duration;\n\t\t\ttouchUp\n\t\t\t\t? controlsDelegate->playbackControlsSeekFinished(progress)\n\t\t\t\t: controlsDelegate->playbackControlsSeekProgress(progress);\n\t\t},\n\t\tstd::move(trackState));\n\n\tself.templateItems = [NSSet setWithArray:@[\n\t\tplayPause,\n\t\trotate,\n\t\tfullscreen,\n\t\tpip,\n\t\tseekBar,\n\t\ttrackPosition]];\n\n\tconst auto items = [](ItemType type) {\n\t\tswitch (type) {\n\t\tcase ItemType::Photo: return @[kRotateItemIdentifier];\n\t\tcase ItemType::Video: return @[\n\t\t\tkRotateItemIdentifier,\n\t\t\tkFullscreenItemIdentifier,\n\t\t\tkPipItemIdentifier,\n\t\t\tkPlayItemIdentifier,\n\t\t\tkSeekItemIdentifier,\n\t\t\tkTrackItemIdentifier];\n\t\tdefault: return @[];\n\t\t};\n\t};\n\n\tstd::move(\n\t\tdisplay\n\t) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](ItemType type) {\n\t\tTouchBar::CustomEnterToCocoaEventLoop([=] {\n\t\t\tself.defaultItemIdentifiers = items(type);\n\t\t});\n\t}, _lifetime);\n\n\treturn self;\n}\n\n@end // @implementation MediaViewTouchBar\n\nnamespace TouchBar {\n\nvoid SetupMediaViewTouchBar(\n\t\tWId winId,\n\t\tnot_null<Delegate*> controlsDelegate,\n\t\trpl::producer<Media::Player::TrackState> trackState,\n\t\trpl::producer<ItemType> display,\n\t\trpl::producer<bool> fullscreenToggled) {\n\tauto *window = [reinterpret_cast<NSView*>(winId) window];\n\tCustomEnterToCocoaEventLoop([=] {\n\t\t[window setTouchBar:[[[MediaViewTouchBar alloc]\n\t\t\tinit:std::move(controlsDelegate)\n\t\t\ttrackState:std::move(trackState)\n\t\t\tdisplay:std::move(display)\n\t\t\tfullscreenToggled:std::move(fullscreenToggled)\n\t\t] autorelease]];\n\t});\n}\n\n} // namespace TouchBar\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/translate_provider_mac.h",
    "content": "// This file is part of Telegram Desktop,\n// the official desktop application for the Telegram messaging service.\n//\n// For license and copyright information please follow this link:\n// https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n//\n#pragma once\n\n#include \"translate_provider.h\"\n\nnamespace Platform {\n\n[[nodiscard]] std::unique_ptr<Ui::TranslateProvider>\nCreateTranslateProvider();\n\n[[nodiscard]] bool IsTranslateProviderAvailable();\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/translate_provider_mac.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/translate_provider_mac.h\"\n\n#ifndef TDESKTOP_DISABLE_SWIFT6\n\n#include \"base/weak_ptr.h\"\n#include \"spellcheck/platform/platform_language.h\"\n#include \"translate_provider_mac_swift_bridge.h\"\n\nnamespace Platform {\nnamespace {\n\n[[nodiscard]] Ui::TranslateProviderError ParseErrorCode(\n\t\tconst char *errorUtf8) {\n\treturn !std::strcmp(errorUtf8, \"local-language-pack-missing\")\n\t\t? Ui::TranslateProviderError::LocalLanguagePackMissing\n\t\t: Ui::TranslateProviderError::Unknown;\n}\n\nclass TranslateProvider final : public Ui::TranslateProvider\n, public base::has_weak_ptr {\npublic:\n\t[[nodiscard]] bool supportsMessageId() const override {\n\t\treturn false;\n\t}\n\n\tvoid request(\n\t\t\tUi::TranslateProviderRequest request,\n\t\t\tLanguageId to,\n\t\t\tFn<void(Ui::TranslateProviderResult)> done) override {\n\t\tif (request.text.text.isEmpty()) {\n\t\t\tdone(Ui::TranslateProviderResult{\n\t\t\t\t.error = Ui::TranslateProviderError::Unknown,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tconst auto text = request.text.text.toUtf8();\n\t\tconst auto target = to.twoLetterCode().toUtf8();\n\t\tif (target.isEmpty()) {\n\t\t\tdone(Ui::TranslateProviderResult{\n\t\t\t\t.error = Ui::TranslateProviderError::Unknown,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tstruct CallbackContext {\n\t\t\tbase::weak_ptr<TranslateProvider> provider;\n\t\t\tFn<void(Ui::TranslateProviderResult)> done;\n\t\t};\n\t\tauto ownedContext = std::make_unique<CallbackContext>(CallbackContext{\n\t\t\t.provider = base::make_weak(this),\n\t\t\t.done = std::move(done),\n\t\t});\n\t\tTranslateProviderMacSwiftTranslate(\n\t\t\ttext.constData(),\n\t\t\ttarget.constData(),\n\t\t\townedContext.release(),\n\t\t\t[](void *context, const char *resultUtf8, const char *errorUtf8) {\n\t\t\t\tauto guard = std::unique_ptr<CallbackContext>(\n\t\t\t\t\tstatic_cast<CallbackContext*>(context));\n\t\t\t\tauto done = std::move(guard->done);\n\t\t\t\tconst auto isAlive = (guard->provider.get() != nullptr);\n\t\t\t\tauto result = Ui::TranslateProviderResult();\n\t\t\t\tif (resultUtf8 != nullptr) {\n\t\t\t\t\tresult.text = TextWithEntities{\n\t\t\t\t\t\t.text = QString::fromUtf8(resultUtf8),\n\t\t\t\t\t};\n\t\t\t\t\tstd::free(const_cast<char*>(resultUtf8));\n\t\t\t\t}\n\t\t\t\tif (errorUtf8 != nullptr) {\n\t\t\t\t\tresult.error = ParseErrorCode(errorUtf8);\n\t\t\t\t\tstd::free(const_cast<char*>(errorUtf8));\n\t\t\t\t} else if (!result.text.has_value()) {\n\t\t\t\t\tresult.error = Ui::TranslateProviderError::Unknown;\n\t\t\t\t}\n\t\t\t\tif (!isAlive) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcrl::on_main([=,\n\t\t\t\t\t\tdone = std::move(done),\n\t\t\t\t\t\tresult = std::move(result)] {\n\t\t\t\t\tdone(std::move(result));\n\t\t\t\t});\n\t\t\t});\n\t}\n\n};\n\n} // namespace\n\nstd::unique_ptr<Ui::TranslateProvider> CreateTranslateProvider() {\n\tif (TranslateProviderMacSwiftIsAvailable()) {\n\t\treturn std::make_unique<TranslateProvider>();\n\t}\n\treturn nullptr;\n}\n\nbool IsTranslateProviderAvailable() {\n\treturn TranslateProviderMacSwiftIsAvailable();\n}\n\n#else // TDESKTOP_DISABLE_SWIFT6\n\n// Local on-device translation disabled (no Swift 6).\nnamespace Platform {\n\nstd::unique_ptr<Ui::TranslateProvider> CreateTranslateProvider() {\n\treturn nullptr;\n}\n\nbool IsTranslateProviderAvailable() {\n\treturn false;\n}\n\n#endif // TDESKTOP_DISABLE_SWIFT6\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/tray_mac.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_tray.h\"\n\n#include \"base/unique_qptr.h\"\n\nclass QMenu;\nclass QIcon;\n\nnamespace Ui {\nclass DynamicImage;\n} // namespace Ui\n\nnamespace Platform {\n\nclass NativeIcon;\n\nclass Tray final {\npublic:\n\tTray();\n\t~Tray();\n\n\t[[nodiscard]] rpl::producer<> aboutToShowRequests() const;\n\t[[nodiscard]] rpl::producer<> showFromTrayRequests() const;\n\t[[nodiscard]] rpl::producer<> hideToTrayRequests() const;\n\t[[nodiscard]] rpl::producer<> iconClicks() const;\n\n\t[[nodiscard]] bool hasIcon() const;\n\n\tvoid createIcon();\n\tvoid destroyIcon();\n\n\tvoid updateIcon();\n\n\tvoid createMenu();\n\tvoid destroyMenu();\n\n\tvoid addAction(rpl::producer<QString> text, Fn<void()> &&callback);\n\tvoid addAction(\n\t\trpl::producer<QString> text,\n\t\tFn<void()> &&callback,\n\t\tconst QIcon &icon);\n\tvoid addAction(\n\t\trpl::producer<QString> text,\n\t\tFn<void()> &&callback,\n\t\tstd::shared_ptr<Ui::DynamicImage> icon,\n\t\tint size);\n\tvoid addSeparator();\n\n\tvoid showTrayMessage() const;\n\t[[nodiscard]] bool hasTrayMessageSupport() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tstd::unique_ptr<NativeIcon> _nativeIcon;\n\tbase::unique_qptr<QMenu> _menu;\n\n\trpl::event_stream<> _showFromTrayRequests;\n\n\trpl::lifetime _actionsLifetime;\n\trpl::lifetime _lifetime;\n\n};\n\ninline bool HasMonochromeSetting() {\n\treturn false;\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/tray_mac.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/mac/tray_mac.h\"\n\n#include \"base/platform/mac/base_utilities_mac.h\"\n#include \"core/application.h\"\n#include \"core/sandbox.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"ui/painter.h\"\n#include \"ui/dynamic_image.h\"\n#include \"styles/style_window.h\"\n\n#include <QtWidgets/QMenu>\n#include <QtGui/QIcon>\n\n#import <AppKit/NSMenu.h>\n#import <AppKit/NSStatusItem.h>\n\n@interface CommonDelegate : NSObject<NSMenuDelegate> {\n}\n\n- (void) menuDidClose:(NSMenu *)menu;\n- (void) menuWillOpen:(NSMenu *)menu;\n- (void) observeValueForKeyPath:(NSString *)keyPath\n\tofObject:(id)object\n\tchange:(NSDictionary<NSKeyValueChangeKey, id> *)change\n\tcontext:(void *)context;\n\n- (rpl::producer<>) closes;\n- (rpl::producer<>) aboutToShowRequests;\n- (rpl::producer<>) appearanceChanges;\n\n@end // @interface CommonDelegate\n\n@implementation CommonDelegate {\n\trpl::event_stream<> _closes;\n\trpl::event_stream<> _aboutToShowRequests;\n\trpl::event_stream<> _appearanceChanges;\n}\n\n- (void) menuDidClose:(NSMenu *)menu {\n\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t_closes.fire({});\n\t});\n}\n\n- (void) menuWillOpen:(NSMenu *)menu {\n\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t_aboutToShowRequests.fire({});\n\t});\n}\n\n// Thanks https://stackoverflow.com/a/64525038\n- (void) observeValueForKeyPath:(NSString *)keyPath\n\t\tofObject:(id)object\n\t\tchange:(NSDictionary<NSKeyValueChangeKey, id> *)change\n\t\tcontext:(void *)context {\n\tif ([keyPath isEqualToString:@\"button.effectiveAppearance\"]) {\n\t\t_appearanceChanges.fire({});\n\t}\n}\n\n- (rpl::producer<>) closes {\n\treturn _closes.events();\n}\n\n- (rpl::producer<>) aboutToShowRequests {\n\treturn _aboutToShowRequests.events();\n}\n\n- (rpl::producer<>) appearanceChanges {\n\treturn _appearanceChanges.events();\n}\n\n@end // @implementation MenuDelegate\n\nnamespace Platform {\n\nnamespace {\n\nenum class TrayClickType {\n\tLeft,\n\tRight,\n};\n\n[[nodiscard]] bool IsAnyActiveForTrayMenu() {\n\tfor (const NSWindow *w in [[NSApplication sharedApplication] windows]) {\n\t\tif (w.isKeyWindow) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n[[nodiscard]] QImage TrayIconBack(bool darkMode) {\n\tstatic const auto WithColor = [](QColor color) {\n\t\treturn st::macTrayIcon.instance(color, 100);\n\t};\n\tstatic const auto DarkModeResult = WithColor({ 255, 255, 255 });\n\tstatic const auto LightModeResult = WithColor({ 0, 0, 0, 180 });\n\tauto result = darkMode ? DarkModeResult : LightModeResult;\n\tresult.detach();\n\treturn result;\n}\n\nvoid PlaceCounter(\n\t\tQImage &img,\n\t\tint size,\n\t\tint count,\n\t\tstyle::color bg,\n\t\tstyle::color color) {\n\tif (!count) {\n\t\treturn;\n\t}\n\tconst auto savedRatio = img.devicePixelRatio();\n\timg.setDevicePixelRatio(1.);\n\n\t{\n\t\tPainter p(&img);\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tconst auto cnt = (count < 100)\n\t\t\t? QString(\"%1\").arg(count)\n\t\t\t: QString(\"..%1\").arg(count % 100, 2, 10, QChar('0'));\n\t\tconst auto cntSize = cnt.size();\n\n\t\tp.setBrush(bg);\n\t\tp.setPen(Qt::NoPen);\n\t\tint32 fontSize, skip;\n\t\tif (size == 22) {\n\t\t\tskip = 1;\n\t\t\tfontSize = 8;\n\t\t} else {\n\t\t\tskip = 2;\n\t\t\tfontSize = 16;\n\t\t}\n\t\tstyle::font f(fontSize, 0, 0);\n\t\tint32 w = f->width(cnt), d, r;\n\t\tif (size == 22) {\n\t\t\td = (cntSize < 2) ? 3 : 2;\n\t\t\tr = (cntSize < 2) ? 6 : 5;\n\t\t} else {\n\t\t\td = (cntSize < 2) ? 6 : 5;\n\t\t\tr = (cntSize < 2) ? 9 : 11;\n\t\t}\n\t\tp.drawRoundedRect(\n\t\t\tQRect(\n\t\t\t\tsize - w - d * 2 - skip,\n\t\t\t\tsize - f->height - skip,\n\t\t\t\tw + d * 2,\n\t\t\t\tf->height),\n\t\t\tr,\n\t\t\tr);\n\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tp.setFont(f);\n\t\tp.setPen(color);\n\t\tp.drawText(\n\t\t\tsize - w - d - skip,\n\t\t\tsize - f->height + f->ascent - skip,\n\t\t\tcnt);\n\t}\n\timg.setDevicePixelRatio(savedRatio);\n}\n\nvoid UpdateIcon(const NSStatusItem *status) {\n\tif (!status) {\n\t\treturn;\n\t}\n\n\tconst auto appearance = status.button.effectiveAppearance;\n\tconst auto darkMode = [[appearance.name lowercaseString]\n\t\tcontainsString:@\"dark\"];\n\n\t// The recommended maximum title bar icon height is 18 points\n\t// (device independent pixels). The menu height on past and\n\t// current OS X versions is 22 points. Provide some future-proofing\n\t// by deriving the icon height from the menu height.\n\tconst int padding = 0;\n\tconst int menuHeight = NSStatusBar.systemStatusBar.thickness;\n\t// [[status.button window] backingScaleFactor];\n\tconst int maxImageHeight = (menuHeight - padding)\n\t\t* style::DevicePixelRatio();\n\n\t// Select pixmap based on the device pixel height. Ideally we would use\n\t// the devicePixelRatio of the target screen, but that value is not\n\t// known until draw time. Use qApp->devicePixelRatio, which returns the\n\t// devicePixelRatio for the \"best\" screen on the system.\n\n\tconst auto side = 22 * style::DevicePixelRatio();\n\tconst auto selectedSize = QSize(side, side);\n\n\tauto result = TrayIconBack(darkMode);\n\tauto resultActive = result;\n\tresultActive.detach();\n\n\tconst auto counter = Core::App().unreadBadge();\n\tconst auto muted = Core::App().unreadBadgeMuted();\n\n\tconst auto &bg = (muted ? st::trayCounterBgMute : st::trayCounterBg);\n\tconst auto &fg = st::trayCounterFg;\n\tconst auto &fgInvert = st::trayCounterFgMacInvert;\n\tconst auto &bgInvert = st::trayCounterBgMacInvert;\n\n\tconst auto &resultFg = !darkMode ? fg : muted ? fgInvert : fg;\n\tPlaceCounter(result, side, counter, bg, resultFg);\n\tPlaceCounter(resultActive, side, counter, bgInvert, fgInvert);\n\n\t// Scale large pixmaps to fit the available menu bar area.\n\tif (result.height() > maxImageHeight) {\n\t\tresult = result.scaledToHeight(\n\t\t\tmaxImageHeight,\n\t\t\tQt::SmoothTransformation);\n\t}\n\tif (resultActive.height() > maxImageHeight) {\n\t\tresultActive = resultActive.scaledToHeight(\n\t\t\tmaxImageHeight,\n\t\t\tQt::SmoothTransformation);\n\t}\n\n\tstatus.button.image = Q2NSImage(result);\n\tstatus.button.alternateImage = Q2NSImage(resultActive);\n\tstatus.button.imageScaling = NSImageScaleProportionallyDown;\n}\n\n} // namespace\n\nclass NativeIcon final {\npublic:\n\tNativeIcon();\n\t~NativeIcon();\n\n\tvoid updateIcon();\n\tvoid showMenu(not_null<QMenu*> menu);\n\tvoid deactivateButton();\n\n\t[[nodiscard]] rpl::producer<TrayClickType> clicks() const;\n\t[[nodiscard]] rpl::producer<> aboutToShowRequests() const;\n\nprivate:\n\tCommonDelegate *_delegate;\n\tNSStatusItem *_status;\n\n\trpl::event_stream<TrayClickType> _clicks;\n\n\trpl::lifetime _lifetime;\n\n};\n\nNativeIcon::NativeIcon()\n: _delegate([[CommonDelegate alloc] init])\n, _status([\n\t[NSStatusBar.systemStatusBar\n\t\tstatusItemWithLength:NSSquareStatusItemLength] retain]) {\n\n\t[_status\n\t\taddObserver:_delegate\n\t\tforKeyPath:@\"button.effectiveAppearance\"\n\t\toptions:0\n\t\t\t| NSKeyValueObservingOptionNew\n\t\t\t| NSKeyValueObservingOptionInitial\n\t\tcontext:nil];\n\n\t[_delegate closes] | rpl::on_next([=] {\n\t\t_status.menu = nil;\n\t}, _lifetime);\n\n\t[_delegate appearanceChanges] | rpl::on_next([=] {\n\t\tupdateIcon();\n\t}, _lifetime);\n\n\tconst auto masks = NSEventMaskLeftMouseDown\n\t\t| NSEventMaskLeftMouseUp\n\t\t| NSEventMaskRightMouseDown\n\t\t| NSEventMaskRightMouseUp\n\t\t| NSEventMaskOtherMouseUp;\n\t[_status.button sendActionOn:masks];\n\n\tid buttonCallback = [^{\n\t\tconst auto event = NSApp.currentEvent;\n\t\tconst auto type = event.type;\n\n\t\tif (type == NSEventTypeLeftMouseDown) {\n\t\t\tCore::Sandbox::Instance().customEnterFromEventLoop([=] {\n\t\t\t\t_clicks.fire(TrayClickType::Left);\n\t\t\t});\n\t\t} else if (type == NSEventTypeRightMouseDown\n\t\t\t|| type == NSEventTypeRightMouseUp) {\n\t\t\tCore::Sandbox::Instance().customEnterFromEventLoop([=] {\n\t\t\t\t_clicks.fire(TrayClickType::Right);\n\t\t\t});\n\t\t}\n\t} copy];\n\n\t_lifetime.add([=] {\n\t\t[buttonCallback release];\n\t});\n\n\t_status.button.target = buttonCallback;\n\t_status.button.action = @selector(invoke);\n\t_status.button.toolTip = Q2NSString(AppNameF.utf16());\n}\n\nNativeIcon::~NativeIcon() {\n\t[_status\n\t\tremoveObserver:_delegate\n\t\tforKeyPath:@\"button.effectiveAppearance\"];\n\t[NSStatusBar.systemStatusBar removeStatusItem:_status];\n\n\t[_status release];\n\t[_delegate release];\n}\n\nvoid NativeIcon::updateIcon() {\n\tUpdateIcon(_status);\n}\n\nvoid NativeIcon::showMenu(not_null<QMenu*> menu) {\n\t_status.menu = menu->toNSMenu();\n\t_status.menu.delegate = _delegate;\n\t[_status.button performClick:nil];\n}\n\nvoid NativeIcon::deactivateButton() {\n\t[_status.button highlight:false];\n}\n\nrpl::producer<TrayClickType> NativeIcon::clicks() const {\n\treturn _clicks.events();\n}\n\nrpl::producer<> NativeIcon::aboutToShowRequests() const {\n\treturn [_delegate aboutToShowRequests];\n}\n\nTray::Tray() {\n}\n\nvoid Tray::createIcon() {\n\tif (!_nativeIcon) {\n\t\t_nativeIcon = std::make_unique<NativeIcon>();\n\t\t// On macOS we are activating the window on click\n\t\t// instead of showing the menu, when the window is not activated.\n\t\t_nativeIcon->clicks(\n\t\t) | rpl::on_next([=](TrayClickType type) {\n\t\t\tif (!_menu) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (type == TrayClickType::Right) {\n\t\t\t\t_nativeIcon->showMenu(_menu.get());\n\t\t\t} else if (IsAnyActiveForTrayMenu()) {\n\t\t\t\t_nativeIcon->showMenu(_menu.get());\n\t\t\t} else {\n\t\t\t\t_nativeIcon->deactivateButton();\n\t\t\t\t_showFromTrayRequests.fire({});\n\t\t\t}\n\t\t}, _lifetime);\n\t}\n\tupdateIcon();\n}\n\nvoid Tray::destroyIcon() {\n\t_nativeIcon = nullptr;\n}\n\nvoid Tray::updateIcon() {\n\tif (_nativeIcon) {\n\t\t_nativeIcon->updateIcon();\n\t}\n}\n\nvoid Tray::createMenu() {\n\tif (!_menu) {\n\t\t_menu = base::make_unique_q<QMenu>(nullptr);\n\t}\n}\n\nvoid Tray::destroyMenu() {\n\tif (_menu) {\n\t\t_menu->clear();\n\t}\n\t_actionsLifetime.destroy();\n}\n\nvoid Tray::addAction(rpl::producer<QString> text, Fn<void()> &&callback) {\n\taddAction(std::move(text), std::move(callback), QIcon());\n}\n\nvoid Tray::addAction(\n\t\trpl::producer<QString> text,\n\t\tFn<void()> &&callback,\n\t\tconst QIcon &icon) {\n\tif (!_menu) {\n\t\treturn;\n\t}\n\n\tconst auto action = _menu->addAction(QString(), std::move(callback));\n\taction->setIcon(icon);\n\tstd::move(\n\t\ttext\n\t) | rpl::on_next([=](const QString &text) {\n\t\taction->setText(text);\n\t}, _actionsLifetime);\n}\n\nvoid Tray::addAction(\n\t\trpl::producer<QString> text,\n\t\tFn<void()> &&callback,\n\t\tstd::shared_ptr<Ui::DynamicImage> icon,\n\t\tint size) {\n\tif (!_menu) {\n\t\treturn;\n\t}\n\n\tconst auto action = _menu->addAction(QString(), std::move(callback));\n\tif (icon) {\n\t\tconst auto updateIcon = crl::guard(action, [=] {\n\t\t\taction->setIcon(QIcon(QPixmap::fromImage(icon->image(size))));\n\t\t});\n\t\ticon->subscribeToUpdates([=] {\n\t\t\tCore::Sandbox::Instance().customEnterFromEventLoop([=] {\n\t\t\t\tupdateIcon();\n\t\t\t});\n\t\t});\n\t\tupdateIcon();\n\t\t_actionsLifetime.add([icon = std::move(icon)] {\n\t\t\ticon->subscribeToUpdates(nullptr);\n\t\t});\n\t}\n\tstd::move(\n\t\ttext\n\t) | rpl::on_next([=](const QString &text) {\n\t\taction->setText(text);\n\t}, _actionsLifetime);\n}\n\nvoid Tray::addSeparator() {\n\tif (_menu) {\n\t\t_menu->addSeparator();\n\t}\n}\n\nvoid Tray::showTrayMessage() const {\n}\n\nbool Tray::hasTrayMessageSupport() const {\n\treturn false;\n}\n\nrpl::producer<> Tray::aboutToShowRequests() const {\n\treturn _nativeIcon\n\t\t? _nativeIcon->aboutToShowRequests()\n\t\t: rpl::never<>();\n}\n\nrpl::producer<> Tray::showFromTrayRequests() const {\n\treturn _showFromTrayRequests.events();\n}\n\nrpl::producer<> Tray::hideToTrayRequests() const {\n\treturn rpl::never<>();\n}\n\nrpl::producer<> Tray::iconClicks() const {\n\treturn rpl::never<>();\n}\n\nbool Tray::hasIcon() const {\n\treturn _nativeIcon != nullptr;\n}\n\nrpl::lifetime &Tray::lifetime() {\n\treturn _lifetime;\n}\n\nTray::~Tray() = default;\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/webauthn_mac.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\n#include \"platform/platform_webauthn.h\"\n\n#if 0\n\n#include \"data/data_passkey_deserialize.h\"\n#include \"base/options.h\"\n\n#import <AuthenticationServices/AuthenticationServices.h>\n#import <Foundation/Foundation.h>\n\n@interface WebAuthnDelegate : NSObject<\n\tASAuthorizationControllerDelegate,\n\tASAuthorizationControllerPresentationContextProviding>\n@property (nonatomic, copy) void (^completionRegister)(\n\tconst Platform::WebAuthn::RegisterResult&);\n@property (nonatomic, copy) void (^completionLogin)(\n\tconst Platform::WebAuthn::LoginResult&);\n@property (nonatomic, strong) ASAuthorizationController *controller API_AVAILABLE(macos(10.15));\n@property (nonatomic, strong)\n\tASAuthorizationPlatformPublicKeyCredentialProvider *provider API_AVAILABLE(macos(12.0));\n@end\n\n@implementation WebAuthnDelegate\n\n- (void)authorizationController:(ASAuthorizationController *)controller\n\tdidCompleteWithAuthorization:(ASAuthorization *)authorization\n\t\tAPI_AVAILABLE(macos(12.0)) {\n\tif (self.completionRegister) {\n\t\tif ([authorization.credential conformsToProtocol:\n\t\t\t\t@protocol(ASAuthorizationPublicKeyCredentialRegistration)]) {\n\t\t\tauto credential\n\t\t\t\t= (id<ASAuthorizationPublicKeyCredentialRegistration>)\n\t\t\t\t\tauthorization.credential;\n\t\t\tauto result = Platform::WebAuthn::RegisterResult();\n\t\t\tresult.success = true;\n\t\t\tresult.credentialId = QByteArray::fromNSData(\n\t\t\t\tcredential.credentialID);\n\t\t\tresult.attestationObject = QByteArray::fromNSData(\n\t\t\t\tcredential.rawAttestationObject);\n\t\t\tresult.clientDataJSON = QByteArray::fromNSData(\n\t\t\t\tcredential.rawClientDataJSON);\n\t\t\tself.completionRegister(result);\n\t\t\tself.completionRegister = nil;\n\t\t}\n\t\tself.controller = nil;\n\t\tif (@available(macOS 12.0, *)) {\n\t\t\tself.provider = nil;\n\t\t}\n\t} else if (self.completionLogin) {\n\t\tif ([authorization.credential conformsToProtocol:\n\t\t\t\t@protocol(ASAuthorizationPublicKeyCredentialAssertion)]) {\n\t\t\tauto credential\n\t\t\t\t= (id<ASAuthorizationPublicKeyCredentialAssertion>)\n\t\t\t\t\tauthorization.credential;\n\t\t\tauto result = Platform::WebAuthn::LoginResult();\n\t\t\tresult.credentialId = QByteArray::fromNSData(\n\t\t\t\tcredential.credentialID);\n\t\t\tresult.authenticatorData = QByteArray::fromNSData(\n\t\t\t\tcredential.rawAuthenticatorData);\n\t\t\tresult.signature = QByteArray::fromNSData(\n\t\t\t\tcredential.signature);\n\t\t\tresult.clientDataJSON = QByteArray::fromNSData(\n\t\t\t\tcredential.rawClientDataJSON);\n\t\t\tresult.userHandle = QByteArray::fromNSData(credential.userID);\n\t\t\tself.completionLogin(result);\n\t\t\tself.completionLogin = nil;\n\t\t}\n\t\tself.controller = nil;\n\t\tif (@available(macOS 12.0, *)) {\n\t\t\tself.provider = nil;\n\t\t}\n\t}\n}\n\n- (void)authorizationController:(ASAuthorizationController *)controller\n\t\t\tdidCompleteWithError:(NSError *)error\n\t\tAPI_AVAILABLE(macos(10.15)) {\n\tconst auto isCancelled = (error.code == ASAuthorizationErrorCanceled);\n\tconst auto isUnsigned = (error.code == 1004 || error.code == 1009);\n\tif (!isCancelled) {\n\t\tNSLog(@\"WebAuthn error: %@ (code: %ld)\",\n\t\t\terror.localizedDescription, (long)error.code);\n\t}\n\tif (self.completionRegister) {\n\t\tauto result = Platform::WebAuthn::RegisterResult();\n\t\tresult.success = false;\n\t\tresult.error = isUnsigned\n\t\t\t? Platform::WebAuthn::Error::UnsignedBuild\n\t\t\t: (isCancelled\n\t\t\t\t? Platform::WebAuthn::Error::Cancelled\n\t\t\t\t: Platform::WebAuthn::Error::Other);\n\t\tself.completionRegister(result);\n\t\tself.completionRegister = nil;\n\t} else if (self.completionLogin) {\n\t\tauto result = Platform::WebAuthn::LoginResult();\n\t\tresult.error = isUnsigned\n\t\t\t? Platform::WebAuthn::Error::UnsignedBuild\n\t\t\t: (isCancelled\n\t\t\t\t? Platform::WebAuthn::Error::Cancelled\n\t\t\t\t: Platform::WebAuthn::Error::Other);\n\t\tself.completionLogin(result);\n\t\tself.completionLogin = nil;\n\t}\n\tself.controller = nil;\n\tif (@available(macOS 12.0, *)) {\n\t\tself.provider = nil;\n\t}\n}\n\n- (ASPresentationAnchor)presentationAnchorForAuthorizationController:\n\t\t(ASAuthorizationController *)controller\n\t\tAPI_AVAILABLE(macos(10.15)) {\n\treturn [NSApp mainWindow];\n}\n\n@end\n\nnamespace {\n\nbase::options::toggle WebAuthnMacOption({\n\t.id = \"webauthn-mac\",\n\t.name = \"Enable Passkey on macOS\",\n\t.description = \"Enable Passkey support on macOS 12.0+. Experimental feature that may cause crash.\",\n\t.defaultValue = false,\n\t.scope = base::options::macos,\n});\n\n} // namespace\n\nnamespace Platform::WebAuthn {\n\nbool IsSupported() {\n\tif (!WebAuthnMacOption.value()) {\n\t\treturn false;\n\t}\n\tif (@available(macOS 12.0, *)) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid RegisterKey(\n\t\tconst Data::Passkey::RegisterData &data,\n\t\tFn<void(RegisterResult result)> callback) {\n\tif (@available(macOS 12.0, *)) {\n\t\tauto rpId = data.rp.id.toNSString();\n\t\tauto userName = data.user.name.toNSString();\n\t\tauto userDisplayName = data.user.displayName.toNSString();\n\t\tauto userId = [NSData dataWithBytes:data.user.id.constData()\n\t\t\tlength:data.user.id.size()];\n\t\tauto challenge = [NSData dataWithBytes:data.challenge.constData()\n\t\t\tlength:data.challenge.size()];\n\n\t\tif (!userId || !challenge) {\n\t\t\tauto result = RegisterResult();\n\t\t\tresult.success = false;\n\t\t\tcallback(result);\n\t\t\treturn;\n\t\t}\n\n\t\tauto provider = [[ASAuthorizationPlatformPublicKeyCredentialProvider\n\t\t\talloc] initWithRelyingPartyIdentifier:rpId];\n\n\t\tauto request = [provider\n\t\t\tcreateCredentialRegistrationRequestWithChallenge:challenge\n\t\t\tname:userName\n\t\t\tuserID:userId];\n\n\t\tif (userDisplayName && userDisplayName.length > 0) {\n\t\t\trequest.displayName = userDisplayName;\n\t\t}\n\n\t\tif (@available(macOS 13.5, *)) {\n\t\t\trequest.attestationPreference =\n\t\t\t\tASAuthorizationPublicKeyCredentialAttestationKindNone;\n\t\t}\n\n\t\tauto controller = [[ASAuthorizationController alloc]\n\t\t\tinitWithAuthorizationRequests:@[request]];\n\n\t\tauto delegate = [[WebAuthnDelegate alloc] init];\n\t\tif (@available(macOS 12.0, *)) {\n\t\t\tdelegate.provider = provider;\n\t\t}\n\t\tdelegate.controller = controller;\n\t\tdelegate.completionRegister = ^(const RegisterResult &result) {\n\t\t\tcallback(result);\n\t\t\t[delegate release];\n\t\t};\n\n\t\tcontroller.delegate = delegate;\n\t\tcontroller.presentationContextProvider = delegate;\n\t\t[controller performRequests];\n\t} else {\n\t\tauto result = RegisterResult();\n\t\tresult.success = false;\n\t\tcallback(result);\n\t}\n}\n\nvoid Login(\n\t\tconst Data::Passkey::LoginData &data,\n\t\tFn<void(LoginResult result)> callback) {\n\tif (@available(macOS 12.0, *)) {\n\t\tauto challenge = [NSData dataWithBytes:data.challenge.constData()\n\t\t\tlength:data.challenge.size()];\n\n\t\tif (!challenge) {\n\t\t\tauto result = LoginResult();\n\t\t\tcallback(result);\n\t\t\treturn;\n\t\t}\n\n\t\tauto provider = [[ASAuthorizationPlatformPublicKeyCredentialProvider\n\t\t\talloc] initWithRelyingPartyIdentifier:data.rpId.toNSString()];\n\n\t\tauto request = [provider\n\t\t\tcreateCredentialAssertionRequestWithChallenge:challenge];\n\n\t\tif (!data.allowCredentials.empty()) {\n\t\t\tNSMutableArray *credentialIds = [NSMutableArray array];\n\t\t\tfor (const auto &cred : data.allowCredentials) {\n\t\t\t\tauto credId = [NSData dataWithBytes:cred.id.constData()\n\t\t\t\t\tlength:cred.id.size()];\n\t\t\t\tif (credId) {\n\t\t\t\t\t[credentialIds addObject:credId];\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (credentialIds.count > 0) {\n\t\t\t\trequest.allowedCredentials = credentialIds;\n\t\t\t}\n\t\t}\n\n\t\tif (data.userVerification == \"required\") {\n\t\t\trequest.userVerificationPreference =\n\t\t\t\tASAuthorizationPublicKeyCredentialUserVerificationPreferenceRequired;\n\t\t} else if (data.userVerification == \"preferred\") {\n\t\t\trequest.userVerificationPreference =\n\t\t\t\tASAuthorizationPublicKeyCredentialUserVerificationPreferencePreferred;\n\t\t} else if (data.userVerification == \"discouraged\") {\n\t\t\trequest.userVerificationPreference =\n\t\t\t\tASAuthorizationPublicKeyCredentialUserVerificationPreferenceDiscouraged;\n\t\t}\n\n\t\tauto controller = [[ASAuthorizationController alloc]\n\t\t\tinitWithAuthorizationRequests:@[request]];\n\n\t\tauto delegate = [[WebAuthnDelegate alloc] init];\n\t\tif (@available(macOS 12.0, *)) {\n\t\t\tdelegate.provider = provider;\n\t\t}\n\t\tdelegate.controller = controller;\n\t\tdelegate.completionLogin = ^(const LoginResult &result) {\n\t\t\tcallback(result);\n\t\t\t[delegate release];\n\t\t};\n\n\t\tcontroller.delegate = delegate;\n\t\tcontroller.presentationContextProvider = delegate;\n\t\t[controller performRequests];\n\t} else {\n\t\tauto result = LoginResult();\n\t\tcallback(result);\n\t}\n}\n\n} // namespace Platform::WebAuthn\n\n#endif\n\nnamespace Platform::WebAuthn {\n\n\nbool IsSupported() {\n\treturn false;\n}\n\n\nvoid RegisterKey(\n\t\tconst Data::Passkey::RegisterData &data,\n\t\tFn<void(RegisterResult result)> callback) {\n}\n\nvoid Login(\n\t\tconst Data::Passkey::LoginData &data,\n\t\tFn<void(LoginResult result)> callback) {\n}\n\n} // namespace Platform::WebAuthn\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/mac/window_title_mac.mm",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/platform_window_title.h\"\n\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"core/application.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_media_view.h\"\n#include \"window/window_controller.h\"\n\n#include <Cocoa/Cocoa.h>\n\nnamespace Platform {\n\n// All the window decorations preview is done without taking cScale() into\n// account, with 100% scale and without \"px\" dimensions, because thats\n// how it will look in real launched macOS app.\nint PreviewTitleHeight() {\n\tif (const auto window = Core::App().activePrimaryWindow()) {\n\t\tif (const auto height = window->widget()->getCustomTitleHeight()) {\n\t\t\treturn height;\n\t\t}\n\t}\n\treturn 22;\n}\n\nQImage PreviewWindowSystemButton(QColor inner, QColor border) {\n\tauto buttonSize = 12;\n\tauto fullSize = buttonSize * style::DevicePixelRatio();\n\tauto result = QImage(fullSize, fullSize, QImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::transparent);\n\t{\n\t\tPainter p(&result);\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tp.setPen(border);\n\t\tp.setBrush(inner);\n\t\tp.drawEllipse(QRectF(0.5, 0.5, fullSize - 1., fullSize - 1.));\n\t}\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\treturn result;\n}\n\nvoid PreviewWindowTitle(Painter &p, const style::palette &palette, QRect body, int titleHeight, int outerWidth) {\n\tauto titleRect = QRect(body.x(), body.y() - titleHeight, body.width(), titleHeight);\n\tp.fillRect(titleRect, QColor(0, 0, 0));\n\tp.fillRect(titleRect, st::titleBgActive[palette]);\n\tp.fillRect(titleRect.x(), titleRect.y() + titleRect.height() - st::lineWidth, titleRect.width(), st::lineWidth, st::titleShadow[palette]);\n\n\tQFont font;\n\tconst auto families = QStringList{\n\t\tu\".AppleSystemUIFont\"_q,\n\t\tu\".SF NS Text\"_q,\n\t\tu\"Helvetica Neue\"_q,\n\t};\n\tfor (auto family : families) {\n\t\tfont.setFamily(family);\n\t\tif (QFontInfo(font).family() == font.family()) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tif (QFontInfo(font).family() != font.family()) {\n\t\tfont = st::semiboldFont;\n\t\tfont.setPixelSize(13);\n\t} else if (font.family() == u\".AppleSystemUIFont\"_q) {\n\t\tfont.setBold(true);\n\t\tfont.setPixelSize(13);\n\t} else {\n\t\tfont.setPixelSize((titleHeight * 15) / 24);\n\t}\n\n\tp.setPen(st::titleFgActive[palette]);\n\tp.setFont(font);\n\n\tp.drawText(titleRect, u\"Telegram\"_q, style::al_center);\n\n\tauto isGraphite = ([NSColor currentControlTint] == NSGraphiteControlTint);\n\tauto buttonSkip = 8;\n\tauto graphiteInner = QColor(141, 141, 146);\n\tauto graphiteBorder = QColor(104, 104, 109);\n\tauto closeInner = isGraphite ? graphiteInner : QColor(252, 96, 92);\n\tauto closeBorder = isGraphite ? graphiteBorder : QColor(222, 64, 59);\n\tauto minimizeInner = isGraphite ? graphiteInner : QColor(254, 192, 65);\n\tauto minimizeBorder = isGraphite ? graphiteBorder : QColor(221, 152, 25);\n\tauto maximizeInner = isGraphite ? graphiteInner : QColor(52, 200, 74);\n\tauto maximizeBorder = isGraphite ? graphiteBorder : QColor(21, 164, 41);\n\tauto close = PreviewWindowSystemButton(closeInner, closeBorder);\n\tauto left = buttonSkip;\n\tp.drawImage(\n\t\ttitleRect.x() + left,\n\t\ttitleRect.y()\n\t\t\t+ (titleRect.height()\n\t\t\t\t- (close.height() / style::DevicePixelRatio())) / 2,\n\t\tclose);\n\tleft += (close.width() / style::DevicePixelRatio()) + buttonSkip;\n\tauto minimize = PreviewWindowSystemButton(minimizeInner, minimizeBorder);\n\tp.drawImage(\n\t\ttitleRect.x() + left,\n\t\ttitleRect.y()\n\t\t\t+ (titleRect.height()\n\t\t\t\t- (minimize.height() / style::DevicePixelRatio())) / 2,\n\t\tminimize);\n\tleft += (minimize.width() / style::DevicePixelRatio()) + buttonSkip;\n\tauto maximize = PreviewWindowSystemButton(maximizeInner, maximizeBorder);\n\tp.drawImage(\n\t\ttitleRect.x() + left,\n\t\ttitleRect.y()\n\t\t\t+ (titleRect.height()\n\t\t\t\t- (maximize.height() / style::DevicePixelRatio())) / 2,\n\t\tmaximize);\n}\n\nvoid PreviewWindowFramePaint(QImage &preview, const style::palette &palette, QRect body, int outerWidth) {\n\tauto retina = style::DevicePixelRatio();\n\tauto titleHeight = PreviewTitleHeight();\n\t{\n\t\tPainter p(&preview);\n\t\tPreviewWindowTitle(p, palette, body, titleHeight, outerWidth);\n\t}\n\tauto inner = QRect(body.x(), body.y() - titleHeight, body.width(), body.height() + titleHeight);\n\n\tauto retinaRadius = st::macWindowRoundRadius * retina;\n\tauto roundMask = QImage(2 * retinaRadius, 2 * retinaRadius, QImage::Format_ARGB32_Premultiplied);\n\troundMask.setDevicePixelRatio(style::DevicePixelRatio());\n\troundMask.fill(Qt::transparent);\n\t{\n\t\tPainter p(&roundMask);\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(QColor(255, 255, 255));\n\t\tp.drawRoundedRect(0, 0, 2 * st::macWindowRoundRadius, 2 * st::macWindowRoundRadius, st::macWindowRoundRadius, st::macWindowRoundRadius);\n\t}\n\tQImage corners[4];\n\tcorners[0] = roundMask.copy(0, 0, retinaRadius, retinaRadius);\n\tcorners[1] = roundMask.copy(retinaRadius, 0, retinaRadius, retinaRadius);\n\tcorners[2] = roundMask.copy(0, retinaRadius, retinaRadius, retinaRadius);\n\tcorners[3] = roundMask.copy(retinaRadius, retinaRadius, retinaRadius, retinaRadius);\n\tauto rounded = Images::Round(\n\t\tpreview.copy(\n\t\t\tinner.x() * retina,\n\t\t\tinner.y() * retina,\n\t\t\tinner.width() * retina,\n\t\t\tinner.height() * retina),\n\t\t\tcorners);\n\trounded.setDevicePixelRatio(style::DevicePixelRatio());\n\tpreview.fill(st::themePreviewBg->c);\n\n\tauto topLeft = st::macWindowShadowTopLeft.instance(QColor(0, 0, 0), 100);\n\tauto topRight = topLeft.mirrored(true, false);\n\tauto bottomLeft = topLeft.mirrored(false, true);\n\tauto bottomRight = bottomLeft.mirrored(true, false);\n\tauto extend = QMargins(37, 28, 37, 28);\n\tauto left = topLeft.copy(0, topLeft.height() - retina, extend.left() * retina, retina);\n\tauto top = topLeft.copy(topLeft.width() - retina, 0, retina, extend.top() * retina);\n\tauto right = topRight.copy(topRight.width() - (extend.right() * retina), topRight.height() - retina, extend.right() * retina, retina);\n\tauto bottom = bottomRight.copy(0, bottomRight.height() - (extend.bottom() * retina), retina, extend.bottom() * retina);\n\t{\n\t\tPainter p(&preview);\n\t\tp.drawImage(inner.x() - extend.left(), inner.y() - extend.top(), topLeft);\n\t\tp.drawImage(inner.x() + inner.width() + extend.right() - (topRight.width() / retina), inner.y() - extend.top(), topRight);\n\t\tp.drawImage(inner.x() - extend.left(), inner.y() + inner.height() + extend.bottom() - (bottomLeft.height() / retina), bottomLeft);\n\t\tp.drawImage(inner.x() + inner.width() + extend.right() - (bottomRight.width() / retina), inner.y() + inner.height() + extend.bottom() - (bottomRight.height() / retina), bottomRight);\n\t\tp.drawImage(QRect(inner.x() - extend.left(), inner.y() - extend.top() + (topLeft.height() / retina), extend.left(), extend.top() + inner.height() + extend.bottom() - (topLeft.height() / retina) - (bottomLeft.height() / retina)), left);\n\t\tp.drawImage(QRect(inner.x() - extend.left() + (topLeft.width() / retina), inner.y() - extend.top(), extend.left() + inner.width() + extend.right() - (topLeft.width() / retina) - (topRight.width() / retina), extend.top()), top);\n\t\tp.drawImage(QRect(inner.x() + inner.width(), inner.y() - extend.top() + (topRight.height() / retina), extend.right(), extend.top() + inner.height() + extend.bottom() - (topRight.height() / retina) - (bottomRight.height() / retina)), right);\n\t\tp.drawImage(QRect(inner.x() - extend.left() + (bottomLeft.width() / retina), inner.y() + inner.height(), extend.left() + inner.width() + extend.right() - (bottomLeft.width() / retina) - (bottomRight.width() / retina), extend.bottom()), bottom);\n\t\tp.drawImage(inner.x(), inner.y(), rounded);\n\t}\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_current_geo_location.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Core {\nstruct GeoLocation;\nstruct GeoAddress;\n} // namespace Core\n\nnamespace Platform {\n\nvoid ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback);\nvoid ResolveLocationAddress(\n\tconst Core::GeoLocation &location,\n\tconst QString &language,\n\tFn<void(Core::GeoAddress)> callback);\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_file_bookmark.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#ifdef Q_OS_MAC\n#include \"platform/mac/file_bookmark_mac.h\"\n#else // Q_OS_MAC\n\nnamespace Platform {\n\nclass FileBookmark {\npublic:\n\tFileBookmark(const QByteArray &bookmark) {\n\t}\n\tbool check() const {\n\t\treturn true;\n\t}\n\tbool enable() const {\n\t\treturn true;\n\t}\n\tvoid disable() const {\n\t}\n\tconst QString &name(const QString &original) const {\n\t\treturn original;\n\t}\n\tQByteArray bookmark() const {\n\t\treturn QByteArray();\n\t}\n\n};\n\n[[nodiscard]] inline QByteArray PathBookmark(const QString &path) {\n\treturn QByteArray();\n}\n\n} // namespace Platform\n\n#endif // Q_OS_MAC\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_file_utilities.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"core/file_utilities.h\"\n\nnamespace Platform {\nnamespace File {\n\nQString UrlToLocal(const QUrl &url);\n\n// All these functions may enter a nested event loop. Use with caution.\nvoid UnsafeOpenUrl(const QString &url);\nvoid UnsafeOpenEmailLink(const QString &email);\nbool UnsafeShowOpenWithDropdown(const QString &filepath);\nbool UnsafeShowOpenWith(const QString &filepath);\nvoid UnsafeLaunch(const QString &filepath);\n\nvoid PostprocessDownloaded(const QString &filepath);\n\n} // namespace File\n\nnamespace FileDialog {\n\nvoid InitLastPath();\n\nbool Get(\n\tQPointer<QWidget> parent,\n\tQStringList &files,\n\tQByteArray &remoteContent,\n\tconst QString &caption,\n\tconst QString &filter,\n\t::FileDialog::internal::Type type,\n\tQString startFile = QString());\n\n} // namespace FileDialog\n} // namespace Platform\n\n// Platform dependent implementations.\n\n#if defined Q_OS_WINRT || defined Q_OS_WIN\n#include \"platform/win/file_utilities_win.h\"\n#elif defined Q_OS_MAC // Q_OS_WINRT || Q_OS_WIN\n#include \"platform/mac/file_utilities_mac.h\"\n#else // Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC\n#include \"platform/linux/file_utilities_linux.h\"\n#endif // else for Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_integration.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/platform_integration.h\"\n\n#if defined Q_OS_WINRT || defined Q_OS_WIN\n#include \"platform/win/integration_win.h\"\n#elif defined Q_OS_MAC // Q_OS_WINRT || Q_OS_WIN\n#include \"platform/mac/integration_mac.h\"\n#else // Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC\n#include \"platform/linux/integration_linux.h\"\n#endif // else Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC\n\nnamespace Platform {\nnamespace {\n\nIntegration *GlobalInstance/* = nullptr*/;\n\n} // namespace\n\nIntegration::~Integration() {\n\tGlobalInstance = nullptr;\n}\n\nstd::unique_ptr<Integration> Integration::Create() {\n\tExpects(GlobalInstance == nullptr);\n\n\tauto result = CreateIntegration();\n\tGlobalInstance = result.get();\n\treturn result;\n}\n\nIntegration &Integration::Instance() {\n\tExpects(GlobalInstance != nullptr);\n\n\treturn *GlobalInstance;\n};\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_integration.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Platform {\n\nclass Integration {\npublic:\n\tvirtual void init() {\n\t}\n\n\tvirtual ~Integration();\n\n\t[[nodiscard]] static std::unique_ptr<Integration> Create();\n\t[[nodiscard]] static Integration &Instance();\n};\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_launcher.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Platform {\n\n//class Launcher : public Core::Launcher {\n//public:\n//\tLauncher(int argc, char *argv[]);\n//\n//\t...\n//\n//};\n\n} // namespace Platform\n\n// Platform dependent implementations.\n\n#if defined Q_OS_WINRT || defined Q_OS_WIN\n#include \"platform/win/launcher_win.h\"\n#elif defined Q_OS_MAC // Q_OS_WINRT || Q_OS_WIN\n#include \"platform/mac/launcher_mac.h\"\n#else // Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC\n#include \"platform/linux/launcher_linux.h\"\n#endif // else for Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_main_window.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"window/main_window.h\"\n\nnamespace Platform {\n\nclass MainWindow;\n\n} // namespace Platform\n\n// Platform dependent implementations.\n\n#ifdef Q_OS_WIN\n#include \"platform/win/main_window_win.h\"\n#elif defined Q_OS_MAC // Q_OS_WIN\n#include \"platform/mac/main_window_mac.h\"\n#else // Q_OS_WIN || Q_OS_MAC\n#include \"platform/linux/main_window_linux.h\"\n#endif // else Q_OS_WIN || Q_OS_MAC\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_notifications_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"window/notifications_manager.h\"\n\nnamespace Platform {\nnamespace Notifications {\n\n[[nodiscard]] bool SkipToastForCustom();\nvoid MaybePlaySoundForCustom(Fn<void()> playSound);\nvoid MaybeFlashBounceForCustom(Fn<void()> flashBounce);\n[[nodiscard]] bool WaitForInputForCustom();\n\n[[nodiscard]] bool Supported();\n[[nodiscard]] bool Enforced();\n[[nodiscard]] bool ByDefault();\n[[nodiscard]] bool VolumeSupported();\nvoid Create(Window::Notifications::System *system);\n\n} // namespace Notifications\n} // namespace Platform\n\n// Platform dependent implementations.\n\n#ifdef Q_OS_WIN\n#include \"platform/win/notifications_manager_win.h\"\n#elif defined Q_OS_MAC // Q_OS_MAC\n#include \"platform/mac/notifications_manager_mac.h\"\n#else // Q_OS_WIN || Q_OS_MAC\n#include \"platform/linux/notifications_manager_linux.h\"\n#endif // else for Q_OS_WIN || Q_OS_MAC\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_overlay_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/platform_overlay_widget.h\"\n\n#include \"ui/effects/animations.h\"\n#include \"ui/platform/ui_platform_window_title.h\"\n#include \"ui/widgets/rp_window.h\"\n#include \"ui/abstract_button.h\"\n#include \"styles/style_media_view.h\"\n\nnamespace Media::View {\n\nQColor OverBackgroundColor() {\n\tauto c1 = st::mediaviewBg->c;\n\tauto c2 = QColor(255, 255, 255);\n\tconst auto mix = [&](int a, int b) {\n\t\tconstexpr auto k1 = 0.15 * 0.85 / (1. - 0.85 * 0.85);\n\t\tconstexpr auto k2 = 0.15 / (1. - 0.85 * 0.85);\n\t\treturn int(a * k1 + b * k2);\n\t};\n\treturn QColor(\n\t\tmix(c1.red(), c2.red()),\n\t\tmix(c1.green(), c2.green()),\n\t\tmix(c1.blue(), c2.blue()));\n}\n\n} // namespace Media::View\n\nnamespace Platform {\nnamespace {\n\nusing namespace Media::View;\n\n} // namespace\n\nclass DefaultOverlayWidgetHelper::Buttons final\n\t: public Ui::Platform::AbstractTitleButtons {\npublic:\n\tusing Control = Ui::Platform::TitleControl;\n\n\tobject_ptr<Ui::AbstractButton> create(\n\t\tnot_null<QWidget*> parent,\n\t\tControl control,\n\t\tconst style::WindowTitle &st) override;\n\tvoid updateState(\n\t\tbool active,\n\t\tbool maximized,\n\t\tconst style::WindowTitle &st,\n\t\tbool pinnedToTop = false) override;\n\tvoid notifySynteticOver(Control control, bool over) override;\n\n\tvoid setMasterOpacity(float64 opacity);\n\t[[nodiscard]] rpl::producer<> activations() const;\n\n\tvoid clearState();\n\nprivate:\n\trpl::event_stream<> _activations;\n\trpl::variable<float64> _masterOpacity = 1.;\n\trpl::variable<bool> _maximized = false;\n\trpl::event_stream<> _clearStateRequests;\n\n};\n\nobject_ptr<Ui::AbstractButton> DefaultOverlayWidgetHelper::Buttons::create(\n\t\tnot_null<QWidget*> parent,\n\t\tControl control,\n\t\tconst style::WindowTitle &st) {\n\tauto result = object_ptr<Ui::AbstractButton>(parent);\n\tconst auto raw = result.data();\n\n\tstruct State {\n\t\tUi::Animations::Simple animation;\n\t\tfloat64 progress = -1.;\n\t\tQImage frame;\n\t\tbool maximized = false;\n\t\tbool over = false;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>();\n\n\trpl::merge(\n\t\t_masterOpacity.changes() | rpl::to_empty,\n\t\t_maximized.changes() | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\traw->update();\n\t}, raw->lifetime());\n\n\t_clearStateRequests.events(\n\t) | rpl::on_next([=] {\n\t\traw->clearState();\n\t\traw->update();\n\t\tstate->over = raw->isOver();\n\t\tstate->animation.stop();\n\t}, raw->lifetime());\n\n\tconst auto icon = [&] {\n\t\tswitch (control) {\n\t\tcase Control::Minimize: return &st::mediaviewTitleMinimize;\n\t\tcase Control::Maximize: return &st::mediaviewTitleMaximize;\n\t\tcase Control::Close: return &st::mediaviewTitleClose;\n\t\tcase Control::OnTop: return &st::mediaviewTitleClose;\n\t\t}\n\t\tUnexpected(\"Value in DefaultOverlayWidgetHelper::Buttons::create.\");\n\t}();\n\n\traw->resize(icon->size());\n\tstate->frame = QImage(\n\t\ticon->size() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tstate->frame.setDevicePixelRatio(style::DevicePixelRatio());\n\n\tconst auto updateOver = [=] {\n\t\tconst auto over = raw->isOver();\n\t\tif (state->over == over) {\n\t\t\treturn;\n\t\t}\n\t\tstate->over = over;\n\t\tstate->animation.start(\n\t\t\t[=] { raw->update(); },\n\t\t\tstate->over ? 0. : 1.,\n\t\t\tstate->over ? 1. : 0.,\n\t\t\tst::mediaviewFadeDuration);\n\t};\n\n\tconst auto prepareFrame = [=] {\n\t\tconst auto progress = state->animation.value(state->over ? 1. : 0.);\n\t\tconst auto maximized = _maximized.current();\n\t\tif (state->progress == progress && state->maximized == maximized) {\n\t\t\treturn;\n\t\t}\n\t\tstate->progress = progress;\n\t\tstate->maximized = maximized;\n\t\tauto current = icon;\n\t\tif (control == Control::Maximize) {\n\t\t\tcurrent = maximized ? &st::mediaviewTitleRestore : icon;\n\t\t}\n\t\tconst auto alpha = progress * kOverBackgroundOpacity;\n\t\tauto color = OverBackgroundColor();\n\t\tcolor.setAlpha(anim::interpolate(0, 255, alpha));\n\t\tstate->frame.fill(color);\n\n\t\tauto q = QPainter(&state->frame);\n\t\tconst auto normal = maximized\n\t\t\t? kMaximizedIconOpacity\n\t\t\t: kNormalIconOpacity;\n\t\tq.setOpacity(progress + (1 - progress) * normal);\n\t\tcurrent->paint(q, 0, 0, raw->width());\n\t\tq.end();\n\t};\n\n\traw->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tupdateOver();\n\t\tprepareFrame();\n\n\t\tauto p = QPainter(raw);\n\t\tp.setOpacity(_masterOpacity.current());\n\t\tp.drawImage(0, 0, state->frame);\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\nvoid DefaultOverlayWidgetHelper::Buttons::updateState(\n\t\tbool active,\n\t\tbool maximized,\n\t\tconst style::WindowTitle &st,\n\t\tbool pinnedToTop) {\n\t_maximized = maximized;\n}\n\nvoid DefaultOverlayWidgetHelper::Buttons::notifySynteticOver(\n\t\tUi::Platform::TitleControl control,\n\t\tbool over) {\n\tif (over) {\n\t\t_activations.fire({});\n\t}\n}\n\nvoid DefaultOverlayWidgetHelper::Buttons::clearState() {\n\t_clearStateRequests.fire({});\n}\n\nvoid DefaultOverlayWidgetHelper::Buttons::setMasterOpacity(float64 opacity) {\n\t_masterOpacity = opacity;\n}\n\nrpl::producer<> DefaultOverlayWidgetHelper::Buttons::activations() const {\n\treturn _activations.events();\n}\n\nvoid OverlayWidgetHelper::minimize(not_null<Ui::RpWindow*> window) {\n\twindow->setWindowState(window->windowState() | Qt::WindowMinimized);\n}\n\nDefaultOverlayWidgetHelper::DefaultOverlayWidgetHelper(\n\tnot_null<Ui::RpWindow*> window,\n\tFn<void(bool)> maximize)\n: _buttons(new DefaultOverlayWidgetHelper::Buttons())\n, _controls(Ui::Platform::SetupSeparateTitleControls(\n\twindow,\n\tstd::make_unique<Ui::Platform::SeparateTitleControls>(\n\t\twindow->body(),\n\t\tst::mediaviewTitle,\n\t\tstd::unique_ptr<DefaultOverlayWidgetHelper::Buttons>(_buttons.get()),\n\t\tstd::move(maximize)))) {\n}\n\nDefaultOverlayWidgetHelper::~DefaultOverlayWidgetHelper() = default;\n\nvoid DefaultOverlayWidgetHelper::orderWidgets() {\n\t_controls->wrap.raise();\n}\n\nbool DefaultOverlayWidgetHelper::skipTitleHitTest(QPoint position) {\n\tusing namespace Ui::Platform;\n\treturn _controls->controls.hitTest(position) != HitTestResult::None;\n}\n\nrpl::producer<> DefaultOverlayWidgetHelper::controlsActivations() {\n\treturn _buttons->activations();\n}\n\nrpl::producer<bool> DefaultOverlayWidgetHelper::controlsSideRightValue() {\n\treturn _controls->controls.layout().value(\n\t) | rpl::map([=](const auto &layout) {\n\t\treturn !layout.onLeft();\n\t}) | rpl::distinct_until_changed();\n}\n\nvoid DefaultOverlayWidgetHelper::beforeShow(bool fullscreen) {\n\t_buttons->clearState();\n}\n\nvoid DefaultOverlayWidgetHelper::clearState() {\n\t_buttons->clearState();\n}\n\nvoid DefaultOverlayWidgetHelper::setControlsOpacity(float64 opacity) {\n\t_buttons->setMasterOpacity(opacity);\n}\n\nauto DefaultOverlayWidgetHelper::mouseEvents() const\n-> rpl::producer<not_null<QMouseEvent*>> {\n\treturn _controls->wrap.events(\n\t) | rpl::filter([](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\treturn (type == QEvent::MouseButtonPress)\n\t\t\t|| (type == QEvent::MouseButtonRelease)\n\t\t\t|| (type == QEvent::MouseMove)\n\t\t\t|| (type == QEvent::MouseButtonDblClick);\n\t}) | rpl::map([](not_null<QEvent*> e) {\n\t\treturn not_null{ static_cast<QMouseEvent*>(e.get()) };\n\t});\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_overlay_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass RpWindow;\n} // namespace Ui\n\nnamespace Ui::Platform {\nstruct SeparateTitleControls;\n} // namespace Ui::Platform\n\nnamespace Media::View {\n\ninline constexpr auto kMaximizedIconOpacity = 0.6;\ninline constexpr auto kNormalIconOpacity = 0.9;\ninline constexpr auto kOverBackgroundOpacity = 0.2775;\ninline constexpr auto kStoriesNavOpacity = 0.3;\ninline constexpr auto kStoriesNavOverOpacity = 0.7;\n[[nodiscard]] QColor OverBackgroundColor();\n\n} // namespace Media::View\n\nnamespace Platform {\n\nclass OverlayWidgetHelper {\npublic:\n\tvirtual ~OverlayWidgetHelper() = default;\n\n\tvirtual void orderWidgets() {\n\t}\n\t[[nodiscard]] virtual bool skipTitleHitTest(QPoint position) {\n\t\treturn false;\n\t}\n\t[[nodiscard]] virtual rpl::producer<> controlsActivations() {\n\t\treturn rpl::never<>();\n\t}\n\t[[nodiscard]] virtual rpl::producer<bool> controlsSideRightValue() {\n\t\treturn rpl::single(true);\n\t}\n\tvirtual void beforeShow(bool fullscreen) {\n\t}\n\tvirtual void afterShow(bool fullscreen) {\n\t}\n\tvirtual void notifyFileDialogShown(bool shown) {\n\t}\n\tvirtual void minimize(not_null<Ui::RpWindow*> window);\n\tvirtual void clearState() {\n\t}\n\tvirtual void setControlsOpacity(float64 opacity) {\n\t}\n\t[[nodiscard]] virtual auto mouseEvents() const\n\t-> rpl::producer<not_null<QMouseEvent*>> {\n\t\treturn rpl::never<not_null<QMouseEvent*>>();\n\t}\n\t[[nodiscard]] virtual rpl::producer<int> topNotchSkipValue() {\n\t\treturn rpl::single(0);\n\t}\n};\n\n[[nodiscard]] std::unique_ptr<OverlayWidgetHelper> CreateOverlayWidgetHelper(\n\tnot_null<Ui::RpWindow*> window,\n\tFn<void(bool)> maximize);\n\nclass DefaultOverlayWidgetHelper final : public OverlayWidgetHelper {\npublic:\n\tDefaultOverlayWidgetHelper(\n\t\tnot_null<Ui::RpWindow*> window,\n\t\tFn<void(bool)> maximize);\n\t~DefaultOverlayWidgetHelper();\n\n\tvoid orderWidgets() override;\n\tbool skipTitleHitTest(QPoint position) override;\n\trpl::producer<> controlsActivations() override;\n\tvoid beforeShow(bool fullscreen) override;\n\tvoid clearState() override;\n\tvoid setControlsOpacity(float64 opacity) override;\n\trpl::producer<bool> controlsSideRightValue() override;\n\trpl::producer<not_null<QMouseEvent*>> mouseEvents() const override;\n\nprivate:\n\tclass Buttons;\n\n\tconst not_null<Buttons*> _buttons;\n\tconst std::unique_ptr<Ui::Platform::SeparateTitleControls> _controls;\n\n};\n\n} // namespace Platform\n\n// Platform dependent implementations.\n\n#ifdef Q_OS_WIN\n#include \"platform/win/overlay_widget_win.h\"\n#elif defined Q_OS_MAC // Q_OS_WIN\n#include \"platform/mac/overlay_widget_mac.h\"\n#else // Q_OS_WIN || Q_OS_MAC\n#include \"platform/linux/overlay_widget_linux.h\"\n#endif // else for Q_OS_WIN || Q_OS_MAC\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_specific.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Core {\nenum class QuitReason;\n} // namespace Core\n\nnamespace Data {\nclass LocationPoint;\n} // namespace Data\n\nnamespace Platform {\n\nvoid start();\nvoid finish();\n\nenum class PermissionStatus {\n\tGranted,\n\tCanRequest,\n\tDenied,\n};\n\nenum class PermissionType {\n\tMicrophone,\n\tCamera,\n};\n\nenum class SystemSettingsType {\n\tAudio,\n};\n\nvoid SetApplicationIcon(const QIcon &icon);\n[[nodiscard]] QString SingleInstanceLocalServerName(const QString &hash);\n[[nodiscard]] PermissionStatus GetPermissionStatus(PermissionType type);\nvoid RequestPermission(PermissionType type, Fn<void(PermissionStatus)> resultCallback);\nvoid OpenSystemSettingsForPermission(PermissionType type);\nbool OpenSystemSettings(SystemSettingsType type);\nvoid IgnoreApplicationActivationRightNow();\n[[nodiscard]] bool AutostartSupported();\nvoid AutostartRequestStateFromSystem(Fn<void(bool)> callback);\nvoid AutostartToggle(bool enabled, Fn<void(bool)> done = nullptr);\n[[nodiscard]] bool AutostartSkip();\n[[nodiscard]] bool TrayIconSupported();\n[[nodiscard]] bool SkipTaskbarSupported();\nvoid WriteCrashDumpDetails();\nvoid NewVersionLaunched(int oldVersion);\n[[nodiscard]] QImage DefaultApplicationIcon();\n[[nodiscard]] QString ApplicationIconName();\n[[nodiscard]] bool PreventsQuit(Core::QuitReason reason);\n[[nodiscard]] QString ExecutablePathForShortcuts();\nvoid LaunchMaps(const Data::LocationPoint &point, Fn<void()> fail);\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)\n[[nodiscard]] std::optional<bool> IsDarkMode();\n#endif // Qt < 6.5.0\n\nnamespace ThirdParty {\n\nvoid start();\nvoid finish();\n\n} // namespace ThirdParty\n} // namespace Platform\n\n#ifdef Q_OS_WIN\n#include \"platform/win/specific_win.h\"\n#elif defined Q_OS_MAC // Q_OS_WIN\n#include \"platform/mac/specific_mac.h\"\n#else // Q_OS_WIN || Q_OS_MAC\n#include \"platform/linux/specific_linux.h\"\n#endif // else for Q_OS_WIN || Q_OS_MAC\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_text_recognition.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Platform {\nnamespace TextRecognition {\n\nstruct RectWithText {\n\tQString text;\n\tQRect rect;\n};\n\nstruct Result {\n\tstd::vector<RectWithText> items;\n\tbool success = false;\n\n\tinline operator bool() const {\n\t\treturn success;\n\t}\n};\n\n[[nodiscard]] bool IsAvailable();\n[[nodiscard]] Result RecognizeText(const QImage &image);\n\n} // namespace TextRecognition\n} // namespace Platform\n\n// Platform dependent implementations.\n\n#if defined Q_OS_WINRT || defined Q_OS_WIN\n#include \"platform/win/text_recognition_win.h\"\n#elif defined Q_OS_MAC // Q_OS_WINRT || Q_OS_WIN\n#include \"platform/mac/text_recognition_mac.h\"\n#else // Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC\n#include \"platform/linux/text_recognition_linux.h\"\n#endif // else for Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_translate_provider.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"translate_provider.h\"\n\nnamespace Platform {\n\n[[nodiscard]] bool IsTranslateProviderAvailable();\n[[nodiscard]] std::unique_ptr<Ui::TranslateProvider> CreateTranslateProvider();\n\n} // namespace Platform\n\n#if defined Q_OS_WINRT || defined Q_OS_WIN\n#include \"platform/win/translate_provider_win.h\"\n#elif defined Q_OS_MAC\n#include \"platform/mac/translate_provider_mac.h\"\n#else\n#include \"platform/linux/translate_provider_linux.h\"\n#endif\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_tray.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Platform {\n\nclass Tray;\n\n[[nodiscard]] bool HasMonochromeSetting();\n\n} // namespace Platform\n\n// Platform dependent implementations.\n\n#ifdef Q_OS_WIN\n#include \"platform/win/tray_win.h\"\n#elif defined Q_OS_MAC // Q_OS_WIN\n#include \"platform/mac/tray_mac.h\"\n#else // Q_OS_WIN || Q_OS_MAC\n#include \"platform/linux/tray_linux.h\"\n#endif // else for Q_OS_WIN || Q_OS_MAC\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_webauthn.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\n#pragma once\n\nnamespace Data::Passkey {\nstruct RegisterData;\nstruct LoginData;\n} // namespace Data::Passkey\n\nnamespace Platform::WebAuthn {\n\nenum class Error {\n\tNone,\n\tCancelled,\n\tUnsignedBuild,\n\tOther,\n};\n\nstruct LoginResult {\n\tQByteArray clientDataJSON;\n\tQByteArray credentialId;\n\tQByteArray authenticatorData;\n\tQByteArray signature;\n\tQByteArray userHandle;\n\tError error = Error::None;\n};\n\nstruct RegisterResult {\n\tQByteArray credentialId;\n\tQByteArray attestationObject;\n\tQByteArray clientDataJSON;\n\tbool success = false;\n\tError error = Error::None;\n};\n\n[[nodiscard]] bool IsSupported();\nvoid RegisterKey(\n\tconst Data::Passkey::RegisterData &data,\n\tFn<void(RegisterResult result)> callback);\nvoid Login(\n\tconst Data::Passkey::LoginData &data,\n\tFn<void(LoginResult result)> callback);\n\n} // namespace Platform::WebAuthn\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/platform_window_title.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"window/themes/window_theme_preview.h\"\n#include \"base/platform/base_platform_info.h\"\n\nnamespace Platform {\n\ninline bool NativeTitleRequiresShadow() {\n\treturn Platform::IsWindows();\n}\n\nint PreviewTitleHeight();\nvoid PreviewWindowFramePaint(QImage &preview, const style::palette &palette, QRect body, int outerWidth);\n\n} // namespace Platform\n\n// Platform dependent implementations.\n\n#ifndef Q_OS_MAC\n\nnamespace Platform {\n\ninline int PreviewTitleHeight() {\n\treturn Window::Theme::DefaultPreviewTitleHeight();\n}\n\ninline void PreviewWindowFramePaint(QImage &preview, const style::palette &palette, QRect body, int outerWidth) {\n\treturn Window::Theme::DefaultPreviewWindowFramePaint(preview, palette, body, outerWidth);\n}\n\n} // namespace Platform\n\n#endif // !Q_OS_MAC\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/current_geo_location_win.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/win/current_geo_location_win.h\"\n\n#include \"base/platform/win/base_windows_winrt.h\"\n#include \"core/current_geo_location.h\"\n\n#include <winrt/Windows.Devices.Geolocation.h>\n#include <winrt/Windows.Foundation.h>\n\n#include <winrt/Windows.Services.Maps.h>\n#include <winrt/Windows.Foundation.Collections.h>\n\nnamespace Platform {\n\nvoid ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {\n\tusing namespace winrt::Windows::Foundation;\n\tusing namespace winrt::Windows::Devices::Geolocation;\n\n\tconst auto success = base::WinRT::Try([&] {\n\t\tGeolocator geolocator;\n\t\tgeolocator.DesiredAccuracy(PositionAccuracy::High);\n\t\tif (geolocator.LocationStatus() == PositionStatus::NotAvailable) {\n\t\t\tcallback({});\n\t\t\treturn;\n\t\t}\n\t\tgeolocator.GetGeopositionAsync().Completed([=](\n\t\t\t\tIAsyncOperation<Geoposition> that,\n\t\t\t\tAsyncStatus status) {\n\t\t\tif (status != AsyncStatus::Completed) {\n\t\t\t\tcrl::on_main([=] {\n\t\t\t\t\tcallback({});\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto point = base::WinRT::Try([&] {\n\t\t\t\tconst auto coordinate = that.GetResults().Coordinate();\n\t\t\t\treturn coordinate.Point().Position();\n\t\t\t});\n\t\t\tcrl::on_main([=] {\n\t\t\t\tif (!point) {\n\t\t\t\t\tcallback({});\n\t\t\t\t} else {\n\t\t\t\t\tcallback({\n\t\t\t\t\t\t.point = { point->Latitude, point->Longitude },\n\t\t\t\t\t\t.accuracy = Core::GeoLocationAccuracy::Exact,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t});\n\tif (!success) {\n\t\tcallback({});\n\t}\n}\n\nvoid ResolveLocationAddress(\n\t\tconst Core::GeoLocation &location,\n\t\tconst QString &language,\n\t\tFn<void(Core::GeoAddress)> callback) {\n\tcallback({});\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/current_geo_location_win.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_current_geo_location.h\"\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/file_utilities_win.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/win/file_utilities_win.h\"\n\n#include \"mainwindow.h\"\n#include \"storage/localstorage.h\"\n#include \"platform/win/windows_dlls.h\"\n#include \"lang/lang_keys.h\"\n#include \"core/application.h\"\n#include \"core/crash_reports.h\"\n#include \"window/window_controller.h\"\n#include \"ui/ui_utility.h\"\n\n#include <QtWidgets/QFileDialog>\n#include <QtGui/QDesktopServices>\n#include <QtCore/QSettings>\n#include <QtCore/QStandardPaths>\n\n#include <Shlwapi.h>\n#include <Windowsx.h>\n\nHBITMAP qt_pixmapToWinHBITMAP(const QPixmap &, int hbitmapFormat);\n\nnamespace Platform {\nnamespace File {\nnamespace {\n\nclass OpenWithApp {\npublic:\n\tOpenWithApp(const QString &name, IAssocHandler *handler, HBITMAP icon = nullptr)\n\t\t: _name(name)\n\t\t, _handler(handler)\n\t\t, _icon(icon) {\n\t}\n\tOpenWithApp(OpenWithApp &&other)\n\t\t: _name(base::take(other._name))\n\t\t, _handler(base::take(other._handler))\n\t\t, _icon(base::take(other._icon)) {\n\t}\n\tOpenWithApp &operator=(OpenWithApp &&other) {\n\t\t_name = base::take(other._name);\n\t\t_icon = base::take(other._icon);\n\t\t_handler = base::take(other._handler);\n\t\treturn (*this);\n\t}\n\n\tOpenWithApp(const OpenWithApp &other) = delete;\n\tOpenWithApp &operator=(const OpenWithApp &other) = delete;\n\n\t~OpenWithApp() {\n\t\tif (_icon) {\n\t\t\tDeleteBitmap(_icon);\n\t\t}\n\t\tif (_handler) {\n\t\t\t_handler->Release();\n\t\t}\n\t}\n\n\tconst QString &name() const {\n\t\treturn _name;\n\t}\n\tHBITMAP icon() const {\n\t\treturn _icon;\n\t}\n\tIAssocHandler *handler() const {\n\t\treturn _handler;\n\t}\n\nprivate:\n\tQString _name;\n\tIAssocHandler *_handler = nullptr;\n\tHBITMAP _icon = nullptr;\n\n};\n\nHBITMAP IconToBitmap(LPWSTR icon, int iconindex) {\n\tif (!icon) return 0;\n\tWCHAR tmpIcon[4096];\n\tif (icon[0] == L'@' && SUCCEEDED(SHLoadIndirectString(icon, tmpIcon, 4096, 0))) {\n\t\ticon = tmpIcon;\n\t}\n\tint32 w = GetSystemMetrics(SM_CXSMICON), h = GetSystemMetrics(SM_CYSMICON);\n\n\tHICON ico = ExtractIcon(0, icon, iconindex);\n\tif (!ico) {\n\t\tif (!iconindex) { // try to read image\n\t\t\tQImage img(QString::fromWCharArray(icon));\n\t\t\tif (!img.isNull()) {\n\t\t\t\treturn qt_pixmapToWinHBITMAP(\n\t\t\t\t\tUi::PixmapFromImage(\n\t\t\t\t\t\timg.scaled(\n\t\t\t\t\t\t\tw,\n\t\t\t\t\t\t\th,\n\t\t\t\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\t\t\t\tQt::SmoothTransformation)),\n\t\t\t\t\t/* HBitmapAlpha */ 2);\n\t\t\t}\n\t\t}\n\t\treturn 0;\n\t}\n\n\tHDC screenDC = GetDC(0), hdc = CreateCompatibleDC(screenDC);\n\tHBITMAP result = CreateCompatibleBitmap(screenDC, w, h);\n\tHGDIOBJ was = SelectObject(hdc, result);\n\tDrawIconEx(hdc, 0, 0, ico, w, h, 0, NULL, DI_NORMAL);\n\tSelectObject(hdc, was);\n\tDeleteDC(hdc);\n\tReleaseDC(0, screenDC);\n\n\tDestroyIcon(ico);\n\n\treturn (HBITMAP)CopyImage(result, IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE | LR_CREATEDIBSECTION);\n}\n\nbool ShouldSaveZoneInformation() {\n\t// Check if the \"Do not preserve zone information in file attachments\" policy is enabled.\n\tconst auto keyName = L\"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\Attachments\";\n\tconst auto valueName = L\"SaveZoneInformation\";\n\tauto key = HKEY();\n\tauto result = RegOpenKeyEx(HKEY_CURRENT_USER, keyName, 0, KEY_READ, &key);\n\tif (result != ERROR_SUCCESS) {\n\t\t// If the registry key cannot be opened, assume the default behavior:\n\t\t// Windows preserves zone information for downloaded files.\n\t\treturn true;\n\t}\n\n\tDWORD value = 0, type = 0, size = sizeof(value);\n\tresult = RegQueryValueEx(key, valueName, 0, &type, (LPBYTE)&value, &size);\n\tRegCloseKey(key);\n\n\tif (result != ERROR_SUCCESS || type != REG_DWORD) {\n\t\treturn true;\n\t}\n\n\treturn (value != 1);\n}\n} // namespace\n\nvoid UnsafeOpenEmailLink(const QString &email) {\n\tauto url = QUrl(qstr(\"mailto:\") + email);\n\tif (!QDesktopServices::openUrl(url)) {\n\t\tauto wstringUrl = url.toString(QUrl::FullyEncoded).toStdWString();\n\t\tif (Dlls::SHOpenWithDialog) {\n\t\t\tOPENASINFO info;\n\t\t\tinfo.oaifInFlags = OAIF_ALLOW_REGISTRATION\n\t\t\t\t| OAIF_REGISTER_EXT\n\t\t\t\t| OAIF_EXEC\n#if WINVER >= 0x0602\n\t\t\t\t| OAIF_FILE_IS_URI\n#endif // WINVER >= 0x602\n\t\t\t\t| OAIF_URL_PROTOCOL;\n\t\t\tinfo.pcszClass = NULL;\n\t\t\tinfo.pcszFile = wstringUrl.c_str();\n\t\t\tDlls::SHOpenWithDialog(0, &info);\n\t\t} else if (Dlls::OpenAs_RunDLL) {\n\t\t\tDlls::OpenAs_RunDLL(0, 0, wstringUrl.c_str(), SW_SHOWNORMAL);\n\t\t} else {\n\t\t\tShellExecute(0, L\"open\", wstringUrl.c_str(), 0, 0, SW_SHOWNORMAL);\n\t\t}\n\t}\n}\n\nbool UnsafeShowOpenWithDropdown(const QString &filepath) {\n\tif (!Dlls::SHAssocEnumHandlers || !Dlls::SHCreateItemFromParsingName) {\n\t\treturn false;\n\t}\n\n\tauto window = Core::App().activeWindow();\n\tif (!window) {\n\t\treturn false;\n\t}\n\n\tauto parentHWND = window->widget()->psHwnd();\n\tauto wstringPath = QDir::toNativeSeparators(filepath).toStdWString();\n\n\tauto result = false;\n\tstd::vector<OpenWithApp> handlers;\n\tIShellItem* pItem = nullptr;\n\tif (SUCCEEDED(Dlls::SHCreateItemFromParsingName(wstringPath.c_str(), nullptr, IID_PPV_ARGS(&pItem)))) {\n\t\tIEnumAssocHandlers *assocHandlers = nullptr;\n\t\tif (SUCCEEDED(pItem->BindToHandler(nullptr, BHID_EnumAssocHandlers, IID_PPV_ARGS(&assocHandlers)))) {\n\t\t\tHRESULT hr = S_FALSE;\n\t\t\tdo {\n\t\t\t\tIAssocHandler *handler = nullptr;\n\t\t\t\tULONG ulFetched = 0;\n\t\t\t\thr = assocHandlers->Next(1, &handler, &ulFetched);\n\t\t\t\tif (FAILED(hr) || hr == S_FALSE || !ulFetched) break;\n\n\t\t\t\tLPWSTR name = 0;\n\t\t\t\tif (SUCCEEDED(handler->GetUIName(&name))) {\n\t\t\t\t\tLPWSTR icon = 0;\n\t\t\t\t\tint iconindex = 0;\n\t\t\t\t\tif (SUCCEEDED(handler->GetIconLocation(&icon, &iconindex)) && icon) {\n\t\t\t\t\t\thandlers.push_back(OpenWithApp(QString::fromWCharArray(name), handler, IconToBitmap(icon, iconindex)));\n\t\t\t\t\t\tCoTaskMemFree(icon);\n\t\t\t\t\t} else {\n\t\t\t\t\t\thandlers.push_back(OpenWithApp(QString::fromWCharArray(name), handler));\n\t\t\t\t\t}\n\t\t\t\t\tCoTaskMemFree(name);\n\t\t\t\t} else {\n\t\t\t\t\thandler->Release();\n\t\t\t\t}\n\t\t\t} while (hr != S_FALSE);\n\t\t\tassocHandlers->Release();\n\t\t}\n\n\t\tif (!handlers.empty()) {\n\t\t\tHMENU menu = CreatePopupMenu();\n\t\t\tranges::sort(handlers, [](const OpenWithApp &a, auto &b) {\n\t\t\t\treturn a.name() < b.name();\n\t\t\t});\n\t\t\tfor (int32 i = 0, l = handlers.size(); i < l; ++i) {\n\t\t\t\tMENUITEMINFO menuInfo = { 0 };\n\t\t\t\tmenuInfo.cbSize = sizeof(menuInfo);\n\t\t\t\tmenuInfo.fMask = MIIM_STRING | MIIM_DATA | MIIM_ID;\n\t\t\t\tmenuInfo.fType = MFT_STRING;\n\t\t\t\tmenuInfo.wID = i + 1;\n\t\t\t\tif (auto icon = handlers[i].icon()) {\n\t\t\t\t\tmenuInfo.fMask |= MIIM_BITMAP;\n\t\t\t\t\tmenuInfo.hbmpItem = icon;\n\t\t\t\t}\n\n\t\t\t\tauto name = handlers[i].name();\n\t\t\t\tif (name.size() > 512) name = name.mid(0, 512);\n\t\t\t\tWCHAR nameArr[1024];\n\t\t\t\tname.toWCharArray(nameArr);\n\t\t\t\tnameArr[name.size()] = 0;\n\t\t\t\tmenuInfo.dwTypeData = nameArr;\n\t\t\t\tInsertMenuItem(menu, GetMenuItemCount(menu), TRUE, &menuInfo);\n\t\t\t}\n\t\t\tMENUITEMINFO sepInfo = { 0 };\n\t\t\tsepInfo.cbSize = sizeof(sepInfo);\n\t\t\tsepInfo.fMask = MIIM_STRING | MIIM_DATA;\n\t\t\tsepInfo.fType = MFT_SEPARATOR;\n\t\t\tInsertMenuItem(menu, GetMenuItemCount(menu), true, &sepInfo);\n\n\t\t\tMENUITEMINFO menuInfo = { 0 };\n\t\t\tmenuInfo.cbSize = sizeof(menuInfo);\n\t\t\tmenuInfo.fMask = MIIM_STRING | MIIM_DATA | MIIM_ID;\n\t\t\tmenuInfo.fType = MFT_STRING;\n\t\t\tmenuInfo.wID = handlers.size() + 1;\n\n\t\t\tQString name = tr::lng_wnd_choose_program_menu(tr::now);\n\t\t\tif (name.size() > 512) name = name.mid(0, 512);\n\t\t\tWCHAR nameArr[1024];\n\t\t\tname.toWCharArray(nameArr);\n\t\t\tnameArr[name.size()] = 0;\n\t\t\tmenuInfo.dwTypeData = nameArr;\n\t\t\tInsertMenuItem(menu, GetMenuItemCount(menu), TRUE, &menuInfo);\n\n\t\t\tPOINT position;\n\t\t\tGetCursorPos(&position);\n\t\t\tint sel = TrackPopupMenu(menu, TPM_LEFTALIGN | TPM_TOPALIGN | TPM_LEFTBUTTON | TPM_RETURNCMD, position.x, position.y, 0, parentHWND, 0);\n\t\t\tDestroyMenu(menu);\n\n\t\t\tif (sel > 0) {\n\t\t\t\tif (sel <= handlers.size()) {\n\t\t\t\t\tIDataObject *dataObj = 0;\n\t\t\t\t\tif (SUCCEEDED(pItem->BindToHandler(nullptr, BHID_DataObject, IID_PPV_ARGS(&dataObj))) && dataObj) {\n\t\t\t\t\t\thandlers[sel - 1].handler()->Invoke(dataObj);\n\t\t\t\t\t\tdataObj->Release();\n\t\t\t\t\t\tresult = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tresult = true;\n\t\t\t}\n\t\t}\n\n\t\tpItem->Release();\n\t}\n\treturn result;\n}\n\nbool UnsafeShowOpenWith(const QString &filepath) {\n\tauto wstringPath = QDir::toNativeSeparators(filepath).toStdWString();\n\tif (Dlls::SHOpenWithDialog) {\n\t\tOPENASINFO info;\n\t\tinfo.oaifInFlags = OAIF_ALLOW_REGISTRATION | OAIF_REGISTER_EXT | OAIF_EXEC;\n\t\tinfo.pcszClass = NULL;\n\t\tinfo.pcszFile = wstringPath.c_str();\n\t\tDlls::SHOpenWithDialog(0, &info);\n\t\treturn true;\n\t} else if (Dlls::OpenAs_RunDLL) {\n\t\tDlls::OpenAs_RunDLL(0, 0, wstringPath.c_str(), SW_SHOWNORMAL);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid UnsafeLaunch(const QString &filepath) {\n\tauto wstringPath = QDir::toNativeSeparators(filepath).toStdWString();\n\tShellExecute(0, L\"open\", wstringPath.c_str(), 0, 0, SW_SHOWNORMAL);\n}\n\nvoid PostprocessDownloaded(const QString &filepath) {\n\t// Mark file saved to the NTFS file system as originating from the Internet security zone\n\t// unless this feature is disabled by Group Policy.\n\tif (!ShouldSaveZoneInformation()) {\n\t\treturn;\n\t}\n\tauto wstringZoneFile = QDir::toNativeSeparators(filepath).toStdWString() + L\":Zone.Identifier\";\n\tauto f = CreateFile(wstringZoneFile.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);\n\tif (f == INVALID_HANDLE_VALUE) { // :(\n\t\treturn;\n\t}\n\n\tconst char data[] = \"[ZoneTransfer]\\r\\nZoneId=3\\r\\n\";\n\n\tDWORD written = 0;\n\tBOOL result = WriteFile(f, data, sizeof(data), &written, NULL);\n\tCloseHandle(f);\n\n\tif (!result || written != sizeof(data)) { // :(\n\t\treturn;\n\t}\n}\n\n} // namespace File\n\nnamespace FileDialog {\nnamespace {\n\nusing Type = ::FileDialog::internal::Type;\n\n} // namespace\n\nvoid InitLastPath() {\n\t// hack to restore previous dir without hurting performance\n\tQSettings settings(QSettings::UserScope, qstr(\"QtProject\"));\n\tsettings.beginGroup(qstr(\"Qt\"));\n\tQByteArray sd = settings.value(qstr(\"filedialog\")).toByteArray();\n\tQDataStream stream(&sd, QIODevice::ReadOnly);\n\tif (!stream.atEnd()) {\n\t\tint version = 3, _QFileDialogMagic = 190;\n\t\tQByteArray splitterState;\n\t\tQByteArray headerData;\n\t\tQList<QUrl> bookmarks;\n\t\tQStringList history;\n\t\tQString currentDirectory;\n\t\tqint32 marker;\n\t\tqint32 v;\n\t\tqint32 viewMode;\n\t\tstream >> marker;\n\t\tstream >> v;\n\t\tif (marker == _QFileDialogMagic && v == version) {\n\t\t\tstream >> splitterState\n\t\t\t\t>> bookmarks\n\t\t\t\t>> history\n\t\t\t\t>> currentDirectory\n\t\t\t\t>> headerData\n\t\t\t\t>> viewMode;\n\t\t\tcSetDialogLastPath(currentDirectory);\n\t\t}\n\t}\n\n\tif (cDialogHelperPath().isEmpty()) {\n\t\tQDir temppath(cWorkingDir() + \"tdata/tdummy/\");\n\t\tif (!temppath.exists()) {\n\t\t\ttemppath.mkpath(temppath.absolutePath());\n\t\t}\n\t\tif (temppath.exists()) {\n\t\t\tcSetDialogHelperPath(temppath.absolutePath());\n\t\t}\n\t}\n}\n\nbool Get(\n\t\tQPointer<QWidget> parent,\n\t\tQStringList &files,\n\t\tQByteArray &remoteContent,\n\t\tconst QString &caption,\n\t\tconst QString &filter,\n\t\t::FileDialog::internal::Type type,\n\t\tQString startFile) {\n\tif (cDialogLastPath().isEmpty()) {\n\t\tPlatform::FileDialog::InitLastPath();\n\t}\n\n\t// A hack for fast dialog create. There was some huge performance problem\n\t// if we open a file dialog in some folder with a large amount of files.\n\t// Some internal Qt watcher iterated over all of them, querying some information\n\t// that forced file icon and maybe other properties being resolved and this was\n\t// a blocking operation.\n\tauto helperPath = cDialogHelperPathFinal();\n\tQFileDialog dialog(parent, caption, helperPath, filter);\n\n\tdialog.setModal(true);\n\tif (type == Type::ReadFile || type == Type::ReadFiles) {\n\t\tdialog.setFileMode((type == Type::ReadFiles) ? QFileDialog::ExistingFiles : QFileDialog::ExistingFile);\n\t\tdialog.setAcceptMode(QFileDialog::AcceptOpen);\n\t} else if (type == Type::ReadFolder) { // save dir\n\t\tdialog.setAcceptMode(QFileDialog::AcceptOpen);\n\t\tdialog.setFileMode(QFileDialog::Directory);\n\t\tdialog.setOption(QFileDialog::ShowDirsOnly);\n\t} else { // save file\n\t\tdialog.setFileMode(QFileDialog::AnyFile);\n\t\tdialog.setAcceptMode(QFileDialog::AcceptSave);\n\t}\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n\tdialog.show();\n#endif // Qt < 6.0.0\n\n\tauto realLastPath = [=] {\n\t\t// If we're given some non empty path containing a folder - use it.\n\t\tif (!startFile.isEmpty() && (startFile.indexOf('/') >= 0 || startFile.indexOf('\\\\') >= 0)) {\n\t\t\treturn QFileInfo(startFile).dir().absolutePath();\n\t\t}\n\t\treturn cDialogLastPath();\n\t}();\n\tif (realLastPath.isEmpty() || realLastPath.endsWith(qstr(\"/tdummy\"))) {\n\t\trealLastPath = QStandardPaths::writableLocation(\n\t\t\tQStandardPaths::DownloadLocation);\n\t}\n\tdialog.setDirectory(realLastPath);\n\n\tauto toSelect = startFile;\n\tif (type == Type::WriteFile) {\n\t\tconst auto lastSlash = toSelect.lastIndexOf('/');\n\t\tif (lastSlash >= 0) {\n\t\t\ttoSelect = toSelect.mid(lastSlash + 1);\n\t\t}\n\t\tconst auto lastBackSlash = toSelect.lastIndexOf('\\\\');\n\t\tif (lastBackSlash >= 0) {\n\t\t\ttoSelect = toSelect.mid(lastBackSlash + 1);\n\t\t}\n\t\tdialog.selectFile(toSelect);\n\t}\n\n\tCrashReports::SetAnnotation(\n\t\t\"file_dialog\",\n\t\tQString(\"caption:%1;helper:%2;filter:%3;real:%4;select:%5\"\n\t\t).arg(caption\n\t\t).arg(helperPath\n\t\t).arg(filter\n\t\t).arg(realLastPath\n\t\t).arg(toSelect));\n\tconst auto result = dialog.exec();\n\tCrashReports::ClearAnnotation(\"file_dialog\");\n\n\tif (type != Type::ReadFolder) {\n\t\t// Save last used directory for all queries except directory choosing.\n\t\tconst auto path = dialog.directory().absolutePath();\n\t\tif (path != cDialogLastPath()) {\n\t\t\tcSetDialogLastPath(path);\n\t\t\tLocal::writeSettings();\n\t\t}\n\t}\n\n\tif (result == QDialog::Accepted) {\n\t\tif (type == Type::ReadFiles) {\n\t\t\tfiles = dialog.selectedFiles();\n\t\t} else {\n\t\t\tfiles = dialog.selectedFiles().mid(0, 1);\n\t\t}\n\t\t//if (type == Type::ReadFile || type == Type::ReadFiles) {\n\t\t//\tremoteContent = dialog.selectedRemoteContent();\n\t\t//}\n\t\treturn true;\n\t}\n\n\tfiles = QStringList();\n\tremoteContent = QByteArray();\n\treturn false;\n}\n\n} // namespace FileDialog\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/file_utilities_win.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_file_utilities.h\"\n\nnamespace Platform {\nnamespace File {\n\ninline QString UrlToLocal(const QUrl &url) {\n\treturn ::File::internal::UrlToLocalDefault(url);\n}\n\ninline void UnsafeOpenUrl(const QString &url) {\n\treturn ::File::internal::UnsafeOpenUrlDefault(url);\n}\n\n} // namespace File\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/integration_win.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/win/integration_win.h\"\n\n#include \"base/platform/win/base_windows_winrt.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/sandbox.h\"\n#include \"lang/lang_keys.h\"\n#include \"platform/win/windows_app_user_model_id.h\"\n#include \"platform/win/tray_win.h\"\n#include \"platform/platform_integration.h\"\n#include \"platform/platform_specific.h\"\n#include \"tray.h\"\n#include \"styles/style_window.h\"\n\n#include <QtCore/QAbstractNativeEventFilter>\n#include <private/qguiapplication_p.h>\n\n#include <propvarutil.h>\n#include <propkey.h>\n\nnamespace Platform {\n\nvoid WindowsIntegration::init() {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n\tusing namespace QNativeInterface::Private;\n\tconst auto native = qApp->nativeInterface<QWindowsApplication>();\n\tif (native) {\n\t\tnative->setHasBorderInFullScreenDefault(true);\n\t}\n#endif // Qt >= 6.5.0\n\tQCoreApplication::instance()->installNativeEventFilter(this);\n\t_taskbarCreatedMsgId = RegisterWindowMessage(L\"TaskbarButtonCreated\");\n}\n\nITaskbarList3 *WindowsIntegration::taskbarList() const {\n\treturn _taskbarList.get();\n}\n\nWindowsIntegration &WindowsIntegration::Instance() {\n\treturn static_cast<WindowsIntegration&>(Integration::Instance());\n}\n\nbool WindowsIntegration::nativeEventFilter(\n\t\tconst QByteArray &eventType,\n\t\tvoid *message,\n\t\tnative_event_filter_result *result) {\n\treturn Core::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\tconst auto msg = static_cast<MSG*>(message);\n\t\treturn processEvent(\n\t\t\tmsg->hwnd,\n\t\t\tmsg->message,\n\t\t\tmsg->wParam,\n\t\t\tmsg->lParam,\n\t\t\t(LRESULT*)result);\n\t});\n}\n\nvoid WindowsIntegration::createCustomJumpList() {\n\t_jumpList = base::WinRT::TryCreateInstance<ICustomDestinationList>(\n\t\tCLSID_DestinationList);\n\tif (_jumpList) {\n\t\trefreshCustomJumpList();\n\t}\n}\n\nvoid WindowsIntegration::refreshCustomJumpList() {\n\tauto added = false;\n\tauto maxSlots = UINT();\n\tauto removed = (IObjectArray*)nullptr;\n\tauto hr = _jumpList->BeginList(&maxSlots, IID_PPV_ARGS(&removed));\n\tif (!SUCCEEDED(hr)) {\n\t\treturn;\n\t}\n\tconst auto guard = gsl::finally([&] {\n\t\tif (added) {\n\t\t\t_jumpList->CommitList();\n\t\t} else {\n\t\t\t_jumpList->AbortList();\n\t\t}\n\t});\n\n\tauto shellLink = base::WinRT::TryCreateInstance<IShellLink>(\n\t\tCLSID_ShellLink);\n\tif (!shellLink) {\n\t\treturn;\n\t}\n\n\t// Set the path to your application and the command-line argument for quitting\n\tconst auto exe = QDir::toNativeSeparators(cExeDir() + cExeName());\n\tconst auto dir = QDir::toNativeSeparators(QDir(cWorkingDir()).absolutePath());\n\tconst auto icon = Tray::QuitJumpListIconPath();\n\tshellLink->SetArguments(L\"-quit\");\n\tshellLink->SetPath(exe.toStdWString().c_str());\n\tshellLink->SetWorkingDirectory(dir.toStdWString().c_str());\n\tshellLink->SetIconLocation(icon.toStdWString().c_str(), 0);\n\n\tif (const auto propertyStore = shellLink.try_as<IPropertyStore>()) {\n\t\tauto appIdPropVar = PROPVARIANT();\n\t\thr = InitPropVariantFromString(\n\t\t\tAppUserModelId::Id().c_str(),\n\t\t\t&appIdPropVar);\n\t\tif (SUCCEEDED(hr)) {\n\t\t\thr = propertyStore->SetValue(\n\t\t\t\tAppUserModelId::Key(),\n\t\t\t\tappIdPropVar);\n\t\t\tPropVariantClear(&appIdPropVar);\n\t\t}\n\t\tauto titlePropVar = PROPVARIANT();\n\t\thr = InitPropVariantFromString(\n\t\t\ttr::lng_quit_from_tray(tr::now).toStdWString().c_str(),\n\t\t\t&titlePropVar);\n\t\tif (SUCCEEDED(hr)) {\n\t\t\thr = propertyStore->SetValue(PKEY_Title, titlePropVar);\n\t\t\tPropVariantClear(&titlePropVar);\n\t\t}\n\t\tpropertyStore->Commit();\n\t}\n\n\tauto collection = base::WinRT::TryCreateInstance<IObjectCollection>(\n\t\tCLSID_EnumerableObjectCollection);\n\tif (!collection) {\n\t\treturn;\n\t}\n\tcollection->AddObject(shellLink.get());\n\n\t_jumpList->AddUserTasks(collection.get());\n\tadded = true;\n}\n\nbool WindowsIntegration::processEvent(\n\t\tHWND hWnd,\n\t\tUINT msg,\n\t\tWPARAM wParam,\n\t\tLPARAM lParam,\n\t\tLRESULT *result) {\n\tif (msg && msg == _taskbarCreatedMsgId && !_taskbarList) {\n\t\t_taskbarList = base::WinRT::TryCreateInstance<ITaskbarList3>(\n\t\t\tCLSID_TaskbarList,\n\t\t\tCLSCTX_ALL);\n\t\tif (_taskbarList) {\n\t\t\tcreateCustomJumpList();\n\t\t}\n\t}\n\n\tswitch (msg) {\n\tcase WM_ENDSESSION:\n\t\tCore::Quit();\n\t\tbreak;\n\n\tcase WM_TIMECHANGE:\n\t\tCore::App().checkAutoLockIn(100);\n\t\tbreak;\n\n\tcase WM_WTSSESSION_CHANGE:\n\t\tif (wParam == WTS_SESSION_LOGOFF\n\t\t\t|| wParam == WTS_SESSION_LOCK) {\n\t\t\tCore::App().setScreenIsLocked(true);\n\t\t} else if (wParam == WTS_SESSION_LOGON\n\t\t\t|| wParam == WTS_SESSION_UNLOCK) {\n\t\t\tCore::App().setScreenIsLocked(false);\n\t\t}\n\t\tbreak;\n\n\tcase WM_SETTINGCHANGE:\n\t\tRefreshTaskbarThemeValue();\n#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)\n\t\tCore::App().settings().setSystemDarkMode(Platform::IsDarkMode());\n#endif // Qt < 6.5.0\n\t\tCore::App().tray().updateIconCounters();\n\t\tif (_jumpList) {\n\t\t\trefreshCustomJumpList();\n\t\t}\n\t\tbreak;\n\t}\n\treturn false;\n}\n\nstd::unique_ptr<Integration> CreateIntegration() {\n\treturn std::make_unique<WindowsIntegration>();\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/integration_win.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/platform/win/base_windows_shlobj_h.h\"\n#include \"base/platform/win/base_windows_winrt.h\"\n#include \"platform/platform_integration.h\"\n\n#include <QAbstractNativeEventFilter>\n\nnamespace Platform {\n\nclass WindowsIntegration final\n\t: public Integration\n\t, public QAbstractNativeEventFilter {\npublic:\n\tvoid init() override;\n\n\t[[nodiscard]] ITaskbarList3 *taskbarList() const;\n\n\t[[nodiscard]] static WindowsIntegration &Instance();\n\nprivate:\n\tbool nativeEventFilter(\n\t\tconst QByteArray &eventType,\n\t\tvoid *message,\n\t\tnative_event_filter_result *result) override;\n\tbool processEvent(\n\t\tHWND hWnd,\n\t\tUINT msg,\n\t\tWPARAM wParam,\n\t\tLPARAM lParam,\n\t\tLRESULT *result);\n\n\tvoid createCustomJumpList();\n\tvoid refreshCustomJumpList();\n\n\tuint32 _taskbarCreatedMsgId = 0;\n\twinrt::com_ptr<ITaskbarList3> _taskbarList;\n\twinrt::com_ptr<ICustomDestinationList> _jumpList;\n\n};\n\n[[nodiscard]] std::unique_ptr<Integration> CreateIntegration();\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/launcher_win.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/win/launcher_win.h\"\n\n#include \"core/crash_reports.h\"\n#include \"core/update_checker.h\"\n\n#include <windows.h>\n#include <shellapi.h>\n#include <VersionHelpers.h>\n\nnamespace Platform {\n\nLauncher::Launcher(int argc, char *argv[])\n: Core::Launcher(argc, argv) {\n}\n\nstd::optional<QStringList> Launcher::readArgumentsHook(\n\t\tint argc,\n\t\tchar *argv[]) const {\n\tauto count = 0;\n\tif (const auto list = CommandLineToArgvW(GetCommandLine(), &count)) {\n\t\tconst auto guard = gsl::finally([&] { LocalFree(list); });\n\t\tif (count > 0) {\n\t\t\tauto result = QStringList();\n\t\t\tresult.reserve(count);\n\t\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t\tresult.push_back(QString::fromWCharArray(list[i]));\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nbool Launcher::launchUpdater(UpdaterLaunch action) {\n\tif (cExeName().isEmpty()) {\n\t\treturn false;\n\t}\n\n\tconst auto operation = (action == UpdaterLaunch::JustRelaunch)\n\t\t? QString()\n\t\t: (cWriteProtected()\n\t\t\t? u\"runas\"_q\n\t\t\t: QString());\n\tconst auto binaryPath = (action == UpdaterLaunch::JustRelaunch)\n\t\t? (cExeDir() + cExeName())\n\t\t: (cWriteProtected()\n\t\t\t? (cWorkingDir() + u\"tupdates/temp/Updater.exe\"_q)\n\t\t\t: (cExeDir() + u\"Updater.exe\"_q));\n\n\tauto argumentsList = QStringList();\n\tconst auto pushArgument = [&](const QString &argument) {\n\t\targumentsList.push_back(argument.trimmed());\n\t};\n\tif (cLaunchMode() == LaunchModeAutoStart) {\n\t\tpushArgument(u\"-autostart\"_q);\n\t}\n\tif (Logs::DebugEnabled()) {\n\t\tpushArgument(u\"-debug\"_q);\n\t}\n\tif (cStartInTray()) {\n\t\tpushArgument(u\"-startintray\"_q);\n\t}\n\tif (customWorkingDir()) {\n\t\tpushArgument(u\"-workdir\"_q);\n\t\tpushArgument('\"' + cWorkingDir() + '\"');\n\t}\n\tif (cDataFile() != u\"data\"_q) {\n\t\tpushArgument(u\"-key\"_q);\n\t\tpushArgument('\"' + cDataFile() + '\"');\n\t}\n\n\tif (action == UpdaterLaunch::JustRelaunch) {\n\t\tpushArgument(u\"-noupdate\"_q);\n\t\tif (cRestartingToSettings()) {\n\t\t\tpushArgument(u\"-tosettings\"_q);\n\t\t}\n\t} else {\n\t\tpushArgument(u\"-update\"_q);\n\t\tpushArgument(u\"-exename\"_q);\n\t\tpushArgument('\"' + cExeName() + '\"');\n\t\tif (cWriteProtected()) {\n\t\t\tpushArgument(u\"-writeprotected\"_q);\n\t\t\tpushArgument('\"' + cExeDir() + '\"');\n\t\t}\n\t}\n\treturn launch(operation, binaryPath, argumentsList);\n}\n\nbool Launcher::launch(\n\t\tconst QString &operation,\n\t\tconst QString &binaryPath,\n\t\tconst QStringList &argumentsList) {\n\tconst auto convertPath = [](const QString &path) {\n\t\treturn QDir::toNativeSeparators(path).toStdWString();\n\t};\n\tconst auto nativeBinaryPath = convertPath(binaryPath);\n\tconst auto nativeWorkingDir = convertPath(cWorkingDir());\n\tconst auto arguments = argumentsList.join(' ');\n\n\tDEBUG_LOG((\"Application Info: executing %1 %2\"\n\t\t).arg(binaryPath\n\t\t).arg(arguments\n\t\t));\n\n\tLogs::closeMain();\n\tCrashReports::Finish();\n\n\tconst auto hwnd = HWND(0);\n\tconst auto result = ShellExecute(\n\t\thwnd,\n\t\toperation.isEmpty() ? nullptr : operation.toStdWString().c_str(),\n\t\tnativeBinaryPath.c_str(),\n\t\targuments.toStdWString().c_str(),\n\t\tnativeWorkingDir.empty() ? nullptr : nativeWorkingDir.c_str(),\n\t\tSW_SHOWNORMAL);\n\tif (int64(result) < 32) {\n\t\tDEBUG_LOG((\"Application Error: failed to execute %1, working directory: '%2', result: %3\"\n\t\t\t).arg(binaryPath\n\t\t\t).arg(cWorkingDir()\n\t\t\t).arg(int64(result)\n\t\t\t));\n\t\treturn false;\n\t}\n\treturn true;\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/launcher_win.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"core/launcher.h\"\n\nnamespace Platform {\n\nclass Launcher : public Core::Launcher {\npublic:\n\tLauncher(int argc, char *argv[]);\n\nprivate:\n\tstd::optional<QStringList> readArgumentsHook(\n\t\tint argc,\n\t\tchar *argv[]) const override;\n\n\tbool launchUpdater(UpdaterLaunch action) override;\n\n\tbool launch(\n\t\tconst QString &operation,\n\t\tconst QString &binaryPath,\n\t\tconst QStringList &argumentsList);\n\n};\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/main_window_win.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/win/main_window_win.h\"\n\n#include \"styles/style_window.h\"\n#include \"platform/platform_specific.h\"\n#include \"platform/platform_notifications_manager.h\"\n#include \"platform/win/tray_win.h\"\n#include \"platform/win/windows_dlls.h\"\n#include \"platform/win/integration_win.h\"\n#include \"window/notifications_manager.h\"\n#include \"window/window_session_controller.h\"\n#include \"mainwindow.h\"\n#include \"main/main_session.h\"\n#include \"base/crc32hash.h\"\n#include \"base/platform/win/base_windows_wrl.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"core/application.h\"\n#include \"core/sandbox.h\"\n#include \"lang/lang_keys.h\"\n#include \"storage/localstorage.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/window_controller.h\"\n#include \"history/history.h\"\n\n#include <QtWidgets/QStyleFactory>\n#include <QtWidgets/QApplication>\n#include <QtGui/QWindow>\n#include <QtGui/QScreen>\n#include <QtCore/QOperatingSystemVersion>\n\n#include <Shobjidl.h>\n#include <shellapi.h>\n#include <WtsApi32.h>\n#include <dwmapi.h>\n\n#include <windows.ui.viewmanagement.h>\n#include <UIViewSettingsInterop.h>\n\n#include <Windowsx.h>\n#include <VersionHelpers.h>\n\n// Taken from qtbase/src/gui/image/qpixmap_win.cpp\nHICON qt_pixmapToWinHICON(const QPixmap &);\nHBITMAP qt_imageToWinHBITMAP(const QImage &, int hbitmapFormat);\n\nnamespace ViewManagement = ABI::Windows::UI::ViewManagement;\n\nnamespace Platform {\nnamespace {\n\n// Mouse down on tray icon deactivates the application.\n// So there is no way to know for sure if the tray icon was clicked from\n// active application or from inactive application. So we assume that\n// if the application was deactivated less than 0.5s ago, then the tray\n// icon click (both left or right button) was made from the active app.\nconstexpr auto kKeepActiveForTrayIcon = crl::time(500);\n\nusing namespace Microsoft::WRL;\n\n// Taken from qtbase/src/gui/image/qpixmap_win.cpp\nenum HBitmapFormat {\n\tHBitmapNoAlpha,\n\tHBitmapPremultipliedAlpha,\n\tHBitmapAlpha\n};\n\nclass EventFilter final : public QAbstractNativeEventFilter {\npublic:\n\texplicit EventFilter(not_null<MainWindow*> window);\n\nprivate:\n\tbool nativeEventFilter(\n\t\tconst QByteArray &eventType,\n\t\tvoid *message,\n\t\tnative_event_filter_result *result) override;\n\n\tbool mainWindowEvent(\n\t\tHWND hWnd,\n\t\tUINT msg,\n\t\tWPARAM wParam,\n\t\tLPARAM lParam,\n\t\tLRESULT *result);\n\n\tconst not_null<MainWindow*> _window;\n\n};\n\n\n[[nodiscard]] HICON NativeIcon(const QIcon &icon, QSize size) {\n\tif (!icon.isNull()) {\n\t\tconst auto pixmap = icon.pixmap(icon.actualSize(size));\n\t\tif (!pixmap.isNull()) {\n\t\t\treturn qt_pixmapToWinHICON(pixmap);\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nstruct RealSize {\n\tQSize value;\n\tbool maximized = false;\n};\n[[nodiscard]] RealSize DetectRealSize(HWND hwnd) {\n\tauto result = RECT();\n\tauto placement = WINDOWPLACEMENT();\n\tif (!GetWindowPlacement(hwnd, &placement)) {\n\t\treturn {};\n\t} else if (placement.flags & WPF_RESTORETOMAXIMIZED) {\n\t\tconst auto monitor = MonitorFromRect(\n\t\t\t&placement.rcNormalPosition,\n\t\t\tMONITOR_DEFAULTTONULL);\n\t\tif (!monitor) {\n\t\t\treturn {};\n\t\t}\n\t\tauto info = MONITORINFO{ .cbSize = sizeof(MONITORINFO) };\n\t\tif (!GetMonitorInfo(monitor, &info)) {\n\t\t\treturn {};\n\t\t}\n\t\tresult = info.rcWork;\n\t} else {\n\t\tCopyRect(&result, &placement.rcNormalPosition);\n\t}\n\treturn {\n\t\t{ int(result.right - result.left), int(result.bottom - result.top) },\n\t\t((placement.flags & WPF_RESTORETOMAXIMIZED) != 0)\n\t};\n}\n\n[[nodiscard]] QImage PrepareLogoPreview(\n\t\tQSize size,\n\t\tQImage::Format format,\n\t\tint radius = 0) {\n\tauto result = QImage(size, QImage::Format_RGB32);\n\tresult.fill(st::windowBg->c);\n\n\tconst auto logo = Window::Logo();\n\tconst auto width = size.width();\n\tconst auto height = size.height();\n\tconst auto side = logo.width();\n\tconst auto skip = width / 8;\n\tconst auto use = std::min({ width - skip, height - skip, side });\n\tauto p = QPainter(&result);\n\tif (use == side) {\n\t\tp.drawImage((width - side) / 2, (height - side) / 2, logo);\n\t} else {\n\t\tconst auto scaled = logo.scaled(\n\t\t\tuse,\n\t\t\tuse,\n\t\t\tQt::KeepAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t\tp.drawImage((width - use) / 2, (height - use) / 2, scaled);\n\t}\n\tp.end();\n\n\treturn radius\n\t\t? Images::Round(std::move(result), Images::CornersMask(radius))\n\t\t: result;\n}\n\nEventFilter::EventFilter(not_null<MainWindow*> window) : _window(window) {\n}\n\nbool EventFilter::nativeEventFilter(\n\t\tconst QByteArray &eventType,\n\t\tvoid *message,\n\t\tnative_event_filter_result *result) {\n\treturn Core::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\tconst auto msg = static_cast<MSG*>(message);\n\t\tif (msg->hwnd == _window->psHwnd()\n\t\t\t|| msg->hwnd && !_window->psHwnd()) {\n\t\t\treturn mainWindowEvent(\n\t\t\t\tmsg->hwnd,\n\t\t\t\tmsg->message,\n\t\t\t\tmsg->wParam,\n\t\t\t\tmsg->lParam,\n\t\t\t\t(LRESULT*)result);\n\t\t}\n\t\treturn false;\n\t});\n}\n\nbool EventFilter::mainWindowEvent(\n\t\tHWND hWnd,\n\t\tUINT msg,\n\t\tWPARAM wParam,\n\t\tLPARAM lParam,\n\t\tLRESULT *result) {\n\tswitch (msg) {\n\n\tcase WM_DESTROY: {\n\t\t_window->destroyedFromSystem();\n\t} return false;\n\n\tcase WM_ACTIVATE: {\n\t\tif (LOWORD(wParam) != WA_INACTIVE) {\n\t\t\t_window->shadowsActivate();\n\t\t} else {\n\t\t\t_window->shadowsDeactivate();\n\t\t}\n\t} return false;\n\n\tcase WM_SIZE: {\n\t\tif (wParam == SIZE_MAXIMIZED || wParam == SIZE_RESTORED || wParam == SIZE_MINIMIZED) {\n\t\t\tif (wParam == SIZE_RESTORED && _window->windowState() == Qt::WindowNoState) {\n\t\t\t\t_window->positionUpdated();\n\t\t\t}\n\t\t}\n\t} return false;\n\n\tcase WM_MOVE: {\n\t\t_window->positionUpdated();\n\t} return false;\n\n\tcase WM_DWMSENDICONICTHUMBNAIL: {\n\t\tif (!Core::App().passcodeLocked()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto size = QSize(int(HIWORD(lParam)), int(LOWORD(lParam)));\n\t\treturn _window->setDwmThumbnail(size);\n\t}\n\n\tcase WM_DWMSENDICONICLIVEPREVIEWBITMAP: {\n\t\tif (!Core::App().passcodeLocked()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto size = DetectRealSize(hWnd);\n\t\tconst auto radius = size.maximized ? 0 : style::ConvertScale(8);\n\t\treturn _window->setDwmPreview(size.value, radius);\n\t}\n\n\t}\n\treturn false;\n}\n\n[[nodiscard]] QString HumanReadableDisplayName(HMONITOR hMonitor) {\n\t// https://github.com/qt/qtbase/commit/6136b92f540c15835e0c7eb7e01ab7b58fbea685\n\tauto monitorInfo = MONITORINFOEX{};\n\tmonitorInfo.cbSize = sizeof(MONITORINFOEX);\n\tif (!GetMonitorInfo(hMonitor, &monitorInfo)) {\n\t\treturn QString();\n\t}\n\n\t// Try Display Configuration API first (Windows 7+).\n\tauto numPathArrayElements = UINT32(0);\n\tauto numModeInfoArrayElements = UINT32(0);\n\tif (GetDisplayConfigBufferSizes(\n\t\t\tQDC_ONLY_ACTIVE_PATHS,\n\t\t\t&numPathArrayElements,\n\t\t\t&numModeInfoArrayElements) != ERROR_SUCCESS) {\n\t\treturn QString();\n\t}\n\n\tauto pathInfos = std::vector<DISPLAYCONFIG_PATH_INFO>(\n\t\tnumPathArrayElements);\n\tauto modeInfos = std::vector<DISPLAYCONFIG_MODE_INFO>(\n\t\tnumModeInfoArrayElements);\n\n\tif (QueryDisplayConfig(\n\t\t\tQDC_ONLY_ACTIVE_PATHS,\n\t\t\t&numPathArrayElements,\n\t\t\tpathInfos.data(),\n\t\t\t&numModeInfoArrayElements,\n\t\t\tmodeInfos.data(),\n\t\t\tnullptr) != ERROR_SUCCESS) {\n\t\treturn QString();\n\t}\n\n\t// Find matching path.\n\tfor (const auto &path : pathInfos) {\n\t\tauto deviceName = DISPLAYCONFIG_SOURCE_DEVICE_NAME{};\n\t\tdeviceName.header.type = static_cast<DISPLAYCONFIG_DEVICE_INFO_TYPE>(\n\t\t\tDISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME);\n\t\tdeviceName.header.size = sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME);\n\t\tdeviceName.header.adapterId = path.sourceInfo.adapterId;\n\t\tdeviceName.header.id = path.sourceInfo.id;\n\n\t\tif (DisplayConfigGetDeviceInfo(&deviceName.header) != ERROR_SUCCESS\n\t\t\t|| wcscmp(\n\t\t\t\tmonitorInfo.szDevice,\n\t\t\t\tdeviceName.viewGdiDeviceName) != 0) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto targetName = DISPLAYCONFIG_TARGET_DEVICE_NAME{};\n\t\ttargetName.header.type = static_cast<DISPLAYCONFIG_DEVICE_INFO_TYPE>(\n\t\t\tDISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME);\n\t\ttargetName.header.size = sizeof(DISPLAYCONFIG_TARGET_DEVICE_NAME);\n\t\ttargetName.header.adapterId = path.targetInfo.adapterId;\n\t\ttargetName.header.id = path.targetInfo.id;\n\n\t\tif (DisplayConfigGetDeviceInfo(&targetName.header) == ERROR_SUCCESS) {\n\t\t\tconst auto friendlyName = QString::fromWCharArray(\n\t\t\t\ttargetName.monitorFriendlyDeviceName);\n\t\t\tif (!friendlyName.isEmpty()) {\n\t\t\t\treturn friendlyName;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fallback to legacy method.\n\tauto displayDevice = DISPLAY_DEVICE{};\n\tdisplayDevice.cb = sizeof(DISPLAY_DEVICE);\n\tfor (auto deviceIndex = DWORD(0);\n\t\t\tEnumDisplayDevices(\n\t\t\t\tmonitorInfo.szDevice,\n\t\t\t\tdeviceIndex,\n\t\t\t\t&displayDevice,\n\t\t\t\t0);\n\t\t\tdeviceIndex++) {\n\t\tif (!(displayDevice.StateFlags & DISPLAY_DEVICE_ACTIVE)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto deviceName = QString::fromWCharArray(\n\t\t\tdisplayDevice.DeviceString);\n\t\tif (!deviceName.isEmpty()\n\t\t\t&& deviceName != u\"Generic PnP Monitor\"_q) {\n\t\t\treturn deviceName;\n\t\t}\n\t\tbreak;\n\t}\n\n\treturn QString();\n}\n\n} // namespace\n\nstruct MainWindow::Private {\n\texplicit Private(not_null<MainWindow*> window) : filter(window) {\n\t}\n\n\tEventFilter filter;\n\tComPtr<ViewManagement::IUIViewSettings> viewSettings;\n};\n\nMainWindow::BitmapPointer::BitmapPointer(HBITMAP value) : _value(value) {\n}\n\nMainWindow::BitmapPointer::BitmapPointer(BitmapPointer &&other)\n: _value(base::take(other._value)) {\n}\n\nMainWindow::BitmapPointer &MainWindow::BitmapPointer::operator=(\n\t\tBitmapPointer &&other) {\n\tif (_value != other._value) {\n\t\treset();\n\t\t_value = base::take(other._value);\n\t}\n\treturn *this;\n}\n\nMainWindow::BitmapPointer::~BitmapPointer() {\n\treset();\n}\n\nHBITMAP MainWindow::BitmapPointer::get() const {\n\treturn _value;\n}\n\nMainWindow::BitmapPointer::operator bool() const {\n\treturn _value != nullptr;\n}\n\nvoid MainWindow::BitmapPointer::release() {\n\t_value = nullptr;\n}\n\nvoid MainWindow::BitmapPointer::reset(HBITMAP value) {\n\tif (_value != value) {\n\t\tif (const auto old = std::exchange(_value, value)) {\n\t\t\tDeleteObject(old);\n\t\t}\n\t}\n}\n\nMainWindow::MainWindow(not_null<Window::Controller*> controller)\n: Window::MainWindow(controller)\n, _private(std::make_unique<Private>(this))\n, _taskbarHiderWindow(std::make_unique<QWindow>()) {\n\tqApp->installNativeEventFilter(&_private->filter);\n\n\tsetupNativeWindowFrame();\n\n\tSetWindowPriority(this, controller->isPrimary() ? 2 : 1);\n\n\tusing namespace rpl::mappers;\n\tCore::App().appDeactivatedValue(\n\t) | rpl::distinct_until_changed(\n\t) | rpl::filter(_1) | rpl::on_next([=] {\n\t\t_lastDeactivateTime = crl::now();\n\t}, lifetime());\n\n\tsetupPreviewPasscodeLock();\n}\n\nvoid MainWindow::setupPreviewPasscodeLock() {\n\tCore::App().passcodeLockValue(\n\t) | rpl::on_next([=](bool locked) {\n\t\t// Use iconic bitmap instead of the window content if passcoded.\n\t\tBOOL fForceIconic = locked ? TRUE : FALSE;\n\t\tBOOL fHasIconicBitmap = fForceIconic;\n\t\tDwmSetWindowAttribute(\n\t\t\t_hWnd,\n\t\t\tDWMWA_FORCE_ICONIC_REPRESENTATION,\n\t\t\t&fForceIconic,\n\t\t\tsizeof(fForceIconic));\n\t\tDwmSetWindowAttribute(\n\t\t\t_hWnd,\n\t\t\tDWMWA_HAS_ICONIC_BITMAP,\n\t\t\t&fHasIconicBitmap,\n\t\t\tsizeof(fHasIconicBitmap));\n\t}, lifetime());\n}\n\nvoid MainWindow::setupNativeWindowFrame() {\n\tauto nativeFrame = rpl::single(\n\t\tCore::App().settings().nativeWindowFrame()\n\t) | rpl::then(\n\t\tCore::App().settings().nativeWindowFrameChanges()\n\t);\n\n\trpl::combine(\n\t\tstd::move(nativeFrame),\n\t\tWindow::Theme::IsNightModeValue()\n\t) | rpl::skip(1) | rpl::on_next([=](bool native, bool night) {\n\t\tvalidateWindowTheme(native, night);\n\t}, lifetime());\n}\n\nvoid MainWindow::shadowsActivate() {\n\t_hasActiveFrame = true;\n}\n\nvoid MainWindow::shadowsDeactivate() {\n\t_hasActiveFrame = false;\n}\n\nvoid MainWindow::destroyedFromSystem() {\n\tif (!Core::App().closeNonLastAsync(&controller())) {\n\t\tCore::Quit();\n\t}\n}\n\nbool MainWindow::setDwmThumbnail(QSize size) {\n\tvalidateDwmPreviewColors();\n\tif (size.isEmpty()) {\n\t\treturn false;\n\t} else if (!_dwmThumbnail || _dwmThumbnailSize != size) {\n\t\tconst auto result = PrepareLogoPreview(size, QImage::Format_RGB32);\n\t\tconst auto bitmap = qt_imageToWinHBITMAP(result, HBitmapNoAlpha);\n\t\tif (!bitmap) {\n\t\t\treturn false;\n\t\t}\n\t\t_dwmThumbnail.reset(bitmap);\n\t\t_dwmThumbnailSize = size;\n\t}\n\tDwmSetIconicThumbnail(_hWnd, _dwmThumbnail.get(), NULL);\n\treturn true;\n}\n\nbool MainWindow::setDwmPreview(QSize size, int radius) {\n\tExpects(radius >= 0);\n\n\tvalidateDwmPreviewColors();\n\tif (size.isEmpty()) {\n\t\treturn false;\n\t} else if (!_dwmPreview\n\t\t|| _dwmPreviewSize != size\n\t\t|| _dwmPreviewRadius != radius) {\n\t\tconst auto format = (radius > 0)\n\t\t\t? QImage::Format_ARGB32_Premultiplied\n\t\t\t: QImage::Format_RGB32;\n\t\tconst auto result = PrepareLogoPreview(size, format, radius);\n\t\tconst auto bitmap = qt_imageToWinHBITMAP(\n\t\t\tresult,\n\t\t\t(radius > 0) ? HBitmapPremultipliedAlpha : HBitmapNoAlpha);\n\t\tif (!bitmap) {\n\t\t\treturn false;\n\t\t}\n\t\t_dwmPreview.reset(bitmap);\n\t\t_dwmPreviewRadius = radius;\n\t\t_dwmPreviewSize = size;\n\t}\n\tconst auto flags = 0;\n\tDwmSetIconicLivePreviewBitmap(_hWnd, _dwmPreview.get(), NULL, flags);\n\treturn true;\n}\n\nvoid MainWindow::validateDwmPreviewColors() {\n\tif (_dwmBackground == st::windowBg->c) {\n\t\treturn;\n\t}\n\t_dwmBackground = st::windowBg->c;\n\t_dwmThumbnail.reset();\n\t_dwmPreview.reset();\n}\n\nvoid MainWindow::forceIconRefresh() {\n\tconst auto refresher = std::make_unique<QWidget>(this);\n\trefresher->setWindowFlags(\n\t\tstatic_cast<Qt::WindowFlags>(Qt::Tool) | Qt::FramelessWindowHint);\n\trefresher->setGeometry(x() + 1, y() + 1, 1, 1);\n\tauto palette = refresher->palette();\n\tpalette.setColor(\n\t\tQPalette::Window,\n\t\t(isActiveWindow() ? st::titleBgActive : st::titleBg)->c);\n\trefresher->setPalette(palette);\n\trefresher->show();\n\trefresher->raise();\n\trefresher->activateWindow();\n\n\tupdateTaskbarAndIconCounters();\n}\n\nvoid MainWindow::workmodeUpdated(Core::Settings::WorkMode mode) {\n\tusing WorkMode = Core::Settings::WorkMode;\n\n\tswitch (mode) {\n\tcase WorkMode::WindowAndTray: {\n\t\tHWND psOwner = (HWND)GetWindowLongPtr(_hWnd, GWLP_HWNDPARENT);\n\t\tif (psOwner) {\n\t\t\tSetWindowLongPtr(_hWnd, GWLP_HWNDPARENT, 0);\n\t\t\twindowHandle()->setTransientParent(nullptr);\n\t\t\tforceIconRefresh();\n\t\t}\n\t} break;\n\n\tcase WorkMode::TrayOnly: {\n\t\tHWND psOwner = (HWND)GetWindowLongPtr(_hWnd, GWLP_HWNDPARENT);\n\t\tif (!psOwner) {\n\t\t\tconst auto hwnd = _taskbarHiderWindow->winId();\n\t\t\tSetWindowLongPtr(_hWnd, GWLP_HWNDPARENT, (LONG_PTR)hwnd);\n\t\t\twindowHandle()->setTransientParent(_taskbarHiderWindow.get());\n\t\t}\n\t} break;\n\n\tcase WorkMode::WindowOnly: {\n\t\tHWND psOwner = (HWND)GetWindowLongPtr(_hWnd, GWLP_HWNDPARENT);\n\t\tif (psOwner) {\n\t\t\tSetWindowLongPtr(_hWnd, GWLP_HWNDPARENT, 0);\n\t\t\twindowHandle()->setTransientParent(nullptr);\n\t\t\tforceIconRefresh();\n\t\t}\n\t} break;\n\t}\n}\n\nbool MainWindow::hasTabletView() const {\n\tif (!_private->viewSettings) {\n\t\treturn false;\n\t}\n\tauto mode = ViewManagement::UserInteractionMode();\n\t_private->viewSettings->get_UserInteractionMode(&mode);\n\treturn (mode == ViewManagement::UserInteractionMode_Touch);\n}\n\nbool MainWindow::initGeometryFromSystem() {\n\tif (!hasTabletView() || !screen()) {\n\t\treturn false;\n\t}\n\tUi::RpWidget::setGeometry(screen()->availableGeometry());\n\treturn true;\n}\n\nbool MainWindow::nativeEvent(\n\t\tconst QByteArray &eventType,\n\t\tvoid *message,\n\t\tnative_event_filter_result *result) {\n\tif (message) {\n\t\tconst auto msg = static_cast<MSG*>(message);\n\t\tif (msg->message == WM_IME_STARTCOMPOSITION) {\n\t\t\tCore::Sandbox::Instance().customEnterFromEventLoop([&] {\n\t\t\t\timeCompositionStartReceived();\n\t\t\t});\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid MainWindow::updateWindowIcon() {\n\tupdateTaskbarAndIconCounters();\n}\n\nbool MainWindow::isActiveForTrayMenu() {\n\treturn !_lastDeactivateTime\n\t\t|| (_lastDeactivateTime + kKeepActiveForTrayIcon >= crl::now());\n}\n\nvoid MainWindow::unreadCounterChangedHook() {\n\tupdateTaskbarAndIconCounters();\n}\n\nvoid MainWindow::updateTaskbarAndIconCounters() {\n\tconst auto counter = Core::App().unreadBadge();\n\tconst auto muted = Core::App().unreadBadgeMuted();\n\tconst auto controller = sessionController();\n\tconst auto session = controller ? &controller->session() : nullptr;\n\n\tconst auto iconSizeSmall = QSize(\n\t\tGetSystemMetrics(SM_CXSMICON),\n\t\tGetSystemMetrics(SM_CYSMICON));\n\tconst auto iconSizeBig = QSize(\n\t\tGetSystemMetrics(SM_CXICON),\n\t\tGetSystemMetrics(SM_CYICON));\n\tconst auto supportMode = session && session->supportMode();\n\n\tauto iconSmallPixmap16 = Tray::IconWithCounter(\n\t\tTray::CounterLayerArgs(16, counter, muted),\n\t\ttrue,\n\t\tfalse,\n\t\tsupportMode);\n\tauto iconSmallPixmap32 = Tray::IconWithCounter(\n\t\tTray::CounterLayerArgs(32, counter, muted),\n\t\ttrue,\n\t\tfalse,\n\t\tsupportMode);\n\tQIcon iconSmall, iconBig;\n\ticonSmall.addPixmap(iconSmallPixmap16);\n\ticonSmall.addPixmap(iconSmallPixmap32);\n\tconst auto integration = &Platform::WindowsIntegration::Instance();\n\tconst auto taskbarList = integration->taskbarList();\n\tconst auto bigCounter = taskbarList ? 0 : counter;\n\ticonBig.addPixmap(Tray::IconWithCounter(\n\t\tTray::CounterLayerArgs(32, bigCounter, muted),\n\t\tfalse,\n\t\tfalse,\n\t\tsupportMode));\n\ticonBig.addPixmap(Tray::IconWithCounter(\n\t\tTray::CounterLayerArgs(64, bigCounter, muted),\n\t\tfalse,\n\t\tfalse,\n\t\tsupportMode));\n\n\tdestroyCachedIcons();\n\t_iconSmall = NativeIcon(iconSmall, iconSizeSmall);\n\t_iconBig = NativeIcon(iconBig, iconSizeBig);\n\tSendMessage(_hWnd, WM_SETICON, ICON_SMALL, (LPARAM)_iconSmall);\n\tSendMessage(_hWnd, WM_SETICON, ICON_BIG, (LPARAM)(_iconBig ? _iconBig : _iconSmall));\n\tif (taskbarList) {\n\t\tif (counter > 0) {\n\t\t\tconst auto pixmap = [&](int size) {\n\t\t\t\treturn Ui::PixmapFromImage(Window::GenerateCounterLayer(\n\t\t\t\t\tTray::CounterLayerArgs(size, counter, muted)));\n\t\t\t};\n\t\t\tQIcon iconOverlay;\n\t\t\ticonOverlay.addPixmap(pixmap(16));\n\t\t\ticonOverlay.addPixmap(pixmap(32));\n\t\t\t_iconOverlay = NativeIcon(iconOverlay, iconSizeSmall);\n\t\t}\n\t\tconst auto description = (counter > 0)\n\t\t\t? tr::lng_unread_bar(tr::now, lt_count, counter).toStdWString()\n\t\t\t: std::wstring();\n\t\ttaskbarList->SetOverlayIcon(_hWnd, _iconOverlay, description.c_str());\n\t}\n\tSetWindowPos(_hWnd, 0, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);\n}\n\nvoid MainWindow::initHook() {\n\t_hWnd = reinterpret_cast<HWND>(winId());\n\tif (!_hWnd) {\n\t\treturn;\n\t}\n\n\tWTSRegisterSessionNotification(_hWnd, NOTIFY_FOR_THIS_SESSION);\n\n\tusing namespace base::Platform;\n\tauto factory = ComPtr<IUIViewSettingsInterop>();\n\tif (SupportsWRL()) {\n\t\tABI::Windows::Foundation::GetActivationFactory(\n\t\t\tStringReferenceWrapper(\n\t\t\t\tRuntimeClass_Windows_UI_ViewManagement_UIViewSettings).Get(),\n\t\t\t&factory);\n\t\tif (factory) {\n\t\t\t// NB! No such method (or IUIViewSettingsInterop) in C++/WinRT :(\n\t\t\tfactory->GetForWindow(\n\t\t\t\t_hWnd,\n\t\t\t\tIID_PPV_ARGS(&_private->viewSettings));\n\t\t}\n\t}\n\n\tvalidateWindowTheme(\n\t\tCore::App().settings().nativeWindowFrame(),\n\t\tWindow::Theme::IsNightMode());\n}\n\nvoid MainWindow::validateWindowTheme(bool native, bool night) {\n\tif (!IsWindows8OrGreater()) {\n\t\tconst auto empty = native ? nullptr : L\" \";\n\t\tSetWindowTheme(_hWnd, empty, empty);\n\t\tQApplication::setStyle(QStyleFactory::create(u\"Windows\"_q));\n#if 0\n\t} else if (!Core::App().settings().systemDarkMode().has_value()/*\n\t\t|| (!Dlls::AllowDarkModeForApp && !Dlls::SetPreferredAppMode)\n\t\t|| !Dlls::AllowDarkModeForWindow\n\t\t|| !Dlls::RefreshImmersiveColorPolicyState\n\t\t|| !Dlls::FlushMenuThemes*/) {\n\t\treturn;\n#endif\n\t} else if (!native) {\n\t\tSetWindowTheme(_hWnd, nullptr, nullptr);\n\t\treturn;\n\t}\n\n\t// See \"https://github.com/microsoft/terminal/blob/\"\n\t// \"eb480b6bbbd83a2aafbe62992d360838e0ab9da5/\"\n\t// \"src/interactivity/win32/windowtheme.cpp#L43-L63\"\n\n\tauto darkValue = BOOL(night ? TRUE : FALSE);\n\n\tconst auto updateStyle = [&] {\n\t\tstatic const auto kSystemVersion = QOperatingSystemVersion::current();\n\t\tif (kSystemVersion.microVersion() >= 18875 && Dlls::SetWindowCompositionAttribute) {\n\t\t\tDlls::WINDOWCOMPOSITIONATTRIBDATA data = {\n\t\t\t\tDlls::WINDOWCOMPOSITIONATTRIB::WCA_USEDARKMODECOLORS,\n\t\t\t\t&darkValue,\n\t\t\t\tsizeof(darkValue)\n\t\t\t};\n\t\t\tDlls::SetWindowCompositionAttribute(_hWnd, &data);\n\t\t} else if (kSystemVersion.microVersion() >= 17763) {\n\t\t\tstatic const auto kDWMWA_USE_IMMERSIVE_DARK_MODE = (kSystemVersion.microVersion() >= 18985)\n\t\t\t\t? DWORD(20)\n\t\t\t\t: DWORD(19);\n\t\t\tDwmSetWindowAttribute(\n\t\t\t\t_hWnd,\n\t\t\t\tkDWMWA_USE_IMMERSIVE_DARK_MODE,\n\t\t\t\t&darkValue,\n\t\t\t\tsizeof(darkValue));\n\t\t}\n\t};\n\n\tupdateStyle();\n\n\t// See \"https://osdn.net/projects/tortoisesvn/scm/svn/blobs/28812/\"\n\t// \"trunk/src/TortoiseIDiff/MainWindow.cpp\"\n\t//\n\t// But for now it works event with a small part of that.\n\t//\n\n\t//const auto updateWindowTheme = [&] {\n\t//\tconst auto set = [&](LPCWSTR name) {\n\t//\t\treturn SetWindowTheme(_hWnd, name, nullptr);\n\t//\t};\n\t//\tif (!night || FAILED(set(L\"DarkMode_Explorer\"))) {\n\t//\t\tset(L\"Explorer\");\n\t//\t}\n\t//};\n\t//\n\t//if (night) {\n\t//\tif (Dlls::SetPreferredAppMode) {\n\t//\t\tDlls::SetPreferredAppMode(Dlls::PreferredAppMode::AllowDark);\n\t//\t} else {\n\t//\t\tDlls::AllowDarkModeForApp(TRUE);\n\t//\t}\n\t//\tDlls::AllowDarkModeForWindow(_hWnd, TRUE);\n\t//\tupdateWindowTheme();\n\t//\tupdateStyle();\n\t//\tDlls::FlushMenuThemes();\n\t//\tDlls::RefreshImmersiveColorPolicyState();\n\t//} else {\n\t//\tupdateWindowTheme();\n\t//\tDlls::AllowDarkModeForWindow(_hWnd, FALSE);\n\t//\tupdateStyle();\n\t//\tDlls::FlushMenuThemes();\n\t//\tDlls::RefreshImmersiveColorPolicyState();\n\t//\tif (Dlls::SetPreferredAppMode) {\n\t//\t\tDlls::SetPreferredAppMode(Dlls::PreferredAppMode::Default);\n\t//\t} else {\n\t//\t\tDlls::AllowDarkModeForApp(FALSE);\n\t//\t}\n\t//}\n\n\t// Didn't find any other way to definitely repaint with the new style.\n\tSendMessage(_hWnd, WM_NCACTIVATE, _hasActiveFrame ? 0 : 1, 0);\n\tSendMessage(_hWnd, WM_NCACTIVATE, _hasActiveFrame ? 1 : 0, 0);\n}\n\nHWND MainWindow::psHwnd() const {\n\treturn _hWnd;\n}\n\nvoid MainWindow::destroyCachedIcons() {\n\tconst auto destroy = [](HICON &icon) {\n\t\tif (icon) {\n\t\t\tDestroyIcon(icon);\n\t\t\ticon = nullptr;\n\t\t}\n\t};\n\tdestroy(_iconBig);\n\tdestroy(_iconSmall);\n\tdestroy(_iconOverlay);\n}\n\nMainWindow::~MainWindow() {\n\tWTSUnRegisterSessionNotification(_hWnd);\n\t_private->viewSettings.Reset();\n\tdestroyCachedIcons();\n}\n\nint32 ScreenNameChecksum(const QString &name) {\n\tconstexpr int DeviceNameSize = base::array_size(MONITORINFOEX().szDevice);\n\twchar_t buffer[DeviceNameSize] = { 0 };\n\tif (name.size() < DeviceNameSize) {\n\t\tname.toWCharArray(buffer);\n\t} else {\n\t\tmemcpy(buffer, name.toStdWString().data(), sizeof(buffer));\n\t}\n\treturn base::crc32(buffer, sizeof(buffer));\n}\n\nint32 ScreenNameChecksum(const QScreen *screen) {\n\treturn ScreenNameChecksum(screen->name());\n}\n\nQString ScreenDisplayLabel(const QScreen *screen) {\n\tif (!screen) {\n\t\treturn QString();\n\t}\n\n\tconst auto geometry = screen->geometry();\n\tconst auto hMonitor = MonitorFromPoint(\n\t\tPOINT{\n\t\t\tgeometry.x() + geometry.width() / 2,\n\t\t\tgeometry.y() + geometry.height() / 2,\n\t\t},\n\t\tMONITOR_DEFAULTTONEAREST);\n\n\tconst auto displayName = HumanReadableDisplayName(hMonitor);\n\tif (!displayName.isEmpty()) {\n\t\treturn displayName;\n\t}\n\n\tconst auto name = screen->name();\n\tconst auto genericName = u\"\\\\\\\\.\\\\DISPLAY\"_q;\n\tif (name.startsWith(genericName)) {\n\t\tconst auto displayNum = name.mid(genericName.size());\n\t\treturn u\"Display %1\"_q.arg(displayNum);\n\t}\n\n\treturn name;\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/main_window_win.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_main_window.h\"\n#include \"base/flags.h\"\n\n#include <windows.h>\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Platform {\n\nclass MainWindow : public Window::MainWindow {\npublic:\n\texplicit MainWindow(not_null<Window::Controller*> controller);\n\n\tHWND psHwnd() const;\n\n\tvoid updateWindowIcon() override;\n\tbool isActiveForTrayMenu() override;\n\n\t// Custom shadows.\n\tvoid shadowsActivate();\n\tvoid shadowsDeactivate();\n\n\t[[nodiscard]] bool hasTabletView() const;\n\n\tvoid destroyedFromSystem();\n\n\tbool setDwmThumbnail(QSize size);\n\tbool setDwmPreview(QSize size, int radius);\n\n\t~MainWindow();\n\nprotected:\n\tvoid initHook() override;\n\tvoid unreadCounterChangedHook() override;\n\n\tvoid workmodeUpdated(Core::Settings::WorkMode mode) override;\n\n\tbool initGeometryFromSystem() override;\n\n\tbool nativeEvent(\n\t\tconst QByteArray &eventType,\n\t\tvoid *message,\n\t\tnative_event_filter_result *result) override;\n\nprivate:\n\tstruct Private;\n\n\tclass BitmapPointer {\n\tpublic:\n\t\tBitmapPointer(HBITMAP value = nullptr);\n\t\tBitmapPointer(BitmapPointer &&other);\n\t\tBitmapPointer& operator=(BitmapPointer &&other);\n\t\t~BitmapPointer();\n\n\t\t[[nodiscard]] HBITMAP get() const;\n\t\t[[nodiscard]] explicit operator bool() const;\n\n\t\tvoid release();\n\t\tvoid reset(HBITMAP value = nullptr);\n\n\tprivate:\n\t\tHBITMAP _value = nullptr;\n\n\t};\n\n\tvoid setupNativeWindowFrame();\n\tvoid setupPreviewPasscodeLock();\n\tvoid updateTaskbarAndIconCounters();\n\tvoid validateWindowTheme(bool native, bool night);\n\n\tvoid forceIconRefresh();\n\tvoid destroyCachedIcons();\n\tvoid validateDwmPreviewColors();\n\n\tconst std::unique_ptr<Private> _private;\n\tconst std::unique_ptr<QWindow> _taskbarHiderWindow;\n\n\tHWND _hWnd = nullptr;\n\tHICON _iconBig = nullptr;\n\tHICON _iconSmall = nullptr;\n\tHICON _iconOverlay = nullptr;\n\n\tBitmapPointer _dwmThumbnail;\n\tBitmapPointer _dwmPreview;\n\tQSize _dwmThumbnailSize;\n\tQSize _dwmPreviewSize;\n\tQColor _dwmBackground;\n\tint _dwmPreviewRadius = 0;\n\n\t// Workarounds for activation from tray icon.\n\tcrl::time _lastDeactivateTime = 0;\n\n\tbool _hasActiveFrame = false;\n\n};\n\n[[nodiscard]] int32 ScreenNameChecksum(const QString &name);\n[[nodiscard]] int32 ScreenNameChecksum(const QScreen *screen);\n\n[[nodiscard]] QString ScreenDisplayLabel(const QScreen *screen);\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/notifications_manager_win.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/win/notifications_manager_win.h\"\n\n#include \"window/notifications_utilities.h\"\n#include \"window/window_session_controller.h\"\n#include \"base/platform/win/base_windows_co_task_mem.h\"\n#include \"base/platform/win/base_windows_rpcndr_h.h\"\n#include \"base/platform/win/base_windows_winrt.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/platform/win/wrl/wrl_module_h.h\"\n#include \"base/qthelp_url.h\"\n#include \"platform/win/windows_app_user_model_id.h\"\n#include \"platform/win/windows_toast_activator.h\"\n#include \"platform/win/windows_dlls.h\"\n#include \"platform/win/specific_win.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_peer.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mainwindow.h\"\n#include \"windows_quiethours_h.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n\n#include <QtCore/QOperatingSystemVersion>\n\n#include <Shobjidl.h>\n#include <shellapi.h>\n#include <strsafe.h>\n\n#include <winrt/Windows.Foundation.h>\n#include <winrt/Windows.Data.Xml.Dom.h>\n#include <winrt/Windows.UI.Notifications.h>\n\nHICON qt_pixmapToWinHICON(const QPixmap &);\n\nusing namespace winrt::Windows::UI::Notifications;\nusing namespace winrt::Windows::Data::Xml::Dom;\nusing namespace winrt::Windows::Foundation;\nusing winrt::com_ptr;\n\nnamespace Platform {\nnamespace Notifications {\nnamespace {\n\nconstexpr auto kQuerySettingsEachMs = 1000;\n\ncrl::time LastSettingsQueryMs/* = 0*/;\n\n[[nodiscard]] bool ShouldQuerySettings() {\n\tconst auto now = crl::now();\n\tif (LastSettingsQueryMs > 0 && now <= LastSettingsQueryMs + kQuerySettingsEachMs) {\n\t\treturn false;\n\t}\n\tLastSettingsQueryMs = now;\n\treturn true;\n}\n\n[[nodiscard]] std::wstring NotificationTemplate(\n\t\tQString id,\n\t\tWindow::Notifications::Manager::DisplayOptions options) {\n\tconst auto wid = id.replace('&', \"&amp;\").toStdWString();\n\tconst auto fastReply = LR\"(\n\t\t<input id=\"fastReply\" type=\"text\" placeHolderContent=\"\"/>\n\t\t<action\n\t\t\tcontent=\"Send\"\n\t\t\targuments=\"action=reply&amp;)\" + wid + LR\"(\"\n\t\t\tactivationType=\"background\"\n\t\t\timageUri=\"\"\n\t\t\thint-inputId=\"fastReply\"/>\n)\";\n\tconst auto markAsRead = LR\"(\n        <action\n            content=\"\"\n            arguments=\"action=mark&amp;)\" + wid + LR\"(\"\n            activationType=\"background\"/>\n)\";\n\tconst auto actions = (options.hideReplyButton ? L\"\" : fastReply)\n\t\t+ (options.hideMarkAsRead ? L\"\" : markAsRead);\n\treturn LR\"(\n<toast launch=\"action=open&amp;)\" + wid + LR\"(\">\n\t<visual>\n\t\t<binding template=\"ToastGeneric\">\n\t\t\t<image placement=\"appLogoOverride\" hint-crop=\"circle\" src=\"\"/>\n\t\t\t<text hint-maxLines=\"1\"></text>\n\t\t\t<text></text>\n\t\t\t<text></text>\n\t\t</binding>\n\t</visual>\n)\" + (actions.empty()\n\t? L\"\"\n\t: (L\"<actions>\" + actions + L\"</actions>\")) + LR\"(\n\t<audio silent=\"true\"/>\n</toast>\n)\";\n}\n\nbool init() {\n\tif (!IsWindows8OrGreater() || !base::WinRT::Supported()) {\n\t\treturn false;\n\t}\n\n\t{\n\t\tusing namespace Microsoft::WRL;\n\t\tconst auto hr = Module<OutOfProc>::GetModule().RegisterObjects();\n\t\tif (!SUCCEEDED(hr)) {\n\t\t\tLOG((\"App Error: Object registration failed.\"));\n\t\t}\n\t}\n\tif (!AppUserModelId::ValidateShortcut()) {\n\t\tLOG((\"App Error: Shortcut validation failed.\"));\n\t\treturn false;\n\t}\n\n\tPWSTR appUserModelId = {};\n\tif (!SUCCEEDED(GetCurrentProcessExplicitAppUserModelID(&appUserModelId))) {\n\t\treturn false;\n\t}\n\n\tconst auto appUserModelIdGuard = gsl::finally([&] {\n\t\tCoTaskMemFree(appUserModelId);\n\t});\n\n\tif (AppUserModelId::Id() != appUserModelId) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\n// Throws.\nvoid SetNodeValueString(\n\t\tconst XmlDocument &xml,\n\t\tconst IXmlNode &node,\n\t\tconst std::wstring &text) {\n\tnode.AppendChild(xml.CreateTextNode(text).as<IXmlNode>());\n}\n\n// Throws.\nvoid SetAudioSilent(const XmlDocument &toastXml) {\n\tconst auto nodeList = toastXml.GetElementsByTagName(L\"audio\");\n\tif (const auto audioNode = nodeList.Item(0)) {\n\t\taudioNode.as<IXmlElement>().SetAttribute(L\"silent\", L\"true\");\n\t} else {\n\t\tauto audioElement = toastXml.CreateElement(L\"audio\");\n\t\taudioElement.SetAttribute(L\"silent\", L\"true\");\n\t\tauto nodeList = toastXml.GetElementsByTagName(L\"toast\");\n\t\tnodeList.Item(0).AppendChild(audioElement.as<IXmlNode>());\n\t}\n}\n\n// Throws.\nvoid SetImageSrc(const XmlDocument &toastXml, const std::wstring &path) {\n\tconst auto nodeList = toastXml.GetElementsByTagName(L\"image\");\n\tconst auto attributes = nodeList.Item(0).Attributes();\n\treturn SetNodeValueString(\n\t\ttoastXml,\n\t\tattributes.GetNamedItem(L\"src\"),\n\t\tL\"file:///\" + path);\n}\n\n// Throws.\nvoid SetReplyIconSrc(const XmlDocument &toastXml, const std::wstring &path) {\n\tconst auto nodeList = toastXml.GetElementsByTagName(L\"action\");\n\tconst auto length = int(nodeList.Length());\n\tfor (auto i = 0; i != length; ++i) {\n\t\tconst auto attributes = nodeList.Item(i).Attributes();\n\t\tif (const auto uri = attributes.GetNamedItem(L\"imageUri\")) {\n\t\t\treturn SetNodeValueString(toastXml, uri, L\"file:///\" + path);\n\t\t}\n\t}\n}\n\n// Throws.\nvoid SetReplyPlaceholder(\n\t\tconst XmlDocument &toastXml,\n\t\tconst std::wstring &placeholder) {\n\tconst auto nodeList = toastXml.GetElementsByTagName(L\"input\");\n\tconst auto attributes = nodeList.Item(0).Attributes();\n\treturn SetNodeValueString(\n\t\ttoastXml,\n\t\tattributes.GetNamedItem(L\"placeHolderContent\"),\n\t\tplaceholder);\n}\n\n// Throws.\nvoid SetAction(const XmlDocument &toastXml, const QString &id) {\n\tauto nodeList = toastXml.GetElementsByTagName(L\"toast\");\n\tif (const auto toast = nodeList.Item(0).try_as<XmlElement>()) {\n\t\ttoast.SetAttribute(L\"launch\", L\"action=open&\" + id.toStdWString());\n\t}\n}\n\n// Throws.\nvoid SetMarkAsReadText(\n\t\tconst XmlDocument &toastXml,\n\t\tconst std::wstring &text) {\n\tconst auto nodeList = toastXml.GetElementsByTagName(L\"action\");\n\tconst auto length = int(nodeList.Length());\n\tfor (auto i = 0; i != length; ++i) {\n\t\tconst auto attributes = nodeList.Item(i).Attributes();\n\t\tif (!attributes.GetNamedItem(L\"imageUri\")) {\n\t\t\treturn SetNodeValueString(\n\t\t\t\ttoastXml,\n\t\t\t\tattributes.GetNamedItem(L\"content\"),\n\t\t\t\ttext);\n\t\t}\n\t}\n}\n\nauto Checked = false;\nauto InitSucceeded = false;\n\nvoid Check() {\n\tInitSucceeded = init();\n}\n\nbool QuietHoursEnabled = false;\nDWORD QuietHoursValue = 0;\n\n[[nodiscard]] bool UseQuietHoursRegistryEntry() {\n\tstatic const bool result = [] {\n\t\tconst auto version = QOperatingSystemVersion::current();\n\n\t\t// At build 17134 (Redstone 4) the \"Quiet hours\" was replaced\n\t\t// by \"Focus assist\" and it looks like it doesn't use registry.\n\t\treturn (version.majorVersion() == 10)\n\t\t\t&& (version.minorVersion() == 0)\n\t\t\t&& (version.microVersion() < 17134);\n\t}();\n\treturn result;\n}\n\n// Thanks https://stackoverflow.com/questions/35600128/get-windows-quiet-hours-from-win32-or-c-sharp-api\nvoid QueryQuietHours() {\n\tif (!UseQuietHoursRegistryEntry()) {\n\t\t// There are quiet hours in Windows starting from Windows 8.1\n\t\t// But there were several reports about the notifications being shut\n\t\t// down according to the registry while no quiet hours were enabled.\n\t\t// So we try this method only starting with Windows 10.\n\t\treturn;\n\t}\n\n\tLPCWSTR lpKeyName = L\"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Notifications\\\\Settings\";\n\tLPCWSTR lpValueName = L\"NOC_GLOBAL_SETTING_TOASTS_ENABLED\";\n\tHKEY key;\n\tauto result = RegOpenKeyEx(HKEY_CURRENT_USER, lpKeyName, 0, KEY_READ, &key);\n\tif (result != ERROR_SUCCESS) {\n\t\treturn;\n\t}\n\n\tDWORD value = 0, type = 0, size = sizeof(value);\n\tresult = RegQueryValueEx(key, lpValueName, 0, &type, (LPBYTE)&value, &size);\n\tRegCloseKey(key);\n\n\tauto quietHoursEnabled = (result == ERROR_SUCCESS) && (value == 0);\n\tif (QuietHoursEnabled != quietHoursEnabled) {\n\t\tQuietHoursEnabled = quietHoursEnabled;\n\t\tQuietHoursValue = value;\n\t\tLOG((\"Quiet hours changed, entry value: %1\").arg(value));\n\t} else if (QuietHoursValue != value) {\n\t\tQuietHoursValue = value;\n\t\tLOG((\"Quiet hours value changed, was value: %1, entry value: %2\").arg(QuietHoursValue).arg(value));\n\t}\n}\n\nbool FocusAssistBlocks = false;\n\n// Thanks https://www.withinrafael.com/2019/09/19/determine-if-your-app-is-in-a-focus-assist-profiles-priority-list/\nvoid QueryFocusAssist() {\n\tconst auto quietHoursSettings = base::WinRT::TryCreateInstance<\n\t\tIQuietHoursSettings\n\t>(CLSID_QuietHoursSettings, CLSCTX_LOCAL_SERVER);\n\tif (!quietHoursSettings) {\n\t\treturn;\n\t}\n\n\tauto profileId = base::CoTaskMemString();\n\tauto hr = quietHoursSettings->get_UserSelectedProfile(profileId.put());\n\tif (FAILED(hr) || !profileId) {\n\t\treturn;\n\t}\n\tconst auto profileName = QString::fromWCharArray(profileId.data());\n\tif (profileName.endsWith(\".alarmsonly\", Qt::CaseInsensitive)) {\n\t\tif (!FocusAssistBlocks) {\n\t\t\tLOG((\"Focus Assist: Alarms Only.\"));\n\t\t\tFocusAssistBlocks = true;\n\t\t}\n\t\treturn;\n\t} else if (!profileName.endsWith(\".priorityonly\", Qt::CaseInsensitive)) {\n\t\tif (!profileName.endsWith(\".unrestricted\", Qt::CaseInsensitive)) {\n\t\t\tLOG((\"Focus Assist Warning: Unknown profile '%1'\"\n\t\t\t\t).arg(profileName));\n\t\t}\n\t\tif (FocusAssistBlocks) {\n\t\t\tLOG((\"Focus Assist: Unrestricted.\"));\n\t\t\tFocusAssistBlocks = false;\n\t\t}\n\t\treturn;\n\t}\n\tconst auto appUserModelId = AppUserModelId::Id();\n\tauto blocked = true;\n\tconst auto guard = gsl::finally([&] {\n\t\tif (FocusAssistBlocks != blocked) {\n\t\t\tLOG((\"Focus Assist: %1, AppUserModelId: %2, Blocks: %3\"\n\t\t\t\t).arg(profileName\n\t\t\t\t).arg(QString::fromStdWString(appUserModelId)\n\t\t\t\t).arg(Logs::b(blocked)));\n\t\t\tFocusAssistBlocks = blocked;\n\t\t}\n\t});\n\n\tcom_ptr<IQuietHoursProfile> profile;\n\thr = quietHoursSettings->GetProfile(profileId.data(), profile.put());\n\tif (FAILED(hr) || !profile) {\n\t\treturn;\n\t}\n\n\tauto apps = base::CoTaskMemStringArray();\n\thr = profile->GetAllowedApps(apps.put_size(), apps.put());\n\tif (FAILED(hr) || !apps) {\n\t\treturn;\n\t}\n\tfor (const auto &app : apps) {\n\t\tif (app && app.data() == appUserModelId) {\n\t\t\tblocked = false;\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nQUERY_USER_NOTIFICATION_STATE UserNotificationState\n\t= QUNS_ACCEPTS_NOTIFICATIONS;\n\nvoid QueryUserNotificationState() {\n\tif (Dlls::SHQueryUserNotificationState != nullptr) {\n\t\tQUERY_USER_NOTIFICATION_STATE state;\n\t\tif (SUCCEEDED(Dlls::SHQueryUserNotificationState(&state))) {\n\t\t\tUserNotificationState = state;\n\t\t}\n\t}\n}\n\nvoid QuerySystemNotificationSettings() {\n\tif (!ShouldQuerySettings()) {\n\t\treturn;\n\t}\n\tQueryQuietHours();\n\tQueryFocusAssist();\n\tQueryUserNotificationState();\n}\n\nbool SkipSoundForCustom() {\n\tQuerySystemNotificationSettings();\n\n\treturn (UserNotificationState == QUNS_NOT_PRESENT)\n\t\t|| (UserNotificationState == QUNS_PRESENTATION_MODE)\n\t\t|| (FocusAssistBlocks && Core::App().settings().skipToastsInFocus())\n\t\t|| Core::App().screenIsLocked();\n}\n\nbool SkipFlashBounceForCustom() {\n\treturn SkipToastForCustom();\n}\n\n} // namespace\n\nvoid MaybePlaySoundForCustom(Fn<void()> playSound) {\n\tif (!SkipSoundForCustom()) {\n\t\tplaySound();\n\t}\n}\n\nbool SkipToastForCustom() {\n\tQuerySystemNotificationSettings();\n\n\treturn (UserNotificationState == QUNS_PRESENTATION_MODE)\n\t\t|| (UserNotificationState == QUNS_RUNNING_D3D_FULL_SCREEN)\n\t\t|| (FocusAssistBlocks && Core::App().settings().skipToastsInFocus());\n}\n\nvoid MaybeFlashBounceForCustom(Fn<void()> flashBounce) {\n\tif (!SkipFlashBounceForCustom()) {\n\t\tflashBounce();\n\t}\n}\n\nbool WaitForInputForCustom() {\n\tQuerySystemNotificationSettings();\n\n\treturn UserNotificationState != QUNS_BUSY;\n}\n\nbool Supported() {\n\tif (!Checked) {\n\t\tChecked = true;\n\t\tCheck();\n\t}\n\treturn InitSucceeded;\n}\n\nbool Enforced() {\n\treturn false;\n}\n\nbool ByDefault() {\n\treturn false;\n}\n\nbool VolumeSupported() {\n\treturn true;\n}\n\nvoid Create(Window::Notifications::System *system) {\n\tsystem->setManager([=] {\n\t\tauto result = std::make_unique<Manager>(system);\n\t\treturn result->init() ? std::move(result) : nullptr;\n\t});\n}\n\nclass Manager::Private {\npublic:\n\tusing Info = Window::Notifications::NativeManager::NotificationInfo;\n\n\texplicit Private(Manager *instance);\n\tbool init();\n\n\tbool showNotification(Info &&info, Ui::PeerUserpicView &userpicView);\n\tvoid clearAll();\n\tvoid clearFromItem(not_null<HistoryItem*> item);\n\tvoid clearFromTopic(not_null<Data::ForumTopic*> topic);\n\tvoid clearFromSublist(not_null<Data::SavedSublist*> sublist);\n\tvoid clearFromHistory(not_null<History*> history);\n\tvoid clearFromSession(not_null<Main::Session*> session);\n\tvoid beforeNotificationActivated(NotificationId id);\n\tvoid afterNotificationActivated(\n\t\tNotificationId id,\n\t\tnot_null<Window::SessionController*> window);\n\tvoid clearNotification(NotificationId id);\n\n\tvoid handleActivation(const ToastActivation &activation);\n\n\t~Private();\n\nprivate:\n\tbool showNotificationInTryCatch(\n\t\tNotificationInfo &&info,\n\t\tUi::PeerUserpicView &userpicView);\n\tvoid tryHide(const ToastNotification &notification);\n\t[[nodiscard]] std::wstring ensureSendButtonIcon();\n\n\tWindow::Notifications::CachedUserpics _cachedUserpics;\n\tstd::wstring _sendButtonIconPath;\n\n\tstd::shared_ptr<Manager*> _guarded;\n\tToastNotifier _notifier = nullptr;\n\n\tbase::flat_map<\n\t\tContextId,\n\t\tbase::flat_map<MsgId, ToastNotification>> _notifications;\n\trpl::lifetime _lifetime;\n\n};\n\nManager::Private::Private(Manager *instance)\n: _guarded(std::make_shared<Manager*>(instance)) {\n\tToastActivations(\n\t) | rpl::on_next([=](const ToastActivation &activation) {\n\t\thandleActivation(activation);\n\t}, _lifetime);\n}\n\nbool Manager::Private::init() {\n\treturn base::WinRT::Try([&] {\n\t\t_notifier = ToastNotificationManager::CreateToastNotifier(\n\t\t\tAppUserModelId::Id());\n\t});\n}\n\nManager::Private::~Private() {\n\tclearAll();\n\n\t_notifications.clear();\n\t_notifier = nullptr;\n}\n\nvoid Manager::Private::clearAll() {\n\tif (!_notifier) {\n\t\treturn;\n\t}\n\n\tfor (const auto &[key, notifications] : base::take(_notifications)) {\n\t\tfor (const auto &[msgId, notification] : notifications) {\n\t\t\ttryHide(notification);\n\t\t}\n\t}\n}\n\nvoid Manager::Private::clearFromItem(not_null<HistoryItem*> item) {\n\tif (!_notifier) {\n\t\treturn;\n\t}\n\n\tauto i = _notifications.find(ContextId{\n\t\t.sessionId = item->history()->session().uniqueId(),\n\t\t.peerId = item->history()->peer->id,\n\t\t.topicRootId = item->topicRootId(),\n\t\t.monoforumPeerId = item->sublistPeerId(),\n\t});\n\tif (i == _notifications.cend()) {\n\t\treturn;\n\t}\n\tconst auto j = i->second.find(item->id);\n\tif (j == end(i->second)) {\n\t\treturn;\n\t}\n\tconst auto taken = std::exchange(j->second, nullptr);\n\ti->second.erase(j);\n\tif (i->second.empty()) {\n\t\t_notifications.erase(i);\n\t}\n\ttryHide(taken);\n}\n\nvoid Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) {\n\tif (!_notifier) {\n\t\treturn;\n\t}\n\n\tconst auto i = _notifications.find(ContextId{\n\t\t.sessionId = topic->session().uniqueId(),\n\t\t.peerId = topic->history()->peer->id,\n\t\t.topicRootId = topic->rootId(),\n\t});\n\tif (i != _notifications.cend()) {\n\t\tconst auto temp = base::take(i->second);\n\t\t_notifications.erase(i);\n\n\t\tfor (const auto &[msgId, notification] : temp) {\n\t\t\ttryHide(notification);\n\t\t}\n\t}\n}\n\nvoid Manager::Private::clearFromSublist(\n\t\tnot_null<Data::SavedSublist*> sublist) {\n\tif (!_notifier) {\n\t\treturn;\n\t}\n\n\tconst auto i = _notifications.find(ContextId{\n\t\t.sessionId = sublist->session().uniqueId(),\n\t\t.peerId = sublist->owningHistory()->peer->id,\n\t\t.monoforumPeerId = sublist->sublistPeer()->id,\n\t});\n\tif (i != _notifications.cend()) {\n\t\tconst auto temp = base::take(i->second);\n\t\t_notifications.erase(i);\n\n\t\tfor (const auto &[msgId, notification] : temp) {\n\t\t\ttryHide(notification);\n\t\t}\n\t}\n}\n\nvoid Manager::Private::clearFromHistory(not_null<History*> history) {\n\tif (!_notifier) {\n\t\treturn;\n\t}\n\n\tconst auto sessionId = history->session().uniqueId();\n\tconst auto peerId = history->peer->id;\n\tauto i = _notifications.lower_bound(ContextId{\n\t\t.sessionId = sessionId,\n\t\t.peerId = peerId,\n\t});\n\twhile (i != _notifications.cend()\n\t\t&& i->first.sessionId == sessionId\n\t\t&& i->first.peerId == peerId) {\n\t\tconst auto temp = base::take(i->second);\n\t\ti = _notifications.erase(i);\n\n\t\tfor (const auto &[msgId, notification] : temp) {\n\t\t\ttryHide(notification);\n\t\t}\n\t}\n}\n\nvoid Manager::Private::clearFromSession(not_null<Main::Session*> session) {\n\tif (!_notifier) {\n\t\treturn;\n\t}\n\n\tconst auto sessionId = session->uniqueId();\n\tauto i = _notifications.lower_bound(ContextId{\n\t\t.sessionId = sessionId,\n\t});\n\twhile (i != _notifications.cend() && i->first.sessionId == sessionId) {\n\t\tconst auto temp = base::take(i->second);\n\t\ti = _notifications.erase(i);\n\n\t\tfor (const auto &[msgId, notification] : temp) {\n\t\t\ttryHide(notification);\n\t\t}\n\t}\n}\n\nvoid Manager::Private::beforeNotificationActivated(NotificationId id) {\n\tclearNotification(id);\n}\n\nvoid Manager::Private::afterNotificationActivated(\n\t\tNotificationId id,\n\t\tnot_null<Window::SessionController*> window) {\n\tSetForegroundWindow(window->widget()->psHwnd());\n}\n\nvoid Manager::Private::clearNotification(NotificationId id) {\n\tauto i = _notifications.find(id.contextId);\n\tif (i != _notifications.cend()) {\n\t\ti->second.remove(id.msgId);\n\t\tif (i->second.empty()) {\n\t\t\t_notifications.erase(i);\n\t\t}\n\t}\n}\n\nvoid Manager::Private::handleActivation(const ToastActivation &activation) {\n\tconst auto parsed = qthelp::url_parse_params(activation.args);\n\tconst auto pid = parsed.value(\"pid\").toULong();\n\tconst auto my = GetCurrentProcessId();\n\tif (pid != my) {\n\t\tDEBUG_LOG((\"Toast Info: \"\n\t\t\t\"Got activation \\\"%1\\\", my %2, activating %3.\"\n\t\t\t).arg(activation.args\n\t\t\t).arg(my\n\t\t\t).arg(pid));\n\t\tconst auto processId = pid;\n\t\tconst auto windowId = 0; // Activate some window.\n\t\tPlatform::ActivateOtherProcess(processId, windowId);\n\t\treturn;\n\t}\n\tconst auto action = parsed.value(\"action\");\n\tconst auto id = NotificationId{\n\t\t.contextId = ContextId{\n\t\t\t.sessionId = parsed.value(\"session\").toULongLong(),\n\t\t\t.peerId = PeerId(parsed.value(\"peer\").toULongLong()),\n\t\t\t.topicRootId = MsgId(parsed.value(\"topic\").toLongLong()),\n\t\t\t.monoforumPeerId = PeerId(\n\t\t\t\t\tparsed.value(\"monoforumpeer\").toULongLong()),\n\t\t},\n\t\t.msgId = MsgId(parsed.value(\"msg\").toLongLong()),\n\t};\n\tif (!id.contextId.sessionId || !id.contextId.peerId || !id.msgId) {\n\t\tDEBUG_LOG((\"Toast Info: Got activation \\\"%1\\\", my %1, skipping.\"\n\t\t\t).arg(activation.args\n\t\t\t).arg(pid));\n\t\treturn;\n\t}\n\tDEBUG_LOG((\"Toast Info: Got activation \\\"%1\\\", my %1, handling.\"\n\t\t).arg(activation.args\n\t\t).arg(pid));\n\tauto text = TextWithTags();\n\tfor (const auto &entry : activation.input) {\n\t\tif (entry.key == \"fastReply\") {\n\t\t\ttext.text = entry.value;\n\t\t}\n\t}\n\tconst auto i = _notifications.find(id.contextId);\n\tif (i == _notifications.cend() || !i->second.contains(id.msgId)) {\n\t\treturn;\n\t}\n\n\tconst auto manager = *_guarded;\n\tif (action == \"reply\") {\n\t\tmanager->notificationReplied(id, text);\n\t} else if (action == \"mark\") {\n\t\tmanager->notificationReplied(id, TextWithTags());\n\t} else {\n\t\tmanager->notificationActivated(id, {\n\t\t\t.draft = std::move(text),\n\t\t});\n\t}\n}\n\nbool Manager::Private::showNotification(\n\t\tInfo &&info,\n\t\tUi::PeerUserpicView &userpicView) {\n\tif (!_notifier) {\n\t\treturn false;\n\t}\n\n\treturn base::WinRT::Try([&] {\n\t\treturn showNotificationInTryCatch(std::move(info), userpicView);\n\t}).value_or(false);\n}\n\nstd::wstring Manager::Private::ensureSendButtonIcon() {\n\tif (_sendButtonIconPath.empty()) {\n\t\tconst auto path = cWorkingDir() + u\"tdata/temp/fast_reply.png\"_q;\n\t\tst::historySendIcon.instance(Qt::white, 300).save(path, \"PNG\");\n\t\t_sendButtonIconPath = path.toStdWString();\n\t}\n\treturn _sendButtonIconPath;\n}\n\nbool Manager::Private::showNotificationInTryCatch(\n\t\tNotificationInfo &&info,\n\t\tUi::PeerUserpicView &userpicView) {\n\tconst auto withSubtitle = !info.subtitle.isEmpty();\n\tconst auto peer = info.peer;\n\tauto toastXml = XmlDocument();\n\n\tconst auto key = ContextId{\n\t\t.sessionId = peer->session().uniqueId(),\n\t\t.peerId = peer->id,\n\t\t.topicRootId = info.topicRootId,\n\t\t.monoforumPeerId = info.monoforumPeerId,\n\t};\n\tconst auto notificationId = NotificationId{\n\t\t.contextId = key,\n\t\t.msgId = info.itemId,\n\t};\n\tconst auto idString = u\"pid=%1&session=%2&peer=%3&topic=%4&monoforumpeer=%5&msg=%6\"_q\n\t\t.arg(GetCurrentProcessId())\n\t\t.arg(key.sessionId)\n\t\t.arg(key.peerId.value)\n\t\t.arg(info.topicRootId.bare)\n\t\t.arg(info.monoforumPeerId.value)\n\t\t.arg(info.itemId.bare);\n\n\tconst auto modern = Platform::IsWindows10OrGreater();\n\tif (modern) {\n\t\ttoastXml.LoadXml(NotificationTemplate(idString, info.options));\n\t} else {\n\t\ttoastXml = ToastNotificationManager::GetTemplateContent(\n\t\t\t(withSubtitle\n\t\t\t\t? ToastTemplateType::ToastImageAndText04\n\t\t\t\t: ToastTemplateType::ToastImageAndText02));\n\t\tSetAudioSilent(toastXml);\n\t\tSetAction(toastXml, idString);\n\t}\n\n\tconst auto userpicKey = info.options.hideNameAndPhoto\n\t\t? InMemoryKey()\n\t\t: peer->userpicUniqueKey(userpicView);\n\tconst auto userpicPath = _cachedUserpics.get(\n\t\tuserpicKey,\n\t\tpeer,\n\t\tuserpicView);\n\tconst auto userpicPathWide = QDir::toNativeSeparators(\n\t\tuserpicPath).toStdWString();\n\tif (modern && !info.options.hideReplyButton) {\n\t\tSetReplyIconSrc(toastXml, ensureSendButtonIcon());\n\t\tSetReplyPlaceholder(\n\t\t\ttoastXml,\n\t\t\ttr::lng_message_ph(tr::now).toStdWString());\n\t}\n\tif (modern && !info.options.hideMarkAsRead) {\n\t\tSetMarkAsReadText(\n\t\t\ttoastXml,\n\t\t\ttr::lng_context_mark_read(tr::now).toStdWString());\n\t}\n\n\tSetImageSrc(toastXml, userpicPathWide);\n\n\tconst auto nodeList = toastXml.GetElementsByTagName(L\"text\");\n\tif (nodeList.Length() < (withSubtitle ? 3U : 2U)) {\n\t\treturn false;\n\t}\n\n\tSetNodeValueString(\n\t\ttoastXml,\n\t\tnodeList.Item(0),\n\t\tinfo.title.toStdWString());\n\tif (withSubtitle) {\n\t\tSetNodeValueString(\n\t\t\ttoastXml,\n\t\t\tnodeList.Item(1),\n\t\t\tinfo.subtitle.toStdWString());\n\t}\n\tSetNodeValueString(\n\t\ttoastXml,\n\t\tnodeList.Item(withSubtitle ? 2 : 1),\n\t\tinfo.message.toStdWString());\n\n\tconst auto weak = std::weak_ptr(_guarded);\n\tconst auto performOnMainQueue = [=](FnMut<void(Manager *manager)> task) {\n\t\tcrl::on_main(weak, [=, task = std::move(task)]() mutable {\n\t\t\ttask(*weak.lock());\n\t\t});\n\t};\n\n\tauto toast = ToastNotification(toastXml);\n\tconst auto token1 = toast.Activated([=](\n\t\t\tconst ToastNotification &sender,\n\t\t\tconst winrt::Windows::Foundation::IInspectable &object) {\n\t\tauto activation = ToastActivation();\n\t\tconst auto string = &ToastActivation::String;\n\t\tif (const auto args = object.try_as<ToastActivatedEventArgs>()) {\n\t\t\tactivation.args = string(args.Arguments().c_str());\n\t\t\tconst auto args2 = args.try_as<IToastActivatedEventArgs2>();\n\t\t\tif (!args2 && activation.args.startsWith(\"action=reply&\")) {\n\t\t\t\tLOG((\"WinRT Error: \"\n\t\t\t\t\t\"FastReply without IToastActivatedEventArgs2 support.\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto input = args2 ? args2.UserInput() : nullptr;\n\t\t\tconst auto reply = input\n\t\t\t\t? input.TryLookup(L\"fastReply\")\n\t\t\t\t: nullptr;\n\t\t\tconst auto data = reply\n\t\t\t\t? reply.try_as<IReference<winrt::hstring>>()\n\t\t\t\t: nullptr;\n\t\t\tif (data) {\n\t\t\t\tactivation.input.push_back({\n\t\t\t\t\t.key = u\"fastReply\"_q,\n\t\t\t\t\t.value = string(data.GetString().c_str()),\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\tactivation.args = \"action=open&\" + idString;\n\t\t}\n\t\tcrl::on_main([=, activation = std::move(activation)]() mutable {\n\t\t\tif (const auto strong = weak.lock()) {\n\t\t\t\t(*strong)->handleActivation(activation);\n\t\t\t}\n\t\t});\n\t});\n\tconst auto token2 = toast.Dismissed([=](\n\t\t\tconst ToastNotification &sender,\n\t\t\tconst ToastDismissedEventArgs &args) {\n\t\tconst auto reason = args.Reason();\n\t\tswitch (reason) {\n\t\tcase ToastDismissalReason::ApplicationHidden:\n\t\tcase ToastDismissalReason::TimedOut: // Went to Action Center.\n\t\t\tbreak;\n\t\tcase ToastDismissalReason::UserCanceled:\n\t\tdefault:\n\t\t\tperformOnMainQueue([notificationId](Manager *manager) {\n\t\t\t\tmanager->clearNotification(notificationId);\n\t\t\t});\n\t\t\tbreak;\n\t\t}\n\t});\n\tconst auto token3 = toast.Failed([=](\n\t\t\tconst ToastNotification &sender,\n\t\t\tconst ToastFailedEventArgs &args) {\n\t\tperformOnMainQueue([notificationId](Manager *manager) {\n\t\t\tmanager->clearNotification(notificationId);\n\t\t});\n\t});\n\n\tauto i = _notifications.find(key);\n\tif (i != _notifications.cend()) {\n\t\tauto j = i->second.find(info.itemId);\n\t\tif (j != i->second.end()) {\n\t\t\tconst auto existing = j->second;\n\t\t\ti->second.erase(j);\n\t\t\ttryHide(existing);\n\t\t\ti = _notifications.find(key);\n\t\t}\n\t}\n\tif (i == _notifications.cend()) {\n\t\ti = _notifications.emplace(\n\t\t\tkey,\n\t\t\tbase::flat_map<MsgId, ToastNotification>()).first;\n\t}\n\tif (!base::WinRT::Try([&] { _notifier.Show(toast); })) {\n\t\ti = _notifications.find(key);\n\t\tif (i != _notifications.cend() && i->second.empty()) {\n\t\t\t_notifications.erase(i);\n\t\t}\n\t\treturn false;\n\t}\n\ti->second.emplace(info.itemId, toast);\n\treturn true;\n}\n\nvoid Manager::Private::tryHide(const ToastNotification &notification) {\n\tbase::WinRT::Try([&] {\n\t\t_notifier.Hide(notification);\n\t});\n}\n\nManager::Manager(Window::Notifications::System *system)\n: NativeManager(system)\n, _private(std::make_unique<Private>(this)) {\n}\n\nbool Manager::init() {\n\treturn _private->init();\n}\n\nvoid Manager::clearNotification(NotificationId id) {\n\t_private->clearNotification(id);\n}\n\nvoid Manager::handleActivation(const ToastActivation &activation) {\n\t_private->handleActivation(activation);\n}\n\nManager::~Manager() = default;\n\nvoid Manager::doShowNativeNotification(\n\t\tNotificationInfo &&info,\n\t\tUi::PeerUserpicView &userpicView) {\n\t_private->showNotification(std::move(info), userpicView);\n}\n\nvoid Manager::doClearAllFast() {\n\t_private->clearAll();\n}\n\nvoid Manager::doClearFromItem(not_null<HistoryItem*> item) {\n\t_private->clearFromItem(item);\n}\n\nvoid Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) {\n\t_private->clearFromTopic(topic);\n}\n\nvoid Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) {\n\t_private->clearFromSublist(sublist);\n}\n\nvoid Manager::doClearFromHistory(not_null<History*> history) {\n\t_private->clearFromHistory(history);\n}\n\nvoid Manager::doClearFromSession(not_null<Main::Session*> session) {\n\t_private->clearFromSession(session);\n}\n\nvoid Manager::onBeforeNotificationActivated(NotificationId id) {\n\t_private->beforeNotificationActivated(id);\n}\n\nvoid Manager::onAfterNotificationActivated(\n\t\tNotificationId id,\n\t\tnot_null<Window::SessionController*> window) {\n\t_private->afterNotificationActivated(id, window);\n}\n\nbool Manager::doSkipToast() const {\n\treturn false;\n}\n\nvoid Manager::doMaybePlaySound(Fn<void()> playSound) {\n\tconst auto skip = SkipSoundForCustom()\n\t\t|| QuietHoursEnabled\n\t\t|| FocusAssistBlocks;\n\tif (!skip) {\n\t\tplaySound();\n\t}\n}\n\nvoid Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {\n\tconst auto skip = SkipFlashBounceForCustom()\n\t\t|| QuietHoursEnabled\n\t\t|| FocusAssistBlocks;\n\tif (!skip) {\n\t\tflashBounce();\n\t}\n}\n\n} // namespace Notifications\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/notifications_manager_win.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_notifications_manager.h\"\n\nstruct ToastActivation;\n\nnamespace Platform {\nnamespace Notifications {\n\nclass Manager : public Window::Notifications::NativeManager {\npublic:\n\tManager(Window::Notifications::System *system);\n\t~Manager();\n\n\tbool init();\n\tvoid clearNotification(NotificationId id);\n\n\tvoid handleActivation(const ToastActivation &activation);\n\nprotected:\n\tvoid doShowNativeNotification(\n\t\tNotificationInfo &&info,\n\t\tUi::PeerUserpicView &userpicView) override;\n\tvoid doClearAllFast() override;\n\tvoid doClearFromItem(not_null<HistoryItem*> item) override;\n\tvoid doClearFromTopic(not_null<Data::ForumTopic*> topic) override;\n\tvoid doClearFromSublist(not_null<Data::SavedSublist*> sublist) override;\n\tvoid doClearFromHistory(not_null<History*> history) override;\n\tvoid doClearFromSession(not_null<Main::Session*> session) override;\n\tvoid onBeforeNotificationActivated(NotificationId id) override;\n\tvoid onAfterNotificationActivated(\n\t\tNotificationId id,\n\t\tnot_null<Window::SessionController*> window) override;\n\tbool doSkipToast() const override;\n\tvoid doMaybePlaySound(Fn<void()> playSound) override;\n\tvoid doMaybeFlashBounce(Fn<void()> flashBounce) override;\n\nprivate:\n\tclass Private;\n\tconst std::unique_ptr<Private> _private;\n\n};\n\n} // namespace Notifications\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/overlay_widget_win.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Platform {\n\ninline std::unique_ptr<OverlayWidgetHelper> CreateOverlayWidgetHelper(\n\t\tnot_null<Ui::RpWindow*> window,\n\t\tFn<void(bool)> maximize) {\n\treturn std::make_unique<DefaultOverlayWidgetHelper>(\n\t\twindow,\n\t\tstd::move(maximize));\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/specific_win.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/win/specific_win.h\"\n\n#include \"platform/win/main_window_win.h\"\n#include \"platform/win/notifications_manager_win.h\"\n#include \"platform/win/windows_app_user_model_id.h\"\n#include \"platform/win/windows_dlls.h\"\n#include \"platform/win/windows_autostart_task.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/platform/win/base_windows_co_task_mem.h\"\n#include \"base/platform/win/base_windows_shlobj_h.h\"\n#include \"base/platform/win/base_windows_winrt.h\"\n#include \"base/call_delayed.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"mainwindow.h\"\n#include \"mainwidget.h\"\n#include \"history/history_location_manager.h\"\n#include \"storage/localstorage.h\"\n#include \"core/application.h\"\n#include \"window/window_controller.h\"\n#include \"core/crash_reports.h\"\n\n#include <QtCore/QOperatingSystemVersion>\n#include <QtWidgets/QApplication>\n#include <QtGui/QDesktopServices>\n#include <QtGui/QWindow>\n\n#include <Shobjidl.h>\n#include <ShObjIdl_core.h>\n#include <shellapi.h>\n\n#include <openssl/conf.h>\n#include <openssl/engine.h>\n#include <openssl/err.h>\n\n#include <dbghelp.h>\n#include <Shlwapi.h>\n#include <Strsafe.h>\n#include <Windowsx.h>\n#include <WtsApi32.h>\n\n#include <SDKDDKVer.h>\n\n#include <sal.h>\n#include <Psapi.h>\n#include <strsafe.h>\n#include <ObjBase.h>\n#include <propvarutil.h>\n#include <functiondiscoverykeys.h>\n#include <intsafe.h>\n#include <guiddef.h>\n#include <locale.h>\n\n#include <ShellScalingApi.h>\n\n#ifndef DCX_USESTYLE\n#define DCX_USESTYLE 0x00010000\n#endif\n\n#ifndef WM_NCPOINTERUPDATE\n#define WM_NCPOINTERUPDATE 0x0241\n#define WM_NCPOINTERDOWN 0x0242\n#define WM_NCPOINTERUP 0x0243\n#endif\n\nusing namespace ::Platform;\n\nnamespace {\n\nbool themeInited = false;\nbool finished = true;\nQMargins simpleMargins, margins;\nHICON bigIcon = 0, smallIcon = 0, overlayIcon = 0;\n\n[[nodiscard]] uint64 WindowIdFromHWND(HWND value) {\n\treturn (reinterpret_cast<uint64>(value) & 0xFFFFFFFFULL);\n}\n\nstruct FindToActivateRequest {\n\tuint64 processId = 0;\n\tuint64 windowId = 0;\n\tHWND result = nullptr;\n\tuint32 resultLevel = 0; // Larger is better.\n};\n\nBOOL CALLBACK FindToActivate(HWND hwnd, LPARAM lParam) {\n\tconst auto request = reinterpret_cast<FindToActivateRequest*>(lParam);\n\n\tDWORD dwProcessId;\n\t::GetWindowThreadProcessId(hwnd, &dwProcessId);\n\n\tif ((uint64)dwProcessId != request->processId) {\n\t\treturn TRUE;\n\t}\n\t// Found a Top-Level window.\n\tif (WindowIdFromHWND(hwnd) == request->windowId) {\n\t\trequest->result = hwnd;\n\t\trequest->resultLevel = 3;\n\t\treturn FALSE;\n\t}\n\tconst auto data = static_cast<uint32>(GetWindowLongPtr(hwnd, GWLP_USERDATA));\n\tif ((data != 1 && data != 2) || (data <= request->resultLevel)) {\n\t\treturn TRUE;\n\t}\n\trequest->result = hwnd;\n\trequest->resultLevel = data;\n\treturn TRUE;\n}\n\nvoid DeleteMyModules() {\n\tconstexpr auto kMaxPathLong = 32767;\n\tauto exePath = std::array<WCHAR, kMaxPathLong + 1>{ 0 };\n\tconst auto exeLength = GetModuleFileName(\n\t\tnullptr,\n\t\texePath.data(),\n\t\tkMaxPathLong + 1);\n\tif (!exeLength || exeLength >= kMaxPathLong + 1) {\n\t\treturn;\n\t}\n\tconst auto exe = std::wstring(exePath.data());\n\tconst auto last1 = exe.find_last_of('\\\\');\n\tconst auto last2 = exe.find_last_of('/');\n\tconst auto last = std::max(\n\t\t(last1 == std::wstring::npos) ? -1 : int(last1),\n\t\t(last2 == std::wstring::npos) ? -1 : int(last2));\n\tif (last < 0) {\n\t\treturn;\n\t}\n\tconst auto modules = exe.substr(0, last + 1) + L\"modules\";\n\tconst auto deleteOne = [&](const wchar_t *name, const wchar_t *arch) {\n\t\tconst auto path = modules + L'\\\\' + arch + L'\\\\' + name;\n\t\tDeleteFile(path.c_str());\n\t};\n\tconst auto deleteBoth = [&](const wchar_t *name) {\n\t\tdeleteOne(name, L\"x86\");\n\t\tdeleteOne(name, L\"x64\");\n\t};\n\tconst auto removeOne = [&](const std::wstring &name) {\n\t\tconst auto path = modules + L'\\\\' + name;\n\t\tRemoveDirectory(path.c_str());\n\t};\n\tconst auto removeBoth = [&](const std::wstring &name) {\n\t\tremoveOne(L\"x86\\\\\" + name);\n\t\tremoveOne(L\"x64\\\\\" + name);\n\t};\n\tdeleteBoth(L\"d3d\\\\d3dcompiler_47.dll\");\n\n\tremoveBoth(L\"d3d\");\n\tremoveOne(L\"x86\");\n\tremoveOne(L\"x64\");\n\tRemoveDirectory(modules.c_str());\n}\n\nbool ManageAppLink(\n\t\tbool create,\n\t\tbool silent,\n\t\tconst GUID &folderId,\n\t\tconst wchar_t *args,\n\t\tconst wchar_t *description) {\n\tif (cExeName().isEmpty()) {\n\t\treturn false;\n\t}\n\tPWSTR startupFolder;\n\tHRESULT hr = SHGetKnownFolderPath(\n\t\tfolderId,\n\t\tKF_FLAG_CREATE,\n\t\tnullptr,\n\t\t&startupFolder);\n\tconst auto guard = gsl::finally([&] {\n\t\tCoTaskMemFree(startupFolder);\n\t});\n\tif (!SUCCEEDED(hr)) {\n\t\tWCHAR buffer[64];\n\t\tconst auto size = base::array_size(buffer) - 1;\n\t\tconst auto length = StringFromGUID2(folderId, buffer, size);\n\t\tif (length > 0 && length <= size) {\n\t\t\tbuffer[length] = 0;\n\t\t\tif (!silent) LOG((\"App Error: could not get %1 folder: %2\").arg(buffer).arg(hr));\n\t\t}\n\t\treturn false;\n\t}\n\tconst auto lnk = QString::fromWCharArray(startupFolder)\n\t\t+ '\\\\'\n\t\t+ AppFile.utf16()\n\t\t+ u\".lnk\"_q;\n\tif (!create) {\n\t\tQFile::remove(lnk);\n\t\treturn true;\n\t}\n\tconst auto shellLink = base::WinRT::TryCreateInstance<IShellLink>(\n\t\tCLSID_ShellLink);\n\tif (!shellLink) {\n\t\tif (!silent) LOG((\"App Error: could not create instance of IID_IShellLink %1\").arg(hr));\n\t\treturn false;\n\t}\n\tQString exe = QDir::toNativeSeparators(cExeDir() + cExeName()), dir = QDir::toNativeSeparators(QDir(cWorkingDir()).absolutePath());\n\tshellLink->SetArguments(args);\n\tshellLink->SetPath(exe.toStdWString().c_str());\n\tshellLink->SetWorkingDirectory(dir.toStdWString().c_str());\n\tshellLink->SetDescription(description);\n\n\tif (const auto propertyStore = shellLink.try_as<IPropertyStore>()) {\n\t\tPROPVARIANT appIdPropVar;\n\t\thr = InitPropVariantFromString(AppUserModelId::Id().c_str(), &appIdPropVar);\n\t\tif (SUCCEEDED(hr)) {\n\t\t\thr = propertyStore->SetValue(AppUserModelId::Key(), appIdPropVar);\n\t\t\tPropVariantClear(&appIdPropVar);\n\t\t\tif (SUCCEEDED(hr)) {\n\t\t\t\thr = propertyStore->Commit();\n\t\t\t}\n\t\t}\n\t}\n\n\tconst auto persistFile = shellLink.try_as<IPersistFile>();\n\tif (!persistFile) {\n\t\tif (!silent) LOG((\"App Error: could not create interface IID_IPersistFile %1\").arg(hr));\n\t\treturn false;\n\t}\n\thr = persistFile->Save(lnk.toStdWString().c_str(), TRUE);\n\tif (!SUCCEEDED(hr)) {\n\t\tif (!silent) LOG((\"App Error: could not save IPersistFile to path %1\").arg(lnk));\n\t\treturn false;\n\t}\n\treturn true;\n}\n\n} // namespace\n\nQString psAppDataPath() {\n\tstatic const int maxFileLen = MAX_PATH * 10;\n\tWCHAR wstrPath[maxFileLen];\n\tif (GetEnvironmentVariable(L\"APPDATA\", wstrPath, maxFileLen)) {\n\t\tQDir appData(QString::fromStdWString(std::wstring(wstrPath)));\n#ifdef OS_WIN_STORE\n\t\treturn appData.absolutePath() + u\"/Telegram Desktop UWP/\"_q;\n#else // OS_WIN_STORE\n\t\treturn appData.absolutePath() + '/' + AppNameF.utf16() + '/';\n#endif // OS_WIN_STORE\n\t}\n\treturn QString();\n}\n\nQString psAppDataPathOld() {\n\tstatic const int maxFileLen = MAX_PATH * 10;\n\tWCHAR wstrPath[maxFileLen];\n\tif (GetEnvironmentVariable(L\"APPDATA\", wstrPath, maxFileLen)) {\n\t\tQDir appData(QString::fromStdWString(std::wstring(wstrPath)));\n\t\treturn appData.absolutePath() + '/' + AppNameOld.utf16() + '/';\n\t}\n\treturn QString();\n}\n\nvoid psDoCleanup() {\n\ttry {\n\t\tPlatform::AutostartToggle(false);\n\t\tpsSendToMenu(false, true);\n\t\tAppUserModelId::CleanupShortcut();\n\t\tDeleteMyModules();\n\t} catch (...) {\n\t}\n}\n\nint psCleanup() {\n\t__try\n\t{\n\t\tpsDoCleanup();\n\t}\n\t__except(EXCEPTION_EXECUTE_HANDLER)\n\t{\n\t\treturn 0;\n\t}\n\treturn 0;\n}\n\nvoid psDoFixPrevious() {\n\ttry {\n\t\tstatic const int bufSize = 4096;\n\t\tDWORD checkType = 0;\n\t\tDWORD checkSize = bufSize * 2;\n\t\tWCHAR checkStr[bufSize] = { 0 };\n\t\tHKEY newKey1 = nullptr;\n\t\tHKEY newKey2 = nullptr;\n\t\tHKEY oldKey1 = nullptr;\n\t\tHKEY oldKey2 = nullptr;\n\n\t\tconst auto appId = AppId.utf16();\n\t\tconst auto newKeyStr1 = QString(\"Software\\\\Wow6432Node\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\%1_is1\").arg(appId).toStdWString();\n\t\tconst auto newKeyStr2 = QString(\"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\%1_is1\").arg(appId).toStdWString();\n\t\tconst auto oldKeyStr1 = QString(\"SOFTWARE\\\\Wow6432Node\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\%1_is1\").arg(appId).toStdWString();\n\t\tconst auto oldKeyStr2 = QString(\"SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\%1_is1\").arg(appId).toStdWString();\n\t\tconst auto newKeyRes1 = RegOpenKeyEx(HKEY_CURRENT_USER, newKeyStr1.c_str(), 0, KEY_READ, &newKey1);\n\t\tconst auto newKeyRes2 = RegOpenKeyEx(HKEY_CURRENT_USER, newKeyStr2.c_str(), 0, KEY_READ, &newKey2);\n\t\tconst auto oldKeyRes1 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, oldKeyStr1.c_str(), 0, KEY_READ, &oldKey1);\n\t\tconst auto oldKeyRes2 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, oldKeyStr2.c_str(), 0, KEY_READ, &oldKey2);\n\n\t\tconst auto existNew1 = (newKeyRes1 == ERROR_SUCCESS) && (RegQueryValueEx(newKey1, L\"InstallDate\", 0, &checkType, (BYTE*)checkStr, &checkSize) == ERROR_SUCCESS); checkSize = bufSize * 2;\n\t\tconst auto existNew2 = (newKeyRes2 == ERROR_SUCCESS) && (RegQueryValueEx(newKey2, L\"InstallDate\", 0, &checkType, (BYTE*)checkStr, &checkSize) == ERROR_SUCCESS); checkSize = bufSize * 2;\n\t\tconst auto existOld1 = (oldKeyRes1 == ERROR_SUCCESS) && (RegQueryValueEx(oldKey1, L\"InstallDate\", 0, &checkType, (BYTE*)checkStr, &checkSize) == ERROR_SUCCESS); checkSize = bufSize * 2;\n\t\tconst auto existOld2 = (oldKeyRes2 == ERROR_SUCCESS) && (RegQueryValueEx(oldKey2, L\"InstallDate\", 0, &checkType, (BYTE*)checkStr, &checkSize) == ERROR_SUCCESS); checkSize = bufSize * 2;\n\n\t\tif (newKeyRes1 == ERROR_SUCCESS) RegCloseKey(newKey1);\n\t\tif (newKeyRes2 == ERROR_SUCCESS) RegCloseKey(newKey2);\n\t\tif (oldKeyRes1 == ERROR_SUCCESS) RegCloseKey(oldKey1);\n\t\tif (oldKeyRes2 == ERROR_SUCCESS) RegCloseKey(oldKey2);\n\n\t\tif (existNew1 || existNew2) {\n\t\t\tif (existOld1) RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr1.c_str());\n\t\t\tif (existOld2) RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr2.c_str());\n\t\t}\n\n\t\tQString userDesktopLnk, commonDesktopLnk;\n\t\tWCHAR userDesktopFolder[MAX_PATH], commonDesktopFolder[MAX_PATH];\n\t\tHRESULT userDesktopRes = SHGetFolderPath(0, CSIDL_DESKTOPDIRECTORY, 0, SHGFP_TYPE_CURRENT, userDesktopFolder);\n\t\tHRESULT commonDesktopRes = SHGetFolderPath(0, CSIDL_COMMON_DESKTOPDIRECTORY, 0, SHGFP_TYPE_CURRENT, commonDesktopFolder);\n\t\tif (SUCCEEDED(userDesktopRes)) {\n\t\t\tuserDesktopLnk = QString::fromWCharArray(userDesktopFolder) + \"\\\\Telegram.lnk\";\n\t\t}\n\t\tif (SUCCEEDED(commonDesktopRes)) {\n\t\t\tcommonDesktopLnk = QString::fromWCharArray(commonDesktopFolder) + \"\\\\Telegram.lnk\";\n\t\t}\n\t\tQFile userDesktopFile(userDesktopLnk), commonDesktopFile(commonDesktopLnk);\n\t\tif (QFile::exists(userDesktopLnk) && QFile::exists(commonDesktopLnk) && userDesktopLnk != commonDesktopLnk) {\n\t\t\tQFile::remove(commonDesktopLnk);\n\t\t}\n\t} catch (...) {\n\t}\n}\n\nint psFixPrevious() {\n\t__try\n\t{\n\t\tpsDoFixPrevious();\n\t}\n\t__except(EXCEPTION_EXECUTE_HANDLER)\n\t{\n\t\treturn 0;\n\t}\n\treturn 0;\n}\n\nnamespace Platform {\nnamespace ThirdParty {\nnamespace {\n\nvoid StartOpenSSL() {\n\t// Don't use dynamic OpenSSL config, it can load unwanted DLLs.\n\tOPENSSL_load_builtin_modules();\n\tENGINE_load_builtin_engines();\n\tERR_clear_error();\n\tOPENSSL_no_config();\n}\n\n} // namespace\n\nvoid start() {\n\tStartOpenSSL();\n\tDlls::CheckLoadedModules();\n}\n\n} // namespace ThirdParty\n\nvoid start() {\n\tconst auto supported = base::WinRT::Supported();\n\tLOG((\"WinRT Supported: %1\").arg(Logs::b(supported)));\n\n\t// https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setlocale-wsetlocale#utf-8-support\n\tsetlocale(LC_ALL, \".UTF8\");\n\n\tconst auto appUserModelId = AppUserModelId::Id();\n\tSetCurrentProcessExplicitAppUserModelID(appUserModelId.c_str());\n\tLOG((\"AppUserModelID: %1\").arg(appUserModelId));\n}\n\nvoid finish() {\n}\n\nvoid SetApplicationIcon(const QIcon &icon) {\n\tQApplication::setWindowIcon(icon);\n}\n\nQString SingleInstanceLocalServerName(const QString &hash) {\n\treturn u\"Global\\\\\"_q + hash + '-' + cGUIDStr();\n}\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)\nstd::optional<bool> IsDarkMode() {\n\tstatic const auto kSystemVersion = QOperatingSystemVersion::current();\n\tstatic const auto kDarkModeAddedVersion = QOperatingSystemVersion(\n\t\tQOperatingSystemVersion::Windows,\n\t\t10,\n\t\t0,\n\t\t17763);\n\tstatic const auto kSupported = (kSystemVersion >= kDarkModeAddedVersion);\n\tif (!kSupported) {\n\t\treturn std::nullopt;\n\t}\n\n\tHIGHCONTRAST hcf = {};\n\thcf.cbSize = static_cast<UINT>(sizeof(HIGHCONTRAST));\n\tif (SystemParametersInfo(SPI_GETHIGHCONTRAST, hcf.cbSize, &hcf, FALSE)\n\t\t\t&& (hcf.dwFlags & HCF_HIGHCONTRASTON)) {\n\t\treturn std::nullopt;\n\t}\n\n\tconst auto keyName = L\"\"\n\t\t\"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Themes\\\\Personalize\";\n\tconst auto valueName = L\"AppsUseLightTheme\";\n\tauto key = HKEY();\n\tauto result = RegOpenKeyEx(HKEY_CURRENT_USER, keyName, 0, KEY_READ, &key);\n\tif (result != ERROR_SUCCESS) {\n\t\treturn std::nullopt;\n\t}\n\n\tDWORD value = 0, type = 0, size = sizeof(value);\n\tresult = RegQueryValueEx(key, valueName, 0, &type, (LPBYTE)&value, &size);\n\tRegCloseKey(key);\n\tif (result != ERROR_SUCCESS) {\n\t\treturn std::nullopt;\n\t}\n\n\treturn (value == 0);\n}\n#endif // Qt < 6.5.0\n\nbool AutostartSupported() {\n\treturn true;\n}\n\nvoid AutostartRequestStateFromSystem(Fn<void(bool)> callback) {\n#ifdef OS_WIN_STORE\n\tAutostartTask::RequestState([=](bool enabled) {\n\t\tcrl::on_main([=] {\n\t\t\tcallback(enabled);\n\t\t});\n\t});\n#endif // OS_WIN_STORE\n}\n\nvoid AutostartToggle(bool enabled, Fn<void(bool)> done) {\n#ifdef OS_WIN_STORE\n\tconst auto requested = enabled;\n\tconst auto callback = [=](bool enabled) { crl::on_main([=] {\n\t\tif (!Core::IsAppLaunched()) {\n\t\t\treturn;\n\t\t}\n\t\tdone(enabled);\n\t\tif (!requested || enabled) {\n\t\t\treturn;\n\t\t} else if (const auto window = Core::App().activeWindow()) {\n\t\t\twindow->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_settings_auto_start_disabled_uwp(),\n\t\t\t\t.confirmed = [](Fn<void()> close) {\n\t\t\t\t\tAutostartTask::OpenSettings();\n\t\t\t\t\tclose();\n\t\t\t\t},\n\t\t\t\t.confirmText = tr::lng_settings_open_system_settings(),\n\t\t\t}));\n\t\t}\n\t}); };\n\tAutostartTask::Toggle(\n\t\tenabled,\n\t\tdone ? Fn<void(bool)>(callback) : nullptr);\n#else // OS_WIN_STORE\n\tconst auto silent = !done;\n\tconst auto success = ManageAppLink(\n\t\tenabled,\n\t\tsilent,\n\t\tFOLDERID_Startup,\n\t\tL\"-autostart\",\n\t\tL\"Telegram autorun link.\\n\"\n\t\t\"You can disable autorun in Telegram settings.\");\n\tif (done) {\n\t\tdone(enabled && success);\n\t}\n#endif // OS_WIN_STORE\n}\n\nbool AutostartSkip() {\n#ifdef OS_WIN_STORE\n\treturn false;\n#else // OS_WIN_STORE\n\treturn !cAutoStart();\n#endif // OS_WIN_STORE\n}\n\nvoid WriteCrashDumpDetails() {\n#ifndef TDESKTOP_DISABLE_CRASH_REPORTS\n\tPROCESS_MEMORY_COUNTERS data = { 0 };\n\tif (Dlls::GetProcessMemoryInfo\n\t\t&& Dlls::GetProcessMemoryInfo(\n\t\t\tGetCurrentProcess(),\n\t\t\t&data,\n\t\t\tsizeof(data))) {\n\t\tconst auto mb = 1024 * 1024;\n\t\tCrashReports::dump()\n\t\t\t<< \"Memory-usage: \"\n\t\t\t<< (data.PeakWorkingSetSize / mb)\n\t\t\t<< \" MB (peak), \"\n\t\t\t<< (data.WorkingSetSize / mb)\n\t\t\t<< \" MB (current)\\n\";\n\t\tCrashReports::dump()\n\t\t\t<< \"Pagefile-usage: \"\n\t\t\t<< (data.PeakPagefileUsage / mb)\n\t\t\t<< \" MB (peak), \"\n\t\t\t<< (data.PagefileUsage / mb)\n\t\t\t<< \" MB (current)\\n\";\n\t}\n#endif // TDESKTOP_DISABLE_CRASH_REPORTS\n}\n\nvoid SetWindowPriority(not_null<QWidget*> window, uint32 priority) {\n\tconst auto hwnd = reinterpret_cast<HWND>(window->winId());\n\tAssert(hwnd != nullptr);\n\n\tSetWindowLongPtr(hwnd, GWLP_USERDATA, static_cast<LONG_PTR>(priority));\n}\n\nuint64 ActivationWindowId(not_null<QWidget*> window) {\n\treturn WindowIdFromHWND(reinterpret_cast<HWND>(window->winId()));\n}\n\nvoid ActivateOtherProcess(uint64 processId, uint64 windowId) {\n\tauto request = FindToActivateRequest{\n\t\t.processId = processId,\n\t\t.windowId = windowId,\n\t};\n\t::EnumWindows((WNDENUMPROC)FindToActivate, (LPARAM)&request);\n\tif (const auto hwnd = request.result) {\n\t\t::SetForegroundWindow(hwnd);\n\t\t::SetFocus(hwnd);\n\t}\n}\n\n} // namespace Platform\n\nnamespace {\n\tvoid _psLogError(const char *str, LSTATUS code) {\n\t\tLPWSTR errorTextFormatted = nullptr;\n\t\tauto formatFlags = FORMAT_MESSAGE_FROM_SYSTEM\n\t\t\t| FORMAT_MESSAGE_ALLOCATE_BUFFER\n\t\t\t| FORMAT_MESSAGE_IGNORE_INSERTS;\n\t\tFormatMessage(\n\t\t\tformatFlags,\n\t\t\tNULL,\n\t\t\tcode,\n\t\t\tMAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),\n\t\t\t(LPTSTR)&errorTextFormatted,\n\t\t\t0,\n\t\t\t0);\n\t\tauto errorText = errorTextFormatted\n\t\t\t? errorTextFormatted\n\t\t\t: L\"(Unknown error)\";\n\t\tLOG((str).arg(code).arg(QString::fromStdWString(errorText)));\n\t\tLocalFree(errorTextFormatted);\n\t}\n\n\tbool _psOpenRegKey(LPCWSTR key, PHKEY rkey) {\n\t\tDEBUG_LOG((\"App Info: opening reg key %1...\").arg(QString::fromStdWString(key)));\n\t\tLSTATUS status = RegOpenKeyEx(HKEY_CURRENT_USER, key, 0, KEY_QUERY_VALUE | KEY_WRITE, rkey);\n\t\tif (status != ERROR_SUCCESS) {\n\t\t\tif (status == ERROR_FILE_NOT_FOUND) {\n\t\t\t\tstatus = RegCreateKeyEx(HKEY_CURRENT_USER, key, 0, 0, REG_OPTION_NON_VOLATILE, KEY_QUERY_VALUE | KEY_WRITE, 0, rkey, 0);\n\t\t\t\tif (status != ERROR_SUCCESS) {\n\t\t\t\t\tQString msg = u\"App Error: could not create '%1' registry key, error %2\"_q.arg(QString::fromStdWString(key)).arg(u\"%1: %2\"_q);\n\t\t\t\t\t_psLogError(msg.toUtf8().constData(), status);\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tQString msg = u\"App Error: could not open '%1' registry key, error %2\"_q.arg(QString::fromStdWString(key)).arg(u\"%1: %2\"_q);\n\t\t\t\t_psLogError(msg.toUtf8().constData(), status);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\tbool _psSetKeyValue(HKEY rkey, LPCWSTR value, QString v) {\n\t\tstatic const int bufSize = 4096;\n\t\tDWORD defaultType, defaultSize = bufSize * 2;\n\t\tWCHAR defaultStr[bufSize] = { 0 };\n\t\tif (RegQueryValueEx(rkey, value, 0, &defaultType, (BYTE*)defaultStr, &defaultSize) != ERROR_SUCCESS || defaultType != REG_SZ || defaultSize != (v.size() + 1) * 2 || QString::fromStdWString(defaultStr) != v) {\n\t\t\tWCHAR tmp[bufSize] = { 0 };\n\t\t\tif (!v.isEmpty()) StringCbPrintf(tmp, bufSize, v.replace(QChar('%'), u\"%%\"_q).toStdWString().c_str());\n\t\t\tLSTATUS status = RegSetValueEx(rkey, value, 0, REG_SZ, (BYTE*)tmp, (wcslen(tmp) + 1) * sizeof(WCHAR));\n\t\t\tif (status != ERROR_SUCCESS) {\n\t\t\t\tQString msg = u\"App Error: could not set %1, error %2\"_q.arg(value ? ('\\'' + QString::fromStdWString(value) + '\\'') : u\"(Default)\"_q).arg(\"%1: %2\");\n\t\t\t\t_psLogError(msg.toUtf8().constData(), status);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n}\n\nnamespace Platform {\n\nPermissionStatus GetPermissionStatus(PermissionType type) {\n\tif (type == PermissionType::Microphone) {\n\t\tPermissionStatus result = PermissionStatus::Granted;\n\t\tHKEY hKey;\n\t\tLSTATUS res = RegOpenKeyEx(HKEY_CURRENT_USER, L\"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\CapabilityAccessManager\\\\ConsentStore\\\\microphone\", 0, KEY_QUERY_VALUE, &hKey);\n\t\tif (res == ERROR_SUCCESS) {\n\t\t\twchar_t buf[20];\n\t\t\tDWORD length = sizeof(buf);\n\t\t\tres = RegQueryValueEx(hKey, L\"Value\", NULL, NULL, (LPBYTE)buf, &length);\n\t\t\tif (res == ERROR_SUCCESS) {\n\t\t\t\tif (wcscmp(buf, L\"Deny\") == 0) {\n\t\t\t\t\tresult = PermissionStatus::Denied;\n\t\t\t\t}\n\t\t\t}\n\t\t\tRegCloseKey(hKey);\n\t\t}\n\t\treturn result;\n\t}\n\treturn PermissionStatus::Granted;\n}\n\nvoid RequestPermission(PermissionType type, Fn<void(PermissionStatus)> resultCallback) {\n\tresultCallback(PermissionStatus::Granted);\n}\n\nvoid OpenSystemSettingsForPermission(PermissionType type) {\n\tif (type == PermissionType::Microphone) {\n\t\tcrl::on_main([] {\n\t\t\tShellExecute(\n\t\t\t\tnullptr,\n\t\t\t\tL\"open\",\n\t\t\t\tL\"ms-settings:privacy-microphone\",\n\t\t\t\tnullptr,\n\t\t\t\tnullptr,\n\t\t\t\tSW_SHOWDEFAULT);\n\t\t});\n\t}\n}\n\nbool OpenSystemSettings(SystemSettingsType type) {\n\tif (type == SystemSettingsType::Audio) {\n\t\tcrl::on_main([] {\n\t\t\tWinExec(\"control.exe mmsys.cpl\", SW_SHOW);\n\t\t\t//QDesktopServices::openUrl(QUrl(\"ms-settings:sound\"));\n\t\t});\n\t}\n\treturn true;\n}\n\nvoid NewVersionLaunched(int oldVersion) {\n\tif (oldVersion <= 4009009) {\n\t\tAppUserModelId::CheckPinned();\n\t}\n\tif (oldVersion > 0 && oldVersion < 2008012) {\n\t\t// Reset icons cache, because we've changed the application icon.\n\t\tif (Dlls::SHChangeNotify) {\n\t\t\tDlls::SHChangeNotify(\n\t\t\t\tSHCNE_ASSOCCHANGED,\n\t\t\t\tSHCNF_IDLIST,\n\t\t\t\tnullptr,\n\t\t\t\tnullptr);\n\t\t}\n\t}\n}\n\nQImage DefaultApplicationIcon() {\n\treturn Window::Logo();\n}\n\nvoid LaunchMaps(const Data::LocationPoint &point, Fn<void()> fail) {\n\tconst auto aar = base::WinRT::TryCreateInstance<\n\t\tIApplicationAssociationRegistration\n\t>(CLSID_ApplicationAssociationRegistration);\n\tif (!aar) {\n\t\tfail();\n\t\treturn;\n\t}\n\n\tauto handler = base::CoTaskMemString();\n\tconst auto result = aar->QueryCurrentDefault(\n\t\tL\"geo\",\n\t\tAT_URLPROTOCOL,\n\t\tAL_EFFECTIVE,\n\t\thandler.put());\n\tif (FAILED(result)\n\t\t|| !handler\n\t\t|| !handler.data()\n\t\t|| std::wstring(handler.data()) == L\"geo\") {\n\t\tfail();\n\t\treturn;\n\t}\n\n\tconst auto url = u\"geo:%1,%2\"_q;\n\tif (!QDesktopServices::openUrl(\n\t\turl.arg(point.latAsString(), point.lonAsString()))) {\n\t\tfail();\n\t}\n}\n\n} // namespace Platform\n\nvoid psSendToMenu(bool send, bool silent) {\n\tManageAppLink(\n\t\tsend,\n\t\tsilent,\n\t\tFOLDERID_SendTo,\n\t\tL\"--\",\n\t\tL\"Telegram send to link.\\n\"\n\t\t\"You can disable send to menu item in Telegram settings.\");\n}\n\n// Stub while we still support Windows 7.\nextern \"C\" {\n\nSTDAPI GetDpiForMonitor(\n\t\t_In_ HMONITOR hmonitor,\n\t\t_In_ MONITOR_DPI_TYPE dpiType,\n\t\t_Out_ UINT *dpiX,\n\t\t_Out_ UINT *dpiY) {\n\treturn Dlls::GetDpiForMonitor\n\t\t? Dlls::GetDpiForMonitor(hmonitor, dpiType, dpiX, dpiY)\n\t\t: E_FAIL;\n}\n\n} // extern \"C\"\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/specific_win.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_specific.h\"\n\n#include <windows.h>\n\nnamespace Platform {\n\ninline void IgnoreApplicationActivationRightNow() {\n}\n\ninline bool TrayIconSupported() {\n\treturn true;\n}\n\ninline bool SkipTaskbarSupported() {\n\treturn true;\n}\n\ninline bool PreventsQuit(Core::QuitReason reason) {\n\treturn false;\n}\n\ninline void ActivateThisProcess() {\n}\n\n// 1 - secondary, 2 - primary.\nvoid SetWindowPriority(not_null<QWidget*> window, uint32 priority);\n\n[[nodiscard]] uint64 ActivationWindowId(not_null<QWidget*> window);\n\n// Activate window with windowId (if found) or the largest priority.\nvoid ActivateOtherProcess(uint64 processId, uint64 windowId);\n\ninline QString ApplicationIconName() {\n\treturn {};\n}\n\ninline QString ExecutablePathForShortcuts() {\n\treturn cExeDir() + cExeName();\n}\n\nnamespace ThirdParty {\n\nvoid start();\n\n} // namespace ThirdParty\n} // namespace Platform\n\ninline void psCheckLocalSocket(const QString &) {\n}\n\nQString psAppDataPath();\nQString psAppDataPathOld();\nvoid psSendToMenu(bool send, bool silent = false);\n\nint psCleanup();\nint psFixPrevious();\n\ninline QByteArray psDownloadPathBookmark(const QString &path) {\n\treturn QByteArray();\n}\ninline void psDownloadPathEnableAccess() {\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/text_recognition_win.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_text_recognition.h\"\n\nnamespace Platform {\nnamespace TextRecognition {\n\ninline bool IsAvailable() {\n\treturn false;\n}\n\ninline Result RecognizeText(const QImage &image) {\n\treturn {};\n}\n\n} // namespace TextRecognition\n} // namespace Platform"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/translate_provider_win.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_translate_provider.h\"\n\nnamespace Platform {\n\ninline bool IsTranslateProviderAvailable() {\n\treturn false;\n}\n\ninline std::unique_ptr<Ui::TranslateProvider> CreateTranslateProvider() {\n\treturn nullptr;\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/tray_win.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/win/tray_win.h\"\n\n#include \"base/invoke_queued.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"core/application.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"storage/localstorage.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_window.h\"\n\n#include <qpa/qplatformscreen.h>\n#include <qpa/qplatformsystemtrayicon.h>\n#include <qpa/qplatformtheme.h>\n#include <private/qguiapplication_p.h>\n#include <private/qhighdpiscaling_p.h>\n#include <QSvgRenderer>\n#include <QBuffer>\n\nnamespace Platform {\n\nnamespace {\n\nconstexpr auto kTooltipDelay = crl::time(10000);\n\nstd::optional<bool> DarkTaskbar;\nbool DarkTasbarValueValid/* = false*/;\n\n[[nodiscard]] std::optional<bool> ReadDarkTaskbarValue() {\n\tconst auto keyName = L\"\"\n\t\t\"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Themes\\\\Personalize\";\n\tconst auto valueName = L\"SystemUsesLightTheme\";\n\tauto key = HKEY();\n\tauto result = RegOpenKeyEx(HKEY_CURRENT_USER, keyName, 0, KEY_READ, &key);\n\tif (result != ERROR_SUCCESS) {\n\t\treturn std::nullopt;\n\t}\n\n\tDWORD value = 0, type = 0, size = sizeof(value);\n\tresult = RegQueryValueEx(key, valueName, 0, &type, (LPBYTE)&value, &size);\n\tRegCloseKey(key);\n\tif (result != ERROR_SUCCESS) {\n\t\treturn std::nullopt;\n\t}\n\n\treturn (value == 0);\n}\n\n[[nodiscard]] std::optional<bool> IsDarkTaskbar() {\n\tstatic const auto kSystemVersion = QOperatingSystemVersion::current();\n\tstatic const auto kDarkModeAddedVersion = QOperatingSystemVersion(\n\t\tQOperatingSystemVersion::Windows,\n\t\t10,\n\t\t0,\n\t\t18282);\n\tstatic const auto kSupported = (kSystemVersion >= kDarkModeAddedVersion);\n\tif (!kSupported) {\n\t\treturn std::nullopt;\n\t} else if (!DarkTasbarValueValid) {\n\t\tDarkTasbarValueValid = true;\n\t\tDarkTaskbar = ReadDarkTaskbarValue();\n\t}\n\treturn DarkTaskbar;\n}\n\n[[nodiscard]] QImage MonochromeIconFor(int size, bool darkMode) {\n\tExpects(size > 0);\n\n\tstatic const auto Content = [&] {\n\t\tauto f = QFile(u\":/gui/icons/tray/monochrome.svg\"_q);\n\t\treturn f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray();\n\t}();\n\tstatic auto Mask = QImage();\n\tstatic auto Size = 0;\n\tif (Mask.isNull() || Size != size) {\n\t\tSize = size;\n\t\tMask = QImage(size, size, QImage::Format_ARGB32_Premultiplied);\n\t\tMask.fill(Qt::transparent);\n\t\tauto p = QPainter(&Mask);\n\t\tQSvgRenderer(Content).render(&p, QRectF(0, 0, size, size));\n\t}\n\tstatic auto Colored = QImage();\n\tstatic auto ColoredDark = QImage();\n\tauto &use = darkMode ? ColoredDark : Colored;\n\tif (use.size() != Mask.size()) {\n\t\tconst auto color = darkMode ? 255 : 0;\n\t\tconst auto alpha = darkMode ? 255 : 228;\n\t\tuse = style::colorizeImage(Mask, { color, color, color, alpha });\n\t}\n\treturn use;\n}\n\n[[nodiscard]] QImage MonochromeWithDot(QImage image, style::color color) {\n\tauto p = QPainter(&image);\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto xm = image.width() / 16.;\n\tconst auto ym = image.height() / 16.;\n\tp.setBrush(color);\n\tp.setPen(Qt::NoPen);\n\tp.drawEllipse(QRectF( // cx=3.9, cy=12.7, r=2.2\n\t\t1.7 * xm,\n\t\t10.5 * ym,\n\t\t4.4 * xm,\n\t\t4.4 * ym));\n\treturn image;\n}\n\n[[nodiscard]] QImage ImageIconWithCounter(\n\t\tWindow::CounterLayerArgs &&args,\n\t\tbool supportMode,\n\t\tbool smallIcon,\n\t\tbool monochrome) {\n\tstatic auto ScaledLogo = base::flat_map<int, QImage>();\n\tstatic auto ScaledLogoNoMargin = base::flat_map<int, QImage>();\n\tstatic auto ScaledLogoDark = base::flat_map<int, QImage>();\n\tstatic auto ScaledLogoLight = base::flat_map<int, QImage>();\n\n\tconst auto darkMode = IsDarkTaskbar();\n\tauto &scaled = (monochrome && darkMode)\n\t\t? (*darkMode\n\t\t\t? ScaledLogoDark\n\t\t\t: ScaledLogoLight)\n\t\t: smallIcon\n\t\t? ScaledLogoNoMargin\n\t\t: ScaledLogo;\n\n\tauto result = [&] {\n\t\tif (const auto it = scaled.find(args.size); it != scaled.end()) {\n\t\t\treturn it->second;\n\t\t} else if (monochrome && darkMode) {\n\t\t\treturn MonochromeIconFor(args.size, *darkMode);\n\t\t}\n\t\treturn scaled.emplace(\n\t\t\targs.size,\n\t\t\t(smallIcon\n\t\t\t\t? Window::LogoNoMargin()\n\t\t\t\t: Window::Logo()\n\t\t\t).scaledToWidth(args.size, Qt::SmoothTransformation)\n\t\t).first->second;\n\t}();\n\tif ((!monochrome || !darkMode) && supportMode) {\n\t\tWindow::ConvertIconToBlack(result);\n\t}\n\tif (!args.count) {\n\t\treturn result;\n\t} else if (smallIcon) {\n\t\tif (monochrome && darkMode) {\n\t\t\treturn MonochromeWithDot(std::move(result), args.bg);\n\t\t}\n\t\treturn Window::WithSmallCounter(std::move(result), std::move(args));\n\t}\n\tQPainter p(&result);\n\tconst auto half = args.size / 2;\n\targs.size = half;\n\tp.drawPixmap(\n\t\thalf,\n\t\thalf,\n\t\tUi::PixmapFromImage(Window::GenerateCounterLayer(std::move(args))));\n\treturn result;\n}\n\n} // namespace\n\nTray::Tray() {\n}\n\nvoid Tray::createIcon() {\n\tif (!_icon) {\n\t\tif (const auto theme = QGuiApplicationPrivate::platformTheme()) {\n\t\t\t_icon.reset(theme->createPlatformSystemTrayIcon());\n\t\t}\n\t\tif (!_icon) {\n\t\t\treturn;\n\t\t}\n\t\t_icon->init();\n\t\tupdateIcon();\n\t\t_icon->updateToolTip(AppNameF.utf16());\n\n\t\tusing Reason = QPlatformSystemTrayIcon::ActivationReason;\n\t\tbase::qt_signal_producer(\n\t\t\t_icon.get(),\n\t\t\t&QPlatformSystemTrayIcon::activated\n\t\t) | rpl::filter(\n\t\t\trpl::mappers::_1 != Reason::Context\n\t\t) | rpl::map_to(\n\t\t\trpl::empty\n\t\t) | rpl::start_to_stream(_iconClicks, _lifetime);\n\n\t\tbase::qt_signal_producer(\n\t\t\t_icon.get(),\n\t\t\t&QPlatformSystemTrayIcon::contextMenuRequested\n\t\t) | rpl::filter([=] {\n\t\t\treturn _menu != nullptr;\n\t\t}) | rpl::on_next([=](\n\t\t\t\tQPoint globalNativePosition,\n\t\t\t\tconst QPlatformScreen *screen) {\n\t\t\t_aboutToShowRequests.fire({});\n\t\t\tconst auto position = QHighDpi::fromNativePixels(\n\t\t\t\tglobalNativePosition,\n\t\t\t\tscreen ? screen->screen() : nullptr);\n\t\t\tInvokeQueued(_menu.get(), [=] {\n\t\t\t\t_menu->popup(position);\n\t\t\t});\n\t\t}, _lifetime);\n\t} else {\n\t\tupdateIcon();\n\t}\n}\n\nvoid Tray::destroyIcon() {\n\t_icon = nullptr;\n}\n\nvoid Tray::updateIcon() {\n\tif (!_icon) {\n\t\treturn;\n\t}\n\tconst auto controller = Core::App().activePrimaryWindow();\n\tconst auto session = !controller\n\t\t? nullptr\n\t\t: !controller->sessionController()\n\t\t? nullptr\n\t\t: &controller->sessionController()->session();\n\n\t// Force Qt to use right icon size, not the larger one.\n\tQIcon forTrayIcon;\n\tforTrayIcon.addPixmap(\n\t\tTray::IconWithCounter(\n\t\t\tCounterLayerArgs(\n\t\t\t\tGetSystemMetrics(SM_CXSMICON),\n\t\t\t\tCore::App().unreadBadge(),\n\t\t\t\tCore::App().unreadBadgeMuted()),\n\t\t\ttrue,\n\t\t\tCore::App().settings().trayIconMonochrome(),\n\t\t\tsession && session->supportMode()));\n\t_icon->updateIcon(forTrayIcon);\n}\n\nvoid Tray::createMenu() {\n\tif (!_menu) {\n\t\t_menu = base::make_unique_q<Ui::PopupMenu>(nullptr);\n\t\t_menu->deleteOnHide(false);\n\t}\n}\n\nvoid Tray::destroyMenu() {\n\t_menu = nullptr;\n\t_actionsLifetime.destroy();\n}\n\nvoid Tray::addAction(rpl::producer<QString> text, Fn<void()> &&callback) {\n\tif (!_menu) {\n\t\treturn;\n\t}\n\n\t// If we try to activate() window before the _menu is hidden,\n\t// then the window will be shown in semi-active state (Qt bug).\n\t// It will receive input events, but it will be rendered as inactive.\n\tauto callbackLater = crl::guard(_menu.get(), [=] {\n\t\tusing namespace rpl::mappers;\n\t\t_callbackFromTrayLifetime = _menu->shownValue(\n\t\t) | rpl::filter(!_1) | rpl::take(1) | rpl::on_next([=] {\n\t\t\tcallback();\n\t\t});\n\t});\n\n\tconst auto action = _menu->addAction(QString(), std::move(callbackLater));\n\tstd::move(\n\t\ttext\n\t) | rpl::on_next([=](const QString &text) {\n\t\taction->setText(text);\n\t}, _actionsLifetime);\n}\n\nvoid Tray::showTrayMessage() const {\n\tif (!cSeenTrayTooltip() && _icon) {\n\t\t_icon->showMessage(\n\t\t\tAppNameF.utf16(),\n\t\t\ttr::lng_tray_icon_text(tr::now),\n\t\t\tQIcon(),\n\t\t\tQPlatformSystemTrayIcon::Information,\n\t\t\tkTooltipDelay);\n\t\tcSetSeenTrayTooltip(true);\n\t\tLocal::writeSettings();\n\t}\n}\n\nbool Tray::hasTrayMessageSupport() const {\n\treturn !cSeenTrayTooltip();\n}\n\nrpl::producer<> Tray::aboutToShowRequests() const {\n\treturn _aboutToShowRequests.events();\n}\n\nrpl::producer<> Tray::showFromTrayRequests() const {\n\treturn rpl::never<>();\n}\n\nrpl::producer<> Tray::hideToTrayRequests() const {\n\treturn rpl::never<>();\n}\n\nrpl::producer<> Tray::iconClicks() const {\n\treturn _iconClicks.events();\n}\n\nbool Tray::hasIcon() const {\n\treturn _icon;\n}\n\nrpl::lifetime &Tray::lifetime() {\n\treturn _lifetime;\n}\n\nWindow::CounterLayerArgs Tray::CounterLayerArgs(\n\t\tint size,\n\t\tint counter,\n\t\tbool muted) {\n\treturn Window::CounterLayerArgs{\n\t\t.size = size,\n\t\t.count = counter,\n\t\t.bg = muted ? st::trayCounterBgMute : st::trayCounterBg,\n\t\t.fg = st::trayCounterFg,\n\t};\n}\n\nQPixmap Tray::IconWithCounter(\n\t\tWindow::CounterLayerArgs &&args,\n\t\tbool smallIcon,\n\t\tbool monochrome,\n\t\tbool supportMode) {\n\tsupportMode |= Core::App().settings().fork().useBlackTrayIcon();\n\treturn Ui::PixmapFromImage(\n\t\tImageIconWithCounter(std::move(args), supportMode, smallIcon, monochrome));\n\treturn Ui::PixmapFromImage(ImageIconWithCounter(\n\t\tstd::move(args),\n\t\tsupportMode,\n\t\tsmallIcon,\n\t\tmonochrome));\n}\n\nvoid WriteIco(const QString &path, std::vector<QImage> images) {\n\tExpects(!images.empty());\n\n\tauto buffer = QByteArray();\n\tconst auto write = [&](auto value) {\n\t\tbuffer.append(reinterpret_cast<const char*>(&value), sizeof(value));\n\t};\n\n\tconst auto count = int(images.size());\n\n\tauto full = 0;\n\tauto pngs = std::vector<QByteArray>();\n\tpngs.reserve(count);\n\tfor (const auto &image : images) {\n\t\tpngs.emplace_back();\n\t\t{\n\t\t\tauto buffer = QBuffer(&pngs.back());\n\t\t\timage.save(&buffer, \"PNG\");\n\t\t}\n\t\tfull += pngs.back().size();\n\t}\n\n\t// Images directory\n\tconstexpr auto entry = sizeof(int8)\n\t\t+ sizeof(int8)\n\t\t+ sizeof(int8)\n\t\t+ sizeof(int8)\n\t\t+ sizeof(int16)\n\t\t+ sizeof(int16)\n\t\t+ sizeof(uint32)\n\t\t+ sizeof(uint32);\n\tstatic_assert(entry == 16);\n\n\tauto offset = 3 * sizeof(int16) + count * entry;\n\tfull += offset;\n\n\tbuffer.reserve(full);\n\n\t// Thanks https://stackoverflow.com/a/54289564/6509833\n\twrite(int16(0));\n\twrite(int16(1));\n\twrite(int16(count));\n\n\tfor (auto i = 0; i != count; ++i) {\n\t\tconst auto &image = images[i];\n\t\tAssert(image.width() <= 256 && image.height() <= 256);\n\n\t\twrite(int8(image.width() == 256 ? 0 : image.width()));\n\t\twrite(int8(image.height() == 256 ? 0 : image.height()));\n\t\twrite(int8(0)); // palette size\n\t\twrite(int8(0)); // reserved\n\t\twrite(int16(1)); // color planes\n\t\twrite(int16(image.depth())); // bits-per-pixel\n\t\twrite(uint32(pngs[i].size())); // size of image in bytes\n\t\twrite(uint32(offset)); // offset\n\t\toffset += pngs[i].size();\n\t}\n\tfor (auto i = 0; i != count; ++i) {\n\t\tbuffer.append(pngs[i]);\n\t}\n\n\tconst auto dir = QFileInfo(path).dir();\n\tdir.mkpath(dir.absolutePath());\n\tauto f = QFile(path);\n\tif (f.open(QIODevice::WriteOnly)) {\n\t\tf.write(buffer);\n\t}\n}\n\nQString Tray::QuitJumpListIconPath() {\n\tconst auto dark = IsDarkTaskbar();\n\tconst auto key = !dark ? 0 : *dark ? 1 : 2;\n\tconst auto path = cWorkingDir() + u\"tdata/temp/quit_%1.ico\"_q.arg(key);\n\tif (QFile::exists(path)) {\n\t\treturn path;\n\t}\n\tconst auto color = !dark\n\t\t? st::trayCounterBg->c\n\t\t: *dark\n\t\t? QColor(255, 255, 255)\n\t\t: QColor(0, 0, 0, 228);\n\tWriteIco(path, {\n\t\tst::winQuitIcon.instance(color, 100, true),\n\t\tst::winQuitIcon.instance(color, 200, true),\n\t\tst::winQuitIcon.instance(color, 300, true),\n\t});\n\treturn path;\n}\n\nbool HasMonochromeSetting() {\n\treturn IsDarkTaskbar().has_value();\n}\n\nvoid RefreshTaskbarThemeValue() {\n\tDarkTasbarValueValid = false;\n}\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/tray_win.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_tray.h\"\n\n#include \"base/unique_qptr.h\"\n\nnamespace Window {\nstruct CounterLayerArgs;\n} // namespace Window\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nclass QPlatformSystemTrayIcon;\n\nnamespace Platform {\n\nclass Tray final {\npublic:\n\tTray();\n\n\t[[nodiscard]] rpl::producer<> aboutToShowRequests() const;\n\t[[nodiscard]] rpl::producer<> showFromTrayRequests() const;\n\t[[nodiscard]] rpl::producer<> hideToTrayRequests() const;\n\t[[nodiscard]] rpl::producer<> iconClicks() const;\n\n\t[[nodiscard]] bool hasIcon() const;\n\n\tvoid createIcon();\n\tvoid destroyIcon();\n\n\tvoid updateIcon();\n\n\tvoid createMenu();\n\tvoid destroyMenu();\n\n\tvoid addAction(rpl::producer<QString> text, Fn<void()> &&callback);\n\n\tvoid showTrayMessage() const;\n\t[[nodiscard]] bool hasTrayMessageSupport() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\n\t// Windows only.\n\t[[nodiscard]] static Window::CounterLayerArgs CounterLayerArgs(\n\t\tint size,\n\t\tint counter,\n\t\tbool muted);\n\t[[nodiscard]] static QPixmap IconWithCounter(\n\t\tWindow::CounterLayerArgs &&args,\n\t\tbool smallIcon,\n\t\tbool monochrome,\n\t\tbool supportMode);\n\t[[nodiscard]] static QString QuitJumpListIconPath();\n\nprivate:\n\tbase::unique_qptr<QPlatformSystemTrayIcon> _icon;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\trpl::event_stream<> _iconClicks;\n\trpl::event_stream<> _aboutToShowRequests;\n\n\trpl::lifetime _callbackFromTrayLifetime;\n\trpl::lifetime _actionsLifetime;\n\trpl::lifetime _lifetime;\n\n};\n\nvoid RefreshTaskbarThemeValue();\n\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/webauthn_win.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\n#include \"platform/platform_webauthn.h\"\n\n#include \"base/platform/win/base_windows_safe_library.h\"\n#include \"data/data_passkey_deserialize.h\"\n\n#include <windows.h>\n#include <combaseapi.h>\n#include <webauthn.h>\n\n#include <QWindow>\n#include <QGuiApplication>\n\nnamespace Platform::WebAuthn {\nnamespace {\n\nHRESULT(__stdcall *WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable)(\n\tBOOL *pbIsUserVerifyingPlatformAuthenticatorAvailable);\nHRESULT(__stdcall *WebAuthNAuthenticatorMakeCredential)(\n\tHWND hWnd,\n\tPCWEBAUTHN_RP_ENTITY_INFORMATION pRpInformation,\n\tPCWEBAUTHN_USER_ENTITY_INFORMATION pUserInformation,\n\tPCWEBAUTHN_COSE_CREDENTIAL_PARAMETERS pPubKeyCredParams,\n\tPCWEBAUTHN_CLIENT_DATA pWebAuthNClientData,\n\tPCWEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS\n\t\tpWebAuthNCredentialOptions,\n\tPWEBAUTHN_CREDENTIAL_ATTESTATION *ppWebAuthNCredentialAttestation);\nvoid(__stdcall *WebAuthNFreeCredentialAttestation)(\n\tPWEBAUTHN_CREDENTIAL_ATTESTATION pWebAuthNCredentialAttestation);\nHRESULT(__stdcall *WebAuthNAuthenticatorGetAssertion)(\n\tHWND hWnd,\n\tLPCWSTR pwszRpId,\n\tPCWEBAUTHN_CLIENT_DATA pWebAuthNClientData,\n\tPCWEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS\n\t\tpWebAuthNGetAssertionOptions,\n\tPWEBAUTHN_ASSERTION *ppWebAuthNAssertion);\nvoid(__stdcall *WebAuthNFreeAssertion)(\n\tPWEBAUTHN_ASSERTION pWebAuthNAssertion);\n\n[[nodiscard]] bool Resolve() {\n\tconst auto webauthn = base::Platform::SafeLoadLibrary(L\"webauthn.dll\");\n\tif (!webauthn) {\n\t\treturn false;\n\t}\n\tauto total = 0, resolved = 0;\n#define LOAD_SYMBOL(name) \\\n\t++total; \\\n\tif (base::Platform::LoadMethod(webauthn, #name, name)) ++resolved;\n\n\tLOAD_SYMBOL(WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable);\n\tLOAD_SYMBOL(WebAuthNAuthenticatorMakeCredential);\n\tLOAD_SYMBOL(WebAuthNFreeCredentialAttestation);\n\tLOAD_SYMBOL(WebAuthNAuthenticatorGetAssertion);\n\tLOAD_SYMBOL(WebAuthNFreeAssertion);\n#undef LOAD_SYMBOL\n\n\treturn (total == resolved);\n}\n\n[[nodiscard]] bool Supported() {\n\tstatic const auto Result = Resolve();\n\treturn Result;\n}\n\n} // namespace\n\nbool IsSupported() {\n\tif (!Supported()) {\n\t\treturn false;\n\t}\n\tauto available = (BOOL)(FALSE);\n\treturn SUCCEEDED(\n\t\tWebAuthNIsUserVerifyingPlatformAuthenticatorAvailable(&available))\n\t\t\t&& available;\n}\n\nvoid RegisterKey(\n\t\tconst Data::Passkey::RegisterData &data,\n\t\tFn<void(RegisterResult result)> callback) {\n\tif (!Supported()) {\n\t\tcallback({});\n\t\treturn;\n\t}\n\n\tauto rpId = data.rp.id.toStdWString();\n\tauto rpName = data.rp.name.toStdWString();\n\tauto userName = data.user.name.toStdWString();\n\tauto userDisplayName = data.user.displayName.toStdWString();\n\n\tauto rpInfo = WEBAUTHN_RP_ENTITY_INFORMATION();\n\tmemset(&rpInfo, 0, sizeof(rpInfo));\n\trpInfo.dwVersion =\n\t\tWEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION;\n\trpInfo.pwszId = rpId.c_str();\n\trpInfo.pwszName = rpName.c_str();\n\n\tauto userInfo = WEBAUTHN_USER_ENTITY_INFORMATION();\n\tmemset(&userInfo, 0, sizeof(userInfo));\n\tuserInfo.dwVersion =\n\t\tWEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION;\n\tuserInfo.cbId = data.user.id.size();\n\tuserInfo.pbId = (PBYTE)data.user.id.data();\n\tuserInfo.pwszName = userName.c_str();\n\tuserInfo.pwszDisplayName = userDisplayName.c_str();\n\n\tauto credParams = std::vector<WEBAUTHN_COSE_CREDENTIAL_PARAMETER>();\n\tfor (const auto &param : data.pubKeyCredParams) {\n\t\tauto cp = WEBAUTHN_COSE_CREDENTIAL_PARAMETER{};\n\t\tcp.dwVersion =\n\t\t\tWEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION;\n\t\tauto type = param.type.toStdWString();\n\t\tcp.pwszCredentialType = type == L\"public-key\"\n\t\t\t? WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY\n\t\t\t: L\"\";\n\t\tcp.lAlg = param.alg;\n\t\tcredParams.push_back(cp);\n\t}\n\n\tauto credParamsList = WEBAUTHN_COSE_CREDENTIAL_PARAMETERS();\n\tmemset(&credParamsList, 0, sizeof(credParamsList));\n\tcredParamsList.cCredentialParameters = credParams.size();\n\tcredParamsList.pCredentialParameters = credParams.data();\n\n\tauto clientDataJson = Data::Passkey::SerializeClientDataCreate(\n\t\tdata.challenge);\n\tauto clientData = WEBAUTHN_CLIENT_DATA();\n\tmemset(&clientData, 0, sizeof(clientData));\n\tclientData.dwVersion = WEBAUTHN_CLIENT_DATA_CURRENT_VERSION;\n\tclientData.cbClientDataJSON = clientDataJson.size();\n\tclientData.pbClientDataJSON = (PBYTE)clientDataJson.data();\n\tclientData.pwszHashAlgId = WEBAUTHN_HASH_ALGORITHM_SHA_256;\n\n\tauto cancellationId = GUID();\n\tCoCreateGuid(&cancellationId);\n\n\tauto options = WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS();\n\tmemset(&options, 0, sizeof(options));\n\toptions.dwVersion =\n\t\tWEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_CURRENT_VERSION;\n\toptions.dwTimeoutMilliseconds = data.timeout;\n\toptions.dwAuthenticatorAttachment =\n\t\tWEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY;\n\toptions.bRequireResidentKey = FALSE;\n\toptions.dwUserVerificationRequirement =\n\t\tWEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED;\n\toptions.dwAttestationConveyancePreference =\n\t\tWEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE;\n\toptions.pCancellationId = &cancellationId;\n\n#if defined(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_4) \\\n\t|| defined(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_5) \\\n\t|| defined(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_6) \\\n\t|| defined(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_7) \\\n\t|| defined(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_8) \\\n\t|| defined(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_9)\n\toptions.bPreferResidentKey = TRUE;\n#endif\n\n\tauto hwnd = (HWND)(nullptr);\n\tif (auto window = QGuiApplication::topLevelWindows().value(0)) {\n\t\thwnd = (HWND)window->winId();\n\t\tif (hwnd) {\n\t\t\tSetForegroundWindow(hwnd);\n\t\t\tSetFocus(hwnd);\n\t\t}\n\t}\n\n\tauto attestation = (PWEBAUTHN_CREDENTIAL_ATTESTATION)(nullptr);\n\tauto hr = (HRESULT)(WebAuthNAuthenticatorMakeCredential)(\n\t\thwnd,\n\t\t&rpInfo,\n\t\t&userInfo,\n\t\t&credParamsList,\n\t\t&clientData,\n\t\t&options,\n\t\t&attestation);\n\n\tif (SUCCEEDED(hr) && attestation) {\n\t\tauto result = RegisterResult();\n\t\tresult.success = true;\n\t\tresult.credentialId = QByteArray(\n\t\t\t(char*)attestation->pbCredentialId,\n\t\t\tattestation->cbCredentialId);\n\t\tresult.attestationObject = QByteArray(\n\t\t\t(char*)attestation->pbAttestationObject,\n\t\t\tattestation->cbAttestationObject);\n\t\tresult.clientDataJSON = QByteArray::fromStdString(clientDataJson);\n\t\tWebAuthNFreeCredentialAttestation(attestation);\n\t\tcallback(result);\n\t} else {\n\t\tcallback({});\n\t}\n}\n\nvoid Login(\n\t\tconst Data::Passkey::LoginData &data,\n\t\tFn<void(LoginResult result)> callback) {\n\tif (!Supported()) {\n\t\tcallback({});\n\t\treturn;\n\t}\n\n\tauto rpId = data.rpId.toStdWString();\n\tauto clientDataJson = Data::Passkey::SerializeClientDataGet(\n\t\tdata.challenge);\n\n\tauto clientData = WEBAUTHN_CLIENT_DATA();\n\tmemset(&clientData, 0, sizeof(clientData));\n\tclientData.dwVersion = WEBAUTHN_CLIENT_DATA_CURRENT_VERSION;\n\tclientData.cbClientDataJSON = clientDataJson.size();\n\tclientData.pbClientDataJSON = (PBYTE)clientDataJson.data();\n\tclientData.pwszHashAlgId = WEBAUTHN_HASH_ALGORITHM_SHA_256;\n\n\tauto allowCredentials = std::vector<WEBAUTHN_CREDENTIAL>();\n\tauto credentialIds = std::vector<QByteArray>();\n\tfor (const auto &cred : data.allowCredentials) {\n\t\tcredentialIds.push_back(cred.id);\n\t\tauto credential = WEBAUTHN_CREDENTIAL();\n\t\tmemset(&credential, 0, sizeof(credential));\n\t\tcredential.dwVersion = WEBAUTHN_CREDENTIAL_CURRENT_VERSION;\n\t\tcredential.cbId = cred.id.size();\n\t\tcredential.pbId = (PBYTE)credentialIds.back().data();\n\t\tcredential.pwszCredentialType = WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY;\n\t\tallowCredentials.push_back(credential);\n\t}\n\n\tauto cancellationId = GUID();\n\tCoCreateGuid(&cancellationId);\n\n\tauto options = WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS();\n\tmemset(&options, 0, sizeof(options));\n\toptions.dwVersion =\n\t\tWEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_CURRENT_VERSION;\n\toptions.dwTimeoutMilliseconds = data.timeout;\n\tif (!allowCredentials.empty()) {\n\t\toptions.CredentialList.cCredentials = allowCredentials.size();\n\t\toptions.CredentialList.pCredentials = allowCredentials.data();\n\t}\n\toptions.pCancellationId = &cancellationId;\n\tif (data.userVerification == \"required\") {\n\t\toptions.dwUserVerificationRequirement =\n\t\t\tWEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED;\n\t} else if (data.userVerification == \"preferred\") {\n\t\toptions.dwUserVerificationRequirement =\n\t\t\tWEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED;\n\t} else {\n\t\toptions.dwUserVerificationRequirement =\n\t\t\tWEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED;\n\t}\n\n\tauto hwnd = (HWND)(nullptr);\n\tif (auto window = QGuiApplication::topLevelWindows().value(0)) {\n\t\thwnd = (HWND)window->winId();\n\t\tif (hwnd) {\n\t\t\tSetForegroundWindow(hwnd);\n\t\t\tSetFocus(hwnd);\n\t\t}\n\t}\n\n\tauto assertion = (PWEBAUTHN_ASSERTION)(nullptr);\n\tauto hr = (HRESULT)(WebAuthNAuthenticatorGetAssertion)(\n\t\thwnd,\n\t\trpId.c_str(),\n\t\t&clientData,\n\t\t&options,\n\t\t&assertion);\n\n\tif (SUCCEEDED(hr) && assertion) {\n\t\tauto result = LoginResult();\n\t\tresult.clientDataJSON = QByteArray::fromStdString(clientDataJson);\n\t\tresult.credentialId = QByteArray(\n\t\t\t(char*)assertion->Credential.pbId,\n\t\t\tassertion->Credential.cbId);\n\t\tresult.authenticatorData = QByteArray(\n\t\t\t(char*)assertion->pbAuthenticatorData,\n\t\t\tassertion->cbAuthenticatorData);\n\t\tresult.signature = QByteArray(\n\t\t\t(char*)assertion->pbSignature,\n\t\t\tassertion->cbSignature);\n\t\tresult.userHandle = assertion->cbUserId > 0\n\t\t\t? QByteArray((char*)assertion->pbUserId, assertion->cbUserId)\n\t\t\t: QByteArray();\n\t\tWebAuthNFreeAssertion(assertion);\n\t\tcallback(result);\n\t} else {\n\t\tcallback({});\n\t}\n}\n\n} // namespace Platform::WebAuthn\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/windows_app_user_model_id.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/win/windows_app_user_model_id.h\"\n\n#include \"platform/win/windows_dlls.h\"\n#include \"platform/win/windows_toast_activator.h\"\n#include \"base/platform/win/base_windows_winrt.h\"\n#include \"core/launcher.h\"\n\n#include <propvarutil.h>\n#include <propkey.h>\n\nnamespace Platform {\nnamespace AppUserModelId {\nnamespace {\n\nconstexpr auto kMaxFileLen = MAX_PATH * 2;\n\nconst PROPERTYKEY pkey_AppUserModel_ID = { { 0x9F4C2855, 0x9F79, 0x4B39, { 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3 } }, 5 };\nconst PROPERTYKEY pkey_AppUserModel_StartPinOption = { { 0x9F4C2855, 0x9F79, 0x4B39, { 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3 } }, 12 };\nconst PROPERTYKEY pkey_AppUserModel_ToastActivator = { { 0x9F4C2855, 0x9F79, 0x4B39, { 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3 } }, 26 };\n\n#ifdef OS_WIN_STORE\nconst WCHAR AppUserModelIdBase[] = L\"Telegram.TelegramDesktop.Store\";\n#else // OS_WIN_STORE\nconst WCHAR AppUserModelIdBase[] = L\"Telegram.TelegramDesktop\";\n#endif // OS_WIN_STORE\n\n[[nodiscard]] QString PinnedIconsPath() {\n\tWCHAR wstrPath[kMaxFileLen] = {};\n\tif (GetEnvironmentVariable(L\"APPDATA\", wstrPath, kMaxFileLen)) {\n\t\tauto appData = QDir(QString::fromStdWString(std::wstring(wstrPath)));\n\t\treturn appData.absolutePath()\n\t\t\t+ u\"/Microsoft/Internet Explorer/Quick Launch/User Pinned/TaskBar/\"_q;\n\t}\n\treturn QString();\n}\n\n} // namespace\n\nconst std::wstring &MyExecutablePath() {\n\tstatic const auto Path = [&] {\n\t\tauto result = std::wstring(kMaxFileLen, 0);\n\t\tconst auto length = GetModuleFileName(\n\t\t\tGetModuleHandle(nullptr),\n\t\t\tresult.data(),\n\t\t\tkMaxFileLen);\n\t\tif (!length || length == kMaxFileLen) {\n\t\t\tresult.clear();\n\t\t} else {\n\t\t\tresult.resize(length + 1);\n\t\t}\n\t\treturn result;\n\t}();\n\treturn Path;\n}\n\nUniqueFileId MyExecutablePathId() {\n\treturn GetUniqueFileId(MyExecutablePath().c_str());\n}\n\nUniqueFileId GetUniqueFileId(LPCWSTR path) {\n\tauto info = BY_HANDLE_FILE_INFORMATION{};\n\tconst auto file = CreateFile(\n\t\tpath,\n\t\t0,\n\t\t0,\n\t\tnullptr,\n\t\tOPEN_EXISTING,\n\t\tFILE_ATTRIBUTE_NORMAL,\n\t\tnullptr);\n\tif (file == INVALID_HANDLE_VALUE) {\n\t\treturn {};\n\t}\n\tconst auto result = GetFileInformationByHandle(file, &info);\n\tCloseHandle(file);\n\tif (!result) {\n\t\treturn {};\n\t}\n\treturn {\n\t\t.part1 = info.dwVolumeSerialNumber,\n\t\t.part2 = ((std::uint64_t(info.nFileIndexLow) << 32)\n\t\t\t| std::uint64_t(info.nFileIndexHigh)),\n\t};\n}\n\nvoid CheckPinned() {\n\tif (!SUCCEEDED(CoInitialize(0))) {\n\t\treturn;\n\t}\n\tconst auto coGuard = gsl::finally([] {\n\t\tCoUninitialize();\n\t});\n\n\tconst auto path = PinnedIconsPath();\n\tconst auto native = QDir::toNativeSeparators(path).toStdWString();\n\n\tconst auto srcid = MyExecutablePathId();\n\tif (!srcid) {\n\t\treturn;\n\t}\n\n\tLOG((\"Checking...\"));\n\tWIN32_FIND_DATA findData;\n\tHANDLE findHandle = FindFirstFileEx(\n\t\t(native + L\"*\").c_str(),\n\t\tFindExInfoStandard,\n\t\t&findData,\n\t\tFindExSearchNameMatch,\n\t\t0,\n\t\t0);\n\tif (findHandle == INVALID_HANDLE_VALUE) {\n\t\tLOG((\"Init Error: could not find files in pinned folder\"));\n\t\treturn;\n\t}\n\tdo {\n\t\tstd::wstring fname = native + findData.cFileName;\n\t\tLOG((\"Checking %1\").arg(QString::fromStdWString(fname)));\n\t\tif (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {\n\t\t\tcontinue;\n\t\t} else {\n\t\t\tDWORD attributes = GetFileAttributes(fname.c_str());\n\t\t\tif (attributes >= 0xFFFFFFF) {\n\t\t\t\tcontinue; // file does not exist\n\t\t\t}\n\n\t\t\tauto shellLink = base::WinRT::TryCreateInstance<IShellLink>(\n\t\t\t\tCLSID_ShellLink);\n\t\t\tif (!shellLink) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tauto persistFile = shellLink.try_as<IPersistFile>();\n\t\t\tif (!persistFile) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tauto hr = persistFile->Load(fname.c_str(), STGM_READWRITE);\n\t\t\tif (!SUCCEEDED(hr)) continue;\n\n\t\t\tWCHAR dst[MAX_PATH] = { 0 };\n\t\t\thr = shellLink->GetPath(dst, MAX_PATH, nullptr, 0);\n\t\t\tif (!SUCCEEDED(hr)) continue;\n\n\t\t\tif (GetUniqueFileId(dst) == srcid) {\n\t\t\t\tauto propertyStore = shellLink.try_as<IPropertyStore>();\n\t\t\t\tif (!propertyStore) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tPROPVARIANT appIdPropVar;\n\t\t\t\thr = propertyStore->GetValue(Key(), &appIdPropVar);\n\t\t\t\tif (!SUCCEEDED(hr)) return;\n\t\t\t\tLOG((\"Reading...\"));\n\t\t\t\tWCHAR already[MAX_PATH];\n\t\t\t\thr = PropVariantToString(appIdPropVar, already, MAX_PATH);\n\t\t\t\tif (SUCCEEDED(hr)) {\n\t\t\t\t\tif (Id() == already) {\n\t\t\t\t\t\tLOG((\"Already!\"));\n\t\t\t\t\t\tPropVariantClear(&appIdPropVar);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (appIdPropVar.vt != VT_EMPTY) {\n\t\t\t\t\tPropVariantClear(&appIdPropVar);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tPropVariantClear(&appIdPropVar);\n\n\t\t\t\thr = InitPropVariantFromString(Id().c_str(), &appIdPropVar);\n\t\t\t\tif (!SUCCEEDED(hr)) return;\n\n\t\t\t\thr = propertyStore->SetValue(Key(), appIdPropVar);\n\t\t\t\tPropVariantClear(&appIdPropVar);\n\t\t\t\tif (!SUCCEEDED(hr)) return;\n\n\t\t\t\thr = propertyStore->Commit();\n\t\t\t\tif (!SUCCEEDED(hr)) return;\n\n\t\t\t\tif (persistFile->IsDirty() == S_OK) {\n\t\t\t\t\tpersistFile->Save(fname.c_str(), TRUE);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t} while (FindNextFile(findHandle, &findData));\n\tDWORD errorCode = GetLastError();\n\tif (errorCode && errorCode != ERROR_NO_MORE_FILES) {\n\t\tLOG((\"Init Error: could not find some files in pinned folder\"));\n\t\treturn;\n\t}\n\tFindClose(findHandle);\n}\n\nQString systemShortcutPath() {\n\tWCHAR wstrPath[kMaxFileLen] = {};\n\tif (GetEnvironmentVariable(L\"APPDATA\", wstrPath, kMaxFileLen)) {\n\t\tauto appData = QDir(QString::fromStdWString(std::wstring(wstrPath)));\n\t\tconst auto path = appData.absolutePath();\n\t\treturn path + u\"/Microsoft/Windows/Start Menu/Programs/\"_q;\n\t}\n\treturn QString();\n}\n\nvoid CleanupShortcut() {\n\tconst auto myid = MyExecutablePathId();\n\tif (!myid) {\n\t\treturn;\n\t}\n\n\tQString path = systemShortcutPath() + u\"Telegram.lnk\"_q;\n\tstd::wstring p = QDir::toNativeSeparators(path).toStdWString();\n\n\tDWORD attributes = GetFileAttributes(p.c_str());\n\tif (attributes >= 0xFFFFFFF) return; // file does not exist\n\n\tauto shellLink = base::WinRT::TryCreateInstance<IShellLink>(\n\t\tCLSID_ShellLink);\n\tif (!shellLink) {\n\t\treturn;\n\t}\n\n\tauto persistFile = shellLink.try_as<IPersistFile>();\n\tif (!persistFile) {\n\t\treturn;\n\t}\n\n\tauto hr = persistFile->Load(p.c_str(), STGM_READWRITE);\n\tif (!SUCCEEDED(hr)) return;\n\n\tWCHAR szGotPath[MAX_PATH];\n\thr = shellLink->GetPath(szGotPath, MAX_PATH, nullptr, 0);\n\tif (!SUCCEEDED(hr)) return;\n\n\tif (GetUniqueFileId(szGotPath) == myid) {\n\t\tQFile().remove(path);\n\t}\n}\n\nbool validateShortcutAt(const QString &path) {\n\tconst auto native = QDir::toNativeSeparators(path).toStdWString();\n\n\tDWORD attributes = GetFileAttributes(native.c_str());\n\tif (attributes >= 0xFFFFFFF) {\n\t\treturn false; // file does not exist\n\t}\n\n\tauto shellLink = base::WinRT::TryCreateInstance<IShellLink>(\n\t\tCLSID_ShellLink);\n\tif (!shellLink) {\n\t\treturn false;\n\t}\n\n\tauto persistFile = shellLink.try_as<IPersistFile>();\n\tif (!persistFile) {\n\t\treturn false;\n\t}\n\n\tauto hr = persistFile->Load(native.c_str(), STGM_READWRITE);\n\tif (!SUCCEEDED(hr)) return false;\n\n\tWCHAR szGotPath[kMaxFileLen] = { 0 };\n\thr = shellLink->GetPath(szGotPath, kMaxFileLen, nullptr, 0);\n\tif (!SUCCEEDED(hr)) {\n\t\treturn false;\n\t}\n\n\tif (GetUniqueFileId(szGotPath) != MyExecutablePathId()) {\n\t\treturn false;\n\t}\n\n\tauto propertyStore = shellLink.try_as<IPropertyStore>();\n\tif (!propertyStore) {\n\t\treturn false;\n\t}\n\n\tPROPVARIANT appIdPropVar;\n\tPROPVARIANT toastActivatorPropVar;\n\thr = propertyStore->GetValue(Key(), &appIdPropVar);\n\tif (!SUCCEEDED(hr)) return false;\n\n\thr = propertyStore->GetValue(\n\t\tpkey_AppUserModel_ToastActivator,\n\t\t&toastActivatorPropVar);\n\tif (!SUCCEEDED(hr)) return false;\n\n\tWCHAR already[MAX_PATH];\n\thr = PropVariantToString(appIdPropVar, already, MAX_PATH);\n\tconst auto good1 = SUCCEEDED(hr) && (Id() == already);\n\tconst auto bad1 = !good1 && (appIdPropVar.vt != VT_EMPTY);\n\tPropVariantClear(&appIdPropVar);\n\n\tauto clsid = CLSID();\n\thr = PropVariantToCLSID(toastActivatorPropVar, &clsid);\n\tconst auto good2 = SUCCEEDED(hr) && (clsid == __uuidof(ToastActivator));\n\tconst auto bad2 = !good2 && (toastActivatorPropVar.vt != VT_EMPTY);\n\tPropVariantClear(&toastActivatorPropVar);\n\tif (good1 && good2) {\n\t\tLOG((\"App Info: Shortcut validated at \\\"%1\\\"\").arg(path));\n\t\treturn true;\n\t} else if (bad1 || bad2) {\n\t\treturn false;\n\t}\n\n\thr = InitPropVariantFromString(Id().c_str(), &appIdPropVar);\n\tif (!SUCCEEDED(hr)) return false;\n\n\thr = propertyStore->SetValue(Key(), appIdPropVar);\n\tPropVariantClear(&appIdPropVar);\n\tif (!SUCCEEDED(hr)) return false;\n\n\thr = InitPropVariantFromCLSID(\n\t\t__uuidof(ToastActivator),\n\t\t&toastActivatorPropVar);\n\tif (!SUCCEEDED(hr)) return false;\n\n\thr = propertyStore->SetValue(\n\t\tpkey_AppUserModel_ToastActivator,\n\t\ttoastActivatorPropVar);\n\tPropVariantClear(&toastActivatorPropVar);\n\tif (!SUCCEEDED(hr)) return false;\n\n\thr = propertyStore->Commit();\n\tif (!SUCCEEDED(hr)) return false;\n\n\tif (persistFile->IsDirty() == S_OK) {\n\t\thr = persistFile->Save(native.c_str(), TRUE);\n\t\tif (!SUCCEEDED(hr)) return false;\n\t}\n\n\tLOG((\"App Info: Shortcut set and validated at \\\"%1\\\"\").arg(path));\n\treturn true;\n}\n\nbool checkInstalled(QString path = {}) {\n\tif (path.isEmpty()) {\n\t\tpath = systemShortcutPath();\n\t\tif (path.isEmpty()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tconst auto installed = u\"Telegram Desktop/Telegram.lnk\"_q;\n\tconst auto old = u\"Telegram Win (Unofficial)/Telegram.lnk\"_q;\n\treturn validateShortcutAt(path + installed)\n\t\t|| validateShortcutAt(path + old);\n}\n\nbool ValidateShortcut() {\n\tQString path = systemShortcutPath();\n\tif (path.isEmpty() || cExeName().isEmpty()) {\n\t\treturn false;\n\t}\n\n\tif (cAlphaVersion()) {\n\t\tpath += u\"TelegramAlpha.lnk\"_q;\n\t\tif (validateShortcutAt(path)) {\n\t\t\treturn true;\n\t\t}\n\t} else {\n\t\tif (checkInstalled(path)) {\n\t\t\treturn true;\n\t\t}\n\n\t\tpath += u\"Telegram.lnk\"_q;\n\t\tif (validateShortcutAt(path)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\tauto shellLink = base::WinRT::TryCreateInstance<IShellLink>(\n\t\tCLSID_ShellLink);\n\tif (!shellLink) {\n\t\treturn false;\n\t}\n\n\tauto hr = shellLink->SetPath(MyExecutablePath().c_str());\n\tif (!SUCCEEDED(hr)) {\n\t\treturn false;\n\t}\n\n\thr = shellLink->SetArguments(L\"\");\n\tif (!SUCCEEDED(hr)) {\n\t\treturn false;\n\t}\n\n\thr = shellLink->SetWorkingDirectory(\n\t\tQDir::toNativeSeparators(\n\t\t\tQDir(cWorkingDir()).absolutePath()).toStdWString().c_str());\n\tif (!SUCCEEDED(hr)) {\n\t\treturn false;\n\t}\n\n\tauto propertyStore = shellLink.try_as<IPropertyStore>();\n\tif (!propertyStore) {\n\t\treturn false;\n\t}\n\n\tPROPVARIANT appIdPropVar;\n\thr = InitPropVariantFromString(Id().c_str(), &appIdPropVar);\n\tif (!SUCCEEDED(hr)) {\n\t\treturn false;\n\t}\n\n\thr = propertyStore->SetValue(Key(), appIdPropVar);\n\tPropVariantClear(&appIdPropVar);\n\tif (!SUCCEEDED(hr)) {\n\t\treturn false;\n\t}\n\n\tPROPVARIANT startPinPropVar;\n\thr = InitPropVariantFromUInt32(\n\t\tAPPUSERMODEL_STARTPINOPTION_NOPINONINSTALL,\n\t\t&startPinPropVar);\n\tif (!SUCCEEDED(hr)) {\n\t\treturn false;\n\t}\n\n\thr = propertyStore->SetValue(\n\t\tpkey_AppUserModel_StartPinOption,\n\t\tstartPinPropVar);\n\tPropVariantClear(&startPinPropVar);\n\tif (!SUCCEEDED(hr)) {\n\t\treturn false;\n\t}\n\n\tPROPVARIANT toastActivatorPropVar{};\n\thr = InitPropVariantFromCLSID(\n\t\t__uuidof(ToastActivator),\n\t\t&toastActivatorPropVar);\n\tif (!SUCCEEDED(hr)) {\n\t\treturn false;\n\t}\n\n\thr = propertyStore->SetValue(\n\t\tpkey_AppUserModel_ToastActivator,\n\t\ttoastActivatorPropVar);\n\tPropVariantClear(&toastActivatorPropVar);\n\tif (!SUCCEEDED(hr)) {\n\t\treturn false;\n\t}\n\n\thr = propertyStore->Commit();\n\tif (!SUCCEEDED(hr)) {\n\t\treturn false;\n\t}\n\n\tauto persistFile = shellLink.try_as<IPersistFile>();\n\tif (!persistFile) {\n\t\treturn false;\n\t}\n\n\thr = persistFile->Save(\n\t\tQDir::toNativeSeparators(path).toStdWString().c_str(),\n\t\tTRUE);\n\tif (!SUCCEEDED(hr)) {\n\t\treturn false;\n\t}\n\n\tLOG((\"App Info: Shortcut created and validated at \\\"%1\\\"\").arg(path));\n\treturn true;\n}\n\nconst std::wstring &Id() {\n\tstatic const auto BaseId = std::wstring(AppUserModelIdBase);\n\tstatic auto CheckingInstalled = false;\n\tif (CheckingInstalled) {\n\t\treturn BaseId;\n\t}\n\tstatic const auto Installed = [] {\n#ifdef OS_WIN_STORE\n\t\treturn true;\n#else // OS_WIN_STORE\n\t\tCheckingInstalled = true;\n\t\tconst auto guard = gsl::finally([] {\n\t\t\tCheckingInstalled = false;\n\t\t});\n\t\tif (!SUCCEEDED(CoInitialize(nullptr))) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto coGuard = gsl::finally([] {\n\t\t\tCoUninitialize();\n\t\t});\n\t\treturn checkInstalled();\n#endif\n\t}();\n\tif (Installed) {\n\t\treturn BaseId;\n\t}\n\tstatic const auto PortableId = [] {\n\t\tconst auto h = Core::Launcher::Instance().instanceHash();\n\t\treturn BaseId + L'.' + std::wstring(h.begin(), h.end());\n\t}();\n\treturn PortableId;\n}\n\nconst PROPERTYKEY &Key() {\n\treturn pkey_AppUserModel_ID;\n}\n\n} // namespace AppUserModelId\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/windows_app_user_model_id.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <wtypes.h>\n\nnamespace Platform {\nnamespace AppUserModelId {\n\nvoid CleanupShortcut();\nvoid CheckPinned();\n\n[[nodiscard]] const std::wstring &Id();\nbool ValidateShortcut();\n\n[[nodiscard]] const PROPERTYKEY &Key();\n\n[[nodiscard]] const std::wstring &MyExecutablePath();\n\nstruct UniqueFileId {\n\tstd::uint64_t part1 = 0;\n\tstd::uint64_t part2 = 0;\n\n\t[[nodiscard]] bool valid() const {\n\t\treturn part1 || part2;\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn valid();\n\t}\n\n\t[[nodiscard]] friend inline auto operator<=>(\n\t\tUniqueFileId a,\n\t\tUniqueFileId b) = default;\n\t[[nodiscard]] friend inline bool operator==(\n\t\tUniqueFileId a,\n\t\tUniqueFileId b) = default;\n};\n\n[[nodiscard]] UniqueFileId GetUniqueFileId(LPCWSTR path);\n[[nodiscard]] UniqueFileId MyExecutablePathId();\n\n} // namespace AppUserModelId\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/windows_autostart_task.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/win/windows_autostart_task.h\"\n\n#include \"base/platform/win/base_windows_winrt.h\"\n\n#include <winrt/Windows.ApplicationModel.h>\n#include <winrt/Windows.Foundation.h>\n#include <winrt/Windows.System.h>\n\nnamespace Platform::AutostartTask {\nnamespace {\n\nusing namespace winrt::Windows::ApplicationModel;\nusing namespace winrt::Windows::System;\nusing namespace winrt::Windows::Foundation;\n\n[[nodiscard]] bool IsEnabled(StartupTaskState state) {\n\tswitch (state) {\n\tcase StartupTaskState::Enabled:\n\tcase StartupTaskState::EnabledByPolicy:\n\t\treturn true;\n\tcase StartupTaskState::Disabled:\n\tcase StartupTaskState::DisabledByPolicy:\n\tcase StartupTaskState::DisabledByUser:\n\tdefault:\n\t\treturn false;\n\t}\n}\n\n} // namespace\n\nvoid Toggle(bool enabled, Fn<void(bool)> done) {\n\tif (!base::WinRT::Supported()) {\n\t\treturn;\n\t}\n\tconst auto processEnableResult = [=](StartupTaskState state) {\n\t\tLOG((\"Startup Task: Enable finished, state: %1\").arg(int(state)));\n\n\t\tdone(IsEnabled(state));\n\t};\n\tconst auto processTask = [=](StartupTask task) {\n\t\tLOG((\"Startup Task: Got it, state: %1, requested: %2\"\n\t\t\t).arg(int(task.State())\n\t\t\t).arg(Logs::b(enabled)));\n\n\t\tif (IsEnabled(task.State()) == enabled) {\n\t\t\treturn;\n\t\t}\n\t\tif (!enabled) {\n\t\t\tLOG((\"Startup Task: Disabling.\"));\n\t\t\ttask.Disable();\n\t\t\treturn;\n\t\t}\n\t\tLOG((\"Startup Task: Requesting enable.\"));\n\t\tconst auto asyncState = task.RequestEnableAsync();\n\t\tif (!done) {\n\t\t\treturn;\n\t\t}\n\t\tasyncState.Completed([=](\n\t\t\t\tIAsyncOperation<StartupTaskState> operation,\n\t\t\t\tAsyncStatus status) {\n\t\t\tbase::WinRT::Try([&] {\n\t\t\t\tprocessEnableResult(operation.GetResults());\n\t\t\t});\n\t\t});\n\t};\n\tbase::WinRT::Try([&] {\n\t\tStartupTask::GetAsync(L\"TelegramStartupTask\").Completed([=](\n\t\t\t\tIAsyncOperation<StartupTask> operation,\n\t\t\t\tAsyncStatus status) {\n\t\t\tbase::WinRT::Try([&] {\n\t\t\t\tprocessTask(operation.GetResults());\n\t\t\t});\n\t\t});\n\t});\n}\n\nvoid RequestState(Fn<void(bool)> callback) {\n\tExpects(callback != nullptr);\n\n\tif (!base::WinRT::Supported()) {\n\t\treturn;\n\t}\n\tconst auto processTask = [=](StartupTask task) {\n\t\tDEBUG_LOG((\"Startup Task: Got value, state: %1\"\n\t\t\t).arg(int(task.State())));\n\n\t\tcallback(IsEnabled(task.State()));\n\t};\n\tbase::WinRT::Try([&] {\n\t\tStartupTask::GetAsync(L\"TelegramStartupTask\").Completed([=](\n\t\t\t\tIAsyncOperation<StartupTask> operation,\n\t\t\t\tAsyncStatus status) {\n\t\t\tbase::WinRT::Try([&] {\n\t\t\t\tprocessTask(operation.GetResults());\n\t\t\t});\n\t\t});\n\t});\n}\n\nvoid OpenSettings() {\n\tLauncher::LaunchUriAsync(Uri(L\"ms-settings:startupapps\"));\n}\n\n} // namespace Platform::AutostartTask\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/windows_autostart_task.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Platform::AutostartTask {\n\nvoid Toggle(bool enabled, Fn<void(bool)> done);\nvoid RequestState(Fn<void(bool)> callback);\nvoid OpenSettings();\n\n} // namespace Platform::AutostartTask\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/windows_dlls.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/win/windows_dlls.h\"\n\n#include \"base/platform/win/base_windows_safe_library.h\"\n#include \"ui/gl/gl_detection.h\"\n\n#include <VersionHelpers.h>\n#include <QtCore/QSysInfo>\n\n#define LOAD_SYMBOL(lib, name) ::base::Platform::LoadMethod(lib, #name, name)\n\n#ifdef DESKTOP_APP_USE_ANGLE\nbool DirectXResolveCompiler();\n#endif // DESKTOP_APP_USE_ANGLE\n\nnamespace Platform {\nnamespace Dlls {\nnamespace {\n\nstruct SafeIniter {\n\tSafeIniter();\n};\n\nSafeIniter::SafeIniter() {\n\tbase::Platform::InitDynamicLibraries();\n\n\tconst auto LibShell32 = LoadLibrary(L\"shell32.dll\");\n\tLOAD_SYMBOL(LibShell32, SHAssocEnumHandlers);\n\tLOAD_SYMBOL(LibShell32, SHCreateItemFromParsingName);\n\tLOAD_SYMBOL(LibShell32, SHOpenWithDialog);\n\tLOAD_SYMBOL(LibShell32, OpenAs_RunDLL);\n\tLOAD_SYMBOL(LibShell32, SHQueryUserNotificationState);\n\tLOAD_SYMBOL(LibShell32, SHChangeNotify);\n\n\t//if (IsWindows10OrGreater()) {\n\t//\tstatic const auto kSystemVersion = QOperatingSystemVersion::current();\n\t//\tstatic const auto kMinor = kSystemVersion.minorVersion();\n\t//\tstatic const auto kBuild = kSystemVersion.microVersion();\n\t//\tif (kMinor > 0 || (kMinor == 0 && kBuild >= 17763)) {\n\t//\t\tconst auto LibUxTheme = LoadLibrary(L\"uxtheme.dll\");\n\t//\t\tif (kBuild < 18362) {\n\t//\t\t\tLOAD_SYMBOL(LibUxTheme, AllowDarkModeForApp, 135);\n\t//\t\t} else {\n\t//\t\t\tLOAD_SYMBOL(LibUxTheme, SetPreferredAppMode, 135);\n\t//\t\t}\n\t//\t\tLOAD_SYMBOL(LibUxTheme, AllowDarkModeForWindow, 133);\n\t//\t\tLOAD_SYMBOL(LibUxTheme, RefreshImmersiveColorPolicyState, 104);\n\t//\t\tLOAD_SYMBOL(LibUxTheme, FlushMenuThemes, 136);\n\t//\t}\n\t//}\n\n\tconst auto LibPropSys = LoadLibrary(L\"propsys.dll\");\n\tLOAD_SYMBOL(LibPropSys, PSStringFromPropertyKey);\n\n\tconst auto LibPsApi = LoadLibrary(L\"psapi.dll\");\n\tLOAD_SYMBOL(LibPsApi, GetProcessMemoryInfo);\n\n\tconst auto LibUser32 = LoadLibrary(L\"user32.dll\");\n\tLOAD_SYMBOL(LibUser32, SetWindowCompositionAttribute);\n\n\tconst auto LibShCore = LoadLibrary(L\"Shcore.dll\");\n\tLOAD_SYMBOL(LibShCore, GetDpiForMonitor);\n}\n\nSafeIniter kSafeIniter;\n\n} // namespace\n\nvoid CheckLoadedModules() {\n#ifdef DESKTOP_APP_USE_ANGLE\n\tif (DirectXResolveCompiler()) {\n\t\tauto LibD3DCompiler = HMODULE();\n\t\tif (GetModuleHandleEx(0, L\"d3dcompiler_47.dll\", &LibD3DCompiler)) {\n\t\t\tconstexpr auto kMaxPathLong = 32767;\n\t\t\tauto path = std::array<WCHAR, kMaxPathLong + 1>{ 0 };\n\t\t\tconst auto length = GetModuleFileName(\n\t\t\t\tLibD3DCompiler,\n\t\t\t\tpath.data(),\n\t\t\t\tkMaxPathLong);\n\t\t\tif (length > 0 && length < kMaxPathLong) {\n\t\t\t\tLOG((\"Using DirectX compiler '%1'.\"\n\t\t\t\t\t).arg(QString::fromWCharArray(path.data())));\n\t\t\t} else {\n\t\t\t\tLOG((\"Error: Could not resolve DirectX compiler path.\"));\n\t\t\t}\n\t\t} else {\n\t\t\tLOG((\"Error: Could not resolve DirectX compiler module.\"));\n\t\t}\n\t} else {\n\t\tLOG((\"Error: Could not resolve DirectX compiler library.\"));\n\t}\n#endif // DESKTOP_APP_USE_ANGLE\n}\n\n} // namespace Dlls\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/windows_dlls.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/platform/win/base_windows_shlobj_h.h\"\n\n#include <windows.h>\n#include <shellapi.h>\n#include <ShellScalingApi.h>\n#include <dwmapi.h>\n#include <RestartManager.h>\n#include <psapi.h>\n\nnamespace Platform {\nnamespace Dlls {\n\nvoid CheckLoadedModules();\n\n//inline void(__stdcall *RefreshImmersiveColorPolicyState)();\n//\n//inline BOOL(__stdcall *AllowDarkModeForApp)(BOOL allow);\n//\n//enum class PreferredAppMode {\n//\tDefault,\n//\tAllowDark,\n//\tForceDark,\n//\tForceLight,\n//\tMax\n//};\n//\n//inline PreferredAppMode(__stdcall *SetPreferredAppMode)(\n//\tPreferredAppMode appMode);\n//inline BOOL(__stdcall *AllowDarkModeForWindow)(HWND hwnd, BOOL allow);\n//inline void(__stdcall *FlushMenuThemes)();\n\n// SHELL32.DLL\ninline HRESULT(__stdcall *SHAssocEnumHandlers)(\n\tPCWSTR pszExtra,\n\tASSOC_FILTER afFilter,\n\tIEnumAssocHandlers **ppEnumHandler);\ninline HRESULT(__stdcall *SHCreateItemFromParsingName)(\n\tPCWSTR pszPath,\n\tIBindCtx *pbc,\n\tREFIID riid,\n\tvoid **ppv);\ninline HRESULT(__stdcall *SHOpenWithDialog)(\n\tHWND hwndParent,\n\tconst OPENASINFO *poainfo);\ninline HRESULT(__stdcall *OpenAs_RunDLL)(\n\tHWND hWnd,\n\tHINSTANCE hInstance,\n\tLPCWSTR lpszCmdLine,\n\tint nCmdShow);\ninline HRESULT(__stdcall *SHQueryUserNotificationState)(\n\tQUERY_USER_NOTIFICATION_STATE *pquns);\ninline void(__stdcall *SHChangeNotify)(\n\tLONG wEventId,\n\tUINT uFlags,\n\t__in_opt LPCVOID dwItem1,\n\t__in_opt LPCVOID dwItem2);\n\n// PROPSYS.DLL\n\ninline HRESULT(__stdcall *PSStringFromPropertyKey)(\n\t_In_ REFPROPERTYKEY pkey,\n\t_Out_writes_(cch) LPWSTR psz,\n\t_In_ UINT cch);\n\n// PSAPI.DLL\n\ninline BOOL(__stdcall *GetProcessMemoryInfo)(\n\tHANDLE Process,\n\tPPROCESS_MEMORY_COUNTERS ppsmemCounters,\n\tDWORD cb);\n\n// USER32.DLL\n\nenum class WINDOWCOMPOSITIONATTRIB {\n\tWCA_UNDEFINED = 0,\n\tWCA_NCRENDERING_ENABLED = 1,\n\tWCA_NCRENDERING_POLICY = 2,\n\tWCA_TRANSITIONS_FORCEDISABLED = 3,\n\tWCA_ALLOW_NCPAINT = 4,\n\tWCA_CAPTION_BUTTON_BOUNDS = 5,\n\tWCA_NONCLIENT_RTL_LAYOUT = 6,\n\tWCA_FORCE_ICONIC_REPRESENTATION = 7,\n\tWCA_EXTENDED_FRAME_BOUNDS = 8,\n\tWCA_HAS_ICONIC_BITMAP = 9,\n\tWCA_THEME_ATTRIBUTES = 10,\n\tWCA_NCRENDERING_EXILED = 11,\n\tWCA_NCADORNMENTINFO = 12,\n\tWCA_EXCLUDED_FROM_LIVEPREVIEW = 13,\n\tWCA_VIDEO_OVERLAY_ACTIVE = 14,\n\tWCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15,\n\tWCA_DISALLOW_PEEK = 16,\n\tWCA_CLOAK = 17,\n\tWCA_CLOAKED = 18,\n\tWCA_ACCENT_POLICY = 19,\n\tWCA_FREEZE_REPRESENTATION = 20,\n\tWCA_EVER_UNCLOAKED = 21,\n\tWCA_VISUAL_OWNER = 22,\n\tWCA_HOLOGRAPHIC = 23,\n\tWCA_EXCLUDED_FROM_DDA = 24,\n\tWCA_PASSIVEUPDATEMODE = 25,\n\tWCA_USEDARKMODECOLORS = 26,\n\tWCA_LAST = 27\n};\n\nstruct WINDOWCOMPOSITIONATTRIBDATA {\n\tWINDOWCOMPOSITIONATTRIB Attrib;\n\tPVOID pvData;\n\tSIZE_T cbData;\n};\n\ninline BOOL(__stdcall *SetWindowCompositionAttribute)(\n\tHWND hWnd,\n\tWINDOWCOMPOSITIONATTRIBDATA*);\n\n// SHCORE.DLL\ninline HRESULT(__stdcall *GetDpiForMonitor)(\n\t_In_ HMONITOR hmonitor,\n\t_In_ MONITOR_DPI_TYPE dpiType,\n\t_Out_ UINT *dpiX,\n\t_Out_ UINT *dpiY);\n\n} // namespace Dlls\n} // namespace Platform\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/windows_quiethours.idl",
    "content": "// © Rafael Rivera\n// License: MIT\n\nimport \"oaidl.idl\";\n\n[uuid(e0b5ef8b-a9b4-497a-8f71-08dd5c8ab2bf)]\nlibrary QuietHours\n{\n\t[uuid(f53321fa-34f8-4b7f-b9a3-361877cb94cf)]\n\tcoclass QuietHoursSettings\n\t{\n\t\t[default] interface IQuietHoursSettings;\n\t}\n\n\t[uuid(af86e2e0-b12d-4c6a-9c5a-d7aa65101e90)]\n\tinterface IQuietMoment : IUnknown\n\t{\n\t\t// Incomplete\n\t}\n\n\t[uuid(e813fe81-62b6-417d-b951-9d2e08486ac1)]\n\tinterface IQuietHoursProfile : IUnknown\n\t{\n\t\t[propget] HRESULT DisplayName([out, string, retval] LPWSTR* displayName);\n\t\t[propget] HRESULT ProfileId([out, string, retval] LPWSTR* profileId);\n\t\tHRESULT GetSetting(int setting, [out, retval] int* value);\n\t\tHRESULT PutSetting(int setting, int value);\n\t\t[propget] HRESULT IsCustomizable([out, retval] BOOL* result);\n\t\tHRESULT GetAllowedContacts([out] UINT32* count, [out, retval] LPWSTR* allowedContacts);\n\t\tHRESULT AddAllowedContact([in, string] LPWSTR allowedContact);\n\t\tHRESULT RemoveAllowedContact([in, string] LPWSTR allowedContact);\n\t\tHRESULT GetAllowedApps([out] UINT32* count, [out, retval] LPWSTR** allowedApps);\n\t\tHRESULT AddAllowedApp([in, string] LPWSTR allowedApp);\n\t\tHRESULT RemoveAllowedApp([in, string] LPWSTR allowedApp);\n\t\t[propget] HRESULT Description([out, string, retval] LPWSTR* description);\n\t\t[propget] HRESULT CustomizeLinkText([out, string, retval] LPWSTR* linkText);\n\t\t[propget] HRESULT RestrictiveLevel([out, string, retval] LPWSTR* restrictiveLevel);\n\t}\n\n\t[uuid(cd86a976-8ea9-404b-a197-42e73dbaa901)]\n\tinterface IQuietHoursPinnedContactManager : IUnknown\n\t{\n\t\tHRESULT GetPinnedContactList([out] UINT32* count, [out, string, retval] LPWSTR* pinnedContacts);\n\t}\n\n\t[uuid(b0217783-87b7-422c-b902-5c148c14f150)]\n\tinterface IQuietMomentsManager : IUnknown\n\t{\n\t\tHRESULT GetAllQuietMomentModes([out] UINT32* count, [out, retval] UINT32** quietMomentModes);\n\t\tHRESULT GetQuietMoment([in] UINT32 quietMomentId, [out, retval] IQuietMoment** quietMoment);\n\t\tHRESULT TurnOffCurrentlyActiveQuietMoment();\n\t\tHRESULT GetActiveQuietMoment([out, retval] UINT32* quietMomentId);\n\t}\n\n\ttypedef struct _QH_PROFILE_DATA\n\t{\n\t\tchar do_not_use__incomplete; // Incomplete\n\t} QH_PROFILE_DATA;\n\n\t[uuid(6bff4732-81ec-4ffb-ae67-b6c1bc29631f)]\n\tinterface IQuietHoursSettings : IUnknown\n\t{\n\t\t[propget] HRESULT UserSelectedProfile([out, string, retval] LPWSTR* profileId);\n\t\t[propput] HRESULT UserSelectedProfile([in] LPWSTR profileId);\n\t\tHRESULT GetProfile([in, string] LPWSTR profileId, [out, retval] IQuietHoursProfile**);\n\t\tHRESULT GetAllProfileData(UINT32* count, QH_PROFILE_DATA*);\n\t\tHRESULT GetDisplayNameForProfile([in, string] LPWSTR profileId, [out, string, retval] LPWSTR* displayName);\n\t\t[propget] HRESULT QuietMomentsManager([out, retval] IQuietMomentsManager**);\n\t\t[propget] HRESULT OffProfileId([out, string, retval] LPWSTR* profileId);\n\t\t[propget] HRESULT ActiveQuietMomentProfile([out, string, retval] LPWSTR* profileId);\n\t\t[propput] HRESULT ActiveQuietMomentProfile([in] LPWSTR profileId);\n\t\t[propget] HRESULT ActiveProfile([out, string, retval] LPWSTR* profileId);\n\t\t[propget] HRESULT QuietHoursPinnedContactManager([out, retval] IQuietHoursPinnedContactManager**);\n\t\t[propput] HRESULT QuietMomentsShowSummaryEnabled([out, retval] BOOL* isEnabled);\n\t\tHRESULT GetAlwaysAllowedApps([out] UINT32* count, [out, string, retval] LPWSTR** allowedApps);\n\t\tHRESULT StartProcessing();\n\t\tHRESULT StopProcessing();\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/windows_toast_activator.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"platform/win/windows_toast_activator.h\"\n\n#pragma warning(push)\n// class has virtual functions, but destructor is not virtual\n#pragma warning(disable:4265)\n#pragma warning(disable:5104)\n#include <wrl/module.h>\n#pragma warning(pop)\n\nnamespace {\n\nrpl::event_stream<ToastActivation> GlobalToastActivations;\n\n} // namespace\n\nQString ToastActivation::String(LPCWSTR value) {\n\tconst auto length = int(wcslen(value));\n\tauto result = value\n\t\t? QString::fromWCharArray(value, std::min(length, 16384))\n\t\t: QString();\n\tif (result.indexOf(QChar('\\n')) < 0) {\n\t\tresult.replace(QChar('\\r'), QChar('\\n'));\n\t}\n\treturn result;\n}\n\nHRESULT ToastActivator::Activate(\n\t\t_In_ LPCWSTR appUserModelId,\n\t\t_In_ LPCWSTR invokedArgs,\n\t\t_In_reads_(dataCount) const NOTIFICATION_USER_INPUT_DATA *data,\n\t\tULONG dataCount) {\n\tDEBUG_LOG((\"Toast Info: COM Activated \\\"%1\\\" with args \\\"%2\\\".\"\n\t\t).arg(QString::fromWCharArray(appUserModelId)\n\t\t).arg(QString::fromWCharArray(invokedArgs)));\n\tconst auto string = &ToastActivation::String;\n\tauto input = std::vector<ToastActivation::UserInput>();\n\tinput.reserve(dataCount);\n\tfor (auto i = 0; i != dataCount; ++i) {\n\t\tinput.push_back({\n\t\t\t.key = string(data[i].Key),\n\t\t\t.value = string(data[i].Value),\n\t\t});\n\t}\n\tauto activation = ToastActivation{\n\t\t.args = string(invokedArgs),\n\t\t.input = std::move(input),\n\t};\n\tcrl::on_main([activation = std::move(activation)]() mutable {\n\t\tGlobalToastActivations.fire(std::move(activation));\n\t});\n\treturn S_OK;\n}\n\nHRESULT ToastActivator::QueryInterface(\n\t\tREFIID riid,\n\t\tvoid **ppObj) {\n\tif (riid == IID_IUnknown\n\t\t|| riid == IID_INotificationActivationCallback) {\n\t\t*ppObj = static_cast<INotificationActivationCallback*>(this);\n\t\tAddRef();\n\t\treturn S_OK;\n\t}\n\n\t*ppObj = NULL;\n\treturn E_NOINTERFACE;\n}\n\nULONG ToastActivator::AddRef() {\n\treturn InterlockedIncrement(&_ref);\n}\n\nULONG ToastActivator::Release() {\n\tlong ref = 0;\n\tref = InterlockedDecrement(&_ref);\n\tif (!ref) {\n\t\tdelete this;\n\t}\n\treturn ref;\n}\n\nrpl::producer<ToastActivation> ToastActivations() {\n\treturn GlobalToastActivations.events();\n}\n\nCoCreatableClass(ToastActivator);\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/windows_toast_activator.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/platform/win/base_windows_rpcndr_h.h\"\n#include \"windows_toastactivator_h.h\"\n\n#include \"base/platform/win/wrl/wrl_implements_h.h\"\n\n// {F11932D3-6110-4BBC-9B02-B2EC07A1BD19}\nclass DECLSPEC_UUID(\"F11932D3-6110-4BBC-9B02-B2EC07A1BD19\") ToastActivator\n\t: public ::Microsoft::WRL::RuntimeClass<\n\t\t::Microsoft::WRL::RuntimeClassFlags<::Microsoft::WRL::ClassicCom>,\n\t\tINotificationActivationCallback,\n\t\t::Microsoft::WRL::FtmBase> {\npublic:\n\tToastActivator() = default;\n\t~ToastActivator() = default;\n\n\tHRESULT STDMETHODCALLTYPE Activate(\n\t\t_In_ LPCWSTR appUserModelId,\n\t\t_In_ LPCWSTR invokedArgs,\n\t\t_In_reads_(dataCount) const NOTIFICATION_USER_INPUT_DATA *data,\n\t\tULONG dataCount) override;\n\n\tHRESULT STDMETHODCALLTYPE QueryInterface(\n\t\tREFIID riid,\n\t\tvoid **ppObj);\n\tULONG STDMETHODCALLTYPE AddRef();\n\tULONG STDMETHODCALLTYPE Release();\n\nprivate:\n\tlong _ref = 1;\n\n};\n\nstruct ToastActivation {\n\tstruct UserInput {\n\t\tQString key;\n\t\tQString value;\n\t};\n\tQString args;\n\tstd::vector<UserInput> input;\n\n\t[[nodiscard]] static QString String(LPCWSTR value);\n};\n[[nodiscard]] rpl::producer<ToastActivation> ToastActivations();\n"
  },
  {
    "path": "Telegram/SourceFiles/platform/win/windows_toastactivator.idl",
    "content": "// ToastActivator.idl : IDL source for ToastActivator\n//\n\n// This file will be processed by the MIDL tool to\n// produce the type library (ToastActivator.tlb) and marshalling code.\n\nimport \"oaidl.idl\";\nimport \"ocidl.idl\";\n\ntypedef struct _NOTIFICATION_USER_INPUT_DATA\n{\n\tLPCWSTR Key;\n\tLPCWSTR Value;\n} NOTIFICATION_USER_INPUT_DATA;\n\n[\n\tobject,\n\tuuid(\"53E31837-6600-4A81-9395-75CFFE746F94\"),\n\tpointer_default(ref)\n]\n\ninterface INotificationActivationCallback : IUnknown\n{\n\tHRESULT Activate(\n\t\t[in, string] LPCWSTR appUserModelId,\n\t\t[in, string] LPCWSTR arguments, // arugments from the invoked button\n\t\t[in, size_is(count), unique] const NOTIFICATION_USER_INPUT_DATA* data, // data from all the input elements in the XML\n\t\t[in] ULONG count);\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/poll/poll_media_upload.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"poll/poll_media_upload.h\"\n\n#include \"api/api_sending.h\"\n#include \"apiwrap.h\"\n#include \"base/event_filter.h\"\n#include \"core/file_utilities.h\"\n#include \"core/mime_type.h\"\n#include \"data/data_document.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"layout/layout_document_generic_preview.h\"\n#include \"main/main_session.h\"\n#include \"platform/platform_file_utilities.h\"\n#include \"storage/file_upload.h\"\n#include \"storage/localimageloader.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_overview.h\"\n#include \"styles/style_polls.h\"\n\n#include <QtCore/QMimeData>\n\nnamespace PollMediaUpload {\n\nLocalImageThumbnail::LocalImageThumbnail(QImage original)\n: _original(std::move(original)) {\n}\n\nstd::shared_ptr<Ui::DynamicImage> LocalImageThumbnail::clone() {\n\treturn std::make_shared<LocalImageThumbnail>(_original);\n}\n\nQImage LocalImageThumbnail::image(int size) {\n\treturn _original;\n}\n\nvoid LocalImageThumbnail::subscribeToUpdates(Fn<void()> callback) {\n}\n\nQImage GenerateDocumentFilePreview(\n\t\tconst QString &filename,\n\t\tint size) {\n\tconst auto preview = Layout::DocumentGenericPreview::Create(filename);\n\tconst auto &color = preview.color;\n\tconst auto &ext = preview.ext;\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto result = QImage(\n\t\tQSize(size, size) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::transparent);\n\tresult.setDevicePixelRatio(ratio);\n\n\tauto p = QPainter(&result);\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(color);\n\tp.drawRoundedRect(\n\t\tQRect(0, 0, size, size),\n\t\tst::roundRadiusSmall,\n\t\tst::roundRadiusSmall);\n\n\tif (!ext.isEmpty()) {\n\t\tconst auto refSize = st::overviewFileLayout.fileThumbSize;\n\t\tconst auto fontSize = std::max(\n\t\t\tsize * st::overviewFileExtFont->f.pixelSize() / refSize,\n\t\t\t8);\n\t\tconst auto font = style::font(\n\t\t\tfontSize,\n\t\t\tst::overviewFileExtFont->flags(),\n\t\t\tst::overviewFileExtFont->family());\n\t\tconst auto padding = size * st::overviewFileExtPadding / refSize;\n\t\tconst auto maxw = size - padding * 2;\n\t\tauto extStr = ext;\n\t\tauto extw = font->width(extStr);\n\t\tif (extw > maxw) {\n\t\t\textStr = font->elided(extStr, maxw, Qt::ElideMiddle);\n\t\t\textw = font->width(extStr);\n\t\t}\n\t\tp.setFont(font);\n\t\tp.setPen(st::overviewFileExtFg);\n\t\tp.drawText(\n\t\t\t(size - extw) / 2,\n\t\t\t(size - font->height) / 2 + font->ascent,\n\t\t\textStr);\n\t}\n\tp.end();\n\treturn result;\n}\n\nbool ValidateFileDragData(not_null<const QMimeData*> data) {\n\tif (data->hasImage()) {\n\t\treturn true;\n\t}\n\tconst auto urls = Core::ReadMimeUrls(data);\n\treturn (urls.size() == 1) && urls.front().isLocalFile();\n}\n\nQVector<MTPDocumentAttribute> ExtractAudioAttributes(\n\t\tconst Ui::PreparedFile &file) {\n\tauto result = QVector<MTPDocumentAttribute>();\n\tif (!file.information) {\n\t\treturn result;\n\t}\n\tconst auto song = std::get_if<Ui::PreparedFileInformation::Song>(\n\t\t&file.information->media);\n\tif (!song) {\n\t\treturn result;\n\t}\n\tconst auto seconds = song->duration / 1000;\n\tusing Flag = MTPDdocumentAttributeAudio::Flag;\n\tresult.push_back(MTP_documentAttributeAudio(\n\t\tMTP_flags(Flag::f_title | Flag::f_performer),\n\t\tMTP_int(seconds),\n\t\tMTP_string(song->title),\n\t\tMTP_string(song->performer),\n\t\tMTPstring()));\n\treturn result;\n}\n\nUi::PreparedList FileListFromMimeData(\n\t\tnot_null<const QMimeData*> data,\n\t\tbool premium) {\n\tusing Error = Ui::PreparedList::Error;\n\tconst auto urls = Core::ReadMimeUrls(data);\n\tif (!urls.isEmpty()) {\n\t\treturn Storage::PrepareMediaList(\n\t\t\turls.mid(0, 1),\n\t\t\tst::sendMediaPreviewSize,\n\t\t\tpremium);\n\t} else if (auto read = Core::ReadMimeImage(data)) {\n\t\treturn Storage::PrepareMediaFromImage(\n\t\t\tstd::move(read.image),\n\t\t\tstd::move(read.content),\n\t\t\tst::sendMediaPreviewSize);\n\t}\n\treturn Ui::PreparedList(Error::EmptyFile, QString());\n}\n\nPollMediaButton::PollMediaButton(\n\tnot_null<QWidget*> parent,\n\tconst style::IconButton &st,\n\tstd::shared_ptr<PollMediaState> state)\n: Ui::RippleButton(parent, st.ripple)\n, _st(st)\n, _state(std::move(state))\n, _attach(Ui::MakeIconThumbnail(_st.icon))\n, _attachOver(_st.iconOver.empty()\n\t? _attach\n\t: Ui::MakeIconThumbnail(_st.iconOver))\n, _radial([=](crl::time now) { radialAnimationCallback(now); }) {\n\tconst auto weak = QPointer<PollMediaButton>(this);\n\t_state->update = [=] {\n\t\tif (weak) {\n\t\t\tweak->updateMediaSubscription();\n\t\t\tweak->update();\n\t\t}\n\t};\n\tresize(_st.width, _st.height);\n\tsetPointerCursor(true);\n\tupdateMediaSubscription();\n}\n\nvoid PollMediaButton::setIconColorOverride(\n\t\tstd::optional<QColor> colorOverride) {\n\t_iconColorOverride = colorOverride;\n\tupdate();\n}\n\nvoid PollMediaButton::setRippleColorOverride(\n\t\tstd::optional<QColor> colorOverride) {\n\t_rippleColorOverride = colorOverride;\n\tupdate();\n}\n\nPollMediaButton::~PollMediaButton() {\n\tif (_subscribed) {\n\t\t_subscribed->subscribeToUpdates(nullptr);\n\t}\n}\n\nvoid PollMediaButton::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\tpaintRipple(\n\t\tp,\n\t\t_st.rippleAreaPosition,\n\t\t_rippleColorOverride ? &*_rippleColorOverride : nullptr);\n\tif (_state->thumbnail) {\n\t\tconst auto target = rippleRect();\n\t\tpaintCover(\n\t\t\tp,\n\t\t\ttarget,\n\t\t\t_state->thumbnail->image(\n\t\t\t\tstd::max(target.width(), target.height())),\n\t\t\t_state->rounded);\n\t} else if (_iconColorOverride) {\n\t\tconst auto &icon = (isOver() && !_st.iconOver.empty())\n\t\t\t? _st.iconOver\n\t\t\t: _st.icon;\n\t\tauto position = _st.iconPosition;\n\t\tif (position.x() < 0) {\n\t\t\tposition.setX((width() - icon.width()) / 2);\n\t\t}\n\t\tif (position.y() < 0) {\n\t\t\tposition.setY((height() - icon.height()) / 2);\n\t\t}\n\t\ticon.paint(p, position, width(), *_iconColorOverride);\n\t} else if (const auto image = currentAttachThumbnail()) {\n\t\tconst auto target = iconRect();\n\t\tp.drawImage(\n\t\t\ttarget,\n\t\t\timage->image(std::max(target.width(), target.height())));\n\t}\n\tif (_state->thumbnail && !_state->uploading) {\n\t\tconst auto viewOpacity = _viewShown.value(\n\t\t\t(isOver() || isDown()) ? 1. : 0.);\n\t\tif (viewOpacity > 0.) {\n\t\t\tp.save();\n\t\t\tp.setOpacity(viewOpacity);\n\t\t\tauto path = QPainterPath();\n\t\t\tpath.addRoundedRect(\n\t\t\t\trippleRect(),\n\t\t\t\tst::roundRadiusSmall,\n\t\t\t\tst::roundRadiusSmall);\n\t\t\tp.setClipPath(path);\n\t\t\tp.fillRect(rippleRect(), st::songCoverOverlayFg);\n\t\t\tst::pollAttachView.paintInCenter(p, rippleRect());\n\t\t\tp.restore();\n\t\t}\n\t}\n\tif (_state->uploading && !_radial.animating()) {\n\t\t_radial.start(_state->progress);\n\t}\n\tif (_state->uploading || _radial.animating()) {\n\t\tif (_state->thumbnail) {\n\t\t\tp.save();\n\t\t\tauto path = QPainterPath();\n\t\t\tpath.addRoundedRect(\n\t\t\t\trippleRect(),\n\t\t\t\tst::roundRadiusSmall,\n\t\t\t\tst::roundRadiusSmall);\n\t\t\tp.setClipPath(path);\n\t\t\tp.fillRect(rippleRect(), st::songCoverOverlayFg);\n\t\t\tp.restore();\n\t\t}\n\t\tconst auto cancelOpacity = _state->uploading\n\t\t\t? _cancelShown.value(\n\t\t\t\t(isOver() || isDown()) ? 1. : 0.)\n\t\t\t: 0.;\n\t\tconst auto line = float64(st::lineWidth * 2);\n\t\tconst auto margin = float64(st::pollAttachProgressMargin);\n\t\tconst auto arc = QRectF(rippleRect()) - Margins(margin);\n\t\tif (cancelOpacity > 0.) {\n\t\t\tp.setOpacity(cancelOpacity);\n\t\t\tst::pollAttachCancel.paintInCenter(p, rippleRect());\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t\t_radial.draw(p, arc, line, st::historyFileThumbRadialFg);\n\t}\n}\n\nvoid PollMediaButton::onStateChanged(State was, StateChangeSource source) {\n\tRippleButton::onStateChanged(was, source);\n\tif (!_state->thumbnail) {\n\t\tupdate();\n\t}\n\tconst auto over = isOver() || isDown();\n\tconst auto wasOver = (was & StateFlag::Over)\n\t\t|| (was & StateFlag::Down);\n\tif (over != wasOver) {\n\t\tif (_state->uploading) {\n\t\t\t_cancelShown.start(\n\t\t\t\t[=] { update(); },\n\t\t\t\tover ? 0. : 1.,\n\t\t\t\tover ? 1. : 0.,\n\t\t\t\tst::universalDuration);\n\t\t}\n\t\tif (_state->thumbnail && !_state->uploading) {\n\t\t\t_viewShown.start(\n\t\t\t\t[=] { update(); },\n\t\t\t\tover ? 0. : 1.,\n\t\t\t\tover ? 1. : 0.,\n\t\t\t\tst::universalDuration);\n\t\t}\n\t}\n}\n\nQImage PollMediaButton::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::EllipseMask(QSize(\n\t\t_st.rippleAreaSize,\n\t\t_st.rippleAreaSize));\n}\n\nQPoint PollMediaButton::prepareRippleStartPosition() const {\n\tauto result = mapFromGlobal(QCursor::pos())\n\t\t- _st.rippleAreaPosition;\n\tconst auto rect = QRect(\n\t\tQPoint(),\n\t\tQSize(_st.rippleAreaSize, _st.rippleAreaSize));\n\treturn rect.contains(result)\n\t\t? result\n\t\t: DisabledRippleStartPosition();\n}\n\nvoid PollMediaButton::paintCover(\n\t\tPainter &p,\n\t\tQRect target,\n\t\tQImage image,\n\t\tbool rounded) const {\n\tif (image.isNull() || target.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto source = QRectF(0, 0, image.width(), image.height());\n\tconst auto kx = target.width() / source.width();\n\tconst auto ky = target.height() / source.height();\n\tconst auto scale = std::max(kx, ky);\n\tconst auto size = QSizeF(\n\t\tsource.width() * scale,\n\t\tsource.height() * scale);\n\tconst auto geometry = QRectF(\n\t\ttarget.x() + (target.width() - size.width()) / 2.,\n\t\ttarget.y() + (target.height() - size.height()) / 2.,\n\t\tsize.width(),\n\t\tsize.height());\n\tp.save();\n\tif (rounded) {\n\t\tauto path = QPainterPath();\n\t\tpath.addRoundedRect(\n\t\t\ttarget,\n\t\t\tst::roundRadiusSmall,\n\t\t\tst::roundRadiusSmall);\n\t\tp.setClipPath(path);\n\t}\n\tp.drawImage(geometry, image, source);\n\tp.restore();\n}\n\nvoid PollMediaButton::radialAnimationCallback(crl::time now) {\n\tconst auto updated = _radial.update(\n\t\t_state->progress,\n\t\t!_state->uploading,\n\t\tnow);\n\tif (!anim::Disabled() || updated || _radial.animating()) {\n\t\tupdate(rippleRect());\n\t}\n}\n\nQRect PollMediaButton::rippleRect() const {\n\treturn QRect(\n\t\t_st.rippleAreaPosition,\n\t\tQSize(_st.rippleAreaSize, _st.rippleAreaSize));\n}\n\nQRect PollMediaButton::iconRect() const {\n\tconst auto over = isOver() || isDown();\n\tconst auto &icon = over && !_st.iconOver.empty()\n\t\t? _st.iconOver\n\t\t: _st.icon;\n\tauto position = _st.iconPosition;\n\tif (position.x() < 0) {\n\t\tposition.setX((width() - icon.width()) / 2);\n\t}\n\tif (position.y() < 0) {\n\t\tposition.setY((height() - icon.height()) / 2);\n\t}\n\treturn QRect(position, QSize(icon.width(), icon.height()));\n}\n\nstd::shared_ptr<Ui::DynamicImage>\nPollMediaButton::currentAttachThumbnail() const {\n\treturn (isOver() || isDown()) ? _attachOver : _attach;\n}\n\nvoid PollMediaButton::updateMediaSubscription() {\n\tif (_subscribed == _state->thumbnail) {\n\t\treturn;\n\t}\n\tif (_subscribed) {\n\t\t_subscribed->subscribeToUpdates(nullptr);\n\t}\n\t_subscribed = _state->thumbnail;\n\tif (!_subscribed) {\n\t\treturn;\n\t}\n\tconst auto weak = QPointer<PollMediaButton>(this);\n\t_subscribed->subscribeToUpdates([=] {\n\t\tif (weak) {\n\t\t\tweak->update();\n\t\t}\n\t});\n}\n\nPreparePollMediaTask::PreparePollMediaTask(\n\tFileLoadTask::Args &&args,\n\tFn<void(std::shared_ptr<FilePrepareResult>)> done)\n: _task(std::move(args))\n, _done(std::move(done)) {\n}\n\nvoid PreparePollMediaTask::process() {\n\t_task.process({ .generateGoodThumbnail = false });\n}\n\nvoid PreparePollMediaTask::finish() {\n\t_done(_task.peekResult());\n}\n\nPollMediaUploader::PollMediaUploader(Args &&args)\n: _session(args.session)\n, _peer(args.peer)\n, _showError(std::move(args.showError))\n, _prepareQueue(std::make_unique<TaskQueue>()) {\n\tsubscribeToUploader();\n}\n\nPollMediaUploader::~PollMediaUploader() = default;\n\nvoid PollMediaUploader::subscribeToUploader() {\n\tauto &uploader = _session->uploader();\n\n\tuploader.photoReady(\n\t) | rpl::on_next([=](const Storage::UploadedMedia &data) {\n\t\tconst auto context = _uploads.take(data.fullId);\n\t\tif (!context) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto media = context->media.lock();\n\t\tif (!media || (media->token != context->token)) {\n\t\t\treturn;\n\t\t}\n\t\tapplyUploaded(media, context->token, data.fullId, data.info.file);\n\t}, _lifetime);\n\n\tuploader.photoProgress(\n\t) | rpl::on_next([=](const FullMsgId &id) {\n\t\tconst auto i = _uploads.find(id);\n\t\tif (i == _uploads.end()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto &context = i->second;\n\t\tconst auto media = context.media.lock();\n\t\tif (!media\n\t\t\t|| (media->token != context.token)\n\t\t\t|| !media->uploadDataId) {\n\t\t\treturn;\n\t\t}\n\t\tmedia->progress = _session->data().photo(\n\t\t\tmedia->uploadDataId)->progress();\n\t\tupdateMedia(media);\n\t}, _lifetime);\n\n\tuploader.photoFailed(\n\t) | rpl::on_next([=](const FullMsgId &id) {\n\t\tconst auto context = _uploads.take(id);\n\t\tif (!context) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto media = context->media.lock();\n\t\tif (!media || (media->token != context->token)) {\n\t\t\treturn;\n\t\t}\n\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t_showError(tr::lng_attach_failed(tr::now));\n\t}, _lifetime);\n\n\tuploader.documentReady(\n\t) | rpl::on_next([=](const Storage::UploadedMedia &data) {\n\t\tconst auto context = _uploads.take(data.fullId);\n\t\tif (!context) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto media = context->media.lock();\n\t\tif (!media || (media->token != context->token)) {\n\t\t\treturn;\n\t\t}\n\t\tapplyUploadedDocument(\n\t\t\tmedia,\n\t\t\tcontext->token,\n\t\t\tdata.fullId,\n\t\t\tdata.info,\n\t\t\t*context);\n\t}, _lifetime);\n\n\tuploader.documentProgress(\n\t) | rpl::on_next([=](const FullMsgId &id) {\n\t\tconst auto i = _uploads.find(id);\n\t\tif (i == _uploads.end()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto &context = i->second;\n\t\tconst auto media = context.media.lock();\n\t\tif (!media\n\t\t\t|| (media->token != context.token)\n\t\t\t|| !media->uploadDataId) {\n\t\t\treturn;\n\t\t}\n\t\tmedia->progress = _session->data().document(\n\t\t\tmedia->uploadDataId)->progress();\n\t\tupdateMedia(media);\n\t}, _lifetime);\n\n\tuploader.documentFailed(\n\t) | rpl::on_next([=](const FullMsgId &id) {\n\t\tconst auto context = _uploads.take(id);\n\t\tif (!context) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto media = context->media.lock();\n\t\tif (!media || (media->token != context->token)) {\n\t\t\treturn;\n\t\t}\n\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t_showError(tr::lng_attach_failed(tr::now));\n\t}, _lifetime);\n}\n\nvoid PollMediaUploader::updateMedia(\n\t\tconst std::shared_ptr<PollMediaState> &media) {\n\tif (media->update) {\n\t\tmedia->update();\n\t}\n}\n\nvoid PollMediaUploader::setMedia(\n\t\tconst std::shared_ptr<PollMediaState> &media,\n\t\tPollMedia value,\n\t\tstd::shared_ptr<Ui::DynamicImage> thumbnail,\n\t\tbool rounded) {\n\tconst auto wasUploading = media->uploading;\n\tmedia->token++;\n\tmedia->media = value;\n\tmedia->thumbnail = std::move(thumbnail);\n\tmedia->rounded = rounded;\n\tmedia->progress = (media->uploading && media->media)\n\t\t? 1.\n\t\t: 0.;\n\tmedia->uploadDataId = 0;\n\tmedia->uploading = false;\n\tif (wasUploading && value) {\n\t\tmedia->uploadedAt = crl::now();\n\t} else {\n\t\tmedia->uploadedAt = 0;\n\t\tmedia->reupload = nullptr;\n\t}\n\tupdateMedia(media);\n}\n\nauto PollMediaUploader::parseUploaded(\n\t\tconst MTPMessageMedia &result,\n\t\tFullMsgId fullId) -> UploadedMedia {\n\tauto parsed = UploadedMedia();\n\tauto &owner = _session->data();\n\tresult.match([&](const MTPDmessageMediaPhoto &media) {\n\t\tif (const auto photo = media.vphoto()) {\n\t\t\tphoto->match([&](const MTPDphoto &) {\n\t\t\t\tparsed.input.photo = owner.processPhoto(*photo);\n\t\t\t\tparsed.thumbnail = Ui::MakePhotoThumbnail(\n\t\t\t\t\tparsed.input.photo,\n\t\t\t\t\tfullId);\n\t\t\t}, [](const auto &) {\n\t\t\t});\n\t\t}\n\t}, [&](const MTPDmessageMediaDocument &media) {\n\t\tif (const auto document = media.vdocument()) {\n\t\t\tdocument->match([&](const MTPDdocument &) {\n\t\t\t\tparsed.input.document = owner.processDocument(\n\t\t\t\t\t*document);\n\t\t\t\tparsed.thumbnail\n\t\t\t\t\t= Ui::MakeDocumentFilePreviewThumbnail(\n\t\t\t\t\t\tparsed.input.document,\n\t\t\t\t\t\tfullId);\n\t\t\t}, [](const auto &) {\n\t\t\t});\n\t\t}\n\t}, [](const auto &) {\n\t});\n\treturn parsed;\n}\n\nvoid PollMediaUploader::applyUploaded(\n\t\tconst std::shared_ptr<PollMediaState> &media,\n\t\tuint64 token,\n\t\tFullMsgId fullId,\n\t\tconst MTPInputFile &file) {\n\tconst auto uploaded = MTP_inputMediaUploadedPhoto(\n\t\tMTP_flags(0),\n\t\tfile,\n\t\tMTP_vector<MTPInputDocument>(QVector<MTPInputDocument>()),\n\t\tMTPint(),\n\t\tMTPInputDocument());\n\t_session->api().request(MTPmessages_UploadMedia(\n\t\tMTP_flags(0),\n\t\tMTPstring(),\n\t\t_peer->input(),\n\t\tuploaded\n\t)).done([=](const MTPMessageMedia &result) {\n\t\tif (media->token != token) {\n\t\t\treturn;\n\t\t}\n\t\tauto parsed = parseUploaded(result, fullId);\n\t\tif (!parsed.input) {\n\t\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t\t_showError(tr::lng_attach_failed(tr::now));\n\t\t\treturn;\n\t\t}\n\t\tsetMedia(\n\t\t\tmedia,\n\t\t\tparsed.input,\n\t\t\tmedia->thumbnail\n\t\t\t\t? media->thumbnail\n\t\t\t\t: std::move(parsed.thumbnail),\n\t\t\ttrue);\n\t}).fail([=](const MTP::Error &) {\n\t\tif (media->token != token) {\n\t\t\treturn;\n\t\t}\n\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t_showError(tr::lng_attach_failed(tr::now));\n\t}).send();\n}\n\nvoid PollMediaUploader::applyUploadedDocument(\n\t\tconst std::shared_ptr<PollMediaState> &media,\n\t\tuint64 token,\n\t\tFullMsgId fullId,\n\t\tconst Api::RemoteFileInfo &info,\n\t\tconst UploadContext &context) {\n\tusing Flag = MTPDinputMediaUploadedDocument::Flag;\n\tconst auto flags = (context.forceFile ? Flag::f_force_file : Flag())\n\t\t| (info.thumb ? Flag::f_thumb : Flag());\n\tauto attributes = !context.attributes.isEmpty()\n\t\t? context.attributes\n\t\t: QVector<MTPDocumentAttribute>{\n\t\t\tMTP_documentAttributeFilename(\n\t\t\t\tMTP_string(context.filename)),\n\t\t};\n\tconst auto uploaded = MTP_inputMediaUploadedDocument(\n\t\tMTP_flags(flags),\n\t\tinfo.file,\n\t\tinfo.thumb.value_or(MTPInputFile()),\n\t\tMTP_string(context.filemime),\n\t\tMTP_vector<MTPDocumentAttribute>(std::move(attributes)),\n\t\tMTP_vector<MTPInputDocument>(),\n\t\tMTPInputPhoto(),\n\t\tMTP_int(0),\n\t\tMTP_int(0));\n\t_session->api().request(MTPmessages_UploadMedia(\n\t\tMTP_flags(0),\n\t\tMTPstring(),\n\t\t_peer->input(),\n\t\tuploaded\n\t)).done([=](const MTPMessageMedia &result) {\n\t\tif (media->token != token) {\n\t\t\treturn;\n\t\t}\n\t\tauto parsed = parseUploaded(result, fullId);\n\t\tif (!parsed.input) {\n\t\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t\t_showError(tr::lng_attach_failed(tr::now));\n\t\t\treturn;\n\t\t}\n\t\tconst auto isVideo = parsed.input.document\n\t\t\t&& parsed.input.document->isVideoFile();\n\t\tsetMedia(\n\t\t\tmedia,\n\t\t\tparsed.input,\n\t\t\tisVideo\n\t\t\t\t? (media->thumbnail\n\t\t\t\t\t? media->thumbnail\n\t\t\t\t\t: std::move(parsed.thumbnail))\n\t\t\t\t: std::move(parsed.thumbnail),\n\t\t\tisVideo);\n\t}).fail([=](const MTP::Error &) {\n\t\tif (media->token != token) {\n\t\t\treturn;\n\t\t}\n\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t_showError(tr::lng_attach_failed(tr::now));\n\t}).send();\n}\n\nvoid PollMediaUploader::startPhotoUpload(\n\t\tstd::shared_ptr<PollMediaState> media,\n\t\tUi::PreparedFile file) {\n\tconst auto token = ++media->token;\n\tmedia->media = PollMedia();\n\tmedia->thumbnail = std::make_shared<LocalImageThumbnail>(\n\t\tstd::move(file.preview));\n\tmedia->rounded = true;\n\tmedia->uploading = true;\n\tmedia->progress = 0.;\n\tmedia->uploadDataId = 0;\n\tupdateMedia(media);\n\t_prepareQueue->addTask(std::make_unique<PreparePollMediaTask>(\n\t\tFileLoadTask::Args{\n\t\t\t.session = _session,\n\t\t\t.filepath = file.path,\n\t\t\t.content = file.content,\n\t\t\t.information = std::move(file.information),\n\t\t\t.videoCover = nullptr,\n\t\t\t.type = SendMediaType::Photo,\n\t\t\t.to = FileLoadTo(\n\t\t\t\t_peer->id,\n\t\t\t\tApi::SendOptions(),\n\t\t\t\tFullReplyTo(),\n\t\t\t\tMsgId()),\n\t\t\t.caption = TextWithTags(),\n\t\t\t.spoiler = false,\n\t\t\t.album = nullptr,\n\t\t\t.forceFile = false,\n\t\t\t.idOverride = 0,\n\t\t\t.displayName = file.displayName,\n\t\t},\n\t\t[=](std::shared_ptr<FilePrepareResult> prepared) {\n\t\t\tif ((media->token != token)\n\t\t\t\t|| !prepared\n\t\t\t\t|| (prepared->type != SendMediaType::Photo)) {\n\t\t\t\tif (media->token == token) {\n\t\t\t\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t\t\t\t_showError(tr::lng_attach_failed(tr::now));\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto uploadId = FullMsgId(\n\t\t\t\t_peer->id,\n\t\t\t\t_session->data().nextLocalMessageId());\n\t\t\t_uploads.emplace(uploadId, UploadContext{\n\t\t\t\t.media = media,\n\t\t\t\t.token = token,\n\t\t\t});\n\t\t\tmedia->uploadDataId = prepared->id;\n\t\t\t_session->uploader().upload(uploadId, prepared);\n\t\t}));\n}\n\nvoid PollMediaUploader::startDocumentUpload(\n\t\tstd::shared_ptr<PollMediaState> media,\n\t\tUi::PreparedFile file) {\n\tconst auto displayName = file.displayName.isEmpty()\n\t\t? QFileInfo(file.path).fileName()\n\t\t: file.displayName;\n\tauto audioAttributes = ExtractAudioAttributes(file);\n\tconst auto isAudio = !audioAttributes.isEmpty();\n\tconst auto token = ++media->token;\n\tmedia->media = PollMedia();\n\tmedia->thumbnail = std::make_shared<LocalImageThumbnail>(\n\t\tGenerateDocumentFilePreview(\n\t\t\tdisplayName,\n\t\t\tst::pollAttach.rippleAreaSize));\n\tmedia->rounded = false;\n\tmedia->uploading = true;\n\tmedia->progress = 0.;\n\tmedia->uploadDataId = 0;\n\tupdateMedia(media);\n\t_prepareQueue->addTask(std::make_unique<PreparePollMediaTask>(\n\t\tFileLoadTask::Args{\n\t\t\t.session = _session,\n\t\t\t.filepath = file.path,\n\t\t\t.content = file.content,\n\t\t\t.information = std::move(file.information),\n\t\t\t.videoCover = nullptr,\n\t\t\t.type = SendMediaType::File,\n\t\t\t.to = FileLoadTo(\n\t\t\t\t_peer->id,\n\t\t\t\tApi::SendOptions(),\n\t\t\t\tFullReplyTo(),\n\t\t\t\tMsgId()),\n\t\t\t.caption = TextWithTags(),\n\t\t\t.spoiler = false,\n\t\t\t.album = nullptr,\n\t\t\t.forceFile = !isAudio,\n\t\t\t.idOverride = 0,\n\t\t\t.displayName = displayName,\n\t\t},\n\t\t[=, attributes = std::move(audioAttributes)](\n\t\t\t\tstd::shared_ptr<FilePrepareResult> prepared) {\n\t\t\tif ((media->token != token) || !prepared) {\n\t\t\t\tif (media->token == token) {\n\t\t\t\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t\t\t\t_showError(tr::lng_attach_failed(tr::now));\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto uploadId = FullMsgId(\n\t\t\t\t_peer->id,\n\t\t\t\t_session->data().nextLocalMessageId());\n\t\t\t_uploads.emplace(uploadId, UploadContext{\n\t\t\t\t.media = media,\n\t\t\t\t.token = token,\n\t\t\t\t.filename = prepared->filename,\n\t\t\t\t.filemime = prepared->filemime,\n\t\t\t\t.attributes = attributes,\n\t\t\t\t.forceFile = !isAudio,\n\t\t\t});\n\t\t\tmedia->uploadDataId = prepared->id;\n\t\t\t_session->uploader().upload(uploadId, prepared);\n\t\t}));\n}\n\nvoid PollMediaUploader::startVideoUpload(\n\t\tstd::shared_ptr<PollMediaState> media,\n\t\tUi::PreparedFile file) {\n\tconst auto token = ++media->token;\n\tmedia->media = PollMedia();\n\tmedia->thumbnail = std::make_shared<LocalImageThumbnail>(\n\t\tstd::move(file.preview));\n\tmedia->rounded = true;\n\tmedia->uploading = true;\n\tmedia->progress = 0.;\n\tmedia->uploadDataId = 0;\n\tupdateMedia(media);\n\t_prepareQueue->addTask(std::make_unique<PreparePollMediaTask>(\n\t\tFileLoadTask::Args{\n\t\t\t.session = _session,\n\t\t\t.filepath = file.path,\n\t\t\t.content = file.content,\n\t\t\t.information = std::move(file.information),\n\t\t\t.videoCover = nullptr,\n\t\t\t.type = SendMediaType::File,\n\t\t\t.to = FileLoadTo(\n\t\t\t\t_peer->id,\n\t\t\t\tApi::SendOptions(),\n\t\t\t\tFullReplyTo(),\n\t\t\t\tMsgId()),\n\t\t\t.caption = TextWithTags(),\n\t\t\t.spoiler = false,\n\t\t\t.album = nullptr,\n\t\t\t.forceFile = false,\n\t\t\t.idOverride = 0,\n\t\t\t.displayName = file.displayName,\n\t\t},\n\t\t[=](std::shared_ptr<FilePrepareResult> prepared) {\n\t\t\tif ((media->token != token) || !prepared) {\n\t\t\t\tif (media->token == token) {\n\t\t\t\t\tsetMedia(media, PollMedia(), nullptr, false);\n\t\t\t\t\t_showError(tr::lng_attach_failed(tr::now));\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto attributes = QVector<MTPDocumentAttribute>();\n\t\t\tprepared->document.match([&](const MTPDdocument &data) {\n\t\t\t\tattributes = data.vattributes().v;\n\t\t\t}, [](const auto &) {\n\t\t\t});\n\t\t\tconst auto uploadId = FullMsgId(\n\t\t\t\t_peer->id,\n\t\t\t\t_session->data().nextLocalMessageId());\n\t\t\t_uploads.emplace(uploadId, UploadContext{\n\t\t\t\t.media = media,\n\t\t\t\t.token = token,\n\t\t\t\t.filename = prepared->filename,\n\t\t\t\t.filemime = prepared->filemime,\n\t\t\t\t.attributes = std::move(attributes),\n\t\t\t\t.forceFile = false,\n\t\t\t});\n\t\t\tmedia->uploadDataId = prepared->id;\n\t\t\t_session->uploader().upload(uploadId, prepared);\n\t\t}));\n}\n\nbool PollMediaUploader::applyPreparedPhotoList(\n\t\tstd::shared_ptr<PollMediaState> media,\n\t\tUi::PreparedList &&list) {\n\tif (list.error != Ui::PreparedList::Error::None\n\t\t|| (list.files.size() != 1)\n\t\t|| (list.files.front().type != Ui::PreparedFile::Type::Photo)) {\n\t\treturn false;\n\t}\n\tstartPhotoUpload(media, std::move(list.files.front()));\n\treturn true;\n}\n\nvoid PollMediaUploader::clearMedia(std::shared_ptr<PollMediaState> media) {\n\tauto toCancel = std::vector<FullMsgId>();\n\tfor (auto i = _uploads.begin(); i != _uploads.end();) {\n\t\tif (i->second.media.lock() == media) {\n\t\t\ttoCancel.push_back(i->first);\n\t\t\ti = _uploads.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tfor (const auto &id : toCancel) {\n\t\t_session->uploader().cancel(id);\n\t}\n\tsetMedia(media, PollMedia(), nullptr, false);\n}\n\nvoid PollMediaUploader::choosePhotoOrVideo(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::shared_ptr<PollMediaState> media) {\n\tconst auto weak = QPointer<QWidget>(parent.get());\n\tconst auto callback = crl::guard(parent.get(), [=](\n\t\t\tFileDialog::OpenResult &&result) {\n\t\tconst auto checkResult = [&](const Ui::PreparedList &list) {\n\t\t\tusing namespace Ui;\n\t\t\tif (list.files.size() != 1) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto type = list.files.front().type;\n\t\t\treturn (type == PreparedFile::Type::Photo)\n\t\t\t\t|| (type == PreparedFile::Type::Video);\n\t\t};\n\t\tconst auto showError = [=](tr::phrase<> text) {\n\t\t\t_showError(text(tr::now));\n\t\t};\n\t\tauto list = Storage::PreparedFileFromFilesDialog(\n\t\t\tstd::move(result),\n\t\t\tcheckResult,\n\t\t\tshowError,\n\t\t\tst::sendMediaPreviewSize,\n\t\t\t_session->premium());\n\t\tif (!list) {\n\t\t\treturn;\n\t\t}\n\t\tauto &file = list->files.front();\n\t\tif (file.type == Ui::PreparedFile::Type::Photo) {\n\t\t\tapplyPreparedPhotoList(media, std::move(*list));\n\t\t} else {\n\t\t\tstartVideoUpload(media, std::move(file));\n\t\t}\n\t});\n\tFileDialog::GetOpenPath(\n\t\tQPointer<QWidget>(parent.get()),\n\t\ttr::lng_attach_photo_or_video(tr::now),\n\t\tFileDialog::PhotoVideoFilesFilter(),\n\t\tcallback);\n}\n\nvoid PollMediaUploader::chooseDocument(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::shared_ptr<PollMediaState> media) {\n\tconst auto callback = crl::guard(parent.get(), [=](\n\t\t\tFileDialog::OpenResult &&result) {\n\t\tif (result.paths.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tauto list = Storage::PrepareMediaList(\n\t\t\tresult.paths.mid(0, 1),\n\t\t\tst::sendMediaPreviewSize,\n\t\t\t_session->premium());\n\t\tif (list.error != Ui::PreparedList::Error::None\n\t\t\t|| list.files.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tstartDocumentUpload(\n\t\t\tmedia,\n\t\t\tstd::move(list.files.front()));\n\t});\n\tFileDialog::GetOpenPath(\n\t\tQPointer<QWidget>(parent.get()),\n\t\ttr::lng_attach_file(tr::now),\n\t\tFileDialog::AllFilesFilter(),\n\t\tcallback);\n}\n\nvoid PollMediaUploader::installDropToWidget(\n\t\tnot_null<QWidget*> widget,\n\t\tstd::shared_ptr<PollMediaState> media,\n\t\tbool allowDocuments) {\n\twidget->setAcceptDrops(true);\n\tbase::install_event_filter(widget, [=](not_null<QEvent*> event) {\n\t\tconst auto type = event->type();\n\t\tif (type != QEvent::DragEnter\n\t\t\t&& type != QEvent::DragMove\n\t\t\t&& type != QEvent::Drop) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\t\tconst auto drop = static_cast<QDropEvent*>(event.get());\n\t\tconst auto data = drop->mimeData();\n\t\tif (!data || !ValidateFileDragData(data)) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\t\tif (type == QEvent::Drop) {\n\t\t\tauto list = FileListFromMimeData(data, _session->premium());\n\t\t\tif (list.error != Ui::PreparedList::Error::None\n\t\t\t\t|| list.files.empty()) {\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t}\n\t\t\tauto &file = list.files.front();\n\t\t\tif (file.type == Ui::PreparedFile::Type::Photo) {\n\t\t\t\tstartPhotoUpload(media, std::move(file));\n\t\t\t} else if (file.type == Ui::PreparedFile::Type::Video) {\n\t\t\t\tstartVideoUpload(media, std::move(file));\n\t\t\t} else if (allowDocuments) {\n\t\t\t\tstartDocumentUpload(media, std::move(file));\n\t\t\t} else {\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t}\n\t\t}\n\t\tdrop->acceptProposedAction();\n\t\treturn base::EventFilterResult::Cancel;\n\t});\n}\n\nvoid PollMediaUploader::installDropToField(\n\t\tnot_null<Ui::InputField*> field,\n\t\tstd::shared_ptr<PollMediaState> media,\n\t\tbool allowDocuments) {\n\tfield->setMimeDataHook([=](\n\t\t\tnot_null<const QMimeData*> data,\n\t\t\tUi::InputField::MimeAction action) {\n\t\tif (action == Ui::InputField::MimeAction::Check) {\n\t\t\treturn ValidateFileDragData(data);\n\t\t} else if (action == Ui::InputField::MimeAction::Insert) {\n\t\t\tauto list = FileListFromMimeData(data, _session->premium());\n\t\t\tif (list.error != Ui::PreparedList::Error::None\n\t\t\t\t|| list.files.empty()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tauto &file = list.files.front();\n\t\t\tif (file.type == Ui::PreparedFile::Type::Photo) {\n\t\t\t\tstartPhotoUpload(media, std::move(file));\n\t\t\t\treturn true;\n\t\t\t} else if (file.type == Ui::PreparedFile::Type::Video) {\n\t\t\t\tstartVideoUpload(media, std::move(file));\n\t\t\t\treturn true;\n\t\t\t} else if (allowDocuments) {\n\t\t\t\tstartDocumentUpload(media, std::move(file));\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\tUnexpected(\"Polls: action in MimeData hook.\");\n\t});\n}\n\n} // namespace PollMediaUpload\n"
  },
  {
    "path": "Telegram/SourceFiles/poll/poll_media_upload.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_poll.h\"\n#include \"storage/localimageloader.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/widgets/buttons.h\"\n\nstruct FilePrepareResult;\n\nnamespace Api {\nstruct RemoteFileInfo;\n} // namespace Api\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Storage {\nstruct UploadedMedia;\n} // namespace Storage\n\nnamespace Ui {\nclass InputField;\nstruct PreparedFile;\nstruct PreparedList;\n} // namespace Ui\n\nclass PeerData;\n\nnamespace PollMediaUpload {\n\nstruct PollMediaState {\n\tPollMedia media;\n\tstd::shared_ptr<Ui::DynamicImage> thumbnail;\n\tbool rounded = false;\n\tbool uploading = false;\n\tfloat64 progress = 0.;\n\tuint64 uploadDataId = 0;\n\tuint64 token = 0;\n\tcrl::time uploadedAt = 0;\n\tFn<void()> update;\n\tFn<void()> reupload;\n};\n\nclass LocalImageThumbnail final : public Ui::DynamicImage {\npublic:\n\texplicit LocalImageThumbnail(QImage original);\n\n\tstd::shared_ptr<Ui::DynamicImage> clone() override;\n\tQImage image(int size) override;\n\tvoid subscribeToUpdates(Fn<void()> callback) override;\n\nprivate:\n\tQImage _original;\n\n};\n\n[[nodiscard]] QImage GenerateDocumentFilePreview(\n\tconst QString &filename,\n\tint size);\n\n[[nodiscard]] QVector<MTPDocumentAttribute> ExtractAudioAttributes(\n\tconst Ui::PreparedFile &file);\n\n[[nodiscard]] bool ValidateFileDragData(not_null<const QMimeData*> data);\n\n[[nodiscard]] Ui::PreparedList FileListFromMimeData(\n\tnot_null<const QMimeData*> data,\n\tbool premium);\n\nclass PollMediaButton final : public Ui::RippleButton {\npublic:\n\tPollMediaButton(\n\t\tnot_null<QWidget*> parent,\n\t\tconst style::IconButton &st,\n\t\tstd::shared_ptr<PollMediaState> state);\n\t~PollMediaButton() override;\n\n\tvoid setIconColorOverride(std::optional<QColor> colorOverride);\n\tvoid setRippleColorOverride(std::optional<QColor> colorOverride);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid onStateChanged(State was, StateChangeSource source) override;\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\nprivate:\n\tvoid paintCover(\n\t\tPainter &p,\n\t\tQRect target,\n\t\tQImage image,\n\t\tbool rounded) const;\n\tvoid radialAnimationCallback(crl::time now);\n\t[[nodiscard]] QRect rippleRect() const;\n\t[[nodiscard]] QRect iconRect() const;\n\tstd::shared_ptr<Ui::DynamicImage> currentAttachThumbnail() const;\n\tvoid updateMediaSubscription();\n\n\tconst style::IconButton &_st;\n\tconst std::shared_ptr<PollMediaState> _state;\n\tconst std::shared_ptr<Ui::DynamicImage> _attach;\n\tconst std::shared_ptr<Ui::DynamicImage> _attachOver;\n\tstd::shared_ptr<Ui::DynamicImage> _subscribed;\n\tUi::RadialAnimation _radial;\n\tUi::Animations::Simple _cancelShown;\n\tUi::Animations::Simple _viewShown;\n\tstd::optional<QColor> _iconColorOverride;\n\tstd::optional<QColor> _rippleColorOverride;\n\n};\n\nclass PreparePollMediaTask final : public Task {\npublic:\n\tPreparePollMediaTask(\n\t\tFileLoadTask::Args &&args,\n\t\tFn<void(std::shared_ptr<FilePrepareResult>)> done);\n\n\tvoid process() override;\n\tvoid finish() override;\n\nprivate:\n\tFileLoadTask _task;\n\tFn<void(std::shared_ptr<FilePrepareResult>)> _done;\n\n};\n\nstruct UploadContext {\n\tstd::weak_ptr<PollMediaState> media;\n\tuint64 token = 0;\n\tQString filename;\n\tQString filemime;\n\tQVector<MTPDocumentAttribute> attributes;\n\tbool forceFile = true;\n};\n\nclass PollMediaUploader final {\npublic:\n\tstruct Args {\n\t\tnot_null<Main::Session*> session;\n\t\tnot_null<PeerData*> peer;\n\t\tFn<void(const QString&)> showError;\n\t};\n\n\texplicit PollMediaUploader(Args &&args);\n\t~PollMediaUploader();\n\n\tvoid startPhotoUpload(\n\t\tstd::shared_ptr<PollMediaState> media,\n\t\tUi::PreparedFile file);\n\tvoid startVideoUpload(\n\t\tstd::shared_ptr<PollMediaState> media,\n\t\tUi::PreparedFile file);\n\tvoid startDocumentUpload(\n\t\tstd::shared_ptr<PollMediaState> media,\n\t\tUi::PreparedFile file);\n\tbool applyPreparedPhotoList(\n\t\tstd::shared_ptr<PollMediaState> media,\n\t\tUi::PreparedList &&list);\n\n\tvoid setMedia(\n\t\tconst std::shared_ptr<PollMediaState> &media,\n\t\tPollMedia value,\n\t\tstd::shared_ptr<Ui::DynamicImage> thumbnail,\n\t\tbool rounded);\n\tvoid clearMedia(std::shared_ptr<PollMediaState> media);\n\n\tvoid choosePhotoOrVideo(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::shared_ptr<PollMediaState> media);\n\tvoid chooseDocument(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::shared_ptr<PollMediaState> media);\n\n\tvoid installDropToWidget(\n\t\tnot_null<QWidget*> widget,\n\t\tstd::shared_ptr<PollMediaState> media,\n\t\tbool allowDocuments);\n\tvoid installDropToField(\n\t\tnot_null<Ui::InputField*> field,\n\t\tstd::shared_ptr<PollMediaState> media,\n\t\tbool allowDocuments);\n\nprivate:\n\tstruct UploadedMedia {\n\t\tPollMedia input;\n\t\tstd::shared_ptr<Ui::DynamicImage> thumbnail;\n\t};\n\n\tvoid updateMedia(const std::shared_ptr<PollMediaState> &media);\n\t[[nodiscard]] UploadedMedia parseUploaded(\n\t\tconst MTPMessageMedia &result,\n\t\tFullMsgId fullId);\n\tvoid applyUploaded(\n\t\tconst std::shared_ptr<PollMediaState> &media,\n\t\tuint64 token,\n\t\tFullMsgId fullId,\n\t\tconst MTPInputFile &file);\n\tvoid applyUploadedDocument(\n\t\tconst std::shared_ptr<PollMediaState> &media,\n\t\tuint64 token,\n\t\tFullMsgId fullId,\n\t\tconst Api::RemoteFileInfo &info,\n\t\tconst UploadContext &context);\n\tvoid subscribeToUploader();\n\n\tconst not_null<Main::Session*> _session;\n\tconst not_null<PeerData*> _peer;\n\tconst Fn<void(const QString&)> _showError;\n\tstd::unique_ptr<TaskQueue> _prepareQueue;\n\tbase::flat_map<FullMsgId, UploadContext> _uploads;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace PollMediaUpload\n"
  },
  {
    "path": "Telegram/SourceFiles/profile/profile.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\n\nusing \"ui/widgets/widgets.style\";\nusing \"info/info.style\";\n\nprofileBg: windowBg;\n\nprofileTopBarHeight: topBarHeight;\n\nprofileDropAreaBg: profileBg;\nprofileDropAreaFg: lightButtonFg;\nprofileDropAreaPadding: margins(25px, 3px, 25px, 20px);\nprofileDropAreaTitleFont: font(24px);\nprofileDropAreaTitleTop: 30px;\nprofileDropAreaSubtitleFont: font(16px);\nprofileDropAreaSubtitleTop: 68px;\nprofileDropAreaBorderFg: profileDropAreaFg;\nprofileDropAreaBorderWidth: 3px;\nprofileDropAreaDuration: 200;\n\nprofileBlockMarginTop: 14px;\nprofileBlockTitleHeight: 24px;\nprofileBlockTitleFont: font(14px semibold);\nprofileBlockTitleFg: windowBoldFg;\nprofileBlockTitlePosition: point(24px, 0px);\nprofileBlockTextPart: FlatLabel(defaultFlatLabel) {\n\tminWidth: 180px;\n\tmargin: margins(5px, 5px, 5px, 5px);\n}\nprofileBlockOneLineTextPart: FlatLabel(profileBlockTextPart) {\n\tminWidth: 0px; // No need to set minWidth in one-line text.\n\tmaxHeight: 20px;\n}\n\nprofileMemberNameFg: windowBoldFg;\n"
  },
  {
    "path": "Telegram/SourceFiles/profile/profile_back_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"profile/profile_back_button.h\"\n\n#include \"ui/text/text.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_profile.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_dialogs.h\"\n\n#include <QGraphicsOpacityEffect>\n\nnamespace Profile {\n\nBackButton::BackButton(QWidget *parent) : Ui::AbstractButton(parent) {\n\tsetCursor(style::cur_pointer);\n}\n\nvoid BackButton::setText(const QString &text) {\n\t_text.setText(st::semiboldTextStyle, text);\n\t_cachedWidth = -1;\n\tupdate();\n}\n\nvoid BackButton::setSubtext(const QString &subtext) {\n\t_subtext.setText(st::defaultTextStyle, subtext);\n\t_cachedWidth = -1;\n\tupdate();\n}\n\nvoid BackButton::setWidget(not_null<Ui::RpWidget*> widget) {\n\t_widget = widget;\n\t_widget->setParent(this);\n\t_widget->show();\n\t_widget->move(\n\t\tst::historyAdminLogTopBarLeft + st::historyAdminLogTopBarUserpicSkip,\n\t\t(st::profileTopBarHeight - _widget->height()) / 2);\n\t_cachedWidth = -1;\n\tupdate();\n}\n\nvoid BackButton::setOpacity(float64 opacity) {\n\t_opacity = opacity;\n\tif (_widget) {\n\t\tif (opacity < 1.) {\n\t\t\tif (!_opacityEffect) {\n\t\t\t\t_opacityEffect = Ui::CreateChild<QGraphicsOpacityEffect>(\n\t\t\t\t\t_widget);\n\t\t\t\t_widget->setGraphicsEffect(_opacityEffect);\n\t\t\t}\n\t\t\t_opacityEffect->setOpacity(opacity);\n\t\t} else {\n\t\t\t_widget->setGraphicsEffect(nullptr);\n\t\t\t_opacityEffect = nullptr;\n\t\t}\n\t}\n\tupdate();\n}\n\nint BackButton::resizeGetHeight(int newWidth) {\n\t_cachedWidth = -1;\n\treturn st::profileTopBarHeight;\n}\n\nvoid BackButton::updateCache() {\n\tif (_cachedWidth == width()) {\n\t\treturn;\n\t}\n\t_cachedWidth = width();\n\tconst auto widgetWidth = _widget\n\t\t? _widget->width() + st::historyAdminLogTopBarUserpicSkip\n\t\t: 0;\n\tconst auto availableWidth = width()\n\t\t- st::historyAdminLogTopBarLeft\n\t\t- widgetWidth;\n\t_elisionWidth = availableWidth;\n}\n\nvoid BackButton::paintEvent(QPaintEvent *e) {\n\tupdateCache();\n\n\tauto p = QPainter(this);\n\n\tp.fillRect(e->rect(), st::profileBg);\n\tst::topBarBack.paint(\n\t\tp,\n\t\tst::historyAdminLogTopBarLeft,\n\t\t(st::topBarHeight - st::topBarBack.height()) / 2,\n\t\twidth());\n\tp.setOpacity(_opacity);\n\n\tconst auto textHeight = st::semiboldFont->height;\n\tconst auto subtextHeight = st::dialogsTextFont->height;\n\tconst auto totalHeight = _subtext.isEmpty()\n\t\t? textHeight\n\t\t: textHeight + subtextHeight;\n\tconst auto startY = (height() - totalHeight) / 2 - st::lineWidth;\n\tconst auto widgetWidth = _widget\n\t\t? _widget->width() + st::historyAdminLogTopBarUserpicSkip\n\t\t: 0;\n\tconst auto textX = st::historyAdminLogTopBarLeft + widgetWidth;\n\n\tconst auto context = Ui::Text::PaintContext{\n\t\t.position = QPoint{ textX, startY },\n\t\t.outerWidth = width(),\n\t\t.availableWidth = _elisionWidth,\n\t\t.elisionLines = 1,\n\t};\n\tp.setPen(st::dialogsNameFg);\n\t_text.draw(p, context);\n\n\tif (!_subtext.isEmpty()) {\n\t\tconst auto subtextContext = Ui::Text::PaintContext{\n\t\t\t.position = { textX, startY + textHeight + st::lineWidth * 2 },\n\t\t\t.outerWidth = width(),\n\t\t\t.availableWidth = _elisionWidth,\n\t\t\t.elisionLines = 1,\n\t\t};\n\t\tp.setPen(st::historyStatusFg);\n\t\t_subtext.draw(p, subtextContext);\n\t}\n}\n\nvoid BackButton::onStateChanged(State was, StateChangeSource source) {\n\tif (isDown() && !(was & StateFlag::Down)) {\n\t\tclicked(Qt::KeyboardModifiers(), Qt::LeftButton);\n\t}\n}\n\n} // namespace Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/profile/profile_back_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/abstract_button.h\"\n#include \"ui/text/text.h\"\n\nclass QGraphicsOpacityEffect;\n\nnamespace Profile {\n\nclass BackButton final : public Ui::AbstractButton {\npublic:\n\tBackButton(QWidget *parent);\n\n\tvoid setText(const QString &text);\n\tvoid setSubtext(const QString &subtext);\n\tvoid setWidget(not_null<Ui::RpWidget*> widget);\n\tvoid setOpacity(float64 opacity);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tint resizeGetHeight(int newWidth) override;\n\tvoid onStateChanged(State was, StateChangeSource source) override;\n\nprivate:\n\tvoid updateCache();\n\n\trpl::lifetime _unreadBadgeLifetime;\n\tUi::Text::String _text;\n\tUi::Text::String _subtext;\n\tUi::RpWidget *_widget = nullptr;\n\n\tint _cachedWidth = -1;\n\tint _elisionWidth = 0;\n\n\tfloat64 _opacity = 1.0;\n\tQGraphicsOpacityEffect *_opacityEffect = nullptr;\n\n};\n\n} // namespace Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/profile/profile_block_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"profile/profile_block_widget.h\"\n\n#include \"ui/painter.h\"\n#include \"styles/style_profile.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Profile {\n\nBlockWidget::BlockWidget(\n\tQWidget *parent,\n\tPeerData *peer,\n\tconst QString &title) : RpWidget(parent)\n, _peer(peer)\n, _title(title) {\n}\n\nint BlockWidget::contentTop() const {\n\treturn emptyTitle() ? 0 : (st::profileBlockMarginTop + st::profileBlockTitleHeight);\n}\n\nvoid BlockWidget::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tpaintTitle(p);\n\tpaintContents(p);\n}\n\nvoid BlockWidget::paintTitle(Painter &p) {\n\tif (emptyTitle()) return;\n\n\tp.setFont(st::profileBlockTitleFont);\n\tp.setPen(st::profileBlockTitleFg);\n\tint titleLeft = st::profileBlockTitlePosition.x();\n\tint titleTop = st::profileBlockMarginTop + st::profileBlockTitlePosition.y();\n\tp.drawTextLeft(titleLeft, titleTop, width(), _title);\n}\n\n} // namespace Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/profile/profile_block_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace Profile {\n\nclass SectionMemento;\n\nclass BlockWidget : public Ui::RpWidget {\npublic:\n\tBlockWidget(QWidget *parent, PeerData *peer, const QString &title);\n\n\tvirtual void showFinished() {\n\t}\n\n\tvirtual void saveState(not_null<SectionMemento*> memento) {\n\t}\n\tvirtual void restoreState(not_null<SectionMemento*> memento) {\n\t}\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvirtual void paintContents(Painter &p) {\n\t}\n\n\t// Where does the block content start (after the title).\n\tint contentTop() const;\n\n\t// Resizes content and counts natural widget height for the desired width.\n\tint resizeGetHeight(int newWidth) override = 0;\n\n\tvoid contentSizeUpdated() {\n\t\tresizeToWidth(width());\n\t}\n\n\tPeerData *peer() const {\n\t\treturn _peer;\n\t}\n\n\tbool emptyTitle() const {\n\t\treturn _title.isEmpty();\n\t}\n\nprivate:\n\tvoid paintTitle(Painter &p);\n\n\tPeerData *_peer;\n\tQString _title;\n\n};\n\n} // namespace Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/profile/profile_cover_drop_area.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"profile/profile_cover_drop_area.h\"\n\n#include \"ui/ui_utility.h\"\n#include \"styles/style_profile.h\"\n\nnamespace Profile {\n\nCoverDropArea::CoverDropArea(\n\tQWidget *parent,\n\tconst QString &title,\n\tconst QString &subtitle)\n: RpWidget(parent)\n, _title(title)\n, _subtitle(subtitle)\n, _titleWidth(st::profileDropAreaTitleFont->width(_title))\n, _subtitleWidth(st::profileDropAreaSubtitleFont->width(_subtitle)) {\n}\n\nvoid CoverDropArea::showAnimated() {\n\tshow();\n\t_hiding = false;\n\tsetupAnimation();\n}\n\nvoid CoverDropArea::hideAnimated(HideFinishCallback &&callback) {\n\t_hideFinishCallback = std::move(callback);\n\t_hiding = true;\n\tsetupAnimation();\n}\n\nvoid CoverDropArea::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tif (_a_appearance.animating()) {\n\t\tp.setOpacity(_a_appearance.value(_hiding ? 0. : 1.));\n\t\tp.drawPixmap(0, 0, _cache);\n\t\treturn;\n\t}\n\n\tif (!_cache.isNull()) {\n\t\t_cache = QPixmap();\n\t\tif (_hiding) {\n\t\t\thide();\n\t\t\tif (_hideFinishCallback) {\n\t\t\t\t_hideFinishCallback(this);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t}\n\n\tp.fillRect(e->rect(), st::profileDropAreaBg);\n\n\tif (width() < st::profileDropAreaPadding.left() + st::profileDropAreaPadding.right()) return;\n\tif (height() < st::profileDropAreaPadding.top() + st::profileDropAreaPadding.bottom()) return;\n\n\tauto border = st::profileDropAreaBorderWidth;\n\tauto &borderFg = st::profileDropAreaBorderFg;\n\tauto inner = rect().marginsRemoved(st::profileDropAreaPadding);\n\tp.fillRect(inner.x(), inner.y(), inner.width(), border, borderFg);\n\tp.fillRect(inner.x(), inner.y() + inner.height() - border, inner.width(), border, borderFg);\n\tp.fillRect(inner.x(), inner.y() + border, border, inner.height() - 2 * border, borderFg);\n\tp.fillRect(inner.x() + inner.width() - border, inner.y() + border, border, inner.height() - 2 * border, borderFg);\n\n\tint titleLeft = inner.x() + (inner.width() - _titleWidth) / 2;\n\tint titleTop = inner.y() + st::profileDropAreaTitleTop + st::profileDropAreaTitleFont->ascent;\n\tp.setFont(st::profileDropAreaTitleFont);\n\tp.setPen(st::profileDropAreaFg);\n\tp.drawText(titleLeft, titleTop, _title);\n\n\tint subtitleLeft = inner.x() + (inner.width() - _subtitleWidth) / 2;\n\tint subtitleTop = inner.y() + st::profileDropAreaSubtitleTop + st::profileDropAreaSubtitleFont->ascent;\n\tp.setFont(st::profileDropAreaSubtitleFont);\n\tp.setPen(st::profileDropAreaFg);\n\tp.drawText(subtitleLeft, subtitleTop, _subtitle);\n}\n\nvoid CoverDropArea::setupAnimation() {\n\tif (_cache.isNull()) {\n\t\t_cache = Ui::GrabWidget(this);\n\t}\n\tauto from = _hiding ? 1. : 0., to = _hiding ? 0. : 1.;\n\t_a_appearance.start(\n\t\t[=] { update(); },\n\t\tfrom,\n\t\tto,\n\t\tst::profileDropAreaDuration);\n}\n\n} // namespace Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/profile/profile_cover_drop_area.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Profile {\n\nclass CoverDropArea : public Ui::RpWidget {\npublic:\n\tCoverDropArea(QWidget *parent, const QString &title, const QString &subtitle);\n\n\tvoid showAnimated();\n\n\tusing HideFinishCallback = Fn<void(CoverDropArea*)>;\n\tvoid hideAnimated(HideFinishCallback &&callback);\n\n\tbool hiding() const {\n\t\treturn _hiding;\n\t}\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid setupAnimation();\n\n\tQString _title, _subtitle;\n\tint _titleWidth, _subtitleWidth;\n\n\tQPixmap _cache;\n\tUi::Animations::Simple _a_appearance;\n\tbool _hiding = false;\n\tHideFinishCallback _hideFinishCallback;\n\n};\n\n} // namespace Profile\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_away_message.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/business/settings_away_message.h\"\n\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"data/business/data_business_info.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/data_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/business/settings_recipients_helper.h\"\n#include \"settings/business/settings_shortcut_messages.h\"\n#include \"ui/boxes/choose_date_time.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nclass AwayMessage final : public Section<AwayMessage> {\npublic:\n\tAwayMessage(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~AwayMessage();\n\n\t[[nodiscard]] bool closeByOutsideClick() const override;\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\nprivate:\n\tvoid setupContent(not_null<Window::SessionController*> controller);\n\tvoid save();\n\n\trpl::variable<bool> _canHave;\n\trpl::event_stream<> _deactivateOnAttempt;\n\trpl::variable<Data::BusinessRecipients> _recipients;\n\trpl::variable<Data::AwaySchedule> _schedule;\n\trpl::variable<bool> _offlineOnly;\n\trpl::variable<bool> _enabled;\n\n};\n\n[[nodiscard]] TimeId StartTimeMin() {\n\t// Telegram was launched in August 2013 :)\n\treturn base::unixtime::serialize(QDateTime(QDate(2013, 8, 1), QTime(0, 0)));\n}\n\n[[nodiscard]] TimeId EndTimeMin() {\n\treturn StartTimeMin() + 3600;\n}\n\n[[nodiscard]] bool BadCustomInterval(const Data::WorkingInterval &interval) {\n\treturn !interval\n\t\t|| (interval.start < StartTimeMin())\n\t\t|| (interval.end < EndTimeMin());\n}\n\nstruct AwayScheduleSelectorDescriptor {\n\tnot_null<Window::SessionController*> controller;\n\tnot_null<rpl::variable<Data::AwaySchedule>*> data;\n};\nvoid AddAwayScheduleSelector(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tAwayScheduleSelectorDescriptor &&descriptor) {\n\tusing Type = Data::AwayScheduleType;\n\tusing namespace rpl::mappers;\n\n\tconst auto controller = descriptor.controller;\n\tconst auto data = descriptor.data;\n\n\tUi::AddSubsectionTitle(container, tr::lng_away_schedule());\n\tconst auto group = std::make_shared<Ui::RadioenumGroup<Type>>(\n\t\tdata->current().type);\n\n\tconst auto add = [&](Type type, const QString &label) {\n\t\tcontainer->add(\n\t\t\tobject_ptr<Ui::Radioenum<Type>>(\n\t\t\t\tcontainer,\n\t\t\t\tgroup,\n\t\t\t\ttype,\n\t\t\t\tlabel),\n\t\t\tst::boxRowPadding + st::settingsAwaySchedulePadding);\n\t};\n\tadd(Type::Always, tr::lng_away_schedule_always(tr::now));\n\tadd(Type::OutsideWorkingHours, tr::lng_away_schedule_outside(tr::now));\n\tadd(Type::Custom, tr::lng_away_schedule_custom(tr::now));\n\n\tconst auto customWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto customInner = customWrap->entity();\n\tcustomWrap->toggleOn(group->value() | rpl::map(_1 == Type::Custom));\n\n\tgroup->changes() | rpl::on_next([=](Type value) {\n\t\tauto copy = data->current();\n\t\tcopy.type = value;\n\t\t*data = copy;\n\t}, customWrap->lifetime());\n\n\tconst auto chooseDate = [=](\n\t\t\trpl::producer<QString> title,\n\t\t\tTimeId now,\n\t\t\tFn<TimeId()> min,\n\t\t\tFn<TimeId()> max,\n\t\t\tFn<void(TimeId)> done) {\n\t\tusing namespace Ui;\n\t\tconst auto box = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\t\tconst auto save = [=](TimeId time) {\n\t\t\tdone(time);\n\t\t\tif (const auto strong = box->get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t};\n\t\t*box = controller->show(Box(ChooseDateTimeBox, ChooseDateTimeBoxArgs{\n\t\t\t.title = std::move(title),\n\t\t\t.submit = tr::lng_settings_save(),\n\t\t\t.done = save,\n\t\t\t.min = min,\n\t\t\t.time = now,\n\t\t\t.max = max,\n\t\t}));\n\t};\n\n\tUi::AddSkip(customInner);\n\tUi::AddDivider(customInner);\n\tUi::AddSkip(customInner);\n\n\tauto startLabel = data->value(\n\t) | rpl::map([=](const Data::AwaySchedule &value) {\n\t\treturn langDateTime(\n\t\t\tbase::unixtime::parse(value.customInterval.start));\n\t});\n\tAddButtonWithLabel(\n\t\tcustomInner,\n\t\ttr::lng_away_custom_start(),\n\t\tstd::move(startLabel),\n\t\tst::settingsButtonNoIcon\n\t)->setClickedCallback([=] {\n\t\tchooseDate(\n\t\t\ttr::lng_away_custom_start(),\n\t\t\tdata->current().customInterval.start,\n\t\t\tStartTimeMin,\n\t\t\t[=] { return data->current().customInterval.end - 1; },\n\t\t\t[=](TimeId time) {\n\t\t\t\tauto copy = data->current();\n\t\t\t\tcopy.customInterval.start = time;\n\t\t\t\t*data = copy;\n\t\t\t});\n\t});\n\n\tauto endLabel = data->value(\n\t) | rpl::map([=](const Data::AwaySchedule &value) {\n\t\treturn langDateTime(\n\t\t\tbase::unixtime::parse(value.customInterval.end));\n\t});\n\tAddButtonWithLabel(\n\t\tcustomInner,\n\t\ttr::lng_away_custom_end(),\n\t\tstd::move(endLabel),\n\t\tst::settingsButtonNoIcon\n\t)->setClickedCallback([=] {\n\t\tchooseDate(\n\t\t\ttr::lng_away_custom_end(),\n\t\t\tdata->current().customInterval.end,\n\t\t\t[=] { return data->current().customInterval.start + 1; },\n\t\t\tnullptr,\n\t\t\t[=](TimeId time) {\n\t\t\t\tauto copy = data->current();\n\t\t\t\tcopy.customInterval.end = time;\n\t\t\t\t*data = copy;\n\t\t\t});\n\t});\n}\n\nAwayMessage::AwayMessage(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent(controller);\n}\n\nAwayMessage::~AwayMessage() {\n\tif (!Core::Quitting()) {\n\t\tsave();\n\t}\n}\n\nbool AwayMessage::closeByOutsideClick() const {\n\treturn false;\n}\n\nrpl::producer<QString> AwayMessage::title() {\n\treturn tr::lng_away_title();\n}\n\nvoid AwayMessage::setupContent(\n\t\tnot_null<Window::SessionController*> controller) {\n\tusing namespace Data;\n\tusing namespace rpl::mappers;\n\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tconst auto info = &controller->session().data().businessInfo();\n\tconst auto current = info->awaySettings();\n\tconst auto disabled = (current.schedule.type == AwayScheduleType::Never);\n\n\t_recipients = disabled\n\t\t? Data::BusinessRecipients{ .allButExcluded = true }\n\t\t: Data::BusinessRecipients::MakeValid(current.recipients);\n\tauto initialSchedule = disabled ? AwaySchedule{\n\t\t.type = AwayScheduleType::Always,\n\t} : current.schedule;\n\tif (BadCustomInterval(initialSchedule.customInterval)) {\n\t\tconst auto now = base::unixtime::now();\n\t\tinitialSchedule.customInterval = WorkingInterval{\n\t\t\t.start = now,\n\t\t\t.end = now + 24 * 60 * 60,\n\t\t};\n\t}\n\t_schedule = initialSchedule;\n\n\tAddDividerTextWithLottie(content, {\n\t\t.lottie = u\"sleep\"_q,\n\t\t.lottieSize = st::settingsCloudPasswordIconSize,\n\t\t.lottieMargins = st::peerAppearanceIconPadding,\n\t\t.showFinished = showFinishes(),\n\t\t.about = tr::lng_away_about(tr::marked),\n\t\t.aboutMargins = st::peerAppearanceCoverLabelMargin,\n\t});\n\n\tconst auto session = &controller->session();\n\t_canHave = rpl::combine(\n\t\tShortcutsCountValue(session),\n\t\tShortcutsLimitValue(session),\n\t\tShortcutExistsValue(session, u\"away\"_q),\n\t\t(_1 < _2) || _3);\n\n\tUi::AddSkip(content);\n\tconst auto enabled = content->add(object_ptr<Ui::SettingsButton>(\n\t\tcontent,\n\t\ttr::lng_away_enable(),\n\t\tst::settingsButtonNoIcon\n\t))->toggleOn(rpl::single(\n\t\t!disabled\n\t) | rpl::then(rpl::merge(\n\t\t_canHave.value() | rpl::filter(!_1),\n\t\t_deactivateOnAttempt.events() | rpl::map_to(false)\n\t)));\n\n\t_enabled = enabled->toggledValue();\n\t_enabled.value() | rpl::filter(_1) | rpl::on_next([=] {\n\t\tif (!_canHave.current()) {\n\t\t\tcontroller->showToast({\n\t\t\t\t.text = { tr::lng_away_limit_reached(tr::now) },\n\t\t\t\t.adaptive = true,\n\t\t\t});\n\t\t\t_deactivateOnAttempt.fire({});\n\t\t}\n\t}, lifetime());\n\n\tconst auto wrap = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::VerticalLayout>(content)));\n\tconst auto inner = wrap->entity();\n\n\tUi::AddSkip(inner);\n\tUi::AddDivider(inner);\n\n\tconst auto createWrap = inner->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tinner,\n\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\tconst auto createInner = createWrap->entity();\n\tUi::AddSkip(createInner);\n\tconst auto create = AddButtonWithLabel(\n\t\tcreateInner,\n\t\trpl::conditional(\n\t\t\tShortcutExistsValue(session, u\"away\"_q),\n\t\t\ttr::lng_business_edit_messages(),\n\t\t\ttr::lng_away_create()),\n\t\tShortcutMessagesCountValue(\n\t\t\tsession,\n\t\t\tu\"away\"_q\n\t\t) | rpl::map([=](int count) {\n\t\t\treturn count\n\t\t\t\t? tr::lng_forum_messages(tr::now, lt_count, count)\n\t\t\t\t: QString();\n\t\t}),\n\t\tst::settingsButtonLightNoIcon);\n\tcreate->setClickedCallback([=] {\n\t\tconst auto owner = &controller->session().data();\n\t\tconst auto id = owner->shortcutMessages().emplaceShortcut(\"away\");\n\t\tshowOther(ShortcutMessagesId(id));\n\t});\n\tUi::AddSkip(createInner);\n\tUi::AddDivider(createInner);\n\n\tcreateWrap->toggleOn(rpl::single(true));\n\n\tUi::AddSkip(inner);\n\tAddAwayScheduleSelector(inner, {\n\t\t.controller = controller,\n\t\t.data = &_schedule,\n\t});\n\tUi::AddSkip(inner);\n\tUi::AddDivider(inner);\n\tUi::AddSkip(inner);\n\n\tconst auto offlineOnly = inner->add(\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\tinner,\n\t\t\ttr::lng_away_offline_only(),\n\t\t\tst::settingsButtonNoIcon)\n\t)->toggleOn(rpl::single(current.offlineOnly));\n\t_offlineOnly = offlineOnly->toggledValue();\n\n\tUi::AddSkip(inner);\n\tUi::AddDividerText(inner, tr::lng_away_offline_only_about());\n\n\tAddBusinessRecipientsSelector(inner, {\n\t\t.controller = controller,\n\t\t.title = tr::lng_away_recipients(),\n\t\t.data = &_recipients,\n\t\t.type = Data::BusinessRecipientsType::Messages,\n\t});\n\n\tUi::AddSkip(inner, st::settingsChatbotsAccessSkip);\n\n\twrap->toggleOn(enabled->toggledValue());\n\twrap->finishAnimating();\n\n\tUi::ResizeFitChild(this, content);\n}\n\nvoid AwayMessage::save() {\n\tconst auto show = controller()->uiShow();\n\tconst auto session = &controller()->session();\n\tconst auto fail = [=](QString error) {\n\t\tif (error == u\"BUSINESS_RECIPIENTS_EMPTY\"_q) {\n\t\t\tshow->showToast(tr::lng_greeting_recipients_empty(tr::now));\n\t\t} else if (error != u\"SHORTCUT_INVALID\"_q) {\n\t\t\tshow->showToast(error);\n\t\t}\n\t};\n\tsession->data().businessInfo().saveAwaySettings(\n\t\t_enabled.current() ? Data::AwaySettings{\n\t\t\t.recipients = _recipients.current(),\n\t\t\t.schedule = _schedule.current(),\n\t\t\t.shortcutId = LookupShortcutId(session, u\"away\"_q),\n\t\t\t.offlineOnly = _offlineOnly.current(),\n\t\t} : Data::AwaySettings(),\n\t\tfail);\n}\n\n} // namespace\n\nType AwayMessageId() {\n\treturn AwayMessage::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_away_message.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\n[[nodiscard]] Type AwayMessageId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_chat_intro.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/business/settings_chat_intro.h\"\n\n#include \"api/api_premium.h\"\n#include \"boxes/peers/edit_peer_color_box.h\" // ButtonStyleWithRightEmoji\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/application.h\"\n#include \"data/business/data_business_info.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/view/media/history_view_media_common.h\"\n#include \"history/view/media/history_view_sticker_player.h\"\n#include \"history/view/history_view_about_view.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"settings/business/settings_recipients_helper.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nusing namespace HistoryView;\n\nclass PreviewDelegate final : public DefaultElementDelegate {\npublic:\n\tPreviewDelegate(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Ui::ChatStyle*> st,\n\t\tFn<void()> update);\n\n\tbool elementAnimationsPaused() override;\n\tnot_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;\n\tContext elementContext() override;\n\nprivate:\n\tconst not_null<QWidget*> _parent;\n\tconst std::unique_ptr<Ui::PathShiftGradient> _pathGradient;\n\n};\n\nclass PreviewWrap final : public Ui::RpWidget {\npublic:\n\tPreviewWrap(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<Data::ChatIntro> value);\n\t~PreviewWrap();\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid resizeTo(int width);\n\tvoid prepare(rpl::producer<Data::ChatIntro> value);\n\n\tconst not_null<History*> _history;\n\tconst std::unique_ptr<Ui::ChatTheme> _theme;\n\tconst std::unique_ptr<Ui::ChatStyle> _style;\n\tconst std::unique_ptr<PreviewDelegate> _delegate;\n\n\tstd::unique_ptr<AboutView> _view;\n\tQPoint _position;\n\n};\n\nclass StickerPanel final {\npublic:\n\tStickerPanel();\n\t~StickerPanel();\n\n\tstruct Descriptor {\n\t\tnot_null<Window::SessionController*> controller;\n\t\tnot_null<QWidget*> button;\n\t};\n\tvoid show(Descriptor &&descriptor);\n\n\tstruct CustomChosen {\n\t\tnot_null<DocumentData*> sticker;\n\t};\n\t[[nodiscard]] rpl::producer<CustomChosen> someCustomChosen() const {\n\t\treturn _someCustomChosen.events();\n\t}\n\nprivate:\n\tvoid create(const Descriptor &descriptor);\n\n\tbase::unique_qptr<ChatHelpers::TabbedPanel> _panel;\n\tQPointer<QWidget> _panelButton;\n\trpl::event_stream<CustomChosen> _someCustomChosen;\n\n};\n\nclass ChatIntro final : public Section<ChatIntro> {\npublic:\n\tChatIntro(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~ChatIntro();\n\n\t[[nodiscard]] bool closeByOutsideClick() const override;\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\tvoid setInnerFocus() override {\n\t\t_setFocus();\n\t}\n\nprivate:\n\tvoid setupContent(not_null<Window::SessionController*> controller);\n\tvoid save();\n\n\tFn<void()> _setFocus;\n\n\trpl::variable<Data::ChatIntro> _intro;\n\n};\n\n[[nodiscard]] int PartLimit(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &key,\n\t\tint defaultValue) {\n\treturn session->appConfig().get<int>(key, defaultValue);\n}\n\n[[nodiscard]] not_null<Ui::InputField*> AddPartInput(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> placeholder,\n\t\tQString current,\n\t\tint limit) {\n\tconst auto field = container->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tcontainer,\n\t\t\tst::settingsChatIntroField,\n\t\t\tstd::move(placeholder),\n\t\t\tcurrent),\n\t\tst::settingsChatIntroFieldMargins);\n\tfield->setMaxLength(limit);\n\tUi::AddLengthLimitLabel(field, limit);\n\treturn field;\n}\n\nrpl::producer<std::shared_ptr<StickerPlayer>> IconPlayerValue(\n\t\tnot_null<DocumentData*> sticker,\n\t\tFn<void()> update) {\n\tconst auto media = sticker->createMediaView();\n\tmedia->checkStickerLarge();\n\tmedia->goodThumbnailWanted();\n\n\treturn rpl::single() | rpl::then(\n\t\tsticker->session().downloaderTaskFinished()\n\t) | rpl::filter([=] {\n\t\treturn media->loaded();\n\t}) | rpl::take(1) | rpl::map([=] {\n\t\tauto result = std::shared_ptr<StickerPlayer>();\n\t\tconst auto info = sticker->sticker();\n\t\tconst auto box = QSize(st::emojiSize, st::emojiSize);\n\t\tif (info->isLottie()) {\n\t\t\tresult = std::make_shared<LottiePlayer>(\n\t\t\t\tChatHelpers::LottiePlayerFromDocument(\n\t\t\t\t\tmedia.get(),\n\t\t\t\t\tChatHelpers::StickerLottieSize::StickerEmojiSize,\n\t\t\t\t\tbox,\n\t\t\t\t\tLottie::Quality::High));\n\t\t} else if (info->isWebm()) {\n\t\t\tresult = std::make_shared<WebmPlayer>(\n\t\t\t\tmedia->owner()->location(),\n\t\t\t\tmedia->bytes(),\n\t\t\t\tbox);\n\t\t} else {\n\t\t\tresult = std::make_shared<StaticStickerPlayer>(\n\t\t\t\tmedia->owner()->location(),\n\t\t\t\tmedia->bytes(),\n\t\t\t\tbox);\n\t\t}\n\t\tresult->setRepaintCallback(update);\n\t\treturn result;\n\t});\n}\n\n[[nodiscard]] object_ptr<Ui::SettingsButton> CreateIntroStickerButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\trpl::producer<DocumentData*> stickerValue,\n\t\tFn<void(DocumentData*)> stickerChosen) {\n\tconst auto button = ButtonStyleWithRightEmoji(\n\t\tparent,\n\t\ttr::lng_chat_intro_random_sticker(tr::now),\n\t\tst::settingsButtonNoIcon);\n\tauto result = Settings::CreateButtonWithIcon(\n\t\tparent,\n\t\ttr::lng_chat_intro_choose_sticker(),\n\t\t*button.st);\n\tconst auto raw = result.data();\n\n\tconst auto right = Ui::CreateChild<Ui::RpWidget>(raw);\n\tright->show();\n\n\tstruct State {\n\t\tStickerPanel panel;\n\t\tDocumentData *sticker = nullptr;\n\t\tstd::shared_ptr<StickerPlayer> player;\n\t\trpl::lifetime playerLifetime;\n\t};\n\tconst auto state = right->lifetime().make_state<State>();\n\tstate->panel.someCustomChosen(\n\t) | rpl::on_next([=](StickerPanel::CustomChosen chosen) {\n\t\tstickerChosen(chosen.sticker);\n\t}, raw->lifetime());\n\n\tstd::move(\n\t\tstickerValue\n\t) | rpl::on_next([=](DocumentData *sticker) {\n\t\tstate->sticker = sticker;\n\t\tif (sticker) {\n\t\t\tright->resize(button.emojiWidth + button.added, right->height());\n\t\t\tIconPlayerValue(\n\t\t\t\tsticker,\n\t\t\t\t[=] { right->update(); }\n\t\t\t) | rpl::on_next([=](\n\t\t\t\t\tstd::shared_ptr<StickerPlayer> player) {\n\t\t\t\tstate->player = std::move(player);\n\t\t\t\tright->update();\n\t\t\t}, state->playerLifetime);\n\t\t} else {\n\t\t\tstate->playerLifetime.destroy();\n\t\t\tstate->player = nullptr;\n\t\t\tright->resize(button.noneWidth + button.added, right->height());\n\t\t\tright->update();\n\t\t}\n\t}, right->lifetime());\n\n\trpl::combine(\n\t\traw->sizeValue(),\n\t\tright->widthValue()\n\t) | rpl::on_next([=](QSize outer, int width) {\n\t\tright->resize(width, outer.height());\n\t\tconst auto skip = st::settingsButton.padding.right();\n\t\tright->moveToRight(skip - button.added, 0, outer.width());\n\t}, right->lifetime());\n\n\tright->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(right);\n\t\tconst auto height = right->height();\n\t\tif (state->player) {\n\t\t\tif (state->player->ready()) {\n\t\t\t\tconst auto frame = state->player->frame(\n\t\t\t\t\tQSize(st::emojiSize, st::emojiSize),\n\t\t\t\t\tQColor(0, 0, 0, 0),\n\t\t\t\t\tfalse,\n\t\t\t\t\tcrl::now(),\n\t\t\t\t\t!right->window()->isActiveWindow()).image;\n\t\t\t\tconst auto target = DownscaledSize(\n\t\t\t\t\tframe.size(),\n\t\t\t\t\tQSize(st::emojiSize, st::emojiSize));\n\t\t\t\tp.drawImage(\n\t\t\t\t\tQRect(\n\t\t\t\t\t\tbutton.added + (st::emojiSize - target.width()) / 2,\n\t\t\t\t\t\t(height - target.height()) / 2,\n\t\t\t\t\t\ttarget.width(),\n\t\t\t\t\t\ttarget.height()),\n\t\t\t\t\tframe);\n\t\t\t\tstate->player->markFrameShown();\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto &font = st::normalFont;\n\t\t\tp.setFont(font);\n\t\t\tp.setPen(st::windowActiveTextFg);\n\t\t\tp.drawText(\n\t\t\t\tQPoint(\n\t\t\t\t\tbutton.added,\n\t\t\t\t\t(height - font->height) / 2 + font->ascent),\n\t\t\t\ttr::lng_chat_intro_random_sticker(tr::now));\n\t\t}\n\t}, right->lifetime());\n\n\traw->setClickedCallback([=] {\n\t\tif (const auto controller = show->resolveWindow()) {\n\t\t\tstate->panel.show({\n\t\t\t\t.controller = controller,\n\t\t\t\t.button = right,\n\t\t\t});\n\t\t}\n\t});\n\n\treturn result;\n}\n\nPreviewDelegate::PreviewDelegate(\n\tnot_null<QWidget*> parent,\n\tnot_null<Ui::ChatStyle*> st,\n\tFn<void()> update)\n: _parent(parent)\n, _pathGradient(MakePathShiftGradient(st, update)) {\n}\n\nbool PreviewDelegate::elementAnimationsPaused() {\n\treturn _parent->window()->isActiveWindow();\n}\n\nauto PreviewDelegate::elementPathShiftGradient()\n-> not_null<Ui::PathShiftGradient*> {\n\treturn _pathGradient.get();\n}\n\nContext PreviewDelegate::elementContext() {\n\treturn Context::History;\n}\n\nPreviewWrap::PreviewWrap(\n\tnot_null<QWidget*> parent,\n\tnot_null<Main::Session*> session,\n\trpl::producer<Data::ChatIntro> value)\n: RpWidget(parent)\n, _history(session->data().history(session->userPeerId()))\n, _theme(Window::Theme::DefaultChatThemeOn(lifetime()))\n, _style(std::make_unique<Ui::ChatStyle>(\n\t_history->session().colorIndicesValue()))\n, _delegate(std::make_unique<PreviewDelegate>(\n\tparent,\n\t_style.get(),\n\t[=] { update(); }))\n, _position(0, st::msgMargin.bottom()) {\n\t_style->apply(_theme.get());\n\n\tsession->data().viewRepaintRequest(\n\t) | rpl::on_next([=](Data::RequestViewRepaint data) {\n\t\tif (data.view == _view->view()) {\n\t\t\tupdate();\n\t\t}\n\t}, lifetime());\n\n\tsession->downloaderTaskFinished() | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\tprepare(std::move(value));\n}\n\nPreviewWrap::~PreviewWrap() {\n\t_view = nullptr;\n}\n\nvoid PreviewWrap::prepare(rpl::producer<Data::ChatIntro> value) {\n\t_view = std::make_unique<AboutView>(\n\t\t_history.get(),\n\t\t_delegate.get());\n\n\tstd::move(value) | rpl::on_next([=](Data::ChatIntro intro) {\n\t\t_view->make(std::move(intro), true);\n\t\tif (width() >= st::msgMinWidth) {\n\t\t\tresizeTo(width());\n\t\t}\n\t\tupdate();\n\t}, lifetime());\n\n\twidthValue(\n\t) | rpl::filter([=](int width) {\n\t\treturn width >= st::msgMinWidth;\n\t}) | rpl::on_next([=](int width) {\n\t\tresizeTo(width);\n\t}, lifetime());\n}\n\nvoid PreviewWrap::resizeTo(int width) {\n\tconst auto height = _position.y()\n\t\t+ _view->view()->resizeGetHeight(width)\n\t\t+ _position.y()\n\t\t+ st::msgServiceMargin.top()\n\t\t+ st::msgServiceGiftBoxTopSkip\n\t\t- st::msgServiceMargin.bottom();\n\tresize(width, height);\n}\n\nvoid PreviewWrap::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\n\tconst auto clip = e->rect();\n\tif (!clip.isEmpty()) {\n\t\tp.setClipRect(clip);\n\t\tWindow::SectionWidget::PaintBackground(\n\t\t\tp,\n\t\t\t_theme.get(),\n\t\t\tQSize(width(), window()->height()),\n\t\t\tclip);\n\t}\n\n\tauto context = _theme->preparePaintContext(\n\t\t_style.get(),\n\t\trect(),\n\t\trect(),\n\t\te->rect(),\n\t\t!window()->isActiveWindow());\n\tp.translate(_position);\n\t_view->view()->draw(p, context);\n}\n\nStickerPanel::StickerPanel() = default;\n\nStickerPanel::~StickerPanel() = default;\n\nvoid StickerPanel::show(Descriptor &&descriptor) {\n\tif (!_panel) {\n\t\tcreate(descriptor);\n\n\t\t_panel->shownValue(\n\t\t) | rpl::filter([=] {\n\t\t\treturn (_panelButton != nullptr);\n\t\t}) | rpl::on_next([=](bool shown) {\n\t\t\tif (shown) {\n\t\t\t\t_panelButton->installEventFilter(_panel.get());\n\t\t\t} else {\n\t\t\t\t_panelButton->removeEventFilter(_panel.get());\n\t\t\t}\n\t\t}, _panel->lifetime());\n\t}\n\tconst auto button = descriptor.button;\n\tif (const auto previous = _panelButton.data()) {\n\t\tif (previous != button) {\n\t\t\tprevious->removeEventFilter(_panel.get());\n\t\t}\n\t}\n\t_panelButton = button;\n\tconst auto parent = _panel->parentWidget();\n\tconst auto global = button->mapToGlobal(QPoint());\n\tconst auto local = parent->mapFromGlobal(global);\n\t_panel->moveBottomRight(\n\t\tlocal.y() + (st::normalFont->height / 2),\n\t\tlocal.x() + button->width() * 3);\n\t_panel->toggleAnimated();\n}\n\nvoid StickerPanel::create(const Descriptor &descriptor) {\n\tusing Selector = ChatHelpers::TabbedSelector;\n\tusing Descriptor = ChatHelpers::TabbedSelectorDescriptor;\n\tusing Mode = ChatHelpers::TabbedSelector::Mode;\n\tconst auto controller = descriptor.controller;\n\tconst auto body = controller->window().widget()->bodyWidget();\n\t_panel = base::make_unique_q<ChatHelpers::TabbedPanel>(\n\t\tbody,\n\t\tcontroller,\n\t\tobject_ptr<Selector>(\n\t\t\tnullptr,\n\t\t\tDescriptor{\n\t\t\t\t.show = controller->uiShow(),\n\t\t\t\t.st = st::backgroundEmojiPan,\n\t\t\t\t.level = Window::GifPauseReason::Layer,\n\t\t\t\t.mode = Mode::ChatIntro,\n\t\t\t\t.features = {\n\t\t\t\t\t.megagroupSet = false,\n\t\t\t\t\t.stickersSettings = false,\n\t\t\t\t\t.openStickerSets = false,\n\t\t\t\t},\n\t\t\t}));\n\t_panel->setDropDown(false);\n\t_panel->setDesiredHeightValues(\n\t\t1.,\n\t\tst::emojiPanMinHeight / 2,\n\t\tst::emojiPanMinHeight);\n\t_panel->hide();\n\n\t_panel->selector()->fileChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\t_someCustomChosen.fire({ data.document });\n\t\t_panel->hideAnimated();\n\t}, _panel->lifetime());\n}\n\nChatIntro::ChatIntro(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent(controller);\n}\n\nChatIntro::~ChatIntro() {\n\tif (!Core::Quitting()) {\n\t\tsave();\n\t}\n}\n\nbool ChatIntro::closeByOutsideClick() const {\n\treturn false;\n}\n\nrpl::producer<QString> ChatIntro::title() {\n\treturn tr::lng_chat_intro_title();\n}\n\n[[nodiscard]] rpl::producer<Data::ChatIntro> IntroWithRandomSticker(\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<Data::ChatIntro> intro) {\n\tauto random = rpl::single(\n\t\tApi::RandomHelloStickerValue(session)\n\t) | rpl::then(rpl::duplicate(\n\t\tintro\n\t) | rpl::map([=](const Data::ChatIntro &intro) {\n\t\treturn intro.sticker;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::filter([](DocumentData *sticker) {\n\t\treturn !sticker;\n\t}) | rpl::map([=] {\n\t\treturn Api::RandomHelloStickerValue(session);\n\t})) | rpl::flatten_latest();\n\n\treturn rpl::combine(\n\t\tstd::move(intro),\n\t\tstd::move(random)\n\t) | rpl::map([=](Data::ChatIntro intro, DocumentData *hello) {\n\t\tif (!intro.sticker) {\n\t\t\tintro.sticker = hello;\n\t\t}\n\t\treturn intro;\n\t});\n}\n\nvoid ChatIntro::setupContent(\n\t\tnot_null<Window::SessionController*> controller) {\n\tusing namespace rpl::mappers;\n\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tconst auto session = &controller->session();\n\t_intro = controller->session().user()->businessDetails().intro;\n\n\tconst auto change = [=](Fn<void(Data::ChatIntro &)> modify) {\n\t\tauto intro = _intro.current();\n\t\tmodify(intro);\n\t\t_intro = intro;\n\t};\n\n\tcontent->add(\n\t\tobject_ptr<PreviewWrap>(\n\t\t\tcontent,\n\t\t\tsession,\n\t\t\tIntroWithRandomSticker(session, _intro.value())),\n\t\tstyle::margins());\n\n\tconst auto title = AddPartInput(\n\t\tcontent,\n\t\ttr::lng_chat_intro_enter_title(),\n\t\t_intro.current().title,\n\t\tPartLimit(session, u\"intro_title_length_limit\"_q, 32));\n\tconst auto description = AddPartInput(\n\t\tcontent,\n\t\ttr::lng_chat_intro_enter_message(),\n\t\t_intro.current().description,\n\t\tPartLimit(session, u\"intro_description_length_limit\"_q, 70));\n\tcontent->add(CreateIntroStickerButton(\n\t\tcontent,\n\t\tcontroller->uiShow(),\n\t\t_intro.value() | rpl::map([](const Data::ChatIntro &intro) {\n\t\t\treturn intro.sticker;\n\t\t}) | rpl::distinct_until_changed(),\n\t\t[=](DocumentData *sticker) {\n\t\t\tchange([&](Data::ChatIntro &intro) {\n\t\t\t\tintro.sticker = sticker;\n\t\t\t});\n\t\t}));\n\tUi::AddSkip(content);\n\n\ttitle->changes() | rpl::on_next([=] {\n\t\tchange([&](Data::ChatIntro &intro) {\n\t\t\tintro.title = title->getLastText();\n\t\t});\n\t}, title->lifetime());\n\n\tdescription->changes() | rpl::on_next([=] {\n\t\tchange([&](Data::ChatIntro &intro) {\n\t\t\tintro.description = description->getLastText();\n\t\t});\n\t}, description->lifetime());\n\n\t_setFocus = [=] {\n\t\ttitle->setFocusFast();\n\t};\n\n\tUi::AddDividerText(\n\t\tcontent,\n\t\ttr::lng_chat_intro_about(),\n\t\tst::peerAppearanceDividerTextMargin);\n\tUi::AddSkip(content);\n\n\tconst auto resetWrap = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_chat_intro_reset(),\n\t\t\t\tst::settingsAttentionButton\n\t\t\t)));\n\tresetWrap->toggleOn(\n\t\t_intro.value() | rpl::map([](const Data::ChatIntro &intro) {\n\t\t\treturn !!intro;\n\t\t}));\n\tresetWrap->entity()->setClickedCallback([=] {\n\t\t_intro = Data::ChatIntro();\n\t\ttitle->clear();\n\t\tdescription->clear();\n\t\ttitle->setFocus();\n\t});\n\n\tUi::ResizeFitChild(this, content);\n}\n\nvoid ChatIntro::save() {\n\tconst auto fail = [=](QString error) {\n\t};\n\tcontroller()->session().data().businessInfo().saveChatIntro(\n\t\t_intro.current(),\n\t\tfail);\n}\n\n} // namespace\n\nType ChatIntroId() {\n\treturn ChatIntro::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_chat_intro.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\n[[nodiscard]] Type ChatIntroId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_chat_links.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/business/settings_chat_links.h\"\n\n#include \"api/api_chat_links.h\"\n#include \"apiwrap.h\"\n#include \"base/event_filter.h\"\n#include \"boxes/peers/edit_peer_invite_link.h\"\n#include \"boxes/peers/edit_peer_invite_links.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/application.h\"\n#include \"core/ui_integration.h\"\n#include \"core/core_settings.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_document.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"settings/business/settings_recipients_helper.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\n#include <QtGui/QGuiApplication>\n\nnamespace Settings {\nnamespace {\n\nconstexpr auto kChangesDebounceTimeout = crl::time(1000);\n\nusing ChatLinkData = Api::ChatLink;\n\nclass ChatLinks final : public Section<ChatLinks> {\npublic:\n\tChatLinks(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~ChatLinks();\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\tconst Ui::RoundRect *bottomSkipRounding() const override {\n\t\treturn &_bottomSkipRounding;\n\t}\n\nprivate:\n\tvoid setupContent(not_null<Window::SessionController*> controller);\n\n\tUi::RoundRect _bottomSkipRounding;\n\n};\n\nstruct ChatLinkAction {\n\tenum class Type {\n\t\tCopy,\n\t\tShare,\n\t\tRename,\n\t\tDelete,\n\t};\n\tQString link;\n\tType type = Type::Copy;\n};\n\nclass Row;\n\nclass RowDelegate {\npublic:\n\tvirtual not_null<Main::Session*> rowSession() = 0;\n\tvirtual void rowUpdateRow(not_null<Row*> row) = 0;\n\tvirtual void rowPaintIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size) = 0;\n};\n\nclass Row final : public PeerListRow {\npublic:\n\tRow(not_null<RowDelegate*> delegate, const ChatLinkData &data);\n\n\tvoid update(const ChatLinkData &data);\n\n\t[[nodiscard]] ChatLinkData data() const;\n\n\tQString generateName() override;\n\tQString generateShortName() override;\n\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\n\tQSize rightActionSize() const override;\n\tQMargins rightActionMargins() const override;\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\tbool rightActionDisabled() const override {\n\t\treturn true;\n\t}\n\n\tvoid paintStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) override;\n\nprivate:\n\tvoid updateStatus(const ChatLinkData &data);\n\n\tconst not_null<RowDelegate*> _delegate;\n\tChatLinkData _data;\n\tUi::Text::String _status;\n\tUi::Text::String _clicks;\n\n};\n\n[[nodiscard]] uint64 ComputeRowId(const ChatLinkData &data) {\n\treturn UniqueRowIdFromString(data.link);\n}\n\n[[nodiscard]] QString ComputeClicks(const ChatLinkData &link) {\n\treturn link.clicks\n\t\t? tr::lng_chat_links_clicks(tr::now, lt_count, link.clicks)\n\t\t: tr::lng_chat_links_no_clicks(tr::now);\n}\n\nRow::Row(not_null<RowDelegate*> delegate, const ChatLinkData &data)\n: PeerListRow(ComputeRowId(data))\n, _delegate(delegate)\n, _data(data) {\n\tsetCustomStatus(QString());\n\tupdateStatus(data);\n}\n\nvoid Row::updateStatus(const ChatLinkData &data) {\n\tconst auto context = Core::TextContext({\n\t\t.session = _delegate->rowSession(),\n\t\t.repaint = [=] { _delegate->rowUpdateRow(this); },\n\t});\n\t_status.setMarkedText(\n\t\tst::messageTextStyle,\n\t\tdata.message,\n\t\tkMarkupTextOptions,\n\t\tcontext);\n\t_clicks.setText(st::messageTextStyle, ComputeClicks(data));\n}\n\nvoid Row::update(const ChatLinkData &data) {\n\t_data = data;\n\tupdateStatus(data);\n\trefreshName(st::inviteLinkList.item);\n\t_delegate->rowUpdateRow(this);\n}\n\nChatLinkData Row::data() const {\n\treturn _data;\n}\n\nQString Row::generateName() {\n\tif (!_data.title.isEmpty()) {\n\t\treturn _data.title;\n\t}\n\tauto result = _data.link;\n\treturn result.replace(\n\t\tu\"https://\"_q,\n\t\tQString()\n\t);\n}\n\nQString Row::generateShortName() {\n\treturn generateName();\n}\n\nPaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) {\n\treturn [=](\n\t\t\tQPainter &p,\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tint outerWidth,\n\t\t\tint size) {\n\t\t_delegate->rowPaintIcon(p, x, y, size);\n\t};\n}\n\nQSize Row::rightActionSize() const {\n\treturn QSize(\n\t\t_clicks.maxWidth(),\n\t\tst::inviteLinkThreeDotsIcon.height());\n}\n\nQMargins Row::rightActionMargins() const {\n\treturn QMargins(\n\t\t0,\n\t\t(st::inviteLinkList.item.height - rightActionSize().height()) / 2,\n\t\tst::inviteLinkThreeDotsSkip,\n\t\t0);\n}\n\nvoid Row::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\tp.setPen(selected ? st::windowSubTextFgOver : st::windowSubTextFg);\n\t_clicks.draw(p, x, y, outerWidth);\n}\n\nvoid Row::paintStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) {\n\tp.setPen(selected ? st.statusFgOver : st.statusFg);\n\t_status.draw(p, {\n\t\t.position = { x, y },\n\t\t.outerWidth = outerWidth,\n\t\t.availableWidth = availableWidth,\n\t\t.palette = &st::defaultTextPalette,\n\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t.now = crl::now(),\n\t\t.elisionLines = 1,\n\t});\n}\n\nclass LinksController final\n\t: public PeerListController\n\t, public RowDelegate\n\t, public base::has_weak_ptr {\npublic:\n\texplicit LinksController(not_null<Window::SessionController*> window);\n\n\t[[nodiscard]] rpl::producer<int> fullCountValue() const {\n\t\treturn _count.value();\n\t}\n\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override;\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\tMain::Session &session() const override;\n\n\tnot_null<Main::Session*> rowSession() override;\n\tvoid rowUpdateRow(not_null<Row*> row) override;\n\tvoid rowPaintIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size) override;\n\nprivate:\n\tvoid appendRow(const ChatLinkData &data);\n\tvoid prependRow(const ChatLinkData &data);\n\tvoid updateRow(const ChatLinkData &data);\n\tbool removeRow(const QString &link);\n\n\tvoid showRowMenu(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool highlightRow);\n\n\t[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row);\n\n\tconst not_null<Window::SessionController*> _window;\n\tconst not_null<Main::Session*> _session;\n\trpl::variable<int> _count;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tQImage _icon;\n\trpl::lifetime _lifetime;\n\n};\n\nstruct LinksList {\n\tnot_null<Ui::RpWidget*> widget;\n\tnot_null<LinksController*> controller;\n};\n\nLinksList AddLinksList(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tauto &lifetime = container->lifetime();\n\tconst auto delegate = lifetime.make_state<PeerListContentDelegateShow>(\n\t\twindow->uiShow());\n\tconst auto controller = lifetime.make_state<LinksController>(window);\n\tcontroller->setStyleOverrides(&st::inviteLinkList);\n\tconst auto content = container->add(object_ptr<PeerListContent>(\n\t\tcontainer,\n\t\tcontroller));\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\n\treturn { content, controller };\n}\n\nvoid EditChatLinkBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tChatLinkData data,\n\t\tFn<void(ChatLinkData, Fn<void()> close)> submit) {\n\tbox->setTitle(data.link.isEmpty()\n\t\t? tr::lng_chat_link_new_title()\n\t\t: tr::lng_chat_link_edit_title());\n\n\tbox->setWidth(st::boxWideWidth);\n\n\tUi::AddDividerText(\n\t\tbox->verticalLayout(),\n\t\ttr::lng_chat_link_description());\n\n\tconst auto peer = controller->session().user();\n\tconst auto outer = box->getDelegate()->outerContainer();\n\tconst auto field = box->addRow(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tbox.get(),\n\t\t\tst::settingsChatLinkField,\n\t\t\tUi::InputField::Mode::MultiLine,\n\t\t\ttr::lng_chat_link_placeholder()));\n\tbox->setFocusCallback([=] {\n\t\tfield->setFocusFast();\n\t});\n\n\tUi::AddDivider(box->verticalLayout());\n\tUi::AddSkip(box->verticalLayout());\n\n\tconst auto title = box->addRow(object_ptr<Ui::InputField>(\n\t\tbox.get(),\n\t\tst::defaultInputField,\n\t\ttr::lng_chat_link_name(),\n\t\tdata.title));\n\n\tconst auto emojiToggle = Ui::CreateChild<Ui::EmojiButton>(\n\t\tfield->parentWidget(),\n\t\tst::defaultComposeFiles.emoji);\n\n\tusing Selector = ChatHelpers::TabbedSelector;\n\tauto &lifetime = box->lifetime();\n\tconst auto emojiPanel = lifetime.make_state<ChatHelpers::TabbedPanel>(\n\t\touter,\n\t\tcontroller,\n\t\tobject_ptr<Selector>(\n\t\t\tnullptr,\n\t\t\tcontroller->uiShow(),\n\t\t\tWindow::GifPauseReason::Layer,\n\t\t\tSelector::Mode::EmojiOnly));\n\temojiPanel->setDesiredHeightValues(\n\t\t1.,\n\t\tst::emojiPanMinHeight / 2,\n\t\tst::emojiPanMinHeight);\n\temojiPanel->hide();\n\temojiPanel->selector()->setCurrentPeer(peer);\n\temojiPanel->selector()->emojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {\n\t\tUi::InsertEmojiAtCursor(field->textCursor(), data.emoji);\n\t}, field->lifetime());\n\temojiPanel->selector()->customEmojiChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\tData::InsertCustomEmoji(field, data.document);\n\t}, field->lifetime());\n\n\temojiToggle->installEventFilter(emojiPanel);\n\temojiToggle->addClickHandler([=] {\n\t\temojiPanel->toggleAnimated();\n\t});\n\n\tconst auto allow = [](not_null<DocumentData*>) { return true; };\n\tInitMessageFieldHandlers(\n\t\tcontroller,\n\t\tfield,\n\t\tWindow::GifPauseReason::Layer,\n\t\tallow);\n\tUi::Emoji::SuggestionsController::Init(\n\t\touter,\n\t\tfield,\n\t\t&controller->session(),\n\t\t{ .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow });\n\n\tfield->setSubmitSettings(Core::App().settings().sendSubmitWay());\n\tfield->setMaxHeight(st::defaultComposeFiles.caption.heightMax);\n\n\tconst auto save = [=] {\n\t\tauto copy = data;\n\t\tcopy.title = title->getLastText().trimmed();\n\t\tauto textWithTags = field->getTextWithAppliedMarkdown();\n\t\tcopy.message = TextWithEntities{\n\t\t\ttextWithTags.text,\n\t\t\tTextUtilities::ConvertTextTagsToEntities(textWithTags.tags)\n\t\t};\n\t\tsubmit(copy, crl::guard(box, [=] {\n\t\t\tbox->closeBox();\n\t\t}));\n\t};\n\tconst auto updateEmojiPanelGeometry = [=] {\n\t\tconst auto parent = emojiPanel->parentWidget();\n\t\tconst auto global = emojiToggle->mapToGlobal({ 0, 0 });\n\t\tconst auto local = parent->mapFromGlobal(global);\n\t\temojiPanel->moveBottomRight(\n\t\t\tlocal.y(),\n\t\t\tlocal.x() + emojiToggle->width() * 3);\n\t};\n\tconst auto filterCallback = [=](not_null<QEvent*> event) {\n\t\tconst auto type = event->type();\n\t\tif (type == QEvent::Move || type == QEvent::Resize) {\n\t\t\t// updateEmojiPanelGeometry uses not only container geometry, but\n\t\t\t// also container children geometries that will be updated later.\n\t\t\tcrl::on_main(emojiPanel, updateEmojiPanelGeometry);\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t};\n\tbase::install_event_filter(emojiPanel, outer, filterCallback);\n\n\tfield->submits(\n\t) | rpl::on_next([=] {\n\t\ttitle->setFocus();\n\t}, field->lifetime());\n\tfield->cancelled(\n\t) | rpl::on_next([=] {\n\t\tbox->closeBox();\n\t}, field->lifetime());\n\n\ttitle->submits(\n\t) | rpl::on_next(save, title->lifetime());\n\n\trpl::combine(\n\t\tbox->sizeValue(),\n\t\tfield->geometryValue()\n\t) | rpl::on_next([=](QSize outer, QRect inner) {\n\t\temojiToggle->moveToLeft(\n\t\t\tinner.x() + inner.width() - emojiToggle->width(),\n\t\t\tinner.y() + st::settingsChatLinkEmojiTop);\n\t\temojiToggle->update();\n\t\tcrl::on_main(emojiPanel, updateEmojiPanelGeometry);\n\t}, emojiToggle->lifetime());\n\n\tconst auto initial = TextWithTags{\n\t\tdata.message.text,\n\t\tTextUtilities::ConvertEntitiesToTextTags(data.message.entities)\n\t};\n\tfield->setTextWithTags(initial, Ui::InputField::HistoryAction::Clear);\n\tauto cursor = field->textCursor();\n\tcursor.movePosition(QTextCursor::End);\n\tfield->setTextCursor(cursor);\n\n\tconst auto checkChangedTimer = lifetime.make_state<base::Timer>([=] {\n\t\tif (field->getTextWithAppliedMarkdown() == initial) {\n\t\t\tbox->setCloseByOutsideClick(true);\n\t\t}\n\t});\n\tfield->changes(\n\t) | rpl::on_next([=] {\n\t\tcheckChangedTimer->callOnce(kChangesDebounceTimeout);\n\t\tbox->setCloseByOutsideClick(false);\n\t}, field->lifetime());\n\n\tbox->addButton(tr::lng_settings_save(), save);\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\nvoid EditChatLink(\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<Main::Session*> session,\n\t\tChatLinkData data) {\n\tconst auto submitting = std::make_shared<bool>();\n\tconst auto submit = [=](ChatLinkData data, Fn<void()> close) {\n\t\tif (std::exchange(*submitting, true)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto done = crl::guard(window, [=](const auto&) {\n\t\t\twindow->showToast(tr::lng_chat_link_saved(tr::now));\n\t\t\tclose();\n\t\t});\n\t\tsession->api().chatLinks().edit(\n\t\t\tdata.link,\n\t\t\tdata.title,\n\t\t\tdata.message,\n\t\t\tdone);\n\t};\n\twindow->show(Box(\n\t\tEditChatLinkBox,\n\t\twindow,\n\t\tdata,\n\t\tcrl::guard(window, submit)));\n}\n\nLinksController::LinksController(\n\tnot_null<Window::SessionController*> window)\n: _window(window)\n, _session(&window->session()) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_icon = QImage();\n\t}, _lifetime);\n\n\t_session->api().chatLinks().updates(\n\t) | rpl::on_next([=](const Api::ChatLinkUpdate &update) {\n\t\tif (!update.now) {\n\t\t\tif (removeRow(update.was)) {\n\t\t\t\tdelegate()->peerListRefreshRows();\n\t\t\t}\n\t\t} else if (update.was.isEmpty()) {\n\t\t\tprependRow(*update.now);\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t} else {\n\t\t\tupdateRow(*update.now);\n\t\t}\n\t}, _lifetime);\n}\n\nvoid LinksController::prepare() {\n\tauto &&list = _session->api().chatLinks().list()\n\t\t| ranges::views::reverse;\n\tfor (const auto &link : list) {\n\t\tappendRow(link);\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid LinksController::rowClicked(not_null<PeerListRow*> row) {\n\tshowRowMenu(row, true);\n}\n\nvoid LinksController::showRowMenu(\n\t\tnot_null<PeerListRow*> row,\n\t\tbool highlightRow) {\n\tdelegate()->peerListShowRowMenu(row, highlightRow);\n}\n\nvoid LinksController::rowRightActionClicked(not_null<PeerListRow*> row) {\n\tdelegate()->peerListShowRowMenu(row, true);\n}\n\nbase::unique_qptr<Ui::PopupMenu> LinksController::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tauto result = createRowContextMenu(parent, row);\n\n\tif (result) {\n\t\t// First clear _menu value, so that we don't check row positions yet.\n\t\tbase::take(_menu);\n\n\t\t// Here unique_qptr is used like a shared pointer, where\n\t\t// not the last destroyed pointer destroys the object, but the first.\n\t\t_menu = base::unique_qptr<Ui::PopupMenu>(result.get());\n\t}\n\n\treturn result;\n}\n\nbase::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tconst auto real = static_cast<Row*>(row.get());\n\tconst auto data = real->data();\n\tconst auto link = data.link;\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\tresult->addAction(tr::lng_group_invite_context_copy(tr::now), [=] {\n\t\tQGuiApplication::clipboard()->setText(link);\n\t\tdelegate()->peerListUiShow()->showToast(\n\t\t\ttr::lng_chat_link_copied(tr::now));\n\t}, &st::menuIconCopy);\n\tresult->addAction(tr::lng_group_invite_context_share(tr::now), [=] {\n\t\tdelegate()->peerListUiShow()->showBox(ShareInviteLinkBox(\n\t\t\t_session,\n\t\t\tlink,\n\t\t\ttr::lng_chat_link_copied(tr::now)));\n\t}, &st::menuIconShare);\n\tresult->addAction(tr::lng_group_invite_context_qr(tr::now), [=] {\n\t\tdelegate()->peerListUiShow()->showBox(InviteLinkQrBox(\n\t\t\tnullptr,\n\t\t\tlink,\n\t\t\ttr::lng_chat_link_qr_title(),\n\t\t\ttr::lng_chat_link_qr_about()));\n\t}, &st::menuIconQrCode);\n\tresult->addAction(tr::lng_group_invite_context_edit(tr::now), [=] {\n\t\tEditChatLink(_window, _session, data);\n\t}, &st::menuIconEdit);\n\tresult->addAction(tr::lng_group_invite_context_delete(tr::now), [=] {\n\t\tconst auto sure = [=](Fn<void()> &&close) {\n\t\t\t_window->session().api().chatLinks().destroy(link, close);\n\t\t};\n\t\t_window->show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_chat_link_delete_sure(tr::now),\n\t\t\t.confirmed = sure,\n\t\t\t.confirmText = tr::lng_box_delete(tr::now),\n\t\t}));\n\t}, &st::menuIconDelete);\n\treturn result;\n}\n\nMain::Session &LinksController::session() const {\n\treturn *_session;\n}\n\nvoid LinksController::appendRow(const ChatLinkData &data) {\n\tdelegate()->peerListAppendRow(std::make_unique<Row>(this, data));\n\t_count = _count.current() + 1;\n}\n\nvoid LinksController::prependRow(const ChatLinkData &data) {\n\tdelegate()->peerListPrependRow(std::make_unique<Row>(this, data));\n\t_count = _count.current() + 1;\n}\n\nvoid LinksController::updateRow(const ChatLinkData &data) {\n\tif (const auto row = delegate()->peerListFindRow(ComputeRowId(data))) {\n\t\tconst auto real = static_cast<Row*>(row);\n\t\treal->update(data);\n\t\tdelegate()->peerListUpdateRow(row);\n\t}\n}\n\nbool LinksController::removeRow(const QString &link) {\n\tconst auto id = UniqueRowIdFromString(link);\n\tif (const auto row = delegate()->peerListFindRow(id)) {\n\t\tdelegate()->peerListRemoveRow(row);\n\t\t_count = std::max(_count.current() - 1, 0);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nnot_null<Main::Session*> LinksController::rowSession() {\n\treturn _session;\n}\n\nvoid LinksController::rowUpdateRow(not_null<Row*> row) {\n\tdelegate()->peerListUpdateRow(row);\n}\n\nvoid LinksController::rowPaintIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size) {\n\tconst auto skip = st::inviteLinkIconSkip;\n\tconst auto inner = size - 2 * skip;\n\tconst auto bg = &st::msgFile1Bg;\n\tif (_icon.isNull()) {\n\t\t_icon = QImage(\n\t\t\tQSize(inner, inner) * style::DevicePixelRatio(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t_icon.fill(Qt::transparent);\n\t\t_icon.setDevicePixelRatio(style::DevicePixelRatio());\n\n\t\tauto p = QPainter(&_icon);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(*bg);\n\t\t{\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tauto rect = QRect(0, 0, inner, inner);\n\t\t\tp.drawEllipse(rect);\n\t\t}\n\t\tst::inviteLinkIcon.paintInCenter(p, Rect(Size(inner)));\n\t}\n\tp.drawImage(x + skip, y + skip, _icon);\n}\n\nChatLinks::ChatLinks(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller)\n, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {\n\tsetupContent(controller);\n}\n\nChatLinks::~ChatLinks() = default;\n\nrpl::producer<QString> ChatLinks::title() {\n\treturn tr::lng_chat_links_title();\n}\n\nvoid ChatLinks::setupContent(\n\t\tnot_null<Window::SessionController*> controller) {\n\tusing namespace rpl::mappers;\n\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tAddDividerTextWithLottie(content, {\n\t\t.lottie = u\"chat_link\"_q,\n\t\t.lottieSize = st::settingsCloudPasswordIconSize,\n\t\t.lottieMargins = st::peerAppearanceIconPadding,\n\t\t.showFinished = showFinishes() | rpl::take(1),\n\t\t.about = tr::lng_chat_links_about(tr::marked),\n\t\t.aboutMargins = st::peerAppearanceCoverLabelMargin,\n\t});\n\n\tUi::AddSkip(content);\n\n\tconst auto limit = controller->session().appConfig().get<int>(\n\t\tu\"business_chat_links_limit\"_q,\n\t\t100);\n\tconst auto add = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\tcontent,\n\t\t\tMakeCreateLinkButton(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_chat_links_create_link()))\n\t)->setDuration(0);\n\n\tconst auto list = AddLinksList(controller, content);\n\tadd->toggleOn(list.controller->fullCountValue() | rpl::map(_1 < limit));\n\tadd->finishAnimating();\n\n\tadd->entity()->setClickedCallback([=] {\n\t\tif (!controller->session().premium()) {\n\t\t\tShowPremiumPreviewToBuy(\n\t\t\t\tcontroller,\n\t\t\t\tPremiumFeature::ChatLinks);\n\t\t\treturn;\n\t\t}\n\t\tconst auto submitting = std::make_shared<bool>();\n\t\tconst auto submit = [=](ChatLinkData data, Fn<void()> close) {\n\t\t\tif (std::exchange(*submitting, true)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto done = [=](const auto&) {\n\t\t\t\tcontroller->showToast(tr::lng_chat_link_saved(tr::now));\n\t\t\t\tclose();\n\t\t\t};\n\t\t\tcontroller->session().api().chatLinks().create(\n\t\t\t\tdata.title,\n\t\t\t\tdata.message,\n\t\t\t\tdone);\n\t\t};\n\t\tcontroller->show(Box(\n\t\t\tEditChatLinkBox,\n\t\t\tcontroller,\n\t\t\tChatLinkData(),\n\t\t\tcrl::guard(this, submit)));\n\t});\n\n\tUi::AddSkip(content);\n\n\tconst auto self = controller->session().user();\n\tconst auto username = self->username();\n\tconst auto make = [&](std::vector<QString> links) {\n\t\tExpects(!links.empty());\n\n\t\tfor (auto &link : links) {\n\t\t\tlink = controller->session().createInternalLink(link);\n\t\t}\n\t\treturn (links.size() > 1)\n\t\t\t? tr::lng_chat_links_footer_both(\n\t\t\t\ttr::now,\n\t\t\t\tlt_username,\n\t\t\t\ttr::link(links[0], \"https://\" + links[0]),\n\t\t\t\tlt_link,\n\t\t\t\ttr::link(links[1], \"https://\" + links[1]),\n\t\t\t\ttr::marked)\n\t\t\t: tr::link(links[0], \"https://\" + links[0]);\n\t};\n\tauto links = !username.isEmpty()\n\t\t? make({ username, '+' + self->phone() })\n\t\t: make({ '+' + self->phone() });\n\tauto label = object_ptr<Ui::FlatLabel>(\n\t\tcontent,\n\t\ttr::lng_chat_links_footer(\n\t\t\tlt_links,\n\t\t\trpl::single(std::move(links)),\n\t\t\ttr::marked),\n\t\tst::boxDividerLabel);\n\tlabel->setClickHandlerFilter([=](ClickHandlerPtr handler, auto) {\n\t\tQGuiApplication::clipboard()->setText(handler->url());\n\t\tcontroller->showToast(tr::lng_chat_link_copied(tr::now));\n\t\treturn false;\n\t});\n\tcontent->add(object_ptr<Ui::DividerLabel>(\n\t\tcontent,\n\t\tstd::move(label),\n\t\tst::settingsChatbotsBottomTextMargin,\n\t\tst::defaultDividerBar,\n\t\tRectPart::Top));\n\n\tUi::ResizeFitChild(this, content);\n}\n\n} // namespace\n\nType ChatLinksId() {\n\treturn ChatLinks::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_chat_links.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\n[[nodiscard]] Type ChatLinksId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_chatbots.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/business/settings_chatbots.h\"\n\n#include \"apiwrap.h\"\n#include \"boxes/peers/edit_peer_permissions_box.h\"\n#include \"boxes/peers/prepare_short_info_box.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"core/application.h\"\n#include \"data/business/data_business_chatbots.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/business/settings_recipients_helper.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nconstexpr auto kDebounceTimeout = crl::time(400);\n\nenum class LookupState {\n\tEmpty,\n\tLoading,\n\tUnsupported,\n\tReady,\n};\n\nstruct BotState {\n\tUserData *bot = nullptr;\n\tLookupState state = LookupState::Empty;\n};\n\n[[nodiscard]] constexpr Data::ChatbotsPermissions Defaults() {\n\treturn Data::ChatbotsPermission::ViewMessages;\n}\n\nclass Chatbots final : public Section<Chatbots> {\npublic:\n\tChatbots(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~Chatbots();\n\n\t[[nodiscard]] bool closeByOutsideClick() const override;\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\tconst Ui::RoundRect *bottomSkipRounding() const override {\n\t\treturn _detailsWrap->count() ? nullptr : &_bottomSkipRounding;\n\t}\n\nprivate:\n\tvoid setupContent();\n\tvoid refreshDetails();\n\tvoid save();\n\n\tUi::RoundRect _bottomSkipRounding;\n\n\tUi::VerticalLayout *_detailsWrap = nullptr;\n\n\trpl::variable<Data::BusinessRecipients> _recipients;\n\trpl::variable<QString> _usernameValue;\n\trpl::variable<BotState> _botValue;\n\trpl::variable<Data::ChatbotsPermissions> _permissions = Defaults();\n\tFn<Data::ChatbotsPermissions()> _resolvePermissions;\n\n};\n\nclass PreviewController final : public PeerListController {\npublic:\n\tPreviewController(not_null<PeerData*> peer, Fn<void()> resetBot);\n\n\tvoid prepare() override;\n\tvoid loadMoreRows() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override;\n\tMain::Session &session() const override;\n\nprivate:\n\tconst not_null<PeerData*> _peer;\n\tconst Fn<void()> _resetBot;\n\trpl::lifetime _lifetime;\n\n};\n\nclass PreviewRow final : public PeerListRow {\npublic:\n\tusing PeerListRow::PeerListRow;\n\n\tQSize rightActionSize() const override;\n\tQMargins rightActionMargins() const override;\n\tvoid rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) override;\n\tvoid rightActionAddRipple(\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) override;\n\tvoid rightActionStopLastRipple() override;\n\nprivate:\n\tstd::unique_ptr<Ui::RippleAnimation> _actionRipple;\n\n};\n\nQSize PreviewRow::rightActionSize() const {\n\treturn QSize(\n\t\tst::settingsChatbotsDeleteIcon.width(),\n\t\tst::settingsChatbotsDeleteIcon.height()) * 2;\n}\n\nQMargins PreviewRow::rightActionMargins() const {\n\tconst auto itemHeight = st::peerListSingleRow.item.height;\n\tconst auto skip = (itemHeight - rightActionSize().height()) / 2;\n\treturn QMargins(0, skip, skip, 0);\n}\n\nvoid PreviewRow::rightActionPaint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tbool actionSelected) {\n\tif (_actionRipple) {\n\t\t_actionRipple->paint(p, x, y, outerWidth);\n\t\tif (_actionRipple->empty()) {\n\t\t\t_actionRipple.reset();\n\t\t}\n\t}\n\tconst auto rect = QRect(QPoint(x, y), PreviewRow::rightActionSize());\n\t(actionSelected\n\t\t? st::settingsChatbotsDeleteIconOver\n\t\t: st::settingsChatbotsDeleteIcon).paintInCenter(p, rect);\n}\n\nvoid PreviewRow::rightActionAddRipple(\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) {\n\tif (!_actionRipple) {\n\t\tauto mask = Ui::RippleAnimation::EllipseMask(rightActionSize());\n\t\t_actionRipple = std::make_unique<Ui::RippleAnimation>(\n\t\t\tst::defaultRippleAnimation,\n\t\t\tstd::move(mask),\n\t\t\tstd::move(updateCallback));\n\t}\n\t_actionRipple->add(point);\n}\n\nvoid PreviewRow::rightActionStopLastRipple() {\n\tif (_actionRipple) {\n\t\t_actionRipple->lastStop();\n\t}\n}\n\nPreviewController::PreviewController(\n\tnot_null<PeerData*> peer,\n\tFn<void()> resetBot)\n: _peer(peer)\n, _resetBot(std::move(resetBot)) {\n}\n\nvoid PreviewController::prepare() {\n\tdelegate()->peerListAppendRow(std::make_unique<PreviewRow>(_peer));\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid PreviewController::loadMoreRows() {\n}\n\nvoid PreviewController::rowClicked(not_null<PeerListRow*> row) {\n}\n\nvoid PreviewController::rowRightActionClicked(not_null<PeerListRow*> row) {\n\t_resetBot();\n}\n\nMain::Session &PreviewController::session() const {\n\treturn _peer->session();\n}\n\n[[nodiscard]] rpl::producer<QString> DebouncedValue(\n\t\tnot_null<Ui::InputField*> field) {\n\treturn [=](auto consumer) {\n\n\t\tauto result = rpl::lifetime();\n\t\tstruct State {\n\t\t\tbase::Timer timer;\n\t\t\tQString lastText;\n\t\t};\n\t\tconst auto state = result.make_state<State>();\n\t\tconst auto push = [=] {\n\t\t\tstate->timer.cancel();\n\t\t\tconsumer.put_next_copy(state->lastText);\n\t\t};\n\t\tstate->timer.setCallback(push);\n\t\tstate->lastText = field->getLastText();\n\t\tconsumer.put_next_copy(field->getLastText());\n\t\tfield->changes() | rpl::on_next([=] {\n\t\t\tconst auto &text = field->getLastText();\n\t\t\tconst auto was = std::exchange(state->lastText, text);\n\t\t\tif (std::abs(int(text.size()) - int(was.size())) == 1) {\n\t\t\t\tstate->timer.callOnce(kDebounceTimeout);\n\t\t\t} else {\n\t\t\t\tpush();\n\t\t\t}\n\t\t}, result);\n\t\treturn result;\n\t};\n}\n\n[[nodiscard]] QString ExtractUsername(QString text) {\n\ttext = text.trimmed();\n\tif (text.startsWith(QChar('@'))) {\n\t\treturn text.mid(1);\n\t}\n\tstatic const auto expression = QRegularExpression(\n\t\t\"^(https://)?([a-zA-Z0-9\\\\.]+/)?([a-zA-Z0-9_\\\\.]+)\");\n\tconst auto match = expression.match(text);\n\treturn match.hasMatch() ? match.captured(3) : text;\n}\n\n[[nodiscard]] rpl::producer<BotState> LookupBot(\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<QString> usernameChanges) {\n\tusing Cache = base::flat_map<QString, UserData*>;\n\tconst auto cache = std::make_shared<Cache>();\n\treturn std::move(\n\t\tusernameChanges\n\t) | rpl::map([=](const QString &username) -> rpl::producer<BotState> {\n\t\tconst auto extracted = ExtractUsername(username);\n\t\tconst auto owner = &session->data();\n\t\tstatic const auto expression = QRegularExpression(\n\t\t\t\"^[a-zA-Z0-9_\\\\.]+$\");\n\t\tif (!expression.match(extracted).hasMatch()) {\n\t\t\treturn rpl::single(BotState());\n\t\t} else if (const auto peer = owner->peerByUsername(extracted)) {\n\t\t\tif (const auto user = peer->asUser(); user && user->isBot()) {\n\t\t\t\tif (user->botInfo->supportsBusiness) {\n\t\t\t\t\treturn rpl::single(BotState{\n\t\t\t\t\t\t.bot = user,\n\t\t\t\t\t\t.state = LookupState::Ready,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn rpl::single(BotState{\n\t\t\t\t\t.state = LookupState::Unsupported,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn rpl::single(BotState{\n\t\t\t\t.state = LookupState::Ready,\n\t\t\t});\n\t\t} else if (const auto i = cache->find(extracted); i != end(*cache)) {\n\t\t\treturn rpl::single(BotState{\n\t\t\t\t.bot = i->second,\n\t\t\t\t.state = LookupState::Ready,\n\t\t\t});\n\t\t}\n\n\t\treturn [=](auto consumer) {\n\t\t\tauto result = rpl::lifetime();\n\n\t\t\tconst auto requestId = result.make_state<mtpRequestId>();\n\t\t\t*requestId = session->api().request(MTPcontacts_ResolveUsername(\n\t\t\t\tMTP_flags(0),\n\t\t\t\tMTP_string(extracted),\n\t\t\t\tMTP_string()\n\t\t\t)).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\t\t\tconst auto &data = result.data();\n\t\t\t\tsession->data().processUsers(data.vusers());\n\t\t\t\tsession->data().processChats(data.vchats());\n\t\t\t\tconst auto peerId = peerFromMTP(data.vpeer());\n\t\t\t\tconst auto peer = session->data().peer(peerId);\n\t\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\t\tif (user->isBot()) {\n\t\t\t\t\t\tcache->emplace(extracted, user);\n\t\t\t\t\t\tconsumer.put_next(BotState{\n\t\t\t\t\t\t\t.bot = user,\n\t\t\t\t\t\t\t.state = LookupState::Ready,\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcache->emplace(extracted, nullptr);\n\t\t\t\tconsumer.put_next(BotState{ .state = LookupState::Ready });\n\t\t\t}).fail([=] {\n\t\t\t\tcache->emplace(extracted, nullptr);\n\t\t\t\tconsumer.put_next(BotState{ .state = LookupState::Ready });\n\t\t\t}).send();\n\n\t\t\tresult.add([=] {\n\t\t\t\tsession->api().request(*requestId).cancel();\n\t\t\t});\n\t\t\treturn result;\n\t\t};\n\t}) | rpl::flatten_latest();\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeBotPreview(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<BotState> state,\n\t\tFn<void()> resetBot) {\n\tauto result = object_ptr<Ui::SlideWrap<>>(\n\t\tparent.get(),\n\t\tobject_ptr<Ui::RpWidget>(parent.get()));\n\tconst auto raw = result.data();\n\tconst auto inner = raw->entity();\n\traw->hide(anim::type::instant);\n\n\tconst auto child = inner->lifetime().make_state<Ui::RpWidget*>(nullptr);\n\tstd::move(state) | rpl::filter([=](BotState state) {\n\t\treturn state.state != LookupState::Loading;\n\t}) | rpl::on_next([=](BotState state) {\n\t\traw->toggle(\n\t\t\t(state.state == LookupState::Ready\n\t\t\t\t|| state.state == LookupState::Unsupported),\n\t\t\tanim::type::normal);\n\t\tif (state.bot) {\n\t\t\tconst auto delegate = parent->lifetime().make_state<\n\t\t\t\tPeerListContentDelegateSimple\n\t\t\t>();\n\t\t\tconst auto controller = parent->lifetime().make_state<\n\t\t\t\tPreviewController\n\t\t\t>(state.bot, resetBot);\n\t\t\tcontroller->setStyleOverrides(&st::peerListSingleRow);\n\t\t\tconst auto content = Ui::CreateChild<PeerListContent>(\n\t\t\t\tinner,\n\t\t\t\tcontroller);\n\t\t\tdelegate->setContent(content);\n\t\t\tcontroller->setDelegate(delegate);\n\t\t\tdelete base::take(*child);\n\t\t\t*child = content;\n\t\t} else if (state.state == LookupState::Ready\n\t\t\t|| state.state == LookupState::Unsupported) {\n\t\t\tconst auto content = Ui::CreateChild<Ui::RpWidget>(inner);\n\t\t\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\t(state.state == LookupState::Unsupported\n\t\t\t\t\t? tr::lng_chatbots_not_supported()\n\t\t\t\t\t: tr::lng_chatbots_not_found()),\n\t\t\t\tst::settingsChatbotsNotFound);\n\t\t\tcontent->resize(\n\t\t\t\tinner->width(),\n\t\t\t\tst::peerListSingleRow.item.height);\n\t\t\trpl::combine(\n\t\t\t\tcontent->sizeValue(),\n\t\t\t\tlabel->sizeValue()\n\t\t\t) | rpl::on_next([=](QSize size, QSize inner) {\n\t\t\t\tlabel->move(\n\t\t\t\t\t(size.width() - inner.width()) / 2,\n\t\t\t\t\t(size.height() - inner.height()) / 2);\n\t\t\t}, label->lifetime());\n\t\t\tdelete base::take(*child);\n\t\t\t*child = content;\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t\t(*child)->show();\n\n\t\tinner->widthValue() | rpl::on_next([=](int width) {\n\t\t\t(*child)->resizeToWidth(width);\n\t\t}, (*child)->lifetime());\n\n\t\t(*child)->heightValue() | rpl::on_next([=](int height) {\n\t\t\tinner->resize(inner->width(), height + st::contactSkip);\n\t\t}, inner->lifetime());\n\t}, inner->lifetime());\n\n\traw->finishAnimating();\n\treturn result;\n}\n\nChatbots::Chatbots(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller)\n, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {\n\tsetupContent();\n}\n\nChatbots::~Chatbots() {\n\tif (!Core::Quitting()) {\n\t\tsave();\n\t}\n}\n\nbool Chatbots::closeByOutsideClick() const {\n\treturn false;\n}\n\nrpl::producer<QString> Chatbots::title() {\n\treturn tr::lng_chatbots_title();\n}\n\nvoid Chatbots::setupContent() {\n\tusing namespace rpl::mappers;\n\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tconst auto current = controller()->session().data().chatbots().current();\n\n\t_recipients = Data::BusinessRecipients::MakeValid(current.recipients);\n\t_permissions = current.permissions;\n\n\tAddDividerTextWithLottie(content, {\n\t\t.lottie = u\"robot\"_q,\n\t\t.lottieSize = st::settingsCloudPasswordIconSize,\n\t\t.lottieMargins = st::peerAppearanceIconPadding,\n\t\t.showFinished = showFinishes(),\n\t\t.about = tr::lng_chatbots_about(\n\t\t\tlt_link,\n\t\t\ttr::lng_chatbots_about_link(\n\t\t\t\ttr::url(tr::lng_chatbots_info_url(tr::now))),\n\t\t\ttr::marked),\n\t\t.aboutMargins = st::peerAppearanceCoverLabelMargin,\n\t});\n\n\tconst auto username = content->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tcontent,\n\t\t\tst::settingsChatbotsUsername,\n\t\t\ttr::lng_chatbots_placeholder(),\n\t\t\t(current.bot\n\t\t\t\t? current.bot->session().createInternalLink(\n\t\t\t\t\tcurrent.bot->username())\n\t\t\t\t: QString())),\n\t\tst::settingsChatbotsUsernameMargins);\n\n\t_usernameValue = DebouncedValue(username);\n\t_botValue = rpl::single(BotState{\n\t\tcurrent.bot,\n\t\tcurrent.bot ? LookupState::Ready : LookupState::Empty\n\t}) | rpl::then(\n\t\tLookupBot(&controller()->session(), _usernameValue.changes())\n\t);\n\n\tconst auto resetBot = [=] {\n\t\tusername->setText(QString());\n\t\tusername->setFocus();\n\n\t\t_permissions = Defaults();\n\t\trefreshDetails();\n\t};\n\tcontent->add(object_ptr<Ui::SlideWrap<Ui::RpWidget>>(\n\t\tcontent,\n\t\tMakeBotPreview(content, _botValue.value(), resetBot)));\n\n\tUi::AddDividerText(\n\t\tcontent,\n\t\ttr::lng_chatbots_add_about(),\n\t\tst::peerAppearanceDividerTextMargin,\n\t\tst::defaultDividerLabel,\n\t\tRectPart::Top);\n\n\t_detailsWrap = content->add(object_ptr<Ui::VerticalLayout>(content));\n\n\trefreshDetails();\n\t_botValue.changes() | rpl::on_next([=](const BotState &value) {\n\t\t_permissions = Defaults();\n\t\trefreshDetails();\n\t}, lifetime());\n\n\tUi::ResizeFitChild(this, content);\n}\n\nvoid Chatbots::refreshDetails() {\n\t_resolvePermissions = [=] {\n\t\treturn Data::ChatbotsPermissions();\n\t};\n\twhile (_detailsWrap->count()) {\n\t\tdelete _detailsWrap->widgetAt(0);\n\t}\n\n\tconst auto bot = _botValue.current().bot;\n\tif (!bot) {\n\t\treturn;\n\t}\n\n\tconst auto content = _detailsWrap;\n\tAddBusinessRecipientsSelector(content, {\n\t\t.controller = controller(),\n\t\t.title = tr::lng_chatbots_access_title(),\n\t\t.data = &_recipients,\n\t\t.type = Data::BusinessRecipientsType::Bots,\n\t});\n\n\tUi::AddSkip(content, st::settingsChatbotsAccessSkip);\n\tUi::AddDividerText(\n\t\tcontent,\n\t\ttr::lng_chatbots_exclude_about(),\n\t\tst::peerAppearanceDividerTextMargin);\n\n\tUi::AddSkip(content);\n\tUi::AddSubsectionTitle(content, tr::lng_chatbots_permissions_title());\n\n\tauto permissions = CreateEditChatbotPermissions(\n\t\tcontent,\n\t\t_permissions.current());\n\tcontent->add(std::move(permissions.widget));\n\t_resolvePermissions = permissions.value;\n\n\n\tstd::move(\n\t\tpermissions.changes\n\t) | rpl::on_next([=](Data::ChatbotsPermissions now) {\n\t\tconst auto warn = [&](tr::phrase<lngtag_bot> text) {\n\t\t\tcontroller()->show(Ui::MakeInformBox({\n\t\t\t\t.text = text(tr::now, lt_bot, tr::bold(bot->name()), tr::rich),\n\t\t\t\t.title = tr::lng_chatbots_warning_title(),\n\t\t\t}));\n\t\t};\n\n\t\tconst auto was = _permissions.current();\n\t\tconst auto diff = now ^ was;\n\t\tconst auto enabled = diff & now;\n\t\tusing Flag = Data::ChatbotsPermission;\n\t\tif (enabled & (Flag::TransferGifts | Flag::SellGifts)) {\n\t\t\tif (enabled & Flag::TransferStars) {\n\t\t\t\twarn(tr::lng_chatbots_warning_both_text);\n\t\t\t} else {\n\t\t\t\twarn(tr::lng_chatbots_warning_gifts_text);\n\t\t\t}\n\t\t} else if (enabled & Flag::TransferStars) {\n\t\t\twarn(tr::lng_chatbots_warning_stars_text);\n\t\t} else if (enabled & Flag::EditUsername) {\n\t\t\twarn(tr::lng_chatbots_warning_username_text);\n\t\t}\n\t\t_permissions = now;\n\t}, lifetime());\n\n\tUi::AddSkip(content);\n\n\t_detailsWrap->resizeToWidth(width());\n}\n\nvoid Chatbots::save() {\n\tconst auto show = controller()->uiShow();\n\tconst auto fail = [=](QString error) {\n\t\tif (error == u\"BUSINESS_RECIPIENTS_EMPTY\"_q) {\n\t\t\tshow->showToast(tr::lng_greeting_recipients_empty(tr::now));\n\t\t} else if (error == u\"BOT_BUSINESS_MISSING\"_q) {\n\t\t\tshow->showToast(tr::lng_chatbots_not_supported(tr::now));\n\t\t}\n\t};\n\tcontroller()->session().data().chatbots().save({\n\t\t.bot = _botValue.current().bot,\n\t\t.recipients = _recipients.current(),\n\t\t.permissions = _resolvePermissions(),\n\t}, [=] {\n\t}, fail);\n}\n\n} // namespace\n\nType ChatbotsId() {\n\treturn Chatbots::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_chatbots.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\n[[nodiscard]] Type ChatbotsId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_greeting.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/business/settings_greeting.h\"\n\n#include \"base/event_filter.h\"\n#include \"core/application.h\"\n#include \"data/business/data_business_info.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/data_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/business/settings_shortcut_messages.h\"\n#include \"settings/business/settings_recipients_helper.h\"\n#include \"ui/boxes/time_picker_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/box_content_divider.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/vertical_drum_picker.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nconstexpr auto kDefaultNoActivityDays = 7;\n\nclass Greeting : public Section<Greeting> {\npublic:\n\tGreeting(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~Greeting();\n\n\t[[nodiscard]] bool closeByOutsideClick() const override;\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\tconst Ui::RoundRect *bottomSkipRounding() const override {\n\t\treturn &_bottomSkipRounding;\n\t}\n\nprivate:\n\tvoid setupContent(not_null<Window::SessionController*> controller);\n\tvoid save();\n\n\tUi::RoundRect _bottomSkipRounding;\n\n\trpl::variable<Data::BusinessRecipients> _recipients;\n\trpl::variable<bool> _canHave;\n\trpl::event_stream<> _deactivateOnAttempt;\n\trpl::variable<int> _noActivityDays;\n\trpl::variable<bool> _enabled;\n\n};\n\nGreeting::Greeting(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller)\n, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {\n\tsetupContent(controller);\n}\n\nvoid EditPeriodBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tint days,\n\t\tFn<void(int)> save) {\n\tauto values = std::vector{ 7, 14, 21, 28 };\n\tif (!ranges::contains(values, days)) {\n\t\tvalues.push_back(days);\n\t\tranges::sort(values);\n\t}\n\n\tconst auto phrases = ranges::views::all(\n\t\tvalues\n\t) | ranges::views::transform([](int days) {\n\t\treturn tr::lng_days(tr::now, lt_count, days);\n\t}) | ranges::to_vector;\n\tconst auto take = TimePickerBox(box, values, phrases, days);\n\n\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\tconst auto weak = base::make_weak(box);\n\t\tsave(take());\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nGreeting::~Greeting() {\n\tif (!Core::Quitting()) {\n\t\tsave();\n\t}\n}\n\nbool Greeting::closeByOutsideClick() const {\n\treturn false;\n}\n\nrpl::producer<QString> Greeting::title() {\n\treturn tr::lng_greeting_title();\n}\n\nvoid Greeting::setupContent(\n\t\tnot_null<Window::SessionController*> controller) {\n\tusing namespace rpl::mappers;\n\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tconst auto info = &controller->session().data().businessInfo();\n\tconst auto current = info->greetingSettings();\n\tconst auto disabled = !current.noActivityDays;\n\n\t_recipients = disabled\n\t\t? Data::BusinessRecipients{ .allButExcluded = true }\n\t\t: Data::BusinessRecipients::MakeValid(current.recipients);\n\t_noActivityDays = disabled\n\t\t? kDefaultNoActivityDays\n\t\t: current.noActivityDays;\n\n\tAddDividerTextWithLottie(content, {\n\t\t.lottie = u\"greeting\"_q,\n\t\t.lottieSize = st::settingsCloudPasswordIconSize,\n\t\t.lottieMargins = st::peerAppearanceIconPadding,\n\t\t.showFinished = showFinishes(),\n\t\t.about = tr::lng_greeting_about(tr::marked),\n\t\t.aboutMargins = st::peerAppearanceCoverLabelMargin,\n\t});\n\n\tconst auto session = &controller->session();\n\t_canHave = rpl::combine(\n\t\tShortcutsCountValue(session),\n\t\tShortcutsLimitValue(session),\n\t\tShortcutExistsValue(session, u\"hello\"_q),\n\t\t(_1 < _2) || _3);\n\n\tUi::AddSkip(content);\n\tconst auto enabled = content->add(object_ptr<Ui::SettingsButton>(\n\t\tcontent,\n\t\ttr::lng_greeting_enable(),\n\t\tst::settingsButtonNoIcon\n\t))->toggleOn(rpl::single(\n\t\t!disabled\n\t) | rpl::then(rpl::merge(\n\t\t_canHave.value() | rpl::filter(!_1),\n\t\t_deactivateOnAttempt.events() | rpl::map_to(false)\n\t)));\n\n\t_enabled = enabled->toggledValue();\n\t_enabled.value() | rpl::filter(_1) | rpl::on_next([=] {\n\t\tif (!_canHave.current()) {\n\t\t\tcontroller->showToast({\n\t\t\t\t.text = { tr::lng_greeting_limit_reached(tr::now) },\n\t\t\t\t.adaptive = true,\n\t\t\t});\n\t\t\t_deactivateOnAttempt.fire({});\n\t\t}\n\t}, lifetime());\n\n\tUi::AddSkip(content);\n\n\tcontent->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::BoxContentDivider>(\n\t\t\t\tcontent,\n\t\t\t\tst::boxDividerHeight,\n\t\t\t\tst::defaultDividerBar,\n\t\t\t\tRectPart::Top))\n\t)->setDuration(0)->toggleOn(enabled->toggledValue() | rpl::map(!_1));\n\tcontent->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::BoxContentDivider>(\n\t\t\t\tcontent))\n\t)->setDuration(0)->toggleOn(enabled->toggledValue());\n\n\tconst auto wrap = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::VerticalLayout>(content)));\n\tconst auto inner = wrap->entity();\n\n\tconst auto createWrap = inner->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tinner,\n\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\tconst auto createInner = createWrap->entity();\n\tUi::AddSkip(createInner);\n\tconst auto create = AddButtonWithLabel(\n\t\tcreateInner,\n\t\trpl::conditional(\n\t\t\tShortcutExistsValue(session, u\"hello\"_q),\n\t\t\ttr::lng_business_edit_messages(),\n\t\t\ttr::lng_greeting_create()),\n\t\tShortcutMessagesCountValue(\n\t\t\tsession,\n\t\t\tu\"hello\"_q\n\t\t) | rpl::map([=](int count) {\n\t\t\treturn count\n\t\t\t\t? tr::lng_forum_messages(tr::now, lt_count, count)\n\t\t\t\t: QString();\n\t\t}),\n\t\tst::settingsButtonLightNoIcon);\n\tcreate->setClickedCallback([=] {\n\t\tconst auto owner = &controller->session().data();\n\t\tconst auto id = owner->shortcutMessages().emplaceShortcut(\"hello\");\n\t\tshowOther(ShortcutMessagesId(id));\n\t});\n\tUi::AddSkip(createInner);\n\tUi::AddDivider(createInner);\n\n\tcreateWrap->toggleOn(rpl::single(true));\n\n\tUi::AddSkip(inner);\n\tAddBusinessRecipientsSelector(inner, {\n\t\t.controller = controller,\n\t\t.title = tr::lng_greeting_recipients(),\n\t\t.data = &_recipients,\n\t\t.type = Data::BusinessRecipientsType::Messages,\n\t});\n\n\tUi::AddSkip(inner);\n\tUi::AddDivider(inner);\n\tUi::AddSkip(inner);\n\n\tAddButtonWithLabel(\n\t\tinner,\n\t\ttr::lng_greeting_period_title(),\n\t\t_noActivityDays.value(\n\t\t) | rpl::map(\n\t\t\t[](int days) { return tr::lng_days(tr::now, lt_count, days); }\n\t\t),\n\t\tst::settingsButtonNoIcon\n\t)->setClickedCallback([=] {\n\t\tcontroller->show(Box(\n\t\t\tEditPeriodBox,\n\t\t\t_noActivityDays.current(),\n\t\t\t[=](int days) { _noActivityDays = days; }));\n\t});\n\n\tUi::AddSkip(inner);\n\tUi::AddDividerText(\n\t\tinner,\n\t\ttr::lng_greeting_period_about(),\n\t\tst::settingsChatbotsBottomTextMargin,\n\t\tst::defaultDividerLabel,\n\t\tRectPart::Top);\n\n\twrap->toggleOn(enabled->toggledValue());\n\twrap->finishAnimating();\n\n\tUi::ResizeFitChild(this, content);\n}\n\nvoid Greeting::save() {\n\tconst auto show = controller()->uiShow();\n\tconst auto session = &controller()->session();\n\tconst auto fail = [=](QString error) {\n\t\tif (error == u\"BUSINESS_RECIPIENTS_EMPTY\"_q) {\n\t\t\tshow->showToast(tr::lng_greeting_recipients_empty(tr::now));\n\t\t} else if (error != u\"SHORTCUT_INVALID\"_q) {\n\t\t\tshow->showToast(error);\n\t\t}\n\t};\n\tsession->data().businessInfo().saveGreetingSettings(\n\t\t_enabled.current() ? Data::GreetingSettings{\n\t\t\t.recipients = _recipients.current(),\n\t\t\t.noActivityDays = _noActivityDays.current(),\n\t\t\t.shortcutId = LookupShortcutId(session, u\"hello\"_q),\n\t\t} : Data::GreetingSettings(),\n\t\tfail);\n}\n\n} // namespace\n\nType GreetingId() {\n\treturn Greeting::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_greeting.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\n[[nodiscard]] Type GreetingId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_location.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/business/settings_location.h\"\n\n#include \"core/application.h\"\n#include \"core/shortcuts.h\"\n#include \"data/business/data_business_info.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"settings/business/settings_recipients_helper.h\"\n#include \"settings/settings_common.h\"\n#include \"storage/storage_account.h\"\n#include \"ui/controls/location_picker.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nclass Location : public Section<Location> {\npublic:\n\tLocation(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~Location();\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\tconst Ui::RoundRect *bottomSkipRounding() const override {\n\t\treturn mapSupported() ? nullptr : &_bottomSkipRounding;\n\t}\n\nprivate:\n\tvoid setupContent(not_null<Window::SessionController*> controller);\n\tvoid save();\n\n\tvoid setupPicker(not_null<Ui::VerticalLayout*> content);\n\tvoid setupUnsupported(not_null<Ui::VerticalLayout*> content);\n\n\t[[nodiscard]] bool mapSupported() const;\n\tvoid chooseOnMap();\n\n\tconst Ui::LocationPickerConfig _config;\n\trpl::variable<Data::BusinessLocation> _data;\n\trpl::variable<Data::CloudImage*> _map = nullptr;\n\tbase::weak_ptr<Ui::LocationPicker> _picker;\n\tstd::shared_ptr<QImage> _view;\n\tUi::RoundRect _bottomSkipRounding;\n\n};\n\n[[nodiscard]] Ui::LocationPickerConfig ResolveBusinessMapsConfig(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto &appConfig = session->appConfig();\n\tauto map = appConfig.get<base::flat_map<QString, QString>>(\n\t\tu\"tdesktop_config_map\"_q,\n\t\tbase::flat_map<QString, QString>());\n\treturn {\n\t\t.mapsToken = map[u\"bmaps\"_q],\n\t\t.geoToken = map[u\"bgeo\"_q],\n\t};\n}\n\nLocation::Location(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller)\n, _config(ResolveBusinessMapsConfig(&controller->session()))\n, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {\n\tsetupContent(controller);\n}\n\nLocation::~Location() {\n\tif (!Core::Quitting()) {\n\t\tsave();\n\t}\n}\n\nrpl::producer<QString> Location::title() {\n\treturn tr::lng_location_title();\n}\n\nvoid Location::setupContent(\n\t\tnot_null<Window::SessionController*> controller) {\n\tusing namespace rpl::mappers;\n\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tif (mapSupported()) {\n\t\tsetupPicker(content);\n\t} else {\n\t\tsetupUnsupported(content);\n\t}\n\n\tUi::ResizeFitChild(this, content);\n}\n\nvoid Location::setupPicker(not_null<Ui::VerticalLayout*> content) {\n\t_data = controller()->session().user()->businessDetails().location;\n\n\tAddDividerTextWithLottie(content, {\n\t\t.lottie = u\"location\"_q,\n\t\t.lottieSize = st::settingsCloudPasswordIconSize,\n\t\t.lottieMargins = st::peerAppearanceIconPadding,\n\t\t.showFinished = showFinishes(),\n\t\t.about = tr::lng_location_about(tr::marked),\n\t\t.aboutMargins = st::peerAppearanceCoverLabelMargin,\n\t});\n\n\tconst auto address = content->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tcontent,\n\t\t\tst::settingsLocationAddress,\n\t\t\tUi::InputField::Mode::MultiLine,\n\t\t\ttr::lng_location_address(),\n\t\t\t_data.current().address),\n\t\tst::settingsChatbotsUsernameMargins);\n\n\t_data.value(\n\t) | rpl::on_next([=](const Data::BusinessLocation &location) {\n\t\taddress->setText(location.address);\n\t}, address->lifetime());\n\n\taddress->changes() | rpl::on_next([=] {\n\t\tauto copy = _data.current();\n\t\tcopy.address = address->getLastText();\n\t\t_data = std::move(copy);\n\t\t}, address->lifetime());\n\n\tAddDivider(content);\n\tAddSkip(content);\n\n\tconst auto maptoggle = AddButtonWithIcon(\n\t\tcontent,\n\t\ttr::lng_location_set_map(),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconAddress }\n\t)->toggleOn(_data.value(\n\t) | rpl::map([](const Data::BusinessLocation &location) {\n\t\treturn location.point.has_value();\n\t}));\n\n\tmaptoggle->toggledValue() | rpl::on_next([=](bool toggled) {\n\t\tif (!toggled) {\n\t\t\tauto copy = _data.current();\n\t\t\tif (copy.point.has_value()) {\n\t\t\t\tcopy.point = std::nullopt;\n\t\t\t\t_data = std::move(copy);\n\t\t\t}\n\t\t} else if (!_data.current().point.has_value()) {\n\t\t\t_data.force_assign(_data.current());\n\t\t\tchooseOnMap();\n\t\t}\n\t}, maptoggle->lifetime());\n\n\tconst auto mapSkip = st::defaultVerticalListSkip;\n\tconst auto mapWrap = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::AbstractButton>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::AbstractButton>(content),\n\t\t\tst::boxRowPadding + QMargins(0, mapSkip, 0, mapSkip)));\n\tmapWrap->toggle(_data.current().point.has_value(), anim::type::instant);\n\n\tconst auto map = mapWrap->entity();\n\tmap->resize(map->width(), st::locationSize.height());\n\n\t_data.value(\n\t) | rpl::on_next([=](const Data::BusinessLocation &location) {\n\t\tconst auto image = location.point.has_value()\n\t\t\t? controller()->session().data().location(*location.point).get()\n\t\t\t: nullptr;\n\t\tif (image) {\n\t\t\timage->load(&controller()->session(), {});\n\t\t\t_view = image->createView();\n\t\t}\n\t\tmapWrap->toggle(image != nullptr, anim::type::normal);\n\t\t_map = image;\n\t}, mapWrap->lifetime());\n\n\tmap->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(map);\n\n\t\tconst auto left = (map->width() - st::locationSize.width()) / 2;\n\t\tconst auto rect = QRect(QPoint(left, 0), st::locationSize);\n\t\tconst auto &image = _view ? *_view : QImage();\n\t\tif (!image.isNull()) {\n\t\t\tp.drawImage(rect, image);\n\t\t}\n\n\t\tconst auto paintMarker = [&](const style::icon &icon) {\n\t\t\ticon.paint(\n\t\t\t\tp,\n\t\t\t\trect.x() + ((rect.width() - icon.width()) / 2),\n\t\t\t\trect.y() + (rect.height() / 2) - icon.height(),\n\t\t\t\twidth());\n\t\t\t};\n\t\tpaintMarker(st::historyMapPoint);\n\t\tpaintMarker(st::historyMapPointInner);\n\t}, map->lifetime());\n\n\tcontroller()->session().downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tmap->update();\n\t}, map->lifetime());\n\n\tmap->setClickedCallback([=] {\n\t\tchooseOnMap();\n\t});\n\n\tshowFinishes() | rpl::on_next([=] {\n\t\taddress->setFocus();\n\t}, address->lifetime());\n}\n\nvoid Location::chooseOnMap() {\n\tif (const auto strong = _picker.get()) {\n\t\tstrong->activate();\n\t\treturn;\n\t}\n\tconst auto callback = [=](Data::InputVenue venue) {\n\t\tauto copy = _data.current();\n\t\tcopy.point = Data::LocationPoint(\n\t\t\tvenue.lat,\n\t\t\tvenue.lon,\n\t\t\tData::LocationPoint::NoAccessHash);\n\t\tcopy.address = venue.address;\n\t\t_data = std::move(copy);\n\t};\n\tconst auto session = &controller()->session();\n\tconst auto current = _data.current().point;\n\tconst auto initial = current\n\t\t? Core::GeoLocation{\n\t\t\t.point = { current->lat(), current->lon() },\n\t\t\t.accuracy = Core::GeoLocationAccuracy::Exact,\n\t\t}\n\t\t: Core::GeoLocation();\n\t_picker = Ui::LocationPicker::Show({\n\t\t.parent = controller()->widget(),\n\t\t.config = _config,\n\t\t.chooseLabel = tr::lng_maps_point_set(),\n\t\t.session = session,\n\t\t.initial = initial,\n\t\t.callback = crl::guard(this, callback),\n\t\t.quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); },\n\t\t.storageId = session->local().resolveStorageIdBots(),\n\t\t.closeRequests = death(),\n\t});\n}\n\nvoid Location::setupUnsupported(not_null<Ui::VerticalLayout*> content) {\n\tAddDividerTextWithLottie(content, {\n\t\t.lottie = u\"phone\"_q,\n\t\t.lottieSize = st::settingsCloudPasswordIconSize,\n\t\t.lottieMargins = st::peerAppearanceIconPadding,\n\t\t.showFinished = showFinishes(),\n\t\t.about = tr::lng_location_fallback(tr::marked),\n\t\t.aboutMargins = st::peerAppearanceCoverLabelMargin,\n\t\t.parts = RectPart::Top,\n\t});\n}\n\nvoid Location::save() {\n\tconst auto fail = [=](QString error) {\n\t};\n\tauto value = _data.current();\n\tvalue.address = value.address.trimmed();\n\tcontroller()->session().data().businessInfo().saveLocation(value, fail);\n}\n\nbool Location::mapSupported() const {\n\treturn Ui::LocationPicker::Available(_config);\n}\n\n} // namespace\n\nType LocationId() {\n\treturn Location::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_location.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\n[[nodiscard]] Type LocationId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_quick_replies.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/business/settings_quick_replies.h\"\n\n#include \"boxes/premium_preview_box.h\"\n#include \"core/application.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/data_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"settings/business/settings_recipients_helper.h\"\n#include \"settings/business/settings_shortcut_messages.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nconstexpr auto kShortcutLimit = 32;\n\nclass QuickReplies : public Section<QuickReplies> {\npublic:\n\tQuickReplies(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~QuickReplies();\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\nprivate:\n\tvoid setupContent(not_null<Window::SessionController*> controller);\n\n\trpl::variable<int> _count;\n\n};\n\nQuickReplies::QuickReplies(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent(controller);\n}\n\nQuickReplies::~QuickReplies() = default;\n\nrpl::producer<QString> QuickReplies::title() {\n\treturn tr::lng_replies_title();\n}\n\nvoid QuickReplies::setupContent(\n\t\tnot_null<Window::SessionController*> controller) {\n\tusing namespace rpl::mappers;\n\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tAddDividerTextWithLottie(content, {\n\t\t.lottie = u\"writing\"_q,\n\t\t.lottieSize = st::settingsCloudPasswordIconSize,\n\t\t.lottieMargins = st::peerAppearanceIconPadding,\n\t\t.showFinished = showFinishes(),\n\t\t.about = tr::lng_replies_about(tr::marked),\n\t\t.aboutMargins = st::peerAppearanceCoverLabelMargin,\n\t});\n\tUi::AddSkip(content);\n\n\tconst auto addWrap = content->add(\n\t\tobject_ptr<Ui::VerticalLayout>(content));\n\n\tconst auto owner = &controller->session().data();\n\tconst auto messages = &owner->shortcutMessages();\n\n\trpl::combine(\n\t\t_count.value(),\n\t\tShortcutsLimitValue(&controller->session())\n\t) | rpl::on_next([=](int count, int limit) {\n\t\twhile (addWrap->count()) {\n\t\t\tdelete addWrap->widgetAt(0);\n\t\t}\n\t\tif (count < limit) {\n\t\t\tconst auto add = addWrap->add(object_ptr<Ui::SettingsButton>(\n\t\t\t\taddWrap,\n\t\t\t\ttr::lng_replies_add(),\n\t\t\t\tst::settingsButtonNoIcon\n\t\t\t));\n\n\t\t\tadd->setClickedCallback([=] {\n\t\t\t\tif (!controller->session().premium()) {\n\t\t\t\t\tShowPremiumPreviewToBuy(\n\t\t\t\t\t\tcontroller,\n\t\t\t\t\t\tPremiumFeature::QuickReplies);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto submit = [=](QString name, Fn<void()> close) {\n\t\t\t\t\tconst auto id = messages->emplaceShortcut(name);\n\t\t\t\t\tshowOther(ShortcutMessagesId(id));\n\t\t\t\t\tclose();\n\t\t\t\t};\n\t\t\t\tcontroller->show(Box(\n\t\t\t\t\tEditShortcutNameBox,\n\t\t\t\t\tQString(),\n\t\t\t\t\tcrl::guard(this, submit)));\n\t\t\t});\n\t\t\tif (count > 0) {\n\t\t\t\tAddSkip(addWrap);\n\t\t\t\tAddDivider(addWrap);\n\t\t\t\tAddSkip(addWrap);\n\t\t\t}\n\t\t}\n\t\tif (const auto width = content->width()) {\n\t\t\tcontent->resizeToWidth(width);\n\t\t}\n\t}, lifetime());\n\n\tconst auto inner = content->add(\n\t\tobject_ptr<Ui::VerticalLayout>(content));\n\trpl::single(rpl::empty) | rpl::then(\n\t\tmessages->shortcutsChanged()\n\t) | rpl::on_next([=] {\n\t\tauto old = inner->count();\n\n\t\tconst auto &shortcuts = messages->shortcuts();\n\t\tfor (const auto &[_, shortcut]\n\t\t\t: shortcuts.list | ranges::views::reverse) {\n\t\t\tif (!shortcut.count) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto name = shortcut.name;\n\t\t\tAddButtonWithLabel(\n\t\t\t\tinner,\n\t\t\t\trpl::single('/' + name),\n\t\t\t\ttr::lng_forum_messages(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(1. * shortcut.count)),\n\t\t\t\tst::settingsButtonNoIcon\n\t\t\t)->setClickedCallback([=] {\n\t\t\t\tconst auto id = messages->emplaceShortcut(name);\n\t\t\t\tshowOther(ShortcutMessagesId(id));\n\t\t\t});\n\t\t\tif (old) {\n\t\t\t\tdelete inner->widgetAt(0);\n\t\t\t\t--old;\n\t\t\t}\n\t\t}\n\t\twhile (old--) {\n\t\t\tdelete inner->widgetAt(0);\n\t\t}\n\t\t_count = inner->count();\n\t}, content->lifetime());\n\n\tUi::ResizeFitChild(this, content);\n}\n\n[[nodiscard]] bool ValidShortcutName(const QString &name) {\n\tif (name.isEmpty() || name.size() > kShortcutLimit) {\n\t\treturn false;\n\t}\n\tfor (const auto &ch : name) {\n\t\tif (!ch.isLetterOrNumber()\n\t\t\t&& (ch != QChar('_'))\n\t\t\t&& (ch.unicode() != 0x200c)\n\t\t\t&& (ch.unicode() != 0x00b7)\n\t\t\t&& (ch.unicode() < 0x0d80 || ch.unicode() > 0x0dff)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n} // namespace\n\nType QuickRepliesId() {\n\treturn QuickReplies::Id();\n}\n\nvoid EditShortcutNameBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tQString name,\n\t\tFn<void(QString, Fn<void()>)> submit) {\n\tname = name.trimmed();\n\tconst auto editing = !name.isEmpty();\n\tbox->setTitle(editing\n\t\t? tr::lng_replies_edit_title()\n\t\t: tr::lng_replies_add_title());\n\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\tbox,\n\t\t(editing\n\t\t\t? tr::lng_replies_edit_about()\n\t\t\t: tr::lng_replies_add_shortcut()),\n\t\tst::settingsAddReplyLabel));\n\tconst auto field = box->addRow(object_ptr<Ui::InputField>(\n\t\tbox,\n\t\tst::settingsAddReplyField,\n\t\ttr::lng_replies_add_placeholder(),\n\t\tname));\n\tbox->setFocusCallback([=] {\n\t\tfield->setFocusFast();\n\t});\n\tfield->selectAll();\n\tfield->setMaxLength(kShortcutLimit * 2);\n\n\tUi::AddLengthLimitLabel(field, kShortcutLimit);\n\n\tconst auto callback = [=] {\n\t\tconst auto name = field->getLastText().trimmed();\n\t\tif (!ValidShortcutName(name)) {\n\t\t\tfield->showError();\n\t\t} else {\n\t\t\tsubmit(name, [weak = base::make_weak(box)] {\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tstrong->closeBox();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\n\tfield->submits(\n\t) | rpl::on_next(callback, field->lifetime());\n\tbox->addButton(tr::lng_settings_save(), callback);\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_quick_replies.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nnamespace Settings {\n\n[[nodiscard]] Type QuickRepliesId();\n\nvoid EditShortcutNameBox(\n\tnot_null<Ui::GenericBox*> box,\n\tQString name,\n\tFn<void(QString, Fn<void()>)> submit);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/business/settings_recipients_helper.h\"\n\n#include \"boxes/filters/edit_filter_chats_list.h\"\n#include \"boxes/filters/edit_filter_chats_preview.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nconstexpr auto kAllExcept = 0;\nconstexpr auto kSelectedOnly = 1;\n\nusing Flag = Data::ChatFilter::Flag;\nusing Flags = Data::ChatFilter::Flags;\n\n[[nodiscard]] Flags TypesToFlags(Data::BusinessChatTypes types) {\n\tusing Type = Data::BusinessChatType;\n\treturn ((types & Type::Contacts) ? Flag::Contacts : Flag())\n\t\t| ((types & Type::NonContacts) ? Flag::NonContacts : Flag())\n\t\t| ((types & Type::NewChats) ? Flag::NewChats : Flag())\n\t\t| ((types & Type::ExistingChats) ? Flag::ExistingChats : Flag());\n}\n\n[[nodiscard]] Data::BusinessChatTypes FlagsToTypes(Flags flags) {\n\tusing Type = Data::BusinessChatType;\n\treturn ((flags & Flag::Contacts) ? Type::Contacts : Type())\n\t\t| ((flags & Flag::NonContacts) ? Type::NonContacts : Type())\n\t\t| ((flags & Flag::NewChats) ? Type::NewChats : Type())\n\t\t| ((flags & Flag::ExistingChats) ? Type::ExistingChats : Type());\n}\n\n} // namespace\n\nvoid EditBusinessChats(\n\t\tnot_null<Window::SessionController*> window,\n\t\tBusinessChatsDescriptor &&descriptor) {\n\tconst auto session = &window->session();\n\tconst auto options = Flag::ExistingChats\n\t\t| Flag::NewChats\n\t\t| Flag::Contacts\n\t\t| Flag::NonContacts;\n\tauto &&peers = descriptor.current.list | ranges::views::transform([=](\n\t\t\tnot_null<UserData*> user) {\n\t\treturn user->owner().history(user);\n\t});\n\tauto controller = std::make_unique<EditFilterChatsListController>(\n\t\tsession,\n\t\t(descriptor.include\n\t\t\t? tr::lng_filters_include_title()\n\t\t\t: tr::lng_filters_exclude_title()),\n\t\t(descriptor.usersOnly ? Flag() : options),\n\t\tTypesToFlags(descriptor.current.types) & options,\n\t\tbase::flat_set<not_null<History*>>(begin(peers), end(peers)),\n\t\t100,\n\t\tnullptr);\n\tconst auto rawController = controller.get();\n\tconst auto save = descriptor.save;\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tbox->setCloseByOutsideClick(false);\n\t\tbox->addButton(tr::lng_settings_save(), crl::guard(box, [=] {\n\t\t\tconst auto peers = box->collectSelectedRows();\n\t\t\tauto &&users = ranges::views::all(\n\t\t\t\tpeers\n\t\t\t) | ranges::views::transform([=](not_null<PeerData*> peer) {\n\t\t\t\treturn not_null(peer->asUser());\n\t\t\t}) | ranges::to_vector;\n\t\t\tsave(Data::BusinessChats{\n\t\t\t\t.types = FlagsToTypes(rawController->chosenOptions()),\n\t\t\t\t.list = std::move(users),\n\t\t\t});\n\t\t\tbox->closeBox();\n\t\t}));\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t};\n\twindow->show(\n\t\tBox<PeerListBox>(std::move(controller), std::move(initBox)));\n}\n\nnot_null<FilterChatsPreview*> SetupBusinessChatsPreview(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<rpl::variable<Data::BusinessChats>*> data) {\n\tconst auto rules = data->current();\n\n\tconst auto locked = std::make_shared<bool>();\n\tauto &&peers = data->current().list | ranges::views::transform([=](\n\t\t\tnot_null<UserData*> user) {\n\t\treturn user->owner().history(user);\n\t});\n\tconst auto preview = container->add(object_ptr<FilterChatsPreview>(\n\t\tcontainer,\n\t\tTypesToFlags(data->current().types),\n\t\tbase::flat_set<not_null<History*>>(begin(peers), end(peers))));\n\n\tpreview->flagRemoved(\n\t) | rpl::on_next([=](Flag flag) {\n\t\t*locked = true;\n\t\t*data = Data::BusinessChats{\n\t\t\tdata->current().types & ~FlagsToTypes(flag),\n\t\t\tdata->current().list\n\t\t};\n\t\t*locked = false;\n\t}, preview->lifetime());\n\n\tpreview->peerRemoved(\n\t) | rpl::on_next([=](not_null<History*> history) {\n\t\tauto list = data->current().list;\n\t\tlist.erase(\n\t\t\tranges::remove(list, not_null(history->peer->asUser())),\n\t\t\tend(list));\n\n\t\t*locked = true;\n\t\t*data = Data::BusinessChats{\n\t\t\tdata->current().types,\n\t\t\tstd::move(list)\n\t\t};\n\t\t*locked = false;\n\t}, preview->lifetime());\n\n\tdata->changes(\n\t) | rpl::filter([=] {\n\t\treturn !*locked;\n\t}) | rpl::on_next([=](const Data::BusinessChats &rules) {\n\t\tauto &&peers = rules.list | ranges::views::transform([=](\n\t\t\t\tnot_null<UserData*> user) {\n\t\t\treturn user->owner().history(user);\n\t\t});\n\t\tpreview->updateData(\n\t\t\tTypesToFlags(rules.types),\n\t\t\tbase::flat_set<not_null<History*>>(begin(peers), end(peers)));\n\t}, preview->lifetime());\n\n\treturn preview;\n}\n\nvoid AddBusinessRecipientsSelector(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tBusinessRecipientsSelectorDescriptor &&descriptor) {\n\tUi::AddSkip(container);\n\tUi::AddSubsectionTitle(container, std::move(descriptor.title));\n\n\tauto &lifetime = container->lifetime();\n\tconst auto controller = descriptor.controller;\n\tconst auto data = descriptor.data;\n\tconst auto includeWithExcluded = (descriptor.type\n\t\t== Data::BusinessRecipientsType::Bots);\n\tconst auto change = [=](Fn<void(Data::BusinessRecipients&)> modify) {\n\t\tauto now = data->current();\n\t\tmodify(now);\n\t\t*data = std::move(now);\n\t};\n\tconst auto &current = data->current();\n\tconst auto all = current.allButExcluded || current.included.empty();\n\tconst auto group = std::make_shared<Ui::RadiobuttonGroup>(\n\t\tall ? kAllExcept : kSelectedOnly);\n\tcontainer->add(\n\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\tcontainer,\n\t\t\tgroup,\n\t\t\tkAllExcept,\n\t\t\ttr::lng_chatbots_all_except(tr::now),\n\t\t\tst::settingsChatbotsAccess),\n\t\tst::settingsChatbotsAccessMargins);\n\tcontainer->add(\n\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\tcontainer,\n\t\t\tgroup,\n\t\t\tkSelectedOnly,\n\t\t\ttr::lng_chatbots_selected(tr::now),\n\t\t\tst::settingsChatbotsAccess),\n\t\tst::settingsChatbotsAccessMargins);\n\n\tUi::AddSkip(container, st::settingsChatbotsAccessSkip);\n\tUi::AddDivider(container);\n\n\tconst auto includeWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container))\n\t)->setDuration(0);\n\tconst auto excludeWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container))\n\t)->setDuration(0);\n\n\tconst auto excludeInner = excludeWrap->entity();\n\n\tUi::AddSkip(excludeInner);\n\tUi::AddSubsectionTitle(excludeInner, tr::lng_chatbots_excluded_title());\n\tconst auto excludeAdd = AddButtonWithIcon(\n\t\texcludeInner,\n\t\ttr::lng_chatbots_exclude_button(),\n\t\tst::settingsChatbotsAdd,\n\t\t{ &st::settingsIconRemove, IconType::Round, &st::windowBgActive });\n\tconst auto addExcluded = [=] {\n\t\tconst auto save = [=](Data::BusinessChats value) {\n\t\t\tchange([&](Data::BusinessRecipients &data) {\n\t\t\t\tif (includeWithExcluded) {\n\t\t\t\t\tif (!data.allButExcluded) {\n\t\t\t\t\t\tvalue.types = {};\n\t\t\t\t\t}\n\t\t\t\t\tfor (const auto &user : value.list) {\n\t\t\t\t\t\tdata.included.list.erase(\n\t\t\t\t\t\t\tranges::remove(data.included.list, user),\n\t\t\t\t\t\t\tend(data.included.list));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!value.empty()) {\n\t\t\t\t\tdata.included = {};\n\t\t\t\t}\n\t\t\t\tdata.excluded = std::move(value);\n\t\t\t});\n\t\t};\n\t\tEditBusinessChats(controller, {\n\t\t\t.current = data->current().excluded,\n\t\t\t.save = crl::guard(excludeAdd, save),\n\t\t\t.usersOnly = (includeWithExcluded\n\t\t\t\t&& !data->current().allButExcluded),\n\t\t\t.include = false,\n\t\t});\n\t};\n\texcludeAdd->setClickedCallback(addExcluded);\n\n\tconst auto excluded = lifetime.make_state<\n\t\trpl::variable<Data::BusinessChats>\n\t>(data->current().excluded);\n\tdata->changes(\n\t) | rpl::on_next([=](const Data::BusinessRecipients &value) {\n\t\t*excluded = value.excluded;\n\t}, lifetime);\n\texcluded->changes(\n\t) | rpl::on_next([=](Data::BusinessChats &&value) {\n\t\tchange([&](Data::BusinessRecipients &data) {\n\t\t\tdata.excluded = std::move(value);\n\t\t});\n\t}, lifetime);\n\n\tSetupBusinessChatsPreview(excludeInner, excluded);\n\n\texcludeWrap->toggleOn(data->value(\n\t) | rpl::map([=](const Data::BusinessRecipients &value) {\n\t\treturn value.allButExcluded || includeWithExcluded;\n\t}));\n\texcludeWrap->finishAnimating();\n\n\tconst auto includeInner = includeWrap->entity();\n\n\tUi::AddSkip(includeInner);\n\tUi::AddSubsectionTitle(includeInner, tr::lng_chatbots_included_title());\n\tconst auto includeAdd = AddButtonWithIcon(\n\t\tincludeInner,\n\t\ttr::lng_chatbots_include_button(),\n\t\tst::settingsChatbotsAdd,\n\t\t{ &st::settingsIconAdd, IconType::Round, &st::windowBgActive });\n\tconst auto addIncluded = [=] {\n\t\tconst auto save = [=](Data::BusinessChats value) {\n\t\t\tchange([&](Data::BusinessRecipients &data) {\n\t\t\t\tif (includeWithExcluded) {\n\t\t\t\t\tfor (const auto &user : value.list) {\n\t\t\t\t\t\tdata.excluded.list.erase(\n\t\t\t\t\t\t\tranges::remove(data.excluded.list, user),\n\t\t\t\t\t\t\tend(data.excluded.list));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!value.empty()) {\n\t\t\t\t\tdata.excluded.types = {};\n\t\t\t\t}\n\t\t\t\tdata.included = std::move(value);\n\t\t\t});\n\t\t\tif (!data->current().included.empty()) {\n\t\t\t\tgroup->setValue(kSelectedOnly);\n\t\t\t}\n\t\t};\n\t\tEditBusinessChats(controller, {\n\t\t\t.current = data->current().included,\n\t\t\t.save = crl::guard(includeAdd, save),\n\t\t\t.include = true,\n\t\t});\n\t};\n\tincludeAdd->setClickedCallback(addIncluded);\n\n\tconst auto included = lifetime.make_state<\n\t\trpl::variable<Data::BusinessChats>\n\t>(data->current().included);\n\tdata->changes(\n\t) | rpl::on_next([=](const Data::BusinessRecipients &value) {\n\t\t*included = value.included;\n\t}, lifetime);\n\tincluded->changes(\n\t) | rpl::on_next([=](Data::BusinessChats &&value) {\n\t\tchange([&](Data::BusinessRecipients &data) {\n\t\t\tdata.included = std::move(value);\n\t\t});\n\t}, lifetime);\n\n\tSetupBusinessChatsPreview(includeInner, included);\n\tincluded->value(\n\t) | rpl::on_next([=](const Data::BusinessChats &value) {\n\t\tif (value.empty() && group->current() == kSelectedOnly) {\n\t\t\tgroup->setValue(kAllExcept);\n\t\t}\n\t}, lifetime);\n\n\tincludeWrap->toggleOn(data->value(\n\t) | rpl::map([](const Data::BusinessRecipients &value) {\n\t\treturn !value.allButExcluded;\n\t}));\n\tincludeWrap->finishAnimating();\n\n\tgroup->setChangedCallback([=](int value) {\n\t\tif (value == kSelectedOnly && data->current().included.empty()) {\n\t\t\tgroup->setValue(kAllExcept);\n\t\t\taddIncluded();\n\t\t\treturn;\n\t\t}\n\t\tchange([&](Data::BusinessRecipients &data) {\n\t\t\tdata.allButExcluded = (value == kAllExcept);\n\t\t});\n\t});\n}\n\nint ShortcutsCount(not_null<Main::Session*> session) {\n\tconst auto &shortcuts = session->data().shortcutMessages().shortcuts();\n\tauto result = 0;\n\tfor (const auto &[_, shortcut] : shortcuts.list) {\n\t\tif (shortcut.count > 0) {\n\t\t\t++result;\n\t\t}\n\t}\n\treturn result;\n}\n\nrpl::producer<int> ShortcutsCountValue(not_null<Main::Session*> session) {\n\tconst auto messages = &session->data().shortcutMessages();\n\treturn rpl::single(rpl::empty) | rpl::then(\n\t\tmessages->shortcutsChanged()\n\t) | rpl::map([=] {\n\t\treturn ShortcutsCount(session);\n\t});\n}\n\nint ShortcutMessagesCount(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &name) {\n\tconst auto &shortcuts = session->data().shortcutMessages().shortcuts();\n\tfor (const auto &[_, shortcut] : shortcuts.list) {\n\t\tif (shortcut.name == name) {\n\t\t\treturn shortcut.count;\n\t\t}\n\t}\n\treturn 0;\n}\n\nrpl::producer<int> ShortcutMessagesCountValue(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &name) {\n\tconst auto messages = &session->data().shortcutMessages();\n\treturn rpl::single(rpl::empty) | rpl::then(\n\t\tmessages->shortcutsChanged()\n\t) | rpl::map([=] {\n\t\treturn ShortcutMessagesCount(session, name);\n\t});\n}\n\nbool ShortcutExists(not_null<Main::Session*> session, const QString &name) {\n\treturn ShortcutMessagesCount(session, name) > 0;\n}\n\nrpl::producer<bool> ShortcutExistsValue(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &name) {\n\treturn ShortcutMessagesCountValue(session, name)\n\t\t| rpl::map(rpl::mappers::_1 > 0);\n}\n\nint ShortcutsLimit(not_null<Main::Session*> session) {\n\tconst auto appConfig = &session->appConfig();\n\treturn appConfig->get<int>(\"quick_replies_limit\", 100);\n}\n\nrpl::producer<int> ShortcutsLimitValue(not_null<Main::Session*> session) {\n\tconst auto appConfig = &session->appConfig();\n\treturn appConfig->value() | rpl::map([=] {\n\t\treturn ShortcutsLimit(session);\n\t});\n}\n\nint ShortcutMessagesLimit(not_null<Main::Session*> session) {\n\tconst auto appConfig = &session->appConfig();\n\treturn appConfig->get<int>(\"quick_reply_messages_limit\", 20);\n}\n\nrpl::producer<int> ShortcutMessagesLimitValue(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto appConfig = &session->appConfig();\n\treturn appConfig->value() | rpl::map([=] {\n\t\treturn ShortcutMessagesLimit(session);\n\t});\n}\n\nBusinessShortcutId LookupShortcutId(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &name) {\n\tconst auto messages = &session->data().shortcutMessages();\n\tfor (const auto &[id, shortcut] : messages->shortcuts().list) {\n\t\tif (shortcut.name == name) {\n\t\t\treturn id;\n\t\t}\n\t}\n\treturn {};\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_recipients_helper.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/required.h\"\n#include \"data/business/data_business_common.h\"\n#include \"settings/settings_common_session.h\"\n\nclass FilterChatsPreview;\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Settings {\n\nstruct BusinessChatsDescriptor {\n\tData::BusinessChats current;\n\tFn<void(const Data::BusinessChats&)> save;\n\tbool usersOnly = false;\n\tbool include = false;\n};\nvoid EditBusinessChats(\n\tnot_null<Window::SessionController*> window,\n\tBusinessChatsDescriptor &&descriptor);\n\nnot_null<FilterChatsPreview*> SetupBusinessChatsPreview(\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<rpl::variable<Data::BusinessChats>*> data);\n\nstruct BusinessRecipientsSelectorDescriptor {\n\tnot_null<Window::SessionController*> controller;\n\trpl::producer<QString> title;\n\tnot_null<rpl::variable<Data::BusinessRecipients>*> data;\n\tbase::required<Data::BusinessRecipientsType> type;\n};\nvoid AddBusinessRecipientsSelector(\n\tnot_null<Ui::VerticalLayout*> container,\n\tBusinessRecipientsSelectorDescriptor &&descriptor);\n\n[[nodiscard]] int ShortcutsCount(not_null<::Main::Session*> session);\n[[nodiscard]] rpl::producer<int> ShortcutsCountValue(\n\tnot_null<::Main::Session*> session);\n[[nodiscard]] int ShortcutMessagesCount(\n\tnot_null<::Main::Session*> session,\n\tconst QString &name);\n[[nodiscard]] rpl::producer<int> ShortcutMessagesCountValue(\n\tnot_null<::Main::Session*> session,\n\tconst QString &name);\n[[nodiscard]] bool ShortcutExists(\n\tnot_null<::Main::Session*> session,\n\tconst QString &name);\n[[nodiscard]] rpl::producer<bool> ShortcutExistsValue(\n\tnot_null<::Main::Session*> session,\n\tconst QString &name);\n[[nodiscard]] int ShortcutsLimit(not_null<::Main::Session*> session);\n[[nodiscard]] rpl::producer<int> ShortcutsLimitValue(\n\tnot_null<::Main::Session*> session);\n[[nodiscard]] int ShortcutMessagesLimit(not_null<::Main::Session*> session);\n[[nodiscard]] rpl::producer<int> ShortcutMessagesLimitValue(\n\tnot_null<::Main::Session*> session);\n\n[[nodiscard]] BusinessShortcutId LookupShortcutId(\n\tnot_null<::Main::Session*> session,\n\tconst QString &name);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/business/settings_shortcut_messages.h\"\n\n#include \"api/api_editing.h\"\n#include \"api/api_sending.h\"\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"boxes/delete_messages_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"boxes/send_files_box.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"core/file_utilities.h\"\n#include \"core/mime_type.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"data/data_message_reaction_id.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"history/view/controls/compose_controls_common.h\"\n#include \"history/view/controls/history_view_compose_controls.h\"\n#include \"history/view/history_view_corner_buttons.h\"\n#include \"history/view/history_view_empty_list_bubble.h\"\n#include \"history/view/history_view_list_widget.h\"\n#include \"history/view/history_view_service_message.h\"\n#include \"history/view/history_view_sticker_toast.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"info/info_wrap_widget.h\"\n#include \"inline_bots/inline_bot_result.h\"\n#include \"lang/lang_keys.h\"\n#include \"lang/lang_numbers_animation.h\"\n#include \"main/main_account.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_send.h\"\n#include \"settings/business/settings_quick_replies.h\"\n#include \"settings/business/settings_recipients_helper.h\"\n#include \"storage/localimageloader.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"storage/storage_shared_media.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/attach/attach_send_files_way.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/controls/jump_down_button.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/painter.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Settings {\nnamespace {\n\nusing namespace HistoryView;\n\nclass ShortcutMessages\n\t: public AbstractSection\n\t, private WindowListDelegate\n\t, private CornerButtonsDelegate {\npublic:\n\tShortcutMessages(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\trpl::producer<Container> containerValue,\n\t\tBusinessShortcutId shortcutId);\n\t~ShortcutMessages();\n\n\t[[nodiscard]] static Type Id(BusinessShortcutId shortcutId);\n\n\t[[nodiscard]] Type id() const final override {\n\t\treturn Id(_shortcutId.current());\n\t}\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\t[[nodiscard]] rpl::producer<> sectionShowBack() override;\n\tvoid setInnerFocus() override;\n\n\trpl::producer<Info::SelectedItems> selectedListValue() override;\n\tvoid selectionAction(Info::SelectionAction action) override;\n\tvoid fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override;\n\n\tbool paintOuter(\n\t\tnot_null<QWidget*> outer,\n\t\tint maxVisibleHeight,\n\t\tQRect clip) override;\n\nprivate:\n\tvoid outerResized();\n\tvoid updateComposeControlsPosition();\n\n\t// ListDelegate interface.\n\tContext listContext() override;\n\tbool listScrollTo(int top, bool syntetic = true) override;\n\tvoid listCancelRequest() override;\n\tvoid listDeleteRequest() override;\n\tvoid listTryProcessKeyInput(not_null<QKeyEvent*> e) override;\n\trpl::producer<Data::MessagesSlice> listSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) override;\n\tbool listAllowsMultiSelect() override;\n\tbool listIsItemGoodForSelection(not_null<HistoryItem*> item) override;\n\tbool listIsLessInOrder(\n\t\tnot_null<HistoryItem*> first,\n\t\tnot_null<HistoryItem*> second) override;\n\tvoid listSelectionChanged(SelectedItems &&items) override;\n\tvoid listMarkReadTill(not_null<HistoryItem*> item) override;\n\tvoid listMarkContentsRead(\n\t\tconst base::flat_set<not_null<HistoryItem*>> &items) override;\n\tMessagesBarData listMessagesBar(\n\t\tconst std::vector<not_null<Element*>> &elements,\n\t\tbool markLastAsRead) override;\n\tvoid listContentRefreshed() override;\n\tvoid listUpdateDateLink(\n\t\tClickHandlerPtr &link,\n\t\tnot_null<Element*> view) override;\n\tbool listElementHideReply(not_null<const Element*> view) override;\n\tbool listElementShownUnread(not_null<const Element*> view) override;\n\tbool listIsGoodForAroundPosition(\n\t\tnot_null<const Element *> view) override;\n\tvoid listSendBotCommand(\n\t\tconst QString &command,\n\t\tconst FullMsgId &context) override;\n\tvoid listSearch(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) override;\n\tvoid listHandleViaClick(not_null<UserData*> bot) override;\n\tnot_null<Ui::ChatTheme*> listChatTheme() override;\n\tCopyRestrictionType listCopyRestrictionType(HistoryItem *item) override;\n\tCopyRestrictionType listCopyMediaRestrictionType(\n\t\tnot_null<HistoryItem*> item) override;\n\tCopyRestrictionType listSelectRestrictionType() override;\n\tauto listAllowedReactionsValue()\n\t\t-> rpl::producer<Data::AllowedReactions> override;\n\tvoid listShowPremiumToast(not_null<DocumentData*> document) override;\n\tvoid listOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) override;\n\tvoid listOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView) override;\n\tvoid listPaintEmpty(\n\t\tPainter &p,\n\t\tconst Ui::ChatPaintContext &context) override;\n\tQString listElementAuthorRank(not_null<const Element*> view) override;\n\tbool listElementHideTopicButton(not_null<const Element*> view) override;\n\tHistory *listTranslateHistory() override;\n\tvoid listAddTranslatedItems(\n\t\tnot_null<TranslateTracker*> tracker) override;\n\tbool listIgnorePaintEvent(QWidget *w, QPaintEvent *e) override;\n\n\t// CornerButtonsDelegate delegate.\n\tvoid cornerButtonsShowAtPosition(\n\t\tData::MessagePosition position) override;\n\tData::Thread *cornerButtonsThread() override;\n\tFullMsgId cornerButtonsCurrentId() override;\n\tbool cornerButtonsIgnoreVisibility() override;\n\tstd::optional<bool> cornerButtonsDownShown() override;\n\tbool cornerButtonsUnreadMayBeShown() override;\n\tbool cornerButtonsHas(CornerButtonType type) override;\n\n\tbase::weak_qptr<Ui::RpWidget> createPinnedToBottom(\n\t\tnot_null<Ui::RpWidget*> parent) override;\n\tvoid setupComposeControls();\n\tvoid processScroll();\n\tvoid updateInnerVisibleArea();\n\n\tvoid checkReplyReturns();\n\tvoid confirmDeleteSelected();\n\tvoid clearSelected();\n\n\tvoid uploadFile(const QByteArray &fileContent, SendMediaType type);\n\tbool confirmSendingFiles(\n\t\tQImage &&image,\n\t\tQByteArray &&content,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos = std::nullopt,\n\t\tconst QString &insertTextOnCancel = QString());\n\tbool confirmSendingFiles(\n\t\tUi::PreparedList &&list,\n\t\tconst QString &insertTextOnCancel = QString());\n\tbool confirmSendingFiles(\n\t\tnot_null<const QMimeData*> data,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos,\n\t\tconst QString &insertTextOnCancel = QString());\n\tbool showSendingFilesError(const Ui::PreparedList &list) const;\n\tbool showSendingFilesError(const Ui::PreparedBundle &bundle) const;\n\n\tvoid sendingFilesConfirmed(\n\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\tApi::SendOptions options);\n\n\tbool sendExistingDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tApi::SendOptions options,\n\t\tstd::optional<MsgId> localId);\n\tvoid sendExistingPhoto(not_null<PhotoData*> photo);\n\tbool sendExistingPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tApi::SendOptions options);\n\tvoid sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot);\n\tvoid sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot,\n\t\tApi::SendOptions options,\n\t\tstd::optional<MsgId> localMessageId);\n\n\t[[nodiscard]] Api::SendAction prepareSendAction(\n\t\tApi::SendOptions options) const;\n\tvoid send();\n\tvoid send(Api::SendOptions options);\n\tvoid sendVoice(Controls::VoiceToSend &&data);\n\tvoid edit(\n\t\tnot_null<HistoryItem*> item,\n\t\tApi::SendOptions options,\n\t\tmtpRequestId *const saveEditMsgRequestId,\n\t\tbool spoilered);\n\tvoid chooseAttach(std::optional<bool> overrideSendImagesAsPhotos);\n\t[[nodiscard]] FullReplyTo replyTo() const;\n\tvoid doSetInnerFocus();\n\tvoid showAtPosition(\n\t\tData::MessagePosition position,\n\t\tFullMsgId originItemId = {});\n\tvoid showAtPosition(\n\t\tData::MessagePosition position,\n\t\tFullMsgId originItemId,\n\t\tconst Window::SectionShow &params);\n\tvoid showAtEnd();\n\tvoid finishSending();\n\tvoid refreshEmptyText();\n\tbool showPremiumRequired() const;\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<Main::Session*> _session;\n\tconst not_null<Ui::ScrollArea*> _scroll;\n\tconst not_null<History*> _history;\n\trpl::variable<BusinessShortcutId> _shortcutId;\n\trpl::variable<QString> _shortcut;\n\trpl::variable<Container> _container;\n\trpl::variable<int> _count;\n\tstd::shared_ptr<Ui::ChatStyle> _style;\n\tstd::shared_ptr<Ui::ChatTheme> _theme;\n\tQPointer<ListWidget> _inner;\n\tstd::unique_ptr<Ui::RpWidget> _controlsWrap;\n\tstd::unique_ptr<ComposeControls> _composeControls;\n\trpl::event_stream<> _showBackRequests;\n\tbool _skipScrollEvent = false;\n\n\tQSize _inOuterResize;\n\tQSize _pendingOuterResize;\n\n\tconst style::icon *_emptyIcon = nullptr;\n\tUi::Text::String _emptyText;\n\tint _emptyTextWidth = 0;\n\tint _emptyTextHeight = 0;\n\n\trpl::variable<Info::SelectedItems> _selectedItems\n\t\t= Info::SelectedItems(Storage::SharedMediaType::kCount);\n\n\tstd::unique_ptr<StickerToast> _stickerToast;\n\n\tFullMsgId _lastShownAt;\n\tCornerButtons _cornerButtons;\n\n\tData::MessagesSlice _lastSlice;\n\tbool _choosingAttach = false;\n\n};\n\nstruct Factory final : AbstractSectionFactory {\n\texplicit Factory(BusinessShortcutId shortcutId)\n\t: shortcutId(shortcutId) {\n\t}\n\n\tobject_ptr<AbstractSection> create(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\trpl::producer<Container> containerValue\n\t) const final override {\n\t\treturn object_ptr<ShortcutMessages>(\n\t\t\tparent,\n\t\t\tcontroller,\n\t\t\tscroll,\n\t\t\tstd::move(containerValue),\n\t\t\tshortcutId);\n\t}\n\n\tconst BusinessShortcutId shortcutId = {};\n};\n\n[[nodiscard]] bool IsAway(const QString &shortcut) {\n\treturn (shortcut == u\"away\"_q);\n}\n\n[[nodiscard]] bool IsGreeting(const QString &shortcut) {\n\treturn (shortcut == u\"hello\"_q);\n}\n\nShortcutMessages::ShortcutMessages(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::ScrollArea*> scroll,\n\trpl::producer<Container> containerValue,\n\tBusinessShortcutId shortcutId)\n: AbstractSection(parent, controller)\n, WindowListDelegate(controller)\n, _controller(controller)\n, _session(&controller->session())\n, _scroll(scroll)\n, _history(_session->data().history(_session->user()->id))\n, _shortcutId(shortcutId)\n, _shortcut(\n\t_session->data().shortcutMessages().lookupShortcut(shortcutId).name)\n, _container(std::move(containerValue))\n, _cornerButtons(\n\t\t_scroll,\n\t\tcontroller->chatStyle(),\n\t\tstatic_cast<HistoryView::CornerButtonsDelegate*>(this)) {\n\tconst auto messages = &_session->data().shortcutMessages();\n\n\tmessages->shortcutIdChanged(\n\t) | rpl::on_next([=](Data::ShortcutIdChange change) {\n\t\tif (change.oldId == _shortcutId.current()) {\n\t\t\tif (change.newId) {\n\t\t\t\t_shortcutId = change.newId;\n\t\t\t} else {\n\t\t\t\t_showBackRequests.fire({});\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\tmessages->shortcutsChanged(\n\t) | rpl::on_next([=] {\n\t\t_shortcut = messages->lookupShortcut(_shortcutId.current()).name;\n\t}, lifetime());\n\n\tcontroller->chatStyle()->paletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_scroll->updateBars();\n\t}, _scroll->lifetime());\n\n\t_style = std::make_shared<Ui::ChatStyle>(_session->colorIndicesValue());\n\t_theme = std::shared_ptr<Ui::ChatTheme>(\n\t\tWindow::Theme::DefaultChatThemeOn(lifetime()));\n\n\t_inner = Ui::CreateChild<ListWidget>(\n\t\tthis,\n\t\t&controller->session(),\n\t\tstatic_cast<ListDelegate*>(this));\n\t_inner->overrideChatMode(ElementChatMode::Default);\n\n\t_scroll->sizeValue() | rpl::filter([](QSize size) {\n\t\treturn !size.isEmpty();\n\t}) | rpl::on_next([=] {\n\t\touterResized();\n\t}, lifetime());\n\n\t_scroll->scrolls(\n\t) | rpl::on_next([=] {\n\t\tprocessScroll();\n\t}, lifetime());\n\n\t_shortcut.value() | rpl::on_next([=] {\n\t\trefreshEmptyText();\n\t\t_inner->update();\n\t}, lifetime());\n\n\t_inner->editMessageRequested(\n\t) | rpl::on_next([=](auto fullId) {\n\t\tif (const auto item = _session->data().message(fullId)) {\n\t\t\tconst auto media = item->media();\n\t\t\tif (!media || media->webpage() || media->allowsEditCaption()) {\n\t\t\t\t_composeControls->editMessage(\n\t\t\t\t\tfullId,\n\t\t\t\t\t_inner->getSelectedTextRange(item));\n\t\t\t} else if (media->todolist()) {\n\t\t\t\tWindow::PeerMenuEditTodoList(_controller, item);\n\t\t\t}\n\t\t}\n\t}, _inner->lifetime());\n\n\t_inner->heightValue() | rpl::on_next([=](int height) {\n\t\tresize(width(), height);\n\t}, lifetime());\n}\n\nShortcutMessages::~ShortcutMessages() = default;\n\nvoid ShortcutMessages::refreshEmptyText() {\n\tconst auto &shortcut = _shortcut.current();\n\tconst auto away = IsAway(shortcut);\n\tconst auto greeting = !away && IsGreeting(shortcut);\n\tauto text = away\n\t\t? tr::lng_away_empty_title(\n\t\t\ttr::now,\n\t\t\ttr::bold\n\t\t).append(\"\\n\\n\").append(tr::lng_away_empty_about(tr::now))\n\t\t: greeting\n\t\t? tr::lng_greeting_empty_title(\n\t\t\ttr::now,\n\t\t\ttr::bold\n\t\t).append(\"\\n\\n\").append(tr::lng_greeting_empty_about(tr::now))\n\t\t: tr::lng_replies_empty_title(\n\t\t\ttr::now,\n\t\t\ttr::bold\n\t\t).append(\"\\n\\n\").append(tr::lng_replies_empty_about(\n\t\t\ttr::now,\n\t\t\tlt_shortcut,\n\t\t\ttr::bold('/' + shortcut),\n\t\t\ttr::marked));\n\t_emptyIcon = away\n\t\t? &st::awayEmptyIcon\n\t\t: greeting\n\t\t? &st::greetingEmptyIcon\n\t\t: &st::repliesEmptyIcon;\n\tconst auto padding = st::repliesEmptyPadding;\n\tconst auto minWidth = st::repliesEmptyWidth / 4;\n\tconst auto maxWidth = std::max(\n\t\tminWidth + 1,\n\t\tst::repliesEmptyWidth - padding.left() - padding.right());\n\t_emptyText = Ui::Text::String(\n\t\tst::messageTextStyle,\n\t\ttext,\n\t\tkMarkupTextOptions,\n\t\tminWidth);\n\tconst auto countHeight = [&](int width) {\n\t\treturn _emptyText.countHeight(width);\n\t};\n\t_emptyTextWidth = Ui::FindNiceTooltipWidth(\n\t\tminWidth,\n\t\tmaxWidth,\n\t\tcountHeight);\n\t_emptyTextHeight = countHeight(_emptyTextWidth);\n}\n\nType ShortcutMessages::Id(BusinessShortcutId shortcutId) {\n\treturn std::make_shared<Factory>(shortcutId);\n}\n\nrpl::producer<QString> ShortcutMessages::title() {\n\treturn _shortcut.value() | rpl::map([=](const QString &shortcut) {\n\t\treturn IsAway(shortcut)\n\t\t\t? tr::lng_away_title()\n\t\t\t: IsGreeting(shortcut)\n\t\t\t? tr::lng_greeting_title()\n\t\t\t: rpl::single('/' + shortcut);\n\t}) | rpl::flatten_latest();\n}\n\nvoid ShortcutMessages::processScroll() {\n\tif (_skipScrollEvent) {\n\t\treturn;\n\t}\n\tupdateInnerVisibleArea();\n}\n\nvoid ShortcutMessages::updateInnerVisibleArea() {\n\tif (!_inner->animatedScrolling()) {\n\t\tcheckReplyReturns();\n\t}\n\tconst auto scrollTop = _scroll->scrollTop();\n\t_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());\n\t_cornerButtons.updateJumpDownVisibility();\n\t_cornerButtons.updateUnreadThingsVisibility();\n}\n\nrpl::producer<> ShortcutMessages::sectionShowBack() {\n\treturn _showBackRequests.events();\n}\n\nvoid ShortcutMessages::setInnerFocus() {\n\t_composeControls->focus();\n}\n\nrpl::producer<Info::SelectedItems> ShortcutMessages::selectedListValue() {\n\treturn _selectedItems.value();\n}\n\nvoid ShortcutMessages::selectionAction(Info::SelectionAction action) {\n\tswitch (action) {\n\tcase Info::SelectionAction::Clear: clearSelected(); return;\n\tcase Info::SelectionAction::Delete: confirmDeleteSelected(); return;\n\t}\n\tUnexpected(\"Action in ShortcutMessages::selectionAction.\");\n}\n\nvoid ShortcutMessages::fillTopBarMenu(\n\t\tconst Ui::Menu::MenuCallback &addAction) {\n\tconst auto owner = &_controller->session().data();\n\tconst auto messages = &owner->shortcutMessages();\n\n\taddAction(tr::lng_context_edit_shortcut(tr::now), [=] {\n\t\tif (!_controller->session().premium()) {\n\t\t\tShowPremiumPreviewToBuy(\n\t\t\t\t_controller,\n\t\t\t\tPremiumFeature::QuickReplies);\n\t\t\treturn;\n\t\t}\n\t\tconst auto submit = [=](QString name, Fn<void()> close) {\n\t\t\tconst auto id = _shortcutId.current();\n\t\t\tconst auto error = [=](QString text) {\n\t\t\t\tif (!text.isEmpty()) {\n\t\t\t\t\t_controller->showToast((text == u\"SHORTCUT_OCCUPIED\"_q)\n\t\t\t\t\t\t? tr::lng_replies_error_occupied(tr::now)\n\t\t\t\t\t\t: text);\n\t\t\t\t}\n\t\t\t};\n\t\t\tmessages->editShortcut(id, name, close, crl::guard(this, error));\n\t\t};\n\t\tconst auto name = _shortcut.current();\n\t\t_controller->show(\n\t\t\tBox(EditShortcutNameBox, name, crl::guard(this, submit)));\n\t}, &st::menuIconEdit);\n\n\tconst auto justDelete = crl::guard(this, [=] {\n\t\tmessages->removeShortcut(_shortcutId.current());\n\t});\n\tconst auto confirmDeleteShortcut = [=] {\n\t\tconst auto slice = messages->list(_shortcutId.current());\n\t\tif (slice.fullCount == 0) {\n\t\t\tjustDelete();\n\t\t} else {\n\t\t\tconst auto confirmed = [=](Fn<void()> close) {\n\t\t\t\tjustDelete();\n\t\t\t\tclose();\n\t\t\t};\n\t\t\t_controller->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = { tr::lng_replies_delete_sure() },\n\t\t\t\t.confirmed = confirmed,\n\t\t\t\t.confirmText = tr::lng_box_delete(),\n\t\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t}));\n\t\t}\n\t};\n\taddAction({\n\t\t.text = tr::lng_context_delete_shortcut(tr::now),\n\t\t.handler = crl::guard(this, confirmDeleteShortcut),\n\t\t.icon = &st::menuIconDeleteAttention,\n\t\t.isAttention = true,\n\t});\n}\n\nbool ShortcutMessages::paintOuter(\n\t\tnot_null<QWidget*> outer,\n\t\tint maxVisibleHeight,\n\t\tQRect clip) {\n\tWindow::SectionWidget::PaintBackground(\n\t\t_theme.get(),\n\t\touter,\n\t\tstd::max(outer->height(), maxVisibleHeight),\n\t\t0,\n\t\tclip);\n\treturn true;\n}\n\nvoid ShortcutMessages::outerResized() {\n\tconst auto outer = _scroll->size();\n\tif (!_inOuterResize.isEmpty()) {\n\t\t_pendingOuterResize = (_inOuterResize != outer)\n\t\t\t? outer\n\t\t\t: QSize();\n\t\treturn;\n\t}\n\t_inOuterResize = outer;\n\n\tdo {\n\t\tconst auto newScrollTop = _scroll->isHidden()\n\t\t\t? std::nullopt\n\t\t\t: _scroll->scrollTop()\n\t\t\t? base::make_optional(_scroll->scrollTop())\n\t\t\t: 0;\n\t\t_skipScrollEvent = true;\n\t\tconst auto minHeight = (_container.current() == Container::Layer)\n\t\t\t? st::boxWidth\n\t\t\t: _inOuterResize.height();\n\t\t_inner->resizeToWidth(_inOuterResize.width(), minHeight);\n\t\t_skipScrollEvent = false;\n\n\t\tif (!_scroll->isHidden() && newScrollTop) {\n\t\t\t_scroll->scrollToY(*newScrollTop);\n\t\t}\n\t\t_inOuterResize = base::take(_pendingOuterResize);\n\t} while (!_inOuterResize.isEmpty());\n\n\tif (!_scroll->isHidden()) {\n\t\tupdateInnerVisibleArea();\n\t}\n\tupdateComposeControlsPosition();\n\t_cornerButtons.updatePositions();\n}\n\nvoid ShortcutMessages::updateComposeControlsPosition() {\n\tconst auto bottom = _scroll->parentWidget()->height();\n\tconst auto controlsHeight = _composeControls->heightCurrent();\n\t_composeControls->move(0, bottom - controlsHeight + st::boxRadius);\n\t_composeControls->setAutocompleteBoundingRect(_scroll->geometry());\n}\n\nvoid ShortcutMessages::setupComposeControls() {\n\t_shortcutId.value() | rpl::on_next([=](BusinessShortcutId id) {\n\t\t_composeControls->updateShortcutId(id);\n\t}, lifetime());\n\n\tconst auto state = Dialogs::EntryState{\n\t\t.key = Dialogs::Key{ _history },\n\t\t.section = Dialogs::EntryState::Section::ShortcutMessages,\n\t\t.currentReplyTo = replyTo(),\n\t\t.currentSuggest = SuggestOptions(),\n\t};\n\t_composeControls->setCurrentDialogsEntryState(state);\n\n\tauto writeRestriction = rpl::combine(\n\t\t_count.value(),\n\t\tShortcutMessagesLimitValue(_session)\n\t) | rpl::map([=](int count, int limit) {\n\t\treturn (count >= limit)\n\t\t\t? Controls::WriteRestriction{\n\t\t\t\t.text = tr::lng_business_limit_reached(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tlimit),\n\t\t\t\t.type = Controls::WriteRestrictionType::Rights,\n\t\t\t} : Controls::WriteRestriction();\n\t});\n\t_composeControls->setHistory({\n\t\t.history = _history.get(),\n\t\t.writeRestriction = std::move(writeRestriction),\n\t});\n\n\t_composeControls->cancelRequests(\n\t) | rpl::on_next([=] {\n\t\tlistCancelRequest();\n\t}, lifetime());\n\n\t_composeControls->sendRequests(\n\t) | rpl::on_next([=] {\n\t\tsend();\n\t}, lifetime());\n\n\t_composeControls->sendVoiceRequests(\n\t) | rpl::on_next([=](ComposeControls::VoiceToSend &&data) {\n\t\tsendVoice(std::move(data));\n\t}, lifetime());\n\n\t_composeControls->sendCommandRequests(\n\t) | rpl::on_next([=](const QString &command) {\n\t\tlistSendBotCommand(command, FullMsgId());\n\t}, lifetime());\n\n\tconst auto saveEditMsgRequestId = lifetime().make_state<mtpRequestId>(0);\n\t_composeControls->editRequests(\n\t) | rpl::on_next([=](auto data) {\n\t\tif (const auto item = _session->data().message(data.fullId)) {\n\t\t\tif (item->isBusinessShortcut()) {\n\t\t\t\tconst auto spoiler = data.spoilered;\n\t\t\t\tedit(item, data.options, saveEditMsgRequestId, spoiler);\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\t_composeControls->attachRequests(\n\t) | rpl::filter([=] {\n\t\treturn !_choosingAttach;\n\t}) | rpl::on_next([=](std::optional<bool> overrideCompress) {\n\t\t_choosingAttach = true;\n\t\tbase::call_delayed(st::historyAttach.ripple.hideDuration, this, [=] {\n\t\t\t_choosingAttach = false;\n\t\t\tchooseAttach(overrideCompress);\n\t\t});\n\t}, lifetime());\n\n\t_composeControls->setSendAsFileConfirmed(crl::guard(this, [=](\n\t\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\t\tApi::SendOptions options) {\n\t\tsendingFilesConfirmed(std::move(bundle), options);\n\t}));\n\n\t_composeControls->fileChosen(\n\t) | rpl::on_next([=](ChatHelpers::FileChosen data) {\n\t\t_controller->hideLayer(anim::type::normal);\n\t\tsendExistingDocument(data.document, {}, std::nullopt);\n\t}, lifetime());\n\n\t_composeControls->photoChosen(\n\t) | rpl::on_next([=](ChatHelpers::PhotoChosen chosen) {\n\t\tsendExistingPhoto(chosen.photo);\n\t}, lifetime());\n\n\t_composeControls->inlineResultChosen(\n\t) | rpl::on_next([=](ChatHelpers::InlineChosen chosen) {\n\t\tsendInlineResult(chosen.result, chosen.bot);\n\t}, lifetime());\n\n\t_composeControls->jumpToItemRequests(\n\t) | rpl::on_next([=](FullReplyTo to) {\n\t\tif (const auto item = _session->data().message(to.messageId)) {\n\t\t\tshowAtPosition(item->position());\n\t\t}\n\t}, lifetime());\n\n\trpl::merge(\n\t\t_composeControls->scrollKeyEvents(),\n\t\t_inner->scrollKeyEvents()\n\t) | rpl::on_next([=](not_null<QKeyEvent*> e) {\n\t\t_scroll->keyPressEvent(e);\n\t}, lifetime());\n\n\t_composeControls->editLastMessageRequests(\n\t) | rpl::on_next([=](not_null<QKeyEvent*> e) {\n\t\tif (!_inner->lastMessageEditRequestNotify()) {\n\t\t\t_scroll->keyPressEvent(e);\n\t\t}\n\t}, lifetime());\n\n\t_composeControls->setMimeDataHook([=](\n\t\t\tnot_null<const QMimeData*> data,\n\t\t\tUi::InputField::MimeAction action) {\n\t\tif (action == Ui::InputField::MimeAction::Check) {\n\t\t\treturn Core::CanSendFiles(data);\n\t\t} else if (action == Ui::InputField::MimeAction::Insert) {\n\t\t\treturn confirmSendingFiles(\n\t\t\t\tdata,\n\t\t\t\tstd::nullopt,\n\t\t\t\tCore::ReadMimeText(data));\n\t\t}\n\t\tUnexpected(\"action in MimeData hook.\");\n\t});\n\n\t_composeControls->lockShowStarts(\n\t) | rpl::on_next([=] {\n\t\t_cornerButtons.updateJumpDownVisibility();\n\t\t_cornerButtons.updateUnreadThingsVisibility();\n\t}, lifetime());\n\n\t_composeControls->viewportEvents(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\t_scroll->viewportEvent(e);\n\t}, lifetime());\n\n\t_controlsWrap->widthValue() | rpl::on_next([=](int width) {\n\t\t_composeControls->resizeToWidth(width);\n\t}, _controlsWrap->lifetime());\n\n\t_composeControls->height(\n\t) | rpl::on_next([=](int height) {\n\t\tconst auto wasMax = (_scroll->scrollTopMax() == _scroll->scrollTop());\n\t\t_controlsWrap->resize(width(), height - st::boxRadius);\n\t\tupdateComposeControlsPosition();\n\t\tif (wasMax) {\n\t\t\tlistScrollTo(_scroll->scrollTopMax());\n\t\t}\n\t}, lifetime());\n}\n\nbase::weak_qptr<Ui::RpWidget> ShortcutMessages::createPinnedToBottom(\n\t\tnot_null<Ui::RpWidget*> parent) {\n\tauto placeholder = rpl::deferred([=] {\n\t\treturn _shortcutId.value();\n\t}) | rpl::map([=](BusinessShortcutId id) {\n\t\treturn _session->data().shortcutMessages().lookupShortcut(id).name;\n\t}) | rpl::map([=](const QString &shortcut) {\n\t\treturn (shortcut == u\"away\"_q)\n\t\t\t? tr::lng_away_message_placeholder()\n\t\t\t: (shortcut == u\"hello\"_q)\n\t\t\t? tr::lng_greeting_message_placeholder()\n\t\t\t: tr::lng_replies_message_placeholder();\n\t}) | rpl::flatten_latest();\n\n\t_controlsWrap = std::make_unique<Ui::RpWidget>(parent);\n\t_composeControls = std::make_unique<ComposeControls>(\n\t\tdynamic_cast<Ui::RpWidget*>(_scroll->parentWidget()),\n\t\tComposeControlsDescriptor{\n\t\t\t.stOverride = &st::repliesComposeControls,\n\t\t\t.show = _controller->uiShow(),\n\t\t\t.unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) {\n\t\t\t\tlistShowPremiumToast(emoji);\n\t\t\t},\n\t\t\t.mode = HistoryView::ComposeControlsMode::Normal,\n\t\t\t.sendMenuDetails = [] { return SendMenu::Details(); },\n\t\t\t.regularWindow = _controller,\n\t\t\t.stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(),\n\t\t\t.customPlaceholder = std::move(placeholder),\n\t\t\t.panelsLevel = Window::GifPauseReason::Layer,\n\t\t\t.voiceCustomCancelText = tr::lng_record_cancel_stories(tr::now),\n\t\t\t.voiceLockFromBottom = true,\n\t\t\t.features = {\n\t\t\t\t.sendAs = false,\n\t\t\t\t.ttlInfo = false,\n\t\t\t\t.botCommandSend = false,\n\t\t\t\t.silentBroadcastToggle = false,\n\t\t\t\t.attachBotsMenu = false,\n\t\t\t\t.megagroupSet = false,\n\t\t\t\t.commonTabbedPanel = false,\n\t\t\t},\n\t\t});\n\n\tsetupComposeControls();\n\n\tshowAtEnd();\n\n\treturn _controlsWrap.get();\n}\n\nContext ShortcutMessages::listContext() {\n\treturn Context::ShortcutMessages;\n}\n\nbool ShortcutMessages::listScrollTo(int top, bool syntetic) {\n\ttop = std::clamp(top, 0, _scroll->scrollTopMax());\n\tif (_scroll->scrollTop() == top) {\n\t\tupdateInnerVisibleArea();\n\t\treturn false;\n\t}\n\t_scroll->scrollToY(top);\n\treturn true;\n}\n\nvoid ShortcutMessages::listCancelRequest() {\n\tif (_inner && !_inner->getSelectedItems().empty()) {\n\t\tclearSelected();\n\t\treturn;\n\t} else if (_composeControls->handleCancelRequest()) {\n\t\treturn;\n\t}\n\t_showBackRequests.fire({});\n}\n\nvoid ShortcutMessages::listDeleteRequest() {\n\tconfirmDeleteSelected();\n}\n\nvoid ShortcutMessages::listTryProcessKeyInput(not_null<QKeyEvent*> e) {\n\t_composeControls->tryProcessKeyInput(e);\n}\n\nrpl::producer<Data::MessagesSlice> ShortcutMessages::listSource(\n\t\tData::MessagePosition aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter) {\n\tconst auto messages = &_session->data().shortcutMessages();\n\treturn _shortcutId.value(\n\t) | rpl::map([=](BusinessShortcutId shortcutId) {\n\t\treturn rpl::single(rpl::empty) | rpl::then(\n\t\t\tmessages->updates(shortcutId)\n\t\t) | rpl::map([=] {\n\t\t\treturn messages->list(shortcutId);\n\t\t});\n\t}) | rpl::flatten_latest(\n\t) | rpl::after_next([=](const Data::MessagesSlice &slice) {\n\t\t_count = slice.fullCount.value_or(\n\t\t\tmessages->count(_shortcutId.current()));\n\t});\n}\n\nbool ShortcutMessages::listAllowsMultiSelect() {\n\treturn true;\n}\n\nbool ShortcutMessages::listIsItemGoodForSelection(\n\t\tnot_null<HistoryItem*> item) {\n\treturn !item->isSending() && !item->hasFailed();\n}\n\nbool ShortcutMessages::listIsLessInOrder(\n\t\tnot_null<HistoryItem*> first,\n\t\tnot_null<HistoryItem*> second) {\n\treturn first->position() < second->position();\n}\n\nvoid ShortcutMessages::listSelectionChanged(SelectedItems &&items) {\n\tauto value = Info::SelectedItems();\n\tvalue.title = [](int count) {\n\t\treturn tr::lng_forum_messages(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tcount,\n\t\t\tUi::StringWithNumbers::FromString);\n\t};\n\tvalue.list = items | ranges::views::transform([](SelectedItem item) {\n\t\tauto result = Info::SelectedItem(GlobalMsgId{ item.msgId });\n\t\tresult.canDelete = item.canDelete;\n\t\treturn result;\n\t}) | ranges::to_vector;\n\t_selectedItems = std::move(value);\n\n\tif (items.empty()) {\n\t\tdoSetInnerFocus();\n\t}\n}\n\nvoid ShortcutMessages::listMarkReadTill(not_null<HistoryItem*> item) {\n}\n\nvoid ShortcutMessages::listMarkContentsRead(\n\tconst base::flat_set<not_null<HistoryItem*>> &items) {\n}\n\nMessagesBarData ShortcutMessages::listMessagesBar(\n\t\tconst std::vector<not_null<Element*>> &elements,\n\t\tbool markLastAsRead) {\n\treturn {};\n}\n\nvoid ShortcutMessages::listContentRefreshed() {\n}\n\nvoid ShortcutMessages::listUpdateDateLink(\n\tClickHandlerPtr &link,\n\tnot_null<Element*> view) {\n}\n\nbool ShortcutMessages::listElementHideReply(not_null<const Element*> view) {\n\treturn false;\n}\n\nbool ShortcutMessages::listElementShownUnread(not_null<const Element*> view) {\n\treturn true;\n}\n\nbool ShortcutMessages::listIsGoodForAroundPosition(\n\t\tnot_null<const Element*> view) {\n\treturn true;\n}\n\nvoid ShortcutMessages::listSendBotCommand(\n\tconst QString &command,\n\tconst FullMsgId &context) {\n}\n\nvoid ShortcutMessages::listSearch(\n\t\tconst QString &query,\n\t\tconst FullMsgId &context) {\n\tconst auto inChat = _history->peer->isUser()\n\t\t? Dialogs::Key()\n\t\t: Dialogs::Key(_history);\n\t_controller->searchMessages(query, inChat);\n}\n\nvoid ShortcutMessages::listHandleViaClick(not_null<UserData*> bot) {\n\t_composeControls->setText({ '@' + bot->username() + ' ' });\n}\n\nnot_null<Ui::ChatTheme*> ShortcutMessages::listChatTheme() {\n\treturn _theme.get();\n}\n\nCopyRestrictionType ShortcutMessages::listCopyRestrictionType(\n\t\tHistoryItem *item) {\n\treturn CopyRestrictionType::None;\n}\n\nCopyRestrictionType ShortcutMessages::listCopyMediaRestrictionType(\n\t\tnot_null<HistoryItem*> item) {\n\tif (const auto media = item->media()) {\n\t\tif (const auto invoice = media->invoice()) {\n\t\t\tif (!invoice->extendedMedia.empty()) {\n\t\t\t\treturn CopyMediaRestrictionTypeFor(_history->peer, item);\n\t\t\t}\n\t\t}\n\t}\n\treturn CopyRestrictionType::None;\n}\n\nCopyRestrictionType ShortcutMessages::listSelectRestrictionType() {\n\treturn CopyRestrictionType::None;\n}\n\nauto ShortcutMessages::listAllowedReactionsValue()\n-> rpl::producer<Data::AllowedReactions> {\n\treturn rpl::single(Data::AllowedReactions());\n}\n\nvoid ShortcutMessages::listShowPremiumToast(\n\t\tnot_null<DocumentData*> document) {\n\tif (!_stickerToast) {\n\t\t_stickerToast = std::make_unique<HistoryView::StickerToast>(\n\t\t\t_controller,\n\t\t\tthis,\n\t\t\t[=] { _stickerToast = nullptr; });\n\t}\n\t_stickerToast->showFor(document);\n}\n\nvoid ShortcutMessages::listOpenPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId context) {\n\t_controller->openPhoto(photo, { context });\n}\n\nvoid ShortcutMessages::listOpenDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId context,\n\t\tbool showInMediaView) {\n\t_controller->openDocument(document, showInMediaView, { context });\n}\n\nvoid ShortcutMessages::listPaintEmpty(\n\t\tPainter &p,\n\t\tconst Ui::ChatPaintContext &context) {\n\tExpects(_emptyIcon != nullptr);\n\n\tconst auto width = st::repliesEmptyWidth;\n\tconst auto padding = st::repliesEmptyPadding;\n\tconst auto height = padding.top()\n\t\t+ _emptyIcon->height()\n\t\t+ st::repliesEmptySkip\n\t\t+ _emptyTextHeight\n\t\t+ padding.bottom();\n\tconst auto r = QRect(\n\t\t(this->width() - width) / 2,\n\t\t(this->height() - height) / 3,\n\t\twidth,\n\t\theight);\n\tHistoryView::ServiceMessagePainter::PaintBubble(p, context.st, r);\n\n\t_emptyIcon->paint(\n\t\tp,\n\t\tr.x() + (r.width() - _emptyIcon->width()) / 2,\n\t\tr.y() + padding.top(),\n\t\tthis->width());\n\tp.setPen(st::msgServiceFg);\n\t_emptyText.draw(\n\t\tp,\n\t\tr.x() + (r.width() - _emptyTextWidth) / 2,\n\t\tr.y() + padding.top() + _emptyIcon->height() + st::repliesEmptySkip,\n\t\t_emptyTextWidth,\n\t\tstyle::al_top);\n}\n\nQString ShortcutMessages::listElementAuthorRank(\n\t\tnot_null<const Element*> view) {\n\treturn {};\n}\n\nbool ShortcutMessages::listElementHideTopicButton(\n\t\tnot_null<const Element*> view) {\n\treturn true;\n}\n\nHistory *ShortcutMessages::listTranslateHistory() {\n\treturn nullptr;\n}\n\nvoid ShortcutMessages::listAddTranslatedItems(\n\tnot_null<TranslateTracker*> tracker) {\n}\n\nbool ShortcutMessages::listIgnorePaintEvent(QWidget *w, QPaintEvent *e) {\n\treturn false;\n}\n\nvoid ShortcutMessages::cornerButtonsShowAtPosition(\n\t\tData::MessagePosition position) {\n\tshowAtPosition(position);\n}\n\nData::Thread *ShortcutMessages::cornerButtonsThread() {\n\treturn _history;\n}\n\nFullMsgId ShortcutMessages::cornerButtonsCurrentId() {\n\treturn _lastShownAt;\n}\n\nbool ShortcutMessages::cornerButtonsIgnoreVisibility() {\n\treturn false;// animatingShow();\n}\n\nstd::optional<bool> ShortcutMessages::cornerButtonsDownShown() {\n\tif (_composeControls->isLockPresent()\n\t\t|| _composeControls->isTTLButtonShown()) {\n\t\treturn false;\n\t}\n\tconst auto top = _scroll->scrollTop() + st::historyToDownShownAfter;\n\tif (top < _scroll->scrollTopMax() || _cornerButtons.replyReturn()) {\n\t\treturn true;\n\t} else if (_inner->loadedAtBottomKnown()) {\n\t\treturn !_inner->loadedAtBottom();\n\t}\n\treturn std::nullopt;\n}\n\nbool ShortcutMessages::cornerButtonsUnreadMayBeShown() {\n\treturn _inner->loadedAtBottomKnown()\n\t\t&& !_composeControls->isLockPresent()\n\t\t&& !_composeControls->isTTLButtonShown();\n}\n\nbool ShortcutMessages::cornerButtonsHas(CornerButtonType type) {\n\treturn (type == CornerButtonType::Down);\n}\n\nvoid ShortcutMessages::checkReplyReturns() {\n\tconst auto currentTop = _scroll->scrollTop();\n\tconst auto shortcutId = _shortcutId.current();\n\twhile (const auto replyReturn = _cornerButtons.replyReturn()) {\n\t\tconst auto position = replyReturn->position();\n\t\tconst auto scrollTop = _inner->scrollTopForPosition(position);\n\t\tconst auto below = scrollTop\n\t\t\t? (currentTop >= std::min(*scrollTop, _scroll->scrollTopMax()))\n\t\t\t: _inner->isBelowPosition(position);\n\t\tif (replyReturn->shortcutId() != shortcutId || below) {\n\t\t\t_cornerButtons.calculateNextReplyReturn();\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nvoid ShortcutMessages::confirmDeleteSelected() {\n\tConfirmDeleteSelectedItems(_inner);\n}\n\nvoid ShortcutMessages::clearSelected() {\n\t_inner->cancelSelection();\n}\n\nvoid ShortcutMessages::uploadFile(\n\t\tconst QByteArray &fileContent,\n\t\tSendMediaType type) {\n\t_session->api().sendFile(fileContent, type, prepareSendAction({}));\n}\n\nbool ShortcutMessages::showSendingFilesError(\n\t\tconst Ui::PreparedList &list) const {\n\tif (showPremiumRequired()) {\n\t\treturn true;\n\t}\n\tconst auto show = _controller->uiShow();\n\tconst auto peer = _controller->session().user();\n\treturn Data::ShowSendError(show, peer, list, std::nullopt, true);\n}\n\nbool ShortcutMessages::showSendingFilesError(\n\t\tconst Ui::PreparedBundle &bundle) const {\n\tif (showPremiumRequired()) {\n\t\treturn true;\n\t}\n\tconst auto peer = _controller->session().user();\n\treturn Data::ShowSendError(_controller->uiShow(), peer, bundle, true);\n}\n\nApi::SendAction ShortcutMessages::prepareSendAction(\n\t\tApi::SendOptions options) const {\n\tauto result = Api::SendAction(_history, options);\n\tresult.replyTo = replyTo();\n\tresult.options.shortcutId = _shortcutId.current();\n\tresult.options.sendAs = _composeControls->sendAsPeer();\n\treturn result;\n}\n\nvoid ShortcutMessages::send() {\n\tif (_composeControls->getTextWithAppliedMarkdown().text.isEmpty()) {\n\t\treturn;\n\t}\n\tsend({});\n}\n\nvoid ShortcutMessages::sendVoice(ComposeControls::VoiceToSend &&data) {\n\tif (showPremiumRequired()) {\n\t\treturn;\n\t}\n\tauto action = prepareSendAction(data.options);\n\t_session->api().sendVoiceMessage(\n\t\tdata.bytes,\n\t\tdata.waveform,\n\t\tdata.duration,\n\t\tdata.video,\n\t\tstd::move(action));\n\n\t_composeControls->cancelReplyMessage();\n\t_composeControls->clearListenState();\n\tfinishSending();\n}\n\nvoid ShortcutMessages::send(Api::SendOptions options) {\n\tif (showPremiumRequired()) {\n\t\treturn;\n\t}\n\t_cornerButtons.clearReplyReturns();\n\n\tauto message = Api::MessageToSend(prepareSendAction(options));\n\tmessage.textWithTags = _composeControls->getTextWithAppliedMarkdown();\n\tmessage.webPage = _composeControls->webPageDraft();\n\n\t_session->api().sendMessage(std::move(message));\n\n\t_composeControls->clear();\n\n\tfinishSending();\n}\n\nvoid ShortcutMessages::edit(\n\t\tnot_null<HistoryItem*> item,\n\t\tApi::SendOptions options,\n\t\tmtpRequestId *const saveEditMsgRequestId,\n\t\tbool spoilered) {\n\tif (*saveEditMsgRequestId) {\n\t\treturn;\n\t}\n\tconst auto webpage = _composeControls->webPageDraft();\n\tauto sending = TextWithEntities();\n\tauto left = _composeControls->prepareTextForEditMsg();\n\n\tconst auto originalLeftSize = left.text.size();\n\tconst auto hasMediaWithCaption = item\n\t\t&& item->media()\n\t\t&& item->media()->allowsEditCaption();\n\tconst auto maxCaptionSize = !hasMediaWithCaption\n\t\t? MaxMessageSize\n\t\t: Data::PremiumLimits(_session).captionLengthCurrent();\n\tif (!TextUtilities::CutPart(sending, left, maxCaptionSize)\n\t\t&& !hasMediaWithCaption) {\n\t\tif (item) {\n\t\t\t_controller->show(Box<DeleteMessagesBox>(item, false));\n\t\t} else {\n\t\t\tdoSetInnerFocus();\n\t\t}\n\t\treturn;\n\t} else if (!left.text.isEmpty()) {\n\t\tconst auto remove = originalLeftSize - maxCaptionSize;\n\t\t_controller->showToast(\n\t\t\ttr::lng_edit_limit_reached(tr::now, lt_count, remove));\n\t\treturn;\n\t}\n\n\tlifetime().add([=] {\n\t\tif (!*saveEditMsgRequestId) {\n\t\t\treturn;\n\t\t}\n\t\t_session->api().request(base::take(*saveEditMsgRequestId)).cancel();\n\t});\n\n\tconst auto done = [=](mtpRequestId requestId) {\n\t\tif (requestId == *saveEditMsgRequestId) {\n\t\t\t*saveEditMsgRequestId = 0;\n\t\t\t_composeControls->cancelEditMessage();\n\t\t}\n\t};\n\n\tconst auto fail = [=](const QString &error, mtpRequestId requestId) {\n\t\tif (requestId == *saveEditMsgRequestId) {\n\t\t\t*saveEditMsgRequestId = 0;\n\t\t}\n\n\t\tif (ranges::contains(Api::kDefaultEditMessagesErrors, error)) {\n\t\t\t_controller->showToast(tr::lng_edit_error(tr::now));\n\t\t} else if (error == u\"MESSAGE_NOT_MODIFIED\"_q) {\n\t\t\t_composeControls->cancelEditMessage();\n\t\t} else if (error == u\"MESSAGE_EMPTY\"_q) {\n\t\t\tdoSetInnerFocus();\n\t\t} else {\n\t\t\t_controller->showToast(tr::lng_edit_error(tr::now));\n\t\t}\n\t\tupdate();\n\t\treturn true;\n\t};\n\n\t*saveEditMsgRequestId = Api::EditTextMessage(\n\t\titem,\n\t\tsending,\n\t\twebpage,\n\t\toptions,\n\t\tcrl::guard(this, done),\n\t\tcrl::guard(this, fail),\n\t\tspoilered);\n\n\t_composeControls->hidePanelsAnimated();\n\tdoSetInnerFocus();\n}\n\nbool ShortcutMessages::confirmSendingFiles(\n\t\tnot_null<const QMimeData*> data,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos,\n\t\tconst QString &insertTextOnCancel) {\n\tconst auto hasImage = data->hasImage();\n\tconst auto premium = _controller->session().user()->isPremium();\n\n\tif (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) {\n\t\tauto list = Storage::PrepareMediaList(\n\t\t\turls,\n\t\t\tst::sendMediaPreviewSize,\n\t\t\tpremium);\n\t\tif (list.error != Ui::PreparedList::Error::NonLocalUrl) {\n\t\t\tif (list.error == Ui::PreparedList::Error::None\n\t\t\t\t|| !hasImage) {\n\t\t\t\tconst auto emptyTextOnCancel = QString();\n\t\t\t\tlist.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;\n\t\t\t\tconfirmSendingFiles(std::move(list), emptyTextOnCancel);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (auto read = Core::ReadMimeImage(data)) {\n\t\tconfirmSendingFiles(\n\t\t\tstd::move(read.image),\n\t\t\tstd::move(read.content),\n\t\t\toverrideSendImagesAsPhotos,\n\t\t\tinsertTextOnCancel);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool ShortcutMessages::confirmSendingFiles(\n\t\tUi::PreparedList &&list,\n\t\tconst QString &insertTextOnCancel) {\n\tif (_composeControls->confirmMediaEdit(list)) {\n\t\treturn true;\n\t} else if (showSendingFilesError(list)) {\n\t\treturn false;\n\t}\n\n\tauto box = Box<SendFilesBox>(\n\t\t_controller,\n\t\tstd::move(list),\n\t\t_composeControls->getTextWithAppliedMarkdown(),\n\t\t_history->peer,\n\t\tApi::SendType::Normal,\n\t\tSendMenu::Details());\n\n\tbox->setConfirmedCallback(crl::guard(this, [=](\n\t\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\t\tApi::SendOptions options) {\n\t\tsendingFilesConfirmed(std::move(bundle), options);\n\t}));\n\tbox->setCancelledCallback(_composeControls->restoreTextCallback(\n\t\tinsertTextOnCancel));\n\tbox->takeTextWithTagsRequests() | rpl::on_next([=](TextWithTags &&text) {\n\t\t_composeControls->setText(std::move(text));\n\t}, box->lifetime());\n\n\t//ActivateWindow(_controller);\n\t_controller->show(std::move(box));\n\n\treturn true;\n}\n\nbool ShortcutMessages::confirmSendingFiles(\n\t\tQImage &&image,\n\t\tQByteArray &&content,\n\t\tstd::optional<bool> overrideSendImagesAsPhotos,\n\t\tconst QString &insertTextOnCancel) {\n\tif (image.isNull()) {\n\t\treturn false;\n\t}\n\n\tauto list = Storage::PrepareMediaFromImage(\n\t\tstd::move(image),\n\t\tstd::move(content),\n\t\tst::sendMediaPreviewSize);\n\tlist.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;\n\treturn confirmSendingFiles(std::move(list), insertTextOnCancel);\n}\n\nvoid ShortcutMessages::sendingFilesConfirmed(\n\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\tApi::SendOptions options) {\n\tif (showSendingFilesError(*bundle)) {\n\t\treturn;\n\t}\n\tconst auto compress = bundle->way.sendImagesAsPhotos();\n\tconst auto type = compress ? SendMediaType::Photo : SendMediaType::File;\n\tauto action = prepareSendAction(options);\n\taction.clearDraft = false;\n\tauto &api = _session->api();\n\tfor (auto &group : bundle->groups) {\n\t\tconst auto album = (group.type != Ui::AlbumType::None)\n\t\t\t? std::make_shared<SendingAlbum>()\n\t\t\t: nullptr;\n\t\tapi.sendFiles(std::move(group.list), type, album, action);\n\t}\n\tif (_composeControls->replyingToMessage() == action.replyTo) {\n\t\t_composeControls->cancelReplyMessage();\n\t}\n\tfinishSending();\n}\n\nvoid ShortcutMessages::chooseAttach(\n\t\tstd::optional<bool> overrideSendImagesAsPhotos) {\n\tif (showPremiumRequired()) {\n\t\treturn;\n\t}\n\t_choosingAttach = false;\n\n\tconst auto filter = (overrideSendImagesAsPhotos == true)\n\t\t? FileDialog::PhotoVideoFilesFilter()\n\t\t: FileDialog::AllOrImagesFilter();\n\tFileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=](\n\t\t\tFileDialog::OpenResult &&result) {\n\t\tif (result.paths.isEmpty() && result.remoteContent.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!result.remoteContent.isEmpty()) {\n\t\t\tauto read = Images::Read({\n\t\t\t\t.content = result.remoteContent,\n\t\t\t});\n\t\t\tif (!read.image.isNull() && !read.animated) {\n\t\t\t\tconfirmSendingFiles(\n\t\t\t\t\tstd::move(read.image),\n\t\t\t\t\tstd::move(result.remoteContent),\n\t\t\t\t\toverrideSendImagesAsPhotos);\n\t\t\t} else {\n\t\t\t\tuploadFile(result.remoteContent, SendMediaType::File);\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto premium = _controller->session().user()->isPremium();\n\t\t\tauto list = Storage::PrepareMediaList(\n\t\t\t\tresult.paths,\n\t\t\t\tst::sendMediaPreviewSize,\n\t\t\t\tpremium);\n\t\t\tlist.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;\n\t\t\tconfirmSendingFiles(std::move(list));\n\t\t}\n\t}), nullptr);\n}\n\nvoid ShortcutMessages::finishSending() {\n\t_composeControls->hidePanelsAnimated();\n\t//if (_previewData && _previewData->pendingTill) previewCancel();\n\tdoSetInnerFocus();\n\tshowAtEnd();\n}\n\nvoid ShortcutMessages::showAtEnd() {\n\tshowAtPosition(Data::MaxMessagePosition);\n}\n\nvoid ShortcutMessages::doSetInnerFocus() {\n\tif (!_inner->getSelectedText().rich.text.isEmpty()\n\t\t|| !_inner->getSelectedItems().empty()\n\t\t|| !_composeControls->focus()) {\n\t\t_inner->setFocus();\n\t}\n}\n\nbool ShortcutMessages::sendExistingDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tApi::SendOptions options,\n\t\tstd::optional<MsgId> localId) {\n\tif (showPremiumRequired()) {\n\t\treturn false;\n\t}\n\n\tApi::SendExistingDocument(\n\t\tApi::MessageToSend(prepareSendAction(options)),\n\t\tdocument,\n\t\tlocalId);\n\n\t_composeControls->cancelReplyMessage();\n\tfinishSending();\n\treturn true;\n}\n\nvoid ShortcutMessages::sendExistingPhoto(not_null<PhotoData*> photo) {\n\tsendExistingPhoto(photo, {});\n}\n\nbool ShortcutMessages::sendExistingPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tApi::SendOptions options) {\n\tif (showPremiumRequired()) {\n\t\treturn false;\n\t}\n\tApi::SendExistingPhoto(\n\t\tApi::MessageToSend(prepareSendAction(options)),\n\t\tphoto);\n\n\t_composeControls->cancelReplyMessage();\n\tfinishSending();\n\treturn true;\n}\n\nvoid ShortcutMessages::sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot) {\n\tif (showPremiumRequired()) {\n\t\treturn;\n\t} else if (const auto error = result->getErrorOnSend(_history)) {\n\t\tData::ShowSendErrorToast(_controller, _history->peer, error);\n\t\treturn;\n\t}\n\tsendInlineResult(std::move(result), bot, {}, std::nullopt);\n}\n\nvoid ShortcutMessages::sendInlineResult(\n\t\tstd::shared_ptr<InlineBots::Result> result,\n\t\tnot_null<UserData*> bot,\n\t\tApi::SendOptions options,\n\t\tstd::optional<MsgId> localMessageId) {\n\tif (showPremiumRequired()) {\n\t\treturn;\n\t}\n\tauto action = prepareSendAction(options);\n\taction.generateLocal = true;\n\t_session->api().sendInlineResult(\n\t\tbot,\n\t\tresult.get(),\n\t\taction,\n\t\tlocalMessageId);\n\n\t_composeControls->clear();\n\t//_saveDraftText = true;\n\t//_saveDraftStart = crl::now();\n\t//onDraftSave();\n\n\tauto &bots = cRefRecentInlineBots();\n\tconst auto index = bots.indexOf(bot);\n\tif (index) {\n\t\tif (index > 0) {\n\t\t\tbots.removeAt(index);\n\t\t} else if (bots.size() >= RecentInlineBotsLimit) {\n\t\t\tbots.resize(RecentInlineBotsLimit - 1);\n\t\t}\n\t\tbots.push_front(bot);\n\t\tbot->session().local().writeRecentHashtagsAndBots();\n\t}\n\tfinishSending();\n}\n\nvoid ShortcutMessages::showAtPosition(\n\t\tData::MessagePosition position,\n\t\tFullMsgId originItemId) {\n\tshowAtPosition(position, originItemId, {});\n}\n\nvoid ShortcutMessages::showAtPosition(\n\t\tData::MessagePosition position,\n\t\tFullMsgId originItemId,\n\t\tconst Window::SectionShow &params) {\n\t_lastShownAt = position.fullId;\n\t_inner->showAtPosition(\n\t\tposition,\n\t\tparams,\n\t\t_cornerButtons.doneJumpFrom(position.fullId, originItemId, true));\n}\n\nFullReplyTo ShortcutMessages::replyTo() const {\n\treturn _composeControls->replyingToMessage();\n}\n\nbool ShortcutMessages::showPremiumRequired() const {\n\tif (!_controller->session().premium()) {\n\t\tShowPremiumPreviewToBuy(_controller, PremiumFeature::QuickReplies);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\n} // namespace\n\nType ShortcutMessagesId(int shortcutId) {\n\treturn ShortcutMessages::Id(shortcutId);\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_shortcut_messages.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\n[[nodiscard]] Type ShortcutMessagesId(int shortcutId);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_working_hours.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/business/settings_working_hours.h\"\n\n#include \"base/event_filter.h\"\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"data/business/data_business_info.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/business/settings_recipients_helper.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/vertical_drum_picker.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nconstexpr auto kDay = Data::WorkingInterval::kDay;\nconstexpr auto kWeek = Data::WorkingInterval::kWeek;\nconstexpr auto kInNextDayMax = Data::WorkingInterval::kInNextDayMax;\n\nclass WorkingHours : public Section<WorkingHours> {\npublic:\n\tWorkingHours(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~WorkingHours();\n\n\t[[nodiscard]] bool closeByOutsideClick() const override;\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\nprivate:\n\tvoid setupContent(not_null<Window::SessionController*> controller);\n\tvoid save();\n\n\trpl::variable<Data::WorkingHours> _hours;\n\trpl::variable<bool> _enabled;\n\n};\n\n[[nodiscard]] QString TimezoneFullName(const Data::Timezone &data) {\n\tconst auto abs = std::abs(data.utcOffset);\n\tconst auto hours = abs / 3600;\n\tconst auto minutes = (abs % 3600) / 60;\n\tconst auto sign = (data.utcOffset < 0) ? '-' : '+';\n\tconst auto prefix = u\"(UTC\"_q\n\t\t+ sign\n\t\t+ QString::number(hours)\n\t\t+ u\":\"_q\n\t\t+ QString::number(minutes).rightJustified(2, u'0')\n\t\t+ u\")\"_q;\n\treturn prefix + ' ' + data.name;\n}\n\n[[nodiscard]] QString FormatDayTime(\n\t\tTimeId time,\n\t\tbool showEndAsNextDay = false) {\n\tconst auto wrap = [](TimeId value) {\n\t\tconst auto hours = value / 3600;\n\t\tconst auto minutes = (value % 3600) / 60;\n\t\treturn QString::number(hours).rightJustified(2, u'0')\n\t\t\t+ ':'\n\t\t\t+ QString::number(minutes).rightJustified(2, u'0');\n\t};\n\treturn (time > kDay || (showEndAsNextDay && time == kDay))\n\t\t? tr::lng_hours_next_day(tr::now, lt_time, wrap(time - kDay))\n\t\t: wrap(time == kDay ? 0 : time);\n}\n\n[[nodiscard]] QString FormatTimeHour(TimeId time) {\n\tconst auto wrap = [](TimeId value) {\n\t\treturn QString::number(value / 3600).rightJustified(2, u'0');\n\t};\n\tif (time < kDay) {\n\t\treturn wrap(time);\n\t}\n\tconst auto wrapped = wrap(time - kDay);\n\tconst auto result = tr::lng_hours_on_next_day(tr::now, lt_time, wrapped);\n\tconst auto i = result.indexOf(wrapped);\n\treturn (i >= 0) ? (result.left(i) + wrapped) : result;\n}\n\n[[nodiscard]] QString FormatTimeMinute(TimeId time) {\n\tconst auto wrap = [](TimeId value) {\n\t\treturn QString::number((value / 60) % 60).rightJustified(2, u'0');\n\t};\n\tif (time < kDay) {\n\t\treturn wrap(time);\n\t}\n\tconst auto wrapped = wrap(time - kDay);\n\tconst auto result = tr::lng_hours_on_next_day(tr::now, lt_time, wrapped);\n\tconst auto i = result.indexOf(wrapped);\n\treturn (i >= 0)\n\t\t? (wrapped + result.right(result.size() - i - wrapped.size()))\n\t\t: result;\n}\n\n[[nodiscard]] QString JoinIntervals(const Data::WorkingIntervals &data) {\n\tauto result = QStringList();\n\tresult.reserve(data.list.size());\n\tfor (const auto &interval : data.list) {\n\t\tconst auto start = FormatDayTime(interval.start);\n\t\tconst auto end = FormatDayTime(interval.end);\n\t\tresult.push_back(start + u\" - \"_q + end);\n\t}\n\treturn result.join(u\", \"_q);\n}\n\nvoid EditTimeBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tTimeId low,\n\t\tTimeId high,\n\t\tTimeId value,\n\t\tFn<void(TimeId)> save) {\n\tExpects(low <= high);\n\n\tconst auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>(\n\t\tbox,\n\t\tst::settingsWorkingHoursPicker));\n\n\tconst auto font = st::boxTextFont;\n\tconst auto itemHeight = st::settingsWorkingHoursPickerItemHeight;\n\tconst auto picker = [=](\n\t\t\tint count,\n\t\t\tint startIndex,\n\t\t\tFn<void(QPainter &p, QRectF rect, int index)> paint) {\n\t\treturn Ui::CreateChild<Ui::VerticalDrumPicker>(\n\t\t\tcontent,\n\t\t\tUi::VerticalDrumPicker::DefaultPaintCallback(\n\t\t\t\tfont,\n\t\t\t\titemHeight,\n\t\t\t\tpaint),\n\t\t\tcount,\n\t\t\titemHeight,\n\t\t\tstartIndex);\n\t};\n\n\tconst auto hoursCount = (high - low + 3600) / 3600;\n\tconst auto hoursStartIndex = (value / 3600) - (low / 3600);\n\tconst auto hoursPaint = [=](QPainter &p, QRectF rect, int index) {\n\t\tp.drawText(\n\t\t\trect,\n\t\t\tFormatTimeHour(((low / 3600) + index) * 3600),\n\t\t\tstyle::al_right);\n\t};\n\tconst auto hours = picker(hoursCount, hoursStartIndex, hoursPaint);\n\tconst auto minutes = content->lifetime().make_state<\n\t\trpl::variable<Ui::VerticalDrumPicker*>\n\t>(nullptr);\n\n\t// hours->value() is valid only after size is set.\n\tconst auto separator = u\":\"_q;\n\tconst auto separatorWidth = st::boxTextFont->width(separator);\n\trpl::combine(\n\t\tcontent->sizeValue(),\n\t\tminutes->value()\n\t) | rpl::on_next([=](QSize s, Ui::VerticalDrumPicker *minutes) {\n\t\tconst auto half = (s.width() - separatorWidth) / 2;\n\t\thours->setGeometry(0, 0, half, s.height());\n\t\tif (minutes) {\n\t\t\tminutes->setGeometry(half + separatorWidth, 0, half, s.height());\n\t\t}\n\t}, content->lifetime());\n\n\tUi::SendPendingMoveResizeEvents(hours);\n\n\tconst auto minutesStart = content->lifetime().make_state<TimeId>();\n\thours->value() | rpl::on_next([=](int hoursIndex) {\n\t\tconst auto start = std::max(low, (hoursIndex + (low / 3600)) * 3600);\n\t\tconst auto end = std::min(high, ((start / 3600) * 60 + 59) * 60);\n\t\tconst auto minutesCount = (end - start + 60) / 60;\n\t\tconst auto minutesStartIndex = minutes->current()\n\t\t\t? std::clamp(\n\t\t\t\t((((*minutesStart) / 60 + minutes->current()->index()) % 60)\n\t\t\t\t\t- ((start / 60) % 60)),\n\t\t\t\t0,\n\t\t\t\t(minutesCount - 1))\n\t\t\t: std::clamp((value / 60) - (start / 60), 0, minutesCount - 1);\n\t\t*minutesStart = start;\n\n\t\tconst auto minutesPaint = [=](QPainter &p, QRectF rect, int index) {\n\t\t\tp.drawText(\n\t\t\t\trect,\n\t\t\t\tFormatTimeMinute(((start / 60) + index) * 60),\n\t\t\t\tstyle::al_left);\n\t\t};\n\t\tconst auto updated = picker(\n\t\t\tminutesCount,\n\t\t\tminutesStartIndex,\n\t\t\tminutesPaint);\n\t\tdelete minutes->current();\n\t\t*minutes = updated;\n\t\tminutes->current()->show();\n\t}, hours->lifetime());\n\n\tcontent->paintRequest(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tauto p = QPainter(content);\n\n\t\tp.fillRect(r, Qt::transparent);\n\n\t\tconst auto lineRect = QRect(\n\t\t\t0,\n\t\t\tcontent->height() / 2,\n\t\t\tcontent->width(),\n\t\t\tst::defaultInputField.borderActive);\n\t\tp.fillRect(lineRect.translated(0, itemHeight / 2), st::activeLineFg);\n\t\tp.fillRect(lineRect.translated(0, -itemHeight / 2), st::activeLineFg);\n\t\tp.drawText(QRectF(content->rect()), separator, style::al_center);\n\t}, content->lifetime());\n\n\tbase::install_event_filter(box, [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::KeyPress) {\n\t\t\thours->handleKeyEvent(static_cast<QKeyEvent*>(e.get()));\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\tconst auto weak = base::make_weak(box);\n\t\tsave(std::clamp(\n\t\t\t((*minutesStart) / 60 + minutes->current()->index()) * 60,\n\t\t\tlow,\n\t\t\thigh));\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nvoid EditDayBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\trpl::producer<QString> title,\n\t\tData::WorkingIntervals intervals,\n\t\tFn<void(Data::WorkingIntervals)> save) {\n\tbox->setTitle(std::move(title));\n\tbox->setWidth(st::boxWideWidth);\n\tstruct State {\n\t\trpl::variable<Data::WorkingIntervals> data;\n\t};\n\tconst auto state = box->lifetime().make_state<State>(State{\n\t\t.data = std::move(intervals),\n\t});\n\n\tconst auto container = box->verticalLayout();\n\tconst auto rows = container->add(\n\t\tobject_ptr<Ui::VerticalLayout>(container));\n\tconst auto makeRow = [=](\n\t\t\tData::WorkingInterval interval,\n\t\t\tTimeId min,\n\t\t\tTimeId max) {\n\t\tauto result = object_ptr<Ui::VerticalLayout>(rows);\n\t\tconst auto raw = result.data();\n\t\tAddDivider(raw);\n\t\tAddSkip(raw);\n\t\tAddButtonWithLabel(\n\t\t\traw,\n\t\t\ttr::lng_hours_opening(),\n\t\t\trpl::single(FormatDayTime(interval.start, true)),\n\t\t\tst::settingsButtonNoIcon\n\t\t)->setClickedCallback([=] {\n\t\t\tconst auto max = std::max(min, interval.end - 60);\n\t\t\tconst auto now = std::clamp(interval.start, min, max);\n\t\t\tconst auto save = crl::guard(box, [=](TimeId value) {\n\t\t\t\tauto now = state->data.current();\n\t\t\t\tconst auto i = ranges::find(now.list, interval);\n\t\t\t\tif (i != end(now.list)) {\n\t\t\t\t\ti->start = value;\n\t\t\t\t\tstate->data = now.normalized();\n\t\t\t\t}\n\t\t\t});\n\t\t\tbox->getDelegate()->show(Box(EditTimeBox, min, max, now, save));\n\t\t});\n\t\tAddButtonWithLabel(\n\t\t\traw,\n\t\t\ttr::lng_hours_closing(),\n\t\t\trpl::single(FormatDayTime(interval.end, true)),\n\t\t\tst::settingsButtonNoIcon\n\t\t)->setClickedCallback([=] {\n\t\t\tconst auto min = std::min(max, interval.start + 60);\n\t\t\tconst auto now = std::clamp(interval.end, min, max);\n\t\t\tconst auto save = crl::guard(box, [=](TimeId value) {\n\t\t\t\tauto now = state->data.current();\n\t\t\t\tconst auto i = ranges::find(now.list, interval);\n\t\t\t\tif (i != end(now.list)) {\n\t\t\t\t\ti->end = value;\n\t\t\t\t\tstate->data = now.normalized();\n\t\t\t\t}\n\t\t\t});\n\t\t\tbox->getDelegate()->show(Box(EditTimeBox, min, max, now, save));\n\t\t});\n\t\traw->add(object_ptr<Ui::SettingsButton>(\n\t\t\traw,\n\t\t\ttr::lng_hours_remove(),\n\t\t\tst::settingsAttentionButton\n\t\t))->setClickedCallback([=] {\n\t\t\tauto now = state->data.current();\n\t\t\tconst auto i = ranges::find(now.list, interval);\n\t\t\tif (i != end(now.list)) {\n\t\t\t\tnow.list.erase(i);\n\t\t\t\tstate->data = std::move(now);\n\t\t\t}\n\t\t});\n\t\tAddSkip(raw);\n\n\t\treturn result;\n\t};\n\n\tconst auto addWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tAddDivider(addWrap->entity());\n\tAddSkip(addWrap->entity());\n\tconst auto add = addWrap->entity()->add(\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\tcontainer,\n\t\t\ttr::lng_hours_add_button(),\n\t\t\tst::settingsButtonLightNoIcon));\n\tadd->setClickedCallback([=] {\n\t\tauto now = state->data.current();\n\t\tif (now.list.empty()) {\n\t\t\tnow.list.push_back({ 8 * 3600, 20 * 3600 });\n\t\t} else if (const auto last = now.list.back().end; last + 60 < kDay) {\n\t\t\tconst auto from = std::max(\n\t\t\t\tstd::min(last + 30 * 60, kDay - 30 * 60),\n\t\t\t\tlast + 60);\n\t\t\tnow.list.push_back({ from, from + 4 * 3600 });\n\t\t}\n\t\tstate->data = std::move(now);\n\t});\n\n\tstate->data.value(\n\t) | rpl::on_next([=](const Data::WorkingIntervals &data) {\n\t\tconst auto count = int(data.list.size());\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tconst auto min = (i == 0) ? 0 : (data.list[i - 1].end + 60);\n\t\t\tconst auto max = (i == count - 1)\n\t\t\t\t? (kDay + kInNextDayMax)\n\t\t\t\t: (data.list[i + 1].start - 60);\n\t\t\trows->insert(i, makeRow(data.list[i], min, max));\n\t\t\tif (rows->count() > i + 1) {\n\t\t\t\tdelete rows->widgetAt(i + 1);\n\t\t\t}\n\t\t}\n\t\twhile (rows->count() > count) {\n\t\t\tdelete rows->widgetAt(count);\n\t\t}\n\t\trows->resizeToWidth(st::boxWideWidth);\n\t\taddWrap->toggle(data.list.empty()\n\t\t\t|| data.list.back().end + 60 < kDay, anim::type::instant);\n\t\tadd->clearState();\n\t}, add->lifetime());\n\taddWrap->finishAnimating();\n\n\tAddSkip(container);\n\tAddDividerText(container, tr::lng_hours_about_day());\n\n\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\tconst auto weak = base::make_weak(box);\n\t\tsave(state->data.current());\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nvoid ChooseTimezoneBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::vector<Data::Timezone> list,\n\t\tQString id,\n\t\tFn<void(QString)> save) {\n\tExpects(!list.empty());\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setTitle(tr::lng_hours_time_zone_title());\n\n\tconst auto height = st::boxWideWidth;\n\tbox->setMaxHeight(height);\n\n\tranges::sort(list, ranges::less(), [](const Data::Timezone &value) {\n\t\treturn std::pair(value.utcOffset, value.name);\n\t});\n\n\tif (!ranges::contains(list, id, &Data::Timezone::id)) {\n\t\tid = Data::FindClosestTimezoneId(list);\n\t}\n\tconst auto i = ranges::find(list, id, &Data::Timezone::id);\n\tconst auto value = int(i - begin(list));\n\tconst auto group = std::make_shared<Ui::RadiobuttonGroup>(value);\n\tconst auto radioPadding = st::defaultCheckbox.margin;\n\tconst auto max = std::max(radioPadding.top(), radioPadding.bottom());\n\tauto index = 0;\n\tauto padding = st::boxRowPadding + QMargins(0, max, 0, max);\n\tauto selected = (Ui::Radiobutton*)nullptr;\n\tfor (const auto &entry : list) {\n\t\tconst auto button = box->addRow(\n\t\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\t\tbox,\n\t\t\t\tgroup,\n\t\t\t\tindex++,\n\t\t\t\tTimezoneFullName(entry)),\n\t\t\tpadding);\n\t\tif (index == value + 1) {\n\t\t\tselected = button;\n\t\t}\n\t\tpadding = st::boxRowPadding + QMargins(0, 0, 0, max);\n\t}\n\tif (selected) {\n\t\tbox->verticalLayout()->resizeToWidth(st::boxWideWidth);\n\t\tconst auto y = selected->y() - (height - selected->height()) / 2;\n\t\tbox->setInitScrollCallback([=] {\n\t\t\tbox->scrollToY(y);\n\t\t});\n\t}\n\tgroup->setChangedCallback([=](int index) {\n\t\tconst auto weak = base::make_weak(box);\n\t\tsave(list[index].id);\n\t\tif (const auto strong = weak.get()) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t});\n\tbox->addButton(tr::lng_close(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nvoid AddWeekButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tint index,\n\t\tnot_null<rpl::variable<Data::WorkingHours>*> data) {\n\tauto label = [&] {\n\t\tswitch (index) {\n\t\tcase 0: return tr::lng_hours_monday();\n\t\tcase 1: return tr::lng_hours_tuesday();\n\t\tcase 2: return tr::lng_hours_wednesday();\n\t\tcase 3: return tr::lng_hours_thursday();\n\t\tcase 4: return tr::lng_hours_friday();\n\t\tcase 5: return tr::lng_hours_saturday();\n\t\tcase 6: return tr::lng_hours_sunday();\n\t\t}\n\t\tUnexpected(\"Index in AddWeekButton.\");\n\t}();\n\tconst auto &st = st::settingsWorkingHoursWeek;\n\tconst auto button = AddButtonWithIcon(\n\t\tcontainer,\n\t\trpl::duplicate(label),\n\t\tst);\n\tbutton->setClickedCallback([=] {\n\t\tconst auto done = [=](Data::WorkingIntervals intervals) {\n\t\t\tauto now = data->current();\n\t\t\tnow.intervals = ReplaceDayIntervals(\n\t\t\t\tnow.intervals,\n\t\t\t\tindex,\n\t\t\t\tstd::move(intervals));\n\t\t\t*data = now.normalized();\n\t\t};\n\t\tcontroller->show(Box(\n\t\t\tEditDayBox,\n\t\t\trpl::duplicate(label),\n\t\t\tExtractDayIntervals(data->current().intervals, index),\n\t\t\tcrl::guard(button, done)));\n\t});\n\n\tconst auto toggleButton = Ui::CreateChild<Ui::SettingsButton>(\n\t\tcontainer.get(),\n\t\tnullptr,\n\t\tst);\n\tconst auto checkView = button->lifetime().make_state<Ui::ToggleView>(\n\t\tst.toggle,\n\t\tfalse,\n\t\t[=] { toggleButton->update(); });\n\n\tauto status = data->value(\n\t) | rpl::map([=](const Data::WorkingHours &data) -> rpl::producer<QString> {\n\t\tusing namespace Data;\n\n\t\tconst auto intervals = ExtractDayIntervals(data.intervals, index);\n\t\tconst auto empty = intervals.list.empty();\n\t\tif (checkView->checked() == empty) {\n\t\t\tcheckView->setChecked(!empty, anim::type::instant);\n\t\t}\n\t\tif (!intervals) {\n\t\t\treturn tr::lng_hours_closed();\n\t\t} else if (IsFullOpen(intervals)) {\n\t\t\treturn tr::lng_hours_open_full();\n\t\t}\n\t\treturn rpl::single(JoinIntervals(intervals));\n\t}) | rpl::flatten_latest();\n\tconst auto details = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton.get(),\n\t\tstd::move(status),\n\t\tst::settingsWorkingHoursDetails);\n\tdetails->show();\n\tdetails->moveToLeft(\n\t\tst.padding.left(),\n\t\tst.padding.top() + st.height - details->height());\n\tdetails->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto separator = Ui::CreateChild<Ui::RpWidget>(container.get());\n\tseparator->paintRequest(\n\t) | rpl::on_next([=, bg = st.textBgOver] {\n\t\tauto p = QPainter(separator);\n\t\tp.fillRect(separator->rect(), bg);\n\t}, separator->lifetime());\n\tconst auto separatorHeight = st.height - 2 * st.toggle.border;\n\tbutton->geometryValue(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tconst auto w = st::rightsButtonToggleWidth;\n\t\ttoggleButton->setGeometry(\n\t\t\tr.x() + r.width() - w,\n\t\t\tr.y(),\n\t\t\tw,\n\t\t\tr.height());\n\t\tseparator->setGeometry(\n\t\t\ttoggleButton->x() - st::lineWidth,\n\t\t\tr.y() + (r.height() - separatorHeight) / 2,\n\t\t\tst::lineWidth,\n\t\t\tseparatorHeight);\n\t}, toggleButton->lifetime());\n\n\tconst auto checkWidget = Ui::CreateChild<Ui::RpWidget>(toggleButton);\n\tcheckWidget->resize(checkView->getSize());\n\tcheckWidget->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(checkWidget);\n\t\tcheckView->paint(p, 0, 0, checkWidget->width());\n\t}, checkWidget->lifetime());\n\ttoggleButton->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tcheckWidget->moveToRight(\n\t\t\tst.toggleSkip,\n\t\t\t(s.height() - checkWidget->height()) / 2);\n\t}, toggleButton->lifetime());\n\n\ttoggleButton->setClickedCallback([=] {\n\t\tconst auto enabled = !checkView->checked();\n\t\tcheckView->setChecked(enabled, anim::type::normal);\n\t\tauto now = data->current();\n\t\tnow.intervals = ReplaceDayIntervals(\n\t\t\tnow.intervals,\n\t\t\tindex,\n\t\t\t(enabled\n\t\t\t\t? Data::WorkingIntervals{ { { 0, kDay } } }\n\t\t\t\t: Data::WorkingIntervals()));\n\t\t*data = now.normalized();\n\t});\n}\n\nWorkingHours::WorkingHours(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent(controller);\n}\n\nWorkingHours::~WorkingHours() {\n\tif (!Core::Quitting()) {\n\t\tsave();\n\t}\n}\n\nbool WorkingHours::closeByOutsideClick() const {\n\treturn false;\n}\n\nrpl::producer<QString> WorkingHours::title() {\n\treturn tr::lng_hours_title();\n}\n\nvoid WorkingHours::setupContent(\n\t\tnot_null<Window::SessionController*> controller) {\n\tusing namespace rpl::mappers;\n\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tstruct State {\n\t\trpl::variable<Data::Timezones> timezones;\n\t\tbool timezoneEditPending = false;\n\t};\n\tconst auto info = &controller->session().data().businessInfo();\n\tconst auto state = content->lifetime().make_state<State>(State{\n\t\t.timezones = info->timezonesValue(),\n\t});\n\t_hours = controller->session().user()->businessDetails().hours;\n\n\tAddDividerTextWithLottie(content, {\n\t\t.lottie = u\"hours\"_q,\n\t\t.lottieSize = st::settingsCloudPasswordIconSize,\n\t\t.lottieMargins = st::peerAppearanceIconPadding,\n\t\t.showFinished = showFinishes(),\n\t\t.about = tr::lng_hours_about(tr::marked),\n\t\t.aboutMargins = st::peerAppearanceCoverLabelMargin,\n\t});\n\n\tUi::AddSkip(content);\n\tconst auto enabled = content->add(object_ptr<Ui::SettingsButton>(\n\t\tcontent,\n\t\ttr::lng_hours_show(),\n\t\tst::settingsButtonNoIcon\n\t))->toggleOn(rpl::single(bool(_hours.current())));\n\n\t_enabled = enabled->toggledValue();\n\n\tconst auto wrap = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::VerticalLayout>(content)));\n\tconst auto inner = wrap->entity();\n\n\tUi::AddSkip(inner);\n\tUi::AddDivider(inner);\n\tUi::AddSkip(inner);\n\n\tfor (auto i = 0; i != 7; ++i) {\n\t\tAddWeekButton(inner, controller, i, &_hours);\n\t}\n\n\tUi::AddSkip(inner);\n\tUi::AddDivider(inner);\n\tUi::AddSkip(inner);\n\n\tstate->timezones.value(\n\t) | rpl::filter([=](const Data::Timezones &value) {\n\t\treturn !value.list.empty();\n\t}) | rpl::on_next([=](const Data::Timezones &value) {\n\t\tconst auto now = _hours.current().timezoneId;\n\t\tif (!ranges::contains(value.list, now, &Data::Timezone::id)) {\n\t\t\tauto copy = _hours.current();\n\t\t\tcopy.timezoneId = Data::FindClosestTimezoneId(value.list);\n\t\t\t_hours = std::move(copy);\n\t\t}\n\t}, inner->lifetime());\n\n\tauto timezoneLabel = rpl::combine(\n\t\t_hours.value(),\n\t\tstate->timezones.value()\n\t) | rpl::map([](\n\t\t\tconst Data::WorkingHours &hours,\n\t\t\tconst Data::Timezones &timezones) {\n\t\tconst auto i = ranges::find(\n\t\t\ttimezones.list,\n\t\t\thours.timezoneId,\n\t\t\t&Data::Timezone::id);\n\t\treturn (i != end(timezones.list)) ? TimezoneFullName(*i) : QString();\n\t});\n\tconst auto editTimezone = [=](const std::vector<Data::Timezone> &list) {\n\t\tconst auto was = _hours.current().timezoneId;\n\t\tcontroller->show(Box(ChooseTimezoneBox, list, was, [=](QString id) {\n\t\t\tif (id != was) {\n\t\t\t\tauto copy = _hours.current();\n\t\t\t\tcopy.timezoneId = id;\n\t\t\t\t_hours = std::move(copy);\n\t\t\t}\n\t\t}));\n\t};\n\tAddButtonWithLabel(\n\t\tinner,\n\t\ttr::lng_hours_time_zone(),\n\t\tstd::move(timezoneLabel),\n\t\tst::settingsButtonNoIcon\n\t)->setClickedCallback([=] {\n\t\tconst auto &list = state->timezones.current().list;\n\t\tif (!list.empty()) {\n\t\t\teditTimezone(list);\n\t\t} else {\n\t\t\tstate->timezoneEditPending = true;\n\t\t}\n\t});\n\n\tif (state->timezones.current().list.empty()) {\n\t\tstate->timezones.value(\n\t\t) | rpl::filter([](const Data::Timezones &value) {\n\t\t\treturn !value.list.empty();\n\t\t}) | rpl::on_next([=](const Data::Timezones &value) {\n\t\t\tif (state->timezoneEditPending) {\n\t\t\t\tstate->timezoneEditPending = false;\n\t\t\t\teditTimezone(value.list);\n\t\t\t}\n\t\t}, inner->lifetime());\n\t}\n\n\twrap->toggleOn(enabled->toggledValue());\n\twrap->finishAnimating();\n\n\tUi::ResizeFitChild(this, content);\n}\n\nvoid WorkingHours::save() {\n\tconst auto show = controller()->uiShow();\n\tcontroller()->session().data().businessInfo().saveWorkingHours(\n\t\t_enabled.current() ? _hours.current() : Data::WorkingHours(),\n\t\t[=](QString error) { show->showToast(error); });\n}\n\n} // namespace\n\nType WorkingHoursId() {\n\treturn WorkingHours::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/business/settings_working_hours.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\n[[nodiscard]] Type WorkingHoursId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/cloud_password/settings_cloud_password_common.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/fields/password_input.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings::CloudPassword {\n\nvoid OneEdgeBoxContentDivider::skipEdge(Qt::Edge edge, bool skip) {\n\tconst auto was = _skipEdges;\n\tif (skip) {\n\t\t_skipEdges |= edge;\n\t} else {\n\t\t_skipEdges &= ~edge;\n\t}\n\tif (was != _skipEdges) {\n\t\tupdate();\n\t}\n}\n\nvoid OneEdgeBoxContentDivider::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tp.fillRect(e->rect(), Ui::BoxContentDivider::color());\n\tif (!(_skipEdges & Qt::TopEdge)) {\n\t\tUi::BoxContentDivider::paintTop(p);\n\t}\n\tif (!(_skipEdges & Qt::BottomEdge)) {\n\t\tUi::BoxContentDivider::paintBottom(p);\n\t}\n}\n\nBottomButton CreateBottomDisableButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<QRect> &&sectionGeometryValue,\n\t\trpl::producer<QString> &&buttonText,\n\t\tFn<void()> &&callback) {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(parent.get());\n\n\tUi::AddSkip(content);\n\n\tconst auto button = content->add(object_ptr<Button>(\n\t\tcontent,\n\t\tstd::move(buttonText),\n\t\tst::settingsAttentionButton));\n\tbutton->addClickHandler(std::move(callback));\n\n\tconst auto divider = Ui::CreateChild<OneEdgeBoxContentDivider>(\n\t\tparent.get());\n\tdivider->skipEdge(Qt::TopEdge, true);\n\trpl::combine(\n\t\tstd::move(sectionGeometryValue),\n\t\tparent->geometryValue(),\n\t\tcontent->geometryValue()\n\t) | rpl::on_next([=](\n\t\t\tconst QRect &r,\n\t\t\tconst QRect &parentRect,\n\t\t\tconst QRect &bottomRect) {\n\t\tconst auto top = r.y() + r.height();\n\t\tdivider->setGeometry(\n\t\t\t0,\n\t\t\ttop,\n\t\t\tr.width(),\n\t\t\tparentRect.height() - top - bottomRect.height());\n\t}, divider->lifetime());\n\tdivider->show();\n\n\treturn {\n\t\t.content = base::make_weak(content),\n\t\t.button = base::make_weak(button),\n\t\t.isBottomFillerShown = divider->geometryValue(\n\t\t) | rpl::map([](const QRect &r) {\n\t\t\treturn r.height() > 0;\n\t\t}),\n\t};\n}\n\nvoid SetupAutoCloseTimer(\n\t\trpl::lifetime &lifetime,\n\t\tFn<void()> callback,\n\t\tFn<crl::time()> lastNonIdleTime) {\n\tconstexpr auto kTimerCheck = crl::time(1000 * 60);\n\tconstexpr auto kAutoCloseTimeout = crl::time(1000 * 60 * 10);\n\n\tconst auto timer = lifetime.make_state<base::Timer>([=] {\n\t\tconst auto idle = crl::now() - lastNonIdleTime();\n\t\tif (idle >= kAutoCloseTimeout) {\n\t\t\tcallback();\n\t\t}\n\t});\n\ttimer->callEach(kTimerCheck);\n}\n\nvoid SetupHeader(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tconst QString &lottie,\n\t\trpl::producer<> &&showFinished,\n\t\trpl::producer<QString> &&subtitle,\n\t\tv::text::data &&about) {\n\tif (!lottie.isEmpty()) {\n\t\tconst auto &size = st::settingsCloudPasswordIconSize;\n\t\tauto icon = CreateLottieIcon(\n\t\t\tcontent,\n\t\t\t{ .name = lottie, .sizeOverride = { size, size } },\n\t\t\tst::settingLocalPasscodeIconPadding);\n\t\tcontent->add(std::move(icon.widget));\n\t\tstd::move(\n\t\t\tshowFinished\n\t\t) | rpl::on_next([animate = std::move(icon.animate)] {\n\t\t\tanimate(anim::repeat::once);\n\t\t}, content->lifetime());\n\t}\n\tUi::AddSkip(content);\n\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\tstd::move(subtitle),\n\t\t\tst::changePhoneTitle),\n\t\tst::changePhoneTitlePadding,\n\t\tstyle::al_top);\n\n\t{\n\t\tconst auto &st = st::settingLocalPasscodeDescription;\n\t\tconst auto description = content->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\tv::text::take_marked(std::move(about)),\n\t\t\t\tst,\n\t\t\t\tst::defaultPopupMenu),\n\t\t\tst::changePhoneDescriptionPadding,\n\t\t\tstyle::al_top);\n\t\tdescription->setTryMakeSimilarLines(true);\n\t\tdescription->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t}\n}\n\nnot_null<Ui::PasswordInput*> AddPasswordField(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\trpl::producer<QString> &&placeholder,\n\t\tconst QString &text) {\n\tconst auto &st = st::settingLocalPasscodeInputField;\n\tauto container = object_ptr<Ui::RpWidget>(content);\n\tcontainer->resize(container->width(), st.heightMin);\n\tconst auto field = Ui::CreateChild<Ui::PasswordInput>(\n\t\tcontainer.data(),\n\t\tst,\n\t\tstd::move(placeholder),\n\t\ttext);\n\n\tcontainer->geometryValue(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tfield->moveToLeft((r.width() - field->width()) / 2, 0);\n\t}, container->lifetime());\n\n\tcontent->add(std::move(container));\n\treturn field;\n}\n\nnot_null<Ui::InputField*> AddWrappedField(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\trpl::producer<QString> &&placeholder,\n\t\tconst QString &text) {\n\treturn content->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tcontent,\n\t\t\tst::settingLocalPasscodeInputField,\n\t\t\tstd::move(placeholder),\n\t\t\ttext),\n\t\tstyle::al_top);\n}\n\nnot_null<Ui::LinkButton*> AddLinkButton(\n\t\tnot_null<Ui::InputField*> input,\n\t\trpl::producer<QString> &&text) {\n\tconst auto button = Ui::CreateChild<Ui::LinkButton>(\n\t\tinput->parentWidget(),\n\t\tQString());\n\tstd::move(\n\t\ttext\n\t) | rpl::on_next([=](const QString &text) {\n\t\tbutton->setText(text);\n\t}, button->lifetime());\n\n\tinput->geometryValue(\n\t) | rpl::on_next([=](QRect r) {\n\t\tbutton->moveToLeft(r.x(), r.y() + r.height() + st::passcodeTextLine);\n\t}, button->lifetime());\n\treturn button;\n}\n\nnot_null<Ui::FlatLabel*> AddError(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tUi::PasswordInput *input) {\n\tconst auto error = content->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\tQString(),\n\t\t\tst::settingLocalPasscodeError),\n\t\tst::changePhoneDescriptionPadding,\n\t\tstyle::al_top);\n\terror->hide();\n\tif (input) {\n\t\tQObject::connect(input, &Ui::MaskedInputField::changed, [=] {\n\t\t\terror->hide();\n\t\t});\n\t}\n\treturn error;\n};\n\nnot_null<Ui::RoundButton*> AddDoneButton(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\trpl::producer<QString> &&text) {\n\tconst auto button = content->add(\n\t\tobject_ptr<Ui::RoundButton>(\n\t\t\tcontent,\n\t\t\tstd::move(text),\n\t\t\tst::changePhoneButton),\n\t\tst::settingLocalPasscodeButtonPadding,\n\t\tstyle::al_top);\n\treturn button;\n}\n\nvoid AddSkipInsteadOfField(not_null<Ui::VerticalLayout*> content) {\n\tUi::AddSkip(content, st::settingLocalPasscodeInputField.heightMin);\n}\n\nvoid AddSkipInsteadOfError(not_null<Ui::VerticalLayout*> content) {\n\tauto dummy = base::make_unique_q<Ui::FlatLabel>(\n\t\tcontent,\n\t\ttr::lng_language_name(tr::now),\n\t\tst::settingLocalPasscodeError);\n\tconst auto &padding = st::changePhoneDescriptionPadding;\n\tUi::AddSkip(content, dummy->height() + padding.top() + padding.bottom());\n\tdummy = nullptr;\n}\n\n} // namespace Settings::CloudPassword\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/text/text_variant.h\"\n#include \"ui/widgets/box_content_divider.h\"\n\nnamespace Ui {\nclass FlatLabel;\nclass InputField;\nclass LinkButton;\nclass PasswordInput;\nclass RoundButton;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Api {\nclass CloudPassword;\n} // namespace Api\n\nnamespace Settings::CloudPassword {\n\nstruct StepData {\n\tQString currentPassword;\n\tQString password;\n\tQString hint;\n\tQString email;\n\tint unconfirmedEmailLengthCode;\n\tbool setOnlyRecoveryEmail = false;\n\tbool suggestionValidate = false;\n\n\tstruct ProcessRecover {\n\t\tbool setNewPassword = false;\n\t\tQString checkedCode;\n\t\tQString emailPattern;\n\t};\n\tProcessRecover processRecover;\n};\n\nvoid SetupAutoCloseTimer(\n\trpl::lifetime &lifetime,\n\tFn<void()> callback,\n\tFn<crl::time()> lastNonIdleTime);\n\nvoid SetupHeader(\n\tnot_null<Ui::VerticalLayout*> content,\n\tconst QString &lottie,\n\trpl::producer<> &&showFinished,\n\trpl::producer<QString> &&subtitle,\n\tv::text::data &&about);\n\n[[nodiscard]] not_null<Ui::PasswordInput*> AddPasswordField(\n\tnot_null<Ui::VerticalLayout*> content,\n\trpl::producer<QString> &&placeholder,\n\tconst QString &text);\n\n[[nodiscard]] not_null<Ui::InputField*> AddWrappedField(\n\tnot_null<Ui::VerticalLayout*> content,\n\trpl::producer<QString> &&placeholder,\n\tconst QString &text);\n\n[[nodiscard]] not_null<Ui::FlatLabel*> AddError(\n\tnot_null<Ui::VerticalLayout*> content,\n\tUi::PasswordInput *input);\n\n[[nodiscard]] not_null<Ui::RoundButton*> AddDoneButton(\n\tnot_null<Ui::VerticalLayout*> content,\n\trpl::producer<QString> &&text);\n\n[[nodiscard]] not_null<Ui::LinkButton*> AddLinkButton(\n\tnot_null<Ui::InputField*> input,\n\trpl::producer<QString> &&text);\n\nvoid AddSkipInsteadOfField(not_null<Ui::VerticalLayout*> content);\nvoid AddSkipInsteadOfError(not_null<Ui::VerticalLayout*> content);\n\nstruct BottomButton {\n\tbase::weak_qptr<Ui::RpWidget> content;\n\tbase::weak_qptr<Ui::RpWidget> button;\n\trpl::producer<bool> isBottomFillerShown;\n};\n\nBottomButton CreateBottomDisableButton(\n\tnot_null<Ui::RpWidget*> parent,\n\trpl::producer<QRect> &&sectionGeometryValue,\n\trpl::producer<QString> &&buttonText,\n\tFn<void()> &&callback);\n\nclass OneEdgeBoxContentDivider : public Ui::BoxContentDivider {\npublic:\n\tusing Ui::BoxContentDivider::BoxContentDivider;\n\n\tvoid skipEdge(Qt::Edge edge, bool skip);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tQt::Edges _skipEdges;\n\n};\n\n} // namespace Settings::CloudPassword\n\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/cloud_password/settings_cloud_password_email.h\"\n\n#include \"api/api_cloud_password.h\"\n#include \"core/core_cloud_password.h\"\n#include \"lang/lang_keys.h\"\n#include \"settings/cloud_password/settings_cloud_password_common.h\"\n#include \"settings/cloud_password/settings_cloud_password_email_confirm.h\"\n#include \"settings/cloud_password/settings_cloud_password_manage.h\"\n#include \"settings/cloud_password/settings_cloud_password_step.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\n/*\nAvailable actions for follow states.\n\nCreateEmail:\n– Continue to EmailConfirm.\n+ Continue to Manage if Email is confirmed already.\n– Warn and Skip to Manage.\n– Back to CreateHint.\n\nChangeEmail from ChangePassword:\n– Continue to EmailConfirm.\n+ Continue to Manage if Email is confirmed already.\n– Warn and Skip to Manage.\n– Back to ChangeHint.\n\nChangeEmail from Manage:\n– Continue to EmailConfirm.\n+ Continue to Manage if Email is confirmed already.\n– Back to Manage.\n*/\n\nnamespace Settings {\nnamespace CloudPassword {\n\nclass Email : public TypedAbstractStep<Email> {\npublic:\n\tusing TypedAbstractStep::TypedAbstractStep;\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\tvoid setupContent();\n\nprivate:\n\trpl::lifetime _requestLifetime;\n\n};\n\nrpl::producer<QString> Email::title() {\n\treturn tr::lng_settings_cloud_password_email_title();\n}\n\nvoid Email::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tauto currentStepData = stepData();\n\tconst auto currentStepDataEmail = base::take(currentStepData.email);\n\tconst auto setOnly = base::take(currentStepData.setOnlyRecoveryEmail);\n\tsetStepData(currentStepData);\n\n\tconst auto state = cloudPassword().stateCurrent();\n\tconst auto hasRecovery = state && state->hasRecovery;\n\n\tSetupHeader(\n\t\tcontent,\n\t\tu\"cloud_password/email\"_q,\n\t\tshowFinishes(),\n\t\thasRecovery\n\t\t\t? tr::lng_settings_cloud_password_manage_email_change()\n\t\t\t: tr::lng_settings_cloud_password_email_subtitle(),\n\t\ttr::lng_settings_cloud_password_email_about());\n\n\tUi::AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip);\n\n\tconst auto newInput = AddWrappedField(\n\t\tcontent,\n\t\ttr::lng_cloud_password_email(),\n\t\tcurrentStepDataEmail);\n\tconst auto error = AddError(content, nullptr);\n\tnewInput->changes(\n\t) | rpl::on_next([=] {\n\t\terror->hide();\n\t}, newInput->lifetime());\n\tAddSkipInsteadOfField(content);\n\n\tconst auto send = [=](Fn<void()> close) {\n\t\tExpects(!_requestLifetime);\n\n\t\tconst auto data = stepData();\n\n\t\t_requestLifetime = (setOnly\n\t\t\t? cloudPassword().setEmail(data.currentPassword, data.email)\n\t\t\t: cloudPassword().set(\n\t\t\t\tdata.currentPassword,\n\t\t\t\tdata.password,\n\t\t\t\tdata.hint,\n\t\t\t\t!data.email.isEmpty(),\n\t\t\t\tdata.email)\n\t\t) | rpl::on_next_error_done([=](Api::CloudPassword::SetOk d) {\n\t\t\t_requestLifetime.destroy();\n\n\t\t\tauto data = stepData();\n\t\t\tdata.unconfirmedEmailLengthCode = d.unconfirmedEmailLengthCode;\n\t\t\tsetStepData(std::move(data));\n\t\t\tshowOther(CloudPasswordEmailConfirmId());\n\t\t}, [=](const QString &type) {\n\t\t\t_requestLifetime.destroy();\n\n\t\t\tif (MTP::IsFloodError(type)) {\n\t\t\t\terror->show();\n\t\t\t\terror->setText(tr::lng_flood_error(tr::now));\n\t\t\t} else if (AbstractStep::isPasswordInvalidError(type)) {\n\t\t\t} else if (type == u\"EMAIL_INVALID\"_q) {\n\t\t\t\terror->show();\n\t\t\t\terror->setText(tr::lng_cloud_password_bad_email(tr::now));\n\t\t\t\tnewInput->setFocus();\n\t\t\t\tnewInput->showError();\n\t\t\t\tnewInput->selectAll();\n\t\t\t}\n\t\t}, [=] {\n\t\t\t_requestLifetime.destroy();\n\n\t\t\tauto empty = StepData();\n\t\t\tempty.currentPassword = stepData().password.isEmpty()\n\t\t\t\t? stepData().currentPassword\n\t\t\t\t: stepData().password;\n\t\t\tsetStepData(std::move(empty));\n\t\t\tshowOther(CloudPasswordManageId());\n\t\t});\n\n\t\tif (close) {\n\t\t\t_requestLifetime.add(close);\n\t\t}\n\t};\n\n\tconst auto confirm = [=](const QString &email) {\n\t\tif (_requestLifetime) {\n\t\t\treturn;\n\t\t}\n\n\t\tauto data = stepData();\n\t\tdata.email = email;\n\t\tsetStepData(std::move(data));\n\n\t\tif (!email.isEmpty()) {\n\t\t\tsend(nullptr);\n\t\t\treturn;\n\t\t}\n\n\t\tcontroller()->show(Ui::MakeConfirmBox({\n\t\t\t.text = { tr::lng_cloud_password_about_recover() },\n\t\t\t.confirmed = crl::guard(this, send),\n\t\t\t.confirmText = tr::lng_cloud_password_skip_email(),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t}));\n\t};\n\n\tconst auto skip = AddLinkButton(\n\t\tnewInput,\n\t\ttr::lng_cloud_password_skip_email());\n\tskip->setClickedCallback([=] {\n\t\tconfirm(QString());\n\t});\n\tskip->setVisible(!setOnly);\n\n\tconst auto button = AddDoneButton(\n\t\tcontent,\n\t\ttr::lng_settings_cloud_password_save());\n\tbutton->setClickedCallback([=] {\n\t\tconst auto newText = newInput->getLastText();\n\t\tif (newText.isEmpty()) {\n\t\t\tnewInput->setFocus();\n\t\t\tnewInput->showError();\n\t\t} else {\n\t\t\tconfirm(newText);\n\t\t}\n\t});\n\n\tconst auto submit = [=] { button->clicked({}, Qt::LeftButton); };\n\tnewInput->submits() | rpl::on_next(submit, newInput->lifetime());\n\n\tsetFocusCallback([=] { newInput->setFocus(); });\n\n\tUi::ResizeFitChild(this, content);\n}\n\n} // namespace CloudPassword\n\nType CloudPasswordEmailId() {\n\treturn CloudPassword::Email::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\nType CloudPasswordEmailId();\n\n} // namespace Settings\n\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/cloud_password/settings_cloud_password_email_confirm.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_cloud_password.h\"\n#include \"base/unixtime.h\"\n#include \"core/core_cloud_password.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/cloud_password/settings_cloud_password_common.h\"\n#include \"settings/cloud_password/settings_cloud_password_email.h\"\n#include \"settings/cloud_password/settings_cloud_password_hint.h\"\n#include \"settings/cloud_password/settings_cloud_password_input.h\"\n#include \"settings/cloud_password/settings_cloud_password_manage.h\"\n#include \"settings/cloud_password/settings_cloud_password_start.h\"\n#include \"settings/cloud_password/settings_cloud_password_step.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/sent_code_field.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\n/*\nAvailable actions for follow states.\n\nCreateEmailConfirm from CreateEmail:\n– Continue to Manage.\n– Abort to Settings.\n– Back to Settings.\n\nChangeEmailConfirm from ChangeEmail:\n– Continue to Manage.\n– Abort to Settings.\n– Back to Settings.\n\nRecover from CreatePassword:\n– Continue to RecreateResetPassword.\n– Back to Settings.\n*/\n\nnamespace Settings {\nnamespace CloudPassword {\n\nclass EmailConfirm : public TypedAbstractStep<EmailConfirm> {\npublic:\n\tusing TypedAbstractStep::TypedAbstractStep;\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\tvoid fillTopBarMenu(\n\t\tconst Ui::Menu::MenuCallback &addAction) override;\n\n\tvoid setupContent();\n\nprotected:\n\t[[nodiscard]] rpl::producer<std::vector<Type>> removeTypes() override;\n\nprivate:\n\trpl::lifetime _requestLifetime;\n\n};\n\nrpl::producer<QString> EmailConfirm::title() {\n\treturn tr::lng_settings_cloud_password_email_title();\n}\n\nvoid EmailConfirm::fillTopBarMenu(\n\t\tconst Ui::Menu::MenuCallback &addAction) {\n\tconst auto api = &controller()->session().api();\n\tif (const auto state = api->cloudPassword().stateCurrent()) {\n\t\tif (state->unconfirmedPattern.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t}\n\taddAction(\n\t\ttr::lng_settings_password_abort(tr::now),\n\t\t[=] { api->cloudPassword().clearUnconfirmedPassword(); },\n\t\t&st::menuIconCancel);\n}\n\nrpl::producer<std::vector<Type>> EmailConfirm::removeTypes() {\n\treturn rpl::single(std::vector<Type>{\n\t\tCloudPasswordStartId(),\n\t\tCloudPasswordInputId(),\n\t\tCloudPasswordHintId(),\n\t\tCloudPasswordEmailId(),\n\t\tCloudPasswordEmailConfirmId(),\n\t\tCloudPasswordManageId(),\n\t});\n}\n\nvoid EmailConfirm::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tauto currentStepData = stepData();\n\tconst auto currentStepDataCodeLength = base::take(\n\t\tcurrentStepData.unconfirmedEmailLengthCode);\n\t// If we go back from Email Confirm to Privacy Settings\n\t// we should forget the current password.\n\tconst auto currentPassword = base::take(currentStepData.currentPassword);\n\tconst auto typedPassword = base::take(currentStepData.password);\n\tconst auto recoverEmailPattern = base::take(\n\t\tcurrentStepData.processRecover.emailPattern);\n\tsetStepData(currentStepData);\n\n\tconst auto state = cloudPassword().stateCurrent();\n\tif (!state) {\n\t\tsetStepData(StepData());\n\t\tshowBack();\n\t\treturn;\n\t}\n\tcloudPassword().state(\n\t) | rpl::on_next([=](const Core::CloudPasswordState &state) {\n\t\tif (!_requestLifetime\n\t\t\t&& state.unconfirmedPattern.isEmpty()\n\t\t\t&& recoverEmailPattern.isEmpty()) {\n\t\t\tsetStepData(StepData());\n\t\t\tshowBack();\n\t\t}\n\t}, lifetime());\n\n\tSetupHeader(\n\t\tcontent,\n\t\tu\"cloud_password/email\"_q,\n\t\tshowFinishes(),\n\t\tstate->unconfirmedPattern.isEmpty()\n\t\t\t? tr::lng_settings_cloud_password_email_recovery_subtitle()\n\t\t\t: tr::lng_cloud_password_confirm(),\n\t\ttr::lng_cloud_password_waiting_code(\n\t\t\tlt_email,\n\t\t\trpl::single(\n\t\t\t\tUi::Text::WrapEmailPattern(\n\t\t\t\t\tstate->unconfirmedPattern.isEmpty()\n\t\t\t\t\t\t? recoverEmailPattern\n\t\t\t\t\t\t: state->unconfirmedPattern)),\n\t\t\tTextWithEntities::Simple));\n\n\tUi::AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip);\n\n\tauto objectInput = object_ptr<Ui::SentCodeField>(\n\t\tcontent,\n\t\tst::settingLocalPasscodeInputField,\n\t\ttr::lng_change_phone_code_title());\n\tconst auto newInput = content->add(\n\t\tstd::move(objectInput),\n\t\tstyle::al_top);\n\n\tconst auto error = AddError(content, nullptr);\n\tnewInput->changes(\n\t) | rpl::on_next([=] {\n\t\terror->hide();\n\t}, newInput->lifetime());\n\tAddSkipInsteadOfField(content);\n\n\tconst auto resendInfo = Ui::CreateChild<Ui::FlatLabel>(\n\t\terror->parentWidget(),\n\t\ttr::lng_cloud_password_resent(tr::now),\n\t\tst::changePhoneLabel);\n\tresendInfo->hide();\n\terror->geometryValue(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tresendInfo->setGeometry(r);\n\t}, resendInfo->lifetime());\n\terror->shownValue(\n\t) | rpl::on_next([=](bool shown) {\n\t\tif (shown) {\n\t\t\tresendInfo->hide();\n\t\t}\n\t}, resendInfo->lifetime());\n\n\tconst auto resend = AddLinkButton(\n\t\tnewInput,\n\t\ttr::lng_cloud_password_resend());\n\tresend->setClickedCallback([=] {\n\t\tif (_requestLifetime) {\n\t\t\treturn;\n\t\t}\n\t\t_requestLifetime = cloudPassword().resendEmailCode(\n\t\t) | rpl::on_error_done([=](const QString &type) {\n\t\t\t_requestLifetime.destroy();\n\n\t\t\terror->show();\n\t\t\terror->setText(Lang::Hard::ServerError());\n\t\t}, [=] {\n\t\t\t_requestLifetime.destroy();\n\n\t\t\terror->hide();\n\t\t\tresendInfo->show();\n\t\t\tnewInput->hideError();\n\t\t});\n\t});\n\n\tif (!recoverEmailPattern.isEmpty()) {\n\t\tresend->setText(tr::lng_signin_try_password(tr::now));\n\n\t\tresend->setClickedCallback([=] {\n\t\t\tconst auto reset = [=](Fn<void()> close) {\n\t\t\t\tif (_requestLifetime) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t_requestLifetime = cloudPassword().resetPassword(\n\t\t\t\t) | rpl::on_next_error_done([=](\n\t\t\t\t\t\tApi::CloudPassword::ResetRetryDate retryDate) {\n\t\t\t\t\t_requestLifetime.destroy();\n\t\t\t\t\tconst auto left = std::max(\n\t\t\t\t\t\tretryDate - base::unixtime::now(),\n\t\t\t\t\t\t60);\n\t\t\t\t\tcontroller()->show(Ui::MakeInformBox(\n\t\t\t\t\t\ttr::lng_cloud_password_reset_later(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_duration,\n\t\t\t\t\t\t\tUi::FormatResetCloudPasswordIn(left))));\n\t\t\t\t}, [=](const QString &type) {\n\t\t\t\t\t_requestLifetime.destroy();\n\t\t\t\t}, [=] {\n\t\t\t\t\t_requestLifetime.destroy();\n\n\t\t\t\t\tcloudPassword().reload();\n\t\t\t\t\tusing PasswordState = Core::CloudPasswordState;\n\t\t\t\t\t_requestLifetime = cloudPassword().state(\n\t\t\t\t\t) | rpl::filter([=](const PasswordState &s) {\n\t\t\t\t\t\treturn s.pendingResetDate != 0;\n\t\t\t\t\t}) | rpl::take(\n\t\t\t\t\t\t1\n\t\t\t\t\t) | rpl::on_next([=](const PasswordState &s) {\n\t\t\t\t\t\tconst auto left = (s.pendingResetDate\n\t\t\t\t\t\t\t- base::unixtime::now());\n\t\t\t\t\t\tif (left > 0) {\n\t\t\t\t\t\t\t_requestLifetime.destroy();\n\t\t\t\t\t\t\tcontroller()->show(Ui::MakeInformBox(\n\t\t\t\t\t\t\t\ttr::lng_settings_cloud_password_reset_in(\n\t\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\t\tlt_duration,\n\t\t\t\t\t\t\t\t\tUi::FormatResetCloudPasswordIn(left))));\n\t\t\t\t\t\t\tsetStepData(StepData());\n\t\t\t\t\t\t\tshowBack();\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t\t_requestLifetime.add(close);\n\t\t\t};\n\n\t\t\tcontroller()->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_cloud_password_reset_with_email(),\n\t\t\t\t.confirmed = reset,\n\t\t\t\t.confirmText = tr::lng_cloud_password_reset_ok(),\n\t\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t}));\n\t\t});\n\t}\n\n\tconst auto button = AddDoneButton(\n\t\tcontent,\n\t\trecoverEmailPattern.isEmpty()\n\t\t\t? tr::lng_settings_cloud_password_email_confirm()\n\t\t\t: tr::lng_passcode_check_button());\n\tbutton->setClickedCallback([=] {\n\t\tconst auto newText = newInput->getDigitsOnly();\n\t\tif (newText.isEmpty()) {\n\t\t\tnewInput->setFocus();\n\t\t\tnewInput->showError();\n\t\t} else if (!_requestLifetime && recoverEmailPattern.isEmpty()) {\n\t\t\t_requestLifetime = cloudPassword().confirmEmail(\n\t\t\t\tnewText\n\t\t\t) | rpl::on_error_done([=](const QString &type) {\n\t\t\t\t_requestLifetime.destroy();\n\n\t\t\t\tnewInput->setFocus();\n\t\t\t\tnewInput->showError();\n\t\t\t\terror->show();\n\n\t\t\t\tif (MTP::IsFloodError(type)) {\n\t\t\t\t\terror->setText(tr::lng_flood_error(tr::now));\n\t\t\t\t} else if (type == u\"CODE_INVALID\"_q) {\n\t\t\t\t\terror->setText(tr::lng_signin_wrong_code(tr::now));\n\t\t\t\t} else if (type == u\"EMAIL_HASH_EXPIRED\"_q) {\n\t\t\t\t\t// Show box?\n\t\t\t\t\terror->setText(Lang::Hard::EmailConfirmationExpired());\n\t\t\t\t} else {\n\t\t\t\t\terror->setText(Lang::Hard::ServerError());\n\t\t\t\t}\n\t\t\t}, [=] {\n\t\t\t\t_requestLifetime.destroy();\n\n\t\t\t\tauto empty = StepData();\n\t\t\t\tconst auto anyPassword = currentPassword.isEmpty()\n\t\t\t\t\t? typedPassword\n\t\t\t\t\t: currentPassword;\n\t\t\t\tempty.currentPassword = anyPassword;\n\t\t\t\tsetStepData(std::move(empty));\n\t\t\t\t// If we don't have the current password\n\t\t\t\t// Then we should go to Privacy Settings.\n\t\t\t\tif (anyPassword.isEmpty()) {\n\t\t\t\t\tshowBack();\n\t\t\t\t} else {\n\t\t\t\t\tshowOther(CloudPasswordManageId());\n\t\t\t\t}\n\t\t\t});\n\t\t} else if (!_requestLifetime) {\n\t\t\t_requestLifetime = cloudPassword().checkRecoveryEmailAddressCode(\n\t\t\t\tnewText\n\t\t\t) | rpl::on_error_done([=](const QString &type) {\n\t\t\t\t_requestLifetime.destroy();\n\n\t\t\t\tnewInput->setFocus();\n\t\t\t\tnewInput->showError();\n\t\t\t\terror->show();\n\n\t\t\t\tif (MTP::IsFloodError(type)) {\n\t\t\t\t\terror->setText(tr::lng_flood_error(tr::now));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (type == u\"PASSWORD_RECOVERY_NA\"_q) {\n\t\t\t\t\tsetStepData(StepData());\n\t\t\t\t\tshowBack();\n\t\t\t\t} else if (type == u\"PASSWORD_RECOVERY_EXPIRED\"_q) {\n\t\t\t\t\tsetStepData(StepData());\n\t\t\t\t\tshowBack();\n\t\t\t\t} else if (type == u\"CODE_INVALID\"_q) {\n\t\t\t\t\terror->setText(tr::lng_signin_wrong_code(tr::now));\n\t\t\t\t} else {\n\t\t\t\t\terror->setText(Logs::DebugEnabled()\n\t\t\t\t\t\t// internal server error\n\t\t\t\t\t\t? type\n\t\t\t\t\t\t: Lang::Hard::ServerError());\n\t\t\t\t}\n\t\t\t}, [=] {\n\t\t\t\t_requestLifetime.destroy();\n\n\t\t\t\tauto empty = StepData();\n\t\t\t\tempty.processRecover.checkedCode = newText;\n\t\t\t\tempty.processRecover.setNewPassword = true;\n\t\t\t\tsetStepData(std::move(empty));\n\t\t\t\tshowOther(CloudPasswordInputId());\n\t\t\t});\n\t\t}\n\t});\n\n\tconst auto submit = [=] { button->clicked({}, Qt::LeftButton); };\n\tnewInput->setAutoSubmit(currentStepDataCodeLength, submit);\n\tnewInput->submits() | rpl::on_next(submit, newInput->lifetime());\n\n\tsetFocusCallback([=] { newInput->setFocus(); });\n\n\tUi::ResizeFitChild(this, content);\n}\n\n} // namespace CloudPassword\n\nType CloudPasswordEmailConfirmId() {\n\treturn CloudPassword::EmailConfirm::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\nType CloudPasswordEmailConfirmId();\n\n} // namespace Settings\n\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_hint.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/cloud_password/settings_cloud_password_hint.h\"\n\n#include \"api/api_cloud_password.h\"\n#include \"lang/lang_keys.h\"\n#include \"settings/cloud_password/settings_cloud_password_common.h\"\n#include \"settings/cloud_password/settings_cloud_password_email.h\"\n#include \"settings/cloud_password/settings_cloud_password_manage.h\"\n#include \"settings/cloud_password/settings_cloud_password_step.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_settings.h\"\n\n/*\nAvailable actions for follow states.\n\nCreateHint:\n– Continue to Email.\n– Skip to Email.\n– Back to CreatePassword.\n\nChangeHint:\n– Continue to Email.\n– Skip to Email.\n– Back to ChangePassword.\n\nRecreateResetHint:\n– Continue to Manage.\n– Skip to Manage.\n– Back to RecreateResetPassword.\n*/\n\nnamespace Settings {\nnamespace CloudPassword {\n\nclass Hint : public TypedAbstractStep<Hint> {\npublic:\n\tusing TypedAbstractStep::TypedAbstractStep;\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\tvoid setupContent();\n\nprivate:\n\trpl::lifetime _requestLifetime;\n\n};\n\nrpl::producer<QString> Hint::title() {\n\treturn tr::lng_settings_cloud_password_hint_title();\n}\n\nvoid Hint::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tauto currentStepData = stepData();\n\tconst auto currentStepDataHint = base::take(currentStepData.hint);\n\tsetStepData(currentStepData);\n\n\tSetupHeader(\n\t\tcontent,\n\t\tu\"cloud_password/hint\"_q,\n\t\tshowFinishes(),\n\t\ttr::lng_settings_cloud_password_hint_subtitle(),\n\t\ttr::lng_settings_cloud_password_hint_about());\n\n\tUi::AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip);\n\n\tconst auto newInput = AddWrappedField(\n\t\tcontent,\n\t\ttr::lng_cloud_password_hint(),\n\t\tcurrentStepDataHint);\n\tconst auto error = AddError(content, nullptr);\n\tnewInput->changes(\n\t) | rpl::on_next([=] {\n\t\terror->hide();\n\t}, newInput->lifetime());\n\tAddSkipInsteadOfField(content);\n\n\tconst auto save = [=](const QString &hint) {\n\t\tif (currentStepData.processRecover.setNewPassword) {\n\t\t\tif (_requestLifetime) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_requestLifetime = cloudPassword().recoverPassword(\n\t\t\t\tcurrentStepData.processRecover.checkedCode,\n\t\t\t\tcurrentStepData.password,\n\t\t\t\thint\n\t\t\t) | rpl::on_error_done([=](const QString &type) {\n\t\t\t\t_requestLifetime.destroy();\n\n\t\t\t\terror->show();\n\t\t\t\tif (MTP::IsFloodError(type)) {\n\t\t\t\t\terror->setText(tr::lng_flood_error(tr::now));\n\t\t\t\t} else {\n\t\t\t\t\terror->setText(Lang::Hard::ServerError());\n\t\t\t\t}\n\t\t\t}, [=] {\n\t\t\t\t_requestLifetime.destroy();\n\n\t\t\t\tauto empty = StepData();\n\t\t\t\tempty.currentPassword = stepData().password;\n\t\t\t\tsetStepData(std::move(empty));\n\t\t\t\tshowOther(CloudPasswordManageId());\n\t\t\t});\n\t\t} else {\n\t\t\tauto data = stepData();\n\t\t\tdata.hint = hint;\n\t\t\tsetStepData(std::move(data));\n\t\t\tshowOther(CloudPasswordEmailId());\n\t\t}\n\t};\n\n\tAddLinkButton(\n\t\tnewInput,\n\t\ttr::lng_settings_cloud_password_skip_hint()\n\t)->setClickedCallback([=] {\n\t\tsave(QString());\n\t});\n\n\tconst auto button = AddDoneButton(content, tr::lng_continue());\n\tbutton->setClickedCallback([=] {\n\t\tconst auto newText = newInput->getLastText();\n\t\tif (newText.isEmpty()) {\n\t\t\tnewInput->setFocus();\n\t\t\tnewInput->showError();\n\t\t} else if (newText == stepData().password) {\n\t\t\terror->show();\n\t\t\terror->setText(tr::lng_cloud_password_bad(tr::now));\n\t\t\tnewInput->setFocus();\n\t\t\tnewInput->showError();\n\t\t} else {\n\t\t\tsave(newText);\n\t\t}\n\t});\n\n\tconst auto submit = [=] { button->clicked({}, Qt::LeftButton); };\n\tnewInput->submits() | rpl::on_next(submit, newInput->lifetime());\n\n\tsetFocusCallback([=] { newInput->setFocus(); });\n\n\tUi::ResizeFitChild(this, content);\n}\n\n} // namespace CloudPassword\n\nType CloudPasswordHintId() {\n\treturn CloudPassword::Hint::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_hint.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\nType CloudPasswordHintId();\n\n} // namespace Settings\n\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/cloud_password/settings_cloud_password_input.h\"\n\n#include \"api/api_cloud_password.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"base/timer.h\"\n#include \"base/unixtime.h\"\n#include \"core/core_cloud_password.h\"\n#include \"data/components/promo_suggestions.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_session.h\"\n#include \"settings/cloud_password/settings_cloud_password_common.h\"\n#include \"settings/cloud_password/settings_cloud_password_email_confirm.h\"\n#include \"settings/cloud_password/settings_cloud_password_hint.h\"\n#include \"settings/cloud_password/settings_cloud_password_manage.h\"\n#include \"settings/cloud_password/settings_cloud_password_step.h\"\n#include \"settings/cloud_password/settings_cloud_password_validate_icon.h\"\n#include \"ui/boxes/boost_box.h\" // Ui::StartFireworks.\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/password_input.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\n/*\nAvailable actions for follow states.\n\nCreatePassword:\n– Continue to CreateHint.\n– Back to Start.\n\nChangePassword:\n– Continue to ChangeHint.\n– Back to Manage.\n\nCheckPassword:\n– Continue to Manage.\n– Recover to EmailConfirm.\n– Reset and wait (+ Cancel reset).\n– Reset now and Back to Settings.\n– Back to Settings.\n\nRecreateResetPassword:\n– Continue to RecreateResetHint.\n– Clear password and Back to Settings.\n– Back to Settings.\n\nValidatePassword:\n- Submit to show good validate.\n- Back to Main Settings.\n*/\n\nnamespace Settings {\nnamespace CloudPassword {\nnamespace {\n\nstruct Icon {\n\tnot_null<Lottie::Icon*> icon;\n\tFn<void()> update;\n};\n\nIcon CreateInteractiveLottieIcon(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tLottie::IconDescriptor &&descriptor,\n\t\tstyle::margins padding) {\n\tauto object = object_ptr<Ui::RpWidget>(container);\n\tconst auto raw = object.data();\n\n\tconst auto width = descriptor.sizeOverride.width();\n\traw->resize((Rect(descriptor.sizeOverride) + padding).size());\n\n\tauto owned = Lottie::MakeIcon(std::move(descriptor));\n\tconst auto icon = owned.get();\n\n\traw->lifetime().add([kept = std::move(owned)]{});\n\n\traw->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\tconst auto left = (raw->width() - width) / 2;\n\t\ticon->paint(p, left, padding.top());\n\t}, raw->lifetime());\n\n\tcontainer->add(std::move(object));\n\treturn { .icon = icon, .update = [=] { raw->update(); } };\n}\n\n[[nodiscard]] not_null<Ui::LinkButton*> AddLinkButton(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tnot_null<Ui::PasswordInput*> input) {\n\tconst auto button = Ui::CreateChild<Ui::LinkButton>(\n\t\tcontent.get(),\n\t\tQString());\n\n\trpl::merge(\n\t\tcontent->geometryValue(),\n\t\tinput->geometryValue()\n\t) | rpl::on_next([=] {\n\t\tconst auto topLeft = input->mapTo(content, input->pos());\n\t\tbutton->moveToLeft(\n\t\t\tinput->pos().x(),\n\t\t\ttopLeft.y() + input->height() + st::passcodeTextLine);\n\t}, button->lifetime());\n\treturn button;\n}\n\n} // namespace\n\nclass Input : public TypedAbstractStep<Input> {\npublic:\n\tusing TypedAbstractStep::TypedAbstractStep;\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\t[[nodiscard]] base::weak_qptr<Ui::RpWidget> createPinnedToTop(\n\t\tnot_null<QWidget*> parent) override;\n\tvoid setupContent();\n\tvoid setupValidateGood();\n\nprotected:\n\t[[nodiscard]] rpl::producer<std::vector<Type>> removeTypes() override;\n\nprivate:\n\tvoid setupRecoverButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Ui::LinkButton*> button,\n\t\tnot_null<Ui::FlatLabel*> info,\n\t\tFn<void()> recoverCallback);\n\n\tQWidget *_parent = nullptr;\n\n\trpl::variable<std::vector<Type>> _removesFromStack;\n\trpl::lifetime _requestLifetime;\n\n};\n\nrpl::producer<std::vector<Type>> Input::removeTypes() {\n\treturn _removesFromStack.value();\n}\n\nrpl::producer<QString> Input::title() {\n\treturn tr::lng_settings_cloud_password_password_title();\n}\n\nbase::weak_qptr<Ui::RpWidget> Input::createPinnedToTop(\n\t\tnot_null<QWidget*> parent) {\n\t_parent = parent;\n\treturn nullptr;\n}\n\nvoid Input::setupValidateGood() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tif (_parent) {\n\t\tUi::StartFireworks(_parent);\n\t}\n\n\tif (auto owned = CreateValidateGoodIcon(&controller()->session())) {\n\t\tcontent->add(\n\t\t\tstd::move(owned),\n\t\t\tQMargins(0, st::lineWidth * 75, 0, 0));\n\t}\n\n\tSetupHeader(\n\t\tcontent,\n\t\tQString(),\n\t\trpl::never<>(),\n\t\ttr::lng_settings_suggestion_password_step_finish_title(),\n\t\ttr::lng_settings_suggestion_password_step_finish_about());\n\n\tconst auto button = AddDoneButton(content, tr::lng_share_done());\n\tbutton->setClickedCallback([=] {\n\t\tshowBack();\n\t});\n\n\tUi::ToggleChildrenVisibility(this, true);\n\tUi::ResizeFitChild(this, content);\n\tcontent->resizeToWidth(width());\n\tUi::SendPendingMoveResizeEvents(content);\n}\n\nvoid Input::setupContent() {\n\tif (QWidget::children().count() > 0) {\n\t\treturn;\n\t}\n\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tauto currentStepData = stepData();\n\tconst auto currentStepDataPassword = base::take(currentStepData.password);\n\tconst auto currentStepProcessRecover = base::take(\n\t\tcurrentStepData.processRecover);\n\tconst auto currentStepValidate = base::take(\n\t\tcurrentStepData.suggestionValidate);\n\tsetStepData(currentStepData);\n\n\tconst auto currentState = cloudPassword().stateCurrent();\n\tconst auto hasPassword = !currentStepProcessRecover.setNewPassword\n\t\t&& (currentState ? currentState->hasPassword : false);\n\tconst auto isCheck = currentStepData.currentPassword.isEmpty()\n\t\t&& hasPassword\n\t\t&& !currentStepProcessRecover.setNewPassword;\n\n\tif (currentStepProcessRecover.setNewPassword) {\n\t\t_removesFromStack = std::vector<Type>{\n\t\t\tCloudPasswordEmailConfirmId()\n\t\t};\n\t}\n\n\tconst auto icon = CreateInteractiveLottieIcon(\n\t\tcontent,\n\t\t{\n\t\t\t.name = currentStepValidate\n\t\t\t\t? u\"cloud_password/validate\"_q\n\t\t\t\t: u\"cloud_password/password_input\"_q,\n\t\t\t.sizeOverride = Size(st::settingsCloudPasswordIconSize),\n\t\t},\n\t\tst::settingLocalPasscodeIconPadding);\n\n\tSetupHeader(\n\t\tcontent,\n\t\tQString(),\n\t\trpl::never<>(),\n\t\tcurrentStepValidate\n\t\t\t? tr::lng_settings_suggestion_password_step_input_title()\n\t\t\t: isCheck\n\t\t\t? tr::lng_settings_cloud_password_check_subtitle()\n\t\t\t: hasPassword\n\t\t\t? tr::lng_settings_cloud_password_manage_password_change()\n\t\t\t: tr::lng_settings_cloud_password_password_subtitle(),\n\t\tcurrentStepValidate\n\t\t\t? tr::lng_settings_suggestion_password_step_input_about()\n\t\t\t: isCheck\n\t\t\t? tr::lng_settings_cloud_password_manage_about1()\n\t\t\t: tr::lng_cloud_password_about());\n\n\tUi::AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip);\n\n\tconst auto newInput = AddPasswordField(\n\t\tcontent,\n\t\t(isCheck\n\t\t\t? tr::lng_cloud_password_enter_old()\n\t\t\t: tr::lng_cloud_password_enter_new()),\n\t\tcurrentStepDataPassword);\n\tconst auto reenterInput = isCheck\n\t\t? (Ui::PasswordInput*)(nullptr)\n\t\t: AddPasswordField(\n\t\t\tcontent,\n\t\t\ttr::lng_cloud_password_confirm_new(),\n\t\t\tcurrentStepDataPassword).get();\n\tconst auto error = AddError(content, newInput);\n\tif (reenterInput) {\n\t\tQObject::connect(reenterInput, &Ui::MaskedInputField::changed, [=] {\n\t\t\terror->hide();\n\t\t});\n\t}\n\n\tif (isCheck) {\n\t\tAddSkipInsteadOfField(content);\n\n\t\tconst auto hint = currentState ? currentState->hint : QString();\n\t\tconst auto hintInfo = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\terror->parentWidget(),\n\t\t\ttr::lng_signin_hint(tr::now, lt_password_hint, hint),\n\t\t\tst::defaultFlatLabel);\n\t\thintInfo->setVisible(!hint.isEmpty());\n\t\trpl::combine(\n\t\t\terror->geometryValue(),\n\t\t\tnewInput->geometryValue()\n\t\t) | rpl::on_next([=](QRect r, QRect input) {\n\t\t\thintInfo->setGeometry(\n\t\t\t\t{ input.x(), r.y(), input.width(), r.height() });\n\t\t}, hintInfo->lifetime());\n\t\terror->shownValue(\n\t\t) | rpl::on_next([=](bool shown) {\n\t\t\tif (shown) {\n\t\t\t\thintInfo->hide();\n\t\t\t} else {\n\t\t\t\thintInfo->setVisible(!hint.isEmpty());\n\t\t\t}\n\t\t}, hintInfo->lifetime());\n\n\t\tauto recoverCallback = [=] {\n\t\t\tif (_requestLifetime) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto state = cloudPassword().stateCurrent();\n\t\t\tif (!state) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state->hasRecovery) {\n\t\t\t\t_requestLifetime = cloudPassword().requestPasswordRecovery(\n\t\t\t\t) | rpl::on_next_error([=](const QString &pattern) {\n\t\t\t\t\t_requestLifetime.destroy();\n\n\t\t\t\t\tauto data = stepData();\n\t\t\t\t\tdata.processRecover = currentStepProcessRecover;\n\t\t\t\t\tdata.processRecover.emailPattern = pattern;\n\t\t\t\t\tsetStepData(std::move(data));\n\t\t\t\t\tshowOther(CloudPasswordEmailConfirmId());\n\t\t\t\t}, [=](const QString &type) {\n\t\t\t\t\t_requestLifetime.destroy();\n\n\t\t\t\t\terror->show();\n\t\t\t\t\tif (MTP::IsFloodError(type)) {\n\t\t\t\t\t\terror->setText(tr::lng_flood_error(tr::now));\n\t\t\t\t\t} else {\n\t\t\t\t\t\terror->setText(Lang::Hard::ServerError());\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconst auto callback = [=](Fn<void()> close) {\n\t\t\t\t\tif (_requestLifetime) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tclose();\n\t\t\t\t\t_requestLifetime = cloudPassword().resetPassword(\n\t\t\t\t\t) | rpl::on_next_error_done([=](\n\t\t\t\t\t\t\tApi::CloudPassword::ResetRetryDate retryDate) {\n\t\t\t\t\t\t_requestLifetime.destroy();\n\t\t\t\t\t\tconst auto left = std::max(\n\t\t\t\t\t\t\tretryDate - base::unixtime::now(),\n\t\t\t\t\t\t\t60);\n\t\t\t\t\t\tcontroller()->show(Ui::MakeInformBox(\n\t\t\t\t\t\t\ttr::lng_cloud_password_reset_later(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_duration,\n\t\t\t\t\t\t\t\tUi::FormatResetCloudPasswordIn(left))));\n\t\t\t\t\t}, [=](const QString &type) {\n\t\t\t\t\t\t_requestLifetime.destroy();\n\t\t\t\t\t}, [=] {\n\t\t\t\t\t\t_requestLifetime.destroy();\n\t\t\t\t\t});\n\t\t\t\t};\n\t\t\t\tcontroller()->show(Ui::MakeConfirmBox({\n\t\t\t\t\t.text = tr::lng_cloud_password_reset_no_email(),\n\t\t\t\t\t.confirmed = callback,\n\t\t\t\t\t.confirmText = tr::lng_cloud_password_reset_ok(),\n\t\t\t\t\t.cancelText = tr::lng_cancel(),\n\t\t\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t\t}));\n\t\t\t}\n\t\t};\n\n\t\tconst auto recover = AddLinkButton(content, newInput);\n\t\tconst auto resetInfo = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\tQString(),\n\t\t\tst::boxDividerLabel);\n\t\trecover->geometryValue(\n\t\t) | rpl::on_next([=](const QRect &r) {\n\t\t\tresetInfo->moveToLeft(r.x(), r.y() + st::passcodeTextLine);\n\t\t}, resetInfo->lifetime());\n\n\t\tsetupRecoverButton(\n\t\t\tcontent,\n\t\t\trecover,\n\t\t\tresetInfo,\n\t\t\tstd::move(recoverCallback));\n\t} else if (currentStepProcessRecover.setNewPassword && reenterInput) {\n\t\tconst auto skip = AddLinkButton(content, reenterInput);\n\t\tskip->setText(tr::lng_settings_auto_night_disable(tr::now));\n\t\tskip->setClickedCallback([=] {\n\t\t\tif (_requestLifetime) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_requestLifetime = cloudPassword().recoverPassword(\n\t\t\t\tcurrentStepProcessRecover.checkedCode,\n\t\t\t\tQString(),\n\t\t\t\tQString()\n\t\t\t) | rpl::on_error_done([=](const QString &type) {\n\t\t\t\t_requestLifetime.destroy();\n\n\t\t\t\terror->show();\n\t\t\t\tif (MTP::IsFloodError(type)) {\n\t\t\t\t\terror->setText(tr::lng_flood_error(tr::now));\n\t\t\t\t} else {\n\t\t\t\t\terror->setText(Lang::Hard::ServerError());\n\t\t\t\t}\n\t\t\t}, [=] {\n\t\t\t\t_requestLifetime.destroy();\n\n\t\t\t\tcontroller()->show(\n\t\t\t\t\tUi::MakeInformBox(tr::lng_cloud_password_removed()));\n\t\t\t\tsetStepData(StepData());\n\t\t\t\tshowBack();\n\t\t\t});\n\t\t});\n\t\tUi::AddSkip(content);\n\t}\n\n\tif (currentStepValidate) {\n\t\ticon.icon->animate(icon.update, 0, icon.icon->framesCount() - 1);\n\t} else if (!newInput->text().isEmpty()) {\n\t\ticon.icon->jumpTo(icon.icon->framesCount() / 2, icon.update);\n\t}\n\n\tconst auto checkPassword = [=](const QString &pass) {\n\t\tif (_requestLifetime) {\n\t\t\treturn;\n\t\t}\n\t\t_requestLifetime = cloudPassword().check(\n\t\t\tpass\n\t\t) | rpl::on_error_done([=](const QString &type) {\n\t\t\t_requestLifetime.destroy();\n\n\t\t\tnewInput->setFocus();\n\t\t\tnewInput->showError();\n\t\t\tnewInput->selectAll();\n\t\t\terror->show();\n\t\t\tif (MTP::IsFloodError(type)) {\n\t\t\t\terror->setText(tr::lng_flood_error(tr::now));\n\t\t\t} else if (type == u\"PASSWORD_HASH_INVALID\"_q\n\t\t\t\t|| type == u\"SRP_PASSWORD_CHANGED\"_q) {\n\t\t\t\terror->setText(tr::lng_cloud_password_wrong(tr::now));\n\t\t\t} else {\n\t\t\t\terror->setText(Lang::Hard::ServerError());\n\t\t\t}\n\t\t}, [=] {\n\t\t\t_requestLifetime.destroy();\n\n\t\t\tif (const auto state = cloudPassword().stateCurrent()) {\n\t\t\t\tif (state->pendingResetDate > 0) {\n\t\t\t\t\tauto lifetime = rpl::lifetime();\n\t\t\t\t\tlifetime = cloudPassword().cancelResetPassword(\n\t\t\t\t\t) | rpl::on_next([] {});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (currentStepValidate) {\n\t\t\t\tcontroller()->session().promoSuggestions().dismiss(\n\t\t\t\t\tData::PromoSuggestions::SugValidatePassword());\n\t\t\t\tsetupValidateGood();\n\t\t\t\tdelete content;\n\t\t\t} else {\n\t\t\t\tauto data = stepData();\n\t\t\t\tdata.currentPassword = pass;\n\t\t\t\tsetStepData(std::move(data));\n\t\t\t\tshowOther(CloudPasswordManageId());\n\t\t\t}\n\n\t\t});\n\t};\n\n\tconst auto button = AddDoneButton(\n\t\tcontent,\n\t\tisCheck ? tr::lng_passcode_check_button() : tr::lng_continue());\n\tbutton->setClickedCallback([=] {\n\t\tconst auto newText = newInput->text();\n\t\tconst auto reenterText = isCheck ? QString() : reenterInput->text();\n\t\tif (newText.isEmpty()) {\n\t\t\tnewInput->setFocus();\n\t\t\tnewInput->showError();\n\t\t} else if (reenterInput && reenterText.isEmpty()) {\n\t\t\treenterInput->setFocus();\n\t\t\treenterInput->showError();\n\t\t} else if (reenterInput && (newText != reenterText)) {\n\t\t\treenterInput->setFocus();\n\t\t\treenterInput->showError();\n\t\t\treenterInput->selectAll();\n\t\t\terror->show();\n\t\t\terror->setText(tr::lng_cloud_password_differ(tr::now));\n\t\t} else if (isCheck) {\n\t\t\tcheckPassword(newText);\n\t\t} else {\n\t\t\tauto data = stepData();\n\t\t\tdata.processRecover = currentStepProcessRecover;\n\t\t\tdata.password = newText;\n\t\t\tsetStepData(std::move(data));\n\t\t\tshowOther(CloudPasswordHintId());\n\t\t}\n\t});\n\n\tif (!currentStepValidate) {\n\t\tbase::qt_signal_producer(\n\t\t\tnewInput.get(),\n\t\t\t&QLineEdit::textChanged // Covers Undo.\n\t\t) | rpl::map([=] {\n\t\t\treturn newInput->text().isEmpty();\n\t\t}) | rpl::distinct_until_changed(\n\t\t) | rpl::on_next([=](bool empty) {\n\t\t\tconst auto from = icon.icon->frameIndex();\n\t\t\tconst auto to = empty ? 0 : (icon.icon->framesCount() / 2 - 1);\n\t\t\ticon.icon->animate(icon.update, from, to);\n\t\t}, content->lifetime());\n\t}\n\n\tconst auto submit = [=] {\n\t\tif (!reenterInput || reenterInput->hasFocus()) {\n\t\t\tbutton->clicked({}, Qt::LeftButton);\n\t\t} else {\n\t\t\treenterInput->setFocus();\n\t\t}\n\t};\n\tQObject::connect(newInput, &Ui::MaskedInputField::submitted, submit);\n\tif (reenterInput) {\n\t\tusing namespace Ui;\n\t\tQObject::connect(reenterInput, &MaskedInputField::submitted, submit);\n\t}\n\n\tsetFocusCallback(crl::guard(content, [=] {\n\t\tif (isCheck || newInput->text().isEmpty()) {\n\t\t\tnewInput->setFocus();\n\t\t} else if (reenterInput->text().isEmpty()) {\n\t\t\treenterInput->setFocus();\n\t\t} else {\n\t\t\tnewInput->setFocus();\n\t\t}\n\t}));\n\n\tUi::ResizeFitChild(this, content);\n}\n\nvoid Input::setupRecoverButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Ui::LinkButton*> button,\n\t\tnot_null<Ui::FlatLabel*> info,\n\t\tFn<void()> recoverCallback) {\n\n\tstruct Status {\n\t\tenum class SuggestAction {\n\t\t\tRecover,\n\t\t\tReset,\n\t\t\tCancelReset,\n\t\t};\n\t\tSuggestAction suggest = SuggestAction::Recover;\n\t\tTimeId left = 0;\n\t};\n\n\tstruct State {\n\t\tbase::Timer timer;\n\t\trpl::variable<Status> status;\n\t};\n\n\tconst auto state = container->lifetime().make_state<State>();\n\n\tconst auto updateStatus = [=] {\n\t\tconst auto passwordState = cloudPassword().stateCurrent();\n\t\tconst auto date = passwordState ? passwordState->pendingResetDate : 0;\n\t\tconst auto left = (date - base::unixtime::now());\n\t\tstate->status = Status{\n\t\t\t.suggest = ((left > 0)\n\t\t\t\t? Status::SuggestAction::CancelReset\n\t\t\t\t: date\n\t\t\t\t? Status::SuggestAction::Reset\n\t\t\t\t: Status::SuggestAction::Recover),\n\t\t\t.left = left,\n\t\t};\n\t};\n\tstate->timer.setCallback(updateStatus);\n\tupdateStatus();\n\n\tstate->status.value(\n\t) | rpl::on_next([=](const Status &status) {\n\t\tswitch (status.suggest) {\n\t\tcase Status::SuggestAction::Recover: {\n\t\t\tinfo->setText(QString());\n\t\t\tbutton->setText(tr::lng_signin_recover(tr::now));\n\t\t} break;\n\t\tcase Status::SuggestAction::Reset: {\n\t\t\tinfo->setText(QString());\n\t\t\tbutton->setText(tr::lng_cloud_password_reset_ready(tr::now));\n\t\t} break;\n\t\tcase Status::SuggestAction::CancelReset: {\n\t\t\tinfo->setText(\n\t\t\t\ttr::lng_settings_cloud_password_reset_in(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_duration,\n\t\t\t\t\tUi::FormatResetCloudPasswordIn(status.left)));\n\t\t\tbutton->setText(\n\t\t\t\ttr::lng_cloud_password_reset_cancel_title(tr::now));\n\t\t} break;\n\t\t}\n\t}, container->lifetime());\n\n\tcloudPassword().state(\n\t) | rpl::on_next([=](const Core::CloudPasswordState &passState) {\n\t\tupdateStatus();\n\t\tstate->timer.cancel();\n\t\tif (passState.pendingResetDate) {\n\t\t\tstate->timer.callEach(999);\n\t\t}\n\t}, container->lifetime());\n\n\tbutton->setClickedCallback([=] {\n\t\tconst auto passState = cloudPassword().stateCurrent();\n\t\tif (_requestLifetime || !passState) {\n\t\t\treturn;\n\t\t}\n\t\tupdateStatus();\n\t\tconst auto suggest = state->status.current().suggest;\n\t\tif (suggest == Status::SuggestAction::Recover) {\n\t\t\trecoverCallback();\n\t\t} else if (suggest == Status::SuggestAction::CancelReset) {\n\t\t\tconst auto cancel = [=](Fn<void()> close) {\n\t\t\t\tif (_requestLifetime) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tclose();\n\t\t\t\t_requestLifetime = cloudPassword().cancelResetPassword(\n\t\t\t\t) | rpl::on_error_done([=](const QString &error) {\n\t\t\t\t\t_requestLifetime.destroy();\n\t\t\t\t}, [=] {\n\t\t\t\t\t_requestLifetime.destroy();\n\t\t\t\t});\n\t\t\t};\n\t\t\tcontroller()->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_cloud_password_reset_cancel_sure(),\n\t\t\t\t.confirmed = cancel,\n\t\t\t\t.confirmText = tr::lng_box_yes(),\n\t\t\t\t.cancelText = tr::lng_box_no(),\n\t\t\t}));\n\t\t} else if (suggest == Status::SuggestAction::Reset) {\n\t\t\t_requestLifetime = cloudPassword().resetPassword(\n\t\t\t) | rpl::on_next_error_done([=](\n\t\t\t\t\tApi::CloudPassword::ResetRetryDate retryDate) {\n\t\t\t\t_requestLifetime.destroy();\n\t\t\t\tconst auto left = std::max(\n\t\t\t\t\tretryDate - base::unixtime::now(),\n\t\t\t\t\t60);\n\t\t\t\tcontroller()->show(Ui::MakeInformBox(\n\t\t\t\t\ttr::lng_cloud_password_reset_later(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_duration,\n\t\t\t\t\t\tUi::FormatResetCloudPasswordIn(left))));\n\t\t\t}, [=](const QString &type) {\n\t\t\t\t_requestLifetime.destroy();\n\t\t\t}, [=] {\n\t\t\t\t_requestLifetime.destroy();\n\n\t\t\t\tcloudPassword().reload();\n\t\t\t\tusing PasswordState = Core::CloudPasswordState;\n\t\t\t\t_requestLifetime = cloudPassword().state(\n\t\t\t\t) | rpl::filter([=](const PasswordState &s) {\n\t\t\t\t\treturn !s.hasPassword;\n\t\t\t\t}) | rpl::take(\n\t\t\t\t\t1\n\t\t\t\t) | rpl::on_next([=](const PasswordState &s) {\n\t\t\t\t\t_requestLifetime.destroy();\n\t\t\t\t\tcontroller()->show(Ui::MakeInformBox(\n\t\t\t\t\t\ttr::lng_cloud_password_removed()));\n\t\t\t\t\tsetStepData(StepData());\n\t\t\t\t\tshowBack();\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t});\n}\n\nclass SuggestionInput : public Input {\npublic:\n\tSuggestionInput(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller)\n\t: Input(parent, controller)\n\t, _stepData(StepData{ .suggestionValidate = true }) {\n\t\tsetStepDataReference(_stepData);\n\t}\n\n\t[[nodiscard]] static Type Id() {\n\t\treturn SectionFactory<SuggestionInput>::Instance();\n\t}\n\nprivate:\n\tstd::any _stepData;\n\n};\n\n} // namespace CloudPassword\n\nType CloudPasswordInputId() {\n\treturn CloudPassword::Input::Id();\n}\n\nType CloudPasswordSuggestionInputId() {\n\treturn CloudPassword::SuggestionInput::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\nType CloudPasswordInputId();\nType CloudPasswordSuggestionInputId();\n\n} // namespace Settings\n\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_login_email.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/cloud_password/settings_cloud_password_login_email.h\"\n\n#include \"api/api_cloud_password.h\"\n#include \"core/core_cloud_password.h\"\n#include \"info/channel_statistics/boosts/giveaway/boost_badge.h\" // InfiniteRadialAnimationWidget.\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/cloud_password/settings_cloud_password_common.h\"\n#include \"settings/cloud_password/settings_cloud_password_login_email_confirm.h\"\n#include \"settings/cloud_password/settings_cloud_password_manage.h\"\n#include \"settings/cloud_password/settings_cloud_password_step.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace CloudPassword {\n\nclass LoginEmail : public TypedAbstractStep<LoginEmail> {\npublic:\n\tusing TypedAbstractStep::TypedAbstractStep;\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\tvoid setupContent();\n\nprivate:\n\trpl::lifetime _requestLifetime;\n\tstd::optional<MTP::Sender> _api;\n\trpl::variable<bool> _confirmButtonBusy = false;\n\n};\n\nrpl::producer<QString> LoginEmail::title() {\n\treturn tr::lng_settings_cloud_login_email_section_title();\n}\n\nvoid LoginEmail::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tauto currentStepData = stepData();\n\tconst auto newEmail = base::take(currentStepData.email);\n\tsetStepData(currentStepData);\n\n\tSetupHeader(\n\t\tcontent,\n\t\tu\"cloud_password/email\"_q,\n\t\tshowFinishes(),\n\t\ttr::lng_settings_cloud_login_email_title(),\n\t\ttr::lng_settings_cloud_login_email_about());\n\n\tUi::AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip);\n\n\tconst auto newInput = AddWrappedField(\n\t\tcontent,\n\t\ttr::lng_settings_cloud_login_email_placeholder(),\n\t\tQString());\n\tconst auto error = AddError(content, nullptr);\n\tnewInput->changes() | rpl::on_next([=] {\n\t\terror->hide();\n\t}, newInput->lifetime());\n\tnewInput->setText(newEmail);\n\tif (newInput->hasText()) {\n\t\tnewInput->selectAll();\n\t}\n\tAddSkipInsteadOfField(content);\n\n\tconst auto send = [=] {\n\t\tExpects(_api == std::nullopt);\n\n\t\t_confirmButtonBusy = true;\n\t\t_api.emplace(&controller()->session().mtp());\n\n\t\tconst auto data = stepData();\n\n\t\tconst auto done = [=](int length, const QString &pattern) {\n\t\t\t_api.reset();\n\t\t\t_confirmButtonBusy = false;\n\t\t\tauto data = stepData();\n\t\t\tdata.unconfirmedEmailLengthCode = length;\n\t\t\tsetStepData(std::move(data));\n\t\t\tshowOther(CloudLoginEmailConfirmId());\n\t\t};\n\t\tconst auto fail = [=](const QString &type) {\n\t\t\t_api.reset();\n\t\t\t_confirmButtonBusy = false;\n\n\t\t\tif (MTP::IsFloodError(type)) {\n\t\t\t\terror->show();\n\t\t\t\terror->setText(tr::lng_flood_error(tr::now));\n\t\t\t} else if (AbstractStep::isPasswordInvalidError(type)) {\n\t\t\t} else if (type == u\"EMAIL_INVALID\"_q) {\n\t\t\t\terror->show();\n\t\t\t\terror->setText(tr::lng_cloud_password_bad_email(tr::now));\n\t\t\t\tnewInput->setFocus();\n\t\t\t\tnewInput->showError();\n\t\t\t\tnewInput->selectAll();\n\t\t\t} else if (type == u\"EMAIL_NOT_SETUP\"_q) {\n\t\t\t\terror->show();\n\t\t\t\terror->setText(Lang::Hard::ServerError());\n\t\t\t\tnewInput->setFocus();\n\t\t\t\tnewInput->showError();\n\t\t\t\tnewInput->selectAll();\n\t\t\t}\n\t\t};\n\n\t\tApi::RequestLoginEmailCode(*_api, data.email, done, fail);\n\t};\n\n\tconst auto confirm = [=](const QString &email) {\n\t\tif (_confirmButtonBusy.current()) {\n\t\t\treturn;\n\t\t}\n\n\t\tauto data = stepData();\n\t\tdata.email = email;\n\t\tsetStepData(std::move(data));\n\n\t\tif (!email.isEmpty()) {\n\t\t\tsend();\n\t\t\treturn;\n\t\t}\n\t};\n\n\tconst auto button = AddDoneButton(\n\t\tcontent,\n\t\trpl::conditional(\n\t\t\t_confirmButtonBusy.value(),\n\t\t\trpl::single(QString()),\n\t\t\ttr::lng_settings_cloud_login_email_confirm()));\n\tbutton->setClickedCallback([=] {\n\t\tconst auto newText = newInput->getLastText();\n\t\tif (newText.isEmpty()) {\n\t\t\tnewInput->setFocus();\n\t\t\tnewInput->showError();\n\t\t} else {\n\t\t\tconfirm(newText);\n\t\t}\n\t});\n\t{\n\t\tusing namespace Info::Statistics;\n\t\tconst auto loadingAnimation = InfiniteRadialAnimationWidget(\n\t\t\tbutton,\n\t\t\tst::giveawayGiftCodeStartButton.height / 2);\n\t\tAddChildToWidgetCenter(button, loadingAnimation);\n\t\tloadingAnimation->showOn(_confirmButtonBusy.value());\n\t}\n\n\tconst auto submit = [=] { button->clicked({}, Qt::LeftButton); };\n\tnewInput->submits() | rpl::on_next(submit, newInput->lifetime());\n\n\tsetFocusCallback([=] { newInput->setFocus(); });\n\n\tUi::ResizeFitChild(this, content);\n}\n\n} // namespace CloudPassword\n\nType CloudLoginEmailId() {\n\treturn CloudPassword::LoginEmail::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_login_email.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\nType CloudLoginEmailId();\n\n} // namespace Settings\n\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_login_email_confirm.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/cloud_password/settings_cloud_password_login_email_confirm.h\"\n\n#include \"api/api_cloud_password.h\"\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"core/core_cloud_password.h\"\n#include \"intro/intro_code_input.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"settings/cloud_password/settings_cloud_password_common.h\"\n#include \"settings/cloud_password/settings_cloud_password_login_email.h\"\n#include \"settings/cloud_password/settings_cloud_password_step.h\"\n#include \"ui/boxes/boost_box.h\" // Ui::StartFireworks.\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/sent_code_field.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\n/*\nAvailable actions for follow states.\n\nLoginEmailConfirm from LoginEmail:\n– Continue to Settings.\n– Back to LoginEmail.\n*/\n\nnamespace Settings {\nnamespace CloudPassword {\n\nclass LoginEmailConfirm : public TypedAbstractStep<LoginEmailConfirm> {\npublic:\n\tusing TypedAbstractStep::TypedAbstractStep;\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\tvoid setupContent();\n\nprotected:\n\t[[nodiscard]] rpl::producer<std::vector<Type>> removeTypes() override;\n\nprivate:\n\tQString _collectedCode;\n\tstd::optional<MTP::Sender> _api;\n\n\trpl::event_stream<> _processFinishes;\n\n};\n\nrpl::producer<std::vector<Type>> LoginEmailConfirm::removeTypes() {\n\treturn _processFinishes.events() | rpl::map([] {\n\t\treturn std::vector<Type>{ CloudLoginEmailId() };\n\t});\n}\n\nrpl::producer<QString> LoginEmailConfirm::title() {\n\treturn tr::lng_settings_cloud_login_email_section_title();\n}\n\nvoid LoginEmailConfirm::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tauto currentStepData = stepData();\n\tconst auto currentStepDataCodeLength = base::take(\n\t\tcurrentStepData.unconfirmedEmailLengthCode);\n\tconst auto newEmail = currentStepData.email;\n\tsetStepData(currentStepData);\n\n\tif (!currentStepDataCodeLength) {\n\t\tsetStepData(StepData());\n\t\tshowBack();\n\t\treturn;\n\t}\n\tcloudPassword().state(\n\t) | rpl::on_next([=](const Core::CloudPasswordState &state) {\n\t\tif (state.loginEmailPattern.isEmpty()) {\n\t\t\tsetStepData(StepData());\n\t\t\tshowBack();\n\t\t}\n\t}, lifetime());\n\n\tSetupHeader(\n\t\tcontent,\n\t\tu\"cloud_password/email\"_q,\n\t\tshowFinishes(),\n\t\ttr::lng_settings_cloud_login_email_code_title(),\n\t\ttr::lng_settings_cloud_login_email_code_about(\n\t\t\tlt_email,\n\t\t\trpl::single(Ui::Text::WrapEmailPattern(newEmail)),\n\t\t\tTextWithEntities::Simple));\n\n\tUi::AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip);\n\n\tconst auto newInput = content->add(\n\t\tobject_ptr<Ui::CodeInput>(content),\n\t\tstyle::al_top);\n\tnewInput->setDigitsCountMax(currentStepDataCodeLength);\n\n\tUi::AddSkip(content);\n\tconst auto error = AddError(content, nullptr);\n\tAddSkipInsteadOfField(content);\n\n\tconst auto submit = [=] {\n\t\t_api.emplace(&controller()->session().mtp());\n\t\tconst auto newText = _collectedCode;\n\t\tif (newText.isEmpty()) {\n\t\t\tnewInput->setFocus();\n\t\t\tnewInput->showError();\n\t\t} else {\n\t\t\tconst auto weak = base::make_weak(controller()->content());\n\t\t\tconst auto done = [=] {\n\t\t\t\t_api.reset();\n\t\t\t\t_processFinishes.fire({});\n\t\t\t\tcloudPassword().reload();\n\t\t\t\tsetStepData(StepData());\n\t\t\t\tshowBack();\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tUi::StartFireworks(strong);\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto fail = [=](const QString &type) {\n\t\t\t\t_api.reset();\n\n\t\t\t\tnewInput->setFocus();\n\t\t\t\tnewInput->showError();\n\t\t\t\terror->show();\n\n\t\t\t\tif (MTP::IsFloodError(type)) {\n\t\t\t\t\terror->setText(tr::lng_flood_error(tr::now));\n\t\t\t\t} else if (type == u\"EMAIL_NOT_ALLOWED\"_q) {\n\t\t\t\t\terror->setText(\n\t\t\t\t\t\ttr::lng_settings_error_email_not_alowed(tr::now));\n\t\t\t\t} else if (type == u\"CODE_INVALID\"_q) {\n\t\t\t\t\terror->setText(tr::lng_signin_wrong_code(tr::now));\n\t\t\t\t} else if (type == u\"EMAIL_HASH_EXPIRED\"_q) {\n\t\t\t\t\t// Show box?\n\t\t\t\t\terror->setText(Lang::Hard::EmailConfirmationExpired());\n\t\t\t\t} else {\n\t\t\t\t\terror->setText(Lang::Hard::ServerError());\n\t\t\t\t}\n\t\t\t};\n\t\t\tApi::VerifyLoginEmail(*_api, newText, done, fail);\n\t\t}\n\t};\n\n\tnewInput->codeCollected(\n\t) | rpl::on_next([=](const QString &code) {\n\t\t_collectedCode = code;\n\t\terror->hide();\n\t\tsubmit();\n\t}, lifetime());\n\n\tsetFocusCallback([=] { newInput->setFocus(); });\n\n\tUi::ResizeFitChild(this, content);\n}\n\n} // namespace CloudPassword\n\nType CloudLoginEmailConfirmId() {\n\treturn CloudPassword::LoginEmailConfirm::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_login_email_confirm.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\nType CloudLoginEmailConfirmId();\n\n} // namespace Settings\n\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/cloud_password/settings_cloud_password_manage.h\"\n\n#include \"api/api_cloud_password.h\"\n#include \"core/application.h\"\n#include \"core/core_cloud_password.h\"\n#include \"lang/lang_keys.h\"\n#include \"settings/cloud_password/settings_cloud_password_common.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/cloud_password/settings_cloud_password_email_confirm.h\"\n#include \"settings/cloud_password/settings_cloud_password_email.h\"\n#include \"settings/cloud_password/settings_cloud_password_hint.h\"\n#include \"settings/cloud_password/settings_cloud_password_input.h\"\n#include \"settings/cloud_password/settings_cloud_password_start.h\"\n#include \"settings/cloud_password/settings_cloud_password_step.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\n/*\nAvailable actions for follow states.\n\nFrom CreateEmail\nFrom CreateEmailConfirm\nFrom ChangeEmail\nFrom ChangeEmailConfirm\nFrom CheckPassword\nFrom RecreateResetHint:\n– Continue to ChangePassword.\n– Continue to ChangeEmail.\n– DisablePassword and Back to Settings.\n– Back to Settings.\n*/\n\nnamespace Settings {\nnamespace CloudPassword {\n\nclass Manage : public TypedAbstractStep<Manage> {\npublic:\n\tusing TypedAbstractStep::TypedAbstractStep;\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\tvoid setupContent();\n\n\t[[nodiscard]] base::weak_qptr<Ui::RpWidget> createPinnedToBottom(\n\t\tnot_null<Ui::RpWidget*> parent) override;\n\nprotected:\n\t[[nodiscard]] rpl::producer<std::vector<Type>> removeTypes() override;\n\nprivate:\n\trpl::variable<bool> _isBottomFillerShown;\n\n\tQString _currentPassword;\n\n\trpl::lifetime _requestLifetime;\n\n\tQPointer<Ui::RpWidget> _changePasswordButton;\n\tQPointer<Ui::RpWidget> _changeEmailButton;\n\tQPointer<Ui::RpWidget> _disableButton;\n\n};\n\nrpl::producer<QString> Manage::title() {\n\treturn tr::lng_settings_cloud_password_start_title();\n}\n\nrpl::producer<std::vector<Type>> Manage::removeTypes() {\n\treturn rpl::single(std::vector<Type>{\n\t\tCloudPasswordStartId(),\n\t\tCloudPasswordInputId(),\n\t\tCloudPasswordHintId(),\n\t\tCloudPasswordEmailId(),\n\t\tCloudPasswordEmailConfirmId(),\n\t\tCloudPasswordManageId(),\n\t});\n}\n\nvoid Manage::setupContent() {\n\tsetFocusPolicy(Qt::StrongFocus);\n\tsetFocus();\n\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tauto currentStepData = stepData();\n\t_currentPassword = base::take(currentStepData.currentPassword);\n\t// If we go back from Password Manage to Privacy Settings\n\t// we should forget the current password.\n\tsetStepData(std::move(currentStepData));\n\n\tconst auto quit = [=] {\n\t\tsetStepData(StepData());\n\t\tshowBack();\n\t};\n\n\tSetupAutoCloseTimer(\n\t\tcontent->lifetime(),\n\t\tquit,\n\t\t[] { return Core::App().lastNonIdleTime(); });\n\n\tconst auto state = cloudPassword().stateCurrent();\n\tif (!state) {\n\t\tquit();\n\t\treturn;\n\t}\n\tcloudPassword().state(\n\t) | rpl::on_next([=](const Core::CloudPasswordState &state) {\n\t\tif (!_requestLifetime && !state.hasPassword) {\n\t\t\tquit();\n\t\t}\n\t}, lifetime());\n\n\tconst auto showOtherAndRememberPassword = [=](Type type) {\n\t\t// Remember the current password to have ability\n\t\t// to return from Change Password to Password Manage.\n\t\tauto data = stepData();\n\t\tdata.currentPassword = _currentPassword;\n\t\tsetStepData(std::move(data));\n\n\t\tshowOther(type);\n\t};\n\n\tAddDividerTextWithLottie(content, {\n\t\t.lottie = u\"cloud_password/intro\"_q,\n\t\t.showFinished = showFinishes(),\n\t\t.about = tr::lng_settings_cloud_password_manage_about1(\n\t\t\tTextWithEntities::Simple),\n\t});\n\n\tUi::AddSkip(content);\n\tconst auto changePasswordButton = AddButtonWithIcon(\n\t\tcontent,\n\t\ttr::lng_settings_cloud_password_manage_password_change(),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconPermissions });\n\t_changePasswordButton = changePasswordButton;\n\tchangePasswordButton->setClickedCallback([=] {\n\t\tshowOtherAndRememberPassword(CloudPasswordInputId());\n\t});\n\tconst auto changeEmailButton = AddButtonWithIcon(\n\t\tcontent,\n\t\tstate->hasRecovery\n\t\t\t? tr::lng_settings_cloud_password_manage_email_change()\n\t\t\t: tr::lng_settings_cloud_password_manage_email_new(),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconRecoveryEmail });\n\t_changeEmailButton = changeEmailButton;\n\tchangeEmailButton->setClickedCallback([=] {\n\t\tauto data = stepData();\n\t\tdata.setOnlyRecoveryEmail = true;\n\t\tsetStepData(std::move(data));\n\n\t\tshowOtherAndRememberPassword(CloudPasswordEmailId());\n\t});\n\tUi::AddSkip(content);\n\n\tshowFinishes() | rpl::take(1) | rpl::on_next([=] {\n\t\tcontroller()->checkHighlightControl(\n\t\t\tu\"2sv/change\"_q,\n\t\t\t_changePasswordButton);\n\t\tcontroller()->checkHighlightControl(\n\t\t\tu\"2sv/change-email\"_q,\n\t\t\t_changeEmailButton);\n\t\tcontroller()->checkHighlightControl(\"2sv/disable\"_q, _disableButton);\n\t}, lifetime());\n\n\tusing Divider = CloudPassword::OneEdgeBoxContentDivider;\n\tconst auto divider = Ui::CreateChild<Divider>(this);\n\tdivider->lower();\n\tconst auto about = content->add(\n\t\tobject_ptr<Ui::PaddingWrap<>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_settings_cloud_password_manage_about2(),\n\t\t\t\tst::boxDividerLabel),\n\t\tst::defaultBoxDividerLabelPadding));\n\trpl::combine(\n\t\tabout->geometryValue(),\n\t\tcontent->widthValue()\n\t) | rpl::on_next([=](QRect r, int w) {\n\t\tr.setWidth(w);\n\t\tdivider->setGeometry(r);\n\t}, divider->lifetime());\n\t_isBottomFillerShown.value(\n\t) | rpl::on_next([=](bool shown) {\n\t\tdivider->skipEdge(Qt::BottomEdge, shown);\n\t}, divider->lifetime());\n\n\tUi::ResizeFitChild(this, content);\n}\n\nbase::weak_qptr<Ui::RpWidget> Manage::createPinnedToBottom(\n\t\tnot_null<Ui::RpWidget*> parent) {\n\n\tconst auto disable = [=](Fn<void()> close) {\n\t\tif (_requestLifetime) {\n\t\t\treturn;\n\t\t}\n\t\t_requestLifetime = cloudPassword().set(\n\t\t\t_currentPassword,\n\t\t\tQString(),\n\t\t\tQString(),\n\t\t\tfalse,\n\t\t\tQString()\n\t\t) | rpl::on_error_done([=](const QString &type) {\n\t\t\tAbstractStep::isPasswordInvalidError(type);\n\t\t}, [=] {\n\t\t\tsetStepData(StepData());\n\t\t\tclose();\n\t\t\tshowBack();\n\t\t});\n\t};\n\n\tauto callback = [=] {\n\t\tcontroller()->show(\n\t\t\tUi::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_settings_cloud_password_manage_disable_sure(),\n\t\t\t\t.confirmed = disable,\n\t\t\t\t.confirmText = tr::lng_settings_auto_night_disable(),\n\t\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t}));\n\t};\n\tauto bottomButton = CloudPassword::CreateBottomDisableButton(\n\t\tparent,\n\t\tgeometryValue(),\n\t\ttr::lng_settings_password_disable(),\n\t\tstd::move(callback));\n\n\t_isBottomFillerShown = base::take(bottomButton.isBottomFillerShown);\n\t_disableButton = bottomButton.button.get();\n\n\treturn bottomButton.content;\n}\n\n} // namespace CloudPassword\n\nType CloudPasswordManageId() {\n\treturn CloudPassword::Manage::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\nType CloudPasswordManageId();\n\n} // namespace Settings\n\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_start.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/cloud_password/settings_cloud_password_start.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"settings/cloud_password/settings_cloud_password_common.h\"\n#include \"settings/cloud_password/settings_cloud_password_input.h\"\n#include \"settings/cloud_password/settings_cloud_password_step.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace CloudPassword {\n\nclass Start : public TypedAbstractStep<Start> {\npublic:\n\tusing TypedAbstractStep::TypedAbstractStep;\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\tvoid setupContent();\n\n};\n\nrpl::producer<QString> Start::title() {\n\treturn tr::lng_settings_cloud_password_start_title();\n}\n\nvoid Start::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tSetupHeader(\n\t\tcontent,\n\t\tu\"cloud_password/intro\"_q,\n\t\tshowFinishes(),\n\t\ttr::lng_settings_cloud_password_start_title(),\n\t\ttr::lng_settings_cloud_password_start_about());\n\n\tAddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip);\n\n\tAddSkipInsteadOfField(content);\n\tAddSkipInsteadOfField(content);\n\tAddSkipInsteadOfError(content);\n\n\tAddDoneButton(\n\t\tcontent,\n\t\ttr::lng_settings_cloud_password_password_subtitle()\n\t)->setClickedCallback([=] {\n\t\tshowOther(CloudPasswordInputId());\n\t});\n\n\tUi::ResizeFitChild(this, content);\n}\n\n} // namespace CloudPassword\n\nType CloudPasswordStartId() {\n\treturn CloudPassword::Start::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_start.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\nType CloudPasswordStartId();\n\n} // namespace Settings\n\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_step.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/cloud_password/settings_cloud_password_step.h\"\n\n#include \"apiwrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/cloud_password/settings_cloud_password_common.h\"\n#include \"settings/cloud_password/settings_cloud_password_email.h\"\n#include \"settings/cloud_password/settings_cloud_password_email_confirm.h\"\n#include \"settings/cloud_password/settings_cloud_password_hint.h\"\n#include \"settings/cloud_password/settings_cloud_password_input.h\"\n#include \"settings/cloud_password/settings_cloud_password_manage.h\"\n#include \"settings/cloud_password/settings_cloud_password_start.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace Settings::CloudPassword {\n\nAbstractStep::AbstractStep(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: AbstractSection(parent, controller) {\n}\n\nApi::CloudPassword &AbstractStep::cloudPassword() {\n\treturn controller()->session().api().cloudPassword();\n}\n\nrpl::producer<AbstractStep::Types> AbstractStep::removeTypes() {\n\treturn rpl::never<Types>();\n}\n\nvoid AbstractStep::showBack() {\n\t_showBack.fire({});\n}\n\nvoid AbstractStep::showOther(Type type) {\n\t_showOther.fire_copy(type);\n}\n\nvoid AbstractStep::setFocusCallback(Fn<void()> callback) {\n\t_setInnerFocusCallback = callback;\n}\n\nrpl::producer<> AbstractStep::showFinishes() const {\n\treturn _showFinished.events();\n}\n\nvoid AbstractStep::showFinished() {\n\t_showFinished.fire({});\n}\n\nvoid AbstractStep::setInnerFocus() {\n\tif (_setInnerFocusCallback) {\n\t\t_setInnerFocusCallback();\n\t}\n}\n\nbool AbstractStep::isPasswordInvalidError(const QString &type) {\n\tif (type == u\"PASSWORD_HASH_INVALID\"_q\n\t\t|| type == u\"SRP_PASSWORD_CHANGED\"_q) {\n\n\t\t// Most likely the cloud password has been changed on another device.\n\t\t// Quit.\n\t\t_quits.fire(AbstractStep::Types{\n\t\t\tCloudPasswordStartId(),\n\t\t\tCloudPasswordInputId(),\n\t\t\tCloudPasswordHintId(),\n\t\t\tCloudPasswordEmailId(),\n\t\t\tCloudPasswordEmailConfirmId(),\n\t\t\tCloudPasswordManageId(),\n\t\t});\n\t\tcontroller()->show(\n\t\t\tUi::MakeInformBox(tr::lng_cloud_password_expired()),\n\t\t\tUi::LayerOption::CloseOther);\n\t\tsetStepData(StepData());\n\t\tshowBack();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nrpl::producer<Type> AbstractStep::sectionShowOther() {\n\treturn _showOther.events();\n}\n\nrpl::producer<> AbstractStep::sectionShowBack() {\n\treturn _showBack.events();\n}\n\nrpl::producer<std::vector<Type>> AbstractStep::removeFromStack() {\n\treturn rpl::merge(removeTypes(), _quits.events());\n}\n\nvoid AbstractStep::setStepDataReference(std::any &data) {\n\t_stepData = &data;\n}\n\nStepData AbstractStep::stepData() const {\n\tif (!_stepData || !_stepData->has_value()) {\n\t\tStepData();\n\t}\n\tconst auto my = std::any_cast<StepData>(_stepData);\n\treturn my ? (*my) : StepData();\n}\n\nvoid AbstractStep::setStepData(StepData data) {\n\tif (_stepData) {\n\t\t*_stepData = data;\n\t}\n}\n\nAbstractStep::~AbstractStep() = default;\n\n} // namespace Settings::CloudPassword\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_step.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_common_session.h\"\n\nnamespace Api {\nclass CloudPassword;\n} // namespace Api\n\nnamespace Settings::CloudPassword {\n\nstruct StepData;\n\nclass AbstractStep : public AbstractSection {\npublic:\n\tusing Types = std::vector<Type>;\n\tAbstractStep(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~AbstractStep();\n\n\tvoid showFinished() override final;\n\tvoid setInnerFocus() override final;\n\t[[nodiscard]] rpl::producer<Type> sectionShowOther() override final;\n\t[[nodiscard]] rpl::producer<> sectionShowBack() override final;\n\n\t[[nodiscard]] rpl::producer<Types> removeFromStack() override final;\n\n\tvoid setStepDataReference(std::any &data) override;\n\nprotected:\n\t[[nodiscard]] Api::CloudPassword &cloudPassword();\n\n\t[[nodiscard]] virtual rpl::producer<Types> removeTypes();\n\n\tbool isPasswordInvalidError(const QString &type);\n\n\tvoid showBack();\n\tvoid showOther(Type type);\n\n\tvoid setFocusCallback(Fn<void()> callback);\n\n\t[[nodiscard]] rpl::producer<> showFinishes() const;\n\n\tStepData stepData() const;\n\tvoid setStepData(StepData data);\n\nprivate:\n\tFn<void()> _setInnerFocusCallback;\n\n\trpl::event_stream<> _showFinished;\n\trpl::event_stream<Type> _showOther;\n\trpl::event_stream<> _showBack;\n\trpl::event_stream<Types> _quits;\n\n\tstd::any *_stepData;\n\n};\n\ntemplate <typename SectionType>\nclass TypedAbstractStep : public AbstractStep {\npublic:\n\tusing AbstractStep::AbstractStep;\n\n\tvoid setStepDataReference(std::any &data) override final {\n\t\tAbstractStep::setStepDataReference(data);\n\t\tstatic_cast<SectionType*>(this)->setupContent();\n\t}\n\n\t[[nodiscard]] static Type Id() {\n\t\treturn SectionFactory<SectionType>::Instance();\n\t}\n\t[[nodiscard]] Type id() const final override {\n\t\treturn Id();\n\t}\n\n};\n\n} // namespace Settings::CloudPassword\n\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/cloud_password/settings_cloud_password_validate_icon.h\"\n\n#include \"apiwrap.h\"\n#include \"base/object_ptr.h\"\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"main/main_session.h\"\n#include \"ui/rect.h\"\n#include \"ui/rp_widget.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\n[[nodiscard]] DocumentData *EmojiValidateGood(\n\t\tnot_null<Main::Session*> session) {\n\tauto emoji = TextWithEntities{\n\t\t.text = (QString(QChar(0xD83D)) + QChar(0xDC4D)),\n\t};\n\tif (const auto e = Ui::Emoji::Find(emoji.text)) {\n\t\tconst auto sticker = session->emojiStickersPack().stickerForEmoji(e);\n\t\treturn sticker.document;\n\t}\n\treturn nullptr;\n}\n\n} // namespace\n\nobject_ptr<Ui::RpWidget> CreateValidateGoodIcon(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto document = EmojiValidateGood(session);\n\tif (!document) {\n\t\treturn nullptr;\n\t}\n\n\tauto owned = object_ptr<Ui::RpWidget>((QWidget*)nullptr);\n\tconst auto widget = owned.data();\n\n\tstruct State {\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji> emoji;\n\t};\n\tconst auto state = widget->lifetime().make_state<State>();\n\tconst auto size = st::settingsCloudPasswordIconSize;\n\tstate->emoji = std::make_unique<Ui::Text::LimitedLoopsEmoji>(\n\t\tsession->data().customEmojiManager().create(\n\t\t\tdocument,\n\t\t\t[=] { widget->update(); },\n\t\t\tData::CustomEmojiManager::SizeTag::Normal,\n\t\t\tsize),\n\t\t1,\n\t\ttrue);\n\twidget->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(widget);\n\t\tstate->emoji->paint(p, Ui::Text::CustomEmojiPaintContext{\n\t\t\t.textColor = st::windowFg->c,\n\t\t\t.now = crl::now(),\n\t\t});\n\t}, widget->lifetime());\n\tconst auto padding = st::settingLocalPasscodeIconPadding;\n\twidget->resize((Rect(Size(size)) + padding).size());\n\twidget->setNaturalWidth(padding.left() + size + padding.right());\n\n\treturn owned;\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\ntemplate <typename Object>\nclass object_ptr;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Settings {\n\n[[nodiscard]] object_ptr<Ui::RpWidget> CreateValidateGoodIcon(\n\tnot_null<Main::Session*> session);\n\n} // namespace Settings\n\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/detailed_settings_button.cpp",
    "content": "#include \"settings/detailed_settings_button.h\"\n\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_settings.h\"\n\nDetailedSettingsButton::DetailedSettingsButton(\n\tQWidget *parent,\n\trpl::producer<QString> title,\n\trpl::producer<QString> description,\n\tSettings::IconDescriptor icon,\n\trpl::producer<bool> toggled,\n\tconst style::DetailedSettingsButtonStyle &rowStyle)\n: Ui::RippleButton(parent, rowStyle.button.ripple)\n, _style(rowStyle)\n, _title(0)\n, _description(0)\n, _toggle(std::make_unique<Ui::ToggleView>(\n\t_style.button.toggle,\n\tfalse,\n\t[this] { rtlupdate(toggleRect()); }))\n, _iconForeground(icon.icon)\n, _iconBackground(icon.background)\n, _iconBackgroundBrush(std::move(icon.backgroundBrush)) {\n\tstd::move(\n\t\ttitle\n\t) | rpl::on_next([=](const QString &text) {\n\t\t_title.setText(\n\t\t\t_style.button.style,\n\t\t\ttext,\n\t\t\tkDefaultTextOptions);\n\t\taccessibilityNameChanged();\n\t\trefreshLayout();\n\t}, lifetime());\n\tstd::move(\n\t\tdescription\n\t) | rpl::on_next([=](const QString &text) {\n\t\t_description.setText(\n\t\t\t_style.description.style,\n\t\t\ttext,\n\t\t\tkDefaultTextOptions);\n\t\trefreshLayout();\n\t}, lifetime());\n\taddClickHandler([=] {\n\t\tif (!_toggleLocked) {\n\t\t\t_toggle->setChecked(!_toggle->checked(), anim::type::normal);\n\t\t}\n\t});\n\tstd::move(\n\t\ttoggled\n\t) | rpl::on_next([=](bool checked) {\n\t\t_toggle->setChecked(checked, anim::type::normal);\n\t}, lifetime());\n\t_toggle->checkedChanges() | rpl::on_next([=] {\n\t\taccessibilityStateChanged({ .checked = true });\n\t}, lifetime());\n\twidthValue() | rpl::on_next([=](int width) {\n\t\tif (width > 0) {\n\t\t\trefreshLayout();\n\t\t}\n\t}, lifetime());\n}\n\nQString DetailedSettingsButton::accessibilityName() {\n\treturn _title.toString();\n}\n\nUi::AccessibilityState DetailedSettingsButton::accessibilityState() const {\n\treturn { .checkable = true, .checked = _toggle->checked() };\n}\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 11, 0)\nQAccessible::Role DetailedSettingsButton::accessibilityRole() {\n\treturn QAccessible::Role::Switch;\n}\n#endif\n\nbool DetailedSettingsButton::toggled() const {\n\treturn _toggle->checked();\n}\n\nrpl::producer<bool> DetailedSettingsButton::toggledChanges() const {\n\treturn _toggle->checkedChanges();\n}\n\nrpl::producer<not_null<QEvent*>>\nDetailedSettingsButton::clickAreaEvents() const {\n\treturn events();\n}\n\nvoid DetailedSettingsButton::setToggleLocked(bool locked) {\n\t_toggleLocked = locked;\n\t_toggle->setLocked(locked);\n}\n\nvoid DetailedSettingsButton::finishAnimating() {\n\t_toggle->finishAnimating();\n}\n\nvoid DetailedSettingsButton::onStateChanged(\n\t\tState was,\n\t\tStateChangeSource source) {\n\tconst auto wasDisabled = !!(was & StateFlag::Disabled);\n\tconst auto nowDisabled = isDisabled();\n\tif (!nowDisabled || !isDown()) {\n\t\tRippleButton::onStateChanged(was, source);\n\t}\n\t_toggle->setStyle(\n\t\tisOver()\n\t\t\t? _style.button.toggleOver\n\t\t\t: _style.button.toggle);\n\tif (nowDisabled != wasDisabled) {\n\t\tsetPointerCursor(!nowDisabled);\n\t}\n\tupdate();\n}\n\nvoid DetailedSettingsButton::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\tconst auto over = isOver() || isDown();\n\tp.fillRect(\n\t\te->rect(),\n\t\tover ? _style.button.textBgOver : _style.button.textBg);\n\tpaintRipple(p, 0, 0);\n\n\tpaintIcon(p);\n\n\tp.setPen(over\n\t\t? _style.button.textFgOver\n\t\t: _style.button.textFg);\n\t_title.drawLeftElided(\n\t\tp,\n\t\t_style.button.padding.left(),\n\t\ttitleTop(),\n\t\t_titleWidth,\n\t\twidth());\n\tp.setPen(_style.description.textFg);\n\tif (_descriptionHeight > 0) {\n\t\t_description.drawLeft(\n\t\t\tp,\n\t\t\t_style.button.padding.left(),\n\t\t\t_descriptionTop,\n\t\t\t_descriptionWidth,\n\t\t\twidth());\n\t}\n\n\tconst auto toggle = toggleRect();\n\t_toggle->paint(p, toggle.left(), toggle.top(), width());\n}\n\nint DetailedSettingsButton::resizeGetHeight(int newWidth) {\n\t_titleWidth = textAvailableWidth(newWidth);\n\t_descriptionWidth\n\t\t= std::max(1, _titleWidth - _style.descriptionRightSkip);\n\t_descriptionTop = descriptionTopValue();\n\t_descriptionHeight = _description.countHeight(_descriptionWidth);\n\treturn _descriptionTop\n\t\t+ _descriptionHeight\n\t\t+ _style.descriptionBottomSkip;\n}\n\nvoid DetailedSettingsButton::refreshLayout() {\n\tUi::PostponeCall(crl::guard(this, [=] {\n\t\tconst auto current = widthNoMargins();\n\t\tif (current > 0) {\n\t\t\tresizeToWidth(current);\n\t\t}\n\t\tupdate();\n\t}));\n}\n\nint DetailedSettingsButton::titleTop() const {\n\treturn _style.button.padding.top();\n}\n\nint DetailedSettingsButton::titleHeight() const {\n\treturn _style.button.height;\n}\n\nint DetailedSettingsButton::descriptionTopValue() const {\n\treturn titleTop() + titleHeight() + _style.descriptionTopSkip;\n}\n\nint DetailedSettingsButton::firstDescriptionLineBottom() const {\n\tconst auto top = (_descriptionTop > 0)\n\t\t? _descriptionTop\n\t\t: descriptionTopValue();\n\treturn top + qMax(\n\t\t_style.description.style.lineHeight,\n\t\t_style.description.style.font->height);\n}\n\nint DetailedSettingsButton::textAvailableWidth(int outerw) const {\n\tconst auto toggle = _toggle->getSize();\n\treturn std::max(\n\t\t1,\n\t\touterw\n\t\t\t- _style.button.padding.left()\n\t\t\t- _style.button.padding.right()\n\t\t\t- _style.button.toggleSkip\n\t\t\t- toggle.width());\n}\n\nQRect DetailedSettingsButton::toggleRect() const {\n\tconst auto toggle = _toggle->getSize();\n\tconst auto anchorBottom = firstDescriptionLineBottom();\n\treturn QRect(\n\t\twidth() - _style.button.toggleSkip - toggle.width(),\n\t\t(anchorBottom - toggle.height()) / 2,\n\t\ttoggle.width(),\n\t\ttoggle.height());\n}\n\nQRect DetailedSettingsButton::iconAreaRect() const {\n\tconst auto size = iconSize();\n\treturn QRect(\n\t\t_style.button.iconLeft,\n\t\ttitleTop(),\n\t\tsize,\n\t\tsize);\n}\n\nQRect DetailedSettingsButton::iconRect() const {\n\tconst auto area = iconAreaRect();\n\tconst auto padding = _style.iconPadding;\n\tconst auto width = std::max(1, area.width() - (2 * padding));\n\tconst auto height = std::max(1, area.height() - (2 * padding));\n\treturn QRect(\n\t\tarea.x() + padding,\n\t\tarea.y() + padding,\n\t\twidth,\n\t\theight);\n}\n\nQRect DetailedSettingsButton::iconForegroundRect(QRect iconRect) const {\n\tconst auto padding = _style.iconForegroundPadding;\n\tconst auto width = std::max(1, iconRect.width() - (2 * padding));\n\tconst auto height = std::max(1, iconRect.height() - (2 * padding));\n\treturn QRect(\n\t\ticonRect.x() + padding,\n\t\ticonRect.y() + padding,\n\t\twidth,\n\t\theight);\n}\n\nint DetailedSettingsButton::iconSize() const {\n\tconst auto result = firstDescriptionLineBottom() - titleTop();\n\treturn std::max(1, result);\n}\n\nvoid DetailedSettingsButton::paintIcon(QPainter &p) const {\n\tif (!_iconForeground && !_iconBackground && !_iconBackgroundBrush) {\n\t\treturn;\n\t}\n\tconst auto rect = iconRect();\n\tif (_iconBackground) {\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(*_iconBackground);\n\t\t{\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.drawRoundedRect(rect, _style.iconRadius, _style.iconRadius);\n\t\t}\n\t} else if (_iconBackgroundBrush) {\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(*_iconBackgroundBrush);\n\t\t{\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.drawRoundedRect(rect, _style.iconRadius, _style.iconRadius);\n\t\t}\n\t}\n\tif (_iconForeground) {\n\t\tconst auto source = _iconForeground->size();\n\t\tif (!source.isEmpty()) {\n\t\t\tconst auto foreground = iconForegroundRect(rect);\n\t\t\tconst auto scale = std::min(\n\t\t\t\tdouble(foreground.width()) / source.width(),\n\t\t\t\tdouble(foreground.height()) / source.height());\n\t\t\tconst auto shiftX = (foreground.width()\n\t\t\t\t- (source.width() * scale)) / 2.;\n\t\t\tconst auto shiftY = (foreground.height()\n\t\t\t\t- (source.height() * scale)) / 2.;\n\t\t\tp.save();\n\t\t\tp.translate(foreground.x() + shiftX, foreground.y() + shiftY);\n\t\t\tp.scale(scale, scale);\n\t\t\t_iconForeground->paint(p, 0, 0, source.width());\n\t\t\tp.restore();\n\t\t}\n\t}\n}\n\nnot_null<DetailedSettingsButton*> AddDetailedSettingsButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> title,\n\t\trpl::producer<QString> description,\n\t\tSettings::IconDescriptor icon,\n\t\trpl::producer<bool> toggled,\n\t\tconst style::DetailedSettingsButtonStyle &rowStyle) {\n\treturn container->add(object_ptr<DetailedSettingsButton>(\n\t\tcontainer,\n\t\tstd::move(title),\n\t\tstd::move(description),\n\t\tstd::move(icon),\n\t\tstd::move(toggled),\n\t\trowStyle));\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/detailed_settings_button.h",
    "content": "#pragma once\n\n#include \"settings/settings_common.h\"\n#include \"ui/text/text.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace style {\nstruct DetailedSettingsButtonStyle;\n} // namespace style\n\nclass DetailedSettingsButton final : public Ui::RippleButton {\npublic:\n\tDetailedSettingsButton(\n\t\tQWidget *parent,\n\t\trpl::producer<QString> title,\n\t\trpl::producer<QString> description,\n\t\tSettings::IconDescriptor icon,\n\t\trpl::producer<bool> toggled,\n\t\tconst style::DetailedSettingsButtonStyle &rowStyle);\n\n\tQString accessibilityName() override;\n\tUi::AccessibilityState accessibilityState() const override;\n#if QT_VERSION >= QT_VERSION_CHECK(6, 11, 0)\n\tQAccessible::Role accessibilityRole() override;\n#endif\n\n\t[[nodiscard]] bool toggled() const;\n\t[[nodiscard]] rpl::producer<bool> toggledChanges() const;\n\t[[nodiscard]] rpl::producer<not_null<QEvent*>> clickAreaEvents() const;\n\tvoid setToggleLocked(bool locked);\n\tvoid finishAnimating();\n\nprotected:\n\tvoid onStateChanged(State was, StateChangeSource source) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tvoid refreshLayout();\n\t[[nodiscard]] int titleTop() const;\n\t[[nodiscard]] int titleHeight() const;\n\t[[nodiscard]] int descriptionTopValue() const;\n\t[[nodiscard]] int firstDescriptionLineBottom() const;\n\t[[nodiscard]] int textAvailableWidth(int outerw) const;\n\t[[nodiscard]] QRect toggleRect() const;\n\t[[nodiscard]] QRect iconAreaRect() const;\n\t[[nodiscard]] QRect iconRect() const;\n\t[[nodiscard]] QRect iconForegroundRect(QRect iconRect) const;\n\t[[nodiscard]] int iconSize() const;\n\tvoid paintIcon(QPainter &p) const;\n\n\tconst style::DetailedSettingsButtonStyle &_style;\n\tUi::Text::String _title;\n\tUi::Text::String _description;\n\tstd::unique_ptr<Ui::ToggleView> _toggle;\n\tconst style::icon *_iconForeground = nullptr;\n\tconst style::color *_iconBackground = nullptr;\n\tconst std::optional<QBrush> _iconBackgroundBrush;\n\tint _titleWidth = 0;\n\tint _descriptionWidth = 0;\n\tint _descriptionTop = 0;\n\tint _descriptionHeight = 0;\n\tbool _toggleLocked = false;\n\n};\n\n[[nodiscard]] not_null<DetailedSettingsButton*> AddDetailedSettingsButton(\n\tnot_null<Ui::VerticalLayout*> container,\n\trpl::producer<QString> title,\n\trpl::producer<QString> description,\n\tSettings::IconDescriptor icon,\n\trpl::producer<bool> toggled,\n\tconst style::DetailedSettingsButtonStyle &rowStyle);\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_active_sessions.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_active_sessions.h\"\n\n#include \"settings/settings_common_session.h\"\n\n#include \"api/api_authorizations.h\"\n#include \"apiwrap.h\"\n#include \"base/algorithm.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/timer.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/peer_lists_box.h\"\n#include \"boxes/self_destruction_box.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_privacy_security.h\"\n#include \"settings/settings_builder.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nusing namespace Builder;\n\nconstexpr auto kShortPollTimeout = 60 * crl::time(1000);\nconstexpr auto kMaxDeviceModelLength = 32;\n\nusing EntryData = Api::Authorizations::Entry;\n\nenum class DeviceType {\n\tWindows,\n\tMac,\n\tUbuntu,\n\tLinux,\n\tiPhone,\n\tiPad,\n\tAndroid,\n\tWeb,\n\tChrome,\n\tEdge,\n\tFirefox,\n\tSafari,\n\tOther,\n};\n\nclass Row;\n\nclass RowDelegate {\npublic:\n\tvirtual void rowUpdateRow(not_null<Row*> row) = 0;\n};\n\nclass Row final : public PeerListRow {\npublic:\n\tRow(not_null<RowDelegate*> delegate, const EntryData &data);\n\n\tvoid update(const EntryData &data);\n\n\t[[nodiscard]] EntryData data() const;\n\n\tQString generateName() override;\n\tQString generateShortName() override;\n\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\n\tQSize rightActionSize() const override {\n\t\treturn elementGeometry(2, 0).size();\n\t}\n\tQMargins rightActionMargins() const override {\n\t\tconst auto rect = elementGeometry(2, 0);\n\t\treturn QMargins(0, rect.y(), -(rect.x() + rect.width()), 0);\n\t}\n\n\tint elementsCount() const override;\n\tQRect elementGeometry(int element, int outerWidth) const override;\n\tbool elementDisabled(int element) const override;\n\tbool elementOnlySelect(int element) const override;\n\tvoid elementAddRipple(\n\t\tint element,\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) override;\n\tvoid elementsStopLastRipple() override;\n\tvoid elementsPaint(\n\t\tPainter &p,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tint selectedElement) override;\n\nprivate:\n\tconst not_null<RowDelegate*> _delegate;\n\tUi::Text::String _location;\n\tDeviceType _type = DeviceType::Other;\n\tEntryData _data;\n\tQImage _userpic;\n\n};\n\nvoid RenameBox(not_null<Ui::GenericBox*> box) {\n\tbox->setTitle(tr::lng_settings_rename_device_title());\n\n\tconst auto skip = st::defaultSubsectionTitlePadding.top();\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_settings_device_name(),\n\t\t\tst::defaultSubsectionTitle),\n\t\tst::boxRowPadding + style::margins(0, skip, 0, 0));\n\tconst auto name = box->addRow(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tbox,\n\t\t\tst::settingsDeviceName,\n\t\t\trpl::single(Platform::DeviceModelPretty()),\n\t\t\tCore::App().settings().customDeviceModel()),\n\t\tst::boxRowPadding - style::margins(\n\t\t\tst::settingsDeviceName.textMargins.left(),\n\t\t\t0,\n\t\t\tst::settingsDeviceName.textMargins.right(),\n\t\t\t0));\n\tname->setMaxLength(kMaxDeviceModelLength);\n\tbox->setFocusCallback([=] {\n\t\tname->setFocusFast();\n\t});\n\tconst auto submit = [=] {\n\t\tconst auto result = base::CleanAndSimplify(\n\t\t\tname->getLastText());\n\t\tbox->closeBox();\n\t\tCore::App().settings().setCustomDeviceModel(result);\n\t\tCore::App().saveSettingsDelayed();\n\t};\n\tname->submits() | rpl::on_next(submit, name->lifetime());\n\tbox->addButton(tr::lng_settings_save(), submit);\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\n[[nodiscard]] QString LocationAndDate(const EntryData &entry) {\n\treturn (entry.location.isEmpty() ? entry.ip : entry.location)\n\t\t+ (entry.hash\n\t\t\t? (' ' + Ui::kQBullet + ' ' + entry.active)\n\t\t\t: QString());\n}\n\n[[nodiscard]] DeviceType TypeFromEntry(const EntryData &entry) {\n\tconst auto platform = entry.platform.toLower();\n\tconst auto device = entry.name.toLower();\n\tconst auto system = entry.system.toLower();\n\tconst auto apiId = entry.apiId;\n\tconst auto kDesktop = std::array{ 2040, 17349, 611335 };\n\tconst auto kMac = std::array{ 2834 };\n\tconst auto kAndroid\n\t\t= std::array{ 5, 6, 24, 1026, 1083, 2458, 2521, 21724 };\n\tconst auto kiOS = std::array{ 1, 7, 10840, 16352 };\n\tconst auto kWeb = std::array{ 2496, 739222, 1025907 };\n\n\tconst auto detectBrowser = [&]() -> std::optional<DeviceType> {\n\t\tif (device.contains(\"edg/\")\n\t\t\t|| device.contains(\"edgios/\")\n\t\t\t|| device.contains(\"edga/\")) {\n\t\t\treturn DeviceType::Edge;\n\t\t} else if (device.contains(\"chrome\")) {\n\t\t\treturn DeviceType::Chrome;\n\t\t} else if (device.contains(\"safari\")) {\n\t\t\treturn DeviceType::Safari;\n\t\t} else if (device.contains(\"firefox\")) {\n\t\t\treturn DeviceType::Firefox;\n\t\t}\n\t\treturn {};\n\t};\n\tconst auto detectDesktop = [&]() -> std::optional<DeviceType> {\n\t\tif (platform.contains(\"windows\") || system.contains(\"windows\")) {\n\t\t\treturn DeviceType::Windows;\n\t\t} else if (platform.contains(\"macos\") || system.contains(\"macos\")) {\n\t\t\treturn DeviceType::Mac;\n\t\t} else if (platform.contains(\"ubuntu\")\n\t\t\t|| system.contains(\"ubuntu\")\n\t\t\t|| platform.contains(\"unity\")\n\t\t\t|| system.contains(\"unity\")) {\n\t\t\treturn DeviceType::Ubuntu;\n\t\t} else if (platform.contains(\"linux\") || system.contains(\"linux\")) {\n\t\t\treturn DeviceType::Linux;\n\t\t}\n\t\treturn {};\n\t};\n\n\tif (ranges::contains(kAndroid, apiId)) {\n\t\treturn DeviceType::Android;\n\t} else if (ranges::contains(kDesktop, apiId)) {\n\t\treturn detectDesktop().value_or(DeviceType::Linux);\n\t} else if (ranges::contains(kMac, apiId)) {\n\t\treturn DeviceType::Mac;\n\t} else if (ranges::contains(kWeb, apiId)) {\n\t\treturn detectBrowser().value_or(DeviceType::Web);\n\t} else if (device.contains(\"chromebook\")) {\n\t\treturn DeviceType::Other;\n\t} else if (const auto browser = detectBrowser()) {\n\t\treturn *browser;\n\t} else if (device.contains(\"iphone\")) {\n\t\treturn DeviceType::iPhone;\n\t} else if (device.contains(\"ipad\")) {\n\t\treturn DeviceType::iPad;\n\t} else if (ranges::contains(kiOS, apiId)) {\n\t\treturn DeviceType::iPhone;\n\t} else if (const auto desktop = detectDesktop()) {\n\t\treturn *desktop;\n\t} else if (platform.contains(\"android\") || system.contains(\"android\")) {\n\t\treturn DeviceType::Android;\n\t} else if (platform.contains(\"ios\") || system.contains(\"ios\")) {\n\t\treturn DeviceType::iPhone;\n\t}\n\treturn DeviceType::Other;\n}\n\n[[nodiscard]] QBrush GradientForType(DeviceType type, int size) {\n\tconst auto colors = [&]() -> std::pair<style::color, style::color> {\n\t\tswitch (type) {\n\t\tcase DeviceType::Windows:\n\t\tcase DeviceType::Mac:\n\t\tcase DeviceType::Other:\n\t\t\treturn { st::historyPeer4UserpicBg, st::historyPeer4UserpicBg2 };\n\t\tcase DeviceType::Ubuntu:\n\t\t\treturn { st::historyPeer8UserpicBg, st::historyPeer8UserpicBg2 };\n\t\tcase DeviceType::Linux:\n\t\t\treturn { st::historyPeer5UserpicBg, st::historyPeer5UserpicBg2 };\n\t\tcase DeviceType::iPhone:\n\t\tcase DeviceType::iPad:\n\t\t\treturn { st::historyPeer7UserpicBg, st::historyPeer7UserpicBg2 };\n\t\tcase DeviceType::Android:\n\t\t\treturn { st::historyPeer2UserpicBg, st::historyPeer2UserpicBg2 };\n\t\tcase DeviceType::Web:\n\t\tcase DeviceType::Chrome:\n\t\tcase DeviceType::Edge:\n\t\tcase DeviceType::Firefox:\n\t\tcase DeviceType::Safari:\n\t\t\treturn { st::historyPeer6UserpicBg, st::historyPeer6UserpicBg2 };\n\t\t}\n\t\tUnexpected(\"Type in GradientForType.\");\n\t}();\n\tauto gradient = QLinearGradient(0, 0, 0, size);\n\tgradient.setStops({\n\t\t{ 0.0, colors.first->c },\n\t\t{ 1.0, colors.second->c },\n\t});\n\treturn QBrush(std::move(gradient));\n}\n\n[[nodiscard]] const style::icon &IconForType(DeviceType type) {\n\tswitch (type) {\n\tcase DeviceType::Windows: return st::sessionIconWindows;\n\tcase DeviceType::Mac: return st::sessionIconMac;\n\tcase DeviceType::Ubuntu: return st::sessionIconUbuntu;\n\tcase DeviceType::Linux: return st::sessionIconLinux;\n\tcase DeviceType::iPhone: return st::sessionIconiPhone;\n\tcase DeviceType::iPad: return st::sessionIconiPad;\n\tcase DeviceType::Android: return st::sessionIconAndroid;\n\tcase DeviceType::Web: return st::sessionIconWeb;\n\tcase DeviceType::Chrome: return st::sessionIconChrome;\n\tcase DeviceType::Edge: return st::sessionIconEdge;\n\tcase DeviceType::Firefox: return st::sessionIconFirefox;\n\tcase DeviceType::Safari: return st::sessionIconSafari;\n\tcase DeviceType::Other: return st::sessionIconOther;\n\t}\n\tUnexpected(\"Type in IconForType.\");\n}\n\n[[nodiscard]] const style::icon *IconBigForType(DeviceType type) {\n\tswitch (type) {\n\tcase DeviceType::Web: return &st::sessionBigIconWeb;\n\tcase DeviceType::Other: return &st::sessionBigIconOther;\n\t}\n\treturn nullptr;\n}\n\n[[nodiscard]] std::unique_ptr<Lottie::Icon> LottieForType(DeviceType type) {\n\tif (IconBigForType(type)) {\n\t\treturn nullptr;\n\t}\n\tconst auto path = [&] {\n\t\tswitch (type) {\n\t\tcase DeviceType::Windows: return \"device_desktop_win\";\n\t\tcase DeviceType::Mac: return \"device_desktop_mac\";\n\t\tcase DeviceType::Ubuntu: return \"device_linux_ubuntu\";\n\t\tcase DeviceType::Linux: return \"device_linux\";\n\t\tcase DeviceType::iPhone: return \"device_phone_ios\";\n\t\tcase DeviceType::iPad: return \"device_tablet_ios\";\n\t\tcase DeviceType::Android: return \"device_phone_android\";\n\t\tcase DeviceType::Chrome: return \"device_web_chrome\";\n\t\tcase DeviceType::Edge: return \"device_web_edge\";\n\t\tcase DeviceType::Firefox: return \"device_web_firefox\";\n\t\tcase DeviceType::Safari: return \"device_web_safari\";\n\t\t}\n\t\tUnexpected(\"Type in LottieForType.\");\n\t}();\n\tconst auto size = st::sessionBigLottieSize;\n\treturn Lottie::MakeIcon({\n\t\t.path = u\":/icons/settings/devices/\"_q + path + u\".lottie\"_q,\n\t\t.sizeOverride = QSize(size, size),\n\t});\n}\n\n[[nodiscard]] QImage GenerateUserpic(DeviceType type) {\n\tconst auto size = st::sessionListItem.photoSize;\n\tconst auto full = size * style::DevicePixelRatio();\n\tconst auto rect = QRect(0, 0, size, size);\n\n\tauto result = QImage(full, full, QImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::transparent);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\n\tauto p = QPainter(&result);\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setBrush(GradientForType(type, size));\n\tp.setPen(Qt::NoPen);\n\tp.drawEllipse(rect);\n\tIconForType(type).paintInCenter(p, rect);\n\tp.end();\n\n\treturn result;\n}\n\n[[nodiscard]] not_null<Ui::RpWidget*> GenerateUserpicBig(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<> shown,\n\t\tDeviceType type) {\n\tconst auto size = st::sessionBigUserpicSize;\n\tconst auto full = size * style::DevicePixelRatio();\n\tconst auto rect = QRect(0, 0, size, size);\n\n\tconst auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());\n\tresult->resize(rect.size());\n\tstruct State {\n\t\tQImage background;\n\t\tstd::unique_ptr<Lottie::Icon> lottie;\n\t\tQImage lottieFrame;\n\t\tQImage colorizedFrame;\n\t};\n\tconst auto state = result->lifetime().make_state<State>();\n\tstate->background = QImage(\n\t\tfull,\n\t\tfull,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tstate->background.fill(Qt::transparent);\n\tstate->background.setDevicePixelRatio(style::DevicePixelRatio());\n\tstate->colorizedFrame = state->lottieFrame = state->background;\n\n\tauto p = QPainter(&state->background);\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setBrush(GradientForType(type, size));\n\tp.setPen(Qt::NoPen);\n\tp.drawEllipse(rect);\n\tif (const auto icon = IconBigForType(type)) {\n\t\ticon->paintInCenter(p, rect);\n\t}\n\tp.end();\n\n\tif ((state->lottie = LottieForType(type))) {\n\t\tstd::move(\n\t\t\tshown\n\t\t) | rpl::on_next([=] {\n\t\t\tstate->lottie->animate(\n\t\t\t\t[=] { result->update(); },\n\t\t\t\t0,\n\t\t\t\tstate->lottie->framesCount() - 1);\n\t\t}, result->lifetime());\n\t}\n\n\tresult->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(result);\n\t\tp.drawImage(QPoint(0, 0), state->background);\n\t\tif (state->lottie) {\n\t\t\tstate->lottieFrame.fill(Qt::black);\n\t\t\tauto q = QPainter(&state->lottieFrame);\n\t\t\tstate->lottie->paintInCenter(q, result->rect());\n\t\t\tq.end();\n\t\t\tstyle::colorizeImage(\n\t\t\t\tstate->lottieFrame,\n\t\t\t\tst::historyPeerUserpicFg->c,\n\t\t\t\t&state->colorizedFrame);\n\t\t\tp.drawImage(QPoint(0, 0), state->colorizedFrame);\n\n\t\t}\n\t}, result->lifetime());\n\n\treturn result;\n}\n\nvoid SessionInfoBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst EntryData &data,\n\t\tFn<void(uint64)> terminate) {\n\tbox->setWidth(st::boxWideWidth);\n\n\tconst auto shown = box->lifetime().make_state<rpl::event_stream<>>();\n\tbox->setShowFinishedCallback([=] {\n\t\tshown->fire({});\n\t});\n\n\tconst auto big = GenerateUserpicBig(\n\t\tbox,\n\t\tshown->events(),\n\t\tTypeFromEntry(data));\n\tbig->setNaturalWidth(big->width());\n\tbox->addRow(\n\t\tobject_ptr<Ui::RpWidget>::fromRaw(big),\n\t\tst::sessionBigCoverPadding,\n\t\tstyle::al_top);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\trpl::single(data.name),\n\t\t\tst::sessionBigName),\n\t\tstyle::al_top);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\trpl::single(\n\t\t\t\tlangDateTimeFull(base::unixtime::parse(data.activeTime))),\n\t\t\tst::sessionDateLabel),\n\t\tstyle::margins(0, 0, 0, st::sessionDateSkip),\n\t\tstyle::al_top);\n\n\tconst auto container = box->verticalLayout();\n\tUi::AddDivider(container);\n\tUi::AddSkip(container, st::sessionSubtitleSkip);\n\tUi::AddSubsectionTitle(container, tr::lng_sessions_info());\n\n\tAddSessionInfoRow(\n\t\tcontainer,\n\t\ttr::lng_sessions_application(),\n\t\tdata.info,\n\t\tst::menuIconDevices);\n\tAddSessionInfoRow(\n\t\tcontainer,\n\t\ttr::lng_sessions_system(),\n\t\tdata.system,\n\t\tst::menuIconInfo);\n\tAddSessionInfoRow(\n\t\tcontainer,\n\t\ttr::lng_sessions_ip(),\n\t\tdata.ip,\n\t\tst::menuIconIpAddress);\n\tAddSessionInfoRow(\n\t\tcontainer,\n\t\ttr::lng_sessions_location(),\n\t\tdata.location,\n\t\tst::menuIconAddress);\n\n\tAddSkip(container, st::sessionValueSkip);\n\tif (!data.location.isEmpty()) {\n\t\tAddDividerText(container, tr::lng_sessions_location_about());\n\t}\n\n\tbox->addButton(tr::lng_about_done(), [=] { box->closeBox(); });\n\tif (const auto hash = data.hash) {\n\t\tbox->addLeftButton(tr::lng_sessions_terminate(), [=] {\n\t\t\tconst auto weak = base::make_weak(box.get());\n\t\t\tterminate(hash);\n\t\t\tif (weak) {\n\t\t\t\tbox->closeBox();\n\t\t\t}\n\t\t}, st::attentionBoxButton);\n\t}\n}\n\nRow::Row(not_null<RowDelegate*> delegate, const EntryData &data)\n: PeerListRow(data.hash)\n, _delegate(delegate)\n, _location(st::defaultTextStyle, LocationAndDate(data))\n, _type(TypeFromEntry(data))\n, _data(data)\n, _userpic(GenerateUserpic(_type)) {\n\tsetCustomStatus(_data.info);\n}\n\nvoid Row::update(const EntryData &data) {\n\t_data = data;\n\tsetCustomStatus(_data.info);\n\trefreshName(st::sessionListItem);\n\t_location.setText(st::defaultTextStyle, LocationAndDate(_data));\n\t_type = TypeFromEntry(_data);\n\t_userpic = GenerateUserpic(_type);\n\t_delegate->rowUpdateRow(this);\n}\n\nEntryData Row::data() const {\n\treturn _data;\n}\n\nQString Row::generateName() {\n\treturn _data.name;\n}\n\nQString Row::generateShortName() {\n\treturn generateName();\n}\n\nPaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) {\n\treturn [=](\n\t\t\tQPainter &p,\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tint outerWidth,\n\t\t\tint size) {\n\t\tp.drawImage(x, y, _userpic);\n\t};\n}\n\nint Row::elementsCount() const {\n\treturn 2;\n}\n\nQRect Row::elementGeometry(int element, int outerWidth) const {\n\tswitch (element) {\n\tcase 1: {\n\t\treturn QRect(\n\t\t\tst::sessionListItem.namePosition.x(),\n\t\t\tst::sessionLocationTop,\n\t\t\touterWidth,\n\t\t\tst::normalFont->height);\n\t} break;\n\tcase 2: {\n\t\tconst auto size = QSize(\n\t\t\tst::sessionTerminate.width,\n\t\t\tst::sessionTerminate.height);\n\t\tconst auto right = st::sessionTerminateSkip;\n\t\tconst auto top = st::sessionTerminateTop;\n\t\tconst auto left = outerWidth - right - size.width();\n\t\treturn QRect(QPoint(left, top), size);\n\t} break;\n\t}\n\treturn QRect();\n}\n\nbool Row::elementDisabled(int element) const {\n\treturn !id() || (element == 1);\n}\n\nbool Row::elementOnlySelect(int element) const {\n\treturn false;\n}\n\nvoid Row::elementAddRipple(\n\t\tint element,\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) {\n}\n\nvoid Row::elementsStopLastRipple() {\n}\n\nvoid Row::elementsPaint(\n\t\tPainter &p,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tint selectedElement) {\n\tif (id()) {\n\t\tconst auto geometry = elementGeometry(2, outerWidth);\n\t\tconst auto position = geometry.topLeft()\n\t\t\t+ st::sessionTerminate.iconPosition;\n\t\tconst auto &icon = (selectedElement == 2)\n\t\t\t? st::sessionTerminate.iconOver\n\t\t\t: st::sessionTerminate.icon;\n\t\ticon.paint(p, position.x(), position.y(), outerWidth);\n\t}\n\tp.setFont(st::normalFont);\n\tp.setPen(st::sessionInfoFg);\n\tconst auto locationLeft = st::sessionListItem.namePosition.x();\n\tconst auto available = outerWidth - locationLeft;\n\t_location.drawLeftElided(\n\t\tp,\n\t\tlocationLeft,\n\t\tst::sessionLocationTop,\n\t\tavailable,\n\t\touterWidth);\n}\n\nclass SessionsContent : public Ui::RpWidget {\npublic:\n\tSessionsContent(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionController*> controller);\n\n\tvoid setupContent();\n\t[[nodiscard]] Ui::RpWidget *terminateAllButton() const;\n\t[[nodiscard]] Ui::RpWidget *autoTerminateButton() const;\n\t[[nodiscard]] Ui::RpWidget *currentHeader() const;\n\t[[nodiscard]] Ui::RpWidget *incompleteHeader() const;\n\t[[nodiscard]] Ui::RpWidget *otherHeader() const;\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tstruct Full {\n\t\tEntryData current;\n\t\tstd::vector<EntryData> incomplete;\n\t\tstd::vector<EntryData> list;\n\t};\n\tclass Inner;\n\tclass ListController;\n\n\tvoid shortPollSessions();\n\tvoid parse(const Api::Authorizations::List &list);\n\n\tvoid terminate(Fn<void()> terminateRequest, QString message);\n\tvoid terminateOne(uint64 hash);\n\tvoid terminateAll();\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<Api::Authorizations*> _authorizations;\n\n\trpl::variable<bool> _loading = false;\n\tFull _data;\n\n\tobject_ptr<Inner> _inner;\n\tbase::weak_qptr<Ui::BoxContent> _terminateBox;\n\n\tbase::Timer _shortPollTimer;\n\n};\n\nclass SessionsContent::ListController final\n\t: public PeerListController\n\t, public RowDelegate\n\t, public base::has_weak_ptr {\npublic:\n\texplicit ListController(not_null<::Main::Session*> session);\n\n\t::Main::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowElementClicked(not_null<PeerListRow*> row, int element) override;\n\n\tvoid rowUpdateRow(not_null<Row*> row) override;\n\n\tvoid showData(gsl::span<const EntryData> items);\n\trpl::producer<int> itemsCount() const;\n\trpl::producer<uint64> terminateRequests() const;\n\t[[nodiscard]] rpl::producer<EntryData> showRequests() const;\n\n\t[[nodiscard]] static std::unique_ptr<ListController> Add(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<::Main::Session*> session,\n\t\tstyle::margins margins = {});\n\nprivate:\n\tconst not_null<::Main::Session*> _session;\n\n\trpl::event_stream<uint64> _terminateRequests;\n\trpl::event_stream<int> _itemsCount;\n\trpl::event_stream<EntryData> _showRequests;\n\n};\n\nclass SessionsContent::Inner : public Ui::RpWidget {\npublic:\n\tInner(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\trpl::producer<int> ttlDays);\n\n\tvoid showData(const Full &data);\n\t[[nodiscard]] rpl::producer<EntryData> showRequests() const;\n\t[[nodiscard]] rpl::producer<uint64> terminateOne() const;\n\t[[nodiscard]] rpl::producer<> terminateAll() const;\n\t[[nodiscard]] Ui::RpWidget *terminateAllButton() const;\n\t[[nodiscard]] Ui::RpWidget *autoTerminateButton() const;\n\t[[nodiscard]] Ui::RpWidget *currentHeader() const;\n\t[[nodiscard]] Ui::RpWidget *incompleteHeader() const;\n\t[[nodiscard]] Ui::RpWidget *otherHeader() const;\n\nprivate:\n\tvoid setupContent();\n\n\tconst not_null<Window::SessionController*> _controller;\n\tstd::unique_ptr<ListController> _current;\n\tQPointer<Ui::SettingsButton> _terminateAll;\n\tQPointer<Ui::SettingsButton> _autoTerminate;\n\tQPointer<Ui::RpWidget> _currentHeader;\n\tQPointer<Ui::RpWidget> _incompleteHeader;\n\tQPointer<Ui::RpWidget> _otherHeader;\n\tstd::unique_ptr<ListController> _incomplete;\n\tstd::unique_ptr<ListController> _list;\n\trpl::variable<int> _ttlDays;\n\n};\n\nSessionsContent::SessionsContent(\n\tQWidget*,\n\tnot_null<Window::SessionController*> controller)\n: _controller(controller)\n, _authorizations(&controller->session().api().authorizations())\n, _inner(this, controller, _authorizations->ttlDays())\n, _shortPollTimer([=] { shortPollSessions(); }) {\n}\n\nvoid SessionsContent::setupContent() {\n\t_inner->resize(width(), st::noContactsHeight);\n\n\t_inner->heightValue(\n\t) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](int height) {\n\t\tresize(width(), height);\n\t}, _inner->lifetime());\n\n\t_inner->showRequests(\n\t) | rpl::on_next([=](const EntryData &data) {\n\t\t_controller->show(Box(\n\t\t\tSessionInfoBox,\n\t\t\tdata,\n\t\t\t[=](uint64 hash) { terminateOne(hash); }));\n\t}, lifetime());\n\n\t_inner->terminateOne(\n\t) | rpl::on_next([=](uint64 hash) {\n\t\tterminateOne(hash);\n\t}, lifetime());\n\n\t_inner->terminateAll(\n\t) | rpl::on_next([=] {\n\t\tterminateAll();\n\t}, lifetime());\n\n\t_loading.changes(\n\t) | rpl::on_next([=](bool value) {\n\t\t_inner->setVisible(!value);\n\t}, lifetime());\n\n\t_authorizations->listValue(\n\t) | rpl::on_next([=](const Api::Authorizations::List &list) {\n\t\tparse(list);\n\t}, lifetime());\n\n\t_loading = true;\n\tshortPollSessions();\n}\n\nvoid SessionsContent::parse(const Api::Authorizations::List &list) {\n\tif (list.empty()) {\n\t\treturn;\n\t}\n\t_data = Full();\n\tfor (const auto &auth : list) {\n\t\tif (!auth.hash) {\n\t\t\t_data.current = auth;\n\t\t} else if (auth.incomplete) {\n\t\t\t_data.incomplete.push_back(auth);\n\t\t} else {\n\t\t\t_data.list.push_back(auth);\n\t\t}\n\t}\n\n\t_loading = false;\n\n\tranges::sort(_data.list, std::greater<>(), &EntryData::activeTime);\n\tranges::sort(_data.incomplete, std::greater<>(), &EntryData::activeTime);\n\n\t_inner->showData(_data);\n\n\t_shortPollTimer.callOnce(kShortPollTimeout);\n}\n\nvoid SessionsContent::resizeEvent(QResizeEvent *e) {\n\tRpWidget::resizeEvent(e);\n\n\t_inner->resize(width(), _inner->height());\n}\n\nvoid SessionsContent::paintEvent(QPaintEvent *e) {\n\tRpWidget::paintEvent(e);\n\n\tPainter p(this);\n\n\tif (_loading.current()) {\n\t\tp.setFont(st::noContactsFont);\n\t\tp.setPen(st::noContactsColor);\n\t\tp.drawText(\n\t\t\tQRect(0, 0, width(), st::noContactsHeight),\n\t\t\ttr::lng_contacts_loading(tr::now),\n\t\t\tstyle::al_center);\n\t}\n}\n\nvoid SessionsContent::shortPollSessions() {\n\tconst auto left = kShortPollTimeout\n\t\t- (crl::now() - _authorizations->lastReceivedTime());\n\tif (left > 0) {\n\t\tparse(_authorizations->list());\n\t\t_shortPollTimer.cancel();\n\t\t_shortPollTimer.callOnce(left);\n\t} else {\n\t\t_authorizations->reload();\n\t}\n\tupdate();\n}\n\nvoid SessionsContent::terminate(Fn<void()> terminateRequest, QString message) {\n\tif (_terminateBox) {\n\t\t_terminateBox->deleteLater();\n\t}\n\tconst auto callback = crl::guard(this, [=] {\n\t\tif (_terminateBox) {\n\t\t\t_terminateBox->closeBox();\n\t\t\t_terminateBox = nullptr;\n\t\t}\n\t\tterminateRequest();\n\t});\n\tauto box = Ui::MakeConfirmBox({\n\t\t.text = message,\n\t\t.confirmed = callback,\n\t\t.confirmText = tr::lng_settings_reset_button(),\n\t\t.confirmStyle = &st::attentionBoxButton,\n\t});\n\t_terminateBox = base::make_weak(box.data());\n\t_controller->show(std::move(box));\n}\n\nvoid SessionsContent::terminateOne(uint64 hash) {\n\tconst auto weak = base::make_weak(this);\n\tauto callback = [=] {\n\t\tauto done = crl::guard(weak, [=](const MTPBool &result) {\n\t\t\tif (mtpIsFalse(result)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto removeByHash = [&](std::vector<EntryData> &list) {\n\t\t\t\tlist.erase(\n\t\t\t\t\tranges::remove(\n\t\t\t\t\t\tlist,\n\t\t\t\t\t\thash,\n\t\t\t\t\t\t[](const EntryData &entry) { return entry.hash; }),\n\t\t\t\t\tend(list));\n\t\t\t};\n\t\t\tremoveByHash(_data.incomplete);\n\t\t\tremoveByHash(_data.list);\n\t\t\t_inner->showData(_data);\n\t\t});\n\t\tauto fail = crl::guard(weak, [=](const MTP::Error &error) {\n\t\t});\n\t\t_authorizations->requestTerminate(\n\t\t\tstd::move(done),\n\t\t\tstd::move(fail),\n\t\t\thash);\n\t};\n\tterminate(std::move(callback), tr::lng_settings_reset_one_sure(tr::now));\n}\n\nvoid SessionsContent::terminateAll() {\n\tconst auto weak = base::make_weak(this);\n\tauto callback = [=] {\n\t\tconst auto reset = crl::guard(weak, [=] {\n\t\t\t_authorizations->cancelCurrentRequest();\n\t\t\t_authorizations->reload();\n\t\t});\n\t\t_authorizations->requestTerminate(\n\t\t\t[=](const MTPBool &result) { reset(); },\n\t\t\t[=](const MTP::Error &result) { reset(); });\n\t\t_loading = true;\n\t};\n\tterminate(std::move(callback), tr::lng_settings_reset_sure(tr::now));\n}\n\nUi::RpWidget *SessionsContent::terminateAllButton() const {\n\treturn _inner ? _inner->terminateAllButton() : nullptr;\n}\n\nUi::RpWidget *SessionsContent::autoTerminateButton() const {\n\treturn _inner ? _inner->autoTerminateButton() : nullptr;\n}\n\nUi::RpWidget *SessionsContent::currentHeader() const {\n\treturn _inner ? _inner->currentHeader() : nullptr;\n}\n\nUi::RpWidget *SessionsContent::incompleteHeader() const {\n\treturn _inner ? _inner->incompleteHeader() : nullptr;\n}\n\nUi::RpWidget *SessionsContent::otherHeader() const {\n\treturn _inner ? _inner->otherHeader() : nullptr;\n}\n\nSessionsContent::Inner::Inner(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\trpl::producer<int> ttlDays)\n: RpWidget(parent)\n, _controller(controller)\n, _ttlDays(std::move(ttlDays)) {\n\tsetupContent();\n}\n\nvoid SessionsContent::Inner::setupContent() {\n\tusing namespace rpl::mappers;\n\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\t_currentHeader = AddSubsectionTitle(\n\t\tcontent,\n\t\ttr::lng_sessions_header());\n\tconst auto rename = Ui::CreateChild<Ui::LinkButton>(\n\t\tcontent,\n\t\ttr::lng_settings_rename_device(tr::now),\n\t\tst::defaultLinkButton);\n\trpl::combine(\n\t\tcontent->sizeValue(),\n\t\t_currentHeader->positionValue()\n\t) | rpl::on_next([=](QSize outer, QPoint position) {\n\t\tconst auto x = st::sessionTerminateSkip\n\t\t\t+ st::sessionTerminate.iconPosition.x();\n\t\tconst auto y = st::defaultSubsectionTitlePadding.top()\n\t\t\t+ st::defaultSubsectionTitle.style.font->ascent\n\t\t\t- st::defaultLinkButton.font->ascent;\n\t\trename->moveToRight(x, y, outer.width());\n\t}, rename->lifetime());\n\trename->setClickedCallback([=] {\n\t\t_controller->show(Box(RenameBox));\n\t});\n\n\tconst auto session = &_controller->session();\n\t_current = ListController::Add(\n\t\tcontent,\n\t\tsession,\n\t\tstyle::margins{ 0, 0, 0, st::sessionCurrentSkip });\n\tconst auto terminateWrap = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::VerticalLayout>(content)))->setDuration(0);\n\tconst auto terminateInner = terminateWrap->entity();\n\t_terminateAll = terminateInner->add(\n\t\tCreateButtonWithIcon(\n\t\t\tterminateInner,\n\t\t\ttr::lng_sessions_terminate_all(),\n\t\t\tst::infoBlockButton,\n\t\t\t{ .icon = &st::infoIconBlock }));\n\tAddSkip(terminateInner);\n\tAddDividerText(terminateInner, tr::lng_sessions_terminate_all_about());\n\n\tconst auto incompleteWrap = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::VerticalLayout>(content)))->setDuration(0);\n\tconst auto incompleteInner = incompleteWrap->entity();\n\tAddSkip(incompleteInner, st::sessionSubtitleSkip);\n\t_incompleteHeader = AddSubsectionTitle(incompleteInner, tr::lng_sessions_incomplete());\n\t_incomplete = ListController::Add(incompleteInner, session);\n\tAddSkip(incompleteInner);\n\tAddDividerText(incompleteInner, tr::lng_sessions_incomplete_about());\n\n\tconst auto listWrap = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::VerticalLayout>(content)))->setDuration(0);\n\tconst auto listInner = listWrap->entity();\n\tAddSkip(listInner, st::sessionSubtitleSkip);\n\t_otherHeader = AddSubsectionTitle(listInner, tr::lng_sessions_other_header());\n\t_list = ListController::Add(listInner, session);\n\tAddSkip(listInner);\n\tAddDividerText(listInner, tr::lng_sessions_about_apps());\n\n\tconst auto ttlWrap = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::VerticalLayout>(content)))->setDuration(0);\n\tconst auto ttlInner = ttlWrap->entity();\n\tAddSkip(ttlInner, st::sessionSubtitleSkip);\n\tAddSubsectionTitle(ttlInner, tr::lng_settings_terminate_title());\n\n\t_autoTerminate = AddButtonWithLabel(\n\t\tttlInner,\n\t\ttr::lng_settings_terminate_if(),\n\t\t_ttlDays.value() | rpl::map(SelfDestructionBox::DaysLabel),\n\t\tst::settingsButtonNoIcon);\n\t_autoTerminate->addClickHandler([=] {\n\t\t_controller->show(Box<SelfDestructionBox>(\n\t\t\t&_controller->session(),\n\t\t\tSelfDestructionBox::Type::Sessions,\n\t\t\t_ttlDays.value()));\n\t});\n\n\tAddSkip(ttlInner);\n\n\tconst auto placeholder = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::FlatLabel>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_sessions_other_desc(),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::defaultBoxDividerLabelPadding))->setDuration(0);\n\n\tterminateWrap->toggleOn(\n\t\trpl::combine(\n\t\t\t_incomplete->itemsCount(),\n\t\t\t_list->itemsCount(),\n\t\t\t(_1 + _2) > 0));\n\tincompleteWrap->toggleOn(_incomplete->itemsCount() | rpl::map(_1 > 0));\n\tlistWrap->toggleOn(_list->itemsCount() | rpl::map(_1 > 0));\n\tttlWrap->toggleOn(_list->itemsCount() | rpl::map(_1 > 0));\n\tplaceholder->toggleOn(_list->itemsCount() | rpl::map(_1 == 0));\n\n\tUi::ResizeFitChild(this, content);\n}\n\nvoid SessionsContent::Inner::showData(const Full &data) {\n\t_current->showData({ &data.current, &data.current + 1 });\n\t_list->showData(data.list);\n\t_incomplete->showData(data.incomplete);\n}\n\nrpl::producer<> SessionsContent::Inner::terminateAll() const {\n\treturn _terminateAll->clicks() | rpl::to_empty;\n}\n\nUi::RpWidget *SessionsContent::Inner::terminateAllButton() const {\n\treturn _terminateAll.data();\n}\n\nUi::RpWidget *SessionsContent::Inner::autoTerminateButton() const {\n\treturn _autoTerminate.data();\n}\n\nUi::RpWidget *SessionsContent::Inner::currentHeader() const {\n\treturn _currentHeader.data();\n}\n\nUi::RpWidget *SessionsContent::Inner::incompleteHeader() const {\n\treturn _incompleteHeader.data();\n}\n\nUi::RpWidget *SessionsContent::Inner::otherHeader() const {\n\treturn _otherHeader.data();\n}\n\nrpl::producer<uint64> SessionsContent::Inner::terminateOne() const {\n\treturn rpl::merge(\n\t\t_incomplete->terminateRequests(),\n\t\t_list->terminateRequests());\n}\n\nrpl::producer<EntryData> SessionsContent::Inner::showRequests() const {\n\treturn rpl::merge(\n\t\t_current->showRequests(),\n\t\t_incomplete->showRequests(),\n\t\t_list->showRequests());\n}\n\nSessionsContent::ListController::ListController(\n\tnot_null<::Main::Session*> session)\n: _session(session) {\n}\n\n::Main::Session &SessionsContent::ListController::session() const {\n\treturn *_session;\n}\n\nvoid SessionsContent::ListController::prepare() {\n}\n\nvoid SessionsContent::ListController::rowClicked(\n\t\tnot_null<PeerListRow*> row) {\n\t_showRequests.fire_copy(static_cast<Row*>(row.get())->data());\n}\n\nvoid SessionsContent::ListController::rowElementClicked(\n\t\tnot_null<PeerListRow*> row,\n\t\tint element) {\n\tif (element == 2) {\n\t\tif (const auto hash = static_cast<Row*>(row.get())->data().hash) {\n\t\t\t_terminateRequests.fire_copy(hash);\n\t\t}\n\t}\n}\n\nvoid SessionsContent::ListController::rowUpdateRow(not_null<Row*> row) {\n\tdelegate()->peerListUpdateRow(row);\n}\n\nvoid SessionsContent::ListController::showData(\n\t\tgsl::span<const EntryData> items) {\n\tauto index = 0;\n\tauto positions = base::flat_map<uint64, int>();\n\tpositions.reserve(items.size());\n\tfor (const auto &entry : items) {\n\t\tconst auto id = entry.hash;\n\t\tpositions.emplace(id, index++);\n\t\tif (const auto row = delegate()->peerListFindRow(id)) {\n\t\t\tstatic_cast<Row*>(row)->update(entry);\n\t\t} else {\n\t\t\tdelegate()->peerListAppendRow(\n\t\t\t\tstd::make_unique<Row>(this, entry));\n\t\t}\n\t}\n\tfor (auto i = 0; i != delegate()->peerListFullRowsCount();) {\n\t\tconst auto row = delegate()->peerListRowAt(i);\n\t\tif (positions.contains(row->id())) {\n\t\t\t++i;\n\t\t\tcontinue;\n\t\t}\n\t\tdelegate()->peerListRemoveRow(row);\n\t}\n\tdelegate()->peerListSortRows([&](\n\t\t\tconst PeerListRow &a,\n\t\t\tconst PeerListRow &b) {\n\t\treturn positions[a.id()] < positions[b.id()];\n\t});\n\tdelegate()->peerListRefreshRows();\n\t_itemsCount.fire(delegate()->peerListFullRowsCount());\n}\n\nrpl::producer<int> SessionsContent::ListController::itemsCount() const {\n\treturn _itemsCount.events_starting_with(\n\t\tdelegate()->peerListFullRowsCount());\n}\n\nrpl::producer<uint64> SessionsContent::ListController::terminateRequests() const {\n\treturn _terminateRequests.events();\n}\n\nrpl::producer<EntryData> SessionsContent::ListController::showRequests() const {\n\treturn _showRequests.events();\n}\n\nauto SessionsContent::ListController::Add(\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<::Main::Session*> session,\n\tstyle::margins margins)\n-> std::unique_ptr<ListController> {\n\tauto &lifetime = container->lifetime();\n\tconst auto delegate = lifetime.make_state<\n\t\tPeerListContentDelegateSimple\n\t>();\n\tauto controller = std::make_unique<ListController>(session);\n\tcontroller->setStyleOverrides(&st::sessionList);\n\tconst auto content = container->add(\n\t\tobject_ptr<PeerListContent>(\n\t\t\tcontainer,\n\t\t\tcontroller.get()),\n\t\tmargins);\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\treturn controller;\n}\n\nvoid BuildSessionsSection(SectionBuilder &builder) {\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"sessions/current\"_q,\n\t\t\t.title = tr::lng_sessions_header(tr::now),\n\t\t\t.keywords = { u\"current\"_q, u\"device\"_q, u\"session\"_q },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"sessions/terminate-all\"_q,\n\t\t\t.title = tr::lng_sessions_terminate_all(tr::now),\n\t\t\t.keywords = { u\"terminate\"_q, u\"logout\"_q, u\"sign out\"_q },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"sessions/incomplete\"_q,\n\t\t\t.title = tr::lng_sessions_incomplete(tr::now),\n\t\t\t.keywords = { u\"incomplete\"_q, u\"unconfirmed\"_q },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"sessions/other\"_q,\n\t\t\t.title = tr::lng_sessions_other_header(tr::now),\n\t\t\t.keywords = { u\"other\"_q, u\"active\"_q, u\"sessions\"_q },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"sessions/auto-terminate\"_q,\n\t\t\t.title = tr::lng_settings_terminate_if(tr::now),\n\t\t\t.keywords = { u\"auto\"_q, u\"terminate\"_q, u\"inactive\"_q, u\"timeout\"_q },\n\t\t};\n\t});\n}\n\nclass Sessions : public Section<Sessions> {\npublic:\n\tSessions(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\tvoid showFinished() override;\n\nprivate:\n\tvoid setupContent();\n\n\trpl::event_stream<> _showFinished;\n\n};\n\nSessions::Sessions(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent();\n}\n\nrpl::producer<QString> Sessions::title() {\n\treturn tr::lng_settings_sessions_title();\n}\n\nvoid Sessions::showFinished() {\n\t_showFinished.fire({});\n\tSection::showFinished();\n}\n\nvoid Sessions::setupContent() {\n\tconst auto container = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tconst SectionBuildMethod buildMethod = [](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tauto &lifetime = container->lifetime();\n\t\tconst auto highlights = lifetime.make_state<HighlightRegistry>();\n\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = Window::PausedIn(\n\t\t\t\tcontroller,\n\t\t\t\tWindow::GifPauseReason::Layer),\n\t\t\t.highlights = highlights,\n\t\t});\n\n\t\tbuilder.addSkip(st::settingsPrivacySkip);\n\n\t\tbuilder.add([=](const WidgetContext &ctx) {\n\t\t\tconst auto content = ctx.container->add(\n\t\t\t\tobject_ptr<SessionsContent>(ctx.container, ctx.controller));\n\t\t\tcontent->setupContent();\n\n\t\t\tif (ctx.highlights) {\n\t\t\t\tctx.highlights->push_back({\n\t\t\t\t\tu\"sessions/current\"_q,\n\t\t\t\t\t{ content->currentHeader(), SubsectionTitleHighlight() },\n\t\t\t\t});\n\t\t\t\tctx.highlights->push_back({\n\t\t\t\t\tu\"sessions/terminate-all\"_q,\n\t\t\t\t\t{ content->terminateAllButton() },\n\t\t\t\t});\n\t\t\t\tctx.highlights->push_back({\n\t\t\t\t\tu\"sessions/incomplete\"_q,\n\t\t\t\t\t{ content->incompleteHeader(), SubsectionTitleHighlight() },\n\t\t\t\t});\n\t\t\t\tctx.highlights->push_back({\n\t\t\t\t\tu\"sessions/other\"_q,\n\t\t\t\t\t{ content->otherHeader(), SubsectionTitleHighlight() },\n\t\t\t\t});\n\t\t\t\tctx.highlights->push_back({\n\t\t\t\t\tu\"sessions/auto-terminate\"_q,\n\t\t\t\t\t{ content->autoTerminateButton() },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn SectionBuilder::WidgetToAdd{};\n\t\t});\n\n\t\tstd::move(showFinished) | rpl::on_next([=] {\n\t\t\tfor (const auto &[id, entry] : *highlights) {\n\t\t\t\tif (entry.widget) {\n\t\t\t\t\tcontroller->checkHighlightControl(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tentry.widget,\n\t\t\t\t\t\tbase::duplicate(entry.args));\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime);\n\t};\n\n\tbuild(container, buildMethod);\n\n\tUi::ResizeFitChild(this, container);\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = Sessions::Id(),\n\t.parentId = PrivacySecurityId(),\n\t.title = &tr::lng_settings_sessions_title,\n\t.icon = &st::menuIconDevices,\n}, [](SectionBuilder &builder) {\n\tBuildSessionsSection(builder);\n});\n\n} // namespace\n\nvoid AddSessionInfoRow(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> label,\n\t\tconst QString &value,\n\t\tconst style::icon &icon) {\n\tif (value.isEmpty()) {\n\t\treturn;\n\t}\n\n\tconst auto text = container->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\trpl::single(value),\n\t\t\tst::boxLabel),\n\t\tst::boxRowPadding + st::sessionValuePadding);\n\tconst auto left = st::sessionValuePadding.left();\n\tcontainer->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\tstd::move(label),\n\t\t\tst::sessionValueLabel),\n\t\t(st::boxRowPadding\n\t\t\t+ style::margins{ left, 0, 0, st::sessionValueSkip }));\n\n\tconst auto widget = Ui::CreateChild<Ui::RpWidget>(container.get());\n\twidget->resize(icon.size());\n\n\ttext->topValue() | rpl::on_next([=](int top) {\n\t\twidget->move(st::sessionValueIconPosition + QPoint(0, top));\n\t}, widget->lifetime());\n\n\twidget->paintRequest() | rpl::on_next([=, &icon] {\n\t\tauto p = QPainter(widget);\n\t\ticon.paintInCenter(p, widget->rect());\n\t}, widget->lifetime());\n}\n\nType SessionsId() {\n\treturn Sessions::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_active_sessions.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Settings {\n\n[[nodiscard]] Type SessionsId();\n\nvoid AddSessionInfoRow(\n\tnot_null<Ui::VerticalLayout*> container,\n\trpl::producer<QString> label,\n\tconst QString &value,\n\tconst style::icon &icon);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_advanced.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_advanced.h\"\n\n#include \"settings/settings_common_session.h\"\n\n#include \"api/api_global_privacy.h\"\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"base/platform/base_platform_custom_app_icon.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/screen_reader_state.h\"\n#include \"boxes/about_box.h\"\n#include \"boxes/auto_download_box.h\"\n#include \"boxes/connection_box.h\"\n#include \"boxes/download_path_box.h\"\n#include \"boxes/local_storage_box.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/file_utilities.h\"\n#include \"core/launcher.h\"\n#include \"core/update_checker.h\"\n#include \"data/data_auto_download.h\"\n#include \"export/export_manager.h\"\n#include \"info/downloads/info_downloads_widget.h\"\n#include \"info/info_memento.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/facade.h\"\n#include \"mtproto/mtp_instance.h\"\n#include \"platform/platform_specific.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"settings/sections/settings_chat.h\"\n#include \"settings/settings_experimental.h\"\n#include \"settings/settings_power_saving.h\"\n#include \"settings/sections/settings_privacy_security.h\"\n#include \"storage/localstorage.h\"\n#include \"storage/storage_domain.h\"\n#include \"tray.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/single_choice_box.h\"\n#include \"ui/gl/gl_detection.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/platform/ui_platform_window.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/screen_reader_mode.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\n#ifdef Q_OS_MAC\n#include \"base/platform/mac/base_confirm_quit.h\"\n#endif // Q_OS_MAC\n\n#ifndef TDESKTOP_DISABLE_SPELLCHECK\n#include \"boxes/dictionaries_manager.h\"\n#include \"chat_helpers/spellchecker_common.h\"\n#include \"spellcheck/platform/platform_spellcheck.h\"\n#endif // !TDESKTOP_DISABLE_SPELLCHECK\n\nnamespace Settings {\nnamespace {\n\nusing namespace Builder;\n\n#if defined Q_OS_MAC && !defined OS_MAC_STORE\n[[nodiscard]] const QImage &IconMacRound() {\n\tstatic const auto result = QImage(u\":/gui/art/icon_round512@2x.png\"_q);\n\treturn result;\n}\n#endif // Q_OS_MAC && !OS_MAC_STORE\n\nvoid BuildDataStorageSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto container = builder.container();\n\tconst auto session = builder.session();\n\tconst auto account = &session->account();\n\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"advanced/data_storage\"_q,\n\t\t.title = tr::lng_settings_data_storage(),\n\t\t.keywords = { u\"storage\"_q, u\"data\"_q, u\"download\"_q, u\"connection\"_q },\n\t});\n\n\tconst auto connectionType = [=] {\n\t\tconst auto transport = account->mtp().dctransport();\n\t\tif (!Core::App().settings().proxy().isEnabled()) {\n\t\t\treturn transport.isEmpty()\n\t\t\t\t? tr::lng_connection_auto_connecting(tr::now)\n\t\t\t\t: tr::lng_connection_auto(tr::now, lt_transport, transport);\n\t\t} else {\n\t\t\treturn transport.isEmpty()\n\t\t\t\t? tr::lng_connection_proxy_connecting(tr::now)\n\t\t\t\t: tr::lng_connection_proxy(tr::now, lt_transport, transport);\n\t\t}\n\t};\n\n\tbuilder.addButton({\n\t\t.id = u\"advanced/connection_type\"_q,\n\t\t.title = tr::lng_settings_connection_type(),\n\t\t.icon = { &st::menuIconNetwork },\n\t\t.label = rpl::merge(\n\t\t\tCore::App().settings().proxy().connectionTypeChanges(),\n\t\t\ttr::lng_connection_auto_connecting() | rpl::to_empty\n\t\t) | rpl::map(connectionType),\n\t\t.onClick = [=] {\n\t\t\tcontroller->window().show(\n\t\t\t\tProxiesBoxController::CreateOwningBox(account));\n\t\t},\n\t\t.keywords = { u\"connection\"_q, u\"proxy\"_q, u\"network\"_q, u\"vpn\"_q },\n\t});\n\n\tconst auto showDownloadPath = container\n\t\t? container->lifetime().make_state<rpl::variable<bool>>(\n\t\t\t!Core::App().settings().askDownloadPath())\n\t\t: nullptr;\n\n\tauto downloadLabel = Core::App().settings().downloadPathValue(\n\t) | rpl::map([](const QString &text) {\n\t\tif (text.isEmpty()) {\n\t\t\treturn Core::App().canReadDefaultDownloadPath()\n\t\t\t\t? tr::lng_download_path_default(tr::now)\n\t\t\t\t: tr::lng_download_path_temp(tr::now);\n\t\t} else if (text == FileDialog::Tmp()) {\n\t\t\treturn tr::lng_download_path_temp(tr::now);\n\t\t}\n\t\treturn QDir::toNativeSeparators(text);\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"advanced/download_path\"_q,\n\t\t.title = tr::lng_download_path(),\n\t\t.icon = { &st::menuIconShowInFolder },\n\t\t.label = std::move(downloadLabel),\n\t\t.onClick = [=] {\n\t\t\tcontroller->show(Box<DownloadPathBox>(controller));\n\t\t},\n\t\t.keywords = { u\"download\"_q, u\"path\"_q, u\"folder\"_q },\n\t\t.shown = showDownloadPath\n\t\t\t? showDownloadPath->value()\n\t\t\t: rpl::single(true) | rpl::type_erased,\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"advanced/storage\"_q,\n\t\t.title = tr::lng_settings_manage_local_storage(),\n\t\t.icon = { &st::menuIconStorage },\n\t\t.onClick = [=] {\n\t\t\tLocalStorageBox::Show(controller);\n\t\t},\n\t\t.keywords = { u\"storage\"_q, u\"cache\"_q, u\"local\"_q },\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"advanced/downloads\"_q,\n\t\t.title = tr::lng_downloads_section(),\n\t\t.icon = { &st::menuIconDownload },\n\t\t.onClick = [=] {\n\t\t\tif (controller) {\n\t\t\t\tcontroller->showSection(\n\t\t\t\t\tInfo::Downloads::Make(controller->session().user()));\n\t\t\t}\n\t\t},\n\t\t.keywords = { u\"downloads\"_q, u\"files\"_q },\n\t});\n\n\tconst auto askDownloadPath = builder.addButton({\n\t\t.id = u\"advanced/ask_download\"_q,\n\t\t.title = tr::lng_download_path_ask(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.toggled = rpl::single(Core::App().settings().askDownloadPath()),\n\t\t.keywords = { u\"download\"_q, u\"path\"_q, u\"ask\"_q },\n\t});\n\n\tif (askDownloadPath) {\n\t\taskDownloadPath->toggledValue(\n\t\t) | rpl::filter([](bool checked) {\n\t\t\treturn (checked != Core::App().settings().askDownloadPath());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tCore::App().settings().setAskDownloadPath(checked);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\tif (showDownloadPath) {\n\t\t\t\t*showDownloadPath = !checked;\n\t\t\t}\n\t\t}, askDownloadPath->lifetime());\n\t}\n\n\tbuilder.addSkip(st::settingsCheckboxesSkip);\n}\n\nvoid BuildAutoDownloadSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto session = builder.session();\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"advanced/auto_download\"_q,\n\t\t.title = tr::lng_media_auto_settings(),\n\t\t.keywords = { u\"auto\"_q, u\"download\"_q, u\"media\"_q },\n\t});\n\n\tusing Source = Data::AutoDownload::Source;\n\n\tbuilder.addButton({\n\t\t.id = u\"advanced/auto_download_private\"_q,\n\t\t.title = tr::lng_media_auto_in_private(),\n\t\t.icon = { &st::menuIconProfile },\n\t\t.onClick = [=] {\n\t\t\tcontroller->show(Box<AutoDownloadBox>(session, Source::User));\n\t\t},\n\t\t.keywords = { u\"auto\"_q, u\"download\"_q, u\"private\"_q, u\"media\"_q },\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"advanced/auto_download_groups\"_q,\n\t\t.title = tr::lng_media_auto_in_groups(),\n\t\t.icon = { &st::menuIconGroups },\n\t\t.onClick = [=] {\n\t\t\tcontroller->show(Box<AutoDownloadBox>(session, Source::Group));\n\t\t},\n\t\t.keywords = { u\"auto\"_q, u\"download\"_q, u\"groups\"_q, u\"media\"_q },\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"advanced/auto_download_channels\"_q,\n\t\t.title = tr::lng_media_auto_in_channels(),\n\t\t.icon = { &st::menuIconChannel },\n\t\t.onClick = [=] {\n\t\t\tcontroller->show(Box<AutoDownloadBox>(session, Source::Channel));\n\t\t},\n\t\t.keywords = { u\"auto\"_q, u\"download\"_q, u\"channels\"_q, u\"media\"_q },\n\t});\n\n\tbuilder.addSkip(st::settingsCheckboxesSkip);\n}\n\nvoid BuildWindowTitleSection(SectionBuilder &builder) {\n\tconst auto settings = &Core::App().settings();\n\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"advanced/window_title\"_q,\n\t\t.title = tr::lng_settings_window_system(),\n\t\t.keywords = { u\"window\"_q, u\"title\"_q, u\"frame\"_q },\n\t});\n\n\tconst auto content = [=] {\n\t\treturn settings->windowTitleContent();\n\t};\n\n\tconst auto showChatName = builder.addCheckbox({\n\t\t.id = u\"advanced/title_chat_name\"_q,\n\t\t.title = tr::lng_settings_title_chat_name(),\n\t\t.checked = !content().hideChatName,\n\t\t.keywords = { u\"title\"_q, u\"chat\"_q, u\"name\"_q },\n\t});\n\tif (showChatName) {\n\t\tshowChatName->checkedChanges(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\treturn (checked == content().hideChatName);\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tauto updated = content();\n\t\t\tupdated.hideChatName = !checked;\n\t\t\tsettings->setWindowTitleContent(updated);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}, showChatName->lifetime());\n\t}\n\n\tconst auto showAccountName = (Core::App().domain().accountsAuthedCount() > 1)\n\t\t? builder.addCheckbox({\n\t\t\t.id = u\"advanced/title_account_name\"_q,\n\t\t\t.title = tr::lng_settings_title_account_name(),\n\t\t\t.checked = !content().hideAccountName,\n\t\t\t.keywords = { u\"title\"_q, u\"account\"_q, u\"name\"_q },\n\t\t})\n\t\t: nullptr;\n\tif (showAccountName) {\n\t\tshowAccountName->checkedChanges(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\treturn (checked == content().hideAccountName);\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tauto updated = content();\n\t\t\tupdated.hideAccountName = !checked;\n\t\t\tsettings->setWindowTitleContent(updated);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}, showAccountName->lifetime());\n\t}\n\n\tconst auto showTotalUnread = builder.addCheckbox({\n\t\t.id = u\"advanced/title_total_unread\"_q,\n\t\t.title = tr::lng_settings_title_total_count(),\n\t\t.checked = !content().hideTotalUnread,\n\t\t.keywords = { u\"title\"_q, u\"unread\"_q, u\"count\"_q, u\"badge\"_q },\n\t});\n\tif (showTotalUnread) {\n\t\tshowTotalUnread->checkedChanges(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\treturn (checked == content().hideTotalUnread);\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tauto updated = content();\n\t\t\tupdated.hideTotalUnread = !checked;\n\t\t\tsettings->setWindowTitleContent(updated);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}, showTotalUnread->lifetime());\n\t}\n\n\tif (Ui::Platform::NativeWindowFrameSupported()) {\n\t\tconst auto nativeFrame = builder.addCheckbox({\n\t\t\t.id = u\"advanced/native_frame\"_q,\n\t\t\t.title = Platform::IsWayland()\n\t\t\t\t? tr::lng_settings_qt_frame()\n\t\t\t\t: tr::lng_settings_native_frame(),\n\t\t\t.checked = settings->nativeWindowFrame(),\n\t\t\t.keywords = { u\"frame\"_q, u\"native\"_q, u\"window\"_q, u\"border\"_q },\n\t\t});\n\t\tif (nativeFrame) {\n\t\t\tnativeFrame->checkedChanges(\n\t\t\t) | rpl::filter([](bool checked) {\n\t\t\t\treturn (checked != Core::App().settings().nativeWindowFrame());\n\t\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\t\tCore::App().settings().setNativeWindowFrame(checked);\n\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t}, nativeFrame->lifetime());\n\t\t}\n\t}\n\n\tbuilder.addSkip();\n}\n\n#if !defined Q_OS_WIN && !defined Q_OS_MAC\nvoid BuildWindowCloseBehaviorSection(SectionBuilder &builder) {\n\tusing Behavior = Core::Settings::CloseBehavior;\n\n\tconst auto settings = &Core::App().settings();\n\tauto shown = Platform::TrayIconSupported()\n\t\t? (Core::App().settings().workModeValue(\n\t\t\t) | rpl::map([](Core::Settings::WorkMode mode) {\n\t\t\t\treturn (mode == Core::Settings::WorkMode::WindowOnly);\n\t\t\t}) | rpl::distinct_until_changed() | rpl::type_erased)\n\t\t: rpl::producer<bool>(nullptr);\n\n\tbuilder.scope([&] {\n\t\tbuilder.addDivider();\n\t\tbuilder.addSkip();\n\t\tbuilder.addSubsectionTitle({\n\t\t\t.id = u\"advanced/window_close\"_q,\n\t\t\t.title = tr::lng_settings_window_close(),\n\t\t\t.keywords = { u\"close\"_q, u\"window\"_q, u\"background\"_q, u\"quit\"_q, u\"taskbar\"_q, u\"minimize\"_q },\n\t\t});\n\n\t\tbuilder.add([settings](const WidgetContext &ctx) {\n\t\t\tconst auto container = ctx.container.get();\n\t\t\tauto wrap = object_ptr<Ui::VerticalLayout>(container);\n\t\t\tconst auto inner = wrap.data();\n\n\t\t\tconst auto group = std::make_shared<Ui::RadioenumGroup<Behavior>>(\n\t\t\t\tsettings->closeBehavior());\n\t\t\tconst auto addRadio = [&](Behavior value, const QString &label) {\n\t\t\t\tinner->add(\n\t\t\t\t\tobject_ptr<Ui::Radioenum<Behavior>>(\n\t\t\t\t\t\tinner,\n\t\t\t\t\t\tgroup,\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t\tlabel,\n\t\t\t\t\t\tst::settingsSendType),\n\t\t\t\t\tst::settingsSendTypePadding);\n\t\t\t};\n\n\t\t\taddRadio(\n\t\t\t\tBehavior::RunInBackground,\n\t\t\t\ttr::lng_settings_run_in_background(tr::now));\n\t\t\taddRadio(\n\t\t\t\tBehavior::CloseToTaskbar,\n\t\t\t\ttr::lng_settings_close_to_taskbar(tr::now));\n\t\t\taddRadio(\n\t\t\t\tBehavior::Quit,\n\t\t\t\ttr::lng_settings_quit_on_close(tr::now));\n\n\t\t\tgroup->value() | rpl::filter([=](Behavior value) {\n\t\t\t\treturn (value != settings->closeBehavior());\n\t\t\t}) | rpl::on_next([=](Behavior value) {\n\t\t\t\tsettings->setCloseBehavior(value);\n\t\t\t\tLocal::writeSettings();\n\t\t\t}, inner->lifetime());\n\n\t\t\treturn SectionBuilder::WidgetToAdd{ .widget = std::move(wrap) };\n\t\t});\n\n\t\tbuilder.addSkip();\n\t}, std::move(shown));\n}\n#endif // !Q_OS_WIN && !Q_OS_MAC\n\nvoid BuildSystemIntegrationSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto settings = &Core::App().settings();\n\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"advanced/system_integration\"_q,\n\t\t.title = tr::lng_settings_system_integration(),\n\t\t.keywords = { u\"system\"_q, u\"tray\"_q, u\"startup\"_q, u\"autostart\"_q },\n\t});\n\n\tusing WorkMode = Core::Settings::WorkMode;\n\n\tif (Platform::TrayIconSupported()) {\n\t\tconst auto trayEnabled = [=] {\n\t\t\tconst auto workMode = settings->workMode();\n\t\t\treturn (workMode == WorkMode::TrayOnly)\n\t\t\t\t|| (workMode == WorkMode::WindowAndTray);\n\t\t};\n\t\tconst auto tray = builder.addCheckbox({\n\t\t\t.id = u\"advanced/tray\"_q,\n\t\t\t.title = tr::lng_settings_workmode_tray(),\n\t\t\t.checked = trayEnabled(),\n\t\t\t.keywords = { u\"tray\"_q, u\"icon\"_q, u\"system\"_q },\n\t\t});\n\n\t\tconst auto taskbarEnabled = [=] {\n\t\t\tconst auto workMode = settings->workMode();\n\t\t\treturn (workMode == WorkMode::WindowOnly)\n\t\t\t\t|| (workMode == WorkMode::WindowAndTray);\n\t\t};\n\t\tconst auto taskbar = Platform::SkipTaskbarSupported()\n\t\t\t? builder.addCheckbox({\n\t\t\t\t.id = u\"advanced/taskbar\"_q,\n\t\t\t\t.title = tr::lng_settings_workmode_window(),\n\t\t\t\t.checked = taskbarEnabled(),\n\t\t\t\t.keywords = { u\"taskbar\"_q, u\"window\"_q },\n\t\t\t})\n\t\t\t: nullptr;\n\n\t\tconst auto monochrome = Platform::HasMonochromeSetting()\n\t\t\t? builder.addCheckbox({\n\t\t\t\t.id = u\"advanced/monochrome_icon\"_q,\n\t\t\t\t.title = tr::lng_settings_monochrome_icon(),\n\t\t\t\t.checked = settings->trayIconMonochrome(),\n\t\t\t\t.keywords = { u\"monochrome\"_q, u\"icon\"_q, u\"tray\"_q },\n\t\t\t\t.shown = tray\n\t\t\t\t\t? tray->checkedValue()\n\t\t\t\t\t: rpl::single(trayEnabled()),\n\t\t\t})\n\t\t\t: nullptr;\n\n\t\tif (monochrome) {\n\t\t\tmonochrome->checkedChanges(\n\t\t\t) | rpl::filter([=](bool value) {\n\t\t\t\treturn (value != settings->trayIconMonochrome());\n\t\t\t}) | rpl::on_next([=](bool value) {\n\t\t\t\tsettings->setTrayIconMonochrome(value);\n\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t}, monochrome->lifetime());\n\t\t}\n\n\t\tconst auto updateWorkmode = [=] {\n\t\t\tconst auto newMode = (tray && tray->checked())\n\t\t\t\t? ((!taskbar || taskbar->checked())\n\t\t\t\t\t? WorkMode::WindowAndTray\n\t\t\t\t\t: WorkMode::TrayOnly)\n\t\t\t\t: WorkMode::WindowOnly;\n\t\t\tif ((newMode == WorkMode::WindowAndTray\n\t\t\t\t|| newMode == WorkMode::TrayOnly)\n\t\t\t\t&& settings->workMode() != newMode) {\n\t\t\t\tcSetSeenTrayTooltip(false);\n\t\t\t}\n\t\t\tsettings->setWorkMode(newMode);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t};\n\n\t\tif (tray) {\n\t\t\ttray->checkedChanges(\n\t\t\t) | rpl::filter([=](bool checked) {\n\t\t\t\treturn (checked != trayEnabled());\n\t\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\t\tif (!checked && taskbar && !taskbar->checked()) {\n\t\t\t\t\ttaskbar->setChecked(true);\n\t\t\t\t} else {\n\t\t\t\t\tupdateWorkmode();\n\t\t\t\t}\n\t\t\t}, tray->lifetime());\n\t\t}\n\n\t\tif (taskbar) {\n\t\t\ttaskbar->checkedChanges(\n\t\t\t) | rpl::filter([=](bool checked) {\n\t\t\t\treturn (checked != taskbarEnabled());\n\t\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\t\tif (!checked && tray && !tray->checked()) {\n\t\t\t\t\ttray->setChecked(true);\n\t\t\t\t} else {\n\t\t\t\t\tupdateWorkmode();\n\t\t\t\t}\n\t\t\t}, taskbar->lifetime());\n\t\t}\n\t}\n\n#ifdef Q_OS_MAC\n\tconst auto warnBeforeQuit = builder.addCheckbox({\n\t\t.id = u\"advanced/warn_before_quit\"_q,\n\t\t.title = tr::lng_settings_mac_warn_before_quit(\n\t\t\tlt_text,\n\t\t\trpl::single(Platform::ConfirmQuit::QuitKeysString())),\n\t\t.checked = settings->macWarnBeforeQuit(),\n\t\t.keywords = { u\"quit\"_q, u\"warn\"_q, u\"close\"_q },\n\t});\n\tif (warnBeforeQuit) {\n\t\twarnBeforeQuit->checkedChanges(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\treturn (checked != settings->macWarnBeforeQuit());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tsettings->setMacWarnBeforeQuit(checked);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}, warnBeforeQuit->lifetime());\n\t}\n\n\tconst auto systemReplace = builder.addCheckbox({\n\t\t.id = u\"advanced/system_text_replace\"_q,\n\t\t.title = tr::lng_settings_system_text_replace(),\n\t\t.checked = settings->systemTextReplace(),\n\t\t.keywords = { u\"text\"_q, u\"replace\"_q, u\"system\"_q },\n\t});\n\tif (systemReplace) {\n\t\tsystemReplace->checkedChanges(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\treturn (checked != settings->systemTextReplace());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tsettings->setSystemTextReplace(checked);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}, systemReplace->lifetime());\n\t}\n\n#ifndef OS_MAC_STORE\n\tconst auto roundIconEnabled = [=] {\n\t\tconst auto digest = base::Platform::CurrentCustomAppIconDigest();\n\t\treturn digest && (settings->macRoundIconDigest() == digest);\n\t};\n\tconst auto roundIcon = builder.addCheckbox({\n\t\t.id = u\"advanced/round_icon\"_q,\n\t\t.title = tr::lng_settings_mac_round_icon(),\n\t\t.checked = roundIconEnabled(),\n\t\t.keywords = { u\"icon\"_q, u\"round\"_q, u\"dock\"_q },\n\t});\n\tif (roundIcon) {\n\t\troundIcon->checkedChanges(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\treturn (checked != roundIconEnabled());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tconst auto digest = checked\n\t\t\t\t? base::Platform::SetCustomAppIcon(IconMacRound())\n\t\t\t\t: std::optional<uint64>();\n\t\t\tif (!checked) {\n\t\t\t\tbase::Platform::ClearCustomAppIcon();\n\t\t\t}\n\t\t\tWindow::OverrideApplicationIcon(checked ? IconMacRound() : QImage());\n\t\t\tCore::App().refreshApplicationIcon();\n\t\t\tsettings->setMacRoundIconDigest(digest);\n\t\t\tCore::App().saveSettings();\n\t\t}, roundIcon->lifetime());\n\t}\n#endif // OS_MAC_STORE\n#elif defined Q_OS_WIN // Q_OS_MAC\n\tusing Behavior = Core::Settings::CloseBehavior;\n\n\tconst auto container = builder.container();\n\tconst auto closeToTaskbarShown = container\n\t\t? container->lifetime().make_state<rpl::variable<bool>>(\n\t\t\t!Core::App().tray().has())\n\t\t: nullptr;\n\n\tif (closeToTaskbarShown) {\n\t\tsettings->workModeValue(\n\t\t) | rpl::on_next([=](WorkMode) {\n\t\t\t*closeToTaskbarShown = !Core::App().tray().has();\n\t\t}, container->lifetime());\n\t}\n\n\tconst auto closeToTaskbar = builder.addCheckbox({\n\t\t.id = u\"advanced/close_to_taskbar\"_q,\n\t\t.title = tr::lng_settings_close_to_taskbar(),\n\t\t.checked = settings->closeBehavior() == Behavior::CloseToTaskbar,\n\t\t.keywords = { u\"close\"_q, u\"taskbar\"_q, u\"minimize\"_q },\n\t\t.shown = closeToTaskbarShown\n\t\t\t? closeToTaskbarShown->value()\n\t\t\t: rpl::single(false),\n\t});\n\tif (closeToTaskbar) {\n\t\tcloseToTaskbar->checkedChanges(\n\t\t) | rpl::map([=](bool checked) {\n\t\t\treturn checked ? Behavior::CloseToTaskbar : Behavior::Quit;\n\t\t}) | rpl::filter([=](Behavior value) {\n\t\t\treturn (settings->closeBehavior() != value);\n\t\t}) | rpl::on_next([=](Behavior value) {\n\t\t\tsettings->setCloseBehavior(value);\n\t\t\tLocal::writeSettings();\n\t\t}, closeToTaskbar->lifetime());\n\t}\n#endif // Q_OS_MAC || Q_OS_WIN\n\n\tif (Platform::AutostartSupported()) {\n\t\tconst auto minimizedToggled = [=] {\n\t\t\treturn cStartMinimized()\n\t\t\t\t&& controller\n\t\t\t\t&& !controller->session().domain().local().hasLocalPasscode();\n\t\t};\n\n\t\tconst auto autostart = builder.addCheckbox({\n\t\t\t.id = u\"advanced/autostart\"_q,\n\t\t\t.title = tr::lng_settings_auto_start(),\n\t\t\t.checked = cAutoStart(),\n\t\t\t.keywords = { u\"autostart\"_q, u\"startup\"_q, u\"boot\"_q },\n\t\t});\n\n\t\tconst auto minimized = builder.addCheckbox({\n\t\t\t.id = u\"advanced/start_minimized\"_q,\n\t\t\t.title = tr::lng_settings_start_min(),\n\t\t\t.checked = minimizedToggled(),\n\t\t\t.keywords = { u\"minimized\"_q, u\"startup\"_q, u\"hidden\"_q },\n\t\t\t.shown = autostart\n\t\t\t\t? autostart->checkedValue()\n\t\t\t\t: rpl::single(cAutoStart()),\n\t\t});\n\n\t\tif (autostart) {\n\t\t\tautostart->checkedChanges(\n\t\t\t) | rpl::filter([](bool checked) {\n\t\t\t\treturn (checked != cAutoStart());\n\t\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\t\tconst auto weak = base::make_weak(controller);\n\t\t\t\tcSetAutoStart(checked);\n\t\t\t\tPlatform::AutostartToggle(checked, crl::guard(autostart, [=](\n\t\t\t\t\t\tbool enabled) {\n\t\t\t\t\tif (checked && !enabled && weak) {\n\t\t\t\t\t\tweak->window().showToast(\n\t\t\t\t\t\t\tLang::Hard::AutostartEnableError());\n\t\t\t\t\t}\n\t\t\t\t\tUi::PostponeCall(autostart, [=] {\n\t\t\t\t\t\tautostart->setChecked(enabled);\n\t\t\t\t\t});\n\t\t\t\t\tif (enabled || !minimized || !minimized->checked()) {\n\t\t\t\t\t\tLocal::writeSettings();\n\t\t\t\t\t} else if (minimized) {\n\t\t\t\t\t\tminimized->setChecked(false);\n\t\t\t\t\t}\n\t\t\t\t}));\n\t\t\t}, autostart->lifetime());\n\n\t\t\tif (controller) {\n\t\t\t\tPlatform::AutostartRequestStateFromSystem(crl::guard(\n\t\t\t\t\tcontroller,\n\t\t\t\t\t[=](bool enabled) { autostart->setChecked(enabled); }));\n\t\t\t}\n\t\t}\n\n\t\tif (minimized && controller) {\n\t\t\tminimized->checkedChanges(\n\t\t\t) | rpl::filter([=](bool checked) {\n\t\t\t\treturn (checked != minimizedToggled());\n\t\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\t\tif (controller->session().domain().local().hasLocalPasscode()) {\n\t\t\t\t\tminimized->setChecked(false);\n\t\t\t\t\tcontroller->show(Ui::MakeInformBox(\n\t\t\t\t\t\ttr::lng_error_start_minimized_passcoded()));\n\t\t\t\t} else {\n\t\t\t\t\tcSetStartMinimized(checked);\n\t\t\t\t\tLocal::writeSettings();\n\t\t\t\t}\n\t\t\t}, minimized->lifetime());\n\n\t\t\tcontroller->session().domain().local().localPasscodeChanged(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tminimized->setChecked(minimizedToggled());\n\t\t\t}, minimized->lifetime());\n\t\t}\n\t}\n\n\tif (Platform::IsWindows() && !Platform::IsWindowsStoreBuild()) {\n\t\tconst auto sendto = builder.addCheckbox({\n\t\t\t.id = u\"advanced/sendto\"_q,\n\t\t\t.title = tr::lng_settings_add_sendto(),\n\t\t\t.checked = cSendToMenu(),\n\t\t\t.keywords = { u\"sendto\"_q, u\"send\"_q, u\"menu\"_q, u\"context\"_q },\n\t\t});\n\t\tif (sendto) {\n\t\t\tsendto->checkedChanges(\n\t\t\t) | rpl::filter([](bool checked) {\n\t\t\t\treturn (checked != cSendToMenu());\n\t\t\t}) | rpl::on_next([](bool checked) {\n\t\t\t\tcSetSendToMenu(checked);\n\t\t\t\tpsSendToMenu(checked);\n\t\t\t\tLocal::writeSettings();\n\t\t\t}, sendto->lifetime());\n\t\t}\n\t}\n\n\tbuilder.addSkip();\n}\n\n#ifdef DESKTOP_APP_USE_ANGLE\nvoid BuildANGLEOption(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tusing ANGLE = Ui::GL::ANGLE;\n\n\tconst auto options = std::vector{\n\t\ttr::lng_settings_angle_backend_auto(tr::now),\n\t\ttr::lng_settings_angle_backend_d3d11(tr::now),\n\t\ttr::lng_settings_angle_backend_d3d9(tr::now),\n\t\ttr::lng_settings_angle_backend_d3d11on12(tr::now),\n\t\ttr::lng_settings_angle_backend_disabled(tr::now),\n\t};\n\tconst auto disabled = int(options.size()) - 1;\n\tconst auto backendIndex = [=] {\n\t\tif (Core::App().settings().disableOpenGL()) {\n\t\t\treturn disabled;\n\t\t} else switch (Ui::GL::CurrentANGLE()) {\n\t\tcase ANGLE::Auto: return 0;\n\t\tcase ANGLE::D3D11: return 1;\n\t\tcase ANGLE::D3D9: return 2;\n\t\tcase ANGLE::D3D11on12: return 3;\n\t\t}\n\t\tUnexpected(\"Ui::GL::CurrentANGLE value in BuildANGLEOption.\");\n\t}();\n\n\tbuilder.addButton({\n\t\t.id = u\"advanced/angle_backend\"_q,\n\t\t.title = tr::lng_settings_angle_backend(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.label = rpl::single(options[backendIndex]),\n\t\t.onClick = [=] {\n\t\t\tcontroller->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\tconst auto save = [=](int index) {\n\t\t\t\t\tif (index == backendIndex) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst auto confirmed = [=] {\n\t\t\t\t\t\tconst auto nowDisabled = (index == disabled);\n\t\t\t\t\t\tif (!nowDisabled) {\n\t\t\t\t\t\t\tUi::GL::ChangeANGLE([&] {\n\t\t\t\t\t\t\t\tswitch (index) {\n\t\t\t\t\t\t\t\tcase 0: return ANGLE::Auto;\n\t\t\t\t\t\t\t\tcase 1: return ANGLE::D3D11;\n\t\t\t\t\t\t\t\tcase 2: return ANGLE::D3D9;\n\t\t\t\t\t\t\t\tcase 3: return ANGLE::D3D11on12;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tUnexpected(\"Index in BuildANGLEOption.\");\n\t\t\t\t\t\t\t}());\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst auto wasDisabled = (backendIndex == disabled);\n\t\t\t\t\t\tif (nowDisabled != wasDisabled) {\n\t\t\t\t\t\t\tCore::App().settings().setDisableOpenGL(nowDisabled);\n\t\t\t\t\t\t\tLocal::writeSettings();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tCore::Restart();\n\t\t\t\t\t};\n\t\t\t\t\tcontroller->show(Ui::MakeConfirmBox({\n\t\t\t\t\t\t.text = tr::lng_settings_need_restart(),\n\t\t\t\t\t\t.confirmed = confirmed,\n\t\t\t\t\t\t.confirmText = tr::lng_settings_restart_now(),\n\t\t\t\t\t}));\n\t\t\t\t};\n\t\t\t\tSingleChoiceBox(box, {\n\t\t\t\t\t.title = tr::lng_settings_angle_backend(),\n\t\t\t\t\t.options = options,\n\t\t\t\t\t.initialSelection = backendIndex,\n\t\t\t\t\t.callback = save,\n\t\t\t\t});\n\t\t\t}));\n\t\t},\n\t\t.keywords = { u\"angle\"_q, u\"opengl\"_q, u\"d3d\"_q, u\"graphics\"_q },\n\t});\n}\n#else\nvoid BuildOpenGLOption(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto opengl = builder.addButton({\n\t\t.id = u\"advanced/opengl\"_q,\n\t\t.title = tr::lng_settings_enable_opengl(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.toggled = rpl::single(!Core::App().settings().disableOpenGL()),\n\t\t.keywords = { u\"opengl\"_q, u\"graphics\"_q, u\"gpu\"_q },\n\t});\n\n\tif (opengl) {\n\t\topengl->toggledValue(\n\t\t) | rpl::filter([](bool enabled) {\n\t\t\treturn (enabled == Core::App().settings().disableOpenGL());\n\t\t}) | rpl::on_next([=](bool enabled) {\n\t\t\tconst auto confirmed = crl::guard(opengl, [=] {\n\t\t\t\tCore::App().settings().setDisableOpenGL(!enabled);\n\t\t\t\tLocal::writeSettings();\n\t\t\t\tCore::Restart();\n\t\t\t});\n\t\t\tcontroller->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_settings_need_restart(),\n\t\t\t\t.confirmed = confirmed,\n\t\t\t\t.confirmText = tr::lng_settings_restart_now(),\n\t\t\t}));\n\t\t}, opengl->lifetime());\n\t}\n}\n#endif\n\nvoid BuildPerformanceSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"advanced/performance\"_q,\n\t\t.title = tr::lng_settings_performance(),\n\t\t.keywords = { u\"performance\"_q, u\"power\"_q, u\"graphics\"_q, u\"hardware\"_q },\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"advanced/power_saving\"_q,\n\t\t.title = tr::lng_settings_power_menu(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.onClick = [=] {\n\t\t\tcontroller->window().show(Box(PowerSavingBox, PowerSaving::Flags()));\n\t\t},\n\t\t.keywords = { u\"power\"_q, u\"saving\"_q, u\"battery\"_q, u\"animation\"_q },\n\t});\n\n\tconst auto hwAccel = builder.addButton({\n\t\t.id = u\"advanced/hw_accel\"_q,\n\t\t.title = tr::lng_settings_enable_hwaccel(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.toggled = rpl::single(\n\t\t\tCore::App().settings().hardwareAcceleratedVideo()),\n\t\t.keywords = { u\"hardware\"_q, u\"acceleration\"_q, u\"video\"_q },\n\t});\n\n\tif (hwAccel) {\n\t\thwAccel->toggledValue(\n\t\t) | rpl::filter([](bool enabled) {\n\t\t\treturn (enabled !=\n\t\t\t\tCore::App().settings().hardwareAcceleratedVideo());\n\t\t}) | rpl::on_next([=](bool enabled) {\n\t\t\tCore::App().settings().setHardwareAcceleratedVideo(enabled);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}, hwAccel->lifetime());\n\t}\n\n#ifdef DESKTOP_APP_USE_ANGLE\n\tBuildANGLEOption(builder);\n#else\n\tif constexpr (!Platform::IsMac()) {\n\t\tBuildOpenGLOption(builder);\n\t}\n#endif\n\n\tbuilder.addSkip();\n}\n\nvoid BuildSpellcheckerSection(SectionBuilder &builder) {\n#ifndef TDESKTOP_DISABLE_SPELLCHECK\n\tconst auto controller = builder.controller();\n\tconst auto session = builder.session();\n\tconst auto settings = &Core::App().settings();\n\tconst auto isSystem = Platform::Spellchecker::IsSystemSpellchecker();\n\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"advanced/spellchecker\"_q,\n\t\t.title = tr::lng_settings_spellchecker(),\n\t\t.keywords = { u\"spellcheck\"_q, u\"spelling\"_q, u\"dictionary\"_q },\n\t});\n\n\tconst auto spellchecker = builder.addButton({\n\t\t.id = u\"advanced/spellchecker_toggle\"_q,\n\t\t.title = isSystem\n\t\t\t? tr::lng_settings_system_spellchecker()\n\t\t\t: tr::lng_settings_custom_spellchecker(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.toggled = rpl::single(settings->spellcheckerEnabled()),\n\t\t.keywords = { u\"spellcheck\"_q, u\"spelling\"_q, u\"dictionary\"_q },\n\t});\n\n\tif (spellchecker) {\n\t\tspellchecker->toggledValue(\n\t\t) | rpl::filter([=](bool enabled) {\n\t\t\treturn (enabled != settings->spellcheckerEnabled());\n\t\t}) | rpl::on_next([=](bool enabled) {\n\t\t\tsettings->setSpellcheckerEnabled(enabled);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}, spellchecker->lifetime());\n\t}\n\n\tif (!isSystem) {\n\t\tbuilder.scope([&] {\n\t\t\tconst auto autoDownload = builder.addButton({\n\t\t\t\t.id = u\"advanced/auto_download_dictionaries\"_q,\n\t\t\t\t.title = tr::lng_settings_auto_download_dictionaries(),\n\t\t\t\t.st = &st::settingsButtonNoIcon,\n\t\t\t\t.toggled = rpl::single(settings->autoDownloadDictionaries()),\n\t\t\t\t.keywords = { u\"dictionary\"_q, u\"download\"_q, u\"spellcheck\"_q },\n\t\t\t});\n\n\t\t\tif (autoDownload) {\n\t\t\t\tautoDownload->toggledValue(\n\t\t\t\t) | rpl::filter([=](bool enabled) {\n\t\t\t\t\treturn (enabled != settings->autoDownloadDictionaries());\n\t\t\t\t}) | rpl::on_next([=](bool enabled) {\n\t\t\t\t\tsettings->setAutoDownloadDictionaries(enabled);\n\t\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t\t}, autoDownload->lifetime());\n\t\t\t}\n\n\t\t\tbuilder.addButton({\n\t\t\t\t.id = u\"advanced/manage_dictionaries\"_q,\n\t\t\t\t.title = tr::lng_settings_manage_dictionaries(),\n\t\t\t\t.st = &st::settingsButtonNoIcon,\n\t\t\t\t.label = Spellchecker::ButtonManageDictsState(session),\n\t\t\t\t.onClick = [=] {\n\t\t\t\t\tcontroller->show(Box<Ui::ManageDictionariesBox>(session));\n\t\t\t\t},\n\t\t\t\t.keywords = { u\"dictionary\"_q, u\"manage\"_q, u\"spellcheck\"_q },\n\t\t\t});\n\t\t}, spellchecker ? spellchecker->toggledValue() : nullptr);\n\t}\n\n\tbuilder.addSkip();\n#endif // !TDESKTOP_DISABLE_SPELLCHECK\n}\n\nvoid BuildUpdateSection(SectionBuilder &builder, bool atTop) {\n\tif (!HasUpdate()) {\n\t\treturn;\n\t}\n\tconst auto container = builder.container();\n\n\tif (!atTop) {\n\t\tbuilder.addDivider();\n\t}\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"advanced/version\"_q,\n\t\t.title = tr::lng_settings_version_info(),\n\t\t.keywords = { u\"version\"_q, u\"update\"_q, u\"check\"_q },\n\t});\n\n\tconst auto version = tr::lng_settings_current_version(\n\t\ttr::now,\n\t\tlt_version,\n\t\tcurrentVersionText());\n\n\tconst auto texts = container\n\t\t? Ui::CreateChild<rpl::event_stream<QString>>(container)\n\t\t: nullptr;\n\tconst auto downloading = container\n\t\t? Ui::CreateChild<rpl::event_stream<bool>>(container)\n\t\t: nullptr;\n\n\tconst auto toggle = builder.addButton({\n\t\t.id = u\"advanced/auto_update\"_q,\n\t\t.title = tr::lng_settings_update_automatically(),\n\t\t.st = &st::settingsUpdateToggle,\n\t\t.toggled = rpl::single(cAutoUpdate()),\n\t\t.keywords = { u\"update\"_q, u\"automatic\"_q, u\"version\"_q },\n\t});\n\n\tif (toggle) {\n\t\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\ttoggle,\n\t\t\ttexts->events(),\n\t\t\tst::settingsUpdateState);\n\n\t\trpl::combine(\n\t\t\ttoggle->widthValue(),\n\t\t\tlabel->widthValue()\n\t\t) | rpl::on_next([=] {\n\t\t\tlabel->moveToLeft(\n\t\t\t\tst::settingsUpdateStatePosition.x(),\n\t\t\t\tst::settingsUpdateStatePosition.y());\n\t\t}, label->lifetime());\n\t\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t}\n\n\tauto optionsShown = rpl::producer<bool>(nullptr);\n\tif (toggle) {\n\t\tCore::UpdateChecker checker;\n\t\toptionsShown = rpl::combine(\n\t\t\ttoggle->toggledValue(),\n\t\t\tdownloading->events_starting_with(\n\t\t\t\tchecker.state() == Core::UpdateChecker::State::Download)\n\t\t) | rpl::map([](bool check, bool downloading) {\n\t\t\treturn check && !downloading;\n\t\t});\n\t}\n\tauto options = (Ui::SlideWrap<Ui::VerticalLayout>*)nullptr;\n\tauto install = (Ui::SettingsButton*)nullptr;\n\tauto check = (Ui::SettingsButton*)nullptr;\n\tbuilder.scope([&] {\n\t\tinstall = cAlphaVersion()\n\t\t\t? nullptr\n\t\t\t: builder.addButton({\n\t\t\t\t.id = u\"advanced/install_beta\"_q,\n\t\t\t\t.title = tr::lng_settings_install_beta(),\n\t\t\t\t.st = &st::settingsButtonNoIcon,\n\t\t\t\t.toggled = rpl::single(cInstallBetaVersion()),\n\t\t\t\t.keywords = { u\"beta\"_q, u\"update\"_q, u\"version\"_q },\n\t\t\t});\n\n\t\tcheck = builder.addButton({\n\t\t\t.id = u\"advanced/check_update\"_q,\n\t\t\t.title = tr::lng_settings_check_now(),\n\t\t\t.st = &st::settingsButtonNoIcon,\n\t\t\t.onClick = [] {\n\t\t\t\tCore::UpdateChecker checker;\n\t\t\t\tcSetLastUpdateCheck(0);\n\t\t\t\tchecker.start();\n\t\t\t},\n\t\t\t.keywords = { u\"check\"_q, u\"update\"_q, u\"version\"_q },\n\t\t});\n\t}, std::move(optionsShown), [&](auto wrap) {\n\t\toptions = wrap;\n\t});\n\n\tif (check && container) {\n\t\tconst auto update = Ui::CreateChild<Ui::SettingsButton>(\n\t\t\tcheck,\n\t\t\ttr::lng_update_telegram(),\n\t\t\tst::settingsUpdate);\n\t\tupdate->hide();\n\t\tcheck->widthValue() | rpl::on_next([=](int width) {\n\t\t\tupdate->resizeToWidth(width);\n\t\t\tupdate->moveToLeft(0, 0);\n\t\t}, update->lifetime());\n\n\t\tconst auto showDownloadProgress = [=](int64 ready, int64 total) {\n\t\t\ttexts->fire(tr::lng_settings_downloading_update(\n\t\t\t\ttr::now,\n\t\t\t\tlt_progress,\n\t\t\t\tUi::FormatDownloadText(ready, total)));\n\t\t\tdownloading->fire(true);\n\t\t};\n\t\tconst auto setDefaultStatus = [=](\n\t\t\t\tconst Core::UpdateChecker &checker) {\n\t\t\tusing State = Core::UpdateChecker::State;\n\t\t\tconst auto state = checker.state();\n\t\t\tswitch (state) {\n\t\t\tcase State::Download:\n\t\t\t\tshowDownloadProgress(checker.already(), checker.size());\n\t\t\t\tbreak;\n\t\t\tcase State::Ready:\n\t\t\t\ttexts->fire(tr::lng_settings_update_ready(tr::now));\n\t\t\t\tupdate->show();\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\ttexts->fire_copy(version);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t};\n\n\t\ttoggle->toggledValue(\n\t\t) | rpl::filter([](bool toggled) {\n\t\t\treturn (toggled != cAutoUpdate());\n\t\t}) | rpl::on_next([=](bool toggled) {\n\t\t\tcSetAutoUpdate(toggled);\n\t\t\tLocal::writeSettings();\n\t\t\tCore::UpdateChecker checker;\n\t\t\tif (cAutoUpdate()) {\n\t\t\t\tchecker.start();\n\t\t\t} else {\n\t\t\t\tchecker.stop();\n\t\t\t\tsetDefaultStatus(checker);\n\t\t\t}\n\t\t}, toggle->lifetime());\n\n\t\tif (install) {\n\t\t\tinstall->toggledValue(\n\t\t\t) | rpl::filter([](bool toggled) {\n\t\t\t\treturn (toggled != cInstallBetaVersion());\n\t\t\t}) | rpl::on_next([=](bool toggled) {\n\t\t\t\tcSetInstallBetaVersion(toggled);\n\t\t\t\tCore::Launcher::Instance().writeInstallBetaVersionsSetting();\n\t\t\t\tCore::UpdateChecker checker;\n\t\t\t\tchecker.stop();\n\t\t\t\tif (toggled) {\n\t\t\t\t\tcSetLastUpdateCheck(0);\n\t\t\t\t}\n\t\t\t\tchecker.start();\n\t\t\t}, toggle->lifetime());\n\t\t}\n\n\t\tCore::UpdateChecker checker;\n\n\t\tchecker.checking() | rpl::on_next([=] {\n\t\t\toptions->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\ttexts->fire(tr::lng_settings_update_checking(tr::now));\n\t\t\tdownloading->fire(false);\n\t\t}, options->lifetime());\n\t\tchecker.isLatest() | rpl::on_next([=] {\n\t\t\toptions->setAttribute(Qt::WA_TransparentForMouseEvents, false);\n\t\t\ttexts->fire(tr::lng_settings_latest_installed(tr::now));\n\t\t\tdownloading->fire(false);\n\t\t}, options->lifetime());\n\t\tchecker.progress(\n\t\t) | rpl::on_next([=](Core::UpdateChecker::Progress progress) {\n\t\t\tshowDownloadProgress(progress.already, progress.size);\n\t\t}, options->lifetime());\n\t\tchecker.failed() | rpl::on_next([=] {\n\t\t\toptions->setAttribute(Qt::WA_TransparentForMouseEvents, false);\n\t\t\ttexts->fire(tr::lng_settings_update_fail(tr::now));\n\t\t\tdownloading->fire(false);\n\t\t}, options->lifetime());\n\t\tchecker.ready() | rpl::on_next([=] {\n\t\t\toptions->setAttribute(Qt::WA_TransparentForMouseEvents, false);\n\t\t\ttexts->fire(tr::lng_settings_update_ready(tr::now));\n\t\t\tupdate->show();\n\t\t\tdownloading->fire(false);\n\t\t}, options->lifetime());\n\n\t\tsetDefaultStatus(checker);\n\n\t\tupdate->setClickedCallback([] {\n\t\t\tif (!Core::UpdaterDisabled()) {\n\t\t\t\tCore::checkReadyUpdate();\n\t\t\t}\n\t\t\tCore::Restart();\n\t\t});\n\t}\n\n\tbuilder.addSkip();\n\tif (atTop) {\n\t\tbuilder.addDivider();\n\t}\n}\n\nvoid BuildExportSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto session = builder.session();\n\tconst auto showOther = builder.showOther();\n\tbuilder.addSkip();\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\n\tbuilder.addButton({\n\t\t.id = u\"advanced/export\"_q,\n\t\t.title = tr::lng_settings_export_data(),\n\t\t.icon = { &st::menuIconExport },\n\t\t.onClick = [=] {\n\t\t\tcontroller->window().hideSettingsAndLayer();\n\t\t\tbase::call_delayed(\n\t\t\t\tst::boxDuration,\n\t\t\t\tsession,\n\t\t\t\t[=] { Core::App().exportManager().start(session); });\n\t\t},\n\t\t.keywords = { u\"export\"_q, u\"data\"_q, u\"backup\"_q },\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"advanced/experimental\"_q,\n\t\t.title = tr::lng_settings_experimental(),\n\t\t.icon = { &st::menuIconExperimental },\n\t\t.onClick = [showOther] { showOther(Experimental::Id()); },\n\t\t.keywords = { u\"experimental\"_q, u\"beta\"_q, u\"features\"_q },\n\t});\n}\n\nvoid BuildScreenReaderSection(SectionBuilder &builder) {\n\tconst auto detected = base::ScreenReaderState::Instance()->active();\n\tconst auto disabled = Ui::ScreenReaderModeDisabled();\n\tif (!detected || !disabled) {\n\t\treturn;\n\t}\n\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"advanced/screen_reader\"_q,\n\t\t.title = tr::lng_screen_reader_settings_title(),\n\t\t.keywords = { u\"screen reader\"_q, u\"accessibility\"_q, u\"voiceover\"_q },\n\t});\n\n\tconst auto toggle = builder.addButton({\n\t\t.id = u\"advanced/screen_reader_disable\"_q,\n\t\t.title = tr::lng_screen_reader_settings_disable(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.toggled = rpl::single(disabled),\n\t\t.keywords = { u\"screen reader\"_q, u\"accessibility\"_q },\n\t});\n\n\tif (toggle) {\n\t\ttoggle->toggledValue(\n\t\t) | rpl::filter([=](bool value) {\n\t\t\treturn (value != Ui::ScreenReaderModeDisabled());\n\t\t}) | rpl::on_next([=](bool value) {\n\t\t\tCore::App().settings().writePref<bool>(\n\t\t\t\tCore::kScreenReaderModeDisabledKey,\n\t\t\t\tvalue);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\tUi::SetScreenReaderModeDisabled(value);\n\t\t}, toggle->lifetime());\n\t}\n\n\tbuilder.addSkip();\n}\n\nclass Advanced : public Section<Advanced> {\npublic:\n\tAdvanced(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\nprivate:\n\tvoid setupContent();\n\n};\n\nconst auto kMeta = BuildHelper({\n\t.id = Advanced::Id(),\n\t.parentId = MainId(),\n\t.title = &tr::lng_settings_advanced,\n\t.icon = &st::menuIconManage,\n}, [](SectionBuilder &builder) {\n\tconst auto autoUpdate = cAutoUpdate();\n\n\tif (!autoUpdate) {\n\t\tBuildUpdateSection(builder, true);\n\t}\n\tBuildDataStorageSection(builder);\n\tBuildAutoDownloadSection(builder);\n\tBuildWindowTitleSection(builder);\n#if !defined Q_OS_WIN && !defined Q_OS_MAC\n\tBuildWindowCloseBehaviorSection(builder);\n#endif\n\tBuildSystemIntegrationSection(builder);\n\tBuildPerformanceSection(builder);\n\tBuildSpellcheckerSection(builder);\n\tBuildScreenReaderSection(builder);\n\tif (autoUpdate) {\n\t\tBuildUpdateSection(builder, false);\n\t}\n\tBuildExportSection(builder);\n});\n\nconst SectionBuildMethod kAdvancedSection = kMeta.build;\n\nAdvanced::Advanced(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent();\n}\n\nrpl::producer<QString> Advanced::title() {\n\treturn tr::lng_settings_advanced();\n}\n\nvoid Advanced::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tbuild(content, kAdvancedSection);\n\n\tUi::ResizeFitChild(this, content);\n}\n\n} // namespace\n\nvoid SetupConnectionType(\n\t\tnot_null<Window::Controller*> controller,\n\t\tnot_null<::Main::Account*> account,\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tconst auto connectionType = [=] {\n\t\tconst auto transport = account->mtp().dctransport();\n\t\tif (!Core::App().settings().proxy().isEnabled()) {\n\t\t\treturn transport.isEmpty()\n\t\t\t\t? tr::lng_connection_auto_connecting(tr::now)\n\t\t\t\t: tr::lng_connection_auto(tr::now, lt_transport, transport);\n\t\t} else {\n\t\t\treturn transport.isEmpty()\n\t\t\t\t? tr::lng_connection_proxy_connecting(tr::now)\n\t\t\t\t: tr::lng_connection_proxy(tr::now, lt_transport, transport);\n\t\t}\n\t};\n\tconst auto button = AddButtonWithLabel(\n\t\tcontainer,\n\t\ttr::lng_settings_connection_type(),\n\t\trpl::merge(\n\t\t\tCore::App().settings().proxy().connectionTypeChanges(),\n\t\t\ttr::lng_connection_auto_connecting() | rpl::to_empty\n\t\t) | rpl::map([=] { return connectionType(); }),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconNetwork });\n\tbutton->addClickHandler([=] {\n\t\tcontroller->show(ProxiesBoxController::CreateOwningBox(account));\n\t});\n}\n\nbool HasUpdate() {\n\treturn !Core::UpdaterDisabled();\n}\n\nvoid SetupUpdate(not_null<Ui::VerticalLayout*> container) {\n\tif (!HasUpdate()) {\n\t\treturn;\n\t}\n\n\tconst auto texts = Ui::CreateChild<rpl::event_stream<QString>>(\n\t\tcontainer.get());\n\tconst auto downloading = Ui::CreateChild<rpl::event_stream<bool>>(\n\t\tcontainer.get());\n\tconst auto version = tr::lng_settings_current_version(\n\t\ttr::now,\n\t\tlt_version,\n\t\tcurrentVersionText());\n\tconst auto toggle = container->add(object_ptr<Button>(\n\t\tcontainer,\n\t\ttr::lng_settings_update_automatically(),\n\t\tst::settingsUpdateToggle));\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\ttoggle,\n\t\ttexts->events(),\n\t\tst::settingsUpdateState);\n\n\tconst auto options = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto inner = options->entity();\n\tconst auto install = cAlphaVersion()\n\t\t? nullptr\n\t\t: inner->add(object_ptr<Button>(\n\t\t\tinner,\n\t\t\ttr::lng_settings_install_beta(),\n\t\t\tst::settingsButtonNoIcon));\n\n\tconst auto check = inner->add(object_ptr<Button>(\n\t\tinner,\n\t\ttr::lng_settings_check_now(),\n\t\tst::settingsButtonNoIcon));\n\tconst auto update = Ui::CreateChild<Button>(\n\t\tcheck,\n\t\ttr::lng_update_telegram(),\n\t\tst::settingsUpdate);\n\tupdate->hide();\n\tcheck->widthValue() | rpl::on_next([=](int width) {\n\t\tupdate->resizeToWidth(width);\n\t\tupdate->moveToLeft(0, 0);\n\t}, update->lifetime());\n\n\trpl::combine(\n\t\ttoggle->widthValue(),\n\t\tlabel->widthValue()\n\t) | rpl::on_next([=] {\n\t\tlabel->moveToLeft(\n\t\t\tst::settingsUpdateStatePosition.x(),\n\t\t\tst::settingsUpdateStatePosition.y());\n\t}, label->lifetime());\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto showDownloadProgress = [=](int64 ready, int64 total) {\n\t\ttexts->fire(tr::lng_settings_downloading_update(\n\t\t\ttr::now,\n\t\t\tlt_progress,\n\t\t\tUi::FormatDownloadText(ready, total)));\n\t\tdownloading->fire(true);\n\t};\n\tconst auto setDefaultStatus = [=](const Core::UpdateChecker &checker) {\n\t\tusing State = Core::UpdateChecker::State;\n\t\tconst auto state = checker.state();\n\t\tswitch (state) {\n\t\tcase State::Download:\n\t\t\tshowDownloadProgress(checker.already(), checker.size());\n\t\t\tbreak;\n\t\tcase State::Ready:\n\t\t\ttexts->fire(tr::lng_settings_update_ready(tr::now));\n\t\t\tupdate->show();\n\t\t\tbreak;\n\t\tdefault:\n\t\t\ttexts->fire_copy(version);\n\t\t\tbreak;\n\t\t}\n\t};\n\n\ttoggle->toggleOn(rpl::single(cAutoUpdate()));\n\ttoggle->toggledValue(\n\t) | rpl::filter([](bool toggled) {\n\t\treturn (toggled != cAutoUpdate());\n\t}) | rpl::on_next([=](bool toggled) {\n\t\tcSetAutoUpdate(toggled);\n\n\t\tLocal::writeSettings();\n\t\tCore::UpdateChecker checker;\n\t\tif (cAutoUpdate()) {\n\t\t\tchecker.start();\n\t\t} else {\n\t\t\tchecker.stop();\n\t\t\tsetDefaultStatus(checker);\n\t\t}\n\t}, toggle->lifetime());\n\n\tif (install) {\n\t\tinstall->toggleOn(rpl::single(cInstallBetaVersion()));\n\t\tinstall->toggledValue(\n\t\t) | rpl::filter([](bool toggled) {\n\t\t\treturn (toggled != cInstallBetaVersion());\n\t\t}) | rpl::on_next([=](bool toggled) {\n\t\t\tcSetInstallBetaVersion(toggled);\n\t\t\tCore::Launcher::Instance().writeInstallBetaVersionsSetting();\n\n\t\t\tCore::UpdateChecker checker;\n\t\t\tchecker.stop();\n\t\t\tif (toggled) {\n\t\t\t\tcSetLastUpdateCheck(0);\n\t\t\t}\n\t\t\tchecker.start();\n\t\t}, toggle->lifetime());\n\t}\n\n\tCore::UpdateChecker checker;\n\toptions->toggleOn(rpl::combine(\n\t\ttoggle->toggledValue(),\n\t\tdownloading->events_starting_with(\n\t\t\tchecker.state() == Core::UpdateChecker::State::Download)\n\t) | rpl::map([](bool check, bool downloading) {\n\t\treturn check && !downloading;\n\t}));\n\n\tchecker.checking() | rpl::on_next([=] {\n\t\toptions->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\ttexts->fire(tr::lng_settings_update_checking(tr::now));\n\t\tdownloading->fire(false);\n\t}, options->lifetime());\n\tchecker.isLatest() | rpl::on_next([=] {\n\t\toptions->setAttribute(Qt::WA_TransparentForMouseEvents, false);\n\t\ttexts->fire(tr::lng_settings_latest_installed(tr::now));\n\t\tdownloading->fire(false);\n\t}, options->lifetime());\n\tchecker.progress(\n\t) | rpl::on_next([=](Core::UpdateChecker::Progress progress) {\n\t\tshowDownloadProgress(progress.already, progress.size);\n\t}, options->lifetime());\n\tchecker.failed() | rpl::on_next([=] {\n\t\toptions->setAttribute(Qt::WA_TransparentForMouseEvents, false);\n\t\ttexts->fire(tr::lng_settings_update_fail(tr::now));\n\t\tdownloading->fire(false);\n\t}, options->lifetime());\n\tchecker.ready() | rpl::on_next([=] {\n\t\toptions->setAttribute(Qt::WA_TransparentForMouseEvents, false);\n\t\ttexts->fire(tr::lng_settings_update_ready(tr::now));\n\t\tupdate->show();\n\t\tdownloading->fire(false);\n\t}, options->lifetime());\n\n\tsetDefaultStatus(checker);\n\n\tcheck->addClickHandler([] {\n\t\tCore::UpdateChecker checker;\n\n\t\tcSetLastUpdateCheck(0);\n\t\tchecker.start();\n\t});\n\tupdate->addClickHandler([] {\n\t\tif (!Core::UpdaterDisabled()) {\n\t\t\tCore::checkReadyUpdate();\n\t\t}\n\t\tCore::Restart();\n\t});\n}\n\nvoid SetupWindowTitleContent(\n\t\tWindow::SessionController *controller,\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tconst auto checkbox = [&](rpl::producer<QString> &&label, bool checked) {\n\t\treturn object_ptr<Ui::Checkbox>(\n\t\t\tcontainer,\n\t\t\tstd::move(label),\n\t\t\tchecked,\n\t\t\tst::settingsCheckbox);\n\t};\n\tconst auto addCheckbox = [&](\n\t\t\trpl::producer<QString> &&label,\n\t\t\tbool checked) {\n\t\treturn container->add(\n\t\t\tcheckbox(std::move(label), checked),\n\t\t\tst::settingsCheckboxPadding);\n\t};\n\tconst auto settings = &Core::App().settings();\n\tif (controller) {\n\t\tconst auto content = [=] {\n\t\t\treturn settings->windowTitleContent();\n\t\t};\n\t\tconst auto showChatName = addCheckbox(\n\t\t\ttr::lng_settings_title_chat_name(),\n\t\t\t!content().hideChatName);\n\t\tshowChatName->checkedChanges(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\treturn (checked == content().hideChatName);\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tauto updated = content();\n\t\t\tupdated.hideChatName = !checked;\n\t\t\tsettings->setWindowTitleContent(updated);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}, showChatName->lifetime());\n\n\t\tif (Core::App().domain().accountsAuthedCount() > 1) {\n\t\t\tconst auto showAccountName = addCheckbox(\n\t\t\t\ttr::lng_settings_title_account_name(),\n\t\t\t\t!content().hideAccountName);\n\t\t\tshowAccountName->checkedChanges(\n\t\t\t) | rpl::filter([=](bool checked) {\n\t\t\t\treturn (checked == content().hideAccountName);\n\t\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\t\tauto updated = content();\n\t\t\t\tupdated.hideAccountName = !checked;\n\t\t\t\tsettings->setWindowTitleContent(updated);\n\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t}, showAccountName->lifetime());\n\t\t}\n\n\t\tconst auto showTotalUnread = addCheckbox(\n\t\t\ttr::lng_settings_title_total_count(),\n\t\t\t!content().hideTotalUnread);\n\t\tshowTotalUnread->checkedChanges(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\treturn (checked == content().hideTotalUnread);\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tauto updated = content();\n\t\t\tupdated.hideTotalUnread = !checked;\n\t\t\tsettings->setWindowTitleContent(updated);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}, showTotalUnread->lifetime());\n\t}\n\n\tif (Ui::Platform::NativeWindowFrameSupported()) {\n\t\tconst auto nativeFrame = addCheckbox(\n\t\t\tPlatform::IsWayland()\n\t\t\t\t? tr::lng_settings_qt_frame()\n\t\t\t\t: tr::lng_settings_native_frame(),\n\t\t\tCore::App().settings().nativeWindowFrame());\n\n\t\tnativeFrame->checkedChanges(\n\t\t) | rpl::filter([](bool checked) {\n\t\t\treturn (checked != Core::App().settings().nativeWindowFrame());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tCore::App().settings().setNativeWindowFrame(checked);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}, nativeFrame->lifetime());\n\t}\n}\n\nvoid SetupSystemIntegrationContent(\n\t\tWindow::SessionController *controller,\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tusing WorkMode = Core::Settings::WorkMode;\n\n\tconst auto checkbox = [&](rpl::producer<QString> &&label, bool checked) {\n\t\treturn object_ptr<Ui::Checkbox>(\n\t\t\tcontainer,\n\t\t\tstd::move(label),\n\t\t\tchecked,\n\t\t\tst::settingsCheckbox);\n\t};\n\tconst auto addCheckbox = [&](\n\t\t\trpl::producer<QString> &&label,\n\t\t\tbool checked) {\n\t\treturn container->add(\n\t\t\tcheckbox(std::move(label), checked),\n\t\t\tst::settingsCheckboxPadding);\n\t};\n\tconst auto addSlidingCheckbox = [&](\n\t\t\trpl::producer<QString> &&label,\n\t\t\tbool checked) {\n\t\treturn container->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::Checkbox>>(\n\t\t\t\tcontainer,\n\t\t\t\tcheckbox(std::move(label), checked),\n\t\t\t\tst::settingsCheckboxPadding));\n\t};\n\n\tconst auto settings = &Core::App().settings();\n\tif (Platform::TrayIconSupported()) {\n\t\tconst auto trayEnabled = [=] {\n\t\t\tconst auto workMode = settings->workMode();\n\t\t\treturn (workMode == WorkMode::TrayOnly)\n\t\t\t\t|| (workMode == WorkMode::WindowAndTray);\n\t\t};\n\t\tconst auto tray = addCheckbox(\n\t\t\ttr::lng_settings_workmode_tray(),\n\t\t\ttrayEnabled());\n\t\tconst auto monochrome = Platform::HasMonochromeSetting()\n\t\t\t? addSlidingCheckbox(\n\t\t\t\ttr::lng_settings_monochrome_icon(),\n\t\t\t\tsettings->trayIconMonochrome())\n\t\t\t: nullptr;\n\t\tif (monochrome) {\n\t\t\tmonochrome->toggle(tray->checked(), anim::type::instant);\n\n\t\t\tmonochrome->entity()->checkedChanges(\n\t\t\t) | rpl::filter([=](bool value) {\n\t\t\t\treturn (value != settings->trayIconMonochrome());\n\t\t\t}) | rpl::on_next([=](bool value) {\n\t\t\t\tsettings->setTrayIconMonochrome(value);\n\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t}, monochrome->lifetime());\n\t\t}\n\n\t\tconst auto taskbarEnabled = [=] {\n\t\t\tconst auto workMode = settings->workMode();\n\t\t\treturn (workMode == WorkMode::WindowOnly)\n\t\t\t\t|| (workMode == WorkMode::WindowAndTray);\n\t\t};\n\t\tconst auto taskbar = Platform::SkipTaskbarSupported()\n\t\t\t? addCheckbox(\n\t\t\t\ttr::lng_settings_workmode_window(),\n\t\t\t\ttaskbarEnabled())\n\t\t\t: nullptr;\n\n\t\tconst auto updateWorkmode = [=] {\n\t\t\tconst auto newMode = tray->checked()\n\t\t\t\t? ((!taskbar || taskbar->checked())\n\t\t\t\t\t? WorkMode::WindowAndTray\n\t\t\t\t\t: WorkMode::TrayOnly)\n\t\t\t\t: WorkMode::WindowOnly;\n\t\t\tif ((newMode == WorkMode::WindowAndTray\n\t\t\t\t|| newMode == WorkMode::TrayOnly)\n\t\t\t\t&& settings->workMode() != newMode) {\n\t\t\t\tcSetSeenTrayTooltip(false);\n\t\t\t}\n\t\t\tsettings->setWorkMode(newMode);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t};\n\n\t\ttray->checkedChanges(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\treturn (checked != trayEnabled());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tif (!checked && taskbar && !taskbar->checked()) {\n\t\t\t\ttaskbar->setChecked(true);\n\t\t\t} else {\n\t\t\t\tupdateWorkmode();\n\t\t\t}\n\t\t\tif (monochrome) {\n\t\t\t\tmonochrome->toggle(checked, anim::type::normal);\n\t\t\t}\n\t\t}, tray->lifetime());\n\n\t\tif (taskbar) {\n\t\t\ttaskbar->checkedChanges(\n\t\t\t) | rpl::filter([=](bool checked) {\n\t\t\t\treturn (checked != taskbarEnabled());\n\t\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\t\tif (!checked && !tray->checked()) {\n\t\t\t\t\ttray->setChecked(true);\n\t\t\t\t} else {\n\t\t\t\t\tupdateWorkmode();\n\t\t\t\t}\n\t\t\t}, taskbar->lifetime());\n\t\t}\n\t}\n\n#ifdef Q_OS_MAC\n\tconst auto warnBeforeQuit = addCheckbox(\n\t\ttr::lng_settings_mac_warn_before_quit(\n\t\t\tlt_text,\n\t\t\trpl::single(Platform::ConfirmQuit::QuitKeysString())),\n\t\tsettings->macWarnBeforeQuit());\n\twarnBeforeQuit->checkedChanges(\n\t) | rpl::filter([=](bool checked) {\n\t\treturn (checked != settings->macWarnBeforeQuit());\n\t}) | rpl::on_next([=](bool checked) {\n\t\tsettings->setMacWarnBeforeQuit(checked);\n\t\tCore::App().saveSettingsDelayed();\n\t}, warnBeforeQuit->lifetime());\n\n#ifndef OS_MAC_STORE\n\tconst auto enabled = [=] {\n\t\tconst auto digest = base::Platform::CurrentCustomAppIconDigest();\n\t\treturn digest && (settings->macRoundIconDigest() == digest);\n\t};\n\tconst auto roundIcon = addCheckbox(\n\t\ttr::lng_settings_mac_round_icon(),\n\t\tenabled());\n\troundIcon->checkedChanges(\n\t) | rpl::filter([=](bool checked) {\n\t\treturn (checked != enabled());\n\t}) | rpl::on_next([=](bool checked) {\n\t\tconst auto digest = checked\n\t\t\t? base::Platform::SetCustomAppIcon(IconMacRound())\n\t\t\t: std::optional<uint64>();\n\t\tif (!checked) {\n\t\t\tbase::Platform::ClearCustomAppIcon();\n\t\t}\n\t\tWindow::OverrideApplicationIcon(checked ? IconMacRound() : QImage());\n\t\tCore::App().refreshApplicationIcon();\n\t\tsettings->setMacRoundIconDigest(digest);\n\t\tCore::App().saveSettings();\n\t}, roundIcon->lifetime());\n#endif // OS_MAC_STORE\n#elif defined Q_OS_WIN // Q_OS_MAC\n\tusing Behavior = Core::Settings::CloseBehavior;\n\tconst auto closeToTaskbar = addSlidingCheckbox(\n\t\ttr::lng_settings_close_to_taskbar(),\n\t\tsettings->closeBehavior() == Behavior::CloseToTaskbar);\n\n\tconst auto closeToTaskbarShown = std::make_shared<\n\t\trpl::variable<bool>\n\t>(false);\n\tsettings->workModeValue(\n\t) | rpl::on_next([=](WorkMode workMode) {\n\t\t*closeToTaskbarShown = !Core::App().tray().has();\n\t}, closeToTaskbar->lifetime());\n\n\tcloseToTaskbar->toggleOn(closeToTaskbarShown->value());\n\tcloseToTaskbar->entity()->checkedChanges(\n\t) | rpl::map([=](bool checked) {\n\t\treturn checked ? Behavior::CloseToTaskbar : Behavior::Quit;\n\t}) | rpl::filter([=](Behavior value) {\n\t\treturn (settings->closeBehavior() != value);\n\t}) | rpl::on_next([=](Behavior value) {\n\t\tsettings->setCloseBehavior(value);\n\t\tLocal::writeSettings();\n\t}, closeToTaskbar->lifetime());\n#endif // Q_OS_MAC || Q_OS_WIN\n\n\tif (Platform::AutostartSupported() && controller) {\n\t\tconst auto minimizedToggled = [=] {\n\t\t\treturn cStartMinimized()\n\t\t\t\t&& !controller->session().domain().local().hasLocalPasscode();\n\t\t};\n\n\t\tconst auto autostart = addCheckbox(\n\t\t\ttr::lng_settings_auto_start(),\n\t\t\tcAutoStart());\n\t\tconst auto minimized = addSlidingCheckbox(\n\t\t\ttr::lng_settings_start_min(),\n\t\t\tminimizedToggled());\n\n\t\tautostart->checkedChanges(\n\t\t) | rpl::filter([](bool checked) {\n\t\t\treturn (checked != cAutoStart());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tconst auto weak = base::make_weak(controller);\n\t\t\tcSetAutoStart(checked);\n\t\t\tPlatform::AutostartToggle(checked, crl::guard(autostart, [=](\n\t\t\t\t\tbool enabled) {\n\t\t\t\tif (checked && !enabled && weak) {\n\t\t\t\t\tweak->window().showToast(\n\t\t\t\t\t\tLang::Hard::AutostartEnableError());\n\t\t\t\t}\n\t\t\t\tUi::PostponeCall(autostart, [=] {\n\t\t\t\t\tautostart->setChecked(enabled);\n\t\t\t\t});\n\t\t\t\tif (enabled || !minimized->entity()->checked()) {\n\t\t\t\t\tLocal::writeSettings();\n\t\t\t\t} else {\n\t\t\t\t\tminimized->entity()->setChecked(false);\n\t\t\t\t}\n\t\t\t}));\n\t\t}, autostart->lifetime());\n\n\t\tPlatform::AutostartRequestStateFromSystem(crl::guard(\n\t\t\tcontroller,\n\t\t\t[=](bool enabled) { autostart->setChecked(enabled); }));\n\n\t\tminimized->toggleOn(autostart->checkedValue());\n\t\tminimized->entity()->checkedChanges(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\treturn (checked != minimizedToggled());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tif (controller->session().domain().local().hasLocalPasscode()) {\n\t\t\t\tminimized->entity()->setChecked(false);\n\t\t\t\tcontroller->show(Ui::MakeInformBox(\n\t\t\t\t\ttr::lng_error_start_minimized_passcoded()));\n\t\t\t} else {\n\t\t\t\tcSetStartMinimized(checked);\n\t\t\t\tLocal::writeSettings();\n\t\t\t}\n\t\t}, minimized->lifetime());\n\n\t\tcontroller->session().domain().local().localPasscodeChanged(\n\t\t) | rpl::on_next([=] {\n\t\t\tminimized->entity()->setChecked(minimizedToggled());\n\t\t}, minimized->lifetime());\n\t}\n\n\tif (Platform::IsWindows() && !Platform::IsWindowsStoreBuild()) {\n\t\tconst auto sendto = addCheckbox(\n\t\t\ttr::lng_settings_add_sendto(),\n\t\t\tcSendToMenu());\n\n\t\tsendto->checkedChanges(\n\t\t) | rpl::filter([](bool checked) {\n\t\t\treturn (checked != cSendToMenu());\n\t\t}) | rpl::on_next([](bool checked) {\n\t\t\tcSetSendToMenu(checked);\n\t\t\tpsSendToMenu(checked);\n\t\t\tLocal::writeSettings();\n\t\t}, sendto->lifetime());\n\t}\n}\n\nvoid SetupAnimations(\n\t\tnot_null<Window::Controller*> window,\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tcontainer->add(object_ptr<Button>(\n\t\tcontainer,\n\t\ttr::lng_settings_power_menu(),\n\t\tst::settingsButtonNoIcon\n\t))->setClickedCallback([=] {\n\t\twindow->show(Box(PowerSavingBox, PowerSaving::Flags()));\n\t});\n}\n\nvoid ArchiveSettingsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller) {\n\tbox->setTitle(tr::lng_settings_archive_title());\n\tbox->setWidth(st::boxWideWidth);\n\n\tbox->addButton(tr::lng_about_done(), [=] { box->closeBox(); });\n\n\tPreloadArchiveSettings(&controller->session());\n\n\tstruct State {\n\t\tUi::SlideWrap<Ui::VerticalLayout> *foldersWrap = nullptr;\n\t\tUi::SettingsButton *folders = nullptr;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tconst auto privacy = &controller->session().api().globalPrivacy();\n\n\tconst auto container = box->verticalLayout();\n\tAddSkip(container);\n\tAddSubsectionTitle(container, tr::lng_settings_unmuted_chats());\n\n\tusing Unarchive = Api::UnarchiveOnNewMessage;\n\tcontainer->add(object_ptr<Button>(\n\t\tcontainer,\n\t\ttr::lng_settings_always_in_archive(),\n\t\tst::settingsButtonNoIcon\n\t))->toggleOn(privacy->unarchiveOnNewMessage(\n\t) | rpl::map(\n\t\trpl::mappers::_1 == Unarchive::None\n\t))->toggledChanges(\n\t) | rpl::filter([=](bool toggled) {\n\t\tconst auto current = privacy->unarchiveOnNewMessageCurrent();\n\t\tstate->foldersWrap->toggle(!toggled, anim::type::normal);\n\t\treturn toggled != (current == Unarchive::None);\n\t}) | rpl::on_next([=](bool toggled) {\n\t\tprivacy->updateUnarchiveOnNewMessage(toggled\n\t\t\t? Unarchive::None\n\t\t\t: state->folders->toggled()\n\t\t\t? Unarchive::NotInFoldersUnmuted\n\t\t\t: Unarchive::AnyUnmuted);\n\t}, container->lifetime());\n\n\tAddSkip(container);\n\tAddDividerText(container, tr::lng_settings_unmuted_chats_about());\n\n\tstate->foldersWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto inner = state->foldersWrap->entity();\n\tAddSkip(inner);\n\tAddSubsectionTitle(inner, tr::lng_settings_chats_from_folders());\n\n\tstate->folders = inner->add(object_ptr<Button>(\n\t\tinner,\n\t\ttr::lng_settings_always_in_archive(),\n\t\tst::settingsButtonNoIcon\n\t))->toggleOn(privacy->unarchiveOnNewMessage(\n\t) | rpl::map(\n\t\trpl::mappers::_1 != Unarchive::AnyUnmuted\n\t));\n\tstate->folders->toggledChanges(\n\t) | rpl::filter([=](bool toggled) {\n\t\tconst auto current = privacy->unarchiveOnNewMessageCurrent();\n\t\treturn toggled != (current != Unarchive::AnyUnmuted);\n\t}) | rpl::on_next([=](bool toggled) {\n\t\tconst auto current = privacy->unarchiveOnNewMessageCurrent();\n\t\tprivacy->updateUnarchiveOnNewMessage(!toggled\n\t\t\t? Unarchive::AnyUnmuted\n\t\t\t: (current == Unarchive::AnyUnmuted)\n\t\t\t? Unarchive::NotInFoldersUnmuted\n\t\t\t: current);\n\t}, inner->lifetime());\n\n\tAddSkip(inner);\n\tAddDividerText(inner, tr::lng_settings_chats_from_folders_about());\n\n\tstate->foldersWrap->toggle(\n\t\tprivacy->unarchiveOnNewMessageCurrent() != Unarchive::None,\n\t\tanim::type::instant);\n\n\tSetupArchiveAndMute(controller, box->verticalLayout());\n}\n\nvoid PreloadArchiveSettings(not_null<::Main::Session*> session) {\n\tsession->api().globalPrivacy().reload();\n}\n\nType AdvancedId() {\n\treturn Advanced::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_advanced.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Main {\nclass Account;\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass GenericBox;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Window {\nclass Controller;\nclass SessionController;\n} // namespace Window\n\nnamespace Settings {\n\n[[nodiscard]] Type AdvancedId();\n\nvoid SetupConnectionType(\n\tnot_null<Window::Controller*> controller,\n\tnot_null<::Main::Account*> account,\n\tnot_null<Ui::VerticalLayout*> container);\nbool HasUpdate();\nvoid SetupUpdate(not_null<Ui::VerticalLayout*> container);\nvoid SetupWindowTitleContent(\n\tWindow::SessionController *controller,\n\tnot_null<Ui::VerticalLayout*> container);\nvoid SetupSystemIntegrationContent(\n\tWindow::SessionController *controller,\n\tnot_null<Ui::VerticalLayout*> container);\nvoid SetupAnimations(\n\tnot_null<Window::Controller*> window,\n\tnot_null<Ui::VerticalLayout*> container);\n\nvoid ArchiveSettingsBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> controller);\nvoid PreloadArchiveSettings(not_null<::Main::Session*> session);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_blocked_peers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_blocked_peers.h\"\n\n#include \"settings/settings_common_session.h\"\n\n#include \"api/api_blocked_peers.h\"\n#include \"apiwrap.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_peer.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_privacy_security.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/settings_privacy_controllers.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Settings {\nnamespace {\n\nusing namespace Builder;\n\nvoid BuildBlockedSection(SectionBuilder &builder) {\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"blocked/block-user\"_q,\n\t\t\t.title = tr::lng_blocked_list_add(tr::now),\n\t\t\t.keywords = { u\"block\"_q, u\"ban\"_q, u\"add\"_q },\n\t\t};\n\t});\n}\n\nclass Blocked : public Section<Blocked> {\npublic:\n\tBlocked(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\tvoid showFinished() override;\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\t[[nodiscard]] base::weak_qptr<Ui::RpWidget> createPinnedToTop(\n\t\tnot_null<QWidget*> parent) override;\n\nprivate:\n\tvoid setupContent();\n\tvoid checkTotal(int total);\n\n\tvoid visibleTopBottomUpdated(int visibleTop, int visibleBottom) override;\n\n\tconst not_null<Ui::VerticalLayout*> _container;\n\n\tbase::unique_qptr<Ui::RpWidget> _loading;\n\n\trpl::variable<int> _countBlocked;\n\n\trpl::event_stream<> _showFinished;\n\trpl::event_stream<bool> _emptinessChanges;\n\n\tQPointer<Ui::RpWidget> _blockUserButton;\n\n};\n\nBlocked::Blocked(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller)\n, _container(Ui::CreateChild<Ui::VerticalLayout>(this)) {\n\n\tsetupContent();\n\n\t{\n\t\tauto padding = st::changePhoneIconPadding;\n\t\tpadding.setBottom(padding.top());\n\t\t_loading = base::make_unique_q<Ui::PaddingWrap<>>(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tthis,\n\t\t\t\ttr::lng_contacts_loading(),\n\t\t\t\tst::changePhoneDescription),\n\t\t\tstd::move(padding));\n\t\tUi::ResizeFitChild(\n\t\t\tthis,\n\t\t\t_loading.get(),\n\t\t\tst::settingsBlockedHeightMin);\n\t}\n\n\tcontroller->session().api().blockedPeers().slice(\n\t) | rpl::on_next([=](const Api::BlockedPeers::Slice &slice) {\n\t\tcheckTotal(slice.total);\n\t}, lifetime());\n\n\tcontroller->session().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::IsBlocked\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tif (update.peer->isBlocked()) {\n\t\t\tcheckTotal(1);\n\t\t}\n\t}, lifetime());\n}\n\nrpl::producer<QString> Blocked::title() {\n\treturn tr::lng_settings_blocked_users();\n}\n\nbase::weak_qptr<Ui::RpWidget> Blocked::createPinnedToTop(\n\t\tnot_null<QWidget*> parent) {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(parent.get());\n\n\tUi::AddSkip(content);\n\n\tconst auto blockButton = AddButtonWithIcon(\n\t\tcontent,\n\t\ttr::lng_blocked_list_add(),\n\t\tst::settingsButtonActive,\n\t\t{ &st::menuIconBlockSettings });\n\t_blockUserButton = blockButton;\n\tblockButton->addClickHandler([=] {\n\t\tBlockedBoxController::BlockNewPeer(controller());\n\t});\n\n\tUi::AddSkip(content);\n\tUi::AddDividerText(content, tr::lng_blocked_list_about());\n\n\t{\n\t\tconst auto subtitle = content->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontent,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(content)))->setDuration(0);\n\t\tUi::AddSkip(subtitle->entity());\n\t\tauto subtitleText = _countBlocked.value(\n\t\t) | rpl::map([=](int count) {\n\t\t\treturn tr::lng_blocked_list_subtitle(tr::now, lt_count, count);\n\t\t});\n\t\tUi::AddSubsectionTitle(\n\t\t\tsubtitle->entity(),\n\t\t\trpl::duplicate(subtitleText),\n\t\t\tst::settingsBlockedListSubtitleAddPadding);\n\t\tsubtitle->toggleOn(\n\t\t\trpl::merge(\n\t\t\t\t_emptinessChanges.events() | rpl::map(!rpl::mappers::_1),\n\t\t\t\t_countBlocked.value() | rpl::map(rpl::mappers::_1 > 0)\n\t\t\t) | rpl::distinct_until_changed());\n\n\t\tstd::move(\n\t\t\tsubtitleText\n\t\t) | rpl::on_next([=] {\n\t\t\tsubtitle->entity()->resizeToWidth(content->width());\n\t\t}, subtitle->lifetime());\n\t}\n\n\treturn base::make_weak(not_null<Ui::RpWidget*>{ content });\n}\n\nvoid Blocked::setupContent() {\n\tusing namespace rpl::mappers;\n\n\tconst auto listWrap = _container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_container,\n\t\t\tobject_ptr<Ui::VerticalLayout>(_container)));\n\tlistWrap->toggleOn(\n\t\t_emptinessChanges.events_starting_with(true) | rpl::map(!_1),\n\t\tanim::type::instant);\n\n\t{\n\t\tstruct State {\n\t\t\tstd::unique_ptr<BlockedBoxController> controller;\n\t\t\tstd::unique_ptr<PeerListContentDelegateSimple> delegate;\n\t\t};\n\n\t\tauto controller = std::make_unique<BlockedBoxController>(\n\t\t\tthis->controller());\n\t\tcontroller->setStyleOverrides(&st::settingsBlockedList);\n\t\tconst auto content = listWrap->entity()->add(\n\t\t\tobject_ptr<PeerListContent>(this, controller.get()));\n\n\t\tconst auto state = content->lifetime().make_state<State>();\n\t\tstate->controller = std::move(controller);\n\t\tstate->delegate = std::make_unique<PeerListContentDelegateSimple>();\n\n\t\tstate->delegate->setContent(content);\n\t\tstate->controller->setDelegate(state->delegate.get());\n\n\t\tstate->controller->rowsCountChanges(\n\t\t) | rpl::on_next([=](int total) {\n\t\t\t_countBlocked = total;\n\t\t\tcheckTotal(total);\n\t\t}, content->lifetime());\n\t\t_countBlocked = content->fullRowsCount();\n\t}\n\n\tconst auto emptyWrap = _container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_container,\n\t\t\tobject_ptr<Ui::VerticalLayout>(_container)));\n\temptyWrap->toggleOn(\n\t\t_emptinessChanges.events_starting_with(false),\n\t\tanim::type::instant);\n\n\t{\n\t\tconst auto content = emptyWrap->entity();\n\t\tauto icon = CreateLottieIcon(\n\t\t\tcontent,\n\t\t\t{\n\t\t\t\t.name = u\"blocked_peers_empty\"_q,\n\t\t\t\t.sizeOverride = st::normalBoxLottieSize,\n\t\t\t},\n\t\t\tst::settingsBlockedListIconPadding);\n\t\tcontent->add(std::move(icon.widget));\n\n\t\t_showFinished.events(\n\t\t) | rpl::on_next([animate = std::move(icon.animate)] {\n\t\t\tanimate(anim::repeat::once);\n\t\t}, content->lifetime());\n\n\t\tcontent->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_blocked_list_empty_title(),\n\t\t\t\tst::changePhoneTitle),\n\t\t\tst::changePhoneTitlePadding,\n\t\t\tstyle::al_top);\n\n\t\tcontent->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_blocked_list_empty_description(),\n\t\t\t\tst::changePhoneDescription),\n\t\t\tst::changePhoneDescriptionPadding,\n\t\t\tstyle::al_top);\n\n\t\tUi::AddSkip(content, st::settingsBlockedListIconPadding.top());\n\t}\n\n\twidthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_container->resizeToWidth(width);\n\t}, _container->lifetime());\n\n\trpl::combine(\n\t\t_container->heightValue(),\n\t\t_emptinessChanges.events_starting_with(true)\n\t) | rpl::on_next([=](int height, bool empty) {\n\t\tconst auto subtitled = !empty || (_countBlocked.current() > 0);\n\t\tconst auto total = st::settingsBlockedHeightMin;\n\t\tconst auto padding = st::defaultSubsectionTitlePadding\n\t\t\t+ st::settingsBlockedListSubtitleAddPadding;\n\t\tconst auto subtitle = st::defaultVerticalListSkip\n\t\t\t+ padding.top()\n\t\t\t+ st::defaultSubsectionTitle.style.font->height\n\t\t\t+ padding.bottom();\n\t\tconst auto min = total - (subtitled ? subtitle : 0);\n\t\tresize(width(), std::max(height, min));\n\t}, _container->lifetime());\n\n\tconst SectionBuildMethod buildMethod = [](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tauto &lifetime = container->lifetime();\n\t\tconst auto highlights = lifetime.make_state<HighlightRegistry>();\n\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = Window::PausedIn(\n\t\t\t\tcontroller,\n\t\t\t\tWindow::GifPauseReason::Layer),\n\t\t\t.highlights = highlights,\n\t\t});\n\n\t\tBuildBlockedSection(builder);\n\n\t\tstd::move(showFinished) | rpl::on_next([=] {\n\t\t\tfor (const auto &[id, entry] : *highlights) {\n\t\t\t\tif (entry.widget) {\n\t\t\t\t\tcontroller->checkHighlightControl(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tentry.widget,\n\t\t\t\t\t\tbase::duplicate(entry.args));\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime);\n\t};\n\n\tbuild(_container, buildMethod);\n}\n\nvoid Blocked::checkTotal(int total) {\n\t_loading = nullptr;\n\t_emptinessChanges.fire(total <= 0);\n}\n\nvoid Blocked::visibleTopBottomUpdated(int visibleTop, int visibleBottom) {\n\tsetChildVisibleTopBottom(_container, visibleTop, visibleBottom);\n}\n\nvoid Blocked::showFinished() {\n\tSection::showFinished();\n\t_showFinished.fire({});\n\tcontroller()->checkHighlightControl(\n\t\tu\"blocked/block-user\"_q,\n\t\t_blockUserButton);\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = Blocked::Id(),\n\t.parentId = PrivacySecurityId(),\n\t.title = &tr::lng_settings_blocked_users,\n\t.icon = &st::menuIconBlock,\n}, [](SectionBuilder &builder) {\n\tBuildBlockedSection(builder);\n});\n\n} // namespace\n\nType BlockedPeersId() {\n\treturn Blocked::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_blocked_peers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Settings {\n\n[[nodiscard]] Type BlockedPeersId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_business.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_business.h\"\n\n#include \"api/api_chat_links.h\"\n#include \"api/api_premium.h\"\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\"\n#include \"data/business/data_business_chatbots.h\"\n#include \"data/business/data_business_info.h\"\n#include \"data/business/data_shortcut_messages.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"info/info_wrap_widget.h\"\n#include \"info/settings/info_settings_widget.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"settings/business/settings_away_message.h\"\n#include \"settings/business/settings_chat_intro.h\"\n#include \"settings/business/settings_chat_links.h\"\n#include \"settings/business/settings_chatbots.h\"\n#include \"settings/business/settings_greeting.h\"\n#include \"settings/business/settings_location.h\"\n#include \"settings/business/settings_quick_replies.h\"\n#include \"settings/business/settings_working_hours.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/settings_common_session.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"ui/controls/swipe_handler.h\"\n#include \"ui/controls/swipe_handler_data.h\"\n#include \"ui/effects/gradient.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/effects/premium_top_bar.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/new_badges.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/gradient_round_button.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_channel_earn.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nusing namespace Builder;\n\nstruct Entry {\n\tconst style::icon *icon;\n\trpl::producer<QString> title;\n\trpl::producer<QString> description;\n\tPremiumFeature feature = PremiumFeature::BusinessLocation;\n\tbool newBadge = false;\n};\n\nstruct BusinessState {\n\tFn<void(bool)> setPaused;\n\tQPointer<Ui::SettingsButton> sponsoredButton;\n\tbase::flat_map<PremiumFeature, QPointer<Ui::SettingsButton>> featureButtons;\n};\n\nusing Order = std::vector<QString>;\n\n[[nodiscard]] Order FallbackOrder() {\n\treturn Order{\n\t\tu\"greeting_message\"_q,\n\t\tu\"away_message\"_q,\n\t\tu\"quick_replies\"_q,\n\t\tu\"business_hours\"_q,\n\t\tu\"business_location\"_q,\n\t\tu\"business_links\"_q,\n\t\tu\"business_intro\"_q,\n\t\tu\"business_bots\"_q,\n\t\tu\"folder_tags\"_q,\n\t};\n}\n\n[[nodiscard]] base::flat_map<QString, Entry> EntryMap() {\n\treturn base::flat_map<QString, Entry>{\n\t\t{\n\t\t\tu\"business_location\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsBusinessIconLocation,\n\t\t\t\ttr::lng_business_subtitle_location(),\n\t\t\t\ttr::lng_business_about_location(),\n\t\t\t\tPremiumFeature::BusinessLocation,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"business_hours\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsBusinessIconHours,\n\t\t\t\ttr::lng_business_subtitle_opening_hours(),\n\t\t\t\ttr::lng_business_about_opening_hours(),\n\t\t\t\tPremiumFeature::BusinessHours,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"quick_replies\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsBusinessIconReplies,\n\t\t\t\ttr::lng_business_subtitle_quick_replies(),\n\t\t\t\ttr::lng_business_about_quick_replies(),\n\t\t\t\tPremiumFeature::QuickReplies,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"greeting_message\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsBusinessIconGreeting,\n\t\t\t\ttr::lng_business_subtitle_greeting_messages(),\n\t\t\t\ttr::lng_business_about_greeting_messages(),\n\t\t\t\tPremiumFeature::GreetingMessage,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"away_message\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsBusinessIconAway,\n\t\t\t\ttr::lng_business_subtitle_away_messages(),\n\t\t\t\ttr::lng_business_about_away_messages(),\n\t\t\t\tPremiumFeature::AwayMessage,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"business_bots\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsBusinessIconChatbots,\n\t\t\t\ttr::lng_business_subtitle_chatbots(),\n\t\t\t\ttr::lng_business_about_chatbots(),\n\t\t\t\tPremiumFeature::BusinessBots,\n\t\t\t\ttrue,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"business_intro\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsBusinessIconChatIntro,\n\t\t\t\ttr::lng_business_subtitle_chat_intro(),\n\t\t\t\ttr::lng_business_about_chat_intro(),\n\t\t\t\tPremiumFeature::ChatIntro,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"business_links\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsBusinessIconChatLinks,\n\t\t\t\ttr::lng_business_subtitle_chat_links(),\n\t\t\t\ttr::lng_business_about_chat_links(),\n\t\t\t\tPremiumFeature::ChatLinks,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"folder_tags\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconTags,\n\t\t\t\ttr::lng_premium_summary_subtitle_filter_tags(),\n\t\t\t\ttr::lng_premium_summary_about_filter_tags(),\n\t\t\t\tPremiumFeature::FilterTags,\n\t\t\t},\n\t\t},\n\t};\n}\n\nvoid AddBusinessSummary(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tstd::shared_ptr<BusinessState> state,\n\t\tFn<void(PremiumFeature)> buttonCallback) {\n\tconst auto &stDefault = st::settingsButton;\n\tconst auto &stLabel = st::defaultFlatLabel;\n\tconst auto iconSize = st::settingsPremiumIconDouble.size();\n\tconst auto &titlePadding = st::settingsPremiumRowTitlePadding;\n\tconst auto &descriptionPadding = st::settingsPremiumRowAboutPadding;\n\n\tauto entryMap = EntryMap();\n\tauto iconContainers = std::vector<Ui::AbstractButton*>();\n\ticonContainers.reserve(int(entryMap.size()));\n\n\tconst auto addRow = [&](Entry &entry) {\n\t\tconst auto labelAscent = stLabel.style.font->ascent;\n\t\tconst auto button = Ui::CreateChild<Ui::SettingsButton>(\n\t\t\tcontent.get(),\n\t\t\trpl::single(QString()));\n\n\t\tconst auto label = content->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\tstd::move(entry.title) | rpl::map(tr::bold),\n\t\t\t\tstLabel),\n\t\t\ttitlePadding);\n\t\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tconst auto description = content->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\tstd::move(entry.description),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tdescriptionPadding);\n\t\tdescription->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t\tif (entry.newBadge) {\n\t\t\tUi::NewBadge::AddAfterLabel(content, label);\n\t\t}\n\t\tconst auto dummy = Ui::CreateChild<Ui::AbstractButton>(content.get());\n\t\tdummy->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t\tcontent->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\tdummy->resize(s.width(), iconSize.height());\n\t\t}, dummy->lifetime());\n\n\t\tlabel->geometryValue(\n\t\t) | rpl::on_next([=](const QRect &r) {\n\t\t\tdummy->moveToLeft(0, r.y() + (r.height() - labelAscent));\n\t\t}, dummy->lifetime());\n\n\t\trpl::combine(\n\t\t\tcontent->widthValue(),\n\t\t\tlabel->heightValue(),\n\t\t\tdescription->heightValue()\n\t\t) | rpl::on_next([=,\n\t\t\ttopPadding = titlePadding,\n\t\t\tbottomPadding = descriptionPadding](\n\t\t\t\tint width,\n\t\t\t\tint topHeight,\n\t\t\t\tint bottomHeight) {\n\t\t\tbutton->resize(\n\t\t\t\twidth,\n\t\t\t\ttopPadding.top()\n\t\t\t\t\t+ topHeight\n\t\t\t\t\t+ topPadding.bottom()\n\t\t\t\t\t+ bottomPadding.top()\n\t\t\t\t\t+ bottomHeight\n\t\t\t\t\t+ bottomPadding.bottom());\n\t\t}, button->lifetime());\n\t\tlabel->topValue(\n\t\t) | rpl::on_next([=, padding = titlePadding.top()](int top) {\n\t\t\tbutton->moveToLeft(0, top - padding);\n\t\t}, button->lifetime());\n\t\tconst auto arrow = Ui::CreateChild<Ui::IconButton>(\n\t\t\tbutton,\n\t\t\tst::backButton);\n\t\tarrow->setIconOverride(\n\t\t\t&st::settingsPremiumArrow,\n\t\t\t&st::settingsPremiumArrowOver);\n\t\tarrow->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tbutton->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\tconst auto &point = st::settingsPremiumArrowShift;\n\t\t\tarrow->moveToRight(\n\t\t\t\t-point.x(),\n\t\t\t\tpoint.y() + (s.height() - arrow->height()) / 2);\n\t\t}, arrow->lifetime());\n\n\t\tconst auto feature = entry.feature;\n\t\tbutton->setClickedCallback([=] { buttonCallback(feature); });\n\n\t\tif (state) {\n\t\t\tstate->featureButtons[feature] = button;\n\t\t}\n\n\t\ticonContainers.push_back(dummy);\n\t};\n\n\tauto icons = std::vector<const style::icon *>();\n\ticons.reserve(int(entryMap.size()));\n\t{\n\t\tconst auto session = &controller->session();\n\t\tconst auto mtpOrder = session->appConfig().get<Order>(\n\t\t\t\"business_promo_order\",\n\t\t\tFallbackOrder());\n\t\tconst auto processEntry = [&](Entry &entry) {\n\t\t\ticons.push_back(entry.icon);\n\t\t\taddRow(entry);\n\t\t};\n\n\t\tfor (const auto &key : mtpOrder) {\n\t\t\tauto it = entryMap.find(key);\n\t\t\tif (it == end(entryMap)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tprocessEntry(it->second);\n\t\t}\n\t}\n\n\tcontent->resizeToWidth(content->height());\n\n\tAssert(iconContainers.size() > 2);\n\tconst auto from = iconContainers.front()->y();\n\tconst auto to = iconContainers.back()->y() + iconSize.height();\n\tauto gradient = QLinearGradient(0, 0, 0, to - from);\n\tgradient.setStops(Ui::Premium::FullHeightGradientStops());\n\tfor (auto i = 0; i < int(icons.size()); i++) {\n\t\tconst auto &iconContainer = iconContainers[i];\n\n\t\tconst auto pointTop = iconContainer->y() - from;\n\t\tconst auto pointBottom = pointTop + iconContainer->height();\n\t\tconst auto ratioTop = pointTop / float64(to - from);\n\t\tconst auto ratioBottom = pointBottom / float64(to - from);\n\n\t\tauto resultGradient = QLinearGradient(\n\t\t\tQPointF(),\n\t\t\tQPointF(0, pointBottom - pointTop));\n\n\t\tresultGradient.setColorAt(\n\t\t\t.0,\n\t\t\tanim::gradient_color_at(gradient, ratioTop));\n\t\tresultGradient.setColorAt(\n\t\t\t.1,\n\t\t\tanim::gradient_color_at(gradient, ratioBottom));\n\n\t\tconst auto brush = QBrush(resultGradient);\n\t\tAddButtonIcon(\n\t\t\ticonContainer,\n\t\t\tstDefault,\n\t\t\t{ .icon = icons[i], .backgroundBrush = brush });\n\t}\n\n\tUi::AddSkip(content, descriptionPadding.bottom());\n}\n\n[[nodiscard]] QString FeatureSearchId(PremiumFeature feature) {\n\tswitch (feature) {\n\tcase PremiumFeature::GreetingMessage: return u\"business/greeting\"_q;\n\tcase PremiumFeature::AwayMessage: return u\"business/away\"_q;\n\tcase PremiumFeature::QuickReplies: return u\"business/replies\"_q;\n\tcase PremiumFeature::BusinessHours: return u\"business/hours\"_q;\n\tcase PremiumFeature::BusinessLocation: return u\"business/location\"_q;\n\tcase PremiumFeature::ChatLinks: return u\"business/links\"_q;\n\tcase PremiumFeature::ChatIntro: return u\"business/intro\"_q;\n\tcase PremiumFeature::BusinessBots: return u\"business/bots\"_q;\n\tcase PremiumFeature::FilterTags: return u\"business/tags\"_q;\n\tdefault: return QString();\n\t}\n}\n\n[[nodiscard]] QString FeatureSearchTitle(PremiumFeature feature) {\n\tswitch (feature) {\n\tcase PremiumFeature::GreetingMessage:\n\t\treturn tr::lng_business_subtitle_greeting_messages(tr::now);\n\tcase PremiumFeature::AwayMessage:\n\t\treturn tr::lng_business_subtitle_away_messages(tr::now);\n\tcase PremiumFeature::QuickReplies:\n\t\treturn tr::lng_business_subtitle_quick_replies(tr::now);\n\tcase PremiumFeature::BusinessHours:\n\t\treturn tr::lng_business_subtitle_opening_hours(tr::now);\n\tcase PremiumFeature::BusinessLocation:\n\t\treturn tr::lng_business_subtitle_location(tr::now);\n\tcase PremiumFeature::ChatLinks:\n\t\treturn tr::lng_business_subtitle_chat_links(tr::now);\n\tcase PremiumFeature::ChatIntro:\n\t\treturn tr::lng_business_subtitle_chat_intro(tr::now);\n\tcase PremiumFeature::BusinessBots:\n\t\treturn tr::lng_business_subtitle_chatbots(tr::now);\n\tcase PremiumFeature::FilterTags:\n\t\treturn tr::lng_premium_summary_subtitle_filter_tags(tr::now);\n\tdefault: return QString();\n\t}\n}\n\nvoid BuildBusinessFeatures(SectionBuilder &builder) {\n\tconst auto session = builder.session();\n\tconst auto mtpOrder = session->appConfig().get<Order>(\n\t\t\"business_promo_order\",\n\t\tFallbackOrder());\n\n\tconst auto entryMap = EntryMap();\n\tfor (const auto &key : mtpOrder) {\n\t\tconst auto it = entryMap.find(key);\n\t\tif (it == end(entryMap)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto feature = it->second.feature;\n\t\tconst auto id = FeatureSearchId(feature);\n\t\tif (id.isEmpty()) {\n\t\t\tcontinue;\n\t\t}\n\t\tbuilder.add(nullptr, [feature, id] {\n\t\t\treturn SearchEntry{\n\t\t\t\t.id = id,\n\t\t\t\t.title = FeatureSearchTitle(feature),\n\t\t\t\t.keywords = { u\"business\"_q },\n\t\t\t};\n\t\t});\n\t}\n}\n\nvoid BuildSponsoredSection(\n\t\tSectionBuilder &builder,\n\t\tstd::shared_ptr<BusinessState> state) {\n\tconst auto controller = builder.controller();\n\tconst auto session = builder.session();\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"business/sponsored\"_q,\n\t\t\t.title = tr::lng_business_button_sponsored(tr::now),\n\t\t\t.keywords = { u\"ads\"_q, u\"advertising\"_q },\n\t\t};\n\t});\n\n\tif (!controller) {\n\t\treturn;\n\t}\n\n\tbuilder.add([controller, session, state](const WidgetContext &ctx) {\n\t\tconst auto content = ctx.container;\n\t\tconst auto sponsoredWrap = content->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontent,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(content)));\n\n\t\tconst auto fillSponsoredWrap = [=] {\n\t\t\twhile (sponsoredWrap->entity()->count()) {\n\t\t\t\tdelete sponsoredWrap->entity()->widgetAt(0);\n\t\t\t}\n\t\t\tUi::AddDivider(sponsoredWrap->entity());\n\t\t\tconst auto loading = sponsoredWrap->entity()->add(\n\t\t\t\tobject_ptr<Ui::SlideWrap<Ui::FlatLabel>>(\n\t\t\t\t\tsponsoredWrap->entity(),\n\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\tsponsoredWrap->entity(),\n\t\t\t\t\t\ttr::lng_contacts_loading())),\n\t\t\t\tst::boxRowPadding);\n\t\t\tloading->entity()->setTextColorOverride(st::windowSubTextFg->c);\n\n\t\t\tconst auto wrap = sponsoredWrap->entity()->add(\n\t\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\t\tsponsoredWrap->entity(),\n\t\t\t\t\tobject_ptr<Ui::VerticalLayout>(sponsoredWrap->entity())));\n\t\t\twrap->toggle(false, anim::type::instant);\n\t\t\tconst auto inner = wrap->entity();\n\t\t\tUi::AddSkip(inner);\n\t\t\tUi::AddSubsectionTitle(\n\t\t\t\tinner,\n\t\t\t\ttr::lng_business_subtitle_sponsored());\n\t\t\tconst auto button = inner->add(object_ptr<Ui::SettingsButton>(\n\t\t\t\tinner,\n\t\t\t\ttr::lng_business_button_sponsored()));\n\t\t\tif (state) {\n\t\t\t\tstate->sponsoredButton = button;\n\t\t\t}\n\t\t\tUi::AddSkip(inner);\n\n\t\t\t{\n\t\t\t\tinner->add(object_ptr<Ui::DividerLabel>(\n\t\t\t\t\tinner,\n\t\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\t\tinner,\n\t\t\t\t\t\ttr::lng_business_about_sponsored(\n\t\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\t\trpl::combine(\n\t\t\t\t\t\t\t\ttr::lng_business_about_sponsored_link(\n\t\t\t\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\t\t\t\trpl::single(Ui::Text::IconEmoji(\n\t\t\t\t\t\t\t\t\t\t&st::textMoreIconEmoji)),\n\t\t\t\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t\t\t\ttr::lng_business_about_sponsored_url()\n\t\t\t\t\t\t\t) | rpl::map([](TextWithEntities text, QString url) {\n\t\t\t\t\t\t\t\treturn tr::link(text, url);\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t\tst::boxDividerLabel),\n\t\t\t\t\tst::defaultBoxDividerLabelPadding));\n\t\t\t}\n\n\t\t\tconst auto api = inner->lifetime().make_state<Api::SponsoredToggle>(\n\t\t\t\tsession);\n\n\t\t\tapi->toggled(\n\t\t\t) | rpl::on_next([=](bool enabled) {\n\t\t\t\tbutton->toggleOn(rpl::single(enabled));\n\t\t\t\twrap->toggle(true, anim::type::instant);\n\t\t\t\tloading->toggle(false, anim::type::instant);\n\n\t\t\t\tbutton->toggledChanges(\n\t\t\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\t\t\tapi->setToggled(\n\t\t\t\t\t\ttoggled\n\t\t\t\t\t) | rpl::on_error_done([=](const QString &error) {\n\t\t\t\t\t\tcontroller->showToast(error);\n\t\t\t\t\t}, [] {\n\t\t\t\t\t}, button->lifetime());\n\t\t\t\t}, button->lifetime());\n\t\t\t}, inner->lifetime());\n\n\t\t\tUi::ToggleChildrenVisibility(sponsoredWrap->entity(), true);\n\t\t\tsponsoredWrap->entity()->resizeToWidth(content->width());\n\t\t};\n\n\t\tData::AmPremiumValue(session) | rpl::on_next([=](bool isPremium) {\n\t\t\tsponsoredWrap->toggle(isPremium, anim::type::normal);\n\t\t\tif (isPremium) {\n\t\t\t\tfillSponsoredWrap();\n\t\t\t}\n\t\t}, content->lifetime());\n\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n}\n\nvoid BuildBusinessSectionContent(\n\t\tSectionBuilder &builder,\n\t\tstd::shared_ptr<BusinessState> state) {\n\tconst auto controller = builder.controller();\n\tconst auto session = builder.session();\n\n\tif (controller) {\n\t\tconst auto owner = &session->data();\n\t\towner->chatbots().preload();\n\t\towner->businessInfo().preload();\n\t\towner->shortcutMessages().preloadShortcuts();\n\t\towner->session().api().chatLinks().preload();\n\t}\n\n\tbuilder.addSkip(st::settingsFromFileTop);\n\n\tBuildBusinessFeatures(builder);\n\n\tif (controller) {\n\t\tbuilder.add([controller, session, state](const WidgetContext &ctx) {\n\t\t\tconst auto content = ctx.container;\n\t\t\tconst auto owner = &session->data();\n\t\t\tconst auto showOther = ctx.showOther;\n\n\t\t\tauto waitingToShow = content->lifetime().make_state<PremiumFeature>(\n\t\t\t\tPremiumFeature::Business);\n\n\t\t\tconst auto showFeature = [=](PremiumFeature feature) {\n\t\t\t\tif (feature == PremiumFeature::FilterTags) {\n\t\t\t\t\tShowPremiumPreviewToBuy(controller, feature);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tshowOther([&] {\n\t\t\t\t\tswitch (feature) {\n\t\t\t\t\tcase PremiumFeature::AwayMessage: return AwayMessageId();\n\t\t\t\t\tcase PremiumFeature::BusinessHours: return WorkingHoursId();\n\t\t\t\t\tcase PremiumFeature::BusinessLocation: return LocationId();\n\t\t\t\t\tcase PremiumFeature::GreetingMessage: return GreetingId();\n\t\t\t\t\tcase PremiumFeature::QuickReplies: return QuickRepliesId();\n\t\t\t\t\tcase PremiumFeature::BusinessBots: return ChatbotsId();\n\t\t\t\t\tcase PremiumFeature::ChatIntro: return ChatIntroId();\n\t\t\t\t\tcase PremiumFeature::ChatLinks: return ChatLinksId();\n\t\t\t\t\t}\n\t\t\t\t\tUnexpected(\"Feature in showFeature.\");\n\t\t\t\t}());\n\t\t\t};\n\n\t\t\tconst auto isReady = [=](PremiumFeature feature) {\n\t\t\t\tswitch (feature) {\n\t\t\t\tcase PremiumFeature::AwayMessage:\n\t\t\t\t\treturn owner->businessInfo().awaySettingsLoaded()\n\t\t\t\t\t\t&& owner->shortcutMessages().shortcutsLoaded();\n\t\t\t\tcase PremiumFeature::BusinessHours:\n\t\t\t\t\treturn owner->session().user()->isFullLoaded()\n\t\t\t\t\t\t&& owner->businessInfo().timezonesLoaded();\n\t\t\t\tcase PremiumFeature::BusinessLocation:\n\t\t\t\t\treturn owner->session().user()->isFullLoaded();\n\t\t\t\tcase PremiumFeature::GreetingMessage:\n\t\t\t\t\treturn owner->businessInfo().greetingSettingsLoaded()\n\t\t\t\t\t\t&& owner->shortcutMessages().shortcutsLoaded();\n\t\t\t\tcase PremiumFeature::QuickReplies:\n\t\t\t\t\treturn owner->shortcutMessages().shortcutsLoaded();\n\t\t\t\tcase PremiumFeature::BusinessBots:\n\t\t\t\t\treturn owner->chatbots().loaded();\n\t\t\t\tcase PremiumFeature::ChatIntro:\n\t\t\t\t\treturn owner->session().user()->isFullLoaded();\n\t\t\t\tcase PremiumFeature::ChatLinks:\n\t\t\t\t\treturn owner->session().api().chatLinks().loaded();\n\t\t\t\tcase PremiumFeature::FilterTags:\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tUnexpected(\"Feature in isReady.\");\n\t\t\t};\n\n\t\t\tconst auto check = [=] {\n\t\t\t\tif (*waitingToShow != PremiumFeature::Business\n\t\t\t\t\t&& isReady(*waitingToShow)) {\n\t\t\t\t\tshowFeature(\n\t\t\t\t\t\tstd::exchange(*waitingToShow, PremiumFeature::Business));\n\t\t\t\t}\n\t\t\t};\n\n\t\t\trpl::merge(\n\t\t\t\towner->businessInfo().awaySettingsChanged(),\n\t\t\t\towner->businessInfo().greetingSettingsChanged(),\n\t\t\t\towner->businessInfo().timezonesValue() | rpl::to_empty,\n\t\t\t\towner->shortcutMessages().shortcutsChanged(),\n\t\t\t\towner->chatbots().changes() | rpl::to_empty,\n\t\t\t\towner->session().changes().peerUpdates(\n\t\t\t\t\towner->session().user(),\n\t\t\t\t\tData::PeerUpdate::Flag::FullInfo) | rpl::to_empty,\n\t\t\t\towner->session().api().chatLinks().loadedUpdates()\n\t\t\t) | rpl::on_next(check, content->lifetime());\n\n\t\t\tAddBusinessSummary(content, controller, state, [=](PremiumFeature feature) {\n\t\t\t\tif (!session->premium()) {\n\t\t\t\t\tif (state && state->setPaused) {\n\t\t\t\t\t\tstate->setPaused(true);\n\t\t\t\t\t}\n\t\t\t\t\tconst auto hidden = crl::guard(content, [=] {\n\t\t\t\t\t\tif (state && state->setPaused) {\n\t\t\t\t\t\t\tstate->setPaused(false);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\tShowPremiumPreviewToBuy(controller, feature, hidden);\n\t\t\t\t\treturn;\n\t\t\t\t} else if (!isReady(feature)) {\n\t\t\t\t\t*waitingToShow = feature;\n\t\t\t\t} else {\n\t\t\t\t\tshowFeature(feature);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\treturn SectionBuilder::WidgetToAdd{};\n\t\t});\n\t}\n\n\tBuildSponsoredSection(builder, state);\n}\n\nclass Business : public Section<Business> {\npublic:\n\tBusiness(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\t[[nodiscard]] base::weak_qptr<Ui::RpWidget> createPinnedToTop(\n\t\tnot_null<QWidget*> parent) override;\n\t[[nodiscard]] base::weak_qptr<Ui::RpWidget> createPinnedToBottom(\n\t\tnot_null<Ui::RpWidget*> parent) override;\n\n\tvoid showFinished() override;\n\n\t[[nodiscard]] bool hasFlexibleTopBar() const override;\n\n\tvoid setStepDataReference(std::any &data) override;\n\n\t[[nodiscard]] rpl::producer<> sectionShowBack() override final;\n\nprivate:\n\tvoid setupContent();\n\tvoid setupSwipeBack();\n\n\tstd::shared_ptr<BusinessState> _state;\n\tQPointer<Ui::GradientButton> _subscribe;\n\tbase::unique_qptr<Ui::FadeWrap<Ui::IconButton>> _back;\n\tbase::unique_qptr<Ui::IconButton> _close;\n\trpl::variable<bool> _backToggles;\n\trpl::variable<Info::Wrap> _wrap;\n\tstd::shared_ptr<Ui::RadiobuttonGroup> _radioGroup;\n\n\trpl::event_stream<> _showBack;\n\trpl::event_stream<> _showFinished;\n\trpl::variable<QString> _buttonText;\n\n};\n\nBusiness::Business(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller)\n, _state(std::make_shared<BusinessState>())\n, _radioGroup(std::make_shared<Ui::RadiobuttonGroup>()) {\n\tsetupContent();\n\tsetupSwipeBack();\n\tcontroller->session().api().premium().reload();\n}\n\nrpl::producer<QString> Business::title() {\n\treturn tr::lng_premium_summary_title();\n}\n\nbool Business::hasFlexibleTopBar() const {\n\treturn true;\n}\n\nrpl::producer<> Business::sectionShowBack() {\n\treturn _showBack.events();\n}\n\nvoid Business::setStepDataReference(std::any &data) {\n\tusing namespace Info::Settings;\n\tconst auto my = std::any_cast<SectionCustomTopBarData>(&data);\n\tif (my) {\n\t\t_backToggles = std::move(\n\t\t\tmy->backButtonEnables\n\t\t) | rpl::map_to(true);\n\t\t_wrap = std::move(my->wrapValue);\n\t}\n}\n\nvoid Business::setupSwipeBack() {\n\tusing namespace Ui::Controls;\n\n\tauto swipeBackData = lifetime().make_state<SwipeBackResult>();\n\n\tauto update = [=](SwipeContextData data) {\n\t\tif (data.translation > 0) {\n\t\t\tif (!swipeBackData->callback) {\n\t\t\t\t(*swipeBackData) = SetupSwipeBack(\n\t\t\t\t\tthis,\n\t\t\t\t\t[]() -> std::pair<QColor, QColor> {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tst::historyForwardChooseBg->c,\n\t\t\t\t\t\t\tst::historyForwardChooseFg->c,\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t}\n\t\t\tswipeBackData->callback(data);\n\t\t\treturn;\n\t\t} else if (swipeBackData->lifetime) {\n\t\t\t(*swipeBackData) = {};\n\t\t}\n\t};\n\n\tauto init = [=](int, Qt::LayoutDirection direction) {\n\t\treturn (direction == Qt::RightToLeft)\n\t\t\t? DefaultSwipeBackHandlerFinishData([=] {\n\t\t\t\t_showBack.fire({});\n\t\t\t})\n\t\t\t: SwipeHandlerFinishData();\n\t};\n\n\tSetupSwipeHandler({\n\t\t.widget = this,\n\t\t.scroll = v::null,\n\t\t.update = std::move(update),\n\t\t.init = std::move(init),\n\t});\n}\n\nvoid Business::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tconst auto state = _state;\n\n\tconst SectionBuildMethod buildMethod = [state](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tconst auto isPaused = Window::PausedIn(\n\t\t\tcontroller,\n\t\t\tWindow::GifPauseReason::Layer);\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = isPaused,\n\t\t});\n\n\t\tBuildBusinessSectionContent(builder, state);\n\t};\n\n\tbuild(content, buildMethod);\n\n\tUi::ResizeFitChild(this, content);\n}\n\nbase::weak_qptr<Ui::RpWidget> Business::createPinnedToTop(\n\t\tnot_null<QWidget*> parent) {\n\tauto title = tr::lng_business_title();\n\tauto about = [&]() -> rpl::producer<TextWithEntities> {\n\t\treturn rpl::conditional(\n\t\t\tData::AmPremiumValue(&controller()->session()),\n\t\t\ttr::lng_business_unlocked(tr::marked),\n\t\t\ttr::lng_business_about(tr::marked));\n\t}();\n\n\tconst auto content = [&]() -> Ui::Premium::TopBarAbstract* {\n\t\tconst auto weak = base::make_weak(controller());\n\t\tconst auto clickContextOther = [=] {\n\t\t\treturn QVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.sessionWindow = weak,\n\t\t\t\t.botStartAutoSubmit = true,\n\t\t\t});\n\t\t};\n\t\treturn Ui::CreateChild<Ui::Premium::TopBar>(\n\t\t\tparent.get(),\n\t\t\tst::defaultPremiumCover,\n\t\t\tUi::Premium::TopBarDescriptor{\n\t\t\t\t.clickContextOther = clickContextOther,\n\t\t\t\t.logo = u\"dollar\"_q,\n\t\t\t\t.title = std::move(title),\n\t\t\t\t.about = std::move(about),\n\t\t\t});\n\t}();\n\t_state->setPaused = [=](bool paused) {\n\t\tcontent->setPaused(paused);\n\t\tif (_subscribe) {\n\t\t\t_subscribe->setGlarePaused(paused);\n\t\t}\n\t};\n\n\t_wrap.value(\n\t) | rpl::on_next([=](Info::Wrap wrap) {\n\t\tcontent->setRoundEdges(wrap == Info::Wrap::Layer);\n\t}, content->lifetime());\n\n\tcontent->setMaximumHeight(st::settingsPremiumTopHeight);\n\tcontent->setMinimumHeight(st::settingsPremiumTopHeight);\n\n\tcontent->resize(content->width(), content->maximumHeight());\n\n\t_wrap.value(\n\t) | rpl::on_next([=](Info::Wrap wrap) {\n\t\tconst auto isLayer = (wrap == Info::Wrap::Layer);\n\t\t_back = base::make_unique_q<Ui::FadeWrap<Ui::IconButton>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::IconButton>(\n\t\t\t\tcontent,\n\t\t\t\t(isLayer\n\t\t\t\t\t? st::settingsPremiumLayerTopBarBack\n\t\t\t\t\t: st::settingsPremiumTopBarBack)),\n\t\t\tst::infoTopBarScale);\n\t\t_back->setDuration(0);\n\t\t_back->toggleOn(isLayer\n\t\t\t? _backToggles.value() | rpl::type_erased\n\t\t\t: rpl::single(true));\n\t\t_back->entity()->addClickHandler([=] {\n\t\t\t_showBack.fire({});\n\t\t});\n\t\t_back->toggledValue(\n\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\tconst auto &st = isLayer ? st::infoLayerTopBar : st::infoTopBar;\n\t\t\tcontent->setTextPosition(\n\t\t\t\ttoggled ? st.back.width : st.titlePosition.x(),\n\t\t\t\tst.titlePosition.y());\n\t\t}, _back->lifetime());\n\n\t\tif (!isLayer) {\n\t\t\t_close = nullptr;\n\t\t} else {\n\t\t\t_close = base::make_unique_q<Ui::IconButton>(\n\t\t\t\tcontent,\n\t\t\t\tst::settingsPremiumTopBarClose);\n\t\t\t_close->addClickHandler([=] {\n\t\t\t\tcontroller()->parentController()->hideLayer();\n\t\t\t\tcontroller()->parentController()->hideSpecialLayer();\n\t\t\t});\n\t\t\tcontent->widthValue(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\t_close->moveToRight(0, 0);\n\t\t\t}, _close->lifetime());\n\t\t}\n\t}, content->lifetime());\n\n\treturn base::make_weak(not_null<Ui::RpWidget*>{ content });\n}\n\nvoid Business::showFinished() {\n\t_showFinished.fire({});\n\tcrl::on_main(this, [=] {\n\t\tfor (const auto &[feature, button] : _state->featureButtons) {\n\t\t\tconst auto id = FeatureSearchId(feature);\n\t\t\tif (!id.isEmpty()) {\n\t\t\t\tcontroller()->checkHighlightControl(id, button);\n\t\t\t}\n\t\t}\n\t\tcontroller()->checkHighlightControl(\n\t\t\tu\"business/sponsored\"_q,\n\t\t\t_state->sponsoredButton);\n\t});\n}\n\nbase::weak_qptr<Ui::RpWidget> Business::createPinnedToBottom(\n\t\tnot_null<Ui::RpWidget*> parent) {\n\tconst auto content = Ui::CreateChild<Ui::RpWidget>(parent.get());\n\n\tconst auto session = &controller()->session();\n\n\tauto buttonText = _buttonText.value();\n\n\t_subscribe = CreateSubscribeButton({\n\t\tcontroller(),\n\t\tcontent,\n\t\t[] { return u\"business\"_q; },\n\t\tstd::move(buttonText),\n\t\tstd::nullopt,\n\t\t[=, options = session->api().premium().subscriptionOptions()] {\n\t\t\tconst auto value = _radioGroup->current();\n\t\t\treturn (value < options.size() && value >= 0)\n\t\t\t\t? options[value].botUrl\n\t\t\t\t: QString();\n\t\t},\n\t});\n\t{\n\t\tconst auto callback = [=](int value) {\n\t\t\tauto &api = controller()->session().api();\n\t\t\tconst auto options = api.premium().subscriptionOptions();\n\t\t\tif (options.empty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tAssert(value < options.size() && value >= 0);\n\t\t\tauto text = tr::lng_premium_subscribe_button(\n\t\t\t\ttr::now,\n\t\t\t\tlt_cost,\n\t\t\t\toptions[value].costPerMonth);\n\t\t\t_buttonText = std::move(text);\n\t\t};\n\t\t_radioGroup->setChangedCallback(callback);\n\t\tcallback(0);\n\t}\n\n\t_showFinished.events(\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t_subscribe->startGlareAnimation();\n\t}, _subscribe->lifetime());\n\n\tcontent->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto padding = st::settingsPremiumButtonPadding;\n\t\t_subscribe->resizeToWidth(width - padding.left() - padding.right());\n\t}, _subscribe->lifetime());\n\n\trpl::combine(\n\t\t_subscribe->heightValue(),\n\t\tData::AmPremiumValue(session),\n\t\tsession->premiumPossibleValue()\n\t) | rpl::on_next([=](\n\t\t\tint buttonHeight,\n\t\t\tbool premium,\n\t\t\tbool premiumPossible) {\n\t\tconst auto padding = st::settingsPremiumButtonPadding;\n\t\tconst auto finalHeight = !premiumPossible\n\t\t\t? 0\n\t\t\t: !premium\n\t\t\t? (padding.top() + buttonHeight + padding.bottom())\n\t\t\t: 0;\n\t\tcontent->resize(content->width(), finalHeight);\n\t\t_subscribe->moveToLeft(padding.left(), padding.top());\n\t\t_subscribe->setVisible(!premium && premiumPossible);\n\t}, _subscribe->lifetime());\n\n\treturn base::make_weak(not_null<Ui::RpWidget*>{ content });\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = Business::Id(),\n\t.parentId = MainId(),\n\t.title = &tr::lng_business_title,\n\t.icon = &st::menuIconShop,\n}, [](SectionBuilder &builder) {\n\tBuildBusinessSectionContent(builder, nullptr);\n});\n\n} // namespace\n\ntemplate <>\nstruct SectionFactory<Business> : AbstractSectionFactory {\n\tobject_ptr<AbstractSection> create(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\trpl::producer<Container> containerValue\n\t) const final override {\n\t\treturn object_ptr<Business>(parent, controller);\n\t}\n\tbool hasCustomTopBar() const final override {\n\t\treturn true;\n\t}\n\n\t[[nodiscard]] static const std::shared_ptr<SectionFactory> &Instance() {\n\t\tstatic const auto result = std::make_shared<SectionFactory>();\n\t\treturn result;\n\t}\n};\n\nType BusinessId() {\n\treturn Business::Id();\n}\n\nvoid ShowBusiness(not_null<Window::SessionController*> controller) {\n\tif (!controller->session().premiumPossible()) {\n\t\tcontroller->show(Box(PremiumUnavailableBox));\n\t\treturn;\n\t}\n\tcontroller->showSettings(Settings::BusinessId());\n}\n\nstd::vector<PremiumFeature> BusinessFeaturesOrder(\n\t\tnot_null<::Main::Session*> session) {\n\tconst auto mtpOrder = session->appConfig().get<Order>(\n\t\t\"business_promo_order\",\n\t\tFallbackOrder());\n\treturn ranges::views::all(\n\t\tmtpOrder\n\t) | ranges::views::transform([](const QString &s) {\n\t\tif (s == u\"greeting_message\"_q) {\n\t\t\treturn PremiumFeature::GreetingMessage;\n\t\t} else if (s == u\"away_message\"_q) {\n\t\t\treturn PremiumFeature::AwayMessage;\n\t\t} else if (s == u\"quick_replies\"_q) {\n\t\t\treturn PremiumFeature::QuickReplies;\n\t\t} else if (s == u\"business_hours\"_q) {\n\t\t\treturn PremiumFeature::BusinessHours;\n\t\t} else if (s == u\"business_location\"_q) {\n\t\t\treturn PremiumFeature::BusinessLocation;\n\t\t} else if (s == u\"business_links\"_q) {\n\t\t\treturn PremiumFeature::ChatLinks;\n\t\t} else if (s == u\"business_intro\"_q) {\n\t\t\treturn PremiumFeature::ChatIntro;\n\t\t} else if (s == u\"business_bots\"_q) {\n\t\t\treturn PremiumFeature::BusinessBots;\n\t\t} else if (s == u\"folder_tags\"_q) {\n\t\t\treturn PremiumFeature::FilterTags;\n\t\t}\n\t\treturn PremiumFeature::kCount;\n\t}) | ranges::views::filter([](PremiumFeature feature) {\n\t\treturn (feature != PremiumFeature::kCount);\n\t}) | ranges::to_vector;\n}\n\nnamespace Builder {\n\nSectionBuildMethod BusinessSection = kMeta.build;\n\n} // namespace Builder\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_business.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_common.h\"\n#include \"settings/settings_type.h\"\n\nenum class PremiumFeature;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Settings {\n\n[[nodiscard]] Type BusinessId();\n\nvoid ShowBusiness(not_null<Window::SessionController*> controller);\n\n[[nodiscard]] std::vector<PremiumFeature> BusinessFeaturesOrder(\n\tnot_null<::Main::Session*> session);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_calls.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_calls.h\"\n\n#include \"api/api_authorizations.h\"\n#include \"apiwrap.h\"\n#include \"base/timer.h\"\n#include \"calls/calls_call.h\"\n#include \"calls/calls_instance.h\"\n#include \"calls/calls_video_bubble.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"platform/platform_specific.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common_session.h\"\n#include \"tgcalls/VideoCaptureInterface.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/single_choice_box.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/level_meter.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"webrtc/webrtc_audio_input_tester.h\"\n#include \"webrtc/webrtc_create_adm.h\"\n#include \"webrtc/webrtc_environment.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\n\nWebrtc::VideoTrack *AddCameraSubsection(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<Ui::VerticalLayout*> content,\n\tbool saveToSettings);\n\nnamespace {\n\nusing namespace Webrtc;\nusing namespace Builder;\n\n[[nodiscard]] rpl::producer<QString> DeviceNameValue(\n\t\tDeviceType type,\n\t\trpl::producer<QString> id) {\n\treturn std::move(id) | rpl::map([type](const QString &id) {\n\t\treturn Core::App().mediaDevices().devicesValue(\n\t\t\ttype\n\t\t) | rpl::map([id](const std::vector<DeviceInfo> &list) {\n\t\t\tconst auto i = ranges::find(list, id, &DeviceInfo::id);\n\t\t\treturn (i != end(list) && !i->inactive)\n\t\t\t\t? i->name\n\t\t\t\t: tr::lng_settings_call_device_default(tr::now);\n\t\t});\n\t}) | rpl::flatten_latest();\n}\n\nvoid InitPlaybackButton(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> text,\n\t\trpl::producer<QString> resolvedId,\n\t\tFn<void(QString)> set) {\n\tAddButtonWithLabel(\n\t\tcontainer,\n\t\ttr::lng_settings_call_output_device(),\n\t\tPlaybackDeviceNameValue(rpl::duplicate(resolvedId)),\n\t\tst::settingsButtonNoIcon\n\t)->addClickHandler([=] {\n\t\tcontroller->show(ChoosePlaybackDeviceBox(\n\t\t\trpl::duplicate(resolvedId),\n\t\t\t[=](const QString &id) {\n\t\t\t\tset(id);\n\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t}));\n\t});\n}\n\nvoid InitCaptureButton(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> text,\n\t\trpl::producer<QString> resolvedId,\n\t\tFn<void(QString)> set,\n\t\trpl::variable<bool> *testingMicrophone) {\n\tAddButtonWithLabel(\n\t\tcontainer,\n\t\ttr::lng_settings_call_input_device(),\n\t\tCaptureDeviceNameValue(rpl::duplicate(resolvedId)),\n\t\tst::settingsButtonNoIcon\n\t)->addClickHandler([=] {\n\t\tcontroller->show(ChooseCaptureDeviceBox(\n\t\t\trpl::duplicate(resolvedId),\n\t\t\t[=](const QString &id) {\n\t\t\t\tset(id);\n\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t}));\n\t});\n\n\tstruct LevelState {\n\t\tstd::unique_ptr<Webrtc::DeviceResolver> deviceId;\n\t\tstd::unique_ptr<Webrtc::AudioInputTester> tester;\n\t\tbase::Timer timer;\n\t\tUi::Animations::Simple animation;\n\t\tfloat level = 0.;\n\t};\n\tconst auto level = container->add(\n\t\tobject_ptr<Ui::LevelMeter>(\n\t\t\tcontainer,\n\t\t\tst::defaultLevelMeter),\n\t\tst::settingsLevelMeterPadding);\n\tconst auto state = level->lifetime().make_state<LevelState>();\n\tlevel->resize(QSize(0, st::defaultLevelMeter.height));\n\n\tstate->timer.setCallback([=] {\n\t\tconst auto was = state->level;\n\t\tstate->level = state->tester->getAndResetLevel();\n\t\tstate->animation.start([=] {\n\t\t\tlevel->setValue(state->animation.value(state->level));\n\t\t}, was, state->level, kMicTestAnimationDuration);\n\t});\n\ttestingMicrophone->value() | rpl::on_next([=](bool testing) {\n\t\tif (testing) {\n\t\t\tstate->deviceId = std::make_unique<Webrtc::DeviceResolver>(\n\t\t\t\t&Core::App().mediaDevices(),\n\t\t\t\tWebrtc::DeviceType::Capture,\n\t\t\t\trpl::duplicate(resolvedId));\n\t\t\tstate->tester = std::make_unique<AudioInputTester>(\n\t\t\t\tstate->deviceId->value());\n\t\t\tstate->timer.callEach(kMicTestUpdateInterval);\n\t\t} else {\n\t\t\tstate->timer.cancel();\n\t\t\tstate->animation.stop();\n\t\t\tstate->tester = nullptr;\n\t\t\tstate->deviceId = nullptr;\n\t\t}\n\t}, level->lifetime());\n}\n\nvoid BuildOutputSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tif (!controller) {\n\t\treturn;\n\t}\n\tconst auto settings = &Core::App().settings();\n\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"calls/output\"_q,\n\t\t.title = tr::lng_settings_call_section_output(),\n\t\t.keywords = { u\"speakers\"_q, u\"output\"_q, u\"audio\"_q },\n\t});\n\n\tbuilder.add([controller, settings](const WidgetContext &ctx) {\n\t\tInitPlaybackButton(\n\t\t\tcontroller,\n\t\t\tctx.container,\n\t\t\ttr::lng_settings_call_output_device(),\n\t\t\trpl::deferred([=] {\n\t\t\t\treturn DeviceIdOrDefault(settings->playbackDeviceIdValue());\n\t\t\t}),\n\t\t\t[=](const QString &id) { settings->setPlaybackDeviceId(id); });\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"calls/output/device\"_q,\n\t\t\t.title = tr::lng_settings_call_output_device(tr::now),\n\t\t\t.keywords = { u\"speakers\"_q, u\"output\"_q, u\"playback\"_q },\n\t\t};\n\t});\n}\n\nvoid BuildInputSection(\n\t\tSectionBuilder &builder,\n\t\trpl::variable<bool> *testingMicrophone) {\n\tconst auto controller = builder.controller();\n\tif (!controller) {\n\t\treturn;\n\t}\n\tconst auto settings = &Core::App().settings();\n\n\tbuilder.addSkip();\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"calls/input\"_q,\n\t\t.title = tr::lng_settings_call_section_input(),\n\t\t.keywords = { u\"microphone\"_q, u\"input\"_q, u\"audio\"_q },\n\t});\n\n\tbuilder.add([controller, settings, testingMicrophone](const WidgetContext &ctx) {\n\t\tInitCaptureButton(\n\t\t\tcontroller,\n\t\t\tctx.container,\n\t\t\ttr::lng_settings_call_input_device(),\n\t\t\trpl::deferred([=] {\n\t\t\t\treturn DeviceIdOrDefault(settings->captureDeviceIdValue());\n\t\t\t}),\n\t\t\t[=](const QString &id) { settings->setCaptureDeviceId(id); },\n\t\t\ttestingMicrophone);\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"calls/input/device\"_q,\n\t\t\t.title = tr::lng_settings_call_input_device(tr::now),\n\t\t\t.keywords = { u\"microphone\"_q, u\"input\"_q, u\"capture\"_q },\n\t\t};\n\t});\n}\n\nvoid BuildCallDevicesSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tif (!controller) {\n\t\treturn;\n\t}\n\tconst auto settings = &Core::App().settings();\n\n\tbuilder.addSkip();\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"calls/devices\"_q,\n\t\t.title = tr::lng_settings_devices_calls(),\n\t\t.keywords = { u\"calls\"_q, u\"devices\"_q, u\"same\"_q },\n\t});\n\n\tconst auto orDefault = [](const QString &value) {\n\t\treturn value.isEmpty() ? kDefaultDeviceId : value;\n\t};\n\n\tconst auto same = builder.addButton({\n\t\t.id = u\"calls/same-devices\"_q,\n\t\t.title = tr::lng_settings_devices_calls_same(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.toggled = rpl::combine(\n\t\t\tsettings->callPlaybackDeviceIdValue(),\n\t\t\tsettings->callCaptureDeviceIdValue()\n\t\t) | rpl::map([](const QString &playback, const QString &capture) {\n\t\t\treturn playback.isEmpty() && capture.isEmpty();\n\t\t}),\n\t\t.keywords = { u\"same\"_q, u\"separate\"_q, u\"devices\"_q },\n\t});\n\n\tif (same) {\n\t\tsame->toggledValue() | rpl::filter([=](bool toggled) {\n\t\t\tconst auto empty = settings->callPlaybackDeviceId().isEmpty()\n\t\t\t\t&& settings->callCaptureDeviceId().isEmpty();\n\t\t\treturn (empty != toggled);\n\t\t}) | rpl::on_next([=](bool toggled) {\n\t\t\tif (toggled) {\n\t\t\t\tsettings->setCallPlaybackDeviceId(QString());\n\t\t\t\tsettings->setCallCaptureDeviceId(QString());\n\t\t\t} else {\n\t\t\t\tsettings->setCallPlaybackDeviceId(\n\t\t\t\t\torDefault(settings->playbackDeviceId()));\n\t\t\t\tsettings->setCallCaptureDeviceId(\n\t\t\t\t\torDefault(settings->captureDeviceId()));\n\t\t\t}\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}, same->lifetime());\n\t}\n\n\tbuilder.add([controller, settings, same](const WidgetContext &ctx) {\n\t\tconst auto container = ctx.container.get();\n\t\tconst auto different = container->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\t\tconst auto calls = different->entity();\n\n\t\tInitPlaybackButton(\n\t\t\tcontroller,\n\t\t\tcalls,\n\t\t\ttr::lng_group_call_speakers(),\n\t\t\trpl::deferred([=] {\n\t\t\t\treturn DeviceIdValueWithFallback(\n\t\t\t\t\tsettings->callPlaybackDeviceIdValue(),\n\t\t\t\t\tsettings->playbackDeviceIdValue());\n\t\t\t}),\n\t\t\t[=](const QString &id) { settings->setCallPlaybackDeviceId(id); });\n\n\t\tstruct LevelStateStub {\n\t\t\tstd::unique_ptr<Webrtc::DeviceResolver> deviceId;\n\t\t\tstd::unique_ptr<Webrtc::AudioInputTester> tester;\n\t\t\tbase::Timer timer;\n\t\t\tUi::Animations::Simple animation;\n\t\t\tfloat level = 0.;\n\t\t};\n\t\tconst auto captureId = rpl::deferred([=] {\n\t\t\treturn DeviceIdValueWithFallback(\n\t\t\t\tsettings->callCaptureDeviceIdValue(),\n\t\t\t\tsettings->captureDeviceIdValue());\n\t\t});\n\t\tAddButtonWithLabel(\n\t\t\tcalls,\n\t\t\ttr::lng_group_call_microphone(),\n\t\t\tCaptureDeviceNameValue(rpl::duplicate(captureId)),\n\t\t\tst::settingsButtonNoIcon\n\t\t)->addClickHandler([=] {\n\t\t\tcontroller->show(ChooseCaptureDeviceBox(\n\t\t\t\trpl::duplicate(captureId),\n\t\t\t\t[=](const QString &id) {\n\t\t\t\t\tsettings->setCallCaptureDeviceId(id);\n\t\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t\t}));\n\t\t});\n\n\t\tif (same) {\n\t\t\tdifferent->toggleOn(\n\t\t\t\tsame->toggledValue() | rpl::map(!rpl::mappers::_1));\n\t\t}\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"calls/call-speakers\"_q,\n\t\t\t.title = tr::lng_group_call_speakers(tr::now),\n\t\t\t.keywords = { u\"speakers\"_q, u\"calls\"_q },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"calls/call-microphone\"_q,\n\t\t\t.title = tr::lng_group_call_microphone(tr::now),\n\t\t\t.keywords = { u\"microphone\"_q, u\"calls\"_q },\n\t\t};\n\t});\n}\n\nvoid BuildCameraSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tif (!controller) {\n\t\treturn;\n\t}\n\n\tif (Core::App().mediaDevices().defaultId(\n\t\t\tWebrtc::DeviceType::Camera).isEmpty()) {\n\t\treturn;\n\t}\n\n\tbuilder.addSkip();\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"calls/camera\"_q,\n\t\t.title = tr::lng_settings_call_camera(),\n\t\t.keywords = { u\"camera\"_q, u\"video\"_q, u\"webcam\"_q },\n\t});\n\n\tbuilder.add([controller](const WidgetContext &ctx) {\n\t\tAddCameraSubsection(controller->uiShow(), ctx.container, true);\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"calls/camera/device\"_q,\n\t\t\t.title = tr::lng_settings_call_input_device(tr::now),\n\t\t\t.keywords = { u\"camera\"_q, u\"video\"_q, u\"webcam\"_q },\n\t\t};\n\t});\n}\n\nvoid BuildOtherSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto session = builder.session();\n\n\tbuilder.addSkip();\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"calls/other\"_q,\n\t\t.title = tr::lng_settings_call_section_other(),\n\t\t.keywords = { u\"calls\"_q, u\"accept\"_q, u\"system\"_q },\n\t});\n\n\tconst auto api = &session->api();\n\tconst auto authorizations = &api->authorizations();\n\tauthorizations->reload();\n\n\tconst auto acceptCalls = builder.addButton({\n\t\t.id = u\"calls/accept\"_q,\n\t\t.title = tr::lng_settings_call_accept_calls(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.toggled = authorizations->callsDisabledHereValue()\n\t\t\t| rpl::map(!rpl::mappers::_1),\n\t\t.keywords = { u\"accept\"_q, u\"receive\"_q, u\"incoming\"_q },\n\t\t.highlight = { .rippleShape = true },\n\t});\n\n\tif (acceptCalls) {\n\t\tacceptCalls->toggledChanges(\n\t\t) | rpl::filter([=](bool value) {\n\t\t\treturn (value == authorizations->callsDisabledHere());\n\t\t}) | rpl::on_next([=](bool value) {\n\t\t\tauthorizations->toggleCallsDisabledHere(!value);\n\t\t}, acceptCalls->lifetime());\n\t}\n\n\tbuilder.addButton({\n\t\t.id = u\"calls/system-prefs\"_q,\n\t\t.title = tr::lng_settings_call_open_system_prefs(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.onClick = [controller] {\n\t\t\tusing namespace ::Platform;\n\t\t\tconst auto opened = OpenSystemSettings(SystemSettingsType::Audio);\n\t\t\tif (!opened) {\n\t\t\t\tcontroller->show(\n\t\t\t\t\tUi::MakeInformBox(tr::lng_linux_no_audio_prefs()));\n\t\t\t}\n\t\t},\n\t\t.keywords = { u\"system\"_q, u\"preferences\"_q, u\"audio\"_q },\n\t\t.highlight = { .rippleShape = true },\n\t});\n\n\tbuilder.addSkip();\n}\n\nvoid BuildCallsSectionContent(SectionBuilder &builder) {\n\tauto *testingMicrophone = builder.container()\n\t\t? builder.container()->lifetime().make_state<rpl::variable<bool>>()\n\t\t: nullptr;\n\n\tBuildOutputSection(builder);\n\tBuildInputSection(builder, testingMicrophone);\n\tBuildCallDevicesSection(builder);\n\tBuildCameraSection(builder);\n\tBuildOtherSection(builder);\n}\n\nvoid ChooseMediaDeviceBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\trpl::producer<QString> title,\n\t\trpl::producer<std::vector<DeviceInfo>> devicesValue,\n\t\trpl::producer<QString> currentId,\n\t\tFn<void(QString id)> chosen,\n\t\tconst style::Checkbox *st,\n\t\tconst style::Radio *radioSt) {\n\tbox->setTitle(std::move(title));\n\tbox->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });\n\tconst auto layout = box->verticalLayout();\n\tconst auto skip = st::boxOptionListPadding.top()\n\t\t+ st::defaultBoxCheckbox.margin.top();\n\tlayout->add(object_ptr<Ui::FixedHeightWidget>(layout, skip));\n\n\tif (!st) {\n\t\tst = &st::defaultBoxCheckbox;\n\t}\n\tif (!radioSt) {\n\t\tradioSt = &st::defaultRadio;\n\t}\n\n\tstruct State {\n\t\tstd::vector<DeviceInfo> list;\n\t\tbase::flat_map<int, QString> ids;\n\t\trpl::variable<QString> currentId;\n\t\tQString currentName;\n\t\tbool ignoreValueChange = false;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tstate->currentId = std::move(currentId);\n\n\tconst auto choose = [=](const QString &id) {\n\t\tconst auto weak = base::make_weak(box);\n\t\tchosen(id);\n\t\tif (weak) {\n\t\t\tbox->closeBox();\n\t\t}\n\t};\n\n\tconst auto group = std::make_shared<Ui::RadiobuttonGroup>();\n\tconst auto fake = std::make_shared<Ui::RadiobuttonGroup>(0);\n\tconst auto buttons = layout->add(object_ptr<Ui::VerticalLayout>(layout));\n\tconst auto other = layout->add(object_ptr<Ui::VerticalLayout>(layout));\n\tconst auto margins = QMargins(\n\t\tst::boxPadding.left() + st::boxOptionListPadding.left(),\n\t\t0,\n\t\tst::boxPadding.right(),\n\t\tst::boxOptionListSkip);\n\tconst auto def = buttons->add(\n\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\tbuttons,\n\t\t\tgroup,\n\t\t\t0,\n\t\t\ttr::lng_settings_call_device_default(tr::now),\n\t\t\t*st,\n\t\t\t*radioSt),\n\t\tmargins);\n\tdef->clicks(\n\t) | rpl::filter([=] {\n\t\treturn !group->value();\n\t}) | rpl::on_next([=] {\n\t\tchoose(kDefaultDeviceId);\n\t}, def->lifetime());\n\tconst auto showUnavailable = [=](QString text) {\n\t\tAddSkip(other);\n\t\tAddSubsectionTitle(other, tr::lng_settings_devices_inactive());\n\t\tconst auto &radio = *radioSt;\n\t\tconst auto button = other->add(\n\t\t\tobject_ptr<Ui::Radiobutton>(other, fake, 0, text, *st, radio),\n\t\t\tmargins);\n\t\tbutton->show();\n\n\t\tbutton->setDisabled(true);\n\t\tbutton->finishAnimating();\n\t\tbutton->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\twhile (other->count() > 3) {\n\t\t\tdelete other->widgetAt(0);\n\t\t}\n\t\tif (const auto width = box->width()) {\n\t\t\tother->resizeToWidth(width);\n\t\t}\n\t};\n\tconst auto hideUnavailable = [=] {\n\t\twhile (other->count() > 0) {\n\t\t\tdelete other->widgetAt(0);\n\t\t}\n\t};\n\n\tconst auto selectCurrent = [=](QString current) {\n\t\tstate->ignoreValueChange = true;\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\tstate->ignoreValueChange = false;\n\t\t});\n\t\tif (current.isEmpty() || current == kDefaultDeviceId) {\n\t\t\tgroup->setValue(0);\n\t\t\thideUnavailable();\n\t\t} else {\n\t\t\tauto found = false;\n\t\t\tfor (const auto &[index, id] : state->ids) {\n\t\t\t\tif (id == current) {\n\t\t\t\t\tgroup->setValue(index);\n\t\t\t\t\tfound = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (found) {\n\t\t\t\thideUnavailable();\n\t\t\t} else {\n\t\t\t\tgroup->setValue(0);\n\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\tstate->list,\n\t\t\t\t\tcurrent,\n\t\t\t\t\t&DeviceInfo::id);\n\t\t\t\tif (i != end(state->list)) {\n\t\t\t\t\tshowUnavailable(i->name);\n\t\t\t\t} else {\n\t\t\t\t\thideUnavailable();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\tstd::move(\n\t\tdevicesValue\n\t) | rpl::on_next([=](std::vector<DeviceInfo> &&list) {\n\t\tauto count = buttons->count();\n\t\tauto index = 1;\n\t\tstate->ids.clear();\n\t\tstate->list = std::move(list);\n\n\t\tstate->ignoreValueChange = true;\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\tstate->ignoreValueChange = false;\n\t\t});\n\n\t\tconst auto current = state->currentId.current();\n\t\tfor (const auto &info : state->list) {\n\t\t\tconst auto id = info.id;\n\t\t\tif (info.inactive) {\n\t\t\t\tcontinue;\n\t\t\t} else if (current == id) {\n\t\t\t\tgroup->setValue(index);\n\t\t\t}\n\t\t\tconst auto button = buttons->insert(\n\t\t\t\tindex,\n\t\t\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\t\t\tbuttons,\n\t\t\t\t\tgroup,\n\t\t\t\t\tindex,\n\t\t\t\t\tinfo.name,\n\t\t\t\t\t*st,\n\t\t\t\t\t*radioSt),\n\t\t\t\tmargins);\n\t\t\tbutton->show();\n\t\t\tbutton->finishAnimating();\n\t\t\tbutton->clicks(\n\t\t\t) | rpl::filter([=] {\n\t\t\t\treturn (group->current() == index);\n\t\t\t}) | rpl::on_next([=] {\n\t\t\t\tchoose(id);\n\t\t\t}, button->lifetime());\n\n\t\t\tstate->ids.emplace(index, id);\n\t\t\tif (index < count) {\n\t\t\t\tdelete buttons->widgetAt(index + 1);\n\t\t\t}\n\t\t\t++index;\n\t\t}\n\t\twhile (index < count) {\n\t\t\tdelete buttons->widgetAt(index);\n\t\t\t--count;\n\t\t}\n\t\tif (const auto width = box->width()) {\n\t\t\tbuttons->resizeToWidth(width);\n\t\t}\n\t\tselectCurrent(current);\n\t}, box->lifetime());\n\n\tstate->currentId.changes(\n\t) | rpl::on_next(selectCurrent, box->lifetime());\n\n\tdef->finishAnimating();\n\n\tgroup->setChangedCallback([=](int value) {\n\t\tif (state->ignoreValueChange) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto i = state->ids.find(value);\n\t\tchoose((i != end(state->ids)) ? i->second : kDefaultDeviceId);\n\t});\n}\n\nclass Calls : public Section<Calls> {\npublic:\n\tCalls(QWidget *parent, not_null<Window::SessionController*> controller);\n\t~Calls();\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\tvoid sectionSaveChanges(FnMut<void()> done) override;\n\nprivate:\n\tvoid setupContent();\n\tvoid requestPermissionAndStartTestingMicrophone();\n\n\trpl::variable<bool> _testingMicrophone;\n\n};\n\nCalls::Calls(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tcontroller->session().api().authorizations().reload();\n\n\tsetupContent();\n\trequestPermissionAndStartTestingMicrophone();\n}\n\nCalls::~Calls() = default;\n\nrpl::producer<QString> Calls::title() {\n\treturn tr::lng_settings_section_devices();\n}\n\nvoid Calls::sectionSaveChanges(FnMut<void()> done) {\n\t_testingMicrophone = false;\n\tdone();\n}\n\nvoid Calls::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tconst SectionBuildMethod buildMethod = [](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tconst auto isPaused = Window::PausedIn(\n\t\t\tcontroller,\n\t\t\tWindow::GifPauseReason::Layer);\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = isPaused,\n\t\t});\n\t\tBuildCallsSectionContent(builder);\n\t};\n\n\tbuild(content, buildMethod);\n\tUi::ResizeFitChild(this, content);\n}\n\nvoid Calls::requestPermissionAndStartTestingMicrophone() {\n\tusing PermissionType = ::Platform::PermissionType;\n\tusing PermissionStatus = ::Platform::PermissionStatus;\n\tconst auto status = GetPermissionStatus(\n\t\tPermissionType::Microphone);\n\tif (status == PermissionStatus::Granted) {\n\t\t_testingMicrophone = true;\n\t} else if (status == PermissionStatus::CanRequest) {\n\t\tconst auto startTestingChecked = crl::guard(this, [=](\n\t\t\t\tPermissionStatus status) {\n\t\t\tif (status == PermissionStatus::Granted) {\n\t\t\t\tcrl::on_main(crl::guard(this, [=] {\n\t\t\t\t\t_testingMicrophone = true;\n\t\t\t\t}));\n\t\t\t}\n\t\t});\n\t\tRequestPermission(\n\t\t\tPermissionType::Microphone,\n\t\t\tstartTestingChecked);\n\t} else {\n\t\tconst auto showSystemSettings = [controller = controller()] {\n\t\t\tOpenSystemSettingsForPermission(\n\t\t\t\tPermissionType::Microphone);\n\t\t\tcontroller->hideLayer();\n\t\t};\n\t\tcontroller()->show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_no_mic_permission(),\n\t\t\t.confirmed = showSystemSettings,\n\t\t\t.confirmText = tr::lng_menu_settings(),\n\t\t}));\n\t}\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = Calls::Id(),\n\t.parentId = MainId(),\n\t.title = &tr::lng_settings_section_devices,\n\t.icon = &st::menuIconUnmute,\n}, [](SectionBuilder &builder) {\n\tBuildCallsSectionContent(builder);\n});\n\n} // namespace\n\nType CallsId() {\n\treturn Calls::Id();\n}\n\nWebrtc::VideoTrack *AddCameraSubsection(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tbool saveToSettings) {\n\tauto &lifetime = content->lifetime();\n\n\tconst auto hasCall = (Core::App().calls().currentCall() != nullptr);\n\n\tauto capturerOwner = lifetime.make_state<\n\t\tstd::shared_ptr<tgcalls::VideoCaptureInterface>\n\t>();\n\n\tconst auto track = lifetime.make_state<VideoTrack>(\n\t\t(hasCall\n\t\t\t? VideoState::Inactive\n\t\t\t: VideoState::Active));\n\n\tconst auto deviceId = lifetime.make_state<rpl::variable<QString>>(\n\t\tCore::App().settings().cameraDeviceId());\n\tauto resolvedId = rpl::deferred([=] {\n\t\treturn DeviceIdOrDefault(deviceId->value());\n\t});\n\tAddButtonWithLabel(\n\t\tcontent,\n\t\ttr::lng_settings_call_input_device(),\n\t\tCameraDeviceNameValue(rpl::duplicate(resolvedId)),\n\t\tst::settingsButtonNoIcon\n\t)->addClickHandler([=] {\n\t\tshow->show(ChooseCameraDeviceBox(\n\t\t\trpl::duplicate(resolvedId),\n\t\t\t[=](const QString &id) {\n\t\t\t\t*deviceId = id;\n\t\t\t\tif (saveToSettings) {\n\t\t\t\t\tCore::App().settings().setCameraDeviceId(id);\n\t\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t\t}\n\t\t\t\tif (*capturerOwner) {\n\t\t\t\t\t(*capturerOwner)->switchToDevice(\n\t\t\t\t\t\tid.toStdString(),\n\t\t\t\t\t\tfalse);\n\t\t\t\t}\n\t\t}));\n\t});\n\tconst auto bubbleWrap = content->add(object_ptr<Ui::RpWidget>(content));\n\tconst auto bubble = lifetime.make_state<::Calls::VideoBubble>(\n\t\tbubbleWrap,\n\t\ttrack);\n\tconst auto padding = st::settingsButtonNoIcon.padding.left();\n\tconst auto top = st::boxRoundShadow.extend.top();\n\tconst auto bottom = st::boxRoundShadow.extend.bottom();\n\n\tauto frameSize = track->renderNextFrame(\n\t) | rpl::map([=] {\n\t\treturn track->frameSize();\n\t}) | rpl::filter([=](QSize size) {\n\t\treturn !size.isEmpty()\n\t\t\t&& !Core::App().calls().currentCall()\n\t\t\t&& !Core::App().calls().currentGroupCall();\n\t});\n\tauto bubbleWidth = bubbleWrap->widthValue(\n\t) | rpl::filter([=](int width) {\n\t\treturn width > 2 * padding + 1;\n\t});\n\trpl::combine(\n\t\tstd::move(bubbleWidth),\n\t\tstd::move(frameSize)\n\t) | rpl::on_next([=](int width, QSize frame) {\n\t\tconst auto useWidth = (width - 2 * padding);\n\t\tconst auto useHeight = std::min(\n\t\t\t((useWidth * frame.height()) / frame.width()),\n\t\t\t(useWidth * 480) / 640);\n\t\tbubbleWrap->resize(width, top + useHeight + bottom);\n\t\tbubble->updateGeometry(\n\t\t\t::Calls::VideoBubble::DragMode::None,\n\t\t\tQRect(padding, top, useWidth, useHeight));\n\t\tbubbleWrap->update();\n\t}, bubbleWrap->lifetime());\n\n\tusing namespace rpl::mappers;\n\tconst auto checkCapturer = [=] {\n\t\tif (*capturerOwner\n\t\t\t|| Core::App().calls().currentCall()\n\t\t\t|| Core::App().calls().currentGroupCall()) {\n\t\t\treturn;\n\t\t}\n\t\t*capturerOwner = Core::App().calls().getVideoCapture(\n\t\t\tCore::App().settings().cameraDeviceId(),\n\t\t\tfalse);\n\t\t(*capturerOwner)->setPreferredAspectRatio(0.);\n\t\ttrack->setState(VideoState::Active);\n\t\t(*capturerOwner)->setState(tgcalls::VideoState::Active);\n\t\t(*capturerOwner)->setOutput(track->sink());\n\t};\n\trpl::combine(\n\t\tCore::App().calls().currentCallValue(),\n\t\tCore::App().calls().currentGroupCallValue(),\n\t\t_1 || _2\n\t) | rpl::on_next([=](bool has) {\n\t\tif (has) {\n\t\t\ttrack->setState(VideoState::Inactive);\n\t\t\tbubbleWrap->resize(bubbleWrap->width(), 0);\n\t\t\t*capturerOwner = nullptr;\n\t\t} else {\n\t\t\tcrl::on_main(content, checkCapturer);\n\t\t}\n\t}, lifetime);\n\n\treturn track;\n}\n\nrpl::producer<QString> PlaybackDeviceNameValue(rpl::producer<QString> id) {\n\treturn DeviceNameValue(DeviceType::Playback, std::move(id));\n}\n\nrpl::producer<QString> CaptureDeviceNameValue(rpl::producer<QString> id) {\n\treturn DeviceNameValue(DeviceType::Capture, std::move(id));\n}\n\nrpl::producer<QString> CameraDeviceNameValue(\n\t\trpl::producer<QString> id) {\n\treturn DeviceNameValue(DeviceType::Camera, std::move(id));\n}\n\nobject_ptr<Ui::GenericBox> ChoosePlaybackDeviceBox(\n\t\trpl::producer<QString> currentId,\n\t\tFn<void(QString id)> chosen,\n\t\tconst style::Checkbox *st,\n\t\tconst style::Radio *radioSt) {\n\treturn Box(\n\t\tChooseMediaDeviceBox,\n\t\ttr::lng_settings_call_output_device(),\n\t\tCore::App().mediaDevices().devicesValue(DeviceType::Playback),\n\t\tstd::move(currentId),\n\t\tstd::move(chosen),\n\t\tst,\n\t\tradioSt);\n}\n\nobject_ptr<Ui::GenericBox> ChooseCaptureDeviceBox(\n\t\trpl::producer<QString> currentId,\n\t\tFn<void(QString id)> chosen,\n\t\tconst style::Checkbox *st,\n\t\tconst style::Radio *radioSt) {\n\treturn Box(\n\t\tChooseMediaDeviceBox,\n\t\ttr::lng_settings_call_input_device(),\n\t\tCore::App().mediaDevices().devicesValue(DeviceType::Capture),\n\t\tstd::move(currentId),\n\t\tstd::move(chosen),\n\t\tst,\n\t\tradioSt);\n}\n\nobject_ptr<Ui::GenericBox> ChooseCameraDeviceBox(\n\t\trpl::producer<QString> currentId,\n\t\tFn<void(QString id)> chosen,\n\t\tconst style::Checkbox *st,\n\t\tconst style::Radio *radioSt) {\n\treturn Box(\n\t\tChooseMediaDeviceBox,\n\t\ttr::lng_settings_call_camera(),\n\t\tCore::App().mediaDevices().devicesValue(DeviceType::Camera),\n\t\tstd::move(currentId),\n\t\tstd::move(chosen),\n\t\tst,\n\t\tradioSt);\n}\n\nnamespace Builder {\n\nSectionBuildMethod CallsSection = kMeta.build;\n\n} // namespace Builder\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_calls.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace style {\nstruct Checkbox;\nstruct Radio;\n} // namespace style\n\nnamespace Webrtc {\nclass VideoTrack;\n} // namespace Webrtc\n\nnamespace Ui {\nclass GenericBox;\nclass Show;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Settings {\n\n[[nodiscard]] Type CallsId();\n\nWebrtc::VideoTrack *AddCameraSubsection(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<Ui::VerticalLayout*> content,\n\tbool saveToSettings);\n\ninline constexpr auto kMicTestUpdateInterval = crl::time(100);\ninline constexpr auto kMicTestAnimationDuration = crl::time(200);\n\n[[nodiscard]] rpl::producer<QString> PlaybackDeviceNameValue(\n\trpl::producer<QString> id);\n[[nodiscard]] rpl::producer<QString> CaptureDeviceNameValue(\n\trpl::producer<QString> id);\n[[nodiscard]] rpl::producer<QString> CameraDeviceNameValue(\n\trpl::producer<QString> id);\n[[nodiscard]] object_ptr<Ui::GenericBox> ChoosePlaybackDeviceBox(\n\trpl::producer<QString> currentId,\n\tFn<void(QString id)> chosen,\n\tconst style::Checkbox *st = nullptr,\n\tconst style::Radio *radioSt = nullptr);\n[[nodiscard]] object_ptr<Ui::GenericBox> ChooseCaptureDeviceBox(\n\trpl::producer<QString> currentId,\n\tFn<void(QString id)> chosen,\n\tconst style::Checkbox *st = nullptr,\n\tconst style::Radio *radioSt = nullptr);\n[[nodiscard]] object_ptr<Ui::GenericBox> ChooseCameraDeviceBox(\n\trpl::producer<QString> currentId,\n\tFn<void(QString id)> chosen,\n\tconst style::Checkbox *st = nullptr,\n\tconst style::Radio *radioSt = nullptr);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_chat.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_chat.h\"\n\n#include \"settings/settings_common_session.h\"\n\n#include \"base/timer_rpl.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/sections/settings_advanced.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"settings/sections/settings_privacy_security.h\"\n#include \"settings/settings_experimental.h\"\n#include \"settings/sections/settings_shortcuts.h\"\n#include \"boxes/abstract_box.h\"\n#include \"boxes/peers/edit_peer_color_box.h\"\n#include \"boxes/connection_box.h\"\n#include \"boxes/auto_download_box.h\"\n#include \"boxes/reactions_settings_box.h\"\n#include \"boxes/stickers_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/background_box.h\"\n#include \"boxes/background_preview_box.h\"\n#include \"boxes/download_path_box.h\"\n#include \"boxes/local_storage_box.h\"\n#include \"dialogs/ui/dialogs_quick_action_context.h\"\n#include \"dialogs/dialogs_quick_action.h\"\n#include \"ui/boxes/choose_font_box.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/color_editor.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/chat/attach/attach_extensions.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/style/style_palette_colorizer.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/image/image.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"history/view/history_view_quick_action.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"export/export_manager.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/themes/window_themes_embedded.h\"\n#include \"window/themes/window_theme_editor_box.h\"\n#include \"window/themes/window_themes_cloud_list.h\"\n#include \"window/window_adaptive.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"info/downloads/info_downloads_widget.h\"\n#include \"info/info_memento.h\"\n#include \"storage/localstorage.h\"\n#include \"core/file_utilities.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"data/data_session.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_user.h\"\n#include \"chat_helpers/emoji_sets_manager.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/call_delayed.h\"\n#include \"support/support_common.h\"\n#include \"support/support_templates.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"mainwidget.h\"\n#include \"styles/style_chat_helpers.h\" // stickersRemove\n#include \"styles/style_settings.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_dialogs.h\"\n\n#include <QAction>\n\nnamespace Settings {\nnamespace {\n\nusing namespace Builder;\n\nconst auto kSchemesList = Window::Theme::EmbeddedThemes();\nconstexpr auto kCustomColorButtonParts = 7;\n\n[[nodiscard]] bool IsSystemAccentColorSupported() {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)\n\treturn true;\n#else\n\treturn !Platform::IsWindows() || !Platform::IsWindows8OrGreater();\n#endif\n}\n\nclass ColorsPalette final {\npublic:\n\tusing Type = Window::Theme::EmbeddedType;\n\tusing Scheme = Window::Theme::EmbeddedScheme;\n\n\texplicit ColorsPalette(not_null<Ui::VerticalLayout*> container);\n\n\tvoid show(Type type);\n\n\t[[nodiscard]] Ui::RpWidget *editButton() const;\n\trpl::producer<QColor> selected() const;\n\nprivate:\n\tclass Button {\n\tpublic:\n\t\tButton(\n\t\t\tnot_null<QWidget*> parent,\n\t\t\tstd::vector<QColor> &&colors,\n\t\t\tbool selected);\n\n\t\tvoid moveToLeft(int x, int y);\n\t\tvoid update(std::vector<QColor> &&colors, bool selected);\n\t\trpl::producer<> clicks() const;\n\t\tbool selected() const;\n\t\tQColor color() const;\n\t\tUi::RpWidget *widget() const;\n\n\tprivate:\n\t\tvoid paint();\n\n\t\tUi::AbstractButton _widget;\n\t\tstd::vector<QColor> _colors;\n\t\tUi::Animations::Simple _selectedAnimation;\n\t\tbool _selected = false;\n\n\t};\n\n\tvoid show(\n\t\tnot_null<const Scheme*> scheme,\n\t\tstd::vector<QColor> &&colors,\n\t\tint selected);\n\tvoid selectCustom(not_null<const Scheme*> scheme);\n\tvoid updateInnerGeometry();\n\n\tnot_null<Ui::SlideWrap<>*> _outer;\n\tstd::vector<std::unique_ptr<Button>> _buttons;\n\n\trpl::event_stream<QColor> _selected;\n\n};\n\nvoid PaintCustomButton(QPainter &p, const std::vector<QColor> &colors) {\n\tExpects(colors.size() >= kCustomColorButtonParts);\n\n\tp.setPen(Qt::NoPen);\n\n\tconst auto size = st::settingsAccentColorSize;\n\tconst auto smallSize = size / 8.;\n\tconst auto drawAround = [&](QPointF center, int index) {\n\t\tconst auto where = QPointF{\n\t\t\tsize * (1. + center.x()) / 2,\n\t\t\tsize * (1. + center.y()) / 2\n\t\t};\n\t\tp.setBrush(colors[index]);\n\t\tp.drawEllipse(\n\t\t\twhere.x() - smallSize,\n\t\t\twhere.y() - smallSize,\n\t\t\t2 * smallSize,\n\t\t\t2 * smallSize);\n\t};\n\tdrawAround(QPointF(), 0);\n\tfor (auto i = 0; i != 6; ++i) {\n\t\tconst auto angle = i * M_PI / 3.;\n\t\tconst auto point = QPointF{ cos(angle), sin(angle) };\n\t\tconst auto adjusted = point * (1. - (2 * smallSize / size));\n\t\tdrawAround(adjusted, i + 1);\n\t}\n\n}\n\nColorsPalette::Button::Button(\n\tnot_null<QWidget*> parent,\n\tstd::vector<QColor> &&colors,\n\tbool selected)\n: _widget(parent.get())\n, _colors(std::move(colors))\n, _selected(selected) {\n\t_widget.show();\n\t_widget.resize(st::settingsAccentColorSize, st::settingsAccentColorSize);\n\t_widget.paintRequest(\n\t) | rpl::on_next([=] {\n\t\tpaint();\n\t}, _widget.lifetime());\n}\n\nvoid ColorsPalette::Button::moveToLeft(int x, int y) {\n\t_widget.moveToLeft(x, y);\n}\n\nvoid ColorsPalette::Button::update(\n\t\tstd::vector<QColor> &&colors,\n\t\tbool selected) {\n\tif (_colors != colors) {\n\t\t_colors = std::move(colors);\n\t\t_widget.update();\n\t}\n\tif (_selected != selected) {\n\t\t_selected = selected;\n\t\t_selectedAnimation.start(\n\t\t\t[=] { _widget.update(); },\n\t\t\t_selected ? 0. : 1.,\n\t\t\t_selected ? 1. : 0.,\n\t\t\tst::defaultRadio.duration * 2);\n\t}\n}\n\nrpl::producer<> ColorsPalette::Button::clicks() const {\n\treturn _widget.clicks() | rpl::to_empty;\n}\n\nbool ColorsPalette::Button::selected() const {\n\treturn _selected;\n}\n\nQColor ColorsPalette::Button::color() const {\n\tExpects(_colors.size() == 1);\n\n\treturn _colors.front();\n}\n\nUi::RpWidget *ColorsPalette::Button::widget() const {\n\treturn const_cast<Ui::AbstractButton*>(&_widget);\n}\n\nvoid ColorsPalette::Button::paint() {\n\tauto p = QPainter(&_widget);\n\tPainterHighQualityEnabler hq(p);\n\n\tif (_colors.size() == 1) {\n\t\tPaintRoundColorButton(\n\t\t\tp,\n\t\t\tst::settingsAccentColorSize,\n\t\t\t_colors.front(),\n\t\t\t_selectedAnimation.value(_selected ? 1. : 0.));\n\t} else if (_colors.size() >= kCustomColorButtonParts) {\n\t\tPaintCustomButton(p, _colors);\n\t}\n}\n\nColorsPalette::ColorsPalette(not_null<Ui::VerticalLayout*> container)\n: _outer(container->add(\n\tobject_ptr<Ui::SlideWrap<>>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::RpWidget>(container)))) {\n\t_outer->hide(anim::type::instant);\n\n\tconst auto inner = _outer->entity();\n\tinner->widthValue(\n\t) | rpl::on_next([=] {\n\t\tupdateInnerGeometry();\n\t}, inner->lifetime());\n}\n\nUi::RpWidget *ColorsPalette::editButton() const {\n\treturn _buttons.empty() ? nullptr : _buttons.back()->widget();\n}\n\nvoid ColorsPalette::show(Type type) {\n\tconst auto scheme = ranges::find(kSchemesList, type, &Scheme::type);\n\tif (scheme == end(kSchemesList)) {\n\t\t_outer->hide(anim::type::instant);\n\t\treturn;\n\t}\n\tauto list = Window::Theme::DefaultAccentColors(type);\n\tif (list.empty()) {\n\t\t_outer->hide(anim::type::instant);\n\t\treturn;\n\t}\n\tlist.insert(list.begin(), scheme->accentColor);\n\tconst auto &settings = Core::App().settings();\n\tconst auto color = settings.themesAccentColors().get(type);\n\tconst auto current = (settings.systemAccentColorEnabled()\n\t\t? Window::Theme::SystemAccentColor()\n\t\t: std::optional<QColor>()).value_or(\n\t\t\tcolor.value_or(scheme->accentColor));\n\tconst auto i = ranges::find(list, current);\n\tif (i == end(list)) {\n\t\tlist.back() = current;\n\t}\n\tconst auto selected = std::clamp(\n\t\tint(i - begin(list)),\n\t\t0,\n\t\tint(list.size()) - 1);\n\n\t_outer->show(anim::type::instant);\n\n\tshow(&*scheme, std::move(list), selected);\n\n\tconst auto inner = _outer->entity();\n\tinner->resize(_outer->width(), inner->height());\n\tupdateInnerGeometry();\n}\n\nvoid ColorsPalette::show(\n\t\tnot_null<const Scheme*> scheme,\n\t\tstd::vector<QColor> &&colors,\n\t\tint selected) {\n\tExpects(selected >= 0 && selected < colors.size());\n\n\twhile (_buttons.size() > colors.size()) {\n\t\t_buttons.pop_back();\n\t}\n\n\tauto index = 0;\n\tconst auto inner = _outer->entity();\n\tconst auto pushButton = [&](std::vector<QColor> &&colors) {\n\t\tauto result = rpl::producer<>();\n\t\tconst auto chosen = (index == selected);\n\t\tif (_buttons.size() > index) {\n\t\t\t_buttons[index]->update(std::move(colors), chosen);\n\t\t} else {\n\t\t\t_buttons.push_back(std::make_unique<Button>(\n\t\t\t\tinner,\n\t\t\t\tstd::move(colors),\n\t\t\t\tchosen));\n\t\t\tresult = _buttons.back()->clicks();\n\t\t}\n\t\t++index;\n\t\treturn result;\n\t};\n\tfor (const auto &color : colors) {\n\t\tauto clicks = pushButton({ color });\n\t\tif (clicks) {\n\t\t\tstd::move(\n\t\t\t\tclicks\n\t\t\t) | rpl::map([=] {\n\t\t\t\treturn _buttons[index - 1]->color();\n\t\t\t}) | rpl::on_next([=](QColor color) {\n\t\t\t\t_selected.fire_copy(color);\n\t\t\t}, inner->lifetime());\n\t\t}\n\t}\n\n\tauto clicks = pushButton(std::move(colors));\n\tif (clicks) {\n\t\tstd::move(\n\t\t\tclicks\n\t\t) | rpl::on_next([=] {\n\t\t\tselectCustom(scheme);\n\t\t}, inner->lifetime());\n\t}\n}\n\nvoid ColorsPalette::selectCustom(not_null<const Scheme*> scheme) {\n\tconst auto selected = ranges::find(_buttons, true, &Button::selected);\n\tAssert(selected != end(_buttons));\n\n\tconst auto colorizer = Window::Theme::ColorizerFrom(\n\t\t*scheme,\n\t\tscheme->accentColor);\n\tUi::show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto editor = box->addRow(object_ptr<ColorEditor>(\n\t\t\tbox,\n\t\t\tColorEditor::Mode::HSL,\n\t\t\t(*selected)->color()));\n\n\t\tconst auto save = crl::guard(_outer, [=] {\n\t\t\t_selected.fire_copy(editor->color());\n\t\t\tbox->closeBox();\n\t\t});\n\t\teditor->submitRequests(\n\t\t) | rpl::on_next(save, editor->lifetime());\n\t\teditor->setLightnessLimits(\n\t\t\tcolorizer.lightnessMin,\n\t\t\tcolorizer.lightnessMax);\n\n\t\tbox->setFocusCallback([=] {\n\t\t\teditor->setInnerFocus();\n\t\t});\n\t\tbox->addButton(tr::lng_settings_save(), save);\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t\tbox->setTitle(tr::lng_settings_theme_accent_title());\n\t\tbox->setWidth(editor->width());\n\t}));\n}\n\nrpl::producer<QColor> ColorsPalette::selected() const {\n\treturn _selected.events();\n}\n\nvoid ColorsPalette::updateInnerGeometry() {\n\tconst auto count = int(_buttons.size());\n\tif (count < 2) {\n\t\treturn;\n\t}\n\tconst auto inner = _outer->entity();\n\tconst auto size = st::settingsAccentColorSize;\n\tconst auto padding = st::settingsButtonNoIcon.padding;\n\tconst auto width = inner->width() - padding.left() - padding.right();\n\tif (width < size * count) {\n\t\treturn;\n\t}\n\tconst auto skip = (width - size * count) / float64(count - 1);\n\tconst auto y = st::defaultVerticalListSkip * 2;\n\tauto x = float64(padding.left());\n\tfor (const auto &button : _buttons) {\n\t\tbutton->moveToLeft(int(base::SafeRound(x)), y);\n\t\tx += size + skip;\n\t}\n\tinner->resize(inner->width(), y + size + st::defaultVerticalListSkip);\n}\n\nclass BackgroundRow : public Ui::RpWidget {\npublic:\n\tBackgroundRow(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] Ui::LinkButton *chooseFromGallery() const;\n\t[[nodiscard]] Ui::LinkButton *chooseFromFile() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tvoid updateImage();\n\n\tfloat64 radialProgress() const;\n\tbool radialLoading() const;\n\tQRect radialRect() const;\n\tvoid radialStart();\n\tcrl::time radialTimeShift() const;\n\tvoid radialAnimationCallback(crl::time now);\n\n\tconst not_null<Window::SessionController*> _controller;\n\tQPixmap _background;\n\tobject_ptr<Ui::LinkButton> _chooseFromGallery;\n\tobject_ptr<Ui::LinkButton> _chooseFromFile;\n\n\tUi::RadialAnimation _radial;\n\n};\n\nvoid ChooseFromFile(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<QWidget*> parent);\n\nBackgroundRow::BackgroundRow(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _chooseFromGallery(\n\tthis,\n\ttr::lng_settings_bg_from_gallery(tr::now),\n\tst::settingsLink)\n, _chooseFromFile(this, tr::lng_settings_bg_from_file(tr::now), st::settingsLink)\n, _radial([=](crl::time now) { radialAnimationCallback(now); }) {\n\tupdateImage();\n\n\t_chooseFromGallery->addClickHandler([=] {\n\t\tcontroller->show(Box<BackgroundBox>(controller));\n\t});\n\t_chooseFromFile->addClickHandler([=] {\n\t\tChooseFromFile(controller, this);\n\t});\n\n\tusing Update = const Window::Theme::BackgroundUpdate;\n\tWindow::Theme::Background()->updates(\n\t) | rpl::filter([](const Update &update) {\n\t\treturn (update.type == Update::Type::New\n\t\t\t|| update.type == Update::Type::Start\n\t\t\t|| update.type == Update::Type::Changed);\n\t}) | rpl::on_next([=] {\n\t\tupdateImage();\n\t}, lifetime());\n}\n\nvoid BackgroundRow::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto radial = _radial.animating();\n\tconst auto radialOpacity = radial ? _radial.opacity() : 0.;\n\tif (radial) {\n\t\tconst auto backThumb = _controller->content()->newBackgroundThumb();\n\t\tif (!backThumb) {\n\t\t\tp.drawPixmap(0, 0, _background);\n\t\t} else {\n\t\t\tconst auto &pix = backThumb->pix(\n\t\t\t\tst::settingsBackgroundThumb,\n\t\t\t\t{ .options = Images::Option::Blur });\n\t\t\tconst auto factor = style::DevicePixelRatio();\n\t\t\tp.drawPixmap(\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\tst::settingsBackgroundThumb,\n\t\t\t\tst::settingsBackgroundThumb,\n\t\t\t\tpix,\n\t\t\t\t0,\n\t\t\t\t(pix.height() - st::settingsBackgroundThumb * factor) / 2,\n\t\t\t\tst::settingsBackgroundThumb * factor,\n\t\t\t\tst::settingsBackgroundThumb * factor);\n\t\t}\n\n\t\tconst auto outer = radialRect();\n\t\tconst auto inner = QRect(\n\t\t\tQPoint(\n\t\t\t\touter.x() + (outer.width() - st::radialSize.width()) / 2,\n\t\t\t\touter.y() + (outer.height() - st::radialSize.height()) / 2),\n\t\t\tst::radialSize);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setOpacity(radialOpacity);\n\t\tp.setBrush(st::radialBg);\n\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(inner);\n\t\t}\n\n\t\tp.setOpacity(1);\n\t\tconst auto arc = inner.marginsRemoved(QMargins(\n\t\t\tst::radialLine,\n\t\t\tst::radialLine,\n\t\t\tst::radialLine,\n\t\t\tst::radialLine));\n\t\t_radial.draw(p, arc, st::radialLine, st::radialFg);\n\t} else {\n\t\tp.drawPixmap(0, 0, _background);\n\t}\n}\n\nint BackgroundRow::resizeGetHeight(int newWidth) {\n\tauto linkTop = st::settingsFromGalleryTop;\n\tauto linkLeft = st::settingsBackgroundThumb + st::settingsThumbSkip;\n\tauto linkWidth = newWidth - linkLeft;\n\t_chooseFromGallery->resizeToWidth(\n\t\tqMin(linkWidth, _chooseFromGallery->naturalWidth()));\n\t_chooseFromFile->resizeToWidth(\n\t\tqMin(linkWidth, _chooseFromFile->naturalWidth()));\n\t_chooseFromGallery->moveToLeft(linkLeft, linkTop, newWidth);\n\tlinkTop += _chooseFromGallery->height() + st::settingsFromFileTop;\n\t_chooseFromFile->moveToLeft(linkLeft, linkTop, newWidth);\n\treturn st::settingsBackgroundThumb;\n}\n\nUi::LinkButton *BackgroundRow::chooseFromGallery() const {\n\treturn _chooseFromGallery.data();\n}\n\nUi::LinkButton *BackgroundRow::chooseFromFile() const {\n\treturn _chooseFromFile.data();\n}\n\nfloat64 BackgroundRow::radialProgress() const {\n\treturn _controller->content()->chatBackgroundProgress();\n}\n\nbool BackgroundRow::radialLoading() const {\n\tconst auto widget = _controller->content();\n\tif (widget->chatBackgroundLoading()) {\n\t\twidget->checkChatBackground();\n\t\tif (widget->chatBackgroundLoading()) {\n\t\t\treturn true;\n\t\t} else {\n\t\t\tconst_cast<BackgroundRow*>(this)->updateImage();\n\t\t}\n\t}\n\treturn false;\n}\n\nQRect BackgroundRow::radialRect() const {\n\treturn QRect(\n\t\t0,\n\t\t0,\n\t\tst::settingsBackgroundThumb,\n\t\tst::settingsBackgroundThumb);\n}\n\nvoid BackgroundRow::radialStart() {\n\tif (radialLoading() && !_radial.animating()) {\n\t\t_radial.start(radialProgress());\n\t\tif (const auto shift = radialTimeShift()) {\n\t\t\t_radial.update(\n\t\t\t\tradialProgress(),\n\t\t\t\t!radialLoading(),\n\t\t\t\tcrl::now() + shift);\n\t\t}\n\t}\n}\n\ncrl::time BackgroundRow::radialTimeShift() const {\n\treturn st::radialDuration;\n}\n\nvoid BackgroundRow::radialAnimationCallback(crl::time now) {\n\tconst auto updated = _radial.update(\n\t\tradialProgress(),\n\t\t!radialLoading(),\n\t\tnow + radialTimeShift());\n\tif (!anim::Disabled() || updated) {\n\t\trtlupdate(radialRect());\n\t}\n}\n\nvoid BackgroundRow::updateImage() {\n\tconst auto size = st::settingsBackgroundThumb;\n\tconst auto fullsize = size * style::DevicePixelRatio();\n\n\tconst auto &background = *Window::Theme::Background();\n\tconst auto &paper = background.paper();\n\tconst auto &prepared = background.prepared();\n\tconst auto preparePattern = [&] {\n\t\tconst auto paintPattern = [&](QPainter &p, bool inverted) {\n\t\t\tif (prepared.isNull()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto w = prepared.width();\n\t\t\tconst auto h = prepared.height();\n\t\t\tconst auto s = [&] {\n\t\t\t\tconst auto scaledw = w * st::windowMinHeight / h;\n\t\t\t\tconst auto result = (w * size) / scaledw;\n\t\t\t\treturn std::min({ result, w, h });\n\t\t\t}();\n\t\t\tauto small = prepared.copy((w - s) / 2, (h - s) / 2, s, s);\n\t\t\tif (inverted) {\n\t\t\t\tsmall = Ui::InvertPatternImage(std::move(small));\n\t\t\t}\n\t\t\tp.drawImage(QRect(0, 0, fullsize, fullsize), small);\n\t\t};\n\t\treturn Ui::GenerateBackgroundImage(\n\t\t\t{ fullsize, fullsize },\n\t\t\tpaper.backgroundColors(),\n\t\t\tpaper.gradientRotation(),\n\t\t\tpaper.patternOpacity(),\n\t\t\tpaintPattern);\n\t};\n\tconst auto prepareNormal = [&] {\n\t\tauto result = QImage(\n\t\t\tQSize{ fullsize, fullsize },\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\t\tif (const auto color = background.colorForFill()) {\n\t\t\tresult.fill(*color);\n\t\t\treturn result;\n\t\t} else if (prepared.isNull()) {\n\t\t\tresult.fill(Qt::transparent);\n\t\t\treturn result;\n\t\t}\n\t\tauto p = QPainter(&result);\n\t\tPainterHighQualityEnabler hq(p);\n\t\tconst auto w = prepared.width();\n\t\tconst auto h = prepared.height();\n\t\tconst auto s = std::min(w, h);\n\t\tp.drawImage(\n\t\t\tQRect(0, 0, size, size),\n\t\t\tprepared,\n\t\t\tQRect((w - s) / 2, (h - s) / 2, s, s));\n\t\tp.end();\n\t\treturn result;\n\t};\n\tauto back = (paper.isPattern() || !background.gradientForFill().isNull())\n\t\t? preparePattern()\n\t\t: prepareNormal();\n\t_background = Ui::PixmapFromImage(\n\t\tImages::Round(std::move(back), ImageRoundRadius::Small));\n\t_background.setDevicePixelRatio(style::DevicePixelRatio());\n\n\trtlupdate(radialRect());\n\n\tif (radialLoading()) {\n\t\tradialStart();\n\t}\n}\n\nvoid ChooseFromFile(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent) {\n\tauto filters = QStringList(\n\t\tu\"Theme files (*.tdesktop-theme *.tdesktop-palette *\"_q\n\t\t+ Ui::ImageExtensions().join(u\" *\"_q)\n\t\t+ u\")\"_q);\n\tfilters.push_back(FileDialog::AllFilesFilter());\n\tconst auto callback = crl::guard(controller, [=](\n\t\t\tconst FileDialog::OpenResult &result) {\n\t\tif (result.paths.isEmpty() && result.remoteContent.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!result.paths.isEmpty()) {\n\t\t\tconst auto filePath = result.paths.front();\n\t\t\tconst auto hasExtension = [&](QLatin1String extension) {\n\t\t\t\treturn filePath.endsWith(extension, Qt::CaseInsensitive);\n\t\t\t};\n\t\t\tif (hasExtension(qstr(\".tdesktop-theme\"))\n\t\t\t\t|| hasExtension(qstr(\".tdesktop-palette\"))) {\n\t\t\t\tWindow::Theme::Apply(filePath);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tauto image = Images::Read({\n\t\t\t.path = result.paths.isEmpty() ? QString() : result.paths.front(),\n\t\t\t.content = result.remoteContent,\n\t\t\t.forceOpaque = true,\n\t\t}).image;\n\t\tif (image.isNull() || image.width() <= 0 || image.height() <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tauto local = Data::CustomWallPaper();\n\t\tlocal.setLocalImageAsThumbnail(std::make_shared<Image>(\n\t\t\tstd::move(image)));\n\t\tcontroller->show(Box<BackgroundPreviewBox>(controller, local));\n\t});\n\tFileDialog::GetOpenPath(\n\t\tparent.get(),\n\t\ttr::lng_choose_image(tr::now),\n\t\tfilters.join(u\";;\"_q),\n\t\tcrl::guard(parent, callback));\n}\n\nvoid SetupSupportSwitchSettings(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tusing SwitchType = Support::SwitchSettings;\n\tconst auto group = std::make_shared<Ui::RadioenumGroup<SwitchType>>(\n\t\tcontroller->session().settings().supportSwitch());\n\tconst auto add = [&](SwitchType value, const QString &label) {\n\t\tcontainer->add(\n\t\t\tobject_ptr<Ui::Radioenum<SwitchType>>(\n\t\t\t\tcontainer,\n\t\t\t\tgroup,\n\t\t\t\tvalue,\n\t\t\t\tlabel,\n\t\t\t\tst::settingsSendType),\n\t\t\tst::settingsSendTypePadding);\n\t};\n\tadd(SwitchType::None, \"Just send the reply\");\n\tadd(SwitchType::Next, \"Send and switch to next\");\n\tadd(SwitchType::Previous, \"Send and switch to previous\");\n\tgroup->setChangedCallback([=](SwitchType value) {\n\t\tcontroller->session().settings().setSupportSwitch(value);\n\t\tcontroller->session().saveSettingsDelayed();\n\t});\n}\n\nvoid SetupSupportChatsLimitSlice(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tconstexpr auto kDayDuration = 24 * 60 * 60;\n\tstruct Option {\n\t\tint days = 0;\n\t\tQString label;\n\t};\n\tconst auto options = std::vector<Option>{\n\t\t{ 1, \"1 day\" },\n\t\t{ 7, \"1 week\" },\n\t\t{ 30, \"1 month\" },\n\t\t{ 365, \"1 year\" },\n\t\t{ 0, \"All of them\" },\n\t};\n\tconst auto current = controller->session().settings().supportChatsTimeSlice();\n\tconst auto days = current / kDayDuration;\n\tconst auto best = ranges::min_element(\n\t\toptions,\n\t\tstd::less<>(),\n\t\t[&](const Option &option) { return std::abs(option.days - days); });\n\n\tconst auto group = std::make_shared<Ui::RadiobuttonGroup>(best->days);\n\tfor (const auto &option : options) {\n\t\tcontainer->add(\n\t\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\t\tcontainer,\n\t\t\t\tgroup,\n\t\t\t\toption.days,\n\t\t\t\toption.label,\n\t\t\t\tst::settingsSendType),\n\t\t\tst::settingsSendTypePadding);\n\t}\n\tgroup->setChangedCallback([=](int days) {\n\t\tcontroller->session().settings().setSupportChatsTimeSlice(\n\t\t\tdays * kDayDuration);\n\t\tcontroller->session().saveSettingsDelayed();\n\t});\n}\n\nvoid BuildThemeOptionsSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto highlights = builder.highlights();\n\n\tbuilder.add([controller, highlights](const WidgetContext &ctx) {\n\t\tSetupThemeOptions(controller, ctx.container.get(), highlights);\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/themes\"_q,\n\t\t\t.title = tr::lng_settings_themes(tr::now),\n\t\t\t.keywords = { u\"themes\"_q, u\"appearance\"_q, u\"dark\"_q, u\"light\"_q },\n\t\t\t.icon = { &st::menuIconPalette },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/themes-edit\"_q,\n\t\t\t.title = tr::lng_settings_theme_accent_title(tr::now),\n\t\t\t.keywords = { u\"accent\"_q, u\"color\"_q, u\"customize\"_q },\n\t\t};\n\t});\n\n\tif (IsSystemAccentColorSupported()) {\n\t\tbuilder.add(nullptr, [] {\n\t\t\treturn SearchEntry{\n\t\t\t\t.id = u\"chat/themes-system-accent\"_q,\n\t\t\t\t.title = tr::lng_settings_theme_system_accent_color(tr::now),\n\t\t\t\t.keywords = {\n\t\t\t\t\tu\"system\"_q,\n\t\t\t\t\tu\"accent\"_q,\n\t\t\t\t\tu\"color\"_q,\n\t\t\t\t\tu\"theme\"_q,\n\t\t\t\t\tu\"os\"_q,\n\t\t\t\t},\n\t\t\t};\n\t\t});\n\t}\n}\n\nvoid BuildThemeSettingsSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\n\tbuilder.add([controller](const WidgetContext &ctx) {\n\t\tSetupThemeSettings(controller, ctx.container.get(), ctx.highlights);\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/peer-color\"_q,\n\t\t\t.title = tr::lng_settings_theme_settings(tr::now),\n\t\t\t.keywords = { u\"color\"_q, u\"profile\"_q, u\"name\"_q },\n\t\t\t.icon = { &st::menuIconChangeColors },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/auto-night-mode\"_q,\n\t\t\t.title = tr::lng_settings_auto_night_mode(tr::now),\n\t\t\t.keywords = { u\"night\"_q, u\"dark\"_q, u\"auto\"_q, u\"system\"_q },\n\t\t\t.icon = { &st::menuIconNightMode },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/font\"_q,\n\t\t\t.title = tr::lng_settings_font_family(tr::now),\n\t\t\t.keywords = { u\"font\"_q, u\"family\"_q, u\"text\"_q },\n\t\t\t.icon = { &st::menuIconFont },\n\t\t};\n\t});\n}\n\nvoid BuildCloudThemesSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto highlights = builder.highlights();\n\n\tbuilder.add([controller, highlights](const WidgetContext &ctx) {\n\t\tSetupCloudThemes(controller, ctx.container.get(), highlights);\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/cloud-themes\"_q,\n\t\t\t.title = tr::lng_settings_bg_cloud_themes(tr::now),\n\t\t\t.keywords = { u\"cloud\"_q, u\"themes\"_q, u\"online\"_q },\n\t\t};\n\t});\n}\n\nvoid BuildChatBackgroundSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto highlights = builder.highlights();\n\n\tbuilder.add([controller, highlights](const WidgetContext &ctx) {\n\t\tSetupChatBackground(controller, ctx.container.get(), highlights);\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/wallpapers\"_q,\n\t\t\t.title = tr::lng_settings_section_background(tr::now),\n\t\t\t.keywords = { u\"background\"_q, u\"wallpaper\"_q, u\"image\"_q },\n\t\t\t.icon = { &st::menuIconPhoto },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/wallpapers-set\"_q,\n\t\t\t.title = tr::lng_settings_bg_from_gallery(tr::now),\n\t\t\t.keywords = { u\"gallery\"_q, u\"wallpaper\"_q },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/wallpapers-choose-photo\"_q,\n\t\t\t.title = tr::lng_settings_bg_from_file(tr::now),\n\t\t\t.keywords = { u\"file\"_q, u\"photo\"_q, u\"upload\"_q },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/adaptive-layout\"_q,\n\t\t\t.title = tr::lng_settings_adaptive_wide(tr::now),\n\t\t\t.keywords = { u\"adaptive\"_q, u\"wide\"_q, u\"layout\"_q },\n\t\t\t.checkIcon = Core::App().settings().adaptiveForWide()\n\t\t\t\t? SearchEntryCheckIcon::Checked\n\t\t\t\t: SearchEntryCheckIcon::Unchecked,\n\t\t};\n\t});\n}\n\nvoid BuildChatListQuickActionSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\n\tbuilder.add([controller](const WidgetContext &ctx) {\n\t\tSetupChatListQuickAction(controller, ctx.container.get(), ctx.highlights);\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/quick-dialog-action\"_q,\n\t\t\t.title = tr::lng_settings_quick_dialog_action_title(tr::now),\n\t\t\t.keywords = { u\"swipe\"_q, u\"quick\"_q, u\"action\"_q, u\"dialog\"_q },\n\t\t};\n\t});\n}\n\nvoid BuildStickersEmojiSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto highlights = builder.highlights();\n\n\tbuilder.add([controller, highlights](const WidgetContext &ctx) {\n\t\tSetupStickersEmoji(controller, ctx.container.get(), highlights);\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/stickers-emoji\"_q,\n\t\t\t.title = tr::lng_settings_stickers_emoji(tr::now),\n\t\t\t.keywords = { u\"stickers\"_q, u\"emoji\"_q },\n\t\t\t.icon = { &st::menuIconStickers },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/large-emoji\"_q,\n\t\t\t.title = tr::lng_settings_large_emoji(tr::now),\n\t\t\t.keywords = { u\"large\"_q, u\"emoji\"_q, u\"big\"_q },\n\t\t\t.checkIcon = Core::App().settings().largeEmoji()\n\t\t\t\t? SearchEntryCheckIcon::Checked\n\t\t\t\t: SearchEntryCheckIcon::Unchecked,\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/replace-emoji\"_q,\n\t\t\t.title = tr::lng_settings_replace_emojis(tr::now),\n\t\t\t.keywords = { u\"replace\"_q, u\"emoji\"_q, u\"convert\"_q },\n\t\t\t.checkIcon = Core::App().settings().replaceEmoji()\n\t\t\t\t? SearchEntryCheckIcon::Checked\n\t\t\t\t: SearchEntryCheckIcon::Unchecked,\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/suggest-emoji\"_q,\n\t\t\t.title = tr::lng_settings_suggest_emoji(tr::now),\n\t\t\t.keywords = { u\"suggest\"_q, u\"emoji\"_q, u\"autocomplete\"_q },\n\t\t\t.checkIcon = Core::App().settings().suggestEmoji()\n\t\t\t\t? SearchEntryCheckIcon::Checked\n\t\t\t\t: SearchEntryCheckIcon::Unchecked,\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/suggest-animated-emoji\"_q,\n\t\t\t.title = tr::lng_settings_suggest_animated_emoji(tr::now),\n\t\t\t.keywords = { u\"animated\"_q, u\"emoji\"_q, u\"premium\"_q },\n\t\t\t.checkIcon = Core::App().settings().suggestAnimatedEmoji()\n\t\t\t\t? SearchEntryCheckIcon::Checked\n\t\t\t\t: SearchEntryCheckIcon::Unchecked,\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/suggest-by-emoji\"_q,\n\t\t\t.title = tr::lng_settings_suggest_by_emoji(tr::now),\n\t\t\t.keywords = { u\"suggest\"_q, u\"stickers\"_q, u\"emoji\"_q },\n\t\t\t.checkIcon = Core::App().settings().suggestStickersByEmoji()\n\t\t\t\t? SearchEntryCheckIcon::Checked\n\t\t\t\t: SearchEntryCheckIcon::Unchecked,\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/loop-stickers\"_q,\n\t\t\t.title = tr::lng_settings_loop_stickers(tr::now),\n\t\t\t.keywords = { u\"loop\"_q, u\"stickers\"_q, u\"animated\"_q },\n\t\t\t.checkIcon = Core::App().settings().loopAnimatedStickers()\n\t\t\t\t? SearchEntryCheckIcon::Checked\n\t\t\t\t: SearchEntryCheckIcon::Unchecked,\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/my-stickers\"_q,\n\t\t\t.title = tr::lng_stickers_you_have(tr::now),\n\t\t\t.keywords = { u\"stickers\"_q, u\"manage\"_q, u\"installed\"_q },\n\t\t\t.icon = { &st::menuIconStickers },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/emoji-sets\"_q,\n\t\t\t.title = tr::lng_emoji_manage_sets(tr::now),\n\t\t\t.keywords = { u\"emoji\"_q, u\"sets\"_q, u\"manage\"_q },\n\t\t\t.icon = { &st::menuIconEmoji },\n\t\t};\n\t});\n}\n\nvoid BuildMessagesSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto highlights = builder.highlights();\n\n\tbuilder.add([controller, highlights](const WidgetContext &ctx) {\n\t\tSetupMessages(controller, ctx.container.get(), highlights);\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/messages\"_q,\n\t\t\t.title = tr::lng_settings_messages(tr::now),\n\t\t\t.keywords = { u\"messages\"_q, u\"send\"_q, u\"enter\"_q },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/send-enter\"_q,\n\t\t\t.title = tr::lng_settings_send_enter(tr::now),\n\t\t\t.keywords = { u\"send\"_q, u\"enter\"_q, u\"keyboard\"_q },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/quick-reaction\"_q,\n\t\t\t.title = tr::lng_settings_chat_quick_action_react(tr::now),\n\t\t\t.keywords = { u\"quick\"_q, u\"reaction\"_q, u\"double\"_q, u\"click\"_q },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/corner-reply\"_q,\n\t\t\t.title = tr::lng_settings_chat_corner_reply(tr::now),\n\t\t\t.keywords = { u\"corner\"_q, u\"reply\"_q },\n\t\t\t.checkIcon = Core::App().settings().cornerReply()\n\t\t\t\t? SearchEntryCheckIcon::Checked\n\t\t\t\t: SearchEntryCheckIcon::Unchecked,\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/corner-reaction\"_q,\n\t\t\t.title = tr::lng_settings_chat_corner_reaction(tr::now),\n\t\t\t.keywords = { u\"corner\"_q, u\"reaction\"_q },\n\t\t\t.checkIcon = Core::App().settings().cornerReaction()\n\t\t\t\t? SearchEntryCheckIcon::Checked\n\t\t\t\t: SearchEntryCheckIcon::Unchecked,\n\t\t};\n\t});\n}\n\nvoid BuildSensitiveContentSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto highlights = builder.highlights();\n\n\tbuilder.add([controller, highlights](const WidgetContext &ctx) {\n\t\tauto updateOnTick = rpl::single(\n\t\t) | rpl::then(base::timer_each(60 * crl::time(1000)));\n\t\tUi::AddDivider(ctx.container.get());\n\t\tSetupSensitiveContent(\n\t\t\tcontroller,\n\t\t\tctx.container.get(),\n\t\t\tstd::move(updateOnTick),\n\t\t\thighlights);\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/sensitive-content\"_q,\n\t\t\t.title = tr::lng_settings_sensitive_title(tr::now),\n\t\t\t.keywords = { u\"sensitive\"_q, u\"content\"_q, u\"nsfw\"_q, u\"adult\"_q },\n\t\t};\n\t});\n}\n\nvoid BuildArchiveSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto showOther = builder.showOther();\n\tconst auto session = builder.session();\n\n\tbuilder.addSkip();\n\n\tbuilder.addSectionButton({\n\t\t.title = tr::lng_settings_shortcuts(),\n\t\t.targetSection = ShortcutsId(),\n\t\t.icon = { &st::menuIconShortcut },\n\t\t.keywords = { u\"shortcuts\"_q, u\"keyboard\"_q, u\"hotkeys\"_q },\n\t});\n\n\tif (controller) {\n\t\tPreloadArchiveSettings(session);\n\t}\n\n\tbuilder.addButton({\n\t\t.id = u\"chat/archive-settings\"_q,\n\t\t.title = tr::lng_context_archive_settings(),\n\t\t.icon = { &st::menuIconArchive },\n\t\t.onClick = [=] {\n\t\t\tif (controller) {\n\t\t\t\tcontroller->show(\n\t\t\t\t\tBox<Ui::GenericBox>(ArchiveSettingsBox, controller));\n\t\t\t}\n\t\t},\n\t\t.keywords = { u\"archive\"_q, u\"settings\"_q, u\"folder\"_q },\n\t});\n}\n\nvoid BuildSupportSection(SectionBuilder &builder) {\n\tconst auto session = builder.session();\n\tif (!session->supportMode()) {\n\t\treturn;\n\t}\n\tconst auto controller = builder.controller();\n\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"chat/support\"_q,\n\t\t.title = rpl::single(u\"Support settings\"_q),\n\t\t.keywords = { u\"support\"_q },\n\t});\n\tbuilder.addSkip(st::settingsSendTypeSkip);\n\n\tusing SwitchType = Support::SwitchSettings;\n\n\tbuilder.add([controller](const WidgetContext &ctx) {\n\t\tconst auto container = ctx.container.get();\n\t\tauto wrap = object_ptr<Ui::VerticalLayout>(container);\n\t\tconst auto inner = wrap.data();\n\n\t\tconst auto group = std::make_shared<Ui::RadioenumGroup<SwitchType>>(\n\t\t\tcontroller->session().settings().supportSwitch());\n\n\t\tconst auto addRadio = [&](SwitchType value, const QString &label) {\n\t\t\tinner->add(\n\t\t\t\tobject_ptr<Ui::Radioenum<SwitchType>>(\n\t\t\t\t\tinner,\n\t\t\t\t\tgroup,\n\t\t\t\t\tvalue,\n\t\t\t\t\tlabel,\n\t\t\t\t\tst::settingsSendType),\n\t\t\t\tst::settingsSendTypePadding);\n\t\t};\n\t\taddRadio(SwitchType::None, \"Just send the reply\");\n\t\taddRadio(SwitchType::Next, \"Send and switch to next\");\n\t\taddRadio(SwitchType::Previous, \"Send and switch to previous\");\n\n\t\tgroup->setChangedCallback([=](SwitchType value) {\n\t\t\tcontroller->session().settings().setSupportSwitch(value);\n\t\t\tcontroller->session().saveSettingsDelayed();\n\t\t});\n\n\t\treturn SectionBuilder::WidgetToAdd{ .widget = std::move(wrap) };\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/support/switch\"_q,\n\t\t\t.title = u\"Send and switch behavior\"_q,\n\t\t\t.keywords = { u\"switch\"_q, u\"next\"_q, u\"previous\"_q, u\"reply\"_q },\n\t\t};\n\t});\n\n\tbuilder.addSkip(st::settingsCheckboxesSkip);\n\n\tconst auto templatesAutocomplete = builder.addCheckbox({\n\t\t.id = u\"chat/support/templates\"_q,\n\t\t.title = rpl::single(u\"Enable templates autocomplete\"_q),\n\t\t.checked = session->settings().supportTemplatesAutocomplete(),\n\t\t.keywords = { u\"templates\"_q, u\"autocomplete\"_q },\n\t});\n\tif (templatesAutocomplete) {\n\t\ttemplatesAutocomplete->checkedChanges(\n\t\t) | rpl::on_next([=](bool checked) {\n\t\t\tcontroller->session().settings().setSupportTemplatesAutocomplete(\n\t\t\t\tchecked);\n\t\t\tcontroller->session().saveSettingsDelayed();\n\t\t}, templatesAutocomplete->lifetime());\n\t}\n\n\tconst auto allSilent = builder.addCheckbox({\n\t\t.id = u\"chat/support/silent\"_q,\n\t\t.title = rpl::single(u\"Send all messages without sound\"_q),\n\t\t.checked = session->settings().supportAllSilent(),\n\t\t.keywords = { u\"silent\"_q, u\"sound\"_q, u\"mute\"_q },\n\t});\n\tif (allSilent) {\n\t\tallSilent->checkedChanges(\n\t\t) | rpl::on_next([=](bool checked) {\n\t\t\tcontroller->session().settings().setSupportAllSilent(checked);\n\t\t\tcontroller->session().saveSettingsDelayed();\n\t\t}, allSilent->lifetime());\n\t}\n\n\tbuilder.addSkip(st::settingsCheckboxesSkip);\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"chat/support/chats-period\"_q,\n\t\t.title = rpl::single(u\"Load chats for a period\"_q),\n\t\t.keywords = { u\"period\"_q, u\"days\"_q },\n\t});\n\n\tbuilder.add([controller](const WidgetContext &ctx) {\n\t\tconstexpr auto kDayDuration = 24 * 60 * 60;\n\t\tstruct Option {\n\t\t\tint days = 0;\n\t\t\tQString label;\n\t\t};\n\t\tconst auto options = std::vector<Option>{\n\t\t\t{ 1, \"1 day\" },\n\t\t\t{ 7, \"1 week\" },\n\t\t\t{ 30, \"1 month\" },\n\t\t\t{ 365, \"1 year\" },\n\t\t\t{ 0, \"All of them\" },\n\t\t};\n\t\tconst auto current = controller->session().settings().supportChatsTimeSlice();\n\t\tconst auto days = current / kDayDuration;\n\t\tconst auto best = ranges::min_element(\n\t\t\toptions,\n\t\t\tstd::less<>(),\n\t\t\t[&](const Option &option) { return std::abs(option.days - days); });\n\n\t\tconst auto container = ctx.container.get();\n\t\tauto wrap = object_ptr<Ui::VerticalLayout>(container);\n\t\tconst auto inner = wrap.data();\n\n\t\tconst auto group = std::make_shared<Ui::RadiobuttonGroup>(best->days);\n\t\tfor (const auto &option : options) {\n\t\t\tinner->add(\n\t\t\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\t\t\tinner,\n\t\t\t\t\tgroup,\n\t\t\t\t\toption.days,\n\t\t\t\t\toption.label,\n\t\t\t\t\tst::settingsSendType),\n\t\t\t\tst::settingsSendTypePadding);\n\t\t}\n\t\tgroup->setChangedCallback([=](int days) {\n\t\t\tcontroller->session().settings().setSupportChatsTimeSlice(\n\t\t\t\tdays * kDayDuration);\n\t\t\tcontroller->session().saveSettingsDelayed();\n\t\t});\n\n\t\treturn SectionBuilder::WidgetToAdd{ .widget = std::move(wrap) };\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"chat/support/chats-period/options\"_q,\n\t\t\t.title = u\"Chat loading period options\"_q,\n\t\t\t.keywords = { u\"week\"_q, u\"month\"_q, u\"year\"_q },\n\t\t};\n\t});\n\n\tbuilder.addSkip(st::settingsCheckboxesSkip);\n\tbuilder.addSkip();\n}\n\nvoid BuildChatSectionContent(SectionBuilder &builder) {\n\tBuildThemeOptionsSection(builder);\n\tBuildThemeSettingsSection(builder);\n\tBuildCloudThemesSection(builder);\n\tBuildChatBackgroundSection(builder);\n\tBuildChatListQuickActionSection(builder);\n\tBuildStickersEmojiSection(builder);\n\tBuildMessagesSection(builder);\n\tBuildSensitiveContentSection(builder);\n\tBuildArchiveSection(builder);\n\tBuildSupportSection(builder);\n}\n\nclass Chat : public Section<Chat> {\npublic:\n\tChat(QWidget *parent, not_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\tvoid fillTopBarMenu(\n\t\tconst Ui::Menu::MenuCallback &addAction) override;\n\nprivate:\n\tvoid setupContent();\n\n};\n\nconst auto kMeta = BuildHelper({\n\t.id = Chat::Id(),\n\t.parentId = MainId(),\n\t.title = &tr::lng_settings_section_chat_settings,\n\t.icon = &st::menuIconChatBubble,\n}, [](SectionBuilder &builder) {\n\tBuildChatSectionContent(builder);\n});\n\nconst SectionBuildMethod kChatSection = kMeta.build;\n\nChat::Chat(QWidget *parent, not_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent();\n}\n\nrpl::producer<QString> Chat::title() {\n\treturn tr::lng_settings_section_chat_settings();\n}\n\nvoid Chat::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {\n\tconst auto window = &controller()->window();\n\tconst auto createTheme = addAction(\n\t\ttr::lng_settings_bg_theme_create(tr::now),\n\t\t[=] { window->show(Box(Window::Theme::CreateBox, window)); },\n\t\t&st::menuIconChangeColors);\n\tcreateTheme->setProperty(\n\t\t\"highlight-control-id\",\n\t\tu\"chat/themes-create\"_q);\n}\n\nvoid Chat::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tbuild(content, kChatSection);\n\n\tUi::ResizeFitChild(this, content);\n}\n\n} // namespace\n\nvoid PaintRoundColorButton(\n\t\tQPainter &p,\n\t\tint size,\n\t\tQBrush brush,\n\t\tfloat64 selected) {\n\tconst auto rect = QRect(0, 0, size, size);\n\n\tp.setBrush(brush);\n\tp.setPen(Qt::NoPen);\n\tp.drawEllipse(rect);\n\n\tif (selected > 0.) {\n\t\tconst auto startSkip = -st::settingsAccentColorLine / 2.;\n\t\tconst auto endSkip = float64(st::settingsAccentColorSkip);\n\t\tconst auto skip = startSkip + (endSkip - startSkip) * selected;\n\t\tauto pen = st::boxBg->p;\n\t\tpen.setWidth(st::settingsAccentColorLine);\n\t\tp.setBrush(Qt::NoBrush);\n\t\tp.setPen(pen);\n\t\tp.setOpacity(selected);\n\t\tp.drawEllipse(QRectF(rect).marginsRemoved({ skip, skip, skip, skip }));\n\t}\n}\n\nvoid SetupStickersEmoji(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tHighlightRegistry *highlights) {\n\tUi::AddSkip(container);\n\n\tconst auto title = Ui::AddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_settings_stickers_emoji());\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/stickers-emoji\"_q, {\n\t\t\ttitle.get(),\n\t\t\tSubsectionTitleHighlight(),\n\t\t} });\n\t}\n\n\tconst auto session = &controller->session();\n\n\tauto wrap = object_ptr<Ui::VerticalLayout>(container);\n\tconst auto inner = wrap.data();\n\tcontainer->add(object_ptr<Ui::OverrideMargins>(\n\t\tcontainer,\n\t\tstd::move(wrap),\n\t\tQMargins(0, 0, 0, st::settingsCheckbox.margin.bottom())));\n\n\tconst auto checkbox = [&](const QString &label, bool checked) {\n\t\treturn object_ptr<Ui::Checkbox>(\n\t\t\tcontainer,\n\t\t\tlabel,\n\t\t\tchecked,\n\t\t\tst::settingsCheckbox);\n\t};\n\tconst auto addWithReturn = [&](\n\t\t\tconst QString &label,\n\t\t\tbool checked,\n\t\t\tauto &&handle) {\n\t\tconst auto result = inner->add(\n\t\t\tcheckbox(label, checked),\n\t\t\tst::settingsCheckboxPadding);\n\t\tresult->checkedChanges(\n\t\t) | rpl::on_next(\n\t\t\tstd::move(handle),\n\t\t\tinner->lifetime());\n\t\treturn result;\n\t};\n\tconst auto addSliding = [&](\n\t\t\tconst QString &label,\n\t\t\tbool checked,\n\t\t\tauto &&handle,\n\t\t\trpl::producer<bool> shown) {\n\t\tconst auto wrap = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::Checkbox>>(\n\t\t\t\tinner,\n\t\t\t\tcheckbox(label, checked),\n\t\t\t\tst::settingsCheckboxPadding));\n\t\twrap->setDuration(0)->toggleOn(std::move(shown))->entity()->checkedChanges(\n\t\t) | rpl::on_next(\n\t\t\tstd::move(handle),\n\t\t\tinner->lifetime());\n\t\treturn wrap->entity();\n\t};\n\n\tconst auto largeEmoji = addWithReturn(\n\t\ttr::lng_settings_large_emoji(tr::now),\n\t\tCore::App().settings().largeEmoji(),\n\t\t[=](bool checked) {\n\t\t\tCore::App().settings().setLargeEmoji(checked);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t});\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/large-emoji\"_q, {\n\t\t\tlargeEmoji,\n\t\t\t{ .radius = st::boxRadius }\n\t\t} });\n\t}\n\n\tconst auto replaceEmoji = addWithReturn(\n\t\ttr::lng_settings_replace_emojis(tr::now),\n\t\tCore::App().settings().replaceEmoji(),\n\t\t[=](bool checked) {\n\t\t\tCore::App().settings().setReplaceEmoji(checked);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t});\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/replace-emoji\"_q, {\n\t\t\treplaceEmoji,\n\t\t\t{ .radius = st::boxRadius },\n\t\t} });\n\t}\n\n\tconst auto suggestEmoji = inner->lifetime().make_state<\n\t\trpl::variable<bool>\n\t>(Core::App().settings().suggestEmoji());\n\tconst auto suggestEmojiCheckbox = addWithReturn(\n\t\ttr::lng_settings_suggest_emoji(tr::now),\n\t\tCore::App().settings().suggestEmoji(),\n\t\t[=](bool checked) {\n\t\t\t*suggestEmoji = checked;\n\t\t\tCore::App().settings().setSuggestEmoji(checked);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t});\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/suggest-emoji\"_q, {\n\t\t\tsuggestEmojiCheckbox,\n\t\t\t{ .radius = st::boxRadius },\n\t\t} });\n\t}\n\n\tusing namespace rpl::mappers;\n\tconst auto suggestAnimated = addSliding(\n\t\ttr::lng_settings_suggest_animated_emoji(tr::now),\n\t\tCore::App().settings().suggestAnimatedEmoji(),\n\t\t[=](bool checked) {\n\t\t\tCore::App().settings().setSuggestAnimatedEmoji(checked);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t},\n\t\trpl::combine(\n\t\t\tData::AmPremiumValue(session),\n\t\t\tsuggestEmoji->value(),\n\t\t\t_1 && _2));\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/suggest-animated-emoji\"_q, {\n\t\t\tsuggestAnimated,\n\t\t\t{ .radius = st::boxRadius }\n\t\t} });\n\t}\n\n\tconst auto suggestByEmoji = addWithReturn(\n\t\ttr::lng_settings_suggest_by_emoji(tr::now),\n\t\tCore::App().settings().suggestStickersByEmoji(),\n\t\t[=](bool checked) {\n\t\t\tCore::App().settings().setSuggestStickersByEmoji(checked);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t});\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/suggest-by-emoji\"_q, {\n\t\t\tsuggestByEmoji,\n\t\t\t{ .radius = st::boxRadius },\n\t\t} });\n\t}\n\n\tconst auto loopStickers = addWithReturn(\n\t\ttr::lng_settings_loop_stickers(tr::now),\n\t\tCore::App().settings().loopAnimatedStickers(),\n\t\t[=](bool checked) {\n\t\t\tCore::App().settings().setLoopAnimatedStickers(checked);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t});\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/loop-stickers\"_q, {\n\t\t\tloopStickers,\n\t\t\t{ .radius = st::boxRadius },\n\t\t} });\n\t}\n\n\tconst auto stickersButton = AddButtonWithIcon(\n\t\tcontainer,\n\t\ttr::lng_stickers_you_have(),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconStickers });\n\tstickersButton->addClickHandler([=] {\n\t\tcontroller->show(Box<StickersBox>(\n\t\t\tcontroller->uiShow(),\n\t\t\tStickersBox::Section::Installed));\n\t});\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/my-stickers\"_q, {\n\t\t\tstickersButton.get(),\n\t\t\t{ .rippleShape = true },\n\t\t} });\n\t}\n\n\tconst auto emojiSetsButton = AddButtonWithIcon(\n\t\tcontainer,\n\t\ttr::lng_emoji_manage_sets(),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconEmoji });\n\temojiSetsButton->addClickHandler([=] {\n\t\tcontroller->show(Box<Ui::Emoji::ManageSetsBox>(session));\n\t});\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/emoji-sets\"_q, {\n\t\t\temojiSetsButton.get(),\n\t\t\t{ .rippleShape = true },\n\t\t} });\n\t}\n\n\tUi::AddSkip(container, st::settingsCheckboxesSkip);\n}\n\nvoid SetupMessages(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tHighlightRegistry *highlights) {\n\tUi::AddDivider(container);\n\tUi::AddSkip(container);\n\n\tconst auto title = Ui::AddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_settings_messages());\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/messages\"_q, {\n\t\t\ttitle.get(),\n\t\t\tSubsectionTitleHighlight(),\n\t\t} });\n\t}\n\n\tUi::AddSkip(container, st::settingsSendTypeSkip);\n\n\tusing SendByType = Ui::InputSubmitSettings;\n\tusing Quick = HistoryView::DoubleClickQuickAction;\n\n\tconst auto skip = st::settingsSendTypeSkip;\n\tauto wrap = object_ptr<Ui::VerticalLayout>(container);\n\tconst auto inner = wrap.data();\n\tcontainer->add(\n\t\tobject_ptr<Ui::OverrideMargins>(\n\t\t\tcontainer,\n\t\t\tstd::move(wrap),\n\t\t\tQMargins(0, skip, 0, skip)));\n\n\tconst auto groupSend = std::make_shared<Ui::RadioenumGroup<SendByType>>(\n\t\tCore::App().settings().sendSubmitWay());\n\tconst auto addSend = [&](SendByType value, const QString &text) {\n\t\treturn inner->add(\n\t\t\tobject_ptr<Ui::Radioenum<SendByType>>(\n\t\t\t\tinner,\n\t\t\t\tgroupSend,\n\t\t\t\tvalue,\n\t\t\t\ttext,\n\t\t\t\tst::settingsSendType),\n\t\t\tst::settingsSendTypePadding);\n\t};\n\tconst auto sendEnter = addSend(\n\t\tSendByType::Enter,\n\t\ttr::lng_settings_send_enter(tr::now));\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/send-enter\"_q, {\n\t\t\tsendEnter,\n\t\t\t{ .radius = st::boxRadius },\n\t\t} });\n\t}\n\taddSend(\n\t\tSendByType::CtrlEnter,\n\t\t(Platform::IsMac()\n\t\t\t? tr::lng_settings_send_cmdenter(tr::now)\n\t\t\t: tr::lng_settings_send_ctrlenter(tr::now)));\n\n\tgroupSend->setChangedCallback([=](SendByType value) {\n\t\tCore::App().settings().setSendSubmitWay(value);\n\t\tCore::App().saveSettingsDelayed();\n\t});\n\n\tUi::AddSkip(inner, st::settingsCheckboxesSkip);\n\n\tconst auto groupQuick = std::make_shared<Ui::RadioenumGroup<Quick>>(\n\t\tCore::App().settings().chatQuickAction());\n\tconst auto addQuick = [&](Quick value, const QString &text) {\n\t\treturn inner->add(\n\t\t\tobject_ptr<Ui::Radioenum<Quick>>(\n\t\t\t\tinner,\n\t\t\t\tgroupQuick,\n\t\t\t\tvalue,\n\t\t\t\ttext,\n\t\t\t\tst::settingsSendType),\n\t\t\tst::settingsSendTypePadding);\n\t};\n\taddQuick(Quick::Reply, tr::lng_settings_chat_quick_action_reply(tr::now));\n\tconst auto react = addQuick(\n\t\tQuick::React,\n\t\ttr::lng_settings_chat_quick_action_react(tr::now));\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/quick-reaction\"_q, {\n\t\t\treact,\n\t\t\t{ .radius = st::boxRadius },\n\t\t} });\n\t}\n\n\tconst auto buttonRight = Ui::CreateSimpleCircleButton(\n\t\tinner,\n\t\tst::stickersRemove.ripple);\n\tbuttonRight->resize(st::stickersRemove.width, st::stickersRemove.height);\n\tconst auto toggleButtonRight = [=](bool value) {\n\t\tbuttonRight->setAttribute(Qt::WA_TransparentForMouseEvents, !value);\n\t};\n\ttoggleButtonRight(false);\n\n\tstruct State {\n\t\tstruct {\n\t\t\tstd::vector<rpl::lifetime> lifetimes;\n\t\t\tbool flag = false;\n\t\t} icons;\n\t};\n\tconst auto state = buttonRight->lifetime().make_state<State>();\n\tstate->icons.lifetimes = std::vector<rpl::lifetime>(2);\n\n\tconst auto &reactions = controller->session().data().reactions();\n\tauto idValue = rpl::single(\n\t\treactions.favoriteId()\n\t) | rpl::then(\n\t\treactions.favoriteUpdates() | rpl::map([=] {\n\t\t\treturn controller->session().data().reactions().favoriteId();\n\t\t})\n\t) | rpl::filter([](const Data::ReactionId &id) {\n\t\treturn !id.empty();\n\t});\n\tauto selected = rpl::duplicate(idValue);\n\tstd::move(\n\t\tselected\n\t) | rpl::on_next([=, idValue = std::move(idValue)](\n\t\t\tconst Data::ReactionId &id) {\n\t\tconst auto index = state->icons.flag ? 1 : 0;\n\t\tconst auto iconSize = st::settingsReactionRightIcon;\n\t\tconst auto &reactions = controller->session().data().reactions();\n\t\tconst auto &list = reactions.list(Data::Reactions::Type::All);\n\t\tconst auto i = ranges::find(list, id, &Data::Reaction::id);\n\t\tstate->icons.lifetimes[index] = rpl::lifetime();\n\t\tif (i != end(list)) {\n\t\t\tAddReactionAnimatedIcon(\n\t\t\t\tinner,\n\t\t\t\tbuttonRight->geometryValue(\n\t\t\t\t) | rpl::map([=](const QRect &r) {\n\t\t\t\t\treturn QPoint(\n\t\t\t\t\t\tr.left() + (r.width() - iconSize) / 2,\n\t\t\t\t\t\tr.top() + (r.height() - iconSize) / 2);\n\t\t\t\t}),\n\t\t\t\ticonSize,\n\t\t\t\t*i,\n\t\t\t\tbuttonRight->events(\n\t\t\t\t) | rpl::filter([=](not_null<QEvent*> event) {\n\t\t\t\t\treturn event->type() == QEvent::Enter;\n\t\t\t\t}) | rpl::to_empty,\n\t\t\t\trpl::duplicate(idValue) | rpl::skip(1) | rpl::to_empty,\n\t\t\t\t&state->icons.lifetimes[index]);\n\t\t} else if (const auto customId = id.custom()) {\n\t\t\tAddReactionCustomIcon(\n\t\t\t\tinner,\n\t\t\t\tbuttonRight->geometryValue(\n\t\t\t\t) | rpl::map([=](const QRect &r) {\n\t\t\t\t\treturn QPoint(\n\t\t\t\t\t\tr.left() + (r.width() - iconSize) / 2,\n\t\t\t\t\t\tr.top() + (r.height() - iconSize) / 2);\n\t\t\t\t}),\n\t\t\t\ticonSize,\n\t\t\t\tcontroller,\n\t\t\t\tcustomId,\n\t\t\t\trpl::duplicate(idValue) | rpl::skip(1) | rpl::to_empty,\n\t\t\t\t&state->icons.lifetimes[index]);\n\t\t}\n\t\tstate->icons.flag = !state->icons.flag;\n\t\ttoggleButtonRight(true);\n\t}, buttonRight->lifetime());\n\n\treact->geometryValue(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tconst auto rightSize = buttonRight->size();\n\t\tbuttonRight->moveToRight(\n\t\t\tst::settingsButtonRightSkip,\n\t\t\tr.y() + (r.height() - rightSize.height()) / 2);\n\t}, buttonRight->lifetime());\n\n\tgroupQuick->setChangedCallback([=](Quick value) {\n\t\tCore::App().settings().setChatQuickAction(value);\n\t\tCore::App().saveSettingsDelayed();\n\t});\n\n\tbuttonRight->setClickedCallback([=, show = controller->uiShow()] {\n\t\tshow->showBox(Box(ReactionsSettingsBox, controller));\n\t});\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/quick-reaction-choose\"_q, {\n\t\t\tbuttonRight.get(),\n\t\t\t{ .shape = HighlightShape::Ellipse },\n\t\t} });\n\t}\n\n\tUi::AddSkip(inner, st::settingsSendTypeSkip);\n\n\tconst auto cornerReply = inner->add(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tinner,\n\t\t\ttr::lng_settings_chat_corner_reply(tr::now),\n\t\t\tCore::App().settings().cornerReply(),\n\t\t\tst::settingsCheckbox),\n\t\tst::settingsCheckboxPadding);\n\tcornerReply->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tCore::App().settings().setCornerReply(checked);\n\t\tCore::App().saveSettingsDelayed();\n\t}, inner->lifetime());\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/corner-reply\"_q, {\n\t\t\tcornerReply,\n\t\t\t{ .radius = st::boxRadius },\n\t\t} });\n\t}\n\n\tconst auto cornerReaction = inner->add(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tinner,\n\t\t\ttr::lng_settings_chat_corner_reaction(tr::now),\n\t\t\tCore::App().settings().cornerReaction(),\n\t\t\tst::settingsCheckbox),\n\t\tst::settingsCheckboxPadding);\n\tcornerReaction->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tCore::App().settings().setCornerReaction(checked);\n\t\tCore::App().saveSettingsDelayed();\n\t}, inner->lifetime());\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/corner-reaction\"_q, {\n\t\t\tcornerReaction,\n\t\t\t{ .radius = st::boxRadius },\n\t\t} });\n\t}\n\n\tUi::AddSkip(inner);\n}\n\nvoid SetupArchive(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tFn<void(Type)> showOther) {\n\tUi::AddSkip(container);\n\n\tAddButtonWithIcon(\n\t\tcontainer,\n\t\ttr::lng_settings_shortcuts(),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconShortcut }\n\t)->addClickHandler([=] {\n\t\tshowOther(ShortcutsId());\n\t});\n\n\tPreloadArchiveSettings(&controller->session());\n\tAddButtonWithIcon(\n\t\tcontainer,\n\t\ttr::lng_context_archive_settings(),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconArchive }\n\t)->addClickHandler([=] {\n\t\tcontroller->show(Box(Settings::ArchiveSettingsBox, controller));\n\t});\n}\n\nvoid SetupExport(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tFn<void(Type)> showOther) {\n\tAddButtonWithIcon(\n\t\tcontainer,\n\t\ttr::lng_settings_export_data(),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconExport }\n\t)->addClickHandler([=] {\n\t\tconst auto session = &controller->session();\n\t\tcontroller->window().hideSettingsAndLayer();\n\t\tbase::call_delayed(\n\t\t\tst::boxDuration,\n\t\t\tsession,\n\t\t\t[=] { Core::App().exportManager().start(session); });\n\t});\n\n\tAddButtonWithIcon(\n\t\tcontainer,\n\t\ttr::lng_settings_experimental(),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconExperimental }\n\t)->addClickHandler([=] {\n\t\tshowOther(Experimental::Id());\n\t});\n}\n\nvoid SetupLocalStorage(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tAddButtonWithIcon(\n\t\tcontainer,\n\t\ttr::lng_settings_manage_local_storage(),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconStorage }\n\t)->addClickHandler([=] { LocalStorageBox::Show(controller); });\n}\n\nvoid SetupDataStorage(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tusing namespace rpl::mappers;\n\n\tUi::AddSkip(container);\n\n\tUi::AddSubsectionTitle(container, tr::lng_settings_data_storage());\n\n\tSetupConnectionType(\n\t\t&controller->window(),\n\t\t&controller->session().account(),\n\t\tcontainer);\n\n#ifndef OS_WIN_STORE\n\tconst auto showpath = container->lifetime(\n\t).make_state<rpl::event_stream<bool>>();\n\n\tconst auto path = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Button>>(\n\t\t\tcontainer,\n\t\t\tCreateButtonWithIcon(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_download_path(),\n\t\t\t\tst::settingsButton,\n\t\t\t\t{ &st::menuIconShowInFolder })));\n\tauto pathtext = Core::App().settings().downloadPathValue(\n\t) | rpl::map([](const QString &text) {\n\t\tif (text.isEmpty()) {\n\t\t\treturn Core::App().canReadDefaultDownloadPath()\n\t\t\t\t? tr::lng_download_path_default(tr::now)\n\t\t\t\t: tr::lng_download_path_temp(tr::now);\n\t\t} else if (text == FileDialog::Tmp()) {\n\t\t\treturn tr::lng_download_path_temp(tr::now);\n\t\t}\n\t\treturn QDir::toNativeSeparators(text);\n\t});\n\tCreateRightLabel(\n\t\tpath->entity(),\n\t\tstd::move(pathtext),\n\t\tst::settingsButton,\n\t\ttr::lng_download_path());\n\tpath->entity()->addClickHandler([=] {\n\t\tcontroller->show(Box<DownloadPathBox>(controller));\n\t});\n#endif // OS_WIN_STORE\n\n\tSetupLocalStorage(controller, container);\n\n\tAddButtonWithIcon(\n\t\tcontainer,\n\t\ttr::lng_downloads_section(),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconDownload }\n\t)->setClickedCallback([=] {\n\t\tcontroller->showSection(\n\t\t\tInfo::Downloads::Make(controller->session().user()));\n\t});\n\n\tconst auto ask = container->add(object_ptr<Ui::SettingsButton>(\n\t\tcontainer,\n\t\ttr::lng_download_path_ask(),\n\t\tst::settingsButtonNoIcon\n\t))->toggleOn(rpl::single(Core::App().settings().askDownloadPath()));\n\n\task->toggledValue(\n\t) | rpl::filter([](bool checked) {\n\t\treturn (checked != Core::App().settings().askDownloadPath());\n\t}) | rpl::on_next([=](bool checked) {\n\t\tCore::App().settings().setAskDownloadPath(checked);\n\t\tCore::App().saveSettingsDelayed();\n\n#ifndef OS_WIN_STORE\n\t\tshowpath->fire_copy(!checked);\n#endif // OS_WIN_STORE\n\n\t}, ask->lifetime());\n\n#ifndef OS_WIN_STORE\n\tpath->toggleOn(ask->toggledValue() | rpl::map(!_1));\n#endif // OS_WIN_STORE\n\n\tUi::AddSkip(container, st::settingsCheckboxesSkip);\n}\n\nvoid SetupAutoDownload(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tUi::AddDivider(container);\n\tUi::AddSkip(container);\n\n\tUi::AddSubsectionTitle(container, tr::lng_media_auto_settings());\n\n\tusing Source = Data::AutoDownload::Source;\n\tconst auto add = [&](\n\t\trpl::producer<QString> label,\n\t\tSource source,\n\t\tIconDescriptor &&descriptor) {\n\t\tAddButtonWithIcon(\n\t\t\tcontainer,\n\t\t\tstd::move(label),\n\t\t\tst::settingsButton,\n\t\t\tstd::move(descriptor)\n\t\t)->addClickHandler([=] {\n\t\t\tcontroller->show(\n\t\t\t\tBox<AutoDownloadBox>(&controller->session(), source));\n\t\t});\n\t};\n\tadd(\n\t\ttr::lng_media_auto_in_private(),\n\t\tSource::User,\n\t\t{ &st::menuIconProfile });\n\tadd(\n\t\ttr::lng_media_auto_in_groups(),\n\t\tSource::Group,\n\t\t{ &st::menuIconGroups });\n\tadd(\n\t\ttr::lng_media_auto_in_channels(),\n\t\tSource::Channel,\n\t\t{ &st::menuIconChannel });\n\n\tUi::AddSkip(container, st::settingsCheckboxesSkip);\n}\n\nvoid SetupChatBackground(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tHighlightRegistry *highlights) {\n\tUi::AddDivider(container);\n\tUi::AddSkip(container);\n\n\tconst auto title = Ui::AddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_settings_section_background());\n\n\tconst auto row = container->add(\n\t\tobject_ptr<BackgroundRow>(container, controller),\n\t\tst::settingsBackgroundPadding);\n\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/wallpapers\"_q, {\n\t\t\ttitle.get(),\n\t\t\tSubsectionTitleHighlight(),\n\t\t} });\n\t\thighlights->push_back({ u\"chat/wallpapers-set\"_q, {\n\t\t\trow->chooseFromGallery(),\n\t\t\tSubsectionTitleHighlight(),\n\t\t} });\n\t\thighlights->push_back({ u\"chat/wallpapers-choose-photo\"_q, {\n\t\t\trow->chooseFromFile(),\n\t\t\tSubsectionTitleHighlight(),\n\t\t} });\n\t}\n\n\tconst auto skipTop = st::settingsCheckbox.margin.top();\n\tconst auto skipBottom = st::settingsCheckbox.margin.bottom();\n\tauto wrap = object_ptr<Ui::VerticalLayout>(container);\n\tconst auto inner = wrap.data();\n\tcontainer->add(\n\t\tobject_ptr<Ui::OverrideMargins>(\n\t\t\tcontainer,\n\t\t\tstd::move(wrap),\n\t\t\tQMargins(0, skipTop, 0, skipBottom)));\n\n\tUi::AddSkip(container, st::settingsTileSkip);\n\n\tconst auto background = Window::Theme::Background();\n\tconst auto tile = inner->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::Checkbox>>(\n\t\t\tinner,\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tinner,\n\t\t\t\ttr::lng_settings_bg_tile(tr::now),\n\t\t\t\tbackground->tile(),\n\t\t\t\tst::settingsCheckbox),\n\t\t\tst::settingsSendTypePadding));\n\tconst auto adaptive = inner->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::Checkbox>>(\n\t\t\tinner,\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tinner,\n\t\t\t\ttr::lng_settings_adaptive_wide(tr::now),\n\t\t\t\tCore::App().settings().adaptiveForWide(),\n\t\t\t\tst::settingsCheckbox),\n\t\t\tst::settingsSendTypePadding));\n\n\ttile->entity()->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tbackground->setTile(checked);\n\t}, tile->lifetime());\n\n\tconst auto shown = [=] {\n\t\treturn !background->paper().isPattern()\n\t\t\t&& !background->colorForFill();\n\t};\n\ttile->toggle(shown(), anim::type::instant);\n\n\tusing Update = const Window::Theme::BackgroundUpdate;\n\tbackground->updates(\n\t) | rpl::filter([](const Update &update) {\n\t\treturn (update.type == Update::Type::Changed)\n\t\t\t|| (update.type == Update::Type::New);\n\t}) | rpl::on_next([=] {\n\t\ttile->entity()->setChecked(background->tile());\n\t\ttile->toggle(shown(), anim::type::instant);\n\t}, tile->lifetime());\n\n\tadaptive->toggleOn(controller->adaptive().chatLayoutValue(\n\t) | rpl::map([](Window::Adaptive::ChatLayout layout) {\n\t\treturn (layout == Window::Adaptive::ChatLayout::Wide);\n\t}));\n\n\tadaptive->entity()->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tCore::App().settings().setAdaptiveForWide(checked);\n\t\tCore::App().saveSettingsDelayed();\n\t}, adaptive->lifetime());\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/adaptive-layout\"_q, {\n\t\t\tadaptive->entity(),\n\t\t\t{ .radius = st::boxRadius },\n\t\t} });\n\t}\n}\n\nvoid SetupChatListQuickAction(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tHighlightRegistry *highlights) {\n\tUi::AddDivider(container);\n\tUi::AddSkip(container);\n\tUi::AddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_settings_quick_dialog_action_title());\n\n\tusing Type = Dialogs::Ui::QuickDialogAction;\n\tusing LabelType = Dialogs::Ui::QuickDialogActionLabel;\n\tconst auto group = std::make_shared<Ui::RadioenumGroup<Type>>(\n\t\tCore::App().settings().quickDialogAction());\n\tgroup->setChangedCallback([=](Type value) {\n\t\tCore::App().settings().setQuickDialogAction(value);\n\t\tCore::App().saveSettings();\n\t});\n\n\tconst auto actionToLabel = [](Type value) {\n\t\tswitch (value) {\n\t\tcase Type::Mute: return LabelType::Mute;\n\t\tcase Type::Pin: return LabelType::Pin;\n\t\tcase Type::Read: return LabelType::Read;\n\t\tcase Type::Archive: return LabelType::Archive;\n\t\tcase Type::Delete: return LabelType::Delete;\n\t\tdefault: return LabelType::Disabled;\n\t\t}\n\t};\n\tstatic constexpr auto kDisabledIconRatio = 1.25;\n\n\tconst auto addPreview = [=](not_null<Ui::VerticalLayout*> container) {\n\t\tconst auto widget = container->add(\n\t\t\tobject_ptr<Ui::RpWidget>(container));\n\t\twidget->resize(0, st::dialogsRowHeight);\n\t\tstruct State {\n\t\t\tstd::unique_ptr<Lottie::Icon> icon;\n\t\t};\n\t\tconst auto state = widget->lifetime().make_state<State>();\n\t\tgroup->value() | rpl::on_next([=](Type value) {\n\t\t\tconst auto label = actionToLabel(value);\n\t\t\tstate->icon = Lottie::MakeIcon({\n\t\t\t\t.name = Dialogs::ResolveQuickDialogLottieIconName(label),\n\t\t\t\t.sizeOverride = Size((label == LabelType::Disabled)\n\t\t\t\t\t? int(st::dialogsQuickActionSize * kDisabledIconRatio)\n\t\t\t\t\t: st::dialogsQuickActionSize),\n\t\t\t});\n\t\t\tstate->icon->animate(\n\t\t\t\t[=] { widget->update(); },\n\t\t\t\t0,\n\t\t\t\tstate->icon->framesCount() - 1);\n\t\t\twidget->update();\n\t\t}, widget->lifetime());\n\t\twidget->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(widget);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\t\tconst auto height = st::dialogsRowHeight;\n\t\t\tconst auto actionWidth = st::dialogsQuickActionRippleSize * 0.75;\n\t\t\tconst auto rightOffset = st::dialogsQuickActionRippleSize\n\t\t\t\t+ st::dialogsQuickActionSize;\n\t\t\tconst auto rect = QRect(\n\t\t\t\twidget->width()\n\t\t\t\t\t- actionWidth\n\t\t\t\t\t- st::boxRowPadding.right()\n\t\t\t\t\t- rightOffset,\n\t\t\t\t0,\n\t\t\t\tactionWidth,\n\t\t\t\theight);\n\n\t        auto path = QPainterPath();\n\t        path.addRoundedRect(\n\t        \tQRect(\n\t        \t\t-actionWidth,\n\t        \t\t0,\n\t        \t\trect::right(rect) + actionWidth,\n\t        \t\theight),\n\t        \tst::roundRadiusLarge,\n\t        \tst::roundRadiusLarge);\n\t        p.setClipPath(path);\n\n\t\t\tconst auto label = actionToLabel(group->current());\n\t\t\tconst auto isDisabled = (label == LabelType::Disabled);\n\n\t\t\tp.fillRect(\n\t\t\t\tQRect(0, 0, rect::right(rect), st::lineWidth),\n\t\t\t\tst::windowBgOver);\n\t\t\tp.fillRect(\n\t\t\t\tQRect(\n\t\t\t\t\t0,\n\t\t\t\t\trect::bottom(rect) - st::lineWidth,\n\t\t\t\t\trect::right(rect),\n\t\t\t\t\tst::lineWidth),\n\t\t\t\tst::windowBgOver);\n\t\t\tp.fillRect(rect, Dialogs::ResolveQuickActionBg(label));\n\t\t\tif (state->icon) {\n\t\t\t\tDialogs::DrawQuickAction(\n\t\t\t\t\tp,\n\t\t\t\t\trect,\n\t\t\t\t\tstate->icon.get(),\n\t\t\t\t\tlabel,\n\t\t\t\t\tisDisabled ? kDisabledIconRatio : 1.,\n\t\t\t\t\tisDisabled);\n\t\t\t}\n\t\t\tp.translate(-height / 2, 0);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(st::windowBgOver);\n\t\t\tp.drawEllipse(Rect(Size(height)) - Margins(height / 6));\n\n\t\t\tconst auto h = st::normalFont->ascent / 1.5;\n\t\t\tp.drawRoundedRect(\n\t\t\t\theight,\n\t\t\t\theight / 2 - h * 1.5,\n\t\t\t\tst::dialogsQuickActionRippleSize * 0.6,\n\t\t\t\th,\n\t\t\t\th / 2,\n\t\t\t\th / 2);\n\t\t\tp.drawRoundedRect(\n\t\t\t\theight,\n\t\t\t\theight / 2 + h,\n\t\t\t\tst::dialogsQuickActionRippleSize * 1.0,\n\t\t\t\th,\n\t\t\t\th / 2,\n\t\t\t\th / 2);\n\n\t\t\tp.setClipping(false);\n\t\t\tp.resetTransform();\n\t\t\tp.setFont(st::settingsQuickDialogActionsTriggerFont);\n\t\t\tp.setPen(st::windowSubTextFg);\n\t\t\tp.drawText(\n\t\t\t\tQRect(\n\t\t\t\t\twidget->width()\n\t\t\t\t\t\t- st::dialogsQuickActionRippleSize\n\t\t\t\t\t\t- st::boxRowPadding.right(),\n\t\t\t\t\t0,\n\t\t\t\t\tst::dialogsQuickActionRippleSize,\n\t\t\t\t\theight),\n\t\t\t\tisDisabled\n\t\t\t\t\t? tr::lng_settings_quick_dialog_action_swipe(tr::now)\n\t\t\t\t\t: tr::lng_settings_quick_dialog_action_both(tr::now),\n\t\t\t\tstyle::al_center);\n\t\t}, widget->lifetime());\n\t};\n\n\tconst auto &st = st::settingsButton;\n\tconst auto button = container->add(\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\tcontainer,\n\t\t\tgroup->value() | rpl::map([](Type value) {\n\t\t\t\treturn ((value == Dialogs::Ui::QuickDialogAction::Mute)\n\t\t\t\t\t? tr::lng_settings_quick_dialog_action_mute\n\t\t\t\t\t: (value == Dialogs::Ui::QuickDialogAction::Pin)\n\t\t\t\t\t? tr::lng_settings_quick_dialog_action_pin\n\t\t\t\t\t: (value == Dialogs::Ui::QuickDialogAction::Read)\n\t\t\t\t\t? tr::lng_settings_quick_dialog_action_read\n\t\t\t\t\t: (value == Dialogs::Ui::QuickDialogAction::Archive)\n\t\t\t\t\t? tr::lng_settings_quick_dialog_action_archive\n\t\t\t\t\t: tr::lng_settings_quick_dialog_action_disabled)();\n\t\t\t}) | rpl::flatten_latest(),\n\t\t\tst));\n\n\t{\n\t\tconst auto icon = button->lifetime().make_state<Ui::RpWidget>(button);\n\t\ticon->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\ticon->resize(st::menuIconArchive.size());\n\t\ticon->show();\n\t\tbutton->sizeValue(\n\t\t) | rpl::on_next([=, left = st.iconLeft](QSize size) {\n\t\t\ticon->moveToLeft(\n\t\t\t\tleft,\n\t\t\t\t(size.height() - icon->height()) / 2,\n\t\t\t\tsize.width());\n\t\t}, icon->lifetime());\n\t\ticon->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = QPainter(icon);\n\t\t\tconst auto value = group->current();\n\t\t\t((value == Dialogs::Ui::QuickDialogAction::Mute)\n\t\t\t\t? st::menuIconMute\n\t\t\t\t: (value == Dialogs::Ui::QuickDialogAction::Pin)\n\t\t\t\t? st::menuIconPin\n\t\t\t\t: (value == Dialogs::Ui::QuickDialogAction::Read)\n\t\t\t\t? st::menuIconMarkRead\n\t\t\t\t: (value == Dialogs::Ui::QuickDialogAction::Delete)\n\t\t\t\t? st::menuIconDelete\n\t\t\t\t: (value == Dialogs::Ui::QuickDialogAction::Archive)\n\t\t\t\t? st::menuIconArchive\n\t\t\t\t: st::menuIconShowInFolder).paintInCenter(p, icon->rect());\n\t\t}, icon->lifetime());\n\t}\n\n\tbutton->setClickedCallback([=] {\n\t\tcontroller->uiShow()->showBox(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tbox->setTitle(tr::lng_settings_quick_dialog_action_title());\n\t\t\tconst auto addRadio = [&](Type value, tr::phrase<> phrase) {\n\t\t\t\tbox->verticalLayout()->add(\n\t\t\t\t\tobject_ptr<Ui::Radioenum<Type>>(\n\t\t\t\t\t\tbox->verticalLayout(),\n\t\t\t\t\t\tgroup,\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t\tphrase(tr::now),\n\t\t\t\t\t\tst::settingsSendType),\n\t\t\t\t\tst::settingsSendTypePadding);\n\t\t\t};\n\t\t\taddPreview(box->verticalLayout());\n\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\taddRadio(Type::Mute, tr::lng_settings_quick_dialog_action_mute);\n\t\t\taddRadio(Type::Pin, tr::lng_settings_quick_dialog_action_pin);\n\t\t\taddRadio(Type::Read, tr::lng_settings_quick_dialog_action_read);\n\t\t\taddRadio(\n\t\t\t\tType::Archive,\n\t\t\t\ttr::lng_settings_quick_dialog_action_archive);\n\t\t\taddRadio(\n\t\t\t\tType::Delete,\n\t\t\t\ttr::lng_settings_quick_dialog_action_delete);\n\t\t\taddRadio(\n\t\t\t\tType::Disabled,\n\t\t\t\ttr::lng_settings_quick_dialog_action_disabled);\n\t\t\tbox->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });\n\t\t}));\n\t});\n\tif (highlights) {\n\t\thighlights->push_back({\n\t\t\tu\"chat/quick-dialog-action\"_q,\n\t\t\t{ button, { .rippleShape = true } },\n\t\t});\n\t}\n\tUi::AddSkip(container);\n\tUi::AddDividerText(\n\t\tcontainer,\n\t\ttr::lng_settings_quick_dialog_action_about());\n\tUi::AddSkip(container);\n}\n\nvoid SetupDefaultThemes(\n\t\tnot_null<Window::Controller*> window,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tHighlightRegistry *highlights) {\n\tusing Type = Window::Theme::EmbeddedType;\n\tusing Scheme = Window::Theme::EmbeddedScheme;\n\tusing Check = Window::Theme::CloudListCheck;\n\tusing namespace Window::Theme;\n\n\tconst auto block = container->add(object_ptr<Ui::FixedHeightWidget>(\n\t\tcontainer));\n\tconst auto palette = Ui::CreateChild<ColorsPalette>(\n\t\tcontainer.get(),\n\t\tcontainer.get());\n\tconst auto systemAccentWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::Checkbox>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_settings_theme_system_accent_color(tr::now),\n\t\t\t\tCore::App().settings().systemAccentColorEnabled(),\n\t\t\t\tst::settingsCheckbox)),\n\t\tst::settingsCheckboxPadding);\n\tsystemAccentWrap->setDuration(0);\n\n\tconst auto chosen = [] {\n\t\tconst auto &object = Background()->themeObject();\n\t\tif (object.cloud.id) {\n\t\t\treturn Type(-1);\n\t\t}\n\t\tfor (const auto &scheme : kSchemesList) {\n\t\t\tif (object.pathAbsolute == scheme.path) {\n\t\t\t\treturn scheme.type;\n\t\t\t}\n\t\t}\n\t\treturn Type(-1);\n\t};\n\tconst auto group = std::make_shared<Ui::RadioenumGroup<Type>>(chosen());\n\n\tconst auto apply = [=](const Scheme &scheme) {\n\t\tconst auto isNight = [](const Scheme &scheme) {\n\t\t\tconst auto type = scheme.type;\n\t\t\treturn (type != Type::DayBlue) && (type != Type::Default);\n\t\t};\n\t\tconst auto currentlyIsCustom = (chosen() == Type(-1))\n\t\t\t&& !Background()->themeObject().cloud.id;\n\t\tconst auto keep = [=] {\n\t\t\tif (!currentlyIsCustom) {\n\t\t\t\tKeepApplied();\n\t\t\t}\n\t\t};\n\t\tif (IsNightMode() == isNight(scheme)) {\n\t\t\tApplyDefaultWithPath(scheme.path);\n\t\t\tkeep();\n\t\t} else {\n\t\t\tWindow::Theme::ToggleNightModeWithConfirmation(\n\t\t\t\twindow,\n\t\t\t\t[=, path = scheme.path] { ToggleNightMode(path); keep();});\n\t\t}\n\t};\n\tconst auto schemeClicked = [=](\n\t\t\tconst Scheme &scheme,\n\t\t\tQt::KeyboardModifiers modifiers) {\n\t\tapply(scheme);\n\t};\n\n\tauto checks = base::flat_map<Type,not_null<Check*>>();\n\tauto buttons = ranges::views::all(\n\t\tkSchemesList\n\t) | ranges::views::transform([&](const Scheme &scheme) {\n\t\tauto check = std::make_unique<Check>(\n\t\t\tColorsFromScheme(scheme),\n\t\t\tfalse);\n\t\tconst auto weak = check.get();\n\t\tconst auto result = Ui::CreateChild<Ui::Radioenum<Type>>(\n\t\t\tblock,\n\t\t\tgroup,\n\t\t\tscheme.type,\n\t\t\tQString(),\n\t\t\tst::settingsTheme,\n\t\t\tstd::move(check));\n\t\trpl::duplicate(\n\t\t\tscheme.name\n\t\t) | rpl::on_next([=](const QString &themeName) {\n\t\t\tresult->setText(themeName);\n\t\t}, result->lifetime());\n\t\tresult->addClickHandler([=] {\n\t\t\tschemeClicked(scheme, result->clickModifiers());\n\t\t});\n\t\tweak->setUpdateCallback([=] { result->update(); });\n\t\tchecks.emplace(scheme.type, weak);\n\t\treturn result;\n\t}) | ranges::to_vector;\n\n\tconst auto refreshColorizer = [=](Type type) {\n\t\tif (type == chosen()) {\n\t\t\tpalette->show(type);\n\t\t}\n\n\t\tconst auto &settings = Core::App().settings();\n\t\tconst auto i = checks.find(type);\n\t\tconst auto scheme = ranges::find(kSchemesList, type, &Scheme::type);\n\t\tif (scheme == end(kSchemesList)) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto color = settings.systemAccentColorEnabled()\n\t\t\t? Window::Theme::SystemAccentColor()\n\t\t\t: settings.themesAccentColors().get(type);\n\t\tif (i != end(checks)) {\n\t\t\tif (color) {\n\t\t\t\tconst auto colorizer = ColorizerFrom(*scheme, *color);\n\t\t\t\ti->second->setColors(ColorsFromScheme(*scheme, colorizer));\n\t\t\t} else {\n\t\t\t\ti->second->setColors(ColorsFromScheme(*scheme));\n\t\t\t}\n\t\t}\n\t};\n\tconst auto refreshSystemAccentVisibility = [=](Type type) {\n\t\tsystemAccentWrap->toggle(\n\t\t\tIsSystemAccentColorSupported() && (type != Type(-1)),\n\t\t\tanim::type::instant);\n\t};\n\tgroup->setChangedCallback([=](Type type) {\n\t\tconst auto scheme = ranges::find(\n\t\t\tkSchemesList,\n\t\t\ttype,\n\t\t\t&Scheme::type);\n\t\tif (scheme != end(kSchemesList)) {\n\t\t\tapply(*scheme);\n\t\t} else {\n\t\t\tgroup->setValue(chosen());\n\t\t}\n\t});\n\tfor (const auto &scheme : kSchemesList) {\n\t\trefreshColorizer(scheme.type);\n\t}\n\trefreshSystemAccentVisibility(chosen());\n\tsystemAccentWrap->entity()->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tauto &settings = Core::App().settings();\n\t\tif (settings.systemAccentColorEnabled() == checked) {\n\t\t\treturn;\n\t\t}\n\t\tsettings.setSystemAccentColorEnabled(checked);\n\t\tLocal::writeSettings();\n\n\t\tconst auto type = chosen();\n\t\tconst auto scheme = ranges::find(kSchemesList, type, &Scheme::type);\n\t\tif (scheme != end(kSchemesList)) {\n\t\t\tapply(*scheme);\n\t\t}\n\t}, container->lifetime());\n\n\tif (highlights) {\n\t\tconst auto add = st::roundRadiusSmall;\n\t\thighlights->push_back({ u\"chat/themes-edit\"_q, {\n\t\t\tpalette->editButton(),\n\t\t\t{\n\t\t\t\t.margin = { -add, -add, -add, -add },\n\t\t\t\t.shape = HighlightShape::Ellipse,\n\t\t\t},\n\t\t} });\n\t\tif (IsSystemAccentColorSupported()) {\n\t\t\thighlights->push_back({ u\"chat/themes-system-accent\"_q, {\n\t\t\t\tsystemAccentWrap->entity(),\n\t\t\t\t{ .radius = st::boxRadius }\n\t\t\t} });\n\t\t}\n\t}\n\n\tBackground()->updates(\n\t) | rpl::filter([](const BackgroundUpdate &update) {\n\t\treturn (update.type == BackgroundUpdate::Type::ApplyingTheme);\n\t}) | rpl::map([=] {\n\t\treturn chosen();\n\t}) | rpl::on_next([=](Type type) {\n\t\trefreshColorizer(type);\n\t\trefreshSystemAccentVisibility(type);\n\t\tgroup->setValue(type);\n\t}, container->lifetime());\n\n\tfor (const auto button : buttons) {\n\t\tbutton->setCheckAlignment(style::al_top);\n\t\tbutton->resizeToWidth(button->width());\n\t}\n\tblock->resize(block->width(), buttons[0]->height());\n\tblock->widthValue(\n\t) | rpl::on_next([buttons = std::move(buttons)](int width) {\n\t\tExpects(!buttons.empty());\n\n\t\tconst auto padding = st::settingsButtonNoIcon.padding;\n\t\twidth -= padding.left() + padding.right();\n\t\tconst auto desired = st::settingsThemePreviewSize.width();\n\t\tconst auto count = int(buttons.size());\n\t\tconst auto skips = count - 1;\n\t\tconst auto minSkip = st::settingsThemeMinSkip;\n\t\tconst auto single = [&] {\n\t\t\tif (width >= skips * minSkip + count * desired) {\n\t\t\t\treturn desired;\n\t\t\t}\n\t\t\treturn (width - skips * minSkip) / count;\n\t\t}();\n\t\tif (single <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto fullSkips = width - count * single;\n\t\tconst auto skip = fullSkips / float64(skips);\n\t\tauto left = padding.left() + 0.;\n\t\tfor (const auto button : buttons) {\n\t\t\tbutton->resizeToWidth(single);\n\t\t\tbutton->moveToLeft(int(base::SafeRound(left)), 0);\n\t\t\tleft += button->width() + skip;\n\t\t}\n\t}, block->lifetime());\n\n\tpalette->selected(\n\t) | rpl::on_next([=](QColor color) {\n\t\tif (Background()->editingTheme()) {\n\t\t\twindow->show(Ui::MakeInformBox(\n\t\t\t\ttr::lng_theme_editor_cant_change_theme()));\n\t\t\treturn;\n\t\t}\n\t\tconst auto type = chosen();\n\t\tconst auto scheme = ranges::find(kSchemesList, type, &Scheme::type);\n\t\tif (scheme == end(kSchemesList)) {\n\t\t\treturn;\n\t\t}\n\t\tauto &settings = Core::App().settings();\n\t\tauto changed = false;\n\t\tif (settings.systemAccentColorEnabled()) {\n\t\t\tsettings.setSystemAccentColorEnabled(false);\n\t\t\tsystemAccentWrap->entity()->setChecked(false);\n\t\t\tchanged = true;\n\t\t}\n\t\tauto &colors = settings.themesAccentColors();\n\t\tif (colors.get(type) != color) {\n\t\t\tcolors.set(type, color);\n\t\t\tchanged = true;\n\t\t}\n\t\tif (changed) {\n\t\t\tLocal::writeSettings();\n\t\t}\n\t\tapply(*scheme);\n\t}, container->lifetime());\n\n\tUi::AddSkip(container);\n}\n\nvoid SetupThemeOptions(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tHighlightRegistry *highlights) {\n\tusing namespace Window::Theme;\n\n\tUi::AddSkip(container, st::settingsPrivacySkip);\n\n\tconst auto title = Ui::AddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_settings_themes());\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/themes\"_q, {\n\t\t\ttitle.get(),\n\t\t\tSubsectionTitleHighlight(),\n\t\t} });\n\t}\n\n\tUi::AddSkip(container, st::settingsThemesTopSkip);\n\tSetupDefaultThemes(&controller->window(), container, highlights);\n\tUi::AddSkip(container);\n}\n\nvoid SetupCloudThemes(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tHighlightRegistry *highlights) {\n\tusing namespace Window::Theme;\n\tusing namespace rpl::mappers;\n\n\tconst auto wrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container))\n\t)->setDuration(0);\n\tconst auto inner = wrap->entity();\n\n\tUi::AddDivider(inner);\n\tUi::AddSkip(inner, st::settingsPrivacySkip);\n\n\tconst auto title = AddSubsectionTitle(\n\t\tinner,\n\t\ttr::lng_settings_bg_cloud_themes());\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/cloud-themes\"_q, {\n\t\t\ttitle.get(),\n\t\t\tSubsectionTitleHighlight(),\n\t\t} });\n\t}\n\tconst auto showAll = Ui::CreateChild<Ui::LinkButton>(\n\t\tinner,\n\t\ttr::lng_settings_bg_show_all(tr::now));\n\n\trpl::combine(\n\t\ttitle->topValue(),\n\t\tinner->widthValue(),\n\t\tshowAll->widthValue()\n\t) | rpl::on_next([=](int top, int outerWidth, int width) {\n\t\tshowAll->moveToRight(\n\t\t\tst::defaultSubsectionTitlePadding.left(),\n\t\t\ttop,\n\t\t\touterWidth);\n\t}, showAll->lifetime());\n\n\tUi::AddSkip(inner, st::settingsThemesTopSkip);\n\n\tconst auto list = inner->lifetime().make_state<CloudList>(\n\t\tinner,\n\t\tcontroller);\n\tinner->add(\n\t\tlist->takeWidget(),\n\t\tstyle::margins(\n\t\t\tst::settingsButtonNoIcon.padding.left(),\n\t\t\t0,\n\t\t\tst::settingsButtonNoIcon.padding.right(),\n\t\t\t0));\n\n\tlist->allShown(\n\t) | rpl::on_next([=](bool shown) {\n\t\tshowAll->setVisible(!shown);\n\t}, showAll->lifetime());\n\n\tshowAll->addClickHandler([=] {\n\t\tlist->showAll();\n\t});\n\n\tconst auto editWrap = inner->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tinner,\n\t\t\tobject_ptr<Ui::VerticalLayout>(inner))\n\t)->setDuration(0);\n\tconst auto edit = editWrap->entity();\n\n\tUi::AddSkip(edit, st::settingsThemesBottomSkip);\n\tAddButtonWithIcon(\n\t\tedit,\n\t\ttr::lng_settings_bg_theme_edit(),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconPalette }\n\t)->addClickHandler([=] {\n\t\tStartEditor(\n\t\t\t&controller->window(),\n\t\t\tBackground()->themeObject().cloud);\n\t});\n\n\teditWrap->toggleOn(rpl::single(BackgroundUpdate(\n\t\tBackgroundUpdate::Type::ApplyingTheme,\n\t\tBackground()->tile()\n\t)) | rpl::then(\n\t\tBackground()->updates()\n\t) | rpl::filter([](const BackgroundUpdate &update) {\n\t\treturn (update.type == BackgroundUpdate::Type::ApplyingTheme);\n\t}) | rpl::map([=] {\n\t\tconst auto userId = controller->session().userId();\n\t\treturn (Background()->themeObject().cloud.createdBy == userId);\n\t}));\n\n\tUi::AddSkip(inner, 2 * st::defaultVerticalListSkip);\n\n\twrap->setDuration(0)->toggleOn(list->empty() | rpl::map(!_1));\n}\n\nvoid SetupThemeSettings(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tHighlightRegistry *highlights) {\n\tUi::AddDivider(container);\n\tUi::AddSkip(container, st::settingsPrivacySkip);\n\n\tconst auto title = Ui::AddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_settings_theme_settings());\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/peer-color\"_q, {\n\t\t\ttitle.get(),\n\t\t\tSubsectionTitleHighlight(),\n\t\t} });\n\t}\n\n\tAddPeerColorButton(\n\t\tcontainer,\n\t\tcontroller->uiShow(),\n\t\tcontroller->session().user(),\n\t\tst::settingsColorButton);\n\n\tconst auto settings = &Core::App().settings();\n\tif (settings->systemDarkMode().has_value()) {\n\t\tauto label = settings->systemDarkModeEnabledValue(\n\t\t) | rpl::map([=](bool enabled) {\n\t\t\treturn enabled\n\t\t\t\t? tr::lng_settings_auto_night_mode_on()\n\t\t\t\t: tr::lng_settings_auto_night_mode_off();\n\t\t}) | rpl::flatten_latest();\n\t\tconst auto button = AddButtonWithLabel(\n\t\t\tcontainer,\n\t\t\ttr::lng_settings_auto_night_mode(),\n\t\t\tstd::move(label),\n\t\t\tst::settingsButton,\n\t\t\t{ &st::menuIconNightMode });\n\t\tbutton->setClickedCallback([=] {\n\t\t\tconst auto now = !settings->systemDarkModeEnabled();\n\t\t\tif (now && Window::Theme::Background()->editingTheme()) {\n\t\t\t\tcontroller->show(Ui::MakeInformBox(\n\t\t\t\t\ttr::lng_theme_editor_cant_change_theme()));\n\t\t\t} else {\n\t\t\t\tsettings->setSystemDarkModeEnabled(now);\n\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t}\n\t\t});\n\t\tif (highlights) {\n\t\t\thighlights->push_back({\n\t\t\t\tu\"chat/auto-night-mode\"_q,\n\t\t\t\t{ button.get(), { .rippleShape = true } },\n\t\t\t});\n\t\t}\n\t}\n\n\tconst auto family = container->lifetime().make_state<\n\t\trpl::variable<QString>\n\t>(settings->customFontFamily());\n\tauto label = family->value() | rpl::map([](QString family) {\n\t\treturn family.isEmpty()\n\t\t\t? tr::lng_font_default(tr::now)\n\t\t\t: (family == style::SystemFontTag())\n\t\t\t? tr::lng_font_system(tr::now)\n\t\t\t: family;\n\t});\n\tconst auto fontButton = AddButtonWithLabel(\n\t\tcontainer,\n\t\ttr::lng_settings_font_family(),\n\t\tstd::move(label),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconFont });\n\tfontButton->setClickedCallback([=] {\n\t\tconst auto save = [=](QString chosen) {\n\t\t\t*family = chosen;\n\t\t\tsettings->setCustomFontFamily(chosen);\n\t\t\tLocal::writeSettings();\n\t\t\tCore::Restart();\n\t\t};\n\n\t\tconst auto theme = std::shared_ptr<Ui::ChatTheme>(\n\t\t\tWindow::Theme::DefaultChatThemeOn(container->lifetime()));\n\t\tconst auto generateBg = [=] {\n\t\t\tconst auto size = st::boxWidth;\n\t\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t\tauto result = QImage(\n\t\t\t\tQSize(size, size) * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tauto p = QPainter(&result);\n\t\t\tWindow::SectionWidget::PaintBackground(\n\t\t\t\tp,\n\t\t\t\ttheme.get(),\n\t\t\t\tQSize(size, size * 3),\n\t\t\t\tQRect(0, 0, size, size));\n\t\t\tp.end();\n\n\t\t\treturn result;\n\t\t};\n\t\tcontroller->show(\n\t\t\tBox(Ui::ChooseFontBox, generateBg, family->current(), save));\n\t});\n\tif (highlights) {\n\t\thighlights->push_back({\n\t\t\tu\"chat/font\"_q,\n\t\t\t{ fontButton.get(), { .rippleShape = true } },\n\t\t});\n\t}\n\n\tUi::AddSkip(container, st::settingsCheckboxesSkip);\n}\n\nvoid SetupSupport(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tUi::AddSkip(container);\n\n\tUi::AddSubsectionTitle(container, rpl::single(u\"Support settings\"_q));\n\n\tUi::AddSkip(container, st::settingsSendTypeSkip);\n\n\tconst auto skip = st::settingsSendTypeSkip;\n\tauto wrap = object_ptr<Ui::VerticalLayout>(container);\n\tconst auto inner = wrap.data();\n\tcontainer->add(\n\t\tobject_ptr<Ui::OverrideMargins>(\n\t\t\tcontainer,\n\t\t\tstd::move(wrap),\n\t\t\tQMargins(0, skip, 0, skip)));\n\n\tSetupSupportSwitchSettings(controller, inner);\n\n\tUi::AddSkip(inner, st::settingsCheckboxesSkip);\n\n\tinner->add(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tinner,\n\t\t\t\"Enable templates autocomplete\",\n\t\t\tcontroller->session().settings().supportTemplatesAutocomplete(),\n\t\t\tst::settingsCheckbox),\n\t\tst::settingsSendTypePadding\n\t)->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tcontroller->session().settings().setSupportTemplatesAutocomplete(\n\t\t\tchecked);\n\t\tcontroller->session().saveSettingsDelayed();\n\t}, inner->lifetime());\n\n\tinner->add(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tinner,\n\t\t\t\"Send all messages without sound\",\n\t\t\tcontroller->session().settings().supportAllSilent(),\n\t\t\tst::settingsCheckbox),\n\t\tst::settingsSendTypePadding\n\t)->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tcontroller->session().settings().setSupportAllSilent(\n\t\t\tchecked);\n\t\tcontroller->session().saveSettingsDelayed();\n\t}, inner->lifetime());\n\n\tUi::AddSkip(inner, st::settingsCheckboxesSkip);\n\n\tUi::AddSubsectionTitle(inner, rpl::single(u\"Load chats for a period\"_q));\n\n\tSetupSupportChatsLimitSlice(controller, inner);\n\n\tUi::AddSkip(inner, st::settingsCheckboxesSkip);\n\n\tUi::AddSkip(inner);\n}\n\nType ChatId() {\n\treturn Chat::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_chat.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_common.h\"\n\nclass QPainter;\nclass QBrush;\n\nnamespace Window {\nclass Controller;\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Settings {\n\n[[nodiscard]] Type ChatId();\n\nvoid SetupDataStorage(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container);\nvoid SetupAutoDownload(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container);\nvoid SetupDefaultThemes(\n\tnot_null<Window::Controller*> window,\n\tnot_null<Ui::VerticalLayout*> container,\n\tHighlightRegistry *highlights = nullptr);\nvoid SetupSupport(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container);\nvoid SetupExport(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container,\n\tFn<void(Type)> showOther);\n\nvoid PaintRoundColorButton(\n\tQPainter &p,\n\tint size,\n\tQBrush brush,\n\tfloat64 selected);\n\nvoid SetupThemeOptions(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container,\n\tHighlightRegistry *highlights = nullptr);\n\nvoid SetupThemeSettings(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container,\n\tHighlightRegistry *highlights = nullptr);\n\nvoid SetupCloudThemes(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container,\n\tHighlightRegistry *highlights = nullptr);\n\nvoid SetupChatBackground(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container,\n\tHighlightRegistry *highlights = nullptr);\n\nvoid SetupChatListQuickAction(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container,\n\tHighlightRegistry *highlights = nullptr);\n\nvoid SetupStickersEmoji(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container,\n\tHighlightRegistry *highlights = nullptr);\n\nvoid SetupMessages(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container,\n\tHighlightRegistry *highlights = nullptr);\n\nvoid SetupArchive(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container,\n\tFn<void(Type)> showOther);\n\nvoid SetupSensitiveContent(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container,\n\trpl::producer<> updateTrigger,\n\tHighlightRegistry *highlights = nullptr);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_credits.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_credits.h\"\n\n#include \"api/api_credits.h\"\n#include \"api/api_earn.h\"\n#include \"api/api_statistics.h\"\n#include \"base/call_delayed.h\"\n#include \"boxes/gift_credits_box.h\"\n#include \"boxes/gift_premium_box.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"chat_helpers/stickers_gift_box_pack.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/components/credits.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"info/bot/earn/info_bot_earn_widget.h\"\n#include \"info/bot/starref/info_bot_starref_common.h\"\n#include \"info/bot/starref/info_bot_starref_join_widget.h\"\n#include \"info/channel_statistics/boosts/giveaway/boost_badge.h\"\n#include \"info/channel_statistics/earn/earn_format.h\"\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"info/channel_statistics/earn/info_channel_earn_list.h\"\n#include \"info/info_memento.h\"\n#include \"info/settings/info_settings_widget.h\"\n#include \"info/statistics/info_statistics_list_controllers.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/settings_common_session.h\"\n#include \"settings/settings_credits_graphics.h\"\n#include \"statistics/widgets/chart_header_widget.h\"\n#include \"ui/boxes/boost_box.h\"\n#include \"ui/controls/swipe_handler.h\"\n#include \"ui/controls/swipe_handler_data.h\"\n#include \"ui/effects/animation_value_f.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/effects/premium_top_bar.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/custom_emoji_instance.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/slider_natural_width.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_statistics.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_channel_earn.h\"\n\nnamespace Settings {\nnamespace {\n\nusing namespace Builder;\n\nvoid BuildCurrencyWithdrawalSection(\n\tnot_null<Ui::VerticalLayout*> content,\n\tnot_null<Window::SessionController*> controller);\n\nvoid BuildCreditsButtons(\n\tSectionBuilder &builder,\n\tbool isCurrency,\n\tQPointer<Ui::SettingsButton> *statsButton,\n\tQPointer<Ui::SettingsButton> *giftButton,\n\tQPointer<Ui::SettingsButton> *earnButton);\n\nvoid BuildCreditsSectionContent(\n\tSectionBuilder &builder,\n\tbool isCurrency,\n\tQPointer<Ui::SettingsButton> *statsButton,\n\tQPointer<Ui::SettingsButton> *giftButton,\n\tQPointer<Ui::SettingsButton> *earnButton,\n\tFn<void(not_null<Ui::VerticalLayout*>)> setupSubscriptions,\n\tFn<void(not_null<Ui::VerticalLayout*>)> setupHistory);\n\nclass Credits : public Section<Credits> {\npublic:\n\tCredits(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tCreditsType type);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\t[[nodiscard]] base::weak_qptr<Ui::RpWidget> createPinnedToTop(\n\t\tnot_null<QWidget*> parent) override;\n\n\tvoid showFinished() override;\n\n\t[[nodiscard]] bool hasFlexibleTopBar() const override;\n\n\tvoid setStepDataReference(std::any &data) override;\n\n\t[[nodiscard]] rpl::producer<> sectionShowBack() override final;\n\nprivate:\n\tvoid setupContent();\n\tvoid setupSwipeBack();\n\tvoid setupHistory(not_null<Ui::VerticalLayout*> container);\n\tvoid setupSubscriptions(not_null<Ui::VerticalLayout*> container);\n\tconst CreditsType _creditsType;\n\n\tQWidget *_parent = nullptr;\n\n\tQImage _star;\n\tQImage _balanceStar;\n\n\tbase::unique_qptr<Ui::FadeWrap<Ui::IconButton>> _back;\n\tbase::unique_qptr<Ui::IconButton> _close;\n\trpl::variable<bool> _backToggles;\n\trpl::variable<Info::Wrap> _wrap;\n\tFn<void(bool)> _setPaused;\n\n\trpl::event_stream<> _showBack;\n\trpl::event_stream<> _showFinished;\n\trpl::variable<QString> _buttonText;\n\n\tQPointer<Ui::SettingsButton> _statsButton;\n\tQPointer<Ui::SettingsButton> _giftButton;\n\tQPointer<Ui::SettingsButton> _earnButton;\n\n};\n\nCredits::Credits(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tCreditsType type)\n: Section(parent, controller)\n, _creditsType(type)\n, _star(Ui::GenerateStars(st::creditsTopupButton.height, 1))\n, _balanceStar((_creditsType == CreditsType::Ton)\n\t\t? Ui::Earn::IconCurrencyColored(\n\t\t\tst::tonFieldIconSize,\n\t\t\tst::currencyFg->c)\n\t\t: Ui::GenerateStars(st::creditsBalanceStarHeight, 1)) {\n\tcontroller->session().giftBoxStickersPacks().tonLoad();\n\tsetupContent();\n\tsetupSwipeBack();\n\n\tcontroller->session().premiumPossibleValue(\n\t) | rpl::on_next([=](bool premiumPossible) {\n\t\tif (!premiumPossible) {\n\t\t\t_showBack.fire({});\n\t\t}\n\t}, lifetime());\n}\n\nrpl::producer<QString> Credits::title() {\n\tif (_creditsType == CreditsType::Ton) {\n\t\treturn tr::lng_credits_currency_summary_title();\n\t}\n\treturn tr::lng_credits_summary_title();\n}\n\nbool Credits::hasFlexibleTopBar() const {\n\treturn true;\n}\n\nrpl::producer<> Credits::sectionShowBack() {\n\treturn _showBack.events();\n}\n\nvoid Credits::setStepDataReference(std::any &data) {\n\tusing SectionCustomTopBarData = Info::Settings::SectionCustomTopBarData;\n\tconst auto my = std::any_cast<SectionCustomTopBarData>(&data);\n\tif (my) {\n\t\t_backToggles = std::move(\n\t\t\tmy->backButtonEnables\n\t\t) | rpl::map_to(true);\n\t\t_wrap = std::move(my->wrapValue);\n\t}\n}\n\nvoid Credits::setupSubscriptions(not_null<Ui::VerticalLayout*> container) {\n\tconst auto history = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto content = history->entity();\n\tconst auto self = controller()->session().user();\n\n\tconst auto fill = [=](const Data::CreditsStatusSlice &fullSlice) {\n\t\tconst auto inner = content;\n\t\tif (fullSlice.subscriptions.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tUi::AddSkip(inner);\n\t\tUi::AddSubsectionTitle(\n\t\t\tinner,\n\t\t\ttr::lng_credits_subscription_section(),\n\t\t\t{ 0, 0, 0, -st::settingsPremiumOptionsPadding.bottom() });\n\n\t\tconst auto fullWrap = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\n\t\tconst auto window = controller()->parentController();\n\t\tconst auto entryClicked = [=](\n\t\t\t\tconst Data::CreditsHistoryEntry &e,\n\t\t\t\tconst Data::SubscriptionEntry &s) {\n\t\t\twindow->uiShow()->show(\n\t\t\t\tBox(ReceiptCreditsBox, window, e, s));\n\t\t};\n\n\t\tInfo::Statistics::AddCreditsHistoryList(\n\t\t\twindow->uiShow(),\n\t\t\tfullSlice,\n\t\t\tfullWrap->entity(),\n\t\t\tentryClicked,\n\t\t\tself,\n\t\t\ttrue,\n\t\t\ttrue,\n\t\t\ttrue);\n\n\t\tUi::AddSkip(inner);\n\t\tUi::AddSkip(inner);\n\t\tUi::AddDivider(inner);\n\n\t\tinner->resizeToWidth(container->width());\n\t};\n\n\tconst auto apiLifetime = content->lifetime().make_state<rpl::lifetime>();\n\t{\n\t\tusing Api = Api::CreditsHistory;\n\t\tconst auto apiFull = apiLifetime->make_state<Api>(self, true, true);\n\t\tapiFull->requestSubscriptions({}, [=](Data::CreditsStatusSlice d) {\n\t\t\tfill(std::move(d));\n\t\t});\n\t}\n\t{\n\t\tusing Rebuilder = Data::Session::CreditsSubsRebuilder;\n\t\tusing RebuilderPtr = std::shared_ptr<Rebuilder>;\n\t\tconst auto rebuilder = content->lifetime().make_state<RebuilderPtr>(\n\t\t\tself->owner().createCreditsSubsRebuilder());\n\t\trebuilder->get()->events(\n\t\t) | rpl::on_next([=](Data::CreditsStatusSlice slice) {\n\t\t\twhile (content->count()) {\n\t\t\t\tdelete content->widgetAt(0);\n\t\t\t}\n\t\t\tfill(std::move(slice));\n\t\t}, content->lifetime());\n\t}\n}\n\nvoid Credits::setupHistory(not_null<Ui::VerticalLayout*> container) {\n\tconst auto history = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto content = history->entity();\n\tconst auto self = controller()->session().user();\n\n\tUi::AddSkip(content, st::lineWidth * 6);\n\n\tconst auto fill = [=](\n\t\t\tnot_null<PeerData*> premiumBot,\n\t\t\tconst Data::CreditsStatusSlice &fullSlice,\n\t\t\tconst Data::CreditsStatusSlice &inSlice,\n\t\t\tconst Data::CreditsStatusSlice &outSlice) {\n\t\tconst auto inner = content;\n\t\tif (fullSlice.list.empty()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto hasOneTab = inSlice.list.empty() && outSlice.list.empty();\n\t\tconst auto hasIn = !inSlice.list.empty();\n\t\tconst auto hasOut = !outSlice.list.empty();\n\t\tconst auto fullTabText = tr::lng_credits_summary_history_tab_full(\n\t\t\ttr::now);\n\t\tconst auto inTabText = tr::lng_credits_summary_history_tab_in(\n\t\t\ttr::now);\n\t\tconst auto outTabText = tr::lng_credits_summary_history_tab_out(\n\t\t\ttr::now);\n\t\tif (hasOneTab) {\n\t\t\tUi::AddSubsectionTitle(\n\t\t\t\tinner,\n\t\t\t\ttr::lng_credits_summary_history_tab_full(),\n\t\t\t\t{ 0, 0, 0, -st::defaultSubsectionTitlePadding.bottom() });\n\t\t}\n\n\t\tconst auto slider = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::CustomWidthSlider>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::CustomWidthSlider>(\n\t\t\t\t\tinner,\n\t\t\t\t\tst::creditsHistoryTabsSlider)),\n\t\t\tst::creditsHistoryTabsSliderPadding);\n\t\tslider->toggle(!hasOneTab, anim::type::instant);\n\t\tif (!hasOneTab) {\n\t\t\tconst auto shadow = Ui::CreateChild<Ui::RpWidget>(inner);\n\t\t\tshadow->paintRequest() | rpl::on_next([=] {\n\t\t\t\tauto p = QPainter(shadow);\n\t\t\t\tp.fillRect(shadow->rect(), st::shadowFg);\n\t\t\t}, shadow->lifetime());\n\t\t\tslider->geometryValue(\n\t\t\t) | rpl::on_next([=](const QRect &r) {\n\t\t\t\tshadow->setGeometry(\n\t\t\t\t\tinner->x(),\n\t\t\t\t\trect::bottom(slider) - st::lineWidth,\n\t\t\t\t\tinner->width(),\n\t\t\t\t\tst::lineWidth);\n\t\t\t\tshadow->show();\n\t\t\t\tshadow->raise();\n\t\t\t}, shadow->lifetime());\n\t\t}\n\n\t\tslider->entity()->addSection(fullTabText);\n\t\tif (hasIn) {\n\t\t\tslider->entity()->addSection(inTabText);\n\t\t}\n\t\tif (hasOut) {\n\t\t\tslider->entity()->addSection(outTabText);\n\t\t}\n\n\t\t{\n\t\t\tconst auto &st = st::creditsHistoryTabsSlider;\n\t\t\tslider->entity()->setNaturalWidth(0\n\t\t\t\t+ st.labelStyle.font->width(fullTabText)\n\t\t\t\t+ (hasIn ? st.labelStyle.font->width(inTabText) : 0)\n\t\t\t\t+ (hasOut ? st.labelStyle.font->width(outTabText) : 0)\n\t\t\t\t+ rect::m::sum::h(st::creditsHistoryTabsSliderPadding));\n\t\t}\n\n\t\tconst auto fullWrap = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\t\tconst auto inWrap = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\t\tconst auto outWrap = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\n\t\trpl::single(0) | rpl::then(\n\t\t\tslider->entity()->sectionActivated()\n\t\t) | rpl::on_next([=](int index) {\n\t\t\tif (index == 0) {\n\t\t\t\tfullWrap->toggle(true, anim::type::instant);\n\t\t\t\tinWrap->toggle(false, anim::type::instant);\n\t\t\t\toutWrap->toggle(false, anim::type::instant);\n\t\t\t} else if (index == 1) {\n\t\t\t\tinWrap->toggle(true, anim::type::instant);\n\t\t\t\tfullWrap->toggle(false, anim::type::instant);\n\t\t\t\toutWrap->toggle(false, anim::type::instant);\n\t\t\t} else {\n\t\t\t\toutWrap->toggle(true, anim::type::instant);\n\t\t\t\tfullWrap->toggle(false, anim::type::instant);\n\t\t\t\tinWrap->toggle(false, anim::type::instant);\n\t\t\t}\n\t\t}, inner->lifetime());\n\n\t\tconst auto window = controller()->parentController();\n\t\tconst auto entryClicked = [=](\n\t\t\t\tconst Data::CreditsHistoryEntry &e,\n\t\t\t\tconst Data::SubscriptionEntry &s) {\n\t\t\twindow->uiShow()->show(Box(\n\t\t\t\tReceiptCreditsBox,\n\t\t\t\twindow,\n\t\t\t\te,\n\t\t\t\ts));\n\t\t};\n\n\t\tInfo::Statistics::AddCreditsHistoryList(\n\t\t\twindow->uiShow(),\n\t\t\tfullSlice,\n\t\t\tfullWrap->entity(),\n\t\t\tentryClicked,\n\t\t\tself,\n\t\t\ttrue,\n\t\t\ttrue);\n\t\tInfo::Statistics::AddCreditsHistoryList(\n\t\t\twindow->uiShow(),\n\t\t\tinSlice,\n\t\t\tinWrap->entity(),\n\t\t\tentryClicked,\n\t\t\tself,\n\t\t\ttrue,\n\t\t\tfalse);\n\t\tInfo::Statistics::AddCreditsHistoryList(\n\t\t\twindow->uiShow(),\n\t\t\toutSlice,\n\t\t\toutWrap->entity(),\n\t\t\tstd::move(entryClicked),\n\t\t\tself,\n\t\t\tfalse,\n\t\t\ttrue);\n\n\t\tUi::AddSkip(inner);\n\t\tUi::AddSkip(inner);\n\n\t\tinner->resizeToWidth(container->width());\n\t};\n\n\tconst auto apiLifetime = content->lifetime().make_state<rpl::lifetime>();\n\t{\n\t\tusing Api = Api::CreditsHistory;\n\t\tconst auto c = (_creditsType == CreditsType::Ton);\n\t\tconst auto apiFull = apiLifetime->make_state<Api>(self, true, true, c);\n\t\tconst auto apiIn = apiLifetime->make_state<Api>(self, true, false, c);\n\t\tconst auto apiOut = apiLifetime->make_state<Api>(self, false, true, c);\n\t\tapiFull->request({}, [=](Data::CreditsStatusSlice fullSlice) {\n\t\t\tapiIn->request({}, [=](Data::CreditsStatusSlice inSlice) {\n\t\t\t\tapiOut->request({}, [=](Data::CreditsStatusSlice outSlice) {\n\t\t\t\t\t::Api::PremiumPeerBot(\n\t\t\t\t\t\t&controller()->session()\n\t\t\t\t\t) | rpl::on_next([=](not_null<PeerData*> bot) {\n\t\t\t\t\t\tfill(bot, fullSlice, inSlice, outSlice);\n\t\t\t\t\t\tapiLifetime->destroy();\n\t\t\t\t\t}, *apiLifetime);\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\t}\n}\n\nvoid Credits::setupSwipeBack() {\n\tusing namespace Ui::Controls;\n\n\tauto swipeBackData = lifetime().make_state<SwipeBackResult>();\n\n\tauto update = [=](SwipeContextData data) {\n\t\tif (data.translation > 0) {\n\t\t\tif (!swipeBackData->callback) {\n\t\t\t\t(*swipeBackData) = SetupSwipeBack(\n\t\t\t\t\tthis,\n\t\t\t\t\t[]() -> std::pair<QColor, QColor> {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tst::historyForwardChooseBg->c,\n\t\t\t\t\t\t\tst::historyForwardChooseFg->c,\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t}\n\t\t\tswipeBackData->callback(data);\n\t\t\treturn;\n\t\t} else if (swipeBackData->lifetime) {\n\t\t\t(*swipeBackData) = {};\n\t\t}\n\t};\n\n\tauto init = [=](int, Qt::LayoutDirection direction) {\n\t\treturn (direction == Qt::RightToLeft)\n\t\t\t? DefaultSwipeBackHandlerFinishData([=] {\n\t\t\t\t_showBack.fire({});\n\t\t\t})\n\t\t\t: SwipeHandlerFinishData();\n\t};\n\n\tSetupSwipeHandler({\n\t\t.widget = this,\n\t\t.scroll = v::null,\n\t\t.update = std::move(update),\n\t\t.init = std::move(init),\n\t});\n}\n\nvoid Credits::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tconst auto isCurrency = _creditsType == CreditsType::Ton;\n\tconst auto statsButton = &_statsButton;\n\tconst auto giftButton = &_giftButton;\n\tconst auto earnButton = &_earnButton;\n\n\tstruct State final {\n\t\tBuyStarsHandler buyStars;\n\t};\n\tconst auto state = content->lifetime().make_state<State>();\n\n\t{\n\t\tconst auto button = content->add(\n\t\t\tobject_ptr<Ui::RoundButton>(\n\t\t\t\tcontent,\n\t\t\t\tnullptr,\n\t\t\t\tst::creditsSettingsBigBalanceButton),\n\t\t\tst::boxRowPadding,\n\t\t\tstyle::al_top);\n\t\tbutton->setContext([&]() -> Ui::Text::MarkedContext {\n\t\t\tauto customEmojiFactory = [=](const auto &...) {\n\t\t\t\tconst auto &icon = st::settingsIconAdd;\n\t\t\t\tauto image = QImage(\n\t\t\t\t\t(icon.size() + QSize(st::lineWidth * 4, 0))\n\t\t\t\t\t\t* style::DevicePixelRatio(),\n\t\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t\tconst auto r = Rect(icon.size()) - Margins(st::lineWidth * 2);\n\t\t\t\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\t\timage.fill(Qt::transparent);\n\t\t\t\t{\n\t\t\t\t\tauto p = QPainter(&image);\n\t\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\t\tp.setBrush(st::activeButtonFg);\n\t\t\t\t\tp.drawEllipse(r);\n\t\t\t\t\ticon.paintInCenter(p, r, st::windowBgActive->c);\n\t\t\t\t}\n\t\t\t\treturn std::make_unique<Ui::CustomEmoji::Internal>(\n\t\t\t\t\tu\"topup_button\"_q,\n\t\t\t\t\tstd::move(image));\n\t\t\t};\n\t\t\treturn { .customEmojiFactory = std::move(customEmojiFactory) };\n\t\t}());\n\t\tbutton->setText(\n\t\t\trpl::conditional(\n\t\t\t\tstate->buyStars.loadingValue(),\n\t\t\t\trpl::single(TextWithEntities()),\n\t\t\t\tisCurrency\n\t\t\t\t\t? tr::lng_credits_currency_summary_in_button(\n\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t: tr::lng_credits_topup_button(\n\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\trpl::single(Ui::Text::SingleCustomEmoji(u\"+\"_q)),\n\t\t\t\t\t\ttr::marked)));\n\t\tconst auto show = controller()->uiShow();\n\t\tif (isCurrency) {\n\t\t\tconst auto url = tr::lng_suggest_low_ton_fragment_url(tr::now);\n\t\t\tbutton->setClickedCallback([=] { UrlClickHandler::Open(url); });\n\t\t} else {\n\t\t\tconst auto paid = [parent = QPointer<QWidget>(_parent)] {\n\t\t\t\tif (parent) {\n\t\t\t\t\tUi::StartFireworks(parent);\n\t\t\t\t}\n\t\t\t};\n\t\t\tbutton->setClickedCallback(state->buyStars.handler(show, paid));\n\t\t}\n\t\t{\n\t\t\tusing namespace Info::Statistics;\n\t\t\tconst auto loadingAnimation = InfiniteRadialAnimationWidget(\n\t\t\t\tbutton,\n\t\t\t\tbutton->height() / 2);\n\t\t\tAddChildToWidgetCenter(button, loadingAnimation);\n\t\t\tloadingAnimation->showOn(state->buyStars.loadingValue());\n\t\t}\n\t}\n\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content, st::lineWidth);\n\n\tconst auto &textSt = st::creditsPremiumCover.about;\n\tauto context = [&]() -> Ui::Text::MarkedContext {\n\t\tconst auto height = textSt.style.font->height;\n\t\tauto customEmojiFactory = [=](const auto &...) {\n\t\t\treturn std::make_unique<Ui::Text::ShiftedEmoji>(\n\t\t\t\tisCurrency\n\t\t\t\t\t? std::make_unique<Ui::CustomEmoji::Internal>(\n\t\t\t\t\t\tu\"currency_icon:%1\"_q.arg(height),\n\t\t\t\t\t\tUi::Earn::IconCurrencyColored(\n\t\t\t\t\t\t\tst::tonFieldIconSize,\n\t\t\t\t\t\t\tst::currencyFg->c))\n\t\t\t\t\t: Ui::MakeCreditsIconEmoji(height, 1),\n\t\t\t\tisCurrency\n\t\t\t\t\t? QPoint(0, st::lineWidth * 2)\n\t\t\t\t\t: QPoint(-st::lineWidth, st::lineWidth));\n\t\t};\n\t\treturn { .customEmojiFactory = std::move(customEmojiFactory) };\n\t}();\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_credits_balance_me_count(\n\t\t\t\tlt_emoji,\n\t\t\t\trpl::single(Ui::MakeCreditsIconEntity()),\n\t\t\t\tlt_amount,\n\t\t\t\t(isCurrency\n\t\t\t\t\t? controller()->session().credits().tonBalanceValue()\n\t\t\t\t\t: controller()->session().credits().balanceValue()\n\t\t\t\t) | rpl::map(\n\t\t\t\t\tLang::FormatCreditsAmountDecimal\n\t\t\t\t) | rpl::map(tr::bold),\n\t\t\t\ttr::marked),\n\t\t\ttextSt,\n\t\t\tst::defaultPopupMenu,\n\t\t\tstd::move(context)),\n\t\tstyle::al_top);\n\tif (isCurrency) {\n\t\tconst auto rate = controller()->session().credits().usdRate();\n\t\tconst auto wrap = content->add(\n\t\t\tobject_ptr<Ui::SlideWrap<>>(\n\t\t\t\tcontent,\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tcontent,\n\t\t\t\t\tcontroller()->session().credits().tonBalanceValue(\n\t\t\t\t\t) | rpl::map([=](CreditsAmount value) {\n\t\t\t\t\t\tusing namespace Info::ChannelEarn;\n\t\t\t\t\t\treturn value ? ToUsd(value, rate, 3) : QString();\n\t\t\t\t\t}),\n\t\t\t\t\tst::channelEarnOverviewSubMinorLabel)),\n\t\t\tstyle::al_top);\n\t\twrap->toggleOn(controller()->session().credits().tonBalanceValue(\n\t\t\t) | rpl::map(rpl::mappers::_1 > CreditsAmount(0)));\n\t\twrap->finishAnimating();\n\t}\n\tUi::AddSkip(content, st::lineWidth);\n\tUi::AddSkip(content, st::lineWidth);\n\tUi::AddSkip(content);\n\n\tUi::AddSkip(content);\n\tif (isCurrency) {\n\t\tUi::AddDividerText(\n\t\t\tcontent,\n\t\t\ttr::lng_credits_currency_summary_in_subtitle());\n\t} else {\n\t\tUi::AddDivider(content);\n\t}\n\tUi::AddSkip(content, st::lineWidth * 4);\n\n\tconst auto setupSubs = [=](not_null<Ui::VerticalLayout*> container) {\n\t\tsetupSubscriptions(container);\n\t};\n\tconst auto setupHist = [=](not_null<Ui::VerticalLayout*> container) {\n\t\tsetupHistory(container);\n\t};\n\n\tconst SectionBuildMethod buildMethod = [=](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tconst auto isPaused = Window::PausedIn(\n\t\t\tcontroller,\n\t\t\tWindow::GifPauseReason::Layer);\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = isPaused,\n\t\t});\n\n\t\tBuildCreditsSectionContent(\n\t\t\tbuilder,\n\t\t\tisCurrency,\n\t\t\tstatsButton,\n\t\t\tgiftButton,\n\t\t\tearnButton,\n\t\t\tsetupSubs,\n\t\t\tsetupHist);\n\t};\n\n\tbuild(content, buildMethod);\n\n\tUi::ResizeFitChild(this, content);\n}\n\nbase::weak_qptr<Ui::RpWidget> Credits::createPinnedToTop(\n\t\tnot_null<QWidget*> parent) {\n\t_parent = parent;\n\tconst auto isCurrency = _creditsType == CreditsType::Ton;\n\n\tconst auto content = [&]() -> Ui::Premium::TopBarAbstract* {\n\t\tconst auto weak = base::make_weak(controller());\n\t\tconst auto clickContextOther = [=] {\n\t\t\treturn QVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.sessionWindow = weak,\n\t\t\t\t.botStartAutoSubmit = true,\n\t\t\t});\n\t\t};\n\t\treturn Ui::CreateChild<Ui::Premium::TopBar>(\n\t\t\tparent.get(),\n\t\t\tst::creditsPremiumCover,\n\t\t\tUi::Premium::TopBarDescriptor{\n\t\t\t\t.clickContextOther = clickContextOther,\n\t\t\t\t.logo = isCurrency ? u\"diamond\"_q : QString(),\n\t\t\t\t.title = title(),\n\t\t\t\t.about = (isCurrency\n\t\t\t\t\t? tr::lng_credits_currency_summary_about\n\t\t\t\t\t: tr::lng_credits_summary_about)(\n\t\t\t\t\t\tTextWithEntities::Simple),\n\t\t\t\t.light = true,\n\t\t\t\t.gradientStops = Ui::Premium::CreditsIconGradientStops(),\n\t\t\t});\n\t}();\n\t_setPaused = [=](bool paused) {\n\t\tcontent->setPaused(paused);\n\t};\n\n\t_wrap.value(\n\t) | rpl::on_next([=](Info::Wrap wrap) {\n\t\tcontent->setRoundEdges(wrap == Info::Wrap::Layer);\n\t}, content->lifetime());\n\n\tcontent->setMaximumHeight(st::settingsPremiumTopHeight);\n\tcontent->setMinimumHeight(st::infoLayerTopBarHeight);\n\n\tcontent->resize(content->width(), content->maximumHeight());\n\tcontent->additionalHeight(\n\t) | rpl::on_next([=](int additionalHeight) {\n\t\tconst auto wasMax = (content->height() == content->maximumHeight());\n\t\tcontent->setMaximumHeight(st::settingsPremiumTopHeight\n\t\t\t+ additionalHeight);\n\t\tif (wasMax) {\n\t\t\tcontent->resize(content->width(), content->maximumHeight());\n\t\t}\n\t}, content->lifetime());\n\n\t{\n\t\tconst auto balance = AddBalanceWidget(\n\t\t\tcontent,\n\t\t\t&controller()->session(),\n\t\t\tisCurrency\n\t\t\t\t? controller()->session().credits().tonBalanceValue()\n\t\t\t\t: controller()->session().credits().balanceValue(),\n\t\t\ttrue,\n\t\t\tcontent->heightValue() | rpl::map([=](int height) {\n\t\t\t\tconst auto ratio = float64(height - content->minimumHeight())\n\t\t\t\t\t/ (content->maximumHeight() - content->minimumHeight());\n\t\t\t\treturn (1. - ratio / 0.35);\n\t\t\t}));\n\t\tcontroller()->session().credits().load(true);\n\t\trpl::combine(\n\t\t\tbalance->sizeValue(),\n\t\t\tcontent->sizeValue()\n\t\t) | rpl::on_next([=](const QSize &, const QSize &) {\n\t\t\tbalance->moveToRight(\n\t\t\t\t(_close\n\t\t\t\t\t? _close->width() + st::creditsHistoryRightSkip\n\t\t\t\t\t: st::creditsHistoryRightSkip * 2),\n\t\t\t\tst::creditsHistoryRightSkip);\n\t\t\tbalance->update();\n\t\t}, balance->lifetime());\n\t}\n\n\t_wrap.value(\n\t) | rpl::on_next([=](Info::Wrap wrap) {\n\t\tconst auto isLayer = (wrap == Info::Wrap::Layer);\n\t\t_back = base::make_unique_q<Ui::FadeWrap<Ui::IconButton>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::IconButton>(\n\t\t\t\tcontent,\n\t\t\t\t(isLayer ? st::infoTopBarBack : st::infoLayerTopBarBack)),\n\t\t\tst::infoTopBarScale);\n\t\t_back->setDuration(0);\n\t\t_back->toggleOn(isLayer\n\t\t\t? _backToggles.value() | rpl::type_erased\n\t\t\t: rpl::single(true));\n\t\t_back->entity()->addClickHandler([=] {\n\t\t\t_showBack.fire({});\n\t\t});\n\t\t_back->toggledValue(\n\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\tconst auto &st = isLayer ? st::infoLayerTopBar : st::infoTopBar;\n\t\t\tcontent->setTextPosition(\n\t\t\t\ttoggled ? st.back.width : st.titlePosition.x(),\n\t\t\t\tst.titlePosition.y());\n\t\t}, _back->lifetime());\n\n\t\tif (!isLayer) {\n\t\t\t_close = nullptr;\n\t\t} else {\n\t\t\t_close = base::make_unique_q<Ui::IconButton>(\n\t\t\t\tcontent,\n\t\t\t\tst::infoTopBarClose);\n\t\t\t_close->addClickHandler([=] {\n\t\t\t\tcontroller()->parentController()->hideLayer();\n\t\t\t\tcontroller()->parentController()->hideSpecialLayer();\n\t\t\t});\n\t\t\tcontent->widthValue(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\t_close->moveToRight(0, 0);\n\t\t\t}, _close->lifetime());\n\t\t}\n\t}, content->lifetime());\n\n\treturn base::make_weak(not_null<Ui::RpWidget*>{ content });\n}\n\nvoid Credits::showFinished() {\n\t_showFinished.fire({});\n\tcontroller()->checkHighlightControl(u\"stars/stats\"_q, _statsButton);\n\tcontroller()->checkHighlightControl(u\"stars/gift\"_q, _giftButton);\n\tcontroller()->checkHighlightControl(u\"stars/earn\"_q, _earnButton);\n}\n\nclass Currency {\n};\n\nvoid BuildCurrencyWithdrawalSection(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tnot_null<Window::SessionController*> controller) {\n\tusing namespace Info::ChannelEarn;\n\tconst auto self = controller->session().user();\n\n\tconst auto fill = [=](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tCreditsAmount value,\n\t\t\tfloat64 multiplier) {\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\n\t\tconst auto labels = container->add(\n\t\t\tobject_ptr<Ui::RpWidget>(container),\n\t\t\tstyle::al_top);\n\n\t\tconst auto majorLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tlabels,\n\t\t\tst::channelEarnBalanceMajorLabel);\n\t\t{\n\t\t\tconst auto &m = st::channelEarnCurrencyCommonMargins;\n\t\t\tconst auto p = QMargins(\n\t\t\t\tm.left(),\n\t\t\t\t-m.top(),\n\t\t\t\tm.right(),\n\t\t\t\tm.bottom());\n\t\t\tAddEmojiToMajor(majorLabel, rpl::single(value), {}, p);\n\t\t}\n\t\tmajorLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tconst auto minorLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tlabels,\n\t\t\tMinorPart(value),\n\t\t\tst::channelEarnBalanceMinorLabel);\n\t\tminorLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\trpl::combine(\n\t\t\tmajorLabel->sizeValue(),\n\t\t\tminorLabel->sizeValue()\n\t\t) | rpl::on_next([=](\n\t\t\t\tconst QSize &majorSize,\n\t\t\t\tconst QSize &minorSize) {\n\t\t\tlabels->resize(\n\t\t\t\tmajorSize.width() + minorSize.width(),\n\t\t\t\tmajorSize.height());\n\t\t\tlabels->setNaturalWidth(\n\t\t\t\tmajorSize.width() + minorSize.width());\n\t\t\tmajorLabel->moveToLeft(0, 0);\n\t\t\tminorLabel->moveToRight(\n\t\t\t\t0,\n\t\t\t\tst::channelEarnBalanceMinorLabelSkip);\n\t\t}, labels->lifetime());\n\t\tUi::ToggleChildrenVisibility(labels, true);\n\n\t\tUi::AddSkip(container);\n\t\tcontainer->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\tToUsd(value, multiplier, 0),\n\t\t\t\tst::channelEarnOverviewSubMinorLabel),\n\t\t\tstyle::al_top);\n\n\t\tUi::AddSkip(container);\n\n\t\tconst auto &stButton = st::creditsSettingsBigBalanceButton;\n\t\tconst auto button = container->add(\n\t\t\tobject_ptr<Ui::RoundButton>(\n\t\t\t\tcontainer,\n\t\t\t\trpl::never<QString>(),\n\t\t\t\tstButton),\n\t\t\tst::boxRowPadding,\n\t\t\tstyle::al_top);\n\t\tbutton->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\n\t\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tbutton,\n\t\t\ttr::lng_channel_earn_balance_button(tr::now),\n\t\t\tst::channelEarnSemiboldLabel);\n\t\tlabel->setTextColorOverride(stButton.textFg->c);\n\t\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\trpl::combine(\n\t\t\tbutton->sizeValue(),\n\t\t\tlabel->sizeValue()\n\t\t) | rpl::on_next([=](const QSize &b, const QSize &l) {\n\t\t\tlabel->moveToLeft(\n\t\t\t\t(b.width() - l.width()) / 2,\n\t\t\t\t(b.height() - l.height()) / 2);\n\t\t}, label->lifetime());\n\n\t\tconst auto colorText = [=](float64 v) {\n\t\t\tlabel->setTextColorOverride(\n\t\t\t\tanim::with_alpha(\n\t\t\t\t\tstButton.textFg->c,\n\t\t\t\t\tanim::interpolateF(.5, 1., v)));\n\t\t};\n\t\tconst auto withdrawalEnabled = true;\n\t\tcolorText(withdrawalEnabled ? 1. : 0.);\n\t\tbutton->setAttribute(\n\t\t\tQt::WA_TransparentForMouseEvents,\n\t\t\t!withdrawalEnabled);\n\n\t\tApi::HandleWithdrawalButton(\n\t\t\t{ .currencyReceiver = self },\n\t\t\tbutton,\n\t\t\tcontroller->uiShow());\n\t\tUi::ToggleChildrenVisibility(button, true);\n\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\tUi::AddSkip(container);\n\t\tUi::AddDividerText(\n\t\t\tcontainer,\n\t\t\ttr::lng_credits_currency_summary_subtitle());\n\t\tUi::AddSkip(container);\n\t};\n\n\tconst auto wrap = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::VerticalLayout>(content)));\n\tconst auto apiLifetime = wrap->lifetime().make_state<rpl::lifetime>();\n\tconst auto api = apiLifetime->make_state<Api::EarnStatistics>(self);\n\twrap->toggle(false, anim::type::instant);\n\tapi->request() | rpl::on_error_done([] {\n\t}, [=] {\n\t\tif (!api->data().availableBalance.empty()) {\n\t\t\twrap->toggle(true, anim::type::normal);\n\t\t\tfill(\n\t\t\t\twrap->entity(),\n\t\t\t\tapi->data().availableBalance,\n\t\t\t\tapi->data().usdRate);\n\t\t\tcontent->resizeToWidth(content->width());\n\t\t}\n\t}, *apiLifetime);\n}\n\nvoid BuildCreditsButtons(\n\t\tSectionBuilder &builder,\n\t\tbool isCurrency,\n\t\tQPointer<Ui::SettingsButton> *statsButton,\n\t\tQPointer<Ui::SettingsButton> *giftButton,\n\t\tQPointer<Ui::SettingsButton> *earnButton) {\n\tconst auto session = builder.session();\n\tconst auto controller = builder.controller();\n\tconst auto self = session->user();\n\n\tif (!isCurrency) {\n\t\tauto statsShown = session->credits().loadedValue(\n\t\t) | rpl::map([session] {\n\t\t\treturn session->credits().statsEnabled();\n\t\t});\n\t\tconst auto stats = builder.addButton({\n\t\t\t.id = u\"stars/stats\"_q,\n\t\t\t.title = tr::lng_credits_stats_button(),\n\t\t\t.st = &st::settingsCreditsButton,\n\t\t\t.icon = { &st::menuIconStats },\n\t\t\t.onClick = [controller, self] {\n\t\t\t\tcontroller->parentController()->showSection(\n\t\t\t\t\tInfo::BotEarn::Make(self));\n\t\t\t},\n\t\t\t.keywords = { u\"statistics\"_q },\n\t\t\t.shown = std::move(statsShown),\n\t\t});\n\t\tif (statsButton) {\n\t\t\t*statsButton = stats;\n\t\t}\n\t}\n\n\tif (!isCurrency) {\n\t\tconst auto gift = builder.addButton({\n\t\t\t.id = u\"stars/gift\"_q,\n\t\t\t.title = tr::lng_credits_gift_button(),\n\t\t\t.st = &st::settingsCreditsButton,\n\t\t\t.icon = { &st::settingsButtonIconGift },\n\t\t\t.onClick = [controller] {\n\t\t\t\tconst auto window = controller->parentController();\n\t\t\t\tUi::ShowGiftCreditsBox(window, nullptr);\n\t\t\t},\n\t\t\t.keywords = { u\"send\"_q, u\"stars\"_q },\n\t\t});\n\t\tif (giftButton) {\n\t\t\t*giftButton = gift;\n\t\t}\n\t}\n\n\tif (!isCurrency && Info::BotStarRef::Join::Allowed(self)) {\n\t\tconst auto earn = builder.addButton({\n\t\t\t.id = u\"stars/earn\"_q,\n\t\t\t.title = tr::lng_credits_earn_button(),\n\t\t\t.st = &st::settingsCreditsButton,\n\t\t\t.icon = { &st::settingsButtonIconEarn },\n\t\t\t.onClick = [controller, self] {\n\t\t\t\tcontroller->parentController()->showSection(\n\t\t\t\t\tInfo::BotStarRef::Join::Make(self));\n\t\t\t},\n\t\t\t.keywords = { u\"affiliate\"_q, u\"referral\"_q },\n\t\t});\n\t\tif (earnButton) {\n\t\t\t*earnButton = earn;\n\t\t}\n\t}\n}\n\nvoid BuildCreditsSectionContent(\n\t\tSectionBuilder &builder,\n\t\tbool isCurrency,\n\t\tQPointer<Ui::SettingsButton> *statsButton,\n\t\tQPointer<Ui::SettingsButton> *giftButton,\n\t\tQPointer<Ui::SettingsButton> *earnButton,\n\t\tFn<void(not_null<Ui::VerticalLayout*>)> setupSubscriptions,\n\t\tFn<void(not_null<Ui::VerticalLayout*>)> setupHistory) {\n\tBuildCreditsButtons(builder, isCurrency, statsButton, giftButton, earnButton);\n\n\tif (isCurrency) {\n\t\tbuilder.add([](const WidgetContext &ctx) {\n\t\t\tBuildCurrencyWithdrawalSection(ctx.container, ctx.controller);\n\t\t\treturn SectionBuilder::WidgetToAdd{};\n\t\t});\n\t}\n\n\tif (!isCurrency) {\n\t\tbuilder.add([=](const WidgetContext &ctx) {\n\t\t\tUi::AddSkip(ctx.container, st::lineWidth * 4);\n\t\t\tUi::AddDivider(ctx.container);\n\t\t\tsetupSubscriptions(ctx.container);\n\t\t\treturn SectionBuilder::WidgetToAdd{};\n\t\t});\n\t}\n\n\tbuilder.add([=](const WidgetContext &ctx) {\n\t\tsetupHistory(ctx.container);\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n}\n\nconst auto kCreditsBuilderMeta = BuildHelper({\n\t.id = Credits::Id(),\n\t.parentId = MainId(),\n\t.title = &tr::lng_credits_summary_title,\n\t.icon = &st::menuIconPremium,\n}, [](SectionBuilder &builder) {\n\tBuildCreditsButtons(builder, false, nullptr, nullptr, nullptr);\n});\n\n} // namespace\n\ntemplate <>\nstruct SectionFactory<Credits> : AbstractSectionFactory {\n\tobject_ptr<AbstractSection> create(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\trpl::producer<Container> containerValue\n\t) const final override {\n\t\treturn object_ptr<Credits>(parent, controller, CreditsType::Stars);\n\t}\n\tbool hasCustomTopBar() const final override {\n\t\treturn true;\n\t}\n\n\t[[nodiscard]] static const std::shared_ptr<SectionFactory> &Instance() {\n\t\tstatic const auto result = std::make_shared<SectionFactory>();\n\t\treturn result;\n\t}\n};\n\ntemplate <>\nstruct SectionFactory<Currency> : AbstractSectionFactory {\n\tobject_ptr<AbstractSection> create(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\trpl::producer<Container> containerValue\n\t) const final override {\n\t\treturn object_ptr<Credits>(parent, controller, CreditsType::Ton);\n\t}\n\tbool hasCustomTopBar() const final override {\n\t\treturn true;\n\t}\n\n\t[[nodiscard]] static const std::shared_ptr<SectionFactory> &Instance() {\n\t\tstatic const auto result = std::make_shared<SectionFactory>();\n\t\treturn result;\n\t}\n};\n\nType CreditsId() {\n\treturn Credits::Id();\n}\n\nType CurrencyId() {\n\treturn SectionFactory<Currency>::Instance();\n}\n\nBuyStarsHandler::BuyStarsHandler() = default;\n\nBuyStarsHandler::~BuyStarsHandler() = default;\n\nFn<void()> BuyStarsHandler::handler(\n\t\tstd::shared_ptr<::Main::SessionShow> show,\n\t\tFn<void()> paid) {\n\tconst auto optionsBox = [=](not_null<Ui::GenericBox*> box) {\n\t\tbox->setStyle(st::giveawayGiftCodeBox);\n\t\tbox->setWidth(st::boxWideWidth);\n\t\tbox->setTitle(tr::lng_credits_summary_options_subtitle());\n\t\tconst auto inner = box->verticalLayout();\n\t\tconst auto self = show->session().user();\n\t\tconst auto options = _api\n\t\t\t? _api->options()\n\t\t\t: Data::CreditTopupOptions();\n\t\tconst auto amount = CreditsAmount();\n\t\tconst auto weak = base::make_weak(box);\n\t\tFillCreditOptions(show, inner, self, amount, [=] {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t\tif (const auto onstack = paid) {\n\t\t\t\tonstack();\n\t\t\t}\n\t\t}, box->showFinishes(), nullptr, options);\n\n\t\tbox->addButton(tr::lng_close(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t};\n\treturn crl::guard(this, [=] {\n\t\tif (_api && !_api->options().empty()) {\n\t\t\t_loading = false;\n\t\t\tshow->show(Box(crl::guard(this, optionsBox)));\n\t\t} else {\n\t\t\t_loading = true;\n\t\t\tconst auto user = show->session().user();\n\t\t\t_api = std::make_unique<Api::CreditsTopupOptions>(user);\n\t\t\t_api->request(\n\t\t\t) | rpl::on_error_done([=](const QString &error) {\n\t\t\t\t_loading = false;\n\t\t\t\tshow->showToast(error);\n\t\t\t}, [=] {\n\t\t\t\t_loading = false;\n\t\t\t\tshow->show(Box(crl::guard(this, optionsBox)));\n\t\t\t}, _lifetime);\n\t\t}\n\t});\n}\n\nrpl::producer<bool> BuyStarsHandler::loadingValue() const {\n\treturn _loading.value();\n}\n\nnamespace Builder {\n\nSectionBuildMethod CreditsSection = kCreditsBuilderMeta.build;\n\n} // namespace Builder\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_credits.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"settings/settings_type.h\"\n\nnamespace Api {\nclass CreditsTopupOptions;\n} // namespace Api\n\nnamespace Main {\nclass SessionShow;\n} // namespace Main\n\nnamespace Settings {\n\n[[nodiscard]] Type CreditsId();\n[[nodiscard]] Type CurrencyId();\n\nclass BuyStarsHandler final : public base::has_weak_ptr {\npublic:\n\tBuyStarsHandler();\n\t~BuyStarsHandler();\n\n\t[[nodiscard]] Fn<void()> handler(\n\t\tstd::shared_ptr<::Main::SessionShow> show,\n\t\tFn<void()> paid = nullptr);\n\t[[nodiscard]] rpl::producer<bool> loadingValue() const;\n\nprivate:\n\tstd::unique_ptr<Api::CreditsTopupOptions> _api;\n\trpl::variable<bool> _loading;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_folders.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_folders.h\"\n\n#include \"api/api_chat_filters.h\"\n#include \"apiwrap.h\"\n#include \"boxes/filters/edit_filter_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"core/application.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/filter_icons.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/box_content_divider.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nusing namespace Builder;\nusing Flag = Data::ChatFilter::Flag;\nusing Flags = Data::ChatFilter::Flags;\n\nclass FilterRowButton final : public Ui::RippleButton {\npublic:\n\tFilterRowButton(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<::Main::Session*> session,\n\t\tconst Data::ChatFilter &filter,\n\t\tconst QString &description = {});\n\n\tvoid setRemoved(bool removed);\n\tvoid updateData(\n\t\tconst Data::ChatFilter &filter,\n\t\tbool ignoreCount = false);\n\tvoid updateCount(const Data::ChatFilter &filter);\n\n\t[[nodiscard]] rpl::producer<> removeRequests() const;\n\t[[nodiscard]] rpl::producer<> restoreRequests() const;\n\t[[nodiscard]] rpl::producer<> addRequests() const;\n\n\tvoid setColorIndexProgress(float64 progress);\n\nprivate:\n\tenum class State {\n\t\tSuggested,\n\t\tRemoved,\n\t\tNormal,\n\t};\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid setup(const Data::ChatFilter &filter, const QString &status);\n\tvoid setState(State state, bool force = false);\n\tvoid updateButtonsVisibility();\n\n\tconst not_null<::Main::Session*> _session;\n\n\tUi::IconButton _remove;\n\tUi::RoundButton _restore;\n\tUi::RoundButton _add;\n\n\tUi::Text::String _title;\n\tQString _status;\n\tUi::FilterIcon _icon = Ui::FilterIcon();\n\tstd::optional<uint8> _colorIndex;\n\tfloat64 _colorIndexProgress = 1.;\n\n\tState _state = State::Normal;\n\n};\n\nstruct FilterRow {\n\tnot_null<FilterRowButton*> button;\n\tData::ChatFilter filter;\n\tbool removed = false;\n\tmtpRequestId removePeersRequestId = 0;\n\tstd::vector<not_null<PeerData*>> suggestRemovePeers;\n\tstd::vector<not_null<PeerData*>> removePeers;\n\tbool added = false;\n\tbool postponedCountUpdate = false;\n};\n\n[[nodiscard]] int CountFilterChats(\n\t\tnot_null<::Main::Session*> session,\n\t\tconst Data::ChatFilter &filter) {\n\tauto result = 0;\n\tconst auto addList = [&](not_null<Dialogs::MainList*> list) {\n\t\tfor (const auto &entry : list->indexed()->all()) {\n\t\t\tif (const auto history = entry->history()) {\n\t\t\t\tif (filter.contains(history)) {\n\t\t\t\t\t++result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\taddList(session->data().chatsList());\n\tconst auto folderId = Data::Folder::kId;\n\tif (const auto folder = session->data().folderLoaded(folderId)) {\n\t\taddList(folder->chatsList());\n\t}\n\treturn result;\n}\n\n[[nodiscard]] int ComputeCount(\n\t\tnot_null<::Main::Session*> session,\n\t\tconst Data::ChatFilter &filter,\n\t\tbool check = false) {\n\tconst auto &list = session->data().chatsFilters().list();\n\tconst auto id = filter.id();\n\tconst auto i = ranges::find(list, id, &Data::ChatFilter::id);\n\tif ((id && i != end(list))\n\t\t&& (!check\n\t\t\t|| (i->flags() == filter.flags()\n\t\t\t\t&& i->always() == filter.always()\n\t\t\t\t&& i->never() == filter.never()))) {\n\t\tconst auto chats = session->data().chatsFilters().chatsList(id);\n\t\treturn chats->indexed()->size();\n\t}\n\treturn CountFilterChats(session, filter);\n}\n\n[[nodiscard]] QString ComputeCountString(\n\t\tnot_null<::Main::Session*> session,\n\t\tconst Data::ChatFilter &filter,\n\t\tbool check = false) {\n\tconst auto count = ComputeCount(session, filter, check);\n\tconst auto result = count\n\t\t? tr::lng_filters_chats_count(tr::now, lt_count_short, count)\n\t\t: tr::lng_filters_no_chats(tr::now);\n\treturn filter.chatlist()\n\t\t? (result\n\t\t\t+ (' ' + Ui::kQBullet + ' ')\n\t\t\t+ tr::lng_filters_shareable_status(tr::now))\n\t\t: result;\n}\n\nFilterRowButton::FilterRowButton(\n\tnot_null<QWidget*> parent,\n\tnot_null<::Main::Session*> session,\n\tconst Data::ChatFilter &filter,\n\tconst QString &description)\n: RippleButton(parent, st::defaultRippleAnimation)\n, _session(session)\n, _remove(this, st::filtersRemove)\n, _restore(this, tr::lng_filters_restore(), st::settingsFilterAddRecommended)\n, _add(\n\tthis,\n\ttr::lng_filters_recommended_add(),\n\tst::settingsFilterAddRecommended)\n, _state(description.isEmpty() ? State::Normal : State::Suggested) {\n\t_restore.setFullRadius(true);\n\t_add.setFullRadius(true);\n\tsetup(filter, description.isEmpty()\n\t\t? ComputeCountString(session, filter)\n\t\t: description);\n}\n\nvoid FilterRowButton::setRemoved(bool removed) {\n\tsetState(removed ? State::Removed : State::Normal);\n}\n\nvoid FilterRowButton::updateData(\n\t\tconst Data::ChatFilter &filter,\n\t\tbool ignoreCount) {\n\tExpects(_session != nullptr);\n\n\tconst auto title = filter.title();\n\t_title.setMarkedText(\n\t\tst::contactsNameStyle,\n\t\ttitle.text,\n\t\tkMarkupTextOptions,\n\t\tCore::TextContext({\n\t\t\t.session = _session,\n\t\t\t.repaint = [=] { update(); },\n\t\t\t.customEmojiLoopLimit = title.isStatic ? -1 : 0,\n\t\t}));\n\t_icon = Ui::ComputeFilterIcon(filter);\n\t_colorIndex = filter.colorIndex();\n\tif (!ignoreCount) {\n\t\tupdateCount(filter);\n\t}\n}\n\nvoid FilterRowButton::updateCount(const Data::ChatFilter &filter) {\n\t_status = ComputeCountString(_session, filter, true);\n\tupdate();\n}\n\nvoid FilterRowButton::setState(State state, bool force) {\n\tif (!force && _state == state) {\n\t\treturn;\n\t}\n\t_state = state;\n\tsetPointerCursor(_state == State::Normal);\n\tsetDisabled(_state != State::Normal);\n\tupdateButtonsVisibility();\n\tupdate();\n}\n\nvoid FilterRowButton::setup(\n\t\tconst Data::ChatFilter &filter,\n\t\tconst QString &status) {\n\tresize(width(), st::defaultPeerListItem.height);\n\n\t_status = status;\n\tupdateData(filter, true);\n\tsetState(_state, true);\n\n\tsizeValue() | rpl::on_next([=](QSize size) {\n\t\tconst auto right = st::contactsPadding.right()\n\t\t\t+ st::contactsCheckPosition.x();\n\t\tconst auto width = size.width();\n\t\tconst auto height = size.height();\n\t\t_restore.moveToRight(right, (height - _restore.height()) / 2, width);\n\t\t_add.moveToRight(right, (height - _add.height()) / 2, width);\n\t\tconst auto skipped = right - st::stickersRemoveSkip;\n\t\t_remove.moveToRight(skipped, (height - _remove.height()) / 2, width);\n\t}, lifetime());\n}\n\nvoid FilterRowButton::updateButtonsVisibility() {\n\t_remove.setVisible(_state == State::Normal);\n\t_restore.setVisible(_state == State::Removed);\n\t_add.setVisible(_state == State::Suggested);\n}\n\nrpl::producer<> FilterRowButton::removeRequests() const {\n\treturn _remove.clicks() | rpl::to_empty;\n}\n\nrpl::producer<> FilterRowButton::restoreRequests() const {\n\treturn _restore.clicks() | rpl::to_empty;\n}\n\nrpl::producer<> FilterRowButton::addRequests() const {\n\treturn _add.clicks() | rpl::to_empty;\n}\n\nvoid FilterRowButton::setColorIndexProgress(float64 progress) {\n\t_colorIndexProgress = progress;\n\tif (_colorIndex) {\n\t\tupdate();\n\t}\n}\n\nvoid FilterRowButton::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\n\tconst auto over = isOver() || isDown();\n\tif (_state == State::Normal) {\n\t\tif (over) {\n\t\t\tp.fillRect(e->rect(), st::windowBgOver);\n\t\t}\n\t\tRippleButton::paintRipple(p, 0, 0);\n\n\t\tif (_colorIndex) {\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(Ui::EmptyUserpic::UserpicColor(*_colorIndex).color2);\n\t\t\tconst auto w = height() / 3;\n\t\t\tconst auto rect = QRect(\n\t\t\t\t_remove.x() - w - st::contactsCheckPosition.x(),\n\t\t\t\t(height() - w) / 2,\n\t\t\t\tw,\n\t\t\t\tw);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.drawEllipse(rect - Margins((1. - _colorIndexProgress) * w / 2));\n\t\t}\n\t} else if (_state == State::Removed) {\n\t\tp.setOpacity(st::stickersRowDisabledOpacity);\n\t}\n\n\tconst auto left = (_state == State::Suggested)\n\t\t? st::defaultSubsectionTitlePadding.left()\n\t\t: st::settingsButtonActive.padding.left();\n\tconst auto buttonsLeft = std::min(\n\t\t_add.x(),\n\t\tstd::min(_remove.x(), _restore.x()));\n\tconst auto availableWidth = buttonsLeft - left;\n\n\tp.setPen(st::contactsNameFg);\n\t_title.drawLeftElided(\n\t\tp,\n\t\tleft,\n\t\tst::contactsPadding.top() + st::contactsNameTop,\n\t\tavailableWidth,\n\t\twidth());\n\n\tp.setFont(st::contactsStatusFont);\n\tp.setPen(st::contactsStatusFg);\n\tp.drawTextLeft(\n\t\tleft,\n\t\tst::contactsPadding.top() + st::contactsStatusTop,\n\t\twidth(),\n\t\t_status);\n\n\tif (_state != State::Suggested) {\n\t\tconst auto icon = Ui::LookupFilterIcon(_icon).normal;\n\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto iconWidth = icon->width() - style::ConvertScale(9);\n\t\tconst auto scale = st::settingsIconAdd.width() / float64(iconWidth);\n\t\tp.translate(\n\t\t\tst::settingsButtonActive.iconLeft,\n\t\t\t(height() - icon->height() * scale) / 2);\n\t\tp.translate(-iconWidth / 2, -iconWidth / 2);\n\t\tp.scale(scale, scale);\n\t\tp.translate(iconWidth / 2, iconWidth / 2);\n\t\ticon->paint(\n\t\t\tp,\n\t\t\t0,\n\t\t\t0,\n\t\t\twidth(),\n\t\t\t(over\n\t\t\t\t? st::activeButtonBgOver\n\t\t\t\t: st::activeButtonBg)->c);\n\t}\n}\n\nstruct FoldersState {\n\tstd::vector<FilterRow> rows;\n\trpl::variable<int> count;\n\trpl::variable<int> suggested;\n\tFn<void(const FilterRowButton*, Fn<void(Data::ChatFilter)>)> save;\n\tUi::Animations::Simple tagsEnabledAnimation;\n\trpl::event_stream<bool> tagsButtonEnabled;\n};\n\nnot_null<Ui::VerticalLayout*> SetupFoldersList(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<FoldersState*> state,\n\t\tHighlightRegistry *highlights) {\n\tconst auto weak = base::make_weak(container);\n\tconst auto session = &controller->session();\n\tconst auto limit = [=] {\n\t\treturn Data::PremiumLimits(session).dialogFiltersCurrent();\n\t};\n\n\tconst auto find = [=](not_null<FilterRowButton*> button) {\n\t\tconst auto i = ranges::find(state->rows, button, &FilterRow::button);\n\t\tAssert(i != end(state->rows));\n\t\treturn &*i;\n\t};\n\tconst auto showLimitReached = [=] {\n\t\tconst auto removed = ranges::count_if(\n\t\t\tstate->rows,\n\t\t\t&FilterRow::removed);\n\t\tconst auto count = int(state->rows.size() - removed);\n\t\tif (count < limit()) {\n\t\t\treturn false;\n\t\t}\n\t\tcontroller->show(Box(FiltersLimitBox, session, count));\n\t\treturn true;\n\t};\n\tconst auto markForRemovalSure = [=](not_null<FilterRowButton*> button) {\n\t\tconst auto row = find(button);\n\t\tauto suggestRemoving = Api::ExtractSuggestRemoving(row->filter);\n\t\tif (row->removed || row->removePeersRequestId > 0) {\n\t\t\treturn;\n\t\t} else if (!suggestRemoving.empty()) {\n\t\t\tconst auto chosen = crl::guard(button, [=](\n\t\t\t\t\tstd::vector<not_null<PeerData*>> peers) {\n\t\t\t\tconst auto row = find(button);\n\t\t\t\trow->removePeers = std::move(peers);\n\t\t\t\trow->removed = true;\n\t\t\t\tbutton->setRemoved(true);\n\t\t\t});\n\t\t\tApi::ProcessFilterRemove(\n\t\t\t\tcontroller,\n\t\t\t\trow->filter.title(),\n\t\t\t\trow->filter.iconEmoji(),\n\t\t\t\tstd::move(suggestRemoving),\n\t\t\t\trow->suggestRemovePeers,\n\t\t\t\tchosen);\n\t\t} else {\n\t\t\trow->removePeers = {};\n\t\t\trow->removed = true;\n\t\t\tbutton->setRemoved(true);\n\t\t}\n\t};\n\tconst auto markForRemoval = [=](not_null<FilterRowButton*> button) {\n\t\tconst auto row = find(button);\n\t\tif (row->removed || row->removePeersRequestId > 0) {\n\t\t\treturn;\n\t\t} else if (row->filter.hasMyLinks()) {\n\t\t\tcontroller->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = { tr::lng_filters_delete_sure(tr::now) },\n\t\t\t\t.confirmed = crl::guard(button, [=](Fn<void()> close) {\n\t\t\t\t\tmarkForRemovalSure(button);\n\t\t\t\t\tclose();\n\t\t\t\t}),\n\t\t\t\t.confirmText = tr::lng_box_delete(),\n\t\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t}));\n\t\t} else {\n\t\t\tmarkForRemovalSure(button);\n\t\t}\n\t};\n\tconst auto remove = [=](not_null<FilterRowButton*> button) {\n\t\tconst auto row = find(button);\n\t\tif (row->removed || row->removePeersRequestId > 0) {\n\t\t\treturn;\n\t\t} else if (row->filter.chatlist() && !row->removePeersRequestId) {\n\t\t\trow->removePeersRequestId = session->api().request(\n\t\t\t\tMTPchatlists_GetLeaveChatlistSuggestions(\n\t\t\t\t\tMTP_inputChatlistDialogFilter(\n\t\t\t\t\t\tMTP_int(row->filter.id())))\n\t\t\t).done(crl::guard(button, [=](const MTPVector<MTPPeer> &result) {\n\t\t\t\tconst auto row = find(button);\n\t\t\t\trow->removePeersRequestId = -1;\n\t\t\t\trow->suggestRemovePeers = ranges::views::all(\n\t\t\t\t\tresult.v\n\t\t\t\t) | ranges::views::transform([=](const MTPPeer &peer) {\n\t\t\t\t\treturn session->data().peer(peerFromMTP(peer));\n\t\t\t\t}) | ranges::to_vector;\n\t\t\t\tmarkForRemoval(button);\n\t\t\t})).fail(crl::guard(button, [=] {\n\t\t\t\tconst auto row = find(button);\n\t\t\t\trow->removePeersRequestId = -1;\n\t\t\t\tmarkForRemoval(button);\n\t\t\t})).send();\n\t\t} else {\n\t\t\tmarkForRemoval(button);\n\t\t}\n\t};\n\tconst auto wrap = container->add(object_ptr<Ui::VerticalLayout>(\n\t\tcontainer));\n\tconst auto addFilter = [=](const Data::ChatFilter &filter) {\n\t\tconst auto button = wrap->add(\n\t\t\tobject_ptr<FilterRowButton>(wrap, session, filter));\n\t\tbutton->removeRequests(\n\t\t) | rpl::on_next([=] {\n\t\t\tremove(button);\n\t\t}, button->lifetime());\n\t\tbutton->restoreRequests(\n\t\t) | rpl::on_next([=] {\n\t\t\tif (showLimitReached()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tbutton->setRemoved(false);\n\t\t\tfind(button)->removed = false;\n\t\t}, button->lifetime());\n\t\tbutton->setClickedCallback([=] {\n\t\t\tconst auto found = find(button);\n\t\t\tif (found->removed) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto doneCallback = [=](const Data::ChatFilter &result) {\n\t\t\t\tfind(button)->filter = result;\n\t\t\t\tbutton->updateData(result);\n\t\t\t};\n\t\t\tconst auto saveAnd = [=](\n\t\t\t\t\tconst Data::ChatFilter &data,\n\t\t\t\t\tFn<void(Data::ChatFilter)> next) {\n\t\t\t\tdoneCallback(data);\n\t\t\t\tstate->save(button, next);\n\t\t\t};\n\t\t\tcontroller->window().show(Box(\n\t\t\t\tEditFilterBox,\n\t\t\t\tcontroller,\n\t\t\t\tfound->filter,\n\t\t\t\tcrl::guard(button, doneCallback),\n\t\t\t\tcrl::guard(button, saveAnd)));\n\t\t});\n\t\tstate->rows.push_back({ button, filter });\n\t\tstate->count = state->rows.size();\n\n\t\tconst auto filters = &controller->session().data().chatsFilters();\n\t\tconst auto id = filter.id();\n\t\tif (ranges::contains(filters->list(), id, &Data::ChatFilter::id)) {\n\t\t\tfilters->chatsList(id)->fullSize().changes(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tconst auto found = find(button);\n\t\t\t\tif (found->postponedCountUpdate) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfound->postponedCountUpdate = true;\n\t\t\t\tUi::PostponeCall(button, [=] {\n\t\t\t\t\tconst auto &list = filters->list();\n\t\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\t\tlist,\n\t\t\t\t\t\tid,\n\t\t\t\t\t\t&Data::ChatFilter::id);\n\t\t\t\t\tif (i == end(list)) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst auto found = find(button);\n\t\t\t\t\tconst auto &now = found->filter;\n\t\t\t\t\tif ((i->flags() != now.flags())\n\t\t\t\t\t\t|| (i->always() != now.always())\n\t\t\t\t\t\t|| (i->never() != now.never())) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tbutton->updateCount(now);\n\t\t\t\t\tfound->postponedCountUpdate = false;\n\t\t\t\t});\n\t\t\t}, button->lifetime());\n\t\t}\n\n\t\twrap->resizeToWidth(container->width());\n\n\t\treturn button;\n\t};\n\tconst auto &list = session->data().chatsFilters().list();\n\tfor (const auto &filter : list) {\n\t\tif (filter.id()) {\n\t\t\taddFilter(filter);\n\t\t}\n\t}\n\n\tsession->data().chatsFilters().isChatlistChanged(\n\t) | rpl::on_next([=](FilterId id) {\n\t\tconst auto filters = &session->data().chatsFilters();\n\t\tconst auto &list = filters->list();\n\t\tconst auto i = ranges::find(list, id, &Data::ChatFilter::id);\n\t\tconst auto j = ranges::find(state->rows, id, [](const auto &row) {\n\t\t\treturn row.filter.id();\n\t\t});\n\t\tif (i == end(list) || j == end(state->rows)) {\n\t\t\treturn;\n\t\t}\n\t\tj->filter = j->filter.withChatlist(i->chatlist(), i->hasMyLinks());\n\t\tj->button->updateCount(j->filter);\n\t}, container->lifetime());\n\n\tconst auto createButton = AddButtonWithIcon(\n\t\tcontainer,\n\t\ttr::lng_filters_create(),\n\t\tst::settingsButtonActive,\n\t\t{ &st::settingsIconAdd, IconType::Round, &st::windowBgActive });\n\tif (highlights) {\n\t\thighlights->push_back({ u\"folders/create\"_q, { createButton.get() } });\n\t}\n\tcreateButton->setClickedCallback([=] {\n\t\tif (showLimitReached()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto created = std::make_shared<FilterRowButton*>(nullptr);\n\t\tconst auto doneCallback = [=](const Data::ChatFilter &result) {\n\t\t\tif (const auto button = *created) {\n\t\t\t\tfind(button)->filter = result;\n\t\t\t\tbutton->updateData(result);\n\t\t\t} else {\n\t\t\t\t*created = addFilter(result);\n\t\t\t}\n\t\t};\n\t\tconst auto saveAnd = [=](\n\t\t\t\tconst Data::ChatFilter &data,\n\t\t\t\tFn<void(Data::ChatFilter)> next) {\n\t\t\tdoneCallback(data);\n\t\t\tstate->save(*created, next);\n\t\t};\n\t\tcontroller->window().show(Box(\n\t\t\tEditFilterBox,\n\t\t\tcontroller,\n\t\t\tData::ChatFilter(),\n\t\t\tcrl::guard(container, doneCallback),\n\t\t\tcrl::guard(container, saveAnd)));\n\t});\n\n\tconst auto prepareGoodIdsForNewFilters = [=] {\n\t\tconst auto &list = session->data().chatsFilters().list();\n\n\t\tauto localId = 1;\n\t\tconst auto chooseNextId = [&] {\n\t\t\t++localId;\n\t\t\twhile (ranges::contains(list, localId, &Data::ChatFilter::id)) {\n\t\t\t\t++localId;\n\t\t\t}\n\t\t\treturn localId;\n\t\t};\n\t\tauto result = base::flat_map<not_null<FilterRowButton*>, FilterId>();\n\t\tfor (auto &row : state->rows) {\n\t\t\tconst auto id = row.filter.id();\n\t\t\tif (row.removed) {\n\t\t\t\tcontinue;\n\t\t\t} else if (!id\n\t\t\t\t|| !ranges::contains(list, id, &Data::ChatFilter::id)) {\n\t\t\t\tresult.emplace(row.button, chooseNextId());\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\n\tstate->save = [=](\n\t\t\tconst FilterRowButton *single,\n\t\t\tFn<void(Data::ChatFilter)> next) {\n\t\tauto ids = prepareGoodIdsForNewFilters();\n\n\t\tauto updated = Data::ChatFilter();\n\n\t\tauto order = std::vector<FilterId>();\n\t\tauto updates = std::vector<MTPUpdate>();\n\t\tauto addRequests = std::vector<MTPmessages_UpdateDialogFilter>();\n\t\tauto removeRequests = std::vector<MTPmessages_UpdateDialogFilter>();\n\t\tauto removeChatlistRequests = std::vector<MTPchatlists_LeaveChatlist>();\n\n\t\tauto &realFilters = session->data().chatsFilters();\n\t\tconst auto &list = realFilters.list();\n\t\torder.reserve(state->rows.size());\n\t\tfor (auto &row : state->rows) {\n\t\t\tif (row.button.get() == single) {\n\t\t\t\tupdated = row.filter;\n\t\t\t}\n\t\t\tconst auto id = row.filter.id();\n\t\t\tconst auto removed = row.removed;\n\t\t\tconst auto i = ranges::find(list, id, &Data::ChatFilter::id);\n\t\t\tif (removed && (i == end(list) || id == FilterId(0))) {\n\t\t\t\tcontinue;\n\t\t\t} else if (!removed && i != end(list) && *i == row.filter) {\n\t\t\t\torder.push_back(id);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto newId = ids.take(row.button).value_or(id);\n\t\t\tif (newId != id) {\n\t\t\t\trow.filter = row.filter.withId(newId);\n\t\t\t\trow.button->updateData(row.filter);\n\t\t\t\tif (row.button.get() == single) {\n\t\t\t\t\tupdated = row.filter;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto tl = removed\n\t\t\t\t? MTPDialogFilter()\n\t\t\t\t: row.filter.tl(newId);\n\t\t\tconst auto removeChatlistWithChats = removed\n\t\t\t\t&& row.filter.chatlist()\n\t\t\t\t&& !row.removePeers.empty();\n\t\t\tif (removeChatlistWithChats) {\n\t\t\t\tauto inputs = ranges::views::all(\n\t\t\t\t\trow.removePeers\n\t\t\t\t) | ranges::views::transform([](not_null<PeerData*> peer) {\n\t\t\t\t\treturn MTPInputPeer(peer->input());\n\t\t\t\t}) | ranges::to<QVector<MTPInputPeer>>();\n\t\t\t\tremoveChatlistRequests.push_back(\n\t\t\t\t\tMTPchatlists_LeaveChatlist(\n\t\t\t\t\t\tMTP_inputChatlistDialogFilter(MTP_int(newId)),\n\t\t\t\t\t\tMTP_vector<MTPInputPeer>(std::move(inputs))));\n\t\t\t} else {\n\t\t\t\tconst auto request = MTPmessages_UpdateDialogFilter(\n\t\t\t\t\tMTP_flags(removed\n\t\t\t\t\t\t? MTPmessages_UpdateDialogFilter::Flag(0)\n\t\t\t\t\t\t: MTPmessages_UpdateDialogFilter::Flag::f_filter),\n\t\t\t\t\tMTP_int(newId),\n\t\t\t\t\ttl);\n\t\t\t\tif (removed) {\n\t\t\t\t\tremoveRequests.push_back(request);\n\t\t\t\t} else {\n\t\t\t\t\taddRequests.push_back(request);\n\t\t\t\t\torder.push_back(newId);\n\t\t\t\t}\n\t\t\t}\n\t\t\tupdates.push_back(MTP_updateDialogFilter(\n\t\t\t\tMTP_flags(removed\n\t\t\t\t\t? MTPDupdateDialogFilter::Flag(0)\n\t\t\t\t\t: MTPDupdateDialogFilter::Flag::f_filter),\n\t\t\t\tMTP_int(newId),\n\t\t\t\ttl));\n\t\t}\n\t\tif (!ranges::contains(order, FilterId(0))) {\n\t\t\tauto position = 0;\n\t\t\tfor (const auto &filter : list) {\n\t\t\t\tconst auto id = filter.id();\n\t\t\t\tif (!id) {\n\t\t\t\t\tbreak;\n\t\t\t\t} else if (const auto i = ranges::find(order, id)\n\t\t\t\t\t; i != order.end()) {\n\t\t\t\t\tposition = int(i - order.begin()) + 1;\n\t\t\t\t}\n\t\t\t}\n\t\t\torder.insert(order.begin() + position, FilterId(0));\n\t\t}\n\t\tif (next) {\n\t\t\tfor (auto i = state->rows.begin(); i != state->rows.end();) {\n\t\t\t\tif (i->removed) {\n\t\t\t\t\tconst auto button = i->button;\n\t\t\t\t\ti = state->rows.erase(i);\n\t\t\t\t\tdelete button;\n\t\t\t\t} else {\n\t\t\t\t\t++i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcrl::on_main(session, [\n\t\t\tsession,\n\t\t\tnext,\n\t\t\tupdated,\n\t\t\torder = std::move(order),\n\t\t\tupdates = std::move(updates),\n\t\t\taddRequests = std::move(addRequests),\n\t\t\tremoveRequests = std::move(removeRequests),\n\t\t\tremoveChatlistRequests = std::move(removeChatlistRequests)\n\t\t] {\n\t\t\tconst auto api = &session->api();\n\t\t\tconst auto filters = &session->data().chatsFilters();\n\t\t\tconst auto ids = std::make_shared<\n\t\t\t\tbase::flat_set<mtpRequestId>\n\t\t\t>();\n\t\t\tconst auto checkFinished = [=] {\n\t\t\t\tif (ids->empty() && next) {\n\t\t\t\t\tAssert(updated.id() != 0);\n\t\t\t\t\tnext(updated);\n\t\t\t\t}\n\t\t\t};\n\t\t\tfor (const auto &update : updates) {\n\t\t\t\tfilters->apply(update);\n\t\t\t}\n\t\t\tauto previousId = mtpRequestId(0);\n\t\t\tconst auto sendRequests = [&](const auto &requests) {\n\t\t\t\tfor (auto &request : requests) {\n\t\t\t\t\tpreviousId = api->request(\n\t\t\t\t\t\tstd::move(request)\n\t\t\t\t\t).done([=](const auto &result, mtpRequestId id) {\n\t\t\t\t\t\tif constexpr (std::is_same_v<\n\t\t\t\t\t\t\t\tstd::decay_t<decltype(result)>,\n\t\t\t\t\t\t\t\tMTPUpdates>) {\n\t\t\t\t\t\t\tsession->api().applyUpdates(result);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tids->remove(id);\n\t\t\t\t\t\tcheckFinished();\n\t\t\t\t\t}).afterRequest(previousId).send();\n\t\t\t\t\tids->emplace(previousId);\n\t\t\t\t}\n\t\t\t};\n\t\t\tsendRequests(removeRequests);\n\t\t\tsendRequests(removeChatlistRequests);\n\t\t\tsendRequests(addRequests);\n\t\t\tif (!order.empty() && !addRequests.empty()) {\n\t\t\t\tfilters->saveOrder(order, previousId);\n\t\t\t}\n\t\t\tcheckFinished();\n\t\t});\n\t};\n\n\treturn wrap;\n}\n\nvoid SetupRecommendedSection(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<FoldersState*> state,\n\t\tHighlightRegistry *highlights,\n\t\tnot_null<Ui::VerticalLayout*> filtersWrap) {\n\tconst auto session = &controller->session();\n\tconst auto limit = [=] {\n\t\treturn Data::PremiumLimits(session).dialogFiltersCurrent();\n\t};\n\n\tconst auto showLimitReached = [=] {\n\t\tconst auto removed = ranges::count_if(\n\t\t\tstate->rows,\n\t\t\t&FilterRow::removed);\n\t\tconst auto count = int(state->rows.size() - removed);\n\t\tif (count < limit()) {\n\t\t\treturn false;\n\t\t}\n\t\tcontroller->show(Box(FiltersLimitBox, session, count));\n\t\treturn true;\n\t};\n\n\tconst auto find = [=](not_null<FilterRowButton*> button) {\n\t\tconst auto i = ranges::find(state->rows, button, &FilterRow::button);\n\t\tAssert(i != end(state->rows));\n\t\treturn &*i;\n\t};\n\n\tconst auto addFilter = [=](const Data::ChatFilter &filter) {\n\t\tconst auto button = filtersWrap->add(\n\t\t\tobject_ptr<FilterRowButton>(filtersWrap, session, filter));\n\t\tbutton->removeRequests(\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto row = find(button);\n\t\t\trow->removed = true;\n\t\t\tbutton->setRemoved(true);\n\t\t}, button->lifetime());\n\t\tbutton->restoreRequests(\n\t\t) | rpl::on_next([=] {\n\t\t\tif (showLimitReached()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tbutton->setRemoved(false);\n\t\t\tfind(button)->removed = false;\n\t\t}, button->lifetime());\n\t\tbutton->setClickedCallback([=] {\n\t\t\tconst auto found = find(button);\n\t\t\tif (found->removed) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto doneCallback = [=](const Data::ChatFilter &result) {\n\t\t\t\tfind(button)->filter = result;\n\t\t\t\tbutton->updateData(result);\n\t\t\t};\n\t\t\tconst auto saveAnd = [=](\n\t\t\t\t\tconst Data::ChatFilter &data,\n\t\t\t\t\tFn<void(Data::ChatFilter)> next) {\n\t\t\t\tdoneCallback(data);\n\t\t\t\tstate->save(button, next);\n\t\t\t};\n\t\t\tcontroller->window().show(Box(\n\t\t\t\tEditFilterBox,\n\t\t\t\tcontroller,\n\t\t\t\tfound->filter,\n\t\t\t\tcrl::guard(button, doneCallback),\n\t\t\t\tcrl::guard(button, saveAnd)));\n\t\t});\n\t\tstate->rows.push_back({ button, filter });\n\t\tstate->count = state->rows.size();\n\n\t\tfiltersWrap->resizeToWidth(container->width());\n\t\treturn button;\n\t};\n\n\tUi::AddSkip(container);\n\tconst auto nonEmptyAbout = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container))\n\t)->setDuration(0);\n\tconst auto aboutRows = nonEmptyAbout->entity();\n\tUi::AddDivider(aboutRows);\n\tUi::AddSkip(aboutRows);\n\tconst auto recommendedTitle = Ui::AddSubsectionTitle(\n\t\taboutRows,\n\t\ttr::lng_filters_recommended());\n\tif (highlights) {\n\t\thighlights->push_back({\n\t\t\tu\"folders/add-recommended\"_q,\n\t\t\t{ recommendedTitle.get(), SubsectionTitleHighlight() },\n\t\t});\n\t}\n\n\tconst auto setTagsProgress = [=](float64 value) {\n\t\tfor (const auto &row : state->rows) {\n\t\t\trow.button->setColorIndexProgress(value);\n\t\t}\n\t};\n\tstate->tagsButtonEnabled.events() | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](bool value) {\n\t\tstate->tagsEnabledAnimation.stop();\n\t\tstate->tagsEnabledAnimation.start(\n\t\t\tsetTagsProgress,\n\t\t\tvalue ? .0 : 1.,\n\t\t\tvalue ? 1. : .0,\n\t\t\tst::universalDuration);\n\t}, container->lifetime());\n\tsetTagsProgress(session->data().chatsFilters().tagsEnabled());\n\n\trpl::single(rpl::empty) | rpl::then(\n\t\tsession->data().chatsFilters().suggestedUpdated()\n\t) | rpl::map([=] {\n\t\treturn session->data().chatsFilters().suggestedFilters();\n\t}) | rpl::filter([=](const std::vector<Data::SuggestedFilter> &list) {\n\t\treturn !list.empty();\n\t}) | rpl::take(\n\t\t1\n\t) | rpl::on_next([=](\n\t\t\tconst std::vector<Data::SuggestedFilter> &suggestions) {\n\t\tfor (const auto &suggestion : suggestions) {\n\t\t\tconst auto &filter = suggestion.filter;\n\t\t\tif (ranges::contains(state->rows, filter, &FilterRow::filter)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tstate->suggested = state->suggested.current() + 1;\n\t\t\tconst auto button = aboutRows->add(object_ptr<FilterRowButton>(\n\t\t\t\taboutRows,\n\t\t\t\tsession,\n\t\t\t\tfilter,\n\t\t\t\tsuggestion.description));\n\t\t\tbutton->addRequests(\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\tif (showLimitReached()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\taddFilter(filter);\n\t\t\t\tstate->suggested = state->suggested.current() - 1;\n\t\t\t\tdelete button;\n\t\t\t}, button->lifetime());\n\t\t}\n\t\taboutRows->resizeToWidth(container->width());\n\t\tUi::AddSkip(aboutRows, st::defaultVerticalListSkip);\n\t}, aboutRows->lifetime());\n\n\tauto showSuggestions = rpl::combine(\n\t\tstate->suggested.value(),\n\t\tstate->count.value(),\n\t\tData::AmPremiumValue(session)\n\t) | rpl::map([limit](int suggested, int count, bool) {\n\t\treturn suggested > 0 && count < limit();\n\t});\n\tnonEmptyAbout->toggleOn(std::move(showSuggestions));\n}\n\nvoid BuildTopContent(SectionBuilder &builder, rpl::producer<> showFinished) {\n\tbuilder.add([showFinished = std::move(showFinished)](\n\t\t\tconst WidgetContext &ctx) mutable {\n\t\tconst auto parent = ctx.container;\n\t\tconst auto divider = Ui::CreateChild<Ui::BoxContentDivider>(\n\t\t\tparent.get());\n\t\tconst auto verticalLayout = parent->add(\n\t\t\tobject_ptr<Ui::VerticalLayout>(parent.get()));\n\n\t\tauto icon = CreateLottieIcon(\n\t\t\tverticalLayout,\n\t\t\t{\n\t\t\t\t.name = u\"filters\"_q,\n\t\t\t\t.sizeOverride = {\n\t\t\t\t\tst::settingsFilterIconSize,\n\t\t\t\t\tst::settingsFilterIconSize,\n\t\t\t\t},\n\t\t\t},\n\t\t\tst::settingsFilterIconPadding);\n\t\tstd::move(\n\t\t\tshowFinished\n\t\t) | rpl::on_next([animate = std::move(icon.animate)] {\n\t\t\tanimate(anim::repeat::once);\n\t\t}, verticalLayout->lifetime());\n\t\tverticalLayout->add(std::move(icon.widget));\n\n\t\tverticalLayout->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tverticalLayout,\n\t\t\t\ttr::lng_filters_about(),\n\t\t\t\tst::settingsFilterDividerLabel),\n\t\t\tst::settingsFilterDividerLabelPadding,\n\t\t\tstyle::al_top)->setTryMakeSimilarLines(true);\n\n\t\tverticalLayout->geometryValue(\n\t\t) | rpl::on_next([=](const QRect &r) {\n\t\t\tdivider->setGeometry(r);\n\t\t}, divider->lifetime());\n\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n}\n\nvoid BuildFoldersListSection(\n\t\tSectionBuilder &builder,\n\t\tnot_null<FoldersState*> state) {\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle(tr::lng_filters_subtitle());\n\n\tbuilder.add([=](const WidgetContext &ctx) {\n\t\tconst auto wrap = SetupFoldersList(\n\t\t\tctx.controller,\n\t\t\tctx.container,\n\t\t\tstate,\n\t\t\tctx.highlights);\n\t\tSetupRecommendedSection(\n\t\t\tctx.controller,\n\t\t\tctx.container,\n\t\t\tstate,\n\t\t\tctx.highlights,\n\t\t\twrap);\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n}\n\nvoid BuildTagsSection(SectionBuilder &builder, not_null<FoldersState*> state) {\n\tif (!builder.session()->premiumPossible()) {\n\t\treturn;\n\t}\n\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\n\tconst auto session = builder.session();\n\n\tbuilder.add([=](const WidgetContext &ctx) {\n\t\tconst auto controller = ctx.controller;\n\t\tconst auto content = ctx.container;\n\n\t\tstruct TagsState final {\n\t\t\trpl::event_stream<bool> tagsTurnOff;\n\t\t\tbase::Timer requestTimer;\n\t\t\tFn<void()> sendCallback;\n\t\t};\n\n\t\tauto premium = Data::AmPremiumValue(session);\n\t\tconst auto tagsButton = content->add(\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_filters_enable_tags(),\n\t\t\t\tst::settingsButtonNoIconLocked));\n\t\tif (ctx.highlights) {\n\t\t\tctx.highlights->push_back({ u\"folders/show-tags\"_q, { tagsButton } });\n\t\t}\n\t\tconst auto tagsState = tagsButton->lifetime().make_state<TagsState>();\n\t\ttagsButton->toggleOn(rpl::merge(\n\t\t\trpl::combine(\n\t\t\t\tsession->data().chatsFilters().tagsEnabledValue(),\n\t\t\t\trpl::duplicate(premium),\n\t\t\t\trpl::mappers::_1 && rpl::mappers::_2),\n\t\t\ttagsState->tagsTurnOff.events()));\n\t\trpl::duplicate(premium) | rpl::on_next([=](bool value) {\n\t\t\ttagsButton->setToggleLocked(!value);\n\t\t}, tagsButton->lifetime());\n\n\t\tconst auto send = [=,\n\t\t\t\tweak = base::make_weak(tagsButton)](bool checked) {\n\t\t\tsession->data().chatsFilters().requestToggleTags(checked, [=] {\n\t\t\t\tif ([[maybe_unused]] const auto strong = weak.get()) {\n\t\t\t\t\ttagsState->tagsTurnOff.fire(!checked);\n\t\t\t\t}\n\t\t\t});\n\t\t};\n\n\t\ttagsButton->toggledValue(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\tconst auto premium = session->premium();\n\t\t\tif (checked && !premium) {\n\t\t\t\tShowPremiumPreviewToBuy(controller, PremiumFeature::FilterTags);\n\t\t\t\ttagsState->tagsTurnOff.fire(false);\n\t\t\t}\n\t\t\tif (!premium) {\n\t\t\t\tstate->tagsButtonEnabled.fire(false);\n\t\t\t} else {\n\t\t\t\tstate->tagsButtonEnabled.fire_copy(checked);\n\t\t\t}\n\t\t\tconst auto proceed = premium\n\t\t\t\t&& (checked != session->data().chatsFilters().tagsEnabled());\n\t\t\tif (!proceed) {\n\t\t\t\ttagsState->requestTimer.cancel();\n\t\t\t}\n\t\t\treturn proceed;\n\t\t}) | rpl::on_next([=](bool v) {\n\t\t\ttagsState->sendCallback = [=] { send(v); };\n\t\t\ttagsState->requestTimer.cancel();\n\t\t\ttagsState->requestTimer.setCallback([=] { send(v); });\n\t\t\ttagsState->requestTimer.callOnce(500);\n\t\t}, tagsButton->lifetime());\n\n\t\ttagsButton->lifetime().add([=] {\n\t\t\tif (tagsState->requestTimer.isActive()) {\n\t\t\t\tif (tagsState->sendCallback) {\n\t\t\t\t\ttagsState->sendCallback();\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"folders/show-tags\"_q,\n\t\t\t.title = tr::lng_filters_enable_tags(tr::now),\n\t\t\t.keywords = { u\"tags\"_q, u\"colors\"_q, u\"premium\"_q },\n\t\t};\n\t});\n\n\tbuilder.addSkip();\n\n\tbuilder.add([=](const WidgetContext &ctx) {\n\t\tauto premium = Data::AmPremiumValue(session);\n\t\tconst auto about = Ui::AddDividerText(\n\t\t\tctx.container,\n\t\t\trpl::conditional(\n\t\t\t\trpl::duplicate(premium),\n\t\t\t\ttr::lng_filters_enable_tags_about(tr::rich),\n\t\t\t\ttr::lng_filters_enable_tags_about_premium(\n\t\t\t\t\tlt_link,\n\t\t\t\t\ttr::lng_effect_premium_link() | rpl::map([](QString t) {\n\t\t\t\t\t\treturn tr::link(std::move(t), u\"internal:\"_q);\n\t\t\t\t\t}),\n\t\t\t\t\ttr::rich)));\n\t\tabout->setClickHandlerFilter([=](const auto &...) {\n\t\t\tSettings::ShowPremium(ctx.controller, u\"folder_tags\"_q);\n\t\t\treturn true;\n\t\t});\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n}\n\nvoid BuildViewSection(SectionBuilder &builder) {\n\tbuilder.add([](const WidgetContext &ctx) {\n\t\tconst auto controller = ctx.controller;\n\t\tconst auto parent = ctx.container;\n\n\t\tconst auto wrap = parent->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tparent,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(parent)));\n\t\twrap->toggleOn(controller->enoughSpaceForFiltersValue());\n\t\tconst auto content = wrap->entity();\n\n\t\tUi::AddDivider(content);\n\t\tUi::AddSkip(content);\n\t\tconst auto title = Ui::AddSubsectionTitle(\n\t\t\tcontent,\n\t\t\ttr::lng_filters_view_subtitle());\n\t\tif (ctx.highlights) {\n\t\t\tctx.highlights->push_back({\n\t\t\t\tu\"folders/tab-view\"_q,\n\t\t\t\t{ title.get(), SubsectionTitleHighlight() },\n\t\t\t});\n\t\t}\n\n\t\tconst auto group = std::make_shared<Ui::RadioenumGroup<bool>>(\n\t\t\tCore::App().settings().chatFiltersHorizontal());\n\t\tconst auto addSend = [&](bool value, const QString &text) {\n\t\t\tcontent->add(\n\t\t\t\tobject_ptr<Ui::Radioenum<bool>>(\n\t\t\t\t\tcontent,\n\t\t\t\t\tgroup,\n\t\t\t\t\tvalue,\n\t\t\t\t\ttext,\n\t\t\t\t\tst::settingsSendType),\n\t\t\t\tst::settingsSendTypePadding);\n\t\t};\n\t\taddSend(false, tr::lng_filters_vertical(tr::now));\n\t\taddSend(true, tr::lng_filters_horizontal(tr::now));\n\n\t\tgroup->setChangedCallback([=](bool value) {\n\t\t\tCore::App().settings().setChatFiltersHorizontal(value);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t});\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"folders/tab-view\"_q,\n\t\t\t.title = tr::lng_filters_view_subtitle(tr::now),\n\t\t\t.keywords = { u\"view\"_q, u\"layout\"_q, u\"tabs\"_q },\n\t\t};\n\t});\n}\n\nclass Folders : public Section<Folders> {\npublic:\n\tFolders(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~Folders();\n\n\tvoid showFinished() override;\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\nprivate:\n\tvoid setupContent();\n\n\tstd::shared_ptr<FoldersState> _state;\n\trpl::event_stream<> _showFinished;\n\n};\n\nFolders::Folders(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller)\n, _state(std::make_shared<FoldersState>()) {\n\tsetupContent();\n}\n\nFolders::~Folders() {\n\tif (!Core::Quitting() && _state->save) {\n\t\t_state->save(nullptr, nullptr);\n\t}\n}\n\nrpl::producer<QString> Folders::title() {\n\treturn tr::lng_filters_title();\n}\n\nvoid Folders::setupContent() {\n\tcontroller()->session().data().chatsFilters().requestSuggested();\n\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tconst auto state = _state;\n\n\tconst SectionBuildMethod buildMethod = [state](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tauto &lifetime = container->lifetime();\n\t\tconst auto highlights = lifetime.make_state<HighlightRegistry>();\n\t\tconst auto isPaused = Window::PausedIn(\n\t\t\tcontroller,\n\t\t\tWindow::GifPauseReason::Layer);\n\t\tauto showFinishedDup = rpl::duplicate(showFinished);\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = isPaused,\n\t\t\t.highlights = highlights,\n\t\t});\n\n\t\tBuildTopContent(builder, std::move(showFinishedDup));\n\t\tBuildFoldersListSection(builder, state.get());\n\t\tBuildTagsSection(builder, state.get());\n\t\tBuildViewSection(builder);\n\n\t\tstd::move(showFinished) | rpl::on_next([=] {\n\t\t\tfor (const auto &[id, entry] : *highlights) {\n\t\t\t\tif (entry.widget) {\n\t\t\t\t\tcontroller->checkHighlightControl(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tentry.widget,\n\t\t\t\t\t\tbase::duplicate(entry.args));\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime);\n\t};\n\n\tbuild(content, buildMethod);\n\n\tUi::ResizeFitChild(this, content);\n}\n\nvoid Folders::showFinished() {\n\t_showFinished.fire({});\n\tSection<Folders>::showFinished();\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = Folders::Id(),\n\t.parentId = MainId(),\n\t.title = &tr::lng_filters_title,\n\t.icon = &st::menuIconShowInFolder,\n}, [](SectionBuilder &builder) {\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"folders/create\"_q,\n\t\t\t.title = tr::lng_filters_create(tr::now),\n\t\t\t.keywords = { u\"folder\"_q, u\"filter\"_q, u\"new\"_q, u\"add\"_q },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"folders/add-recommended\"_q,\n\t\t\t.title = tr::lng_filters_recommended(tr::now),\n\t\t\t.keywords = { u\"suggested\"_q, u\"recommended\"_q },\n\t\t};\n\t});\n\n\tif (builder.session()->premiumPossible()) {\n\t\tbuilder.add(nullptr, [] {\n\t\t\treturn SearchEntry{\n\t\t\t\t.id = u\"folders/show-tags\"_q,\n\t\t\t\t.title = tr::lng_filters_enable_tags(tr::now),\n\t\t\t\t.keywords = { u\"tags\"_q, u\"colors\"_q, u\"premium\"_q },\n\t\t\t};\n\t\t});\n\t}\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"folders/tab-view\"_q,\n\t\t\t.title = tr::lng_filters_view_subtitle(tr::now),\n\t\t\t.keywords = { u\"view\"_q, u\"layout\"_q, u\"tabs\"_q },\n\t\t};\n\t});\n});\n\n} // namespace\n\nType FoldersId() {\n\treturn Folders::Id();\n}\n\nnamespace Builder {\n\nSectionBuildMethod FoldersSection = kMeta.build;\n\n} // namespace Builder\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_folders.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Settings {\n\n[[nodiscard]] Type FoldersId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_fork.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_fork.h\"\n\n#include \"api/api_authorizations.h\"\n#include \"apiwrap.h\"\n#include \"base/timer.h\"\n#include \"calls/calls_call.h\"\n#include \"calls/calls_instance.h\"\n#include \"calls/calls_video_bubble.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"platform/platform_specific.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common_session.h\"\n#include \"tgcalls/VideoCaptureInterface.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/single_choice_box.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/level_meter.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"webrtc/webrtc_audio_input_tester.h\"\n#include \"webrtc/webrtc_create_adm.h\"\n#include \"webrtc/webrtc_environment.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\n#include \"base/qthelp_url.h\"\n#include \"base/weak_ptr.h\"\n#include \"boxes/abstract_box.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/file_utilities.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"storage/localstorage.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Settings {\n\nnamespace {\nusing langString = tr::phrase<>;\nusing SessionController = not_null<Window::SessionController*>;\n\nclass SettingBox : public Ui::BoxContent, public base::has_weak_ptr  {\npublic:\n\texplicit SettingBox(\n\t\tQWidget*,\n\t\tFn<void(bool)> callback,\n\t\tlangString title,\n\t\tlangString info);\n\n\tvoid setInnerFocus() override;\n\nprotected:\n\tvoid prepare() override;\n\n\tvirtual QString getOrSetGlobal(QString value) = 0;\n\tvirtual bool isInvalidUrl(QString linkUrl) = 0;\n\n\tFn<void(bool)> _callback;\n\tFn<void()> _setInnerFocus;\n\tlangString _info;\n\tlangString _title;\n};\n\nSettingBox::SettingBox(\n\tQWidget*,\n\tFn<void(bool)> callback,\n\tlangString title,\n\tlangString info)\n: _callback(std::move(callback))\n, _info(info)\n, _title(title) {\n\tExpects(_callback != nullptr);\n}\n\nvoid SettingBox::setInnerFocus() {\n\tExpects(_setInnerFocus != nullptr);\n\n\t_setInnerFocus();\n}\n\nvoid SettingBox::prepare() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tconst auto url = content->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tcontent,\n\t\t\tst::defaultInputField,\n\t\t\t_info(),\n\t\t\tgetOrSetGlobal(QString())),\n\t\tst::markdownLinkFieldPadding);\n\n\tconst auto submit = [=] {\n\t\tconst auto linkUrl = url->getLastText();\n\t\tconst auto isInvalid = isInvalidUrl(linkUrl);\n\t\tif (isInvalid) {\n\t\t\turl->showError();\n\t\t\treturn;\n\t\t}\n\t\tconst auto weak = base::make_weak(this);\n\t\tgetOrSetGlobal(linkUrl);\n\t\tCore::App().saveSettings();\n\t\t_callback(!isInvalid);\n\t\tif (weak) {\n\t\t\tcloseBox();\n\t\t}\n\t};\n\n\turl->submits(\n\t) | rpl::on_next([=] {\n\t\tsubmit();\n\t}, lifetime());\n\n\tsetTitle(_title());\n\n\taddButton(tr::lng_box_ok(), submit);\n\taddButton(tr::lng_cancel(), [=] {\n\t\t_callback(!getOrSetGlobal(QString()).isEmpty());\n\t\tcloseBox();\n\t});\n\n\tcontent->resizeToWidth(st::boxWidth);\n\tcontent->moveToLeft(0, 0);\n\tsetDimensions(st::boxWidth, content->height());\n\n\t_setInnerFocus = [=] {\n\t\turl->setFocusFast();\n\t};\n}\n\n//////\n\nclass SearchEngineBox : public SettingBox {\n\n\tusing SettingBox::SettingBox;\n\nprotected:\n\tQString getOrSetGlobal(QString value) override;\n\tbool isInvalidUrl(QString linkUrl) override;\n};\n\nQString SearchEngineBox::getOrSetGlobal(QString value) {\n\tif (value.isEmpty()) {\n\t\treturn Core::App().settings().fork().searchEngineUrl();\n\t}\n\tCore::App().settings().fork().setSearchEngineUrl(value);\n\treturn QString();\n}\n\nbool SearchEngineBox::isInvalidUrl(QString linkUrl) {\n\tlinkUrl = qthelp::validate_url(linkUrl);\n\treturn linkUrl.isEmpty() || linkUrl.indexOf(\"%q\") == -1;\n}\n\n\n//////\n\nclass URISchemeBox : public SettingBox {\n\n\tusing SettingBox::SettingBox;\n\nprotected:\n\tQString getOrSetGlobal(QString value) override;\n\tbool isInvalidUrl(QString linkUrl) override;\n};\n\nQString URISchemeBox::getOrSetGlobal(QString value) {\n\tif (value.isEmpty()) {\n\t\treturn Core::App().settings().fork().uriScheme();\n\t}\n\tCore::App().settings().fork().setUriScheme(value);\n\treturn QString();\n}\n\nbool URISchemeBox::isInvalidUrl(QString linkUrl) {\n\treturn linkUrl.indexOf(\"://\") < 2;\n}\n\n//////\n\nclass StickerSizeBox : public SettingBox {\n\tusing SettingBox::SettingBox;\n\nprotected:\n\tQString getOrSetGlobal(QString value) override;\n\tbool isInvalidUrl(QString linkUrl) override;\n\nprivate:\n\tint _startSize = 0;\n};\n\nQString StickerSizeBox::getOrSetGlobal(QString value) {\n\tif (value.isEmpty()) {\n\t\tif (!_startSize) {\n\t\t\t_startSize = Core::App().settings().fork().customStickerSize();\n\t\t} else if (_startSize\n\t\t\t\t== Core::App().settings().fork().customStickerSize()) {\n\t\t\treturn QString();\n\t\t}\n\t\treturn QString::number(\n\t\t\tCore::App().settings().fork().customStickerSize());\n\t}\n\tif (const auto number = value.toInt()) {\n\t\tCore::App().settings().fork().setCustomStickerSize(number);\n\t}\n\treturn QString();\n}\n\nbool StickerSizeBox::isInvalidUrl(QString linkUrl) {\n\tconst auto number = linkUrl.toInt();\n\treturn !number || number < 50 || number > 256;\n}\n\n//////\n\nusing namespace Builder;\n\nvoid BuildForkSectionContent(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tif (!controller) {\n\t\treturn;\n\t}\n\tstruct State {\n\t\trpl::variable<bool> checked;\n\t};\n\n\tconst auto add = [&](\n\t\t\tauto id,\n\t\t\tQStringList keywords,\n\t\t\tauto title,\n\t\t\tauto checkedCallback,\n\t\t\tauto ok) {\n\t\tconst auto checkbox = builder.addButton({\n\t\t\t.id = std::move(id),\n\t\t\t.title = std::move(title),\n\t\t\t.st = &st::settingsButtonNoIcon,\n\t\t\t.toggled = rpl::single(checkedCallback()),\n\t\t\t.keywords = std::move(keywords),\n\t\t});\n\t\tcheckbox->toggledValue(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\treturn (checked != checkedCallback());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tok(checked);\n\t\t\tCore::App().saveSettings();\n\t\t}, checkbox->lifetime());\n\t};\n\n\tconst auto restartBox = [=](Fn<void()> ok, Fn<void()> cancel) {\n\t\tcontroller->show(\n\t\t\tUi::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_settings_need_restart(tr::now),\n\t\t\t\t.confirmed = [=] {\n\t\t\t\t\tok();\n\t\t\t\t\tCore::App().saveSettings();\n\t\t\t\t\tCore::Restart();\n\t\t\t\t},\n\t\t\t\t.cancelled = [=](Fn<void()> &&close) {\n\t\t\t\t\tcancel();\n\t\t\t\t\tclose();\n\t\t\t\t},\n\t\t\t\t.confirmText = tr::lng_settings_restart_now(tr::now)\n\t\t\t}),\n\t\t\tUi::LayerOption::KeepOther);\n\t};\n\tconst auto addWithBox = [&](\n\t\t\tauto id,\n\t\t\tQStringList keywords,\n\t\t\tauto title,\n\t\t\tauto checkedCallback,\n\t\t\tauto ok,\n\t\t\tauto customBox) {\n\t\tconst auto state = std::make_shared<State>();\n\t\tconst auto checkbox = builder.addButton({\n\t\t\t.id = std::move(id),\n\t\t\t.title = std::move(title),\n\t\t\t.st = &st::settingsButtonNoIcon,\n\t\t\t.toggled = rpl::single(\n\t\t\t\tcheckedCallback()\n\t\t\t) | rpl::then(state->checked.changes()),\n\t\t\t.keywords = std::move(keywords),\n\t\t});\n\t\tcheckbox->toggledValue(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\treturn (checked != checkedCallback());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tcustomBox(checked, state.get());\n\t\t}, checkbox->lifetime());\n\t};\n\tconst auto addRestart = [&](\n\t\t\tauto id,\n\t\t\tQStringList keywords,\n\t\t\tauto title,\n\t\t\tauto checkedCallback,\n\t\t\tauto ok) {\n\t\taddWithBox(\n\t\t\tstd::move(id),\n\t\t\tstd::move(keywords),\n\t\t\tstd::move(title),\n\t\t\tstd::move(checkedCallback),\n\t\t\tstd::move(ok),\n\t\t\t[=](bool checked, State *state) {\n\t\t\t\trestartBox(\n\t\t\t\t\t[=] { ok(checked); },\n\t\t\t\t\t[=] { state->checked.force_assign(!checked); });\n\t\t\t});\n\t};\n\n\t//\n\taddRestart(\n\t\tu\"fork/square_avatars\"_q,\n\t\t{ u\"square\"_q, u\"avatars\"_q, u\"userpic\"_q, u\"circle\"_q },\n\t\ttr::lng_settings_square_avatats(),\n\t\t[] { return Core::App().settings().fork().squareUserpics(); },\n\t\t[=](bool checked) {\n\t\t\tCore::App().settings().fork().setSquareUserpics(checked);\n\t\t});\n\n\t//\n\tadd(\n\t\tu\"fork/audio_fade\"_q,\n\t\t{ u\"audio\"_q, u\"fade\"_q },\n\t\ttr::lng_settings_audio_fade(),\n\t\t[] { return Core::App().settings().fork().audioFade(); },\n\t\t[=](bool checked) {\n\t\t\tCore::App().settings().fork().setAudioFade(checked);\n\t\t});\n\n\t//\n\taddWithBox(\n\t\tu\"fork/uri_scheme\"_q,\n\t\t{ u\"URI\"_q, u\"scheme\"_q, u\"custom link\"_q },\n\t\ttr::lng_settings_uri_scheme(),\n\t\t[] { return Core::App().settings().fork().askUriScheme(); },\n\t\t[=](bool checked) {\n\t\t\tCore::App().settings().fork().setAskUriScheme(checked);\n\t\t},\n\t\t[=](bool checked, State *state) {\n\t\t\tconst auto callback = [=](bool isSuccess) {\n\t\t\t\tif (isSuccess) {\n\t\t\t\t\tCore::App().settings().fork().setAskUriScheme(isSuccess);\n\t\t\t\t\tCore::App().saveSettings();\n\t\t\t\t} else {\n\t\t\t\t\tstate->checked.force_assign(false);\n\t\t\t\t}\n\t\t\t};\n\t\t\tif (!checked) {\n\t\t\t\tCore::App().settings().fork().setAskUriScheme(false);\n\t\t\t\tCore::App().saveSettings();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcontroller->show(\n\t\t\t\tBox<URISchemeBox>(\n\t\t\t\t\tstd::move(callback),\n\t\t\t\t\ttr::lng_settings_uri_scheme_box_title,\n\t\t\t\t\ttr::lng_settings_uri_scheme_field_label),\n\t\t\t\tUi::LayerOption::KeepOther);\n\t\t});\n\n\t//\n\tadd(\n\t\tu\"fork/last_seen_in_dialogs\"_q,\n\t\t{ u\"last\"_q, u\"seen\"_q, u\"dialogs\"_q, u\"online\"_q },\n\t\ttr::lng_settings_last_seen_in_dialogs(),\n\t\t[] { return Core::App().settings().fork().lastSeenInDialogs(); },\n\t\t[=](bool checked) {\n\t\t\tCore::App().settings().fork().setLastSeenInDialogs(checked);\n\t\t});\n\n\t//\n\taddWithBox(\n\t\tu\"fork/custom_search\"_q,\n\t\t{ u\"custom\"_q, u\"search\"_q, u\"engine\"_q },\n\t\ttr::lng_settings_search_engine(),\n\t\t[] { return Core::App().settings().fork().searchEngine(); },\n\t\t[=](bool checked) {\n\t\t\tCore::App().settings().fork().setSearchEngine(checked);\n\t\t},\n\t\t[=](bool checked, State *state) {\n\t\t\tconst auto callback = [=](bool isSuccess) {\n\t\t\t\tif (isSuccess) {\n\t\t\t\t\tCore::App().settings().fork().setSearchEngine(isSuccess);\n\t\t\t\t\tCore::App().saveSettings();\n\t\t\t\t} else {\n\t\t\t\t\tstate->checked.force_assign(false);\n\t\t\t\t}\n\t\t\t};\n\t\t\tif (!checked) {\n\t\t\t\tCore::App().settings().fork().setSearchEngine(false);\n\t\t\t\tCore::App().saveSettings();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcontroller->show(\n\t\t\t\tBox<SearchEngineBox>(\n\t\t\t\t\tstd::move(callback),\n\t\t\t\t\ttr::lng_settings_search_engine_box_title,\n\t\t\t\t\ttr::lng_settings_search_engine_field_label),\n\t\t\t\tUi::LayerOption::KeepOther);\n\t\t});\n\n\t//\n\tadd(\n\t\tu\"fork/mention_by_name\"_q,\n\t\t{ u\"mention\"_q, u\"by\"_q, u\"name\"_q },\n\t\ttr::lng_settings_mention_by_name(),\n\t\t[] { return Core::App().settings().fork().mentionByNameDisabled(); },\n\t\t[=](bool checked) {\n\t\t\tCore::App().settings().fork().setMentionByNameDisabled(checked);\n\t\t});\n\n\t//\n\tadd(\n\t\tu\"fork/all_recent_stickers\"_q,\n\t\t{ u\"all\"_q, u\"recent\"_q, u\"stickers\"_q },\n\t\ttr::lng_settings_show_all_recent_stickers(),\n\t\t[] { return Core::App().settings().fork().allRecentStickers(); },\n\t\t[=](bool checked) {\n\t\t\tCore::App().settings().fork().setAllRecentStickers(checked);\n\t\t});\n\n#ifndef Q_OS_LINUX\n#ifdef Q_OS_WIN\n\tbuilder.addSkip();\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\tadd(\n\t\tu\"fork/use_black_tray_icon\"_q,\n\t\t{ u\"icon\"_q, u\"black\"_q, u\"tray\"_q },\n\t\ttr::lng_settings_use_black_tray_icon(),\n\t\t[] { return Core::App().settings().fork().useBlackTrayIcon(); },\n\t\t[](bool checked) {\n\t\t\tCore::App().settings().fork().setUseBlackTrayIcon(checked);\n\t\t\tCore::App().saveSettings();\n\t\t\tCore::App().domain().notifyUnreadBadgeChanged();\n\t\t});\n#else // !Q_OS_WIN\n\tbuilder.addSkip();\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\taddRestart(\n\t\tu\"fork/use_black_tray_icon\"_q,\n\t\t{ u\"icon\"_q, u\"black\"_q, u\"tray\"_q },\n\t\ttr::lng_settings_use_black_tray_icon(),\n\t\t[] { return Core::App().settings().fork().useBlackTrayIcon(); },\n\t\t[](bool checked) {\n\t\t\tCore::App().settings().fork().setUseBlackTrayIcon(checked);\n\t\t});\n#endif // Q_OS_WIN\n\n\taddRestart(\n\t\tu\"fork/use_original_tray_icon\"_q,\n\t\t{ u\"icon\"_q, u\"original\"_q, u\"tray\"_q },\n\t\ttr::lng_settings_use_original_tray_icon(),\n\t\t[] { return Core::App().settings().fork().useOriginalTrayIcon(); },\n\t\t[](bool checked) {\n\t\t\tCore::App().settings().fork().setUseOriginalTrayIcon(checked);\n\t\t});\n#endif // !Q_OS_LINUX\n\tbuilder.addSkip();\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\n\t//\n\tbuilder.addButton({\n\t\t.id = u\"fork/custom_sticker_size\"_q,\n\t\t.title = tr::lng_settings_custom_sticker_size(),\n\t\t.st = &st::settingsButton,\n\t\t.icon = { &st::menuIconStickers },\n\t\t.label = rpl::single(QString::number(Core::App().settings().fork().customStickerSize())),\n\t\t.onClick = [=] {\n\t\t\tcontroller->show(\n\t\t\t\tBox<StickerSizeBox>(\n\t\t\t\t\t[=](bool isSuccess) {\n\t\t\t\t\t\tif (isSuccess) {\n\t\t\t\t\t\t\trestartBox([] {}, [] {});\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\ttr::lng_settings_custom_sticker_size,\n\t\t\t\t\ttr::lng_settings_sticker_size_label));\n\t\t},\n\t\t.keywords = { u\"custom\"_q, u\"sticker\"_q, u\"size\"_q },\n\t});\n\n\t//\n\tadd(\n\t\tu\"fork/auto_submit_passcode\"_q,\n\t\t{ u\"auto\"_q, u\"submit\"_q, u\"passcode\"_q },\n\t\ttr::lng_settings_auto_submit_passcode(),\n\t\t[] { return Core::App().settings().fork().autoSubmitPasscode(); },\n\t\t[](bool checked) {\n\t\t\tCore::App().settings().fork().setAutoSubmitPasscode(checked);\n\t\t\tCore::App().saveSettings();\n\t\t});\n\n\t//\n\taddRestart(\n\t\tu\"fork/emoji_on_click\"_q,\n\t\t{ u\"emoji\"_q, u\"click\"_q, u\"panel\"_q },\n\t\ttr::lng_settings_emoji_on_click(),\n\t\t[] { return Core::App().settings().fork().emojiPopupOnClick(); },\n\t\t[](bool checked) {\n\t\t\tCore::App().settings().fork().setEmojiPopupOnClick(checked);\n\t\t});\n\n\t//\n\taddRestart(\n\t\tu\"fork/primary_unmuted\"_q,\n\t\t{ u\"primary\"_q, u\"unmuted\"_q, u\"dialogs\"_q },\n\t\ttr::lng_settings_primary_unmuted(),\n\t\t[] { return Core::App().settings().fork().primaryUnmutedMessages(); },\n\t\t[](bool checked) {\n\t\t\tCore::App().settings().fork().setPrimaryUnmutedMessages(checked);\n\t\t});\n\n\t//\n\tadd(\n\t\tu\"fork/remember_media_menu\"_q,\n\t\t{ u\"remember\"_q, u\"media\"_q, u\"menu\"_q },\n\t\trpl::single(u\"Add 'Remember' to menu for media\"_q),\n\t\t[] { return Core::App().settings().fork().addToMenuRememberMedia(); },\n\t\t[](bool checked) {\n\t\t\tCore::App().settings().fork().setAddToMenuRememberMedia(checked);\n\t\t});\n\n\t//\n\taddRestart(\n\t\tu\"fork/hide_all_chats_tab\"_q,\n\t\t{ u\"hide\"_q, u\"all_chats\"_q, u\"tab\"_q },\n\t\trpl::single(u\"Hide 'All Chats' tab\"_q),\n\t\t[] { return Core::App().settings().fork().hideAllChatsTab(); },\n\t\t[](bool checked) {\n\t\t\tCore::App().settings().fork().setHideAllChatsTab(checked);\n\t\t});\n\n\t//\n\tadd(\n\t\tu\"fork/disable_global_search\"_q,\n\t\t{ u\"disable\"_q, u\"global\"_q, u\"search\"_q },\n\t\trpl::single(u\"Disable global search\"_q),\n\t\t[] { return Core::App().settings().fork().globalSearchDisabled(); },\n\t\t[](bool checked) {\n\t\t\tCore::App().settings().fork().setGlobalSearchDisabled(checked);\n\t\t});\n\n\t//\n\tadd(\n\t\tu\"fork/forward_and_remove\"_q,\n\t\t{ u\"forward\"_q, u\"button\"_q, u\"remove\"_q },\n\t\trpl::single(u\"Button to forward and remove\"_q),\n\t\t[] { return Core::App().settings().fork().thirdButtonTopBar(); },\n\t\t[](bool checked) {\n\t\t\tCore::App().settings().fork().setThirdButtonTopBar(checked);\n\t\t});\n\n\t//\n\tadd(\n\t\tu\"fork/auto_copy_incoming_login_codes\"_q,\n\t\t{ u\"auto_copy\"_q, u\"login\"_q, u\"code\"_q },\n\t\trpl::single(u\"Auto-copy incoming login codes\"_q),\n\t\t[] { return Core::App().settings().fork().copyLoginCode(); },\n\t\t[](bool checked) {\n\t\t\tCore::App().settings().fork().setCopyLoginCode(checked);\n\t\t});\n\n\t//\n\tadd(\n\t\tu\"fork/hide_archived_stories\"_q,\n\t\t{ u\"hide\"_q, u\"archived\"_q, u\"stories\"_q },\n\t\trpl::single(u\"Hide archived stories\"_q),\n\t\t[] { return Core::App().settings().fork().archivedStoriesAreHidden(); },\n\t\t[](bool checked) {\n\t\t\tCore::App().settings().fork().setArchivedStoriesAreHidden(checked);\n\t\t});\n\n\tbuilder.addSkip();\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\n\tbuilder.addSubsectionTitle(tr::lng_filters_type_bots());\n\t//\n\tadd(\n\t\tu\"fork/skip_share_from_bot\"_q,\n\t\t{ u\"skip\"_q, u\"share\"_q, u\"bot\"_q },\n\t\trpl::single(u\"Skip share box from app bots\"_q),\n\t\t[] { return Core::App().settings().fork().skipShareFromBot(); },\n\t\t[](bool checked) {\n\t\t\tCore::App().settings().fork().setSkipShareFromBot(checked);\n\t\t});\n\tadd(\n\t\tu\"fork/additional_buttons_web_bot\"_q,\n\t\t{ u\"additional\"_q, u\"button\"_q, u\"web_bot\"_q },\n\t\trpl::single(u\"Display additional buttons for app bots\"_q),\n\t\t[] { return Core::App().settings().fork().additionalButtonsWebBot(); },\n\t\t[](bool checked) {\n\t\t\tCore::App().settings().fork().setAdditionalButtonsWebBot(checked);\n\t\t});\n\n\tbuilder.addSkip();\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n}\n\nclass Fork : public Section<Fork> {\npublic:\n\tFork(QWidget *parent, not_null<Window::SessionController*> controller);\n\t~Fork();\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\tvoid sectionSaveChanges(FnMut<void()> done) override;\n\nprivate:\n\tvoid setupContent();\n\n};\n\nFork::Fork(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent();\n}\n\nFork::~Fork() = default;\n\nrpl::producer<QString> Fork::title() {\n\treturn tr::lng_settings_section_fork();\n}\n\nvoid Fork::sectionSaveChanges(FnMut<void()> done) {\n\tdone();\n}\n\nvoid Fork::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tconst SectionBuildMethod buildMethod = [](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tconst auto isPaused = Window::PausedIn(\n\t\t\tcontroller,\n\t\t\tWindow::GifPauseReason::Layer);\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = isPaused,\n\t\t});\n\t\tBuildForkSectionContent(builder);\n\t};\n\n\tbuild(content, buildMethod);\n\tUi::ResizeFitChild(this, content);\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = Fork::Id(),\n\t.parentId = MainId(),\n\t.title = &tr::lng_settings_section_fork,\n\t.icon = &st::menuIconForkSettings,\n}, [](SectionBuilder &builder) {\n\tBuildForkSectionContent(builder);\n});\n\n} // namespace\n\nType ForkId() {\n\treturn Fork::Id();\n}\n\nnamespace Builder {\n\nSectionBuildMethod ForkSection = kMeta.build;\n\n} // namespace Builder\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_fork.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace style {\nstruct Checkbox;\nstruct Radio;\n} // namespace style\n\nnamespace Ui {\nclass GenericBox;\nclass Show;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Settings {\n\n[[nodiscard]] Type ForkId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_global_ttl.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_global_ttl.h\"\n\n#include \"api/api_self_destruct.h\"\n#include \"apiwrap.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_ttl_validator.h\"\n#include \"settings/sections/settings_privacy_security.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/settings_common_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_calls.h\"\n\nnamespace Settings {\nnamespace {\n\nusing namespace Builder;\n\nclass TTLRow : public ChatsListBoxController::Row {\npublic:\n\tusing ChatsListBoxController::Row::Row;\n\n\tvoid paintStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) override;\n\n};\n\nvoid TTLRow::paintStatusText(\n\t\tPainter &p,\n\t\tconst style::PeerListItem &st,\n\t\tint x,\n\t\tint y,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) {\n\tauto icon = history()->peer->messagesTTL()\n\t\t? &st::settingsTTLChatsOn\n\t\t: &st::settingsTTLChatsOff;\n\ticon->paint(\n\t\tp,\n\t\tx + st::callArrowPosition.x(),\n\t\ty + st::callArrowPosition.y(),\n\t\touterWidth);\n\tauto shift = st::callArrowPosition.x()\n\t\t+ icon->width()\n\t\t+ st::callArrowSkip;\n\tx += shift;\n\tavailableWidth -= shift;\n\n\tPeerListRow::paintStatusText(\n\t\tp,\n\t\tst,\n\t\tx,\n\t\ty,\n\t\tavailableWidth,\n\t\touterWidth,\n\t\tselected);\n}\n\nclass TTLChatsBoxController : public ChatsListBoxController {\npublic:\n\n\tTTLChatsBoxController(not_null<::Main::Session*> session);\n\n\t::Main::Session &session() const override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\nprotected:\n\tvoid prepareViewHook() override;\n\tstd::unique_ptr<Row> createRow(not_null<History*> history) override;\n\nprivate:\n\tconst not_null<::Main::Session*> _session;\n\n\trpl::lifetime _lifetime;\n\n};\n\nTTLChatsBoxController::TTLChatsBoxController(not_null<::Main::Session*> session)\n: ChatsListBoxController(session)\n, _session(session) {\n}\n\n::Main::Session &TTLChatsBoxController::session() const {\n\treturn *_session;\n}\n\nvoid TTLChatsBoxController::prepareViewHook() {\n\tdelegate()->peerListSetTitle(tr::lng_settings_ttl_title());\n}\n\nvoid TTLChatsBoxController::rowClicked(not_null<PeerListRow*> row) {\n\tif (!TTLMenu::TTLValidator(nullptr, row->peer()).can()) {\n\t\tdelegate()->peerListUiShow()->showToast(\n\t\t\t{ tr::lng_settings_ttl_select_chats_sorry(tr::now) });\n\t\treturn;\n\t}\n\tdelegate()->peerListSetRowChecked(row, !row->checked());\n}\n\nstd::unique_ptr<TTLChatsBoxController::Row> TTLChatsBoxController::createRow(\n\t\tnot_null<History*> history) {\n\tconst auto peer = history->peer;\n\tif (peer->isSelf() || peer->isRepliesChat() || peer->isVerifyCodes()) {\n\t\treturn nullptr;\n\t} else if (peer->isChat() && peer->asChat()->amIn()) {\n\t} else if (peer->isMegagroup()) {\n\t} else if (!TTLMenu::TTLValidator(nullptr, peer).can()) {\n\t\treturn nullptr;\n\t}\n\tif (session().data().contactsNoChatsList()->contains({ history })) {\n\t\treturn nullptr;\n\t}\n\tauto result = std::make_unique<TTLRow>(history);\n\tconst auto applyStatus = [=, raw = result.get()] {\n\t\tconst auto ttl = peer->messagesTTL();\n\t\traw->setCustomStatus(\n\t\t\tttl\n\t\t\t\t? tr::lng_settings_ttl_select_chats_status(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_after_duration,\n\t\t\t\t\tUi::FormatTTLAfter(ttl))\n\t\t\t\t: tr::lng_settings_ttl_select_chats_status_disabled(tr::now),\n\t\t\tttl);\n\t};\n\tapplyStatus();\n\treturn result;\n}\n\nstruct GlobalTTLState {\n\tstd::shared_ptr<Ui::RadiobuttonGroup> group;\n\tstd::shared_ptr<::Main::SessionShow> show;\n\tnot_null<Ui::VerticalLayout*> buttons;\n\tQPointer<Ui::SettingsButton> customButton;\n\trpl::lifetime requestLifetime;\n};\n\nvoid BuildTopContent(SectionBuilder &builder, rpl::producer<> showFinished) {\n\tbuilder.add([showFinished = std::move(showFinished)](\n\t\t\tconst WidgetContext &ctx) mutable {\n\t\tconst auto parent = ctx.container;\n\t\tconst auto divider = Ui::CreateChild<Ui::BoxContentDivider>(\n\t\t\tparent.get());\n\t\tconst auto verticalLayout = parent->add(\n\t\t\tobject_ptr<Ui::VerticalLayout>(parent.get()));\n\n\t\tauto icon = CreateLottieIcon(\n\t\t\tverticalLayout,\n\t\t\t{\n\t\t\t\t.name = u\"ttl\"_q,\n\t\t\t\t.sizeOverride = {\n\t\t\t\t\tst::settingsCloudPasswordIconSize,\n\t\t\t\t\tst::settingsCloudPasswordIconSize,\n\t\t\t\t},\n\t\t\t},\n\t\t\tst::settingsFilterIconPadding);\n\t\tstd::move(\n\t\t\tshowFinished\n\t\t) | rpl::on_next([animate = std::move(icon.animate)] {\n\t\t\tanimate(anim::repeat::loop);\n\t\t}, verticalLayout->lifetime());\n\t\tverticalLayout->add(std::move(icon.widget));\n\n\t\tverticalLayout->geometryValue(\n\t\t) | rpl::on_next([=](const QRect &r) {\n\t\t\tdivider->setGeometry(r);\n\t\t}, divider->lifetime());\n\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n}\n\nvoid RebuildButtons(\n\t\tnot_null<GlobalTTLState*> state,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tTimeId currentTTL) {\n\tauto ttls = std::vector<TimeId>{\n\t\t0,\n\t\t3600 * 24,\n\t\t3600 * 24 * 7,\n\t\t3600 * 24 * 31,\n\t};\n\tif (!ranges::contains(ttls, currentTTL)) {\n\t\tttls.push_back(currentTTL);\n\t\tranges::sort(ttls);\n\t}\n\tif (state->buttons->count() > int(ttls.size())) {\n\t\treturn;\n\t}\n\n\tconst auto request = [=](TimeId ttl) {\n\t\tcontroller->session().api().selfDestruct().updateDefaultHistoryTTL(ttl);\n\t};\n\n\tconst auto showSure = [=](TimeId ttl, bool rebuild) {\n\t\tconst auto ttlText = Ui::FormatTTLAfter(ttl);\n\t\tconst auto confirmed = [=] {\n\t\t\tif (rebuild) {\n\t\t\t\tRebuildButtons(state, controller, ttl);\n\t\t\t}\n\t\t\tstate->group->setChangedCallback([=](int value) {\n\t\t\t\tstate->group->setChangedCallback(nullptr);\n\t\t\t\tstate->show->showToast(tr::lng_settings_ttl_after_toast(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_after_duration,\n\t\t\t\t\t{ .text = ttlText },\n\t\t\t\t\ttr::marked));\n\t\t\t\tstate->show->hideLayer();\n\t\t\t});\n\t\t\trequest(ttl);\n\t\t};\n\t\tif (state->group->value()) {\n\t\t\tconfirmed();\n\t\t\treturn;\n\t\t}\n\t\tstate->show->showBox(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_settings_ttl_after_sure(\n\t\t\t\tlt_after_duration,\n\t\t\t\trpl::single(ttlText)),\n\t\t\t.confirmed = confirmed,\n\t\t\t.cancelled = [=](Fn<void()> &&close) {\n\t\t\t\tstate->group->setChangedCallback(nullptr);\n\t\t\t\tclose();\n\t\t\t},\n\t\t\t.confirmText = tr::lng_sure_enable(),\n\t\t}));\n\t};\n\n\tstate->buttons->clear();\n\tfor (const auto &ttl : ttls) {\n\t\tconst auto ttlText = Ui::FormatTTLAfter(ttl);\n\t\tconst auto button = state->buttons->add(object_ptr<Ui::SettingsButton>(\n\t\t\tstate->buttons,\n\t\t\t(!ttl)\n\t\t\t\t? tr::lng_settings_ttl_after_off()\n\t\t\t\t: tr::lng_settings_ttl_after(\n\t\t\t\t\tlt_after_duration,\n\t\t\t\t\trpl::single(ttlText)),\n\t\t\tst::settingsButtonNoIcon));\n\t\tbutton->setClickedCallback([=] {\n\t\t\tif (state->group->current() == ttl) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!ttl) {\n\t\t\t\tstate->group->setChangedCallback(nullptr);\n\t\t\t\trequest(ttl);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tshowSure(ttl, false);\n\t\t});\n\t\tconst auto radio = Ui::CreateChild<Ui::Radiobutton>(\n\t\t\tbutton,\n\t\t\tstate->group,\n\t\t\tttl,\n\t\t\tQString());\n\t\tradio->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tradio->show();\n\t\tconst auto padding = button->st().padding;\n\t\tbutton->sizeValue(\n\t\t) | rpl::on_next([=](QSize s) {\n\t\t\tradio->moveToLeft(\n\t\t\t\ts.width() - radio->checkRect().width() - padding.left(),\n\t\t\t\tradio->checkRect().top());\n\t\t}, radio->lifetime());\n\t}\n\tstate->buttons->resizeToWidth(state->buttons->width());\n}\n\nvoid BuildTTLOptions(\n\t\tSectionBuilder &builder,\n\t\tnot_null<GlobalTTLState*> state) {\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"auto-delete/period\"_q,\n\t\t.title = tr::lng_settings_ttl_after_subtitle(),\n\t\t.keywords = { u\"ttl\"_q, u\"auto-delete\"_q, u\"timer\"_q },\n\t});\n\n\tbuilder.add([=](const WidgetContext &ctx) {\n\t\tconst auto controller = ctx.controller;\n\t\tctx.container->add(\n\t\t\tobject_ptr<Ui::VerticalLayout>::fromRaw(state->buttons.get()));\n\n\t\tconst auto &apiTTL = controller->session().api().selfDestruct();\n\t\tconst auto rebuild = [=](TimeId period) {\n\t\t\tRebuildButtons(state, controller, period);\n\t\t\tstate->group->setValue(period);\n\t\t};\n\t\trebuild(apiTTL.periodDefaultHistoryTTLCurrent());\n\t\tapiTTL.periodDefaultHistoryTTL(\n\t\t) | rpl::on_next(rebuild, ctx.container->lifetime());\n\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n}\n\nvoid BuildCustomButton(\n\t\tSectionBuilder &builder,\n\t\tnot_null<GlobalTTLState*> state) {\n\tbuilder.add([=](const WidgetContext &ctx) {\n\t\tconst auto controller = ctx.controller;\n\t\tconst auto show = controller->uiShow();\n\n\t\tconst auto showSure = [=](TimeId ttl, bool rebuild) {\n\t\t\tconst auto ttlText = Ui::FormatTTLAfter(ttl);\n\t\t\tconst auto confirmed = [=] {\n\t\t\t\tif (rebuild) {\n\t\t\t\t\tRebuildButtons(state, controller, ttl);\n\t\t\t\t}\n\t\t\t\tstate->group->setChangedCallback([=](int value) {\n\t\t\t\t\tstate->group->setChangedCallback(nullptr);\n\t\t\t\t\tstate->show->showToast(tr::lng_settings_ttl_after_toast(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_after_duration,\n\t\t\t\t\t\t{ .text = ttlText },\n\t\t\t\t\t\ttr::marked));\n\t\t\t\t\tstate->show->hideLayer();\n\t\t\t\t});\n\t\t\t\tcontroller->session().api().selfDestruct().updateDefaultHistoryTTL(ttl);\n\t\t\t};\n\t\t\tif (state->group->value()) {\n\t\t\t\tconfirmed();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->show->showBox(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_settings_ttl_after_sure(\n\t\t\t\t\tlt_after_duration,\n\t\t\t\t\trpl::single(ttlText)),\n\t\t\t\t.confirmed = confirmed,\n\t\t\t\t.cancelled = [=](Fn<void()> &&close) {\n\t\t\t\t\tstate->group->setChangedCallback(nullptr);\n\t\t\t\t\tclose();\n\t\t\t\t},\n\t\t\t\t.confirmText = tr::lng_sure_enable(),\n\t\t\t}));\n\t\t};\n\n\t\tstate->customButton = ctx.container->add(object_ptr<Ui::SettingsButton>(\n\t\t\tctx.container,\n\t\t\ttr::lng_settings_ttl_after_custom(),\n\t\t\tst::settingsButtonNoIcon));\n\t\tstate->customButton->setClickedCallback([=] {\n\t\t\tshow->showBox(Box(TTLMenu::TTLBox, TTLMenu::Args{\n\t\t\t\t.show = show,\n\t\t\t\t.startTtl = state->group->current(),\n\t\t\t\t.callback = [=](TimeId ttl, Fn<void()>) { showSure(ttl, true); },\n\t\t\t\t.hideDisable = true,\n\t\t\t}));\n\t\t});\n\t\tif (ctx.highlights) {\n\t\t\tctx.highlights->push_back({\n\t\t\t\tu\"auto-delete/set-custom\"_q,\n\t\t\t\t{ state->customButton.data(), { .rippleShape = true } },\n\t\t\t});\n\t\t}\n\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"auto-delete/set-custom\"_q,\n\t\t\t.title = tr::lng_settings_ttl_after_custom(tr::now),\n\t\t\t.keywords = { u\"custom\"_q, u\"ttl\"_q, u\"period\"_q },\n\t\t};\n\t});\n}\n\nvoid BuildApplyToExisting(\n\t\tSectionBuilder &builder,\n\t\tnot_null<GlobalTTLState*> state) {\n\tbuilder.addSkip();\n\n\tbuilder.add([=](const WidgetContext &ctx) {\n\t\tconst auto controller = ctx.controller;\n\t\tconst auto session = &controller->session();\n\n\t\tauto footer = object_ptr<Ui::FlatLabel>(\n\t\t\tctx.container,\n\t\t\ttr::lng_settings_ttl_after_about(\n\t\t\t\tlt_link,\n\t\t\t\ttr::lng_settings_ttl_after_about_link(\n\t\t\t\t) | rpl::map([](QString s) { return tr::link(s, 1); }),\n\t\t\t\ttr::marked),\n\t\t\tst::boxDividerLabel);\n\t\tfooter->setLink(1, std::make_shared<LambdaClickHandler>([=] {\n\t\t\tauto boxController = std::make_unique<TTLChatsBoxController>(session);\n\t\t\tauto initBox = [=, ctrl = boxController.get()](\n\t\t\t\t\tnot_null<PeerListBox*> box) {\n\t\t\t\tbox->addButton(tr::lng_settings_apply(), [=] {\n\t\t\t\t\tconst auto &peers = box->collectSelectedRows();\n\t\t\t\t\tif (peers.empty()) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst auto &apiTTL = session->api().selfDestruct();\n\t\t\t\t\tconst auto ttl = apiTTL.periodDefaultHistoryTTLCurrent();\n\t\t\t\t\tfor (const auto &peer : peers) {\n\t\t\t\t\t\tpeer->session().api().request(MTPmessages_SetHistoryTTL(\n\t\t\t\t\t\t\tpeer->input(),\n\t\t\t\t\t\t\tMTP_int(ttl)\n\t\t\t\t\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t\t\t\t\tpeer->session().api().applyUpdates(result);\n\t\t\t\t\t\t}).send();\n\t\t\t\t\t}\n\t\t\t\t\tbox->showToast(ttl\n\t\t\t\t\t\t? tr::lng_settings_ttl_select_chats_toast(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\tpeers.size(),\n\t\t\t\t\t\t\tlt_duration,\n\t\t\t\t\t\t\t{ .text = Ui::FormatTTL(ttl) },\n\t\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t\t: tr::lng_settings_ttl_select_chats_disabled_toast(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\tpeers.size(),\n\t\t\t\t\t\t\ttr::marked));\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t});\n\t\t\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t\t\t};\n\t\t\tcontroller->show(\n\t\t\t\tBox<PeerListBox>(std::move(boxController), std::move(initBox)));\n\t\t}));\n\t\tctx.container->add(object_ptr<Ui::DividerLabel>(\n\t\t\tctx.container,\n\t\t\tstd::move(footer),\n\t\t\tst::defaultBoxDividerLabelPadding));\n\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n}\n\nclass GlobalTTL : public Section<GlobalTTL> {\npublic:\n\tGlobalTTL(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\tvoid showFinished() override;\n\nprivate:\n\tvoid setupContent();\n\n\tstd::shared_ptr<GlobalTTLState> _state;\n\trpl::event_stream<> _showFinished;\n\n};\n\nGlobalTTL::GlobalTTL(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller)\n, _state(std::make_shared<GlobalTTLState>(GlobalTTLState{\n\t.group = std::make_shared<Ui::RadiobuttonGroup>(0),\n\t.show = controller->uiShow(),\n\t.buttons = Ui::CreateChild<Ui::VerticalLayout>(this),\n})) {\n\tsetupContent();\n}\n\nrpl::producer<QString> GlobalTTL::title() {\n\treturn tr::lng_settings_ttl_title();\n}\n\nvoid GlobalTTL::setupContent() {\n\tsetFocusPolicy(Qt::StrongFocus);\n\tsetFocus();\n\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tconst auto state = _state;\n\n\tconst SectionBuildMethod buildMethod = [state](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tauto &lifetime = container->lifetime();\n\t\tconst auto highlights = lifetime.make_state<HighlightRegistry>();\n\t\tconst auto isPaused = Window::PausedIn(\n\t\t\tcontroller,\n\t\t\tWindow::GifPauseReason::Layer);\n\t\tauto showFinishedDup = rpl::duplicate(showFinished);\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = isPaused,\n\t\t\t.highlights = highlights,\n\t\t});\n\n\t\tBuildTopContent(builder, std::move(showFinishedDup));\n\t\tBuildTTLOptions(builder, state.get());\n\t\tBuildCustomButton(builder, state.get());\n\t\tBuildApplyToExisting(builder, state.get());\n\n\t\tstd::move(showFinished) | rpl::on_next([=] {\n\t\t\tfor (const auto &[id, entry] : *highlights) {\n\t\t\t\tif (entry.widget) {\n\t\t\t\t\tcontroller->checkHighlightControl(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tentry.widget,\n\t\t\t\t\t\tbase::duplicate(entry.args));\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime);\n\t};\n\n\tbuild(content, buildMethod);\n\n\tUi::ResizeFitChild(this, content);\n}\n\nvoid GlobalTTL::showFinished() {\n\t_showFinished.fire({});\n\tSection<GlobalTTL>::showFinished();\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = GlobalTTL::Id(),\n\t.parentId = PrivacySecurityId(),\n\t.title = &tr::lng_settings_ttl_title,\n\t.icon = &st::menuIconTTL,\n}, [](SectionBuilder &builder) {\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"auto-delete/period\"_q,\n\t\t\t.title = tr::lng_settings_ttl_after_subtitle(tr::now),\n\t\t\t.keywords = { u\"ttl\"_q, u\"auto-delete\"_q, u\"timer\"_q },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"auto-delete/set-custom\"_q,\n\t\t\t.title = tr::lng_settings_ttl_after_custom(tr::now),\n\t\t\t.keywords = { u\"custom\"_q, u\"ttl\"_q, u\"period\"_q },\n\t\t};\n\t});\n});\n\n} // namespace\n\nType GlobalTTLId() {\n\treturn GlobalTTL::Id();\n}\n\nnamespace Builder {\n\nSectionBuildMethod GlobalTTLSection = kMeta.build;\n\n} // namespace Builder\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_global_ttl.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Settings {\n\n[[nodiscard]] Type GlobalTTLId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_information.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_information.h\"\n\n#include \"settings/sections/settings_main.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common_session.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/vertical_layout_reorder.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/box_content_divider.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/delayed_activation.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/unread_badge_paint.h\"\n#include \"ui/ui_utility.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/core_settings.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"boxes/add_contact_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"boxes/username_box.h\"\n#include \"boxes/peers/edit_peer_color_box.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_premium_limits.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"info/profile/info_profile_badge.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"main/main_domain.h\"\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_peer_menu.h\"\n#include \"apiwrap.h\"\n#include \"api/api_peer_photo.h\"\n#include \"api/api_user_names.h\"\n#include \"api/api_user_privacy.h\"\n#include \"base/call_delayed.h\"\n#include \"base/options.h\"\n#include \"base/unixtime.h\"\n#include \"base/random.h\"\n#include \"styles/style_chat.h\" // popupMenuExpandedSeparator\n#include \"styles/style_dialogs.h\" // dialogsPremiumIcon\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_window.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtCore/QBuffer>\n\nnamespace Settings {\nnamespace {\n\nusing namespace Builder;\n\nstruct InformationHighlightTargets {\n\tQPointer<Ui::RpWidget> photo;\n\tQPointer<Ui::RpWidget> uploadPhoto;\n\tQPointer<Ui::RpWidget> bio;\n\tQPointer<Ui::RpWidget> colorButton;\n\tQPointer<Ui::RpWidget> channelButton;\n\tQPointer<Ui::RpWidget> addAccount;\n\tQPointer<Ui::RpWidget> name;\n\tQPointer<Ui::RpWidget> phone;\n\tQPointer<Ui::RpWidget> username;\n\tQPointer<Ui::RpWidget> birthday;\n};\n\nconstexpr auto kSaveBioTimeout = 1000;\nconstexpr auto kPlayStatusLimit = 2;\n\nclass ComposedBadge final : public Ui::RpWidget {\npublic:\n\tComposedBadge(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Ui::SettingsButton*> button,\n\t\tnot_null<::Main::Session*> session,\n\t\trpl::producer<QString> &&text,\n\t\tbool hasUnread,\n\t\tFn<bool()> animationPaused);\n\nprivate:\n\trpl::variable<QString> _text;\n\trpl::event_stream<int> _unreadWidth;\n\trpl::event_stream<int> _premiumWidth;\n\n\tQPointer<Ui::RpWidget> _unread;\n\tInfo::Profile::Badge _badge;\n\n};\n\nComposedBadge::ComposedBadge(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Ui::SettingsButton*> button,\n\tnot_null<::Main::Session*> session,\n\trpl::producer<QString> &&text,\n\tbool hasUnread,\n\tFn<bool()> animationPaused)\n: Ui::RpWidget(parent)\n, _text(std::move(text))\n, _badge(\n\t\tthis,\n\t\tst::settingsInfoPeerBadge,\n\t\tsession,\n\t\tInfo::Profile::BadgeContentForPeer(session->user()),\n\t\tnullptr,\n\t\tstd::move(animationPaused),\n\t\tkPlayStatusLimit,\n\t\tInfo::Profile::BadgeType::Premium) {\n\tif (hasUnread) {\n\t\t_unread = Badge::CreateUnread(this, rpl::single(\n\t\t\trpl::empty\n\t\t) | rpl::then(\n\t\t\tsession->data().unreadBadgeChanges()\n\t\t) | rpl::map([=] {\n\t\t\tauto &owner = session->data();\n\t\t\treturn Badge::UnreadBadge{\n\t\t\t\towner.unreadWithMentionsBadge(),\n\t\t\t\towner.unreadWithMentionsBadgeMuted(),\n\t\t\t};\n\t\t}));\n\t\trpl::combine(\n\t\t\t_unread->shownValue(),\n\t\t\t_unread->widthValue()\n\t\t) | rpl::map([=](bool shown, int width) {\n\t\t\treturn shown ? width : 0;\n\t\t}) | rpl::start_to_stream(_unreadWidth, _unread->lifetime());\n\t}\n\n\t_badge.updated(\n\t) | rpl::on_next([=] {\n\t\tif (const auto button = _badge.widget()) {\n\t\t\tbutton->widthValue(\n\t\t\t) | rpl::start_to_stream(_premiumWidth, button->lifetime());\n\t\t} else {\n\t\t\t_premiumWidth.fire(0);\n\t\t}\n\t}, lifetime());\n\n\tauto textWidth = _text.value() | rpl::map([=] {\n\t\treturn button->fullTextWidth();\n\t});\n\trpl::combine(\n\t\t_unreadWidth.events_starting_with(_unread ? _unread->width() : 0),\n\t\t_premiumWidth.events_starting_with(_badge.widget()\n\t\t\t? _badge.widget()->width()\n\t\t\t: 0),\n\t\tstd::move(textWidth),\n\t\tbutton->sizeValue()\n\t) | rpl::on_next([=](\n\t\t\tint unreadWidth,\n\t\t\tint premiumWidth,\n\t\t\tint textWidth,\n\t\t\tconst QSize &buttonSize) {\n\t\tconst auto &st = button->st();\n\t\tconst auto skip = st.style.font->spacew;\n\t\tconst auto textRightPosition = st.padding.left()\n\t\t\t+ textWidth\n\t\t\t+ skip;\n\t\tconst auto minWidth = unreadWidth + premiumWidth + skip;\n\t\tconst auto maxTextWidth = buttonSize.width()\n\t\t\t- minWidth\n\t\t\t- st.padding.right();\n\n\t\tconst auto finalTextRight = std::min(textRightPosition, maxTextWidth);\n\n\t\tresize(\n\t\t\tbuttonSize.width() - st.padding.right() - finalTextRight,\n\t\t\tbuttonSize.height());\n\n\t\t_badge.move(\n\t\t\t0,\n\t\t\tst.padding.top(),\n\t\t\tbuttonSize.height() - st.padding.top());\n\t\tif (_unread) {\n\t\t\t_unread->moveToRight(\n\t\t\t\t0,\n\t\t\t\t(buttonSize.height() - _unread->height()) / 2);\n\t\t}\n\t}, lifetime());\n}\n\nclass AccountsList final {\npublic:\n\tAccountsList(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] rpl::producer<> closeRequests() const;\n\t[[nodiscard]] Ui::RpWidget *addAccountButton() const;\n\nprivate:\n\tvoid setup();\n\n\t[[nodiscard]] not_null<Ui::SlideWrap<Ui::SettingsButton>*> setupAdd();\n\tvoid rebuild();\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<Ui::VerticalLayout*> _outer;\n\tint _outerIndex = 0;\n\n\tUi::SlideWrap<Ui::SettingsButton> *_addAccount = nullptr;\n\tbase::flat_map<\n\t\tnot_null<::Main::Account*>,\n\t\tbase::unique_qptr<Ui::SettingsButton>> _watched;\n\n\tbase::unique_qptr<Ui::PopupMenu> _contextMenu;\n\tstd::unique_ptr<Ui::VerticalLayoutReorder> _reorder;\n\tint _reordering = 0;\n\n\trpl::event_stream<> _closeRequests;\n\n\tbase::binary_guard _accountSwitchGuard;\n\n};\n\n[[nodiscard]] rpl::producer<TextWithEntities> StatusValue(\n\t\tnot_null<UserData*> user) {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\t\tconst auto timer = lifetime.make_state<base::Timer>();\n\t\tconst auto push = [=] {\n\t\t\tconst auto now = base::unixtime::now();\n\t\t\tconsumer.put_next(Data::OnlineTextActive(user, now)\n\t\t\t\t? tr::link(Data::OnlineText(user, now))\n\t\t\t\t: tr::marked(Data::OnlineText(user, now)));\n\t\t\ttimer->callOnce(Data::OnlineChangeTimeout(user, now));\n\t\t};\n\t\ttimer->setCallback(push);\n\t\tuser->session().changes().peerFlagsValue(\n\t\t\tuser,\n\t\t\tData::PeerUpdate::Flag::OnlineStatus\n\t\t) | rpl::on_next(push, lifetime);\n\t\treturn lifetime;\n\t};\n}\n\nvoid SetupPhoto(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> self,\n\t\tInformationHighlightTargets *targets) {\n\tconst auto wrap = container->add(object_ptr<Ui::FixedHeightWidget>(\n\t\tcontainer,\n\t\tst::settingsInfoPhotoHeight));\n\tconst auto photo = Ui::CreateChild<Ui::UserpicButton>(\n\t\twrap,\n\t\tcontroller,\n\t\tself,\n\t\tUi::UserpicButton::Role::OpenPhoto,\n\t\tUi::UserpicButton::Source::PeerPhoto,\n\t\tst::settingsInfoPhoto);\n\tconst auto upload = CreateUploadSubButton(wrap, controller);\n\tif (targets) {\n\t\ttargets->photo = photo;\n\t\ttargets->uploadPhoto = upload;\n\t}\n\n\tupload->chosenImages(\n\t) | rpl::on_next([=](Ui::UserpicButton::ChosenImage &&chosen) {\n\t\tauto &image = chosen.image;\n\t\tUpdatePhotoLocally(self, image);\n\t\tphoto->showCustom(base::duplicate(image));\n\t\tconst auto isMarkup = (chosen.markup.documentId != 0);\n\t\tself->session().api().peerPhoto().upload(\n\t\t\tself,\n\t\t\t{\n\t\t\t\tstd::move(image),\n\t\t\t\tchosen.markup.documentId,\n\t\t\t\tchosen.markup.colors,\n\t\t\t});\n\t\tif (!isMarkup) {\n\t\t\tphoto->showUploadProgress();\n\t\t}\n\t}, upload->lifetime());\n\n\tconst auto name = Ui::CreateChild<Ui::FlatLabel>(\n\t\twrap,\n\t\tInfo::Profile::NameValue(self),\n\t\tst::settingsCoverName);\n\tconst auto status = Ui::CreateChild<Ui::FlatLabel>(\n\t\twrap,\n\t\tStatusValue(self),\n\t\tst::settingsCoverStatus);\n\tstatus->setAttribute(Qt::WA_TransparentForMouseEvents);\n\trpl::combine(\n\t\twrap->widthValue(),\n\t\tphoto->widthValue(),\n\t\tInfo::Profile::NameValue(self),\n\t\tstatus->widthValue()\n\t) | rpl::on_next([=](\n\t\t\tint max,\n\t\t\tint photoWidth,\n\t\t\tconst QString&,\n\t\t\tint statusWidth) {\n\t\tphoto->moveToLeft(\n\t\t\t(max - photoWidth) / 2,\n\t\t\tst::settingsInfoPhotoTop);\n\t\tupload->moveToLeft(\n\t\t\t((max - photoWidth) / 2\n\t\t\t\t+ photoWidth\n\t\t\t\t- upload->width()\n\t\t\t\t+ st::settingsInfoUploadLeft),\n\t\t\tphoto->y() + photo->height() - upload->height());\n\t\tconst auto skip = st::settingsButton.iconLeft;\n\t\tname->resizeToNaturalWidth(max - 2 * skip);\n\t\tname->moveToLeft(\n\t\t\t(max - name->width()) / 2,\n\t\t\t(photo->y() + photo->height() + st::settingsInfoPhotoSkip));\n\t\tstatus->moveToLeft(\n\t\t\t(max - statusWidth) / 2,\n\t\t\t(name->y() + name->height() + st::settingsInfoNameSkip));\n\t}, photo->lifetime());\n}\n\nvoid ShowMenu(\n\t\tQWidget *parent,\n\t\tconst QString &copyButton,\n\t\tconst QString &text) {\n\tconst auto menu = Ui::CreateChild<Ui::PopupMenu>(parent);\n\n\tmenu->addAction(copyButton, [=] {\n\t\tQGuiApplication::clipboard()->setText(text);\n\t});\n\tmenu->popup(QCursor::pos());\n}\n\nnot_null<Ui::SettingsButton*> AddRow(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> label,\n\t\trpl::producer<TextWithEntities> value,\n\t\tconst QString &copyButton,\n\t\tFn<void()> edit,\n\t\tIconDescriptor &&descriptor) {\n\tconst auto wrap = AddButtonWithLabel(\n\t\tcontainer,\n\t\tstd::move(label),\n\t\tstd::move(value) | rpl::map([](const auto &t) { return t.text; }),\n\t\tst::settingsButton,\n\t\tstd::move(descriptor));\n\tconst auto forcopy = Ui::CreateChild<QString>(wrap.get());\n\twrap->setAcceptBoth();\n\twrap->clicks(\n\t) | rpl::filter([=] {\n\t\treturn !wrap->isDisabled();\n\t}) | rpl::on_next([=](Qt::MouseButton button) {\n\t\tif (button == Qt::LeftButton) {\n\t\t\tedit();\n\t\t} else if (!forcopy->isEmpty()) {\n\t\t\tShowMenu(wrap, copyButton, *forcopy);\n\t\t}\n\t}, wrap->lifetime());\n\n\tauto existing = base::duplicate(\n\t\tvalue\n\t) | rpl::map([](const TextWithEntities &text) {\n\t\treturn text.entities.isEmpty();\n\t});\n\tbase::duplicate(\n\t\tvalue\n\t) | rpl::filter([](const TextWithEntities &text) {\n\t\treturn text.entities.isEmpty();\n\t}) | rpl::on_next([=](const TextWithEntities &text) {\n\t\t*forcopy = text.text;\n\t}, wrap->lifetime());\n\treturn wrap;\n}\n\nvoid SetupBirthday(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> self,\n\t\tInformationHighlightTargets *targets) {\n\tconst auto session = &self->session();\n\n\tUi::AddSkip(container);\n\n\tauto value = rpl::combine(\n\t\tInfo::Profile::BirthdayValue(self),\n\t\ttr::lng_settings_birthday_add()\n\t) | rpl::map([](Data::Birthday birthday, const QString &add) {\n\t\tconst auto text = Data::BirthdayText(birthday);\n\t\treturn TextWithEntities{ !text.isEmpty() ? text : add };\n\t});\n\tconst auto edit = [=] {\n\t\tCore::App().openInternalUrl(\n\t\t\tu\"internal:edit_birthday\"_q,\n\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.sessionWindow = base::make_weak(controller),\n\t\t\t}));\n\t};\n\tconst auto birthdayButton = AddRow(\n\t\tcontainer,\n\t\ttr::lng_settings_birthday_label(),\n\t\tstd::move(value),\n\t\ttr::lng_mediaview_copy(tr::now),\n\t\tedit,\n\t\t{ &st::menuIconGiftPremium });\n\tif (targets) {\n\t\ttargets->birthday = birthdayButton;\n\t}\n\n\tconst auto key = Api::UserPrivacy::Key::Birthday;\n\tsession->api().userPrivacy().reload(key);\n\tauto isExactlyContacts = session->api().userPrivacy().value(\n\t\tkey\n\t) | rpl::map([=](const Api::UserPrivacy::Rule &value) {\n\t\treturn (value.option == Api::UserPrivacy::Option::Contacts)\n\t\t\t&& value.always.peers.empty()\n\t\t\t&& !value.always.premiums\n\t\t\t&& value.never.peers.empty();\n\t}) | rpl::distinct_until_changed();\n\n\tUi::AddSkip(container);\n\tUi::AddDividerText(container, rpl::conditional(\n\t\tstd::move(isExactlyContacts),\n\t\ttr::lng_settings_birthday_contacts(\n\t\t\tlt_link,\n\t\t\ttr::lng_settings_birthday_contacts_link(\n\t\t\t\ttr::url(u\"internal:edit_privacy_birthday\"_q)),\n\t\t\ttr::marked),\n\t\ttr::lng_settings_birthday_about(\n\t\t\tlt_link,\n\t\t\ttr::lng_settings_birthday_about_link(\n\t\t\t\ttr::url(u\"internal:edit_privacy_birthday\"_q)),\n\t\t\ttr::marked)));\n}\n\nvoid SetupPersonalChannel(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> self,\n\t\tInformationHighlightTargets *targets) {\n\tUi::AddSkip(container);\n\n\tauto value = rpl::combine(\n\t\tInfo::Profile::PersonalChannelValue(self),\n\t\ttr::lng_settings_channel_add()\n\t) | rpl::map([](ChannelData *channel, const QString &add) {\n\t\treturn TextWithEntities{ channel ? channel->name() : add };\n\t});\n\tconst auto edit = [=] {\n\t\tCore::App().openInternalUrl(\n\t\t\tu\"internal:edit_personal_channel\"_q,\n\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.sessionWindow = base::make_weak(controller),\n\t\t\t}));\n\t};\n\tconst auto channelButton = AddRow(\n\t\tcontainer,\n\t\ttr::lng_settings_channel_label(),\n\t\tstd::move(value),\n\t\ttr::lng_mediaview_copy(tr::now),\n\t\tedit,\n\t\t{ &st::menuIconChannel });\n\n\tconst auto colorButton = AddPeerColorButton(\n\t\tcontainer,\n\t\tcontroller->uiShow(),\n\t\tself,\n\t\tst::settingsColorButton);\n\tif (targets) {\n\t\ttargets->channelButton = channelButton;\n\t\ttargets->colorButton = colorButton;\n\t}\n\n\tUi::AddSkip(container);\n\tUi::AddDivider(container);\n}\n\nvoid SetupRows(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> self,\n\t\tInformationHighlightTargets *targets) {\n\tconst auto session = &self->session();\n\n\tUi::AddSkip(container);\n\n\tconst auto showEditName = [=] {\n\t\tif (controller->showFrozenError()) {\n\t\t\treturn;\n\t\t}\n\t\tcontroller->show(Box<EditNameBox>(self));\n\t};\n\tconst auto nameButton = AddRow(\n\t\tcontainer,\n\t\ttr::lng_settings_name_label(),\n\t\tInfo::Profile::NameValue(self) | rpl::map(tr::marked),\n\t\ttr::lng_profile_copy_fullname(tr::now),\n\t\tshowEditName,\n\t\t{ &st::menuIconProfile });\n\tif (targets) {\n\t\ttargets->name = nameButton;\n\t}\n\n\tconst auto showChangePhone = [=] {\n\t\tcontroller->show(\n\t\t\tUi::MakeInformBox(tr::lng_change_phone_error()));\n\t\tcontroller->window().activate();\n\t};\n\tconst auto phoneButton = AddRow(\n\t\tcontainer,\n\t\ttr::lng_settings_phone_label(),\n\t\tInfo::Profile::PhoneValue(self),\n\t\ttr::lng_profile_copy_phone(tr::now),\n\t\tshowChangePhone,\n\t\t{ &st::menuIconPhone });\n\tif (targets) {\n\t\ttargets->phone = phoneButton;\n\t}\n\n\tauto username = Info::Profile::UsernameValue(self);\n\tauto empty = base::duplicate(\n\t\tusername\n\t) | rpl::map([](const TextWithEntities &username) {\n\t\treturn username.text.isEmpty();\n\t});\n\tauto label = rpl::combine(\n\t\ttr::lng_settings_username_label(),\n\t\tstd::move(empty)\n\t) | rpl::map([](const QString &label, bool empty) {\n\t\treturn empty ? \"t.me/username\" : label;\n\t});\n\tauto usernameValue = rpl::combine(\n\t\tstd::move(username),\n\t\ttr::lng_settings_username_add()\n\t) | rpl::map([](const TextWithEntities &username, const QString &add) {\n\t\tif (!username.text.isEmpty()) {\n\t\t\treturn username;\n\t\t}\n\t\tauto result = TextWithEntities{ add };\n\t\tresult.entities.push_back({\n\t\t\tEntityType::CustomUrl,\n\t\t\t0,\n\t\t\tint(add.size()),\n\t\t\t\"internal:edit_username\" });\n\t\treturn result;\n\t});\n\tsession->api().usernames().requestToCache(session->user());\n\tconst auto usernameButton = AddRow(\n\t\tcontainer,\n\t\tstd::move(label),\n\t\tstd::move(usernameValue),\n\t\ttr::lng_context_copy_mention(tr::now),\n\t\t[=] {\n\t\t\tif (controller->showFrozenError()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto box = controller->show(\n\t\t\t\tBox(UsernamesBox, session->user()));\n\t\t\tbox->boxClosing(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tsession->api().usernames().requestToCache(session->user());\n\t\t\t}, box->lifetime());\n\t\t},\n\t\t{ &st::menuIconUsername });\n\tif (targets) {\n\t\ttargets->username = usernameButton;\n\t}\n\n\tUi::AddSkip(container);\n\tUi::AddDividerText(container, tr::lng_settings_username_about());\n}\n\nvoid SetupBio(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<UserData*> self,\n\t\tInformationHighlightTargets *targets) {\n\tconst auto limits = Data::PremiumLimits(&self->session());\n\tconst auto defaultLimit = limits.aboutLengthDefault();\n\tconst auto premiumLimit = limits.aboutLengthPremium();\n\tconst auto bioStyle = [=] {\n\t\tauto result = st::settingsBio;\n\t\tresult.textMargins.setRight(st::boxTextFont->spacew\n\t\t\t+ st::boxTextFont->width('-' + QString::number(premiumLimit)));\n\t\treturn result;\n\t};\n\tconst auto style = Ui::AttachAsChild(container, bioStyle());\n\tconst auto current = Ui::AttachAsChild(container, self->about());\n\tconst auto changed = Ui::CreateChild<rpl::event_stream<bool>>(\n\t\tcontainer.get());\n\tconst auto bio = container->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tcontainer,\n\t\t\t*style,\n\t\t\tUi::InputField::Mode::MultiLine,\n\t\t\ttr::lng_bio_placeholder(),\n\t\t\t*current),\n\t\tst::settingsBioMargins);\n\tif (targets) {\n\t\ttargets->bio = bio;\n\t}\n\n\tconst auto countdown = Ui::CreateChild<Ui::FlatLabel>(\n\t\tcontainer.get(),\n\t\tQString(),\n\t\tst::settingsBioCountdown);\n\n\trpl::combine(\n\t\tbio->geometryValue(),\n\t\tcountdown->widthValue()\n\t) | rpl::on_next([=](QRect geometry, int width) {\n\t\tcountdown->move(\n\t\t\tgeometry.x() + geometry.width() - width,\n\t\t\tgeometry.y() + style->textMargins.top());\n\t}, countdown->lifetime());\n\n\tconst auto assign = [=](QString text) {\n\t\tauto position = bio->textCursor().position();\n\t\tbio->setText(text.replace('\\n', ' '));\n\t\tauto cursor = bio->textCursor();\n\t\tcursor.setPosition(position);\n\t\tbio->setTextCursor(cursor);\n\t};\n\tconst auto updated = [=] {\n\t\tauto text = bio->getLastText();\n\t\tif (text.indexOf('\\n') >= 0) {\n\t\t\tassign(text);\n\t\t\ttext = bio->getLastText();\n\t\t}\n\t\tchanged->fire(*current != text);\n\t\tconst auto limit = self->isPremium() ? premiumLimit : defaultLimit;\n\t\tconst auto countLeft = limit - Ui::ComputeFieldCharacterCount(bio);\n\t\tcountdown->setText(QString::number(countLeft));\n\t\tcountdown->setTextColorOverride(\n\t\t\tcountLeft < 0 ? st::boxTextFgError->c : std::optional<QColor>());\n\t};\n\tconst auto save = [=] {\n\t\tself->session().api().saveSelfBio(\n\t\t\tTextUtilities::PrepareForSending(bio->getLastText()));\n\t};\n\n\tInfo::Profile::AboutValue(\n\t\tself\n\t) | rpl::on_next([=](const TextWithEntities &text) {\n\t\tconst auto wasChanged = (*current != bio->getLastText());\n\t\t*current = text.text;\n\t\tif (wasChanged) {\n\t\t\tchanged->fire(*current != bio->getLastText());\n\t\t} else {\n\t\t\tassign(text.text);\n\t\t\t*current = bio->getLastText();\n\t\t}\n\t}, bio->lifetime());\n\n\tconst auto generation = Ui::CreateChild<int>(bio);\n\tchanged->events(\n\t) | rpl::on_next([=](bool changed) {\n\t\tif (changed) {\n\t\t\tconst auto saved = *generation = std::abs(*generation) + 1;\n\t\t\tbase::call_delayed(kSaveBioTimeout, bio, [=] {\n\t\t\t\tif (*generation == saved) {\n\t\t\t\t\tsave();\n\t\t\t\t\t*generation = 0;\n\t\t\t\t}\n\t\t\t});\n\t\t} else if (*generation > 0) {\n\t\t\t*generation = -*generation;\n\t\t}\n\t}, bio->lifetime());\n\n\tcontainer->lifetime().add([=] {\n\t\tif (*generation > 0) {\n\t\t\tsave();\n\t\t}\n\t});\n\n\tbio->setMaxLength(premiumLimit * 2);\n\tbio->setSubmitSettings(Ui::InputField::SubmitSettings::Both);\n\tauto cursor = bio->textCursor();\n\tcursor.setPosition(bio->getLastText().size());\n\tbio->setTextCursor(cursor);\n\tbio->submits() | rpl::on_next([=] { save(); }, bio->lifetime());\n\tbio->changes() | rpl::on_next(updated, bio->lifetime());\n\tbio->setInstantReplaces(Ui::InstantReplaces::Default());\n\tbio->setInstantReplacesEnabled(\n\t\tCore::App().settings().replaceEmojiValue(),\n\t\tCore::App().settings().systemTextReplaceValue());\n\tUi::Emoji::SuggestionsController::Init(\n\t\tcontainer->window(),\n\t\tbio,\n\t\t&self->session());\n\tupdated();\n\n\tUi::AddDividerText(container, tr::lng_settings_about_bio());\n}\n\nvoid SetupAccountsWrap(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tInformationHighlightTargets *targets) {\n\tUi::AddSkip(container);\n\n\tauto events = SetupAccounts(container, controller);\n\tif (targets) {\n\t\ttargets->addAccount = events.addAccountButton;\n\t}\n}\n\n[[nodiscard]] bool IsAltShift(Qt::KeyboardModifiers modifiers) {\n\treturn (modifiers & Qt::ShiftModifier) && (modifiers & Qt::AltModifier);\n}\n\n[[nodiscard]] object_ptr<Ui::SettingsButton> MakeAccountButton(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<::Main::Account*> account,\n\t\tFn<void(Qt::KeyboardModifiers)> callback,\n\t\tbool locked) {\n\tconst auto active = (account == &window->session().account());\n\tconst auto session = &account->session();\n\tconst auto user = session->user();\n\n\tauto text = rpl::single(\n\t\tuser->name()\n\t) | rpl::then(session->changes().realtimeNameUpdates(\n\t\tuser\n\t) | rpl::map([=] {\n\t\treturn user->name();\n\t}));\n\tauto result = object_ptr<Ui::SettingsButton>(\n\t\tparent,\n\t\trpl::duplicate(text),\n\t\tst::mainMenuAddAccountButton);\n\tconst auto raw = result.data();\n\n\t{\n\t\tconst auto container = Badge::AddRight(raw, st::mainMenuAccountLine);\n\t\tconst auto composedBadge = Ui::CreateChild<ComposedBadge>(\n\t\t\tcontainer.get(),\n\t\t\traw,\n\t\t\tsession,\n\t\t\tstd::move(text),\n\t\t\t!active,\n\t\t\t[=] { return window->isGifPausedAtLeastFor(\n\t\t\t\tWindow::GifPauseReason::Layer); });\n\t\tcomposedBadge->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\tcontainer->resize(s);\n\t\t}, container->lifetime());\n\t}\n\n\tstruct State {\n\t\tState(QWidget *parent) : userpic(parent) {\n\t\t\tuserpic.setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t}\n\n\t\tUi::RpWidget userpic;\n\t\tUi::PeerUserpicView view;\n\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>(raw);\n\n\tconst auto userpicSkip = 2 * st::mainMenuAccountLine + st::lineWidth;\n\tconst auto userpicSize = st::mainMenuAccountSize\n\t\t+ userpicSkip * 2;\n\traw->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tconst auto left = st::mainMenuAddAccountButton.iconLeft\n\t\t\t+ (st::settingsIconAdd.width() - userpicSize) / 2;\n\t\tconst auto top = (height - userpicSize) / 2;\n\t\tstate->userpic.setGeometry(left, top, userpicSize, userpicSize);\n\t}, state->userpic.lifetime());\n\n\tstate->userpic.paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = Painter(&state->userpic);\n\t\tconst auto size = st::mainMenuAccountSize;\n\t\tconst auto line = st::mainMenuAccountLine;\n\t\tconst auto skip = 2 * line + st::lineWidth;\n\t\tconst auto full = size + skip * 2;\n\t\tuser->paintUserpicLeft(p, state->view, skip, skip, full, size);\n\t\tif (active) {\n\t\t\tconst auto shift = st::lineWidth + (line * 0.5);\n\t\t\tconst auto diameter = full - 2 * shift;\n\t\t\tconst auto rect = QRectF(shift, shift, diameter, diameter);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tauto pen = st::windowBgActive->p; // The same as '+' in add.\n\t\t\tpen.setWidthF(line);\n\t\t\tp.setPen(pen);\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\tif (Core::App().settings().fork().squareUserpics()) {\n\t\t\t\tp.drawRect(rect);\n\t\t\t} else {\n\t\t\t\tp.drawEllipse(rect);\n\t\t\t}\n\t\t}\n\t}, state->userpic.lifetime());\n\n\traw->setAcceptBoth(true);\n\traw->clicks(\n\t) | rpl::on_next([=](Qt::MouseButton which) {\n\t\tif (which == Qt::LeftButton) {\n\t\t\tcallback(raw->clickModifiers());\n\t\t\treturn;\n\t\t} else if (which == Qt::MiddleButton) {\n\t\t\tcallback(Qt::ControlModifier);\n\t\t\treturn;\n\t\t} else if (which != Qt::RightButton) {\n\t\t\treturn;\n\t\t}\n\t\tif (state->menu) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto isActive = session == &window->session();\n\t\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\traw,\n\t\t\tst::popupMenuExpandedSeparator);\n\t\tconst auto addAction = Ui::Menu::CreateAddActionCallback(\n\t\t\tstate->menu);\n\t\tif (!isActive) {\n\t\t\taddAction(tr::lng_context_new_window(tr::now), [=] {\n\t\t\t\tUi::PreventDelayedActivation();\n\t\t\t\tcallback(Qt::ControlModifier);\n\t\t\t}, &st::menuIconNewWindow);\n\t\t\tWindow::AddSeparatorAndShiftUp(addAction);\n\t\t}\n\n\t\taddAction(tr::lng_profile_copy_phone(tr::now), [=] {\n\t\t\tconst auto phone = rpl::variable<TextWithEntities>(\n\t\t\t\tInfo::Profile::PhoneValue(session->user()));\n\t\t\tQGuiApplication::clipboard()->setText(phone.current().text);\n\t\t}, &st::menuIconCopy);\n\n\t\tif (!locked) {\n\t\t\tif (!isActive) {\n\t\t\t\taddAction(tr::lng_menu_activate(tr::now), [=] {\n\t\t\t\t\tcallback({});\n\t\t\t\t}, &st::menuIconProfile);\n\t\t\t}\n\t\t\tWindow::MenuAddMarkAsReadAllChatsAction(\n\t\t\t\tsession,\n\t\t\t\twindow->uiShow(),\n\t\t\t\taddAction);\n\t\t}\n\n\t\tif (!isActive) {\n\t\t\tauto logoutCallback = [=] {\n\t\t\t\tconst auto callback = [=](Fn<void()> &&close) {\n\t\t\t\t\tclose();\n\t\t\t\t\tCore::App().logoutWithChecks(&session->account());\n\t\t\t\t};\n\t\t\t\twindow->show(\n\t\t\t\t\tUi::MakeConfirmBox({\n\t\t\t\t\t\t.text = tr::lng_sure_logout(),\n\t\t\t\t\t\t.confirmed = crl::guard(session, callback),\n\t\t\t\t\t\t.confirmText = tr::lng_settings_logout(),\n\t\t\t\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t\t\t}),\n\t\t\t\t\tUi::LayerOption::CloseOther);\n\t\t\t};\n\t\t\taddAction({\n\t\t\t\t.text = tr::lng_settings_logout(tr::now),\n\t\t\t\t.handler = std::move(logoutCallback),\n\t\t\t\t.icon = &st::menuIconLeaveAttention,\n\t\t\t\t.isAttention = true,\n\t\t\t});\n\t\t}\n\t\tstate->menu->popup(QCursor::pos());\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\nAccountsList::AccountsList(\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<Window::SessionController*> controller)\n: _controller(controller)\n, _outer(container)\n, _outerIndex(container->count()) {\n\tsetup();\n}\n\nrpl::producer<> AccountsList::closeRequests() const {\n\treturn _closeRequests.events();\n}\n\nUi::RpWidget *AccountsList::addAccountButton() const {\n\treturn _addAccount ? _addAccount->entity() : nullptr;\n}\n\nvoid AccountsList::setup() {\n\t_addAccount = setupAdd();\n\n\trpl::single(rpl::empty) | rpl::then(\n\t\tCore::App().domain().accountsChanges()\n\t) | rpl::on_next([=] {\n\t\tconst auto &list = Core::App().domain().accounts();\n\t\tconst auto exists = [&](not_null<::Main::Account*> account) {\n\t\t\tfor (const auto &[index, existing] : list) {\n\t\t\t\tif (account == existing.get()) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\t\tfor (auto i = _watched.begin(); i != _watched.end();) {\n\t\t\tif (!exists(i->first)) {\n\t\t\t\ti = _watched.erase(i);\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t\tfor (const auto &[index, account] : list) {\n\t\t\tif (_watched.emplace(account.get()).second) {\n\t\t\t\taccount->sessionChanges(\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\trebuild();\n\t\t\t\t}, _outer->lifetime());\n\t\t\t}\n\t\t}\n\t\trebuild();\n\t}, _outer->lifetime());\n\n\tCore::App().domain().maxAccountsChanges(\n\t) | rpl::on_next([=] {\n\t\tfor (auto i = _watched.begin(); i != _watched.end(); i++) {\n\t\t\ti->second = nullptr;\n\t\t}\n\t\trebuild();\n\t}, _outer->lifetime());\n}\n\n\nnot_null<Ui::SlideWrap<Ui::SettingsButton>*> AccountsList::setupAdd() {\n\tconst auto result = _outer->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\t_outer.get(),\n\t\t\tCreateButtonWithIcon(\n\t\t\t\t_outer.get(),\n\t\t\t\ttr::lng_menu_add_account(),\n\t\t\t\tst::mainMenuAddAccountButton,\n\t\t\t\t{\n\t\t\t\t\t&st::settingsIconAdd,\n\t\t\t\t\tIconType::Round,\n\t\t\t\t\t&st::windowBgActive\n\t\t\t\t})))->setDuration(0);\n\tconst auto button = result->entity();\n\n\tusing Environment = MTP::Environment;\n\tconst auto add = [=](Environment environment, bool newWindow = false) {\n\t\tauto &domain = _controller->session().domain();\n\t\tauto found = false;\n\t\tfor (const auto &[index, account] : domain.accounts()) {\n\t\t\tconst auto raw = account.get();\n\t\t\tif (!raw->sessionExists()\n\t\t\t\t&& raw->mtp().environment() == environment) {\n\t\t\t\tfound = true;\n\t\t\t}\n\t\t}\n\t\tif (!found && domain.accounts().size() >= domain.maxAccounts()) {\n\t\t\t_controller->show(\n\t\t\t\tBox(AccountsLimitBox, &_controller->session()));\n\t\t} else if (newWindow) {\n\t\t\tdomain.addActivated(environment, true);\n\t\t} else {\n\t\t\t_controller->window().preventOrInvoke([=] {\n\t\t\t\t_controller->session().domain().addActivated(environment);\n\t\t\t});\n\t\t}\n\t};\n\n\tbutton->setAcceptBoth(true);\n\tbutton->clicks(\n\t) | rpl::on_next([=](Qt::MouseButton which) {\n\t\tif (which == Qt::LeftButton) {\n\t\t\tconst auto modifiers = button->clickModifiers();\n\t\t\tconst auto newWindow = (modifiers & Qt::ControlModifier);\n\t\t\tadd(Environment::Production, newWindow);\n\t\t\treturn;\n\t\t} else if (which != Qt::RightButton\n\t\t\t|| !IsAltShift(button->clickModifiers())) {\n#ifdef _DEBUG\n\t\t\tif (which != Qt::RightButton) {\n\t\t\t\treturn;\n\t\t\t}\n#else // _DEBUG\n\t\t\treturn;\n#endif // !_DEBUG\n\t\t}\n\t\t_contextMenu = base::make_unique_q<Ui::PopupMenu>(_outer);\n\t\t_contextMenu->addAction(\"Production Server\", [=] {\n\t\t\tadd(Environment::Production);\n\t\t});\n\t\t_contextMenu->addAction(\"Test Server\", [=] {\n\t\t\tadd(Environment::Test);\n\t\t});\n\t\t_contextMenu->popup(QCursor::pos());\n\t}, button->lifetime());\n\n\treturn result;\n}\n\nvoid AccountsList::rebuild() {\n\tconst auto inner = _outer->insert(\n\t\t_outerIndex,\n\t\tobject_ptr<Ui::VerticalLayout>(_outer.get()));\n\n\t_reorder = std::make_unique<Ui::VerticalLayoutReorder>(inner);\n\t_reorder->updates(\n\t) | rpl::on_next([=](Ui::VerticalLayoutReorder::Single data) {\n\t\tusing State = Ui::VerticalLayoutReorder::State;\n\t\tif (data.state == State::Started) {\n\t\t\t++_reordering;\n\t\t} else {\n\t\t\tUi::PostponeCall(inner, [=] {\n\t\t\t\t--_reordering;\n\t\t\t});\n\t\t\tif (data.state == State::Applied) {\n\t\t\t\tstd::vector<uint64> order;\n\t\t\t\torder.reserve(inner->count());\n\t\t\t\tfor (auto i = 0; i < inner->count(); i++) {\n\t\t\t\t\tfor (const auto &[account, button] : _watched) {\n\t\t\t\t\t\tif (button.get() == inner->widgetAt(i)) {\n\t\t\t\t\t\t\torder.push_back(account->session().uniqueId());\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tCore::App().settings().setAccountsOrder(order);\n\t\t\t\tCore::App().saveSettings();\n\t\t\t}\n\t\t}\n\t}, inner->lifetime());\n\n\tconst auto premiumLimit = _controller->session().domain().maxAccounts();\n\tconst auto list = _controller->session().domain().orderedAccounts();\n\tfor (const auto &account : list) {\n\t\tauto i = _watched.find(account);\n\t\tAssert(i != _watched.end());\n\n\t\tauto &button = i->second;\n\t\tif (!account->sessionExists() || list.size() == 1) {\n\t\t\tbutton = nullptr;\n\t\t} else if (!button) {\n\t\t\tconst auto nextIsLocked = (inner->count() >= premiumLimit);\n\t\t\tauto callback = [=](Qt::KeyboardModifiers modifiers) {\n\t\t\t\tif (_reordering) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (account == &_controller->session().account()) {\n\t\t\t\t\t_closeRequests.fire({});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto newWindow = (modifiers & Qt::ControlModifier);\n\t\t\t\tauto activate = [=, guard = _accountSwitchGuard.make_guard()]{\n\t\t\t\t\tif (guard) {\n\t\t\t\t\t\t_reorder->finishReordering();\n\t\t\t\t\t\tif (newWindow) {\n\t\t\t\t\t\t\t_closeRequests.fire({});\n\t\t\t\t\t\t\tCore::App().ensureSeparateWindowFor(account);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tCore::App().domain().maybeActivate(account);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tif (const auto window = Core::App().separateWindowFor(\n\t\t\t\t\t\taccount)) {\n\t\t\t\t\t_closeRequests.fire({});\n\t\t\t\t\twindow->activate();\n\t\t\t\t} else {\n\t\t\t\t\tbase::call_delayed(\n\t\t\t\t\t\tst::defaultRippleAnimation.hideDuration,\n\t\t\t\t\t\taccount,\n\t\t\t\t\t\tstd::move(activate));\n\t\t\t\t}\n\t\t\t};\n\t\t\tbutton.reset(inner->add(MakeAccountButton(\n\t\t\t\tinner,\n\t\t\t\t_controller,\n\t\t\t\taccount,\n\t\t\t\tstd::move(callback),\n\t\t\t\tnextIsLocked)));\n\t\t}\n\t}\n\tinner->resizeToWidth(_outer->width());\n\n\tconst auto count = int(list.size());\n\n\t_reorder->addPinnedInterval(\n\t\tpremiumLimit,\n\t\tstd::max(1, count - premiumLimit));\n\n\t_addAccount->toggle(\n\t\t(count < ::Main::Domain::kPremiumMaxAccounts),\n\t\tanim::type::instant);\n\n\t_reorder->start();\n}\n\nvoid BuildInformationSection(SectionBuilder &builder) {\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"edit/bio\"_q,\n\t\t\t.title = tr::lng_bio_placeholder(tr::now),\n\t\t\t.keywords = { u\"bio\"_q, u\"about\"_q, u\"description\"_q },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"edit/name\"_q,\n\t\t\t.title = tr::lng_settings_name_label(tr::now),\n\t\t\t.keywords = { u\"name\"_q, u\"first\"_q, u\"last\"_q },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"edit/phone\"_q,\n\t\t\t.title = tr::lng_settings_phone_label(tr::now),\n\t\t\t.keywords = { u\"phone\"_q, u\"number\"_q, u\"mobile\"_q },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"edit/username\"_q,\n\t\t\t.title = tr::lng_settings_username_label(tr::now),\n\t\t\t.keywords = { u\"username\"_q, u\"link\"_q, u\"t.me\"_q },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"edit/your-color\"_q,\n\t\t\t.title = tr::lng_settings_theme_name_color(tr::now),\n\t\t\t.keywords = { u\"color\"_q, u\"theme\"_q, u\"name\"_q },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"edit/channel\"_q,\n\t\t\t.title = tr::lng_settings_channel_label(tr::now),\n\t\t\t.keywords = { u\"channel\"_q, u\"personal\"_q },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"edit/birthday\"_q,\n\t\t\t.title = tr::lng_settings_birthday_label(tr::now),\n\t\t\t.keywords = { u\"birthday\"_q, u\"date\"_q, u\"birth\"_q },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"edit/add-account\"_q,\n\t\t\t.title = tr::lng_menu_add_account(tr::now),\n\t\t\t.keywords = { u\"account\"_q, u\"add\"_q, u\"switch\"_q, u\"multiple\"_q },\n\t\t};\n\t});\n}\n\nclass Information : public Section<Information> {\npublic:\n\tInformation(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\tvoid showFinished() override;\n\nprivate:\n\tvoid setupContent();\n\n\tQPointer<Ui::RpWidget> _photo;\n\tQPointer<Ui::RpWidget> _uploadPhoto;\n\tQPointer<Ui::RpWidget> _bio;\n\tQPointer<Ui::RpWidget> _colorButton;\n\tQPointer<Ui::RpWidget> _channelButton;\n\tQPointer<Ui::RpWidget> _addAccount;\n\tQPointer<Ui::RpWidget> _name;\n\tQPointer<Ui::RpWidget> _phone;\n\tQPointer<Ui::RpWidget> _username;\n\tQPointer<Ui::RpWidget> _birthday;\n\n};\n\nInformation::Information(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent();\n}\n\nrpl::producer<QString> Information::title() {\n\treturn tr::lng_settings_section_info();\n}\n\nvoid Information::showFinished() {\n\tSection<Information>::showFinished();\n}\n\nvoid Information::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tconst SectionBuildMethod buildMethod = [\n\t\tphoto = &_photo,\n\t\tuploadPhoto = &_uploadPhoto,\n\t\tbio = &_bio,\n\t\tcolorButton = &_colorButton,\n\t\tchannelButton = &_channelButton,\n\t\taddAccount = &_addAccount,\n\t\tname = &_name,\n\t\tphone = &_phone,\n\t\tusername = &_username,\n\t\tbirthday = &_birthday\n\t](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tauto &lifetime = container->lifetime();\n\t\tconst auto highlights = lifetime.make_state<HighlightRegistry>();\n\t\tconst auto isPaused = Window::PausedIn(\n\t\t\tcontroller,\n\t\t\tWindow::GifPauseReason::Layer);\n\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = isPaused,\n\t\t\t.highlights = highlights,\n\t\t});\n\n\t\tconst auto self = controller->session().user();\n\t\tauto targets = InformationHighlightTargets();\n\n\t\tSetupPhoto(container, controller, self, &targets);\n\t\tSetupBio(container, self, &targets);\n\t\tSetupRows(container, controller, self, &targets);\n\t\tSetupPersonalChannel(container, controller, self, &targets);\n\t\tSetupBirthday(container, controller, self, &targets);\n\t\tSetupAccountsWrap(container, controller, &targets);\n\n\t\t*photo = targets.photo;\n\t\t*uploadPhoto = targets.uploadPhoto;\n\t\t*bio = targets.bio;\n\t\t*colorButton = targets.colorButton;\n\t\t*channelButton = targets.channelButton;\n\t\t*addAccount = targets.addAccount;\n\t\t*name = targets.name;\n\t\t*phone = targets.phone;\n\t\t*username = targets.username;\n\t\t*birthday = targets.birthday;\n\n\t\tif (highlights) {\n\t\t\tif (*photo) {\n\t\t\t\thighlights->push_back({\n\t\t\t\t\tu\"profile-photo\"_q,\n\t\t\t\t\t{ photo->data(), { .shape = HighlightShape::Ellipse } },\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (*uploadPhoto) {\n\t\t\t\thighlights->push_back({\n\t\t\t\t\tu\"profile-photo/use-emoji\"_q,\n\t\t\t\t\t{ uploadPhoto->data(), { .shape = HighlightShape::Ellipse } },\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (*bio) {\n\t\t\t\thighlights->push_back({\n\t\t\t\t\tu\"edit/bio\"_q,\n\t\t\t\t\t{ bio->data(), { .margin = st::settingsBioHighlightMargin } },\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (*colorButton) {\n\t\t\t\thighlights->push_back({\n\t\t\t\t\tu\"edit/your-color\"_q,\n\t\t\t\t\t{ colorButton->data(), { .rippleShape = true } },\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (*channelButton) {\n\t\t\t\thighlights->push_back({\n\t\t\t\t\tu\"edit/channel\"_q,\n\t\t\t\t\t{ channelButton->data(), { .rippleShape = true } },\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (*addAccount) {\n\t\t\t\thighlights->push_back({\n\t\t\t\t\tu\"edit/add-account\"_q,\n\t\t\t\t\t{ addAccount->data(), { .rippleShape = true } },\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (*name) {\n\t\t\t\thighlights->push_back({\n\t\t\t\t\tu\"edit/name\"_q,\n\t\t\t\t\t{ name->data(), { .rippleShape = true } },\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (*phone) {\n\t\t\t\thighlights->push_back({\n\t\t\t\t\tu\"edit/phone\"_q,\n\t\t\t\t\t{ phone->data(), { .rippleShape = true } },\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (*username) {\n\t\t\t\thighlights->push_back({\n\t\t\t\t\tu\"edit/username\"_q,\n\t\t\t\t\t{ username->data(), { .rippleShape = true } },\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (*birthday) {\n\t\t\t\thighlights->push_back({\n\t\t\t\t\tu\"edit/birthday\"_q,\n\t\t\t\t\t{ birthday->data(), { .rippleShape = true } },\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tstd::move(showFinished) | rpl::on_next([=] {\n\t\t\tfor (const auto &[id, entry] : *highlights) {\n\t\t\t\tif (entry.widget) {\n\t\t\t\t\tcontroller->checkHighlightControl(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tentry.widget,\n\t\t\t\t\t\tbase::duplicate(entry.args));\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime);\n\t};\n\n\tbuild(content, buildMethod);\n\n\tUi::ResizeFitChild(this, content);\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = Information::Id(),\n\t.parentId = MainId(),\n\t.title = &tr::lng_settings_section_info,\n\t.icon = &st::menuIconProfile,\n}, [](SectionBuilder &builder) {\n\tBuildInformationSection(builder);\n});\n\n} // namespace\n\nType InformationId() {\n\treturn Information::Id();\n}\n\nAccountsEvents SetupAccounts(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto list = container->lifetime().make_state<AccountsList>(\n\t\tcontainer,\n\t\tcontroller);\n\treturn {\n\t\t.closeRequests = list->closeRequests(),\n\t\t.addAccountButton = list->addAccountButton(),\n\t};\n}\n\nvoid UpdatePhotoLocally(not_null<UserData*> user, const QImage &image) {\n\tauto bytes = QByteArray();\n\tauto buffer = QBuffer(&bytes);\n\timage.save(&buffer, \"JPG\", 87);\n\tuser->setUserpic(\n\t\tbase::RandomValue<PhotoId>(),\n\t\tImageLocation(\n\t\t\t{ .data = InMemoryLocation{ .bytes = bytes } },\n\t\t\timage.width(),\n\t\t\timage.height()),\n\t\tfalse);\n}\n\nnamespace Badge {\n\nUi::UnreadBadgeStyle Style() {\n\tauto result = Ui::UnreadBadgeStyle();\n\tresult.font = st::mainMenuBadgeFont;\n\tresult.size = st::mainMenuBadgeSize;\n\tresult.sizeId = Ui::UnreadBadgeSize::MainMenu;\n\treturn result;\n}\n\nnot_null<Ui::RpWidget*> AddRight(\n\t\tnot_null<Ui::SettingsButton*> button,\n\t\tint rightPadding) {\n\tconst auto widget = Ui::CreateChild<Ui::RpWidget>(button.get());\n\n\trpl::combine(\n\t\tbutton->sizeValue(),\n\t\twidget->sizeValue(),\n\t\twidget->shownValue()\n\t) | rpl::on_next([=](QSize outer, QSize inner, bool shown) {\n\t\tauto padding = button->st().padding;\n\t\tif (shown) {\n\t\t\twidget->moveToRight(\n\t\t\t\tpadding.right() + rightPadding,\n\t\t\t\t(outer.height() - inner.height()) / 2,\n\t\t\t\touter.width());\n\t\t\tpadding.setRight(padding.right() + inner.width() + rightPadding);\n\t\t}\n\t\tbutton->setPaddingOverride(padding);\n\t\tbutton->update();\n\t}, widget->lifetime());\n\n\treturn widget;\n}\n\nnot_null<Ui::RpWidget*> CreateUnread(\n\t\tnot_null<Ui::RpWidget*> container,\n\t\trpl::producer<UnreadBadge> value) {\n\tstruct State {\n\t\tState(QWidget *parent) : widget(parent) {\n\t\t\twidget.setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t}\n\n\t\tUi::RpWidget widget;\n\t\tUi::UnreadBadgeStyle st = Style();\n\t\tint count = 0;\n\t\tQString string;\n\t};\n\tconst auto state = container->lifetime().make_state<State>(container);\n\n\tstd::move(\n\t\tvalue\n\t) | rpl::on_next([=](UnreadBadge badge) {\n\t\tstate->st.muted = badge.muted;\n\t\tstate->count = badge.count;\n\t\tif (!state->count) {\n\t\t\tstate->widget.hide();\n\t\t\treturn;\n\t\t}\n\t\tstate->string = Lang::FormatCountToShort(state->count).string;\n\t\tstate->widget.resize(Ui::CountUnreadBadgeSize(state->string, state->st));\n\t\tif (state->widget.isHidden()) {\n\t\t\tstate->widget.show();\n\t\t}\n\t}, state->widget.lifetime());\n\n\tstate->widget.paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = Painter(&state->widget);\n\t\tUi::PaintUnreadBadge(\n\t\t\tp,\n\t\t\tstate->string,\n\t\t\tstate->widget.width(),\n\t\t\t0,\n\t\t\tstate->st);\n\t}, state->widget.lifetime());\n\n\treturn &state->widget;\n}\n\nvoid AddUnread(\n\t\tnot_null<Ui::SettingsButton*> button,\n\t\trpl::producer<UnreadBadge> value) {\n\tconst auto container = AddRight(button);\n\tconst auto badge = CreateUnread(container, std::move(value));\n\tbadge->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tcontainer->resize(s);\n\t}, container->lifetime());\n}\n\n} // namespace Badge\n\nnamespace Builder {\n\nSectionBuildMethod InformationSection = kMeta.build;\n\n} // namespace Builder\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_information.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nclass UserData;\n\nnamespace Ui {\nclass RpWidget;\nclass SettingsButton;\nclass VerticalLayout;\nstruct UnreadBadgeStyle;\n} // namespace Ui\n\nnamespace Main {\nclass Account;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Settings {\n\n[[nodiscard]] Type InformationId();\n\nstruct AccountsEvents {\n\trpl::producer<> closeRequests;\n\tQPointer<Ui::RpWidget> addAccountButton;\n};\nAccountsEvents SetupAccounts(\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<Window::SessionController*> controller);\n\nvoid UpdatePhotoLocally(not_null<UserData*> user, const QImage &image);\n\nnamespace Badge {\n\n[[nodiscard]] Ui::UnreadBadgeStyle Style();\n\nstruct UnreadBadge {\n\tint count = 0;\n\tbool muted = false;\n};\n[[nodiscard]] not_null<Ui::RpWidget*> AddRight(\n\tnot_null<Ui::SettingsButton*> button,\n\tint rightPadding = 0);\n[[nodiscard]] not_null<Ui::RpWidget*> CreateUnread(\n\tnot_null<Ui::RpWidget*> container,\n\trpl::producer<UnreadBadge> value);\nvoid AddUnread(\n\tnot_null<Ui::SettingsButton*> button,\n\trpl::producer<UnreadBadge> value);\n\n} // namespace Badge\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_link_device.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_link_device.h\"\n\n#include \"settings/settings_common_session.h\"\n\n#include \"apiwrap.h\"\n#include \"base/timer.h\"\n#include \"calls/calls_instance.h\"\n#include \"calls/calls_video_bubble.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"settings/settings_builder.h\"\n#include \"tgcalls/VideoCaptureInterface.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\n#include <zbar.h>\n\n#include <QtGui/QClipboard>\n#include <QtGui/QGuiApplication>\n\nnamespace Settings {\nnamespace {\n\nusing namespace Builder;\n\nusing VideoState = Webrtc::VideoState;\nusing VideoTrack = Webrtc::VideoTrack;\n\nconstexpr auto kQrScanInterval = crl::time(500);\n\nstruct QrResult {\n\tQString text;\n\tint qrCount = 0;\n\tint decodeErrors = 0;\n\tQString lastError;\n};\n\n[[nodiscard]] QrResult TryDecodeQr(const QImage &frame, bool mirror) {\n\tif (frame.isNull()) {\n\t\treturn {};\n\t}\n\tconst auto flipped = mirror\n\t\t? frame.mirrored(true, false)\n\t\t: frame;\n\tconst auto gray = flipped.convertToFormat(QImage::Format_Grayscale8);\n\tconst auto w = gray.width();\n\tconst auto h = gray.height();\n\n\tauto *scanner = zbar::zbar_image_scanner_create();\n\tif (!scanner) {\n\t\treturn {};\n\t}\n\tconst auto scannerGuard = gsl::finally([&] {\n\t\tzbar::zbar_image_scanner_destroy(scanner);\n\t});\n\tzbar::zbar_image_scanner_set_config(\n\t\tscanner,\n\t\tzbar::ZBAR_QRCODE,\n\t\tzbar::ZBAR_CFG_ENABLE,\n\t\t1);\n\n\tauto *zimg = zbar::zbar_image_create();\n\tif (!zimg) {\n\t\treturn {};\n\t}\n\tconst auto imgGuard = gsl::finally([&] {\n\t\tzbar::zbar_image_destroy(zimg);\n\t});\n\tzbar::zbar_image_set_format(\n\t\tzimg,\n\t\tzbar_fourcc('Y', '8', '0', '0'));\n\tzbar::zbar_image_set_size(zimg, w, h);\n\n\tauto packed = QByteArray(w * h, Qt::Uninitialized);\n\tconst auto stride = gray.bytesPerLine();\n\tif (stride == w) {\n\t\tmemcpy(packed.data(), gray.constBits(), w * h);\n\t} else {\n\t\tfor (auto y = 0; y < h; ++y) {\n\t\t\tmemcpy(packed.data() + y * w, gray.constScanLine(y), w);\n\t\t}\n\t}\n\tzbar::zbar_image_set_data(\n\t\tzimg,\n\t\tpacked.constData(),\n\t\tpacked.size(),\n\t\tnullptr);\n\n\tzbar::zbar_scan_image(scanner, zimg);\n\n\tauto result = QrResult();\n\tconst auto *sym = zbar::zbar_image_first_symbol(zimg);\n\twhile (sym) {\n\t\t++result.qrCount;\n\t\tif (zbar::zbar_symbol_get_type(sym) == zbar::ZBAR_QRCODE) {\n\t\t\tauto text = QString::fromUtf8(\n\t\t\t\tzbar::zbar_symbol_get_data(sym),\n\t\t\t\tzbar::zbar_symbol_get_data_length(sym));\n\t\t\tif (text.startsWith(u\"tg://login?token=\"_q)) {\n\t\t\t\tresult.text = text;\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tif (result.text.isEmpty()) {\n\t\t\t\tresult.text = text;\n\t\t\t}\n\t\t}\n\t\tsym = zbar::zbar_symbol_next(sym);\n\t}\n\treturn result;\n}\n\n[[nodiscard]] QByteArray ParseLoginToken(const QString &url) {\n\tconst auto prefix = u\"tg://login?token=\"_q;\n\tif (!url.startsWith(prefix)) {\n\t\treturn {};\n\t}\n\treturn QByteArray::fromBase64(\n\t\turl.mid(prefix.size()).toLatin1(),\n\t\tQByteArray::Base64UrlEncoding);\n}\n\nvoid AcceptLoginToken(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QByteArray &token,\n\t\tFn<void()> success,\n\t\tFn<void(QString)> fail) {\n\tsession->api().request(MTPauth_AcceptLoginToken(\n\t\tMTP_bytes(token)\n\t)).done([=](const MTPAuthorization &result) {\n\t\tsuccess();\n\t}).fail([=](const MTP::Error &error) {\n\t\tfail(error.type());\n\t}).send();\n}\n\nvoid HandleQrToken(\n\t\tconst QByteArray &token,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Ui::FlatLabel*> debugLabel,\n\t\tFn<void()> stopScanning) {\n\tconst auto session = &controller->session();\n\tconst auto weak = QPointer<Ui::VerticalLayout>(container.get());\n\tcontroller->show(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_settings_link_device_confirm(tr::now),\n\t\t.confirmed = [=](Fn<void()> close) {\n\t\t\tAcceptLoginToken(session, token, [=] {\n\t\t\t\tclose();\n\t\t\t\tif (weak) {\n\t\t\t\t\tstopScanning();\n\t\t\t\t\tUi::Toast::Show(\n\t\t\t\t\t\tweak.data(),\n\t\t\t\t\t\ttr::lng_settings_link_device_success(tr::now));\n\t\t\t\t}\n\t\t\t}, [=](const QString &error) {\n\t\t\t\tclose();\n\t\t\t\tif (weak) {\n\t\t\t\t\tstopScanning();\n\t\t\t\t\tUi::Toast::Show(\n\t\t\t\t\t\tweak.data(),\n\t\t\t\t\t\ttr::lng_settings_link_device_error(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_error,\n\t\t\t\t\t\t\terror));\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t\t.cancelled = [=](Fn<void()> close) {\n\t\t\tclose();\n\t\t\tstopScanning();\n\t\t},\n\t}));\n}\n\nvoid SetupLinkDeviceContent(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller) {\n\tcontainer->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_settings_link_device_privacy(),\n\t\t\tst::boxDividerLabel),\n\t\tst::boxRowPadding);\n\n\tUi::AddSkip(container);\n\tUi::AddDivider(container);\n\tUi::AddSkip(container);\n\n\tauto &lifetime = container->lifetime();\n\n\tconst auto scanning = lifetime.make_state<rpl::variable<bool>>(false);\n\tconst auto capturerOwner = lifetime.make_state<\n\t\tstd::shared_ptr<tgcalls::VideoCaptureInterface>>();\n\tconst auto track = lifetime.make_state<VideoTrack>(\n\t\tVideoState::Inactive);\n\tconst auto lastDecoded = lifetime.make_state<QString>();\n\tconst auto scanTimer = lifetime.make_state<base::Timer>();\n\n\tconst auto bubbleWrap = container->add(\n\t\tobject_ptr<Ui::RpWidget>(container));\n\tconst auto bubble = lifetime.make_state<::Calls::VideoBubble>(\n\t\tbubbleWrap,\n\t\ttrack);\n\n\tconst auto padding = st::settingsButtonNoIcon.padding.left();\n\tconst auto top = st::boxRoundShadow.extend.top();\n\tconst auto bottom = st::boxRoundShadow.extend.bottom();\n\n\tauto frameSize = track->renderNextFrame(\n\t) | rpl::map([=] {\n\t\treturn track->frameSize();\n\t}) | rpl::filter([=](QSize size) {\n\t\treturn !size.isEmpty();\n\t});\n\tauto bubbleWidth = bubbleWrap->widthValue(\n\t) | rpl::filter([=](int width) {\n\t\treturn width > 2 * padding + 1;\n\t});\n\trpl::combine(\n\t\tstd::move(bubbleWidth),\n\t\tstd::move(frameSize)\n\t) | rpl::on_next([=](int width, QSize frame) {\n\t\tconst auto useWidth = (width - 2 * padding);\n\t\tconst auto useHeight = std::min(\n\t\t\t((useWidth * frame.height()) / frame.width()),\n\t\t\t(useWidth * 480) / 640);\n\t\tbubbleWrap->resize(width, top + useHeight + bottom);\n\t\tbubble->updateGeometry(\n\t\t\t::Calls::VideoBubble::DragMode::None,\n\t\t\tQRect(padding, top, useWidth, useHeight));\n\t\tbubbleWrap->update();\n\t}, bubbleWrap->lifetime());\n\n\tbubbleWrap->resize(bubbleWrap->width(), 0);\n\n\tUi::AddSkip(container);\n\n\tconst auto statusLabel = container->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_settings_link_device_scanning(),\n\t\t\tst::boxDividerLabel),\n\t\tst::boxRowPadding);\n\tstatusLabel->setVisible(false);\n\n\tUi::AddSkip(container);\n\n\tconst auto buttonPadding = st::boxRowPadding;\n\tconst auto buttonPaddingH = buttonPadding.left() + buttonPadding.right();\n\tconst auto setupFullWidth = [=](not_null<Ui::RoundButton*> btn) {\n\t\tbtn->setFullRadius(true);\n\t\trpl::combine(\n\t\t\tcontainer->widthValue(),\n\t\t\tbtn->sizeValue()\n\t\t) | rpl::on_next([=](int w, QSize) {\n\t\t\tconst auto target = w - buttonPaddingH;\n\t\t\tif (btn->width() != target) {\n\t\t\t\tbtn->resize(target, btn->height());\n\t\t\t}\n\t\t}, btn->lifetime());\n\t};\n\n\tconst auto debugWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::FlatLabel>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\trpl::single(QString()),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::boxRowPadding));\n\tdebugWrap->toggle(false, anim::type::instant);\n\tconst auto debugLabel = debugWrap->entity();\n\n\tconst auto debugButton = container->add(\n\t\tobject_ptr<Ui::LinkButton>(\n\t\t\tcontainer,\n\t\t\tQString(\"debug\")),\n\t\tst::boxRowPadding);\n\n\tdebugButton->setClickedCallback([=] {\n\t\tdebugWrap->toggle(\n\t\t\t!debugWrap->toggled(),\n\t\t\tanim::type::normal);\n\t});\n\n\tUi::AddSkip(container);\n\n\tconst auto toggleButton = container->add(\n\t\tobject_ptr<Ui::RoundButton>(\n\t\t\tcontainer,\n\t\t\ttr::lng_settings_link_device_start(),\n\t\t\tst::defaultActiveButton),\n\t\tbuttonPadding);\n\tsetupFullWidth(toggleButton);\n\n\tUi::AddSkip(container);\n\n\tconst auto clipboardButton = container->add(\n\t\tobject_ptr<Ui::RoundButton>(\n\t\t\tcontainer,\n\t\t\ttr::lng_settings_link_device_clipboard(),\n\t\t\tst::defaultActiveButton),\n\t\tbuttonPadding);\n\tsetupFullWidth(clipboardButton);\n\n\tUi::AddSkip(container);\n\tUi::AddSkip(container);\n\n\tconst auto stopScanning = [=] {\n\t\tscanTimer->cancel();\n\t\ttrack->setState(VideoState::Inactive);\n\t\t*capturerOwner = nullptr;\n\t\t*scanning = false;\n\t\tbubbleWrap->resize(bubbleWrap->width(), 0);\n\t};\n\n\tconst auto startScanning = [=] {\n\t\tif (*capturerOwner) {\n\t\t\treturn;\n\t\t}\n\t\t*capturerOwner = Core::App().calls().getVideoCapture(\n\t\t\tCore::App().settings().cameraDeviceId(),\n\t\t\tfalse);\n\t\t(*capturerOwner)->setPreferredAspectRatio(0.);\n\t\ttrack->setState(VideoState::Active);\n\t\t(*capturerOwner)->setState(tgcalls::VideoState::Active);\n\t\t(*capturerOwner)->setOutput(track->sink());\n\t\t*scanning = true;\n\t\t*lastDecoded = QString();\n\n\t\tscanTimer->setCallback([=] {\n\t\t\tif (!scanning->current()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto frame = track->frame({});\n\t\t\tif (frame.isNull()) {\n\t\t\t\tdebugLabel->setText(\n\t\t\t\t\tQString(\"Debug: frame is null\"));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tauto result = TryDecodeQr(frame, true);\n\t\t\tif (result.text.isEmpty() && result.qrCount == 0) {\n\t\t\t\tresult = TryDecodeQr(frame, false);\n\t\t\t}\n\t\t\ttrack->markFrameShown();\n\n\t\t\tdebugLabel->setText(QString(\n\t\t\t\t\"Debug: %1x%2 fmt=%3 qrs=%4 err=%5 decoded=\\\"%6\\\" lastErr=\\\"%7\\\"\"\n\t\t\t).arg(frame.width()\n\t\t\t).arg(frame.height()\n\t\t\t).arg(frame.format()\n\t\t\t).arg(result.qrCount\n\t\t\t).arg(result.decodeErrors\n\t\t\t).arg(result.text.left(60)\n\t\t\t).arg(result.lastError));\n\n\t\t\tconst auto decoded = result.text;\n\t\t\tif (decoded.isEmpty() || decoded == *lastDecoded) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!decoded.startsWith(u\"tg://login?token=\"_q)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t*lastDecoded = decoded;\n\n\t\t\tconst auto token = ParseLoginToken(decoded);\n\t\t\tif (token.isEmpty()) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tscanTimer->cancel();\n\t\t\tHandleQrToken(token, controller, container, debugLabel, stopScanning);\n\t\t});\n\t\tscanTimer->callEach(kQrScanInterval);\n\t};\n\n\tscanning->value(\n\t) | rpl::on_next([=](bool active) {\n\t\tstatusLabel->setVisible(active);\n\t\tif (active) {\n\t\t\ttoggleButton->setText(tr::lng_settings_link_device_stop());\n\t\t} else {\n\t\t\ttoggleButton->setText(tr::lng_settings_link_device_start());\n\t\t}\n\t\tcontainer->resizeToWidth(container->width());\n\t}, container->lifetime());\n\n\ttoggleButton->setClickedCallback([=] {\n\t\tif (scanning->current()) {\n\t\t\tstopScanning();\n\t\t} else {\n\t\t\tstartScanning();\n\t\t}\n\t});\n\n\tclipboardButton->setClickedCallback([=] {\n\t\tconst auto clipboard = QGuiApplication::clipboard();\n\t\tconst auto image = clipboard->image();\n\t\tif (image.isNull()) {\n\t\t\tdebugLabel->setText(\n\t\t\t\tQString(\"Debug: clipboard has no image\"));\n\t\t\treturn;\n\t\t}\n\n\t\tauto result = TryDecodeQr(image, false);\n\t\tdebugLabel->setText(QString(\n\t\t\t\"Debug: clip %1x%2 fmt=%3 qrs=%4 err=%5 decoded=\\\"%6\\\" lastErr=\\\"%7\\\"\"\n\t\t).arg(image.width()\n\t\t).arg(image.height()\n\t\t).arg(image.format()\n\t\t).arg(result.qrCount\n\t\t).arg(result.decodeErrors\n\t\t).arg(result.text.left(60)\n\t\t).arg(result.lastError));\n\n\t\tif (!result.text.startsWith(u\"tg://login?token=\"_q)) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto token = ParseLoginToken(result.text);\n\t\tif (token.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tHandleQrToken(token, controller, container, debugLabel, stopScanning);\n\t});\n}\n\nclass LinkDevice : public Section<LinkDevice> {\npublic:\n\tLinkDevice(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~LinkDevice();\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\nprivate:\n\tvoid setupContent();\n\n};\n\nLinkDevice::LinkDevice(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent();\n}\n\nLinkDevice::~LinkDevice() = default;\n\nrpl::producer<QString> LinkDevice::title() {\n\treturn tr::lng_settings_link_device();\n}\n\nvoid LinkDevice::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tconst SectionBuildMethod buildMethod = [](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t});\n\n\t\tbuilder.addSkip();\n\t\tbuilder.add([](const BuildContext &ctx) {\n\t\t\tif (const auto *widget = std::get_if<WidgetContext>(&ctx)) {\n\t\t\t\tSetupLinkDeviceContent(\n\t\t\t\t\twidget->container,\n\t\t\t\t\twidget->controller);\n\t\t\t}\n\t\t});\n\t};\n\n\tbuild(content, buildMethod);\n\tUi::ResizeFitChild(this, content);\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = LinkDevice::Id(),\n\t.parentId = MainId(),\n\t.title = &tr::lng_settings_link_device,\n\t.icon = &st::menuIconQrCode,\n}, [](SectionBuilder &builder) {\n\tbuilder.addSkip();\n\tbuilder.addDividerText(tr::lng_settings_link_device_privacy());\n});\n\n} // namespace\n\nType LinkDeviceId() {\n\treturn LinkDevice::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_link_device.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\n[[nodiscard]] Type LinkDeviceId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_local_passcode.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_local_passcode.h\"\n\n#include \"base/platform/base_platform_last_input.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/system_unlock.h\"\n#include \"boxes/auto_lock_box.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"settings/cloud_password/settings_cloud_password_common.h\"\n#include \"settings/cloud_password/settings_cloud_password_step.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common.h\"\n#include \"storage/storage_domain.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/password_input.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nusing namespace Builder;\n\nvoid SetPasscode(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &pass) {\n\tcSetPasscodeBadTries(0);\n\tcontroller->session().domain().local().setPasscode(pass.toUtf8());\n\tCore::App().localPasscodeChanged();\n}\n\n} // namespace\n\nnamespace details {\n\nclass LocalPasscodeEnter : public AbstractSection {\npublic:\n\tenum class EnterType {\n\t\tCreate,\n\t\tCheck,\n\t\tChange,\n\t};\n\n\tLocalPasscodeEnter(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~LocalPasscodeEnter();\n\n\tvoid showFinished() override;\n\tvoid setInnerFocus() override;\n\t[[nodiscard]] rpl::producer<Type> sectionShowOther() override;\n\t[[nodiscard]] rpl::producer<> sectionShowBack() override;\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\nprotected:\n\tvoid setupContent();\n\n\t[[nodiscard]] virtual EnterType enterType() const = 0;\n\nprivate:\n\trpl::event_stream<> _showFinished;\n\trpl::event_stream<> _setInnerFocus;\n\trpl::event_stream<Type> _showOther;\n\trpl::event_stream<> _showBack;\n\tbool _systemUnlockWithBiometric = false;\n\n};\n\nLocalPasscodeEnter::LocalPasscodeEnter(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: AbstractSection(parent, controller) {\n}\n\nrpl::producer<QString> LocalPasscodeEnter::title() {\n\treturn tr::lng_settings_passcode_title();\n}\n\nvoid LocalPasscodeEnter::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tbase::SystemUnlockStatus(\n\t\ttrue\n\t) | rpl::on_next([=](base::SystemUnlockAvailability status) {\n\t\t_systemUnlockWithBiometric = status.available\n\t\t\t&& status.withBiometrics;\n\t}, lifetime());\n\n\tconst auto isCreate = (enterType() == EnterType::Create);\n\tconst auto isCheck = (enterType() == EnterType::Check);\n\t[[maybe_unused]] const auto isChange = (enterType() == EnterType::Change);\n\n\tauto icon = CreateLottieIcon(\n\t\tcontent,\n\t\t{\n\t\t\t.name = u\"local_passcode_enter\"_q,\n\t\t\t.sizeOverride = st::normalBoxLottieSize,\n\t\t},\n\t\tst::settingLocalPasscodeIconPadding);\n\tcontent->add(std::move(icon.widget));\n\t_showFinished.events(\n\t) | rpl::on_next([animate = std::move(icon.animate)] {\n\t\tanimate(anim::repeat::once);\n\t}, content->lifetime());\n\n\tif (isChange) {\n\t\tCloudPassword::SetupAutoCloseTimer(\n\t\t\tcontent->lifetime(),\n\t\t\t[=] { _showBack.fire({}); },\n\t\t\t[] { return Core::App().lastNonIdleTime(); });\n\t}\n\n\tUi::AddSkip(content);\n\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\tisCreate\n\t\t\t\t? tr::lng_passcode_create_title()\n\t\t\t\t: isCheck\n\t\t\t\t? tr::lng_passcode_check_title()\n\t\t\t\t: tr::lng_passcode_change_title(),\n\t\t\tst::changePhoneTitle),\n\t\tst::changePhoneTitlePadding,\n\t\tstyle::al_top);\n\n\tconst auto addDescription = [&](rpl::producer<QString> &&text) {\n\t\tconst auto &st = st::settingLocalPasscodeDescription;\n\t\tcontent->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(content, std::move(text), st),\n\t\t\tst::changePhoneDescriptionPadding,\n\t\t\tstyle::al_top\n\t\t)->setTryMakeSimilarLines(true);\n\t};\n\n\taddDescription(tr::lng_passcode_about1());\n\tUi::AddSkip(content);\n\taddDescription(tr::lng_passcode_about2());\n\n\tUi::AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip);\n\n\tconst auto addField = [&](rpl::producer<QString> &&text) {\n\t\tconst auto &st = st::settingLocalPasscodeInputField;\n\t\tauto container = object_ptr<Ui::RpWidget>(content);\n\t\tcontainer->resize(container->width(), st.heightMin);\n\t\tconst auto field = Ui::CreateChild<Ui::PasswordInput>(\n\t\t\tcontainer.data(),\n\t\t\tst,\n\t\t\tstd::move(text));\n\n\t\tcontainer->geometryValue(\n\t\t) | rpl::on_next([=](const QRect &r) {\n\t\t\tfield->moveToLeft((r.width() - field->width()) / 2, 0);\n\t\t}, container->lifetime());\n\n\t\tcontent->add(std::move(container));\n\t\treturn field;\n\t};\n\n\tconst auto addError = [&](not_null<Ui::PasswordInput*> input) {\n\t\tconst auto error = content->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_language_name(tr::now),\n\t\t\t\tst::settingLocalPasscodeError),\n\t\t\tst::changePhoneDescriptionPadding,\n\t\t\tstyle::al_top);\n\t\terror->hide();\n\t\tQObject::connect(input.get(), &Ui::MaskedInputField::changed, [=] {\n\t\t\terror->hide();\n\t\t});\n\t\treturn error;\n\t};\n\n\tconst auto newPasscode = addField(isCreate\n\t\t? tr::lng_passcode_enter_first()\n\t\t: tr::lng_passcode_enter());\n\n\tconst auto reenterPasscode = isCheck\n\t\t? (Ui::PasswordInput*)(nullptr)\n\t\t: addField(tr::lng_passcode_confirm_new());\n\tconst auto error = addError(isCheck ? newPasscode : reenterPasscode);\n\n\tconst auto button = content->add(\n\t\tobject_ptr<Ui::RoundButton>(\n\t\t\tcontent,\n\t\t\t(isCreate\n\t\t\t\t? tr::lng_passcode_create_button()\n\t\t\t\t: isCheck\n\t\t\t\t? tr::lng_passcode_check_button()\n\t\t\t\t: tr::lng_passcode_change_button()),\n\t\t\tst::changePhoneButton),\n\t\tst::settingLocalPasscodeButtonPadding,\n\t\tstyle::al_top);\n\tbutton->setClickedCallback([=] {\n\t\tconst auto newText = newPasscode->text();\n\t\tconst auto reenterText = reenterPasscode\n\t\t\t? reenterPasscode->text()\n\t\t\t: QString();\n\t\tif (isCreate || isChange) {\n\t\t\tif (newText.isEmpty()) {\n\t\t\t\tnewPasscode->setFocus();\n\t\t\t\tnewPasscode->showError();\n\t\t\t} else if (reenterText.isEmpty()) {\n\t\t\t\treenterPasscode->setFocus();\n\t\t\t\treenterPasscode->showError();\n\t\t\t} else if (newText != reenterText) {\n\t\t\t\treenterPasscode->setFocus();\n\t\t\t\treenterPasscode->showError();\n\t\t\t\treenterPasscode->selectAll();\n\t\t\t\terror->show();\n\t\t\t\terror->setText(tr::lng_passcode_differ(tr::now));\n\t\t\t} else {\n\t\t\t\tif (isChange) {\n\t\t\t\t\tconst auto &domain = controller()->session().domain();\n\t\t\t\t\tif (domain.local().checkPasscode(newText.toUtf8())) {\n\t\t\t\t\t\tnewPasscode->setFocus();\n\t\t\t\t\t\tnewPasscode->showError();\n\t\t\t\t\t\tnewPasscode->selectAll();\n\t\t\t\t\t\terror->show();\n\t\t\t\t\t\terror->setText(tr::lng_passcode_is_same(tr::now));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tSetPasscode(controller(), newText);\n\t\t\t\tif (isCreate) {\n\t\t\t\t\tif (Platform::IsWindows() || _systemUnlockWithBiometric) {\n\t\t\t\t\t\tCore::App().settings().setSystemUnlockEnabled(true);\n\t\t\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t\t\t}\n\t\t\t\t\t_showOther.fire(LocalPasscodeManageId());\n\t\t\t\t} else if (isChange) {\n\t\t\t\t\t_showBack.fire({});\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (isCheck) {\n\t\t\tif (!passcodeCanTry()) {\n\t\t\t\tnewPasscode->setFocus();\n\t\t\t\tnewPasscode->showError();\n\t\t\t\terror->show();\n\t\t\t\terror->setText(tr::lng_flood_error(tr::now));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto &domain = controller()->session().domain();\n\t\t\tif (domain.local().checkPasscode(newText.toUtf8())) {\n\t\t\t\tcSetPasscodeBadTries(0);\n\t\t\t\t_showOther.fire(LocalPasscodeManageId());\n\t\t\t} else {\n\t\t\t\tcSetPasscodeBadTries(cPasscodeBadTries() + 1);\n\t\t\t\tcSetPasscodeLastTry(crl::now());\n\n\t\t\t\tnewPasscode->selectAll();\n\t\t\t\tnewPasscode->setFocus();\n\t\t\t\tnewPasscode->showError();\n\t\t\t\terror->show();\n\t\t\t\terror->setText(tr::lng_passcode_wrong(tr::now));\n\t\t\t}\n\t\t}\n\t});\n\n\tconst auto submit = [=] {\n\t\tif (!reenterPasscode || reenterPasscode->hasFocus()) {\n\t\t\tbutton->clicked({}, Qt::LeftButton);\n\t\t} else {\n\t\t\treenterPasscode->setFocus();\n\t\t}\n\t};\n\tconnect(newPasscode, &Ui::MaskedInputField::submitted, submit);\n\tif (reenterPasscode) {\n\t\tconnect(reenterPasscode, &Ui::MaskedInputField::submitted, submit);\n\t}\n\n\t_setInnerFocus.events(\n\t) | rpl::on_next([=] {\n\t\tif (newPasscode->text().isEmpty()) {\n\t\t\tnewPasscode->setFocus();\n\t\t} else if (reenterPasscode && reenterPasscode->text().isEmpty()) {\n\t\t\treenterPasscode->setFocus();\n\t\t} else {\n\t\t\tnewPasscode->setFocus();\n\t\t}\n\t}, content->lifetime());\n\n\tUi::ResizeFitChild(this, content);\n}\n\nvoid LocalPasscodeEnter::showFinished() {\n\t_showFinished.fire({});\n}\n\nvoid LocalPasscodeEnter::setInnerFocus() {\n\t_setInnerFocus.fire({});\n}\n\nrpl::producer<Type> LocalPasscodeEnter::sectionShowOther() {\n\treturn _showOther.events();\n}\n\nrpl::producer<> LocalPasscodeEnter::sectionShowBack() {\n\treturn _showBack.events();\n}\n\nLocalPasscodeEnter::~LocalPasscodeEnter() = default;\n\n} // namespace details\n\nclass LocalPasscodeCreate;\nclass LocalPasscodeCheck;\nclass LocalPasscodeChange;\n\ntemplate <typename SectionType>\nclass TypedLocalPasscodeEnter : public details::LocalPasscodeEnter {\npublic:\n\tTypedLocalPasscodeEnter(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller)\n\t: details::LocalPasscodeEnter(parent, controller) {\n\t\tsetupContent();\n\t}\n\n\t[[nodiscard]] static Type Id() {\n\t\treturn SectionFactory<SectionType>::Instance();\n\t}\n\t[[nodiscard]] Type id() const final override {\n\t\treturn Id();\n\t}\n\nprotected:\n\t[[nodiscard]] EnterType enterType() const final override {\n\t\tif constexpr (std::is_same_v<SectionType, LocalPasscodeCreate>) {\n\t\t\treturn EnterType::Create;\n\t\t}\n\t\tif constexpr (std::is_same_v<SectionType, LocalPasscodeCheck>) {\n\t\t\treturn EnterType::Check;\n\t\t}\n\t\tif constexpr (std::is_same_v<SectionType, LocalPasscodeChange>) {\n\t\t\treturn EnterType::Change;\n\t\t}\n\t\treturn EnterType::Create;\n\t}\n\n};\n\nclass LocalPasscodeCreate final\n\t: public TypedLocalPasscodeEnter<LocalPasscodeCreate> {\npublic:\n\tusing TypedLocalPasscodeEnter::TypedLocalPasscodeEnter;\n\n};\n\nclass LocalPasscodeCheck final\n\t: public TypedLocalPasscodeEnter<LocalPasscodeCheck> {\npublic:\n\tusing TypedLocalPasscodeEnter::TypedLocalPasscodeEnter;\n\n};\n\nclass LocalPasscodeChange final\n\t: public TypedLocalPasscodeEnter<LocalPasscodeChange> {\npublic:\n\tusing TypedLocalPasscodeEnter::TypedLocalPasscodeEnter;\n\n};\n\nnamespace {\n\nenum class UnlockType {\n\tNone,\n\tDefault,\n\tBiometrics,\n\tCompanion,\n};\n\nvoid BuildManageContent(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tif (!controller) {\n\t\treturn;\n\t}\n\n\tconst auto container = builder.container();\n\n\tstruct State {\n\t\trpl::event_stream<> autoLockBoxClosing;\n\t};\n\tconst auto state = container->lifetime().make_state<State>();\n\n\tbuilder.addSkip();\n\n\tbuilder.addButton({\n\t\t.id = u\"passcode/change\"_q,\n\t\t.title = tr::lng_passcode_change(),\n\t\t.icon = { &st::menuIconLock },\n\t\t.onClick = [=] {\n\t\t\tbuilder.showOther()(LocalPasscodeChange::Id());\n\t\t},\n\t\t.keywords = { u\"password\"_q, u\"code\"_q },\n\t});\n\n\tauto autolockLabel = state->autoLockBoxClosing.events_starting_with(\n\t\t{}\n\t) | rpl::map([] {\n\t\tconst auto autolock = Core::App().settings().autoLock();\n\t\tconst auto hours = autolock / 3600;\n\t\tconst auto minutes = (autolock - (hours * 3600)) / 60;\n\n\t\treturn (hours && minutes)\n\t\t\t? tr::lng_passcode_autolock_hours_minutes(\n\t\t\t\ttr::now,\n\t\t\t\tlt_hours_count,\n\t\t\t\tQString::number(hours),\n\t\t\t\tlt_minutes_count,\n\t\t\t\tQString::number(minutes))\n\t\t\t: minutes\n\t\t\t? tr::lng_minutes(tr::now, lt_count, minutes)\n\t\t\t: tr::lng_hours(tr::now, lt_count, hours);\n\t});\n\n\tconst auto autoLockButton = builder.addButton({\n\t\t.id = u\"passcode/auto-lock\"_q,\n\t\t.title = base::Platform::LastUserInputTimeSupported()\n\t\t\t? tr::lng_passcode_autolock_away()\n\t\t\t: tr::lng_passcode_autolock_inactive(),\n\t\t.icon = { &st::menuIconTimer },\n\t\t.label = std::move(autolockLabel),\n\t\t.keywords = { u\"timeout\"_q, u\"lock\"_q, u\"time\"_q },\n\t});\n\tif (autoLockButton) {\n\t\tautoLockButton->addClickHandler([=] {\n\t\t\tconst auto box = controller->show(Box<AutoLockBox>());\n\t\t\tbox->boxClosing(\n\t\t\t) | rpl::start_to_stream(state->autoLockBoxClosing, box->lifetime());\n\t\t});\n\t}\n\n\tbuilder.addSkip();\n\n\tusing Divider = CloudPassword::OneEdgeBoxContentDivider;\n\tbuilder.add([](const WidgetContext &ctx) {\n\t\tconst auto divider = Ui::CreateChild<Divider>(ctx.container.get());\n\t\tdivider->lower();\n\t\tconst auto about = ctx.container->add(\n\t\t\tobject_ptr<Ui::PaddingWrap<>>(\n\t\t\t\tctx.container,\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tctx.container,\n\t\t\t\t\trpl::combine(\n\t\t\t\t\t\ttr::lng_passcode_about1(),\n\t\t\t\t\t\ttr::lng_passcode_about3()\n\t\t\t\t\t) | rpl::map([](const QString &s1, const QString &s2) {\n\t\t\t\t\t\treturn s1 + \"\\n\\n\" + s2;\n\t\t\t\t\t}),\n\t\t\t\t\tst::boxDividerLabel),\n\t\t\tst::defaultBoxDividerLabelPadding));\n\t\tabout->geometryValue(\n\t\t) | rpl::on_next([=](const QRect &r) {\n\t\t\tdivider->setGeometry(r);\n\t\t}, divider->lifetime());\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n\n\tbuilder.add([](const WidgetContext &ctx) {\n\t\tconst auto systemUnlockWrap = ctx.container->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tctx.container,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(ctx.container))\n\t\t)->setDuration(0);\n\t\tconst auto systemUnlockContent = systemUnlockWrap->entity();\n\n\t\tconst auto unlockType = systemUnlockContent->lifetime().make_state<\n\t\t\trpl::variable<UnlockType>\n\t\t>(base::SystemUnlockStatus(\n\t\t\ttrue\n\t\t) | rpl::map([](base::SystemUnlockAvailability status) {\n\t\t\treturn status.withBiometrics\n\t\t\t\t? UnlockType::Biometrics\n\t\t\t\t: status.withCompanion\n\t\t\t\t? UnlockType::Companion\n\t\t\t\t: status.available\n\t\t\t\t? UnlockType::Default\n\t\t\t\t: UnlockType::None;\n\t\t}));\n\n\t\tconst auto highlights = ctx.highlights;\n\n\t\tunlockType->value(\n\t\t) | rpl::on_next([=](UnlockType type) {\n\t\t\twhile (systemUnlockContent->count()) {\n\t\t\t\tdelete systemUnlockContent->widgetAt(0);\n\t\t\t}\n\n\t\t\tUi::AddSkip(systemUnlockContent);\n\n\t\t\tconst auto biometricsButton = AddButtonWithIcon(\n\t\t\t\tsystemUnlockContent,\n\t\t\t\t(Platform::IsWindows()\n\t\t\t\t\t? tr::lng_settings_use_winhello()\n\t\t\t\t\t: (type == UnlockType::Biometrics)\n\t\t\t\t\t? tr::lng_settings_use_touchid()\n\t\t\t\t\t: (type == UnlockType::Companion)\n\t\t\t\t\t? tr::lng_settings_use_applewatch()\n\t\t\t\t\t: tr::lng_settings_use_systempwd()),\n\t\t\t\tst::settingsButton,\n\t\t\t\t{ Platform::IsWindows()\n\t\t\t\t\t? &st::menuIconWinHello\n\t\t\t\t\t: (type == UnlockType::Biometrics)\n\t\t\t\t\t? &st::menuIconTouchID\n\t\t\t\t\t: (type == UnlockType::Companion)\n\t\t\t\t\t? &st::menuIconAppleWatch\n\t\t\t\t\t: &st::menuIconSystemPwd });\n\t\t\tbiometricsButton->toggleOn(\n\t\t\t\trpl::single(Core::App().settings().systemUnlockEnabled())\n\t\t\t)->toggledChanges(\n\t\t\t) | rpl::filter([=](bool value) {\n\t\t\t\treturn value != Core::App().settings().systemUnlockEnabled();\n\t\t\t}) | rpl::on_next([=](bool value) {\n\t\t\t\tCore::App().settings().setSystemUnlockEnabled(value);\n\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t}, systemUnlockContent->lifetime());\n\n\t\t\tif (highlights) {\n\t\t\t\thighlights->push_back({\n\t\t\t\t\tu\"passcode/biometrics\"_q,\n\t\t\t\t\t{ biometricsButton.get() },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tUi::AddSkip(systemUnlockContent);\n\n\t\t\tUi::AddDividerText(\n\t\t\t\tsystemUnlockContent,\n\t\t\t\t(Platform::IsWindows()\n\t\t\t\t\t? tr::lng_settings_use_winhello_about()\n\t\t\t\t\t: (type == UnlockType::Biometrics)\n\t\t\t\t\t? tr::lng_settings_use_touchid_about()\n\t\t\t\t\t: (type == UnlockType::Companion)\n\t\t\t\t\t? tr::lng_settings_use_applewatch_about()\n\t\t\t\t\t: tr::lng_settings_use_systempwd_about()));\n\n\t\t}, systemUnlockContent->lifetime());\n\n\t\tsystemUnlockWrap->toggleOn(unlockType->value(\n\t\t) | rpl::map(rpl::mappers::_1 != UnlockType::None));\n\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"passcode/biometrics\"_q,\n\t\t\t.title = Platform::IsWindows()\n\t\t\t\t? tr::lng_settings_use_winhello(tr::now)\n\t\t\t\t: tr::lng_settings_use_touchid(tr::now),\n\t\t\t.keywords = { u\"biometrics\"_q, u\"touchid\"_q, u\"faceid\"_q,\n\t\t\t\tu\"winhello\"_q, u\"fingerprint\"_q },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"passcode/disable\"_q,\n\t\t\t.title = tr::lng_settings_passcode_disable(tr::now),\n\t\t\t.keywords = { u\"disable\"_q, u\"remove\"_q, u\"turn off\"_q },\n\t\t};\n\t});\n}\n\nclass LocalPasscodeManage : public Section<LocalPasscodeManage> {\npublic:\n\tLocalPasscodeManage(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~LocalPasscodeManage();\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\tvoid showFinished() override;\n\t[[nodiscard]] rpl::producer<> sectionShowBack() override;\n\n\t[[nodiscard]] rpl::producer<std::vector<Type>> removeFromStack() override;\n\n\t[[nodiscard]] base::weak_qptr<Ui::RpWidget> createPinnedToBottom(\n\t\tnot_null<Ui::RpWidget*> parent) override;\n\nprivate:\n\tvoid setupContent();\n\n\trpl::variable<bool> _isBottomFillerShown;\n\trpl::event_stream<> _showBack;\n\tQPointer<Ui::RpWidget> _disableButton;\n\n};\n\nLocalPasscodeManage::LocalPasscodeManage(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent();\n}\n\nrpl::producer<QString> LocalPasscodeManage::title() {\n\treturn tr::lng_settings_passcode_title();\n}\n\nrpl::producer<std::vector<Type>> LocalPasscodeManage::removeFromStack() {\n\treturn rpl::single(std::vector<Type>{\n\t\tLocalPasscodeManage::Id(),\n\t\tLocalPasscodeCreate::Id(),\n\t\tLocalPasscodeCheck::Id(),\n\t\tLocalPasscodeChange::Id(),\n\t});\n}\n\nvoid LocalPasscodeManage::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tCloudPassword::SetupAutoCloseTimer(\n\t\tcontent->lifetime(),\n\t\t[=] { _showBack.fire({}); },\n\t\t[] { return Core::App().lastNonIdleTime(); });\n\n\tconst SectionBuildMethod buildMethod = [](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tauto &lifetime = container->lifetime();\n\t\tconst auto highlights = lifetime.make_state<HighlightRegistry>();\n\n\t\tconst auto isPaused = Window::PausedIn(\n\t\t\tcontroller,\n\t\t\tWindow::GifPauseReason::Layer);\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = isPaused,\n\t\t\t.highlights = highlights,\n\t\t});\n\t\tBuildManageContent(builder);\n\n\t\tstd::move(showFinished) | rpl::on_next([=] {\n\t\t\tfor (const auto &[id, entry] : *highlights) {\n\t\t\t\tif (entry.widget) {\n\t\t\t\t\tcontroller->checkHighlightControl(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tentry.widget,\n\t\t\t\t\t\tbase::duplicate(entry.args));\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime);\n\t};\n\n\tbuild(content, buildMethod);\n\n\tUi::ResizeFitChild(this, content);\n}\n\nbase::weak_qptr<Ui::RpWidget> LocalPasscodeManage::createPinnedToBottom(\n\t\tnot_null<Ui::RpWidget*> parent) {\n\tconst auto weak = base::make_weak(this);\n\tauto callback = [=] {\n\t\tcontroller()->show(\n\t\t\tUi::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_settings_passcode_disable_sure(),\n\t\t\t\t.confirmed = [=](Fn<void()> &&close) {\n\t\t\t\t\tSetPasscode(controller(), QString());\n\t\t\t\t\tCore::App().settings().setSystemUnlockEnabled(false);\n\t\t\t\t\tCore::App().saveSettingsDelayed();\n\n\t\t\t\t\tclose();\n\t\t\t\t\tif (weak) {\n\t\t\t\t\t\t_showBack.fire({});\n\t\t\t\t\t}\n\t\t\t\t\tif (weak) {\n\t\t\t\t\t\tcontroller()->hideSpecialLayer();\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t.confirmText = tr::lng_settings_auto_night_disable(),\n\t\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t}));\n\t};\n\tauto bottomButton = CloudPassword::CreateBottomDisableButton(\n\t\tparent,\n\t\tgeometryValue(),\n\t\ttr::lng_settings_passcode_disable(),\n\t\tstd::move(callback));\n\n\t_isBottomFillerShown = base::take(bottomButton.isBottomFillerShown);\n\t_disableButton = bottomButton.button.get();\n\n\treturn bottomButton.content;\n}\n\nvoid LocalPasscodeManage::showFinished() {\n\tSection<LocalPasscodeManage>::showFinished();\n\tcontroller()->checkHighlightControl(\n\t\tu\"passcode/disable\"_q,\n\t\t_disableButton);\n}\n\nrpl::producer<> LocalPasscodeManage::sectionShowBack() {\n\treturn _showBack.events();\n}\n\nLocalPasscodeManage::~LocalPasscodeManage() = default;\n\nconst auto kMeta = BuildHelper({\n\t.id = LocalPasscodeManage::Id(),\n\t.parentId = nullptr,\n\t.title = &tr::lng_settings_passcode_title,\n\t.icon = &st::menuIconLock,\n}, [](SectionBuilder &builder) {\n\tBuildManageContent(builder);\n});\n\n} // namespace\n\nType LocalPasscodeCreateId() {\n\treturn LocalPasscodeCreate::Id();\n}\n\nType LocalPasscodeCheckId() {\n\treturn LocalPasscodeCheck::Id();\n}\n\nType LocalPasscodeManageId() {\n\treturn LocalPasscodeManage::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_local_passcode.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\nType LocalPasscodeCreateId();\nType LocalPasscodeCheckId();\nType LocalPasscodeManageId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_main.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_main.h\"\n\n#include \"settings/settings_common_session.h\"\n\n#include \"api/api_cloud_password.h\"\n#include \"api/api_credits.h\"\n#include \"api/api_global_privacy.h\"\n#include \"api/api_peer_photo.h\"\n#include \"api/api_premium.h\"\n#include \"api/api_sensitive_content.h\"\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"boxes/language_box.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"boxes/username_box.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/components/credits.h\"\n#include \"data/components/promo_suggestions.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"info/profile/info_profile_badge.h\"\n#include \"info/profile/info_profile_emoji_status_panel.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_cloud_manager.h\"\n#include \"lang/lang_instance.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"menu/menu_checked_action.h\"\n#include \"main/main_account.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/cloud_password/settings_cloud_password_input.h\"\n#include \"settings/sections/settings_advanced.h\"\n#include \"settings/sections/settings_business.h\"\n#include \"settings/sections/settings_calls.h\"\n#include \"settings/sections/settings_chat.h\"\n#include \"settings/sections/settings_fork.h\"\n#include \"settings/sections/settings_link_device.h\"\n#include \"settings/settings_codes.h\"\n#include \"settings/settings_faq_suggestions.h\"\n#include \"settings/sections/settings_credits.h\"\n#include \"settings/sections/settings_folders.h\"\n#include \"settings/sections/settings_information.h\"\n#include \"settings/sections/settings_notifications.h\"\n#include \"settings/settings_power_saving.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"settings/sections/settings_privacy_security.h\"\n#include \"settings/settings_scale_preview.h\"\n#include \"storage/localstorage.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/boxes/peer_qr_box.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/new_badges.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_item_base.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\n#include <QtGui/QClipboard>\n#include <QtGui/QGuiApplication>\n#include <QtGui/QWindow>\n\nnamespace Settings {\nnamespace {\n\nusing namespace Builder;\n\nconstexpr auto kSugValidatePhone = \"VALIDATE_PHONE_NUMBER\"_cs;\n\nclass Cover final : public Ui::FixedHeightWidget {\npublic:\n\tCover(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> user);\n\t~Cover();\n\n\t[[nodiscard]] not_null<Ui::UserpicButton*> userpic() const {\n\t\treturn _userpic.data();\n\t}\n\nprivate:\n\tvoid setupChildGeometry();\n\tvoid initViewers();\n\tvoid updatePhoneText();\n\tvoid refreshNameGeometry(int newWidth);\n\tvoid refreshPhoneGeometry(int newWidth);\n\tvoid refreshUsernameGeometry(int newWidth);\n\tvoid refreshQrButtonGeometry(int newWidth);\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<UserData*> _user;\n\tInfo::Profile::EmojiStatusPanel _emojiStatusPanel;\n\tInfo::Profile::Badge _badge;\n\n\tobject_ptr<Ui::UserpicButton> _userpic;\n\tobject_ptr<Ui::FlatLabel> _name = { nullptr };\n\tobject_ptr<Ui::FlatLabel> _phone = { nullptr };\n\tQString _phoneText;\n\tobject_ptr<Ui::FlatLabel> _username = { nullptr };\n\tobject_ptr<Ui::IconButton> _qrButton = { nullptr };\n\n};\n\nCover::Cover(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<UserData*> user)\n: FixedHeightWidget(\n\tparent,\n\tst::settingsPhotoTop\n\t\t+ st::infoProfileCover.photo.size.height()\n\t\t+ st::settingsPhotoBottom)\n, _controller(controller)\n, _user(user)\n, _badge(\n\tthis,\n\tst::settingsCoverBadge,\n\t&user->session(),\n\tInfo::Profile::BadgeContentForPeer(user),\n\t&_emojiStatusPanel,\n\t[=] {\n\t\treturn controller->isGifPausedAtLeastFor(\n\t\t\tWindow::GifPauseReason::Layer);\n\t},\n\t0, // customStatusLoopsLimit\n\tInfo::Profile::BadgeType::Premium)\n, _userpic(\n\tthis,\n\tcontroller,\n\t_user,\n\tUi::UserpicButton::Role::OpenPhoto,\n\tUi::UserpicButton::Source::PeerPhoto,\n\tst::infoProfileCover.photo)\n, _name(this, st::infoProfileCover.name)\n, _phone(this, st::defaultFlatLabel, st::popupMenuWithIcons)\n, _username(this, st::infoProfileMegagroupCover.status) {\n\t_user->updateFull();\n\n\t_name->setSelectable(true);\n\t_name->setContextCopyText(tr::lng_profile_copy_fullname(tr::now));\n\n\t_phone->setSelectable(true);\n\t_phone->setContextCopyText(tr::lng_profile_copy_phone(tr::now));\n\tconst auto hook = [=](Ui::FlatLabel::ContextMenuRequest request) {\n\t\tif (request.selection.empty()) {\n\t\t\tconst auto callback = [=] {\n\t\t\t\tauto phone = rpl::variable<TextWithEntities>(\n\t\t\t\t\tInfo::Profile::PhoneValue(_user)).current().text;\n\t\t\t\tphone.replace(' ', QString()).replace('-', QString());\n\t\t\t\tTextUtilities::SetClipboardText({ phone });\n\t\t\t};\n\t\t\trequest.menu->addAction(\n\t\t\t\ttr::lng_profile_copy_phone(tr::now),\n\t\t\t\tcallback,\n\t\t\t\t&st::menuIconCopy);\n\t\t} else {\n\t\t\t_phone->fillContextMenu(request);\n\t\t}\n\t\tconst auto hidden = _user->session().settings().phoneNumberHidden();\n\t\tconst auto toggle = [=] {\n\t\t\t_user->session().settings().setPhoneNumberHidden(\n\t\t\t\t!_user->session().settings().phoneNumberHidden());\n\t\t\t_user->session().saveSettingsDelayed();\n\t\t\tupdatePhoneText();\n\t\t};\n\t\tMenu::AddCheckedAction(\n\t\t\trequest.menu,\n\t\t\ttr::lng_context_spoiler_effect(tr::now),\n\t\t\ttoggle,\n\t\t\t&st::menuIconSpoiler,\n\t\t\thidden);\n\t};\n\t_phone->setContextMenuHook(hook);\n\n\tinitViewers();\n\tsetupChildGeometry();\n\n\t_userpic->switchChangePhotoOverlay(_user->isSelf(), [=](\n\t\t\tUi::UserpicButton::ChosenImage chosen) {\n\t\tauto &image = chosen.image;\n\t\t_userpic->showCustom(base::duplicate(image));\n\t\tconst auto isMarkup = (chosen.markup.documentId != 0);\n\t\t_user->session().api().peerPhoto().upload(\n\t\t\t_user,\n\t\t\t{\n\t\t\t\tstd::move(image),\n\t\t\t\tchosen.markup.documentId,\n\t\t\t\tchosen.markup.colors,\n\t\t\t});\n\t\tif (!isMarkup) {\n\t\t\t_userpic->showUploadProgress();\n\t\t}\n\t});\n\n\t_badge.setPremiumClickCallback([=] {\n\t\t_emojiStatusPanel.show(\n\t\t\t_controller,\n\t\t\t_badge.widget(),\n\t\t\t_badge.sizeTag());\n\t});\n\t_badge.updated() | rpl::on_next([=] {\n\t\trefreshNameGeometry(width());\n\t}, _name->lifetime());\n\n\t_qrButton.create(this, st::infoProfileLabeledButtonQr);\n\t_qrButton->setAccessibleName(tr::lng_group_invite_context_qr(tr::now));\n\t_qrButton->setClickedCallback([=, show = controller->uiShow()] {\n\t\tUi::DefaultShowFillPeerQrBoxCallback(show, _user);\n\t});\n\tInfo::Profile::UsernamesValue(\n\t\t_user\n\t) | rpl::on_next([=](const auto &usernames) {\n\t\t_qrButton->setVisible(!usernames.empty());\n\t\trefreshNameGeometry(width());\n\t\trefreshQrButtonGeometry(width());\n\t}, _qrButton->lifetime());\n}\n\nCover::~Cover() = default;\n\nvoid Cover::setupChildGeometry() {\n\tusing namespace rpl::mappers;\n\twidthValue(\n\t) | rpl::on_next([=](int newWidth) {\n\t\t_userpic->moveToLeft(\n\t\t\tst::settingsPhotoLeft,\n\t\t\tst::settingsPhotoTop,\n\t\t\tnewWidth);\n\t\trefreshNameGeometry(newWidth);\n\t\trefreshPhoneGeometry(newWidth);\n\t\trefreshUsernameGeometry(newWidth);\n\t\trefreshQrButtonGeometry(newWidth);\n\t}, lifetime());\n}\n\nvoid Cover::initViewers() {\n\tInfo::Profile::NameValue(\n\t\t_user\n\t) | rpl::on_next([=](const QString &name) {\n\t\t_name->setText(name);\n\t\trefreshNameGeometry(width());\n\t}, lifetime());\n\n\tInfo::Profile::PhoneValue(\n\t\t_user\n\t) | rpl::on_next([=](const TextWithEntities &value) {\n\t\t_phoneText = value.text;\n\t\tupdatePhoneText();\n\t}, lifetime());\n\n\tInfo::Profile::UsernameValue(\n\t\t_user\n\t) | rpl::on_next([=](const TextWithEntities &value) {\n\t\t_username->setMarkedText(tr::link(value.text.isEmpty()\n\t\t\t? tr::lng_settings_username_add(tr::now)\n\t\t\t: value.text));\n\t\trefreshUsernameGeometry(width());\n\t}, lifetime());\n\n\t_username->overrideLinkClickHandler([=] {\n\t\tif (_controller->showFrozenError()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto username = _user->username();\n\t\tif (username.isEmpty()) {\n\t\t\t_controller->show(Box(UsernamesBox, _user));\n\t\t} else {\n\t\t\tQGuiApplication::clipboard()->setText(\n\t\t\t\t_user->session().createInternalLinkFull(username));\n\t\t\t_controller->showToast(tr::lng_username_copied(tr::now));\n\t\t}\n\t});\n}\n\nvoid Cover::refreshNameGeometry(int newWidth) {\n\tconst auto nameLeft = st::settingsNameLeft;\n\tconst auto nameTop = st::settingsNameTop;\n\tconst auto qrButtonWidth = (_qrButton && !_qrButton->isHidden())\n\t\t? (_qrButton->width() + st::infoProfileCover.rightSkip)\n\t\t: 0;\n\tauto nameWidth = newWidth\n\t\t- nameLeft\n\t\t- st::infoProfileCover.rightSkip\n\t\t- qrButtonWidth;\n\tif (const auto width = _badge.widget() ? _badge.widget()->width() : 0) {\n\t\tnameWidth -= st::infoVerifiedCheckPosition.x() + width;\n\t}\n\t_name->resizeToNaturalWidth(nameWidth);\n\t_name->moveToLeft(nameLeft, nameTop, newWidth);\n\tconst auto badgeLeft = nameLeft + _name->width();\n\tconst auto badgeTop = nameTop;\n\tconst auto badgeBottom = nameTop + _name->height();\n\t_badge.move(badgeLeft, badgeTop, badgeBottom);\n}\n\nvoid Cover::updatePhoneText() {\n\tif (_user->session().settings().phoneNumberHidden()) {\n\t\t_phone->setMarkedText(\n\t\t\tUi::Text::Wrapped({ _phoneText }, EntityType::Spoiler));\n\t} else {\n\t\t_phone->setText(_phoneText);\n\t}\n\trefreshPhoneGeometry(width());\n}\n\nvoid Cover::refreshPhoneGeometry(int newWidth) {\n\tconst auto phoneLeft = st::settingsPhoneLeft;\n\tconst auto phoneTop = st::settingsPhoneTop;\n\tconst auto phoneWidth = newWidth\n\t\t- phoneLeft\n\t\t- st::infoProfileCover.rightSkip;\n\t_phone->resizeToWidth(phoneWidth);\n\t_phone->moveToLeft(phoneLeft, phoneTop, newWidth);\n}\n\nvoid Cover::refreshUsernameGeometry(int newWidth) {\n\tconst auto usernameLeft = st::settingsUsernameLeft;\n\tconst auto usernameTop = st::settingsUsernameTop;\n\tconst auto usernameRight = st::infoProfileCover.rightSkip;\n\tconst auto usernameWidth = newWidth - usernameLeft - usernameRight;\n\t_username->resizeToWidth(usernameWidth);\n\t_username->moveToLeft(usernameLeft, usernameTop, newWidth);\n}\n\nvoid Cover::refreshQrButtonGeometry(int newWidth) {\n\tif (!_qrButton) {\n\t\treturn;\n\t}\n\tconst auto buttonTop = (height() - _qrButton->height()) / 2;\n\tconst auto buttonRight = st::infoProfileCover.rightSkip;\n\tconst auto inset = st::infoProfileLabeledButtonQrInset;\n\t_qrButton->moveToRight(buttonRight - inset, buttonTop, newWidth);\n}\n\nvoid BuildSectionButtons(SectionBuilder &builder) {\n\tconst auto session = builder.session();\n\tconst auto controller = builder.controller();\n\tconst auto showOther = builder.showOther();\n\n\tif (!session->supportMode()) {\n\t\tbuilder.addSectionButton({\n\t\t\t.title = tr::lng_settings_my_account(),\n\t\t\t.targetSection = InformationId(),\n\t\t\t.icon = { &st::menuIconProfile },\n\t\t\t.keywords = { u\"profile\"_q, u\"edit\"_q, u\"information\"_q },\n\t\t});\n\t}\n\n\tbuilder.addSectionButton({\n\t\t.title = tr::lng_settings_section_notify(),\n\t\t.targetSection = NotificationsId(),\n\t\t.icon = { &st::menuIconNotifications },\n\t\t.keywords = { u\"alerts\"_q, u\"sounds\"_q, u\"badge\"_q },\n\t});\n\n\tbuilder.addSectionButton({\n\t\t.title = tr::lng_settings_section_privacy(),\n\t\t.targetSection = PrivacySecurityId(),\n\t\t.icon = { &st::menuIconLock },\n\t\t.keywords = { u\"security\"_q, u\"passcode\"_q, u\"password\"_q, u\"2fa\"_q },\n\t});\n\n\tbuilder.addSectionButton({\n\t\t.title = tr::lng_settings_section_chat_settings(),\n\t\t.targetSection = ChatId(),\n\t\t.icon = { &st::menuIconChatBubble },\n\t\t.keywords = { u\"themes\"_q, u\"appearance\"_q, u\"stickers\"_q },\n\t});\n\n\t{ // Folders\n\t\tconst auto preload = [=] {\n\t\t\tsession->data().chatsFilters().requestSuggested();\n\t\t};\n\t\tconst auto hasFilters = session->data().chatsFilters().has()\n\t\t\t|| session->settings().dialogsFiltersEnabled();\n\n\t\tauto shownProducer = hasFilters\n\t\t\t? rpl::single(true) | rpl::type_erased\n\t\t\t: (rpl::single(rpl::empty) | rpl::then(\n\t\t\t\tsession->appConfig().refreshed()\n\t\t\t) | rpl::map([=] {\n\t\t\tconst auto enabled = session->appConfig().get<bool>(\n\t\t\t\tu\"dialog_filters_enabled\"_q,\n\t\t\t\tfalse);\n\t\t\tif (enabled) {\n\t\t\t\tpreload();\n\t\t\t}\n\t\t\treturn enabled;\n\t\t}));\n\n\t\tif (hasFilters) {\n\t\t\tpreload();\n\t\t}\n\n\t\tbuilder.addButton({\n\t\t\t.title = tr::lng_settings_section_filters(),\n\t\t\t.icon = { &st::menuIconShowInFolder },\n\t\t\t.onClick = [=] { showOther(FoldersId()); },\n\t\t\t.keywords = { u\"filters\"_q, u\"tabs\"_q },\n\t\t\t.shown = std::move(shownProducer),\n\t\t});\n\t}\n\n\tbuilder.addSectionButton({\n\t\t.title = tr::lng_settings_advanced(),\n\t\t.targetSection = AdvancedId(),\n\t\t.icon = { &st::menuIconManage },\n\t\t.keywords = { u\"performance\"_q, u\"proxy\"_q, u\"experimental\"_q },\n\t});\n\n\tbuilder.addSectionButton({\n\t\t.title = tr::lng_settings_section_devices(),\n\t\t.targetSection = CallsId(),\n\t\t.icon = { &st::menuIconUnmute },\n\t\t.keywords = { u\"sessions\"_q, u\"calls\"_q },\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"main/power\"_q,\n\t\t.title = tr::lng_settings_power_menu(),\n\t\t.icon = { &st::menuIconPowerUsage },\n\t\t.onClick = [=] {\n\t\t\tcontroller->show(Box(PowerSavingBox, PowerSaving::Flags()));\n\t\t},\n\t\t.keywords = { u\"battery\"_q, u\"animations\"_q, u\"power\"_q, u\"saving\"_q },\n\t});\n\n\tbuilder.addSectionButton({\n\t\t.title = tr::lng_settings_link_device(),\n\t\t.targetSection = LinkDeviceId(),\n\t\t.icon = { &st::menuIconQrCode },\n\t\t.keywords = { u\"link\"_q, u\"device\"_q, u\"qr\"_q, u\"scan\"_q },\n\t});\n\n\tbuilder.addSectionButton({\n\t\t.title = tr::lng_settings_section_fork(),\n\t\t.targetSection = ForkId(),\n\t\t.icon = { &st::menuIconForkSettings },\n\t\t.keywords = { u\"fork\"_q },\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"main/language\"_q,\n\t\t.title = tr::lng_settings_language(),\n\t\t.icon = { &st::menuIconTranslate },\n\t\t.label = rpl::single(\n\t\t\tLang::GetInstance().id()\n\t\t) | rpl::then(\n\t\t\tLang::GetInstance().idChanges()\n\t\t) | rpl::map([] { return Lang::GetInstance().nativeName(); }),\n\t\t.onClick = [=] {\n\t\t\tstatic auto Guard = base::binary_guard();\n\t\t\tGuard = LanguageBox::Show(controller);\n\t\t},\n\t\t.keywords = { u\"translate\"_q, u\"localization\"_q, u\"language\"_q },\n\t});\n}\n\nvoid BuildInterfaceScale(SectionBuilder &builder) {\n\tif (!HasInterfaceScale()) {\n\t\treturn;\n\t}\n\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\n\tbuilder.add([](const WidgetContext &ctx) {\n\t\tconst auto window = &ctx.controller->window();\n\t\tauto wrap = object_ptr<Ui::VerticalLayout>(ctx.container);\n\t\tSetupInterfaceScale(window, wrap.data());\n\t\treturn SectionBuilder::WidgetToAdd{ .widget = std::move(wrap) };\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"main/scale\"_q,\n\t\t\t.title = tr::lng_settings_default_scale(tr::now),\n\t\t\t.keywords = { u\"zoom\"_q, u\"size\"_q, u\"interface\"_q, u\"ui\"_q },\n\t\t};\n\t});\n\n\tbuilder.addSkip();\n}\n\nvoid BuildPremiumSection(SectionBuilder &builder) {\n\tconst auto session = builder.session();\n\tconst auto controller = builder.controller();\n\tconst auto showOther = builder.showOther();\n\n\tif (!session->premiumPossible()) {\n\t\treturn;\n\t}\n\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\n\tbuilder.addPremiumButton({\n\t\t.id = u\"main/premium\"_q,\n\t\t.title = tr::lng_premium_summary_title(),\n\t\t.onClick = [=] {\n\t\t\tcontroller->setPremiumRef(\"settings\");\n\t\t\tshowOther(PremiumId());\n\t\t},\n\t\t.keywords = { u\"subscription\"_q },\n\t});\n\n\tsession->credits().load();\n\tbuilder.addPremiumButton({\n\t\t.id = u\"main/credits\"_q,\n\t\t.title = tr::lng_settings_credits(),\n\t\t.label = session->credits().balanceValue(\n\t\t) | rpl::map([](CreditsAmount c) {\n\t\t\treturn c\n\t\t\t\t? Lang::FormatCreditsAmountToShort(c).string\n\t\t\t\t: QString();\n\t\t}),\n\t\t.credits = true,\n\t\t.onClick = [=] {\n\t\t\tcontroller->setPremiumRef(\"settings\");\n\t\t\tshowOther(CreditsId());\n\t\t},\n\t\t.keywords = { u\"stars\"_q, u\"balance\"_q },\n\t});\n\n\tsession->credits().tonLoad();\n\tbuilder.addButton({\n\t\t.id = u\"main/currency\"_q,\n\t\t.title = tr::lng_settings_currency(),\n\t\t.icon = { &st::menuIconTon },\n\t\t.label = session->credits().tonBalanceValue(\n\t\t) | rpl::map([](CreditsAmount c) {\n\t\t\treturn c ? Lang::FormatCreditsAmountToShort(c).string : u\"\"_q;\n\t\t}),\n\t\t.onClick = [=] {\n\t\t\tcontroller->setPremiumRef(\"settings\");\n\t\t\tshowOther(CurrencyId());\n\t\t},\n\t\t.keywords = { u\"ton\"_q, u\"crypto\"_q, u\"wallet\"_q },\n\t\t.shown = session->credits().tonBalanceValue(\n\t\t) | rpl::map([](CreditsAmount c) { return !c.empty(); }),\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"main/business\"_q,\n\t\t.title = tr::lng_business_title(),\n\t\t.icon = { .icon = &st::menuIconShop },\n\t\t.onClick = [=] { showOther(BusinessId()); },\n\t\t.keywords = { u\"work\"_q, u\"company\"_q },\n\t});\n\n\tif (session->premiumCanBuy()) {\n\t\tbuilder.addButton({\n\t\t\t.id = u\"main/send-gift\"_q,\n\t\t\t.title = tr::lng_settings_gift_premium(),\n\t\t\t.icon = { .icon = &st::menuIconGiftPremium, .newBadge = true },\n\t\t\t.onClick = [=] { Ui::ChooseStarGiftRecipient(controller); },\n\t\t\t.keywords = { u\"present\"_q, u\"send\"_q },\n\t\t});\n\t}\n\n\tbuilder.addSkip();\n}\n\nvoid BuildHelpSection(SectionBuilder &builder) {\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\n\tconst auto controller = builder.controller();\n\tbuilder.addButton({\n\t\t.id = u\"main/faq\"_q,\n\t\t.title = tr::lng_settings_faq(),\n\t\t.icon = { &st::menuIconFaq },\n\t\t.onClick = [=] { OpenFaq(controller); },\n\t\t.keywords = { u\"help\"_q, u\"support\"_q, u\"questions\"_q },\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"main/features\"_q,\n\t\t.title = tr::lng_settings_features(),\n\t\t.icon = { &st::menuIconEmojiObjects },\n\t\t.onClick = [] {\n\t\t\tUrlClickHandler::Open(tr::lng_telegram_features_url(tr::now));\n\t\t},\n\t\t.keywords = { u\"tips\"_q, u\"tutorial\"_q },\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"main/ask-question\"_q,\n\t\t.title = tr::lng_settings_ask_question(),\n\t\t.icon = { &st::menuIconDiscussion },\n\t\t.onClick = [=] { OpenAskQuestionConfirm(controller); },\n\t\t.keywords = { u\"contact\"_q, u\"feedback\"_q },\n\t});\n\n\tbuilder.addSkip();\n}\n\nvoid BuildValidationSuggestions(SectionBuilder &builder) {\n\tbuilder.add([](const WidgetContext &ctx) {\n\t\tconst auto controller = ctx.controller.get();\n\t\tconst auto showOther = ctx.showOther;\n\t\tauto wrap = object_ptr<Ui::VerticalLayout>(ctx.container);\n\t\tSetupValidatePhoneNumberSuggestion(controller, wrap.data(), showOther);\n\t\treturn SectionBuilder::WidgetToAdd{ .widget = std::move(wrap) };\n\t});\n\n\tbuilder.add([](const WidgetContext &ctx) {\n\t\tconst auto controller = ctx.controller.get();\n\t\tconst auto showOther = ctx.showOther;\n\t\tauto wrap = object_ptr<Ui::VerticalLayout>(ctx.container);\n\t\tSetupValidatePasswordSuggestion(controller, wrap.data(), showOther);\n\t\treturn SectionBuilder::WidgetToAdd{ .widget = std::move(wrap) };\n\t});\n}\n\nclass Main final : public Section<Main> {\npublic:\n\tMain(QWidget *parent, not_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\tvoid fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override;\n\tvoid showFinished() override;\n\nprotected:\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tvoid setupContent();\n\n\tQPointer<Ui::UserpicButton> _userpic;\n\n};\n\nMain::Main(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent();\n}\n\nrpl::producer<QString> Main::title() {\n\treturn tr::lng_menu_settings();\n}\n\nvoid Main::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {\n\tconst auto &list = Core::App().domain().accounts();\n\tif (list.size() < Core::App().domain().maxAccounts()) {\n\t\taddAction(tr::lng_menu_add_account(tr::now), [=] {\n\t\t\tCore::App().domain().addActivated(MTP::Environment{});\n\t\t}, &st::menuIconAddAccount);\n\t}\n\tif (!controller()->session().supportMode()) {\n\t\taddAction(\n\t\t\ttr::lng_settings_information(tr::now),\n\t\t\t[=] { showOther(InformationId()); },\n\t\t\t&st::menuIconEdit);\n\t}\n\tconst auto window = &controller()->window();\n\tconst auto logout = addAction({\n\t\t.text = tr::lng_settings_logout(tr::now),\n\t\t.handler = [=] { window->showLogoutConfirmation(); },\n\t\t.icon = &st::menuIconLeaveAttention,\n\t\t.isAttention = true,\n\t});\n\tlogout->setProperty(\"highlight-control-id\", u\"settings/log-out\"_q);\n}\n\nvoid Main::keyPressEvent(QKeyEvent *e) {\n\tcrl::on_main(this, [=, text = e->text()]{\n\t\tCodesFeedString(controller(), text);\n\t});\n\treturn Section::keyPressEvent(e);\n}\n\nvoid Main::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tconst auto window = controller();\n\tconst auto session = &window->session();\n\tconst auto cover = content->add(object_ptr<Cover>(\n\t\tcontent,\n\t\twindow,\n\t\tsession->user()));\n\t_userpic = cover->userpic();\n\n\tconst SectionBuildMethod buildMethod = [](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tauto &lifetime = container->lifetime();\n\t\tconst auto highlights = lifetime.make_state<HighlightRegistry>();\n\t\tconst auto isPaused = Window::PausedIn(\n\t\t\tcontroller,\n\t\t\tWindow::GifPauseReason::Layer);\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = isPaused,\n\t\t\t.highlights = highlights,\n\t\t});\n\t\tbuilder.addDivider();\n\t\tbuilder.addSkip();\n\t\tBuildValidationSuggestions(builder);\n\t\tBuildSectionButtons(builder);\n\t\tbuilder.addSkip();\n\t\tBuildInterfaceScale(builder);\n\t\tBuildPremiumSection(builder);\n\t\tBuildHelpSection(builder);\n\n\t\tstd::move(showFinished) | rpl::on_next([=] {\n\t\t\tfor (const auto &[id, entry] : *highlights) {\n\t\t\t\tif (entry.widget) {\n\t\t\t\t\tcontroller->checkHighlightControl(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tentry.widget,\n\t\t\t\t\t\tbase::duplicate(entry.args));\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime);\n\t};\n\tbuild(content, buildMethod);\n\n\tUi::ResizeFitChild(this, content);\n\n\tsession->api().cloudPassword().reload();\n\tsession->api().reloadContactSignupSilent();\n\tsession->api().sensitiveContent().reload();\n\tsession->api().globalPrivacy().reload();\n\tsession->api().premium().reload();\n\tsession->data().cloudThemes().refresh();\n\tsession->faqSuggestions().request();\n}\n\nvoid Main::showFinished() {\n\tcontroller()->checkHighlightControl(u\"profile-photo\"_q, _userpic.data(), {\n\t\t.margin = st::settingsPhotoHighlightMargin,\n\t\t.shape = HighlightShape::Ellipse,\n\t});\n\tconst auto emojiId = u\"profile-photo/use-emoji\"_q;\n\tif (controller()->takeHighlightControlId(emojiId)) {\n\t\tif (const auto popupMenu = _userpic->showChangePhotoMenu()) {\n\t\t\tconst auto menu = popupMenu->menu();\n\t\t\tfor (const auto &action : menu->actions()) {\n\t\t\t\tconst auto controlId = \"highlight-control-id\";\n\t\t\t\tif (action->property(controlId).toString() == emojiId) {\n\t\t\t\t\tif (const auto item = menu->itemForAction(action)) {\n\t\t\t\t\t\tHighlightWidget(item);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tSection<Main>::showFinished();\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = Main::Id(),\n\t.parentId = nullptr,\n\t.title = &tr::lng_menu_settings,\n\t.icon = &st::menuIconSettings,\n}, [](SectionBuilder &builder) {\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"main/profile-photo\"_q,\n\t\t\t.title = tr::lng_profile_set_photo_for(tr::now),\n\t\t\t.keywords = { u\"photo\"_q, u\"avatar\"_q, u\"picture\"_q, u\"profile\"_q },\n\t\t\t.icon = { &st::menuIconProfile },\n\t\t\t.deeplink = u\"tg://settings/profile-photo\"_q,\n\t\t};\n\t});\n\n\tBuildValidationSuggestions(builder);\n\tBuildSectionButtons(builder);\n\n\tbuilder.addSkip();\n\n\tBuildInterfaceScale(builder);\n\tBuildPremiumSection(builder);\n\tBuildHelpSection(builder);\n});\n\n} // namespace\n\nvoid SetupLanguageButton(\n\t\tnot_null<Window::Controller*> window,\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\tconst auto button = AddButtonWithLabel(\n\t\tcontainer,\n\t\ttr::lng_settings_language(),\n\t\trpl::single(\n\t\t\tLang::GetInstance().id()\n\t\t) | rpl::then(\n\t\t\tLang::GetInstance().idChanges()\n\t\t) | rpl::map([] { return Lang::GetInstance().nativeName(); }),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconTranslate });\n\tconst auto guard = Ui::CreateChild<base::binary_guard>(button.get());\n\tbutton->addClickHandler([=] {\n\t\tconst auto m = button->clickModifiers();\n\t\tif ((m & Qt::ShiftModifier) && (m & Qt::AltModifier)) {\n\t\t\tLang::CurrentCloudManager().switchToLanguage({ u\"#custom\"_q });\n\t\t} else {\n\t\t\t*guard = LanguageBox::Show(window->sessionController());\n\t\t}\n\t});\n}\n\nvoid SetupValidatePhoneNumberSuggestion(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tFn<void(Type)> showOther) {\n\tif (!controller->session().promoSuggestions().current(\n\t\t\tkSugValidatePhone.utf8())) {\n\t\treturn;\n\t}\n\tconst auto mainWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto content = mainWrap->entity();\n\tUi::AddSubsectionTitle(\n\t\tcontent,\n\t\ttr::lng_settings_suggestion_phone_number_title(\n\t\t\tlt_phone,\n\t\t\trpl::single(\n\t\t\t\tUi::FormatPhone(controller->session().user()->phone()))),\n\t\tQMargins(\n\t\t\tst::boxRowPadding.left()\n\t\t\t\t- st::defaultSubsectionTitlePadding.left(),\n\t\t\t0,\n\t\t\t0,\n\t\t\t0));\n\tconst auto label = content->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_settings_suggestion_phone_number_about(\n\t\t\t\tlt_link,\n\t\t\t\ttr::lng_collectible_learn_more(tr::url(\n\t\t\t\t\ttr::lng_settings_suggestion_phone_number_about_link(\n\t\t\t\t\t\ttr::now))),\n\t\t\t\ttr::marked),\n\t\t\tst::boxLabel),\n\t\tst::boxRowPadding);\n\tlabel->setClickHandlerFilter([=, weak = base::make_weak(controller)](\n\t\t\tconst auto &...) {\n\t\tUrlClickHandler::Open(\n\t\t\ttr::lng_settings_suggestion_phone_number_about_link(tr::now),\n\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.sessionWindow = weak,\n\t\t\t}));\n\t\treturn false;\n\t});\n\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\n\tconst auto wrap = content->add(\n\t\tobject_ptr<Ui::FixedHeightWidget>(\n\t\t\tcontent,\n\t\t\tst::inviteLinkButton.height),\n\t\tst::inviteLinkButtonsPadding);\n\tconst auto yes = Ui::CreateChild<Ui::RoundButton>(\n\t\twrap,\n\t\ttr::lng_box_yes(),\n\t\tst::inviteLinkButton);\n\tyes->setClickedCallback([=] {\n\t\tcontroller->session().promoSuggestions().dismiss(\n\t\t\tkSugValidatePhone.utf8());\n\t\tmainWrap->toggle(false, anim::type::normal);\n\t});\n\tconst auto no = Ui::CreateChild<Ui::RoundButton>(\n\t\twrap,\n\t\ttr::lng_box_no(),\n\t\tst::inviteLinkButton);\n\tno->setClickedCallback([=] {\n\t\tconst auto sharedLabel = std::make_shared<base::weak_qptr<Ui::FlatLabel>>();\n\t\tconst auto height = st::boxLabel.style.font->height;\n\t\tconst auto customEmojiFactory = [=](\n\t\t\tQStringView data,\n\t\t\tconst Ui::Text::MarkedContext &context\n\t\t) -> std::unique_ptr<Ui::Text::CustomEmoji> {\n\t\t\tauto repaint = [=] {\n\t\t\t\tif (*sharedLabel) {\n\t\t\t\t\t(*sharedLabel)->update();\n\t\t\t\t}\n\t\t\t};\n\t\t\treturn Lottie::MakeEmoji(\n\t\t\t\t{ .name = u\"change_number\"_q, .sizeOverride = Size(height) },\n\t\t\t\tstd::move(repaint));\n\t\t};\n\n\t\tcontroller->uiShow()->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tbox->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });\n\t\t\t*sharedLabel = box->verticalLayout()->add(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tbox->verticalLayout(),\n\t\t\t\t\ttr::lng_settings_suggestion_phone_number_change(\n\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\trpl::single(Ui::Text::SingleCustomEmoji(u\"@\"_q)),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\tst::boxLabel,\n\t\t\t\t\tst::defaultPopupMenu,\n\t\t\t\t\tUi::Text::MarkedContext{\n\t\t\t\t\t\t.customEmojiFactory = customEmojiFactory,\n\t\t\t\t\t}),\n\t\t\t\tst::boxPadding);\n\t\t}));\n\t});\n\n\twrap->widthValue() | rpl::on_next([=](int width) {\n\t\tconst auto buttonWidth = (width - st::inviteLinkButtonsSkip) / 2;\n\t\tyes->setFullWidth(buttonWidth);\n\t\tno->setFullWidth(buttonWidth);\n\t\tyes->moveToLeft(0, 0, width);\n\t\tno->moveToRight(0, 0, width);\n\t}, wrap->lifetime());\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tUi::AddDivider(content);\n\tUi::AddSkip(content);\n}\n\nvoid SetupValidatePasswordSuggestion(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tFn<void(Type)> showOther) {\n\tif (!controller->session().promoSuggestions().current(\n\t\t\tData::PromoSuggestions::SugValidatePassword())\n\t\t|| controller->session().promoSuggestions().current(\n\t\t\tkSugValidatePhone.utf8())) {\n\t\treturn;\n\t}\n\tconst auto mainWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto content = mainWrap->entity();\n\tUi::AddSubsectionTitle(\n\t\tcontent,\n\t\ttr::lng_settings_suggestion_password_title(),\n\t\tQMargins(\n\t\t\tst::boxRowPadding.left()\n\t\t\t\t- st::defaultSubsectionTitlePadding.left(),\n\t\t\t0,\n\t\t\t0,\n\t\t\t0));\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_settings_suggestion_password_about(),\n\t\t\tst::boxLabel),\n\t\tst::boxRowPadding);\n\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\n\tconst auto wrap = content->add(\n\t\tobject_ptr<Ui::FixedHeightWidget>(\n\t\t\tcontent,\n\t\t\tst::inviteLinkButton.height),\n\t\tst::inviteLinkButtonsPadding);\n\tconst auto yes = Ui::CreateChild<Ui::RoundButton>(\n\t\twrap,\n\t\ttr::lng_settings_suggestion_password_yes(),\n\t\tst::inviteLinkButton);\n\tyes->setClickedCallback([=] {\n\t\tcontroller->session().promoSuggestions().dismiss(\n\t\t\tData::PromoSuggestions::SugValidatePassword());\n\t\tmainWrap->toggle(false, anim::type::normal);\n\t});\n\tconst auto no = Ui::CreateChild<Ui::RoundButton>(\n\t\twrap,\n\t\ttr::lng_settings_suggestion_password_no(),\n\t\tst::inviteLinkButton);\n\tno->setClickedCallback([=] {\n\t\tshowOther(Settings::CloudPasswordSuggestionInputId());\n\t});\n\n\twrap->widthValue() | rpl::on_next([=](int width) {\n\t\tconst auto buttonWidth = (width - st::inviteLinkButtonsSkip) / 2;\n\t\tyes->setFullWidth(buttonWidth);\n\t\tno->setFullWidth(buttonWidth);\n\t\tyes->moveToLeft(0, 0, width);\n\t\tno->moveToRight(0, 0, width);\n\t}, wrap->lifetime());\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tUi::AddDivider(content);\n\tUi::AddSkip(content);\n}\n\nbool HasInterfaceScale() {\n\treturn true;\n}\n\nvoid SetupInterfaceScale(\n\t\tnot_null<Window::Controller*> window,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tbool icon) {\n\tif (!HasInterfaceScale()) {\n\t\treturn;\n\t}\n\n\tconst auto toggled = Ui::CreateChild<rpl::event_stream<bool>>(\n\t\tcontainer.get());\n\n\tconst auto switched = (cConfigScale() == style::kScaleAuto);\n\tconst auto button = AddButtonWithIcon(\n\t\tcontainer,\n\t\ttr::lng_settings_default_scale(),\n\t\ticon ? st::settingsButton : st::settingsButtonNoIcon,\n\t\t{ icon ? &st::menuIconShowInChat : nullptr }\n\t)->toggleOn(toggled->events_starting_with_copy(switched));\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto scaleMin = style::kScaleMin;\n\tconst auto scaleMax = style::MaxScaleForRatio(ratio);\n\tconst auto scaleConfig = cConfigScale();\n\tconst auto step = 5;\n\tAssert(!((scaleMax - scaleMin) % step));\n\tauto values = std::vector<int>();\n\tfor (auto i = scaleMin; i != scaleMax; i += step) {\n\t\tvalues.push_back(i);\n\t\tif (scaleConfig > i && scaleConfig < i + step) {\n\t\t\tvalues.push_back(scaleConfig);\n\t\t}\n\t}\n\tvalues.push_back(scaleMax);\n\tconst auto valuesCount = int(values.size());\n\n\tconst auto valueFromScale = [=](int scale) {\n\t\tscale = cEvalScale(scale);\n\t\tauto result = 0;\n\t\tfor (const auto value : values) {\n\t\t\tif (scale == value) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t++result;\n\t\t}\n\t\treturn ((result == valuesCount) ? (result - 1) : result)\n\t\t\t/ float64(valuesCount - 1);\n\t};\n\tauto sliderWithLabel = MakeSliderWithLabel(\n\t\tcontainer,\n\t\tst::settingsScale,\n\t\tst::settingsScaleLabel,\n\t\tst::normalFont->spacew * 2,\n\t\tst::settingsScaleLabel.style.font->width(\"300%\"),\n\t\ttrue);\n\tcontainer->add(\n\t\tstd::move(sliderWithLabel.widget),\n\t\ticon ? st::settingsScalePadding : st::settingsBigScalePadding);\n\tconst auto slider = sliderWithLabel.slider;\n\tconst auto label = sliderWithLabel.label;\n\tslider->setAccessibleName(tr::lng_settings_scale(tr::now));\n\n\tconst auto updateLabel = [=](int scale) {\n\t\tconst auto labelText = [&](int scale) {\n\t\t\tif constexpr (Platform::IsMac()) {\n\t\t\t\treturn QString::number(scale) + '%';\n\t\t\t} else {\n\t\t\t\tconst auto handle = window->widget()->windowHandle();\n\t\t\t\tconst auto ratio = handle->devicePixelRatio();\n\t\t\t\treturn QString::number(base::SafeRound(scale * ratio)) + '%';\n\t\t\t}\n\t\t};\n\t\tlabel->setText(labelText(cEvalScale(scale)));\n\t};\n\tupdateLabel(cConfigScale());\n\n\tconst auto inSetScale = container->lifetime().make_state<bool>();\n\tconst auto setScale = [=](int scale, const auto &repeatSetScale) -> void {\n\t\tif (*inSetScale) {\n\t\t\treturn;\n\t\t}\n\t\t*inSetScale = true;\n\t\tconst auto guard = gsl::finally([=] { *inSetScale = false; });\n\n\t\tupdateLabel(scale);\n\t\ttoggled->fire(scale == style::kScaleAuto);\n\t\tslider->setValue(valueFromScale(scale));\n\t\tif (cEvalScale(scale) != cEvalScale(cConfigScale())) {\n\t\t\tconst auto confirmed = crl::guard(button, [=] {\n\t\t\t\tcSetConfigScale(scale);\n\t\t\t\tLocal::writeSettings();\n\t\t\t\tCore::Restart();\n\t\t\t});\n\t\t\tconst auto cancelled = crl::guard(button, [=](Fn<void()> close) {\n\t\t\t\tbase::call_delayed(\n\t\t\t\t\tst::defaultSettingsSlider.duration,\n\t\t\t\t\tbutton,\n\t\t\t\t\t[=] { repeatSetScale(cConfigScale(), repeatSetScale); });\n\t\t\t\tclose();\n\t\t\t});\n\t\t\twindow->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_settings_need_restart(),\n\t\t\t\t.confirmed = confirmed,\n\t\t\t\t.cancelled = cancelled,\n\t\t\t\t.confirmText = tr::lng_settings_restart_now(),\n\t\t\t}));\n\t\t} else if (scale != cConfigScale()) {\n\t\t\tcSetConfigScale(scale);\n\t\t\tLocal::writeSettings();\n\t\t}\n\t};\n\n\tconst auto shown = container->lifetime().make_state<bool>();\n\tconst auto togglePreview = SetupScalePreview(window, slider);\n\tconst auto toggleForScale = [=](int scale) {\n\t\tscale = cEvalScale(scale);\n\t\tconst auto show = *shown\n\t\t\t? ScalePreviewShow::Update\n\t\t\t: ScalePreviewShow::Show;\n\t\t*shown = true;\n\t\tfor (auto i = 0; i != valuesCount; ++i) {\n\t\t\tif (values[i] <= scale\n\t\t\t\t&& (i + 1 == valuesCount || values[i + 1] > scale)) {\n\t\t\t\tconst auto x = (slider->width() * i) / (valuesCount - 1);\n\t\t\t\ttogglePreview(show, scale, x);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\ttogglePreview(show, scale, slider->width() / 2);\n\t};\n\tconst auto toggleHidePreview = [=] {\n\t\ttogglePreview(ScalePreviewShow::Hide, 0, 0);\n\t\t*shown = false;\n\t};\n\n\tslider->setPseudoDiscrete(\n\t\tvaluesCount,\n\t\t[=](int index) { return values[index]; },\n\t\tcConfigScale(),\n\t\t[=](int scale) { updateLabel(scale); toggleForScale(scale); },\n\t\t[=](int scale) { toggleHidePreview(); setScale(scale, setScale); });\n\n\tbutton->toggledValue(\n\t) | rpl::map([](bool checked) {\n\t\treturn checked ? style::kScaleAuto : cEvalScale(cConfigScale());\n\t}) | rpl::on_next([=](int scale) {\n\t\tsetScale(scale, setScale);\n\t}, button->lifetime());\n\n\tif (!icon) {\n\t\tUi::AddSkip(container, st::settingsThumbSkip);\n\t}\n}\n\nType MainId() {\n\treturn Main::Id();\n}\n\nvoid OpenFaq(base::weak_ptr<Window::SessionController> weak) {\n\tUrlClickHandler::Open(\n\t\ttr::lng_settings_faq_link(tr::now),\n\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t.sessionWindow = weak,\n\t\t}));\n}\n\nvoid OpenAskQuestionConfirm(not_null<Window::SessionController*> window) {\n\tconst auto requestId = std::make_shared<mtpRequestId>();\n\tconst auto sure = [=](Fn<void()> close) {\n\t\tif (*requestId) {\n\t\t\treturn;\n\t\t}\n\t\t*requestId = window->session().api().request(\n\t\t\tMTPhelp_GetSupport()\n\t\t).done(crl::guard(window, [=](const MTPhelp_Support &result) {\n\t\t\t*requestId = 0;\n\t\t\tresult.match([&](const MTPDhelp_support &data) {\n\t\t\t\tauto &owner = window->session().data();\n\t\t\t\tif (const auto user = owner.processUser(data.vuser())) {\n\t\t\t\t\twindow->showPeerHistory(user);\n\t\t\t\t}\n\t\t\t});\n\t\t\tclose();\n\t\t})).fail([=] {\n\t\t\t*requestId = 0;\n\t\t\tclose();\n\t\t}).send();\n\t};\n\twindow->show(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_settings_ask_sure(),\n\t\t.confirmed = sure,\n\t\t.cancelled = [=](Fn<void()> close) {\n\t\t\tOpenFaq(window);\n\t\t\tclose();\n\t\t},\n\t\t.confirmText = tr::lng_settings_ask_ok(),\n\t\t.cancelText = tr::lng_settings_faq_button(),\n\t\t.strictCancel = true,\n\t}));\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_main.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Window {\nclass Controller;\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Settings {\n\n[[nodiscard]] Type MainId();\n\nvoid SetupLanguageButton(\n\tnot_null<Window::Controller*> window,\n\tnot_null<Ui::VerticalLayout*> container);\nbool HasInterfaceScale();\nvoid SetupInterfaceScale(\n\tnot_null<Window::Controller*> window,\n\tnot_null<Ui::VerticalLayout*> container,\n\tbool icon = true);\n\nvoid SetupValidatePhoneNumberSuggestion(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container,\n\tFn<void(Type)> showOther);\nvoid SetupValidatePasswordSuggestion(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container,\n\tFn<void(Type)> showOther);\n\nvoid OpenFaq(base::weak_ptr<Window::SessionController> weak);\nvoid OpenAskQuestionConfirm(not_null<Window::SessionController*> window);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_notifications.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_notifications.h\"\n\n#include \"settings/settings_common_session.h\"\n\n#include \"api/api_authorizations.h\"\n#include \"api/api_reactions_notify_settings.h\"\n#include \"api/api_ringtones.h\"\n#include \"apiwrap.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"boxes/ringtones_box.h\"\n#include \"core/application.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_session.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/notify/data_peer_notify_volume.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"mainwindow.h\"\n#include \"platform/platform_notifications_manager.h\"\n#include \"platform/platform_specific.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"settings/settings_notifications_common.h\"\n#include \"settings/sections/settings_notifications_reactions.h\"\n#include \"settings/sections/settings_notifications_type.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/controls/chat_service_checkbox.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/discrete_sliders.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/notifications_manager.h\"\n#include \"window/section_widget.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_window.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QScreen>\n#include <QSvgRenderer>\n\nnamespace Settings {\n\nusing ChangeType = Window::Notifications::ChangeType;\n\nint CurrentNotificationsCount() {\n\treturn std::clamp(\n\t\tCore::App().settings().notificationsCount(),\n\t\t1,\n\t\tkMaxNotificationsCount);\n}\n\nclass NotificationsCount::SampleWidget : public QWidget {\npublic:\n\tSampleWidget(NotificationsCount *owner, const QPixmap &cache);\n\n\tvoid detach();\n\tvoid showFast();\n\tvoid hideFast();\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid startAnimation();\n\tvoid animationCallback();\n\n\tNotificationsCount *_owner;\n\tQPixmap _cache;\n\tUi::Animations::Simple _opacity;\n\tbool _hiding = false;\n\n};\n\nSplitToggle SetupSplitToggle(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> title,\n\t\tconst style::icon *icon,\n\t\tbool checked,\n\t\trpl::producer<QString> details) {\n\tconst auto button = AddButtonWithIcon(\n\t\tcontainer,\n\t\tstd::move(title),\n\t\tst::settingsNotificationType,\n\t\t{ icon });\n\n\tconst auto &st = st::settingsNotificationType;\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton.get(),\n\t\tstd::move(details),\n\t\tst::settingsNotificationTypeDetails);\n\tlabel->show();\n\tlabel->moveToLeft(\n\t\tst.padding.left(),\n\t\tst.padding.top() + st.height - label->height());\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto toggle = Ui::CreateChild<Ui::SettingsButton>(\n\t\tcontainer.get(),\n\t\tnullptr,\n\t\tst);\n\tconst auto checkView = button->lifetime().make_state<Ui::ToggleView>(\n\t\tst.toggle,\n\t\tchecked,\n\t\t[=] { toggle->update(); });\n\n\tconst auto separator = Ui::CreateChild<Ui::RpWidget>(container.get());\n\tseparator->paintRequest(\n\t) | rpl::on_next([=, bg = st.textBgOver] {\n\t\tauto p = QPainter(separator);\n\t\tp.fillRect(separator->rect(), bg);\n\t}, separator->lifetime());\n\tconst auto separatorHeight = st.height - 2 * st.toggle.border;\n\tbutton->geometryValue(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tconst auto w = st::rightsButtonToggleWidth;\n\t\ttoggle->setGeometry(\n\t\t\tr.x() + r.width() - w,\n\t\t\tr.y(),\n\t\t\tw,\n\t\t\tr.height());\n\t\tseparator->setGeometry(\n\t\t\ttoggle->x() - st::lineWidth,\n\t\t\tr.y() + (r.height() - separatorHeight) / 2,\n\t\t\tst::lineWidth,\n\t\t\tseparatorHeight);\n\t}, toggle->lifetime());\n\n\tconst auto checkWidget = Ui::CreateChild<Ui::RpWidget>(toggle);\n\tcheckWidget->resize(checkView->getSize());\n\tcheckWidget->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(checkWidget);\n\t\tcheckView->paint(p, 0, 0, checkWidget->width());\n\t}, checkWidget->lifetime());\n\ttoggle->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tcheckWidget->moveToRight(\n\t\t\tst.toggleSkip,\n\t\t\t(s.height() - checkWidget->height()) / 2);\n\t}, toggle->lifetime());\n\n\treturn { button, toggle, checkView };\n}\n\n[[nodiscard]] not_null<Ui::SettingsButton*> AddTypeButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tData::DefaultNotify type,\n\t\tFn<void(Type)> showOther) {\n\tusing Type = Data::DefaultNotify;\n\tauto label = [&] {\n\t\tswitch (type) {\n\t\tcase Type::User: return tr::lng_notification_private_chats();\n\t\tcase Type::Group: return tr::lng_notification_groups();\n\t\tcase Type::Broadcast: return tr::lng_notification_channels();\n\t\t}\n\t\tUnexpected(\"Type value in AddTypeButton.\");\n\t}();\n\tconst auto icon = [&] {\n\t\tswitch (type) {\n\t\tcase Type::User: return &st::menuIconProfile;\n\t\tcase Type::Group: return &st::menuIconGroups;\n\t\tcase Type::Broadcast: return &st::menuIconChannel;\n\t\t}\n\t\tUnexpected(\"Type value in AddTypeButton.\");\n\t}();\n\n\tconst auto session = &controller->session();\n\tconst auto settings = &session->data().notifySettings();\n\tauto status = rpl::combine(\n\t\tNotificationsEnabledForTypeValue(session, type),\n\t\trpl::single(\n\t\t\ttype\n\t\t) | rpl::then(settings->exceptionsUpdates(\n\t\t) | rpl::filter(rpl::mappers::_1 == type))\n\t) | rpl::map([=](bool enabled, const auto &) {\n\t\tconst auto count = int(settings->exceptions(type).size());\n\t\treturn !count\n\t\t\t? tr::lng_notification_click_to_change()\n\t\t\t: (enabled\n\t\t\t\t? tr::lng_notification_on\n\t\t\t\t: tr::lng_notification_off)(\n\t\t\t\t\tlt_exceptions,\n\t\t\t\t\ttr::lng_notification_exceptions(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single(float64(count))));\n\t}) | rpl::flatten_latest();\n\n\tconst auto [button, toggleButton, checkView] = SetupSplitToggle(\n\t\tcontainer,\n\t\tstd::move(label),\n\t\ticon,\n\t\tNotificationsEnabledForType(session, type),\n\t\tstd::move(status));\n\tbutton->setClickedCallback([=] {\n\t\tshowOther(NotificationsType::Id(type));\n\t});\n\n\tconst auto toggle = crl::guard(toggleButton, [=] {\n\t\tconst auto enabled = !checkView->checked();\n\t\tcheckView->setChecked(enabled, anim::type::normal);\n\t\tsettings->defaultUpdate(type, Data::MuteValue{\n\t\t\t.unmute = enabled,\n\t\t\t.forever = !enabled,\n\t\t});\n\t});\n\ttoggleButton->clicks(\n\t) | rpl::on_next([=] {\n\t\tconst auto count = int(settings->exceptions(type).size());\n\t\tif (!count) {\n\t\t\ttoggle();\n\t\t} else {\n\t\t\tcontroller->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\tconst auto phrase = [&] {\n\t\t\t\t\tswitch (type) {\n\t\t\t\t\tcase Type::User:\n\t\t\t\t\t\treturn tr::lng_notification_about_private_chats;\n\t\t\t\t\tcase Type::Group:\n\t\t\t\t\t\treturn tr::lng_notification_about_groups;\n\t\t\t\t\tcase Type::Broadcast:\n\t\t\t\t\t\treturn tr::lng_notification_about_channels;\n\t\t\t\t\t}\n\t\t\t\t\tUnexpected(\"Type in AddTypeButton.\");\n\t\t\t\t}();\n\t\t\t\tUi::ConfirmBox(box, {\n\t\t\t\t\t.text = phrase(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single(float64(count)),\n\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t.confirmed = [=](auto close) { toggle(); close(); },\n\t\t\t\t\t.confirmText = tr::lng_box_ok(),\n\t\t\t\t\t.title = tr::lng_notification_exceptions_title(),\n\t\t\t\t\t.inform = true,\n\t\t\t\t});\n\t\t\t\tbox->addLeftButton(\n\t\t\t\t\ttr::lng_notification_exceptions_view(),\n\t\t\t\t\t[=] {\n\t\t\t\t\t\tbox->closeBox();\n\t\t\t\t\t\tshowOther(NotificationsType::Id(type));\n\t\t\t\t\t});\n\t\t\t}));\n\t\t}\n\t}, toggleButton->lifetime());\n\treturn button;\n}\n\n[[nodiscard]] not_null<Ui::SettingsButton*> AddReactionsButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFn<void(Type)> showOther) {\n\tconst auto session = &controller->session();\n\tauto &rs = session->api().reactionsNotifySettings();\n\trs.reload();\n\n\tusing From = Api::ReactionsNotifyFrom;\n\tauto status = rpl::combine(\n\t\trs.messagesFrom(),\n\t\trs.pollVotesFrom()\n\t) | rpl::map([](From messages, From pollVotes) {\n\t\tauto parts = QStringList();\n\t\tif (messages != From::None) {\n\t\t\tparts.push_back(\n\t\t\t\ttr::lng_notification_reactions_messages(tr::now));\n\t\t}\n\t\tif (pollVotes != From::None) {\n\t\t\tparts.push_back(\n\t\t\t\ttr::lng_notification_reactions_poll_votes(tr::now));\n\t\t}\n\t\treturn parts.isEmpty()\n\t\t\t? tr::lng_notification_click_to_change(tr::now)\n\t\t\t: parts.join(u\", \"_q);\n\t});\n\n\tconst auto [button, toggleButton, checkView] = SetupSplitToggle(\n\t\tcontainer,\n\t\ttr::lng_notification_reactions(),\n\t\t&st::menuIconGroupReactions,\n\t\trs.enabledCurrent(),\n\t\tstd::move(status));\n\tbutton->setClickedCallback([=] {\n\t\tshowOther(NotificationsReactions::Id());\n\t});\n\n\trs.enabled(\n\t) | rpl::on_next([=](bool enabled) {\n\t\tcheckView->setChecked(enabled, anim::type::normal);\n\t}, button->lifetime());\n\n\ttoggleButton->clicks(\n\t) | rpl::on_next([=] {\n\t\tconst auto enabled = !checkView->checked();\n\t\tconst auto from = enabled ? From::All : From::None;\n\t\tsession->api().reactionsNotifySettings().setAllFrom(from);\n\t}, toggleButton->lifetime());\n\treturn button;\n}\n\nNotificationsCount::NotificationsCount(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: _controller(controller)\n, _chosenCorner(Core::App().settings().notificationsCorner())\n, _oldCount(CurrentNotificationsCount()) {\n\tsetMouseTracking(true);\n\n\t_sampleOpacities.resize(kMaxNotificationsCount);\n\n\tprepareNotificationSampleSmall();\n\tprepareNotificationSampleLarge();\n}\n\nvoid NotificationsCount::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tauto contentLeft = getContentLeft();\n\n\tauto screenRect = getScreenRect();\n\tp.fillRect(\n\t\tscreenRect.x(),\n\t\tscreenRect.y(),\n\t\tst::notificationsBoxScreenSize.width(),\n\t\tst::notificationsBoxScreenSize.height(),\n\t\tst::notificationsBoxScreenBg);\n\n\tauto monitorTop = 0;\n\tst::notificationsBoxMonitor.paint(p, contentLeft, monitorTop, width());\n\n\tfor (int corner = 0; corner != 4; ++corner) {\n\t\tauto screenCorner = static_cast<ScreenCorner>(corner);\n\t\tauto isLeft = Core::Settings::IsLeftCorner(screenCorner);\n\t\tauto isTop = Core::Settings::IsTopCorner(screenCorner);\n\t\tauto sampleLeft = isLeft ? (screenRect.x() + st::notificationsSampleSkip) : (screenRect.x() + screenRect.width() - st::notificationsSampleSkip - st::notificationSampleSize.width());\n\t\tauto sampleTop = isTop ? (screenRect.y() + st::notificationsSampleTopSkip) : (screenRect.y() + screenRect.height() - st::notificationsSampleBottomSkip - st::notificationSampleSize.height());\n\t\tif (corner == static_cast<int>(_chosenCorner)) {\n\t\t\tauto count = _oldCount;\n\t\t\tfor (int i = 0; i != kMaxNotificationsCount; ++i) {\n\t\t\t\tauto opacity = _sampleOpacities[i].value((i < count) ? 1. : 0.);\n\t\t\t\tp.setOpacity(opacity);\n\t\t\t\tp.drawPixmapLeft(sampleLeft, sampleTop, width(), _notificationSampleSmall);\n\t\t\t\tsampleTop += (isTop ? 1 : -1) * (st::notificationSampleSize.height() + st::notificationsSampleMargin);\n\t\t\t}\n\t\t\tp.setOpacity(1.);\n\t\t} else {\n\t\t\tp.setOpacity(st::notificationSampleOpacity);\n\t\t\tp.drawPixmapLeft(sampleLeft, sampleTop, width(), _notificationSampleSmall);\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t}\n}\n\nvoid NotificationsCount::setCount(int count) {\n\tauto moreSamples = (count > _oldCount);\n\tauto from = moreSamples ? 0. : 1.;\n\tauto to = moreSamples ? 1. : 0.;\n\tauto indexDelta = moreSamples ? 1 : -1;\n\tauto animatedDelta = moreSamples ? 0 : -1;\n\tfor (; _oldCount != count; _oldCount += indexDelta) {\n\t\t_sampleOpacities[_oldCount + animatedDelta].start([this] { update(); }, from, to, st::notifyFastAnim);\n\t}\n\n\tif (count != Core::App().settings().notificationsCount()) {\n\t\tCore::App().settings().setNotificationsCount(count);\n\t\tCore::App().saveSettingsDelayed();\n\t\tCore::App().notifications().notifySettingsChanged(\n\t\t\tChangeType::MaxCount);\n\t}\n}\n\nint NotificationsCount::getContentLeft() const {\n\treturn (width() - st::notificationsBoxMonitor.width()) / 2;\n}\n\nQRect NotificationsCount::getScreenRect() const {\n\treturn getScreenRect(width());\n}\n\nQRect NotificationsCount::getScreenRect(int width) const {\n\tauto screenLeft = (width - st::notificationsBoxScreenSize.width()) / 2;\n\tauto screenTop = st::notificationsBoxScreenTop;\n\treturn QRect(screenLeft, screenTop, st::notificationsBoxScreenSize.width(), st::notificationsBoxScreenSize.height());\n}\n\nint NotificationsCount::resizeGetHeight(int newWidth) {\n\tupdate();\n\treturn st::notificationsBoxMonitor.height();\n}\n\nvoid NotificationsCount::prepareNotificationSampleSmall() {\n\tauto width = st::notificationSampleSize.width();\n\tauto height = st::notificationSampleSize.height();\n\tauto sampleImage = QImage(\n\t\tQSize(width, height) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tsampleImage.setDevicePixelRatio(style::DevicePixelRatio());\n\tsampleImage.fill(st::notificationBg->c);\n\t{\n\t\tPainter p(&sampleImage);\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tp.setPen(Qt::NoPen);\n\n\t\tauto padding = height / 8;\n\t\tauto userpicSize = height - 2 * padding;\n\t\tp.setBrush(st::notificationSampleUserpicFg);\n\t\tp.drawEllipse(style::rtlrect(padding, padding, userpicSize, userpicSize, width));\n\n\t\tauto rowLeft = height;\n\t\tauto rowHeight = padding;\n\t\tauto nameTop = (height - 5 * padding) / 2;\n\t\tauto nameWidth = height;\n\t\tp.setBrush(st::notificationSampleNameFg);\n\t\tp.drawRoundedRect(style::rtlrect(rowLeft, nameTop, nameWidth, rowHeight, width), rowHeight / 2, rowHeight / 2);\n\n\t\tauto rowWidth = (width - rowLeft - 3 * padding);\n\t\tauto rowTop = nameTop + rowHeight + padding;\n\t\tp.setBrush(st::notificationSampleTextFg);\n\t\tp.drawRoundedRect(style::rtlrect(rowLeft, rowTop, rowWidth, rowHeight, width), rowHeight / 2, rowHeight / 2);\n\t\trowTop += rowHeight + padding;\n\t\tp.drawRoundedRect(style::rtlrect(rowLeft, rowTop, rowWidth, rowHeight, width), rowHeight / 2, rowHeight / 2);\n\n\t\tauto closeLeft = width - 2 * padding;\n\t\tp.fillRect(style::rtlrect(closeLeft, padding, padding, padding, width), st::notificationSampleCloseFg);\n\t}\n\t_notificationSampleSmall = Ui::PixmapFromImage(std::move(sampleImage));\n\t_notificationSampleSmall.setDevicePixelRatio(style::DevicePixelRatio());\n}\n\nvoid NotificationsCount::prepareNotificationSampleUserpic() {\n\tif (_notificationSampleUserpic.isNull()) {\n\t\t_notificationSampleUserpic = Ui::PixmapFromImage(\n\t\t\tWindow::LogoNoMargin().scaled(\n\t\t\t\tst::notifyPhotoSize * style::DevicePixelRatio(),\n\t\t\t\tst::notifyPhotoSize * style::DevicePixelRatio(),\n\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\tQt::SmoothTransformation));\n\t\t_notificationSampleUserpic.setDevicePixelRatio(\n\t\t\tstyle::DevicePixelRatio());\n\t}\n}\n\nvoid NotificationsCount::prepareNotificationSampleLarge() {\n\tint w = st::notifyWidth, h = st::notifyMinHeight;\n\tauto sampleImage = QImage(\n\t\tw * style::DevicePixelRatio(),\n\t\th * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tsampleImage.setDevicePixelRatio(style::DevicePixelRatio());\n\tsampleImage.fill(st::notificationBg->c);\n\t{\n\t\tPainter p(&sampleImage);\n\t\tp.fillRect(0, 0, w - st::notifyBorderWidth, st::notifyBorderWidth, st::notifyBorder->b);\n\t\tp.fillRect(w - st::notifyBorderWidth, 0, st::notifyBorderWidth, h - st::notifyBorderWidth, st::notifyBorder->b);\n\t\tp.fillRect(st::notifyBorderWidth, h - st::notifyBorderWidth, w - st::notifyBorderWidth, st::notifyBorderWidth, st::notifyBorder->b);\n\t\tp.fillRect(0, st::notifyBorderWidth, st::notifyBorderWidth, h - st::notifyBorderWidth, st::notifyBorder->b);\n\n\t\tprepareNotificationSampleUserpic();\n\t\tp.drawPixmap(st::notifyPhotoPos.x(), st::notifyPhotoPos.y(), _notificationSampleUserpic);\n\n\t\tint itemWidth = w - st::notifyPhotoPos.x() - st::notifyPhotoSize - st::notifyTextLeft - st::notifyClosePos.x() - st::notifyClose.width;\n\n\t\tauto rectForName = style::rtlrect(st::notifyPhotoPos.x() + st::notifyPhotoSize + st::notifyTextLeft, st::notifyTextTop, itemWidth, st::msgNameFont->height, w);\n\n\t\tauto notifyText = st::dialogsTextFont->elided(tr::lng_notification_sample(tr::now), itemWidth);\n\t\tp.setFont(st::dialogsTextFont);\n\t\tp.setPen(st::dialogsTextFgService);\n\t\tp.drawText(st::notifyPhotoPos.x() + st::notifyPhotoSize + st::notifyTextLeft, st::notifyItemTop + st::msgNameFont->height + st::dialogsTextFont->ascent, notifyText);\n\n\t\tp.setPen(st::dialogsNameFg);\n\t\tp.setFont(st::msgNameFont);\n\n\t\tauto notifyTitle = st::msgNameFont->elided(AppNameF.utf8(), rectForName.width());\n\t\tp.drawText(rectForName.left(), rectForName.top() + st::msgNameFont->ascent, notifyTitle);\n\n\t\tst::notifyClose.icon.paint(p, w - st::notifyClosePos.x() - st::notifyClose.width + st::notifyClose.iconPosition.x(), st::notifyClosePos.y() + st::notifyClose.iconPosition.y(), w);\n\t}\n\n\t_notificationSampleLarge = Ui::PixmapFromImage(std::move(sampleImage));\n}\n\nvoid NotificationsCount::removeSample(SampleWidget *widget) {\n\tfor (auto &samples : _cornerSamples) {\n\t\tfor (int i = 0, size = samples.size(); i != size; ++i) {\n\t\t\tif (samples[i] == widget) {\n\t\t\t\tfor (int j = i + 1; j != size; ++j) {\n\t\t\t\t\tsamples[j]->detach();\n\t\t\t\t}\n\t\t\t\tsamples.resize(i);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid NotificationsCount::mouseMoveEvent(QMouseEvent *e) {\n\tauto screenRect = getScreenRect();\n\tauto cornerWidth = screenRect.width() / 3;\n\tauto cornerHeight = screenRect.height() / 3;\n\tauto topLeft = style::rtlrect(screenRect.x(), screenRect.y(), cornerWidth, cornerHeight, width());\n\tauto topRight = style::rtlrect(screenRect.x() + screenRect.width() - cornerWidth, screenRect.y(), cornerWidth, cornerHeight, width());\n\tauto bottomRight = style::rtlrect(screenRect.x() + screenRect.width() - cornerWidth, screenRect.y() + screenRect.height() - cornerHeight, cornerWidth, cornerHeight, width());\n\tauto bottomLeft = style::rtlrect(screenRect.x(), screenRect.y() + screenRect.height() - cornerHeight, cornerWidth, cornerHeight, width());\n\tif (topLeft.contains(e->pos())) {\n\t\tsetOverCorner(ScreenCorner::TopLeft);\n\t} else if (topRight.contains(e->pos())) {\n\t\tsetOverCorner(ScreenCorner::TopRight);\n\t} else if (bottomRight.contains(e->pos())) {\n\t\tsetOverCorner(ScreenCorner::BottomRight);\n\t} else if (bottomLeft.contains(e->pos())) {\n\t\tsetOverCorner(ScreenCorner::BottomLeft);\n\t} else {\n\t\tclearOverCorner();\n\t}\n}\n\nvoid NotificationsCount::leaveEventHook(QEvent *e) {\n\tclearOverCorner();\n}\n\nvoid NotificationsCount::setOverCorner(ScreenCorner corner) {\n\tif (_isOverCorner) {\n\t\tif (corner == _overCorner) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto index = static_cast<int>(_overCorner);\n\t\tfor (const auto widget : _cornerSamples[index]) {\n\t\t\twidget->hideFast();\n\t\t}\n\t} else {\n\t\t_isOverCorner = true;\n\t\tsetCursor(style::cur_pointer);\n\t\tCore::App().notifications().notifySettingsChanged(\n\t\t\tChangeType::DemoIsShown);\n\t}\n\t_overCorner = corner;\n\n\tauto &samples = _cornerSamples[static_cast<int>(_overCorner)];\n\tauto samplesAlready = int(samples.size());\n\tauto samplesNeeded = _oldCount;\n\tauto samplesLeave = qMin(samplesAlready, samplesNeeded);\n\tfor (int i = 0; i != samplesLeave; ++i) {\n\t\tsamples[i]->showFast();\n\t}\n\tif (samplesNeeded > samplesLeave) {\n\t\tconst auto r = Window::Notifications::NotificationDisplayRect(\n\t\t\t&_controller->window());\n\t\tauto isLeft = Core::Settings::IsLeftCorner(_overCorner);\n\t\tauto isTop = Core::Settings::IsTopCorner(_overCorner);\n\t\tauto sampleLeft = (isLeft == rtl()) ? (r.x() + r.width() - st::notifyWidth - st::notifyDeltaX) : (r.x() + st::notifyDeltaX);\n\t\tauto sampleTop = isTop ? (r.y() + st::notifyDeltaY) : (r.y() + r.height() - st::notifyDeltaY - st::notifyMinHeight);\n\t\tfor (int i = samplesLeave; i != samplesNeeded; ++i) {\n\t\t\tauto widget = std::make_unique<SampleWidget>(this, _notificationSampleLarge);\n\t\t\twidget->move(sampleLeft, sampleTop + (isTop ? 1 : -1) * i * (st::notifyMinHeight + st::notifyDeltaY));\n\t\t\twidget->showFast();\n\t\t\tsamples.push_back(widget.release());\n\t\t}\n\t} else {\n\t\tfor (int i = samplesLeave; i != samplesAlready; ++i) {\n\t\t\tsamples[i]->hideFast();\n\t\t}\n\t}\n}\n\nvoid NotificationsCount::clearOverCorner() {\n\tif (_isOverCorner) {\n\t\t_isOverCorner = false;\n\t\tsetCursor(style::cur_default);\n\t\tCore::App().notifications().notifySettingsChanged(\n\t\t\tChangeType::DemoIsHidden);\n\n\t\tfor (const auto &samples : _cornerSamples) {\n\t\t\tfor (const auto widget : samples) {\n\t\t\t\twidget->hideFast();\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid NotificationsCount::mousePressEvent(QMouseEvent *e) {\n\t_isDownCorner = _isOverCorner;\n\t_downCorner = _overCorner;\n}\n\nvoid NotificationsCount::mouseReleaseEvent(QMouseEvent *e) {\n\tauto isDownCorner = base::take(_isDownCorner);\n\tif (isDownCorner\n\t\t&& _isOverCorner\n\t\t&& _downCorner == _overCorner\n\t\t&& _downCorner != _chosenCorner) {\n\t\t_chosenCorner = _downCorner;\n\t\tupdate();\n\n\t\tif (_chosenCorner != Core::App().settings().notificationsCorner()) {\n\t\t\tCore::App().settings().setNotificationsCorner(_chosenCorner);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\tCore::App().notifications().notifySettingsChanged(\n\t\t\t\tChangeType::Corner);\n\t\t}\n\t}\n}\n\nNotificationsCount::~NotificationsCount() {\n\tfor (const auto &samples : _cornerSamples) {\n\t\tfor (const auto widget : samples) {\n\t\t\twidget->detach();\n\t\t}\n\t}\n\tclearOverCorner();\n}\n\nNotificationsCount::SampleWidget::SampleWidget(\n\tNotificationsCount *owner,\n\tconst QPixmap &cache)\n: _owner(owner)\n, _cache(cache) {\n\tsetFixedSize(\n\t\tcache.width() / cache.devicePixelRatio(),\n\t\tcache.height() / cache.devicePixelRatio());\n\n\tsetWindowFlags(Qt::WindowFlags(Qt::FramelessWindowHint)\n\t\t| Qt::WindowStaysOnTopHint\n\t\t| Qt::BypassWindowManagerHint\n\t\t| Qt::NoDropShadowWindowHint\n\t\t| Qt::Tool);\n\tsetAttribute(Qt::WA_MacAlwaysShowToolWindow);\n\tsetAttribute(Qt::WA_TransparentForMouseEvents);\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\n\tsetWindowOpacity(0.);\n\tshow();\n}\n\nvoid NotificationsCount::SampleWidget::detach() {\n\t_owner = nullptr;\n\thideFast();\n}\n\nvoid NotificationsCount::SampleWidget::showFast() {\n\t_hiding = false;\n\tstartAnimation();\n}\n\nvoid NotificationsCount::SampleWidget::hideFast() {\n\t_hiding = true;\n\tstartAnimation();\n}\n\nvoid NotificationsCount::SampleWidget::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\tp.drawPixmap(0, 0, _cache);\n}\n\nvoid NotificationsCount::SampleWidget::startAnimation() {\n\t_opacity.start(\n\t\t[=] { animationCallback(); },\n\t\t_hiding ? 1. : 0.,\n\t\t_hiding ? 0. : 1.,\n\t\tst::notifyFastAnim);\n}\n\nvoid NotificationsCount::SampleWidget::animationCallback() {\n\tsetWindowOpacity(_opacity.value(_hiding ? 0. : 1.));\n\tif (!_opacity.animating() && _hiding) {\n\t\tif (_owner) {\n\t\t\t_owner->removeSample(this);\n\t\t}\n\t\thide();\n\t\tdeleteLater();\n\t}\n}\n\nclass NotifyPreview final {\npublic:\n\tNotifyPreview(bool nameShown, bool previewShown);\n\n\tvoid setNameShown(bool shown);\n\tvoid setPreviewShown(bool shown);\n\n\tint resizeGetHeight(int newWidth);\n\tvoid paint(Painter &p, int x, int y);\n\nprivate:\n\tint _width = 0;\n\tint _height = 0;\n\tbool _nameShown = false;\n\tbool _previewShown = false;\n\tUi::RoundRect _roundRect;\n\tUi::Text::String _name, _title;\n\tUi::Text::String _text, _preview;\n\tQSvgRenderer _userpic;\n\tQImage _logo;\n\n};\n\nNotifyPreview::NotifyPreview(bool nameShown, bool previewShown)\n: _nameShown(nameShown)\n, _previewShown(previewShown)\n, _roundRect(st::boxRadius, st::msgInBg)\n, _userpic(u\":/gui/icons/settings/dino.svg\"_q)\n, _logo(Window::LogoNoMargin()) {\n\tconst auto ratio = style::DevicePixelRatio();\n\t_logo = _logo.scaledToWidth(\n\t\tst::notifyPreviewUserpicSize * ratio,\n\t\tQt::SmoothTransformation);\n\t_logo.setDevicePixelRatio(ratio);\n\n\t_name.setText(\n\t\tst::defaultSubsectionTitle.style,\n\t\ttr::lng_notification_preview_title(tr::now));\n\t_title.setText(st::defaultSubsectionTitle.style, AppNameF.utf16());\n\n\t_text.setText(\n\t\tst::boxTextStyle,\n\t\ttr::lng_notification_preview_text(tr::now));\n\t_preview.setText(\n\t\tst::boxTextStyle,\n\t\ttr::lng_notification_preview(tr::now));\n}\n\nvoid NotifyPreview::setNameShown(bool shown) {\n\t_nameShown = shown;\n}\n\nvoid NotifyPreview::setPreviewShown(bool shown) {\n\t_previewShown = shown;\n}\n\nint NotifyPreview::resizeGetHeight(int newWidth) {\n\t_width = newWidth;\n\t_height = st::notifyPreviewUserpicPosition.y()\n\t\t+ st::notifyPreviewUserpicSize\n\t\t+ st::notifyPreviewUserpicPosition.y();\n\tconst auto available = _width\n\t\t- st::notifyPreviewTextPosition.x()\n\t\t- st::notifyPreviewUserpicPosition.x();\n\tif (std::max(_text.maxWidth(), _preview.maxWidth()) >= available) {\n\t\t_height += st::defaultTextStyle.font->height;\n\t}\n\treturn _height;\n}\n\nvoid NotifyPreview::paint(Painter &p, int x, int y) {\n\tif (!_width || !_height) {\n\t\treturn;\n\t}\n\tp.translate(x, y);\n\tconst auto guard = gsl::finally([&] { p.translate(-x, -y); });\n\n\t_roundRect.paint(p, { 0, 0, _width, _height });\n\tconst auto userpic = QRect(\n\t\tst::notifyPreviewUserpicPosition,\n\t\tQSize{ st::notifyPreviewUserpicSize, st::notifyPreviewUserpicSize });\n\n\tif (_nameShown) {\n\t\t_userpic.render(&p, QRectF(userpic));\n\t} else {\n\t\tp.drawImage(userpic.topLeft(), _logo);\n\t}\n\n\tp.setPen(st::historyTextInFg);\n\n\tconst auto &title = _nameShown ? _name : _title;\n\ttitle.drawElided(\n\t\tp,\n\t\tst::notifyPreviewTitlePosition.x(),\n\t\tst::notifyPreviewTitlePosition.y(),\n\t\t_width - st::notifyPreviewTitlePosition.x() - userpic.x());\n\n\tconst auto &text = _previewShown ? _text : _preview;\n\ttext.drawElided(\n\t\tp,\n\t\tst::notifyPreviewTextPosition.x(),\n\t\tst::notifyPreviewTextPosition.y(),\n\t\t_width - st::notifyPreviewTextPosition.x() - userpic.x(),\n\t\t2);\n}\n\nNotifyViewCheckboxes SetupNotifyViewOptions(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tbool nameShown,\n\t\tbool previewShown) {\n\tusing namespace rpl::mappers;\n\n\tauto wrap = container->add(object_ptr<Ui::SlideWrap<>>(\n\t\tcontainer,\n\t\tobject_ptr<Ui::RpWidget>(container)));\n\tconst auto widget = wrap->entity();\n\n\tconst auto makeCheckbox = [&](const QString &text, bool checked) {\n\t\treturn Ui::MakeChatServiceCheckbox(\n\t\t\twidget,\n\t\t\ttext,\n\t\t\tst::backgroundCheckbox,\n\t\t\tst::backgroundCheck,\n\t\t\tchecked).release();\n\t};\n\tconst auto name = makeCheckbox(\n\t\ttr::lng_notification_show_name(tr::now),\n\t\tnameShown);\n\tconst auto preview = makeCheckbox(\n\t\ttr::lng_notification_show_text(tr::now),\n\t\tpreviewShown);\n\n\tconst auto view = widget->lifetime().make_state<NotifyPreview>(\n\t\tnameShown,\n\t\tpreviewShown);\n\tusing ThemePtr = std::unique_ptr<Ui::ChatTheme>;\n\tconst auto theme = widget->lifetime().make_state<ThemePtr>(\n\t\tWindow::Theme::DefaultChatThemeOn(widget->lifetime()));\n\twidget->widthValue(\n\t) | rpl::filter(\n\t\t_1 >= (st::historyMinimalWidth / 2)\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto margins = st::notifyPreviewMargins;\n\t\tconst auto bubblew = width - margins.left() - margins.right();\n\t\tconst auto bubbleh = view->resizeGetHeight(bubblew);\n\t\tconst auto height = bubbleh + margins.top() + margins.bottom();\n\t\twidget->resize(width, height);\n\n\t\tconst auto skip = st::notifyPreviewChecksSkip;\n\t\tconst auto checksWidth = name->width() + skip + preview->width();\n\t\tconst auto checksLeft = (width - checksWidth) / 2;\n\t\tconst auto checksTop = height\n\t\t\t- (margins.bottom() + name->height()) / 2;\n\t\tname->move(checksLeft, checksTop);\n\t\tpreview->move(checksLeft + name->width() + skip, checksTop);\n\t}, widget->lifetime());\n\n\twidget->paintRequest(\n\t) | rpl::on_next([=](QRect rect) {\n\t\tPainter p(widget);\n\t\tp.setClipRect(rect);\n\t\tWindow::SectionWidget::PaintBackground(\n\t\t\tp,\n\t\t\ttheme->get(),\n\t\t\tQSize(widget->width(), widget->window()->height()),\n\t\t\trect);\n\n\t\tview->paint(\n\t\t\tp,\n\t\t\tst::notifyPreviewMargins.left(),\n\t\t\tst::notifyPreviewMargins.top());\n\t}, widget->lifetime());\n\n\tname->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tview->setNameShown(checked);\n\t\twidget->update();\n\t}, name->lifetime());\n\n\tpreview->checkedChanges(\n\t) | rpl::on_next([=](bool checked) {\n\t\tview->setPreviewShown(checked);\n\t\twidget->update();\n\t}, preview->lifetime());\n\n\treturn {\n\t\t.wrap = wrap,\n\t\t.name = name,\n\t\t.preview = preview,\n\t};\n}\n\nnamespace {\n\nusing namespace Builder;\n\nconstexpr auto kDefaultDisplayIndex = -1;\n\nusing NotifyView = Core::Settings::NotifyView;\n\nvoid BuildMultiAccountSection(SectionBuilder &builder) {\n\tif (Core::App().domain().accounts().size() < 2) {\n\t\treturn;\n\t}\n\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"notifications/multi-account\"_q,\n\t\t.title = tr::lng_settings_show_from(),\n\t\t.keywords = { u\"accounts\"_q, u\"multiple\"_q },\n\t});\n\n\tconst auto fromAll = builder.addButton({\n\t\t.id = u\"notifications/accounts\"_q,\n\t\t.title = tr::lng_settings_notify_all(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.toggled = rpl::single(Core::App().settings().notifyFromAll()),\n\t\t.keywords = { u\"all accounts\"_q, u\"multiple\"_q },\n\t});\n\n\tif (fromAll) {\n\t\tfromAll->toggledChanges(\n\t\t) | rpl::filter([](bool checked) {\n\t\t\treturn (checked != Core::App().settings().notifyFromAll());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tCore::App().settings().setNotifyFromAll(checked);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\tif (!checked) {\n\t\t\t\tauto &notifications = Core::App().notifications();\n\t\t\t\tconst auto &list = Core::App().domain().accounts();\n\t\t\t\tfor (const auto &[index, account] : list) {\n\t\t\t\t\tif (account.get() == &Core::App().domain().active()) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t} else if (const auto session = account->maybeSession()) {\n\t\t\t\t\t\tnotifications.clearFromSession(session);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}, fromAll->lifetime());\n\t}\n\n\tbuilder.addSkip();\n\tbuilder.addDividerText(tr::lng_settings_notify_all_about());\n\tbuilder.addSkip();\n}\n\nvoid BuildGlobalNotificationsSection(SectionBuilder &builder) {\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"notifications/global\"_q,\n\t\t.title = tr::lng_settings_notify_global(),\n\t\t.keywords = { u\"global\"_q, u\"desktop\"_q, u\"sound\"_q },\n\t});\n\n\tconst auto container = builder.container();\n\tconst auto session = builder.session();\n\tconst auto &settings = Core::App().settings();\n\n\tconst auto desktopToggles = container\n\t\t? container->lifetime().make_state<rpl::event_stream<bool>>()\n\t\t: nullptr;\n\tconst auto desktop = builder.addButton({\n\t\t.id = u\"notifications/desktop\"_q,\n\t\t.title = tr::lng_settings_desktop_notify(),\n\t\t.icon = { &st::menuIconNotifications },\n\t\t.toggled = desktopToggles\n\t\t\t? desktopToggles->events_starting_with(settings.desktopNotify())\n\t\t\t: rpl::single(settings.desktopNotify()) | rpl::type_erased,\n\t\t.keywords = { u\"desktop\"_q, u\"popup\"_q, u\"show\"_q },\n\t});\n\n\tconst auto flashbounceToggles = container\n\t\t? container->lifetime().make_state<rpl::event_stream<bool>>()\n\t\t: nullptr;\n\tconst auto flashbounce = builder.addButton({\n\t\t.id = u\"notifications/flash\"_q,\n\t\t.title = (Platform::IsWindows()\n\t\t\t? tr::lng_settings_alert_windows\n\t\t\t: Platform::IsMac()\n\t\t\t? tr::lng_settings_alert_mac\n\t\t\t: tr::lng_settings_alert_linux)(),\n\t\t.icon = { &st::menuIconDockBounce },\n\t\t.toggled = flashbounceToggles\n\t\t\t? flashbounceToggles->events_starting_with(settings.flashBounceNotify())\n\t\t\t: rpl::single(settings.flashBounceNotify()) | rpl::type_erased,\n\t\t.keywords = { u\"flash\"_q, u\"bounce\"_q, u\"taskbar\"_q },\n\t});\n\n\tconst auto soundAllowed = container\n\t\t? container->lifetime().make_state<rpl::event_stream<bool>>()\n\t\t: nullptr;\n\tconst auto allowed = [=] {\n\t\treturn Core::App().settings().soundNotify();\n\t};\n\tconst auto sound = builder.addButton({\n\t\t.id = u\"notifications/sound\"_q,\n\t\t.title = tr::lng_settings_sound_allowed(),\n\t\t.icon = { &st::menuIconUnmute },\n\t\t.toggled = soundAllowed\n\t\t\t? soundAllowed->events_starting_with(allowed())\n\t\t\t: rpl::single(allowed()) | rpl::type_erased,\n\t\t.keywords = { u\"sound\"_q, u\"audio\"_q, u\"mute\"_q },\n\t});\n\n\tbuilder.add([session](const WidgetContext &ctx) {\n\t\tUi::AddRingtonesVolumeSlider(\n\t\t\tctx.container,\n\t\t\trpl::single(true),\n\t\t\ttr::lng_settings_master_volume_notifications(),\n\t\t\tData::VolumeController{\n\t\t\t\t.volume = []() -> ushort {\n\t\t\t\t\tconst auto volume\n\t\t\t\t\t\t= Core::App().settings().notificationsVolume();\n\t\t\t\t\treturn volume ? volume : 100;\n\t\t\t\t},\n\t\t\t\t.saveVolume = [=](ushort volume) {\n\t\t\t\t\tCore::App().notifications().playSound(\n\t\t\t\t\t\tsession,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tvolume / 100.);\n\t\t\t\t\tCore::App().settings().setNotificationsVolume(volume);\n\t\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t\t}});\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n\n\tbuilder.addSkip();\n\n\tif (desktop) {\n\t\tconst auto changed = [=](ChangeType change) {\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\tCore::App().notifications().notifySettingsChanged(change);\n\t\t};\n\n\t\tdesktop->toggledChanges(\n\t\t) | rpl::filter([](bool checked) {\n\t\t\treturn (checked != Core::App().settings().desktopNotify());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tCore::App().settings().setDesktopNotify(checked);\n\t\t\tchanged(ChangeType::DesktopEnabled);\n\t\t}, desktop->lifetime());\n\n\t\tif (sound) {\n\t\t\tsound->toggledChanges(\n\t\t\t) | rpl::filter([](bool checked) {\n\t\t\t\treturn (checked != Core::App().settings().soundNotify());\n\t\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\t\tCore::App().settings().setSoundNotify(checked);\n\t\t\t\tchanged(ChangeType::SoundEnabled);\n\t\t\t}, sound->lifetime());\n\t\t}\n\n\t\tif (flashbounce) {\n\t\t\tflashbounce->toggledChanges(\n\t\t\t) | rpl::filter([](bool checked) {\n\t\t\t\treturn (checked != Core::App().settings().flashBounceNotify());\n\t\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\t\tCore::App().settings().setFlashBounceNotify(checked);\n\t\t\t\tchanged(ChangeType::FlashBounceEnabled);\n\t\t\t}, flashbounce->lifetime());\n\t\t}\n\n\t\tCore::App().notifications().settingsChanged(\n\t\t) | rpl::on_next([=](ChangeType change) {\n\t\t\tif (change == ChangeType::DesktopEnabled) {\n\t\t\t\tdesktopToggles->fire(Core::App().settings().desktopNotify());\n\t\t\t} else if (change == ChangeType::SoundEnabled) {\n\t\t\t\tsoundAllowed->fire(allowed());\n\t\t\t} else if (change == ChangeType::FlashBounceEnabled) {\n\t\t\t\tflashbounceToggles->fire(\n\t\t\t\t\tCore::App().settings().flashBounceNotify());\n\t\t\t}\n\t\t}, desktop->lifetime());\n\t}\n}\n\nvoid BuildNotifyViewSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tif (!controller) {\n\t\treturn;\n\t}\n\n\tbuilder.add([controller](const WidgetContext &ctx) {\n\t\tconst auto container = ctx.container.get();\n\t\tconst auto &settings = Core::App().settings();\n\t\tconst auto checkboxes = SetupNotifyViewOptions(\n\t\t\tcontroller,\n\t\t\tcontainer,\n\t\t\t(settings.notifyView() <= NotifyView::ShowName),\n\t\t\t(settings.notifyView() <= NotifyView::ShowPreview));\n\t\tconst auto name = checkboxes.name;\n\t\tconst auto preview = checkboxes.preview;\n\t\tconst auto previewWrap = checkboxes.wrap;\n\n\t\tconst auto previewDivider = container->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::BoxContentDivider>(container)));\n\t\tpreviewWrap->toggle(settings.desktopNotify(), anim::type::instant);\n\t\tpreviewDivider->toggle(!settings.desktopNotify(), anim::type::instant);\n\n\t\tconst auto changed = [=](ChangeType change) {\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\tCore::App().notifications().notifySettingsChanged(change);\n\t\t};\n\n\t\tname->checkedChanges(\n\t\t) | rpl::map([=](bool checked) {\n\t\t\tif (!checked) {\n\t\t\t\tpreview->setChecked(false);\n\t\t\t\treturn NotifyView::ShowNothing;\n\t\t\t} else if (!preview->checked()) {\n\t\t\t\treturn NotifyView::ShowName;\n\t\t\t}\n\t\t\treturn NotifyView::ShowPreview;\n\t\t}) | rpl::filter([=](NotifyView value) {\n\t\t\treturn (value != Core::App().settings().notifyView());\n\t\t}) | rpl::on_next([=](NotifyView value) {\n\t\t\tCore::App().settings().setNotifyView(value);\n\t\t\tchanged(ChangeType::ViewParams);\n\t\t}, name->lifetime());\n\n\t\tpreview->checkedChanges(\n\t\t) | rpl::map([=](bool checked) {\n\t\t\tif (checked) {\n\t\t\t\tname->setChecked(true);\n\t\t\t\treturn NotifyView::ShowPreview;\n\t\t\t} else if (name->checked()) {\n\t\t\t\treturn NotifyView::ShowName;\n\t\t\t}\n\t\t\treturn NotifyView::ShowNothing;\n\t\t}) | rpl::filter([=](NotifyView value) {\n\t\t\treturn (value != Core::App().settings().notifyView());\n\t\t}) | rpl::on_next([=](NotifyView value) {\n\t\t\tCore::App().settings().setNotifyView(value);\n\t\t\tchanged(ChangeType::ViewParams);\n\t\t}, preview->lifetime());\n\n\t\tCore::App().notifications().settingsChanged(\n\t\t) | rpl::on_next([=](ChangeType change) {\n\t\t\tif (change == ChangeType::DesktopEnabled) {\n\t\t\t\tpreviewWrap->toggle(\n\t\t\t\t\tCore::App().settings().desktopNotify(),\n\t\t\t\t\tanim::type::normal);\n\t\t\t\tpreviewDivider->toggle(\n\t\t\t\t\t!Core::App().settings().desktopNotify(),\n\t\t\t\t\tanim::type::normal);\n\t\t\t}\n\t\t}, container->lifetime());\n\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n}\n\nvoid BuildNotifyTypeSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto showOther = builder.showOther();\n\n\tbuilder.addSkip(st::notifyPreviewBottomSkip);\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"notifications/types\"_q,\n\t\t.title = tr::lng_settings_notify_title(),\n\t\t.keywords = { u\"private\"_q, u\"groups\"_q, u\"channels\"_q },\n\t});\n\n\tif (controller) {\n\t\tcontroller->session().data().notifySettings().loadExceptions();\n\t}\n\n\tbuilder.add([controller, showOther](const WidgetContext &ctx) {\n\t\tconst auto privateChats = AddTypeButton(\n\t\t\tctx.container,\n\t\t\tcontroller,\n\t\t\tData::DefaultNotify::User,\n\t\t\tshowOther);\n\t\tconst auto groups = AddTypeButton(\n\t\t\tctx.container,\n\t\t\tcontroller,\n\t\t\tData::DefaultNotify::Group,\n\t\t\tshowOther);\n\t\tconst auto channels = AddTypeButton(\n\t\t\tctx.container,\n\t\t\tcontroller,\n\t\t\tData::DefaultNotify::Broadcast,\n\t\t\tshowOther);\n\t\tconst auto reactions = AddReactionsButton(\n\t\t\tctx.container,\n\t\t\tcontroller,\n\t\t\tshowOther);\n\t\tif (ctx.highlights) {\n\t\t\tctx.highlights->push_back({\n\t\t\t\tu\"notifications/private\"_q,\n\t\t\t\t{ privateChats.get(), { .rippleShape = true } },\n\t\t\t});\n\t\t\tctx.highlights->push_back({\n\t\t\t\tu\"notifications/groups\"_q,\n\t\t\t\t{ groups.get(), { .rippleShape = true } },\n\t\t\t});\n\t\t\tctx.highlights->push_back({\n\t\t\t\tu\"notifications/channels\"_q,\n\t\t\t\t{ channels.get(), { .rippleShape = true } },\n\t\t\t});\n\t\t\tctx.highlights->push_back({\n\t\t\t\tu\"notifications/reactions\"_q,\n\t\t\t\t{ reactions.get(), { .rippleShape = true } },\n\t\t\t});\n\t\t}\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"notifications/private\"_q,\n\t\t\t.title = tr::lng_notification_private_chats(tr::now),\n\t\t\t.keywords = { u\"private\"_q, u\"chats\"_q, u\"direct\"_q },\n\t\t\t.icon = { &st::menuIconProfile },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"notifications/groups\"_q,\n\t\t\t.title = tr::lng_notification_groups(tr::now),\n\t\t\t.keywords = { u\"groups\"_q, u\"chats\"_q },\n\t\t\t.icon = { &st::menuIconGroups },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"notifications/channels\"_q,\n\t\t\t.title = tr::lng_notification_channels(tr::now),\n\t\t\t.keywords = { u\"channels\"_q, u\"broadcast\"_q },\n\t\t\t.icon = { &st::menuIconChannel },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"notifications/reactions\"_q,\n\t\t\t.title = tr::lng_notification_reactions(tr::now),\n\t\t\t.keywords = { u\"reactions\"_q },\n\t\t\t.icon = { &st::menuIconGroupReactions },\n\t\t};\n\t});\n}\n\nvoid BuildEventNotificationsSection(SectionBuilder &builder) {\n\tbuilder.addSkip(st::settingsCheckboxesSkip);\n\tbuilder.addDivider();\n\tbuilder.addSkip(st::settingsCheckboxesSkip);\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"notifications/events\"_q,\n\t\t.title = tr::lng_settings_events_title(),\n\t\t.keywords = { u\"events\"_q, u\"joined\"_q, u\"pinned\"_q },\n\t});\n\n\tconst auto session = builder.session();\n\tconst auto &settings = Core::App().settings();\n\n\tauto joinSilent = rpl::single(\n\t\tsession->api().contactSignupSilentCurrent().value_or(false)\n\t) | rpl::then(session->api().contactSignupSilent());\n\n\tconst auto joined = builder.addButton({\n\t\t.id = u\"notifications/events/joined\"_q,\n\t\t.title = tr::lng_settings_events_joined(),\n\t\t.icon = { &st::menuIconInvite },\n\t\t.toggled = std::move(joinSilent) | rpl::map([](bool s) { return !s; }),\n\t\t.keywords = { u\"joined\"_q, u\"contacts\"_q, u\"signup\"_q },\n\t});\n\tif (joined) {\n\t\tjoined->toggledChanges(\n\t\t) | rpl::filter([=](bool enabled) {\n\t\t\tconst auto silent = session->api().contactSignupSilentCurrent();\n\t\t\treturn (enabled == silent.value_or(false));\n\t\t}) | rpl::on_next([=](bool enabled) {\n\t\t\tsession->api().saveContactSignupSilent(!enabled);\n\t\t}, joined->lifetime());\n\t}\n\n\tconst auto pinned = builder.addButton({\n\t\t.id = u\"notifications/events/pinned\"_q,\n\t\t.title = tr::lng_settings_events_pinned(),\n\t\t.icon = { &st::menuIconPin },\n\t\t.toggled = rpl::single(\n\t\t\tsettings.notifyAboutPinned()\n\t\t) | rpl::then(settings.notifyAboutPinnedChanges()),\n\t\t.keywords = { u\"pinned\"_q, u\"message\"_q },\n\t});\n\tif (pinned) {\n\t\tpinned->toggledChanges(\n\t\t) | rpl::filter([=](bool notify) {\n\t\t\treturn (notify != Core::App().settings().notifyAboutPinned());\n\t\t}) | rpl::on_next([=](bool notify) {\n\t\t\tCore::App().settings().setNotifyAboutPinned(notify);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}, pinned->lifetime());\n\t}\n}\n\nvoid BuildCallNotificationsSection(SectionBuilder &builder) {\n\tbuilder.addSkip(st::settingsCheckboxesSkip);\n\tbuilder.addDivider();\n\tbuilder.addSkip(st::settingsCheckboxesSkip);\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"notifications/calls\"_q,\n\t\t.title = tr::lng_settings_notifications_calls_title(),\n\t\t.keywords = { u\"calls\"_q, u\"incoming\"_q, u\"receive\"_q },\n\t});\n\n\tconst auto session = builder.session();\n\tconst auto authorizations = &session->api().authorizations();\n\tauthorizations->reload();\n\n\tconst auto acceptCalls = builder.addButton({\n\t\t.id = u\"notifications/calls/accept\"_q,\n\t\t.title = tr::lng_settings_call_accept_calls(),\n\t\t.icon = { &st::menuIconCallsReceive },\n\t\t.toggled = authorizations->callsDisabledHereValue()\n\t\t\t| rpl::map([](bool disabled) { return !disabled; }),\n\t\t.keywords = { u\"calls\"_q, u\"receive\"_q, u\"incoming\"_q },\n\t});\n\tif (acceptCalls) {\n\t\tacceptCalls->toggledChanges(\n\t\t) | rpl::filter([=](bool toggled) {\n\t\t\treturn (toggled == authorizations->callsDisabledHere());\n\t\t}) | rpl::on_next([=](bool toggled) {\n\t\t\tauthorizations->toggleCallsDisabledHere(!toggled);\n\t\t}, acceptCalls->lifetime());\n\t}\n}\n\nvoid BuildBadgeCounterSection(SectionBuilder &builder) {\n\tbuilder.addSkip(st::settingsCheckboxesSkip);\n\tbuilder.addDivider();\n\tbuilder.addSkip(st::settingsCheckboxesSkip);\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"notifications/badge\"_q,\n\t\t.title = tr::lng_settings_badge_title(),\n\t\t.keywords = { u\"badge\"_q, u\"counter\"_q, u\"unread\"_q },\n\t});\n\n\tconst auto session = builder.session();\n\tconst auto &settings = Core::App().settings();\n\n\tconst auto muted = builder.addButton({\n\t\t.id = u\"notifications/include-muted-chats\"_q,\n\t\t.title = tr::lng_settings_include_muted(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.toggled = rpl::single(settings.includeMutedCounter()),\n\t\t.keywords = { u\"muted\"_q, u\"badge\"_q, u\"counter\"_q },\n\t});\n\n\tconst auto hasFolders = session->data().chatsFilters().has();\n\tconst auto mutedFolders = hasFolders ? builder.addButton({\n\t\t.id = u\"notifications/badge/muted_folders\"_q,\n\t\t.title = tr::lng_settings_include_muted_folders(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.toggled = rpl::single(settings.includeMutedCounterFolders()),\n\t\t.keywords = { u\"muted\"_q, u\"folders\"_q },\n\t}) : nullptr;\n\n\tconst auto count = builder.addButton({\n\t\t.id = u\"notifications/count-unread-messages\"_q,\n\t\t.title = tr::lng_settings_count_unread(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.toggled = rpl::single(settings.countUnreadMessages()),\n\t\t.keywords = { u\"unread\"_q, u\"messages\"_q, u\"count\"_q },\n\t});\n\n\tconst auto changed = [=](ChangeType change) {\n\t\tCore::App().saveSettingsDelayed();\n\t\tCore::App().notifications().notifySettingsChanged(change);\n\t};\n\n\tif (muted) {\n\t\tmuted->toggledChanges(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\treturn (checked != Core::App().settings().includeMutedCounter());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tCore::App().settings().setIncludeMutedCounter(checked);\n\t\t\tchanged(ChangeType::IncludeMuted);\n\t\t}, muted->lifetime());\n\t}\n\n\tif (mutedFolders) {\n\t\tmutedFolders->toggledChanges(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\treturn (checked\n\t\t\t\t!= Core::App().settings().includeMutedCounterFolders());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tCore::App().settings().setIncludeMutedCounterFolders(checked);\n\t\t\tchanged(ChangeType::IncludeMuted);\n\t\t}, mutedFolders->lifetime());\n\t}\n\n\tif (count) {\n\t\tcount->toggledChanges(\n\t\t) | rpl::filter([=](bool checked) {\n\t\t\treturn (checked != Core::App().settings().countUnreadMessages());\n\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\tCore::App().settings().setCountUnreadMessages(checked);\n\t\t\tchanged(ChangeType::CountMessages);\n\t\t}, count->lifetime());\n\t}\n}\n\nvoid BuildSystemIntegrationAndAdvancedSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\n\tauto nativeText = [&]() -> rpl::producer<QString> {\n\t\tif (!Platform::Notifications::Supported()\n\t\t\t|| Core::App().notifications().nativeEnforced()) {\n\t\t\treturn rpl::producer<QString>();\n\t\t} else if (Platform::IsWindows()) {\n\t\t\treturn tr::lng_settings_use_windows();\n\t\t}\n\t\treturn tr::lng_settings_use_native_notifications();\n\t}();\n\n\tif (nativeText) {\n\t\tbuilder.addSkip(st::settingsCheckboxesSkip);\n\t\tbuilder.addDivider();\n\t\tbuilder.addSkip(st::settingsCheckboxesSkip);\n\t\tbuilder.addSubsectionTitle({\n\t\t\t.id = u\"notifications/native\"_q,\n\t\t\t.title = tr::lng_settings_native_title(),\n\t\t\t.keywords = { u\"native\"_q, u\"system\"_q, u\"windows\"_q },\n\t\t});\n\t}\n\n\tconst auto &settings = Core::App().settings();\n\tconst auto native = nativeText ? builder.addButton({\n\t\t.id = u\"notifications/use-native\"_q,\n\t\t.title = std::move(nativeText),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.toggled = rpl::single(settings.nativeNotifications()),\n\t\t.keywords = { u\"native\"_q, u\"system\"_q, u\"windows\"_q },\n\t}) : nullptr;\n\n\tif (Core::App().notifications().nativeEnforced()) {\n\t\treturn;\n\t}\n\tif (!controller) {\n\t\treturn;\n\t}\n\n\tbuilder.add([native, controller](const WidgetContext &ctx) {\n\t\tconst auto container = ctx.container.get();\n\t\tconst auto advancedSlide = container->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\t\tconst auto advancedWrap = advancedSlide->entity();\n\n\t\tif (native) {\n\t\t\tnative->toggledChanges(\n\t\t\t) | rpl::filter([](bool checked) {\n\t\t\t\treturn (checked != Core::App().settings().nativeNotifications());\n\t\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\t\tCore::App().settings().setNativeNotifications(checked);\n\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t\tCore::App().notifications().createManager();\n\t\t\t\tadvancedSlide->toggle(!checked, anim::type::normal);\n\t\t\t}, native->lifetime());\n\t\t}\n\n\t\tif (Platform::IsWindows()) {\n\t\t\tconst auto skipInFocus = advancedWrap->add(object_ptr<Ui::SettingsButton>(\n\t\t\t\tadvancedWrap,\n\t\t\t\ttr::lng_settings_skip_in_focus(),\n\t\t\t\tst::settingsButtonNoIcon\n\t\t\t))->toggleOn(rpl::single(Core::App().settings().skipToastsInFocus()));\n\n\t\t\tskipInFocus->toggledChanges(\n\t\t\t) | rpl::filter([](bool checked) {\n\t\t\t\treturn (checked != Core::App().settings().skipToastsInFocus());\n\t\t\t}) | rpl::on_next([=](bool checked) {\n\t\t\t\tCore::App().settings().setSkipToastsInFocus(checked);\n\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t\tif (checked && Platform::Notifications::SkipToastForCustom()) {\n\t\t\t\t\tCore::App().notifications().notifySettingsChanged(\n\t\t\t\t\t\tChangeType::DesktopEnabled);\n\t\t\t\t}\n\t\t\t}, skipInFocus->lifetime());\n\t\t}\n\n\t\tconst auto screens = QGuiApplication::screens();\n\t\tif (screens.size() > 1) {\n\t\t\tUi::AddSkip(advancedWrap, st::settingsCheckboxesSkip);\n\t\t\tUi::AddDivider(advancedWrap);\n\t\t\tUi::AddSkip(advancedWrap, st::settingsCheckboxesSkip);\n\t\t\tUi::AddSubsectionTitle(\n\t\t\t\tadvancedWrap,\n\t\t\t\ttr::lng_settings_notifications_display());\n\n\t\t\tconst auto currentChecksum\n\t\t\t\t= Core::App().settings().notificationsDisplayChecksum();\n\t\t\tauto currentIndex = (currentChecksum == 0)\n\t\t\t\t? kDefaultDisplayIndex\n\t\t\t\t: 0;\n\t\t\tfor (auto i = 0; i < screens.size(); ++i) {\n\t\t\t\tif (Platform::ScreenNameChecksum(screens[i]) == currentChecksum) {\n\t\t\t\t\tcurrentIndex = i;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst auto group = std::make_shared<Ui::RadiobuttonGroup>(\n\t\t\t\tcurrentIndex);\n\n\t\t\tadvancedWrap->add(\n\t\t\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\t\t\tadvancedWrap,\n\t\t\t\t\tgroup,\n\t\t\t\t\tkDefaultDisplayIndex,\n\t\t\t\t\ttr::lng_settings_notifications_display_default(tr::now),\n\t\t\t\t\tst::settingsSendType),\n\t\t\t\tst::settingsSendTypePadding);\n\n\t\t\tfor (auto i = 0; i < screens.size(); ++i) {\n\t\t\t\tconst auto &screen = screens[i];\n\t\t\t\tconst auto name = Platform::ScreenDisplayLabel(screen);\n\t\t\t\tconst auto geometry = screen->geometry();\n\t\t\t\tconst auto resolution = QString::number(geometry.width())\n\t\t\t\t\t+ QChar(0x00D7)\n\t\t\t\t\t+ QString::number(geometry.height());\n\t\t\t\tconst auto label = name.isEmpty()\n\t\t\t\t\t? QString(\"Display (%1)\").arg(resolution)\n\t\t\t\t\t: QString(\"%1 (%2)\").arg(name).arg(resolution);\n\t\t\t\tadvancedWrap->add(\n\t\t\t\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\t\t\t\tadvancedWrap,\n\t\t\t\t\t\tgroup,\n\t\t\t\t\t\ti,\n\t\t\t\t\t\tlabel,\n\t\t\t\t\t\tst::settingsSendType),\n\t\t\t\t\tst::settingsSendTypePadding);\n\t\t\t}\n\t\t\tgroup->setChangedCallback([=](int selectedIndex) {\n\t\t\t\tif (selectedIndex == kDefaultDisplayIndex) {\n\t\t\t\t\tCore::App().settings().setNotificationsDisplayChecksum(0);\n\t\t\t\t\tCore::App().saveSettings();\n\t\t\t\t\tCore::App().notifications().notifySettingsChanged(\n\t\t\t\t\t\tChangeType::Corner);\n\t\t\t\t} else {\n\t\t\t\t\tconst auto screens = QGuiApplication::screens();\n\t\t\t\t\tif (selectedIndex >= 0 && selectedIndex < screens.size()) {\n\t\t\t\t\t\tconst auto checksum = Platform::ScreenNameChecksum(\n\t\t\t\t\t\t\tscreens[selectedIndex]);\n\t\t\t\t\t\tCore::App().settings().setNotificationsDisplayChecksum(\n\t\t\t\t\t\t\tchecksum);\n\t\t\t\t\t\tCore::App().saveSettings();\n\t\t\t\t\t\tCore::App().notifications().notifySettingsChanged(\n\t\t\t\t\t\t\tChangeType::Corner);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tUi::AddSkip(advancedWrap, st::settingsCheckboxesSkip);\n\t\tUi::AddDivider(advancedWrap);\n\t\tUi::AddSkip(advancedWrap, st::settingsCheckboxesSkip);\n\t\tUi::AddSubsectionTitle(\n\t\t\tadvancedWrap,\n\t\t\ttr::lng_settings_notifications_position());\n\t\tUi::AddSkip(advancedWrap, st::settingsCheckboxesSkip);\n\n\t\tconst auto position = advancedWrap->add(\n\t\t\tobject_ptr<NotificationsCount>(advancedWrap, controller));\n\n\t\tUi::AddSkip(advancedWrap, st::settingsCheckboxesSkip);\n\t\tUi::AddSubsectionTitle(advancedWrap, tr::lng_settings_notifications_count());\n\n\t\tconst auto countSlider = advancedWrap->add(\n\t\t\tobject_ptr<Ui::SettingsSlider>(advancedWrap, st::settingsSlider),\n\t\t\tst::settingsBigScalePadding);\n\t\tfor (int i = 0; i != kMaxNotificationsCount; ++i) {\n\t\t\tcountSlider->addSection(QString::number(i + 1));\n\t\t}\n\t\tcountSlider->setActiveSectionFast(CurrentNotificationsCount() - 1);\n\t\tcountSlider->sectionActivated(\n\t\t) | rpl::on_next([=](int section) {\n\t\t\tposition->setCount(section + 1);\n\t\t}, countSlider->lifetime());\n\t\tUi::AddSkip(advancedWrap, st::settingsCheckboxesSkip);\n\n\t\tif (Core::App().settings().nativeNotifications()) {\n\t\t\tadvancedSlide->hide(anim::type::instant);\n\t\t}\n\n\t\tCore::App().notifications().settingsChanged(\n\t\t) | rpl::on_next([=](ChangeType change) {\n\t\t\tif (change == ChangeType::DesktopEnabled) {\n\t\t\t\tconst auto native = Core::App().settings().nativeNotifications();\n\t\t\t\tadvancedSlide->toggle(!native, anim::type::normal);\n\t\t\t}\n\t\t}, advancedSlide->lifetime());\n\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n}\n\nvoid BuildNotificationsSectionContent(SectionBuilder &builder) {\n\tbuilder.addSkip(st::settingsPrivacySkip);\n\n\tBuildMultiAccountSection(builder);\n\tBuildGlobalNotificationsSection(builder);\n\tBuildNotifyViewSection(builder);\n\tBuildNotifyTypeSection(builder);\n\tBuildEventNotificationsSection(builder);\n\tBuildCallNotificationsSection(builder);\n\tBuildBadgeCounterSection(builder);\n\tBuildSystemIntegrationAndAdvancedSection(builder);\n}\n\nclass Notifications : public Section<Notifications> {\npublic:\n\tNotifications(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\nprivate:\n\tvoid setupContent();\n\n};\n\nconst auto kMeta = BuildHelper({\n\t.id = Notifications::Id(),\n\t.parentId = MainId(),\n\t.title = &tr::lng_settings_section_notify,\n\t.icon = &st::menuIconNotifications,\n}, [](SectionBuilder &builder) {\n\tBuildNotificationsSectionContent(builder);\n});\n\nconst SectionBuildMethod kNotificationsSection = kMeta.build;\n\nNotifications::Notifications(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent();\n}\n\nrpl::producer<QString> Notifications::title() {\n\treturn tr::lng_settings_section_notify();\n}\n\nvoid Notifications::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tbuild(content, kNotificationsSection);\n\tUi::ResizeFitChild(this, content);\n}\n\n} // namespace\n\nType NotificationsId() {\n\treturn Notifications::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_notifications.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Settings {\n\n[[nodiscard]] Type NotificationsId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_notifications_reactions.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_notifications_reactions.h\"\n\n#include \"api/api_reactions_notify_settings.h\"\n#include \"apiwrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_notifications.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/settings_notifications_common.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nusing NotifyFrom = Api::ReactionsNotifyFrom;\nusing namespace Builder;\n\n[[nodiscard]] rpl::producer<QString> FromLabel(NotifyFrom from) {\n\tswitch (from) {\n\tcase NotifyFrom::None:\n\t\treturn tr::lng_notification_reactions_from_nobody();\n\tcase NotifyFrom::Contacts:\n\t\treturn tr::lng_notification_reactions_from_contacts();\n\tcase NotifyFrom::All:\n\t\treturn tr::lng_notification_reactions_from_all();\n\t}\n\tUnexpected(\"Value in FromLabel.\");\n}\n\nvoid ShowFromBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tNotifyFrom current,\n\t\tFn<void(NotifyFrom)> done) {\n\tcontroller->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tbox->setTitle(tr::lng_notification_reactions_from());\n\n\t\tconst auto initial = (current == NotifyFrom::None)\n\t\t\t? int(NotifyFrom::All)\n\t\t\t: int(current);\n\t\tconst auto group = std::make_shared<Ui::RadiobuttonGroup>(initial);\n\n\t\tconst auto addOption = [&](NotifyFrom value, const QString &label) {\n\t\t\tbox->addRow(\n\t\t\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\t\t\tbox,\n\t\t\t\t\tgroup,\n\t\t\t\t\tint(value),\n\t\t\t\t\tlabel,\n\t\t\t\t\tst::defaultBoxCheckbox),\n\t\t\t\tst::boxOptionListPadding\n\t\t\t\t\t+ QMargins(\n\t\t\t\t\t\tst::boxPadding.left(),\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tst::boxPadding.right(),\n\t\t\t\t\t\tst::boxOptionListSkip));\n\t\t};\n\t\taddOption(\n\t\t\tNotifyFrom::All,\n\t\t\ttr::lng_notification_reactions_from_all(tr::now));\n\t\taddOption(\n\t\t\tNotifyFrom::Contacts,\n\t\t\ttr::lng_notification_reactions_from_contacts(tr::now));\n\t\tbox->addButton(tr::lng_box_ok(), [=] {\n\t\t\tdone(NotifyFrom(group->current()));\n\t\t\tbox->closeBox();\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t}));\n}\n\nvoid AddToggleRow(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\trpl::producer<QString> title,\n\t\tconst style::icon *icon,\n\t\trpl::producer<NotifyFrom> fromValue,\n\t\tFn<NotifyFrom()> fromCurrent,\n\t\tFn<void(NotifyFrom)> updateFrom) {\n\tauto forToggle = rpl::duplicate(fromValue);\n\tauto status = std::move(\n\t\tfromValue\n\t) | rpl::map([](NotifyFrom from) {\n\t\treturn FromLabel(from);\n\t}) | rpl::flatten_latest();\n\n\tconst auto [button, toggleButton, checkView] = SetupSplitToggle(\n\t\tcontainer,\n\t\tstd::move(title),\n\t\ticon,\n\t\tfromCurrent() != NotifyFrom::None,\n\t\tstd::move(status));\n\n\tstd::move(\n\t\tforToggle\n\t) | rpl::on_next([=](NotifyFrom from) {\n\t\tcheckView->setChecked(\n\t\t\tfrom != NotifyFrom::None,\n\t\t\tanim::type::normal);\n\t}, button->lifetime());\n\n\ttoggleButton->clicks(\n\t) | rpl::on_next([=] {\n\t\tconst auto enabled = !checkView->checked();\n\t\tupdateFrom(enabled ? NotifyFrom::All : NotifyFrom::None);\n\t}, toggleButton->lifetime());\n\n\tbutton->setClickedCallback([=] {\n\t\tif (fromCurrent() == NotifyFrom::None) {\n\t\t\tupdateFrom(NotifyFrom::All);\n\t\t\treturn;\n\t\t}\n\t\tShowFromBox(controller, fromCurrent(), [=](NotifyFrom from) {\n\t\t\tupdateFrom(from);\n\t\t});\n\t});\n}\n\nvoid BuildNotificationsReactionsContent(SectionBuilder &builder) {\n\tbuilder.addSkip(st::settingsCheckboxesSkip);\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"notifications/reactions/about\"_q,\n\t\t.title = tr::lng_notification_reactions_notify_about(),\n\t});\n\n\tbuilder.add([](const WidgetContext &ctx) {\n\t\tconst auto session = &ctx.controller->session();\n\t\tauto &rs = session->api().reactionsNotifySettings();\n\n\t\tAddToggleRow(\n\t\t\tctx.container,\n\t\t\tctx.controller,\n\t\t\ttr::lng_notification_reactions_messages_full(),\n\t\t\t&st::menuIconMarkUnread,\n\t\t\trs.messagesFrom(),\n\t\t\t[session] {\n\t\t\t\treturn session->api().reactionsNotifySettings()\n\t\t\t\t\t.messagesFromCurrent();\n\t\t\t},\n\t\t\t[session](NotifyFrom from) {\n\t\t\t\tsession->api().reactionsNotifySettings()\n\t\t\t\t\t.updateMessagesFrom(from);\n\t\t\t});\n\n\t\tAddToggleRow(\n\t\t\tctx.container,\n\t\t\tctx.controller,\n\t\t\ttr::lng_notification_reactions_poll_votes_full(),\n\t\t\t&st::menuIconCreatePoll,\n\t\t\trs.pollVotesFrom(),\n\t\t\t[session] {\n\t\t\t\treturn session->api().reactionsNotifySettings()\n\t\t\t\t\t.pollVotesFromCurrent();\n\t\t\t},\n\t\t\t[session](NotifyFrom from) {\n\t\t\t\tsession->api().reactionsNotifySettings()\n\t\t\t\t\t.updatePollVotesFrom(from);\n\t\t\t});\n\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"notifications/reactions/messages\"_q,\n\t\t\t.title = tr::lng_notification_reactions_messages_full(tr::now),\n\t\t\t.keywords = { u\"reactions\"_q, u\"messages\"_q },\n\t\t};\n\t});\n\n\tbuilder.addSkip(st::settingsCheckboxesSkip);\n\tbuilder.addDivider();\n\tbuilder.addSkip(st::settingsCheckboxesSkip);\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"notifications/reactions/settings\"_q,\n\t\t.title = tr::lng_notification_reactions_settings(),\n\t});\n\n\tbuilder.add([](const WidgetContext &ctx) {\n\t\tconst auto session = &ctx.controller->session();\n\t\tauto &rs = session->api().reactionsNotifySettings();\n\n\t\tconst auto showSender = AddButtonWithIcon(\n\t\t\tctx.container,\n\t\t\ttr::lng_notification_reactions_show_sender(),\n\t\t\tst::settingsButtonNoIcon\n\t\t)->toggleOn(rs.showPreviews());\n\n\t\tshowSender->toggledChanges(\n\t\t) | rpl::filter([session](bool checked) {\n\t\t\treturn (checked\n\t\t\t\t!= session->api().reactionsNotifySettings()\n\t\t\t\t\t.showPreviewsCurrent());\n\t\t}) | rpl::on_next([session](bool checked) {\n\t\t\tsession->api().reactionsNotifySettings()\n\t\t\t\t.updateShowPreviews(checked);\n\t\t}, showSender->lifetime());\n\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"notifications/reactions/preview\"_q,\n\t\t\t.title = tr::lng_notification_reactions_show_sender(tr::now),\n\t\t\t.keywords = { u\"sender\"_q, u\"preview\"_q },\n\t\t};\n\t});\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = NotificationsReactions::Id(),\n\t.parentId = NotificationsId(),\n\t.title = &tr::lng_notification_reactions,\n\t.icon = &st::menuIconGroupReactions,\n}, [](SectionBuilder &builder) {\n\tBuildNotificationsReactionsContent(builder);\n});\n\n} // namespace\n\nNotificationsReactions::NotificationsReactions(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent(controller);\n}\n\nrpl::producer<QString> NotificationsReactions::title() {\n\treturn tr::lng_notification_reactions_title();\n}\n\nvoid NotificationsReactions::setupContent(\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto container = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tconst SectionBuildMethod buildMethod = [](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tauto &lifetime = container->lifetime();\n\t\tconst auto highlights\n\t\t\t= lifetime.make_state<HighlightRegistry>();\n\n\t\tconst auto session = &controller->session();\n\t\tauto &rs = session->api().reactionsNotifySettings();\n\t\trs.reload();\n\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = Window::PausedIn(\n\t\t\t\tcontroller,\n\t\t\t\tWindow::GifPauseReason::Layer),\n\t\t\t.highlights = highlights,\n\t\t});\n\t\tBuildNotificationsReactionsContent(builder);\n\n\t\tstd::move(showFinished) | rpl::on_next([=] {\n\t\t\tfor (const auto &[id, entry] : *highlights) {\n\t\t\t\tif (entry.widget) {\n\t\t\t\t\tcontroller->checkHighlightControl(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tentry.widget,\n\t\t\t\t\t\tbase::duplicate(entry.args));\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime);\n\t};\n\n\tbuild(container, buildMethod);\n\n\tUi::ResizeFitChild(this, container);\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_notifications_reactions.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_common_session.h\"\n\nnamespace Settings {\n\nclass NotificationsReactions : public Section<NotificationsReactions> {\npublic:\n\tNotificationsReactions(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\nprivate:\n\tvoid setupContent(not_null<Window::SessionController*> controller);\n\n};\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_notifications_type.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_notifications_type.h\"\n\n#include \"api/api_ringtones.h\"\n#include \"apiwrap.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/ringtones_box.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"core/application.h\"\n#include \"data/notify/data_peer_notify_volume.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_mute.h\"\n#include \"platform/platform_notifications_manager.h\"\n#include \"settings/sections/settings_notifications.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nusing Notify = Data::DefaultNotify;\nusing namespace Builder;\n\nstruct Factory : AbstractSectionFactory {\n\texplicit Factory(Notify type) : type(type) {\n\t}\n\n\tobject_ptr<AbstractSection> create(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\trpl::producer<Container> containerValue\n\t) const final override {\n\t\treturn object_ptr<NotificationsType>(parent, controller, type);\n\t}\n\n\tconst Notify type = {};\n};\n\nclass AddExceptionBoxController final\n\t: public ChatsListBoxController\n\t, public base::has_weak_ptr {\npublic:\n\tAddExceptionBoxController(\n\t\tnot_null<::Main::Session*> session,\n\t\tNotify type,\n\t\tFn<void(not_null<PeerData*>)> done);\n\n\t::Main::Session &session() const override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\nprivate:\n\tvoid prepareViewHook() override;\n\tstd::unique_ptr<Row> createRow(not_null<History*> history) override;\n\n\tconst not_null<::Main::Session*> _session;\n\tconst Notify _type;\n\tconst Fn<void(not_null<PeerData*>)> _done;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\tPeerData *_lastClickedPeer = nullptr;\n\n\trpl::lifetime _lifetime;\n\n};\n\nclass ExceptionsController final : public PeerListController {\npublic:\n\tExceptionsController(\n\t\tnot_null<Window::SessionController*> window,\n\t\tNotify type);\n\n\t::Main::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) override;\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\n\tvoid bringToTop(not_null<PeerData*> peer);\n\n\t[[nodiscard]] rpl::producer<int> countValue() const;\n\nprivate:\n\tvoid refreshRows();\n\tbool appendRow(not_null<PeerData*> peer);\n\tstd::unique_ptr<PeerListRow> createRow(not_null<PeerData*> peer) const;\n\tvoid refreshStatus(not_null<PeerListRow*> row) const;\n\n\tvoid sort();\n\n\tconst not_null<Window::SessionController*> _window;\n\tconst Notify _type;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n\tbase::flat_map<not_null<PeerData*>, int> _topOrdered;\n\tint _topOrder = 0;\n\n\trpl::variable<int> _count;\n\n\trpl::lifetime _lifetime;\n\n};\n\nAddExceptionBoxController::AddExceptionBoxController(\n\tnot_null<::Main::Session*> session,\n\tNotify type,\n\tFn<void(not_null<PeerData*>)> done)\n: ChatsListBoxController(session)\n, _session(session)\n, _type(type)\n, _done(std::move(done)) {\n}\n\n::Main::Session &AddExceptionBoxController::session() const {\n\treturn *_session;\n}\n\nvoid AddExceptionBoxController::prepareViewHook() {\n\tdelegate()->peerListSetTitle(tr::lng_notification_exceptions_add());\n\n\t_session->changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::Notifications\n\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\treturn update.peer == _lastClickedPeer;\n\t}) | rpl::on_next([=] {\n\t\tif (const auto onstack = _done) {\n\t\t\tonstack(_lastClickedPeer);\n\t\t}\n\t}, _lifetime);\n}\n\nvoid AddExceptionBoxController::rowClicked(not_null<PeerListRow*> row) {\n\tdelegate()->peerListShowRowMenu(row, true);\n}\n\nbase::unique_qptr<Ui::PopupMenu> AddExceptionBoxController::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tconst auto peer = row->peer();\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\n\tMuteMenu::FillMuteMenu(\n\t\tresult.get(),\n\t\tpeer->owner().history(peer),\n\t\tdelegate()->peerListUiShow());\n\n\tbase::take(_menu);\n\n\t_menu = base::unique_qptr<Ui::PopupMenu>(result.get());\n\t_menu->setDestroyedCallback(crl::guard(this, [=] {\n\t\t_lastClickedPeer = nullptr;\n\t}));\n\t_lastClickedPeer = peer;\n\n\treturn result;\n}\n\nauto AddExceptionBoxController::createRow(not_null<History*> history)\n-> std::unique_ptr<AddExceptionBoxController::Row> {\n\tconst auto peer = history->peer;\n\tif (Data::DefaultNotifyType(peer) != _type\n\t\t|| peer->isSelf()\n\t\t|| peer->isRepliesChat()\n\t\t|| peer->isVerifyCodes()) {\n\t\treturn nullptr;\n\t}\n\treturn std::make_unique<Row>(history);\n}\n\nExceptionsController::ExceptionsController(\n\tnot_null<Window::SessionController*> window,\n\tNotify type)\n: _window(window)\n, _type(type) {\n}\n\n::Main::Session &ExceptionsController::session() const {\n\treturn _window->session();\n}\n\nvoid ExceptionsController::prepare() {\n\trefreshRows();\n\n\tsession().data().notifySettings().exceptionsUpdates(\n\t) | rpl::filter(rpl::mappers::_1 == _type) | rpl::on_next([=] {\n\t\trefreshRows();\n\t}, lifetime());\n\n\tsession().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::Notifications\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tconst auto peer = update.peer;\n\t\tif (const auto row = delegate()->peerListFindRow(peer->id.value)) {\n\t\t\tif (peer->notify().muteUntil().has_value()) {\n\t\t\t\trefreshStatus(row);\n\t\t\t} else {\n\t\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t\t\tdelegate()->peerListRefreshRows();\n\t\t\t\t_count = delegate()->peerListFullRowsCount();\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n}\n\nvoid ExceptionsController::loadMoreRows() {\n}\n\nvoid ExceptionsController::bringToTop(not_null<PeerData*> peer) {\n\t_topOrdered[peer] = ++_topOrder;\n\tif (delegate()->peerListFindRow(peer->id.value)) {\n\t\tsort();\n\t}\n}\n\nrpl::producer<int> ExceptionsController::countValue() const {\n\treturn _count.value();\n}\n\nvoid ExceptionsController::rowClicked(not_null<PeerListRow*> row) {\n\tdelegate()->peerListShowRowMenu(row, true);\n}\n\nvoid ExceptionsController::rowRightActionClicked(\n\t\tnot_null<PeerListRow*> row) {\n\tsession().data().notifySettings().resetToDefault(row->peer());\n}\n\nvoid ExceptionsController::refreshRows() {\n\tauto seen = base::flat_set<not_null<PeerData*>>();\n\tconst auto &list = session().data().notifySettings().exceptions(_type);\n\tauto removed = false, added = false;\n\tauto already = delegate()->peerListFullRowsCount();\n\tseen.reserve(std::min(int(list.size()), already));\n\tfor (auto i = 0; i != already;) {\n\t\tconst auto row = delegate()->peerListRowAt(i);\n\t\tif (list.contains(row->peer())) {\n\t\t\tseen.emplace(row->peer());\n\t\t\t++i;\n\t\t} else {\n\t\t\tdelegate()->peerListRemoveRow(row);\n\t\t\t--already;\n\t\t\tremoved = true;\n\t\t}\n\t}\n\tfor (const auto &peer : list) {\n\t\tif (!seen.contains(peer)) {\n\t\t\tappendRow(peer);\n\t\t\tadded = true;\n\t\t}\n\t}\n\tif (added || removed) {\n\t\tif (added) {\n\t\t\tsort();\n\t\t}\n\t\tdelegate()->peerListRefreshRows();\n\t\t_count = delegate()->peerListFullRowsCount();\n\t}\n}\n\nbase::unique_qptr<Ui::PopupMenu> ExceptionsController::rowContextMenu(\n\t\tQWidget *parent,\n\t\tnot_null<PeerListRow*> row) {\n\tconst auto peer = row->peer();\n\tauto result = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\n\tresult->addAction(\n\t\t(peer->isUser()\n\t\t\t? tr::lng_context_view_profile\n\t\t\t: peer->isBroadcast()\n\t\t\t? tr::lng_context_view_channel\n\t\t\t: tr::lng_context_view_group)(tr::now),\n\t\tcrl::guard(_window, [window = _window.get(), peer] {\n\t\t\twindow->showPeerInfo(peer);\n\t\t}),\n\t\t(peer->isUser() ? &st::menuIconProfile : &st::menuIconInfo));\n\tresult->addSeparator();\n\n\tMuteMenu::FillMuteMenu(\n\t\tresult.get(),\n\t\tpeer->owner().history(peer),\n\t\t_window->uiShow());\n\n\tbase::take(_menu);\n\n\t_menu = base::unique_qptr<Ui::PopupMenu>(result.get());\n\n\treturn result;\n}\n\nbool ExceptionsController::appendRow(not_null<PeerData*> peer) {\n\tdelegate()->peerListAppendRow(createRow(peer));\n\treturn true;\n}\n\nstd::unique_ptr<PeerListRow> ExceptionsController::createRow(\n\t\tnot_null<PeerData*> peer) const {\n\tauto row = std::make_unique<PeerListRowWithLink>(peer);\n\trow->setActionLink(tr::lng_notification_exceptions_remove(tr::now));\n\trefreshStatus(row.get());\n\treturn row;\n}\n\nvoid ExceptionsController::refreshStatus(not_null<PeerListRow*> row) const {\n\tconst auto peer = row->peer();\n\tconst auto status = peer->owner().notifySettings().isMuted(peer)\n\t\t? tr::lng_notification_exceptions_muted(tr::now)\n\t\t: tr::lng_notification_exceptions_unmuted(tr::now);\n\trow->setCustomStatus(status);\n}\n\nvoid ExceptionsController::sort() {\n\tauto keys = base::flat_map<PeerListRowId, QString>();\n\tkeys.reserve(delegate()->peerListFullRowsCount());\n\tconst auto length = QString::number(_topOrder).size();\n\tconst auto key = [&](const PeerListRow &row) {\n\t\tconst auto id = row.id();\n\t\tconst auto i = keys.find(id);\n\t\tif (i != end(keys)) {\n\t\t\treturn i->second;\n\t\t}\n\t\tconst auto peer = row.peer();\n\t\tconst auto top = _topOrdered.find(peer);\n\t\tif (top != end(_topOrdered)) {\n\t\t\tconst auto order = _topOrder - top->second;\n\t\t\treturn keys.emplace(\n\t\t\t\tid,\n\t\t\t\tu\"0%1\"_q.arg(order, length, 10, QChar('0'))).first->second;\n\t\t}\n\t\tconst auto history = peer->owner().history(peer);\n\t\treturn keys.emplace(\n\t\t\tid,\n\t\t\t'1' + history->chatListNameSortKey()).first->second;\n\t};\n\tconst auto predicate = [&](const PeerListRow &a, const PeerListRow &b) {\n\t\treturn (key(a).compare(key(b)) < 0);\n\t};\n\tdelegate()->peerListSortRows(predicate);\n}\n\n[[nodiscard]] rpl::producer<QString> Title(Notify type) {\n\tswitch (type) {\n\tcase Notify::User: return tr::lng_notification_title_private_chats();\n\tcase Notify::Group: return tr::lng_notification_title_groups();\n\tcase Notify::Broadcast: return tr::lng_notification_title_channels();\n\t}\n\tUnexpected(\"Type in Title.\");\n}\n\n[[nodiscard]] rpl::producer<QString> VolumeSubtitle(Notify type) {\n\tswitch (type) {\n\tcase Notify::User: return tr::lng_notification_volume_private_chats();\n\tcase Notify::Group: return tr::lng_notification_volume_groups();\n\tcase Notify::Broadcast: return tr::lng_notification_volume_channel();\n\t}\n\tUnexpected(\"Type in VolumeSubtitle.\");\n}\n\nstruct ChecksWidgets {\n\tUi::SettingsButton *enabled = nullptr;\n\tUi::SettingsButton *sound = nullptr;\n\tUi::SettingsButton *tone = nullptr;\n};\n\nstruct ExceptionsWidgets {\n\tUi::SettingsButton *add = nullptr;\n\tUi::SettingsButton *deleteAll = nullptr;\n};\n\nvoid SetupChecks(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tNotify type,\n\t\tChecksWidgets *widgets) {\n\tUi::AddSubsectionTitle(container, Title(type));\n\n\tconst auto session = &controller->session();\n\tconst auto settings = &session->data().notifySettings();\n\n\tconst auto enabled = container->add(\n\t\tCreateButtonWithIcon(\n\t\t\tcontainer,\n\t\t\ttr::lng_notification_enable(),\n\t\t\tst::settingsButton,\n\t\t\t{ &st::menuIconNotifications }));\n\tenabled->toggleOn(\n\t\tNotificationsEnabledForTypeValue(session, type),\n\t\ttrue);\n\n\tif (widgets) {\n\t\twidgets->enabled = enabled;\n\t}\n\n\tenabled->setAcceptBoth();\n\tMuteMenu::SetupMuteMenu(\n\t\tenabled,\n\t\tenabled->clicks(\n\t\t) | rpl::filter([=](Qt::MouseButton button) {\n\t\t\tif (button == Qt::RightButton) {\n\t\t\t\treturn true;\n\t\t\t} else if (settings->isMuted(type)) {\n\t\t\t\tsettings->defaultUpdate(type, { .unmute = true });\n\t\t\t\treturn false;\n\t\t\t} else {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}) | rpl::to_empty,\n\t\t[=] { return MuteMenu::DefaultDescriptor(session, type); },\n\t\tcontroller->uiShow());\n\n\tconst auto soundWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tsoundWrap->toggleOn(enabled->toggledValue());\n\tsoundWrap->finishAnimating();\n\n\tconst auto soundInner = soundWrap->entity();\n\tconst auto soundValue = [=] {\n\t\tconst auto sound = settings->defaultSettings(type).sound();\n\t\treturn !sound || !sound->none;\n\t};\n\tconst auto sound = soundInner->add(\n\t\tCreateButtonWithIcon(\n\t\t\tsoundInner,\n\t\t\ttr::lng_notification_sound(),\n\t\t\tst::settingsButton,\n\t\t\t{ &st::menuIconUnmute }));\n\tsound->toggleOn(rpl::single(\n\t\tsoundValue()\n\t) | rpl::then(settings->defaultUpdates(\n\t\ttype\n\t) | rpl::map([=] { return soundValue(); })));\n\n\tif (widgets) {\n\t\twidgets->sound = sound;\n\t}\n\n\tconst auto toneWrap = soundInner->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\ttoneWrap->toggleOn(sound->toggledValue());\n\ttoneWrap->finishAnimating();\n\n\tconst auto toneInner = toneWrap->entity();\n\tconst auto toneLabel = toneInner->lifetime(\n\t).make_state<rpl::event_stream<QString>>();\n\tconst auto toneValue = [=] {\n\t\tconst auto sound = settings->defaultSettings(type).sound();\n\t\treturn sound.value_or(Data::NotifySound());\n\t};\n\tconst auto label = [=] {\n\t\tconst auto now = toneValue();\n\t\treturn !now.id\n\t\t\t? tr::lng_ringtones_box_default(tr::now)\n\t\t\t: ExtractRingtoneName(session->data().document(now.id));\n\t};\n\tsettings->defaultUpdates(\n\t\tNotify::User\n\t) | rpl::on_next([=] {\n\t\ttoneLabel->fire(label());\n\t}, toneInner->lifetime());\n\tsession->api().ringtones().listUpdates(\n\t) | rpl::on_next([=] {\n\t\ttoneLabel->fire(label());\n\t}, toneInner->lifetime());\n\n\tconst auto tone = AddButtonWithLabel(\n\t\ttoneInner,\n\t\ttr::lng_notification_tone(),\n\t\ttoneLabel->events_starting_with(label()),\n\t\tst::settingsButton,\n\t\t{ &st::menuIconSoundOn });\n\n\tif (widgets) {\n\t\twidgets->tone = tone;\n\t}\n\n\t{\n\t\tauto controller = DefaultRingtonesVolumeController(session, type);\n\t\tUi::AddRingtonesVolumeSlider(\n\t\t\ttoneInner,\n\t\t\trpl::single(true),\n\t\t\tVolumeSubtitle(type),\n\t\t\tData::VolumeController{\n\t\t\t\t.volume = base::take(controller.volume),\n\t\t\t\t.saveVolume = [=](ushort volume) {\n\t\t\t\t\tCore::App().notifications().playSound(\n\t\t\t\t\t\tsession,\n\t\t\t\t\t\ttoneValue().id,\n\t\t\t\t\t\t0.01 * volume);\n\t\t\t\t\tcontroller.saveVolume(volume);\n\t\t\t\t},\n\t\t\t});\n\t}\n\n\tenabled->toggledValue(\n\t) | rpl::filter([=](bool value) {\n\t\treturn (value != NotificationsEnabledForType(session, type));\n\t}) | rpl::on_next([=](bool value) {\n\t\tsettings->defaultUpdate(type, Data::MuteValue{\n\t\t\t.unmute = value,\n\t\t\t.forever = !value,\n\t\t});\n\t}, sound->lifetime());\n\n\tsound->toggledValue(\n\t) | rpl::filter([=](bool enabled) {\n\t\tconst auto sound = settings->defaultSettings(type).sound();\n\t\treturn (!sound || !sound->none) != enabled;\n\t}) | rpl::on_next([=](bool enabled) {\n\t\tconst auto value = Data::NotifySound{ .none = !enabled };\n\t\tsettings->defaultUpdate(type, {}, {}, value);\n\t}, sound->lifetime());\n\n\ttone->setClickedCallback([=] {\n\t\tcontroller->show(Box(RingtonesBox, session, toneValue(), [=](\n\t\t\t\tData::NotifySound sound) {\n\t\t\tsettings->defaultUpdate(type, {}, {}, sound);\n\t\t}, Data::VolumeController{\n\t\t\tDefaultRingtonesVolumeController(session, type).volume,\n\t\t}));\n\t});\n}\n\nvoid SetupExceptions(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> window,\n\t\tNotify type,\n\t\tExceptionsWidgets *widgets) {\n\tconst auto add = AddButtonWithIcon(\n\t\tcontainer,\n\t\ttr::lng_notification_exceptions_add(),\n\t\tst::settingsButtonActive,\n\t\t{ &st::menuIconInviteSettings });\n\tif (widgets) {\n\t\twidgets->add = add;\n\t}\n\n\tauto controller = std::make_unique<ExceptionsController>(window, type);\n\tcontroller->setStyleOverrides(&st::settingsBlockedList);\n\tconst auto content = container->add(\n\t\tobject_ptr<PeerListContent>(container, controller.get()));\n\n\tstruct State {\n\t\tstd::unique_ptr<ExceptionsController> controller;\n\t\tstd::unique_ptr<PeerListContentDelegateSimple> delegate;\n\t};\n\tconst auto state = content->lifetime().make_state<State>();\n\tstate->controller = std::move(controller);\n\tstate->delegate = std::make_unique<PeerListContentDelegateSimple>();\n\n\tstate->delegate->setContent(content);\n\tstate->controller->setDelegate(state->delegate.get());\n\n\tadd->setClickedCallback([=] {\n\t\tconst auto box = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\t\tconst auto done = [=](not_null<PeerData*> peer) {\n\t\t\tstate->controller->bringToTop(peer);\n\t\t\tif (*box) {\n\t\t\t\t(*box)->closeBox();\n\t\t\t}\n\t\t};\n\t\tauto controller = std::make_unique<AddExceptionBoxController>(\n\t\t\t&window->session(),\n\t\t\ttype,\n\t\t\tcrl::guard(content, done));\n\t\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\t\tbox->addButton(tr::lng_cancel(), [box] { box->closeBox(); });\n\t\t};\n\t\t*box = window->show(\n\t\t\tBox<PeerListBox>(std::move(controller), std::move(initBox)));\n\t});\n\n\tconst auto wrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\tcontainer,\n\t\t\tCreateButtonWithIcon(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_notification_exceptions_clear(),\n\t\t\t\tst::settingsAttentionButtonWithIcon,\n\t\t\t\t{ &st::menuIconDeleteAttention })));\n\tif (widgets) {\n\t\twidgets->deleteAll = wrap->entity();\n\t}\n\twrap->entity()->setClickedCallback([=] {\n\t\tconst auto clear = [=](Fn<void()> close) {\n\t\t\twindow->session().data().notifySettings().clearExceptions(type);\n\t\t\tclose();\n\t\t};\n\t\twindow->show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_notification_exceptions_clear_sure(),\n\t\t\t.confirmed = clear,\n\t\t\t.confirmText = tr::lng_notification_exceptions_clear_button(),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t.title = tr::lng_notification_exceptions_clear(),\n\t\t}));\n\t});\n\twrap->toggleOn(\n\t\tstate->controller->countValue() | rpl::map(rpl::mappers::_1 > 1),\n\t\tanim::type::instant);\n}\n\nvoid BuildNotificationsTypeContent(SectionBuilder &builder, Notify type) {\n\tbuilder.addSkip(st::settingsPrivacySkip);\n\n\tbuilder.add([=](const WidgetContext &ctx) {\n\t\tChecksWidgets widgets;\n\t\tSetupChecks(ctx.container, ctx.controller, type, &widgets);\n\t\tif (ctx.highlights) {\n\t\t\tif (widgets.enabled) {\n\t\t\t\tctx.highlights->push_back({\n\t\t\t\t\tu\"notifications/type/show\"_q,\n\t\t\t\t\t{ widgets.enabled, { .rippleShape = true } },\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (widgets.sound) {\n\t\t\t\tctx.highlights->push_back({\n\t\t\t\t\tu\"notifications/type/sound\"_q,\n\t\t\t\t\t{ widgets.sound, { .rippleShape = true } },\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (widgets.tone) {\n\t\t\t\tctx.highlights->push_back({\n\t\t\t\t\tu\"notifications/type/tone\"_q,\n\t\t\t\t\t{ widgets.tone, { .rippleShape = true } },\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"notifications/type/show\"_q,\n\t\t\t.title = tr::lng_notification_enable(tr::now),\n\t\t\t.keywords = { u\"enable\"_q, u\"notifications\"_q, u\"mute\"_q },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"notifications/type/sound\"_q,\n\t\t\t.title = tr::lng_notification_sound(tr::now),\n\t\t\t.keywords = { u\"sound\"_q, u\"audio\"_q, u\"tone\"_q },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"notifications/type/tone\"_q,\n\t\t\t.title = tr::lng_notification_tone(tr::now),\n\t\t\t.keywords = { u\"tone\"_q, u\"ringtone\"_q, u\"notification\"_q },\n\t\t};\n\t});\n\n\tbuilder.addSkip();\n\tbuilder.addDivider();\n\tbuilder.addSkip();\n\n\tbuilder.add([=](const WidgetContext &ctx) {\n\t\tExceptionsWidgets widgets;\n\t\tSetupExceptions(ctx.container, ctx.controller, type, &widgets);\n\t\tif (ctx.highlights) {\n\t\t\tif (widgets.add) {\n\t\t\t\tctx.highlights->push_back({\n\t\t\t\t\tu\"notifications/type/add-exception\"_q,\n\t\t\t\t\t{ widgets.add, { .rippleShape = true } },\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (widgets.deleteAll) {\n\t\t\t\tctx.highlights->push_back({\n\t\t\t\t\tu\"notifications/type/delete-exceptions\"_q,\n\t\t\t\t\t{ widgets.deleteAll, { .rippleShape = true } },\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t}, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"notifications/type/add-exception\"_q,\n\t\t\t.title = tr::lng_notification_exceptions_add(tr::now),\n\t\t\t.keywords = { u\"exception\"_q, u\"add\"_q, u\"exclude\"_q },\n\t\t};\n\t});\n\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"notifications/type/delete-exceptions\"_q,\n\t\t\t.title = tr::lng_notification_exceptions_clear(tr::now),\n\t\t\t.keywords = { u\"clear\"_q, u\"delete\"_q, u\"exceptions\"_q },\n\t\t};\n\t});\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = NotificationsType::Id(Notify::User),\n\t.parentId = NotificationsId(),\n\t.title = &tr::lng_notification_private_chats,\n\t.icon = &st::menuIconProfile,\n}, [](SectionBuilder &builder) {\n\tBuildNotificationsTypeContent(builder, Notify::User);\n});\n\n} // namespace\n\nNotificationsType::NotificationsType(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tNotify type)\n: AbstractSection(parent, controller)\n, _type(type) {\n\tsetupContent(controller);\n}\n\nrpl::producer<QString> NotificationsType::title() {\n\tswitch (_type) {\n\tcase Notify::User: return tr::lng_notification_private_chats();\n\tcase Notify::Group: return tr::lng_notification_groups();\n\tcase Notify::Broadcast: return tr::lng_notification_channels();\n\t}\n\tUnexpected(\"Type in NotificationsType.\");\n}\n\nvoid NotificationsType::showFinished() {\n\tAbstractSection::showFinished();\n}\n\nType NotificationsType::Id(Notify type) {\n\treturn std::make_shared<Factory>(type);\n}\n\nvoid NotificationsType::setupContent(\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto container = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tconst auto type = _type;\n\tconst SectionBuildMethod buildMethod = [type](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tauto &lifetime = container->lifetime();\n\t\tconst auto highlights = lifetime.make_state<HighlightRegistry>();\n\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = Window::PausedIn(\n\t\t\t\tcontroller,\n\t\t\t\tWindow::GifPauseReason::Layer),\n\t\t\t.highlights = highlights,\n\t\t});\n\t\tBuildNotificationsTypeContent(builder, type);\n\n\t\tstd::move(showFinished) | rpl::on_next([=] {\n\t\t\tfor (const auto &[id, entry] : *highlights) {\n\t\t\t\tif (entry.widget) {\n\t\t\t\t\tcontroller->checkHighlightControl(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tentry.widget,\n\t\t\t\t\t\tbase::duplicate(entry.args));\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime);\n\t};\n\n\tbuild(container, buildMethod);\n\n\tUi::ResizeFitChild(this, container);\n}\n\nbool NotificationsEnabledForType(\n\t\tnot_null<::Main::Session*> session,\n\t\tNotify type) {\n\tconst auto settings = &session->data().notifySettings();\n\tconst auto until = settings->defaultSettings(type).muteUntil();\n\treturn until && (*until <= base::unixtime::now());\n}\n\nrpl::producer<bool> NotificationsEnabledForTypeValue(\n\t\tnot_null<::Main::Session*> session,\n\t\tNotify type) {\n\tconst auto settings = &session->data().notifySettings();\n\treturn rpl::single(\n\t\trpl::empty\n\t) | rpl::then(\n\t\tsettings->defaultUpdates(type)\n\t) | rpl::map([=] {\n\t\treturn NotificationsEnabledForType(session, type);\n\t});\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_notifications_type.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_common_session.h\"\n#include \"data/notify/data_notify_settings.h\"\n\nnamespace Settings {\n\nclass NotificationsType : public AbstractSection {\npublic:\n\tNotificationsType(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tData::DefaultNotify type);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\t[[nodiscard]] static Type Id(Data::DefaultNotify type);\n\n\t[[nodiscard]] Type id() const final override {\n\t\treturn Id(_type);\n\t}\n\n\tvoid showFinished() override;\n\nprivate:\n\tvoid setupContent(not_null<Window::SessionController*> controller);\n\n\tconst Data::DefaultNotify _type;\n\n};\n\n[[nodiscard]] bool NotificationsEnabledForType(\n\tnot_null<::Main::Session*> session,\n\tData::DefaultNotify type);\n\n[[nodiscard]] rpl::producer<bool> NotificationsEnabledForTypeValue(\n\tnot_null<::Main::Session*> session,\n\tData::DefaultNotify type);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_passkeys.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_passkeys.h\"\n\n#include \"base/unixtime.h\"\n#include \"core/application.h\"\n#include \"data/components/passkeys.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_session.h\"\n#include \"platform/platform_webauthn.h\"\n#include \"settings/cloud_password/settings_cloud_password_common.h\"\n#include \"settings/sections/settings_privacy_security.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/settings_common_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/custom_emoji_instance.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_channel_earn.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nusing namespace Builder;\n\nclass Passkeys : public Section<Passkeys> {\npublic:\n\tPasskeys(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\tvoid showFinished() override;\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\tconst Ui::RoundRect *bottomSkipRounding() const override {\n\t\treturn &_bottomSkipRounding;\n\t}\n\nprivate:\n\tvoid setupContent();\n\n\tconst not_null<Ui::VerticalLayout*> _container;\n\n\tQPointer<Ui::SettingsButton> _addButton;\n\tUi::RoundRect _bottomSkipRounding;\n\n\trpl::event_stream<> _showFinished;\n\n};\n\nvoid BuildPasskeysSection(\n\t\tSectionBuilder &builder,\n\t\tQPointer<Ui::SettingsButton> *addButton,\n\t\tFn<void(not_null<Ui::VerticalLayout*>)> setupList) {\n\tconst auto session = builder.session();\n\tconst auto controller = builder.controller();\n\n\tbuilder.add([=](const WidgetContext &ctx) {\n\t\tsetupList(ctx.container);\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n\n\tauto buttonShown = session->passkeys().requestList(\n\t) | rpl::map([session] { return session->passkeys().canRegister(); });\n\n\tconst auto button = builder.addButton({\n\t\t.id = u\"passkeys/create\"_q,\n\t\t.title = tr::lng_settings_passkeys_button(),\n\t\t.st = &st::settingsButtonActive,\n\t\t.icon = { &st::settingsIconPasskeys },\n\t\t.onClick = [controller, session] {\n\t\t\tcontroller->show(Box(PasskeysNoneBox, session));\n\t\t},\n\t\t.keywords = { u\"add\"_q, u\"register\"_q, u\"create\"_q },\n\t\t.highlight = { .rippleShape = true },\n\t\t.shown = std::move(buttonShown),\n\t});\n\tif (addButton) {\n\t\t*addButton = button;\n\t}\n\n\tbuilder.addSkip();\n\tbuilder.add([=](const WidgetContext &ctx) {\n\t\tconst auto label = Ui::AddDividerText(\n\t\t\tctx.container,\n\t\t\ttr::lng_settings_passkeys_button_about(\n\t\t\t\tlt_link,\n\t\t\t\ttr::lng_channel_earn_about_link(\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\trpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\t\t\ttr::rich\n\t\t\t\t) | rpl::map([](TextWithEntities text) {\n\t\t\t\t\treturn tr::link(std::move(text), u\"internal\"_q);\n\t\t\t\t}),\n\t\t\t\ttr::rich\n\t\t\t));\n\t\tlabel->setClickHandlerFilter([controller, session](const auto &...) {\n\t\t\tcontroller->show(Box(PasskeysNoneBox, session));\n\t\t\treturn false;\n\t\t});\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = Passkeys::Id(),\n\t.parentId = PrivacySecurityId(),\n\t.title = &tr::lng_settings_passkeys_title,\n\t.icon = &st::menuIconPermissions,\n}, [](SectionBuilder &builder) {\n\tBuildPasskeysSection(builder, nullptr, [](not_null<Ui::VerticalLayout*>) {});\n});\n\nPasskeys::Passkeys(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller)\n, _container(Ui::CreateChild<Ui::VerticalLayout>(this))\n, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {\n\tsetupContent();\n}\n\nvoid Passkeys::showFinished() {\n\tSection::showFinished();\n\t_showFinished.fire({});\n\tif (_addButton) {\n\t\tcontroller()->checkHighlightControl(\n\t\t\tu\"passkeys/create\"_q,\n\t\t\t_addButton,\n\t\t\t{ .rippleShape = true });\n\t}\n}\n\nrpl::producer<QString> Passkeys::title() {\n\treturn tr::lng_settings_passkeys_title();\n}\n\nvoid Passkeys::setupContent() {\n\tconst auto session = &controller()->session();\n\tconst auto addButton = &_addButton;\n\n\tCloudPassword::SetupHeader(\n\t\t_container,\n\t\tu\"passkeys\"_q,\n\t\t_showFinished.events(),\n\t\trpl::single(QString()),\n\t\ttr::lng_settings_passkeys_about());\n\n\tUi::AddSkip(_container);\n\n\tconst auto setupList = [=](not_null<Ui::VerticalLayout*> container) {\n\t\tconst auto passkeysListContainer = container->add(\n\t\t\tobject_ptr<Ui::VerticalLayout>(container));\n\n\t\tconst auto &st = st::peerListBoxItem;\n\t\tconst auto nameStyle = &st.nameStyle;\n\t\tconst auto ctrl = controller();\n\t\tconst auto rebuild = [=] {\n\t\t\twhile (passkeysListContainer->count()) {\n\t\t\t\tdelete passkeysListContainer->widgetAt(0);\n\t\t\t}\n\t\t\tfor (const auto &passkey : session->passkeys().list()) {\n\t\t\t\tconst auto button = passkeysListContainer->add(\n\t\t\t\t\tobject_ptr<Ui::AbstractButton>(passkeysListContainer));\n\t\t\t\tbutton->resize(button->width(), st.height);\n\t\t\t\tconst auto menu = Ui::CreateChild<Ui::IconButton>(\n\t\t\t\t\tbutton,\n\t\t\t\t\tst::themesMenuToggle);\n\t\t\t\tmenu->setClickedCallback([=] {\n\t\t\t\t\tconst auto popup = Ui::CreateChild<Ui::PopupMenu>(\n\t\t\t\t\t\tmenu,\n\t\t\t\t\t\tst::popupMenuWithIcons);\n\t\t\t\t\tconst auto handler = [=, id = passkey.id] {\n\t\t\t\t\t\tctrl->show(Ui::MakeConfirmBox({\n\t\t\t\t\t\t\t.text = rpl::combine(\n\t\t\t\t\t\t\t\ttr::lng_settings_passkeys_delete_sure_about(),\n\t\t\t\t\t\t\t\ttr::lng_settings_passkeys_delete_sure_about2()\n\t\t\t\t\t\t\t) | rpl::map([](QString a, QString b) {\n\t\t\t\t\t\t\t\treturn a + \"\\n\\n\" + b;\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t.confirmed = [=](Fn<void()> close) {\n\t\t\t\t\t\t\t\tsession->passkeys().deletePasskey(\n\t\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\t\tclose,\n\t\t\t\t\t\t\t\t\t[](QString) {});\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t.confirmText = tr::lng_box_delete(),\n\t\t\t\t\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t\t\t\t\t.title\n\t\t\t\t\t\t\t\t= tr::lng_settings_passkeys_delete_sure_title(),\n\t\t\t\t\t\t}));\n\t\t\t\t\t};\n\t\t\t\t\tUi::Menu::CreateAddActionCallback(popup)({\n\t\t\t\t\t\t.text = tr::lng_proxy_menu_delete(tr::now),\n\t\t\t\t\t\t.handler = handler,\n\t\t\t\t\t\t.icon = &st::menuIconDeleteAttention,\n\t\t\t\t\t\t.isAttention = true,\n\t\t\t\t\t});\n\t\t\t\t\tpopup->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);\n\t\t\t\t\tconst auto menuGlobal = menu->mapToGlobal(\n\t\t\t\t\t\tQPoint(menu->width(), menu->height()));\n\t\t\t\t\tpopup->popup(menuGlobal);\n\t\t\t\t});\n\t\t\t\tbutton->widthValue() | rpl::on_next([=](int width) {\n\t\t\t\t\tmenu->moveToRight(0, (st.height - menu->height()) / 2, width);\n\t\t\t\t}, button->lifetime());\n\t\t\t\tconst auto iconSize = st::settingsIconPasskeys.width();\n\t\t\t\tconst auto emoji = iconSize;\n\t\t\t\tconst auto iconLeft = st::settingsButton.iconLeft;\n\t\t\t\tauto emojiInstance = passkey.softwareEmojiId\n\t\t\t\t\t? session->data().customEmojiManager().create(\n\t\t\t\t\t\tpasskey.softwareEmojiId,\n\t\t\t\t\t\t[=] { button->update(); },\n\t\t\t\t\t\tData::CustomEmojiSizeTag::Large,\n\t\t\t\t\t\temoji)\n\t\t\t\t\t: nullptr;\n\t\t\t\tconst auto emojiPtr = emojiInstance.get();\n\t\t\t\tbutton->lifetime().add([emoji = std::move(emojiInstance)] {});\n\t\t\t\tconst auto formatDateTime = [](TimeId timestamp) {\n\t\t\t\t\tconst auto dt = base::unixtime::parse(timestamp);\n\t\t\t\t\treturn tr::lng_mediaview_date_time(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_date,\n\t\t\t\t\t\tlangDayOfMonthFull(dt.date()),\n\t\t\t\t\t\tlt_time,\n\t\t\t\t\t\tQLocale().toString(dt.time(), QLocale::ShortFormat));\n\t\t\t\t};\n\t\t\t\tconst auto date = (passkey.lastUsageDate > 0)\n\t\t\t\t\t? tr::lng_settings_passkeys_last_used(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_date,\n\t\t\t\t\t\tformatDateTime(passkey.lastUsageDate))\n\t\t\t\t\t: tr::lng_settings_passkeys_created(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_date,\n\t\t\t\t\t\tformatDateTime(passkey.date));\n\t\t\t\tconst auto nameText = button->lifetime().make_state<\n\t\t\t\t\tUi::Text::String>(\n\t\t\t\t\t\t*nameStyle,\n\t\t\t\t\t\tpasskey.name.isEmpty()\n\t\t\t\t\t\t\t? tr::lng_settings_passkey_unknown(tr::now)\n\t\t\t\t\t\t\t: passkey.name);\n\t\t\t\tconst auto dateText = button->lifetime().make_state<\n\t\t\t\t\tUi::Text::String>(st::defaultTextStyle, date);\n\t\t\t\tbutton->paintOn([=](QPainter &p) {\n\t\t\t\t\tconst auto iconTop = (st.height - iconSize) / 2;\n\t\t\t\t\tif (emojiPtr) {\n\t\t\t\t\t\temojiPtr->paint(p, {\n\t\t\t\t\t\t\t.textColor = st.nameFg->c,\n\t\t\t\t\t\t\t.now = crl::now(),\n\t\t\t\t\t\t\t.position = QPoint(iconLeft, iconTop),\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst auto w = button->width();\n\t\t\t\t\t\tst::settingsIconPasskeys.paint(p, iconLeft, iconTop, w);\n\t\t\t\t\t}\n\t\t\t\t\tconst auto textLeft = st::settingsButton.padding.left();\n\t\t\t\t\tconst auto textWidth = button->width() - textLeft\n\t\t\t\t\t\t- st::settingsButton.padding.right();\n\t\t\t\t\tp.setPen(st.nameFg);\n\t\t\t\t\tnameText->draw(p, {\n\t\t\t\t\t\t.position = { textLeft, st.namePosition.y() },\n\t\t\t\t\t\t.outerWidth = button->width(),\n\t\t\t\t\t\t.availableWidth = textWidth,\n\t\t\t\t\t\t.elisionLines = 1,\n\t\t\t\t\t});\n\t\t\t\t\tp.setPen(st.statusFg);\n\t\t\t\t\tdateText->draw(p, {\n\t\t\t\t\t\t.position = { textLeft, st.statusPosition.y() },\n\t\t\t\t\t\t.outerWidth = button->width(),\n\t\t\t\t\t\t.availableWidth = textWidth,\n\t\t\t\t\t\t.elisionLines = 1,\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t\tbutton->showChildren();\n\t\t\t}\n\t\t\tpasskeysListContainer->showChildren();\n\t\t\tpasskeysListContainer->resizeToWidth(container->width());\n\t\t};\n\n\t\tsession->passkeys().requestList(\n\t\t) | rpl::on_next(rebuild, container->lifetime());\n\t\trebuild();\n\t};\n\n\tconst SectionBuildMethod buildMethod = [=](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tconst auto isPaused = Window::PausedIn(\n\t\t\tcontroller,\n\t\t\tWindow::GifPauseReason::Layer);\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = isPaused,\n\t\t});\n\n\t\tBuildPasskeysSection(builder, addButton, setupList);\n\t};\n\n\tbuild(_container, buildMethod);\n\n\twidthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_container->resizeToWidth(width);\n\t}, _container->lifetime());\n\n\t_container->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tresize(width(), height);\n\t}, _container->lifetime());\n}\n\n} // namespace\n\nvoid PasskeysNoneBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<::Main::Session*> session) {\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setNoContentMargin(true);\n\tbox->setCloseByEscape(true);\n\tbox->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });\n\n\tconst auto content = box->verticalLayout().get();\n\n\tUi::AddSkip(content);\n\t{\n\t\tconst auto &size = st::settingsCloudPasswordIconSize;\n\t\tauto icon = CreateLottieIcon(\n\t\t\tcontent,\n\t\t\t{ .name = u\"passkeys\"_q, .sizeOverride = { size, size } },\n\t\t\tst::settingLocalPasscodeIconPadding);\n\t\tconst auto animate = std::move(icon.animate);\n\t\tbox->addRow(std::move(icon.widget), style::al_top);\n\t\tbox->showFinishes() | rpl::take(1) | rpl::on_next([=] {\n\t\t\tanimate(anim::repeat::once);\n\t\t}, content->lifetime());\n\t}\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_settings_passkeys_none_title(),\n\t\t\tst::boxTitle),\n\t\tstyle::al_top);\n\tUi::AddSkip(content);\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_settings_passkeys_none_about(),\n\t\t\tst::channelEarnLearnDescription),\n\t\tstyle::al_top);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\t{\n\t\tconst auto padding = QMargins(\n\t\t\tst::settingsButton.padding.left(),\n\t\t\tst::boxRowPadding.top(),\n\t\t\tst::boxRowPadding.right(),\n\t\t\tst::boxRowPadding.bottom());\n\t\tconst auto iconLeft = st::settingsButton.iconLeft;\n\t\tconst auto addEntry = [&](\n\t\t\t\trpl::producer<QString> title,\n\t\t\t\trpl::producer<QString> about,\n\t\t\t\tconst style::icon &icon) {\n\t\t\tconst auto top = content->add(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tcontent,\n\t\t\t\t\tstd::move(title),\n\t\t\t\t\tst::channelEarnSemiboldLabel),\n\t\t\t\tpadding);\n\t\t\tUi::AddSkip(content, st::channelEarnHistoryThreeSkip);\n\t\t\tcontent->add(\n\t\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t\tcontent,\n\t\t\t\t\tstd::move(about),\n\t\t\t\t\tst::channelEarnHistoryRecipientLabel),\n\t\t\t\tpadding);\n\t\t\tconst auto left = Ui::CreateChild<Ui::RpWidget>(\n\t\t\t\tbox->verticalLayout().get());\n\t\t\tleft->paintRequest(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tauto p = Painter(left);\n\t\t\t\ticon.paint(p, 0, 0, left->width());\n\t\t\t}, left->lifetime());\n\t\t\tleft->resize(icon.size());\n\t\t\ttop->geometryValue(\n\t\t\t) | rpl::on_next([=](const QRect &g) {\n\t\t\t\tleft->moveToLeft(\n\t\t\t\t\ticonLeft,\n\t\t\t\t\tg.top() + st::channelEarnHistoryThreeSkip);\n\t\t\t}, left->lifetime());\n\t\t};\n\t\taddEntry(\n\t\t\ttr::lng_settings_passkeys_none_info1_title(),\n\t\t\ttr::lng_settings_passkeys_none_info1_about(),\n\t\t\tst::settingsIconPasskeysAboutIcon1);\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\t\taddEntry(\n\t\t\ttr::lng_settings_passkeys_none_info2_title(),\n\t\t\ttr::lng_settings_passkeys_none_info2_about(),\n\t\t\tst::settingsIconPasskeysAboutIcon2);\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\t\taddEntry(\n\t\t\ttr::lng_settings_passkeys_none_info3_title(),\n\t\t\ttr::lng_settings_passkeys_none_info3_about(),\n\t\t\tst::settingsIconPasskeysAboutIcon3);\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\t}\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\t{\n\t\tconst auto &st = st::premiumPreviewDoubledLimitsBox;\n\t\tconst auto canRegister = session->passkeys().canRegister();\n\t\tbox->setStyle(st);\n\t\tauto button = object_ptr<Ui::RoundButton>(\n\t\t\tbox,\n\t\t\tcanRegister\n\t\t\t\t? tr::lng_settings_passkeys_none_button()\n\t\t\t\t: tr::lng_settings_passkeys_none_button_unsupported(),\n\t\t\tst::defaultActiveButton);\n\t\tconst auto createButton = button.data();\n\t\tbutton->resizeToWidth(box->width()\n\t\t\t- st.buttonPadding.left()\n\t\t\t- st.buttonPadding.left());\n\t\tbutton->setClickedCallback([=] {\n\t\t\tsession->passkeys().initRegistration([=](\n\t\t\t\t\tconst Data::Passkey::RegisterData &data) {\n\t\t\t\tPlatform::WebAuthn::RegisterKey(data, [=](\n\t\t\t\t\t\tPlatform::WebAuthn::RegisterResult result) {\n\t\t\t\t\tif (!result.success) {\n\t\t\t\t\t\tusing Error = Platform::WebAuthn::Error;\n\t\t\t\t\t\tif (result.error == Error::UnsignedBuild) {\n\t\t\t\t\t\t\tbox->uiShow()->showToast(\n\t\t\t\t\t\t\t\ttr::lng_settings_passkeys_unsigned_error(\n\t\t\t\t\t\t\t\t\ttr::now));\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tsession->passkeys().registerPasskey(result, [=] {\n\t\t\t\t\t\tbox->closeBox();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\t\tif (!canRegister) {\n\t\t\tbutton->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\tbutton->setTextFgOverride(\n\t\t\t\tanim::with_alpha(button->st().textFg->c, 0.5));\n\t\t}\n\t\tbox->addButton(std::move(button));\n\n\t\tbox->showFinishes(\n\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\tif (const auto window = Core::App().findWindow(box)) {\n\t\t\t\twindow->checkHighlightControl(\n\t\t\t\t\tu\"passkeys/create\"_q,\n\t\t\t\t\tcreateButton,\n\t\t\t\t\t{\n\t\t\t\t\t\t.color = &st::activeButtonFg,\n\t\t\t\t\t\t.opacity = 0.6,\n\t\t\t\t\t\t.rippleShape = true,\n\t\t\t\t\t\t.scroll = false,\n\t\t\t\t\t});\n\t\t\t}\n\t\t}, box->lifetime());\n\t}\n}\n\nType PasskeysId() {\n\treturn Passkeys::Id();\n}\n\nnamespace Builder {\n\nSectionBuildMethod PasskeysSection = kMeta.build;\n\n} // namespace Builder\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_passkeys.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Ui {\nclass GenericBox;\n} // namespace Ui\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Settings {\n\nvoid PasskeysNoneBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<::Main::Session*> session);\n\nType PasskeysId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_premium.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_premium.h\"\n\n#include \"boxes/premium_preview_box.h\"\n#include \"boxes/sticker_set_box.h\"\n#include \"chat_helpers/stickers_lottie.h\" // LottiePlayerFromDocument.\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/local_url_handlers.h\" // Core::TryConvertUrlToLocal.\n#include \"core/ui_integration.h\" // TextContext.\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\" // SerializeCustomEmojiId.\n#include \"data/stickers/data_stickers.h\"\n#include \"history/view/media/history_view_sticker.h\" // EmojiSize.\n#include \"history/view/media/history_view_sticker_player.h\"\n#include \"info/info_wrap_widget.h\" // Info::Wrap.\n#include \"info/profile/info_profile_values.h\"\n#include \"info/settings/info_settings_widget.h\" // SectionCustomTopBarData.\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common_session.h\"\n#include \"ui/abstract_button.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"ui/effects/gradient.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/effects/premium_stars_colored.h\"\n#include \"ui/effects/premium_top_bar.h\"\n#include \"ui/controls/swipe_handler.h\"\n#include \"ui/controls/swipe_handler_data.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/checkbox.h\" // Ui::RadiobuttonGroup.\n#include \"ui/widgets/gradient_round_button.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/new_badges.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_session_controller_link_info.h\"\n#include \"base/unixtime.h\"\n#include \"apiwrap.h\"\n#include \"api/api_premium.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Settings {\nnamespace {\n\nusing namespace Builder;\n\nusing SectionCustomTopBarData = Info::Settings::SectionCustomTopBarData;\n\n[[nodiscard]] Data::PremiumSubscriptionOptions SubscriptionOptionsForRows(\n\t\tData::PremiumSubscriptionOptions result) {\n\tfor (auto &option : result) {\n\t\tconst auto perYear = option.costPerYear;\n\t\tconst auto perMonth = option.costPerMonth;\n\n\t\toption.costPerYear = tr::lng_premium_gift_per(\n\t\t\ttr::now,\n\t\t\tlt_cost,\n\t\t\tperMonth);\n\t\toption.costPerMonth = tr::lng_premium_subscribe_total(\n\t\t\ttr::now,\n\t\t\tlt_cost,\n\t\t\tperYear);\n\n\t\tif (option.duration == tr::lng_months(tr::now, lt_count, 1)) {\n\t\t\toption.costPerMonth = QString();\n\t\t\toption.costNoDiscount = QString();\n\t\t\toption.duration = tr::lng_premium_subscribe_months_1(tr::now);\n\t\t} else if (option.duration == tr::lng_months(tr::now, lt_count, 6)) {\n\t\t\toption.duration = tr::lng_premium_subscribe_months_6(tr::now);\n\t\t} else if (option.duration == tr::lng_years(tr::now, lt_count, 1)) {\n\t\t\toption.duration = tr::lng_premium_subscribe_months_12(tr::now);\n\t\t} else if (option.duration == tr::lng_years(tr::now, lt_count, 2)) {\n\t\t\toption.duration = tr::lng_premium_subscribe_months_24(tr::now);\n\t\t}\n\t}\n\treturn result;\n}\n\n[[nodiscard]] int TopTransitionSkip() {\n\treturn (st::settingsButton.padding.top()\n\t\t+ st::settingsPremiumRowTitlePadding.top()) / 2;\n}\n\nnamespace Ref {\nnamespace Gift {\n\nstruct Data {\n\tPeerId peerId;\n\tint days = 0;\n\tbool me = false;\n\n\texplicit operator bool() const {\n\t\treturn peerId != 0;\n\t}\n};\n\n[[nodiscard]] QString Serialize(const Data &gift) {\n\treturn QString::number(gift.peerId.value)\n\t\t+ ':'\n\t\t+ QString::number(gift.days)\n\t\t+ ':'\n\t\t+ QString::number(gift.me ? 1 : 0);\n}\n\n[[nodiscard]] Data Parse(QStringView data) {\n\tconst auto components = data.split(':');\n\tif (components.size() != 3) {\n\t\treturn {};\n\t}\n\treturn {\n\t\t.peerId = PeerId(components[0].toULongLong()),\n\t\t.days = components[1].toInt(),\n\t\t.me = (components[2].toInt() == 1),\n\t};\n}\n\n} // namespace Gift\n\nnamespace EmojiStatus {\n\nstruct Data {\n\tPeerId peerId;\n\n\texplicit operator bool() const {\n\t\treturn peerId != 0;\n\t}\n};\n\n[[nodiscard]] QString Serialize(const Data &gift) {\n\treturn QString(\"profile_:%1\").arg(QString::number(gift.peerId.value));\n}\n\n[[nodiscard]] Data Parse(QStringView data) {\n\tif (data.startsWith(u\"profile_:\"_q)) {\n\t\tconst auto components = data.split(':');\n\t\tif (components.size() != 2) {\n\t\t\treturn {};\n\t\t}\n\t\treturn {\n\t\t\t.peerId = PeerId(components[1].toULongLong()),\n\t\t};\n\t}\n\treturn {};\n}\n\n} // namespace EmojiStatus\n\nnamespace PremiumGift {\n\nstruct Data {\n\tDocumentId documentId = 0;\n\tint perUserTotal = 0;\n\n\texplicit operator bool() const {\n\t\treturn documentId != 0;\n\t}\n};\n\n[[nodiscard]] QString Serialize(const Data &gift) {\n\treturn u\"premiumgift_:%1,peruser_:%2\"_q\n\t\t.arg(gift.documentId)\n\t\t.arg(gift.perUserTotal);\n}\n\n[[nodiscard]] Data Parse(QStringView data) {\n\tif (data.startsWith(u\"premiumgift_:\"_q)) {\n\t\tconst auto components = data.split(',');\n\t\tif (components.size() != 2) {\n\t\t\treturn {};\n\t\t}\n\t\tconst auto first = components[0].split(':');\n\t\tconst auto second = components[1].split(':');\n\t\tif (first.size() != 2 || second.size() != 2) {\n\t\t\treturn {};\n\t\t}\n\t\treturn {\n\t\t\t.documentId = DocumentId(first[1].toULongLong()),\n\t\t\t.perUserTotal = second[1].toInt(),\n\t\t};\n\t}\n\treturn {};\n}\n\n} // namespace PremiumGift\n\n} // namespace Ref\n\nstruct Entry {\n\tconst style::icon *icon;\n\trpl::producer<QString> title;\n\trpl::producer<QString> description;\n\tPremiumFeature section = PremiumFeature::DoubleLimits;\n\tbool newBadge = false;\n};\n\nstruct PremiumState {\n\tQString ref;\n\tFn<void(bool)> setPaused;\n\tstd::shared_ptr<Ui::RadiobuttonGroup> radioGroup;\n\tbase::flat_map<QString, QPointer<Ui::AbstractButton>> featureButtons;\n\n};\n\nusing Order = std::vector<QString>;\n\n[[nodiscard]] Order FallbackOrder() {\n\treturn Order{\n\t\tu\"stories\"_q,\n\t\tu\"more_upload\"_q,\n\t\tu\"double_limits\"_q,\n\t\tu\"last_seen\"_q,\n\t\tu\"voice_to_text\"_q,\n\t\tu\"faster_download\"_q,\n\t\tu\"translations\"_q,\n\t\tu\"animated_emoji\"_q,\n\t\tu\"emoji_status\"_q,\n\t\tu\"saved_tags\"_q,\n\t\t//u\"peer_colors\"_q,\n\t\tu\"wallpapers\"_q,\n\t\tu\"profile_badge\"_q,\n\t\tu\"message_privacy\"_q,\n\t\tu\"advanced_chat_management\"_q,\n\t\tu\"no_ads\"_q,\n\t\t//u\"app_icons\"_q,\n\t\tu\"infinite_reactions\"_q,\n\t\tu\"animated_userpics\"_q,\n\t\tu\"premium_stickers\"_q,\n\t\tu\"business\"_q,\n\t\tu\"effects\"_q,\n\t\tu\"ai_compose\"_q,\n\t};\n}\n\n[[nodiscard]] base::flat_map<QString, Entry> EntryMap() {\n\treturn base::flat_map<QString, Entry>{\n\t\t{\n\t\t\tu\"saved_tags\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconTags,\n\t\t\t\ttr::lng_premium_summary_subtitle_tags_for_messages(),\n\t\t\t\ttr::lng_premium_summary_about_tags_for_messages(),\n\t\t\t\tPremiumFeature::TagsForMessages,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"last_seen\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconLastSeen,\n\t\t\t\ttr::lng_premium_summary_subtitle_last_seen(),\n\t\t\t\ttr::lng_premium_summary_about_last_seen(),\n\t\t\t\tPremiumFeature::LastSeen,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"message_privacy\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconPrivacy,\n\t\t\t\ttr::lng_premium_summary_subtitle_message_privacy(),\n\t\t\t\ttr::lng_premium_summary_about_message_privacy(),\n\t\t\t\tPremiumFeature::MessagePrivacy,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"wallpapers\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconWallpapers,\n\t\t\t\ttr::lng_premium_summary_subtitle_wallpapers(),\n\t\t\t\ttr::lng_premium_summary_about_wallpapers(),\n\t\t\t\tPremiumFeature::Wallpapers,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"peer_colors\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconPeerColors,\n\t\t\t\ttr::lng_premium_summary_subtitle_peer_colors(),\n\t\t\t\ttr::lng_premium_summary_about_peer_colors(),\n\t\t\t\tPremiumFeature::PeerColors,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"stories\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconStories,\n\t\t\t\ttr::lng_premium_summary_subtitle_stories(),\n\t\t\t\ttr::lng_premium_summary_about_stories(),\n\t\t\t\tPremiumFeature::Stories,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"double_limits\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconDouble,\n\t\t\t\ttr::lng_premium_summary_subtitle_double_limits(),\n\t\t\t\ttr::lng_premium_summary_about_double_limits(),\n\t\t\t\tPremiumFeature::DoubleLimits,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"more_upload\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconFiles,\n\t\t\t\ttr::lng_premium_summary_subtitle_more_upload(),\n\t\t\t\ttr::lng_premium_summary_about_more_upload(),\n\t\t\t\tPremiumFeature::MoreUpload,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"faster_download\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconSpeed,\n\t\t\t\ttr::lng_premium_summary_subtitle_faster_download(),\n\t\t\t\ttr::lng_premium_summary_about_faster_download(),\n\t\t\t\tPremiumFeature::FasterDownload,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"voice_to_text\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconVoice,\n\t\t\t\ttr::lng_premium_summary_subtitle_voice_to_text(),\n\t\t\t\ttr::lng_premium_summary_about_voice_to_text(),\n\t\t\t\tPremiumFeature::VoiceToText,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"no_ads\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconChannelsOff,\n\t\t\t\ttr::lng_premium_summary_subtitle_no_ads(),\n\t\t\t\ttr::lng_premium_summary_about_no_ads(),\n\t\t\t\tPremiumFeature::NoAds,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"emoji_status\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconStatus,\n\t\t\t\ttr::lng_premium_summary_subtitle_emoji_status(),\n\t\t\t\ttr::lng_premium_summary_about_emoji_status(),\n\t\t\t\tPremiumFeature::EmojiStatus,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"infinite_reactions\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconLike,\n\t\t\t\ttr::lng_premium_summary_subtitle_infinite_reactions(),\n\t\t\t\ttr::lng_premium_summary_about_infinite_reactions(),\n\t\t\t\tPremiumFeature::InfiniteReactions,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"premium_stickers\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsIconStickers,\n\t\t\t\ttr::lng_premium_summary_subtitle_premium_stickers(),\n\t\t\t\ttr::lng_premium_summary_about_premium_stickers(),\n\t\t\t\tPremiumFeature::Stickers,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"animated_emoji\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsIconEmoji,\n\t\t\t\ttr::lng_premium_summary_subtitle_animated_emoji(),\n\t\t\t\ttr::lng_premium_summary_about_animated_emoji(),\n\t\t\t\tPremiumFeature::AnimatedEmoji,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"advanced_chat_management\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsIconChat,\n\t\t\t\ttr::lng_premium_summary_subtitle_advanced_chat_management(),\n\t\t\t\ttr::lng_premium_summary_about_advanced_chat_management(),\n\t\t\t\tPremiumFeature::AdvancedChatManagement,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"profile_badge\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconStar,\n\t\t\t\ttr::lng_premium_summary_subtitle_profile_badge(),\n\t\t\t\ttr::lng_premium_summary_about_profile_badge(),\n\t\t\t\tPremiumFeature::ProfileBadge,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"animated_userpics\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconPlay,\n\t\t\t\ttr::lng_premium_summary_subtitle_animated_userpics(),\n\t\t\t\ttr::lng_premium_summary_about_animated_userpics(),\n\t\t\t\tPremiumFeature::AnimatedUserpics,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"translations\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconTranslations,\n\t\t\t\ttr::lng_premium_summary_subtitle_translation(),\n\t\t\t\ttr::lng_premium_summary_about_translation(),\n\t\t\t\tPremiumFeature::RealTimeTranslation,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"business\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconBusiness,\n\t\t\t\ttr::lng_premium_summary_subtitle_business(),\n\t\t\t\ttr::lng_premium_summary_about_business(),\n\t\t\t\tPremiumFeature::Business,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"effects\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconEffects,\n\t\t\t\ttr::lng_premium_summary_subtitle_effects(),\n\t\t\t\ttr::lng_premium_summary_about_effects(),\n\t\t\t\tPremiumFeature::Effects,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"todo\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconChecklist,\n\t\t\t\ttr::lng_premium_summary_subtitle_todo_lists(),\n\t\t\t\ttr::lng_premium_summary_about_todo_lists(),\n\t\t\t\tPremiumFeature::TodoLists,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"no_forwards\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconNoForwards,\n\t\t\t\ttr::lng_premium_summary_subtitle_no_forwards(),\n\t\t\t\ttr::lng_premium_summary_about_no_forwards(),\n\t\t\t\tPremiumFeature::NoForwards,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tu\"ai_compose\"_q,\n\t\t\tEntry{\n\t\t\t\t&st::settingsPremiumIconAiCompose,\n\t\t\t\ttr::lng_premium_summary_subtitle_ai_compose(),\n\t\t\t\ttr::lng_premium_summary_about_ai_compose(),\n\t\t\t\tPremiumFeature::AiCompose,\n\t\t\t\ttrue,\n\t\t\t},\n\t\t},\n\t};\n}\n\nvoid SendAppLog(\n\t\tnot_null<::Main::Session*> session,\n\t\tconst QString &type,\n\t\tconst MTPJSONValue &data) {\n\tconst auto now = double(base::unixtime::now())\n\t\t+ (QTime::currentTime().msec() / 1000.);\n\tsession->api().request(MTPhelp_SaveAppLog(\n\t\tMTP_vector<MTPInputAppEvent>(1, MTP_inputAppEvent(\n\t\t\tMTP_double(now),\n\t\t\tMTP_string(type),\n\t\t\tMTP_long(0),\n\t\t\tdata\n\t\t))\n\t)).send();\n}\n\n[[nodiscard]] QString ResolveRef(const QString &ref) {\n\treturn ref.isEmpty() ? \"settings\" : ref;\n}\n\nvoid SendScreenShow(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst std::vector<QString> &order,\n\t\tconst QString &ref) {\n\tauto list = QVector<MTPJSONValue>();\n\tlist.reserve(order.size());\n\tfor (const auto &element : order) {\n\t\tlist.push_back(MTP_jsonString(MTP_string(element)));\n\t}\n\tauto values = QVector<MTPJSONObjectValue>{\n\t\tMTP_jsonObjectValue(\n\t\t\tMTP_string(\"premium_promo_order\"),\n\t\t\tMTP_jsonArray(MTP_vector<MTPJSONValue>(std::move(list)))),\n\t\tMTP_jsonObjectValue(\n\t\t\tMTP_string(\"source\"),\n\t\t\tMTP_jsonString(MTP_string(ResolveRef(ref)))),\n\t};\n\tconst auto data = MTP_jsonObject(\n\t\tMTP_vector<MTPJSONObjectValue>(std::move(values)));\n\tSendAppLog(\n\t\t&controller->session(),\n\t\t\"premium.promo_screen_show\",\n\t\tdata);\n}\n\nvoid SendScreenAccept(not_null<Window::SessionController*> controller) {\n\tSendAppLog(\n\t\t&controller->session(),\n\t\t\"premium.promo_screen_accept\",\n\t\tMTP_jsonNull());\n}\n\nclass EmojiStatusTopBar final {\npublic:\n\tEmojiStatusTopBar(\n\t\tnot_null<DocumentData*> document,\n\t\tFn<void(QRect)> callback,\n\t\tQSizeF size);\n\n\tvoid setCenter(QPointF position);\n\tvoid setPaused(bool paused);\n\tvoid paint(QPainter &p);\n\nprivate:\n\tQRectF _rect;\n\tstd::shared_ptr<Data::DocumentMedia> _media;\n\tstd::unique_ptr<HistoryView::StickerPlayer> _player;\n\tbool _paused = false;\n\trpl::lifetime _lifetime;\n\n};\n\nEmojiStatusTopBar::EmojiStatusTopBar(\n\tnot_null<DocumentData*> document,\n\tFn<void(QRect)> callback,\n\tQSizeF size)\n: _rect(QPointF(), size) {\n\tconst auto sticker = document->sticker();\n\tAssert(sticker != nullptr);\n\t_media = document->createMediaView();\n\t_media->checkStickerLarge();\n\t_media->goodThumbnailWanted();\n\n\trpl::single() | rpl::then(\n\t\tdocument->owner().session().downloaderTaskFinished()\n\t) | rpl::on_next([=] {\n\t\tif (!_media->loaded()) {\n\t\t\treturn;\n\t\t}\n\t\t_lifetime.destroy();\n\t\tif (sticker->isLottie()) {\n\t\t\t_player = std::make_unique<HistoryView::LottiePlayer>(\n\t\t\t\tChatHelpers::LottiePlayerFromDocument(\n\t\t\t\t_media.get(),\n\t\t\t\tChatHelpers::StickerLottieSize::EmojiInteractionReserved7, //\n\t\t\t\tsize.toSize(),\n\t\t\t\tLottie::Quality::High));\n\t\t} else if (sticker->isWebm()) {\n\t\t\t_player = std::make_unique<HistoryView::WebmPlayer>(\n\t\t\t\t_media->owner()->location(),\n\t\t\t\t_media->bytes(),\n\t\t\t\tsize.toSize());\n\t\t} else if (sticker) {\n\t\t\t_player = std::make_unique<HistoryView::StaticStickerPlayer>(\n\t\t\t\t_media->owner()->location(),\n\t\t\t\t_media->bytes(),\n\t\t\t\tsize.toSize());\n\t\t}\n\t\tif (_player) {\n\t\t\t_player->setRepaintCallback([=] { callback(_rect.toRect()); });\n\t\t} else {\n\t\t\tcallback(_rect.toRect());\n\t\t}\n\t}, _lifetime);\n}\n\nvoid EmojiStatusTopBar::setCenter(QPointF position) {\n\tconst auto size = _rect.size();\n\tconst auto shift = QPointF(size.width() / 2., size.height() / 2.);\n\t_rect = QRectF(QPointF(position - shift), QPointF(position + shift));\n}\n\nvoid EmojiStatusTopBar::setPaused(bool paused) {\n\t_paused = paused;\n}\n\nvoid EmojiStatusTopBar::paint(QPainter &p) {\n\tif (_player && _player->ready()) {\n\t\tconst auto frame = _player->frame(\n\t\t\t_rect.size().toSize(),\n\t\t\t(_media->owner()->emojiUsesTextColor()\n\t\t\t\t? st::profileVerifiedCheckBg->c\n\t\t\t\t: QColor(0, 0, 0, 0)),\n\t\t\tfalse,\n\t\t\tcrl::now(),\n\t\t\t_paused || On(PowerSaving::kEmojiStatus));\n\n\t\tp.drawImage(_rect.toRect(), frame.image);\n\t\tif (!_paused) {\n\t\t\t_player->markFrameShown();\n\t\t}\n\t}\n}\n\nenum class TopBarWithStickerType {\n\tEmojiStatus,\n\tPremiumGift,\n};\n\nstruct TopBarWithStickerArgs {\n\trpl::producer<DocumentData*> stickerValue;\n\trpl::producer<QString> nameValue;\n\trpl::producer<TextWithEntities> aboutValue;\n\tTopBarWithStickerType type = TopBarWithStickerType::EmojiStatus;\n};\n\nclass TopBarWithSticker final : public Ui::Premium::TopBarAbstract {\npublic:\n\tTopBarWithSticker(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<> showFinished);\n\tTopBarWithSticker(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tTopBarWithStickerArgs args,\n\t\trpl::producer<> showFinished);\n\n\tvoid setPaused(bool paused) override;\n\tvoid setTextPosition(int x, int y) override;\n\n\trpl::producer<int> additionalHeight() const override;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid updateTitle(\n\t\tDocumentData *document,\n\t\tconst TextWithEntities &name,\n\t\tnot_null<Window::SessionController*> controller);\n\tvoid updateAbout(\n\t\tDocumentData *document,\n\t\tconst TextWithEntities &about) const;\n\n\tTopBarWithStickerType _type = TopBarWithStickerType::EmojiStatus;\n\tobject_ptr<Ui::RpWidget> _content;\n\tobject_ptr<Ui::FlatLabel> _title;\n\tobject_ptr<Ui::FlatLabel> _about;\n\tUi::Premium::ColoredMiniStars _ministars;\n\n\tstruct {\n\t\tobject_ptr<Ui::RpWidget> widget;\n\t\tUi::Text::String text;\n\t\tUi::Animations::Simple animation;\n\t\tbool shown = false;\n\t\tQPoint position;\n\t} _smallTop;\n\n\tstd::unique_ptr<EmojiStatusTopBar> _emojiStatus;\n\tQImage _imageStar;\n\n\tQRectF _starRect;\n\n};\n\nTopBarWithSticker::TopBarWithSticker(\n\tnot_null<QWidget*> parent,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\trpl::producer<> showFinished)\n: TopBarWithSticker(parent, controller, {\n\t.stickerValue = Info::Profile::EmojiStatusIdValue(\n\t\tpeer\n\t) | rpl::map([=](EmojiStatusId id) -> DocumentData* {\n\t\tconst auto documentId = id.collectible\n\t\t\t? id.collectible->documentId\n\t\t\t: id.documentId;\n\t\tconst auto document = documentId\n\t\t\t? controller->session().data().document(documentId).get()\n\t\t\t: nullptr;\n\t\treturn (document && document->sticker()) ? document : nullptr;\n\t}),\n\t.nameValue = Info::Profile::NameValue(peer),\n\t.type = TopBarWithStickerType::EmojiStatus,\n}, std::move(showFinished)) {\n}\n\nTopBarWithSticker::TopBarWithSticker(\n\tnot_null<QWidget*> parent,\n\tnot_null<Window::SessionController*> controller,\n\tTopBarWithStickerArgs args,\n\trpl::producer<> showFinished)\n: TopBarAbstract(parent, st::userPremiumCover)\n, _type(args.type)\n, _content(this)\n, _title(_content, st::settingsPremiumUserTitle)\n, _about(_content, st::userPremiumCover.about)\n, _ministars(_content, true)\n, _smallTop({\n\t.widget = object_ptr<Ui::RpWidget>(this),\n\t.text = Ui::Text::String(\n\t\tst::boxTitle.style,\n\t\ttr::lng_premium_summary_title(tr::now)),\n}) {\n\t_starRect = TopBarAbstract::starRect(1., 1.);\n\n\tif (_type == TopBarWithStickerType::PremiumGift) {\n\t\t_ministars.setColorOverride(Ui::Premium::CreditsIconGradientStops());\n\t}\n\n\trpl::single() | rpl::then(\n\t\tstyle::PaletteChanged()\n\t) | rpl::on_next([=] {\n\t\tTopBarAbstract::computeIsDark();\n\t\tupdate();\n\t}, lifetime());\n\n\trpl::combine(\n\t\t(args.stickerValue\n\t\t\t? std::move(args.stickerValue)\n\t\t\t: rpl::single((DocumentData*)nullptr)),\n\t\t(args.nameValue\n\t\t\t? std::move(args.nameValue)\n\t\t\t: rpl::single(QString())),\n\t\t(args.aboutValue\n\t\t\t? std::move(args.aboutValue)\n\t\t\t: rpl::single(TextWithEntities()))\n\t) | rpl::on_next([=](\n\t\t\tDocumentData *document,\n\t\t\tconst QString &name,\n\t\t\tconst TextWithEntities &about) {\n\t\tif (document) {\n\t\t\t_emojiStatus = std::make_unique<EmojiStatusTopBar>(\n\t\t\t\tdocument,\n\t\t\t\t[=](QRect r) { _content->update(std::move(r)); },\n\t\t\t\tHistoryView::Sticker::EmojiSize());\n\t\t\t_imageStar = QImage();\n\t\t} else {\n\t\t\t_emojiStatus = nullptr;\n\t\t\t_imageStar = Ui::Premium::GenerateStarForLightTopBar(_starRect);\n\t\t}\n\n\t\tupdateTitle(document, { name }, controller);\n\t\tupdateAbout(document, about);\n\n\t\tauto event = QResizeEvent(size(), size());\n\t\tresizeEvent(&event);\n\t\tupdate();\n\t}, lifetime());\n\n\t_title->naturalWidthValue() | rpl::on_next([=] {\n\t\t_title->resizeToNaturalWidth(st::settingsPremiumUserTitle.minWidth);\n\t}, _title->lifetime());\n\n\t_about->naturalWidthValue() | rpl::on_next([=] {\n\t\t_about->resizeToNaturalWidth(st::userPremiumCover.about.minWidth);\n\t}, _about->lifetime());\n\n\trpl::combine(\n\t\t_title->sizeValue(),\n\t\t_about->sizeValue(),\n\t\t_content->sizeValue()\n\t) | rpl::on_next([=](\n\t\t\tconst QSize &titleSize,\n\t\t\tconst QSize &aboutSize,\n\t\t\tconst QSize &size) {\n\t\tconst auto rect = TopBarAbstract::starRect(1., 1.);\n\t\tconst auto &padding = st::settingsPremiumUserTitlePadding;\n\t\t_title->moveToLeft(\n\t\t\t(size.width() - titleSize.width()) / 2,\n\t\t\trect.top() + rect.height() + padding.top());\n\t\t_about->moveToLeft(\n\t\t\t(size.width() - aboutSize.width()) / 2,\n\t\t\t_title->y() + titleSize.height() + padding.bottom());\n\n\t\tconst auto aboutBottom = _about->y() + _about->height();\n\t\tconst auto height = (aboutBottom > st::settingsPremiumUserHeight)\n\t\t\t? aboutBottom + padding.bottom()\n\t\t\t: st::settingsPremiumUserHeight;\n\t\t{\n\t\t\tconst auto was = maximumHeight();\n\t\t\tconst auto now = height;\n\t\t\tif (was != now) {\n\t\t\t\tsetMaximumHeight(now);\n\t\t\t\tif (was == size.height()) {\n\t\t\t\t\tresize(size.width(), now);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t_content->resize(size.width(), maximumHeight());\n\t}, lifetime());\n\n\tconst auto smallTopShadow = Ui::CreateChild<Ui::FadeShadow>(\n\t\t_smallTop.widget.data());\n\tsmallTopShadow->setDuration(st::infoTopBarDuration);\n\trpl::combine(\n\t\trpl::single(\n\t\t\tfalse\n\t\t) | rpl::then(std::move(showFinished) | rpl::map_to(true)),\n\t\tsizeValue()\n\t) | rpl::on_next([=](bool showFinished, const QSize &size) {\n\t\t_content->resize(size.width(), maximumHeight());\n\t\tconst auto skip = TopTransitionSkip();\n\t\t_content->moveToLeft(0, size.height() - _content->height() - skip);\n\n\t\t_smallTop.widget->resize(size.width(), minimumHeight());\n\t\tsmallTopShadow->resizeToWidth(size.width());\n\t\tsmallTopShadow->moveToLeft(\n\t\t\t0,\n\t\t\t_smallTop.widget->height() - smallTopShadow->height());\n\t\tconst auto shown = (minimumHeight() * 2 > size.height());\n\t\tif (_smallTop.shown != shown) {\n\t\t\t_smallTop.shown = shown;\n\t\t\tif (!showFinished) {\n\t\t\t\t_smallTop.widget->update();\n\t\t\t\tsmallTopShadow->toggle(_smallTop.shown, anim::type::instant);\n\t\t\t} else {\n\t\t\t\t_smallTop.animation.start(\n\t\t\t\t\t[=] { _smallTop.widget->update(); },\n\t\t\t\t\t_smallTop.shown ? 0. : 1.,\n\t\t\t\t\t_smallTop.shown ? 1. : 0.,\n\t\t\t\t\tst::infoTopBarDuration);\n\t\t\t\tsmallTopShadow->toggle(_smallTop.shown, anim::type::normal);\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\t_smallTop.widget->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(_smallTop.widget);\n\n\t\tp.setOpacity(_smallTop.animation.value(_smallTop.shown ? 1. : 0.));\n\t\tTopBarAbstract::paintEdges(p);\n\n\t\tp.setPen(st::boxTitleFg);\n\t\t_smallTop.text.drawLeft(\n\t\t\tp,\n\t\t\t_smallTop.position.x(),\n\t\t\t_smallTop.position.y(),\n\t\t\twidth(),\n\t\t\twidth());\n\t}, lifetime());\n\n\t_content->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(_content);\n\n\t\t_ministars.paint(p);\n\n\t\tif (_emojiStatus) {\n\t\t\t_emojiStatus->paint(p);\n\t\t} else if (!_imageStar.isNull()) {\n\t\t\tp.drawImage(_starRect.topLeft(), _imageStar);\n\t\t}\n\t}, lifetime());\n}\n\nvoid TopBarWithSticker::updateTitle(\n\t\tDocumentData *document,\n\t\tconst TextWithEntities &name,\n\t\tnot_null<Window::SessionController*> controller) {\n\tif (_type == TopBarWithStickerType::PremiumGift) {\n\t\treturn _title->setMarkedText(name);\n\t} else if (!document) {\n\t\treturn _title->setMarkedText(\n\t\t\ttr::lng_premium_summary_user_title(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tstd::move(name),\n\t\t\t\ttr::marked));\n\t}\n\tconst auto stickerInfo = document->sticker();\n\tif (!stickerInfo) {\n\t\treturn;\n\t}\n\tconst auto owner = &document->owner();\n\tconst auto &sets = owner->stickers().sets();\n\tconst auto setId = stickerInfo->set.id;\n\tconst auto it = sets.find(setId);\n\tif (it == sets.cend()) {\n\t\treturn;\n\t}\n\tconst auto set = it->second.get();\n\tconst auto coloredId = owner->customEmojiManager().coloredSetId();\n\n\tconst auto text = (set->thumbnailDocumentId ? QChar('0') : QChar())\n\t\t+ set->title;\n\tconst auto linkIndex = 1;\n\tconst auto entityEmojiData = Data::SerializeCustomEmojiId(\n\t\tset->thumbnailDocumentId);\n\tconst auto entities = EntitiesInText{\n\t\t{ EntityType::CustomEmoji, 0, 1, entityEmojiData },\n\t\ttr::link(text, linkIndex).entities.front(),\n\t};\n\tauto title = (setId == coloredId)\n\t\t? tr::lng_premium_emoji_status_title_colored(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\tstd::move(name),\n\t\t\ttr::marked)\n\t\t: tr::lng_premium_emoji_status_title(\n\t\t\ttr::now,\n\t\t\tlt_user,\n\t\t\tstd::move(name),\n\t\t\tlt_link,\n\t\t\t{ .text = text, .entities = entities, },\n\t\t\ttr::marked);\n\t_title->setMarkedText(\n\t\tstd::move(title),\n\t\tCore::TextContext({ .session = &controller->session() }));\n\tauto link = std::make_shared<LambdaClickHandler>([=,\n\t\t\tstickerSetIdentifier = stickerInfo->set] {\n\t\tsetPaused(true);\n\t\tconst auto box = controller->show(Box<StickerSetBox>(\n\t\t\tcontroller->uiShow(),\n\t\t\tstickerSetIdentifier,\n\t\t\tData::StickersType::Emoji));\n\n\t\tbox->boxClosing(\n\t\t) | rpl::on_next(crl::guard(this, [=] {\n\t\t\tsetPaused(false);\n\t\t}), box->lifetime());\n\t});\n\t_title->setLink(linkIndex, std::move(link));\n}\n\nvoid TopBarWithSticker::updateAbout(\n\t\tDocumentData *document,\n\t\tconst TextWithEntities &about) const {\n\t_about->setMarkedText((_type == TopBarWithStickerType::PremiumGift)\n\t\t? about\n\t\t: (document\n\t\t\t? tr::lng_premium_emoji_status_about\n\t\t\t: tr::lng_premium_summary_user_about)(\n\t\t\t\ttr::now,\n\t\t\t\ttr::rich));\n}\n\nvoid TopBarWithSticker::setPaused(bool paused) {\n\t_ministars.setPaused(paused);\n\tif (_emojiStatus) {\n\t\t_emojiStatus->setPaused(paused);\n\t}\n}\n\nvoid TopBarWithSticker::setTextPosition(int x, int y) {\n\t_smallTop.position = { x, y };\n}\n\nrpl::producer<int> TopBarWithSticker::additionalHeight() const {\n\treturn rpl::never<int>();\n}\n\nvoid TopBarWithSticker::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tTopBarAbstract::paintEdges(p);\n}\n\nvoid TopBarWithSticker::resizeEvent(QResizeEvent *e) {\n\t_starRect = TopBarAbstract::starRect(1., 1.);\n\n\t_ministars.setCenter(_starRect.toRect());\n\n\tif (_emojiStatus) {\n\t\t_emojiStatus->setCenter(_starRect.center());\n\t}\n}\n\n[[nodiscard]] QString GetFeatureTitle(const QString &key) {\n\tif (key == u\"saved_tags\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_tags_for_messages(tr::now);\n\t} else if (key == u\"last_seen\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_last_seen(tr::now);\n\t} else if (key == u\"message_privacy\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_message_privacy(tr::now);\n\t} else if (key == u\"wallpapers\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_wallpapers(tr::now);\n\t} else if (key == u\"peer_colors\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_peer_colors(tr::now);\n\t} else if (key == u\"stories\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_stories(tr::now);\n\t} else if (key == u\"double_limits\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_double_limits(tr::now);\n\t} else if (key == u\"more_upload\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_more_upload(tr::now);\n\t} else if (key == u\"faster_download\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_faster_download(tr::now);\n\t} else if (key == u\"voice_to_text\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_voice_to_text(tr::now);\n\t} else if (key == u\"no_ads\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_no_ads(tr::now);\n\t} else if (key == u\"emoji_status\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_emoji_status(tr::now);\n\t} else if (key == u\"infinite_reactions\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_infinite_reactions(tr::now);\n\t} else if (key == u\"premium_stickers\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_premium_stickers(tr::now);\n\t} else if (key == u\"animated_emoji\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_animated_emoji(tr::now);\n\t} else if (key == u\"advanced_chat_management\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_advanced_chat_management(tr::now);\n\t} else if (key == u\"profile_badge\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_profile_badge(tr::now);\n\t} else if (key == u\"animated_userpics\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_animated_userpics(tr::now);\n\t} else if (key == u\"translations\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_translation(tr::now);\n\t} else if (key == u\"business\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_business(tr::now);\n\t} else if (key == u\"effects\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_effects(tr::now);\n\t} else if (key == u\"todo\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_todo_lists(tr::now);\n\t} else if (key == u\"no_forwards\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_no_forwards(tr::now);\n\t} else if (key == u\"ai_compose\"_q) {\n\t\treturn tr::lng_premium_summary_subtitle_ai_compose(tr::now);\n\t}\n\treturn QString();\n}\n\nvoid BuildPremiumFeatures(SectionBuilder &builder) {\n\tconst auto session = builder.session();\n\tconst auto mtpOrder = session->appConfig().get<Order>(\n\t\t\"premium_promo_order\",\n\t\tFallbackOrder());\n\n\tfor (const auto &key : mtpOrder) {\n\t\tconst auto title = GetFeatureTitle(key);\n\t\tif (title.isEmpty()) {\n\t\t\tcontinue;\n\t\t}\n\t\tbuilder.add(nullptr, [key, title] {\n\t\t\treturn SearchEntry{\n\t\t\t\t.id = u\"premium/\"_q + key,\n\t\t\t\t.title = title,\n\t\t\t};\n\t\t});\n\t}\n}\n\nvoid SetupSubscriptionOptions(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &ref,\n\t\tstd::shared_ptr<Ui::RadiobuttonGroup> radioGroup) {\n\tconst auto isEmojiStatus = (!!Ref::EmojiStatus::Parse(ref));\n\tconst auto isGift = (!!Ref::Gift::Parse(ref));\n\n\tconst auto options = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto skip = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto content = options->entity();\n\n\tUi::AddSkip(content, st::settingsPremiumOptionsPadding.top());\n\n\tconst auto apiPremium = &controller->session().api().premium();\n\tUi::Premium::AddGiftOptions(\n\t\tcontent,\n\t\tradioGroup,\n\t\tSubscriptionOptionsForRows(apiPremium->subscriptionOptions()),\n\t\tst::premiumSubscriptionOption,\n\t\ttrue);\n\n\tUi::AddSkip(content, st::settingsPremiumOptionsPadding.bottom());\n\tUi::AddDivider(content);\n\n\tconst auto lastSkip = TopTransitionSkip() * (isEmojiStatus ? 1 : 2);\n\n\tUi::AddSkip(content, lastSkip - st::defaultVerticalListSkip);\n\tUi::AddSkip(skip->entity(), lastSkip);\n\n\tif (isEmojiStatus || isGift) {\n\t\toptions->toggle(false, anim::type::instant);\n\t\tskip->toggle(true, anim::type::instant);\n\t\treturn;\n\t}\n\tauto toggleOn = rpl::combine(\n\t\tData::AmPremiumValue(&controller->session()),\n\t\tapiPremium->statusTextValue(\n\t\t) | rpl::map([=] {\n\t\t\treturn apiPremium->subscriptionOptions().size() < 2;\n\t\t})\n\t) | rpl::map([=](bool premium, bool noOptions) {\n\t\treturn !premium && !noOptions;\n\t});\n\toptions->toggleOn(rpl::duplicate(toggleOn), anim::type::instant);\n\tskip->toggleOn(std::move(\n\t\ttoggleOn\n\t) | rpl::map([](bool value) { return !value; }), anim::type::instant);\n}\n\nvoid AddSummaryPremium(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &ref,\n\t\tFn<void(PremiumFeature)> buttonCallback,\n\t\tstd::shared_ptr<PremiumState> state) {\n\tconst auto &stDefault = st::settingsButton;\n\tconst auto &stLabel = st::defaultFlatLabel;\n\tconst auto iconSize = st::settingsPremiumIconDouble.size();\n\tconst auto &titlePadding = st::settingsPremiumRowTitlePadding;\n\tconst auto &descriptionPadding = st::settingsPremiumRowAboutPadding;\n\n\tauto entryMap = EntryMap();\n\tauto iconContainers = std::vector<Ui::AbstractButton*>();\n\ticonContainers.reserve(int(entryMap.size()));\n\n\tconst auto addRow = [&](const QString &key, Entry &entry) {\n\t\tconst auto labelAscent = stLabel.style.font->ascent;\n\t\tconst auto button = Ui::CreateChild<Ui::SettingsButton>(\n\t\t\tcontent.get(),\n\t\t\trpl::single(QString()));\n\n\t\tconst auto label = content->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\tstd::move(entry.title) | rpl::map(tr::bold),\n\t\t\t\tstLabel),\n\t\t\ttitlePadding);\n\t\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tconst auto description = content->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\tstd::move(entry.description),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tdescriptionPadding);\n\t\tdescription->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t\tif (entry.newBadge) {\n\t\t\tUi::NewBadge::AddAfterLabel(content, label);\n\t\t}\n\t\tconst auto dummy = Ui::CreateChild<Ui::AbstractButton>(content.get());\n\t\tdummy->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t\tcontent->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\tdummy->resize(s.width(), iconSize.height());\n\t\t}, dummy->lifetime());\n\n\t\tlabel->geometryValue(\n\t\t) | rpl::on_next([=](const QRect &r) {\n\t\t\tdummy->moveToLeft(0, r.y() + (r.height() - labelAscent));\n\t\t}, dummy->lifetime());\n\n\t\trpl::combine(\n\t\t\tcontent->widthValue(),\n\t\t\tlabel->heightValue(),\n\t\t\tdescription->heightValue()\n\t\t) | rpl::on_next([=,\n\t\t\ttopPadding = titlePadding,\n\t\t\tbottomPadding = descriptionPadding](\n\t\t\t\tint width,\n\t\t\t\tint topHeight,\n\t\t\t\tint bottomHeight) {\n\t\t\tbutton->resize(\n\t\t\t\twidth,\n\t\t\t\ttopPadding.top()\n\t\t\t\t\t+ topHeight\n\t\t\t\t\t+ topPadding.bottom()\n\t\t\t\t\t+ bottomPadding.top()\n\t\t\t\t\t+ bottomHeight\n\t\t\t\t\t+ bottomPadding.bottom());\n\t\t}, button->lifetime());\n\t\tlabel->topValue(\n\t\t) | rpl::on_next([=, padding = titlePadding.top()](int top) {\n\t\t\tbutton->moveToLeft(0, top - padding);\n\t\t}, button->lifetime());\n\t\tconst auto arrow = Ui::CreateChild<Ui::IconButton>(\n\t\t\tbutton,\n\t\t\tst::backButton);\n\t\tarrow->setIconOverride(\n\t\t\t&st::settingsPremiumArrow,\n\t\t\t&st::settingsPremiumArrowOver);\n\t\tarrow->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tbutton->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\tconst auto &point = st::settingsPremiumArrowShift;\n\t\t\tarrow->moveToRight(\n\t\t\t\t-point.x(),\n\t\t\t\tpoint.y() + (s.height() - arrow->height()) / 2);\n\t\t}, arrow->lifetime());\n\n\t\tconst auto section = entry.section;\n\t\tbutton->setClickedCallback([=] { buttonCallback(section); });\n\n\t\tif (state) {\n\t\t\tstate->featureButtons[key] = button;\n\t\t}\n\n\t\ticonContainers.push_back(dummy);\n\t};\n\n\tauto icons = std::vector<const style::icon *>();\n\ticons.reserve(int(entryMap.size()));\n\t{\n\t\tconst auto session = &controller->session();\n\t\tconst auto mtpOrder = session->appConfig().get<Order>(\n\t\t\t\"premium_promo_order\",\n\t\t\tFallbackOrder());\n\t\tconst auto processEntry = [&](const QString &key, Entry &entry) {\n\t\t\ticons.push_back(entry.icon);\n\t\t\taddRow(key, entry);\n\t\t};\n\n\t\tfor (const auto &key : mtpOrder) {\n\t\t\tauto it = entryMap.find(key);\n\t\t\tif (it == end(entryMap)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tprocessEntry(key, it->second);\n\t\t}\n\n\t\tSendScreenShow(controller, mtpOrder, ref);\n\t}\n\n\tcontent->resizeToWidth(content->height());\n\n\t// Icons.\n\tAssert(iconContainers.size() > 2);\n\tconst auto from = iconContainers.front()->y();\n\tconst auto to = iconContainers.back()->y() + iconSize.height();\n\tauto gradient = QLinearGradient(0, 0, 0, to - from);\n\tgradient.setStops(Ui::Premium::FullHeightGradientStops());\n\tfor (auto i = 0; i < int(icons.size()); i++) {\n\t\tconst auto &iconContainer = iconContainers[i];\n\n\t\tconst auto pointTop = iconContainer->y() - from;\n\t\tconst auto pointBottom = pointTop + iconContainer->height();\n\t\tconst auto ratioTop = pointTop / float64(to - from);\n\t\tconst auto ratioBottom = pointBottom / float64(to - from);\n\n\t\tauto resultGradient = QLinearGradient(\n\t\t\tQPointF(),\n\t\t\tQPointF(0, pointBottom - pointTop));\n\n\t\tresultGradient.setColorAt(\n\t\t\t.0,\n\t\t\tanim::gradient_color_at(gradient, ratioTop));\n\t\tresultGradient.setColorAt(\n\t\t\t.1,\n\t\t\tanim::gradient_color_at(gradient, ratioBottom));\n\n\t\tconst auto brush = QBrush(resultGradient);\n\t\tAddButtonIcon(\n\t\t\ticonContainer,\n\t\t\tstDefault,\n\t\t\t{ .icon = icons[i], .backgroundBrush = brush });\n\t}\n\n\tUi::AddSkip(content, descriptionPadding.bottom());\n}\n\nvoid BuildPremiumSectionContent(\n\t\tSectionBuilder &builder,\n\t\tstd::shared_ptr<PremiumState> state) {\n\tconst auto controller = builder.controller();\n\n\tif (controller && state) {\n\t\tbuilder.add([controller, state](const WidgetContext &ctx) {\n\t\t\tSetupSubscriptionOptions(\n\t\t\t\tctx.container,\n\t\t\t\tcontroller,\n\t\t\t\tstate->ref,\n\t\t\t\tstate->radioGroup);\n\n\t\t\tauto buttonCallback = [controller, state](PremiumFeature section) {\n\t\t\t\tif (state->setPaused) {\n\t\t\t\t\tstate->setPaused(true);\n\t\t\t\t}\n\t\t\t\tconst auto hidden = crl::guard(\n\t\t\t\t\t(QObject*)controller->widget(),\n\t\t\t\t\t[state] {\n\t\t\t\t\t\tif (state->setPaused) {\n\t\t\t\t\t\t\tstate->setPaused(false);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\tShowPremiumPreviewToBuy(controller, section, hidden);\n\t\t\t};\n\t\t\tAddSummaryPremium(\n\t\t\t\tctx.container,\n\t\t\t\tcontroller,\n\t\t\t\tstate->ref,\n\t\t\t\tstd::move(buttonCallback),\n\t\t\t\tstate);\n\n\t\t\treturn SectionBuilder::WidgetToAdd{};\n\t\t});\n\t}\n\n\tBuildPremiumFeatures(builder);\n}\n\nclass Premium : public Section<Premium> {\npublic:\n\tPremium(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\t[[nodiscard]] base::weak_qptr<Ui::RpWidget> createPinnedToTop(\n\t\tnot_null<QWidget*> parent) override;\n\t[[nodiscard]] base::weak_qptr<Ui::RpWidget> createPinnedToBottom(\n\t\tnot_null<Ui::RpWidget*> parent) override;\n\n\tvoid showFinished() override;\n\n\t[[nodiscard]] bool hasFlexibleTopBar() const override;\n\n\tvoid setStepDataReference(std::any &data) override;\n\n\t[[nodiscard]] rpl::producer<> sectionShowBack() override final;\n\nprivate:\n\tvoid setupContent();\n\tvoid setupSwipeBack();\n\n\tstd::shared_ptr<PremiumState> _state;\n\n\tQPointer<Ui::GradientButton> _subscribe;\n\tbase::unique_qptr<Ui::FadeWrap<Ui::IconButton>> _back;\n\tbase::unique_qptr<Ui::IconButton> _close;\n\trpl::variable<bool> _backToggles;\n\trpl::variable<Info::Wrap> _wrap;\n\n\trpl::event_stream<> _showBack;\n\trpl::event_stream<> _showFinished;\n\trpl::variable<QString> _buttonText;\n\n};\n\nPremium::Premium(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller)\n, _state(std::make_shared<PremiumState>(PremiumState{\n\t.ref = ResolveRef(controller->premiumRef()),\n\t.radioGroup = std::make_shared<Ui::RadiobuttonGroup>(),\n})) {\n\tsetupContent();\n\tsetupSwipeBack();\n\tcontroller->session().api().premium().reload();\n}\n\nrpl::producer<QString> Premium::title() {\n\treturn tr::lng_premium_summary_title();\n}\n\nbool Premium::hasFlexibleTopBar() const {\n\treturn true;\n}\n\nrpl::producer<> Premium::sectionShowBack() {\n\treturn _showBack.events();\n}\n\nvoid Premium::setStepDataReference(std::any &data) {\n\tconst auto my = std::any_cast<SectionCustomTopBarData>(&data);\n\tif (my) {\n\t\t_backToggles = std::move(\n\t\t\tmy->backButtonEnables\n\t\t) | rpl::map_to(true);\n\t\t_wrap = std::move(my->wrapValue);\n\t}\n}\n\nvoid Premium::setupSwipeBack() {\n\tusing namespace Ui::Controls;\n\n\tauto swipeBackData = lifetime().make_state<SwipeBackResult>();\n\n\tauto update = [=](SwipeContextData data) {\n\t\tif (data.translation > 0) {\n\t\t\tif (!swipeBackData->callback) {\n\t\t\t\t(*swipeBackData) = SetupSwipeBack(\n\t\t\t\t\tthis,\n\t\t\t\t\t[]() -> std::pair<QColor, QColor> {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tst::historyForwardChooseBg->c,\n\t\t\t\t\t\t\tst::historyForwardChooseFg->c,\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t}\n\t\t\tswipeBackData->callback(data);\n\t\t\treturn;\n\t\t} else if (swipeBackData->lifetime) {\n\t\t\t(*swipeBackData) = {};\n\t\t}\n\t};\n\n\tauto init = [=](int, Qt::LayoutDirection direction) {\n\t\treturn (direction == Qt::RightToLeft)\n\t\t\t? DefaultSwipeBackHandlerFinishData([=] {\n\t\t\t\t_showBack.fire({});\n\t\t\t})\n\t\t\t: SwipeHandlerFinishData();\n\t};\n\n\tSetupSwipeHandler({\n\t\t.widget = this,\n\t\t.scroll = v::null,\n\t\t.update = std::move(update),\n\t\t.init = std::move(init),\n\t});\n}\n\nvoid Premium::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tconst auto state = _state;\n\n\tconst SectionBuildMethod buildMethod = [state](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tconst auto isPaused = Window::PausedIn(\n\t\t\tcontroller,\n\t\t\tWindow::GifPauseReason::Layer);\n\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = isPaused,\n\t\t});\n\n\t\tBuildPremiumSectionContent(builder, state);\n\t};\n\n\tbuild(content, buildMethod);\n\n\tUi::ResizeFitChild(this, content);\n}\n\nbase::weak_qptr<Ui::RpWidget> Premium::createPinnedToTop(\n\t\tnot_null<QWidget*> parent) {\n\tauto title = controller()->session().premium()\n\t\t? tr::lng_premium_summary_title()\n\t\t: rpl::conditional(\n\t\t\tData::AmPremiumValue(&controller()->session()),\n\t\t\ttr::lng_premium_summary_title_subscribed(),\n\t\t\ttr::lng_premium_summary_title());\n\tauto about = [&]() -> rpl::producer<TextWithEntities> {\n\t\tconst auto gift = Ref::Gift::Parse(_state->ref);\n\t\tif (gift) {\n\t\t\tauto &data = controller()->session().data();\n\t\t\tif (const auto peer = data.peer(gift.peerId)) {\n\t\t\t\tconst auto months = gift.days / 30;\n\t\t\t\treturn (gift.me\n\t\t\t\t\t? (months\n\t\t\t\t\t\t? tr::lng_premium_summary_subtitle_gift_me\n\t\t\t\t\t\t: tr::lng_premium_summary_subtitle_gift_days_me)\n\t\t\t\t\t: (months\n\t\t\t\t\t\t? tr::lng_premium_summary_subtitle_gift\n\t\t\t\t\t\t: tr::lng_premium_summary_subtitle_gift_days))(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single(float64(months ? months : gift.days)),\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\trpl::single(tr::bold(peer->name())),\n\t\t\t\t\t\ttr::rich);\n\t\t\t}\n\t\t}\n\t\treturn rpl::conditional(\n\t\t\tData::AmPremiumValue(&controller()->session()),\n\t\t\tcontroller()->session().api().premium().statusTextValue(),\n\t\t\ttr::lng_premium_summary_top_about(tr::rich));\n\t}();\n\n\tconst auto emojiStatusData = Ref::EmojiStatus::Parse(_state->ref);\n\tconst auto premiumGiftData = Ref::PremiumGift::Parse(_state->ref);\n\tconst auto isEmojiStatus = (!!emojiStatusData);\n\tconst auto isPremiumGift = (!!premiumGiftData);\n\n\tauto peerWithPremium = [&]() -> PeerData* {\n\t\tif (isEmojiStatus) {\n\t\t\tauto &data = controller()->session().data();\n\t\t\tif (const auto peer = data.peer(emojiStatusData.peerId)) {\n\t\t\t\treturn peer;\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t}();\n\tauto premiumGift = [&]() -> DocumentData* {\n\t\tif (isPremiumGift) {\n\t\t\tauto &data = controller()->session().data();\n\t\t\treturn data.document(premiumGiftData.documentId);\n\t\t}\n\t\treturn nullptr;\n\t}();\n\n\tconst auto content = [&]() -> Ui::Premium::TopBarAbstract* {\n\t\tif (peerWithPremium) {\n\t\t\treturn Ui::CreateChild<TopBarWithSticker>(\n\t\t\t\tparent.get(),\n\t\t\t\tcontroller(),\n\t\t\t\tpeerWithPremium,\n\t\t\t\t_showFinished.events());\n\t\t} else if (premiumGift) {\n\t\t\treturn Ui::CreateChild<TopBarWithSticker>(\n\t\t\t\tparent.get(),\n\t\t\t\tcontroller(),\n\t\t\t\tTopBarWithStickerArgs{\n\t\t\t\t\t.stickerValue = rpl::single(premiumGift),\n\t\t\t\t\t.nameValue = tr::lng_gift_premium_title(),\n\t\t\t\t\t.aboutValue = tr::lng_gift_premium_text(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single(premiumGiftData.perUserTotal * 1.),\n\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t.type = TopBarWithStickerType::PremiumGift,\n\t\t\t\t},\n\t\t\t\t_showFinished.events());\n\t\t}\n\t\tconst auto weak = base::make_weak(controller());\n\t\tconst auto clickContextOther = [=] {\n\t\t\treturn QVariant::fromValue(ClickHandlerContext{\n\t\t\t\t.sessionWindow = weak,\n\t\t\t\t.botStartAutoSubmit = true,\n\t\t\t});\n\t\t};\n\t\treturn Ui::CreateChild<Ui::Premium::TopBar>(\n\t\t\tparent.get(),\n\t\t\tst::defaultPremiumCover,\n\t\t\tUi::Premium::TopBarDescriptor{\n\t\t\t\t.clickContextOther = clickContextOther,\n\t\t\t\t.title = std::move(title),\n\t\t\t\t.about = std::move(about),\n\t\t\t});\n\t}();\n\t_state->setPaused = [=](bool paused) {\n\t\tcontent->setPaused(paused);\n\t\tif (_subscribe) {\n\t\t\t_subscribe->setGlarePaused(paused);\n\t\t}\n\t};\n\n\t_wrap.value(\n\t) | rpl::on_next([=](Info::Wrap wrap) {\n\t\tcontent->setRoundEdges(wrap == Info::Wrap::Layer);\n\t}, content->lifetime());\n\n\tconst auto calculateMaximumHeight = [=] {\n\t\treturn (isEmojiStatus || isPremiumGift)\n\t\t\t? st::settingsPremiumUserHeight + TopTransitionSkip()\n\t\t\t: st::settingsPremiumTopHeight;\n\t};\n\n\tcontent->setMaximumHeight(calculateMaximumHeight());\n\tcontent->setMinimumHeight(st::infoLayerTopBarHeight);\n\n\tcontent->resize(content->width(), content->maximumHeight());\n\tcontent->additionalHeight(\n\t) | rpl::on_next([=](int additionalHeight) {\n\t\tconst auto wasMax = (content->height() == content->maximumHeight());\n\t\tcontent->setMaximumHeight(calculateMaximumHeight()\n\t\t\t+ additionalHeight);\n\t\tif (wasMax) {\n\t\t\tcontent->resize(content->width(), content->maximumHeight());\n\t\t}\n\t}, content->lifetime());\n\n\t_wrap.value(\n\t) | rpl::on_next([=](Info::Wrap wrap) {\n\t\tconst auto isLayer = (wrap == Info::Wrap::Layer);\n\t\t_back = base::make_unique_q<Ui::FadeWrap<Ui::IconButton>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::IconButton>(\n\t\t\t\tcontent,\n\t\t\t\tisEmojiStatus\n\t\t\t\t\t? (isLayer ? st::infoTopBarBack : st::infoLayerTopBarBack)\n\t\t\t\t\t: (isLayer\n\t\t\t\t\t\t? st::settingsPremiumLayerTopBarBack\n\t\t\t\t\t\t: st::settingsPremiumTopBarBack)),\n\t\t\tst::infoTopBarScale);\n\t\t_back->setDuration(0);\n\t\t_back->toggleOn(isLayer\n\t\t\t? _backToggles.value() | rpl::type_erased\n\t\t\t: rpl::single(true));\n\t\t_back->entity()->addClickHandler([=] {\n\t\t\t_showBack.fire({});\n\t\t});\n\t\t_back->toggledValue(\n\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\tconst auto &st = isLayer ? st::infoLayerTopBar : st::infoTopBar;\n\t\t\tcontent->setTextPosition(\n\t\t\t\ttoggled ? st.back.width : st.titlePosition.x(),\n\t\t\t\tst.titlePosition.y());\n\t\t}, _back->lifetime());\n\n\t\tif (!isLayer) {\n\t\t\t_close = nullptr;\n\t\t} else {\n\t\t\t_close = base::make_unique_q<Ui::IconButton>(\n\t\t\t\tcontent,\n\t\t\t\tisEmojiStatus\n\t\t\t\t\t? st::infoTopBarClose\n\t\t\t\t\t: st::settingsPremiumTopBarClose);\n\t\t\t_close->addClickHandler([=] {\n\t\t\t\tcontroller()->parentController()->hideLayer();\n\t\t\t\tcontroller()->parentController()->hideSpecialLayer();\n\t\t\t});\n\t\t\tcontent->widthValue(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\t_close->moveToRight(0, 0);\n\t\t\t}, _close->lifetime());\n\t\t}\n\t}, content->lifetime());\n\n\treturn base::make_weak(not_null<Ui::RpWidget*>{ content });\n}\n\nvoid Premium::showFinished() {\n\t_showFinished.fire({});\n\tcrl::on_main(this, [=] {\n\t\tfor (const auto &[key, button] : _state->featureButtons) {\n\t\t\tconst auto id = u\"premium/\"_q + key;\n\t\t\tcontroller()->checkHighlightControl(id, button);\n\t\t}\n\t});\n}\n\nbase::weak_qptr<Ui::RpWidget> Premium::createPinnedToBottom(\n\t\tnot_null<Ui::RpWidget*> parent) {\n\tconst auto content = Ui::CreateChild<Ui::RpWidget>(parent.get());\n\n\tif (Ref::Gift::Parse(_state->ref)) {\n\t\treturn nullptr;\n\t}\n\n\tconst auto emojiStatusData = Ref::EmojiStatus::Parse(_state->ref);\n\tconst auto session = &controller()->session();\n\n\tauto buttonText = [&]() -> std::optional<rpl::producer<QString>> {\n\t\tif (emojiStatusData) {\n\t\t\tauto &data = session->data();\n\t\t\tif (const auto peer = data.peer(emojiStatusData.peerId)) {\n\t\t\t\treturn Info::Profile::EmojiStatusIdValue(\n\t\t\t\t\tpeer\n\t\t\t\t) | rpl::map([=](EmojiStatusId id) {\n\t\t\t\t\treturn id\n\t\t\t\t\t\t? tr::lng_premium_emoji_status_button()\n\t\t\t\t\t\t: _buttonText.value();\n\t\t\t\t\t\t// : tr::lng_premium_summary_user_button();\n\t\t\t\t}) | rpl::flatten_latest();\n\t\t\t}\n\t\t}\n\t\treturn _buttonText.value();\n\t}();\n\n\t_subscribe = CreateSubscribeButton({\n\t\tcontroller(),\n\t\tcontent,\n\t\t[ref = _state->ref] { return ref; },\n\t\tstd::move(buttonText),\n\t\tstd::nullopt,\n\t\t[=, options = session->api().premium().subscriptionOptions()] {\n\t\t\tconst auto value = _state->radioGroup->current();\n\t\t\treturn (value < options.size() && value >= 0)\n\t\t\t\t? options[value].botUrl\n\t\t\t\t: QString();\n\t\t},\n\t});\n#if 0\n\tif (emojiStatusData) {\n\t\t// \"Learn More\" should open the general Premium Settings\n\t\t// so we override the button callback.\n\t\t// To have ability to jump back to the User Premium Settings\n\t\t// we should replace the ref explicitly.\n\t\t_subscribe->setClickedCallback([=] {\n\t\t\tconst auto ref = _ref;\n\t\t\tconst auto window = controller();\n\t\t\tShowPremium(window, QString());\n\t\t\twindow->setPremiumRef(ref);\n\t\t});\n\t} else {\n#endif\n\t{\n\t\tconst auto callback = [=](int value) {\n\t\t\tauto &api = controller()->session().api();\n\t\t\tconst auto options = api.premium().subscriptionOptions();\n\t\t\tif (options.empty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tAssert(value < options.size() && value >= 0);\n\t\t\tauto text = tr::lng_premium_subscribe_button(\n\t\t\t\ttr::now,\n\t\t\t\tlt_cost,\n\t\t\t\toptions[value].costPerMonth);\n\t\t\t_buttonText = std::move(text);\n\t\t};\n\t\t_state->radioGroup->setChangedCallback(callback);\n\t\tcallback(0);\n\t}\n\n\t_showFinished.events(\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t_subscribe->startGlareAnimation();\n\t}, _subscribe->lifetime());\n\n\tcontent->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto padding = st::settingsPremiumButtonPadding;\n\t\t_subscribe->resizeToWidth(width - padding.left() - padding.right());\n\t}, _subscribe->lifetime());\n\n\trpl::combine(\n\t\t_subscribe->heightValue(),\n\t\tData::AmPremiumValue(session),\n\t\tsession->premiumPossibleValue()\n\t) | rpl::on_next([=](\n\t\t\tint buttonHeight,\n\t\t\tbool premium,\n\t\t\tbool premiumPossible) {\n\t\tconst auto padding = st::settingsPremiumButtonPadding;\n\t\tconst auto finalHeight = !premiumPossible\n\t\t\t? 0\n\t\t\t: !premium\n\t\t\t? (padding.top() + buttonHeight + padding.bottom())\n\t\t\t: 0;\n\t\tcontent->resize(content->width(), finalHeight);\n\t\t_subscribe->moveToLeft(padding.left(), padding.top());\n\t\t_subscribe->setVisible(!premium && premiumPossible);\n\t}, _subscribe->lifetime());\n\n\treturn base::make_weak(not_null<Ui::RpWidget*>{ content });\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = Premium::Id(),\n\t.parentId = MainId(),\n\t.title = &tr::lng_premium_summary_title,\n\t.icon = &st::menuIconPremium,\n}, [](SectionBuilder &builder) {\n\tBuildPremiumSectionContent(builder, nullptr);\n});\n\n} // namespace\n\ntemplate <>\nstruct SectionFactory<Premium> : AbstractSectionFactory {\n\tobject_ptr<AbstractSection> create(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\trpl::producer<Container> containerValue\n\t) const final override {\n\t\treturn object_ptr<Premium>(parent, controller);\n\t}\n\tbool hasCustomTopBar() const final override {\n\t\treturn true;\n\t}\n\n\t[[nodiscard]] static const std::shared_ptr<SectionFactory> &Instance() {\n\t\tstatic const auto result = std::make_shared<SectionFactory>();\n\t\treturn result;\n\t}\n};\n\nType PremiumId() {\n\treturn Premium::Id();\n}\n\nvoid ShowPremium(not_null<::Main::Session*> session, const QString &ref) {\n\tconst auto active = Core::App().activeWindow();\n\tconst auto controller = (active && active->isPrimary())\n\t\t? active->sessionController()\n\t\t: nullptr;\n\tif (controller && session == &controller->session()) {\n\t\tShowPremium(controller, ref);\n\t} else {\n\t\tfor (const auto &controller : session->windows()) {\n\t\t\tif (controller->window().isPrimary()) {\n\t\t\t\tShowPremium(controller, ref);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ShowPremium(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &ref) {\n\tif (!controller->session().premiumPossible()) {\n\t\tcontroller->show(Box(PremiumUnavailableBox));\n\t\treturn;\n\t}\n\tcontroller->setPremiumRef(ref);\n\tcontroller->showSettings(Settings::PremiumId());\n}\n\nvoid ShowGiftPremium(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tint days,\n\t\tbool me) {\n\tShowPremium(controller, Ref::Gift::Serialize({ peer->id, days, me }));\n}\n\nvoid ShowEmojiStatusPremium(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer) {\n\tif (const auto unique = peer->emojiStatusId().collectible.get()) {\n\t\tCore::ResolveAndShowUniqueGift(controller->uiShow(), unique->slug);\n\t} else {\n\t\tShowPremium(controller, Ref::EmojiStatus::Serialize({ peer->id }));\n\t}\n}\n\nvoid ShowPremiumGiftPremium(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst Data::StarGift &gift) {\n\tShowPremium(controller, Ref::PremiumGift::Serialize({\n\t\t.documentId = gift.document->id,\n\t\t.perUserTotal = gift.perUserTotal,\n\t}));\n}\n\nvoid StartPremiumPayment(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst QString &ref) {\n\tconst auto session = &controller->session();\n\tconst auto username = session->appConfig().get<QString>(\n\t\tu\"premium_bot_username\"_q,\n\t\tQString());\n\tconst auto slug = session->appConfig().get<QString>(\n\t\tu\"premium_invoice_slug\"_q,\n\t\tQString());\n\tif (!username.isEmpty()) {\n\t\tcontroller->showPeerByLink(Window::PeerByLinkInfo{\n\t\t\t.usernameOrId = username,\n\t\t\t.resolveType = Window::ResolveType::BotStart,\n\t\t\t.startToken = ref,\n\t\t\t.startAutoSubmit = true,\n\t\t});\n\t} else if (!slug.isEmpty()) {\n\t\tUrlClickHandler::Open(\"https://t.me/$\" + slug);\n\t}\n}\n\nQString LookupPremiumRef(PremiumFeature section) {\n\tfor (const auto &[ref, entry] : EntryMap()) {\n\t\tif (entry.section == section) {\n\t\t\treturn ref;\n\t\t}\n\t}\n\treturn QString();\n}\n\nvoid ShowPremiumPromoToast(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tTextWithEntities textWithLink,\n\t\tconst QString &ref) {\n\tShowPremiumPromoToast(show, [=](\n\t\t\tnot_null<::Main::Session*> session) {\n\t\tExpects(&show->session() == session);\n\n\t\treturn show->resolveWindow();\n\t}, std::move(textWithLink), ref);\n}\n\nvoid ShowPremiumPromoToast(\n\t\tstd::shared_ptr<::Main::SessionShow> show,\n\t\tFn<Window::SessionController*(\n\t\t\tnot_null<::Main::Session*>)> resolveWindow,\n\t\tTextWithEntities textWithLink,\n\t\tconst QString &ref) {\n\tusing WeakToast = base::weak_ptr<Ui::Toast::Instance>;\n\tconst auto toast = std::make_shared<WeakToast>();\n\t(*toast) = show->showToast({\n\t\t.text = std::move(textWithLink),\n\t\t.filter = crl::guard(&show->session(), [=](\n\t\t\t\tconst ClickHandlerPtr &handler,\n\t\t\t\tQt::MouseButton button) {\n\t\t\tif (button != Qt::LeftButton) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst auto url = handler ? handler->url() : QString();\n\t\t\tif (!url.isEmpty() && !url.startsWith(u\"internal:\"_q)) {\n\t\t\t\tif (const auto strong = toast->get()) {\n\t\t\t\t\tstrong->hideAnimated();\n\t\t\t\t\t(*toast) = nullptr;\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (const auto strong = toast->get()) {\n\t\t\t\tstrong->hideAnimated();\n\t\t\t\t(*toast) = nullptr;\n\t\t\t\tif (const auto controller = resolveWindow(\n\t\t\t\t\t\t&show->session())) {\n\t\t\t\t\tSettings::ShowPremium(controller, ref);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}),\n\t\t.icon = &st::settingsToastStarIcon,\n\t\t.adaptive = true,\n\t\t.duration = Ui::Toast::kDefaultDuration * 2,\n\t});\n}\n\nnot_null<Ui::RoundButton*> CreateLockedButton(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<QString> text,\n\t\tconst style::RoundButton &st,\n\t\trpl::producer<bool> locked) {\n\tconst auto result = Ui::CreateChild<Ui::RoundButton>(\n\t\tparent.get(),\n\t\trpl::single(QString()),\n\t\tst);\n\n\tconst auto labelSt = result->lifetime().make_state<style::FlatLabel>(\n\t\tst::defaultFlatLabel);\n\tlabelSt->style.font = st.style.font;\n\tlabelSt->textFg = st.textFg;\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tresult,\n\t\tstd::move(text),\n\t\t*labelSt);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto icon = Ui::CreateChild<Ui::RpWidget>(result);\n\ticon->setAttribute(Qt::WA_TransparentForMouseEvents);\n\ticon->resize(st::stickersPremiumLock.size());\n\ticon->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(icon);\n\t\tst::stickersPremiumLock.paint(p, 0, 0, icon->width());\n\t}, icon->lifetime());\n\n\trpl::combine(\n\t\tresult->widthValue(),\n\t\tlabel->widthValue(),\n\t\tstd::move(locked)\n\t) | rpl::on_next([=](int outer, int inner, bool locked) {\n\t\tif (locked) {\n\t\t\ticon->show();\n\t\t\tinner += icon->width();\n\t\t\tlabel->move(\n\t\t\t\t(outer - inner) / 2 + icon->width(),\n\t\t\t\tst::similarChannelsLock.textTop);\n\t\t\ticon->move(\n\t\t\t\t(outer - inner) / 2,\n\t\t\t\tst::similarChannelsLock.textTop);\n\t\t} else {\n\t\t\ticon->hide();\n\t\t\tlabel->move(\n\t\t\t\t(outer - inner) / 2,\n\t\t\t\tst::similarChannelsLock.textTop);\n\t\t}\n\t}, result->lifetime());\n\n\treturn result;\n}\n\nnot_null<Ui::GradientButton*> CreateSubscribeButton(\n\t\tSubscribeButtonArgs &&args) {\n\tExpects(args.show || args.controller);\n\n\tauto show = args.show ? std::move(args.show) : args.controller->uiShow();\n\tauto resolve = [show](not_null<::Main::Session*> session) {\n\t\tExpects(session == &show->session());\n\n\t\treturn show->resolveWindow();\n\t};\n\treturn CreateSubscribeButton(\n\t\tstd::move(show),\n\t\tstd::move(resolve),\n\t\tstd::move(args));\n}\n\nnot_null<Ui::GradientButton*> CreateSubscribeButton(\n\t\tstd::shared_ptr<::Main::SessionShow> show,\n\t\tFn<Window::SessionController*(\n\t\t\tnot_null<::Main::Session*>)> resolveWindow,\n\t\tSubscribeButtonArgs &&args) {\n\tconst auto result = Ui::CreateChild<Ui::GradientButton>(\n\t\targs.parent.get(),\n\t\targs.gradientStops\n\t\t\t? base::take(*args.gradientStops)\n\t\t\t: Ui::Premium::ButtonGradientStops());\n\n\tresult->setClickedCallback([\n\t\t\tshow,\n\t\t\tresolveWindow,\n\t\t\tpromo = args.showPromo,\n\t\t\tcomputeRef = args.computeRef,\n\t\t\tcomputeBotUrl = args.computeBotUrl] {\n\t\tconst auto window = resolveWindow(\n\t\t\t&show->session());\n\t\tif (!window) {\n\t\t\treturn;\n\t\t} else if (promo) {\n\t\t\tSettings::ShowPremium(window, computeRef());\n\t\t\treturn;\n\t\t}\n\t\tconst auto url = computeBotUrl ? computeBotUrl() : QString();\n\t\tif (!url.isEmpty()) {\n\t\t\tconst auto local = Core::TryConvertUrlToLocal(url);\n\t\t\tif (local.isEmpty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tUrlClickHandler::Open(\n\t\t\t\tlocal,\n\t\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t\t.sessionWindow = base::make_weak(window),\n\t\t\t\t\t.botStartAutoSubmit = true,\n\t\t\t\t}));\n\t\t} else {\n\t\t\tSendScreenAccept(window);\n\t\t\tStartPremiumPayment(window, computeRef());\n\t\t}\n\t});\n\n\tconst auto &st = st::premiumPreviewBox.button;\n\tresult->resize(args.parent->width(), st.height);\n\n\tconst auto premium = &show->session().api().premium();\n\tpremium->reload();\n\tconst auto computeCost = [=] {\n\t\tconst auto amount = premium->monthlyAmount();\n\t\tconst auto currency = premium->monthlyCurrency();\n\t\tconst auto valid = (amount > 0) && !currency.isEmpty();\n\t\treturn Ui::FillAmountAndCurrency(\n\t\t\tvalid ? amount : 500,\n\t\t\tvalid ? currency : \"USD\");\n\t};\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tresult,\n\t\targs.text\n\t\t\t? base::take(*args.text)\n\t\t\t: tr::lng_premium_summary_button(\n\t\t\t\tlt_cost,\n\t\t\t\tpremium->statusTextValue() | rpl::map(computeCost)),\n\t\tst::premiumPreviewButtonLabel);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\trpl::combine(\n\t\tresult->widthValue(),\n\t\tlabel->widthValue()\n\t) | rpl::on_next([=](int outer, int width) {\n\t\tlabel->moveToLeft(\n\t\t\t(outer - width) / 2,\n\t\t\tst::premiumPreviewBox.button.textTop,\n\t\t\touter);\n\t}, label->lifetime());\n\n\treturn result;\n}\n\nstd::vector<PremiumFeature> PremiumFeaturesOrder(\n\t\tnot_null<::Main::Session*> session) {\n\tconst auto mtpOrder = session->appConfig().get<Order>(\n\t\t\"premium_promo_order\",\n\t\tFallbackOrder());\n\treturn ranges::views::all(\n\t\tmtpOrder\n\t) | ranges::views::transform([](const QString &s) {\n\t\tif (s == u\"more_upload\"_q) {\n\t\t\treturn PremiumFeature::MoreUpload;\n\t\t} else if (s == u\"faster_download\"_q) {\n\t\t\treturn PremiumFeature::FasterDownload;\n\t\t} else if (s == u\"voice_to_text\"_q) {\n\t\t\treturn PremiumFeature::VoiceToText;\n\t\t} else if (s == u\"no_ads\"_q) {\n\t\t\treturn PremiumFeature::NoAds;\n\t\t} else if (s == u\"emoji_status\"_q) {\n\t\t\treturn PremiumFeature::EmojiStatus;\n\t\t} else if (s == u\"infinite_reactions\"_q) {\n\t\t\treturn PremiumFeature::InfiniteReactions;\n\t\t} else if (s == u\"saved_tags\"_q) {\n\t\t\treturn PremiumFeature::TagsForMessages;\n\t\t} else if (s == u\"last_seen\"_q) {\n\t\t\treturn PremiumFeature::LastSeen;\n\t\t} else if (s == u\"message_privacy\"_q) {\n\t\t\treturn PremiumFeature::MessagePrivacy;\n\t\t} else if (s == u\"premium_stickers\"_q) {\n\t\t\treturn PremiumFeature::Stickers;\n\t\t} else if (s == u\"animated_emoji\"_q) {\n\t\t\treturn PremiumFeature::AnimatedEmoji;\n\t\t} else if (s == u\"advanced_chat_management\"_q) {\n\t\t\treturn PremiumFeature::AdvancedChatManagement;\n\t\t} else if (s == u\"profile_badge\"_q) {\n\t\t\treturn PremiumFeature::ProfileBadge;\n\t\t} else if (s == u\"animated_userpics\"_q) {\n\t\t\treturn PremiumFeature::AnimatedUserpics;\n\t\t} else if (s == u\"translations\"_q) {\n\t\t\treturn PremiumFeature::RealTimeTranslation;\n\t\t} else if (s == u\"wallpapers\"_q) {\n\t\t\treturn PremiumFeature::Wallpapers;\n\t\t} else if (s == u\"effects\"_q) {\n\t\t\treturn PremiumFeature::Effects;\n\t\t} else if (s == u\"todo\"_q) {\n\t\t\treturn PremiumFeature::TodoLists;\n\t\t} else if (s == u\"peer_colors\"_q) {\n\t\t\treturn PremiumFeature::PeerColors;\n\t\t} else if (s == u\"gifts\"_q) {\n\t\t\treturn PremiumFeature::Gifts;\n\t\t} else if (s == u\"no_forwards\"_q) {\n\t\t\treturn PremiumFeature::NoForwards;\n\t\t} else if (s == u\"ai_compose\"_q) {\n\t\t\treturn PremiumFeature::AiCompose;\n\t\t}\n\t\treturn PremiumFeature::kCount;\n\t}) | ranges::views::filter([](PremiumFeature type) {\n\t\treturn (type != PremiumFeature::kCount);\n\t}) | ranges::to_vector;\n}\n\nstd::unique_ptr<Ui::RpWidget> MakeEmojiStatusPreview(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<DocumentData*> document) {\n\tauto result = std::make_unique<Ui::RpWidget>(parent);\n\n\tconst auto raw = result.get();\n\tconst auto size = HistoryView::Sticker::EmojiSize();\n\tconst auto emoji = raw->lifetime().make_state<EmojiStatusTopBar>(\n\t\tdocument,\n\t\t[=](QRect r) { raw->update(std::move(r)); },\n\t\tsize);\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\temoji->paint(p);\n\t}, raw->lifetime());\n\n\traw->sizeValue() | rpl::on_next([=](QSize size) {\n\t\temoji->setCenter(QPointF(size.width() / 2., size.height() / 2.));\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_premium.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_common.h\"\n#include \"settings/settings_type.h\"\n\nclass DocumentData;\nenum class PremiumFeature;\n\nnamespace style {\nstruct RoundButton;\n} // namespace style\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nstruct StarGift;\n} // namespace Data\n\nnamespace Ui {\nclass RpWidget;\nclass RoundButton;\nclass GradientButton;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Main {\nclass Session;\nclass SessionShow;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Settings {\n\n[[nodiscard]] Type PremiumId();\n\nvoid ShowPremium(not_null<::Main::Session*> session, const QString &ref);\nvoid ShowPremium(\n\tnot_null<Window::SessionController*> controller,\n\tconst QString &ref);\nvoid ShowGiftPremium(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\tint days,\n\tbool me);\nvoid ShowEmojiStatusPremium(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer);\nvoid ShowPremiumGiftPremium(\n\tnot_null<Window::SessionController*> controller,\n\tconst Data::StarGift &gift);\n\nvoid StartPremiumPayment(\n\tnot_null<Window::SessionController*> controller,\n\tconst QString &ref);\n\n[[nodiscard]] QString LookupPremiumRef(PremiumFeature section);\n\nvoid ShowPremiumPromoToast(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tTextWithEntities textWithLink,\n\tconst QString &ref);\nvoid ShowPremiumPromoToast(\n\tstd::shared_ptr<::Main::SessionShow> show,\n\tFn<Window::SessionController*(not_null<::Main::Session*>)> resolveWindow,\n\tTextWithEntities textWithLink,\n\tconst QString &ref);\n\nstruct SubscribeButtonArgs final {\n\tWindow::SessionController *controller = nullptr;\n\tnot_null<Ui::RpWidget*> parent;\n\tFn<QString()> computeRef;\n\tstd::optional<rpl::producer<QString>> text;\n\tstd::optional<QGradientStops> gradientStops;\n\tFn<QString()> computeBotUrl; // nullable\n\tstd::shared_ptr<ChatHelpers::Show> show;\n\tbool showPromo = false;\n};\n\n\n[[nodiscard]] not_null<Ui::RoundButton*> CreateLockedButton(\n\tnot_null<QWidget*> parent,\n\trpl::producer<QString> text,\n\tconst style::RoundButton &st,\n\trpl::producer<bool> locked);\n\n[[nodiscard]] not_null<Ui::GradientButton*> CreateSubscribeButton(\n\tSubscribeButtonArgs &&args);\n\n[[nodiscard]] not_null<Ui::GradientButton*> CreateSubscribeButton(\n\tstd::shared_ptr<::Main::SessionShow> show,\n\tFn<Window::SessionController*(not_null<::Main::Session*>)> resolveWindow,\n\tSubscribeButtonArgs &&args);\n\n[[nodiscard]] std::vector<PremiumFeature> PremiumFeaturesOrder(\n\tnot_null<::Main::Session*> session);\n\n[[nodiscard]] std::unique_ptr<Ui::RpWidget> MakeEmojiStatusPreview(\n\tnot_null<QWidget*> parent,\n\tnot_null<DocumentData*> document);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_privacy_security.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_privacy_security.h\"\n\n#include \"settings/settings_common_session.h\"\n\n#include \"api/api_authorizations.h\"\n#include \"api/api_blocked_peers.h\"\n#include \"api/api_cloud_password.h\"\n#include \"api/api_global_privacy.h\"\n#include \"api/api_self_destruct.h\"\n#include \"api/api_sensitive_content.h\"\n#include \"api/api_websites.h\"\n#include \"apiwrap.h\"\n#include \"base/system_unlock.h\"\n#include \"base/timer_rpl.h\"\n#include \"boxes/edit_privacy_box.h\"\n#include \"boxes/passcode_box.h\"\n#include \"boxes/self_destruction_box.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/core_cloud_password.h\"\n#include \"core/core_settings.h\"\n#include \"core/update_checker.h\"\n#include \"data/components/passkeys.h\"\n#include \"data/components/top_peers.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_session.h\"\n#include \"history/view/media/history_view_media_common.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"platform/platform_webauthn.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/cloud_password/settings_cloud_password_email_confirm.h\"\n#include \"settings/cloud_password/settings_cloud_password_input.h\"\n#include \"settings/cloud_password/settings_cloud_password_start.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"settings/sections/settings_active_sessions.h\"\n#include \"settings/sections/settings_blocked_peers.h\"\n#include \"settings/sections/settings_global_ttl.h\"\n#include \"settings/sections/settings_local_passcode.h\"\n#include \"settings/sections/settings_passkeys.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"settings/settings_privacy_controllers.h\"\n#include \"settings/sections/settings_websites.h\"\n#include \"storage/storage_domain.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtSvg/QSvgRenderer>\n\nnamespace Settings {\nnamespace {\n\nconstexpr auto kUpdateTimeout = 60 * crl::time(1000);\n\nusing Privacy = Api::UserPrivacy;\n\nQString PrivacyBase(Privacy::Key key, const Privacy::Rule &rule) {\n\tusing Key = Privacy::Key;\n\tusing Option = Privacy::Option;\n\tswitch (key) {\n\tcase Key::CallsPeer2Peer:\n\t\tswitch (rule.option) {\n\t\tcase Option::Everyone:\n\t\t\treturn tr::lng_edit_privacy_calls_p2p_everyone(tr::now);\n\t\tcase Option::Contacts:\n\t\t\treturn tr::lng_edit_privacy_calls_p2p_contacts(tr::now);\n\t\tcase Option::Nobody:\n\t\t\treturn tr::lng_edit_privacy_calls_p2p_nobody(tr::now);\n\t\t}\n\t\t[[fallthrough]];\n\tdefault:\n\t\tswitch (rule.option) {\n\t\tcase Option::Everyone:\n\t\t\treturn rule.never.miniapps\n\t\t\t\t? tr::lng_edit_privacy_no_miniapps(tr::now)\n\t\t\t\t: tr::lng_edit_privacy_everyone(tr::now);\n\t\tcase Option::Contacts:\n\t\t\treturn rule.always.premiums\n\t\t\t\t? tr::lng_edit_privacy_contacts_and_premium(tr::now)\n\t\t\t\t: rule.always.miniapps\n\t\t\t\t? tr::lng_edit_privacy_contacts_and_miniapps(tr::now)\n\t\t\t\t: tr::lng_edit_privacy_contacts(tr::now);\n\t\tcase Option::CloseFriends:\n\t\t\treturn tr::lng_edit_privacy_close_friends(tr::now);\n\t\tcase Option::Nobody:\n\t\t\treturn rule.always.premiums\n\t\t\t\t? tr::lng_edit_privacy_premium(tr::now)\n\t\t\t\t: rule.always.miniapps\n\t\t\t\t? tr::lng_edit_privacy_miniapps(tr::now)\n\t\t\t\t: tr::lng_edit_privacy_nobody(tr::now);\n\t\t}\n\t\tUnexpected(\"Value in Privacy::Option.\");\n\t}\n}\n\nrpl::producer<QString> PrivacyString(\n\t\tnot_null<::Main::Session*> session,\n\t\tPrivacy::Key key) {\n\tsession->api().userPrivacy().reload(key);\n\treturn session->api().userPrivacy().value(\n\t\tkey\n\t) | rpl::map([=](const Privacy::Rule &value) {\n\t\tauto add = QStringList();\n\t\tif (const auto never = ExceptionUsersCount(value.never.peers)) {\n\t\t\tadd.push_back(\"-\" + QString::number(never));\n\t\t}\n\t\tif (const auto always = ExceptionUsersCount(value.always.peers)) {\n\t\t\tadd.push_back(\"+\" + QString::number(always));\n\t\t}\n\t\tif (!add.isEmpty()) {\n\t\t\treturn PrivacyBase(key, value)\n\t\t\t\t+ \" (\" + add.join(\", \") + \")\";\n\t\t} else {\n\t\t\treturn PrivacyBase(key, value);\n\t\t}\n\t});\n}\n\nvoid ClearPaymentInfoBoxBuilder(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<::Main::Session*> session) {\n\tbox->setTitle(tr::lng_clear_payment_info_title());\n\n\tconst auto checkboxPadding = style::margins(\n\t\tst::boxRowPadding.left(),\n\t\tst::boxRowPadding.left(),\n\t\tst::boxRowPadding.right(),\n\t\tst::boxRowPadding.bottom());\n\tconst auto label = box->addRow(object_ptr<Ui::FlatLabel>(\n\t\tbox,\n\t\ttr::lng_clear_payment_info_sure(),\n\t\tst::boxLabel));\n\tconst auto shipping = box->addRow(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tbox,\n\t\t\ttr::lng_clear_payment_info_shipping(tr::now),\n\t\t\ttrue,\n\t\t\tst::defaultBoxCheckbox),\n\t\tcheckboxPadding);\n\tconst auto payment = box->addRow(\n\t\tobject_ptr<Ui::Checkbox>(\n\t\t\tbox,\n\t\t\ttr::lng_clear_payment_info_payment(tr::now),\n\t\t\ttrue,\n\t\t\tst::defaultBoxCheckbox),\n\t\tcheckboxPadding);\n\n\tusing Flags = MTPpayments_ClearSavedInfo::Flags;\n\tconst auto flags = box->lifetime().make_state<Flags>();\n\n\tbox->addButton(tr::lng_clear_payment_info_clear(), [=] {\n\t\tusing Flag = Flags::Enum;\n\t\t*flags = (shipping->checked() ? Flag::f_info : Flag(0))\n\t\t\t| (payment->checked() ? Flag::f_credentials : Flag(0));\n\t\tdelete label;\n\t\tdelete shipping;\n\t\tdelete payment;\n\t\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_clear_payment_info_confirm(),\n\t\t\tst::boxLabel));\n\t\tbox->clearButtons();\n\t\tbox->addButton(tr::lng_clear_payment_info_clear(), [=] {\n\t\t\tsession->api().request(MTPpayments_ClearSavedInfo(\n\t\t\t\tMTP_flags(*flags)\n\t\t\t)).send();\n\t\t\tbox->closeBox();\n\t\t}, st::attentionBoxButton);\n\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t}, st::attentionBoxButton);\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\n} // namespace\n\nrpl::producer<QString> PrivacyButtonLabel(\n\t\tnot_null<::Main::Session*> session,\n\t\tPrivacy::Key key) {\n\treturn PrivacyString(session, key);\n}\n\nvoid AddPrivacyPremiumStar(\n\t\tnot_null<Ui::SettingsButton*> button,\n\t\tnot_null<::Main::Session*> session,\n\t\trpl::producer<QString> label,\n\t\tconst QMargins &padding) {\n\tconst auto badge = Ui::CreateChild<Ui::RpWidget>(button.get());\n\tbadge->showOn(Data::AmPremiumValue(session));\n\tconst auto sampleLeft = st::settingsColorSamplePadding.left();\n\tconst auto badgeLeft = padding.left() + sampleLeft;\n\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto size = Size(st::settingsButtonNoIcon.style.font->ascent);\n\tauto starImage = QImage(\n\t\tsize * factor,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tstarImage.setDevicePixelRatio(factor);\n\tstarImage.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&starImage);\n\t\tauto star = QSvgRenderer(\n\t\t\tUi::Premium::ColorizedSvg(Ui::Premium::ButtonGradientStops()));\n\t\tstar.render(&p, Rect(size));\n\t}\n\n\tbadge->resize(starImage.size() / style::DevicePixelRatio());\n\tbadge->paintRequest(\n\t) | rpl::on_next([=, star = std::move(starImage)] {\n\t\tauto p = QPainter(badge);\n\t\tp.drawImage(0, 0, star);\n\t}, badge->lifetime());\n\n\trpl::combine(\n\t\tbutton->sizeValue(),\n\t\tstd::move(label)\n\t) | rpl::on_next([=](const QSize &s, const QString &) {\n\t\tif (s.isNull()) {\n\t\t\treturn;\n\t\t}\n\t\tbadge->moveToLeft(\n\t\t\tbutton->fullTextWidth() + badgeLeft,\n\t\t\t(s.height() - badge->height()) / 2);\n\t}, badge->lifetime());\n}\n\nobject_ptr<Ui::BoxContent> ClearPaymentInfoBox(not_null<::Main::Session*> session) {\n\treturn Box(ClearPaymentInfoBoxBuilder, session);\n}\n\nvoid SetupSensitiveContent(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<> updateTrigger,\n\t\tHighlightRegistry *highlights) {\n\tusing namespace rpl::mappers;\n\n\tconst auto wrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto inner = wrap->entity();\n\n\tUi::AddSkip(inner);\n\tUi::AddSubsectionTitle(inner, tr::lng_settings_sensitive_title());\n\n\tconst auto show = controller->uiShow();\n\tconst auto session = &controller->session();\n\tconst auto disable = inner->lifetime().make_state<rpl::event_stream<>>();\n\n\tstd::move(\n\t\tupdateTrigger\n\t) | rpl::on_next([=] {\n\t\tsession->api().sensitiveContent().reload();\n\t}, container->lifetime());\n\tconst auto button = inner->add(object_ptr<Button>(\n\t\tinner,\n\t\ttr::lng_settings_sensitive_disable_filtering(),\n\t\tst::settingsButtonNoIcon));\n\tbutton->toggleOn(rpl::merge(\n\t\tsession->api().sensitiveContent().enabled(),\n\t\tdisable->events() | rpl::map_to(false)\n\t))->toggledChanges(\n\t) | rpl::filter([=](bool toggled) {\n\t\treturn toggled != session->api().sensitiveContent().enabledCurrent();\n\t}) | rpl::on_next([=](bool toggled) {\n\t\tif (toggled && session->appConfig().ageVerifyNeeded()) {\n\t\t\tdisable->fire({});\n\n\t\t\tHistoryView::ShowAgeVerificationRequired(\n\t\t\t\tshow,\n\t\t\t\tsession,\n\t\t\t\t[] {});\n\t\t} else {\n\t\t\tsession->api().sensitiveContent().update(toggled);\n\t\t}\n\t}, container->lifetime());\n\n\tif (highlights) {\n\t\thighlights->push_back({ u\"chat/show-18-content\"_q, { button } });\n\t}\n\n\tUi::AddSkip(inner);\n\tUi::AddDividerText(inner, tr::lng_settings_sensitive_about());\n\n\twrap->toggleOn(session->api().sensitiveContent().canChange());\n}\n\nint ExceptionUsersCount(const std::vector<not_null<PeerData*>> &exceptions) {\n\tconst auto add = [](int already, not_null<PeerData*> peer) {\n\t\tif (const auto chat = peer->asChat()) {\n\t\t\treturn already + chat->count;\n\t\t} else if (const auto channel = peer->asChannel()) {\n\t\t\treturn already + channel->membersCount();\n\t\t}\n\t\treturn already + 1;\n\t};\n\treturn ranges::accumulate(exceptions, 0, add);\n}\n\nbool CheckEditCloudPassword(not_null<::Main::Session*> session) {\n\tconst auto current = session->api().cloudPassword().stateCurrent();\n\tAssert(current.has_value());\n\n\treturn !current->outdatedClient;\n}\n\nobject_ptr<Ui::BoxContent> EditCloudPasswordBox(not_null<::Main::Session*> session) {\n\tconst auto current = session->api().cloudPassword().stateCurrent();\n\tAssert(current.has_value());\n\n\tauto result = Box<PasscodeBox>(\n\t\tsession,\n\t\tPasscodeBox::CloudFields::From(*current));\n\tconst auto box = result.data();\n\n\trpl::merge(\n\t\tbox->newPasswordSet() | rpl::to_empty,\n\t\tbox->passwordReloadNeeded()\n\t) | rpl::on_next([=] {\n\t\tsession->api().cloudPassword().reload();\n\t}, box->lifetime());\n\n\tbox->clearUnconfirmedPassword(\n\t) | rpl::on_next([=] {\n\t\tsession->api().cloudPassword().clearUnconfirmedPassword();\n\t}, box->lifetime());\n\n\treturn result;\n}\n\nvoid OpenFileConfirmationsBox(not_null<Ui::GenericBox*> box) {\n\tbox->setTitle(tr::lng_settings_file_confirmations());\n\n\tconst auto settings = &Core::App().settings();\n\tconst auto &list = settings->noWarningExtensions();\n\tconst auto text = QStringList(begin(list), end(list)).join(' ');\n\tconst auto layout = box->verticalLayout();\n\tconst auto extensions = box->addRow(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tbox,\n\t\t\tst::defaultInputField,\n\t\t\tUi::InputField::Mode::MultiLine,\n\t\t\ttr::lng_settings_edit_extensions(),\n\t\t\tTextWithTags{ text }),\n\t\tst::boxRowPadding + QMargins(0, 0, 0, st::settingsPrivacySkip));\n\tUi::AddDividerText(layout, tr::lng_settings_edit_extensions_about());\n\tUi::AddSkip(layout);\n\tconst auto ip = layout->add(object_ptr<Ui::SettingsButton>(\n\t\tbox,\n\t\ttr::lng_settings_edit_ip_confirm(),\n\t\tst::settingsButtonNoIcon\n\t))->toggleOn(rpl::single(settings->ipRevealWarning()));\n\tUi::AddSkip(layout);\n\tUi::AddDividerText(layout, tr::lng_settings_edit_ip_confirm_about());\n\n\tbox->setFocusCallback([=] {\n\t\textensions->setFocusFast();\n\t});\n\n\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\tconst auto extensionsList = extensions->getLastText()\n\t\t\t.mid(0, 10240)\n\t\t\t.split(' ', Qt::SkipEmptyParts)\n\t\t\t.mid(0, 1024);\n\t\tauto extensionsSet = base::flat_set<QString>(\n\t\t\textensionsList.begin(),\n\t\t\textensionsList.end());\n\t\tconst auto ipRevealWarning = ip->toggled();\n\t\tif (extensionsSet != settings->noWarningExtensions()\n\t\t\t|| ipRevealWarning != settings->ipRevealWarning()) {\n\t\t\tsettings->setNoWarningExtensions(std::move(extensionsSet));\n\t\t\tsettings->setIpRevealWarning(ipRevealWarning);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}\n\t\tbox->closeBox();\n\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\nvoid RemoveCloudPassword(not_null<Window::SessionController*> controller) {\n\tconst auto session = &controller->session();\n\tconst auto current = session->api().cloudPassword().stateCurrent();\n\tAssert(current.has_value());\n\n\tif (!current->hasPassword) {\n\t\tsession->api().cloudPassword().clearUnconfirmedPassword();\n\t\treturn;\n\t}\n\tauto fields = PasscodeBox::CloudFields::From(*current);\n\tfields.turningOff = true;\n\tauto box = Box<PasscodeBox>(session, fields);\n\n\trpl::merge(\n\t\tbox->newPasswordSet() | rpl::to_empty,\n\t\tbox->passwordReloadNeeded()\n\t) | rpl::on_next([=] {\n\t\tsession->api().cloudPassword().reload();\n\t}, box->lifetime());\n\n\tbox->clearUnconfirmedPassword(\n\t) | rpl::on_next([=] {\n\t\tsession->api().cloudPassword().clearUnconfirmedPassword();\n\t}, box->lifetime());\n\n\tcontroller->show(std::move(box));\n}\n\nobject_ptr<Ui::BoxContent> CloudPasswordAppOutdatedBox() {\n\tconst auto callback = [=](Fn<void()> &&close) {\n\t\tCore::UpdateApplication();\n\t\tclose();\n\t};\n\treturn Ui::MakeConfirmBox({\n\t\t.text = tr::lng_passport_app_out_of_date(),\n\t\t.confirmed = callback,\n\t\t.confirmText = tr::lng_menu_update(),\n\t});\n}\n\nnot_null<Ui::SettingsButton*> AddPrivacyButton(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> label,\n\t\tIconDescriptor &&descriptor,\n\t\tPrivacy::Key key,\n\t\tFn<std::unique_ptr<EditPrivacyController>()> controllerFactory,\n\t\tconst style::SettingsButton *stOverride) {\n\tconst auto shower = Ui::CreateChild<rpl::lifetime>(container.get());\n\tconst auto session = &controller->session();\n\tconst auto button = AddButtonWithLabel(\n\t\tcontainer,\n\t\tstd::move(label),\n\t\tPrivacyString(session, key),\n\t\tstOverride ? *stOverride : st::settingsButtonNoIcon,\n\t\tstd::move(descriptor));\n\tbutton->addClickHandler([=] {\n\t\t*shower = session->api().userPrivacy().value(\n\t\t\tkey\n\t\t) | rpl::take(\n\t\t\t1\n\t\t) | rpl::on_next(crl::guard(controller, [=](\n\t\t\t\tconst Privacy::Rule &value) {\n\t\t\tcontroller->show(Box<EditPrivacyBox>(\n\t\t\t\tcontroller,\n\t\t\t\tcontrollerFactory(),\n\t\t\t\tvalue));\n\t\t}));\n\t});\n\treturn button;\n}\n\nvoid SetupArchiveAndMute(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tHighlightRegistry *highlights) {\n\tusing namespace rpl::mappers;\n\n\tconst auto wrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto inner = wrap->entity();\n\n\tUi::AddSkip(inner);\n\tUi::AddSubsectionTitle(inner, tr::lng_settings_new_unknown());\n\n\tconst auto session = &controller->session();\n\n\tconst auto privacy = &session->api().globalPrivacy();\n\tprivacy->reload();\n\tconst auto button = inner->add(object_ptr<Button>(\n\t\tinner,\n\t\ttr::lng_settings_auto_archive(),\n\t\tst::settingsButtonNoIcon\n\t));\n\tbutton->toggleOn(\n\t\tprivacy->archiveAndMute()\n\t)->toggledChanges(\n\t) | rpl::filter([=](bool toggled) {\n\t\treturn toggled != privacy->archiveAndMuteCurrent();\n\t}) | rpl::on_next([=](bool toggled) {\n\t\tprivacy->updateArchiveAndMute(toggled);\n\t}, container->lifetime());\n\n\tif (highlights) {\n\t\thighlights->push_back({ u\"privacy/archive_and_mute\"_q, { button } });\n\t}\n\n\tUi::AddSkip(inner);\n\tUi::AddDividerText(inner, tr::lng_settings_auto_archive_about());\n\n\tauto shown = rpl::single(\n\t\tfalse\n\t) | rpl::then(session->api().globalPrivacy().showArchiveAndMute(\n\t) | rpl::filter(_1) | rpl::take(1));\n\tauto premium = Data::AmPremiumValue(&controller->session());\n\n\tusing namespace rpl::mappers;\n\twrap->toggleOn(rpl::combine(\n\t\tstd::move(shown),\n\t\tstd::move(premium),\n\t\t_1 || _2));\n}\n\nnamespace {\n\nusing namespace Builder;\n\nvoid BuildSecuritySection(\n\t\tSectionBuilder &builder,\n\t\trpl::producer<> updateTrigger) {\n\tconst auto controller = builder.controller();\n\tconst auto showOther = builder.showOther();\n\tconst auto session = builder.session();\n\n\tbuilder.addSkip(st::settingsPrivacySkip);\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"security/section\"_q,\n\t\t.title = tr::lng_settings_security(),\n\t\t.keywords = { u\"security\"_q, u\"password\"_q, u\"passcode\"_q },\n\t});\n\n\tusing State = Core::CloudPasswordState;\n\tenum class PasswordState {\n\t\tLoading,\n\t\tOn,\n\t\tOff,\n\t\tUnconfirmed,\n\t};\n\tauto passwordState = rpl::single(\n\t\tPasswordState::Loading\n\t) | rpl::then(session->api().cloudPassword().state(\n\t) | rpl::map([](const State &state) {\n\t\treturn (!state.unconfirmedPattern.isEmpty())\n\t\t\t? PasswordState::Unconfirmed\n\t\t\t: state.hasPassword\n\t\t\t? PasswordState::On\n\t\t\t: PasswordState::Off;\n\t})) | rpl::distinct_until_changed();\n\n\tauto cloudPasswordLabel = rpl::duplicate(\n\t\tpasswordState\n\t) | rpl::map([=](PasswordState state) {\n\t\treturn (state == PasswordState::Loading)\n\t\t\t? tr::lng_profile_loading(tr::now)\n\t\t\t: (state == PasswordState::On)\n\t\t\t? tr::lng_settings_cloud_password_on(tr::now)\n\t\t\t: tr::lng_settings_cloud_password_off(tr::now);\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"security/cloud_password\"_q,\n\t\t.title = tr::lng_settings_cloud_password_start_title(),\n\t\t.icon = { &st::menuIcon2SV },\n\t\t.label = std::move(cloudPasswordLabel),\n\t\t.onClick = [=, passwordState = base::duplicate(passwordState)] {\n\t\t\tconst auto state = rpl::variable<PasswordState>(\n\t\t\t\tbase::duplicate(passwordState)).current();\n\t\t\tif (state == PasswordState::Loading) {\n\t\t\t\treturn;\n\t\t\t} else if (state == PasswordState::On) {\n\t\t\t\tshowOther(CloudPasswordInputId());\n\t\t\t} else if (state == PasswordState::Off) {\n\t\t\t\tshowOther(CloudPasswordStartId());\n\t\t\t} else if (state == PasswordState::Unconfirmed) {\n\t\t\t\tshowOther(CloudPasswordEmailConfirmId());\n\t\t\t}\n\t\t},\n\t\t.keywords = { u\"password\"_q, u\"2fa\"_q, u\"two-factor\"_q },\n\t});\n\n\tsession->api().cloudPassword().reload();\n\n\tauto ttlLabel = rpl::combine(\n\t\tsession->api().selfDestruct().periodDefaultHistoryTTL(),\n\t\ttr::lng_settings_ttl_after_off()\n\t) | rpl::map([](int ttl, const QString &none) {\n\t\treturn ttl ? Ui::FormatTTL(ttl) : none;\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"security/ttl\"_q,\n\t\t.title = tr::lng_settings_ttl_title(),\n\t\t.icon = { &st::menuIconTTL },\n\t\t.label = std::move(ttlLabel),\n\t\t.onClick = [showOther] {\n\t\t\tshowOther(GlobalTTLId());\n\t\t},\n\t\t.keywords = { u\"ttl\"_q, u\"auto-delete\"_q, u\"timer\"_q },\n\t});\n\n\tbuilder.add([session, updateTrigger = rpl::duplicate(updateTrigger)](const WidgetContext &ctx) mutable {\n\t\tstd::move(updateTrigger) | rpl::on_next([=] {\n\t\t\tsession->api().selfDestruct().reload();\n\t\t}, ctx.container->lifetime());\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n\n\tauto passcodeHas = rpl::single(rpl::empty) | rpl::then(\n\t\tsession->domain().local().localPasscodeChanged()\n\t) | rpl::map([=] {\n\t\treturn session->domain().local().hasLocalPasscode();\n\t});\n\tauto passcodeLabel = rpl::combine(\n\t\ttr::lng_settings_cloud_password_on(),\n\t\ttr::lng_settings_cloud_password_off(),\n\t\trpl::duplicate(passcodeHas)\n\t) | rpl::map([](const QString &on, const QString &off, bool has) {\n\t\treturn has ? on : off;\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"security/passcode\"_q,\n\t\t.title = tr::lng_settings_passcode_title(),\n\t\t.icon = { &st::menuIconLock },\n\t\t.label = std::move(passcodeLabel),\n\t\t.onClick = [=, passcodeHas = std::move(passcodeHas)]() mutable {\n\t\t\tif (rpl::variable<bool>(std::move(passcodeHas)).current()) {\n\t\t\t\tshowOther(LocalPasscodeCheckId());\n\t\t\t} else {\n\t\t\t\tshowOther(LocalPasscodeCreateId());\n\t\t\t}\n\t\t},\n\t\t.keywords = { u\"passcode\"_q, u\"lock\"_q, u\"pin\"_q },\n\t});\n\n\tif (session->passkeys().possible()) {\n\t\tauto passkeysLabel = rpl::combine(\n\t\t\ttr::lng_profile_loading(),\n\t\t\t(rpl::single(rpl::empty_value())\n\t\t\t\t| rpl::then(session->passkeys().requestList())) | rpl::map([=] {\n\t\t\t\treturn session->passkeys().list().size();\n\t\t\t})\n\t\t) | rpl::map([=](const QString &loading, int count) {\n\t\t\treturn !session->passkeys().listKnown()\n\t\t\t\t? loading\n\t\t\t\t: count == 1\n\t\t\t\t? session->passkeys().list().front().name\n\t\t\t\t: count\n\t\t\t\t? QString::number(count)\n\t\t\t\t: tr::lng_settings_cloud_password_off(tr::now);\n\t\t});\n\n\t\tauto passkeysShown = (rpl::single(rpl::empty_value())\n\t\t\t| rpl::then(session->passkeys().requestList())) | rpl::map([=] {\n\t\t\treturn Platform::WebAuthn::IsSupported()\n\t\t\t\t|| !session->passkeys().list().empty();\n\t\t});\n\n\t\tbuilder.addButton({\n\t\t\t.id = u\"security/passkeys\"_q,\n\t\t\t.title = tr::lng_settings_passkeys_title(),\n\t\t\t.icon = { &st::menuIconPermissions },\n\t\t\t.label = std::move(passkeysLabel),\n\t\t\t.onClick = [=] {\n\t\t\t\tif (!session->passkeys().listKnown()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto count = session->passkeys().list().size();\n\t\t\t\tif (count == 0) {\n\t\t\t\t\tcontroller->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\t\t\tPasskeysNoneBox(box, session);\n\t\t\t\t\t\tbox->boxClosing() | rpl::on_next([=] {\n\t\t\t\t\t\t\tif (session->passkeys().list().size()) {\n\t\t\t\t\t\t\t\tcontroller->showSettings(PasskeysId());\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}, box->lifetime());\n\t\t\t\t\t}));\n\t\t\t\t} else {\n\t\t\t\t\tcontroller->showSettings(PasskeysId());\n\t\t\t\t}\n\t\t\t},\n\t\t\t.keywords = { u\"passkeys\"_q, u\"biometric\"_q },\n\t\t\t.shown = std::move(passkeysShown),\n\t\t});\n\t}\n\n\t{\n\t\tauto loginEmailShown = session->api().cloudPassword().state(\n\t\t) | rpl::map([](const Core::CloudPasswordState &state) {\n\t\t\treturn !state.loginEmailPattern.isEmpty();\n\t\t}) | rpl::distinct_until_changed();\n\n\t\tauto loginEmailLabel = session->api().cloudPassword().state(\n\t\t) | rpl::map([](const Core::CloudPasswordState &state) {\n\t\t\tauto email = state.loginEmailPattern;\n\t\t\tif (email.contains(' ')) {\n\t\t\t\treturn tr::lng_settings_cloud_password_off(tr::now);\n\t\t\t}\n\t\t\treturn Ui::Text::WrapEmailPattern(\n\t\t\t\temail.replace(QRegularExpression(\"\\\\*{4,}\"), \"****\")).text;\n\t\t});\n\n\t\tbuilder.addButton({\n\t\t\t.id = u\"security/login_email\"_q,\n\t\t\t.title = tr::lng_settings_cloud_login_email_section_title(),\n\t\t\t.st = &st::settingsButtonRightLabelSpoiler,\n\t\t\t.icon = { &st::menuIconRecoveryEmail },\n\t\t\t.label = std::move(loginEmailLabel),\n\t\t\t.onClick = [=] {\n\t\t\t\tUrlClickHandler::Open(u\"tg://settings/login_email\"_q);\n\t\t\t},\n\t\t\t.keywords = { u\"email\"_q, u\"login\"_q },\n\t\t\t.shown = std::move(loginEmailShown),\n\t\t});\n\t}\n\n\tauto blockedCount = rpl::combine(\n\t\tsession->api().blockedPeers().slice(\n\t\t) | rpl::map([](const Api::BlockedPeers::Slice &data) {\n\t\t\treturn data.total;\n\t\t}),\n\t\ttr::lng_settings_no_blocked_users()\n\t) | rpl::map([](int count, const QString &none) {\n\t\treturn count ? QString::number(count) : none;\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"security/blocked\"_q,\n\t\t.title = tr::lng_settings_blocked_users(),\n\t\t.icon = { &st::menuIconBlock },\n\t\t.label = std::move(blockedCount),\n\t\t.onClick = [=] {\n\t\t\tshowOther(BlockedPeersId());\n\t\t},\n\t\t.keywords = { u\"blocked\"_q, u\"ban\"_q },\n\t});\n\n\tbuilder.add([session, updateTrigger = rpl::duplicate(updateTrigger)](const WidgetContext &ctx) mutable {\n\t\tstd::move(updateTrigger) | rpl::on_next([=] {\n\t\t\tsession->api().blockedPeers().reload();\n\t\t}, ctx.container->lifetime());\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n\n\tauto websitesCount = session->api().websites().totalValue();\n\tauto websitesShown = rpl::duplicate(websitesCount) | rpl::map(\n\t\trpl::mappers::_1 > 0);\n\tauto websitesLabel = rpl::duplicate(\n\t\twebsitesCount\n\t) | rpl::filter(rpl::mappers::_1 > 0) | rpl::map([](int count) {\n\t\treturn QString::number(count);\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"security/websites\"_q,\n\t\t.title = tr::lng_settings_logged_in(),\n\t\t.icon = { &st::menuIconIpAddress },\n\t\t.label = std::move(websitesLabel),\n\t\t.onClick = [=] {\n\t\t\tshowOther(WebsitesId());\n\t\t},\n\t\t.keywords = { u\"websites\"_q, u\"bots\"_q, u\"logged\"_q },\n\t\t.shown = std::move(websitesShown),\n\t});\n\n\tbuilder.add([session, updateTrigger = rpl::duplicate(updateTrigger)](const WidgetContext &ctx) mutable {\n\t\tstd::move(updateTrigger) | rpl::on_next([=] {\n\t\t\tsession->api().websites().reload();\n\t\t}, ctx.container->lifetime());\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n\n\tauto sessionsCount = session->api().authorizations().totalValue(\n\t) | rpl::map([](int count) {\n\t\treturn count ? QString::number(count) : QString();\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"security/sessions\"_q,\n\t\t.title = tr::lng_settings_show_sessions(),\n\t\t.icon = { &st::menuIconDevices },\n\t\t.label = std::move(sessionsCount),\n\t\t.onClick = [=] {\n\t\t\tshowOther(SessionsId());\n\t\t},\n\t\t.keywords = { u\"sessions\"_q, u\"devices\"_q, u\"active\"_q },\n\t});\n\n\tbuilder.add([session, updateTrigger = std::move(updateTrigger)](const WidgetContext &ctx) mutable {\n\t\tstd::move(updateTrigger) | rpl::on_next([=] {\n\t\t\tsession->api().authorizations().reload();\n\t\t}, ctx.container->lifetime());\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n\n\tbuilder.addSkip();\n\tbuilder.addDividerText(tr::lng_settings_sessions_about());\n}\n\nvoid BuildPrivacySection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto session = builder.session();\n\n\tbuilder.addSkip(st::settingsPrivacySkip);\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"privacy/section\"_q,\n\t\t.title = tr::lng_settings_privacy_title(),\n\t\t.keywords = { u\"privacy\"_q, u\"visibility\"_q },\n\t});\n\n\tusing Key = Privacy::Key;\n\n\tbuilder.addPrivacyButton({\n\t\t.id = u\"privacy/phone_number\"_q,\n\t\t.title = tr::lng_settings_phone_number_privacy(),\n\t\t.key = Key::PhoneNumber,\n\t\t.controllerFactory = [=] {\n\t\t\treturn std::make_unique<PhoneNumberPrivacyController>(controller);\n\t\t},\n\t\t.keywords = { u\"phone\"_q, u\"number\"_q },\n\t});\n\n\tbuilder.addPrivacyButton({\n\t\t.id = u\"privacy/last_seen\"_q,\n\t\t.title = tr::lng_settings_last_seen(),\n\t\t.key = Key::LastSeen,\n\t\t.controllerFactory = [=] {\n\t\t\treturn std::make_unique<LastSeenPrivacyController>(session);\n\t\t},\n\t\t.keywords = { u\"last seen\"_q, u\"online\"_q },\n\t});\n\n\tbuilder.addPrivacyButton({\n\t\t.id = u\"privacy/profile_photo\"_q,\n\t\t.title = tr::lng_settings_profile_photo_privacy(),\n\t\t.key = Key::ProfilePhoto,\n\t\t.controllerFactory = [] {\n\t\t\treturn std::make_unique<ProfilePhotoPrivacyController>();\n\t\t},\n\t\t.keywords = { u\"photo\"_q, u\"avatar\"_q },\n\t});\n\n\tbuilder.addPrivacyButton({\n\t\t.id = u\"privacy/forwards\"_q,\n\t\t.title = tr::lng_settings_forwards_privacy(),\n\t\t.key = Key::Forwards,\n\t\t.controllerFactory = [=] {\n\t\t\treturn std::make_unique<ForwardsPrivacyController>(controller);\n\t\t},\n\t\t.keywords = { u\"forwards\"_q, u\"link\"_q },\n\t});\n\n\tbuilder.addPrivacyButton({\n\t\t.id = u\"privacy/calls\"_q,\n\t\t.title = tr::lng_settings_calls(),\n\t\t.key = Key::Calls,\n\t\t.controllerFactory = [] {\n\t\t\treturn std::make_unique<CallsPrivacyController>();\n\t\t},\n\t\t.keywords = { u\"calls\"_q, u\"voice\"_q },\n\t});\n\n\tbuilder.addPrivacyButton({\n\t\t.id = u\"privacy/voices\"_q,\n\t\t.title = tr::lng_settings_voices_privacy(),\n\t\t.key = Key::Voices,\n\t\t.controllerFactory = [=] {\n\t\t\treturn std::make_unique<VoicesPrivacyController>(session);\n\t\t},\n\t\t.premium = true,\n\t\t.keywords = { u\"voice\"_q, u\"messages\"_q },\n\t});\n\n\tconst auto privacy = &session->api().globalPrivacy();\n\tauto messagesLabel = rpl::combine(\n\t\tprivacy->newRequirePremium(),\n\t\tprivacy->newChargeStars()\n\t) | rpl::map([=](bool requirePremium, int chargeStars) {\n\t\treturn chargeStars\n\t\t\t? tr::lng_edit_privacy_paid()\n\t\t\t: requirePremium\n\t\t\t? tr::lng_edit_privacy_contacts_and_premium()\n\t\t\t: tr::lng_edit_privacy_everyone();\n\t}) | rpl::flatten_latest();\n\n\tconst auto messagesPremium = !session->appConfig().newRequirePremiumFree();\n\tconst auto messagesButton = builder.addButton({\n\t\t.id = u\"privacy/messages\"_q,\n\t\t.title = tr::lng_settings_messages_privacy(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.label = rpl::duplicate(messagesLabel),\n\t\t.onClick = [=] {\n\t\t\tcontroller->show(Box(EditMessagesPrivacyBox, controller, QString()));\n\t\t},\n\t\t.keywords = { u\"messages\"_q, u\"new\"_q, u\"unknown\"_q },\n\t});\n\tif (messagesPremium && messagesButton) {\n\t\tAddPrivacyPremiumStar(\n\t\t\tmessagesButton,\n\t\t\tsession,\n\t\t\tstd::move(messagesLabel),\n\t\t\tst::settingsButtonNoIcon.padding);\n\t}\n\n\tbuilder.addPrivacyButton({\n\t\t.id = u\"privacy/birthday\"_q,\n\t\t.title = tr::lng_settings_birthday_privacy(),\n\t\t.key = Key::Birthday,\n\t\t.controllerFactory = [] {\n\t\t\treturn std::make_unique<BirthdayPrivacyController>();\n\t\t},\n\t\t.keywords = { u\"birthday\"_q, u\"age\"_q },\n\t});\n\n\tbuilder.addPrivacyButton({\n\t\t.id = u\"privacy/gifts\"_q,\n\t\t.title = tr::lng_settings_gifts_privacy(),\n\t\t.key = Key::GiftsAutoSave,\n\t\t.controllerFactory = [] {\n\t\t\treturn std::make_unique<GiftsAutoSavePrivacyController>();\n\t\t},\n\t\t.keywords = { u\"gifts\"_q },\n\t});\n\n\tbuilder.addPrivacyButton({\n\t\t.id = u\"privacy/bio\"_q,\n\t\t.title = tr::lng_settings_bio_privacy(),\n\t\t.key = Key::About,\n\t\t.controllerFactory = [] {\n\t\t\treturn std::make_unique<AboutPrivacyController>();\n\t\t},\n\t\t.keywords = { u\"bio\"_q, u\"about\"_q },\n\t});\n\n\tbuilder.addPrivacyButton({\n\t\t.id = u\"privacy/saved_music\"_q,\n\t\t.title = tr::lng_settings_saved_music_privacy(),\n\t\t.key = Key::SavedMusic,\n\t\t.controllerFactory = [] {\n\t\t\treturn std::make_unique<SavedMusicPrivacyController>();\n\t\t},\n\t\t.keywords = { u\"music\"_q, u\"saved\"_q },\n\t});\n\n\tbuilder.addPrivacyButton({\n\t\t.id = u\"privacy/groups\"_q,\n\t\t.title = tr::lng_settings_groups_invite(),\n\t\t.key = Key::Invites,\n\t\t.controllerFactory = [] {\n\t\t\treturn std::make_unique<GroupsInvitePrivacyController>();\n\t\t},\n\t\t.keywords = { u\"groups\"_q, u\"invite\"_q },\n\t});\n\n\tsession->api().userPrivacy().reload(Privacy::Key::AddedByPhone);\n\n\tbuilder.addSkip(st::settingsPrivacySecurityPadding);\n\tbuilder.addDivider();\n}\n\nvoid BuildArchiveAndMuteSection(SectionBuilder &builder) {\n\tconst auto session = builder.session();\n\tconst auto privacy = &session->api().globalPrivacy();\n\n\tprivacy->reload();\n\n\tauto shown = rpl::single(\n\t\tfalse\n\t) | rpl::then(privacy->showArchiveAndMute(\n\t) | rpl::filter(rpl::mappers::_1) | rpl::take(1));\n\tauto premium = Data::AmPremiumValue(session);\n\n\tbuilder.scope([&] {\n\t\tbuilder.addSkip();\n\t\tbuilder.addSubsectionTitle({\n\t\t\t.id = u\"privacy/new_unknown\"_q,\n\t\t\t.title = tr::lng_settings_new_unknown(),\n\t\t\t.keywords = { u\"unknown\"_q, u\"archive\"_q, u\"mute\"_q },\n\t\t});\n\n\t\tconst auto toggle = builder.addButton({\n\t\t\t.id = u\"privacy/archive_and_mute\"_q,\n\t\t\t.title = tr::lng_settings_auto_archive(),\n\t\t\t.st = &st::settingsButtonNoIcon,\n\t\t\t.toggled = privacy->archiveAndMute(),\n\t\t\t.keywords = { u\"archive\"_q, u\"mute\"_q, u\"unknown\"_q },\n\t\t});\n\n\t\tif (toggle) {\n\t\t\ttoggle->toggledChanges(\n\t\t\t) | rpl::filter([=](bool toggled) {\n\t\t\t\treturn toggled != privacy->archiveAndMuteCurrent();\n\t\t\t}) | rpl::on_next([=](bool toggled) {\n\t\t\t\tprivacy->updateArchiveAndMute(toggled);\n\t\t\t}, toggle->lifetime());\n\t\t}\n\n\t\tbuilder.addSkip();\n\t\tbuilder.addDividerText(tr::lng_settings_auto_archive_about());\n\t}, rpl::combine(\n\t\tstd::move(shown),\n\t\tstd::move(premium),\n\t\trpl::mappers::_1 || rpl::mappers::_2));\n}\n\nvoid BuildBotsAndWebsitesSection(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto session = builder.session();\n\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"privacy/bots\"_q,\n\t\t.title = tr::lng_settings_security_bots(),\n\t\t.keywords = { u\"bots\"_q, u\"payment\"_q, u\"websites\"_q },\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"privacy/bots_payment\"_q,\n\t\t.title = tr::lng_settings_clear_payment_info(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.onClick = [=] {\n\t\t\tcontroller->show(ClearPaymentInfoBox(session));\n\t\t},\n\t\t.keywords = { u\"payment\"_q, u\"bots\"_q, u\"clear\"_q },\n\t});\n\n\tbuilder.addSkip();\n\tbuilder.addDivider();\n}\n\nvoid BuildTopPeersSection(SectionBuilder &builder) {\n\tconst auto session = builder.session();\n\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"privacy/top_peers\"_q,\n\t\t.title = tr::lng_settings_top_peers_title(),\n\t\t.keywords = { u\"suggest\"_q, u\"contacts\"_q, u\"frequent\"_q },\n\t});\n\n\tconst auto toggle = builder.addButton({\n\t\t.id = u\"privacy/top_peers_toggle\"_q,\n\t\t.title = tr::lng_settings_top_peers_suggest(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.toggled = rpl::single(\n\t\t\trpl::empty\n\t\t) | rpl::then(\n\t\t\tsession->topPeers().updates()\n\t\t) | rpl::map([=] {\n\t\t\treturn !session->topPeers().disabled();\n\t\t}),\n\t\t.keywords = { u\"suggest\"_q, u\"contacts\"_q },\n\t});\n\n\tif (toggle) {\n\t\ttoggle->toggledChanges(\n\t\t) | rpl::filter([=](bool enabled) {\n\t\t\treturn enabled == session->topPeers().disabled();\n\t\t}) | rpl::on_next([=](bool enabled) {\n\t\t\tsession->topPeers().toggleDisabled(!enabled);\n\t\t}, toggle->lifetime());\n\t}\n\n\tbuilder.addSkip();\n\tbuilder.addDividerText(tr::lng_settings_top_peers_about());\n}\n\nvoid BuildSelfDestructionSection(\n\t\tSectionBuilder &builder,\n\t\trpl::producer<> updateTrigger) {\n\tconst auto controller = builder.controller();\n\tconst auto session = builder.session();\n\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"privacy/self_destruct\"_q,\n\t\t.title = tr::lng_settings_destroy_title(),\n\t\t.keywords = { u\"delete\"_q, u\"destroy\"_q, u\"inactive\"_q, u\"account\"_q },\n\t});\n\n\tbuilder.add([session, updateTrigger = std::move(updateTrigger)](const WidgetContext &ctx) mutable {\n\t\tstd::move(updateTrigger) | rpl::on_next([=] {\n\t\t\tsession->api().selfDestruct().reload();\n\t\t}, ctx.container->lifetime());\n\t\treturn SectionBuilder::WidgetToAdd{};\n\t});\n\n\tauto label = session->api().selfDestruct().daysAccountTTL(\n\t) | rpl::map(SelfDestructionBox::DaysLabel);\n\n\tbuilder.addButton({\n\t\t.id = u\"privacy/self_destruct_button\"_q,\n\t\t.title = tr::lng_settings_destroy_if(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.label = std::move(label),\n\t\t.onClick = [=] {\n\t\t\tcontroller->show(Box<SelfDestructionBox>(\n\t\t\t\tsession,\n\t\t\t\tSelfDestructionBox::Type::Account,\n\t\t\t\tsession->api().selfDestruct().daysAccountTTL()));\n\t\t},\n\t\t.keywords = { u\"delete\"_q, u\"destroy\"_q, u\"inactive\"_q },\n\t});\n\n\tbuilder.addSkip();\n}\n\nvoid BuildConfirmationExtensions(SectionBuilder &builder) {\n\tconst auto controller = builder.controller();\n\tconst auto hasExtensions = !Core::App().settings().noWarningExtensions().empty()\n\t\t|| !Core::App().settings().ipRevealWarning();\n\n\tif (!hasExtensions) {\n\t\treturn;\n\t}\n\n\tbuilder.addSkip();\n\tbuilder.addSubsectionTitle({\n\t\t.id = u\"privacy/file_confirmations\"_q,\n\t\t.title = tr::lng_settings_file_confirmations(),\n\t\t.keywords = { u\"extensions\"_q, u\"files\"_q, u\"confirmations\"_q },\n\t});\n\n\tbuilder.addButton({\n\t\t.id = u\"privacy/file_confirmations_button\"_q,\n\t\t.title = tr::lng_settings_edit_extensions(),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.onClick = [=] {\n\t\t\tcontroller->show(Box(OpenFileConfirmationsBox));\n\t\t},\n\t\t.keywords = { u\"extensions\"_q, u\"files\"_q, u\"confirmations\"_q },\n\t});\n\n\tbuilder.addSkip();\n\tbuilder.addDividerText(tr::lng_settings_edit_extensions_about());\n}\n\nvoid BuildPrivacySecuritySectionContent(SectionBuilder &builder) {\n\tauto updateOnTick = rpl::single(\n\t) | rpl::then(base::timer_each(kUpdateTimeout));\n\tconst auto trigger = [&] {\n\t\treturn rpl::duplicate(updateOnTick);\n\t};\n\n\tBuildSecuritySection(builder, trigger());\n\tBuildPrivacySection(builder);\n\tBuildArchiveAndMuteSection(builder);\n\tBuildBotsAndWebsitesSection(builder);\n\tBuildConfirmationExtensions(builder);\n\tBuildTopPeersSection(builder);\n\tBuildSelfDestructionSection(builder, trigger());\n}\n\nclass PrivacySecurity : public Section<PrivacySecurity> {\npublic:\n\tPrivacySecurity(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\nprivate:\n\tvoid setupContent();\n\n};\n\nconst auto kMeta = BuildHelper({\n\t.id = PrivacySecurity::Id(),\n\t.parentId = MainId(),\n\t.title = &tr::lng_settings_section_privacy,\n\t.icon = &st::menuIconLock,\n}, [](SectionBuilder &builder) {\n\tBuildPrivacySecuritySectionContent(builder);\n});\n\nconst SectionBuildMethod kPrivacySecuritySection = kMeta.build;\n\nPrivacySecurity::PrivacySecurity(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent();\n\t[[maybe_unused]] auto preload = base::SystemUnlockStatus();\n}\n\nrpl::producer<QString> PrivacySecurity::title() {\n\treturn tr::lng_settings_section_privacy();\n}\n\nvoid PrivacySecurity::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\tbuild(content, kPrivacySecuritySection);\n\tUi::ResizeFitChild(this, content);\n}\n\n} // namespace\n\nType PrivacySecurityId() {\n\treturn PrivacySecurity::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_privacy_security.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_common.h\"\n#include \"api/api_user_privacy.h\"\n\nclass EditPrivacyController;\n\nnamespace Ui {\nclass BoxContent;\nclass GenericBox;\n} // namespace Ui\n\nnamespace Settings {\n\n[[nodiscard]] Type PrivacySecurityId();\n\nint ExceptionUsersCount(const std::vector<not_null<PeerData*>> &exceptions);\n\nbool CheckEditCloudPassword(not_null<::Main::Session*> session);\nobject_ptr<Ui::BoxContent> EditCloudPasswordBox(\n\tnot_null<::Main::Session*> session);\nobject_ptr<Ui::BoxContent> ClearPaymentInfoBox(\n\tnot_null<::Main::Session*> session);\nvoid OpenFileConfirmationsBox(not_null<Ui::GenericBox*> box);\nvoid RemoveCloudPassword(not_null<Window::SessionController*> session);\nobject_ptr<Ui::BoxContent> CloudPasswordAppOutdatedBox();\n\nnot_null<Ui::SettingsButton*> AddPrivacyButton(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container,\n\trpl::producer<QString> label,\n\tIconDescriptor &&descriptor,\n\tApi::UserPrivacy::Key key,\n\tFn<std::unique_ptr<EditPrivacyController>()> controllerFactory,\n\tconst style::SettingsButton *stOverride = nullptr);\n\n[[nodiscard]] rpl::producer<QString> PrivacyButtonLabel(\n\tnot_null<::Main::Session*> session,\n\tApi::UserPrivacy::Key key);\n\nvoid AddPrivacyPremiumStar(\n\tnot_null<Ui::SettingsButton*> button,\n\tnot_null<::Main::Session*> session,\n\trpl::producer<QString> label,\n\tconst QMargins &padding);\n\nvoid SetupArchiveAndMute(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container,\n\tHighlightRegistry *highlights = nullptr);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_shortcuts.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_shortcuts.h\"\n\n#include \"base/event_filter.h\"\n#include \"core/application.h\"\n#include \"core/shortcuts.h\"\n#include \"lang/lang_keys.h\"\n#include \"settings/sections/settings_chat.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common_session.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n\n#include <private/qkeymapper_p.h>\n\nnamespace Settings {\nnamespace {\n\nnamespace S = ::Shortcuts;\n\nusing namespace Builder;\n\nstruct Labeled {\n\tS::Command command = {};\n\trpl::producer<QString> label;\n};\n\n[[nodiscard]] std::vector<Labeled> Entries() {\n\tusing C = S::Command;\n\tconst auto pinned = [](int index) {\n\t\treturn tr::lng_shortcuts_chat_pinned_n(\n\t\t\tlt_index,\n\t\t\trpl::single(QString::number(index)));\n\t};\n\tconst auto account = [](int index) {\n\t\treturn tr::lng_shortcuts_show_account_n(\n\t\t\tlt_index,\n\t\t\trpl::single(QString::number(index)));\n\t};\n\tconst auto folder = [](int index) {\n\t\treturn tr::lng_shortcuts_show_folder_n(\n\t\t\tlt_index,\n\t\t\trpl::single(QString::number(index)));\n\t};\n\tconst auto separator = Labeled{ C(), nullptr };\n\treturn {\n\t\t{ C::Close, tr::lng_shortcuts_close() },\n\t\t{ C::Lock, tr::lng_shortcuts_lock() },\n\t\t{ C::Minimize, tr::lng_shortcuts_minimize() },\n\t\t{ C::Quit, tr::lng_shortcuts_quit() },\n\t\tseparator,\n\t\t{ C::Search, tr::lng_shortcuts_search() },\n\t\tseparator,\n\t\t{ C::ChatPrevious, tr::lng_shortcuts_chat_previous() },\n\t\t{ C::ChatNext, tr::lng_shortcuts_chat_next() },\n\t\t{ C::ChatFirst, tr::lng_shortcuts_chat_first() },\n\t\t{ C::ChatLast, tr::lng_shortcuts_chat_last() },\n\t\t{ C::ChatSelf, tr::lng_shortcuts_chat_self() },\n\t\tseparator,\n\t\t{ C::ChatPinned1, pinned(1) },\n\t\t{ C::ChatPinned2, pinned(2) },\n\t\t{ C::ChatPinned3, pinned(3) },\n\t\t{ C::ChatPinned4, pinned(4) },\n\t\t{ C::ChatPinned5, pinned(5) },\n\t\t{ C::ChatPinned6, pinned(6) },\n\t\t{ C::ChatPinned7, pinned(7) },\n\t\t{ C::ChatPinned8, pinned(8) },\n\t\tseparator,\n\t\t{ C::ShowAccount1, account(1) },\n\t\t{ C::ShowAccount2, account(2) },\n\t\t{ C::ShowAccount3, account(3) },\n\t\t{ C::ShowAccount4, account(4) },\n\t\t{ C::ShowAccount5, account(5) },\n\t\t{ C::ShowAccount6, account(6) },\n\t\tseparator,\n\t\t{ C::ShowAllChats, tr::lng_shortcuts_show_all_chats() },\n\t\t{ C::ShowFolder1, folder(1) },\n\t\t{ C::ShowFolder2, folder(2) },\n\t\t{ C::ShowFolder3, folder(3) },\n\t\t{ C::ShowFolder4, folder(4) },\n\t\t{ C::ShowFolder5, folder(5) },\n\t\t{ C::ShowFolder6, folder(6) },\n\t\t{ C::ShowFolderLast, tr::lng_shortcuts_show_folder_last() },\n\t\t{ C::FolderNext, tr::lng_shortcuts_folder_next() },\n\t\t{ C::FolderPrevious, tr::lng_shortcuts_folder_previous() },\n\t\t{ C::ShowArchive, tr::lng_shortcuts_archive() },\n\t\t{ C::ShowContacts, tr::lng_shortcuts_contacts() },\n\t\tseparator,\n\t\t{ C::ReadChat, tr::lng_shortcuts_read_chat() },\n\t\t{ C::ArchiveChat, tr::lng_shortcuts_archive_chat() },\n\t\t{ C::ShowScheduled, tr::lng_shortcuts_scheduled() },\n\t\t{ C::ShowChatMenu, tr::lng_shortcuts_show_chat_menu() },\n\t\t{ C::ShowChatPreview, tr::lng_shortcuts_show_chat_preview() },\n\t\tseparator,\n\t\t{ C::JustSendMessage, tr::lng_shortcuts_just_send() },\n\t\t{ C::SendSilentMessage, tr::lng_shortcuts_silent_send() },\n\t\t{ C::ScheduleMessage, tr::lng_shortcuts_schedule() },\n\t\tseparator,\n\t\t{ C::RecordVoice, tr::lng_shortcuts_record_voice_message() },\n\t\t{ C::RecordRound, tr::lng_shortcuts_record_round_message() },\n\t\tseparator,\n\t\t{ C::ShowAdminLog, tr::lng_shortcuts_admin_log() },\n\t\tseparator,\n\t\t{ C::MediaViewerFullscreen, tr::lng_shortcuts_media_fullscreen() },\n\t\tseparator,\n\t\t{ C::MediaPlay, tr::lng_shortcuts_media_play() },\n\t\t{ C::MediaPause, tr::lng_shortcuts_media_pause() },\n\t\t{ C::MediaPlayPause, tr::lng_shortcuts_media_play_pause() },\n\t\t{ C::MediaStop, tr::lng_shortcuts_media_stop() },\n\t\t{ C::MediaPrevious, tr::lng_shortcuts_media_previous() },\n\t\t{ C::MediaNext, tr::lng_shortcuts_media_next() },\n\t};\n}\n\n[[nodiscard]] QString ToString(const QKeySequence &key) {\n\tauto result = key.toString();\n#ifdef Q_OS_MAC\n\tresult = result.replace(u\"Ctrl+\"_q, QString() + QChar(0x2318));\n\tresult = result.replace(u\"Meta+\"_q, QString() + QChar(0x2303));\n\tresult = result.replace(u\"Alt+\"_q, QString() + QChar(0x2325));\n\tresult = result.replace(u\"Shift+\"_q, QString() + QChar(0x21E7));\n#endif // Q_OS_MAC\n\treturn result;\n}\n\nstruct SetupShortcutsResult {\n\tFn<void()> save;\n\tQPointer<Ui::RpWidget> resetButton;\n};\n\n[[nodiscard]] SetupShortcutsResult SetupShortcutsContent(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> content) {\n\tconst auto &defaults = S::KeysDefaults();\n\tconst auto &currents = S::KeysCurrents();\n\n\tstruct Button {\n\t\tS::Command command;\n\t\tstd::unique_ptr<Ui::SettingsButton> widget;\n\t\trpl::variable<QKeySequence> key;\n\t\trpl::variable<bool> removed;\n\t};\n\tstruct Entry {\n\t\tS::Command command;\n\t\trpl::producer<QString> label;\n\t\tstd::vector<QKeySequence> original;\n\t\tstd::vector<QKeySequence> now;\n\t\tUi::VerticalLayout *wrap = nullptr;\n\t\tstd::vector<std::unique_ptr<Button>> buttons;\n\t};\n\tstruct State {\n\t\tstd::vector<Entry> entries;\n\t\trpl::variable<bool> modified;\n\t\trpl::variable<Button*> recording;\n\t\trpl::variable<QKeySequence> lastKey;\n\t\tFn<void(S::Command command)> showMenuFor;\n\t};\n\tconst auto state = content->lifetime().make_state<State>();\n\tconst auto labeled = Entries();\n\tauto &entries = state->entries = ranges::views::all(\n\t\tlabeled\n\t) | ranges::views::transform([](Labeled labeled) {\n\t\treturn Entry{ labeled.command, std::move(labeled.label) };\n\t}) | ranges::to_vector;\n\n\tfor (const auto &[keys, commands] : defaults) {\n\t\tfor (const auto command : commands) {\n\t\t\tconst auto i = ranges::find(entries, command, &Entry::command);\n\t\t\tif (i != end(entries)) {\n\t\t\t\ti->original.push_back(keys);\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const auto &[keys, commands] : currents) {\n\t\tfor (const auto command : commands) {\n\t\t\tconst auto i = ranges::find(entries, command, &Entry::command);\n\t\t\tif (i != end(entries)) {\n\t\t\t\ti->now.push_back(keys);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst auto checkModified = [=] {\n\t\tfor (const auto &entry : state->entries) {\n\t\t\tauto original = entry.original;\n\t\t\tauto now = entry.now;\n\t\t\tranges::sort(original);\n\t\t\tranges::sort(now);\n\t\t\tif (original != now) {\n\t\t\t\tstate->modified = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tstate->modified = false;\n\t};\n\tcheckModified();\n\n\tconst auto menu = std::make_shared<base::weak_qptr<Ui::PopupMenu>>();\n\tconst auto fill = [=](Entry &entry) {\n\t\tauto index = 0;\n\t\tif (entry.original.empty()) {\n\t\t\tentry.original.push_back(QKeySequence());\n\t\t}\n\t\tif (entry.now.empty()) {\n\t\t\tentry.now.push_back(QKeySequence());\n\t\t}\n\t\tfor (const auto &now : entry.now) {\n\t\t\tif (index < entry.buttons.size()) {\n\t\t\t\tentry.buttons[index]->key = now;\n\t\t\t\tentry.buttons[index]->removed = false;\n\t\t\t} else {\n\t\t\t\tauto button = std::make_unique<Button>(Button{\n\t\t\t\t\t.command = entry.command,\n\t\t\t\t\t.key = now,\n\t\t\t\t});\n\t\t\t\tconst auto raw = button.get();\n\t\t\t\tconst auto widget = entry.wrap->add(\n\t\t\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\t\t\tentry.wrap,\n\t\t\t\t\t\trpl::duplicate(entry.label),\n\t\t\t\t\t\tst::settingsButtonNoIcon));\n\t\t\t\tconst auto keys = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\t\twidget,\n\t\t\t\t\tst::settingsButtonNoIcon.rightLabel);\n\t\t\t\tkeys->show();\n\t\t\t\trpl::combine(\n\t\t\t\t\twidget->widthValue(),\n\t\t\t\t\trpl::duplicate(entry.label),\n\t\t\t\t\tbutton->key.value(),\n\t\t\t\t\tstate->recording.value(),\n\t\t\t\t\tbutton->removed.value()\n\t\t\t\t) | rpl::on_next([=](\n\t\t\t\t\t\tint width,\n\t\t\t\t\t\tconst QString &button,\n\t\t\t\t\t\tconst QKeySequence &key,\n\t\t\t\t\t\tButton *recording,\n\t\t\t\t\t\tbool removed) {\n\t\t\t\t\tconst auto &st = st::settingsButtonNoIcon;\n\t\t\t\t\tconst auto available = width\n\t\t\t\t\t\t- st.padding.left()\n\t\t\t\t\t\t- st.padding.right()\n\t\t\t\t\t\t- st.style.font->width(button)\n\t\t\t\t\t\t- st::settingsButtonRightSkip;\n\t\t\t\t\tkeys->setMarkedText((recording == raw)\n\t\t\t\t\t\t? tr::italic(\n\t\t\t\t\t\t\ttr::lng_shortcuts_recording(tr::now))\n\t\t\t\t\t\t: key.isEmpty()\n\t\t\t\t\t\t? TextWithEntities()\n\t\t\t\t\t\t: removed\n\t\t\t\t\t\t? Ui::Text::Wrapped(\n\t\t\t\t\t\t\tTextWithEntities{ ToString(key) },\n\t\t\t\t\t\t\tEntityType::StrikeOut)\n\t\t\t\t\t\t: TextWithEntities{ ToString(key) });\n\t\t\t\t\tkeys->setTextColorOverride((recording == raw)\n\t\t\t\t\t\t? st::boxTextFgGood->c\n\t\t\t\t\t\t: removed\n\t\t\t\t\t\t? st::attentionButtonFg->c\n\t\t\t\t\t\t: std::optional<QColor>());\n\t\t\t\t\tkeys->resizeToNaturalWidth(available);\n\t\t\t\t\tkeys->moveToRight(\n\t\t\t\t\t\tst::settingsButtonRightSkip,\n\t\t\t\t\t\tst.padding.top());\n\t\t\t\t}, keys->lifetime());\n\t\t\t\tkeys->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t\t\t\twidget->setAcceptBoth(true);\n\t\t\t\twidget->clicks(\n\t\t\t\t) | rpl::on_next([=](Qt::MouseButton button) {\n\t\t\t\t\tif (const auto strong = *menu) {\n\t\t\t\t\t\tstrong->hideMenu();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (button == Qt::RightButton) {\n\t\t\t\t\t\tstate->showMenuFor(raw->command);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tS::Pause();\n\t\t\t\t\t\tstate->recording = raw;\n\t\t\t\t\t}\n\t\t\t\t}, widget->lifetime());\n\n\t\t\t\tbutton->widget.reset(widget);\n\t\t\t\tentry.buttons.push_back(std::move(button));\n\t\t\t}\n\t\t\t++index;\n\t\t}\n\t\twhile (entry.wrap->count() > index) {\n\t\t\tentry.buttons.pop_back();\n\t\t}\n\t};\n\tstate->showMenuFor = [=](S::Command command) {\n\t\t*menu = Ui::CreateChild<Ui::PopupMenu>(\n\t\t\tcontent,\n\t\t\tst::popupMenuWithIcons);\n\t\t(*menu)->addAction(tr::lng_shortcuts_add_another(tr::now), [=] {\n\t\t\tconst auto i = ranges::find(\n\t\t\t\tstate->entries,\n\t\t\t\tcommand,\n\t\t\t\t&Entry::command);\n\t\t\tif (i != end(state->entries)) {\n\t\t\t\tS::Pause();\n\t\t\t\tconst auto j = ranges::find(i->now, QKeySequence());\n\t\t\t\tif (j != end(i->now)) {\n\t\t\t\t\tstate->recording = i->buttons[j - begin(i->now)].get();\n\t\t\t\t} else {\n\t\t\t\t\ti->now.push_back(QKeySequence());\n\t\t\t\t\tfill(*i);\n\t\t\t\t\tstate->recording = i->buttons.back().get();\n\t\t\t\t}\n\t\t\t}\n\t\t}, &st::menuIconTopics);\n\t\t(*menu)->popup(QCursor::pos());\n\t};\n\n\tconst auto stopRecording = [=](std::optional<QKeySequence> result = {}) {\n\t\tconst auto button = state->recording.current();\n\t\tif (!button) {\n\t\t\treturn;\n\t\t}\n\t\tstate->recording = nullptr;\n\t\tInvokeQueued(content, [=] {\n\t\t\tInvokeQueued(content, [=] {\n\t\t\t\tS::Unpause();\n\t\t\t});\n\t\t});\n\t\tauto was = button->key.current();\n\t\tconst auto now = result.value_or(was);\n\t\tif (now == was) {\n\t\t\tif (!now.isEmpty() && (!result || !button->removed.current())) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\twas = QKeySequence();\n\t\t\tbutton->removed = false;\n\t\t}\n\n\t\tauto changed = false;\n\t\tconst auto command = button->command;\n\t\tfor (auto &entry : state->entries) {\n\t\t\tconst auto i = ranges::find(\n\t\t\t\tentry.buttons,\n\t\t\t\tbutton,\n\t\t\t\t&std::unique_ptr<Button>::get);\n\t\t\tif (i != end(entry.buttons)) {\n\t\t\t\tconst auto index = i - begin(entry.buttons);\n\t\t\t\tif (now.isEmpty()) {\n\t\t\t\t\tentry.now.erase(begin(entry.now) + index);\n\t\t\t\t} else {\n\t\t\t\t\tconst auto i = ranges::find(entry.now, now);\n\t\t\t\t\tif (i == end(entry.now)) {\n\t\t\t\t\t\tentry.now[index] = now;\n\t\t\t\t\t} else if (i != begin(entry.now) + index) {\n\t\t\t\t\t\tstd::swap(entry.now[index], *i);\n\t\t\t\t\t\tentry.now.erase(i);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfill(entry);\n\t\t\t\tcheckModified();\n\t\t\t} else if (now != was) {\n\t\t\t\tconst auto i = now.isEmpty()\n\t\t\t\t\t? end(entry.now)\n\t\t\t\t\t: ranges::find(entry.now, now);\n\t\t\t\tif (i != end(entry.now)) {\n\t\t\t\t\tentry.buttons[i - begin(entry.now)]->removed = true;\n\t\t\t\t}\n\t\t\t\tconst auto j = was.isEmpty()\n\t\t\t\t\t? end(entry.now)\n\t\t\t\t\t: ranges::find(entry.now, was);\n\t\t\t\tif (j != end(entry.now)) {\n\t\t\t\t\tentry.buttons[j - begin(entry.now)]->removed = false;\n\t\t\t\t\tS::Change(was, now, command, entry.command);\n\t\t\t\t\twas = QKeySequence();\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!changed) {\n\t\t\tS::Change(was, now, command);\n\t\t}\n\t};\n\tbase::install_event_filter(content, qApp, [=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::ShortcutOverride && state->recording.current()) {\n\t\t\tif (!content->window()->isActiveWindow()) {\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t}\n\t\t\tconst auto key = static_cast<QKeyEvent*>(e.get());\n\t\t\tconst auto m = key->modifiers();\n\t\t\tconst auto k = key->key();\n\t\t\tconst auto clear = !m\n\t\t\t\t&& (k == Qt::Key_Backspace || k == Qt::Key_Delete);\n\t\t\tif (k == Qt::Key_Control\n\t\t\t\t|| k == Qt::Key_Shift\n\t\t\t\t|| k == Qt::Key_Alt\n\t\t\t\t|| k == Qt::Key_Meta) {\n\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t} else if (!m && !clear && !S::AllowWithoutModifiers(k)) {\n\t\t\t\tif (k != Qt::Key_Escape) {\n\t\t\t\t\tstopRecording();\n\t\t\t\t}\n\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t}\n\t\t\tconst auto r = [&] {\n\t\t\t\tauto result = int(k);\n\t\t\t\tif (m & Qt::ShiftModifier) {\n\t\t\t\t\tconst auto keys = QKeyMapper::possibleKeys(key);\n\t\t\t\t\tfor (const auto &possible : keys) {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\t\t\t\t\t\tif (possible.keyboardModifiers() == m) {\n\t\t\t\t\t\t\treturn int(possible.key());\n\t\t\t\t\t\t}\n#else // Qt >= 6.7.0\n\t\t\t\t\t\tif (possible > int(m)) {\n\t\t\t\t\t\t\treturn possible - int(m);\n\t\t\t\t\t\t}\n#endif // Qt < 6.7.0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t}();\n\t\t\tstopRecording(clear ? QKeySequence() : QKeySequence(r | m));\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t} else if (type == QEvent::KeyPress && state->recording.current()) {\n\t\t\tif (!content->window()->isActiveWindow()) {\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t}\n\t\t\tif (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Escape) {\n\t\t\t\tstopRecording();\n\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t}\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\tconst auto modifiedWrap = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::VerticalLayout>(content)));\n\tconst auto modifiedInner = modifiedWrap->entity();\n\tAddDivider(modifiedInner);\n\tAddSkip(modifiedInner);\n\tconst auto reset = modifiedInner->add(object_ptr<Ui::SettingsButton>(\n\t\tmodifiedInner,\n\t\ttr::lng_shortcuts_reset(),\n\t\tst::settingsButtonNoIcon));\n\treset->setClickedCallback([=] {\n\t\tstopRecording();\n\t\tfor (auto &entry : state->entries) {\n\t\t\tif (entry.now != entry.original) {\n\t\t\t\tentry.now = entry.original;\n\t\t\t\tfill(entry);\n\t\t\t}\n\t\t}\n\t\tcheckModified();\n\t\tS::ResetToDefaults();\n\t});\n\tAddSkip(modifiedInner);\n\tAddDivider(modifiedInner);\n\tmodifiedWrap->toggleOn(state->modified.value());\n\n\tAddSkip(content);\n\tfor (auto &entry : entries) {\n\t\tif (!entry.label) {\n\t\t\tAddSkip(content);\n\t\t\tAddDivider(content);\n\t\t\tAddSkip(content);\n\t\t\tcontinue;\n\t\t}\n\t\tentry.wrap = content->add(object_ptr<Ui::VerticalLayout>(content));\n\t\tfill(entry);\n\t}\n\n\treturn {\n\t\t.save = [=] {},\n\t\t.resetButton = reset,\n\t};\n}\n\nvoid BuildShortcutsSection(SectionBuilder &builder) {\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"shortcuts/reset\"_q,\n\t\t\t.title = tr::lng_shortcuts_reset(tr::now),\n\t\t\t.keywords = { u\"reset\"_q, u\"defaults\"_q, u\"restore\"_q },\n\t\t};\n\t});\n}\n\nclass Shortcuts : public Section<Shortcuts> {\npublic:\n\tShortcuts(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\t~Shortcuts();\n\n\tvoid showFinished() override;\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\nprivate:\n\tvoid setupContent();\n\n\tFn<void()> _save;\n\tQPointer<Ui::RpWidget> _resetButton;\n\n};\n\nShortcuts::Shortcuts(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent();\n}\n\nShortcuts::~Shortcuts() {\n\tif (!Core::Quitting()) {\n\t\t_save();\n\t}\n}\n\nrpl::producer<QString> Shortcuts::title() {\n\treturn tr::lng_settings_shortcuts();\n}\n\nvoid Shortcuts::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tconst SectionBuildMethod buildMethod = [\n\t\tresetButton = &_resetButton,\n\t\tsave = &_save\n\t](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tauto &lifetime = container->lifetime();\n\t\tconst auto highlights = lifetime.make_state<HighlightRegistry>();\n\t\tconst auto isPaused = Window::PausedIn(\n\t\t\tcontroller,\n\t\t\tWindow::GifPauseReason::Layer);\n\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = isPaused,\n\t\t\t.highlights = highlights,\n\t\t});\n\n\t\tauto result = SetupShortcutsContent(controller, container);\n\t\t*save = std::move(result.save);\n\t\t*resetButton = result.resetButton;\n\n\t\tif (highlights && *resetButton) {\n\t\t\thighlights->push_back({\n\t\t\t\tu\"shortcuts/reset\"_q,\n\t\t\t\t{ resetButton->data(), { .rippleShape = true } },\n\t\t\t});\n\t\t}\n\n\t\tstd::move(showFinished) | rpl::on_next([=] {\n\t\t\tfor (const auto &[id, entry] : *highlights) {\n\t\t\t\tif (entry.widget) {\n\t\t\t\t\tcontroller->checkHighlightControl(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tentry.widget,\n\t\t\t\t\t\tbase::duplicate(entry.args));\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime);\n\t};\n\n\tbuild(content, buildMethod);\n\n\tUi::ResizeFitChild(this, content);\n}\n\nvoid Shortcuts::showFinished() {\n\tSection<Shortcuts>::showFinished();\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = Shortcuts::Id(),\n\t.parentId = ChatId(),\n\t.title = &tr::lng_settings_shortcuts,\n\t.icon = &st::menuIconShortcut,\n}, [](SectionBuilder &builder) {\n\tBuildShortcutsSection(builder);\n});\n\n} // namespace\n\nType ShortcutsId() {\n\treturn Shortcuts::Id();\n}\n\nnamespace Builder {\n\nSectionBuildMethod ShortcutsSection = kMeta.build;\n\n} // namespace Builder\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_shortcuts.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Settings {\n\n[[nodiscard]] Type ShortcutsId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_websites.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/sections/settings_websites.h\"\n\n#include \"api/api_websites.h\"\n#include \"settings/settings_common_session.h\"\n#include \"apiwrap.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_active_sessions.h\"\n#include \"settings/sections/settings_privacy_security.h\"\n#include \"settings/settings_builder.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Settings {\nnamespace {\n\nusing namespace Builder;\n\nconstexpr auto kShortPollTimeout = 60 * crl::time(1000);\n\nusing EntryData = Api::Websites::Entry;\n\nclass Row;\n\nclass RowDelegate {\npublic:\n\tvirtual void rowUpdateRow(not_null<Row*> row) = 0;\n};\n\nclass Row final : public PeerListRow {\npublic:\n\tRow(not_null<RowDelegate*> delegate, const EntryData &data);\n\n\tvoid update(const EntryData &data);\n\tvoid updateName(const QString &name);\n\n\t[[nodiscard]] EntryData data() const;\n\n\tQString generateName() override;\n\tQString generateShortName() override;\n\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\n\tQSize rightActionSize() const override {\n\t\treturn elementGeometry(2, 0).size();\n\t}\n\tQMargins rightActionMargins() const override {\n\t\tconst auto rect = elementGeometry(2, 0);\n\t\treturn QMargins(0, rect.y(), -(rect.x() + rect.width()), 0);\n\t}\n\n\tint elementsCount() const override;\n\tQRect elementGeometry(int element, int outerWidth) const override;\n\tbool elementDisabled(int element) const override;\n\tbool elementOnlySelect(int element) const override;\n\tvoid elementAddRipple(\n\t\tint element,\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) override;\n\tvoid elementsStopLastRipple() override;\n\tvoid elementsPaint(\n\t\tPainter &p,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tint selectedElement) override;\n\nprivate:\n\tconst not_null<RowDelegate*> _delegate;\n\tQImage _emptyUserpic;\n\tUi::PeerUserpicView _userpic;\n\tUi::Text::String _location;\n\tEntryData _data;\n\n};\n\n[[nodiscard]] QString JoinNonEmpty(QStringList list) {\n\tlist.erase(ranges::remove(list, QString()), list.end());\n\treturn list.join(\", \");\n}\n\n[[nodiscard]] QString LocationAndDate(const EntryData &entry) {\n\treturn (entry.location.isEmpty() ? entry.ip : entry.location)\n\t\t+ (entry.hash\n\t\t\t? ((' ' + Ui::kQBullet + ' ') + entry.active)\n\t\t\t: QString());\n}\n\nvoid InfoBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst EntryData &data,\n\t\tFn<void(uint64)> terminate) {\n\tbox->setWidth(st::boxWideWidth);\n\n\tconst auto shown = box->lifetime().make_state<rpl::event_stream<>>();\n\tbox->setShowFinishedCallback([=] {\n\t\tshown->fire({});\n\t});\n\n\tconst auto userpic = box->addRow(\n\t\tobject_ptr<Ui::UserpicButton>(\n\t\t\tbox,\n\t\t\tdata.bot,\n\t\t\tst::websiteBigUserpic),\n\t\tst::sessionBigCoverPadding,\n\t\tstyle::al_top);\n\tuserpic->overrideShape(Ui::PeerUserpicShape::Forum);\n\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\trpl::single(data.bot->name()),\n\t\t\tst::sessionBigName),\n\t\tstyle::al_top);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\trpl::single(data.domain),\n\t\t\tst::sessionDateLabel),\n\t\tstyle::margins(0, 0, 0, st::sessionDateSkip),\n\t\tstyle::al_top);\n\n\tconst auto container = box->verticalLayout();\n\tUi::AddDivider(container);\n\tUi::AddSkip(container, st::sessionSubtitleSkip);\n\tUi::AddSubsectionTitle(container, tr::lng_sessions_info());\n\n\tAddSessionInfoRow(\n\t\tcontainer,\n\t\ttr::lng_sessions_browser(),\n\t\tJoinNonEmpty({ data.browser, data.platform }),\n\t\tst::menuIconDevices);\n\tAddSessionInfoRow(\n\t\tcontainer,\n\t\ttr::lng_sessions_ip(),\n\t\tdata.ip,\n\t\tst::menuIconIpAddress);\n\tAddSessionInfoRow(\n\t\tcontainer,\n\t\ttr::lng_sessions_location(),\n\t\tdata.location,\n\t\tst::menuIconAddress);\n\n\tUi::AddSkip(container, st::sessionValueSkip);\n\tif (!data.location.isEmpty()) {\n\t\tUi::AddDividerText(container, tr::lng_sessions_location_about());\n\t}\n\n\tbox->addButton(tr::lng_about_done(), [=] { box->closeBox(); });\n\tif (const auto hash = data.hash) {\n\t\tbox->addLeftButton(tr::lng_settings_disconnect(), [=] {\n\t\t\tconst auto weak = base::make_weak(box.get());\n\t\t\tterminate(hash);\n\t\t\tif (weak) {\n\t\t\t\tbox->closeBox();\n\t\t\t}\n\t\t}, st::attentionBoxButton);\n\t}\n}\n\nRow::Row(not_null<RowDelegate*> delegate, const EntryData &data)\n: PeerListRow(data.hash)\n, _delegate(delegate)\n, _location(st::defaultTextStyle, LocationAndDate(data))\n, _data(data) {\n\tsetCustomStatus(_data.ip);\n}\n\nvoid Row::update(const EntryData &data) {\n\t_data = data;\n\tsetCustomStatus(\n\t\tJoinNonEmpty({ _data.domain, _data.browser, _data.platform }));\n\trefreshName(st::websiteListItem);\n\t_location.setText(st::defaultTextStyle, LocationAndDate(_data));\n\t_delegate->rowUpdateRow(this);\n}\n\nEntryData Row::data() const {\n\treturn _data;\n}\n\nQString Row::generateName() {\n\treturn _data.bot->name();\n}\n\nQString Row::generateShortName() {\n\treturn _data.bot->shortName();\n}\n\nPaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) {\n\tconst auto peer = _data.bot;\n\tauto userpic = _userpic = peer->createUserpicView();\n\treturn [=](Painter &p, int x, int y, int outerWidth, int size) mutable {\n\t\tpeer->paintUserpic(p, _userpic, {\n\t\t\t.position = QPoint(x, y),\n\t\t\t.size = size,\n\t\t\t.shape = Ui::PeerUserpicShape::Forum,\n\t\t});\n\t};\n}\n\nint Row::elementsCount() const {\n\treturn 2;\n}\n\nQRect Row::elementGeometry(int element, int outerWidth) const {\n\tswitch (element) {\n\tcase 1: {\n\t\treturn QRect(\n\t\t\tst::websiteListItem.namePosition.x(),\n\t\t\tst::websiteLocationTop,\n\t\t\touterWidth,\n\t\t\tst::normalFont->height);\n\t} break;\n\tcase 2: {\n\t\tconst auto size = QSize(\n\t\t\tst::sessionTerminate.width,\n\t\t\tst::sessionTerminate.height);\n\t\tconst auto right = st::sessionTerminateSkip;\n\t\tconst auto top = st::sessionTerminateTop;\n\t\tconst auto left = outerWidth - right - size.width();\n\t\treturn QRect(QPoint(left, top), size);\n\t} break;\n\t}\n\treturn QRect();\n}\n\nbool Row::elementDisabled(int element) const {\n\treturn !id() || (element == 1);\n}\n\nbool Row::elementOnlySelect(int element) const {\n\treturn false;\n}\n\nvoid Row::elementAddRipple(\n\t\tint element,\n\t\tQPoint point,\n\t\tFn<void()> updateCallback) {\n}\n\nvoid Row::elementsStopLastRipple() {\n}\n\nvoid Row::elementsPaint(\n\t\tPainter &p,\n\t\tint outerWidth,\n\t\tbool selected,\n\t\tint selectedElement) {\n\tconst auto geometry = elementGeometry(2, outerWidth);\n\tconst auto position = geometry.topLeft()\n\t\t+ st::sessionTerminate.iconPosition;\n\tconst auto &icon = (selectedElement == 2)\n\t\t? st::sessionTerminate.iconOver\n\t\t: st::sessionTerminate.icon;\n\ticon.paint(p, position.x(), position.y(), outerWidth);\n\n\tp.setFont(st::normalFont);\n\tp.setPen(st::sessionInfoFg);\n\tconst auto locationLeft = st::websiteListItem.namePosition.x();\n\tconst auto available = outerWidth - locationLeft;\n\t_location.drawLeftElided(\n\t\tp,\n\t\tlocationLeft,\n\t\tst::websiteLocationTop,\n\t\tavailable,\n\t\touterWidth);\n}\n\nclass Content : public Ui::RpWidget {\npublic:\n\tContent(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionController*> controller);\n\n\tvoid setupContent();\n\t[[nodiscard]] Ui::RpWidget *terminateAllButton() const;\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tclass Inner;\n\tclass ListController;\n\n\tvoid shortPoll();\n\tvoid parse(const Api::Websites::List &list);\n\n\tvoid terminate(\n\t\tFn<void(bool block)> sendRequest,\n\t\trpl::producer<QString> title,\n\t\trpl::producer<QString> text,\n\t\tQString blockText = QString());\n\tvoid terminateOne(uint64 hash);\n\tvoid terminateAll();\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<Api::Websites*> _websites;\n\n\trpl::variable<bool> _loading = false;\n\tApi::Websites::List _data;\n\n\tobject_ptr<Inner> _inner;\n\tbase::weak_qptr<Ui::BoxContent> _terminateBox;\n\n\tbase::Timer _shortPollTimer;\n\n};\n\nclass Content::ListController final\n\t: public PeerListController\n\t, public RowDelegate\n\t, public base::has_weak_ptr {\npublic:\n\texplicit ListController(not_null<::Main::Session*> session);\n\n\t::Main::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowElementClicked(not_null<PeerListRow*> row, int element) override;\n\n\tvoid rowUpdateRow(not_null<Row*> row) override;\n\n\tvoid showData(gsl::span<const EntryData> items);\n\trpl::producer<int> itemsCount() const;\n\trpl::producer<uint64> terminateRequests() const;\n\t[[nodiscard]] rpl::producer<EntryData> showRequests() const;\n\n\t[[nodiscard]] static std::unique_ptr<ListController> Add(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<::Main::Session*> session,\n\t\tstyle::margins margins = {});\n\nprivate:\n\tconst not_null<::Main::Session*> _session;\n\n\trpl::event_stream<uint64> _terminateRequests;\n\trpl::event_stream<int> _itemsCount;\n\trpl::event_stream<EntryData> _showRequests;\n\n};\n\nclass Content::Inner : public Ui::RpWidget {\npublic:\n\tInner(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\tvoid showData(const Api::Websites::List &data);\n\t[[nodiscard]] rpl::producer<EntryData> showRequests() const;\n\t[[nodiscard]] rpl::producer<uint64> terminateOne() const;\n\t[[nodiscard]] rpl::producer<> terminateAll() const;\n\t[[nodiscard]] Ui::RpWidget *terminateAllButton() const;\n\nprivate:\n\tvoid setupContent();\n\n\tconst not_null<Window::SessionController*> _controller;\n\tQPointer<Ui::SettingsButton> _terminateAll;\n\tstd::unique_ptr<ListController> _list;\n\n};\n\nContent::Content(\n\tQWidget*,\n\tnot_null<Window::SessionController*> controller)\n: _controller(controller)\n, _websites(&controller->session().api().websites())\n, _inner(this, controller)\n, _shortPollTimer([=] { shortPoll(); }) {\n}\n\nvoid Content::setupContent() {\n\t_inner->heightValue(\n\t) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](int height) {\n\t\tresize(width(), height);\n\t}, _inner->lifetime());\n\n\t_inner->showRequests(\n\t) | rpl::on_next([=](const EntryData &data) {\n\t\t_controller->show(Box(\n\t\t\tInfoBox,\n\t\t\tdata,\n\t\t\t[=](uint64 hash) { terminateOne(hash); }));\n\t}, lifetime());\n\n\t_inner->terminateOne(\n\t) | rpl::on_next([=](uint64 hash) {\n\t\tterminateOne(hash);\n\t}, lifetime());\n\n\t_inner->terminateAll(\n\t) | rpl::on_next([=] {\n\t\tterminateAll();\n\t}, lifetime());\n\n\t_loading.changes(\n\t) | rpl::on_next([=](bool value) {\n\t\t_inner->setVisible(!value);\n\t}, lifetime());\n\n\t_websites->listValue(\n\t) | rpl::on_next([=](const Api::Websites::List &list) {\n\t\tparse(list);\n\t}, lifetime());\n\n\t_loading = true;\n\tshortPoll();\n}\n\nvoid Content::parse(const Api::Websites::List &list) {\n\t_loading = false;\n\n\t_data = list;\n\n\tranges::sort(_data, std::greater<>(), &EntryData::activeTime);\n\n\t_inner->showData(_data);\n\n\t_shortPollTimer.callOnce(kShortPollTimeout);\n}\n\nvoid Content::resizeEvent(QResizeEvent *e) {\n\tRpWidget::resizeEvent(e);\n\n\t_inner->resize(width(), _inner->height());\n}\n\nvoid Content::paintEvent(QPaintEvent *e) {\n\tRpWidget::paintEvent(e);\n\n\tPainter p(this);\n\n\tif (_loading.current()) {\n\t\tp.setFont(st::noContactsFont);\n\t\tp.setPen(st::noContactsColor);\n\t\tp.drawText(\n\t\t\tQRect(0, 0, width(), st::noContactsHeight),\n\t\t\ttr::lng_contacts_loading(tr::now),\n\t\t\tstyle::al_center);\n\t}\n}\n\nvoid Content::shortPoll() {\n\tconst auto left = kShortPollTimeout\n\t\t- (crl::now() - _websites->lastReceivedTime());\n\tif (left > 0) {\n\t\tparse(_websites->list());\n\t\t_shortPollTimer.cancel();\n\t\t_shortPollTimer.callOnce(left);\n\t} else {\n\t\t_websites->reload();\n\t}\n\tupdate();\n}\n\nvoid Content::terminate(\n\t\tFn<void(bool block)> sendRequest,\n\t\trpl::producer<QString> title,\n\t\trpl::producer<QString> text,\n\t\tQString blockText) {\n\tif (const auto strong = _terminateBox.get()) {\n\t\tstrong->deleteLater();\n\t}\n\tauto box = Box([=](not_null<Ui::GenericBox*> box) {\n\t\tauto &lifetime = box->lifetime();\n\t\tconst auto block = lifetime.make_state<Ui::Checkbox*>(nullptr);\n\t\tconst auto callback = crl::guard(this, [=] {\n\t\t\tconst auto blocked = (*block) && (*block)->checked();\n\t\t\tif (_terminateBox) {\n\t\t\t\t_terminateBox->closeBox();\n\t\t\t\t_terminateBox = nullptr;\n\t\t\t}\n\t\t\tsendRequest(blocked);\n\t\t});\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = rpl::duplicate(text),\n\t\t\t.confirmed = callback,\n\t\t\t.confirmText = tr::lng_settings_disconnect(),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t.title = rpl::duplicate(title),\n\t\t});\n\t\tif (!blockText.isEmpty()) {\n\t\t\t*block = box->addRow(object_ptr<Ui::Checkbox>(box, blockText));\n\t\t}\n\t});\n\t_terminateBox = base::make_weak(box.data());\n\t_controller->show(std::move(box));\n}\n\nvoid Content::terminateOne(uint64 hash) {\n\tconst auto weak = base::make_weak(this);\n\tconst auto i = ranges::find(_data, hash, &EntryData::hash);\n\tif (i == end(_data)) {\n\t\treturn;\n\t}\n\n\tconst auto bot = i->bot;\n\tauto callback = [=](bool block) {\n\t\tauto done = crl::guard(weak, [=](const MTPBool &result) {\n\t\t\t_data.erase(\n\t\t\t\tranges::remove(_data, hash, &EntryData::hash),\n\t\t\t\tend(_data));\n\t\t\t_inner->showData(_data);\n\t\t});\n\t\tauto fail = crl::guard(weak, [=](const MTP::Error &error) {\n\t\t});\n\t\t_websites->requestTerminate(\n\t\t\tstd::move(done),\n\t\t\tstd::move(fail),\n\t\t\thash,\n\t\t\tblock ? bot.get() : nullptr);\n\t};\n\tterminate(\n\t\tstd::move(callback),\n\t\ttr::lng_settings_disconnect_title(),\n\t\ttr::lng_settings_disconnect_sure(lt_domain, rpl::single(i->domain)),\n\t\ttr::lng_settings_disconnect_block(tr::now, lt_name, bot->name()));\n}\n\nvoid Content::terminateAll() {\n\tconst auto weak = base::make_weak(this);\n\tauto callback = [=](bool block) {\n\t\tconst auto reset = crl::guard(weak, [=] {\n\t\t\t_websites->cancelCurrentRequest();\n\t\t\t_websites->reload();\n\t\t});\n\t\t_websites->requestTerminate(\n\t\t\t[=](const MTPBool &result) { reset(); },\n\t\t\t[=](const MTP::Error &result) { reset(); });\n\t\t_loading = true;\n\t};\n\tterminate(\n\t\tstd::move(callback),\n\t\ttr::lng_settings_disconnect_all_title(),\n\t\ttr::lng_settings_disconnect_all_sure());\n}\n\nUi::RpWidget *Content::terminateAllButton() const {\n\treturn _inner ? _inner->terminateAllButton() : nullptr;\n}\n\nContent::Inner::Inner(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: RpWidget(parent)\n, _controller(controller) {\n\tresize(width(), st::noContactsHeight);\n\tsetupContent();\n}\n\nvoid Content::Inner::setupContent() {\n\tusing namespace rpl::mappers;\n\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tconst auto session = &_controller->session();\n\tconst auto terminateWrap = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::VerticalLayout>(content)))->setDuration(0);\n\tconst auto terminateInner = terminateWrap->entity();\n\t_terminateAll = terminateInner->add(\n\t\tCreateButtonWithIcon(\n\t\t\tterminateInner,\n\t\t\ttr::lng_settings_disconnect_all(),\n\t\t\tst::infoBlockButton,\n\t\t\t{ .icon = &st::infoIconBlock }));\n\tUi::AddSkip(terminateInner);\n\tUi::AddDividerText(\n\t\tterminateInner,\n\t\ttr::lng_settings_logged_in_description());\n\n\tconst auto listWrap = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::VerticalLayout>(content)))->setDuration(0);\n\tconst auto listInner = listWrap->entity();\n\tUi::AddSkip(listInner, st::sessionSubtitleSkip);\n\tUi::AddSubsectionTitle(listInner, tr::lng_settings_logged_in_title());\n\t_list = ListController::Add(listInner, session);\n\tUi::AddSkip(listInner);\n\n\tconst auto skip = st::noContactsHeight / 2;\n\tconst auto placeholder = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::FlatLabel>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_settings_logged_in_description(),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::defaultBoxDividerLabelPadding + QMargins(0, skip, 0, skip))\n\t)->setDuration(0);\n\n\tterminateWrap->toggleOn(_list->itemsCount() | rpl::map(_1 > 0));\n\tlistWrap->toggleOn(_list->itemsCount() | rpl::map(_1 > 0));\n\tplaceholder->toggleOn(_list->itemsCount() | rpl::map(_1 == 0));\n\n\tUi::ResizeFitChild(this, content);\n}\n\nvoid Content::Inner::showData(const Api::Websites::List &data) {\n\t_list->showData(data);\n}\n\nrpl::producer<> Content::Inner::terminateAll() const {\n\treturn _terminateAll->clicks() | rpl::to_empty;\n}\n\nrpl::producer<uint64> Content::Inner::terminateOne() const {\n\treturn _list->terminateRequests();\n}\n\nrpl::producer<EntryData> Content::Inner::showRequests() const {\n\treturn _list->showRequests();\n}\n\nUi::RpWidget *Content::Inner::terminateAllButton() const {\n\treturn _terminateAll.data();\n}\n\nContent::ListController::ListController(\n\tnot_null<::Main::Session*> session)\n: _session(session) {\n}\n\n::Main::Session &Content::ListController::session() const {\n\treturn *_session;\n}\n\nvoid Content::ListController::prepare() {\n}\n\nvoid Content::ListController::rowClicked(\n\t\tnot_null<PeerListRow*> row) {\n\t_showRequests.fire_copy(static_cast<Row*>(row.get())->data());\n}\n\nvoid Content::ListController::rowElementClicked(\n\t\tnot_null<PeerListRow*> row,\n\t\tint element) {\n\tif (element == 2) {\n\t\tif (const auto hash = static_cast<Row*>(row.get())->data().hash) {\n\t\t\t_terminateRequests.fire_copy(hash);\n\t\t}\n\t}\n}\n\nvoid Content::ListController::rowUpdateRow(not_null<Row*> row) {\n\tdelegate()->peerListUpdateRow(row);\n}\n\nvoid Content::ListController::showData(\n\t\tgsl::span<const EntryData> items) {\n\tauto index = 0;\n\tauto positions = base::flat_map<uint64, int>();\n\tpositions.reserve(items.size());\n\tfor (const auto &entry : items) {\n\t\tconst auto id = entry.hash;\n\t\tpositions.emplace(id, index++);\n\t\tif (const auto row = delegate()->peerListFindRow(id)) {\n\t\t\tstatic_cast<Row*>(row)->update(entry);\n\t\t} else {\n\t\t\tdelegate()->peerListAppendRow(\n\t\t\t\tstd::make_unique<Row>(this, entry));\n\t\t}\n\t}\n\tfor (auto i = 0; i != delegate()->peerListFullRowsCount();) {\n\t\tconst auto row = delegate()->peerListRowAt(i);\n\t\tif (positions.contains(row->id())) {\n\t\t\t++i;\n\t\t\tcontinue;\n\t\t}\n\t\tdelegate()->peerListRemoveRow(row);\n\t}\n\tdelegate()->peerListSortRows([&](\n\t\t\tconst PeerListRow &a,\n\t\t\tconst PeerListRow &b) {\n\t\treturn positions[a.id()] < positions[b.id()];\n\t});\n\tdelegate()->peerListRefreshRows();\n\t_itemsCount.fire(delegate()->peerListFullRowsCount());\n}\n\nrpl::producer<int> Content::ListController::itemsCount() const {\n\treturn _itemsCount.events_starting_with(\n\t\tdelegate()->peerListFullRowsCount());\n}\n\nrpl::producer<uint64> Content::ListController::terminateRequests() const {\n\treturn _terminateRequests.events();\n}\n\nrpl::producer<EntryData> Content::ListController::showRequests() const {\n\treturn _showRequests.events();\n}\n\nauto Content::ListController::Add(\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<::Main::Session*> session,\n\tstyle::margins margins)\n-> std::unique_ptr<ListController> {\n\tauto &lifetime = container->lifetime();\n\tconst auto delegate = lifetime.make_state<\n\t\tPeerListContentDelegateSimple\n\t>();\n\tauto controller = std::make_unique<ListController>(session);\n\tcontroller->setStyleOverrides(&st::websiteList);\n\tconst auto content = container->add(\n\t\tobject_ptr<PeerListContent>(\n\t\t\tcontainer,\n\t\t\tcontroller.get()),\n\t\tmargins);\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\treturn controller;\n}\n\nvoid BuildWebsitesSection(SectionBuilder &builder) {\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"websites/disconnect-all\"_q,\n\t\t\t.title = tr::lng_settings_disconnect_all(tr::now),\n\t\t\t.keywords = { u\"disconnect\"_q, u\"terminate\"_q, u\"logout\"_q },\n\t\t};\n\t});\n\tbuilder.add(nullptr, [] {\n\t\treturn SearchEntry{\n\t\t\t.id = u\"websites/list\"_q,\n\t\t\t.title = tr::lng_settings_logged_in_title(tr::now),\n\t\t\t.keywords = { u\"websites\"_q, u\"bots\"_q, u\"logged\"_q },\n\t\t};\n\t});\n}\n\nclass Websites : public Section<Websites> {\npublic:\n\tWebsites(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\tvoid showFinished() override;\n\nprivate:\n\tvoid setupContent();\n\n\trpl::event_stream<> _showFinished;\n\n};\n\nWebsites::Websites(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent();\n}\n\nrpl::producer<QString> Websites::title() {\n\treturn tr::lng_settings_connected_title();\n}\n\nvoid Websites::showFinished() {\n\t_showFinished.fire({});\n\tSection::showFinished();\n}\n\nvoid Websites::setupContent() {\n\tconst auto container = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tconst SectionBuildMethod buildMethod = [](\n\t\t\tnot_null<Ui::VerticalLayout*> container,\n\t\t\tnot_null<Window::SessionController*> controller,\n\t\t\tFn<void(Type)> showOther,\n\t\t\trpl::producer<> showFinished) {\n\t\tauto &lifetime = container->lifetime();\n\t\tconst auto highlights = lifetime.make_state<HighlightRegistry>();\n\n\t\tauto builder = SectionBuilder(WidgetContext{\n\t\t\t.container = container,\n\t\t\t.controller = controller,\n\t\t\t.showOther = std::move(showOther),\n\t\t\t.isPaused = Window::PausedIn(\n\t\t\t\tcontroller,\n\t\t\t\tWindow::GifPauseReason::Layer),\n\t\t\t.highlights = highlights,\n\t\t});\n\n\t\tbuilder.addSkip();\n\n\t\tbuilder.add([=](const WidgetContext &ctx) {\n\t\t\tconst auto content = ctx.container->add(\n\t\t\t\tobject_ptr<Content>(ctx.container, ctx.controller));\n\t\t\tcontent->setupContent();\n\n\t\t\tif (ctx.highlights) {\n\t\t\t\tctx.highlights->push_back({\n\t\t\t\t\tu\"websites/disconnect-all\"_q,\n\t\t\t\t\t{ content->terminateAllButton() },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn SectionBuilder::WidgetToAdd{};\n\t\t});\n\n\t\tstd::move(showFinished) | rpl::on_next([=] {\n\t\t\tfor (const auto &[id, entry] : *highlights) {\n\t\t\t\tif (entry.widget) {\n\t\t\t\t\tcontroller->checkHighlightControl(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tentry.widget,\n\t\t\t\t\t\tbase::duplicate(entry.args));\n\t\t\t\t}\n\t\t\t}\n\t\t}, lifetime);\n\t};\n\n\tbuild(container, buildMethod);\n\n\tUi::ResizeFitChild(this, container);\n}\n\nconst auto kMeta = BuildHelper({\n\t.id = Websites::Id(),\n\t.parentId = PrivacySecurityId(),\n\t.title = &tr::lng_settings_connected_title,\n\t.icon = &st::menuIconIpAddress,\n}, [](SectionBuilder &builder) {\n\tBuildWebsitesSection(builder);\n});\n\n} // namespace\n\nType WebsitesId() {\n\treturn Websites::Id();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/sections/settings_websites.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_type.h\"\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Settings {\n\n[[nodiscard]] Type WebsitesId();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\nusing \"ui/widgets/widgets.style\";\nusing \"info/info.style\";\nusing \"boxes/boxes.style\";\n\nsettingsButton: SettingsButton(infoProfileButton) {\n\tstyle: boxTextStyle;\n\tpadding: margins(60px, 10px, 22px, 10px);\n\ticonLeft: 20px;\n}\nsettingsButtonRightLabelSpoiler: SettingsButton(settingsButton) {\n\trightLabel: FlatLabel(defaultSettingsRightLabel) {\n\t\tpalette: TextPalette(defaultTextPalette) {\n\t\t\tspoilerFg: windowActiveTextFg;\n\t\t}\n\t}\n}\nsettingsButtonNoIcon: SettingsButton(settingsButton) {\n\tpadding: margins(22px, 10px, 22px, 8px);\n}\nsettingsButtonLight: SettingsButton(settingsButton) {\n\ttextFg: lightButtonFg;\n\ttextFgOver: lightButtonFgOver;\n}\nsettingsButtonLightNoIcon: SettingsButton(settingsButtonLight, settingsButtonNoIcon) {\n}\nsettingsButtonNoIconLocked: SettingsButton(settingsButtonNoIcon) {\n\ttoggle: Toggle(infoProfileToggle) {\n\t\tlockIcon: icon {{ \"info/info_rights_lock\", menuIconFg }};\n\t}\n\ttoggleOver: Toggle(infoProfileToggleOver) {\n\t\tlockIcon: icon {{ \"info/info_rights_lock\", menuIconFgOver }};\n\t}\n}\nsettingsButtonActive: SettingsButton(infoMainButton, settingsButton) {\n}\nsettingsAttentionButton: SettingsButton(settingsButtonNoIcon) {\n\ttextFg: attentionButtonFg;\n\ttextFgOver: attentionButtonFgOver;\n}\nsettingsAttentionButtonWithIcon: SettingsButton(settingsButton) {\n\ttextFg: attentionButtonFg;\n\ttextFgOver: attentionButtonFgOver;\n}\nsettingsOptionDisabled: SettingsButton(settingsButtonNoIcon) {\n\ttextFg: windowSubTextFg;\n\ttextFgOver: windowSubTextFg;\n\ttextBg: windowBg;\n\ttextBgOver: windowBg;\n\ttoggleOver: infoProfileToggle;\n}\nsettingsButtonRightSkip: 23px;\nsettingsScalePadding: margins(60px, 7px, 22px, 4px);\nsettingsBigScalePadding: margins(21px, 7px, 21px, 4px);\nsettingsSlider: SettingsSlider(defaultSettingsSlider) {\n\tbarFg: windowBgOver;\n\tlabelFg: windowSubTextFg;\n\tlabelFgActive: windowActiveTextFg;\n}\nsettingsScale: MediaSlider(defaultContinuousSlider) {\n\tseekSize: size(15px, 15px);\n}\nsettingsScaleLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowActiveTextFg;\n}\nsettingsUpdateToggle: SettingsButton(settingsButtonNoIcon) {\n\theight: 40px;\n\tpadding: margins(22px, 8px, 22px, 8px);\n}\nsettingsUpdateState: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n}\nsettingsUpdate: SettingsButton(infoMainButton, settingsButtonNoIcon) {\n}\nsettingsUpdateStatePosition: point(22px, 29px);\n\nsettingsIconFork: icon {{ \"settings/settings_fork-22x22\", settingsIconFg }};\nsettingsIconChat: icon {{ \"settings/chat\", settingsIconFg }};\nsettingsIconInterfaceScale: icon {{ \"settings/interface_scale\", settingsIconFg }};\nsettingsIconStickers: icon {{ \"settings/stickers\", settingsIconFg }};\nsettingsIconEmoji: icon {{ \"settings/emoji\", settingsIconFg }};\n\nsettingsPremiumIconWallpapers: icon {{ \"settings/photo\", settingsIconFg }};\nsettingsPremiumIconStories: icon {{ \"settings/stories\", settingsIconFg }};\nsettingsPremiumIconChannelsOff: icon {{ \"settings/premium/channels_off\", settingsIconFg }};\nsettingsPremiumIconDouble: icon {{ \"settings/premium/double\", settingsIconFg }};\nsettingsPremiumIconStatus: icon {{ \"settings/premium/status\", settingsIconFg }};\nsettingsPremiumIconLike: icon {{ \"settings/premium/like\", settingsIconFg }};\nsettingsPremiumIconPlay: icon {{ \"settings/premium/play\", settingsIconFg }};\nsettingsPremiumIconSpeed: icon {{ \"settings/premium/speed\", settingsIconFg }};\nsettingsPremiumIconStar: icon {{ \"settings/premium/star\", settingsIconFg }};\nsettingsPremiumIconVoice: icon {{ \"settings/premium/voice\", settingsIconFg }};\nsettingsPremiumIconFiles: icon {{ \"settings/premium/files\", settingsIconFg }};\nsettingsPremiumIconTranslations: icon {{ \"settings/premium/translations\", settingsIconFg }};\nsettingsPremiumIconTags: icon {{ \"settings/premium/tags\", settingsIconFg }};\nsettingsPremiumIconLastSeen: icon {{ \"settings/premium/lastseen\", settingsIconFg }};\nsettingsPremiumIconPrivacy: icon {{ \"settings/premium/privacy\", settingsIconFg }};\nsettingsPremiumIconBusiness: icon {{ \"settings/premium/market\", settingsIconFg }};\nsettingsPremiumIconEffects: icon {{ \"settings/premium/effects\", settingsIconFg }};\nsettingsPremiumIconChecklist: icon {{ \"settings/premium/checklist\", settingsIconFg }};\nsettingsPremiumIconPeerColors: icon {{ \"settings/premium/peer_colors-24x24\", settingsIconFg }};\nsettingsPremiumIconNoForwards: icon {{ \"menu/share_off-24x24\", settingsIconFg }};\nsettingsPremiumIconAiCompose: icon{\n\t{ size(24px, 24px), transparent },\n\t{ \"chat/ai_letters-20x20\", settingsIconFg, point(2px, 2px) },\n\t{ \"chat/ai_star1-20x20\", settingsIconFg, point(2px, 2px) },\n\t{ \"chat/ai_star2-20x20\", settingsIconFg, point(2px, 2px) },\n};\n\nsettingsStoriesIconOrder: icon {{ \"settings/premium/stories_order\", premiumButtonBg1 }};\nsettingsStoriesIconStealth: icon {{ \"menu/stealth\", premiumButtonBg1 }};\nsettingsStoriesIconViews: icon {{ \"menu/show_in_chat\", premiumButtonBg1 }};\nsettingsStoriesIconExpiration: icon {{ \"settings/premium/timer\", premiumButtonBg1 }};\nsettingsStoriesIconDownload: icon {{ \"menu/download\", premiumButtonBg1 }};\nsettingsStoriesIconCaption: icon {{ \"settings/premium/stories_caption\", premiumButtonBg1 }};\nsettingsStoriesIconLinks: icon {{ \"menu/links_profile\", premiumButtonBg1 }};\n\nsettingsBusinessIconLocation: icon {{ \"settings/premium/business/business_location\", settingsIconFg }};\nsettingsBusinessIconHours: icon {{ \"settings/premium/business/business_hours\", settingsIconFg }};\nsettingsBusinessIconReplies: icon {{ \"settings/premium/business/business_quick\", settingsIconFg }};\nsettingsBusinessIconGreeting: icon {{ \"settings/premium/status\", settingsIconFg }};\nsettingsBusinessIconAway: icon {{ \"settings/premium/business/business_away\", settingsIconFg }};\nsettingsBusinessIconChatbots: icon {{ \"settings/premium/business/business_chatbots\", settingsIconFg }};\nsettingsBusinessIconChatIntro: icon {{ \"settings/premium/business/business_intro\", settingsIconFg }};\nsettingsBusinessIconChatLinks: icon {{ \"settings/premium/business/business_links\", settingsIconFg }};\n\nsettingsBusinessPromoLocation: icon {{ \"settings/premium/promo/business_location\", premiumButtonBg1 }};\nsettingsBusinessPromoHours: icon {{ \"settings/premium/promo/business_hours\", premiumButtonBg1 }};\nsettingsBusinessPromoReplies: icon {{ \"settings/premium/promo/business_quickreply\", premiumButtonBg1 }};\nsettingsBusinessPromoGreeting: icon {{ \"settings/premium/promo/business_greeting\", premiumButtonBg1 }};\nsettingsBusinessPromoAway: icon {{ \"settings/premium/promo/business_away\", premiumButtonBg1 }};\nsettingsBusinessPromoChatbots: icon {{ \"settings/premium/promo/business_chatbot\", premiumButtonBg1 }};\nsettingsBusinessPromoChatIntro: icon {{ \"settings/premium/promo/business_intro\", premiumButtonBg1 }};\nsettingsBusinessPromoChatLinks: icon {{ \"settings/premium/promo/business_chatlink\", premiumButtonBg1 }};\n\nsettingsStarRefEarnStars: icon {{ \"settings/premium/business/earn_stars\", settingsIconFg }};\n\nsettingsPremiumNewBadge: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(10px semibold);\n\t}\n\ttextFg: windowFgActive;\n}\nsettingsPremiumNewBadgePosition: point(4px, 1px);\nsettingsPremiumNewBadgePadding: margins(4px, 1px, 4px, 1px);\n\nsettingsTTLChatsOff: icon {{ \"settings/ttl/autodelete_off\", windowSubTextFg }};\nsettingsTTLChatsOn: icon {{ \"settings/ttl/autodelete_on\", windowActiveTextFg }};\n\nsettingsIconAdd: icon {{ \"settings/add\", windowFgActive }};\nsettingsIconRemove: icon {{ \"settings/remove\", windowFgActive }};\nsettingsFolderShareIcon: icon {{ \"settings/folder_links\", lightButtonFg }};\n\nsettingsCheckbox: Checkbox(defaultBoxCheckbox) {\n\ttextPosition: point(15px, 1px);\n}\nsettingsCheckboxPadding: margins(22px, 10px, 10px, 10px);\nsettingsLink: boxLinkButton;\nsettingsCheckboxesSkip: 12px;\n\nsettingsSendType: settingsCheckbox;\nsettingsSendTypePadding: margins(22px, 5px, 10px, 5px);\nsettingsSendTypeSkip: 5px;\n\nsettingsBackgroundThumb: 76px;\nsettingsThumbSkip: 16px;\nsettingsBackgroundPadding: margins(22px, 11px, 10px, 12px);\nsettingsTileSkip: 15px;\nsettingsFromGalleryTop: 2px;\nsettingsFromFileTop: 14px;\nsettingsPrivacyOption: Checkbox(settingsCheckbox) {\n\ttextPosition: point(13px, 1px);\n}\nsettingsPrivacySecurityPadding: 12px;\nsettingsPrivacySkip: 14px;\nsettingsPrivacySkipTop: 4px;\nsettingsPrivacyPremium: icon{{ \"profile_premium\", premiumButtonFg }};\n\nsettingsPrivacyAddBirthday: FlatLabel(defaultFlatLabel) {\n\tminWidth: 256px;\n}\n\nsettingsCloudPasswordIconSize: 100px;\n\nsettingLocalPasscodeInputField: InputField(defaultInputField) {\n\twidth: 256px;\n}\nsettingLocalPasscodeDescription: FlatLabel(changePhoneDescription) {\n\tminWidth: 256px;\n}\nsettingLocalPasscodeDescriptionHeight: 53px;\nsettingLocalPasscodeError: FlatLabel(changePhoneError) {\n\tminWidth: 256px;\n}\nsettingLocalPasscodeDescriptionBottomSkip: 15px;\nsettingLocalPasscodeIconPadding: margins(0px, 19px, 0px, 5px);\nsettingLocalPasscodeButtonPadding: margins(0px, 19px, 0px, 35px);\n\nsettingsInfoPhotoHeight: 162px;\nsettingsInfoPhotoSize: 100px;\nsettingsInfoPhoto: UserpicButton(defaultUserpicButton) {\n\tsize: size(settingsInfoPhotoSize, settingsInfoPhotoSize);\n\tphotoSize: settingsInfoPhotoSize;\n}\nsettingsInfoPhotoTop: 2px;\nsettingsInfoPhotoSkip: 7px;\nsettingsInfoNameSkip: -1px;\nsettingsInfoUploadLeft: 6px;\nsettingsInfoPeerBadge: InfoPeerBadge {\n\tverified: icon {\n\t\t{ \"dialogs/dialogs_verified_star\", dialogsVerifiedIconBg },\n\t\t{ \"dialogs/dialogs_verified_check\", dialogsVerifiedIconFg },\n\t};\n\tpremium: icon {{ \"dialogs/dialogs_premium\", dialogsVerifiedIconBg }};\n\tpremiumFg: dialogsVerifiedIconBg;\n\tsizeTag: 0; // Normal\n}\nsettingsCoverBadge: InfoPeerBadge(infoPeerBadge) {\n\tposition: point(4px, 2px);\n}\n\nsettingsBio: InputField(defaultInputField) {\n\ttextBg: transparent;\n\ttextMargins: margins(0px, 7px, 0px, 13px);\n\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(2px, 0px, 2px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n\n\tborder: 0px;\n\tborderActive: 0px;\n\n\theightMin: 32px;\n}\n\nsettingsBioMargins: margins(22px, 6px, 22px, 4px);\nsettingsBioHighlightMargin: margins(-22px, 0px, -22px, 0px);\nsettingsBioCountdown: FlatLabel(defaultFlatLabel) {\n\tstyle: boxTextStyle;\n\ttextFg: windowSubTextFg;\n}\n\nsettingsCoverName: FlatLabel(defaultFlatLabel) {\n\tmaxHeight: 24px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(17px semibold);\n\t}\n}\nsettingsCoverStatus: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n}\n\nsettingsThemesTopSkip: 10px;\nsettingsThemesBottomSkip: 8px;\nsettingsTheme: Checkbox(defaultCheckbox) {\n\ttextFg: windowSubTextFg;\n\ttextFgActive: windowActiveTextFg;\n\n\twidth: 80px;\n\tmargin: margins(0px, 0px, 0px, 0px);\n\n\ttextPosition: point(0px, 99px);\n\tcheckPosition: point(0px, 0px);\n\n\tstyle: defaultTextStyle;\n\n\tdisabledOpacity: 0.5;\n}\n\nsettingsThemePreviewSize: size(80px, 92px);\nsettingsThemeBubbleSize: size(40px, 14px);\nsettingsThemeBubbleRadius: 2px;\nsettingsThemeBubblePosition: point(6px, 8px);\nsettingsThemeBubbleSkip: 6px;\nsettingsThemeRadioBottom: 12px;\nsettingsThemeMinSkip: 4px;\n\nsettingsThemeNotSupportedBg: windowBgOver;\nsettingsThemeNotSupportedIcon: icon {{ \"theme_preview\", menuIconFg }};\n\nchatThemeTitlePadding: margins(22px, 13px, 22px, 9px);\nchatThemePreviewSize: size(80px, 108px);\nchatThemeBubbleSize: size(48px, 22px);\nchatThemeBubbleRadius: 10px;\nchatThemeBubblePosition: point(6px, 12px);\nchatThemeBubbleSkip: 6px;\nchatThemeEntrySkip: 10px;\nchatThemeEntryMargin: margins(16px, 10px, 16px, 8px);\nchatThemeEmptyPreviewTop: 16px;\nchatThemeEmojiBottom: 12px;\nchatThemeButtonMargin: margins(10px, 0px, 10px, 8px);\nchatThemeGiftTaken: icon {{ \"mediaview/mini_repost\", msgFileInBg }};\n\nautoDownloadLimitButton: SettingsButton(settingsButtonNoIcon) {\n\tpadding: margins(22px, 10px, 22px, 0px);\n}\nsettingsLevelMeterPadding: margins(22px, 10px, 20px, 10px);\n\nsettingsForwardPrivacyPadding: 10px;\nsettingsForwardPrivacyArrowSkip: 32px;\nsettingsForwardPrivacyArrowSize: 7px;\nsettingsForwardPrivacyTooltipPadding: margins(12px, 7px, 12px, 7px);\n\nsettingsAccentColorSize: 24px;\nsettingsAccentColorSkip: 4px;\nsettingsAccentColorLine: 3px;\n\nsettingsFilterDividerLabel: FlatLabel(boxDividerLabel) {\n\tminWidth: 200px;\n\tmaxHeight: 0px;\n\talign: align(top);\n}\nsettingsFilterDividerLabelPadding: margins(0px, 16px, 0px, 22px);\nsettingsFilterIconSize: 74px;\nsettingsFilterIconPadding: margins(0px, 17px, 0px, 5px);\n\nsettingsFilterTagPreviewSkip: 14px;\n\nsettingsFilterAddRecommended: RoundButton(defaultActiveButton) {\n\twidth: -32px;\n\theight: 26px;\n\ttextTop: 4px;\n}\n\nsettingsDeviceName: InputField(defaultInputField) {\n\ttextBg: transparent;\n\ttextMargins: margins(1px, 3px, 1px, 4px);\n\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(1px, 0px, 1px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n\n\theightMin: 29px;\n}\n\ndictionariesSectionButton: SettingsButton(settingsUpdateToggle) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px semibold);\n\t}\n}\n\nsessionsScroll: boxScroll;\nsessionsHeight: 350px;\nsessionLocationTop: 54px;\nsessionCurrentSkip: 8px;\nsessionSubtitleSkip: 14px;\nsessionInfoFg: windowSubTextFg;\nsessionTerminateTop: 8px;\nsessionTerminateSkip: 11px;\nsessionTerminate: IconButton {\n\twidth: 34px;\n\theight: 34px;\n\n\ticon: smallCloseIcon;\n\ticonOver: smallCloseIconOver;\n\ticonPosition: point(12px, 12px);\n}\nsessionIconWindows: icon{{ \"settings/devices/device_desktop_win\", historyPeerUserpicFg }};\nsessionIconMac: icon{{ \"settings/devices/device_desktop_mac\", historyPeerUserpicFg }};\nsessionIconUbuntu: icon{{ \"settings/devices/device_linux_ubuntu\", historyPeerUserpicFg }};\nsessionIconLinux: icon{{ \"settings/devices/device_linux\", historyPeerUserpicFg }};\nsessionIconiPhone: icon{{ \"settings/devices/device_phone_ios\", historyPeerUserpicFg }};\nsessionIconiPad: icon{{ \"settings/devices/device_tablet_ios\", historyPeerUserpicFg }};\nsessionIconAndroid: icon{{ \"settings/devices/device_phone_android\", historyPeerUserpicFg }};\nsessionIconWeb: icon{{ \"settings/devices/device_web_other\", historyPeerUserpicFg }};\nsessionIconChrome: icon{{ \"settings/devices/device_web_chrome\", historyPeerUserpicFg }};\nsessionIconEdge: icon{{ \"settings/devices/device_web_edge\", historyPeerUserpicFg }};\nsessionIconFirefox: icon{{ \"settings/devices/device_web_firefox\", historyPeerUserpicFg }};\nsessionIconSafari: icon{{ \"settings/devices/device_web_safari\", historyPeerUserpicFg }};\nsessionIconOther: icon{{ \"settings/devices/device_other\", historyPeerUserpicFg }};\nsessionBigUserpicSize: 70px;\nsessionBigLottieSize: 52px;\nsessionBigIconOther: icon{{ \"settings/devices/device_other_large\", historyPeerUserpicFg }};\nsessionBigIconWeb: icon{{ \"settings/devices/device_web_other_large\", historyPeerUserpicFg }};\nsessionBigCoverPadding: margins(0px, 18px, 0px, 7px);\nsessionBigName: FlatLabel(defaultFlatLabel) {\n\ttextFg: boxTitleFg;\n\tmaxHeight: 29px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(20px semibold);\n\t}\n\talign: align(top);\n}\nsessionDateLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n\talign: align(top);\n}\nsessionDateSkip: 19px;\nsessionValuePadding: margins(37px, 5px, 0px, 0px);\nsessionValueLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n}\nsessionValueSkip: 8px;\nsessionValueIconPosition: point(20px, 9px);\n\nsessionListItem: PeerListItem(defaultPeerListItem) {\n\tbutton: OutlineButton(defaultPeerListButton) {\n\t\tfont: normalFont;\n\t\tpadding: margins(11px, 5px, 11px, 5px);\n\t}\n\theight: 84px;\n\tphotoPosition: point(21px, 10px);\n\tnameStyle: TextStyle(defaultTextStyle) {\n\t\tfont: msgNameFont;\n\t}\n\tnamePosition: point(78px, 11px);\n\tstatusPosition: point(78px, 32px);\n\tphotoSize: 42px;\n\tstatusFg: boxTextFg;\n\tstatusFgOver: boxTextFg;\n}\nsessionList: PeerList(defaultPeerList) {\n\titem: sessionListItem;\n\tpadding: margins(0px, 4px, 0px, 0px);\n}\nwebsiteListItem: PeerListItem(sessionListItem) {\n\theight: 72px;\n\tphotoPosition: point(18px, 10px);\n\tnamePosition: point(64px, 6px);\n\tstatusPosition: point(64px, 26px);\n\tphotoSize: 32px;\n}\nwebsiteList: PeerList(sessionList) {\n\titem: websiteListItem;\n}\nwebsiteLocationTop: 46px;\nwebsiteBigUserpic: UserpicButton(defaultUserpicButton) {\n\tsize: size(70px, 70px);\n\tphotoSize: 70px;\n}\n\nsettingsPhotoLeft: 22px;\nsettingsPhotoTop: 8px;\nsettingsPhotoBottom: 16px;\nsettingsPhotoHighlightMargin: margins(-4px, -4px, -4px, -4px);\nsettingsNameLeft: 112px;\nsettingsNameTop: 12px;\nsettingsPhoneLeft: settingsNameLeft;\nsettingsPhoneTop: 37px;\nsettingsUsernameLeft: settingsNameLeft;\nsettingsUsernameTop: 58px;\nsettingsPeerToPeerSkip: 9px;\n\nsettingsIconRadius: 6px;\n\nsettingsReactionSize: 25px;\nsettingsReactionRightSkip: 0px;\nsettingsReactionCornerSize: size(28px, 22px);\nsettingsReactionCornerSkip: point(11px, -6px);\nsettingsReactionMessageSize: 18px;\n\nsettingsReactionRightIcon: 20px;\n\nnotifyPreviewMargins: margins(40px, 20px, 40px, 58px);\nnotifyPreviewUserpicSize: 36px;\nnotifyPreviewUserpicPosition: point(14px, 11px);\nnotifyPreviewTitlePosition: point(64px, 9px);\nnotifyPreviewTextPosition: point(64px, 30px);\nnotifyPreviewChecksSkip: 12px;\nnotifyPreviewBottomSkip: 9px;\n\nsettingsPremiumButtonPadding: margins(11px, 11px, 11px, 3px);\nsettingsPremiumTopBarBackIcon: icon {{ \"info/info_back\", premiumButtonFg }};\nsettingsPremiumTopBarBackIconOver: icon {{ \"info/info_back\", premiumButtonFg }};\nsettingsPremiumTopBarBack: IconButton(infoTopBarBack) {\n\ticon: settingsPremiumTopBarBackIcon;\n\ticonOver: settingsPremiumTopBarBackIconOver;\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: callMuteRipple;\n\t}\n}\nsettingsPremiumLayerTopBarBack: IconButton(infoLayerTopBarBack) {\n\ticon: settingsPremiumTopBarBackIcon;\n\ticonOver: settingsPremiumTopBarBackIconOver;\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: callMuteRipple;\n\t}\n}\nsettingsPremiumTopBarClose: IconButton(infoTopBarClose) {\n\ticon: icon {{ \"info/info_close\", premiumButtonFg }};\n\ticonOver: icon {{ \"info/info_close\", premiumButtonFg }};\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: callMuteRipple;\n\t}\n}\nsettingsPremiumMoveLeft: IconButton(settingsPremiumLayerTopBarBack) {\n\twidth: 52px;\n\theight: 56px;\n\ticonPosition: point(-1px, -1px);\n}\nsettingsPremiumMoveRightIcon: icon {{ \"info/info_back-flip_horizontal\", premiumButtonFg }};\nsettingsPremiumMoveRightIconOver: icon {{ \"info/info_back-flip_horizontal\", premiumButtonFg }};\nsettingsPremiumMoveRight: IconButton(settingsPremiumMoveLeft) {\n\ticon: settingsPremiumMoveRightIcon;\n\ticonOver: settingsPremiumMoveRightIconOver;\n}\nsettingsPremiumRowTitlePadding: margins(59px, 5px, 46px, 1px);\nsettingsPremiumRowAboutPadding: margins(59px, 0px, 46px, 6px);\nsettingsPremiumPreviewTitlePadding: margins(24px, 13px, 24px, 3px);\nsettingsPremiumPreviewAboutPadding: margins(24px, 0px, 24px, 11px);\nsettingsPremiumPreviewLinePadding: margins(18px, 0px, 18px, 8px);\nsettingsPremiumPreviewIconTitlePadding: margins(62px, 13px, 24px, 1px);\nsettingsPremiumPreviewIconAboutPadding: margins(62px, 0px, 24px, 0px);\nsettingsPremiumPreviewIconPosition: point(20px, 7px);\n\nsettingsPremiumArrowShift: point(-5px, -1px);\nsettingsPremiumArrow: icon{{ \"settings/premium/arrow\", menuIconFg }};\nsettingsPremiumArrowOver: icon{{ \"settings/premium/arrow\", menuIconFgOver }};\n\nsettingsPremiumOptionsPadding: margins(0px, 9px, 0px, 2px);\nsettingsPremiumTopHeight: 202px;\nsettingsPremiumUserHeight: 205px;\nsettingsPremiumUserTitlePadding: margins(0px, 16px, 0px, 6px);\nsettingsPremiumUserTitle: FlatLabel(boxTitle) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: boxTitleFont;\n\t\tlineHeight: 20px;\n\t}\n\tminWidth: 300px;\n\tmaxHeight: 0px;\n\talign: align(top);\n}\n\nsettingsBlockedListSubtitleAddPadding: margins(0px, 1px, 0px, -4px);\nsettingsBlockedListIconPadding: margins(0px, 24px, 0px, 5px);\nsettingsBlockedList: PeerList(peerListBox) {\n\tpadding: margins(0px, 0px, 0px, membersMarginBottom);\n}\nsettingsBlockedHeightMin: 240px;\n\nsettingsNotificationType: SettingsButton(settingsButton) {\n\theight: 40px;\n\tpadding: margins(60px, 4px, 22px, 4px);\n}\nsettingsNotificationTypeDetails: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n}\n\nsettingsSearchResult: SettingsButton(settingsButton) {\n\theight: 40px;\n\tpadding: margins(60px, 4px, 22px, 4px);\n}\nsettingsSearchResultNoIcon: SettingsButton(settingsSearchResult) {\n\tpadding: margins(22px, 4px, 22px, 4px);\n}\nsettingsSearchResultDetails: FlatLabel(settingsNotificationTypeDetails) {\n}\nsettingsSearchNoResults: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n\talign: align(center);\n}\nsettingsSearchNoResultsPadding: margins(22px, 32px, 22px, 32px);\n\npowerSavingButton: SettingsButton(settingsButton) {\n\tstyle: boxTextStyle;\n\tpadding: margins(57px, 8px, 22px, 8px);\n\ticonLeft: 20px;\n}\npowerSavingButtonNoIcon: SettingsButton(powerSavingButton) {\n\tpadding: margins(22px, 8px, 22px, 8px);\n}\npowerSavingSubtitlePadding: margins(0px, 4px, 0px, -2px);\n\nfilterInviteBox: Box(defaultBox) {\n\tbuttonPadding: margins(10px, 9px, 10px, 9px);\n\tbuttonHeight: 42px;\n\tbutton: RoundButton(defaultActiveButton) {\n\t\theight: 42px;\n\t\ttextTop: 12px;\n\t\tstyle: semiboldTextStyle;\n\t}\n}\nfilterInviteButtonStyle: TextStyle(defaultTextStyle) {\n\tfont: font(13px semibold);\n}\nfilterInviteButtonBadgeStyle: TextStyle(defaultTextStyle) {\n\tfont: font(12px semibold);\n}\nfilterInviteButtonBadgePadding: margins(5px, 0px, 5px, 2px);\nfilterInviteButtonBadgeSkip: 5px;\nfilterLinkDividerLabelPadding: margins(0px, 10px, 0px, 17px);\nfilterLinkTitlePadding: margins(0px, 15px, 0px, 17px);\nfilterLinkAboutTextStyle: TextStyle(defaultTextStyle) {\n\tfont: font(12px);\n\tlineHeight: 17px;\n}\nfilterLinkAbout: FlatLabel(defaultFlatLabel) {\n\tstyle: filterLinkAboutTextStyle;\n\talign: align(top);\n\tminWidth: 190px;\n}\nfilterLinkAboutTop: 170px;\nfilterLinkAboutBottom: 15px;\nfilterLinkPreview: 96px;\nfilterLinkPreviewRadius: 13px;\nfilterLinkPreviewTop: 30px;\nfilterLinkPreviewColumn: 65px;\nfilterLinkPreviewAllBottom: 18px;\nfilterLinkPreviewAllTop: 15px;\nfilterLinkPreviewMyBottom: 74px;\nfilterLinkPreviewMyTop: 71px;\nfilterLinkPreviewChatSize: 36px;\nfilterLinkPreviewChatSkip: 10px;\nfilterLinkPreviewBadgeLeft: 40px;\nfilterLinkPreviewBadgeTop: 38px;\nfilterLinkSubsectionTitlePadding: margins(0px, 5px, 0px, -4px);\nfilterLinkChatsList: PeerList(peerListBox) {\n\tpadding: margins(0px, 0px, 0px, membersMarginBottom);\n}\n\nsettingsColorSampleCutout: 3px;\nsettingsColorProfileSampleShift: 5px;\nsettingsColorSampleSize: 20px;\nsettingsColorSampleCenter: 6px;\nsettingsColorSampleCenterRadius: 2px;\nsettingsColorSamplePadding: margins(8px, 2px, 8px, 2px);\nsettingsColorSampleSkip: 6px;\nsettingsColorButton: SettingsButton(settingsButton) {\n\tpadding: margins(60px, 10px, 48px, 10px);\n}\nsettingsColorRadioMargin: 17px;\nsettingsColorRadioSkip: 13px;\nsettingsColorRadioStroke: 2px;\nsettingsLevelBadgeLock: IconEmoji{\n\ticon: icon {{ \"chat/mini_lock\", premiumButtonFg }};\n\tpadding: margins(0px, 4px, 0px, 0px);\n\tuseIconColor: true;\n}\n\nmessagePrivacyTopSkip: 8px;\nmessagePrivacyRadioSkip: 6px;\nmessagePrivacyBottomSkip: 10px;\nmessagePrivacyCheck: Checkbox(settingsPrivacyOption) {\n\ttextPosition: point(13px, 1px);\n}\nmessagePrivacySubscribe: SettingsButton(settingsButtonLight) {\n\tpadding: margins(56px, 10px, 22px, 8px);\n\ticonLeft: 20px;\n}\nmessagePrivacyLock: icon {{ \"info/info_rights_lock\", checkboxFg }};\n\npeerAppearanceButton: SettingsButton(settingsButtonLight) {\n\tpadding: margins(60px, 8px, 22px, 8px);\n\ticonLeft: 20px;\n}\npeerAppearanceCoverLabelMargin: margins(22px, 0px, 22px, 17px);\npeerAppearanceIconPadding: margins(0px, 15px, 0px, 5px);\npeerAppearanceDividerTextMargin: margins(22px, 8px, 22px, 11px);\n\nsettingsChatbotsUsername: InputField(defaultMultiSelectSearchField) {\n}\nsettingsChatbotsAccess: Checkbox(defaultCheckbox) {\n\ttextPosition: point(18px, 2px);\n}\nsettingsLocationAddress: InputField(defaultMultiSelectSearchField) {\n}\nsettingsChatbotsUsernameMargins: margins(20px, 8px, 20px, 8px);\nsettingsChatbotsAccessMargins: margins(22px, 5px, 22px, 9px);\nsettingsChatbotsAccessSkip: 4px;\nsettingsChatbotsBottomTextMargin: margins(22px, 8px, 22px, 3px);\nsettingsChatbotsAdd: SettingsButton(settingsButton) {\n\ticonLeft: 22px;\n}\nsettingsWorkingHoursWeek: SettingsButton(settingsButtonNoIcon) {\n\theight: 40px;\n\tpadding: margins(22px, 4px, 22px, 4px);\n}\nsettingsWorkingHoursDetails: settingsNotificationTypeDetails;\nsettingsWorkingHoursPicker: 200px;\nsettingsWorkingHoursPickerItemHeight: 40px;\n\nsettingsAwaySchedulePadding: margins(0px, 8px, 0px, 8px);\n\nsettingsAddReplyLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 256px;\n}\nsettingsAddReplyField: InputField(defaultInputField) {\n\ttextBg: transparent;\n\ttextMargins: margins(0px, 10px, 32px, 2px);\n\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(2px, 0px, 2px, 0px);\n\tplaceholderScale: 0.;\n\n\theightMin: 36px;\n}\nsettingsChatbotsNotFound: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n\talign: align(top);\n}\nsettingsChatbotsDeleteIcon: icon {{ \"dialogs/dialogs_cancel_search\", dialogsMenuIconFg }};\nsettingsChatbotsDeleteIconOver: icon {{ \"dialogs/dialogs_cancel_search\", dialogsMenuIconFgOver }};\n\nsettingsChatIntroField: InputField(defaultMultiSelectSearchField) {\n\ttextMargins: margins(2px, 0px, 32px, 0px);\n}\nsettingsChatIntroFieldMargins: margins(20px, 15px, 20px, 8px);\n\nsettingsChatLinkEmojiTop: 2px;\nsettingsChatLinkField: InputField(defaultInputField) {\n\ttextBg: transparent;\n\ttextMargins: margins(2px, 8px, 2px, 8px);\n\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(0px, 0px, 0px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n\n\tborder: 0px;\n\tborderActive: 0px;\n\n\theightMin: 32px;\n\n\tstyle: defaultTextStyle;\n}\n\nsettingsQuickDialogActionsTriggerFont: font(11px);\n\nsettingsGiftIconEmoji: IconEmoji {\n\ticon: icon{{ \"settings/mini_gift\", windowFg }};\n\tpadding: margins(1px, 2px, 1px, 0px);\n}\n\nsettingsCreditsButtonBuyIcon: icon {{ \"settings/add\", windowBgActive, point(7px, 0px) }};\n\nsettingsCreditsButton: SettingsButton(settingsButton) {\n\tpadding: margins(62px, 8px, 22px, 8px);\n}\nsettingsButtonIconGift: icon {{ \"settings/gift\", menuIconColor }};\nsettingsButtonIconEarn: icon {{ \"settings/earn\", menuIconColor }};\n\nsettingsIconPasskeys: icon {{ \"menu/permissions\", windowBgActive }};\nsettingsIconPasskeysAboutIcon1: icon {{ \"menu/permissions\", boxTextFg }};\nsettingsIconPasskeysAboutIcon2: icon {{ \"menu/passcode_winhello\", boxTextFg }};\nsettingsIconPasskeysAboutIcon3: icon {{ \"menu/lock\", boxTextFg }};\n\nsettingsAgeVerifyBox: Box(filterInviteBox) {\n\tbuttonPadding: margins(24px, 24px, 24px, 24px);\n}\nsettingsAgeVerifyText: FlatLabel(defaultFlatLabel) {\n\tminWidth: 200px;\n\talign: align(top);\n}\nsettingsAgeVerifyMargin: margins(0px, 6px, 0px, 6px);\nsettingsAgeVerifyTitle: FlatLabel(boxTitle) {\n\talign: align(top);\n}\nsettingsAgeVerifyIcon: icon {{ \"settings/filled_verify_age-48x48\", windowFgActive }};\nsettingsAgeVerifyIconPadding: margins(16px, 16px, 16px, 16px);\nsettingsAgeVerifyIconMargin: margins(0px, 24px, 0px, 14px);\n\nDetailedSettingsButtonStyle {\n\tbutton: SettingsButton;\n\tdescription: FlatLabel;\n\ticonRadius: pixels;\n\ticonPadding: pixels;\n\ticonForegroundPadding: pixels;\n\tdescriptionTopSkip: pixels;\n\tdescriptionBottomSkip: pixels;\n\tdescriptionRightSkip: pixels;\n}\n\ndetailedSettingsButtonStyle: DetailedSettingsButtonStyle {\n\tbutton: SettingsButton(settingsButtonNoIconLocked) {\n\t\tstyle: defaultTextStyle;\n\t\ttextBg: transparent;\n\t\ttextBgOver: transparent;\n\t\tpadding: margins(68px, 8px, 22px, 8px);\n\t\ticonLeft: 17px;\n\t}\n\tdescription: FlatLabel(defaultFlatLabel) {\n\t\ttextFg: windowSubTextFg;\n\t}\n\ticonRadius: 10px;\n\ticonPadding: 6px;\n\ticonForegroundPadding: 3px;\n\tdescriptionTopSkip: 0px;\n\tdescriptionBottomSkip: 8px;\n\tdescriptionRightSkip: 4px;\n}\n\nsettingsToastStarIcon: icon {{ \"toast/star\", toastFg }};\n\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_builder.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/settings_builder.h\"\n\n#include \"apiwrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"api/api_user_privacy.h\"\n#include \"boxes/edit_privacy_box.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/sections/settings_privacy_security.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings::Builder {\nnamespace {\n\n[[nodiscard]] QString ResolveTitle(rpl::producer<QString> title) {\n\tauto result = QString();\n\tauto lifetime = rpl::lifetime();\n\tstd::move(title).start(\n\t\t[&](QString value) { result = std::move(value); },\n\t\t[](auto&&) {},\n\t\t[] {},\n\t\tlifetime);\n\treturn result;\n}\n\n} // namespace\n\nBuildHelper::BuildHelper(\n\tSectionMeta &&meta,\n\tFnMut<void(SectionBuilder&)> method)\n: build([=](\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFn<void(Type)> showOther,\n\t\trpl::producer<> showFinished) {\n\tauto &lifetime = container->lifetime();\n\tconst auto highlights = lifetime.make_state<HighlightRegistry>();\n\tconst auto isPaused = Window::PausedIn(\n\t\tcontroller,\n\t\tWindow::GifPauseReason::Layer);\n\tauto builder = SectionBuilder(WidgetContext{\n\t\t.container = static_cast<Ui::VerticalLayout*>(\n\t\t\tcontainer->add(object_ptr<Ui::OverrideMargins>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(container)))->entity()),\n\t\t.controller = controller,\n\t\t.showOther = std::move(showOther),\n\t\t.isPaused = isPaused,\n\t\t.highlights = highlights,\n\t});\n\t_method(builder);\n\n\tstd::move(showFinished) | rpl::on_next([=] {\n\t\tfor (const auto &[id, entry] : *highlights) {\n\t\t\tif (entry.widget) {\n\t\t\t\tcontroller->checkHighlightControl(\n\t\t\t\t\tid,\n\t\t\t\t\tentry.widget,\n\t\t\t\t\tbase::duplicate(entry.args));\n\t\t\t}\n\t\t}\n\t}, lifetime);\n})\n, _meta(std::move(meta))\n, _method(std::move(method)) {\n\tExpects(_method != nullptr);\n\n\tSearchRegistry::Instance().add(\n\t\t&_meta,\n\t\t[=](not_null<::Main::Session*> session) { return index(session); });\n}\n\nSearchRegistry &SearchRegistry::Instance() {\n\tstatic SearchRegistry instance;\n\treturn instance;\n}\n\nvoid SearchRegistry::add(\n\t\tnot_null<const SectionMeta*> meta,\n\t\tSearchEntriesIndexer indexer) {\n\t_sections[meta->id] = meta;\n\t_indexers.push_back({ meta->id, std::move(indexer) });\n}\n\nstd::vector<SearchEntry> SearchRegistry::collectAll(\n\t\tnot_null<Main::Session*> session) const {\n\tauto result = std::vector<SearchEntry>();\n\tfor (const auto &[sectionId, meta] : _sections) {\n\t\tif (meta->parentId) {\n\t\t\tresult.push_back({\n\t\t\t\t.title = (*meta->title)(tr::now),\n\t\t\t\t.section = sectionId,\n\t\t\t\t.icon = { meta->icon },\n\t\t\t});\n\t\t}\n\t}\n\tfor (const auto &entry : _indexers) {\n\t\tauto entries = entry.indexer(session);\n\t\tresult.insert(result.end(), entries.begin(), entries.end());\n\t}\n\treturn result;\n}\n\nQString SearchRegistry::sectionTitle(Type sectionId) const {\n\tconst auto it = _sections.find(sectionId);\n\treturn (it != _sections.end()) ? (*it->second->title)(tr::now) : QString();\n}\n\nQString SearchRegistry::sectionPath(Type sectionId, bool parentsOnly) const {\n\tauto parts = QStringList();\n\tauto current = sectionId;\n\twhile (current) {\n\t\tif (const auto title = sectionTitle(current); !title.isEmpty()) {\n\t\t\tparts.prepend(title);\n\t\t}\n\t\tconst auto it = _sections.find(current);\n\t\tcurrent = (it != _sections.end()) ? it->second->parentId : nullptr;\n\t}\n\tif (parts.size() > 1 && parentsOnly) {\n\t\tparts.pop_back();\n\t}\n\treturn parts.join(u\" > \"_q);\n}\n\nstd::vector<SearchEntry> BuildHelper::index(\n\t\tnot_null<Main::Session*> session) const {\n\tauto entries = std::vector<SearchEntry>();\n\tauto builder = SectionBuilder(SearchContext{\n\t\t.sectionId = _meta.id,\n\t\t.session = session,\n\t\t.entries = &entries,\n\t});\n\t_method(builder);\n\tfor (auto &entry : entries) {\n\t\tif (!entry.section) {\n\t\t\tentry.section = _meta.id;\n\t\t}\n\t}\n\treturn entries;\n}\n\nSectionBuilder::SectionBuilder(BuildContext context)\n: _context(std::move(context)) {\n}\n\nvoid SectionBuilder::add(FnMut<void(const BuildContext &ctx)> method) {\n\tExpects(method != nullptr);\n\n\tmethod(_context);\n}\n\nUi::VerticalLayout *SectionBuilder::scope(\n\t\tFnMut<void()> method,\n\t\trpl::producer<bool> shown,\n\t\tFnMut<void(ToggledScopePtr)> hook) {\n\tauto result = (Ui::VerticalLayout*)nullptr;\n\tv::match(_context, [&](WidgetContext &wctx) {\n\t\tconst auto outer = wctx.container;\n\t\tconst auto toggled = (shown || hook);\n\t\tconst auto wrap = toggled\n\t\t\t? outer->add(\n\t\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\t\touter,\n\t\t\t\t\tobject_ptr<Ui::VerticalLayout>(outer)))\n\t\t\t: nullptr;\n\t\tconst auto inner = wrap\n\t\t\t? wrap->entity()\n\t\t\t: outer->add(object_ptr<Ui::VerticalLayout>(outer));\n\t\twctx.container = inner;\n\t\tmethod();\n\t\tif (shown) {\n\t\t\twrap->toggleOn(std::move(shown));\n\t\t\twrap->finishAnimating();\n\t\t}\n\t\tif (hook) {\n\t\t\thook(wrap);\n\t\t}\n\t\twctx.container = outer;\n\t\tresult = inner;\n\t}, [&](const SearchContext &sctx) {\n\t\tmethod();\n\t});\n\treturn result;\n}\n\nUi::RpWidget *SectionBuilder::add(\n\t\tFnMut<WidgetToAdd(const WidgetContext &ctx)> widget,\n\t\tFnMut<SearchEntry()> search) {\n\tauto result = (Ui::RpWidget*)nullptr;\n\tadd([&](const BuildContext &ctx) {\n\t\tv::match(ctx, [&](const WidgetContext &wctx) {\n\t\t\tif (auto w = widget ? widget(wctx) : WidgetToAdd()) {\n\t\t\t\tresult = w.widget.data();\n\t\t\t\twctx.container->add(std::move(w.widget), w.margin, w.align);\n\n\t\t\t\tif (auto entry = search ? search() : SearchEntry()) {\n\t\t\t\t\tif (wctx.highlights) {\n\t\t\t\t\t\twctx.highlights->push_back({\n\t\t\t\t\t\t\tstd::move(entry.id),\n\t\t\t\t\t\t\t{ result, std::move(w.highlight) },\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}, [&](const SearchContext &sctx) {\n\t\t\tif (auto entry = search ? search() : SearchEntry()) {\n\t\t\t\tentry.section = sctx.sectionId;\n\t\t\t\tsctx.entries->push_back(std::move(entry));\n\t\t\t}\n\t\t});\n\t});\n\treturn result;\n}\n\nUi::RpWidget *SectionBuilder::addControl(ControlArgs &&args) {\n\tif (auto shown = base::take(args.shown)) {\n\t\tauto result = (Ui::RpWidget*)nullptr;\n\t\tscope([&] {\n\t\t\tresult = addControl(std::move(args));\n\t\t}, std::move(shown));\n\t\treturn result;\n\t}\n\treturn add([&](const WidgetContext &ctx) {\n\t\treturn WidgetToAdd{\n\t\t\t.widget = args.factory ? args.factory(ctx.container) : nullptr,\n\t\t\t.margin = args.margin,\n\t\t\t.align = args.align,\n\t\t\t.highlight = std::move(args.highlight),\n\t\t};\n\t}, [&]() mutable {\n\t\treturn SearchEntry{\n\t\t\t.id = std::move(args.id),\n\t\t\t.title = ResolveTitle(std::move(args.title)),\n\t\t\t.keywords = std::move(args.keywords),\n\t\t\t.icon = std::move(args.searchIcon),\n\t\t\t.checkIcon = args.searchCheckIcon,\n\t\t};\n\t});\n}\n\nUi::SettingsButton *SectionBuilder::addButton(ButtonArgs &&args) {\n\tconst auto &st = args.st ? *args.st : st::settingsButton;\n\tauto iconForSearch = IconDescriptor{ args.icon.icon };\n\tconst auto factory = [&](not_null<Ui::VerticalLayout*> container) {\n\t\tauto button = CreateButtonWithIcon(\n\t\t\targs.container ? args.container : container.get(),\n\t\t\trpl::duplicate(args.title),\n\t\t\tst,\n\t\t\tstd::move(args.icon));\n\t\tif (button && args.onClick) {\n\t\t\tbutton->addClickHandler(std::move(args.onClick));\n\t\t}\n\t\tif (args.label) {\n\t\t\tCreateRightLabel(\n\t\t\t\tbutton.data(),\n\t\t\t\tstd::move(args.label),\n\t\t\t\tst,\n\t\t\t\trpl::duplicate(args.title));\n\t\t}\n\t\tif (args.toggled) {\n\t\t\tbutton->toggleOn(std::move(args.toggled));\n\t\t}\n\t\treturn button;\n\t};\n\treturn static_cast<Ui::SettingsButton*>(addControl({\n\t\t.factory = factory,\n\t\t.id = std::move(args.id),\n\t\t.title = rpl::duplicate(args.title),\n\t\t.highlight = std::move(args.highlight),\n\t\t.shown = std::move(args.shown),\n\n\t\t.keywords = std::move(args.keywords),\n\t\t.searchIcon = std::move(iconForSearch),\n\t}));\n}\n\nUi::SettingsButton *SectionBuilder::addSectionButton(SectionArgs &&args) {\n\tconst auto wctx = std::get_if<WidgetContext>(&_context);\n\tconst auto showOther = wctx ? wctx->showOther : nullptr;\n\tconst auto target = args.targetSection;\n\treturn addButton({\n\t\t.title = std::move(args.title),\n\t\t.icon = std::move(args.icon),\n\t\t.onClick = [=] { showOther(target); },\n\t\t.keywords = std::move(args.keywords),\n\t});\n}\n\nvoid SectionBuilder::addDivider() {\n\tv::match(_context, [&](const WidgetContext &ctx) {\n\t\tUi::AddDivider(ctx.container);\n\t}, [](const SearchContext &) {\n\t});\n}\n\nvoid SectionBuilder::addSkip() {\n\tv::match(_context, [&](const WidgetContext &ctx) {\n\t\tUi::AddSkip(ctx.container);\n\t}, [](const SearchContext &) {\n\t});\n}\n\nvoid SectionBuilder::addSkip(int height) {\n\tv::match(_context, [&](const WidgetContext &ctx) {\n\t\tUi::AddSkip(ctx.container, height);\n\t}, [](const SearchContext &) {\n\t});\n}\n\nvoid SectionBuilder::addDividerText(rpl::producer<QString> text) {\n\tv::match(_context, [&](const WidgetContext &ctx) {\n\t\tUi::AddDividerText(ctx.container, std::move(text));\n\t}, [](const SearchContext &) {\n\t});\n}\n\nUi::SettingsButton *SectionBuilder::addPremiumButton(PremiumButtonArgs &&args) {\n\tconst auto result = addButton({\n\t\t.id = std::move(args.id),\n\t\t.title = std::move(args.title),\n\t\t.label = std::move(args.label),\n\t\t.onClick = std::move(args.onClick),\n\t\t.keywords = std::move(args.keywords),\n\t});\n\tif (result) {\n\t\tAddPremiumStar(\n\t\t\tresult,\n\t\t\targs.credits,\n\t\t\tv::get<WidgetContext>(_context).isPaused);\n\t}\n\treturn result;\n}\n\nUi::SettingsButton *SectionBuilder::addPrivacyButton(PrivacyButtonArgs &&args) {\n\tconst auto controller = this->controller();\n\tconst auto session = this->session();\n\n\tconst auto button = addButton({\n\t\t.id = args.id,\n\t\t.title = rpl::duplicate(args.title),\n\t\t.st = &st::settingsButtonNoIcon,\n\t\t.label = PrivacyButtonLabel(session, args.key),\n\t\t.keywords = args.keywords,\n\t});\n\tif (button) {\n\t\tconst auto id = args.id;\n\t\tconst auto premium = args.premium;\n\t\tconst auto shower = Ui::CreateChild<rpl::lifetime>(button);\n\t\tconst auto factory = args.controllerFactory;\n\t\tbutton->addClickHandler([=, key = args.key] {\n\t\t\t*shower = session->api().userPrivacy().value(\n\t\t\t\tkey\n\t\t\t) | rpl::take(\n\t\t\t\t1\n\t\t\t) | rpl::on_next(crl::guard(controller, [=](\n\t\t\t\t\tconst Api::UserPrivacy::Rule &value) {\n\t\t\t\tcontroller->show(Box<EditPrivacyBox>(\n\t\t\t\t\tcontroller,\n\t\t\t\t\tfactory(),\n\t\t\t\t\tvalue));\n\t\t\t}));\n\t\t});\n\t\tif (premium) {\n\t\t\tAddPrivacyPremiumStar(\n\t\t\t\tbutton,\n\t\t\t\tsession,\n\t\t\t\tstd::move(args.title),\n\t\t\t\tst::settingsButtonNoIcon.padding);\n\t\t}\n\t}\n\treturn button;\n}\n\nUi::Checkbox *SectionBuilder::addCheckbox(CheckboxArgs &&args) {\n\tconst auto factory = [&](not_null<Ui::VerticalLayout*> container) {\n\t\treturn object_ptr<Ui::Checkbox>(\n\t\t\tcontainer,\n\t\t\tResolveTitle(rpl::duplicate(args.title)),\n\t\t\targs.checked,\n\t\t\tst::settingsCheckbox);\n\t};\n\treturn static_cast<Ui::Checkbox*>(addControl({\n\t\t.factory = factory,\n\t\t.id = std::move(args.id),\n\t\t.title = rpl::duplicate(args.title),\n\t\t.margin = st::settingsCheckboxPadding,\n\t\t.highlight = std::move(args.highlight),\n\t\t.shown = std::move(args.shown),\n\t\t.keywords = std::move(args.keywords),\n\t\t.searchCheckIcon = (args.checked\n\t\t\t? SearchEntryCheckIcon::Checked\n\t\t\t: SearchEntryCheckIcon::Unchecked),\n\t}));\n}\n\nvoid SectionBuilder::addSubsectionTitle(SubsectionTitleArgs &&args) {\n\tv::match(_context, [&](const WidgetContext &ctx) {\n\t\tconst auto title = AddSubsectionTitle(\n\t\t\tctx.container,\n\t\t\trpl::duplicate(args.title));\n\t\tif (!args.id.isEmpty() && ctx.highlights) {\n\t\t\tctx.highlights->push_back({\n\t\t\t\targs.id,\n\t\t\t\t{ title.get(), SubsectionTitleHighlight() },\n\t\t\t});\n\t\t}\n\t}, [&](const SearchContext &ctx) {\n\t\tif (!args.id.isEmpty()) {\n\t\t\tctx.entries->push_back({\n\t\t\t\t.id = std::move(args.id),\n\t\t\t\t.title = ResolveTitle(std::move(args.title)),\n\t\t\t\t.keywords = std::move(args.keywords),\n\t\t\t\t.section = ctx.sectionId,\n\t\t\t});\n\t\t}\n\t});\n}\n\nvoid SectionBuilder::addSubsectionTitle(rpl::producer<QString> text) {\n\tv::match(_context, [&](const WidgetContext &ctx) {\n\t\tAddSubsectionTitle(ctx.container, std::move(text));\n\t}, [](const SearchContext &) {\n\t});\n}\n\nUi::VerticalLayout *SectionBuilder::container() const {\n\treturn v::match(_context, [](const WidgetContext &ctx) {\n\t\treturn ctx.container.get();\n\t}, [](const SearchContext &) -> Ui::VerticalLayout* {\n\t\treturn nullptr;\n\t});\n}\n\nWindow::SessionController *SectionBuilder::controller() const {\n\treturn v::match(_context, [](const WidgetContext &ctx) {\n\t\treturn ctx.controller.get();\n\t}, [](const SearchContext &) -> Window::SessionController* {\n\t\treturn nullptr;\n\t});\n}\n\nnot_null<Main::Session*> SectionBuilder::session() const {\n\treturn v::match(_context, [](const WidgetContext &ctx) {\n\t\treturn &ctx.controller->session();\n\t}, [](const SearchContext &ctx) {\n\t\treturn ctx.session.get();\n\t});\n}\n\nFn<void(Type)> SectionBuilder::showOther() const {\n\treturn v::match(_context, [](const WidgetContext &ctx) {\n\t\treturn ctx.showOther;\n\t}, [](const SearchContext &) -> Fn<void(Type)> {\n\t\treturn nullptr;\n\t});\n}\n\nHighlightRegistry *SectionBuilder::highlights() const {\n\treturn v::match(_context, [](const WidgetContext &ctx) {\n\t\treturn ctx.highlights;\n\t}, [](const SearchContext &) -> HighlightRegistry* {\n\t\treturn nullptr;\n\t});\n}\n\nvoid SectionBuilder::registerHighlight(\n\t\tQString id,\n\t\tQWidget *widget,\n\t\tHighlightArgs &&args) {\n\tv::match(_context, [&](const WidgetContext &ctx) {\n\t\tif (ctx.highlights && widget) {\n\t\t\tctx.highlights->push_back({\n\t\t\t\tstd::move(id),\n\t\t\t\t{ widget, std::move(args) },\n\t\t\t});\n\t\t}\n\t}, [](const SearchContext &) {\n\t});\n}\n\n} // namespace Settings::Builder\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_builder.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_user_privacy.h\"\n#include \"base/object_ptr.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/settings_type.h\"\n\n#include <variant>\n#include <vector>\n\nclass EditPrivacyController;\n\nnamespace Ui {\nclass RpWidget;\nclass VerticalLayout;\nclass SettingsButton;\nclass Checkbox;\n} // namespace Ui\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace style {\nstruct SettingsButton;\n} // namespace style\n\nnamespace st {\nextern const int &boxRadius;\n} // namespace st\n\nnamespace Ui {\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace tr {\ntemplate <typename ...Tags>\nstruct phrase;\n} // namespace tr\n\nnamespace Settings::Builder {\n\nclass SectionBuilder;\n\nenum class SearchEntryCheckIcon {\n\tNone,\n\tChecked,\n\tUnchecked,\n};\nstruct SearchEntry {\n\tQString id;\n\tQString title;\n\tQStringList keywords;\n\tType section;\n\tIconDescriptor icon;\n\tSearchEntryCheckIcon checkIcon = SearchEntryCheckIcon::None;\n\tQString deeplink;\n\n\texplicit operator bool() const {\n\t\treturn !id.isEmpty();\n\t}\n};\n\nusing SearchEntriesIndexer = Fn<std::vector<SearchEntry>(\n\tnot_null<::Main::Session*> session)>;\n\nstruct SearchIndexerEntry {\n\tType sectionId;\n\tSearchEntriesIndexer indexer;\n};\n\nstruct SectionMeta {\n\tType id;\n\tType parentId;\n\tnot_null<const tr::phrase<>*> title;\n\tnot_null<const style::icon*> icon;\n};\n\nclass SearchRegistry {\npublic:\n\tstatic SearchRegistry &Instance();\n\n\tvoid add(\n\t\tnot_null<const SectionMeta*> meta,\n\t\tSearchEntriesIndexer indexer);\n\n\t[[nodiscard]] std::vector<SearchEntry> collectAll(\n\t\tnot_null<::Main::Session*> session) const;\n\n\t[[nodiscard]] QString sectionTitle(Type sectionId) const;\n\t[[nodiscard]] QString sectionPath(\n\t\tType sectionId,\n\t\tbool parentsOnly = false) const;\n\nprivate:\n\tstd::vector<SearchIndexerEntry> _indexers;\n\tbase::flat_map<Type, const SectionMeta*> _sections;\n\n};\n\n\nclass BuildHelper {\npublic:\n\tBuildHelper(SectionMeta &&meta, FnMut<void(SectionBuilder&)> method);\n\n\tconst SectionBuildMethod build;\n\n\t[[nodiscard]] std::vector<SearchEntry> index(\n\t\tnot_null<::Main::Session*> session) const;\n\nprivate:\n\tconst SectionMeta _meta;\n\tmutable FnMut<void(SectionBuilder &)> _method;\n\n};\n\nstruct WidgetContext {\n\tnot_null<Ui::VerticalLayout*> container;\n\tnot_null<Window::SessionController*> controller;\n\tFn<void(Type)> showOther;\n\tFn<bool()> isPaused;\n\tHighlightRegistry *highlights = nullptr;\n};\n\nstruct SearchContext {\n\tType sectionId;\n\tnot_null<::Main::Session*> session;\n\tnot_null<std::vector<SearchEntry>*> entries;\n};\n\nusing BuildContext = std::variant<WidgetContext, SearchContext>;\n\nclass SectionBuilder {\npublic:\n\texplicit SectionBuilder(BuildContext context);\n\n\tvoid add(FnMut<void(const BuildContext &ctx)> method);\n\n\tusing ToggledScopePtr = not_null<Ui::SlideWrap<Ui::VerticalLayout>*>;\n\tUi::VerticalLayout *scope(\n\t\tFnMut<void()> method,\n\t\trpl::producer<bool> shown = nullptr,\n\t\tFnMut<void(ToggledScopePtr)> hook = nullptr);\n\n\tstruct WidgetToAdd {\n\t\tobject_ptr<Ui::RpWidget> widget = { nullptr };\n\t\tQMargins margin;\n\t\tstyle::align align = style::al_left;\n\t\tHighlightArgs highlight;\n\n\t\texplicit operator bool() const {\n\t\t\treturn widget != nullptr;\n\t\t}\n\t};\n\tUi::RpWidget *add(\n\t\tFnMut<WidgetToAdd(const WidgetContext &ctx)> widget,\n\t\tFnMut<SearchEntry()> search = nullptr);\n\n\tstruct ControlArgs {\n\t\tFn<object_ptr<Ui::RpWidget>(not_null<Ui::VerticalLayout*>)> factory;\n\t\tQString id;\n\t\trpl::producer<QString> title;\n\t\tstyle::margins margin;\n\t\tstyle::align align = style::al_left;\n\t\tHighlightArgs highlight;\n\t\trpl::producer<bool> shown;\n\n\t\tQStringList keywords;\n\t\tIconDescriptor searchIcon;\n\t\tSearchEntryCheckIcon searchCheckIcon = SearchEntryCheckIcon::None;\n\t};\n\tUi::RpWidget *addControl(ControlArgs &&args);\n\n\tstruct ButtonArgs {\n\t\tQString id;\n\t\trpl::producer<QString> title;\n\t\tconst style::SettingsButton *st = nullptr;\n\t\tIconDescriptor icon;\n\t\tUi::VerticalLayout *container = nullptr;\n\t\trpl::producer<QString> label;\n\t\trpl::producer<bool> toggled;\n\t\tFn<void()> onClick;\n\t\tQStringList keywords;\n\t\tHighlightArgs highlight;\n\t\trpl::producer<bool> shown;\n\t};\n\tUi::SettingsButton *addButton(ButtonArgs &&args);\n\n\tstruct SectionArgs {\n\t\t//QString id; // Sections should register themselves in search.\n\t\trpl::producer<QString> title;\n\t\tType targetSection;\n\t\tIconDescriptor icon;\n\t\tQStringList keywords;\n\t};\n\tUi::SettingsButton *addSectionButton(SectionArgs &&args);\n\n\tstruct PremiumButtonArgs {\n\t\tQString id;\n\t\trpl::producer<QString> title;\n\t\trpl::producer<QString> label;\n\t\tbool credits = false;\n\t\tFn<void()> onClick;\n\t\tQStringList keywords;\n\t};\n\tUi::SettingsButton *addPremiumButton(PremiumButtonArgs &&args);\n\n\tstruct PrivacyButtonArgs {\n\t\tQString id;\n\t\trpl::producer<QString> title;\n\t\tApi::UserPrivacy::Key key;\n\t\tFn<std::unique_ptr<EditPrivacyController>()> controllerFactory;\n\t\tbool premium = false;\n\t\tQStringList keywords;\n\t};\n\tUi::SettingsButton *addPrivacyButton(PrivacyButtonArgs &&args);\n\n\tstruct CheckboxArgs {\n\t\tQString id;\n\t\trpl::producer<QString> title;\n\t\tbool checked = false;\n\t\tQStringList keywords;\n\t\tHighlightArgs highlight = { .radius = st::boxRadius };\n\t\trpl::producer<bool> shown;\n\t};\n\tUi::Checkbox *addCheckbox(CheckboxArgs &&args);\n\n\tstruct SubsectionTitleArgs {\n\t\tQString id;\n\t\trpl::producer<QString> title;\n\t\tQStringList keywords;\n\t};\n\tvoid addSubsectionTitle(SubsectionTitleArgs &&args);\n\tvoid addSubsectionTitle(rpl::producer<QString> text);\n\tvoid addDivider();\n\tvoid addDividerText(rpl::producer<QString> text);\n\tvoid addSkip();\n\tvoid addSkip(int height);\n\n\t[[nodiscard]] Ui::VerticalLayout *container() const;\n\t[[nodiscard]] Window::SessionController *controller() const;\n\t[[nodiscard]] not_null<::Main::Session*> session() const;\n\t[[nodiscard]] Fn<void(Type)> showOther() const;\n\t[[nodiscard]] HighlightRegistry *highlights() const;\n\nprivate:\n\tvoid registerHighlight(\n\t\tQString id,\n\t\tQWidget *widget,\n\t\tHighlightArgs &&args);\n\n\tBuildContext _context;\n\n};\n\n} // namespace Settings::Builder\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_codes.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/settings_codes.h\"\n\n#include \"ui/toast/toast.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"data/data_session.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"history/history_item_components.h\"\n#include \"main/main_session.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"lang/lang_cloud_manager.h\"\n#include \"lang/lang_instance.h\"\n#include \"core/application.h\"\n#include \"mtproto/mtp_instance.h\"\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"core/file_utilities.h\"\n#include \"core/update_checker.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/themes/window_theme_editor.h\"\n#include \"window/window_session_controller.h\"\n#include \"media/audio/media_audio_track.h\"\n#include \"settings/sections/settings_folders.h\"\n#include \"storage/storage_account.h\"\n#include \"api/api_updates.h\"\n#include \"base/qt/qt_common_adapters.h\"\n#include \"base/custom_app_icon.h\"\n#include \"base/options.h\"\n#include \"boxes/abstract_box.h\" // Ui::show().\n\n#include <zlib.h>\n\nnamespace Settings {\nnamespace {\n\nusing SessionController = Window::SessionController;\n\n[[nodiscard]] QByteArray UnpackRawGzip(const QByteArray &bytes) {\n\tz_stream stream;\n\tstream.zalloc = nullptr;\n\tstream.zfree = nullptr;\n\tstream.opaque = nullptr;\n\tstream.avail_in = 0;\n\tstream.next_in = nullptr;\n\tint res = inflateInit2(&stream, -MAX_WBITS);\n\tif (res != Z_OK) {\n\t\treturn QByteArray();\n\t}\n\tconst auto guard = gsl::finally([&] { inflateEnd(&stream); });\n\n\tauto result = QByteArray(1024 * 1024, char(0));\n\tstream.avail_in = bytes.size();\n\tstream.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(bytes.data()));\n\tstream.avail_out = 0;\n\twhile (!stream.avail_out) {\n\t\tstream.avail_out = result.size();\n\t\tstream.next_out = reinterpret_cast<Bytef*>(result.data());\n\t\tint res = inflate(&stream, Z_NO_FLUSH);\n\t\tif (res != Z_OK && res != Z_STREAM_END) {\n\t\t\treturn QByteArray();\n\t\t} else if (!stream.avail_out) {\n\t\t\treturn QByteArray();\n\t\t}\n\t}\n\tresult.resize(result.size() - stream.avail_out);\n\treturn result;\n}\n\nauto GenerateCodes() {\n\tauto codes = std::map<QString, Fn<void(SessionController*)>>();\n\tcodes.emplace(u\"debugmode\"_q, [](SessionController *window) {\n\t\tQString text = Logs::DebugEnabled()\n\t\t\t? u\"Do you want to disable DEBUG logs?\"_q\n\t\t\t: u\"Do you want to enable DEBUG logs?\\n\\nAll network events will be logged.\"_q;\n\t\tUi::show(Ui::MakeConfirmBox({ text, [] {\n\t\t\tCore::App().switchDebugMode();\n\t\t} }));\n\t});\n\tcodes.emplace(u\"viewlogs\"_q, [](SessionController *window) {\n\t\tFile::ShowInFolder(cWorkingDir() + \"log.txt\");\n\t});\n\tif (!Core::UpdaterDisabled()) {\n\t\tcodes.emplace(u\"testupdate\"_q, [](SessionController *window) {\n\t\t\tCore::UpdateChecker().test();\n\t\t});\n\t}\n\tcodes.emplace(u\"loadlang\"_q, [](SessionController *window) {\n\t\tLang::CurrentCloudManager().switchToLanguage({ u\"#custom\"_q });\n\t});\n\tcodes.emplace(u\"crashplease\"_q, [](SessionController *window) {\n\t\tUnexpected(\"Crashed in Settings!\");\n\t});\n\tcodes.emplace(u\"moderate\"_q, [](SessionController *window) {\n\t\tauto text = Core::App().settings().moderateModeEnabled() ? u\"Disable moderate mode?\"_q : u\"Enable moderate mode?\"_q;\n\t\tUi::show(Ui::MakeConfirmBox({ text, [=] {\n\t\t\tCore::App().settings().setModerateModeEnabled(!Core::App().settings().moderateModeEnabled());\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\tUi::hideLayer();\n\t\t} }));\n\t});\n\tcodes.emplace(u\"getdifference\"_q, [](SessionController *window) {\n\t\tif (window) {\n\t\t\twindow->session().updates().getDifference();\n\t\t}\n\t});\n\tcodes.emplace(u\"loadcolors\"_q, [](SessionController *window) {\n\t\tFileDialog::GetOpenPath(Core::App().getFileDialogParent(), \"Open palette file\", \"Palette (*.tdesktop-palette)\", [](const FileDialog::OpenResult &result) {\n\t\t\tif (!result.paths.isEmpty()) {\n\t\t\t\tWindow::Theme::Apply(result.paths.front());\n\t\t\t}\n\t\t});\n\t});\n\tcodes.emplace(u\"endpoints\"_q, [](SessionController *window) {\n\t\tif (!Core::App().domain().started()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto weak = window\n\t\t\t? base::make_weak(&window->session().account())\n\t\t\t: nullptr;\n\t\tFileDialog::GetOpenPath(Core::App().getFileDialogParent(), \"Open DC endpoints\", \"DC Endpoints (*.tdesktop-endpoints)\", [weak](const FileDialog::OpenResult &result) {\n\t\t\tif (!result.paths.isEmpty()) {\n\t\t\t\tconst auto loadFor = [&](not_null<Main::Account*> account) {\n\t\t\t\t\tif (!account->mtp().dcOptions().loadFromFile(result.paths.front())) {\n\t\t\t\t\t\tUi::show(Ui::MakeInformBox(\"Could not load endpoints\"\n\t\t\t\t\t\t\t\" :( Errors in 'log.txt'.\"));\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\tloadFor(strong);\n\t\t\t\t} else {\n\t\t\t\t\tfor (const auto &pair : Core::App().domain().accounts()) {\n\t\t\t\t\t\tloadFor(pair.account.get());\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n\tcodes.emplace(u\"testmode\"_q, [](SessionController *window) {\n\t\tauto &domain = Core::App().domain();\n\t\tif (domain.started()\n\t\t\t&& (domain.accounts().size() == 1)\n\t\t\t&& !domain.active().sessionExists()) {\n\t\t\tconst auto environment = domain.active().mtp().environment();\n\t\t\tdomain.addActivated([&] {\n\t\t\t\treturn (environment == MTP::Environment::Production)\n\t\t\t\t\t? MTP::Environment::Test\n\t\t\t\t\t: MTP::Environment::Production;\n\t\t\t}());\n\t\t\tUi::Toast::Show((environment == MTP::Environment::Production)\n\t\t\t\t? \"Switched to the test environment.\"\n\t\t\t\t: \"Switched to the production environment.\");\n\t\t}\n\t});\n\tcodes.emplace(u\"folders\"_q, [](SessionController *window) {\n\t\tif (window) {\n\t\t\twindow->showSettings(Settings::FoldersId());\n\t\t}\n\t});\n\tcodes.emplace(u\"registertg\"_q, [](SessionController *window) {\n\t\tCore::Application::RegisterUrlScheme();\n\t\tUi::Toast::Show(\"Forced custom scheme register.\");\n\t});\n\tcodes.emplace(u\"numberbuttons\"_q, [](SessionController *window) {\n\t\tusing namespace base::options;\n\t\tauto &option = lookup<bool>(kOptionFastButtonsMode);\n\t\tconst auto now = !option.value();\n\t\toption.set(now);\n\t\tUi::Toast::Show(now\n\t\t\t? u\"Fast buttons mode enabled.\"_q\n\t\t\t: u\"Fast buttons mode disabled.\"_q);\n\t});\n\n\tauto audioFilters = u\"Audio files (*.wav *.mp3);;\"_q + FileDialog::AllFilesFilter();\n\tauto audioKeys = {\n\t\tu\"msg_incoming\"_q,\n\t\tu\"call_incoming\"_q,\n\t\tu\"call_outgoing\"_q,\n\t\tu\"call_busy\"_q,\n\t\tu\"call_connect\"_q,\n\t\tu\"call_end\"_q,\n\t};\n\tfor (auto &key : audioKeys) {\n\t\tcodes.emplace(key, [=](SessionController *window) {\n\t\t\tFileDialog::GetOpenPath(Core::App().getFileDialogParent(), \"Open audio file\", audioFilters, [=](const FileDialog::OpenResult &result) {\n\t\t\t\tif (!result.paths.isEmpty()) {\n\t\t\t\t\tauto track = Media::Audio::Current().createTrack();\n\t\t\t\t\ttrack->fillFromFile(result.paths.front());\n\t\t\t\t\tif (track->failed()) {\n\t\t\t\t\t\tUi::show(Ui::MakeInformBox(\n\t\t\t\t\t\t\t\"Could not audio :( Errors in 'log.txt'.\"));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tCore::App().settings().setSoundOverride(\n\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\tresult.paths.front());\n\t\t\t\t\t\tCore::App().saveSettingsDelayed();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\tcodes.emplace(u\"sounds_reset\"_q, [](SessionController *window) {\n\t\tCore::App().settings().clearSoundOverrides();\n\t\tCore::App().saveSettingsDelayed();\n\t\tUi::show(Ui::MakeInformBox(\"All sound overrides were reset.\"));\n\t});\n\tcodes.emplace(u\"unpacklog\"_q, [](SessionController *window) {\n\t\tFileDialog::GetOpenPath(Core::App().getFileDialogParent(), \"Open crash log file\", \"Crash dump (*.txt)\", [=](const FileDialog::OpenResult &result) {\n\t\t\tif (result.paths.isEmpty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto f = QFile(result.paths.front());\n\t\t\tif (!f.open(QIODevice::ReadOnly)) {\n\t\t\t\tUi::Toast::Show(\"Could not open log :(\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto all = f.readAll();\n\t\t\tconst auto log = all.indexOf(\"Log: \");\n\t\t\tif (log < 0) {\n\t\t\t\tUi::Toast::Show(\"Could not find log :(\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto base = all.mid(log + 5);\n\t\t\tconst auto end = base.indexOf('\\n');\n\t\t\tif (end <= 0) {\n\t\t\t\tUi::Toast::Show(\"Could not find log end :(\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto based = QByteArray::fromBase64(base.mid(0, end));\n\t\t\tconst auto uncompressed = UnpackRawGzip(based);\n\t\t\tif (uncompressed.isEmpty()) {\n\t\t\t\tUi::Toast::Show(\"Could not unpack log :(\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tFileDialog::GetWritePath(Core::App().getFileDialogParent(), \"Save detailed log\", \"Crash dump (*.txt)\", QString(), [=](QString &&result) {\n\t\t\t\tif (result.isEmpty()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tauto f = QFile(result);\n\t\t\t\tif (!f.open(QIODevice::WriteOnly)) {\n\t\t\t\t\tUi::Toast::Show(\"Could not open details :(\");\n\t\t\t\t} else if (f.write(uncompressed) != uncompressed.size()) {\n\t\t\t\t\tUi::Toast::Show(\"Could not write details :(\");\n\t\t\t\t} else {\n\t\t\t\t\tf.close();\n\t\t\t\t\tUi::Toast::Show(\"Done!\");\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t});\n\tcodes.emplace(u\"testchatcolors\"_q, [](SessionController *window) {\n\t\tconst auto now = !Data::CloudThemes::TestingColors();\n\t\tData::CloudThemes::SetTestingColors(now);\n\t\tUi::Toast::Show(now ? \"Testing chat theme colors!\" : \"Not testing..\");\n\t});\n\n#ifdef Q_OS_MAC\n\tcodes.emplace(u\"customicon\"_q, [](SessionController *window) {\n\t\tconst auto iconFilters = u\"Icon files (*.icns *.png);;\"_q + FileDialog::AllFilesFilter();\n\t\tconst auto change = [](const QString &path) {\n\t\t\tconst auto success = path.isEmpty()\n\t\t\t\t? base::ClearCustomAppIcon()\n\t\t\t\t: base::SetCustomAppIcon(path);\n\t\t\tUi::Toast::Show(success\n\t\t\t\t? (path.isEmpty()\n\t\t\t\t\t? \"Icon cleared. Restarting the Dock.\"\n\t\t\t\t\t: \"Icon updated. Restarting the Dock.\")\n\t\t\t\t: (path.isEmpty()\n\t\t\t\t\t? \"Icon clear failed. See log.txt for details.\"\n\t\t\t\t\t: \"Icon update failed. See log.txt for details.\"));\n\t\t};\n\t\tFileDialog::GetOpenPath(Core::App().getFileDialogParent(), \"Choose custom icon\", iconFilters, [=](const FileDialog::OpenResult &result) {\n\t\t\tchange(result.paths.isEmpty() ? QString() : result.paths.front());\n\t\t}, [=] {\n\t\t\tchange(QString());\n\t\t});\n\t});\n#endif // Q_OS_MAC\n\n\treturn codes;\n}\n\n} // namespace\n\nvoid CodesFeedString(SessionController *window, const QString &text) {\n\tstatic const auto codes = GenerateCodes();\n\tstatic auto secret = QString();\n\n\tsecret += text.toLower();\n\tint size = secret.size(), from = 0;\n\twhile (size > from) {\n\t\tauto piece = base::StringViewMid(secret,from);\n\t\tauto found = false;\n\t\tfor (const auto &[key, method] : codes) {\n\t\t\tif (piece == key) {\n\t\t\t\tmethod(window);\n\t\t\t\tfrom = size;\n\t\t\t\tfound = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (found) break;\n\n\t\tfound = ranges::any_of(codes, [&](const auto &pair) {\n\t\t\treturn pair.first.startsWith(piece);\n\t\t});\n\t\tif (found) break;\n\n\t\t++from;\n\t}\n\tsecret = (size > from) ? secret.mid(from) : QString();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_codes.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Window {\nclass SessionController;\n} // namespace Main\n\nnamespace Settings {\n\nvoid CodesFeedString(Window::SessionController *window, const QString &text);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/settings_common.h\"\n\n#include \"base/timer.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/effects/premium_top_bar.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/widgets/elastic_scroll.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_widgets.h\"\n\n#include <QAction>\n\nnamespace Settings {\nnamespace {\n\n[[nodiscard]] HighlightArgs MaybeFillFromRipple(\n\t\tnot_null<QWidget*> target,\n\t\tHighlightArgs args) {\n\tif (!args.rippleShape) {\n\t\treturn args;\n\t}\n\tconst auto widget = target.get();\n\tif (const auto icon = dynamic_cast<Ui::IconButton*>(widget)) {\n\t\tconst auto &st = icon->st();\n\t\targs.shape = HighlightShape::Ellipse;\n\t\targs.margin = QMargins(\n\t\t\tst.rippleAreaPosition.x(),\n\t\t\tst.rippleAreaPosition.y(),\n\t\t\tst.width - st.rippleAreaPosition.x() - st.rippleAreaSize,\n\t\t\tst.height - st.rippleAreaPosition.y() - st.rippleAreaSize);\n\t} else if (const auto round = dynamic_cast<Ui::RoundButton*>(widget)) {\n\t\tconst auto &st = round->st();\n\t\targs.shape = HighlightShape::Rect;\n\t\targs.radius = st.radius ? st.radius : st::buttonRadius;\n\t}\n\treturn args;\n}\n\nclass HighlightOverlay final : public Ui::RpWidget {\npublic:\n\tHighlightOverlay(not_null<QWidget*> target, HighlightArgs &&args);\n\nprotected:\n\tbool eventFilter(QObject *o, QEvent *e) override;\n\nprivate:\n\tenum class Phase {\n\t\tWait,\n\t\tFadeIn,\n\t\tShown,\n\t\tFadeOut,\n\t};\n\n\tvoid updateGeometryFromTarget();\n\tvoid updateZOrder();\n\tvoid startFadeIn();\n\tvoid startFadeOut();\n\tvoid nextPhase();\n\tvoid nextPhaseIn(crl::time delay);\n\tvoid finish();\n\n\tconst QPointer<QWidget> _target;\n\tconst HighlightArgs _args;\n\tconst style::color &_color;\n\n\tUi::Animations::Simple _animation;\n\tbase::Timer _phaseTimer;\n\tPhase _phase = Phase::Wait;\n\tbool _finishing = false;\n\n};\n\nHighlightOverlay::HighlightOverlay(\n\tnot_null<QWidget*> target,\n\tHighlightArgs &&args)\n: RpWidget(target->parentWidget())\n, _target(target.get())\n, _args(MaybeFillFromRipple(target, std::move(args)))\n, _color(_args.color ? *_args.color : st::windowBgActive)\n, _phaseTimer([=] { nextPhase(); }) {\n\tsetAttribute(Qt::WA_TransparentForMouseEvents);\n\n\ttarget->installEventFilter(this);\n\n\tQObject::connect(\n\t\ttarget.get(),\n\t\t&QObject::destroyed,\n\t\tthis,\n\t\t[this] { finish(); });\n\n\tupdateGeometryFromTarget();\n\tupdateZOrder();\n\n\tpaintRequest() | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(this);\n\n\t\tconst auto progress = _animation.value(\n\t\t\t(_phase == Phase::Wait || _phase == Phase::FadeOut) ? 0. : 1.);\n\t\tconst auto alpha = _args.opacity * progress;\n\n\t\tauto color = _color->c;\n\t\tcolor.setAlphaF(color.alphaF() * alpha);\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(color);\n\n\t\tconst auto r = rect();\n\t\tswitch (_args.shape) {\n\t\tcase HighlightShape::Rect:\n\t\t\tif (_args.radius > 0) {\n\t\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\t\tp.drawRoundedRect(r, _args.radius, _args.radius);\n\t\t\t} else {\n\t\t\t\tp.drawRect(r);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase HighlightShape::Ellipse:\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(r);\n\t\t\tbreak;\n\t\t}\n\t}, lifetime());\n\n\thide();\n\tnextPhaseIn(_args.showDelay);\n}\n\nbool HighlightOverlay::eventFilter(QObject *o, QEvent *e) {\n\tif (o != _target.data()) {\n\t\treturn false;\n\t}\n\tswitch (e->type()) {\n\tcase QEvent::Move:\n\tcase QEvent::Resize:\n\t\tupdateGeometryFromTarget();\n\t\tbreak;\n\tcase QEvent::ZOrderChange:\n\t\tupdateZOrder();\n\t\tbreak;\n\tcase QEvent::Show:\n\tcase QEvent::Hide:\n\t\tsetVisible(!_target->isHidden());\n\t\tbreak;\n\tdefault:\n\t\tbreak;\n\t}\n\treturn false;\n}\n\nvoid HighlightOverlay::updateGeometryFromTarget() {\n\tif (_target) {\n\t\tsetGeometry(_target->geometry().marginsRemoved(_args.margin));\n\t}\n}\n\nvoid HighlightOverlay::updateZOrder() {\n\tif (!_target) {\n\t\treturn;\n\t}\n\tif (_args.below) {\n\t\tstackUnder(_target);\n\t} else {\n\t\tconst auto parent = _target->parentWidget();\n\t\tconst auto siblings = parent ? parent->children() : QObjectList();\n\t\tconst auto it = ranges::find(siblings, _target.data());\n\t\tconst auto next = (it != siblings.end()) ? (it + 1) : it;\n\t\tif (next != siblings.end()) {\n\t\t\tif (const auto widget = qobject_cast<QWidget*>(*next)) {\n\t\t\t\tstackUnder(widget);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\traise();\n\t}\n}\n\nvoid HighlightOverlay::startFadeIn() {\n\tshow();\n\n\t_phase = Phase::FadeIn;\n\t_animation.start([=] {\n\t\tupdate();\n\t\tif (!_animation.animating()) {\n\t\t\t_phase = Phase::Shown;\n\t\t\tnextPhaseIn(_args.shownDuration);\n\t\t}\n\t}, 0., 1., _args.showDuration, anim::easeOutCubic);\n}\n\nvoid HighlightOverlay::startFadeOut() {\n\t_phase = Phase::FadeOut;\n\t_animation.start([=] {\n\t\tupdate();\n\t\tif (!_animation.animating()) {\n\t\t\tfinish();\n\t\t}\n\t}, 1., 0., _args.hideDuration, anim::easeInCubic);\n}\n\nvoid HighlightOverlay::nextPhase() {\n\tswitch (_phase) {\n\tcase Phase::Wait:\n\t\tstartFadeIn();\n\t\tbreak;\n\tcase Phase::Shown:\n\t\tstartFadeOut();\n\t\tbreak;\n\tdefault:\n\t\tUnexpected(\"Next phase called during Fade phase.\");\n\t}\n}\n\nvoid HighlightOverlay::nextPhaseIn(crl::time delay) {\n\tif (!delay) {\n\t\t_phaseTimer.cancel();\n\t\tnextPhase();\n\t} else {\n\t\t_phaseTimer.callOnce(delay);\n\t}\n}\n\nvoid HighlightOverlay::finish() {\n\tif (_finishing) {\n\t\treturn;\n\t}\n\t_finishing = true;\n\t_phaseTimer.cancel();\n\t_animation.stop();\n\tdeleteLater();\n}\n\n} // namespace\n\nvoid HighlightWidget(QWidget *target, HighlightArgs &&args) {\n\tif (!target) {\n\t\treturn;\n\t}\n\tif (args.scroll) {\n\t\tScrollToWidget(target);\n\t}\n\tnew HighlightOverlay(target, std::move(args));\n}\n\nvoid ScrollToWidget(not_null<QWidget*> target) {\n\tconst auto scrollIn = [&](auto &&scroll) {\n\t\tif (const auto inner = scroll->widget()) {\n\t\t\tconst auto globalPosition = target->mapToGlobal(QPoint(0, 0));\n\t\t\tconst auto localPosition = inner->mapFromGlobal(globalPosition);\n\t\t\tconst auto localTop = localPosition.y();\n\t\t\tconst auto targetHeight = target->height();\n\t\t\tconst auto scrollHeight = scroll->height();\n\t\t\tconst auto centered = localTop - (scrollHeight - targetHeight) / 2;\n\t\t\tconst auto top = std::clamp(centered, 0, scroll->scrollTopMax());\n\t\t\tscroll->scrollToY(top);\n\t\t}\n\t};\n\tfor (auto parent = target->parentWidget()\n\t\t; parent\n\t\t; parent = parent->parentWidget()) {\n\t\tif (const auto scroll = dynamic_cast<Ui::ScrollArea*>(parent)) {\n\t\t\tscrollIn(scroll);\n\t\t\treturn;\n\t\t}\n\t\tif (const auto scroll = dynamic_cast<Ui::ElasticScroll*>(parent)) {\n\t\t\tscrollIn(scroll);\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nHighlightArgs SubsectionTitleHighlight() {\n\tconst auto radius = st::roundRadiusSmall;\n\treturn { .margin = { -radius, 0, -radius, 0 }, .radius = radius };\n}\n\nAbstractSection::AbstractSection(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: _controller(controller) {\n}\n\nvoid AbstractSection::build(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tSectionBuildMethod method) {\n\tmethod(\n\t\tcontainer,\n\t\t_controller,\n\t\tshowOtherMethod(),\n\t\t_showFinished.events());\n}\n\nIcon::Icon(IconDescriptor descriptor) : _icon(descriptor.icon) {\n\tconst auto background = [&]() -> const style::color* {\n\t\tif (descriptor.type == IconType::Simple) {\n\t\t\treturn nullptr;\n\t\t}\n\t\treturn descriptor.background;\n\t}();\n\tif (background) {\n\t\tconst auto radius = (descriptor.type == IconType::Rounded)\n\t\t\t? st::settingsIconRadius\n\t\t\t: (std::min(_icon->width(), _icon->height()) / 2);\n\t\t_background.emplace(radius, *background);\n\t} else if (const auto brush = descriptor.backgroundBrush) {\n\t\tconst auto radius = (descriptor.type == IconType::Rounded)\n\t\t\t? st::settingsIconRadius\n\t\t\t: (std::min(_icon->width(), _icon->height()) / 2);\n\t\t_backgroundBrush.emplace(radius, std::move(*brush));\n\t}\n}\n\nvoid Icon::paint(QPainter &p, QPoint position) const {\n\tpaint(p, position.x(), position.y());\n}\n\nvoid Icon::paint(QPainter &p, int x, int y) const {\n\tif (_background) {\n\t\t_background->paint(p, { { x, y }, _icon->size() });\n\t} else if (_backgroundBrush) {\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(_backgroundBrush->second);\n\t\tp.drawRoundedRect(\n\t\t\tQRect(QPoint(x, y), _icon->size()),\n\t\t\t_backgroundBrush->first,\n\t\t\t_backgroundBrush->first);\n\t}\n\t_icon->paint(p, { x, y }, 2 * x + _icon->width());\n}\n\nint Icon::width() const {\n\treturn _icon->width();\n}\n\nint Icon::height() const {\n\treturn _icon->height();\n}\n\nQSize Icon::size() const {\n\treturn _icon->size();\n}\n\nvoid AddButtonIcon(\n\t\tnot_null<Ui::AbstractButton*> button,\n\t\tconst style::SettingsButton &st,\n\t\tIconDescriptor &&descriptor) {\n\tExpects(descriptor.icon != nullptr);\n\n\tstruct IconWidget {\n\t\tIconWidget(QWidget *parent, IconDescriptor &&descriptor)\n\t\t: widget(parent)\n\t\t, icon(std::move(descriptor)) {\n\t\t}\n\t\tUi::RpWidget widget;\n\t\tIcon icon;\n\t};\n\tconst auto icon = button->lifetime().make_state<IconWidget>(\n\t\tbutton,\n\t\tstd::move(descriptor));\n\ticon->widget.setAttribute(Qt::WA_TransparentForMouseEvents);\n\ticon->widget.resize(icon->icon.size());\n\ticon->widget.show();\n\tbutton->sizeValue(\n\t) | rpl::on_next([=, left = st.iconLeft](QSize size) {\n\t\ticon->widget.moveToLeft(\n\t\t\tleft,\n\t\t\t(size.height() - icon->widget.height()) / 2,\n\t\t\tsize.width());\n\t}, icon->widget.lifetime());\n\ticon->widget.paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(&icon->widget);\n\t\ticon->icon.paint(p, 0, 0);\n\t}, icon->widget.lifetime());\n}\n\nobject_ptr<Button> CreateButtonWithIcon(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<QString> text,\n\t\tconst style::SettingsButton &st,\n\t\tIconDescriptor &&descriptor) {\n\tauto result = object_ptr<Button>(parent, std::move(text), st);\n\tconst auto button = result.data();\n\tif (descriptor) {\n\t\tAddButtonIcon(button, st, std::move(descriptor));\n\t}\n\treturn result;\n}\n\nnot_null<Button*> AddButtonWithIcon(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> text,\n\t\tconst style::SettingsButton &st,\n\t\tIconDescriptor &&descriptor) {\n\treturn container->add(\n\t\tCreateButtonWithIcon(\n\t\t\tcontainer,\n\t\t\tstd::move(text),\n\t\t\tst,\n\t\t\tstd::move(descriptor)));\n}\n\nvoid CreateRightLabel(\n\t\tnot_null<Button*> button,\n\t\tv::text::data &&label,\n\t\tconst style::SettingsButton &st,\n\t\trpl::producer<QString> buttonText,\n\t\tUi::Text::MarkedContext context) {\n\tconst auto name = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton.get(),\n\t\tst.rightLabel);\n\tname->show();\n\tif (v::text::is_plain(label)) {\n\t\trpl::combine(\n\t\t\tbutton->widthValue(),\n\t\t\tstd::move(buttonText),\n\t\t\tv::text::take_plain(std::move(label))\n\t\t) | rpl::on_next([=, &st](\n\t\t\t\tint width,\n\t\t\t\tconst QString &button,\n\t\t\t\tconst QString &text) {\n\t\t\tconst auto available = width\n\t\t\t\t- st.padding.left()\n\t\t\t\t- st.padding.right()\n\t\t\t\t- st.style.font->width(button)\n\t\t\t\t- st::settingsButtonRightSkip;\n\t\t\tname->setText(text);\n\t\t\tname->resizeToNaturalWidth(available);\n\t\t\tname->moveToRight(st::settingsButtonRightSkip, st.padding.top());\n\t\t}, name->lifetime());\n\t} else if (v::text::is_marked(label)) {\n\t\trpl::combine(\n\t\t\tbutton->widthValue(),\n\t\t\tstd::move(buttonText),\n\t\t\tv::text::take_marked(std::move(label))\n\t\t) | rpl::on_next([=, &st](\n\t\t\t\tint width,\n\t\t\t\tconst QString &button,\n\t\t\t\tconst TextWithEntities &text) {\n\t\t\tconst auto available = width\n\t\t\t\t- st.padding.left()\n\t\t\t\t- st.padding.right()\n\t\t\t\t- st.style.font->width(button)\n\t\t\t\t- st::settingsButtonRightSkip;\n\t\t\tname->setMarkedText(text, context);\n\t\t\tname->resizeToNaturalWidth(available);\n\t\t\tname->moveToRight(st::settingsButtonRightSkip, st.padding.top());\n\t\t}, name->lifetime());\n\t}\n\tname->setAttribute(Qt::WA_TransparentForMouseEvents);\n}\n\nnot_null<Button*> AddButtonWithLabel(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> text,\n\t\trpl::producer<QString> label,\n\t\tconst style::SettingsButton &st,\n\t\tIconDescriptor &&descriptor) {\n\tconst auto button = AddButtonWithIcon(\n\t\tcontainer,\n\t\trpl::duplicate(text),\n\t\tst,\n\t\tstd::move(descriptor));\n\tCreateRightLabel(button, std::move(label), st, std::move(text));\n\treturn button;\n}\n\nvoid AddDividerTextWithLottie(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tDividerWithLottieDescriptor &&descriptor) {\n\tconst auto divider = Ui::CreateChild<Ui::BoxContentDivider>(\n\t\tcontainer.get(),\n\t\t0,\n\t\tst::defaultDividerBar,\n\t\tdescriptor.parts);\n\tconst auto verticalLayout = container->add(\n\t\tobject_ptr<Ui::VerticalLayout>(container.get()));\n\tconst auto size = descriptor.lottieSize.value_or(\n\t\tst::settingsFilterIconSize);\n\tauto icon = CreateLottieIcon(\n\t\tverticalLayout,\n\t\t{\n\t\t\t.name = descriptor.lottie,\n\t\t\t.sizeOverride = { size, size },\n\t\t},\n\t\tdescriptor.lottieMargins.value_or(st::settingsFilterIconPadding));\n\tif (descriptor.showFinished) {\n\t\tconst auto repeat = descriptor.lottieRepeat.value_or(\n\t\t\tanim::repeat::once);\n\t\tstd::move(\n\t\t\tdescriptor.showFinished\n\t\t) | rpl::on_next([animate = std::move(icon.animate), repeat] {\n\t\t\tanimate(repeat);\n\t\t}, verticalLayout->lifetime());\n\t}\n\tverticalLayout->add(std::move(icon.widget));\n\n\tif (descriptor.about) {\n\t\tverticalLayout->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tverticalLayout,\n\t\t\t\tstd::move(descriptor.about),\n\t\t\t\tst::settingsFilterDividerLabel),\n\t\t\tdescriptor.aboutMargins.value_or(\n\t\t\t\tst::settingsFilterDividerLabelPadding),\n\t\t\tstyle::al_top)->setTryMakeSimilarLines(true);\n\t}\n\n\tverticalLayout->geometryValue(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tdivider->setGeometry(r);\n\t}, divider->lifetime());\n}\n\nLottieIcon CreateLottieIcon(\n\t\tnot_null<QWidget*> parent,\n\t\tLottie::IconDescriptor &&descriptor,\n\t\tstyle::margins padding,\n\t\tFn<QColor()> colorOverride) {\n\tExpects(!descriptor.frame); // I'm not sure it considers limitFps.\n\n\tdescriptor.limitFps = true;\n\n\tauto object = object_ptr<Ui::RpWidget>(parent);\n\tconst auto raw = object.data();\n\n\tconst auto width = descriptor.sizeOverride.width();\n\traw->resize(QRect(\n\t\tQPoint(),\n\t\tdescriptor.sizeOverride).marginsAdded(padding).size());\n\n\tauto owned = Lottie::MakeIcon(std::move(descriptor));\n\tconst auto icon = owned.get();\n\n\traw->lifetime().add([kept = std::move(owned)]{});\n\tconst auto looped = raw->lifetime().make_state<bool>(true);\n\n\tconst auto start = [=] {\n\t\ticon->animate([=] {\n\t\t\traw->update();\n\t\t}, 0, icon->framesCount() - 1);\n\t};\n\tconst auto animate = [=](anim::repeat repeat) {\n\t\t*looped = (repeat == anim::repeat::loop);\n\t\tstart();\n\t};\n\traw->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\tconst auto left = (raw->width() - width) / 2;\n\t\ticon->paint(p, left, padding.top(), colorOverride\n\t\t\t? colorOverride()\n\t\t\t: std::optional<QColor>());\n\t\tif (!icon->animating() && icon->frameIndex() > 0 && *looped) {\n\t\t\tstart();\n\t\t}\n\n\t}, raw->lifetime());\n\n\treturn { .widget = std::move(object), .animate = std::move(animate) };\n}\n\nSliderWithLabel MakeSliderWithLabel(\n\t\tQWidget *parent,\n\t\tconst style::MediaSlider &sliderSt,\n\t\tconst style::FlatLabel &labelSt,\n\t\tint skip,\n\t\tint minLabelWidth,\n\t\tbool ignoreWheel) {\n\tauto result = object_ptr<Ui::RpWidget>(parent);\n\tconst auto raw = result.data();\n\tconst auto height = std::max(\n\t\tsliderSt.seekSize.height(),\n\t\tlabelSt.style.font->height);\n\traw->resize(sliderSt.seekSize.width(), height);\n\tconst auto slider = ignoreWheel\n\t\t? Ui::CreateChild<Ui::MediaSliderWheelless>(raw, sliderSt)\n\t\t: Ui::CreateChild<Ui::MediaSlider>(raw, sliderSt);\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(raw, labelSt);\n\tslider->resize(slider->width(), sliderSt.seekSize.height());\n\trpl::combine(\n\t\traw->sizeValue(),\n\t\tlabel->sizeValue()\n\t) | rpl::on_next([=](QSize outer, QSize size) {\n\t\tconst auto right = std::max(size.width(), minLabelWidth) + skip;\n\t\tlabel->moveToRight(0, (outer.height() - size.height()) / 2);\n\t\tconst auto width = std::max(\n\t\t\tsliderSt.seekSize.width(),\n\t\t\touter.width() - right);\n\t\tslider->resizeToWidth(width);\n\t\tslider->moveToLeft(0, (outer.height() - slider->height()) / 2);\n\t}, label->lifetime());\n\treturn {\n\t\t.widget = std::move(result),\n\t\t.slider = slider,\n\t\t.label = label,\n\t};\n}\n\nvoid AddLottieIconWithCircle(\n\t\tnot_null<Ui::VerticalLayout*> layout,\n\t\tobject_ptr<Ui::RpWidget> icon,\n\t\tQMargins iconPadding,\n\t\tQSize circleSize) {\n\tconst auto iconRow = layout->add(\n\t\tstd::move(icon),\n\t\ticonPadding,\n\t\tstyle::al_top);\n\n\tconst auto circle = Ui::CreateChild<Ui::RpWidget>(\n\t\ticonRow->parentWidget());\n\tcircle->lower();\n\tcircle->paintOn([=](QPainter &p) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto left = (circle->width() - circleSize.width()) / 2;\n\t\tconst auto top = (circle->height() - circleSize.height()) / 2;\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::activeButtonBg);\n\t\tp.drawEllipse(QRect(QPoint(left, top), circleSize));\n\t});\n\n\ticonRow->geometryValue() | rpl::on_next([=](const QRect &g) {\n\t\tcircle->setGeometry(g);\n\t}, circle->lifetime());\n}\n\nvoid AddPremiumStar(\n\t\tnot_null<Button*> button,\n\t\tbool credits,\n\t\tFn<bool()> isPaused) {\n\tconst auto stops = credits\n\t\t? Ui::Premium::CreditsIconGradientStops()\n\t\t: Ui::Premium::ButtonGradientStops();\n\n\tconst auto ministarsContainer = Ui::CreateChild<Ui::RpWidget>(button);\n\tconst auto &buttonSt = button->st();\n\tconst auto fullHeight = buttonSt.height\n\t\t+ rect::m::sum::v(buttonSt.padding);\n\tusing MiniStars = Ui::Premium::ColoredMiniStars;\n\tconst auto ministars = button->lifetime().make_state<MiniStars>(\n\t\tministarsContainer,\n\t\tfalse);\n\tministars->setColorOverride(stops);\n\n\tconst auto isPausedValue\n\t\t= button->lifetime().make_state<rpl::variable<bool>>(isPaused());\n\tisPausedValue->value() | rpl::on_next([=](bool value) {\n\t\tministars->setPaused(value);\n\t}, ministarsContainer->lifetime());\n\n\tministarsContainer->paintRequest(\n\t) | rpl::on_next([=] {\n\t\t(*isPausedValue) = isPaused();\n\t\tauto p = QPainter(ministarsContainer);\n\t\t{\n\t\t\tconstexpr auto kScale = 0.35;\n\t\t\tconst auto r = ministarsContainer->rect();\n\t\t\tp.translate(r.center());\n\t\t\tp.scale(kScale, kScale);\n\t\t\tp.translate(-r.center());\n\t\t}\n\t\tministars->paint(p);\n\t}, ministarsContainer->lifetime());\n\n\tconst auto badge = Ui::CreateChild<Ui::RpWidget>(button.get());\n\n\tauto star = [&] {\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tconst auto size = Size(st::settingsButtonNoIcon.style.font->ascent);\n\t\tauto image = QImage(\n\t\t\tsize * factor,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\timage.setDevicePixelRatio(factor);\n\t\timage.fill(Qt::transparent);\n\t\t{\n\t\t\tauto p = QPainter(&image);\n\t\t\tauto star = QSvgRenderer(Ui::Premium::ColorizedSvg(stops));\n\t\t\tstar.render(&p, Rect(size));\n\t\t}\n\t\treturn image;\n\t}();\n\tbadge->resize(star.size() / style::DevicePixelRatio());\n\tbadge->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(badge);\n\t\tp.drawImage(0, 0, star);\n\t}, badge->lifetime());\n\n\tbutton->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tbadge->moveToLeft(\n\t\t\tbutton->st().iconLeft\n\t\t\t\t+ (st::menuIconShop.width() - badge->width()) / 2,\n\t\t\t(s.height() - badge->height()) / 2);\n\t\tministarsContainer->moveToLeft(\n\t\t\tbadge->x() - (fullHeight - badge->height()) / 2,\n\t\t\t0);\n\t}, badge->lifetime());\n\n\tministarsContainer->resize(fullHeight, fullHeight);\n\tministars->setCenter(ministarsContainer->rect());\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/text/text_variant.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/round_rect.h\"\n#include \"base/object_ptr.h\"\n#include \"settings/settings_type.h\"\n\n#include <any>\n\nnamespace anim {\nenum class repeat : uchar;\n} // namespace anim\n\nnamespace Info {\nstruct SelectedItems;\nenum class SelectionAction;\n} // namespace Info\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass VerticalLayout;\nclass FlatLabel;\nclass SettingsButton;\nclass AbstractButton;\nclass MediaSlider;\n} // namespace Ui\n\nnamespace Ui::Menu {\nstruct MenuCallback;\n} // namespace Ui::Menu\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace style {\nstruct FlatLabel;\nstruct SettingsButton;\nstruct MediaSlider;\n} // namespace style\n\nnamespace Lottie {\nstruct IconDescriptor;\n} // namespace Lottie\n\nnamespace Settings {\n\nusing Button = Ui::SettingsButton;\n\nenum class HighlightShape {\n\tRect,\n\tEllipse,\n};\n\nstruct HighlightArgs {\n\tstyle::margins margin;\n\tHighlightShape shape = HighlightShape::Rect;\n\tint radius = 0;\n\tconst style::color *color = nullptr;\n\tfloat64 opacity = 0.4;\n\tbool below = false;\n\tbool rippleShape = false;\n\tbool scroll = true;\n\tcrl::time showDelay = 400;\n\tcrl::time showDuration = 600;\n\tcrl::time shownDuration = 400;\n\tcrl::time hideDuration = 600;\n};\n\nvoid HighlightWidget(QWidget *target, HighlightArgs &&args = {});\nvoid ScrollToWidget(not_null<QWidget*> target);\n\n[[nodiscard]] HighlightArgs SubsectionTitleHighlight();\n\nstruct HighlightEntry {\n\tQPointer<QWidget> widget;\n\tHighlightArgs args;\n};\n\nusing HighlightRegistry = std::vector<std::pair<QString, HighlightEntry>>;\n\nusing SectionBuildMethod = Fn<void(\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<Window::SessionController*> controller,\n\tFn<void(Type)> showOther,\n\trpl::producer<> showFinished)>;\n\nclass AbstractSection : public Ui::RpWidget {\npublic:\n\tAbstractSection(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] not_null<Window::SessionController*> controller() const {\n\t\treturn _controller;\n\t}\n\n\t[[nodiscard]] virtual Type id() const = 0;\n\t[[nodiscard]] virtual rpl::producer<Type> sectionShowOther() {\n\t\treturn _showOtherRequests.events();\n\t}\n\t[[nodiscard]] virtual rpl::producer<> sectionShowBack() {\n\t\treturn nullptr;\n\t}\n\t[[nodiscard]] virtual rpl::producer<std::vector<Type>> removeFromStack() {\n\t\treturn nullptr;\n\t}\n\t[[nodiscard]] virtual bool closeByOutsideClick() const {\n\t\treturn true;\n\t}\n\tvirtual void checkBeforeClose(Fn<void()> close) {\n\t\tclose();\n\t}\n\t[[nodiscard]] virtual rpl::producer<QString> title() = 0;\n\tvirtual void sectionSaveChanges(FnMut<void()> done) {\n\t\tdone();\n\t}\n\tvirtual void showFinished() {\n\t\t_showFinished.fire({});\n\t}\n\tvirtual void setInnerFocus() {\n\t\tsetFocus();\n\t}\n\t[[nodiscard]] virtual const Ui::RoundRect *bottomSkipRounding() const {\n\t\treturn nullptr;\n\t}\n\t[[nodiscard]] virtual base::weak_qptr<Ui::RpWidget> createPinnedToTop(\n\t\t\tnot_null<QWidget*> parent) {\n\t\treturn nullptr;\n\t}\n\t[[nodiscard]] virtual base::weak_qptr<Ui::RpWidget> createPinnedToBottom(\n\t\t\tnot_null<Ui::RpWidget*> parent) {\n\t\treturn nullptr;\n\t}\n\t[[nodiscard]] virtual bool hasFlexibleTopBar() const {\n\t\treturn false;\n\t}\n\tvirtual void setStepDataReference(std::any &data) {\n\t}\n\tvirtual void sectionSaveState(std::any &state) {\n\t}\n\tvirtual void sectionRestoreState(const std::any &state) {\n\t}\n\n\t[[nodiscard]] virtual auto selectedListValue()\n\t-> rpl::producer<Info::SelectedItems> {\n\t\treturn nullptr;\n\t}\n\tvirtual void selectionAction(Info::SelectionAction action) {\n\t}\n\tvirtual void fillTopBarMenu(\n\t\tconst Ui::Menu::MenuCallback &addAction) {\n\t}\n\n\tvirtual bool paintOuter(\n\t\t\tnot_null<QWidget*> outer,\n\t\t\tint maxVisibleHeight,\n\t\t\tQRect clip) {\n\t\treturn false;\n\t}\n\n\t[[nodiscard]] rpl::producer<> showFinishes() const {\n\t\treturn _showFinished.events();\n\t}\n\n\tvoid showOther(Type type) {\n\t\t_showOtherRequests.fire_copy(type);\n\t}\n\t[[nodiscard]] Fn<void(Type)> showOtherMethod() {\n\t\treturn crl::guard(this, [=](Type type) {\n\t\t\tshowOther(type);\n\t\t});\n\t}\n\nprotected:\n\tvoid build(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tSectionBuildMethod method);\n\nprivate:\n\tconst not_null<Window::SessionController*> _controller;\n\trpl::event_stream<Type> _showOtherRequests;\n\trpl::event_stream<> _showFinished;\n\n};\n\nenum class IconType {\n\tRounded,\n\tRound,\n\tSimple,\n};\n\nstruct IconDescriptor {\n\tconst style::icon *icon = nullptr;\n\tIconType type = IconType::Rounded;\n\tconst style::color *background = nullptr;\n\tstd::optional<QBrush> backgroundBrush; // Can be useful for gradients.\n\tbool newBadge = false;\n\n\texplicit operator bool() const {\n\t\treturn (icon != nullptr);\n\t}\n};\n\nclass Icon final {\npublic:\n\texplicit Icon(IconDescriptor descriptor);\n\n\tvoid paint(QPainter &p, QPoint position) const;\n\tvoid paint(QPainter &p, int x, int y) const;\n\n\t[[nodiscard]] int width() const;\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] QSize size() const;\n\nprivate:\n\tnot_null<const style::icon*> _icon;\n\tstd::optional<Ui::RoundRect> _background;\n\tstd::optional<std::pair<int, QBrush>> _backgroundBrush;\n\n};\n\nvoid AddButtonIcon(\n\tnot_null<Ui::AbstractButton*> button,\n\tconst style::SettingsButton &st,\n\tIconDescriptor &&descriptor);\nobject_ptr<Button> CreateButtonWithIcon(\n\tnot_null<QWidget*> parent,\n\trpl::producer<QString> text,\n\tconst style::SettingsButton &st,\n\tIconDescriptor &&descriptor = {});\nnot_null<Button*> AddButtonWithIcon(\n\tnot_null<Ui::VerticalLayout*> container,\n\trpl::producer<QString> text,\n\tconst style::SettingsButton &st,\n\tIconDescriptor &&descriptor = {});\nnot_null<Button*> AddButtonWithLabel(\n\tnot_null<Ui::VerticalLayout*> container,\n\trpl::producer<QString> text,\n\trpl::producer<QString> label,\n\tconst style::SettingsButton &st,\n\tIconDescriptor &&descriptor = {});\nvoid CreateRightLabel(\n\tnot_null<Button*> button,\n\tv::text::data &&label,\n\tconst style::SettingsButton &st,\n\trpl::producer<QString> buttonText,\n\tUi::Text::MarkedContext context = {});\n\nstruct DividerWithLottieDescriptor {\n\tQString lottie;\n\tstd::optional<anim::repeat> lottieRepeat;\n\tstd::optional<int> lottieSize;\n\tstd::optional<QMargins> lottieMargins;\n\trpl::producer<> showFinished;\n\trpl::producer<TextWithEntities> about;\n\tstd::optional<QMargins> aboutMargins;\n\tRectParts parts = RectPart::Top | RectPart::Bottom;\n};\nvoid AddDividerTextWithLottie(\n\tnot_null<Ui::VerticalLayout*> container,\n\tDividerWithLottieDescriptor &&descriptor);\n\nstruct LottieIcon {\n\tobject_ptr<Ui::RpWidget> widget;\n\tFn<void(anim::repeat repeat)> animate;\n};\n[[nodiscard]] LottieIcon CreateLottieIcon(\n\tnot_null<QWidget*> parent,\n\tLottie::IconDescriptor &&descriptor,\n\tstyle::margins padding = {},\n\tFn<QColor()> colorOverride = nullptr);\n\nstruct SliderWithLabel {\n\tobject_ptr<Ui::RpWidget> widget;\n\tnot_null<Ui::MediaSlider*> slider;\n\tnot_null<Ui::FlatLabel*> label;\n};\n[[nodiscard]] SliderWithLabel MakeSliderWithLabel(\n\tQWidget *parent,\n\tconst style::MediaSlider &sliderSt,\n\tconst style::FlatLabel &labelSt,\n\tint skip,\n\tint minLabelWidth = 0,\n\tbool ignoreWheel = false);\n\nvoid AddLottieIconWithCircle(\n\tnot_null<Ui::VerticalLayout*>,\n\tobject_ptr<Ui::RpWidget> icon,\n\tQMargins iconPadding,\n\tQSize circleSize);\n\nvoid AddPremiumStar(\n\tnot_null<Button*> button,\n\tbool credits,\n\tFn<bool()> isPaused);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_common_session.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/settings_common_session.h\"\n\n#include \"settings/cloud_password/settings_cloud_password_email_confirm.h\"\n#include \"settings/settings_experimental.h\"\n#include \"settings/sections/settings_chat.h\"\n#include \"settings/sections/settings_main.h\"\n\nnamespace Settings {\n\nbool HasMenu(Type type) {\n\treturn (type == ::Settings::CloudPasswordEmailConfirmId())\n\t\t|| (type == MainId())\n\t\t|| (type == ChatId())\n\t\t|| (type == Experimental::Id());\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_common_session.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_common.h\"\n#include \"ui/rp_widget.h\"\n#include \"base/object_ptr.h\"\n#include \"settings/settings_type.h\"\n\nnamespace Ui {\nclass ScrollArea;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Ui::Menu {\nstruct MenuCallback;\n} // namespace Ui::Menu\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Settings {\n\nenum class Container {\n\tSection,\n\tLayer,\n};\n\nclass AbstractSection;\n\nstruct AbstractSectionFactory {\n\t[[nodiscard]] virtual object_ptr<AbstractSection> create(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\trpl::producer<Container> containerValue) const = 0;\n\t[[nodiscard]] virtual bool hasCustomTopBar() const {\n\t\treturn false;\n\t}\n\n\tvirtual ~AbstractSectionFactory() = default;\n};\n\ntemplate <typename SectionType>\nstruct SectionFactory : AbstractSectionFactory {\n\tobject_ptr<AbstractSection> create(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\trpl::producer<Container> containerValue\n\t) const final override {\n\t\treturn object_ptr<SectionType>(parent, controller);\n\t}\n\n\t[[nodiscard]] static const std::shared_ptr<SectionFactory> &Instance() {\n\t\tstatic const auto result = std::make_shared<SectionFactory>();\n\t\treturn result;\n\t}\n\n};\n\ntemplate <typename SectionType>\nclass Section : public AbstractSection {\npublic:\n\tusing AbstractSection::AbstractSection;\n\n\t[[nodiscard]] static Type Id() {\n\t\treturn SectionFactory<SectionType>::Instance();\n\t}\n\t[[nodiscard]] Type id() const final override {\n\t\treturn Id();\n\t}\n\n};\n\nbool HasMenu(Type type);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_credits_graphics.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/settings_credits_graphics.h\"\n\n#include \"api/api_chat_invite.h\"\n#include \"api/api_credits.h\"\n#include \"api/api_earn.h\"\n#include \"api/api_premium.h\"\n#include \"apiwrap.h\"\n#include \"base/random.h\"\n#include \"base/timer_rpl.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/gift_premium_box.h\"\n#include \"boxes/share_box.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"boxes/star_gift_craft_box.h\"\n#include \"boxes/star_gift_resale_box.h\"\n#include \"boxes/transfer_gift_box.h\"\n#include \"chat_helpers/stickers_gift_box_pack.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/click_handler_types.h\" // UrlClickHandler\n#include \"core/ui_integration.h\"\n#include \"data/components/credits.h\"\n#include \"data/components/recent_shared_media_gifts.h\"\n#include \"data/data_boosts.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_subscriptions.h\"\n#include \"data/data_user.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/history_item_components.h\" // HistoryServicePaymentRefund.\n#include \"info/bot/starref/info_bot_starref_common.h\"\n#include \"info/channel_statistics/boosts/giveaway/boost_badge.h\" // InfiniteRadialAnimationWidget.\n#include \"info/channel_statistics/earn/info_channel_earn_widget.h\" // Info::ChannelEarn::Make.\n#include \"info/channel_statistics/earn/earn_format.h\"\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"info/peer_gifts/info_peer_gifts_common.h\"\n#include \"info/peer_gifts/info_peer_gifts_widget.h\"\n#include \"info/settings/info_settings_widget.h\" // SectionCustomTopBarData.\n#include \"info/statistics/info_statistics_list_controllers.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"iv/iv_instance.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"payments/payments_checkout_process.h\"\n#include \"payments/payments_form.h\"\n#include \"payments/payments_non_panel_process.h\"\n#include \"settings/settings_common_session.h\"\n#include \"settings/sections/settings_credits.h\"\n#include \"statistics/widgets/chart_header_widget.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/controls/button_labels.h\"\n#include \"ui/controls/ton_common.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/effects/loading_element.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/effects/premium_stars_colored.h\"\n#include \"ui/effects/premium_top_bar.h\"\n#include \"ui/effects/toggle_arrow.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/discrete_sliders.h\"\n#include \"ui/widgets/fields/number_input.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_channel_earn.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_media_view.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_statistics.h\"\n\n#include <xxhash.h> // XXH64.\n\n#include <QtSvg/QSvgRenderer>\n\nnamespace Settings {\nnamespace {\n\nconst auto kTopUpPrefix = \"cloud_lng_topup_purpose_\";\n\n[[nodiscard]] uint64 UniqueIdFromOption(\n\t\tconst Data::CreditTopupOption &d) {\n\tconst auto string = QString::number(d.credits)\n\t\t+ d.product\n\t\t+ d.currency\n\t\t+ QString::number(d.amount);\n\n\treturn XXH64(string.data(), string.size() * sizeof(ushort), 0);\n}\n\n[[nodiscard]] int WithdrawalMin(not_null<Main::Session*> session) {\n\tconst auto key = u\"stars_revenue_withdrawal_min\"_q;\n\treturn session->appConfig().get<int>(key, 1000);\n}\n\n[[nodiscard]] rpl::producer<TextWithEntities> DeepLinkBalanceAbout(\n\t\tconst QString &purpose) {\n\tconst auto phrase = Lang::GetNonDefaultValue(\n\t\tkTopUpPrefix + purpose.toUtf8());\n\treturn phrase.isEmpty()\n\t\t? tr::lng_credits_small_balance_fallback(tr::rich)\n\t\t: rpl::single(tr::rich(phrase));\n}\n\nclass Balance final\n\t: public Ui::RpWidget\n\t, public Ui::AbstractTooltipShower {\npublic:\n\tusing Ui::RpWidget::RpWidget;\n\n\tvoid setBalance(CreditsAmount balance) {\n\t\t_balance = balance;\n\t\t_tooltip = Lang::FormatCreditsAmountDecimal(balance);\n\t}\n\n\tvoid enterEventHook(QEnterEvent *e) override {\n\t\tif (_balance >= CreditsAmount(10'000)) {\n\t\t\tUi::Tooltip::Show(1000, this);\n\t\t}\n\t}\n\n\tvoid leaveEventHook(QEvent *e) override {\n\t\tUi::Tooltip::Hide();\n\t}\n\n\tQString tooltipText() const override {\n\t\treturn _tooltip;\n\t}\n\n\tQPoint tooltipPos() const override {\n\t\treturn QCursor::pos();\n\t}\n\n\tbool tooltipWindowActive() const override {\n\t\treturn Ui::AppInFocus() && Ui::InFocusChain(window());\n\t}\n\nprivate:\n\tQString _tooltip;\n\tCreditsAmount _balance;\n\n};\n\nvoid ToggleStarGiftSaved(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tData::SavedStarGiftId savedId,\n\t\tbool save,\n\t\tFn<void(bool)> done = nullptr) {\n\tusing Flag = MTPpayments_SaveStarGift::Flag;\n\tconst auto api = &show->session().api();\n\tconst auto channelGift = savedId.chat();\n\tapi->request(MTPpayments_SaveStarGift(\n\t\tMTP_flags(save ? Flag(0) : Flag::f_unsave),\n\t\tApi::InputSavedStarGiftId(savedId)\n\t)).done([=] {\n\t\tusing GiftAction = Data::GiftUpdate::Action;\n\t\tshow->session().data().notifyGiftUpdate({\n\t\t\t.id = savedId,\n\t\t\t.action = (save ? GiftAction::Save : GiftAction::Unsave),\n\t\t});\n\n\t\tif (const auto onstack = done) {\n\t\t\tonstack(true);\n\t\t}\n\t\tshow->showToast((save\n\t\t\t? (channelGift\n\t\t\t\t? tr::lng_gift_display_done_channel\n\t\t\t\t: tr::lng_gift_display_done)\n\t\t\t: (channelGift\n\t\t\t\t? tr::lng_gift_display_done_hide_channel\n\t\t\t\t: tr::lng_gift_display_done_hide))(tr::now));\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (const auto onstack = done) {\n\t\t\tonstack(false);\n\t\t}\n\t\tif (!Ui::ShowGiftErrorToast(show, error)) {\n\t\t\tshow->showToast(error.type());\n\t\t}\n\t}).send();\n}\n\nvoid ConfirmConvertStarGift(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\trpl::producer<TextWithEntities> confirmText,\n\t\tint stars,\n\t\tint daysLeft,\n\t\tFn<void()> convert) {\n\tauto text = rpl::combine(\n\t\tstd::move(confirmText),\n\t\ttr::lng_gift_convert_sure_limit(\n\t\t\tlt_count,\n\t\t\trpl::single(daysLeft * 1.),\n\t\t\ttr::rich),\n\t\ttr::lng_gift_convert_sure_caution(tr::rich)\n\t) | rpl::map([](\n\t\t\tTextWithEntities &&a,\n\t\t\tTextWithEntities &&b,\n\t\t\tTextWithEntities &&c) {\n\t\treturn a.append(\"\\n\\n\").append(b).append(\"\\n\\n\").append(c);\n\t});\n\tshow->show(Ui::MakeConfirmBox({\n\t\t.text = std::move(text),\n\t\t.confirmed = [=](Fn<void()> close) { close(); convert(); },\n\t\t.confirmText = tr::lng_gift_convert_sure(),\n\t\t.title = tr::lng_gift_convert_sure_title(),\n\t}));\n}\n\nvoid ConvertStarGift(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tData::SavedStarGiftId savedId,\n\t\tint stars,\n\t\tFn<void(bool)> done) {\n\tconst auto api = &show->session().api();\n\tapi->request(MTPpayments_ConvertStarGift(\n\t\tApi::InputSavedStarGiftId(savedId)\n\t)).done([=] {\n\t\tif (const auto window = show->resolveWindow()) {\n\t\t\tif (const auto channel = savedId.chat()) {\n\t\t\t\twindow->showSection(Info::ChannelEarn::Make(channel));\n\t\t\t} else {\n\t\t\t\twindow->showSettings(Settings::CreditsId());\n\t\t\t}\n\t\t}\n\t\tshow->showToast((savedId.chat()\n\t\t\t? tr::lng_gift_channel_got\n\t\t\t: tr::lng_gift_got_stars)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count,\n\t\t\t\tstars,\n\t\t\t\ttr::rich));\n\t\tdone(true);\n\t}).fail([=](const MTP::Error &error) {\n\t\tif (!Ui::ShowGiftErrorToast(show, error)) {\n\t\t\tshow->showToast(error.type());\n\t\t}\n\t\tdone(false);\n\t}).send();\n}\n\nvoid AddViewMediaHandler(\n\t\tnot_null<Ui::RpWidget*> thumb,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::CreditsHistoryEntry &e) {\n\tif (e.extended.empty()) {\n\t\treturn;\n\t}\n\tthumb->setCursor(style::cur_pointer);\n\n\tstruct State {\n\t\t~State() {\n\t\t\tif (item) {\n\t\t\t\titem->destroy();\n\t\t\t}\n\t\t}\n\n\t\tHistoryItem *item = nullptr;\n\t\tbool pressed = false;\n\t\tbool over = false;\n\t};\n\tconst auto state = thumb->lifetime().make_state<State>();\n\tconst auto session = &show->session();\n\tconst auto owner = &session->data();\n\tconst auto peerId = e.barePeerId\n\t\t? PeerId(e.barePeerId)\n\t\t: session->userPeerId();\n\tconst auto history = owner->history(session->user());\n\tstate->item = history->makeMessage({\n\t\t.id = history->nextNonHistoryEntryId(),\n\t\t.flags = MessageFlag::HasFromId | MessageFlag::AdminLogEntry,\n\t\t.from = peerId,\n\t\t.date = base::unixtime::serialize(e.date),\n\t}, TextWithEntities(), MTP_messageMediaEmpty());\n\tauto fake = std::vector<std::unique_ptr<Data::Media>>();\n\tfake.reserve(e.extended.size());\n\tfor (const auto &item : e.extended) {\n\t\tif (item.type == Data::CreditsHistoryMediaType::Photo) {\n\t\t\tfake.push_back(std::make_unique<Data::MediaPhoto>(\n\t\t\t\tstate->item,\n\t\t\t\towner->photo(item.id),\n\t\t\t\tfalse)); // spoiler\n\t\t} else {\n\t\t\tconst auto document = owner->document(item.id);\n\t\t\tconst auto item = state->item;\n\t\t\tusing MediaFile = Data::MediaFile;\n\t\t\tusing Args = MediaFile::Args;\n\t\t\tfake.push_back(std::make_unique<MediaFile>(item, document, Args{\n\t\t\t\t.skipPremiumEffect = true,\n\t\t\t}));\n\t\t}\n\t}\n\tstate->item->overrideMedia(std::make_unique<Data::MediaInvoice>(\n\t\tstate->item,\n\t\tData::Invoice{\n\t\t\t.amount = uint64(e.credits.abs().whole()),\n\t\t\t.currency = Ui::kCreditsCurrency,\n\t\t\t.extendedMedia = std::move(fake),\n\t\t\t.isPaidMedia = true,\n\t\t}));\n\tconst auto showMedia = [=] {\n\t\tconst auto window = show->resolveWindow();\n\t\tif (!window) {\n\t\t\treturn;\n\t\t} else if (const auto media = state->item->media()) {\n\t\t\tif (const auto invoice = media->invoice()) {\n\t\t\t\tif (!invoice->extendedMedia.empty()) {\n\t\t\t\t\tconst auto first = invoice->extendedMedia[0].get();\n\t\t\t\t\tif (const auto photo = first->photo()) {\n\t\t\t\t\t\twindow->openPhoto(photo, {\n\t\t\t\t\t\t\t.id = state->item->fullId(),\n\t\t\t\t\t\t});\n\t\t\t\t\t} else if (const auto document = first->document()) {\n\t\t\t\t\t\twindow->openDocument(document, true, {\n\t\t\t\t\t\t\t.id = state->item->fullId(),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\tthumb->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::MouseButtonPress) {\n\t\t\tconst auto mouse = static_cast<QMouseEvent*>(e.get());\n\t\t\tif (mouse->button() == Qt::LeftButton) {\n\t\t\t\tstate->over = true;\n\t\t\t\tstate->pressed = true;\n\t\t\t}\n\t\t} else if (e->type() == QEvent::MouseButtonRelease\n\t\t\t&& state->over\n\t\t\t&& state->pressed) {\n\t\t\tshowMedia();\n\t\t} else if (e->type() == QEvent::Enter) {\n\t\t\tstate->over = true;\n\t\t} else if (e->type() == QEvent::Leave) {\n\t\t\tstate->over = false;\n\t\t}\n\t}, thumb->lifetime());\n}\n\n} // namespace\n\nvoid AddMiniStars(\n\t\tnot_null<Ui::VerticalLayout*> content,\n\t\tnot_null<Ui::RpWidget*> widget,\n\t\tint photoSize,\n\t\tint boxWidth,\n\t\tfloat64 heightRatio) {\n\tusing ColoredMiniStars = Ui::Premium::ColoredMiniStars;\n\tconst auto stars = widget->lifetime().make_state<ColoredMiniStars>(\n\t\twidget,\n\t\tfalse,\n\t\tUi::Premium::MiniStarsType::BiStars);\n\tstars->setColorOverride(Ui::Premium::CreditsIconGradientStops());\n\twidget->resize(boxWidth - photoSize, photoSize * heightRatio);\n\tcontent->sizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\twidget->moveToLeft(photoSize / 2, 0);\n\t\tconst auto starsRect = Rect(widget->size());\n\t\tstars->setPosition(starsRect.topLeft());\n\t\tstars->setSize(starsRect.size());\n\t\twidget->lower();\n\t}, widget->lifetime());\n\twidget->paintRequest(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tauto p = QPainter(widget);\n\t\tp.fillRect(r, Qt::transparent);\n\t\tstars->paint(p);\n\t}, widget->lifetime());\n}\n\nSubscriptionRightLabel PaintSubscriptionRightLabelCallback(\n\t\tnot_null<Main::Session*> session,\n\t\tconst style::PeerListItem &st,\n\t\tint amount) {\n\tauto helper = Ui::Text::CustomEmojiHelper();\n\tauto starIcon = helper.paletteDependent(\n\t\tUi::Earn::IconCreditsEmoji());\n\tconst auto text = std::make_shared<Ui::Text::String>();\n\ttext->setMarkedText(\n\t\tst::semiboldTextStyle,\n\t\tstarIcon.append(' ').append(Lang::FormatCountDecimal(amount)),\n\t\tkMarkupTextOptions,\n\t\thelper.context());\n\tconst auto &font = text->style()->font;\n\tconst auto &statusFont = st::contactsStatusFont;\n\tconst auto status = tr::lng_group_invite_joined_right(tr::now);\n\tconst auto rightSkip = st::boxRowPadding.right();\n\tconst auto statusWidth = statusFont->width(status);\n\tconst auto size = QSize(\n\t\tstd::max(text->maxWidth(), statusWidth) + rightSkip,\n\t\tfont->height + statusFont->height);\n\tconst auto statusX = size.width() - statusWidth;\n\tauto draw = [=](QPainter &p, int x, int y, int h) {\n\t\tp.setPen(st.statusFg);\n\t\tp.setFont(statusFont);\n\t\tconst auto skip = y + (h - size.height()) / 2;\n\t\tp.drawText(\n\t\t\tx + statusX,\n\t\t\tfont->height + statusFont->ascent + skip,\n\t\t\tstatus);\n\n\t\tp.setPen(st.nameFg);\n\t\tconst auto textWidth = text->maxWidth();\n\t\ttext->draw(p, Ui::Text::PaintContext{\n\t\t\t.position = QPoint(x + size.width() - textWidth, skip),\n\t\t\t.outerWidth = textWidth,\n\t\t\t.availableWidth = textWidth,\n\t\t});\n\t};\n\treturn { std::move(draw), size };\n}\n\nvoid FillCreditOptions(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<PeerData*> peer,\n\t\tCreditsAmount minimumCredits,\n\t\tFn<void()> paid,\n\t\trpl::producer<> showFinishes,\n\t\trpl::producer<QString> subtitle,\n\t\tstd::vector<Data::CreditTopupOption> preloadedTopupOptions,\n\t\tbool dark,\n\t\tPeerId spendPurposePeerId) {\n\tconst auto options = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto content = options->entity();\n\n\tconst auto singleStarWidth = Ui::GenerateStars(\n\t\tst::creditsTopupButton.height,\n\t\t1).width() / style::DevicePixelRatio();\n\n\tstruct StarsState {\n\t\tbase::flat_map<int, QImage> cache;\n\t\trpl::variable<bool> ready = false;\n\t};\n\tconst auto starsState = content->lifetime().make_state<StarsState>();\n\tconst auto weak = base::make_weak(content);\n\tcrl::async([=]() mutable {\n\t\tconstexpr auto kPreloadCount = 10;\n\t\tauto cache = base::flat_map<int, QImage>();\n\t\tfor (auto i = 1; i <= kPreloadCount; ++i) {\n\t\t\tcache[i] = Ui::GenerateStars(st::creditsTopupButton.height, i);\n\t\t}\n\t\tcrl::on_main(weak, [=, result = std::move(cache)]() mutable {\n\t\t\tstarsState->cache = std::move(result);\n\t\t\tstarsState->ready = true;\n\t\t});\n\t});\n\n\tconst auto loadingContainer = content->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontent,\n\t\t\tobject_ptr<Ui::VerticalLayout>(content)));\n\tloadingContainer->toggle(true, anim::type::instant);\n\tconst auto fillLoading = [=] {\n\t\tUi::AddSkip(content, st::settingsPremiumOptionsPadding.top());\n\t\tif (subtitle) {\n\t\t\tUi::AddSubsectionTitle(\n\t\t\t\tcontent,\n\t\t\t\tstd::move(subtitle),\n\t\t\t\t{},\n\t\t\t\tdark ? &st::groupCallSubsectionTitle : nullptr);\n\t\t}\n\t\tconst auto &st = dark\n\t\t\t? st::videoStreamTopupButton\n\t\t\t: st::creditsTopupButton;\n\t\tconst auto loadingList = content;\n\t\tconst auto isRtl = QLocale().textDirection() == Qt::RightToLeft;\n\t\tusing WidgetPtr = object_ptr<Ui::RpWidget>;\n\t\tfor (auto i = 0; i < 4; i++) {\n\t\t\tauto owned = object_ptr<Ui::SettingsButton>(\n\t\t\t\tloadingList,\n\t\t\t\trpl::never<QString>(),\n\t\t\t\tst);\n\t\t\towned->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\tauto loadingLeft = Ui::CreateLoadingTextWidget(\n\t\t\t\towned,\n\t\t\t\tst.style,\n\t\t\t\t1,\n\t\t\t\trpl::single(isRtl));\n\t\t\tcontent->widthValue(\n\t\t\t) | rpl::on_next([=, raw = loadingLeft.get()](int w) {\n\t\t\t\tif (w > 0) {\n\t\t\t\t\tconst auto availableWidth = w\n\t\t\t\t\t\t- st.iconLeft\n\t\t\t\t\t\t- st::boxRowPadding.right();\n\t\t\t\t\traw->resize(availableWidth, st.style.font->height);\n\t\t\t\t}\n\t\t\t}, loadingLeft->lifetime());\n\t\t\tloadingLeft->moveToLeft(\n\t\t\t\tst.iconLeft,\n\t\t\t\tst.padding.top() - st::lineWidth * 2);\n\t\t\towned->lifetime().make_state<WidgetPtr>(std::move(loadingLeft));\n\t\t\tUi::ToggleChildrenVisibility(owned, true);\n\t\t\tloadingList->add(std::move(owned));\n\t\t}\n\t\tcontent->resizeToWidth(container->width());\n\t};\n\n\tconst auto fill = [=](Data::CreditTopupOptions options) {\n\t\twhile (content->count()) {\n\t\t\tdelete content->widgetAt(0);\n\t\t}\n\t\tUi::AddSkip(content, st::settingsPremiumOptionsPadding.top());\n\t\tif (subtitle) {\n\t\t\tUi::AddSubsectionTitle(\n\t\t\t\tcontent,\n\t\t\t\tstd::move(subtitle),\n\t\t\t\t{},\n\t\t\t\tdark ? &st::groupCallSubsectionTitle : nullptr);\n\t\t}\n\n\t\tconst auto buttons = content->add(\n\t\t\tobject_ptr<Ui::VerticalLayout>(content));\n\n\t\tconst auto showMoreWrap = content->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\t\tcontent,\n\t\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\t\tcontent,\n\t\t\t\t\ttr::lng_credits_more_options(),\n\t\t\t\t\t(dark\n\t\t\t\t\t\t? st::videoStreamShowMoreButton\n\t\t\t\t\t\t: st::statisticsShowMoreButton))));\n\t\tconst auto showMore = showMoreWrap->entity();\n\t\tshowMore->setClickedCallback([=] {\n\t\t\tshowMoreWrap->toggle(false, anim::type::instant);\n\t\t});\n\t\tUi::AddToggleUpDownArrowToMoreButton(showMore);\n\n\t\tconst auto &st = dark\n\t\t\t? st::videoStreamTopupButton\n\t\t\t: st::creditsTopupButton;\n\t\tconst auto diffBetweenTextAndStar = st.padding.left()\n\t\t\t- st.iconLeft\n\t\t\t- int(singleStarWidth * 1.5);\n\t\tconst auto buttonHeight = st.height + rect::m::sum::v(st.padding);\n\t\tconst auto minCredits = (!options.empty()\n\t\t\t\t&& (minimumCredits > CreditsAmount(options.back().credits)))\n\t\t\t? CreditsAmount()\n\t\t\t: minimumCredits;\n\t\tfor (auto i = 0; i < options.size(); i++) {\n\t\t\tconst auto &option = options[i];\n\t\t\tif (CreditsAmount(option.credits) < minCredits) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto button = [&] {\n\t\t\t\tauto owned = object_ptr<Ui::SettingsButton>(\n\t\t\t\t\tbuttons,\n\t\t\t\t\trpl::never<QString>(),\n\t\t\t\t\tst);\n\t\t\t\tif (!option.extended) {\n\t\t\t\t\treturn buttons->add(std::move(owned));\n\t\t\t\t}\n\t\t\t\tconst auto wrap = buttons->add(\n\t\t\t\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\t\t\t\tbuttons,\n\t\t\t\t\t\tstd::move(owned)));\n\t\t\t\twrap->toggle(false, anim::type::instant);\n\t\t\t\tshowMore->clicks() | rpl::on_next([=] {\n\t\t\t\t\twrap->toggle(true, anim::type::normal);\n\t\t\t\t}, wrap->lifetime());\n\t\t\t\treturn wrap->entity();\n\t\t\t}();\n\t\t\tconst auto text = button->lifetime().make_state<Ui::Text::String>(\n\t\t\t\tst.style,\n\t\t\t\ttr::lng_credits_summary_options_credits(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\toption.credits));\n\t\t\tconst auto price = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\t\tbutton,\n\t\t\t\tUi::FillAmountAndCurrency(option.amount, option.currency),\n\t\t\t\tdark ? st::videoStreamTopupPrice : st::creditsTopupPrice);\n\t\t\tconst auto inner = Ui::CreateChild<Ui::RpWidget>(button);\n\t\t\tconst auto getStars = [=] {\n\t\t\t\tconst auto starIndex = i + 1;\n\t\t\t\tif (starsState->cache.contains(starIndex)) {\n\t\t\t\t\treturn starsState->cache[starIndex];\n\t\t\t\t}\n\t\t\t\treturn Ui::GenerateStars(st.height, starIndex);\n\t\t\t};\n\t\t\tconst auto stars = getStars();\n\t\t\tconst auto textLeft = diffBetweenTextAndStar\n\t\t\t\t+ stars.width() / style::DevicePixelRatio();\n\t\t\tinner->paintRequest(\n\t\t\t) | rpl::on_next([=](const QRect &rect) {\n\t\t\t\tauto p = QPainter(inner);\n\t\t\t\tp.drawImage(0, 0, stars);\n\t\t\t\tp.setPen(st.textFg);\n\t\t\t\ttext->draw(p, {\n\t\t\t\t\t.position = QPoint(textLeft, 0),\n\t\t\t\t\t.availableWidth = inner->width() - textLeft,\n\t\t\t\t\t.elisionLines = 1,\n\t\t\t\t});\n\t\t\t}, inner->lifetime());\n\t\t\tbutton->widthValue(\n\t\t\t) | rpl::on_next([=](int width) {\n\t\t\t\tprice->moveToRight(st.padding.right(), st.padding.top());\n\t\t\t\tinner->moveToLeft(st.iconLeft, st.padding.top());\n\t\t\t\tinner->resize(\n\t\t\t\t\twidth - price->width() - st.padding.left(),\n\t\t\t\t\tbuttonHeight);\n\t\t\t}, button->lifetime());\n\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\tconst auto invoice = Payments::InvoiceCredits{\n\t\t\t\t\t.session = &show->session(),\n\t\t\t\t\t.randomId = UniqueIdFromOption(option),\n\t\t\t\t\t.credits = option.credits,\n\t\t\t\t\t.product = option.product,\n\t\t\t\t\t.currency = option.currency,\n\t\t\t\t\t.amount = option.amount,\n\t\t\t\t\t.extended = option.extended,\n\t\t\t\t\t.giftPeerId = PeerId(option.giftBarePeerId),\n\t\t\t\t\t.spendPurposePeerId = spendPurposePeerId,\n\t\t\t\t};\n\n\t\t\t\tconst auto weak = base::make_weak(button);\n\t\t\t\tconst auto done = [=](Payments::CheckoutResult result) {\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->window()->setFocus();\n\t\t\t\t\t\tif (result == Payments::CheckoutResult::Paid) {\n\t\t\t\t\t\t\tif (const auto onstack = paid) {\n\t\t\t\t\t\t\t\tonstack();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tPayments::CheckoutProcess::Start(std::move(invoice), done);\n\t\t\t});\n\t\t\tUi::ToggleChildrenVisibility(button, true);\n\t\t}\n\n\t\t// Footer.\n\t\t{\n\t\t\tauto text = tr::lng_credits_summary_options_about(\n\t\t\t\tlt_link,\n\t\t\t\trpl::combine(\n\t\t\t\t\ttr::lng_credits_summary_options_about_link(),\n\t\t\t\t\ttr::lng_credits_summary_options_about_url()\n\t\t\t\t) | rpl::map([](const QString &text, const QString &url) {\n\t\t\t\t\treturn tr::link(text, url);\n\t\t\t\t}),\n\t\t\t\ttr::rich);\n\t\t\tUi::AddSkip(content);\n\t\t\tUi::AddDividerText(\n\t\t\t\tcontent,\n\t\t\t\tstd::move(text),\n\t\t\t\tst::defaultBoxDividerLabelPadding,\n\t\t\t\tdark ? st::groupCallDividerLabel : st::defaultDividerLabel);\n\t\t}\n\n\t\tcontent->resizeToWidth(container->width());\n\t};\n\n\tusing ApiOptions = Api::CreditsTopupOptions;\n\tconst auto apiCredits = content->lifetime().make_state<ApiOptions>(peer);\n\n\tif (show->session().premiumPossible()) {\n\t\tif (preloadedTopupOptions.empty()) {\n\t\t\tfillLoading();\n\t\t\tstd::move(showFinishes) | rpl::on_next([=] {\n\t\t\t\tapiCredits->request(\n\t\t\t\t) | rpl::on_error_done([=](const QString &error) {\n\t\t\t\t\tshow->showToast(error);\n\t\t\t\t}, [=] {\n\t\t\t\t\tstarsState->ready.value() | rpl::filter(\n\t\t\t\t\t\trpl::mappers::_1\n\t\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\t\tfill(apiCredits->options());\n\t\t\t\t\t}, content->lifetime());\n\t\t\t\t}, content->lifetime());\n\t\t\t}, content->lifetime());\n\t\t} else {\n\t\t\tfill(std::move(preloadedTopupOptions));\n\t\t}\n\t}\n\n\tshow->session().premiumPossibleValue(\n\t) | rpl::on_next([=](bool premiumPossible) {\n\t\tif (!premiumPossible) {\n\t\t\tfill({});\n\t\t}\n\t}, content->lifetime());\n}\n\n[[nodiscard]] object_ptr<Ui::FlatLabel> CreateCreditsTermsLabel(\n\t\tnot_null<Ui::GenericBox*> box) {\n\treturn object_ptr<Ui::FlatLabel>(\n\t\tbox,\n\t\ttr::lng_credits_box_out_about(\n\t\t\tlt_link,\n\t\t\ttr::lng_payments_terms_link(\n\t\t\t\ttr::url(tr::lng_credits_box_out_about_link(tr::now))),\n\t\t\ttr::marked),\n\t\tst::creditsBoxAboutDivider);\n}\n\nnot_null<Ui::RpWidget*> AddBalanceWidget(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<CreditsAmount> balanceValue,\n\t\tbool rightAlign,\n\t\trpl::producer<float64> opacityValue,\n\t\tbool dark) {\n\tstruct State final {\n\t\tfloat64 opacity = 1.0;\n\t\tUi::Text::String label;\n\t\tUi::Text::String count;\n\t};\n\tconst auto balance = Ui::CreateChild<Balance>(parent);\n\tconst auto state = balance->lifetime().make_state<State>();\n\tstate->label = Ui::Text::String(\n\t\tst::defaultTextStyle,\n\t\ttr::lng_credits_summary_balance(tr::now));\n\tstate->count = Ui::Text::String(\n\t\tst::semiboldTextStyle,\n\t\ttr::lng_contacts_loading(tr::now));\n\tif (opacityValue) {\n\t\tstd::move(opacityValue) | rpl::on_next([=](float64 value) {\n\t\t\tstate->opacity = value;\n\t\t}, balance->lifetime());\n\t}\n\tconst auto resize = [=] {\n\t\tbalance->resize(\n\t\t\tstd::max(state->label.maxWidth(), state->count.maxWidth()),\n\t\t\t(state->label.style()->font->height\n\t\t\t\t+ state->count.style()->font->height));\n\t};\n\tstd::move(\n\t\tbalanceValue\n\t) | rpl::on_next([=](CreditsAmount value) {\n\t\tauto text = TextWithEntities();\n\t\tauto helper = Ui::Text::CustomEmojiHelper();\n\t\tif (value.ton()) {\n\t\t\ttext.append(\n\t\t\t\thelper.paletteDependent(Ui::Earn::IconCurrencyEmoji())\n\t\t\t).append(' ').append(Lang::FormatCreditsAmountDecimal(value));\n\t\t} else {\n\t\t\ttext.append(\n\t\t\t\thelper.paletteDependent(Ui::Earn::IconCreditsEmoji())\n\t\t\t).append(' ').append(\n\t\t\t\tLang::FormatCreditsAmountToShort(value).string);\n\t\t}\n\t\tstate->count.setMarkedText(\n\t\t\tst::semiboldTextStyle,\n\t\t\ttext,\n\t\t\tkMarkupTextOptions,\n\t\t\thelper.context([=] { balance->update(); }));\n\t\tbalance->setBalance(value);\n\t\tresize();\n\t}, balance->lifetime());\n\tbalance->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(balance);\n\n\t\tp.setOpacity(state->opacity);\n\t\tp.setPen(dark ? st::groupCallMembersFg : st::boxTextFg);\n\n\t\tstate->label.draw(p, {\n\t\t\t.position = QPoint(\n\t\t\t\trightAlign ? (balance->width() - state->label.maxWidth()) : 0,\n\t\t\t\t0),\n\t\t\t.availableWidth = balance->width(),\n\t\t});\n\t\tstate->count.draw(p, {\n\t\t\t.position = QPoint(\n\t\t\t\trightAlign ? (balance->width() - state->count.maxWidth()) : 0,\n\t\t\t\tstate->label.minHeight()),\n\t\t\t.availableWidth = balance->width(),\n\t\t});\n\t}, balance->lifetime());\n\treturn balance;\n}\n\nvoid BoostCreditsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst Data::Boost &b) {\n\tbox->setStyle(st::giveawayGiftCodeBox);\n\tbox->setNoContentMargin(true);\n\n\tconst auto content = box->verticalLayout();\n\tUi::AddSkip(content);\n\t{\n\t\tconst auto &stUser = st::premiumGiftsUserpicButton;\n\t\tconst auto widget = content->add(object_ptr<Ui::RpWidget>(content));\n\t\tAddMiniStars(content, widget, stUser.photoSize, st::boxWidth, 1.3);\n\t\tconst auto svg = std::make_shared<QSvgRenderer>(\n\t\t\tUi::Premium::ColorizedSvg(\n\t\t\t\tUi::Premium::CreditsIconGradientStops()));\n\t\twidget->paintRequest() | rpl::on_next([=](const QRect &r) {\n\t\t\tauto p = QPainter(widget);\n\t\t\tsvg->render(\n\t\t\t\t&p,\n\t\t\t\tQRectF(\n\t\t\t\t\t(widget->width() - stUser.photoSize) / 2.,\n\t\t\t\t\t(widget->height() - stUser.photoSize) / 2.,\n\t\t\t\t\tstUser.photoSize,\n\t\t\t\t\tstUser.photoSize));\n\t\t}, widget->lifetime());\n\t}\n\tcontent->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_gift_stars_title(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(b.credits))),\n\t\t\tst::boxTitle),\n\t\tstyle::al_top);\n\tUi::AddSkip(content);\n\tif (b.multiplier) {\n\t\tconst auto &st = st::statisticsDetailsBottomCaptionStyle;\n\t\tconst auto badge = content->add(object_ptr<Ui::RpWidget>(content));\n\t\tbadge->resize(badge->width(), st.font->height * 1.5);\n\t\tconst auto text = badge->lifetime().make_state<Ui::Text::String>(\n\t\t\tst::boxWidth\n\t\t\t\t- st::boxRowPadding.left()\n\t\t\t\t- st::boxRowPadding.right());\n\t\tauto textWithEntities = TextWithEntities();\n\t\ttextWithEntities.append(\n\t\t\tUi::Text::IconEmoji(&st::boostsListEntryIcon)\n\t\t).append(\n\t\t\ttr::lng_boosts_list_title(tr::now, lt_count, b.multiplier));\n\t\ttext->setMarkedText(\n\t\t\tst,\n\t\t\tstd::move(textWithEntities),\n\t\t\tkMarkupTextOptions,\n\t\t\tUi::Text::MarkedContext{\n\t\t\t\t.repaint = [=] { badge->update(); },\n\t\t\t});\n\t\tbadge->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = QPainter(badge);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tconst auto radius = badge->height() / 2;\n\t\t\tconst auto badgeWidth = text->maxWidth() + radius;\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(st::premiumButtonBg2);\n\t\t\tp.drawRoundedRect(\n\t\t\t\tQRect(\n\t\t\t\t\t(badge->width() - badgeWidth) / 2,\n\t\t\t\t\t0,\n\t\t\t\t\tbadgeWidth,\n\t\t\t\t\tbadge->height()),\n\t\t\t\tradius,\n\t\t\t\tradius);\n\t\t\tp.setPen(st::premiumButtonFg);\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\ttext->draw(p, Ui::Text::PaintContext{\n\t\t\t\t.position = QPoint(\n\t\t\t\t\t(badge->width() - text->maxWidth() - radius) / 2,\n\t\t\t\t\t(badge->height() - text->minHeight()) / 2),\n\t\t\t\t.outerWidth = badge->width(),\n\t\t\t\t.availableWidth = badge->width(),\n\t\t\t});\n\t\t}, badge->lifetime());\n\n\t\tUi::AddSkip(content);\n\t}\n\tAddCreditsBoostTable(controller->uiShow(), content, {}, b);\n\tUi::AddSkip(content);\n\n\tbox->addRow(CreateCreditsTermsLabel(box), style::al_top);\n\tUi::AddSkip(content);\n\n\tbox->addButton(tr::lng_box_ok(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nvoid ProcessReceivedSubscriptions(\n\t\tbase::weak_qptr<Ui::GenericBox> weak,\n\t\tnot_null<Main::Session*> session) {\n\tconst auto rebuilder = session->data().activeCreditsSubsRebuilder();\n\tif (const auto strong = weak.get()) {\n\t\tif (!rebuilder) {\n\t\t\treturn strong->closeBox();\n\t\t}\n\t\tconst auto api\n\t\t\t= strong->lifetime().make_state<Api::CreditsHistory>(\n\t\t\t\tsession->user(),\n\t\t\t\ttrue,\n\t\t\t\ttrue);\n\t\tapi->requestSubscriptions({}, [=](Data::CreditsStatusSlice first) {\n\t\t\trebuilder->fire(std::move(first));\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t});\n\t}\n}\n\n[[nodiscard]] bool ShowResellButton(\n\t\tnot_null<Main::Session*> session,\n\t\tconst Data::CreditsHistoryEntry &e) {\n\tconst auto unique = e.uniqueGift.get();\n\tconst auto host = (unique && unique->hostId)\n\t\t? session->data().peer(unique->hostId).get()\n\t\t: (unique && unique->ownerId)\n\t\t? session->data().peer(unique->ownerId).get()\n\t\t: nullptr;\n\treturn !host\n\t\t? false\n\t\t: host->isSelf()\n\t\t? e.in\n\t\t: false;\n\t// Currently we're not reselling channel gifts.\n\t// (host->isChannel() && host->asChannel()->canTransferGifts());\n}\n\n[[nodiscard]] bool CanResellGift(\n\t\tnot_null<Main::Session*> session,\n\t\tconst Data::CreditsHistoryEntry &e) {\n\tconst auto unique = e.uniqueGift.get();\n\tconst auto owner = (unique && unique->ownerId)\n\t\t? session->data().peer(unique->ownerId).get()\n\t\t: nullptr;\n\treturn !owner\n\t\t? false\n\t\t: owner->isSelf()\n\t\t? e.in\n\t\t: false;\n\t// Currently we're not reselling channel gifts.\n\t// (owner->isChannel() && owner->asChannel()->canTransferGifts());\n}\n\n[[nodiscard]] bool CanCraftGift(\n\t\tnot_null<Main::Session*> session,\n\t\tconst Data::CreditsHistoryEntry &e) {\n\tconst auto unique = e.uniqueGift.get();\n\tif (!unique || !unique->craftChancePermille) {\n\t\treturn false;\n\t}\n\tconst auto owner = (unique && unique->ownerId)\n\t\t? session->data().peer(unique->ownerId).get()\n\t\t: nullptr;\n\treturn !owner\n\t\t? false\n\t\t: owner->isSelf()\n\t\t? e.in\n\t\t: false;\n\t// Currently we're not crafting channel gifts.\n\t// (owner->isChannel() && owner->asChannel()->canTransferGifts());\n}\n\n[[nodiscard]] bool ShowOfferBuyButton(\n\t\tnot_null<Main::Session*> session,\n\t\tconst Data::CreditsHistoryEntry &e) {\n\tconst auto unique = e.uniqueGift.get();\n\tconst auto owner = (unique && unique->ownerId)\n\t\t? session->data().peer(unique->ownerId).get()\n\t\t: nullptr;\n\treturn owner\n\t\t&& owner->isUser()\n\t\t&& !owner->isSelf()\n\t\t&& (unique->starsMinOffer >= 0);\n\t// Currently we're not making offers for channel gifts.\n\t// (owner->isChannel() && !owner->asChannel()->canTransferGifts());\n}\n\nvoid FillUniqueGiftMenu(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst Data::CreditsHistoryEntry &e,\n\t\tSavedStarGiftMenuType type,\n\t\tCreditsEntryBoxStyleOverrides st) {\n\tconst auto session = &show->session();\n\tconst auto savedId = EntryToSavedStarGiftId(session, e);\n\tconst auto giftChannel = savedId.chat();\n\tconst auto canToggle = savedId\n\t\t&& e.id.isEmpty()\n\t\t&& (e.in || (giftChannel && giftChannel->canManageGifts()))\n\t\t&& !e.giftTransferred\n\t\t&& !e.giftRefunded\n\t\t&& !e.converted;\n\n\tconst auto unique = e.uniqueGift;\n\tif (unique\n\t\t&& canToggle\n\t\t&& e.savedToProfile\n\t\t&& e.pinnedSavedGifts) {\n\t\tconst auto pinned = e.pinnedSavedGifts;\n\t\tconst auto ids = [session](\n\t\t\t\tconst std::vector<Data::CreditsHistoryEntry> &pinned) {\n\t\t\tauto result = std::vector<Data::SavedStarGiftId>();\n\t\t\tresult.reserve(pinned.size());\n\t\t\tfor (const auto &entry : pinned) {\n\t\t\t\tresult.push_back(EntryToSavedStarGiftId(session, entry));\n\t\t\t}\n\t\t\treturn result;\n\t\t};\n\t\tif (e.giftPinned) {\n\t\t\tmenu->addAction(tr::lng_context_unpin_from_top(tr::now), [=] {\n\t\t\t\tsession->recentSharedGifts().togglePinned(\n\t\t\t\t\tshow,\n\t\t\t\t\tgiftChannel ? giftChannel : session->user(),\n\t\t\t\t\tsavedId,\n\t\t\t\t\tfalse,\n\t\t\t\t\tunique);\n\t\t\t}, st.unpin ? st.unpin : &st::menuIconUnpin);\n\t\t} else {\n\t\t\tmenu->addAction(tr::lng_context_pin_to_top(tr::now), [=] {\n\t\t\t\tconst auto list = pinned();\n\t\t\t\tconst auto &appConfig = show->session().appConfig();\n\t\t\t\tconst auto limit = appConfig.pinnedGiftsLimit();\n\t\t\t\tauto already = ids(list);\n\t\t\t\tif (list.size() >= limit) {\n\t\t\t\t\tInfo::PeerGifts::SelectGiftToUnpin(show, list, [=](\n\t\t\t\t\t\t\tData::SavedStarGiftId id) {\n\t\t\t\t\t\tauto copy = already;\n\t\t\t\t\t\tconst auto i = ranges::find(copy, id);\n\t\t\t\t\t\tconst auto replaced = (i != end(copy))\n\t\t\t\t\t\t\t? list[i - begin(copy)].uniqueGift\n\t\t\t\t\t\t\t: nullptr;\n\t\t\t\t\t\tif (i != end(copy)) {\n\t\t\t\t\t\t\tcopy.erase(i);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tusing GiftAction = Data::GiftUpdate::Action;\n\t\t\t\t\t\tshow->session().data().notifyGiftUpdate({\n\t\t\t\t\t\t\t.id = id,\n\t\t\t\t\t\t\t.action = GiftAction::Unpin,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tsession->recentSharedGifts().togglePinned(\n\t\t\t\t\t\t\tshow,\n\t\t\t\t\t\t\tgiftChannel ? giftChannel : session->user(),\n\t\t\t\t\t\t\tsavedId,\n\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\tunique,\n\t\t\t\t\t\t\treplaced);\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tsession->recentSharedGifts().togglePinned(\n\t\t\t\t\t\tshow,\n\t\t\t\t\t\tgiftChannel ? giftChannel : session->user(),\n\t\t\t\t\t\tsavedId,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t\tunique);\n\t\t\t\t}\n\t\t\t}, st.pin ? st.pin : &st::menuIconPin);\n\t\t}\n\t}\n\tif (unique) {\n\t\tconst auto local = u\"nft/\"_q + unique->slug;\n\t\tconst auto url = show->session().createInternalLinkFull(local);\n\t\tmenu->addAction(tr::lng_context_copy_link(tr::now), [=] {\n\t\t\tTextUtilities::SetClipboardText({ url });\n\t\t\tshow->showToast(tr::lng_channel_public_link_copied(tr::now));\n\t\t}, st.link ? st.link : &st::menuIconLink);\n\n\t\tconst auto shareBoxSt = st.shareBox;\n\t\tmenu->addAction(tr::lng_chat_link_share(tr::now), [=] {\n\t\t\tFastShareLink(\n\t\t\t\tshow,\n\t\t\t\turl,\n\t\t\t\tshareBoxSt ? *shareBoxSt : ShareBoxStyleOverrides());\n\t\t}, st.share ? st.share : &st::menuIconShare);\n\t}\n\n\tif (canToggle && type == SavedStarGiftMenuType::List) {\n\t\tif (e.savedToProfile) {\n\t\t\tmenu->addAction(tr::lng_gift_menu_hide(tr::now), [=] {\n\t\t\t\tToggleStarGiftSaved(show, savedId, false);\n\t\t\t}, st.hide ? st.hide : &st::menuIconStealth);\n\t\t} else {\n\t\t\tmenu->addAction(tr::lng_gift_menu_show(tr::now), [=] {\n\t\t\t\tToggleStarGiftSaved(show, savedId, true);\n\t\t\t}, st.show ? st.show : &st::menuIconShowInChat);\n\t\t}\n\t}\n\n\tif (!unique) {\n\t\treturn;\n\t}\n\tif (unique->canBeTheme) {\n\t\tmenu->addAction(tr::lng_gift_transfer_set_theme(tr::now), [=] {\n\t\t\tif (const auto window = show->resolveWindow()) {\n\t\t\t\tSetThemeFromUniqueGift(window, unique);\n\t\t\t}\n\t\t}, st.theme ? st.theme : &st::menuIconChangeColors);\n\t}\n\tconst auto owner = unique->ownerId\n\t\t? show->session().data().peer(unique->ownerId).get()\n\t\t: (PeerData*)nullptr;\n\tconst auto host = unique->hostId\n\t\t? show->session().data().peer(unique->hostId).get()\n\t\t: owner;\n\tif (!host) {\n\t\treturn;\n\t}\n\tif (CanCraftGift(&show->session(), e)) {\n\t\tmenu->addAction(tr::lng_gift_craft_menu_button(tr::now), [=] {\n\t\t\tconst auto unique = e.uniqueGift;\n\t\t\tif (Ui::ShowCraftLaterError(show, unique)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (Ui::ShowCraftAddressError(show, unique)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto savedId = EntryToSavedStarGiftId(&show->session(), e);\n\t\t\tif (const auto window = show->resolveWindow()) {\n\t\t\t\tUi::ShowGiftCraftInfoBox(window, unique, savedId);\n\t\t\t}\n\t\t}, st.craft ? st.craft : &st::menuIconCraft);\n\t}\n\tconst auto transfer = savedId\n\t\t&& (savedId.isUser() ? e.in : savedId.chat()->canTransferGifts())\n\t\t&& (unique->starsForTransfer >= 0);\n\tif (transfer) {\n\t\tmenu->addAction(tr::lng_gift_transfer_button(tr::now), [=] {\n\t\t\tif (!owner) {\n\t\t\t\tShowActionLocked(show, unique->slug);\n\t\t\t} else if (const auto window = show->resolveWindow()) {\n\t\t\t\tShowTransferGiftBox(window, unique, savedId);\n\t\t\t}\n\t\t}, st.transfer ? st.transfer : &st::menuIconReplace);\n\t}\n\tconst auto wear = host->isSelf()\n\t\t? e.in\n\t\t: (host->isChannel() && host->asChannel()->canEditEmoji());\n\tif (wear) {\n\t\tconst auto name = UniqueGiftName(*unique);\n\t\tconst auto now = host->emojiStatusId().collectible;\n\t\tif (now && unique->slug == now->slug) {\n\t\t\tmenu->addAction(tr::lng_gift_transfer_take_off(tr::now), [=] {\n\t\t\t\tshow->session().data().emojiStatuses().set(host, {});\n\t\t\t}, st.takeoff ? st.takeoff : &st::menuIconNftTakeOff);\n\t\t} else {\n\t\t\tmenu->addAction(tr::lng_gift_transfer_wear(tr::now), [=] {\n\t\t\t\tShowUniqueGiftWearBox(show, host, *unique, st.giftWearBox\n\t\t\t\t\t? *st.giftWearBox\n\t\t\t\t\t: GiftWearBoxStyleOverride());\n\t\t\t}, st.wear ? st.wear : &st::menuIconNftWear);\n\t\t}\n\t}\n\tif (ShowResellButton(&show->session(), e)) {\n\t\tconst auto can = CanResellGift(&show->session(), e);\n\t\tconst auto inResale = (unique->starsForResale > 0);\n\t\tconst auto editPrice = (inResale\n\t\t\t? tr::lng_gift_transfer_update\n\t\t\t: tr::lng_gift_transfer_sell)(tr::now);\n\t\tmenu->addAction(editPrice, [=] {\n\t\t\tif (!can) {\n\t\t\t\tShowActionLocked(show, unique->slug);\n\t\t\t} else {\n\t\t\t\tconst auto style = st.giftWearBox\n\t\t\t\t\t? *st.giftWearBox\n\t\t\t\t\t: GiftWearBoxStyleOverride();\n\t\t\t\tShowUniqueGiftSellBox(show, unique, savedId, style);\n\t\t\t}\n\t\t}, st.resell ? st.resell : &st::menuIconTagSell);\n\t\tif (inResale) {\n\t\t\tmenu->addAction(tr::lng_gift_transfer_unlist(tr::now), [=] {\n\t\t\t\tconst auto name = UniqueGiftName(*unique);\n\t\t\t\tconst auto confirm = [=](Fn<void()> close) {\n\t\t\t\t\tclose();\n\t\t\t\t\tUi::UpdateGiftSellPrice(show, unique, savedId, {});\n\t\t\t\t};\n\t\t\t\tshow->show(Ui::MakeConfirmBox({\n\t\t\t\t\t.text = tr::lng_gift_sell_unlist_sure(),\n\t\t\t\t\t.confirmed = confirm,\n\t\t\t\t\t.confirmText = tr::lng_gift_transfer_unlist(),\n\t\t\t\t\t.title = tr::lng_gift_sell_unlist_title(\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\trpl::single(name)),\n\t\t\t\t}));\n\t\t\t}, st.unlist ? st.unlist : &st::menuIconTagRemove);\n\t\t}\n\t} else if (ShowOfferBuyButton(&show->session(), e)) {\n\t\tmenu->addAction(tr::lng_gift_offer_button(tr::now), [=] {\n\t\t\tShowOfferBuyBox(show, unique);\n\t\t}, st.offer ? st.offer : &st::menuIconEarn);\n\t}\n}\n\nGiftWearBoxStyleOverride DarkGiftWearBoxStyle() {\n\treturn {\n\t\t.box = &st::darkUpgradeGiftBox,\n\t\t.close = &st::darkGiftBoxClose,\n\t\t.title = &st::darkUpgradeGiftTitle,\n\t\t.subtitle = &st::darkUpgradeGiftSubtitle,\n\t\t.radiantIcon = &st::darkUpgradeGiftRadiant,\n\t\t.profileIcon = &st::darkUpgradeGiftProfile,\n\t\t.proofIcon = &st::darkUpgradeGiftProof,\n\t\t.infoTitle = &st::darkUpgradeGiftInfoTitle,\n\t\t.infoAbout = &st::darkUpgradeGiftInfoAbout,\n\t};\n}\n\nCreditsEntryBoxStyleOverrides DarkCreditsEntryBoxStyle() {\n\treturn {\n\t\t.box = &st::darkGiftCodeBox,\n\t\t.menu = &st::mediaviewPopupMenu,\n\t\t.table = &st::darkGiftTable,\n\t\t.tableValueMultiline = &st::darkGiftTableValueMultiline,\n\t\t.tableValueMessage = &st::darkGiftTableMessage,\n\t\t.link = &st::darkGiftLink,\n\t\t.share = &st::darkGiftShare,\n\t\t.theme = &st::darkGiftTheme,\n\t\t.transfer = &st::darkGiftTransfer,\n\t\t.craft = &st::darkGiftCraft,\n\t\t.wear = &st::darkGiftNftWear,\n\t\t.takeoff = &st::darkGiftNftTakeOff,\n\t\t.resell = &st::darkGiftNftResell,\n\t\t.unlist = &st::darkGiftNftUnlist,\n\t\t.show = &st::darkGiftShow,\n\t\t.hide = &st::darkGiftHide,\n\t\t.pin = &st::darkGiftPin,\n\t\t.unpin = &st::darkGiftUnpin,\n\t\t.offer = &st::darkGiftOffer,\n\t\t.shareBox = std::make_shared<ShareBoxStyleOverrides>(\n\t\t\tDarkShareBoxStyle()),\n\t\t.giftWearBox = std::make_shared<GiftWearBoxStyleOverride>(\n\t\t\tDarkGiftWearBoxStyle()),\n\t};\n}\n\nrpl::producer<CreditsAmount> UniqueGiftResalePrice(\n\t\tstd::shared_ptr<Data::UniqueGift> unique,\n\t\tbool forceTon) {\n\tconst auto slug = unique->slug;\n\treturn rpl::single(\n\t\trpl::empty\n\t) | rpl::then(unique->model.document->owner().giftUpdates(\n\t) | rpl::filter([=](const Data::GiftUpdate &update) {\n\t\treturn (update.action == Data::GiftUpdate::Action::ResaleChange)\n\t\t\t&& (update.slug == slug);\n\t}) | rpl::to_empty) | rpl::map([=] {\n\t\tconst auto result = forceTon\n\t\t\t? Data::UniqueGiftResaleTon(*unique)\n\t\t\t: Data::UniqueGiftResaleAsked(*unique);\n\t\treturn (result.value() < 0) ? CreditsAmount() : result;\n\t});\n}\n\nbool UniqueGiftCanRemoveDetails(const Data::CreditsHistoryEntry &entry) {\n\treturn entry.uniqueGift && (entry.starsForDetailsRemove > 0);\n}\n\nFn<void(Fn<void()> removed)> UniqueGiftRemoveDetailsHandler(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::CreditsHistoryEntry &entry) {\n\treturn [=](Fn<void()> removed) {\n\t\tconst auto session = &show->session();\n\t\tconst auto unique = entry.uniqueGift;\n\t\tconst auto savedId = EntryToSavedStarGiftId(session, entry);\n\t\tauto done = [=](\n\t\t\t\tPayments::CheckoutResult result,\n\t\t\t\tconst MTPUpdates *updates) {\n\t\t\tif (result == Payments::CheckoutResult::Paid) {\n\t\t\t\tremoved();\n\n\t\t\t\tconst auto name = Data::UniqueGiftName(*unique);\n\t\t\t\tshow->showToast(tr::lng_gift_unique_info_removed(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_name,\n\t\t\t\t\ttr::bold(name),\n\t\t\t\t\ttr::marked));\n\t\t\t\tunique->originalDetails = Data::UniqueGiftOriginalDetails();\n\t\t\t}\n\t\t};\n\t\tRequestStarsFormAndSubmit(\n\t\t\tshow,\n\t\t\tMTP_inputInvoiceStarGiftDropOriginalDetails(\n\t\t\t\tApi::InputSavedStarGiftId(savedId, unique)),\n\t\t\tstd::move(done));\n\t};\n}\n\nvoid GenericCreditsEntryCover(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::CreditsHistoryEntry &e,\n\t\tconst Data::SubscriptionEntry &s,\n\t\tCreditsEntryBoxStyleOverrides st = {}) {\n\tconst auto session = &show->session();\n\tconst auto owner = &session->data();\n\tconst auto isStarGift = e.stargift || e.soldOutInfo;\n\tconst auto uniqueGift = e.uniqueGift.get();\n\n\tbox->setStyle(st.box ? *st.box : st::giveawayGiftCodeBox);\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setNoContentMargin(true);\n\n\tconst auto content = box->verticalLayout();\n\tif (!uniqueGift) {\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\t}\n\n\tconst auto &stUser = st::boostReplaceUserpic;\n\tconst auto isPrize = e.bareGiveawayMsgId > 0;\n\tconst auto starGiftSticker = (isStarGift && e.bareGiftStickerId)\n\t\t? owner->document(e.bareGiftStickerId).get()\n\t\t: nullptr;\n\tconst auto peer = isPrize\n\t\t? nullptr\n\t\t: (s.barePeerId)\n\t\t? owner->peer(PeerId(s.barePeerId)).get()\n\t\t: (e.peerType == Data::CreditsHistoryEntry::PeerType::PremiumBot)\n\t\t? nullptr\n\t\t: e.bareActorId\n\t\t? owner->peer(PeerId(e.bareActorId)).get()\n\t\t: e.barePeerId\n\t\t? owner->peer(PeerId(e.barePeerId)).get()\n\t\t: nullptr;\n\tif (uniqueGift) {\n\t\tconst auto forceTon = e.giftResaleForceTon;\n\t\tconst auto cover = Ui::UniqueGiftCover{ *uniqueGift };\n\t\tconst auto wearSt = st.giftWearBox\n\t\t\t? *st.giftWearBox\n\t\t\t: GiftWearBoxStyleOverride();\n\t\tconst auto savedId = EntryToSavedStarGiftId(session, e);\n\t\tconst auto resaleClick = CanResellGift(session, e)\n\t\t\t? [=] {\n\t\t\t\tShowUniqueGiftSellBox(show, e.uniqueGift, savedId, wearSt);\n\t\t\t}\n\t\t\t: Fn<void()>();\n\t\tAddUniqueGiftCover(content, rpl::single(cover), {\n\t\t\t.numberText = (uniqueGift->number > 0)\n\t\t\t\t? rpl::single(u\"#\"_q + Lang::FormatCountDecimal(uniqueGift->number))\n\t\t\t\t: rpl::producer<QString>(),\n\t\t\t.resalePrice = UniqueGiftResalePrice(e.uniqueGift, forceTon),\n\t\t\t.resaleClick = resaleClick,\n\t\t});\n\t\tif (e.bareGiftOwnerId == session->userPeerId().value) {\n\t\t\tif (const auto fromId = PeerId(e.barePeerId)) {\n\t\t\t\tconst auto from = session->data().peer(fromId);\n\t\t\t\tconst auto crafted = uniqueGift->crafted;\n\t\t\t\tAttachGiftSenderBadge(box, show, from, e.date, crafted);\n\t\t\t}\n\t\t}\n\t} else if (const auto callback = Ui::PaintPreviewCallback(session, e)) {\n\t\tconst auto thumb = content->add(\n\t\t\tGenericEntryPhoto(content, callback, stUser.photoSize),\n\t\t\tstyle::al_top);\n\t\tAddViewMediaHandler(thumb, show, e);\n\t} else if (s.photoId || (e.photoId && !e.subscriptionUntil.isNull())) {\n\t\tif (!(s.cancelled || s.expired || s.cancelledByBot)) {\n\t\t\tconst auto widget = Ui::CreateChild<Ui::RpWidget>(content);\n\t\t\tconst auto photoSize = stUser.photoSize;\n\t\t\tAddMiniStars(content, widget, photoSize, st::boxWideWidth, 1.5);\n\t\t}\n\t\tconst auto photoId = s.photoId ? s.photoId : e.photoId;\n\t\tconst auto callback = [=](Fn<void()> update) {\n\t\t\treturn Ui::GenerateCreditsPaintEntryCallback(\n\t\t\t\towner->photo(photoId),\n\t\t\t\tstd::move(update));\n\t\t};\n\t\tcontent->add(\n\t\t\tGenericEntryPhoto(content, callback, stUser.photoSize),\n\t\t\tstyle::al_top);\n\t} else if (peer\n\t\t&& !e.gift\n\t\t&& !e.premiumMonthsForStars\n\t\t&& !e.postsSearch) {\n\t\tif (e.subscriptionUntil.isNull() && s.until.isNull()) {\n\t\t\tcontent->add(\n\t\t\t\tobject_ptr<Ui::UserpicButton>(content, peer, stUser),\n\t\t\t\tstyle::al_top);\n\t\t} else {\n\t\t\tcontent->add(\n\t\t\t\tSubscriptionUserpic(content, peer, stUser.photoSize),\n\t\t\t\tstyle::al_top);\n\t\t}\n\t} else if (e.gift || isPrize || e.premiumMonthsForStars) {\n\t\tstruct State final {\n\t\t\tDocumentData *sticker = nullptr;\n\t\t\tstd::shared_ptr<Data::DocumentMedia> media;\n\t\t\tstd::unique_ptr<Lottie::SinglePlayer> lottie;\n\t\t\trpl::lifetime downloadLifetime;\n\t\t};\n\t\tUi::AddSkip(content, isStarGift\n\t\t\t? st::creditsHistoryEntryStarGiftSpace\n\t\t\t: st::creditsHistoryEntryGiftStickerSpace);\n\t\tconst auto icon = Ui::CreateChild<Ui::RpWidget>(content);\n\t\ticon->resize(Size(isStarGift\n\t\t\t? st::creditsHistoryEntryStarGiftSize\n\t\t\t: st::creditsHistoryEntryGiftStickerSize));\n\t\tconst auto state = icon->lifetime().make_state<State>();\n\t\tauto &packs = session->giftBoxStickersPacks();\n\t\tconst auto document = starGiftSticker\n\t\t\t? starGiftSticker\n\t\t\t: e.credits.ton()\n\t\t\t? packs.tonLookup(e.credits.whole())\n\t\t\t: packs.lookup(\n\t\t\t\te.premiumMonthsForStars\n\t\t\t\t\t? e.premiumMonthsForStars\n\t\t\t\t\t: packs.monthsForStars(e.credits.whole()));\n\t\tif (document && document->sticker()) {\n\t\t\tconst auto origin = starGiftSticker\n\t\t\t\t? starGiftSticker->stickerOrGifOrigin()\n\t\t\t\t: e.credits.ton()\n\t\t\t\t? packs.tonOrigin()\n\t\t\t\t: packs.origin();\n\t\t\tstate->sticker = document;\n\t\t\tstate->media = document->createMediaView();\n\t\t\tstate->media->thumbnailWanted(origin);\n\t\t\tstate->media->automaticLoad(origin, nullptr);\n\t\t\trpl::single() | rpl::then(\n\t\t\t\tsession->downloaderTaskFinished()\n\t\t\t) | rpl::filter([=] {\n\t\t\t\treturn state->media->loaded();\n\t\t\t}) | rpl::on_next([=] {\n\t\t\t\tstate->lottie = ChatHelpers::LottiePlayerFromDocument(\n\t\t\t\t\tstate->media.get(),\n\t\t\t\t\tChatHelpers::StickerLottieSize::MessageHistory,\n\t\t\t\t\ticon->size(),\n\t\t\t\t\tLottie::Quality::High);\n\t\t\t\tstate->lottie->updates() | rpl::on_next([=] {\n\t\t\t\t\ticon->update();\n\t\t\t\t}, icon->lifetime());\n\t\t\t\tstate->downloadLifetime.destroy();\n\t\t\t}, state->downloadLifetime);\n\t\t}\n\t\ticon->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = Painter(icon);\n\t\t\tconst auto &lottie = state->lottie;\n\t\t\tconst auto factor = style::DevicePixelRatio();\n\t\t\tconst auto request = Lottie::FrameRequest{\n\t\t\t\t.box = icon->size() * factor,\n\t\t\t};\n\t\t\tconst auto frame = (lottie && lottie->ready())\n\t\t\t\t? lottie->frameInfo(request)\n\t\t\t\t: Lottie::Animation::FrameInfo();\n\t\t\tif (!frame.image.isNull()) {\n\t\t\t\tp.drawImage(\n\t\t\t\t\tQRect(QPoint(), frame.image.size() / factor),\n\t\t\t\t\tframe.image);\n\t\t\t\tif (lottie->frameIndex() < lottie->framesCount() - 1) {\n\t\t\t\t\tlottie->markFrameShown();\n\t\t\t\t}\n\t\t\t}\n\t\t}, icon->lifetime());\n\t\tcontent->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &size) {\n\t\t\ticon->move((size.width() - icon->width()) / 2, isStarGift\n\t\t\t\t? st::creditsHistoryEntryStarGiftSkip\n\t\t\t\t: st::creditsHistoryEntryGiftStickerSkip);\n\t\t}, icon->lifetime());\n\t} else if (!e.postsSearch) {\n\t\tconst auto widget = content->add(\n\t\t\tobject_ptr<Ui::RpWidget>(content),\n\t\t\tstyle::al_top);\n\t\tusing Draw = Fn<void(Painter &, int, int, int, int)>;\n\t\tconst auto draw = widget->lifetime().make_state<Draw>(\n\t\t\tUi::GenerateCreditsPaintUserpicCallback(e));\n\t\twidget->resize(Size(stUser.photoSize));\n\t\twidget->setNaturalWidth(stUser.photoSize);\n\t\twidget->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = Painter(widget);\n\t\t\t(*draw)(p, 0, 0, stUser.photoSize, stUser.photoSize);\n\t\t}, widget->lifetime());\n\t}\n}\n\nvoid GenericCreditsEntryBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::CreditsHistoryEntry &e,\n\t\tconst Data::SubscriptionEntry &s,\n\t\tCreditsEntryBoxStyleOverrides st) {\n\tGenericCreditsEntryCover(box, show, e, s, st);\n\tGenericCreditsEntryBody(box, show, e, s, nullptr, st);\n}\n\nvoid GenericCreditsEntryBody(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::CreditsHistoryEntry &e) {\n\tGenericCreditsEntryBody(box, std::move(show), e, {}, nullptr, {});\n}\n\nvoid GenericCreditsEntryBody(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::CreditsHistoryEntry &e,\n\t\tconst Data::SubscriptionEntry &s,\n\t\tstd::shared_ptr<Data::GiftUpgradeSpinner> upgradeSpinner,\n\t\tCreditsEntryBoxStyleOverrides st) {\n\tconst auto session = &show->session();\n\tconst auto selfPeerId = session->userPeerId().value;\n\tconst auto owner = &session->data();\n\tconst auto item = owner->message(\n\t\tPeerId(e.barePeerId),\n\t\tMsgId(e.bareMsgId));\n\tconst auto isStarGift = e.stargift || e.soldOutInfo;\n\tconst auto creditsHistoryStarGift = isStarGift && !e.id.isEmpty();\n\tconst auto sentStarGift = creditsHistoryStarGift && !e.in;\n\tconst auto giftToSelf = isStarGift\n\t\t&& (e.barePeerId == selfPeerId)\n\t\t&& (e.in || e.bareGiftOwnerId == selfPeerId);\n\tconst auto giftChannel = (isStarGift && e.giftChannelSavedId)\n\t\t? session->data().peer(\n\t\t\tPeerId(e.bareEntryOwnerId))->asChannel()\n\t\t: nullptr;\n\tconst auto giftToChannel = (giftChannel != nullptr);\n\tconst auto giftToChannelCanManage = giftToChannel\n\t\t&& giftChannel->canManageGifts();\n\tconst auto giftToChannelCanTransfer = giftToChannel\n\t\t&& giftChannel->canTransferGifts();\n\tconst auto starGiftCanManage = isStarGift\n\t\t&& !creditsHistoryStarGift\n\t\t&& (e.in || giftToChannelCanManage)\n\t\t&& !e.fromGiftSlug\n\t\t&& !e.converted;\n\tconst auto starGiftCanTransfer = isStarGift\n\t\t&& !creditsHistoryStarGift\n\t\t&& (e.in || giftToChannelCanTransfer);\n\tconst auto starGiftSender = (isStarGift && item)\n\t\t? item->history()->peer->asUser()\n\t\t: (isStarGift && e.in)\n\t\t? owner->peer(PeerId(e.barePeerId))->asUser()\n\t\t: (isStarGift && e.bareActorId)\n\t\t? owner->peer(PeerId(e.bareActorId)).get()\n\t\t: nullptr;\n\tconst auto convertLast = base::unixtime::serialize(e.date)\n\t\t+ session->appConfig().stargiftConvertPeriodMax();\n\tconst auto timeLeft = int64(convertLast) - int64(base::unixtime::now());\n\tconst auto timeExceeded = (timeLeft <= 0);\n\tconst auto uniqueGift = e.uniqueGift.get();\n\tconst auto forConvert = starGiftCanTransfer\n\t\t&& e.starsConverted\n\t\t&& !e.converted\n\t\t&& starGiftSender;\n\tconst auto canConvert = forConvert && !timeExceeded;\n\tconst auto inResale = uniqueGift && (uniqueGift->starsForResale > 0);\n\tconst auto canBuyResold = inResale && (e.bareGiftOwnerId != selfPeerId);\n\tconst auto &stUser = st::boostReplaceUserpic;\n\tconst auto isPrize = e.bareGiveawayMsgId > 0;\n\tconst auto starGiftSticker = (isStarGift && e.bareGiftStickerId)\n\t\t? owner->document(e.bareGiftStickerId).get()\n\t\t: nullptr;\n\tconst auto peer = isPrize\n\t\t? nullptr\n\t\t: (s.barePeerId)\n\t\t? owner->peer(PeerId(s.barePeerId)).get()\n\t\t: (e.peerType == Data::CreditsHistoryEntry::PeerType::PremiumBot)\n\t\t? nullptr\n\t\t: e.bareActorId\n\t\t? owner->peer(PeerId(e.bareActorId)).get()\n\t\t: e.barePeerId\n\t\t? owner->peer(PeerId(e.barePeerId)).get()\n\t\t: nullptr;\n\n\tif (auto savedId = EntryToSavedStarGiftId(session, e)) {\n\t\tsession->data().giftUpdates(\n\t\t) | rpl::on_next([=](const Data::GiftUpdate &update) {\n\t\t\tif (update.id == savedId\n\t\t\t\t&& update.action != Data::GiftUpdate::Action::ResaleChange) {\n\t\t\t\tbox->closeBox();\n\t\t\t}\n\t\t}, box->lifetime());\n\t}\n\n\tconst auto content = box->verticalLayout();\n\tif (uniqueGift) {\n\t\tAddSkip(content, st::defaultVerticalListSkip * 2);\n\n\t\tconst auto canCraft = CanCraftGift(session, e);\n\t\tconst auto craft = canCraft ? [=] {\n\t\t\tconst auto unique = e.uniqueGift;\n\t\t\tif (Ui::ShowCraftLaterError(show, unique)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (Ui::ShowCraftAddressError(show, unique)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto savedId = EntryToSavedStarGiftId(&show->session(), e);\n\t\t\tif (const auto window = show->resolveWindow()) {\n\t\t\t\tUi::ShowGiftCraftInfoBox(window, unique, savedId);\n\t\t\t}\n\t\t} : Fn<void()>();\n\t\tAddUniqueCloseMoreButton(box, st, [=](not_null<Ui::PopupMenu*> menu) {\n\t\t\tconst auto type = SavedStarGiftMenuType::View;\n\t\t\tFillUniqueGiftMenu(show, menu, e, type, st);\n\t\t}, craft);\n\n\t\tif (CanResellGift(session, e)) {\n\t\t\tUi::PreloadUniqueGiftResellPrices(session);\n\t\t}\n\t} else {\n\t\tUi::AddSkip(content);\n\t\tUi::AddSkip(content);\n\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\trpl::single(!s.title.isEmpty()\n\t\t\t\t\t? s.title\n\t\t\t\t\t: !s.until.isNull()\n\t\t\t\t\t? tr::lng_credits_box_subscription_title(tr::now)\n\t\t\t\t\t: isPrize\n\t\t\t\t\t? tr::lng_credits_box_history_entry_giveaway_name(tr::now)\n\t\t\t\t\t: (!e.subscriptionUntil.isNull() && e.title.isEmpty())\n\t\t\t\t\t? tr::lng_credits_box_history_entry_subscription(tr::now)\n\t\t\t\t\t: e.isLiveStoryReaction()\n\t\t\t\t\t? tr::lng_credits_paid_messages_fee_live_reaction(tr::now)\n\t\t\t\t\t: e.paidMessagesCount\n\t\t\t\t\t? tr::lng_credits_paid_messages_fee(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\te.paidMessagesCount)\n\t\t\t\t\t: e.postsSearch\n\t\t\t\t\t? tr::lng_credits_box_history_entry_posts_search(tr::now)\n\t\t\t\t\t: e.premiumMonthsForStars\n\t\t\t\t\t? tr::lng_premium_summary_title(tr::now)\n\t\t\t\t\t: !e.title.isEmpty()\n\t\t\t\t\t? e.title\n\t\t\t\t\t: e.starrefCommission\n\t\t\t\t\t? tr::lng_credits_commission(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_amount,\n\t\t\t\t\t\tInfo::BotStarRef::FormatCommission(e.starrefCommission))\n\t\t\t\t\t: e.soldOutInfo\n\t\t\t\t\t? tr::lng_credits_box_history_entry_gift_unavailable(tr::now)\n\t\t\t\t\t: sentStarGift\n\t\t\t\t\t? tr::lng_credits_box_history_entry_gift_sent(tr::now)\n\t\t\t\t\t: e.converted\n\t\t\t\t\t? tr::lng_credits_box_history_entry_gift_converted(tr::now)\n\t\t\t\t\t: (e.giftNumber && !e.giftTitle.isEmpty())\n\t\t\t\t\t? Data::UniqueGiftName(e.giftTitle, e.giftNumber)\n\t\t\t\t\t: (isStarGift && !starGiftCanManage)\n\t\t\t\t\t? tr::lng_gift_link_label_gift(tr::now)\n\t\t\t\t\t: giftToSelf\n\t\t\t\t\t? ((uniqueGift && uniqueGift->crafted)\n\t\t\t\t\t\t? tr::lng_action_gift_crafted_subtitle(tr::now)\n\t\t\t\t\t\t: tr::lng_action_gift_self_subtitle(tr::now))\n\t\t\t\t\t: e.gift\n\t\t\t\t\t? tr::lng_credits_box_history_entry_gift_name(tr::now)\n\t\t\t\t\t: (peer && !e.reaction)\n\t\t\t\t\t? peer->name()\n\t\t\t\t\t: Ui::GenerateEntryName(e).text),\n\t\t\t\tst::creditsBoxAboutTitle),\n\t\t\tstyle::al_top);\n\n\t\tUi::AddSkip(content);\n\t}\n\tif (e.bareGiftReleasedById && !e.uniqueGift) {\n\t\tconst auto peer = owner->peer(PeerId(e.bareGiftReleasedById));\n\t\tconst auto released = content->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\ttr::lng_credits_box_history_entry_gift_released(\n\t\t\t\t\tlt_name,\n\t\t\t\t\trpl::single(tr::link('@' + peer->username())),\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::creditsReleasedByLabel),\n\t\t\tstyle::al_top);\n\t\treleased->setClickHandlerFilter([=](const auto &...) {\n\t\t\tUi::GiftReleasedByHandler(peer);\n\t\t\treturn false;\n\t\t});\n\t} else if (!isStarGift || creditsHistoryStarGift || e.soldOutInfo) {\n\t\tconstexpr auto kMinus = QChar(0x2212);\n\t\tauto &lifetime = content->lifetime();\n\t\tconst auto text = lifetime.make_state<Ui::Text::String>();\n\t\tauto minorText = (Ui::Text::String*)(nullptr);\n\t\tconst auto roundedText = e.refunded\n\t\t\t? tr::lng_channel_earn_history_return(tr::now)\n\t\t\t: e.pending\n\t\t\t? tr::lng_channel_earn_history_pending(tr::now)\n\t\t\t: e.failed\n\t\t\t? tr::lng_channel_earn_history_failed(tr::now)\n\t\t\t: QString();\n\t\tconst auto rounded = !roundedText.isEmpty()\n\t\t\t? lifetime.make_state<Ui::Text::String>(\n\t\t\t\tst::defaultTextStyle,\n\t\t\t\troundedText)\n\t\t\t: (Ui::Text::String*)(nullptr);\n\n\t\tconst auto amount = content->add(\n\t\t\tobject_ptr<Ui::FixedHeightWidget>(\n\t\t\t\tcontent,\n\t\t\t\tst::defaultTextStyle.font->height));\n\t\tauto helper = Ui::Text::CustomEmojiHelper();\n\t\tconst auto starEmoji = helper.paletteDependent(\n\t\t\tUi::Earn::IconCreditsEmoji());\n\t\tif (e.soldOutInfo) {\n\t\t\ttext->setText(\n\t\t\t\tst::defaultTextStyle,\n\t\t\t\ttr::lng_credits_box_history_entry_gift_sold_out(tr::now));\n\t\t} else if (s) {\n\t\t\ttext->setMarkedText(\n\t\t\t\tst::defaultTextStyle,\n\t\t\t\ttr::lng_credits_subscription_subtitle(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\tstarEmoji,\n\t\t\t\t\tlt_cost,\n\t\t\t\t\t{ QString::number(s.subscription.credits) },\n\t\t\t\t\ttr::marked),\n\t\t\t\tkMarkupTextOptions,\n\t\t\t\thelper.context([=] { amount->update(); }));\n\t\t} else if (e.credits.stars()) {\n\t\t\tauto t = TextWithEntities()\n\t\t\t\t.append((e.in && (creditsHistoryStarGift || !isStarGift))\n\t\t\t\t\t? QChar('+')\n\t\t\t\t\t: (e.gift && !creditsHistoryStarGift)\n\t\t\t\t\t? QChar()\n\t\t\t\t\t: kMinus)\n\t\t\t\t.append(Lang::FormatCreditsAmountDecimal(e.credits.abs()))\n\t\t\t\t.append(QChar(' '))\n\t\t\t\t.append(starEmoji);\n\t\t\ttext->setMarkedText(\n\t\t\t\tst::semiboldTextStyle,\n\t\t\t\tstd::move(t),\n\t\t\t\tkMarkupTextOptions,\n\t\t\t\thelper.context([=] { amount->update(); }));\n\t\t} else if (e.credits.ton()) {\n\t\t\tauto t = TextWithEntities()\n\t\t\t\t.append((e.in ? QChar('+') : kMinus))\n\t\t\t\t.append(Info::ChannelEarn::MajorPart(e.credits.abs()));\n\t\t\ttext->setMarkedText(\n\t\t\t\tst::channelEarnHistoryMajorLabel.style,\n\t\t\t\tstd::move(t),\n\t\t\t\tkMarkupTextOptions,\n\t\t\t\thelper.context([=] { amount->update(); }));\n\n\t\t\tauto minor = TextWithEntities()\n\t\t\t\t.append(Info::ChannelEarn::MinorPart(e.credits.abs()))\n\t\t\t\t.append(QChar(' '))\n\t\t\t\t.append(Ui::Text::IconEmoji(&st::tonIconEmojiInSmall));\n\t\t\tminorText = lifetime.make_state<Ui::Text::String>();\n\t\t\tminorText->setMarkedText(\n\t\t\t\tst::channelEarnHistoryMinorLabel.style,\n\t\t\t\tstd::move(minor),\n\t\t\t\tkMarkupTextOptions,\n\t\t\t\thelper.context([=] { amount->update(); }));\n\t\t}\n\t\tconst auto font = text->style()->font;\n\t\tconst auto roundedFont = st::defaultTextStyle.font;\n\t\tconst auto roundedSkip = roundedFont->spacew * 2;\n\t\tconst auto roundedWidth = rounded\n\t\t\t? roundedFont->width(roundedText)\n\t\t\t\t+ roundedSkip\n\t\t\t\t+ roundedFont->height\n\t\t\t: 0;\n\t\tconst auto fullWidth = text->maxWidth()\n\t\t\t+ roundedWidth\n\t\t\t+ (minorText ? minorText->maxWidth() : 0);\n\t\tamount->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = Painter(amount);\n\t\t\tp.setPen(e.soldOutInfo\n\t\t\t\t? st::menuIconAttentionColor\n\t\t\t\t: s\n\t\t\t\t? st::windowSubTextFg\n\t\t\t\t: e.pending\n\t\t\t\t? st::creditsStroke\n\t\t\t\t: (e.in || (isStarGift && !creditsHistoryStarGift))\n\t\t\t\t? st::boxTextFgGood\n\t\t\t\t: (e.gift && !creditsHistoryStarGift)\n\t\t\t\t? st::windowBoldFg\n\t\t\t\t: st::menuIconAttentionColor);\n\t\t\tconst auto x = (amount->width() - fullWidth) / 2;\n\t\t\tconst auto y = (amount->height() - font->height) / 2;\n\t\t\ttext->draw(p, Ui::Text::PaintContext{\n\t\t\t\t.position = QPoint(x, y),\n\t\t\t\t.outerWidth = amount->width(),\n\t\t\t\t.availableWidth = amount->width(),\n\t\t\t});\n\t\t\tif (minorText) {\n\t\t\t\tminorText->draw(p, Ui::Text::PaintContext{\n\t\t\t\t\t.position = QPoint(\n\t\t\t\t\t\tx + text->maxWidth(),\n\t\t\t\t\t\ty + st::lineWidth * 2),\n\t\t\t\t\t.outerWidth = amount->width(),\n\t\t\t\t\t.availableWidth = amount->width(),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (rounded) {\n\t\t\t\tconst auto roundedLeft = fullWidth\n\t\t\t\t\t+ x\n\t\t\t\t\t- roundedWidth\n\t\t\t\t\t+ roundedSkip;\n\t\t\t\tconst auto pen = p.pen();\n\t\t\t\tauto color = pen.color();\n\t\t\t\tcolor.setAlphaF(color.alphaF() * 0.15);\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(color);\n\t\t\t\t{\n\t\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\t\tp.drawRoundedRect(\n\t\t\t\t\t\troundedLeft,\n\t\t\t\t\t\t(amount->height() - roundedFont->height) / 2,\n\t\t\t\t\t\troundedWidth - roundedSkip,\n\t\t\t\t\t\troundedFont->height,\n\t\t\t\t\t\troundedFont->height / 2,\n\t\t\t\t\t\troundedFont->height / 2);\n\t\t\t\t}\n\t\t\t\tp.setPen(pen);\n\t\t\t\trounded->draw(p, Ui::Text::PaintContext{\n\t\t\t\t\t.position = QPoint(\n\t\t\t\t\t\troundedLeft + roundedFont->height / 2,\n\t\t\t\t\t\t(amount->height() - roundedFont->height) / 2),\n\t\t\t\t\t.outerWidth = roundedWidth,\n\t\t\t\t\t.availableWidth = roundedWidth,\n\t\t\t\t});\n\t\t\t}\n\t\t}, amount->lifetime());\n\t}\n\n\tif (!isStarGift && !e.description.empty()) {\n\t\tUi::AddSkip(content);\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\trpl::single(e.description),\n\t\t\t\tst::creditsBoxAbout),\n\t\t\tstyle::al_top);\n\t}\n\n\tconst auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji);\n\tif (!uniqueGift && (starGiftCanManage || e.converted)) {\n\t\tUi::AddSkip(content);\n\t\tconst auto about = box->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\t(e.giftRefunded\n\t\t\t\t\t? tr::lng_action_gift_refunded(\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: e.starsUpgradedBySender\n\t\t\t\t\t? tr::lng_action_gift_got_upgradable_text(\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: (e.starsToUpgrade\n\t\t\t\t\t\t&& giftToSelf\n\t\t\t\t\t\t&& !e.giftTransferred)\n\t\t\t\t\t? tr::lng_action_gift_self_about_unique(\n\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t: (e.starsToUpgrade\n\t\t\t\t\t\t&& giftToChannelCanManage\n\t\t\t\t\t\t&& !e.giftTransferred)\n\t\t\t\t\t? tr::lng_action_gift_channel_about_unique(\n\t\t\t\t\t\ttr::marked)\n\t\t\t\t\t: ((canConvert || e.converted)\n\t\t\t\t\t\t? rpl::combine(\n\t\t\t\t\t\t\t(canConvert\n\t\t\t\t\t\t\t\t? (giftToSelf\n\t\t\t\t\t\t\t\t\t? tr::lng_action_gift_self_about\n\t\t\t\t\t\t\t\t\t: giftToChannelCanTransfer\n\t\t\t\t\t\t\t\t\t? tr::lng_action_gift_channel_about\n\t\t\t\t\t\t\t\t\t: tr::lng_action_gift_got_stars_text)\n\t\t\t\t\t\t\t\t: (giftToChannel\n\t\t\t\t\t\t\t\t\t? tr::lng_gift_channel_got\n\t\t\t\t\t\t\t\t\t: tr::lng_gift_got_stars))(\n\t\t\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\t\t\trpl::single(e.starsConverted * 1.),\n\t\t\t\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t\t\ttr::lng_paid_about_link()\n\t\t\t\t\t\t) | rpl::map([](\n\t\t\t\t\t\t\t\tTextWithEntities text,\n\t\t\t\t\t\t\t\tQString link) {\n\t\t\t\t\t\t\treturn text.append(' ').append(\n\t\t\t\t\t\t\t\ttr::link(link));\n\t\t\t\t\t\t})\n\t\t\t\t\t\t: (e.savedToProfile\n\t\t\t\t\t\t\t? (giftToChannel\n\t\t\t\t\t\t\t\t? tr::lng_action_gift_can_remove_channel\n\t\t\t\t\t\t\t\t: tr::lng_action_gift_can_remove_text)\n\t\t\t\t\t\t\t: (giftToChannel\n\t\t\t\t\t\t\t\t? tr::lng_action_gift_got_gift_channel\n\t\t\t\t\t\t\t\t: tr::lng_action_gift_got_gift_text))(\n\t\t\t\t\t\t\t\t\ttr::marked))),\n\t\t\t\tst::creditsBoxAbout),\n\t\t\tstyle::al_top);\n\t\tabout->setClickHandlerFilter([=](const auto &...) {\n\t\t\tCore::App().iv().openWithIvPreferred(\n\t\t\t\tsession,\n\t\t\t\ttr::lng_paid_about_link_url(tr::now));\n\t\t\treturn false;\n\t\t});\n\t\tif (e.giftRefunded) {\n\t\t\tabout->setTextColorOverride(st::menuIconAttentionColor->c);\n\t\t}\n\t} else if (isStarGift) {\n\t} else if ((e.gift || isPrize) && e.credits.stars()) {\n\t\tUi::AddSkip(content);\n\t\tauto link = tr::lng_credits_box_history_entry_gift_about_link(\n\t\t\tlt_emoji,\n\t\t\trpl::single(arrow),\n\t\t\ttr::rich\n\t\t) | rpl::map([](TextWithEntities text) {\n\t\t\treturn tr::link(\n\t\t\t\tstd::move(text),\n\t\t\t\tu\"internal:stars_examples\"_q);\n\t\t});\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\t(!e.in && peer)\n\t\t\t\t\t? tr::lng_credits_box_history_entry_gift_out_about(\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\trpl::single(TextWithEntities{ peer->shortName() }),\n\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\tstd::move(link),\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: tr::lng_credits_box_history_entry_gift_in_about(\n\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\tstd::move(link),\n\t\t\t\t\t\ttr::rich),\n\t\t\t\tst::creditsBoxAbout),\n\t\t\tstyle::al_top);\n\t} else if (e.paidMessagesCommission && e.barePeerId) {\n\t\tUi::AddSkip(content);\n\t\tauto link = tr::lng_credits_paid_messages_fee_about_link(\n\t\t\tlt_emoji,\n\t\t\trpl::single(arrow),\n\t\t\ttr::rich\n\t\t) | rpl::map([id = e.barePeerId](TextWithEntities text) {\n\t\t\treturn tr::link(\n\t\t\t\tstd::move(text),\n\t\t\t\tu\"internal:edit_paid_messages_fee/\"_q + QString::number(id));\n\t\t});\n\t\tconst auto percent = 100. - (e.paidMessagesCommission / 10.);\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_credits_paid_messages_fee_about(\n\t\t\t\t\tlt_percent,\n\t\t\t\t\trpl::single(\n\t\t\t\t\t\ttr::bold(QString::number(percent) + '%')),\n\t\t\t\t\tlt_link,\n\t\t\t\t\tstd::move(link),\n\t\t\t\t\ttr::rich),\n\t\t\t\tst::creditsBoxAbout),\n\t\t\tstyle::al_top);\n\t}\n\n\tUi::AddSkip(content);\n\n\tconst auto addGiftLinkTON = [&] {\n\t\tif (!uniqueGift) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto address = !uniqueGift->giftAddress.isEmpty()\n\t\t\t? uniqueGift->giftAddress\n\t\t\t: uniqueGift->ownerAddress;\n\t\tif (address.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto label = box->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_gift_in_blockchain(\n\t\t\t\t\tlt_link,\n\t\t\t\t\ttr::lng_gift_in_blockchain_link_arrow(\n\t\t\t\t\t\tlt_arrow,\n\t\t\t\t\t\trpl::single(arrow),\n\t\t\t\t\t\ttr::link),\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::creditsBoxAboutDivider),\n\t\t\tstyle::al_top);\n\t\tlabel->setClickHandlerFilter([=](const auto &...) {\n\t\t\tUrlClickHandler::Open(TonAddressUrl(session, address));\n\t\t\treturn false;\n\t\t});\n\t};\n\n\tif (starGiftCanManage) {\n\t\taddGiftLinkTON();\n\t}\n\n\tUi::AddSkip(content);\n\n\tstruct State final {\n\t\trpl::variable<bool> confirmButtonBusy;\n\t\trpl::variable<bool> convertButtonBusy;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\n\tconst auto canToggle = starGiftCanManage\n\t\t&& !e.giftTransferred\n\t\t&& !e.giftRefunded\n\t\t&& !e.converted;\n\tconst auto toggleVisibility = [=, weak = base::make_weak(box)](bool save) {\n\t\tconst auto showSection = !e.fromGiftsList;\n\t\tconst auto savedId = EntryToSavedStarGiftId(&show->session(), e);\n\t\tconst auto done = [=](bool ok) {\n\t\t\tif (ok && showSection) {\n\t\t\t\tif (const auto window = show->resolveWindow()) {\n\t\t\t\t\twindow->showSection(\n\t\t\t\t\t\tInfo::PeerGifts::Make(window->session().user()));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tif (ok) {\n\t\t\t\t\tstrong->closeBox();\n\t\t\t\t} else {\n\t\t\t\t\tstate->confirmButtonBusy = false;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tToggleStarGiftSaved(show, savedId, save, done);\n\t};\n\n\tconst auto canUpgrade = e.stargiftId\n\t\t&& e.canUpgradeGift\n\t\t&& (e.in || giftToSelf || giftToChannelCanManage)\n\t\t&& !e.uniqueGift;\n\tconst auto canUpgradeFree = canUpgrade && (e.starsUpgradedBySender > 0);\n\tconst auto canGiftUpgrade = !e.uniqueGift\n\t\t&& !e.in\n\t\t&& !e.giftPrepayUpgradeHash.isEmpty();\n\tconst auto canRemoveDetails = UniqueGiftCanRemoveDetails(e);\n\tconst auto removeDetails = UniqueGiftRemoveDetailsHandler(show, e);\n\tconst auto upgradeGuard = std::make_shared<bool>();\n\tconst auto upgrade = [=] {\n\t\tconst auto window = show->resolveWindow();\n\t\tif (!window || *upgradeGuard || !starGiftSticker) {\n\t\t\treturn;\n\t\t}\n\t\t*upgradeGuard = true;\n\t\tconst auto savedId = EntryToSavedStarGiftId(&window->session(), e);\n\t\tconst auto openWhenDone = (giftToChannel || canGiftUpgrade)\n\t\t\t? window->session().data().peer(PeerId(e.bareGiftOwnerId)).get()\n\t\t\t: starGiftSender;\n\t\tusing namespace Ui;\n\t\tShowStarGiftUpgradeBox({\n\t\t\t.controller = window,\n\t\t\t.stargift = Data::StarGift{\n\t\t\t\t.id = e.stargiftId,\n\t\t\t\t.unique = e.uniqueGift,\n\t\t\t\t.stars = e.credits.ton() ? 0 : int(e.credits.whole()),\n\t\t\t\t.document = starGiftSticker,\n\t\t\t\t.limitedLeft = e.limitedLeft,\n\t\t\t\t.limitedCount = e.limitedCount,\n\t\t\t},\n\t\t\t.ready = [=](bool) { *upgradeGuard = false; },\n\t\t\t.upgraded = crl::guard(box, [=] { box->closeBox(); }),\n\t\t\t.peer = openWhenDone,\n\t\t\t.savedId = savedId,\n\t\t\t.giftPrepayUpgradeHash = e.giftPrepayUpgradeHash,\n\t\t\t.cost = e.starsUpgradedBySender ? 0 : e.starsToUpgrade,\n\t\t\t.canAddSender = !giftToSelf && !e.anonymous,\n\t\t\t.canAddComment = (!giftToSelf\n\t\t\t\t&& !e.anonymous\n\t\t\t\t&& e.hasGiftComment),\n\t\t\t.canAddMyComment = (giftToSelf && e.hasGiftComment),\n\t\t\t.addDetailsDefault = (giftToSelf\n\t\t\t\t|| (e.starsUpgradedBySender\n\t\t\t\t\t&& !e.giftUpgradeSeparate\n\t\t\t\t\t&& !e.anonymous)),\n\t\t});\n\t};\n\n\tif (isStarGift && e.id.isEmpty()) {\n\t\tconst auto convert = [=, weak = base::make_weak(box)] {\n\t\t\tconst auto stars = e.starsConverted;\n\t\t\tconst auto days = canConvert ? ((timeLeft + 86399) / 86400) : 0;\n\t\t\tauto text = giftToChannelCanManage\n\t\t\t\t? tr::lng_gift_convert_sure_confirm_channel(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(stars * 1.),\n\t\t\t\t\tlt_channel,\n\t\t\t\t\trpl::single(tr::bold(giftChannel->name())),\n\t\t\t\t\ttr::rich)\n\t\t\t\t: tr::lng_gift_convert_sure_confirm(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(stars * 1.),\n\t\t\t\t\tlt_user,\n\t\t\t\t\trpl::single(tr::bold(starGiftSender->shortName())),\n\t\t\t\t\ttr::rich);\n\t\t\tConfirmConvertStarGift(show, std::move(text), stars, days, [=] {\n\t\t\t\tif (state->convertButtonBusy.current()\n\t\t\t\t\t|| state->confirmButtonBusy.current()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tstate->convertButtonBusy = true;\n\t\t\t\tconst auto savedId = EntryToSavedStarGiftId(\n\t\t\t\t\t&show->session(),\n\t\t\t\t\te);\n\t\t\t\tif (stars) {\n\t\t\t\t\tconst auto done = [=](bool ok) {\n\t\t\t\t\t\tif (ok) {\n\t\t\t\t\t\t\tusing GiftAction = Data::GiftUpdate::Action;\n\t\t\t\t\t\t\tshow->session().data().notifyGiftUpdate({\n\t\t\t\t\t\t\t\t.id = savedId,\n\t\t\t\t\t\t\t\t.action = GiftAction::Convert,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\t\tif (ok) {\n\t\t\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tstate->convertButtonBusy = false;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\tConvertStarGift(show, savedId, stars, done);\n\t\t\t\t}\n\t\t\t});\n\t\t};\n\t\tAddStarGiftTable(\n\t\t\tshow,\n\t\t\tcontent,\n\t\t\tst,\n\t\t\te,\n\t\t\tupgradeSpinner,\n\t\t\tcanConvert ? convert : Fn<void()>(),\n\t\t\tcanUpgrade,\n\t\t\tcanRemoveDetails ? removeDetails : Fn<void(Fn<void()>)>());\n\t} else {\n\t\tAddCreditsHistoryEntryTable(show, content, st, e);\n\t\tAddSubscriptionEntryTable(show, content, st, s);\n\t}\n\n\tUi::AddSkip(content);\n\n\tconst auto showNextToUpgrade = e.nextToUpgradeShow;\n\tif (!isStarGift && e.credits.stars() && e.credits.value()) {\n\t\tbox->addRow(CreateCreditsTermsLabel(box), style::al_top);\n\t} else if (starGiftCanManage) {\n\t\tconst auto hiddenPhrase = giftToChannelCanManage\n\t\t\t? tr::lng_gift_hidden_hint_channel\n\t\t\t: uniqueGift\n\t\t\t? tr::lng_gift_hidden_unique\n\t\t\t: tr::lng_gift_hidden_hint;\n\t\tconst auto visiblePhrase = giftToChannelCanManage\n\t\t\t? tr::lng_gift_visible_hint_channel\n\t\t\t: tr::lng_gift_visible_hint;\n\t\tauto withShow = rpl::combine(\n\t\t\thiddenPhrase(),\n\t\t\ttr::lng_gift_visible_show_arrow(\n\t\t\t\tlt_arrow,\n\t\t\t\trpl::single(arrow),\n\t\t\t\ttr::marked)\n\t\t) | rpl::map([=](QString &&hint, const TextWithEntities &hide) {\n\t\t\treturn TextWithEntities{ std::move(hint) }.append(' ').append(\n\t\t\t\ttr::link(hide));\n\t\t});\n\t\tauto withHide = rpl::combine(\n\t\t\tvisiblePhrase(),\n\t\t\ttr::lng_gift_visible_hide_arrow(\n\t\t\t\tlt_arrow,\n\t\t\t\trpl::single(arrow),\n\t\t\t\ttr::marked)\n\t\t) | rpl::map([](QString &&hint, const TextWithEntities &hide) {\n\t\t\treturn TextWithEntities{ std::move(hint) }.append(' ').append(\n\t\t\t\ttr::link(hide));\n\t\t});\n\t\tauto text = (!e.savedToProfile\n\t\t\t&& canToggle\n\t\t\t&& (canUpgrade || showNextToUpgrade))\n\t\t\t? std::move(withShow)\n\t\t\t: !e.savedToProfile\n\t\t\t? hiddenPhrase(tr::marked)\n\t\t\t: canToggle\n\t\t\t? std::move(withHide)\n\t\t\t: visiblePhrase(tr::marked);\n\t\tif (e.anonymous && e.barePeerId && !uniqueGift) {\n\t\t\ttext = rpl::combine(\n\t\t\t\tstd::move(text),\n\t\t\t\t(giftToChannelCanManage\n\t\t\t\t\t? tr::lng_gift_anonymous_hint_channel\n\t\t\t\t\t: tr::lng_gift_anonymous_hint)()\n\t\t\t) | rpl::map([](TextWithEntities &&a, QString &&b) {\n\t\t\t\treturn a.append(\"\\n\\n\").append(b);\n\t\t\t});\n\t\t}\n\t\tconst auto label = box->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\tstd::move(text),\n\t\t\t\tst::creditsBoxAboutDivider),\n\t\t\tstyle::al_top);\n\t\tlabel->setClickHandlerFilter([=](const auto &...) {\n\t\t\ttoggleVisibility(!e.savedToProfile);\n\t\t\treturn false;\n\t\t});\n\t} else if (e.credits.stars() && e.credits.value()) {\n\t\tbox->addRow(CreateCreditsTermsLabel(box), style::al_top);\n\t} else {\n\t\taddGiftLinkTON();\n\t}\n\tif (s) {\n\t\tconst auto user = peer ? peer->asUser() : nullptr;\n\t\tconst auto bot = (user && !user->isSelf()) ? user : nullptr;\n\t\tconst auto toCancel = !s.expired && !s.cancelled && !s.cancelledByBot;\n\t\tif (toCancel) {\n\t\t\tUi::AddSkip(content);\n\t\t}\n\t\tUi::AddSkip(content);\n\t\tauto label = object_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\t((s.cancelledByBot && bot)\n\t\t\t\t? tr::lng_credits_subscription_off_by_bot_about(\n\t\t\t\t\tlt_bot,\n\t\t\t\t\trpl::single(bot->name()))\n\t\t\t\t: toCancel\n\t\t\t\t? tr::lng_credits_subscription_on_button()\n\t\t\t\t: s.cancelled\n\t\t\t\t? tr::lng_credits_subscription_off_about()\n\t\t\t\t: tr::lng_credits_subscription_on_about(\n\t\t\t\t\tlt_date,\n\t\t\t\t\trpl::single(langDayOfMonthFull(s.until.date())))),\n\t\t\tst::creditsBoxAboutDivider);\n\t\tif (toCancel) {\n\t\t\tlabel->setClickHandlerFilter([=](\n\t\t\t\t\tconst auto &,\n\t\t\t\t\tQt::MouseButton button) {\n\t\t\t\tif (button != Qt::LeftButton) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst auto done = [=, weak = base::make_weak(box)] {\n\t\t\t\t\tProcessReceivedSubscriptions(weak, session);\n\t\t\t\t};\n\t\t\t\tconst auto fail = [=, s = box->uiShow()](const QString &e) {\n\t\t\t\t\ts->showToast(e);\n\t\t\t\t};\n\t\t\t\tApi::EditCreditsSubscription(session, s.id, true, done, fail);\n\t\t\t\treturn true;\n\t\t\t});\n\t\t\tlabel->setMarkedText(\n\t\t\t\ttr::link(\n\t\t\t\t\ttr::lng_credits_subscription_on_button(tr::now),\n\t\t\t\t\tu\"internal:\"_q));\n\t\t} else if (s.cancelled || s.cancelledByBot) {\n\t\t\tlabel->setTextColorOverride(st::menuIconAttentionColor->c);\n\t\t}\n\t\tbox->addRow(std::move(label), style::al_top);\n\t}\n\n\tUi::AddSkip(content);\n\n\tif (e.peerType == Data::CreditsHistoryEntry::PeerType::PremiumBot) {\n\t\tconst auto widget = Ui::CreateChild<Ui::RpWidget>(content);\n\t\tAddMiniStars(content, widget, stUser.photoSize, st::boxWideWidth, 2);\n\t}\n\n\tconst auto rejoinByApi = base::unixtime::serialize(s.until)\n\t\t> base::unixtime::now();\n\tconst auto rejoinByInvite = !s.inviteHash.isEmpty();\n\tconst auto rejoinBySlug = !s.slug.isEmpty();\n\tconst auto toRenew = (s.cancelled || s.expired)\n\t\t&& (rejoinByApi || rejoinByInvite)\n\t\t&& !s.cancelledByBot;\n\tconst auto toRejoin = (s.cancelled || s.expired)\n\t\t&& rejoinBySlug\n\t\t&& !s.cancelledByBot;\n\t//const auto suggestUpgradeNext = uniqueGift\n\t//\t&& canToggle\n\t//\t&& e.savedToProfile;\n\tauto confirmText = rpl::conditional(\n\t\tstate->confirmButtonBusy.value(),\n\t\trpl::single(QString()),\n\t\t(toRenew\n\t\t\t? tr::lng_credits_subscription_off_button()\n\t\t\t: toRejoin\n\t\t\t? tr::lng_credits_subscription_off_rejoin_button()\n\t\t\t: e.craftAnotherCallback\n\t\t\t? tr::lng_gift_craft_another_button()\n\t\t\t: canUpgradeFree\n\t\t\t? tr::lng_gift_upgrade_free()\n\t\t\t: canUpgrade\n\t\t\t? tr::lng_gift_unique_upgrade()\n\t\t\t: canGiftUpgrade\n\t\t\t? tr::lng_gift_unique_gift_upgrade()\n\t\t\t: (canToggle && !e.savedToProfile)\n\t\t\t? (e.giftChannelSavedId\n\t\t\t\t? tr::lng_gift_show_on_channel\n\t\t\t\t: tr::lng_gift_show_on_page)()\n\t\t\t: tr::lng_box_ok()));\n\tconst auto send = [=, weak = base::make_weak(box)] {\n\t\tif (toRejoin && !toRenew) {\n\t\t\tif (const auto window = show->resolveWindow()) {\n\t\t\t\tconst auto finish = [=](Payments::CheckoutResult&&) {\n\t\t\t\t\tProcessReceivedSubscriptions(weak, session);\n\t\t\t\t};\n\t\t\t\tPayments::CheckoutProcess::Start(\n\t\t\t\t\t&window->session(),\n\t\t\t\t\ts.slug,\n\t\t\t\t\t[](auto) {},\n\t\t\t\t\tPayments::ProcessNonPanelPaymentFormFactory(\n\t\t\t\t\t\twindow,\n\t\t\t\t\t\tfinish));\n\t\t\t}\n\t\t} else if (toRenew && s.expired) {\n\t\t\tif (const auto window = show->resolveWindow()) {\n\t\t\t\tApi::CheckChatInvite(window, s.inviteHash, nullptr, [=] {\n\t\t\t\t\tProcessReceivedSubscriptions(weak, session);\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto done = [=] {\n\t\t\t\tProcessReceivedSubscriptions(weak, session);\n\t\t\t};\n\t\t\tconst auto fail = [=, show = box->uiShow()](const QString &e) {\n\t\t\t\tif ([[maybe_unused]] const auto strong = weak.get()) {\n\t\t\t\t\tstate->confirmButtonBusy = false;\n\t\t\t\t}\n\t\t\t\tshow->showToast(e);\n\t\t\t};\n\t\t\tApi::EditCreditsSubscription(session, s.id, false, done, fail);\n\t\t}\n\t};\n\n\tconst auto willBusy = toRejoin || (peer && toRenew);\n\tif (willBusy) {\n\t\tconst auto close = Ui::CreateChild<Ui::IconButton>(\n\t\t\tcontent,\n\t\t\tst::boxTitleClose);\n\t\tclose->setClickedCallback([=] { box->closeBox(); });\n\t\tcontent->widthValue() | rpl::on_next([=](int) {\n\t\t\tclose->moveToRight(0, 0);\n\t\t}, content->lifetime());\n\t}\n\n\tconst auto initButtons = [=] {\n\t\tbox->clearButtons();\n\t\tconst auto button = box->addButton(std::move(confirmText), [=] {\n\t\t\tif (showNextToUpgrade) {\n\t\t\t\tconst auto close = crl::guard(box, [=] { box->closeBox(); });\n\t\t\t\tshowNextToUpgrade();\n\t\t\t\tclose();\n\t\t\t\treturn;\n\t\t\t} else if (e.craftAnotherCallback) {\n\t\t\t\te.craftAnotherCallback();\n\t\t\t\treturn;\n\t\t\t} else if (state->confirmButtonBusy.current()\n\t\t\t\t|| state->convertButtonBusy.current()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (willBusy) {\n\t\t\t\tstate->confirmButtonBusy = true;\n\t\t\t\tsend();\n\t\t\t} else if (canBuyResold) {\n\t\t\t\tconst auto to = e.bareGiftResaleRecipientId\n\t\t\t\t\t? show->session().data().peer(\n\t\t\t\t\t\tPeerId(e.bareGiftResaleRecipientId))\n\t\t\t\t\t: show->session().user();\n\t\t\t\tShowBuyResaleGiftBox(\n\t\t\t\t\tshow,\n\t\t\t\t\te.uniqueGift,\n\t\t\t\t\te.giftResaleForceTon,\n\t\t\t\t\tto,\n\t\t\t\t\tcrl::guard(box, [=](bool) { box->closeBox(); }));\n\t\t\t} else if (canUpgrade || canGiftUpgrade) {\n\t\t\t\tupgrade();\n\t\t\t} else if (canToggle && !e.savedToProfile) {\n\t\t\t\ttoggleVisibility(true);\n\t\t\t} else {\n\t\t\t\tbox->closeBox();\n\t\t\t}\n\t\t}, showNextToUpgrade\n\t\t\t? st::giveawayGiftCodeBoxUpgradeNext\n\t\t\t: st::giveawayGiftCodeBox.button);\n\t\tif (canBuyResold) {\n\t\t\tif (uniqueGift->onlyAcceptTon || e.giftResaleForceTon) {\n\t\t\t\tbutton->setText(rpl::single(QString()));\n\t\t\t\tUi::SetButtonTwoLabels(\n\t\t\t\t\tbutton,\n\t\t\t\t\ttr::lng_gift_buy_resale_button(\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\trpl::single(Data::FormatGiftResaleTon(*uniqueGift)),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\ttr::lng_gift_buy_resale_equals(\n\t\t\t\t\t\tlt_cost,\n\t\t\t\t\t\trpl::single(Ui::Text::IconEmoji(\n\t\t\t\t\t\t\t&st::starIconEmojiSmall\n\t\t\t\t\t\t).append(Lang::FormatCountDecimal(\n\t\t\t\t\t\t\tuniqueGift->starsForResale))),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\tst::resaleButtonTitle,\n\t\t\t\t\tst::resaleButtonSubtitle);\n\t\t\t} else {\n\t\t\t\tbutton->setText(tr::lng_gift_buy_resale_button(\n\t\t\t\t\tlt_cost,\n\t\t\t\t\trpl::single(Ui::Text::IconEmoji(&st::starIconEmoji).append(\n\t\t\t\t\t\tLang::FormatCountDecimal(uniqueGift->starsForResale))),\n\t\t\t\t\ttr::marked));\n\t\t\t}\n\t\t} else if (showNextToUpgrade) {\n\t\t\tconst auto session = &show->session();\n\t\t\tconst auto sticker = e.nextToUpgradeStickerId\n\t\t\t\t? session->data().document(e.nextToUpgradeStickerId).get()\n\t\t\t\t: nullptr;\n\t\t\tconst auto document = (sticker && sticker->sticker())\n\t\t\t\t? sticker\n\t\t\t\t: nullptr;\n\t\t\tbutton->setContext(Core::TextContext({ .session = session }));\n\t\t\tbutton->setText(tr::lng_gift_unique_upgrade_next(\n\t\t\t) | rpl::map([=](const QString &text) {\n\t\t\t\tauto result = TextWithEntities{ text };\n\t\t\t\tif (document) {\n\t\t\t\t\tresult.append(' ').append(Data::SingleCustomEmoji(document));\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t}));\n\t\t}\n\t\t{\n\t\t\tusing namespace Info::Statistics;\n\t\t\tconst auto loadingAnimation = InfiniteRadialAnimationWidget(\n\t\t\t\tbutton,\n\t\t\t\tbutton->height() / 2);\n\t\t\tAddChildToWidgetCenter(button, loadingAnimation);\n\t\t\tloadingAnimation->showOn(state->confirmButtonBusy.value());\n\t\t}\n\t};\n\tif (upgradeSpinner) {\n\t\tusing SpinnerState = Data::GiftUpgradeSpinner::State;\n\t\tbox->clearButtons();\n\t\tconst auto button = box->addButton(tr::lng_create_group_skip(), [=] {\n\t\t\tinitButtons();\n\t\t\tupgradeSpinner->state = SpinnerState::FinishedModel;\n\t\t});\n\n\t\tupgradeSpinner->state.value(\n\t\t) | rpl::on_next([=](SpinnerState state) {\n\t\t\tif (state >= SpinnerState::Finished) {\n\t\t\t\tcrl::on_main(button, initButtons);\n\t\t\t}\n\t\t}, button->lifetime());\n\t} else {\n\t\tinitButtons();\n\t}\n}\n\nvoid UniqueGiftValueBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::CreditsHistoryEntry &e,\n\t\tCreditsEntryBoxStyleOverrides st) {\n\tbox->setStyle(st.box ? *st.box : st::giveawayGiftCodeBox);\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setNoContentMargin(true);\n\n\tconst auto unique = e.uniqueGift;\n\tconst auto value = unique ? unique->value : nullptr;\n\tAssert(unique && value);\n\n\tconst auto showLastPrice = (value->lastSalePrice > value->averagePrice);\n\n\tconst auto content = box->verticalLayout();\n\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\n\tstruct State final {\n\t\tDocumentData *sticker = nullptr;\n\t\tstd::shared_ptr<Data::DocumentMedia> media;\n\t\tstd::unique_ptr<Lottie::SinglePlayer> lottie;\n\t\trpl::lifetime downloadLifetime;\n\t\trpl::lifetime buyLifetime;\n\t};\n\tUi::AddSkip(content, st::creditsHistoryEntryStarGiftSpace);\n\n\tconst auto icon = Ui::CreateChild<Ui::RpWidget>(content);\n\ticon->resize(Size(st::creditsHistoryEntryStarGiftSize));\n\tconst auto state = icon->lifetime().make_state<State>();\n\tconst auto document = unique->model.document;\n\tif (document && document->sticker()) {\n\t\tconst auto origin = document->stickerOrGifOrigin();\n\t\tstate->sticker = document;\n\t\tstate->media = document->createMediaView();\n\t\tstate->media->thumbnailWanted(origin);\n\t\tstate->media->automaticLoad(origin, nullptr);\n\t\trpl::single() | rpl::then(\n\t\t\tdocument->session().downloaderTaskFinished()\n\t\t) | rpl::filter([=] {\n\t\t\treturn state->media->loaded();\n\t\t}) | rpl::on_next([=] {\n\t\t\tstate->lottie = ChatHelpers::LottiePlayerFromDocument(\n\t\t\t\tstate->media.get(),\n\t\t\t\tChatHelpers::StickerLottieSize::MessageHistory,\n\t\t\t\ticon->size(),\n\t\t\t\tLottie::Quality::High);\n\t\t\tstate->lottie->updates() | rpl::on_next([=] {\n\t\t\t\ticon->update();\n\t\t\t}, icon->lifetime());\n\t\t\tstate->downloadLifetime.destroy();\n\t\t}, state->downloadLifetime);\n\t}\n\ticon->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = Painter(icon);\n\t\tconst auto &lottie = state->lottie;\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tconst auto request = Lottie::FrameRequest{\n\t\t\t.box = icon->size() * factor,\n\t\t};\n\t\tconst auto frame = (lottie && lottie->ready())\n\t\t\t? lottie->frameInfo(request)\n\t\t\t: Lottie::Animation::FrameInfo();\n\t\tif (!frame.image.isNull()) {\n\t\t\tp.drawImage(\n\t\t\t\tQRect(QPoint(), frame.image.size() / factor),\n\t\t\t\tframe.image);\n\t\t\tif (lottie->frameIndex() < lottie->framesCount() - 1) {\n\t\t\t\tlottie->markFrameShown();\n\t\t\t}\n\t\t}\n\t}, icon->lifetime());\n\tcontent->sizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\ticon->move(\n\t\t\t(size.width() - icon->width()) / 2,\n\t\t\tst::creditsHistoryEntryStarGiftSkip);\n\t}, icon->lifetime());\n\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\n\tconst auto bubble = box->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\trpl::single(\n\t\t\t\tUi::FillAmountAndCurrency(value->valuePrice, value->currency)),\n\t\t\tst::uniqueGiftValuePrice),\n\t\tstyle::al_top);\n\tbubble->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(bubble);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setBrush(st::windowBgActive);\n\t\tp.setPen(Qt::NoPen);\n\t\tconst auto rect = bubble->rect();\n\t\tconst auto radius = std::min(rect.width(), rect.height()) / 2.;\n\t\tp.drawRoundedRect(rect, radius, radius);\n\t}, bubble->lifetime());\n\n\tUi::AddSkip(content);\n\n\tconst auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji);\n\tUi::AddSkip(content);\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\t(showLastPrice\n\t\t\t\t? tr::lng_gift_value_about_last(\n\t\t\t\t\tlt_gift,\n\t\t\t\t\trpl::single(tr::bold(\n\t\t\t\t\t\tData::UniqueGiftName(*unique))),\n\t\t\t\t\tlt_platform,\n\t\t\t\t\t(value->lastSaleFragment\n\t\t\t\t\t\t? tr::lng_gift_value_fragment\n\t\t\t\t\t\t: tr::lng_gift_value_telegram)(\n\t\t\t\t\t\t\ttr::marked),\n\t\t\t\t\ttr::rich)\n\t\t\t\t: tr::lng_gift_value_about_average(\n\t\t\t\t\tlt_gift,\n\t\t\t\t\trpl::single(tr::bold(unique->title)),\n\t\t\t\t\ttr::rich)),\n\t\t\tst::uniqueGiftValueAbout)\n\t)->setTryMakeSimilarLines(true);\n\n\tUi::AddSkip(content);\n\tUi::AddSkip(content);\n\n\tAddUniqueGiftValueTable(show, content, st, e);\n\n\tUi::AddSkip(content);\n\n\tconst auto addAvailability = [&](int count, tr::phrase<> platform) {\n\t\treturn box->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_gift_value_availability(\n\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\trpl::single(count * 1.),\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\trpl::single(Data::SingleCustomEmoji(document)),\n\t\t\t\t\tlt_platform,\n\t\t\t\t\tplatform(tr::marked),\n\t\t\t\t\tlt_arrow,\n\t\t\t\t\trpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\t\t\ttr::link),\n\t\t\t\tst::uniqueGiftValueAvailableLink,\n\t\t\t\tst::defaultPopupMenu,\n\t\t\t\tCore::TextContext({ .session = &show->session() })),\n\t\t\tst::boxRowPadding + st::uniqueGiftValueAvailableMargin,\n\t\t\tstyle::al_top);\n\t};\n\n\tif (const auto count = value->forSaleOnTelegram; count > 0) {\n\t\taddAvailability(\n\t\t\tcount,\n\t\t\ttr::lng_gift_value_telegram\n\t\t)->setClickHandlerFilter([=](const auto &...) {\n\t\t\tif (const auto window = show->resolveWindow()) {\n\t\t\t\tstate->buyLifetime = Ui::ShowStarGiftResale(\n\t\t\t\t\twindow,\n\t\t\t\t\twindow->session().user(),\n\t\t\t\t\tunique->initialGiftId,\n\t\t\t\t\tunique->title,\n\t\t\t\t\tcrl::guard(box, [=] { state->buyLifetime.destroy(); }));\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t}\n\tif (const auto count = value->forSaleOnFragment; count > 0) {\n\t\tconst auto url = value->fragmentUrl;\n\t\taddAvailability(\n\t\t\tcount,\n\t\t\ttr::lng_gift_value_fragment\n\t\t)->setClickHandlerFilter([=](const auto &...) {\n\t\t\tUrlClickHandler::Open(url);\n\t\t\treturn false;\n\t\t});\n\t}\n\n\tbox->addButton(tr::lng_box_ok(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nvoid ReceiptCreditsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst Data::CreditsHistoryEntry &e,\n\t\tconst Data::SubscriptionEntry &s) {\n\tGenericCreditsEntryBox(box, controller->uiShow(), e, s);\n}\n\nvoid GiftedCreditsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> from,\n\t\tnot_null<PeerData*> to,\n\t\tint count,\n\t\tTimeId date) {\n\tconst auto received = to->isSelf();\n\tconst auto anonymous = from->isServiceUser();\n\tconst auto peer = received ? from : to;\n\tusing PeerType = Data::CreditsHistoryEntry::PeerType;\n\tSettings::ReceiptCreditsBox(box, controller, {\n\t\t.id = QString(),\n\t\t.title = (received\n\t\t\t? tr::lng_credits_box_history_entry_gift_name\n\t\t\t: tr::lng_credits_box_history_entry_gift_sent)(tr::now),\n\t\t.date = base::unixtime::parse(date),\n\t\t.credits = CreditsAmount(count),\n\t\t.bareMsgId = uint64(),\n\t\t.barePeerId = (anonymous ? uint64() : peer->id.value),\n\t\t.peerType = (anonymous ? PeerType::Fragment : PeerType::Peer),\n\t\t.in = received,\n\t\t.gift = true,\n\t}, {});\n}\n\nvoid CreditsPrizeBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst Data::GiftCode &data,\n\t\tTimeId date) {\n\tusing Type = Data::CreditsHistoryEntry::PeerType;\n\tSettings::ReceiptCreditsBox(\n\t\tbox,\n\t\tcontroller,\n\t\tData::CreditsHistoryEntry{\n\t\t\t.id = data.slug,\n\t\t\t.title = QString(),\n\t\t\t.description = TextWithEntities(),\n\t\t\t.date = base::unixtime::parse(date),\n\t\t\t.credits = CreditsAmount(data.count),\n\t\t\t.barePeerId = data.channel\n\t\t\t\t? data.channel->id.value\n\t\t\t\t: 0,\n\t\t\t.bareGiveawayMsgId = uint64(data.giveawayMsgId.bare),\n\t\t\t.peerType = Type::Peer,\n\t\t\t.in = true,\n\t\t},\n\t\tData::SubscriptionEntry());\n}\n\nvoid GlobalStarGiftBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tconst Data::StarGift &data,\n\t\tStarGiftResaleInfo resale,\n\t\tCreditsEntryBoxStyleOverrides st) {\n\tconst auto selfId = show->session().userPeerId();\n\tconst auto ownerId = data.unique ? data.unique->ownerId.value : 0;\n\tconst auto hostId = data.unique ? data.unique->hostId.value : 0;\n\tSettings::GenericCreditsEntryBox(\n\t\tbox,\n\t\tshow,\n\t\tData::CreditsHistoryEntry{\n\t\t\t.credits = CreditsAmount(data.stars),\n\t\t\t.bareGiftStickerId = data.document->id,\n\t\t\t.bareGiftOwnerId = ownerId,\n\t\t\t.bareGiftHostId = hostId,\n\t\t\t.bareGiftResaleRecipientId = ((resale.recipientId != selfId)\n\t\t\t\t? resale.recipientId.value\n\t\t\t\t: 0),\n\t\t\t.stargiftId = data.id,\n\t\t\t.uniqueGift = data.unique,\n\t\t\t.peerType = Data::CreditsHistoryEntry::PeerType::Peer,\n\t\t\t.limitedCount = data.limitedCount,\n\t\t\t.limitedLeft = data.limitedLeft,\n\t\t\t.stargift = true,\n\t\t\t.giftResaleForceTon = resale.forceTon,\n\t\t\t.fromGiftSlug = true,\n\t\t\t.in = (ownerId == show->session().userPeerId().value),\n\t\t\t.gift = true,\n\t\t},\n\t\tData::SubscriptionEntry(),\n\t\tst);\n}\n\nData::CreditsHistoryEntry SavedStarGiftEntry(\n\t\tnot_null<PeerData*> owner,\n\t\tconst Data::SavedStarGift &data) {\n\tconst auto chatGiftPeer = data.manageId.chat();\n\tconst auto ownerId = data.info.unique\n\t\t? data.info.unique->ownerId\n\t\t: owner->id;\n\tconst auto hostId = data.info.unique\n\t\t? data.info.unique->hostId\n\t\t: PeerId();\n\treturn {\n\t\t.description = data.message,\n\t\t.date = base::unixtime::parse(data.date),\n\t\t.credits = CreditsAmount(data.info.stars),\n\t\t.bareMsgId = uint64(data.manageId.userMessageId().bare),\n\t\t.barePeerId = data.fromId.value,\n\t\t.bareGiftStickerId = data.info.document->id,\n\t\t.bareGiftOwnerId = ownerId.value,\n\t\t.bareGiftHostId = hostId.value,\n\t\t.bareActorId = data.fromId.value,\n\t\t.bareEntryOwnerId = chatGiftPeer ? chatGiftPeer->id.value : 0,\n\t\t.giftChannelSavedId = data.manageId.chatSavedId(),\n\t\t.stargiftId = data.info.id,\n\t\t.giftPrepayUpgradeHash = data.giftPrepayUpgradeHash,\n\t\t.giftTitle = data.info.resellTitle,\n\t\t.uniqueGift = data.info.unique,\n\t\t.peerType = Data::CreditsHistoryEntry::PeerType::Peer,\n\t\t.limitedCount = data.info.limitedCount,\n\t\t.limitedLeft = data.info.limitedLeft,\n\t\t.starsConverted = int(data.starsConverted),\n\t\t.starsToUpgrade = int(data.info.starsToUpgrade),\n\t\t.starsUpgradedBySender = int(data.starsUpgradedBySender),\n\t\t.starsForDetailsRemove = int(data.starsForDetailsRemove),\n\t\t.giftNumber = data.giftNum,\n\t\t.converted = false,\n\t\t.anonymous = data.anonymous,\n\t\t.stargift = true,\n\t\t.giftUpgradeSeparate = data.upgradeSeparate,\n\t\t.giftPinned = data.pinned,\n\t\t.savedToProfile = !data.hidden,\n\t\t.fromGiftsList = true,\n\t\t.canUpgradeGift = data.upgradable,\n\t\t.in = data.mine,\n\t\t.gift = true,\n\t};\n}\n\nData::SavedStarGiftId EntryToSavedStarGiftId(\n\t\tnot_null<Main::Session*> session,\n\t\tconst Data::CreditsHistoryEntry &entry) {\n\treturn (!entry.stargift || (!entry.in && !entry.giftChannelSavedId))\n\t\t? Data::SavedStarGiftId()\n\t\t: (entry.bareEntryOwnerId && entry.giftChannelSavedId)\n\t\t? Data::SavedStarGiftId::Chat(\n\t\t\tsession->data().peer(PeerId(entry.bareEntryOwnerId)),\n\t\t\tentry.giftChannelSavedId)\n\t\t: Data::SavedStarGiftId::User(MsgId(entry.bareMsgId));\n}\n\nvoid ShowSavedStarGiftBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> owner,\n\t\tconst Data::SavedStarGift &data,\n\t\tFn<std::vector<Data::CreditsHistoryEntry>()> pinned) {\n\tcontroller->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tauto entry = SavedStarGiftEntry(owner, data);\n\t\tentry.pinnedSavedGifts = std::move(pinned);\n\t\tSettings::ReceiptCreditsBox(\n\t\t\tbox,\n\t\t\tcontroller,\n\t\t\tstd::move(entry),\n\t\t\tData::SubscriptionEntry());\n\t}));\n}\n\nvoid FillSavedStarGiftMenu(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<Ui::PopupMenu*> menu,\n\t\tconst Data::CreditsHistoryEntry &e,\n\t\tSavedStarGiftMenuType type,\n\t\tCreditsEntryBoxStyleOverrides st) {\n\tFillUniqueGiftMenu(show, menu, e, type, st);\n}\n\nvoid ShowStarGiftViewBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst Data::GiftCode &data,\n\t\tFullMsgId itemId,\n\t\tstd::optional<Data::SavedStarGift> upgradeNext) {\n\tconst auto item = controller->session().data().message(itemId);\n\tif (!item) {\n\t\treturn;\n\t}\n\tconst auto peer = item->history()->peer;\n\tconst auto toChannel = peer->isServiceUser() && data.channel;\n\tconst auto incoming = !toChannel\n\t\t&& !data.auctionTo\n\t\t&& ((data.upgrade ? item->out() : !item->out())\n\t\t\t|| peer->isSelf());\n\tconst auto fromId = incoming ? peer->id : peer->session().userPeerId();\n\tconst auto toId = incoming\n\t\t? peer->session().userPeerId()\n\t\t: data.auctionTo\n\t\t? data.auctionTo->id\n\t\t: peer->id;\n\tconst auto ownerId = data.unique ? data.unique->ownerId : toId;\n\tconst auto hostId = data.unique ? data.unique->hostId : PeerId();\n\tconst auto nextToUpgradeStickerId = upgradeNext\n\t\t? upgradeNext->info.document->id\n\t\t: uint64();\n\tconst auto nextToUpgradeShow = upgradeNext\n\t\t? [=] { ShowSavedStarGiftBox(\n\t\t\tcontroller,\n\t\t\tcontroller->session().data().peer(ownerId),\n\t\t\t*upgradeNext); }\n\t\t: Fn<void()>();\n\tconst auto entry = Data::CreditsHistoryEntry{\n\t\t.id = data.slug,\n\t\t.description = data.message,\n\t\t.date = base::unixtime::parse(item->date()),\n\t\t.credits = CreditsAmount(data.count),\n\t\t.bareMsgId = uint64(data.realGiftMsgId\n\t\t\t? data.realGiftMsgId.bare\n\t\t\t: item->id.bare),\n\t\t.barePeerId = fromId.value,\n\t\t.bareGiftStickerId = data.document ? data.document->id : 0,\n\t\t.bareGiftOwnerId = ownerId.value,\n\t\t.bareGiftHostId = hostId.value,\n\t\t.bareGiftReleasedById = (data.stargiftReleasedBy\n\t\t\t? data.stargiftReleasedBy->id.value\n\t\t\t: 0),\n\t\t.bareActorId = (toChannel ? data.channelFrom->id.value : 0),\n\t\t.bareEntryOwnerId = (toChannel ? data.channel->id.value : 0),\n\t\t.giftChannelSavedId = data.channelSavedId,\n\t\t.stargiftId = data.stargiftId,\n\t\t.giftPrepayUpgradeHash = data.giftPrepayUpgradeHash,\n\t\t.giftTitle = data.giftTitle,\n\t\t.uniqueGift = data.unique,\n\t\t.nextToUpgradeStickerId = nextToUpgradeStickerId,\n\t\t.nextToUpgradeShow = std::move(nextToUpgradeShow),\n\t\t.peerType = Data::CreditsHistoryEntry::PeerType::Peer,\n\t\t.limitedCount = data.limitedCount,\n\t\t.limitedLeft = data.limitedLeft,\n\t\t.starsConverted = data.starsConverted,\n\t\t.starsToUpgrade = data.starsToUpgrade,\n\t\t.starsUpgradedBySender = data.starsUpgradedBySender,\n\t\t.starsForDetailsRemove = data.starsForDetailsRemove,\n\t\t.giftNumber = data.giftNum,\n\t\t.converted = data.converted,\n\t\t.anonymous = data.anonymous,\n\t\t.stargift = true,\n\t\t.auction = (data.auctionTo != nullptr),\n\t\t.giftTransferred = data.transferred,\n\t\t.giftRefunded = data.refunded,\n\t\t.giftUpgradeSeparate = data.upgradeSeparate,\n\t\t.giftUpgradeGifted = data.upgradeGifted,\n\t\t.savedToProfile = data.saved,\n\t\t.canUpgradeGift = data.upgradable,\n\t\t.hasGiftComment = !data.message.empty(),\n\t\t.in = incoming,\n\t\t.gift = true,\n\t};\n\tcontroller->show(Box(\n\t\tSettings::ReceiptCreditsBox,\n\t\tcontroller,\n\t\tentry,\n\t\tData::SubscriptionEntry()));\n}\n\nvoid ShowStarGiftViewBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst Data::GiftCode &data,\n\t\tFullMsgId itemId) {\n\t// Now we suggest upgrading next gift after a gift upgrade.\n\t// No need to suggest it every gift open from a chat.\n\t//\n\t//const auto item = controller->session().data().message(itemId);\n\t//if (!item) {\n\t//\treturn;\n\t//}\n\t//const auto peer = item->history()->peer;\n\t//const auto toChannel = peer->isServiceUser() && data.channel;\n\t//const auto incoming = !toChannel\n\t//\t&& (data.upgrade ? item->out() : !item->out());\n\t//const auto toId = incoming ? peer->session().userPeerId() : peer->id;\n\t//const auto ownerId = data.unique ? data.unique->ownerId : toId;\n\t//const auto owner = peer->owner().peer(ownerId);\n\t//if (data.unique && owner->canManageGifts()) {\n\t//\tconst auto weak = base::make_weak(controller);\n\t//\towner->owner().nextForUpgradeGiftRequest(owner, crl::guard(weak, [=](\n\t//\t\t\tstd::optional<Data::SavedStarGift> nextToUpgrade) {\n\t//\t\tShowStarGiftViewBox(\n\t//\t\t\tcontroller,\n\t//\t\t\tdata,\n\t//\t\t\titemId,\n\t//\t\t\tstd::move(nextToUpgrade));\n\t//\t}));\n\t//} else {\n\t\tShowStarGiftViewBox(controller, data, itemId, std::nullopt);\n\t//}\n}\n\nvoid ShowRefundInfoBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFullMsgId refundItemId) {\n\tconst auto owner = &controller->session().data();\n\tconst auto item = owner->message(refundItemId);\n\tconst auto refund = item\n\t\t? item->Get<HistoryServicePaymentRefund>()\n\t\t: nullptr;\n\tif (!refund) {\n\t\treturn;\n\t}\n\tAssert(refund->peer != nullptr);\n\tauto info = Data::CreditsHistoryEntry();\n\tinfo.id = refund->transactionId;\n\tinfo.date = base::unixtime::parse(item->date());\n\tinfo.credits = CreditsAmount(refund->amount);\n\tinfo.barePeerId = refund->peer->id.value;\n\tinfo.peerType = Data::CreditsHistoryEntry::PeerType::Peer;\n\tinfo.refunded = true;\n\tinfo.in = true;\n\tcontroller->show(Box(\n\t\t::Settings::ReceiptCreditsBox,\n\t\tcontroller,\n\t\tinfo,\n\t\tData::SubscriptionEntry{}));\n}\n\nobject_ptr<Ui::RpWidget> GenericEntryPhoto(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tFn<Fn<void(Painter &, int, int, int, int)>(Fn<void()>)> callback,\n\t\tint photoSize) {\n\tauto owned = object_ptr<Ui::RpWidget>(parent);\n\tconst auto widget = owned.data();\n\twidget->resize(Size(photoSize));\n\twidget->setNaturalWidth(photoSize);\n\n\tconst auto draw = callback(\n\t\tcrl::guard(widget, [=] { widget->update(); }));\n\twidget->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = Painter(widget);\n\t\tdraw(p, 0, 0, photoSize, photoSize);\n\t}, widget->lifetime());\n\n\treturn owned;\n}\n\nobject_ptr<Ui::RpWidget> HistoryEntryPhoto(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<PhotoData*> photo,\n\t\tint photoSize) {\n\treturn GenericEntryPhoto(\n\t\tparent,\n\t\t[=](Fn<void()> update) {\n\t\t\treturn Ui::GenerateCreditsPaintEntryCallback(photo, update);\n\t\t},\n\t\tphotoSize);\n}\n\nobject_ptr<Ui::RpWidget> PaidMediaThumbnail(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<PhotoData*> photo,\n\t\tPhotoData *second,\n\t\tint totalCount,\n\t\tint photoSize) {\n\treturn GenericEntryPhoto(\n\t\tparent,\n\t\t[=](Fn<void()> update) {\n\t\t\treturn Ui::GeneratePaidMediaPaintCallback(\n\t\t\t\tphoto,\n\t\t\t\tsecond,\n\t\t\t\ttotalCount,\n\t\t\t\tupdate);\n\t\t},\n\t\tphotoSize);\n}\n\nobject_ptr<Ui::RpWidget> SubscriptionUserpic(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<PeerData*> peer,\n\t\tint photoSize) {\n\tauto widget = object_ptr<Ui::RpWidget>(parent);\n\tconst auto raw = widget.data();\n\twidget->resize(photoSize, photoSize);\n\twidget->setNaturalWidth(photoSize);\n\tconst auto userpicMedia = Ui::MakeUserpicThumbnail(peer, false);\n\tuserpicMedia->subscribeToUpdates([=] { raw->update(); });\n\tconst auto creditsIconSize = photoSize / 3;\n\tconst auto creditsIconCallback =\n\t\tUi::PaintOutlinedColoredCreditsIconCallback(\n\t\t\tcreditsIconSize,\n\t\t\t1.5);\n\twidget->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\tp.fillRect(Rect(Size(photoSize)), Qt::transparent);\n\t\tauto image = userpicMedia->image(photoSize);\n\t\t{\n\t\t\tauto q = QPainter(&image);\n\t\t\tq.translate(photoSize, photoSize);\n\t\t\tq.translate(-creditsIconSize, -creditsIconSize);\n\t\t\tcreditsIconCallback(q);\n\t\t}\n\t\tp.drawImage(0, 0, image);\n\t}, widget->lifetime());\n\treturn widget;\n}\n\nvoid SmallBalanceBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tuint64 wholeCredits,\n\t\tSmallBalanceSource source,\n\t\tFn<void()> paid) {\n\tExpects(show->session().credits().loaded());\n\n\tauto credits = CreditsAmount(wholeCredits);\n\n\tbox->setWidth(st::boxWideWidth);\n\tbox->addButton(tr::lng_close(), [=] { box->closeBox(); });\n\tconst auto done = [=] {\n\t\tbox->closeBox();\n\t\tpaid();\n\t};\n\n\tauto dark = false;\n\tconst auto owner = &show->session().data();\n\tconst auto name = v::match(source, [&](SmallBalanceBot value) {\n\t\treturn value.botId\n\t\t\t? owner->peer(peerFromUser(value.botId))->name()\n\t\t\t: QString();\n\t}, [&](SmallBalanceReaction value) {\n\t\treturn owner->peer(peerFromChannel(value.channelId))->name();\n\t}, [&](SmallBalanceVideoStream value) {\n\t\tdark = true;\n\t\treturn owner->peer(value.streamerId)->name();\n\t}, [](SmallBalanceSubscription value) {\n\t\treturn value.name;\n\t}, [](SmallBalanceDeepLink) {\n\t\treturn QString();\n\t}, [&](SmallBalanceStarGift value) {\n\t\treturn owner->peer(value.recipientId)->shortName();\n\t}, [&](SmallBalanceForMessage value) {\n\t\treturn value.recipientId\n\t\t\t? owner->peer(value.recipientId)->shortName()\n\t\t\t: QString();\n\t}, [&](SmallBalanceForSuggest value) {\n\t\treturn value.recipientId\n\t\t\t? owner->peer(value.recipientId)->shortName()\n\t\t\t: QString();\n\t}, [](SmallBalanceForOffer) {\n\t\treturn QString();\n\t}, [](SmallBalanceForSearch) {\n\t\treturn QString();\n\t});\n\n\tauto needed = show->session().credits().balanceValue(\n\t) | rpl::map([=](CreditsAmount balance) {\n\t\treturn (balance < credits) ? (credits - balance) : CreditsAmount();\n\t});\n\tconst auto content = [&]() -> Ui::Premium::TopBarAbstract* {\n\t\treturn box->setPinnedToTopContent(object_ptr<Ui::Premium::TopBar>(\n\t\t\tbox,\n\t\t\t(dark\n\t\t\t\t? st::videoStreamStarsCover\n\t\t\t\t: st::creditsLowBalancePremiumCover),\n\t\t\tUi::Premium::TopBarDescriptor{\n\t\t\t\t.title = tr::lng_credits_small_balance_title(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::duplicate(\n\t\t\t\t\t\tneeded\n\t\t\t\t\t) | rpl::filter(\n\t\t\t\t\t\trpl::mappers::_1 > CreditsAmount(0)\n\t\t\t\t\t) | rpl::map([](CreditsAmount amount) {\n\t\t\t\t\t\treturn amount.value();\n\t\t\t\t\t})),\n\t\t\t\t.about = (v::is<SmallBalanceSubscription>(source)\n\t\t\t\t\t? tr::lng_credits_small_balance_subscribe(\n\t\t\t\t\t\tlt_channel,\n\t\t\t\t\t\trpl::single(tr::bold(name)),\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: v::is<SmallBalanceReaction>(source)\n\t\t\t\t\t? tr::lng_credits_small_balance_reaction(\n\t\t\t\t\t\tlt_channel,\n\t\t\t\t\t\trpl::single(tr::bold(name)),\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: v::is<SmallBalanceVideoStream>(source)\n\t\t\t\t\t? tr::lng_credits_small_balance_video_stream(\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\trpl::single(tr::bold(name)),\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: v::is<SmallBalanceDeepLink>(source)\n\t\t\t\t\t? DeepLinkBalanceAbout(\n\t\t\t\t\t\tv::get<SmallBalanceDeepLink>(source).purpose)\n\t\t\t\t\t: v::is<SmallBalanceStarGift>(source)\n\t\t\t\t\t? tr::lng_credits_small_balance_star_gift(\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\trpl::single(tr::bold(name)),\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: v::is<SmallBalanceForMessage>(source)\n\t\t\t\t\t? (name.isEmpty()\n\t\t\t\t\t\t? tr::lng_credits_small_balance_for_messages(\n\t\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t\t: tr::lng_credits_small_balance_for_message(\n\t\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\t\trpl::single(tr::bold(name)),\n\t\t\t\t\t\t\ttr::rich))\n\t\t\t\t\t: v::is<SmallBalanceForSuggest>(source)\n\t\t\t\t\t? tr::lng_credits_small_balance_for_suggest(\n\t\t\t\t\t\tlt_channel,\n\t\t\t\t\t\trpl::single(tr::bold(name)),\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: v::is<SmallBalanceForOffer>(source)\n\t\t\t\t\t? tr::lng_credits_small_balance_for_offer(tr::rich)\n\t\t\t\t\t: v::is<SmallBalanceForSearch>(source)\n\t\t\t\t\t? tr::lng_credits_small_balance_for_search(\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: name.isEmpty()\n\t\t\t\t\t? tr::lng_credits_small_balance_fallback(\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: tr::lng_credits_small_balance_about(\n\t\t\t\t\t\tlt_bot,\n\t\t\t\t\t\trpl::single(TextWithEntities{ name }),\n\t\t\t\t\t\ttr::rich)),\n\t\t\t\t.light = true,\n\t\t\t\t.gradientStops = Ui::Premium::CreditsIconGradientStops(),\n\t\t\t}));\n\t}();\n\n\tconst auto peerIfBotOrChannel = [owner](PeerId id) -> PeerId {\n\t\tif (!id) {\n\t\t\treturn PeerId();\n\t\t}\n\t\tconst auto peer = owner->peer(id);\n\t\tif (const auto broadcast = peer->monoforumBroadcast()) {\n\t\t\treturn broadcast->id;\n\t\t} else if (!peer->isBot() && !peer->isChannel()) {\n\t\t\treturn PeerId();\n\t\t}\n\t\treturn id;\n\t};\n\tconst auto purposePeerId = v::match(source, [](SmallBalanceBot value) {\n\t\treturn value.botId ? peerFromUser(value.botId) : PeerId();\n\t}, [](SmallBalanceReaction value) {\n\t\treturn value.channelId ? peerFromChannel(value.channelId) : PeerId();\n\t}, [=](SmallBalanceVideoStream value) {\n\t\treturn peerIfBotOrChannel(value.streamerId);\n\t}, [](SmallBalanceSubscription) {\n\t\treturn PeerId();\n\t}, [](SmallBalanceDeepLink) {\n\t\treturn PeerId();\n\t}, [](SmallBalanceStarGift) {\n\t\treturn PeerId();\n\t}, [=](SmallBalanceForMessage value) {\n\t\treturn peerIfBotOrChannel(value.recipientId);\n\t}, [=](SmallBalanceForSuggest value) {\n\t\treturn peerIfBotOrChannel(value.recipientId);\n\t}, [](SmallBalanceForOffer) {\n\t\treturn PeerId();\n\t}, [](SmallBalanceForSearch) {\n\t\treturn PeerId();\n\t});\n\n\tFillCreditOptions(\n\t\tshow,\n\t\tbox->verticalLayout(),\n\t\tshow->session().user(),\n\t\tcredits - show->session().credits().balance(),\n\t\t[=] { show->session().credits().load(true); },\n\t\tbox->showFinishes(),\n\t\ttr::lng_credits_summary_options_subtitle(),\n\t\t{},\n\t\tdark,\n\t\tpurposePeerId);\n\n\tcontent->setMaximumHeight(st::creditsLowBalancePremiumCoverHeight);\n\tcontent->setMinimumHeight(st::infoLayerTopBarHeight);\n\n\tcontent->resize(content->width(), content->maximumHeight());\n\tcontent->additionalHeight(\n\t) | rpl::on_next([=](int additionalHeight) {\n\t\tconst auto wasMax = (content->height() == content->maximumHeight());\n\t\tcontent->setMaximumHeight(st::creditsLowBalancePremiumCoverHeight\n\t\t\t+ additionalHeight);\n\t\tif (wasMax) {\n\t\t\tcontent->resize(content->width(), content->maximumHeight());\n\t\t}\n\t}, content->lifetime());\n\n\t{\n\t\tconst auto balance = AddBalanceWidget(\n\t\t\tcontent,\n\t\t\t&show->session(),\n\t\t\tshow->session().credits().balanceValue(),\n\t\t\ttrue,\n\t\t\tnullptr,\n\t\t\tdark);\n\t\tshow->session().credits().load(true);\n\n\t\trpl::combine(\n\t\t\tbalance->sizeValue(),\n\t\t\tcontent->sizeValue()\n\t\t) | rpl::on_next([=](const QSize &, const QSize &) {\n\t\t\tbalance->moveToRight(\n\t\t\t\tst::creditsHistoryRightSkip * 2,\n\t\t\t\tst::creditsHistoryRightSkip);\n\t\t\tbalance->update();\n\t\t}, balance->lifetime());\n\t}\n\n\tstd::move(\n\t\tneeded\n\t) | rpl::filter(\n\t\t!rpl::mappers::_1\n\t) | rpl::on_next(done, content->lifetime());\n}\n\nvoid AddWithdrawalWidget(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\trpl::producer<QString> secondButtonUrl,\n\t\trpl::producer<CreditsAmount> availableBalanceValue,\n\t\trpl::producer<QDateTime> dateValue,\n\t\tbool withdrawalEnabled,\n\t\trpl::producer<QString> usdValue) {\n\tUi::AddSkip(container);\n\n\tconst auto labels = container->add(\n\t\tobject_ptr<Ui::RpWidget>(container),\n\t\tstyle::al_top);\n\n\tconst auto majorLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\tlabels,\n\t\trpl::duplicate(\n\t\t\tavailableBalanceValue\n\t\t) | rpl::map([](CreditsAmount v) {\n\t\t\treturn Lang::FormatCreditsAmountDecimal(v);\n\t\t}),\n\t\tst::channelEarnBalanceMajorLabel);\n\tconst auto icon = Ui::CreateSingleStarWidget(\n\t\tlabels,\n\t\tmajorLabel->height());\n\tmajorLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tmajorLabel->sizeValue(\n\t) | rpl::on_next([=](const QSize &majorSize) {\n\t\tconst auto skip = st::channelEarnBalanceMinorLabelSkip;\n\t\tlabels->resize(\n\t\t\tmajorSize.width() + icon->width() + skip,\n\t\t\tmajorSize.height());\n\t\tlabels->setNaturalWidth(majorSize.width() + icon->width() + skip);\n\t\tmajorLabel->moveToLeft(icon->width() + skip, 0);\n\t}, labels->lifetime());\n\tUi::ToggleChildrenVisibility(labels, true);\n\n\tUi::AddSkip(container);\n\tcontainer->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\tstd::move(usdValue),\n\t\t\tst::channelEarnOverviewSubMinorLabel),\n\t\tstyle::al_top);\n\n\tUi::AddSkip(container);\n\n\tconst auto withdrawalWrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tconst auto starsWithdrawMax = CreditsAmount(\n\t\tcontroller->session().appConfig().starsWithdrawMax());\n\tconst auto input = Ui::AddInputFieldForCredits(\n\t\twithdrawalWrap->entity(),\n\t\trpl::duplicate(\n\t\t\tavailableBalanceValue\n\t\t) | rpl::map([=](CreditsAmount amount) {\n\t\t\treturn (amount > starsWithdrawMax) ? starsWithdrawMax : amount;\n\t\t}));\n\n\tUi::AddSkip(withdrawalWrap->entity());\n\tUi::AddSkip(withdrawalWrap->entity());\n\n\tconst auto &stButton = st::defaultActiveButton;\n\tconst auto buttonsContainer = withdrawalWrap->entity()->add(\n\t\tUi::CreateSkipWidget(withdrawalWrap->entity(), stButton.height),\n\t\tst::boxRowPadding);\n\n\tconst auto button = Ui::CreateChild<Ui::RoundButton>(\n\t\tbuttonsContainer,\n\t\trpl::never<QString>(),\n\t\tstButton);\n\tbutton->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\n\tconst auto buttonCredits = Ui::CreateChild<Ui::RoundButton>(\n\t\tbuttonsContainer,\n\t\ttr::lng_bot_earn_balance_button_buy_ads(),\n\t\tstButton);\n\t{\n\t\tconst auto icon = Ui::CreateChild<Ui::RpWidget>(buttonCredits);\n\t\tconst auto &st = st::msgBotKbUrlIcon;\n\t\ticon->resize(st.width(), st.height());\n\t\ticon->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(icon);\n\t\t\tst.paint(p, { 0, 0 }, icon->width(), stButton.textFg->c);\n\t\t}, icon->lifetime());\n\t\tbuttonCredits->sizeValue(\n\t\t) | rpl::on_next([=, padding = st::msgBotKbIconPadding] {\n\t\t\ticon->moveToRight(padding, padding);\n\t\t}, icon->lifetime());\n\t\ticon->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t}\n\n\tUi::ToggleChildrenVisibility(buttonsContainer, true);\n\n\tconst auto updateButtonState = [=](bool disabled) {\n\t\tbutton->setBrushOverride(disabled\n\t\t\t? std::optional(st::windowSubTextFg)\n\t\t\t: std::nullopt);\n\t\tbutton->setAttribute(Qt::WA_TransparentForMouseEvents, disabled);\n\t};\n\n\tstruct UrlState {\n\t\tQString url;\n\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t};\n\tconst auto urlState = buttonsContainer->lifetime().make_state<UrlState>();\n\n\trpl::combine(\n\t\tstd::move(secondButtonUrl),\n\t\tbuttonsContainer->sizeValue()\n\t) | rpl::on_next([=](const QString &url, const QSize &size) {\n\t\tconst auto secondVisible = !url.isEmpty();\n\t\turlState->url = url;\n\t\twithdrawalWrap->toggle(\n\t\t\twithdrawalEnabled || secondVisible,\n\t\t\tanim::type::instant);\n\t\tupdateButtonState(!withdrawalEnabled);\n\t\tif (!secondVisible) {\n\t\t\tbutton->resize(size.width(), size.height());\n\t\t\tbuttonCredits->resize(0, 0);\n\t\t} else {\n\t\t\tconst auto w = size.width() - st::boxRowPadding.left() / 2;\n\t\t\tbutton->resize(w / 2, size.height());\n\t\t\tbuttonCredits->resize(w / 2, size.height());\n\t\t\tbuttonCredits->moveToRight(0, 0);\n\t\t\tbuttonCredits->setClickedCallback([=] {\n\t\t\t\tUrlClickHandler::Open(url);\n\t\t\t});\n\t\t\tbuttonCredits->events(\n\t\t\t) | rpl::filter([](not_null<QEvent*> e) {\n\t\t\t\treturn e->type() == QEvent::ContextMenu;\n\t\t\t}) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\t\t\tif (urlState->url.isEmpty()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\turlState->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\t\tbuttonCredits,\n\t\t\t\t\tst::popupMenuWithIcons);\n\t\t\t\turlState->menu->addAction(\n\t\t\t\t\ttr::lng_context_copy_link(tr::now),\n\t\t\t\t\t[=, show = controller->uiShow()] {\n\t\t\t\t\t\tTextUtilities::SetClipboardText({ urlState->url });\n\t\t\t\t\t\tshow->showToast(\n\t\t\t\t\t\t\ttr::lng_channel_public_link_copied(tr::now));\n\t\t\t\t\t},\n\t\t\t\t\t&st::menuIconCopy);\n\t\t\t\turlState->menu->popup(QCursor::pos());\n\t\t\t\te->accept();\n\t\t\t}, buttonCredits->lifetime());\n\t\t}\n\t}, buttonsContainer->lifetime());\n\n\tauto lockedValue = rpl::duplicate(\n\t\tdateValue\n\t) | rpl::map([](const QDateTime &dt) { return !dt.isNull(); });\n\n\trpl::duplicate(\n\t\tlockedValue\n\t) | rpl::on_next([=](bool v) {\n\t\tif (withdrawalEnabled) {\n\t\t\tupdateButtonState(v);\n\t\t}\n\t}, button->lifetime());\n\n\tconst auto session = &controller->session();\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton,\n\t\ttr::lng_channel_earn_balance_button(tr::now),\n\t\tst::channelEarnSemiboldLabel);\n\tconst auto processInputChange = [&] {\n\t\tusing Balance = rpl::variable<CreditsAmount>;\n\t\tconst auto currentBalance = input->lifetime().make_state<Balance>(\n\t\t\trpl::duplicate(availableBalanceValue));\n\t\tconst auto process = [=] {\n\t\t\tconst auto amount = input->getLastText().toDouble();\n\t\t\tif (amount >= currentBalance->current().value()) {\n\t\t\t\tlabel->setText(\n\t\t\t\t\ttr::lng_bot_earn_balance_button_all(tr::now));\n\t\t\t} else {\n\t\t\t\tlabel->setMarkedText(\n\t\t\t\t\ttr::lng_bot_earn_balance_button(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tamount,\n\t\t\t\t\t\tlt_emoji,\n\t\t\t\t\t\tUi::Text::IconEmoji(&st::starIconEmojiLarge),\n\t\t\t\t\t\ttr::rich));\n\t\t\t}\n\t\t};\n\t\tQObject::connect(input, &Ui::MaskedInputField::changed, process);\n\t\tprocess();\n\t\treturn process;\n\t}();\n\tlabel->setTextColorOverride(stButton.textFg->c);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\trpl::combine(\n\t\trpl::duplicate(lockedValue),\n\t\tbutton->sizeValue(),\n\t\tlabel->sizeValue()\n\t) | rpl::on_next([=](bool v, const QSize &b, const QSize &l) {\n\t\tlabel->moveToLeft(\n\t\t\t(b.width() - l.width()) / 2,\n\t\t\t(v ? -10 : 1) * (b.height() - l.height()) / 2);\n\t}, label->lifetime());\n\n\tconst auto lockedColor = anim::with_alpha(stButton.textFg->c, .5);\n\tconst auto lockedLabel = Ui::CreateChild<Ui::RpWidget>(button);\n\tlockedLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tstruct LockedState final {\n\t\tUi::Text::String text;\n\t\tbool locked = false;\n\t\tbool dateIsNull = false;\n\t\trpl::lifetime dateUpdateLifetime;\n\t};\n\tconst auto state = lockedLabel->lifetime().make_state<LockedState>();\n\trpl::combine(\n\t\trpl::duplicate(lockedValue),\n\t\tbutton->sizeValue()\n\t) | rpl::on_next([=](bool locked, const QSize &s) {\n\t\tstate->locked = locked;\n\t\tlockedLabel->resize(s);\n\t}, lockedLabel->lifetime());\n\tlockedLabel->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(lockedLabel);\n\t\tp.setPen(state->locked ? QPen(lockedColor) : stButton.textFg->p);\n\t\tif (state->dateIsNull && state->locked) {\n\t\t\tp.setFont(st::channelEarnSemiboldLabel.style.font);\n\t\t\tp.drawText(\n\t\t\t\tlockedLabel->rect(),\n\t\t\t\tstyle::al_center,\n\t\t\t\ttr::lng_bot_earn_balance_button_locked(tr::now));\n\t\t\treturn;\n\t\t}\n\t\tstate->text.draw(p, {\n\t\t\t.position = QPoint(\n\t\t\t\t0,\n\t\t\t\t(lockedLabel->height() - state->text.minHeight()) / 2),\n\t\t\t.outerWidth = lockedLabel->width(),\n\t\t\t.availableWidth = lockedLabel->width(),\n\t\t\t.align = style::al_center,\n\t\t});\n\t}, lockedLabel->lifetime());\n\n\tstd::move(\n\t\tdateValue\n\t) | rpl::on_next([=](const QDateTime &dt) {\n\t\tstate->dateUpdateLifetime.destroy();\n\t\tstate->dateIsNull = dt.isNull();\n\t\tif (dt.isNull()) {\n\t\t\treturn;\n\t\t}\n\t\tconstexpr auto kDateUpdateInterval = crl::time(250);\n\t\tconst auto was = base::unixtime::serialize(dt);\n\n\t\tconst auto context = Ui::Text::MarkedContext{\n\t\t\t.repaint = [=] { lockedLabel->update(); },\n\t\t};\n\t\tconst auto emoji = Ui::Text::IconEmoji(&st::botEarnButtonLock);\n\n\t\trpl::single(\n\t\t\trpl::empty\n\t\t) | rpl::then(\n\t\t\tbase::timer_each(kDateUpdateInterval)\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto secondsDifference = std::max(\n\t\t\t\twas - base::unixtime::now() - 1,\n\t\t\t\t0);\n\t\t\tconst auto hours = secondsDifference / 3600;\n\t\t\tconst auto minutes = (secondsDifference % 3600) / 60;\n\t\t\tconst auto seconds = secondsDifference % 60;\n\t\t\tconstexpr auto kZero = QChar('0');\n\t\t\tconst auto formatted = (hours > 0)\n\t\t\t\t? (u\"%1:%2:%3\"_q)\n\t\t\t\t\t.arg(hours, 2, 10, kZero)\n\t\t\t\t\t.arg(minutes, 2, 10, kZero)\n\t\t\t\t\t.arg(seconds, 2, 10, kZero)\n\t\t\t\t: (u\"%1:%2\"_q)\n\t\t\t\t\t.arg(minutes, 2, 10, kZero)\n\t\t\t\t\t.arg(seconds, 2, 10, kZero);\n\t\t\tstate->text.setMarkedText(\n\t\t\t\tst::botEarnLockedButtonLabel.style,\n\t\t\t\tTextWithEntities()\n\t\t\t\t\t.append(tr::lng_bot_earn_balance_button_locked(tr::now))\n\t\t\t\t\t.append('\\n')\n\t\t\t\t\t.append(emoji)\n\t\t\t\t\t.append(formatted),\n\t\t\t\tkMarkupTextOptions,\n\t\t\t\tcontext);\n\t\t\tlockedLabel->update();\n\t\t}, state->dateUpdateLifetime);\n\t}, lockedLabel->lifetime());\n\n\tApi::HandleWithdrawalButton(\n\t\tApi::RewardReceiver{\n\t\t\t.creditsReceiver = peer,\n\t\t\t.creditsAmount = [=, show = controller->uiShow()] {\n\t\t\t\tconst auto amount = input->getLastText().toULongLong();\n\t\t\t\tconst auto min = float64(WithdrawalMin(session));\n\t\t\t\tif (amount < min) {\n\t\t\t\t\tauto text = tr::lng_bot_earn_credits_out_minimal(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\ttr::link(\n\t\t\t\t\t\t\ttr::lng_bot_earn_credits_out_minimal_link(\n\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\t\tmin),\n\t\t\t\t\t\t\tu\"internal:\"_q),\n\t\t\t\t\t\ttr::rich);\n\t\t\t\t\tshow->showToast(Ui::Toast::Config{\n\t\t\t\t\t\t.text = std::move(text),\n\t\t\t\t\t\t.filter = [=](const auto ...) {\n\t\t\t\t\t\t\tinput->setText(QString::number(min));\n\t\t\t\t\t\t\tprocessInputChange();\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\treturn 0ULL;\n\t\t\t\t}\n\t\t\t\treturn amount;\n\t\t\t},\n\t\t},\n\t\tbutton,\n\t\tcontroller->uiShow());\n\tUi::ToggleChildrenVisibility(button, true);\n\n\tUi::AddSkip(container);\n\tUi::AddSkip(container);\n\n\tauto about = object_ptr<Ui::FlatLabel>(\n\t\tcontainer,\n\t\t(peer->isSelf()\n\t\t\t? tr::lng_self_earn_learn_credits_out_about\n\t\t\t: tr::lng_bot_earn_learn_credits_out_about)(\n\t\t\t\tlt_link,\n\t\t\t\ttr::lng_channel_earn_about_link(\n\t\t\t\t\tlt_emoji,\n\t\t\t\t\trpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\t\t\ttr::rich\n\t\t\t\t) | rpl::map([](TextWithEntities text) {\n\t\t\t\t\treturn tr::link(\n\t\t\t\t\t\tstd::move(text),\n\t\t\t\t\t\ttr::lng_bot_earn_balance_about_url(tr::now));\n\t\t\t\t}),\n\t\t\ttr::rich),\n\t\tst::boxDividerLabel);\n\tUi::AddSkip(container);\n\tcontainer->add(object_ptr<Ui::DividerLabel>(\n\t\tcontainer,\n\t\tstd::move(about),\n\t\tst::defaultBoxDividerLabelPadding));\n\n\tUi::AddSkip(container);\n}\n\nvoid MaybeRequestBalanceIncrease(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tuint64 credits,\n\t\tSmallBalanceSource source,\n\t\tFn<void(SmallBalanceResult)> done) {\n\tstruct State {\n\t\trpl::lifetime lifetime;\n\t\tbool success = false;\n\t};\n\tconst auto state = std::make_shared<State>();\n\n\tconst auto session = &show->session();\n\tsession->credits().load();\n\tsession->credits().loadedValue(\n\t) | rpl::filter(rpl::mappers::_1) | rpl::on_next([=] {\n\t\tstate->lifetime.destroy();\n\n\t\tconst auto balance = session->credits().balance();\n\t\tif (CreditsAmount(credits) <= balance) {\n\t\t\tif (const auto onstack = done) {\n\t\t\t\tonstack(SmallBalanceResult::Already);\n\t\t\t}\n\t\t} else if (show->session().premiumPossible()) {\n\t\t\tconst auto success = [=] {\n\t\t\t\tstate->success = true;\n\t\t\t\tif (const auto onstack = done) {\n\t\t\t\t\tonstack(SmallBalanceResult::Success);\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto box = show->show(Box(\n\t\t\t\tSettings::SmallBalanceBox,\n\t\t\t\tshow,\n\t\t\t\tcredits,\n\t\t\t\tsource,\n\t\t\t\tsuccess));\n\t\t\tbox->boxClosing() | rpl::on_next([=] {\n\t\t\t\tcrl::on_main([=] {\n\t\t\t\t\tif (!state->success) {\n\t\t\t\t\t\tif (const auto onstack = done) {\n\t\t\t\t\t\t\tonstack(SmallBalanceResult::Cancelled);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}, box->lifetime());\n\t\t} else {\n\t\t\tshow->showToast(\n\t\t\t\ttr::lng_credits_purchase_blocked(tr::now));\n\t\t\tif (const auto onstack = done) {\n\t\t\t\tonstack(SmallBalanceResult::Blocked);\n\t\t\t}\n\t\t}\n\t}, state->lifetime);\n}\n\nvoid AddUniqueCloseMoreButton(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tSettings::CreditsEntryBoxStyleOverrides st,\n\t\tFn<void(not_null<Ui::PopupMenu*>)> fillMenu,\n\t\tFn<void()> launchCraft) {\n\tconst auto close = Ui::CreateChild<Ui::IconButton>(\n\t\tbox,\n\t\tst::uniqueCloseButton);\n\tconst auto menu = fillMenu\n\t\t? Ui::CreateChild<Ui::IconButton>(box, st::uniqueMenuButton)\n\t\t: nullptr;\n\tconst auto craft = launchCraft\n\t\t? Ui::CreateChild<Ui::IconButton>(box, st::uniqueCraftButton)\n\t\t: nullptr;\n\tclose->show();\n\tclose->raise();\n\tif (menu) {\n\t\tmenu->show();\n\t\tmenu->raise();\n\t}\n\tif (craft) {\n\t\tcraft->show();\n\t\tcraft->raise();\n\t}\n\tbox->widthValue() | rpl::on_next([=](int width) {\n\t\tauto right = 0;\n\t\tclose->moveToRight(right, 0, width);\n\t\tclose->raise();\n\t\tright += close->width();\n\t\tif (menu) {\n\t\t\tmenu->moveToRight(right, 0, width);\n\t\t\tmenu->raise();\n\t\t\tright += menu->width();\n\t\t}\n\t\tif (craft) {\n\t\t\tcraft->moveToRight(right, 0, width);\n\t\t\tcraft->raise();\n\t\t}\n\t}, close->lifetime());\n\tclose->setClickedCallback([=] {\n\t\tbox->closeBox();\n\t});\n\tif (menu) {\n\t\tconst auto state = menu->lifetime().make_state<\n\t\t\tbase::unique_qptr<Ui::PopupMenu>\n\t\t>();\n\t\tmenu->setClickedCallback([=] {\n\t\t\tif (*state) {\n\t\t\t\t*state = nullptr;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t*state = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\tmenu,\n\t\t\t\tst.menu ? *st.menu : st::popupMenuWithIcons);\n\t\t\tfillMenu(state->get());\n\t\t\tif (!(*state)->empty()) {\n\t\t\t\t(*state)->popup(QCursor::pos());\n\t\t\t}\n\t\t});\n\t}\n\tif (craft) {\n\t\tcraft->setClickedCallback(std::move(launchCraft));\n\t}\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_credits_graphics.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\ntemplate <typename Object>\nclass object_ptr;\n\nclass PeerData;\nstruct ShareBoxStyleOverrides;\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nstruct Boost;\nstruct CreditsHistoryEntry;\nstruct SubscriptionEntry;\nstruct GiftCode;\nstruct CreditTopupOption;\nstruct SavedStarGift;\nclass SavedStarGiftId;\nstruct StarGift;\nstruct UniqueGift;\nstruct GiftUpgradeSpinner;\n} // namespace Data\n\nnamespace HistoryView {\nstruct ScheduleBoxStyleArgs;\n} // namespace HistoryView\n\nnamespace Main {\nclass Session;\nclass SessionShow;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace style {\nstruct Box;\nstruct Table;\nstruct FlatLabel;\nstruct PopupMenu;\nstruct IconButton;\nstruct PeerListItem;\n} // namespace style\n\nnamespace Ui {\nclass GenericBox;\nclass RpWidget;\nclass VerticalLayout;\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Settings {\n\nstruct SubscriptionRightLabel {\n\tFn<void(QPainter &, int x, int y, int h)> draw;\n\tQSize size;\n};\nSubscriptionRightLabel PaintSubscriptionRightLabelCallback(\n\tnot_null<::Main::Session*> session,\n\tconst style::PeerListItem &st,\n\tint amount);\n\nvoid FillCreditOptions(\n\tstd::shared_ptr<::Main::SessionShow> show,\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<PeerData*> peer,\n\tCreditsAmount minCredits,\n\tFn<void()> paid,\n\trpl::producer<> showFinishes,\n\trpl::producer<QString> subtitle,\n\tstd::vector<Data::CreditTopupOption> preloadedTopupOptions,\n\tbool dark = false,\n\tPeerId spendPurposePeerId = PeerId(0));\n\n[[nodiscard]] not_null<Ui::RpWidget*> AddBalanceWidget(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<::Main::Session*> session,\n\trpl::producer<CreditsAmount> balanceValue,\n\tbool rightAlign,\n\trpl::producer<float64> opacityValue = nullptr,\n\tbool dark = false);\n\nvoid AddWithdrawalWidget(\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\trpl::producer<QString> secondButtonUrl,\n\trpl::producer<CreditsAmount> availableBalanceValue,\n\trpl::producer<QDateTime> dateValue,\n\tbool withdrawalEnabled,\n\trpl::producer<QString> usdValue);\n\nstruct GiftWearBoxStyleOverride {\n\tconst style::Box *box = nullptr;\n\tconst style::IconButton *close = nullptr;\n\tconst style::FlatLabel *title = nullptr;\n\tconst style::FlatLabel *subtitle = nullptr;\n\tconst style::icon *radiantIcon = nullptr;\n\tconst style::icon *profileIcon = nullptr;\n\tconst style::icon *proofIcon = nullptr;\n\tconst style::FlatLabel *infoTitle = nullptr;\n\tconst style::FlatLabel *infoAbout = nullptr;\n};\n[[nodiscard]] GiftWearBoxStyleOverride DarkGiftWearBoxStyle();\n\nstruct CreditsEntryBoxStyleOverrides {\n\tconst style::Box *box = nullptr;\n\tconst style::PopupMenu *menu = nullptr;\n\tconst style::Table *table = nullptr;\n\tconst style::FlatLabel *tableValueMultiline = nullptr;\n\tconst style::FlatLabel *tableValueMessage = nullptr;\n\tconst style::icon *link = nullptr;\n\tconst style::icon *share = nullptr;\n\tconst style::icon *theme = nullptr;\n\tconst style::icon *transfer = nullptr;\n\tconst style::icon *craft = nullptr;\n\tconst style::icon *wear = nullptr;\n\tconst style::icon *takeoff = nullptr;\n\tconst style::icon *resell = nullptr;\n\tconst style::icon *unlist = nullptr;\n\tconst style::icon *show = nullptr;\n\tconst style::icon *hide = nullptr;\n\tconst style::icon *pin = nullptr;\n\tconst style::icon *unpin = nullptr;\n\tconst style::icon *offer = nullptr;\n\tstd::shared_ptr<ShareBoxStyleOverrides> shareBox;\n\tstd::shared_ptr<GiftWearBoxStyleOverride> giftWearBox;\n};\n[[nodiscard]] CreditsEntryBoxStyleOverrides DarkCreditsEntryBoxStyle();\n\n[[nodiscard]] rpl::producer<CreditsAmount> UniqueGiftResalePrice(\n\tstd::shared_ptr<Data::UniqueGift> unique,\n\tbool forceTon = false);\n[[nodiscard]] bool UniqueGiftCanRemoveDetails(\n\tconst Data::CreditsHistoryEntry &entry);\n[[nodiscard]] Fn<void(Fn<void()> removed)> UniqueGiftRemoveDetailsHandler(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst Data::CreditsHistoryEntry &entry);\n\nvoid GenericCreditsEntryBox(\n\tnot_null<Ui::GenericBox*> box,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst Data::CreditsHistoryEntry &e,\n\tconst Data::SubscriptionEntry &s,\n\tCreditsEntryBoxStyleOverrides st = {});\nvoid GenericCreditsEntryBody(\n\tnot_null<Ui::GenericBox*> box,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst Data::CreditsHistoryEntry &e);\nvoid GenericCreditsEntryBody(\n\tnot_null<Ui::GenericBox*> box,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst Data::CreditsHistoryEntry &e,\n\tconst Data::SubscriptionEntry &s,\n\tstd::shared_ptr<Data::GiftUpgradeSpinner> upgradeSpinner,\n\tCreditsEntryBoxStyleOverrides st = {});\nvoid UniqueGiftValueBox(\n\tnot_null<Ui::GenericBox*> box,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst Data::CreditsHistoryEntry &e,\n\tCreditsEntryBoxStyleOverrides st = {});\nvoid ReceiptCreditsBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> controller,\n\tconst Data::CreditsHistoryEntry &e,\n\tconst Data::SubscriptionEntry &s);\nvoid BoostCreditsBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> controller,\n\tconst Data::Boost &b);\nvoid GiftedCreditsBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> from,\n\tnot_null<PeerData*> to,\n\tint count,\n\tTimeId date);\nvoid CreditsPrizeBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> controller,\n\tconst Data::GiftCode &data,\n\tTimeId date);\n\nstruct StarGiftResaleInfo {\n\tPeerId recipientId;\n\tbool forceTon = false;\n};\nvoid GlobalStarGiftBox(\n\tnot_null<Ui::GenericBox*> box,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tconst Data::StarGift &data,\n\tStarGiftResaleInfo resale,\n\tCreditsEntryBoxStyleOverrides st = {});\n\n[[nodiscard]] Data::CreditsHistoryEntry SavedStarGiftEntry(\n\tnot_null<PeerData*> owner,\n\tconst Data::SavedStarGift &data);\n[[nodiscard]] Data::SavedStarGiftId EntryToSavedStarGiftId(\n\tnot_null<::Main::Session*> session,\n\tconst Data::CreditsHistoryEntry &entry);\nvoid ShowSavedStarGiftBox(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> owner,\n\tconst Data::SavedStarGift &data,\n\tFn<std::vector<Data::CreditsHistoryEntry>()> pinned = nullptr);\nenum class SavedStarGiftMenuType {\n\tList,\n\tView,\n};\nvoid FillSavedStarGiftMenu(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<Ui::PopupMenu*> menu,\n\tconst Data::CreditsHistoryEntry &e,\n\tSavedStarGiftMenuType type,\n\tCreditsEntryBoxStyleOverrides st = {});\n\nvoid ShowStarGiftViewBox(\n\tnot_null<Window::SessionController*> controller,\n\tconst Data::GiftCode &data,\n\tFullMsgId itemId);\nvoid ShowRefundInfoBox(\n\tnot_null<Window::SessionController*> controller,\n\tFullMsgId refundItemId);\n\n[[nodiscard]] object_ptr<Ui::RpWidget> GenericEntryPhoto(\n\tnot_null<Ui::RpWidget*> parent,\n\tFn<Fn<void(Painter &, int, int, int, int)>(Fn<void()> update)> callback,\n\tint photoSize);\n\n[[nodiscard]] object_ptr<Ui::RpWidget> HistoryEntryPhoto(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<PhotoData*> photo,\n\tint photoSize);\n\n[[nodiscard]] object_ptr<Ui::RpWidget> PaidMediaThumbnail(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<PhotoData*> photo,\n\tPhotoData *second,\n\tint totalCount,\n\tint photoSize);\n\n[[nodiscard]] object_ptr<Ui::RpWidget> SubscriptionUserpic(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<PeerData*> peer,\n\tint photoSize);\n\nstruct SmallBalanceBot {\n\tUserId botId = 0;\n};\nstruct SmallBalanceReaction {\n\tChannelId channelId = 0;\n};\nstruct SmallBalanceVideoStream {\n\tPeerId streamerId = 0;\n};\nstruct SmallBalanceSubscription {\n\tQString name;\n};\nstruct SmallBalanceDeepLink {\n\tQString purpose;\n};\nstruct SmallBalanceStarGift {\n\tPeerId recipientId;\n};\nstruct SmallBalanceForMessage {\n\tPeerId recipientId;\n};\nstruct SmallBalanceForSuggest {\n\tPeerId recipientId;\n};\nstruct SmallBalanceForOffer {\n};\nstruct SmallBalanceForSearch {\n};\nstruct SmallBalanceSource : std::variant<\n\tSmallBalanceBot,\n\tSmallBalanceReaction,\n\tSmallBalanceVideoStream,\n\tSmallBalanceSubscription,\n\tSmallBalanceDeepLink,\n\tSmallBalanceStarGift,\n\tSmallBalanceForMessage,\n\tSmallBalanceForSuggest,\n\tSmallBalanceForOffer,\n\tSmallBalanceForSearch> {\n\tusing variant::variant;\n};\n\nvoid SmallBalanceBox(\n\tnot_null<Ui::GenericBox*> box,\n\tstd::shared_ptr<::Main::SessionShow> show,\n\tuint64 wholeCredits,\n\tSmallBalanceSource source,\n\tFn<void()> paid);\n\nenum class SmallBalanceResult {\n\tAlready,\n\tSuccess,\n\tBlocked,\n\tCancelled,\n};\n\nvoid MaybeRequestBalanceIncrease(\n\tstd::shared_ptr<::Main::SessionShow> show,\n\tuint64 credits,\n\tSmallBalanceSource source,\n\tFn<void(SmallBalanceResult)> done);\n\nvoid AddMiniStars(\n\tnot_null<Ui::VerticalLayout*> content,\n\tnot_null<Ui::RpWidget*> widget,\n\tint photoSize,\n\tint boxWidth,\n\tfloat64 heightRatio);\n\nvoid AddUniqueCloseMoreButton(\n\tnot_null<Ui::GenericBox*> box,\n\tSettings::CreditsEntryBoxStyleOverrides st,\n\tFn<void(not_null<Ui::PopupMenu*>)> fillMenu = nullptr,\n\tFn<void()> launchCraft = nullptr);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_experimental.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/settings_experimental.h\"\n\n#include \"data/components/passkeys.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/text_entity.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/gl/gl_detection.h\"\n#include \"ui/chat/chat_style_radius.h\"\n#include \"ui/controls/compose_ai_button_factory.h\"\n#include \"base/options.h\"\n#include \"boxes/moderate_messages_box.h\"\n#include \"core/application.h\"\n#include \"core/launcher.h\"\n#include \"core/sandbox.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"dialogs/dialogs_widget.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"history/history_item_components.h\"\n#include \"info/profile/info_profile_actions.h\"\n#include \"lang/lang_keys.h\"\n#include \"mainwindow.h\"\n#include \"mainwidget.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"mtproto/session_private.h\"\n#include \"webview/webview_embed.h\"\n#include \"window/main_window.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"window/notifications_manager.h\"\n#include \"info/info_flexible_scroll.h\"\n#include \"chat_helpers/stickers_list_widget.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtCore/QJsonDocument>\n#include <QtGui/QGuiApplication>\n\nnamespace Settings {\nnamespace {\n\nconst auto kOptionsClipboardPrefix = u\"tdesktop-flags:\"_q;\n\nstruct DecodeOptionsResult {\n\tbool ok = false;\n\tQString json;\n};\n\n[[nodiscard]] QString EncodeOptionsToText(const QString &json) {\n\tconst auto flags = QByteArray::Base64UrlEncoding\n\t\t| QByteArray::OmitTrailingEquals;\n\treturn kOptionsClipboardPrefix\n\t\t+ qs(qCompress(json.toLatin1(), 9).toBase64(flags));\n}\n\n[[nodiscard]] DecodeOptionsResult DecodeOptionsFromText(const QString &text) {\n\tauto result = DecodeOptionsResult();\n\tif (!text.startsWith(kOptionsClipboardPrefix)) {\n\t\treturn result;\n\t}\n\tauto encoded = QStringView(text).mid(\n\t\tkOptionsClipboardPrefix.size()).toLatin1();\n\tconst auto compressed = QByteArray::fromBase64Encoding(\n\t\tstd::move(encoded),\n\t\tQByteArray::Base64UrlEncoding\n\t\t\t| QByteArray::AbortOnBase64DecodingErrors);\n\tif (!compressed || (*compressed).isEmpty()) {\n\t\treturn result;\n\t}\n\tconst auto decoded = qUncompress(*compressed);\n\tif (decoded.isEmpty()) {\n\t\treturn result;\n\t}\n\n\tauto error = QJsonParseError();\n\tconst auto parsed = QJsonDocument::fromJson(decoded, &error);\n\tif ((error.error != QJsonParseError::NoError) || !parsed.isObject()) {\n\t\treturn result;\n\t}\n\tresult.ok = true;\n\tresult.json = QString::fromUtf8(decoded);\n\treturn result;\n}\n\nvoid AddOption(\n\t\tnot_null<Window::Controller*> window,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tbase::options::option<bool> &option,\n\t\trpl::producer<> resetClicks,\n\t\trpl::producer<> reloadOptionsRequests) {\n\tauto &lifetime = container->lifetime();\n\tconst auto name = option.name().isEmpty() ? option.id() : option.name();\n\tconst auto toggles = lifetime.make_state<rpl::event_stream<bool>>();\n\tstd::move(\n\t\tresetClicks\n\t) | rpl::map_to(\n\t\toption.defaultValue()\n\t) | rpl::start_to_stream(*toggles, lifetime);\n\tstd::move(reloadOptionsRequests) | rpl::on_next([=, &option] {\n\t\ttoggles->fire_copy(option.value());\n\t}, lifetime);\n\n\tconst auto button = container->add(object_ptr<Button>(\n\t\tcontainer,\n\t\trpl::single(name),\n\t\t(option.relevant()\n\t\t\t? st::settingsButtonNoIcon\n\t\t\t: st::settingsOptionDisabled)\n\t))->toggleOn(toggles->events_starting_with(option.value()));\n\n\tconst auto restarter = (option.relevant() && option.restartRequired())\n\t\t? button->lifetime().make_state<base::Timer>()\n\t\t: nullptr;\n\tif (restarter) {\n\t\trestarter->setCallback([=] {\n\t\t\twindow->show(Ui::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_settings_need_restart(),\n\t\t\t\t.confirmed = [] { Core::Restart(); },\n\t\t\t\t.confirmText = tr::lng_settings_restart_now(),\n\t\t\t\t.cancelText = tr::lng_settings_restart_later(),\n\t\t\t}));\n\t\t});\n\t}\n\tbutton->toggledChanges(\n\t) | rpl::on_next([=, &option](bool toggled) {\n\t\tif (!option.relevant() && toggled != option.defaultValue()) {\n\t\t\ttoggles->fire_copy(option.defaultValue());\n\t\t\twindow->showToast(\n\t\t\t\ttr::lng_settings_experimental_irrelevant(tr::now));\n\t\t\treturn;\n\t\t}\n\t\toption.set(toggled);\n\t\tif (restarter) {\n\t\t\trestarter->callOnce(st::settingsButtonNoIcon.toggle.duration);\n\t\t}\n\t}, container->lifetime());\n\n\tconst auto &description = option.description();\n\tif (!description.isEmpty()) {\n\t\tUi::AddSkip(container, st::settingsCheckboxesSkip);\n\t\tUi::AddDividerText(container, rpl::single(description));\n\t\tUi::AddSkip(container, st::settingsCheckboxesSkip);\n\t}\n}\n\nvoid SetupExperimental(\n\t\tnot_null<Window::Controller*> window,\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<> reloadOptionsRequests) {\n\tUi::AddSkip(container, st::settingsCheckboxesSkip);\n\n\tcontainer->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\ttr::lng_settings_experimental_about(),\n\t\t\tst::boxLabel),\n\t\tst::defaultBoxDividerLabelPadding);\n\n\tauto reset = (Button*)nullptr;\n\tif (base::options::changed()) {\n\t\tconst auto wrap = container->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\t\tconst auto inner = wrap->entity();\n\t\tUi::AddDivider(inner);\n\t\tUi::AddSkip(inner, st::settingsCheckboxesSkip);\n\t\treset = inner->add(object_ptr<Button>(\n\t\t\tinner,\n\t\t\ttr::lng_settings_experimental_restore(),\n\t\t\tst::settingsButtonNoIcon));\n\t\treset->addClickHandler([=] {\n\t\t\tbase::options::reset();\n\t\t\twrap->hide(anim::type::normal);\n\t\t});\n\t\tUi::AddSkip(inner, st::settingsCheckboxesSkip);\n\t}\n\n\tUi::AddDivider(container);\n\tUi::AddSkip(container, st::settingsCheckboxesSkip);\n\n\tconst auto addToggle = [&](const char name[]) {\n\t\tAddOption(\n\t\t\twindow,\n\t\t\tcontainer,\n\t\t\tbase::options::lookup<bool>(name),\n\t\t\t(reset\n\t\t\t\t? (reset->clicks() | rpl::to_empty)\n\t\t\t\t: rpl::producer<>()),\n\t\t\trpl::duplicate(reloadOptionsRequests));\n\t};\n\n\taddToggle(ChatHelpers::kOptionTabbedPanelShowOnClick);\n\taddToggle(Dialogs::kOptionForumHideChatsList);\n\taddToggle(Dialogs::Ui::kOptionDialogsMuteIcon);\n\taddToggle(Core::kOptionFractionalScalingEnabled);\n\taddToggle(Core::kOptionHighDpiDownscale);\n\taddToggle(Window::kOptionViewProfileInChatsListContextMenu);\n\taddToggle(Info::Profile::kOptionShowPeerIdBelowAbout);\n\taddToggle(Info::Profile::kOptionShowChannelJoinedBelowAbout);\n\taddToggle(Ui::kOptionUseSmallMsgBubbleRadius);\n\taddToggle(Media::Player::kOptionDisableAutoplayNext);\n\taddToggle(Webview::kOptionWebviewDebugEnabled);\n\taddToggle(Webview::kOptionWebviewLegacyEdge);\n\taddToggle(kOptionAutoScrollInactiveChat);\n\taddToggle(Window::Notifications::kOptionHideReplyButton);\n\taddToggle(Window::Notifications::kOptionCustomNotification);\n\taddToggle(Window::Notifications::kOptionGNotification);\n\taddToggle(Core::kOptionFreeType);\n\taddToggle(Core::kOptionSkipUrlSchemeRegister);\n\taddToggle(Core::kOptionDeadlockDetector);\n\taddToggle(Window::kOptionExternalMediaViewer);\n\taddToggle(Window::kOptionNewWindowsSizeAsFirst);\n\taddToggle(MTP::details::kOptionPreferIPv6);\n\tif (base::options::lookup<bool>(kOptionFastButtonsMode).value()) {\n\t\taddToggle(kOptionFastButtonsMode);\n\t}\n\taddToggle(Window::kOptionDisableTouchbar);\n\taddToggle(Info::kAlternativeScrollProcessing);\n\taddToggle(kModerateCommonGroups);\n\taddToggle(kForceComposeSearchOneColumn);\n\taddToggle(ChatHelpers::kOptionUnlimitedRecentStickers);\n\taddToggle(Ui::kOptionHideAiButton);\n\taddToggle(Core::kOptionUseOpenGLRenderer);\n}\n\n} // namespace\n\nExperimental::Experimental(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent();\n}\n\nrpl::producer<QString> Experimental::title() {\n\treturn tr::lng_settings_experimental();\n}\n\nvoid Experimental::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {\n\tconst auto window = &controller()->window();\n\taddAction(\n\t\ttr::lng_theme_editor_menu_export(tr::now),\n\t\t[=] {\n\t\t\tTextUtilities::SetClipboardText(\n\t\t\t\t{ EncodeOptionsToText(base::options::serialize()) });\n\t\t\twindow->showToast(u\"Experimental settings code copied to clipboard.\"_q);\n\t\t},\n\t\t&st::menuIconCopy);\n\tif (!DecodeOptionsFromText(QGuiApplication::clipboard()->text()).ok) {\n\t\treturn;\n\t}\n\taddAction(\n\t\ttr::lng_theme_editor_menu_import(tr::now),\n\t\t[=] {\n\t\t\tconst auto decoded = DecodeOptionsFromText(\n\t\t\t\tQGuiApplication::clipboard()->text());\n\t\t\tif (!decoded.ok) {\n\t\t\t\twindow->showToast(u\"Clipboard does not contain \"\n\t\t\t\t\t\"a valid experimental settings code.\"_q);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!base::options::deserialize(decoded.json)) {\n\t\t\t\twindow->showToast(u\"Experimental settings code is valid\"\n\t\t\t\t\t\", but data format is not supported.\"_q);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_reloadOptionsRequests.fire({});\n\t\t\twindow->showToast(u\"Experimental settings imported \"\n\t\t\t\t\"from code in clipboard.\"_q);\n\t\t},\n\t\t&st::menuIconImportTheme);\n}\n\nvoid Experimental::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\tSetupExperimental(\n\t\t&controller()->window(),\n\t\tcontent,\n\t\t_reloadOptionsRequests.events());\n\n\tUi::ResizeFitChild(this, content);\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_experimental.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"settings/settings_common_session.h\"\n\nnamespace Settings {\n\nclass Experimental : public Section<Experimental> {\npublic:\n\tExperimental(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\tvoid fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override;\n\nprivate:\n\tvoid setupContent();\n\n\trpl::event_stream<> _reloadOptionsRequests;\n\n};\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_faq_suggestions.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/settings_faq_suggestions.h\"\n\n#include \"apiwrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n\nnamespace Settings {\nnamespace {\n\n[[nodiscard]] QString ExtractPlainText(const MTPRichText &text) {\n\treturn text.match([&](const MTPDtextPlain &data) {\n\t\treturn qs(data.vtext());\n\t}, [&](const MTPDtextConcat &data) {\n\t\tauto result = QString();\n\t\tfor (const auto &part : data.vtexts().v) {\n\t\t\tresult += ExtractPlainText(part);\n\t\t}\n\t\treturn result;\n\t}, [&](const MTPDtextBold &data) {\n\t\treturn ExtractPlainText(data.vtext());\n\t}, [&](const MTPDtextItalic &data) {\n\t\treturn ExtractPlainText(data.vtext());\n\t}, [&](const MTPDtextUrl &data) {\n\t\treturn ExtractPlainText(data.vtext());\n\t}, [&](const auto &) {\n\t\treturn QString();\n\t});\n}\n\nstruct TocTextUrl {\n\tQString title;\n\tQString url;\n};\n\n[[nodiscard]] std::optional<TocTextUrl> ExtractTextUrl(\n\t\tconst MTPRichText &text,\n\t\tconst QString &baseUrl) {\n\treturn text.match([&](const MTPDtextUrl &data) -> std::optional<TocTextUrl> {\n\t\tconst auto url = qs(data.vurl());\n\t\tif (!url.startsWith(baseUrl + '#')) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t\treturn TocTextUrl{\n\t\t\t.title = ExtractPlainText(data.vtext()),\n\t\t\t.url = url,\n\t\t};\n\t}, [&](const auto &) -> std::optional<TocTextUrl> {\n\t\treturn std::nullopt;\n\t});\n}\n\n[[nodiscard]] std::optional<QString> ExtractBoldSection(\n\t\tconst MTPRichText &text) {\n\treturn text.match([&](const MTPDtextBold &data) -> std::optional<QString> {\n\t\treturn ExtractPlainText(data.vtext());\n\t}, [&](const auto &) -> std::optional<QString> {\n\t\treturn std::nullopt;\n\t});\n}\n\n} // namespace\n\nFaqSuggestions::FaqSuggestions(not_null<Main::Session*> session)\n: _session(session) {\n}\n\nFaqSuggestions::~FaqSuggestions() {\n\tif (_requestId) {\n\t\t_session->api().request(_requestId).cancel();\n\t}\n}\n\nvoid FaqSuggestions::request() {\n\tif (_loaded.current() || _requestId) {\n\t\treturn;\n\t}\n\tauto link = tr::lng_settings_faq_link(tr::now);\n\tconst auto hash = link.indexOf('#');\n\tconst auto url = (hash > 0) ? link.mid(0, hash) : link;\n\n\t_requestId = _session->api().request(MTPmessages_GetWebPage(\n\t\tMTP_string(url),\n\t\tMTP_int(0)\n\t)).done([=](const MTPmessages_WebPage &result) {\n\t\t_requestId = 0;\n\t\tresult.data().vwebpage().match([&](const MTPDwebPage &data) {\n\t\t\tparse(data);\n\t\t}, [&](const auto &) {});\n\t\t_loaded = true;\n\t}).fail([=] {\n\t\t_requestId = 0;\n\t\t_loaded = true;\n\t}).send();\n}\n\nbool FaqSuggestions::loaded() const {\n\treturn _loaded.current();\n}\n\nrpl::producer<bool> FaqSuggestions::loadedValue() const {\n\treturn _loaded.value();\n}\n\nconst std::vector<FaqEntry> &FaqSuggestions::entries() const {\n\treturn _entries;\n}\n\nvoid FaqSuggestions::parse(const MTPDwebPage &page) {\n\tconst auto cachedPage = page.vcached_page();\n\tif (!cachedPage) {\n\t\treturn;\n\t}\n\tconst auto baseUrl = qs(page.vurl());\n\tconst auto &data = cachedPage->data();\n\n\tauto currentSection = QString();\n\tauto inToc = false;\n\n\tfor (const auto &block : data.vblocks().v) {\n\t\tblock.match([&](const MTPDpageBlockParagraph &data) {\n\t\t\tif (const auto section = ExtractBoldSection(data.vtext())) {\n\t\t\t\tcurrentSection = *section;\n\t\t\t\tinToc = true;\n\t\t\t} else {\n\t\t\t\tinToc = false;\n\t\t\t}\n\t\t}, [&](const MTPDpageBlockList &data) {\n\t\t\tif (!inToc || currentSection.isEmpty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfor (const auto &item : data.vitems().v) {\n\t\t\t\titem.match([&](const MTPDpageListItemText &data) {\n\t\t\t\t\tif (auto entry = ExtractTextUrl(data.vtext(), baseUrl)) {\n\t\t\t\t\t\t_entries.push_back({\n\t\t\t\t\t\t\t.section = currentSection,\n\t\t\t\t\t\t\t.title = std::move(entry->title),\n\t\t\t\t\t\t\t.url = std::move(entry->url),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}, [&](const auto &) {});\n\t\t\t}\n\t\t}, [&](const MTPDpageBlockDivider &) {\n\t\t\tinToc = false;\n\t\t}, [&](const MTPDpageBlockHeader &) {\n\t\t\tinToc = false;\n\t\t}, [&](const auto &) {});\n\n\t\tif (!inToc && !_entries.empty()) {\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_faq_suggestions.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Settings {\n\nstruct FaqEntry {\n\tQString section;\n\tQString title;\n\tQString url;\n};\n\nclass FaqSuggestions final {\npublic:\n\texplicit FaqSuggestions(not_null<Main::Session*> session);\n\t~FaqSuggestions();\n\n\tvoid request();\n\t[[nodiscard]] bool loaded() const;\n\t[[nodiscard]] rpl::producer<bool> loadedValue() const;\n\t[[nodiscard]] const std::vector<FaqEntry> &entries() const;\n\nprivate:\n\tvoid parse(const MTPDwebPage &page);\n\n\tconst not_null<Main::Session*> _session;\n\n\tstd::vector<FaqEntry> _entries;\n\trpl::variable<bool> _loaded = false;\n\tmtpRequestId _requestId = 0;\n\n};\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_intro.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/settings_intro.h\"\n\n#include \"settings/sections/settings_advanced.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"settings/sections/settings_chat.h\"\n#include \"settings/settings_codes.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/vertical_list.h\"\n#include \"lang/lang_keys.h\"\n#include \"boxes/abstract_box.h\"\n#include \"window/window_controller.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_info.h\"\n\nnamespace Settings {\nnamespace {\n\nclass TopBar : public Ui::RpWidget {\npublic:\n\tTopBar(QWidget *parent, const style::InfoTopBar &st);\n\n\tvoid setTitle(rpl::producer<QString> &&title);\n\n\ttemplate <typename ButtonWidget>\n\tButtonWidget *addButton(base::unique_qptr<ButtonWidget> button) {\n\t\tauto result = button.get();\n\t\tpushButton(std::move(button));\n\t\treturn result;\n\t}\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid updateControlsGeometry(int newWidth);\n\tUi::RpWidget *pushButton(base::unique_qptr<Ui::RpWidget> button);\n\n\tconst style::InfoTopBar &_st;\n\tstd::vector<base::unique_qptr<Ui::RpWidget>> _buttons;\n\tQPointer<Ui::FlatLabel> _title;\n\n};\n\nobject_ptr<Ui::RpWidget> CreateIntroSettings(\n\t\tQWidget *parent,\n\t\tnot_null<Window::Controller*> window) {\n\tauto result = object_ptr<Ui::VerticalLayout>(parent);\n\n\tUi::AddDivider(result);\n\tUi::AddSkip(result);\n\tSetupLanguageButton(window, result);\n\tSetupConnectionType(window, &window->account(), result);\n\tUi::AddSkip(result);\n\tif (HasUpdate()) {\n\t\tUi::AddDivider(result);\n\t\tUi::AddSkip(result);\n\t\tSetupUpdate(result);\n\t\tUi::AddSkip(result);\n\t}\n\t{\n\t\tauto wrap = object_ptr<Ui::VerticalLayout>(result);\n\t\tSetupSystemIntegrationContent(\n\t\t\twindow->sessionController(),\n\t\t\twrap.data());\n\t\tSetupWindowTitleContent(\n\t\t\twindow->sessionController(),\n\t\t\twrap.data());\n\t\tif (wrap->count() > 0) {\n\t\t\tUi::AddDivider(result);\n\t\t\tUi::AddSkip(result);\n\t\t\tresult->add(object_ptr<Ui::OverrideMargins>(\n\t\t\t\tresult,\n\t\t\t\tstd::move(wrap)));\n\t\t\tUi::AddSkip(result);\n\t\t}\n\t}\n\tUi::AddDivider(result);\n\tUi::AddSkip(result);\n\tSetupInterfaceScale(window, result, false);\n\tSetupDefaultThemes(window, result);\n\tUi::AddSkip(result);\n\n\tif (anim::Disabled()) {\n\t\tUi::AddDivider(result);\n\t\tUi::AddSkip(result);\n\t\tSetupAnimations(window, result);\n\t\tUi::AddSkip(result);\n\t}\n\n\tUi::AddDivider(result);\n\tUi::AddSkip(result);\n\n\tAddButtonWithIcon(\n\t\tresult,\n\t\ttr::lng_settings_faq(),\n\t\tst::settingsButtonNoIcon\n\t)->addClickHandler([] {\n\t\tOpenFaq(nullptr);\n\t});\n\n\treturn result;\n}\n\nTopBar::TopBar(QWidget *parent, const style::InfoTopBar &st)\n: RpWidget(parent)\n, _st(st) {\n}\n\nvoid TopBar::setTitle(rpl::producer<QString> &&title) {\n\tif (_title) {\n\t\tdelete _title;\n\t}\n\t_title = Ui::CreateChild<Ui::FlatLabel>(\n\t\tthis,\n\t\tstd::move(title),\n\t\t_st.title);\n\tupdateControlsGeometry(width());\n}\n\nUi::RpWidget *TopBar::pushButton(base::unique_qptr<Ui::RpWidget> button) {\n\tauto wrapped = std::move(button);\n\tauto weak = wrapped.get();\n\t_buttons.push_back(std::move(wrapped));\n\tweak->widthValue(\n\t) | rpl::on_next([this] {\n\t\tupdateControlsGeometry(width());\n\t}, lifetime());\n\treturn weak;\n}\n\nint TopBar::resizeGetHeight(int newWidth) {\n\tupdateControlsGeometry(newWidth);\n\treturn _st.height;\n}\n\nvoid TopBar::updateControlsGeometry(int newWidth) {\n\tauto right = 0;\n\tfor (auto &button : _buttons) {\n\t\tif (!button) continue;\n\t\tbutton->moveToRight(right, 0, newWidth);\n\t\tright += button->width();\n\t}\n\tif (_title) {\n\t\t_title->moveToLeft(\n\t\t\t_st.titlePosition.x(),\n\t\t\t_st.titlePosition.y(),\n\t\t\tnewWidth);\n\t}\n}\n\nvoid TopBar::paintEvent(QPaintEvent *e) {\n\tconst auto radius = st::boxRadius;\n\tQPainter(this).fillRect(\n\t\te->rect().intersected({ 0, radius, width(), height() - radius }),\n\t\t_st.bg);\n}\n\n} // namespace\n\nclass IntroWidget : public Ui::RpWidget {\npublic:\n\tIntroWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::Controller*> window);\n\n\tQAccessible::Role accessibilityRole() override {\n\t\treturn QAccessible::Dialog;\n\t}\n\tQString accessibilityName() override;\n\n\tvoid forceContentRepaint();\n\n\trpl::producer<int> desiredHeightValue() const override;\n\n\tvoid updateGeometry(QRect newGeometry, int additionalScroll);\n\tint scrollTillBottom(int forHeight) const;\n\trpl::producer<int> scrollTillBottomChanges() const;\n\n\tvoid setInnerFocus();\n\n\t~IntroWidget();\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tvoid updateControlsGeometry();\n\tQRect contentGeometry() const;\n\tvoid setInnerWidget(object_ptr<Ui::RpWidget> content);\n\tvoid showContent(not_null<Window::Controller*> window);\n\trpl::producer<bool> topShadowToggledValue() const;\n\tvoid createTopBar(not_null<Window::Controller*> window);\n\tvoid applyAdditionalScroll(int additionalScroll);\n\n\trpl::variable<int> _scrollTopSkip = -1;\n\trpl::event_stream<int> _scrollTillBottomChanges;\n\tobject_ptr<Ui::RpWidget> _wrap;\n\tnot_null<Ui::ScrollArea*> _scroll;\n\tUi::PaddingWrap<Ui::RpWidget> *_innerWrap = nullptr;\n\tint _innerDesiredHeight = 0;\n\n\tint _additionalScroll = 0;\n\tobject_ptr<TopBar> _topBar = { nullptr };\n\n\tobject_ptr<Ui::FadeShadow> _topShadow;\n\n};\n\nIntroWidget::IntroWidget(\n\tQWidget *parent,\n\tnot_null<Window::Controller*> window)\n: RpWidget(parent)\n, _wrap(this)\n, _scroll(Ui::CreateChild<Ui::ScrollArea>(_wrap.data()))\n, _topShadow(this) {\n\t_wrap->setAttribute(Qt::WA_OpaquePaintEvent);\n\t_wrap->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(_wrap.data());\n\t\tp.fillRect(clip, st::boxBg);\n\t}, _wrap->lifetime());\n\n\t_scrollTopSkip.changes(\n\t) | rpl::on_next([this] {\n\t\tupdateControlsGeometry();\n\t}, lifetime());\n\n\tcreateTopBar(window);\n\tshowContent(window);\n\t_topShadow->toggleOn(\n\t\ttopShadowToggledValue(\n\t\t) | rpl::filter([](bool shown) {\n\t\t\treturn true;\n\t\t}));\n}\n\nQString IntroWidget::accessibilityName() {\n\treturn tr::lng_menu_settings(tr::now);\n}\n\nvoid IntroWidget::updateControlsGeometry() {\n\tif (!_innerWrap) {\n\t\treturn;\n\t}\n\n\t_topBar->resizeToWidth(width());\n\t_topShadow->resizeToWidth(width());\n\t_topShadow->moveToLeft(0, _topBar->height());\n\t_wrap->setGeometry(contentGeometry());\n\n\tauto scrollGeometry = _wrap->rect().marginsRemoved(\n\t\tQMargins(0, _scrollTopSkip.current(), 0, 0));\n\tif (_scroll->geometry() != scrollGeometry) {\n\t\t_scroll->setGeometry(scrollGeometry);\n\t\t_innerWrap->resizeToWidth(_scroll->width());\n\t}\n\n\tif (!_scroll->isHidden()) {\n\t\tauto scrollTop = _scroll->scrollTop();\n\t\t_innerWrap->setVisibleTopBottom(\n\t\t\tscrollTop,\n\t\t\tscrollTop + _scroll->height());\n\t}\n}\n\nvoid IntroWidget::forceContentRepaint() {\n\t// WA_OpaquePaintEvent on TopBar creates render glitches when\n\t// animating the LayerWidget's height :( Fixing by repainting.\n\n\tif (_topBar) {\n\t\t_topBar->update();\n\t}\n\t_scroll->update();\n\tif (_innerWrap) {\n\t\t_innerWrap->update();\n\t}\n}\n\nvoid IntroWidget::createTopBar(not_null<Window::Controller*> window) {\n\t_topBar.create(this, st::infoLayerTopBar);\n\t_topBar->setTitle(tr::lng_menu_settings());\n\tauto close = _topBar->addButton(\n\t\tbase::make_unique_q<Ui::IconButton>(\n\t\t\t_topBar,\n\t\t\tst::infoLayerTopBarClose));\n\tclose->setAccessibleName(tr::lng_close(tr::now));\n\tclose->addClickHandler([=] {\n\t\twindow->hideSettingsAndLayer();\n\t});\n\n\t_topBar->lower();\n\t_topBar->resizeToWidth(width());\n\t_topBar->show();\n}\n\nvoid IntroWidget::setInnerWidget(object_ptr<Ui::RpWidget> content) {\n\t_innerWrap = _scroll->setOwnedWidget(\n\t\tobject_ptr<Ui::PaddingWrap<Ui::RpWidget>>(\n\t\t\tthis,\n\t\t\tstd::move(content),\n\t\t\t_innerWrap ? _innerWrap->padding() : style::margins()));\n\t_innerWrap->move(0, 0);\n\n\t// MSVC BUG + REGRESSION rpl::mappers::tuple :(\n\trpl::combine(\n\t\t_scroll->scrollTopValue(),\n\t\t_scroll->heightValue(),\n\t\t_innerWrap->entity()->desiredHeightValue()\n\t) | rpl::on_next([this](\n\t\t\tint top,\n\t\t\tint height,\n\t\t\tint desired) {\n\t\tconst auto bottom = top + height;\n\t\t_innerDesiredHeight = desired;\n\t\t_innerWrap->setVisibleTopBottom(top, bottom);\n\t\t_scrollTillBottomChanges.fire_copy(std::max(desired - bottom, 0));\n\t}, _innerWrap->lifetime());\n}\n\nrpl::producer<bool> IntroWidget::topShadowToggledValue() const {\n\tusing namespace rpl::mappers;\n\treturn rpl::combine(\n\t\t_scroll->scrollTopValue(),\n\t\t_scrollTopSkip.value()\n\t) | rpl::map((_1 > 0) || (_2 > 0));\n}\n\nvoid IntroWidget::showContent(not_null<Window::Controller*> window) {\n\tsetInnerWidget(CreateIntroSettings(_scroll, window));\n\n\t_additionalScroll = 0;\n\tupdateControlsGeometry();\n\t_topShadow->raise();\n\t_topShadow->finishAnimating();\n}\n\nvoid IntroWidget::setInnerFocus() {\n\tfor (const auto childObject : _innerWrap->entity()->children()) {\n\t\tconst auto childWidget = static_cast<QWidget*>(childObject);\n\t\tif (childObject->isWidgetType() && childWidget->focusPolicy() != Qt::NoFocus) {\n\t\t\tchildWidget->setFocus(Qt::OtherFocusReason);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tsetFocus();\n}\n\nrpl::producer<int> IntroWidget::desiredHeightValue() const {\n\tusing namespace rpl::mappers;\n\treturn rpl::combine(\n\t\t_topBar->heightValue(),\n\t\t_innerWrap->entity()->desiredHeightValue(),\n\t\t_scrollTopSkip.value()\n\t) | rpl::map(_1 + _2 + _3);\n}\n\nQRect IntroWidget::contentGeometry() const {\n\treturn rect().marginsRemoved({ 0, _topBar->height(), 0, 0 });\n}\n\nvoid IntroWidget::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid IntroWidget::keyPressEvent(QKeyEvent *e) {\n\tcrl::on_main(this, [text = e->text()]{\n\t\tCodesFeedString(nullptr, text);\n\t});\n\treturn RpWidget::keyPressEvent(e);\n}\n\nvoid IntroWidget::applyAdditionalScroll(int additionalScroll) {\n\tif (_innerWrap) {\n\t\t_innerWrap->setPadding({ 0, 0, 0, additionalScroll });\n\t}\n}\n\nvoid IntroWidget::updateGeometry(QRect newGeometry, int additionalScroll) {\n\tauto scrollChanged = (_additionalScroll != additionalScroll);\n\tauto geometryChanged = (geometry() != newGeometry);\n\tauto shrinkingContent = (additionalScroll < _additionalScroll);\n\t_additionalScroll = additionalScroll;\n\n\tif (geometryChanged) {\n\t\tif (shrinkingContent) {\n\t\t\tsetGeometry(newGeometry);\n\t\t}\n\t\tif (scrollChanged) {\n\t\t\tapplyAdditionalScroll(additionalScroll);\n\t\t}\n\t\tif (!shrinkingContent) {\n\t\t\tsetGeometry(newGeometry);\n\t\t}\n\t} else if (scrollChanged) {\n\t\tapplyAdditionalScroll(additionalScroll);\n\t}\n}\n\nint IntroWidget::scrollTillBottom(int forHeight) const {\n\tauto scrollHeight = forHeight\n\t\t- _scrollTopSkip.current()\n\t\t- _topBar->height();\n\tauto scrollBottom = _scroll->scrollTop() + scrollHeight;\n\tauto desired = _innerDesiredHeight;\n\treturn std::max(desired - scrollBottom, 0);\n}\n\nrpl::producer<int> IntroWidget::scrollTillBottomChanges() const {\n\treturn _scrollTillBottomChanges.events();\n}\n\nIntroWidget::~IntroWidget() = default;\n\nLayerWidget::LayerWidget(QWidget*, not_null<Window::Controller*> window)\n: _content(this, window) {\n\tsetupHeightConsumers();\n}\n\nvoid LayerWidget::setupHeightConsumers() {\n\t_content->scrollTillBottomChanges(\n\t) | rpl::filter([this] {\n\t\treturn !_inResize;\n\t}) | rpl::on_next([this] {\n\t\tresizeToWidth(width());\n\t}, lifetime());\n\t_content->desiredHeightValue(\n\t) | rpl::on_next([this](int height) {\n\t\taccumulate_max(_desiredHeight, height);\n\t\tif (_content && !_inResize) {\n\t\t\tresizeToWidth(width());\n\t\t}\n\t}, lifetime());\n}\n\nvoid LayerWidget::showFinished() {\n}\n\nvoid LayerWidget::parentResized() {\n\tconst auto parentSize = parentWidget()->size();\n\tconst auto parentWidth = parentSize.width();\n\tconst auto newWidth = (parentWidth < MinimalSupportedWidth())\n\t\t? parentWidth\n\t\t: qMin(\n\t\t\tparentWidth - 2 * st::infoMinimalLayerMargin,\n\t\t\tst::infoDesiredWidth);\n\tresizeToWidth(newWidth);\n}\n\nint LayerWidget::MinimalSupportedWidth() {\n\tauto minimalMargins = 2 * st::infoMinimalLayerMargin;\n\treturn st::infoMinimalWidth + minimalMargins;\n}\n\nint LayerWidget::resizeGetHeight(int newWidth) {\n\tif (!parentWidget() || !_content) {\n\t\treturn 0;\n\t}\n\t_inResize = true;\n\tauto guard = gsl::finally([&] { _inResize = false; });\n\n\tauto parentSize = parentWidget()->size();\n\tauto windowWidth = parentSize.width();\n\tauto windowHeight = parentSize.height();\n\tauto newLeft = (windowWidth - newWidth) / 2;\n\tif (!newLeft) {\n\t\t_content->updateGeometry({ 0, 0, windowWidth, windowHeight }, 0);\n\t\tauto newGeometry = QRect(0, 0, windowWidth, windowHeight);\n\t\tif (newGeometry != geometry()) {\n\t\t\t_content->forceContentRepaint();\n\t\t}\n\t\tif (newGeometry.topLeft() != geometry().topLeft()) {\n\t\t\tmove(newGeometry.topLeft());\n\t\t}\n\t\t_tillTop = _tillBottom = true;\n\t\treturn windowHeight;\n\t}\n\tauto newTop = std::clamp(\n\t\twindowHeight / 24,\n\t\tst::infoLayerTopMinimal,\n\t\tst::infoLayerTopMaximal);\n\tauto newBottom = newTop;\n\tauto desiredHeight = _desiredHeight + st::boxRadius;\n\taccumulate_min(desiredHeight, windowHeight - newTop - newBottom);\n\n\t// First resize content to new width and get the new desired height.\n\tauto contentLeft = 0;\n\tauto contentTop = 0;\n\tauto contentBottom = st::boxRadius;\n\tauto contentWidth = newWidth;\n\tauto contentHeight = desiredHeight - contentTop - contentBottom;\n\tauto scrollTillBottom = _content->scrollTillBottom(contentHeight);\n\tauto additionalScroll = std::min(scrollTillBottom, newBottom);\n\n\tdesiredHeight += additionalScroll;\n\tcontentHeight += additionalScroll;\n\t_tillTop = false;\n\t_tillBottom = (newTop + desiredHeight >= windowHeight);\n\tif (_tillBottom) {\n\t\tcontentHeight += contentBottom;\n\t\tadditionalScroll += contentBottom;\n\t}\n\t_content->updateGeometry({\n\t\tcontentLeft,\n\t\tcontentTop,\n\t\tcontentWidth,\n\t\tcontentHeight }, additionalScroll);\n\n\tauto newGeometry = QRect(newLeft, newTop, newWidth, desiredHeight);\n\tif (newGeometry != geometry()) {\n\t\t_content->forceContentRepaint();\n\t}\n\tif (newGeometry.topLeft() != geometry().topLeft()) {\n\t\tmove(newGeometry.topLeft());\n\t}\n\n\treturn desiredHeight;\n}\n\nvoid LayerWidget::doSetInnerFocus() {\n\t_content->setInnerFocus();\n}\n\nvoid LayerWidget::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tauto clip = e->rect();\n\tauto r = st::boxRadius;\n\tconst auto &pixmaps = Ui::CachedCornerPixmaps(Ui::BoxCorners);\n\tif (!_tillTop && clip.intersects({ 0, 0, width(), r })) {\n\t\tUi::FillRoundRect(p, 0, 0, width(), r, st::boxBg, {\n\t\t\t.p = { pixmaps.p[0], pixmaps.p[1], QPixmap(), QPixmap() },\n\t\t});\n\t}\n\tif (!_tillBottom && clip.intersects({ 0, height() - r, width(), r })) {\n\t\tUi::FillRoundRect(p, 0, height() - r, width(), r, st::boxBg, {\n\t\t\t.p = { QPixmap(), QPixmap(), pixmaps.p[2], pixmaps.p[3] },\n\t\t});\n\t}\n\tif (_tillTop) {\n\t\tp.fillRect(0, 0, width(), r, st::boxBg);\n\t}\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_intro.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/layer_widget.h\"\n\nnamespace Ui {\nclass VerticalLayout;\nclass FadeShadow;\nclass FlatLabel;\ntemplate <typename Widget>\nclass FadeWrap;\n} // namespace Ui\n\nnamespace Window {\nclass Controller;\n} // namespace Window\n\nnamespace Settings {\n\nclass IntroWidget;\n\nclass LayerWidget : public Ui::LayerWidget {\npublic:\n\tLayerWidget(QWidget*, not_null<Window::Controller*> window);\n\n\tvoid showFinished() override;\n\tvoid parentResized() override;\n\n\tstatic int MinimalSupportedWidth();\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid doSetInnerFocus() override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid setupHeightConsumers();\n\n\tobject_ptr<IntroWidget> _content;\n\n\tint _desiredHeight = 0;\n\tbool _inResize = false;\n\tbool _tillTop = false;\n\tbool _tillBottom = false;\n\n};\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_notifications_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <cstdint>\n\n#include \"base/object_ptr.h\"\n#include \"core/core_settings.h\"\n#include \"settings/settings_type.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\nclass Checkbox;\nclass SettingsButton;\nclass ToggleView;\nclass VerticalLayout;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Data {\nenum class DefaultNotify : uint8_t;\n} // namespace Data\n\nnamespace Settings {\n\nconstexpr auto kMaxNotificationsCount = 5;\n\n[[nodiscard]] int CurrentNotificationsCount();\n\nclass NotificationsCount : public Ui::RpWidget {\npublic:\n\tNotificationsCount(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\tvoid setCount(int count);\n\n\t~NotificationsCount();\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tusing ScreenCorner = Core::Settings::ScreenCorner;\n\tvoid setOverCorner(ScreenCorner corner);\n\tvoid clearOverCorner();\n\n\tclass SampleWidget;\n\tvoid removeSample(SampleWidget *widget);\n\n\tQRect getScreenRect() const;\n\tQRect getScreenRect(int width) const;\n\tint getContentLeft() const;\n\tvoid prepareNotificationSampleSmall();\n\tvoid prepareNotificationSampleLarge();\n\tvoid prepareNotificationSampleUserpic();\n\n\tconst not_null<Window::SessionController*> _controller;\n\n\tQPixmap _notificationSampleUserpic;\n\tQPixmap _notificationSampleSmall;\n\tQPixmap _notificationSampleLarge;\n\tScreenCorner _chosenCorner;\n\tstd::vector<Ui::Animations::Simple> _sampleOpacities;\n\n\tbool _isOverCorner = false;\n\tScreenCorner _overCorner = ScreenCorner::TopLeft;\n\tbool _isDownCorner = false;\n\tScreenCorner _downCorner = ScreenCorner::TopLeft;\n\n\tint _oldCount;\n\n\tstd::vector<SampleWidget*> _cornerSamples[4];\n\n};\n\nstruct NotifyViewCheckboxes {\n\tnot_null<Ui::SlideWrap<Ui::RpWidget>*> wrap;\n\tnot_null<Ui::Checkbox*> name;\n\tnot_null<Ui::Checkbox*> preview;\n};\n\n[[nodiscard]] NotifyViewCheckboxes SetupNotifyViewOptions(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Ui::VerticalLayout*> container,\n\tbool nameShown,\n\tbool previewShown);\n\nstruct SplitToggle {\n\tnot_null<Ui::SettingsButton*> button;\n\tnot_null<Ui::SettingsButton*> toggle;\n\tnot_null<Ui::ToggleView*> checkView;\n};\n\n[[nodiscard]] SplitToggle SetupSplitToggle(\n\tnot_null<Ui::VerticalLayout*> container,\n\trpl::producer<QString> title,\n\tconst style::icon *icon,\n\tbool checked,\n\trpl::producer<QString> details);\n\n[[nodiscard]] not_null<Ui::SettingsButton*> AddTypeButton(\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<Window::SessionController*> controller,\n\tData::DefaultNotify type,\n\tFn<void(Type)> showOther);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_power_saving.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/settings_power_saving.h\"\n\n#include \"base/battery_saving.h\"\n#include \"boxes/peers/edit_peer_permissions_box.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/vertical_list.h\"\n#include \"settings/settings_common.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Settings {\nnamespace {\n\nconstexpr auto kForceDisableTooltipDuration = 3 * crl::time(1000);\n\n} // namespace\n\nvoid PowerSavingBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tPowerSaving::Flags highlightFlags) {\n\tbox->setStyle(st::layerBox);\n\tbox->setTitle(tr::lng_settings_power_title());\n\tbox->setWidth(st::boxWideWidth);\n\n\tconst auto container = box->verticalLayout();\n\tconst auto ignore = Core::App().settings().ignoreBatterySaving();\n\tconst auto batterySaving = Core::App().batterySaving().enabled();\n\n\t// Force top shadow visibility.\n\tbox->setPinnedToTopContent(\n\t\tobject_ptr<Ui::FixedHeightWidget>(box, st::lineWidth));\n\n\tconst auto subtitle = Ui::AddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_settings_power_subtitle(),\n\t\tst::powerSavingSubtitlePadding);\n\n\tstruct State {\n\t\trpl::variable<QString> forceDisabledMessage;\n\t};\n\tconst auto state = container->lifetime().make_state<State>();\n\tstate->forceDisabledMessage = (batterySaving.value_or(false) && !ignore)\n\t\t? tr::lng_settings_power_turn_off(tr::now)\n\t\t: QString();\n\n\tauto [checkboxes, getResult, changes, highlightWidget] = CreateEditPowerSaving(\n\t\tbox,\n\t\tPowerSaving::kAll & ~PowerSaving::Current(),\n\t\tstate->forceDisabledMessage.value(),\n\t\thighlightFlags);\n\n\tconst auto controlsRaw = checkboxes.data();\n\tconst auto highlightWidgetRaw = highlightWidget.data();\n\tbox->addRow(std::move(checkboxes), style::margins());\n\n\tauto automatic = (Ui::SettingsButton*)nullptr;\n\tif (batterySaving.has_value()) {\n\t\tUi::AddSkip(container);\n\t\tUi::AddDivider(container);\n\t\tUi::AddSkip(container);\n\t\tautomatic = container->add(object_ptr<Ui::SettingsButton>(\n\t\t\tcontainer,\n\t\t\ttr::lng_settings_power_auto(),\n\t\t\tst::powerSavingButtonNoIcon\n\t\t))->toggleOn(rpl::single(!ignore));\n\t\tUi::AddSkip(container);\n\t\tUi::AddDividerText(container, tr::lng_settings_power_auto_about());\n\n\t\tstate->forceDisabledMessage = rpl::combine(\n\t\t\tautomatic->toggledValue(),\n\t\t\tCore::App().batterySaving().value()\n\t\t) | rpl::map([=](bool dontIgnore, bool saving) {\n\t\t\treturn (saving && dontIgnore)\n\t\t\t\t? tr::lng_settings_power_turn_off()\n\t\t\t\t: rpl::single(QString());\n\t\t}) | rpl::flatten_latest();\n\n\t\tconst auto show = box->uiShow();\n\t\tconst auto disabler = Ui::CreateChild<Ui::AbstractButton>(\n\t\t\tcontainer.get());\n\t\tdisabler->setClickedCallback([=] {\n\t\t\tshow->showToast(\n\t\t\t\ttr::lng_settings_power_turn_off(tr::now),\n\t\t\t\tkForceDisableTooltipDuration);\n\t\t});\n\t\tdisabler->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\t\tauto color = st::boxBg->c;\n\t\t\tcolor.setAlpha(96);\n\t\t\tQPainter(disabler).fillRect(clip, color);\n\t\t}, disabler->lifetime());\n\t\trpl::combine(\n\t\t\tsubtitle->geometryValue(),\n\t\t\tcontrolsRaw->geometryValue()\n\t\t) | rpl::on_next([=](QRect subtitle, QRect controls) {\n\t\t\tdisabler->setGeometry(subtitle.united(controls));\n\t\t}, disabler->lifetime());\n\t\tdisabler->showOn(state->forceDisabledMessage.value(\n\t\t) | rpl::map([=](const QString &value) {\n\t\t\treturn !value.isEmpty();\n\t\t}));\n\t}\n\n\tbox->addButton(tr::lng_settings_save(), [=, collect = getResult] {\n\t\tconst auto ignore = automatic\n\t\t\t? !automatic->toggled()\n\t\t\t: Core::App().settings().ignoreBatterySaving();\n\t\tconst auto batterySaving = Core::App().batterySaving().enabled();\n\t\tif (ignore || !batterySaving.value_or(false)) {\n\t\t\tSet(PowerSaving::kAll & ~collect());\n\t\t}\n\t\tCore::App().settings().setIgnoreBatterySavingValue(ignore);\n\t\tCore::App().saveSettingsDelayed();\n\t\tbox->closeBox();\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\n\tif (highlightWidgetRaw) {\n\t\tbox->showFinishes(\n\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\tHighlightWidget(highlightWidgetRaw);\n\t\t}, box->lifetime());\n\t}\n}\n\nEditFlagsDescriptor<PowerSaving::Flags> PowerSavingLabels() {\n\tusing namespace PowerSaving;\n\tusing Label = EditFlagsLabel<Flags>;\n\n\tauto stickers = std::vector<Label>{\n\t\t{\n\t\t\tkStickersPanel,\n\t\t\ttr::lng_settings_power_stickers_panel(tr::now),\n\t\t\t&st::menuIconStickers,\n\t\t},\n\t\t{ kStickersChat, tr::lng_settings_power_stickers_chat(tr::now) },\n\t};\n\tauto emoji = std::vector<Label>{\n\t\t{\n\t\t\tkEmojiPanel,\n\t\t\ttr::lng_settings_power_emoji_panel(tr::now),\n\t\t\t&st::menuIconEmoji,\n\t\t},\n\t\t{ kEmojiReactions, tr::lng_settings_power_emoji_reactions(tr::now) },\n\t\t{ kEmojiChat, tr::lng_settings_power_emoji_chat(tr::now) },\n\t\t{ kEmojiStatus, tr::lng_settings_power_emoji_status(tr::now) },\n\t};\n\tauto chat = std::vector<Label>{\n\t\t{\n\t\t\tkChatBackground,\n\t\t\ttr::lng_settings_power_chat_background(tr::now),\n\t\t\t&st::menuIconChatBubble,\n\t\t},\n\t\t{ kChatSpoiler, tr::lng_settings_power_chat_spoiler(tr::now) },\n\t\t{ kChatEffects, tr::lng_settings_power_chat_effects(tr::now) },\n\t};\n\tauto calls = std::vector<Label>{\n\t\t{\n\t\t\tkCalls,\n\t\t\ttr::lng_settings_power_calls(tr::now),\n\t\t\t&st::menuIconPhone,\n\t\t},\n\t};\n\tauto animations = std::vector<Label>{\n\t\t{\n\t\t\tkAnimations,\n\t\t\ttr::lng_settings_power_ui(tr::now),\n\t\t\t&st::menuIconStartStream,\n\t\t},\n\t};\n\treturn { .labels = {\n\t\t{ tr::lng_settings_power_stickers(), std::move(stickers) },\n\t\t{ tr::lng_settings_power_emoji(), std::move(emoji) },\n\t\t{ tr::lng_settings_power_chat(), std::move(chat) },\n\t\t{ std::nullopt, std::move(calls) },\n\t\t{ std::nullopt, std::move(animations) },\n\t}, .st = &st::powerSavingButton };\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_power_saving.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n\ntemplate <typename Flags>\nstruct EditFlagsDescriptor;\n\nnamespace PowerSaving {\nenum Flag : uint32;\nusing Flags = base::flags<Flag>;\n} // namespace PowerSaving\n\nnamespace Ui {\nclass GenericBox;\nclass RpWidget;\n} // namespace Ui\n\nnamespace Settings {\n\nvoid PowerSavingBox(\n\tnot_null<Ui::GenericBox*> box,\n\tPowerSaving::Flags highlightFlags = PowerSaving::Flags());\n\n[[nodiscard]] EditFlagsDescriptor<PowerSaving::Flags> PowerSavingLabels();\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_privacy_controllers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/settings_privacy_controllers.h\"\n\n#include \"api/api_global_privacy.h\"\n#include \"api/api_peer_photo.h\"\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"base/event_filter.h\"\n#include \"base/unixtime.h\"\n#include \"boxes/abstract_box.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/peers/peer_short_info_box.h\"\n#include \"boxes/peers/prepare_short_info_box.h\"\n#include \"calls/calls_instance.h\"\n#include \"core/application.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer_values.h\" // Data::AmPremiumValue.\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"data/data_user_photos.h\" // UserPhotosViewer.\n#include \"editor/photo_editor_common.h\"\n#include \"editor/photo_editor_layer_widget.h\"\n#include \"history/admin_log/history_admin_log_item.h\"\n#include \"history/history.h\"\n#include \"history/history_item_components.h\"\n#include \"history/view/history_view_message.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"settings/sections/settings_privacy_security.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/text/format_values.h\" // Ui::FormatPhone\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QClipboard>\n\nnamespace Settings {\nnamespace {\n\nusing UserPrivacy = Api::UserPrivacy;\nusing PrivacyRule = Api::UserPrivacy::Rule;\nusing Option = EditPrivacyBox::Option;\n\n[[nodiscard]] QString PublicLinkByPhone(not_null<UserData*> user) {\n\treturn user->session().createInternalLinkFull('+' + user->phone());\n}\n\nclass BlockPeerBoxController final : public ChatsListBoxController {\npublic:\n\texplicit BlockPeerBoxController(not_null<Main::Session*> session);\n\n\tMain::Session &session() const override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\n\tvoid setBlockPeerCallback(Fn<void(not_null<PeerData*> peer)> callback) {\n\t\t_blockPeerCallback = std::move(callback);\n\t}\n\nprotected:\n\tvoid prepareViewHook() override;\n\tstd::unique_ptr<Row> createRow(not_null<History*> history) override;\n\tvoid updateRowHook(not_null<Row*> row) override {\n\t\tupdateIsBlocked(row, row->peer());\n\t\tdelegate()->peerListUpdateRow(row);\n\t}\n\nprivate:\n\tvoid updateIsBlocked(not_null<PeerListRow*> row, PeerData *peer) const;\n\n\tconst not_null<Main::Session*> _session;\n\tFn<void(not_null<PeerData*> peer)> _blockPeerCallback;\n\n};\n\nBlockPeerBoxController::BlockPeerBoxController(\n\tnot_null<Main::Session*> session)\n: ChatsListBoxController(session)\n, _session(session) {\n}\n\nMain::Session &BlockPeerBoxController::session() const {\n\treturn *_session;\n}\n\nvoid BlockPeerBoxController::prepareViewHook() {\n\tdelegate()->peerListSetTitle(tr::lng_blocked_list_add_title());\n\tsession().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::IsBlocked\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\tif (auto row = delegate()->peerListFindRow(update.peer->id.value)) {\n\t\t\tupdateIsBlocked(row, update.peer);\n\t\t\tdelegate()->peerListUpdateRow(row);\n\t\t}\n\t}, lifetime());\n}\n\nvoid BlockPeerBoxController::updateIsBlocked(\n\t\tnot_null<PeerListRow*> row,\n\t\tPeerData *peer) const {\n\tif (!peer) {\n\t\treturn;\n\t}\n\tconst auto blocked = peer->isBlocked();\n\trow->setDisabledState(blocked\n\t\t? PeerListRow::State::DisabledChecked\n\t\t: PeerListRow::State::Active);\n\tif (blocked) {\n\t\trow->setCustomStatus(tr::lng_blocked_list_already_blocked(tr::now));\n\t} else {\n\t\trow->clearCustomStatus();\n\t}\n}\n\nvoid BlockPeerBoxController::rowClicked(not_null<PeerListRow*> row) {\n\t_blockPeerCallback(row->peer());\n}\n\nauto BlockPeerBoxController::createRow(not_null<History*> history)\n-> std::unique_ptr<BlockPeerBoxController::Row> {\n\tconst auto peer = history->peer;\n\tif (!peer->isUser()\n\t\t|| peer->isServiceUser()\n\t\t|| peer->isSelf()\n\t\t|| peer->isRepliesChat()\n\t\t|| peer->isVerifyCodes()) {\n\t\treturn nullptr;\n\t}\n\tauto row = std::make_unique<Row>(history);\n\tupdateIsBlocked(row.get(), peer);\n\treturn row;\n}\n\nAdminLog::OwnedItem GenerateForwardedItem(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate,\n\t\tnot_null<History*> history,\n\t\tconst QString &text) {\n\tExpects(history->peer->isUser());\n\n\tusing Flag = MTPDmessage::Flag;\n\tconst auto flags = Flag::f_from_id | Flag::f_fwd_from;\n\tconst auto item = MTP_message(\n\t\tMTP_flags(flags),\n\t\tMTP_int(0), // Not used (would've been trimmed to 32 bits).\n\t\tpeerToMTP(history->peer->id),\n\t\tMTPint(), // from_boosts_applied\n\t\tMTPstring(), // from_rank\n\t\tpeerToMTP(history->peer->id),\n\t\tMTPPeer(), // saved_peer_id\n\t\tMTP_messageFwdHeader(\n\t\t\tMTP_flags(MTPDmessageFwdHeader::Flag::f_from_id),\n\t\t\tpeerToMTP(history->session().userPeerId()),\n\t\t\tMTPstring(), // from_name\n\t\t\tMTP_int(base::unixtime::now()),\n\t\t\tMTPint(), // channel_post\n\t\t\tMTPstring(), // post_author\n\t\t\tMTPPeer(), // saved_from_peer\n\t\t\tMTPint(), // saved_from_msg_id\n\t\t\tMTPPeer(), // saved_from_id\n\t\t\tMTPstring(), // saved_from_name\n\t\t\tMTPint(), // saved_date\n\t\t\tMTPstring()), // psa_type\n\t\tMTPlong(), // via_bot_id\n\t\tMTPlong(), // via_business_bot_id\n\t\tMTPMessageReplyHeader(),\n\t\tMTP_int(base::unixtime::now()), // date\n\t\tMTP_string(text),\n\t\tMTPMessageMedia(),\n\t\tMTPReplyMarkup(),\n\t\tMTPVector<MTPMessageEntity>(),\n\t\tMTPint(), // views\n\t\tMTPint(), // forwards\n\t\tMTPMessageReplies(),\n\t\tMTPint(), // edit_date\n\t\tMTPstring(), // post_author\n\t\tMTPlong(), // grouped_id\n\t\tMTPMessageReactions(),\n\t\tMTPVector<MTPRestrictionReason>(),\n\t\tMTPint(), // ttl_period\n\t\tMTPint(), // quick_reply_shortcut_id\n\t\tMTPlong(), // effect\n\t\tMTPFactCheck(),\n\t\tMTPint(), // report_delivery_until_date\n\t\tMTPlong(), // paid_message_stars\n\t\tMTPSuggestedPost(),\n\t\tMTPint(), // schedule_repeat_period\n\t\tMTPstring() // summary_from_language\n\t).match([&](const MTPDmessage &data) {\n\t\treturn history->makeMessage(\n\t\t\thistory->nextNonHistoryEntryId(),\n\t\t\tdata,\n\t\t\tMessageFlag::FakeHistoryItem);\n\t}, [](auto &&) -> not_null<HistoryItem*> {\n\t\tUnexpected(\"Type in GenerateForwardedItem.\");\n\t});\n\n\treturn AdminLog::OwnedItem(delegate, item);\n}\n\nstruct ForwardedTooltip {\n\tQRect geometry;\n\tFn<void(QPainter&)> paint;\n};\n[[nodiscard]] ForwardedTooltip PrepareForwardedTooltip(\n\t\tnot_null<HistoryView::Element*> view,\n\t\tOption value) {\n\t// This breaks HistoryView::Element encapsulation :(\n\tconst auto forwarded = view->data()->Get<HistoryMessageForwarded>();\n\tconst auto availableWidth = view->width()\n\t\t- st::msgMargin.left()\n\t\t- st::msgMargin.right();\n\tconst auto bubbleWidth = ranges::min({\n\t\tavailableWidth,\n\t\tview->maxWidth(),\n\t\tst::msgMaxWidth\n\t});\n\tconst auto innerWidth = bubbleWidth\n\t\t- st::msgPadding.left()\n\t\t- st::msgPadding.right();\n\tconst auto phrase = tr::lng_forwarded(\n\t\ttr::now,\n\t\tlt_user,\n\t\tview->history()->session().user()->name());\n\tconst auto kReplacementPosition = QChar(0x0001);\n\tconst auto possiblePosition = tr::lng_forwarded(\n\t\ttr::now,\n\t\tlt_user,\n\t\tQString(1, kReplacementPosition)\n\t).indexOf(kReplacementPosition);\n\tconst auto position = (possiblePosition >= 0\n\t\t&& possiblePosition < phrase.size())\n\t\t? possiblePosition\n\t\t: 0;\n\tconst auto before = phrase.mid(0, position);\n\tconst auto skip = st::msgMargin.left() + st::msgPadding.left();\n\tconst auto small = forwarded->text.countHeight(innerWidth)\n\t\t< 2 * st::msgServiceFont->height;\n\tconst auto nameLeft = skip\n\t\t+ (small ? st::msgServiceFont->width(before) : 0);\n\tconst auto right = skip + innerWidth;\n\tconst auto text = [&] {\n\t\tswitch (value) {\n\t\tcase Option::Everyone:\n\t\t\treturn tr::lng_edit_privacy_forwards_sample_everyone(tr::now);\n\t\tcase Option::Contacts:\n\t\tcase Option::CloseFriends:\n\t\t\treturn tr::lng_edit_privacy_forwards_sample_contacts(tr::now);\n\t\tcase Option::Nobody:\n\t\t\treturn tr::lng_edit_privacy_forwards_sample_nobody(tr::now);\n\t\t}\n\t\tUnexpected(\"Option value in ForwardsPrivacyController.\");\n\t}();\n\tconst auto &font = st::defaultToast.style.font;\n\tconst auto textWidth = font->width(text);\n\tconst auto arrowSkip = st::settingsForwardPrivacyArrowSkip;\n\tconst auto arrowSize = st::settingsForwardPrivacyArrowSize;\n\tconst auto padding = st::settingsForwardPrivacyTooltipPadding;\n\tconst auto rect = QRect(0, 0, textWidth, font->height).marginsAdded(\n\t\tpadding\n\t).translated(padding.left(), padding.top());\n\n\tconst auto top = st::settingsForwardPrivacyPadding\n\t\t+ view->marginTop()\n\t\t+ st::msgPadding.top()\n\t\t- arrowSize\n\t\t- rect.height();\n\tconst auto left1 = std::min(nameLeft, right - rect.width());\n\tconst auto left2 = std::max(left1, skip);\n\tconst auto left = left2;\n\tconst auto arrowLeft1 = nameLeft + arrowSkip;\n\tconst auto arrowLeft2 = std::min(\n\t\tarrowLeft1,\n\t\tstd::max((left + right) / 2, right - arrowSkip));\n\tconst auto arrowLeft = arrowLeft2;\n\tconst auto geometry = rect.translated(left, top);\n\n\tconst auto line = st::lineWidth;\n\tconst auto full = geometry.marginsAdded(\n\t\t{ line, line, line, line + arrowSize });\n\tconst auto origin = full.topLeft();\n\n\tconst auto rounded = std::make_shared<Ui::RoundRect>(\n\t\tImageRoundRadius::Large,\n\t\tst::toastBg);\n\tconst auto paint = [=](QPainter &p) {\n\t\tp.translate(-origin);\n\n\t\trounded->paint(p, geometry);\n\n\t\tp.setFont(font);\n\t\tp.setPen(st::toastFg);\n\t\tp.drawText(\n\t\t\tgeometry.x() + padding.left(),\n\t\t\tgeometry.y() + padding.top() + font->ascent,\n\t\t\ttext);\n\n\t\tconst auto bottom = full.y() + full.height() - line;\n\n\t\tQPainterPath path;\n\t\tpath.moveTo(arrowLeft - arrowSize, bottom - arrowSize);\n\t\tpath.lineTo(arrowLeft, bottom);\n\t\tpath.lineTo(arrowLeft + arrowSize, bottom - arrowSize);\n\t\tpath.lineTo(arrowLeft - arrowSize, bottom - arrowSize);\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.fillPath(path, st::toastBg);\n\t\t}\n\t};\n\treturn { .geometry = full, .paint = paint };\n}\n\n} // namespace\n\nBlockedBoxController::BlockedBoxController(\n\tnot_null<Window::SessionController*> window)\n: _window(window) {\n}\n\nMain::Session &BlockedBoxController::session() const {\n\treturn _window->session();\n}\n\nvoid BlockedBoxController::prepare() {\n\tdelegate()->peerListSetTitle(tr::lng_blocked_list_title());\n\tsetDescriptionText(tr::lng_contacts_loading(tr::now));\n\tdelegate()->peerListRefreshRows();\n\n\tsession().changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::IsBlocked\n\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\thandleBlockedEvent(update.peer);\n\t}, lifetime());\n\n\tsession().api().blockedPeers().slice(\n\t) | rpl::take(\n\t\t1\n\t) | rpl::on_next([=](const Api::BlockedPeers::Slice &result) {\n\t\tsetDescriptionText(tr::lng_blocked_list_about(tr::now));\n\t\tapplySlice(result);\n\t\tloadMoreRows();\n\t}, lifetime());\n}\n\nvoid BlockedBoxController::loadMoreRows() {\n\tif (_allLoaded) {\n\t\treturn;\n\t}\n\n\tsession().api().blockedPeers().request(\n\t\t_offset,\n\t\tcrl::guard(&_guard, [=](const Api::BlockedPeers::Slice &slice) {\n\t\t\tapplySlice(slice);\n\t\t}));\n}\n\nvoid BlockedBoxController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto peer = row->peer();\n\tconst auto window = _window;\n\tcrl::on_main(window, [=] {\n\t\twindow->showPeerHistory(peer);\n\t});\n}\n\nvoid BlockedBoxController::rowRightActionClicked(\n\t\tnot_null<PeerListRow*> row) {\n\tsession().api().blockedPeers().unblock(row->peer());\n}\n\nvoid BlockedBoxController::applySlice(\n\t\tconst Api::BlockedPeers::Slice &slice) {\n\tif (slice.list.empty()) {\n\t\t_allLoaded = true;\n\t}\n\n\t_offset += slice.list.size();\n\tfor (const auto &item : slice.list) {\n\t\tif (const auto peer = session().data().peerLoaded(item.id)) {\n\t\t\tappendRow(peer);\n\t\t\tpeer->setIsBlocked(true);\n\t\t}\n\t}\n\tif (_offset >= slice.total) {\n\t\t_allLoaded = true;\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid BlockedBoxController::handleBlockedEvent(not_null<PeerData*> user) {\n\tif (user->isBlocked()) {\n\t\tif (prependRow(user)) {\n\t\t\tdelegate()->peerListRefreshRows();\n\t\t\tdelegate()->peerListScrollToTop();\n\t\t}\n\t} else if (auto row = delegate()->peerListFindRow(user->id.value)) {\n\t\tdelegate()->peerListRemoveRow(row);\n\t\tdelegate()->peerListRefreshRows();\n\t\t_rowsCountChanges.fire(delegate()->peerListFullRowsCount());\n\t}\n}\n\nvoid BlockedBoxController::BlockNewPeer(\n\t\tnot_null<Window::SessionController*> window) {\n\tauto controller = std::make_unique<BlockPeerBoxController>(\n\t\t&window->session());\n\tauto initBox = [=, controller = controller.get()](\n\t\t\tnot_null<PeerListBox*> box) {\n\t\tcontroller->setBlockPeerCallback([=](not_null<PeerData*> peer) {\n\t\t\twindow->session().api().blockedPeers().block(peer);\n\t\t\tbox->closeBox();\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [box] { box->closeBox(); });\n\t};\n\twindow->show(\n\t\tBox<PeerListBox>(std::move(controller), std::move(initBox)));\n}\n\nbool BlockedBoxController::appendRow(not_null<PeerData*> peer) {\n\tif (delegate()->peerListFindRow(peer->id.value)) {\n\t\treturn false;\n\t}\n\tdelegate()->peerListAppendRow(createRow(peer));\n\t_rowsCountChanges.fire(delegate()->peerListFullRowsCount());\n\treturn true;\n}\n\nbool BlockedBoxController::prependRow(not_null<PeerData*> peer) {\n\tif (delegate()->peerListFindRow(peer->id.value)) {\n\t\treturn false;\n\t}\n\tdelegate()->peerListPrependRow(createRow(peer));\n\t_rowsCountChanges.fire(delegate()->peerListFullRowsCount());\n\treturn true;\n}\n\nstd::unique_ptr<PeerListRow> BlockedBoxController::createRow(\n\t\tnot_null<PeerData*> peer) const {\n\tauto row = std::make_unique<PeerListRowWithLink>(peer);\n\trow->setActionLink(tr::lng_blocked_list_unblock(tr::now));\n\tconst auto status = [&] {\n\t\tconst auto user = peer->asUser();\n\t\tif (!user) {\n\t\t\treturn tr::lng_group_status(tr::now);\n\t\t} else if (!user->phone().isEmpty()) {\n\t\t\treturn Ui::FormatPhone(user->phone());\n\t\t} else if (!user->username().isEmpty()) {\n\t\t\treturn '@' + user->username();\n\t\t} else if (user->isBot()) {\n\t\t\treturn tr::lng_status_bot(tr::now);\n\t\t}\n\t\treturn tr::lng_blocked_list_unknown_phone(tr::now);\n\t}();\n\trow->setCustomStatus(status);\n\treturn row;\n}\n\nrpl::producer<int> BlockedBoxController::rowsCountChanges() const {\n\treturn _rowsCountChanges.events();\n}\n\nPhoneNumberPrivacyController::PhoneNumberPrivacyController(\n\tnot_null<Window::SessionController*> controller)\n: _controller(controller) {\n}\n\nUserPrivacy::Key PhoneNumberPrivacyController::key() const {\n\treturn Key::PhoneNumber;\n}\n\nrpl::producer<QString> PhoneNumberPrivacyController::title() const {\n\treturn tr::lng_edit_privacy_phone_number_title();\n}\n\nauto PhoneNumberPrivacyController::optionsTitleKey() const\n-> rpl::producer<QString> {\n\treturn tr::lng_edit_privacy_phone_number_header();\n}\n\nauto PhoneNumberPrivacyController::warning() const\n-> rpl::producer<TextWithEntities> {\n\tusing namespace rpl::mappers;\n\tconst auto self = _controller->session().user();\n\treturn rpl::combine(\n\t\t_phoneNumberOption.value(),\n\t\t_addedByPhone.value(),\n\t\t(_1 == Option::Nobody) && (_2 != Option::Everyone)\n\t) | rpl::map([=](bool onlyContactsSee) {\n\t\treturn onlyContactsSee\n\t\t\t? tr::lng_edit_privacy_phone_number_contacts(\n\t\t\t\ttr::marked)\n\t\t\t: rpl::combine(\n\t\t\t\ttr::lng_edit_privacy_phone_number_warning(),\n\t\t\t\ttr::lng_username_link()\n\t\t\t) | rpl::map([=](const QString &warning, const QString &added) {\n\t\t\t\tauto base = TextWithEntities{\n\t\t\t\t\twarning + \"\\n\\n\" + added + \"\\n\",\n\t\t\t\t};\n\t\t\t\tconst auto link = PublicLinkByPhone(self);\n\t\t\t\treturn base.append(tr::link(link, link));\n\t\t\t});\n\t}) | rpl::flatten_latest();\n}\n\nvoid PhoneNumberPrivacyController::prepareWarningLabel(\n\t\tnot_null<Ui::FlatLabel*> warning) const {\n\twarning->overrideLinkClickHandler([=] {\n\t\tQGuiApplication::clipboard()->setText(PublicLinkByPhone(\n\t\t\t_controller->session().user()));\n\t\t_controller->window().showToast(\n\t\t\ttr::lng_username_copied(tr::now));\n\t});\n}\n\nrpl::producer<QString> PhoneNumberPrivacyController::exceptionButtonTextKey(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_phone_number_always_empty();\n\tcase Exception::Never:\n\t\treturn tr::lng_edit_privacy_phone_number_never_empty();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nrpl::producer<QString> PhoneNumberPrivacyController::exceptionBoxTitle(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always: {\n\t\treturn tr::lng_edit_privacy_phone_number_always_title();\n\t};\n\tcase Exception::Never: {\n\t\treturn tr::lng_edit_privacy_phone_number_never_title();\n\t};\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nauto PhoneNumberPrivacyController::exceptionsDescription() const\n-> rpl::producer<QString> {\n\treturn tr::lng_edit_privacy_phone_number_exceptions();\n}\n\nobject_ptr<Ui::RpWidget> PhoneNumberPrivacyController::setupMiddleWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> optionValue) {\n\tconst auto key = UserPrivacy::Key::AddedByPhone;\n\tcontroller->session().api().userPrivacy().reload(key);\n\n\t_phoneNumberOption = std::move(optionValue);\n\n\tauto widget = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\tparent,\n\t\tobject_ptr<Ui::VerticalLayout>(parent));\n\n\tconst auto container = widget->entity();\n\tUi::AddSkip(container);\n\tUi::AddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_edit_privacy_phone_number_find());\n\tconst auto group = std::make_shared<Ui::RadioenumGroup<Option>>();\n\tgroup->setChangedCallback([=](Option value) {\n\t\t_addedByPhone = value;\n\t});\n\tcontroller->session().api().userPrivacy().value(\n\t\tkey\n\t) | rpl::take(\n\t\t1\n\t) | rpl::on_next([=](const PrivacyRule &value) {\n\t\tgroup->setValue(value.option);\n\t}, widget->lifetime());\n\n\tconst auto addOption = [&](Option option) {\n\t\treturn EditPrivacyBox::AddOption(container, this, group, option);\n\t};\n\taddOption(Option::Everyone);\n\taddOption(Option::Contacts);\n\tUi::AddSkip(\n\t\tcontainer,\n\t\tst::defaultVerticalListSkip + st::settingsPrivacySkipTop);\n\tUi::AddDivider(container);\n\n\tusing namespace rpl::mappers;\n\twidget->toggleOn(_phoneNumberOption.value(\n\t) | rpl::map(\n\t\t_1 == Option::Nobody\n\t));\n\n\t_saveAdditional = [=] {\n\t\tcontroller->session().api().userPrivacy().save(\n\t\t\tApi::UserPrivacy::Key::AddedByPhone,\n\t\t\tApi::UserPrivacy::Rule{ .option = group->current() });\n\t};\n\n\treturn widget;\n}\n\nvoid PhoneNumberPrivacyController::saveAdditional() {\n\tif (const auto onstack = _saveAdditional) {\n\t\tonstack();\n\t}\n}\n\nLastSeenPrivacyController::LastSeenPrivacyController(\n\tnot_null<::Main::Session*> session)\n: _session(session) {\n}\n\nUserPrivacy::Key LastSeenPrivacyController::key() const {\n\treturn Key::LastSeen;\n}\n\nrpl::producer<QString> LastSeenPrivacyController::title() const {\n\treturn tr::lng_edit_privacy_lastseen_title();\n}\n\nrpl::producer<QString> LastSeenPrivacyController::optionsTitleKey() const {\n\treturn tr::lng_edit_privacy_lastseen_header();\n}\n\nauto LastSeenPrivacyController::warning() const\n-> rpl::producer<TextWithEntities> {\n\treturn tr::lng_edit_privacy_lastseen_warning(tr::marked);\n}\n\nrpl::producer<QString> LastSeenPrivacyController::exceptionButtonTextKey(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_lastseen_always_empty();\n\tcase Exception::Never:\n\t\treturn tr::lng_edit_privacy_lastseen_never_empty();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nrpl::producer<QString> LastSeenPrivacyController::exceptionBoxTitle(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always: {\n\t\treturn tr::lng_edit_privacy_lastseen_always_title();\n\t};\n\tcase Exception::Never: {\n\t\treturn tr::lng_edit_privacy_lastseen_never_title();\n\t};\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nauto LastSeenPrivacyController::exceptionsDescription() const\n-> rpl::producer<QString>{\n\treturn tr::lng_edit_privacy_lastseen_exceptions();\n}\n\nobject_ptr<Ui::RpWidget> LastSeenPrivacyController::setupBelowWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> option) {\n\tusing namespace rpl::mappers;\n\n\tauto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\tparent,\n\t\tobject_ptr<Ui::VerticalLayout>(parent));\n\n\t_option = std::move(option);\n\n\tconst auto content = result->entity();\n\n\tUi::AddSkip(content);\n\n\tconst auto privacy = &controller->session().api().globalPrivacy();\n\tconst auto hideReadTimeButton = content->add(object_ptr<Ui::SettingsButton>(\n\t\tcontent,\n\t\ttr::lng_edit_lastseen_hide_read_time(),\n\t\tst::settingsButtonNoIcon\n\t));\n\t_hideReadTimeButton = hideReadTimeButton;\n\thideReadTimeButton->toggleOn(privacy->hideReadTime())->toggledValue(\n\t) | rpl::on_next([=](bool value) {\n\t\t_hideReadTime = value;\n\t}, content->lifetime());\n\n\tUi::AddSkip(content);\n\tUi::AddDividerText(\n\t\tcontent,\n\t\ttr::lng_edit_lastseen_hide_read_time_about());\n\tif (!controller->session().premium()) {\n\t\tUi::AddSkip(content);\n\t\tcontent->add(object_ptr<Ui::SettingsButton>(\n\t\t\tcontent,\n\t\t\ttr::lng_edit_lastseen_subscribe(),\n\t\t\tst::settingsButtonLightNoIcon\n\t\t))->setClickedCallback([=] {\n\t\t\tSettings::ShowPremium(controller, u\"lastseen\"_q);\n\t\t});\n\t\tUi::AddSkip(content);\n\t\tUi::AddDividerText(\n\t\t\tcontent,\n\t\t\ttr::lng_edit_lastseen_subscribe_about());\n\t}\n\n\tresult->toggleOn(rpl::combine(\n\t\t_option.value(),\n\t\t_exceptionsNever.value(),\n\t\t(_1 != Option::Everyone) || (_2 > 0)));\n\n\treturn result;\n}\n\nvoid LastSeenPrivacyController::handleExceptionsChange(\n\t\tException exception,\n\t\trpl::producer<int> value) {\n\tif (exception == Exception::Never) {\n\t\t_exceptionsNever = std::move(value);\n\t}\n}\n\nvoid LastSeenPrivacyController::confirmSave(\n\t\tbool someAreDisallowed,\n\t\tFn<void()> saveCallback) {\n\tif (someAreDisallowed\n\t\t&& !Core::App().settings().lastSeenWarningSeen()) {\n\t\tauto callback = [\n\t\t\t=,\n\t\t\tsaveCallback = std::move(saveCallback)\n\t\t](Fn<void()> &&close) {\n\t\t\tclose();\n\t\t\tsaveCallback();\n\t\t\tCore::App().settings().setLastSeenWarningSeen(true);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t};\n\t\tauto box = Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_edit_privacy_lastseen_warning(),\n\t\t\t.confirmed = std::move(callback),\n\t\t\t.confirmText = tr::lng_continue(),\n\t\t});\n\t\tUi::show(std::move(box), Ui::LayerOption::KeepOther);\n\t} else {\n\t\tsaveCallback();\n\t}\n}\n\nvoid LastSeenPrivacyController::saveAdditional() {\n\tif (_option.current() == Option::Everyone\n\t\t&& !_exceptionsNever.current()) {\n\t\treturn;\n\t}\n\tconst auto privacy = &_session->api().globalPrivacy();\n\tif (privacy->hideReadTimeCurrent() != _hideReadTime) {\n\t\tprivacy->updateHideReadTime(_hideReadTime);\n\t}\n}\n\nvoid LastSeenPrivacyController::checkHighlightControls(\n\t\tnot_null<Window::SessionController*> controller) {\n\tcontroller->checkHighlightControl(\n\t\tu\"privacy/hide-read-time\"_q,\n\t\t_hideReadTimeButton.data());\n}\n\nUserPrivacy::Key GroupsInvitePrivacyController::key() const {\n\treturn Key::Invites;\n}\n\nrpl::producer<QString> GroupsInvitePrivacyController::title() const {\n\treturn tr::lng_edit_privacy_groups_title();\n}\n\nrpl::producer<QString> GroupsInvitePrivacyController::optionsTitleKey(\n\t\t) const {\n\treturn tr::lng_edit_privacy_groups_header();\n}\n\nauto GroupsInvitePrivacyController::exceptionButtonTextKey(\n\tException exception) const\n-> rpl::producer<QString> {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_groups_always_empty();\n\tcase Exception::Never:\n\t\treturn tr::lng_edit_privacy_groups_never_empty();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nrpl::producer<QString> GroupsInvitePrivacyController::exceptionBoxTitle(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_groups_always_title();\n\tcase Exception::Never:\n\t\treturn tr::lng_edit_privacy_groups_never_title();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nauto GroupsInvitePrivacyController::exceptionsDescription() const\n-> rpl::producer<QString> {\n\treturn tr::lng_edit_privacy_groups_exceptions();\n}\n\nbool GroupsInvitePrivacyController::allowPremiumsToggle(\n\t\tException exception) const {\n\treturn (exception == Exception::Always);\n}\n\nUserPrivacy::Key CallsPrivacyController::key() const {\n\treturn Key::Calls;\n}\n\nrpl::producer<QString> CallsPrivacyController::title() const {\n\treturn tr::lng_edit_privacy_calls_title();\n}\n\nrpl::producer<QString> CallsPrivacyController::optionsTitleKey() const {\n\treturn tr::lng_edit_privacy_calls_header();\n}\n\nrpl::producer<QString> CallsPrivacyController::exceptionButtonTextKey(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_calls_always_empty();\n\tcase Exception::Never:\n\t\treturn tr::lng_edit_privacy_calls_never_empty();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nrpl::producer<QString> CallsPrivacyController::exceptionBoxTitle(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_calls_always_title();\n\tcase Exception::Never: return tr::lng_edit_privacy_calls_never_title();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nauto CallsPrivacyController::exceptionsDescription() const\n-> rpl::producer<QString>{\n\treturn tr::lng_edit_privacy_calls_exceptions();\n}\n\nobject_ptr<Ui::RpWidget> CallsPrivacyController::setupBelowWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> option) {\n\tauto result = object_ptr<Ui::VerticalLayout>(parent);\n\tconst auto content = result.data();\n\n\tUi::AddSkip(content, st::settingsPeerToPeerSkip);\n\tUi::AddSubsectionTitle(\n\t\tcontent,\n\t\ttr::lng_settings_calls_peer_to_peer_title());\n\tSettings::AddPrivacyButton(\n\t\tcontroller,\n\t\tcontent,\n\t\ttr::lng_settings_calls_peer_to_peer_button(),\n\t\t{ &st::menuIconNetwork },\n\t\tUserPrivacy::Key::CallsPeer2Peer,\n\t\t[] { return std::make_unique<CallsPeer2PeerPrivacyController>(); },\n\t\t&st::settingsButton);\n\tUi::AddSkip(content);\n\n\treturn result;\n}\n\nUserPrivacy::Key CallsPeer2PeerPrivacyController::key() const {\n\treturn Key::CallsPeer2Peer;\n}\n\nrpl::producer<QString> CallsPeer2PeerPrivacyController::title() const {\n\treturn tr::lng_edit_privacy_calls_p2p_title();\n}\n\nauto CallsPeer2PeerPrivacyController::optionsTitleKey() const\n-> rpl::producer<QString> {\n\treturn tr::lng_edit_privacy_calls_p2p_header();\n}\n\nQString CallsPeer2PeerPrivacyController::optionLabel(\n\t\tEditPrivacyBox::Option option) const {\n\tswitch (option) {\n\tcase Option::Everyone:\n\t\treturn tr::lng_edit_privacy_calls_p2p_everyone(tr::now);\n\tcase Option::Contacts:\n\t\treturn tr::lng_edit_privacy_calls_p2p_contacts(tr::now);\n\tcase Option::CloseFriends:\n\t\treturn tr::lng_edit_privacy_close_friends(tr::now); // unused\n\tcase Option::Nobody:\n\t\treturn tr::lng_edit_privacy_calls_p2p_nobody(tr::now);\n\t}\n\tUnexpected(\"Option value in optionsLabelKey.\");\n}\n\nauto CallsPeer2PeerPrivacyController::warning() const\n-> rpl::producer<TextWithEntities> {\n\treturn tr::lng_settings_peer_to_peer_about(tr::marked);\n}\n\nauto CallsPeer2PeerPrivacyController::exceptionButtonTextKey(\n\tException exception) const\n-> rpl::producer<QString> {\n\tswitch (exception) {\n\tcase Exception::Always: {\n\t\treturn tr::lng_edit_privacy_calls_p2p_always_empty();\n\t};\n\tcase Exception::Never: {\n\t\treturn tr::lng_edit_privacy_calls_p2p_never_empty();\n\t};\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nrpl::producer<QString> CallsPeer2PeerPrivacyController::exceptionBoxTitle(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always: {\n\t\treturn tr::lng_edit_privacy_calls_p2p_always_title();\n\t};\n\tcase Exception::Never: {\n\t\treturn tr::lng_edit_privacy_calls_p2p_never_title();\n\t};\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nauto CallsPeer2PeerPrivacyController::exceptionsDescription() const\n-> rpl::producer<QString>{\n\treturn tr::lng_edit_privacy_calls_p2p_exceptions();\n}\n\nForwardsPrivacyController::ForwardsPrivacyController(\n\tnot_null<Window::SessionController*> controller)\n: SimpleElementDelegate(controller, [] {})\n, _controller(controller)\n, _chatStyle(\n\tstd::make_unique<Ui::ChatStyle>(\n\t\tcontroller->session().colorIndicesValue())) {\n\t_chatStyle->apply(controller->defaultChatTheme().get());\n}\n\nUserPrivacy::Key ForwardsPrivacyController::key() const {\n\treturn Key::Forwards;\n}\n\nrpl::producer<QString> ForwardsPrivacyController::title() const {\n\treturn tr::lng_edit_privacy_forwards_title();\n}\n\nrpl::producer<QString> ForwardsPrivacyController::optionsTitleKey() const {\n\treturn tr::lng_edit_privacy_forwards_header();\n}\n\nauto ForwardsPrivacyController::warning() const\n-> rpl::producer<TextWithEntities> {\n\treturn tr::lng_edit_privacy_forwards_warning(tr::marked);\n}\n\nrpl::producer<QString> ForwardsPrivacyController::exceptionButtonTextKey(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always: {\n\t\treturn tr::lng_edit_privacy_forwards_always_empty();\n\t};\n\tcase Exception::Never: {\n\t\treturn tr::lng_edit_privacy_forwards_never_empty();\n\t};\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nrpl::producer<QString> ForwardsPrivacyController::exceptionBoxTitle(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always: {\n\t\treturn tr::lng_edit_privacy_forwards_always_title();\n\t};\n\tcase Exception::Never: {\n\t\treturn tr::lng_edit_privacy_forwards_never_title();\n\t};\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nauto ForwardsPrivacyController::exceptionsDescription() const\n-> rpl::producer<QString> {\n\treturn tr::lng_edit_privacy_forwards_exceptions();\n}\n\nobject_ptr<Ui::RpWidget> ForwardsPrivacyController::setupAboveWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> optionValue,\n\t\tnot_null<QWidget*> outerContainer) {\n\tusing namespace rpl::mappers;\n\n\tauto message = GenerateForwardedItem(\n\t\tdelegate(),\n\t\tcontroller->session().data().history(\n\t\t\tPeerData::kServiceNotificationsId),\n\t\ttr::lng_edit_privacy_forwards_sample_message(tr::now));\n\tconst auto view = message.get();\n\n\tauto result = object_ptr<Ui::PaddingWrap<Ui::RpWidget>>(\n\t\tparent,\n\t\tobject_ptr<Ui::RpWidget>(parent),\n\t\tstyle::margins(\n\t\t\t0,\n\t\t\tst::defaultVerticalListSkip,\n\t\t\t0,\n\t\t\tst::settingsPrivacySkipTop));\n\tconst auto widget = result->entity();\n\n\tstruct State {\n\t\tAdminLog::OwnedItem item;\n\t\tOption option = {};\n\t\tbase::unique_qptr<Ui::RpWidget> tooltip;\n\t\tForwardedTooltip info;\n\t\tFn<void()> refreshGeometry;\n\t};\n\tconst auto state = widget->lifetime().make_state<State>();\n\tstate->item = std::move(message);\n\tstate->tooltip = base::make_unique_q<Ui::RpWidget>(outerContainer);\n\tstate->tooltip->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tif (state->info.paint) {\n\t\t\tauto p = QPainter(state->tooltip.get());\n\t\t\tstate->info.paint(p);\n\t\t}\n\t}, state->tooltip->lifetime());\n\tstate->refreshGeometry = [=] {\n\t\tstate->tooltip->show();\n\t\tstate->tooltip->raise();\n\t\tauto position = state->info.geometry.topLeft();\n\t\tauto parent = (QWidget*)widget;\n\t\twhile (parent && parent != outerContainer) {\n\t\t\tposition += parent->pos();\n\t\t\tparent = parent->parentWidget();\n\t\t}\n\t\tstate->tooltip->move(position);\n\t};\n\tconst auto watch = [&](QWidget *widget, const auto &self) -> void {\n\t\tif (!widget) {\n\t\t\treturn;\n\t\t}\n\t\tbase::install_event_filter(state->tooltip, widget, [=](\n\t\t\t\tnot_null<QEvent*> e) {\n\t\t\tif (e->type() == QEvent::Move\n\t\t\t\t|| e->type() == QEvent::Show\n\t\t\t\t|| e->type() == QEvent::ShowToParent\n\t\t\t\t|| e->type() == QEvent::ZOrderChange) {\n\t\t\t\tstate->refreshGeometry();\n\t\t\t}\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t});\n\t\tif (widget == outerContainer) {\n\t\t\treturn;\n\t\t}\n\t\tself(widget->parentWidget(), self);\n\t};\n\twatch(widget, watch);\n\n\tconst auto padding = st::settingsForwardPrivacyPadding;\n\twidget->widthValue(\n\t) | rpl::filter(\n\t\t_1 >= (st::historyMinimalWidth / 2)\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto height = view->resizeGetHeight(width);\n\t\tconst auto top = view->marginTop();\n\t\tconst auto bottom = view->marginBottom();\n\t\tconst auto full = padding + top + height + bottom + padding;\n\t\twidget->resize(width, full);\n\t}, widget->lifetime());\n\n\trpl::combine(\n\t\twidget->widthValue(),\n\t\tstd::move(optionValue)\n\t) | rpl::on_next([=](int width, Option value) {\n\t\tstate->info = PrepareForwardedTooltip(view, value);\n\t\tstate->tooltip->resize(state->info.geometry.size());\n\t\tstate->refreshGeometry();\n\t\tstate->tooltip->update();\n\t}, state->tooltip->lifetime());\n\n\twidget->paintRequest(\n\t) | rpl::on_next([=](QRect rect) {\n\t\t// #TODO themes\n\t\tWindow::SectionWidget::PaintBackground(\n\t\t\tcontroller,\n\t\t\tcontroller->defaultChatTheme().get(), // #TODO themes\n\t\t\twidget,\n\t\t\trect);\n\n\t\tPainter p(widget);\n\t\tconst auto theme = controller->defaultChatTheme().get();\n\t\tauto context = theme->preparePaintContext(\n\t\t\t_chatStyle.get(),\n\t\t\twidget->rect(),\n\t\t\twidget->rect(),\n\t\t\twidget->rect(),\n\t\t\tcontroller->isGifPausedAtLeastFor(\n\t\t\t\tWindow::GifPauseReason::Layer));\n\t\tp.translate(padding / 2, padding + view->marginBottom());\n\t\tcontext.outbg = view->hasOutLayout();\n\t\tview->draw(p, context);\n\t}, widget->lifetime());\n\n\treturn result;\n}\n\nauto ForwardsPrivacyController::delegate()\n-> not_null<HistoryView::ElementDelegate*> {\n\treturn static_cast<HistoryView::ElementDelegate*>(this);\n}\n\nHistoryView::Context ForwardsPrivacyController::elementContext() {\n\treturn HistoryView::Context::ContactPreview;\n}\n\nUserPrivacy::Key ProfilePhotoPrivacyController::key() const {\n\treturn Key::ProfilePhoto;\n}\n\nrpl::producer<QString> ProfilePhotoPrivacyController::title() const {\n\treturn tr::lng_edit_privacy_profile_photo_title();\n}\n\nauto ProfilePhotoPrivacyController::optionsTitleKey() const\n-> rpl::producer<QString> {\n\treturn tr::lng_edit_privacy_profile_photo_header();\n}\n\nobject_ptr<Ui::RpWidget> ProfilePhotoPrivacyController::setupAboveWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> optionValue,\n\t\tnot_null<QWidget*> outerContainer) {\n\t_option = std::move(optionValue);\n\treturn nullptr;\n}\n\nobject_ptr<Ui::RpWidget> ProfilePhotoPrivacyController::setupMiddleWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> optionValue) {\n\tconst auto self = controller->session().user();\n\tauto widget = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\tparent,\n\t\tobject_ptr<Ui::VerticalLayout>(parent));\n\n\tconst auto container = widget->entity();\n\tstruct State {\n\t\tvoid updatePhoto(QImage &&image, bool local) {\n\t\t\tauto result = image.scaled(\n\t\t\t\tuserpicSize * style::DevicePixelRatio(),\n\t\t\t\tQt::KeepAspectRatio,\n\t\t\t\tQt::SmoothTransformation);\n\t\t\tresult = Images::Round(\n\t\t\t\tstd::move(result),\n\t\t\t\tImageRoundRadius::Ellipse);\n\t\t\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\t(local ? localPhoto : photo) = std::move(result);\n\t\t\tif (local) {\n\t\t\t\tlocalOriginal = std::move(image);\n\t\t\t}\n\t\t\thasPhoto.fire(!localPhoto.isNull() || !photo.isNull());\n\t\t}\n\n\t\trpl::event_stream<bool> hasPhoto;\n\t\trpl::variable<bool> hiddenByUser = false;\n\t\trpl::variable<QString> setUserpicButtonText;\n\t\tQSize userpicSize;\n\t\tQImage photo;\n\t\tQImage localPhoto;\n\t\tQImage localOriginal;\n\t};\n\tconst auto state = container->lifetime().make_state<State>();\n\tstate->userpicSize = QSize(\n\t\tst::inviteLinkUserpics.size,\n\t\tst::inviteLinkUserpics.size);\n\n\tUi::AddSkip(container);\n\tconst auto setUserpicButton = AddButtonWithIcon(\n\t\tcontainer,\n\t\tstate->setUserpicButtonText.value(),\n\t\tst::settingsButtonLight,\n\t\t{ &st::menuBlueIconPhotoSet });\n\t_setPublicButton = setUserpicButton;\n\tconst auto &stRemoveButton = st::settingsAttentionButtonWithIcon;\n\tconst auto removeButton = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::SettingsButton>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tparent,\n\t\t\t\ttr::lng_edit_privacy_profile_photo_public_remove(),\n\t\t\t\tstRemoveButton)));\n\t_removePublicButton = removeButton->entity();\n\tUi::AddSkip(container);\n\tUi::AddDividerText(\n\t\tcontainer,\n\t\ttr::lng_edit_privacy_profile_photo_public_about());\n\n\tconst auto userpic = Ui::CreateChild<Ui::RpWidget>(\n\t\tremoveButton->entity());\n\tuserpic->resize(state->userpicSize);\n\tuserpic->paintRequest(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tauto p = QPainter(userpic);\n\t\tp.fillRect(r, Qt::transparent);\n\t\tif (!state->localPhoto.isNull()) {\n\t\t\tp.drawImage(0, 0, state->localPhoto);\n\t\t} else if (!state->photo.isNull()) {\n\t\t\tp.drawImage(0, 0, state->photo);\n\t\t}\n\t}, userpic->lifetime());\n\tremoveButton->entity()->heightValue(\n\t) | rpl::on_next([=,\n\t\t\tleft = stRemoveButton.iconLeft,\n\t\t\twidth = st::menuBlueIconPhotoSet.width()](int height) {\n\t\tuserpic->moveToLeft(\n\t\t\tleft + (width - userpic->width()) / 2,\n\t\t\t(height - userpic->height()) / 2);\n\t}, userpic->lifetime());\n\tremoveButton->toggleOn(rpl::combine(\n\t\tstate->hasPhoto.events_starting_with(false),\n\t\tstate->hiddenByUser.value()\n\t) | rpl::map(rpl::mappers::_1 && !rpl::mappers::_2));\n\n\t(\n\t\tPrepareShortInfoFallbackUserpic(self, st::shortInfoCover).value\n\t) | rpl::on_next([=](PeerShortInfoUserpic info) {\n\t\tstate->updatePhoto(base::take(info.photo), false);\n\t\tuserpic->update();\n\t}, userpic->lifetime());\n\tsetUserpicButton->setClickedCallback([=] {\n\t\tbase::call_delayed(\n\t\t\tst::settingsButton.ripple.hideDuration,\n\t\t\tcrl::guard(container, [=] {\n\t\t\t\tusing namespace Editor;\n\t\t\t\tPrepareProfilePhotoFromFile(\n\t\t\t\t\tcontainer,\n\t\t\t\t\t&controller->window(),\n\t\t\t\t\t{\n\t\t\t\t\t\t.confirm = tr::lng_profile_set_photo_button(\n\t\t\t\t\t\t\ttr::now),\n\t\t\t\t\t\t.cropType = EditorData::CropType::Ellipse,\n\t\t\t\t\t\t.keepAspectRatio = true,\n\t\t\t\t\t},\n\t\t\t\t\t[=](QImage &&image) {\n\t\t\t\t\t\tstate->updatePhoto(std::move(image), true);\n\t\t\t\t\t\tstate->hiddenByUser = false;\n\t\t\t\t\t\tuserpic->update();\n\t\t\t\t\t});\n\t\t\t}));\n\t});\n\tremoveButton->entity()->setClickedCallback([=] {\n\t\tstate->hiddenByUser = true;\n\t});\n\tstate->setUserpicButtonText = removeButton->toggledValue(\n\t) | rpl::map([](bool toggled) {\n\t\treturn !toggled\n\t\t\t? tr::lng_edit_privacy_profile_photo_public_set()\n\t\t\t: tr::lng_edit_privacy_profile_photo_public_update();\n\t}) | rpl::flatten_latest();\n\n\t_saveAdditional = [=] {\n\t\tif (removeButton->isHidden()) {\n\t\t\tif (const auto photoId = SyncUserFallbackPhotoViewer(self)) {\n\t\t\t\tif (const auto photo = self->owner().photo(*photoId)) {\n\t\t\t\t\tcontroller->session().api().peerPhoto().clear(photo);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (!state->localOriginal.isNull()) {\n\t\t\tcontroller->session().api().peerPhoto().uploadFallback(\n\t\t\t\tself,\n\t\t\t\t{ base::take(state->localOriginal) });\n\t\t}\n\t};\n\n\twidget->toggleOn(rpl::combine(\n\t\tstd::move(optionValue),\n\t\t_exceptionsNever.value()\n\t) | rpl::map(rpl::mappers::_1 != Option::Everyone || rpl::mappers::_2));\n\n\treturn widget;\n}\n\nvoid ProfilePhotoPrivacyController::saveAdditional() {\n\tif (const auto onstack = _saveAdditional) {\n\t\tonstack();\n\t}\n}\n\nvoid ProfilePhotoPrivacyController::checkHighlightControls(\n\t\tnot_null<Window::SessionController*> controller) {\n\tcontroller->checkHighlightControl(\n\t\tu\"privacy/set-public\"_q,\n\t\t_setPublicButton.data());\n\tcontroller->checkHighlightControl(\n\t\tu\"privacy/update-public\"_q,\n\t\t_setPublicButton.data());\n\tcontroller->checkHighlightControl(\n\t\tu\"privacy/remove-public\"_q,\n\t\t_removePublicButton.data());\n}\n\nauto ProfilePhotoPrivacyController::exceptionButtonTextKey(\n\tException exception) const\n-> rpl::producer<QString> {\n\tswitch (exception) {\n\tcase Exception::Always: {\n\t\treturn tr::lng_edit_privacy_profile_photo_always_empty();\n\t};\n\tcase Exception::Never: {\n\t\treturn tr::lng_edit_privacy_profile_photo_never_empty();\n\t};\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nrpl::producer<QString> ProfilePhotoPrivacyController::exceptionBoxTitle(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always: {\n\t\treturn tr::lng_edit_privacy_profile_photo_always_title();\n\t};\n\tcase Exception::Never: {\n\t\treturn tr::lng_edit_privacy_profile_photo_never_title();\n\t};\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nauto ProfilePhotoPrivacyController::exceptionsDescription() const\n-> rpl::producer<QString> {\n\treturn _option.value(\n\t) | rpl::map([](Option option) {\n\t\tswitch (option) {\n\t\tcase Option::Everyone:\n\t\t\treturn tr::lng_edit_privacy_forwards_exceptions_everyone();\n\t\tcase Option::Contacts:\n\t\tcase Option::CloseFriends:\n\t\t\treturn tr::lng_edit_privacy_forwards_exceptions();\n\t\tcase Option::Nobody:\n\t\t\treturn tr::lng_edit_privacy_forwards_exceptions_nobody();\n\t\t}\n\t\tUnexpected(\"Option value in exceptionsDescription.\");\n\t}) | rpl::flatten_latest();\n}\n\n\nvoid ProfilePhotoPrivacyController::handleExceptionsChange(\n\t\tException exception,\n\t\trpl::producer<int> value) {\n\tif (exception == Exception::Never) {\n\t\t_exceptionsNever = std::move(value);\n\t}\n}\n\nVoicesPrivacyController::VoicesPrivacyController(\n\t\tnot_null<::Main::Session*> session) {\n\tData::AmPremiumValue(\n\t\tsession\n\t) | rpl::on_next([=](bool premium) {\n\t\tif (!premium) {\n\t\t\tif (const auto box = view()) {\n\t\t\t\tbox->closeBox();\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n}\n\nUserPrivacy::Key VoicesPrivacyController::key() const {\n\treturn Key::Voices;\n}\n\nrpl::producer<QString> VoicesPrivacyController::title() const {\n\treturn tr::lng_edit_privacy_voices_title();\n}\n\nrpl::producer<QString> VoicesPrivacyController::optionsTitleKey() const {\n\treturn tr::lng_edit_privacy_voices_header();\n}\n\nrpl::producer<QString> VoicesPrivacyController::exceptionButtonTextKey(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_voices_always_empty();\n\tcase Exception::Never:\n\t\treturn tr::lng_edit_privacy_voices_never_empty();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nrpl::producer<QString> VoicesPrivacyController::exceptionBoxTitle(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_voices_always_title();\n\tcase Exception::Never: return tr::lng_edit_privacy_voices_never_title();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nauto VoicesPrivacyController::exceptionsDescription() const\n-> rpl::producer<QString> {\n\treturn tr::lng_edit_privacy_voices_exceptions();\n}\n\nobject_ptr<Ui::RpWidget> VoicesPrivacyController::setupBelowWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> option) {\n\tusing namespace rpl::mappers;\n\n\tauto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\tparent,\n\t\tobject_ptr<Ui::VerticalLayout>(parent));\n\tresult->toggleOn(\n\t\tData::AmPremiumValue(&controller->session()) | rpl::map(!_1),\n\t\tanim::type::instant);\n\n\tconst auto content = result->entity();\n\n\tUi::AddSkip(content);\n\tSettings::AddButtonWithIcon(\n\t\tcontent,\n\t\ttr::lng_messages_privacy_premium_button(),\n\t\tst::messagePrivacySubscribe,\n\t\t{ .icon = &st::menuBlueIconPremium }\n\t)->setClickedCallback([=] {\n\t\tSettings::ShowPremium(\n\t\t\tcontroller,\n\t\t\tu\"voice_restrictions_require_premium\"_q);\n\t});\n\tUi::AddSkip(content);\n\tUi::AddDividerText(content, tr::lng_messages_privacy_premium_about());\n\n\treturn result;\n}\n\nFn<void()> VoicesPrivacyController::premiumClickedCallback(\n\t\tOption option,\n\t\tnot_null<Window::SessionController*> controller) {\n\tif (option == Option::Everyone) {\n\t\treturn nullptr;\n\t}\n\tconst auto showToast = [=] {\n\t\tauto link = tr::link(\n\t\t\ttr::semibold(\n\t\t\t\ttr::lng_settings_privacy_premium_link(tr::now)));\n\t\t_toastInstance = controller->showToast({\n\t\t\t.text = tr::lng_settings_privacy_premium(\n\t\t\t\ttr::now,\n\t\t\t\tlt_link,\n\t\t\t\tlink,\n\t\t\t\ttr::marked),\n\t\t\t.filter = crl::guard(&controller->session(), [=](\n\t\t\t\t\tconst ClickHandlerPtr &,\n\t\t\t\t\tQt::MouseButton button) {\n\t\t\t\tif (button == Qt::LeftButton) {\n\t\t\t\t\tif (const auto strong = _toastInstance.get()) {\n\t\t\t\t\t\tstrong->hideAnimated();\n\t\t\t\t\t\t_toastInstance = nullptr;\n\t\t\t\t\t\tSettings::ShowPremium(\n\t\t\t\t\t\t\tcontroller,\n\t\t\t\t\t\t\tu\"voice_restrictions_require_premium\"_q);\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}),\n\t\t\t.duration = Ui::Toast::kDefaultDuration * 2,\n\t\t});\n\t};\n\n\treturn showToast;\n}\n\nUserPrivacy::Key AboutPrivacyController::key() const {\n\treturn Key::About;\n}\n\nrpl::producer<QString> AboutPrivacyController::title() const {\n\treturn tr::lng_edit_privacy_about_title();\n}\n\nrpl::producer<QString> AboutPrivacyController::optionsTitleKey() const {\n\treturn tr::lng_edit_privacy_about_header();\n}\n\nrpl::producer<QString> AboutPrivacyController::exceptionButtonTextKey(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_about_always_empty();\n\tcase Exception::Never: return tr::lng_edit_privacy_about_never_empty();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nrpl::producer<QString> AboutPrivacyController::exceptionBoxTitle(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_about_always_title();\n\tcase Exception::Never: return tr::lng_edit_privacy_about_never_title();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nauto AboutPrivacyController::exceptionsDescription() const\n-> rpl::producer<QString> {\n\treturn tr::lng_edit_privacy_about_exceptions();\n}\n\nUserPrivacy::Key BirthdayPrivacyController::key() const {\n\treturn Key::Birthday;\n}\n\nrpl::producer<QString> BirthdayPrivacyController::title() const {\n\treturn tr::lng_edit_privacy_birthday_title();\n}\n\nrpl::producer<QString> BirthdayPrivacyController::optionsTitleKey() const {\n\treturn tr::lng_edit_privacy_birthday_header();\n}\n\nrpl::producer<QString> BirthdayPrivacyController::exceptionButtonTextKey(\n\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_birthday_always_empty();\n\tcase Exception::Never:\n\t\treturn tr::lng_edit_privacy_birthday_never_empty();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nrpl::producer<QString> BirthdayPrivacyController::exceptionBoxTitle(\n\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_birthday_always_title();\n\tcase Exception::Never:\n\t\treturn tr::lng_edit_privacy_birthday_never_title();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nauto BirthdayPrivacyController::exceptionsDescription() const\n-> rpl::producer<QString> {\n\treturn tr::lng_edit_privacy_birthday_exceptions();\n}\n\nobject_ptr<Ui::RpWidget> BirthdayPrivacyController::setupAboveWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> optionValue,\n\t\tnot_null<QWidget*> outerContainer) {\n\tconst auto session = &controller->session();\n\tconst auto user = session->user();\n\tauto result = object_ptr<Ui::SlideWrap<Ui::DividerLabel>>(\n\t\tparent,\n\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\tparent,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tparent,\n\t\t\t\ttr::lng_edit_privacy_birthday_yet(\n\t\t\t\t\tlt_link,\n\t\t\t\t\ttr::lng_edit_privacy_birthday_yet_link(\n\t\t\t\t\t\ttr::url(u\"internal:edit_birthday\"_q)),\n\t\t\t\t\ttr::marked),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::defaultBoxDividerLabelPadding));\n\tresult->toggleOn(session->changes().peerFlagsValue(\n\t\tuser,\n\t\tData::PeerUpdate::Flag::Birthday\n\t) | rpl::map([=] {\n\t\treturn !user->birthday();\n\t}));\n\tresult->finishAnimating();\n\treturn result;\n}\n\nstruct GiftsAutoSavePrivacyController::AdditionalState {\n\tApi::DisallowedGiftTypes disallowed;\n\trpl::event_stream<> disables;\n\tFn<void()> promo;\n\tFn<void()> save;\n};\n\nUserPrivacy::Key GiftsAutoSavePrivacyController::key() const {\n\treturn Key::GiftsAutoSave;\n}\n\nrpl::producer<QString> GiftsAutoSavePrivacyController::title() const {\n\treturn tr::lng_edit_privacy_gifts_title();\n}\n\nauto GiftsAutoSavePrivacyController::optionsTitleKey() const\n-> rpl::producer<QString> {\n\treturn tr::lng_edit_privacy_gifts_header();\n}\n\nauto GiftsAutoSavePrivacyController::exceptionButtonTextKey(\n\tException exception) const\n-> rpl::producer<QString> {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_gifts_always_empty();\n\tcase Exception::Never:\n\t\treturn tr::lng_edit_privacy_gifts_never_empty();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nrpl::producer<QString> GiftsAutoSavePrivacyController::exceptionBoxTitle(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_gifts_always_title();\n\tcase Exception::Never: return tr::lng_edit_privacy_gifts_never_title();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nauto GiftsAutoSavePrivacyController::exceptionsDescription() const\n-> rpl::producer<QString> {\n\treturn tr::lng_edit_privacy_lastseen_exceptions();\n}\n\nbool GiftsAutoSavePrivacyController::allowMiniAppsToggle(\n\t\tException exception) const {\n\treturn true;\n}\n\nvoid GiftsAutoSavePrivacyController::ensureAdditionalState(\n\t\tnot_null<Window::SessionController*> controller,\n\t\trpl::lifetime &on) {\n\tif (_state) {\n\t\treturn;\n\t}\n\tconst auto session = &controller->session();\n\tconst auto globalPrivacy = &session->api().globalPrivacy();\n\n\t_state = on.make_state<AdditionalState>();\n\t_state->disallowed = globalPrivacy->disallowedGiftTypesCurrent();\n\t_state->promo = [=] {\n\t\t_state->disables.fire({});\n\t\tconst auto link = tr::bold(\n\t\t\ttr::lng_settings_generic_subscribe_link(tr::now));\n\t\tSettings::ShowPremiumPromoToast(\n\t\t\tcontroller->uiShow(),\n\t\t\ttr::lng_settings_generic_subscribe(\n\t\t\t\ttr::now,\n\t\t\t\tlt_link,\n\t\t\t\ttr::link(link),\n\t\t\t\ttr::marked),\n\t\t\tu\"gifts_privacy\"_q);\n\t};\n\t_state->save = [=] {\n\t\tconst auto now = _state->disallowed;\n\t\tif (!session->premium()) {\n\t\t\treturn;\n\t\t} else if (globalPrivacy->disallowedGiftTypesCurrent() == now) {\n\t\t\treturn;\n\t\t} else {\n\t\t\tglobalPrivacy->updateDisallowedGiftTypes(now);\n\t\t}\n\t};\n}\n\nobject_ptr<Ui::RpWidget> GiftsAutoSavePrivacyController::setupAboveWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> optionValue,\n\t\tnot_null<QWidget*> outerContainer) {\n\tauto result = object_ptr<Ui::VerticalLayout>(parent);\n\tconst auto content = result.data();\n\n\tensureAdditionalState(controller, content->lifetime());\n\tusing Type = Api::DisallowedGiftType;\n\n\tconst auto session = &controller->session();\n\tconst auto icon = content->add(object_ptr<Ui::SettingsButton>(\n\t\tcontent,\n\t\ttr::lng_edit_privacy_gifts_show_icon(),\n\t\tst::settingsButtonNoIconLocked));\n\t_showIconButton = icon;\n\ticon->toggleOn(rpl::single(\n\t\tsession->premium() && (_state->disallowed & Type::SendHide)\n\t) | rpl::then(_state->disables.events() | rpl::map([=] {\n\t\treturn false;\n\t})));\n\tData::AmPremiumValue(session) | rpl::on_next([=](bool value) {\n\t\ticon->setToggleLocked(!value);\n\t\tif (!value) {\n\t\t\t_state->disables.fire({});\n\t\t}\n\t}, icon->lifetime());\n\ticon->toggledValue() | rpl::on_next([=](bool enable) {\n\t\tif (!enable) {\n\t\t\t_state->disallowed &= ~Type::SendHide;\n\t\t} else if (!session->premium()) {\n\t\t\t_state->promo();\n\t\t} else {\n\t\t\t_state->disallowed |= Type::SendHide;\n\t\t}\n\t}, icon->lifetime());\n\tUi::AddSkip(content);\n\tUi::AddDividerText(\n\t\tcontent,\n\t\ttr::lng_edit_privacy_gifts_show_icon_about(\n\t\t\tlt_emoji,\n\t\t\trpl::single(Ui::Text::IconEmoji(&st::settingsGiftIconEmoji)),\n\t\t\ttr::marked));\n\n\treturn result;\n}\n\nobject_ptr<Ui::RpWidget> GiftsAutoSavePrivacyController::setupBelowWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> option) {\n\tauto result = object_ptr<Ui::VerticalLayout>(parent);\n\tconst auto content = result.data();\n\n\tensureAdditionalState(controller, content->lifetime());\n\tusing Type = Api::DisallowedGiftType;\n\n\tconst auto session = &controller->session();\n\tauto premium = Data::AmPremiumValue(session);\n\n\tUi::AddSkip(content, st::settingsPeerToPeerSkip);\n\tconst auto typesTitle = Ui::AddSubsectionTitle(\n\t\tcontent,\n\t\ttr::lng_edit_privacy_gifts_types());\n\t_acceptedTypesTitle = typesTitle;\n\tconst auto types = base::flat_map<Type, rpl::producer<QString>>{\n\t\t{ Type::Limited, tr::lng_edit_privacy_gifts_limited() },\n\t\t{ Type::Unlimited, tr::lng_edit_privacy_gifts_unlimited() },\n\t\t{ Type::Unique, tr::lng_edit_privacy_gifts_unique() },\n\t\t{ Type::FromChannels, tr::lng_edit_privacy_gifts_channels() },\n\t\t{ Type::Premium, tr::lng_edit_privacy_gifts_premium() },\n\t};\n\tfor (const auto &[type, title] : types) {\n\t\tconst auto button = content->add(object_ptr<Ui::SettingsButton>(\n\t\t\tcontent,\n\t\t\trpl::duplicate(title),\n\t\t\tst::settingsButtonNoIconLocked));\n\t\tbutton->toggleOn(rpl::single(\n\t\t\t!session->premium() || !(_state->disallowed & type)\n\t\t) | rpl::then(_state->disables.events() | rpl::map([=] {\n\t\t\treturn true;\n\t\t})));\n\t\trpl::duplicate(premium) | rpl::on_next([=](bool value) {\n\t\t\tbutton->setToggleLocked(!value);\n\t\t}, button->lifetime());\n\t\tbutton->toggledValue() | rpl::on_next([=](bool enable) {\n\t\t\tif (enable) {\n\t\t\t\t_state->disallowed &= ~type;\n\t\t\t} else if (!session->premium()) {\n\t\t\t\t_state->promo();\n\t\t\t} else {\n\t\t\t\t_state->disallowed |= type;\n\t\t\t}\n\t\t}, button->lifetime());\n\t}\n\tUi::AddSkip(content);\n\tUi::AddDividerText(content, tr::lng_edit_privacy_gifts_types_about());\n\n\treturn result;\n}\n\nvoid GiftsAutoSavePrivacyController::saveAdditional() {\n\tif (const auto onstack = _state->save) {\n\t\tonstack();\n\t}\n}\n\nvoid GiftsAutoSavePrivacyController::checkHighlightControls(\n\t\tnot_null<Window::SessionController*> controller) {\n\tcontroller->checkHighlightControl(\n\t\tu\"privacy/show-icon\"_q,\n\t\t_showIconButton.data());\n\tcontroller->checkHighlightControl(\n\t\tu\"privacy/accepted-types\"_q,\n\t\t_acceptedTypesTitle.data(),\n\t\tSubsectionTitleHighlight());\n}\n\nUserPrivacy::Key SavedMusicPrivacyController::key() const {\n\treturn Key::SavedMusic;\n}\n\nrpl::producer<QString> SavedMusicPrivacyController::title() const {\n\treturn tr::lng_edit_privacy_saved_music_title();\n}\n\nrpl::producer<QString> SavedMusicPrivacyController::optionsTitleKey() const {\n\treturn tr::lng_edit_privacy_saved_music_header();\n}\n\nrpl::producer<QString> SavedMusicPrivacyController::exceptionButtonTextKey(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_saved_music_always_empty();\n\tcase Exception::Never:\n\t\treturn tr::lng_edit_privacy_saved_music_never_empty();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nrpl::producer<QString> SavedMusicPrivacyController::exceptionBoxTitle(\n\t\tException exception) const {\n\tswitch (exception) {\n\tcase Exception::Always:\n\t\treturn tr::lng_edit_privacy_saved_music_always_title();\n\tcase Exception::Never:\n\t\treturn tr::lng_edit_privacy_saved_music_never_title();\n\t}\n\tUnexpected(\"Invalid exception value.\");\n}\n\nauto SavedMusicPrivacyController::exceptionsDescription() const\n-> rpl::producer<QString> {\n\treturn tr::lng_edit_privacy_saved_music_exceptions();\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_privacy_controllers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"boxes/peer_list_box.h\"\n#include \"boxes/edit_privacy_box.h\"\n#include \"history/view/history_view_element.h\"\n#include \"api/api_blocked_peers.h\"\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass ChatStyle;\nnamespace Toast {\nclass Instance;\n} // namespace Toast\n} // namespace Ui\n\nnamespace Settings {\n\nclass BlockedBoxController final : public PeerListController {\npublic:\n\texplicit BlockedBoxController(\n\t\tnot_null<Window::SessionController*> window);\n\n\t::Main::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override;\n\tvoid loadMoreRows() override;\n\n\t[[nodiscard]] rpl::producer<int> rowsCountChanges() const;\n\n\tstatic void BlockNewPeer(not_null<Window::SessionController*> window);\n\nprivate:\n\tvoid applySlice(const Api::BlockedPeers::Slice &slice);\n\tvoid handleBlockedEvent(not_null<PeerData*> peer);\n\n\tbool appendRow(not_null<PeerData*> peer);\n\tbool prependRow(not_null<PeerData*> peer);\n\tstd::unique_ptr<PeerListRow> createRow(not_null<PeerData*> peer) const;\n\n\tconst not_null<Window::SessionController*> _window;\n\n\tint _offset = 0;\n\tbool _allLoaded = false;\n\n\tbase::has_weak_ptr _guard;\n\n\trpl::event_stream<int> _rowsCountChanges;\n\n};\n\nclass PhoneNumberPrivacyController final : public EditPrivacyController {\npublic:\n\tusing Option = EditPrivacyBox::Option;\n\tusing Exception = EditPrivacyBox::Exception;\n\n\texplicit PhoneNumberPrivacyController(\n\t\tnot_null<Window::SessionController*> controller);\n\n\tKey key() const override;\n\n\trpl::producer<QString> title() const override;\n\trpl::producer<QString> optionsTitleKey() const override;\n\trpl::producer<TextWithEntities> warning() const override;\n\tvoid prepareWarningLabel(not_null<Ui::FlatLabel*> warning) const override;\n\trpl::producer<QString> exceptionButtonTextKey(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionBoxTitle(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionsDescription() const override;\n\n\tobject_ptr<Ui::RpWidget> setupMiddleWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> optionValue) override;\n\n\tvoid saveAdditional() override;\n\nprivate:\n\tconst not_null<Window::SessionController*> _controller;\n\trpl::variable<Option> _phoneNumberOption = { Option::Contacts };\n\trpl::variable<Option> _addedByPhone = { Option::Everyone };\n\tFn<void()> _saveAdditional;\n\n};\n\nclass LastSeenPrivacyController final : public EditPrivacyController {\npublic:\n\tusing Option = EditPrivacyBox::Option;\n\tusing Exception = EditPrivacyBox::Exception;\n\n\texplicit LastSeenPrivacyController(not_null<::Main::Session*> session);\n\n\tKey key() const override;\n\n\trpl::producer<QString> title() const override;\n\trpl::producer<QString> optionsTitleKey() const override;\n\trpl::producer<TextWithEntities> warning() const override;\n\trpl::producer<QString> exceptionButtonTextKey(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionBoxTitle(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionsDescription() const override;\n\n\tvoid handleExceptionsChange(\n\t\tException exception,\n\t\trpl::producer<int> value) override;\n\n\tobject_ptr<Ui::RpWidget> setupBelowWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> option) override;\n\n\tvoid confirmSave(\n\t\tbool someAreDisallowed,\n\t\tFn<void()> saveCallback) override;\n\n\tvoid saveAdditional() override;\n\n\tvoid checkHighlightControls(\n\t\tnot_null<Window::SessionController*> controller) override;\n\nprivate:\n\tconst not_null<::Main::Session*> _session;\n\trpl::variable<Option> _option;\n\trpl::variable<int> _exceptionsNever;\n\tbool _hideReadTime = false;\n\tQPointer<QWidget> _hideReadTimeButton;\n\n};\n\nclass GroupsInvitePrivacyController final : public EditPrivacyController {\npublic:\n\tusing Option = EditPrivacyBox::Option;\n\tusing Exception = EditPrivacyBox::Exception;\n\n\tKey key() const override;\n\n\trpl::producer<QString> title() const override;\n\trpl::producer<QString> optionsTitleKey() const override;\n\trpl::producer<QString> exceptionButtonTextKey(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionBoxTitle(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionsDescription() const override;\n\tbool allowPremiumsToggle(Exception exception) const override;\n\n};\n\nclass CallsPrivacyController final : public EditPrivacyController {\npublic:\n\tusing Option = EditPrivacyBox::Option;\n\tusing Exception = EditPrivacyBox::Exception;\n\n\tKey key() const override;\n\n\trpl::producer<QString> title() const override;\n\trpl::producer<QString> optionsTitleKey() const override;\n\trpl::producer<QString> exceptionButtonTextKey(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionBoxTitle(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionsDescription() const override;\n\n\tobject_ptr<Ui::RpWidget> setupBelowWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> option) override;\n\n};\n\nclass CallsPeer2PeerPrivacyController final : public EditPrivacyController {\npublic:\n\tusing Option = EditPrivacyBox::Option;\n\tusing Exception = EditPrivacyBox::Exception;\n\n\tKey key() const override;\n\n\trpl::producer<QString> title() const override;\n\trpl::producer<QString> optionsTitleKey() const override;\n\tQString optionLabel(EditPrivacyBox::Option option) const override;\n\trpl::producer<TextWithEntities> warning() const override;\n\trpl::producer<QString> exceptionButtonTextKey(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionBoxTitle(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionsDescription() const override;\n\n};\n\nclass ForwardsPrivacyController final\n\t: public EditPrivacyController\n\t, private HistoryView::SimpleElementDelegate {\npublic:\n\tusing Option = EditPrivacyBox::Option;\n\tusing Exception = EditPrivacyBox::Exception;\n\n\texplicit ForwardsPrivacyController(\n\t\tnot_null<Window::SessionController*> controller);\n\n\tKey key() const override;\n\n\trpl::producer<QString> title() const override;\n\trpl::producer<QString> optionsTitleKey() const override;\n\trpl::producer<TextWithEntities> warning() const override;\n\trpl::producer<QString> exceptionButtonTextKey(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionBoxTitle(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionsDescription() const override;\n\n\tobject_ptr<Ui::RpWidget> setupAboveWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> optionValue,\n\t\tnot_null<QWidget*> outerContainer) override;\n\nprivate:\n\tusing Element = HistoryView::Element;\n\tnot_null<HistoryView::ElementDelegate*> delegate();\n\tHistoryView::Context elementContext() override;\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst std::unique_ptr<Ui::ChatStyle> _chatStyle;\n\n};\n\nclass ProfilePhotoPrivacyController final : public EditPrivacyController {\npublic:\n\tusing Option = EditPrivacyBox::Option;\n\tusing Exception = EditPrivacyBox::Exception;\n\n\tKey key() const override;\n\n\trpl::producer<QString> title() const override;\n\trpl::producer<QString> optionsTitleKey() const override;\n\trpl::producer<QString> exceptionButtonTextKey(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionBoxTitle(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionsDescription() const override;\n\n\tvoid handleExceptionsChange(\n\t\tException exception,\n\t\trpl::producer<int> value) override;\n\n\tobject_ptr<Ui::RpWidget> setupAboveWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> optionValue,\n\t\tnot_null<QWidget*> outerContainer) override;\n\n\tobject_ptr<Ui::RpWidget> setupMiddleWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> optionValue) override;\n\n\tvoid saveAdditional() override;\n\n\tvoid checkHighlightControls(\n\t\tnot_null<Window::SessionController*> controller) override;\n\nprivate:\n\tFn<void()> _saveAdditional;\n\trpl::variable<Option> _option;\n\trpl::variable<int> _exceptionsNever;\n\tQPointer<QWidget> _setPublicButton;\n\tQPointer<QWidget> _removePublicButton;\n\n};\n\nclass VoicesPrivacyController final : public EditPrivacyController {\npublic:\n\tusing Option = EditPrivacyBox::Option;\n\tusing Exception = EditPrivacyBox::Exception;\n\n\texplicit VoicesPrivacyController(not_null<::Main::Session*> session);\n\n\tKey key() const override;\n\n\trpl::producer<QString> title() const override;\n\trpl::producer<QString> optionsTitleKey() const override;\n\trpl::producer<QString> exceptionButtonTextKey(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionBoxTitle(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionsDescription() const override;\n\tobject_ptr<Ui::RpWidget> setupBelowWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> option) override;\n\tFn<void()> premiumClickedCallback(\n\t\tOption option,\n\t\tnot_null<Window::SessionController*> controller) override;\n\nprivate:\n\tbase::weak_ptr<Ui::Toast::Instance> _toastInstance;\n\trpl::lifetime _lifetime;\n\n};\n\nclass AboutPrivacyController final : public EditPrivacyController {\npublic:\n\tusing Option = EditPrivacyBox::Option;\n\tusing Exception = EditPrivacyBox::Exception;\n\n\tKey key() const override;\n\n\trpl::producer<QString> title() const override;\n\trpl::producer<QString> optionsTitleKey() const override;\n\trpl::producer<QString> exceptionButtonTextKey(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionBoxTitle(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionsDescription() const override;\n\n};\n\nclass BirthdayPrivacyController : public EditPrivacyController {\npublic:\n\tusing Option = EditPrivacyBox::Option;\n\tusing Exception = EditPrivacyBox::Exception;\n\n\tKey key() const override;\n\n\trpl::producer<QString> title() const override;\n\trpl::producer<QString> optionsTitleKey() const override;\n\trpl::producer<QString> exceptionButtonTextKey(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionBoxTitle(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionsDescription() const override;\n\n\tobject_ptr<Ui::RpWidget> setupAboveWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> optionValue,\n\t\tnot_null<QWidget*> outerContainer) override;\n\n};\n\nclass GiftsAutoSavePrivacyController final : public EditPrivacyController {\npublic:\n\tusing Option = EditPrivacyBox::Option;\n\tusing Exception = EditPrivacyBox::Exception;\n\n\tKey key() const override;\n\n\trpl::producer<QString> title() const override;\n\trpl::producer<QString> optionsTitleKey() const override;\n\trpl::producer<QString> exceptionButtonTextKey(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionBoxTitle(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionsDescription() const override;\n\tbool allowMiniAppsToggle(Exception exception) const override;\n\n\tobject_ptr<Ui::RpWidget> setupAboveWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> optionValue,\n\t\tnot_null<QWidget*> outerContainer) override;\n\tobject_ptr<Ui::RpWidget> setupBelowWidget(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<Option> option) override;\n\n\tvoid saveAdditional() override;\n\n\tvoid checkHighlightControls(\n\t\tnot_null<Window::SessionController*> controller) override;\n\nprivate:\n\tstruct AdditionalState;\n\n\tvoid ensureAdditionalState(\n\t\tnot_null<Window::SessionController*> controller,\n\t\trpl::lifetime &on);\n\n\tAdditionalState *_state = nullptr;\n\tQPointer<QWidget> _showIconButton;\n\tQPointer<QWidget> _acceptedTypesTitle;\n\n};\n\nclass SavedMusicPrivacyController final : public EditPrivacyController {\npublic:\n\tusing Option = EditPrivacyBox::Option;\n\tusing Exception = EditPrivacyBox::Exception;\n\n\tKey key() const override;\n\n\trpl::producer<QString> title() const override;\n\trpl::producer<QString> optionsTitleKey() const override;\n\trpl::producer<QString> exceptionButtonTextKey(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionBoxTitle(\n\t\tException exception) const override;\n\trpl::producer<QString> exceptionsDescription() const override;\n\n};\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_recent_searches.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/settings_recent_searches.h\"\n\n#include \"main/main_session.h\"\n#include \"storage/serialize_common.h\"\n#include \"storage/storage_account.h\"\n\nnamespace Settings {\nnamespace {\n\nconstexpr auto kLimit = 32;\n\n} // namespace\n\nRecentSearches::RecentSearches(not_null<Main::Session*> session)\n: _session(session) {\n}\n\nRecentSearches::~RecentSearches() = default;\n\nconst std::vector<QString> &RecentSearches::list() const {\n\t_session->local().readSearchSuggestions();\n\n\treturn _list;\n}\n\nvoid RecentSearches::bump(const QString &entryId) {\n\tif (entryId.isEmpty()) {\n\t\treturn;\n\t}\n\t_session->local().readSearchSuggestions();\n\n\tif (!_list.empty() && _list.front() == entryId) {\n\t\treturn;\n\t}\n\tauto i = ranges::find(_list, entryId);\n\tif (i == end(_list)) {\n\t\tif (int(_list.size()) >= kLimit) {\n\t\t\t_list.pop_back();\n\t\t}\n\t\t_list.push_back(entryId);\n\t\ti = end(_list) - 1;\n\t}\n\tranges::rotate(begin(_list), i, i + 1);\n\n\t_session->local().writeSearchSuggestionsDelayed();\n}\n\nvoid RecentSearches::remove(const QString &entryId) {\n\tconst auto i = ranges::find(_list, entryId);\n\tif (i != end(_list)) {\n\t\t_list.erase(i);\n\t}\n\t_session->local().writeSearchSuggestionsDelayed();\n}\n\nQByteArray RecentSearches::serialize() const {\n\t_session->local().readSearchSuggestions();\n\n\tif (_list.empty()) {\n\t\treturn {};\n\t}\n\tconst auto count = std::min(int(_list.size()), kLimit);\n\tauto size = 2 * int(sizeof(quint32));\n\tfor (auto i = 0; i < count; ++i) {\n\t\tsize += Serialize::stringSize(_list[i]);\n\t}\n\tauto stream = Serialize::ByteArrayWriter(size);\n\tstream\n\t\t<< quint32(AppVersion)\n\t\t<< quint32(count);\n\tfor (auto i = 0; i < count; ++i) {\n\t\tstream << _list[i];\n\t}\n\treturn std::move(stream).result();\n}\n\nvoid RecentSearches::applyLocal(QByteArray serialized) {\n\t_list.clear();\n\tif (serialized.isEmpty()) {\n\t\treturn;\n\t}\n\tauto stream = Serialize::ByteArrayReader(serialized);\n\tauto version = quint32();\n\tauto count = quint32();\n\tstream >> version >> count;\n\tif (!stream.ok()) {\n\t\treturn;\n\t}\n\t_list.reserve(count);\n\tfor (auto i = quint32(0); i < count; ++i) {\n\t\tauto value = QString();\n\t\tstream >> value;\n\t\tif (!stream.ok()) {\n\t\t\t_list.clear();\n\t\t\treturn;\n\t\t}\n\t\t_list.push_back(value);\n\t}\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_recent_searches.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Settings {\n\nclass RecentSearches final {\npublic:\n\texplicit RecentSearches(not_null<Main::Session*> session);\n\t~RecentSearches();\n\n\t[[nodiscard]] const std::vector<QString> &list() const;\n\n\tvoid bump(const QString &entryId);\n\tvoid remove(const QString &entryId);\n\n\t[[nodiscard]] QByteArray serialize() const;\n\tvoid applyLocal(QByteArray serialized);\n\nprivate:\n\tconst not_null<Main::Session*> _session;\n\n\tstd::vector<QString> _list;\n\n};\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_scale_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/settings_scale_preview.h\"\n\n#include \"base/platform/base_platform_info.h\"\n#include \"base/event_filter.h\"\n#include \"data/data_user.h\"\n#include \"data/data_peer_values.h\"\n#include \"history/history_item_components.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/platform/ui_platform_utility.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QScreen>\n\nnamespace Settings {\nnamespace {\n\nconstexpr auto kMinTextWidth = 120;\nconstexpr auto kMaxTextWidth = 320;\nconstexpr auto kMaxTextLines = 3;\n\nclass Preview final {\npublic:\n\tPreview(QWidget *slider, rpl::producer<QImage> userpic);\n\n\tvoid toggle(ScalePreviewShow show, int scale, int globalX);\n\nprivate:\n\tvoid init();\n\tvoid initAsWindow();\n\tvoid watchParent();\n\tvoid reparent();\n\n\tvoid updateToScale(int scale);\n\tvoid updateGlobalPosition(int globalX);\n\tvoid updateGlobalPosition();\n\tvoid updateWindowGlobalPosition(QPoint global);\n\tvoid updateOuterPosition(int globalX);\n\t[[nodiscard]] QRect adjustByScreenGeometry(QRect geometry) const;\n\n\tvoid toggleShown(bool shown);\n\tvoid toggleFilter();\n\tvoid update();\n\n\tvoid paint(Painter &p, QRect clip);\n\tvoid paintLayer(Painter &p, QRect clip);\n\tvoid paintInner(Painter &p, QRect clip);\n\tvoid paintUserpic(Painter &p, QRect clip);\n\tvoid paintBubble(Painter &p, QRect clip);\n\tvoid paintContent(Painter &p, QRect clip);\n\tvoid paintReply(Painter &p, QRect clip);\n\tvoid paintMessage(Painter &p, QRect clip);\n\n\tvoid validateUserpicCache();\n\tvoid validateBubbleCache();\n\tvoid validateShadowCache();\n\n\t[[nodiscard]] int scaled(int value) const;\n\t[[nodiscard]] QPoint scaled(QPoint value) const;\n\t[[nodiscard]] QMargins scaled(QMargins value) const;\n\t[[nodiscard]] style::font scaled(\n\t\tconst style::font &value,\n\t\tint size) const;\n\t[[nodiscard]] style::QuoteStyle scaled(\n\t\tconst style::QuoteStyle &value) const;\n\t[[nodiscard]] style::TextStyle scaled(\n\t\tconst style::TextStyle &value,\n\t\tint fontSize) const;\n\t[[nodiscard]] QImage scaled(\n\t\tconst style::icon &icon,\n\t\tconst QColor &color) const;\n\n\tUi::RpWidget _widget;\n\tnot_null<QWidget*> _slider;\n\tUi::ChatTheme _theme;\n\tstyle::TextStyle _nameStyle = st::fwdTextStyle;\n\tUi::Text::String _nameText = { kMaxTextWidth / 3 };\n\tstyle::TextStyle _textStyle = st::messageTextStyle;\n\tUi::Text::String _replyText = { kMaxTextWidth / 3 };\n\tUi::Text::String _messageText = { kMaxTextWidth / 3 };\n\tstyle::Shadow _shadow = st::callShadow;\n\tstd::array<QImage, 4> _shadowSides;\n\tstd::array<QImage, 4> _shadowCorners;\n\tUi::CornersPixmaps _bubbleCorners;\n\tQPixmap _bubbleShadowBottomRight;\n\tint _bubbleShadow = 0;\n\tint _localShiftLeft = 0;\n\tQImage _bubbleTail;\n\tQRect _replyRect;\n\tQRect _name;\n\tQRect _reply;\n\tQRect _message;\n\tQRect _content;\n\tQRect _bubble;\n\tQRect _userpic;\n\tQRect _inner;\n\tQRect _outer;\n\tQSize _minOuterSize;\n\tQSize _maxOuterSize;\n\tQImage _layer, _canvas;\n\tQPoint _cursor;\n\tstd::array<QImage, 4> _canvasCornerMasks;\n\tQImage _userpicOriginal;\n\tQImage _userpicImage;\n\tint _scale = 0;\n\tint _ratio = 0;\n\tbool _window = false;\n\n\tUi::Animations::Simple _shownAnimation;\n\tbool _shown = false;\n\n\tstd::unique_ptr<QObject> _filter;\n\tstd::unique_ptr<QObject> _parentWatcher;\n\n};\n\nPreview::Preview(QWidget *slider, rpl::producer<QImage> userpic)\n: _widget(slider->window())\n, _slider(slider)\n, _ratio(style::DevicePixelRatio())\n, _window(Ui::Platform::TranslucentWindowsSupported()) {\n\tstd::move(userpic) | rpl::on_next([=](QImage &&userpic) {\n\t\t_userpicOriginal = std::move(userpic);\n\t\tif (!_userpicImage.isNull()) {\n\t\t\t_userpicImage = {};\n\t\t\tupdate();\n\t\t}\n\t}, _widget.lifetime());\n\n\twatchParent();\n\n\tinit();\n}\n\nvoid Preview::watchParent() {\n\tconst auto parent = _widget.parentWidget();\n\t_parentWatcher.reset(base::install_event_filter(parent, [=](\n\t\t\tnot_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::ParentChange) {\n\t\t\tif (_widget.window() != parent) {\n\t\t\t\treparent();\n\t\t\t}\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t}));\n}\n\nvoid Preview::reparent() {\n\tconst auto parent = _widget.parentWidget();\n\tif (!parent) {\n\t\t// macOS just removes parenting for a _window.\n\t\t_parentWatcher = nullptr;\n\t\treturn;\n\t}\n\t_widget.setParent(parent->window(), _widget.windowFlags());\n\tif (_shown) {\n\t\t_widget.show();\n\t\tupdateGlobalPosition();\n\t}\n\twatchParent();\n}\n\nvoid Preview::toggle(ScalePreviewShow show, int scale, int sliderX) {\n\tif (show == ScalePreviewShow::Hide) {\n\t\ttoggleShown(false);\n\t\treturn;\n\t} else if (show == ScalePreviewShow::Update && !_shown) {\n\t\treturn;\n\t}\n\tupdateToScale(scale);\n\tupdateGlobalPosition(sliderX);\n\tif (_widget.isHidden()) {\n\t\tUi::ForceFullRepaintSync(&_widget);\n\t}\n\ttoggleShown(true);\n}\n\nvoid Preview::toggleShown(bool shown) {\n\tif (_shown == shown) {\n\t\treturn;\n\t}\n\t_shown = shown;\n\ttoggleFilter();\n\tif (_shown) {\n\t\t_widget.show();\n\t} else if (_widget.isHidden()) {\n\t\t_shownAnimation.stop();\n\t\treturn;\n\t}\n\tconst auto callback = [=] {\n\t\tupdate();\n\t\tif (!_shown && !_shownAnimation.animating()) {\n\t\t\t_widget.hide();\n\t\t}\n\t};\n\t_shownAnimation.start(\n\t\tcallback,\n\t\tshown ? 0. : 1.,\n\t\tshown ? 1. : 0.,\n\t\tst::slideWrapDuration);\n}\n\nvoid Preview::toggleFilter() {\n\tif (!_shown) {\n\t\t_filter = nullptr;\n\t\treturn;\n\t} else if (_filter) {\n\t\treturn;\n\t}\n\t_filter = std::make_unique<QObject>();\n\tconst auto watch = [&](QWidget *widget, const auto &self) -> void {\n\t\tif (!widget) {\n\t\t\treturn;\n\t\t}\n\t\tbase::install_event_filter(_filter.get(), widget, [=](\n\t\t\t\tnot_null<QEvent*> e) {\n\t\t\tif (e->type() == QEvent::Move\n\t\t\t\t|| e->type() == QEvent::Resize\n\t\t\t\t|| e->type() == QEvent::Show\n\t\t\t\t|| e->type() == QEvent::ShowToParent\n\t\t\t\t|| e->type() == QEvent::ZOrderChange) {\n\t\t\t\tupdateGlobalPosition();\n\t\t\t}\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t});\n\t\tif (!_window && widget == _widget.window()) {\n\t\t\treturn;\n\t\t}\n\t\tself(widget->parentWidget(), self);\n\t};\n\twatch(_slider, watch);\n\n\tconst auto checkDeactivation = [=](Qt::ApplicationState state) {\n\t\tif (state != Qt::ApplicationActive) {\n\t\t\ttoggle(ScalePreviewShow::Hide, 0, 0);\n\t\t}\n\t};\n\tQObject::connect(\n\t\tqApp,\n\t\t&QGuiApplication::applicationStateChanged,\n\t\t_filter.get(),\n\t\tcheckDeactivation,\n\t\tQt::QueuedConnection);\n}\n\nvoid Preview::update() {\n\t_widget.update(_outer);\n}\n\nvoid Preview::init() {\n\tconst auto background = Window::Theme::Background();\n\tconst auto &paper = background->paper();\n\t_theme.setBackground({\n\t\t.prepared = background->prepared(),\n\t\t.preparedForTiled = background->preparedForTiled(),\n\t\t.gradientForFill = background->gradientForFill(),\n\t\t.colorForFill = background->colorForFill(),\n\t\t.colors = paper.backgroundColors(),\n\t\t.patternOpacity = paper.patternOpacity(),\n\t\t.gradientRotation = paper.gradientRotation(),\n\t\t.isPattern = paper.isPattern(),\n\t\t.tile = background->tile(),\n\t});\n\n\t_widget.paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = Painter(&_widget);\n\t\tpaint(p, clip);\n\t}, _widget.lifetime());\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_bubbleCorners = {};\n\t\t_bubbleTail = {};\n\t\t_bubbleShadowBottomRight = {};\n\t\tupdate();\n\t}, _widget.lifetime());\n\n\tif (_window) {\n\t\tinitAsWindow();\n\t\tupdateToScale(style::kScaleMin);\n\t\t_minOuterSize = _outer.size();\n\t\tupdateToScale(style::MaxScaleForRatio(_ratio));\n\t\t_maxOuterSize = _outer.size();\n\t}\n}\n\nint Preview::scaled(int value) const {\n\treturn style::ConvertScale(value, _scale);\n}\n\nQPoint Preview::scaled(QPoint value) const {\n\treturn { scaled(value.x()), scaled(value.y()) };\n}\n\nQMargins Preview::scaled(QMargins value) const {\n\treturn {\n\t\tscaled(value.left()),\n\t\tscaled(value.top()),\n\t\tscaled(value.right()),\n\t\tscaled(value.bottom()),\n\t};\n}\n\nstyle::font Preview::scaled(const style::font &font, int size) const {\n\treturn style::font(scaled(size), font->flags(), font->family());\n}\n\nstyle::QuoteStyle Preview::scaled(const style::QuoteStyle &value) const {\n\treturn {\n\t\t.icon = value.icon,\n\t\t.scrollable = value.scrollable,\n\t};\n}\n\nstyle::TextStyle Preview::scaled(\n\t\tconst style::TextStyle &value,\n\t\tint fontSize) const {\n\treturn {\n\t\t.font = scaled(value.font, fontSize),\n\t\t.linkUnderline = value.linkUnderline,\n\t\t.blockquote = scaled(value.blockquote),\n\t\t.pre = scaled(value.pre),\n\t};\n}\n\nQImage Preview::scaled(\n\t\tconst style::icon &icon,\n\t\tconst QColor &color) const {\n\treturn icon.instance(color, _scale);\n}\n\nvoid Preview::updateToScale(int scale) {\n\tusing style::ConvertScale;\n\n\tif (_scale == scale) {\n\t\treturn;\n\t}\n\t_scale = scale;\n\t_nameStyle = scaled(st::fwdTextStyle, 13);\n\t_textStyle = scaled(st::messageTextStyle, 13);\n\t_textStyle.blockquote.verticalSkip = scaled(4);\n\t_textStyle.blockquote.outline = scaled(3);\n\t_textStyle.blockquote.outlineShift = scaled(2);\n\t_textStyle.blockquote.radius = scaled(5);\n\t_textStyle.blockquote.padding = scaled(QMargins{ 10, 2, 20, 2 });\n\t_textStyle.blockquote.iconPosition = scaled(QPoint{ 4, 4 });\n\t_textStyle.pre.verticalSkip = scaled(4);\n\t_textStyle.pre.outline = scaled(3);\n\t_textStyle.pre.outlineShift = scaled(2);\n\t_textStyle.pre.radius = scaled(5);\n\t_textStyle.pre.header = scaled(20);\n\t_textStyle.pre.headerPosition = scaled(QPoint{ 10, 2 });\n\t_textStyle.pre.padding = scaled(QMargins{ 10, 2, 4, 2 });\n\t_textStyle.pre.iconPosition = scaled(QPoint{ 4, 2 });\n\t_nameText.setText(\n\t\t_nameStyle,\n\t\tu\"Bob Harris\"_q,\n\t\tUi::NameTextOptions());\n\t_replyText.setText(\n\t\t_textStyle,\n\t\tu\"Good morning!\"_q,\n\t\tUi::ItemTextDefaultOptions());\n\t_messageText.setText(\n\t\t_textStyle,\n\t\tu\"Do you know what time it is?\"_q,\n\t\tUi::ItemTextDefaultOptions());\n\n\tconst auto namePosition = QPoint(\n\t\tscaled(11), // st::historyReplyPadding.left()\n\t\tscaled(2)); // st::historyReplyPadding.top()\n\tconst auto replyPosition = QPoint(\n\t\tscaled(11), // st::historyReplyPadding.left()\n\t\t(scaled(2) // st::historyReplyPadding.top()\n\t\t\t+ _nameStyle.font->height)); // + st::msgServiceNameFont->height\n\tconst auto paddingRight = scaled(6); // st::historyReplyPadding.right()\n\n\tconst auto wantedWidth = std::max({\n\t\tnamePosition.x() + _nameText.maxWidth() + paddingRight,\n\t\treplyPosition.x() + _replyText.maxWidth() + paddingRight,\n\t\t_messageText.maxWidth(),\n\t});\n\n\tconst auto minTextWidth = scaled(kMinTextWidth);\n\tconst auto maxTextWidth = scaled(kMaxTextWidth);\n\tconst auto messageWidth = std::clamp(\n\t\twantedWidth,\n\t\tminTextWidth,\n\t\tmaxTextWidth);\n\tconst auto messageHeight = std::min(\n\t\t_messageText.countHeight(maxTextWidth),\n\t\tkMaxTextLines * _textStyle.font->height);\n\n\t_replyRect = QRect(\n\t\t0, // st::msgReplyBarPos.x(),\n\t\tscaled(2),// st::historyReplyTop\n\t\tmessageWidth,\n\t\t(scaled(2) // st::historyReplyPadding.top()\n\t\t\t+ _nameStyle.font->height // + st::msgServiceNameFont->height\n\t\t\t+ _textStyle.font->height // + st::normalFont->height\n\t\t\t+ scaled(2))); // + st::historyReplyPadding.bottom()\n\n\t_name = QRect(\n\t\t_replyRect.topLeft() + namePosition,\n\t\tQSize(messageWidth - namePosition.x(), _nameStyle.font->height));\n\t_reply = QRect(\n\t\t_replyRect.topLeft() + replyPosition,\n\t\tQSize(messageWidth - replyPosition.x(), _textStyle.font->height));\n\t_message = QRect(0, 0, messageWidth, messageHeight);\n\n\t// replyRect.bottom + st::historyReplyBottom;\n\tconst auto replySkip = _replyRect.y() + _replyRect.height() + scaled(2);\n\t_message.moveTop(replySkip);\n\n\t_content = QRect(0, 0, messageWidth, replySkip + messageHeight);\n\n\tconst auto msgPadding = scaled(QMargins(13, 7, 13, 8)); // st::msgPadding\n\t_bubble = _content.marginsAdded(msgPadding);\n\t_content.moveTopLeft(-_bubble.topLeft());\n\t_bubble.moveTopLeft({});\n\t_bubbleShadow = scaled(2); // st::msgShadow\n\t_bubbleCorners = {};\n\t_bubbleTail = {};\n\t_bubbleShadowBottomRight = {};\n\n\tconst auto hasUserpic = !_userpicOriginal.isNull();\n\tconst auto bubbleMargin = scaled(QMargins(20, 16, 20, 16));\n\tconst auto userpicSkip = hasUserpic ? scaled(40) : 0; // st::msgPhotoSkip\n\t_inner = _bubble.marginsAdded(\n\t\tbubbleMargin + QMargins(userpicSkip, 0, 0, 0));\n\t_bubble.moveTopLeft(-_inner.topLeft());\n\t_inner.moveTopLeft({});\n\tif (hasUserpic) {\n\t\tconst auto userpicSize = scaled(33); // st::msgPhotoSize\n\t\t_userpic = QRect(\n\t\t\tbubbleMargin.left(),\n\t\t\t_bubble.y() + _bubble.height() - userpicSize,\n\t\t\tuserpicSize,\n\t\t\tuserpicSize);\n\t\t_userpicImage = {};\n\t}\n\n\t_shadow.extend = scaled(QMargins(9, 8, 9, 10)); // st::callShadow.extend\n\t_shadowSides = {};\n\t_shadowCorners = {};\n\n\tupdate();\n\t_outer = _inner.marginsAdded(_shadow.extend);\n\t_inner.moveTopLeft(-_outer.topLeft());\n\t_outer.moveTopLeft({});\n\n\t_layer = QImage(\n\t\t_outer.size() * _ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_layer.setDevicePixelRatio(_ratio);\n\t_canvas = QImage(\n\t\t_inner.size() * _ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_canvas.setDevicePixelRatio(_ratio);\n\t_canvas.fill(Qt::transparent);\n\n\t_canvasCornerMasks = Images::CornersMask(scaled(6)); // st::callRadius\n}\n\nvoid Preview::updateGlobalPosition(int sliderX) {\n\t_localShiftLeft = sliderX;\n\tif (_window) {\n\t\tupdateWindowGlobalPosition(_slider->mapToGlobal(QPoint()));\n\t} else {\n\t\tupdateGlobalPosition();\n\t}\n}\n\nvoid Preview::updateGlobalPosition() {\n\tif (_window) {\n\t\tconst auto global = _slider->mapToGlobal(QPoint());\n\t\tupdateWindowGlobalPosition(global);\n\t} else {\n\t\tconst auto parent = _widget.parentWidget();\n\t\tconst auto global = Ui::MapFrom(parent, _slider, QPoint());\n\t\tconst auto desiredLeft = global.x()\n\t\t\t+ _localShiftLeft\n\t\t\t- (_outer.width() / 2);\n\t\tconst auto desiredTop = global.y() - _outer.height();\n\t\tconst auto requiredRight = std::min(\n\t\t\tdesiredLeft + _outer.width(),\n\t\t\tparent->width());\n\t\tconst auto left = std::max(\n\t\t\tstd::min(desiredLeft, requiredRight - _outer.width()),\n\t\t\t0);\n\t\t_widget.setGeometry(QRect(QPoint(left, desiredTop), _outer.size()));\n\t}\n\t_widget.raise();\n}\n\nvoid Preview::updateWindowGlobalPosition(QPoint global) {\n\tconst auto desiredLeft = global.x() - (_minOuterSize.width() / 2);\n\tconst auto desiredRight = global.x()\n\t\t+ _slider->width()\n\t\t+ (_maxOuterSize.width() / 2);\n\tconst auto requiredLeft = desiredRight - _maxOuterSize.width();\n\tconst auto left = std::min(desiredLeft, requiredLeft);\n\tconst auto requiredRight = left + _maxOuterSize.width();\n\tconst auto right = std::max(desiredRight, requiredRight);\n\tconst auto top = global.y() - _maxOuterSize.height();\n\tauto result = QRect(left, top, right - left, _maxOuterSize.height());\n\t_widget.setGeometry(adjustByScreenGeometry(result));\n\tupdateOuterPosition(global.x() + _localShiftLeft);\n}\n\nQRect Preview::adjustByScreenGeometry(QRect geometry) const {\n\tconst auto screen = _slider->screen();\n\tif (!screen) {\n\t\treturn geometry;\n\t}\n\tconst auto screenGeometry = screen->availableGeometry();\n\tif (!screenGeometry.intersects(geometry)\n\t\t|| screenGeometry.width() < _maxOuterSize.width()\n\t\t|| screenGeometry.height() < _maxOuterSize.height()) {\n\t\treturn geometry;\n\t}\n\tconst auto edgeLeft = screenGeometry.x();\n\tconst auto edgeRight = screenGeometry.x() + screenGeometry.width();\n\tconst auto edgedRight = std::min(\n\t\tedgeRight,\n\t\tgeometry.x() + geometry.width());\n\tconst auto left = std::max(\n\t\tstd::min(geometry.x(), edgedRight - _maxOuterSize.width()),\n\t\tedgeLeft);\n\tconst auto right = std::max(edgedRight, left + _maxOuterSize.width());\n\treturn { left, geometry.y(), right - left, geometry.height() };\n}\n\nvoid Preview::updateOuterPosition(int globalX) {\n\tif (_window) {\n\t\tupdate();\n\t\tconst auto global = _widget.geometry();\n\t\tconst auto desiredLeft = globalX\n\t\t\t- (_outer.width() / 2)\n\t\t\t- global.x();\n\t\t_outer.moveLeft(std::max(\n\t\t\tstd::min(desiredLeft, global.width() - _outer.width()),\n\t\t\t0));\n\t\t_outer.moveTop(_maxOuterSize.height() - _outer.height());\n\t\tupdate();\n\t}\n}\n\nvoid Preview::paint(Painter &p, QRect clip) {\n\t//p.setCompositionMode(QPainter::CompositionMode_Source);\n\t//p.fillRect(clip, Qt::transparent);\n\t//p.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\n\tconst auto outer = clip.intersected(_outer);\n\tif (outer.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto local = outer.translated(-_outer.topLeft());\n\tauto q = Painter(&_layer);\n\tq.setClipRect(local);\n\tpaintLayer(q, local);\n\tq.end();\n\n\tconst auto shown = _shownAnimation.value(_shown ? 1. : 0.);\n\tp.setClipRect(clip);\n\tp.setOpacity(shown);\n\tauto hq = std::optional<PainterHighQualityEnabler>();\n\tif (shown < 1.) {\n\t\tconst auto middle = _outer.x() + (_outer.width() / 2);\n\t\tconst auto bottom = _outer.y() + _outer.height();\n\t\tconst auto scale = 0.3 + shown * 0.7;\n\t\tp.translate(middle, bottom);\n\t\tp.scale(scale, scale);\n\t\tp.translate(-middle, -bottom);\n\t\thq.emplace(p);\n\t}\n\tp.drawImage(_outer.topLeft(), _layer);\n}\n\nvoid Preview::paintLayer(Painter &p, QRect clip) {\n\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\tvalidateShadowCache();\n\tUi::Shadow::paint(\n\t\tp,\n\t\t_inner,\n\t\t_outer.width(),\n\t\t_shadow,\n\t\t_shadowSides,\n\t\t_shadowCorners);\n\n\tconst auto inner = clip.intersected(_inner);\n\tif (inner.isEmpty()) {\n\t\treturn;\n\t}\n\tconst auto local = inner.translated(-_inner.topLeft());\n\tauto q = Painter(&_canvas);\n\tq.setClipRect(local);\n\tpaintInner(q, local);\n\tq.end();\n\t_canvas = Images::Round(std::move(_canvas), _canvasCornerMasks);\n\n\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\tp.drawImage(_inner.topLeft(), _canvas);\n}\n\nvoid Preview::paintInner(Painter &p, QRect clip) {\n\tWindow::SectionWidget::PaintBackground(\n\t\tp,\n\t\t&_theme,\n\t\tQSize(_inner.width(), _inner.width() * 3),\n\t\tclip);\n\n\tpaintUserpic(p, clip);\n\n\tp.translate(_bubble.topLeft());\n\tpaintBubble(p, clip.translated(-_bubble.topLeft()));\n}\n\nvoid Preview::paintUserpic(Painter &p, QRect clip) {\n\tif (clip.intersected(_userpic).isEmpty()) {\n\t\treturn;\n\t}\n\tvalidateUserpicCache();\n\tp.drawImage(_userpic.topLeft(), _userpicImage);\n}\n\nvoid Preview::paintBubble(Painter &p, QRect clip) {\n\tvalidateBubbleCache();\n\tconst auto bubble = QRect(QPoint(), _bubble.size());\n\tconst auto cornerShadow = _bubbleShadowBottomRight.size()\n\t\t/ _bubbleShadowBottomRight.devicePixelRatio();\n\tp.drawPixmap(\n\t\tbubble.width() - cornerShadow.width(),\n\t\tbubble.height() + _bubbleShadow - cornerShadow.height(),\n\t\t_bubbleShadowBottomRight);\n\tUi::FillRoundRect(p, bubble, st::msgInBg, _bubbleCorners);\n\tconst auto tail = _bubbleTail.size() / _bubbleTail.devicePixelRatio();\n\tp.drawImage(-tail.width(), bubble.height() - tail.height(), _bubbleTail);\n\tp.fillRect(\n\t\t-tail.width(),\n\t\tbubble.height(),\n\t\ttail.width() + bubble.width() - cornerShadow.width(),\n\t\t_bubbleShadow,\n\t\tst::msgInShadow);\n\n\tconst auto content = clip.intersected(_content);\n\tif (content.isEmpty()) {\n\t\treturn;\n\t}\n\tp.translate(_content.topLeft());\n\tconst auto local = content.translated(-_content.topLeft());\n\tp.setClipRect(local);\n\tpaintContent(p, local);\n}\n\nvoid Preview::paintContent(Painter &p, QRect clip) {\n\tpaintReply(p, clip);\n\n\tconst auto message = clip.intersected(_message);\n\tif (message.isEmpty()) {\n\t\treturn;\n\t}\n\tp.translate(_message.topLeft());\n\tconst auto local = message.translated(-_message.topLeft());\n\tp.setClipRect(local);\n\tpaintMessage(p, local);\n}\n\nvoid Preview::paintReply(Painter &p, QRect clip) {\n\t{\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::msgInReplyBarColor);\n\n\t\tconst auto outline = _textStyle.blockquote.outline;\n\t\tconst auto radius = _textStyle.blockquote.radius;\n\t\tp.setOpacity(Ui::kDefaultOutline1Opacity);\n\t\tp.setClipRect(\n\t\t\t_replyRect.x(),\n\t\t\t_replyRect.y(),\n\t\t\toutline,\n\t\t\t_replyRect.height());\n\t\tp.drawRoundedRect(_replyRect, radius, radius);\n\t\tp.setOpacity(Ui::kDefaultBgOpacity);\n\t\tp.setClipRect(\n\t\t\t_replyRect.x() + outline,\n\t\t\t_replyRect.y(),\n\t\t\t_replyRect.width() - outline,\n\t\t\t_replyRect.height());\n\t\tp.drawRoundedRect(_replyRect, radius, radius);\n\t}\n\tp.setOpacity(1.);\n\tp.setClipping(false);\n\n\tp.setPen(st::msgInServiceFg);\n\t_nameText.drawLeftElided(\n\t\tp,\n\t\t_name.x(),\n\t\t_name.y(),\n\t\t_name.width(),\n\t\t_content.width());\n\n\tp.setPen(st::historyTextInFg);\n\t_replyText.drawLeftElided(\n\t\tp,\n\t\t_reply.x(),\n\t\t_reply.y(),\n\t\t_reply.width(),\n\t\t_content.width());\n}\n\nvoid Preview::paintMessage(Painter &p, QRect clip) {\n\tp.setPen(st::historyTextInFg);\n\t_messageText.drawLeftElided(\n\t\tp,\n\t\t0,\n\t\t0,\n\t\t_message.width(),\n\t\t_message.width(),\n\t\tkMaxTextLines);\n}\n\nvoid Preview::validateUserpicCache() {\n\tif (!_userpicImage.isNull()\n\t\t|| _userpicOriginal.isNull()\n\t\t|| _userpic.isEmpty()) {\n\t\treturn;\n\t}\n\t_userpicImage = Images::Circle(_userpicOriginal.scaled(\n\t\t_userpic.size() * _ratio,\n\t\tQt::IgnoreAspectRatio,\n\t\tQt::SmoothTransformation));\n\t_userpicImage.setDevicePixelRatio(_ratio);\n}\n\nvoid Preview::validateBubbleCache() {\n\tif (!_bubbleCorners.p[0].isNull()) {\n\t\treturn;\n\t}\n\tconst auto radius = scaled(16); // st::bubbleRadiusLarge\n\t_bubbleCorners = Ui::PrepareCornerPixmaps(radius, st::msgInBg);\n\t_bubbleCorners.p[2] = {};\n\t_bubbleTail = scaled(st::historyBubbleTailInLeft, st::msgInBg->c);\n\t_bubbleShadowBottomRight\n\t\t= Ui::PrepareCornerPixmaps(radius, st::msgInShadow).p[3];\n}\n\nvoid Preview::validateShadowCache() {\n\tif (!_shadowSides[0].isNull()) {\n\t\treturn;\n\t}\n\tconst auto &shadowColor = st::windowShadowFg->c;\n\t_shadowSides[0] = scaled(st::callShadow.left, shadowColor);\n\t_shadowSides[1] = scaled(st::callShadow.top, shadowColor);\n\t_shadowSides[2] = scaled(st::callShadow.right, shadowColor);\n\t_shadowSides[3] = scaled(st::callShadow.bottom, shadowColor);\n\t_shadowCorners[0] = scaled(st::callShadow.topLeft, shadowColor);\n\t_shadowCorners[1] = scaled(st::callShadow.bottomLeft, shadowColor);\n\t_shadowCorners[2] = scaled(st::callShadow.topRight, shadowColor);\n\t_shadowCorners[3] = scaled(st::callShadow.bottomRight, shadowColor);\n}\n\nvoid Preview::initAsWindow() {\n\t_widget.setWindowFlags(Qt::WindowFlags(Qt::FramelessWindowHint)\n\t\t| Qt::BypassWindowManagerHint\n\t\t| Qt::NoDropShadowWindowHint\n\t\t| Qt::ToolTip);\n\t_widget.setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_widget.hide();\n\n\t_widget.setAttribute(Qt::WA_NoSystemBackground);\n\t_widget.setAttribute(Qt::WA_TranslucentBackground);\n}\n\n} // namespace\n\n[[nodiscard]] Fn<void(ScalePreviewShow, int, int)> SetupScalePreview(\n\t\tnot_null<Window::Controller*> window,\n\t\tnot_null<Ui::RpWidget*> slider) {\n\tconst auto controller = window->sessionController();\n\tconst auto user = controller\n\t\t? controller->session().user().get()\n\t\t: nullptr;\n\tconst auto preview = slider->lifetime().make_state<Preview>(\n\t\tslider.get(),\n\t\tuser ? Data::PeerUserpicImageValue(user, 160, 0) : nullptr);\n\treturn [=](ScalePreviewShow show, int scale, int globalX) {\n\t\tpreview->toggle(show, scale, globalX);\n\t};\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_scale_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Window {\nclass Controller;\n} // namespace Window\n\nnamespace Settings {\n\nenum class ScalePreviewShow {\n\tShow,\n\tUpdate,\n\tHide,\n};\n\n[[nodiscard]] Fn<void(ScalePreviewShow, int, int)> SetupScalePreview(\n\tnot_null<Window::Controller*> window,\n\tnot_null<Ui::RpWidget*> slider);\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_search.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings/settings_search.h\"\n\n#include \"base/event_filter.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common.h\"\n#include \"settings/settings_faq_suggestions.h\"\n#include \"settings/settings_recent_searches.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/text_entity.h\"\n#include \"ui/search_field_controller.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/elastic_scroll.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Settings {\nnamespace {\n\nstruct SearchResultItem {\n\tint index = 0;\n\tint matchCount = 0;\n};\n\n[[nodiscard]] QStringList PrepareEntryWords(const Builder::SearchEntry &entry) {\n\tauto combined = entry.title;\n\tfor (const auto &keyword : entry.keywords) {\n\t\tcombined += ' ' + keyword;\n\t}\n\treturn TextUtilities::PrepareSearchWords(combined);\n}\n\n[[nodiscard]] int CalculateDepth(\n\t\tType sectionId,\n\t\tconst Builder::SearchRegistry &registry) {\n\tconst auto path = registry.sectionPath(sectionId);\n\tif (path.isEmpty()) {\n\t\treturn 0;\n\t}\n\treturn path.count(u\" > \"_q) + 1;\n}\n\nvoid SetupCheckIcon(\n\t\tnot_null<Ui::SettingsButton*> button,\n\t\tBuilder::SearchEntryCheckIcon checkIcon,\n\t\tconst style::SettingsButton &st) {\n\tstruct CheckWidget {\n\t\tCheckWidget(QWidget *parent, bool checked)\n\t\t: widget(parent)\n\t\t, view(st::defaultCheck, checked) {\n\t\t\tview.finishAnimating();\n\t\t}\n\t\tUi::RpWidget widget;\n\t\tUi::CheckView view;\n\t};\n\tconst auto checked = (checkIcon == Builder::SearchEntryCheckIcon::Checked);\n\tconst auto check = button->lifetime().make_state<CheckWidget>(\n\t\tbutton,\n\t\tchecked);\n\tcheck->widget.setAttribute(Qt::WA_TransparentForMouseEvents);\n\tcheck->widget.resize(check->view.getSize());\n\tcheck->widget.show();\n\n\tbutton->sizeValue(\n\t) | rpl::on_next([=, left = st.iconLeft](QSize size) {\n\t\tcheck->widget.moveToLeft(\n\t\t\tleft,\n\t\t\t(size.height() - check->widget.height()) / 2,\n\t\t\tsize.width());\n\t}, check->widget.lifetime());\n\n\tcheck->widget.paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(&check->widget);\n\t\tcheck->view.paint(p, 0, 0, check->widget.width());\n\t\tp.setOpacity(0.5);\n\t\tp.fillRect(clip, st::boxBg);\n\t}, check->widget.lifetime());\n}\n\n[[nodiscard]] not_null<Ui::SettingsButton*> CreateSearchResultButtonRaw(\n\t\tnot_null<QWidget*> parent,\n\t\tconst QString &title,\n\t\tconst QString &subtitle,\n\t\tconst style::SettingsButton &st,\n\t\tIconDescriptor &&icon,\n\t\tBuilder::SearchEntryCheckIcon checkIcon) {\n\tauto buttonObj = CreateButtonWithIcon(\n\t\tparent,\n\t\trpl::single(title),\n\t\tst,\n\t\tstd::move(icon));\n\tconst auto button = buttonObj.release();\n\tbutton->setPointerCursor(false);\n\tif (checkIcon != Builder::SearchEntryCheckIcon::None) {\n\t\tSetupCheckIcon(button, checkIcon, st);\n\t}\n\tconst auto details = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton,\n\t\tsubtitle,\n\t\tst::settingsSearchResultDetails);\n\tdetails->show();\n\tdetails->moveToLeft(\n\t\tst.padding.left(),\n\t\tst.padding.top() + st.height - details->height());\n\tdetails->setAttribute(Qt::WA_TransparentForMouseEvents);\n\treturn button;\n}\n\n} // namespace\n\nSearch::Search(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: Section(parent, controller) {\n\tsetupContent();\n}\n\nrpl::producer<QString> Search::title() {\n\treturn tr::lng_dlg_filter();\n}\n\nvoid Search::setInnerFocus() {\n\tif (_searchField) {\n\t\t_searchField->setFocus();\n\t}\n}\n\nbase::weak_qptr<Ui::RpWidget> Search::createPinnedToTop(\n\t\tnot_null<QWidget*> parent) {\n\t_searchController = std::make_unique<Ui::SearchFieldController>(\"\");\n\tauto rowView = _searchController->createRowView(\n\t\tparent,\n\t\tst::infoLayerMediaSearch);\n\t_searchField = rowView.field;\n\t_searchField->customUpDown(true);\n\n\tconst auto searchContainer = Ui::CreateChild<Ui::FixedHeightWidget>(\n\t\tparent.get(),\n\t\tst::infoLayerMediaSearch.height);\n\tconst auto wrap = rowView.wrap.release();\n\twrap->setParent(searchContainer);\n\twrap->show();\n\n\tsearchContainer->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\twrap->resizeToWidth(width);\n\t\twrap->moveToLeft(0, 0);\n\t}, searchContainer->lifetime());\n\n\t_searchController->queryChanges() | rpl::on_next([=](QString &&query) {\n\t\trebuildResults(std::move(query));\n\t}, searchContainer->lifetime());\n\n\t_searchField->submits(\n\t) | rpl::on_next([=](Qt::KeyboardModifiers) {\n\t\tconst auto index = (_selected >= 0) ? _selected : 0;\n\t\tif (index < int(_visibleButtons.size())) {\n\t\t\t_visibleButtons[index]->clicked(\n\t\t\t\tQt::NoModifier,\n\t\t\t\tQt::LeftButton);\n\t\t}\n\t}, searchContainer->lifetime());\n\n\tbase::install_event_filter(_searchField, [=](not_null<QEvent*> e) {\n\t\tif (e->type() != QEvent::KeyPress) {\n\t\t\treturn base::EventFilterResult::Continue;\n\t\t}\n\t\tconst auto key = static_cast<QKeyEvent*>(e.get())->key();\n\t\tif (key == Qt::Key_Up\n\t\t\t|| key == Qt::Key_Down\n\t\t\t|| key == Qt::Key_PageUp\n\t\t\t|| key == Qt::Key_PageDown) {\n\t\t\thandleKeyNavigation(key);\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t}, lifetime());\n\n\tif (!_pendingQuery.isEmpty()) {\n\t\t_searchField->setText(base::take(_pendingQuery));\n\t}\n\n\treturn base::make_weak(not_null<Ui::RpWidget*>{ searchContainer });\n}\n\nvoid Search::setupContent() {\n\tconst auto content = Ui::CreateChild<Ui::VerticalLayout>(this);\n\n\t_list = content->add(object_ptr<Ui::VerticalLayout>(content));\n\n\tsetupCustomizations();\n\tbuildIndex();\n\trebuildResults(QString());\n\n\tcontroller()->session().faqSuggestions().loadedValue(\n\t) | rpl::filter([](bool loaded) {\n\t\treturn loaded;\n\t}) | rpl::take(1) | rpl::on_next([=] {\n\t\tfor (auto i = _faqStartIndex; i < int(_entries.size()); ++i) {\n\t\t\tconst auto it = _buttonCache.find(i);\n\t\t\tif (it != _buttonCache.end()) {\n\t\t\t\t_trackedButtons.remove(it->second);\n\t\t\t\tdelete it->second;\n\t\t\t\t_buttonCache.erase(it);\n\t\t\t}\n\t\t}\n\t\tbuildIndex();\n\t\tconst auto query = _searchController\n\t\t\t? _searchController->query()\n\t\t\t: QString();\n\t\trebuildResults(query);\n\t}, lifetime());\n\n\tUi::ResizeFitChild(this, content);\n}\n\nvoid Search::setupCustomizations() {\n\tconst auto isPaused = Window::PausedIn(\n\t\tcontroller(),\n\t\tWindow::GifPauseReason::Layer);\n\tconst auto add = [&](const QString &id, ResultCustomization value) {\n\t\t_customizations[id] = std::move(value);\n\t};\n\n\tadd(u\"main/credits\"_q, {\n\t\t.hook = [=](not_null<Ui::SettingsButton*> b) {\n\t\t\tAddPremiumStar(b, true, isPaused);\n\t\t},\n\t\t.st = &st::settingsSearchResult,\n\t});\n\tadd(u\"main/premium\"_q, {\n\t\t.hook = [=](not_null<Ui::SettingsButton*> b) {\n\t\t\tAddPremiumStar(b, false, isPaused);\n\t\t},\n\t\t.st = &st::settingsSearchResult,\n\t});\n}\n\nvoid Search::buildIndex() {\n\t_entries.clear();\n\t_firstLetterIndex.clear();\n\t_entryIdToIndex.clear();\n\n\tconst auto &registry = Builder::SearchRegistry::Instance();\n\tconst auto rawEntries = registry.collectAll(&controller()->session());\n\n\t_entries.reserve(rawEntries.size());\n\tfor (const auto &entry : rawEntries) {\n\t\tconst auto index = int(_entries.size());\n\t\tauto indexed = IndexedEntry{\n\t\t\t.entry = entry,\n\t\t\t.terms = PrepareEntryWords(entry),\n\t\t\t.depth = CalculateDepth(entry.section, registry),\n\t\t};\n\t\tif (!entry.id.isEmpty()) {\n\t\t\t_entryIdToIndex[entry.id] = index;\n\t\t}\n\t\t_entries.push_back(std::move(indexed));\n\t}\n\n\t_faqStartIndex = int(_entries.size());\n\n\tconst auto &faq = controller()->session().faqSuggestions();\n\tfor (const auto &faqEntry : faq.entries()) {\n\t\tauto entry = Builder::SearchEntry{\n\t\t\t.title = faqEntry.title,\n\t\t};\n\t\tauto indexed = IndexedEntry{\n\t\t\t.entry = std::move(entry),\n\t\t\t.terms = TextUtilities::PrepareSearchWords(faqEntry.title),\n\t\t\t.depth = 1000,\n\t\t\t.faqUrl = faqEntry.url,\n\t\t\t.faqSection = faqEntry.section,\n\t\t};\n\t\t_entries.push_back(std::move(indexed));\n\t}\n\n\tfor (auto i = 0; i < int(_entries.size()); ++i) {\n\t\tfor (const auto &term : _entries[i].terms) {\n\t\t\tif (!term.isEmpty()) {\n\t\t\t\t_firstLetterIndex[term[0]].insert(i);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Search::clearSelection() {\n\tif (_selected >= 0 && _selected < int(_visibleButtons.size())) {\n\t\t_visibleButtons[_selected]->setSynteticOver(false);\n\t}\n\t_selected = -1;\n}\n\nvoid Search::scrollToButton(not_null<Ui::SettingsButton*> button) {\n\tconst auto scrollIn = [&](auto &&scroll) {\n\t\tif (const auto inner = scroll->widget()) {\n\t\t\tconst auto globalPos = button->mapToGlobal(QPoint(0, 0));\n\t\t\tconst auto localPos = inner->mapFromGlobal(globalPos);\n\t\t\tscroll->scrollToY(\n\t\t\t\tlocalPos.y(),\n\t\t\t\tlocalPos.y() + button->height());\n\t\t}\n\t};\n\tfor (auto widget = button->parentWidget()\n\t\t; widget\n\t\t; widget = widget->parentWidget()) {\n\t\tif (const auto scroll = dynamic_cast<Ui::ScrollArea*>(widget)) {\n\t\t\tscrollIn(scroll);\n\t\t\treturn;\n\t\t}\n\t\tif (const auto scroll = dynamic_cast<Ui::ElasticScroll*>(widget)) {\n\t\t\tscrollIn(scroll);\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nvoid Search::selectByKeyboard(int newSelected) {\n\tconst auto count = int(_visibleButtons.size());\n\tif (!count) {\n\t\treturn;\n\t}\n\tnewSelected = std::clamp(newSelected, 0, count - 1);\n\tif (newSelected == _selected) {\n\t\treturn;\n\t}\n\tconst auto applySelection = [&] {\n\t\tfor (auto i = 0; i < count; ++i) {\n\t\t\tif (i != newSelected && _visibleButtons[i]->isOver()) {\n\t\t\t\t_visibleButtons[i]->setSynteticOver(false);\n\t\t\t}\n\t\t}\n\t\t_selected = newSelected;\n\t\t_visibleButtons[_selected]->setSynteticOver(true);\n\t};\n\tapplySelection();\n\tscrollToButton(_visibleButtons[_selected]);\n\tapplySelection();\n}\n\nvoid Search::setupButtonMouseTracking(\n\t\tnot_null<Ui::SettingsButton*> button) {\n\tif (!_trackedButtons.emplace(button).second) {\n\t\treturn;\n\t}\n\tbutton->events(\n\t) | rpl::filter([](not_null<QEvent*> e) {\n\t\treturn e->type() == QEvent::Enter;\n\t}) | rpl::on_next([=] {\n\t\tif (_selected >= 0) {\n\t\t\tclearSelection();\n\t\t}\n\t}, button->lifetime());\n}\n\nvoid Search::handleKeyNavigation(int key) {\n\tconstexpr auto kPageSkip = 5;\n\tconst auto startIndex = [&] {\n\t\tif (_selected >= 0) {\n\t\t\treturn _selected;\n\t\t}\n\t\tfor (auto i = 0; i < int(_visibleButtons.size()); ++i) {\n\t\t\tif (_visibleButtons[i]->isOver()) {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t}();\n\n\tif (key == Qt::Key_Down) {\n\t\tselectByKeyboard((startIndex < 0) ? 0 : (startIndex + 1));\n\t} else if (key == Qt::Key_Up) {\n\t\tif (startIndex > 0) {\n\t\t\tselectByKeyboard(startIndex - 1);\n\t\t} else if (startIndex == 0) {\n\t\t\tclearSelection();\n\t\t}\n\t} else if (key == Qt::Key_PageDown) {\n\t\tselectByKeyboard((startIndex < 0) ? 0 : (startIndex + kPageSkip));\n\t} else if (key == Qt::Key_PageUp) {\n\t\tif (startIndex > 0) {\n\t\t\tselectByKeyboard(startIndex - kPageSkip);\n\t\t} else if (startIndex == 0) {\n\t\t\tclearSelection();\n\t\t}\n\t}\n}\n\nnot_null<Ui::SettingsButton*> Search::createEntryButton(\n\t\tint entryIndex,\n\t\tconst QString &subtitle) {\n\tconst auto &indexed = _entries[entryIndex];\n\tconst auto &entry = indexed.entry;\n\tconst auto hasIcon = entry.icon.icon != nullptr;\n\tconst auto hasCheckIcon = !hasIcon\n\t\t&& (entry.checkIcon != Builder::SearchEntryCheckIcon::None);\n\n\tconst auto it = _customizations.find(entry.id);\n\tconst auto custom = (it != _customizations.end())\n\t\t? &it->second\n\t\t: nullptr;\n\n\tconst auto &st = custom && custom->st\n\t\t? *custom->st\n\t\t: (hasIcon || hasCheckIcon)\n\t\t? st::settingsSearchResult\n\t\t: st::settingsSearchResultNoIcon;\n\n\tconst auto button = CreateSearchResultButtonRaw(\n\t\tthis,\n\t\tentry.title,\n\t\tsubtitle,\n\t\tst,\n\t\tIconDescriptor{ entry.icon.icon },\n\t\t(hasCheckIcon\n\t\t\t? entry.checkIcon\n\t\t\t: Builder::SearchEntryCheckIcon::None));\n\n\tif (custom && custom->hook) {\n\t\tcustom->hook(button);\n\t}\n\n\tconst auto controlId = entry.id;\n\tif (!controlId.isEmpty()) {\n\t\tconst auto targetSection = entry.section;\n\t\tconst auto deeplink = entry.deeplink;\n\t\tbutton->addClickHandler([=] {\n\t\t\tbumpRecentEntry(controlId);\n\t\t\tif (!deeplink.isEmpty()) {\n\t\t\t\tCore::App().openLocalUrl(\n\t\t\t\t\tdeeplink,\n\t\t\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t\t\t.sessionWindow = base::make_weak(controller()),\n\t\t\t\t\t}));\n\t\t\t} else {\n\t\t\t\tcontroller()->setHighlightControlId(controlId);\n\t\t\t\tshowOtherMethod()(targetSection);\n\t\t\t}\n\t\t});\n\n\t\tbase::install_event_filter(button, [=](not_null<QEvent*> e) {\n\t\t\tif (e->type() != QEvent::ContextMenu) {\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t}\n\t\t\tconst auto &recentIds\n\t\t\t\t= controller()->session().recentSettingsSearches().list();\n\t\t\tif (!ranges::contains(recentIds, controlId)) {\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t}\n\t\t\t_contextMenu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\tbutton,\n\t\t\t\tst::popupMenuWithIcons);\n\t\t\t_contextMenu->addAction(\n\t\t\t\ttr::lng_recent_remove(tr::now),\n\t\t\t\t[=] {\n\t\t\t\t\tcontroller()->session().recentSettingsSearches().remove(\n\t\t\t\t\t\tcontrolId);\n\t\t\t\t\tconst auto query = _searchController\n\t\t\t\t\t\t? _searchController->query()\n\t\t\t\t\t\t: QString();\n\t\t\t\t\trebuildResults(query);\n\t\t\t\t},\n\t\t\t\t&st::menuIconDelete);\n\t\t\t_contextMenu->popup(QCursor::pos());\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t}, button->lifetime());\n\t}\n\n\t_buttonCache.emplace(entryIndex, button);\n\treturn button;\n}\n\nvoid Search::rebuildResults(const QString &query) {\n\tfor (auto i = 0, count = _list->count(); i != count; ++i) {\n\t\t_list->widgetAt(i)->hide();\n\t}\n\t_list->clear();\n\tclearSelection();\n\t_visibleButtons.clear();\n\n\tconst auto queryWords = TextUtilities::PrepareSearchWords(query);\n\n\tif (queryWords.isEmpty()) {\n\t\trebuildRecentResults();\n\t\trebuildFaqResults();\n\t\t_list->resizeToWidth(_list->width());\n\t\treturn;\n\t}\n\n\tauto results = std::vector<SearchResultItem>();\n\t{\n\t\tauto toFilter = (const base::flat_set<int>*)nullptr;\n\t\tfor (const auto &word : queryWords) {\n\t\t\tif (word.isEmpty()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto it = _firstLetterIndex.find(word[0]);\n\t\t\tif (it == _firstLetterIndex.end() || it->second.empty()) {\n\t\t\t\ttoFilter = nullptr;\n\t\t\t\tbreak;\n\t\t\t} else if (!toFilter || it->second.size() < toFilter->size()) {\n\t\t\t\ttoFilter = &it->second;\n\t\t\t}\n\t\t}\n\n\t\tif (toFilter) {\n\t\t\tfor (const auto entryIndex : *toFilter) {\n\t\t\t\tconst auto &indexed = _entries[entryIndex];\n\t\t\t\tauto matched = 0;\n\t\t\t\tfor (const auto &queryWord : queryWords) {\n\t\t\t\t\tfor (const auto &term : indexed.terms) {\n\t\t\t\t\t\tif (term.startsWith(queryWord)) {\n\t\t\t\t\t\t\t++matched;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (matched > 0) {\n\t\t\t\t\tresults.push_back({\n\t\t\t\t\t\t.index = entryIndex,\n\t\t\t\t\t\t.matchCount = matched,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tranges::sort(results, [&](const auto &a, const auto &b) {\n\t\t\t\tif (a.matchCount != b.matchCount) {\n\t\t\t\t\treturn a.matchCount > b.matchCount;\n\t\t\t\t}\n\t\t\t\tconst auto &entryA = _entries[a.index];\n\t\t\t\tconst auto &entryB = _entries[b.index];\n\t\t\t\tif (entryA.depth != entryB.depth) {\n\t\t\t\t\treturn entryA.depth < entryB.depth;\n\t\t\t\t}\n\t\t\t\treturn entryA.entry.title < entryB.entry.title;\n\t\t\t});\n\t\t}\n\t}\n\n\tif (results.empty() && !queryWords.isEmpty()) {\n\t\t_list->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t_list,\n\t\t\t\ttr::lng_search_tab_no_results(),\n\t\t\t\tst::settingsSearchNoResults),\n\t\t\tst::settingsSearchNoResultsPadding);\n\t} else {\n\t\tconst auto &registry = Builder::SearchRegistry::Instance();\n\t\tconst auto faqSubtitle = tr::lng_settings_faq_subtitle(tr::now);\n\t\tconst auto weak = base::make_weak(controller());\n\n\t\tfor (const auto &result : results) {\n\t\t\tconst auto entryIndex = result.index;\n\t\t\tconst auto &indexed = _entries[entryIndex];\n\t\t\tconst auto &entry = indexed.entry;\n\t\t\tconst auto isFaq = !indexed.faqUrl.isEmpty();\n\n\t\t\tconst auto cached = _buttonCache.find(entryIndex);\n\t\t\tif (cached != _buttonCache.end()) {\n\t\t\t\taddButton(cached->second);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (isFaq) {\n\t\t\t\tconst auto subtitle = faqSubtitle\n\t\t\t\t\t+ u\" > \"_q\n\t\t\t\t\t+ indexed.faqSection;\n\t\t\t\tconst auto button = CreateSearchResultButtonRaw(\n\t\t\t\t\tthis,\n\t\t\t\t\tentry.title,\n\t\t\t\t\tsubtitle,\n\t\t\t\t\tst::settingsSearchResultNoIcon,\n\t\t\t\t\tIconDescriptor{},\n\t\t\t\t\tBuilder::SearchEntryCheckIcon::None);\n\n\t\t\t\tconst auto url = indexed.faqUrl;\n\t\t\t\tbutton->addClickHandler([=] {\n\t\t\t\t\tUrlClickHandler::Open(\n\t\t\t\t\t\turl,\n\t\t\t\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t\t\t\t.sessionWindow = weak,\n\t\t\t\t\t\t}));\n\t\t\t\t});\n\n\t\t\t\t_buttonCache.emplace(entryIndex, button);\n\t\t\t\taddButton(button);\n\t\t\t} else {\n\t\t\t\tconst auto parentsOnly = entry.id.isEmpty();\n\t\t\t\tconst auto subtitle = registry.sectionPath(\n\t\t\t\t\tentry.section,\n\t\t\t\t\tparentsOnly);\n\t\t\t\taddButton(createEntryButton(entryIndex, subtitle));\n\t\t\t}\n\t\t}\n\t}\n\n\t_list->resizeToWidth(_list->width());\n}\n\nvoid Search::sectionSaveState(std::any &state) {\n\tconst auto query = _searchController\n\t\t? _searchController->query()\n\t\t: _pendingQuery;\n\tif (!query.isEmpty()) {\n\t\tstate = SearchSectionState{ query };\n\t}\n}\n\nvoid Search::sectionRestoreState(const std::any &state) {\n\tconst auto saved = std::any_cast<SearchSectionState>(&state);\n\tif (saved && !saved->query.isEmpty()) {\n\t\tif (_searchField) {\n\t\t\t_searchField->setText(saved->query);\n\t\t} else {\n\t\t\t_pendingQuery = saved->query;\n\t\t}\n\t}\n}\n\nvoid Search::bumpRecentEntry(const QString &entryId) {\n\tif (!entryId.isEmpty()) {\n\t\tcontroller()->session().recentSettingsSearches().bump(entryId);\n\t}\n}\n\nvoid Search::rebuildRecentResults() {\n\tconst auto &recentIds\n\t\t= controller()->session().recentSettingsSearches().list();\n\tif (recentIds.empty()) {\n\t\treturn;\n\t}\n\n\tconst auto &registry = Builder::SearchRegistry::Instance();\n\n\tauto added = false;\n\tfor (const auto &entryId : recentIds) {\n\t\tconst auto it = _entryIdToIndex.find(entryId);\n\t\tif (it == _entryIdToIndex.end()) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!added) {\n\t\t\tUi::AddSubsectionTitle(_list, tr::lng_recent_title());\n\t\t\tadded = true;\n\t\t}\n\t\tconst auto entryIndex = it->second;\n\t\tconst auto cached = _buttonCache.find(entryIndex);\n\t\tif (cached != _buttonCache.end()) {\n\t\t\taddButton(cached->second);\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto &entry = _entries[entryIndex].entry;\n\t\tconst auto parentsOnly = entry.id.isEmpty();\n\t\tconst auto subtitle = registry.sectionPath(\n\t\t\tentry.section,\n\t\t\tparentsOnly);\n\t\taddButton(createEntryButton(entryIndex, subtitle));\n\t}\n}\n\nvoid Search::rebuildFaqResults() {\n\tif (_faqStartIndex >= int(_entries.size())) {\n\t\treturn;\n\t}\n\n\tif (!_visibleButtons.empty()) {\n\t\tUi::AddSubsectionTitle(_list, tr::lng_settings_faq());\n\t}\n\n\tconst auto faqSubtitle = tr::lng_settings_faq_subtitle(tr::now);\n\tconst auto weak = base::make_weak(controller());\n\n\tfor (auto i = _faqStartIndex; i < int(_entries.size()); ++i) {\n\t\tconst auto &indexed = _entries[i];\n\n\t\tconst auto cached = _buttonCache.find(i);\n\t\tif (cached != _buttonCache.end()) {\n\t\t\taddButton(cached->second);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto subtitle = faqSubtitle + u\" > \"_q + indexed.faqSection;\n\t\tconst auto button = CreateSearchResultButtonRaw(\n\t\t\tthis,\n\t\t\tindexed.entry.title,\n\t\t\tsubtitle,\n\t\t\tst::settingsSearchResultNoIcon,\n\t\t\tIconDescriptor{},\n\t\t\tBuilder::SearchEntryCheckIcon::None);\n\n\t\tconst auto url = indexed.faqUrl;\n\t\tbutton->addClickHandler([=] {\n\t\t\tUrlClickHandler::Open(\n\t\t\t\turl,\n\t\t\t\tQVariant::fromValue(ClickHandlerContext{\n\t\t\t\t\t.sessionWindow = weak,\n\t\t\t\t}));\n\t\t});\n\n\t\t_buttonCache.emplace(i, button);\n\t\taddButton(button);\n\t}\n}\n\nvoid Search::addButton(not_null<Ui::SettingsButton*> button) {\n\tbutton->show();\n\t_list->add(object_ptr<Ui::SettingsButton>::fromRaw(button));\n\t_visibleButtons.push_back(button);\n\tsetupButtonMouseTracking(button);\n}\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_search.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <any>\n\n#include \"base/flat_map.h\"\n#include \"settings/settings_builder.h\"\n#include \"settings/settings_common_session.h\"\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass InputField;\nclass PopupMenu;\nclass RpWidget;\nclass SearchFieldController;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Settings {\n\nstruct SearchSectionState {\n\tQString query;\n};\n\nclass Search : public Section<Search> {\npublic:\n\tSearch(QWidget *parent, not_null<Window::SessionController*> controller);\n\n\t[[nodiscard]] rpl::producer<QString> title() override;\n\n\tvoid setInnerFocus() override;\n\tvoid sectionSaveState(std::any &state) override;\n\tvoid sectionRestoreState(const std::any &state) override;\n\t[[nodiscard]] base::weak_qptr<Ui::RpWidget> createPinnedToTop(\n\t\tnot_null<QWidget*> parent) override;\n\nprivate:\n\tstruct IndexedEntry {\n\t\tBuilder::SearchEntry entry;\n\t\tQStringList terms;\n\t\tint depth = 0;\n\t\tQString faqUrl;\n\t\tQString faqSection;\n\t};\n\n\tstruct ResultCustomization {\n\t\tFn<void(not_null<Ui::SettingsButton*>)> hook;\n\t\tconst style::SettingsButton *st = nullptr;\n\t};\n\n\tvoid setupContent();\n\tvoid setupCustomizations();\n\tvoid buildIndex();\n\tvoid rebuildResults(const QString &query);\n\tvoid rebuildRecentResults();\n\tvoid rebuildFaqResults();\n\tvoid bumpRecentEntry(const QString &entryId);\n\t[[nodiscard]] not_null<Ui::SettingsButton*> createEntryButton(\n\t\tint entryIndex,\n\t\tconst QString &subtitle);\n\tvoid selectByKeyboard(int newSelected);\n\tvoid clearSelection();\n\tvoid handleKeyNavigation(int key);\n\tvoid scrollToButton(not_null<Ui::SettingsButton*> button);\n\tvoid setupButtonMouseTracking(not_null<Ui::SettingsButton*> button);\n\tvoid addButton(not_null<Ui::SettingsButton*> button);\n\n\tstd::unique_ptr<Ui::SearchFieldController> _searchController;\n\tUi::InputField *_searchField = nullptr;\n\tUi::VerticalLayout *_list = nullptr;\n\tbase::flat_map<QString, ResultCustomization> _customizations;\n\tbase::flat_map<QString, int> _entryIdToIndex;\n\tQString _pendingQuery;\n\tstd::vector<IndexedEntry> _entries;\n\tbase::flat_map<QChar, base::flat_set<int>> _firstLetterIndex;\n\tbase::flat_map<int, Ui::SettingsButton*> _buttonCache;\n\tint _faqStartIndex = 0;\n\tstd::vector<Ui::SettingsButton*> _visibleButtons;\n\tbase::flat_set<not_null<Ui::SettingsButton*>> _trackedButtons;\n\tbase::unique_qptr<Ui::PopupMenu> _contextMenu;\n\tint _selected = -1;\n\n};\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings/settings_type.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Settings {\n\nstruct AbstractSectionFactory;\nusing Type = std::shared_ptr<AbstractSectionFactory>;\n\n} // namespace Settings\n"
  },
  {
    "path": "Telegram/SourceFiles/settings.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"settings.h\"\n\n#include \"ui/emoji_config.h\"\n\nQt::LayoutDirection gLangDir = Qt::LeftToRight;\n\nbool gInstallBetaVersion = AppBetaVersion;\nuint64 gAlphaVersion = AppAlphaVersion;\nuint64 gRealAlphaVersion = AppAlphaVersion;\nQByteArray gAlphaPrivateKey;\n\nbool gManyInstance = false;\nQString gKeyFile;\nQString gWorkingDir;\n\nQList<QUrl> gStartUrls;\n\nQString gDialogLastPath, gDialogHelperPath; // optimize QFileDialog\n\nbool gStartMinimized = false;\nbool gStartInTray = false;\nbool gAutoStart = false;\nbool gSendToMenu = false;\nbool gAutoUpdate = true;\nLaunchMode gLaunchMode = LaunchModeNormal;\nbool gSeenTrayTooltip = false;\nbool gRestartingUpdate = false, gRestarting = false, gRestartingToSettings = false, gWriteProtected = false;\nbool gQuit = false;\nint32 gLastUpdateCheck = 0;\nbool gNoStartUpdate = false;\nbool gStartToSettings = false;\nbool gDebugMode = false;\n\nuint32 gConnectionsInSession = 1;\n\nQByteArray gLocalSalt;\nint gScreenScale = style::kScaleAuto;\nint gConfigScale = style::kScaleAuto;\n\nRecentStickerPreload gRecentStickersPreload;\nRecentStickerPack gRecentStickers;\n\nRecentHashtagPack gRecentWriteHashtags, gRecentSearchHashtags;\n\nRecentInlineBots gRecentInlineBots;\n\nbool gPasswordRecovered = false;\nint32 gPasscodeBadTries = 0;\ncrl::time gPasscodeLastTry = 0;\n\nfloat64 gRetinaFactor = 1.;\nint32 gIntRetinaFactor = 1;\n\nint gOtherOnline = 0;\n\nint32 gAutoDownloadPhoto = 0; // all auto download\nint32 gAutoDownloadAudio = 0;\nint32 gAutoDownloadGif = 0;\n"
  },
  {
    "path": "Telegram/SourceFiles/settings.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/integration.h\"\n#include \"ui/style/style_core.h\"\n\n#define DeclareReadSetting(Type, Name) extern Type g##Name; \\\ninline const Type &c##Name() { \\\n\treturn g##Name; \\\n}\n\n#define DeclareSetting(Type, Name) DeclareReadSetting(Type, Name) \\\ninline void cSet##Name(const Type &Name) { \\\n\tg##Name = Name; \\\n}\n\n#define DeclareRefSetting(Type, Name) DeclareSetting(Type, Name) \\\ninline Type &cRef##Name() { \\\n\treturn g##Name; \\\n}\n\nDeclareSetting(Qt::LayoutDirection, LangDir);\ninline bool rtl() {\n\treturn style::RightToLeft();\n}\n\nDeclareSetting(bool, InstallBetaVersion);\nDeclareSetting(uint64, AlphaVersion);\nDeclareSetting(uint64, RealAlphaVersion);\nDeclareSetting(QByteArray, AlphaPrivateKey);\n\nDeclareSetting(bool, AutoStart);\nDeclareSetting(bool, StartMinimized);\nDeclareSetting(bool, StartInTray);\nDeclareSetting(bool, SendToMenu);\nenum LaunchMode {\n\tLaunchModeNormal = 0,\n\tLaunchModeAutoStart,\n\tLaunchModeFixPrevious,\n\tLaunchModeCleanup,\n};\nDeclareReadSetting(LaunchMode, LaunchMode);\nDeclareSetting(QString, WorkingDir);\ninline void cForceWorkingDir(const QString &newDir) {\n\tcSetWorkingDir(newDir);\n\tif (!gWorkingDir.isEmpty()) {\n\t\tcSetWorkingDir(QDir(gWorkingDir).absolutePath() + '/');\n\t\tQDir().mkpath(gWorkingDir);\n\t\tQFile::setPermissions(gWorkingDir,\n\t\t\tQFileDevice::ReadUser | QFileDevice::WriteUser | QFileDevice::ExeUser);\n\t}\n\n}\ninline QString cExeName() {\n\treturn base::Integration::Instance().executableName();\n}\ninline QString cExeDir() {\n\treturn base::Integration::Instance().executableDir();\n}\nDeclareSetting(QString, DialogLastPath);\nDeclareSetting(QString, DialogHelperPath);\ninline QString cDialogHelperPathFinal() {\n\treturn cDialogHelperPath().isEmpty() ? cExeDir() : cDialogHelperPath();\n}\n\nDeclareSetting(bool, AutoUpdate);\n\nDeclareSetting(bool, SeenTrayTooltip);\nDeclareSetting(bool, RestartingUpdate);\nDeclareSetting(bool, Restarting);\nDeclareSetting(bool, RestartingToSettings);\nDeclareSetting(bool, WriteProtected);\nDeclareSetting(int32, LastUpdateCheck);\nDeclareSetting(bool, NoStartUpdate);\nDeclareSetting(bool, StartToSettings);\nDeclareSetting(bool, DebugMode);\nDeclareReadSetting(bool, ManyInstance);\nDeclareSetting(bool, Quit);\n\nDeclareSetting(QByteArray, LocalSalt);\nDeclareSetting(int, ScreenScale);\nDeclareSetting(int, ConfigScale);\n\nclass DocumentData;\n\ntypedef QList<QPair<DocumentData*, int16>> RecentStickerPackOld;\ntypedef QVector<QPair<uint64, ushort>> RecentStickerPreload;\ntypedef QVector<QPair<DocumentData*, ushort>> RecentStickerPack;\nDeclareSetting(RecentStickerPreload, RecentStickersPreload);\nDeclareRefSetting(RecentStickerPack, RecentStickers);\n\ntypedef QList<QPair<QString, ushort>> RecentHashtagPack;\nDeclareRefSetting(RecentHashtagPack, RecentWriteHashtags);\nDeclareSetting(RecentHashtagPack, RecentSearchHashtags);\n\nclass UserData;\ntypedef QVector<UserData*> RecentInlineBots;\nDeclareRefSetting(RecentInlineBots, RecentInlineBots);\n\nDeclareSetting(bool, PasswordRecovered);\n\nDeclareSetting(int32, PasscodeBadTries);\nDeclareSetting(crl::time, PasscodeLastTry);\n\nDeclareRefSetting(QList<QUrl>, StartUrls);\n\nDeclareSetting(int, OtherOnline);\n\ninline bool passcodeCanTry() {\n\tif (cPasscodeBadTries() < 3) return true;\n\tauto dt = crl::now() - cPasscodeLastTry();\n\tswitch (cPasscodeBadTries()) {\n\tcase 3: return dt >= 5000;\n\tcase 4: return dt >= 10000;\n\tcase 5: return dt >= 15000;\n\tcase 6: return dt >= 20000;\n\tcase 7: return dt >= 25000;\n\t}\n\treturn dt >= 30000;\n}\n\ninline int cEvalScale(int scale) {\n\treturn (scale == style::kScaleAuto) ? cScreenScale() : scale;\n}\n\ninline int cScale() {\n\treturn style::Scale();\n}\n\ninline void SetScaleChecked(int scale) {\n\tcSetConfigScale(style::CheckScale(scale));\n}\n\ninline void ValidateScale() {\n\tSetScaleChecked(cConfigScale());\n\tstyle::SetScale(cEvalScale(cConfigScale()));\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/chart_lines_filter_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/chart_lines_filter_controller.h\"\n\nnamespace Statistic {\n\nLinesFilterController::LinesFilterController() = default;\n\nvoid LinesFilterController::setEnabled(int id, bool enabled, crl::time now) {\n\tconst auto it = _entries.find(id);\n\tif (it == end(_entries)) {\n\t\t_entries[id] = Entry{\n\t\t\t.enabled = enabled,\n\t\t\t.startedAt = now,\n\t\t\t.anim = anim::value(enabled ? 0. : 1., enabled ? 1. : 0.),\n\t\t};\n\t} else if (it->second.enabled != enabled) {\n\t\tauto &entry = it->second;\n\t\tentry.enabled = enabled;\n\t\tentry.startedAt = now;\n\t\tentry.dtCurrent = 0.;\n\t\tentry.anim.start(enabled ? 1. : 0.);\n\t}\n\t_isFinished = false;\n}\n\nbool LinesFilterController::isFinished() const {\n\treturn _isFinished;\n}\n\nbool LinesFilterController::isEnabled(int id) const {\n\tconst auto it = _entries.find(id);\n\treturn (it == end(_entries)) ? true : it->second.enabled;\n}\n\nfloat64 LinesFilterController::alpha(int id) const {\n\tconst auto it = _entries.find(id);\n\treturn (it == end(_entries)) ? 1. : it->second.alpha;\n}\n\nvoid LinesFilterController::tick(float64 dtSpeed) {\n\tauto finishedCount = 0;\n\tauto idsToRemove = std::vector<int>();\n\tfor (auto &[id, entry] : _entries) {\n\t\tif (!entry.startedAt) {\n\t\t\tcontinue;\n\t\t}\n\t\tentry.dtCurrent = std::min(entry.dtCurrent + dtSpeed, 1.);\n\t\tentry.anim.update(entry.dtCurrent, anim::easeInCubic);\n\t\tconst auto progress = entry.anim.current();\n\t\tentry.alpha = std::clamp(progress, 0., 1.);\n\t\tif ((entry.alpha == 1.) && entry.enabled) {\n\t\t\tidsToRemove.push_back(id);\n\t\t}\n\t\tif (entry.anim.current() == entry.anim.to()) {\n\t\t\tfinishedCount++;\n\t\t\tentry.anim.finish();\n\t\t}\n\t}\n\t_isFinished = (finishedCount == _entries.size());\n\tfor (const auto &id : idsToRemove) {\n\t\t_entries.remove(id);\n\t}\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/chart_lines_filter_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animation_value.h\"\n\nnamespace Statistic {\n\nclass LinesFilterController final {\npublic:\n\tLinesFilterController();\n\n\tvoid setEnabled(int id, bool enabled, crl::time now);\n\t[[nodiscard]] bool isEnabled(int id) const;\n\t[[nodiscard]] bool isFinished() const;\n\t[[nodiscard]] float64 alpha(int id) const;\n\n\tvoid tick(float64 dtSpeed);\n\nprivate:\n\tstruct Entry final {\n\t\tbool enabled = false;\n\t\tcrl::time startedAt = 0;\n\t\tfloat64 alpha = 1.;\n\t\tanim::value anim;\n\t\tfloat64 dtCurrent = 0.;\n\t};\n\n\tbase::flat_map<int, Entry> _entries;\n\tbool _isFinished = true;\n\n};\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/chart_rulers_data.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/chart_rulers_data.h\"\n\n#include \"lang/lang_tag.h\"\n\n#include <QLocale>\n\nnamespace Statistic {\nnamespace {\n\nconstexpr auto kMinLines = ChartValue(2);\nconstexpr auto kMaxLines = ChartValue(6);\nconstexpr auto kStep = 5.;\n\n[[nodiscard]] ChartValue Round(ChartValue maxValue) {\n\tconst auto k = ChartValue(maxValue / kStep);\n\treturn (k % 10 == 0) ? maxValue : ((maxValue / 10 + 1) * 10);\n}\n\n[[nodiscard]] QString Format(ChartValue absoluteValue) {\n\tstatic constexpr auto kTooMuch = ChartValue(10'000);\n\treturn (absoluteValue >= kTooMuch)\n\t\t? Lang::FormatCountToShort(absoluteValue).string\n\t\t: QLocale().toString(absoluteValue);\n}\n\n} // namespace\n\nChartRulersData::ChartRulersData(\n\t\tChartValue newMaxHeight,\n\t\tChartValue newMinHeight,\n\t\tbool useMinHeight,\n\t\tfloat64 rightRatio,\n\t\tFn<QString(float64)> leftCustomCaption,\n\t\tFn<QString(float64)> rightCustomCaption) {\n\tif (!useMinHeight) {\n\t\tconst auto v = (newMaxHeight > 100)\n\t\t\t? Round(newMaxHeight)\n\t\t\t: newMaxHeight;\n\n\t\tconst auto step = std::max(\n\t\t\tChartValue(1),\n\t\t\tChartValue(std::ceil(v / kStep)));\n\n\t\tauto n = kMaxLines;\n\t\tif (v < kMaxLines) {\n\t\t\tn = std::max(2, int(v + 1));\n\t\t} else if (v / 2 < kMaxLines) {\n\t\t\tn = v / 2 + 1;\n\t\t\tif (v % 2 != 0) {\n\t\t\t\tn++;\n\t\t\t}\n\t\t}\n\n\t\tlines.resize(n);\n\n\t\tfor (auto i = 1; i < n; i++) {\n\t\t\tauto &line = lines[i];\n\t\t\tline.absoluteValue = i * step;\n\t\t\tline.caption = Lang::FormatCountToShort(\n\t\t\t\tline.absoluteValue).string;\n\t\t}\n\t} else {\n\t\tauto n = int(0);\n\t\tconst auto diff = newMaxHeight - newMinHeight;\n\t\tauto step = 0.;\n\t\tif (diff == 0) {\n\t\t\tnewMinHeight--;\n\t\t\tn = kMaxLines / 2;\n\t\t\tstep = 1.;\n\t\t} else if (diff < kMaxLines) {\n\t\t\tn = std::max(kMinLines, diff + 1);\n\t\t\tstep = 1.;\n\t\t} else if (diff / 2 < kMaxLines) {\n\t\t\tn = diff / 2 + diff % 2 + 1;\n\t\t\tstep = 2.;\n\t\t} else {\n\t\t\tstep = (newMaxHeight - newMinHeight) / kStep;\n\t\t\tif (step <= 0) {\n\t\t\t\tstep = 1;\n\t\t\t\tn = std::max(kMinLines, newMaxHeight - newMinHeight + 1);\n\t\t\t} else {\n\t\t\t\tn = 6;\n\t\t\t}\n\t\t}\n\n\t\tlines.resize(n);\n\t\tconst auto diffAbsoluteValue = ChartValue((n - 1) * step);\n\t\tconst auto skipFloatValues = (step / rightRatio) < 1;\n\t\tfor (auto i = 0; i < n; i++) {\n\t\t\tauto &line = lines[i];\n\t\t\tconst auto value = ChartValue(i * step);\n\t\t\tline.absoluteValue = newMinHeight + value;\n\t\t\tline.relativeValue = 1. - value / float64(diffAbsoluteValue);\n\t\t\tline.caption = leftCustomCaption\n\t\t\t\t? leftCustomCaption(line.absoluteValue)\n\t\t\t\t: Format(line.absoluteValue);\n\t\t\tif (rightRatio > 0 || rightCustomCaption) {\n\t\t\t\tconst auto v = (newMinHeight + i * step) / rightRatio;\n\t\t\t\tline.scaledLineCaption = rightCustomCaption\n\t\t\t\t\t? rightCustomCaption(line.absoluteValue)\n\t\t\t\t\t: (!skipFloatValues)\n\t\t\t\t\t? Format(v)\n\t\t\t\t\t: ((v - ChartValue(v)) < 0.01)\n\t\t\t\t\t? Format(v)\n\t\t\t\t\t: QString();\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ChartRulersData::computeRelative(\n\t\tChartValue newMaxHeight,\n\t\tChartValue newMinHeight) {\n\tfor (auto &line : lines) {\n\t\tline.relativeValue = 1.\n\t\t\t- ((line.absoluteValue - newMinHeight)\n\t\t\t\t/ (newMaxHeight - newMinHeight));\n\t}\n}\n\nChartValue ChartRulersData::LookupHeight(ChartValue maxValue) {\n\tconst auto v = (maxValue > 100) ? Round(maxValue) : maxValue;\n\n\tconst auto step = ChartValue(std::ceil(v / kStep));\n\treturn step * kStep;\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/chart_rulers_data.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"statistics/statistics_types.h\"\n\nnamespace Statistic {\n\nstruct ChartRulersData final {\npublic:\n\tChartRulersData(\n\t\tChartValue newMaxHeight,\n\t\tChartValue newMinHeight,\n\t\tbool useMinHeight,\n\t\tfloat64 rightRatio,\n\t\tFn<QString(float64)> leftCustomCaption = nullptr,\n\t\tFn<QString(float64)> rightCustomCaption = nullptr);\n\n\tvoid computeRelative(\n\t\tChartValue newMaxHeight,\n\t\tChartValue newMinHeight);\n\n\t[[nodiscard]] static ChartValue LookupHeight(ChartValue maxValue);\n\n\tstruct Line final {\n\t\tfloat64 absoluteValue = 0.;\n\t\tfloat64 relativeValue = 0.;\n\t\tQString caption;\n\t\tQString scaledLineCaption;\n\t\tfloat64 rightCaptionWidth = 0.;\n\t};\n\n\tstd::vector<Line> lines;\n\tfloat64 alpha = 0.;\n\tfloat64 fixedAlpha = 1.;\n\n};\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/chart_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/chart_widget.h\"\n\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"lang/lang_keys.h\"\n#include \"statistics/chart_lines_filter_controller.h\"\n#include \"statistics/statistics_format_values.h\"\n#include \"statistics/view/abstract_chart_view.h\"\n#include \"statistics/view/chart_view_factory.h\"\n#include \"statistics/view/stack_chart_common.h\"\n#include \"statistics/widgets/chart_header_widget.h\"\n#include \"statistics/widgets/chart_lines_filter_widget.h\"\n#include \"statistics/widgets/point_details_widget.h\"\n#include \"ui/abstract_button.h\"\n#include \"ui/effects/animation_value_f.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/show_animation.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_statistics.h\"\n\nnamespace Statistic {\n\nnamespace {\n\nconstexpr auto kHeightLimitsUpdateTimeout = crl::time(320);\n\ninline float64 InterpolationRatio(float64 from, float64 to, float64 result) {\n\treturn (result - from) / (to - from);\n};\n\nvoid FillLineColorsByKey(Data::StatisticalChart &chartData) {\n\tfor (auto &line : chartData.lines) {\n\t\tif (line.colorKey == u\"BLUE\"_q) {\n\t\t\tline.color = st::statisticsChartLineBlue->c;\n\t\t} else if (line.colorKey == u\"GREEN\"_q) {\n\t\t\tline.color = st::statisticsChartLineGreen->c;\n\t\t} else if (line.colorKey == u\"RED\"_q) {\n\t\t\tline.color = st::statisticsChartLineRed->c;\n\t\t} else if (line.colorKey == u\"GOLDEN\"_q) {\n\t\t\tline.color = st::statisticsChartLineGolden->c;\n\t\t} else if (line.colorKey == u\"LIGHTBLUE\"_q) {\n\t\t\tline.color = st::statisticsChartLineLightblue->c;\n\t\t} else if (line.colorKey == u\"LIGHTGREEN\"_q) {\n\t\t\tline.color = st::statisticsChartLineLightgreen->c;\n\t\t} else if (line.colorKey == u\"ORANGE\"_q) {\n\t\t\tline.color = st::statisticsChartLineOrange->c;\n\t\t} else if (line.colorKey == u\"INDIGO\"_q) {\n\t\t\tline.color = st::statisticsChartLineIndigo->c;\n\t\t} else if (line.colorKey == u\"PURPLE\"_q) {\n\t\t\tline.color = st::statisticsChartLinePurple->c;\n\t\t} else if (line.colorKey == u\"CYAN\"_q) {\n\t\t\tline.color = st::statisticsChartLineCyan->c;\n\t\t}\n\t}\n}\n\n[[nodiscard]] QString HeaderSubTitle(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tint xIndexMin,\n\t\tint xIndexMax) {\n\tconstexpr auto kOneDay = 3600 * 24 * 1000;\n\tconst auto leftTimestamp = chartData.x[xIndexMin];\n\tif (leftTimestamp < kOneDay) {\n\t\treturn {};\n\t}\n\tconst auto leftText = LangDayMonthYear(leftTimestamp / 1000);\n\tif ((xIndexMin == xIndexMax) && !chartData.weekFormat) {\n\t\treturn leftText;\n\t} else {\n\t\tconstexpr auto kSevenDays = 3600 * 24 * 7;\n\t\tconst auto rightTimestamp = 0\n\t\t\t+ (chartData.x[xIndexMax] / 1000)\n\t\t\t+ (chartData.weekFormat ? kSevenDays : 0);\n\t\treturn leftText\n\t\t\t+ ' '\n\t\t\t+ QChar(8212)\n\t\t\t+ ' '\n\t\t\t+ LangDayMonthYear(rightTimestamp);\n\t}\n}\n\nvoid PaintBottomLine(\n\t\tQPainter &p,\n\t\tconst std::vector<ChartWidget::BottomCaptionLineData> &dates,\n\t\tData::StatisticalChart &chartData,\n\t\tconst Limits &xPercentageLimits,\n\t\tint fullWidth,\n\t\tint chartWidth,\n\t\tint y,\n\t\tint captionIndicesOffset) {\n\tp.setFont(st::statisticsDetailsBottomCaptionStyle.font);\n\tconst auto opacity = p.opacity();\n\n\tconst auto startXIndex = chartData.findStartIndex(\n\t\txPercentageLimits.min);\n\tconst auto endXIndex = chartData.findEndIndex(\n\t\tstartXIndex,\n\t\txPercentageLimits.max);\n\n\tconst auto captionMaxWidth = chartData.dayStringMaxWidth;\n\tconst auto edgeAlphaSize = captionMaxWidth / 4.;\n\n\tfor (auto k = 0; k < dates.size(); k++) {\n\t\tconst auto &date = dates[k];\n\t\tconst auto isLast = (k == dates.size() - 1);\n\t\tconst auto resultAlpha = date.alpha;\n\t\tconst auto step = std::max(date.step, 1);\n\n\t\tauto start = startXIndex - captionIndicesOffset;\n\t\twhile (start % step != 0) {\n\t\t\tstart--;\n\t\t}\n\n\t\tauto end = endXIndex - captionIndicesOffset;\n\t\twhile ((end % step != 0) || end < (chartData.x.size() - 1)) {\n\t\t\tend++;\n\t\t}\n\n\t\tstart += captionIndicesOffset;\n\t\tend += captionIndicesOffset;\n\n\t\tconst auto offset = fullWidth * xPercentageLimits.min;\n\n\t\t// 30 ms / 200 ms = 0.15.\n\t\tconstexpr auto kFastAlphaSpeed = 0.85;\n\t\tconst auto hasFastAlpha = (date.stepRaw < dates.back().stepMinFast);\n\t\tconst auto fastAlpha = isLast\n\t\t\t? 1.\n\t\t\t: std::max(resultAlpha - kFastAlphaSpeed, 0.);\n\n\t\tfor (auto i = start; i < end; i += step) {\n\t\t\tif ((i < 0) || (i >= (chartData.x.size() - 1))) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto xPercentage = (chartData.x[i] - chartData.x.front())\n\t\t\t\t/ float64(chartData.x.back() - chartData.x.front());\n\t\t\tconst auto xPoint = xPercentage * fullWidth - offset;\n\t\t\tconst auto r = QRectF(\n\t\t\t\txPoint - captionMaxWidth / 2.,\n\t\t\t\ty,\n\t\t\t\tcaptionMaxWidth,\n\t\t\t\tst::statisticsChartBottomCaptionHeight);\n\t\t\tconst auto edgeAlpha = (r.x() < 0)\n\t\t\t\t? std::max(\n\t\t\t\t\t0.,\n\t\t\t\t\t1. + (r.x() / edgeAlphaSize))\n\t\t\t\t: (rect::right(r) > chartWidth)\n\t\t\t\t? std::max(\n\t\t\t\t\t0.,\n\t\t\t\t\t1. + ((chartWidth - rect::right(r)) / edgeAlphaSize))\n\t\t\t\t: 1.;\n\t\t\tp.setOpacity(opacity\n\t\t\t\t* edgeAlpha\n\t\t\t\t* (hasFastAlpha ? fastAlpha : resultAlpha));\n\t\t\tp.drawText(r, chartData.getDayString(i), style::al_center);\n\t\t}\n\t}\n}\n\n} // namespace\n\nclass RpMouseWidget : public Ui::AbstractButton {\npublic:\n\tusing Ui::AbstractButton::AbstractButton;\n\n\tstruct State {\n\t\tQPoint point;\n\t\tQEvent::Type mouseState;\n\t};\n\n\t[[nodiscard]] const QPoint &start() const;\n\t[[nodiscard]] rpl::producer<State> mouseStateChanged() const;\n\nprotected:\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\nprivate:\n\tQPoint _start = QPoint(-1, -1);\n\n\trpl::event_stream<State> _mouseStateChanged;\n\n};\n\nconst QPoint &RpMouseWidget::start() const {\n\treturn _start;\n}\n\nrpl::producer<RpMouseWidget::State> RpMouseWidget::mouseStateChanged() const {\n\treturn _mouseStateChanged.events();\n}\n\nvoid RpMouseWidget::mousePressEvent(QMouseEvent *e) {\n\t_start = e->pos();\n\t_mouseStateChanged.fire({ e->pos(), QEvent::MouseButtonPress });\n}\n\nvoid RpMouseWidget::mouseMoveEvent(QMouseEvent *e) {\n\tif (_start.x() >= 0 || _start.y() >= 0) {\n\t\t_mouseStateChanged.fire({ e->pos(), QEvent::MouseMove });\n\t}\n}\n\nvoid RpMouseWidget::mouseReleaseEvent(QMouseEvent *e) {\n\t_start = { -1, -1 };\n\t_mouseStateChanged.fire({ e->pos(), QEvent::MouseButtonRelease });\n}\n\nclass ChartWidget::Footer final : public RpMouseWidget {\npublic:\n\tusing PaintCallback = Fn<void(QPainter &, const QRect &)>;\n\n\texplicit Footer(not_null<Ui::RpWidget*> parent);\n\n\tvoid setXPercentageLimits(const Limits &xLimits);\n\n\t[[nodiscard]] Limits xPercentageLimits() const;\n\t[[nodiscard]] rpl::producer<Limits> xPercentageLimitsChange() const;\n\n\tvoid setPaintChartCallback(PaintCallback paintChartCallback);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tvoid moveSide(bool left, float64 x);\n\tvoid moveCenter(\n\t\tbool isDirectionToLeft,\n\t\tfloat64 x,\n\t\tfloat64 diffBetweenStartAndLeft);\n\n\tvoid fire() const;\n\n\tenum class DragArea {\n\t\tNone,\n\t\tMiddle,\n\t\tLeft,\n\t\tRight,\n\t};\n\tDragArea _dragArea = DragArea::None;\n\tfloat64 _diffBetweenStartAndSide = 0;\n\tUi::Animations::Simple _moveCenterAnimation;\n\tbool _draggedAfterPress = false;\n\n\tconst QPen _sidePen;\n\n\tfloat64 _width = 0.;\n\tfloat64 _widthBetweenSides = 0.;\n\n\tPaintCallback _paintChartCallback;\n\n\tQImage _frame;\n\tQImage _mask;\n\n\tLimits _leftSide;\n\tLimits _rightSide;\n\n\trpl::event_stream<Limits> _xPercentageLimitsChange;\n\n};\n\nChartWidget::Footer::Footer(not_null<Ui::RpWidget*> parent)\n: RpMouseWidget(parent)\n, _sidePen(\n\tst::premiumButtonFg,\n\tst::statisticsChartLineWidth,\n\tQt::SolidLine,\n\tQt::RoundCap) {\n\tsizeValue(\n\t) | rpl::take(2) | rpl::on_next([=](const QSize &s) {\n\t\tconst auto current = xPercentageLimits();\n\t\tif (current.min == current.max) {\n\t\t\tsetXPercentageLimits({ 0., 1. });\n\t\t}\n\t}, lifetime());\n\n\tmouseStateChanged(\n\t) | rpl::on_next([=](const RpMouseWidget::State &state) {\n\t\tif (_moveCenterAnimation.animating()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto posX = state.point.x();\n\t\tconst auto isLeftSide = (posX >= _leftSide.min)\n\t\t\t&& (posX <= _leftSide.max);\n\t\tconst auto isRightSide = !isLeftSide\n\t\t\t&& (posX >= _rightSide.min)\n\t\t\t&& (posX <= _rightSide.max);\n\t\tswitch (state.mouseState) {\n\t\tcase QEvent::MouseMove: {\n\t\t\t_draggedAfterPress = true;\n\t\t\tif (_dragArea == DragArea::None) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto resultX = posX - _diffBetweenStartAndSide;\n\t\t\tif (_dragArea == DragArea::Right) {\n\t\t\t\tmoveSide(false, resultX);\n\t\t\t} else if (_dragArea == DragArea::Left) {\n\t\t\t\tmoveSide(true, resultX);\n\t\t\t} else if (_dragArea == DragArea::Middle) {\n\t\t\t\tconst auto toLeft = (posX\n\t\t\t\t\t- _diffBetweenStartAndSide\n\t\t\t\t\t- _leftSide.min) <= 0;\n\t\t\t\tmoveCenter(toLeft, posX, _diffBetweenStartAndSide);\n\t\t\t}\n\t\t\tfire();\n\t\t} break;\n\t\tcase QEvent::MouseButtonPress: {\n\t\t\t_draggedAfterPress = false;\n\t\t\t_dragArea = isLeftSide\n\t\t\t\t? DragArea::Left\n\t\t\t\t: isRightSide\n\t\t\t\t? DragArea::Right\n\t\t\t\t: ((posX < _leftSide.min) || (posX > _rightSide.max))\n\t\t\t\t? DragArea::None\n\t\t\t\t: DragArea::Middle;\n\t\t\t_diffBetweenStartAndSide = isRightSide\n\t\t\t\t? (start().x() - _rightSide.min)\n\t\t\t\t: (start().x() - _leftSide.min);\n\t\t} break;\n\t\tcase QEvent::MouseButtonRelease: {\n\t\t\tconst auto finish = [=] {\n\t\t\t\t_dragArea = DragArea::None;\n\t\t\t\tfire();\n\t\t\t};\n\t\t\tif ((_dragArea == DragArea::None) && !_draggedAfterPress) {\n\t\t\t\tconst auto startX = _leftSide.min\n\t\t\t\t\t+ (_rightSide.max - _leftSide.min) / 2;\n\t\t\t\tconst auto finishX = posX;\n\t\t\t\tconst auto toLeft = (finishX <= startX);\n\t\t\t\tconst auto diffBetweenStartAndLeft = startX - _leftSide.min;\n\t\t\t\t_moveCenterAnimation.stop();\n\t\t\t\t_moveCenterAnimation.start([=](float64 value) {\n\t\t\t\t\tmoveCenter(toLeft, value, diffBetweenStartAndLeft);\n\t\t\t\t\tfire();\n\t\t\t\t\tupdate();\n\t\t\t\t\tif (value == finishX) {\n\t\t\t\t\t\tfinish();\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tstartX,\n\t\t\t\tfinishX,\n\t\t\t\tst::slideWrapDuration,\n\t\t\t\tanim::sineInOut);\n\t\t\t} else {\n\t\t\t\tfinish();\n\t\t\t}\n\t\t} break;\n\t\t}\n\t\tupdate();\n\t}, lifetime());\n}\n\nint ChartWidget::Footer::resizeGetHeight(int newWidth) {\n\tconst auto h = st::statisticsChartFooterHeight;\n\tif (!newWidth) {\n\t\treturn h;\n\t}\n\tconst auto was = xPercentageLimits();\n\tconst auto w = float64(st::statisticsChartFooterSideWidth);\n\t_width = newWidth - w;\n\t_widthBetweenSides = newWidth - w * 2.;\n\t_mask = Ui::RippleAnimation::RoundRectMask(\n\t\tQSize(newWidth, h - st::lineWidth * 2),\n\t\tst::boxRadius);\n\t_frame = _mask;\n\tif (_widthBetweenSides && was.max) {\n\t\tsetXPercentageLimits(was);\n\t}\n\treturn h;\n}\n\nLimits ChartWidget::Footer::xPercentageLimits() const {\n\treturn {\n\t\t.min = _widthBetweenSides ? _leftSide.min / _widthBetweenSides : 0.,\n\t\t.max = _widthBetweenSides\n\t\t\t? (_rightSide.min - st::statisticsChartFooterSideWidth)\n\t\t\t\t/ _widthBetweenSides\n\t\t\t: 0.,\n\t};\n}\n\nvoid ChartWidget::Footer::fire() const {\n\t_xPercentageLimitsChange.fire(xPercentageLimits());\n}\n\nvoid ChartWidget::Footer::moveCenter(\n\t\tbool isDirectionToLeft,\n\t\tfloat64 x,\n\t\tfloat64 diffBetweenStartAndLeft) {\n\tconst auto resultX = x - diffBetweenStartAndLeft;\n\tconst auto diffBetweenSides = std::max(\n\t\t_rightSide.min - _leftSide.min,\n\t\tfloat64(st::statisticsChartFooterBetweenSide));\n\tif (isDirectionToLeft) {\n\t\tmoveSide(true, resultX);\n\t\tmoveSide(false, _leftSide.min + diffBetweenSides);\n\t} else {\n\t\tmoveSide(false, resultX + diffBetweenSides);\n\t\tmoveSide(true, _rightSide.min - diffBetweenSides);\n\t}\n}\n\nvoid ChartWidget::Footer::moveSide(bool left, float64 x) {\n\tconst auto w = float64(st::statisticsChartFooterSideWidth);\n\tconst auto mid = float64(st::statisticsChartFooterBetweenSide);\n\tif (_width < (2 * w + mid)) {\n\t\treturn;\n\t} else if (left) {\n\t\tconst auto rightLimit = _rightSide.min - w - mid;\n\t\tconst auto min = std::clamp(\n\t\t\tx,\n\t\t\t0.,\n\t\t\t(rightLimit <= 0) ? _widthBetweenSides : rightLimit);\n\t\t_leftSide = Limits{ .min = min, .max = min + w };\n\t} else if (!left) {\n\t\tconst auto min = std::clamp(x, _leftSide.max + mid, _width);\n\t\t_rightSide = Limits{ .min = min, .max = min + w };\n\t}\n}\n\nvoid ChartWidget::Footer::setPaintChartCallback(\n\t\tPaintCallback paintChartCallback) {\n\t_paintChartCallback = std::move(paintChartCallback);\n}\n\nvoid ChartWidget::Footer::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tconst auto lineWidth = st::lineWidth;\n\tconst auto innerMargins = QMargins{ 0, lineWidth, 0, lineWidth };\n\tconst auto r = rect();\n\tconst auto innerRect = r - innerMargins;\n\tconst auto &inactiveColor = st::statisticsChartInactive;\n\n\t_frame.fill(Qt::transparent);\n\tif (_paintChartCallback) {\n\t\tauto q = QPainter(&_frame);\n\n\t\t{\n\t\t\tconst auto opacity = q.opacity();\n\t\t\t_paintChartCallback(q, Rect(innerRect.size()));\n\t\t\tq.setOpacity(opacity);\n\t\t}\n\n\t\tq.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\tq.drawImage(0, 0, _mask);\n\t}\n\n\tp.drawImage(0, lineWidth, _frame);\n\n\tauto inactivePath = QPainterPath();\n\tinactivePath.addRoundedRect(\n\t\tinnerRect,\n\t\tst::statisticsChartFooterSideRadius,\n\t\tst::statisticsChartFooterSideRadius);\n\n\tauto sidesPath = QPainterPath();\n\tsidesPath.addRoundedRect(\n\t\t_leftSide.min,\n\t\t0,\n\t\t_rightSide.max - _leftSide.min,\n\t\tr.height(),\n\t\tst::statisticsChartFooterSideRadius,\n\t\tst::statisticsChartFooterSideRadius);\n\tinactivePath = inactivePath.subtracted(sidesPath);\n\tsidesPath.addRect(\n\t\t_leftSide.max,\n\t\tlineWidth,\n\t\t_rightSide.min - _leftSide.max,\n\t\tr.height() - lineWidth * 2);\n\n\tp.setBrush(st::statisticsChartActive);\n\tp.setPen(Qt::NoPen);\n\tp.drawPath(sidesPath);\n\tp.setBrush(inactiveColor);\n\tp.drawPath(inactivePath);\n\n\t{\n\t\tp.setPen(_sidePen);\n\t\tconst auto halfWidth = st::statisticsChartLineWidth / 2.;\n\t\tconst auto left = _leftSide.min\n\t\t\t+ (_leftSide.max - _leftSide.min) / 2.\n\t\t\t+ halfWidth;\n\t\tconst auto right = _rightSide.min\n\t\t\t+ (_rightSide.max - _rightSide.min) / 2.;\n\t\tconst auto halfHeight = st::statisticsChartFooterArrowHeight / 2.\n\t\t\t- halfWidth;\n\t\tconst auto center = r.height() / 2.;\n\t\tconst auto top = center - halfHeight;\n\t\tconst auto bottom = center + halfHeight;\n\t\tp.drawLine(left, top, left, bottom);\n\t\tp.drawLine(right, top, right, bottom);\n\t}\n}\n\nvoid ChartWidget::Footer::setXPercentageLimits(const Limits &xLimits) {\n\tconst auto left = xLimits.min * _widthBetweenSides;\n\tconst auto right = xLimits.max * _widthBetweenSides\n\t\t+ st::statisticsChartFooterSideWidth;\n\tmoveSide(true, left);\n\tmoveSide(false, right);\n\tfire();\n\tupdate();\n}\n\nrpl::producer<Limits> ChartWidget::Footer::xPercentageLimitsChange() const {\n\treturn _xPercentageLimitsChange.events();\n}\n\nChartWidget::ChartAnimationController::ChartAnimationController(\n\tFn<void()> &&updateCallback)\n: _animation(std::move(updateCallback)) {\n}\n\nvoid ChartWidget::ChartAnimationController::setXPercentageLimits(\n\t\tData::StatisticalChart &chartData,\n\t\tLimits xPercentageLimits,\n\t\tconst std::unique_ptr<AbstractChartView> &chartView,\n\t\tconst std::shared_ptr<LinesFilterController> &linesFilter,\n\t\tcrl::time now) {\n\tif ((_animationValueXMin.to() == xPercentageLimits.min)\n\t\t&& (_animationValueXMax.to() == xPercentageLimits.max)\n\t\t&& linesFilter->isFinished()) {\n\t\treturn;\n\t}\n\tstart();\n\t_animationValueXMin.start(xPercentageLimits.min);\n\t_animationValueXMax.start(xPercentageLimits.max);\n\t_lastUserInteracted = now;\n\n\tconst auto startXIndex = chartData.findStartIndex(\n\t\t_animationValueXMin.to());\n\tconst auto endXIndex = chartData.findEndIndex(\n\t\tstartXIndex,\n\t\t_animationValueXMax.to());\n\t_currentXIndices = { float64(startXIndex), float64(endXIndex) };\n\n\t{\n\t\tconst auto heightLimits = chartView->heightLimits(\n\t\t\tchartData,\n\t\t\t_currentXIndices);\n\t\tif (heightLimits.ranged.min == heightLimits.ranged.max) {\n\t\t\treturn;\n\t\t}\n\t\t_previousFullHeightLimits = _finalHeightLimits;\n\t\t_finalHeightLimits = heightLimits.ranged;\n\t\tif (!_previousFullHeightLimits.max) {\n\t\t\t_previousFullHeightLimits = _finalHeightLimits;\n\t\t}\n\t\tif (!linesFilter->isFinished()) {\n\t\t\t_animationValueFooterHeightMin = anim::value(\n\t\t\t\t_animationValueFooterHeightMin.current(),\n\t\t\t\theightLimits.full.min);\n\t\t\t_animationValueFooterHeightMax = anim::value(\n\t\t\t\t_animationValueFooterHeightMax.current(),\n\t\t\t\theightLimits.full.max);\n\t\t} else if (!_animationValueFooterHeightMax.to()) {\n\t\t\t// Will be finished in setChartData.\n\t\t\t_animationValueFooterHeightMin = anim::value(\n\t\t\t\t0,\n\t\t\t\theightLimits.full.min);\n\t\t\t_animationValueFooterHeightMax = anim::value(\n\t\t\t\t0,\n\t\t\t\theightLimits.full.max);\n\t\t}\n\t}\n\n\t_animationValueHeightMin = anim::value(\n\t\t_animationValueHeightMin.current(),\n\t\t_finalHeightLimits.min);\n\t_animationValueHeightMax = anim::value(\n\t\t_animationValueHeightMax.current(),\n\t\t_finalHeightLimits.max);\n\n\t{\n\t\tconst auto previousDelta = _previousFullHeightLimits.max\n\t\t\t- _previousFullHeightLimits.min;\n\t\tauto k = previousDelta\n\t\t\t/ float64(_finalHeightLimits.max - _finalHeightLimits.min);\n\t\tif (k > 1.) {\n\t\t\tk = 1. / k;\n\t\t}\n\t\tconstexpr auto kDtHeightSpeed1 = 0.03 * 2;\n\t\tconstexpr auto kDtHeightSpeed2 = 0.03 * 2;\n\t\tconstexpr auto kDtHeightSpeed3 = 0.045 * 2;\n\t\tconstexpr auto kDtHeightSpeedFilter = kDtHeightSpeed1 / 1.2;\n\t\tconstexpr auto kDtHeightSpeedThreshold1 = 0.7;\n\t\tconstexpr auto kDtHeightSpeedThreshold2 = 0.1;\n\t\tconstexpr auto kDtHeightInstantThreshold = 0.97;\n\t\tif (k < 1.) {\n\t\t\tauto &alpha = _animationValueHeightAlpha;\n\t\t\talpha = anim::value(\n\t\t\t\t(alpha.current() == alpha.to()) ? 0. : alpha.current(),\n\t\t\t\t1.);\n\t\t\t_dtHeight.currentAlpha = 0.;\n\t\t\t_addRulerRequests.fire({});\n\t\t}\n\t\t_dtHeight.speed = (!linesFilter->isFinished())\n\t\t\t? kDtHeightSpeedFilter\n\t\t\t: (k > kDtHeightSpeedThreshold1)\n\t\t\t? kDtHeightSpeed1\n\t\t\t: (k < kDtHeightSpeedThreshold2)\n\t\t\t? kDtHeightSpeed2\n\t\t\t: kDtHeightSpeed3;\n\t\tif (k < kDtHeightInstantThreshold) {\n\t\t\t_dtHeight.current = { 0., 0. };\n\t\t}\n\t}\n}\n\nauto ChartWidget::ChartAnimationController::addRulerRequests() const\n-> rpl::producer<> {\n\treturn _addRulerRequests.events();\n}\n\nvoid ChartWidget::ChartAnimationController::start() {\n\tif (!_animation.animating()) {\n\t\t_animation.start();\n\t}\n}\n\nvoid ChartWidget::ChartAnimationController::finish() {\n\t_animation.stop();\n\t_animationValueXMin.finish();\n\t_animationValueXMax.finish();\n\t_animationValueHeightMin.finish();\n\t_animationValueHeightMax.finish();\n\t_animationValueFooterHeightMin.finish();\n\t_animationValueFooterHeightMax.finish();\n\t_animationValueHeightAlpha.finish();\n\t_benchmark = {};\n}\n\nvoid ChartWidget::ChartAnimationController::restartBottomLineAlpha() {\n\t_bottomLineAlphaAnimationStartedAt = crl::now();\n\t_animValueBottomLineAlpha = anim::value(0., 1.);\n\tstart();\n}\n\nvoid ChartWidget::ChartAnimationController::tick(\n\t\tcrl::time now,\n\t\tChartRulersView &rulersView,\n\t\tstd::vector<BottomCaptionLineData> &dateLines,\n\t\tconst std::unique_ptr<AbstractChartView> &chartView,\n\t\tconst std::shared_ptr<LinesFilterController> &linesFilter) {\n\tif (!_animation.animating()) {\n\t\treturn;\n\t}\n\tconstexpr auto kXExpandingDuration = 200.;\n\tconstexpr auto kAlphaExpandingDuration = 200.;\n\n\t{\n\t\tconstexpr auto kIdealFPS = float64(60);\n\t\tconst auto currentFPS = _benchmark.lastTickedAt\n\t\t\t? (1000. / (now - _benchmark.lastTickedAt))\n\t\t\t: kIdealFPS;\n\t\tif (!_benchmark.lastFPSSlow) {\n\t\t\tconstexpr auto kAcceptableFPS = int(30);\n\t\t\t_benchmark.lastFPSSlow = (currentFPS < kAcceptableFPS);\n\t\t}\n\t\t_benchmark.lastTickedAt = now;\n\n\n\t\tconst auto k = (kIdealFPS / currentFPS)\n\t\t\t// Speed up to reduce ugly frames count.\n\t\t\t* (_benchmark.lastFPSSlow ? 2. : 1.);\n\t\tconst auto speed = _dtHeight.speed * k;\n\t\tlinesFilter->tick(speed);\n\t\t_dtHeight.current.min = std::min(_dtHeight.current.min + speed, 1.);\n\t\t_dtHeight.current.max = std::min(_dtHeight.current.max + speed, 1.);\n\t\t_dtHeight.currentAlpha = std::min(_dtHeight.currentAlpha + speed, 1.);\n\t}\n\n\tconst auto dtX = std::min(\n\t\t(now - _animation.started()) / kXExpandingDuration,\n\t\t1.);\n\tconst auto dtBottomLineAlpha = std::min(\n\t\t(now - _bottomLineAlphaAnimationStartedAt) / kAlphaExpandingDuration,\n\t\t1.);\n\n\tconst auto isFinished = [](const anim::value &anim) {\n\t\treturn anim.current() == anim.to();\n\t};\n\n\tconst auto xFinished = isFinished(_animationValueXMin)\n\t\t&& isFinished(_animationValueXMax);\n\tconst auto yFinished = isFinished(_animationValueHeightMin)\n\t\t&& isFinished(_animationValueHeightMax);\n\tconst auto alphaFinished = isFinished(_animationValueHeightAlpha)\n\t\t&& isFinished(_animationValueHeightMax);\n\tconst auto bottomLineAlphaFinished = isFinished(\n\t\t_animValueBottomLineAlpha);\n\n\tconst auto footerMinFinished = isFinished(_animationValueFooterHeightMin);\n\tconst auto footerMaxFinished = isFinished(_animationValueFooterHeightMax);\n\n\tif (xFinished\n\t\t\t&& yFinished\n\t\t\t&& alphaFinished\n\t\t\t&& bottomLineAlphaFinished\n\t\t\t&& footerMinFinished\n\t\t\t&& footerMaxFinished\n\t\t\t&& linesFilter->isFinished()) {\n\t\tif ((_finalHeightLimits.min == _animationValueHeightMin.to())\n\t\t\t&& _finalHeightLimits.max == _animationValueHeightMax.to()) {\n\t\t\t_animation.stop();\n\t\t\t_benchmark = {};\n\t\t}\n\t}\n\tif (xFinished) {\n\t\t_animationValueXMin.finish();\n\t\t_animationValueXMax.finish();\n\t} else {\n\t\t_animationValueXMin.update(dtX, anim::linear);\n\t\t_animationValueXMax.update(dtX, anim::linear);\n\t}\n\tif (bottomLineAlphaFinished) {\n\t\t_animValueBottomLineAlpha.finish();\n\t\t_bottomLineAlphaAnimationStartedAt = 0;\n\t} else {\n\t\t_animValueBottomLineAlpha.update(\n\t\t\tdtBottomLineAlpha,\n\t\t\tanim::easeInCubic);\n\t}\n\tif (!yFinished) {\n\t\t_animationValueHeightMin.update(\n\t\t\t_dtHeight.current.min,\n\t\t\tanim::easeInCubic);\n\t\t_animationValueHeightMax.update(\n\t\t\t_dtHeight.current.max,\n\t\t\tanim::easeInCubic);\n\n\t\trulersView.computeRelative(\n\t\t\t_animationValueHeightMax.current(),\n\t\t\t_animationValueHeightMin.current());\n\t}\n\tif (!footerMinFinished) {\n\t\t_animationValueFooterHeightMin.update(\n\t\t\t_dtHeight.current.min,\n\t\t\tanim::easeInCubic);\n\t}\n\tif (!footerMaxFinished) {\n\t\t_animationValueFooterHeightMax.update(\n\t\t\t_dtHeight.current.max,\n\t\t\tanim::easeInCubic);\n\t}\n\n\tif (!alphaFinished) {\n\t\t_animationValueHeightAlpha.update(\n\t\t\t_dtHeight.currentAlpha,\n\t\t\tanim::easeInCubic);\n\t\trulersView.setAlpha(_animationValueHeightAlpha.current());\n\t}\n\n\tif (!bottomLineAlphaFinished) {\n\t\tconst auto value = _animValueBottomLineAlpha.current();\n\t\tfor (auto &date : dateLines) {\n\t\t\tdate.alpha = (1. - value) * date.fixedAlpha;\n\t\t}\n\t\tdateLines.back().alpha = value;\n\t} else {\n\t\tif (dateLines.size() > 1) {\n\t\t\tconst auto data = dateLines.back();\n\t\t\tdateLines.clear();\n\t\t\tdateLines.push_back(data);\n\t\t}\n\t}\n}\n\nLimits ChartWidget::ChartAnimationController::currentXLimits() const {\n\treturn { _animationValueXMin.current(), _animationValueXMax.current() };\n}\n\nLimits ChartWidget::ChartAnimationController::currentXIndices() const {\n\treturn _currentXIndices;\n}\n\nLimits ChartWidget::ChartAnimationController::finalXLimits() const {\n\treturn { _animationValueXMin.to(), _animationValueXMax.to() };\n}\n\nLimits ChartWidget::ChartAnimationController::currentHeightLimits() const {\n\treturn {\n\t\t_animationValueHeightMin.current(),\n\t\t_animationValueHeightMax.current(),\n\t};\n}\n\nauto ChartWidget::ChartAnimationController::currentFooterHeightLimits() const\n-> Limits {\n\treturn {\n\t\t_animationValueFooterHeightMin.current(),\n\t\t_animationValueFooterHeightMax.current(),\n\t};\n}\n\nLimits ChartWidget::ChartAnimationController::finalHeightLimits() const {\n\treturn _finalHeightLimits;\n}\n\nbool ChartWidget::ChartAnimationController::animating() const {\n\treturn _animation.animating();\n}\n\nbool ChartWidget::ChartAnimationController::footerAnimating() const {\n\treturn (_animationValueFooterHeightMin.current()\n\t\t\t!= _animationValueFooterHeightMin.to())\n\t\t|| (_animationValueFooterHeightMax.current()\n\t\t\t!= _animationValueFooterHeightMax.to());\n}\n\nChartWidget::ChartWidget(not_null<Ui::RpWidget*> parent)\n: Ui::RpWidget(parent)\n, _chartArea(base::make_unique_q<RpMouseWidget>(this))\n, _header(std::make_unique<Header>(this))\n, _footer(std::make_unique<Footer>(this))\n, _linesFilterController(std::make_shared<LinesFilterController>())\n, _animationController([=] {\n\t_chartArea->update();\n\tif (_animationController.footerAnimating()\n\t\t|| !_linesFilterController->isFinished()) {\n\t\t_footer->update();\n\t}\n}) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tif (_chartData) {\n\t\t\tFillLineColorsByKey(_chartData);\n\t\t}\n\t}, lifetime());\n\tsetupChartArea();\n\t// We have to create the footer,\n\t// even if it has to be hidden, otherwise it breaks some things.\n\tsetupFooter();\n}\n\nint ChartWidget::resizeGetHeight(int newWidth) {\n\tif (newWidth <= 0) {\n\t\treturn 0;\n\t}\n\tif (_filterButtons) {\n\t\t_filterButtons->resizeToWidth(newWidth);\n\t}\n\tconst auto filtersTopSkip = st::statisticsFilterButtonsPadding.top();\n\tconst auto filtersHeight = _filterButtons\n\t\t? (_filterButtons->height()\n\t\t\t+ st::statisticsFilterButtonsPadding.bottom())\n\t\t: 0;\n\tconst auto &headerPadding = st::statisticsChartHeaderPadding;\n\t{\n\t\t_header->moveToLeft(headerPadding.left(), headerPadding.top());\n\t\t_header->resizeToWidth(newWidth - rect::m::sum::h(headerPadding));\n\t}\n\tconst auto headerArea = rect::m::sum::v(headerPadding)\n\t\t+ _header->height();\n\tconst auto footerArea = (!_footer->isHidden())\n\t\t? (st::statisticsChartFooterHeight + st::statisticsChartFooterSkip)\n\t\t: 0;\n\tconst auto resultHeight = headerArea\n\t\t+ st::statisticsChartHeight\n\t\t+ footerArea\n\t\t+ filtersTopSkip\n\t\t+ filtersHeight;\n\t{\n\t\tif (footerArea) {\n\t\t\t_footer->resizeToWidth(newWidth);\n\t\t\t_footer->moveToLeft(\n\t\t\t\t0,\n\t\t\t\tresultHeight\n\t\t\t\t\t- st::statisticsChartFooterHeight\n\t\t\t\t\t- filtersTopSkip\n\t\t\t\t\t- filtersHeight);\n\t\t}\n\t\tif (_filterButtons) {\n\t\t\t_filterButtons->moveToLeft(0, resultHeight - filtersHeight);\n\t\t}\n\t\t_chartArea->setGeometry(\n\t\t\t0,\n\t\t\theaderArea,\n\t\t\tnewWidth,\n\t\t\tresultHeight\n\t\t\t\t- headerArea\n\t\t\t\t- footerArea\n\t\t\t\t- filtersTopSkip\n\t\t\t\t- filtersHeight);\n\n\t\t{\n\t\t\tupdateChartFullWidth(newWidth);\n\t\t\tupdateBottomDates();\n\t\t}\n\t}\n\treturn resultHeight;\n}\n\nvoid ChartWidget::updateChartFullWidth(int w) {\n\tconst auto finalXLimits = _animationController.finalXLimits();\n\t_bottomLine.chartFullWidth = (finalXLimits.max == finalXLimits.min)\n\t\t? 0\n\t\t: (w / (finalXLimits.max - finalXLimits.min));\n}\n\nQRect ChartWidget::chartAreaRect() const {\n\treturn _chartArea->rect()\n\t\t- QMargins(\n\t\t\tst::lineWidth,\n\t\t\tst::boxTextFont->height,\n\t\t\tst::lineWidth,\n\t\t\tst::lineWidth\n\t\t\t\t+ st::statisticsChartBottomCaptionHeight\n\t\t\t\t+ st::statisticsChartBottomCaptionSkip);\n}\n\nvoid ChartWidget::setupChartArea() {\n\t_chartArea->paintRequest(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tauto p = QPainter(_chartArea.get());\n\n\t\tconst auto now = crl::now();\n\n\t\t_animationController.tick(\n\t\t\tnow,\n\t\t\t_rulersView,\n\t\t\t_bottomLine.dates,\n\t\t\t_chartView,\n\t\t\t_linesFilterController);\n\n\t\tconst auto chartRect = chartAreaRect();\n\n\t\tp.fillRect(r, st::boxBg);\n\n\t\tif (!_chartData) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!_areRulersAbove) {\n\t\t\t_rulersView.paintRulers(p, chartRect);\n\t\t}\n\n\t\tconst auto context = PaintContext{\n\t\t\t_chartData,\n\t\t\t_animationController.currentXIndices(),\n\t\t\t_animationController.currentXLimits(),\n\t\t\t_animationController.currentHeightLimits(),\n\t\t\tchartRect,\n\t\t\tfalse,\n\t\t};\n\n\t\t{\n\t\t\tPainterHighQualityEnabler hp(p);\n\t\t\t_chartView->paint(p, context);\n\t\t}\n\n\t\tif (!_areRulersAbove) {\n\t\t\t_rulersView.paintCaptionsToRulers(p, chartRect);\n\t\t}\n\t\t{\n\t\t\t[[maybe_unused]] const auto o = ScopedPainterOpacity(\n\t\t\t\tp,\n\t\t\t\tp.opacity() * kRulerLineAlpha);\n\t\t\tconst auto bottom = rect()\n\t\t\t\t- QMargins{ 0, rect::bottom(chartRect), 0, 0 };\n\t\t\tp.fillRect(bottom, st::boxBg);\n\t\t\tp.fillRect(\n\t\t\t\tQRect(bottom.x(), bottom.y(), bottom.width(), st::lineWidth),\n\t\t\t\tst::boxTextFg);\n\t\t}\n\t\tif (_details.widget) {\n\t\t\tconst auto detailsAlpha = _details.widget->alpha();\n\n\t\t\tfor (const auto &line : _chartData.lines) {\n\t\t\t\t_details.widget->setLineAlpha(\n\t\t\t\t\tline.id,\n\t\t\t\t\t_linesFilterController->alpha(line.id));\n\t\t\t}\n\t\t\t_chartView->paintSelectedXIndex(\n\t\t\t\tp,\n\t\t\t\tcontext,\n\t\t\t\t_details.widget->xIndex(),\n\t\t\t\tdetailsAlpha);\n\t\t}\n\t\tif (_areRulersAbove) {\n\t\t\t_rulersView.paintRulers(p, chartRect);\n\t\t\t_rulersView.paintCaptionsToRulers(p, chartRect);\n\t\t}\n\n\t\tp.setPen(st::windowSubTextFg);\n\t\tPaintBottomLine(\n\t\t\tp,\n\t\t\t_bottomLine.dates,\n\t\t\t_chartData,\n\t\t\t_animationController.finalXLimits(),\n\t\t\t_bottomLine.chartFullWidth,\n\t\t\t_chartArea->width(),\n\t\t\trect::bottom(chartRect) + st::statisticsChartBottomCaptionSkip,\n\t\t\t_bottomLine.captionIndicesOffset);\n\t}, _footer->lifetime());\n}\n\nvoid ChartWidget::updateBottomDates() {\n\tif (!_chartData || !_bottomLine.chartFullWidth) {\n\t\treturn;\n\t}\n\tconst auto d = _bottomLine.chartFullWidth * _chartData.oneDayPercentage;\n\tconst auto k = _chartArea->width() / d;\n\tconst auto stepRaw = int(k / 6);\n\n\tconst auto by = int(_chartArea->width() / float64(_chartData.x.size()));\n\t_bottomLine.captionIndicesOffset = 0\n\t\t+ _chartData.dayStringMaxWidth / std::max(by, 1);\n\n\tconst auto isCurrentNull = (_bottomLine.current.stepMinFast == 0);\n\tif (!isCurrentNull\n\t\t&& (stepRaw < _bottomLine.current.stepMax)\n\t\t&& (stepRaw > _bottomLine.current.stepMin)) {\n\t\treturn;\n\t}\n\tconst auto highestOneBit = [](unsigned int v) {\n\t\tif (!v) {\n\t\t\treturn 0;\n\t\t}\n\t\tauto r = unsigned(1);\n\n\t\twhile (v >>= 1) {\n\t\t\tr *= 2;\n\t\t}\n\t\treturn int(r);\n\t};\n\tconst auto step = highestOneBit(stepRaw) << 1;\n\tif (!isCurrentNull && (_bottomLine.current.step == step)) {\n\t\treturn;\n\t}\n\n\tconstexpr auto kStepRatio = 0.1;\n\tconstexpr auto kFastStepOffset = 4;\n\tconst auto stepMax = int(step + step * kStepRatio);\n\tconst auto stepMin = int(step - step * kStepRatio);\n\tconst auto stepMinFast = stepMin - kFastStepOffset;\n\n\tauto data = BottomCaptionLineData{\n\t\t.step = step,\n\t\t.stepMax = stepMax,\n\t\t.stepMin = stepMin,\n\t\t.stepMinFast = stepMinFast,\n\t\t.stepRaw = stepRaw,\n\t\t.alpha = 1.,\n\t};\n\n\tif (isCurrentNull) {\n\t\t_bottomLine.current = data;\n\t\t_bottomLine.dates.push_back(data);\n\t\treturn;\n\t}\n\n\t_bottomLine.current = data;\n\n\tfor (auto &date : _bottomLine.dates) {\n\t\tdate.fixedAlpha = date.alpha;\n\t}\n\n\t_bottomLine.dates.push_back(data);\n\tif (_bottomLine.dates.size() > 2) {\n\t\t_bottomLine.dates.erase(begin(_bottomLine.dates));\n\t}\n\n\t_animationController.restartBottomLineAlpha();\n}\n\nvoid ChartWidget::updateHeader() {\n\tif (!_chartData) {\n\t\treturn;\n\t}\n\tconst auto i = _animationController.currentXIndices();\n\t_header->setSubTitle(HeaderSubTitle(_chartData, i.min, i.max));\n\t_header->update();\n}\n\nvoid ChartWidget::setupFooter() {\n\t_footer->setPaintChartCallback([=, fullXLimits = Limits{ 0., 1. }](\n\t\t\tQPainter &p,\n\t\t\tconst QRect &r) {\n\t\tif (_chartData) {\n\t\t\tp.fillRect(r, st::boxBg);\n\n\t\t\tauto hp = PainterHighQualityEnabler(p);\n\t\t\t_chartView->paint(\n\t\t\t\tp,\n\t\t\t\tPaintContext{\n\t\t\t\t\t_chartData,\n\t\t\t\t\t{ 0., float64(_chartData.x.size() - 1) },\n\t\t\t\t\tfullXLimits,\n\t\t\t\t\t_animationController.currentFooterHeightLimits(),\n\t\t\t\t\tr,\n\t\t\t\t\ttrue,\n\t\t\t\t});\n\t\t}\n\t});\n\n\t_animationController.addRulerRequests(\n\t) | rpl::on_next([=] {\n\t\t_rulersView.add(\n\t\t\t_animationController.finalHeightLimits(),\n\t\t\ttrue);\n\t\t_animationController.start();\n\t}, _footer->lifetime());\n\n\t_footer->xPercentageLimitsChange(\n\t) | rpl::on_next([=](Limits xPercentageLimits) {\n\t\tif (!_chartView) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto now = crl::now();\n\t\tif (_details.widget\n\t\t\t&& (_details.widget->xIndex() >= 0)\n\t\t\t&& !_details.animation.animating()) {\n\t\t\t_details.hideOnAnimationEnd = true;\n\t\t\t_details.animation.start();\n\t\t}\n\t\t_animationController.setXPercentageLimits(\n\t\t\t_chartData,\n\t\t\txPercentageLimits,\n\t\t\t_chartView,\n\t\t\t_linesFilterController,\n\t\t\tnow);\n\t\tupdateChartFullWidth(_chartArea->width());\n\t\tupdateBottomDates();\n\t\tupdateHeader();\n\t\tif ((now - _lastHeightLimitsChanged) < kHeightLimitsUpdateTimeout) {\n\t\t\treturn;\n\t\t}\n\t\t_lastHeightLimitsChanged = now;\n\t\t_rulersView.add(\n\t\t\t_animationController.finalHeightLimits(),\n\t\t\ttrue);\n\t}, _footer->lifetime());\n}\n\nvoid ChartWidget::setupDetails() {\n\tif (!_chartData) {\n\t\t_details.widget = nullptr;\n\t\t_chartArea->update();\n\t\treturn;\n\t}\n\tif (hasLocalZoom()) {\n\t\t_zoomEnabled = true;\n\t}\n\t_details.widget = base::make_unique_q<PointDetailsWidget>(\n\t\tthis,\n\t\t_chartData,\n\t\t_zoomEnabled);\n\t_details.widget->setClickedCallback([=] {\n\t\tconst auto index = _details.widget->xIndex();\n\t\tif (index < 0) {\n\t\t\treturn;\n\t\t}\n\t\tif (hasLocalZoom()) {\n\t\t\tprocessLocalZoom(index);\n\t\t} else {\n\t\t\t_zoomRequests.fire_copy(_chartData.x[index]);\n\t\t}\n\t});\n\n\t_details.widget->shownValue(\n\t) | rpl::on_next([=](bool shown) {\n\t\tif (shown && _details.widget->xIndex() < 0) {\n\t\t\t_details.widget->hide();\n\t\t}\n\t}, _details.widget->lifetime());\n\n\t_chartArea->mouseStateChanged(\n\t) | rpl::on_next([=](const RpMouseWidget::State &state) {\n\t\tif (_animationController.animating()) {\n\t\t\treturn;\n\t\t}\n\t\tswitch (state.mouseState) {\n\t\tcase QEvent::MouseButtonPress:\n\t\tcase QEvent::MouseMove: {\n\t\t\tconst auto wasXIndex = _details.widget->xIndex();\n\t\t\tconst auto chartRect = chartAreaRect();\n\t\t\tconst auto currentXLimits = _animationController.finalXLimits();\n\t\t\tconst auto nearestXIndex = _chartView->findXIndexByPosition(\n\t\t\t\t_chartData,\n\t\t\t\tcurrentXLimits,\n\t\t\t\tchartRect,\n\t\t\t\tstate.point.x());\n\t\t\tif (nearestXIndex < 0) {\n\t\t\t\t_details.widget->setXIndex(nearestXIndex);\n\t\t\t\t_details.widget->hide();\n\t\t\t\t_chartArea->update();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto currentX = 0\n\t\t\t\t+ chartRect.width() * InterpolationRatio(\n\t\t\t\t\tcurrentXLimits.min,\n\t\t\t\t\tcurrentXLimits.max,\n\t\t\t\t\t_chartData.xPercentage[nearestXIndex]);\n\t\t\tconst auto widgetArea = _details.widget->width()\n\t\t\t\t+ st::statisticsDetailsPopupPadding.left();\n\t\t\tconst auto xLeft = currentX - widgetArea;\n\t\t\tconst auto x = (xLeft >= 0)\n\t\t\t\t? xLeft\n\t\t\t\t: ((currentX + widgetArea - _chartArea->width()) > 0)\n\t\t\t\t? 0\n\t\t\t\t: currentX;\n\t\t\t_details.widget->moveToLeft(\n\t\t\t\tstd::clamp(\n\t\t\t\t\tint(x),\n\t\t\t\t\t_chartArea->x(),\n\t\t\t\t\trect::right(_chartArea) - widgetArea),\n\t\t\t\t_chartArea->y());\n\t\t\t_details.widget->setXIndex(nearestXIndex);\n\t\t\tif (_details.widget->isHidden()) {\n\t\t\t\t_details.hideOnAnimationEnd = false;\n\t\t\t\t_details.animation.start();\n\t\t\t} else if ((state.mouseState == QEvent::MouseButtonPress)\n\t\t\t\t&& (wasXIndex == nearestXIndex)) {\n\t\t\t\t_details.hideOnAnimationEnd = true;\n\t\t\t\t_details.animation.start();\n\t\t\t}\n\t\t\t_details.widget->show();\n\t\t\t_chartArea->update();\n\t\t} break;\n\t\tcase QEvent::MouseButtonRelease: {\n\t\t} break;\n\t\t}\n\t}, _details.widget->lifetime());\n\n\t_details.animation.init([=](crl::time now) {\n\t\tconst auto value = std::clamp(\n\t\t\t(now - _details.animation.started()) / float64(200),\n\t\t\t0.,\n\t\t\t1.);\n\t\tconst auto alpha = _details.hideOnAnimationEnd ? (1. - value) : value;\n\t\tif (_details.widget) {\n\t\t\t_details.widget->setAlpha(alpha);\n\t\t\t_details.widget->update();\n\t\t}\n\t\tif (value >= 1.) {\n\t\t\tif (_details.hideOnAnimationEnd && _details.widget) {\n\t\t\t\t_details.widget->hide();\n\t\t\t\t_details.widget->setXIndex(-1);\n\t\t\t}\n\t\t\t_details.animation.stop();\n\t\t}\n\t\t_chartArea->update();\n\t});\n}\n\nbool ChartWidget::hasLocalZoom() const {\n\treturn _chartData\n\t\t&& _chartView->maybeLocalZoom({\n\t\t\t_chartData,\n\t\t\tAbstractChartView::LocalZoomArgs::Type::CheckAvailability,\n\t\t}).hasZoom;\n}\n\nvoid ChartWidget::processLocalZoom(int xIndex) {\n\tusing Type = AbstractChartView::LocalZoomArgs::Type;\n\tconstexpr auto kFooterZoomDuration = crl::time(400);\n\tif (_footer->isHidden()) {\n\t\treturn;\n\t}\n\t_localZoomLifetime.destroy();\n\tconst auto wasZoom = _footer->xPercentageLimits();\n\n\tconst auto headerOwned\n\t\t= _localZoomLifetime.make_state<base::unique_qptr<Header>>(\n\t\t\tbase::make_unique_q<Header>(this));\n\tconst auto header = headerOwned->get();\n\theader->show();\n\t_header->geometryValue(\n\t) | rpl::on_next([=](const QRect &g) {\n\t\theader->setGeometry(g);\n\t}, header->lifetime());\n\theader->setTitle(_header->title());\n\theader->setSubTitle(HeaderSubTitle(_chartData, xIndex, xIndex));\n\n\tconst auto enableMouse = [=](bool value) {\n\t\tsetAttribute(Qt::WA_TransparentForMouseEvents, !value);\n\t};\n\n\tconst auto mouseTrackingLifetime\n\t\t= _localZoomLifetime.make_state<rpl::lifetime>();\n\t_chartView->setUpdateCallback([=] { _chartArea->update(); });\n\tconst auto createMouseTracking = [=] {\n\t\t_chartArea->setMouseTracking(true);\n\t\t*mouseTrackingLifetime = _chartArea->events(\n\t\t) | rpl::filter([](not_null<QEvent*> event) {\n\t\t\treturn (event->type() == QEvent::MouseMove)\n\t\t\t\t|| (event->type() == QEvent::Leave);\n\t\t}) | rpl::on_next([=](not_null<QEvent*> event) {\n\t\t\tauto pos = QPoint();\n\t\t\tif (event->type() == QEvent::MouseMove) {\n\t\t\t\tconst auto e = static_cast<QMouseEvent*>(event.get());\n\t\t\t\tpos = e->pos();\n\t\t\t}\n\t\t\t_chartView->handleMouseMove(_chartData, _chartArea->rect(), pos);\n\t\t});\n\t\tmouseTrackingLifetime->add(crl::guard(_chartArea.get(), [=] {\n\t\t\t_chartArea->setMouseTracking(false);\n\t\t}));\n\t};\n\n\tconst auto zoomOutButton = Ui::CreateChild<Ui::RoundButton>(\n\t\theader,\n\t\ttr::lng_stats_zoom_out(),\n\t\tst::statisticsHeaderButton);\n\tzoomOutButton->moveToRight(\n\t\t0,\n\t\t(header->height() - zoomOutButton->height()) / 2);\n\tzoomOutButton->show();\n\tzoomOutButton->setClickedCallback([=] {\n\t\tauto lifetime = _localZoomLifetime.make_state<rpl::lifetime>();\n\t\tconst auto animation = lifetime->make_state<Ui::Animations::Simple>();\n\t\tconst auto currentXPercentage = _footer->xPercentageLimits();\n\t\tanimation->start([=](float64 value) {\n\t\t\t_chartView->maybeLocalZoom({\n\t\t\t\t_chartData,\n\t\t\t\tType::SkipCalculation,\n\t\t\t\tvalue,\n\t\t\t});\n\t\t\tconst auto p = value;\n\t\t\t_footer->setXPercentageLimits({\n\t\t\t\tanim::interpolateF(wasZoom.min, currentXPercentage.min, p),\n\t\t\t\tanim::interpolateF(wasZoom.max, currentXPercentage.max, p),\n\t\t\t});\n\t\t\tif (value == 0.) {\n\t\t\t\tif (lifetime) {\n\t\t\t\t\tlifetime->destroy();\n\t\t\t\t}\n\t\t\t\tmouseTrackingLifetime->destroy();\n\t\t\t\tenableMouse(true);\n\t\t\t}\n\t\t}, 1., 0., kFooterZoomDuration, anim::easeOutCirc);\n\t\tenableMouse(false);\n\n\t\tUi::Animations::HideWidgets({ header });\n\t});\n\n\tUi::Animations::ShowWidgets({ header });\n\n\tconst auto finish = [=](const Limits &zoomLimitIndices) {\n\t\tcreateMouseTracking();\n\t\t_footer->xPercentageLimitsChange(\n\t\t) | rpl::on_next([=](const Limits &l) {\n\t\t\tconst auto r = FindStackXIndicesFromRawXPercentages(\n\t\t\t\t_chartData,\n\t\t\t\tl,\n\t\t\t\tzoomLimitIndices);\n\t\t\theader->setSubTitle(HeaderSubTitle(_chartData, r.min, r.max));\n\t\t\theader->update();\n\t\t}, header->lifetime());\n\t};\n\n\t{\n\t\tauto lifetime = _localZoomLifetime.make_state<rpl::lifetime>();\n\t\tconst auto animation = lifetime->make_state<Ui::Animations::Simple>();\n\t\t_chartView->maybeLocalZoom({ _chartData, Type::Prepare });\n\t\tanimation->start([=](float64 value) {\n\t\t\tconst auto zoom = _chartView->maybeLocalZoom({\n\t\t\t\t_chartData,\n\t\t\t\tType::Process,\n\t\t\t\tvalue,\n\t\t\t\txIndex,\n\t\t\t});\n\t\t\tconst auto result = Limits{\n\t\t\t\tanim::interpolateF(wasZoom.min, zoom.range.min, value),\n\t\t\t\tanim::interpolateF(wasZoom.max, zoom.range.max, value),\n\t\t\t};\n\t\t\t_footer->setXPercentageLimits(result);\n\t\t\tif (value == 1.) {\n\t\t\t\tif (lifetime) {\n\t\t\t\t\tlifetime->destroy();\n\t\t\t\t}\n\t\t\t\tfinish(zoom.limitIndices);\n\t\t\t\tenableMouse(true);\n\t\t\t}\n\t\t}, 0., 1., kFooterZoomDuration, anim::easeOutCirc);\n\t\tenableMouse(false);\n\t}\n}\n\nvoid ChartWidget::setupFilterButtons() {\n\tif (!_chartData || (_chartData.lines.size() <= 1)) {\n\t\t_filterButtons = nullptr;\n\t\t_chartArea->update();\n\t\treturn;\n\t}\n\t_filterButtons = base::make_unique_q<ChartLinesFilterWidget>(this);\n\t_filterButtons->show();\n\t{\n\t\tauto data = std::vector<ChartLinesFilterWidget::ButtonData>();\n\t\tdata.reserve(_chartData.lines.size());\n\t\tfor (const auto &line : _chartData.lines) {\n\t\t\tdata.push_back({\n\t\t\t\tline.name,\n\t\t\t\tline.color,\n\t\t\t\tline.id,\n\t\t\t\tline.isHiddenOnStart,\n\t\t\t});\n\t\t\tif (line.isHiddenOnStart) {\n\t\t\t\t_linesFilterController->setEnabled(line.id, false, 1);\n\t\t\t}\n\t\t}\n\n\t\t_filterButtons->fillButtons(data);\n\t\t_linesFilterController->tick(1.);\n\t}\n\n\t_filterButtons->buttonEnabledChanges(\n\t) | rpl::on_next([=](const ChartLinesFilterWidget::Entry &e) {\n\t\tconst auto now = crl::now();\n\t\t_linesFilterController->setEnabled(e.id, e.enabled, now);\n\n\t\t_animationController.setXPercentageLimits(\n\t\t\t_chartData,\n\t\t\t_animationController.currentXLimits(),\n\t\t\t_chartView,\n\t\t\t_linesFilterController,\n\t\t\tnow);\n\t}, _filterButtons->lifetime());\n}\n\nvoid ChartWidget::setChartData(\n\t\tData::StatisticalChart chartData,\n\t\tChartViewType type) {\n\tif (width() < st::statisticsChartHeight) {\n\t\tsizeValue(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\tif (s.width() > st::statisticsChartHeight) {\n\t\t\t\tsetChartData(chartData, type);\n\t\t\t\t_waitingSizeLifetime.destroy();\n\t\t\t}\n\t\t}, _waitingSizeLifetime);\n\t\treturn;\n\t}\n\tif (_chartData) {\n\t\t// We don't really support a replacement of chart data in runtime.\n\t\treturn;\n\t}\n\t_chartData = std::move(chartData);\n\tFillLineColorsByKey(_chartData);\n\n\t_chartView = CreateChartView(type);\n\t_chartView->setLinesFilterController(_linesFilterController);\n\t_rulersView.setChartData(_chartData, type, _linesFilterController);\n\t_areRulersAbove = (type == ChartViewType::StackBar);\n\n\tif (_chartData.isFooterHidden) {\n\t\t_footer->hide();\n\t}\n\n\tsetupDetails();\n\tsetupFilterButtons();\n\n\tconst auto defaultZoom = Limits{\n\t\t_chartData.xPercentage[_chartData.defaultZoomXIndex.min],\n\t\t_chartData.xPercentage[_chartData.defaultZoomXIndex.max],\n\t};\n\t_footer->setXPercentageLimits(defaultZoom);\n\t_animationController.setXPercentageLimits(\n\t\t_chartData,\n\t\tdefaultZoom,\n\t\t_chartView,\n\t\t_linesFilterController,\n\t\t0);\n\tupdateHeader();\n\t_animationController.finish();\n\t_rulersView.add(_animationController.finalHeightLimits(), false);\n\n\t_chartArea->update();\n\t_footer->update();\n\n\tRpWidget::resizeToWidth(width());\n}\n\nvoid ChartWidget::setTitle(rpl::producer<QString> &&title) {\n\tstd::move(\n\t\ttitle\n\t) | rpl::on_next([=](QString t) {\n\t\t_header->setTitle(std::move(t));\n\t\t_header->update();\n\t}, _header->lifetime());\n}\n\nvoid ChartWidget::setZoomedChartData(\n\t\tData::StatisticalChart chartData,\n\t\tfloat64 x,\n\t\tChartViewType type) {\n\t_zoomedChartWidget = base::make_unique_q<ChartWidget>(\n\t\tdynamic_cast<Ui::RpWidget*>(parentWidget()));\n\tgeometryValue(\n\t) | rpl::on_next([=](const QRect &geometry) {\n\t\t_zoomedChartWidget->moveToLeft(geometry.x(), geometry.y());\n\t}, _zoomedChartWidget->lifetime());\n\t_zoomedChartWidget->show();\n\t_zoomedChartWidget->resizeToWidth(width());\n\t_zoomedChartWidget->setChartData(std::move(chartData), type);\n\n\tconst auto customHeader = Ui::CreateChild<Header>(\n\t\t_zoomedChartWidget.get());\n\t{\n\t\tconst auto xIndex = std::distance(\n\t\t\tbegin(_chartData.x),\n\t\t\tranges::find(_chartData.x, x));\n\t\tcustomHeader->setTitle(_header->title());\n\t\tif ((xIndex >= 0) && (xIndex < _chartData.x.size())) {\n\t\t\tcustomHeader->setSubTitle(\n\t\t\t\tHeaderSubTitle(_chartData, xIndex, xIndex));\n\t\t}\n\t\tconst auto &headerPadding = st::statisticsChartHeaderPadding;\n\t\tcustomHeader->moveToLeft(headerPadding.left(), headerPadding.top());\n\t\tcustomHeader->resizeToWidth(width() - rect::m::sum::h(headerPadding));\n\t}\n\n\tconst auto zoomOutButton = Ui::CreateChild<Ui::RoundButton>(\n\t\tcustomHeader,\n\t\ttr::lng_stats_zoom_out(),\n\t\tst::statisticsHeaderButton);\n\tzoomOutButton->moveToRight(\n\t\t0,\n\t\t(customHeader->height() - zoomOutButton->height()) / 2);\n\tzoomOutButton->setClickedCallback([=] {\n\t\tshownValue(\n\t\t) | rpl::on_next([=](bool shown) {\n\t\t\tif (shown) {\n\t\t\t\t_zoomedChartWidget = nullptr;\n\t\t\t}\n\t\t}, _zoomedChartWidget->lifetime());\n\t\tUi::Animations::ShowWidgets({ this });\n\t\tUi::Animations::HideWidgets({ _zoomedChartWidget.get() });\n\t});\n\n\tUi::Animations::ShowWidgets({ _zoomedChartWidget.get(), customHeader });\n\tUi::Animations::HideWidgets({ this });\n}\n\nrpl::producer<float64> ChartWidget::zoomRequests() {\n\t_zoomEnabled = true;\n\tsetupDetails();\n\treturn _zoomRequests.events();\n}\n\nvoid FixCacheForHighDPIChartWidget(not_null<Ui::RpWidget*> container) {\n\tif (style::DevicePixelRatio() != 1) {\n\t\tcontainer->paintRequest() | rpl::take(2) | rpl::on_next([=] {\n\t\t\tcontainer->update();\n\t\t}, container->lifetime());\n\t}\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/chart_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_statistics_chart.h\"\n#include \"statistics/view/chart_rulers_view.h\"\n#include \"statistics/statistics_common.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Statistic {\n\nclass RpMouseWidget;\nclass PointDetailsWidget;\nclass ChartLinesFilterWidget;\nclass AbstractChartView;\nclass Header;\nclass LinesFilterController;\n\nclass ChartWidget : public Ui::RpWidget {\npublic:\n\tChartWidget(not_null<Ui::RpWidget*> parent);\n\n\tvoid setChartData(Data::StatisticalChart chartData, ChartViewType type);\n\tvoid setTitle(rpl::producer<QString> &&title);\n\tvoid setZoomedChartData(\n\t\tData::StatisticalChart chartData,\n\t\tfloat64 x,\n\t\tChartViewType type);\n\tvoid addRuler(Limits newHeight, bool animated);\n\n\t[[nodiscard]] rpl::producer<float64> zoomRequests();\n\n\tstruct BottomCaptionLineData final {\n\t\tint step = 0;\n\t\tint stepMax = 0;\n\t\tint stepMin = 0;\n\t\tint stepMinFast = 0;\n\t\tint stepRaw = 0;\n\n\t\tfloat64 alpha = 0.;\n\t\tfloat64 fixedAlpha = 0.;\n\t};\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tclass Footer;\n\n\tclass ChartAnimationController final {\n\tpublic:\n\t\tChartAnimationController(Fn<void()> &&updateCallback);\n\n\t\tvoid setXPercentageLimits(\n\t\t\tData::StatisticalChart &chartData,\n\t\t\tLimits xPercentageLimits,\n\t\t\tconst std::unique_ptr<AbstractChartView> &chartView,\n\t\t\tconst std::shared_ptr<LinesFilterController> &linesFilter,\n\t\t\tcrl::time now);\n\t\tvoid start();\n\t\tvoid finish();\n\t\tvoid resetAlpha();\n\t\tvoid restartBottomLineAlpha();\n\t\tvoid tick(\n\t\t\tcrl::time now,\n\t\t\tChartRulersView &rulersView,\n\t\t\tstd::vector<BottomCaptionLineData> &dateLines,\n\t\t\tconst std::unique_ptr<AbstractChartView> &chartView,\n\t\t\tconst std::shared_ptr<LinesFilterController> &linesFilter);\n\n\t\t[[nodiscard]] Limits currentXLimits() const;\n\t\t[[nodiscard]] Limits currentXIndices() const;\n\t\t[[nodiscard]] Limits finalXLimits() const;\n\t\t[[nodiscard]] Limits currentHeightLimits() const;\n\t\t[[nodiscard]] Limits currentFooterHeightLimits() const;\n\t\t[[nodiscard]] Limits finalHeightLimits() const;\n\t\t[[nodiscard]] bool animating() const;\n\t\t[[nodiscard]] bool footerAnimating() const;\n\n\t\t[[nodiscard]] rpl::producer<> addRulerRequests() const;\n\n\tprivate:\n\t\tUi::Animations::Basic _animation;\n\n\t\tcrl::time _lastUserInteracted = 0;\n\t\tcrl::time _bottomLineAlphaAnimationStartedAt = 0;\n\n\t\tanim::value _animationValueXMin;\n\t\tanim::value _animationValueXMax;\n\t\tanim::value _animationValueHeightMin;\n\t\tanim::value _animationValueHeightMax;\n\n\t\tanim::value _animationValueFooterHeightMin;\n\t\tanim::value _animationValueFooterHeightMax;\n\n\t\tanim::value _animationValueHeightAlpha;\n\n\t\tanim::value _animValueBottomLineAlpha;\n\n\t\tLimits _finalHeightLimits;\n\t\tLimits _currentXIndices;\n\n\t\tstruct {\n\t\t\tfloat speed = 0.;\n\t\t\tLimits current;\n\n\t\t\tfloat64 currentAlpha = 0.;\n\t\t} _dtHeight;\n\t\tLimits _previousFullHeightLimits;\n\n\t\tstruct {\n\t\t\tcrl::time lastTickedAt = 0;\n\t\t\tbool lastFPSSlow = false;\n\t\t} _benchmark;\n\n\t\trpl::event_stream<> _addRulerRequests;\n\n\t};\n\n\t[[nodiscard]] QRect chartAreaRect() const;\n\n\tvoid setupChartArea();\n\tvoid setupFooter();\n\tvoid setupDetails();\n\tvoid setupFilterButtons();\n\n\tvoid updateBottomDates();\n\tvoid updateHeader();\n\n\tvoid updateChartFullWidth(int w);\n\n\t[[nodiscard]] bool hasLocalZoom() const;\n\tvoid processLocalZoom(int xIndex);\n\n\tconst base::unique_qptr<RpMouseWidget> _chartArea;\n\tconst std::unique_ptr<Header> _header;\n\tconst std::unique_ptr<Footer> _footer;\n\tbase::unique_qptr<ChartLinesFilterWidget> _filterButtons;\n\tData::StatisticalChart _chartData;\n\n\tbase::unique_qptr<ChartWidget> _zoomedChartWidget;\n\n\tstd::unique_ptr<AbstractChartView> _chartView;\n\n\tstruct {\n\t\tbase::unique_qptr<PointDetailsWidget> widget;\n\t\tUi::Animations::Basic animation;\n\t\tbool hideOnAnimationEnd = false;\n\t} _details;\n\n\tstruct {\n\t\tBottomCaptionLineData current;\n\t\tstd::vector<BottomCaptionLineData> dates;\n\t\tint chartFullWidth = 0;\n\t\tint captionIndicesOffset = 0;\n\t} _bottomLine;\n\n\tbool _areRulersAbove = false;\n\n\tstd::shared_ptr<LinesFilterController> _linesFilterController;\n\n\tChartAnimationController _animationController;\n\tcrl::time _lastHeightLimitsChanged = 0;\n\n\tChartRulersView _rulersView;\n\n\tbool _zoomEnabled = false;\n\trpl::event_stream<float64> _zoomRequests;\n\n\trpl::lifetime _waitingSizeLifetime;\n\trpl::lifetime _localZoomLifetime;\n\n};\n\nvoid FixCacheForHighDPIChartWidget(not_null<Ui::RpWidget*> container);\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/segment_tree.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/segment_tree.h\"\n\nnamespace Statistic {\nnamespace {\n\nconstexpr auto kMinArraySize = size_t(30);\n\n} // namespace\n\nSegmentTree::SegmentTree(std::vector<ChartValue> array)\n: _array(std::move(array)) {\n\tif (_array.size() < kMinArraySize) {\n\t\treturn;\n\t}\n\n\t// The max size of this array is about 2 * 2 ^ log2(n) + 1.\n\tconst auto size = 2 * std::pow(\n\t\t2.,\n\t\tstd::floor((std::log(_array.size()) / std::log(2.)) + 1));\n\t_heap.resize(int(size));\n\tbuild(1, 0, _array.size());\n}\n\nvoid SegmentTree::build(ChartValue v, int from, int size) {\n\t_heap[v].from = from;\n\t_heap[v].to = (from + size - 1);\n\n\tif (size == 1) {\n\t\t_heap[v].sum = _array[from];\n\t\t_heap[v].max = _array[from];\n\t\t_heap[v].min = _array[from];\n\t} else {\n\t\t// Build children.\n\t\tbuild(2 * v, from, size / 2);\n\t\tbuild(2 * v + 1, from + size / 2, size - size / 2);\n\n\t\t_heap[v].sum = _heap[2 * v].sum + _heap[2 * v + 1].sum;\n\t\t// max = max of the children.\n\t\t_heap[v].max = std::max(_heap[2 * v].max, _heap[2 * v + 1].max);\n\t\t_heap[v].min = std::min(_heap[2 * v].min, _heap[2 * v + 1].min);\n\t}\n}\n\nChartValue SegmentTree::rMaxQ(int from, int to) {\n\tif (_array.size() < kMinArraySize) {\n\t\tauto max = ChartValue(0);\n\t\tfrom = std::max(from, 0);\n\t\tto = std::min(to, int(_array.size() - 1));\n\t\tfor (auto i = from; i <= to; i++) {\n\t\t\tmax = std::max(max, _array[i]);\n\t\t}\n\t\treturn max;\n\t}\n\treturn rMaxQ(1, from, to);\n}\n\nChartValue SegmentTree::rMaxQ(ChartValue v, int from, int to) {\n\tconst auto &n = _heap[v];\n\t// If you did a range update that contained this node,\n\t// you can infer the Min value without going down the tree.\n\tif (n.pendingVal && contains(n.from, n.to, from, to)) {\n\t\treturn n.pendingVal.value;\n\t}\n\n\tif (contains(from, to, n.from, n.to)) {\n\t\treturn _heap[v].max;\n\t}\n\n\tif (intersects(from, to, n.from, n.to)) {\n\t\tpropagate(v);\n\t\tconst auto leftMin = rMaxQ(2 * v, from, to);\n\t\tconst auto rightMin = rMaxQ(2 * v + 1, from, to);\n\n\t\treturn std::max(leftMin, rightMin);\n\t}\n\n\treturn 0;\n}\n\nChartValue SegmentTree::rMinQ(int from, int to) {\n\tif (_array.size() < kMinArraySize) {\n\t\tauto min = std::numeric_limits<ChartValue>::max();\n\t\tfrom = std::max(from, 0);\n\t\tto = std::min(to, int(_array.size() - 1));\n\t\tfor (auto i = from; i <= to; i++) {\n\t\t\tmin = std::min(min, _array[i]);\n\t\t}\n\t\treturn min;\n\t}\n\treturn rMinQ(1, from, to);\n}\n\nChartValue SegmentTree::rMinQ(ChartValue v, int from, int to) {\n\tconst auto &n = _heap[v];\n\t// If you did a range update that contained this node,\n\t// you can infer the Min value without going down the tree.\n\tif (n.pendingVal && contains(n.from, n.to, from, to)) {\n\t\treturn n.pendingVal.value;\n\t}\n\n\tif (contains(from, to, n.from, n.to)) {\n\t\treturn _heap[v].min;\n\t}\n\n\tif (intersects(from, to, n.from, n.to)) {\n\t\tpropagate(v);\n\t\tconst auto leftMin = rMinQ(2 * v, from, to);\n\t\tconst auto rightMin = rMinQ(2 * v + 1, from, to);\n\n\t\treturn std::min(leftMin, rightMin);\n\t}\n\n\treturn std::numeric_limits<ChartValue>::max();\n}\n\nvoid SegmentTree::propagate(ChartValue v) {\n\tauto &n = _heap[v];\n\n\tif (n.pendingVal) {\n\t\tconst auto value = n.pendingVal.value;\n\t\tn.pendingVal = {};\n\t\tchange(_heap[2 * v], value);\n\t\tchange(_heap[2 * v + 1], value);\n\t}\n}\n\nvoid SegmentTree::change(SegmentTree::Node &n, ChartValue value) {\n\tn.pendingVal = { value, true };\n\tn.sum = n.size() * value;\n\tn.max = value;\n\tn.min = value;\n\t_array[n.from] = value;\n}\n\nbool SegmentTree::contains(int from1, int to1, int from2, int to2) const {\n\treturn (from2 >= from1) && (to2 <= to1);\n}\n\nbool SegmentTree::intersects(int from1, int to1, int from2, int to2) const {\n\treturn ((from1 <= from2) && (to1 >= from2)) // (.[..)..] or (.[...]..)\n\t\t|| ((from1 >= from2) && (from1 <= to2)); // [.(..]..) or [..(..)..\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/segment_tree.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"statistics/statistics_types.h\"\n\nnamespace Statistic {\n\nclass SegmentTree final {\npublic:\n\tSegmentTree() = default;\n\tSegmentTree(std::vector<ChartValue> array);\n\n\t[[nodiscard]] bool empty() const {\n\t\treturn _array.empty();\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n\n\t[[nodiscard]] ChartValue rMaxQ(int from, int to);\n\t[[nodiscard]] ChartValue rMinQ(int from, int to);\n\nprivate:\n\tstruct Node final {\n\t\tChartValue sum = 0;\n\t\tChartValue max = 0;\n\t\tChartValue min = 0;\n\n\t\tstruct PendingVal {\n\t\t\t[[nodiscard]] explicit operator bool() const {\n\t\t\t\treturn available;\n\t\t\t}\n\t\t\tChartValue value = 0;\n\t\t\tbool available = false;\n\t\t};\n\t\tPendingVal pendingVal;\n\n\t\tint from = 0;\n\t\tint to = 0;\n\n\t\t[[nodiscard]] int size() {\n\t\t\treturn to - from + 1;\n\t\t}\n\t};\n\n\tvoid build(ChartValue v, int from, int size);\n\tvoid propagate(ChartValue v);\n\tvoid change(Node &n, ChartValue value);\n\n\t[[nodiscard]] ChartValue rMaxQ(ChartValue v, int from, int to);\n\t[[nodiscard]] ChartValue rMinQ(ChartValue v, int from, int to);\n\n\t[[nodiscard]] bool contains(int from1, int to1, int from2, int to2) const;\n\t[[nodiscard]] bool intersects(\n\t\tint from1,\n\t\tint to1,\n\t\tint from2,\n\t\tint to2) const;\n\n\tstd::vector<ChartValue> _array;\n\tstd::vector<Node> _heap;\n\n};\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/statistics.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\n\nusing \"window/window.style\";\nusing \"ui/widgets/widgets.style\";\n\nstatisticsLayerOverviewMargins: margins(0px, 17px, 0px, 9px);\nstatisticsLayerMargins: margins(20px, 0px, 20px, 0px);\n\nstatisticsChartHeight: 200px;\n\nstatisticsChartEntryPadding: margins(0px, 13px, 0px, 2px);\n\nstatisticsDetailsArrowShift: 3px;\nstatisticsDetailsArrowStroke: 1.5;\nstatisticsDetailsPopupMargins: margins(12px, 8px, 12px, 11px);\nstatisticsDetailsPopupPadding: margins(6px, 6px, 6px, 6px);\nstatisticsDetailsPopupMidLineSpace: 4px;\nstatisticsDetailsDotRadius: 5px;\nstatisticsChartLineWidth: 2px;\n\nstatisticsChartFooterSkip: 11px;\nstatisticsChartFooterHeight: 42px;\nstatisticsChartFooterBetweenSide: 5px;\nstatisticsChartFooterSideWidth: 10px;\nstatisticsChartFooterArrowHeight: 10px;\nstatisticsChartFooterSideRadius: 6px;\n\nstatisticsChartRulerCaptionSkip: 4px;\n\nstatisticsChartBottomCaptionHeight: 15px;\nstatisticsChartBottomCaptionSkip: 6px;\n\nstatisticsChartFlatCheckboxMargins: margins(4px, 3px, 4px, 5px);\nstatisticsChartFlatCheckboxCheckWidth: 3px;\nstatisticsChartFlatCheckboxShrinkkWidth: 4px;\n\nstatisticsFilterButtonsPadding: margins(0px, 12px, 0px, 8px);\n\nstatisticsDetailsPopupHeaderStyle: TextStyle(defaultTextStyle) {\n\tfont: font(12px semibold);\n}\nstatisticsDetailsPopupStyle: TextStyle(defaultTextStyle) {\n\tfont: font(12px);\n}\nstatisticsDetailsBottomCaptionStyle: TextStyle(defaultTextStyle) {\n\tfont: font(10px);\n}\n\nstatisticsPieChartFont: font(20px);\nstatisticsPieChartPartOffset: 8px;\n\nstatisticsChartHeaderHeight: 36px;\nstatisticsChartHeaderPadding: margins(2px, 0px, 0px, 10px);\nstatisticsHeaderTitleTextStyle: TextStyle(defaultTextStyle) {\n\tfont: font(boxFontSize semibold);\n}\nstatisticsHeaderDatesTextStyle: TextStyle(defaultTextStyle) {\n\tfont: font(11px);\n}\nstatisticsHeaderButton: RoundButton(defaultLightButton) {\n\twidth: -14px;\n\theight: 20px;\n\ttextTop: 2px;\n\tstyle: TextStyle(semiboldTextStyle) {\n\t\tfont: font(11px semibold);\n\t}\n}\n\nstatisticsLoadingSubtext: FlatLabel(changePhoneDescription) {\n\tminWidth: 256px;\n}\n\nstatisticsOverviewValue: FlatLabel(boxLabel) {\n\tminWidth: 0px;\n\tmaxHeight: 60px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px);\n\t}\n}\nstatisticsOverviewValuePadding: margins(2px, 1px, 0px, 0px);\nstatisticsOverviewSecondValuePadding: margins(5px, 3px, 0px, 0px);\nstatisticsOverviewSecondValue: FlatLabel(boxLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(11px);\n\t}\n}\nstatisticsOverviewSubtext: FlatLabel(boxLabel) {\n\tminWidth: 152px;\n\tmaxHeight: 32px;\n\tstyle: statisticsHeaderDatesTextStyle;\n}\nstatisticsOverviewMidSkip: 50px;\nstatisticsOverviewRightSkip: 14px;\n\nstatisticsRecentPostRowHeight: 40px;\nstatisticsRecentPostButton: SettingsButton(defaultSettingsButton) {\n\theight: 56px;\n\tpadding: margins(7px, 0px, 24px, 0px);\n}\nstatisticsRecentPostIconSkip: 1px;\nstatisticsRecentPostShareIcon: icon {{ \"statistics/mini_stats_share\", windowSubTextFg }};\nstatisticsRecentPostReactionIcon: icon {{ \"statistics/mini_stats_like\", windowSubTextFg }};\nstatisticsRecentPostUserpic: UserpicButton(defaultUserpicButton) {\n\tsize: size(contactsPhotoSize, contactsPhotoSize);\n\tphotoSize: contactsPhotoSize;\n}\n\nstatisticsShowMoreButton: SettingsButton(defaultSettingsButton) {\n\ttextFg: lightButtonFg;\n\ttextFgOver: lightButtonFgOver;\n\n\tpadding: margins(70px, 10px, 22px, 8px);\n}\n\nstatisticsShowMoreButtonArrowPosition: point(29px, 13px);\nstatisticsShowMoreButtonArrowSize: 7px;\n\nstatisticsLimitsDividerPadding: margins(22px, -26px, 22px, 22px);\nstatisticsLimitsLinePadding: margins(0px, -2px, 0px, 0px);\n\nboostsLayerOverviewMargins: margins(0px, 12px, 0px, 4px);\nboostsOverviewValuePadding: margins(2px, 0px, 0px, 0px);\nboostsChartHeaderPadding: margins(2px, 1px, 0px, 1px);\n\nboostsListBox: PeerList(defaultPeerList) {\n\tpadding: margins(\n\t\t0px,\n\t\t7px,\n\t\t0px,\n\t\t7px);\n\titem: PeerListItem(defaultPeerListItem) {\n\t\theight: 52px;\n\t\tphotoPosition: point(18px, 4px);\n\t\tnamePosition: point(70px, 6px);\n\t\tstatusPosition: point(70px, 26px);\n\t\tphotoSize: 42px;\n\t}\n}\nboostsLinkSkip: 5px;\nboostsLinkFieldPadding: margins(22px, 7px, 22px, 12px);\nboostsButton: SettingsButton(defaultSettingsButton) {\n\ttextFg: lightButtonFg;\n\ttextFgOver: lightButtonFgOver;\n}\n\ngetBoostsButton: SettingsButton(reportReasonButton) {\n\ttextFg: lightButtonFg;\n\ttextFgOver: lightButtonFg;\n}\ngetBoostsButtonIcon: icon {{ \"menu/gift_premium\", lightButtonFg }};\n\nboostsListMiniIcon: icon{{ \"boosts/boost_mini2\", premiumButtonFg }};\nboostsListMiniIconPadding: margins(1px, 0px, 0px, 0px);\nboostsListMiniIconSkip: 1px;\nboostsListEntryIcon: IconEmoji {\n\ticon: boostsListMiniIcon;\n\tpadding: margins(6px, 2px, 0px, 0px);\n}\nboostsListBadgeTextPadding: margins(16px, 1px, 6px, 0px);\nboostsListBadgePadding: margins(4px, 1px, 4px, 0px);\nboostsListBadgeHeight: 16px;\n\nboostsListRightBadgeTextStyle: TextStyle(defaultTextStyle) {\n\tfont: font(12px semibold);\n}\nboostsListRightBadgeTextPadding: margins(22px, 1px, 8px, 0px);\nboostsListRightBadgePadding: margins(4px, 5px, 12px, 0px);\nboostsListRightBadgeHeight: 20px;\nboostsListGiftMiniIconPadding: margins(4px, 2px, 0px, 0px);\nboostsListGiftMiniIcon: icon{{ \"boosts/mini_gift\", historyPeer8UserpicBg2 }};\nboostsListGiveawayMiniIcon: icon{{ \"boosts/mini_giveaway\", historyPeer4UserpicBg2 }};\nboostsListUnclaimedIcon: icon{{ \"boosts/boost_unclaimed\", premiumButtonFg }};\nboostsListUnknownIcon: icon{{ \"boosts/boost_unknown\", premiumButtonFg }};\nboostsListCreditsIconSize: 13px;\n\nstatisticsCurrencyIcon: icon {{ \"statistics/mini_currency_graph\", windowSubTextFg }};\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/statistics_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Statistic {\n\nconstexpr auto kRulerLineAlpha = 0.06;\n\nstruct Limits final {\n\tfloat64 min = 0;\n\tfloat64 max = 0;\n};\n\nenum class ChartViewType {\n\tLinear,\n\tBar,\n\tStackBar,\n\tDoubleLinear,\n\tStackLinear,\n};\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/statistics_data_deserialize.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/statistics_data_deserialize.h\"\n\n#include \"base/debug_log.h\"\n#include \"data/data_statistics_chart.h\"\n#include \"statistics/statistics_types.h\"\n#include \"ui/text/format_values.h\" // kCreditsCurrency.\n\n#include <QtCore/QJsonArray>\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonObject>\n#include <QtCore/QJsonValue>\n\nnamespace Statistic {\n\nData::StatisticalChart StatisticalChartFromJSON(const QByteArray &json) {\n\tauto error = QJsonParseError{ 0, QJsonParseError::NoError };\n\tconst auto document = QJsonDocument::fromJson(json, &error);\n\tif (error.error != QJsonParseError::NoError || !document.isObject()) {\n\t\tLOG((\"API Error: Bad stats graph json received.\"));\n\t\treturn {};\n\t}\n\tconst auto root = document.object();\n\tconst auto columns = root.value(u\"columns\"_q).toArray();\n\tif (columns.empty()) {\n\t\tLOG((\"API Error: Empty columns list from stats graph received.\"));\n\t\treturn {};\n\t}\n\n\tconst auto hiddenLinesRaw = root.value(u\"hidden\"_q).toArray();\n\tconst auto hiddenLines = ranges::views::all(\n\t\thiddenLinesRaw\n\t) | ranges::views::transform([](const auto &q) {\n\t\treturn q.toString();\n\t}) | ranges::to_vector;\n\n\tauto result = Data::StatisticalChart();\n\n\t{\n\t\tconst auto tickFormatIt = root.constFind(u\"yTickFormatter\"_q);\n\t\tif (tickFormatIt != root.constEnd()) {\n\t\t\tconst auto tickFormat = tickFormatIt->toString();\n\t\t\tif (tickFormat.contains(u\"TON\"_q)) {\n\t\t\t\tresult.currency = Data::StatisticalCurrency::Ton;\n\t\t\t} else if (tickFormat.contains(Ui::kCreditsCurrency)) {\n\t\t\t\tresult.currency = Data::StatisticalCurrency::Credits;\n\t\t\t}\n\t\t}\n\t}\n\tauto columnIdCount = 0;\n\tfor (const auto &column : columns) {\n\t\tconst auto array = column.toArray();\n\t\tif (array.empty()) {\n\t\t\tLOG((\"API Error: Empty column from stats graph received.\"));\n\t\t\treturn {};\n\t\t}\n\t\tconst auto columnId = array.first().toString();\n\t\tif (columnId == u\"x\"_q) {\n\t\t\tconst auto length = array.size() - 1;\n\t\t\tresult.x.reserve(length);\n\t\t\tfor (auto i = 0; i < length; i++) {\n\t\t\t\tresult.x.push_back(array.at(i + 1).toDouble());\n\t\t\t}\n\t\t} else {\n\t\t\tauto line = Data::StatisticalChart::Line();\n\t\t\tconst auto length = array.size() - 1;\n\t\t\tline.id = (++columnIdCount);\n\t\t\tline.idString = columnId;\n\t\t\tline.isHiddenOnStart = ranges::contains(hiddenLines, columnId);\n\t\t\tline.y.resize(length);\n\t\t\tfor (auto i = 0; i < length; i++) {\n\t\t\t\tconst auto multiplier = 1;\n\t\t\t\tconst auto value = ChartValue(\n\t\t\t\t\t\tbase::SafeRound(array.at(i + 1).toDouble()))\n\t\t\t\t\t* multiplier;\n\t\t\t\tline.y[i] = value;\n\t\t\t\tif (value > line.maxValue) {\n\t\t\t\t\tline.maxValue = value;\n\t\t\t\t}\n\t\t\t\tif (value < line.minValue) {\n\t\t\t\t\tline.minValue = value;\n\t\t\t\t}\n\t\t\t}\n\t\t\tresult.lines.push_back(std::move(line));\n\t\t}\n\t\tif (result.x.size() > 1) {\n\t\t\tresult.timeStep = std::max(1., result.x[1] - result.x[0]);\n\t\t} else {\n\t\t\tconstexpr auto kOneDay = 3600 * 24 * 1000;\n\t\t\tresult.timeStep = kOneDay;\n\t\t}\n\t\tresult.measure();\n\t}\n\tif (result.maxValue == result.minValue) {\n\t\tif (result.minValue) {\n\t\t\tresult.minValue = 0;\n\t\t} else {\n\t\t\tresult.maxValue = 1;\n\t\t}\n\t}\n\n\t{\n\t\tconst auto subchart = root.value(u\"subchart\"_q).toObject();\n\t\tconst auto subchartShowIt = subchart.constFind(u\"show\"_q);\n\t\tif (subchartShowIt != subchart.constEnd()) {\n\t\t\tif (subchartShowIt->isBool()) {\n\t\t\t\tresult.isFooterHidden = !(subchartShowIt->toBool());\n\t\t\t}\n\t\t}\n\t\tconst auto defaultZoomIt = subchart.constFind(u\"defaultZoom\"_q);\n\t\tauto min = int(0);\n\t\tauto max = int(result.x.size() - 1);\n\t\tif (defaultZoomIt != subchart.constEnd()) {\n\t\t\tif (const auto array = defaultZoomIt->toArray(); !array.empty()) {\n\t\t\t\tconst auto minValue = array.first().toDouble();\n\t\t\t\tconst auto maxValue = array.last().toDouble();\n\t\t\t\tfor (auto i = 0; i < result.x.size(); i++) {\n\t\t\t\t\tif (result.x[i] == minValue) {\n\t\t\t\t\t\tmin = i;\n\t\t\t\t\t}\n\t\t\t\t\tif (result.x[i] == maxValue) {\n\t\t\t\t\t\tmax = i;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tresult.defaultZoomXIndex.min = std::min(min, max);\n\t\tresult.defaultZoomXIndex.max = std::max(min, max);\n\t}\n\t{\n\n\t\tconst auto percentageShowIt = root.constFind(u\"percentage\"_q);\n\t\tif (percentageShowIt != root.constEnd()) {\n\t\t\tif (percentageShowIt->isBool()) {\n\t\t\t\tresult.hasPercentages = (percentageShowIt->toBool());\n\t\t\t}\n\t\t}\n\t}\n\t{\n\t\tconst auto tooltipFormatIt = root.constFind(u\"xTooltipFormatter\"_q);\n\t\tif (tooltipFormatIt != root.constEnd()) {\n\t\t\tconst auto tooltipFormat = tooltipFormatIt->toString();\n\t\t\tresult.weekFormat = tooltipFormat.contains(u\"'week'\"_q);\n\t\t}\n\t}\n\n\tconst auto colors = root.value(u\"colors\"_q).toObject();\n\tconst auto names = root.value(u\"names\"_q).toObject();\n\n\tfor (auto &line : result.lines) {\n\t\tconst auto colorIt = colors.constFind(line.idString);\n\t\tif (colorIt != colors.constEnd() && (*colorIt).isString()) {\n\t\t\tstatic const auto RegExp = QRegularExpression(u\"(.*)(#.*)\"_q);\n\t\t\tconst auto match = RegExp.match(\n\t\t\t\tcolorIt->toString());\n\t\t\tif (match.hasMatch()) {\n\t\t\t\tline.colorKey = match.captured(1);\n\t\t\t\tline.color = QColor(match.captured(2));\n\t\t\t}\n\t\t}\n\t\tconst auto nameIt = names.constFind(line.idString);\n\t\tif (nameIt != names.constEnd() && (*nameIt).isString()) {\n\t\t\tline.name = nameIt->toString().replace('-', QChar(8212));\n\t\t}\n\t}\n\n\treturn result;\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/statistics_data_deserialize.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nstruct StatisticalChart;\n} // namespace Data\n\nclass QByteArray;\n\nnamespace Statistic {\n\n[[nodiscard]] Data::StatisticalChart StatisticalChartFromJSON(\n\tconst QByteArray &json);\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/statistics_format_values.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/statistics_format_values.h\"\n\n#include \"base/unixtime.h\"\n#include \"lang/lang_keys.h\"\n\n#include <QtCore/QLocale>\n\nnamespace Statistic {\n\nQString LangDayMonthYear(crl::time seconds) {\n\tconst auto date = base::unixtime::parse(seconds).date();\n\treturn tr::lng_stats_day_month_year(\n\t\ttr::now,\n\t\tlt_days_count,\n\t\tQString::number(date.day()),\n\t\tlt_month,\n\t\tLang::MonthSmall(date.month())(tr::now),\n\t\tlt_year,\n\t\tQString::number(date.year()));\n}\n\nQString LangDayMonth(crl::time seconds) {\n\tconst auto date = base::unixtime::parse(seconds).date();\n\treturn tr::lng_stats_day_month(\n\t\ttr::now,\n\t\tlt_days_count,\n\t\tQString::number(date.day()),\n\t\tlt_month,\n\t\tLang::MonthSmall(date.month())(tr::now));\n}\n\nQString LangDetailedDayMonth(crl::time seconds) {\n\tconst auto dateTime = base::unixtime::parse(seconds);\n\tif (dateTime.toUTC().time().hour() || dateTime.toUTC().time().minute()) {\n\t\tconstexpr auto kOneDay = 3600 * 24;\n\t\tif (seconds < kOneDay) {\n\t\t\treturn QLocale().toString(dateTime, QLocale::ShortFormat);\n\t\t}\n\t\treturn tr::lng_stats_weekday_day_month_time(\n\t\t\ttr::now,\n\t\t\tlt_day,\n\t\t\tLang::Weekday(dateTime.date().dayOfWeek())(tr::now),\n\t\t\tlt_days_count,\n\t\t\tQString::number(dateTime.date().day()),\n\t\t\tlt_month,\n\t\t\tLang::MonthSmall(dateTime.date().month())(tr::now),\n\t\t\tlt_time,\n\t\t\tQLocale().toString(dateTime.time(), QLocale::ShortFormat));\n\t} else {\n\t\treturn tr::lng_stats_weekday_day_month_year(\n\t\t\ttr::now,\n\t\t\tlt_day,\n\t\t\tLang::Weekday(dateTime.date().dayOfWeek())(tr::now),\n\t\t\tlt_days_count,\n\t\t\tQString::number(dateTime.date().day()),\n\t\t\tlt_month,\n\t\t\tLang::MonthSmall(dateTime.date().month())(tr::now),\n\t\t\tlt_year,\n\t\t\tQString::number(dateTime.date().year()));\n\t}\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/statistics_format_values.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Statistic {\n\n[[nodiscard]] QString LangDayMonthYear(crl::time seconds);\n[[nodiscard]] QString LangDayMonth(crl::time seconds);\n[[nodiscard]] QString LangDetailedDayMonth(crl::time seconds);\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/statistics_graphics.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/statistics_graphics.h\"\n\n#include \"data/data_statistics_chart.h\"\n#include \"ui/effects/credits_graphics.h\" // GenerateStars.\n#include \"ui/painter.h\"\n#include \"styles/style_basic.h\"\n#include \"styles/style_statistics.h\"\n\nnamespace Statistic {\n\nQImage ChartCurrencyIcon(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tstd::optional<QColor> color) {\n\tauto result = QImage();\n\tconst auto iconSize = st::statisticsCurrencyIcon.size();\n\tif (chartData.currency == Data::StatisticalCurrency::Ton) {\n\t\tresult = QImage(\n\t\t\ticonSize * style::DevicePixelRatio(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\t\tresult.fill(Qt::transparent);\n\t\t{\n\t\t\tauto p = Painter(&result);\n\t\t\tif (const auto w = iconSize.width(); w && color) {\n\t\t\t\tst::statisticsCurrencyIcon.paint(p, 0, 0, w, *color);\n\t\t\t} else {\n\t\t\t\tst::statisticsCurrencyIcon.paint(p, 0, 0, iconSize.width());\n\t\t\t}\n\t\t}\n\t} else if (chartData.currency == Data::StatisticalCurrency::Credits) {\n\t\treturn Ui::GenerateStars(iconSize.height(), 1);\n\t}\n\treturn result;\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/statistics_graphics.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nstruct StatisticalChart;\n} // namespace Data\n\nnamespace Statistic {\n\n[[nodiscard]] QImage ChartCurrencyIcon(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tstd::optional<QColor> color);\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/statistics_types.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Statistic {\n\nusing ChartValue = int64;\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/abstract_chart_view.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/statistics_common.h\"\n#include \"statistics/view/abstract_chart_view.h\"\n\n#include \"data/data_statistics_chart.h\"\n#include \"statistics/chart_lines_filter_controller.h\"\n#include \"statistics/statistics_types.h\"\n\nnamespace Statistic {\n\nbool CachedSelectedPoints::isSame(int x, const PaintContext &c) const {\n\treturn (lastXIndex == x)\n\t\t&& (lastHeightLimits.min == c.heightLimits.min)\n\t\t&& (lastHeightLimits.max == c.heightLimits.max)\n\t\t&& (lastXLimits.min == c.xPercentageLimits.min)\n\t\t&& (lastXLimits.max == c.xPercentageLimits.max);\n}\n\nDoubleLineRatios::DoubleLineRatios(bool isDouble) {\n\tfirst = second = (isDouble ? 0 : 1);\n}\n\nvoid DoubleLineRatios::init(const Data::StatisticalChart &chartData) {\n\tif (chartData.lines.size() != 2) {\n\t\tfirst = 1.;\n\t\tsecond = 1.;\n\t} else {\n\t\tconst auto firstMax = chartData.lines.front().maxValue;\n\t\tconst auto secondMax = chartData.lines.back().maxValue;\n\t\tif (firstMax > secondMax) {\n\t\t\tfirst = 1.;\n\t\t\tsecond = firstMax / float64(secondMax);\n\t\t} else {\n\t\t\tfirst = secondMax / float64(firstMax);\n\t\t\tsecond = 1.;\n\t\t}\n\t}\n}\n\nfloat64 DoubleLineRatios::ratio(int lineId) const {\n\treturn (lineId == 1) ? first : second;\n}\n\nvoid AbstractChartView::setUpdateCallback(Fn<void()> callback) {\n\t_updateCallback = std::move(callback);\n}\n\nvoid AbstractChartView::update() {\n\tif (_updateCallback) {\n\t\t_updateCallback();\n\t}\n}\n\nvoid AbstractChartView::setLinesFilterController(\n\t\tstd::shared_ptr<LinesFilterController> c) {\n\t_linesFilterController = std::move(c);\n}\n\nauto AbstractChartView::linesFilterController() const\n-> std::shared_ptr<LinesFilterController> {\n\treturn _linesFilterController;\n}\n\nAbstractChartView::HeightLimits DefaultHeightLimits(\n\t\tconst DoubleLineRatios &ratios,\n\t\tconst std::shared_ptr<LinesFilterController> &linesFilter,\n\t\tData::StatisticalChart &chartData,\n\t\tLimits xIndices) {\n\tauto minValue = std::numeric_limits<ChartValue>::max();\n\tauto maxValue = ChartValue(0);\n\n\tauto minValueFull = std::numeric_limits<ChartValue>::max();\n\tauto maxValueFull = ChartValue(0);\n\tfor (auto &l : chartData.lines) {\n\t\tif (!linesFilter->isEnabled(l.id)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto r = ratios.ratio(l.id);\n\t\tconst auto lineMax = l.segmentTree.rMaxQ(xIndices.min, xIndices.max);\n\t\tconst auto lineMin = l.segmentTree.rMinQ(xIndices.min, xIndices.max);\n\t\tmaxValue = std::max(ChartValue(lineMax * r), maxValue);\n\t\tminValue = std::min(ChartValue(lineMin * r), minValue);\n\n\t\tmaxValueFull = std::max(ChartValue(l.maxValue * r), maxValueFull);\n\t\tminValueFull = std::min(ChartValue(l.minValue * r), minValueFull);\n\t}\n\tif (maxValue == minValue) {\n\t\tmaxValue = chartData.maxValue;\n\t\tminValue = chartData.minValue;\n\t}\n\treturn {\n\t\t.full = Limits{ float64(minValueFull), float64(maxValueFull) },\n\t\t.ranged = Limits{ float64(minValue), float64(maxValue) },\n\t};\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/abstract_chart_view.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nstruct StatisticalChart;\n} // namespace Data\n\nnamespace Statistic {\n\nstruct Limits;\nclass LinesFilterController;\n\nstruct PaintContext final {\n\tconst Data::StatisticalChart &chartData;\n\tconst Limits xIndices;\n\tconst Limits xPercentageLimits;\n\tconst Limits heightLimits;\n\tconst QRect &rect;\n\tbool footer = false;\n};\n\nstruct CachedSelectedPoints final {\n\t[[nodiscard]] bool isSame(int x, const PaintContext &c) const;\n\n\tint lastXIndex = -1;\n\tLimits lastHeightLimits;\n\tLimits lastXLimits;\n\tbase::flat_map<int, QPointF> points;\n};\n\nclass DoubleLineRatios final : std::pair<float64, float64> {\npublic:\n\tDoubleLineRatios(bool isDouble);\n\n\toperator bool() const {\n\t\treturn first > 0;\n\t}\n\n\tvoid init(const Data::StatisticalChart &chartData);\n\t[[nodiscard]] float64 ratio(int lineId) const;\n};\n\nclass AbstractChartView {\npublic:\n\tvirtual ~AbstractChartView() = default;\n\n\tvirtual void paint(QPainter &p, const PaintContext &c) = 0;\n\n\tvirtual void paintSelectedXIndex(\n\t\tQPainter &p,\n\t\tconst PaintContext &c,\n\t\tint selectedXIndex,\n\t\tfloat64 progress) = 0;\n\n\t[[nodiscard]] virtual int findXIndexByPosition(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tconst Limits &xPercentageLimits,\n\t\tconst QRect &rect,\n\t\tfloat64 x) = 0;\n\n\tstruct HeightLimits final {\n\t\tLimits full;\n\t\tLimits ranged;\n\t};\n\n\t[[nodiscard]] virtual HeightLimits heightLimits(\n\t\tData::StatisticalChart &chartData,\n\t\tLimits xIndices) = 0;\n\n\tstruct LocalZoomResult final {\n\t\tbool hasZoom = false;\n\t\tLimits limitIndices;\n\t\tLimits range;\n\t};\n\n\tstruct LocalZoomArgs final {\n\t\tenum class Type {\n\t\t\tPrepare,\n\t\t\tSkipCalculation,\n\t\t\tCheckAvailability,\n\t\t\tProcess,\n\t\t\tSaveZoomFromFooter,\n\t\t};\n\t\tconst Data::StatisticalChart &chartData;\n\t\tType type;\n\t\tfloat64 progress = 0.;\n\t\tint xIndex = 0;\n\t};\n\n\tvirtual LocalZoomResult maybeLocalZoom(const LocalZoomArgs &args) {\n\t\treturn {};\n\t}\n\n\tvirtual void handleMouseMove(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tconst QRect &rect,\n\t\tconst QPoint &p) {\n\t}\n\n\tvoid setUpdateCallback(Fn<void()> callback);\n\tvoid update();\n\n\tvoid setLinesFilterController(std::shared_ptr<LinesFilterController> c);\n\nprotected:\n\tusing LinesFilterControllerPtr = std::shared_ptr<LinesFilterController>;\n\t[[nodiscard]] LinesFilterControllerPtr linesFilterController() const;\n\nprivate:\n\tLinesFilterControllerPtr _linesFilterController;\n\tFn<void()> _updateCallback;\n\n};\n\nAbstractChartView::HeightLimits DefaultHeightLimits(\n\tconst DoubleLineRatios &ratios,\n\tconst std::shared_ptr<LinesFilterController> &linesFilter,\n\tData::StatisticalChart &chartData,\n\tLimits xIndices);\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/bar_chart_view.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/view/bar_chart_view.h\"\n\n#include \"data/data_statistics_chart.h\"\n#include \"statistics/chart_lines_filter_controller.h\"\n#include \"statistics/view/stack_chart_common.h\"\n#include \"ui/effects/animation_value_f.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"styles/style_statistics.h\"\n\nnamespace Statistic {\n\nBarChartView::BarChartView(bool isStack)\n: _isStack(isStack)\n, _cachedLineRatios(false) {\n}\n\nBarChartView::~BarChartView() = default;\n\nvoid BarChartView::paint(QPainter &p, const PaintContext &c) {\n\tconstexpr auto kOffset = float64(2);\n\t_lastPaintedXIndices = {\n\t\tfloat64(std::max(0., c.xIndices.min - kOffset)),\n\t\tfloat64(std::min(\n\t\t\tfloat64(c.chartData.xPercentage.size() - 1),\n\t\t\tc.xIndices.max + kOffset)),\n\t};\n\n\tBarChartView::paintChartAndSelected(p, c);\n}\n\nvoid BarChartView::paintChartAndSelected(\n\t\tQPainter &p,\n\t\tconst PaintContext &c) {\n\tconst auto &[localStart, localEnd] = _lastPaintedXIndices;\n\tconst auto &[leftStart, w] = ComputeLeftStartAndStep(\n\t\tc.chartData,\n\t\tc.xPercentageLimits,\n\t\tc.rect,\n\t\tlocalStart);\n\n\tp.setClipRect(0, 0, c.rect.width() * 2, rect::bottom(c.rect));\n\n\tconst auto opacity = p.opacity();\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tauto bottoms = std::vector<float64>(\n\t\tlocalEnd - localStart + 1,\n\t\t-c.rect.y());\n\tauto selectedBottoms = std::vector<float64>();\n\tconst auto hasSelectedXIndex = _isStack\n\t\t&& !c.footer\n\t\t&& (_lastSelectedXIndex >= 0);\n\tif (hasSelectedXIndex) {\n\t\tselectedBottoms = std::vector<float64>(c.chartData.lines.size(), 0);\n\t\tconstexpr auto kSelectedAlpha = 0.5;\n\t\tp.setOpacity(\n\t\t\tanim::interpolateF(1.0, kSelectedAlpha, _lastSelectedXProgress));\n\t}\n\n\tfor (auto i = 0; i < c.chartData.lines.size(); i++) {\n\t\tconst auto &line = c.chartData.lines[i];\n\t\tauto path = QPainterPath();\n\t\tfor (auto x = localStart; x <= localEnd; x++) {\n\t\t\tif (line.y[x] <= 0 && _isStack) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto yPercentage = (line.y[x] - c.heightLimits.min)\n\t\t\t\t/ float64(c.heightLimits.max - c.heightLimits.min);\n\t\t\tconst auto yPoint = yPercentage\n\t\t\t\t* c.rect.height()\n\t\t\t\t* linesFilterController()->alpha(line.id);\n\n\t\t\tconst auto bottomIndex = x - localStart;\n\t\t\tconst auto column = QRectF(\n\t\t\t\tleftStart + (x - localStart) * w,\n\t\t\t\tc.rect.height() - bottoms[bottomIndex] - yPoint,\n\t\t\t\tw,\n\t\t\t\tyPoint);\n\t\t\tif (hasSelectedXIndex && (x == _lastSelectedXIndex)) {\n\t\t\t\tselectedBottoms[i] = column.y();\n\t\t\t}\n\t\t\tif (_isStack) {\n\t\t\t\tpath.addRect(column);\n\t\t\t\tbottoms[bottomIndex] += yPoint;\n\t\t\t} else {\n\t\t\t\tif (path.isEmpty()) {\n\t\t\t\t\tpath.moveTo(column.topLeft());\n\t\t\t\t} else {\n\t\t\t\t\tpath.lineTo(column.topLeft());\n\t\t\t\t}\n\t\t\t\tif (x == localEnd) {\n\t\t\t\t\tpath.lineTo(c.rect.width(), column.y());\n\t\t\t\t} else {\n\t\t\t\t\tpath.lineTo(rect::right(column), column.y());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (_isStack) {\n\t\t\tp.fillPath(path, line.color);\n\t\t} else {\n\t\t\tp.strokePath(path, line.color);\n\t\t}\n\t}\n\n\tfor (auto i = 0; i < selectedBottoms.size(); i++) {\n\t\tp.setOpacity(opacity);\n\t\tif (selectedBottoms[i] <= 0) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto &line = c.chartData.lines[i];\n\t\tconst auto yPercentage = 0.\n\t\t\t+ (line.y[_lastSelectedXIndex] - c.heightLimits.min)\n\t\t\t\t/ float64(c.heightLimits.max - c.heightLimits.min);\n\t\tconst auto yPoint = yPercentage\n\t\t\t* c.rect.height()\n\t\t\t* linesFilterController()->alpha(line.id);\n\n\t\tconst auto column = QRectF(\n\t\t\tleftStart + (_lastSelectedXIndex - localStart) * w,\n\t\t\tselectedBottoms[i],\n\t\t\tw,\n\t\t\tyPoint);\n\t\tp.fillRect(column, line.color);\n\t}\n\n\tp.setClipping(false);\n}\n\nvoid BarChartView::paintSelectedXIndex(\n\t\tQPainter &p,\n\t\tconst PaintContext &c,\n\t\tint selectedXIndex,\n\t\tfloat64 progress) {\n\tconst auto was = _lastSelectedXIndex;\n\t_lastSelectedXIndex = selectedXIndex;\n\t_lastSelectedXProgress = progress;\n\n\tif ((_lastSelectedXIndex < 0) && (was < 0)) {\n\t\treturn;\n\t}\n\n\tif (_isStack) {\n\t\tBarChartView::paintChartAndSelected(p, c);\n\t} else if (selectedXIndex >= 0) {\n\t\tconst auto linesFilter = linesFilterController();\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tauto o = ScopedPainterOpacity(p, progress);\n\t\tp.setBrush(st::boxBg);\n\t\tconst auto r = st::statisticsDetailsDotRadius;\n\t\tconst auto isSameToken = _selectedPoints.isSame(selectedXIndex, c);\n\t\tauto linePainted = false;\n\n\t\tconst auto &[localStart, localEnd] = _lastPaintedXIndices;\n\t\tconst auto &[leftStart, w] = ComputeLeftStartAndStep(\n\t\t\tc.chartData,\n\t\t\tc.xPercentageLimits,\n\t\t\tc.rect,\n\t\t\tlocalStart);\n\n\t\tfor (auto i = 0; i < c.chartData.lines.size(); i++) {\n\t\t\tconst auto &line = c.chartData.lines[i];\n\t\t\tconst auto lineAlpha = linesFilter->alpha(line.id);\n\t\t\tconst auto useCache = isSameToken\n\t\t\t\t|| (lineAlpha < 1. && !linesFilter->isEnabled(line.id));\n\t\t\tif (!useCache) {\n\t\t\t\t// Calculate.\n\t\t\t\tconst auto x = _lastSelectedXIndex;\n\t\t\t\tconst auto yPercentage = (line.y[x] - c.heightLimits.min)\n\t\t\t\t\t/ float64(c.heightLimits.max - c.heightLimits.min);\n\t\t\t\tconst auto yPoint = (1. - yPercentage) * c.rect.height();\n\n\t\t\t\tconst auto column = QRectF(\n\t\t\t\t\tleftStart + (x - localStart) * w,\n\t\t\t\t\tc.rect.height() - 0 - yPoint,\n\t\t\t\t\tw,\n\t\t\t\t\tyPoint);\n\t\t\t\tconst auto xPoint = column.left() + column.width() / 2.;\n\t\t\t\t_selectedPoints.points[line.id] = QPointF(xPoint, yPoint)\n\t\t\t\t\t+ c.rect.topLeft();\n\t\t\t}\n\n\t\t\tif (!linePainted && lineAlpha) {\n\t\t\t\t[[maybe_unused]] const auto o = ScopedPainterOpacity(\n\t\t\t\t\tp,\n\t\t\t\t\tp.opacity() * progress * kRulerLineAlpha);\n\t\t\t\tconst auto lineRect = QRectF(\n\t\t\t\t\tbegin(_selectedPoints.points)->second.x()\n\t\t\t\t\t\t- (st::lineWidth / 2.),\n\t\t\t\t\tc.rect.y(),\n\t\t\t\t\tst::lineWidth,\n\t\t\t\t\tc.rect.height());\n\t\t\t\tp.fillRect(lineRect, st::boxTextFg);\n\t\t\t\tlinePainted = true;\n\t\t\t}\n\n\t\t\t// Paint.\n\t\t\tauto o = ScopedPainterOpacity(p, lineAlpha * p.opacity());\n\t\t\tp.setPen(QPen(line.color, st::statisticsChartLineWidth));\n\t\t\tp.drawEllipse(_selectedPoints.points[line.id], r, r);\n\t\t}\n\t\t_selectedPoints.lastXIndex = selectedXIndex;\n\t\t_selectedPoints.lastHeightLimits = c.heightLimits;\n\t\t_selectedPoints.lastXLimits = c.xPercentageLimits;\n\t}\n}\n\nint BarChartView::findXIndexByPosition(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tconst Limits &xPercentageLimits,\n\t\tconst QRect &rect,\n\t\tfloat64 xPos) {\n\tif ((xPos < rect.x()) || (xPos > (rect.x() + rect.width()))) {\n\t\treturn _lastSelectedXIndex = -1;\n\t}\n\tconst auto &[localStart, localEnd] = _lastPaintedXIndices;\n\tconst auto &[leftStart, w] = ComputeLeftStartAndStep(\n\t\tchartData,\n\t\txPercentageLimits,\n\t\trect,\n\t\tlocalStart);\n\n\tfor (auto i = 0; i < chartData.lines.size(); i++) {\n\t\tfor (auto x = localStart; x <= localEnd; x++) {\n\t\t\tconst auto left = leftStart + (x - localStart) * w;\n\t\t\tif ((xPos >= left) && (xPos < (left + w))) {\n\t\t\t\treturn _lastSelectedXIndex = x;\n\t\t\t}\n\t\t}\n\t}\n\treturn _lastSelectedXIndex = -1;\n}\n\nAbstractChartView::HeightLimits BarChartView::heightLimits(\n\t\tData::StatisticalChart &chartData,\n\t\tLimits xIndices) {\n\tif (!_isStack) {\n\t\tif (!_cachedLineRatios) {\n\t\t\t_cachedLineRatios.init(chartData);\n\t\t}\n\n\t\treturn DefaultHeightLimits(\n\t\t\t_cachedLineRatios,\n\t\t\tlinesFilterController(),\n\t\t\tchartData,\n\t\t\txIndices);\n\t}\n\t_cachedHeightLimits = {};\n\tif (_cachedHeightLimits.ySum.empty()) {\n\t\t_cachedHeightLimits.ySum.reserve(chartData.x.size());\n\n\t\tauto maxValueFull = ChartValue(0);\n\t\tfor (auto i = 0; i < chartData.x.size(); i++) {\n\t\t\tauto sum = ChartValue(0);\n\t\t\tfor (const auto &line : chartData.lines) {\n\t\t\t\tif (linesFilterController()->isEnabled(line.id)) {\n\t\t\t\t\tsum += line.y[i];\n\t\t\t\t}\n\t\t\t}\n\t\t\t_cachedHeightLimits.ySum.push_back(sum);\n\t\t\tmaxValueFull = std::max(sum, maxValueFull);\n\t\t}\n\n\t\t_cachedHeightLimits.ySumSegmentTree = SegmentTree(\n\t\t\t_cachedHeightLimits.ySum);\n\t\t_cachedHeightLimits.full = { 0., float64(maxValueFull) };\n\t}\n\tconst auto max = std::max(\n\t\t_cachedHeightLimits.ySumSegmentTree.rMaxQ(\n\t\t\txIndices.min,\n\t\t\txIndices.max),\n\t\tChartValue(1));\n\treturn {\n\t\t.full = _cachedHeightLimits.full,\n\t\t.ranged = { 0., float64(max) },\n\t};\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/bar_chart_view.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"statistics/segment_tree.h\"\n#include \"statistics/statistics_common.h\"\n#include \"statistics/statistics_types.h\"\n#include \"statistics/view/abstract_chart_view.h\"\n#include \"ui/effects/animation_value.h\"\n\nnamespace Data {\nstruct StatisticalChart;\n} // namespace Data\n\nnamespace Statistic {\n\nstruct Limits;\n\nclass BarChartView final : public AbstractChartView {\npublic:\n\tBarChartView(bool isStack);\n\t~BarChartView() override final;\n\n\tvoid paint(QPainter &p, const PaintContext &c) override;\n\n\tvoid paintSelectedXIndex(\n\t\tQPainter &p,\n\t\tconst PaintContext &c,\n\t\tint selectedXIndex,\n\t\tfloat64 progress) override;\n\n\tint findXIndexByPosition(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tconst Limits &xPercentageLimits,\n\t\tconst QRect &rect,\n\t\tfloat64 x) override;\n\n\t[[nodiscard]] HeightLimits heightLimits(\n\t\tData::StatisticalChart &chartData,\n\t\tLimits xIndices) override;\n\nprivate:\n\tvoid paintChartAndSelected(QPainter &p, const PaintContext &c);\n\n\tstruct {\n\t\tLimits full;\n\t\tstd::vector<ChartValue> ySum;\n\t\tSegmentTree ySumSegmentTree;\n\t} _cachedHeightLimits;\n\n\tconst bool _isStack;\n\tDoubleLineRatios _cachedLineRatios; // Non-stack.\n\tLimits _lastPaintedXIndices;\n\tint _lastSelectedXIndex = -1;\n\tfloat64 _lastSelectedXProgress = 0;\n\n\tCachedSelectedPoints _selectedPoints; // Non-stack.\n\n};\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/view/chart_rulers_view.h\"\n\n#include \"info/channel_statistics/earn/earn_format.h\"\n#include \"lang/lang_keys.h\"\n#include \"statistics/chart_lines_filter_controller.h\"\n#include \"statistics/statistics_common.h\"\n#include \"statistics/statistics_graphics.h\"\n#include \"styles/style_basic.h\"\n#include \"styles/style_statistics.h\"\n\n#include <QtCore/QLocale>\n\nnamespace Statistic {\nnamespace {\n\n[[nodiscard]] QString FormatF(float64 absoluteValue) {\n\tstatic constexpr auto kTooMuch = int(10'000);\n\tstatic constexpr auto kTooSmall = 1e-9;\n\treturn (std::abs(absoluteValue) <= kTooSmall)\n\t\t? u\"0\"_q\n\t\t: (absoluteValue >= kTooMuch)\n\t\t? Lang::FormatCountToShort(absoluteValue).string\n\t\t: QLocale().toString(absoluteValue);\n}\n\n} // namespace\n\nChartRulersView::ChartRulersView() = default;\n\nvoid ChartRulersView::setChartData(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tChartViewType type,\n\t\tstd::shared_ptr<LinesFilterController> linesFilter) {\n\t_rulers.clear();\n\t_isDouble = (type == ChartViewType::DoubleLinear)\n\t\t|| chartData.currencyRate;\n\tif (chartData.currencyRate) {\n\t\t_currencyIcon = ChartCurrencyIcon(chartData, {});\n\t\tif (chartData.currency == Data::StatisticalCurrency::Ton) {\n\t\t\t_leftCustomCaption = [=](float64 value) {\n\t\t\t\treturn FormatF(value / float64(kOneStarInNano));\n\t\t\t};\n\t\t\t_rightCustomCaption = [=, rate = chartData.currencyRate](float64 v) {\n\t\t\t\treturn Info::ChannelEarn::ToUsd(v / float64(kOneStarInNano), rate, 0);\n\t\t\t};\n\t\t} else {\n\t\t\t_leftCustomCaption = [=](float64 value) {\n\t\t\t\treturn FormatF(value);\n\t\t\t};\n\t\t\t_rightCustomCaption = [=, rate = chartData.currencyRate](float64 v) {\n\t\t\t\treturn Info::ChannelEarn::ToUsd(v, rate, 0);\n\t\t\t};\n\t\t}\n\t\t_rightPen = QPen(st::windowSubTextFg);\n\t}\n\tif (_isDouble && (chartData.lines.size() == 2)) {\n\t\t_linesFilter = std::move(linesFilter);\n\t\t_leftPen = QPen(chartData.lines.front().color);\n\t\t_rightPen = QPen(chartData.lines.back().color);\n\t\t_leftLineId = chartData.lines.front().id;\n\t\t_rightLineId = chartData.lines.back().id;\n\n\t\tconst auto firstMax = chartData.lines.front().maxValue;\n\t\tconst auto secondMax = chartData.lines.back().maxValue;\n\t\tif (firstMax > secondMax) {\n\t\t\t_isLeftLineScaled = false;\n\t\t\t_scaledLineRatio = firstMax / float64(secondMax);\n\t\t} else {\n\t\t\t_isLeftLineScaled = true;\n\t\t\t_scaledLineRatio = secondMax / float64(firstMax);\n\t\t}\n\t}\n}\n\nvoid ChartRulersView::paintRulers(\n\t\tQPainter &p,\n\t\tconst QRect &r) {\n\tconst auto alpha = p.opacity();\n\tfor (auto &ruler : _rulers) {\n\t\tp.setOpacity(alpha * ruler.alpha * kRulerLineAlpha);\n\t\tfor (const auto &line : ruler.lines) {\n\t\t\tconst auto lineRect = QRect(\n\t\t\t\t0,\n\t\t\t\tr.y() + r.height() * line.relativeValue,\n\t\t\t\tr.x() + r.width(),\n\t\t\t\tst::lineWidth);\n\t\t\tp.fillRect(lineRect, st::boxTextFg);\n\t\t}\n\t}\n\tp.setOpacity(alpha);\n}\n\nvoid ChartRulersView::paintCaptionsToRulers(\n\t\tQPainter &p,\n\t\tconst QRect &r) {\n\tconst auto offset = r.y() - st::statisticsChartRulerCaptionSkip;\n\tp.setFont(st::statisticsDetailsBottomCaptionStyle.font);\n\tconst auto alpha = p.opacity();\n\tfor (auto &ruler : _rulers) {\n\t\tconst auto rulerAlpha = alpha * ruler.alpha;\n\t\tp.setOpacity(rulerAlpha);\n\t\tconst auto left = _currencyIcon.isNull()\n\t\t\t ? 0\n\t\t\t : _currencyIcon.width() / style::DevicePixelRatio();\n\t\tfor (const auto &line : ruler.lines) {\n\t\t\tconst auto y = offset + r.height() * line.relativeValue;\n\t\t\tconst auto hasLinesFilter = _isDouble && _linesFilter;\n\t\t\tif (hasLinesFilter) {\n\t\t\t\tp.setPen(_leftPen);\n\t\t\t\tp.setOpacity(rulerAlpha * _linesFilter->alpha(_leftLineId));\n\t\t\t} else {\n\t\t\t\tp.setPen(st::windowSubTextFg);\n\t\t\t}\n\t\t\tif (!_currencyIcon.isNull()) {\n\t\t\t\tconst auto iconTop = y\n\t\t\t\t\t- _currencyIcon.height() / style::DevicePixelRatio()\n\t\t\t\t\t+ st::statisticsChartRulerCaptionSkip;\n\t\t\t\tp.drawImage(0, iconTop, _currencyIcon);\n\t\t\t}\n\t\t\tp.drawText(\n\t\t\t\tleft,\n\t\t\t\ty,\n\t\t\t\t(!_isDouble)\n\t\t\t\t\t? line.caption\n\t\t\t\t\t: _isLeftLineScaled\n\t\t\t\t\t? line.scaledLineCaption\n\t\t\t\t\t: line.caption);\n\t\t\tif (hasLinesFilter || _rightCustomCaption) {\n\t\t\t\tif (_linesFilter) {\n\t\t\t\t\tp.setOpacity(rulerAlpha * _linesFilter->alpha(_rightLineId));\n\t\t\t\t}\n\t\t\t\tp.setPen(_rightPen);\n\t\t\t\tp.drawText(\n\t\t\t\t\tr.width() - line.rightCaptionWidth,\n\t\t\t\t\ty,\n\t\t\t\t\t_isLeftLineScaled\n\t\t\t\t\t\t? line.caption\n\t\t\t\t\t\t: line.scaledLineCaption);\n\t\t\t}\n\t\t}\n\t}\n\tp.setOpacity(alpha);\n}\n\nvoid ChartRulersView::computeRelative(\n\t\tint newMaxHeight,\n\t\tint newMinHeight) {\n\tfor (auto &ruler : _rulers) {\n\t\truler.computeRelative(newMaxHeight, newMinHeight);\n\t}\n}\n\nvoid ChartRulersView::setAlpha(float64 value) {\n\tfor (auto &ruler : _rulers) {\n\t\truler.alpha = ruler.fixedAlpha * (1. - value);\n\t}\n\t_rulers.back().alpha = value;\n\tif (value == 1.) {\n\t\twhile (_rulers.size() > 1) {\n\t\t\tconst auto startIt = begin(_rulers);\n\t\t\tif (!startIt->alpha) {\n\t\t\t\t_rulers.erase(startIt);\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ChartRulersView::add(Limits newHeight, bool animated) {\n\tauto newLinesData = ChartRulersData(\n\t\tnewHeight.max,\n\t\tnewHeight.min,\n\t\ttrue,\n\t\t_isDouble ? _scaledLineRatio : 0.,\n\t\t_leftCustomCaption,\n\t\t_rightCustomCaption);\n\tif (_isDouble) {\n\t\tconst auto &font = st::statisticsDetailsBottomCaptionStyle.font;\n\t\tfor (auto &line : newLinesData.lines) {\n\t\t\tline.rightCaptionWidth = font->width(_isLeftLineScaled\n\t\t\t\t? line.caption\n\t\t\t\t: line.scaledLineCaption);\n\t\t}\n\t}\n\tif (!animated) {\n\t\t_rulers.clear();\n\t}\n\tfor (auto &ruler : _rulers) {\n\t\truler.fixedAlpha = ruler.alpha;\n\t}\n\t_rulers.push_back(newLinesData);\n\tif (!animated) {\n\t\t_rulers.back().alpha = 1.;\n\t}\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/chart_rulers_view.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"statistics/chart_rulers_data.h\"\n\nnamespace Data {\nstruct StatisticalChart;\n} // namespace Data\n\nnamespace Statistic {\n\nenum class ChartViewType;\nstruct Limits;\nclass LinesFilterController;\n\nstruct ChartRulersView final {\npublic:\n\tChartRulersView();\n\n\tvoid setChartData(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tChartViewType type,\n\t\tstd::shared_ptr<LinesFilterController> linesFilter);\n\n\tvoid paintRulers(QPainter &p, const QRect &r);\n\n\tvoid paintCaptionsToRulers(QPainter &p, const QRect &r);\n\n\tvoid computeRelative(int newMaxHeight, int newMinHeight);\n\tvoid setAlpha(float64 value);\n\tvoid add(Limits newHeight, bool animated);\n\nprivate:\n\tbool _isDouble = false;\n\tQPen _leftPen;\n\tQPen _rightPen;\n\tint _leftLineId = 0;\n\tint _rightLineId = 0;\n\tQImage _currencyIcon;\n\n\tFn<QString(float64)> _leftCustomCaption = nullptr;\n\tFn<QString(float64)> _rightCustomCaption = nullptr;\n\n\tstd::vector<ChartRulersData> _rulers;\n\n\tstd::shared_ptr<LinesFilterController> _linesFilter;\n\n\tfloat64 _scaledLineRatio = 0.;\n\tbool _isLeftLineScaled = false;\n\n};\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/chart_view_factory.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/view/chart_view_factory.h\"\n\n#include \"statistics/statistics_common.h\"\n#include \"statistics/view/linear_chart_view.h\"\n#include \"statistics/view/bar_chart_view.h\"\n#include \"statistics/view/stack_linear_chart_view.h\"\n\nnamespace Statistic {\n\nstd::unique_ptr<AbstractChartView> CreateChartView(ChartViewType type) {\n\tswitch (type) {\n\tcase ChartViewType::Linear: {\n\t\treturn std::make_unique<LinearChartView>(false);\n\t} break;\n\tcase ChartViewType::Bar: {\n\t\treturn std::make_unique<BarChartView>(false);\n\t} break;\n\tcase ChartViewType::StackBar: {\n\t\treturn std::make_unique<BarChartView>(true);\n\t} break;\n\tcase ChartViewType::DoubleLinear: {\n\t\treturn std::make_unique<LinearChartView>(true);\n\t} break;\n\tcase ChartViewType::StackLinear: {\n\t\treturn std::make_unique<StackLinearChartView>();\n\t} break;\n\tdefault: Unexpected(\"Type in Statistic::CreateChartView.\");\n\t}\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/chart_view_factory.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Statistic {\n\nclass AbstractChartView;\nenum class ChartViewType;\n\n[[nodiscard]] std::unique_ptr<AbstractChartView> CreateChartView(\n\tChartViewType type);\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/linear_chart_view.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/view/linear_chart_view.h\"\n\n#include \"data/data_statistics_chart.h\"\n#include \"statistics/chart_lines_filter_controller.h\"\n#include \"statistics/statistics_common.h\"\n#include \"ui/effects/animation_value_f.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_statistics.h\"\n\nnamespace Statistic {\nnamespace {\n\nvoid PaintChartLine(\n\t\tQPainter &p,\n\t\tint lineIndex,\n\t\tconst PaintContext &c,\n\t\tconst DoubleLineRatios &ratios) {\n\tconst auto &line = c.chartData.lines[lineIndex];\n\n\tauto chartPoints = QPolygonF();\n\n\tconstexpr auto kOffset = float64(2);\n\tconst auto localStart = int(std::max(0., c.xIndices.min - kOffset));\n\tconst auto localEnd = int(std::min(\n\t\tfloat64(c.chartData.xPercentage.size() - 1),\n\t\tc.xIndices.max + kOffset));\n\n\tconst auto ratio = ratios.ratio(line.id);\n\n\tfor (auto i = localStart; i <= localEnd; i++) {\n\t\tif (line.y[i] < 0) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto xPoint = c.rect.width()\n\t\t\t* ((c.chartData.xPercentage[i] - c.xPercentageLimits.min)\n\t\t\t\t/ (c.xPercentageLimits.max - c.xPercentageLimits.min));\n\t\tconst auto yPercentage = (line.y[i] * ratio - c.heightLimits.min)\n\t\t\t/ float64(c.heightLimits.max - c.heightLimits.min);\n\t\tconst auto yPoint = (1. - yPercentage) * c.rect.height();\n\t\tchartPoints << QPointF(xPoint, yPoint);\n\t}\n\tp.setPen(QPen(\n\t\tline.color,\n\t\tc.footer ? st::lineWidth : st::statisticsChartLineWidth));\n\tp.setBrush(Qt::NoBrush);\n\tp.drawPolyline(chartPoints);\n}\n\n} // namespace\n\nLinearChartView::LinearChartView(bool isDouble)\n: _cachedLineRatios(isDouble) {\n}\n\nLinearChartView::~LinearChartView() = default;\n\nvoid LinearChartView::paint(QPainter &p, const PaintContext &c) {\n\tconst auto cacheToken = LinearChartView::CacheToken(\n\t\tc.xIndices,\n\t\tc.xPercentageLimits,\n\t\tc.heightLimits,\n\t\tc.rect.size());\n\n\tconst auto opacity = p.opacity();\n\tconst auto linesFilter = linesFilterController();\n\tconst auto imageSize = c.rect.size() * style::DevicePixelRatio();\n\tconst auto cacheScale = 1. / style::DevicePixelRatio();\n\tauto &caches = (c.footer ? _footerCaches : _mainCaches);\n\n\tfor (auto i = 0; i < c.chartData.lines.size(); i++) {\n\t\tconst auto &line = c.chartData.lines[i];\n\t\tp.setOpacity(linesFilter->alpha(line.id));\n\t\tif (!p.opacity()) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto &cache = caches[line.id];\n\n\t\tconst auto isSameToken = (cache.lastToken == cacheToken);\n\t\tif ((isSameToken && cache.hq)\n\t\t\t|| (p.opacity() < 1. && !linesFilter->isEnabled(line.id))) {\n\t\t\tp.drawImage(c.rect.topLeft(), cache.image);\n\t\t\tcontinue;\n\t\t}\n\t\tcache.hq = isSameToken;\n\t\tauto image = QImage();\n\t\timage = QImage(\n\t\t\timageSize * (isSameToken ? 1. : cacheScale),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\t\timage.fill(Qt::transparent);\n\t\t{\n\t\t\tauto imagePainter = QPainter(&image);\n\t\t\tauto hq = PainterHighQualityEnabler(imagePainter);\n\t\t\tif (!isSameToken) {\n\t\t\t\timagePainter.scale(cacheScale, cacheScale);\n\t\t\t}\n\n\t\t\tPaintChartLine(imagePainter, i, c, _cachedLineRatios);\n\t\t}\n\n\t\tif (!isSameToken) {\n\t\t\timage = image.scaled(\n\t\t\t\timageSize,\n\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\tQt::FastTransformation);\n\t\t}\n\t\tp.drawImage(c.rect.topLeft(), image);\n\t\tcache.lastToken = cacheToken;\n\t\tcache.image = std::move(image);\n\t}\n\tp.setOpacity(opacity);\n}\n\nvoid LinearChartView::paintSelectedXIndex(\n\t\tQPainter &p,\n\t\tconst PaintContext &c,\n\t\tint selectedXIndex,\n\t\tfloat64 progress) {\n\tif (selectedXIndex < 0) {\n\t\treturn;\n\t}\n\tconst auto linesFilter = linesFilterController();\n\tauto hq = PainterHighQualityEnabler(p);\n\tauto o = ScopedPainterOpacity(p, progress);\n\tp.setBrush(st::boxBg);\n\tconst auto r = st::statisticsDetailsDotRadius;\n\tconst auto i = selectedXIndex;\n\tconst auto isSameToken = _selectedPoints.isSame(selectedXIndex, c);\n\tauto linePainted = false;\n\tfor (const auto &line : c.chartData.lines) {\n\t\tconst auto lineAlpha = linesFilter->alpha(line.id);\n\t\tconst auto useCache = isSameToken\n\t\t\t|| (lineAlpha < 1. && !linesFilter->isEnabled(line.id));\n\t\tif (!useCache) {\n\t\t\t// Calculate.\n\t\t\tconst auto r = _cachedLineRatios.ratio(line.id);\n\t\t\tconst auto xPoint = c.rect.width()\n\t\t\t\t* ((c.chartData.xPercentage[i] - c.xPercentageLimits.min)\n\t\t\t\t\t/ (c.xPercentageLimits.max - c.xPercentageLimits.min));\n\t\t\tconst auto yPercentage = (line.y[i] * r - c.heightLimits.min)\n\t\t\t\t/ float64(c.heightLimits.max - c.heightLimits.min);\n\t\t\tconst auto yPoint = (1. - yPercentage) * c.rect.height();\n\t\t\t_selectedPoints.points[line.id] = QPointF(xPoint, yPoint)\n\t\t\t\t+ c.rect.topLeft();\n\t\t}\n\n\t\tif (!linePainted && lineAlpha) {\n\t\t\t[[maybe_unused]] const auto o = ScopedPainterOpacity(\n\t\t\t\tp,\n\t\t\t\tp.opacity() * progress * kRulerLineAlpha);\n\t\t\tconst auto lineRect = QRectF(\n\t\t\t\tbegin(_selectedPoints.points)->second.x()\n\t\t\t\t\t- (st::lineWidth / 2.),\n\t\t\t\tc.rect.y(),\n\t\t\t\tst::lineWidth,\n\t\t\t\tc.rect.height());\n\t\t\tp.fillRect(lineRect, st::boxTextFg);\n\t\t\tlinePainted = true;\n\t\t}\n\n\t\t// Paint.\n\t\tauto o = ScopedPainterOpacity(p, lineAlpha * p.opacity());\n\t\tp.setPen(QPen(line.color, st::statisticsChartLineWidth));\n\t\tp.drawEllipse(_selectedPoints.points[line.id], r, r);\n\t}\n\t_selectedPoints.lastXIndex = selectedXIndex;\n\t_selectedPoints.lastHeightLimits = c.heightLimits;\n\t_selectedPoints.lastXLimits = c.xPercentageLimits;\n}\n\nint LinearChartView::findXIndexByPosition(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tconst Limits &xPercentageLimits,\n\t\tconst QRect &rect,\n\t\tfloat64 x) {\n\tif ((x < rect.x()) || (x > (rect.x() + rect.width()))) {\n\t\treturn -1;\n\t}\n\tconst auto pointerRatio = std::clamp(\n\t\t(x - rect.x()) / rect.width(),\n\t\t0.,\n\t\t1.);\n\tconst auto rawXPercentage = anim::interpolateF(\n\t\txPercentageLimits.min,\n\t\txPercentageLimits.max,\n\t\tpointerRatio);\n\tconst auto it = ranges::lower_bound(\n\t\tchartData.xPercentage,\n\t\trawXPercentage);\n\tconst auto left = rawXPercentage - (*(it - 1));\n\tconst auto right = (*it) - rawXPercentage;\n\tconst auto nearest = ((right) > (left)) ? (it - 1) : it;\n\tconst auto resultXPercentageIt = ((*nearest) > xPercentageLimits.max)\n\t\t? (nearest - 1)\n\t\t: ((*nearest) < xPercentageLimits.min)\n\t\t? (nearest + 1)\n\t\t: nearest;\n\tif (resultXPercentageIt == end(chartData.xPercentage)) {\n\t\treturn chartData.xPercentage.size() - 1;\n\t}\n\treturn std::distance(begin(chartData.xPercentage), resultXPercentageIt);\n}\n\nAbstractChartView::HeightLimits LinearChartView::heightLimits(\n\t\tData::StatisticalChart &chartData,\n\t\tLimits xIndices) {\n\tif (!_cachedLineRatios) {\n\t\t_cachedLineRatios.init(chartData);\n\t}\n\n\treturn DefaultHeightLimits(\n\t\t_cachedLineRatios,\n\t\tlinesFilterController(),\n\t\tchartData,\n\t\txIndices);\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/linear_chart_view.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"statistics/statistics_common.h\"\n#include \"statistics/view/abstract_chart_view.h\"\n\nnamespace Data {\nstruct StatisticalChart;\n} // namespace Data\n\nnamespace Statistic {\n\nstruct Limits;\n\nclass LinearChartView final : public AbstractChartView {\npublic:\n\tLinearChartView(bool isDouble);\n\t~LinearChartView() override final;\n\n\tvoid paint(QPainter &p, const PaintContext &c) override;\n\n\tvoid paintSelectedXIndex(\n\t\tQPainter &p,\n\t\tconst PaintContext &c,\n\t\tint selectedXIndex,\n\t\tfloat64 progress) override;\n\n\tint findXIndexByPosition(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tconst Limits &xPercentageLimits,\n\t\tconst QRect &rect,\n\t\tfloat64 x) override;\n\n\t[[nodiscard]] HeightLimits heightLimits(\n\t\tData::StatisticalChart &chartData,\n\t\tLimits xIndices) override;\n\nprivate:\n\tDoubleLineRatios _cachedLineRatios;\n\n\t[[nodiscard]] float64 lineRatio() const;\n\n\tstruct CacheToken final {\n\t\texplicit CacheToken() = default;\n\t\texplicit CacheToken(\n\t\t\tLimits xIndices,\n\t\t\tLimits xPercentageLimits,\n\t\t\tLimits heightLimits,\n\t\t\tQSize rectSize)\n\t\t: xIndices(std::move(xIndices))\n\t\t, xPercentageLimits(std::move(xPercentageLimits))\n\t\t, heightLimits(std::move(heightLimits))\n\t\t, rectSize(std::move(rectSize)) {\n\t\t}\n\n\t\tbool operator==(const CacheToken &other) const {\n\t\t\treturn (rectSize == other.rectSize)\n\t\t\t\t&& (xIndices.min == other.xIndices.min)\n\t\t\t\t&& (xIndices.max == other.xIndices.max)\n\t\t\t\t&& (xPercentageLimits.min == other.xPercentageLimits.min)\n\t\t\t\t&& (xPercentageLimits.max == other.xPercentageLimits.max)\n\t\t\t\t&& (heightLimits.min == other.heightLimits.min)\n\t\t\t\t&& (heightLimits.max == other.heightLimits.max);\n\t\t}\n\n\t\tbool operator!=(const CacheToken &other) const {\n\t\t\treturn !(*this == other);\n\t\t}\n\n\t\tLimits xIndices;\n\t\tLimits xPercentageLimits;\n\t\tLimits heightLimits;\n\t\tQSize rectSize;\n\t};\n\n\tstruct Cache final {\n\t\tQImage image;\n\t\tCacheToken lastToken;\n\t\tbool hq = false;\n\t};\n\n\tbase::flat_map<int, Cache> _mainCaches;\n\tbase::flat_map<int, Cache> _footerCaches;\n\n\tCachedSelectedPoints _selectedPoints;\n\n};\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/stack_chart_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/view/stack_chart_common.h\"\n\n#include \"data/data_statistics_chart.h\"\n#include \"statistics/statistics_common.h\"\n#include \"ui/effects/animation_value_f.h\"\n\nnamespace Statistic {\n\nLeftStartAndStep ComputeLeftStartAndStep(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tconst Limits &xPercentageLimits,\n\t\tconst QRect &rect,\n\t\tfloat64 xIndexStart) {\n\tconst auto fullWidth = rect.width()\n\t\t/ (xPercentageLimits.max - xPercentageLimits.min);\n\tconst auto offset = fullWidth * xPercentageLimits.min;\n\tconst auto p = (chartData.xPercentage.size() < 2)\n\t\t? 1.\n\t\t: chartData.xPercentage[1] * fullWidth;\n\tconst auto w = chartData.xPercentage[1] * (fullWidth - p);\n\tconst auto leftStart = rect.x()\n\t\t+ chartData.xPercentage[xIndexStart] * (fullWidth - p)\n\t\t- offset;\n\treturn { leftStart, w };\n}\n\nLimits FindStackXIndicesFromRawXPercentages(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tconst Limits &rawXPercentageLimits,\n\t\tconst Limits &zoomedInLimitXIndices) {\n\tconst auto zoomLimit = Limits{\n\t\tchartData.xPercentage[zoomedInLimitXIndices.min],\n\t\tchartData.xPercentage[zoomedInLimitXIndices.max],\n\t};\n\t// Due to a specificity of the stack chart plotting,\n\t// the right edge has a special offset to the left.\n\t// This reduces the number of displayed points by 1,\n\t// but allows the last point to be displayed.\n\tconst auto offset = (zoomLimit.max == 1.) ? 0 : -1;\n\tconst auto rightShrink = (rawXPercentageLimits.max == 1.)\n\t\t? ((zoomLimit.max == 1.) ? 0 : 1)\n\t\t: 0;\n\tconst auto n = chartData.xPercentage.size();\n\tauto minIt = -1;\n\tauto maxIt = n;\n\tconst auto zoomedIn = Limits{\n\t\tanim::interpolateF(\n\t\t\tzoomLimit.min,\n\t\t\tzoomLimit.max,\n\t\t\trawXPercentageLimits.min),\n\t\tanim::interpolateF(\n\t\t\tzoomLimit.min,\n\t\t\tzoomLimit.max,\n\t\t\trawXPercentageLimits.max),\n\t};\n\tfor (auto i = int(0); i < n; i++) {\n\t\tif (minIt < 0) {\n\t\t\tif (chartData.xPercentage[i] > zoomedIn.min) {\n\t\t\t\tminIt = i;\n\t\t\t}\n\t\t}\n\t\tif (maxIt >= n) {\n\t\t\tif (chartData.xPercentage[i] > zoomedIn.max) {\n\t\t\t\tmaxIt = i;\n\t\t\t}\n\t\t}\n\t}\n\treturn {\n\t\t.min = std::clamp(\n\t\t\tfloat64(minIt + offset),\n\t\t\tzoomedInLimitXIndices.min,\n\t\t\tzoomedInLimitXIndices.max - rightShrink),\n\t\t.max = std::clamp(\n\t\t\tfloat64(maxIt + offset),\n\t\t\tzoomedInLimitXIndices.min,\n\t\t\tzoomedInLimitXIndices.max - rightShrink),\n\t};\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/stack_chart_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nstruct StatisticalChart;\n} // namespace Data\n\nnamespace Statistic {\n\nstruct Limits;\n\nstruct LeftStartAndStep final {\n\tfloat64 start = 0.;\n\tfloat64 step = 0.;\n};\n\n[[nodiscard]] LeftStartAndStep ComputeLeftStartAndStep(\n\tconst Data::StatisticalChart &chartData,\n\tconst Limits &xPercentageLimits,\n\tconst QRect &rect,\n\tfloat64 xIndexStart);\n\n[[nodiscard]] Limits FindStackXIndicesFromRawXPercentages(\n\tconst Data::StatisticalChart &chartData,\n\tconst Limits &rawXPercentageLimits,\n\tconst Limits &zoomedInLimitXIndices);\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/stack_linear_chart_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/view/stack_linear_chart_common.h\"\n\n#include \"data/data_statistics_chart.h\"\n#include \"statistics/chart_lines_filter_controller.h\"\n#include \"statistics/statistics_common.h\"\n\nnamespace Statistic {\n\nPiePartData PiePartsPercentage(\n\t\tconst std::vector<float64> &sums,\n\t\tfloat64 totalSum,\n\t\tbool round) {\n\tauto result = PiePartData();\n\tresult.parts.reserve(sums.size());\n\tauto stackedPercentage = 0.;\n\n\tauto sumPercDiffs = 0.;\n\tauto maxPercDiff = 0.;\n\tauto minPercDiff = 0.;\n\tauto maxPercDiffIndex = int(-1);\n\tauto minPercDiffIndex = int(-1);\n\tauto roundedPercentagesSum = 0.;\n\n\tresult.pieHasSinglePart = false;\n\tconstexpr auto kPerChar = '%';\n\tfor (auto k = 0; k < sums.size(); k++) {\n\t\tconst auto rawPercentage = totalSum ? (sums[k] / totalSum) : 0.;\n\t\tconst auto rounded = round\n\t\t\t? (0.01 * std::round(rawPercentage * 100.))\n\t\t\t: rawPercentage;\n\t\troundedPercentagesSum += rounded;\n\t\tconst auto diff = rawPercentage - rounded;\n\t\tsumPercDiffs += diff;\n\t\tconst auto diffAbs = std::abs(diff);\n\t\tif (maxPercDiff < diffAbs) {\n\t\t\tmaxPercDiff = diffAbs;\n\t\t\tmaxPercDiffIndex = k;\n\t\t}\n\t\tif (minPercDiff < diffAbs) {\n\t\t\tminPercDiff = diffAbs;\n\t\t\tminPercDiffIndex = k;\n\t\t}\n\n\t\tstackedPercentage += rounded;\n\t\tresult.parts.push_back({\n\t\t\trounded,\n\t\t\tstackedPercentage * 360. - 180.,\n\t\t\tQString::number(int(rounded * 100)) + kPerChar,\n\t\t});\n\t\tresult.pieHasSinglePart |= (rounded == 1.);\n\t}\n\tif (round) {\n\t\tconst auto index = (roundedPercentagesSum > 1.)\n\t\t\t? maxPercDiffIndex\n\t\t\t: minPercDiffIndex;\n\t\tif (index >= 0) {\n\t\t\tresult.parts[index].roundedPercentage += sumPercDiffs;\n\t\t\tresult.parts[index].percentageText = QString::number(\n\t\t\t\tint(result.parts[index].roundedPercentage * 100)) + kPerChar;\n\t\t\tconst auto angleShrink = (sumPercDiffs) * 360.;\n\t\t\tfor (auto &part : result.parts) {\n\t\t\t\tpart.stackedAngle += angleShrink;\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nPiePartData PiePartsPercentageByIndices(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tconst std::shared_ptr<LinesFilterController> &linesFilter,\n\t\tconst Limits &xIndices) {\n\tauto sums = std::vector<float64>();\n\tsums.reserve(chartData.lines.size());\n\tauto totalSum = 0.;\n\tfor (const auto &line : chartData.lines) {\n\t\tauto sum = ChartValue(0);\n\t\tfor (auto i = xIndices.min; i <= xIndices.max; i++) {\n\t\t\tconst auto index = int(base::SafeRound(i));\n\t\t\tAssert(index >= 0 && index < line.y.size());\n\t\t\tsum += line.y[i];\n\t\t}\n\t\tif (linesFilter) {\n\t\t\tsum *= linesFilter->alpha(line.id);\n\t\t}\n\t\ttotalSum += sum;\n\t\tsums.push_back(sum);\n\t}\n\treturn PiePartsPercentage(sums, totalSum, true);\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/stack_linear_chart_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nstruct StatisticalChart;\n} // namespace Data\n\nnamespace Statistic {\n\nstruct Limits;\nclass LinesFilterController;\n\nstruct PiePartData final {\n\tstruct Part final {\n\t\tfloat64 roundedPercentage = 0; // 0.XX.\n\t\tfloat64 stackedAngle = 0.;\n\t\tQString percentageText;\n\t};\n\tstd::vector<Part> parts;\n\tbool pieHasSinglePart = false;\n};\n\n[[nodiscard]] PiePartData PiePartsPercentage(\n\tconst std::vector<float64> &sums,\n\tfloat64 totalSum,\n\tbool round);\n\n[[nodiscard]] PiePartData PiePartsPercentageByIndices(\n\tconst Data::StatisticalChart &chartData,\n\tconst std::shared_ptr<LinesFilterController> &linesFilter,\n\tconst Limits &xIndices);\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/stack_linear_chart_view.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/view/stack_linear_chart_view.h\"\n\n#include \"data/data_statistics_chart.h\"\n#include \"statistics/chart_lines_filter_controller.h\"\n#include \"statistics/view/stack_chart_common.h\"\n#include \"statistics/widgets/point_details_widget.h\"\n#include \"ui/effects/animation_value_f.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"styles/style_statistics.h\"\n\n#include <QtCore/QtMath>\n\nnamespace Statistic {\nnamespace {\n\nconstexpr auto kCircleSizeRatio = 0.42;\nconstexpr auto kMinTextScaleRatio = 0.3;\nconstexpr auto kPieAngleOffset = 90;\n\nconstexpr auto kRightTop = short(0);\nconstexpr auto kRightBottom = short(1);\nconstexpr auto kLeftBottom = short(2);\nconstexpr auto kLeftTop = short(3);\n\n[[nodiscard]] short QuarterForPoint(const QRect &r, const QPointF &p) {\n\tif (p.x() >= r.center().x() && p.y() <= r.center().y()) {\n\t\treturn kRightTop;\n\t} else if (p.x() >= r.center().x() && p.y() >= r.center().y()) {\n\t\treturn kRightBottom;\n\t} else if (p.x() < r.center().x() && p.y() >= r.center().y()) {\n\t\treturn kLeftBottom;\n\t} else {\n\t\treturn kLeftTop;\n\t}\n}\n\ninline float64 InterpolationRatio(float64 from, float64 to, float64 result) {\n\treturn (result - from) / (to - from);\n};\n\n[[nodiscard]] Limits FindAdditionalZoomedOutXIndices(const PaintContext &c) {\n\tconstexpr auto kOffset = int(1);\n\tauto &xPercentage = c.chartData.xPercentage;\n\tauto leftResult = 0.;\n\t{\n\t\tauto i = std::max(int(c.xIndices.min) - kOffset, 0);\n\t\tif (xPercentage[i] > c.xPercentageLimits.min) {\n\t\t\twhile (true) {\n\t\t\t\ti--;\n\t\t\t\tif (i < 0) {\n\t\t\t\t\tleftResult = 0;\n\t\t\t\t\tbreak;\n\t\t\t\t} else if (!(xPercentage[i] > c.xPercentageLimits.min)) {\n\t\t\t\t\tleftResult = i;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tleftResult = i;\n\t\t}\n\t}\n\t{\n\t\tconst auto lastIndex = float64(xPercentage.size() - 1);\n\t\tauto i = std::min(lastIndex, float64(c.xIndices.max) + kOffset);\n\t\tif (xPercentage[i] < c.xPercentageLimits.max) {\n\t\t\twhile (true) {\n\t\t\t\ti++;\n\t\t\t\tif (i > lastIndex) {\n\t\t\t\t\treturn { leftResult, lastIndex };\n\t\t\t\t} else if (!(xPercentage[i] < c.xPercentageLimits.max)) {\n\t\t\t\t\treturn { leftResult, i };\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\treturn { leftResult, i };\n\t\t}\n\t}\n}\n\n} // namespace\n\nvoid StackLinearChartView::ChangingPiePartController::setParts(\n\t\tconst std::vector<PiePartData::Part> &was,\n\t\tconst std::vector<PiePartData::Part> &now) {\n\tif (_animValues.size() != was.size()) {\n\t\t_animValues = std::vector<anim::value>(was.size(), anim::value());\n\t\tfor (auto i = 0; i < was.size(); i++) {\n\t\t\t_animValues[i] = anim::value(\n\t\t\t\twas[i].roundedPercentage,\n\t\t\t\tnow[i].roundedPercentage);\n\t\t}\n\t} else {\n\t\tfor (auto i = 0; i < was.size(); i++) {\n\t\t\t_animValues[i] = anim::value(\n\t\t\t\t_animValues[i].current(),\n\t\t\t\tnow[i].roundedPercentage);\n\t\t}\n\t}\n\t_startedAt = crl::now();\n\t_isFinished = false;\n}\n\nvoid StackLinearChartView::ChangingPiePartController::update() {\n\tconst auto progress = std::clamp(\n\t\t(crl::now() - _startedAt) / float64(st::slideWrapDuration),\n\t\t0.,\n\t\t1.);\n\tauto totalSum = 0.;\n\tauto finished = true;\n\tauto result = std::vector<float64>();\n\tresult.reserve(_animValues.size());\n\tfor (auto &anim : _animValues) {\n\t\tanim.update(progress, anim::easeOutCubic);\n\t\tif (finished && (anim.current() != anim.to())) {\n\t\t\tfinished = false;\n\t\t}\n\t\tconst auto value = anim.current();\n\t\tresult.push_back(value);\n\t\ttotalSum += value;\n\t}\n\t_isFinished = finished;\n\t_current = PiePartsPercentage(result, totalSum, false);\n}\n\nPiePartData StackLinearChartView::ChangingPiePartController::current() const {\n\treturn _current;\n}\n\nbool StackLinearChartView::ChangingPiePartController::isFinished() const {\n\treturn _isFinished;\n}\n\nStackLinearChartView::StackLinearChartView() {\n\t_piePartAnimation.init([=] { AbstractChartView::update(); });\n}\n\nStackLinearChartView::~StackLinearChartView() = default;\n\nvoid StackLinearChartView::paint(QPainter &p, const PaintContext &c) {\n\tif (!_transition.progress && !c.footer) {\n\t\tprepareZoom(c, TransitionStep::ZoomedOut);\n\t}\n\tif (_transition.pendingPrepareToZoomIn) {\n\t\t_transition.pendingPrepareToZoomIn = false;\n\t\tprepareZoom(c, TransitionStep::PrepareToZoomIn);\n\t}\n\n\tStackLinearChartView::paintChartOrZoomAnimation(p, c);\n}\n\nvoid StackLinearChartView::prepareZoom(\n\t\tconst PaintContext &c,\n\t\tTransitionStep step) {\n\tif (step == TransitionStep::ZoomedOut) {\n\t\t_transition.zoomedOutXIndicesAdditional\n\t\t\t= FindAdditionalZoomedOutXIndices(c);\n\t\t_transition.zoomedOutXIndices = c.xIndices;\n\t\t_transition.zoomedOutXPercentage = c.xPercentageLimits;\n\t} else if (step == TransitionStep::PrepareToZoomIn) {\n\t\tconst auto &[zoomedStart, zoomedEnd]\n\t\t\t= _transition.zoomedOutXIndices;\n\t\t_transition.lines = std::vector<Transition::TransitionLine>(\n\t\t\tc.chartData.lines.size(),\n\t\t\tTransition::TransitionLine());\n\n\t\tconst auto xPercentageLimits = _transition.zoomedOutXPercentage;\n\t\tconst auto &linesFilter = linesFilterController();\n\n\t\tfor (auto j = 0; j < 2; j++) {\n\t\t\tconst auto i = int((j == 1) ? zoomedEnd : zoomedStart);\n\t\t\tauto stackOffset = 0;\n\t\t\tauto sum = 0.;\n\t\t\tauto drawingLinesCount = 0;\n\t\t\tfor (const auto &line : c.chartData.lines) {\n\t\t\t\tif (line.y[i] > 0) {\n\t\t\t\t\tconst auto alpha = linesFilter->alpha(line.id);\n\t\t\t\t\tsum += line.y[i] * alpha;\n\t\t\t\t\tif (alpha > 0.) {\n\t\t\t\t\t\tdrawingLinesCount++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (auto k = 0; k < c.chartData.lines.size(); k++) {\n\t\t\t\tauto &linePoint = (j\n\t\t\t\t\t? _transition.lines[k].end\n\t\t\t\t\t: _transition.lines[k].start);\n\t\t\t\tconst auto &line = c.chartData.lines[k];\n\t\t\t\tconst auto yPercentage = (drawingLinesCount == 1)\n\t\t\t\t\t? (line.y[i] ? linesFilter->alpha(line.id) : 0)\n\t\t\t\t\t: (sum\n\t\t\t\t\t\t? (line.y[i] * linesFilter->alpha(line.id) / sum)\n\t\t\t\t\t\t: 0);\n\n\t\t\t\tconst auto xPoint = c.rect.width()\n\t\t\t\t\t* ((c.chartData.xPercentage[i] - xPercentageLimits.min)\n\t\t\t\t\t\t/ (xPercentageLimits.max - xPercentageLimits.min));\n\t\t\t\tconst auto height = yPercentage * c.rect.height();\n\t\t\t\tconst auto yPoint = rect::bottom(c.rect)\n\t\t\t\t\t- height\n\t\t\t\t\t- stackOffset;\n\t\t\t\tlinePoint = { xPoint, yPoint };\n\t\t\t\tstackOffset += height;\n\t\t\t}\n\t\t}\n\n\t\tsavePieTextParts(c);\n\t\tapplyParts(_transition.textParts);\n\t}\n}\n\nvoid StackLinearChartView::applyParts(\n\t\tconst std::vector<PiePartData::Part> &parts) {\n\tfor (auto k = 0; k < parts.size(); k++) {\n\t\t_transition.lines[k].angle = parts[k].stackedAngle;\n\t}\n}\n\nvoid StackLinearChartView::saveZoomRange(const PaintContext &c) {\n\t_transition.zoomedInRangeXIndices = FindStackXIndicesFromRawXPercentages(\n\t\tc.chartData,\n\t\tc.xPercentageLimits,\n\t\t_transition.zoomedInLimitXIndices);\n\t_transition.zoomedInRange = {\n\t\tc.chartData.xPercentage[_transition.zoomedInRangeXIndices.min],\n\t\tc.chartData.xPercentage[_transition.zoomedInRangeXIndices.max],\n\t};\n}\n\nvoid StackLinearChartView::savePieTextParts(const PaintContext &c) {\n\tauto data = PiePartsPercentageByIndices(\n\t\tc.chartData,\n\t\tlinesFilterController(),\n\t\t_transition.zoomedInRangeXIndices);\n\t_transition.textParts = std::move(data.parts);\n\t_pieHasSinglePart = data.pieHasSinglePart;\n}\n\nvoid StackLinearChartView::paintChartOrZoomAnimation(\n\t\tQPainter &p,\n\t\tconst PaintContext &c) {\n\tif (_transition.progress == 1.) {\n\t\tif (c.footer) {\n\t\t\tpaintZoomedFooter(p, c);\n\t\t} else {\n\t\t\tpaintZoomed(p, c);\n\t\t}\n\t\treturn p.setOpacity(0.);\n\t}\n\tconst auto &linesFilter = linesFilterController();\n\tconst auto hasTransitionAnimation = _transition.progress && !c.footer;\n\tconst auto &[localStart, localEnd] = c.footer\n\t\t? Limits{ 0., float64(c.chartData.xPercentage.size() - 1) }\n\t\t: _transition.zoomedOutXIndicesAdditional;\n\t_skipPoints = std::vector<bool>(c.chartData.lines.size(), false);\n\tauto paths = std::vector<QPainterPath>(\n\t\tc.chartData.lines.size(),\n\t\tQPainterPath());\n\n\tconst auto center = QPointF(c.rect.center());\n\n\tconst auto rotate = [&](float64 ang, const QPointF &p) {\n\t\treturn QTransform()\n\t\t\t.translate(center.x(), center.y())\n\t\t\t.rotate(ang)\n\t\t\t.translate(-center.x(), -center.y())\n\t\t\t.map(p);\n\t};\n\n\tconst auto xPercentageLimits = !c.footer\n\t\t? _transition.zoomedOutXPercentage\n\t\t: Limits{\n\t\t\tc.chartData.xPercentage[localStart],\n\t\t\tc.chartData.xPercentage[localEnd],\n\t\t};\n\n\tauto straightLineProgress = 0.;\n\tauto hasEmptyPoint = false;\n\n\tauto ovalPath = QPainterPath();\n\tif (hasTransitionAnimation) {\n\t\tconstexpr auto kStraightLinePart = 0.6;\n\t\tstraightLineProgress = std::clamp(\n\t\t\t_transition.progress / kStraightLinePart,\n\t\t\t0.,\n\t\t\t1.);\n\t\tauto rectPath = QPainterPath();\n\t\trectPath.addRect(c.rect);\n\t\tconst auto r = anim::interpolateF(\n\t\t\t1.,\n\t\t\tkCircleSizeRatio,\n\t\t\t_transition.progress);\n\t\tconst auto per = anim::interpolateF(0., 100., _transition.progress);\n\t\tconst auto side = (c.rect.width() / 2.) * r;\n\t\tconst auto rectF = QRectF(\n\t\t\tcenter - QPointF(side, side),\n\t\t\tcenter + QPointF(side, side));\n\t\tovalPath.addRoundedRect(rectF, per, per, Qt::RelativeSize);\n\t\tovalPath = ovalPath.intersected(rectPath);\n\t}\n\n\tfor (auto i = localStart; i <= localEnd; i++) {\n\t\tauto stackOffset = 0.;\n\t\tauto sum = 0.;\n\t\tauto lastEnabled = int(0);\n\n\t\tauto drawingLinesCount = int(0);\n\n\t\tconst auto xPoint = c.rect.width()\n\t\t\t* ((c.chartData.xPercentage[i] - xPercentageLimits.min)\n\t\t\t\t/ (xPercentageLimits.max - xPercentageLimits.min));\n\n\t\tfor (auto k = 0; k < c.chartData.lines.size(); k++) {\n\t\t\tconst auto &line = c.chartData.lines[k];\n\t\t\tconst auto alpha = linesFilter->alpha(line.id);\n\t\t\tif (!alpha) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (line.y[i] > 0) {\n\t\t\t\tsum += line.y[i] * alpha;\n\t\t\t\tdrawingLinesCount++;\n\t\t\t}\n\t\t\tlastEnabled = k;\n\t\t}\n\n\t\tfor (auto k = 0; k < c.chartData.lines.size(); k++) {\n\t\t\tconst auto &line = c.chartData.lines[k];\n\t\t\tconst auto isLastLine = (k == lastEnabled);\n\t\t\tconst auto lineAlpha = linesFilter->alpha(line.id);\n\t\t\tif (isLastLine && (lineAlpha < 1.)) {\n\t\t\t\thasEmptyPoint = true;\n\t\t\t}\n\t\t\tif (!lineAlpha) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto &transitionLine = hasTransitionAnimation\n\t\t\t\t? _transition.lines[k]\n\t\t\t\t: Transition::TransitionLine();\n\t\t\tconst auto &y = line.y;\n\n\t\t\tauto &chartPath = paths[k];\n\n\t\t\tconst auto yPercentage = (drawingLinesCount == 1)\n\t\t\t\t? float64(y[i] ? lineAlpha : 0.)\n\t\t\t\t: float64(sum ? (y[i] * lineAlpha / sum) : 0.);\n\n\t\t\tif (isLastLine && !yPercentage) {\n\t\t\t\thasEmptyPoint = true;\n\t\t\t}\n\t\t\tconst auto height = yPercentage * c.rect.height();\n\t\t\tconst auto yPoint = rect::bottom(c.rect) - height - stackOffset;\n\t\t\t// startFromY[k] = yPoint;\n\n\t\t\tauto angle = 0.;\n\t\t\tauto resultPoint = QPointF(xPoint, yPoint);\n\t\t\tauto pointZero = QPointF(xPoint, c.rect.y() + c.rect.height());\n\t\t\t// if (i == localEnd) {\n\t\t\t// \tendXPoint = xPoint;\n\t\t\t// } else if (i == localStart) {\n\t\t\t// \tstartXPoint = xPoint;\n\t\t\t// }\n\t\t\tif (hasTransitionAnimation && !isLastLine) {\n\t\t\t\tconst auto point1 = (resultPoint.x() < center.x())\n\t\t\t\t\t? transitionLine.start\n\t\t\t\t\t: transitionLine.end;\n\n\t\t\t\tconst auto diff = center - point1;\n\t\t\t\tconst auto yTo = point1.y()\n\t\t\t\t\t+ diff.y() * (resultPoint.x() - point1.x()) / diff.x();\n\t\t\t\tconst auto yToResult = yTo * straightLineProgress;\n\t\t\t\tconst auto revProgress = (1. - straightLineProgress);\n\n\t\t\t\tresultPoint.setY(resultPoint.y() * revProgress + yToResult);\n\t\t\t\tpointZero.setY(pointZero.y() * revProgress + yToResult);\n\n\t\t\t\t{\n\t\t\t\t\tconst auto angleK = diff.y() / float64(diff.x());\n\t\t\t\t\tangle = (angleK > 0)\n\t\t\t\t\t\t? (-std::atan(angleK)) * (180. / M_PI)\n\t\t\t\t\t\t: (std::atan(std::abs(angleK))) * (180. / M_PI);\n\t\t\t\t\tangle -= 90;\n\t\t\t\t}\n\n\t\t\t\tif (resultPoint.x() >= center.x()) {\n\t\t\t\t\tconst auto resultAngle = _transition.progress * angle;\n\t\t\t\t\tconst auto rotated = rotate(resultAngle, resultPoint);\n\t\t\t\t\tresultPoint = QPointF(\n\t\t\t\t\t\tstd::max(rotated.x(), center.x()),\n\t\t\t\t\t\trotated.y());\n\n\t\t\t\t\tpointZero = QPointF(\n\t\t\t\t\t\tstd::max(pointZero.x(), center.x()),\n\t\t\t\t\t\trotate(resultAngle, pointZero).y());\n\t\t\t\t} else {\n\t\t\t\t\tconst auto &xLimits = xPercentageLimits;\n\t\t\t\t\tconst auto isNextXPointAfterCenter = false\n\t\t\t\t\t\t|| center.x() < (c.rect.width() * ((i == localEnd)\n\t\t\t\t\t\t\t? 1.\n\t\t\t\t\t\t\t: ((c.chartData.xPercentage[i + 1] - xLimits.min)\n\t\t\t\t\t\t\t\t/ (xLimits.max - xLimits.min))));\n\t\t\t\t\tif (isNextXPointAfterCenter) {\n\t\t\t\t\t\tpointZero = resultPoint = QPointF()\n\t\t\t\t\t\t\t+ center * straightLineProgress\n\t\t\t\t\t\t\t+ resultPoint * revProgress;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst auto resultAngle = _transition.progress * angle\n\t\t\t\t\t\t\t+ _transition.progress * transitionLine.angle;\n\t\t\t\t\t\tresultPoint = rotate(resultAngle, resultPoint);\n\t\t\t\t\t\tpointZero = rotate(resultAngle, pointZero);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (i == localStart) {\n\t\t\t\tconst auto bottomLeft = QPointF(\n\t\t\t\t\tc.rect.x(),\n\t\t\t\t\trect::bottom(c.rect));\n\t\t\t\tconst auto local = (hasTransitionAnimation && !isLastLine)\n\t\t\t\t\t? rotate(\n\t\t\t\t\t\t_transition.progress * angle\n\t\t\t\t\t\t\t+ _transition.progress * transitionLine.angle,\n\t\t\t\t\t\tbottomLeft - QPointF(center.x(), 0))\n\t\t\t\t\t: bottomLeft;\n\t\t\t\tchartPath.setFillRule(Qt::WindingFill);\n\t\t\t\tchartPath.moveTo(local);\n\t\t\t\t_skipPoints[k] = false;\n\t\t\t}\n\n\t\t\tconst auto yRatio = 1. - (isLastLine ? _transition.progress : 0.);\n\t\t\tif ((!yPercentage)\n\t\t\t\t&& (i > 0 && (y[i - 1] == 0))\n\t\t\t\t&& (i < localEnd && (y[i + 1] == 0))\n\t\t\t\t&& (!hasTransitionAnimation)) {\n\t\t\t\tif (!_skipPoints[k]) {\n\t\t\t\t\tchartPath.lineTo(pointZero.x(), pointZero.y() * yRatio);\n\t\t\t\t}\n\t\t\t\t_skipPoints[k] = true;\n\t\t\t} else {\n\t\t\t\tif (_skipPoints[k]) {\n\t\t\t\t\tchartPath.lineTo(pointZero.x(), pointZero.y() * yRatio);\n\t\t\t\t}\n\t\t\t\tchartPath.lineTo(resultPoint.x(), resultPoint.y() * yRatio);\n\t\t\t\t_skipPoints[k] = false;\n\t\t\t}\n\n\t\t\tif (i == localEnd) {\n\t\t\t\tif (hasTransitionAnimation && !isLastLine) {\n\t\t\t\t\t{\n\t\t\t\t\t\tconst auto diff = center - transitionLine.start;\n\t\t\t\t\t\tconst auto angleK = diff.y() / diff.x();\n\t\t\t\t\t\tangle = (angleK > 0)\n\t\t\t\t\t\t\t? ((-std::atan(angleK)) * (180. / M_PI))\n\t\t\t\t\t\t\t: ((std::atan(std::abs(angleK))) * (180. / M_PI));\n\t\t\t\t\t\tangle -= 90;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst auto local = rotate(\n\t\t\t\t\t\t_transition.progress * angle\n\t\t\t\t\t\t\t+ _transition.progress * transitionLine.angle,\n\t\t\t\t\t\ttransitionLine.start);\n\n\t\t\t\t\tconst auto ending = true\n\t\t\t\t\t\t&& (std::abs(resultPoint.x() - local.x()) < 0.001)\n\t\t\t\t\t\t&& ((local.y() < center.y()\n\t\t\t\t\t\t\t\t&& resultPoint.y() < center.y())\n\t\t\t\t\t\t\t|| (local.y() > center.y()\n\t\t\t\t\t\t\t\t&& resultPoint.y() > center.y()));\n\t\t\t\t\tconst auto endQuarter = (!ending)\n\t\t\t\t\t\t? QuarterForPoint(c.rect, resultPoint)\n\t\t\t\t\t\t: kRightTop;\n\t\t\t\t\tconst auto startQuarter = (!ending)\n\t\t\t\t\t\t? QuarterForPoint(c.rect, local)\n\t\t\t\t\t\t: (transitionLine.angle == -180.)\n\t\t\t\t\t\t? kRightTop\n\t\t\t\t\t\t: kLeftTop;\n\n\t\t\t\t\tfor (auto q = endQuarter; q <= startQuarter; q++) {\n\t\t\t\t\t\tchartPath.lineTo(\n\t\t\t\t\t\t\t(q == kLeftTop || q == kLeftBottom)\n\t\t\t\t\t\t\t\t? c.rect.x()\n\t\t\t\t\t\t\t\t: rect::right(c.rect),\n\t\t\t\t\t\t\t(q == kLeftTop || q == kRightTop)\n\t\t\t\t\t\t\t\t? c.rect.y()\n\t\t\t\t\t\t\t\t: rect::right(c.rect));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tchartPath.lineTo(\n\t\t\t\t\t\trect::right(c.rect),\n\t\t\t\t\t\trect::bottom(c.rect));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tstackOffset += height;\n\t\t}\n\t}\n\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tp.fillRect(c.rect + QMargins(0, 0, 0, st::lineWidth), st::boxBg);\n\tif (!ovalPath.isEmpty()) {\n\t\tp.setClipPath(ovalPath);\n\t}\n\n\tif (hasEmptyPoint) {\n\t\tp.fillRect(c.rect, st::boxDividerBg);\n\t}\n\n\tconst auto opacity = c.footer ? (1. - _transition.progress) : 1.;\n\tfor (auto k = int(c.chartData.lines.size() - 1); k >= 0; k--) {\n\t\tif (paths[k].isEmpty()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto &line = c.chartData.lines[k];\n\t\tp.setPen(Qt::NoPen);\n\t\tp.fillPath(paths[k], line.color);\n\t}\n\tp.setOpacity(opacity);\n\tif (!c.footer) {\n\t\tconstexpr auto kAlphaTextPart = 0.6;\n\t\tconst auto progress = std::clamp(\n\t\t\t(_transition.progress - kAlphaTextPart) / (1. - kAlphaTextPart),\n\t\t\t0.,\n\t\t\t1.);\n\t\tif (progress > 0) {\n\t\t\tauto o = ScopedPainterOpacity(p, progress);\n\t\t\tpaintPieText(p, c);\n\t\t}\n\t} else if (_transition.progress) {\n\t\tpaintZoomedFooter(p, c);\n\t}\n\n\t// Fix ugly outline.\n\tif (!c.footer || !_transition.progress) {\n\t\tp.setBrush(Qt::transparent);\n\t\tp.setPen(st::boxBg);\n\t\tp.drawPath(ovalPath);\n\t}\n\n\tif (!ovalPath.isEmpty()) {\n\t\tp.setClipRect(c.rect, Qt::NoClip);\n\t}\n\tp.setOpacity(1. - _transition.progress);\n}\n\nvoid StackLinearChartView::paintZoomed(QPainter &p, const PaintContext &c) {\n\tif (c.footer) {\n\t\treturn;\n\t}\n\n\tconst auto wasZoomedInRangeXIndices = _transition.zoomedInRangeXIndices;\n\tsaveZoomRange(c);\n\tconst auto &[zoomedStart, zoomedEnd] = _transition.zoomedInRangeXIndices;\n\tconst auto partsData = PiePartsPercentageByIndices(\n\t\tc.chartData,\n\t\tlinesFilterController(),\n\t\t_transition.zoomedInRangeXIndices);\n\tconst auto xIndicesChanged = (wasZoomedInRangeXIndices.min != zoomedStart)\n\t\t|| (wasZoomedInRangeXIndices.max != zoomedEnd);\n\tif (xIndicesChanged) {\n\t\tconst auto wasParts = PiePartsPercentageByIndices(\n\t\t\tc.chartData,\n\t\t\tlinesFilterController(),\n\t\t\twasZoomedInRangeXIndices);\n\t\t_changingPieController.setParts(wasParts.parts, partsData.parts);\n\t\tif (!_piePartAnimation.animating()) {\n\t\t\t_piePartAnimation.start();\n\t\t}\n\t}\n\tif (!_changingPieController.isFinished()) {\n\t\t_changingPieController.update();\n\t}\n\t_pieHasSinglePart = partsData.pieHasSinglePart;\n\tapplyParts(partsData.parts);\n\tconst auto &parts = _changingPieController.isFinished()\n\t\t? partsData.parts\n\t\t: _changingPieController.current().parts;\n\n\tp.fillRect(c.rect + QMargins(0, 0, 0, st::lineWidth), st::boxBg);\n\tconst auto center = QPointF(c.rect.center());\n\tconst auto side = (c.rect.width() / 2.) * kCircleSizeRatio;\n\tconst auto rectF = QRectF(\n\t\tcenter - QPointF(side, side),\n\t\tcenter + QPointF(side, side));\n\n\tauto hq = PainterHighQualityEnabler(p);\n\tauto selectedLineIndex = -1;\n\tconst auto skipTranslation = skipSelectedTranslation();\n\tfor (auto k = 0; k < c.chartData.lines.size(); k++) {\n\t\tconst auto previous = k\n\t\t\t? parts[k - 1].stackedAngle\n\t\t\t: -180;\n\t\tconst auto now = parts[k].stackedAngle;\n\n\t\tconst auto &line = c.chartData.lines[k];\n\t\tp.setBrush(line.color);\n\t\tp.setPen(Qt::NoPen);\n\t\tconst auto textAngle = (previous + kPieAngleOffset)\n\t\t\t+ (now - previous) / 2.;\n\t\tconst auto partOffset = skipTranslation\n\t\t\t? QPointF()\n\t\t\t: _piePartController.offset(line.id, textAngle);\n\t\tp.translate(partOffset);\n\t\tp.drawPie(\n\t\t\trectF,\n\t\t\t-(previous + kPieAngleOffset) * 16,\n\t\t\t-(now - previous) * 16);\n\t\tp.translate(-partOffset);\n\t\tif (_piePartController.selected() == line.id) {\n\t\t\tselectedLineIndex = k;\n\t\t}\n\t}\n\tif (_piePartController.isFinished() && _changingPieController.isFinished()) {\n\t\t_piePartAnimation.stop();\n\t}\n\tpaintPieText(p, c);\n\n\tif (selectedLineIndex >= 0) {\n\t\tconst auto &line = c.chartData.lines[selectedLineIndex];\n\t\tauto sum = ChartValue(0);\n\t\tfor (auto i = zoomedStart; i <= zoomedEnd; i++) {\n\t\t\tsum += line.y[i];\n\t\t}\n\t\tsum *= linesFilterController()->alpha(line.id);\n\t\tif (sum > 0) {\n\t\t\tPaintDetails(p, line, sum, c.rect);\n\t\t}\n\t}\n}\n\nvoid StackLinearChartView::paintZoomedFooter(\n\t\tQPainter &p,\n\t\tconst PaintContext &c) {\n\tif (!c.footer) {\n\t\treturn;\n\t}\n\tauto o = ScopedPainterOpacity(p, _transition.progress);\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto &[zoomedStart, zoomedEnd] = _transition.zoomedInLimitXIndices;\n\tconst auto sideW = st::statisticsChartFooterSideWidth;\n\tconst auto width = c.rect.width() - sideW * 2.;\n\tconst auto leftStart = c.rect.x() + sideW;\n\tconst auto &xPercentage = c.chartData.xPercentage;\n\tauto previousX = leftStart;\n\t// Read FindStackXIndicesFromRawXPercentages.\n\tconst auto offset = (xPercentage[zoomedEnd] == 1.) ? 0 : 1;\n\tfor (auto i = zoomedStart; i <= zoomedEnd; i++) {\n\t\tauto sum = 0.;\n\t\tauto lastEnabledId = int(0);\n\t\tfor (const auto &line : c.chartData.lines) {\n\t\t\tconst auto alpha = linesFilterController()->alpha(line.id);\n\t\t\tsum += line.y[i] * alpha;\n\t\t\tif (alpha > 0.) {\n\t\t\t\tlastEnabledId = line.id;\n\t\t\t}\n\t\t}\n\n\t\tconst auto columnMargins = QMarginsF(\n\t\t\t(i == zoomedStart) ? sideW : 0,\n\t\t\t0,\n\t\t\t(i == zoomedEnd - offset) ? sideW : 0,\n\t\t\t0);\n\n\t\tconst auto next = std::clamp(i + offset, zoomedStart, zoomedEnd);\n\t\tconst auto xPointPercentage\n\t\t\t= (xPercentage[next] - xPercentage[zoomedStart])\n\t\t\t\t/ (xPercentage[zoomedEnd] - xPercentage[zoomedStart]);\n\t\tconst auto xPoint = leftStart + width * xPointPercentage;\n\n\t\tauto stack = 0.;\n\t\tfor (auto k = int(c.chartData.lines.size() - 1); k >= 0; k--) {\n\t\t\tconst auto &line = c.chartData.lines[k];\n\t\t\tconst auto visibleHeight = c.rect.height()\n\t\t\t\t* (line.y[i] * linesFilterController()->alpha(line.id) / sum);\n\t\t\tif (!visibleHeight) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto height = (line.id == lastEnabledId)\n\t\t\t\t? c.rect.height()\n\t\t\t\t: visibleHeight;\n\n\t\t\tconst auto column = columnMargins + QRectF(\n\t\t\t\tpreviousX,\n\t\t\t\tstack,\n\t\t\t\txPoint - previousX,\n\t\t\t\theight);\n\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.fillRect(column, line.color);\n\t\t\tstack += visibleHeight;\n\t\t}\n\t\tpreviousX = xPoint;\n\t}\n}\n\nvoid StackLinearChartView::paintPieText(QPainter &p, const PaintContext &c) {\n\tconstexpr auto kMinPercentage = 0.039;\n\tif (_transition.progress == 1.) {\n\t\tsavePieTextParts(c);\n\t}\n\tconst auto &parts = _changingPieController.isFinished()\n\t\t? _transition.textParts\n\t\t: _changingPieController.current().parts;\n\n\tconst auto center = QPointF(c.rect.center());\n\tconst auto side = (c.rect.width() / 2.) * kCircleSizeRatio;\n\tconst auto rectF = QRectF(\n\t\tcenter - QPointF(side, side),\n\t\tcenter + QPointF(side, side));\n\tconst auto &font = st::statisticsPieChartFont;\n\tconst auto maxScale = side / (font->height * 2);\n\tconst auto minScale = maxScale * kMinTextScaleRatio;\n\tp.setBrush(Qt::NoBrush);\n\tp.setPen(st::premiumButtonFg);\n\tp.setFont(font);\n\tconst auto opacity = p.opacity();\n\tconst auto skipTranslation = skipSelectedTranslation();\n\tfor (auto k = 0; k < c.chartData.lines.size(); k++) {\n\t\tconst auto previous = k\n\t\t\t? parts[k - 1].stackedAngle\n\t\t\t: -180;\n\t\tconst auto now = parts[k].stackedAngle;\n\t\tconst auto percentage = parts[k].roundedPercentage;\n\t\tif (percentage <= kMinPercentage) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst auto rText = side * std::sqrt(1. - percentage);\n\t\tconst auto textAngle = (now == previous)\n\t\t\t? 0.\n\t\t\t: ((previous + kPieAngleOffset) + (now - previous) / 2.);\n\t\tconst auto textRadians = textAngle * M_PI / 180.;\n\t\tconst auto scale = (maxScale == minScale)\n\t\t\t? 0.\n\t\t\t: (minScale) + percentage * (maxScale - minScale);\n\t\tconst auto text = parts[k].percentageText;\n\t\tconst auto textW = font->width(text);\n\t\tconst auto textXShift = textW / 2.;\n\t\tconst auto textYShift = textW / 2.;\n\t\tconst auto textRectCenter = rectF.center() + QPointF(\n\t\t\t(rText - textXShift * (1. - scale)) * std::cos(textRadians),\n\t\t\t(rText - textYShift * (1. - scale)) * std::sin(textRadians));\n\t\tconst auto textRect = QRectF(\n\t\t\ttextRectCenter - QPointF(textXShift, textYShift),\n\t\t\ttextRectCenter + QPointF(textXShift, textYShift));\n\t\tconst auto partOffset = skipTranslation\n\t\t\t? QPointF()\n\t\t\t: _piePartController.offset(c.chartData.lines[k].id, textAngle);\n\t\tp.setTransform(\n\t\t\tQTransform()\n\t\t\t\t.translate(\n\t\t\t\t\ttextRectCenter.x() + partOffset.x(),\n\t\t\t\t\ttextRectCenter.y() + partOffset.y())\n\t\t\t\t.scale(scale, scale)\n\t\t\t\t.translate(-textRectCenter.x(), -textRectCenter.y()));\n\t\tp.setOpacity(opacity\n\t\t\t* linesFilterController()->alpha(c.chartData.lines[k].id));\n\t\tp.drawText(textRect, text, style::al_center);\n\t}\n\tp.resetTransform();\n}\n\nbool StackLinearChartView::PiePartController::set(int id) {\n\tif (_selected != id) {\n\t\tupdate(_selected);\n\t\t_selected = id;\n\t\tupdate(_selected);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid StackLinearChartView::PiePartController::update(int id) {\n\tif (id >= 0) {\n\t\tconst auto was = _startedAt[id];\n\t\tconst auto p = (crl::now() - was) / float64(st::slideWrapDuration);\n\t\tconst auto progress = ((p > 0) && (p < 1)) ? (1. - p) : 0.;\n\t\t_startedAt[id] = crl::now() - (st::slideWrapDuration * progress);\n\t}\n}\n\nfloat64 StackLinearChartView::PiePartController::progress(int id) const {\n\tconst auto it = _startedAt.find(id);\n\tif (it == end(_startedAt)) {\n\t\treturn 0.;\n\t}\n\tconst auto at = it->second;\n\tconst auto show = (_selected == id);\n\tconst auto progress = std::clamp(\n\t\t(crl::now() - at) / float64(st::slideWrapDuration),\n\t\t0.,\n\t\t1.);\n\treturn std::clamp(show ? progress : (1. - progress), 0., 1.);\n}\n\nQPointF StackLinearChartView::PiePartController::offset(\n\t\tLineId id,\n\t\tfloat64 angle) const {\n\tconst auto offset = st::statisticsPieChartPartOffset * progress(id);\n\tconst auto radians = angle * M_PI / 180.;\n\treturn { std::cos(radians) * offset, std::sin(radians) * offset };\n}\n\nauto StackLinearChartView::PiePartController::selected() const -> LineId {\n\treturn _selected;\n}\n\nbool StackLinearChartView::PiePartController::isFinished() const {\n\tfor (const auto &[id, _] : _startedAt) {\n\t\tconst auto p = progress(id);\n\t\tif (p > 0 && p < 1) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid StackLinearChartView::handleMouseMove(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tconst QRect &rect,\n\t\tconst QPoint &p) {\n\tif (_transition.progress < 1) {\n\t\treturn;\n\t}\n\tconst auto center = rect.center();\n\tconst auto theta = std::atan2(center.y() - p.y(), (center.x() - p.x()));\n\tconst auto rawAngle = theta * (180. / M_PI) + 90.;\n\tconst auto angle = (rawAngle > 180.) ? (rawAngle - 360.) : rawAngle;\n\tfor (auto k = 0; k < chartData.lines.size(); k++) {\n\t\tconst auto previous = k\n\t\t\t? _transition.lines[k - 1].angle\n\t\t\t: -180;\n\t\tconst auto now = _transition.lines[k].angle;\n\t\tif (angle > previous && angle <= now) {\n\t\t\tconst auto id = p.isNull()\n\t\t\t\t? -1\n\t\t\t\t: chartData.lines[k].id;\n\t\t\tif (_piePartController.set(id)) {\n\t\t\t\tif (!_piePartAnimation.animating()) {\n\t\t\t\t\t_piePartAnimation.start();\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nbool StackLinearChartView::skipSelectedTranslation() const {\n\treturn _pieHasSinglePart;\n}\n\nvoid StackLinearChartView::paintSelectedXIndex(\n\t\tQPainter &p,\n\t\tconst PaintContext &c,\n\t\tint selectedXIndex,\n\t\tfloat64 progress) {\n\tif ((selectedXIndex < 0) || c.footer) {\n\t\treturn;\n\t}\n\tconst auto xPercentageLimits = _transition.zoomedOutXPercentage;\n\tp.setBrush(st::boxBg);\n\tconst auto i = selectedXIndex;\n\tconst auto isSameToken = (_selectedPoints.lastXIndex == selectedXIndex)\n\t\t&& (_selectedPoints.lastHeightLimits.min == c.heightLimits.min)\n\t\t&& (_selectedPoints.lastHeightLimits.max == c.heightLimits.max)\n\t\t&& (_selectedPoints.lastXLimits.min == xPercentageLimits.min)\n\t\t&& (_selectedPoints.lastXLimits.max == xPercentageLimits.max);\n\t{\n\t\tconst auto useCache = isSameToken;\n\t\tif (!useCache) {\n\t\t\t// Calculate.\n\t\t\tconst auto xPoint = c.rect.width()\n\t\t\t\t* ((c.chartData.xPercentage[i] - xPercentageLimits.min)\n\t\t\t\t\t/ (xPercentageLimits.max - xPercentageLimits.min));\n\t\t\t_selectedPoints.xPoint = xPoint;\n\t\t}\n\n\t\t{\n\t\t\t[[maybe_unused]] const auto o = ScopedPainterOpacity(\n\t\t\t\tp,\n\t\t\t\tp.opacity() * progress * kRulerLineAlpha);\n\t\t\tconst auto lineRect = QRectF(\n\t\t\t\t_selectedPoints.xPoint - (st::lineWidth / 2.),\n\t\t\t\tc.rect.y(),\n\t\t\t\tst::lineWidth,\n\t\t\t\tc.rect.height());\n\t\t\tp.fillRect(lineRect, st::boxTextFg);\n\t\t}\n\t}\n\t_selectedPoints.lastXIndex = selectedXIndex;\n\t_selectedPoints.lastHeightLimits = c.heightLimits;\n\t_selectedPoints.lastXLimits = xPercentageLimits;\n}\n\nint StackLinearChartView::findXIndexByPosition(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tconst Limits &xPercentageLimits,\n\t\tconst QRect &rect,\n\t\tfloat64 x) {\n\tif (_transition.progress == 1.) {\n\t\treturn -1;\n\t} else if (x < rect.x()) {\n\t\treturn 0;\n\t} else if (x > (rect.x() + rect.width())) {\n\t\treturn chartData.xPercentage.size() - 1;\n\t}\n\tconst auto pointerRatio = std::clamp(\n\t\t(x - rect.x()) / rect.width(),\n\t\t0.,\n\t\t1.);\n\tconst auto &[localStart, localEnd] = _transition.zoomedOutXIndices;\n\tconst auto rawXPercentage = anim::interpolateF(\n\t\t_transition.zoomedOutXPercentage.min,\n\t\t_transition.zoomedOutXPercentage.max,\n\t\tpointerRatio);\n\tconst auto it = ranges::lower_bound(\n\t\tchartData.xPercentage,\n\t\trawXPercentage);\n\tconst auto left = rawXPercentage - (*(it - 1));\n\tconst auto right = (*it) - rawXPercentage;\n\tconst auto nearestXPercentageIt = ((right) > (left)) ? (it - 1) : it;\n\treturn std::clamp(\n\t\tstd::distance(begin(chartData.xPercentage), nearestXPercentageIt),\n\t\tstd::ptrdiff_t(localStart),\n\t\tstd::ptrdiff_t(localEnd));\n}\n\nAbstractChartView::HeightLimits StackLinearChartView::heightLimits(\n\t\tData::StatisticalChart &chartData,\n\t\tLimits xIndices) {\n\tconstexpr auto kMaxStackLinear = 100.;\n\treturn {\n\t\t.full = { 0, kMaxStackLinear },\n\t\t.ranged = { 0., kMaxStackLinear },\n\t};\n}\n\nauto StackLinearChartView::maybeLocalZoom(\n\t\tconst LocalZoomArgs &args) -> LocalZoomResult {\n\t// 8 days.\n\tconstexpr auto kLimitLength = int(8);\n\t// 1 day in middle of limits.\n\tconstexpr auto kRangeLength = int(0);\n\tconstexpr auto kLeftSide = int(kLimitLength / 2 + kRangeLength);\n\tconstexpr auto kRightSide = int(kLimitLength / 2) + int(1);\n\n\t_transition.progress = args.progress;\n\tif (args.type == LocalZoomArgs::Type::SkipCalculation) {\n\t\treturn { true, _transition.zoomedInLimit, _transition.zoomedInRange };\n\t} else if (args.type == LocalZoomArgs::Type::CheckAvailability) {\n\t\treturn { .hasZoom = true };\n\t} else if (args.type == LocalZoomArgs::Type::Prepare) {\n\t\t_transition.pendingPrepareToZoomIn = true;\n\t}\n\tconst auto xIndex = args.xIndex;\n\tconst auto &xPercentage = args.chartData.xPercentage;\n\tconst auto backIndex = (xPercentage.size() - 1);\n\tconst auto localRangeIndex = (xIndex == backIndex)\n\t\t? (backIndex - kRangeLength)\n\t\t: xIndex;\n\t_transition.zoomedInRange = {\n\t\txPercentage[localRangeIndex],\n\t\txPercentage[localRangeIndex + kRangeLength],\n\t};\n\t_transition.zoomedInRangeXIndices = {\n\t\tfloat64(localRangeIndex),\n\t\tfloat64(localRangeIndex + kRangeLength),\n\t};\n\t_transition.zoomedInLimitXIndices = (xIndex < kLeftSide)\n\t\t? Limits{ 0, kLeftSide + kRightSide }\n\t\t: (xIndex > (backIndex - kRightSide - kRangeLength))\n\t\t? Limits{ float64(backIndex - kLimitLength), float64(backIndex) }\n\t\t: Limits{ float64(xIndex - kLeftSide), float64(xIndex + kRightSide) };\n\t_transition.zoomedInLimit = {\n\t\tanim::interpolateF(\n\t\t\t0.,\n\t\t\txPercentage[_transition.zoomedInLimitXIndices.min],\n\t\t\targs.progress),\n\t\tanim::interpolateF(\n\t\t\t1.,\n\t\t\txPercentage[_transition.zoomedInLimitXIndices.max],\n\t\t\targs.progress),\n\t};\n\tconst auto oneDay = std::abs(xPercentage[localRangeIndex]\n\t\t- xPercentage[localRangeIndex + ((xIndex == backIndex) ? -1 : 1)]);\n\t// Read FindStackXIndicesFromRawXPercentages.\n\tconst auto offset = (_transition.zoomedInLimitXIndices.max == backIndex)\n\t\t? -oneDay\n\t\t: 0.;\n\tconst auto resultRange = Limits{\n\t\tInterpolationRatio(\n\t\t\t_transition.zoomedInLimit.min,\n\t\t\t_transition.zoomedInLimit.max,\n\t\t\t_transition.zoomedInRange.min + oneDay * 0.25 + offset),\n\t\tInterpolationRatio(\n\t\t\t_transition.zoomedInLimit.min,\n\t\t\t_transition.zoomedInLimit.max,\n\t\t\t_transition.zoomedInRange.max + oneDay * 0.75 + offset),\n\t};\n\treturn { true, _transition.zoomedInLimitXIndices, resultRange };\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/view/stack_linear_chart_view.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"statistics/segment_tree.h\"\n#include \"statistics/statistics_common.h\"\n#include \"statistics/view/abstract_chart_view.h\"\n#include \"statistics/view/stack_linear_chart_common.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/animation_value.h\"\n\nnamespace Data {\nstruct StatisticalChart;\n} // namespace Data\n\nnamespace Statistic {\n\nstruct Limits;\n\nclass StackLinearChartView final : public AbstractChartView {\npublic:\n\tStackLinearChartView();\n\t~StackLinearChartView() override final;\n\n\tvoid paint(QPainter &p, const PaintContext &c) override;\n\n\tvoid paintSelectedXIndex(\n\t\tQPainter &p,\n\t\tconst PaintContext &c,\n\t\tint selectedXIndex,\n\t\tfloat64 progress) override;\n\n\tint findXIndexByPosition(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tconst Limits &xPercentageLimits,\n\t\tconst QRect &rect,\n\t\tfloat64 x) override;\n\n\t[[nodiscard]] HeightLimits heightLimits(\n\t\tData::StatisticalChart &chartData,\n\t\tLimits xIndices) override;\n\n\tLocalZoomResult maybeLocalZoom(const LocalZoomArgs &args) override final;\n\n\tvoid handleMouseMove(\n\t\tconst Data::StatisticalChart &chartData,\n\t\tconst QRect &rect,\n\t\tconst QPoint &p) override;\n\nprivate:\n\tenum class TransitionStep {\n\t\tPrepareToZoomIn,\n\t\tPrepareToZoomOut,\n\t\tZoomedOut,\n\t};\n\tvoid paintChartOrZoomAnimation(QPainter &p, const PaintContext &c);\n\n\tvoid paintZoomed(QPainter &p, const PaintContext &c);\n\tvoid paintZoomedFooter(QPainter &p, const PaintContext &c);\n\tvoid paintPieText(QPainter &p, const PaintContext &c);\n\n\t[[nodiscard]] bool skipSelectedTranslation() const;\n\n\tvoid prepareZoom(const PaintContext &c, TransitionStep step);\n\n\tvoid saveZoomRange(const PaintContext &c);\n\tvoid savePieTextParts(const PaintContext &c);\n\tvoid applyParts(const std::vector<PiePartData::Part> &parts);\n\n\tstruct SelectedPoints final {\n\t\tint lastXIndex = -1;\n\t\tLimits lastHeightLimits;\n\t\tLimits lastXLimits;\n\t\tfloat64 xPoint = 0.;\n\t};\n\tSelectedPoints _selectedPoints;\n\n\tstruct Transition {\n\t\tstruct TransitionLine {\n\t\t\tQPointF start;\n\t\t\tQPointF end;\n\t\t\tfloat64 angle = 0.;\n\t\t\tfloat64 sum = 0.;\n\t\t};\n\t\tstd::vector<TransitionLine> lines;\n\t\tfloat64 progress = 0;\n\n\t\tbool pendingPrepareToZoomIn = false;\n\n\t\tLimits zoomedOutXIndices;\n\t\tLimits zoomedOutXIndicesAdditional;\n\t\tLimits zoomedOutXPercentage;\n\t\tLimits zoomedInLimit;\n\t\tLimits zoomedInLimitXIndices;\n\t\tLimits zoomedInRange;\n\t\tLimits zoomedInRangeXIndices;\n\n\t\tstd::vector<PiePartData::Part> textParts;\n\t} _transition;\n\n\tstd::vector<bool> _skipPoints;\n\n\tclass PiePartController final {\n\tpublic:\n\t\tusing LineId = int;\n\t\tbool set(LineId id);\n\t\t[[nodiscard]] float64 progress(LineId id) const;\n\t\t[[nodiscard]] QPointF offset(LineId id, float64 angle) const;\n\t\t[[nodiscard]] LineId selected() const;\n\t\t[[nodiscard]] bool isFinished() const;\n\n\tprivate:\n\t\tvoid update(LineId id);\n\n\t\tbase::flat_map<LineId, crl::time> _startedAt;\n\t\tLineId _selected = -1;\n\n\t};\n\n\tclass ChangingPiePartController final {\n\tpublic:\n\t\tvoid setParts(\n\t\t\tconst std::vector<PiePartData::Part> &was,\n\t\t\tconst std::vector<PiePartData::Part> &now);\n\t\tvoid update();\n\t\t[[nodiscard]] PiePartData current() const;\n\t\t[[nodiscard]] bool isFinished() const;\n\n\tprivate:\n\t\tcrl::time _startedAt = 0;\n\t\tstd::vector<anim::value> _animValues;\n\t\tPiePartData _current;\n\t\tbool _isFinished = true;\n\n\t};\n\n\tPiePartController _piePartController;\n\tChangingPiePartController _changingPieController;\n\tUi::Animations::Basic _piePartAnimation;\n\n\tbool _pieHasSinglePart = false;\n\n};\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/widgets/chart_header_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/widgets/chart_header_widget.h\"\n\n#include \"ui/painter.h\"\n#include \"styles/style_statistics.h\"\n\nnamespace Statistic {\n\nHeader::Header(not_null<Ui::RpWidget*> parent)\n: Ui::RpWidget(parent)\n, _height(st::statisticsChartHeaderHeight) {\n}\n\nQString Header::title() const {\n\treturn _title.toString();\n}\n\nvoid Header::setTitle(QString title) {\n\t_title.setText(st::statisticsHeaderTitleTextStyle, std::move(title));\n}\n\nint Header::resizeGetHeight(int newWidth) {\n\treturn _height;\n}\n\nvoid Header::setSubTitle(QString subTitle) {\n\t_height = subTitle.isEmpty()\n\t\t? st::statisticsHeaderTitleTextStyle.font->height\n\t\t: st::statisticsChartHeaderHeight;\n\t_subTitle.setText(\n\t\tst::statisticsHeaderDatesTextStyle,\n\t\tstd::move(subTitle));\n}\n\nvoid Header::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\n\tp.fillRect(rect(), st::boxBg);\n\n\tp.setPen(st::windowActiveTextFg);\n\t_title.drawLeftElided(p, 0, 0, width(), width());\n\n\tp.setPen(st::windowSubTextFg);\n\t_subTitle.drawLeftElided(p, 0, _infoTop, width(), width());\n}\n\nvoid Header::resizeEvent(QResizeEvent *e) {\n\t_infoTop = e->size().height()\n\t\t- st::statisticsHeaderDatesTextStyle.font->height;\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/widgets/chart_header_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace Statistic {\n\nclass Header final : public Ui::RpWidget {\npublic:\n\texplicit Header(not_null<Ui::RpWidget*> parent);\n\n\t[[nodiscard]] QString title() const;\n\tvoid setTitle(QString title);\n\tvoid setSubTitle(QString subTitle);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tUi::Text::String _title;\n\tUi::Text::String _subTitle;\n\tint _infoTop = 0;\n\tint _height = 0;\n\n};\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/widgets/chart_lines_filter_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/widgets/chart_lines_filter_widget.h\"\n\n#include \"ui/abstract_button.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/shake_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"styles/style_basic.h\"\n#include \"styles/style_statistics.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Statistic {\n\nclass ChartLinesFilterWidget::FlatCheckbox final : public Ui::AbstractButton {\npublic:\n\tFlatCheckbox(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst QString &text,\n\t\tQColor activeColor);\n\n\tvoid shake();\n\tvoid setChecked(bool value, bool animated);\n\t[[nodiscard]] bool checked() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tconst QColor _inactiveTextColor;\n\tconst QColor _activeColor;\n\tconst QColor _inactiveColor;\n\tUi::Text::String _text;\n\n\tUi::Animations::Simple _animation;\n\n\tstruct {\n\t\tUi::Animations::Simple animation;\n\t\tint shift = 0;\n\t} _shake;\n\n\tbool _checked = true;\n\n};\n\nChartLinesFilterWidget::FlatCheckbox::FlatCheckbox(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst QString &text,\n\tQColor activeColor)\n: Ui::AbstractButton(parent)\n, _inactiveTextColor(st::premiumButtonFg->c)\n, _activeColor(activeColor)\n, _inactiveColor(st::boxBg->c)\n, _text(st::statisticsDetailsPopupStyle, text) {\n\tconst auto &margins = st::statisticsChartFlatCheckboxMargins;\n\tconst auto h = _text.minHeight() + rect::m::sum::v(margins) * 2;\n\tresize(\n\t\t_text.maxWidth()\n\t\t\t+ rect::m::sum::h(margins)\n\t\t\t+ h\n\t\t\t+ st::statisticsChartFlatCheckboxCheckWidth * 3\n\t\t\t- st::statisticsChartFlatCheckboxShrinkkWidth,\n\t\th);\n}\n\nvoid ChartLinesFilterWidget::FlatCheckbox::setChecked(\n\t\tbool value,\n\t\tbool animated) {\n\tif (_checked == value) {\n\t\treturn;\n\t}\n\t_checked = value;\n\tif (!animated) {\n\t\t_animation.stop();\n\t} else {\n\t\tconst auto from = value ? 0. : 1.;\n\t\tconst auto to = value ? 1. : 0.;\n\t\t_animation.start([=] { update(); }, from, to, st::shakeDuration);\n\t}\n}\n\nbool ChartLinesFilterWidget::FlatCheckbox::checked() const {\n\treturn _checked;\n}\n\nvoid ChartLinesFilterWidget::FlatCheckbox::shake() {\n\tif (_shake.animation.animating()) {\n\t\treturn;\n\t}\n\t_shake.animation.start(Ui::DefaultShakeCallback([=](int shift) {\n\t\t_shake.shift = shift;\n\t\tupdate();\n\t}), 0., 1., st::shakeDuration);\n}\n\nvoid ChartLinesFilterWidget::FlatCheckbox::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto progress = _animation.value(_checked ? 1. : 0.);\n\n\tp.translate(_shake.shift, 0);\n\n\tconst auto checkWidth = st::statisticsChartFlatCheckboxCheckWidth;\n\tconst auto r = rect() - st::statisticsChartFlatCheckboxMargins;\n\tconst auto heightHalf = r.height() / 2.;\n\tconst auto textX = anim::interpolate(\n\t\tr.center().x() - _text.maxWidth() / 2.,\n\t\tr.x() + heightHalf + checkWidth * 5,\n\t\tprogress);\n\tconst auto textY = (r - st::statisticsChartFlatCheckboxMargins).y();\n\tp.fillRect(r, Qt::transparent);\n\n\tconstexpr auto kCheckPartProgress = 0.5;\n\tconst auto checkProgress = progress / kCheckPartProgress;\n\tconst auto textColor = (progress <= kCheckPartProgress)\n\t\t? anim::color(_activeColor, _inactiveTextColor, checkProgress)\n\t\t: _inactiveTextColor;\n\tconst auto fillColor = (progress <= kCheckPartProgress)\n\t\t? anim::color(_inactiveColor, _activeColor, checkProgress)\n\t\t: _activeColor;\n\n\tp.setPen(QPen(_activeColor, st::statisticsChartLineWidth));\n\tp.setBrush(fillColor);\n\tconst auto radius = r.height() / 2.;\n\t{\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.drawRoundedRect(r, radius, radius);\n\t}\n\n\tp.setPen(textColor);\n\tconst auto textContext = Ui::Text::PaintContext{\n\t\t.position = QPoint(textX, textY),\n\t\t.availableWidth = width(),\n\t};\n\t_text.draw(p, textContext);\n\n\tif (progress > kCheckPartProgress) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(QPen(textColor, st::statisticsChartLineWidth));\n\t\tconst auto bounceProgress = checkProgress - 1.;\n\t\tconst auto start = QPoint(\n\t\t\tr.x() + heightHalf + checkWidth,\n\t\t\ttextY + _text.style()->font->ascent);\n\t\tp.translate(start);\n\t\tp.drawLine({}, -QPoint(checkWidth, checkWidth) * bounceProgress);\n\t\tp.drawLine({}, QPoint(checkWidth, -checkWidth) * bounceProgress * 2);\n\t}\n}\n\nChartLinesFilterWidget::ChartLinesFilterWidget(\n\tnot_null<Ui::RpWidget*> parent)\n: Ui::RpWidget(parent) {\n}\n\nvoid ChartLinesFilterWidget::resizeToWidth(int outerWidth) {\n\tauto maxRight = 0;\n\tfor (auto i = 0; i < _buttons.size(); i++) {\n\t\tconst auto raw = _buttons[i].get();\n\t\tif (!i) {\n\t\t\traw->move(0, 0);\n\t\t} else {\n\t\t\tconst auto prevRaw = _buttons[i - 1].get();\n\t\t\tconst auto prevLeft = rect::right(prevRaw);\n\t\t\tconst auto isOut = (prevLeft + raw->width() > outerWidth);\n\t\t\tconst auto left = isOut ? 0 : prevLeft;\n\t\t\tconst auto top = isOut ? rect::bottom(prevRaw) : prevRaw->y();\n\t\t\traw->move(left, top);\n\t\t}\n\t\tmaxRight = std::max(maxRight, rect::right(raw));\n\t}\n\tif (!_buttons.empty()) {\n\t\tresize(maxRight, rect::bottom(_buttons.back().get()));\n\t}\n}\n\nvoid ChartLinesFilterWidget::fillButtons(\n\t\tconst std::vector<ButtonData> &buttonsData) {\n\t_buttons.clear();\n\n\t_buttons.reserve(buttonsData.size());\n\tfor (auto i = 0; i < buttonsData.size(); i++) {\n\t\tconst auto &buttonData = buttonsData[i];\n\t\tauto button = base::make_unique_q<FlatCheckbox>(\n\t\t\tthis,\n\t\t\tbuttonData.text,\n\t\t\tbuttonData.color);\n\t\tbutton->show();\n\t\tif (buttonData.disabled) {\n\t\t\tbutton->setChecked(false, false);\n\t\t}\n\t\tconst auto id = buttonData.id;\n\t\tbutton->setClickedCallback([=, raw = button.get()] {\n\t\t\tconst auto checked = !raw->checked();\n\t\t\tif (!checked) {\n\t\t\t\tconst auto cancel = [&] {\n\t\t\t\t\tfor (const auto &b : _buttons) {\n\t\t\t\t\t\tif (b.get() == raw) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (b->checked()) {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t}();\n\t\t\t\tif (cancel) {\n\t\t\t\t\traw->shake();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\traw->setChecked(checked, true);\n\t\t\t_buttonEnabledChanges.fire({ .id = id, .enabled = checked });\n\t\t});\n\n\t\t_buttons.push_back(std::move(button));\n\t}\n}\n\nauto ChartLinesFilterWidget::buttonEnabledChanges() const\n-> rpl::producer<ChartLinesFilterWidget::Entry> {\n\treturn _buttonEnabledChanges.events();\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/widgets/chart_lines_filter_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace Statistic {\n\nclass ChartLinesFilterWidget final : public Ui::RpWidget {\npublic:\n\tChartLinesFilterWidget(not_null<Ui::RpWidget*> parent);\n\n\tstruct ButtonData final {\n\t\tQString text;\n\t\tQColor color;\n\t\tint id = 0;\n\t\tbool disabled = false;\n\t};\n\n\tvoid fillButtons(const std::vector<ButtonData> &buttonsData);\n\n\tvoid resizeToWidth(int outerWidth);\n\n\tstruct Entry final {\n\t\tint id = 0;\n\t\tbool enabled = 0;\n\t};\n\t[[nodiscard]] rpl::producer<Entry> buttonEnabledChanges() const;\n\nprivate:\n\tclass FlatCheckbox;\n\n\tstd::vector<base::unique_qptr<FlatCheckbox>> _buttons;\n\n\trpl::event_stream<Entry> _buttonEnabledChanges;\n\n};\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"statistics/widgets/point_details_widget.h\"\n\n#include \"base/debug_log.h\"\n#include \"info/channel_statistics/earn/earn_format.h\"\n#include \"lang/lang_keys.h\"\n#include \"statistics/statistics_common.h\"\n#include \"statistics/statistics_format_values.h\"\n#include \"statistics/statistics_graphics.h\"\n#include \"statistics/view/stack_linear_chart_common.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_statistics.h\"\n\n#include <QtCore/QDateTime>\n#include <QtCore/QLocale>\n\nnamespace Statistic {\nnamespace {\n\n[[nodiscard]] QString FormatWeek(float64 timestamp) {\n\tconstexpr auto kSevenDays = 3600 * 24 * 7;\n\ttimestamp /= 1000;\n\treturn LangDayMonth(timestamp)\n\t\t+ ' '\n\t\t+ QChar(8212)\n\t\t+ ' '\n\t\t+ LangDayMonthYear(timestamp + kSevenDays);\n}\n\nvoid PaintShadow(QPainter &p, int radius, const QRect &r) {\n\tconstexpr auto kHorizontalOffset = 1;\n\tconstexpr auto kHorizontalOffset2 = 2;\n\tconstexpr auto kVerticalOffset = 2;\n\tconstexpr auto kVerticalOffset2 = 3;\n\tconstexpr auto kOpacityStep = 0.2;\n\tconstexpr auto kOpacityStep2 = 0.4;\n\tconst auto hOffset = style::ConvertScale(kHorizontalOffset);\n\tconst auto hOffset2 = style::ConvertScale(kHorizontalOffset2);\n\tconst auto vOffset = style::ConvertScale(kVerticalOffset);\n\tconst auto vOffset2 = style::ConvertScale(kVerticalOffset2);\n\tconst auto opacity = p.opacity();\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tp.setOpacity(opacity);\n\tp.drawRoundedRect(r + QMarginsF(0, hOffset, 0, hOffset), radius, radius);\n\n\tp.setOpacity(opacity * kOpacityStep);\n\tp.drawRoundedRect(r + QMarginsF(hOffset, 0, hOffset, 0), radius, radius);\n\tp.setOpacity(opacity * kOpacityStep2);\n\tp.drawRoundedRect(\n\t\tr + QMarginsF(hOffset2, 0, hOffset2, 0),\n\t\tradius,\n\t\tradius);\n\n\tp.setOpacity(opacity * kOpacityStep);\n\tp.drawRoundedRect(r + QMarginsF(0, 0, 0, vOffset), radius, radius);\n\tp.setOpacity(opacity * kOpacityStep2);\n\tp.drawRoundedRect(r + QMarginsF(0, 0, 0, vOffset2), radius, radius);\n\n\tp.setOpacity(opacity);\n}\n\n} // namespace\n\nvoid PaintDetails(\n\t\tQPainter &p,\n\t\tconst Data::StatisticalChart::Line &line,\n\t\tint absoluteValue,\n\t\tconst QRect &rect) {\n\tauto name = Ui::Text::String(\n\t\tst::statisticsDetailsPopupStyle,\n\t\tline.name);\n\tauto value = Ui::Text::String(\n\t\tst::statisticsDetailsPopupStyle,\n\t\tLang::FormatCountDecimal(absoluteValue));\n\tconst auto nameWidth = name.maxWidth();\n\tconst auto valueWidth = value.maxWidth();\n\n\tconst auto width = valueWidth\n\t\t+ rect::m::sum::h(st::statisticsDetailsPopupMargins)\n\t\t+ rect::m::sum::h(st::statisticsDetailsPopupPadding)\n\t\t+ st::statisticsDetailsPopupPadding.left() // Between strings.\n\t\t+ nameWidth;\n\n\tconst auto height = st::statisticsDetailsPopupStyle.font->height\n\t\t+ rect::m::sum::v(st::statisticsDetailsPopupMargins)\n\t\t+ rect::m::sum::v(st::statisticsDetailsPopupPadding);\n\n\tconst auto fullRect = QRect(\n\t\trect.x() + rect.width() - width,\n\t\trect.y(),\n\t\twidth,\n\t\theight);\n\n\tconst auto innerRect = fullRect - st::statisticsDetailsPopupPadding;\n\tconst auto textRect = innerRect - st::statisticsDetailsPopupMargins;\n\n\tp.setBrush(st::shadowFg);\n\tp.setPen(Qt::NoPen);\n\tPaintShadow(p, st::boxRadius, innerRect);\n\tUi::FillRoundRect(p, innerRect, st::boxBg, Ui::BoxCorners);\n\n\tconst auto lineY = textRect.y();\n\tconst auto valueContext = Ui::Text::PaintContext{\n\t\t.position = QPoint(rect::right(textRect) - valueWidth, lineY),\n\t\t.outerWidth = textRect.width(),\n\t\t.availableWidth = valueWidth,\n\t};\n\tconst auto nameContext = Ui::Text::PaintContext{\n\t\t.position = QPoint(textRect.x(), lineY),\n\t\t.outerWidth = textRect.width(),\n\t\t.availableWidth = textRect.width() - valueWidth,\n\t};\n\tp.setPen(st::boxTextFg);\n\tname.draw(p, nameContext);\n\tp.setPen(line.color);\n\tvalue.draw(p, valueContext);\n}\n\nPointDetailsWidget::PointDetailsWidget(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst Data::StatisticalChart &chartData,\n\tbool zoomEnabled)\n: Ui::AbstractButton(parent)\n, _zoomEnabled(zoomEnabled)\n, _chartData(chartData)\n, _textStyle(st::statisticsDetailsPopupStyle)\n, _headerStyle(st::statisticsDetailsPopupHeaderStyle) {\n\tif (zoomEnabled) {\n\t\trpl::single(rpl::empty_value()) | rpl::then(\n\t\t\tstyle::PaletteChanged()\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto w = st::statisticsDetailsArrowShift;\n\t\t\tconst auto stroke = style::ConvertScaleExact(\n\t\t\t\tst::statisticsDetailsArrowStroke);\n\t\t\t_arrow = QImage(\n\t\t\t\tQSize(w + stroke, w * 2 + stroke) * style::DevicePixelRatio(),\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_arrow.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\t_arrow.fill(Qt::transparent);\n\t\t\t{\n\t\t\t\tauto p = QPainter(&_arrow);\n\n\t\t\t\tconst auto hq = PainterHighQualityEnabler(p);\n\t\t\t\tconst auto s = stroke / 2.;\n\n\t\t\t\tp.setPen(QPen(st::windowSubTextFg, stroke));\n\t\t\t\tp.drawLine(QLineF(s, s, w, w + s));\n\t\t\t\tp.drawLine(QLineF(s, s + w * 2, w, w + s));\n\t\t\t}\n\t\t\tinvalidateCache();\n\t\t}, lifetime());\n\t}\n\n\t_maxPercentageWidth = [&] {\n\t\tif (_chartData.hasPercentages) {\n\t\t\tconst auto maxPercentageText = Ui::Text::String(\n\t\t\t\t_textStyle,\n\t\t\t\tu\"10000%\"_q);\n\t\t\treturn maxPercentageText.maxWidth();\n\t\t}\n\t\treturn 0;\n\t}();\n\n\tconst auto hasUsdLine = (_chartData.currencyRate != 0)\n\t\t&& (_chartData.currency != Data::StatisticalCurrency::None)\n\t\t&& (_chartData.lines.size() == 1);\n\n\tconst auto maxValueTextWidth = [&] {\n\t\tif (hasUsdLine) {\n\t\t\tauto maxValueWidth = 0;\n\t\t\tconst auto multiplier = float64(kOneStarInNano);\n\t\t\tfor (const auto &value : _chartData.lines.front().y) {\n\t\t\t\tconst auto valueText = Ui::Text::String(\n\t\t\t\t\t_textStyle,\n\t\t\t\t\tLang::FormatExactCountDecimal(value / multiplier));\n\t\t\t\tconst auto usdText = Ui::Text::String(\n\t\t\t\t\t_textStyle,\n\t\t\t\t\tInfo::ChannelEarn::ToUsd(\n\t\t\t\t\t\tvalue / multiplier,\n\t\t\t\t\t\t_chartData.currencyRate,\n\t\t\t\t\t\t0));\n\t\t\t\tconst auto width = std::max(\n\t\t\t\t\tusdText.maxWidth(),\n\t\t\t\t\tvalueText.maxWidth());\n\t\t\t\tif (width > maxValueWidth) {\n\t\t\t\t\tmaxValueWidth = width;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn maxValueWidth;\n\t\t}\n\t\tconst auto maxAbsoluteValue = [&] {\n\t\t\tauto maxValue = ChartValue(0);\n\t\t\tfor (const auto &l : _chartData.lines) {\n\t\t\t\tmaxValue = std::max(l.maxValue, maxValue);\n\t\t\t}\n\t\t\treturn maxValue;\n\t\t}();\n\t\tconst auto maxValueText = Ui::Text::String(\n\t\t\t_textStyle,\n\t\t\tLang::FormatCountDecimal(maxAbsoluteValue));\n\t\treturn maxValueText.maxWidth();\n\t}();\n\n\tconst auto calculatedWidth = [&]{\n\t\tauto maxNameTextWidth = 0;\n\t\tconst auto isCredits\n\t\t\t= _chartData.currency == Data::StatisticalCurrency::Credits;\n\t\tfor (const auto &dataLine : _chartData.lines) {\n\t\t\tconst auto maxNameText = Ui::Text::String(\n\t\t\t\t_textStyle,\n\t\t\t\tdataLine.name);\n\t\t\tmaxNameTextWidth = std::max(\n\t\t\t\tmaxNameText.maxWidth(),\n\t\t\t\tmaxNameTextWidth);\n\t\t\tif (hasUsdLine) {\n\t\t\t\tconst auto text = isCredits\n\t\t\t\t\t? tr::lng_channel_earn_chart_overriden_detail_credits\n\t\t\t\t\t: tr::lng_channel_earn_chart_overriden_detail_currency;\n\t\t\t\tconst auto currency = Ui::Text::String(\n\t\t\t\t\t_textStyle,\n\t\t\t\t\ttext(tr::now));\n\t\t\t\tconst auto usd = Ui::Text::String(\n\t\t\t\t\t_textStyle,\n\t\t\t\t\ttr::lng_channel_earn_chart_overriden_detail_usd(\n\t\t\t\t\t\ttr::now));\n\t\t\t\tmaxNameTextWidth = std::max(\n\t\t\t\t\tstd::max(currency.maxWidth(), usd.maxWidth()),\n\t\t\t\t\tmaxNameTextWidth);\n\t\t\t}\n\t\t}\n\t\t{\n\t\t\tconst auto maxHeaderText = Ui::Text::String(\n\t\t\t\t_headerStyle,\n\t\t\t\t_chartData.weekFormat\n\t\t\t\t\t? FormatWeek(_chartData.x.front())\n\t\t\t\t\t: LangDetailedDayMonth(_chartData.x.front() / 1000));\n\t\t\tmaxNameTextWidth = std::max(\n\t\t\t\tmaxHeaderText.maxWidth()\n\t\t\t\t\t+ st::statisticsDetailsPopupPadding.left(),\n\t\t\t\tmaxNameTextWidth);\n\t\t}\n\t\treturn maxValueTextWidth\n\t\t\t+ rect::m::sum::h(st::statisticsDetailsPopupMargins)\n\t\t\t+ rect::m::sum::h(st::statisticsDetailsPopupPadding)\n\t\t\t+ st::statisticsDetailsPopupPadding.left() // Between strings.\n\t\t\t+ maxNameTextWidth\n\t\t\t+ (_valueIcon.isNull()\n\t\t\t\t? 0\n\t\t\t\t: _valueIcon.width() / style::DevicePixelRatio())\n\t\t\t+ _maxPercentageWidth;\n\t}();\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tconst auto fullRect = s.isNull()\n\t\t\t? Rect(Size(calculatedWidth))\n\t\t\t: Rect(s);\n\t\t_innerRect = fullRect - st::statisticsDetailsPopupPadding;\n\t\t_textRect = _innerRect - st::statisticsDetailsPopupMargins;\n\t\tinvalidateCache();\n\t}, lifetime());\n\n\tresize(calculatedWidth, height());\n\tresizeHeight();\n}\n\nvoid PointDetailsWidget::setLineAlpha(int lineId, float64 alpha) {\n\tfor (auto &line : _lines) {\n\t\tif (line.id == lineId) {\n\t\t\tif (line.alpha != alpha) {\n\t\t\t\tline.alpha = alpha;\n\t\t\t\tresizeHeight();\n\t\t\t\tinvalidateCache();\n\t\t\t\tupdate();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nvoid PointDetailsWidget::resizeHeight() {\n\tresize(\n\t\twidth(),\n\t\tlineYAt(_chartData.lines.size() + (_chartData.currencyRate ? 1 : 0))\n\t\t\t+ st::statisticsDetailsPopupMargins.bottom());\n}\n\nint PointDetailsWidget::xIndex() const {\n\treturn _xIndex;\n}\n\nvoid PointDetailsWidget::setXIndex(int xIndex) {\n\t_xIndex = xIndex;\n\tif (xIndex < 0) {\n\t\treturn;\n\t}\n\tif (xIndex >= _chartData.x.size()) {\n\t\tLOG((u\"xIndex out of bounds: %1, max: %2\"_q)\n\t\t\t.arg(xIndex)\n\t\t\t.arg(_chartData.x.size() - 1));\n\t\txIndex = _chartData.x.size() - 1;\n\t}\n\t{\n\t\tconstexpr auto kOneDay = 3600 * 24 * 1000;\n\t\tconst auto timestamp = _chartData.x[xIndex];\n\t\t_header.setText(\n\t\t\t_headerStyle,\n\t\t\t(timestamp < kOneDay)\n\t\t\t\t? _chartData.getDayString(xIndex)\n\t\t\t\t: _chartData.weekFormat\n\t\t\t\t? FormatWeek(timestamp)\n\t\t\t\t: LangDetailedDayMonth(timestamp / 1000));\n\t}\n\n\t_lines.clear();\n\t_lines.reserve(_chartData.lines.size());\n\tauto hasPositiveValues = false;\n\tconst auto parts = _maxPercentageWidth\n\t\t? PiePartsPercentageByIndices(\n\t\t\t_chartData,\n\t\t\tnullptr,\n\t\t\t{ float64(xIndex), float64(xIndex) }).parts\n\t\t: std::vector<PiePartData::Part>();\n\tconst auto isCredits\n\t\t= (_chartData.currency == Data::StatisticalCurrency::Credits);\n\tfor (auto i = 0; i < _chartData.lines.size(); i++) {\n\t\tconst auto &dataLine = _chartData.lines[i];\n\t\tAssert(xIndex < dataLine.y.size());\n\t\tauto textLine = Line();\n\t\ttextLine.id = dataLine.id;\n\t\tif (_maxPercentageWidth) {\n\t\t\ttextLine.percentage.setText(_textStyle, parts[i].percentageText);\n\t\t}\n\t\ttextLine.name.setText(_textStyle, dataLine.name);\n\t\ttextLine.value.setText(\n\t\t\t_textStyle,\n\t\t\tLang::FormatCountDecimal(dataLine.y[xIndex]));\n\t\thasPositiveValues |= (dataLine.y[xIndex] > 0);\n\t\ttextLine.valueColor = QColor(dataLine.color);\n\t\tif (_chartData.currencyRate) {\n\t\t\tauto copy = Line();\n\t\t\tcopy.id = dataLine.id * 100;\n\t\t\tcopy.valueColor = QColor(dataLine.color);\n\t\t\tcopy.name.setText(\n\t\t\t\t_textStyle,\n\t\t\t\t(isCredits\n\t\t\t\t\t? tr::lng_channel_earn_chart_overriden_detail_credits\n\t\t\t\t\t: tr::lng_channel_earn_chart_overriden_detail_currency)(\n\t\t\t\t\t\ttr::now));\n\t\t\tconst auto provided = dataLine.y[xIndex];\n\t\t\tconst auto value = isCredits\n\t\t\t\t? CreditsAmount(provided, CreditsType::Stars)\n\t\t\t\t: CreditsAmount(\n\t\t\t\t\tprovided / kOneStarInNano,\n\t\t\t\t\tprovided % kOneStarInNano,\n\t\t\t\t\tCreditsType::Ton);\n\t\t\tcopy.value.setText(\n\t\t\t\t_textStyle,\n\t\t\t\tLang::FormatCreditsAmountDecimal(value));\n\t\t\t_lines.push_back(std::move(copy));\n\t\t\ttextLine.name.setText(\n\t\t\t\t_textStyle,\n\t\t\t\ttr::lng_channel_earn_chart_overriden_detail_usd(tr::now));\n\t\t\ttextLine.value.setText(\n\t\t\t\t_textStyle,\n\t\t\t\tInfo::ChannelEarn::ToUsd(value, _chartData.currencyRate, 0));\n\t\t}\n\t\t_lines.push_back(std::move(textLine));\n\t}\n\tif (_chartData.currencyRate && _valueIcon.isNull()) {\n\t\t_valueIcon = ChartCurrencyIcon(_chartData, _lines.front().valueColor);\n\t}\n\tconst auto clickable = _zoomEnabled && hasPositiveValues;\n\t_hasPositiveValues = hasPositiveValues;\n\tQWidget::setAttribute(\n\t\tQt::WA_TransparentForMouseEvents,\n\t\t!clickable);\n\tinvalidateCache();\n}\n\nvoid PointDetailsWidget::setAlpha(float64 alpha) {\n\t_alpha = alpha;\n\tupdate();\n}\n\nfloat64 PointDetailsWidget::alpha() const {\n\treturn _alpha;\n}\n\nint PointDetailsWidget::lineYAt(int index) const {\n\tauto linesHeight = 0.;\n\tfor (auto i = 0; i < index; i++) {\n\t\tconst auto alpha = (i >= _lines.size()) ? 1. : _lines[i].alpha;\n\t\tlinesHeight += alpha\n\t\t\t* (_textStyle.font->height\n\t\t\t\t+ st::statisticsDetailsPopupMidLineSpace);\n\t}\n\n\treturn _textRect.y()\n\t\t+ _headerStyle.font->height\n\t\t+ st::statisticsDetailsPopupMargins.bottom() / 2\n\t\t+ std::ceil(linesHeight);\n}\n\nvoid PointDetailsWidget::invalidateCache() {\n\t_cache = QImage();\n}\n\nvoid PointDetailsWidget::mousePressEvent(QMouseEvent *e) {\n\tAbstractButton::mousePressEvent(e);\n\tconst auto position = e->pos() - _innerRect.topLeft();\n\tif (!_ripple) {\n\t\t_ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\tst::defaultRippleAnimation,\n\t\t\tUi::RippleAnimation::RoundRectMask(\n\t\t\t\t_innerRect.size(),\n\t\t\t\tst::boxRadius),\n\t\t\t[=] { update(); });\n\t}\n\t_ripple->add(position);\n}\n\nvoid PointDetailsWidget::mouseReleaseEvent(QMouseEvent *e) {\n\tAbstractButton::mouseReleaseEvent(e);\n\tif (_ripple) {\n\t\t_ripple->lastStop();\n\t}\n}\n\nvoid PointDetailsWidget::paintEvent(QPaintEvent *e) {\n\tauto painter = QPainter(this);\n\n\tif (_cache.isNull()) {\n\t\t_cache = QImage(\n\t\t\tsize() * style::DevicePixelRatio(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t_cache.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t_cache.fill(Qt::transparent);\n\n\t\tauto p = QPainter(&_cache);\n\n\t\tp.setBrush(st::shadowFg);\n\t\tp.setPen(Qt::NoPen);\n\t\tPaintShadow(p, st::boxRadius, _innerRect);\n\t\tUi::FillRoundRect(p, _innerRect, st::boxBg, Ui::BoxCorners);\n\n\t\tif (_ripple) {\n\t\t\t_ripple->paint(p, _innerRect.left(), _innerRect.top(), width());\n\t\t\tif (_ripple->empty()) {\n\t\t\t\t_ripple.reset();\n\t\t\t}\n\t\t}\n\n\t\tp.setPen(st::boxTextFg);\n\t\tconst auto headerContext = Ui::Text::PaintContext{\n\t\t\t.position = _textRect.topLeft(),\n\t\t\t.availableWidth = _textRect.width(),\n\t\t};\n\t\t_header.draw(p, headerContext);\n\t\tfor (auto i = 0; i < _lines.size(); i++) {\n\t\t\tconst auto &line = _lines[i];\n\t\t\tconst auto lineY = lineYAt(i);\n\t\t\tconst auto valueWidth = line.value.maxWidth();\n\t\t\tconst auto valueContext = Ui::Text::PaintContext{\n\t\t\t\t.position = QPoint(\n\t\t\t\t\trect::right(_textRect) - valueWidth,\n\t\t\t\t\tlineY),\n\t\t\t\t.outerWidth = _textRect.width(),\n\t\t\t\t.availableWidth = valueWidth,\n\t\t\t};\n\t\t\tif (!i && !_valueIcon.isNull()) {\n\t\t\t\tp.drawImage(\n\t\t\t\t\tvalueContext.position.x()\n\t\t\t\t\t\t- _valueIcon.width() / style::DevicePixelRatio(),\n\t\t\t\t\tlineY + st::lineWidth,\n\t\t\t\t\t_valueIcon);\n\t\t\t}\n\t\t\tconst auto nameContext = Ui::Text::PaintContext{\n\t\t\t\t.position = QPoint(\n\t\t\t\t\t_textRect.x() + _maxPercentageWidth,\n\t\t\t\t\tlineY),\n\t\t\t\t.outerWidth = _textRect.width(),\n\t\t\t\t.availableWidth = _textRect.width() - valueWidth,\n\t\t\t};\n\t\t\tp.setOpacity(line.alpha * line.alpha);\n\t\t\tp.setPen(st::boxTextFg);\n\t\t\tif (_maxPercentageWidth) {\n\t\t\t\tconst auto percentageContext = Ui::Text::PaintContext{\n\t\t\t\t\t.position = QPoint(_textRect.x(), lineY),\n\t\t\t\t\t.outerWidth = _textRect.width(),\n\t\t\t\t\t.availableWidth = _textRect.width() - valueWidth,\n\t\t\t\t};\n\t\t\t\tline.percentage.draw(p, percentageContext);\n\t\t\t}\n\t\t\tline.name.draw(p, nameContext);\n\t\t\tp.setPen(line.valueColor);\n\t\t\tline.value.draw(p, valueContext);\n\t\t}\n\n\t\tif (_zoomEnabled && _hasPositiveValues) {\n\t\t\tconst auto s = _arrow.size() / style::DevicePixelRatio();\n\t\t\tconst auto x = rect::right(_textRect) - s.width();\n\t\t\tconst auto y = _textRect.y()\n\t\t\t\t+ (_headerStyle.font->ascent - s.height());\n\t\t\tp.drawImage(x, y, _arrow);\n\t\t}\n\t}\n\tif (_alpha < 1.) {\n\t\tpainter.setOpacity(_alpha);\n\t}\n\tpainter.drawImage(0, 0, _cache);\n\tif (_ripple) {\n\t\tinvalidateCache();\n\t}\n}\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/statistics/widgets/point_details_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_statistics_chart.h\"\n#include \"ui/abstract_button.h\"\n\nnamespace Ui {\nclass RippleAnimation;\n} // namespace Ui\n\nnamespace Statistic {\n\nvoid PaintDetails(\n\tQPainter &p,\n\tconst Data::StatisticalChart::Line &line,\n\tint absoluteValue,\n\tconst QRect &rect);\n\nclass PointDetailsWidget : public Ui::AbstractButton {\npublic:\n\tPointDetailsWidget(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst Data::StatisticalChart &chartData,\n\t\tbool zoomEnabled);\n\n\t[[nodiscard]] int xIndex() const;\n\tvoid setXIndex(int xIndex);\n\tvoid setAlpha(float64 alpha);\n\t[[nodiscard]] float64 alpha() const;\n\tvoid setLineAlpha(int lineId, float64 alpha);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\nprivate:\n\tconst bool _zoomEnabled;\n\tconst Data::StatisticalChart &_chartData;\n\tconst style::TextStyle &_textStyle;\n\tconst style::TextStyle &_headerStyle;\n\tUi::Text::String _header;\n\tQImage _valueIcon;\n\n\tvoid invalidateCache();\n\n\t[[nodiscard]] int lineYAt(int index) const;\n\n\tvoid resizeHeight();\n\n\tstruct Line final {\n\t\tint id = 0;\n\t\tUi::Text::String name;\n\t\tUi::Text::String value;\n\t\tUi::Text::String percentage;\n\t\tQColor valueColor;\n\t\tfloat64 alpha = 1.;\n\t};\n\n\tbool _hasPositiveValues = true;\n\n\tint _maxPercentageWidth = 0;\n\n\tQRect _innerRect;\n\tQRect _textRect;\n\tQImage _arrow;\n\n\tQImage _cache;\n\n\tint _xIndex = -1;\n\tfloat64 _alpha = 1.;\n\n\tstd::vector<Line> _lines;\n\n\tstd::unique_ptr<Ui::RippleAnimation> _ripple;\n\n};\n\n} // namespace Statistic\n"
  },
  {
    "path": "Telegram/SourceFiles/stdafx.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\n#define __HUGE\n\n#ifdef __cplusplus\n\n#include <cmath>\n\n// False positive warning in clang for QMap member function value:\n// const T QMap<Key, T>::value(const Key &akey, const T &adefaultValue)\n// fires with \"Returning address of local temporary object\" which is not true.\n#ifdef __clang__\n#pragma clang diagnostic push\n#pragma clang diagnostic ignored \"-Wreturn-stack-address\"\n#elif defined _MSC_VER && _MSC_VER >= 1914 // __clang__\n#pragma warning(push)\n#pragma warning(disable:4180)\n#endif // __clang__ || _MSC_VER >= 1914\n\n#include <QtCore/QMap>\n\n#ifdef __clang__\n#pragma clang diagnostic pop\n#elif defined _MSC_VER && _MSC_VER >= 1914 // __clang__\n#pragma warning(pop)\n#endif // __clang__ || _MSC_VER >= 1914\n\n#include <QtCore/QtMath>\n#include <QtCore/QObject>\n#include <QtCore/QPointer>\n#include <QtCore/QMutex>\n#include <QtCore/QReadWriteLock>\n#include <QtCore/QDataStream>\n#include <QtCore/QDir>\n#include <QtCore/QFile>\n#include <QtCore/QFileInfo>\n#include <QtCore/QThread>\n#include <QtCore/QByteArray>\n#include <QtCore/QChar>\n#include <QtCore/QDateTime>\n#include <QtCore/QHash>\n#include <QtCore/QList>\n#include <QtCore/QMargins>\n#include <QtCore/QPair>\n#include <QtCore/QPoint>\n#include <QtCore/QRect>\n#include <QtCore/QRegularExpression>\n#include <QtCore/QSet>\n#include <QtCore/QSize>\n#include <QtCore/QString>\n#include <QtCore/QStringList>\n#include <QtCore/QVector>\n\n#include <QtGui/QIcon>\n#include <QtGui/QImage>\n#include <QtGui/QImageReader>\n#include <QtGui/QPixmap>\n#include <QtGui/QtEvents>\n#include <QtGui/QBrush>\n#include <QtGui/QColor>\n#include <QtGui/QPainter>\n#include <QtGui/QPainterPath>\n#include <QtGui/QPen>\n#include <QtGui/QRegion>\n#include <QtGui/QRgb>\n#include <QtGui/QFont>\n#include <QtGui/QFontInfo>\n\n#include <QtWidgets/QWidget>\n#include <QOpenGLWidget>\n\n// Fix Google Breakpad build for Mac App Store and Linux version\n#ifndef Q_OS_WIN\n#define __STDC_FORMAT_MACROS\n#endif // !Q_OS_WIN\n\n// Remove 'small' macro definition.\n#ifdef Q_OS_WIN\n#include <rpc.h>\n#ifdef small\n#undef small\n#endif // small\n#endif // Q_OS_WIN\n\n#include <array>\n#include <vector>\n#include <deque>\n#include <set>\n#include <map>\n#include <unordered_map>\n#include <unordered_set>\n#include <algorithm>\n#include <memory>\n#include <any>\n#include <optional>\n\n#include <range/v3/all.hpp>\n\n// Redefine Ensures/Expects by our own assertions.\n#include \"base/assertion.h\"\n\n#include <gsl/gsl>\n#include <rpl/rpl.h>\n#include <crl/crl.h>\n\n#include \"base/algorithm.h\"\n#include \"base/basic_types.h\"\n#include \"base/debug_destroy_informer.h\" // _DEBUG only.\n#include \"base/flat_set.h\"\n#include \"base/flat_map.h\"\n#include \"base/invoke_queued.h\"\n#include \"base/optional.h\"\n#include \"base/variant.h\"\n#include \"base/weak_ptr.h\"\n#include \"base/weak_qptr.h\"\n\n#include \"scheme.h\"\n#include \"mtproto/type_utils.h\"\n\n#include \"ui/image/image_location.h\"\n#include \"ui/style/style_core.h\"\n#include \"ui/text/text.h\"\n#include \"ui/arc_angles.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/qt_object_factory.h\"\n#include \"ui/ui_rpl_filter.h\"\n\n#include \"styles/palette.h\"\n#include \"styles/style_basic.h\"\n\n#include \"core/credits_amount.h\"\n#include \"core/utils.h\"\n#include \"logs.h\"\n#include \"config.h\"\n\n#include \"data/data_types.h\"\n\n#endif // __cplusplus\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/details/storage_file_utilities.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/details/storage_file_utilities.h\"\n\n#include \"mtproto/mtproto_auth_key.h\"\n#include \"base/platform/base_platform_file_utilities.h\"\n#include \"base/openssl_help.h\"\n#include \"base/random.h\"\n\n#include <crl/crl_object_on_thread.h>\n#include <QtCore/QtEndian>\n#include <QtCore/QSaveFile>\n\nnamespace Storage {\nnamespace details {\nnamespace {\n\nconstexpr char TdfMagic[] = { 'T', 'D', 'F', '$' };\nconstexpr auto TdfMagicLen = int(sizeof(TdfMagic));\n\nconstexpr auto kStrongIterationsCount = 100'000;\n\nstruct WriteEntry {\n\tQString basePath;\n\tQString base;\n\tQByteArray data;\n\tQByteArray md5;\n};\n\nclass WriteManager final {\npublic:\n\texplicit WriteManager(crl::weak_on_thread<WriteManager> weak);\n\n\tvoid write(WriteEntry &&entry);\n\tvoid writeSync(WriteEntry &&entry);\n\tvoid writeSyncAll();\n\nprivate:\n\tvoid scheduleWrite();\n\tvoid writeScheduled();\n\tbool writeOneScheduledNow();\n\tvoid writeNow(WriteEntry &&entry);\n\n\ttemplate <typename File>\n\t[[nodiscard]] bool open(File &file, const WriteEntry &entry, char postfix);\n\n\t[[nodiscard]] QString path(const WriteEntry &entry, char postfix) const;\n\t[[nodiscard]] bool writeHeader(\n\t\tconst QString &basePath,\n\t\tQFileDevice &file);\n\n\tcrl::weak_on_thread<WriteManager> _weak;\n\tstd::deque<WriteEntry> _scheduled;\n\n};\n\nclass AsyncWriteManager final {\npublic:\n\tvoid write(WriteEntry &&entry);\n\tvoid writeSync(WriteEntry &&entry);\n\tvoid sync();\n\tvoid stop();\n\nprivate:\n\tstd::optional<crl::object_on_thread<WriteManager>> _manager;\n\tbool _finished = false;\n\n};\n\nWriteManager::WriteManager(crl::weak_on_thread<WriteManager> weak)\n: _weak(std::move(weak)) {\n}\n\nvoid WriteManager::write(WriteEntry &&entry) {\n\tconst auto i = ranges::find(_scheduled, entry.base, &WriteEntry::base);\n\tif (i == end(_scheduled)) {\n\t\t_scheduled.push_back(std::move(entry));\n\t} else {\n\t\t*i = std::move(entry);\n\t}\n\tscheduleWrite();\n}\n\nvoid WriteManager::writeSync(WriteEntry &&entry) {\n\tconst auto i = ranges::find(_scheduled, entry.base, &WriteEntry::base);\n\tif (i != end(_scheduled)) {\n\t\t_scheduled.erase(i);\n\t}\n\twriteNow(std::move(entry));\n}\n\nvoid WriteManager::writeNow(WriteEntry &&entry) {\n\tconst auto path = [&](char postfix) {\n\t\treturn this->path(entry, postfix);\n\t};\n\tconst auto open = [&](auto &file, char postfix) {\n\t\treturn this->open(file, entry, postfix);\n\t};\n\tconst auto write = [&](auto &file) {\n\t\tfile.write(entry.data);\n\t\tfile.write(entry.md5);\n\t};\n\tconst auto safe = path('s');\n\tconst auto simple = path('0');\n\tconst auto backup = path('1');\n\tQSaveFile save;\n\tif (open(save, 's')) {\n\t\twrite(save);\n\t\tif (save.commit()) {\n\t\t\tQFile::remove(simple);\n\t\t\tQFile::remove(backup);\n\t\t\treturn;\n\t\t}\n\t\tLOG((\"Storage Error: Could not commit '%1'.\").arg(safe));\n\t}\n\tQFile plain;\n\tif (open(plain, '0')) {\n\t\twrite(plain);\n\t\tbase::Platform::FlushFileData(plain);\n\t\tplain.close();\n\n\t\tQFile::remove(backup);\n\t\tif (base::Platform::RenameWithOverwrite(simple, safe)) {\n\t\t\treturn;\n\t\t}\n\t\tQFile::remove(safe);\n\t\tLOG((\"Storage Error: Could not rename '%1' to '%2', removing.\").arg(\n\t\t\tsimple,\n\t\t\tsafe));\n\t}\n}\n\nvoid WriteManager::writeSyncAll() {\n\twhile (writeOneScheduledNow()) {\n\t}\n}\n\nbool WriteManager::writeOneScheduledNow() {\n\tif (_scheduled.empty()) {\n\t\treturn false;\n\t}\n\n\tauto entry = std::move(_scheduled.front());\n\t_scheduled.pop_front();\n\n\twriteNow(std::move(entry));\n\treturn true;\n}\n\nbool WriteManager::writeHeader(const QString &basePath, QFileDevice &file) {\n\tif (!file.open(QIODevice::WriteOnly)) {\n\t\tconst auto dir = QDir(basePath);\n\t\tif (dir.exists()) {\n\t\t\treturn false;\n\t\t} else if (!QDir().mkpath(dir.absolutePath())) {\n\t\t\treturn false;\n\t\t} else if (!file.open(QIODevice::WriteOnly)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\tfile.write(TdfMagic, TdfMagicLen);\n\tconst auto version = qint32(AppVersion);\n\tfile.write((const char*)&version, sizeof(version));\n\treturn true;\n}\n\nQString WriteManager::path(const WriteEntry &entry, char postfix) const {\n\treturn entry.base + postfix;\n}\n\ntemplate <typename File>\nbool WriteManager::open(File &file, const WriteEntry &entry, char postfix) {\n\tconst auto name = path(entry, postfix);\n\tfile.setFileName(name);\n\tif (!writeHeader(entry.basePath, file)) {\n\t\tLOG((\"Storage Error: Could not open '%1' for writing.\").arg(name));\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nvoid WriteManager::scheduleWrite() {\n\t_weak.with([](WriteManager &that) {\n\t\tthat.writeScheduled();\n\t});\n}\n\nvoid WriteManager::writeScheduled() {\n\tif (writeOneScheduledNow() && !_scheduled.empty()) {\n\t\tscheduleWrite();\n\t}\n}\n\nvoid AsyncWriteManager::write(WriteEntry &&entry) {\n\tExpects(!_finished);\n\n\tif (!_manager) {\n\t\t_manager.emplace();\n\t}\n\t_manager->with([entry = std::move(entry)](WriteManager &manager) mutable {\n\t\tmanager.write(std::move(entry));\n\t});\n}\n\nvoid AsyncWriteManager::writeSync(WriteEntry &&entry) {\n\tExpects(!_finished);\n\n\tif (!_manager) {\n\t\t_manager.emplace();\n\t}\n\t_manager->with_sync([&](WriteManager &manager) {\n\t\tmanager.writeSync(std::move(entry));\n\t});\n}\n\nvoid AsyncWriteManager::sync() {\n\tif (_manager) {\n\t\t_manager->with_sync([](WriteManager &manager) {\n\t\t\tmanager.writeSyncAll();\n\t\t});\n\t}\n}\n\nvoid AsyncWriteManager::stop() {\n\tif (_manager) {\n\t\tsync();\n\t\t_manager.reset();\n\t}\n\t_finished = true;\n}\n\nAsyncWriteManager Manager;\n\n} // namespace\n\nQString ToFilePart(FileKey val) {\n\tQString result;\n\tresult.reserve(0x10);\n\tfor (int32 i = 0; i < 0x10; ++i) {\n\t\tuchar v = (val & 0x0F);\n\t\tresult.push_back((v < 0x0A) ? QChar('0' + v) : QChar('A' + (v - 0x0A)));\n\t\tval >>= 4;\n\t}\n\treturn result;\n}\n\nbool KeyAlreadyUsed(QString &name) {\n\tname += '0';\n\tif (QFileInfo::exists(name)) {\n\t\treturn true;\n\t}\n\tname[name.size() - 1] = '1';\n\tif (QFileInfo::exists(name)) {\n\t\treturn true;\n\t}\n\tname[name.size() - 1] = 's';\n\tif (QFileInfo::exists(name)) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nFileKey GenerateKey(const QString &basePath) {\n\tFileKey result;\n\tQString path;\n\tpath.reserve(basePath.size() + 0x11);\n\tpath += basePath;\n\tdo {\n\t\tresult = base::RandomValue<FileKey>();\n\t\tpath.resize(basePath.size());\n\t\tpath += ToFilePart(result);\n\t} while (!result || KeyAlreadyUsed(path));\n\n\treturn result;\n}\n\nvoid ClearKey(const FileKey &key, const QString &basePath) {\n\tQString name;\n\tname.reserve(basePath.size() + 0x11);\n\tname.append(basePath).append(ToFilePart(key)).append('0');\n\tQFile::remove(name);\n\tname[name.size() - 1] = '1';\n\tQFile::remove(name);\n\tname[name.size() - 1] = 's';\n\tQFile::remove(name);\n}\n\nbool CheckStreamStatus(QDataStream &stream) {\n\tif (stream.status() != QDataStream::Ok) {\n\t\tLOG((\"Bad data stream status: %1\").arg(stream.status()));\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nMTP::AuthKeyPtr CreateLocalKey(\n\t\tconst QByteArray &passcode,\n\t\tconst QByteArray &salt) {\n\tconst auto s = bytes::make_span(salt);\n\tconst auto hash = openssl::Sha512(s, bytes::make_span(passcode), s);\n\tconst auto iterationsCount = passcode.isEmpty()\n\t\t? 1 // Don't slow down for no password.\n\t\t: kStrongIterationsCount;\n\n\tauto key = MTP::AuthKey::Data{ { gsl::byte{} } };\n\tPKCS5_PBKDF2_HMAC(\n\t\treinterpret_cast<const char*>(hash.data()),\n\t\thash.size(),\n\t\treinterpret_cast<const unsigned char*>(s.data()),\n\t\ts.size(),\n\t\titerationsCount,\n\t\tEVP_sha512(),\n\t\tkey.size(),\n\t\treinterpret_cast<unsigned char*>(key.data()));\n\treturn std::make_shared<MTP::AuthKey>(key);\n}\n\nMTP::AuthKeyPtr CreateLegacyLocalKey(\n\t\tconst QByteArray &passcode,\n\t\tconst QByteArray &salt) {\n\tauto key = MTP::AuthKey::Data{ { gsl::byte{} } };\n\tconst auto iterationsCount = passcode.isEmpty()\n\t\t? LocalEncryptNoPwdIterCount // Don't slow down for no password.\n\t\t: LocalEncryptIterCount;\n\n\tPKCS5_PBKDF2_HMAC_SHA1(\n\t\tpasscode.constData(),\n\t\tpasscode.size(),\n\t\t(uchar*)salt.data(),\n\t\tsalt.size(),\n\t\titerationsCount,\n\t\tkey.size(),\n\t\t(uchar*)key.data());\n\n\treturn std::make_shared<MTP::AuthKey>(key);\n}\n\nFileReadDescriptor::~FileReadDescriptor() {\n\tif (version) {\n\t\tstream.setDevice(nullptr);\n\t\tif (buffer.isOpen()) {\n\t\t\tbuffer.close();\n\t\t}\n\t\tbuffer.setBuffer(nullptr);\n\t}\n}\n\nEncryptedDescriptor::EncryptedDescriptor() {\n}\n\nEncryptedDescriptor::EncryptedDescriptor(uint32 size) {\n\tuint32 fullSize = sizeof(uint32) + size;\n\tif (fullSize & 0x0F) fullSize += 0x10 - (fullSize & 0x0F);\n\tdata.reserve(fullSize);\n\n\tdata.resize(sizeof(uint32));\n\tbuffer.setBuffer(&data);\n\tbuffer.open(QIODevice::WriteOnly);\n\tbuffer.seek(sizeof(uint32));\n\tstream.setDevice(&buffer);\n\tstream.setVersion(QDataStream::Qt_5_1);\n}\n\nEncryptedDescriptor::~EncryptedDescriptor() {\n\tfinish();\n}\n\nvoid EncryptedDescriptor::finish() {\n\tif (stream.device()) stream.setDevice(nullptr);\n\tif (buffer.isOpen()) buffer.close();\n\tbuffer.setBuffer(nullptr);\n}\n\nFileWriteDescriptor::FileWriteDescriptor(\n\tconst FileKey &key,\n\tconst QString &basePath,\n\tbool sync)\n: FileWriteDescriptor(ToFilePart(key), basePath, sync) {\n}\n\nFileWriteDescriptor::FileWriteDescriptor(\n\tconst QString &name,\n\tconst QString &basePath,\n\tbool sync)\n: _basePath(basePath)\n, _sync(sync) {\n\tinit(name);\n}\n\nFileWriteDescriptor::~FileWriteDescriptor() {\n\tfinish();\n}\n\nvoid FileWriteDescriptor::init(const QString &name) {\n\t_base = _basePath + name;\n\t_buffer.setBuffer(&_safeData);\n\tconst auto opened = _buffer.open(QIODevice::WriteOnly);\n\tAssert(opened);\n\t_stream.setDevice(&_buffer);\n}\n\nvoid FileWriteDescriptor::writeData(const QByteArray &data) {\n\tif (!_stream.device()) {\n\t\treturn;\n\t}\n\t_stream << data;\n\tquint32 len = data.isNull() ? 0xffffffff : data.size();\n\tif (QSysInfo::ByteOrder != QSysInfo::BigEndian) {\n\t\tlen = qbswap(len);\n\t}\n\t_md5.feed(&len, sizeof(len));\n\t_md5.feed(data.constData(), data.size());\n\t_fullSize += sizeof(len) + data.size();\n}\n\nvoid FileWriteDescriptor::writeEncrypted(\n\tEncryptedDescriptor &data,\n\tconst MTP::AuthKeyPtr &key) {\n\twriteData(PrepareEncrypted(data, key));\n}\n\nvoid FileWriteDescriptor::finish() {\n\tif (!_stream.device()) {\n\t\treturn;\n\t}\n\n\t_stream.setDevice(nullptr);\n\t_md5.feed(&_fullSize, sizeof(_fullSize));\n\tqint32 version = AppVersion;\n\t_md5.feed(&version, sizeof(version));\n\t_md5.feed(TdfMagic, TdfMagicLen);\n\n\t_buffer.close();\n\n\tauto entry = WriteEntry{\n\t\t.basePath = _basePath,\n\t\t.base = _base,\n\t\t.data = _safeData,\n\t\t.md5 = QByteArray((const char*)_md5.result(), 0x10)\n\t};\n\tif (_sync) {\n\t\tManager.writeSync(std::move(entry));\n\t} else {\n\t\tManager.write(std::move(entry));\n\t}\n}\n\n[[nodiscard]] QByteArray PrepareEncrypted(\n\t\tEncryptedDescriptor &data,\n\t\tconst MTP::AuthKeyPtr &key) {\n\tdata.finish();\n\tQByteArray &toEncrypt(data.data);\n\n\t// prepare for encryption\n\tuint32 size = toEncrypt.size(), fullSize = size;\n\tif (fullSize & 0x0F) {\n\t\tfullSize += 0x10 - (fullSize & 0x0F);\n\t\ttoEncrypt.resize(fullSize);\n\t\tbase::RandomFill(toEncrypt.data() + size, fullSize - size);\n\t}\n\t*(uint32*)toEncrypt.data() = size;\n\tQByteArray encrypted(0x10 + fullSize, Qt::Uninitialized); // 128bit of sha1 - key128, sizeof(data), data\n\thashSha1(toEncrypt.constData(), toEncrypt.size(), encrypted.data());\n\tMTP::aesEncryptLocal(toEncrypt.constData(), encrypted.data() + 0x10, fullSize, key, encrypted.constData());\n\n\treturn encrypted;\n}\n\nbool ReadFile(\n\t\tFileReadDescriptor &result,\n\t\tconst QString &name,\n\t\tconst QString &basePath) {\n\tconst auto base = basePath + name;\n\n\t// detect order of read attempts\n\tQString toTry[2];\n\tconst auto modern = base + 's';\n\tif (QFileInfo::exists(modern)) {\n\t\ttoTry[0] = modern;\n\t} else {\n\t\t// Legacy way.\n\t\ttoTry[0] = base + '0';\n\t\tQFileInfo toTry0(toTry[0]);\n\t\tif (toTry0.exists()) {\n\t\t\ttoTry[1] = basePath + name + '1';\n\t\t\tQFileInfo toTry1(toTry[1]);\n\t\t\tif (toTry1.exists()) {\n\t\t\t\tQDateTime mod0 = toTry0.lastModified();\n\t\t\t\tQDateTime mod1 = toTry1.lastModified();\n\t\t\t\tif (mod0 < mod1) {\n\t\t\t\t\tqSwap(toTry[0], toTry[1]);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttoTry[1] = QString();\n\t\t\t}\n\t\t} else {\n\t\t\ttoTry[0][toTry[0].size() - 1] = '1';\n\t\t}\n\t}\n\tfor (int32 i = 0; i < 2; ++i) {\n\t\tQString fname(toTry[i]);\n\t\tif (fname.isEmpty()) break;\n\n\t\tQFile f(fname);\n\t\tif (!f.open(QIODevice::ReadOnly)) {\n\t\t\tDEBUG_LOG((\"App Info: failed to open '%1' for reading\"\n\t\t\t\t).arg(name));\n\t\t\tcontinue;\n\t\t}\n\n\t\t// check magic\n\t\tchar magic[TdfMagicLen];\n\t\tif (f.read(magic, TdfMagicLen) != TdfMagicLen) {\n\t\t\tDEBUG_LOG((\"App Info: failed to read magic from '%1'\"\n\t\t\t\t).arg(name));\n\t\t\tcontinue;\n\t\t}\n\t\tif (memcmp(magic, TdfMagic, TdfMagicLen)) {\n\t\t\tDEBUG_LOG((\"App Info: bad magic %1 in '%2'\").arg(\n\t\t\t\tLogs::mb(magic, TdfMagicLen).str(),\n\t\t\t\tname));\n\t\t\tcontinue;\n\t\t}\n\n\t\t// read app version\n\t\tqint32 version;\n\t\tif (f.read((char*)&version, sizeof(version)) != sizeof(version)) {\n\t\t\tDEBUG_LOG((\"App Info: failed to read version from '%1'\"\n\t\t\t\t).arg(name));\n\t\t\tcontinue;\n\t\t}\n\t\tif (version > AppVersion) {\n\t\t\tDEBUG_LOG((\"App Info: version too big %1 for '%2', my version %3\"\n\t\t\t\t).arg(version\n\t\t\t\t).arg(name\n\t\t\t\t).arg(AppVersion));\n\t\t\tcontinue;\n\t\t}\n\n\t\t// read data\n\t\tQByteArray bytes = f.read(f.size());\n\t\tint32 dataSize = bytes.size() - 16;\n\t\tif (dataSize < 0) {\n\t\t\tDEBUG_LOG((\"App Info: bad file '%1', could not read sign part\"\n\t\t\t\t).arg(name));\n\t\t\tcontinue;\n\t\t}\n\n\t\t// check signature\n\t\tHashMd5 md5;\n\t\tmd5.feed(bytes.constData(), dataSize);\n\t\tmd5.feed(&dataSize, sizeof(dataSize));\n\t\tmd5.feed(&version, sizeof(version));\n\t\tmd5.feed(magic, TdfMagicLen);\n\t\tif (memcmp(md5.result(), bytes.constData() + dataSize, 16)) {\n\t\t\tDEBUG_LOG((\"App Info: bad file '%1', signature did not match\"\n\t\t\t\t).arg(name));\n\t\t\tcontinue;\n\t\t}\n\n\t\tbytes.resize(dataSize);\n\t\tresult.data = bytes;\n\t\tbytes = QByteArray();\n\n\t\tresult.version = version;\n\t\tresult.buffer.setBuffer(&result.data);\n\t\tresult.buffer.open(QIODevice::ReadOnly);\n\t\tresult.stream.setDevice(&result.buffer);\n\t\tresult.stream.setVersion(QDataStream::Qt_5_1);\n\n\t\tif ((i == 0 && !toTry[1].isEmpty()) || i == 1) {\n\t\t\tQFile::remove(toTry[1 - i]);\n\t\t}\n\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool DecryptLocal(\n\t\tEncryptedDescriptor &result,\n\t\tconst QByteArray &encrypted,\n\t\tconst MTP::AuthKeyPtr &key) {\n\tif (encrypted.size() <= 16 || (encrypted.size() & 0x0F)) {\n\t\tLOG((\"App Error: bad encrypted part size: %1\").arg(encrypted.size()));\n\t\treturn false;\n\t}\n\tuint32 fullLen = encrypted.size() - 16;\n\n\tQByteArray decrypted;\n\tdecrypted.resize(fullLen);\n\tconst char *encryptedKey = encrypted.constData(), *encryptedData = encrypted.constData() + 16;\n\taesDecryptLocal(encryptedData, decrypted.data(), fullLen, key, encryptedKey);\n\tuchar sha1Buffer[20];\n\tif (memcmp(hashSha1(decrypted.constData(), decrypted.size(), sha1Buffer), encryptedKey, 16)) {\n\t\tLOG((\"App Info: bad decrypt key, data not decrypted - incorrect password?\"));\n\t\treturn false;\n\t}\n\n\tuint32 dataLen = *(const uint32*)decrypted.constData();\n\tif (dataLen > uint32(decrypted.size()) || dataLen <= fullLen - 16 || dataLen < sizeof(uint32)) {\n\t\tLOG((\"App Error: bad decrypted part size: %1, fullLen: %2, decrypted size: %3\").arg(dataLen).arg(fullLen).arg(decrypted.size()));\n\t\treturn false;\n\t}\n\n\tdecrypted.resize(dataLen);\n\tresult.data = decrypted;\n\tdecrypted = QByteArray();\n\n\tresult.buffer.setBuffer(&result.data);\n\tresult.buffer.open(QIODevice::ReadOnly);\n\tresult.buffer.seek(sizeof(uint32)); // skip len\n\tresult.stream.setDevice(&result.buffer);\n\tresult.stream.setVersion(QDataStream::Qt_5_1);\n\n\treturn true;\n}\n\nbool ReadEncryptedFile(\n\t\tFileReadDescriptor &result,\n\t\tconst QString &name,\n\t\tconst QString &basePath,\n\t\tconst MTP::AuthKeyPtr &key) {\n\tif (!ReadFile(result, name, basePath)) {\n\t\treturn false;\n\t}\n\tQByteArray encrypted;\n\tresult.stream >> encrypted;\n\n\tEncryptedDescriptor data;\n\tif (!DecryptLocal(data, encrypted, key)) {\n\t\tresult.stream.setDevice(nullptr);\n\t\tif (result.buffer.isOpen()) result.buffer.close();\n\t\tresult.buffer.setBuffer(nullptr);\n\t\tresult.data = QByteArray();\n\t\tresult.version = 0;\n\t\treturn false;\n\t}\n\n\tresult.stream.setDevice(0);\n\tif (result.buffer.isOpen()) {\n\t\tresult.buffer.close();\n\t}\n\tresult.buffer.setBuffer(0);\n\tresult.data = data.data;\n\tresult.buffer.setBuffer(&result.data);\n\tresult.buffer.open(QIODevice::ReadOnly);\n\tresult.buffer.seek(data.buffer.pos());\n\tresult.stream.setDevice(&result.buffer);\n\tresult.stream.setVersion(QDataStream::Qt_5_1);\n\n\treturn true;\n}\n\nbool ReadEncryptedFile(\n\t\tFileReadDescriptor &result,\n\t\tconst FileKey &fkey,\n\t\tconst QString &basePath,\n\t\tconst MTP::AuthKeyPtr &key) {\n\treturn ReadEncryptedFile(result, ToFilePart(fkey), basePath, key);\n}\n\nvoid Sync() {\n\tManager.sync();\n}\n\nvoid Finish() {\n\tManager.stop();\n}\n\n} // namespace details\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/details/storage_file_utilities.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"storage/storage_account.h\"\n\n#include <QtCore/QBuffer>\n\nnamespace Storage {\nnamespace details {\n\n[[nodiscard]] QString ToFilePart(FileKey val);\n[[nodiscard]] bool KeyAlreadyUsed(QString &name);\n[[nodiscard]] FileKey GenerateKey(const QString &basePath);\nvoid ClearKey(const FileKey &key, const QString &basePath);\n\n[[nodiscard]] bool CheckStreamStatus(QDataStream &stream);\n[[nodiscard]] MTP::AuthKeyPtr CreateLocalKey(\n\tconst QByteArray &passcode,\n\tconst QByteArray &salt);\n[[nodiscard]] MTP::AuthKeyPtr CreateLegacyLocalKey(\n\tconst QByteArray &passcode,\n\tconst QByteArray &salt);\n\nstruct FileReadDescriptor final {\n\t~FileReadDescriptor();\n\n\tint32 version = 0;\n\tQByteArray data;\n\tQBuffer buffer;\n\tQDataStream stream;\n};\n\nstruct EncryptedDescriptor final {\n\tEncryptedDescriptor();\n\texplicit EncryptedDescriptor(uint32 size);\n\t~EncryptedDescriptor();\n\n\tvoid finish();\n\n\tQByteArray data;\n\tQBuffer buffer;\n\tQDataStream stream;\n};\n\n[[nodiscard]] QByteArray PrepareEncrypted(\n\tEncryptedDescriptor &data,\n\tconst MTP::AuthKeyPtr &key);\n\nclass FileWriteDescriptor final {\npublic:\n\tFileWriteDescriptor(\n\t\tconst FileKey &key,\n\t\tconst QString &basePath,\n\t\tbool sync = false);\n\tFileWriteDescriptor(\n\t\tconst QString &name,\n\t\tconst QString &basePath,\n\t\tbool sync = false);\n\t~FileWriteDescriptor();\n\n\tvoid writeData(const QByteArray &data);\n\tvoid writeEncrypted(\n\t\tEncryptedDescriptor &data,\n\t\tconst MTP::AuthKeyPtr &key);\n\nprivate:\n\tvoid init(const QString &name);\n\tvoid finish();\n\n\tconst QString _basePath;\n\tQBuffer _buffer;\n\tQDataStream _stream;\n\tQByteArray _safeData;\n\tQString _base;\n\tHashMd5 _md5;\n\tint _fullSize = 0;\n\tbool _sync = false;\n\n};\n\nbool ReadFile(\n\tFileReadDescriptor &result,\n\tconst QString &name,\n\tconst QString &basePath);\n\nbool DecryptLocal(\n\tEncryptedDescriptor &result,\n\tconst QByteArray &encrypted,\n\tconst MTP::AuthKeyPtr &key);\n\nbool ReadEncryptedFile(\n\tFileReadDescriptor &result,\n\tconst QString &name,\n\tconst QString &basePath,\n\tconst MTP::AuthKeyPtr &key);\n\nbool ReadEncryptedFile(\n\tFileReadDescriptor &result,\n\tconst FileKey &fkey,\n\tconst QString &basePath,\n\tconst MTP::AuthKeyPtr &key);\n\nvoid Sync();\nvoid Finish();\n\n} // namespace details\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/details/storage_settings_scheme.h\"\n\n#include \"storage/details/storage_file_utilities.h\"\n#include \"storage/cache/storage_cache_database.h\"\n#include \"storage/serialize_common.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/chat/attach/attach_send_files_way.h\"\n#include \"ui/power_saving.h\"\n#include \"window/themes/window_theme.h\"\n#include \"core/update_checker.h\"\n#include \"platform/platform_specific.h\"\n#include \"boxes/send_files_box.h\"\n\nnamespace Storage {\nnamespace details {\nnamespace {\n\nusing Cache::Database;\n\n[[nodiscard]] bool NoTimeLimit(qint32 storedLimitValue) {\n\t// This is a workaround for a bug in storing the cache time limit.\n\t// See https://github.com/telegramdesktop/tdesktop/issues/5611\n\treturn !storedLimitValue\n\t\t|| (storedLimitValue == qint32(std::numeric_limits<int32>::max()))\n\t\t|| (storedLimitValue == qint32(std::numeric_limits<int64>::max()));\n}\n\n} // namespace\n\nbool ReadSetting(\n\t\tquint32 blockId,\n\t\tQDataStream &stream,\n\t\tint version,\n\t\tReadSettingsContext &context) {\n\tswitch (blockId) {\n\tcase dbiDcOptionOldOld: {\n\t\tquint32 dcId, port;\n\t\tQString host, ip;\n\t\tstream >> dcId >> host >> ip >> port;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.fallbackConfigLegacyDcOptions.constructAddOne(\n\t\t\tdcId,\n\t\t\t0,\n\t\t\tip.toStdString(),\n\t\t\tport,\n\t\t\t{});\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiDcOptionOld: {\n\t\tquint32 dcIdWithShift, port;\n\t\tqint32 flags;\n\t\tQString ip;\n\t\tstream >> dcIdWithShift >> flags >> ip >> port;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.fallbackConfigLegacyDcOptions.constructAddOne(\n\t\t\tdcIdWithShift,\n\t\t\tMTPDdcOption::Flags::from_raw(flags),\n\t\t\tip.toStdString(),\n\t\t\tport,\n\t\t\t{});\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiDcOptionsOld: {\n\t\tauto serialized = QByteArray();\n\t\tstream >> serialized;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.fallbackConfigLegacyDcOptions.constructFromSerialized(\n\t\t\tserialized);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiApplicationSettings: {\n\t\tauto serialized = QByteArray();\n\t\tstream >> serialized;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().addFromSerialized(serialized);\n\t} break;\n\n\tcase dbiForkgramSettings: {\n\t\tauto serialized = QByteArray();\n\t\tstream >> serialized;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().fork().addFromSerialized(serialized);\n\t} break;\n\n\tcase dbiChatSizeMaxOld: {\n\t\tqint32 maxSize;\n\t\tstream >> maxSize;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.fallbackConfigLegacyChatSizeMax = maxSize;\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiSavedGifsLimitOld: {\n\t\tqint32 limit;\n\t\tstream >> limit;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.fallbackConfigLegacySavedGifsLimit = limit;\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiStickersRecentLimitOld: {\n\t\tqint32 limit;\n\t\tstream >> limit;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.fallbackConfigLegacyStickersRecentLimit = limit;\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiStickersFavedLimitOld: {\n\t\tqint32 limit;\n\t\tstream >> limit;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.fallbackConfigLegacyStickersFavedLimit = limit;\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiMegagroupSizeMaxOld: {\n\t\tqint32 maxSize;\n\t\tstream >> maxSize;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.fallbackConfigLegacyMegagroupSizeMax = maxSize;\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiUser: {\n\t\tquint32 dcId;\n\t\tqint32 userId;\n\t\tstream >> userId >> dcId;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tDEBUG_LOG((\"MTP Info: user found, dc %1, uid %2\").arg(dcId).arg(userId));\n\t\tcontext.mtpLegacyMainDcId = dcId;\n\t\tcontext.mtpLegacyUserId = userId;\n\t} break;\n\n\tcase dbiKey: {\n\t\tqint32 dcId;\n\t\tstream >> dcId;\n\t\tauto key = Serialize::read<MTP::AuthKey::Data>(stream);\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.mtpLegacyKeys.push_back(std::make_shared<MTP::AuthKey>(\n\t\t\tMTP::AuthKey::Type::ReadFromFile,\n\t\t\tdcId,\n\t\t\tkey));\n\t} break;\n\n\tcase dbiMtpAuthorization: {\n\t\tauto serialized = QByteArray();\n\t\tstream >> serialized;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.mtpAuthorization = serialized;\n\t} break;\n\n\tcase dbiAutoStart: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcSetAutoStart(v == 1);\n\t} break;\n\n\tcase dbiStartMinimized: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcSetStartMinimized(v == 1);\n\t} break;\n\n\tcase dbiSendToMenu: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcSetSendToMenu(v == 1);\n\t} break;\n\n\tcase dbiUseExternalVideoPlayerOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\t} break;\n\n\tcase dbiCacheSettingsOld: {\n\t\tqint64 size;\n\t\tqint32 time;\n\t\tstream >> size >> time;\n\t\tif (!CheckStreamStatus(stream)\n\t\t\t|| size <= Database::Settings().maxDataSize\n\t\t\t|| (!NoTimeLimit(time) && time < 0)) {\n\t\t\treturn false;\n\t\t}\n\t\tcontext.cacheTotalSizeLimit = size;\n\t\tcontext.cacheTotalTimeLimit = NoTimeLimit(time) ? 0 : time;\n\t\tcontext.cacheBigFileTotalSizeLimit = size;\n\t\tcontext.cacheBigFileTotalTimeLimit = NoTimeLimit(time) ? 0 : time;\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiCacheSettings: {\n\t\tqint64 size, sizeBig;\n\t\tqint32 time, timeBig;\n\t\tstream >> size >> time >> sizeBig >> timeBig;\n\t\tif (!CheckStreamStatus(stream)\n\t\t\t|| size <= Database::Settings().maxDataSize\n\t\t\t|| sizeBig <= Database::Settings().maxDataSize\n\t\t\t|| (!NoTimeLimit(time) && time < 0)\n\t\t\t|| (!NoTimeLimit(timeBig) && timeBig < 0)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tcontext.cacheTotalSizeLimit = size;\n\t\tcontext.cacheTotalTimeLimit = NoTimeLimit(time) ? 0 : time;\n\t\tcontext.cacheBigFileTotalSizeLimit = sizeBig;\n\t\tcontext.cacheBigFileTotalTimeLimit = NoTimeLimit(timeBig) ? 0 : timeBig;\n\t} break;\n\n\tcase dbiPowerSaving: {\n\t\tqint32 settings;\n\t\tstream >> settings;\n\t\tif (!CheckStreamStatus(stream)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tPowerSaving::Set(PowerSaving::Flags::from_raw(settings));\n\t} break;\n\n\tcase dbiSoundFlashBounceNotifyOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setSoundNotify((v & 0x01) == 0x01);\n\t\tCore::App().settings().setFlashBounceNotify((v & 0x02) == 0x00);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiAutoDownloadOld: {\n\t\tqint32 photo, audio, gif;\n\t\tstream >> photo >> audio >> gif;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tusing namespace Data::AutoDownload;\n\t\tauto &settings = context.sessionSettings().autoDownload();\n\t\tconst auto disabled = [](qint32 value, qint32 mask) {\n\t\t\treturn (value & mask) != 0;\n\t\t};\n\t\tconst auto set = [&](Type type, qint32 value) {\n\t\t\tconstexpr auto kNoPrivate = qint32(0x01);\n\t\t\tconstexpr auto kNoGroups = qint32(0x02);\n\t\t\tif (disabled(value, kNoPrivate)) {\n\t\t\t\tsettings.setBytesLimit(Source::User, type, 0);\n\t\t\t}\n\t\t\tif (disabled(value, kNoGroups)) {\n\t\t\t\tsettings.setBytesLimit(Source::Group, type, 0);\n\t\t\t\tsettings.setBytesLimit(Source::Channel, type, 0);\n\t\t\t}\n\t\t};\n\t\tset(Type::Photo, photo);\n\t\tset(Type::VoiceMessage, audio);\n\t\tset(Type::AutoPlayGIF, gif);\n\t\tset(Type::AutoPlayVideoMessage, gif);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiAutoPlayOld: {\n\t\tqint32 gif;\n\t\tstream >> gif;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tif (!gif) {\n\t\t\tusing namespace Data::AutoDownload;\n\t\t\tauto &settings = context.sessionSettings().autoDownload();\n\t\t\tconst auto types = {\n\t\t\t\tType::AutoPlayGIF,\n\t\t\t\tType::AutoPlayVideo,\n\t\t\t\tType::AutoPlayVideoMessage,\n\t\t\t};\n\t\t\tconst auto sources = {\n\t\t\t\tSource::User,\n\t\t\t\tSource::Group,\n\t\t\t\tSource::Channel\n\t\t\t};\n\t\t\tfor (const auto source : sources) {\n\t\t\t\tfor (const auto type : types) {\n\t\t\t\t\tsettings.setBytesLimit(source, type, 0);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiDialogsModeOld: {\n\t\tqint32 enabled, modeInt;\n\t\tstream >> enabled >> modeInt;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiDialogsFiltersOld: {\n\t\tqint32 enabled;\n\t\tstream >> enabled;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.sessionSettings().setDialogsFiltersEnabled(enabled == 1);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiModerateModeOld: {\n\t\tqint32 enabled;\n\t\tstream >> enabled;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setModerateModeEnabled(enabled == 1);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiIncludeMutedOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setIncludeMutedCounter(v == 1);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiShowingSavedGifsOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiDesktopNotifyOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setDesktopNotify(v == 1);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiWindowsNotificationsOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiNativeNotificationsOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setNativeNotifications(v == 1);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiNotificationsCountOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setNotificationsCount((v > 0 ? v : 3));\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiNotificationsCornerOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setNotificationsCorner(static_cast<Core::Settings::ScreenCorner>((v >= 0 && v < 4) ? v : 2));\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiDialogsWidthRatioOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().updateDialogsWidthRatio(v / 1000000., false);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiLastSeenWarningSeenOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setLastSeenWarningSeen(v == 1);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiSessionSettings: {\n\t\tQByteArray v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.sessionSettings().addFromSerialized(v);\n\t} break;\n\n\tcase dbiWorkModeOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tconst auto newMode = [v] {\n\t\t\tusing WorkMode = Core::Settings::WorkMode;\n\t\t\tswitch (static_cast<WorkMode>(v)) {\n\t\t\tcase WorkMode::TrayOnly: return WorkMode::TrayOnly;\n\t\t\tcase WorkMode::WindowOnly: return WorkMode::WindowOnly;\n\t\t\t};\n\t\t\treturn WorkMode::WindowAndTray;\n\t\t}();\n\t\tCore::App().settings().setWorkMode(newMode);\n\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiTxtDomainStringOldOld: {\n\t\tQString v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiTxtDomainStringOld: {\n\t\tQString v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.fallbackConfigLegacyTxtDomainString = v;\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiConnectionTypeOldOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\t// const auto dbictAuto = 0;\n\t\t// const auto dbictHttpAuto = 1; // not used\n\t\tconst auto dbictHttpProxy = 2;\n\t\tconst auto dbictTcpProxy = 3;\n\t\t// const auto dbictProxiesListOld = 4;\n\t\t// const auto dbictProxiesList = 5;\n\n\t\tMTP::ProxyData proxy;\n\t\tswitch (v) {\n\t\tcase dbictHttpProxy:\n\t\tcase dbictTcpProxy: {\n\t\t\tqint32 port;\n\t\t\tstream >> proxy.host >> port >> proxy.user >> proxy.password;\n\t\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\t\tproxy.port = uint32(port);\n\t\t\tproxy.type = (v == dbictTcpProxy)\n\t\t\t\t? MTP::ProxyData::Type::Socks5\n\t\t\t\t: MTP::ProxyData::Type::Http;\n\t\t} break;\n\t\t};\n\t\tauto &proxySettings = Core::App().settings().proxy();\n\t\tproxySettings.setSelected(proxy ? proxy : MTP::ProxyData());\n\t\tproxySettings.setSettings(proxy\n\t\t\t? MTP::ProxyData::Settings::Enabled\n\t\t\t: MTP::ProxyData::Settings::System);\n\t\tproxySettings.list() = proxy\n\t\t\t? std::vector<MTP::ProxyData>{ 1, proxy }\n\t\t\t: std::vector<MTP::ProxyData>{};\n\t\tCore::App().refreshGlobalProxy();\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiConnectionTypeOld: {\n\t\tqint32 connectionType;\n\t\tstream >> connectionType;\n\t\tif (!CheckStreamStatus(stream)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tauto &proxySettings = Core::App().settings().proxy();\n\n\t\tconst auto legacyProxyTypeShift = 1024;\n\n\t\t// const auto dbictAuto = 0;\n\t\t// const auto dbictHttpAuto = 1; // not used\n\t\tconst auto dbictHttpProxy = 2;\n\t\tconst auto dbictTcpProxy = 3;\n\t\tconst auto dbictProxiesListOld = 4;\n\t\tconst auto dbictProxiesList = 5;\n\n\t\tconst auto readProxy = [&] {\n\t\t\tusing Type = MTP::ProxyData::Type;\n\t\t\tqint32 proxyType, port;\n\t\t\tMTP::ProxyData proxy;\n\t\t\tstream\n\t\t\t\t>> proxyType\n\t\t\t\t>> proxy.host\n\t\t\t\t>> port\n\t\t\t\t>> proxy.user\n\t\t\t\t>> proxy.password;\n\t\t\tproxy.port = port;\n\t\t\tproxy.type = (proxyType == dbictTcpProxy)\n\t\t\t\t? Type::Socks5\n\t\t\t\t: (proxyType == dbictHttpProxy)\n\t\t\t\t? Type::Http\n\t\t\t\t: (proxyType == legacyProxyTypeShift + int(Type::Socks5))\n\t\t\t\t? Type::Socks5\n\t\t\t\t: (proxyType == legacyProxyTypeShift + int(Type::Http))\n\t\t\t\t? Type::Http\n\t\t\t\t: (proxyType == legacyProxyTypeShift + int(Type::Mtproto))\n\t\t\t\t? Type::Mtproto\n\t\t\t\t: Type::None;\n\t\t\treturn proxy;\n\t\t};\n\t\tif (connectionType == dbictProxiesListOld\n\t\t\t|| connectionType == dbictProxiesList) {\n\t\t\tqint32 count = 0, index = 0;\n\t\t\tstream >> count >> index;\n\t\t\tqint32 settings = 0, calls = 0;\n\t\t\tif (connectionType == dbictProxiesList) {\n\t\t\t\tstream >> settings >> calls;\n\t\t\t} else if (std::abs(index) > count) {\n\t\t\t\tcalls = 1;\n\t\t\t\tindex -= (index > 0 ? count : -count);\n\t\t\t}\n\n\t\t\tauto list = std::vector<MTP::ProxyData>();\n\t\t\tfor (auto i = 0; i < count; ++i) {\n\t\t\t\tconst auto proxy = readProxy();\n\t\t\t\tif (proxy) {\n\t\t\t\t\tlist.push_back(proxy);\n\t\t\t\t} else if (index < -int64(list.size())) {\n\t\t\t\t\t++index;\n\t\t\t\t} else if (index > list.size()) {\n\t\t\t\t\t--index;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!CheckStreamStatus(stream)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tproxySettings.list() = list;\n\t\t\tif (connectionType == dbictProxiesListOld) {\n\t\t\t\tsettings = static_cast<qint32>(\n\t\t\t\t\t(index > 0 && index <= list.size()\n\t\t\t\t\t\t? MTP::ProxyData::Settings::Enabled\n\t\t\t\t\t\t: MTP::ProxyData::Settings::System));\n\t\t\t\tindex = std::abs(index);\n\t\t\t}\n\t\t\tproxySettings.setSelected((index > 0 && index <= list.size())\n\t\t\t\t? list[index - 1]\n\t\t\t\t: MTP::ProxyData());\n\n\t\t\tconst auto unchecked = static_cast<MTP::ProxyData::Settings>(settings);\n\t\t\tswitch (unchecked) {\n\t\t\tcase MTP::ProxyData::Settings::Enabled:\n\t\t\t\tproxySettings.setSettings(proxySettings.selected()\n\t\t\t\t\t? MTP::ProxyData::Settings::Enabled\n\t\t\t\t\t: MTP::ProxyData::Settings::System);\n\t\t\t\tbreak;\n\t\t\tcase MTP::ProxyData::Settings::Disabled:\n\t\t\tcase MTP::ProxyData::Settings::System:\n\t\t\t\tproxySettings.setSettings(unchecked);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tproxySettings.setSettings(MTP::ProxyData::Settings::System);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tproxySettings.setUseProxyForCalls(calls == 1);\n\t\t} else {\n\t\t\tconst auto proxy = readProxy();\n\t\t\tif (!CheckStreamStatus(stream)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (proxy) {\n\t\t\t\tproxySettings.list() = { 1, proxy };\n\t\t\t\tproxySettings.setSelected(proxy);\n\t\t\t\tproxySettings.setSettings((connectionType == dbictTcpProxy\n\t\t\t\t\t|| connectionType == dbictHttpProxy)\n\t\t\t\t\t\t? MTP::ProxyData::Settings::Enabled\n\t\t\t\t\t\t: MTP::ProxyData::Settings::System);\n\t\t\t} else {\n\t\t\t\tproxySettings.list() = {};\n\t\t\t\tproxySettings.setSelected(MTP::ProxyData());\n\t\t\t\tproxySettings.setSettings(MTP::ProxyData::Settings::System);\n\t\t\t}\n\t\t}\n\t\tCore::App().refreshGlobalProxy();\n\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiThemeKeyOld: {\n\t\tquint64 key = 0;\n\t\tstream >> key;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tDEBUG_LOG((\"Theme: read key legacy: %1\").arg(key));\n\t\tcontext.themeKeyLegacy = key;\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiThemeKey: {\n\t\tquint64 keyDay = 0, keyNight = 0;\n\t\tquint32 nightMode = 0;\n\t\tstream >> keyDay >> keyNight >> nightMode;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tDEBUG_LOG((\"Theme: read keys (night: %1) day_key: %2, night_key: %3, \"\n\t\t\t).arg(Logs::b(nightMode == 1)\n\t\t\t).arg(keyDay\n\t\t\t).arg(keyNight));\n\t\tcontext.themeKeyDay = keyDay;\n\t\tcontext.themeKeyNight = keyNight;\n\t\tWindow::Theme::SetNightModeValue(nightMode == 1);\n\t} break;\n\n\tcase dbiBackgroundKey: {\n\t\tquint64 keyDay = 0, keyNight = 0;\n\t\tstream >> keyDay >> keyNight;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.backgroundKeyDay = keyDay;\n\t\tcontext.backgroundKeyNight = keyNight;\n\t\tcontext.backgroundKeysRead = true;\n\t} break;\n\n\tcase dbiLangPackKey: {\n\t\tquint64 langPackKey = 0;\n\t\tstream >> langPackKey;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.langPackKey = langPackKey;\n\t} break;\n\n\tcase dbiLanguagesKey: {\n\t\tquint64 languagesKey = 0;\n\t\tstream >> languagesKey;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.languagesKey = languagesKey;\n\t} break;\n\n\tcase dbiTryIPv6Old: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\t\tCore::App().settings().proxy().setTryIPv6(v == 1);\n\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiSeenTrayTooltip: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcSetSeenTrayTooltip(v == 1);\n\t} break;\n\n\tcase dbiAutoUpdate: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcSetAutoUpdate(v == 1);\n\t\tif (!Core::UpdaterDisabled() && !cAutoUpdate()) {\n\t\t\tCore::UpdateChecker().stop();\n\t\t}\n\t} break;\n\n\tcase dbiLastUpdateCheck: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcSetLastUpdateCheck(v);\n\t} break;\n\n\tcase dbiScaleOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tSetScaleChecked([&] {\n\t\t\tconstexpr auto kAuto = 0;\n\t\t\tconstexpr auto kOne = 1;\n\t\t\tconstexpr auto kOneAndQuarter = 2;\n\t\t\tconstexpr auto kOneAndHalf = 3;\n\t\t\tconstexpr auto kTwo = 4;\n\t\t\tswitch (v) {\n\t\t\tcase kAuto: return style::kScaleAuto;\n\t\t\tcase kOne: return 100;\n\t\t\tcase kOneAndQuarter: return 125;\n\t\t\tcase kOneAndHalf: return 150;\n\t\t\tcase kTwo: return 200;\n\t\t\t}\n\t\t\treturn cConfigScale();\n\t\t}());\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiScalePercent: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\t// If cConfigScale() has value then it was set via command line.\n\t\tif (cConfigScale() == style::kScaleAuto) {\n\t\t\tSetScaleChecked(v);\n\t\t}\n\t} break;\n\n\tcase dbiLangOld: { // deprecated\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiLangFileOld: { // deprecated\n\t\tQString v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiWindowPositionOld: {\n\t\tauto position = Core::WindowPosition();\n\t\tif (!CheckStreamStatus(stream)) {\n\t\t\treturn false;\n\t\t}\n\t\tstream >> position.x >> position.y >> position.w >> position.h;\n\t\tstream >> position.moncrc >> position.maximized;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tDEBUG_LOG((\"Window Pos: Read from legacy storage %1, %2, %3, %4 (scale %5%, maximized %6)\")\n\t\t\t.arg(position.x)\n\t\t\t.arg(position.y)\n\t\t\t.arg(position.w)\n\t\t\t.arg(position.h)\n\t\t\t.arg(position.scale)\n\t\t\t.arg(Logs::b(position.maximized)));\n\n\t\tCore::App().settings().setWindowPosition(position);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiLoggedPhoneNumberOld: { // deprecated\n\t\tQString v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiMutePeerOld: { // deprecated\n\t\tquint64 peerId;\n\t\tstream >> peerId;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiMutedPeersOld: { // deprecated\n\t\tquint32 count;\n\t\tstream >> count;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tfor (uint32 i = 0; i < count; ++i) {\n\t\t\tquint64 peerId;\n\t\t\tstream >> peerId;\n\t\t}\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiSendKeyOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tusing SendSettings = Ui::InputSubmitSettings;\n\t\tconst auto unchecked = static_cast<SendSettings>(v);\n\n\t\tif (unchecked != SendSettings::Enter\n\t\t\t&& unchecked != SendSettings::CtrlEnter) {\n\t\t\treturn false;\n\t\t}\n\t\tCore::App().settings().setSendSubmitWay(unchecked);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiCatsAndDogsOld: { // deprecated\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiTileBackgroundOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tbool tile = (version < 8005 && !context.legacyHasCustomDayBackground)\n\t\t\t? false\n\t\t\t: (v == 1);\n\t\tif (Window::Theme::IsNightMode()) {\n\t\t\tcontext.tileDay = tile;\n\t\t} else {\n\t\t\tcontext.tileNight = tile;\n\t\t}\n\t\tcontext.tileRead = true;\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiTileBackground: {\n\t\tqint32 tileDay, tileNight;\n\t\tstream >> tileDay >> tileNight;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.tileDay = tileDay;\n\t\tcontext.tileNight = tileNight;\n\t\tcontext.tileRead = true;\n\t} break;\n\n\tcase dbiAdaptiveForWideOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setAdaptiveForWide(v == 1);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiAutoLockOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setAutoLock(v);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiReplaceEmojiOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setReplaceEmoji(v == 1);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiSuggestEmojiOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setSuggestEmoji(v == 1);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiSuggestStickersByEmojiOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setSuggestStickersByEmoji(v == 1);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiDefaultAttach: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\t} break;\n\n\tcase dbiNotifyViewOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tconst auto newView = [&] {\n\t\t\tusing Notify = Core::Settings::NotifyView;\n\t\t\tswitch (static_cast<Notify>(v)) {\n\t\t\tcase Notify::ShowNothing: return Notify::ShowNothing;\n\t\t\tcase Notify::ShowName: return Notify::ShowName;\n\t\t\t}\n\t\t\treturn Notify::ShowPreview;\n\t\t}();\n\t\tCore::App().settings().setNotifyView(newView);\n\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiAskDownloadPathOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setAskDownloadPath(v == 1);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiDownloadPathOldOld: {\n\t\tQString v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n#ifndef OS_WIN_STORE\n\t\tif (!v.isEmpty() && v != FileDialog::Tmp() && !v.endsWith('/')) {\n\t\t\tv += '/';\n\t\t}\n\t\tCore::App().settings().setDownloadPathBookmark(QByteArray());\n\t\tCore::App().settings().setDownloadPath(v);\n#endif // OS_WIN_STORE\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiDownloadPathOld: {\n\t\tQString v;\n\t\tQByteArray bookmark;\n\t\tstream >> v >> bookmark;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n#ifndef OS_WIN_STORE\n\t\tif (!v.isEmpty() && v != FileDialog::Tmp() && !v.endsWith('/')) {\n\t\t\tv += '/';\n\t\t}\n\t\tCore::App().settings().setDownloadPathBookmark(bookmark);\n\t\tCore::App().settings().setDownloadPath(v);\n\t\tpsDownloadPathEnableAccess();\n#endif // OS_WIN_STORE\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiCompressPastedImageOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tauto way = Ui::SendFilesWay();\n\t\tway.setGroupFiles(v == 1);\n\t\tway.setSendImagesAsPhotos(v == 1);\n\t\tCore::App().settings().setSendFilesWay(way);\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiEmojiTabOld: { // deprecated\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiRecentEmojiOldOldOld: {\n\t\tauto v = QVector<QPair<uint32, ushort>>();\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tif (!v.isEmpty()) {\n\t\t\tauto p = QVector<QPair<QString, ushort>>();\n\t\t\tp.reserve(v.size());\n\t\t\tfor (auto &item : v) {\n\t\t\t\tauto oldKey = uint64(item.first);\n\t\t\t\tswitch (oldKey) {\n\t\t\t\tcase 0xD83CDDEFLLU: oldKey = 0xD83CDDEFD83CDDF5LLU; break;\n\t\t\t\tcase 0xD83CDDF0LLU: oldKey = 0xD83CDDF0D83CDDF7LLU; break;\n\t\t\t\tcase 0xD83CDDE9LLU: oldKey = 0xD83CDDE9D83CDDEALLU; break;\n\t\t\t\tcase 0xD83CDDE8LLU: oldKey = 0xD83CDDE8D83CDDF3LLU; break;\n\t\t\t\tcase 0xD83CDDFALLU: oldKey = 0xD83CDDFAD83CDDF8LLU; break;\n\t\t\t\tcase 0xD83CDDEBLLU: oldKey = 0xD83CDDEBD83CDDF7LLU; break;\n\t\t\t\tcase 0xD83CDDEALLU: oldKey = 0xD83CDDEAD83CDDF8LLU; break;\n\t\t\t\tcase 0xD83CDDEELLU: oldKey = 0xD83CDDEED83CDDF9LLU; break;\n\t\t\t\tcase 0xD83CDDF7LLU: oldKey = 0xD83CDDF7D83CDDFALLU; break;\n\t\t\t\tcase 0xD83CDDECLLU: oldKey = 0xD83CDDECD83CDDE7LLU; break;\n\t\t\t\t}\n\t\t\t\tauto id = Ui::Emoji::IdFromOldKey(oldKey);\n\t\t\t\tif (!id.isEmpty()) {\n\t\t\t\t\tp.push_back(qMakePair(id, item.second));\n\t\t\t\t}\n\t\t\t}\n\t\t\tCore::App().settings().setLegacyRecentEmojiPreload(std::move(p));\n\t\t}\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiRecentEmojiOldOld: {\n\t\tauto v = QVector<QPair<uint64, ushort>>();\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tif (!v.isEmpty()) {\n\t\t\tauto p = QVector<QPair<QString, ushort>>();\n\t\t\tp.reserve(v.size());\n\t\t\tfor (auto &item : v) {\n\t\t\t\tauto id = Ui::Emoji::IdFromOldKey(item.first);\n\t\t\t\tif (!id.isEmpty()) {\n\t\t\t\t\tp.push_back(qMakePair(id, item.second));\n\t\t\t\t}\n\t\t\t}\n\t\t\tCore::App().settings().setLegacyRecentEmojiPreload(std::move(p));\n\t\t}\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiRecentEmojiOld: {\n\t\tauto v = QVector<QPair<QString, ushort>>();\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setLegacyRecentEmojiPreload(std::move(v));\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiRecentStickers: {\n\t\tRecentStickerPreload v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcSetRecentStickersPreload(v);\n\t} break;\n\n\tcase dbiEmojiVariantsOldOld: {\n\t\tauto v = QMap<uint32, uint64>();\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tauto variants = QMap<QString, int>();\n\t\tfor (auto i = v.cbegin(), e = v.cend(); i != e; ++i) {\n\t\t\tauto id = Ui::Emoji::IdFromOldKey(static_cast<uint64>(i.key()));\n\t\t\tif (!id.isEmpty()) {\n\t\t\t\tauto index = Ui::Emoji::ColorIndexFromOldKey(i.value());\n\t\t\t\tvariants.insert(id, index);\n\t\t\t}\n\t\t}\n\t\tCore::App().settings().setLegacyEmojiVariants(std::move(variants));\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiEmojiVariantsOld: {\n\t\tauto v = QMap<QString, int>();\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setLegacyEmojiVariants(std::move(v));\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiHiddenPinnedMessagesOld: {\n\t\tauto v = QMap<uint64, int32>();\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tfor (auto i = v.begin(), e = v.end(); i != e; ++i) {\n\t\t\tcontext.sessionSettings().setHiddenPinnedMessageId(\n\t\t\t\tDeserializePeerId(i.key()),\n\t\t\t\tMsgId(0), // topicRootId\n\t\t\t\tPeerId(0), // monoforumPeerId\n\t\t\t\tMsgId(i.value()));\n\t\t}\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiDialogLastPath: {\n\t\tQString path;\n\t\tstream >> path;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcSetDialogLastPath(path);\n\t} break;\n\n\tcase dbiSongVolumeOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setSongVolume(std::clamp(v / 1e6, 0., 1.));\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiVideoVolumeOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tCore::App().settings().setVideoVolume(std::clamp(v / 1e6, 0., 1.));\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiPlaybackSpeedOld: {\n\t\tqint32 v;\n\t\tstream >> v;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tif (v == 2) {\n\t\t\tCore::App().settings().setVoicePlaybackSpeed(2.);\n\t\t\tCore::App().settings().setAudioPlaybackSpeed(2.);\n\t\t}\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiCallSettingsOld: {\n\t\tQByteArray callSettings;\n\t\tstream >> callSettings;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tQDataStream settingsStream(&callSettings, QIODevice::ReadOnly);\n\t\tsettingsStream.setVersion(QDataStream::Qt_5_1);\n\t\tQString outputDeviceID;\n\t\tQString inputDeviceID;\n\t\tqint32 outputVolume;\n\t\tqint32 inputVolume;\n\t\tqint32 duckingEnabled;\n\n\t\tsettingsStream >> outputDeviceID;\n\t\tsettingsStream >> outputVolume;\n\t\tsettingsStream >> inputDeviceID;\n\t\tsettingsStream >> inputVolume;\n\t\tsettingsStream >> duckingEnabled;\n\t\tif (CheckStreamStatus(settingsStream)) {\n\t\t\tauto &app = Core::App().settings();\n\t\t\tapp.setCallPlaybackDeviceId(outputDeviceID);\n\t\t\tapp.setCallCaptureDeviceId(inputDeviceID);\n\t\t\tapp.setCallOutputVolume(outputVolume);\n\t\t\tapp.setCallInputVolume(inputVolume);\n\t\t\tapp.setCallAudioDuckingEnabled(duckingEnabled);\n\t\t}\n\t\tcontext.legacyRead = true;\n\t} break;\n\n\tcase dbiFallbackProductionConfig: {\n\t\tQByteArray config;\n\t\tstream >> config;\n\t\tif (!CheckStreamStatus(stream)) return false;\n\n\t\tcontext.fallbackConfig = config;\n\t} break;\n\n\tdefault:\n\t\tLOG((\"App Error: unknown blockId in _readSetting: %1\").arg(blockId));\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\nvoid ApplyReadFallbackConfig(ReadSettingsContext &context) {\n\tif (context.fallbackConfig.isEmpty()) {\n\t\tauto &config = Core::App().fallbackProductionConfig();\n\t\tconfig.dcOptions().addFromOther(\n\t\t\tstd::move(context.fallbackConfigLegacyDcOptions));\n\t\tif (context.fallbackConfigLegacyChatSizeMax > 0) {\n\t\t\tconfig.setChatSizeMax(context.fallbackConfigLegacyChatSizeMax);\n\t\t}\n\t\tif (context.fallbackConfigLegacyStickersRecentLimit > 0) {\n\t\t\tconfig.setStickersRecentLimit(\n\t\t\t\tcontext.fallbackConfigLegacyStickersRecentLimit);\n\t\t}\n\t\tif (context.fallbackConfigLegacyMegagroupSizeMax > 0) {\n\t\t\tconfig.setMegagroupSizeMax(\n\t\t\t\tcontext.fallbackConfigLegacyMegagroupSizeMax);\n\t\t}\n\t\tif (!context.fallbackConfigLegacyTxtDomainString.isEmpty()) {\n\t\t\tconfig.setTxtDomainString(\n\t\t\t\tcontext.fallbackConfigLegacyTxtDomainString);\n\t\t}\n\t} else {\n\t\tCore::App().constructFallbackProductionConfig(context.fallbackConfig);\n\t}\n}\n\n} // namespace details\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/details/storage_settings_scheme.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"main/main_session_settings.h\"\n#include \"storage/storage_account.h\"\n\nnamespace MTP {\nclass AuthKey;\n} // namespace MTP\n\nnamespace Storage {\nnamespace details {\n\nstruct ReadSettingsContext {\n\t[[nodiscard]] Main::SessionSettings &sessionSettings() {\n\t\tif (!sessionSettingsStorage) {\n\t\t\tsessionSettingsStorage\n\t\t\t\t= std::make_unique<Main::SessionSettings>();\n\t\t}\n\t\treturn *sessionSettingsStorage;\n\t}\n\n\t// This field is read in ReadSetting.\n\tbool legacyHasCustomDayBackground = false;\n\n\t// Those fields are written in ReadSetting.\n\tMTP::DcOptions fallbackConfigLegacyDcOptions\n\t\t= MTP::DcOptions(MTP::Environment::Production);\n\tqint32 fallbackConfigLegacyChatSizeMax = 0;\n\tqint32 fallbackConfigLegacySavedGifsLimit = 0;\n\tqint32 fallbackConfigLegacyStickersRecentLimit = 0;\n\tqint32 fallbackConfigLegacyStickersFavedLimit = 0;\n\tqint32 fallbackConfigLegacyMegagroupSizeMax = 0;\n\tQString fallbackConfigLegacyTxtDomainString;\n\tQByteArray fallbackConfig;\n\n\tqint64 cacheTotalSizeLimit = 0;\n\tqint32 cacheTotalTimeLimit = 0;\n\tqint64 cacheBigFileTotalSizeLimit = 0;\n\tqint32 cacheBigFileTotalTimeLimit = 0;\n\n\tstd::unique_ptr<Main::SessionSettings> sessionSettingsStorage;\n\n\tFileKey themeKeyLegacy = 0;\n\tFileKey themeKeyDay = 0;\n\tFileKey themeKeyNight = 0;\n\tFileKey backgroundKeyDay = 0;\n\tFileKey backgroundKeyNight = 0;\n\tbool backgroundKeysRead = false;\n\tbool tileDay = false;\n\tbool tileNight = true;\n\tbool tileRead = false;\n\tFileKey langPackKey = 0;\n\tFileKey languagesKey = 0;\n\n\tQByteArray mtpAuthorization;\n\tstd::vector<std::shared_ptr<MTP::AuthKey>> mtpLegacyKeys;\n\tqint32 mtpLegacyMainDcId = 0;\n\tqint32 mtpLegacyUserId = 0;\n\n\tbool legacyRead = false;\n};\n\n[[nodiscard]] bool ReadSetting(\n\tquint32 blockId,\n\tQDataStream &stream,\n\tint version,\n\tReadSettingsContext &context);\n\nvoid ApplyReadFallbackConfig(ReadSettingsContext &context);\n\nenum {\n\tdbiKey = 0x00,\n\tdbiUser = 0x01,\n\tdbiDcOptionOldOld = 0x02,\n\tdbiChatSizeMaxOld = 0x03,\n\tdbiMutePeerOld = 0x04,\n\tdbiSendKeyOld = 0x05,\n\tdbiAutoStart = 0x06,\n\tdbiStartMinimized = 0x07,\n\tdbiSoundFlashBounceNotifyOld = 0x08,\n\tdbiWorkModeOld = 0x09,\n\tdbiSeenTrayTooltip = 0x0a,\n\tdbiDesktopNotifyOld = 0x0b,\n\tdbiAutoUpdate = 0x0c,\n\tdbiLastUpdateCheck = 0x0d,\n\tdbiWindowPositionOld = 0x0e,\n\tdbiConnectionTypeOldOld = 0x0f,\n\t// 0x10 reserved\n\tdbiDefaultAttach = 0x11,\n\tdbiCatsAndDogsOld = 0x12,\n\tdbiReplaceEmojiOld = 0x13,\n\tdbiAskDownloadPathOld = 0x14,\n\tdbiDownloadPathOldOld = 0x15,\n\tdbiScaleOld = 0x16,\n\tdbiEmojiTabOld = 0x17,\n\tdbiRecentEmojiOldOldOld = 0x18,\n\tdbiLoggedPhoneNumberOld = 0x19,\n\tdbiMutedPeersOld = 0x1a,\n\t// 0x1b reserved\n\tdbiNotifyViewOld = 0x1c,\n\tdbiSendToMenu = 0x1d,\n\tdbiCompressPastedImageOld = 0x1e,\n\tdbiLangOld = 0x1f,\n\tdbiLangFileOld = 0x20,\n\tdbiTileBackgroundOld = 0x21,\n\tdbiAutoLockOld = 0x22,\n\tdbiDialogLastPath = 0x23,\n\tdbiRecentEmojiOldOld = 0x24,\n\tdbiEmojiVariantsOldOld = 0x25,\n\tdbiRecentStickers = 0x26,\n\tdbiDcOptionOld = 0x27,\n\tdbiTryIPv6Old = 0x28,\n\tdbiSongVolumeOld = 0x29,\n\tdbiWindowsNotificationsOld = 0x30,\n\tdbiIncludeMutedOld = 0x31,\n\tdbiMegagroupSizeMaxOld = 0x32,\n\tdbiDownloadPathOld = 0x33,\n\tdbiAutoDownloadOld = 0x34,\n\tdbiSavedGifsLimitOld = 0x35,\n\tdbiShowingSavedGifsOld = 0x36,\n\tdbiAutoPlayOld = 0x37,\n\tdbiAdaptiveForWideOld = 0x38,\n\tdbiHiddenPinnedMessagesOld = 0x39,\n\tdbiRecentEmojiOld = 0x3a,\n\tdbiEmojiVariantsOld = 0x3b,\n\tdbiDialogsModeOld = 0x40,\n\tdbiModerateModeOld = 0x41,\n\tdbiVideoVolumeOld = 0x42,\n\tdbiStickersRecentLimitOld = 0x43,\n\tdbiNativeNotificationsOld = 0x44,\n\tdbiNotificationsCountOld = 0x45,\n\tdbiNotificationsCornerOld = 0x46,\n\tdbiThemeKeyOld = 0x47,\n\tdbiDialogsWidthRatioOld = 0x48,\n\tdbiUseExternalVideoPlayerOld = 0x49,\n\tdbiDcOptionsOld = 0x4a,\n\tdbiMtpAuthorization = 0x4b,\n\tdbiLastSeenWarningSeenOld = 0x4c,\n\tdbiSessionSettings = 0x4d,\n\tdbiLangPackKey = 0x4e,\n\tdbiConnectionTypeOld = 0x4f,\n\tdbiStickersFavedLimitOld = 0x50,\n\tdbiSuggestStickersByEmojiOld = 0x51,\n\tdbiSuggestEmojiOld = 0x52,\n\tdbiTxtDomainStringOldOld = 0x53,\n\tdbiThemeKey = 0x54,\n\tdbiTileBackground = 0x55,\n\tdbiCacheSettingsOld = 0x56,\n\tdbiPowerSaving = 0x57,\n\tdbiScalePercent = 0x58,\n\tdbiPlaybackSpeedOld = 0x59,\n\tdbiLanguagesKey = 0x5a,\n\tdbiCallSettingsOld = 0x5b,\n\tdbiCacheSettings = 0x5c,\n\tdbiTxtDomainStringOld = 0x5d,\n\tdbiApplicationSettings = 0x5e,\n\tdbiDialogsFiltersOld = 0x5f,\n\tdbiFallbackProductionConfig = 0x60,\n\tdbiBackgroundKey = 0x61,\n\tdbiForkgramSettings = 0x62,\n\n\tdbiEncryptedWithSalt = 333,\n\tdbiEncrypted = 444,\n\n\t// 500-600 reserved\n\n\tdbiVersion = 666,\n};\n\n} // namespace details\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/download_manager_mtproto.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/download_manager_mtproto.h\"\n\n#include \"mtproto/facade.h\"\n#include \"mtproto/mtproto_auth_key.h\"\n#include \"mtproto/mtproto_response.h\"\n#include \"main/main_session.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"apiwrap.h\"\n#include \"base/openssl_help.h\"\n\nnamespace Storage {\nnamespace {\n\nconstexpr auto kKillSessionTimeout = 15 * crl::time(1000);\nconstexpr auto kStartWaitedInSession = 4 * kDownloadPartSize;\nconstexpr auto kMaxWaitedInSession = 16 * kDownloadPartSize;\nconstexpr auto kStartSessionsCount = 1;\nconstexpr auto kMaxSessionsCount = 8;\nconstexpr auto kMaxTrackedSessionRemoves = 64;\nconstexpr auto kRetryAddSessionTimeout = 8 * crl::time(1000);\nconstexpr auto kRetryAddSessionSuccesses = 3;\nconstexpr auto kMaxTrackedSuccesses = kRetryAddSessionSuccesses\n\t* kMaxTrackedSessionRemoves;\nconstexpr auto kRemoveSessionAfterTimeouts = 4;\nconstexpr auto kResetDownloadPrioritiesTimeout = crl::time(200);\nconstexpr auto kBadRequestDurationThreshold = 8 * crl::time(1000);\n\n// Each (session remove by timeouts) we wait for time:\n// kRetryAddSessionTimeout * max(removesCount, kMaxTrackedSessionRemoves)\n// and for successes in all remaining sessions:\n// kRetryAddSessionSuccesses * max(removesCount, kMaxTrackedSessionRemoves)\n\n} // namespace\n\nvoid DownloadManagerMtproto::Queue::enqueue(\n\t\tnot_null<Task*> task,\n\t\tint priority) {\n\tconst auto position = ranges::find_if(_tasks, [&](const Enqueued &task) {\n\t\treturn task.priority <= priority;\n\t}) - begin(_tasks);\n\tconst auto now = ranges::find(_tasks, task, &Enqueued::task);\n\tconst auto i = [&] {\n\t\tif (now != end(_tasks)) {\n\t\t\t(now->priority = priority);\n\t\t\treturn now;\n\t\t}\n\t\t_tasks.push_back({ task, priority });\n\t\treturn end(_tasks) - 1;\n\t}();\n\tconst auto j = begin(_tasks) + position;\n\tif (j < i) {\n\t\tstd::rotate(j, i, i + 1);\n\t} else if (j > i + 1) {\n\t\tstd::rotate(i, i + 1, j);\n\t}\n}\n\nvoid DownloadManagerMtproto::Queue::remove(not_null<Task*> task) {\n\t_tasks.erase(ranges::remove(_tasks, task, &Enqueued::task), end(_tasks));\n}\n\nvoid DownloadManagerMtproto::Queue::resetGeneration() {\n\tconst auto from = ranges::find(_tasks, 0, &Enqueued::priority);\n\tfor (auto &task : ranges::make_subrange(from, end(_tasks))) {\n\t\tif (task.priority) {\n\t\t\tAssert(task.priority == -1);\n\t\t\tbreak;\n\t\t}\n\t\ttask.priority = -1;\n\t}\n}\n\nbool DownloadManagerMtproto::Queue::empty() const {\n\treturn _tasks.empty();\n}\n\nauto DownloadManagerMtproto::Queue::nextTask(bool onlyHighestPriority) const\n-> Task* {\n\tif (_tasks.empty()) {\n\t\treturn nullptr;\n\t}\n\tconst auto highestPriority = _tasks.front().priority;\n\tconst auto notHighestPriority = [&](const Enqueued &enqueued) {\n\t\treturn (enqueued.priority != highestPriority);\n\t};\n\tconst auto till = (onlyHighestPriority && highestPriority > 0)\n\t\t? ranges::find_if(_tasks, notHighestPriority)\n\t\t: end(_tasks);\n\tconst auto readyToRequest = [&](const Enqueued &enqueued) {\n\t\treturn enqueued.task->readyToRequest();\n\t};\n\tconst auto first = ranges::find_if(\n\t\tranges::make_subrange(begin(_tasks), till),\n\t\treadyToRequest);\n\treturn (first != till) ? first->task.get() : nullptr;\n}\n\nvoid DownloadManagerMtproto::Queue::removeSession(int index) {\n\tfor (const auto &enqueued : _tasks) {\n\t\tenqueued.task->removeSession(index);\n\t}\n}\n\nDownloadManagerMtproto::DcSessionBalanceData::DcSessionBalanceData()\n: maxWaitedAmount(kStartWaitedInSession) {\n}\n\nDownloadManagerMtproto::DcBalanceData::DcBalanceData()\n: sessions(kStartSessionsCount) {\n}\n\nDownloadManagerMtproto::DownloadManagerMtproto(not_null<ApiWrap*> api)\n: _api(api)\n, _resetGenerationTimer([=] { resetGeneration(); })\n, _killSessionsTimer([=] { killSessions(); }) {\n\t_api->instance().restartsByTimeout(\n\t) | rpl::filter([](MTP::ShiftedDcId shiftedDcId) {\n\t\treturn MTP::isDownloadDcId(shiftedDcId);\n\t}) | rpl::on_next([=](MTP::ShiftedDcId shiftedDcId) {\n\t\tsessionTimedOut(\n\t\t\tMTP::BareDcId(shiftedDcId),\n\t\t\tMTP::GetDcIdShift(shiftedDcId));\n\t}, _lifetime);\n}\n\nDownloadManagerMtproto::~DownloadManagerMtproto() {\n\tkillSessions();\n}\n\nvoid DownloadManagerMtproto::enqueue(not_null<Task*> task, int priority) {\n\tconst auto dcId = task->dcId();\n\tauto &queue = _queues[dcId];\n\tqueue.enqueue(task, priority);\n\tif (!_resetGenerationTimer.isActive()) {\n\t\t_resetGenerationTimer.callOnce(kResetDownloadPrioritiesTimeout);\n\t}\n\tcheckSendNext(dcId, queue);\n}\n\nvoid DownloadManagerMtproto::remove(not_null<Task*> task) {\n\tconst auto dcId = task->dcId();\n\tauto &queue = _queues[dcId];\n\tqueue.remove(task);\n\tcheckSendNext(dcId, queue);\n}\n\nvoid DownloadManagerMtproto::resetGeneration() {\n\t_resetGenerationTimer.cancel();\n\tfor (auto &[dcId, queue] : _queues) {\n\t\tqueue.resetGeneration();\n\t}\n}\n\nvoid DownloadManagerMtproto::checkSendNext() {\n\tfor (auto &[dcId, queue] : _queues) {\n\t\tif (queue.empty()) {\n\t\t\tcontinue;\n\t\t}\n\t\tcheckSendNext(dcId, queue);\n\t}\n}\n\nvoid DownloadManagerMtproto::checkSendNext(MTP::DcId dcId, Queue &queue) {\n\twhile (trySendNextPart(dcId, queue)) {\n\t}\n}\n\nvoid DownloadManagerMtproto::checkSendNextAfterSuccess(MTP::DcId dcId) {\n\tcheckSendNext(dcId, _queues[dcId]);\n}\n\nbool DownloadManagerMtproto::trySendNextPart(MTP::DcId dcId, Queue &queue) {\n\tauto &balanceData = _balanceData[dcId];\n\tconst auto &sessions = balanceData.sessions;\n\tconst auto bestIndex = [&] {\n\t\tconst auto proj = [](const DcSessionBalanceData &data) {\n\t\t\treturn (data.requested < data.maxWaitedAmount)\n\t\t\t\t? data.requested\n\t\t\t\t: kMaxWaitedInSession;\n\t\t};\n\t\tconst auto j = ranges::min_element(sessions, ranges::less(), proj);\n\t\treturn (j->requested + kDownloadPartSize <= j->maxWaitedAmount)\n\t\t\t? (j - begin(sessions))\n\t\t\t: -1;\n\t}();\n\tif (bestIndex < 0) {\n\t\treturn false;\n\t}\n\tconst auto onlyHighestPriority = (balanceData.totalRequested > 0);\n\tif (const auto task = queue.nextTask(onlyHighestPriority)) {\n\t\ttask->loadPart(bestIndex);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nint DownloadManagerMtproto::changeRequestedAmount(\n\t\tMTP::DcId dcId,\n\t\tint index,\n\t\tint delta) {\n\tconst auto i = _balanceData.find(dcId);\n\tAssert(i != _balanceData.end());\n\tAssert(index < i->second.sessions.size());\n\tconst auto result = (i->second.sessions[index].requested += delta);\n\ti->second.totalRequested += delta;\n\tconst auto findNonEmptySession = [](const DcBalanceData &data) {\n\t\tusing namespace rpl::mappers;\n\t\treturn ranges::find_if(\n\t\t\tdata.sessions,\n\t\t\t_1 > 0,\n\t\t\t&DcSessionBalanceData::requested);\n\t};\n\tif (delta > 0) {\n\t\tkillSessionsCancel(dcId);\n\t} else if (findNonEmptySession(i->second) == end(i->second.sessions)) {\n\t\tkillSessionsSchedule(dcId);\n\t}\n\treturn result;\n}\n\nvoid DownloadManagerMtproto::requestSucceeded(\n\t\tMTP::DcId dcId,\n\t\tint index,\n\t\tint amountAtRequestStart,\n\t\tcrl::time timeAtRequestStart) {\n\tusing namespace rpl::mappers;\n\n\tconst auto i = _balanceData.find(dcId);\n\tAssert(i != end(_balanceData));\n\tauto &dc = i->second;\n\tAssert(index < dc.sessions.size());\n\tauto &data = dc.sessions[index];\n\tconst auto overloaded = (timeAtRequestStart <= dc.lastSessionRemove)\n\t\t|| (amountAtRequestStart > data.maxWaitedAmount);\n\tconst auto parts = amountAtRequestStart / kDownloadPartSize;\n\tconst auto duration = (crl::now() - timeAtRequestStart);\n\tDEBUG_LOG((\"Download (%1,%2) request done, duration: %3, parts: %4%5\"\n\t\t).arg(dcId\n\t\t).arg(index\n\t\t).arg(duration\n\t\t).arg(parts\n\t\t).arg(overloaded ? \" (overloaded)\" : \"\"));\n\tif (overloaded) {\n\t\treturn;\n\t}\n\n\tif (duration >= kBadRequestDurationThreshold) {\n\t\tDEBUG_LOG((\"Duration too large, signaling time out.\"));\n\t\tcrl::on_main(this, [=] {\n\t\t\tsessionTimedOut(dcId, index);\n\t\t});\n\t\treturn;\n\t}\n\tif (amountAtRequestStart == data.maxWaitedAmount\n\t\t&& data.maxWaitedAmount < kMaxWaitedInSession) {\n\t\tdata.maxWaitedAmount = std::min(\n\t\t\tdata.maxWaitedAmount + kDownloadPartSize,\n\t\t\tkMaxWaitedInSession);\n\t\tDEBUG_LOG((\"Download (%1,%2) increased max waited amount %3.\"\n\t\t\t).arg(dcId\n\t\t\t).arg(index\n\t\t\t).arg(data.maxWaitedAmount));\n\t}\n\tdata.successes = std::min(data.successes + 1, kMaxTrackedSuccesses);\n\tconst auto notEnough = ranges::any_of(\n\t\tdc.sessions,\n\t\t_1 < (dc.sessionRemoveTimes + 1) * kRetryAddSessionSuccesses,\n\t\t&DcSessionBalanceData::successes);\n\tif (notEnough) {\n\t\treturn;\n\t}\n\tfor (auto &session : dc.sessions) {\n\t\tsession.successes = 0;\n\t}\n\tif (dc.timeouts > 0) {\n\t\t--dc.timeouts;\n\t\treturn;\n\t} else if (dc.sessions.size() == kMaxSessionsCount) {\n\t\treturn;\n\t}\n\tconst auto now = crl::now();\n\tconst auto delay = (dc.sessionRemoveTimes + 1) * kRetryAddSessionTimeout;\n\tif (dc.lastSessionRemove && now < dc.lastSessionRemove + delay) {\n\t\treturn;\n\t}\n\tdc.sessions.emplace_back();\n\tDEBUG_LOG((\"Download (%1,%2) adding, now sessions: %3\"\n\t\t).arg(dcId\n\t\t).arg(dc.sessions.size() - 1\n\t\t).arg(dc.sessions.size()));\n}\n\nint DownloadManagerMtproto::chooseSessionIndex(MTP::DcId dcId) const {\n\tconst auto i = _balanceData.find(dcId);\n\tAssert(i != end(_balanceData));\n\tconst auto &sessions = i->second.sessions;\n\tconst auto j = ranges::min_element(\n\t\tsessions,\n\t\tranges::less(),\n\t\t&DcSessionBalanceData::requested);\n\treturn (j - begin(sessions));\n}\n\nvoid DownloadManagerMtproto::sessionTimedOut(MTP::DcId dcId, int index) {\n\tconst auto i = _balanceData.find(dcId);\n\tif (i == end(_balanceData)) {\n\t\treturn;\n\t}\n\tauto &dc = i->second;\n\tif (index >= dc.sessions.size()) {\n\t\treturn;\n\t}\n\tDEBUG_LOG((\"Download (%1,%2) session timed-out.\").arg(dcId).arg(index));\n\tfor (auto &session : dc.sessions) {\n\t\tsession.successes = 0;\n\t}\n\tif (dc.sessions.size() == kStartSessionsCount\n\t\t|| ++dc.timeouts < kRemoveSessionAfterTimeouts) {\n\t\treturn;\n\t}\n\tdc.timeouts = 0;\n\tremoveSession(dcId);\n}\n\nvoid DownloadManagerMtproto::removeSession(MTP::DcId dcId) {\n\tauto &dc = _balanceData[dcId];\n\tAssert(dc.sessions.size() > kStartSessionsCount);\n\tconst auto index = int(dc.sessions.size() - 1);\n\tDEBUG_LOG((\"Download (%1,%2) removing, now sessions: %3\"\n\t\t).arg(dcId\n\t\t).arg(index\n\t\t).arg(index));\n\tauto &queue = _queues[dcId];\n\tif (dc.sessionRemoveIndex == index) {\n\t\tdc.sessionRemoveTimes = std::min(\n\t\t\tdc.sessionRemoveTimes + 1,\n\t\t\tkMaxTrackedSessionRemoves);\n\t} else {\n\t\tdc.sessionRemoveIndex = index;\n\t\tdc.sessionRemoveTimes = 1;\n\t}\n\tauto &session = dc.sessions.back();\n\n\t// Make sure we don't send anything to that session while redirecting.\n\tsession.requested += kMaxWaitedInSession * kMaxSessionsCount;\n\tqueue.removeSession(index);\n\tAssert(session.requested == kMaxWaitedInSession * kMaxSessionsCount);\n\n\tdc.sessions.pop_back();\n\tapi().instance().killSession(MTP::downloadDcId(dcId, index));\n\n\tdc.lastSessionRemove = crl::now();\n}\n\nvoid DownloadManagerMtproto::killSessionsSchedule(MTP::DcId dcId) {\n\tif (!_killSessionsWhen.contains(dcId)) {\n\t\t_killSessionsWhen.emplace(dcId, crl::now() + kKillSessionTimeout);\n\t}\n\tif (!_killSessionsTimer.isActive()) {\n\t\t_killSessionsTimer.callOnce(kKillSessionTimeout + 5);\n\t}\n}\n\nvoid DownloadManagerMtproto::killSessionsCancel(MTP::DcId dcId) {\n\t_killSessionsWhen.erase(dcId);\n\tif (_killSessionsWhen.empty()) {\n\t\t_killSessionsTimer.cancel();\n\t}\n}\n\nvoid DownloadManagerMtproto::killSessions() {\n\tconst auto now = crl::now();\n\tauto left = kKillSessionTimeout;\n\tfor (auto i = begin(_killSessionsWhen); i != end(_killSessionsWhen); ) {\n\t\tif (i->second <= now) {\n\t\t\tkillSessions(i->first);\n\t\t\ti = _killSessionsWhen.erase(i);\n\t\t} else {\n\t\t\tif (i->second - now < left) {\n\t\t\t\tleft = i->second - now;\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t}\n\tif (!_killSessionsWhen.empty()) {\n\t\t_killSessionsTimer.callOnce(left);\n\t}\n}\n\nvoid DownloadManagerMtproto::killSessions(MTP::DcId dcId) {\n\tconst auto i = _balanceData.find(dcId);\n\tif (i != end(_balanceData)) {\n\t\tauto &dc = i->second;\n\t\tAssert(dc.totalRequested == 0);\n\t\tauto sessions = base::take(dc.sessions);\n\t\tdc = DcBalanceData();\n\t\tfor (auto j = 0; j != int(sessions.size()); ++j) {\n\t\t\tAssert(sessions[j].requested == 0);\n\t\t\tsessions[j] = DcSessionBalanceData();\n\t\t\tapi().instance().stopSession(MTP::downloadDcId(dcId, j));\n\t\t}\n\t\tdc.sessions = base::take(sessions);\n\t}\n}\n\nDownloadMtprotoTask::DownloadMtprotoTask(\n\tnot_null<DownloadManagerMtproto*> owner,\n\tconst StorageFileLocation &location,\n\tData::FileOrigin origin)\n: _owner(owner)\n, _dcId(location.dcId())\n, _location({ location })\n, _origin(origin) {\n}\n\nDownloadMtprotoTask::DownloadMtprotoTask(\n\tnot_null<DownloadManagerMtproto*> owner,\n\tMTP::DcId dcId,\n\tconst Location &location)\n: _owner(owner)\n, _dcId(dcId)\n, _location(location) {\n}\n\nDownloadMtprotoTask::~DownloadMtprotoTask() {\n\tcancelAllRequests();\n\t_owner->remove(this);\n}\n\nMTP::DcId DownloadMtprotoTask::dcId() const {\n\treturn _dcId;\n}\n\nData::FileOrigin DownloadMtprotoTask::fileOrigin() const {\n\treturn _origin;\n}\n\nuint64 DownloadMtprotoTask::objectId() const {\n\tif (const auto v = std::get_if<StorageFileLocation>(&_location.data)) {\n\t\treturn v->objectId();\n\t}\n\treturn 0;\n}\n\nconst DownloadMtprotoTask::Location &DownloadMtprotoTask::location() const {\n\treturn _location;\n}\n\nvoid DownloadMtprotoTask::refreshFileReferenceFrom(\n\t\tconst Data::UpdatedFileReferences &updates,\n\t\tint requestId,\n\t\tconst QByteArray &current) {\n\tif (const auto v = std::get_if<StorageFileLocation>(&_location.data)) {\n\t\tv->refreshFileReference(updates);\n\t\tif (v->fileReference() == current) {\n\t\t\tcancelOnFail();\n\t\t\treturn;\n\t\t}\n\t} else {\n\t\tcancelOnFail();\n\t\treturn;\n\t}\n\tif (_sentRequests.contains(requestId)) {\n\t\tmakeRequest(finishSentRequest(\n\t\t\trequestId,\n\t\t\tFinishRequestReason::Redirect));\n\t}\n}\n\nvoid DownloadMtprotoTask::loadPart(int sessionIndex) {\n\tmakeRequest({ takeNextRequestOffset(), sessionIndex });\n}\n\nvoid DownloadMtprotoTask::removeSession(int sessionIndex) {\n\tstruct Redirect {\n\t\tmtpRequestId requestId = 0;\n\t\tint64 offset = 0;\n\t};\n\tauto redirect = std::vector<Redirect>();\n\tfor (const auto &[requestId, requestData] : _sentRequests) {\n\t\tif (requestData.sessionIndex == sessionIndex) {\n\t\t\tredirect.reserve(_sentRequests.size());\n\t\t\tredirect.push_back({ requestId, requestData.offset });\n\t\t}\n\t}\n\tfor (auto &[requestData, bytes] : _cdnUncheckedParts) {\n\t\tif (requestData.sessionIndex == sessionIndex) {\n\t\t\tconst auto newIndex = _owner->chooseSessionIndex(dcId());\n\t\t\tAssert(newIndex < sessionIndex);\n\t\t\trequestData.sessionIndex = newIndex;\n\t\t}\n\t}\n\tfor (const auto &[requestId, offset] : redirect) {\n\t\tconst auto needMakeRequest = (requestId != _cdnHashesRequestId);\n\t\tcancelRequest(requestId);\n\t\tif (needMakeRequest) {\n\t\t\tconst auto newIndex = _owner->chooseSessionIndex(dcId());\n\t\t\tAssert(newIndex < sessionIndex);\n\t\t\tmakeRequest({ offset, newIndex });\n\t\t}\n\t}\n}\n\nmtpRequestId DownloadMtprotoTask::sendRequest(\n\t\tconst RequestData &requestData) {\n\tconst auto offset = requestData.offset;\n\tconst auto limit = Storage::kDownloadPartSize;\n\tconst auto shiftedDcId = MTP::downloadDcId(\n\t\t_cdnDcId ? _cdnDcId : dcId(),\n\t\trequestData.sessionIndex);\n\tif (_cdnDcId) {\n\t\treturn api().request(MTPupload_GetCdnFile(\n\t\t\tMTP_bytes(_cdnToken),\n\t\t\tMTP_long(offset),\n\t\t\tMTP_int(limit)\n\t\t)).done([=](const MTPupload_CdnFile &result, mtpRequestId id) {\n\t\t\tcdnPartLoaded(result, id);\n\t\t}).fail([=](const MTP::Error &error, mtpRequestId id) {\n\t\t\tcdnPartFailed(error, id);\n\t\t}).toDC(shiftedDcId).send();\n\t}\n\treturn v::match(_location.data, [&](const WebFileLocation &location) {\n\t\treturn api().request(MTPupload_GetWebFile(\n\t\t\tMTP_inputWebFileLocation(\n\t\t\t\tMTP_bytes(location.url()),\n\t\t\t\tMTP_long(location.accessHash())),\n\t\t\tMTP_int(offset),\n\t\t\tMTP_int(limit)\n\t\t)).done([=](const MTPupload_WebFile &result, mtpRequestId id) {\n\t\t\twebPartLoaded(result, id);\n\t\t}).fail([=](const MTP::Error &error, mtpRequestId id) {\n\t\t\tpartFailed(error, id);\n\t\t}).toDC(shiftedDcId).send();\n\t}, [&](const GeoPointLocation &location) {\n\t\treturn api().request(MTPupload_GetWebFile(\n\t\t\tMTP_inputWebFileGeoPointLocation(\n\t\t\t\tMTP_inputGeoPoint(\n\t\t\t\t\tMTP_flags(0),\n\t\t\t\t\tMTP_double(location.lat),\n\t\t\t\t\tMTP_double(location.lon),\n\t\t\t\t\tMTP_int(0)), // accuracy_radius\n\t\t\t\tMTP_long(location.access),\n\t\t\t\tMTP_int(location.width),\n\t\t\t\tMTP_int(location.height),\n\t\t\t\tMTP_int(location.zoom),\n\t\t\t\tMTP_int(location.scale)),\n\t\t\tMTP_int(offset),\n\t\t\tMTP_int(limit)\n\t\t)).done([=](const MTPupload_WebFile &result, mtpRequestId id) {\n\t\t\twebPartLoaded(result, id);\n\t\t}).fail([=](const MTP::Error &error, mtpRequestId id) {\n\t\t\tpartFailed(error, id);\n\t\t}).toDC(shiftedDcId).send();\n\t}, [&](const AudioAlbumThumbLocation &location) {\n\t\tusing Flag = MTPDinputWebFileAudioAlbumThumbLocation::Flag;\n\t\tconst auto owner = &api().session().data();\n\t\treturn api().request(MTPupload_GetWebFile(\n\t\t\tMTP_inputWebFileAudioAlbumThumbLocation(\n\t\t\t\tMTP_flags(Flag::f_document | Flag::f_small),\n\t\t\t\towner->document(location.documentId)->mtpInput(),\n\t\t\t\tMTPstring(),\n\t\t\t\tMTPstring()),\n\t\t\tMTP_int(offset),\n\t\t\tMTP_int(limit)\n\t\t)).done([=](const MTPupload_WebFile &result, mtpRequestId id) {\n\t\t\twebPartLoaded(result, id);\n\t\t}).fail([=](const MTP::Error &error, mtpRequestId id) {\n\t\t\tpartFailed(error, id);\n\t\t}).toDC(shiftedDcId).send();\n\t}, [&](const StorageFileLocation &location) {\n\t\tconst auto reference = location.fileReference();\n\t\treturn api().request(MTPupload_GetFile(\n\t\t\tMTP_flags(MTPupload_GetFile::Flag::f_cdn_supported),\n\t\t\tlocation.tl(api().session().userId()),\n\t\t\tMTP_long(offset),\n\t\t\tMTP_int(limit)\n\t\t)).done([=](const MTPupload_File &result, mtpRequestId id) {\n\t\t\tnormalPartLoaded(result, id);\n\t\t}).fail([=](const MTP::Error &error, mtpRequestId id) {\n\t\t\tnormalPartFailed(reference, error, id);\n\t\t}).toDC(shiftedDcId).send();\n\t});\n}\n\nbool DownloadMtprotoTask::setWebFileSizeHook(int64 size) {\n\treturn true;\n}\n\nvoid DownloadMtprotoTask::makeRequest(const RequestData &requestData) {\n\tplaceSentRequest(sendRequest(requestData), requestData);\n}\n\nvoid DownloadMtprotoTask::requestMoreCdnFileHashes() {\n\tif (_cdnHashesRequestId || _cdnUncheckedParts.empty()) {\n\t\treturn;\n\t}\n\n\tconst auto requestData = _cdnUncheckedParts.cbegin()->first;\n\tconst auto shiftedDcId = MTP::downloadDcId(\n\t\tdcId(),\n\t\trequestData.sessionIndex);\n\t_cdnHashesRequestId = api().request(MTPupload_GetCdnFileHashes(\n\t\tMTP_bytes(_cdnToken),\n\t\tMTP_long(requestData.offset)\n\t)).done([=](const MTPVector<MTPFileHash> &result, mtpRequestId id) {\n\t\tgetCdnFileHashesDone(result, id);\n\t}).fail([=](const MTP::Error &error, mtpRequestId id) {\n\t\tcdnPartFailed(error, id);\n\t}).toDC(shiftedDcId).send();\n\tplaceSentRequest(_cdnHashesRequestId, requestData);\n}\n\nvoid DownloadMtprotoTask::normalPartLoaded(\n\t\tconst MTPupload_File &result,\n\t\tmtpRequestId requestId) {\n\tconst auto requestData = finishSentRequest(\n\t\trequestId,\n\t\tFinishRequestReason::Success);\n\tconst auto owner = _owner;\n\tconst auto dcId = this->dcId();\n\tresult.match([&](const MTPDupload_fileCdnRedirect &data) {\n\t\tswitchToCDN(requestData, data);\n\t}, [&](const MTPDupload_file &data) {\n\t\tpartLoaded(requestData.offset, data.vbytes().v);\n\t});\n\n\t// 'this' may be deleted at this point.\n\towner->checkSendNextAfterSuccess(dcId);\n}\n\nvoid DownloadMtprotoTask::webPartLoaded(\n\t\tconst MTPupload_WebFile &result,\n\t\tmtpRequestId requestId) {\n\tconst auto requestData = finishSentRequest(\n\t\trequestId,\n\t\tFinishRequestReason::Success);\n\tconst auto owner = _owner;\n\tconst auto dcId = this->dcId();\n\tresult.match([&](const MTPDupload_webFile &data) {\n\t\tif (setWebFileSizeHook(data.vsize().v)) {\n\t\t\tpartLoaded(requestData.offset, data.vbytes().v);\n\t\t}\n\t});\n\n\t// 'this' may be deleted at this point.\n\towner->checkSendNextAfterSuccess(dcId);\n}\n\nvoid DownloadMtprotoTask::cdnPartLoaded(const MTPupload_CdnFile &result, mtpRequestId requestId) {\n\tresult.match([&](const MTPDupload_cdnFileReuploadNeeded &data) {\n\t\tconst auto requestData = finishSentRequest(\n\t\t\trequestId,\n\t\t\tFinishRequestReason::Redirect);\n\t\tconst auto shiftedDcId = MTP::downloadDcId(\n\t\t\tdcId(),\n\t\t\trequestData.sessionIndex);\n\t\tconst auto requestId = api().request(MTPupload_ReuploadCdnFile(\n\t\t\tMTP_bytes(_cdnToken),\n\t\t\tdata.vrequest_token()\n\t\t)).done([=](const MTPVector<MTPFileHash> &result, mtpRequestId id) {\n\t\t\treuploadDone(result, id);\n\t\t}).fail([=](const MTP::Error &error, mtpRequestId id) {\n\t\t\tcdnPartFailed(error, id);\n\t\t}).toDC(shiftedDcId).send();\n\t\tplaceSentRequest(requestId, requestData);\n\t}, [&](const MTPDupload_cdnFile &data) {\n\t\tconst auto requestData = finishSentRequest(\n\t\t\trequestId,\n\t\t\tFinishRequestReason::Success);\n\t\tconst auto owner = _owner;\n\t\tconst auto dcId = this->dcId();\n\t\tconst auto guard = gsl::finally([=] {\n\t\t\t// 'this' may be deleted at this point.\n\t\t\towner->checkSendNextAfterSuccess(dcId);\n\t\t});\n\n\t\tauto key = bytes::make_span(_cdnEncryptionKey);\n\t\tauto iv = bytes::make_span(_cdnEncryptionIV);\n\t\tExpects(key.size() == MTP::CTRState::KeySize);\n\t\tExpects(iv.size() == MTP::CTRState::IvecSize);\n\n\t\tauto state = MTP::CTRState();\n\t\tauto ivec = bytes::make_span(state.ivec);\n\t\tstd::copy(iv.begin(), iv.end(), ivec.begin());\n\n\t\tauto counterOffset = static_cast<uint32>(requestData.offset >> 4);\n\t\tstate.ivec[15] = static_cast<uchar>(counterOffset & 0xFF);\n\t\tstate.ivec[14] = static_cast<uchar>((counterOffset >> 8) & 0xFF);\n\t\tstate.ivec[13] = static_cast<uchar>((counterOffset >> 16) & 0xFF);\n\t\tstate.ivec[12] = static_cast<uchar>((counterOffset >> 24) & 0xFF);\n\n\t\tauto decryptInPlace = data.vbytes().v;\n\t\tauto buffer = bytes::make_detached_span(decryptInPlace);\n\t\tMTP::aesCtrEncrypt(buffer, key.data(), &state);\n\n\t\tswitch (checkCdnFileHash(requestData.offset, buffer)) {\n\t\tcase CheckCdnHashResult::NoHash: {\n\t\t\t_cdnUncheckedParts.emplace(requestData, decryptInPlace);\n\t\t\trequestMoreCdnFileHashes();\n\t\t} return;\n\n\t\tcase CheckCdnHashResult::Invalid: {\n\t\t\tLOG((\"API Error: Wrong cdnFileHash for offset %1.\"\n\t\t\t\t).arg(requestData.offset));\n\t\t\tcancelOnFail();\n\t\t} return;\n\n\t\tcase CheckCdnHashResult::Good: {\n\t\t\tpartLoaded(requestData.offset, decryptInPlace);\n\t\t} return;\n\t\t}\n\t\tUnexpected(\"Result of checkCdnFileHash()\");\n\t});\n}\n\nDownloadMtprotoTask::CheckCdnHashResult DownloadMtprotoTask::checkCdnFileHash(\n\t\tint64 offset,\n\t\tbytes::const_span buffer) {\n\tconst auto cdnFileHashIt = _cdnFileHashes.find(offset);\n\tif (cdnFileHashIt == _cdnFileHashes.cend()) {\n\t\treturn CheckCdnHashResult::NoHash;\n\t}\n\tconst auto realHash = openssl::Sha256(buffer);\n\tconst auto receivedHash = bytes::make_span(cdnFileHashIt->second.hash);\n\tif (bytes::compare(realHash, receivedHash)) {\n\t\treturn CheckCdnHashResult::Invalid;\n\t}\n\treturn CheckCdnHashResult::Good;\n}\n\nvoid DownloadMtprotoTask::reuploadDone(\n\t\tconst MTPVector<MTPFileHash> &result,\n\t\tmtpRequestId requestId) {\n\tconst auto requestData = finishSentRequest(\n\t\trequestId,\n\t\tFinishRequestReason::Redirect);\n\taddCdnHashes(result.v);\n\tmakeRequest(requestData);\n}\n\nvoid DownloadMtprotoTask::getCdnFileHashesDone(\n\t\tconst MTPVector<MTPFileHash> &result,\n\t\tmtpRequestId requestId) {\n\tExpects(_cdnHashesRequestId == requestId);\n\n\tconst auto requestData = finishSentRequest(\n\t\trequestId,\n\t\tFinishRequestReason::Redirect);\n\taddCdnHashes(result.v);\n\tauto someMoreChecked = false;\n\tfor (auto i = _cdnUncheckedParts.begin(); i != _cdnUncheckedParts.cend();) {\n\t\tconst auto uncheckedData = i->first;\n\t\tconst auto uncheckedBytes = bytes::make_span(i->second);\n\n\t\tswitch (checkCdnFileHash(uncheckedData.offset, uncheckedBytes)) {\n\t\tcase CheckCdnHashResult::NoHash: {\n\t\t\t++i;\n\t\t} break;\n\n\t\tcase CheckCdnHashResult::Invalid: {\n\t\t\tLOG((\"API Error: Wrong cdnFileHash for offset %1.\"\n\t\t\t\t).arg(uncheckedData.offset));\n\t\t\tcancelOnFail();\n\t\t\treturn;\n\t\t} break;\n\n\t\tcase CheckCdnHashResult::Good: {\n\t\t\tsomeMoreChecked = true;\n\t\t\tconst auto goodOffset = uncheckedData.offset;\n\t\t\tconst auto goodBytes = std::move(i->second);\n\t\t\tconst auto weak = base::make_weak(this);\n\t\t\ti = _cdnUncheckedParts.erase(i);\n\t\t\tif (!feedPart(goodOffset, goodBytes) || !weak) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t} break;\n\n\t\tdefault: Unexpected(\"Result of checkCdnFileHash()\");\n\t\t}\n\t}\n\tif (!someMoreChecked) {\n\t\tLOG((\"API Error: \"\n\t\t\t\"Could not find cdnFileHash for offset %1 \"\n\t\t\t\"after getCdnFileHashes request.\"\n\t\t\t).arg(requestData.offset));\n\t\tcancelOnFail();\n\t\treturn;\n\t}\n\trequestMoreCdnFileHashes();\n}\n\nvoid DownloadMtprotoTask::placeSentRequest(\n\t\tmtpRequestId requestId,\n\t\tconst RequestData &requestData) {\n\tif (_sentRequests.empty()) {\n\t\tsubscribeToNonPremiumLimit();\n\t}\n\n\tconst auto amount = _owner->changeRequestedAmount(\n\t\tdcId(),\n\t\trequestData.sessionIndex,\n\t\tStorage::kDownloadPartSize);\n\tconst auto &[i, ok1] = _sentRequests.emplace(requestId, requestData);\n\tconst auto &[j, ok2] = _requestByOffset.emplace(\n\t\trequestData.offset,\n\t\trequestId);\n\n\ti->second.requestedInSession = amount;\n\ti->second.sent = crl::now();\n\n\tEnsures(ok1 && ok2);\n}\n\nvoid DownloadMtprotoTask::subscribeToNonPremiumLimit() {\n\tif (_nonPremiumLimitSubscription) {\n\t\treturn;\n\t}\n\t_owner->api().instance().nonPremiumDelayedRequests(\n\t) | rpl::on_next([=](mtpRequestId id) {\n\t\tif (_sentRequests.contains(id)) {\n\t\t\tif (const auto documentId = objectId()) {\n\t\t\t\tconst auto type = v::get<StorageFileLocation>(\n\t\t\t\t\t_location.data).type();\n\t\t\t\tif (type == StorageFileLocation::Type::Document) {\n\t\t\t\t\t_owner->notifyNonPremiumDelay(documentId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, _nonPremiumLimitSubscription);\n}\n\nauto DownloadMtprotoTask::finishSentRequest(\n\tmtpRequestId requestId,\n\tFinishRequestReason reason)\n-> RequestData {\n\tauto it = _sentRequests.find(requestId);\n\tAssert(it != _sentRequests.cend());\n\n\tif (_cdnHashesRequestId == requestId) {\n\t\t_cdnHashesRequestId = 0;\n\t}\n\tconst auto result = it->second;\n\t_owner->changeRequestedAmount(\n\t\tdcId(),\n\t\tresult.sessionIndex,\n\t\t-Storage::kDownloadPartSize);\n\t_sentRequests.erase(it);\n\tconst auto ok = _requestByOffset.remove(result.offset);\n\n\tif (_sentRequests.empty()) {\n\t\t_nonPremiumLimitSubscription.destroy();\n\t}\n\n\tif (reason == FinishRequestReason::Success) {\n\t\t_owner->requestSucceeded(\n\t\t\tdcId(),\n\t\t\tresult.sessionIndex,\n\t\t\tresult.requestedInSession,\n\t\t\tresult.sent);\n\t}\n\n\tEnsures(ok);\n\treturn result;\n}\n\nbool DownloadMtprotoTask::haveSentRequests() const {\n\treturn !_sentRequests.empty() || !_cdnUncheckedParts.empty();\n}\n\nbool DownloadMtprotoTask::haveSentRequestForOffset(int64 offset) const {\n\treturn _requestByOffset.contains(offset)\n\t\t|| _cdnUncheckedParts.contains({ offset, 0 });\n}\n\nvoid DownloadMtprotoTask::cancelAllRequests() {\n\twhile (!_sentRequests.empty()) {\n\t\tcancelRequest(_sentRequests.begin()->first);\n\t}\n\t_cdnUncheckedParts.clear();\n}\n\nvoid DownloadMtprotoTask::cancelRequestForOffset(int64 offset) {\n\tconst auto i = _requestByOffset.find(offset);\n\tif (i != end(_requestByOffset)) {\n\t\tcancelRequest(i->second);\n\t}\n\t_cdnUncheckedParts.remove({ offset, 0 });\n}\n\nvoid DownloadMtprotoTask::cancelRequest(mtpRequestId requestId) {\n\tconst auto hashes = (_cdnHashesRequestId == requestId);\n\tapi().request(requestId).cancel();\n\t[[maybe_unused]] const auto data = finishSentRequest(\n\t\trequestId,\n\t\tFinishRequestReason::Cancel);\n\tif (hashes && !_cdnUncheckedParts.empty()) {\n\t\tcrl::on_main(this, [=] {\n\t\t\trequestMoreCdnFileHashes();\n\t\t});\n\t}\n}\n\nvoid DownloadMtprotoTask::addToQueue(int priority) {\n\t_owner->enqueue(this, priority);\n}\n\nvoid DownloadMtprotoTask::removeFromQueue() {\n\t_owner->remove(this);\n}\n\nvoid DownloadMtprotoTask::partLoaded(\n\t\tint64 offset,\n\t\tconst QByteArray &bytes) {\n\tfeedPart(offset, bytes);\n}\n\nbool DownloadMtprotoTask::normalPartFailed(\n\t\tQByteArray fileReference,\n\t\tconst MTP::Error &error,\n\t\tmtpRequestId requestId) {\n\tif (MTP::IsDefaultHandledError(error)) {\n\t\treturn false;\n\t}\n\tif (error.code() == 400\n\t\t&& error.type().startsWith(u\"FILE_REFERENCE_\"_q)) {\n\t\tapi().refreshFileReference(\n\t\t\t_origin,\n\t\t\tthis,\n\t\t\trequestId,\n\t\t\tfileReference);\n\t\treturn true;\n\t}\n\treturn partFailed(error, requestId);\n}\n\nbool DownloadMtprotoTask::partFailed(\n\t\tconst MTP::Error &error,\n\t\tmtpRequestId requestId) {\n\tif (MTP::IsDefaultHandledError(error)) {\n\t\treturn false;\n\t}\n\tcancelOnFail();\n\treturn true;\n}\n\nbool DownloadMtprotoTask::cdnPartFailed(\n\t\tconst MTP::Error &error,\n\t\tmtpRequestId requestId) {\n\tif (MTP::IsDefaultHandledError(error)) {\n\t\treturn false;\n\t}\n\n\tif (error.type() == u\"FILE_TOKEN_INVALID\"_q\n\t\t|| error.type() == u\"REQUEST_TOKEN_INVALID\"_q) {\n\t\tconst auto requestData = finishSentRequest(\n\t\t\trequestId,\n\t\t\tFinishRequestReason::Redirect);\n\t\tchangeCDNParams(\n\t\t\trequestData,\n\t\t\t0,\n\t\t\tQByteArray(),\n\t\t\tQByteArray(),\n\t\t\tQByteArray(),\n\t\t\tQVector<MTPFileHash>());\n\t\treturn true;\n\t}\n\treturn partFailed(error, requestId);\n}\n\nvoid DownloadMtprotoTask::switchToCDN(\n\t\tconst RequestData &requestData,\n\t\tconst MTPDupload_fileCdnRedirect &redirect) {\n\tchangeCDNParams(\n\t\trequestData,\n\t\tredirect.vdc_id().v,\n\t\tredirect.vfile_token().v,\n\t\tredirect.vencryption_key().v,\n\t\tredirect.vencryption_iv().v,\n\t\tredirect.vfile_hashes().v);\n}\n\nvoid DownloadMtprotoTask::addCdnHashes(\n\t\tconst QVector<MTPFileHash> &hashes) {\n\tfor (const auto &hash : hashes) {\n\t\thash.match([&](const MTPDfileHash &data) {\n\t\t\t_cdnFileHashes.emplace(\n\t\t\t\tdata.voffset().v,\n\t\t\t\tCdnFileHash{ data.vlimit().v, data.vhash().v });\n\t\t});\n\t}\n}\n\nvoid DownloadMtprotoTask::changeCDNParams(\n\t\tconst RequestData &requestData,\n\t\tMTP::DcId dcId,\n\t\tconst QByteArray &token,\n\t\tconst QByteArray &encryptionKey,\n\t\tconst QByteArray &encryptionIV,\n\t\tconst QVector<MTPFileHash> &hashes) {\n\tif (dcId != 0\n\t\t&& (encryptionKey.size() != MTP::CTRState::KeySize\n\t\t\t|| encryptionIV.size() != MTP::CTRState::IvecSize)) {\n\t\tLOG((\"Message Error: Wrong key (%1) / iv (%2) size in CDN params\"\n\t\t\t).arg(encryptionKey.size()\n\t\t\t).arg(encryptionIV.size()));\n\t\tcancelOnFail();\n\t\treturn;\n\t}\n\n\tauto resendAllRequests = (_cdnDcId != dcId\n\t\t|| _cdnToken != token\n\t\t|| _cdnEncryptionKey != encryptionKey\n\t\t|| _cdnEncryptionIV != encryptionIV);\n\t_cdnDcId = dcId;\n\t_cdnToken = token;\n\t_cdnEncryptionKey = encryptionKey;\n\t_cdnEncryptionIV = encryptionIV;\n\taddCdnHashes(hashes);\n\n\tif (resendAllRequests && !_sentRequests.empty()) {\n\t\tauto resendRequests = std::vector<RequestData>();\n\t\tresendRequests.reserve(_sentRequests.size());\n\t\twhile (!_sentRequests.empty()) {\n\t\t\tconst auto requestId = _sentRequests.begin()->first;\n\t\t\tapi().request(requestId).cancel();\n\t\t\tresendRequests.push_back(finishSentRequest(\n\t\t\t\trequestId,\n\t\t\t\tFinishRequestReason::Redirect));\n\t\t}\n\t\tfor (const auto &requestData : resendRequests) {\n\t\t\tmakeRequest(requestData);\n\t\t}\n\t}\n\tmakeRequest(requestData);\n}\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/download_manager_mtproto.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_file_origin.h\"\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n\nclass ApiWrap;\n\nnamespace MTP {\nclass Error;\n} // namespace MTP\n\nnamespace Storage {\n\n// Different part sizes are not supported for now :(\n// Because we start downloading with some part size\n// and then we get a CDN-redirect where we support only\n// fixed part size download for hash checking.\nconstexpr auto kDownloadPartSize = 128 * 1024;\n\nclass DownloadMtprotoTask;\n\nclass DownloadManagerMtproto final : public base::has_weak_ptr {\npublic:\n\tusing Task = DownloadMtprotoTask;\n\n\texplicit DownloadManagerMtproto(not_null<ApiWrap*> api);\n\t~DownloadManagerMtproto();\n\n\t[[nodiscard]] ApiWrap &api() const {\n\t\treturn *_api;\n\t}\n\n\tvoid enqueue(not_null<Task*> task, int priority);\n\tvoid remove(not_null<Task*> task);\n\n\tvoid notifyTaskFinished() {\n\t\t_taskFinished.fire({});\n\t}\n\t[[nodiscard]] rpl::producer<> taskFinished() const {\n\t\treturn _taskFinished.events();\n\t}\n\n\tint changeRequestedAmount(MTP::DcId dcId, int index, int delta);\n\tvoid requestSucceeded(\n\t\tMTP::DcId dcId,\n\t\tint index,\n\t\tint amountAtRequestStart,\n\t\tcrl::time timeAtRequestStart);\n\tvoid checkSendNextAfterSuccess(MTP::DcId dcId);\n\t[[nodiscard]] int chooseSessionIndex(MTP::DcId dcId) const;\n\n\tvoid notifyNonPremiumDelay(DocumentId id) {\n\t\t_nonPremiumDelays.fire_copy(id);\n\t}\n\t[[nodiscard]] rpl::producer<DocumentId> nonPremiumDelays() const {\n\t\treturn _nonPremiumDelays.events();\n\t}\n\nprivate:\n\tclass Queue final {\n\tpublic:\n\t\tvoid enqueue(not_null<Task*> task, int priority);\n\t\tvoid remove(not_null<Task*> task);\n\t\tvoid resetGeneration();\n\t\t[[nodiscard]] bool empty() const;\n\t\t[[nodiscard]] Task *nextTask(bool onlyHighestPriority) const;\n\t\tvoid removeSession(int index);\n\n\tprivate:\n\t\tstruct Enqueued {\n\t\t\tnot_null<Task*> task;\n\t\t\tint priority = 0;\n\t\t};\n\t\tstd::vector<Enqueued> _tasks;\n\n\t};\n\tstruct DcSessionBalanceData {\n\t\tDcSessionBalanceData();\n\n\t\tint requested = 0;\n\t\tint successes = 0; // Since last timeout in this dc in any session.\n\t\tint maxWaitedAmount = 0;\n\t};\n\tstruct DcBalanceData {\n\t\tDcBalanceData();\n\n\t\tstd::vector<DcSessionBalanceData> sessions;\n\t\tcrl::time lastSessionRemove = 0;\n\t\tint sessionRemoveIndex = 0;\n\t\tint sessionRemoveTimes = 0;\n\t\tint timeouts = 0; // Since all sessions had successes >= required.\n\t\tint totalRequested = 0;\n\t};\n\n\tvoid checkSendNext();\n\tvoid checkSendNext(MTP::DcId dcId, Queue &queue);\n\tbool trySendNextPart(MTP::DcId dcId, Queue &queue);\n\n\tvoid killSessionsSchedule(MTP::DcId dcId);\n\tvoid killSessionsCancel(MTP::DcId dcId);\n\tvoid killSessions();\n\tvoid killSessions(MTP::DcId dcId);\n\n\tvoid resetGeneration();\n\tvoid sessionTimedOut(MTP::DcId dcId, int index);\n\tvoid removeSession(MTP::DcId dcId);\n\n\tconst not_null<ApiWrap*> _api;\n\n\trpl::event_stream<> _taskFinished;\n\trpl::event_stream<DocumentId> _nonPremiumDelays;\n\n\tbase::flat_map<MTP::DcId, DcBalanceData> _balanceData;\n\tbase::Timer _resetGenerationTimer;\n\n\tbase::flat_map<MTP::DcId, crl::time> _killSessionsWhen;\n\tbase::Timer _killSessionsTimer;\n\n\tbase::flat_map<MTP::DcId, Queue> _queues;\n\trpl::lifetime _lifetime;\n\n};\n\nclass DownloadMtprotoTask : public base::has_weak_ptr {\npublic:\n\tstruct Location {\n\t\tstd::variant<\n\t\t\tStorageFileLocation,\n\t\t\tWebFileLocation,\n\t\t\tGeoPointLocation,\n\t\t\tAudioAlbumThumbLocation> data;\n\t};\n\n\tDownloadMtprotoTask(\n\t\tnot_null<DownloadManagerMtproto*> owner,\n\t\tconst StorageFileLocation &location,\n\t\tData::FileOrigin origin);\n\tDownloadMtprotoTask(\n\t\tnot_null<DownloadManagerMtproto*> owner,\n\t\tMTP::DcId dcId,\n\t\tconst Location &location);\n\tvirtual ~DownloadMtprotoTask();\n\n\t[[nodiscard]] MTP::DcId dcId() const;\n\t[[nodiscard]] Data::FileOrigin fileOrigin() const;\n\t[[nodiscard]] uint64 objectId() const;\n\t[[nodiscard]] const Location &location() const;\n\n\t[[nodiscard]] virtual bool readyToRequest() const = 0;\n\tvoid loadPart(int sessionIndex);\n\tvoid removeSession(int sessionIndex);\n\n\tvoid refreshFileReferenceFrom(\n\t\tconst Data::UpdatedFileReferences &updates,\n\t\tint requestId,\n\t\tconst QByteArray &current);\n\nprotected:\n\t[[nodiscard]] bool haveSentRequests() const;\n\t[[nodiscard]] bool haveSentRequestForOffset(int64 offset) const;\n\tvoid cancelAllRequests();\n\tvoid cancelRequestForOffset(int64 offset);\n\n\tvoid addToQueue(int priority = 0);\n\tvoid removeFromQueue();\n\n\t[[nodiscard]] ApiWrap &api() const {\n\t\treturn _owner->api();\n\t}\n\nprivate:\n\tstruct RequestData {\n\t\tint64 offset = 0;\n\t\tmutable int sessionIndex = 0;\n\t\tint requestedInSession = 0;\n\t\tcrl::time sent = 0;\n\n\t\tinline bool operator<(const RequestData &other) const {\n\t\t\treturn offset < other.offset;\n\t\t}\n\t};\n\tstruct CdnFileHash {\n\t\tCdnFileHash(int limit, QByteArray hash) : limit(limit), hash(hash) {\n\t\t}\n\t\tint limit = 0;\n\t\tQByteArray hash;\n\t};\n\tenum class CheckCdnHashResult {\n\t\tNoHash,\n\t\tInvalid,\n\t\tGood,\n\t};\n\tenum class FinishRequestReason {\n\t\tSuccess,\n\t\tRedirect,\n\t\tCancel,\n\t};\n\n\t// Called only if readyToRequest() == true.\n\t[[nodiscard]] virtual int64 takeNextRequestOffset() = 0;\n\tvirtual bool feedPart(int64 offset, const QByteArray &bytes) = 0;\n\tvirtual bool setWebFileSizeHook(int64 size);\n\tvirtual void cancelOnFail() = 0;\n\n\tvoid cancelRequest(mtpRequestId requestId);\n\tvoid makeRequest(const RequestData &requestData);\n\tvoid normalPartLoaded(\n\t\tconst MTPupload_File &result,\n\t\tmtpRequestId requestId);\n\tvoid webPartLoaded(\n\t\tconst MTPupload_WebFile &result,\n\t\tmtpRequestId requestId);\n\tvoid cdnPartLoaded(\n\t\tconst MTPupload_CdnFile &result,\n\t\tmtpRequestId requestId);\n\tvoid reuploadDone(\n\t\tconst MTPVector<MTPFileHash> &result,\n\t\tmtpRequestId requestId);\n\tvoid requestMoreCdnFileHashes();\n\tvoid getCdnFileHashesDone(\n\t\tconst MTPVector<MTPFileHash> &result,\n\t\tmtpRequestId requestId);\n\n\tvoid partLoaded(int64 offset, const QByteArray &bytes);\n\n\tbool partFailed(const MTP::Error &error, mtpRequestId requestId);\n\tbool normalPartFailed(\n\t\tQByteArray fileReference,\n\t\tconst MTP::Error &error,\n\t\tmtpRequestId requestId);\n\tbool cdnPartFailed(const MTP::Error &error, mtpRequestId requestId);\n\n\t[[nodiscard]] mtpRequestId sendRequest(const RequestData &requestData);\n\tvoid placeSentRequest(\n\t\tmtpRequestId requestId,\n\t\tconst RequestData &requestData);\n\t[[nodiscard]] RequestData finishSentRequest(\n\t\tmtpRequestId requestId,\n\t\tFinishRequestReason reason);\n\tvoid switchToCDN(\n\t\tconst RequestData &requestData,\n\t\tconst MTPDupload_fileCdnRedirect &redirect);\n\tvoid addCdnHashes(const QVector<MTPFileHash> &hashes);\n\tvoid changeCDNParams(\n\t\tconst RequestData &requestData,\n\t\tMTP::DcId dcId,\n\t\tconst QByteArray &token,\n\t\tconst QByteArray &encryptionKey,\n\t\tconst QByteArray &encryptionIV,\n\t\tconst QVector<MTPFileHash> &hashes);\n\n\t[[nodiscard]] CheckCdnHashResult checkCdnFileHash(\n\t\tint64 offset,\n\t\tbytes::const_span buffer);\n\n\tvoid subscribeToNonPremiumLimit();\n\n\tconst not_null<DownloadManagerMtproto*> _owner;\n\tconst MTP::DcId _dcId = 0;\n\n\t// _location can be changed with an updated file_reference.\n\tLocation _location;\n\tconst Data::FileOrigin _origin;\n\n\tbase::flat_map<mtpRequestId, RequestData> _sentRequests;\n\tbase::flat_map<int64, mtpRequestId> _requestByOffset;\n\n\tMTP::DcId _cdnDcId = 0;\n\tQByteArray _cdnToken;\n\tQByteArray _cdnEncryptionKey;\n\tQByteArray _cdnEncryptionIV;\n\tbase::flat_map<int64, CdnFileHash> _cdnFileHashes;\n\tbase::flat_map<RequestData, QByteArray> _cdnUncheckedParts;\n\tmtpRequestId _cdnHashesRequestId = 0;\n\n\trpl::lifetime _nonPremiumLimitSubscription;\n\n};\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/file_download.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/file_download.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_file_origin.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"core/application.h\"\n#include \"core/file_location.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/file_download_mtproto.h\"\n#include \"storage/file_download_web.h\"\n#include \"platform/platform_file_utilities.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"core/crash_reports.h\"\n#include \"base/bytes.h\"\n\nnamespace {\n\nclass FromMemoryLoader final : public FileLoader {\npublic:\n\tFromMemoryLoader(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QByteArray &data,\n\t\tconst QString &toFile,\n\t\tint64 loadSize,\n\t\tint64 fullSize,\n\t\tLocationType locationType,\n\t\tLoadToCacheSetting toCache,\n\t\tLoadFromCloudSetting fromCloud,\n\t\tbool autoLoading,\n\t\tuint8 cacheTag);\n\nprivate:\n\tStorage::Cache::Key cacheKey() const override;\n\tstd::optional<MediaKey> fileLocationKey() const override;\n\tvoid cancelHook() override;\n\tvoid startLoading() override;\n\n\tQByteArray _data;\n\n};\n\nFromMemoryLoader::FromMemoryLoader(\n\tnot_null<Main::Session*> session,\n\tconst QByteArray &data,\n\tconst QString &toFile,\n\tint64 loadSize,\n\tint64 fullSize,\n\tLocationType locationType,\n\tLoadToCacheSetting toCache,\n\tLoadFromCloudSetting fromCloud,\n\tbool autoLoading,\n\tuint8 cacheTag\n) : FileLoader(\n\tsession,\n\ttoFile,\n\tloadSize,\n\tfullSize,\n\tlocationType,\n\ttoCache,\n\tfromCloud,\n\tautoLoading,\n\tcacheTag)\n, _data(data) {\n}\n\nStorage::Cache::Key FromMemoryLoader::cacheKey() const {\n\treturn {};\n}\n\nstd::optional<MediaKey> FromMemoryLoader::fileLocationKey() const {\n\treturn std::nullopt;\n}\n\nvoid FromMemoryLoader::cancelHook() {\n}\n\nvoid FromMemoryLoader::startLoading() {\n\tfinishWithBytes(_data);\n}\n\n} // namespace\n\nFileLoader::FileLoader(\n\tnot_null<Main::Session*> session,\n\tconst QString &toFile,\n\tint64 loadSize,\n\tint64 fullSize,\n\tLocationType locationType,\n\tLoadToCacheSetting toCache,\n\tLoadFromCloudSetting fromCloud,\n\tbool autoLoading,\n\tuint8 cacheTag)\n: _session(session)\n, _autoLoading(autoLoading)\n, _cacheTag(cacheTag)\n, _filename(toFile)\n, _file(_filename)\n, _toCache(toCache)\n, _fromCloud(fromCloud)\n, _loadSize(loadSize)\n, _fullSize(fullSize)\n, _locationType(locationType) {\n\tExpects(_loadSize <= _fullSize);\n\tExpects(!_filename.isEmpty() || (_fullSize <= Storage::kMaxFileInMemory));\n}\n\nFileLoader::~FileLoader() {\n\tExpects(_finished);\n}\n\nMain::Session &FileLoader::session() const {\n\treturn *_session;\n}\n\nvoid FileLoader::finishWithBytes(const QByteArray &data) {\n\t_data = data;\n\t_localStatus = LocalStatus::Loaded;\n\tif (!_filename.isEmpty() && _toCache == LoadToCacheAsWell) {\n\t\tif (!_fileIsOpen) _fileIsOpen = _file.open(QIODevice::WriteOnly);\n\t\tif (!_fileIsOpen) {\n\t\t\tcancel(FailureReason::FileWriteFailure);\n\t\t\treturn;\n\t\t}\n\t\t_file.seek(0);\n\t\tif (_file.write(_data) != qint64(_data.size())) {\n\t\t\tcancel(FailureReason::FileWriteFailure);\n\t\t\treturn;\n\t\t}\n\t}\n\n\t_finished = true;\n\tif (_fileIsOpen) {\n\t\t_file.close();\n\t\t_fileIsOpen = false;\n\t\tPlatform::File::PostprocessDownloaded(\n\t\t\tQFileInfo(_file).absoluteFilePath());\n\t}\n\tconst auto session = _session;\n\t_updates.fire_done();\n\tsession->notifyDownloaderTaskFinished();\n}\n\nQImage FileLoader::imageData(int progressiveSizeLimit) const {\n\tif (_imageData.isNull() && _locationType == UnknownFileLocation) {\n\t\treadImage(progressiveSizeLimit);\n\t}\n\treturn _imageData;\n}\n\nvoid FileLoader::readImage(int progressiveSizeLimit) const {\n\tconst auto buffer = progressiveSizeLimit\n\t\t? QByteArray::fromRawData(_data.data(), progressiveSizeLimit)\n\t\t: _data;\n\tauto read = Images::Read({ .content = buffer });\n\tif (!read.image.isNull()) {\n\t\t_imageData = std::move(read.image);\n\t\t_imageFormat = read.format;\n\t}\n}\n\nData::FileOrigin FileLoader::fileOrigin() const {\n\treturn Data::FileOrigin();\n}\n\nfloat64 FileLoader::currentProgress() const {\n\treturn _finished\n\t\t? 1.\n\t\t: !_loadSize\n\t\t? 0.\n\t\t: std::clamp(float64(currentOffset()) / _loadSize, 0., 1.);\n}\n\nbool FileLoader::setFileName(const QString &fileName) {\n\tif (_toCache != LoadToCacheAsWell || !_filename.isEmpty()) {\n\t\treturn fileName.isEmpty() || (fileName == _filename);\n\t}\n\t_filename = fileName;\n\t_file.setFileName(_filename);\n\treturn true;\n}\n\nvoid FileLoader::permitLoadFromCloud() {\n\t_fromCloud = LoadFromCloudOrLocal;\n}\n\nvoid FileLoader::increaseLoadSize(int64 size, bool autoLoading) {\n\tExpects(size > _loadSize);\n\tExpects(size <= _fullSize);\n\n\t_loadSize = size;\n\t_autoLoading = autoLoading;\n}\n\nvoid FileLoader::notifyAboutProgress() {\n\t_updates.fire({});\n}\n\nvoid FileLoader::localLoaded(\n\t\tconst StorageImageSaved &result,\n\t\tconst QByteArray &imageFormat,\n\t\tconst QImage &imageData) {\n\t_localLoading = nullptr;\n\tif (result.data.isEmpty()) {\n\t\t_localStatus = LocalStatus::NotFound;\n\t\tstart();\n\t\treturn;\n\t}\n\tconst auto partial = result.data.startsWith(\"partial:\");\n\tconstexpr auto kPrefix = 8;\n\tif (partial\t&& result.data.size() < _loadSize + kPrefix) {\n\t\t_localStatus = LocalStatus::NotFound;\n\t\tif (checkForOpen()) {\n\t\t\tstartLoadingWithPartial(result.data);\n\t\t}\n\t\treturn;\n\t}\n\tif (!imageData.isNull()) {\n\t\t_imageFormat = imageFormat;\n\t\t_imageData = imageData;\n\t}\n\tfinishWithBytes(partial\n\t\t? QByteArray::fromRawData(\n\t\t\tresult.data.data() + kPrefix,\n\t\t\tresult.data.size() - kPrefix)\n\t\t: result.data);\n}\n\nvoid FileLoader::start() {\n\tif (_finished || tryLoadLocal()) {\n\t\treturn;\n\t} else if (_fromCloud == LoadFromLocalOnly) {\n\t\tcancel();\n\t\treturn;\n\t}\n\n\tif (checkForOpen()) {\n\t\tstartLoading();\n\t}\n}\n\nbool FileLoader::checkForOpen() {\n\tif (_filename.isEmpty()\n\t\t|| (_toCache != LoadToFileOnly)\n\t\t|| _fileIsOpen) {\n\t\treturn true;\n\t}\n\t_fileIsOpen = _file.open(QIODevice::WriteOnly);\n\tif (_fileIsOpen) {\n\t\treturn true;\n\t}\n\tcancel(FailureReason::FileWriteFailure);\n\treturn false;\n}\n\nvoid FileLoader::loadLocal(const Storage::Cache::Key &key) {\n\tconst auto readImage = (_locationType != AudioFileLocation);\n\tauto done = [=, guard = _localLoading.make_guard()](\n\t\t\tQByteArray &&value,\n\t\t\tQImage &&image,\n\t\t\tQByteArray &&format) mutable {\n\t\tcrl::on_main(std::move(guard), [\n\t\t\t=,\n\t\t\tvalue = std::move(value),\n\t\t\timage = std::move(image),\n\t\t\tformat = std::move(format)\n\t\t]() mutable {\n\t\t\tlocalLoaded(\n\t\t\t\tStorageImageSaved(std::move(value)),\n\t\t\t\tformat,\n\t\t\t\tstd::move(image));\n\t\t});\n\t};\n\t_session->data().cache().get(key, [=, callback = std::move(done)](\n\t\t\tQByteArray &&value) mutable {\n\t\tif (readImage && !value.startsWith(\"partial:\")) {\n\t\t\tcrl::async([\n\t\t\t\tvalue = std::move(value),\n\t\t\t\tdone = std::move(callback)\n\t\t\t]() mutable {\n\t\t\t\tauto read = Images::Read({ .content = value });\n\t\t\t\tif (!read.image.isNull()) {\n\t\t\t\t\tdone(\n\t\t\t\t\t\tstd::move(value),\n\t\t\t\t\t\tstd::move(read.image),\n\t\t\t\t\t\tstd::move(read.format));\n\t\t\t\t} else {\n\t\t\t\t\tdone(std::move(value), {}, {});\n\t\t\t\t}\n\t\t\t});\n\t\t} else {\n\t\t\tcallback(std::move(value), {}, {});\n\t\t}\n\t});\n}\n\nbool FileLoader::tryLoadLocal() {\n\tif (_localStatus == LocalStatus::NotFound\n\t\t|| _localStatus == LocalStatus::Loaded) {\n\t\treturn false;\n\t} else if (_localStatus == LocalStatus::Loading) {\n\t\treturn true;\n\t}\n\n\tif (_toCache == LoadToCacheAsWell) {\n\t\tconst auto key = cacheKey();\n\t\tif (key.low || key.high) {\n\t\t\tloadLocal(key);\n\t\t\tnotifyAboutProgress();\n\t\t}\n\t}\n\tif (_localStatus != LocalStatus::NotTried) {\n\t\treturn _finished;\n\t} else if (_localLoading) {\n\t\t_localStatus = LocalStatus::Loading;\n\t\treturn true;\n\t}\n\t_localStatus = LocalStatus::NotFound;\n\treturn false;\n}\n\nvoid FileLoader::cancel() {\n\tcancel(FailureReason::NoFailure);\n}\n\nvoid FileLoader::cancel(FailureReason fail) {\n\tconst auto started = (currentOffset() > 0);\n\n\tcancelHook();\n\n\t_cancelled = true;\n\t_finished = true;\n\tif (_fileIsOpen) {\n\t\t_file.close();\n\t\t_fileIsOpen = false;\n\t\t_file.remove();\n\t}\n\t_data = QByteArray();\n\n\tconst auto weak = base::make_weak(this);\n\tif (fail != FailureReason::NoFailure) {\n\t\t_updates.fire_error_copy({ fail, started });\n\t} else {\n\t\t_updates.fire_done();\n\t}\n\tif (weak) {\n\t\t_filename = QString();\n\t\t_file.setFileName(_filename);\n\t}\n}\n\nint64 FileLoader::currentOffset() const {\n\treturn (_fileIsOpen ? _file.size() : _data.size()) - _skippedBytes;\n}\n\nbool FileLoader::writeResultPart(int64 offset, bytes::const_span buffer) {\n\tExpects(!_finished);\n\n\tif (buffer.empty()) {\n\t\treturn true;\n\t}\n\tif (_fileIsOpen) {\n\t\tauto fsize = _file.size();\n\t\tif (offset < fsize) {\n\t\t\t_skippedBytes -= buffer.size();\n\t\t} else if (offset > fsize) {\n\t\t\t_skippedBytes += offset - fsize;\n\t\t}\n\t\t_file.seek(offset);\n\t\tif (_file.write(reinterpret_cast<const char*>(buffer.data()), buffer.size()) != qint64(buffer.size())) {\n\t\t\tcancel(FailureReason::FileWriteFailure);\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\t_data.reserve(offset + buffer.size());\n\tif (offset > _data.size()) {\n\t\t_skippedBytes += offset - _data.size();\n\t\t_data.resize(offset);\n\t}\n\tif (offset == _data.size()) {\n\t\t_data.append(reinterpret_cast<const char*>(buffer.data()), buffer.size());\n\t} else {\n\t\t_skippedBytes -= buffer.size();\n\t\tif (int64(offset + buffer.size()) > _data.size()) {\n\t\t\t_data.resize(offset + buffer.size());\n\t\t}\n\t\tconst auto dst = bytes::make_detached_span(_data).subspan(\n\t\t\toffset,\n\t\t\tbuffer.size());\n\t\tbytes::copy(dst, buffer);\n\t}\n\treturn true;\n}\n\nQByteArray FileLoader::readLoadedPartBack(int64 offset, int size) {\n\tExpects(offset >= 0 && size > 0);\n\n\tif (_fileIsOpen) {\n\t\tif (_file.openMode() == QIODevice::WriteOnly) {\n\t\t\t_file.close();\n\t\t\t_fileIsOpen = _file.open(QIODevice::ReadWrite);\n\t\t\tif (!_fileIsOpen) {\n\t\t\t\tcancel(FailureReason::FileWriteFailure);\n\t\t\t\treturn QByteArray();\n\t\t\t}\n\t\t}\n\t\tif (!_file.seek(offset)) {\n\t\t\treturn QByteArray();\n\t\t}\n\t\tauto result = _file.read(size);\n\t\treturn (result.size() == size) ? result : QByteArray();\n\t}\n\treturn (offset + size <= _data.size())\n\t\t? _data.mid(offset, size)\n\t\t: QByteArray();\n}\n\nbool FileLoader::finalizeResult() {\n\tExpects(!_finished);\n\n\tif (!_filename.isEmpty() && (_toCache == LoadToCacheAsWell)) {\n\t\tif (!_fileIsOpen) {\n\t\t\t_fileIsOpen = _file.open(QIODevice::WriteOnly);\n\t\t}\n\t\t_file.seek(0);\n\t\tif (!_fileIsOpen || _file.write(_data) != qint64(_data.size())) {\n\t\t\tcancel(FailureReason::FileWriteFailure);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t_finished = true;\n\tif (_fileIsOpen) {\n\t\t_file.close();\n\t\t_fileIsOpen = false;\n\t\tPlatform::File::PostprocessDownloaded(\n\t\t\tQFileInfo(_file).absoluteFilePath());\n\t}\n\tif (_localStatus == LocalStatus::NotFound) {\n\t\tif (const auto key = fileLocationKey()) {\n\t\t\tif (!_filename.isEmpty()) {\n\t\t\t\t_session->local().writeFileLocation(\n\t\t\t\t\t*key,\n\t\t\t\t\tCore::FileLocation(_filename));\n\t\t\t}\n\t\t}\n\t\tconst auto key = cacheKey();\n\t\tif ((_toCache == LoadToCacheAsWell)\n\t\t\t&& (_data.size() <= Storage::kMaxFileInMemory)\n\t\t\t&& (key.low || key.high)) {\n\t\t\t_session->data().cache().put(\n\t\t\t\tcacheKey(),\n\t\t\t\tStorage::Cache::Database::TaggedValue(\n\t\t\t\t\tbase::duplicate((!_fullSize || _data.size() == _fullSize)\n\t\t\t\t\t\t? _data\n\t\t\t\t\t\t: (\"partial:\" + _data)),\n\t\t\t\t\t_cacheTag));\n\t\t}\n\t}\n\tconst auto session = _session;\n\t_updates.fire_done();\n\tsession->notifyDownloaderTaskFinished();\n\treturn true;\n}\n\nstd::unique_ptr<FileLoader> CreateFileLoader(\n\t\tnot_null<Main::Session*> session,\n\t\tconst DownloadLocation &location,\n\t\tData::FileOrigin origin,\n\t\tconst QString &toFile,\n\t\tint64 loadSize,\n\t\tint64 fullSize,\n\t\tLocationType locationType,\n\t\tLoadToCacheSetting toCache,\n\t\tLoadFromCloudSetting fromCloud,\n\t\tbool autoLoading,\n\t\tuint8 cacheTag) {\n\tauto result = std::unique_ptr<FileLoader>();\n\tv::match(location.data, [&](const StorageFileLocation &data) {\n\t\tresult = std::make_unique<mtpFileLoader>(\n\t\t\tsession,\n\t\t\tdata,\n\t\t\torigin,\n\t\t\tlocationType,\n\t\t\ttoFile,\n\t\t\tloadSize,\n\t\t\tfullSize,\n\t\t\ttoCache,\n\t\t\tfromCloud,\n\t\t\tautoLoading,\n\t\t\tcacheTag);\n\t}, [&](const WebFileLocation &data) {\n\t\tresult = std::make_unique<mtpFileLoader>(\n\t\t\tsession,\n\t\t\tdata,\n\t\t\tloadSize,\n\t\t\tfullSize,\n\t\t\tfromCloud,\n\t\t\tautoLoading,\n\t\t\tcacheTag);\n\t}, [&](const GeoPointLocation &data) {\n\t\tresult = std::make_unique<mtpFileLoader>(\n\t\t\tsession,\n\t\t\tdata,\n\t\t\tloadSize,\n\t\t\tfullSize,\n\t\t\tfromCloud,\n\t\t\tautoLoading,\n\t\t\tcacheTag);\n\t}, [&](const PlainUrlLocation &data) {\n\t\tresult = std::make_unique<webFileLoader>(\n\t\t\tsession,\n\t\t\tdata.url,\n\t\t\ttoFile,\n\t\t\tfromCloud,\n\t\t\tautoLoading,\n\t\t\tcacheTag);\n\t}, [&](const AudioAlbumThumbLocation &data) {\n\t\tresult = std::make_unique<mtpFileLoader>(\n\t\t\tsession,\n\t\t\tdata,\n\t\t\tloadSize,\n\t\t\tfullSize,\n\t\t\tfromCloud,\n\t\t\tautoLoading,\n\t\t\tcacheTag);\n\t}, [&](const InMemoryLocation &data) {\n\t\tresult = std::make_unique<FromMemoryLoader>(\n\t\t\tsession,\n\t\t\tdata.bytes,\n\t\t\ttoFile,\n\t\t\tloadSize,\n\t\t\tfullSize,\n\t\t\tlocationType,\n\t\t\ttoCache,\n\t\t\tLoadFromCloudOrLocal,\n\t\t\tautoLoading,\n\t\t\tcacheTag);\n\t});\n\n\tEnsures(result != nullptr);\n\treturn result;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/file_download.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/binary_guard.h\"\n#include \"base/weak_ptr.h\"\n\n#include <QtNetwork/QNetworkReply>\n\nnamespace Data {\nstruct FileOrigin;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Storage {\nnamespace Cache {\nstruct Key;\n} // namespace Cache\n\n// 10 MB max file could be hold in memory\n// This value is used in local cache database settings!\nconstexpr auto kMaxFileInMemory = 10 * 1024 * 1024;\n\n// 2 MB stickers hold in memory, auto loaded and displayed inline\nconstexpr auto kMaxStickerBytesSize = 2 * 1024 * 1024;\n\n// 10 MB GIF and mp4 animations held in memory while playing\nconstexpr auto kMaxWallPaperInMemory = kMaxFileInMemory;\nconstexpr auto kMaxAnimationInMemory = kMaxFileInMemory;\n\n// 4096x4096 is max area.\nconstexpr auto kMaxWallPaperDimension = 4096;\n\n} // namespace Storage\n\nstruct StorageImageSaved {\n\tStorageImageSaved() = default;\n\texplicit StorageImageSaved(const QByteArray &data) : data(data) {\n\t}\n\n\tQByteArray data;\n\n};\n\nclass FileLoader : public base::has_weak_ptr {\npublic:\n\tenum class FailureReason {\n\t\tNoFailure,\n\t\tFileWriteFailure,\n\t\tOtherFailure,\n\t};\n\n\tstruct Error {\n\t\tFailureReason failureReason = FailureReason::NoFailure;\n\t\tbool started = false;\n\t};\n\n\tFileLoader(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &toFile,\n\t\tint64 loadSize,\n\t\tint64 fullSize,\n\t\tLocationType locationType,\n\t\tLoadToCacheSetting toCache,\n\t\tLoadFromCloudSetting fromCloud,\n\t\tbool autoLoading,\n\t\tuint8 cacheTag);\n\tvirtual ~FileLoader();\n\n\t[[nodiscard]] Main::Session &session() const;\n\n\t[[nodiscard]] bool finished() const {\n\t\treturn _finished;\n\t}\n\tvoid finishWithBytes(const QByteArray &data);\n\t[[nodiscard]] bool cancelled() const {\n\t\treturn _cancelled;\n\t}\n\t[[nodiscard]] const QByteArray &bytes() const {\n\t\treturn _data;\n\t}\n\t[[nodiscard]] virtual uint64 objId() const {\n\t\treturn 0;\n\t}\n\t[[nodiscard]] QImage imageData(int progressiveSizeLimit = 0) const;\n\t[[nodiscard]] QString fileName() const {\n\t\treturn _filename;\n\t}\n\t// Used in MainWidget::documentLoadFailed.\n\t[[nodiscard]] virtual Data::FileOrigin fileOrigin() const;\n\t[[nodiscard]] float64 currentProgress() const;\n\t[[nodiscard]] virtual int64 currentOffset() const;\n\t[[nodiscard]] int64 fullSize() const {\n\t\treturn _fullSize;\n\t}\n\t[[nodiscard]] int64 loadSize() const {\n\t\treturn _loadSize;\n\t}\n\n\tbool setFileName(const QString &filename); // set filename for loaders to cache\n\tvoid permitLoadFromCloud();\n\tvoid increaseLoadSize(int64 size, bool autoLoading);\n\n\tvoid start();\n\tvoid cancel();\n\n\t[[nodiscard]] bool loadingLocal() const {\n\t\treturn (_localStatus == LocalStatus::Loading);\n\t}\n\t[[nodiscard]] bool autoLoading() const {\n\t\treturn _autoLoading;\n\t}\n\n\tvoid localLoaded(\n\t\tconst StorageImageSaved &result,\n\t\tconst QByteArray &imageFormat,\n\t\tconst QImage &imageData);\n\n\t[[nodiscard]] rpl::producer<rpl::empty_value, Error> updates() const {\n\t\treturn _updates.events();\n\t}\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprotected:\n\tenum class LocalStatus {\n\t\tNotTried,\n\t\tNotFound,\n\t\tLoading,\n\t\tLoaded,\n\t};\n\n\tvoid readImage(int progressiveSizeLimit) const;\n\n\tbool checkForOpen();\n\tbool tryLoadLocal();\n\tvoid loadLocal(const Storage::Cache::Key &key);\n\tvirtual Storage::Cache::Key cacheKey() const = 0;\n\tvirtual std::optional<MediaKey> fileLocationKey() const = 0;\n\tvirtual void cancelHook() = 0;\n\tvirtual void startLoading() = 0;\n\tvirtual void startLoadingWithPartial(const QByteArray &data) {\n\t\tstartLoading();\n\t}\n\n\tvoid cancel(FailureReason failed);\n\n\tvoid notifyAboutProgress();\n\n\tbool writeResultPart(int64 offset, bytes::const_span buffer);\n\tbool finalizeResult();\n\t[[nodiscard]] QByteArray readLoadedPartBack(int64 offset, int size);\n\n\tconst not_null<Main::Session*> _session;\n\n\tbool _autoLoading = false;\n\tuint8 _cacheTag = 0;\n\tbool _finished = false;\n\tbool _cancelled = false;\n\tmutable LocalStatus _localStatus = LocalStatus::NotTried;\n\n\tQString _filename;\n\tQFile _file;\n\tbool _fileIsOpen = false;\n\n\tLoadToCacheSetting _toCache;\n\tLoadFromCloudSetting _fromCloud;\n\n\tQByteArray _data;\n\n\tint64 _loadSize = 0;\n\tint64 _fullSize = 0;\n\tint64 _skippedBytes = 0;\n\tLocationType _locationType = LocationType();\n\n\tbase::binary_guard _localLoading;\n\tmutable QByteArray _imageFormat;\n\tmutable QImage _imageData;\n\n\trpl::lifetime _lifetime;\n\trpl::event_stream<rpl::empty_value, Error> _updates;\n\n};\n\n[[nodiscard]] std::unique_ptr<FileLoader> CreateFileLoader(\n\tnot_null<Main::Session*> session,\n\tconst DownloadLocation &location,\n\tData::FileOrigin origin,\n\tconst QString &toFile,\n\tint64 loadSize,\n\tint64 fullSize,\n\tLocationType locationType,\n\tLoadToCacheSetting toCache,\n\tLoadFromCloudSetting fromCloud,\n\tbool autoLoading,\n\tuint8 cacheTag);\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/file_download_mtproto.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/file_download_mtproto.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_file_origin.h\"\n#include \"storage/cache/storage_cache_types.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"mtproto/mtp_instance.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"mtproto/mtproto_auth_key.h\"\n\nmtpFileLoader::mtpFileLoader(\n\tnot_null<Main::Session*> session,\n\tconst StorageFileLocation &location,\n\tData::FileOrigin origin,\n\tLocationType type,\n\tconst QString &to,\n\tint64 loadSize,\n\tint64 fullSize,\n\tLoadToCacheSetting toCache,\n\tLoadFromCloudSetting fromCloud,\n\tbool autoLoading,\n\tuint8 cacheTag)\n: FileLoader(\n\tsession,\n\tto,\n\tloadSize,\n\tfullSize,\n\ttype,\n\ttoCache,\n\tfromCloud,\n\tautoLoading,\n\tcacheTag)\n, DownloadMtprotoTask(&session->downloader(), location, origin) {\n}\n\nmtpFileLoader::mtpFileLoader(\n\tnot_null<Main::Session*> session,\n\tconst WebFileLocation &location,\n\tint64 loadSize,\n\tint64 fullSize,\n\tLoadFromCloudSetting fromCloud,\n\tbool autoLoading,\n\tuint8 cacheTag)\n: FileLoader(\n\tsession,\n\tQString(),\n\tloadSize,\n\tfullSize,\n\tUnknownFileLocation,\n\tLoadToCacheAsWell,\n\tfromCloud,\n\tautoLoading,\n\tcacheTag)\n, DownloadMtprotoTask(\n\t&session->downloader(),\n\tsession->serverConfig().webFileDcId,\n\t{ location }) {\n}\n\nmtpFileLoader::mtpFileLoader(\n\tnot_null<Main::Session*> session,\n\tconst GeoPointLocation &location,\n\tint64 loadSize,\n\tint64 fullSize,\n\tLoadFromCloudSetting fromCloud,\n\tbool autoLoading,\n\tuint8 cacheTag)\n: FileLoader(\n\tsession,\n\tQString(),\n\tloadSize,\n\tfullSize,\n\tUnknownFileLocation,\n\tLoadToCacheAsWell,\n\tfromCloud,\n\tautoLoading,\n\tcacheTag)\n, DownloadMtprotoTask(\n\t&session->downloader(),\n\tsession->serverConfig().webFileDcId,\n\t{ location }) {\n}\n\nmtpFileLoader::mtpFileLoader(\n\tnot_null<Main::Session*> session,\n\tconst AudioAlbumThumbLocation &location,\n\tint64 loadSize,\n\tint64 fullSize,\n\tLoadFromCloudSetting fromCloud,\n\tbool autoLoading,\n\tuint8 cacheTag)\n: FileLoader(\n\tsession,\n\tQString(),\n\tloadSize,\n\tfullSize,\n\tUnknownFileLocation,\n\tLoadToCacheAsWell,\n\tfromCloud,\n\tautoLoading,\n\tcacheTag)\n, DownloadMtprotoTask(\n\t&session->downloader(),\n\tsession->serverConfig().webFileDcId,\n\t{ location }) {\n}\n\nmtpFileLoader::~mtpFileLoader() {\n\tif (!_finished) {\n\t\tcancel();\n\t}\n}\n\nData::FileOrigin mtpFileLoader::fileOrigin() const {\n\treturn DownloadMtprotoTask::fileOrigin();\n}\n\nuint64 mtpFileLoader::objId() const {\n\treturn DownloadMtprotoTask::objectId();\n}\n\nbool mtpFileLoader::readyToRequest() const {\n\treturn !_finished\n\t\t&& !_lastComplete\n\t\t&& (_fullSize != 0 || !haveSentRequests())\n\t\t&& (!_fullSize || _nextRequestOffset < _loadSize);\n}\n\nint64 mtpFileLoader::takeNextRequestOffset() {\n\tExpects(readyToRequest());\n\n\tconst auto result = _nextRequestOffset;\n\t_nextRequestOffset += Storage::kDownloadPartSize;\n\treturn result;\n}\n\nbool mtpFileLoader::feedPart(int64 offset, const QByteArray &bytes) {\n\tconst auto buffer = bytes::make_span(bytes);\n\tif (!writeResultPart(offset, buffer)) {\n\t\treturn false;\n\t}\n\tif (buffer.empty() || (buffer.size() % 1024)) { // bad next offset\n\t\t_lastComplete = true;\n\t}\n\tconst auto finished = !haveSentRequests()\n\t\t&& (_lastComplete || (_fullSize && _nextRequestOffset >= _loadSize));\n\tif (finished) {\n\t\tremoveFromQueue();\n\t\tif (!finalizeResult()) {\n\t\t\treturn false;\n\t\t}\n\t} else {\n\t\tnotifyAboutProgress();\n\t}\n\treturn true;\n}\n\nvoid mtpFileLoader::cancelOnFail() {\n\tcancel(FailureReason::OtherFailure);\n}\n\nbool mtpFileLoader::setWebFileSizeHook(int64 size) {\n\tif (!_fullSize || _fullSize == size) {\n\t\t_fullSize = _loadSize = size;\n\t\treturn true;\n\t}\n\tLOG((\"MTP Error: \"\n\t\t\"Bad size provided by bot for webDocument: %1, real: %2\"\n\t\t).arg(_fullSize\n\t\t).arg(size));\n\tcancel(FailureReason::OtherFailure);\n\treturn false;\n}\n\nvoid mtpFileLoader::startLoading() {\n\taddToQueue();\n}\n\nvoid mtpFileLoader::startLoadingWithPartial(const QByteArray &data) {\n\tExpects(data.startsWith(\"partial:\"));\n\n\tconstexpr auto kPrefix = 8;\n\tconst auto parts = (data.size() - kPrefix) / Storage::kDownloadPartSize;\n\tconst auto use = parts * int64(Storage::kDownloadPartSize);\n\tif (use > 0) {\n\t\t_nextRequestOffset = use;\n\t\tfeedPart(0, QByteArray::fromRawData(data.data() + kPrefix, use));\n\t}\n\tstartLoading();\n}\n\nvoid mtpFileLoader::cancelHook() {\n\tcancelAllRequests();\n}\n\nStorage::Cache::Key mtpFileLoader::cacheKey() const {\n\treturn v::match(location().data, [&](const WebFileLocation &location) {\n\t\treturn Data::WebDocumentCacheKey(location);\n\t}, [&](const GeoPointLocation &location) {\n\t\treturn Data::GeoPointCacheKey(location);\n\t}, [&](const StorageFileLocation &location) {\n\t\treturn location.cacheKey();\n\t}, [&](const AudioAlbumThumbLocation &location) {\n\t\treturn Data::AudioAlbumThumbCacheKey(location);\n\t});\n}\n\nstd::optional<MediaKey> mtpFileLoader::fileLocationKey() const {\n\tif (_locationType != UnknownFileLocation) {\n\t\treturn mediaKey(_locationType, dcId(), objId());\n\t}\n\treturn std::nullopt;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/file_download_mtproto.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"storage/file_download.h\"\n#include \"storage/download_manager_mtproto.h\"\n\nclass mtpFileLoader final\n\t: public FileLoader\n\t, private Storage::DownloadMtprotoTask {\npublic:\n\tmtpFileLoader(\n\t\tnot_null<Main::Session*> session,\n\t\tconst StorageFileLocation &location,\n\t\tData::FileOrigin origin,\n\t\tLocationType type,\n\t\tconst QString &toFile,\n\t\tint64 loadSize,\n\t\tint64 fullSize,\n\t\tLoadToCacheSetting toCache,\n\t\tLoadFromCloudSetting fromCloud,\n\t\tbool autoLoading,\n\t\tuint8 cacheTag);\n\tmtpFileLoader(\n\t\tnot_null<Main::Session*> session,\n\t\tconst WebFileLocation &location,\n\t\tint64 loadSize,\n\t\tint64 fullSize,\n\t\tLoadFromCloudSetting fromCloud,\n\t\tbool autoLoading,\n\t\tuint8 cacheTag);\n\tmtpFileLoader(\n\t\tnot_null<Main::Session*> session,\n\t\tconst GeoPointLocation &location,\n\t\tint64 loadSize,\n\t\tint64 fullSize,\n\t\tLoadFromCloudSetting fromCloud,\n\t\tbool autoLoading,\n\t\tuint8 cacheTag);\n\tmtpFileLoader(\n\t\tnot_null<Main::Session*> session,\n\t\tconst AudioAlbumThumbLocation &location,\n\t\tint64 loadSize,\n\t\tint64 fullSize,\n\t\tLoadFromCloudSetting fromCloud,\n\t\tbool autoLoading,\n\t\tuint8 cacheTag);\n\t~mtpFileLoader();\n\n\tData::FileOrigin fileOrigin() const override;\n\tuint64 objId() const override;\n\nprivate:\n\tStorage::Cache::Key cacheKey() const override;\n\tstd::optional<MediaKey> fileLocationKey() const override;\n\tvoid startLoading() override;\n\tvoid startLoadingWithPartial(const QByteArray &data) override;\n\tvoid cancelHook() override;\n\n\tbool readyToRequest() const override;\n\tint64 takeNextRequestOffset() override;\n\tbool feedPart(int64 offset, const QByteArray &bytes) override;\n\tvoid cancelOnFail() override;\n\tbool setWebFileSizeHook(int64 size) override;\n\n\tbool _lastComplete = false;\n\tint64 _nextRequestOffset = 0;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/file_download_web.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/file_download_web.h\"\n\n#include \"storage/cache/storage_cache_types.h\"\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n\n#include <QtNetwork/QAuthenticator>\n\nnamespace {\n\nconstexpr auto kMaxWebFileQueries = 8;\nconstexpr auto kMaxHttpRedirects = 5;\nconstexpr auto kResetDownloadPrioritiesTimeout = crl::time(200);\nconstexpr auto kMaxWebFile = 4000 * int64(1024 * 1024);\n\nstd::weak_ptr<WebLoadManager> GlobalLoadManager;\n\n[[nodiscard]] std::shared_ptr<WebLoadManager> GetManager() {\n\tauto result = GlobalLoadManager.lock();\n\tif (!result) {\n\t\tGlobalLoadManager = result = std::make_shared<WebLoadManager>();\n\t}\n\treturn result;\n}\n\nenum class ProcessResult {\n\tError,\n\tProgress,\n\tFinished,\n};\n\nenum class Error {\n};\n\nstruct Progress {\n\tqint64 ready = 0;\n\tqint64 total = 0;\n\tQByteArray streamed;\n};\n\nusing Update = std::variant<Progress, QByteArray, Error>;\n\nstruct UpdateForLoader {\n\tnot_null<webFileLoader*> loader;\n\tUpdate data;\n};\n\n} // namespace\n\nclass WebLoadManager final : public base::has_weak_ptr {\npublic:\n\tWebLoadManager();\n\t~WebLoadManager();\n\n\tvoid enqueue(not_null<webFileLoader*> loader);\n\tvoid remove(not_null<webFileLoader*> loader);\n\n\t[[nodiscard]] rpl::producer<Update> updates(\n\t\tnot_null<webFileLoader*> loader) const;\n\nprivate:\n\tstruct Enqueued {\n\t\tint id = 0;\n\t\tQString url;\n\t\tbool stream = false;\n\t};\n\tstruct Sent {\n\t\tQString url;\n\t\tnot_null<QNetworkReply*> reply;\n\t\tbool stream = false;\n\t\tQByteArray data;\n\t\tint64 ready = 0;\n\t\tint64 total = 0;\n\t\tint redirectsLeft = kMaxHttpRedirects;\n\t};\n\n\t// Constructor.\n\tvoid handleNetworkErrors();\n\n\t// Worker thread.\n\tvoid enqueue(int id, const QString &url, bool stream);\n\tvoid remove(int id);\n\tvoid resetGeneration();\n\tvoid checkSendNext();\n\tvoid send(const Enqueued &entry);\n\t[[nodiscard]] not_null<QNetworkReply*> send(int id, const QString &url);\n\t[[nodiscard]] Sent *findSent(int id, not_null<QNetworkReply*> reply);\n\tvoid removeSent(int id);\n\tvoid progress(\n\t\tint id,\n\t\tnot_null<QNetworkReply*> reply,\n\t\tint64 ready,\n\t\tint64 total);\n\tvoid failed(\n\t\tint id,\n\t\tnot_null<QNetworkReply*> reply,\n\t\tQNetworkReply::NetworkError error);\n\tvoid redirect(int id, not_null<QNetworkReply*> reply);\n\tvoid notify(\n\t\tint id,\n\t\tnot_null<QNetworkReply*> reply,\n\t\tint64 ready,\n\t\tint64 total);\n\tvoid failed(int id, not_null<QNetworkReply*> reply);\n\tvoid finished(int id, not_null<QNetworkReply*> reply);\n\tvoid deleteDeferred(not_null<QNetworkReply*> reply);\n\tvoid queueProgressUpdate(\n\t\tint id,\n\t\tint64 ready,\n\t\tint64 total,\n\t\tQByteArray streamed);\n\tvoid queueFailedUpdate(int id);\n\tvoid queueFinishedUpdate(int id, const QByteArray &data);\n\tvoid clear();\n\n\t// Main thread.\n\tvoid sendUpdate(int id, Update &&data);\n\n\tQThread _thread;\n\tstd::unique_ptr<QNetworkAccessManager> _network;\n\tbase::Timer _resetGenerationTimer;\n\n\t// Main thread.\n\trpl::event_stream<UpdateForLoader> _updates;\n\tint _autoincrement = 0;\n\tbase::flat_map<not_null<webFileLoader*>, int> _ids;\n\n\t// Worker thread.\n\tstd::deque<Enqueued> _queue;\n\tstd::deque<Enqueued> _previousGeneration;\n\tbase::flat_map<int, Sent> _sent;\n\tstd::vector<QPointer<QNetworkReply>> _repliesBeingDeleted;\n\n};\n\nWebLoadManager::WebLoadManager()\n: _network(std::make_unique<QNetworkAccessManager>())\n, _resetGenerationTimer(&_thread, [=] { resetGeneration(); }) {\n\thandleNetworkErrors();\n\n\t_network->moveToThread(&_thread);\n\tQObject::connect(&_thread, &QThread::finished, [=] {\n\t\tclear();\n\t\t_network = nullptr;\n\t});\n\t_thread.start();\n}\n\nvoid WebLoadManager::handleNetworkErrors() {\n\tconst auto fail = [=](QNetworkReply *reply) {\n\t\tfor (const auto &[id, sent] : _sent) {\n\t\t\tif (sent.reply == reply) {\n\t\t\t\tfailed(id, reply);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t};\n\tQObject::connect(\n\t\t_network.get(),\n\t\t&QNetworkAccessManager::authenticationRequired,\n\t\tfail);\n\tQObject::connect(\n\t\t_network.get(),\n\t\t&QNetworkAccessManager::sslErrors,\n\t\tfail);\n}\n\nWebLoadManager::~WebLoadManager() {\n\t_thread.quit();\n\t_thread.wait();\n}\n\n[[nodiscard]] rpl::producer<Update> WebLoadManager::updates(\n\t\tnot_null<webFileLoader*> loader) const {\n\treturn _updates.events(\n\t) | rpl::filter([=](const UpdateForLoader &update) {\n\t\treturn (update.loader == loader);\n\t}) | rpl::map([=](UpdateForLoader &&update) {\n\t\treturn std::move(update.data);\n\t});\n}\n\nvoid WebLoadManager::enqueue(not_null<webFileLoader*> loader) {\n\tconst auto id = [&] {\n\t\tconst auto i = _ids.find(loader);\n\t\treturn (i != end(_ids))\n\t\t\t? i->second\n\t\t\t: _ids.emplace(loader, ++_autoincrement).first->second;\n\t}();\n\tconst auto url = loader->url();\n\tconst auto stream = loader->streamLoading();\n\tInvokeQueued(_network.get(), [=] {\n\t\tenqueue(id, url, stream);\n\t});\n}\n\nvoid WebLoadManager::remove(not_null<webFileLoader*> loader) {\n\tconst auto i = _ids.find(loader);\n\tif (i == end(_ids)) {\n\t\treturn;\n\t}\n\tconst auto id = i->second;\n\t_ids.erase(i);\n\tInvokeQueued(_network.get(), [=] {\n\t\tremove(id);\n\t});\n}\n\nvoid WebLoadManager::enqueue(int id, const QString &url, bool stream) {\n\tconst auto i = ranges::find(_queue, id, &Enqueued::id);\n\tif (i != end(_queue)) {\n\t\treturn;\n\t}\n\t_previousGeneration.erase(\n\t\tranges::remove(_previousGeneration, id, &Enqueued::id),\n\t\tend(_previousGeneration));\n\t_queue.push_back(Enqueued{ id, url, stream });\n\tif (!_resetGenerationTimer.isActive()) {\n\t\t_resetGenerationTimer.callOnce(kResetDownloadPrioritiesTimeout);\n\t}\n\tcheckSendNext();\n}\n\nvoid WebLoadManager::remove(int id) {\n\t_queue.erase(ranges::remove(_queue, id, &Enqueued::id), end(_queue));\n\t_previousGeneration.erase(\n\t\tranges::remove(_previousGeneration, id, &Enqueued::id),\n\t\tend(_previousGeneration));\n\tremoveSent(id);\n}\n\nvoid WebLoadManager::resetGeneration() {\n\tif (!_previousGeneration.empty()) {\n\t\tstd::copy(\n\t\t\tbegin(_previousGeneration),\n\t\t\tend(_previousGeneration),\n\t\t\tstd::back_inserter(_queue));\n\t\t_previousGeneration.clear();\n\t}\n\tstd::swap(_queue, _previousGeneration);\n}\n\nvoid WebLoadManager::checkSendNext() {\n\tif (_sent.size() >= kMaxWebFileQueries\n\t\t|| (_queue.empty() && _previousGeneration.empty())) {\n\t\treturn;\n\t}\n\tconst auto entry = _queue.empty()\n\t\t? _previousGeneration.front()\n\t\t: _queue.front();\n\t(_queue.empty() ? _previousGeneration : _queue).pop_front();\n\tsend(entry);\n}\n\nvoid WebLoadManager::send(const Enqueued &entry) {\n\tconst auto id = entry.id;\n\tconst auto url = entry.url;\n\t_sent.emplace(id, Sent{ url, send(id, url), entry.stream });\n}\n\nvoid WebLoadManager::removeSent(int id) {\n\tif (const auto i = _sent.find(id); i != end(_sent)) {\n\t\tdeleteDeferred(i->second.reply);\n\t\t_sent.erase(i);\n\t\tcheckSendNext();\n\t}\n}\n\nnot_null<QNetworkReply*> WebLoadManager::send(int id, const QString &url) {\n\tconst auto result = _network->get(QNetworkRequest(url));\n\tconst auto handleProgress = [=](qint64 ready, qint64 total) {\n\t\tprogress(id, result, ready, total);\n\t};\n\tconst auto handleError = [=](QNetworkReply::NetworkError error) {\n\t\tfailed(id, result, error);\n\t};\n\tQObject::connect(\n\t\tresult,\n\t\t&QNetworkReply::downloadProgress,\n\t\thandleProgress);\n\tQObject::connect(result, &QNetworkReply::errorOccurred, handleError);\n\treturn result;\n}\n\nWebLoadManager::Sent *WebLoadManager::findSent(\n\t\tint id,\n\t\tnot_null<QNetworkReply*> reply) {\n\tconst auto i = _sent.find(id);\n\treturn (i != end(_sent) && i->second.reply == reply)\n\t\t? &i->second\n\t\t: nullptr;\n}\n\nvoid WebLoadManager::progress(\n\t\tint id,\n\t\tnot_null<QNetworkReply*> reply,\n\t\tint64 ready,\n\t\tint64 total) {\n\tif (total <= 0) {\n\t\tconst auto originalContentLength = reply->attribute(\n\t\t\tQNetworkRequest::OriginalContentLengthAttribute);\n\t\tif (originalContentLength.isValid()) {\n\t\t\ttotal = originalContentLength.toLongLong();\n\t\t}\n\t}\n\tconst auto statusCode = reply->attribute(\n\t\tQNetworkRequest::HttpStatusCodeAttribute);\n\tconst auto status = statusCode.isValid() ? statusCode.toInt() : 200;\n\tif (status == 301 || status == 302) {\n\t\tredirect(id, reply);\n\t} else if (status != 200 && status != 206 && status != 416) {\n\t\tLOG((\"Network Error: \"\n\t\t\t\"Bad HTTP status received in WebLoadManager::onProgress() %1\"\n\t\t\t).arg(status));\n\t\tfailed(id, reply);\n\t} else {\n\t\tnotify(id, reply, ready, std::max(ready, total));\n\t}\n}\n\nvoid WebLoadManager::redirect(int id, not_null<QNetworkReply*> reply) {\n\tconst auto header = reply->header(QNetworkRequest::LocationHeader);\n\tconst auto url = header.toString();\n\tif (url.isEmpty()) {\n\t\treturn;\n\t}\n\n\tif (const auto sent = findSent(id, reply)) {\n\t\tif (!sent->redirectsLeft--) {\n\t\t\tLOG((\"Network Error: \"\n\t\t\t\t\"Too many HTTP redirects in onFinished() \"\n\t\t\t\t\"for web file loader: %1\").arg(url));\n\t\t\tfailed(id, reply);\n\t\t\treturn;\n\t\t}\n\t\tdeleteDeferred(reply);\n\t\tsent->url = url;\n\t\tsent->reply = send(id, url);\n\t}\n}\n\nvoid WebLoadManager::notify(\n\t\tint id,\n\t\tnot_null<QNetworkReply*> reply,\n\t\tint64 ready,\n\t\tint64 total) {\n\tif (const auto sent = findSent(id, reply)) {\n\t\tsent->ready = ready;\n\t\tsent->total = std::max(total, int64(0));\n\t\tif (total <= 0) {\n\t\t\tLOG((\"Network Error: \"\n\t\t\t\t\"Bad size received for HTTP download progress \"\n\t\t\t\t\"in WebLoadManager::onProgress(): %1 / %2 (bytes %3)\"\n\t\t\t\t).arg(ready\n\t\t\t\t).arg(total\n\t\t\t\t).arg(sent->data.size()));\n\t\t\tfailed(id, reply);\n\t\t\treturn;\n\t\t}\n\t\tauto bytes = reply->readAll();\n\t\tif (sent->stream) {\n\t\t\tif (total > kMaxWebFile) {\n\t\t\t\tLOG((\"Network Error: \"\n\t\t\t\t\t\"Bad size received for HTTP download progress \"\n\t\t\t\t\t\"in WebLoadManager::onProgress(): %1 / %2\"\n\t\t\t\t\t).arg(ready\n\t\t\t\t\t).arg(total));\n\t\t\t\tfailed(id, reply);\n\t\t\t} else {\n\t\t\t\tqueueProgressUpdate(\n\t\t\t\t\tid,\n\t\t\t\t\tsent->ready,\n\t\t\t\t\tsent->total,\n\t\t\t\t\tstd::move(bytes));\n\t\t\t\tif (ready >= total) {\n\t\t\t\t\tfinished(id, reply);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tsent->data.append(std::move(bytes));\n\t\t\tif (total > Storage::kMaxFileInMemory\n\t\t\t\t|| sent->data.size() > Storage::kMaxFileInMemory) {\n\t\t\t\tLOG((\"Network Error: \"\n\t\t\t\t\t\"Bad size received for HTTP download progress \"\n\t\t\t\t\t\"in WebLoadManager::onProgress(): %1 / %2 (bytes %3)\"\n\t\t\t\t\t).arg(ready\n\t\t\t\t\t).arg(total\n\t\t\t\t\t).arg(sent->data.size()));\n\t\t\t\tfailed(id, reply);\n\t\t\t} else if (ready >= total) {\n\t\t\t\tfinished(id, reply);\n\t\t\t} else {\n\t\t\t\tqueueProgressUpdate(id, sent->ready, sent->total, {});\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid WebLoadManager::failed(\n\t\tint id,\n\t\tnot_null<QNetworkReply*> reply,\n\t\tQNetworkReply::NetworkError error) {\n\tif (const auto sent = findSent(id, reply)) {\n\t\tLOG((\"Network Error: \"\n\t\t\t\"Failed to request '%1', error %2 (%3)\"\n\t\t\t).arg(sent->url\n\t\t\t).arg(int(error)\n\t\t\t).arg(reply->errorString()));\n\t\tfailed(id, reply);\n\t}\n}\n\nvoid WebLoadManager::failed(int id, not_null<QNetworkReply*> reply) {\n\tif ([[maybe_unused]] const auto sent = findSent(id, reply)) {\n\t\tremoveSent(id);\n\t\tqueueFailedUpdate(id);\n\t}\n}\n\nvoid WebLoadManager::deleteDeferred(not_null<QNetworkReply*> reply) {\n\treply->deleteLater();\n\t_repliesBeingDeleted.erase(\n\t\tranges::remove(_repliesBeingDeleted, nullptr),\n\t\tend(_repliesBeingDeleted));\n\t_repliesBeingDeleted.emplace_back(reply.get());\n}\n\nvoid WebLoadManager::finished(int id, not_null<QNetworkReply*> reply) {\n\tif (const auto sent = findSent(id, reply)) {\n\t\tconst auto data = base::take(sent->data);\n\t\tremoveSent(id);\n\t\tqueueFinishedUpdate(id, data);\n\t}\n}\n\nvoid WebLoadManager::clear() {\n\tfor (const auto &[id, sent] : base::take(_sent)) {\n\t\tsent.reply->abort();\n\t\tdelete sent.reply;\n\t}\n\tfor (const auto &reply : base::take(_repliesBeingDeleted)) {\n\t\tif (reply) {\n\t\t\tdelete reply;\n\t\t}\n\t}\n}\n\nvoid WebLoadManager::queueProgressUpdate(\n\t\tint id,\n\t\tint64 ready,\n\t\tint64 total,\n\t\tQByteArray streamed) {\n\tcrl::on_main(this, [=, bytes = std::move(streamed)]() mutable {\n\t\tsendUpdate(id, Progress{ ready, total, std::move(bytes) });\n\t});\n}\n\nvoid WebLoadManager::queueFailedUpdate(int id) {\n\tcrl::on_main(this, [=] {\n\t\tsendUpdate(id, Error{});\n\t});\n}\n\nvoid WebLoadManager::queueFinishedUpdate(int id, const QByteArray &data) {\n\tcrl::on_main(this, [=] {\n\t\tfor (const auto &[loader, loaderId] : _ids) {\n\t\t\tif (loaderId == id) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tsendUpdate(id, QByteArray(data));\n\t});\n}\n\nvoid WebLoadManager::sendUpdate(int id, Update &&data) {\n\tfor (const auto &[loader, loaderId] : _ids) {\n\t\tif (loaderId == id) {\n\t\t\t_updates.fire(UpdateForLoader{ loader, std::move(data) });\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nwebFileLoader::webFileLoader(\n\tnot_null<Main::Session*> session,\n\tconst QString &url,\n\tconst QString &to,\n\tLoadFromCloudSetting fromCloud,\n\tbool autoLoading,\n\tuint8 cacheTag)\n: FileLoader(\n\tsession,\n\tQString(),\n\t0,\n\t0,\n\tUnknownFileLocation,\n\tLoadToCacheAsWell,\n\tfromCloud,\n\tautoLoading,\n\tcacheTag)\n, _url(url) {\n}\n\nwebFileLoader::webFileLoader(\n\tnot_null<Main::Session*> session,\n\tconst QString &url,\n\tconst QString &path,\n\tWebRequestType type)\n: FileLoader(\n\tsession,\n\tpath,\n\t0,\n\t0,\n\tUnknownFileLocation,\n\tLoadToFileOnly,\n\tLoadFromCloudOrLocal,\n\tfalse,\n\t0)\n, _url(url)\n, _requestType(type) {\n}\n\nwebFileLoader::~webFileLoader() {\n\tif (!_finished) {\n\t\tcancel();\n\t}\n}\n\nQString webFileLoader::url() const {\n\treturn _url;\n}\n\nWebRequestType webFileLoader::requestType() const {\n\treturn _requestType;\n}\n\nbool webFileLoader::streamLoading() const {\n\treturn (_toCache == LoadToFileOnly);\n}\n\nvoid webFileLoader::startLoading() {\n\tif (_finished) {\n\t\treturn;\n\t} else if (!_manager) {\n\t\t_manager = GetManager();\n\t\t_manager->updates(\n\t\t\tthis\n\t\t) | rpl::on_next([=](const Update &data) {\n\t\t\tif (const auto progress = std::get_if<Progress>(&data)) {\n\t\t\t\tloadProgress(\n\t\t\t\t\tprogress->ready,\n\t\t\t\t\tprogress->total,\n\t\t\t\t\tprogress->streamed);\n\t\t\t} else if (const auto bytes = std::get_if<QByteArray>(&data)) {\n\t\t\t\tloadFinished(*bytes);\n\t\t\t} else {\n\t\t\t\tloadFailed();\n\t\t\t}\n\t\t}, _managerLifetime);\n\t}\n\t_manager->enqueue(this);\n}\n\nint64 webFileLoader::currentOffset() const {\n\treturn _ready;\n}\n\nvoid webFileLoader::loadProgress(\n\t\tqint64 ready,\n\t\tqint64 total,\n\t\tconst QByteArray &streamed) {\n\t_fullSize = _loadSize = total;\n\t_ready = ready;\n\tif (!streamed.isEmpty()\n\t\t&& !writeResultPart(_streamedOffset, bytes::make_span(streamed))) {\n\t\tloadFailed();\n\t} else {\n\t\t_streamedOffset += streamed.size();\n\t\tnotifyAboutProgress();\n\t}\n}\n\nvoid webFileLoader::loadFinished(const QByteArray &data) {\n\tcancelRequest();\n\tif (writeResultPart(0, bytes::make_span(data))) {\n\t\tfinalizeResult();\n\t}\n}\n\nvoid webFileLoader::loadFailed() {\n\tcancel(FailureReason::OtherFailure);\n}\n\nStorage::Cache::Key webFileLoader::cacheKey() const {\n\treturn Data::UrlCacheKey(_url);\n}\n\nstd::optional<MediaKey> webFileLoader::fileLocationKey() const {\n\treturn std::nullopt;\n}\n\nvoid webFileLoader::cancelHook() {\n\tcancelRequest();\n}\n\nvoid webFileLoader::cancelRequest() {\n\tif (!_manager) {\n\t\treturn;\n\t}\n\t_managerLifetime.destroy();\n\t_manager->remove(this);\n\t_manager = nullptr;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/file_download_web.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"storage/file_download.h\"\n\nclass WebLoadManager;\n\nenum class WebRequestType {\n\tFullLoad,\n\tOnlySize,\n};\n\nclass webFileLoader final : public FileLoader {\npublic:\n\twebFileLoader(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &url,\n\t\tconst QString &to,\n\t\tLoadFromCloudSetting fromCloud,\n\t\tbool autoLoading,\n\t\tuint8 cacheTag);\n\twebFileLoader(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QString &url,\n\t\tconst QString &path,\n\t\tWebRequestType type);\n\t~webFileLoader();\n\n\t[[nodiscard]] QString url() const;\n\t[[nodiscard]] WebRequestType requestType() const;\n\t[[nodiscard]] bool streamLoading() const;\n\n\tint64 currentOffset() const override;\n\nprivate:\n\tvoid cancelRequest();\n\tvoid cancelHook() override;\n\tvoid startLoading() override;\n\tStorage::Cache::Key cacheKey() const override;\n\tstd::optional<MediaKey> fileLocationKey() const override;\n\n\tvoid loadProgress(\n\t\tqint64 ready,\n\t\tqint64 size,\n\t\tconst QByteArray &streamed);\n\tvoid loadFinished(const QByteArray &data);\n\tvoid loadFailed();\n\n\tconst QString _url;\n\tint64 _ready = 0;\n\tint64 _streamedOffset = 0;\n\tWebRequestType _requestType = {};\n\n\tstd::shared_ptr<WebLoadManager> _manager;\n\trpl::lifetime _managerLifetime;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/file_upload.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/file_upload.h\"\n\n#include \"api/api_editing.h\"\n#include \"api/api_send_progress.h\"\n#include \"storage/localimageloader.h\"\n#include \"storage/file_download.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"ui/image/image_location_factory.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"core/file_location.h\"\n#include \"core/application.h\"\n#include \"core/mime_type.h\"\n#include \"main/main_session.h\"\n#include \"storage/storage_account.h\"\n#include \"apiwrap.h\"\n\nnamespace Storage {\nnamespace {\n\n// max 1mb uploaded at the same time in each session\nconstexpr auto kMaxUploadPerSession = 1024 * 1024;\n\nconstexpr auto kDocumentMaxPartsCountDefault = 4000;\n\n// 32kb for tiny document ( < 1mb )\nconstexpr auto kDocumentUploadPartSize0 = 32 * 1024;\n\n// 64kb for little document ( <= 32mb )\nconstexpr auto kDocumentUploadPartSize1 = 64 * 1024;\n\n// 128kb for small document ( <= 375mb )\nconstexpr auto kDocumentUploadPartSize2 = 128 * 1024;\n\n// 256kb for medium document ( <= 750mb )\nconstexpr auto kDocumentUploadPartSize3 = 256 * 1024;\n\n// 512kb for large document ( <= 1500mb )\nconstexpr auto kDocumentUploadPartSize4 = 512 * 1024;\n\n// One part each half second, if not uploaded faster.\nconstexpr auto kUploadRequestInterval = crl::time(250);\n\n// How much time without upload causes additional session kill.\nconstexpr auto kKillSessionTimeout = 15 * crl::time(1000);\n\n// How much wait after session kill before killing another one.\nconstexpr auto kWaitForNormalizeTimeout = 8 * crl::time(1000);\n\nconstexpr auto kMaxSessionsCount = 8;\nconstexpr auto kFastRequestThreshold = 1 * crl::time(1000);\nconstexpr auto kSlowRequestThreshold = 8 * crl::time(1000);\n\n// Request is 'fast' if it was done in less than 1s and\n// (it-s size + queued before size) >= 512kb.\nconstexpr auto kAcceptAsFastIfTotalAtLeast = 512 * 1024;\n\n[[nodiscard]] const char *ThumbnailFormat(const QString &mime) {\n\treturn Core::IsMimeSticker(mime) ? \"WEBP\" : \"JPG\";\n}\n\n} // namespace\n\nstruct Uploader::Entry {\n\tEntry(FullMsgId itemId, const std::shared_ptr<FilePrepareResult> &file);\n\n\tvoid setDocSize(int64 size);\n\tbool setPartSize(int partSize);\n\n\t// const, but non-const for the move-assignment in the\n\tFullMsgId itemId;\n\tstd::shared_ptr<FilePrepareResult> file;\n\tnot_null<std::vector<QByteArray>*> parts;\n\tuint64 partsOfId = 0;\n\n\tint64 sentSize = 0;\n\tushort partsSent = 0;\n\tushort partsWaiting = 0;\n\n\tHashMd5 md5Hash;\n\n\tstd::unique_ptr<QFile> docFile;\n\tint64 docSize = 0;\n\tint64 docSentSize = 0;\n\tint docPartSize = 0;\n\tushort docPartsSent = 0;\n\tushort docPartsCount = 0;\n\tushort docPartsWaiting = 0;\n\n};\n\nstruct Uploader::Request {\n\tFullMsgId itemId;\n\tcrl::time sent = 0;\n\tQByteArray bytes;\n\tint queued = 0;\n\tushort part = 0;\n\tuchar dcIndex = 0;\n\tbool docPart = false;\n\tbool bigPart = false;\n\tbool nonPremiumDelayed = false;\n};\n\nUploader::Entry::Entry(\n\tFullMsgId itemId,\n\tconst std::shared_ptr<FilePrepareResult> &file)\n: itemId(itemId)\n, file(file)\n, parts((file->type == SendMediaType::Photo\n\t|| file->type == SendMediaType::Secure)\n\t\t? &file->fileparts\n\t\t: &file->thumbparts)\n, partsOfId((file->type == SendMediaType::Photo\n\t|| file->type == SendMediaType::Secure)\n\t\t? file->id\n\t\t: file->thumbId) {\n\tif (file->type == SendMediaType::File\n\t\t|| file->type == SendMediaType::ThemeFile\n\t\t|| file->type == SendMediaType::Audio\n\t\t|| file->type == SendMediaType::Round) {\n\t\tsetDocSize(file->filesize);\n\t}\n}\n\nvoid Uploader::Entry::setDocSize(int64 size) {\n\tdocSize = size;\n\tconstexpr auto limit0 = 1024 * 1024;\n\tconstexpr auto limit1 = 32 * limit0;\n\tif (docSize >= limit0 || !setPartSize(kDocumentUploadPartSize0)) {\n\t\tif (docSize > limit1 || !setPartSize(kDocumentUploadPartSize1)) {\n\t\t\tif (!setPartSize(kDocumentUploadPartSize2)) {\n\t\t\t\tif (!setPartSize(kDocumentUploadPartSize3)) {\n\t\t\t\t\tsetPartSize(kDocumentUploadPartSize4);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nbool Uploader::Entry::setPartSize(int partSize) {\n\tdocPartSize = partSize;\n\tdocPartsCount = (docSize + docPartSize - 1) / docPartSize;\n\treturn (docPartsCount <= kDocumentMaxPartsCountDefault);\n}\n\nUploader::Uploader(not_null<ApiWrap*> api)\n: _api(api)\n, _nextTimer([=] { maybeSend(); })\n, _stopSessionsTimer([=] { stopSessions(); }) {\n\tconst auto session = &_api->session();\n\tphotoReady(\n\t) | rpl::on_next([=](UploadedMedia &&data) {\n\t\tif (data.edit) {\n\t\t\tconst auto item = session->data().message(data.fullId);\n\t\t\tApi::EditMessageWithUploadedPhoto(\n\t\t\t\titem,\n\t\t\t\tstd::move(data.info),\n\t\t\t\tdata.options);\n\t\t} else {\n\t\t\t_api->sendUploadedPhoto(\n\t\t\t\tdata.fullId,\n\t\t\t\tstd::move(data.info),\n\t\t\t\tdata.options);\n\t\t}\n\t}, _lifetime);\n\n\tdocumentReady(\n\t) | rpl::on_next([=](UploadedMedia &&data) {\n\t\tif (data.edit) {\n\t\t\tconst auto item = session->data().message(data.fullId);\n\t\t\tApi::EditMessageWithUploadedDocument(\n\t\t\t\titem,\n\t\t\t\tstd::move(data.info),\n\t\t\t\tdata.options);\n\t\t} else {\n\t\t\t_api->sendUploadedDocument(\n\t\t\t\tdata.fullId,\n\t\t\t\tstd::move(data.info),\n\t\t\t\tdata.options);\n\t\t}\n\t}, _lifetime);\n\n\tphotoProgress(\n\t) | rpl::on_next([=](const FullMsgId &fullId) {\n\t\tprocessPhotoProgress(fullId);\n\t}, _lifetime);\n\n\tphotoFailed(\n\t) | rpl::on_next([=](const FullMsgId &fullId) {\n\t\tprocessPhotoFailed(fullId);\n\t}, _lifetime);\n\n\tdocumentProgress(\n\t) | rpl::on_next([=](const FullMsgId &fullId) {\n\t\tprocessDocumentProgress(fullId);\n\t}, _lifetime);\n\n\tdocumentFailed(\n\t) | rpl::on_next([=](const FullMsgId &fullId) {\n\t\tprocessDocumentFailed(fullId);\n\t}, _lifetime);\n\n\t_api->instance().nonPremiumDelayedRequests(\n\t) | rpl::on_next([=](mtpRequestId id) {\n\t\tconst auto i = _requests.find(id);\n\t\tif (i != end(_requests)) {\n\t\t\ti->second.nonPremiumDelayed = true;\n\t\t}\n\t}, _lifetime);\n}\n\nvoid Uploader::processPhotoProgress(FullMsgId itemId) {\n\tif (const auto item = session().data().message(itemId)) {\n\t\tsendProgressUpdate(item, Api::SendProgressType::UploadPhoto);\n\t}\n}\n\nvoid Uploader::processDocumentProgress(FullMsgId itemId) {\n\tif (const auto item = session().data().message(itemId)) {\n\t\tconst auto media = item->media();\n\t\tconst auto document = media ? media->document() : nullptr;\n\t\tconst auto sendAction = (document && document->isVoiceMessage())\n\t\t\t? Api::SendProgressType::UploadVoice\n\t\t\t: (document && document->isVideoMessage())\n\t\t\t? Api::SendProgressType::UploadRound\n\t\t\t: Api::SendProgressType::UploadFile;\n\t\tconst auto progress = (document && document->uploading())\n\t\t\t? ((document->uploadingData->offset * 100)\n\t\t\t\t/ document->uploadingData->size)\n\t\t\t: 0;\n\t\tsendProgressUpdate(item, sendAction, progress);\n\t}\n}\n\nvoid Uploader::processPhotoFailed(FullMsgId itemId) {\n\tif (const auto item = session().data().message(itemId)) {\n\t\tsendProgressUpdate(item, Api::SendProgressType::UploadPhoto, -1);\n\t}\n}\n\nvoid Uploader::processDocumentFailed(FullMsgId itemId) {\n\tif (const auto item = session().data().message(itemId)) {\n\t\tconst auto media = item->media();\n\t\tconst auto document = media ? media->document() : nullptr;\n\t\tconst auto sendAction = (document && document->isVoiceMessage())\n\t\t\t? Api::SendProgressType::UploadVoice\n\t\t\t: (document && document->isVideoMessage())\n\t\t\t? Api::SendProgressType::UploadRound\n\t\t\t: Api::SendProgressType::UploadFile;\n\t\tsendProgressUpdate(item, sendAction, -1);\n\t}\n}\n\nvoid Uploader::sendProgressUpdate(\n\t\tnot_null<HistoryItem*> item,\n\t\tApi::SendProgressType type,\n\t\tint progress) {\n\tconst auto history = item->history();\n\tauto &manager = _api->session().sendProgressManager();\n\tmanager.update(history, type, progress);\n\tif (const auto replyTo = item->replyToTop()) {\n\t\tif (history->peer->isMegagroup()) {\n\t\t\tmanager.update(history, replyTo, type, progress);\n\t\t}\n\t} else if (history->isForum()) {\n\t\tmanager.update(history, item->topicRootId(), type, progress);\n\t}\n\t_api->session().data().requestItemRepaint(item);\n}\n\nUploader::~Uploader() {\n\tclear();\n}\n\nMain::Session &Uploader::session() const {\n\treturn _api->session();\n}\n\nFullMsgId Uploader::currentUploadId() const {\n\treturn _queue.empty() ? FullMsgId() : _queue.front().itemId;\n}\n\nvoid Uploader::upload(\n\t\tFullMsgId itemId,\n\t\tconst std::shared_ptr<FilePrepareResult> &file) {\n\tif (file->type == SendMediaType::Photo) {\n\t\tconst auto photo = session().data().processPhoto(\n\t\t\tfile->photo,\n\t\t\tfile->photoThumbs);\n\t\tphoto->uploadingData = std::make_unique<Data::UploadState>(\n\t\t\tfile->partssize);\n\t} else if (file->type == SendMediaType::File\n\t\t|| file->type == SendMediaType::ThemeFile\n\t\t|| file->type == SendMediaType::Audio\n\t\t|| file->type == SendMediaType::Round) {\n\t\tconst auto document = file->thumb.isNull()\n\t\t\t? session().data().processDocument(file->document)\n\t\t\t: session().data().processDocument(\n\t\t\t\tfile->document,\n\t\t\t\tImages::FromImageInMemory(\n\t\t\t\t\tfile->thumb,\n\t\t\t\t\tThumbnailFormat(file->filemime),\n\t\t\t\t\tfile->thumbbytes));\n\t\tdocument->uploadingData = std::make_unique<Data::UploadState>(\n\t\t\tdocument->size);\n\t\tif (const auto active = document->activeMediaView()) {\n\t\t\tif (!file->goodThumbnail.isNull()) {\n\t\t\t\tactive->setGoodThumbnail(std::move(file->goodThumbnail));\n\t\t\t}\n\t\t\tif (!file->thumb.isNull()) {\n\t\t\t\tactive->setThumbnail(file->thumb);\n\t\t\t}\n\t\t}\n\t\tif (!file->goodThumbnailBytes.isEmpty()) {\n\t\t\tdocument->owner().cache().putIfEmpty(\n\t\t\t\tdocument->goodThumbnailCacheKey(),\n\t\t\t\tStorage::Cache::Database::TaggedValue(\n\t\t\t\t\tstd::move(file->goodThumbnailBytes),\n\t\t\t\t\tData::kImageCacheTag));\n\t\t}\n\t\tif (!file->content.isEmpty()) {\n\t\t\tdocument->setDataAndCache(file->content);\n\t\t}\n\t\tif (!file->filepath.isEmpty()) {\n\t\t\tdocument->setLocation(Core::FileLocation(file->filepath));\n\t\t} else if (!file->content.isEmpty()\n\t\t\t&& Core::App().canSaveFileWithoutAskingForPath()) {\n\t\t\tconst auto path = DocumentFileNameForSave(document);\n\t\t\tif (!path.isEmpty()) {\n\t\t\t\tauto f = QFile(path);\n\t\t\t\tif (f.open(QIODevice::WriteOnly)\n\t\t\t\t\t&& f.write(file->content) == file->content.size()) {\n\t\t\t\t\tf.close();\n\t\t\t\t\tdocument->setLocation(Core::FileLocation(path));\n\t\t\t\t\tsession().local().writeFileLocation(\n\t\t\t\t\t\tdocument->mediaKey(),\n\t\t\t\t\t\tCore::FileLocation(path));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (file->type == SendMediaType::ThemeFile) {\n\t\t\tdocument->checkWallPaperProperties();\n\t\t}\n\t\tif (file->videoCover) {\n\t\t\tsession().data().processPhoto(\n\t\t\t\tfile->videoCover->photo,\n\t\t\t\tfile->videoCover->photoThumbs);\n\t\t}\n\t}\n\t_queue.push_back({ itemId, file });\n\tif (!_nextTimer.isActive()) {\n\t\tmaybeSend();\n\t}\n}\n\nvoid Uploader::failed(FullMsgId itemId) {\n\tconst auto i = ranges::find(_queue, itemId, &Entry::itemId);\n\tif (i != end(_queue)) {\n\t\tconst auto entry = std::move(*i);\n\t\t_queue.erase(i);\n\t\tnotifyFailed(entry);\n\t} else if (const auto coverId = _videoIdToCoverId.take(itemId)) {\n\t\tif (const auto video = _videoWaitingCover.take(*coverId)) {\n\t\t\tconst auto document = session().data().document(video->id);\n\t\t\tif (document->uploading()) {\n\t\t\t\tdocument->status = FileUploadFailed;\n\t\t\t}\n\t\t\t_documentFailed.fire_copy(video->fullId);\n\t\t}\n\t\tfailed(*coverId);\n\t} else if (const auto video = _videoWaitingCover.take(itemId)) {\n\t\t_videoIdToCoverId.remove(video->fullId);\n\t\tconst auto document = session().data().document(video->id);\n\t\tif (document->uploading()) {\n\t\t\tdocument->status = FileUploadFailed;\n\t\t}\n\t\t_documentFailed.fire_copy(video->fullId);\n\t}\n\tcancelRequests(itemId);\n\tmaybeFinishFront();\n\n\tcrl::on_main(this, [=] {\n\t\tmaybeSend();\n\t});\n}\n\nvoid Uploader::notifyFailed(const Entry &entry) {\n\tconst auto type = entry.file->type;\n\tif (type == SendMediaType::Photo) {\n\t\t_photoFailed.fire_copy(entry.itemId);\n\t} else if (type == SendMediaType::File\n\t\t|| type == SendMediaType::ThemeFile\n\t\t|| type == SendMediaType::Audio\n\t\t|| type == SendMediaType::Round) {\n\t\tconst auto document = session().data().document(entry.file->id);\n\t\tif (document->uploading()) {\n\t\t\tdocument->status = FileUploadFailed;\n\t\t}\n\t\t_documentFailed.fire_copy(entry.itemId);\n\t} else if (type == SendMediaType::Secure) {\n\t\t_secureFailed.fire_copy(entry.itemId);\n\t} else {\n\t\tUnexpected(\"Type in Uploader::failed.\");\n\t}\n}\n\nvoid Uploader::stopSessions() {\n\tif (ranges::any_of(_sentPerDcIndex, rpl::mappers::_1 != 0)) {\n\t\t_stopSessionsTimer.callOnce(kKillSessionTimeout);\n\t} else {\n\t\tfor (auto i = 0; i != int(_sentPerDcIndex.size()); ++i) {\n\t\t\t_api->instance().stopSession(MTP::uploadDcId(i));\n\t\t}\n\t\t_sentPerDcIndex.clear();\n\t\t_dcIndicesWithFastRequests.clear();\n\t}\n}\n\nQByteArray Uploader::readDocPart(not_null<Entry*> entry) {\n\tconst auto checked = [&](QByteArray result) {\n\t\tif ((entry->file->type == SendMediaType::File\n\t\t\t|| entry->file->type == SendMediaType::ThemeFile\n\t\t\t|| entry->file->type == SendMediaType::Audio\n\t\t\t|| entry->file->type == SendMediaType::Round)\n\t\t\t&& entry->docSize <= kUseBigFilesFrom) {\n\t\t\tentry->md5Hash.feed(result.data(), result.size());\n\t\t}\n\t\tif (result.isEmpty()\n\t\t\t|| (result.size() > entry->docPartSize)\n\t\t\t|| ((result.size() < entry->docPartSize\n\t\t\t\t&& entry->docPartsSent + 1 != entry->docPartsCount))) {\n\t\t\treturn QByteArray();\n\t\t}\n\t\treturn result;\n\t};\n\tauto &content = entry->file->content;\n\tif (!content.isEmpty()) {\n\t\tconst auto offset = entry->docPartsSent * entry->docPartSize;\n\t\treturn checked(content.mid(offset, entry->docPartSize));\n\t} else if (!entry->docFile) {\n\t\tconst auto filepath = entry->file->filepath;\n\t\tentry->docFile = std::make_unique<QFile>(filepath);\n\t\tif (!entry->docFile->open(QIODevice::ReadOnly)) {\n\t\t\treturn QByteArray();\n\t\t}\n\t}\n\treturn checked(entry->docFile->read(entry->docPartSize));\n}\n\nbool Uploader::canAddDcIndex() const {\n\tconst auto count = int(_sentPerDcIndex.size());\n\treturn (count < kMaxSessionsCount)\n\t\t&& (count == int(_dcIndicesWithFastRequests.size()));\n}\n\nstd::optional<uchar> Uploader::chooseDcIndexForNextRequest(\n\t\tconst base::flat_set<uchar> &used) {\n\tfor (auto i = 0, count = int(_sentPerDcIndex.size()); i != count; ++i) {\n\t\tif (!_sentPerDcIndex[i] && !used.contains(i)) {\n\t\t\treturn i;\n\t\t}\n\t}\n\tif (canAddDcIndex()) {\n\t\tconst auto result = int(_sentPerDcIndex.size());\n\t\t_sentPerDcIndex.push_back(0);\n\t\t_dcIndicesWithFastRequests.clear();\n\t\t_latestDcIndexAdded = crl::now();\n\n\t\tDEBUG_LOG((\"Uploader: Added dc index %1.\").arg(result));\n\t\treturn result;\n\t}\n\tauto result = std::optional<int>();\n\tfor (auto i = 0, count = int(_sentPerDcIndex.size()); i != count; ++i) {\n\t\tif (!used.contains(i)\n\t\t\t&& (!result.has_value()\n\t\t\t\t|| _sentPerDcIndex[i] < _sentPerDcIndex[*result])) {\n\t\t\tresult = i;\n\t\t}\n\t}\n\treturn result;\n}\n\nUploader::Entry *Uploader::chooseEntryForNextRequest() {\n\tif (!_pendingFromRemovedDcIndices.empty()) {\n\t\tconst auto itemId = _pendingFromRemovedDcIndices.front().itemId;\n\t\tconst auto i = ranges::find(_queue, itemId, &Entry::itemId);\n\t\tAssert(i != end(_queue));\n\t\treturn &*i;\n\t}\n\n\tfor (auto i = begin(_queue); i != end(_queue); ++i) {\n\t\tif (i->partsSent < i->parts->size()\n\t\t\t|| i->docPartsSent < i->docPartsCount) {\n\t\t\treturn &*i;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nauto Uploader::sendPart(not_null<Entry*> entry, uchar dcIndex)\n-> SendResult {\n\treturn !_pendingFromRemovedDcIndices.empty()\n\t\t? sendPendingPart(entry, dcIndex)\n\t\t: (entry->partsSent < entry->parts->size())\n\t\t? sendSlicedPart(entry, dcIndex)\n\t\t: sendDocPart(entry, dcIndex);\n}\n\ntemplate <typename Prepared>\nvoid Uploader::sendPreparedRequest(Prepared &&prepared, Request &&request) {\n\tauto &sentInSession = _sentPerDcIndex[request.dcIndex];\n\tconst auto queued = sentInSession;\n\tsentInSession += int(request.bytes.size());\n\n\tconst auto requestId = _api->request(\n\t\tstd::move(prepared)\n\t).done([=](const MTPBool &result, mtpRequestId requestId) {\n\t\tpartLoaded(result, requestId);\n\t}).fail([=](const MTP::Error &error, mtpRequestId requestId) {\n\t\tpartFailed(error, requestId);\n\t}).toDC(MTP::uploadDcId(request.dcIndex)).send();\n\n\trequest.sent = crl::now();\n\trequest.queued = queued;\n\t_requests.emplace(requestId, std::move(request));\n}\n\nauto Uploader::sendPendingPart(not_null<Entry*> entry, uchar dcIndex)\n-> SendResult {\n\tExpects(!_pendingFromRemovedDcIndices.empty());\n\tExpects(_pendingFromRemovedDcIndices.front().itemId == entry->itemId);\n\n\tauto request = std::move(_pendingFromRemovedDcIndices.front());\n\t_pendingFromRemovedDcIndices.erase(begin(_pendingFromRemovedDcIndices));\n\n\tconst auto part = request.part;\n\tconst auto bytes = request.bytes;\n\trequest.dcIndex = dcIndex;\n\tif (request.bigPart) {\n\t\tsendPreparedRequest(MTPupload_SaveBigFilePart(\n\t\t\tMTP_long(entry->file->id),\n\t\t\tMTP_int(part),\n\t\t\tMTP_int(entry->docPartsCount),\n\t\t\tMTP_bytes(bytes)\n\t\t), std::move(request));\n\t} else {\n\t\tconst auto id = request.docPart ? entry->file->id : entry->partsOfId;\n\t\tsendPreparedRequest(MTPupload_SaveFilePart(\n\t\t\tMTP_long(id),\n\t\t\tMTP_int(part),\n\t\t\tMTP_bytes(bytes)\n\t\t), std::move(request));\n\t}\n\treturn SendResult::Success;\n}\n\nauto Uploader::sendDocPart(not_null<Entry*> entry, uchar dcIndex)\n-> SendResult {\n\tconst auto itemId = entry->itemId;\n\tconst auto alreadySent = _sentPerDcIndex[dcIndex];\n\tconst auto willProbablyBeSent = entry->docPartSize;\n\tif (alreadySent + willProbablyBeSent > kMaxUploadPerSession) {\n\t\treturn SendResult::DcIndexFull;\n\t}\n\n\tAssert(entry->docPartsSent < entry->docPartsCount);\n\n\tconst auto partBytes = readDocPart(entry);\n\tif (partBytes.isEmpty()) {\n\t\tfailed(itemId);\n\t\treturn SendResult::Failed;\n\t}\n\tconst auto part = entry->docPartsSent++;\n\t++entry->docPartsWaiting;\n\n\tconst auto send = [&](auto &&request, bool big) {\n\t\tsendPreparedRequest(std::move(request), {\n\t\t\t.itemId = itemId,\n\t\t\t.bytes = partBytes,\n\t\t\t.part = part,\n\t\t\t.dcIndex = dcIndex,\n\t\t\t.docPart = true,\n\t\t\t.bigPart = big,\n\t\t});\n\t};\n\tif (entry->docSize > kUseBigFilesFrom) {\n\t\tsend(MTPupload_SaveBigFilePart(\n\t\t\tMTP_long(entry->file->id),\n\t\t\tMTP_int(part),\n\t\t\tMTP_int(entry->docPartsCount),\n\t\t\tMTP_bytes(partBytes)\n\t\t), true);\n\t} else {\n\t\tsend(MTPupload_SaveFilePart(\n\t\t\tMTP_long(entry->file->id),\n\t\t\tMTP_int(part),\n\t\t\tMTP_bytes(partBytes)\n\t\t), false);\n\t}\n\treturn SendResult::Success;\n}\n\nauto Uploader::sendSlicedPart(not_null<Entry*> entry, uchar dcIndex)\n-> SendResult {\n\tconst auto itemId = entry->itemId;\n\tconst auto alreadySent = _sentPerDcIndex[dcIndex];\n\tconst auto willBeSent = entry->parts->at(entry->partsSent).size();\n\tif (alreadySent + willBeSent >= kMaxUploadPerSession) {\n\t\treturn SendResult::DcIndexFull;\n\t}\n\n\t++entry->partsWaiting;\n\tconst auto index = entry->partsSent++;\n\tconst auto partBytes = entry->parts->at(index);\n\tsendPreparedRequest(MTPupload_SaveFilePart(\n\t\tMTP_long(entry->partsOfId),\n\t\tMTP_int(index),\n\t\tMTP_bytes(partBytes)\n\t), {\n\t\t.itemId = itemId,\n\t\t.bytes = partBytes,\n\t\t.dcIndex = dcIndex,\n\t});\n\treturn SendResult::Success;\n}\n\nvoid Uploader::maybeSend() {\n\tconst auto stopping = _stopSessionsTimer.isActive();\n\tif (_queue.empty()) {\n\t\tif (!stopping) {\n\t\t\t_stopSessionsTimer.callOnce(kKillSessionTimeout);\n\t\t}\n\t\t_pausedId = FullMsgId();\n\t\treturn;\n\t} else if (_pausedId) {\n\t\treturn;\n\t} else if (stopping) {\n\t\t_stopSessionsTimer.cancel();\n\t}\n\n\tauto usedDcIndices = base::flat_set<uchar>();\n\twhile (true) {\n\t\tconst auto maybeDcIndex = chooseDcIndexForNextRequest(usedDcIndices);\n\t\tif (!maybeDcIndex.has_value()) {\n\t\t\tbreak;\n\t\t}\n\t\tconst auto dcIndex = *maybeDcIndex;\n\t\twhile (true) {\n\t\t\tconst auto entry = chooseEntryForNextRequest();\n\t\t\tif (!entry) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto result = sendPart(entry, dcIndex);\n\t\t\tif (result == SendResult::DcIndexFull) {\n\t\t\t\treturn;\n\t\t\t} else if (result == SendResult::Success) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t// If this entry failed, we try the next one.\n\t\t}\n\t\tif (_sentPerDcIndex[dcIndex] >= kAcceptAsFastIfTotalAtLeast) {\n\t\t\tusedDcIndices.emplace(dcIndex);\n\t\t}\n\t}\n\tif (usedDcIndices.empty()) {\n\t\t_nextTimer.cancel();\n\t} else {\n\t\t_nextTimer.callOnce(kUploadRequestInterval);\n\t}\n}\n\nvoid Uploader::cancel(FullMsgId itemId) {\n\tfailed(itemId);\n}\n\nvoid Uploader::cancelAll() {\n\twhile (!_queue.empty()) {\n\t\tfailed(_queue.front().itemId);\n\t}\n\tclear();\n\tunpause();\n}\n\nvoid Uploader::pause(FullMsgId itemId) {\n\t_pausedId = itemId;\n}\n\nvoid Uploader::unpause() {\n\t_pausedId = FullMsgId();\n\tmaybeSend();\n}\n\nvoid Uploader::cancelRequests(FullMsgId itemId) {\n\tfor (auto i = begin(_requests); i != end(_requests);) {\n\t\tif (i->second.itemId == itemId) {\n\t\t\tconst auto bytes = int(i->second.bytes.size());\n\t\t\t_sentPerDcIndex[i->second.dcIndex] -= bytes;\n\t\t\t_api->request(i->first).cancel();\n\t\t\ti = _requests.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\t_pendingFromRemovedDcIndices.erase(ranges::remove(\n\t\t_pendingFromRemovedDcIndices,\n\t\titemId,\n\t\t&Request::itemId\n\t), end(_pendingFromRemovedDcIndices));\n}\n\nvoid Uploader::cancelAllRequests() {\n\tfor (const auto &[requestId, request] : base::take(_requests)) {\n\t\t_api->request(requestId).cancel();\n\t}\n\tranges::fill(_sentPerDcIndex, 0);\n}\n\nvoid Uploader::clear() {\n\t_queue.clear();\n\tcancelAllRequests();\n\tstopSessions();\n\t_stopSessionsTimer.cancel();\n}\n\nUploader::Request Uploader::finishRequest(mtpRequestId requestId) {\n\tconst auto taken = _requests.take(requestId);\n\tAssert(taken.has_value());\n\n\t_sentPerDcIndex[taken->dcIndex] -= int(taken->bytes.size());\n\treturn *taken;\n}\n\nvoid Uploader::partLoaded(const MTPBool &result, mtpRequestId requestId) {\n\tconst auto request = finishRequest(requestId);\n\n\tconst auto bytes = int(request.bytes.size());\n\tconst auto itemId = request.itemId;\n\n\tif (mtpIsFalse(result)) { // failed to upload current file\n\t\tfailed(itemId);\n\t\treturn;\n\t}\n\n\tconst auto i = ranges::find(_queue, itemId, &Entry::itemId);\n\tAssert(i != end(_queue));\n\tauto &entry = *i;\n\n\tconst auto now = crl::now();\n\tconst auto duration = now - request.sent;\n\tconst auto fast = (duration < kFastRequestThreshold);\n\tconst auto slowish = !fast;\n\tconst auto slow = (duration >= kSlowRequestThreshold);\n\n\tif (slowish) {\n\t\t_dcIndicesWithFastRequests.clear();\n\t\tif (slow) {\n\t\t\tconst auto elapsed = (now - _latestDcIndexRemoved);\n\t\t\tconst auto remove = (elapsed >= kWaitForNormalizeTimeout);\n\t\t\tif (remove && _sentPerDcIndex.size() > 1) {\n\t\t\t\tDEBUG_LOG((\"Uploader: Slow request, removing dc index.\"));\n\t\t\t\tremoveDcIndex();\n\t\t\t\t_latestDcIndexRemoved = now;\n\t\t\t} else {\n\t\t\t\tDEBUG_LOG((\"Uploader: Slow request, clear fast records.\"));\n\t\t\t}\n\t\t} else {\n\t\t\tDEBUG_LOG((\"Uploader: Slow-ish request, clear fast records.\"));\n\t\t}\n\t} else if (request.sent > _latestDcIndexAdded\n\t\t&& (request.queued + bytes >= kAcceptAsFastIfTotalAtLeast)) {\n\t\tif (_dcIndicesWithFastRequests.emplace(request.dcIndex).second) {\n\t\t\tDEBUG_LOG((\"Uploader: Mark %1 of %2 as fast.\"\n\t\t\t\t).arg(request.dcIndex\n\t\t\t\t).arg(_sentPerDcIndex.size()));\n\t\t}\n\t}\n\n\tif (request.docPart) {\n\t\t--entry.docPartsWaiting;\n\t\tentry.docSentSize += bytes;\n\t} else {\n\t\t--entry.partsWaiting;\n\t\tentry.sentSize += bytes;\n\t}\n\n\tif (entry.file->type == SendMediaType::Photo) {\n\t\tconst auto photo = session().data().photo(entry.file->id);\n\t\tif (photo->uploading()) {\n\t\t\tphoto->uploadingData->size = entry.file->partssize;\n\t\t\tphoto->uploadingData->offset = entry.sentSize;\n\t\t}\n\t\t_photoProgress.fire_copy(itemId);\n\t} else if (entry.file->type == SendMediaType::File\n\t\t|| entry.file->type == SendMediaType::ThemeFile\n\t\t|| entry.file->type == SendMediaType::Audio\n\t\t|| entry.file->type == SendMediaType::Round) {\n\t\tconst auto document = session().data().document(entry.file->id);\n\t\tif (document->uploading()) {\n\t\t\tdocument->uploadingData->offset = std::min(\n\t\t\t\tdocument->uploadingData->size,\n\t\t\t\tentry.docSentSize);\n\t\t}\n\t\t_documentProgress.fire_copy(itemId);\n\t} else if (entry.file->type == SendMediaType::Secure) {\n\t\t_secureProgress.fire_copy({\n\t\t\t.fullId = itemId,\n\t\t\t.offset = entry.sentSize,\n\t\t\t.size = entry.file->partssize,\n\t\t});\n\t}\n\tif (request.nonPremiumDelayed) {\n\t\t_nonPremiumDelays.fire_copy(itemId);\n\t}\n\n\tif (!_queue.empty() && itemId == _queue.front().itemId) {\n\t\tmaybeFinishFront();\n\t}\n\tmaybeSend();\n}\n\nvoid Uploader::removeDcIndex() {\n\tExpects(_sentPerDcIndex.size() > 1);\n\n\tconst auto dcIndex = int(_sentPerDcIndex.size()) - 1;\n\tfor (auto i = begin(_requests); i != end(_requests);) {\n\t\tif (i->second.dcIndex == dcIndex) {\n\t\t\tconst auto bytes = int(i->second.bytes.size());\n\t\t\t_sentPerDcIndex[dcIndex] -= bytes;\n\t\t\t_api->request(i->first).cancel();\n\t\t\t_pendingFromRemovedDcIndices.push_back(std::move(i->second));\n\t\t\ti = _requests.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tAssert(_sentPerDcIndex.back() == 0);\n\t_sentPerDcIndex.pop_back();\n\t_dcIndicesWithFastRequests.remove(dcIndex);\n\t_api->instance().stopSession(MTP::uploadDcId(dcIndex));\n\tDEBUG_LOG((\"Uploader: Removed dc index %1.\").arg(dcIndex));\n}\n\nvoid Uploader::maybeFinishFront() {\n\twhile (!_queue.empty()) {\n\t\tconst auto &entry = _queue.front();\n\t\tif (entry.partsSent >= entry.parts->size()\n\t\t\t&& entry.docPartsSent >= entry.docPartsCount\n\t\t\t&& !entry.partsWaiting\n\t\t\t&& !entry.docPartsWaiting) {\n\t\t\tfinishFront();\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nvoid Uploader::finishFront() {\n\tExpects(!_queue.empty());\n\n\tauto entry = std::move(_queue.front());\n\t_queue.erase(_queue.begin());\n\n\tconst auto options = entry.file\n\t\t? entry.file->to.options\n\t\t: Api::SendOptions();\n\tconst auto edit = entry.file &&\n\t\tentry.file->to.replaceMediaOf;\n\tconst auto attachedStickers = entry.file\n\t\t? entry.file->attachedStickers\n\t\t: std::vector<MTPInputDocument>();\n\tif (entry.file->type == SendMediaType::Photo) {\n\t\tauto photoFilename = entry.file->filename;\n\t\tif (!photoFilename.endsWith(u\".jpg\"_q, Qt::CaseInsensitive)) {\n\t\t\t// Server has some extensions checking for inputMediaUploadedPhoto,\n\t\t\t// so force the extension to be .jpg anyway. It doesn't matter,\n\t\t\t// because the filename from inputFile is not used anywhere.\n\t\t\tphotoFilename += u\".jpg\"_q;\n\t\t}\n\t\tconst auto md5 = entry.file->filemd5;\n\t\tconst auto file = MTP_inputFile(\n\t\t\tMTP_long(entry.file->id),\n\t\t\tMTP_int(entry.parts->size()),\n\t\t\tMTP_string(photoFilename),\n\t\t\tMTP_bytes(md5));\n\t\tauto ready = UploadedMedia{\n\t\t\t.id = entry.file->id,\n\t\t\t.fullId = entry.itemId,\n\t\t\t.info = {\n\t\t\t\t.file = file,\n\t\t\t\t.attachedStickers = attachedStickers,\n\t\t\t},\n\t\t\t.options = options,\n\t\t\t.edit = edit,\n\t\t};\n\t\tconst auto i = _videoWaitingCover.find(entry.itemId);\n\t\tif (i != end(_videoWaitingCover)) {\n\t\t\tuploadCoverAsPhoto(i->second.fullId, std::move(ready));\n\t\t} else {\n\t\t\t_photoReady.fire(std::move(ready));\n\t\t}\n\t} else if (entry.file->type == SendMediaType::File\n\t\t|| entry.file->type == SendMediaType::ThemeFile\n\t\t|| entry.file->type == SendMediaType::Audio\n\t\t|| entry.file->type == SendMediaType::Round) {\n\t\tQByteArray docMd5(32, Qt::Uninitialized);\n\t\thashMd5Hex(entry.md5Hash.result(), docMd5.data());\n\n\t\tconst auto file = (entry.docSize > kUseBigFilesFrom)\n\t\t\t? MTP_inputFileBig(\n\t\t\t\tMTP_long(entry.file->id),\n\t\t\t\tMTP_int(entry.docPartsCount),\n\t\t\t\tMTP_string(entry.file->filename))\n\t\t\t: MTP_inputFile(\n\t\t\t\tMTP_long(entry.file->id),\n\t\t\t\tMTP_int(entry.docPartsCount),\n\t\t\t\tMTP_string(entry.file->filename),\n\t\t\t\tMTP_bytes(docMd5));\n\t\tconst auto thumb = [&]() -> std::optional<MTPInputFile> {\n\t\t\tif (entry.parts->empty()) {\n\t\t\t\treturn std::nullopt;\n\t\t\t}\n\t\t\tconst auto thumbFilename = entry.file->thumbname;\n\t\t\tconst auto thumbMd5 = entry.file->thumbmd5;\n\t\t\treturn MTP_inputFile(\n\t\t\t\tMTP_long(entry.file->thumbId),\n\t\t\t\tMTP_int(entry.parts->size()),\n\t\t\t\tMTP_string(thumbFilename),\n\t\t\t\tMTP_bytes(thumbMd5));\n\t\t}();\n\t\tauto ready = UploadedMedia{\n\t\t\t.id = entry.file->id,\n\t\t\t.fullId = entry.itemId,\n\t\t\t.info = {\n\t\t\t\t.file = file,\n\t\t\t\t.thumb = thumb,\n\t\t\t\t.attachedStickers = attachedStickers,\n\t\t\t\t.forceFile = entry.file->forceFile,\n\t\t\t},\n\t\t\t.options = options,\n\t\t\t.edit = edit,\n\t\t};\n\t\tif (entry.file->videoCover) {\n\t\t\tuploadVideoCover(std::move(ready), entry.file->videoCover);\n\t\t} else {\n\t\t\t_documentReady.fire(std::move(ready));\n\t\t}\n\t} else if (entry.file->type == SendMediaType::Secure) {\n\t\t_secureReady.fire({\n\t\t\tentry.itemId,\n\t\t\tentry.file->id,\n\t\t\tint(entry.parts->size()),\n\t\t});\n\t}\n}\n\nvoid Uploader::partFailed(const MTP::Error &error, mtpRequestId requestId) {\n\tconst auto request = finishRequest(requestId);\n\tfailed(request.itemId);\n}\n\nvoid Uploader::uploadVideoCover(\n\t\tUploadedMedia &&video,\n\t\tstd::shared_ptr<FilePrepareResult> videoCover) {\n\tconst auto coverId = FullMsgId(\n\t\tvideoCover->to.peer,\n\t\tsession().data().nextLocalMessageId());\n\t_videoIdToCoverId.emplace(video.fullId, coverId);\n\t_videoWaitingCover.emplace(coverId, std::move(video));\n\n\tupload(coverId, videoCover);\n}\n\nvoid Uploader::uploadCoverAsPhoto(\n\t\tFullMsgId videoId,\n\t\tUploadedMedia &&cover) {\n\tconst auto coverId = cover.fullId;\n\t_api->request(MTPmessages_UploadMedia(\n\t\tMTP_flags(0),\n\t\tMTPstring(), // business_connection_id\n\t\tsession().data().peer(videoId.peer)->input(),\n\t\tMTP_inputMediaUploadedPhoto(\n\t\t\tMTP_flags(0),\n\t\t\tcover.info.file,\n\t\t\tMTP_vector<MTPInputDocument>(0),\n\t\t\tMTP_int(0),\n\t\t\tMTPInputDocument()) // video\n\t)).done([=](const MTPMessageMedia &result) {\n\t\tresult.match([&](const MTPDmessageMediaPhoto &data) {\n\t\t\tconst auto photo = data.vphoto();\n\t\t\tif (!photo || photo->type() != mtpc_photo) {\n\t\t\t\tfailed(coverId);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto &fields = photo->c_photo();\n\t\t\tif (const auto coverId = _videoIdToCoverId.take(videoId)) {\n\t\t\t\tif (auto video = _videoWaitingCover.take(*coverId)) {\n\t\t\t\t\tvideo->info.videoCover = MTP_inputPhoto(\n\t\t\t\t\t\tfields.vid(),\n\t\t\t\t\t\tfields.vaccess_hash(),\n\t\t\t\t\t\tfields.vfile_reference());\n\t\t\t\t\t_documentReady.fire(std::move(*video));\n\t\t\t\t}\n\t\t\t}\n\t\t}, [&](const auto &) {\n\t\t\tfailed(coverId);\n\t\t});\n\t}).fail([=] {\n\t\tfailed(coverId);\n\t}).send();\n}\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/file_upload.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_common.h\"\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n#include \"mtproto/facade.h\"\n\nclass ApiWrap;\nstruct FilePrepareResult;\n\nnamespace Api {\nenum class SendProgressType;\n} // namespace Api\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Storage {\n\n// MTP big files methods used for files greater than 30mb.\nconstexpr auto kUseBigFilesFrom = 30 * 1024 * 1024;\n\nstruct UploadedMedia {\n\tuint64 id = 0;\n\tFullMsgId fullId;\n\tApi::RemoteFileInfo info;\n\tApi::SendOptions options;\n\tbool edit = false;\n};\n\nstruct UploadSecureProgress {\n\tFullMsgId fullId;\n\tint64 offset = 0;\n\tint64 size = 0;\n};\n\nstruct UploadSecureDone {\n\tFullMsgId fullId;\n\tuint64 fileId = 0;\n\tint partsCount = 0;\n};\n\nclass Uploader final : public base::has_weak_ptr {\npublic:\n\texplicit Uploader(not_null<ApiWrap*> api);\n\t~Uploader();\n\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] FullMsgId currentUploadId() const;\n\n\tvoid upload(\n\t\tFullMsgId itemId,\n\t\tconst std::shared_ptr<FilePrepareResult> &file);\n\n\tvoid pause(FullMsgId itemId);\n\tvoid cancel(FullMsgId itemId);\n\tvoid cancelAll();\n\n\t[[nodiscard]] rpl::producer<UploadedMedia> photoReady() const {\n\t\treturn _photoReady.events();\n\t}\n\t[[nodiscard]] rpl::producer<UploadedMedia> documentReady() const {\n\t\treturn _documentReady.events();\n\t}\n\t[[nodiscard]] rpl::producer<UploadSecureDone> secureReady() const {\n\t\treturn _secureReady.events();\n\t}\n\t[[nodiscard]] rpl::producer<FullMsgId> photoProgress() const {\n\t\treturn _photoProgress.events();\n\t}\n\t[[nodiscard]] rpl::producer<FullMsgId> documentProgress() const {\n\t\treturn _documentProgress.events();\n\t}\n\t[[nodiscard]] auto secureProgress() const\n\t-> rpl::producer<UploadSecureProgress> {\n\t\treturn _secureProgress.events();\n\t}\n\t[[nodiscard]] rpl::producer<FullMsgId> photoFailed() const {\n\t\treturn _photoFailed.events();\n\t}\n\t[[nodiscard]] rpl::producer<FullMsgId> documentFailed() const {\n\t\treturn _documentFailed.events();\n\t}\n\t[[nodiscard]] rpl::producer<FullMsgId> secureFailed() const {\n\t\treturn _secureFailed.events();\n\t}\n\n\t[[nodiscard]] rpl::producer<FullMsgId> nonPremiumDelays() const {\n\t\treturn _nonPremiumDelays.events();\n\t}\n\n\tvoid unpause();\n\tvoid stopSessions();\n\nprivate:\n\tstruct Entry;\n\tstruct Request;\n\n\tenum class SendResult : uchar {\n\t\tSuccess,\n\t\tFailed,\n\t\tDcIndexFull,\n\t};\n\n\tvoid maybeSend();\n\t[[nodiscard]] bool canAddDcIndex() const;\n\t[[nodiscard]] std::optional<uchar> chooseDcIndexForNextRequest(\n\t\tconst base::flat_set<uchar> &used);\n\t[[nodiscard]] Entry *chooseEntryForNextRequest();\n\t[[nodiscard]] SendResult sendPart(not_null<Entry*> entry, uchar dcIndex);\n\t[[nodiscard]] auto sendPendingPart(not_null<Entry*> entry, uchar dcIndex)\n\t\t-> SendResult;\n\t[[nodiscard]] auto sendDocPart(not_null<Entry*> entry, uchar dcIndex)\n\t\t-> SendResult;\n\t[[nodiscard]] auto sendSlicedPart(not_null<Entry*> entry, uchar dcIndex)\n\t\t-> SendResult;\n\t[[nodiscard]] QByteArray readDocPart(not_null<Entry*> entry);\n\tvoid removeDcIndex();\n\n\ttemplate <typename Prepared>\n\tvoid sendPreparedRequest(Prepared &&prepared, Request &&request);\n\n\tvoid maybeFinishFront();\n\tvoid finishFront();\n\n\tvoid partLoaded(const MTPBool &result, mtpRequestId requestId);\n\tvoid partFailed(const MTP::Error &error, mtpRequestId requestId);\n\tRequest finishRequest(mtpRequestId requestId);\n\n\tvoid uploadVideoCover(\n\t\tUploadedMedia &&video,\n\t\tstd::shared_ptr<FilePrepareResult> videoCover);\n\tvoid uploadCoverAsPhoto(FullMsgId videoId, UploadedMedia &&cover);\n\n\tvoid processPhotoProgress(FullMsgId itemId);\n\tvoid processPhotoFailed(FullMsgId itemId);\n\tvoid processDocumentProgress(FullMsgId itemId);\n\tvoid processDocumentFailed(FullMsgId itemId);\n\n\tvoid notifyFailed(const Entry &entry);\n\tvoid failed(FullMsgId itemId);\n\tvoid cancelRequests(FullMsgId itemId);\n\tvoid cancelAllRequests();\n\tvoid clear();\n\n\tvoid sendProgressUpdate(\n\t\tnot_null<HistoryItem*> item,\n\t\tApi::SendProgressType type,\n\t\tint progress = 0);\n\n\tconst not_null<ApiWrap*> _api;\n\n\tstd::vector<Entry> _queue;\n\n\tbase::flat_map<mtpRequestId, Request> _requests;\n\tstd::vector<int> _sentPerDcIndex;\n\n\t// Fast requests since the latest dc index addition.\n\tbase::flat_set<uchar> _dcIndicesWithFastRequests;\n\tcrl::time _latestDcIndexAdded = 0;\n\tcrl::time _latestDcIndexRemoved = 0;\n\tstd::vector<Request> _pendingFromRemovedDcIndices;\n\n\tbase::flat_map<FullMsgId, FullMsgId> _videoIdToCoverId;\n\tbase::flat_map<FullMsgId, UploadedMedia> _videoWaitingCover;\n\n\tFullMsgId _pausedId;\n\tbase::Timer _nextTimer, _stopSessionsTimer;\n\n\trpl::event_stream<UploadedMedia> _photoReady;\n\trpl::event_stream<UploadedMedia> _documentReady;\n\trpl::event_stream<UploadSecureDone> _secureReady;\n\trpl::event_stream<FullMsgId> _photoProgress;\n\trpl::event_stream<FullMsgId> _documentProgress;\n\trpl::event_stream<UploadSecureProgress> _secureProgress;\n\trpl::event_stream<FullMsgId> _photoFailed;\n\trpl::event_stream<FullMsgId> _documentFailed;\n\trpl::event_stream<FullMsgId> _secureFailed;\n\trpl::event_stream<FullMsgId> _nonPremiumDelays;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/localimageloader.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/localimageloader.h\"\n\n#include \"api/api_text_entities.h\"\n#include \"api/api_sending.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/file_utilities.h\"\n#include \"core/mime_type.h\"\n#include \"base/unixtime.h\"\n#include \"base/random.h\"\n#include \"editor/scene/scene_item_sticker.h\"\n#include \"editor/scene/scene.h\"\n#include \"media/audio/media_audio.h\"\n#include \"media/clip/media_clip_reader.h\"\n#include \"mtproto/facade.h\"\n#include \"lottie/lottie_animation.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"boxes/abstract_box.h\"\n#include \"boxes/send_files_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"lang/lang_keys.h\"\n#include \"storage/file_download.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"window/themes/window_theme_preview.h\"\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"main/main_session.h\"\n\n#include <QtCore/QBuffer>\n#include <QtGui/QImageWriter>\n\nnamespace {\n\nconstexpr auto kThumbnailQuality = 95;\nconstexpr auto kThumbnailSize = 320;\nconstexpr auto kPhotoUploadPartSize = 32 * 1024;\nconstexpr auto kRecompressAfterBpp = 4;\n\nusing Ui::ValidateThumbDimensions;\n\nstruct PreparedFileThumbnail {\n\tuint64 id = 0;\n\tQString name;\n\tQImage image;\n\tQByteArray bytes;\n\tMTPPhotoSize mtpSize = MTP_photoSizeEmpty(MTP_string());\n};\n\n[[nodiscard]] PreparedFileThumbnail PrepareFileThumbnail(QImage &&original) {\n\tconst auto width = original.width();\n\tconst auto height = original.height();\n\tif (!ValidateThumbDimensions(width, height)) {\n\t\treturn {};\n\t}\n\tauto result = PreparedFileThumbnail();\n\tresult.id = base::RandomValue<uint64>();\n\tconst auto scaled = (width > kThumbnailSize || height > kThumbnailSize);\n\tconst auto scaledWidth = [&] {\n\t\treturn (width > height)\n\t\t\t? kThumbnailSize\n\t\t\t: int(base::SafeRound(kThumbnailSize * width / float64(height)));\n\t};\n\tconst auto scaledHeight = [&] {\n\t\treturn (width > height)\n\t\t\t? int(base::SafeRound(kThumbnailSize * height / float64(width)))\n\t\t\t: kThumbnailSize;\n\t};\n\tresult.image = scaled\n\t\t? original.scaled(\n\t\t\tscaledWidth(),\n\t\t\tscaledHeight(),\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation)\n\t\t: std::move(original);\n\tresult.mtpSize = MTP_photoSize(\n\t\tMTP_string(),\n\t\tMTP_int(result.image.width()),\n\t\tMTP_int(result.image.height()),\n\t\tMTP_int(0));\n\treturn result;\n}\n\n[[nodiscard]] bool FileThumbnailUploadRequired(\n\t\tconst QString &filemime,\n\t\tint64 filesize) {\n\tconstexpr auto kThumbnailUploadBySize = 5 * int64(1024 * 1024);\n\tconst auto kThumbnailKnownMimes = {\n\t\t\"image/jpeg\",\n\t\t\"image/gif\",\n\t\t\"image/png\",\n\t\t\"image/webp\",\n\t\t\"video/mp4\",\n\t};\n\treturn (filesize > kThumbnailUploadBySize)\n\t\t|| (ranges::find(kThumbnailKnownMimes, filemime.toLower())\n\t\t\t== end(kThumbnailKnownMimes));\n}\n\n[[nodiscard]] PreparedFileThumbnail FinalizeFileThumbnail(\n\t\tPreparedFileThumbnail &&prepared,\n\t\tconst QString &filemime,\n\t\tint64 filesize,\n\t\tbool isSticker) {\n\tprepared.name = isSticker ? u\"thumb.webp\"_q : u\"thumb.jpg\"_q;\n\tif (FileThumbnailUploadRequired(filemime, filesize)) {\n\t\tconst auto format = isSticker ? \"WEBP\" : \"JPG\";\n\t\tauto buffer = QBuffer(&prepared.bytes);\n\t\tprepared.image.save(&buffer, format, kThumbnailQuality);\n\t}\n\treturn std::move(prepared);\n}\n\n[[nodiscard]] auto FindAlbumItem(\n\t\tstd::vector<SendingAlbum::Item> &items,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto result = ranges::find(\n\t\titems,\n\t\titem->fullId(),\n\t\t&SendingAlbum::Item::msgId);\n\n\tEnsures(result != end(items));\n\treturn result;\n}\n\n[[nodiscard]] MTPInputSingleMedia PrepareAlbumItemMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPInputMedia &media,\n\t\tuint64 randomId) {\n\tauto caption = item->originalText();\n\tTextUtilities::Trim(caption);\n\tauto sentEntities = Api::EntitiesToMTP(\n\t\t&item->history()->session(),\n\t\tcaption.entities,\n\t\tApi::ConvertOption::SkipLocal);\n\tconst auto flags = !sentEntities.v.isEmpty()\n\t\t? MTPDinputSingleMedia::Flag::f_entities\n\t\t: MTPDinputSingleMedia::Flag(0);\n\n\treturn MTP_inputSingleMedia(\n\t\tMTP_flags(flags),\n\t\tmedia,\n\t\tMTP_long(randomId),\n\t\tMTP_string(caption.text),\n\t\tsentEntities);\n}\n\n[[nodiscard]] std::vector<not_null<DocumentData*>> ExtractStickersFromScene(\n\t\tnot_null<const Ui::PreparedFileInformation::Image*> info) {\n\tconst auto allItems = info->modifications.paint->items();\n\n\treturn ranges::views::all(\n\t\tallItems\n\t) | ranges::views::filter([](const Editor::Scene::ItemPtr &i) {\n\t\treturn i->isVisible() && (i->type() == Editor::ItemSticker::Type);\n\t}) | ranges::views::transform([](const Editor::Scene::ItemPtr &i) {\n\t\treturn static_cast<Editor::ItemSticker*>(i.get())->sticker();\n\t}) | ranges::to_vector;\n}\n\n[[nodiscard]] QByteArray ComputePhotoJpegBytes(\n\t\tQImage &full,\n\t\tconst QByteArray &bytes,\n\t\tconst QByteArray &format) {\n\tif (!bytes.isEmpty()\n\t\t&& (bytes.size()\n\t\t\t<= full.width() * full.height() * kRecompressAfterBpp / 8)\n\t\t&& (format == u\"jpeg\"_q)) {\n\t\tif (!Images::IsProgressiveJpeg(bytes)) {\n\t\t\tif (const auto result = Images::MakeProgressiveJpeg(bytes)\n\t\t\t\t; !result.isEmpty()) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t} else {\n\t\t\treturn bytes;\n\t\t}\n\t}\n\n\tauto result = QByteArray();\n\tQBuffer buffer(&result);\n\tQImageWriter writer(&buffer, \"JPEG\");\n\twriter.setQuality(87);\n\twriter.setProgressiveScanWrite(true);\n\twriter.write(full);\n\tbuffer.close();\n\n\treturn result;\n}\n\n} // namespace\n\nint PhotoSideLimit(bool large) {\n\treturn large ? 2560 : 1280;\n}\n\nint PhotoSideLimit() {\n\treturn PhotoSideLimit(\n\t\tCore::App().settings().sendFilesWay().sendLargePhotos());\n}\n\nTaskQueue::TaskQueue(crl::time stopTimeoutMs) {\n\tif (stopTimeoutMs > 0) {\n\t\t_stopTimer = new QTimer(this);\n\t\tconnect(_stopTimer, SIGNAL(timeout()), this, SLOT(stop()));\n\t\t_stopTimer->setSingleShot(true);\n\t\t_stopTimer->setInterval(int(stopTimeoutMs));\n\t}\n}\n\nTaskId TaskQueue::addTask(std::unique_ptr<Task> &&task) {\n\tconst auto result = task->id();\n\t{\n\t\tQMutexLocker lock(&_tasksToProcessMutex);\n\t\t_tasksToProcess.push_back(std::move(task));\n\t}\n\n\twakeThread();\n\n\treturn result;\n}\n\nvoid TaskQueue::addTasks(std::vector<std::unique_ptr<Task>> &&tasks) {\n\t{\n\t\tQMutexLocker lock(&_tasksToProcessMutex);\n\t\tfor (auto &task : tasks) {\n\t\t\t_tasksToProcess.push_back(std::move(task));\n\t\t}\n\t}\n\n\twakeThread();\n}\n\nvoid TaskQueue::wakeThread() {\n\tif (!_thread) {\n\t\t_thread = new QThread();\n\n\t\t_worker = new TaskQueueWorker(this);\n\t\t_worker->moveToThread(_thread);\n\n\t\tconnect(this, SIGNAL(taskAdded()), _worker, SLOT(onTaskAdded()));\n\t\tconnect(_worker, SIGNAL(taskProcessed()), this, SLOT(onTaskProcessed()));\n\n\t\t_thread->start();\n\t}\n\tif (_stopTimer) _stopTimer->stop();\n\ttaskAdded();\n}\n\nvoid TaskQueue::cancelTask(TaskId id) {\n\tconst auto removeFrom = [&](std::deque<std::unique_ptr<Task>> &queue) {\n\t\tconst auto proj = [](const std::unique_ptr<Task> &task) {\n\t\t\treturn task->id();\n\t\t};\n\t\tauto i = ranges::find(queue, id, proj);\n\t\tif (i != queue.end()) {\n\t\t\tqueue.erase(i);\n\t\t}\n\t};\n\t{\n\t\tQMutexLocker lock(&_tasksToProcessMutex);\n\t\tremoveFrom(_tasksToProcess);\n\t\tif (_taskInProcessId == id) {\n\t\t\t_taskInProcessId = TaskId();\n\t\t}\n\t}\n\tQMutexLocker lock(&_tasksToFinishMutex);\n\tremoveFrom(_tasksToFinish);\n}\n\nvoid TaskQueue::onTaskProcessed() {\n\tdo {\n\t\tauto task = std::unique_ptr<Task>();\n\t\t{\n\t\t\tQMutexLocker lock(&_tasksToFinishMutex);\n\t\t\tif (_tasksToFinish.empty()) break;\n\t\t\ttask = std::move(_tasksToFinish.front());\n\t\t\t_tasksToFinish.pop_front();\n\t\t}\n\t\ttask->finish();\n\t} while (true);\n\n\tif (_stopTimer) {\n\t\tQMutexLocker lock(&_tasksToProcessMutex);\n\t\tif (_tasksToProcess.empty() && !_taskInProcessId) {\n\t\t\t_stopTimer->start();\n\t\t}\n\t}\n}\n\nvoid TaskQueue::stop() {\n\tif (_thread) {\n\t\t_thread->requestInterruption();\n\t\t_thread->quit();\n\t\tDEBUG_LOG((\"Waiting for taskThread to finish\"));\n\t\t_thread->wait();\n\t\tdelete base::take(_worker);\n\t\tdelete base::take(_thread);\n\t}\n\t_tasksToProcess.clear();\n\t_tasksToFinish.clear();\n\t_taskInProcessId = TaskId();\n}\n\nTaskQueue::~TaskQueue() {\n\tstop();\n\tdelete _stopTimer;\n}\n\nvoid TaskQueueWorker::onTaskAdded() {\n\tif (_inTaskAdded) return;\n\t_inTaskAdded = true;\n\n\tbool someTasksLeft = false;\n\tdo {\n\t\tauto task = std::unique_ptr<Task>();\n\t\t{\n\t\t\tQMutexLocker lock(&_queue->_tasksToProcessMutex);\n\t\t\tif (!_queue->_tasksToProcess.empty()) {\n\t\t\t\ttask = std::move(_queue->_tasksToProcess.front());\n\t\t\t\t_queue->_tasksToProcess.pop_front();\n\t\t\t\t_queue->_taskInProcessId = task->id();\n\t\t\t}\n\t\t}\n\n\t\tif (task) {\n\t\t\ttask->process();\n\t\t\tbool emitTaskProcessed = false;\n\t\t\t{\n\t\t\t\tQMutexLocker lockToProcess(&_queue->_tasksToProcessMutex);\n\t\t\t\tif (_queue->_taskInProcessId == task->id()) {\n\t\t\t\t\t_queue->_taskInProcessId = TaskId();\n\t\t\t\t\tsomeTasksLeft = !_queue->_tasksToProcess.empty();\n\n\t\t\t\t\tQMutexLocker lockToFinish(&_queue->_tasksToFinishMutex);\n\t\t\t\t\temitTaskProcessed = _queue->_tasksToFinish.empty();\n\t\t\t\t\t_queue->_tasksToFinish.push_back(std::move(task));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (emitTaskProcessed) {\n\t\t\t\ttaskProcessed();\n\t\t\t}\n\t\t}\n\t\tQCoreApplication::processEvents();\n\t} while (someTasksLeft && !thread()->isInterruptionRequested());\n\n\t_inTaskAdded = false;\n}\n\nSendingAlbum::SendingAlbum() : groupId(base::RandomValue<uint64>()) {\n}\n\nvoid SendingAlbum::fillMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPInputMedia &media,\n\t\tuint64 randomId) {\n\tconst auto i = FindAlbumItem(items, item);\n\tAssert(!i->media);\n\n\ti->randomId = randomId;\n\ti->media = PrepareAlbumItemMedia(item, media, randomId);\n}\n\nvoid SendingAlbum::refreshMediaCaption(not_null<HistoryItem*> item) {\n\tconst auto i = FindAlbumItem(items, item);\n\tif (!i->media) {\n\t\treturn;\n\t}\n\ti->media = i->media->match([&](const MTPDinputSingleMedia &data) {\n\t\treturn PrepareAlbumItemMedia(\n\t\t\titem,\n\t\t\tdata.vmedia(),\n\t\t\tdata.vrandom_id().v);\n\t});\n}\n\nvoid SendingAlbum::removeItem(not_null<HistoryItem*> item) {\n\tconst auto localId = item->fullId();\n\tconst auto i = ranges::find(items, localId, &Item::msgId);\n\tconst auto moveCaption = (items.size() > 1) && (i == begin(items));\n\tAssert(i != end(items));\n\titems.erase(i);\n\tif (moveCaption) {\n\t\tauto caption = item->originalText();\n\t\tconst auto firstId = items.front().msgId;\n\t\tif (const auto first = item->history()->owner().message(firstId)) {\n\t\t\t// We don't need to finishEdition() here, because the whole\n\t\t\t// album will be rebuilt after one item was removed from it.\n\t\t\tauto firstCaption = first->originalText();\n\t\t\tfirst->setText(firstCaption.text.isEmpty()\n\t\t\t\t? std::move(caption)\n\t\t\t\t: firstCaption.append('\\n').append(std::move(caption)));\n\t\t\trefreshMediaCaption(first);\n\t\t}\n\t}\n}\n\nSendingAlbum::Item::Item(TaskId taskId)\n: taskId(taskId) {\n}\n\nFilePrepareResult::FilePrepareResult(FilePrepareDescriptor &&descriptor)\n: taskId(descriptor.taskId)\n, id(descriptor.id)\n, to(std::move(descriptor.to))\n, album(std::move(descriptor.album))\n, type(descriptor.type)\n, caption(std::move(descriptor.caption))\n, spoiler(descriptor.spoiler) {\n}\n\nvoid FilePrepareResult::setFileData(const QByteArray &filedata) {\n\tif (filedata.isEmpty()) {\n\t\tpartssize = 0;\n\t} else {\n\t\tpartssize = filedata.size();\n\t\tfileparts.reserve(\n\t\t\t(partssize + kPhotoUploadPartSize - 1) / kPhotoUploadPartSize);\n\t\tfor (int32 i = 0, part = 0; i < partssize; i += kPhotoUploadPartSize, ++part) {\n\t\t\tfileparts.push_back(filedata.mid(i, kPhotoUploadPartSize));\n\t\t}\n\t\tfilemd5.resize(32);\n\t\thashMd5Hex(filedata.constData(), filedata.size(), filemd5.data());\n\t}\n}\n\nvoid FilePrepareResult::setThumbData(const QByteArray &thumbdata) {\n\tif (!thumbdata.isEmpty()) {\n\t\tthumbbytes = thumbdata;\n\t\tint32 size = thumbdata.size();\n\t\tthumbparts.reserve(\n\t\t\t(size + kPhotoUploadPartSize - 1) / kPhotoUploadPartSize);\n\t\tfor (int32 i = 0, part = 0; i < size; i += kPhotoUploadPartSize, ++part) {\n\t\t\tthumbparts.push_back(thumbdata.mid(i, kPhotoUploadPartSize));\n\t\t}\n\t\tthumbmd5.resize(32);\n\t\thashMd5Hex(thumbdata.constData(), thumbdata.size(), thumbmd5.data());\n\t}\n}\n\nstd::shared_ptr<FilePrepareResult> MakePreparedFile(\n\t\tFilePrepareDescriptor &&descriptor) {\n\treturn std::make_shared<FilePrepareResult>(std::move(descriptor));\n}\n\nFileLoadTask::FileLoadTask(Args &&args)\n: _id(args.idOverride ? args.idOverride : base::RandomValue<uint64>())\n, _session(args.session)\n, _dcId(args.session->mainDcId())\n, _to(std::move(args.to))\n, _album(std::move(args.album))\n, _filepath(std::move(args.filepath))\n, _displayName(std::move(args.displayName))\n, _content(std::move(args.content))\n, _videoCover(std::move(args.videoCover))\n, _information(std::move(args.information))\n, _type(args.type)\n, _caption(std::move(args.caption))\n, _spoiler(args.spoiler)\n, _forceFile(args.forceFile)\n, _sendLargePhotos(args.sendLargePhotos) {\n\tExpects(_to.options.scheduled\n\t\t|| _to.options.shortcutId\n\t\t|| !_to.replaceMediaOf\n\t\t|| IsServerMsgId(_to.replaceMediaOf));\n}\n\nFileLoadTask::FileLoadTask(VoiceArgs &&args)\n: _id(base::RandomValue<uint64>())\n, _session(args.session)\n, _dcId(args.session->mainDcId())\n, _to(std::move(args.to))\n, _content(std::move(args.voice))\n, _duration(args.duration)\n, _waveform(std::move(args.waveform))\n, _type(args.video ? SendMediaType::Round : SendMediaType::Audio)\n, _caption(std::move(args.caption)) {\n}\n\nFileLoadTask::~FileLoadTask() = default;\n\nauto FileLoadTask::ReadMediaInformation(\n\tconst QString &filepath,\n\tconst QByteArray &content,\n\tconst QString &filemime)\n-> std::unique_ptr<Ui::PreparedFileInformation> {\n\tauto result = std::make_unique<Ui::PreparedFileInformation>();\n\tresult->filemime = filemime;\n\n\tif (CheckForSong(filepath, content, result)) {\n\t\treturn result;\n\t} else if (CheckForVideo(filepath, content, result)) {\n\t\treturn result;\n\t} else if (CheckForImage(filepath, content, result)) {\n\t\treturn result;\n\t}\n\treturn result;\n}\n\ntemplate <typename Mimes, typename Extensions>\nbool FileLoadTask::CheckMimeOrExtensions(\n\t\tconst QString &filepath,\n\t\tconst QString &filemime,\n\t\tMimes &mimes,\n\t\tExtensions &extensions) {\n\tif (std::find(std::begin(mimes), std::end(mimes), filemime) != std::end(mimes)) {\n\t\treturn true;\n\t}\n\tif (std::find_if(std::begin(extensions), std::end(extensions), [&filepath](auto &extension) {\n\t\treturn filepath.endsWith(extension, Qt::CaseInsensitive);\n\t}) != std::end(extensions)) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool FileLoadTask::CheckForSong(\n\t\tconst QString &filepath,\n\t\tconst QByteArray &content,\n\t\tstd::unique_ptr<Ui::PreparedFileInformation> &result) {\n\tstatic const auto mimes = {\n\t\tu\"audio/mp3\"_q,\n\t\tu\"audio/m4a\"_q,\n\t\tu\"audio/aac\"_q,\n\t\tu\"audio/ogg\"_q,\n\t\tu\"audio/flac\"_q,\n\t\tu\"audio/opus\"_q,\n\t};\n\tstatic const auto extensions = {\n\t\tu\".mp3\"_q,\n\t\tu\".m4a\"_q,\n\t\tu\".aac\"_q,\n\t\tu\".ogg\"_q,\n\t\tu\".flac\"_q,\n\t\tu\".opus\"_q,\n\t\tu\".oga\"_q,\n\t};\n\tif (!filepath.isEmpty()\n\t\t&& !CheckMimeOrExtensions(\n\t\t\tfilepath,\n\t\t\tresult->filemime,\n\t\t\tmimes,\n\t\t\textensions)) {\n\t\treturn false;\n\t}\n\n\tauto media = v::get<Ui::PreparedFileInformation::Song>(\n\t\tMedia::Player::PrepareForSending(filepath, content).media);\n\tif (media.duration < 0) {\n\t\treturn false;\n\t}\n\tif (!ValidateThumbDimensions(media.cover.width(), media.cover.height())) {\n\t\tmedia.cover = QImage();\n\t}\n\tresult->media = std::move(media);\n\treturn true;\n}\n\nbool FileLoadTask::CheckForVideo(\n\t\tconst QString &filepath,\n\t\tconst QByteArray &content,\n\t\tstd::unique_ptr<Ui::PreparedFileInformation> &result) {\n\tstatic const auto mimes = {\n\t\tu\"video/mp4\"_q,\n\t\tu\"video/quicktime\"_q,\n\t};\n\tstatic const auto extensions = {\n\t\tu\".mp4\"_q,\n\t\tu\".mov\"_q,\n\t\tu\".m4v\"_q,\n\t\tu\".webm\"_q,\n\t};\n\tif (!CheckMimeOrExtensions(filepath, result->filemime, mimes, extensions)) {\n\t\treturn false;\n\t}\n\n\tauto media = v::get<Ui::PreparedFileInformation::Video>(\n\t\tMedia::Clip::PrepareForSending(filepath, content).media);\n\tif (media.duration < 0) {\n\t\treturn false;\n\t}\n\n\tauto coverWidth = media.thumbnail.width();\n\tauto coverHeight = media.thumbnail.height();\n\tif (!ValidateThumbDimensions(coverWidth, coverHeight)) {\n\t\treturn false;\n\t}\n\n\tif (filepath.endsWith(u\".mp4\"_q, Qt::CaseInsensitive)) {\n\t\tresult->filemime = u\"video/mp4\"_q;\n\t}\n\tresult->media = std::move(media);\n\treturn true;\n}\n\nbool FileLoadTask::CheckForImage(\n\t\tconst QString &filepath,\n\t\tconst QByteArray &content,\n\t\tstd::unique_ptr<Ui::PreparedFileInformation> &result) {\n\tauto read = [&] {\n\t\tif (filepath.endsWith(u\".tgs\"_q, Qt::CaseInsensitive)) {\n\t\t\tauto image = Lottie::ReadThumbnail(\n\t\t\t\tLottie::ReadContent(content, filepath));\n\t\t\tconst auto success = !image.isNull();\n\t\t\tif (success) {\n\t\t\t\tresult->filemime = u\"application/x-tgsticker\"_q;\n\t\t\t}\n\t\t\treturn Images::ReadResult{\n\t\t\t\t.image = std::move(image),\n\t\t\t\t.animated = success,\n\t\t\t};\n\t\t}\n\t\treturn Images::Read({\n\t\t\t.path = filepath,\n\t\t\t.content = content,\n\t\t\t.returnContent = true,\n\t\t});\n\t}();\n\treturn FillImageInformation(\n\t\tstd::move(read.image),\n\t\tread.animated,\n\t\tresult,\n\t\tstd::move(read.content),\n\t\tstd::move(read.format));\n}\n\nbool FileLoadTask::FillImageInformation(\n\t\tQImage &&image,\n\t\tbool animated,\n\t\tstd::unique_ptr<Ui::PreparedFileInformation> &result,\n\t\tQByteArray content,\n\t\tQByteArray format) {\n\tExpects(result != nullptr);\n\n\tif (image.isNull()) {\n\t\treturn false;\n\t}\n\tauto media = Ui::PreparedFileInformation::Image();\n\tmedia.data = std::move(image);\n\tmedia.bytes = std::move(content);\n\tmedia.format = std::move(format);\n\tmedia.animated = animated;\n\tresult->media = media;\n\treturn true;\n}\n\nvoid FileLoadTask::process(ProcessArgs &&args) {\n\t_result = MakePreparedFile({\n\t\t.taskId = id(),\n\t\t.id = _id,\n\t\t.to = _to,\n\t\t.caption = _caption,\n\t\t.spoiler = _spoiler,\n\t\t.album = _album,\n\t});\n\tif (const auto cover = _videoCover.get()) {\n\t\tcover->process();\n\t\tif (const auto &result = cover->peekResult()) {\n\t\t\tif (result->type == SendMediaType::Photo\n\t\t\t\t&& !result->fileparts.empty()) {\n\t\t\t\t_result->videoCover = result;\n\t\t\t}\n\t\t}\n\t}\n\n\tQString filename, filemime;\n\tqint64 filesize = 0;\n\tQByteArray filedata;\n\n\tauto isAnimation = false;\n\tauto isSong = false;\n\tauto isVideo = false;\n\tauto isVoice = (_type == SendMediaType::Audio);\n\tauto isRound = (_type == SendMediaType::Round);\n\tauto isSticker = false;\n\n\tauto fullimage = QImage();\n\tauto fullimagebytes = QByteArray();\n\tauto fullimageformat = QByteArray();\n\tauto info = _filepath.isEmpty() ? QFileInfo() : QFileInfo(_filepath);\n\tif (info.exists()) {\n\t\tif (info.isDir()) {\n\t\t\t_result->filesize = -1;\n\t\t\treturn;\n\t\t}\n\n\t\t// Voice sending is supported only from memory for now.\n\t\t// Because for voice we force mime type and don't read MediaInformation.\n\t\t// For a real file we always read mime type and read MediaInformation.\n\t\tAssert(!isVoice && !isRound);\n\n\t\tfilesize = info.size();\n\t\tfilename = info.fileName();\n\t\tif (!_information) {\n\t\t\t_information = readMediaInformation(Core::MimeTypeForFile(info).name());\n\t\t}\n\t\tfilemime = _information->filemime;\n\t\tif (auto image = std::get_if<Ui::PreparedFileInformation::Image>(\n\t\t\t\t&_information->media)) {\n\t\t\tfullimage = base::take(image->data);\n\t\t\tfullimagebytes = base::take(image->bytes);\n\t\t\tfullimageformat = base::take(image->format);\n\t\t\tif (!Core::IsMimeSticker(filemime)\n\t\t\t\t&& fullimageformat != u\"jpeg\"_q) {\n\t\t\t\tfullimage = Images::Opaque(std::move(fullimage));\n\t\t\t\tfullimagebytes = fullimageformat = QByteArray();\n\t\t\t}\n\t\t\tisAnimation = image->animated;\n\t\t}\n\t} else if (!_content.isEmpty()) {\n\t\tfilesize = _content.size();\n\t\tif (isVoice) {\n\t\t\tfilename = filedialogDefaultName(u\"audio\"_q, u\".ogg\"_q, QString(), true);\n\t\t\tfilemime = \"audio/ogg\";\n\t\t} else if (isRound) {\n\t\t\tfilename = filedialogDefaultName(u\"round\"_q, u\".mp4\"_q, QString(), true);\n\t\t\tfilemime = \"video/mp4\";\n\t\t} else {\n\t\t\tif (_information) {\n\t\t\t\tif (auto image = std::get_if<Ui::PreparedFileInformation::Image>(\n\t\t\t\t\t\t&_information->media)) {\n\t\t\t\t\tfullimage = base::take(image->data);\n\t\t\t\t\tfullimagebytes = base::take(image->bytes);\n\t\t\t\t\tfullimageformat = base::take(image->format);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto mimeType = Core::MimeTypeForData(_content);\n\t\t\tfilemime = mimeType.name();\n\t\t\tif (!Core::IsMimeSticker(filemime)\n\t\t\t\t&& fullimageformat != u\"jpeg\"_q) {\n\t\t\t\tfullimage = Images::Opaque(std::move(fullimage));\n\t\t\t\tfullimagebytes = fullimageformat = QByteArray();\n\t\t\t}\n\t\t\tif (filemime == \"image/jpeg\") {\n\t\t\t\tfilename = filedialogDefaultName(u\"photo\"_q, u\".jpg\"_q, QString(), true);\n\t\t\t} else if (filemime == \"image/png\") {\n\t\t\t\tfilename = filedialogDefaultName(u\"image\"_q, u\".png\"_q, QString(), true);\n\t\t\t} else {\n\t\t\t\tQString ext;\n\t\t\t\tQStringList patterns = mimeType.globPatterns();\n\t\t\t\tif (!patterns.isEmpty()) {\n\t\t\t\t\text = patterns.front().replace('*', QString());\n\t\t\t\t}\n\t\t\t\tfilename = filedialogDefaultName(u\"file\"_q, ext, QString(), true);\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (_information) {\n\t\t\tif (auto image = std::get_if<Ui::PreparedFileInformation::Image>(\n\t\t\t\t\t&_information->media)) {\n\t\t\t\tfullimage = base::take(image->data);\n\t\t\t\tfullimagebytes = base::take(image->bytes);\n\t\t\t\tfullimageformat = base::take(image->format);\n\t\t\t}\n\t\t}\n\t\tif (!fullimage.isNull() && fullimage.width() > 0) {\n\t\t\tif (_type == SendMediaType::Photo) {\n\t\t\t\tif (ValidateThumbDimensions(fullimage.width(), fullimage.height())) {\n\t\t\t\t\tfilesize = -1; // Fill later.\n\t\t\t\t\tfilemime = Core::MimeTypeForName(\"image/jpeg\").name();\n\t\t\t\t\tfilename = filedialogDefaultName(u\"image\"_q, u\".jpg\"_q, QString(), true);\n\t\t\t\t} else {\n\t\t\t\t\t_type = SendMediaType::File;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (_type == SendMediaType::File) {\n\t\t\t\tfilemime = Core::MimeTypeForName(\"image/png\").name();\n\t\t\t\tfilename = filedialogDefaultName(u\"image\"_q, u\".png\"_q, QString(), true);\n\t\t\t\t{\n\t\t\t\t\tQBuffer buffer(&_content);\n\t\t\t\t\tfullimage.save(&buffer, \"PNG\");\n\t\t\t\t}\n\t\t\t\tfilesize = _content.size();\n\t\t\t}\n\t\t\tfullimage = Images::Opaque(std::move(fullimage));\n\t\t\tfullimagebytes = fullimageformat = QByteArray();\n\t\t}\n\t}\n\t_result->filesize = qMin(filesize, qint64(UINT_MAX));\n\n\tif (!filesize || filesize > kFileSizePremiumLimit) {\n\t\treturn;\n\t}\n\n\tPreparedPhotoThumbs photoThumbs;\n\tQVector<MTPPhotoSize> photoSizes;\n\tQImage goodThumbnail;\n\tQByteArray goodThumbnailBytes;\n\n\tauto attributes = QVector<MTPDocumentAttribute>(\n\t\t1,\n\t\tMTP_documentAttributeFilename(MTP_string(_displayName.isEmpty()\n\t\t\t? filename\n\t\t\t: _displayName)));\n\n\tauto thumbnail = PreparedFileThumbnail();\n\n\tauto photo = MTP_photoEmpty(MTP_long(0));\n\tauto document = MTP_documentEmpty(MTP_long(0));\n\n\tif (isRound) {\n\t\t_information = readMediaInformation(u\"video/mp4\"_q);\n\t\tif (auto video = std::get_if<Ui::PreparedFileInformation::Video>(\n\t\t\t&_information->media)) {\n\t\t\tisVideo = true;\n\t\t\tauto coverWidth = video->thumbnail.width();\n\t\t\tauto coverHeight = video->thumbnail.height();\n\t\t\tif (video->isGifv && !_album) {\n\t\t\t\tattributes.push_back(MTP_documentAttributeAnimated());\n\t\t\t}\n\t\t\tauto flags = MTPDdocumentAttributeVideo::Flags(\n\t\t\t\tMTPDdocumentAttributeVideo::Flag::f_round_message);\n\t\t\tif (video->supportsStreaming) {\n\t\t\t\tflags |= MTPDdocumentAttributeVideo::Flag::f_supports_streaming;\n\t\t\t}\n\t\t\tconst auto realSeconds = video->duration / 1000.;\n\t\t\tattributes.push_back(MTP_documentAttributeVideo(\n\t\t\t\tMTP_flags(flags),\n\t\t\t\tMTP_double(realSeconds),\n\t\t\t\tMTP_int(coverWidth),\n\t\t\t\tMTP_int(coverHeight),\n\t\t\t\tMTPint(), // preload_prefix_size\n\t\t\t\tMTPdouble(), // video_start_ts\n\t\t\t\tMTPstring())); // video_codec\n\n\t\t\tif (args.generateGoodThumbnail) {\n\t\t\t\tgoodThumbnail = video->thumbnail;\n\t\t\t\t{\n\t\t\t\t\tQBuffer buffer(&goodThumbnailBytes);\n\t\t\t\t\tgoodThumbnail.save(&buffer, \"JPG\", kThumbnailQuality);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthumbnail = PrepareFileThumbnail(std::move(video->thumbnail));\n\t\t}\n\t} else if (!isVoice) {\n\t\tif (!_information) {\n\t\t\t_information = readMediaInformation(filemime);\n\t\t\tfilemime = _information->filemime;\n\t\t}\n\t\tif (auto song = std::get_if<Ui::PreparedFileInformation::Song>(\n\t\t\t\t&_information->media)) {\n\t\t\tisSong = true;\n\t\t\tconst auto seconds = song->duration / 1000;\n\t\t\tauto flags = MTPDdocumentAttributeAudio::Flag::f_title | MTPDdocumentAttributeAudio::Flag::f_performer;\n\t\t\tattributes.push_back(MTP_documentAttributeAudio(MTP_flags(flags), MTP_int(seconds), MTP_string(song->title), MTP_string(song->performer), MTPstring()));\n\t\t\tthumbnail = PrepareFileThumbnail(std::move(song->cover));\n\t\t} else if (auto video = std::get_if<Ui::PreparedFileInformation::Video>(\n\t\t\t\t&_information->media)) {\n\t\t\tisVideo = true;\n\t\t\tauto coverWidth = video->thumbnail.width();\n\t\t\tauto coverHeight = video->thumbnail.height();\n\t\t\tif (!_forceFile) {\n\t\t\t\tif (video->isGifv && !_album) {\n\t\t\t\t\tattributes.push_back(MTP_documentAttributeAnimated());\n\t\t\t\t}\n\t\t\t\tauto flags = MTPDdocumentAttributeVideo::Flags(0);\n\t\t\t\tif (video->supportsStreaming) {\n\t\t\t\t\tflags |= MTPDdocumentAttributeVideo::Flag::f_supports_streaming;\n\t\t\t\t}\n\t\t\t\tconst auto realSeconds = video->duration / 1000.;\n\t\t\t\tattributes.push_back(MTP_documentAttributeVideo(\n\t\t\t\t\tMTP_flags(flags),\n\t\t\t\t\tMTP_double(realSeconds),\n\t\t\t\t\tMTP_int(coverWidth),\n\t\t\t\t\tMTP_int(coverHeight),\n\t\t\t\t\tMTPint(),\n\t\t\t\t\tMTPdouble(),\n\t\t\t\t\tMTPstring()));\n\t\t\t}\n\n\t\t\tif (args.generateGoodThumbnail) {\n\t\t\t\tgoodThumbnail = video->thumbnail;\n\t\t\t\t{\n\t\t\t\t\tQBuffer buffer(&goodThumbnailBytes);\n\t\t\t\t\tgoodThumbnail.save(&buffer, \"JPG\", kThumbnailQuality);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthumbnail = PrepareFileThumbnail(std::move(video->thumbnail));\n\t\t} else if (filemime == u\"application/x-tdesktop-theme\"_q\n\t\t\t|| filemime == u\"application/x-tgtheme-tdesktop\"_q) {\n\t\t\tgoodThumbnail = Window::Theme::GeneratePreview(_content, _filepath);\n\t\t\tif (!goodThumbnail.isNull()) {\n\t\t\t\tQBuffer buffer(&goodThumbnailBytes);\n\t\t\t\tgoodThumbnail.save(&buffer, \"JPG\", kThumbnailQuality);\n\n\t\t\t\tthumbnail = PrepareFileThumbnail(base::duplicate(goodThumbnail));\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!fullimage.isNull() && fullimage.width() > 0 && !isSong && !isVideo && !isVoice && !isRound) {\n\t\tauto w = fullimage.width(), h = fullimage.height();\n\t\tattributes.push_back(MTP_documentAttributeImageSize(MTP_int(w), MTP_int(h)));\n\n\t\tif (ValidateThumbDimensions(w, h)) {\n\t\t\tisSticker = Core::IsMimeSticker(filemime)\n\t\t\t\t&& (filesize < Storage::kMaxStickerBytesSize)\n\t\t\t\t&& (Core::IsMimeStickerAnimated(filemime)\n\t\t\t\t\t|| (_type == SendMediaType::File\n\t\t\t\t\t\t&& GoodStickerDimensions(w, h)));\n\t\t\tif (isSticker) {\n\t\t\t\tattributes.push_back(MTP_documentAttributeSticker(\n\t\t\t\t\tMTP_flags(0),\n\t\t\t\t\tMTP_string(),\n\t\t\t\t\tMTP_inputStickerSetEmpty(),\n\t\t\t\t\tMTPMaskCoords()));\n\t\t\t\tif (isAnimation && args.generateGoodThumbnail) {\n\t\t\t\t\tgoodThumbnail = fullimage;\n\t\t\t\t\t{\n\t\t\t\t\t\tQBuffer buffer(&goodThumbnailBytes);\n\t\t\t\t\t\tgoodThumbnail.save(&buffer, \"WEBP\", kThumbnailQuality);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (isAnimation) {\n\t\t\t\tattributes.push_back(MTP_documentAttributeAnimated());\n\t\t\t} else if (filemime.startsWith(u\"image/\"_q)\n\t\t\t\t&& _type != SendMediaType::File) {\n\t\t\t\tif (Core::IsMimeSticker(filemime)) {\n\t\t\t\t\tfullimage = Images::Opaque(std::move(fullimage));\n\t\t\t\t}\n\t\t\t\tauto medium = (w > 320 || h > 320) ? fullimage.scaled(320, 320, Qt::KeepAspectRatio, Qt::SmoothTransformation) : fullimage;\n\n\t\t\t\tconst auto limit = PhotoSideLimit(_sendLargePhotos);\n\t\t\t\tconst auto downscaled = (w > limit || h > limit);\n\t\t\t\tauto full = downscaled ? fullimage.scaled(limit, limit, Qt::KeepAspectRatio, Qt::SmoothTransformation) : fullimage;\n\t\t\t\tif (downscaled) {\n\t\t\t\t\tfullimagebytes = fullimageformat = QByteArray();\n\t\t\t\t}\n\t\t\t\tfiledata = ComputePhotoJpegBytes(full, fullimagebytes, fullimageformat);\n\n\t\t\t\tphotoThumbs.emplace('m', PreparedPhotoThumb{ .image = medium });\n\t\t\t\tphotoSizes.push_back(MTP_photoSize(MTP_string(\"m\"), MTP_int(medium.width()), MTP_int(medium.height()), MTP_int(0)));\n\n\t\t\t\tphotoThumbs.emplace('y', PreparedPhotoThumb{\n\t\t\t\t\t.image = full,\n\t\t\t\t\t.bytes = filedata\n\t\t\t\t});\n\t\t\t\tphotoSizes.push_back(MTP_photoSize(MTP_string(\"y\"), MTP_int(full.width()), MTP_int(full.height()), MTP_int(0)));\n\n\t\t\t\tphoto = MTP_photo(\n\t\t\t\t\tMTP_flags(0),\n\t\t\t\t\tMTP_long(_id),\n\t\t\t\t\tMTP_long(0),\n\t\t\t\t\tMTP_bytes(),\n\t\t\t\t\tMTP_int(base::unixtime::now()),\n\t\t\t\t\tMTP_vector<MTPPhotoSize>(photoSizes),\n\t\t\t\t\tMTPVector<MTPVideoSize>(),\n\t\t\t\t\tMTP_int(_dcId));\n\n\t\t\t\tif (filesize < 0) {\n\t\t\t\t\tfilesize = _result->filesize = filedata.size();\n\t\t\t\t}\n\t\t\t}\n\t\t\tthumbnail = PrepareFileThumbnail(std::move(fullimage));\n\t\t}\n\t}\n\tthumbnail = FinalizeFileThumbnail(\n\t\tstd::move(thumbnail),\n\t\tfilemime,\n\t\tfilesize,\n\t\tisSticker);\n\n\tif (_type == SendMediaType::Photo && photoThumbs.empty()) {\n\t\t_type = SendMediaType::File;\n\t}\n\n\tif (isVoice) {\n\t\tconst auto seconds = _duration / 1000;\n\t\tauto flags = MTPDdocumentAttributeAudio::Flag::f_voice | MTPDdocumentAttributeAudio::Flag::f_waveform;\n\t\tattributes[0] = MTP_documentAttributeAudio(MTP_flags(flags), MTP_int(seconds), MTPstring(), MTPstring(), MTP_bytes(documentWaveformEncode5bit(_waveform)));\n\t\tattributes.resize(1);\n\t\tdocument = MTP_document(\n\t\t\tMTP_flags(0),\n\t\t\tMTP_long(_id),\n\t\t\tMTP_long(0),\n\t\t\tMTP_bytes(),\n\t\t\tMTP_int(base::unixtime::now()),\n\t\t\tMTP_string(filemime),\n\t\t\tMTP_long(filesize),\n\t\t\tMTP_vector<MTPPhotoSize>(1, thumbnail.mtpSize),\n\t\t\tMTPVector<MTPVideoSize>(),\n\t\t\tMTP_int(_dcId),\n\t\t\tMTP_vector<MTPDocumentAttribute>(attributes));\n\t} else if (_type != SendMediaType::Photo) {\n\t\tdocument = MTP_document(\n\t\t\tMTP_flags(0),\n\t\t\tMTP_long(_id),\n\t\t\tMTP_long(0),\n\t\t\tMTP_bytes(),\n\t\t\tMTP_int(base::unixtime::now()),\n\t\t\tMTP_string(filemime),\n\t\t\tMTP_long(filesize),\n\t\t\tMTP_vector<MTPPhotoSize>(1, thumbnail.mtpSize),\n\t\t\tMTPVector<MTPVideoSize>(),\n\t\t\tMTP_int(_dcId),\n\t\t\tMTP_vector<MTPDocumentAttribute>(attributes));\n\t\t_type = isRound ? SendMediaType::Round : SendMediaType::File;\n\t}\n\n\tif (_information) {\n\t\tif (auto image = std::get_if<Ui::PreparedFileInformation::Image>(\n\t\t\t\t&_information->media)) {\n\t\t\tif (image->modifications.paint) {\n\t\t\t\tconst auto documents = ExtractStickersFromScene(image);\n\t\t\t\t_result->attachedStickers = documents\n\t\t\t\t\t| ranges::views::transform(&DocumentData::mtpInput)\n\t\t\t\t\t| ranges::to_vector;\n\t\t\t}\n\t\t}\n\t}\n\n\t_result->type = _type;\n\t_result->filepath = _filepath;\n\t_result->content = _content;\n\n\t_result->filename = filename;\n\t_result->filemime = filemime;\n\t_result->setFileData(filedata);\n\n\t_result->thumbId = thumbnail.id;\n\t_result->thumbname = thumbnail.name;\n\t_result->setThumbData(thumbnail.bytes);\n\t_result->thumb = std::move(thumbnail.image);\n\n\t_result->goodThumbnail = std::move(goodThumbnail);\n\t_result->goodThumbnailBytes = std::move(goodThumbnailBytes);\n\n\t_result->photo = photo;\n\t_result->document = document;\n\t_result->photoThumbs = photoThumbs;\n\t_result->forceFile = _forceFile;\n}\n\nvoid FileLoadTask::finish() {\n\tconst auto session = _session.get();\n\tif (!session) {\n\t\treturn;\n\t}\n\tconst auto premium = session->user()->isPremium();\n\tif (!_result || !_result->filesize || _result->filesize < 0) {\n\t\tUi::show(\n\t\t\tUi::MakeInformBox(\n\t\t\t\ttr::lng_send_image_empty(tr::now, lt_name, _filepath)),\n\t\t\tUi::LayerOption::KeepOther);\n\t\tremoveFromAlbum();\n\t} else if (_result->filesize > kFileSizePremiumLimit\n\t\t|| (_result->filesize > kFileSizeLimit && !premium)) {\n\t\tUi::show(\n\t\t\tBox(FileSizeLimitBox, session, _result->filesize, nullptr),\n\t\t\tUi::LayerOption::KeepOther);\n\t\tremoveFromAlbum();\n\t} else {\n\t\tApi::SendConfirmedFile(session, _result);\n\t}\n}\n\nconst std::shared_ptr<FilePrepareResult> &FileLoadTask::peekResult() const {\n\treturn _result;\n}\n\nstd::unique_ptr<Ui::PreparedFileInformation> FileLoadTask::readMediaInformation(\n\t\tconst QString &filemime) const {\n\treturn ReadMediaInformation(_filepath, _content, filemime);\n}\n\nvoid FileLoadTask::removeFromAlbum() {\n\tif (!_album) {\n\t\treturn;\n\t}\n\tconst auto proj = [](const SendingAlbum::Item &item) {\n\t\treturn item.taskId;\n\t};\n\tconst auto it = ranges::find(_album->items, id(), proj);\n\tAssert(it != _album->items.end());\n\n\t_album->items.erase(it);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/localimageloader.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/variant.h\"\n#include \"api/api_common.h\"\n\nnamespace Ui {\nstruct PreparedFileInformation;\n} // namespace Ui\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\n// Load files up to 2'000 MB.\nconstexpr auto kFileSizeLimit = 2'000 * int64(1024 * 1024);\n\n// Load files up to 4'000 MB.\nconstexpr auto kFileSizePremiumLimit = 4'000 * int64(1024 * 1024);\n\n[[nodiscard]] int PhotoSideLimit(bool large);\n[[nodiscard]] int PhotoSideLimit();\n\nenum class SendMediaType {\n\tPhoto,\n\tAudio,\n\tRound,\n\tFile,\n\tThemeFile,\n\tSecure,\n};\n\nusing TaskId = void*; // no interface, just id\ninline constexpr auto kEmptyTaskId = TaskId();\n\nclass Task {\npublic:\n\tvirtual void process() = 0; // is executed in a separate thread\n\tvirtual void finish() = 0; // is executed in the same as TaskQueue thread\n\tvirtual ~Task() = default;\n\n\tTaskId id() const {\n\t\treturn static_cast<TaskId>(const_cast<Task*>(this));\n\t}\n\n};\n\nclass TaskQueueWorker;\nclass TaskQueue : public QObject {\n\tQ_OBJECT\n\npublic:\n\texplicit TaskQueue(crl::time stopTimeoutMs = 0); // <= 0 - never stop worker\n\n\tTaskId addTask(std::unique_ptr<Task> &&task);\n\tvoid addTasks(std::vector<std::unique_ptr<Task>> &&tasks);\n\tvoid cancelTask(TaskId id); // this task finish() won't be called\n\n\t~TaskQueue();\n\nQ_SIGNALS:\n\tvoid taskAdded();\n\npublic Q_SLOTS:\n\tvoid onTaskProcessed();\n\tvoid stop();\n\nprivate:\n\tfriend class TaskQueueWorker;\n\n\tvoid wakeThread();\n\n\tstd::deque<std::unique_ptr<Task>> _tasksToProcess;\n\tstd::deque<std::unique_ptr<Task>> _tasksToFinish;\n\tTaskId _taskInProcessId = TaskId();\n\tQMutex _tasksToProcessMutex, _tasksToFinishMutex;\n\tQThread *_thread = nullptr;\n\tTaskQueueWorker *_worker = nullptr;\n\tQTimer *_stopTimer = nullptr;\n\n};\n\nclass TaskQueueWorker : public QObject {\n\tQ_OBJECT\n\npublic:\n\tTaskQueueWorker(TaskQueue *queue) : _queue(queue) {\n\t}\n\nQ_SIGNALS:\n\tvoid taskProcessed();\n\npublic Q_SLOTS:\n\tvoid onTaskAdded();\n\nprivate:\n\tTaskQueue *_queue;\n\tbool _inTaskAdded = false;\n\n};\n\nstruct SendingAlbum {\n\tstruct Item {\n\t\texplicit Item(TaskId taskId);\n\n\t\tTaskId taskId = kEmptyTaskId;\n\t\tuint64 randomId = 0;\n\t\tFullMsgId msgId;\n\t\tstd::optional<MTPInputSingleMedia> media;\n\t};\n\n\tSendingAlbum();\n\n\tvoid fillMedia(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst MTPInputMedia &media,\n\t\tuint64 randomId);\n\tvoid refreshMediaCaption(not_null<HistoryItem*> item);\n\tvoid removeItem(not_null<HistoryItem*> item);\n\n\tuint64 groupId = 0;\n\tstd::vector<Item> items;\n\tApi::SendOptions options;\n\tbool sent = false;\n\n};\n\nstruct FileLoadTo {\n\tFileLoadTo(\n\t\tPeerId peer,\n\t\tApi::SendOptions options,\n\t\tFullReplyTo replyTo,\n\t\tMsgId replaceMediaOf)\n\t: peer(peer)\n\t, options(options)\n\t, replyTo(replyTo)\n\t, replaceMediaOf(replaceMediaOf) {\n\t}\n\tPeerId peer;\n\tApi::SendOptions options;\n\tFullReplyTo replyTo;\n\tMsgId replaceMediaOf;\n};\n\nusing UploadFileParts = std::vector<QByteArray>;\nstruct FilePrepareDescriptor {\n\tTaskId taskId = kEmptyTaskId;\n\tbase::required<uint64> id;\n\tSendMediaType type = SendMediaType::File;\n\tFileLoadTo to = { PeerId(), Api::SendOptions(), FullReplyTo(), MsgId() };\n\tTextWithTags caption;\n\tbool spoiler = false;\n\tstd::shared_ptr<SendingAlbum> album;\n};\nstruct FilePrepareResult {\n\texplicit FilePrepareResult(FilePrepareDescriptor &&descriptor);\n\n\tTaskId taskId = kEmptyTaskId;\n\tuint64 id = 0;\n\tFileLoadTo to;\n\tstd::shared_ptr<SendingAlbum> album;\n\tSendMediaType type = SendMediaType::File;\n\tQString filepath;\n\tQByteArray content;\n\n\tQString filename;\n\tQString filemime;\n\tint64 filesize = 0;\n\tUploadFileParts fileparts;\n\tQByteArray filemd5;\n\tint64 partssize = 0;\n\n\tuint64 thumbId = 0; // id is always file-id of media, thumbId is file-id of thumb ( == id for photos)\n\tQString thumbname;\n\tUploadFileParts thumbparts;\n\tQByteArray thumbbytes;\n\tQByteArray thumbmd5;\n\tQImage thumb;\n\n\tQImage goodThumbnail;\n\tQByteArray goodThumbnailBytes;\n\n\tMTPPhoto photo = MTP_photoEmpty(MTP_long(0));\n\tMTPDocument document = MTP_documentEmpty(MTP_long(0));\n\n\tPreparedPhotoThumbs photoThumbs;\n\tTextWithTags caption;\n\tbool spoiler = false;\n\tbool forceFile = false;\n\n\tstd::vector<MTPInputDocument> attachedStickers;\n\n\tstd::shared_ptr<FilePrepareResult> videoCover;\n\n\tvoid setFileData(const QByteArray &filedata);\n\tvoid setThumbData(const QByteArray &thumbdata);\n\n};\n\n[[nodiscard]] std::shared_ptr<FilePrepareResult> MakePreparedFile(\n\tFilePrepareDescriptor &&descriptor);\n\nclass FileLoadTask final : public Task {\npublic:\n\tstatic std::unique_ptr<Ui::PreparedFileInformation> ReadMediaInformation(\n\t\tconst QString &filepath,\n\t\tconst QByteArray &content,\n\t\tconst QString &filemime);\n\tstatic bool FillImageInformation(\n\t\tQImage &&image,\n\t\tbool animated,\n\t\tstd::unique_ptr<Ui::PreparedFileInformation> &result,\n\t\tQByteArray content = {},\n\t\tQByteArray format = {});\n\n\tstruct Args {\n\t\tnot_null<Main::Session*> session;\n\t\tQString filepath;\n\t\tQByteArray content;\n\t\tstd::unique_ptr<Ui::PreparedFileInformation> information;\n\t\tstd::unique_ptr<FileLoadTask> videoCover;\n\t\tSendMediaType type;\n\t\tFileLoadTo to;\n\t\tTextWithTags caption;\n\t\tbool spoiler = false;\n\t\tstd::shared_ptr<SendingAlbum> album;\n\t\tbool forceFile = false;\n\t\tbool sendLargePhotos = false;\n\t\tuint64 idOverride = 0;\n\t\tQString displayName;\n\t};\n\n\tstruct VoiceArgs {\n\t\tnot_null<Main::Session*> session;\n\t\tQByteArray voice;\n\t\tcrl::time duration = 0;\n\t\tVoiceWaveform waveform;\n\t\tbool video = false;\n\t\tFileLoadTo to;\n\t\tTextWithTags caption;\n\t};\n\n\texplicit FileLoadTask(Args &&args);\n\texplicit FileLoadTask(VoiceArgs &&args);\n\t~FileLoadTask();\n\n\tuint64 fileid() const {\n\t\treturn _id;\n\t}\n\n\tstruct ProcessArgs {\n\t\tbool generateGoodThumbnail = true;\n\t};\n\tvoid process(ProcessArgs &&args);\n\n\tvoid process() override {\n\t\tprocess({});\n\t}\n\tvoid finish() override;\n\n\t[[nodiscard]] auto peekResult() const\n\t\t-> const std::shared_ptr<FilePrepareResult> &;\n\nprivate:\n\tstatic bool CheckForSong(\n\t\tconst QString &filepath,\n\t\tconst QByteArray &content,\n\t\tstd::unique_ptr<Ui::PreparedFileInformation> &result);\n\tstatic bool CheckForVideo(\n\t\tconst QString &filepath,\n\t\tconst QByteArray &content,\n\t\tstd::unique_ptr<Ui::PreparedFileInformation> &result);\n\tstatic bool CheckForImage(\n\t\tconst QString &filepath,\n\t\tconst QByteArray &content,\n\t\tstd::unique_ptr<Ui::PreparedFileInformation> &result);\n\n\ttemplate <typename Mimes, typename Extensions>\n\tstatic bool CheckMimeOrExtensions(const QString &filepath, const QString &filemime, Mimes &mimes, Extensions &extensions);\n\n\tstd::unique_ptr<Ui::PreparedFileInformation> readMediaInformation(const QString &filemime) const;\n\tvoid removeFromAlbum();\n\n\tuint64 _id = 0;\n\tbase::weak_ptr<Main::Session> _session;\n\tMTP::DcId _dcId = 0;\n\tFileLoadTo _to;\n\tconst std::shared_ptr<SendingAlbum> _album;\n\tQString _filepath;\n\tQString _displayName;\n\tQByteArray _content;\n\tstd::unique_ptr<FileLoadTask> _videoCover;\n\tstd::unique_ptr<Ui::PreparedFileInformation> _information;\n\tcrl::time _duration = 0;\n\tVoiceWaveform _waveform;\n\tSendMediaType _type;\n\tTextWithTags _caption;\n\tbool _spoiler = false;\n\tbool _forceFile = false;\n\tbool _sendLargePhotos = false;\n\n\tstd::shared_ptr<FilePrepareResult> _result;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/localstorage.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/localstorage.h\"\n\n#include \"storage/serialize_common.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/details/storage_file_utilities.h\"\n#include \"storage/details/storage_settings_scheme.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/random.h\"\n#include \"ui/power_saving.h\"\n#include \"core/update_checker.h\"\n#include \"core/file_location.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"media/audio/media_audio.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"window/themes/window_theme.h\"\n#include \"lang/lang_instance.h\"\n\n#include <QtCore/QDirIterator>\n\n#ifndef Q_OS_WIN\n#include <unistd.h>\n#endif // Q_OS_WIN\n\n//extern \"C\" {\n//#include <openssl/evp.h>\n//} // extern \"C\"\n\nnamespace Local {\nnamespace {\n\nconstexpr auto kThemeFileSizeLimit = 5 * 1024 * 1024;\nconstexpr auto kFileLoaderQueueStopTimeout = crl::time(5000);\n\nconstexpr auto kSavedBackgroundFormat = QImage::Format_ARGB32_Premultiplied;\nconstexpr auto kWallPaperLegacySerializeTagId = int32(-111);\nconstexpr auto kWallPaperSerializeTagId = int32(-112);\nconstexpr auto kWallPaperSidesLimit = 10'000;\n\nconst auto kThemeNewPathRelativeTag = u\"special://new_tag\"_q;\n\nusing namespace Storage::details;\nusing Storage::FileKey;\n\nusing Database = Storage::Cache::Database;\n\nQString _basePath, _userBasePath, _userDbPath;\n\nTaskQueue *_localLoader = nullptr;\n\nQByteArray _settingsSalt;\n\nauto OldKey = MTP::AuthKeyPtr();\nauto SettingsKey = MTP::AuthKeyPtr();\n\nFileKey _themeKeyDay = 0;\nFileKey _themeKeyNight = 0;\n\n// Theme key legacy may be read in start() with settings.\n// But it should be moved to keyDay or keyNight inside InitialLoadTheme()\n// and never used after.\nFileKey _themeKeyLegacy = 0;\nFileKey _langPackKey = 0;\nFileKey _languagesKey = 0;\n\nFileKey _backgroundKeyDay = 0;\nFileKey _backgroundKeyNight = 0;\nbool _useGlobalBackgroundKeys = false;\nbool _backgroundCanWrite = true;\n\nint32 _oldSettingsVersion = 0;\nbool _settingsRewriteNeeded = false;\nbool _settingsWriteAllowed = false;\n\nenum class WriteMapWhen {\n\tNow,\n\tFast,\n\tSoon,\n};\n\nbool CheckStreamStatus(QDataStream &stream) {\n\tif (stream.status() != QDataStream::Ok) {\n\t\tLOG((\"Bad data stream status: %1\").arg(stream.status()));\n\t\treturn false;\n\t}\n\treturn true;\n}\n\n[[nodiscard]] const MTP::Config &LookupFallbackConfig() {\n\tstatic const auto lookupConfig = [](not_null<Main::Account*> account) {\n\t\tconst auto mtp = &account->mtp();\n\t\tconst auto production = MTP::Environment::Production;\n\t\treturn (mtp->environment() == production)\n\t\t\t? &mtp->config()\n\t\t\t: nullptr;\n\t};\n\tconst auto &app = Core::App();\n\tconst auto &domain = app.domain();\n\tif (!domain.started()) {\n\t\treturn app.fallbackProductionConfig();\n\t}\n\tif (const auto result = lookupConfig(&app.activeAccount())) {\n\t\treturn *result;\n\t}\n\tfor (const auto &[_, account] : domain.accounts()) {\n\t\tif (const auto result = lookupConfig(account.get())) {\n\t\t\treturn *result;\n\t\t}\n\t}\n\treturn app.fallbackProductionConfig();\n}\n\nvoid applyReadContext(ReadSettingsContext &&context) {\n\tApplyReadFallbackConfig(context);\n\n\tDEBUG_LOG((\"Theme: applying context, legacy: %1, day: %2, night: %3\"\n\t\t).arg(context.themeKeyLegacy\n\t\t).arg(context.themeKeyDay\n\t\t).arg(context.themeKeyNight));\n\t_themeKeyLegacy = context.themeKeyLegacy;\n\t_themeKeyDay = context.themeKeyDay;\n\t_themeKeyNight = context.themeKeyNight;\n\t_backgroundKeyDay = context.backgroundKeyDay;\n\t_backgroundKeyNight = context.backgroundKeyNight;\n\t_useGlobalBackgroundKeys = context.backgroundKeysRead;\n\t_langPackKey = context.langPackKey;\n\t_languagesKey = context.languagesKey;\n}\n\nbool _readOldSettings(bool remove, ReadSettingsContext &context) {\n\tbool result = false;\n\tauto file = QFile(cWorkingDir() + u\"tdata/config\"_q);\n\tif (file.open(QIODevice::ReadOnly)) {\n\t\tLOG((\"App Info: reading old config...\"));\n\t\tQDataStream stream(&file);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\n\t\tqint32 version = 0;\n\t\twhile (!stream.atEnd()) {\n\t\t\tquint32 blockId;\n\t\t\tstream >> blockId;\n\t\t\tif (!CheckStreamStatus(stream)) break;\n\n\t\t\tif (blockId == dbiVersion) {\n\t\t\t\tstream >> version;\n\t\t\t\tif (!CheckStreamStatus(stream)) break;\n\n\t\t\t\tif (version > AppVersion) break;\n\t\t\t} else if (!ReadSetting(blockId, stream, version, context)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tfile.close();\n\t\tresult = true;\n\t}\n\tif (remove) file.remove();\n\treturn result;\n}\n\nvoid _readOldUserSettingsFields(\n\t\tQIODevice *device,\n\t\tqint32 &version,\n\t\tReadSettingsContext &context) {\n\tQDataStream stream(device);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\n\twhile (!stream.atEnd()) {\n\t\tquint32 blockId;\n\t\tstream >> blockId;\n\t\tif (!CheckStreamStatus(stream)) {\n\t\t\tbreak;\n\t\t}\n\n\t\tif (blockId == dbiVersion) {\n\t\t\tstream >> version;\n\t\t\tif (!CheckStreamStatus(stream)) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (version > AppVersion) return;\n\t\t} else if (blockId == dbiEncryptedWithSalt) {\n\t\t\tQByteArray salt, data, decrypted;\n\t\t\tstream >> salt >> data;\n\t\t\tif (!CheckStreamStatus(stream)) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (salt.size() != 32) {\n\t\t\t\tLOG((\"App Error: bad salt in old user config encrypted part, size: %1\").arg(salt.size()));\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tOldKey = CreateLegacyLocalKey(QByteArray(), salt);\n\n\t\t\tif (data.size() <= 16 || (data.size() & 0x0F)) {\n\t\t\t\tLOG((\"App Error: bad encrypted part size in old user config: %1\").arg(data.size()));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tuint32 fullDataLen = data.size() - 16;\n\t\t\tdecrypted.resize(fullDataLen);\n\t\t\tconst char *dataKey = data.constData(), *encrypted = data.constData() + 16;\n\t\t\taesDecryptLocal(encrypted, decrypted.data(), fullDataLen, OldKey, dataKey);\n\t\t\tuchar sha1Buffer[20];\n\t\t\tif (memcmp(hashSha1(decrypted.constData(), decrypted.size(), sha1Buffer), dataKey, 16)) {\n\t\t\t\tLOG((\"App Error: bad decrypt key, data from old user config not decrypted\"));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tuint32 dataLen = *(const uint32*)decrypted.constData();\n\t\t\tif (dataLen > uint32(decrypted.size()) || dataLen <= fullDataLen - 16 || dataLen < 4) {\n\t\t\t\tLOG((\"App Error: bad decrypted part size in old user config: %1, fullDataLen: %2, decrypted size: %3\").arg(dataLen).arg(fullDataLen).arg(decrypted.size()));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tdecrypted.resize(dataLen);\n\t\t\tQBuffer decryptedStream(&decrypted);\n\t\t\tdecryptedStream.open(QIODevice::ReadOnly);\n\t\t\tdecryptedStream.seek(4); // skip size\n\t\t\tLOG((\"App Info: reading encrypted old user config...\"));\n\n\t\t\t_readOldUserSettingsFields(&decryptedStream, version, context);\n\t\t} else if (!ReadSetting(blockId, stream, version, context)) {\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nbool _readOldUserSettings(bool remove, ReadSettingsContext &context) {\n\tbool result = false;\n\t// We dropped old test authorizations when migrated to multi auth.\n\t//const auto testPrefix = (cTestMode() ? u\"_test\"_q : QString());\n\tconst auto testPrefix = QString();\n\tQFile file(cWorkingDir() + cDataFile() + testPrefix + u\"_config\"_q);\n\tif (file.open(QIODevice::ReadOnly)) {\n\t\tLOG((\"App Info: reading old user config...\"));\n\t\tqint32 version = 0;\n\t\t_readOldUserSettingsFields(&file, version, context);\n\t\tfile.close();\n\t\tresult = true;\n\t}\n\tif (remove) file.remove();\n\treturn result;\n}\n\nvoid _readOldMtpDataFields(\n\t\tQIODevice *device,\n\t\tqint32 &version,\n\t\tReadSettingsContext &context) {\n\tQDataStream stream(device);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\n\twhile (!stream.atEnd()) {\n\t\tquint32 blockId;\n\t\tstream >> blockId;\n\t\tif (!CheckStreamStatus(stream)) {\n\t\t\tbreak;\n\t\t}\n\n\t\tif (blockId == dbiVersion) {\n\t\t\tstream >> version;\n\t\t\tif (!CheckStreamStatus(stream)) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (version > AppVersion) return;\n\t\t} else if (blockId == dbiEncrypted) {\n\t\t\tQByteArray data, decrypted;\n\t\t\tstream >> data;\n\t\t\tif (!CheckStreamStatus(stream)) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (!OldKey) {\n\t\t\t\tLOG((\"MTP Error: reading old encrypted keys without old key!\"));\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (data.size() <= 16 || (data.size() & 0x0F)) {\n\t\t\t\tLOG((\"MTP Error: bad encrypted part size in old keys: %1\").arg(data.size()));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tuint32 fullDataLen = data.size() - 16;\n\t\t\tdecrypted.resize(fullDataLen);\n\t\t\tconst char *dataKey = data.constData(), *encrypted = data.constData() + 16;\n\t\t\taesDecryptLocal(encrypted, decrypted.data(), fullDataLen, OldKey, dataKey);\n\t\t\tuchar sha1Buffer[20];\n\t\t\tif (memcmp(hashSha1(decrypted.constData(), decrypted.size(), sha1Buffer), dataKey, 16)) {\n\t\t\t\tLOG((\"MTP Error: bad decrypt key, data from old keys not decrypted\"));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tuint32 dataLen = *(const uint32*)decrypted.constData();\n\t\t\tif (dataLen > uint32(decrypted.size()) || dataLen <= fullDataLen - 16 || dataLen < 4) {\n\t\t\t\tLOG((\"MTP Error: bad decrypted part size in old keys: %1, fullDataLen: %2, decrypted size: %3\").arg(dataLen).arg(fullDataLen).arg(decrypted.size()));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tdecrypted.resize(dataLen);\n\t\t\tQBuffer decryptedStream(&decrypted);\n\t\t\tdecryptedStream.open(QIODevice::ReadOnly);\n\t\t\tdecryptedStream.seek(4); // skip size\n\t\t\tLOG((\"App Info: reading encrypted old keys...\"));\n\n\t\t\t_readOldMtpDataFields(&decryptedStream, version, context);\n\t\t} else if (!ReadSetting(blockId, stream, version, context)) {\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nbool _readOldMtpData(bool remove, ReadSettingsContext &context) {\n\tbool result = false;\n\t// We dropped old test authorizations when migrated to multi auth.\n\t//const auto testPostfix = (cTestMode() ? u\"_test\"_q : QString());\n\tconst auto testPostfix = QString();\n\tQFile file(cWorkingDir() + cDataFile() + testPostfix);\n\tif (file.open(QIODevice::ReadOnly)) {\n\t\tLOG((\"App Info: reading old keys...\"));\n\t\tqint32 version = 0;\n\t\t_readOldMtpDataFields(&file, version, context);\n\t\tfile.close();\n\t\tresult = true;\n\t}\n\tif (remove) file.remove();\n\treturn result;\n}\n\n} // namespace\n\nvoid sync() {\n\tStorage::details::Sync();\n}\n\nvoid finish() {\n\tdelete base::take(_localLoader);\n\tStorage::details::Finish();\n}\n\nvoid InitialLoadTheme();\nbool ApplyDefaultNightMode();\nvoid readLangPack();\n\nvoid start() {\n\tExpects(_basePath.isEmpty());\n\n\t_localLoader = new TaskQueue(kFileLoaderQueueStopTimeout);\n\n\t_basePath = cWorkingDir() + u\"tdata/\"_q;\n\tif (!QDir().exists(_basePath)) QDir().mkpath(_basePath);\n\n\tReadSettingsContext context;\n\tFileReadDescriptor settingsData;\n\t// We dropped old test authorizations when migrated to multi auth.\n\t//const auto name = cTestMode() ? u\"settings_test\"_q : u\"settings\"_q;\n\tconst auto name = u\"settings\"_q;\n\tif (!ReadFile(settingsData, name, _basePath)) {\n\t\t_readOldSettings(true, context);\n\t\t_readOldUserSettings(false, context); // needed further in _readUserSettings\n\t\t_readOldMtpData(false, context); // needed further in _readMtpData\n\t\tapplyReadContext(std::move(context));\n\n\t\t_settingsRewriteNeeded = true;\n\t\tApplyDefaultNightMode();\n\t\treturn;\n\t}\n\tLOG((\"App Info: reading settings...\"));\n\n\tQByteArray salt, settingsEncrypted;\n\tsettingsData.stream >> salt >> settingsEncrypted;\n\tif (!CheckStreamStatus(settingsData.stream)) {\n\t\treturn writeSettings();\n\t}\n\n\tif (salt.size() != LocalEncryptSaltSize) {\n\t\tLOG((\"App Error: bad salt in settings file, size: %1\").arg(salt.size()));\n\t\treturn writeSettings();\n\t}\n\tSettingsKey = CreateLegacyLocalKey(QByteArray(), salt);\n\n\tEncryptedDescriptor settings;\n\tif (!DecryptLocal(settings, settingsEncrypted, SettingsKey)) {\n\t\tLOG((\"App Error: could not decrypt settings from settings file...\"));\n\t\treturn writeSettings();\n\t}\n\n\tLOG((\"App Info: reading encrypted settings...\"));\n\twhile (!settings.stream.atEnd()) {\n\t\tquint32 blockId;\n\t\tsettings.stream >> blockId;\n\t\tif (!CheckStreamStatus(settings.stream)) {\n\t\t\treturn writeSettings();\n\t\t}\n\n\t\tif (!ReadSetting(blockId, settings.stream, settingsData.version, context)) {\n\t\t\treturn writeSettings();\n\t\t}\n\t}\n\n\t_oldSettingsVersion = settingsData.version;\n\t_settingsSalt = salt;\n\n\tapplyReadContext(std::move(context));\n\tif (context.legacyRead) {\n\t\twriteSettings();\n\t}\n\n\tInitialLoadTheme();\n\n\tif (context.tileRead && _useGlobalBackgroundKeys) {\n\t\tWindow::Theme::Background()->setTileDayValue(context.tileDay);\n\t\tWindow::Theme::Background()->setTileNightValue(context.tileNight);\n\t}\n\n\treadLangPack();\n}\n\nvoid writeSettings() {\n\tif (!_settingsWriteAllowed) {\n\t\t_settingsRewriteNeeded = true;\n\n\t\t// We need to generate SettingsKey anyway,\n\t\t// for the moveLegacyBackground to work.\n\t\tif (SettingsKey) {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (_basePath.isEmpty()) {\n\t\tLOG((\"App Error: _basePath is empty in writeSettings()\"));\n\t\treturn;\n\t}\n\n\tif (!QDir().exists(_basePath)) QDir().mkpath(_basePath);\n\n\t// We dropped old test authorizations when migrated to multi auth.\n\t//const auto name = cTestMode() ? u\"settings_test\"_q : u\"settings\"_q;\n\tconst auto name = u\"settings\"_q;\n\tFileWriteDescriptor settings(name, _basePath);\n\tif (_settingsSalt.isEmpty() || !SettingsKey) {\n\t\t_settingsSalt.resize(LocalEncryptSaltSize);\n\t\tbase::RandomFill(_settingsSalt.data(), _settingsSalt.size());\n\t\tSettingsKey = CreateLegacyLocalKey(QByteArray(), _settingsSalt);\n\t}\n\tsettings.writeData(_settingsSalt);\n\n\tif (!_settingsWriteAllowed) {\n\t\tEncryptedDescriptor data(0);\n\t\tsettings.writeEncrypted(data, SettingsKey);\n\t\treturn;\n\t}\n\tconst auto configSerialized = LookupFallbackConfig().serialize();\n\tconst auto applicationSettings = Core::App().settings().serialize();\n\tconst auto forkSettings = Core::App().settings().fork().serialize();\n\n\tquint32 size = 9 * (sizeof(quint32) + sizeof(qint32));\n\tsize += sizeof(quint32) + Serialize::bytearraySize(configSerialized);\n\tsize += sizeof(quint32) + Serialize::bytearraySize(applicationSettings);\n\tsize += sizeof(quint32) + Serialize::stringSize(cDialogLastPath());\n\tsize += sizeof(quint32) + Serialize::bytearraySize(forkSettings);\n\n\t// Theme keys and night mode.\n\tsize += sizeof(quint32) + sizeof(quint64) * 2 + sizeof(quint32);\n\tsize += sizeof(quint32) + sizeof(quint64) * 2;\n\tif (_langPackKey) {\n\t\tsize += sizeof(quint32) + sizeof(quint64);\n\t}\n\tsize += sizeof(quint32) + sizeof(qint32) * 8;\n\n\tconst auto powerSaving = PowerSaving::Current().value();\n\n\tEncryptedDescriptor data(size);\n\tdata.stream << quint32(dbiAutoStart) << qint32(cAutoStart());\n\tdata.stream << quint32(dbiStartMinimized) << qint32(cStartMinimized());\n\tdata.stream << quint32(dbiSendToMenu) << qint32(cSendToMenu());\n\tdata.stream << quint32(dbiSeenTrayTooltip) << qint32(cSeenTrayTooltip());\n\tdata.stream << quint32(dbiAutoUpdate) << qint32(cAutoUpdate());\n\tdata.stream << quint32(dbiLastUpdateCheck) << qint32(cLastUpdateCheck());\n\tdata.stream << quint32(dbiScalePercent) << qint32(cConfigScale());\n\tdata.stream << quint32(dbiFallbackProductionConfig) << configSerialized;\n\tdata.stream << quint32(dbiApplicationSettings) << applicationSettings;\n\tdata.stream << quint32(dbiDialogLastPath) << cDialogLastPath();\n\tdata.stream << quint32(dbiPowerSaving) << qint32(powerSaving);\n\n\tdata.stream\n\t\t<< quint32(dbiThemeKey)\n\t\t<< quint64(_themeKeyDay)\n\t\t<< quint64(_themeKeyNight)\n\t\t<< quint32(Window::Theme::IsNightMode() ? 1 : 0);\n\tif (_useGlobalBackgroundKeys) {\n\t\tdata.stream\n\t\t\t<< quint32(dbiBackgroundKey)\n\t\t\t<< quint64(_backgroundKeyDay)\n\t\t\t<< quint64(_backgroundKeyNight);\n\t\tdata.stream\n\t\t\t<< quint32(dbiTileBackground)\n\t\t\t<< qint32(Window::Theme::Background()->tileDay() ? 1 : 0)\n\t\t\t<< qint32(Window::Theme::Background()->tileNight() ? 1 : 0);\n\t}\n\tif (_langPackKey) {\n\t\tdata.stream << quint32(dbiLangPackKey) << quint64(_langPackKey);\n\t}\n\tif (_languagesKey) {\n\t\tdata.stream << quint32(dbiLanguagesKey) << quint64(_languagesKey);\n\t}\n\tdata.stream << quint32(dbiForkgramSettings) << forkSettings;\n\n\tsettings.writeEncrypted(data, SettingsKey);\n}\n\nvoid rewriteSettingsIfNeeded() {\n\tif (_settingsWriteAllowed) {\n\t\treturn;\n\t}\n\t_settingsWriteAllowed = true;\n\tif (_oldSettingsVersion < AppVersion || _settingsRewriteNeeded) {\n\t\twriteSettings();\n\t}\n}\n\nconst QString &AutoupdatePrefix(const QString &replaceWith = {}) {\n\tExpects(!Core::UpdaterDisabled());\n\n\tstatic auto value = QString();\n\tif (!replaceWith.isEmpty()) {\n\t\tvalue = replaceWith;\n\t}\n\treturn value;\n}\n\nQString autoupdatePrefixFile() {\n\tExpects(!Core::UpdaterDisabled());\n\n\treturn cWorkingDir() + \"tdata/prefix\";\n}\n\nconst QString &readAutoupdatePrefixRaw() {\n\tExpects(!Core::UpdaterDisabled());\n\n\tconst auto &result = AutoupdatePrefix();\n\tif (!result.isEmpty()) {\n\t\treturn result;\n\t}\n\tQFile f(autoupdatePrefixFile());\n\tif (f.open(QIODevice::ReadOnly)) {\n\t\tconst auto value = QString::fromUtf8(f.readAll());\n\t\tif (!value.isEmpty()) {\n\t\t\treturn AutoupdatePrefix(value);\n\t\t}\n\t}\n\treturn AutoupdatePrefix(\"https://dummy_link.dummy_link\");\n}\n\nvoid writeAutoupdatePrefix(const QString &prefix) {\n\tif (Core::UpdaterDisabled()) {\n\t\treturn;\n\t}\n\n\tconst auto current = readAutoupdatePrefixRaw();\n\tif (current != prefix) {\n\t\tAutoupdatePrefix(prefix);\n\t\tQFile f(autoupdatePrefixFile());\n\t\tif (f.open(QIODevice::WriteOnly)) {\n\t\t\tf.write(prefix.toUtf8());\n\t\t\tf.close();\n\t\t}\n\t\tif (cAutoUpdate()) {\n\t\t\tCore::UpdateChecker checker;\n\t\t\tchecker.start();\n\t\t}\n\t}\n}\n\nQString readAutoupdatePrefix() {\n\tExpects(!Core::UpdaterDisabled());\n\n\tstatic const auto RegExp = QRegularExpression(\"/+$\");\n\tauto result = readAutoupdatePrefixRaw();\n\treturn result.replace(RegExp, QString());\n}\n\nvoid writeBackground(const Data::WallPaper &paper, const QImage &image) {\n\tExpects(_settingsWriteAllowed);\n\n\tif (!_backgroundCanWrite) {\n\t\treturn;\n\t}\n\n\t_useGlobalBackgroundKeys = true;\n\tauto &backgroundKey = Window::Theme::IsNightMode()\n\t\t? _backgroundKeyNight\n\t\t: _backgroundKeyDay;\n\tauto imageData = QByteArray();\n\tif (!image.isNull()) {\n\t\tconst auto width = qint32(image.width());\n\t\tconst auto height = qint32(image.height());\n\t\tconst auto perpixel = (image.depth() >> 3);\n\t\tconst auto srcperline = image.bytesPerLine();\n\t\tconst auto srcsize = srcperline * height;\n\t\tconst auto dstperline = width * perpixel;\n\t\tconst auto dstsize = dstperline * height;\n\t\tconst auto copy = (image.format() != kSavedBackgroundFormat)\n\t\t\t? image.convertToFormat(kSavedBackgroundFormat)\n\t\t\t: image;\n\t\timageData.resize(2 * sizeof(qint32) + dstsize);\n\n\t\tauto dst = bytes::make_detached_span(imageData);\n\t\tbytes::copy(dst, bytes::object_as_span(&width));\n\t\tdst = dst.subspan(sizeof(qint32));\n\t\tbytes::copy(dst, bytes::object_as_span(&height));\n\t\tdst = dst.subspan(sizeof(qint32));\n\t\tconst auto src = bytes::make_span(copy.constBits(), srcsize);\n\t\tif (srcsize == dstsize) {\n\t\t\tbytes::copy(dst, src);\n\t\t} else {\n\t\t\tfor (auto y = 0; y != height; ++y) {\n\t\t\t\tbytes::copy(dst, src.subspan(y * srcperline, dstperline));\n\t\t\t\tdst = dst.subspan(dstperline);\n\t\t\t}\n\t\t}\n\t}\n\tif (!backgroundKey) {\n\t\tbackgroundKey = GenerateKey(_basePath);\n\t\twriteSettings();\n\t}\n\tconst auto serialized = paper.serialize();\n\tquint32 size = sizeof(qint32)\n\t\t+ Serialize::bytearraySize(serialized)\n\t\t+ Serialize::bytearraySize(imageData);\n\tEncryptedDescriptor data(size);\n\tdata.stream\n\t\t<< qint32(kWallPaperSerializeTagId)\n\t\t<< serialized\n\t\t<< imageData;\n\n\tFileWriteDescriptor file(backgroundKey, _basePath);\n\tfile.writeEncrypted(data, SettingsKey);\n}\n\nbool readBackground() {\n\tFileReadDescriptor bg;\n\tauto &backgroundKey = Window::Theme::IsNightMode()\n\t\t? _backgroundKeyNight\n\t\t: _backgroundKeyDay;\n\tif (!ReadEncryptedFile(bg, backgroundKey, _basePath, SettingsKey)) {\n\t\tif (backgroundKey) {\n\t\t\tClearKey(backgroundKey, _basePath);\n\t\t\tbackgroundKey = 0;\n\t\t\twriteSettings();\n\t\t}\n\t\treturn false;\n\t}\n\n\tqint32 legacyId = 0;\n\tbg.stream >> legacyId;\n\tconst auto paper = [&] {\n\t\tif (legacyId == kWallPaperLegacySerializeTagId) {\n\t\t\tquint64 id = 0;\n\t\t\tquint64 accessHash = 0;\n\t\t\tquint32 flags = 0;\n\t\t\tQString slug;\n\t\t\tbg.stream\n\t\t\t\t>> id\n\t\t\t\t>> accessHash\n\t\t\t\t>> flags\n\t\t\t\t>> slug;\n\t\t\treturn Data::WallPaper::FromLegacySerialized(\n\t\t\t\tid,\n\t\t\t\taccessHash,\n\t\t\t\tflags,\n\t\t\t\tslug);\n\t\t} else if (legacyId == kWallPaperSerializeTagId) {\n\t\t\tQByteArray serialized;\n\t\t\tbg.stream >> serialized;\n\t\t\treturn Data::WallPaper::FromSerialized(serialized);\n\t\t} else {\n\t\t\treturn Data::WallPaper::FromLegacyId(legacyId);\n\t\t}\n\t}();\n\tif (bg.stream.status() != QDataStream::Ok || !paper) {\n\t\treturn false;\n\t}\n\n\tQByteArray imageData;\n\tbg.stream >> imageData;\n\tconst auto isOldEmptyImage = (bg.stream.status() != QDataStream::Ok);\n\tif (isOldEmptyImage\n\t\t|| Data::IsLegacy1DefaultWallPaper(*paper)\n\t\t|| (Data::IsLegacy2DefaultWallPaper(*paper) && bg.version < 3000000)\n\t\t|| (Data::IsLegacy3DefaultWallPaper(*paper) && bg.version < 3000000)\n\t\t|| (Data::IsLegacy4DefaultWallPaper(*paper) && bg.version < 3000000)\n\t\t|| Data::IsDefaultWallPaper(*paper)) {\n\t\t_backgroundCanWrite = false;\n\t\tif (isOldEmptyImage || bg.version < 3000000) {\n\t\t\tWindow::Theme::Background()->set(Data::DefaultWallPaper());\n\t\t} else {\n\t\t\tWindow::Theme::Background()->set(*paper);\n\t\t}\n\t\t_backgroundCanWrite = true;\n\t\treturn true;\n\t} else if (Data::IsThemeWallPaper(*paper) && imageData.isEmpty()) {\n\t\t_backgroundCanWrite = false;\n\t\tWindow::Theme::Background()->set(*paper);\n\t\t_backgroundCanWrite = true;\n\t\treturn true;\n\t}\n\tauto image = QImage();\n\tif (legacyId == kWallPaperSerializeTagId) {\n\t\tconst auto perpixel = 4;\n\t\tauto src = bytes::make_span(imageData);\n\t\tauto width = qint32();\n\t\tauto height = qint32();\n\t\tif (src.size() > 2 * sizeof(qint32)) {\n\t\t\tbytes::copy(\n\t\t\t\tbytes::object_as_span(&width),\n\t\t\t\tsrc.subspan(0, sizeof(qint32)));\n\t\t\tsrc = src.subspan(sizeof(qint32));\n\t\t\tbytes::copy(\n\t\t\t\tbytes::object_as_span(&height),\n\t\t\t\tsrc.subspan(0, sizeof(qint32)));\n\t\t\tsrc = src.subspan(sizeof(qint32));\n\t\t\tif (width + height <= kWallPaperSidesLimit\n\t\t\t\t&& src.size() == width * height * perpixel) {\n\t\t\t\timage = QImage(\n\t\t\t\t\twidth,\n\t\t\t\t\theight,\n\t\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t\tif (!image.isNull()) {\n\t\t\t\t\tconst auto srcperline = width * perpixel;\n\t\t\t\t\tconst auto srcsize = srcperline * height;\n\t\t\t\t\tconst auto dstperline = image.bytesPerLine();\n\t\t\t\t\tconst auto dstsize = dstperline * height;\n\t\t\t\t\tAssert(srcsize == dstsize);\n\t\t\t\t\tbytes::copy(\n\t\t\t\t\t\tbytes::make_span(image.bits(), dstsize),\n\t\t\t\t\t\tsrc);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tauto buffer = QBuffer(&imageData);\n\t\tauto reader = QImageReader(&buffer);\n\t\treader.setAutoTransform(true);\n\t\tif (!reader.read(&image)) {\n\t\t\timage = QImage();\n\t\t}\n\t}\n\tif (!image.isNull() || !paper->backgroundColors().empty()) {\n\t\t_backgroundCanWrite = false;\n\t\tWindow::Theme::Background()->set(*paper, std::move(image));\n\t\t_backgroundCanWrite = true;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid moveLegacyBackground(\n\t\tconst QString &fromBasePath,\n\t\tconst MTP::AuthKeyPtr &fromLocalKey,\n\t\tuint64 legacyBackgroundKeyDay,\n\t\tuint64 legacyBackgroundKeyNight) {\n\tif (_useGlobalBackgroundKeys\n\t\t|| (!legacyBackgroundKeyDay && !legacyBackgroundKeyNight)) {\n\t\treturn;\n\t}\n\tconst auto move = [&](uint64 from, FileKey &to) {\n\t\tif (!from || to) {\n\t\t\treturn;\n\t\t}\n\t\tto = GenerateKey(_basePath);\n\t\tFileReadDescriptor read;\n\t\tif (!ReadEncryptedFile(read, from, fromBasePath, fromLocalKey)) {\n\t\t\treturn;\n\t\t}\n\t\tEncryptedDescriptor data;\n\t\tdata.data = read.data;\n\t\tFileWriteDescriptor write(to, _basePath);\n\t\twrite.writeEncrypted(data, SettingsKey);\n\t};\n\tmove(legacyBackgroundKeyDay, _backgroundKeyDay);\n\tmove(legacyBackgroundKeyNight, _backgroundKeyNight);\n\t_useGlobalBackgroundKeys = true;\n\t_settingsRewriteNeeded = true;\n}\n\nvoid reset() {\n\tif (_localLoader) {\n\t\t_localLoader->stop();\n\t}\n\n\tWindow::Theme::Background()->reset();\n\t_oldSettingsVersion = 0;\n\tCore::App().settings().resetOnLastLogout();\n\twriteSettings();\n}\n\nint32 oldSettingsVersion() {\n\treturn _oldSettingsVersion;\n}\n\nclass CountWaveformTask : public Task {\npublic:\n\tCountWaveformTask(not_null<Data::DocumentMedia*> media)\n\t: _doc(media->owner())\n\t, _loc(_doc->location(true))\n\t, _data(media->bytes())\n\t, _wavemax(0) {\n\t\tif (_data.isEmpty() && !_loc.accessEnable()) {\n\t\t\t_doc = nullptr;\n\t\t}\n\t}\n\tvoid process() override {\n\t\tif (!_doc) return;\n\n\t\t_waveform = audioCountWaveform(_loc, _data);\n\t\t_wavemax = _waveform.empty()\n\t\t\t? char(0)\n\t\t\t: *ranges::max_element(_waveform);\n\t}\n\tvoid finish() override {\n\t\tif (const auto voice = _doc ? _doc->voice() : nullptr) {\n\t\t\tif (!_waveform.isEmpty()) {\n\t\t\t\tvoice->waveform = _waveform;\n\t\t\t\tvoice->wavemax = _wavemax;\n\t\t\t}\n\t\t\tif (voice->waveform.isEmpty()) {\n\t\t\t\tvoice->waveform.resize(1);\n\t\t\t\tvoice->waveform[0] = -2;\n\t\t\t\tvoice->wavemax = 0;\n\t\t\t} else if (voice->waveform[0] < 0) {\n\t\t\t\tvoice->waveform[0] = -2;\n\t\t\t\tvoice->wavemax = 0;\n\t\t\t}\n\t\t\t_doc->owner().requestDocumentViewRepaint(_doc);\n\t\t}\n\t}\n\t~CountWaveformTask() {\n\t\tif (_data.isEmpty() && _doc) {\n\t\t\t_loc.accessDisable();\n\t\t}\n\t}\n\nprotected:\n\tDocumentData *_doc = nullptr;\n\tCore::FileLocation _loc;\n\tQByteArray _data;\n\tVoiceWaveform _waveform;\n\tchar _wavemax;\n\n};\n\nvoid countVoiceWaveform(not_null<Data::DocumentMedia*> media) {\n\tconst auto document = media->owner();\n\tif (const auto voice = document->voice()) {\n\t\tif (_localLoader) {\n\t\t\tvoice->waveform.resize(1 + sizeof(TaskId));\n\t\t\tvoice->waveform[0] = -1; // counting\n\t\t\tTaskId taskId = _localLoader->addTask(\n\t\t\t\tstd::make_unique<CountWaveformTask>(media));\n\t\t\tmemcpy(voice->waveform.data() + 1, &taskId, sizeof(taskId));\n\t\t}\n\t}\n}\n\nvoid cancelTask(TaskId id) {\n\tif (_localLoader) {\n\t\t_localLoader->cancelTask(id);\n\t}\n}\n\nWindow::Theme::Saved readThemeUsingKey(FileKey key) {\n\tusing namespace Window::Theme;\n\n\tFileReadDescriptor theme;\n\tif (!ReadEncryptedFile(theme, key, _basePath, SettingsKey)) {\n\t\tDEBUG_LOG((\"Theme: Could not read file for key: %1\").arg(key));\n\t\treturn {};\n\t}\n\n\tauto tag = QString();\n\tauto result = Saved();\n\tauto &object = result.object;\n\tauto &cache = result.cache;\n\tauto field1 = qint32();\n\tauto field2 = quint32();\n\ttheme.stream >> object.content;\n\ttheme.stream >> tag >> object.pathAbsolute;\n\tif (tag == kThemeNewPathRelativeTag) {\n\t\ttheme.stream\n\t\t\t>> object.pathRelative\n\t\t\t>> object.cloud.id\n\t\t\t>> object.cloud.accessHash\n\t\t\t>> object.cloud.slug\n\t\t\t>> object.cloud.title\n\t\t\t>> object.cloud.documentId\n\t\t\t>> field1;\n\t} else {\n\t\tobject.pathRelative = tag;\n\t}\n\tif (theme.stream.status() != QDataStream::Ok) {\n\t\tDEBUG_LOG((\"Theme: Bad status for key: %1, tag: %2\"\n\t\t\t).arg(key\n\t\t\t).arg(tag));\n\t\treturn {};\n\t}\n\n\tauto ignoreCache = false;\n\tif (!object.cloud.id) {\n\t\tauto file = QFile(object.pathRelative);\n\t\tif (object.pathRelative.isEmpty() || !file.exists()) {\n\t\t\tfile.setFileName(object.pathAbsolute);\n\t\t}\n\t\tif (!file.fileName().isEmpty()\n\t\t\t&& file.exists()\n\t\t\t&& file.open(QIODevice::ReadOnly)) {\n\t\t\tif (file.size() > kThemeFileSizeLimit) {\n\t\t\t\tLOG((\"Error: theme file too large: %1 \"\n\t\t\t\t\t\"(should be less than 5 MB, got %2)\"\n\t\t\t\t\t).arg(file.fileName()\n\t\t\t\t\t).arg(file.size()));\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\tauto fileContent = file.readAll();\n\t\t\tfile.close();\n\t\t\tif (object.content != fileContent) {\n\t\t\t\tobject.content = fileContent;\n\t\t\t\tignoreCache = true;\n\t\t\t}\n\t\t}\n\t}\n\tint32 cachePaletteChecksum = 0;\n\tint32 cacheContentChecksum = 0;\n\tQByteArray cacheColors;\n\tQByteArray cacheBackground;\n\ttheme.stream\n\t\t>> cachePaletteChecksum\n\t\t>> cacheContentChecksum\n\t\t>> cacheColors\n\t\t>> cacheBackground\n\t\t>> field2;\n\tif (!ignoreCache) {\n\t\tif (theme.stream.status() != QDataStream::Ok) {\n\t\t\tDEBUG_LOG((\"Theme: Bad status for cache, key: %1, tag: %2\"\n\t\t\t\t).arg(key\n\t\t\t\t).arg(tag));\n\t\t\treturn {};\n\t\t}\n\t\tcache.paletteChecksum = cachePaletteChecksum;\n\t\tcache.contentChecksum = cacheContentChecksum;\n\t\tcache.colors = std::move(cacheColors);\n\t\tcache.background = std::move(cacheBackground);\n\t\tcache.tiled = ((field2 & quint32(0xFF)) == 1);\n\t}\n\tif (tag == kThemeNewPathRelativeTag) {\n\t\tobject.cloud.createdBy = UserId(\n\t\t\t((quint64(field2) >> 8) << 32) | quint64(quint32(field1)));\n\t}\n\treturn result;\n}\n\nstd::optional<QString> InitialLoadThemeUsingKey(FileKey key) {\n\tauto read = readThemeUsingKey(key);\n\tconst auto result = read.object.pathAbsolute;\n\tif (read.object.content.isEmpty()) {\n\t\tDEBUG_LOG((\"Theme: Could not read content for key: %1\").arg(key));\n\t}\n\tif (read.object.content.isEmpty()\n\t\t|| !Window::Theme::Initialize(std::move(read))) {\n\t\tDEBUG_LOG((\"Theme: Could not initialized for key: %1\").arg(key));\n\t\treturn std::nullopt;\n\t}\n\treturn result;\n}\n\nvoid writeTheme(const Window::Theme::Saved &saved) {\n\tusing namespace Window::Theme;\n\n\tif (_themeKeyLegacy) {\n\t\tDEBUG_LOG((\"Theme: skipping write, because legacy: %1\"\n\t\t\t).arg(_themeKeyLegacy));\n\t\treturn;\n\t}\n\tauto &themeKey = IsNightMode()\n\t\t? _themeKeyNight\n\t\t: _themeKeyDay;\n\tDEBUG_LOG((\"Theme: writing (night: %1), key_day: %2, key_night: %3\"\n\t\t).arg(Logs::b(IsNightMode())\n\t\t).arg(_themeKeyDay\n\t\t).arg(_themeKeyNight));\n\tif (saved.object.content.isEmpty()) {\n\t\tif (themeKey) {\n\t\t\tif (IsNightMode()) {\n\t\t\t\tDEBUG_LOG((\"Theme: cleared for night mode.\"));\n\t\t\t\tSetNightModeValue(false);\n\t\t\t}\n\t\t\tClearKey(themeKey, _basePath);\n\t\t\tthemeKey = 0;\n\t\t\twriteSettings();\n\t\t}\n\t\treturn;\n\t}\n\n\tif (!themeKey) {\n\t\tthemeKey = GenerateKey(_basePath);\n\t\twriteSettings();\n\t}\n\n\tconst auto &object = saved.object;\n\tconst auto &cache = saved.cache;\n\tconst auto tag = QString(kThemeNewPathRelativeTag);\n\tquint32 size = Serialize::bytearraySize(object.content)\n\t\t+ Serialize::stringSize(tag)\n\t\t+ Serialize::stringSize(object.pathAbsolute)\n\t\t+ Serialize::stringSize(object.pathRelative)\n\t\t+ sizeof(uint64) * 3\n\t\t+ Serialize::stringSize(object.cloud.slug)\n\t\t+ Serialize::stringSize(object.cloud.title)\n\t\t+ sizeof(qint32)\n\t\t+ sizeof(qint32) * 2\n\t\t+ Serialize::bytearraySize(cache.colors)\n\t\t+ Serialize::bytearraySize(cache.background)\n\t\t+ sizeof(quint32);\n\tconst auto bareCreatedById = object.cloud.createdBy.bare;\n\tAssert((bareCreatedById & PeerId::kChatTypeMask) == bareCreatedById);\n\tconst auto field1 = qint32(quint32(bareCreatedById & 0xFFFFFFFFULL));\n\tconst auto field2 = quint32(cache.tiled ? 1 : 0)\n\t\t| (quint32(bareCreatedById >> 32) << 8);\n\tEncryptedDescriptor data(size);\n\tdata.stream\n\t\t<< object.content\n\t\t<< tag\n\t\t<< object.pathAbsolute\n\t\t<< object.pathRelative\n\t\t<< object.cloud.id\n\t\t<< object.cloud.accessHash\n\t\t<< object.cloud.slug\n\t\t<< object.cloud.title\n\t\t<< object.cloud.documentId\n\t\t<< field1\n\t\t<< cache.paletteChecksum\n\t\t<< cache.contentChecksum\n\t\t<< cache.colors\n\t\t<< cache.background\n\t\t<< field2;\n\n\tFileWriteDescriptor file(themeKey, _basePath);\n\tfile.writeEncrypted(data, SettingsKey);\n}\n\nvoid clearTheme() {\n\twriteTheme(Window::Theme::Saved());\n}\n\nvoid InitialLoadTheme() {\n\tconst auto key = (_themeKeyLegacy != 0)\n\t\t? _themeKeyLegacy\n\t\t: (Window::Theme::IsNightMode()\n\t\t\t? _themeKeyNight\n\t\t\t: _themeKeyDay);\n\tDEBUG_LOG((\"Theme: initial load (night: %1), \"\n\t\t\"key_legacy: %2, key_day: %3, key_night: %4\"\n\t\t).arg(Logs::b(Window::Theme::IsNightMode())\n\t\t).arg(_themeKeyLegacy\n\t\t).arg(_themeKeyDay\n\t\t).arg(_themeKeyNight));\n\tif (!key) {\n\t\tif (Window::Theme::IsNightMode()) {\n\t\t\tDEBUG_LOG((\"Theme: zero key for night mode.\"));\n\t\t\tWindow::Theme::SetNightModeValue(false);\n\t\t}\n\t\treturn;\n\t} else if (const auto path = InitialLoadThemeUsingKey(key)) {\n\t\tDEBUG_LOG((\"Theme: loaded with result: %1\").arg(*path));\n\t\tif (_themeKeyLegacy) {\n\t\t\tWindow::Theme::SetNightModeValue(*path\n\t\t\t\t== Window::Theme::NightThemePath());\n\t\t\t(Window::Theme::IsNightMode()\n\t\t\t\t? _themeKeyNight\n\t\t\t\t: _themeKeyDay) = base::take(_themeKeyLegacy);\n\t\t\tDEBUG_LOG((\"Theme: now (night: %1), \"\n\t\t\t\t\"key_legacy: %2, key_day: %3, key_night: %4 (path: %5)\"\n\t\t\t\t).arg(Logs::b(Window::Theme::IsNightMode())\n\t\t\t\t).arg(_themeKeyLegacy\n\t\t\t\t).arg(_themeKeyDay\n\t\t\t\t).arg(_themeKeyNight\n\t\t\t\t).arg(*path));\n\t\t}\n\t} else {\n\t\tDEBUG_LOG((\"Theme: could not load, clearing..\"));\n\t\tclearTheme();\n\t}\n}\n\nbool ApplyDefaultNightMode() {\n\tconst auto NightByDefault = Platform::IsMacStoreBuild();\n\tif (!NightByDefault\n\t\t|| Window::Theme::IsNightMode()\n\t\t|| _themeKeyDay\n\t\t|| _themeKeyNight\n\t\t|| _themeKeyLegacy) {\n\t\treturn false;\n\t}\n\tCore::App().startSettingsAndBackground();\n\tWindow::Theme::ToggleNightMode();\n\tWindow::Theme::KeepApplied();\n\treturn true;\n}\n\nWindow::Theme::Saved readThemeAfterSwitch() {\n\tconst auto key = Window::Theme::IsNightMode()\n\t\t? _themeKeyNight\n\t\t: _themeKeyDay;\n\treturn readThemeUsingKey(key);\n}\n\nvoid readLangPack() {\n\tFileReadDescriptor langpack;\n\tif (!_langPackKey || !ReadEncryptedFile(langpack, _langPackKey, _basePath, SettingsKey)) {\n\t\treturn;\n\t}\n\tauto data = QByteArray();\n\tlangpack.stream >> data;\n\tif (langpack.stream.status() == QDataStream::Ok) {\n\t\tLang::GetInstance().fillFromSerialized(data, langpack.version);\n\t}\n}\n\nvoid writeLangPack() {\n\tauto langpack = Lang::GetInstance().serialize();\n\tif (!_langPackKey) {\n\t\t_langPackKey = GenerateKey(_basePath);\n\t\twriteSettings();\n\t}\n\n\tEncryptedDescriptor data(Serialize::bytearraySize(langpack));\n\tdata.stream << langpack;\n\n\tFileWriteDescriptor file(_langPackKey, _basePath);\n\tfile.writeEncrypted(data, SettingsKey);\n}\n\nvoid saveRecentLanguages(const std::vector<Lang::Language> &list) {\n\tif (list.empty()) {\n\t\tif (_languagesKey) {\n\t\t\tClearKey(_languagesKey, _basePath);\n\t\t\t_languagesKey = 0;\n\t\t\twriteSettings();\n\t\t}\n\t\treturn;\n\t}\n\n\tauto size = sizeof(qint32);\n\tfor (const auto &language : list) {\n\t\tsize += Serialize::stringSize(language.id)\n\t\t\t+ Serialize::stringSize(language.pluralId)\n\t\t\t+ Serialize::stringSize(language.baseId)\n\t\t\t+ Serialize::stringSize(language.name)\n\t\t\t+ Serialize::stringSize(language.nativeName);\n\t}\n\tif (!_languagesKey) {\n\t\t_languagesKey = GenerateKey(_basePath);\n\t\twriteSettings();\n\t}\n\n\tEncryptedDescriptor data(size);\n\tdata.stream << qint32(list.size());\n\tfor (const auto &language : list) {\n\t\tdata.stream\n\t\t\t<< language.id\n\t\t\t<< language.pluralId\n\t\t\t<< language.baseId\n\t\t\t<< language.name\n\t\t\t<< language.nativeName;\n\t}\n\n\tFileWriteDescriptor file(_languagesKey, _basePath);\n\tfile.writeEncrypted(data, SettingsKey);\n}\n\nvoid pushRecentLanguage(const Lang::Language &language) {\n\tif (language.id.startsWith('#')) {\n\t\treturn;\n\t}\n\tauto list = readRecentLanguages();\n\tlist.erase(\n\t\tranges::remove_if(\n\t\t\tlist,\n\t\t\t[&](const Lang::Language &v) { return (v.id == language.id); }),\n\t\tend(list));\n\tlist.insert(list.begin(), language);\n\n\tsaveRecentLanguages(list);\n}\n\nvoid removeRecentLanguage(const QString &id) {\n\tauto list = readRecentLanguages();\n\tlist.erase(\n\t\tranges::remove_if(\n\t\t\tlist,\n\t\t\t[&](const Lang::Language &v) { return (v.id == id); }),\n\t\tend(list));\n\n\tsaveRecentLanguages(list);\n}\n\nstd::vector<Lang::Language> readRecentLanguages() {\n\tFileReadDescriptor languages;\n\tif (!_languagesKey || !ReadEncryptedFile(languages, _languagesKey, _basePath, SettingsKey)) {\n\t\treturn {};\n\t}\n\tqint32 count = 0;\n\tlanguages.stream >> count;\n\tif (count <= 0) {\n\t\treturn {};\n\t}\n\tauto result = std::vector<Lang::Language>();\n\tresult.reserve(count);\n\tfor (auto i = 0; i != count; ++i) {\n\t\tauto language = Lang::Language();\n\t\tlanguages.stream\n\t\t\t>> language.id\n\t\t\t>> language.pluralId\n\t\t\t>> language.baseId\n\t\t\t>> language.name\n\t\t\t>> language.nativeName;\n\t\tresult.push_back(language);\n\t}\n\tif (languages.stream.status() != QDataStream::Ok) {\n\t\treturn {};\n\t}\n\treturn result;\n}\n\nWindow::Theme::Object ReadThemeContent() {\n\tusing namespace Window::Theme;\n\n\tauto &themeKey = IsNightMode() ? _themeKeyNight : _themeKeyDay;\n\tif (!themeKey) {\n\t\treturn Object();\n\t}\n\n\tFileReadDescriptor theme;\n\tif (!ReadEncryptedFile(theme, themeKey, _basePath, SettingsKey)) {\n\t\treturn Object();\n\t}\n\n\tQByteArray content;\n\tQString pathRelative, pathAbsolute;\n\ttheme.stream >> content >> pathRelative >> pathAbsolute;\n\tif (theme.stream.status() != QDataStream::Ok) {\n\t\treturn Object();\n\t}\n\n\tauto result = Object();\n\tresult.pathAbsolute = pathAbsolute;\n\tresult.content = content;\n\treturn result;\n}\n\nvoid incrementRecentHashtag(RecentHashtagPack &recent, const QString &tag) {\n\tauto i = recent.begin(), e = recent.end();\n\tfor (; i != e; ++i) {\n\t\tif (i->first == tag) {\n\t\t\t++i->second;\n\t\t\tif (qAbs(i->second) > 0x4000) {\n\t\t\t\tfor (auto j = recent.begin(); j != e; ++j) {\n\t\t\t\t\tif (j->second > 1) {\n\t\t\t\t\t\tj->second /= 2;\n\t\t\t\t\t} else if (j->second > 0) {\n\t\t\t\t\t\tj->second = 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (; i != recent.begin(); --i) {\n\t\t\t\tif (qAbs((i - 1)->second) > qAbs(i->second)) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tqSwap(*i, *(i - 1));\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (i == e) {\n\t\twhile (recent.size() >= 64) recent.pop_back();\n\t\trecent.push_back(qMakePair(tag, 1));\n\t\tfor (i = recent.end() - 1; i != recent.begin(); --i) {\n\t\t\tif ((i - 1)->second > i->second) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tqSwap(*i, *(i - 1));\n\t\t}\n\t}\n}\n\nbool readOldMtpData(bool remove, ReadSettingsContext &context) {\n\treturn _readOldMtpData(remove, context);\n}\n\nbool readOldUserSettings(bool remove, ReadSettingsContext &context) {\n\treturn _readOldUserSettings(remove, context);\n}\n\n} // namespace Local\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/localstorage.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"storage/file_download.h\"\n#include \"storage/localimageloader.h\"\n\n#include <QtCore/QTimer>\n\nclass History;\n\nnamespace Data {\nclass WallPaper;\nclass DocumentMedia;\n} // namespace Data\n\nnamespace Lang {\nstruct Language;\n} // namespace Lang\n\nnamespace Storage {\nnamespace details {\nstruct ReadSettingsContext;\n} // namespace details\nclass EncryptionKey;\n} // namespace Storage\n\nnamespace Window {\nnamespace Theme {\nstruct Object;\nstruct Saved;\n} // namespace Theme\n} // namespace Window\n\nnamespace Export {\nstruct Settings;\n} // namespace Export\n\nnamespace MTP {\nclass AuthKey;\nusing AuthKeyPtr = std::shared_ptr<AuthKey>;\n} // namespace MTP\n\nnamespace Local {\n\nvoid start();\nvoid sync();\nvoid finish();\n\nvoid writeSettings();\nvoid rewriteSettingsIfNeeded();\n\nvoid writeAutoupdatePrefix(const QString &prefix);\nQString readAutoupdatePrefix();\n\nvoid writeBackground(const Data::WallPaper &paper, const QImage &image);\nbool readBackground();\nvoid moveLegacyBackground(\n\tconst QString &fromBasePath,\n\tconst MTP::AuthKeyPtr &fromLocalKey,\n\tuint64 legacyBackgroundKeyDay,\n\tuint64 legacyBackgroundKeyNight);\n\nvoid reset();\n\nint32 oldSettingsVersion();\n\nvoid countVoiceWaveform(not_null<Data::DocumentMedia*> media);\n\nvoid cancelTask(TaskId id);\n\nvoid writeTheme(const Window::Theme::Saved &saved);\nvoid clearTheme();\n[[nodiscard]] Window::Theme::Saved readThemeAfterSwitch();\n\n[[nodiscard]] Window::Theme::Object ReadThemeContent();\n\nvoid writeLangPack();\nvoid pushRecentLanguage(const Lang::Language &language);\nstd::vector<Lang::Language> readRecentLanguages();\nvoid saveRecentLanguages(const std::vector<Lang::Language> &list);\nvoid removeRecentLanguage(const QString &id);\nvoid incrementRecentHashtag(RecentHashtagPack &recent, const QString &tag);\n\nbool readOldMtpData(\n\tbool remove,\n\tStorage::details::ReadSettingsContext &context);\nbool readOldUserSettings(\n\tbool remove,\n\tStorage::details::ReadSettingsContext &context);\n\n} // namespace Local\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/serialize_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/serialize_common.h\"\n\nnamespace Serialize {\n\nByteArrayWriter::ByteArrayWriter(int expectedSize)\n: _stream(&_result, QIODevice::WriteOnly) {\n\tif (expectedSize) {\n\t\t_result.reserve(expectedSize);\n\t}\n\t_stream.setVersion(QDataStream::Qt_5_1);\n}\n\nQByteArray ByteArrayWriter::result() && {\n\t_stream.device()->close();\n\treturn std::move(_result);\n}\n\nByteArrayReader::ByteArrayReader(QByteArray data)\n: _data(std::move(data))\n, _stream(&_data, QIODevice::ReadOnly) {\n\t_stream.setVersion(QDataStream::Qt_5_1);\n}\n\nvoid writeColor(QDataStream &stream, const QColor &color) {\n\tstream << (quint32(uchar(color.red()))\n\t\t| (quint32(uchar(color.green())) << 8)\n\t\t| (quint32(uchar(color.blue())) << 16)\n\t\t| (quint32(uchar(color.alpha())) << 24));\n}\n\nQColor readColor(QDataStream &stream) {\n\tauto value = quint32();\n\tstream >> value;\n\treturn QColor(\n\t\tint(value & 0xFFU),\n\t\tint((value >> 8) & 0xFFU),\n\t\tint((value >> 16) & 0xFFU),\n\t\tint((value >> 24) & 0xFFU));\n}\n\n} // namespace Serialize\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/serialize_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/mtproto_auth_key.h\"\n#include \"base/bytes.h\"\n\n#include <QtCore/QDataStream>\n\nnamespace Serialize {\n\nclass ByteArrayWriter final {\npublic:\n\texplicit ByteArrayWriter(int expectedSize = 0);\n\n\t[[nodiscard]] QDataStream &underlying() {\n\t\treturn _stream;\n\t}\n\t[[nodiscard]] operator QDataStream &() {\n\t\treturn _stream;\n\t}\n\t[[nodiscard]] QByteArray result() &&;\n\nprivate:\n\tQByteArray _result;\n\tQDataStream _stream;\n\n};\n\ntemplate <typename T>\ninline ByteArrayWriter &operator<<(ByteArrayWriter &stream, const T &data) {\n\tstream.underlying() << data;\n\treturn stream;\n}\n\nclass ByteArrayReader final {\npublic:\n\texplicit ByteArrayReader(QByteArray data);\n\n\t[[nodiscard]] QDataStream &underlying() {\n\t\treturn _stream;\n\t}\n\t[[nodiscard]] operator QDataStream &() {\n\t\treturn _stream;\n\t}\n\n\t[[nodiscard]] bool atEnd() const {\n\t\treturn _stream.atEnd();\n\t}\n\t[[nodiscard]] bool status() const {\n\t\treturn _stream.status();\n\t}\n\t[[nodiscard]] bool ok() const {\n\t\treturn _stream.status() == QDataStream::Ok;\n\t}\n\nprivate:\n\tQByteArray _data;\n\tQDataStream _stream;\n\n};\n\ntemplate <typename T>\ninline ByteArrayReader &operator>>(ByteArrayReader &stream, T &data) {\n\tif (!stream.ok()) {\n\t\tdata = T();\n\t} else {\n\t\tstream.underlying() >> data;\n\t\tif (!stream.ok()) {\n\t\t\tdata = T();\n\t\t}\n\t}\n\treturn stream;\n}\n\ninline int stringSize(const QString &str) {\n\treturn sizeof(quint32) + str.size() * sizeof(ushort);\n}\n\ninline int bytearraySize(const QByteArray &arr) {\n\treturn sizeof(quint32) + arr.size();\n}\n\ninline int bytesSize(bytes::const_span bytes) {\n\treturn sizeof(quint32) + bytes.size();\n}\n\ninline int colorSize() {\n\treturn sizeof(quint32);\n}\n\nvoid writeColor(QDataStream &stream, const QColor &color);\nQColor readColor(QDataStream &stream);\n\nstruct ReadBytesVectorWrap {\n\tbytes::vector &bytes;\n};\n\ninline ReadBytesVectorWrap bytes(bytes::vector &bytes) {\n\treturn ReadBytesVectorWrap { bytes };\n}\n\n// Compatible with QDataStream &operator>>(QDataStream &, QByteArray &);\ninline QDataStream &operator>>(QDataStream &stream, ReadBytesVectorWrap data) {\n\tauto &bytes = data.bytes;\n\tbytes.clear();\n\tquint32 len;\n\tstream >> len;\n\tif (stream.status() != QDataStream::Ok || len == 0xFFFFFFFF) {\n\t\treturn stream;\n\t}\n\n\tconstexpr auto kStep = quint32(1024 * 1024);\n\tfor (auto allocated = quint32(0); allocated < len;) {\n\t\tauto blockSize = qMin(kStep, len - allocated);\n\t\tbytes.resize(allocated + blockSize);\n\t\tif (stream.readRawData(reinterpret_cast<char*>(bytes.data()) + allocated, blockSize) != blockSize) {\n\t\t\tbytes.clear();\n\t\t\tstream.setStatus(QDataStream::ReadPastEnd);\n\t\t\treturn stream;\n\t\t}\n\t\tallocated += blockSize;\n\t}\n\n\treturn stream;\n}\n\nstruct WriteBytesWrap {\n\tbytes::const_span bytes;\n};\n\ninline WriteBytesWrap bytes(bytes::const_span bytes) {\n\treturn WriteBytesWrap { bytes };\n}\n\ninline QDataStream &operator<<(QDataStream &stream, WriteBytesWrap data) {\n\tauto bytes = data.bytes;\n\tif (bytes.empty()) {\n\t\tstream << quint32(0xFFFFFFFF);\n\t} else {\n\t\tauto size = quint32(bytes.size());\n\t\tstream << size;\n\t\tstream.writeRawData(reinterpret_cast<const char*>(bytes.data()), size);\n\t}\n\treturn stream;\n}\n\ninline QDataStream &operator<<(QDataStream &stream, ReadBytesVectorWrap data) {\n\treturn stream << WriteBytesWrap { data.bytes };\n}\n\ninline int dateTimeSize() {\n\treturn (sizeof(qint64) + sizeof(quint32) + sizeof(qint8));\n}\n\ntemplate <typename T>\ninline T read(QDataStream &stream) {\n\tauto result = T();\n\tstream >> result;\n\treturn result;\n}\n\ntemplate <>\ninline MTP::AuthKey::Data read<MTP::AuthKey::Data>(QDataStream &stream) {\n\tauto result = MTP::AuthKey::Data();\n\tstream.readRawData(reinterpret_cast<char*>(result.data()), result.size());\n\treturn result;\n}\n\n} // namespace Serialize\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/serialize_document.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/serialize_document.h\"\n\n#include \"storage/serialize_common.h\"\n#include \"storage/serialize_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"ui/image/image.h\"\n#include \"main/main_session.h\"\n\nnamespace Serialize {\nnamespace {\n\nconstexpr auto kVersionTag = int32(0x7FFFFFFF);\nconstexpr auto kVersion = 6;\n\nenum StickerSetType {\n\tStickerSetTypeEmpty = 0,\n\tStickerSetTypeID = 1,\n\tStickerSetTypeShortName = 2,\n\tStickerSetTypeEmoji = 3,\n\tStickerSetTypeMasks = 4,\n};\n\n} // namespace\n\nvoid Document::writeToStream(QDataStream &stream, DocumentData *document) {\n\tstream\n\t\t<< quint64(document->id)\n\t\t<< quint64(document->_access)\n\t\t<< qint32(document->date)\n\t\t<< document->_fileReference\n\t\t<< qint32(kVersionTag)\n\t\t<< qint32(kVersion)\n\t\t<< document->filename()\n\t\t<< document->mimeString()\n\t\t<< qint32(document->_dc)\n\t\t// FileSize: Right now any file size fits 32 bit.\n\t\t<< qint32(uint32(document->size))\n\t\t<< qint32(document->dimensions.width())\n\t\t<< qint32(document->dimensions.height())\n\t\t<< qint32(document->type);\n\tif (const auto sticker = document->sticker()) {\n\t\tstream << sticker->alt;\n\t\tif (sticker->setType == Data::StickersType::Emoji) {\n\t\t\tstream << qint32(StickerSetTypeEmoji);\n\t\t} else if (sticker->setType == Data::StickersType::Masks) {\n\t\t\tstream << qint32(StickerSetTypeMasks);\n\t\t} else if (sticker->set.id) {\n\t\t\tstream << qint32(StickerSetTypeID);\n\t\t} else {\n\t\t\tstream << qint32(StickerSetTypeEmpty);\n\t\t}\n\t}\n\tstream << qint64(document->hasDuration() ? document->duration() : -1);\n\tif (document->type == StickerDocument) {\n\t\tconst auto premium = document->isPremiumSticker()\n\t\t\t|| document->isPremiumEmoji();\n\t\tstream << qint32(premium ? 1 : 0);\n\t\tstream << qint32(document->emojiUsesTextColor() ? 1 : 0);\n\t}\n\twriteImageLocation(stream, document->thumbnailLocation());\n\tstream << qint32(document->thumbnailByteSize());\n\twriteImageLocation(stream, document->videoThumbnailLocation());\n\tstream\n\t\t<< qint32(document->videoThumbnailByteSize())\n\t\t<< qint32(document->inlineThumbnailIsPath() ? 1 : 0)\n\t\t<< document->inlineThumbnailBytes();\n}\n\nDocumentData *Document::readFromStreamHelper(\n\t\tnot_null<Main::Session*> session,\n\t\tint streamAppVersion,\n\t\tQDataStream &stream,\n\t\tconst StickerSetInfo *info) {\n\tquint64 id, access;\n\tQString name, mime;\n\tqint32 date, dc, size, width, height, type, versionTag, version = 0;\n\tQByteArray fileReference;\n\tstream >> id >> access >> date;\n\tif (streamAppVersion >= 9061) {\n\t\tif (streamAppVersion >= 1003013) {\n\t\t\tstream >> fileReference;\n\t\t}\n\t\tstream >> versionTag;\n\t\tif (versionTag == kVersionTag) {\n\t\t\tstream >> version;\n\t\t}\n\t} else {\n\t\tversionTag = 0;\n\t\tversion = 0;\n\t}\n\tstream\n\t\t>> name\n\t\t>> mime\n\t\t>> dc\n\t\t>> size // FileSize: Right now any file size fits 32 bit.\n\t\t>> width\n\t\t>> height\n\t\t>> type;\n\n\tQVector<MTPDocumentAttribute> attributes;\n\tif (!name.isEmpty()) {\n\t\tattributes.push_back(MTP_documentAttributeFilename(MTP_string(name)));\n\t}\n\n\tqint64 duration = -1;\n\tqint32 isPremiumSticker = 0;\n\tqint32 useTextColor = 0;\n\tif (type == StickerDocument) {\n\t\tQString alt;\n\t\tqint32 typeOfSet;\n\t\tstream >> alt >> typeOfSet;\n\t\tif (version >= 6) {\n\t\t\tstream >> duration >> isPremiumSticker >> useTextColor;\n\t\t} else if (version >= 3) {\n\t\t\tqint32 oldDuration = -1;\n\t\t\tstream >> oldDuration;\n\t\t\tduration = (oldDuration < 0) ? oldDuration : oldDuration * 1000;\n\t\t\tif (version >= 4) {\n\t\t\t\tstream >> isPremiumSticker;\n\t\t\t\tif (version >= 5) {\n\t\t\t\t\tstream >> useTextColor;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (typeOfSet == StickerSetTypeEmpty) {\n\t\t\tattributes.push_back(MTP_documentAttributeSticker(MTP_flags(0), MTP_string(alt), MTP_inputStickerSetEmpty(), MTPMaskCoords()));\n\t\t} else if (info) {\n\t\t\tif (info->setId == Data::Stickers::DefaultSetId\n\t\t\t\t|| info->setId == Data::Stickers::CloudRecentSetId\n\t\t\t\t|| info->setId == Data::Stickers::CloudRecentAttachedSetId\n\t\t\t\t|| info->setId == Data::Stickers::FavedSetId\n\t\t\t\t|| info->setId == Data::Stickers::CustomSetId\n\t\t\t\t|| info->setId == Data::Stickers::CollectibleSetId) {\n\t\t\t\ttypeOfSet = StickerSetTypeEmpty;\n\t\t\t}\n\n\t\t\tswitch (typeOfSet) {\n\t\t\tcase StickerSetTypeID: {\n\t\t\t\tattributes.push_back(MTP_documentAttributeSticker(MTP_flags(0), MTP_string(alt), MTP_inputStickerSetID(MTP_long(info->setId), MTP_long(info->accessHash)), MTPMaskCoords()));\n\t\t\t} break;\n\t\t\tcase StickerSetTypeMasks: {\n\t\t\t\tattributes.push_back(MTP_documentAttributeSticker(MTP_flags(MTPDdocumentAttributeSticker::Flag::f_mask), MTP_string(alt), MTP_inputStickerSetID(MTP_long(info->setId), MTP_long(info->accessHash)), MTPMaskCoords()));\n\t\t\t} break;\n\t\t\tcase StickerSetTypeEmoji: {\n\t\t\t\tif (version < 5) {\n\t\t\t\t\t// We didn't store useTextColor yet, can't use.\n\t\t\t\t\tstream.setStatus(QDataStream::ReadCorruptData);\n\t\t\t\t\treturn nullptr;\n\t\t\t\t}\n\t\t\t\tusing Flag = MTPDdocumentAttributeCustomEmoji::Flag;\n\t\t\t\tattributes.push_back(MTP_documentAttributeCustomEmoji(\n\t\t\t\t\tMTP_flags((isPremiumSticker ? Flag(0) : Flag::f_free)\n\t\t\t\t\t\t| (useTextColor ? Flag::f_text_color : Flag(0))),\n\t\t\t\t\tMTP_string(alt),\n\t\t\t\t\tMTP_inputStickerSetID(\n\t\t\t\t\t\tMTP_long(info->setId),\n\t\t\t\t\t\tMTP_long(info->accessHash))));\n\t\t\t} break;\n\t\t\tcase StickerSetTypeEmpty:\n\t\t\tdefault: {\n\t\t\t\tattributes.push_back(MTP_documentAttributeSticker(MTP_flags(0), MTP_string(alt), MTP_inputStickerSetEmpty(), MTPMaskCoords()));\n\t\t\t} break;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (version >= 6) {\n\t\t\tstream >> duration;\n\t\t} else {\n\t\t\tqint32 oldDuration = -1;\n\t\t\tstream >> oldDuration;\n\t\t\tduration = (oldDuration < 0) ? oldDuration : oldDuration * 1000;\n\t\t}\n\t\tif (type == AnimatedDocument) {\n\t\t\tattributes.push_back(MTP_documentAttributeAnimated());\n\t\t}\n\t}\n\tstd::optional<ImageLocation> videoThumb;\n\tqint32 thumbnailByteSize = 0, videoThumbnailByteSize = 0;\n\tqint32 inlineThumbnailIsPath = 0;\n\tQByteArray inlineThumbnailBytes;\n\tconst auto thumb = readImageLocation(streamAppVersion, stream);\n\tif (version >= 1) {\n\t\tstream >> thumbnailByteSize;\n\t\tvideoThumb = readImageLocation(streamAppVersion, stream);\n\t\tstream >> videoThumbnailByteSize;\n\t\tif (version >= 2) {\n\t\t\tstream >> inlineThumbnailIsPath >> inlineThumbnailBytes;\n\t\t}\n\t} else {\n\t\tvideoThumb = ImageLocation();\n\t}\n\tif (width > 0 && height > 0) {\n\t\tif (duration >= 0) {\n\t\t\tauto flags = MTPDdocumentAttributeVideo::Flags(0);\n\t\t\tif (type == RoundVideoDocument) {\n\t\t\t\tflags |= MTPDdocumentAttributeVideo::Flag::f_round_message;\n\t\t\t}\n\t\t\tattributes.push_back(MTP_documentAttributeVideo(\n\t\t\t\tMTP_flags(flags),\n\t\t\t\tMTP_double(duration / 1000.),\n\t\t\t\tMTP_int(width),\n\t\t\t\tMTP_int(height),\n\t\t\t\tMTPint(), // preload_prefix_size\n\t\t\t\tMTPdouble(), // video_start_ts\n\t\t\t\tMTPstring())); // video_codec\n\t\t} else {\n\t\t\tattributes.push_back(MTP_documentAttributeImageSize(\n\t\t\t\tMTP_int(width),\n\t\t\t\tMTP_int(height)));\n\t\t}\n\t}\n\n\tif ((stream.status() != QDataStream::Ok)\n\t\t|| (!dc && !access)\n\t\t|| !thumb\n\t\t|| !videoThumb) {\n\t\tstream.setStatus(QDataStream::ReadCorruptData);\n\t\treturn nullptr;\n\t}\n\tconst auto storage = std::get_if<StorageFileLocation>(\n\t\t&thumb->file().data);\n\tif (thumb->valid()\n\t\t&& (!storage || !storage->isDocumentThumbnail())) {\n\t\tstream.setStatus(QDataStream::ReadCorruptData);\n\t\t// We can't convert legacy thumbnail location to modern, because\n\t\t// size letter ('s' or 'm') is lost, it was not saved in legacy.\n\t\treturn nullptr;\n\t}\n\treturn session->data().document(\n\t\tid,\n\t\taccess,\n\t\tfileReference,\n\t\tdate,\n\t\tattributes,\n\t\tmime,\n\t\tInlineImageLocation{\n\t\t\tinlineThumbnailBytes,\n\t\t\t(inlineThumbnailIsPath == 1),\n\t\t},\n\t\tImageWithLocation{\n\t\t\t.location = *thumb,\n\t\t\t.bytesCount = thumbnailByteSize\n\t\t},\n\t\tImageWithLocation{\n\t\t\t.location = *videoThumb,\n\t\t\t.bytesCount = videoThumbnailByteSize\n\t\t},\n\t\t(isPremiumSticker == 1),\n\t\tdc,\n\t\tint64(uint32(size)));\n}\n\nDocumentData *Document::readStickerFromStream(\n\t\tnot_null<Main::Session*> session,\n\t\tint streamAppVersion,\n\t\tQDataStream &stream,\n\t\tconst StickerSetInfo &info) {\n\treturn readFromStreamHelper(session, streamAppVersion, stream, &info);\n}\n\nDocumentData *Document::readFromStream(\n\t\tnot_null<Main::Session*> session,\n\t\tint streamAppVersion,\n\t\tQDataStream &stream) {\n\treturn readFromStreamHelper(session, streamAppVersion, stream, nullptr);\n}\n\nint Document::sizeInStream(DocumentData *document) {\n\tint result = 0;\n\n\t// id + access + date\n\tresult += sizeof(quint64) + sizeof(quint64) + sizeof(qint32);\n\t// file_reference + version tag + version\n\tresult += bytearraySize(document->_fileReference) + sizeof(qint32) * 2;\n\t// + namelen + name + mimelen + mime + dc + size\n\tresult += stringSize(document->filename()) + stringSize(document->mimeString()) + sizeof(qint32) + sizeof(qint32);\n\t// + width + height\n\tresult += sizeof(qint32) + sizeof(qint32);\n\t// + type\n\tresult += sizeof(qint32);\n\n\tif (auto sticker = document->sticker()) { // type == StickerDocument\n\t\t// + altlen + alt + type-of-set\n\t\tresult += stringSize(sticker->alt) + sizeof(qint32);\n\t} else {\n\t\t// + duration\n\t\tresult += sizeof(qint32);\n\t}\n\t// + thumb loc\n\tresult += Serialize::imageLocationSize(document->thumbnailLocation());\n\tresult += sizeof(qint32); // thumbnail_byte_size\n\tresult += Serialize::imageLocationSize(document->videoThumbnailLocation());\n\tresult += sizeof(qint32); // video_thumbnail_byte_size\n\n\tresult += sizeof(qint32) + Serialize::bytearraySize(document->inlineThumbnailBytes());\n\n\treturn result;\n}\n\n} // namespace Serialize\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/serialize_document.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_document.h\"\n\nnamespace Serialize {\n\nclass Document {\npublic:\n\tstruct StickerSetInfo {\n\t\tStickerSetInfo(uint64 setId, uint64 accessHash, QString shortName)\n\t\t\t: setId(setId)\n\t\t\t, accessHash(accessHash)\n\t\t\t, shortName(shortName) {\n\t\t}\n\t\tuint64 setId;\n\t\tuint64 accessHash;\n\t\tQString shortName;\n\t};\n\n\tstatic void writeToStream(QDataStream &stream, DocumentData *document);\n\tstatic DocumentData *readStickerFromStream(\n\t\tnot_null<Main::Session*> session,\n\t\tint streamAppVersion,\n\t\tQDataStream &stream,\n\t\tconst StickerSetInfo &info);\n\tstatic DocumentData *readFromStream(\n\t\tnot_null<Main::Session*> session,\n\t\tint streamAppVersion,\n\t\tQDataStream &stream);\n\tstatic int sizeInStream(DocumentData *document);\n\nprivate:\n\tstatic DocumentData *readFromStreamHelper(\n\t\tnot_null<Main::Session*> session,\n\t\tint streamAppVersion,\n\t\tQDataStream &stream,\n\t\tconst StickerSetInfo *info);\n\n};\n\n} // namespace Serialize\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/serialize_peer.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/serialize_peer.h\"\n\n#include \"storage/serialize_common.h\"\n#include \"main/main_session.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"ui/image/image.h\"\n#include \"ui/text/format_values.h\" // Ui::FormatPhone\n\nnamespace Serialize {\nnamespace {\n\nconstexpr auto kModernImageLocationTag = std::numeric_limits<qint32>::min();\nconstexpr auto kVersionTag = uint64(0x77FF'FFFF'FFFF'FFFFULL);\nconstexpr auto kVersion = 2;\n\n} // namespace\n\nstd::optional<StorageImageLocation> readLegacyStorageImageLocationOrTag(\n\t\tint streamAppVersion,\n\t\tQDataStream &stream) {\n\tqint32 width, height, dc, local;\n\tquint64 volume, secret;\n\tQByteArray fileReference;\n\tstream >> width;\n\tif (width == kModernImageLocationTag) {\n\t\treturn std::nullopt;\n\t}\n\tstream >> height >> dc >> volume >> local >> secret;\n\tif (streamAppVersion >= 1003013) {\n\t\tstream >> fileReference;\n\t}\n\tif (stream.status() != QDataStream::Ok) {\n\t\treturn std::nullopt;\n\t}\n\treturn StorageImageLocation(\n\t\tStorageFileLocation(\n\t\t\tdc,\n\t\t\tUserId(0), // self\n\t\t\tMTP_inputFileLocation(\n\t\t\t\tMTP_long(volume),\n\t\t\t\tMTP_int(local),\n\t\t\t\tMTP_long(secret),\n\t\t\t\tMTP_bytes(fileReference))),\n\t\twidth,\n\t\theight);\n}\n\nint storageImageLocationSize(const StorageImageLocation &location) {\n\t// Modern image location tag + (size + content) of the serialization.\n\treturn sizeof(qint32) * 2 + location.serializeSize();\n}\n\nvoid writeStorageImageLocation(\n\t\tQDataStream &stream,\n\t\tconst StorageImageLocation &location) {\n\tstream << kModernImageLocationTag << location.serialize();\n}\n\nstd::optional<StorageImageLocation> readStorageImageLocation(\n\t\tint streamAppVersion,\n\t\tQDataStream &stream) {\n\tconst auto legacy = readLegacyStorageImageLocationOrTag(\n\t\tstreamAppVersion,\n\t\tstream);\n\tif (legacy) {\n\t\treturn legacy;\n\t}\n\tauto serialized = QByteArray();\n\tstream >> serialized;\n\treturn (stream.status() == QDataStream::Ok)\n\t\t? StorageImageLocation::FromSerialized(serialized)\n\t\t: std::nullopt;\n}\n\nint imageLocationSize(const ImageLocation &location) {\n\t// Modern image location tag + (size + content) of the serialization.\n\treturn sizeof(qint32) * 2 + location.serializeSize();\n}\n\nvoid writeImageLocation(QDataStream &stream, const ImageLocation &location) {\n\tstream << kModernImageLocationTag << location.serialize();\n}\n\nstd::optional<ImageLocation> readImageLocation(\n\t\tint streamAppVersion,\n\t\tQDataStream &stream) {\n\tconst auto legacy = readLegacyStorageImageLocationOrTag(\n\t\tstreamAppVersion,\n\t\tstream);\n\tif (legacy) {\n\t\treturn ImageLocation(\n\t\t\tDownloadLocation{ legacy->file() },\n\t\t\tlegacy->width(),\n\t\t\tlegacy->height());\n\t}\n\tauto serialized = QByteArray();\n\tstream >> serialized;\n\treturn (stream.status() == QDataStream::Ok)\n\t\t? ImageLocation::FromSerialized(serialized)\n\t\t: std::nullopt;\n}\n\nuint32 peerSize(not_null<PeerData*> peer) {\n\tuint32 result = sizeof(quint64) // id\n\t\t+ sizeof(quint64) // version tag\n\t\t+ sizeof(qint32) // version\n\t\t+ sizeof(quint64) // userpic photo id\n\t\t+ imageLocationSize(peer->userpicLocation())\n\t\t+ sizeof(qint32); // userpic has video\n\tif (const auto user = peer->asUser()) {\n\t\tconst auto botInlinePlaceholder = user->isBot()\n\t\t\t? user->botInfo->inlinePlaceholder\n\t\t\t: QString();\n\t\tresult += stringSize(user->firstName)\n\t\t\t+ stringSize(user->lastName)\n\t\t\t+ stringSize(user->phone())\n\t\t\t+ stringSize(user->username())\n\t\t\t+ sizeof(quint64) // access\n\t\t\t+ sizeof(qint32) // flags\n\t\t\t+ stringSize(botInlinePlaceholder)\n\t\t\t+ sizeof(quint32) // lastseen\n\t\t\t+ sizeof(qint32) // contact\n\t\t\t+ sizeof(qint32); // botInfoVersion\n\t} else if (const auto chat = peer->asChat()) {\n\t\tresult += stringSize(chat->name())\n\t\t\t+ sizeof(qint32) // count\n\t\t\t+ sizeof(qint32) // date\n\t\t\t+ sizeof(qint32) // version\n\t\t\t+ sizeof(qint32) // creator id 1\n\t\t\t+ sizeof(qint32) // creator id 2\n\t\t\t+ sizeof(quint32) // flags\n\t\t\t+ stringSize(chat->inviteLink());\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tresult += stringSize(channel->name())\n\t\t\t+ sizeof(quint64) // access\n\t\t\t+ sizeof(qint32) // date\n\t\t\t+ sizeof(qint32) // version\n\t\t\t+ sizeof(qint32) // old forbidden\n\t\t\t+ sizeof(quint32) // flags\n\t\t\t+ stringSize(channel->inviteLink());\n\t}\n\treturn result;\n}\n\nvoid writePeer(QDataStream &stream, not_null<PeerData*> peer) {\n\tstream\n\t\t<< SerializePeerId(peer->id)\n\t\t<< quint64(kVersionTag)\n\t\t<< qint32(kVersion)\n\t\t<< quint64(peer->userpicPhotoId());\n\twriteImageLocation(stream, peer->userpicLocation());\n\tstream << qint32(peer->userpicHasVideo() ? 1 : 0);\n\tif (const auto user = peer->asUser()) {\n\t\tconst auto botInlinePlaceholder = user->isBot()\n\t\t\t? user->botInfo->inlinePlaceholder\n\t\t\t: QString();\n\t\tstream\n\t\t\t<< user->firstName\n\t\t\t<< user->lastName\n\t\t\t<< user->phone()\n\t\t\t<< user->username()\n\t\t\t<< quint64(user->accessHash())\n\t\t\t<< qint32(user->flags())\n\t\t\t<< botInlinePlaceholder\n\t\t\t<< quint32(user->lastseen().serialize())\n\t\t\t<< qint32(user->isContact() ? 1 : 0)\n\t\t\t<< qint32(user->isBot() ? user->botInfo->version : -1);\n\t} else if (const auto chat = peer->asChat()) {\n\t\tauto field1 = qint32(uint32(chat->creator.bare & 0xFFFFFFFFULL));\n\t\tauto field2 = qint32(uint32(chat->creator.bare >> 32) << 8);\n\t\tstream\n\t\t\t<< chat->name()\n\t\t\t<< qint32(chat->count)\n\t\t\t<< qint32(chat->date)\n\t\t\t<< qint32(chat->version())\n\t\t\t<< field1\n\t\t\t<< field2\n\t\t\t<< quint32(chat->flags())\n\t\t\t<< chat->inviteLink();\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tstream\n\t\t\t<< channel->name()\n\t\t\t<< quint64(channel->accessHash())\n\t\t\t<< qint32(channel->date)\n\t\t\t<< qint32(0) // legacy - version\n\t\t\t<< qint32(0)\n\t\t\t<< quint32(channel->flags())\n\t\t\t<< channel->inviteLink();\n\t}\n}\n\nPeerData *readPeer(\n\t\tnot_null<Main::Session*> session,\n\t\tint streamAppVersion,\n\t\tQDataStream &stream) {\n\tquint64 peerIdSerialized = 0, versionTag = 0, photoId = 0;\n\tqint32 version = 0, photoHasVideo = 0;\n\tstream >> peerIdSerialized >> versionTag;\n\tconst auto peerId = DeserializePeerId(peerIdSerialized);\n\tif (!peerId) {\n\t\treturn nullptr;\n\t}\n\tif (versionTag == kVersionTag) {\n\t\tstream >> version >> photoId;\n\t} else {\n\t\tphotoId = versionTag;\n\t}\n\n\tconst auto userpic = readImageLocation(streamAppVersion, stream);\n\tauto userpicAccessHash = uint64(0);\n\tif (!userpic) {\n\t\treturn nullptr;\n\t}\n\tif (version > 0) {\n\t\tstream >> photoHasVideo;\n\t}\n\n\tconst auto selfId = session->userPeerId();\n\tconst auto loaded = (peerId == selfId)\n\t\t? session->user().get()\n\t\t: session->data().peerLoaded(peerId);\n\tconst auto apply = !loaded || !loaded->isLoaded();\n\tconst auto result = loaded ? loaded : session->data().peer(peerId).get();\n\tif (apply) {\n\t\tresult->setLoadedStatus(PeerData::LoadedStatus::Normal);\n\t}\n\tif (const auto user = result->asUser()) {\n\t\tQString first, last, phone, username, inlinePlaceholder;\n\t\tquint64 access;\n\t\tqint32 flags = 0, contact, botInfoVersion;\n\t\tquint32 lastseen;\n\t\tstream >> first >> last >> phone >> username >> access;\n\t\tif (streamAppVersion >= 9012) {\n\t\t\tstream >> flags;\n\t\t}\n\t\tif (streamAppVersion >= 9016) {\n\t\t\tstream >> inlinePlaceholder;\n\t\t}\n\t\tstream >> lastseen >> contact >> botInfoVersion;\n\n\t\tuserpicAccessHash = access;\n\n\t\tif (apply) {\n\t\t\tconst auto showPhone = !user->isServiceUser()\n\t\t\t\t&& (user->id != selfId)\n\t\t\t\t&& (contact <= 0);\n\t\t\tconst auto pname = (showPhone && !phone.isEmpty())\n\t\t\t\t? Ui::FormatPhone(phone)\n\t\t\t\t: QString();\n\n\t\t\tuser->setPhone(phone);\n\t\t\tuser->setName(first, last, pname, username);\n\t\t\tif (streamAppVersion >= 2008007) {\n\t\t\t\tuser->setFlags(UserDataFlags::from_raw(flags));\n\t\t\t} else {\n\t\t\t\tusing Saved = MTPDuser::Flag;\n\t\t\t\tusing Flag = UserDataFlag;\n\t\t\t\tstruct Conversion {\n\t\t\t\t\tSaved saved;\n\t\t\t\t\tFlag flag;\n\t\t\t\t};\n\t\t\t\tconst auto conversions = {\n\t\t\t\t\tConversion{ Saved::f_deleted, Flag::Deleted },\n\t\t\t\t\tConversion{ Saved::f_verified, Flag::Verified },\n\t\t\t\t\tConversion{ Saved::f_scam, Flag::Scam },\n\t\t\t\t\tConversion{ Saved::f_fake, Flag::Fake },\n\t\t\t\t\tConversion{ Saved::f_bot_inline_geo, Flag::BotInlineGeo },\n\t\t\t\t\tConversion{ Saved::f_support, Flag::Support },\n\t\t\t\t\tConversion{ Saved::f_contact, Flag::Contact },\n\t\t\t\t\tConversion{ Saved::f_mutual_contact, Flag::MutualContact },\n\t\t\t\t};\n\t\t\t\tauto flagsMask = Flag() | Flag();\n\t\t\t\tauto flagsSet = Flag() | Flag();\n\t\t\t\tfor (const auto &conversion : conversions) {\n\t\t\t\t\tflagsMask |= conversion.flag;\n\t\t\t\t\tif (flags & int(conversion.saved)) {\n\t\t\t\t\t\tflagsSet |= conversion.flag;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tuser->setFlags((user->flags() & ~flagsMask) | flagsSet);\n\t\t\t}\n\t\t\tuser->setAccessHash(access);\n\t\t\tuser->updateLastseen((version > 1)\n\t\t\t\t? Data::LastseenStatus::FromSerialized(lastseen)\n\t\t\t\t: Data::LastseenStatus::FromLegacy(lastseen));\n\t\t\tuser->setIsContact(contact == 1);\n\t\t\tuser->setBotInfoVersion(botInfoVersion);\n\t\t\tif (!inlinePlaceholder.isEmpty() && user->isBot()) {\n\t\t\t\tuser->botInfo->inlinePlaceholder = inlinePlaceholder;\n\t\t\t}\n\t\t}\n\t} else if (const auto chat = result->asChat()) {\n\t\tQString name, inviteLink;\n\t\tqint32 count, date, version, field1, field2;\n\t\tquint32 flags;\n\t\tstream\n\t\t\t>> name\n\t\t\t>> count\n\t\t\t>> date\n\t\t\t>> version\n\t\t\t>> field1\n\t\t\t>> field2\n\t\t\t>> flags\n\t\t\t>> inviteLink;\n\t\tif (apply) {\n\t\t\tconst auto creator = UserId(\n\t\t\t\tBareId(uint32(field1)) | (BareId(uint32(field2) >> 8) << 32));\n\t\t\tchat->setName(name);\n\t\t\tchat->count = count;\n\t\t\tchat->date = date;\n\n\t\t\t// We don't save participants, admin status and banned rights.\n\t\t\t// So we don't restore the version field, info is still unknown.\n\t\t\tchat->setVersion(0);\n\n\t\t\tif (streamAppVersion >= 2008007) {\n\t\t\t\tchat->setFlags(ChatDataFlags::from_raw(flags));\n\t\t\t} else {\n\t\t\t\tconst auto oldForbidden = ((uint32(field2) & 0xFF) == 1);\n\n\t\t\t\tusing Saved = MTPDchat::Flag;\n\t\t\t\tusing Flag = ChatDataFlag;\n\t\t\t\tstruct Conversion {\n\t\t\t\t\tSaved saved;\n\t\t\t\t\tFlag flag;\n\t\t\t\t};\n\t\t\t\tconst auto conversions = {\n\t\t\t\t\tConversion{ Saved::f_left, Flag::Left },\n\t\t\t\t\tConversion{ Saved::f_creator, Flag::Creator },\n\t\t\t\t\tConversion{ Saved::f_deactivated, Flag::Deactivated },\n\t\t\t\t\tConversion{ Saved(1U << 31), Flag::Forbidden },\n\t\t\t\t\tConversion{ Saved::f_call_active, Flag::CallActive },\n\t\t\t\t\tConversion{ Saved::f_call_not_empty, Flag::CallNotEmpty },\n\t\t\t\t};\n\t\t\t\tauto flagsMask = Flag() | Flag();\n\t\t\t\tauto flagsSet = Flag() | Flag();\n\t\t\t\tif (streamAppVersion >= 9012) {\n\t\t\t\t\tfor (const auto &conversion : conversions) {\n\t\t\t\t\t\tflagsMask |= conversion.flag;\n\t\t\t\t\t\tif (flags & int(conversion.saved)) {\n\t\t\t\t\t\t\tflagsSet |= conversion.flag;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// flags was haveLeft\n\t\t\t\t\tif (flags == 1) {\n\t\t\t\t\t\tflagsSet |= Flag::Left;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (oldForbidden) {\n\t\t\t\t\tflagsSet |= Flag::Forbidden;\n\t\t\t\t}\n\t\t\t\tchat->setFlags((chat->flags() & ~flagsMask) | flagsSet);\n\t\t\t}\n\n\t\t\tchat->creator = creator;\n\t\t\tchat->setInviteLink(inviteLink);\n\t\t}\n\t} else if (const auto channel = result->asChannel()) {\n\t\tQString name, inviteLink;\n\t\tquint64 access;\n\t\tqint32 date, version, oldForbidden;\n\t\tquint32 flags;\n\t\tstream\n\t\t\t>> name\n\t\t\t>> access\n\t\t\t>> date\n\t\t\t>> version\n\t\t\t>> oldForbidden\n\t\t\t>> flags\n\t\t\t>> inviteLink;\n\n\t\tuserpicAccessHash = access;\n\n\t\tif (apply) {\n\t\t\tchannel->setName(name, QString());\n\t\t\tchannel->setAccessHash(access);\n\t\t\tchannel->date = date;\n\n\t\t\tif (streamAppVersion >= 2008007) {\n\t\t\t\tchannel->setFlags(ChannelDataFlags::from_raw(flags));\n\t\t\t} else {\n\t\t\t\tusing Saved = MTPDchannel::Flag;\n\t\t\t\tusing Flag = ChannelDataFlag;\n\t\t\t\tstruct Conversion {\n\t\t\t\t\tSaved saved;\n\t\t\t\t\tFlag flag;\n\t\t\t\t};\n\t\t\t\tconst auto conversions = {\n\t\t\t\t\tConversion{ Saved::f_broadcast, Flag::Broadcast },\n\t\t\t\t\tConversion{ Saved::f_verified, Flag::Verified},\n\t\t\t\t\tConversion{ Saved::f_scam, Flag::Scam},\n\t\t\t\t\tConversion{ Saved::f_fake, Flag::Fake},\n\t\t\t\t\tConversion{ Saved::f_megagroup, Flag::Megagroup},\n\t\t\t\t\tConversion{ Saved::f_gigagroup, Flag::Gigagroup},\n\t\t\t\t\tConversion{ Saved::f_username, Flag::Username},\n\t\t\t\t\tConversion{ Saved::f_signatures, Flag::Signatures},\n\t\t\t\t\tConversion{ Saved::f_has_link, Flag::HasLink},\n\t\t\t\t\tConversion{\n\t\t\t\t\t\tSaved::f_slowmode_enabled,\n\t\t\t\t\t\tFlag::SlowmodeEnabled },\n\t\t\t\t\tConversion{ Saved::f_call_active, Flag::CallActive },\n\t\t\t\t\tConversion{ Saved::f_call_not_empty, Flag::CallNotEmpty },\n\t\t\t\t\tConversion{ Saved(1U << 31), Flag::Forbidden },\n\t\t\t\t\tConversion{ Saved::f_left, Flag::Left },\n\t\t\t\t\tConversion{ Saved::f_creator, Flag::Creator },\n\t\t\t\t};\n\t\t\t\tauto flagsMask = Flag() | Flag();\n\t\t\t\tauto flagsSet = Flag() | Flag();\n\t\t\t\tfor (const auto &conversion : conversions) {\n\t\t\t\t\tflagsMask |= conversion.flag;\n\t\t\t\t\tif (flags & int(conversion.saved)) {\n\t\t\t\t\t\tflagsSet |= conversion.flag;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (oldForbidden) {\n\t\t\t\t\tflagsSet |= Flag::Forbidden;\n\t\t\t\t}\n\t\t\t\tchannel->setFlags((channel->flags() & ~flagsMask) | flagsSet);\n\t\t\t}\n\n\t\t\tchannel->setInviteLink(inviteLink);\n\t\t}\n\t}\n\tif (apply) {\n\t\tconst auto location = userpic->convertToModernPeerPhoto(\n\t\t\tresult->id.value,\n\t\t\tuserpicAccessHash,\n\t\t\tphotoId);\n\t\tresult->setUserpic(photoId, location, (photoHasVideo == 1));\n\t}\n\treturn result;\n}\n\nQString peekUserPhone(int streamAppVersion, QDataStream &stream) {\n\tquint64 peerIdSerialized = 0, versionTag = 0, photoId = 0;\n\tqint32 version = 0, photoHasVideo = 0;\n\tstream >> peerIdSerialized >> versionTag;\n\tconst auto peerId = DeserializePeerId(peerIdSerialized);\n\tDEBUG_LOG((\"peekUserPhone.id: %1\").arg(peerId.value));\n\tif (!peerId || !peerIsUser(peerId)) {\n\t\treturn nullptr;\n\t}\n\tif (versionTag == kVersionTag) {\n\t\tstream >> version >> photoId;\n\t} else {\n\t\tphotoId = versionTag;\n\t}\n\n\tif (!readImageLocation(streamAppVersion, stream)) {\n\t\treturn nullptr;\n\t}\n\tif (version > 0) {\n\t\tstream >> photoHasVideo;\n\t}\n\n\tQString first, last, phone;\n\tstream >> first >> last >> phone;\n\tDEBUG_LOG((\"peekUserPhone.data: %1 %2 %3\"\n\t\t).arg(first, last, phone));\n\treturn phone;\n}\n\n} // namespace Serialize\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/serialize_peer.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Serialize {\n\nint storageImageLocationSize(const StorageImageLocation &location);\nvoid writeStorageImageLocation(\n\tQDataStream &stream,\n\tconst StorageImageLocation &location);\n\n// NB! This method can return StorageFileLocation with Type::Generic!\n// The reader should discard it or convert to one of the valid modern types.\nstd::optional<StorageImageLocation> readStorageImageLocation(\n\tint streamAppVersion,\n\tQDataStream &stream);\n\nint imageLocationSize(const ImageLocation &location);\nvoid writeImageLocation(QDataStream &stream, const ImageLocation &location);\n\n// NB! This method can return StorageFileLocation with Type::Generic!\n// The reader should discard it or convert to one of the valid modern types.\nstd::optional<ImageLocation> readImageLocation(\n\tint streamAppVersion,\n\tQDataStream &stream);\n\nuint32 peerSize(not_null<PeerData*> peer);\nvoid writePeer(QDataStream &stream, not_null<PeerData*> peer);\nPeerData *readPeer(\n\tnot_null<Main::Session*> session,\n\tint streamAppVersion,\n\tQDataStream &stream);\nQString peekUserPhone(int streamAppVersion, QDataStream &stream);\n\n} // namespace Serialize\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_account.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/storage_account.h\"\n\n#include \"storage/localstorage.h\"\n#include \"storage/storage_domain.h\"\n#include \"storage/storage_encryption.h\"\n#include \"storage/storage_clear_legacy.h\"\n#include \"storage/cache/storage_cache_types.h\"\n#include \"storage/details/storage_file_utilities.h\"\n#include \"storage/details/storage_settings_scheme.h\"\n#include \"storage/serialize_common.h\"\n#include \"storage/serialize_peer.h\"\n#include \"storage/serialize_document.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"mtproto/mtproto_dc_options.h\"\n#include \"mtproto/mtp_instance.h\"\n#include \"lang/lang_keys.h\"\n#include \"history/history.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/file_location.h\"\n#include \"data/components/recent_peers.h\"\n#include \"settings/settings_recent_searches.h\"\n#include \"data/components/top_peers.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_user.h\"\n#include \"data/data_drafts.h\"\n#include \"export/export_settings.h\"\n#include \"webview/webview_interface.h\"\n#include \"window/themes/window_theme.h\"\n\nnamespace Storage {\nnamespace {\n\nusing namespace details;\nusing Database = Cache::Database;\n\nconstexpr auto kDelayedWriteTimeout = crl::time(1000);\nconstexpr auto kWriteSearchSuggestionsDelay = 5 * crl::time(1000);\nconstexpr auto kMaxSavedPlaybackPositions = 256;\n\nconstexpr auto kStickersVersionTag = quint32(-1);\nconstexpr auto kStickersSerializeVersion = 4;\nconstexpr auto kMaxSavedStickerSetsCount = 1000;\nconstexpr auto kDefaultStickerInstallDate = TimeId(1);\n\nconstexpr auto kSinglePeerTypeUserOld = qint32(1);\nconstexpr auto kSinglePeerTypeChatOld = qint32(2);\nconstexpr auto kSinglePeerTypeChannelOld = qint32(3);\nconstexpr auto kSinglePeerTypeUser = qint32(8 + 1);\nconstexpr auto kSinglePeerTypeChat = qint32(8 + 2);\nconstexpr auto kSinglePeerTypeChannel = qint32(8 + 3);\nconstexpr auto kSinglePeerTypeSelf = qint32(4);\nconstexpr auto kSinglePeerTypeEmpty = qint32(0);\nconstexpr auto kMultiDraftTagOld = quint64(0xFFFF'FFFF'FFFF'FF01ULL);\nconstexpr auto kMultiDraftCursorsTagOld = quint64(0xFFFF'FFFF'FFFF'FF02ULL);\nconstexpr auto kMultiDraftTag = quint64(0xFFFF'FFFF'FFFF'FF03ULL);\nconstexpr auto kMultiDraftCursorsTag = quint64(0xFFFF'FFFF'FFFF'FF04ULL);\nconstexpr auto kRichDraftsTag = quint64(0xFFFF'FFFF'FFFF'FF05ULL);\nconstexpr auto kDraftsTag2 = quint64(0xFFFF'FFFF'FFFF'FF06ULL);\n\nenum { // Local Storage Keys\n\tlskUserMap = 0x00,\n\tlskDraft = 0x01, // data: PeerId peer\n\tlskDraftPosition = 0x02, // data: PeerId peer\n\tlskLegacyImages = 0x03, // legacy\n\tlskLocations = 0x04, // no data\n\tlskLegacyStickerImages = 0x05, // legacy\n\tlskLegacyAudios = 0x06, // legacy\n\tlskRecentStickersOld = 0x07, // no data\n\tlskBackgroundOldOld = 0x08, // no data\n\tlskUserSettings = 0x09, // no data\n\tlskRecentHashtagsAndBots = 0x0a, // no data\n\tlskStickersOld = 0x0b, // no data\n\tlskSavedPeersOld = 0x0c, // no data\n\tlskReportSpamStatusesOld = 0x0d, // no data\n\tlskSavedGifsOld = 0x0e, // no data\n\tlskSavedGifs = 0x0f, // no data\n\tlskStickersKeys = 0x10, // no data\n\tlskTrustedPeers = 0x11, // no data\n\tlskFavedStickers = 0x12, // no data\n\tlskExportSettings = 0x13, // no data\n\tlskBackgroundOld = 0x14, // no data\n\tlskSelfSerialized = 0x15, // serialized self\n\tlskMasksKeys = 0x16, // no data\n\tlskCustomEmojiKeys = 0x17, // no data\n\tlskSearchSuggestions = 0x18, // no data\n\tlskWebviewTokens = 0x19, // data: QByteArray bots, QByteArray other\n\tlskRoundPlaceholder = 0x1a, // no data\n\tlskInlineBotsDownloads = 0x1b, // no data\n\tlskMediaLastPlaybackPositions = 0x1c, // no data\n\tlskBotStorages = 0x1d, // data: PeerId botId\n\tlskPrefs = 0x1e, // no data\n};\n\nauto EmptyMessageDraftSources()\n-> const base::flat_map<Data::DraftKey, MessageDraftSource> & {\n\tstatic const auto result = base::flat_map<\n\t\tData::DraftKey,\n\t\tMessageDraftSource>();\n\treturn result;\n}\n\n[[nodiscard]] FileKey ComputeDataNameKey(const QString &dataName) {\n\t// We dropped old test authorizations when migrated to multi auth.\n\t//const auto testAddition = (cTestMode() ? u\":/test/\"_q : QString());\n\tconst auto testAddition = QString();\n\tconst auto dataNameUtf8 = (dataName + testAddition).toUtf8();\n\tFileKey dataNameHash[2] = { 0 };\n\thashMd5(dataNameUtf8.constData(), dataNameUtf8.size(), dataNameHash);\n\treturn dataNameHash[0];\n}\n\n[[nodiscard]] QString BaseGlobalPath() {\n\treturn cWorkingDir() + u\"tdata/\"_q;\n}\n\n[[nodiscard]] QString ComputeDatabasePath(const QString &dataName) {\n\treturn BaseGlobalPath()\n\t\t+ \"user_\" + dataName\n\t\t// We dropped old test authorizations when migrated to multi auth.\n\t\t//+ (cTestMode() ? \"[test]\" : \"\")\n\t\t+ '/';\n}\n\n[[nodiscard]] QString LegacyTempDirectory() {\n\treturn cWorkingDir() + u\"tdata/tdld/\"_q;\n}\n\n[[nodiscard]] std::pair<quint64, quint64> SerializeSuggest(\n\t\tSuggestOptions options) {\n\treturn {\n\t\t((quint64(options.exists) << 63)\n\t\t\t| (quint64(quint32(options.date)))),\n\t\t((quint64(options.ton) << 63)\n\t\t\t| (quint64(options.priceWhole) << 32)\n\t\t\t| (quint64(options.priceNano))),\n\t};\n}\n\n[[nodiscard]] SuggestOptions DeserializeSuggest(\n\t\tstd::pair<quint64, quint64> suggest) {\n\tconst auto exists = (suggest.first >> 63) ? 1 : 0;\n\tconst auto date = TimeId(uint32(suggest.first & 0xFFFF'FFFFULL));\n\tconst auto ton = (suggest.second >> 63) ? 1 : 0;\n\tconst auto priceWhole = uint32((suggest.second >> 32) & 0x7FFF'FFFFULL);\n\tconst auto priceNano = uint32(suggest.second & 0xFFFF'FFFFULL);\n\treturn {\n\t\t.exists = uint32(exists),\n\t\t.priceWhole = priceWhole,\n\t\t.priceNano = priceNano,\n\t\t.ton = uint32(ton),\n\t\t.date = date,\n\t};\n}\n\n} // namespace\n\nAccount::Account(not_null<Main::Account*> owner, const QString &dataName)\n: _owner(owner)\n, _dataName(dataName)\n, _dataNameKey(ComputeDataNameKey(dataName))\n, _basePath(BaseGlobalPath() + ToFilePart(_dataNameKey) + QChar('/'))\n, _tempPath(BaseGlobalPath() + \"temp_\" + _dataName + QChar('/'))\n, _databasePath(ComputeDatabasePath(dataName))\n, _cacheTotalSizeLimit(Database::Settings().totalSizeLimit)\n, _cacheBigFileTotalSizeLimit(Database::Settings().totalSizeLimit)\n, _cacheTotalTimeLimit(Database::Settings().totalTimeLimit)\n, _cacheBigFileTotalTimeLimit(Database::Settings().totalTimeLimit)\n, _writeMapTimer([=] { writeMap(); })\n, _writePrefsTimer([=] { writePrefs(); })\n, _writeLocationsTimer([=] { writeLocations(); })\n, _writeSearchSuggestionsTimer([=] { writeSearchSuggestions(); }) {\n}\n\nAccount::~Account() {\n\tExpects(!_writeSearchSuggestionsTimer.isActive());\n\n\tif (_localKey) {\n\t\tif (_prefsChanged) {\n\t\t\twritePrefs();\n\t\t}\n\t\tif (_mapChanged) {\n\t\t\twriteMap();\n\t\t}\n\t}\n}\n\nQString Account::tempDirectory() const {\n\treturn _tempPath;\n}\n\nQString Account::supportModePath() const {\n\treturn _databasePath + u\"support\"_q;\n}\n\nStartResult Account::legacyStart(const QByteArray &passcode) {\n\tconst auto result = readMapWith(MTP::AuthKeyPtr(), passcode);\n\tif (result == ReadMapResult::Failed) {\n\t\tAssert(_localKey == nullptr);\n\t} else if (result == ReadMapResult::IncorrectPasscode) {\n\t\treturn StartResult::IncorrectPasscodeLegacy;\n\t}\n\tclearLegacyFiles();\n\treturn StartResult::Success;\n}\n\nstd::unique_ptr<MTP::Config> Account::start(MTP::AuthKeyPtr localKey) {\n\tExpects(localKey != nullptr);\n\n\t_localKey = std::move(localKey);\n\treadMapWith(_localKey);\n\tclearLegacyFiles();\n\treturn readMtpConfig();\n}\n\nvoid Account::startAdded(MTP::AuthKeyPtr localKey) {\n\tExpects(localKey != nullptr);\n\n\t_localKey = std::move(localKey);\n\tclearLegacyFiles();\n}\n\nvoid Account::clearLegacyFiles() {\n\tconst auto weak = base::make_weak(_owner);\n\tClearLegacyFiles(_basePath, [weak, this](\n\t\t\tFnMut<void(base::flat_set<QString>&&)> then) {\n\t\tcrl::on_main(weak, [this, then = std::move(then)]() mutable {\n\t\t\tthen(collectGoodNames());\n\t\t});\n\t});\n}\n\nbase::flat_set<QString> Account::collectGoodNames() const {\n\tconst auto keys = {\n\t\t_prefsKey,\n\t\t_locationsKey,\n\t\t_settingsKey,\n\t\t_installedStickersKey,\n\t\t_featuredStickersKey,\n\t\t_recentStickersKey,\n\t\t_favedStickersKey,\n\t\t_archivedStickersKey,\n\t\t_recentStickersKeyOld,\n\t\t_savedGifsKey,\n\t\t_legacyBackgroundKeyNight,\n\t\t_legacyBackgroundKeyDay,\n\t\t_recentHashtagsAndBotsKey,\n\t\t_exportSettingsKey,\n\t\t_trustedPeersKey,\n\t\t_installedMasksKey,\n\t\t_recentMasksKey,\n\t\t_archivedMasksKey,\n\t\t_installedCustomEmojiKey,\n\t\t_featuredCustomEmojiKey,\n\t\t_archivedCustomEmojiKey,\n\t\t_searchSuggestionsKey,\n\t\t_roundPlaceholderKey,\n\t\t_inlineBotsDownloadsKey,\n\t\t_mediaLastPlaybackPositionsKey,\n\t};\n\tauto result = base::flat_set<QString>{\n\t\t\"map0\",\n\t\t\"map1\",\n\t\t\"maps\",\n\t\t\"configs\",\n\t};\n\tconst auto push = [&](FileKey key) {\n\t\tif (!key) {\n\t\t\treturn;\n\t\t}\n\t\tauto name = ToFilePart(key) + '0';\n\t\tresult.emplace(name);\n\t\tname[name.size() - 1] = '1';\n\t\tresult.emplace(name);\n\t\tname[name.size() - 1] = 's';\n\t\tresult.emplace(name);\n\t};\n\tfor (const auto &[key, value] : _draftsMap) {\n\t\tpush(value);\n\t}\n\tfor (const auto &[key, value] : _draftCursorsMap) {\n\t\tpush(value);\n\t}\n\tfor (const auto &[key, value] : _botStoragesMap) {\n\t\tpush(value);\n\t}\n\tfor (const auto &value : keys) {\n\t\tpush(value);\n\t}\n\treturn result;\n}\n\nAccount::ReadMapResult Account::readMapWith(\n\t\tMTP::AuthKeyPtr localKey,\n\t\tconst QByteArray &legacyPasscode) {\n\tauto ms = crl::now();\n\n\tFileReadDescriptor mapData;\n\tif (!ReadFile(mapData, u\"map\"_q, _basePath)) {\n\t\treturn ReadMapResult::Failed;\n\t}\n\tLOG((\"App Info: reading map...\"));\n\n\tQByteArray legacySalt, legacyKeyEncrypted, mapEncrypted;\n\tmapData.stream >> legacySalt >> legacyKeyEncrypted >> mapEncrypted;\n\tif (!CheckStreamStatus(mapData.stream)) {\n\t\treturn ReadMapResult::Failed;\n\t}\n\tif (!localKey) {\n\t\tif (legacySalt.size() != LocalEncryptSaltSize) {\n\t\t\tLOG((\"App Error: bad salt in map file, size: %1\").arg(legacySalt.size()));\n\t\t\treturn ReadMapResult::Failed;\n\t\t}\n\t\tauto legacyPasscodeKey = CreateLegacyLocalKey(legacyPasscode, legacySalt);\n\n\t\tEncryptedDescriptor keyData;\n\t\tif (!DecryptLocal(keyData, legacyKeyEncrypted, legacyPasscodeKey)) {\n\t\t\tLOG((\"App Info: could not decrypt pass-protected key from map file, maybe bad password...\"));\n\t\t\treturn ReadMapResult::IncorrectPasscode;\n\t\t}\n\t\tauto key = Serialize::read<MTP::AuthKey::Data>(keyData.stream);\n\t\tif (keyData.stream.status() != QDataStream::Ok || !keyData.stream.atEnd()) {\n\t\t\tLOG((\"App Error: could not read pass-protected key from map file\"));\n\t\t\treturn ReadMapResult::Failed;\n\t\t}\n\t\tlocalKey = std::make_shared<MTP::AuthKey>(key);\n\t}\n\n\tEncryptedDescriptor map;\n\tif (!DecryptLocal(map, mapEncrypted, localKey)) {\n\t\tLOG((\"App Error: could not decrypt map.\"));\n\t\treturn ReadMapResult::Failed;\n\t}\n\tLOG((\"App Info: reading encrypted map...\"));\n\n\tQByteArray selfSerialized;\n\tbase::flat_map<PeerId, FileKey> draftsMap;\n\tbase::flat_map<PeerId, FileKey> draftCursorsMap;\n\tbase::flat_map<PeerId, bool> draftsNotReadMap;\n\tbase::flat_map<PeerId, FileKey> botStoragesMap;\n\tbase::flat_map<PeerId, bool> botStoragesNotReadMap;\n\tquint64 prefsKey = 0, locationsKey = 0, reportSpamStatusesKey = 0, trustedPeersKey = 0;\n\tquint64 recentStickersKeyOld = 0;\n\tquint64 installedStickersKey = 0, featuredStickersKey = 0, recentStickersKey = 0, favedStickersKey = 0, archivedStickersKey = 0;\n\tquint64 installedMasksKey = 0, recentMasksKey = 0, archivedMasksKey = 0;\n\tquint64 installedCustomEmojiKey = 0, featuredCustomEmojiKey = 0, archivedCustomEmojiKey = 0;\n\tquint64 savedGifsKey = 0;\n\tquint64 legacyBackgroundKeyDay = 0, legacyBackgroundKeyNight = 0;\n\tquint64 userSettingsKey = 0, recentHashtagsAndBotsKey = 0, exportSettingsKey = 0;\n\tquint64 searchSuggestionsKey = 0;\n\tquint64 roundPlaceholderKey = 0;\n\tquint64 inlineBotsDownloadsKey = 0;\n\tquint64 mediaLastPlaybackPositionsKey = 0;\n\tQByteArray webviewStorageTokenBots, webviewStorageTokenOther;\n\twhile (!map.stream.atEnd()) {\n\t\tquint32 keyType;\n\t\tmap.stream >> keyType;\n\t\tswitch (keyType) {\n\t\tcase lskDraft: {\n\t\t\tquint32 count = 0;\n\t\t\tmap.stream >> count;\n\t\t\tfor (quint32 i = 0; i < count; ++i) {\n\t\t\t\tFileKey key;\n\t\t\t\tquint64 peerIdSerialized;\n\t\t\t\tmap.stream >> key >> peerIdSerialized;\n\t\t\t\tconst auto peerId = DeserializePeerId(peerIdSerialized);\n\t\t\t\tdraftsMap.emplace(peerId, key);\n\t\t\t\tdraftsNotReadMap.emplace(peerId, true);\n\t\t\t}\n\t\t} break;\n\t\tcase lskSelfSerialized: {\n\t\t\tmap.stream >> selfSerialized;\n\t\t} break;\n\t\tcase lskDraftPosition: {\n\t\t\tquint32 count = 0;\n\t\t\tmap.stream >> count;\n\t\t\tfor (quint32 i = 0; i < count; ++i) {\n\t\t\t\tFileKey key;\n\t\t\t\tquint64 peerIdSerialized;\n\t\t\t\tmap.stream >> key >> peerIdSerialized;\n\t\t\t\tconst auto peerId = DeserializePeerId(peerIdSerialized);\n\t\t\t\tdraftCursorsMap.emplace(peerId, key);\n\t\t\t}\n\t\t} break;\n\t\tcase lskLegacyImages:\n\t\tcase lskLegacyStickerImages:\n\t\tcase lskLegacyAudios: {\n\t\t\tquint32 count = 0;\n\t\t\tmap.stream >> count;\n\t\t\tfor (quint32 i = 0; i < count; ++i) {\n\t\t\t\tFileKey key;\n\t\t\t\tquint64 first, second;\n\t\t\t\tqint32 size;\n\t\t\t\tmap.stream >> key >> first >> second >> size;\n\t\t\t\t// Just ignore the key, it will be removed as a leaked one.\n\t\t\t}\n\t\t} break;\n\t\tcase lskPrefs: {\n\t\t\tmap.stream >> prefsKey;\n\t\t} break;\n\t\tcase lskLocations: {\n\t\t\tmap.stream >> locationsKey;\n\t\t} break;\n\t\tcase lskReportSpamStatusesOld: {\n\t\t\tmap.stream >> reportSpamStatusesKey;\n\t\t\tClearKey(reportSpamStatusesKey, _basePath);\n\t\t} break;\n\t\tcase lskTrustedPeers: {\n\t\t\tmap.stream >> trustedPeersKey;\n\t\t} break;\n\t\tcase lskRecentStickersOld: {\n\t\t\tmap.stream >> recentStickersKeyOld;\n\t\t} break;\n\t\tcase lskBackgroundOldOld: {\n\t\t\tmap.stream >> (Window::Theme::IsNightMode()\n\t\t\t\t? legacyBackgroundKeyNight\n\t\t\t\t: legacyBackgroundKeyDay);\n\t\t} break;\n\t\tcase lskBackgroundOld: {\n\t\t\tmap.stream >> legacyBackgroundKeyDay >> legacyBackgroundKeyNight;\n\t\t} break;\n\t\tcase lskUserSettings: {\n\t\t\tmap.stream >> userSettingsKey;\n\t\t} break;\n\t\tcase lskRecentHashtagsAndBots: {\n\t\t\tmap.stream >> recentHashtagsAndBotsKey;\n\t\t} break;\n\t\tcase lskStickersOld: {\n\t\t\tmap.stream >> installedStickersKey;\n\t\t} break;\n\t\tcase lskStickersKeys: {\n\t\t\tmap.stream >> installedStickersKey >> featuredStickersKey >> recentStickersKey >> archivedStickersKey;\n\t\t} break;\n\t\tcase lskFavedStickers: {\n\t\t\tmap.stream >> favedStickersKey;\n\t\t} break;\n\t\tcase lskSavedGifsOld: {\n\t\t\tquint64 key;\n\t\t\tmap.stream >> key;\n\t\t} break;\n\t\tcase lskSavedGifs: {\n\t\t\tmap.stream >> savedGifsKey;\n\t\t} break;\n\t\tcase lskSavedPeersOld: {\n\t\t\tquint64 key;\n\t\t\tmap.stream >> key;\n\t\t} break;\n\t\tcase lskExportSettings: {\n\t\t\tmap.stream >> exportSettingsKey;\n\t\t} break;\n\t\tcase lskMasksKeys: {\n\t\t\tmap.stream\n\t\t\t\t>> installedMasksKey\n\t\t\t\t>> recentMasksKey\n\t\t\t\t>> archivedMasksKey;\n\t\t} break;\n\t\tcase lskCustomEmojiKeys: {\n\t\t\tmap.stream\n\t\t\t\t>> installedCustomEmojiKey\n\t\t\t\t>> featuredCustomEmojiKey\n\t\t\t\t>> archivedCustomEmojiKey;\n\t\t} break;\n\t\tcase lskSearchSuggestions: {\n\t\t\tmap.stream >> searchSuggestionsKey;\n\t\t} break;\n\t\tcase lskRoundPlaceholder: {\n\t\t\tmap.stream >> roundPlaceholderKey;\n\t\t} break;\n\t\tcase lskInlineBotsDownloads: {\n\t\t\tmap.stream >> inlineBotsDownloadsKey;\n\t\t} break;\n\t\tcase lskMediaLastPlaybackPositions: {\n\t\t\tmap.stream >> mediaLastPlaybackPositionsKey;\n\t\t} break;\n\t\tcase lskWebviewTokens: {\n\t\t\tmap.stream\n\t\t\t\t>> webviewStorageTokenBots\n\t\t\t\t>> webviewStorageTokenOther;\n\t\t} break;\n\t\tcase lskBotStorages: {\n\t\t\tquint32 count = 0;\n\t\t\tmap.stream >> count;\n\t\t\tfor (quint32 i = 0; i < count; ++i) {\n\t\t\t\tFileKey key;\n\t\t\t\tquint64 peerIdSerialized;\n\t\t\t\tmap.stream >> key >> peerIdSerialized;\n\t\t\t\tconst auto peerId = DeserializePeerId(peerIdSerialized);\n\t\t\t\tbotStoragesMap.emplace(peerId, key);\n\t\t\t\tbotStoragesNotReadMap.emplace(peerId, true);\n\t\t\t}\n\t\t} break;\n\t\tdefault:\n\t\t\tLOG((\"App Error: unknown key type in encrypted map: %1\").arg(keyType));\n\t\t\treturn ReadMapResult::Failed;\n\t\t}\n\t\tif (!CheckStreamStatus(map.stream)) {\n\t\t\treturn ReadMapResult::Failed;\n\t\t}\n\t}\n\n\t_localKey = std::move(localKey);\n\n\t_draftsMap = draftsMap;\n\t_draftCursorsMap = draftCursorsMap;\n\t_draftsNotReadMap = draftsNotReadMap;\n\t_botStoragesMap = botStoragesMap;\n\t_botStoragesNotReadMap = botStoragesNotReadMap;\n\n\t_prefsKey = prefsKey;\n\t_locationsKey = locationsKey;\n\t_trustedPeersKey = trustedPeersKey;\n\t_recentStickersKeyOld = recentStickersKeyOld;\n\t_installedStickersKey = installedStickersKey;\n\t_featuredStickersKey = featuredStickersKey;\n\t_recentStickersKey = recentStickersKey;\n\t_favedStickersKey = favedStickersKey;\n\t_archivedStickersKey = archivedStickersKey;\n\t_savedGifsKey = savedGifsKey;\n\t_installedMasksKey = installedMasksKey;\n\t_recentMasksKey = recentMasksKey;\n\t_archivedMasksKey = archivedMasksKey;\n\t_installedCustomEmojiKey = installedCustomEmojiKey;\n\t_featuredCustomEmojiKey = featuredCustomEmojiKey;\n\t_archivedCustomEmojiKey = archivedCustomEmojiKey;\n\t_legacyBackgroundKeyDay = legacyBackgroundKeyDay;\n\t_legacyBackgroundKeyNight = legacyBackgroundKeyNight;\n\t_settingsKey = userSettingsKey;\n\t_recentHashtagsAndBotsKey = recentHashtagsAndBotsKey;\n\t_exportSettingsKey = exportSettingsKey;\n\t_searchSuggestionsKey = searchSuggestionsKey;\n\t_roundPlaceholderKey = roundPlaceholderKey;\n\t_inlineBotsDownloadsKey = inlineBotsDownloadsKey;\n\t_mediaLastPlaybackPositionsKey = mediaLastPlaybackPositionsKey;\n\t_oldMapVersion = mapData.version;\n\t_webviewStorageIdBots.token = webviewStorageTokenBots;\n\t_webviewStorageIdOther.token = webviewStorageTokenOther;\n\n\tif (_oldMapVersion < AppVersion) {\n\t\twriteMapDelayed();\n\t} else {\n\t\t_mapChanged = false;\n\t}\n\n\tif (_prefsKey) {\n\t\treadPrefs();\n\t}\n\tif (_locationsKey) {\n\t\treadLocations();\n\t}\n\tif (_legacyBackgroundKeyDay || _legacyBackgroundKeyNight) {\n\t\tLocal::moveLegacyBackground(\n\t\t\t_basePath,\n\t\t\t_localKey,\n\t\t\t_legacyBackgroundKeyDay,\n\t\t\t_legacyBackgroundKeyNight);\n\t}\n\n\tauto stored = readSessionSettings();\n\treadMtpData();\n\n\tDEBUG_LOG((\"selfSerialized set: %1\").arg(selfSerialized.size()));\n\t_owner->setSessionFromStorage(\n\t\tstd::move(stored),\n\t\tstd::move(selfSerialized),\n\t\t_oldMapVersion);\n\n\tLOG((\"Map read time: %1\").arg(crl::now() - ms));\n\n\treturn ReadMapResult::Success;\n}\n\nvoid Account::writeMapDelayed() {\n\t_mapChanged = true;\n\t_writeMapTimer.callOnce(kDelayedWriteTimeout);\n}\n\nvoid Account::writeMapQueued() {\n\t_mapChanged = true;\n\tcrl::on_main(_owner, [=] {\n\t\twriteMap();\n\t});\n}\n\nvoid Account::writeMap() {\n\tExpects(_localKey != nullptr);\n\n\t_writeMapTimer.cancel();\n\tif (!_mapChanged) {\n\t\treturn;\n\t}\n\t_mapChanged = false;\n\n\tif (!QDir().exists(_basePath)) {\n\t\tQDir().mkpath(_basePath);\n\t}\n\n\tFileWriteDescriptor map(u\"map\"_q, _basePath);\n\tmap.writeData(QByteArray());\n\tmap.writeData(QByteArray());\n\n\tuint32 mapSize = 0;\n\tconst auto self = [&] {\n\t\tif (!_owner->sessionExists()) {\n\t\t\tDEBUG_LOG((\"AuthSelf Warning: Session does not exist.\"));\n\t\t\treturn QByteArray();\n\t\t}\n\t\tconst auto self = _owner->session().user();\n\t\tif (self->phone().isEmpty()) {\n\t\t\tDEBUG_LOG((\"AuthSelf Error: Phone is empty.\"));\n\t\t\treturn QByteArray();\n\t\t}\n\t\tauto result = QByteArray();\n\t\tresult.reserve(Serialize::peerSize(self)\n\t\t\t+ Serialize::stringSize(self->about()));\n\t\t{\n\t\t\tQBuffer buffer(&result);\n\t\t\tbuffer.open(QIODevice::WriteOnly);\n\t\t\tQDataStream stream(&buffer);\n\t\t\tSerialize::writePeer(stream, self);\n\t\t\tstream << self->about();\n\t\t}\n\t\treturn result;\n\t}();\n\tif (!self.isEmpty()) mapSize += sizeof(quint32) + Serialize::bytearraySize(self);\n\tif (!_draftsMap.empty()) mapSize += sizeof(quint32) * 2 + _draftsMap.size() * sizeof(quint64) * 2;\n\tif (!_draftCursorsMap.empty()) mapSize += sizeof(quint32) * 2 + _draftCursorsMap.size() * sizeof(quint64) * 2;\n\tif (_prefsKey) mapSize += sizeof(quint32) + sizeof(quint64);\n\tif (_locationsKey) mapSize += sizeof(quint32) + sizeof(quint64);\n\tif (_trustedPeersKey) mapSize += sizeof(quint32) + sizeof(quint64);\n\tif (_recentStickersKeyOld) mapSize += sizeof(quint32) + sizeof(quint64);\n\tif (_installedStickersKey || _featuredStickersKey || _recentStickersKey || _archivedStickersKey) {\n\t\tmapSize += sizeof(quint32) + 4 * sizeof(quint64);\n\t}\n\tif (_favedStickersKey) mapSize += sizeof(quint32) + sizeof(quint64);\n\tif (_savedGifsKey) mapSize += sizeof(quint32) + sizeof(quint64);\n\tif (_settingsKey) mapSize += sizeof(quint32) + sizeof(quint64);\n\tif (_recentHashtagsAndBotsKey) mapSize += sizeof(quint32) + sizeof(quint64);\n\tif (_exportSettingsKey) mapSize += sizeof(quint32) + sizeof(quint64);\n\tif (_installedMasksKey || _recentMasksKey || _archivedMasksKey) {\n\t\tmapSize += sizeof(quint32) + 3 * sizeof(quint64);\n\t}\n\tif (_installedCustomEmojiKey || _featuredCustomEmojiKey || _archivedCustomEmojiKey) {\n\t\tmapSize += sizeof(quint32) + 3 * sizeof(quint64);\n\t}\n\tif (_searchSuggestionsKey) mapSize += sizeof(quint32) + sizeof(quint64);\n\tif (!_webviewStorageIdBots.token.isEmpty()\n\t\t|| !_webviewStorageIdOther.token.isEmpty()) {\n\t\tmapSize += sizeof(quint32)\n\t\t\t+ Serialize::bytearraySize(_webviewStorageIdBots.token)\n\t\t\t+ Serialize::bytearraySize(_webviewStorageIdOther.token);\n\t}\n\tif (_roundPlaceholderKey) mapSize += sizeof(quint32) + sizeof(quint64);\n\tif (_inlineBotsDownloadsKey) mapSize += sizeof(quint32) + sizeof(quint64);\n\tif (_mediaLastPlaybackPositionsKey) mapSize += sizeof(quint32) + sizeof(quint64);\n\tif (!_botStoragesMap.empty()) mapSize += sizeof(quint32) * 2 + _botStoragesMap.size() * sizeof(quint64) * 2;\n\n\tEncryptedDescriptor mapData(mapSize);\n\tif (!self.isEmpty()) {\n\t\tmapData.stream << quint32(lskSelfSerialized) << self;\n\t}\n\tif (!_draftsMap.empty()) {\n\t\tmapData.stream << quint32(lskDraft) << quint32(_draftsMap.size());\n\t\tfor (const auto &[key, value] : _draftsMap) {\n\t\t\tmapData.stream << quint64(value) << SerializePeerId(key);\n\t\t}\n\t}\n\tif (!_draftCursorsMap.empty()) {\n\t\tmapData.stream << quint32(lskDraftPosition) << quint32(_draftCursorsMap.size());\n\t\tfor (const auto &[key, value] : _draftCursorsMap) {\n\t\t\tmapData.stream << quint64(value) << SerializePeerId(key);\n\t\t}\n\t}\n\tif (_prefsKey) {\n\t\tmapData.stream << quint32(lskPrefs) << quint64(_prefsKey);\n\t}\n\tif (_locationsKey) {\n\t\tmapData.stream << quint32(lskLocations) << quint64(_locationsKey);\n\t}\n\tif (_trustedPeersKey) {\n\t\tmapData.stream << quint32(lskTrustedPeers) << quint64(_trustedPeersKey);\n\t}\n\tif (_recentStickersKeyOld) {\n\t\tmapData.stream << quint32(lskRecentStickersOld) << quint64(_recentStickersKeyOld);\n\t}\n\tif (_installedStickersKey || _featuredStickersKey || _recentStickersKey || _archivedStickersKey) {\n\t\tmapData.stream << quint32(lskStickersKeys);\n\t\tmapData.stream << quint64(_installedStickersKey) << quint64(_featuredStickersKey) << quint64(_recentStickersKey) << quint64(_archivedStickersKey);\n\t}\n\tif (_favedStickersKey) {\n\t\tmapData.stream << quint32(lskFavedStickers) << quint64(_favedStickersKey);\n\t}\n\tif (_savedGifsKey) {\n\t\tmapData.stream << quint32(lskSavedGifs) << quint64(_savedGifsKey);\n\t}\n\tif (_settingsKey) {\n\t\tmapData.stream << quint32(lskUserSettings) << quint64(_settingsKey);\n\t}\n\tif (_recentHashtagsAndBotsKey) {\n\t\tmapData.stream << quint32(lskRecentHashtagsAndBots) << quint64(_recentHashtagsAndBotsKey);\n\t}\n\tif (_exportSettingsKey) {\n\t\tmapData.stream << quint32(lskExportSettings) << quint64(_exportSettingsKey);\n\t}\n\tif (_installedMasksKey || _recentMasksKey || _archivedMasksKey) {\n\t\tmapData.stream << quint32(lskMasksKeys);\n\t\tmapData.stream\n\t\t\t<< quint64(_installedMasksKey)\n\t\t\t<< quint64(_recentMasksKey)\n\t\t\t<< quint64(_archivedMasksKey);\n\t}\n\tif (_installedCustomEmojiKey || _featuredCustomEmojiKey || _archivedCustomEmojiKey) {\n\t\tmapData.stream << quint32(lskCustomEmojiKeys);\n\t\tmapData.stream\n\t\t\t<< quint64(_installedCustomEmojiKey)\n\t\t\t<< quint64(_featuredCustomEmojiKey)\n\t\t\t<< quint64(_archivedCustomEmojiKey);\n\t}\n\tif (_searchSuggestionsKey) {\n\t\tmapData.stream << quint32(lskSearchSuggestions);\n\t\tmapData.stream << quint64(_searchSuggestionsKey);\n\t}\n\tif (!_webviewStorageIdBots.token.isEmpty()\n\t\t|| !_webviewStorageIdOther.token.isEmpty()) {\n\t\tmapData.stream << quint32(lskWebviewTokens);\n\t\tmapData.stream\n\t\t\t<< _webviewStorageIdBots.token\n\t\t\t<< _webviewStorageIdOther.token;\n\t}\n\tif (_roundPlaceholderKey) {\n\t\tmapData.stream << quint32(lskRoundPlaceholder);\n\t\tmapData.stream << quint64(_roundPlaceholderKey);\n\t}\n\tif (_inlineBotsDownloadsKey) {\n\t\tmapData.stream << quint32(lskInlineBotsDownloads);\n\t\tmapData.stream << quint64(_inlineBotsDownloadsKey);\n\t}\n\tif (_mediaLastPlaybackPositionsKey) {\n\t\tmapData.stream << quint32(lskMediaLastPlaybackPositions);\n\t\tmapData.stream << quint64(_mediaLastPlaybackPositionsKey);\n\t}\n\tif (!_botStoragesMap.empty()) {\n\t\tmapData.stream << quint32(lskBotStorages) << quint32(_botStoragesMap.size());\n\t\tfor (const auto &[key, value] : _botStoragesMap) {\n\t\t\tmapData.stream << quint64(value) << SerializePeerId(key);\n\t\t}\n\t}\n\tmap.writeEncrypted(mapData, _localKey);\n\n\t_mapChanged = false;\n}\n\nvoid Account::reset() {\n\t_writeSearchSuggestionsTimer.cancel();\n\n\tauto names = collectGoodNames();\n\t_draftsMap.clear();\n\t_draftCursorsMap.clear();\n\t_draftsNotReadMap.clear();\n\t_botStoragesMap.clear();\n\t_botStoragesNotReadMap.clear();\n\t_prefsKey = _locationsKey = _trustedPeersKey = 0;\n\t_recentStickersKeyOld = 0;\n\t_installedStickersKey = 0;\n\t_featuredStickersKey = 0;\n\t_recentStickersKey = 0;\n\t_favedStickersKey = 0;\n\t_archivedStickersKey = 0;\n\t_savedGifsKey = 0;\n\t_installedMasksKey = 0;\n\t_recentMasksKey = 0;\n\t_archivedMasksKey = 0;\n\t_installedCustomEmojiKey = 0;\n\t_featuredCustomEmojiKey = 0;\n\t_archivedCustomEmojiKey = 0;\n\t_legacyBackgroundKeyDay = _legacyBackgroundKeyNight = 0;\n\t_settingsKey = _recentHashtagsAndBotsKey = _exportSettingsKey = 0;\n\t_searchSuggestionsKey = 0;\n\t_roundPlaceholderKey = 0;\n\t_inlineBotsDownloadsKey = 0;\n\t_mediaLastPlaybackPositionsKey = 0;\n\t_oldMapVersion = 0;\n\t_fileLocations.clear();\n\t_fileLocationPairs.clear();\n\t_fileLocationAliases.clear();\n\t_downloadsSerialize = nullptr;\n\t_downloadsSerialized = QByteArray();\n\t_cacheTotalSizeLimit = Database::Settings().totalSizeLimit;\n\t_cacheTotalTimeLimit = Database::Settings().totalTimeLimit;\n\t_cacheBigFileTotalSizeLimit = Database::Settings().totalSizeLimit;\n\t_cacheBigFileTotalTimeLimit = Database::Settings().totalTimeLimit;\n\t_mediaLastPlaybackPosition.clear();\n\n\tconst auto wvbots = _webviewStorageIdBots.path;\n\tconst auto wvother = _webviewStorageIdOther.path;\n\tconst auto wvclear = [](Webview::StorageId &storageId) {\n\t\tWebview::ClearStorageDataByToken(\n\t\t\tbase::take(storageId).token.toStdString());\n\t};\n\twvclear(_webviewStorageIdBots);\n\twvclear(_webviewStorageIdOther);\n\n\t_mapChanged = true;\n\twriteMap();\n\twriteMtpData();\n\n\tcrl::async([\n\t\tbase = _basePath,\n\t\ttemp = _tempPath,\n\t\tnames = std::move(names),\n\t\twvbots,\n\t\twvother\n\t] {\n\t\tfor (const auto &name : names) {\n\t\t\tif (!name.endsWith(u\"map0\"_q)\n\t\t\t\t&& !name.endsWith(u\"map1\"_q)\n\t\t\t\t&& !name.endsWith(u\"maps\"_q)\n\t\t\t\t&& !name.endsWith(u\"configs\"_q)) {\n\t\t\t\tQFile::remove(base + name);\n\t\t\t}\n\t\t}\n\t\tQDir(LegacyTempDirectory()).removeRecursively();\n\t\tif (!wvbots.isEmpty()) {\n\t\t\tQDir(wvbots).removeRecursively();\n\t\t}\n\t\tif (!wvother.isEmpty()) {\n\t\t\tQDir(wvother).removeRecursively();\n\t\t}\n\t\tQDir(temp).removeRecursively();\n\t});\n\n\tLocal::sync();\n}\n\nvoid Account::writeLocations() {\n\t_writeLocationsTimer.cancel();\n\tif (!_locationsChanged) {\n\t\treturn;\n\t}\n\t_locationsChanged = false;\n\n\tif (_downloadsSerialize) {\n\t\tif (auto serialized = _downloadsSerialize()) {\n\t\t\t_downloadsSerialized = std::move(*serialized);\n\t\t}\n\t}\n\tif (_fileLocations.isEmpty() && _downloadsSerialized.isEmpty()) {\n\t\tif (_locationsKey) {\n\t\t\tClearKey(_locationsKey, _basePath);\n\t\t\t_locationsKey = 0;\n\t\t\twriteMapDelayed();\n\t\t}\n\t} else {\n\t\tif (!_locationsKey) {\n\t\t\t_locationsKey = GenerateKey(_basePath);\n\t\t\twriteMapQueued();\n\t\t}\n\t\tquint32 size = 0;\n\t\tfor (auto i = _fileLocations.cbegin(), e = _fileLocations.cend(); i != e; ++i) {\n\t\t\t// location + type + namelen + name\n\t\t\tsize += sizeof(quint64) * 2 + sizeof(quint32) + Serialize::stringSize(i.value().name());\n\t\t\tif (AppVersion > 9013) {\n\t\t\t\t// bookmark\n\t\t\t\tsize += Serialize::bytearraySize(i.value().bookmark());\n\t\t\t}\n\t\t\t// date + size\n\t\t\tsize += Serialize::dateTimeSize() + sizeof(quint32);\n\t\t}\n\n\t\t//end mark\n\t\tsize += sizeof(quint64) * 2 + sizeof(quint32) + Serialize::stringSize(QString());\n\t\tif (AppVersion > 9013) {\n\t\t\tsize += Serialize::bytearraySize(QByteArray());\n\t\t}\n\t\tsize += Serialize::dateTimeSize() + sizeof(quint32);\n\n\t\tsize += sizeof(quint32); // aliases count\n\t\tfor (auto i = _fileLocationAliases.cbegin(), e = _fileLocationAliases.cend(); i != e; ++i) {\n\t\t\t// alias + location\n\t\t\tsize += sizeof(quint64) * 2 + sizeof(quint64) * 2;\n\t\t}\n\n\t\tsize += sizeof(quint32); // legacy webLocationsCount\n\t\tsize += Serialize::bytearraySize(_downloadsSerialized);\n\n\t\tEncryptedDescriptor data(size);\n\t\tauto legacyTypeField = 0;\n\t\tfor (auto i = _fileLocations.cbegin(); i != _fileLocations.cend(); ++i) {\n\t\t\tdata.stream << quint64(i.key().first) << quint64(i.key().second) << quint32(legacyTypeField) << i.value().name();\n\t\t\tif (AppVersion > 9013) {\n\t\t\t\tdata.stream << i.value().bookmark();\n\t\t\t}\n\t\t\tdata.stream << i.value().modified << quint32(i.value().size);\n\t\t}\n\n\t\tdata.stream << quint64(0) << quint64(0) << quint32(0) << QString();\n\t\tif (AppVersion > 9013) {\n\t\t\tdata.stream << QByteArray();\n\t\t}\n\t\tdata.stream << QDateTime::currentDateTime() << quint32(0);\n\n\t\tdata.stream << quint32(_fileLocationAliases.size());\n\t\tfor (auto i = _fileLocationAliases.cbegin(), e = _fileLocationAliases.cend(); i != e; ++i) {\n\t\t\tdata.stream << quint64(i.key().first) << quint64(i.key().second) << quint64(i.value().first) << quint64(i.value().second);\n\t\t}\n\n\t\tdata.stream << quint32(0) << _downloadsSerialized;\n\n\t\tFileWriteDescriptor file(_locationsKey, _basePath);\n\t\tfile.writeEncrypted(data, _localKey);\n\t}\n}\n\nvoid Account::writeLocationsQueued() {\n\t_locationsChanged = true;\n\tcrl::on_main(_owner, [=] {\n\t\twriteLocations();\n\t});\n}\n\nvoid Account::writeLocationsDelayed() {\n\t_locationsChanged = true;\n\t_writeLocationsTimer.callOnce(kDelayedWriteTimeout);\n}\n\nvoid Account::readLocations() {\n\tFileReadDescriptor locations;\n\tif (!ReadEncryptedFile(locations, _locationsKey, _basePath, _localKey)) {\n\t\tClearKey(_locationsKey, _basePath);\n\t\t_locationsKey = 0;\n\t\twriteMapDelayed();\n\t\treturn;\n\t}\n\n\tbool endMarkFound = false;\n\twhile (!locations.stream.atEnd()) {\n\t\tquint64 first, second;\n\t\tQByteArray bookmark;\n\t\tCore::FileLocation loc;\n\t\tquint32 legacyTypeField = 0;\n\t\tquint32 size = 0;\n\t\tlocations.stream >> first >> second >> legacyTypeField >> loc.fname;\n\t\tif (locations.version > 9013) {\n\t\t\tlocations.stream >> bookmark;\n\t\t}\n\t\tlocations.stream >> loc.modified >> size;\n\t\tloc.setBookmark(bookmark);\n\t\tloc.size = int64(size);\n\n\t\tif (!first && !second && !legacyTypeField && loc.fname.isEmpty() && !loc.size) { // end mark\n\t\t\tendMarkFound = true;\n\t\t\tbreak;\n\t\t}\n\n\t\tMediaKey key(first, second);\n\n\t\t_fileLocations.insert(key, loc);\n\t\tif (!loc.inMediaCache()) {\n\t\t\t_fileLocationPairs.insert(loc.fname, { key, loc });\n\t\t}\n\t}\n\n\tif (endMarkFound) {\n\t\tquint32 cnt;\n\t\tlocations.stream >> cnt;\n\t\tfor (quint32 i = 0; i < cnt; ++i) {\n\t\t\tquint64 kfirst, ksecond, vfirst, vsecond;\n\t\t\tlocations.stream >> kfirst >> ksecond >> vfirst >> vsecond;\n\t\t\t_fileLocationAliases.insert(MediaKey(kfirst, ksecond), MediaKey(vfirst, vsecond));\n\t\t}\n\n\t\tif (!locations.stream.atEnd()) {\n\t\t\tquint32 webLocationsCount;\n\t\t\tlocations.stream >> webLocationsCount;\n\t\t\tfor (quint32 i = 0; i < webLocationsCount; ++i) {\n\t\t\t\tQString url;\n\t\t\t\tquint64 key;\n\t\t\t\tqint32 size;\n\t\t\t\tlocations.stream >> url >> key >> size;\n\t\t\t\tClearKey(key, _basePath);\n\t\t\t}\n\n\t\t\tif (!locations.stream.atEnd()) {\n\t\t\t\tlocations.stream >> _downloadsSerialized;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Account::updateDownloads(\n\t\tFn<std::optional<QByteArray>()> downloadsSerialize) {\n\t_downloadsSerialize = std::move(downloadsSerialize);\n\twriteLocationsDelayed();\n}\n\nQByteArray Account::downloadsSerialized() const {\n\treturn _downloadsSerialized;\n}\n\nvoid Account::writeSessionSettings() {\n\twriteSessionSettings(nullptr);\n}\n\nvoid Account::writeSessionSettings(Main::SessionSettings *stored) {\n\tif (_readingUserSettings) {\n\t\tLOG((\"App Error: attempt to write settings while reading them!\"));\n\t\treturn;\n\t}\n\tLOG((\"App Info: writing encrypted user settings...\"));\n\n\tif (!_settingsKey) {\n\t\t_settingsKey = GenerateKey(_basePath);\n\t\twriteMapQueued();\n\t}\n\n\tauto userDataInstance = stored\n\t\t? stored\n\t\t: _owner->getSessionSettings();\n\tauto userData = userDataInstance\n\t\t? userDataInstance->serialize()\n\t\t: QByteArray();\n\n\tauto recentStickers = cRecentStickersPreload();\n\tif (recentStickers.isEmpty() && _owner->sessionExists()) {\n\t\tconst auto &stickers = _owner->session().data().stickers();\n\t\trecentStickers.reserve(stickers.getRecentPack().size());\n\t\tfor (const auto &pair : std::as_const(stickers.getRecentPack())) {\n\t\t\trecentStickers.push_back(qMakePair(pair.first->id, pair.second));\n\t\t}\n\t}\n\n\tuint32 size = 24 * (sizeof(quint32) + sizeof(qint32));\n\tsize += sizeof(quint32);\n\tsize += sizeof(quint32) + sizeof(qint32) + recentStickers.size() * (sizeof(uint64) + sizeof(ushort));\n\tsize += sizeof(quint32) + 3 * sizeof(qint32);\n\tsize += sizeof(quint32) + 2 * sizeof(qint32);\n\tsize += sizeof(quint32) + sizeof(qint64) + sizeof(qint32);\n\tif (!userData.isEmpty()) {\n\t\tsize += sizeof(quint32) + Serialize::bytearraySize(userData);\n\t}\n\n\tEncryptedDescriptor data(size);\n\tdata.stream << quint32(dbiCacheSettings) << qint64(_cacheTotalSizeLimit) << qint32(_cacheTotalTimeLimit) << qint64(_cacheBigFileTotalSizeLimit) << qint32(_cacheBigFileTotalTimeLimit);\n\tif (!userData.isEmpty()) {\n\t\tdata.stream << quint32(dbiSessionSettings) << userData;\n\t}\n\tdata.stream << quint32(dbiRecentStickers) << recentStickers;\n\n\tFileWriteDescriptor file(_settingsKey, _basePath);\n\tfile.writeEncrypted(data, _localKey);\n}\n\nReadSettingsContext Account::prepareReadSettingsContext() const {\n\treturn ReadSettingsContext{\n\t\t.legacyHasCustomDayBackground = (_legacyBackgroundKeyDay != 0)\n\t};\n}\n\nstd::unique_ptr<Main::SessionSettings> Account::readSessionSettings() {\n\tReadSettingsContext context;\n\tFileReadDescriptor userSettings;\n\tif (!ReadEncryptedFile(userSettings, _settingsKey, _basePath, _localKey)) {\n\t\tLOG((\"App Info: could not read encrypted user settings...\"));\n\n\t\tLocal::readOldUserSettings(true, context);\n\t\tauto result = applyReadContext(std::move(context));\n\n\t\twriteSessionSettings(result.get());\n\n\t\treturn result;\n\t}\n\n\tLOG((\"App Info: reading encrypted user settings...\"));\n\t_readingUserSettings = true;\n\twhile (!userSettings.stream.atEnd()) {\n\t\tquint32 blockId;\n\t\tuserSettings.stream >> blockId;\n\t\tif (!CheckStreamStatus(userSettings.stream)) {\n\t\t\t_readingUserSettings = false;\n\t\t\twriteSessionSettings();\n\t\t\treturn nullptr;\n\t\t}\n\n\t\tif (!ReadSetting(blockId, userSettings.stream, userSettings.version, context)) {\n\t\t\t_readingUserSettings = false;\n\t\t\twriteSessionSettings();\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\t_readingUserSettings = false;\n\tLOG((\"App Info: encrypted user settings read.\"));\n\n\tauto result = applyReadContext(std::move(context));\n\tif (context.legacyRead) {\n\t\twriteSessionSettings(result.get());\n\t}\n\treturn result;\n}\n\nstd::unique_ptr<Main::SessionSettings> Account::applyReadContext(\n\t\tReadSettingsContext &&context) {\n\tApplyReadFallbackConfig(context);\n\n\tif (context.cacheTotalSizeLimit) {\n\t\t_cacheTotalSizeLimit = context.cacheTotalSizeLimit;\n\t\t_cacheTotalTimeLimit = context.cacheTotalTimeLimit;\n\t\t_cacheBigFileTotalSizeLimit = context.cacheBigFileTotalSizeLimit;\n\t\t_cacheBigFileTotalTimeLimit = context.cacheBigFileTotalTimeLimit;\n\n\t\tconst auto &normal = Database::Settings();\n\t\tAssert(_cacheTotalSizeLimit > normal.maxDataSize);\n\t\tAssert(_cacheBigFileTotalSizeLimit > normal.maxDataSize);\n\t}\n\n\tif (!context.mtpAuthorization.isEmpty()) {\n\t\t_owner->setMtpAuthorization(context.mtpAuthorization);\n\t} else {\n\t\tfor (auto &key : context.mtpLegacyKeys) {\n\t\t\t_owner->setLegacyMtpKey(std::move(key));\n\t\t}\n\t\tif (context.mtpLegacyMainDcId) {\n\t\t\t_owner->setMtpMainDcId(context.mtpLegacyMainDcId);\n\t\t\t_owner->setSessionUserId(context.mtpLegacyUserId);\n\t\t}\n\t}\n\n\tif (context.tileRead) {\n\t\tWindow::Theme::Background()->setTileDayValue(context.tileDay);\n\t\tWindow::Theme::Background()->setTileNightValue(context.tileNight);\n\t}\n\n\treturn std::move(context.sessionSettingsStorage);\n}\n\nvoid Account::writeMtpData() {\n\tExpects(_localKey != nullptr);\n\n\tconst auto serialized = _owner->serializeMtpAuthorization();\n\tconst auto size = sizeof(quint32) + Serialize::bytearraySize(serialized);\n\n\tFileWriteDescriptor mtp(ToFilePart(_dataNameKey), BaseGlobalPath());\n\tEncryptedDescriptor data(size);\n\tdata.stream << quint32(dbiMtpAuthorization) << serialized;\n\tmtp.writeEncrypted(data, _localKey);\n}\n\nvoid Account::readMtpData() {\n\tauto context = prepareReadSettingsContext();\n\n\tFileReadDescriptor mtp;\n\tif (!ReadEncryptedFile(mtp, ToFilePart(_dataNameKey), BaseGlobalPath(), _localKey)) {\n\t\tif (_localKey) {\n\t\t\tLocal::readOldMtpData(true, context);\n\t\t\tapplyReadContext(std::move(context));\n\t\t\twriteMtpData();\n\t\t}\n\t\treturn;\n\t}\n\n\tLOG((\"App Info: reading encrypted mtp data...\"));\n\twhile (!mtp.stream.atEnd()) {\n\t\tquint32 blockId;\n\t\tmtp.stream >> blockId;\n\t\tif (!CheckStreamStatus(mtp.stream)) {\n\t\t\treturn writeMtpData();\n\t\t}\n\n\t\tif (!ReadSetting(blockId, mtp.stream, mtp.version, context)) {\n\t\t\treturn writeMtpData();\n\t\t}\n\t}\n\tapplyReadContext(std::move(context));\n}\n\nvoid Account::writeMtpConfig() {\n\tExpects(_localKey != nullptr);\n\n\tconst auto serialized = _owner->mtp().config().serialize();\n\tconst auto size = Serialize::bytearraySize(serialized);\n\n\tFileWriteDescriptor file(u\"config\"_q, _basePath);\n\tEncryptedDescriptor data(size);\n\tdata.stream << serialized;\n\tfile.writeEncrypted(data, _localKey);\n}\n\nstd::unique_ptr<MTP::Config> Account::readMtpConfig() {\n\tExpects(_localKey != nullptr);\n\n\tFileReadDescriptor file;\n\tif (!ReadEncryptedFile(file, \"config\", _basePath, _localKey)) {\n\t\treturn nullptr;\n\t}\n\n\tLOG((\"App Info: reading encrypted mtp config...\"));\n\tauto serialized = QByteArray();\n\tfile.stream >> serialized;\n\tif (!CheckStreamStatus(file.stream)) {\n\t\treturn nullptr;\n\t}\n\treturn MTP::Config::FromSerialized(serialized);\n}\n\ntemplate <typename Callback>\nvoid EnumerateDrafts(\n\t\tconst Data::HistoryDrafts &map,\n\t\tbool supportMode,\n\t\tconst base::flat_map<Data::DraftKey, MessageDraftSource> &sources,\n\t\tCallback &&callback) {\n\tfor (const auto &[key, draft] : map) {\n\t\tif (key.isCloud() || sources.contains(key)) {\n\t\t\tcontinue;\n\t\t} else if (key.isLocal()\n\t\t\t&& (!supportMode || key.topicRootId())) {\n\t\t\tconst auto i = map.find(\n\t\t\t\tData::DraftKey::Cloud(\n\t\t\t\t\tkey.topicRootId(),\n\t\t\t\t\tkey.monoforumPeerId()));\n\t\t\tconst auto cloud = (i != end(map)) ? i->second.get() : nullptr;\n\t\t\tif (Data::DraftsAreEqual(draft.get(), cloud)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tcallback(\n\t\t\tkey,\n\t\t\tdraft->reply,\n\t\t\tdraft->suggest,\n\t\t\tdraft->textWithTags,\n\t\t\tdraft->webpage,\n\t\t\tdraft->cursor);\n\t}\n\tfor (const auto &[key, source] : sources) {\n\t\tconst auto draft = source.draft();\n\t\tconst auto cursor = source.cursor();\n\t\tif (draft.reply.messageId\n\t\t\t|| !draft.textWithTags.text.isEmpty()\n\t\t\t|| cursor != MessageCursor()) {\n\t\t\tcallback(\n\t\t\t\tkey,\n\t\t\t\tdraft.reply,\n\t\t\t\tdraft.suggest,\n\t\t\t\tdraft.textWithTags,\n\t\t\t\tdraft.webpage,\n\t\t\t\tcursor);\n\t\t}\n\t}\n}\n\nvoid Account::registerDraftSource(\n\t\tnot_null<History*> history,\n\t\tData::DraftKey key,\n\t\tMessageDraftSource source) {\n\tExpects(source.draft != nullptr);\n\tExpects(source.cursor != nullptr);\n\n\t_draftSources[history][key] = std::move(source);\n}\n\nvoid Account::unregisterDraftSource(\n\t\tnot_null<History*> history,\n\t\tData::DraftKey key) {\n\tconst auto i = _draftSources.find(history);\n\tif (i != _draftSources.end()) {\n\t\ti->second.remove(key);\n\t\tif (i->second.empty()) {\n\t\t\t_draftSources.erase(i);\n\t\t}\n\t}\n}\n\nvoid Account::writeDrafts(not_null<History*> history) {\n\tconst auto peerId = history->peer->id;\n\tconst auto &map = history->draftsMap();\n\tconst auto supportMode = history->session().supportMode();\n\tconst auto sourcesIt = _draftSources.find(history);\n\tconst auto &sources = (sourcesIt != _draftSources.end())\n\t\t? sourcesIt->second\n\t\t: EmptyMessageDraftSources();\n\tauto count = 0;\n\tEnumerateDrafts(\n\t\tmap,\n\t\tsupportMode,\n\t\tsources,\n\t\t[&](auto&&...) { ++count; });\n\tif (!count) {\n\t\tauto i = _draftsMap.find(peerId);\n\t\tif (i != _draftsMap.cend()) {\n\t\t\tClearKey(i->second, _basePath);\n\t\t\t_draftsMap.erase(i);\n\t\t\twriteMapDelayed();\n\t\t}\n\n\t\t_draftsNotReadMap.remove(peerId);\n\t\treturn;\n\t}\n\n\tauto i = _draftsMap.find(peerId);\n\tif (i == _draftsMap.cend()) {\n\t\ti = _draftsMap.emplace(peerId, GenerateKey(_basePath)).first;\n\t\twriteMapQueued();\n\t}\n\n\tauto size = int(sizeof(quint64) * 2 + sizeof(quint32));\n\tconst auto sizeCallback = [&](\n\t\t\tauto&&, // key\n\t\t\tconst FullReplyTo &reply,\n\t\t\tSuggestOptions suggest,\n\t\t\tconst TextWithTags &text,\n\t\t\tconst Data::WebPageDraft &webpage,\n\t\t\tauto&&) { // cursor\n\t\tsize += sizeof(qint64) // key\n\t\t\t+ Serialize::stringSize(text.text)\n\t\t\t+ TextUtilities::SerializeTagsSize(text.tags)\n\t\t\t+ sizeof(qint64) + sizeof(qint64) // messageId\n\t\t\t+ (sizeof(quint64) * 2) // suggest\n\t\t\t+ Serialize::stringSize(webpage.url)\n\t\t\t+ sizeof(qint32) // webpage.forceLargeMedia\n\t\t\t+ sizeof(qint32) // webpage.forceSmallMedia\n\t\t\t+ sizeof(qint32) // webpage.invert\n\t\t\t+ sizeof(qint32) // webpage.manual\n\t\t\t+ sizeof(qint32); // webpage.removed\n\t};\n\tEnumerateDrafts(\n\t\tmap,\n\t\tsupportMode,\n\t\tsources,\n\t\tsizeCallback);\n\n\tEncryptedDescriptor data(size);\n\tdata.stream\n\t\t<< quint64(kDraftsTag2)\n\t\t<< SerializePeerId(peerId)\n\t\t<< quint32(count);\n\n\tconst auto writeCallback = [&](\n\t\t\tconst Data::DraftKey &key,\n\t\t\tconst FullReplyTo &reply,\n\t\t\tSuggestOptions suggest,\n\t\t\tconst TextWithTags &text,\n\t\t\tconst Data::WebPageDraft &webpage,\n\t\t\tauto&&) { // cursor\n\t\tconst auto serialized = SerializeSuggest(suggest);\n\t\tdata.stream\n\t\t\t<< key.serialize()\n\t\t\t<< text.text\n\t\t\t<< TextUtilities::SerializeTags(text.tags)\n\t\t\t<< qint64(reply.messageId.peer.value)\n\t\t\t<< qint64(reply.messageId.msg.bare)\n\t\t\t<< serialized.first\n\t\t\t<< serialized.second\n\t\t\t<< webpage.url\n\t\t\t<< qint32(webpage.forceLargeMedia ? 1 : 0)\n\t\t\t<< qint32(webpage.forceSmallMedia ? 1 : 0)\n\t\t\t<< qint32(webpage.invert ? 1 : 0)\n\t\t\t<< qint32(webpage.manual ? 1 : 0)\n\t\t\t<< qint32(webpage.removed ? 1 : 0);\n\t};\n\tEnumerateDrafts(\n\t\tmap,\n\t\tsupportMode,\n\t\tsources,\n\t\twriteCallback);\n\n\tFileWriteDescriptor file(i->second, _basePath);\n\tfile.writeEncrypted(data, _localKey);\n\n\t_draftsNotReadMap.remove(peerId);\n}\n\nvoid Account::writeDraftCursors(not_null<History*> history) {\n\tconst auto peerId = history->peer->id;\n\tconst auto &map = history->draftsMap();\n\tconst auto supportMode = history->session().supportMode();\n\tconst auto sourcesIt = _draftSources.find(history);\n\tconst auto &sources = (sourcesIt != _draftSources.end())\n\t\t? sourcesIt->second\n\t\t: EmptyMessageDraftSources();\n\tauto count = 0;\n\tEnumerateDrafts(\n\t\tmap,\n\t\tsupportMode,\n\t\tsources,\n\t\t[&](auto&&...) { ++count; });\n\tif (!count) {\n\t\tclearDraftCursors(peerId);\n\t\treturn;\n\t}\n\tauto i = _draftCursorsMap.find(peerId);\n\tif (i == _draftCursorsMap.cend()) {\n\t\ti = _draftCursorsMap.emplace(peerId, GenerateKey(_basePath)).first;\n\t\twriteMapQueued();\n\t}\n\n\tauto size = int(sizeof(quint64) * 2\n\t\t+ sizeof(quint32)\n\t\t+ (sizeof(qint64) + sizeof(qint32) * 3) * count);\n\n\tEncryptedDescriptor data(size);\n\tdata.stream\n\t\t<< quint64(kMultiDraftCursorsTag)\n\t\t<< SerializePeerId(peerId)\n\t\t<< quint32(count);\n\n\tconst auto writeCallback = [&](\n\t\t\tconst Data::DraftKey &key,\n\t\t\tauto&&, // reply\n\t\t\tauto&&, // suggest\n\t\t\tauto&&, // text\n\t\t\tauto&&, // webpage\n\t\t\tconst MessageCursor &cursor) { // cursor\n\t\tdata.stream\n\t\t\t<< key.serialize()\n\t\t\t<< qint32(cursor.position)\n\t\t\t<< qint32(cursor.anchor)\n\t\t\t<< qint32(cursor.scroll);\n\t};\n\tEnumerateDrafts(\n\t\tmap,\n\t\tsupportMode,\n\t\tsources,\n\t\twriteCallback);\n\n\tFileWriteDescriptor file(i->second, _basePath);\n\tfile.writeEncrypted(data, _localKey);\n}\n\nvoid Account::clearDraftCursors(PeerId peerId) {\n\tconst auto i = _draftCursorsMap.find(peerId);\n\tif (i != _draftCursorsMap.cend()) {\n\t\tClearKey(i->second, _basePath);\n\t\t_draftCursorsMap.erase(i);\n\t\twriteMapDelayed();\n\t}\n}\n\nvoid Account::readDraftCursors(PeerId peerId, Data::HistoryDrafts &map) {\n\tconst auto j = _draftCursorsMap.find(peerId);\n\tif (j == _draftCursorsMap.cend()) {\n\t\treturn;\n\t}\n\n\tFileReadDescriptor draft;\n\tif (!ReadEncryptedFile(draft, j->second, _basePath, _localKey)) {\n\t\tclearDraftCursors(peerId);\n\t\treturn;\n\t}\n\tquint64 tag = 0;\n\tdraft.stream >> tag;\n\tif (tag != kMultiDraftCursorsTag\n\t\t&& tag != kMultiDraftCursorsTagOld\n\t\t&& tag != kMultiDraftTagOld) {\n\t\treadDraftCursorsLegacy(peerId, draft, tag, map);\n\t\treturn;\n\t}\n\tquint64 draftPeerSerialized = 0;\n\tquint32 count = 0;\n\tdraft.stream >> draftPeerSerialized >> count;\n\tconst auto draftPeer = DeserializePeerId(draftPeerSerialized);\n\tif (!count || count > 1000 || draftPeer != peerId) {\n\t\tclearDraftCursors(peerId);\n\t\treturn;\n\t}\n\tconst auto keysWritten = (tag == kMultiDraftCursorsTag);\n\tconst auto keysOld = (tag == kMultiDraftCursorsTagOld);\n\tfor (auto i = 0; i != count; ++i) {\n\t\tqint64 keyValue = 0;\n\t\tqint32 keyValueOld = 0;\n\t\tif (keysWritten) {\n\t\t\tdraft.stream >> keyValue;\n\t\t} else if (keysOld) {\n\t\t\tdraft.stream >> keyValueOld;\n\t\t}\n\t\tconst auto key = keysWritten\n\t\t\t? Data::DraftKey::FromSerialized(keyValue)\n\t\t\t: keysOld\n\t\t\t? Data::DraftKey::FromSerializedOld(keyValueOld)\n\t\t\t: Data::DraftKey::Local(MsgId(), PeerId());\n\t\tqint32 position = 0, anchor = 0, scroll = Ui::kQFixedMax;\n\t\tdraft.stream >> position >> anchor >> scroll;\n\t\tif (const auto i = map.find(key); i != end(map)) {\n\t\t\ti->second->cursor = MessageCursor(position, anchor, scroll);\n\t\t}\n\t}\n}\n\nvoid Account::readDraftCursorsLegacy(\n\t\tPeerId peerId,\n\t\tdetails::FileReadDescriptor &draft,\n\t\tquint64 draftPeerSerialized,\n\t\tData::HistoryDrafts &map) {\n\tqint32 localPosition = 0, localAnchor = 0, localScroll = Ui::kQFixedMax;\n\tqint32 editPosition = 0, editAnchor = 0, editScroll = Ui::kQFixedMax;\n\tdraft.stream >> localPosition >> localAnchor >> localScroll;\n\tif (!draft.stream.atEnd()) {\n\t\tdraft.stream >> editPosition >> editAnchor >> editScroll;\n\t}\n\n\tconst auto draftPeer = DeserializePeerId(draftPeerSerialized);\n\tif (draftPeer != peerId) {\n\t\tclearDraftCursors(peerId);\n\t\treturn;\n\t}\n\n\tif (const auto i = map.find(Data::DraftKey::Local(MsgId(), PeerId()))\n\t\t; i != end(map)) {\n\t\ti->second->cursor = MessageCursor(\n\t\t\tlocalPosition,\n\t\t\tlocalAnchor,\n\t\t\tlocalScroll);\n\t}\n\tif (const auto i = map.find(Data::DraftKey::LocalEdit(MsgId(), PeerId()))\n\t\t; i != end(map)) {\n\t\ti->second->cursor = MessageCursor(\n\t\t\teditPosition,\n\t\t\teditAnchor,\n\t\t\teditScroll);\n\t}\n}\n\nvoid Account::readDraftsWithCursors(not_null<History*> history) {\n\tconst auto guard = gsl::finally([&] {\n\t\tif (const auto migrated = history->migrateFrom()) {\n\t\t\treadDraftsWithCursors(migrated);\n\t\t\tmigrated->clearLocalEditDraft(MsgId(), PeerId());\n\t\t\thistory->takeLocalDraft(migrated);\n\t\t}\n\t});\n\n\tPeerId peerId = history->peer->id;\n\tif (!_draftsNotReadMap.remove(peerId)) {\n\t\tclearDraftCursors(peerId);\n\t\treturn;\n\t}\n\n\tconst auto j = _draftsMap.find(peerId);\n\tif (j == _draftsMap.cend()) {\n\t\tclearDraftCursors(peerId);\n\t\treturn;\n\t}\n\tFileReadDescriptor draft;\n\tif (!ReadEncryptedFile(draft, j->second, _basePath, _localKey)) {\n\t\tClearKey(j->second, _basePath);\n\t\t_draftsMap.erase(j);\n\t\tclearDraftCursors(peerId);\n\t\treturn;\n\t}\n\n\tquint64 tag = 0;\n\tdraft.stream >> tag;\n\tif (tag != kRichDraftsTag\n\t\t&& tag != kMultiDraftTag\n\t\t&& tag != kMultiDraftTagOld) {\n\t\treadDraftsWithCursorsLegacy(history, draft, tag);\n\t\treturn;\n\t}\n\tquint32 count = 0;\n\tquint64 draftPeerSerialized = 0;\n\tdraft.stream >> draftPeerSerialized >> count;\n\tconst auto draftPeer = DeserializePeerId(draftPeerSerialized);\n\tif (!count || count > 1000 || draftPeer != peerId) {\n\t\tClearKey(j->second, _basePath);\n\t\t_draftsMap.erase(j);\n\t\tclearDraftCursors(peerId);\n\t\treturn;\n\t}\n\tauto map = Data::HistoryDrafts();\n\tconst auto keysOld = (tag == kMultiDraftTagOld);\n\tconst auto withSuggest = (tag == kDraftsTag2);\n\tconst auto rich = (tag == kRichDraftsTag) || withSuggest;\n\tfor (auto i = 0; i != count; ++i) {\n\t\tTextWithTags text;\n\t\tQByteArray textTagsSerialized;\n\t\tqint64 keyValue = 0;\n\t\tqint64 messageIdPeer = 0, messageIdMsg = 0;\n\t\tstd::pair<quint64, quint64> suggestSerialized;\n\t\tqint32 keyValueOld = 0;\n\t\tQString webpageUrl;\n\t\tqint32 webpageForceLargeMedia = 0;\n\t\tqint32 webpageForceSmallMedia = 0;\n\t\tqint32 webpageInvert = 0;\n\t\tqint32 webpageManual = 0;\n\t\tqint32 webpageRemoved = 0;\n\t\tif (keysOld) {\n\t\t\tdraft.stream >> keyValueOld;\n\t\t} else {\n\t\t\tdraft.stream >> keyValue;\n\t\t}\n\t\tif (!rich) {\n\t\t\tqint32 uncheckedPreviewState = 0;\n\t\t\tdraft.stream\n\t\t\t\t>> text.text\n\t\t\t\t>> textTagsSerialized\n\t\t\t\t>> messageIdMsg\n\t\t\t\t>> uncheckedPreviewState;\n\t\t\tenum class PreviewState : char {\n\t\t\t\tAllowed,\n\t\t\t\tCancelled,\n\t\t\t\tEmptyOnEdit,\n\t\t\t};\n\t\t\tif (uncheckedPreviewState == int(PreviewState::Cancelled)) {\n\t\t\t\twebpageRemoved = 1;\n\t\t\t}\n\t\t\tmessageIdPeer = peerId.value;\n\t\t} else {\n\t\t\tdraft.stream\n\t\t\t\t>> text.text\n\t\t\t\t>> textTagsSerialized\n\t\t\t\t>> messageIdPeer\n\t\t\t\t>> messageIdMsg;\n\t\t\tif (withSuggest) {\n\t\t\t\tdraft.stream\n\t\t\t\t\t>> suggestSerialized.first\n\t\t\t\t\t>> suggestSerialized.second;\n\t\t\t}\n\t\t\tdraft.stream\n\t\t\t\t>> webpageUrl\n\t\t\t\t>> webpageForceLargeMedia\n\t\t\t\t>> webpageForceSmallMedia\n\t\t\t\t>> webpageInvert\n\t\t\t\t>> webpageManual\n\t\t\t\t>> webpageRemoved;\n\t\t}\n\t\ttext.tags = TextUtilities::DeserializeTags(\n\t\t\ttextTagsSerialized,\n\t\t\ttext.text.size());\n\t\tconst auto key = keysOld\n\t\t\t? Data::DraftKey::FromSerializedOld(keyValueOld)\n\t\t\t: Data::DraftKey::FromSerialized(keyValue);\n\t\tif (key && !key.isCloud()) {\n\t\t\tmap.emplace(key, std::make_unique<Data::Draft>(\n\t\t\t\ttext,\n\t\t\t\tFullReplyTo{\n\t\t\t\t\t.messageId = FullMsgId(\n\t\t\t\t\t\tPeerId(messageIdPeer),\n\t\t\t\t\t\tMsgId(messageIdMsg)),\n\t\t\t\t\t.topicRootId = key.topicRootId(),\n\t\t\t\t},\n\t\t\t\tDeserializeSuggest(suggestSerialized),\n\t\t\t\tMessageCursor(),\n\t\t\t\tData::WebPageDraft{\n\t\t\t\t\t.url = webpageUrl,\n\t\t\t\t\t.forceLargeMedia = (webpageForceLargeMedia == 1),\n\t\t\t\t\t.forceSmallMedia = (webpageForceSmallMedia == 1),\n\t\t\t\t\t.invert = (webpageInvert == 1),\n\t\t\t\t\t.manual = (webpageManual == 1),\n\t\t\t\t\t.removed = (webpageRemoved == 1),\n\t\t\t\t}));\n\t\t}\n\t}\n\tif (draft.stream.status() != QDataStream::Ok) {\n\t\tClearKey(j->second, _basePath);\n\t\t_draftsMap.erase(j);\n\t\tclearDraftCursors(peerId);\n\t\treturn;\n\t}\n\treadDraftCursors(peerId, map);\n\thistory->setDraftsMap(std::move(map));\n}\n\nvoid Account::readDraftsWithCursorsLegacy(\n\t\tnot_null<History*> history,\n\t\tdetails::FileReadDescriptor &draft,\n\t\tquint64 draftPeerSerialized) {\n\tTextWithTags msgData, editData;\n\tQByteArray msgTagsSerialized, editTagsSerialized;\n\tqint32 msgReplyTo = 0, msgPreviewCancelled = 0, editMsgId = 0, editPreviewCancelled = 0;\n\tdraft.stream >> msgData.text;\n\tif (draft.version >= 9048) {\n\t\tdraft.stream >> msgTagsSerialized;\n\t}\n\tif (draft.version >= 7021) {\n\t\tdraft.stream >> msgReplyTo;\n\t\tif (draft.version >= 8001) {\n\t\t\tdraft.stream >> msgPreviewCancelled;\n\t\t\tif (!draft.stream.atEnd()) {\n\t\t\t\tdraft.stream >> editData.text;\n\t\t\t\tif (draft.version >= 9048) {\n\t\t\t\t\tdraft.stream >> editTagsSerialized;\n\t\t\t\t}\n\t\t\t\tdraft.stream >> editMsgId >> editPreviewCancelled;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto peerId = history->peer->id;\n\tconst auto draftPeer = DeserializePeerId(draftPeerSerialized);\n\tif (draftPeer != peerId) {\n\t\tconst auto j = _draftsMap.find(peerId);\n\t\tif (j != _draftsMap.cend()) {\n\t\t\tClearKey(j->second, _basePath);\n\t\t\t_draftsMap.erase(j);\n\t\t}\n\t\tclearDraftCursors(peerId);\n\t\treturn;\n\t}\n\n\tmsgData.tags = TextUtilities::DeserializeTags(\n\t\tmsgTagsSerialized,\n\t\tmsgData.text.size());\n\teditData.tags = TextUtilities::DeserializeTags(\n\t\teditTagsSerialized,\n\t\teditData.text.size());\n\n\tconst auto topicRootId = MsgId();\n\tconst auto monoforumPeerId = PeerId();\n\tauto map = base::flat_map<Data::DraftKey, std::unique_ptr<Data::Draft>>();\n\tif (!msgData.text.isEmpty() || msgReplyTo) {\n\t\tmap.emplace(\n\t\t\tData::DraftKey::Local(topicRootId, monoforumPeerId),\n\t\t\tstd::make_unique<Data::Draft>(\n\t\t\t\tmsgData,\n\t\t\t\tFullReplyTo{ FullMsgId(peerId, MsgId(msgReplyTo)) },\n\t\t\t\tSuggestOptions(),\n\t\t\t\tMessageCursor(),\n\t\t\t\tData::WebPageDraft{\n\t\t\t\t\t.removed = (msgPreviewCancelled == 1),\n\t\t\t\t}));\n\t}\n\tif (editMsgId) {\n\t\tmap.emplace(\n\t\t\tData::DraftKey::LocalEdit(topicRootId, monoforumPeerId),\n\t\t\tstd::make_unique<Data::Draft>(\n\t\t\t\teditData,\n\t\t\t\tFullReplyTo{ FullMsgId(peerId, editMsgId) },\n\t\t\t\tSuggestOptions(),\n\t\t\t\tMessageCursor(),\n\t\t\t\tData::WebPageDraft{\n\t\t\t\t\t.removed = (editPreviewCancelled == 1),\n\t\t\t\t}));\n\t}\n\treadDraftCursors(peerId, map);\n\thistory->setDraftsMap(std::move(map));\n}\n\nbool Account::hasDraftCursors(PeerId peer) {\n\treturn _draftCursorsMap.contains(peer);\n}\n\nbool Account::hasDraft(PeerId peer) {\n\treturn _draftsMap.contains(peer);\n}\n\nvoid Account::writeFileLocation(MediaKey location, const Core::FileLocation &local) {\n\tif (local.fname.isEmpty()) {\n\t\treturn;\n\t}\n\tif (!local.inMediaCache()) {\n\t\tconst auto aliasIt = _fileLocationAliases.constFind(location);\n\t\tif (aliasIt != _fileLocationAliases.cend()) {\n\t\t\tlocation = aliasIt.value();\n\t\t}\n\n\t\tconst auto i = _fileLocationPairs.find(local.fname);\n\t\tif (i != _fileLocationPairs.cend()) {\n\t\t\tif (i.value().second == local) {\n\t\t\t\tif (i.value().first != location) {\n\t\t\t\t\t_fileLocationAliases.insert(location, i.value().first);\n\t\t\t\t\twriteLocationsQueued();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (i.value().first != location) {\n\t\t\t\tfor (auto j = _fileLocations.find(i.value().first), e = _fileLocations.end(); (j != e) && (j.key() == i.value().first); ++j) {\n\t\t\t\t\tif (j.value() == i.value().second) {\n\t\t\t\t\t\t_fileLocations.erase(j);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t_fileLocationPairs.erase(i);\n\t\t\t}\n\t\t}\n\t\t_fileLocationPairs.insert(local.fname, { location, local });\n\t} else {\n\t\tfor (auto i = _fileLocations.find(location); (i != _fileLocations.end()) && (i.key() == location);) {\n\t\t\tif (i.value().inMediaCache() || i.value().check()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ti = _fileLocations.erase(i);\n\t\t}\n\t}\n\t_fileLocations.insert(location, local);\n\twriteLocationsQueued();\n}\n\nvoid Account::removeFileLocation(MediaKey location) {\n\tauto i = _fileLocations.find(location);\n\tif (i == _fileLocations.end()) {\n\t\treturn;\n\t}\n\twhile (i != _fileLocations.end() && (i.key() == location)) {\n\t\ti = _fileLocations.erase(i);\n\t}\n\twriteLocationsQueued();\n}\n\nCore::FileLocation Account::readFileLocation(MediaKey location) {\n\tconst auto aliasIt = _fileLocationAliases.constFind(location);\n\tif (aliasIt != _fileLocationAliases.cend()) {\n\t\tlocation = aliasIt.value();\n\t}\n\n\tfor (auto i = _fileLocations.find(location); (i != _fileLocations.end()) && (i.key() == location);) {\n\t\tif (!i.value().inMediaCache() && !i.value().check()) {\n\t\t\t_fileLocationPairs.remove(i.value().fname);\n\t\t\ti = _fileLocations.erase(i);\n\t\t\twriteLocationsDelayed();\n\t\t\tcontinue;\n\t\t}\n\t\treturn i.value();\n\t}\n\treturn Core::FileLocation();\n}\n\nEncryptionKey Account::cacheKey() const {\n\tExpects(_localKey != nullptr);\n\n\treturn EncryptionKey(bytes::make_vector(_localKey->data()));\n}\n\nEncryptionKey Account::cacheBigFileKey() const {\n\treturn cacheKey();\n}\n\nQString Account::cachePath() const {\n\tExpects(!_databasePath.isEmpty());\n\n\treturn _databasePath + \"cache\";\n}\n\nCache::Database::Settings Account::cacheSettings() const {\n\tauto result = Cache::Database::Settings();\n\tresult.clearOnWrongKey = true;\n\tresult.totalSizeLimit = _cacheTotalSizeLimit;\n\tresult.totalTimeLimit = _cacheTotalTimeLimit;\n\tresult.maxDataSize = kMaxFileInMemory;\n\treturn result;\n}\n\nvoid Account::updateCacheSettings(\n\t\tCache::Database::SettingsUpdate &update,\n\t\tCache::Database::SettingsUpdate &updateBig) {\n\tExpects(update.totalSizeLimit > Database::Settings().maxDataSize);\n\tExpects(update.totalTimeLimit >= 0);\n\tExpects(updateBig.totalSizeLimit > Database::Settings().maxDataSize);\n\tExpects(updateBig.totalTimeLimit >= 0);\n\n\tif (_cacheTotalSizeLimit == update.totalSizeLimit\n\t\t&& _cacheTotalTimeLimit == update.totalTimeLimit\n\t\t&& _cacheBigFileTotalSizeLimit == updateBig.totalSizeLimit\n\t\t&& _cacheBigFileTotalTimeLimit == updateBig.totalTimeLimit) {\n\t\treturn;\n\t}\n\t_cacheTotalSizeLimit = update.totalSizeLimit;\n\t_cacheTotalTimeLimit = update.totalTimeLimit;\n\t_cacheBigFileTotalSizeLimit = updateBig.totalSizeLimit;\n\t_cacheBigFileTotalTimeLimit = updateBig.totalTimeLimit;\n\twriteSessionSettings();\n}\n\nQString Account::cacheBigFilePath() const {\n\tExpects(!_databasePath.isEmpty());\n\n\treturn _databasePath + \"media_cache\";\n}\n\nCache::Database::Settings Account::cacheBigFileSettings() const {\n\tauto result = Cache::Database::Settings();\n\tresult.clearOnWrongKey = true;\n\tresult.totalSizeLimit = _cacheBigFileTotalSizeLimit;\n\tresult.totalTimeLimit = _cacheBigFileTotalTimeLimit;\n\tresult.maxDataSize = kMaxFileInMemory;\n\treturn result;\n}\n\nvoid Account::writeStickerSet(\n\t\tQDataStream &stream,\n\t\tconst Data::StickersSet &set) {\n\tusing SetFlag = Data::StickersSetFlag;\n\tconst auto writeInfo = [&](int count) {\n\t\tstream\n\t\t\t<< quint64(set.id)\n\t\t\t<< quint64(set.accessHash)\n\t\t\t<< quint64(set.hash)\n\t\t\t<< set.title\n\t\t\t<< set.shortName\n\t\t\t<< qint32(count)\n\t\t\t<< qint32(set.flags)\n\t\t\t<< qint32(set.installDate)\n\t\t\t<< quint64(set.thumbnailDocumentId)\n\t\t\t<< qint32(set.thumbnailType());\n\t\tSerialize::writeImageLocation(stream, set.thumbnailLocation());\n\t};\n\tif (set.flags & SetFlag::NotLoaded) {\n\t\twriteInfo(-set.count);\n\t\treturn;\n\t} else if (set.stickers.isEmpty()) {\n\t\treturn;\n\t}\n\n\twriteInfo(set.stickers.size());\n\tfor (const auto &sticker : set.stickers) {\n\t\tSerialize::Document::writeToStream(stream, sticker);\n\t}\n\tstream << qint32(set.dates.size());\n\tif (!set.dates.empty()) {\n\t\tAssert(set.dates.size() == set.stickers.size());\n\t\tfor (const auto date : set.dates) {\n\t\t\tstream << qint32(date);\n\t\t}\n\t}\n\tstream << qint32(set.emoji.size());\n\tfor (auto j = set.emoji.cbegin(), e = set.emoji.cend(); j != e; ++j) {\n\t\tstream << j->first->id() << qint32(j->second.size());\n\t\tfor (const auto sticker : j->second) {\n\t\t\tstream << quint64(sticker->id);\n\t\t}\n\t}\n}\n\n// In generic method _writeStickerSets() we look through all the sets and call a\n// callback on each set to see, if we write it, skip it or abort the whole write.\nenum class StickerSetCheckResult {\n\tWrite,\n\tSkip,\n\tAbort,\n};\n\n// CheckSet is a functor on Data::StickersSet, which returns a StickerSetCheckResult.\ntemplate <typename CheckSet>\nvoid Account::writeStickerSets(\n\t\tFileKey &stickersKey,\n\t\tCheckSet checkSet,\n\t\tconst Data::StickersSetsOrder &order) {\n\tusing SetFlag = Data::StickersSetFlag;\n\n\tconst auto &sets = _owner->session().data().stickers().sets();\n\tif (sets.empty()) {\n\t\tif (stickersKey) {\n\t\t\tClearKey(stickersKey, _basePath);\n\t\t\tstickersKey = 0;\n\t\t\twriteMapDelayed();\n\t\t}\n\t\treturn;\n\t}\n\n\t// versionTag + version + count\n\tquint32 size = sizeof(quint32) + sizeof(qint32) + sizeof(qint32);\n\n\tint32 setsCount = 0;\n\tfor (const auto &[id, set] : sets) {\n\t\tconst auto raw = set.get();\n\t\tauto result = checkSet(*raw);\n\t\tif (result == StickerSetCheckResult::Abort) {\n\t\t\treturn;\n\t\t} else if (result == StickerSetCheckResult::Skip) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// id\n\t\t// + accessHash\n\t\t// + hash\n\t\t// + title\n\t\t// + shortName\n\t\t// + stickersCount\n\t\t// + flags\n\t\t// + installDate\n\t\t// + thumbnailDocumentId\n\t\t// + thumbnailType\n\t\t// + thumbnailLocation\n\t\tsize += sizeof(quint64) * 3\n\t\t\t+ Serialize::stringSize(raw->title)\n\t\t\t+ Serialize::stringSize(raw->shortName)\n\t\t\t+ sizeof(qint32) * 3\n\t\t\t+ sizeof(quint64)\n\t\t\t+ sizeof(qint32)\n\t\t\t+ Serialize::imageLocationSize(raw->thumbnailLocation());\n\t\tif (raw->flags & SetFlag::NotLoaded) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const auto sticker : std::as_const(raw->stickers)) {\n\t\t\tsize += Serialize::Document::sizeInStream(sticker);\n\t\t}\n\n\t\tsize += sizeof(qint32); // datesCount\n\t\tif (!raw->dates.empty()) {\n\t\t\tAssert(raw->stickers.size() == raw->dates.size());\n\t\t\tsize += raw->dates.size() * sizeof(qint32);\n\t\t}\n\n\t\tsize += sizeof(qint32); // emojiCount\n\t\tfor (auto j = raw->emoji.cbegin(), e = raw->emoji.cend(); j != e; ++j) {\n\t\t\tsize += Serialize::stringSize(j->first->id()) + sizeof(qint32) + (j->second.size() * sizeof(quint64));\n\t\t}\n\n\t\t++setsCount;\n\t}\n\tif (!setsCount && order.isEmpty()) {\n\t\tif (stickersKey) {\n\t\t\tClearKey(stickersKey, _basePath);\n\t\t\tstickersKey = 0;\n\t\t\twriteMapDelayed();\n\t\t}\n\t\treturn;\n\t}\n\tsize += sizeof(qint32) + (order.size() * sizeof(quint64));\n\n\tif (!stickersKey) {\n\t\tstickersKey = GenerateKey(_basePath);\n\t\twriteMapQueued();\n\t}\n\tEncryptedDescriptor data(size);\n\tdata.stream\n\t\t<< quint32(kStickersVersionTag)\n\t\t<< qint32(kStickersSerializeVersion)\n\t\t<< qint32(setsCount);\n\tfor (const auto &[id, set] : sets) {\n\t\tauto result = checkSet(*set);\n\t\tif (result == StickerSetCheckResult::Abort) {\n\t\t\treturn;\n\t\t} else if (result == StickerSetCheckResult::Skip) {\n\t\t\tcontinue;\n\t\t}\n\t\twriteStickerSet(data.stream, *set);\n\t}\n\tdata.stream << order;\n\n\tFileWriteDescriptor file(stickersKey, _basePath);\n\tfile.writeEncrypted(data, _localKey);\n}\n\nvoid Account::readStickerSets(\n\t\tFileKey &stickersKey,\n\t\tData::StickersSetsOrder *outOrder,\n\t\tData::StickersSetFlags readingFlags) {\n\tusing SetFlag = Data::StickersSetFlag;\n\n\tif (!stickersKey) {\n\t\treturn;\n\t}\n\n\tFileReadDescriptor stickers;\n\tif (!ReadEncryptedFile(stickers, stickersKey, _basePath, _localKey)) {\n\t\tClearKey(stickersKey, _basePath);\n\t\tstickersKey = 0;\n\t\twriteMapDelayed();\n\t\treturn;\n\t}\n\n\tconst auto failed = [&] {\n\t\tClearKey(stickersKey, _basePath);\n\t\tstickersKey = 0;\n\t};\n\n\tauto &sets = _owner->session().data().stickers().setsRef();\n\tif (outOrder) outOrder->clear();\n\n\tquint32 versionTag = 0;\n\tqint32 version = 0;\n\tstickers.stream >> versionTag >> version;\n\tif (versionTag != kStickersVersionTag || version < 2) {\n\t\t// Old data, without sticker set thumbnails.\n\t\treturn failed();\n\t}\n\tqint32 count = 0;\n\tstickers.stream >> count;\n\tif (!CheckStreamStatus(stickers.stream)\n\t\t|| (count < 0)\n\t\t|| (count > kMaxSavedStickerSetsCount)) {\n\t\treturn failed();\n\t}\n\tfor (auto i = 0; i != count; ++i) {\n\t\tquint64 setId = 0, setAccessHash = 0, setHash = 0;\n\t\tquint64 setThumbnailDocumentId = 0;\n\t\tQString setTitle, setShortName;\n\t\tqint32 scnt = 0;\n\t\tqint32 setInstallDate = 0;\n\t\tData::StickersSetFlags setFlags = 0;\n\t\tqint32 setFlagsValue = 0;\n\t\tqint32 setThumbnailType = qint32(StickerType::Webp);\n\t\tImageLocation setThumbnail;\n\n\t\tstickers.stream\n\t\t\t>> setId\n\t\t\t>> setAccessHash\n\t\t\t>> setHash\n\t\t\t>> setTitle\n\t\t\t>> setShortName\n\t\t\t>> scnt\n\t\t\t>> setFlagsValue\n\t\t\t>> setInstallDate;\n\t\tif (version > 2) {\n\t\t\tstickers.stream >> setThumbnailDocumentId;\n\t\t\tif (version > 3) {\n\t\t\t\tstickers.stream >> setThumbnailType;\n\t\t\t}\n\t\t}\n\n\t\tconstexpr auto kLegacyFlagWebm = (1 << 8);\n\t\tif ((version < 4) && (setFlagsValue & kLegacyFlagWebm)) {\n\t\t\tsetThumbnailType = qint32(StickerType::Webm);\n\t\t}\n\t\tconst auto thumbnail = Serialize::readImageLocation(\n\t\t\tstickers.version,\n\t\t\tstickers.stream);\n\t\tif (!thumbnail || !CheckStreamStatus(stickers.stream)) {\n\t\t\treturn failed();\n\t\t} else if (thumbnail->valid() && thumbnail->isLegacy()) {\n\t\t\t// No thumb_version information in legacy location.\n\t\t\treturn failed();\n\t\t} else {\n\t\t\tsetThumbnail = *thumbnail;\n\t\t}\n\n\t\tsetFlags = Data::StickersSetFlags::from_raw(setFlagsValue);\n\t\tif (setId == Data::Stickers::DefaultSetId) {\n\t\t\tsetTitle = tr::lng_stickers_default_set(tr::now);\n\t\t\tsetFlags |= SetFlag::Official | SetFlag::Special;\n\t\t} else if (setId == Data::Stickers::CustomSetId) {\n\t\t\tsetTitle = u\"Custom stickers\"_q;\n\t\t\tsetFlags |= SetFlag::Special;\n\t\t} else if ((setId == Data::Stickers::CloudRecentSetId)\n\t\t\t\t|| (setId == Data::Stickers::CloudRecentAttachedSetId)) {\n\t\t\tsetTitle = tr::lng_recent_stickers(tr::now);\n\t\t\tsetFlags |= SetFlag::Special;\n\t\t} else if (setId == Data::Stickers::FavedSetId) {\n\t\t\tsetTitle = Lang::Hard::FavedSetTitle();\n\t\t\tsetFlags |= SetFlag::Special;\n\t\t} else if (!setId) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto it = sets.find(setId);\n\t\tauto settingSet = (it == sets.cend());\n\t\tif (settingSet) {\n\t\t\t// We will set this flags from order lists when reading those stickers.\n\t\t\tsetFlags &= ~(SetFlag::Installed | SetFlag::Featured);\n\t\t\tit = sets.emplace(setId, std::make_unique<Data::StickersSet>(\n\t\t\t\t&_owner->session().data(),\n\t\t\t\tsetId,\n\t\t\t\tsetAccessHash,\n\t\t\t\tsetHash,\n\t\t\t\tsetTitle,\n\t\t\t\tsetShortName,\n\t\t\t\t0,\n\t\t\t\tsetFlags,\n\t\t\t\tsetInstallDate)).first;\n\t\t\tit->second->thumbnailDocumentId = setThumbnailDocumentId;\n\t\t}\n\t\tconst auto set = it->second.get();\n\t\tconst auto inputSet = set->identifier();\n\t\tconst auto fillStickers = set->stickers.isEmpty();\n\n\t\tif (scnt < 0) { // disabled not loaded set\n\t\t\tif (!set->count || fillStickers) {\n\t\t\t\tset->count = -scnt;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (fillStickers) {\n\t\t\tset->stickers.reserve(scnt);\n\t\t\tset->count = 0;\n\t\t}\n\n\t\tSerialize::Document::StickerSetInfo info(\n\t\t\tsetId,\n\t\t\tsetAccessHash,\n\t\t\tsetShortName);\n\t\tbase::flat_set<DocumentId> read;\n\t\tfor (int32 j = 0; j < scnt; ++j) {\n\t\t\tauto document = Serialize::Document::readStickerFromStream(\n\t\t\t\t&_owner->session(),\n\t\t\t\tstickers.version,\n\t\t\t\tstickers.stream, info);\n\t\t\tif (!CheckStreamStatus(stickers.stream)) {\n\t\t\t\treturn failed();\n\t\t\t} else if (!document\n\t\t\t\t|| !document->sticker()\n\t\t\t\t|| read.contains(document->id)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tread.emplace(document->id);\n\t\t\tif (fillStickers) {\n\t\t\t\tset->stickers.push_back(document);\n\t\t\t\tif (!(set->flags & SetFlag::Special)) {\n\t\t\t\t\tif (!document->sticker()->set.id) {\n\t\t\t\t\t\tdocument->sticker()->set = inputSet;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t++set->count;\n\t\t\t}\n\t\t}\n\n\t\tqint32 datesCount = 0;\n\t\tstickers.stream >> datesCount;\n\t\tif (datesCount > 0) {\n\t\t\tif (datesCount != scnt) {\n\t\t\t\treturn failed();\n\t\t\t}\n\t\t\tconst auto fillDates\n\t\t\t\t= ((set->id == Data::Stickers::CloudRecentSetId)\n\t\t\t\t\t|| (set->id == Data::Stickers::CloudRecentAttachedSetId))\n\t\t\t\t&& (set->stickers.size() == datesCount);\n\t\t\tif (fillDates) {\n\t\t\t\tset->dates.clear();\n\t\t\t\tset->dates.reserve(datesCount);\n\t\t\t}\n\t\t\tfor (auto i = 0; i != datesCount; ++i) {\n\t\t\t\tqint32 date = 0;\n\t\t\t\tstickers.stream >> date;\n\t\t\t\tif (fillDates) {\n\t\t\t\t\tset->dates.push_back(TimeId(date));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tqint32 emojiCount = 0;\n\t\tstickers.stream >> emojiCount;\n\t\tif (!CheckStreamStatus(stickers.stream) || emojiCount < 0) {\n\t\t\treturn failed();\n\t\t}\n\t\tfor (int32 j = 0; j < emojiCount; ++j) {\n\t\t\tQString emojiString;\n\t\t\tqint32 stickersCount;\n\t\t\tstickers.stream >> emojiString >> stickersCount;\n\t\t\tData::StickersPack pack;\n\t\t\tpack.reserve(stickersCount);\n\t\t\tfor (int32 k = 0; k < stickersCount; ++k) {\n\t\t\t\tquint64 id;\n\t\t\t\tstickers.stream >> id;\n\t\t\t\tconst auto doc = _owner->session().data().document(id);\n\t\t\t\tif (!doc->sticker()) continue;\n\n\t\t\t\tpack.push_back(doc);\n\t\t\t}\n\t\t\tif (fillStickers) {\n\t\t\t\tif (auto emoji = Ui::Emoji::Find(emojiString)) {\n\t\t\t\t\temoji = emoji->original();\n\t\t\t\t\tset->emoji[emoji] = std::move(pack);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (settingSet) {\n\t\t\tif (version < 4\n\t\t\t\t&& setThumbnailType == qint32(StickerType::Webp)\n\t\t\t\t&& !set->stickers.empty()\n\t\t\t\t&& set->stickers.front()->sticker()) {\n\t\t\t\tconst auto first = set->stickers.front();\n\t\t\t\tsetThumbnailType = qint32(first->sticker()->type);\n\t\t\t}\n\t\t\tconst auto thumbType = [&] {\n\t\t\t\tswitch (setThumbnailType) {\n\t\t\t\tcase qint32(StickerType::Webp): return StickerType::Webp;\n\t\t\t\tcase qint32(StickerType::Tgs): return StickerType::Tgs;\n\t\t\t\tcase qint32(StickerType::Webm): return StickerType::Webm;\n\t\t\t\t}\n\t\t\t\treturn StickerType::Webp;\n\t\t\t}();\n\t\t\tset->setThumbnail(\n\t\t\t\tImageWithLocation{ .location = setThumbnail }, thumbType);\n\t\t}\n\t}\n\n\t// Read orders of installed and featured stickers.\n\tif (outOrder) {\n\t\tauto outOrderCount = quint32();\n\t\tstickers.stream >> outOrderCount;\n\t\tif (!CheckStreamStatus(stickers.stream) || outOrderCount > 1000) {\n\t\t\treturn failed();\n\t\t}\n\t\toutOrder->reserve(outOrderCount);\n\t\tfor (auto i = 0; i != outOrderCount; ++i) {\n\t\t\tauto value = uint64();\n\t\t\tstickers.stream >> value;\n\t\t\tif (!CheckStreamStatus(stickers.stream)) {\n\t\t\t\toutOrder->clear();\n\t\t\t\treturn failed();\n\t\t\t}\n\t\t\toutOrder->push_back(value);\n\t\t}\n\t}\n\tif (!CheckStreamStatus(stickers.stream)) {\n\t\treturn failed();\n\t}\n\n\t// Set flags that we dropped above from the order.\n\tif (readingFlags && outOrder) {\n\t\tfor (const auto setId : std::as_const(*outOrder)) {\n\t\t\tauto it = sets.find(setId);\n\t\t\tif (it != sets.cend()) {\n\t\t\t\tconst auto set = it->second.get();\n\t\t\t\tset->flags |= readingFlags;\n\t\t\t\tif ((readingFlags == SetFlag::Installed)\n\t\t\t\t\t&& !set->installDate) {\n\t\t\t\t\tset->installDate = kDefaultStickerInstallDate;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Account::writeInstalledStickers() {\n\tusing SetFlag = Data::StickersSetFlag;\n\n\twriteStickerSets(_installedStickersKey, [](const Data::StickersSet &set) {\n\t\tif (set.id == Data::Stickers::CloudRecentSetId\n\t\t\t|| set.id == Data::Stickers::FavedSetId\n\t\t\t|| set.id == Data::Stickers::CloudRecentAttachedSetId\n\t\t\t|| set.id == Data::Stickers::CollectibleSetId) {\n\t\t\t// separate files for them\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t} else if (set.flags & SetFlag::Special) {\n\t\t\tif (set.stickers.isEmpty()) { // all other special are \"installed\"\n\t\t\t\treturn StickerSetCheckResult::Skip;\n\t\t\t}\n\t\t} else if (!(set.flags & SetFlag::Installed)\n\t\t\t|| (set.flags & SetFlag::Archived)\n\t\t\t|| (set.type() != Data::StickersType::Stickers)) {\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t} else if (set.flags & SetFlag::NotLoaded) {\n\t\t\t// waiting to receive\n\t\t\treturn StickerSetCheckResult::Abort;\n\t\t} else if (set.stickers.isEmpty()) {\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t}\n\t\treturn StickerSetCheckResult::Write;\n\t}, _owner->session().data().stickers().setsOrder());\n}\n\nvoid Account::writeFeaturedStickers() {\n\tusing SetFlag = Data::StickersSetFlag;\n\n\twriteStickerSets(_featuredStickersKey, [](const Data::StickersSet &set) {\n\t\tif (set.id == Data::Stickers::CloudRecentSetId\n\t\t\t|| set.id == Data::Stickers::FavedSetId\n\t\t\t|| set.id == Data::Stickers::CloudRecentAttachedSetId\n\t\t\t|| set.id == Data::Stickers::CollectibleSetId) {\n\t\t\t// separate files for them\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t} else if ((set.flags & SetFlag::Special)\n\t\t\t|| !(set.flags & SetFlag::Featured)\n\t\t\t|| (set.type() != Data::StickersType::Stickers)) {\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t} else if (set.flags & SetFlag::NotLoaded) { // waiting to receive\n\t\t\treturn StickerSetCheckResult::Abort;\n\t\t} else if (set.stickers.isEmpty()) {\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t}\n\t\treturn StickerSetCheckResult::Write;\n\t}, _owner->session().data().stickers().featuredSetsOrder());\n}\n\nvoid Account::writeFeaturedCustomEmoji() {\n\tusing SetFlag = Data::StickersSetFlag;\n\n\twriteStickerSets(_featuredCustomEmojiKey, [](const Data::StickersSet &set) {\n\t\tif (!(set.flags & SetFlag::Featured)\n\t\t\t|| (set.type() != Data::StickersType::Emoji)) {\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t} else if (set.flags & SetFlag::NotLoaded) { // waiting to receive\n\t\t\treturn StickerSetCheckResult::Abort;\n\t\t} else if (set.stickers.isEmpty()) {\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t}\n\t\treturn StickerSetCheckResult::Write;\n\t}, _owner->session().data().stickers().featuredEmojiSetsOrder());\n}\n\nvoid Account::writeRecentStickers() {\n\twriteStickerSets(_recentStickersKey, [](const Data::StickersSet &set) {\n\t\tif (set.id != Data::Stickers::CloudRecentSetId\n\t\t\t|| set.stickers.isEmpty()) {\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t}\n\t\treturn StickerSetCheckResult::Write;\n\t}, Data::StickersSetsOrder());\n}\n\nvoid Account::writeFavedStickers() {\n\twriteStickerSets(_favedStickersKey, [](const Data::StickersSet &set) {\n\t\tif (set.id != Data::Stickers::FavedSetId || set.stickers.isEmpty()) {\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t}\n\t\treturn StickerSetCheckResult::Write;\n\t}, Data::StickersSetsOrder());\n}\n\nvoid Account::writeArchivedStickers() {\n\tusing SetFlag = Data::StickersSetFlag;\n\n\twriteStickerSets(_archivedStickersKey, [](const Data::StickersSet &set) {\n\t\tif (!(set.flags & SetFlag::Archived)\n\t\t\t|| (set.type() != Data::StickersType::Stickers)\n\t\t\t|| set.stickers.isEmpty()) {\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t}\n\t\treturn StickerSetCheckResult::Write;\n\t}, _owner->session().data().stickers().archivedSetsOrder());\n}\n\nvoid Account::writeArchivedMasks() {\n\tusing SetFlag = Data::StickersSetFlag;\n\n\twriteStickerSets(_archivedStickersKey, [](const Data::StickersSet &set) {\n\t\tif (!(set.flags & SetFlag::Archived)\n\t\t\t|| (set.type() != Data::StickersType::Masks)\n\t\t\t|| set.stickers.isEmpty()) {\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t}\n\t\treturn StickerSetCheckResult::Write;\n\t}, _owner->session().data().stickers().archivedMaskSetsOrder());\n}\n\nvoid Account::writeInstalledMasks() {\n\tusing SetFlag = Data::StickersSetFlag;\n\n\twriteStickerSets(_installedMasksKey, [](const Data::StickersSet &set) {\n\t\tif (!(set.flags & SetFlag::Installed)\n\t\t\t|| (set.flags & SetFlag::Archived)\n\t\t\t|| (set.type() != Data::StickersType::Masks)\n\t\t\t|| set.stickers.isEmpty()) {\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t}\n\t\treturn StickerSetCheckResult::Write;\n\t}, _owner->session().data().stickers().maskSetsOrder());\n}\n\nvoid Account::writeRecentMasks() {\n\twriteStickerSets(_recentMasksKey, [](const Data::StickersSet &set) {\n\t\tif (set.id != Data::Stickers::CloudRecentAttachedSetId\n\t\t\t|| set.stickers.isEmpty()) {\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t}\n\t\treturn StickerSetCheckResult::Write;\n\t}, Data::StickersSetsOrder());\n}\n\nvoid Account::writeInstalledCustomEmoji() {\n\tusing SetFlag = Data::StickersSetFlag;\n\n\twriteStickerSets(_installedCustomEmojiKey, [](const Data::StickersSet &set) {\n\t\tif (!(set.flags & SetFlag::Installed)\n\t\t\t|| (set.flags & SetFlag::Archived)\n\t\t\t|| (set.type() != Data::StickersType::Emoji)) {\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t} else if (set.flags & SetFlag::NotLoaded) {\n\t\t\t// waiting to receive\n\t\t\treturn StickerSetCheckResult::Abort;\n\t\t} else if (set.stickers.isEmpty()) {\n\t\t\treturn StickerSetCheckResult::Skip;\n\t\t}\n\t\treturn StickerSetCheckResult::Write;\n\t}, _owner->session().data().stickers().emojiSetsOrder());\n}\n\nvoid Account::importOldRecentStickers() {\n\tusing SetFlag = Data::StickersSetFlag;\n\n\tif (!_recentStickersKeyOld) {\n\t\treturn;\n\t}\n\n\tFileReadDescriptor stickers;\n\tif (!ReadEncryptedFile(stickers, _recentStickersKeyOld, _basePath, _localKey)) {\n\t\tClearKey(_recentStickersKeyOld, _basePath);\n\t\t_recentStickersKeyOld = 0;\n\t\twriteMapDelayed();\n\t\treturn;\n\t}\n\n\tauto &sets = _owner->session().data().stickers().setsRef();\n\tsets.clear();\n\n\tauto &order = _owner->session().data().stickers().setsOrderRef();\n\torder.clear();\n\n\tauto &recent = cRefRecentStickers();\n\trecent.clear();\n\n\tconst auto def = sets.emplace(\n\t\tData::Stickers::DefaultSetId,\n\t\tstd::make_unique<Data::StickersSet>(\n\t\t\t&_owner->session().data(),\n\t\t\tData::Stickers::DefaultSetId,\n\t\t\tuint64(0), // accessHash\n\t\t\tuint64(0), // hash\n\t\t\ttr::lng_stickers_default_set(tr::now),\n\t\t\tQString(),\n\t\t\t0, // count\n\t\t\t(SetFlag::Official | SetFlag::Installed | SetFlag::Special),\n\t\t\tkDefaultStickerInstallDate)).first->second.get();\n\tconst auto custom = sets.emplace(\n\t\tData::Stickers::CustomSetId,\n\t\tstd::make_unique<Data::StickersSet>(\n\t\t\t&_owner->session().data(),\n\t\t\tData::Stickers::CustomSetId,\n\t\t\tuint64(0), // accessHash\n\t\t\tuint64(0), // hash\n\t\t\tu\"Custom stickers\"_q,\n\t\t\tQString(),\n\t\t\t0, // count\n\t\t\t(SetFlag::Installed | SetFlag::Special),\n\t\t\tkDefaultStickerInstallDate)).first->second.get();\n\n\tQMap<uint64, bool> read;\n\twhile (!stickers.stream.atEnd()) {\n\t\tquint64 id, access;\n\t\tQString name, mime, alt;\n\t\tqint32 date, dc, size, width, height, type;\n\t\tqint16 value;\n\t\tstickers.stream >> id >> value >> access >> date >> name >> mime >> dc >> size >> width >> height >> type;\n\t\tif (stickers.version >= 7021) {\n\t\t\tstickers.stream >> alt;\n\t\t}\n\t\tif (!value || read.contains(id)) continue;\n\t\tread.insert(id, true);\n\n\t\tQVector<MTPDocumentAttribute> attributes;\n\t\tif (!name.isEmpty()) attributes.push_back(MTP_documentAttributeFilename(MTP_string(name)));\n\t\tif (type == AnimatedDocument) {\n\t\t\tattributes.push_back(MTP_documentAttributeAnimated());\n\t\t} else if (type == StickerDocument) {\n\t\t\tattributes.push_back(MTP_documentAttributeSticker(MTP_flags(0), MTP_string(alt), MTP_inputStickerSetEmpty(), MTPMaskCoords()));\n\t\t}\n\t\tif (width > 0 && height > 0) {\n\t\t\tattributes.push_back(MTP_documentAttributeImageSize(MTP_int(width), MTP_int(height)));\n\t\t}\n\n\t\tconst auto doc = _owner->session().data().document(\n\t\t\tid,\n\t\t\taccess,\n\t\t\tQByteArray(),\n\t\t\tdate,\n\t\t\tattributes,\n\t\t\tmime,\n\t\t\tInlineImageLocation(),\n\t\t\tImageWithLocation(), // thumbnail\n\t\t\tImageWithLocation(), // videoThumbnail\n\t\t\tfalse, // isPremiumSticker\n\t\t\tdc,\n\t\t\tsize);\n\t\tif (!doc->sticker()) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (value > 0) {\n\t\t\tdef->stickers.push_back(doc);\n\t\t\t++def->count;\n\t\t} else {\n\t\t\tcustom->stickers.push_back(doc);\n\t\t\t++custom->count;\n\t\t}\n\t\tif (qAbs(value) > 1\n\t\t\t&& (recent.size()\n\t\t\t\t< _owner->session().serverConfig().stickersRecentLimit)) {\n\t\t\trecent.push_back(qMakePair(doc, qAbs(value)));\n\t\t}\n\t}\n\tif (def->stickers.isEmpty()) {\n\t\tsets.remove(Data::Stickers::DefaultSetId);\n\t} else {\n\t\torder.push_front(Data::Stickers::DefaultSetId);\n\t}\n\tif (custom->stickers.isEmpty()) {\n\t\tsets.remove(Data::Stickers::CustomSetId);\n\t}\n\n\twriteInstalledStickers();\n\twriteSessionSettings();\n\n\tClearKey(_recentStickersKeyOld, _basePath);\n\t_recentStickersKeyOld = 0;\n\twriteMapDelayed();\n}\n\nvoid Account::readInstalledStickers() {\n\tDEBUG_LOG((\"Init: Read installed sticker sets.\"));\n\n\tif (!_installedStickersKey) {\n\t\treturn importOldRecentStickers();\n\t}\n\n\t_owner->session().data().stickers().setsRef().clear();\n\treadStickerSets(\n\t\t_installedStickersKey,\n\t\t&_owner->session().data().stickers().setsOrderRef(),\n\t\tData::StickersSetFlag::Installed);\n}\n\nvoid Account::readFeaturedStickers() {\n\tDEBUG_LOG((\"Init: Read featured sticker sets.\"));\n\n\treadStickerSets(\n\t\t_featuredStickersKey,\n\t\t&_owner->session().data().stickers().featuredSetsOrderRef(),\n\t\tData::StickersSetFlag::Featured);\n\n\tconst auto &sets = _owner->session().data().stickers().sets();\n\tconst auto &order = _owner->session().data().stickers().featuredSetsOrder();\n\tint unreadCount = 0;\n\tfor (const auto setId : order) {\n\t\tauto it = sets.find(setId);\n\t\tif (it != sets.cend()\n\t\t\t&& (it->second->flags & Data::StickersSetFlag::Unread)) {\n\t\t\t++unreadCount;\n\t\t}\n\t}\n\t_owner->session().data().stickers().setFeaturedSetsUnreadCount(unreadCount);\n}\n\nvoid Account::readFeaturedCustomEmoji() {\n\tDEBUG_LOG((\"Init: Read featured emoji sets.\"));\n\n\treadStickerSets(\n\t\t_featuredCustomEmojiKey,\n\t\t&_owner->session().data().stickers().featuredEmojiSetsOrderRef(),\n\t\tData::StickersSetFlag::Featured);\n}\n\nvoid Account::readRecentStickers() {\n\tDEBUG_LOG((\"Init: Read recent stickers.\"));\n\n\treadStickerSets(_recentStickersKey);\n}\n\nvoid Account::readRecentMasks() {\n\tDEBUG_LOG((\"Init: Read recent masks.\"));\n\n\treadStickerSets(_recentMasksKey);\n}\n\nvoid Account::readFavedStickers() {\n\tDEBUG_LOG((\"Init: Read faved masks.\"));\n\n\treadStickerSets(_favedStickersKey);\n}\n\nvoid Account::readArchivedStickers() {\n\tDEBUG_LOG((\"Init: Read archived stickers.\"));\n\n\t// TODO: refactor to support for multiple accounts.\n\tstatic bool archivedStickersRead = false;\n\tif (!archivedStickersRead) {\n\t\treadStickerSets(\n\t\t\t_archivedStickersKey,\n\t\t\t&_owner->session().data().stickers().archivedSetsOrderRef());\n\t\tarchivedStickersRead = true;\n\t}\n}\n\nvoid Account::readArchivedMasks() {\n\tDEBUG_LOG((\"Init: Read archived masks.\"));\n\n\t// TODO: refactor to support for multiple accounts.\n\tstatic bool archivedMasksRead = false;\n\tif (!archivedMasksRead) {\n\t\treadStickerSets(\n\t\t\t_archivedMasksKey,\n\t\t\t&_owner->session().data().stickers().archivedMaskSetsOrderRef());\n\t\tarchivedMasksRead = true;\n\t}\n}\n\nvoid Account::readInstalledMasks() {\n\tDEBUG_LOG((\"Init: Read installed masks.\"));\n\n\treadStickerSets(\n\t\t_installedMasksKey,\n\t\t&_owner->session().data().stickers().maskSetsOrderRef(),\n\t\tData::StickersSetFlag::Installed);\n}\n\nvoid Account::readInstalledCustomEmoji() {\n\tDEBUG_LOG((\"Init: Read installed emoji sets.\"));\n\n\treadStickerSets(\n\t\t_installedCustomEmojiKey,\n\t\t&_owner->session().data().stickers().emojiSetsOrderRef(),\n\t\tData::StickersSetFlag::Installed);\n}\n\nvoid Account::writeSavedGifs() {\n\tconst auto &saved = _owner->session().data().stickers().savedGifs();\n\tif (saved.isEmpty()) {\n\t\tif (_savedGifsKey) {\n\t\t\tClearKey(_savedGifsKey, _basePath);\n\t\t\t_savedGifsKey = 0;\n\t\t\twriteMapDelayed();\n\t\t}\n\t} else {\n\t\tquint32 size = sizeof(quint32); // count\n\t\tfor (const auto gif : saved) {\n\t\t\tsize += Serialize::Document::sizeInStream(gif);\n\t\t}\n\n\t\tif (!_savedGifsKey) {\n\t\t\t_savedGifsKey = GenerateKey(_basePath);\n\t\t\twriteMapQueued();\n\t\t}\n\t\tEncryptedDescriptor data(size);\n\t\tdata.stream << quint32(saved.size());\n\t\tfor (const auto gif : saved) {\n\t\t\tSerialize::Document::writeToStream(data.stream, gif);\n\t\t}\n\t\tFileWriteDescriptor file(_savedGifsKey, _basePath);\n\t\tfile.writeEncrypted(data, _localKey);\n\t}\n}\n\nvoid Account::readSavedGifs() {\n\tDEBUG_LOG((\"Init: Read saved GIFs.\"));\n\n\tif (!_savedGifsKey) {\n\t\treturn;\n\t}\n\n\tFileReadDescriptor gifs;\n\tif (!ReadEncryptedFile(gifs, _savedGifsKey, _basePath, _localKey)) {\n\t\tClearKey(_savedGifsKey, _basePath);\n\t\t_savedGifsKey = 0;\n\t\twriteMapDelayed();\n\t\treturn;\n\t}\n\n\tauto &saved = _owner->session().data().stickers().savedGifsRef();\n\tconst auto failed = [&] {\n\t\tClearKey(_savedGifsKey, _basePath);\n\t\t_savedGifsKey = 0;\n\t\tsaved.clear();\n\t};\n\tsaved.clear();\n\n\tquint32 cnt;\n\tgifs.stream >> cnt;\n\tsaved.reserve(cnt);\n\tOrderedSet<DocumentId> read;\n\tfor (uint32 i = 0; i < cnt; ++i) {\n\t\tauto document = Serialize::Document::readFromStream(\n\t\t\t&_owner->session(),\n\t\t\tgifs.version,\n\t\t\tgifs.stream);\n\t\tif (!CheckStreamStatus(gifs.stream)) {\n\t\t\treturn failed();\n\t\t} else if (!document || !document->isGifv()) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (read.contains(document->id)) continue;\n\t\tread.insert(document->id);\n\n\t\tsaved.push_back(document);\n\t}\n}\n\nvoid Account::writeRecentHashtagsAndBots() {\n\tconst auto &write = cRecentWriteHashtags();\n\tconst auto &search = cRecentSearchHashtags();\n\tconst auto &bots = cRecentInlineBots();\n\n\tif (write.isEmpty() && search.isEmpty() && bots.isEmpty()) {\n\t\treadRecentHashtagsAndBots();\n\t}\n\tif (write.isEmpty() && search.isEmpty() && bots.isEmpty()) {\n\t\tif (_recentHashtagsAndBotsKey) {\n\t\t\tClearKey(_recentHashtagsAndBotsKey, _basePath);\n\t\t\t_recentHashtagsAndBotsKey = 0;\n\t\t\twriteMapDelayed();\n\t\t}\n\t\treturn;\n\t}\n\tif (!_recentHashtagsAndBotsKey) {\n\t\t_recentHashtagsAndBotsKey = GenerateKey(_basePath);\n\t\twriteMapQueued();\n\t}\n\tquint32 size = sizeof(quint32) * 3, writeCnt = 0, searchCnt = 0, botsCnt = cRecentInlineBots().size();\n\tfor (auto i = write.cbegin(), e = write.cend(); i != e; ++i) {\n\t\tif (!i->first.isEmpty()) {\n\t\t\tsize += Serialize::stringSize(i->first) + sizeof(quint16);\n\t\t\t++writeCnt;\n\t\t}\n\t}\n\tfor (auto i = search.cbegin(), e = search.cend(); i != e; ++i) {\n\t\tif (!i->first.isEmpty()) {\n\t\t\tsize += Serialize::stringSize(i->first) + sizeof(quint16);\n\t\t\t++searchCnt;\n\t\t}\n\t}\n\tfor (auto i = bots.cbegin(), e = bots.cend(); i != e; ++i) {\n\t\tsize += Serialize::peerSize(*i);\n\t}\n\n\tEncryptedDescriptor data(size);\n\tdata.stream << quint32(writeCnt) << quint32(searchCnt);\n\tfor (auto i = write.cbegin(), e = write.cend(); i != e; ++i) {\n\t\tif (!i->first.isEmpty()) data.stream << i->first << quint16(i->second);\n\t}\n\tfor (auto i = search.cbegin(), e = search.cend(); i != e; ++i) {\n\t\tif (!i->first.isEmpty()) data.stream << i->first << quint16(i->second);\n\t}\n\tdata.stream << quint32(botsCnt);\n\tfor (auto i = bots.cbegin(), e = bots.cend(); i != e; ++i) {\n\t\tSerialize::writePeer(data.stream, *i);\n\t}\n\tFileWriteDescriptor file(_recentHashtagsAndBotsKey, _basePath);\n\tfile.writeEncrypted(data, _localKey);\n}\n\nvoid Account::readRecentHashtagsAndBots() {\n\tif (_recentHashtagsAndBotsWereRead) return;\n\t_recentHashtagsAndBotsWereRead = true;\n\n\tif (!_recentHashtagsAndBotsKey) return;\n\n\tFileReadDescriptor hashtags;\n\tif (!ReadEncryptedFile(hashtags, _recentHashtagsAndBotsKey, _basePath, _localKey)) {\n\t\tClearKey(_recentHashtagsAndBotsKey, _basePath);\n\t\t_recentHashtagsAndBotsKey = 0;\n\t\twriteMapDelayed();\n\t\treturn;\n\t}\n\n\tquint32 writeCount = 0, searchCount = 0, botsCount = 0;\n\thashtags.stream >> writeCount >> searchCount;\n\n\tQString tag;\n\tquint16 count;\n\n\tRecentHashtagPack write, search;\n\tRecentInlineBots bots;\n\tif (writeCount) {\n\t\twrite.reserve(writeCount);\n\t\tfor (uint32 i = 0; i < writeCount; ++i) {\n\t\t\thashtags.stream >> tag >> count;\n\t\t\twrite.push_back(qMakePair(tag.trimmed(), count));\n\t\t}\n\t}\n\tif (searchCount) {\n\t\tsearch.reserve(searchCount);\n\t\tfor (uint32 i = 0; i < searchCount; ++i) {\n\t\t\thashtags.stream >> tag >> count;\n\t\t\tsearch.push_back(qMakePair(tag.trimmed(), count));\n\t\t}\n\t}\n\tcSetRecentWriteHashtags(write);\n\tcSetRecentSearchHashtags(search);\n\n\tif (!hashtags.stream.atEnd()) {\n\t\thashtags.stream >> botsCount;\n\t\tif (botsCount) {\n\t\t\tbots.reserve(botsCount);\n\t\t\tfor (auto i = 0; i < botsCount; ++i) {\n\t\t\t\tconst auto peer = Serialize::readPeer(\n\t\t\t\t\t&_owner->session(),\n\t\t\t\t\thashtags.version,\n\t\t\t\t\thashtags.stream);\n\t\t\t\tif (!peer) {\n\t\t\t\t\treturn; // Broken data.\n\t\t\t\t} else if (peer->isUser()\n\t\t\t\t\t&& peer->asUser()->isBot()\n\t\t\t\t\t&& !peer->asUser()->botInfo->inlinePlaceholder.isEmpty()\n\t\t\t\t\t&& !peer->asUser()->username().isEmpty()) {\n\t\t\t\t\tbots.push_back(peer->asUser());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcSetRecentInlineBots(bots);\n\t}\n}\n\nstd::optional<RecentHashtagPack> Account::saveRecentHashtags(\n\t\tFn<RecentHashtagPack()> getPack,\n\t\tconst QString &text) {\n\tauto found = false;\n\tauto m = QRegularExpressionMatch();\n\tauto recent = getPack();\n\tfor (auto i = 0, next = 0\n\t\t; (m = TextUtilities::RegExpHashtag(false).match(text, i)).hasMatch()\n\t\t; i = next) {\n\t\ti = m.capturedStart();\n\t\tnext = m.capturedEnd();\n\t\tif (m.hasMatch()) {\n\t\t\tif (!m.capturedView(1).isEmpty()) {\n\t\t\t\t++i;\n\t\t\t}\n\t\t\tif (!m.capturedView(2).isEmpty()) {\n\t\t\t\t--next;\n\t\t\t}\n\t\t}\n\t\tconst auto tag = text.mid(i + 1, next - i - 1);\n\t\tif (TextUtilities::RegExpHashtagExclude().match(tag).hasMatch()) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!found\n\t\t\t&& cRecentWriteHashtags().isEmpty()\n\t\t\t&& cRecentSearchHashtags().isEmpty()) {\n\t\t\treadRecentHashtagsAndBots();\n\t\t\trecent = getPack();\n\t\t}\n\t\tfound = true;\n\t\tLocal::incrementRecentHashtag(recent, tag);\n\t}\n\treturn found ? base::make_optional(recent) : std::nullopt;\n}\n\nvoid Account::saveRecentSentHashtags(const QString &text) {\n\tconst auto result = saveRecentHashtags(\n\t\t[] { return cRecentWriteHashtags(); },\n\t\ttext);\n\tif (result) {\n\t\tcSetRecentWriteHashtags(*result);\n\t\twriteRecentHashtagsAndBots();\n\t}\n}\n\nvoid Account::saveRecentSearchHashtags(const QString &text) {\n\tconst auto result = saveRecentHashtags(\n\t\t[] { return cRecentSearchHashtags(); },\n\t\ttext);\n\tif (result) {\n\t\tcSetRecentSearchHashtags(*result);\n\t\twriteRecentHashtagsAndBots();\n\t}\n}\n\nvoid Account::writeExportSettings(const Export::Settings &settings) {\n\tconst auto check = Export::Settings();\n\tif (settings.types == check.types\n\t\t&& settings.fullChats == check.fullChats\n\t\t&& settings.media.types == check.media.types\n\t\t&& settings.media.sizeLimit == check.media.sizeLimit\n\t\t&& settings.path == check.path\n\t\t&& settings.format == check.format\n\t\t&& settings.availableAt == check.availableAt\n\t\t&& !settings.onlySinglePeer()) {\n\t\tif (_exportSettingsKey) {\n\t\t\tClearKey(_exportSettingsKey, _basePath);\n\t\t\t_exportSettingsKey = 0;\n\t\t\twriteMapDelayed();\n\t\t}\n\t\treturn;\n\t}\n\tif (!_exportSettingsKey) {\n\t\t_exportSettingsKey = GenerateKey(_basePath);\n\t\twriteMapQueued();\n\t}\n\tquint32 size = sizeof(quint32) * 6\n\t\t+ Serialize::stringSize(settings.path)\n\t\t+ sizeof(qint32) * 2 + sizeof(quint64);\n\tEncryptedDescriptor data(size);\n\tdata.stream\n\t\t<< quint32(settings.types)\n\t\t<< quint32(settings.fullChats)\n\t\t<< quint32(settings.media.types)\n\t\t<< quint32(settings.media.sizeLimit)\n\t\t<< quint32(settings.format)\n\t\t<< settings.path\n\t\t<< quint32(settings.availableAt);\n\tsettings.singlePeer.match([&](const MTPDinputPeerUser &user) {\n\t\tdata.stream\n\t\t\t<< kSinglePeerTypeUser\n\t\t\t<< quint64(user.vuser_id().v)\n\t\t\t<< quint64(user.vaccess_hash().v);\n\t}, [&](const MTPDinputPeerChat & chat) {\n\t\tdata.stream << kSinglePeerTypeChat << quint64(chat.vchat_id().v);\n\t}, [&](const MTPDinputPeerChannel & channel) {\n\t\tdata.stream\n\t\t\t<< kSinglePeerTypeChannel\n\t\t\t<< quint64(channel.vchannel_id().v)\n\t\t\t<< quint64(channel.vaccess_hash().v);\n\t}, [&](const MTPDinputPeerSelf &) {\n\t\tdata.stream << kSinglePeerTypeSelf;\n\t}, [&](const MTPDinputPeerEmpty &) {\n\t\tdata.stream << kSinglePeerTypeEmpty;\n\t}, [&](const MTPDinputPeerUserFromMessage &) {\n\t\tUnexpected(\"From message peer in single peer export settings.\");\n\t}, [&](const MTPDinputPeerChannelFromMessage &) {\n\t\tUnexpected(\"From message peer in single peer export settings.\");\n\t});\n\tdata.stream << qint32(settings.singlePeerFrom);\n\tdata.stream << qint32(settings.singlePeerTill);\n\n\tFileWriteDescriptor file(_exportSettingsKey, _basePath);\n\tfile.writeEncrypted(data, _localKey);\n}\n\nExport::Settings Account::readExportSettings() {\n\tif (!_exportSettingsKey) {\n\t\treturn {};\n\t}\n\n\tFileReadDescriptor file;\n\tif (!ReadEncryptedFile(file, _exportSettingsKey, _basePath, _localKey)) {\n\t\tClearKey(_exportSettingsKey, _basePath);\n\t\t_exportSettingsKey = 0;\n\t\twriteMapDelayed();\n\t\treturn {};\n\t}\n\n\tquint32 types = 0, fullChats = 0;\n\tquint32 mediaTypes = 0, mediaSizeLimit = 0;\n\tquint32 format = 0, availableAt = 0;\n\tQString path;\n\tqint32 singlePeerType = 0, singlePeerBareIdOld = 0;\n\tquint64 singlePeerBareId = 0;\n\tquint64 singlePeerAccessHash = 0;\n\tqint32 singlePeerFrom = 0, singlePeerTill = 0;\n\tfile.stream\n\t\t>> types\n\t\t>> fullChats\n\t\t>> mediaTypes\n\t\t>> mediaSizeLimit\n\t\t>> format\n\t\t>> path\n\t\t>> availableAt;\n\tif (!file.stream.atEnd()) {\n\t\tfile.stream >> singlePeerType;\n\t\tswitch (singlePeerType) {\n\t\tcase kSinglePeerTypeUserOld:\n\t\tcase kSinglePeerTypeChannelOld: {\n\t\t\tfile.stream >> singlePeerBareIdOld >> singlePeerAccessHash;\n\t\t} break;\n\t\tcase kSinglePeerTypeChatOld: file.stream >> singlePeerBareIdOld; break;\n\n\t\tcase kSinglePeerTypeUser:\n\t\tcase kSinglePeerTypeChannel: {\n\t\t\tfile.stream >> singlePeerBareId >> singlePeerAccessHash;\n\t\t} break;\n\t\tcase kSinglePeerTypeChat: file.stream >> singlePeerBareId; break;\n\t\tcase kSinglePeerTypeSelf:\n\t\tcase kSinglePeerTypeEmpty: break;\n\t\tdefault: return Export::Settings();\n\t\t}\n\t}\n\tif (!file.stream.atEnd()) {\n\t\tfile.stream >> singlePeerFrom >> singlePeerTill;\n\t}\n\tauto result = Export::Settings();\n\tresult.types = Export::Settings::Types::from_raw(types);\n\tresult.fullChats = Export::Settings::Types::from_raw(fullChats);\n\tresult.media.types = Export::MediaSettings::Types::from_raw(mediaTypes);\n\tresult.media.sizeLimit = mediaSizeLimit;\n\tresult.format = Export::Output::Format(format);\n\tresult.path = path;\n\tresult.availableAt = availableAt;\n\tresult.singlePeer = [&] {\n\t\tswitch (singlePeerType) {\n\t\tcase kSinglePeerTypeUserOld:\n\t\t\treturn MTP_inputPeerUser(\n\t\t\t\tMTP_long(singlePeerBareIdOld),\n\t\t\t\tMTP_long(singlePeerAccessHash));\n\t\tcase kSinglePeerTypeChatOld:\n\t\t\treturn MTP_inputPeerChat(MTP_long(singlePeerBareIdOld));\n\t\tcase kSinglePeerTypeChannelOld:\n\t\t\treturn MTP_inputPeerChannel(\n\t\t\t\tMTP_long(singlePeerBareIdOld),\n\t\t\t\tMTP_long(singlePeerAccessHash));\n\n\t\tcase kSinglePeerTypeUser:\n\t\t\treturn MTP_inputPeerUser(\n\t\t\t\tMTP_long(singlePeerBareId),\n\t\t\t\tMTP_long(singlePeerAccessHash));\n\t\tcase kSinglePeerTypeChat:\n\t\t\treturn MTP_inputPeerChat(MTP_long(singlePeerBareId));\n\t\tcase kSinglePeerTypeChannel:\n\t\t\treturn MTP_inputPeerChannel(\n\t\t\t\tMTP_long(singlePeerBareId),\n\t\t\t\tMTP_long(singlePeerAccessHash));\n\t\tcase kSinglePeerTypeSelf:\n\t\t\treturn MTP_inputPeerSelf();\n\t\tcase kSinglePeerTypeEmpty:\n\t\t\treturn MTP_inputPeerEmpty();\n\t\t}\n\t\tUnexpected(\"Type in export data single peer.\");\n\t}();\n\tresult.singlePeerFrom = singlePeerFrom;\n\tresult.singlePeerTill = singlePeerTill;\n\treturn (file.stream.status() == QDataStream::Ok && result.validate())\n\t\t? result\n\t\t: Export::Settings();\n}\n\nvoid Account::setMediaLastPlaybackPosition(DocumentId id, crl::time time) {\n\tauto &map = _mediaLastPlaybackPosition;\n\tconst auto i = ranges::find(\n\t\tmap,\n\t\tid,\n\t\t&std::pair<DocumentId, crl::time>::first);\n\tif (i != map.end()) {\n\t\tif (time > 0) {\n\t\t\tif (i->second == time) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ti->second = time;\n\t\t\tstd::rotate(i, i + 1, map.end());\n\t\t} else {\n\t\t\tmap.erase(i);\n\t\t}\n\t} else if (time > 0) {\n\t\tif (map.size() >= kMaxSavedPlaybackPositions) {\n\t\t\tmap.erase(map.begin());\n\t\t}\n\t\tmap.emplace_back(id, time);\n\t}\n\twriteMediaLastPlaybackPositions();\n}\n\ncrl::time Account::mediaLastPlaybackPosition(DocumentId id) const {\n\tconst_cast<Account*>(this)->readMediaLastPlaybackPositions();\n\tconst auto i = ranges::find(\n\t\t_mediaLastPlaybackPosition,\n\t\tid,\n\t\t&std::pair<DocumentId, crl::time>::first);\n\treturn (i != _mediaLastPlaybackPosition.end()) ? i->second : 0;\n}\n\nvoid Account::writeMediaLastPlaybackPositions() {\n\tif (_mediaLastPlaybackPosition.empty()) {\n\t\tif (_mediaLastPlaybackPositionsKey) {\n\t\t\tClearKey(_mediaLastPlaybackPositionsKey, _basePath);\n\t\t\t_mediaLastPlaybackPositionsKey = 0;\n\t\t\twriteMapDelayed();\n\t\t}\n\t\treturn;\n\t}\n\tif (!_mediaLastPlaybackPositionsKey) {\n\t\t_mediaLastPlaybackPositionsKey = GenerateKey(_basePath);\n\t\twriteMapQueued();\n\t}\n\tquint32 size = sizeof(quint32)\n\t\t+ _mediaLastPlaybackPosition.size() * sizeof(quint64) * 2;\n\tEncryptedDescriptor data(size);\n\tdata.stream << quint32(_mediaLastPlaybackPosition.size());\n\tfor (const auto &[id, time] : _mediaLastPlaybackPosition) {\n\t\tdata.stream << quint64(id) << qint64(time);\n\t}\n\n\tFileWriteDescriptor file(_mediaLastPlaybackPositionsKey, _basePath);\n\tfile.writeEncrypted(data, _localKey);\n}\n\nvoid Account::readMediaLastPlaybackPositions() {\n\tif (_mediaLastPlaybackPositionsRead) {\n\t\treturn;\n\t}\n\t_mediaLastPlaybackPositionsRead = true;\n\tif (!_mediaLastPlaybackPositionsKey) {\n\t\treturn;\n\t}\n\n\tFileReadDescriptor file;\n\tif (!ReadEncryptedFile(\n\t\t\tfile,\n\t\t\t_mediaLastPlaybackPositionsKey,\n\t\t\t_basePath,\n\t\t\t_localKey)) {\n\t\tClearKey(_mediaLastPlaybackPositionsKey, _basePath);\n\t\t_mediaLastPlaybackPositionsKey = 0;\n\t\twriteMapDelayed();\n\t\treturn;\n\t}\n\n\tquint32 size = 0;\n\tfile.stream >> size;\n\tfor (auto i = 0; i < size; ++i) {\n\t\tquint64 id = 0;\n\t\tqint64 time = 0;\n\t\tfile.stream >> id >> time;\n\t\t_mediaLastPlaybackPosition.emplace_back(DocumentId(id), time);\n\t}\n}\n\nvoid Account::writeSearchSuggestionsDelayed() {\n\tExpects(_owner->sessionExists());\n\n\tif (!_writeSearchSuggestionsTimer.isActive()) {\n\t\t_writeSearchSuggestionsTimer.callOnce(kWriteSearchSuggestionsDelay);\n\t}\n}\n\nvoid Account::writeSearchSuggestionsIfNeeded() {\n\tif (_writeSearchSuggestionsTimer.isActive()) {\n\t\t_writeSearchSuggestionsTimer.cancel();\n\t\twriteSearchSuggestions();\n\t}\n}\n\nvoid Account::writeSearchSuggestions() {\n\tExpects(_owner->sessionExists());\n\n\tconst auto top = _owner->session().topPeers().serialize();\n\tconst auto recent = _owner->session().recentPeers().serialize();\n\tconst auto settingsSearches\n\t\t= _owner->session().recentSettingsSearches().serialize();\n\tif (top.isEmpty() && recent.isEmpty() && settingsSearches.isEmpty()) {\n\t\tif (_searchSuggestionsKey) {\n\t\t\tClearKey(_searchSuggestionsKey, _basePath);\n\t\t\t_searchSuggestionsKey = 0;\n\t\t\twriteMapDelayed();\n\t\t}\n\t\treturn;\n\t}\n\tif (!_searchSuggestionsKey) {\n\t\t_searchSuggestionsKey = GenerateKey(_basePath);\n\t\twriteMapQueued();\n\t}\n\tquint32 size = Serialize::bytearraySize(top)\n\t\t+ Serialize::bytearraySize(recent)\n\t\t+ Serialize::bytearraySize(settingsSearches);\n\tEncryptedDescriptor data(size);\n\tdata.stream << top << recent << settingsSearches;\n\n\tFileWriteDescriptor file(_searchSuggestionsKey, _basePath);\n\tfile.writeEncrypted(data, _localKey);\n}\n\nvoid Account::readSearchSuggestions() {\n\tif (_searchSuggestionsRead) {\n\t\treturn;\n\t}\n\t_searchSuggestionsRead = true;\n\tif (!_searchSuggestionsKey) {\n\t\tDEBUG_LOG((\"Suggestions: No key.\"));\n\t\treturn;\n\t}\n\n\tFileReadDescriptor suggestions;\n\tif (!ReadEncryptedFile(suggestions, _searchSuggestionsKey, _basePath, _localKey)) {\n\t\tDEBUG_LOG((\"Suggestions: Could not read file.\"));\n\t\tClearKey(_searchSuggestionsKey, _basePath);\n\t\t_searchSuggestionsKey = 0;\n\t\twriteMapDelayed();\n\t\treturn;\n\t}\n\n\tauto top = QByteArray();\n\tauto recent = QByteArray();\n\tauto settingsSearches = QByteArray();\n\tsuggestions.stream >> top >> recent;\n\tif (!suggestions.stream.atEnd()) {\n\t\tsuggestions.stream >> settingsSearches;\n\t}\n\tif (CheckStreamStatus(suggestions.stream)) {\n\t\t_owner->session().topPeers().applyLocal(top);\n\t\t_owner->session().recentPeers().applyLocal(recent);\n\t\t_owner->session().recentSettingsSearches().applyLocal(\n\t\t\tsettingsSearches);\n\t} else {\n\t\tDEBUG_LOG((\"Suggestions: Could not read content.\"));\n\t}\n}\n\nvoid Account::writeSelf() {\n\twriteMapDelayed();\n}\n\nvoid Account::readSelf(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QByteArray &serialized,\n\t\tint32 streamVersion) {\n\tQDataStream stream(serialized);\n\tconst auto user = session->user();\n\tconst auto wasLoadedStatus = user->loadedStatus();\n\tuser->setLoadedStatus(PeerData::LoadedStatus::Not);\n\tconst auto self = Serialize::readPeer(\n\t\tsession,\n\t\tstreamVersion,\n\t\tstream);\n\tif (!self || !self->isSelf() || self != user) {\n\t\tuser->setLoadedStatus(wasLoadedStatus);\n\t\treturn;\n\t}\n\n\tQString about;\n\tstream >> about;\n\tif (CheckStreamStatus(stream)) {\n\t\tself->asUser()->setAbout(about);\n\t}\n}\n\nvoid Account::writeTrustedPeers() {\n\tif (_trustedPeers.empty() && _trustedPayPerMessage.empty()) {\n\t\tif (_trustedPeersKey) {\n\t\t\tClearKey(_trustedPeersKey, _basePath);\n\t\t\t_trustedPeersKey = 0;\n\t\t\twriteMapDelayed();\n\t\t}\n\t\treturn;\n\t}\n\tif (!_trustedPeersKey) {\n\t\t_trustedPeersKey = GenerateKey(_basePath);\n\t\twriteMapQueued();\n\t}\n\tquint32 size = sizeof(qint32)\n\t\t+ _trustedPeers.size() * sizeof(quint64)\n\t\t+ sizeof(qint32)\n\t\t+ _trustedPayPerMessage.size() * (sizeof(quint64) + sizeof(qint32));\n\tEncryptedDescriptor data(size);\n\tdata.stream << qint32(_trustedPeers.size());\n\tfor (const auto &[peerId, mask] : _trustedPeers) {\n\t\t// value: 8 bit mask, 56 bit peer_id.\n\t\tauto value = SerializePeerId(peerId);\n\t\tAssert((value >> 56) == 0);\n\t\tvalue |= (quint64(mask) << 56);\n\t\tdata.stream << value;\n\t}\n\tdata.stream << qint32(_trustedPayPerMessage.size());\n\tfor (const auto &[peerId, stars] : _trustedPayPerMessage) {\n\t\tdata.stream << SerializePeerId(peerId) << qint32(stars);\n\t}\n\n\tFileWriteDescriptor file(_trustedPeersKey, _basePath);\n\tfile.writeEncrypted(data, _localKey);\n}\n\nvoid Account::readTrustedPeers() {\n\tif (_trustedPeersRead) {\n\t\treturn;\n\t}\n\t_trustedPeersRead = true;\n\tif (!_trustedPeersKey) {\n\t\treturn;\n\t}\n\n\tFileReadDescriptor trusted;\n\tif (!ReadEncryptedFile(trusted, _trustedPeersKey, _basePath, _localKey)) {\n\t\tClearKey(_trustedPeersKey, _basePath);\n\t\t_trustedPeersKey = 0;\n\t\twriteMapDelayed();\n\t\treturn;\n\t}\n\n\tqint32 trustedCount = 0;\n\ttrusted.stream >> trustedCount;\n\tfor (int i = 0; i < trustedCount; ++i) {\n\t\tauto value = quint64();\n\t\ttrusted.stream >> value;\n\t\tconst auto mask = base::flags<PeerTrustFlag>::from_raw(\n\t\t\tuchar(value >> 56));\n\t\tconst auto peerIdSerialized = value & ~(0xFFULL << 56);\n\t\tconst auto peerId = DeserializePeerId(peerIdSerialized);\n\t\t_trustedPeers.emplace(peerId, mask);\n\t}\n\tif (trusted.stream.atEnd()) {\n\t\treturn;\n\t}\n\tqint32 payPerMessageCount = 0;\n\ttrusted.stream >> payPerMessageCount;\n\tconst auto owner = _owner->sessionExists()\n\t\t? &_owner->session().data()\n\t\t: nullptr;\n\tfor (int i = 0; i < payPerMessageCount; ++i) {\n\t\tauto value = quint64();\n\t\tauto stars = qint32();\n\t\ttrusted.stream >> value >> stars;\n\t\tconst auto peerId = DeserializePeerId(value);\n\t\tconst auto peer = owner ? owner->peerLoaded(peerId) : nullptr;\n\t\tconst auto now = peer ? peer->starsPerMessage() : stars;\n\t\tif (now > 0 && now <= stars) {\n\t\t\t_trustedPayPerMessage.emplace(peerId, stars);\n\t\t}\n\t}\n\tif (_trustedPayPerMessage.size() != payPerMessageCount) {\n\t\twriteTrustedPeers();\n\t}\n}\n\nvoid Account::markPeerTrustedOpenGame(PeerId peerId) {\n\tif (isPeerTrustedOpenGame(peerId)) {\n\t\treturn;\n\t}\n\tconst auto i = _trustedPeers.find(peerId);\n\tif (i == end(_trustedPeers)) {\n\t\t_trustedPeers.emplace(peerId, PeerTrustFlag());\n\t} else {\n\t\ti->second &= ~PeerTrustFlag::NoOpenGame;\n\t}\n\twriteTrustedPeers();\n}\n\nbool Account::isPeerTrustedOpenGame(PeerId peerId) {\n\treadTrustedPeers();\n\tconst auto i = _trustedPeers.find(peerId);\n\treturn (i != end(_trustedPeers))\n\t\t&& ((i->second & PeerTrustFlag::NoOpenGame) == 0);\n}\n\nvoid Account::markPeerTrustedPayment(PeerId peerId) {\n\tif (isPeerTrustedPayment(peerId)) {\n\t\treturn;\n\t}\n\tconst auto i = _trustedPeers.find(peerId);\n\tif (i == end(_trustedPeers)) {\n\t\t_trustedPeers.emplace(\n\t\t\tpeerId,\n\t\t\tPeerTrustFlag::NoOpenGame | PeerTrustFlag::Payment);\n\t} else {\n\t\ti->second |= PeerTrustFlag::Payment;\n\t}\n\twriteTrustedPeers();\n}\n\nbool Account::isPeerTrustedPayment(PeerId peerId) {\n\treadTrustedPeers();\n\tconst auto i = _trustedPeers.find(peerId);\n\treturn (i != end(_trustedPeers))\n\t\t&& ((i->second & PeerTrustFlag::Payment) != 0);\n}\n\nvoid Account::markPeerTrustedOpenWebView(PeerId peerId) {\n\tif (isPeerTrustedOpenWebView(peerId)) {\n\t\treturn;\n\t}\n\tconst auto i = _trustedPeers.find(peerId);\n\tif (i == end(_trustedPeers)) {\n\t\t_trustedPeers.emplace(\n\t\t\tpeerId,\n\t\t\tPeerTrustFlag::NoOpenGame | PeerTrustFlag::OpenWebView);\n\t} else {\n\t\ti->second |= PeerTrustFlag::OpenWebView;\n\t}\n\twriteTrustedPeers();\n}\n\nbool Account::isPeerTrustedOpenWebView(PeerId peerId) {\n\treadTrustedPeers();\n\tconst auto i = _trustedPeers.find(peerId);\n\treturn (i != end(_trustedPeers))\n\t\t&& ((i->second & PeerTrustFlag::OpenWebView) != 0);\n}\n\nvoid Account::markPeerTrustedPayForMessage(\n\t\tPeerId peerId,\n\t\tint starsPerMessage) {\n\tif (isPeerTrustedPayForMessage(peerId, starsPerMessage)) {\n\t\treturn;\n\t}\n\tconst auto i = _trustedPayPerMessage.find(peerId);\n\tif (i == end(_trustedPayPerMessage)) {\n\t\t_trustedPayPerMessage.emplace(peerId, starsPerMessage);\n\t} else {\n\t\ti->second = starsPerMessage;\n\t}\n\twriteTrustedPeers();\n}\n\nbool Account::isPeerTrustedPayForMessage(\n\t\tPeerId peerId,\n\t\tint starsPerMessage) {\n\tif (starsPerMessage <= 0) {\n\t\treturn true;\n\t}\n\treadTrustedPeers();\n\tconst auto i = _trustedPayPerMessage.find(peerId);\n\treturn (i != end(_trustedPayPerMessage))\n\t\t&& (i->second >= starsPerMessage);\n}\n\nbool Account::peerTrustedPayForMessageRead() const {\n\treturn _trustedPeersRead;\n}\n\nbool Account::hasPeerTrustedPayForMessageEntry(PeerId peerId) const {\n\treturn _trustedPayPerMessage.contains(peerId);\n}\n\nvoid Account::clearPeerTrustedPayForMessage(PeerId peerId) {\n\tconst auto i = _trustedPayPerMessage.find(peerId);\n\tif (i != end(_trustedPayPerMessage)) {\n\t\t_trustedPayPerMessage.erase(i);\n\t\twriteTrustedPeers();\n\t}\n}\n\nvoid Account::enforceModernStorageIdBots() {\n\tif (_webviewStorageIdBots.token.isEmpty()) {\n\t\t_webviewStorageIdBots.token = QByteArray::fromStdString(\n\t\t\tWebview::GenerateStorageToken());\n\t\twriteMapDelayed();\n\t}\n}\n\nWebview::StorageId Account::resolveStorageIdBots() {\n\tif (!_webviewStorageIdBots) {\n\t\tauto &token = _webviewStorageIdBots.token;\n\t\tconst auto legacy = Webview::LegacyStorageIdToken();\n\t\tif (token.isEmpty()) {\n\t\t\tauto legacyTaken = false;\n\t\t\tconst auto &list = _owner->domain().accounts();\n\t\t\tfor (const auto &[index, account] : list) {\n\t\t\t\tif (account.get() != _owner.get()) {\n\t\t\t\t\tconst auto &id = account->local()._webviewStorageIdBots;\n\t\t\t\t\tif (id.token == legacy) {\n\t\t\t\t\t\tlegacyTaken = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\ttoken = legacyTaken\n\t\t\t\t? QByteArray::fromStdString(Webview::GenerateStorageToken())\n\t\t\t\t: legacy;\n\t\t\twriteMapDelayed();\n\t\t}\n\t\t_webviewStorageIdBots.path = (token == legacy)\n\t\t\t? (BaseGlobalPath() + u\"webview\"_q)\n\t\t\t: (_databasePath + u\"wvbots\"_q);\n\t}\n\treturn _webviewStorageIdBots;\n}\n\nWebview::StorageId Account::resolveStorageIdOther() {\n\tif (!_webviewStorageIdOther) {\n\t\tif (_webviewStorageIdOther.token.isEmpty()) {\n\t\t\t_webviewStorageIdOther.token = QByteArray::fromStdString(\n\t\t\t\tWebview::GenerateStorageToken());\n\t\t\twriteMapDelayed();\n\t\t}\n\t\t_webviewStorageIdOther.path = _databasePath + u\"wvother\"_q;\n\t}\n\treturn _webviewStorageIdOther;\n}\n\nQImage Account::readRoundPlaceholder() {\n\tif (!_roundPlaceholder.isNull()) {\n\t\treturn _roundPlaceholder;\n\t} else if (!_roundPlaceholderKey) {\n\t\treturn QImage();\n\t}\n\n\tFileReadDescriptor placeholder;\n\tif (!ReadEncryptedFile(\n\t\t\tplaceholder,\n\t\t\t_roundPlaceholderKey,\n\t\t\t_basePath,\n\t\t\t_localKey)) {\n\t\tClearKey(_roundPlaceholderKey, _basePath);\n\t\t_roundPlaceholderKey = 0;\n\t\twriteMapDelayed();\n\t\treturn QImage();\n\t}\n\n\tauto bytes = QByteArray();\n\tplaceholder.stream >> bytes;\n\t_roundPlaceholder = Images::Read({ .content = bytes }).image;\n\treturn _roundPlaceholder;\n}\n\nvoid Account::writeRoundPlaceholder(const QImage &placeholder) {\n\tif (placeholder.isNull()) {\n\t\treturn;\n\t}\n\t_roundPlaceholder = placeholder;\n\n\tauto bytes = QByteArray();\n\tauto buffer = QBuffer(&bytes);\n\tplaceholder.save(&buffer, \"JPG\", 87);\n\n\tquint32 size = Serialize::bytearraySize(bytes);\n\tif (!_roundPlaceholderKey) {\n\t\t_roundPlaceholderKey = GenerateKey(_basePath);\n\t\twriteMapQueued();\n\t}\n\tEncryptedDescriptor data(size);\n\tdata.stream << bytes;\n\tFileWriteDescriptor file(_roundPlaceholderKey, _basePath);\n\tfile.writeEncrypted(data, _localKey);\n}\n\nQByteArray Account::readInlineBotsDownloads() {\n\tif (_inlineBotsDownloadsRead) {\n\t\treturn QByteArray();\n\t}\n\t_inlineBotsDownloadsRead = true;\n\tif (!_inlineBotsDownloadsKey) {\n\t\treturn QByteArray();\n\t}\n\n\tFileReadDescriptor inlineBotsDownloads;\n\tif (!ReadEncryptedFile(\n\t\t\tinlineBotsDownloads,\n\t\t\t_inlineBotsDownloadsKey,\n\t\t\t_basePath,\n\t\t\t_localKey)) {\n\t\tClearKey(_inlineBotsDownloadsKey, _basePath);\n\t\t_inlineBotsDownloadsKey = 0;\n\t\twriteMapDelayed();\n\t\treturn QByteArray();\n\t}\n\n\tauto bytes = QByteArray();\n\tinlineBotsDownloads.stream >> bytes;\n\treturn bytes;\n}\n\nvoid Account::writeInlineBotsDownloads(const QByteArray &bytes) {\n\tif (!_inlineBotsDownloadsKey) {\n\t\t_inlineBotsDownloadsKey = GenerateKey(_basePath);\n\t\twriteMapQueued();\n\t}\n\tquint32 size = Serialize::bytearraySize(bytes);\n\tEncryptedDescriptor data(size);\n\tdata.stream << bytes;\n\tFileWriteDescriptor file(_inlineBotsDownloadsKey, _basePath);\n\tfile.writeEncrypted(data, _localKey);\n}\n\nvoid Account::writeBotStorage(PeerId botId, const QByteArray &serialized) {\n\tif (serialized.isEmpty()) {\n\t\tauto i = _botStoragesMap.find(botId);\n\t\tif (i != _botStoragesMap.cend()) {\n\t\t\tClearKey(i->second, _basePath);\n\t\t\t_botStoragesMap.erase(i);\n\t\t\twriteMapDelayed();\n\t\t}\n\t\t_botStoragesNotReadMap.remove(botId);\n\t\treturn;\n\t}\n\n\tauto i = _botStoragesMap.find(botId);\n\tif (i == _botStoragesMap.cend()) {\n\t\ti = _botStoragesMap.emplace(botId, GenerateKey(_basePath)).first;\n\t\twriteMapQueued();\n\t}\n\n\tauto size = Serialize::bytearraySize(serialized);\n\n\tEncryptedDescriptor data(size);\n\tdata.stream << serialized;\n\n\tFileWriteDescriptor file(i->second, _basePath);\n\tfile.writeEncrypted(data, _localKey);\n\n\t_botStoragesNotReadMap.remove(botId);\n}\n\nQByteArray Account::readBotStorage(PeerId botId) {\n\tif (!_botStoragesNotReadMap.remove(botId)) {\n\t\treturn {};\n\t}\n\n\tconst auto j = _botStoragesMap.find(botId);\n\tif (j == _botStoragesMap.cend()) {\n\t\treturn {};\n\t}\n\tFileReadDescriptor storage;\n\tif (!ReadEncryptedFile(storage, j->second, _basePath, _localKey)) {\n\t\tClearKey(j->second, _basePath);\n\t\t_botStoragesMap.erase(j);\n\t\twriteMapDelayed();\n\t\treturn {};\n\t}\n\n\tauto result = QByteArray();\n\tstorage.stream >> result;\n\tif (storage.stream.status() != QDataStream::Ok) {\n\t\tClearKey(j->second, _basePath);\n\t\t_botStoragesMap.erase(j);\n\t\twriteMapDelayed();\n\t\treturn {};\n\t}\n\treturn result;\n}\n\nbool Account::encrypt(\n\t\tconst void *src,\n\t\tvoid *dst,\n\t\tuint32 len,\n\t\tconst void *key128) const {\n\tif (!_localKey) {\n\t\treturn false;\n\t}\n\tMTP::aesEncryptLocal(src, dst, len, _localKey, key128);\n\treturn true;\n}\n\nbool Account::decrypt(\n\t\tconst void *src,\n\t\tvoid *dst,\n\t\tuint32 len,\n\t\tconst void *key128) const {\n\tif (!_localKey) {\n\t\treturn false;\n\t}\n\tMTP::aesDecryptLocal(src, dst, len, _localKey, key128);\n\treturn true;\n}\n\nWebview::StorageId TonSiteStorageId() {\n\tauto result = Webview::StorageId{\n\t\t.path = BaseGlobalPath() + u\"webview-tonsite\"_q,\n\t\t.token = Core::App().settings().tonsiteStorageToken(),\n\t};\n\tif (result.token.isEmpty()) {\n\t\tresult.token = QByteArray::fromStdString(\n\t\t\tWebview::GenerateStorageToken());\n\t\tCore::App().settings().setTonsiteStorageToken(result.token);\n\t\tCore::App().saveSettingsDelayed();\n\t}\n\treturn result;\n}\n\nvoid Account::clearPref(std::string_view key) {\n\tconst auto i = _prefs.find(QByteArray(key.data(), key.size()));\n\tif (i == end(_prefs)) {\n\t\treturn;\n\t}\n\t_prefs.erase(i);\n\twritePrefsDelayed();\n}\n\nvoid Account::writePrefGeneric(\n\t\tstd::string_view key,\n\t\tconst QByteArray &value) {\n\tconst auto raw = QByteArray(key.data(), key.size());\n\tif (const auto i = _prefs.find(raw); i != end(_prefs)) {\n\t\tif (i->second == value) {\n\t\t\treturn;\n\t\t}\n\t\ti->second = value;\n\t} else {\n\t\t_prefs.emplace(raw, value);\n\t}\n\twritePrefsDelayed();\n}\n\nstd::optional<QByteArray> Account::readPrefGeneric(std::string_view key) {\n\tconst auto i = _prefs.find(QByteArray(key.data(), key.size()));\n\treturn (i != end(_prefs)) ? i->second : std::optional<QByteArray>();\n}\n\nvoid Account::writePrefsDelayed() {\n\t_prefsChanged = true;\n\t_writePrefsTimer.callOnce(kDelayedWriteTimeout);\n}\n\nvoid Account::writePrefs() {\n\t_writePrefsTimer.cancel();\n\tif (!_prefsChanged) {\n\t\treturn;\n\t}\n\t_prefsChanged = false;\n\n\tif (_prefs.empty()) {\n\t\tif (_prefsKey) {\n\t\t\tClearKey(_prefsKey, _basePath);\n\t\t\t_prefsKey = 0;\n\t\t\twriteMapDelayed();\n\t\t}\n\t} else {\n\t\tif (!_prefsKey) {\n\t\t\t_prefsKey = GenerateKey(_basePath);\n\t\t\twriteMapQueued();\n\t\t}\n\t\tquint32 size = sizeof(quint32);\n\t\tfor (const auto &[key, value] : _prefs) {\n\t\t\tsize += 2 * sizeof(quint32) + key.size() + value.size();\n\t\t}\n\n\t\tEncryptedDescriptor data(size);\n\t\tdata.stream << quint32(_prefs.size());\n\t\tfor (const auto &[key, value] : _prefs) {\n\t\t\tdata.stream << quint32(key.size()) << quint32(value.size());\n\t\t\tdata.stream.writeRawData(key.constData(), key.size());\n\t\t\tdata.stream.writeRawData(value.constData(), value.size());\n\t\t}\n\n\t\tFileWriteDescriptor file(_prefsKey, _basePath);\n\t\tfile.writeEncrypted(data, _localKey);\n\t}\n}\n\nvoid Account::readPrefs() {\n\tFileReadDescriptor prefs;\n\tif (!ReadEncryptedFile(prefs, _prefsKey, _basePath, _localKey)) {\n\t\tClearKey(_prefsKey, _basePath);\n\t\t_prefsKey = 0;\n\t\twriteMapDelayed();\n\t\treturn;\n\t}\n\n\tauto count = quint32();\n\tprefs.stream >> count;\n\tif (prefs.stream.status() != QDataStream::Ok) {\n\t\treturn;\n\t}\n\tauto map = base::flat_map<QByteArray, QByteArray>();\n\tmap.reserve(count);\n\tfor (auto i = quint32(); i != count; ++i) {\n\t\tauto keySize = quint32(), valueSize = quint32();\n\t\tprefs.stream >> keySize >> valueSize;\n\t\tauto key = QByteArray(keySize, Qt::Uninitialized);\n\t\tauto value = QByteArray(valueSize, Qt::Uninitialized);\n\t\tprefs.stream.readRawData(key.data(), keySize);\n\t\tprefs.stream.readRawData(value.data(), valueSize);\n\t\tif (prefs.stream.status() != QDataStream::Ok) {\n\t\t\treturn;\n\t\t}\n\t\tmap.emplace(std::move(key), std::move(value));\n\t}\n\t_prefs = std::move(map);\n}\n\n// Define your own pref types in the similar way.\ntemplate <>\nstd::optional<bool> Account::readPrefImpl<bool>(std::string_view key) {\n\tif (const auto data = readPrefGeneric(key)) {\n\t\treturn !data->isEmpty();\n\t}\n\treturn {};\n}\n\ntemplate <>\nvoid Account::writePrefImpl<bool>(std::string_view key, bool value) {\n\twritePrefGeneric(key, value ? \"\\x1\"_q : QByteArray());\n}\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_account.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"base/flags.h\"\n#include \"storage/cache/storage_cache_database.h\"\n#include \"data/stickers/data_stickers_set.h\"\n#include \"data/data_drafts.h\"\n#include \"webview/webview_common.h\"\n\nclass History;\n\nnamespace Core {\nclass FileLocation;\n} // namespace Core\n\nnamespace Export {\nstruct Settings;\n} // namespace Export\n\nnamespace Main {\nclass Account;\nclass SessionSettings;\n} // namespace Main\n\nnamespace Data {\nclass WallPaper;\n} // namespace Data\n\nnamespace MTP {\nclass Config;\nclass AuthKey;\nusing AuthKeyPtr = std::shared_ptr<AuthKey>;\n} // namespace MTP\n\nnamespace Storage {\nnamespace details {\nstruct ReadSettingsContext;\nstruct FileReadDescriptor;\n} // namespace details\n\nclass EncryptionKey;\n\nusing FileKey = quint64;\n\nenum class StartResult : uchar;\n\nstruct MessageDraft {\n\tFullReplyTo reply;\n\tSuggestOptions suggest;\n\tTextWithTags textWithTags;\n\tData::WebPageDraft webpage;\n};\n\nstruct MessageDraftSource {\n\tFn<MessageDraft()> draft;\n\tFn<MessageCursor()> cursor;\n};\n\nclass Account final {\npublic:\n\tAccount(not_null<Main::Account*> owner, const QString &dataName);\n\t~Account();\n\n\t[[nodiscard]] StartResult legacyStart(const QByteArray &passcode);\n\t[[nodiscard]] std::unique_ptr<MTP::Config> start(\n\t\tMTP::AuthKeyPtr localKey);\n\tvoid startAdded(MTP::AuthKeyPtr localKey);\n\t[[nodiscard]] int oldMapVersion() const {\n\t\treturn _oldMapVersion;\n\t}\n\n\t[[nodiscard]] QString tempDirectory() const;\n\t[[nodiscard]] QString supportModePath() const;\n\n\t[[nodiscard]] MTP::AuthKeyPtr peekLegacyLocalKey() const {\n\t\treturn _localKey;\n\t}\n\n\tvoid writeSessionSettings();\n\tvoid writeMtpData();\n\tvoid writeMtpConfig();\n\n\tvoid registerDraftSource(\n\t\tnot_null<History*> history,\n\t\tData::DraftKey key,\n\t\tMessageDraftSource source);\n\tvoid unregisterDraftSource(\n\t\tnot_null<History*> history,\n\t\tData::DraftKey key);\n\tvoid writeDrafts(not_null<History*> history);\n\tvoid readDraftsWithCursors(not_null<History*> history);\n\tvoid writeDraftCursors(not_null<History*> history);\n\t[[nodiscard]] bool hasDraftCursors(PeerId peerId);\n\t[[nodiscard]] bool hasDraft(PeerId peerId);\n\n\tvoid writeFileLocation(\n\t\tMediaKey location,\n\t\tconst Core::FileLocation &local);\n\t[[nodiscard]] Core::FileLocation readFileLocation(MediaKey location);\n\tvoid removeFileLocation(MediaKey location);\n\n\tvoid updateDownloads(Fn<std::optional<QByteArray>()> downloadsSerialize);\n\t[[nodiscard]] QByteArray downloadsSerialized() const;\n\n\t[[nodiscard]] EncryptionKey cacheKey() const;\n\t[[nodiscard]] QString cachePath() const;\n\t[[nodiscard]] Cache::Database::Settings cacheSettings() const;\n\tvoid updateCacheSettings(\n\t\tCache::Database::SettingsUpdate &update,\n\t\tCache::Database::SettingsUpdate &updateBig);\n\n\t[[nodiscard]] EncryptionKey cacheBigFileKey() const;\n\t[[nodiscard]] QString cacheBigFilePath() const;\n\t[[nodiscard]] Cache::Database::Settings cacheBigFileSettings() const;\n\n\tvoid writeInstalledStickers();\n\tvoid writeFeaturedStickers();\n\tvoid writeRecentStickers();\n\tvoid writeFavedStickers();\n\tvoid writeArchivedStickers();\n\tvoid writeArchivedMasks();\n\tvoid readInstalledStickers();\n\tvoid readFeaturedStickers();\n\tvoid readRecentStickers();\n\tvoid readFavedStickers();\n\tvoid readArchivedStickers();\n\tvoid readArchivedMasks();\n\tvoid writeSavedGifs();\n\tvoid readSavedGifs();\n\tvoid writeInstalledMasks();\n\tvoid writeRecentMasks();\n\tvoid readInstalledMasks();\n\tvoid readRecentMasks();\n\tvoid writeInstalledCustomEmoji();\n\tvoid writeFeaturedCustomEmoji();\n\tvoid readInstalledCustomEmoji();\n\tvoid readFeaturedCustomEmoji();\n\n\tvoid writeRecentHashtagsAndBots();\n\tvoid readRecentHashtagsAndBots();\n\tvoid saveRecentSentHashtags(const QString &text);\n\tvoid saveRecentSearchHashtags(const QString &text);\n\n\tvoid writeExportSettings(const Export::Settings &settings);\n\t[[nodiscard]] Export::Settings readExportSettings();\n\n\tvoid setMediaLastPlaybackPosition(DocumentId id, crl::time time);\n\t[[nodiscard]] crl::time mediaLastPlaybackPosition(DocumentId id) const;\n\n\tvoid writeSearchSuggestionsDelayed();\n\tvoid writeSearchSuggestionsIfNeeded();\n\tvoid writeSearchSuggestions();\n\tvoid readSearchSuggestions();\n\n\tvoid writeSelf();\n\n\t// Read self is special, it can't get session from account, because\n\t// it is not really there yet - it is still being constructed.\n\tvoid readSelf(\n\t\tnot_null<Main::Session*> session,\n\t\tconst QByteArray& serialized,\n\t\tint32 streamVersion);\n\n\tvoid markPeerTrustedOpenGame(PeerId peerId);\n\t[[nodiscard]] bool isPeerTrustedOpenGame(PeerId peerId);\n\tvoid markPeerTrustedPayment(PeerId peerId);\n\t[[nodiscard]] bool isPeerTrustedPayment(PeerId peerId);\n\tvoid markPeerTrustedOpenWebView(PeerId peerId);\n\t[[nodiscard]] bool isPeerTrustedOpenWebView(PeerId peerId);\n\tvoid markPeerTrustedPayForMessage(PeerId peerId, int starsPerMessage);\n\t[[nodiscard]] bool isPeerTrustedPayForMessage(\n\t\tPeerId peerId,\n\t\tint starsPerMessage);\n\t[[nodiscard]] bool peerTrustedPayForMessageRead() const;\n\t[[nodiscard]] bool hasPeerTrustedPayForMessageEntry(PeerId peerId) const;\n\tvoid clearPeerTrustedPayForMessage(PeerId peerId);\n\n\ttemplate <typename Type, typename Other>\n\tvoid writePref(std::string_view key, Other &&value) {\n\t\twritePrefImpl<Type>(key, std::forward<Other>(value));\n\t}\n\tvoid clearPref(std::string_view key);\n\n\ttemplate <typename Type, typename Other = Type>\n\t[[nodiscard]] Type readPref(\n\t\t\tstd::string_view key,\n\t\t\tOther &&fallback = Type()) {\n\t\treturn readPrefImpl<Type>(key).value_or(std::forward<Other>(fallback));\n\t}\n\n\tvoid enforceModernStorageIdBots();\n\t[[nodiscard]] Webview::StorageId resolveStorageIdBots();\n\t[[nodiscard]] Webview::StorageId resolveStorageIdOther();\n\n\t[[nodiscard]] QImage readRoundPlaceholder();\n\tvoid writeRoundPlaceholder(const QImage &placeholder);\n\n\t[[nodiscard]] QByteArray readInlineBotsDownloads();\n\tvoid writeInlineBotsDownloads(const QByteArray &bytes);\n\n\tvoid writeBotStorage(PeerId botId, const QByteArray &serialized);\n\t[[nodiscard]] QByteArray readBotStorage(PeerId botId);\n\n\t[[nodiscard]] bool encrypt(\n\t\tconst void *src,\n\t\tvoid *dst,\n\t\tuint32 len,\n\t\tconst void *key128) const;\n\t[[nodiscard]] bool decrypt(\n\t\tconst void *src,\n\t\tvoid *dst,\n\t\tuint32 len,\n\t\tconst void *key128) const;\n\n\tvoid reset();\n\nprivate:\n\tenum class ReadMapResult {\n\t\tSuccess,\n\t\tIncorrectPasscode,\n\t\tFailed,\n\t};\n\tenum class PeerTrustFlag : uchar {\n\t\tNoOpenGame        = (1 << 0),\n\t\tPayment           = (1 << 1),\n\t\tOpenWebView       = (1 << 2),\n\t};\n\tfriend inline constexpr bool is_flag_type(PeerTrustFlag) { return true; };\n\n\t[[nodiscard]] base::flat_set<QString> collectGoodNames() const;\n\t[[nodiscard]] auto prepareReadSettingsContext() const\n\t\t-> details::ReadSettingsContext;\n\n\tReadMapResult readMapWith(\n\t\tMTP::AuthKeyPtr localKey,\n\t\tconst QByteArray &legacyPasscode = QByteArray());\n\tvoid clearLegacyFiles();\n\tvoid writeMapDelayed();\n\tvoid writeMapQueued();\n\tvoid writeMap();\n\n\tvoid readLocations();\n\tvoid writeLocations();\n\tvoid writeLocationsQueued();\n\tvoid writeLocationsDelayed();\n\n\tvoid readPrefs();\n\tvoid writePrefs();\n\tvoid writePrefsDelayed();\n\n\tstd::unique_ptr<Main::SessionSettings> readSessionSettings();\n\tvoid writeSessionSettings(Main::SessionSettings *stored);\n\n\tstd::unique_ptr<MTP::Config> readMtpConfig();\n\tvoid readMtpData();\n\tstd::unique_ptr<Main::SessionSettings> applyReadContext(\n\t\tdetails::ReadSettingsContext &&context);\n\n\tvoid readDraftCursors(PeerId peerId, Data::HistoryDrafts &map);\n\tvoid readDraftCursorsLegacy(\n\t\tPeerId peerId,\n\t\tdetails::FileReadDescriptor &draft,\n\t\tquint64 draftPeerSerialized,\n\t\tData::HistoryDrafts &map);\n\tvoid clearDraftCursors(PeerId peerId);\n\tvoid readDraftsWithCursorsLegacy(\n\t\tnot_null<History*> history,\n\t\tdetails::FileReadDescriptor &draft,\n\t\tquint64 draftPeerSerialized);\n\n\tvoid writeStickerSet(\n\t\tQDataStream &stream,\n\t\tconst Data::StickersSet &set);\n\ttemplate <typename CheckSet>\n\tvoid writeStickerSets(\n\t\tFileKey &stickersKey,\n\t\tCheckSet checkSet,\n\t\tconst Data::StickersSetsOrder &order);\n\tvoid readStickerSets(\n\t\tFileKey &stickersKey,\n\t\tData::StickersSetsOrder *outOrder = nullptr,\n\t\tData::StickersSetFlags readingFlags = 0);\n\tvoid importOldRecentStickers();\n\n\tvoid readTrustedPeers();\n\tvoid writeTrustedPeers();\n\n\tvoid readMediaLastPlaybackPositions();\n\tvoid writeMediaLastPlaybackPositions();\n\n\tstd::optional<RecentHashtagPack> saveRecentHashtags(\n\t\tFn<RecentHashtagPack()> getPack,\n\t\tconst QString &text);\n\n\ttemplate <typename Type>\n\tvoid writePrefImpl(std::string_view key, Type value);\n\n\ttemplate <typename Type>\n\t[[nodiscard]] std::optional<Type> readPrefImpl(std::string_view key);\n\n\tvoid writePrefGeneric(std::string_view key, const QByteArray &value);\n\t[[nodiscard]] std::optional<QByteArray> readPrefGeneric(\n\t\tstd::string_view key);\n\n\tconst not_null<Main::Account*> _owner;\n\tconst QString _dataName;\n\tconst FileKey _dataNameKey = 0;\n\tconst QString _basePath;\n\tconst QString _tempPath;\n\tconst QString _databasePath;\n\n\tMTP::AuthKeyPtr _localKey;\n\n\tbase::flat_map<PeerId, FileKey> _draftsMap;\n\tbase::flat_map<PeerId, FileKey> _draftCursorsMap;\n\tbase::flat_map<PeerId, bool> _draftsNotReadMap;\n\tbase::flat_map<\n\t\tnot_null<History*>,\n\t\tbase::flat_map<Data::DraftKey, MessageDraftSource>> _draftSources;\n\tbase::flat_map<PeerId, FileKey> _botStoragesMap;\n\tbase::flat_map<PeerId, bool> _botStoragesNotReadMap;\n\n\tQMultiMap<MediaKey, Core::FileLocation> _fileLocations;\n\tQMap<QString, QPair<MediaKey, Core::FileLocation>> _fileLocationPairs;\n\tQMap<MediaKey, MediaKey> _fileLocationAliases;\n\n\tQByteArray _downloadsSerialized;\n\tFn<std::optional<QByteArray>()> _downloadsSerialize;\n\n\tFileKey _prefsKey = 0;\n\tFileKey _locationsKey = 0;\n\tFileKey _trustedPeersKey = 0;\n\tFileKey _installedStickersKey = 0;\n\tFileKey _featuredStickersKey = 0;\n\tFileKey _recentStickersKey = 0;\n\tFileKey _favedStickersKey = 0;\n\tFileKey _archivedStickersKey = 0;\n\tFileKey _archivedMasksKey = 0;\n\tFileKey _savedGifsKey = 0;\n\tFileKey _recentStickersKeyOld = 0;\n\tFileKey _legacyBackgroundKeyDay = 0;\n\tFileKey _legacyBackgroundKeyNight = 0;\n\tFileKey _settingsKey = 0;\n\tFileKey _recentHashtagsAndBotsKey = 0;\n\tFileKey _exportSettingsKey = 0;\n\tFileKey _installedMasksKey = 0;\n\tFileKey _recentMasksKey = 0;\n\tFileKey _installedCustomEmojiKey = 0;\n\tFileKey _featuredCustomEmojiKey = 0;\n\tFileKey _archivedCustomEmojiKey = 0;\n\tFileKey _searchSuggestionsKey = 0;\n\tFileKey _roundPlaceholderKey = 0;\n\tFileKey _inlineBotsDownloadsKey = 0;\n\tFileKey _mediaLastPlaybackPositionsKey = 0;\n\n\tqint64 _cacheTotalSizeLimit = 0;\n\tqint64 _cacheBigFileTotalSizeLimit = 0;\n\tqint32 _cacheTotalTimeLimit = 0;\n\tqint32 _cacheBigFileTotalTimeLimit = 0;\n\n\tbase::flat_map<PeerId, base::flags<PeerTrustFlag>> _trustedPeers;\n\tbase::flat_map<PeerId, int> _trustedPayPerMessage;\n\tbool _trustedPeersRead = false;\n\tbool _readingUserSettings = false;\n\tbool _recentHashtagsAndBotsWereRead = false;\n\tbool _searchSuggestionsRead = false;\n\tbool _inlineBotsDownloadsRead = false;\n\tbool _mediaLastPlaybackPositionsRead = false;\n\n\tstd::vector<std::pair<DocumentId, crl::time>> _mediaLastPlaybackPosition;\n\n\tWebview::StorageId _webviewStorageIdBots;\n\tWebview::StorageId _webviewStorageIdOther;\n\n\tbase::flat_map<QByteArray, QByteArray> _prefs;\n\n\tint _oldMapVersion = 0;\n\n\tbase::Timer _writeMapTimer;\n\tbase::Timer _writePrefsTimer;\n\tbase::Timer _writeLocationsTimer;\n\tbase::Timer _writeSearchSuggestionsTimer;\n\tbool _mapChanged = false;\n\tbool _prefsChanged = false;\n\tbool _locationsChanged = false;\n\n\tQImage _roundPlaceholder;\n\n};\n\n[[nodiscard]] Webview::StorageId TonSiteStorageId();\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_cloud_blob.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/storage_cloud_blob.h\"\n\n#include \"base/zlib_help.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/text/format_values.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n\nnamespace Storage::CloudBlob {\n\nnamespace {\n\nQByteArray ReadFinalFile(const QString &path) {\n\tconstexpr auto kMaxZipSize = 10 * 1024 * 1024;\n\tauto file = QFile(path);\n\tif (file.size() > kMaxZipSize || !file.open(QIODevice::ReadOnly)) {\n\t\treturn QByteArray();\n\t}\n\treturn file.readAll();\n}\n\nbool ExtractZipFile(zlib::FileToRead &zip, const QString path) {\n\tconstexpr auto kMaxSize = 25 * 1024 * 1024;\n\tconst auto content = zip.readCurrentFileContent(kMaxSize);\n\tif (content.isEmpty() || zip.error() != UNZ_OK) {\n\t\treturn false;\n\t}\n\tauto file = QFile(path);\n\treturn file.open(QIODevice::WriteOnly)\n\t\t&& (file.write(content) == content.size());\n}\n\n} // namespace\n\nbool UnpackBlob(\n\t\tconst QString &path,\n\t\tconst QString &folder,\n\t\tFn<bool(const QString &)> checkNameCallback) {\n\tconst auto bytes = ReadFinalFile(path);\n\tif (bytes.isEmpty()) {\n\t\treturn false;\n\t}\n\tauto zip = zlib::FileToRead(bytes);\n\tif (zip.goToFirstFile() != UNZ_OK) {\n\t\treturn false;\n\t}\n\tdo {\n\t\tconst auto name = zip.getCurrentFileName();\n\t\tconst auto path = folder + '/' + name;\n\t\tif (checkNameCallback(name) && !ExtractZipFile(zip, path)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst auto jump = zip.goToNextFile();\n\t\tif (jump == UNZ_END_OF_LIST_OF_FILE) {\n\t\t\tbreak;\n\t\t} else if (jump != UNZ_OK) {\n\t\t\treturn false;\n\t\t}\n\t} while (true);\n\treturn true;\n}\n\nQString StateDescription(const BlobState &state, tr::phrase<> activeText) {\n\treturn v::match(state, [](const Available &data) {\n\t\treturn tr::lng_emoji_set_download(\n\t\t\ttr::now,\n\t\t\tlt_size,\n\t\t\tUi::FormatSizeText(data.size));\n\t}, [](const Ready &data) -> QString {\n\t\treturn tr::lng_emoji_set_ready(tr::now);\n\t}, [&](const Active &data) -> QString {\n\t\treturn activeText(tr::now);\n\t}, [](const Loading &data) {\n\t\tconst auto percent = (data.size > 0)\n\t\t\t? std::clamp((data.already * 100) / float64(data.size), 0., 100.)\n\t\t\t: 0.;\n\t\treturn tr::lng_emoji_set_loading(\n\t\t\ttr::now,\n\t\t\tlt_percent,\n\t\t\tQString::number(int(base::SafeRound(percent))) + '%',\n\t\t\tlt_progress,\n\t\t\tUi::FormatDownloadText(data.already, data.size));\n\t}, [](const Failed &data) {\n\t\treturn tr::lng_attach_failed(tr::now);\n\t});\n}\n\nBlobLoader::BlobLoader(\n\tQObject *parent,\n\tnot_null<Main::Session*> session,\n\tint id,\n\tMTP::DedicatedLoader::Location location,\n\tconst QString &folder,\n\tint64 size)\n: QObject(parent)\n, _folder(folder)\n, _id(id)\n, _state(Loading{ 0, size })\n, _mtproto(session.get()) {\n\tconst auto ready = [=](std::unique_ptr<MTP::DedicatedLoader> loader) {\n\t\tif (loader) {\n\t\t\tsetImplementation(std::move(loader));\n\t\t} else {\n\t\t\tfail();\n\t\t}\n\t};\n\tMTP::StartDedicatedLoader(&_mtproto, location, _folder, ready);\n}\n\nint BlobLoader::id() const {\n\treturn _id;\n}\n\nrpl::producer<BlobState> BlobLoader::state() const {\n\treturn _state.value();\n}\n\nvoid BlobLoader::setImplementation(\n\t\tstd::unique_ptr<MTP::DedicatedLoader> loader) {\n\t_implementation = std::move(loader);\n\t_state = _implementation->progress(\n\t) | rpl::map([](const Loading &state) {\n\t\treturn BlobState(state);\n\t});\n\t_implementation->failed(\n\t) | rpl::on_next([=] {\n\t\tfail();\n\t}, _implementation->lifetime());\n\n\t_implementation->ready(\n\t) | rpl::on_next([=](const QString &filepath) {\n\t\tunpack(filepath);\n\t}, _implementation->lifetime());\n\n\tQDir(_folder).removeRecursively();\n\t_implementation->start();\n}\n\nvoid BlobLoader::fail() {\n\t_state = Failed();\n}\n\n} // namespace Storage::CloudBlob\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_cloud_blob.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/dedicated_file_loader.h\"\n\nnamespace tr {\ntemplate <typename ...>\nstruct phrase;\n} // namespace tr\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Storage::CloudBlob {\n\nconstexpr auto kCloudLocationUsername = \"tdhbcfiles\"_cs;\n\nstruct Blob {\n\tint id = 0;\n\tint postId = 0;\n\tint64 size = 0;\n\tQString name;\n};\n\nstruct Available {\n\tint64 size = 0;\n\n\tinline bool operator<(const Available &other) const {\n\t\treturn size < other.size;\n\t}\n\tinline bool operator==(const Available &other) const {\n\t\treturn size == other.size;\n\t}\n};\nstruct Ready {\n\tinline bool operator<(const Ready &other) const {\n\t\treturn false;\n\t}\n\tinline bool operator==(const Ready &other) const {\n\t\treturn true;\n\t}\n};\nstruct Active {\n\tinline bool operator<(const Active &other) const {\n\t\treturn false;\n\t}\n\tinline bool operator==(const Active &other) const {\n\t\treturn true;\n\t}\n};\nstruct Failed {\n\tinline bool operator<(const Failed &other) const {\n\t\treturn false;\n\t}\n\tinline bool operator==(const Failed &other) const {\n\t\treturn true;\n\t}\n};\n\nusing Loading = MTP::DedicatedLoader::Progress;\nusing BlobState = std::variant<\n\tAvailable,\n\tReady,\n\tActive,\n\tFailed,\n\tLoading>;\n\nbool UnpackBlob(\n\tconst QString &path,\n\tconst QString &folder,\n\tFn<bool(const QString &)> checkNameCallback);\n\nQString StateDescription(const BlobState &state, tr::phrase<> activeText);\n\nclass BlobLoader : public QObject {\npublic:\n\tBlobLoader(\n\t\tQObject *parent,\n\t\tnot_null<Main::Session*> session,\n\t\tint id,\n\t\tMTP::DedicatedLoader::Location location,\n\t\tconst QString &folder,\n\t\tint64 size);\n\n\tint id() const;\n\n\trpl::producer<BlobState> state() const;\n\tvirtual void destroy() = 0;\n\tvirtual void unpack(const QString &path) = 0;\n\nprotected:\n\tvirtual void fail();\n\n\tconst QString _folder;\n\nprivate:\n\tvoid setImplementation(std::unique_ptr<MTP::DedicatedLoader> loader);\n\n\tint _id = 0;\n\trpl::variable<BlobState> _state;\n\n\tMTP::WeakInstance _mtproto;\n\n\tstd::unique_ptr<MTP::DedicatedLoader> _implementation;\n\n};\n\n} // namespace Storage::CloudBlob\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_domain.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/storage_domain.h\"\n\n#include \"storage/details/storage_file_utilities.h\"\n#include \"storage/serialize_common.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_account.h\"\n#include \"base/random.h\"\n\nnamespace Storage {\nnamespace {\n\nusing namespace details;\n\n[[nodiscard]] QString BaseGlobalPath() {\n\treturn cWorkingDir() + u\"tdata/\"_q;\n}\n\n[[nodiscard]] QString ComputeKeyName(const QString &dataName) {\n\t// We dropped old test authorizations when migrated to multi auth.\n\t//return \"key_\" + dataName + (cTestMode() ? \"[test]\" : \"\");\n\treturn \"key_\" + dataName;\n}\n\n} // namespace\n\nDomain::Domain(not_null<Main::Domain*> owner, const QString &dataName)\n: _owner(owner)\n, _dataName(dataName) {\n}\n\nDomain::~Domain() = default;\n\nStartResult Domain::start(const QByteArray &passcode) {\n\tconst auto modern = startModern(passcode);\n\tif (modern == StartModernResult::Success) {\n\t\tif (_oldVersion < AppVersion) {\n\t\t\twriteAccounts();\n\t\t}\n\t\treturn StartResult::Success;\n\t} else if (modern == StartModernResult::IncorrectPasscode) {\n\t\treturn StartResult::IncorrectPasscode;\n\t} else if (modern == StartModernResult::Failed) {\n\t\tstartFromScratch();\n\t\treturn StartResult::Success;\n\t}\n\tauto legacy = std::make_unique<Main::Account>(_owner, _dataName, 0);\n\tconst auto result = legacy->legacyStart(passcode);\n\tif (result == StartResult::Success) {\n\t\t_oldVersion = legacy->local().oldMapVersion();\n\t\tstartWithSingleAccount(passcode, std::move(legacy));\n\t}\n\treturn result;\n}\n\nvoid Domain::startAdded(\n\t\tnot_null<Main::Account*> account,\n\t\tstd::unique_ptr<MTP::Config> config) {\n\tExpects(_localKey != nullptr);\n\n\taccount->prepareToStartAdded(_localKey);\n\taccount->start(std::move(config));\n}\n\nvoid Domain::startWithSingleAccount(\n\t\tconst QByteArray &passcode,\n\t\tstd::unique_ptr<Main::Account> account) {\n\tExpects(account != nullptr);\n\n\tif (auto localKey = account->local().peekLegacyLocalKey()) {\n\t\t_localKey = std::move(localKey);\n\t\tencryptLocalKey(passcode);\n\t\taccount->start(nullptr);\n\t} else {\n\t\tgenerateLocalKey();\n\t\taccount->start(account->prepareToStart(_localKey));\n\t}\n\t_owner->accountAddedInStorage(Main::Domain::AccountWithIndex{\n\t\t.account = std::move(account)\n\t});\n\twriteAccounts();\n}\n\nvoid Domain::generateLocalKey() {\n\tExpects(_localKey == nullptr);\n\tExpects(_passcodeKeySalt.isEmpty());\n\tExpects(_passcodeKeyEncrypted.isEmpty());\n\n\tauto pass = QByteArray(MTP::AuthKey::kSize, Qt::Uninitialized);\n\tauto salt = QByteArray(LocalEncryptSaltSize, Qt::Uninitialized);\n\tbase::RandomFill(pass.data(), pass.size());\n\tbase::RandomFill(salt.data(), salt.size());\n\t_localKey = CreateLocalKey(pass, salt);\n\n\tencryptLocalKey(QByteArray());\n}\n\nvoid Domain::encryptLocalKey(const QByteArray &passcode) {\n\t_passcodeKeySalt.resize(LocalEncryptSaltSize);\n\tbase::RandomFill(_passcodeKeySalt.data(), _passcodeKeySalt.size());\n\t_passcodeKey = CreateLocalKey(passcode, _passcodeKeySalt);\n\n\tEncryptedDescriptor passKeyData(MTP::AuthKey::kSize);\n\t_localKey->write(passKeyData.stream);\n\t_passcodeKeyEncrypted = PrepareEncrypted(passKeyData, _passcodeKey);\n\t_hasLocalPasscode = !passcode.isEmpty();\n}\n\nDomain::StartModernResult Domain::startModern(\n\t\tconst QByteArray &passcode) {\n\tconst auto name = ComputeKeyName(_dataName);\n\n\tFileReadDescriptor keyData;\n\tif (!ReadFile(keyData, name, BaseGlobalPath())) {\n\t\treturn StartModernResult::Empty;\n\t}\n\tLOG((\"App Info: reading accounts info...\"));\n\n\tQByteArray salt, keyEncrypted, infoEncrypted;\n\tkeyData.stream >> salt >> keyEncrypted >> infoEncrypted;\n\tif (!CheckStreamStatus(keyData.stream)) {\n\t\treturn StartModernResult::Failed;\n\t}\n\n\tif (salt.size() != LocalEncryptSaltSize) {\n\t\tLOG((\"App Error: bad salt in info file, size: %1\").arg(salt.size()));\n\t\treturn StartModernResult::Failed;\n\t}\n\t_passcodeKey = CreateLocalKey(passcode, salt);\n\n\tEncryptedDescriptor keyInnerData, info;\n\tif (!DecryptLocal(keyInnerData, keyEncrypted, _passcodeKey)) {\n\t\tLOG((\"App Info: could not decrypt pass-protected key from info file, \"\n\t\t\t\"maybe bad password...\"));\n\t\treturn StartModernResult::IncorrectPasscode;\n\t}\n\tauto key = Serialize::read<MTP::AuthKey::Data>(keyInnerData.stream);\n\tif (keyInnerData.stream.status() != QDataStream::Ok\n\t\t|| !keyInnerData.stream.atEnd()) {\n\t\tLOG((\"App Error: could not read pass-protected key from info file\"));\n\t\treturn StartModernResult::Failed;\n\t}\n\t_localKey = std::make_shared<MTP::AuthKey>(key);\n\n\t_passcodeKeyEncrypted = keyEncrypted;\n\t_passcodeKeySalt = salt;\n\t_hasLocalPasscode = !passcode.isEmpty();\n\n\tif (!DecryptLocal(info, infoEncrypted, _localKey)) {\n\t\tLOG((\"App Error: could not decrypt info.\"));\n\t\treturn StartModernResult::Failed;\n\t}\n\tLOG((\"App Info: reading encrypted info...\"));\n\tauto count = qint32();\n\tinfo.stream >> count;\n\tif (count <= 0 || count > Main::Domain::kPremiumMaxAccounts) {\n\t\tLOG((\"App Error: bad accounts count: %1\").arg(count));\n\t\treturn StartModernResult::Failed;\n\t}\n\n\t_oldVersion = keyData.version;\n\n\tauto tried = base::flat_set<int>();\n\tauto sessions = base::flat_set<uint64>();\n\tauto active = 0;\n\tfor (auto i = 0; i != count; ++i) {\n\t\tauto index = qint32();\n\t\tinfo.stream >> index;\n\t\tif (index >= 0\n\t\t\t&& index < Main::Domain::kPremiumMaxAccounts\n\t\t\t&& tried.emplace(index).second) {\n\t\t\tauto account = std::make_unique<Main::Account>(\n\t\t\t\t_owner,\n\t\t\t\t_dataName,\n\t\t\t\tindex);\n\t\t\tauto config = account->prepareToStart(_localKey);\n\t\t\tconst auto sessionId = account->willHaveSessionUniqueId(\n\t\t\t\tconfig.get());\n\t\t\tif (!sessions.contains(sessionId)\n\t\t\t\t&& (sessionId != 0 || (sessions.empty() && i + 1 == count))) {\n\t\t\t\tif (sessions.empty()) {\n\t\t\t\t\tactive = index;\n\t\t\t\t}\n\t\t\t\taccount->start(std::move(config));\n\t\t\t\t_owner->accountAddedInStorage({\n\t\t\t\t\t.index = index,\n\t\t\t\t\t.account = std::move(account)\n\t\t\t\t});\n\t\t\t\tsessions.emplace(sessionId);\n\t\t\t}\n\t\t}\n\t}\n\tif (sessions.empty()) {\n\t\tLOG((\"App Error: no accounts read.\"));\n\t\treturn StartModernResult::Failed;\n\t}\n\n\tif (!info.stream.atEnd()) {\n\t\tinfo.stream >> active;\n\t}\n\t_owner->activateFromStorage(active);\n\n\tEnsures(!sessions.empty());\n\treturn StartModernResult::Success;\n}\n\nvoid Domain::writeAccounts() {\n\tExpects(!_owner->accounts().empty());\n\n\tconst auto path = BaseGlobalPath();\n\tif (!QDir().exists(path)) {\n\t\tQDir().mkpath(path);\n\t}\n\n\tFileWriteDescriptor key(ComputeKeyName(_dataName), path);\n\tkey.writeData(_passcodeKeySalt);\n\tkey.writeData(_passcodeKeyEncrypted);\n\n\tconst auto &list = _owner->accounts();\n\n\tauto keySize = sizeof(qint32) + sizeof(qint32) * list.size();\n\n\tEncryptedDescriptor keyData(keySize);\n\tkeyData.stream << qint32(list.size());\n\tfor (const auto &[index, account] : list) {\n\t\tkeyData.stream << qint32(index);\n\t}\n\tkeyData.stream << qint32(_owner->activeForStorage());\n\tkey.writeEncrypted(keyData, _localKey);\n}\n\nvoid Domain::startFromScratch() {\n\tstartWithSingleAccount(\n\t\tQByteArray(),\n\t\tstd::make_unique<Main::Account>(_owner, _dataName, 0));\n}\n\nbool Domain::checkPasscode(const QByteArray &passcode) const {\n\tExpects(!_passcodeKeySalt.isEmpty());\n\tExpects(_passcodeKey != nullptr);\n\n\tconst auto checkKey = CreateLocalKey(passcode, _passcodeKeySalt);\n\treturn checkKey->equals(_passcodeKey);\n}\n\nvoid Domain::setPasscode(const QByteArray &passcode) {\n\tExpects(!_passcodeKeySalt.isEmpty());\n\tExpects(_localKey != nullptr);\n\n\tencryptLocalKey(passcode);\n\twriteAccounts();\n\n\t_passcodeKeyChanged.fire({});\n}\n\nint Domain::oldVersion() const {\n\treturn _oldVersion;\n}\n\nvoid Domain::clearOldVersion() {\n\t_oldVersion = 0;\n}\n\nrpl::producer<> Domain::localPasscodeChanged() const {\n\treturn _passcodeKeyChanged.events();\n}\n\nbool Domain::hasLocalPasscode() const {\n\treturn _hasLocalPasscode;\n}\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_domain.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace MTP {\nclass Config;\nclass AuthKey;\nusing AuthKeyPtr = std::shared_ptr<AuthKey>;\n} // namespace MTP\n\nnamespace Main {\nclass Account;\nclass Domain;\n} // namespace Main\n\nnamespace Storage {\n\nenum class StartResult : uchar {\n\tSuccess,\n\tIncorrectPasscode,\n\tIncorrectPasscodeLegacy,\n};\n\nclass Domain final {\npublic:\n\tDomain(not_null<Main::Domain*> owner, const QString &dataName);\n\t~Domain();\n\n\t[[nodiscard]] StartResult start(const QByteArray &passcode);\n\tvoid startAdded(\n\t\tnot_null<Main::Account*> account,\n\t\tstd::unique_ptr<MTP::Config> config);\n\tvoid writeAccounts();\n\tvoid startFromScratch();\n\n\t[[nodiscard]] bool checkPasscode(const QByteArray &passcode) const;\n\tvoid setPasscode(const QByteArray &passcode);\n\n\t[[nodiscard]] int oldVersion() const;\n\tvoid clearOldVersion();\n\n\t[[nodiscard]] rpl::producer<> localPasscodeChanged() const;\n\t[[nodiscard]] bool hasLocalPasscode() const;\n\nprivate:\n\tenum class StartModernResult {\n\t\tSuccess,\n\t\tIncorrectPasscode,\n\t\tFailed,\n\t\tEmpty,\n\t};\n\n\t[[nodiscard]] StartModernResult startModern(const QByteArray &passcode);\n\tvoid startWithSingleAccount(\n\t\tconst QByteArray &passcode,\n\t\tstd::unique_ptr<Main::Account> account);\n\tvoid generateLocalKey();\n\tvoid encryptLocalKey(const QByteArray &passcode);\n\n\tconst not_null<Main::Domain*> _owner;\n\tconst QString _dataName;\n\n\tMTP::AuthKeyPtr _localKey;\n\tMTP::AuthKeyPtr _passcodeKey;\n\tQByteArray _passcodeKeySalt;\n\tQByteArray _passcodeKeyEncrypted;\n\tint _oldVersion = 0;\n\n\tbool _hasLocalPasscode = false;\n\trpl::event_stream<> _passcodeKeyChanged;\n\n};\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_facade.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/storage_facade.h\"\n\n#include \"storage/storage_shared_media.h\"\n#include \"storage/storage_user_photos.h\"\n\nnamespace Storage {\n\nclass Facade::Impl {\npublic:\n\tvoid add(SharedMediaAddNew &&query);\n\tvoid add(SharedMediaAddExisting &&query);\n\tvoid add(SharedMediaAddSlice &&query);\n\tvoid remove(SharedMediaRemoveOne &&query);\n\tvoid remove(SharedMediaRemoveAll &&query);\n\tvoid invalidate(SharedMediaInvalidateBottom &&query);\n\tvoid unload(SharedMediaUnloadThread &&query);\n\trpl::producer<SharedMediaResult> query(SharedMediaQuery &&query) const;\n\tSharedMediaResult snapshot(const SharedMediaQuery &query) const;\n\tbool empty(const SharedMediaKey &key) const;\n\trpl::producer<SharedMediaSliceUpdate> sharedMediaSliceUpdated() const;\n\trpl::producer<SharedMediaRemoveOne> sharedMediaOneRemoved() const;\n\trpl::producer<SharedMediaRemoveAll> sharedMediaAllRemoved() const;\n\trpl::producer<SharedMediaInvalidateBottom> sharedMediaBottomInvalidated() const;\n\n\tvoid add(UserPhotosSetBack &&query);\n\tvoid add(UserPhotosAddNew &&query);\n\tvoid add(UserPhotosAddSlice &&query);\n\tvoid remove(UserPhotosRemoveOne &&query);\n\tvoid remove(UserPhotosRemoveAfter &&query);\n\tvoid replace(UserPhotosReplace &&query);\n\trpl::producer<UserPhotosResult> query(UserPhotosQuery &&query) const;\n\trpl::producer<UserPhotosSliceUpdate> userPhotosSliceUpdated() const;\n\nprivate:\n\tSharedMedia _sharedMedia;\n\tUserPhotos _userPhotos;\n\n};\n\nvoid Facade::Impl::add(SharedMediaAddNew &&query) {\n\t_sharedMedia.add(std::move(query));\n}\n\nvoid Facade::Impl::add(SharedMediaAddExisting &&query) {\n\t_sharedMedia.add(std::move(query));\n}\n\nvoid Facade::Impl::add(SharedMediaAddSlice &&query) {\n\t_sharedMedia.add(std::move(query));\n}\n\nvoid Facade::Impl::remove(SharedMediaRemoveOne &&query) {\n\t_sharedMedia.remove(std::move(query));\n}\n\nvoid Facade::Impl::remove(SharedMediaRemoveAll &&query) {\n\t_sharedMedia.remove(std::move(query));\n}\n\nvoid Facade::Impl::invalidate(SharedMediaInvalidateBottom &&query) {\n\t_sharedMedia.invalidate(std::move(query));\n}\n\nvoid Facade::Impl::unload(SharedMediaUnloadThread &&query) {\n\t_sharedMedia.unload(std::move(query));\n}\n\nrpl::producer<SharedMediaResult> Facade::Impl::query(SharedMediaQuery &&query) const {\n\treturn _sharedMedia.query(std::move(query));\n}\n\nSharedMediaResult Facade::Impl::snapshot(const SharedMediaQuery &query) const {\n\treturn _sharedMedia.snapshot(query);\n}\n\nbool Facade::Impl::empty(const SharedMediaKey &key) const {\n\treturn _sharedMedia.empty(key);\n}\n\nrpl::producer<SharedMediaSliceUpdate> Facade::Impl::sharedMediaSliceUpdated() const {\n\treturn _sharedMedia.sliceUpdated();\n}\n\nrpl::producer<SharedMediaRemoveOne> Facade::Impl::sharedMediaOneRemoved() const {\n\treturn _sharedMedia.oneRemoved();\n}\n\nrpl::producer<SharedMediaRemoveAll> Facade::Impl::sharedMediaAllRemoved() const {\n\treturn _sharedMedia.allRemoved();\n}\n\nrpl::producer<SharedMediaInvalidateBottom> Facade::Impl::sharedMediaBottomInvalidated() const {\n\treturn _sharedMedia.bottomInvalidated();\n}\n\nvoid Facade::Impl::add(UserPhotosSetBack &&query) {\n\treturn _userPhotos.add(std::move(query));\n}\n\nvoid Facade::Impl::add(UserPhotosAddNew &&query) {\n\treturn _userPhotos.add(std::move(query));\n}\n\nvoid Facade::Impl::add(UserPhotosAddSlice &&query) {\n\treturn _userPhotos.add(std::move(query));\n}\n\nvoid Facade::Impl::remove(UserPhotosRemoveOne &&query) {\n\treturn _userPhotos.remove(std::move(query));\n}\n\nvoid Facade::Impl::remove(UserPhotosRemoveAfter &&query) {\n\treturn _userPhotos.remove(std::move(query));\n}\n\nvoid Facade::Impl::replace(UserPhotosReplace &&query) {\n\treturn _userPhotos.replace(std::move(query));\n}\n\nrpl::producer<UserPhotosResult> Facade::Impl::query(UserPhotosQuery &&query) const {\n\treturn _userPhotos.query(std::move(query));\n}\n\nrpl::producer<UserPhotosSliceUpdate> Facade::Impl::userPhotosSliceUpdated() const {\n\treturn _userPhotos.sliceUpdated();\n}\n\nFacade::Facade() : _impl(std::make_unique<Impl>()) {\n}\n\nvoid Facade::add(SharedMediaAddNew &&query) {\n\t_impl->add(std::move(query));\n}\n\nvoid Facade::add(SharedMediaAddExisting &&query) {\n\t_impl->add(std::move(query));\n}\n\nvoid Facade::add(SharedMediaAddSlice &&query) {\n\t_impl->add(std::move(query));\n}\n\nvoid Facade::remove(SharedMediaRemoveOne &&query) {\n\t_impl->remove(std::move(query));\n}\n\nvoid Facade::remove(SharedMediaRemoveAll &&query) {\n\t_impl->remove(std::move(query));\n}\n\nvoid Facade::invalidate(SharedMediaInvalidateBottom &&query) {\n\t_impl->invalidate(std::move(query));\n}\n\nvoid Facade::unload(SharedMediaUnloadThread &&query) {\n\t_impl->unload(std::move(query));\n}\n\nrpl::producer<SharedMediaResult> Facade::query(SharedMediaQuery &&query) const {\n\treturn _impl->query(std::move(query));\n}\n\nSharedMediaResult Facade::snapshot(const SharedMediaQuery &query) const {\n\treturn _impl->snapshot(query);\n}\n\nbool Facade::empty(const SharedMediaKey &key) const {\n\treturn _impl->empty(key);\n}\n\nrpl::producer<SharedMediaSliceUpdate> Facade::sharedMediaSliceUpdated() const {\n\treturn _impl->sharedMediaSliceUpdated();\n}\n\nrpl::producer<SharedMediaRemoveOne> Facade::sharedMediaOneRemoved() const {\n\treturn _impl->sharedMediaOneRemoved();\n}\n\nrpl::producer<SharedMediaRemoveAll> Facade::sharedMediaAllRemoved() const {\n\treturn _impl->sharedMediaAllRemoved();\n}\n\nrpl::producer<SharedMediaInvalidateBottom> Facade::sharedMediaBottomInvalidated() const {\n\treturn _impl->sharedMediaBottomInvalidated();\n}\n\nvoid Facade::add(UserPhotosSetBack &&query) {\n\treturn _impl->add(std::move(query));\n}\n\nvoid Facade::add(UserPhotosAddNew &&query) {\n\treturn _impl->add(std::move(query));\n}\n\nvoid Facade::add(UserPhotosAddSlice &&query) {\n\treturn _impl->add(std::move(query));\n}\n\nvoid Facade::remove(UserPhotosRemoveOne &&query) {\n\treturn _impl->remove(std::move(query));\n}\n\nvoid Facade::remove(UserPhotosRemoveAfter &&query) {\n\treturn _impl->remove(std::move(query));\n}\n\nvoid Facade::replace(UserPhotosReplace &&query) {\n\treturn _impl->replace(std::move(query));\n}\n\nrpl::producer<UserPhotosResult> Facade::query(UserPhotosQuery &&query) const {\n\treturn _impl->query(std::move(query));\n}\n\nrpl::producer<UserPhotosSliceUpdate> Facade::userPhotosSliceUpdated() const {\n\treturn _impl->userPhotosSliceUpdated();\n}\n\nFacade::~Facade() = default;\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_facade.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <rpl/producer.h>\n#include \"base/enum_mask.h\"\n\nnamespace Data {\nstruct MessagesResult;\n} // namespace Data\n\nnamespace Storage {\n\nstruct SparseIdsListResult;\n\nstruct SharedMediaAddNew;\nstruct SharedMediaAddExisting;\nstruct SharedMediaAddSlice;\nstruct SharedMediaRemoveOne;\nstruct SharedMediaRemoveAll;\nstruct SharedMediaInvalidateBottom;\nstruct SharedMediaUnloadThread;\nstruct SharedMediaQuery;\nstruct SharedMediaKey;\nusing SharedMediaResult = SparseIdsListResult;\nstruct SharedMediaSliceUpdate;\n\nstruct UserPhotosSetBack;\nstruct UserPhotosAddNew;\nstruct UserPhotosAddSlice;\nstruct UserPhotosRemoveOne;\nstruct UserPhotosRemoveAfter;\nstruct UserPhotosReplace;\nstruct UserPhotosQuery;\nstruct UserPhotosResult;\nstruct UserPhotosSliceUpdate;\n\nclass Facade {\npublic:\n\tFacade();\n\n\tvoid add(SharedMediaAddNew &&query);\n\tvoid add(SharedMediaAddExisting &&query);\n\tvoid add(SharedMediaAddSlice &&query);\n\tvoid remove(SharedMediaRemoveOne &&query);\n\tvoid remove(SharedMediaRemoveAll &&query);\n\tvoid invalidate(SharedMediaInvalidateBottom &&query);\n\tvoid unload(SharedMediaUnloadThread &&query);\n\n\trpl::producer<SharedMediaResult> query(SharedMediaQuery &&query) const;\n\tSharedMediaResult snapshot(const SharedMediaQuery &query) const;\n\tbool empty(const SharedMediaKey &key) const;\n\trpl::producer<SharedMediaSliceUpdate> sharedMediaSliceUpdated() const;\n\trpl::producer<SharedMediaRemoveOne> sharedMediaOneRemoved() const;\n\trpl::producer<SharedMediaRemoveAll> sharedMediaAllRemoved() const;\n\trpl::producer<SharedMediaInvalidateBottom> sharedMediaBottomInvalidated() const;\n\n\tvoid add(UserPhotosSetBack &&query);\n\tvoid add(UserPhotosAddNew &&query);\n\tvoid add(UserPhotosAddSlice &&query);\n\tvoid remove(UserPhotosRemoveOne &&query);\n\tvoid remove(UserPhotosRemoveAfter &&query);\n\tvoid replace(UserPhotosReplace &&query);\n\n\trpl::producer<UserPhotosResult> query(UserPhotosQuery &&query) const;\n\trpl::producer<UserPhotosSliceUpdate> userPhotosSliceUpdated() const;\n\n\t~Facade();\n\nprivate:\n\tclass Impl;\n\tconst std::unique_ptr<Impl> _impl;\n\n};\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_file_lock.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/basic_types.h\"\n#include <QtCore/QFile>\n\nnamespace Storage {\n\nclass FileLock {\npublic:\n\tFileLock();\n\n\tbool lock(QFile &file, QIODevice::OpenMode mode);\n\tvoid unlock();\n\n\tstatic constexpr auto kSkipBytes = size_type(4);\n\n\t~FileLock();\n\nprivate:\n\tclass Lock;\n\tstruct Descriptor;\n\tstruct LockingPid;\n\n\tstatic constexpr auto kLockOffset = index_type(0);\n\tstatic constexpr auto kLockLimit = kSkipBytes;\n\n\tstd::unique_ptr<Lock> _lock;\n\n};\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_file_lock_posix.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/storage_file_lock.h\"\n\n#include \"base/variant.h\"\n\n#include <unistd.h>\n#include <fcntl.h>\n#include <sys/types.h>\n#include <signal.h>\n\nnamespace Storage {\nnamespace {\n\nbool KillProcess(pid_t pid) {\n\tauto signal = SIGTERM;\n\tauto attempts = 0;\n\twhile (true) {\n\t\tconst auto result = kill(pid, signal);\n\t\tif (result < 0) {\n\t\t\treturn (errno == ESRCH);\n\t\t}\n\t\tusleep(10000);\n\t\tif (++attempts == 50) {\n\t\t\tsignal = SIGKILL;\n\t\t}\n\t}\n}\n\n} // namespace\n\nstruct FileLock::Descriptor {\n\tint value;\n};\n\nstruct FileLock::LockingPid {\n\tpid_t value;\n};\n\nclass FileLock::Lock {\npublic:\n\tusing Result = base::variant<Descriptor, LockingPid>;\n\tstatic Result Acquire(const QFile &file);\n\n\texplicit Lock(int descriptor);\n\t~Lock();\n\nprivate:\n\tint _descriptor = 0;\n\n};\n\nFileLock::Lock::Result FileLock::Lock::Acquire(const QFile &file) {\n\tconst auto descriptor = file.handle();\n\tif (!descriptor || !file.isOpen()) {\n\t\treturn Descriptor{ 0 };\n\t}\n\twhile (true) {\n\t\tstruct flock lock;\n\t\tlock.l_type = F_WRLCK;\n\t\tlock.l_whence = SEEK_SET;\n\t\tlock.l_start = kLockOffset;\n\t\tlock.l_len = kLockLimit;\n\t\tif (fcntl(descriptor, F_SETLK, &lock) == 0) {\n\t\t\treturn Descriptor{ descriptor };\n\t\t} else if (fcntl(descriptor, F_GETLK, &lock) < 0) {\n\t\t\treturn LockingPid{ 0 };\n\t\t} else if (lock.l_type != F_UNLCK) {\n\t\t\treturn LockingPid{ lock.l_pid };\n\t\t}\n\t}\n}\n\nFileLock::Lock::Lock(int descriptor) : _descriptor(descriptor) {\n}\n\nFileLock::Lock::~Lock() {\n\tstruct flock unlock;\n\tunlock.l_type = F_UNLCK;\n\tunlock.l_whence = SEEK_SET;\n\tunlock.l_start = kLockOffset;\n\tunlock.l_len = kLockLimit;\n\tfcntl(_descriptor, F_SETLK, &unlock);\n}\n\nFileLock::FileLock() = default;\n\nbool FileLock::lock(QFile &file, QIODevice::OpenMode mode) {\n\tExpects(_lock == nullptr || file.isOpen());\n\n\tunlock();\n\tfile.close();\n\tif (!file.open(mode)) {\n\t\treturn false;\n\t}\n\twhile (true) {\n\t\tconst auto result = Lock::Acquire(file);\n\t\tif (const auto descriptor = base::get_if<Descriptor>(&result)) {\n\t\t\tif (descriptor->value > 0) {\n\t\t\t\t_lock = std::make_unique<Lock>(descriptor->value);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tbreak;\n\t\t} else if (const auto pid = base::get_if<LockingPid>(&result)) {\n\t\t\tif (pid->value <= 0 || !KillProcess(pid->value)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid FileLock::unlock() {\n\t_lock = nullptr;\n}\n\nFileLock::~FileLock() = default;\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_file_lock_win.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/storage_file_lock.h\"\n\n#include \"platform/win/windows_dlls.h\"\n#include \"base/platform/win/base_windows_h.h\"\n\n#include <io.h>\n#include <fileapi.h>\n#include <RestartManager.h>\n\nnamespace Storage {\nnamespace {\n\nbool CloseProcesses(const QString &filename) {\n\tusing namespace Platform;\n\n\tif (!Dlls::RmStartSession\n\t\t|| !Dlls::RmRegisterResources\n\t\t|| !Dlls::RmGetList\n\t\t|| !Dlls::RmShutdown\n\t\t|| !Dlls::RmEndSession) {\n\t\treturn false;\n\t}\n\tauto result = BOOL(FALSE);\n\tauto session = DWORD();\n\tauto sessionKey = std::wstring(CCH_RM_SESSION_KEY + 1, wchar_t(0));\n\tauto error = Dlls::RmStartSession(&session, 0, sessionKey.data());\n\tif (error != ERROR_SUCCESS) {\n\t\treturn false;\n\t}\n\tconst auto guard = gsl::finally([&] { Dlls::RmEndSession(session); });\n\n\tconst auto path = QDir::toNativeSeparators(filename).toStdWString();\n\tauto nullterm = path.c_str();\n\terror = Dlls::RmRegisterResources(\n\t\tsession,\n\t\t1,\n\t\t&nullterm,\n\t\t0,\n\t\tnullptr,\n\t\t0,\n\t\tnullptr);\n\tif (error != ERROR_SUCCESS) {\n\t\treturn false;\n\t}\n\n\tauto processInfoNeeded = UINT(0);\n\tauto processInfoCount = UINT(0);\n\tauto reason = DWORD();\n\n\terror = Dlls::RmGetList(\n\t\tsession,\n\t\t&processInfoNeeded,\n\t\t&processInfoCount,\n\t\tnullptr,\n\t\t&reason);\n\tif (error != ERROR_SUCCESS && error != ERROR_MORE_DATA) {\n\t\treturn false;\n\t} else if (processInfoNeeded <= 0) {\n\t\treturn true;\n\t}\n\terror = Dlls::RmShutdown(session, RmForceShutdown, NULL);\n\tif (error != ERROR_SUCCESS) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\n} // namespace\n\nclass FileLock::Lock {\npublic:\n\tstatic int Acquire(const QFile &file);\n\n\texplicit Lock(int descriptor);\n\t~Lock();\n\nprivate:\n\tstatic constexpr auto offsetLow = DWORD(kLockOffset);\n\tstatic constexpr auto offsetHigh = DWORD(0);\n\tstatic constexpr auto limitLow = DWORD(kLockLimit);\n\tstatic constexpr auto limitHigh = DWORD(0);\n\n\tint _descriptor = 0;\n\n};\n\nint FileLock::Lock::Acquire(const QFile &file) {\n\tconst auto descriptor = file.handle();\n\tif (!descriptor || !file.isOpen()) {\n\t\treturn false;\n\t}\n\tconst auto handle = HANDLE(_get_osfhandle(descriptor));\n\tif (!handle) {\n\t\treturn false;\n\t}\n\treturn LockFile(handle, offsetLow, offsetHigh, limitLow, limitHigh)\n\t\t? descriptor\n\t\t: 0;\n}\n\nFileLock::Lock::Lock(int descriptor) : _descriptor(descriptor) {\n}\n\nFileLock::Lock::~Lock() {\n\tif (const auto handle = HANDLE(_get_osfhandle(_descriptor))) {\n\t\tUnlockFile(handle, offsetLow, offsetHigh, limitLow, limitHigh);\n\t}\n}\n\nFileLock::FileLock() = default;\n\nbool FileLock::lock(QFile &file, QIODevice::OpenMode mode) {\n\tExpects(_lock == nullptr || file.isOpen());\n\n\tunlock();\n\tfile.close();\n\tdo {\n\t\tif (!file.open(mode)) {\n\t\t\treturn false;\n\t\t} else if (const auto descriptor = Lock::Acquire(file)) {\n\t\t\t_lock = std::make_unique<Lock>(descriptor);\n\t\t\treturn true;\n\t\t}\n\t\tfile.close();\n\t} while (CloseProcesses(file.fileName()));\n\n\treturn false;\n}\n\nvoid FileLock::unlock() {\n\t_lock = nullptr;\n}\n\nFileLock::~FileLock() = default;\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_media_prepare.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/storage_media_prepare.h\"\n\n#include \"editor/photo_editor_common.h\"\n#include \"platform/platform_file_utilities.h\"\n#include \"lang/lang_keys.h\"\n#include \"storage/localimageloader.h\"\n#include \"core/mime_type.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"core/crash_reports.h\"\n\n#include <QtCore/QSemaphore>\n#include <QtCore/QMimeData>\n\nnamespace Storage {\nnamespace {\n\nusing Ui::PreparedFileInformation;\nusing Ui::PreparedFile;\nusing Ui::PreparedList;\n\nusing Image = PreparedFileInformation::Image;\n\nbool ValidPhotoForAlbum(\n\t\tconst Image &image,\n\t\tconst QString &mime) {\n\tExpects(!image.data.isNull());\n\n\tif (image.animated\n\t\t|| (!mime.isEmpty() && !mime.startsWith(u\"image/\"))) {\n\t\treturn false;\n\t}\n\tconst auto width = image.data.width();\n\tconst auto height = image.data.height();\n\treturn Ui::ValidateThumbDimensions(width, height);\n}\n\nbool ValidVideoForAlbum(const PreparedFileInformation::Video &video) {\n\tconst auto width = video.thumbnail.width();\n\tconst auto height = video.thumbnail.height();\n\treturn Ui::ValidateThumbDimensions(width, height);\n}\n\nQSize PrepareShownDimensions(const QImage &preview, int sideLimit) {\n\tconst auto result = preview.size();\n\treturn (result.width() > sideLimit || result.height() > sideLimit)\n\t\t? result.scaled(sideLimit, sideLimit, Qt::KeepAspectRatio)\n\t\t: result;\n}\n\nvoid PrepareDetailsInParallel(PreparedList &result, int previewWidth) {\n\tExpects(result.files.size() <= Ui::MaxAlbumItems());\n\n\tif (result.files.empty()) {\n\t\treturn;\n\t}\n\tconst auto sideLimit = PhotoSideLimit(); // Get on main thread.\n\tQSemaphore semaphore;\n\tfor (auto &file : result.files) {\n\t\tcrl::async([=, &semaphore, &file] {\n\t\t\tPrepareDetails(file, previewWidth, sideLimit);\n\t\t\tsemaphore.release();\n\t\t});\n\t}\n\tsemaphore.acquire(result.files.size());\n}\n\n} // namespace\n\nbool ValidatePhotoEditorMediaDragData(not_null<const QMimeData*> data) {\n\tconst auto urls = Core::ReadMimeUrls(data);\n\tif (urls.size() > 1) {\n\t\treturn false;\n\t} else if (data->hasImage()) {\n\t\treturn true;\n\t}\n\n\tif (!urls.isEmpty()) {\n\t\tconst auto url = urls.front();\n\t\tif (url.isLocalFile()) {\n\t\t\tusing namespace Core;\n\t\t\tconst auto file = Platform::File::UrlToLocal(url);\n\t\t\tconst auto info = QFileInfo(file);\n\t\t\treturn FileIsImage(file, MimeTypeForFile(info).name())\n\t\t\t\t&& QImageReader(file).canRead();\n\t\t}\n\t}\n\n\treturn false;\n}\n\nbool ValidateEditMediaDragData(\n\t\tnot_null<const QMimeData*> data,\n\t\tUi::AlbumType albumType) {\n\tconst auto urls = Core::ReadMimeUrls(data);\n\tif (urls.size() > 1) {\n\t\treturn false;\n\t} else if (data->hasImage()) {\n\t\treturn (albumType != Ui::AlbumType::Music);\n\t}\n\n\tif (albumType == Ui::AlbumType::PhotoVideo && !urls.isEmpty()) {\n\t\tconst auto url = urls.front();\n\t\tif (url.isLocalFile()) {\n\t\t\tusing namespace Core;\n\t\t\tconst auto info = QFileInfo(Platform::File::UrlToLocal(url));\n\t\t\treturn IsMimeAcceptedForPhotoVideoAlbum(MimeTypeForFile(info).name());\n\t\t}\n\t}\n\n\treturn true;\n}\n\nMimeDataState ComputeMimeDataState(const QMimeData *data) {\n\tif (!data || data->hasFormat(u\"application/x-td-forward\"_q)) {\n\t\treturn MimeDataState::None;\n\t}\n\n\tif (data->hasImage()) {\n\t\treturn MimeDataState::Image;\n\t}\n\n\tconst auto urls = Core::ReadMimeUrls(data);\n\tif (urls.isEmpty()) {\n\t\treturn MimeDataState::None;\n\t}\n\n\tauto allAreSmallImages = true;\n\tauto allAreMedia = true;\n\tfor (const auto &url : urls) {\n\t\tif (!url.isLocalFile()) {\n\t\t\treturn MimeDataState::None;\n\t\t}\n\t\tconst auto file = Platform::File::UrlToLocal(url);\n\n\t\tconst auto info = QFileInfo(file);\n\t\tif (info.isDir()) {\n\t\t\treturn MimeDataState::None;\n\t\t}\n\n\t\tusing namespace Core;\n\t\tconst auto filesize = info.size();\n\t\tif (filesize > kFileSizePremiumLimit) {\n\t\t\treturn MimeDataState::None;\n\t\t//} else if (filesize > kFileSizeLimit) {\n\t\t//\treturn MimeDataState::PremiumFile;\n\t\t} else if (allAreSmallImages) {\n\t\t\tif (filesize > Images::kReadBytesLimit) {\n\t\t\t\tallAreSmallImages = false;\n\t\t\t} else {\n\t\t\t\tconst auto mime = MimeTypeForFile(info).name();\n\t\t\t\tif (mime == u\"image/gif\"_q\n\t\t\t\t\t|| !FileIsImage(file, mime)\n\t\t\t\t\t|| !QImageReader(file).canRead()) {\n\t\t\t\t\tallAreSmallImages = false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (allAreMedia) {\n\t\t\tconst auto type = DetectNameType(file);\n\t\t\tif (type != NameType::Image && type != NameType::Video) {\n\t\t\t\tallAreMedia = false;\n\t\t\t}\n\t\t}\n\t}\n\treturn allAreSmallImages\n\t\t? MimeDataState::PhotoFiles\n\t\t: allAreMedia\n\t\t? MimeDataState::MediaFiles\n\t\t: MimeDataState::Files;\n}\n\nPreparedList PrepareMediaList(\n\t\tconst QList<QUrl> &files,\n\t\tint previewWidth,\n\t\tbool premium) {\n\tauto locals = QStringList();\n\tlocals.reserve(files.size());\n\tfor (const auto &url : files) {\n\t\tif (!url.isLocalFile()) {\n\t\t\treturn {\n\t\t\t\tPreparedList::Error::NonLocalUrl,\n\t\t\t\turl.toDisplayString()\n\t\t\t};\n\t\t}\n\t\tlocals.push_back(Platform::File::UrlToLocal(url));\n\t}\n\treturn PrepareMediaList(locals, previewWidth, premium);\n}\n\nPreparedList PrepareMediaList(\n\t\tconst QStringList &files,\n\t\tint previewWidth,\n\t\tbool premium) {\n\tauto result = PreparedList();\n\tresult.files.reserve(files.size());\n\tfor (const auto &file : files) {\n\t\tconst auto fileinfo = QFileInfo(file);\n\t\tconst auto filesize = fileinfo.size();\n\t\tif (fileinfo.isDir()) {\n\t\t\treturn {\n\t\t\t\tPreparedList::Error::Directory,\n\t\t\t\tfile\n\t\t\t};\n\t\t} else if (filesize <= 0) {\n\t\t\treturn {\n\t\t\t\tPreparedList::Error::EmptyFile,\n\t\t\t\tfile\n\t\t\t};\n\t\t} else if (filesize > kFileSizePremiumLimit\n\t\t\t|| (filesize > kFileSizeLimit && !premium)) {\n\t\t\tauto errorResult = PreparedList(\n\t\t\t\tPreparedList::Error::TooLargeFile,\n\t\t\t\tQString());\n\t\t\terrorResult.files.emplace_back(file);\n\t\t\terrorResult.files.back().size = filesize;\n\t\t\treturn errorResult;\n\t\t}\n\t\tif (result.files.size() < Ui::MaxAlbumItems()) {\n\t\t\tresult.files.emplace_back(file);\n\t\t\tresult.files.back().size = filesize;\n\t\t} else {\n\t\t\tresult.filesToProcess.emplace_back(file);\n\t\t\tresult.files.back().size = filesize;\n\t\t}\n\t}\n\tPrepareDetailsInParallel(result, previewWidth);\n\treturn result;\n}\n\nPreparedList PrepareMediaFromImage(\n\t\tQImage &&image,\n\t\tQByteArray &&content,\n\t\tint previewWidth) {\n\tExpects(!image.isNull());\n\n\tauto result = PreparedList();\n\tauto file = PreparedFile(QString());\n\tfile.content = content;\n\tif (file.content.isEmpty()) {\n\t\tfile.information = std::make_unique<PreparedFileInformation>();\n\t\tconst auto animated = false;\n\t\tFileLoadTask::FillImageInformation(\n\t\t\tstd::move(image),\n\t\t\tanimated,\n\t\t\tfile.information);\n\t}\n\tresult.files.push_back(std::move(file));\n\tPrepareDetailsInParallel(result, previewWidth);\n\treturn result;\n}\n\nstd::optional<PreparedList> PreparedFileFromFilesDialog(\n\t\tFileDialog::OpenResult &&result,\n\t\tFn<bool(const Ui::PreparedList&)> checkResult,\n\t\tFn<void(tr::phrase<>)> errorCallback,\n\t\tint previewWidth,\n\t\tbool premium) {\n\tif (result.paths.isEmpty() && result.remoteContent.isEmpty()) {\n\t\treturn std::nullopt;\n\t}\n\n\tauto list = result.remoteContent.isEmpty()\n\t\t? PrepareMediaList(result.paths, previewWidth, premium)\n\t\t: PrepareMediaFromImage(\n\t\t\tQImage(),\n\t\t\tstd::move(result.remoteContent),\n\t\t\tpreviewWidth);\n\tif (list.error != PreparedList::Error::None) {\n\t\terrorCallback(tr::lng_send_media_invalid_files);\n\t\treturn std::nullopt;\n\t} else if (!checkResult(list)) {\n\t\treturn std::nullopt;\n\t} else {\n\t\treturn list;\n\t}\n}\n\nvoid PrepareDetails(PreparedFile &file, int previewWidth, int sideLimit) {\n\tif (!file.path.isEmpty()) {\n\t\tfile.information = FileLoadTask::ReadMediaInformation(\n\t\t\tfile.path,\n\t\t\tQByteArray(),\n\t\t\tCore::MimeTypeForFile(QFileInfo(file.path)).name());\n\t} else if (!file.content.isEmpty()) {\n\t\tfile.information = FileLoadTask::ReadMediaInformation(\n\t\t\tQString(),\n\t\t\tfile.content,\n\t\t\tCore::MimeTypeForData(file.content).name());\n\t} else {\n\t\tAssert(file.information != nullptr);\n\t}\n\n\tusing Video = PreparedFileInformation::Video;\n\tusing Song = PreparedFileInformation::Song;\n\tif (const auto image = std::get_if<Image>(\n\t\t\t&file.information->media)) {\n\t\tAssert(!image->data.isNull());\n\t\tif (ValidPhotoForAlbum(*image, file.information->filemime)) {\n\t\t\tUpdateImageDetails(file, previewWidth, sideLimit);\n\t\t\tfile.type = PreparedFile::Type::Photo;\n\t\t} else {\n\t\t\tfile.originalDimensions = image->data.size();\n\t\t\tif (image->animated) {\n\t\t\t\tfile.type = PreparedFile::Type::None;\n\t\t\t}\n\t\t}\n\t} else if (const auto video = std::get_if<Video>(\n\t\t\t&file.information->media)) {\n\t\tif (ValidVideoForAlbum(*video)) {\n\t\t\tauto blurred = Images::Blur(\n\t\t\t\tImages::Opaque(base::duplicate(video->thumbnail)));\n\t\t\tfile.originalDimensions = video->thumbnail.size();\n\t\t\tfile.shownDimensions = PrepareShownDimensions(\n\t\t\t\tvideo->thumbnail,\n\t\t\t\tsideLimit);\n\t\t\tfile.preview = std::move(blurred).scaledToWidth(\n\t\t\t\tpreviewWidth * style::DevicePixelRatio(),\n\t\t\t\tQt::SmoothTransformation);\n\t\t\tAssert(!file.preview.isNull());\n\t\t\tfile.preview.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\tfile.type = PreparedFile::Type::Video;\n\t\t}\n\t} else if (v::is<Song>(file.information->media)) {\n\t\tfile.type = PreparedFile::Type::Music;\n\t}\n}\n\nvoid UpdateImageDetails(\n\t\tPreparedFile &file,\n\t\tint previewWidth,\n\t\tint sideLimit) {\n\tconst auto image = std::get_if<Image>(&file.information->media);\n\tif (!image) {\n\t\treturn;\n\t}\n\tAssert(!image->data.isNull());\n\tauto preview = image->modifications\n\t\t? Editor::ImageModified(image->data, image->modifications)\n\t\t: image->data;\n\tAssert(!preview.isNull());\n\tfile.originalDimensions = preview.size();\n\tfile.shownDimensions = PrepareShownDimensions(preview, sideLimit);\n\tconst auto toWidth = std::min(\n\t\tpreviewWidth,\n\t\tstyle::ConvertScale(preview.width())\n\t) * style::DevicePixelRatio();\n\tauto scaled = preview.scaledToWidth(\n\t\ttoWidth,\n\t\tQt::SmoothTransformation);\n\tif (scaled.isNull()) {\n\t\tCrashReports::SetAnnotation(\"Info\", QString(\"%1x%2:%3*%4->%5;%6x%7\"\n\t\t).arg(preview.width()).arg(preview.height()\n\t\t).arg(previewWidth).arg(style::DevicePixelRatio()\n\t\t).arg(toWidth\n\t\t).arg(scaled.width()).arg(scaled.height()));\n\t\tUnexpected(\"Scaled is null.\");\n\t}\n\tAssert(!scaled.isNull());\n\tfile.preview = Images::Opaque(std::move(scaled));\n\tAssert(!file.preview.isNull());\n\tfile.preview.setDevicePixelRatio(style::DevicePixelRatio());\n}\n\nbool ApplyModifications(PreparedList &list) {\n\tauto applied = false;\n\tconst auto apply = [&](PreparedFile &file, QSize strictSize = {}) {\n\t\tconst auto image = std::get_if<Image>(&file.information->media);\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\tif (!image || strictSize.isEmpty()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tapplied = true;\n\t\t\tfile.path = QString();\n\t\t\tfile.content = QByteArray();\n\t\t\timage->data = image->data.scaled(\n\t\t\t\tstrictSize,\n\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\tQt::SmoothTransformation);\n\t\t});\n\t\tif (!image || !image->modifications) {\n\t\t\treturn;\n\t\t}\n\t\tapplied = true;\n\t\tfile.path = QString();\n\t\tfile.content = QByteArray();\n\t\timage->data = Editor::ImageModified(\n\t\t\tstd::move(image->data),\n\t\t\timage->modifications);\n\t};\n\tfor (auto &file : list.files) {\n\t\tapply(file);\n\t\tif (const auto cover = file.videoCover.get()) {\n\t\t\tconst auto video = file.information\n\t\t\t\t? std::get_if<Ui::PreparedFileInformation::Video>(\n\t\t\t\t\t&file.information->media)\n\t\t\t\t: nullptr;\n\t\t\tapply(*cover, video ? video->thumbnail.size() : QSize());\n\t\t}\n\t}\n\treturn applied;\n}\n\n} // namespace Storage\n\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_media_prepare.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"core/file_utilities.h\"\n\nnamespace tr {\ntemplate <typename ...>\nstruct phrase;\n} // namespace tr\n\nnamespace Ui {\nstruct PreparedFileInformation;\nstruct PreparedFile;\nstruct PreparedList;\nenum class AlbumType;\n} // namespace Ui\n\nnamespace Storage {\n\nenum class MimeDataState {\n\tNone,\n\tFiles,\n\tPhotoFiles,\n\tMediaFiles,\n\t//PremiumFile,\n\tImage,\n};\n\n[[nodiscard]] std::optional<Ui::PreparedList> PreparedFileFromFilesDialog(\n\tFileDialog::OpenResult &&result,\n\tFn<bool(const Ui::PreparedList&)> checkResult,\n\tFn<void(tr::phrase<>)> errorCallback,\n\tint previewWidth,\n\tbool premium);\n[[nodiscard]] MimeDataState ComputeMimeDataState(const QMimeData *data);\n[[nodiscard]] bool ValidatePhotoEditorMediaDragData(\n\tnot_null<const QMimeData*> data);\n[[nodiscard]] bool ValidateEditMediaDragData(\n\tnot_null<const QMimeData*> data,\n\tUi::AlbumType albumType);\n[[nodiscard]] Ui::PreparedList PrepareMediaList(\n\tconst QList<QUrl> &files,\n\tint previewWidth,\n\tbool premium);\n[[nodiscard]] Ui::PreparedList PrepareMediaList(\n\tconst QStringList &files,\n\tint previewWidth,\n\tbool premium);\n[[nodiscard]] Ui::PreparedList PrepareMediaFromImage(\n\tQImage &&image,\n\tQByteArray &&content,\n\tint previewWidth);\nvoid PrepareDetails(Ui::PreparedFile &file, int previewWidth, int sideLimit);\nvoid UpdateImageDetails(\n\tUi::PreparedFile &file,\n\tint previewWidth,\n\tint sideLimit);\n\nbool ApplyModifications(Ui::PreparedList &list);\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_shared_media.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/storage_shared_media.h\"\n\n#include <rpl/map.h>\n\nnamespace Storage {\n\nauto SharedMedia::enforceLists(Key key)\n-> std::map<Key, SharedMedia::Lists>::iterator {\n\tauto result = _lists.find(key);\n\tif (result != _lists.end()) {\n\t\treturn result;\n\t}\n\tresult = _lists.emplace(key, Lists {}).first;\n\tfor (auto index = 0; index != kSharedMediaTypeCount; ++index) {\n\t\tauto &list = result->second[index];\n\t\tauto type = static_cast<SharedMediaType>(index);\n\n\t\tlist.sliceUpdated(\n\t\t) | rpl::map([=](const SparseIdsSliceUpdate &update) {\n\t\t\treturn SharedMediaSliceUpdate(\n\t\t\t\tkey.peerId,\n\t\t\t\tkey.topicRootId,\n\t\t\t\tkey.monoforumPeerId,\n\t\t\t\ttype,\n\t\t\t\tupdate);\n\t\t}) | rpl::start_to_stream(_sliceUpdated, _lifetime);\n\t}\n\treturn result;\n}\n\nvoid SharedMedia::add(SharedMediaAddNew &&query) {\n\tconst auto addByIt = [&](const auto i) {\n\t\tfor (auto index = 0; index != kSharedMediaTypeCount; ++index) {\n\t\t\tauto type = static_cast<SharedMediaType>(index);\n\t\t\tif (query.types.test(type)) {\n\t\t\t\ti->second[index].addNew(query.messageId);\n\t\t\t}\n\t\t}\n\t};\n\taddByIt(enforceLists({ query.peerId, MsgId(0) }));\n\tconst auto topicIt = query.topicRootId\n\t\t? _lists.find({ query.peerId, query.topicRootId })\n\t\t: end(_lists);\n\tif (topicIt != end(_lists)) {\n\t\taddByIt(topicIt);\n\t}\n\tconst auto monoforumPeerIt = query.monoforumPeerId\n\t\t? _lists.find({ query.peerId, MsgId(), query.monoforumPeerId })\n\t\t: end(_lists);\n\tif (monoforumPeerIt != end(_lists)) {\n\t\taddByIt(monoforumPeerIt);\n\t}\n}\n\nvoid SharedMedia::add(SharedMediaAddExisting &&query) {\n\tauto peerIt = enforceLists({\n\t\tquery.peerId,\n\t\tquery.topicRootId,\n\t\tquery.monoforumPeerId,\n\t});\n\tfor (auto index = 0; index != kSharedMediaTypeCount; ++index) {\n\t\tauto type = static_cast<SharedMediaType>(index);\n\t\tif (query.types.test(type)) {\n\t\t\tpeerIt->second[index].addExisting(\n\t\t\t\tquery.messageId,\n\t\t\t\tquery.noSkipRange);\n\t\t}\n\t}\n}\n\nvoid SharedMedia::add(SharedMediaAddSlice &&query) {\n\tExpects(IsValidSharedMediaType(query.type));\n\n\tauto peerIt = enforceLists({\n\t\tquery.peerId,\n\t\tquery.topicRootId,\n\t\tquery.monoforumPeerId,\n\t});\n\tauto index = static_cast<int>(query.type);\n\tpeerIt->second[index].addSlice(\n\t\tstd::move(query.messageIds),\n\t\tquery.noSkipRange,\n\t\tquery.count);\n}\n\nvoid SharedMedia::remove(SharedMediaRemoveOne &&query) {\n\tauto peerIt = _lists.lower_bound({ query.peerId, MsgId(0) });\n\twhile (peerIt != end(_lists) && peerIt->first.peerId == query.peerId) {\n\t\tfor (auto index = 0; index != kSharedMediaTypeCount; ++index) {\n\t\t\tauto type = static_cast<SharedMediaType>(index);\n\t\t\tif (query.types.test(type)) {\n\t\t\t\tpeerIt->second[index].removeOne(query.messageId);\n\t\t\t}\n\t\t}\n\t\t++peerIt;\n\t}\n\t_oneRemoved.fire(std::move(query));\n}\n\nvoid SharedMedia::remove(SharedMediaRemoveAll &&query) {\n\tauto peerIt = _lists.lower_bound({\n\t\tquery.peerId,\n\t\tquery.topicRootId,\n\t\tquery.monoforumPeerId,\n\t});\n\twhile (peerIt != end(_lists)\n\t\t&& peerIt->first.peerId == query.peerId\n\t\t&& (!query.topicRootId\n\t\t\t|| peerIt->first.topicRootId == query.topicRootId)\n\t\t&& (!query.monoforumPeerId\n\t\t\t|| peerIt->first.monoforumPeerId == query.monoforumPeerId)) {\n\t\tfor (auto index = 0; index != kSharedMediaTypeCount; ++index) {\n\t\t\tauto type = static_cast<SharedMediaType>(index);\n\t\t\tif (query.types.test(type)) {\n\t\t\t\tpeerIt->second[index].removeAll();\n\t\t\t}\n\t\t}\n\t\t++peerIt;\n\t}\n\t_allRemoved.fire(std::move(query));\n}\n\nvoid SharedMedia::invalidate(SharedMediaInvalidateBottom &&query) {\n\tauto peerIt = _lists.lower_bound({ query.peerId, MsgId(0) });\n\twhile (peerIt != end(_lists) && peerIt->first.peerId == query.peerId) {\n\t\tfor (auto index = 0; index != kSharedMediaTypeCount; ++index) {\n\t\t\tpeerIt->second[index].invalidateBottom();\n\t\t}\n\t\t++peerIt;\n\t}\n\t_bottomInvalidated.fire(std::move(query));\n}\n\nvoid SharedMedia::unload(SharedMediaUnloadThread &&query) {\n\t_lists.erase({ query.peerId, query.topicRootId, query.monoforumPeerId });\n}\n\nrpl::producer<SharedMediaResult> SharedMedia::query(SharedMediaQuery &&query) const {\n\tExpects(IsValidSharedMediaType(query.key.type));\n\n\tauto peerIt = _lists.find({\n\t\tquery.key.peerId,\n\t\tquery.key.topicRootId,\n\t\tquery.key.monoforumPeerId,\n\t});\n\tif (peerIt != _lists.end()) {\n\t\tauto index = static_cast<int>(query.key.type);\n\t\treturn peerIt->second[index].query(SparseIdsListQuery(\n\t\t\tquery.key.messageId,\n\t\t\tquery.limitBefore,\n\t\t\tquery.limitAfter));\n\t}\n\treturn [](auto consumer) {\n\t\tconsumer.put_done();\n\t\treturn rpl::lifetime();\n\t};\n}\n\nSharedMediaResult SharedMedia::snapshot(const SharedMediaQuery &query) const {\n\tExpects(IsValidSharedMediaType(query.key.type));\n\n\tauto peerIt = _lists.find({\n\t\tquery.key.peerId,\n\t\tquery.key.topicRootId,\n\t\tquery.key.monoforumPeerId,\n\t});\n\tif (peerIt != _lists.end()) {\n\t\tauto index = static_cast<int>(query.key.type);\n\t\treturn peerIt->second[index].snapshot(SparseIdsListQuery(\n\t\t\tquery.key.messageId,\n\t\t\tquery.limitBefore,\n\t\t\tquery.limitAfter));\n\t}\n\treturn {};\n}\n\nbool SharedMedia::empty(const SharedMediaKey &key) const {\n\tExpects(IsValidSharedMediaType(key.type));\n\n\tauto peerIt = _lists.find({\n\t\tkey.peerId,\n\t\tkey.topicRootId,\n\t\tkey.monoforumPeerId,\n\t});\n\tif (peerIt != _lists.end()) {\n\t\tauto index = static_cast<int>(key.type);\n\t\treturn peerIt->second[index].empty();\n\t}\n\treturn true;\n}\n\nrpl::producer<SharedMediaSliceUpdate> SharedMedia::sliceUpdated() const {\n\treturn _sliceUpdated.events();\n}\n\nrpl::producer<SharedMediaRemoveOne> SharedMedia::oneRemoved() const {\n\treturn _oneRemoved.events();\n}\n\nrpl::producer<SharedMediaRemoveAll> SharedMedia::allRemoved() const {\n\treturn _allRemoved.events();\n}\n\nrpl::producer<SharedMediaInvalidateBottom> SharedMedia::bottomInvalidated() const {\n\treturn _bottomInvalidated.events();\n}\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_shared_media.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <rpl/event_stream.h>\n#include \"storage/storage_facade.h\"\n#include \"storage/storage_sparse_ids_list.h\"\n\nnamespace Storage {\n\n// Allow forward declarations.\nenum class SharedMediaType : signed char {\n\tPhoto,\n\tVideo,\n\tPhotoVideo,\n\tMusicFile,\n\tFile,\n\tVoiceFile,\n\tLink,\n\tChatPhoto,\n\tRoundVoiceFile,\n\tGIF,\n\tRoundFile,\n\tPinned,\n\tPoll,\n\n\tkCount,\n};\nconstexpr auto kSharedMediaTypeCount = static_cast<int>(SharedMediaType::kCount);\nconstexpr bool IsValidSharedMediaType(SharedMediaType type) {\n\treturn (static_cast<int>(type) >= 0)\n\t\t&& (static_cast<int>(type) < kSharedMediaTypeCount);\n}\n\nusing SharedMediaTypesMask = base::enum_mask<SharedMediaType>;\n\nstruct SharedMediaAddNew {\n\tSharedMediaAddNew(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tSharedMediaTypesMask types,\n\t\tMsgId messageId)\n\t: peerId(peerId)\n\t, topicRootId(topicRootId)\n\t, monoforumPeerId(monoforumPeerId)\n\t, messageId(messageId)\n\t, types(types) {\n\t}\n\n\tPeerId peerId = 0;\n\tMsgId topicRootId = 0;\n\tPeerId monoforumPeerId = 0;\n\tMsgId messageId = 0;\n\tSharedMediaTypesMask types;\n\n};\n\nstruct SharedMediaAddExisting {\n\tSharedMediaAddExisting(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tSharedMediaTypesMask types,\n\t\tMsgId messageId,\n\t\tMsgRange noSkipRange)\n\t: peerId(peerId)\n\t, topicRootId(topicRootId)\n\t, monoforumPeerId(monoforumPeerId)\n\t, messageId(messageId)\n\t, noSkipRange(noSkipRange)\n\t, types(types) {\n\t}\n\n\tPeerId peerId = 0;\n\tMsgId topicRootId = 0;\n\tPeerId monoforumPeerId = 0;\n\tMsgId messageId = 0;\n\tMsgRange noSkipRange;\n\tSharedMediaTypesMask types;\n\n};\n\nstruct SharedMediaAddSlice {\n\tSharedMediaAddSlice(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tSharedMediaType type,\n\t\tstd::vector<MsgId> &&messageIds,\n\t\tMsgRange noSkipRange,\n\t\tstd::optional<int> count = std::nullopt)\n\t: peerId(peerId)\n\t, topicRootId(topicRootId)\n\t, monoforumPeerId(monoforumPeerId)\n\t, messageIds(std::move(messageIds))\n\t, noSkipRange(noSkipRange)\n\t, type(type)\n\t, count(count) {\n\t}\n\n\tPeerId peerId = 0;\n\tMsgId topicRootId = 0;\n\tPeerId monoforumPeerId = 0;\n\tstd::vector<MsgId> messageIds;\n\tMsgRange noSkipRange;\n\tSharedMediaType type = SharedMediaType::kCount;\n\tstd::optional<int> count;\n\n};\n\nstruct SharedMediaRemoveOne {\n\tSharedMediaRemoveOne(\n\t\tPeerId peerId,\n\t\tSharedMediaTypesMask types,\n\t\tMsgId messageId)\n\t: peerId(peerId)\n\t, messageId(messageId)\n\t, types(types) {\n\t}\n\n\tPeerId peerId = 0;\n\tMsgId messageId = 0;\n\tSharedMediaTypesMask types;\n\n};\n\nstruct SharedMediaRemoveAll {\n\tSharedMediaRemoveAll(\n\t\tPeerId peerId,\n\t\tSharedMediaTypesMask types = SharedMediaTypesMask::All())\n\t: peerId(peerId)\n\t, types(types) {\n\t}\n\tSharedMediaRemoveAll(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tSharedMediaTypesMask types = SharedMediaTypesMask::All())\n\t: peerId(peerId)\n\t, topicRootId(topicRootId)\n\t, types(types) {\n\t}\n\tSharedMediaRemoveAll(\n\t\tPeerId peerId,\n\t\tPeerId monoforumPeerId,\n\t\tSharedMediaTypesMask types = SharedMediaTypesMask::All())\n\t: peerId(peerId)\n\t, monoforumPeerId(monoforumPeerId)\n\t, types(types) {\n\t}\n\n\tPeerId peerId = 0;\n\tMsgId topicRootId = 0;\n\tPeerId monoforumPeerId = 0;\n\tSharedMediaTypesMask types;\n\n};\n\nstruct SharedMediaInvalidateBottom {\n\tSharedMediaInvalidateBottom(PeerId peerId) : peerId(peerId) {\n\t}\n\n\tPeerId peerId = 0;\n\n};\n\nstruct SharedMediaKey {\n\tSharedMediaKey(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tSharedMediaType type,\n\t\tMsgId messageId)\n\t: peerId(peerId)\n\t, topicRootId(topicRootId)\n\t, monoforumPeerId(monoforumPeerId)\n\t, type(type)\n\t, messageId(messageId) {\n\t}\n\n\tfriend inline constexpr auto operator<=>(\n\t\tconst SharedMediaKey &,\n\t\tconst SharedMediaKey &) = default;\n\n\tPeerId peerId = 0;\n\tMsgId topicRootId = 0;\n\tPeerId monoforumPeerId = 0;\n\tSharedMediaType type = SharedMediaType::kCount;\n\tMsgId messageId = 0;\n\n};\n\nstruct SharedMediaQuery {\n\tSharedMediaQuery(\n\t\tSharedMediaKey key,\n\t\tint limitBefore,\n\t\tint limitAfter)\n\t: key(key)\n\t, limitBefore(limitBefore)\n\t, limitAfter(limitAfter) {\n\t}\n\n\tSharedMediaKey key;\n\tint limitBefore = 0;\n\tint limitAfter = 0;\n\n};\n\nusing SharedMediaResult = SparseIdsListResult;\n\nstruct SharedMediaSliceUpdate {\n\tSharedMediaSliceUpdate(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tSharedMediaType type,\n\t\tconst SparseIdsSliceUpdate &data)\n\t: peerId(peerId)\n\t, topicRootId(topicRootId)\n\t, monoforumPeerId(monoforumPeerId)\n\t, type(type)\n\t, data(data) {\n\t}\n\n\tPeerId peerId = 0;\n\tMsgId topicRootId = 0;\n\tPeerId monoforumPeerId = 0;\n\tSharedMediaType type = SharedMediaType::kCount;\n\tSparseIdsSliceUpdate data;\n};\n\nstruct SharedMediaUnloadThread {\n\tSharedMediaUnloadThread(\n\t\tPeerId peerId,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId)\n\t: peerId(peerId)\n\t, topicRootId(topicRootId)\n\t, monoforumPeerId(monoforumPeerId) {\n\t}\n\n\tPeerId peerId = 0;\n\tMsgId topicRootId = 0;\n\tPeerId monoforumPeerId = 0;\n};\n\nclass SharedMedia {\npublic:\n\tusing Type = SharedMediaType;\n\n\tvoid add(SharedMediaAddNew &&query);\n\tvoid add(SharedMediaAddExisting &&query);\n\tvoid add(SharedMediaAddSlice &&query);\n\tvoid remove(SharedMediaRemoveOne &&query);\n\tvoid remove(SharedMediaRemoveAll &&query);\n\tvoid invalidate(SharedMediaInvalidateBottom &&query);\n\tvoid unload(SharedMediaUnloadThread &&query);\n\n\trpl::producer<SharedMediaResult> query(SharedMediaQuery &&query) const;\n\tSharedMediaResult snapshot(const SharedMediaQuery &query) const;\n\tbool empty(const SharedMediaKey &key) const;\n\trpl::producer<SharedMediaSliceUpdate> sliceUpdated() const;\n\trpl::producer<SharedMediaRemoveOne> oneRemoved() const;\n\trpl::producer<SharedMediaRemoveAll> allRemoved() const;\n\trpl::producer<SharedMediaInvalidateBottom> bottomInvalidated() const;\n\nprivate:\n\tstruct Key {\n\t\tPeerId peerId = 0;\n\t\tMsgId topicRootId = 0;\n\t\tPeerId monoforumPeerId = 0;\n\n\t\tfriend inline constexpr auto operator<=>(Key, Key) = default;\n\t};\n\tusing Lists = std::array<SparseIdsList, kSharedMediaTypeCount>;\n\n\tstd::map<Key, Lists>::iterator enforceLists(Key key);\n\n\tstd::map<Key, Lists> _lists;\n\n\trpl::event_stream<SharedMediaSliceUpdate> _sliceUpdated;\n\trpl::event_stream<SharedMediaRemoveOne> _oneRemoved;\n\trpl::event_stream<SharedMediaRemoveAll> _allRemoved;\n\trpl::event_stream<SharedMediaInvalidateBottom> _bottomInvalidated;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_sparse_ids_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/storage_sparse_ids_list.h\"\n\nnamespace Storage {\n\nSparseIdsList::Slice::Slice(\n\tbase::flat_set<MsgId> &&messages,\n\tMsgRange range)\n: messages(std::move(messages))\n, range(range) {\n}\n\ntemplate <typename Range>\nvoid SparseIdsList::Slice::merge(\n\t\tconst Range &moreMessages,\n\t\tMsgRange moreNoSkipRange) {\n\tExpects(moreNoSkipRange.from <= range.till);\n\tExpects(range.from <= moreNoSkipRange.till);\n\n\tmessages.merge(std::begin(moreMessages), std::end(moreMessages));\n\trange = {\n\t\tqMin(range.from, moreNoSkipRange.from),\n\t\tqMax(range.till, moreNoSkipRange.till)\n\t};\n}\n\ntemplate <typename Range>\nSparseIdsList::AddResult SparseIdsList::uniteAndAdd(\n\t\tSparseIdsSliceUpdate &update,\n\t\tbase::flat_set<Slice>::iterator uniteFrom,\n\t\tbase::flat_set<Slice>::iterator uniteTill,\n\t\tconst Range &messages,\n\t\tMsgRange noSkipRange) {\n\tconst auto uniteFromIndex = uniteFrom - _slices.begin();\n\tconst auto was = int(uniteFrom->messages.size());\n\t_slices.modify(uniteFrom, [&](Slice &slice) {\n\t\tslice.merge(messages, noSkipRange);\n\t});\n\tconst auto firstToErase = uniteFrom + 1;\n\tif (firstToErase != uniteTill) {\n\t\tfor (auto it = firstToErase; it != uniteTill; ++it) {\n\t\t\t_slices.modify(uniteFrom, [&](Slice &slice) {\n\t\t\t\tslice.merge(it->messages, it->range);\n\t\t\t});\n\t\t}\n\t\t_slices.erase(firstToErase, uniteTill);\n\t\tuniteFrom = _slices.begin() + uniteFromIndex;\n\t}\n\tupdate.messages = &uniteFrom->messages;\n\tupdate.range = uniteFrom->range;\n\treturn { int(uniteFrom->messages.size()) - was };\n}\n\ntemplate <typename Range>\nSparseIdsList::AddResult SparseIdsList::addRangeItemsAndCountNew(\n\t\tSparseIdsSliceUpdate &update,\n\t\tconst Range &messages,\n\t\tMsgRange noSkipRange) {\n\tExpects(noSkipRange.from <= noSkipRange.till);\n\n\tif (noSkipRange.from == noSkipRange.till\n\t\t&& std::begin(messages) == std::end(messages)) {\n\t\treturn { 0 };\n\t}\n\tauto uniteFrom = ranges::lower_bound(\n\t\t_slices,\n\t\tnoSkipRange.from,\n\t\tstd::less<>(),\n\t\t[](const Slice &slice) { return slice.range.till; });\n\tauto uniteTill = ranges::upper_bound(\n\t\t_slices,\n\t\tnoSkipRange.till,\n\t\tstd::less<>(),\n\t\t[](const Slice &slice) { return slice.range.from; });\n\tif (uniteFrom < uniteTill) {\n\t\treturn uniteAndAdd(update, uniteFrom, uniteTill, messages, noSkipRange);\n\t}\n\n\tauto sliceMessages = base::flat_set<MsgId> {\n\t\tstd::begin(messages),\n\t\tstd::end(messages) };\n\tauto slice = _slices.emplace(\n\t\tstd::move(sliceMessages),\n\t\tnoSkipRange\n\t).first;\n\tupdate.messages = &slice->messages;\n\tupdate.range = slice->range;\n\treturn { int(slice->messages.size()) };\n}\n\ntemplate <typename Range>\nvoid SparseIdsList::addRange(\n\t\tconst Range &messages,\n\t\tMsgRange noSkipRange,\n\t\tstd::optional<int> count,\n\t\tbool incrementCount) {\n\tExpects(!count || !incrementCount);\n\n\tauto update = SparseIdsSliceUpdate();\n\tconst auto result = addRangeItemsAndCountNew(\n\t\tupdate,\n\t\tmessages,\n\t\tnoSkipRange);\n\tif (count) {\n\t\t_count = count;\n\t} else if (incrementCount && _count && result.added > 0) {\n\t\t*_count += result.added;\n\t}\n\tif (_slices.size() == 1) {\n\t\tif (_count && _slices.front().messages.size() >= *_count) {\n\t\t\t_slices.modify(_slices.begin(), [&](Slice &slice) {\n\t\t\t\tslice.range = { 0, ServerMaxMsgId };\n\t\t\t});\n\t\t}\n\t\tif (_slices.front().range == MsgRange{ 0, ServerMaxMsgId }) {\n\t\t\t_count = _slices.front().messages.size();\n\t\t}\n\t}\n\tif (_count && update.messages) {\n\t\taccumulate_max(*_count, int(update.messages->size()));\n\t}\n\tupdate.count = _count;\n\t_sliceUpdated.fire(std::move(update));\n}\n\nvoid SparseIdsList::addNew(MsgId messageId) {\n\tauto range = { messageId };\n\taddRange(range, { messageId, ServerMaxMsgId }, std::nullopt, true);\n}\n\nvoid SparseIdsList::addExisting(\n\t\tMsgId messageId,\n\t\tMsgRange noSkipRange) {\n\tauto range = { messageId };\n\taddRange(range, noSkipRange, std::nullopt);\n}\n\nvoid SparseIdsList::addSlice(\n\t\tstd::vector<MsgId> &&messageIds,\n\t\tMsgRange noSkipRange,\n\t\tstd::optional<int> count) {\n\taddRange(messageIds, noSkipRange, count);\n}\n\nvoid SparseIdsList::removeOne(MsgId messageId) {\n\tauto slice = ranges::lower_bound(\n\t\t_slices,\n\t\tmessageId,\n\t\tstd::less<>(),\n\t\t[](const Slice &slice) { return slice.range.till; });\n\tif (slice != _slices.end() && slice->range.from <= messageId) {\n\t\t_slices.modify(slice, [messageId](Slice &slice) {\n\t\t\treturn slice.messages.remove(messageId);\n\t\t});\n\t}\n\tif (_count) {\n\t\t--*_count;\n\t}\n}\n\nvoid SparseIdsList::removeAll() {\n\t_slices.clear();\n\t_slices.emplace(base::flat_set<MsgId>{}, MsgRange { 0, ServerMaxMsgId });\n\t_count = 0;\n}\n\nvoid SparseIdsList::invalidateBottom() {\n\tif (!_slices.empty()) {\n\t\tconst auto &last = _slices.back();\n\t\tif (last.range.till == ServerMaxMsgId) {\n\t\t\t_slices.modify(_slices.end() - 1, [](Slice &slice) {\n\t\t\t\tslice.range.till = slice.messages.empty()\n\t\t\t\t\t? slice.range.from\n\t\t\t\t\t: slice.messages.back();\n\t\t\t});\n\t\t}\n\t}\n\t_count = std::nullopt;\n}\n\nrpl::producer<SparseIdsListResult> SparseIdsList::query(\n\t\tSparseIdsListQuery &&query) const {\n\treturn [this, query = std::move(query)](auto consumer) {\n\t\tauto now = snapshot(query);\n\t\tif (!now.messageIds.empty() || now.count) {\n\t\t\tconsumer.put_next(std::move(now));\n\t\t}\n\t\tconsumer.put_done();\n\t\treturn rpl::lifetime();\n\t};\n}\n\nSparseIdsListResult SparseIdsList::snapshot(\n\t\tconst SparseIdsListQuery &query) const {\n\tauto slice = query.aroundId\n\t\t? ranges::lower_bound(\n\t\t\t_slices,\n\t\t\tquery.aroundId,\n\t\t\tstd::less<>(),\n\t\t\t[](const Slice &slice) { return slice.range.till; })\n\t\t: _slices.end();\n\tif (slice != _slices.end()\n\t\t&& slice->range.from <= query.aroundId) {\n\t\treturn queryFromSlice(query, *slice);\n\t} else if (_count) {\n\t\tauto result = SparseIdsListResult{};\n\t\tresult.count = _count;\n\t\treturn result;\n\t}\n\treturn {};\n}\n\nbool SparseIdsList::empty() const {\n\tfor (const auto &slice : _slices) {\n\t\tif (!slice.messages.empty()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nrpl::producer<SparseIdsSliceUpdate> SparseIdsList::sliceUpdated() const {\n\treturn _sliceUpdated.events();\n}\n\nSparseIdsListResult SparseIdsList::queryFromSlice(\n\t\tconst SparseIdsListQuery &query,\n\t\tconst Slice &slice) const {\n\tauto result = SparseIdsListResult {};\n\tauto position = ranges::lower_bound(slice.messages, query.aroundId);\n\tauto haveBefore = int(position - slice.messages.begin());\n\tauto haveEqualOrAfter = int(slice.messages.end() - position);\n\tauto before = qMin(haveBefore, query.limitBefore);\n\tauto equalOrAfter = qMin(haveEqualOrAfter, query.limitAfter + 1);\n\tauto ids = std::vector<MsgId>(position - before, position + equalOrAfter);\n\tresult.messageIds.merge(ids.begin(), ids.end());\n\tif (slice.range.from == 0) {\n\t\tresult.skippedBefore = haveBefore - before;\n\t}\n\tif (slice.range.till == ServerMaxMsgId) {\n\t\tresult.skippedAfter = haveEqualOrAfter - equalOrAfter;\n\t}\n\tif (_count) {\n\t\tresult.count = _count;\n\t\tif (!result.skippedBefore && result.skippedAfter) {\n\t\t\tresult.skippedBefore = *result.count\n\t\t\t\t- *result.skippedAfter\n\t\t\t\t- int(result.messageIds.size());\n\t\t} else if (!result.skippedAfter && result.skippedBefore) {\n\t\t\tresult.skippedAfter = *result.count\n\t\t\t\t- *result.skippedBefore\n\t\t\t\t- int(result.messageIds.size());\n\t\t}\n\t}\n\treturn result;\n}\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_sparse_ids_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Storage {\n\nstruct SparseIdsListQuery {\n\tSparseIdsListQuery(\n\t\tMsgId aroundId,\n\t\tint limitBefore,\n\t\tint limitAfter)\n\t: aroundId(aroundId)\n\t, limitBefore(limitBefore)\n\t, limitAfter(limitAfter) {\n\t}\n\n\tMsgId aroundId = 0;\n\tint limitBefore = 0;\n\tint limitAfter = 0;\n\n};\n\nstruct SparseIdsListResult {\n\tstd::optional<int> count;\n\tstd::optional<int> skippedBefore;\n\tstd::optional<int> skippedAfter;\n\tbase::flat_set<MsgId> messageIds;\n};\n\nstruct SparseIdsSliceUpdate {\n\tconst base::flat_set<MsgId> *messages = nullptr;\n\tMsgRange range;\n\tstd::optional<int> count;\n};\n\nclass SparseIdsList {\npublic:\n\tvoid addNew(MsgId messageId);\n\tvoid addExisting(MsgId messageId, MsgRange noSkipRange);\n\tvoid addSlice(\n\t\tstd::vector<MsgId> &&messageIds,\n\t\tMsgRange noSkipRange,\n\t\tstd::optional<int> count);\n\tvoid removeOne(MsgId messageId);\n\tvoid removeAll();\n\tvoid invalidateBottom();\n\trpl::producer<SparseIdsListResult> query(SparseIdsListQuery &&query) const;\n\trpl::producer<SparseIdsSliceUpdate> sliceUpdated() const;\n\tSparseIdsListResult snapshot(const SparseIdsListQuery &query) const;\n\tbool empty() const;\n\nprivate:\n\tstruct Slice {\n\t\tSlice(base::flat_set<MsgId> &&messages, MsgRange range);\n\n\t\ttemplate <typename Range>\n\t\tvoid merge(const Range &moreMessages, MsgRange moreNoSkipRange);\n\n\t\tbase::flat_set<MsgId> messages;\n\t\tMsgRange range;\n\n\t\tinline bool operator<(const Slice &other) const {\n\t\t\treturn range.from < other.range.from;\n\t\t}\n\n\t};\n\n\tstruct AddResult {\n\t\tint added = 0;\n\t};\n\ttemplate <typename Range>\n\tAddResult uniteAndAdd(\n\t\tSparseIdsSliceUpdate &update,\n\t\tbase::flat_set<Slice>::iterator uniteFrom,\n\t\tbase::flat_set<Slice>::iterator uniteTill,\n\t\tconst Range &messages,\n\t\tMsgRange noSkipRange);\n\ttemplate <typename Range>\n\tAddResult addRangeItemsAndCountNew(\n\t\tSparseIdsSliceUpdate &update,\n\t\tconst Range &messages,\n\t\tMsgRange noSkipRange);\n\ttemplate <typename Range>\n\tvoid addRange(\n\t\tconst Range &messages,\n\t\tMsgRange noSkipRange,\n\t\tstd::optional<int> count,\n\t\tbool incrementCount = false);\n\n\tSparseIdsListResult queryFromSlice(\n\t\tconst SparseIdsListQuery &query,\n\t\tconst Slice &slice) const;\n\n\tstd::optional<int> _count;\n\tbase::flat_set<Slice> _slices;\n\n\trpl::event_stream<SparseIdsSliceUpdate> _sliceUpdated;\n\n};\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_user_photos.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/storage_user_photos.h\"\n\nnamespace Storage {\n\nvoid UserPhotos::List::setBack(PhotoId photoId) {\n\tif (_backPhotoId != photoId) {\n\t\tdetachBack();\n\t\t_backPhotoId = photoId;\n\t\tattachBack();\n\t\tsendUpdate();\n\t}\n}\n\nvoid UserPhotos::List::detachBack() {\n\tif (_backPhotoId) {\n\t\tremoveOne(_backPhotoId);\n\t}\n}\n\nvoid UserPhotos::List::attachBack() {\n\tif (_backPhotoId) {\n\t\t_photoIds.push_front(_backPhotoId);\n\t\tif (_count) {\n\t\t\t++*_count;\n\t\t}\n\t}\n}\n\nvoid UserPhotos::List::addNew(PhotoId photoId) {\n\tif (!base::contains(_photoIds, photoId)) {\n\t\tdetachBack();\n\t\t_photoIds.push_back(photoId);\n\t\tif (_count) {\n\t\t\t++*_count;\n\t\t}\n\t\tattachBack();\n\t\tsendUpdate();\n\t}\n}\n\nvoid UserPhotos::List::addSlice(\n\t\tstd::vector<PhotoId> &&photoIds,\n\t\tint count) {\n\tdetachBack();\n\tfor (auto photoId : photoIds) {\n\t\tif (!base::contains(_photoIds, photoId)) {\n\t\t\t_photoIds.push_front(photoId);\n\t\t}\n\t}\n\n\t_count = count;\n\tif ((_count && *_count < _photoIds.size()) || photoIds.empty()) {\n\t\t_count = _photoIds.size();\n\t}\n\tattachBack();\n\tsendUpdate();\n}\n\nvoid UserPhotos::List::removeOne(PhotoId photoId) {\n\tauto position = ranges::find(_photoIds, photoId);\n\tif (position == _photoIds.end()) {\n\t\t_count = std::nullopt;\n\t} else {\n\t\tif (_count) {\n\t\t\t--*_count;\n\t\t}\n\t\t_photoIds.erase(position);\n\t}\n\tsendUpdate();\n}\n\nvoid UserPhotos::List::removeAfter(PhotoId photoId) {\n\tauto position = ranges::find(_photoIds, photoId);\n\tif (position == _photoIds.end()) {\n\t\t_count = std::nullopt;\n\t\t_photoIds.clear();\n\t} else {\n\t\tif (_count) {\n\t\t\t*_count -= (_photoIds.end() - position);\n\t\t}\n\t\t_photoIds.erase(position, _photoIds.end());\n\t}\n\tsendUpdate();\n}\n\nvoid UserPhotos::List::replace(PhotoId oldPhotoId, PhotoId newPhotoId) {\n\tauto position = ranges::find(_photoIds, oldPhotoId);\n\tif (position != _photoIds.end()) {\n\t\t*position = newPhotoId;\n\t}\n\tif (_backPhotoId == oldPhotoId) {\n\t\t_backPhotoId = newPhotoId;\n\t}\n\tsendUpdate();\n}\n\nvoid UserPhotos::List::sendUpdate() {\n\tauto update = SliceUpdate();\n\tupdate.photoIds = &_photoIds;\n\tupdate.count = _count;\n\t_sliceUpdated.fire(std::move(update));\n}\n\nrpl::producer<UserPhotosResult> UserPhotos::List::query(\n\t\tUserPhotosQuery &&query) const {\n\treturn [this, query = std::move(query)](auto consumer) {\n\t\tauto result = UserPhotosResult();\n\t\tresult.count = _count;\n\n\t\tauto position = ranges::find(_photoIds, query.key.photoId);\n\t\tif (position != _photoIds.end()) {\n\t\t\tauto haveBefore = int(position - _photoIds.begin());\n\t\t\tauto haveEqualOrAfter = int(_photoIds.end() - position);\n\t\t\tauto before = qMin(haveBefore, query.limitBefore);\n\t\t\tauto equalOrAfter = qMin(haveEqualOrAfter, query.limitAfter + 1);\n\t\t\tresult.photoIds = std::deque<PhotoId>(\n\t\t\t\tposition - before,\n\t\t\t\tposition + equalOrAfter);\n\n\t\t\tauto skippedInIds = (haveBefore - before);\n\t\t\tresult.skippedBefore = _count\n\t\t\t\t| func::add(-int(_photoIds.size()) + skippedInIds);\n\t\t\tresult.skippedBefore = haveBefore - before;\n\t\t\tresult.skippedAfter = (haveEqualOrAfter - equalOrAfter);\n\t\t\tconsumer.put_next(std::move(result));\n\t\t} else if (query.key.back && _backPhotoId) {\n\t\t\tresult.photoIds.push_front(_backPhotoId);\n\t\t\tresult.count = 1;\n\t\t\tconsumer.put_next(std::move(result));\n\t\t} else if (_count) {\n\t\t\tconsumer.put_next(std::move(result));\n\t\t}\n\t\tconsumer.put_done();\n\t\treturn rpl::lifetime();\n\t};\n}\n\nauto UserPhotos::List::sliceUpdated() const -> rpl::producer<SliceUpdate> {\n\treturn _sliceUpdated.events();\n}\n\nrpl::producer<UserPhotosSliceUpdate> UserPhotos::sliceUpdated() const {\n\treturn _sliceUpdated.events();\n}\n\nstd::map<UserId, UserPhotos::List>::iterator UserPhotos::enforceLists(\n\t\tUserId user) {\n\tauto result = _lists.find(user);\n\tif (result != _lists.end()) {\n\t\treturn result;\n\t}\n\tresult = _lists.emplace(user, List {}).first;\n\tresult->second.sliceUpdated(\n\t) | rpl::on_next([this, user](\n\t\t\tconst SliceUpdate &update) {\n\t\t_sliceUpdated.fire(UserPhotosSliceUpdate(\n\t\t\tuser,\n\t\t\tupdate.photoIds,\n\t\t\tupdate.count));\n\t}, _lifetime);\n\treturn result;\n}\n\nvoid UserPhotos::add(UserPhotosSetBack &&query) {\n\tauto userIt = enforceLists(query.userId);\n\tuserIt->second.setBack(query.photoId);\n}\n\nvoid UserPhotos::add(UserPhotosAddNew &&query) {\n\tauto userIt = enforceLists(query.userId);\n\tuserIt->second.addNew(query.photoId);\n}\n\nvoid UserPhotos::add(UserPhotosAddSlice &&query) {\n\tauto userIt = enforceLists(query.userId);\n\tuserIt->second.addSlice(\n\t\tstd::move(query.photoIds),\n\t\tquery.count);\n}\n\nvoid UserPhotos::remove(UserPhotosRemoveOne &&query) {\n\tauto userIt = _lists.find(query.userId);\n\tif (userIt != _lists.end()) {\n\t\tuserIt->second.removeOne(query.photoId);\n\t}\n}\n\nvoid UserPhotos::remove(UserPhotosRemoveAfter &&query) {\n\tauto userIt = _lists.find(query.userId);\n\tif (userIt != _lists.end()) {\n\t\tuserIt->second.removeAfter(query.photoId);\n\t}\n}\n\nvoid UserPhotos::replace(UserPhotosReplace &&query) {\n\tauto userIt = _lists.find(query.userId);\n\tif (userIt != _lists.end()) {\n\t\tuserIt->second.replace(query.oldPhotoId, query.newPhotoId);\n\t}\n}\n\nrpl::producer<UserPhotosResult> UserPhotos::query(\n\t\tUserPhotosQuery &&query) const {\n\tauto userIt = _lists.find(query.key.userId);\n\tif (userIt != _lists.end()) {\n\t\treturn userIt->second.query(std::move(query));\n\t}\n\treturn [](auto consumer) {\n\t\tconsumer.put_done();\n\t\treturn rpl::lifetime();\n\t};\n}\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/storage_user_photos.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <rpl/event_stream.h>\n#include \"storage/storage_facade.h\"\n\nnamespace Storage {\n\nstruct UserPhotosSetBack {\n\tUserPhotosSetBack(UserId userId, PhotoId photoId)\n\t\t: userId(userId), photoId(photoId) {\n\t}\n\n\tUserId userId = 0;\n\tPhotoId photoId = 0;\n\n};\n\nstruct UserPhotosAddNew {\n\tUserPhotosAddNew(UserId userId, PhotoId photoId)\n\t\t: userId(userId), photoId(photoId) {\n\t}\n\n\tUserId userId = 0;\n\tPhotoId photoId = 0;\n\n};\n\nstruct UserPhotosAddSlice {\n\tUserPhotosAddSlice(\n\t\tUserId userId,\n\t\tstd::vector<PhotoId> &&photoIds,\n\t\tint count)\n\t\t: userId(userId)\n\t\t, photoIds(std::move(photoIds))\n\t\t, count(count) {\n\t}\n\n\tUserId userId = 0;\n\tstd::vector<PhotoId> photoIds;\n\tint count = 0;\n\n};\n\nstruct UserPhotosRemoveOne {\n\tUserPhotosRemoveOne(\n\t\tUserId userId,\n\t\tPhotoId photoId)\n\t: userId(userId)\n\t, photoId(photoId) {\n\t}\n\n\tUserId userId = 0;\n\tPhotoId photoId = 0;\n\n};\n\nstruct UserPhotosRemoveAfter {\n\tUserPhotosRemoveAfter(\n\t\tUserId userId,\n\t\tPhotoId photoId)\n\t\t: userId(userId)\n\t\t, photoId(photoId) {\n\t}\n\n\tUserId userId = 0;\n\tPhotoId photoId = 0;\n\n};\n\nstruct UserPhotosReplace {\n\tUserPhotosReplace(\n\t\tUserId userId,\n\t\tPhotoId oldPhotoId,\n\t\tPhotoId newPhotoId)\n\t\t: userId(userId)\n\t\t, oldPhotoId(oldPhotoId)\n\t\t, newPhotoId(newPhotoId) {\n\t}\n\n\tUserId userId = 0;\n\tPhotoId oldPhotoId = 0;\n\tPhotoId newPhotoId = 0;\n\n};\n\nstruct UserPhotosKey {\n\tUserPhotosKey(\n\t\tUserId userId,\n\t\tPhotoId photoId)\n\t\t: userId(userId)\n\t\t, photoId(photoId) {\n\t}\n\tUserPhotosKey(UserId userId, bool back) : userId(userId), back(back) {\n\t}\n\n\tbool operator==(const UserPhotosKey &other) const {\n\t\treturn (userId == other.userId)\n\t\t\t&& (photoId == other.photoId)\n\t\t\t&& (back == other.back);\n\t}\n\tbool operator!=(const UserPhotosKey &other) const {\n\t\treturn !(*this == other);\n\t}\n\n\tUserId userId = 0;\n\tPhotoId photoId = 0;\n\tbool back = false;\n\n};\n\nstruct UserPhotosQuery {\n\tUserPhotosQuery(\n\t\tUserPhotosKey key,\n\t\tint limitBefore,\n\t\tint limitAfter)\n\t\t: key(key)\n\t\t, limitBefore(limitBefore)\n\t\t, limitAfter(limitAfter) {\n\t}\n\n\tUserPhotosKey key;\n\tint limitBefore = 0;\n\tint limitAfter = 0;\n\n};\n\nstruct UserPhotosResult {\n\tstd::optional<int> count;\n\tstd::optional<int> skippedBefore;\n\tint skippedAfter = 0;\n\tstd::deque<PhotoId> photoIds;\n};\n\nstruct UserPhotosSliceUpdate {\n\tUserPhotosSliceUpdate(\n\t\tUserId userId,\n\t\tconst std::deque<PhotoId> *photoIds,\n\t\tstd::optional<int> count)\n\t\t: userId(userId)\n\t\t, photoIds(photoIds)\n\t\t, count(count) {\n\t}\n\n\tUserId userId = 0;\n\tconst std::deque<PhotoId> *photoIds = nullptr;\n\tstd::optional<int> count;\n};\n\nclass UserPhotos {\npublic:\n\tvoid add(UserPhotosSetBack &&query);\n\tvoid add(UserPhotosAddNew &&query);\n\tvoid add(UserPhotosAddSlice &&query);\n\tvoid remove(UserPhotosRemoveOne &&query);\n\tvoid remove(UserPhotosRemoveAfter &&query);\n\tvoid replace(UserPhotosReplace &&query);\n\n\trpl::producer<UserPhotosResult> query(UserPhotosQuery &&query) const;\n\trpl::producer<UserPhotosSliceUpdate> sliceUpdated() const;\n\nprivate:\n\tclass List {\n\tpublic:\n\t\tvoid setBack(PhotoId photoId);\n\t\tvoid addNew(PhotoId photoId);\n\t\tvoid addSlice(\n\t\t\tstd::vector<PhotoId> &&photoIds,\n\t\t\tint count);\n\t\tvoid removeOne(PhotoId photoId);\n\t\tvoid removeAfter(PhotoId photoId);\n\t\tvoid replace(PhotoId oldPhotoId, PhotoId newPhotoId);\n\t\trpl::producer<UserPhotosResult> query(UserPhotosQuery &&query) const;\n\n\t\tstruct SliceUpdate {\n\t\t\tconst std::deque<PhotoId> *photoIds = nullptr;\n\t\t\tstd::optional<int> count;\n\t\t};\n\t\trpl::producer<SliceUpdate> sliceUpdated() const;\n\n\tprivate:\n\t\tvoid sendUpdate();\n\t\tvoid detachBack();\n\t\tvoid attachBack();\n\n\t\tstd::optional<int> _count;\n\t\tstd::deque<PhotoId> _photoIds;\n\n\t\tPhotoId _backPhotoId = PhotoId(0);\n\n\t\trpl::event_stream<SliceUpdate> _sliceUpdated;\n\n\t};\n\tusing SliceUpdate = List::SliceUpdate;\n\n\tstd::map<UserId, List>::iterator enforceLists(UserId user);\n\n\tstd::map<UserId, List> _lists;\n\n\trpl::event_stream<UserPhotosSliceUpdate> _sliceUpdated;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/streamed_file_downloader.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"storage/streamed_file_downloader.h\"\n\n#include \"media/streaming/media_streaming_loader.h\"\n#include \"media/streaming/media_streaming_reader.h\"\n\nnamespace Storage {\nnamespace {\n\nusing namespace Media::Streaming;\n\nconstexpr auto kPartSize = Loader::kPartSize;\nconstexpr auto kRequestPartsCount = 32;\n\n} // namespace\n\nStreamedFileDownloader::StreamedFileDownloader(\n\tnot_null<Main::Session*> session,\n\n\tuint64 objectId,\n\tMTP::DcId dcId,\n\tData::FileOrigin origin,\n\tCache::Key cacheKey,\n\tMediaKey fileLocationKey,\n\tstd::shared_ptr<Reader> reader,\n\n\t// For FileLoader\n\tconst QString &toFile,\n\tint64 size,\n\tLocationType locationType,\n\tLoadToCacheSetting toCache,\n\tLoadFromCloudSetting fromCloud,\n\tbool autoLoading,\n\tuint8 cacheTag)\n: FileLoader(\n\tsession,\n\ttoFile,\n\tsize,\n\tsize,\n\tlocationType,\n\ttoCache,\n\tfromCloud,\n\tautoLoading,\n\tcacheTag)\n, _objectId(objectId)\n, _origin(origin)\n, _cacheKey(cacheKey)\n, _fileLocationKey(fileLocationKey)\n, _reader(std::move(reader))\n, _partsCount((size + kPartSize - 1) / kPartSize) {\n\t_partIsSaved.resize(_partsCount, false);\n\n\t_reader->partsForDownloader(\n\t) | rpl::on_next([=](const LoadedPart &part) {\n\t\tif (part.offset == LoadedPart::kFailedOffset) {\n\t\t\tcancel(FailureReason::OtherFailure);\n\t\t} else {\n\t\t\tsavePart(std::move(part));\n\t\t}\n\t}, _lifetime);\n}\n\nStreamedFileDownloader::~StreamedFileDownloader() {\n\tif (!_finished) {\n\t\tcancel();\n\t} else {\n\t\t_reader->cancelForDownloader(this);\n\t}\n}\n\nuint64 StreamedFileDownloader::objId() const {\n\treturn _objectId;\n}\n\nData::FileOrigin StreamedFileDownloader::fileOrigin() const {\n\treturn _origin;\n}\n\nvoid StreamedFileDownloader::requestParts() {\n\twhile (!_finished\n\t\t&& _nextPartIndex < _partsCount\n\t\t&& _partsRequested < kRequestPartsCount) {\n\t\trequestPart();\n\t}\n\t_reader->continueDownloaderFromMainThread();\n}\n\nvoid StreamedFileDownloader::requestPart() {\n\tExpects(!_finished);\n\n\tconst auto index = std::find(\n\t\tbegin(_partIsSaved) + _nextPartIndex,\n\t\tend(_partIsSaved),\n\t\tfalse\n\t) - begin(_partIsSaved);\n\tif (index == _partsCount) {\n\t\t_nextPartIndex = _partsCount;\n\t\treturn;\n\t}\n\t_nextPartIndex = index + 1;\n\t_reader->loadForDownloader(this, index * kPartSize);\n\t++_partsRequested;\n}\n\nQByteArray StreamedFileDownloader::readLoadedPart(int64 offset) {\n\tExpects(offset >= 0 && offset < _fullSize);\n\tExpects(!(offset % kPartSize));\n\n\tconst auto index = (offset / kPartSize);\n\treturn _partIsSaved[index]\n\t\t? readLoadedPartBack(offset, kPartSize)\n\t\t: QByteArray();\n}\n\nStorage::Cache::Key StreamedFileDownloader::cacheKey() const {\n\treturn _cacheKey;\n}\n\nstd::optional<MediaKey> StreamedFileDownloader::fileLocationKey() const {\n\treturn _fileLocationKey;\n}\n\nvoid StreamedFileDownloader::cancelHook() {\n\t_partsRequested = 0;\n\t_nextPartIndex = 0;\n\n\t_reader->cancelForDownloader(this);\n}\n\nvoid StreamedFileDownloader::startLoading() {\n\trequestParts();\n}\n\nvoid StreamedFileDownloader::savePart(const LoadedPart &part) {\n\tExpects(part.offset >= 0 && part.offset < _reader->size());\n\tExpects(part.offset % kPartSize == 0);\n\n\tif (_finished || _cancelled) {\n\t\treturn;\n\t}\n\n\tconst auto offset = part.offset;\n\tconst auto index = offset / kPartSize;\n\tAssert(index >= 0 && index < _partsCount);\n\tif (_partIsSaved[index]) {\n\t\treturn;\n\t}\n\t_partIsSaved[index] = true;\n\t++_partsSaved;\n\n\tif (index < _nextPartIndex) {\n\t\t--_partsRequested;\n\t}\n\tif (!writeResultPart(offset, bytes::make_span(part.bytes))) {\n\t\treturn;\n\t}\n\t_reader->doneForDownloader(offset);\n\tif (_partsSaved == _partsCount) {\n\t\tfinalizeResult();\n\t} else {\n\t\trequestParts();\n\t\tnotifyAboutProgress();\n\t}\n}\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/storage/streamed_file_downloader.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"storage/file_download.h\"\n#include \"storage/cache/storage_cache_types.h\"\n#include \"data/data_file_origin.h\"\n\nnamespace Media {\nnamespace Streaming {\nclass Reader;\nstruct LoadedPart;\n} // namespace Streaming\n} // namespace Media\n\nnamespace Storage {\n\nclass StreamedFileDownloader final : public FileLoader {\npublic:\n\tStreamedFileDownloader(\n\t\tnot_null<Main::Session*> session,\n\n\t\tuint64 objectId,\n\t\tMTP::DcId dcId,\n\t\tData::FileOrigin origin,\n\t\tCache::Key cacheKey,\n\t\tMediaKey fileLocationKey,\n\t\tstd::shared_ptr<Media::Streaming::Reader> reader,\n\n\t\t// For FileLoader\n\t\tconst QString &toFile,\n\t\tint64 size,\n\t\tLocationType locationType,\n\t\tLoadToCacheSetting toCache,\n\t\tLoadFromCloudSetting fromCloud,\n\t\tbool autoLoading,\n\t\tuint8 cacheTag);\n\t~StreamedFileDownloader();\n\n\tuint64 objId() const override;\n\tData::FileOrigin fileOrigin() const override;\n\n\tQByteArray readLoadedPart(int64 offset);\n\nprivate:\n\tvoid startLoading() override;\n\tCache::Key cacheKey() const override;\n\tstd::optional<MediaKey> fileLocationKey() const override;\n\tvoid cancelHook() override;\n\tvoid requestParts();\n\tvoid requestPart();\n\n\tvoid savePart(const Media::Streaming::LoadedPart &part);\n\n\tuint64 _objectId = 0;\n\tData::FileOrigin _origin;\n\tCache::Key _cacheKey;\n\tMediaKey _fileLocationKey;\n\tstd::shared_ptr<Media::Streaming::Reader> _reader;\n\n\tstd::vector<bool> _partIsSaved; // vector<bool> :D\n\tmutable int _nextPartIndex = 0;\n\tint _partsCount = 0;\n\tint _partsRequested = 0;\n\tint _partsSaved = 0;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Storage\n"
  },
  {
    "path": "Telegram/SourceFiles/support/support_autocomplete.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"support/support_autocomplete.h\"\n\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/painter.h\"\n#include \"support/support_templates.h\"\n#include \"support/support_common.h\"\n#include \"history/view/history_view_message.h\"\n#include \"history/view/history_view_service_message.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"base/unixtime.h\"\n#include \"base/call_delayed.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"apiwrap.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Support {\nnamespace {\n\nclass Inner : public Ui::RpWidget {\npublic:\n\tInner(QWidget *parent);\n\n\tusing Question = details::TemplatesQuestion;\n\tvoid showRows(std::vector<Question> &&rows);\n\n\tstd::pair<int, int> moveSelection(int delta);\n\n\tstd::optional<Question> selected() const;\n\n\tauto activated() const {\n\t\treturn _activated.events();\n\t}\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tstruct Row {\n\t\tQuestion data;\n\t\tUi::Text::String question = { st::windowMinWidth / 2 };\n\t\tUi::Text::String keys = { st::windowMinWidth / 2 };\n\t\tUi::Text::String answer = { st::windowMinWidth / 2 };\n\t\tint top = 0;\n\t\tint height = 0;\n\t};\n\n\tvoid prepareRow(Row &row);\n\tint resizeRowGetHeight(Row &row, int newWidth);\n\tvoid setSelected(int selected);\n\n\tstd::vector<Row> _rows;\n\tint _selected = -1;\n\tint _pressed = -1;\n\tbool _selectByKeys = false;\n\trpl::event_stream<> _activated;\n\n};\n\nint TextHeight(const Ui::Text::String &text, int available, int lines) {\n\tExpects(text.style() != nullptr);\n\n\tconst auto st = text.style();\n\tconst auto line = st->lineHeight ? st->lineHeight : st->font->height;\n\treturn std::min(text.countHeight(available), lines * line);\n};\n\nInner::Inner(QWidget *parent) : RpWidget(parent) {\n\tsetMouseTracking(true);\n}\n\nvoid Inner::showRows(std::vector<Question> &&rows) {\n\t_rows.resize(0);\n\t_rows.reserve(rows.size());\n\tfor (auto &row : rows) {\n\t\t_rows.push_back({ std::move(row) });\n\t\tauto &added = _rows.back();\n\t\tprepareRow(added);\n\t}\n\tresizeToWidth(width());\n\t_selected = _pressed = -1;\n\tmoveSelection(1);\n\tupdate();\n}\n\nstd::pair<int, int> Inner::moveSelection(int delta) {\n\tconst auto selected = _selected + delta;\n\tif (selected >= 0 && selected < _rows.size()) {\n\t\t_selectByKeys = true;\n\t\tsetSelected(selected);\n\t\tconst auto top = _rows[_selected].top;\n\t\treturn { top, top + _rows[_selected].height };\n\t}\n\treturn { -1, -1 };\n}\n\nauto Inner::selected() const -> std::optional<Question> {\n\tif (_rows.empty()) {\n\t\treturn std::nullopt;\n\t} else if (_selected < 0) {\n\t\treturn _rows[0].data;\n\t}\n\treturn _rows[_selected].data;\n}\n\nvoid Inner::prepareRow(Row &row) {\n\trow.question.setText(st::autocompleteRowTitle, row.data.question);\n\trow.keys.setText(\n\t\tst::autocompleteRowKeys,\n\t\trow.data.originalKeys.join(u\", \"_q));\n\trow.answer.setText(st::autocompleteRowAnswer, row.data.value);\n}\n\nint Inner::resizeRowGetHeight(Row &row, int newWidth) {\n\tconst auto available = newWidth\n\t\t- st::autocompleteRowPadding.left()\n\t\t- st::autocompleteRowPadding.right();\n\treturn row.height = st::autocompleteRowPadding.top()\n\t\t+ TextHeight(row.question, available, 1)\n\t\t+ TextHeight(row.keys, available, 1)\n\t\t+ TextHeight(row.answer, available, 2)\n\t\t+ st::autocompleteRowPadding.bottom()\n\t\t+ st::lineWidth;\n}\n\nint Inner::resizeGetHeight(int newWidth) {\n\tauto top = 0;\n\tfor (auto &row : _rows) {\n\t\trow.top = top;\n\t\ttop += resizeRowGetHeight(row, newWidth);\n\t}\n\treturn top ? (top - st::lineWidth) : (3 * st::mentionHeight);\n}\n\nvoid Inner::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tif (_rows.empty()) {\n\t\tp.setFont(st::boxTextFont);\n\t\tp.setPen(st::windowSubTextFg);\n\t\tp.drawText(\n\t\t\trect(),\n\t\t\t\"Search by question, keys or value\",\n\t\t\tstyle::al_center);\n\t\treturn;\n\t}\n\n\tconst auto clip = e->rect();\n\tconst auto from = ranges::upper_bound(\n\t\t_rows,\n\t\tclip.y(),\n\t\tstd::less<>(),\n\t\t[](const Row &row) { return row.top + row.height; });\n\tconst auto till = ranges::lower_bound(\n\t\t_rows,\n\t\tclip.y() + clip.height(),\n\t\tstd::less<>(),\n\t\t[](const Row &row) { return row.top; });\n\tif (from == end(_rows)) {\n\t\treturn;\n\t}\n\tp.translate(0, from->top);\n\tconst auto padding = st::autocompleteRowPadding;\n\tconst auto available = width() - padding.left() - padding.right();\n\tauto top = padding.top();\n\tconst auto drawText = [&](const Ui::Text::String &text, int lines) {\n\t\ttext.drawLeftElided(\n\t\t\tp,\n\t\t\tpadding.left(),\n\t\t\ttop,\n\t\t\tavailable,\n\t\t\twidth(),\n\t\t\tlines);\n\t\ttop += TextHeight(text, available, lines);\n\t};\n\tfor (auto i = from; i != till; ++i) {\n\t\tconst auto over = (i - begin(_rows) == _selected);\n\t\tif (over) {\n\t\t\tp.fillRect(0, 0, width(), i->height, st::windowBgOver);\n\t\t}\n\t\tp.setPen(st::mentionNameFg);\n\t\tdrawText(i->question, 1);\n\t\tp.setPen(over ? st::mentionFgOver : st::mentionFg);\n\t\tdrawText(i->keys, 1);\n\t\tp.setPen(st::windowFg);\n\t\tdrawText(i->answer, 2);\n\n\t\tp.translate(0, i->height);\n\t\ttop = padding.top();\n\n\t\tif (i - begin(_rows) + 1 == _selected) {\n\t\t\tp.fillRect(\n\t\t\t\t0,\n\t\t\t\t-st::lineWidth,\n\t\t\t\twidth(),\n\t\t\t\tst::lineWidth,\n\t\t\t\tst::windowBgOver);\n\t\t} else if (!over) {\n\t\t\tp.fillRect(\n\t\t\t\tpadding.left(),\n\t\t\t\t-st::lineWidth,\n\t\t\t\tavailable,\n\t\t\t\tst::lineWidth,\n\t\t\t\tst::shadowFg);\n\t\t}\n\t}\n}\n\nvoid Inner::mouseMoveEvent(QMouseEvent *e) {\n\tstatic auto lastGlobalPos = QPoint();\n\tconst auto moved = (e->globalPos() != lastGlobalPos);\n\tif (!moved && _selectByKeys) {\n\t\treturn;\n\t}\n\t_selectByKeys = false;\n\tlastGlobalPos = e->globalPos();\n\tconst auto i = ranges::upper_bound(\n\t\t_rows,\n\t\te->pos().y(),\n\t\tstd::less<>(),\n\t\t[](const Row &row) { return row.top + row.height; });\n\tsetSelected((i == end(_rows)) ? -1 : (i - begin(_rows)));\n}\n\nvoid Inner::leaveEventHook(QEvent *e) {\n\tsetSelected(-1);\n}\n\nvoid Inner::setSelected(int selected) {\n\tif (_selected != selected) {\n\t\t_selected = selected;\n\t\t\tupdate();\n\t}\n}\n\nvoid Inner::mousePressEvent(QMouseEvent *e) {\n\t_pressed = _selected;\n}\n\nvoid Inner::mouseReleaseEvent(QMouseEvent *e) {\n\tconst auto pressed = base::take(_pressed);\n\tif (pressed == _selected && pressed >= 0) {\n\t\t_activated.fire({});\n\t}\n}\n\nAdminLog::OwnedItem GenerateCommentItem(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate,\n\t\tnot_null<History*> history,\n\t\tconst Contact &data) {\n\tif (data.comment.isEmpty()) {\n\t\treturn nullptr;\n\t}\n\tconst auto item = history->makeMessage({\n\t\t.id = history->nextNonHistoryEntryId(),\n\t\t.flags = (MessageFlag::HasFromId\n\t\t\t| MessageFlag::Outgoing\n\t\t\t| MessageFlag::FakeHistoryItem),\n\t\t.from = history->session().userPeerId(),\n\t\t.date = base::unixtime::now(),\n\t}, TextWithEntities{ data.comment }, MTP_messageMediaEmpty());\n\treturn AdminLog::OwnedItem(delegate, item);\n}\n\nAdminLog::OwnedItem GenerateContactItem(\n\t\tnot_null<HistoryView::ElementDelegate*> delegate,\n\t\tnot_null<History*> history,\n\t\tconst Contact &data) {\n\tconst auto item = history->makeMessage({\n\t\t.id = history->nextNonHistoryEntryId(),\n\t\t.flags = (MessageFlag::HasFromId\n\t\t\t| MessageFlag::Outgoing\n\t\t\t| MessageFlag::FakeHistoryItem),\n\t\t.from = history->session().userPeerId(),\n\t\t.date = base::unixtime::now(),\n\t}, TextWithEntities(), MTP_messageMediaContact(\n\t\tMTP_string(data.phone),\n\t\tMTP_string(data.firstName),\n\t\tMTP_string(data.lastName),\n\t\tMTP_string(), // vcard\n\t\tMTP_long(0))); // user_id\n\treturn AdminLog::OwnedItem(delegate, item);\n}\n\n} // namespace\n\nAutocomplete::Autocomplete(QWidget *parent, not_null<Main::Session*> session)\n: RpWidget(parent)\n, _session(session) {\n\tsetupContent();\n}\n\nvoid Autocomplete::activate(not_null<Ui::InputField*> field) {\n\tif (_session->settings().supportTemplatesAutocomplete()) {\n\t\t_activate();\n\t} else {\n\t\tconst auto &templates = _session->supportTemplates();\n\t\tconst auto max = templates.maxKeyLength();\n\t\tauto cursor = field->textCursor();\n\t\tconst auto position = cursor.position();\n\t\tconst auto anchor = cursor.anchor();\n\t\tconst auto text = (position != anchor)\n\t\t\t? field->getTextWithTagsPart(\n\t\t\t\tstd::min(position, anchor),\n\t\t\t\tstd::max(position, anchor))\n\t\t\t: field->getTextWithTagsPart(\n\t\t\t\tstd::max(position - max, 0),\n\t\t\t\tposition);\n\t\tconst auto result = (position != anchor)\n\t\t\t? templates.matchExact(text.text)\n\t\t\t: templates.matchFromEnd(text.text);\n\t\tif (result) {\n\t\t\tconst auto till = std::max(position, anchor);\n\t\t\tconst auto from = till - result->key.size();\n\t\t\tcursor.setPosition(from);\n\t\t\tcursor.setPosition(till, QTextCursor::KeepAnchor);\n\t\t\tfield->setTextCursor(cursor);\n\t\t\tsubmitValue(result->question.value);\n\t\t}\n\t}\n}\n\nvoid Autocomplete::deactivate() {\n\t_deactivate();\n}\n\nvoid Autocomplete::setBoundings(QRect rect) {\n\tconst auto maxHeight = int(4.5 * st::mentionHeight);\n\tconst auto height = std::min(rect.height(), maxHeight);\n\tsetGeometry(\n\t\trect.x(),\n\t\trect.y() + rect.height() - height,\n\t\trect.width(),\n\t\theight);\n}\n\nrpl::producer<QString> Autocomplete::insertRequests() const {\n\treturn _insertRequests.events();\n}\n\nrpl::producer<Contact> Autocomplete::shareContactRequests() const {\n\treturn _shareContactRequests.events();\n}\n\nvoid Autocomplete::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Up) {\n\t\t_moveSelection(-1);\n\t} else if (e->key() == Qt::Key_Down) {\n\t\t_moveSelection(1);\n\t}\n}\n\nvoid Autocomplete::setupContent() {\n\tconst auto inputWrap = Ui::CreateChild<Ui::PaddingWrap<Ui::InputField>>(\n\t\tthis,\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tthis,\n\t\t\tst::defaultMultiSelectSearchField,\n\t\t\trpl::single(u\"Search for templates\"_q)), // #TODO hard_lang\n\t\tst::autocompleteSearchPadding);\n\tconst auto input = inputWrap->entity();\n\tconst auto scroll = Ui::CreateChild<Ui::ScrollArea>(this);\n\n\tconst auto inner = scroll->setOwnedWidget(object_ptr<Inner>(scroll));\n\n\tconst auto submit = [=] {\n\t\tif (const auto question = inner->selected()) {\n\t\t\tsubmitValue(question->value);\n\t\t}\n\t};\n\n\tconst auto refresh = [=] {\n\t\tinner->showRows(\n\t\t\t_session->supportTemplates().query(input->getLastText()));\n\t\tscroll->scrollToY(0);\n\t};\n\n\tinner->activated() | rpl::on_next(submit, lifetime());\n\tinput->focusedChanges(\n\t) | rpl::filter(!rpl::mappers::_1) | rpl::on_next([=] {\n\t\tbase::call_delayed(10, this, [=] {\n\t\t\tif (!input->hasFocus()) {\n\t\t\t\tdeactivate();\n\t\t\t}\n\t\t});\n\t}, input->lifetime());\n\tinput->cancelled(\n\t) | rpl::on_next([=] {\n\t\tdeactivate();\n\t}, input->lifetime());\n\tinput->changes() | rpl::on_next(refresh, input->lifetime());\n\tinput->submits() | rpl::on_next(submit, input->lifetime());\n\tinput->customUpDown(true);\n\n\t_activate = [=] {\n\t\tinput->setText(QString());\n\t\tshow();\n\t\tinput->setFocus();\n\t};\n\t_deactivate = [=] {\n\t\thide();\n\t};\n\t_moveSelection = [=](int delta) {\n\t\tconst auto range = inner->moveSelection(delta);\n\t\tif (range.second > range.first) {\n\t\t\tscroll->scrollToY(range.first, range.second);\n\t\t}\n\t};\n\n\tpaintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tQPainter p(this);\n\t\tp.fillRect(\n\t\t\tclip.intersected(QRect(0, st::lineWidth, width(), height())),\n\t\t\tst::mentionBg);\n\t\tp.fillRect(\n\t\t\tclip.intersected(QRect(0, 0, width(), st::lineWidth)),\n\t\t\tst::shadowFg);\n\t}, lifetime());\n\n\tsizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tinputWrap->resizeToWidth(size.width());\n\t\tinputWrap->moveToLeft(0, st::lineWidth, size.width());\n\t\tscroll->setGeometry(\n\t\t\t0,\n\t\t\tinputWrap->height(),\n\t\t\tsize.width(),\n\t\t\tsize.height() - inputWrap->height() - st::lineWidth);\n\t\tinner->resizeToWidth(size.width());\n\t}, lifetime());\n}\n\nvoid Autocomplete::submitValue(const QString &value) {\n\tconst auto prefix = u\"contact:\"_q;\n\tif (value.startsWith(prefix)) {\n\t\tconst auto line = value.indexOf('\\n');\n\t\tconst auto text = (line > 0) ? value.mid(line + 1) : QString();\n\t\tconst auto contact = value.mid(\n\t\t\tprefix.size(),\n\t\t\t(line > 0) ? (line - prefix.size()) : -1);\n\t\tconst auto parts = contact.split(' ', Qt::SkipEmptyParts);\n\t\tif (parts.size() > 1) {\n\t\t\tconst auto phone = parts[0];\n\t\t\tconst auto firstName = parts[1];\n\t\t\tconst auto lastName = (parts.size() > 2)\n\t\t\t\t? QStringList(parts.mid(2)).join(' ')\n\t\t\t\t: QString();\n\t\t\t_shareContactRequests.fire(Contact{\n\t\t\t\ttext,\n\t\t\t\tphone,\n\t\t\t\tfirstName,\n\t\t\t\tlastName });\n\t\t}\n\t} else {\n\t\t_insertRequests.fire_copy(value);\n\t}\n}\n\nConfirmContactBox::ConfirmContactBox(\n\tQWidget*,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<History*> history,\n\tconst Contact &data,\n\tFn<void(Qt::KeyboardModifiers)> submit)\n: SimpleElementDelegate(controller, [=] { update(); })\n, _chatStyle(std::make_unique<Ui::ChatStyle>(\n\thistory->session().colorIndicesValue()))\n, _comment(GenerateCommentItem(this, history, data))\n, _contact(GenerateContactItem(this, history, data))\n, _submit(submit) {\n\t_chatStyle->apply(controller->defaultChatTheme().get());\n}\n\nvoid ConfirmContactBox::prepare() {\n\tsetTitle(u\"Confirmation\"_q); // #TODO hard_lang\n\n\tauto maxWidth = 0;\n\tif (_comment) {\n\t\t_comment->setAttachToNext(true, _contact.get());\n\t\t_contact->setAttachToPrevious(true, _comment.get());\n\t\t_comment->initDimensions();\n\t\taccumulate_max(maxWidth, _comment->maxWidth());\n\t}\n\t_contact->initDimensions();\n\taccumulate_max(maxWidth, _contact->maxWidth());\n\tmaxWidth += st::boxPadding.left() + st::boxPadding.right();\n\tconst auto width = std::clamp(maxWidth, st::boxWidth, st::boxWideWidth);\n\tconst auto available = width\n\t\t- st::boxPadding.left()\n\t\t- st::boxPadding.right();\n\tauto height = 0;\n\tif (_comment) {\n\t\theight += _comment->resizeGetHeight(available);\n\t}\n\theight += _contact->resizeGetHeight(available);\n\tsetDimensions(width, height);\n\t_contact->initDimensions();\n\n\t_submit = [=, original = std::move(_submit)](Qt::KeyboardModifiers m) {\n\t\tconst auto weak = base::make_weak(this);\n\t\toriginal(m);\n\t\tif (weak) {\n\t\t\tcloseBox();\n\t\t}\n\t};\n\n\tconst auto button = addButton(tr::lng_send_button(), [] {});\n\tbutton->clicks(\n\t) | rpl::on_next([=](Qt::MouseButton which) {\n\t\t_submit((which == Qt::RightButton)\n\t\t\t? SkipSwitchModifiers()\n\t\t\t: button->clickModifiers());\n\t}, button->lifetime());\n\tbutton->setAcceptBoth(true);\n\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n}\n\nvoid ConfirmContactBox::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {\n\t\t_submit(e->modifiers());\n\t} else {\n\t\tBoxContent::keyPressEvent(e);\n\t}\n}\n\nvoid ConfirmContactBox::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tp.fillRect(e->rect(), st::boxBg);\n\n\tconst auto theme = controller()->defaultChatTheme().get();\n\tauto context = theme->preparePaintContext(\n\t\t_chatStyle.get(),\n\t\trect(),\n\t\trect(),\n\t\trect(),\n\t\tcontroller()->isGifPausedAtLeastFor(Window::GifPauseReason::Layer));\n\tp.translate(st::boxPadding.left(), 0);\n\tif (_comment) {\n\t\tcontext.outbg = _comment->hasOutLayout();\n\t\t_comment->draw(p, context);\n\t\tp.translate(0, _comment->height());\n\t}\n\tcontext.outbg = _contact->hasOutLayout();\n\t_contact->draw(p, context);\n}\n\nHistoryView::Context ConfirmContactBox::elementContext() {\n\treturn HistoryView::Context::ContactPreview;\n}\n\n} // namespace Support\n"
  },
  {
    "path": "Telegram/SourceFiles/support/support_autocomplete.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/layers/box_content.h\"\n#include \"history/admin_log/history_admin_log_item.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/history.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\nclass ScrollArea;\nclass InputField;\nclass ChatStyle;\n} // namespace Ui\n\nnamespace Support {\n\nstruct Contact {\n\tQString comment;\n\tQString phone;\n\tQString firstName;\n\tQString lastName;\n};\n\nclass Autocomplete final : public Ui::RpWidget {\npublic:\n\tAutocomplete(QWidget *parent, not_null<Main::Session*> session);\n\n\tvoid activate(not_null<Ui::InputField*> field);\n\tvoid deactivate();\n\tvoid setBoundings(QRect rect);\n\n\trpl::producer<QString> insertRequests() const;\n\trpl::producer<Contact> shareContactRequests() const;\n\nprotected:\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tvoid setupContent();\n\tvoid submitValue(const QString &value);\n\n\tnot_null<Main::Session*> _session;\n\tFn<void()> _activate;\n\tFn<void()> _deactivate;\n\tFn<void(int delta)> _moveSelection;\n\n\trpl::event_stream<QString> _insertRequests;\n\trpl::event_stream<Contact> _shareContactRequests;\n\n};\n\nclass ConfirmContactBox\n\t: public Ui::BoxContent\n\t, public HistoryView::SimpleElementDelegate {\npublic:\n\tConfirmContactBox(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<History*> history,\n\t\tconst Contact &data,\n\t\tFn<void(Qt::KeyboardModifiers)> submit);\n\n\tusing Element = HistoryView::Element;\n\tHistoryView::Context elementContext() override;\n\nprotected:\n\tvoid prepare() override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tstd::unique_ptr<Ui::ChatStyle> _chatStyle;\n\tAdminLog::OwnedItem _comment;\n\tAdminLog::OwnedItem _contact;\n\tFn<void(Qt::KeyboardModifiers)> _submit;\n\n};\n\n} //namespace Support\n"
  },
  {
    "path": "Telegram/SourceFiles/support/support_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"support/support_common.h\"\n\n#include \"core/shortcuts.h\"\n\nnamespace Support {\n\nbool HandleSwitch(Qt::KeyboardModifiers modifiers) {\n\treturn !(modifiers & Qt::ShiftModifier)\n\t\t|| (!(modifiers & Qt::ControlModifier)\n\t\t\t&& !(modifiers & Qt::MetaModifier));\n}\n\nQt::KeyboardModifiers SkipSwitchModifiers() {\n\treturn Qt::ControlModifier | Qt::ShiftModifier;\n}\n\nstd::optional<Shortcuts::Command> GetSwitchCommand(SwitchSettings value) {\n\tswitch (value) {\n\tcase SwitchSettings::Next:\n\t\treturn Shortcuts::Command::ChatNext;\n\tcase SwitchSettings::Previous:\n\t\treturn Shortcuts::Command::ChatPrevious;\n\t}\n\treturn std::nullopt;\n}\n\nFnMut<bool()> GetSwitchMethod(SwitchSettings value) {\n\tconst auto command = GetSwitchCommand(value);\n\treturn command ? Shortcuts::RequestHandler(*command) : nullptr;\n}\n\n} // namespace Support\n"
  },
  {
    "path": "Telegram/SourceFiles/support/support_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Shortcuts {\nenum class Command;\n} // namespace Shortcuts\n\nnamespace Support {\n\nenum class SwitchSettings {\n\tNone,\n\tNext,\n\tPrevious,\n};\n\n[[nodiscard]] Qt::KeyboardModifiers SkipSwitchModifiers();\n[[nodiscard]] bool HandleSwitch(Qt::KeyboardModifiers modifiers);\n[[nodiscard]] std::optional<Shortcuts::Command> GetSwitchCommand(\n\tSwitchSettings value);\n[[nodiscard]] FnMut<bool()> GetSwitchMethod(SwitchSettings value);\n\n} // namespace Support\n"
  },
  {
    "path": "Telegram/SourceFiles/support/support_helper.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"support/support_helper.h\"\n\n#include \"dialogs/dialogs_key.h\"\n#include \"data/data_drafts.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"api/api_text_entities.h\"\n#include \"history/history.h\"\n#include \"boxes/abstract_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_entity.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"base/unixtime.h\"\n#include \"lang/lang_keys.h\"\n#include \"window/window_session_controller.h\"\n#include \"storage/storage_account.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"storage/localimageloader.h\"\n#include \"core/launcher.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonArray>\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Support {\nnamespace {\n\nconstexpr auto kOccupyFor = TimeId(60);\nconstexpr auto kReoccupyEach = 30 * crl::time(1000);\nconstexpr auto kMaxSupportInfoLength = MaxMessageSize * 4;\nconstexpr auto kTopicRootId = MsgId(0);\nconstexpr auto kMonoforumPeerId = PeerId(0);\n\nclass EditInfoBox : public Ui::BoxContent {\npublic:\n\tEditInfoBox(\n\t\tQWidget*,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tconst TextWithTags &text,\n\t\tFn<void(TextWithTags, Fn<void(bool success)>)> submit);\n\nprotected:\n\tvoid prepare() override;\n\tvoid setInnerFocus() override;\n\nprivate:\n\tconst not_null<Window::SessionController*> _controller;\n\tobject_ptr<Ui::InputField> _field = { nullptr };\n\tFn<void(TextWithTags, Fn<void(bool success)>)> _submit;\n\n};\n\nEditInfoBox::EditInfoBox(\n\tQWidget*,\n\tnot_null<Window::SessionController*> controller,\n\tconst TextWithTags &text,\n\tFn<void(TextWithTags, Fn<void(bool success)>)> submit)\n: _controller(controller)\n, _field(\n\tthis,\n\tst::supportInfoField,\n\tUi::InputField::Mode::MultiLine,\n\trpl::single(u\"Support information\"_q), // #TODO hard_lang\n\ttext)\n, _submit(std::move(submit)) {\n\t_field->setMaxLength(kMaxSupportInfoLength);\n\t_field->setSubmitSettings(\n\t\tCore::App().settings().sendSubmitWay());\n\t_field->setInstantReplaces(Ui::InstantReplaces::Default());\n\t_field->setInstantReplacesEnabled(\n\t\tCore::App().settings().replaceEmojiValue(),\n\t\tCore::App().settings().systemTextReplaceValue());\n\t_field->setMarkdownReplacesEnabled(true);\n\t_field->setEditLinkCallback(\n\t\tDefaultEditLinkCallback(controller->uiShow(), _field));\n}\n\nvoid EditInfoBox::prepare() {\n\tsetTitle(u\"Edit support information\"_q); // #TODO hard_lang\n\n\tconst auto save = [=] {\n\t\tconst auto done = crl::guard(this, [=](bool success) {\n\t\t\tif (success) {\n\t\t\t\tcloseBox();\n\t\t\t} else {\n\t\t\t\t_field->showError();\n\t\t\t}\n\t\t});\n\t\t_submit(_field->getTextWithAppliedMarkdown(), done);\n\t};\n\taddButton(tr::lng_settings_save(), save);\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\n\t_field->submits() | rpl::on_next(save, _field->lifetime());\n\t_field->cancelled(\n\t) | rpl::on_next([=] {\n\t\tcloseBox();\n\t}, _field->lifetime());\n\tUi::Emoji::SuggestionsController::Init(\n\t\tgetDelegate()->outerContainer(),\n\t\t_field,\n\t\t&_controller->session());\n\n\tauto cursor = _field->textCursor();\n\tcursor.movePosition(QTextCursor::End);\n\t_field->setTextCursor(cursor);\n\n\twidthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_field->resizeToWidth(\n\t\t\twidth - st::boxPadding.left() - st::boxPadding.right());\n\t\t_field->moveToLeft(st::boxPadding.left(), st::boxPadding.bottom());\n\t}, _field->lifetime());\n\n\t_field->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tsetDimensions(\n\t\t\tst::boxWideWidth,\n\t\t\tst::boxPadding.bottom() + height + st::boxPadding.bottom());\n\t}, _field->lifetime());\n}\n\nvoid EditInfoBox::setInnerFocus() {\n\t_field->setFocusFast();\n}\n\nuint32 OccupationTag() {\n\treturn uint32(Core::Launcher::Instance().installationTag() & 0xFFFFFFFF);\n}\n\nQString NormalizeName(QString name) {\n\treturn name.replace(':', '_').replace(';', '_');\n}\n\nData::Draft OccupiedDraft(const QString &normalizedName) {\n\tconst auto now = base::unixtime::now(), till = now + kOccupyFor;\n\treturn {\n\t\tTextWithTags{ \"t:\"\n\t\t\t+ QString::number(till)\n\t\t\t+ \";u:\"\n\t\t\t+ QString::number(OccupationTag())\n\t\t\t+ \";n:\"\n\t\t\t+ normalizedName },\n\t\tFullReplyTo(),\n\t\tSuggestOptions(),\n\t\tMessageCursor(),\n\t\tData::WebPageDraft()\n\t};\n}\n\n[[nodiscard]] bool TrackHistoryOccupation(History *history) {\n\tif (!history) {\n\t\treturn false;\n\t} else if (const auto user = history->peer->asUser()) {\n\t\treturn !user->isBot();\n\t}\n\treturn false;\n}\n\nuint32 ParseOccupationTag(History *history) {\n\tif (!TrackHistoryOccupation(history)) {\n\t\treturn 0;\n\t}\n\tconst auto draft = history->cloudDraft(kTopicRootId, kMonoforumPeerId);\n\tif (!draft) {\n\t\treturn 0;\n\t}\n\tconst auto &text = draft->textWithTags.text;\n\tconst auto parts = QStringView(text).split(';');\n\tauto valid = false;\n\tauto result = uint32();\n\tfor (const auto &part : parts) {\n\t\tif (part.startsWith(u\"t:\"_q)) {\n\t\t\tif (base::StringViewMid(part, 2).toInt() >= base::unixtime::now()) {\n\t\t\t\tvalid = true;\n\t\t\t} else {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t} else if (part.startsWith(u\"u:\"_q)) {\n\t\t\tresult = base::StringViewMid(part, 2).toUInt();\n\t\t}\n\t}\n\treturn valid ? result : 0;\n}\n\nQString ParseOccupationName(History *history) {\n\tif (!TrackHistoryOccupation(history)) {\n\t\treturn QString();\n\t}\n\tconst auto draft = history->cloudDraft(kTopicRootId, kMonoforumPeerId);\n\tif (!draft) {\n\t\treturn QString();\n\t}\n\tconst auto &text = draft->textWithTags.text;\n\tconst auto parts = QStringView(text).split(';');\n\tauto valid = false;\n\tauto result = QString();\n\tfor (const auto &part : parts) {\n\t\tif (part.startsWith(u\"t:\"_q)) {\n\t\t\tif (base::StringViewMid(part, 2).toInt() >= base::unixtime::now()) {\n\t\t\t\tvalid = true;\n\t\t\t} else {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t} else if (part.startsWith(u\"n:\"_q)) {\n\t\t\tresult = base::StringViewMid(part, 2).toString();\n\t\t}\n\t}\n\treturn valid ? result : QString();\n}\n\nTimeId OccupiedBySomeoneTill(History *history) {\n\tif (!TrackHistoryOccupation(history)) {\n\t\treturn 0;\n\t}\n\tconst auto draft = history->cloudDraft(kTopicRootId, kMonoforumPeerId);\n\tif (!draft) {\n\t\treturn 0;\n\t}\n\tconst auto &text = draft->textWithTags.text;\n\tconst auto parts = QStringView(text).split(';');\n\tauto valid = false;\n\tauto result = TimeId();\n\tfor (const auto &part : parts) {\n\t\tif (part.startsWith(u\"t:\"_q)) {\n\t\t\tif (base::StringViewMid(part, 2).toInt() >= base::unixtime::now()) {\n\t\t\t\tresult = base::StringViewMid(part, 2).toInt();\n\t\t\t} else {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t} else if (part.startsWith(u\"u:\"_q)) {\n\t\t\tif (base::StringViewMid(part, 2).toUInt() != OccupationTag()) {\n\t\t\t\tvalid = true;\n\t\t\t} else {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t}\n\t}\n\treturn valid ? result : 0;\n}\n\nQString FastButtonModeIdsPath(not_null<Main::Session*> session) {\n\tconst auto base = session->account().local().supportModePath();\n\tQDir().mkpath(base);\n\treturn base + u\"/fast_button_mode_ids.json\"_q;\n}\n\n} // namespace\n\nHelper::Helper(not_null<Main::Session*> session)\n: _session(session)\n, _api(&_session->mtp())\n, _templates(_session)\n, _reoccupyTimer([=] { reoccupy(); })\n, _checkOccupiedTimer([=] { checkOccupiedChats(); }) {\n\t_api.request(MTPhelp_GetSupportName(\n\t)).done([=](const MTPhelp_SupportName &result) {\n\t\tresult.match([&](const MTPDhelp_supportName &data) {\n\t\t\tsetSupportName(qs(data.vname()));\n\t\t});\n\t}).fail([=] {\n\t\tsetSupportName(\n\t\t\tu\"[rand^\"_q\n\t\t\t+ QString::number(Core::Launcher::Instance().installationTag())\n\t\t\t+ ']');\n\t}).send();\n}\n\nstd::unique_ptr<Helper> Helper::Create(not_null<Main::Session*> session) {\n\t//return std::make_unique<Helper>(session); AssertIsDebug();\n\treturn ShouldUse(session) ? std::make_unique<Helper>(session) : nullptr;\n}\n\nvoid Helper::CheckIfLost(not_null<Window::SessionController*> controller) {\n\tstatic auto Checked = false;\n\tif (Checked) {\n\t\treturn;\n\t}\n\tChecked = true;\n\n\tconst auto session = &controller->session();\n\tif (!ShouldUse(session) || session->supportMode()) {\n\t\treturn;\n\t}\n\tsession->local().writeSelf();\n\tcontroller->show(Ui::MakeConfirmBox({\n\t\t.text = u\"This account should have support mode, \"\n\t\t\t\"but it seems it was lost. Restart?\"_q,\n\t\t.confirmed = [=] { Core::Restart(); },\n\t\t.confirmText = u\"Restart\"_q,\n\t\t.title = u\"Support Mode Lost\"_q,\n\t}));\n}\n\nbool Helper::ShouldUse(not_null<Main::Session*> session) {\n\treturn session->user()->phone().startsWith(u\"424\"_q);\n}\n\nvoid Helper::registerWindow(not_null<Window::SessionController*> controller) {\n\tcontroller->activeChatValue(\n\t) | rpl::map([](Dialogs::Key key) {\n\t\tconst auto history = key.history();\n\t\treturn TrackHistoryOccupation(history) ? history : nullptr;\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](History *history) {\n\t\tupdateOccupiedHistory(controller, history);\n\t}, controller->lifetime());\n}\n\nvoid Helper::cloudDraftChanged(not_null<History*> history) {\n\tchatOccupiedUpdated(history);\n\tif (history != _occupiedHistory) {\n\t\treturn;\n\t}\n\toccupyIfNotYet();\n}\n\nvoid Helper::chatOccupiedUpdated(not_null<History*> history) {\n\tif (const auto till = OccupiedBySomeoneTill(history)) {\n\t\t_occupiedChats[history] = till + 2;\n\t\thistory->session().changes().historyUpdated(\n\t\t\thistory,\n\t\t\tData::HistoryUpdate::Flag::ChatOccupied);\n\t\tcheckOccupiedChats();\n\t} else if (_occupiedChats.take(history)) {\n\t\thistory->session().changes().historyUpdated(\n\t\t\thistory,\n\t\t\tData::HistoryUpdate::Flag::ChatOccupied);\n\t}\n}\n\nvoid Helper::checkOccupiedChats() {\n\tconst auto now = base::unixtime::now();\n\twhile (!_occupiedChats.empty()) {\n\t\tconst auto nearest = ranges::min_element(\n\t\t\t_occupiedChats,\n\t\t\tstd::less<>(),\n\t\t\t[](const auto &pair) { return pair.second; });\n\t\tif (nearest->second <= now) {\n\t\t\tconst auto history = nearest->first;\n\t\t\t_occupiedChats.erase(nearest);\n\t\t\thistory->session().changes().historyUpdated(\n\t\t\t\thistory,\n\t\t\t\tData::HistoryUpdate::Flag::ChatOccupied);\n\t\t} else {\n\t\t\t_checkOccupiedTimer.callOnce(\n\t\t\t\t(nearest->second - now) * crl::time(1000));\n\t\t\treturn;\n\t\t}\n\t}\n\t_checkOccupiedTimer.cancel();\n}\n\nvoid Helper::updateOccupiedHistory(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tHistory *history) {\n\tif (isOccupiedByMe(_occupiedHistory)) {\n\t\t_occupiedHistory->clearCloudDraft(kTopicRootId, kMonoforumPeerId);\n\t\t_session->api().saveDraftToCloudDelayed(_occupiedHistory);\n\t}\n\t_occupiedHistory = history;\n\toccupyInDraft();\n}\n\nvoid Helper::setSupportName(const QString &name) {\n\t_supportName = name;\n\t_supportNameNormalized = NormalizeName(name);\n\toccupyIfNotYet();\n}\n\nvoid Helper::occupyIfNotYet() {\n\tif (!isOccupiedByMe(_occupiedHistory)) {\n\t\toccupyInDraft();\n\t}\n}\n\nvoid Helper::occupyInDraft() {\n\tif (_occupiedHistory\n\t\t&& !isOccupiedBySomeone(_occupiedHistory)\n\t\t&& !_supportName.isEmpty()) {\n\t\tconst auto draft = OccupiedDraft(_supportNameNormalized);\n\t\t_occupiedHistory->createCloudDraft(\n\t\t\tkTopicRootId,\n\t\t\tkMonoforumPeerId,\n\t\t\t&draft);\n\t\t_session->api().saveDraftToCloudDelayed(_occupiedHistory);\n\t\t_reoccupyTimer.callEach(kReoccupyEach);\n\t}\n}\n\nvoid Helper::reoccupy() {\n\tif (isOccupiedByMe(_occupiedHistory)) {\n\t\tconst auto draft = OccupiedDraft(_supportNameNormalized);\n\t\t_occupiedHistory->createCloudDraft(\n\t\t\tkTopicRootId,\n\t\t\tkMonoforumPeerId,\n\t\t\t&draft);\n\t\t_session->api().saveDraftToCloudDelayed(_occupiedHistory);\n\t}\n}\n\nbool Helper::isOccupiedByMe(History *history) const {\n\tif (const auto tag = ParseOccupationTag(history)) {\n\t\treturn (tag == OccupationTag());\n\t}\n\treturn false;\n}\n\nbool Helper::isOccupiedBySomeone(History *history) const {\n\tif (const auto tag = ParseOccupationTag(history)) {\n\t\treturn (tag != OccupationTag());\n\t}\n\treturn false;\n}\n\nvoid Helper::refreshInfo(not_null<UserData*> user) {\n\t_api.request(MTPhelp_GetUserInfo(\n\t\tuser->inputUser()\n\t)).done([=](const MTPhelp_UserInfo &result) {\n\t\tapplyInfo(user, result);\n\t\tif (const auto controller = _userInfoEditPending.take(user)) {\n\t\t\tif (const auto strong = controller->get()) {\n\t\t\t\tshowEditInfoBox(strong, user);\n\t\t\t}\n\t\t}\n\t}).send();\n}\n\nvoid Helper::applyInfo(\n\t\tnot_null<UserData*> user,\n\t\tconst MTPhelp_UserInfo &result) {\n\tconst auto notify = [&] {\n\t\tuser->session().changes().peerUpdated(\n\t\t\tuser,\n\t\t\tData::PeerUpdate::Flag::SupportInfo);\n\t};\n\tconst auto remove = [&] {\n\t\tif (_userInformation.take(user)) {\n\t\t\tnotify();\n\t\t}\n\t};\n\tresult.match([&](const MTPDhelp_userInfo &data) {\n\t\tauto info = UserInfo();\n\t\tinfo.author = qs(data.vauthor());\n\t\tinfo.date = data.vdate().v;\n\t\tinfo.text = TextWithEntities{\n\t\t\tqs(data.vmessage()),\n\t\t\tApi::EntitiesFromMTP(&user->session(), data.ventities().v) };\n\t\tif (info.text.empty()) {\n\t\t\tremove();\n\t\t} else if (_userInformation[user] != info) {\n\t\t\t_userInformation[user] = info;\n\t\t\tnotify();\n\t\t}\n\t}, [&](const MTPDhelp_userInfoEmpty &) {\n\t\tremove();\n\t});\n}\n\nrpl::producer<UserInfo> Helper::infoValue(not_null<UserData*> user) const {\n\treturn user->session().changes().peerFlagsValue(\n\t\tuser,\n\t\tData::PeerUpdate::Flag::SupportInfo\n\t) | rpl::map([=] {\n\t\treturn infoCurrent(user);\n\t});\n}\n\nrpl::producer<QString> Helper::infoLabelValue(\n\t\tnot_null<UserData*> user) const {\n\treturn infoValue(\n\t\tuser\n\t) | rpl::map([](const Support::UserInfo &info) {\n\t\tconst auto time = Ui::FormatDateTime(\n\t\t\tbase::unixtime::parse(info.date));\n\t\treturn info.author + \", \" + time;\n\t});\n}\n\nrpl::producer<TextWithEntities> Helper::infoTextValue(\n\t\tnot_null<UserData*> user) const {\n\treturn infoValue(\n\t\tuser\n\t) | rpl::map([](const Support::UserInfo &info) {\n\t\treturn info.text;\n\t});\n}\n\nUserInfo Helper::infoCurrent(not_null<UserData*> user) const {\n\tconst auto i = _userInformation.find(user);\n\treturn (i != end(_userInformation)) ? i->second : UserInfo();\n}\n\nvoid Helper::editInfo(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> user) {\n\tif (!_userInfoEditPending.contains(user)) {\n\t\t_userInfoEditPending.emplace(user, controller.get());\n\t\trefreshInfo(user);\n\t}\n}\n\nvoid Helper::showEditInfoBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> user) {\n\tconst auto info = infoCurrent(user);\n\tconst auto editData = TextWithTags{\n\t\tinfo.text.text,\n\t\tTextUtilities::ConvertEntitiesToTextTags(info.text.entities)\n\t};\n\n\tconst auto save = [=](TextWithTags result, Fn<void(bool)> done) {\n\t\tsaveInfo(user, TextWithEntities{\n\t\t\tresult.text,\n\t\t\tTextUtilities::ConvertTextTagsToEntities(result.tags)\n\t\t}, done);\n\t};\n\tcontroller->show(Box<EditInfoBox>(controller, editData, save));\n}\n\nvoid Helper::saveInfo(\n\t\tnot_null<UserData*> user,\n\t\tTextWithEntities text,\n\t\tFn<void(bool success)> done) {\n\tconst auto i = _userInfoSaving.find(user);\n\tif (i != end(_userInfoSaving)) {\n\t\tif (i->second.data == text) {\n\t\t\treturn;\n\t\t} else {\n\t\t\ti->second.data = text;\n\t\t\t_api.request(base::take(i->second.requestId)).cancel();\n\t\t}\n\t} else {\n\t\t_userInfoSaving.emplace(user, SavingInfo{ text });\n\t}\n\n\tTextUtilities::PrepareForSending(\n\t\ttext,\n\t\tUi::ItemTextDefaultOptions().flags);\n\tTextUtilities::Trim(text);\n\n\tconst auto entities = Api::EntitiesToMTP(\n\t\t&user->session(),\n\t\ttext.entities,\n\t\tApi::ConvertOption::SkipLocal);\n\t_userInfoSaving[user].requestId = _api.request(MTPhelp_EditUserInfo(\n\t\tuser->inputUser(),\n\t\tMTP_string(text.text),\n\t\tentities\n\t)).done([=](const MTPhelp_UserInfo &result) {\n\t\tapplyInfo(user, result);\n\t\tdone(true);\n\t}).fail([=] {\n\t\tdone(false);\n\t}).send();\n}\n\nTemplates &Helper::templates() {\n\treturn _templates;\n}\n\nFastButtonsBots::FastButtonsBots(not_null<Main::Session*> session)\n: _session(session) {\n}\n\nbool FastButtonsBots::enabled(not_null<PeerData*> peer) const {\n\tif (!_read) {\n\t\tconst_cast<FastButtonsBots*>(this)->read();\n\t}\n\treturn _bots.contains(peer->id);\n}\n\nrpl::producer<bool> FastButtonsBots::enabledValue(\n\t\tnot_null<PeerData*> peer) const {\n\treturn rpl::single(\n\t\tenabled(peer)\n\t) | rpl::then(_changes.events(\n\t) | rpl::filter([=](PeerId id) {\n\t\treturn (peer->id == id);\n\t}) | rpl::map([=] {\n\t\treturn enabled(peer);\n\t}));\n}\n\nvoid FastButtonsBots::setEnabled(not_null<PeerData*> peer, bool value) {\n\tif (value == enabled(peer)) {\n\t\treturn;\n\t} else if (value) {\n\t\t_bots.emplace(peer->id);\n\t} else {\n\t\t_bots.remove(peer->id);\n\t}\n\tif (_bots.empty()) {\n\t\tQFile(FastButtonModeIdsPath(_session)).remove();\n\t} else {\n\t\twrite();\n\t}\n\t_changes.fire_copy(peer->id);\n\tif (const auto history = peer->owner().history(peer)) {\n\t\tif (const auto item = history->lastMessage()) {\n\t\t\thistory->owner().requestItemRepaint(item);\n\t\t}\n\t}\n}\n\nvoid FastButtonsBots::write() {\n\tauto array = QJsonArray();\n\tfor (const auto &id : _bots) {\n\t\tarray.append(QString::number(id.value));\n\t}\n\tauto object = QJsonObject();\n\tobject[u\"ids\"_q] = array;\n\tauto f = QFile(FastButtonModeIdsPath(_session));\n\tif (f.open(QIODevice::WriteOnly)) {\n\t\tf.write(QJsonDocument(object).toJson(QJsonDocument::Indented));\n\t}\n}\n\nvoid FastButtonsBots::read() {\n\t_read = true;\n\n\tauto f = QFile(FastButtonModeIdsPath(_session));\n\tif (!f.open(QIODevice::ReadOnly)) {\n\t\treturn;\n\t}\n\tconst auto data = f.readAll();\n\tconst auto json = QJsonDocument::fromJson(data);\n\tif (!json.isObject()) {\n\t\treturn;\n\t}\n\tconst auto object = json.object();\n\tconst auto array = object.value(u\"ids\"_q).toArray();\n\tfor (const auto &value : array) {\n\t\tconst auto bareId = value.toString().toULongLong();\n\t\t_bots.emplace(PeerId(bareId));\n\t}\n}\n\nQString ChatOccupiedString(not_null<History*> history) {\n\tconst auto hand = QString::fromUtf8(\"\\xe2\\x9c\\x8b\\xef\\xb8\\x8f\");\n\tconst auto name = ParseOccupationName(history);\n\treturn (name.isEmpty() || name.startsWith(u\"[rand^\"_q))\n\t\t? hand + \" chat taken\"\n\t\t: hand + ' ' + name + \" is here\";\n}\n\nQString InterpretSendPath(\n\t\tnot_null<Window::SessionController*> window,\n\t\tconst QString &path) {\n\tQFile f(path);\n\tif (!f.open(QIODevice::ReadOnly)) {\n\t\treturn \"App Error: Could not open interpret file: \" + path;\n\t}\n\tconst auto content = QString::fromUtf8(f.readAll());\n\tf.close();\n\tconst auto lines = content.split('\\n');\n\tauto toId = PeerId(0);\n\tauto topicRootId = MsgId(0);\n\tauto filePath = QString();\n\tauto caption = QString();\n\tfor (const auto &line : lines) {\n\t\tif (line.startsWith(u\"from: \"_q)) {\n\t\t\tif (window->session().userId().bare\n\t\t\t\t!= base::StringViewMid(\n\t\t\t\t\tline,\n\t\t\t\t\tu\"from: \"_q.size()).toULongLong()) {\n\t\t\t\treturn \"App Error: Wrong current user.\";\n\t\t\t}\n\t\t} else if (line.startsWith(u\"channel: \"_q)) {\n\t\t\tconst auto channelId = base::StringViewMid(\n\t\t\t\tline,\n\t\t\t\tu\"channel: \"_q.size()).toULongLong();\n\t\t\ttoId = peerFromChannel(channelId);\n\t\t} else if (line.startsWith(u\"topic: \"_q)) {\n\t\t\tconst auto topicId = base::StringViewMid(\n\t\t\t\tline,\n\t\t\t\tu\"topic: \"_q.size()).toULongLong();\n\t\t\ttopicRootId = MsgId(topicId);\n\t\t} else if (line.startsWith(u\"file: \"_q)) {\n\t\t\tconst auto path = line.mid(u\"file: \"_q.size());\n\t\t\tif (!QFile(path).exists()) {\n\t\t\t\treturn \"App Error: Could not find file with path: \" + path;\n\t\t\t}\n\t\t\tfilePath = path;\n\t\t} else if (line.startsWith(u\"caption: \"_q)) {\n\t\t\tcaption = line.mid(u\"caption: \"_q.size());\n\t\t} else if (!caption.isEmpty()) {\n\t\t\tcaption += '\\n' + line;\n\t\t} else {\n\t\t\treturn \"App Error: Invalid command: \" + line;\n\t\t}\n\t}\n\tconst auto history = window->session().data().historyLoaded(toId);\n\tconst auto sendTo = [=](not_null<Data::Thread*> thread) {\n\t\twindow->showThread(thread);\n\t\tconst auto premium = thread->session().user()->isPremium();\n\t\tauto list = Storage::PrepareMediaList(\n\t\t\tQStringList(filePath),\n\t\t\tst::sendMediaPreviewSize,\n\t\t\tpremium);\n\t\tif (!list.files.empty()) {\n\t\t\tlist.files.back().caption.text = caption;\n\t\t\tthread->session().api().sendFiles(\n\t\t\t\tstd::move(list),\n\t\t\t\tSendMediaType::File,\n\t\t\t\tnullptr,\n\t\t\t\tApi::SendAction(thread));\n\t\t}\n\t};\n\tif (!history) {\n\t\treturn \"App Error: Could not find channel with id: \"\n\t\t\t+ QString::number(peerToChannel(toId).bare);\n\t} else if (const auto forum = history->asForum()) {\n\t\tforum->requestTopic(topicRootId, [=] {\n\t\t\tif (const auto forum = history->asForum()) {\n\t\t\t\tif (const auto topic = forum->topicFor(topicRootId)) {\n\t\t\t\t\tsendTo(topic);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t} else if (!topicRootId) {\n\t\tsendTo(history);\n\t}\n\treturn QString();\n}\n\n} // namespace Support\n"
  },
  {
    "path": "Telegram/SourceFiles/support/support_helper.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"support/support_templates.h\"\n#include \"mtproto/sender.h\"\n\nclass History;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Support {\n\nstruct UserInfo {\n\tQString author;\n\tTimeId date = 0;\n\tTextWithEntities text;\n};\n\ninline bool operator==(const UserInfo &a, const UserInfo &b) {\n\treturn (a.author == b.author)\n\t\t&& (a.date == b.date)\n\t\t&& (a.text == b.text);\n}\n\ninline bool operator!=(const UserInfo &a, const UserInfo &b) {\n\treturn !(a == b);\n}\n\nclass Helper final {\npublic:\n\texplicit Helper(not_null<Main::Session*> session);\n\n\tstatic std::unique_ptr<Helper> Create(not_null<Main::Session*> session);\n\tstatic void CheckIfLost(not_null<Window::SessionController*> controller);\n\n\tvoid registerWindow(not_null<Window::SessionController*> controller);\n\tvoid cloudDraftChanged(not_null<History*> history);\n\n\tvoid chatOccupiedUpdated(not_null<History*> history);\n\n\t[[nodiscard]] bool isOccupiedByMe(History *history) const;\n\t[[nodiscard]] bool isOccupiedBySomeone(History *history) const;\n\n\tvoid refreshInfo(not_null<UserData*> user);\n\t[[nodiscard]] rpl::producer<UserInfo> infoValue(\n\t\tnot_null<UserData*> user) const;\n\t[[nodiscard]] rpl::producer<QString> infoLabelValue(\n\t\tnot_null<UserData*> user) const;\n\t[[nodiscard]] rpl::producer<TextWithEntities> infoTextValue(\n\t\tnot_null<UserData*> user) const;\n\t[[nodiscard]] UserInfo infoCurrent(not_null<UserData*> user) const;\n\tvoid editInfo(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> user);\n\n\tTemplates &templates();\n\nprivate:\n\tstruct SavingInfo {\n\t\tTextWithEntities data;\n\t\tmtpRequestId requestId = 0;\n\t};\n\tvoid checkOccupiedChats();\n\tvoid updateOccupiedHistory(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tHistory *history);\n\tvoid setSupportName(const QString &name);\n\tvoid occupyIfNotYet();\n\tvoid occupyInDraft();\n\tvoid reoccupy();\n\n\tvoid applyInfo(\n\t\tnot_null<UserData*> user,\n\t\tconst MTPhelp_UserInfo &result);\n\tvoid showEditInfoBox(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> user);\n\tvoid saveInfo(\n\t\tnot_null<UserData*> user,\n\t\tTextWithEntities text,\n\t\tFn<void(bool success)> done);\n\n\tstatic bool ShouldUse(not_null<Main::Session*> session);\n\n\tconst not_null<Main::Session*> _session;\n\tMTP::Sender _api;\n\tTemplates _templates;\n\tQString _supportName;\n\tQString _supportNameNormalized;\n\n\tHistory *_occupiedHistory = nullptr;\n\tbase::Timer _reoccupyTimer;\n\tbase::Timer _checkOccupiedTimer;\n\tbase::flat_map<not_null<History*>, TimeId> _occupiedChats;\n\n\tbase::flat_map<not_null<UserData*>, UserInfo> _userInformation;\n\tbase::flat_map<\n\t\tnot_null<UserData*>,\n\t\tbase::weak_ptr<Window::SessionController>> _userInfoEditPending;\n\tbase::flat_map<not_null<UserData*>, SavingInfo> _userInfoSaving;\n\n\trpl::lifetime _lifetime;\n\n};\n\nclass FastButtonsBots final {\npublic:\n\texplicit FastButtonsBots(not_null<Main::Session*> session);\n\n\t[[nodiscard]] bool enabled(not_null<PeerData*> peer) const;\n\t[[nodiscard]] rpl::producer<bool> enabledValue(\n\t\tnot_null<PeerData*> peer) const;\n\tvoid setEnabled(not_null<PeerData*> peer, bool value);\n\nprivate:\n\tvoid write();\n\tvoid read();\n\n\tconst not_null<Main::Session*> _session;\n\n\tbase::flat_set<PeerId> _bots;\n\trpl::event_stream<PeerId> _changes;\n\tbool _read = false;\n\n};\n\nQString ChatOccupiedString(not_null<History*> history);\n\nQString InterpretSendPath(\n\tnot_null<Window::SessionController*> window,\n\tconst QString &path);\n\n} // namespace Support\n"
  },
  {
    "path": "Telegram/SourceFiles/support/support_preload.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"support/support_preload.h\"\n\n#include \"history/history.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_histories.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n\nnamespace Support {\nnamespace {\n\nconstexpr auto kPreloadMessagesCount = 50;\n\n} // namespace\n\nint SendPreloadRequest(not_null<History*> history, Fn<void()> retry) {\n\tauto offsetId = MsgId();\n\tauto offset = 0;\n\tauto loadCount = kPreloadMessagesCount;\n\tif (const auto around = history->loadAroundId()) {\n\t\thistory->getReadyFor(ShowAtUnreadMsgId);\n\t\toffset = -loadCount / 2;\n\t\toffsetId = around;\n\t}\n\tconst auto offsetDate = 0;\n\tconst auto maxId = 0;\n\tconst auto minId = 0;\n\tconst auto historyHash = uint64(0);\n\tconst auto type = Data::Histories::RequestType::History;\n\tauto &histories = history->owner().histories();\n\treturn histories.sendRequest(history, type, [=](Fn<void()> finish) {\n\t\treturn history->session().api().request(MTPmessages_GetHistory(\n\t\t\thistory->peer->input(),\n\t\t\tMTP_int(offsetId),\n\t\t\tMTP_int(offsetDate),\n\t\t\tMTP_int(offset),\n\t\t\tMTP_int(loadCount),\n\t\t\tMTP_int(maxId),\n\t\t\tMTP_int(minId),\n\t\t\tMTP_long(historyHash)\n\t\t)).done([=](const MTPmessages_Messages &result) {\n\t\t\tif (const auto around = history->loadAroundId()) {\n\t\t\t\tif (around != offsetId) {\n\t\t\t\t\tretry();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\thistory->clear(History::ClearType::Unload);\n\t\t\t\thistory->getReadyFor(ShowAtUnreadMsgId);\n\t\t\t} else if (offsetId) {\n\t\t\t\tretry();\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\thistory->clear(History::ClearType::Unload);\n\t\t\t\thistory->getReadyFor(ShowAtTheEndMsgId);\n\t\t\t}\n\t\t\tresult.match([](const MTPDmessages_messagesNotModified&) {\n\t\t\t}, [&](const auto &data) {\n\t\t\t\thistory->owner().processUsers(data.vusers());\n\t\t\t\thistory->owner().processChats(data.vchats());\n\t\t\t\thistory->addOlderSlice(data.vmessages().v);\n\t\t\t});\n\t\t\tfinish();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tfinish();\n\t\t}).send();\n\t});\n}\n\n} // namespace Support\n"
  },
  {
    "path": "Telegram/SourceFiles/support/support_preload.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass History;\n\nnamespace Support {\n\n// Returns histories().request, not api().request.\n[[nodiscard]] int SendPreloadRequest(\n\tnot_null<History*> history,\n\tFn<void()> retry);\n\n} // namespace Support\n"
  },
  {
    "path": "Telegram/SourceFiles/support/support_templates.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"support/support_templates.h\"\n\n#include \"ui/toast/toast.h\"\n#include \"data/data_session.h\"\n#include \"core/shortcuts.h\"\n#include \"main/main_session.h\"\n\n#include <QtNetwork/QNetworkAccessManager>\n\nnamespace Support {\nnamespace details {\nnamespace {\n\nconstexpr auto kQueryLimit = 10;\nconstexpr auto kWeightStep = 1000;\n\nstruct Delta {\n\tstd::vector<const TemplatesQuestion*> added;\n\tstd::vector<const TemplatesQuestion*> changed;\n\tstd::vector<const TemplatesQuestion*> removed;\n\n\tstd::map<QString, QStringList> keys;\n\n\texplicit operator bool() const {\n\t\treturn !added.empty() || !changed.empty() || !removed.empty();\n\t}\n};\n\nbool IsTemplatesFile(const QString &file) {\n\treturn file.startsWith(u\"tl_\"_q, Qt::CaseInsensitive)\n\t\t&& file.endsWith(u\".txt\"_q, Qt::CaseInsensitive);\n}\n\nQString NormalizeQuestion(const QString &question) {\n\tauto result = QString();\n\tresult.reserve(question.size());\n\tfor (const auto &ch : question) {\n\t\tif (ch.isLetterOrNumber()) {\n\t\t\tresult.append(ch.toLower());\n\t\t}\n\t}\n\treturn result;\n}\n\nQString NormalizeKey(const QString &query) {\n\treturn TextUtilities::RemoveAccents(query.trimmed().toLower());\n}\n\nstruct FileResult {\n\tTemplatesFile result;\n\tQStringList errors;\n};\n\nenum class ReadState {\n\tNone,\n\tQuestion,\n\tKeys,\n\tValue,\n\tUrl,\n};\n\ntemplate <typename StateChange, typename LineCallback>\nvoid ReadByLine(\n\tconst QByteArray &blob,\n\tStateChange &&stateChange,\n\tLineCallback &&lineCallback) {\n\tusing State = ReadState;\n\tauto state = State::None;\n\tauto hadKeys = false;\n\tauto hadValue = false;\n\tfor (const auto &utf : blob.split('\\n')) {\n\t\tconst auto line = QString::fromUtf8(utf).trimmed();\n\t\tconst auto match = QRegularExpression(\n\t\t\tu\"^\\\\{([A-Z_]+)\\\\}$\"_q\n\t\t).match(line);\n\t\tif (match.hasMatch()) {\n\t\t\tconst auto token = match.captured(1);\n\t\t\tif (state == State::Value) {\n\t\t\t\thadKeys = hadValue = false;\n\t\t\t}\n\t\t\tconst auto newState = [&] {\n\t\t\t\tif (token == u\"VALUE\"_q) {\n\t\t\t\t\treturn hadValue ? State::None : State::Value;\n\t\t\t\t} else if (token == u\"KEYS\"_q) {\n\t\t\t\t\treturn hadKeys ? State::None : State::Keys;\n\t\t\t\t} else if (token == u\"QUESTION\"_q) {\n\t\t\t\t\treturn State::Question;\n\t\t\t\t} else if (token == u\"URL\"_q) {\n\t\t\t\t\treturn State::Url;\n\t\t\t\t} else {\n\t\t\t\t\treturn State::None;\n\t\t\t\t}\n\t\t\t}();\n\t\t\tstateChange(state, newState);\n\t\t\tstate = newState;\n\t\t\tlineCallback(state, line, true);\n\t\t} else {\n\t\t\tif (!line.isEmpty()) {\n\t\t\t\tif (state == State::Value) {\n\t\t\t\t\thadValue = true;\n\t\t\t\t} else if (state == State::Keys) {\n\t\t\t\t\thadKeys = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tlineCallback(state, line, false);\n\t\t}\n\t}\n}\n\ntemplate <typename Callback>\nQString ReadByLineGetUrl(const QByteArray &blob, Callback &&callback) {\n\tusing State = ReadState;\n\tauto url = QString();\n\tauto question = TemplatesQuestion();\n\tconst auto call = [&] {\n\t\twhile (question.value.endsWith('\\n')) {\n\t\t\tquestion.value.chop(1);\n\t\t}\n\t\treturn callback(base::take(question));\n\t};\n\tReadByLine(blob, [&](State was, State now) {\n\t\tif (was == State::Value) {\n\t\t\tcall();\n\t\t}\n\t}, [&](State state, const QString &line, bool stateChangeLine) {\n\t\tif (stateChangeLine) {\n\t\t\treturn;\n\t\t}\n\t\tswitch (state) {\n\t\tcase State::Keys:\n\t\t\tif (!line.isEmpty()) {\n\t\t\t\tquestion.originalKeys.push_back(line);\n\t\t\t\tif (const auto norm = NormalizeKey(line); !norm.isEmpty()) {\n\t\t\t\t\tquestion.normalizedKeys.push_back(norm);\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\tcase State::Value:\n\t\t\tif (!question.value.isEmpty()) {\n\t\t\t\tquestion.value += '\\n';\n\t\t\t}\n\t\t\tquestion.value += line;\n\t\t\tbreak;\n\t\tcase State::Question:\n\t\t\tif (question.question.isEmpty()) {\n\t\t\t\tquestion.question = line;\n\t\t\t}\n\t\t\tbreak;\n\t\tcase State::Url:\n\t\t\tif (url.isEmpty()) {\n\t\t\t\turl = line;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t});\n\tcall();\n\treturn url;\n}\n\nFileResult ReadFromBlob(const QByteArray &blob) {\n\tauto result = FileResult();\n\tresult.result.url = ReadByLineGetUrl(blob, [&](TemplatesQuestion &&q) {\n\t\tconst auto normalized = NormalizeQuestion(q.question);\n\t\tif (!normalized.isEmpty()) {\n\t\t\tresult.result.questions.emplace(normalized, std::move(q));\n\t\t}\n\t});\n\treturn result;\n}\n\nFileResult ReadFile(const QString &path) {\n\tQFile f(path);\n\tif (!f.open(QIODevice::ReadOnly)) {\n\t\tauto result = FileResult();\n\t\tresult.errors.push_back(\n\t\t\tu\"Couldn't open '%1' for reading!\"_q.arg(path));\n\t\treturn result;\n\t}\n\n\tconst auto blob = f.readAll();\n\tf.close();\n\n\treturn ReadFromBlob(blob);\n}\n\nvoid WriteWithOwnUrlAndKeys(\n\t\tQIODevice &device,\n\t\tconst QByteArray &blob,\n\t\tconst QString &url,\n\t\tconst Delta &delta) {\n\tdevice.write(\"{URL}\\n\");\n\tdevice.write(url.toUtf8());\n\tdevice.write(\"\\n\\n\");\n\n\tusing State = ReadState;\n\tauto question = QString();\n\tauto normalized = QString();\n\tauto ownKeysWritten = false;\n\tReadByLine(blob, [&](State was, State now) {\n\t\tif (was == State::Value) {\n\t\t\tquestion = normalized = QString();\n\t\t}\n\t}, [&](State state, const QString &line, bool stateChangeLine) {\n\t\tconst auto writeLine = [&] {\n\t\t\tdevice.write(line.toUtf8());\n\t\t\tdevice.write(\"\\n\", 1);\n\t\t};\n\t\tswitch (state) {\n\t\tcase State::Keys:\n\t\t\tif (stateChangeLine) {\n\t\t\t\twriteLine();\n\t\t\t\townKeysWritten = [&] {\n\t\t\t\t\tif (normalized.isEmpty()) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\tconst auto i = delta.keys.find(normalized);\n\t\t\t\t\tif (i == end(delta.keys)) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\tdevice.write(i->second.join('\\n').toUtf8());\n\t\t\t\t\tdevice.write(\"\\n\", 1);\n\t\t\t\t\treturn true;\n\t\t\t\t}();\n\t\t\t} else if (!ownKeysWritten) {\n\t\t\t\twriteLine();\n\t\t\t}\n\t\t\tbreak;\n\t\tcase State::Value:\n\t\t\twriteLine();\n\t\t\tbreak;\n\t\tcase State::Question:\n\t\t\twriteLine();\n\t\t\tif (!stateChangeLine && question.isEmpty()) {\n\t\t\t\tquestion = line;\n\t\t\t\tnormalized = NormalizeQuestion(line);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase State::Url:\n\t\t\tbreak;\n\t\t}\n\t});\n}\n\nstruct FilesResult {\n\tTemplatesData result;\n\tTemplatesIndex index;\n\tQStringList errors;\n};\n\nFilesResult ReadFiles(const QString &folder) {\n\tauto result = FilesResult();\n\tconst auto files = QDir(folder).entryList(QDir::Files);\n\tfor (const auto &path : files) {\n\t\tif (!IsTemplatesFile(path)) {\n\t\t\tcontinue;\n\t\t}\n\t\tauto file = ReadFile(folder + '/' + path);\n\t\tif (!file.result.url.isEmpty() || !file.result.questions.empty()) {\n\t\t\tresult.result.files[path] = std::move(file.result);\n\t\t}\n\t\tresult.errors.append(std::move(file.errors));\n\t}\n\treturn result;\n}\n\nTemplatesIndex ComputeIndex(const TemplatesData &data) {\n\tusing Id = TemplatesIndex::Id;\n\tusing Term = TemplatesIndex::Term;\n\n\tauto uniqueFirst = std::map<QChar, base::flat_set<Id>>();\n\tauto uniqueFull = std::map<Id, base::flat_set<Term>>();\n\tconst auto pushString = [&](\n\t\t\tconst Id &id,\n\t\t\tconst QString &string,\n\t\t\tint weight) {\n\t\tconst auto list = TextUtilities::PrepareSearchWords(string);\n\t\tfor (const auto &word : list) {\n\t\t\tuniqueFirst[word[0]].emplace(id);\n\t\t\tuniqueFull[id].emplace(std::make_pair(word, weight));\n\t\t}\n\t};\n\tfor (const auto &[path, file] : data.files) {\n\t\tfor (const auto &[normalized, question] : file.questions) {\n\t\t\tconst auto id = std::make_pair(path, normalized);\n\t\t\tfor (const auto &key : question.normalizedKeys) {\n\t\t\t\tpushString(id, key, kWeightStep * kWeightStep);\n\t\t\t}\n\t\t\tpushString(id, question.question, kWeightStep);\n\t\t\tpushString(id, question.value, 1);\n\t\t}\n\t}\n\n\tauto result = TemplatesIndex();\n\tfor (const auto &[ch, unique] : uniqueFirst) {\n\t\tresult.first.emplace(ch, unique | ranges::to_vector);\n\t}\n\tfor (const auto &[id, unique] : uniqueFull) {\n\t\tresult.full.emplace(id, unique | ranges::to_vector);\n\t}\n\treturn result;\n}\n\nvoid ReplaceFileIndex(\n\t\tTemplatesIndex &result,\n\t\tTemplatesIndex &&source,\n\t\tconst QString &path) {\n\tfor (auto i = begin(result.full); i != end(result.full);) {\n\t\tif (i->first.first == path) {\n\t\t\ti = result.full.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tfor (auto &[id, list] : source.full) {\n\t\tresult.full.emplace(id, std::move(list));\n\t}\n\n\tusing Id = TemplatesIndex::Id;\n\tfor (auto &[ch, list] : result.first) {\n\t\tauto i = ranges::lower_bound(\n\t\t\tlist,\n\t\t\tstd::make_pair(path, QString()));\n\t\tauto j = std::find_if(i, end(list), [&](const Id &id) {\n\t\t\treturn id.first != path;\n\t\t});\n\t\tlist.erase(i, j);\n\t}\n\tfor (auto &[ch, list] : source.first) {\n\t\tauto &to = result.first[ch];\n\t\tto.insert(\n\t\t\tend(to),\n\t\t\tstd::make_move_iterator(begin(list)),\n\t\t\tstd::make_move_iterator(end(list)));\n\t\tranges::sort(to);\n\t}\n}\n\nvoid MoveKeys(TemplatesFile &to, const TemplatesFile &from) {\n\tconst auto &existing = from.questions;\n\tfor (auto &[normalized, question] : to.questions) {\n\t\tif (const auto i = existing.find(normalized); i != end(existing)) {\n\t\t\tquestion.originalKeys = i->second.originalKeys;\n\t\t\tquestion.normalizedKeys = i->second.normalizedKeys;\n\t\t}\n\t}\n}\n\nDelta ComputeDelta(const TemplatesFile &was, const TemplatesFile &now) {\n\tauto result = Delta();\n\tfor (const auto &[normalized, question] : now.questions) {\n\t\tconst auto i = was.questions.find(normalized);\n\t\tif (i == end(was.questions)) {\n\t\t\tresult.added.push_back(&question);\n\t\t} else {\n\t\t\tresult.keys.emplace(normalized, i->second.originalKeys);\n\t\t\tif (i->second.value != question.value) {\n\t\t\t\tresult.changed.push_back(&question);\n\t\t\t}\n\t\t}\n\t}\n\tfor (const auto &[normalized, question] : was.questions) {\n\t\tif (result.keys.find(normalized) == end(result.keys)) {\n\t\t\tresult.removed.push_back(&question);\n\t\t}\n\t}\n\treturn result;\n}\n\nQString FormatUpdateNotification(const QString &path, const Delta &delta) {\n\tauto result = u\"Template file '%1' updated!\\n\\n\"_q.arg(path);\n\tif (!delta.added.empty()) {\n\t\tresult += u\"-------- Added --------\\n\\n\"_q;\n\t\tfor (const auto question : delta.added) {\n\t\t\tresult += u\"Q: %1\\nK: %2\\nA: %3\\n\\n\"_q.arg(\n\t\t\t\tquestion->question,\n\t\t\t\tquestion->originalKeys.join(u\", \"_q),\n\t\t\t\tquestion->value.trimmed());\n\t\t}\n\t}\n\tif (!delta.changed.empty()) {\n\t\tresult += u\"-------- Modified --------\\n\\n\"_q;\n\t\tfor (const auto question : delta.changed) {\n\t\t\tresult += u\"Q: %1\\nA: %2\\n\\n\"_q.arg(\n\t\t\t\tquestion->question,\n\t\t\t\tquestion->value.trimmed());\n\t\t}\n\t}\n\tif (!delta.removed.empty()) {\n\t\tresult += u\"-------- Removed --------\\n\\n\"_q;\n\t\tfor (const auto question : delta.removed) {\n\t\t\tresult += u\"Q: %1\\n\\n\"_q.arg(question->question);\n\t\t}\n\t}\n\treturn result;\n}\n\nQString UpdateFile(\n\tconst QString &path,\n\tconst QByteArray &content,\n\tconst QString &url,\n\tconst Delta &delta) {\n\tauto result = QString();\n\tconst auto full = cWorkingDir() + \"TEMPLATES/\" + path;\n\tconst auto old = full + u\".old\"_q;\n\tQFile(old).remove();\n\tif (QFile(full).copy(old)) {\n\t\tresult += u\"(old file saved at '%1')\"_q.arg(path + u\".old\"_q);\n\n\t\tQFile f(full);\n\t\tif (f.open(QIODevice::WriteOnly)) {\n\t\t\tWriteWithOwnUrlAndKeys(f, content, url, delta);\n\t\t} else {\n\t\t\tresult += u\"\\n\\nError: could not open new file '%1'!\"_q.arg(full);\n\t\t}\n\t} else {\n\t\tresult += u\"Error: could not save old file '%1'!\"_q.arg(old);\n\t}\n\treturn result;\n}\n\nint CountMaxKeyLength(const TemplatesData &data) {\n\tauto result = 0;\n\tfor (const auto &[path, file] : data.files) {\n\t\tfor (const auto &[normalized, question] : file.questions) {\n\t\t\tfor (const auto &key : question.normalizedKeys) {\n\t\t\t\taccumulate_max(result, int(key.size()));\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\n} // namespace\n} // namespace details\n\nusing namespace details;\n\nstruct Templates::Updates {\n\tQNetworkAccessManager manager;\n\tstd::map<QString, QNetworkReply*> requests;\n};\n\nTemplates::Templates(not_null<Main::Session*> session) : _session(session) {\n\tload();\n\tShortcuts::Requests(\n\t) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {\n\t\tusing Command = Shortcuts::Command;\n\t\trequest->check(\n\t\t\tCommand::SupportReloadTemplates\n\t\t) && request->handle([=] {\n\t\t\treload();\n\t\t\treturn true;\n\t\t});\n\t}, _lifetime);\n}\n\nvoid Templates::reload() {\n\t_reloadToastSubscription = errors(\n\t) | rpl::on_next([=](QStringList errors) {\n\t\tUi::Toast::Show(errors.isEmpty()\n\t\t\t? \"Templates reloaded!\"\n\t\t\t: (\"Errors:\\n\\n\" + errors.join(\"\\n\\n\")));\n\t});\n\n\tload();\n}\n\nvoid Templates::load() {\n\tif (_reloadAfterRead) {\n\t\treturn;\n\t} else if (_reading || _updates) {\n\t\t_reloadAfterRead = true;\n\t\treturn;\n\t}\n\n\tcrl::async([=, guard = _reading.make_guard()]() mutable {\n\t\tauto result = ReadFiles(cWorkingDir() + \"TEMPLATES\");\n\t\tresult.index = ComputeIndex(result.result);\n\t\tcrl::on_main(std::move(guard), [\n\t\t\t=,\n\t\t\tresult = std::move(result)\n\t\t]() mutable {\n\t\t\tsetData(std::move(result.result));\n\t\t\t_index = std::move(result.index);\n\t\t\t_errors.fire(std::move(result.errors));\n\t\t\tcrl::on_main(this, [=] {\n\t\t\t\tif (base::take(_reloadAfterRead)) {\n\t\t\t\t\treload();\n\t\t\t\t} else {\n\t\t\t\t\tupdate();\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t});\n}\n\nvoid Templates::setData(TemplatesData &&data) {\n\t_data = std::move(data);\n\t_maxKeyLength = CountMaxKeyLength(_data);\n}\n\nvoid Templates::ensureUpdatesCreated() {\n\tif (_updates) {\n\t\treturn;\n\t}\n\t_updates = std::make_unique<Updates>();\n\tQObject::connect(\n\t\t&_updates->manager,\n\t\t&QNetworkAccessManager::finished,\n\t\t[=](QNetworkReply *reply) { updateRequestFinished(reply); });\n}\n\nvoid Templates::update() {\n\tconst auto sendRequest = [&](const QString &path, const QString &url) {\n\t\tensureUpdatesCreated();\n\t\tif (_updates->requests.find(path) != end(_updates->requests)) {\n\t\t\treturn;\n\t\t}\n\t\t_updates->requests.emplace(\n\t\t\tpath,\n\t\t\t_updates->manager.get(QNetworkRequest(url)));\n\t};\n\n\tfor (const auto &[path, file] : _data.files) {\n\t\tif (!file.url.isEmpty()) {\n\t\t\tsendRequest(path, file.url);\n\t\t}\n\t}\n}\n\nvoid Templates::updateRequestFinished(QNetworkReply *reply) {\n\treply->deleteLater();\n\n\tconst auto path = [&] {\n\t\tfor (const auto &[file, sent] : _updates->requests) {\n\t\t\tif (sent == reply) {\n\t\t\t\treturn file;\n\t\t\t}\n\t\t}\n\t\treturn QString();\n\t}();\n\tif (path.isEmpty()) {\n\t\treturn;\n\t}\n\t_updates->requests[path] = nullptr;\n\tif (reply->error() != QNetworkReply::NoError) {\n\t\tconst auto message = (\n\t\t\tu\"Error: template update failed, url '%1', error %2, %3\"_q\n\t\t).arg(reply->url().toDisplayString()\n\t\t).arg(reply->error()\n\t\t).arg(reply->errorString());\n\t\t_session->data().serviceNotification({ message });\n\t\treturn;\n\t}\n\tLOG((\"Got template from url '%1'\"\n\t\t).arg(reply->url().toDisplayString()));\n\tconst auto content = reply->readAll();\n\tcrl::async([=, weak = base::make_weak(this)]{\n\t\tauto result = ReadFromBlob(content);\n\t\tauto one = TemplatesData();\n\t\tone.files.emplace(path, std::move(result.result));\n\t\tauto index = ComputeIndex(one);\n\t\tcrl::on_main(weak,[\n\t\t\t=,\n\t\t\tone = std::move(one),\n\t\t\terrors = std::move(result.errors),\n\t\t\tindex = std::move(index)\n\t\t]() mutable {\n\t\t\tauto &existing = _data.files.at(path);\n\t\t\tauto &parsed = one.files.at(path);\n\t\t\tMoveKeys(parsed, existing);\n\t\t\tReplaceFileIndex(_index, ComputeIndex(one), path);\n\t\t\tif (!errors.isEmpty()) {\n\t\t\t\t_errors.fire(std::move(errors));\n\t\t\t}\n\t\t\tif (const auto delta = ComputeDelta(existing, parsed)) {\n\t\t\t\tconst auto text = FormatUpdateNotification(\n\t\t\t\t\tpath,\n\t\t\t\t\tdelta);\n\t\t\t\tconst auto copy = UpdateFile(\n\t\t\t\t\tpath,\n\t\t\t\t\tcontent,\n\t\t\t\t\texisting.url,\n\t\t\t\t\tdelta);\n\t\t\t\tconst auto full = text + copy;\n\t\t\t\t_session->data().serviceNotification({ full });\n\t\t\t}\n\t\t\t_data.files.at(path) = std::move(one.files.at(path));\n\n\t\t\t_updates->requests.erase(path);\n\t\t\tcheckUpdateFinished();\n\t\t});\n\t\t});\n}\n\nvoid Templates::checkUpdateFinished() {\n\tif (!_updates || !_updates->requests.empty()) {\n\t\treturn;\n\t}\n\t_updates = nullptr;\n\tif (base::take(_reloadAfterRead)) {\n\t\treload();\n\t}\n}\n\nauto Templates::matchExact(QString query) const\n-> std::optional<QuestionByKey> {\n\tif (query.isEmpty() || query.size() > _maxKeyLength) {\n\t\treturn {};\n\t}\n\n\tquery = NormalizeKey(query);\n\n\tfor (const auto &[path, file] : _data.files) {\n\t\tfor (const auto &[normalized, question] : file.questions) {\n\t\t\tfor (const auto &key : question.normalizedKeys) {\n\t\t\t\tif (key == query) {\n\t\t\t\t\treturn QuestionByKey{ question, key };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn {};\n}\n\nauto Templates::matchFromEnd(QString query) const\n-> std::optional<QuestionByKey> {\n\tif (query.size() > _maxKeyLength) {\n\t\tquery = query.mid(query.size() - _maxKeyLength);\n\t}\n\n\tconst auto size = query.size();\n\tauto queries = std::vector<QString>();\n\tqueries.reserve(size);\n\tfor (auto i = 0; i != size; ++i) {\n\t\tqueries.push_back(NormalizeKey(query.mid(size - i - 1)));\n\t}\n\n\tauto result = std::optional<QuestionByKey>();\n\tfor (const auto &[path, file] : _data.files) {\n\t\tfor (const auto &[normalized, question] : file.questions) {\n\t\t\tfor (const auto &key : question.normalizedKeys) {\n\t\t\t\tif (key.size() <= queries.size()\n\t\t\t\t\t&& queries[key.size() - 1] == key\n\t\t\t\t\t&& (!result || result->key.size() <= key.size())) {\n\t\t\t\t\tresult = QuestionByKey{ question, key };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nTemplates::~Templates() = default;\n\nauto Templates::query(const QString &text) const -> std::vector<Question> {\n\tconst auto words = TextUtilities::PrepareSearchWords(text);\n\tconst auto questions = [&](const QString &word) {\n\t\tconst auto i = _index.first.find(word[0]);\n\t\treturn (i == end(_index.first)) ? 0 : i->second.size();\n\t};\n\tconst auto best = ranges::min_element(words, std::less<>(), questions);\n\tif (best == std::end(words)) {\n\t\treturn {};\n\t}\n\tconst auto narrowed = _index.first.find((*best)[0]);\n\tif (narrowed == end(_index.first)) {\n\t\treturn {};\n\t}\n\tusing Id = TemplatesIndex::Id;\n\tusing Term = TemplatesIndex::Term;\n\tconst auto questionById = [&](const Id &id) {\n\t\treturn _data.files.at(id.first).questions.at(id.second);\n\t};\n\n\tconst auto computeWeight = [&](const Id &id) {\n\t\tauto result = 0;\n\t\tconst auto full = _index.full.find(id);\n\t\tfor (const auto &word : words) {\n\t\t\tconst auto from = ranges::lower_bound(\n\t\t\t\tfull->second,\n\t\t\t\tword,\n\t\t\t\tstd::less<>(),\n\t\t\t\t[](const Term &term) { return term.first; });\n\t\t\tconst auto till = std::find_if(\n\t\t\t\tfrom,\n\t\t\t\tend(full->second),\n\t\t\t\t[&](const Term &term) {\n\t\t\t\t\treturn !term.first.startsWith(word);\n\t\t\t\t});\n\t\t\tconst auto weight = std::max_element(\n\t\t\t\tfrom,\n\t\t\t\ttill,\n\t\t\t\t[](const Term &a, const Term &b) {\n\t\t\t\t\treturn a.second < b.second;\n\t\t\t\t});\n\t\t\tif (weight == till) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\tresult += weight->second * (weight->first == word ? 2 : 1);\n\t\t}\n\t\treturn result;\n\t};\n\tusing Pair = std::pair<Id, int>;\n\tconst auto pairById = [&](const Id &id) {\n\t\treturn std::make_pair(id, computeWeight(id));\n\t};\n\tconst auto sorter = [](const Pair &a, const Pair &b) {\n\t\t// weight DESC filename DESC question ASC\n\t\tif (a.second > b.second) {\n\t\t\treturn true;\n\t\t} else if (a.second < b.second) {\n\t\t\treturn false;\n\t\t} else if (a.first.first > b.first.first) {\n\t\t\treturn true;\n\t\t} else if (a.first.first < b.first.first) {\n\t\t\treturn false;\n\t\t} else {\n\t\t\treturn (a.first.second < b.first.second);\n\t\t}\n\t};\n\tconst auto good = narrowed->second | ranges::views::transform(\n\t\tpairById\n\t) | ranges::views::filter([](const Pair &pair) {\n\t\treturn pair.second > 0;\n\t}) | ranges::to_vector | ranges::actions::stable_sort(sorter);\n\treturn good | ranges::views::transform([&](const Pair &pair) {\n\t\treturn questionById(pair.first);\n\t}) | ranges::views::take(kQueryLimit) | ranges::to_vector;\n}\n\n} // namespace Support\n"
  },
  {
    "path": "Telegram/SourceFiles/support/support_templates.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/binary_guard.h\"\n\n#include <QtNetwork/QNetworkReply>\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Support {\nnamespace details {\n\nstruct TemplatesQuestion {\n\tQString question;\n\tQStringList originalKeys;\n\tQStringList normalizedKeys;\n\tQString value;\n};\n\nstruct TemplatesFile {\n\tQString url;\n\tstd::map<QString, TemplatesQuestion> questions;\n};\n\nstruct TemplatesData {\n\tstd::map<QString, TemplatesFile> files;\n};\n\nstruct TemplatesIndex {\n\tusing Id = std::pair<QString, QString>; // filename, normalized question\n\tusing Term = std::pair<QString, int>; // search term, weight\n\n\tstd::map<QChar, std::vector<Id>> first;\n\tstd::map<Id, std::vector<Term>> full;\n};\n\n} // namespace details\n\nclass Templates : public base::has_weak_ptr {\npublic:\n\texplicit Templates(not_null<Main::Session*> session);\n\n\tvoid reload();\n\n\tusing Question = details::TemplatesQuestion;\n\tstd::vector<Question> query(const QString &text) const;\n\n\tauto errors() const {\n\t\treturn _errors.events();\n\t}\n\n\tstruct QuestionByKey {\n\t\tQuestion question;\n\t\tQString key;\n\t};\n\tstd::optional<QuestionByKey> matchExact(QString text) const;\n\tstd::optional<QuestionByKey> matchFromEnd(QString text) const;\n\tint maxKeyLength() const {\n\t\treturn _maxKeyLength;\n\t}\n\n\t~Templates();\n\nprivate:\n\tstruct Updates;\n\n\tvoid load();\n\tvoid update();\n\tvoid ensureUpdatesCreated();\n\tvoid updateRequestFinished(QNetworkReply *reply);\n\tvoid checkUpdateFinished();\n\tvoid setData(details::TemplatesData &&data);\n\n\tnot_null<Main::Session*> _session;\n\n\tdetails::TemplatesData _data;\n\tdetails::TemplatesIndex _index;\n\trpl::event_stream<QStringList> _errors;\n\tbase::binary_guard _reading;\n\tbool _reloadAfterRead = false;\n\trpl::lifetime _reloadToastSubscription;\n\n\tint _maxKeyLength = 0;\n\n\tstd::unique_ptr<Updates> _updates;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Support\n"
  },
  {
    "path": "Telegram/SourceFiles/tde2e/tde2e_api.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"tde2e/tde2e_api.h\"\n\n#include \"base/assertion.h\"\n#include \"base/debug_log.h\"\n\n#include <td/e2e/e2e_api.h>\n\n#define LOG_ERROR(error) \\\n\tLOG((\"TdE2E Error %1: %2\").arg(int(error.code)).arg(error.message.c_str()))\n\n#define LOG_AND_FAIL(error, reason) \\\n\tLOG_ERROR(error); \\\n\tfail(reason)\n\n#define LOG_APPLY(subchain, slice) \\\n\tDEBUG_LOG((\"TdE2E Apply[%1]: %2\").arg(subchain).arg(WrapForLog(slice)))\n\nnamespace TdE2E {\nnamespace {\n\nconstexpr auto kPermissionAdd = 1;\nconstexpr auto kPermissionRemove = 2;\nconstexpr auto kShortPollChainBlocksTimeout = 5 * crl::time(1000);\nconstexpr auto kShortPollChainBlocksWaitFor = crl::time(1000);\n\n[[nodiscard]] tde2e_api::Slice Slice(const QByteArray &data) {\n\treturn {\n\t\tdata.constData(),\n\t\tstd::string_view::size_type(data.size())\n\t};\n}\n\n[[nodiscard]] tde2e_api::Slice Slice(const std::vector<uint8_t> &data) {\n\treturn {\n\t\treinterpret_cast<const char*>(data.data()),\n\t\tstd::string_view::size_type(data.size()),\n\t};\n}\n\n[[nodiscard]] ParticipantsSet ParseParticipantsSet(\n\t\tconst tde2e_api::CallState &state) {\n\tauto result = ParticipantsSet();\n\tconst auto &list = state.participants;\n\tresult.list.reserve(list.size());\n\tfor (const auto &entry : list) {\n\t\tresult.list.emplace(UserId{ uint64(entry.user_id) });\n\t}\n\treturn result;\n}\n\n[[nodiscard]] QString WrapForLog(std::string_view v) {\n\tauto result = QString();\n\tconst auto count = std::min(int(v.size()), 16);\n\tresult.reserve(count * 3 + 2);\n\tfor (auto i = 0; i != count; ++i) {\n\t\tconst auto byte = uint8(v[i]);\n\t\tresult += QString::number(byte, 16).rightJustified(2, '0');\n\t\tif (i + 1 != count) {\n\t\t\tresult += ' ';\n\t\t}\n\t}\n\tif (v.size() > count) {\n\t\tresult += \"...\";\n\t}\n\treturn result.toUpper();\n}\n\n} // namespace\n\nauto EncryptDecrypt::callback()\n-> Fn<EncryptionBuffer(const EncryptionBuffer&, int64_t, bool, int32_t)> {\n\treturn [that = shared_from_this()](\n\t\t\tconst EncryptionBuffer &data,\n\t\t\tint64_t userId,\n\t\t\tbool encrypt,\n\t\t\tint32_t unencryptedPrefixSize) -> EncryptionBuffer {\n\t\tconst auto libId = that->_id.load();\n\t\tif (!libId) {\n\t\t\treturn {};\n\t\t}\n\t\tconst auto channelId = tde2e_api::CallChannelId(0);\n\t\tconst auto slice = Slice(data);\n\t\tconst auto result = encrypt\n\t\t\t? tde2e_api::call_encrypt(\n\t\t\t\tlibId,\n\t\t\t\tchannelId,\n\t\t\t\tslice,\n\t\t\t\tsize_t(unencryptedPrefixSize))\n\t\t\t: tde2e_api::call_decrypt(libId, userId, channelId, slice);\n\t\tif (!result.is_ok()) {\n\t\t\treturn {};\n\t\t}\n\t\tconst auto &value = result.value();\n\t\tconst auto start = reinterpret_cast<const uint8_t*>(value.data());\n\t\tconst auto end = start + value.size();\n\t\treturn { start, end };\n\t};\n}\n\nvoid EncryptDecrypt::setCallId(CallId id) {\n\tExpects(id.v != 0);\n\n\t_id.store(id.v);\n}\n\nvoid EncryptDecrypt::clearCallId(CallId fromId) {\n\tExpects(fromId.v != 0);\n\n\t_id.compare_exchange_strong(fromId.v, 0);\n}\n\nCall::Call(UserId myUserId)\n: _myUserId(myUserId) {\n\tconst auto id = tde2e_api::key_generate_temporary_private_key();\n\tAssert(id.is_ok());\n\t_myKeyId = { .v = uint64(id.value()) };\n\n\tconst auto key = tde2e_api::key_to_public_key(_myKeyId.v);\n\tAssert(key.is_ok());\n\tAssert(key.value().size() == sizeof(_myKey));\n\tmemcpy(&_myKey, key.value().data(), sizeof(_myKey));\n}\n\nCall::~Call() {\n\tif (const auto id = libId()) {\n\t\tif (const auto raw = _encryptDecrypt.get()) {\n\t\t\traw->clearCallId(_id);\n\t\t}\n\t\ttde2e_api::call_destroy(id);\n\t}\n}\n\nvoid Call::fail(CallFailure reason) {\n\t_emojiHash = QByteArray();\n\t_failure = reason;\n\t_failures.fire_copy(reason);\n}\n\nPublicKey Call::myKey() const {\n\treturn _myKey;\n}\n\nbool Call::hasLastBlock0() const {\n\treturn _lastBlock0.has_value();\n}\n\nvoid Call::refreshLastBlock0(std::optional<Block> block) {\n\t_lastBlock0 = std::move(block);\n}\n\nBlock Call::makeJoinBlock() {\n\tif (failed()) {\n\t\treturn {};\n\t}\n\n\tconst auto publicKeyView = std::string_view{\n\t\treinterpret_cast<const char*>(&_myKey),\n\t\tsizeof(_myKey),\n\t};\n\tconst auto publicKeyId = tde2e_api::key_from_public_key(publicKeyView);\n\tAssert(publicKeyId.is_ok());\n\n\tconst auto myKeyId = std::int64_t(_myKeyId.v);\n\tconst auto myParticipant = tde2e_api::CallParticipant{\n\t\t.user_id = std::int64_t(_myUserId.v),\n\t\t.public_key_id = publicKeyId.value(),\n\t\t.permissions = kPermissionAdd | kPermissionRemove,\n\t};\n\n\tconst auto result = _lastBlock0\n\t\t? tde2e_api::call_create_self_add_block(\n\t\t\tmyKeyId,\n\t\t\tSlice(_lastBlock0->data),\n\t\t\tmyParticipant)\n\t\t: tde2e_api::call_create_zero_block(myKeyId, {\n\t\t\t.height = 0,\n\t\t\t.participants = { myParticipant },\n\t\t});\n\tif (!result.is_ok()) {\n\t\tLOG_AND_FAIL(result.error(), CallFailure::Unknown);\n\t\treturn {};\n\t}\n\n\treturn {\n\t\t.data = QByteArray::fromStdString(result.value()),\n\t};\n}\n\nBlock Call::makeRemoveBlock(const base::flat_set<UserId> &ids) {\n\tif (failed() || !_id) {\n\t\treturn {};\n\t}\n\n\tauto state = tde2e_api::call_get_state(libId());\n\tif (!state.is_ok()) {\n\t\tLOG_AND_FAIL(state.error(), CallFailure::Unknown);\n\t\treturn {};\n\t}\n\tauto found = false;\n\tauto updated = state.value();\n\tauto &list = updated.participants;\n\tfor (auto i = begin(list); i != end(list);) {\n\t\tconst auto userId = UserId{ uint64(i->user_id) };\n\t\tif (userId != _myUserId && ids.contains(userId)) {\n\t\t\ti = list.erase(i);\n\t\t\tfound = true;\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tif (!found) {\n\t\treturn {};\n\t}\n\tconst auto result = tde2e_api::call_create_change_state_block(\n\t\tlibId(),\n\t\tupdated);\n\tif (!result.is_ok()) {\n\t\tLOG_AND_FAIL(result.error(), CallFailure::Unknown);\n\t\treturn {};\n\t}\n\treturn {\n\t\t.data = QByteArray::fromStdString(result.value()),\n\t};\n}\n\nrpl::producer<ParticipantsSet> Call::participantsSetValue() const {\n\treturn _participantsSet.value();\n}\n\nvoid Call::joined() {\n\tif (!_id) {\n\t\tLOG((\"TdE2E Error: Call::joined() without id.\"));\n\t\t_failure = CallFailure::Unknown;\n\t\treturn;\n\t}\n\tshortPoll(0);\n\tshortPoll(1);\n}\n\nvoid Call::apply(int subchain, const Block &last) {\n\tExpects(_id || !subchain);\n\n\tauto verification = std::optional<tde2e_api::CallVerificationState>();\n\tconst auto guard = gsl::finally([&] {\n\t\tif (failed() || !_id) {\n\t\t\treturn;\n\t\t} else if (!verification) {\n\t\t\tconst auto id = libId();\n\t\t\tauto result = tde2e_api::call_get_verification_state(id);\n\t\t\tif (!result.is_ok()) {\n\t\t\t\tLOG_AND_FAIL(result.error(), CallFailure::Unknown);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tverification = std::move(result.value());\n\t\t}\n\t\t_emojiHash = verification->emoji_hash.has_value()\n\t\t\t? QByteArray::fromStdString(*verification->emoji_hash)\n\t\t\t: QByteArray();\n\t\tcheckForOutboundMessages();\n\t});\n\n\tif (subchain) {\n\t\tLOG_APPLY(1, Slice(last.data));\n\t\tauto result = tde2e_api::call_receive_inbound_message(\n\t\t\tlibId(),\n\t\t\tSlice(last.data));\n\t\tif (!result.is_ok()) {\n\t\t\tLOG_AND_FAIL(result.error(), CallFailure::Unknown);\n\t\t} else {\n\t\t\tverification = std::move(result.value());\n\t\t}\n\t\treturn;\n\t} else if (_id) {\n\t\tLOG_APPLY(0, Slice(last.data));\n\t\tconst auto result = tde2e_api::call_apply_block(\n\t\t\tlibId(),\n\t\t\tSlice(last.data));\n\t\tif (!result.is_ok()) {\n\t\t\tLOG_AND_FAIL(result.error(), CallFailure::Unknown);\n\t\t} else {\n\t\t\t_participantsSet = ParseParticipantsSet(result.value());\n\t\t}\n\t\treturn;\n\t}\n\tLOG_APPLY(-1, Slice(last.data));\n\tconst auto id = tde2e_api::call_create(\n\t\tstd::int64_t(_myUserId.v),\n\t\tstd::int64_t(_myKeyId.v),\n\t\tSlice(last.data));\n\tif (!id.is_ok()) {\n\t\tLOG_AND_FAIL(id.error(), CallFailure::Unknown);\n\t\treturn;\n\t}\n\tsetId({ uint64(id.value()) });\n\n\tfor (auto i = 0; i != kSubChainsCount; ++i) {\n\t\tauto &entry = _subchains[i];\n\t\tentry.waitingTimer.setCallback([=] {\n\t\t\tcheckWaitingBlocks(i, true);\n\t\t});\n\t\tentry.shortPollTimer.setCallback([=] {\n\t\t\tshortPoll(i);\n\t\t});\n\t\tif (!entry.waitingTimer.isActive()) {\n\t\t\tentry.shortPollTimer.callOnce(kShortPollChainBlocksTimeout);\n\t\t}\n\t}\n\n\tconst auto state = tde2e_api::call_get_state(libId());\n\tif (!state.is_ok()) {\n\t\tLOG_AND_FAIL(state.error(), CallFailure::Unknown);\n\t\treturn;\n\t}\n\t_participantsSet = ParseParticipantsSet(state.value());\n}\n\nvoid Call::setId(CallId id) {\n\tExpects(!_id);\n\n\t_id = id;\n\tif (const auto raw = _encryptDecrypt.get()) {\n\t\traw->setCallId(id);\n\t}\n}\n\nvoid Call::checkForOutboundMessages() {\n\tExpects(_id);\n\n\tconst auto result = tde2e_api::call_pull_outbound_messages(libId());\n\tif (!result.is_ok()) {\n\t\tLOG_AND_FAIL(result.error(), CallFailure::Unknown);\n\t\treturn;\n\t} else if (!result.value().empty()) {\n\t\t_outboundBlocks.fire(\n\t\t\tQByteArray::fromStdString(result.value().back()));\n\t}\n}\n\nvoid Call::apply(\n\t\tint subchain,\n\t\tint indexAfterLast,\n\t\tconst std::vector<Block> &blocks,\n\t\tbool fromShortPoll) {\n\tExpects(subchain >= 0 && subchain < kSubChainsCount);\n\n\tif (!subchain && !blocks.empty() && indexAfterLast > _lastBlock0Height) {\n\t\t_lastBlock0 = blocks.back();\n\t\t_lastBlock0Height = indexAfterLast;\n\t}\n\n\tauto &entry = _subchains[subchain];\n\tif (fromShortPoll) {\n\t\tauto i = begin(entry.waiting);\n\t\twhile (i != end(entry.waiting) && i->first < indexAfterLast) {\n\t\t\t++i;\n\t\t}\n\t\tentry.waiting.erase(begin(entry.waiting), i);\n\n\t\tif (subchain && !_id && !blocks.empty()) {\n\t\t\tLOG((\"TdE2E Error: Broadcast shortpoll block without id.\"));\n\t\t\tfail(CallFailure::Unknown);\n\t\t\treturn;\n\t\t}\n\t} else {\n\t\tentry.lastUpdate = crl::now();\n\t}\n\tif (failed()) {\n\t\treturn;\n\t}\n\n\tauto index = indexAfterLast - int(blocks.size());\n\tif (!fromShortPoll && (index > entry.height || (!_id && subchain))) {\n\t\tfor (const auto &block : blocks) {\n\t\t\tentry.waiting.emplace(index++, block);\n\t\t}\n\t} else {\n\t\tfor (const auto &block : blocks) {\n\t\t\tif (!_id || (entry.height == index)) {\n\t\t\t\tapply(subchain, block);\n\t\t\t}\n\t\t\tentry.height = std::max(entry.height, ++index);\n\t\t}\n\t\tentry.height = std::max(entry.height, indexAfterLast);\n\t}\n\tcheckWaitingBlocks(subchain);\n}\n\nvoid Call::checkWaitingBlocks(int subchain, bool waited) {\n\tExpects(subchain >= 0 && subchain < kSubChainsCount);\n\n\tif (failed()) {\n\t\treturn;\n\t}\n\n\tauto &entry = _subchains[subchain];\n\tif (!_id) {\n\t\tentry.waitingTimer.callOnce(kShortPollChainBlocksWaitFor);\n\t\treturn;\n\t} else if (entry.shortPolling) {\n\t\treturn;\n\t}\n\tauto &waiting = entry.waiting;\n\tentry.shortPollTimer.cancel();\n\twhile (!waiting.empty()) {\n\t\tconst auto index = waiting.begin()->first;\n\t\tif (index > entry.height) {\n\t\t\tif (waited) {\n\t\t\t\tshortPoll(subchain);\n\t\t\t} else {\n\t\t\t\tentry.waitingTimer.callOnce(kShortPollChainBlocksWaitFor);\n\t\t\t}\n\t\t\treturn;\n\t\t} else if (index == entry.height) {\n\t\t\tconst auto slice = Slice(waiting.begin()->second.data);\n\t\t\tif (subchain) {\n\t\t\t\tLOG_APPLY(1, slice);\n\t\t\t\tauto result = tde2e_api::call_receive_inbound_message(\n\t\t\t\t\tlibId(),\n\t\t\t\t\tslice);\n\t\t\t\tif (!result.is_ok()) {\n\t\t\t\t\tLOG_AND_FAIL(result.error(), CallFailure::Unknown);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t_emojiHash = result.value().emoji_hash.has_value()\n\t\t\t\t\t? QByteArray::fromStdString(*result.value().emoji_hash)\n\t\t\t\t\t: QByteArray();\n\t\t\t\tcheckForOutboundMessages();\n\t\t\t} else {\n\t\t\t\tLOG_APPLY(0, slice);\n\t\t\t\tconst auto result = tde2e_api::call_apply_block(\n\t\t\t\t\tlibId(),\n\t\t\t\t\tslice);\n\t\t\t\tif (!result.is_ok()) {\n\t\t\t\t\tLOG_AND_FAIL(result.error(), CallFailure::Unknown);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t_participantsSet = ParseParticipantsSet(result.value());\n\t\t\t}\n\t\t\tentry.height = std::max(entry.height, index + 1);\n\t\t}\n\t\twaiting.erase(waiting.begin());\n\t}\n\tentry.waitingTimer.cancel();\n\tentry.shortPollTimer.callOnce(kShortPollChainBlocksTimeout);\n}\n\nvoid Call::shortPoll(int subchain) {\n\tExpects(subchain >= 0 && subchain < kSubChainsCount);\n\n\tauto &entry = _subchains[subchain];\n\tentry.waitingTimer.cancel();\n\tentry.shortPollTimer.cancel();\n\tif (subchain && !_id) {\n\t\t// Not ready.\n\t\tentry.waitingTimer.callOnce(kShortPollChainBlocksWaitFor);\n\t\treturn;\n\t}\n\tentry.shortPolling = true;\n\t_subchainRequests.fire({ subchain, entry.height });\n}\n\nstd::int64_t Call::libId() const {\n\treturn std::int64_t(_id.v);\n}\n\nrpl::producer<Call::SubchainRequest> Call::subchainRequests() const {\n\treturn _subchainRequests.events();\n}\n\nvoid Call::subchainBlocksRequestFinished(int subchain) {\n\tExpects(subchain >= 0 && subchain < kSubChainsCount);\n\n\tif (failed()) {\n\t\treturn;\n\t}\n\n\tauto &entry = _subchains[subchain];\n\tentry.shortPolling = false;\n\tcheckWaitingBlocks(subchain);\n}\n\nrpl::producer<QByteArray> Call::sendOutboundBlock() const {\n\treturn _outboundBlocks.events();\n}\n\nstd::optional<CallFailure> Call::failed() const {\n\treturn _failure;\n}\n\nrpl::producer<CallFailure> Call::failures() const {\n\tif (_failure) {\n\t\treturn rpl::single(*_failure);\n\t}\n\treturn _failures.events();\n}\n\nQByteArray Call::emojiHash() const {\n\treturn _emojiHash.current();\n}\n\nrpl::producer<QByteArray> Call::emojiHashValue() const {\n\treturn _emojiHash.value();\n}\n\nvoid Call::registerEncryptDecrypt(std::shared_ptr<EncryptDecrypt> object) {\n\tExpects(object != nullptr);\n\tExpects(_encryptDecrypt == nullptr);\n\n\t_encryptDecrypt = std::move(object);\n\tif (_id) {\n\t\t_encryptDecrypt->setCallId(_id);\n\t}\n}\n\nrpl::lifetime &Call::lifetime() {\n\treturn _lifetime;\n}\n\n} // namespace TdE2E\n"
  },
  {
    "path": "Telegram/SourceFiles/tde2e/tde2e_api.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/basic_types.h\"\n#include \"base/flat_set.h\"\n#include \"base/timer.h\"\n\n#include <rpl/event_stream.h>\n#include <rpl/lifetime.h>\n#include <rpl/producer.h>\n#include <rpl/variable.h>\n\n#include <crl/crl_time.h>\n\nnamespace TdE2E {\n\nstruct UserId {\n\tuint64 v = 0;\n\n\tfriend inline constexpr auto operator<=>(UserId, UserId) = default;\n\tfriend inline constexpr bool operator==(UserId, UserId) = default;\n};\n\nstruct PrivateKeyId {\n\tuint64 v = 0;\n};\n\nstruct CallId {\n\tuint64 v = 0;\n\n\texplicit operator bool() const {\n\t\treturn v != 0;\n\t}\n};\n\nstruct PublicKey {\n\tuint64 a = 0;\n\tuint64 b = 0;\n\tuint64 c = 0;\n\tuint64 d = 0;\n};\n\nstruct ParticipantState {\n\tUserId id;\n\tPublicKey key;\n};\n\nstruct ParticipantsSet {\n\tbase::flat_set<UserId> list;\n\n\tfriend inline bool operator==(\n\t\tconst ParticipantsSet &,\n\t\tconst ParticipantsSet &) = default;\n};\n\nstruct Block {\n\tQByteArray data;\n};\n\nenum class CallFailure {\n\tUnknown,\n};\n\nusing EncryptionBuffer = std::vector<uint8_t>;\n\nclass EncryptDecrypt final\n\t: public std::enable_shared_from_this<EncryptDecrypt> {\npublic:\n\t[[nodiscard]] auto callback()\n\t-> Fn<EncryptionBuffer(const EncryptionBuffer&, int64_t, bool, int32_t)>;\n\n\tvoid setCallId(CallId id);\n\tvoid clearCallId(CallId fromId);\n\nprivate:\n\tstd::atomic<uint64> _id = 0;\n\n};\n\nclass Call final {\npublic:\n\texplicit Call(UserId myUserId);\n\t~Call();\n\n\t[[nodiscard]] PublicKey myKey() const;\n\n\tvoid joined();\n\tvoid apply(\n\t\tint subchain,\n\t\tint indexAfterLast,\n\t\tconst std::vector<Block> &blocks,\n\t\tbool fromShortPoll);\n\n\tstruct SubchainRequest {\n\t\tint subchain = 0;\n\t\tint height = 0;\n\t};\n\t[[nodiscard]] rpl::producer<SubchainRequest> subchainRequests() const;\n\tvoid subchainBlocksRequestFinished(int subchain);\n\n\t[[nodiscard]] rpl::producer<QByteArray> sendOutboundBlock() const;\n\n\t[[nodiscard]] std::optional<CallFailure> failed() const;\n\t[[nodiscard]] rpl::producer<CallFailure> failures() const;\n\n\t[[nodiscard]] QByteArray emojiHash() const;\n\t[[nodiscard]] rpl::producer<QByteArray> emojiHashValue() const;\n\n\t[[nodiscard]] bool hasLastBlock0() const;\n\tvoid refreshLastBlock0(std::optional<Block> block);\n\t[[nodiscard]] Block makeJoinBlock();\n\t[[nodiscard]] Block makeRemoveBlock(const base::flat_set<UserId> &ids);\n\n\t[[nodiscard]] auto participantsSetValue() const\n\t\t-> rpl::producer<ParticipantsSet>;\n\n\tvoid registerEncryptDecrypt(std::shared_ptr<EncryptDecrypt> object);\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tstatic constexpr int kSubChainsCount = 2;\n\n\tstruct SubChainState {\n\t\tbase::Timer shortPollTimer;\n\t\tbase::Timer waitingTimer;\n\t\tcrl::time lastUpdate = 0;\n\t\tbase::flat_map<int, Block> waiting;\n\t\tbool shortPolling = true;\n\t\tint height = 0;\n\t};\n\n\tvoid setId(CallId id);\n\tvoid apply(int subchain, const Block &last);\n\tvoid fail(CallFailure reason);\n\n\tvoid checkForOutboundMessages();\n\tvoid checkWaitingBlocks(int subchain, bool waited = false);\n\tvoid shortPoll(int subchain);\n\n\t[[nodiscard]] std::int64_t libId() const;\n\n\tCallId _id;\n\tUserId _myUserId;\n\tPrivateKeyId _myKeyId;\n\tPublicKey _myKey;\n\tstd::optional<CallFailure> _failure;\n\trpl::event_stream<CallFailure> _failures;\n\tstd::shared_ptr<EncryptDecrypt> _encryptDecrypt;\n\n\tSubChainState _subchains[kSubChainsCount];\n\trpl::event_stream<SubchainRequest> _subchainRequests;\n\trpl::event_stream<QByteArray> _outboundBlocks;\n\n\tstd::optional<Block> _lastBlock0;\n\tint _lastBlock0Height = 0;\n\n\trpl::variable<ParticipantsSet> _participantsSet;\n\trpl::variable<QByteArray> _emojiHash;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace TdE2E\n"
  },
  {
    "path": "Telegram/SourceFiles/tde2e/tde2e_integration.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"tde2e/tde2e_integration.h\"\n\n#include \"data/data_user.h\"\n#include \"tde2e/tde2e_api.h\"\n\nnamespace TdE2E {\n\nUserId MakeUserId(not_null<UserData*> user) {\n\treturn MakeUserId(peerToUser(user->id));\n}\n\nUserId MakeUserId(::UserId id) {\n\treturn { .v = id.bare };\n}\n\nMTPint256 PublicKeyToMTP(const PublicKey &key) {\n\treturn MTP_int256(MTP_int128(key.a, key.b), MTP_int128(key.c, key.d));\n}\n\n} // namespace TdE2E\n"
  },
  {
    "path": "Telegram/SourceFiles/tde2e/tde2e_integration.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace TdE2E {\n\nstruct UserId;\nstruct PublicKey;\n\n[[nodiscard]] UserId MakeUserId(not_null<UserData*> user);\n[[nodiscard]] UserId MakeUserId(::UserId id);\n\n[[nodiscard]] MTPint256 PublicKeyToMTP(const PublicKey &key);\n\n} // namespace TdE2E\n"
  },
  {
    "path": "Telegram/SourceFiles/tests/test_main.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"tests/test_main.h\"\n\n#include \"base/invoke_queued.h\"\n#include \"base/integration.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/widgets/rp_window.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/painter.h\"\n\n#include <QApplication>\n#include <QAbstractNativeEventFilter>\n#include <QScreen>\n#include <QThread>\n#include <QDir>\n\n#include <qpa/qplatformscreen.h>\n\nnamespace Test {\n\nbool App::notifyOrInvoke(QObject *receiver, QEvent *e) {\n\tif (e->type() == base::InvokeQueuedEvent::Type()) {\n\t\tstatic_cast<base::InvokeQueuedEvent*>(e)->invoke();\n\t\treturn true;\n\t}\n\treturn QApplication::notify(receiver, e);\n}\n\nbool App::nativeEventFilter(\n\t\tconst QByteArray &eventType,\n\t\tvoid *message,\n\t\tnative_event_filter_result *result) {\n\tregisterEnterFromEventLoop();\n\treturn false;\n}\n\nvoid App::checkForEmptyLoopNestingLevel() {\n\t// _loopNestingLevel == _eventNestingLevel means that we had a\n\t// native event in a nesting loop that didn't get a notify() call\n\t// after. That means we already have exited the nesting loop and\n\t// there must not be any postponed calls with that nesting level.\n\tif (_loopNestingLevel == _eventNestingLevel) {\n\t\tAssert(_postponedCalls.empty()\n\t\t\t|| _postponedCalls.back().loopNestingLevel < _loopNestingLevel);\n\t\tAssert(!_previousLoopNestingLevels.empty());\n\n\t\t_loopNestingLevel = _previousLoopNestingLevels.back();\n\t\t_previousLoopNestingLevels.pop_back();\n\t}\n}\n\nvoid App::postponeCall(FnMut<void()> &&callable) {\n\tExpects(callable != nullptr);\n\tExpects(_eventNestingLevel >= _loopNestingLevel);\n\n\tcheckForEmptyLoopNestingLevel();\n\t_postponedCalls.push_back({\n\t\t_loopNestingLevel,\n\t\tstd::move(callable)\n\t\t});\n}\n\nvoid App::processPostponedCalls(int level) {\n\twhile (!_postponedCalls.empty()) {\n\t\tauto &last = _postponedCalls.back();\n\t\tif (last.loopNestingLevel != level) {\n\t\t\tbreak;\n\t\t}\n\t\tauto taken = std::move(last);\n\t\t_postponedCalls.pop_back();\n\t\ttaken.callable();\n\t}\n}\n\nvoid App::incrementEventNestingLevel() {\n\t++_eventNestingLevel;\n}\n\nvoid App::decrementEventNestingLevel() {\n\tExpects(_eventNestingLevel >= _loopNestingLevel);\n\n\tif (_eventNestingLevel == _loopNestingLevel) {\n\t\t_loopNestingLevel = _previousLoopNestingLevels.back();\n\t\t_previousLoopNestingLevels.pop_back();\n\t}\n\tconst auto processTillLevel = _eventNestingLevel - 1;\n\tprocessPostponedCalls(processTillLevel);\n\tcheckForEmptyLoopNestingLevel();\n\t_eventNestingLevel = processTillLevel;\n\n\tEnsures(_eventNestingLevel >= _loopNestingLevel);\n}\n\nvoid App::registerEnterFromEventLoop() {\n\tExpects(_eventNestingLevel >= _loopNestingLevel);\n\n\tif (_eventNestingLevel > _loopNestingLevel) {\n\t\t_previousLoopNestingLevels.push_back(_loopNestingLevel);\n\t\t_loopNestingLevel = _eventNestingLevel;\n\t}\n}\n\nbool App::notify(QObject *receiver, QEvent *e) {\n\tif (QThread::currentThreadId() != _mainThreadId) {\n\t\treturn notifyOrInvoke(receiver, e);\n\t}\n\n\tconst auto wrap = createEventNestingLevel();\n\tif (e->type() == QEvent::UpdateRequest) {\n\t\tconst auto weak = QPointer<QObject>(receiver);\n\t\t_widgetUpdateRequests.fire({});\n\t\tif (!weak) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn notifyOrInvoke(receiver, e);\n}\n\nrpl::producer<> App::widgetUpdateRequests() const {\n\treturn _widgetUpdateRequests.events();\n}\n\nvoid BaseIntegration::enterFromEventLoop(FnMut<void()> &&method) {\n\tapp().customEnterFromEventLoop(std::move(method));\n}\n\nbool BaseIntegration::logSkipDebug() {\n\treturn true;\n}\n\nvoid BaseIntegration::logMessageDebug(const QString &message) {\n}\n\nvoid BaseIntegration::logMessage(const QString &message) {\n}\n\nvoid UiIntegration::postponeCall(FnMut<void()> &&callable) {\n\tapp().postponeCall(std::move(callable));\n}\n\nvoid UiIntegration::registerLeaveSubscription(not_null<QWidget*> widget) {\n}\n\nvoid UiIntegration::unregisterLeaveSubscription(not_null<QWidget*> widget) {\n}\n\nQString UiIntegration::emojiCacheFolder() {\n\treturn QDir().currentPath() + \"/tests/\" + name() + \"/emoji\";\n}\n\nQString UiIntegration::openglCheckFilePath() {\n\treturn QDir().currentPath() + \"/tests/\" + name() + \"/opengl\";\n}\n\nQString UiIntegration::angleBackendFilePath() {\n\treturn QDir().currentPath() + \"/test/\" + name() + \"/angle\";\n}\n\n} // namespace Test\n\nint main(int argc, char *argv[]) {\n\tusing namespace Test;\n\n\tauto app = App(argc, argv);\n\tapp.installNativeEventFilter(&app);\n\n\tconst auto ratio = app.devicePixelRatio();\n\tconst auto useRatio = std::clamp(qCeil(ratio), 1, 3);\n\tstyle::SetDevicePixelRatio(useRatio);\n\n\tconst auto screen = App::primaryScreen();\n\tconst auto dpi = screen->logicalDotsPerInch();\n\tconst auto basePair = screen->handle()->logicalBaseDpi();\n\tconst auto baseMiddle = (basePair.first + basePair.second) * 0.5;\n\tconst auto screenExact = dpi / baseMiddle;\n\tconst auto screenScale = int(base::SafeRound(screenExact * 20)) * 5;\n\tconst auto chosen = std::clamp(\n\t\tscreenScale,\n\t\tstyle::kScaleMin,\n\t\tstyle::MaxScaleForRatio(useRatio));\n\n\tBaseIntegration base(argc, argv);\n\tbase::Integration::Set(&base);\n\n\tUiIntegration ui;\n\tUi::Integration::Set(&ui);\n\n\tInvokeQueued(&app, [=] {\n\t\tnew Ui::Animations::Manager();\n\t\tstyle::StartManager(chosen);\n\n\t\tUi::Emoji::Init();\n\n\t\tconst auto window = new Ui::RpWindow();\n\t\twindow->setGeometry(\n\t\t\t{ scale(100), scale(100), scale(800), scale(600) });\n\t\twindow->show();\n\n\t\twindow->setMinimumSize({ scale(240), scale(320) });\n\n\t\ttest(window, window->body());\n\t});\n\n\treturn app.exec();\n}\n\nnamespace crl {\n\nrpl::producer<> on_main_update_requests() {\n\treturn Test::app().widgetUpdateRequests();\n}\n\n} // namespace crl\n"
  },
  {
    "path": "Telegram/SourceFiles/tests/test_main.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/basic_types.h\"\n#include \"base/integration.h\"\n#include \"ui/style/style_core_scale.h\"\n#include \"ui/integration.h\"\n\n#include <crl/crl.h>\n#include <rpl/rpl.h>\n\n#include <QApplication>\n#include <QAbstractNativeEventFilter>\n#include <QThread>\n#include <QDir>\n\nnamespace Ui {\nclass RpWidget;\nclass RpWindow;\n} // namespace Ui\n\nnamespace Test {\n\n[[nodiscard]] QString name();\n\nvoid test(not_null<Ui::RpWindow*> window, not_null<Ui::RpWidget*> widget);\n\n[[nodiscard]] inline int scale(int value) {\n\treturn style::ConvertScale(value);\n};\n\nclass App final : public QApplication, public QAbstractNativeEventFilter {\npublic:\n\tusing QApplication::QApplication;\n\n\ttemplate <typename Callable>\n\tauto customEnterFromEventLoop(Callable &&callable) {\n\t\tregisterEnterFromEventLoop();\n\t\tconst auto wrap = createEventNestingLevel();\n\t\treturn callable();\n\t}\n\n\tvoid postponeCall(FnMut<void()> &&callable);\n\n\t[[nodiscard]] rpl::producer<> widgetUpdateRequests() const;\n\nprivate:\n\tstruct PostponedCall {\n\t\tint loopNestingLevel = 0;\n\t\tFnMut<void()> callable;\n\t};\n\n\tauto createEventNestingLevel() {\n\t\tincrementEventNestingLevel();\n\t\treturn gsl::finally([=] { decrementEventNestingLevel(); });\n\t}\n\n\tvoid checkForEmptyLoopNestingLevel();\n\tvoid processPostponedCalls(int level);\n\tvoid incrementEventNestingLevel();\n\tvoid decrementEventNestingLevel();\n\tvoid registerEnterFromEventLoop();\n\n\tbool notifyOrInvoke(QObject *receiver, QEvent *e);\n\tbool notify(QObject *receiver, QEvent *e) override;\n\tbool nativeEventFilter(\n\t\tconst QByteArray &eventType,\n\t\tvoid *message,\n\t\tnative_event_filter_result *result) override;\n\n\trpl::event_stream<> _widgetUpdateRequests;\n\tQt::HANDLE _mainThreadId = QThread::currentThreadId();\n\tint _eventNestingLevel = 0;\n\tint _loopNestingLevel = 0;\n\tstd::vector<int> _previousLoopNestingLevels;\n\tstd::vector<PostponedCall> _postponedCalls;\n\n};\n\n[[nodiscard]] inline App &app() {\n\treturn *static_cast<App*>(QCoreApplication::instance());\n}\n\nclass BaseIntegration final : public base::Integration {\npublic:\n\tusing Integration::Integration;\n\n\tvoid enterFromEventLoop(FnMut<void()> &&method);\n\tbool logSkipDebug();\n\tvoid logMessageDebug(const QString &message);\n\tvoid logMessage(const QString &message);\n};\n\nclass UiIntegration final : public Ui::Integration {\npublic:\n\tvoid postponeCall(FnMut<void()> &&callable);\n\tvoid registerLeaveSubscription(not_null<QWidget*> widget);\n\tvoid unregisterLeaveSubscription(not_null<QWidget*> widget);\n\tQString emojiCacheFolder();\n\tQString openglCheckFilePath();\n\tQString angleBackendFilePath();\n};\n\n} // namespace Test\n"
  },
  {
    "path": "Telegram/SourceFiles/tests/test_text.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"tests/test_main.h\"\n\n#include \"base/invoke_queued.h\"\n#include \"base/integration.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/text/text.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/rp_window.h\"\n#include \"ui/painter.h\"\n\n#include <QApplication>\n#include <QAbstractNativeEventFilter>\n#include <QThread>\n#include <QDir>\n\nnamespace Test {\n\nQString name() {\n\treturn u\"text\"_q;\n}\n\nvoid test(not_null<Ui::RpWindow*> window, not_null<Ui::RpWidget*> body) {\n\tauto text = new Ui::Text::String(scale(64));\n\n\tconst auto like = QString::fromUtf8(\"\\xf0\\x9f\\x91\\x8d\");\n\tconst auto dislike = QString::fromUtf8(\"\\xf0\\x9f\\x91\\x8e\");\n\tconst auto hebrew = QString() + QChar(1506) + QChar(1460) + QChar(1489);\n\n\tauto data = TextWithEntities();\n\tdata.append(\n\t\tu\"Lorem ipsu7m dolor sit amet, \"_q\n\t).append(Ui::Text::Bold(\n\t\tu\"consectetur adipiscing: \"_q\n\t\t+ hebrew\n\t\t+ u\" elit, sed do eiusmod tempor incididunt test\"_q\n\t)).append(Ui::Text::Wrapped(Ui::Text::Bold(\n\t\tu\". ut labore et dolore magna aliqua.\"_q\n\t\t+ like\n\t\t+ dislike\n\t\t+ u\"Ut enim ad minim veniam\"_q\n\t), EntityType::Italic)).append(\n\t\tu\", quis nostrud exercitation ullamco laboris nisi ut aliquip ex \\\nea commodo consequat. Duis aute irure dolor in reprehenderit in \\\nvoluptate velit esse cillum dolore eu fugiat nulla pariatur. \\\nExcepteur sint occaecat cupidatat non proident, sunt in culpa \\\nqui officia deserunt mollit anim id est laborum.\"_q\n).append(u\"\\n\\n\"_q).append(hebrew).append(\"\\n\\n\").append(\n\t\t\"Duisauteiruredolorinreprehenderitinvoluptatevelitessecillumdoloreeu\\\nfugiatnullapariaturExcepteursintoccaecatcupidatatnonproident, sunt in culpa \\\nqui officia deserunt mollit anim id est laborum. \\\nDuisauteiruredolorinreprehenderitinvoluptate.\");\n\tdata.append(data);\n\t//data.append(\"hi\\n\\nguys\");\n\ttext->setMarkedText(st::defaultTextStyle, data);\n\n\tbody->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(body);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tconst auto width = body->width();\n\t\tconst auto height = body->height();\n\n\t\tp.fillRect(clip, QColor(255, 255, 255));\n\n\t\tconst auto border = QColor(0, 128, 0, 16);\n\t\tauto skip = scale(20);\n\t\tp.fillRect(0, 0, skip, height, border);\n\t\tp.fillRect(skip, 0, width - skip, skip, border);\n\t\tp.fillRect(skip, height - skip, width - skip, skip, border);\n\t\tp.fillRect(width - skip, skip, skip, height - skip * 2, border);\n\n\t\tconst auto inner = body->rect().marginsRemoved(\n\t\t\t{ skip, skip, skip, skip });\n\n\t\tp.fillRect(QRect{\n\t\t\tinner.x(),\n\t\t\tinner.y(),\n\t\t\tinner.width(),\n\t\t\ttext->countHeight(inner.width())\n\t\t}, QColor(128, 0, 0, 16));\n\n\t\tauto widths = text->countLineWidths(inner.width());\n\t\tauto top = 0;\n\t\tfor (const auto width : widths) {\n\t\t\tp.fillRect(QRect{\n\t\t\t\tinner.x(),\n\t\t\t\tinner.y() + top,\n\t\t\t\twidth,\n\t\t\t\tst::defaultTextStyle.font->height\n\t\t\t}, QColor(0, 0, 128, 16));\n\t\t\ttop += st::defaultTextStyle.font->height;\n\t\t}\n\n\t\ttext->draw(p, {\n\t\t\t.position = inner.topLeft(),\n\t\t\t.availableWidth = inner.width(),\n\t\t});\n\n\t\t//const auto to = QRectF(\n\t\t//\tinner.marginsRemoved({ 0, inner.height() / 2, 0, 0 }));\n\t\t//const auto t = u\"hi\\n\\nguys\"_q;\n\t\t//p.drawText(to, t);\n\t}, body->lifetime());\n}\n\n} // namespace Test\n"
  },
  {
    "path": "Telegram/SourceFiles/tray.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"tray.h\"\n#include \"tray_accounts_menu.h\"\n\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"platform/platform_notifications_manager.h\"\n#include \"platform/platform_specific.h\"\n#include \"lang/lang_keys.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace Core {\n\nTray::Tray() {\n}\n\nvoid Tray::create() {\n\trebuildMenu();\n\tusing WorkMode = Settings::WorkMode;\n\tif (Core::App().settings().workMode() != WorkMode::WindowOnly) {\n\t\t_tray.createIcon();\n\t}\n\n\tCore::App().settings().workModeValue(\n\t) | rpl::combine_previous(\n\t) | rpl::on_next([=](WorkMode previous, WorkMode state) {\n\t\tconst auto wasHasIcon = (previous != WorkMode::WindowOnly);\n\t\tconst auto nowHasIcon = (state != WorkMode::WindowOnly);\n\t\tif (wasHasIcon != nowHasIcon) {\n\t\t\tif (nowHasIcon) {\n\t\t\t\t_tray.createIcon();\n\t\t\t} else {\n\t\t\t\t_tray.destroyIcon();\n\t\t\t}\n\t\t}\n\t}, _tray.lifetime());\n\n\tCore::App().settings().trayIconMonochromeChanges(\n\t) | rpl::on_next([=] {\n\t\tupdateIconCounters();\n\t}, _tray.lifetime());\n\n\tCore::App().passcodeLockChanges(\n\t) | rpl::on_next([=] {\n\t\trebuildMenu();\n\t}, _tray.lifetime());\n\n\tTrayAccountsMenu::SetupChangesSubscription(\n\t\t[=] { rebuildMenu(); },\n\t\t_tray.lifetime());\n\n\t_tray.iconClicks(\n\t) | rpl::on_next([=] {\n\t\tconst auto skipTrayClick = (_lastTrayClickTime > 0)\n\t\t\t&& (crl::now() - _lastTrayClickTime\n\t\t\t\t< QApplication::doubleClickInterval());\n\t\tif (!skipTrayClick) {\n\t\t\t_activeForTrayIconAction = Core::App().isActiveForTrayMenu();\n\t\t\t_minimizeMenuItemClicks.fire({});\n\t\t\t_lastTrayClickTime = crl::now();\n\t\t}\n\t}, _tray.lifetime());\n}\n\nvoid Tray::rebuildMenu() {\n\t_tray.destroyMenu();\n\t_tray.createMenu();\n\n\t{\n\t\tauto minimizeText = _textUpdates.events(\n\t\t) | rpl::map([=] {\n\t\t\t_activeForTrayIconAction = Core::App().isActiveForTrayMenu();\n\t\t\treturn _activeForTrayIconAction\n\t\t\t\t? tr::lng_minimize_to_tray(tr::now)\n\t\t\t\t: tr::lng_open_from_tray(tr::now);\n\t\t});\n\n\t\t_tray.addAction(\n\t\t\tstd::move(minimizeText),\n\t\t\t[=] { _minimizeMenuItemClicks.fire({}); });\n\t}\n\n\tif (!Core::App().passcodeLocked()) {\n\t\tauto notificationsText = _textUpdates.events(\n\t\t) | rpl::map([=] {\n\t\t\treturn Core::App().settings().desktopNotify()\n\t\t\t\t? tr::lng_disable_notifications_from_tray(tr::now)\n\t\t\t\t: tr::lng_enable_notifications_from_tray(tr::now);\n\t\t});\n\n\t\t_tray.addAction(\n\t\t\tstd::move(notificationsText),\n\t\t\t[=] { toggleSoundNotifications(); });\n\t}\n\n\t_tray.addAction(tr::lng_quit_from_tray(), [] { Core::Quit(); });\n\n\tTrayAccountsMenu::Fill(_tray);\n\n\tupdateMenuText();\n}\n\nvoid Tray::updateMenuText() {\n\t_textUpdates.fire({});\n}\n\nvoid Tray::updateIconCounters() {\n\t_tray.updateIcon();\n}\n\nrpl::producer<> Tray::aboutToShowRequests() const {\n\treturn _tray.aboutToShowRequests();\n}\n\nrpl::producer<> Tray::showFromTrayRequests() const {\n\treturn rpl::merge(\n\t\t_tray.showFromTrayRequests(),\n\t\t_minimizeMenuItemClicks.events() | rpl::filter([=] {\n\t\t\treturn !_activeForTrayIconAction;\n\t\t})\n\t);\n}\n\nrpl::producer<> Tray::hideToTrayRequests() const {\n\tauto triggers = rpl::merge(\n\t\t_tray.hideToTrayRequests(),\n\t\t_minimizeMenuItemClicks.events() | rpl::filter([=] {\n\t\t\treturn _activeForTrayIconAction;\n\t\t})\n\t);\n\tif (_tray.hasTrayMessageSupport()) {\n\t\treturn std::move(triggers) | rpl::map([=]() -> rpl::empty_value {\n\t\t\t_tray.showTrayMessage();\n\t\t\treturn {};\n\t\t});\n\t} else {\n\t\treturn triggers;\n\t}\n}\n\nvoid Tray::toggleSoundNotifications() {\n\tauto soundNotifyChanged = false;\n\tauto flashBounceNotifyChanged = false;\n\tauto &settings = Core::App().settings();\n\tsettings.setDesktopNotify(!settings.desktopNotify());\n\tif (settings.desktopNotify()) {\n\t\tif (settings.rememberedSoundNotifyFromTray()\n\t\t\t&& !settings.soundNotify()) {\n\t\t\tsettings.setSoundNotify(true);\n\t\t\tsettings.setRememberedSoundNotifyFromTray(false);\n\t\t\tsoundNotifyChanged = true;\n\t\t}\n\t\tif (settings.rememberedFlashBounceNotifyFromTray()\n\t\t\t&& !settings.flashBounceNotify()) {\n\t\t\tsettings.setFlashBounceNotify(true);\n\t\t\tsettings.setRememberedFlashBounceNotifyFromTray(false);\n\t\t\tflashBounceNotifyChanged = true;\n\t\t}\n\t} else {\n\t\tif (settings.soundNotify()) {\n\t\t\tsettings.setSoundNotify(false);\n\t\t\tsettings.setRememberedSoundNotifyFromTray(true);\n\t\t\tsoundNotifyChanged = true;\n\t\t} else {\n\t\t\tsettings.setRememberedSoundNotifyFromTray(false);\n\t\t}\n\t\tif (settings.flashBounceNotify()) {\n\t\t\tsettings.setFlashBounceNotify(false);\n\t\t\tsettings.setRememberedFlashBounceNotifyFromTray(true);\n\t\t\tflashBounceNotifyChanged = true;\n\t\t} else {\n\t\t\tsettings.setRememberedFlashBounceNotifyFromTray(false);\n\t\t}\n\t}\n\tCore::App().saveSettingsDelayed();\n\tusing Change = Window::Notifications::ChangeType;\n\tauto &notifications = Core::App().notifications();\n\tnotifications.notifySettingsChanged(Change::DesktopEnabled);\n\tif (soundNotifyChanged) {\n\t\tnotifications.notifySettingsChanged(Change::SoundEnabled);\n\t}\n\tif (flashBounceNotifyChanged) {\n\t\tnotifications.notifySettingsChanged(Change::FlashBounceEnabled);\n\t}\n}\n\nbool Tray::has() const {\n\treturn _tray.hasIcon() && Platform::TrayIconSupported();\n}\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/tray.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_tray.h\"\n\nnamespace Core {\n\nclass Tray final {\npublic:\n\tTray();\n\n\tvoid create();\n\tvoid updateMenuText();\n\tvoid updateIconCounters();\n\n\t[[nodiscard]] rpl::producer<> aboutToShowRequests() const;\n\t[[nodiscard]] rpl::producer<> showFromTrayRequests() const;\n\t[[nodiscard]] rpl::producer<> hideToTrayRequests() const;\n\n\t[[nodiscard]] bool has() const;\n\nprivate:\n\tvoid rebuildMenu();\n\tvoid toggleSoundNotifications();\n\n\tPlatform::Tray _tray;\n\n\tbool _activeForTrayIconAction = false;\n\tcrl::time _lastTrayClickTime = 0;\n\n\trpl::event_stream<> _textUpdates;\n\trpl::event_stream<> _minimizeMenuItemClicks;\n\n};\n\n} // namespace Core\n"
  },
  {
    "path": "Telegram/SourceFiles/tray_accounts_menu.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"tray_accounts_menu.h\"\n\n#include \"base/weak_ptr.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"core/application.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"styles/style_window.h\"\n#include \"ui/dynamic_thumbnails.h\"\n\nnamespace Core::TrayAccountsMenu {\n\nvoid SetupChangesSubscription(Fn<void()> callback, rpl::lifetime &lifetime) {\n\tconst auto accountSessionsLifetime = lifetime.make_state<rpl::lifetime>();\n\tconst auto watchAccountSessions = [=] {\n\t\taccountSessionsLifetime->destroy();\n\t\tfor (const auto &[index, account] : Core::App().domain().accounts()) {\n\t\t\taccount->sessionChanges(\n\t\t\t) | rpl::on_next([=](Main::Session*) {\n\t\t\t\tcallback();\n\t\t\t}, *accountSessionsLifetime);\n\t\t}\n\t};\n\tCore::App().domain().accountsChanges() | rpl::on_next([=] {\n\t\twatchAccountSessions();\n\t\tcallback();\n\t}, lifetime);\n\twatchAccountSessions();\n}\n\nvoid Fill(Platform::Tray &tray) {\n\tauto accounts = std::vector<not_null<Main::Account*>>();\n\tfor (const auto &account : Core::App().domain().orderedAccounts()) {\n\t\tif (account->sessionExists()) {\n\t\t\taccounts.push_back(account);\n\t\t}\n\t}\n\tif (accounts.size() <= 1) {\n\t\treturn;\n\t}\n\ttray.addSeparator();\n\tconstexpr auto kMaxLength = 30;\n\tfor (const auto account : accounts) {\n\t\tconst auto user = account->session().user();\n\t\tconst auto weak = base::make_weak(account);\n\t\ttray.addAction(\n\t\t\tInfo::Profile::NameValue(\n\t\t\t\tuser\n\t\t\t) | rpl::map([=](const QString &name) {\n\t\t\t\treturn (name.size() > kMaxLength)\n\t\t\t\t\t? (name.mid(0, kMaxLength) + Ui::kQEllipsis)\n\t\t\t\t\t: name;\n\t\t\t}),\n\t\t\t[weak] {\n\t\t\t\tconst auto strong = weak.get();\n\t\t\t\tif (!strong || !strong->sessionExists()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (base::IsCtrlPressed()) {\n\t\t\t\t\tCore::App().ensureSeparateWindowFor({ strong });\n\t\t\t\t} else {\n\t\t\t\t\tCore::App().domain().maybeActivate(strong);\n\t\t\t\t}\n\t\t\t},\n\t\t\tUi::MakeUserpicThumbnail(user, true),\n\t\t\tst::notifyMacPhotoSize);\n\t}\n}\n\n} // namespace Core::TrayAccountsMenu\n"
  },
  {
    "path": "Telegram/SourceFiles/tray_accounts_menu.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"platform/platform_tray.h\"\n\nnamespace Core::TrayAccountsMenu {\n\nvoid SetupChangesSubscription(\n\tFn<void()> callback,\n\trpl::lifetime &lifetime);\nvoid Fill(Platform::Tray &tray);\n\n} // namespace Core::TrayAccountsMenu\n"
  },
  {
    "path": "Telegram/SourceFiles/tray_accounts_menu_dummy.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"tray_accounts_menu.h\"\n\nnamespace Core::TrayAccountsMenu {\n\nvoid SetupChangesSubscription(\n\t\t[[maybe_unused]] Fn<void()> callback,\n\t\t[[maybe_unused]] rpl::lifetime &lifetime) {\n}\n\nvoid Fill([[maybe_unused]] Platform::Tray &tray) {\n}\n\n} // namespace Core::TrayAccountsMenu\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/about_cocoon_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/about_cocoon_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/controls/feature_list.h\"\n#include \"ui/effects/ministar_particles.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/top_background_gradient.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_info.h\" // infoStarsUnderstood\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Ui {\nnamespace {\n\nvoid AddCocoonBoxCover(not_null<Ui::VerticalLayout*> container) {\n\tconst auto cover = container->add(object_ptr<Ui::RpWidget>(container));\n\n\tstatic const auto gradientEdge = QColor(0x06, 0x11, 0x29);\n\tstatic const auto gradientCenter = QColor(0x09, 0x16, 0x43);\n\n\tstatic const auto gradientLeft = QColor(0x68, 0xc9, 0xff);\n\tstatic const auto gradientRight = QColor(0xcb, 0x57, 0xff);\n\n\tstatic const auto textColor = QColor(0xb8, 0xc9, 0xef);\n\tstatic const auto boldColor = QColor(0xff, 0xff, 0xff);\n\n\tconst auto logoTop = st::cocoonLogoTop;\n\tconst auto logoSize = st::cocoonLogoSize;\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto logo = QImage(u\":/gui/art/cocoon.webp\"_q).scaled(\n\t\tQSize(logoSize, logoSize) * ratio,\n\t\tQt::IgnoreAspectRatio,\n\t\tQt::SmoothTransformation);\n\tlogo.setDevicePixelRatio(ratio);\n\tconst auto titleTop = logoTop + logoSize + st::cocoonTitleTop;\n\tconst auto subtitleTop = titleTop\n\t\t+ st::cocoonTitleFont->height\n\t\t+ st::cocoonSubtitleTop;\n\n\tconst auto boldToColorized = [](QString text) {\n\t\tauto result = tr::rich(text);\n\t\tauto &entities = result.entities;\n\t\tfor (auto i = entities.begin(); i != entities.end(); ++i) {\n\t\t\tif (i->type() == EntityType::Bold) {\n\t\t\t\ti = entities.insert(\n\t\t\t\t\ti,\n\t\t\t\t\tEntityInText(\n\t\t\t\t\t\tEntityType::Colorized,\n\t\t\t\t\t\ti->offset(),\n\t\t\t\t\t\ti->length()));\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\n\tstruct State {\n\t\tQImage gradient;\n\t\tUi::Animations::Basic animation;\n\t\tstd::optional<Ui::StarParticles> particles;\n\t\tstyle::owned_color subtitleFg = style::owned_color{ textColor };\n\t\tstyle::owned_color subtitleBoldFg = style::owned_color{ boldColor };\n\t\tstyle::FlatLabel subtitleSt = st::cocoonSubtitle;\n\t};\n\tconst auto state = cover->lifetime().make_state<State>();\n\tstate->subtitleSt.textFg = state->subtitleFg.color();\n\tstate->subtitleSt.palette.linkFg = state->subtitleBoldFg.color();\n\tstate->animation.init([=] {\n\t\tcover->update();\n\t\tif (anim::Disabled()) {\n\t\t\tstate->animation.stop();\n\t\t}\n\t});\n\n\tconstexpr auto kParticlesCount = 50;\n\tstate->particles.emplace(\n\t\tUi::StarParticles::Type::RadialInside,\n\t\tkParticlesCount,\n\t\tst::cocoonLogoSize / 12);\n\tstate->particles->setSpeed(0.05);\n\tauto particleColors = std::vector<QColor>();\n\tconstexpr auto kParticleColors = 12;\n\tparticleColors.reserve(kParticleColors);\n\tfor (auto i = 0; i != kParticleColors; ++i) {\n\t\tparticleColors.push_back(anim::color(\n\t\t\tgradientLeft,\n\t\t\tgradientRight,\n\t\t\tfloat64(i) / (kParticleColors - 1)));\n\t}\n\tstate->particles->setColors(std::move(particleColors));\n\n\tconst auto subtitle = CreateChild<Ui::FlatLabel>(\n\t\tcover,\n\t\ttr::lng_translate_cocoon_subtitle(\n\t\t\tlt_text,\n\t\t\ttr::lng_translate_cocoon_explain(boldToColorized),\n\t\t\ttr::rich),\n\t\tstate->subtitleSt);\n\tsubtitle->setTryMakeSimilarLines(true);\n\n\tcover->widthValue() | rpl::on_next([=](int width) {\n\t\tconst auto available = width\n\t\t\t- st::boxRowPadding.left()\n\t\t\t- st::boxRowPadding.right();\n\t\tsubtitle->resizeToWidth(available);\n\t\tsubtitle->moveToLeft(st::boxRowPadding.left(), subtitleTop);\n\n\t\tconst auto bottom = st::cocoonSubtitleBottom;\n\t\tcover->resize(width, subtitle->y() + subtitle->height() + bottom);\n\t}, cover->lifetime());\n\n\tcover->paintRequest() | rpl::on_next([=] {\n\t\tauto p = Painter(cover);\n\n\t\tconst auto width = cover->width();\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tif (state->gradient.size() != cover->size() * ratio) {\n\t\t\tstate->gradient = Ui::CreateTopBgGradient(\n\t\t\t\tcover->size(),\n\t\t\t\tgradientCenter,\n\t\t\t\tgradientEdge);\n\n\t\t\tauto font = st::cocoonTitleFont->f;\n\t\t\tfont.setWeight(QFont::Bold);\n\t\t\tauto metrics = QFontMetrics(font);\n\n\t\t\tconst auto text = tr::lng_translate_cocoon_title(tr::now);\n\t\t\tconst auto textw = metrics.horizontalAdvance(text);\n\t\t\tconst auto left = (width - textw) / 2;\n\n\t\t\tauto q = QPainter(&state->gradient);\n\t\t\tauto hq = PainterHighQualityEnabler(q);\n\n\t\t\tauto gradient = QLinearGradient(left, 0, left + textw, 0);\n\t\t\tgradient.setStops({\n\t\t\t\t{ 0., gradientLeft },\n\t\t\t\t{ 1., gradientRight },\n\t\t\t});\n\t\t\tauto pen = QPen(QBrush(gradient), 1.);\n\t\t\tq.setPen(pen);\n\t\t\tq.setFont(font);\n\t\t\tq.setBrush(Qt::NoBrush);\n\n\t\t\tq.drawText(left, titleTop + metrics.ascent(), text);\n\t\t}\n\t\tp.drawImage(0, 0, state->gradient);\n\n\t\tconst auto logoRect = QRect(\n\t\t\t(width - logoSize) / 2,\n\t\t\tlogoTop,\n\t\t\tlogoSize,\n\t\t\tlogoSize);\n\t\tconst auto paddingAdd = int(base::SafeRound(logoTop * 1.2));\n\t\tconst auto particlesRect = logoRect.marginsAdded(\n\t\t\t{ paddingAdd, paddingAdd, paddingAdd, paddingAdd });\n\n\t\tstate->particles->paint(p, particlesRect, crl::now(), false);\n\t\tif (!anim::Disabled() && !state->animation.animating()) {\n\t\t\tstate->animation.start();\n\t\t}\n\t\tp.drawImage(logoRect, logo);\n\t}, cover->lifetime());\n}\n\nstruct CocoonLinkInfo {\n\tQString text;\n\tQString link;\n};\n\n[[nodiscard]] CocoonLinkInfo CocoonMention() {\n\tconst auto mention = tr::lng_translate_cocoon_private_mention(tr::now);\n\tconst auto username = QString(mention).replace('@', QString());\n\tconst auto link = u\"tg://resolve?domain=\"_q + username;\n\treturn { mention, link };\n}\n\n[[nodiscard]] CocoonLinkInfo CocoonDomain() {\n\tconst auto domain = tr::lng_translate_cocoon_everyone_domain(tr::now);\n\tconst auto link = u\"https://\"_q + domain;\n\treturn { domain, link };\n}\n\n[[nodiscard]] CocoonLinkInfo CocoonDirect() {\n\tconst auto text = tr::lng_translate_cocoon_text_link(tr::now);\n\tconst auto link = u\"https://\"_q + text;\n\treturn { text, link };\n}\n\n} // namespace\n\nvoid AboutCocoonBox(not_null<Ui::GenericBox*> box) {\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setStyle(st::stakeBox);\n\tbox->setNoContentMargin(true);\n\n\tconst auto container = box->verticalLayout();\n\tAddCocoonBoxCover(container);\n\n\tAddUniqueCloseButton(box);\n\n\tconst auto mention = CocoonMention();\n\tconst auto domain = CocoonDomain();\n\tconst auto direct = CocoonDirect();\n\tconst auto features = std::vector<Ui::FeatureListEntry>{\n\t\t{\n\t\t\tst::menuIconLock,\n\t\t\ttr::lng_translate_cocoon_private_title(tr::now),\n\t\t\ttr::lng_translate_cocoon_private_text(\n\t\t\t\ttr::now,\n\t\t\t\tlt_mention,\n\t\t\t\ttr::link(mention.text, mention.link),\n\t\t\t\ttr::rich),\n\t\t},\n\t\t{\n\t\t\tst::menuIconStats,\n\t\t\ttr::lng_translate_cocoon_efficient_title(tr::now),\n\t\t\ttr::lng_translate_cocoon_efficient_text(tr::now, tr::rich),\n\t\t},\n\t\t{\n\t\t\tst::menuIconGiftPremium,\n\t\t\ttr::lng_translate_cocoon_everyone_title(tr::now),\n\t\t\ttr::lng_translate_cocoon_everyone_text(\n\t\t\t\ttr::now,\n\t\t\t\tlt_domain,\n\t\t\t\ttr::link(domain.text, domain.link),\n\t\t\t\ttr::rich),\n\t\t},\n\t};\n\tauto margin = QMargins(0, st::defaultVerticalListSkip, 0, 0);\n\tfor (const auto &feature : features) {\n\t\tbox->addRow(\n\t\t\tMakeFeatureListEntry(box, feature),\n\t\t\tst::boxRowPadding + margin);\n\t\tmargin = {};\n\t}\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::PlainShadow>(box),\n\t\tst::cocoonJoinSeparatorPadding);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_translate_cocoon_text(\n\t\t\t\tlt_link,\n\t\t\t\trpl::single(tr::link(direct.text, direct.link)),\n\t\t\t\ttr::rich),\n\t\t\tst::cocoonJoinLabel),\n\t\tstyle::al_top);\n\n\tbox->addButton(rpl::single(QString()), [=] {\n\t\tbox->closeBox();\n\t})->setText(rpl::single(Ui::Text::IconEmoji(\n\t\t&st::infoStarsUnderstood\n\t).append(' ').append(tr::lng_translate_cocoon_done(tr::now))));\n}\n\nvoid AddUniqueCloseButton(not_null<GenericBox*> box) {\n\tconst auto close = Ui::CreateChild<IconButton>(\n\t\tbox,\n\t\tst::uniqueCloseButton);\n\tclose->show();\n\tclose->raise();\n\tbox->widthValue() | rpl::on_next([=](int width) {\n\t\tclose->moveToRight(0, 0, width);\n\t\tclose->raise();\n\t}, close->lifetime());\n\tclose->setClickedCallback([=] {\n\t\tbox->closeBox();\n\t});\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/about_cocoon_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nclass GenericBox;\n\nvoid AboutCocoonBox(not_null<Ui::GenericBox*> box);\nvoid AddUniqueCloseButton(not_null<GenericBox*> box);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/auto_delete_settings.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/auto_delete_settings.h\"\n\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/painter.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Ui {\nnamespace {\n\nobject_ptr<Ui::RpWidget> CreateSliderForTTL(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::vector<QString> labels,\n\t\tint dashedAfterIndex,\n\t\tint selected,\n\t\tFn<void(int)> callback) {\n\tExpects(labels.size() > 1);\n\tExpects(selected >= 0 && selected < labels.size());\n\tExpects(dashedAfterIndex >= 0 && dashedAfterIndex < labels.size());\n\n\tstruct State {\n\t\tstd::vector<int> points;\n\t\tstd::vector<QString> labels;\n\t\tint selected = 0;\n\t};\n\tstatic const auto st = &st::defaultSliderForTTL;\n\tconst auto height = st->font->height + st->skip + st->chosenSize;\n\tconst auto count = int(labels.size());\n\n\tauto result = object_ptr<Ui::FixedHeightWidget>(parent.get(), height);\n\tconst auto raw = result.data();\n\tconst auto slider = Ui::CreateChild<Ui::FixedHeightWidget>(\n\t\traw,\n\t\tst->chosenSize);\n\tslider->setCursor(style::cur_pointer);\n\tslider->move(0, height - slider->height());\n\n\tauto &lifetime = raw->lifetime();\n\tconst auto state = lifetime.make_state<State>(State{\n\t\t.labels = std::move(labels),\n\t\t.selected = selected\n\t});\n\tstate->points.resize(count, 0);\n\n\traw->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tstate->points[i] = (width * i) / (count - 1);\n\t\t}\n\t\tslider->resize(width, slider->height());\n\t}, lifetime);\n\n\traw->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\n\t\tp.setFont(st->font);\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\t// Label\n\t\t\tp.setPen(st->textFg);\n\t\t\tconst auto &text = state->labels[i];\n\t\t\tconst auto textWidth = st->font->width(text);\n\t\t\tconst auto shift = (i == count - 1)\n\t\t\t\t? textWidth\n\t\t\t\t: (i > 0)\n\t\t\t\t? (textWidth / 2)\n\t\t\t\t: 0;\n\t\t\tconst auto x = state->points[i] - shift;\n\t\t\tconst auto y = st->font->ascent;\n\t\t\tp.drawText(x, y, text);\n\t\t}\n\t}, lifetime);\n\n\tslider->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(slider);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tp.setFont(st->font);\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tconst auto middle = (st->chosenSize / 2.);\n\n\t\t\t// Point\n\t\t\tconst auto size = (i == state->selected)\n\t\t\t\t? st->chosenSize\n\t\t\t\t: st->pointSize;\n\t\t\tconst auto pointfg = (i <= state->selected)\n\t\t\t\t? st->activeFg\n\t\t\t\t: st->inactiveFg;\n\t\t\tconst auto shift = (i == count - 1)\n\t\t\t\t? float64(size)\n\t\t\t\t: (i > 0)\n\t\t\t\t? (size / 2.)\n\t\t\t\t: 0.;\n\t\t\tconst auto pointx = state->points[i] - shift;\n\t\t\tconst auto pointy = middle - (size / 2.);\n\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(pointfg);\n\t\t\tp.drawEllipse(QRectF{ pointx, pointy, size * 1., size * 1. });\n\n\t\t\t// Line\n\t\t\tif (i + 1 == count) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst auto nextSize = (i + 1 == state->selected)\n\t\t\t\t? st->chosenSize\n\t\t\t\t: st->pointSize;\n\t\t\tconst auto nextShift = (i + 1 == count - 1)\n\t\t\t\t? float64(nextSize)\n\t\t\t\t: (nextSize / 2.);\n\t\t\tconst auto &linefg = (i + 1 <= state->selected)\n\t\t\t\t? st->activeFg\n\t\t\t\t: st->inactiveFg;\n\t\t\tconst auto from = pointx + size + st->stroke * 1.5;\n\t\t\tconst auto till = state->points[i + 1] - nextShift - st->stroke * 1.5;\n\n\t\t\tauto pen = linefg->p;\n\t\t\tpen.setWidthF(st->stroke);\n\t\t\tif (i >= dashedAfterIndex) {\n\t\t\t\t// Try to fill the line with exact number of dash segments.\n\t\t\t\t// UPD Doesn't work so well because it changes when clicking.\n\t\t\t\t//const auto length = till - from;\n\t\t\t\t//const auto offSegmentsCount = int(base::SafeRound(\n\t\t\t\t//\t(length - st->dashOn) / (st->dashOn + st->dashOff)));\n\t\t\t\t//const auto onSegmentsCount = offSegmentsCount + 1;\n\t\t\t\t//const auto idealLength = offSegmentsCount * st->dashOff\n\t\t\t\t//\t+ onSegmentsCount * st->dashOn;\n\t\t\t\t//const auto multiplier = length / float64(idealLength);\n\n\t\t\t\tconst auto multiplier = 1.;\n\t\t\t\tauto dashPattern = QVector<qreal>{\n\t\t\t\t\tst->dashOn * multiplier / st->stroke,\n\t\t\t\t\tst->dashOff * multiplier / st->stroke\n\t\t\t\t};\n\t\t\t\tpen.setDashPattern(dashPattern);\n\t\t\t}\n\t\t\tpen.setCapStyle(Qt::RoundCap);\n\t\t\tp.setPen(pen);\n\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\tp.drawLine(QPointF(from, middle), QPointF(till, middle));\n\t\t}\n\t}, lifetime);\n\n\tslider->events(\n\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\treturn (e->type() == QEvent::MouseButtonPress)\n\t\t\t&& (static_cast<QMouseEvent*>(e.get())->button()\n\t\t\t\t== Qt::LeftButton)\n\t\t\t&& (state->points[1] > 0);\n\t}) | rpl::map([=](not_null<QEvent*> e) {\n\t\treturn rpl::single(\n\t\t\tstatic_cast<QMouseEvent*>(e.get())->pos()\n\t\t) | rpl::then(slider->events(\n\t\t) | rpl::take_while([=](not_null<QEvent*> e) {\n\t\t\treturn (e->type() != QEvent::MouseButtonRelease)\n\t\t\t\t|| (static_cast<QMouseEvent*>(e.get())->button()\n\t\t\t\t\t!= Qt::LeftButton);\n\t\t}) | rpl::filter([=](not_null<QEvent*> e) {\n\t\t\treturn (e->type() == QEvent::MouseMove);\n\t\t}) | rpl::map([=](not_null<QEvent*> e) {\n\t\t\treturn static_cast<QMouseEvent*>(e.get())->pos();\n\t\t}));\n\t}) | rpl::flatten_latest(\n\t) | rpl::on_next([=](QPoint position) {\n\t\tstate->selected = std::clamp(\n\t\t\t(position.x() + (state->points[1] / 2)) / state->points[1],\n\t\t\t0,\n\t\t\tcount - 1);\n\t\tslider->update();\n\t\tcallback(state->selected);\n\t}, lifetime);\n\n\treturn result;\n}\n\n} // namespace\n\nvoid AutoDeleteSettingsBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tTimeId ttlPeriod,\n\t\trpl::producer<QString> about,\n\t\tFn<void(TimeId)> callback) {\n\tbox->setTitle(tr::lng_manage_messages_ttl_title());\n\n\tstruct State {\n\t\tTimeId period = 0;\n\t};\n\n\tconst auto state = box->lifetime().make_state<State>(State{\n\t\t.period = ttlPeriod,\n\t});\n\n\tconst auto options = std::vector<QString>{\n\t\ttr::lng_manage_messages_ttl_disable(tr::now),\n\t\t//u\"5 seconds\"_q, AssertIsDebug()\n\t\ttr::lng_manage_messages_ttl_after1(tr::now),\n\t\ttr::lng_manage_messages_ttl_after2(tr::now),\n\t\ttr::lng_manage_messages_ttl_after3(tr::now),\n\t};\n\tconst auto periodToIndex = [&](TimeId period) {\n\t\treturn !period\n\t\t\t? 0\n\t\t\t//: (period == 5) AssertIsDebug()\n\t\t\t//? 1 AssertIsDebug()\n\t\t\t: (period < 2 * 86400)\n\t\t\t? 1\n\t\t\t: (period < 8 * 86400)\n\t\t\t? 2\n\t\t\t: 3;\n\t};\n\tconst auto indexToPeriod = [&](int index) {\n\t\treturn !index\n\t\t\t? 0\n\t\t\t//: (index == 1) AssertIsDebug()\n\t\t\t//? 5 AssertIsDebug()\n\t\t\t: (index == 1)\n\t\t\t? 86400\n\t\t\t: (index == 2)\n\t\t\t? 7 * 86400\n\t\t\t: 31 * 86400;\n\t};\n\tconst auto sliderCallback = [=](int index) {\n\t\tstate->period = indexToPeriod(index);\n\t};\n\tbox->addRow(\n\t\tCreateSliderForTTL(\n\t\t\tbox,\n\t\t\toptions | ranges::to_vector,\n\t\t\toptions.size() - 1,\n\t\t\tperiodToIndex(ttlPeriod),\n\t\t\tsliderCallback),\n\t\t{\n\t\t\tst::boxRowPadding.left(),\n\t\t\t0,\n\t\t\tst::boxRowPadding.right(),\n\t\t\tst::boxMediumSkip });\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::DividerLabel>(\n\t\t\tbox,\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\tstd::move(about),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tst::ttlDividerLabelPadding),\n\t\tstyle::margins());\n\n\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\tconst auto period = state->period;\n\t\tbox->closeBox();\n\n\t\tcallback(period);\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/auto_delete_settings.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n\nnamespace Ui {\n\nvoid AutoDeleteSettingsBox(\n\tnot_null<Ui::GenericBox*> box,\n\tTimeId ttlPeriod,\n\trpl::producer<QString> about,\n\tFn<void(TimeId)> callback);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/boost_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/boost_box.h\"\n\n#include \"info/profile/info_profile_icon.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/effects/fireworks_animation.h\"\n#include \"ui/effects/premium_bubble.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_premium.h\"\n\n#include <QtGui/QGuiApplication>\n\nnamespace Ui {\nnamespace {\n\n[[nodiscard]] BoostCounters AdjustByReached(BoostCounters data) {\n\tconst auto exact = (data.boosts == data.thisLevelBoosts);\n\tconst auto reached = !data.nextLevelBoosts || (exact && data.mine > 0);\n\tif (reached) {\n\t\t--data.level;\n\t\tdata.boosts = data.nextLevelBoosts = std::max({\n\t\t\tdata.boosts,\n\t\t\tdata.thisLevelBoosts,\n\t\t\t1\n\t\t});\n\t\tdata.thisLevelBoosts = 0;\n\t} else {\n\t\tdata.boosts = std::max(data.thisLevelBoosts, data.boosts);\n\t\tdata.nextLevelBoosts = std::max(\n\t\t\tdata.nextLevelBoosts,\n\t\t\tdata.boosts + 1);\n\t}\n\treturn data;\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeTitle(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<QString> title,\n\t\trpl::producer<QString> repeated,\n\t\tbool centered = true) {\n\tauto result = object_ptr<Ui::RpWidget>(parent);\n\n\tstruct State {\n\t\tnot_null<Ui::FlatLabel*> title;\n\t\tnot_null<Ui::FlatLabel*> repeated;\n\t};\n\tconst auto notEmpty = [](const QString &text) {\n\t\treturn !text.isEmpty();\n\t};\n\tconst auto state = parent->lifetime().make_state<State>(State{\n\t\t.title = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tresult.data(),\n\t\t\trpl::duplicate(title),\n\t\t\tst::boostTitle),\n\t\t.repeated = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tresult.data(),\n\t\t\trpl::duplicate(repeated) | rpl::filter(notEmpty),\n\t\t\tst::boostTitleBadge),\n\t});\n\tstate->title->show();\n\tstate->repeated->showOn(std::move(repeated) | rpl::map(notEmpty));\n\n\tresult->resize(result->width(), st::boostTitle.style.font->height);\n\n\trpl::combine(\n\t\tresult->widthValue(),\n\t\trpl::duplicate(title),\n\t\tstate->repeated->shownValue(),\n\t\tstate->repeated->widthValue()\n\t) | rpl::on_next([=](int outer, auto&&, bool shown, int badge) {\n\t\tconst auto repeated = shown ? badge : 0;\n\t\tconst auto skip = st::boostTitleBadgeSkip;\n\t\tconst auto available = outer - repeated - skip;\n\t\tconst auto use = std::min(state->title->textMaxWidth(), available);\n\t\tstate->title->resizeToWidth(use);\n\t\tconst auto left = centered\n\t\t\t? (outer - use - skip - repeated) / 2\n\t\t\t: 0;\n\t\tstate->title->moveToLeft(left, 0);\n\t\tconst auto mleft = st::boostTitleBadge.margin.left();\n\t\tconst auto mtop = st::boostTitleBadge.margin.top();\n\t\tstate->repeated->moveToLeft(left + use + skip + mleft, mtop);\n\t}, result->lifetime());\n\n\tconst auto badge = state->repeated;\n\tbadge->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(badge);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto radius = std::min(badge->width(), badge->height()) / 2;\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::premiumButtonBg2);\n\t\tp.drawRoundedRect(badge->rect(), radius, radius);\n\t}, badge->lifetime());\n\n\treturn result;\n}\n\n[[nodiscard]] object_ptr<Ui::FlatLabel> MakeFeaturesBadge(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<QString> text) {\n\treturn MakeBoostFeaturesBadge(parent, std::move(text), [](QRect rect) {\n\t\tauto gradient = QLinearGradient(\n\t\t\trect.topLeft(),\n\t\t\trect.topRight());\n\t\tgradient.setStops(Ui::Premium::GiftGradientStops());\n\t\treturn QBrush(gradient);\n\t});\n}\n\nvoid AddFeaturesList(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst Ui::BoostFeatures &features,\n\t\tint startFromLevel,\n\t\tbool group) {\n\tconst auto add = [&](\n\t\t\trpl::producer<TextWithEntities> text,\n\t\t\tconst style::icon &st) {\n\t\tconst auto label = container->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\tstd::move(text),\n\t\t\t\tst::boostFeatureLabel),\n\t\t\tst::boostFeaturePadding);\n\t\tobject_ptr<Info::Profile::FloatingIcon>(\n\t\t\tlabel,\n\t\t\tst,\n\t\t\tst::boostFeatureIconPosition);\n\t};\n\tconst auto lowMax = std::max({\n\t\tfeatures.linkLogoLevel,\n\t\tfeatures.profileIconLevel,\n\t\tfeatures.autotranslateLevel,\n\t\tfeatures.transcribeLevel,\n\t\tfeatures.emojiPackLevel,\n\t\tfeatures.emojiStatusLevel,\n\t\tfeatures.wallpaperLevel,\n\t\tfeatures.customWallpaperLevel,\n\t\t(features.nameColorsByLevel.empty()\n\t\t\t? 0\n\t\t\t: features.nameColorsByLevel.back().first),\n\t\t(features.linkStylesByLevel.empty()\n\t\t\t? 0\n\t\t\t: features.linkStylesByLevel.back().first),\n\t\t(features.profileColorsByLevel.empty()\n\t\t\t? 0\n\t\t\t: features.profileColorsByLevel.back().first),\n\t});\n\tconst auto highMax = std::max(lowMax, features.sponsoredLevel);\n\tauto nameColors = 0;\n\tauto linkStyles = 0;\n\tauto profileColors = 0;\n\tfor (auto i = std::max(startFromLevel, 1); i <= highMax; ++i) {\n\t\tif ((i > lowMax) && (i < highMax)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto unlocks = (i == startFromLevel);\n\t\t{\n\t\t\tconst auto badge = container->add(\n\t\t\t\tMakeFeaturesBadge(\n\t\t\t\t\tcontainer,\n\t\t\t\t\t(unlocks\n\t\t\t\t\t\t? tr::lng_boost_level_unlocks\n\t\t\t\t\t\t: tr::lng_boost_level)(\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\trpl::single(float64(i)))),\n\t\t\t\tst::boostLevelBadgePadding,\n\t\t\t\tstyle::al_top);\n\t\t\tconst auto padding = st::boxRowPadding;\n\t\t\tconst auto line = Ui::CreateChild<Ui::RpWidget>(container);\n\t\t\tbadge->geometryValue() | rpl::on_next([=](const QRect &r) {\n\t\t\t\tline->setGeometry(\n\t\t\t\t\tpadding.left(),\n\t\t\t\t\tr.y(),\n\t\t\t\t\tcontainer->width() - rect::m::sum::h(padding),\n\t\t\t\t\tr.height());\n\t\t\t}, line->lifetime());\n\t\t\tconst auto shift = st::lineWidth * 10;\n\t\t\tline->paintRequest() | rpl::on_next([=] {\n\t\t\t\tauto p = QPainter(line);\n\t\t\t\tp.setPen(st::windowSubTextFg);\n\t\t\t\tconst auto y = line->height() / 2;\n\t\t\t\tconst auto left = badge->x() - shift - padding.left();\n\t\t\t\tconst auto right = left + badge->width() + shift * 2;\n\t\t\t\tif (left > 0) {\n\t\t\t\t\tp.drawLine(0, y, left, y);\n\t\t\t\t}\n\t\t\t\tif (right < line->width()) {\n\t\t\t\t\tp.drawLine(right, y, line->width(), y);\n\t\t\t\t}\n\t\t\t}, line->lifetime());\n\t\t}\n\t\tif (i >= features.sponsoredLevel) {\n\t\t\tadd(\n\t\t\t\ttr::lng_channel_earn_off(tr::rich),\n\t\t\t\tst::boostFeatureOffSponsored);\n\t\t}\n\t\tif (i >= features.customWallpaperLevel) {\n\t\t\tadd(\n\t\t\t\t(group\n\t\t\t\t\t? tr::lng_feature_custom_background_group\n\t\t\t\t\t: tr::lng_feature_custom_background_channel)(tr::rich),\n\t\t\t\tst::boostFeatureCustomBackground);\n\t\t}\n\t\tif (i >= features.wallpaperLevel) {\n\t\t\tadd(\n\t\t\t\t(group\n\t\t\t\t\t? tr::lng_feature_backgrounds_group\n\t\t\t\t\t: tr::lng_feature_backgrounds_channel)(\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\trpl::single(float64(features.wallpapersCount)),\n\t\t\t\t\t\ttr::rich),\n\t\t\t\tst::boostFeatureBackground);\n\t\t}\n\t\tif (i >= features.emojiStatusLevel) {\n\t\t\tadd(\n\t\t\t\ttr::lng_feature_emoji_status(tr::rich),\n\t\t\t\tst::boostFeatureEmojiStatus);\n\t\t}\n\t\tif (const auto j = features.profileColorsByLevel.find(i)\n\t\t\t; j != end(features.profileColorsByLevel)) {\n\t\t\tprofileColors += j->second;\n\t\t}\n\t\tif (i >= features.profileIconLevel) {\n\t\t\tadd(\n\t\t\t\t(group\n\t\t\t\t\t? tr::lng_feature_profile_icon_group\n\t\t\t\t\t: tr::lng_feature_profile_icon_channel)(tr::rich),\n\t\t\t\tst::boostFeatureProfileIcon);\n\t\t}\n\t\tif (profileColors > 0) {\n\t\t\tadd((group\n\t\t\t\t? tr::lng_feature_profile_color_group\n\t\t\t\t: tr::lng_feature_profile_color_channel)(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(float64(profileColors)),\n\t\t\t\t\ttr::rich\n\t\t\t\t), st::boostFeatureProfileColor);\n\t\t}\n\t\tif (!group) {\n\t\t\tif (const auto j = features.linkStylesByLevel.find(i)\n\t\t\t\t; j != end(features.linkStylesByLevel)) {\n\t\t\t\tlinkStyles += j->second;\n\t\t\t}\n\t\t\tif (i >= features.linkLogoLevel) {\n\t\t\t\tadd(\n\t\t\t\t\ttr::lng_feature_link_emoji(tr::rich),\n\t\t\t\t\tst::boostFeatureCustomLink);\n\t\t\t}\n\t\t\tif (linkStyles > 0) {\n\t\t\t\tadd(tr::lng_feature_link_style_channel(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(float64(linkStyles)),\n\t\t\t\t\ttr::rich\n\t\t\t\t), st::boostFeatureLink);\n\t\t\t}\n\t\t\tif (const auto j = features.nameColorsByLevel.find(i)\n\t\t\t\t; j != end(features.nameColorsByLevel)) {\n\t\t\t\tnameColors += j->second;\n\t\t\t}\n\t\t\tif (nameColors > 0) {\n\t\t\t\tadd(tr::lng_feature_name_color_channel(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(float64(nameColors)),\n\t\t\t\t\ttr::rich\n\t\t\t\t), st::boostFeatureName);\n\t\t\t}\n\t\t\tadd(tr::lng_feature_reactions(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(i)),\n\t\t\t\ttr::rich\n\t\t\t), st::boostFeatureCustomReactions);\n\t\t}\n\t\tadd(\n\t\t\ttr::lng_feature_stories(lt_count, rpl::single(1. * i), tr::rich),\n\t\t\tst::boostFeatureStories);\n\t\tif (!group && i >= features.autotranslateLevel) {\n\t\t\tadd(\n\t\t\t\ttr::lng_feature_autotranslate(tr::rich),\n\t\t\t\tst::boostFeatureAutoTranslate);\n\t\t}\n\t\tif (group && i >= features.transcribeLevel) {\n\t\t\tadd(\n\t\t\t\ttr::lng_feature_transcribe(tr::rich),\n\t\t\t\tst::boostFeatureTranscribe);\n\t\t}\n\t\tif (group && i >= features.emojiPackLevel) {\n\t\t\tadd(\n\t\t\t\ttr::lng_feature_custom_emoji_pack(tr::rich),\n\t\t\t\tst::boostFeatureCustomEmoji);\n\t\t}\n\t}\n}\n\n} // namespace\n\nvoid StartFireworks(not_null<QWidget*> parent) {\n\tconst auto result = Ui::CreateChild<RpWidget>(parent.get());\n\tresult->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tresult->setGeometry(parent->rect());\n\tresult->show();\n\n\tauto &lifetime = result->lifetime();\n\tconst auto animation = lifetime.make_state<FireworksAnimation>([=] {\n\t\tresult->update();\n\t});\n\tresult->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(result);\n\t\tif (!animation->paint(p, result->rect())) {\n\t\t\tcrl::on_main(result, [=] { delete result; });\n\t\t}\n\t}, lifetime);\n}\n\nvoid BoostBox(\n\t\tnot_null<GenericBox*> box,\n\t\tBoostBoxData data,\n\t\tFn<void(Fn<void(BoostCounters)>)> boost) {\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setStyle(st::boostBox);\n\n\t//AssertIsDebug();\n\t//data.boost = {\n\t//\t.level = 2,\n\t//\t.boosts = 3,\n\t//\t.thisLevelBoosts = 2,\n\t//\t.nextLevelBoosts = 5,\n\t//\t.mine = 2,\n\t//};\n\n\tstruct State {\n\t\trpl::variable<BoostCounters> data;\n\t\trpl::variable<bool> full;\n\t\tbool submitted = false;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tstate->data = std::move(data.boost);\n\n\tFillBoostLimit(\n\t\tBoxShowFinishes(box),\n\t\tbox->verticalLayout(),\n\t\tstate->data.value(),\n\t\tst::boxRowPadding);\n\n\tbox->setMaxHeight(st::boostBoxMaxHeight);\n\tconst auto close = box->addTopButton(\n\t\tst::boxTitleClose,\n\t\t[=] { box->closeBox(); });\n\n\tconst auto name = data.name;\n\n\tauto title = state->data.value(\n\t) | rpl::map([=](BoostCounters counters) {\n\t\treturn (counters.mine > 0)\n\t\t\t? tr::lng_boost_channel_you_title(\n\t\t\t\tlt_channel,\n\t\t\t\trpl::single(name))\n\t\t\t: !counters.nextLevelBoosts\n\t\t\t? tr::lng_boost_channel_title_max()\n\t\t\t: counters.level\n\t\t\t? (data.group\n\t\t\t\t? tr::lng_boost_channel_title_more_group()\n\t\t\t\t: tr::lng_boost_channel_title_more())\n\t\t\t: (data.group\n\t\t\t\t? tr::lng_boost_channel_title_first_group()\n\t\t\t\t: tr::lng_boost_channel_title_first());\n\t}) | rpl::flatten_latest();\n\tauto repeated = state->data.value(\n\t) | rpl::map([=](BoostCounters counters) {\n\t\treturn (counters.mine > 1) ? u\"x%1\"_q.arg(counters.mine) : u\"\"_q;\n\t});\n\n\tconst auto wasMine = state->data.current().mine;\n\tconst auto wasLifting = data.lifting;\n\tauto text = state->data.value(\n\t) | rpl::map([=](BoostCounters counters) {\n\t\tconst auto lifting = wasLifting\n\t\t\t? (wasLifting\n\t\t\t\t- std::clamp(counters.mine - wasMine, 0, wasLifting - 1))\n\t\t\t: 0;\n\t\tconst auto bold = tr::bold(name);\n\t\tconst auto now = counters.boosts;\n\t\tconst auto full = !counters.nextLevelBoosts;\n\t\tconst auto left = (counters.nextLevelBoosts > now)\n\t\t\t? (counters.nextLevelBoosts - now)\n\t\t\t: 0;\n\t\tauto post = tr::lng_boost_channel_post_stories(\n\t\t\tlt_count,\n\t\t\trpl::single(float64(counters.level + (left ? 1 : 0))),\n\t\t\ttr::rich);\n\t\treturn (lifting > 1)\n\t\t\t? tr::lng_boost_group_lift_restrictions_many(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(lifting)),\n\t\t\t\ttr::rich)\n\t\t\t: lifting\n\t\t\t? tr::lng_boost_group_lift_restrictions(tr::rich)\n\t\t\t: (counters.mine || full)\n\t\t\t? (left\n\t\t\t\t? tr::lng_boost_channel_needs_unlock(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(float64(left)),\n\t\t\t\t\tlt_channel,\n\t\t\t\t\trpl::single(bold),\n\t\t\t\t\ttr::rich)\n\t\t\t\t: (!counters.level\n\t\t\t\t\t? (data.group\n\t\t\t\t\t\t? tr::lng_boost_channel_reached_first_group\n\t\t\t\t\t\t: tr::lng_boost_channel_reached_first)(\n\t\t\t\t\t\t\ttr::rich)\n\t\t\t\t\t: (data.group\n\t\t\t\t\t\t? tr::lng_boost_channel_reached_more_group\n\t\t\t\t\t\t: tr::lng_boost_channel_reached_more)(\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\trpl::single(float64(counters.level)),\n\t\t\t\t\t\t\tlt_post,\n\t\t\t\t\t\t\tstd::move(post),\n\t\t\t\t\t\t\ttr::rich)))\n\t\t\t: tr::lng_boost_channel_needs_unlock(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(left)),\n\t\t\t\tlt_channel,\n\t\t\t\trpl::single(bold),\n\t\t\t\ttr::rich);\n\t}) | rpl::flatten_latest();\n\tif (wasLifting) {\n\t\tstate->data.value(\n\t\t) | rpl::on_next([=](BoostCounters counters) {\n\t\t\tif (counters.mine - wasMine >= wasLifting) {\n\t\t\t\tbox->closeBox();\n\t\t\t}\n\t\t}, box->lifetime());\n\t}\n\n\tauto faded = object_ptr<Ui::FadeWrap<>>(\n\t\tclose->parentWidget(),\n\t\tMakeTitle(\n\t\t\tbox,\n\t\t\t(data.group\n\t\t\t\t? tr::lng_boost_group_button\n\t\t\t\t: tr::lng_boost_channel_button)(),\n\t\t\trpl::duplicate(repeated),\n\t\t\tfalse));\n\tconst auto titleInner = faded.data();\n\ttitleInner->move(st::boxTitlePosition);\n\ttitleInner->resizeToWidth(st::boxWideWidth\n\t\t- st::boxTitleClose.width\n\t\t- st::boxTitlePosition.x());\n\ttitleInner->hide(anim::type::instant);\n\tcrl::on_main(titleInner, [=] {\n\t\ttitleInner->raise();\n\t\ttitleInner->toggleOn(rpl::single(\n\t\t\trpl::empty\n\t\t) | rpl::then(\n\t\t\tbox->scrolls()\n\t\t) | rpl::map([=] {\n\t\t\treturn box->scrollTop() > 0;\n\t\t}));\n\t});\n\n\tbox->addRow(\n\t\tMakeTitle(box, std::move(title), std::move(repeated)),\n\t\tst::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0));\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(text),\n\t\t\tst::boostText),\n\t\t(st::boxRowPadding\n\t\t\t+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)));\n\n\tconst auto current = state->data.current();\n\tbox->setTitle(QString());\n\tAddFeaturesList(\n\t\tbox->verticalLayout(),\n\t\tdata.features,\n\t\tcurrent.level + (current.nextLevelBoosts ? 1 : 0),\n\t\tdata.group);\n\n\tconst auto allowMulti = data.allowMulti;\n\tauto submit = state->data.value(\n\t) | rpl::map([=](BoostCounters counters) {\n\t\treturn (!counters.nextLevelBoosts || (counters.mine && !allowMulti))\n\t\t\t? tr::lng_box_ok()\n\t\t\t: (counters.mine > 0)\n\t\t\t? tr::lng_boost_again_button()\n\t\t\t: data.group\n\t\t\t? tr::lng_boost_group_button()\n\t\t\t: tr::lng_boost_channel_button();\n\t}) | rpl::flatten_latest();\n\n\tbox->addButton(rpl::duplicate(submit), [=] {\n\t\tif (state->submitted) {\n\t\t\treturn;\n\t\t} else if (state->data.current().nextLevelBoosts > 0\n\t\t\t&& (allowMulti || !state->data.current().mine)) {\n\t\t\tstate->submitted = true;\n\t\t\tconst auto was = state->data.current().mine;\n\n\t\t\t//AssertIsDebug();\n\t\t\t//state->submitted = false;\n\t\t\t//if (state->data.current().level == 5\n\t\t\t//\t&& state->data.current().boosts == 11) {\n\t\t\t//\tstate->data = BoostCounters{\n\t\t\t//\t\t.level = 5,\n\t\t\t//\t\t.boosts = 14,\n\t\t\t//\t\t.thisLevelBoosts = 9,\n\t\t\t//\t\t.nextLevelBoosts = 15,\n\t\t\t//\t\t.mine = 14,\n\t\t\t//\t};\n\t\t\t//} else if (state->data.current().level == 5) {\n\t\t\t//\tstate->data = BoostCounters{\n\t\t\t//\t\t.level = 7,\n\t\t\t//\t\t.boosts = 16,\n\t\t\t//\t\t.thisLevelBoosts = 15,\n\t\t\t//\t\t.nextLevelBoosts = 19,\n\t\t\t//\t\t.mine = 16,\n\t\t\t//\t};\n\t\t\t//} else if (state->data.current().level == 4) {\n\t\t\t//\tstate->data = BoostCounters{\n\t\t\t//\t\t.level = 5,\n\t\t\t//\t\t.boosts = 11,\n\t\t\t//\t\t.thisLevelBoosts = 9,\n\t\t\t//\t\t.nextLevelBoosts = 15,\n\t\t\t//\t\t.mine = 9,\n\t\t\t//\t};\n\t\t\t//} else if (state->data.current().level == 3) {\n\t\t\t//\tstate->data = BoostCounters{\n\t\t\t//\t\t.level = 4,\n\t\t\t//\t\t.boosts = 7,\n\t\t\t//\t\t.thisLevelBoosts = 7,\n\t\t\t//\t\t.nextLevelBoosts = 9,\n\t\t\t//\t\t.mine = 5,\n\t\t\t//\t};\n\t\t\t//} else {\n\t\t\t//\tstate->data = BoostCounters{\n\t\t\t//\t\t.level = 3,\n\t\t\t//\t\t.boosts = 5,\n\t\t\t//\t\t.thisLevelBoosts = 5,\n\t\t\t//\t\t.nextLevelBoosts = 7,\n\t\t\t//\t\t.mine = 3,\n\t\t\t//\t};\n\t\t\t//}\n\t\t\t//return;\n\n\t\t\tboost(crl::guard(box, [=](BoostCounters result) {\n\t\t\t\tstate->submitted = false;\n\n\t\t\t\tif (result.thisLevelBoosts || result.nextLevelBoosts) {\n\t\t\t\t\tif (result.mine > was) {\n\t\t\t\t\t\tStartFireworks(box->parentWidget());\n\t\t\t\t\t}\n\t\t\t\t\tstate->data = result;\n\t\t\t\t}\n\t\t\t}));\n\t\t} else {\n\t\t\tbox->closeBox();\n\t\t}\n\t});\n}\n\nobject_ptr<Ui::RpWidget> MakeLinkLabel(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<QString> text,\n\t\trpl::producer<QString> link,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tobject_ptr<Ui::RpWidget> right) {\n\tauto result = object_ptr<Ui::AbstractButton>(parent);\n\tconst auto raw = result.data();\n\n\tconst auto rawRight = right.release();\n\tif (rawRight) {\n\t\trawRight->setParent(raw);\n\t\trawRight->show();\n\t}\n\n\tstruct State {\n\t\tState(\n\t\t\tnot_null<QWidget*> parent,\n\t\t\trpl::producer<QString> value,\n\t\t\trpl::producer<QString> link)\n\t\t: text(std::move(value))\n\t\t, link(std::move(link))\n\t\t, label(parent, text.value(), st::giveawayGiftCodeLink)\n\t\t, bg(st::roundRadiusLarge, st::windowBgOver) {\n\t\t}\n\n\t\trpl::variable<QString> text;\n\t\trpl::variable<QString> link;\n\t\tUi::FlatLabel label;\n\t\tUi::RoundRect bg;\n\t};\n\n\tconst auto state = raw->lifetime().make_state<State>(\n\t\traw,\n\t\trpl::duplicate(text),\n\t\tstd::move(link));\n\tstate->label.setSelectable(true);\n\n\trpl::combine(\n\t\traw->widthValue(),\n\t\tstd::move(text)\n\t) | rpl::on_next([=](int outer, const auto&) {\n\t\tconst auto textWidth = state->label.textMaxWidth();\n\t\tconst auto skipLeft = st::giveawayGiftCodeLink.margin.left();\n\t\tconst auto skipRight = rawRight\n\t\t\t? rawRight->width()\n\t\t\t: st::giveawayGiftCodeLink.margin.right();\n\t\tconst auto available = outer - skipRight - skipLeft;\n\t\tconst auto use = std::min(textWidth, available);\n\t\tstate->label.resizeToWidth(use);\n\t\tconst auto forCenter = (outer - use) / 2;\n\t\tconst auto x = (forCenter < skipLeft)\n\t\t\t? skipLeft\n\t\t\t: (forCenter > outer - skipRight - use)\n\t\t\t? (outer - skipRight - use)\n\t\t\t: forCenter;\n\t\tstate->label.moveToLeft(x, st::giveawayGiftCodeLink.margin.top());\n\t}, raw->lifetime());\n\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\t\tstate->bg.paint(p, raw->rect());\n\t}, raw->lifetime());\n\n\tstate->label.setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\traw->resize(raw->width(), st::giveawayGiftCodeLinkHeight);\n\tif (rawRight) {\n\t\traw->widthValue() | rpl::on_next([=](int width) {\n\t\t\trawRight->move(width - rawRight->width(), 0);\n\t\t}, raw->lifetime());\n\t}\n\traw->setClickedCallback([=] {\n\t\tQGuiApplication::clipboard()->setText(state->link.current());\n\t\tshow->showToast(tr::lng_username_copied(tr::now));\n\t});\n\n\treturn result;\n}\n\nvoid BoostBoxAlready(not_null<GenericBox*> box, bool group) {\n\tConfirmBox(box, {\n\t\t.text = (group\n\t\t\t? tr::lng_boost_error_already_text_group\n\t\t\t: tr::lng_boost_error_already_text)(tr::rich),\n\t\t.title = tr::lng_boost_error_already_title(),\n\t\t.inform = true,\n\t});\n}\n\nvoid GiftForBoostsBox(\n\t\tnot_null<GenericBox*> box,\n\t\tQString channel,\n\t\tint receive,\n\t\tbool again) {\n\tConfirmBox(box, {\n\t\t.text = (again\n\t\t\t? tr::lng_boost_need_more_again\n\t\t\t: tr::lng_boost_need_more_text)(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(receive) | tr::to_count(),\n\t\t\t\tlt_channel,\n\t\t\t\trpl::single(TextWithEntities{ channel }),\n\t\t\t\ttr::rich),\n\t\t.title = tr::lng_boost_need_more(),\n\t\t.inform = true,\n\t});\n}\n\nvoid GiftedNoBoostsBox(not_null<GenericBox*> box, bool group) {\n\tInformBox(box, {\n\t\t.text = (group\n\t\t\t? tr::lng_boost_error_gifted_text_group\n\t\t\t: tr::lng_boost_error_gifted_text)(tr::rich),\n\t\t.title = tr::lng_boost_error_gifted_title(),\n\t});\n}\n\nvoid PremiumForBoostsBox(\n\t\tnot_null<GenericBox*> box,\n\t\tbool group,\n\t\tFn<void()> buyPremium) {\n\tConfirmBox(box, {\n\t\t.text = (group\n\t\t\t? tr::lng_boost_error_premium_text_group\n\t\t\t: tr::lng_boost_error_premium_text)(tr::rich),\n\t\t.confirmed = buyPremium,\n\t\t.confirmText = tr::lng_boost_error_premium_yes(),\n\t\t.title = tr::lng_boost_error_premium_title(),\n\t});\n}\n\nvoid AskBoostBox(\n\t\tnot_null<GenericBox*> box,\n\t\tAskBoostBoxData data,\n\t\tFn<void()> openStatistics,\n\t\tFn<void()> startGiveaway) {\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setStyle(st::boostBox);\n\tbox->setNoContentMargin(true);\n\tbox->addSkip(st::boxRowPadding.left());\n\n\tFillBoostLimit(\n\t\tBoxShowFinishes(box),\n\t\tbox->verticalLayout(),\n\t\trpl::single(data.boost),\n\t\tst::boxRowPadding);\n\n\tbox->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });\n\n\tauto title = v::match(data.reason.data, [](AskBoostChannelColor) {\n\t\treturn tr::lng_boost_channel_title_color();\n\t}, [](AskBoostAutotranslate) {\n\t\treturn tr::lng_boost_channel_title_autotranslate();\n\t}, [](AskBoostWallpaper) {\n\t\treturn tr::lng_boost_channel_title_wallpaper();\n\t}, [](AskBoostEmojiStatus) {\n\t\treturn tr::lng_boost_channel_title_status();\n\t}, [](AskBoostEmojiPack) {\n\t\treturn tr::lng_boost_group_title_emoji();\n\t}, [](AskBoostCustomReactions) {\n\t\treturn tr::lng_boost_channel_title_reactions();\n\t}, [](AskBoostCpm) {\n\t\treturn tr::lng_boost_channel_title_cpm();\n\t}, [](AskBoostWearCollectible) {\n\t\treturn tr::lng_boost_channel_title_wear();\n\t});\n\tauto isGroup = false;\n\tauto reasonText = v::match(data.reason.data, [&](\n\t\t\tAskBoostChannelColor data) {\n\t\treturn tr::lng_boost_channel_needs_level_color(\n\t\t\tlt_count,\n\t\t\trpl::single(float64(data.requiredLevel)),\n\t\t\ttr::rich);\n\t}, [&](AskBoostAutotranslate data) {\n\t\treturn tr::lng_boost_channel_needs_level_autotranslate(\n\t\t\tlt_count,\n\t\t\trpl::single(float64(data.requiredLevel)),\n\t\t\ttr::rich);\n\t}, [&](AskBoostWallpaper data) {\n\t\tisGroup = data.group;\n\t\treturn (data.group\n\t\t\t? tr::lng_boost_group_needs_level_wallpaper\n\t\t\t: tr::lng_boost_channel_needs_level_wallpaper)(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(data.requiredLevel)),\n\t\t\t\ttr::rich);\n\t}, [&](AskBoostEmojiStatus data) {\n\t\tisGroup = data.group;\n\t\treturn (data.group\n\t\t\t? tr::lng_boost_group_needs_level_status\n\t\t\t: tr::lng_boost_channel_needs_level_status)(\n\t\t\t\tlt_count,\n\t\t\t\trpl::single(float64(data.requiredLevel)),\n\t\t\t\ttr::rich);\n\t}, [&](AskBoostEmojiPack data) {\n\t\tisGroup = true;\n\t\treturn tr::lng_boost_group_needs_level_emoji(\n\t\t\tlt_count,\n\t\t\trpl::single(float64(data.requiredLevel)),\n\t\t\ttr::rich);\n\t}, [&](AskBoostCustomReactions data) {\n\t\treturn tr::lng_boost_channel_needs_level_reactions(\n\t\t\tlt_count,\n\t\t\trpl::single(float64(data.count)),\n\t\t\tlt_same_count,\n\t\t\trpl::single(TextWithEntities{ QString::number(data.count) }),\n\t\t\ttr::rich);\n\t}, [&](AskBoostCpm data) {\n\t\treturn tr::lng_boost_channel_needs_level_cpm(\n\t\t\tlt_count,\n\t\t\trpl::single(float64(data.requiredLevel)),\n\t\t\ttr::rich);\n\t}, [&](AskBoostWearCollectible data) {\n\t\treturn tr::lng_boost_channel_needs_level_wear(\n\t\t\tlt_count,\n\t\t\trpl::single(float64(data.requiredLevel)),\n\t\t\ttr::rich);\n\t});\n\tauto text = rpl::combine(\n\t\tstd::move(reasonText),\n\t\t(isGroup ? tr::lng_boost_group_ask : tr::lng_boost_channel_ask)(\n\t\t\ttr::rich)\n\t) | rpl::map([](TextWithEntities &&text, TextWithEntities &&ask) {\n\t\treturn text.append(u\"\\n\\n\"_q).append(std::move(ask));\n\t});\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(title),\n\t\t\tst::boostCenteredTitle),\n\t\tst::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0),\n\t\tstyle::al_top);\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(text),\n\t\t\tst::boostText),\n\t\t(st::boxRowPadding\n\t\t\t+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)),\n\t\tstyle::al_top);\n\n\tauto stats = object_ptr<Ui::IconButton>(box, st::boostLinkStatsButton);\n\tstats->setClickedCallback(openStatistics);\n\tbox->addRow(MakeLinkLabel(\n\t\tbox,\n\t\trpl::single(data.link),\n\t\trpl::single(data.link),\n\t\tbox->uiShow(),\n\t\tstd::move(stats)));\n\n\tAddFeaturesList(\n\t\tbox->verticalLayout(),\n\t\tdata.features,\n\t\tdata.boost.level + (data.boost.nextLevelBoosts ? 1 : 0),\n\t\tdata.group);\n\n\tauto submit = tr::lng_boost_channel_ask_button();\n\tbox->addButton(rpl::duplicate(submit), [=] {\n\t\tQGuiApplication::clipboard()->setText(data.link);\n\t\tbox->uiShow()->showToast(tr::lng_username_copied(tr::now));\n\t});\n}\n\nvoid FillBoostLimit(\n\t\trpl::producer<> showFinished,\n\t\tnot_null<VerticalLayout*> container,\n\t\trpl::producer<BoostCounters> data,\n\t\tstyle::margins limitLinePadding) {\n\tconst auto addSkip = [&](int skip) {\n\t\tcontainer->add(object_ptr<Ui::FixedHeightWidget>(container, skip));\n\t};\n\n\tconst auto ratio = [=](BoostCounters counters) {\n\t\tconst auto min = counters.thisLevelBoosts;\n\t\tconst auto max = counters.nextLevelBoosts;\n\n\t\tAssert(counters.boosts >= min && counters.boosts <= max);\n\t\tconst auto count = (max - min);\n\t\tconst auto index = (counters.boosts - min);\n\t\tif (!index) {\n\t\t\treturn 0.;\n\t\t} else if (index == count) {\n\t\t\treturn 1.;\n\t\t} else if (count == 2) {\n\t\t\treturn 0.5;\n\t\t}\n\t\tconst auto available = st::boxWideWidth\n\t\t\t- st::boxPadding.left()\n\t\t\t- st::boxPadding.right();\n\t\tconst auto average = available / float64(count);\n\t\tconst auto levelWidth = [&](int add) {\n\t\t\treturn st::normalFont->width(\n\t\t\t\ttr::lng_boost_level(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tcounters.level + add));\n\t\t};\n\t\tconst auto paddings = 2 * st::premiumLineTextSkip;\n\t\tconst auto labelLeftWidth = paddings + levelWidth(0);\n\t\tconst auto labelRightWidth = paddings + levelWidth(1);\n\t\tconst auto first = std::max(average, labelLeftWidth * 1.);\n\t\tconst auto last = std::max(average, labelRightWidth * 1.);\n\t\tconst auto other = (available - first - last) / (count - 2);\n\t\treturn (first + (index - 1) * other) / available;\n\t};\n\n\tauto adjustedData = rpl::duplicate(data) | rpl::map(AdjustByReached);\n\n\tauto bubbleRowState = rpl::duplicate(\n\t\tadjustedData\n\t) | rpl::combine_previous(\n\t\tBoostCounters()\n\t) | rpl::map([=](BoostCounters previous, BoostCounters counters) {\n\t\treturn Premium::BubbleRowState{\n\t\t\t.counter = counters.boosts,\n\t\t\t.ratio = ratio(counters),\n\t\t\t.animateFromZero = (counters.level != previous.level),\n\t\t\t.dynamic = true,\n\t\t};\n\t});\n\tPremium::AddBubbleRow(\n\t\tcontainer,\n\t\tst::boostBubble,\n\t\tstd::move(showFinished),\n\t\trpl::duplicate(bubbleRowState),\n\t\tPremium::BubbleType::Premium,\n\t\tnullptr,\n\t\t&st::premiumIconBoost,\n\t\tlimitLinePadding);\n\taddSkip(st::premiumLineTextSkip);\n\n\tconst auto level = [](int level) {\n\t\treturn tr::lng_boost_level(tr::now, lt_count, level);\n\t};\n\tauto limitState = std::move(\n\t\tbubbleRowState\n\t) | rpl::map([](const Premium::BubbleRowState &state) {\n\t\treturn Premium::LimitRowState{\n\t\t\t.ratio = state.ratio,\n\t\t\t.animateFromZero = state.animateFromZero,\n\t\t\t.dynamic = state.dynamic\n\t\t};\n\t});\n\tauto left = rpl::duplicate(\n\t\tadjustedData\n\t) | rpl::map([=](BoostCounters counters) {\n\t\treturn level(counters.level);\n\t});\n\tauto right = rpl::duplicate(\n\t\tadjustedData\n\t) | rpl::map([=](BoostCounters counters) {\n\t\treturn level(counters.level + 1);\n\t});\n\tPremium::AddLimitRow(\n\t\tcontainer,\n\t\tst::boostLimits,\n\t\tPremium::LimitRowLabels{\n\t\t\t.leftLabel = std::move(left),\n\t\t\t.rightLabel = std::move(right),\n\t\t},\n\t\tstd::move(limitState),\n\t\tlimitLinePadding);\n}\n\nobject_ptr<Ui::FlatLabel> MakeBoostFeaturesBadge(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<QString> text,\n\t\tFn<QBrush(QRect)> bg) {\n\tauto result = object_ptr<Ui::FlatLabel>(\n\t\tparent,\n\t\tstd::move(text),\n\t\tst::boostLevelBadge);\n\tconst auto label = result.data();\n\n\tlabel->show();\n\tlabel->paintRequest() | rpl::on_next([=] {\n\t\tconst auto size = label->textMaxWidth();\n\t\tconst auto rect = QRect(\n\t\t\t(label->width() - size) / 2,\n\t\t\tst::boostLevelBadge.margin.top(),\n\t\t\tsize,\n\t\t\tst::boostLevelBadge.style.font->height\n\t\t).marginsAdded(st::boostLevelBadge.margin);\n\t\tauto p = QPainter(label);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setBrush(bg(rect));\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawRoundedRect(rect, rect.height() / 2., rect.height() / 2.);\n\n\t\tconst auto &lineFg = st::windowBgRipple;\n\t\tconst auto line = st::boostLevelBadgeLine;\n\t\tconst auto top = st::boostLevelBadge.margin.top()\n\t\t\t+ ((st::boostLevelBadge.style.font->height - line) / 2);\n\t\tconst auto left = 0;\n\t\tconst auto skip = st::boostLevelBadgeSkip;\n\t\tif (const auto right = rect.x() - skip; right > left) {\n\t\t\tp.fillRect(left, top, right - left, line, lineFg);\n\t\t}\n\t\tconst auto right = label->width();\n\t\tif (const auto left = rect.x() + rect.width() + skip\n\t\t\t; left < right) {\n\t\t\tp.fillRect(left, top, right - left, line, lineFg);\n\t\t}\n\t}, label->lifetime());\n\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/boost_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\n\nvoid StartFireworks(not_null<QWidget*> parent);\n\nclass Show;\nclass RpWidget;\nclass GenericBox;\nclass VerticalLayout;\nclass FlatLabel;\n\nstruct BoostCounters {\n\tint level = 0;\n\tint boosts = 0;\n\tint thisLevelBoosts = 0;\n\tint nextLevelBoosts = 0; // Zero means no next level is available.\n\tint mine = 0;\n\n\tfriend inline constexpr bool operator==(\n\t\tBoostCounters,\n\t\tBoostCounters) = default;\n};\n\nstruct BoostFeatures {\n\tbase::flat_map<int, int> nameColorsByLevel;\n\tbase::flat_map<int, int> linkStylesByLevel;\n\tbase::flat_map<int, int> profileColorsByLevel;\n\tint linkLogoLevel = 0;\n\tint profileIconLevel = 0;\n\tint autotranslateLevel = 0;\n\tint transcribeLevel = 0;\n\tint emojiPackLevel = 0;\n\tint emojiStatusLevel = 0;\n\tint wallpaperLevel = 0;\n\tint wallpapersCount = 0;\n\tint customWallpaperLevel = 0;\n\tint sponsoredLevel = 0;\n};\n\nstruct BoostBoxData {\n\tQString name;\n\tBoostCounters boost;\n\tBoostFeatures features;\n\tint lifting = 0;\n\tbool allowMulti = false;\n\tbool group = false;\n};\n\nvoid BoostBox(\n\tnot_null<GenericBox*> box,\n\tBoostBoxData data,\n\tFn<void(Fn<void(BoostCounters)>)> boost);\n\nvoid BoostBoxAlready(not_null<GenericBox*> box, bool group);\nvoid GiftForBoostsBox(\n\tnot_null<GenericBox*> box,\n\tQString channel,\n\tint receive,\n\tbool again);\nvoid GiftedNoBoostsBox(not_null<GenericBox*> box, bool group);\nvoid PremiumForBoostsBox(\n\tnot_null<GenericBox*> box,\n\tbool group,\n\tFn<void()> buyPremium);\n\nstruct AskBoostChannelColor {\n\tint requiredLevel = 0;\n};\n\nstruct AskBoostAutotranslate {\n\tint requiredLevel = 0;\n};\n\nstruct AskBoostWallpaper {\n\tint requiredLevel = 0;\n\tbool group = false;\n};\n\nstruct AskBoostEmojiStatus {\n\tint requiredLevel = 0;\n\tbool group = false;\n};\n\nstruct AskBoostEmojiPack {\n\tint requiredLevel = 0;\n};\n\nstruct AskBoostCustomReactions {\n\tint count = 0;\n};\n\nstruct AskBoostCpm {\n\tint requiredLevel = 0;\n};\n\nstruct AskBoostWearCollectible {\n\tint requiredLevel = 0;\n};\n\nstruct AskBoostReason {\n\tstd::variant<\n\t\tAskBoostChannelColor,\n\t\tAskBoostAutotranslate,\n\t\tAskBoostWallpaper,\n\t\tAskBoostEmojiStatus,\n\t\tAskBoostEmojiPack,\n\t\tAskBoostCustomReactions,\n\t\tAskBoostCpm,\n\t\tAskBoostWearCollectible> data;\n};\n\nstruct AskBoostBoxData {\n\tQString link;\n\tBoostCounters boost;\n\tBoostFeatures features;\n\tAskBoostReason reason;\n\tbool group = false;\n};\n\nvoid AskBoostBox(\n\tnot_null<GenericBox*> box,\n\tAskBoostBoxData data,\n\tFn<void()> openStatistics,\n\tFn<void()> startGiveaway);\n\n[[nodiscard]] object_ptr<RpWidget> MakeLinkLabel(\n\tnot_null<QWidget*> parent,\n\trpl::producer<QString> text,\n\trpl::producer<QString> link,\n\tstd::shared_ptr<Show> show,\n\tobject_ptr<RpWidget> right);\n\nvoid FillBoostLimit(\n\trpl::producer<> showFinished,\n\tnot_null<VerticalLayout*> container,\n\trpl::producer<BoostCounters> data,\n\tstyle::margins limitLinePadding);\n\n[[nodiscard]] object_ptr<Ui::FlatLabel> MakeBoostFeaturesBadge(\n\tnot_null<QWidget*> parent,\n\trpl::producer<QString> text,\n\tFn<QBrush(QRect)> bg);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/calendar_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/calendar_box.h\"\n\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/vertical_drum_picker.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/painter.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"lang/lang_keys.h\"\n#include \"base/flat_map.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_layers.h\"\n\n#include <QtCore/QLocale>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kDaysInWeek = 7;\nconstexpr auto kTooltipDelay = crl::time(1000);\nconstexpr auto kJumpDelay = 2 * crl::time(1000);\n\n// QDate -> 0..6\n[[nodiscard]] int DayOfWeekIndex(const QDate &date, int firstDayOfWeek) {\n\treturn (kDaysInWeek + date.dayOfWeek() - firstDayOfWeek) % kDaysInWeek;\n}\n\nvoid FillMonthYearPicker(\n\t\tnot_null<GenericBox*> box,\n\t\tQDate current,\n\t\tQDate minDate,\n\t\tQDate maxDate,\n\t\tconst style::CalendarSizes &st,\n\t\tFn<void(QDate)> done) {\n\tbox->setWidth(st::boxWideWidth);\n\tconst auto content = box->addRow(\n\t\tobject_ptr<FixedHeightWidget>(box, st::settingsWorkingHoursPicker));\n\n\tconst auto font = st::boxTextFont;\n\tconst auto itemHeight = st::settingsWorkingHoursPickerItemHeight;\n\tconst auto picker = [=](\n\t\t\tint count,\n\t\t\tint startIndex,\n\t\t\tFn<void(QPainter&, QRectF, int)> paint) {\n\t\tconst auto result = CreateChild<VerticalDrumPicker>(\n\t\t\tcontent,\n\t\t\tVerticalDrumPicker::DefaultPaintCallback(\n\t\t\t\tfont,\n\t\t\t\titemHeight,\n\t\t\t\tpaint),\n\t\t\tcount,\n\t\t\titemHeight,\n\t\t\tstartIndex);\n\t\tresult->show();\n\t\treturn result;\n\t};\n\n\tconst auto effectiveMaxDate = maxDate.isValid()\n\t\t? maxDate\n\t\t: QDate::currentDate().addDays(365);\n\tconst auto minYear = minDate.isValid() ? minDate.year() : 2013;\n\tconst auto maxYear = effectiveMaxDate.year();\n\tconst auto yearsCount = maxYear - minYear + 1;\n\tconst auto yearsStartIndex = current.year() - minYear;\n\tconst auto yearsPaint = [=](QPainter &p, QRectF rect, int index) {\n\t\tp.drawText(rect, QString::number(minYear + index), style::al_center);\n\t};\n\tconst auto years = picker(yearsCount, yearsStartIndex, yearsPaint);\n\n\tconst auto monthsRange = [=](int year) {\n\t\tauto minMonth = 1;\n\t\tauto maxMonth = 12;\n\t\tif (minDate.isValid() && minDate.year() == year) {\n\t\t\tminMonth = minDate.month();\n\t\t}\n\t\tif (maxDate.isValid() && maxDate.year() == year) {\n\t\t\tmaxMonth = maxDate.month();\n\t\t}\n\t\treturn std::make_pair(minMonth, maxMonth);\n\t};\n\n\tstruct State {\n\t\tbase::unique_qptr<VerticalDrumPicker> months;\n\t\tint currentMinMonth = 0;\n\t\tint currentMaxMonth = 0;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\n\tconst auto [minMonth, maxMonth] = monthsRange(current.year());\n\tconst auto monthsCount = maxMonth - minMonth + 1;\n\tconst auto monthsStartIndex = current.month() - minMonth;\n\tconst auto monthsPaint = [=, minMonth = minMonth](\n\t\t\tQPainter &p,\n\t\t\tQRectF rect,\n\t\t\tint index) {\n\t\tp.drawText(\n\t\t\trect,\n\t\t\tLang::Month(minMonth + index)(tr::now),\n\t\t\tstyle::al_center);\n\t};\n\tstate->months = base::unique_qptr<VerticalDrumPicker>(\n\t\t\tpicker(monthsCount, monthsStartIndex, monthsPaint));\n\tstate->currentMinMonth = minMonth;\n\tstate->currentMaxMonth = maxMonth;\n\n\tyears->value(\n\t) | rpl::skip(1) | rpl::on_next([=](int yearIndex) {\n\t\tconst auto year = minYear + yearIndex;\n\t\tconst auto [newMinMonth, newMaxMonth] = monthsRange(year);\n\n\t\tif (newMinMonth != state->currentMinMonth\n\t\t\t|| newMaxMonth != state->currentMaxMonth) {\n\t\t\tconst auto newMonthsCount = newMaxMonth - newMinMonth + 1;\n\t\t\tconst auto oldMonth = state->currentMinMonth\n\t\t\t\t+ state->months->index();\n\t\t\tconst auto clampedMonth = std::clamp(\n\t\t\t\toldMonth - newMinMonth,\n\t\t\t\t0,\n\t\t\t\tnewMonthsCount - 1);\n\t\t\tconst auto newMonthsPaint = [=, minMonth = newMinMonth](\n\t\t\t\t\tQPainter &p,\n\t\t\t\t\tQRectF rect,\n\t\t\t\t\tint index) {\n\t\t\t\tp.drawText(\n\t\t\t\t\trect,\n\t\t\t\t\tLang::Month(minMonth + index)(tr::now),\n\t\t\t\t\tstyle::al_center);\n\t\t\t};\n\t\t\tstate->months = base::unique_qptr<VerticalDrumPicker>(\n\t\t\t\t\tpicker(newMonthsCount, clampedMonth, newMonthsPaint));\n\t\t\tstate->currentMinMonth = newMinMonth;\n\t\t\tstate->currentMaxMonth = newMaxMonth;\n\t\t\tconst auto s = content->size();\n\t\t\tif (s.isValid()) {\n\t\t\t\tconst auto half = s.width() / 2;\n\t\t\t\tstate->months->setGeometry(0, 0, half, s.height());\n\t\t\t}\n\t\t}\n\t}, box->lifetime());\n\n\tcontent->sizeValue(\n\t) | rpl::on_next([=](QSize s) {\n\t\tconst auto half = s.width() / 2;\n\t\tstate->months->setGeometry(0, 0, half, s.height());\n\t\tyears->setGeometry(half, 0, half, s.height());\n\t}, content->lifetime());\n\n\tcontent->paintRequest(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tauto p = QPainter(content);\n\t\tp.fillRect(r, Qt::transparent);\n\t\tconst auto lineRect = QRect(\n\t\t\t0,\n\t\t\tcontent->height() / 2,\n\t\t\tcontent->width(),\n\t\t\tst::defaultInputField.borderActive);\n\t\tp.fillRect(\n\t\t\tlineRect.translated(0, itemHeight / 2),\n\t\t\tst::activeLineFg);\n\t\tp.fillRect(\n\t\t\tlineRect.translated(0, -itemHeight / 2),\n\t\t\tst::activeLineFg);\n\t}, content->lifetime());\n\n\tbox->addButton(tr::lng_gift_menu_show(), [=] {\n\t\tconst auto year = minYear + years->index();\n\t\tconst auto [minMonth, maxMonth] = monthsRange(year);\n\t\tconst auto month = minMonth + state->months->index();\n\t\tdone(QDate(year, month, 1));\n\t\tbox->closeBox();\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\n} // namespace\n\nclass CalendarBox::Context {\npublic:\n\tContext(QDate month, QDate highlighted);\n\n\tvoid setAllowsSelection(bool allowsSelection);\n\t[[nodiscard]] bool allowsSelection() const {\n\t\treturn _allowsSelection;\n\t}\n\n\tvoid setMinDate(QDate date);\n\tvoid setMaxDate(QDate date);\n\n\t[[nodiscard]] int minDayIndex() const {\n\t\treturn _minDayIndex;\n\t}\n\t[[nodiscard]] int maxDayIndex() const {\n\t\treturn _maxDayIndex;\n\t}\n\n\tvoid skipMonth(int skip);\n\tvoid showMonth(QDate month);\n\t[[nodiscard]] bool showsMonthOf(QDate date) const;\n\n\t[[nodiscard]] int highlightedIndex() const {\n\t\treturn _highlightedIndex;\n\t}\n\t[[nodiscard]] int rowsCount() const {\n\t\treturn _rowsCount;\n\t}\n\t[[nodiscard]] int rowsCountMax() const {\n\t\treturn 6;\n\t}\n\t[[nodiscard]] int daysShift() const {\n\t\treturn _daysShift;\n\t}\n\t[[nodiscard]] int daysCount() const {\n\t\treturn _daysCount;\n\t}\n\t[[nodiscard]] bool isEnabled(int index) const {\n\t\treturn (index >= _minDayIndex) && (index <= _maxDayIndex);\n\t}\n\n\t[[nodiscard]] QDate month() const {\n\t\treturn _month.current();\n\t}\n\t[[nodiscard]] rpl::producer<QDate> monthValue() const {\n\t\treturn _month.value();\n\t}\n\t[[nodiscard]] int firstDayOfWeek() const {\n\t\treturn _firstDayOfWeek;\n\t}\n\n\t[[nodiscard]] QDate dateFromIndex(int index) const;\n\t[[nodiscard]] QString labelFromIndex(int index) const;\n\n\tvoid toggleSelectionMode(bool enabled);\n\t[[nodiscard]] bool selectionMode() const;\n\t[[nodiscard]] rpl::producer<> selectionUpdates() const;\n\t[[nodiscard]] std::optional<int> selectedMin() const;\n\t[[nodiscard]] std::optional<int> selectedMax() const;\n\n\tvoid startSelection(int index);\n\tvoid updateSelection(int index);\n\nprivate:\n\tstruct Selection {\n\t\tQDate min;\n\t\tQDate max;\n\t\tint minIndex = 0;\n\t\tint maxIndex = 0;\n\t};\n\tvoid applyMonth(const QDate &month, bool forced = false);\n\n\tstatic int DaysShiftForMonth(\n\t\tconst QDate &month,\n\t\tQDate min,\n\t\tint firstDayOfWeek);\n\tstatic int RowsCountForMonth(\n\t\tconst QDate &month,\n\t\tQDate min,\n\t\tQDate max,\n\t\tint firstDayOfWeek);\n\n\tconst int _firstDayOfWeek = 0;\n\tbool _allowsSelection = false;\n\n\trpl::variable<QDate> _month;\n\tQDate _min, _max;\n\tQDate _highlighted;\n\tFn<QString(int)> _dayOfWeek;\n\tFn<QString(int, int)> _monthOfYear;\n\n\tint _highlightedIndex = 0;\n\tint _minDayIndex = 0;\n\tint _maxDayIndex = 0;\n\tint _daysCount = 0;\n\tint _daysShift = 0;\n\tint _rowsCount = 0;\n\n\tSelection _selection;\n\tQDate _selectionStart;\n\tint _selectionStartIndex = 0;\n\trpl::event_stream<> _selectionUpdates;\n\tbool _selectionMode = false;\n\n};\n\nCalendarBox::Context::Context(QDate month, QDate highlighted)\n: _firstDayOfWeek(static_cast<int>(QLocale().firstDayOfWeek())) // 1..7\n, _highlighted(highlighted) {\n\tshowMonth(month);\n}\n\nvoid CalendarBox::Context::setAllowsSelection(bool allows) {\n\t_allowsSelection = allows;\n}\n\nvoid CalendarBox::Context::setMinDate(QDate date) {\n\t_min = date;\n\tapplyMonth(_month.current(), true);\n}\n\nvoid CalendarBox::Context::setMaxDate(QDate date) {\n\t_max = date;\n\tapplyMonth(_month.current(), true);\n}\n\nvoid CalendarBox::Context::showMonth(QDate month) {\n\tif (month.day() != 1) {\n\t\tmonth = QDate(month.year(), month.month(), 1);\n\t}\n\tapplyMonth(month);\n}\n\nbool CalendarBox::Context::showsMonthOf(QDate date) const {\n\tconst auto shown = _month.current();\n\treturn (shown.year() == date.year()) && (shown.month() == date.month());\n}\n\nvoid CalendarBox::Context::applyMonth(const QDate &month, bool forced) {\n\tconst auto was = _month.current();\n\t_daysCount = month.daysInMonth();\n\t_daysShift = DaysShiftForMonth(month, _min, _firstDayOfWeek);\n\t_rowsCount = RowsCountForMonth(month, _min, _max, _firstDayOfWeek);\n\t_highlightedIndex = month.daysTo(_highlighted);\n\t_minDayIndex = _min.isNull() ? INT_MIN : month.daysTo(_min);\n\t_maxDayIndex = _max.isNull() ? INT_MAX : month.daysTo(_max);\n\tconst auto shift = was.isNull() ? 0 : month.daysTo(was);\n\tauto updated = false;\n\tconst auto update = [&](const QDate &date, int &index) {\n\t\tif (shift && !date.isNull()) {\n\t\t\tindex += shift;\n\t\t}\n\t};\n\tupdate(_selection.min, _selection.minIndex);\n\tupdate(_selection.max, _selection.maxIndex);\n\tupdate(_selectionStart, _selectionStartIndex);\n\tif (forced) {\n\t\t_month.force_assign(month);\n\t} else {\n\t\t_month = month;\n\t}\n\tif (updated) {\n\t\t_selectionUpdates.fire({});\n\t}\n}\n\nvoid CalendarBox::Context::skipMonth(int skip) {\n\tauto year = _month.current().year();\n\tauto month = _month.current().month();\n\tmonth += skip;\n\twhile (month < 1) {\n\t\t--year;\n\t\tmonth += 12;\n\t}\n\twhile (month > 12) {\n\t\t++year;\n\t\tmonth -= 12;\n\t}\n\tshowMonth(QDate(year, month, 1));\n}\n\nint CalendarBox::Context::DaysShiftForMonth(\n\t\tconst QDate &month,\n\t\tQDate min,\n\t\tint firstDayOfWeek) {\n\tExpects(!month.isNull());\n\n\tconstexpr auto kMaxRows = 6;\n\tconst auto inMonthIndex = month.day() - 1;\n\tconst auto inWeekIndex = DayOfWeekIndex(month, firstDayOfWeek);\n\tconst auto from = ((kMaxRows * kDaysInWeek) + inWeekIndex - inMonthIndex)\n\t\t% kDaysInWeek;\n\tif (min.isNull()) {\n\t\tmin = month.addYears(-1);\n\t} else if (min >= month) {\n\t\treturn from;\n\t}\n\tif (min.day() != 1) {\n\t\tmin = QDate(min.year(), min.month(), 1);\n\t}\n\tconst auto add = min.daysTo(month)\n\t\t- inWeekIndex\n\t\t+ DayOfWeekIndex(min, firstDayOfWeek);\n\treturn from + add;\n}\n\nint CalendarBox::Context::RowsCountForMonth(\n\t\tconst QDate &month,\n\t\tQDate min,\n\t\tQDate max,\n\t\tint firstDayOfWeek) {\n\tExpects(!month.isNull());\n\n\tconst auto daysShift = DaysShiftForMonth(month, min, firstDayOfWeek);\n\tconst auto daysCount = month.daysInMonth();\n\tconst auto cellsCount = daysShift + daysCount;\n\tauto result = (cellsCount / kDaysInWeek);\n\tif (cellsCount % kDaysInWeek) {\n\t\t++result;\n\t}\n\tif (max.isNull()) {\n\t\tmax = month.addYears(1);\n\t}\n\tif (max < month.addMonths(1)) {\n\t\treturn result;\n\t}\n\tif (max.day() != 1) {\n\t\tmax = QDate(max.year(), max.month(), 1);\n\t}\n\tmax = max.addMonths(1);\n\tmax = max.addDays(-DayOfWeekIndex(max, firstDayOfWeek));\n\tconst auto cellsFull = daysShift + (month.day() - 1) + month.daysTo(max);\n\treturn cellsFull / kDaysInWeek;\n}\n\nQDate CalendarBox::Context::dateFromIndex(int index) const {\n\tconstexpr auto kMonthsCount = 12;\n\tauto month = _month.current().month();\n\tauto year = _month.current().year();\n\twhile (index < 0) {\n\t\tif (!--month) {\n\t\t\tmonth += kMonthsCount;\n\t\t\t--year;\n\t\t}\n\t\tindex += QDate(year, month, 1).daysInMonth();\n\t}\n\tfor (auto maxIndex = QDate(year, month, 1).daysInMonth(); index >= maxIndex; maxIndex = QDate(year, month, 1).daysInMonth()) {\n\t\tindex -= maxIndex;\n\t\tif (month++ == kMonthsCount) {\n\t\t\tmonth -= kMonthsCount;\n\t\t\t++year;\n\t\t}\n\t}\n\treturn QDate(year, month, index + 1);\n}\n\nQString CalendarBox::Context::labelFromIndex(int index) const {\n\tauto day = [this, index] {\n\t\tif (index >= 0 && index < daysCount()) {\n\t\t\treturn index + 1;\n\t\t}\n\t\treturn dateFromIndex(index).day();\n\t};\n\treturn QString::number(day());\n}\n\nvoid CalendarBox::Context::toggleSelectionMode(bool enabled) {\n\tif (_selectionMode == enabled) {\n\t\treturn;\n\t}\n\t_selectionMode = enabled;\n\t_selectionStart = {};\n\t_selection = {};\n\t_selectionUpdates.fire({});\n}\n\nbool CalendarBox::Context::selectionMode() const {\n\treturn _selectionMode;\n}\n\nrpl::producer<> CalendarBox::Context::selectionUpdates() const {\n\treturn _selectionUpdates.events();\n}\n\nstd::optional<int> CalendarBox::Context::selectedMin() const {\n\treturn _selection.min.isNull()\n\t\t? std::optional<int>()\n\t\t: _selection.minIndex;\n}\n\nstd::optional<int> CalendarBox::Context::selectedMax() const {\n\treturn _selection.max.isNull()\n\t\t? std::optional<int>()\n\t\t: _selection.maxIndex;\n}\n\nvoid CalendarBox::Context::startSelection(int index) {\n\tExpects(_selectionMode);\n\n\tif (!_selectionStart.isNull() && _selectionStartIndex == index) {\n\t\treturn;\n\t}\n\t_selectionStartIndex = index;\n\t_selectionStart = dateFromIndex(index);\n\tupdateSelection(index);\n}\n\nvoid CalendarBox::Context::updateSelection(int index) {\n\tExpects(_selectionMode);\n\tExpects(!_selectionStart.isNull());\n\n\tindex = std::clamp(index, minDayIndex(), maxDayIndex());\n\tconst auto start = _selectionStartIndex;\n\tconst auto min = std::min(index, start);\n\tconst auto max = std::max(index, start);\n\tif (!_selection.min.isNull()\n\t\t&& _selection.minIndex == min\n\t\t&& !_selection.max.isNull()\n\t\t&& _selection.maxIndex == max) {\n\t\treturn;\n\t}\n\t_selection = Selection{\n\t\t.min = dateFromIndex(min),\n\t\t.max = dateFromIndex(max),\n\t\t.minIndex = min,\n\t\t.maxIndex = max,\n\t};\n\t_selectionUpdates.fire({});\n}\n\nclass CalendarBox::Inner final : public RpWidget {\npublic:\n\tInner(\n\t\tQWidget *parent,\n\t\tnot_null<Context*> context,\n\t\tconst style::CalendarSizes &st,\n\t\tconst style::CalendarColors &styleColors,\n\t\tFn<void(QDate, CalendarImageSetter)> dynamicImageForDate);\n\n\t[[nodiscard]] int countMaxHeight() const;\n\tvoid setDateChosenCallback(Fn<void(QDate)> callback);\n\tvoid setDynamicImage(QDate date, std::shared_ptr<DynamicImage> image);\n\tvoid setRequireImage(bool require);\n\n\t~Inner();\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\nprivate:\n\tvoid monthChanged(QDate month);\n\tvoid setSelected(int selected);\n\tvoid setPressed(int pressed);\n\tvoid loadDynamicImages();\n\n\tint rowsLeft() const;\n\tint rowsTop() const;\n\tvoid resizeToCurrent();\n\tvoid paintRows(QPainter &p, QRect clip);\n\n\tconst style::CalendarSizes &_st;\n\tconst style::CalendarColors &_styleColors;\n\tconst not_null<Context*> _context;\n\tbool _twoPressSelectionStarted = false;\n\tbool _requireImage = false;\n\tFn<void(QDate, CalendarImageSetter)> _dynamicImageForDate;\n\n\tstruct DynamicImageState {\n\t\tstd::shared_ptr<DynamicImage> image;\n\t\tbool requested = false;\n\t\tbool animationFinished = false;\n\t\tanim::value animation;\n\t\tcrl::time animationStart = 0;\n\n\t\t[[nodiscard]] bool animating() const {\n\t\t\treturn animationStart > 0;\n\t\t}\n\t};\n\n\tstd::map<int, std::unique_ptr<RippleAnimation>> _ripples;\n\tbase::flat_map<QDate, DynamicImageState> _dynamicImageStates;\n\tUi::Animations::Basic _animation;\n\n\tFn<void(QDate)> _dateChosenCallback;\n\n\tstatic constexpr auto kEmptySelection = INT_MIN / 2;\n\tint _selected = kEmptySelection;\n\tint _pressed = kEmptySelection;\n\tbool _pointerCursor = false;\n\tbool _cursorSetWithoutMouseMove = false;\n\n\tQPoint _lastGlobalPosition;\n\tbool _mouseMoved = false;\n\n};\n\nclass CalendarBox::FloatingDate final {\npublic:\n\tFloatingDate(QWidget *parent, not_null<Context*> context);\n\n\t[[nodiscard]] rpl::producer<int> widthValue() const;\n\tvoid move(int x, int y);\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tvoid paint();\n\n\tconst not_null<Context*> _context;\n\tRpWidget _widget;\n\tCornersPixmaps _corners;\n\tQString _text;\n\n};\n\nCalendarBox::FloatingDate::FloatingDate(\n\tQWidget *parent,\n\tnot_null<Context*> context)\n: _context(context)\n, _widget(parent)\n, _corners(\n\tPrepareCornerPixmaps(\n\t\tHistoryServiceMsgRadius(),\n\t\tst::roundedBg)) {\n\t_context->monthValue(\n\t) | rpl::on_next([=](QDate month) {\n\t\t_text = langMonthOfYearFull(month.month(), month.year());\n\t\tconst auto width = st::msgServiceFont->width(_text);\n\t\tconst auto rect = QRect(0, 0, width, st::msgServiceFont->height);\n\t\t_widget.resize(rect.marginsAdded(st::msgServicePadding).size());\n\t\t_widget.update();\n\t}, _widget.lifetime());\n\n\t_widget.paintRequest(\n\t) | rpl::on_next([=] {\n\t\tpaint();\n\t}, _widget.lifetime());\n\n\t_widget.setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_widget.show();\n}\n\nrpl::producer<int> CalendarBox::FloatingDate::widthValue() const {\n\treturn _widget.widthValue();\n}\n\nvoid CalendarBox::FloatingDate::move(int x, int y) {\n\t_widget.move(x, y);\n}\n\nrpl::lifetime &CalendarBox::FloatingDate::lifetime() {\n\treturn _widget.lifetime();\n}\n\nvoid CalendarBox::FloatingDate::paint() {\n\tauto p = QPainter(&_widget);\n\n\tFillRoundRect(p, _widget.rect(), st::roundedBg, _corners);\n\n\tp.setFont(st::msgServiceFont);\n\tp.setPen(st::roundedFg);\n\tp.drawText(\n\t\tst::msgServicePadding.left(),\n\t\tst::msgServicePadding.top() + st::msgServiceFont->ascent,\n\t\t_text);\n}\n\nCalendarBox::Inner::Inner(\n\tQWidget *parent,\n\tnot_null<Context*> context,\n\tconst style::CalendarSizes &st,\n\tconst style::CalendarColors &styleColors,\n\tFn<void(QDate, CalendarImageSetter)> dynamicImageForDate)\n: RpWidget(parent)\n, _st(st)\n, _styleColors(styleColors)\n, _context(context)\n, _dynamicImageForDate(std::move(dynamicImageForDate))\n, _animation([=](crl::time now) {\n\tauto animating = false;\n\tfor (auto &[date, state] : _dynamicImageStates) {\n\t\tif (!state.animating()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto dt = std::clamp(\n\t\t\t(now - state.animationStart) / float64(st::fadeWrapDuration),\n\t\t\t0.,\n\t\t\t1.);\n\t\tstate.animation.update(dt, anim::linear);\n\t\tif (dt >= 1.) {\n\t\t\tstate.animationStart = 0;\n\t\t\tstate.animationFinished = true;\n\t\t} else {\n\t\t\tanimating = true;\n\t\t}\n\t}\n\tupdate();\n\treturn animating;\n}) {\n\tsetMouseTracking(true);\n\n\tcontext->monthValue(\n\t) | rpl::on_next([=](QDate month) {\n\t\tmonthChanged(month);\n\t}, lifetime());\n\n\tcontext->selectionUpdates(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n}\n\nvoid CalendarBox::Inner::monthChanged(QDate month) {\n\tsetSelected(kEmptySelection);\n\t_ripples.clear();\n\tfor (auto &[date, state] : _dynamicImageStates) {\n\t\tstate.requested = false;\n\t}\n\tloadDynamicImages();\n\tresizeToCurrent();\n\tupdate();\n\tSendSynteticMouseEvent(this, QEvent::MouseMove, Qt::NoButton);\n}\n\nvoid CalendarBox::Inner::loadDynamicImages() {\n\tif (!_dynamicImageForDate) {\n\t\treturn;\n\t}\n\tconst auto currentMonth = _context->month();\n\tconst auto prevMonth = currentMonth.addMonths(-1);\n\tconst auto nextMonth = currentMonth.addMonths(1);\n\n\tconst auto matchesMonth = [&](const QDate &d, const QDate &month) {\n\t\treturn d.year() == month.year() && d.month() == month.month();\n\t};\n\tconst auto from = -_context->daysShift();\n\tconst auto till = from + _context->rowsCount() * kDaysInWeek;\n\tfor (auto i = from; i != till; ++i) {\n\t\tconst auto date = _context->dateFromIndex(i);\n\t\tif (matchesMonth(date, currentMonth)\n\t\t\t|| matchesMonth(date, prevMonth)\n\t\t\t|| matchesMonth(date, nextMonth)) {\n\t\t\tauto &state = _dynamicImageStates[date];\n\t\t\tif (state.image || state.requested) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tstate.requested = true;\n\t\t\t_dynamicImageForDate(\n\t\t\t\tdate,\n\t\t\t\t[=](QDate imageDate, std::shared_ptr<DynamicImage> image) {\n\t\t\t\t\tsetDynamicImage(imageDate, std::move(image));\n\t\t\t\t});\n\t\t}\n\t}\n}\n\nvoid CalendarBox::Inner::resizeToCurrent() {\n\tconst auto height = _context->rowsCount() * _st.cellSize.height();\n\tresize(_st.width, _st.padding.top() + height + _st.padding.bottom());\n}\n\nvoid CalendarBox::Inner::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tauto clip = e->rect();\n\n\tpaintRows(p, clip);\n}\n\nint CalendarBox::Inner::rowsLeft() const {\n\treturn _st.padding.left();\n}\n\nint CalendarBox::Inner::rowsTop() const {\n\treturn _st.padding.top();\n}\n\nvoid CalendarBox::Inner::paintRows(QPainter &p, QRect clip) {\n\tp.setFont(st::calendarDaysFont);\n\tauto y = rowsTop();\n\tauto index = -_context->daysShift();\n\tconst auto selectionMode = _context->selectionMode();\n\tconst auto impossible = index - 45;\n\tconst auto selectedMin = _context->selectedMin().value_or(impossible);\n\tconst auto selectedMax = _context->selectedMax().value_or(impossible);\n\tconst auto highlightedIndex = selectionMode\n\t\t? impossible\n\t\t: _context->highlightedIndex();\n\tconst auto daysCount = _context->daysCount();\n\tconst auto rowsCount = _context->rowsCount();\n\tconst auto rowHeight = _st.cellSize.height();\n\tconst auto fromRow = std::max(clip.y() - y, 0) / rowHeight;\n\tconst auto tillRow = std::min(\n\t\t(clip.y() + clip.height() + rowHeight - 1) / rowHeight,\n\t\trowsCount);\n\ty += fromRow * rowHeight;\n\tindex += fromRow * kDaysInWeek;\n\tconst auto innerSkipLeft = (_st.cellSize.width() - _st.cellInner) / 2;\n\tconst auto innerSkipTop = (_st.cellSize.height() - _st.cellInner) / 2;\n\tfor (auto row = fromRow; row != tillRow; ++row, y += rowHeight) {\n\t\tauto x = rowsLeft();\n\t\tconst auto fromIndex = index;\n\t\tconst auto tillIndex = (index + kDaysInWeek);\n\t\tconst auto selectedFrom = std::max(fromIndex, selectedMin);\n\t\tconst auto selectedTill = std::min(tillIndex, selectedMax + 1);\n\t\tconst auto selectedInRow = (selectedTill - selectedFrom);\n\t\tif (selectedInRow > 0) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(st::activeButtonBg);\n\t\t\tp.drawRoundedRect(\n\t\t\t\t(x\n\t\t\t\t\t+ (selectedFrom - index) * _st.cellSize.width()\n\t\t\t\t\t+ innerSkipLeft\n\t\t\t\t\t- st::lineWidth),\n\t\t\t\ty + innerSkipTop - st::lineWidth,\n\t\t\t\t((selectedInRow - 1) * _st.cellSize.width()\n\t\t\t\t\t+ 2 * st::lineWidth\n\t\t\t\t\t+ _st.cellInner),\n\t\t\t\t_st.cellInner + 2 * st::lineWidth,\n\t\t\t\t(_st.cellInner / 2.) + st::lineWidth,\n\t\t\t\t(_st.cellInner / 2.) + st::lineWidth);\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t}\n\t\tfor (auto col = 0; col != kDaysInWeek; ++col, ++index, x += _st.cellSize.width()) {\n\t\t\tconst auto rect = myrtlrect(x, y, _st.cellSize.width(), _st.cellSize.height());\n\t\t\tconst auto selected = (index >= selectedMin) && (index <= selectedMax);\n\t\t\tconst auto grayedOut = !selected && (index < 0 || index >= daysCount);\n\t\t\tconst auto highlighted = (index == highlightedIndex);\n\t\t\tconst auto enabled = _context->isEnabled(index);\n\t\t\tconst auto innerLeft = x + innerSkipLeft;\n\t\t\tconst auto innerTop = y + innerSkipTop;\n\t\t\tconst auto date = _context->dateFromIndex(index);\n\t\t\tauto dynamicImageProgress = -1.;\n\t\t\tif (const auto it = _dynamicImageStates.find(date);\n\t\t\t\t\tit != end(_dynamicImageStates)) {\n\t\t\t\tauto &state = it->second;\n\t\t\t\tif (state.image) {\n\t\t\t\t\tif (!state.animating() && !state.animationFinished) {\n\t\t\t\t\t\tstate.animation = anim::value(0., 1.);\n\t\t\t\t\t\tstate.animationStart = crl::now();\n\t\t\t\t\t\tif (!_animation.animating()) {\n\t\t\t\t\t\t\t_animation.start();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tauto image = state.image->image(_st.cellInner);\n\t\t\t\t\tif (!image.isNull()) {\n\t\t\t\t\t\tconst auto opacity = grayedOut ? 0.5 : 1.;\n\t\t\t\t\t\tconst auto alpha = state.animating()\n\t\t\t\t\t\t\t? state.animation.current()\n\t\t\t\t\t\t\t: 1.;\n\t\t\t\t\t\tdynamicImageProgress = alpha;\n\t\t\t\t\t\tif (alpha > 0.) {\n\t\t\t\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\t\t\t\tp.setOpacity(alpha * opacity);\n\t\t\t\t\t\t\tconst auto imgRect = myrtlrect(\n\t\t\t\t\t\t\t\tinnerLeft,\n\t\t\t\t\t\t\t\tinnerTop,\n\t\t\t\t\t\t\t\t_st.cellInner,\n\t\t\t\t\t\t\t\t_st.cellInner);\n\t\t\t\t\t\t\tp.drawImage(\n\t\t\t\t\t\t\t\timgRect,\n\t\t\t\t\t\t\t\tImages::Circle(std::move(image)));\n\t\t\t\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\t\t\t\tp.setBrush(st::songCoverOverlayFg);\n\t\t\t\t\t\t\tp.drawEllipse(imgRect);\n\t\t\t\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t\t\t\t\tp.setOpacity(1.);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (highlighted) {\n\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(grayedOut ? st::windowBgOver : st::dialogsBgActive);\n\t\t\t\tp.drawEllipse(myrtlrect(innerLeft, innerTop, _st.cellInner, _st.cellInner));\n\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t}\n\t\t\tconst auto it = _ripples.find(index);\n\t\t\tif (it != _ripples.cend() && !selectionMode) {\n\t\t\t\tconst auto colorOverride = ((dynamicImageProgress != -1)\n\t\t\t\t\t? st::shadowFg\n\t\t\t\t\t: !highlighted\n\t\t\t\t\t? _styleColors.rippleColor\n\t\t\t\t\t: grayedOut\n\t\t\t\t\t? _styleColors.rippleGrayedOutColor\n\t\t\t\t\t: _styleColors.rippleColorHighlighted)->c;\n\t\t\t\tit->second->paint(p, innerLeft, innerTop, width(), &colorOverride);\n\t\t\t\tif (it->second->empty()) {\n\t\t\t\t\t_ripples.erase(it);\n\t\t\t\t}\n\t\t\t}\n\t\t\tp.setPen(selected\n\t\t\t\t? st::activeButtonFg\n\t\t\t\t: highlighted\n\t\t\t\t? (grayedOut\n\t\t\t\t\t? _styleColors.dayTextGrayedOutColor\n\t\t\t\t\t: st::dialogsNameFgActive)\n\t\t\t\t: enabled\n\t\t\t\t? (grayedOut\n\t\t\t\t\t? _styleColors.dayTextGrayedOutColor\n\t\t\t\t\t: _styleColors.dayTextColor)\n\t\t\t\t: st::windowSubTextFg);\n\t\t\tif (dynamicImageProgress != -1) {\n\t\t\t\tauto pen = p.pen();\n\t\t\t\tpen.setColor(\n\t\t\t\t\tanim::color(\n\t\t\t\t\t\tpen.color(),\n\t\t\t\t\t\tst::activeButtonFg->c,\n\t\t\t\t\t\tdynamicImageProgress));\n\t\t\t\tp.setPen(std::move(pen));\n\t\t\t}\n\t\t\tp.drawText(rect, _context->labelFromIndex(index), style::al_center);\n\t\t}\n\t}\n}\n\nvoid CalendarBox::Inner::mouseMoveEvent(QMouseEvent *e) {\n\tconst auto globalPosition = e->globalPos();\n\t_mouseMoved = (_lastGlobalPosition != globalPosition);\n\t_lastGlobalPosition = globalPosition;\n\n\tconst auto size = _st.cellSize;\n\tconst auto point = e->pos();\n\tconst auto inner = QRect(\n\t\trowsLeft(),\n\t\trowsTop(),\n\t\tkDaysInWeek * size.width(),\n\t\t_context->rowsCount() * size.height());\n\tif (inner.contains(point)) {\n\t\tconst auto row = (point.y() - rowsTop()) / size.height();\n\t\tconst auto col = (point.x() - rowsLeft()) / size.width();\n\t\tconst auto index = row * kDaysInWeek + col - _context->daysShift();\n\t\tsetSelected(index);\n\t} else {\n\t\tsetSelected(kEmptySelection);\n\t}\n\tif (_pressed != kEmptySelection && _context->selectionMode()) {\n\t\tconst auto row = (point.y() >= rowsTop())\n\t\t\t? (point.y() - rowsTop()) / size.height()\n\t\t\t: -1;\n\t\tconst auto col = (point.y() < rowsTop())\n\t\t\t? 0\n\t\t\t: (point.x() >= rowsLeft())\n\t\t\t? std::min(\n\t\t\t\t(point.x() - rowsLeft()) / size.width(),\n\t\t\t\tkDaysInWeek - 1)\n\t\t\t: 0;\n\t\tconst auto index = row * kDaysInWeek + col - _context->daysShift();\n\t\t_context->updateSelection(index);\n\t}\n}\n\nvoid CalendarBox::Inner::setSelected(int selected) {\n\tif (selected != kEmptySelection && !_context->isEnabled(selected)) {\n\t\tselected = kEmptySelection;\n\t}\n\tif (selected != kEmptySelection && _requireImage) {\n\t\tconst auto date = _context->dateFromIndex(selected);\n\t\tconst auto it = _dynamicImageStates.find(date);\n\t\tif (it == end(_dynamicImageStates) || !it->second.image) {\n\t\t\tselected = kEmptySelection;\n\t\t}\n\t}\n\t_selected = selected;\n\tconst auto pointer = (_selected != kEmptySelection);\n\tconst auto force = (_mouseMoved && _cursorSetWithoutMouseMove);\n\tif (_pointerCursor != pointer || force) {\n\t\tif (force) {\n\t\t\t// Workaround some strange bug. When I call setCursor while\n\t\t\t// scrolling by touchpad the new cursor is not applied and\n\t\t\t// then it is not applied until it is changed.\n\t\t\tsetCursor(pointer ? style::cur_default : style::cur_pointer);\n\t\t}\n\t\tsetCursor(pointer ? style::cur_pointer : style::cur_default);\n\t\t_cursorSetWithoutMouseMove = !_mouseMoved;\n\t\t_pointerCursor = pointer;\n\t}\n\t_mouseMoved = false;\n}\n\nvoid CalendarBox::Inner::mousePressEvent(QMouseEvent *e) {\n\tsetPressed(_selected);\n\tif (_selected != kEmptySelection) {\n\t\tauto index = _selected + _context->daysShift();\n\t\tAssert(index >= 0);\n\n\t\tauto row = index / kDaysInWeek;\n\t\tauto col = index % kDaysInWeek;\n\t\tauto cell = QRect(rowsLeft() + col * _st.cellSize.width(), rowsTop() + row * _st.cellSize.height(), _st.cellSize.width(), _st.cellSize.height());\n\t\tauto it = _ripples.find(_selected);\n\t\tif (it == _ripples.cend()) {\n\t\t\tauto mask = RippleAnimation::EllipseMask(QSize(_st.cellInner, _st.cellInner));\n\t\t\tauto update = [this, cell] { rtlupdate(cell); };\n\t\t\tit = _ripples.emplace(_selected, std::make_unique<RippleAnimation>(st::defaultRippleAnimation, std::move(mask), std::move(update))).first;\n\t\t}\n\t\tauto ripplePosition = QPoint(cell.x() + (_st.cellSize.width() - _st.cellInner) / 2, cell.y() + (_st.cellSize.height() - _st.cellInner) / 2);\n\t\tit->second->add(e->pos() - ripplePosition);\n\n\t\tif (_context->selectionMode()) {\n\t\t\tif (_context->selectedMin().has_value()\n\t\t\t\t&& ((e->modifiers() & Qt::ShiftModifier)\n\t\t\t\t\t|| (_twoPressSelectionStarted\n\t\t\t\t\t\t&& (_context->selectedMin()\n\t\t\t\t\t\t\t== _context->selectedMax())))) {\n\t\t\t\t_context->updateSelection(_selected);\n\t\t\t\t_twoPressSelectionStarted = false;\n\t\t\t} else {\n\t\t\t\t_context->startSelection(_selected);\n\t\t\t\t_twoPressSelectionStarted = true;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid CalendarBox::Inner::mouseReleaseEvent(QMouseEvent *e) {\n\tauto pressed = _pressed;\n\tsetPressed(kEmptySelection);\n\tif (pressed != kEmptySelection\n\t\t&& pressed == _selected\n\t\t&& !_context->selectionMode()) {\n\t\tcrl::on_main(this, [=] {\n\t\t\tconst auto onstack = _dateChosenCallback;\n\t\t\tonstack(_context->dateFromIndex(pressed));\n\t\t});\n\t}\n}\n\nvoid CalendarBox::Inner::setPressed(int pressed) {\n\tif (_pressed != pressed) {\n\t\tif (_pressed != kEmptySelection) {\n\t\t\tauto it = _ripples.find(_pressed);\n\t\t\tif (it != _ripples.cend()) {\n\t\t\t\tit->second->lastStop();\n\t\t\t}\n\t\t}\n\t\t_pressed = pressed;\n\t}\n}\n\nint CalendarBox::Inner::countMaxHeight() const {\n\tconst auto innerHeight = _context->rowsCountMax() * _st.cellSize.height();\n\treturn _st.padding.top()\n\t\t+ innerHeight\n\t\t+ _st.padding.bottom();\n}\n\nvoid CalendarBox::Inner::setDateChosenCallback(Fn<void(QDate)> callback) {\n\t_dateChosenCallback = std::move(callback);\n}\n\nvoid CalendarBox::Inner::setDynamicImage(\n\t\tQDate date,\n\t\tstd::shared_ptr<DynamicImage> image) {\n\tauto &state = _dynamicImageStates[date];\n\tif (image) {\n\t\tstate.image = std::move(image);\n\t\tstate.image->subscribeToUpdates([=] { update(); });\n\t} else {\n\t\t_dynamicImageStates.remove(date);\n\t}\n\tupdate();\n}\n\nvoid CalendarBox::Inner::setRequireImage(bool require) {\n\t_requireImage = require;\n}\n\nCalendarBox::Inner::~Inner() = default;\n\nclass CalendarBox::Title final : public AbstractButton {\npublic:\n\tTitle(\n\t\tQWidget *parent,\n\t\tnot_null<Context*> context,\n\t\tconst style::CalendarSizes &st,\n\t\tconst style::CalendarColors &styleColors);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e);\n\nprivate:\n\tvoid setTextFromMonth(QDate month);\n\tvoid setText(QString text);\n\tvoid paintDayNames(Painter &p, QRect clip);\n\n\tconst style::CalendarSizes &_st;\n\tconst style::CalendarColors &_styleColors;\n\tconst not_null<Context*> _context;\n\n\tQString _text;\n\tint _textWidth = 0;\n\tint _textLeft = 0;\n\n};\n\nCalendarBox::Title::Title(\n\tQWidget *parent,\n\tnot_null<Context*> context,\n\tconst style::CalendarSizes &st,\n\tconst style::CalendarColors &styleColors)\n: AbstractButton(parent)\n, _st(st)\n, _styleColors(styleColors)\n, _context(context) {\n\tconst auto dayWidth = st::calendarDaysFont->width(langDayOfWeek(1));\n\t_textLeft = _st.padding.left() + (_st.cellSize.width() - dayWidth) / 2;\n\n\t_context->monthValue(\n\t) | rpl::filter([=] {\n\t\treturn !_context->selectionMode();\n\t}) | rpl::on_next([=](QDate date) {\n\t\tsetTextFromMonth(date);\n\t}, lifetime());\n\n\t_context->selectionUpdates(\n\t) | rpl::on_next([=] {\n\t\tif (!_context->selectionMode()) {\n\t\t\tsetTextFromMonth(_context->month());\n\t\t\tsetAttribute(Qt::WA_TransparentForMouseEvents, false);\n\t\t} else {\n\t\t\tsetAttribute(Qt::WA_TransparentForMouseEvents, true);\n\t\t\tif (!_context->selectedMin()) {\n\t\t\t\tsetText(tr::lng_calendar_select_days(tr::now));\n\t\t\t} else {\n\t\t\t\tsetText(tr::lng_calendar_days(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\t(1 + *_context->selectedMax()\n\t\t\t\t\t\t- *_context->selectedMin())));\n\t\t\t}\n\t\t}\n\t}, lifetime());\n}\n\nvoid CalendarBox::Title::setTextFromMonth(QDate month) {\n\tsetText(langMonthOfYearFull(month.month(), month.year()));\n}\n\nvoid CalendarBox::Title::setText(QString text) {\n\t_text = std::move(text);\n\t_textWidth = st::calendarTitleFont->width(_text);\n\tupdate();\n}\n\nvoid CalendarBox::Title::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tconst auto clip = e->rect();\n\n\tconst auto triangleSize = st::lineWidth * 6;\n\tconst auto triangleX = _textLeft;\n\tconst auto triangleY = (st::calendarTitleHeight\n\t\t- st::calendarTitleFont->height) / 2\n\t\t+ st::calendarTitleFont->height / 2;\n\tauto triangle = QPainterPath();\n\ttriangle.moveTo(triangleX, triangleY - triangleSize / 2);\n\ttriangle.lineTo(triangleX + triangleSize, triangleY);\n\ttriangle.lineTo(triangleX, triangleY + triangleSize / 2);\n\ttriangle.closeSubpath();\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(st::windowSubTextFg);\n\tp.drawPath(triangle);\n\n\tp.setFont(st::calendarTitleFont);\n\tp.setPen(_styleColors.titleTextColor);\n\tp.drawTextLeft(\n\t\t_textLeft + triangleSize * 2,\n\t\t(st::calendarTitleHeight - st::calendarTitleFont->height) / 2,\n\t\twidth(),\n\t\t_text,\n\t\t_textWidth);\n\n\tpaintDayNames(p, clip);\n}\n\nvoid CalendarBox::Title::paintDayNames(Painter &p, QRect clip) {\n\tp.setFont(st::calendarDaysFont);\n\tp.setPen(st::calendarDaysFg);\n\tauto y = st::calendarTitleHeight + _st.padding.top();\n\tauto x = _st.padding.left();\n\tif (!myrtlrect(x, y, _st.cellSize.width() * kDaysInWeek, _st.daysHeight).intersects(clip)) {\n\t\treturn;\n\t}\n\tconst auto from = _context->firstDayOfWeek();\n\tconst auto to = from + kDaysInWeek;\n\tfor (auto i = from; i != to; ++i, x += _st.cellSize.width()) {\n\t\tauto rect = myrtlrect(x, y, _st.cellSize.width(), _st.daysHeight);\n\t\tif (!rect.intersects(clip)) {\n\t\t\tcontinue;\n\t\t}\n\t\tp.drawText(rect, langDayOfWeek(((i - 1) % 7) + 1), style::al_top);\n\t}\n}\n\nCalendarBox::CalendarBox(QWidget*, CalendarBoxArgs &&args)\n: _st(args.st)\n, _styleColors(args.stColors)\n, _context(\n\tstd::make_unique<Context>(args.month.value(), args.highlighted.value()))\n, _scroll(std::make_unique<ScrollArea>(this, st::calendarScroll))\n, _inner(_scroll->setOwnedWidget(object_ptr<Inner>(\n\tthis,\n\t_context.get(),\n\t_st,\n\t_styleColors,\n\tstd::move(args.dynamicImageForDate))))\n, _title(this, _context.get(), _st, _styleColors)\n, _previous(this, _styleColors.iconButtonPrevious)\n, _next(this, _styleColors.iconButtonNext)\n, _callback(std::move(args.callback.value()))\n, _finalize(std::move(args.finalize))\n, _jumpTimer([=] { jump(_jumpButton); })\n, _selectionChanged(std::move(args.selectionChanged)) {\n\t_inner->setRequireImage(args.requireImage);\n\t_context->setAllowsSelection(args.allowsSelection);\n\t_context->setMinDate(args.minDate);\n\t_context->setMaxDate(args.maxDate);\n\n\t_title->setClickedCallback([=,\n\t\t\tminDate = args.minDate,\n\t\t\tmaxDate = args.maxDate] {\n\t\tBoxContent::uiShow()->show(Box([=](not_null<GenericBox*> box) {\n\t\t\tFillMonthYearPicker(\n\t\t\t\tbox,\n\t\t\t\t_context->month(),\n\t\t\t\tminDate,\n\t\t\t\tmaxDate,\n\t\t\t\t_st,\n\t\t\t\t[=](QDate date) {\n\t\t\t\t\t_watchScroll = false;\n\t\t\t\t\t_context->showMonth(date);\n\t\t\t\t\tsetExactScroll();\n\t\t\t\t});\n\t\t}), LayerOption::KeepOther);\n\t});\n\n\t_scroll->scrolls(\n\t) | rpl::filter([=] {\n\t\treturn _watchScroll;\n\t}) | rpl::on_next([=] {\n\t\tprocessScroll();\n\t}, lifetime());\n\n\tconst auto setupJumps = [&](\n\t\t\tnot_null<IconButton*> button,\n\t\t\tnot_null<bool*> enabled) {\n\t\tbutton->events(\n\t\t) | rpl::filter([=] {\n\t\t\treturn *enabled;\n\t\t}) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\t\tconst auto type = e->type();\n\t\t\tif (type == QEvent::MouseMove\n\t\t\t\t&& !(static_cast<QMouseEvent*>(e.get())->buttons()\n\t\t\t\t\t& Qt::LeftButton)) {\n\t\t\t\tshowJumpTooltip(button);\n\t\t\t} else if (type == QEvent::Leave) {\n\t\t\t\tUi::Tooltip::Hide();\n\t\t\t} else if (type == QEvent::MouseButtonPress\n\t\t\t\t&& (static_cast<QMouseEvent*>(e.get())->button()\n\t\t\t\t\t== Qt::LeftButton)) {\n\t\t\t\tjumpAfterDelay(button);\n\t\t\t} else if (type == QEvent::MouseButtonRelease\n\t\t\t\t&& (static_cast<QMouseEvent*>(e.get())->button()\n\t\t\t\t\t== Qt::LeftButton)) {\n\t\t\t\t_jumpTimer.cancel();\n\t\t\t}\n\t\t}, lifetime());\n\t};\n\tsetupJumps(_previous.data(), &_previousEnabled);\n\tsetupJumps(_next.data(), &_nextEnabled);\n\n\t_context->selectionUpdates(\n\t) | rpl::on_next([=] {\n\t\tif (!_context->selectionMode()) {\n\t\t\t_floatingDate = nullptr;\n\t\t} else if (!_floatingDate) {\n\t\t\t_floatingDate = std::make_unique<FloatingDate>(\n\t\t\t\tthis,\n\t\t\t\t_context.get());\n\t\t\trpl::combine(\n\t\t\t\t_scroll->geometryValue(),\n\t\t\t\t_floatingDate->widthValue()\n\t\t\t) | rpl::on_next([=](QRect scroll, int width) {\n\t\t\t\tconst auto shift = _st.daysHeight\n\t\t\t\t\t- _st.padding.top()\n\t\t\t\t\t- st::calendarDaysFont->height;\n\t\t\t\t_floatingDate->move(\n\t\t\t\t\tscroll.x() + (scroll.width() - width) / 2,\n\t\t\t\t\tscroll.y() - shift);\n\t\t\t}, _floatingDate->lifetime());\n\t\t}\n\t}, lifetime());\n}\n\nCalendarBox::~CalendarBox() = default;\n\nvoid CalendarBox::toggleSelectionMode(bool enabled) {\n\t_context->toggleSelectionMode(enabled);\n}\n\nQDate CalendarBox::selectedFirstDate() const {\n\tconst auto min = _context->selectedMin();\n\treturn min.has_value() ? _context->dateFromIndex(*min) : QDate();\n}\n\nQDate CalendarBox::selectedLastDate() const {\n\tconst auto max = _context->selectedMax();\n\treturn max.has_value() ? _context->dateFromIndex(*max) : QDate();\n}\n\nvoid CalendarBox::setDynamicImage(\n\t\tQDate date,\n\t\tstd::shared_ptr<DynamicImage> image) {\n\t_inner->setDynamicImage(date, std::move(image));\n}\n\nvoid CalendarBox::showJumpTooltip(not_null<IconButton*> button) {\n\t_tooltipButton = button;\n\tUi::Tooltip::Show(kTooltipDelay, this);\n}\n\nvoid CalendarBox::jumpAfterDelay(not_null<IconButton*> button) {\n\t_jumpButton = button;\n\t_jumpTimer.callOnce(kJumpDelay);\n\tUi::Tooltip::Hide();\n}\n\nvoid CalendarBox::jump(QPointer<IconButton> button) {\n\tconst auto jumpToIndex = [&](int index) {\n\t\t_watchScroll = false;\n\t\t_context->showMonth(_context->dateFromIndex(index));\n\t\tsetExactScroll();\n\t};\n\tif (button == _previous.data() && _previousEnabled) {\n\t\tjumpToIndex(_context->minDayIndex());\n\t} else if (button == _next.data() && _nextEnabled) {\n\t\tjumpToIndex(_context->maxDayIndex());\n\t}\n\t_jumpButton = nullptr;\n\t_jumpTimer.cancel();\n}\n\nvoid CalendarBox::prepare() {\n\t_previous->setClickedCallback([=] { goPreviousMonth(); });\n\t_next->setClickedCallback([=] { goNextMonth(); });\n\n\t_inner->setDateChosenCallback([=, c = std::move(_callback)](\n\t\t\tconst QDate &date) {\n\t\tc(date, crl::guard(this, [=] { closeBox(); }));\n\t});\n\n\t_context->monthValue(\n\t) | rpl::on_next([=](QDate month) {\n\t\tmonthChanged(month);\n\t}, lifetime());\n\tsetExactScroll();\n\n\t_context->selectionUpdates(\n\t) | rpl::on_next([=] {\n\t\t_selectionMode = _context->selectionMode();\n\t\tif (_selectionChanged) {\n\t\t\tconst auto count = !_selectionMode\n\t\t\t\t? std::optional<int>()\n\t\t\t\t: !_context->selectedMin()\n\t\t\t\t? 0\n\t\t\t\t: (1 + *_context->selectedMax() - *_context->selectedMin());\n\t\t\t_selectionChanged(this, count);\n\t\t}\n\t\tif (!_selectionMode) {\n\t\t\tclearButtons();\n\t\t\tcreateButtons();\n\t\t}\n\t}, lifetime());\n\tcreateButtons();\n\n\tif (_finalize) {\n\t\t_finalize(this);\n\t}\n}\n\nbool CalendarBox::isPreviousEnabled() const {\n\treturn (_context->minDayIndex() < 0);\n}\n\nbool CalendarBox::isNextEnabled() const {\n\treturn (_context->maxDayIndex() >= _context->daysCount());\n}\n\nvoid CalendarBox::goPreviousMonth() {\n\tif (isPreviousEnabled()) {\n\t\t_watchScroll = false;\n\t\t_context->skipMonth(-1);\n\t\tsetExactScroll();\n\t}\n}\n\nvoid CalendarBox::goNextMonth() {\n\tif (isNextEnabled()) {\n\t\t_watchScroll = false;\n\t\t_context->skipMonth(1);\n\t\tsetExactScroll();\n\t}\n}\n\nvoid CalendarBox::setExactScroll() {\n\tconst auto top = _st.padding.top()\n\t\t+ (_context->daysShift() / kDaysInWeek) * _st.cellSize.height();\n\t_scroll->scrollToY(top);\n\t_watchScroll = true;\n}\n\nvoid CalendarBox::processScroll() {\n\tconst auto wasTop = _scroll->scrollTop();\n\tconst auto wasShift = _context->daysShift();\n\tconst auto point = _scroll->rect().center() + QPoint(0, wasTop);\n\tconst auto row = (point.y() - _st.padding.top()) / _st.cellSize.height();\n\tconst auto col = (point.x() - _st.padding.left()) / _st.cellSize.width();\n\tconst auto index = row * kDaysInWeek + col;\n\tconst auto date = _context->dateFromIndex(index - wasShift);\n\tif (_context->showsMonthOf(date)) {\n\t\treturn;\n\t}\n\tconst auto wasFirst = _context->dateFromIndex(-wasShift);\n\tconst auto month = QDate(date.year(), date.month(), 1);\n\t_watchScroll = false;\n\t_context->showMonth(month);\n\tconst auto nowShift = _context->daysShift();\n\tconst auto nowFirst = _context->dateFromIndex(-nowShift);\n\tconst auto delta = nowFirst.daysTo(wasFirst) / kDaysInWeek;\n\t_scroll->scrollToY(wasTop + delta * _st.cellSize.height());\n\t_watchScroll = true;\n}\n\nvoid CalendarBox::createButtons() {\n\tif (!_context->allowsSelection()) {\n\t\taddButton(tr::lng_close(), [=] { closeBox(); });\n\t} else if (!_context->selectionMode()) {\n\t\taddButton(tr::lng_close(), [=] { closeBox(); });\n\t\taddLeftButton(tr::lng_calendar_select_days(), [=] {\n\t\t\t_context->toggleSelectionMode(true);\n\t\t});\n\t} else {\n\t\taddButton(tr::lng_cancel(), [=] {\n\t\t\t_context->toggleSelectionMode(false);\n\t\t});\n\t}\n}\n\nQString CalendarBox::tooltipText() const {\n\tif (_tooltipButton == _previous.data()) {\n\t\treturn tr::lng_calendar_start_tip(tr::now);\n\t} else if (_tooltipButton == _next.data()) {\n\t\treturn tr::lng_calendar_end_tip(tr::now);\n\t}\n\treturn QString();\n}\n\nQPoint CalendarBox::tooltipPos() const {\n\treturn QCursor::pos();\n}\n\nbool CalendarBox::tooltipWindowActive() const {\n\treturn window()->isActiveWindow();\n}\n\nvoid CalendarBox::monthChanged(QDate month) {\n\tsetDimensions(\n\t\t_st.width,\n\t\tst::calendarTitleHeight + _st.daysHeight + _inner->countMaxHeight());\n\n\t_previousEnabled = isPreviousEnabled();\n\t_previous->setIconOverride(_previousEnabled\n\t\t? nullptr\n\t\t: &_styleColors.iconButtonPreviousDisabled);\n\t_previous->setRippleColorOverride(_previousEnabled\n\t\t? nullptr\n\t\t: &_styleColors.iconButtonRippleColorDisabled);\n\t_previous->setPointerCursor(_previousEnabled);\n\tif (!_previousEnabled) {\n\t\t_previous->clearState();\n\t}\n\t_nextEnabled = isNextEnabled();\n\t_next->setIconOverride(_nextEnabled\n\t\t? nullptr\n\t\t: &_styleColors.iconButtonNextDisabled);\n\t_next->setRippleColorOverride(_nextEnabled\n\t\t? nullptr\n\t\t: &_styleColors.iconButtonRippleColorDisabled);\n\t_next->setPointerCursor(_nextEnabled);\n\tif (!_nextEnabled) {\n\t\t_next->clearState();\n\t}\n}\n\nvoid CalendarBox::resizeEvent(QResizeEvent *e) {\n\tconst auto dayWidth = st::calendarDaysFont->width(langDayOfWeek(7));\n\tconst auto skip = _st.padding.left()\n\t\t+ _st.cellSize.width() * (kDaysInWeek - 1)\n\t\t+ (_st.cellSize.width() - dayWidth) / 2\n\t\t+ dayWidth;\n\tconst auto right = width() - skip;\n\tconst auto shift = _next->width()\n\t\t- (_next->width() - st::calendarPrevious.icon.width()) / 2\n\t\t- st::calendarPrevious.icon.width();\n\t_next->moveToRight(right - shift, 0);\n\t_previous->moveToRight(right - shift + _next->width(), 0);\n\tconst auto title = st::calendarTitleHeight + _st.daysHeight;\n\t_title->setGeometryToLeft(0, 0, width(), title);\n\t_scroll->setGeometryToLeft(0, title, width(), height() - title);\n\tBoxContent::resizeEvent(e);\n}\n\nvoid CalendarBox::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Escape) {\n\t\tif (_context->selectionMode()) {\n\t\t\t_context->toggleSelectionMode(false);\n\t\t} else {\n\t\t\te->ignore();\n\t\t}\n\t} else if (e->key() == Qt::Key_Home) {\n\t\tjump(_previous.data());\n\t} else if (e->key() == Qt::Key_End) {\n\t\tjump(_next.data());\n\t} else if (e->key() == Qt::Key_Left\n\t\t|| e->key() == Qt::Key_Up\n\t\t|| e->key() == Qt::Key_PageUp) {\n\t\tgoPreviousMonth();\n\t} else if (e->key() == Qt::Key_Right\n\t\t|| e->key() == Qt::Key_Down\n\t\t|| e->key() == Qt::Key_PageDown) {\n\t\tgoNextMonth();\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/calendar_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"base/required.h\"\n#include \"base/timer.h\"\n\n#include <QtCore/QDate>\n\nnamespace style {\nstruct CalendarSizes;\nstruct CalendarColors;\n} // namespace style\n\nnamespace st {\nextern const style::CalendarSizes &defaultCalendarSizes;\nextern const style::CalendarColors &defaultCalendarColors;\n} // namespace st\n\nnamespace Ui {\n\nclass IconButton;\nclass ScrollArea;\nclass CalendarBox;\nclass DynamicImage;\n\nusing CalendarImageSetter = Fn<void(QDate, std::shared_ptr<DynamicImage>)>;\nusing JumpCallback = Fn<void(QDate date, Fn<void()> close)>;\n\nstruct CalendarBoxArgs {\n\ttemplate <typename T>\n\tusing required = base::required<T>;\n\n\trequired<QDate> month;\n\trequired<QDate> highlighted;\n\trequired<JumpCallback> callback;\n\tFnMut<void(not_null<CalendarBox*>)> finalize;\n\tconst style::CalendarSizes &st = st::defaultCalendarSizes;\n\tQDate minDate;\n\tQDate maxDate;\n\tbool allowsSelection = false;\n\tFn<void(\n\t\tnot_null<Ui::CalendarBox*>,\n\t\tstd::optional<int>)> selectionChanged;\n\tconst style::CalendarColors &stColors = st::defaultCalendarColors;\n\tFn<void(QDate, CalendarImageSetter)> dynamicImageForDate;\n\tbool requireImage = false;\n};\n\nclass CalendarBox final : public BoxContent, private AbstractTooltipShower {\npublic:\n\tCalendarBox(QWidget*, CalendarBoxArgs &&args);\n\t~CalendarBox();\n\n\tvoid toggleSelectionMode(bool enabled);\n\n\t[[nodiscard]] QDate selectedFirstDate() const;\n\t[[nodiscard]] QDate selectedLastDate() const;\n\n\tvoid setDynamicImage(QDate date, std::shared_ptr<DynamicImage> image);\n\nprotected:\n\tvoid prepare() override;\n\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid monthChanged(QDate month);\n\n\tbool isPreviousEnabled() const;\n\tbool isNextEnabled() const;\n\n\tvoid goPreviousMonth();\n\tvoid goNextMonth();\n\tvoid setExactScroll();\n\tvoid processScroll();\n\tvoid createButtons();\n\n\tvoid showJumpTooltip(not_null<IconButton*> button);\n\tvoid jumpAfterDelay(not_null<IconButton*> button);\n\tvoid jump(QPointer<IconButton> button);\n\n\tQString tooltipText() const override;\n\tQPoint tooltipPos() const override;\n\tbool tooltipWindowActive() const override;\n\n\tconst style::CalendarSizes &_st;\n\tconst style::CalendarColors &_styleColors;\n\n\tclass Context;\n\tstd::unique_ptr<Context> _context;\n\n\tstd::unique_ptr<ScrollArea> _scroll;\n\n\tclass Inner;\n\tnot_null<Inner*> _inner;\n\n\tclass FloatingDate;\n\tstd::unique_ptr<FloatingDate> _floatingDate;\n\n\tclass Title;\n\tobject_ptr<Title> _title;\n\tobject_ptr<IconButton> _previous;\n\tobject_ptr<IconButton> _next;\n\tbool _previousEnabled = false;\n\tbool _nextEnabled = false;\n\n\tJumpCallback _callback;\n\tFnMut<void(not_null<CalendarBox*>)> _finalize;\n\tbool _watchScroll = false;\n\n\tQPointer<IconButton> _tooltipButton;\n\tQPointer<IconButton> _jumpButton;\n\tbase::Timer _jumpTimer;\n\n\tbool _selectionMode = false;\n\tFn<void(\n\t\tnot_null<Ui::CalendarBox*>,\n\t\tstd::optional<int>)> _selectionChanged;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/choose_date_time.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/choose_date_time.h\"\n\n#include \"base/unixtime.h\"\n#include \"base/event_filter.h\"\n#include \"ui/boxes/calendar_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/time_input.h\"\n#include \"ui/ui_utility.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\n#include <QtWidgets/QTextEdit>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kMinimalSchedule = TimeId(10);\n\nQString DayString(const QDate &date) {\n\tconst auto month = Lang::MonthDay(date.month())(tr::now);\n\tconst auto day = QString::number(date.day());\n\tif (date.year() != QDate::currentDate().year()) {\n\t\treturn tr::lng_month_day_year(\n\t\t\ttr::now,\n\t\t\tlt_month,\n\t\t\tmonth,\n\t\t\tlt_day,\n\t\t\tday,\n\t\t\tlt_year,\n\t\t\tQString::number(date.year()));\n\t}\n\treturn tr::lng_month_day(tr::now, lt_month, month, lt_day, day);\n}\n\nQString TimeString(QTime time) {\n\treturn QString(\"%1:%2\"\n\t).arg(time.hour()\n\t).arg(time.minute(), 2, 10, QLatin1Char('0'));\n}\n\n} // namespace\n\nChooseDateTimeStyleArgs::ChooseDateTimeStyleArgs()\n: labelStyle(&st::boxLabel)\n, dateFieldStyle(&st::scheduleDateField)\n, timeFieldStyle(&st::scheduleTimeField)\n, separatorStyle(&st::scheduleTimeSeparator)\n, atStyle(&st::scheduleAtLabel)\n, calendarStyle(&st::defaultCalendarColors) {\n}\n\nChooseDateTimeBoxDescriptor ChooseDateTimeBox(\n\t\tnot_null<GenericBox*> box,\n\t\tChooseDateTimeBoxArgs &&args) {\n\tstruct State {\n\t\trpl::variable<QDate> date;\n\t\trpl::variable<int> width;\n\t\tnot_null<InputField*> day;\n\t\tnot_null<TimeInput*> time;\n\t\tnot_null<FlatLabel*> at;\n\t};\n\tbox->setTitle(std::move(args.title));\n\tbox->setWidth(st::boxWideWidth);\n\n\tconst auto content = box->addRow(\n\t\tobject_ptr<FixedHeightWidget>(box, st::scheduleHeight),\n\t\tstyle::al_top);\n\tif (args.description) {\n\t\tbox->addRow(object_ptr<FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(args.description),\n\t\t\t*args.style.labelStyle));\n\t}\n\tconst auto parsed = base::unixtime::parse(args.time);\n\tconst auto state = box->lifetime().make_state<State>(State{\n\t\t.date = parsed.date(),\n\t\t.day = CreateChild<InputField>(\n\t\t\tcontent,\n\t\t\t*args.style.dateFieldStyle),\n\t\t.time = CreateChild<TimeInput>(\n\t\t\tcontent,\n\t\t\tTimeString(parsed.time()),\n\t\t\t*args.style.timeFieldStyle,\n\t\t\t*args.style.dateFieldStyle,\n\t\t\t*args.style.separatorStyle,\n\t\t\tst::scheduleTimeSeparatorPadding),\n\t\t.at = CreateChild<FlatLabel>(\n\t\t\tcontent,\n\t\t\ttr::lng_schedule_at(),\n\t\t\t*args.style.atStyle),\n\t});\n\n\tstate->date.value(\n\t) | rpl::on_next([=](QDate date) {\n\t\tstate->day->setText(DayString(date));\n\t\tstate->time->setFocusFast();\n\t}, state->day->lifetime());\n\n\tconst auto min = args.min ? args.min : [] {\n\t\treturn base::unixtime::now() + kMinimalSchedule;\n\t};\n\tconst auto max = args.max ? args.max : [] {\n\t\treturn base::unixtime::serialize(\n\t\t\tQDateTime::currentDateTime().addYears(1)) - 1;\n\t};\n\tconst auto minDate = [=] {\n\t\treturn base::unixtime::parse(min()).date();\n\t};\n\tconst auto maxDate = [=] {\n\t\treturn base::unixtime::parse(max()).date();\n\t};\n\n\tconst auto &dayViewport = state->day->rawTextEdit()->viewport();\n\tbase::install_event_filter(dayViewport, [=](not_null<QEvent*> event) {\n\t\tif (event->type() == QEvent::Wheel) {\n\t\t\tconst auto e = static_cast<QWheelEvent*>(event.get());\n\t\t\tconst auto direction = Ui::WheelDirection(e);\n\t\t\tif (!direction) {\n\t\t\t\treturn base::EventFilterResult::Continue;\n\t\t\t}\n\t\t\tconst auto d = state->date.current().addDays(direction);\n\t\t\tstate->date = std::clamp(d, minDate(), maxDate());\n\t\t\treturn base::EventFilterResult::Cancel;\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\tstate->at->widthValue() | rpl::on_next([=](int width) {\n\t\tconst auto full = st::scheduleDateWidth\n\t\t\t+ st::scheduleAtSkip\n\t\t\t+ width\n\t\t\t+ st::scheduleAtSkip\n\t\t\t+ st::scheduleTimeWidth;\n\t\tcontent->setNaturalWidth(full);\n\t\tstate->width = full;\n\t}, state->at->lifetime());\n\n\tcontent->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto paddings = width\n\t\t\t- state->at->width()\n\t\t\t- 2 * st::scheduleAtSkip\n\t\t\t- st::scheduleDateWidth\n\t\t\t- st::scheduleTimeWidth;\n\t\tconst auto left = paddings / 2;\n\t\tstate->day->resizeToWidth(st::scheduleDateWidth);\n\t\tstate->day->moveToLeft(left, st::scheduleDateTop, width);\n\t\tstate->at->moveToLeft(\n\t\t\tleft + st::scheduleDateWidth + st::scheduleAtSkip,\n\t\t\tst::scheduleAtTop,\n\t\t\twidth);\n\t\tstate->time->resizeToWidth(st::scheduleTimeWidth);\n\t\tstate->time->moveToLeft(\n\t\t\twidth - left - st::scheduleTimeWidth,\n\t\t\tst::scheduleDateTop,\n\t\t\twidth);\n\t}, content->lifetime());\n\n\tconst auto calendar\n\t\t= content->lifetime().make_state<base::weak_qptr<CalendarBox>>();\n\tconst auto calendarStyle = args.style.calendarStyle;\n\tstate->day->focusedChanges(\n\t) | rpl::on_next([=](bool focused) {\n\t\tif (*calendar || !focused) {\n\t\t\treturn;\n\t\t}\n\t\t*calendar = box->getDelegate()->show(\n\t\t\tBox<CalendarBox>(Ui::CalendarBoxArgs{\n\t\t\t\t.month = state->date.current(),\n\t\t\t\t.highlighted = state->date.current(),\n\t\t\t\t.callback = crl::guard(box, [=](\n\t\t\t\t\t\tQDate chosen,\n\t\t\t\t\t\tFn<void()> close) {\n\t\t\t\t\tstate->date = chosen;\n\t\t\t\t\tclose();\n\t\t\t\t}),\n\t\t\t\t.minDate = minDate(),\n\t\t\t\t.maxDate = maxDate(),\n\t\t\t\t.stColors = *calendarStyle,\n\t\t\t}));\n\t\t(*calendar)->boxClosing(\n\t\t) | rpl::on_next(crl::guard(state->time, [=] {\n\t\t\tstate->time->setFocusFast();\n\t\t}), (*calendar)->lifetime());\n\t}, state->day->lifetime());\n\n\tconst auto collect = [=] {\n\t\tconst auto timeValue = state->time->valueCurrent().split(':');\n\t\tif (timeValue.size() != 2) {\n\t\t\treturn 0;\n\t\t}\n\t\tconst auto time = QTime(timeValue[0].toInt(), timeValue[1].toInt());\n\t\tif (!time.isValid()) {\n\t\t\treturn 0;\n\t\t}\n\t\tconst auto result = base::unixtime::serialize(\n\t\t\tQDateTime(state->date.current(), time));\n\t\tif (result < min() || result > max()) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn result;\n\t};\n\tconst auto save = [=, done = args.done] {\n\t\tif (const auto result = collect()) {\n\t\t\tdone(result);\n\t\t} else {\n\t\t\tstate->time->showError();\n\t\t}\n\t};\n\tstate->time->submitRequests(\n\t) | rpl::on_next(save, state->time->lifetime());\n\n\tauto result = ChooseDateTimeBoxDescriptor();\n\tbox->setFocusCallback([=] { state->time->setFocusFast(); });\n\tresult.width = state->width.value();\n\tresult.submit = box->addButton(std::move(args.submit), save);\n\tresult.collect = [=] {\n\t\tif (const auto result = collect()) {\n\t\t\treturn result;\n\t\t}\n\t\tstate->time->showError();\n\t\treturn 0;\n\t};\n\tresult.values = rpl::combine(\n\t\tstate->date.value(),\n\t\tstate->time->value()\n\t) | rpl::map(collect);\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\n\treturn result;\n}\n\nobject_ptr<Ui::RpWidget> ChooseRepeatPeriod(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tChooseRepeatPeriodArgs &&args) {\n\tauto result = object_ptr<Ui::RpWidget>(parent.get());\n\tconst auto raw = result.data();\n\n\tstruct Entry {\n\t\tTimeId value = 0;\n\t\tQString text;\n\t};\n\tauto map = std::vector<Entry>{\n\t\t{ 0, tr::lng_schedule_repeat_never(tr::now) },\n\t\t{ 24 * 60 * 60, tr::lng_schedule_repeat_daily(tr::now) },\n\t\t{ 7 * 24 * 60 * 60, tr::lng_schedule_repeat_weekly(tr::now) },\n\t\t{ 14 * 24 * 60 * 60, tr::lng_schedule_repeat_biweekly(tr::now) },\n\t\t{ 30 * 24 * 60 * 60, tr::lng_schedule_repeat_monthly(tr::now) },\n\t\t{\n\t\t\t91 * 24 * 60 * 60,\n\t\t\ttr::lng_schedule_repeat_every_month(tr::now, lt_count, 3)\n\t\t},\n\t\t{\n\t\t\t182 * 24 * 60 * 60,\n\t\t\ttr::lng_schedule_repeat_every_month(tr::now, lt_count, 6)\n\t\t},\n\t\t{ 365 * 24 * 60 * 60, tr::lng_schedule_repeat_yearly(tr::now) },\n\t};\n\tif (args.test) {\n\t\tmap.insert(begin(map) + 1, Entry{ 300, u\"Every 5 minutes\"_q });\n\t\tmap.insert(begin(map) + 1, Entry{ 60, u\"Every minute\"_q });\n\t}\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(raw, QString());\n\trpl::combine(\n\t\traw->widthValue(),\n\t\tlabel->naturalWidthValue()\n\t) | rpl::on_next([=](int outer, int natural) {\n\t\tlabel->resizeToWidth(std::min(outer, natural));\n\t}, raw->lifetime());\n\tlabel->heightValue() | rpl::on_next([=](int height) {\n\t\traw->resize(raw->width(), height);\n\t}, label->lifetime());\n\n\tstruct State {\n\t\trpl::variable<TimeId> value;\n\t\trpl::variable<bool> locked;\n\t\tstd::unique_ptr<Ui::PopupMenu> menu;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>(State{\n\t\t.value = args.value,\n\t\t.locked = std::move(args.locked),\n\t});\n\n\trpl::combine(\n\t\tstate->value.value(),\n\t\tstate->locked.value()\n\t) | rpl::on_next([=](TimeId value, bool locked) {\n\t\tauto result = tr::lng_schedule_repeat_label(\n\t\t\ttr::now,\n\t\t\ttr::marked);\n\n\t\tconst auto text = [&] {\n\t\t\tconst auto i = ranges::lower_bound(\n\t\t\t\tmap,\n\t\t\t\tvalue,\n\t\t\t\tranges::less{},\n\t\t\t\t&Entry::value);\n\t\t\treturn (i != end(map)) ? i->text : map.back().text;\n\t\t}();\n\n\t\tlabel->setMarkedText(result.append(' ').append(tr::link(\n\t\t\ttr::bold(text).append(\n\t\t\t\tUi::Text::IconEmoji(locked\n\t\t\t\t\t? &st::scheduleRepeatDropdownLock\n\t\t\t\t\t: &st::scheduleRepeatDropdownArrow))\n\t\t)));\n\t\treturn result;\n\t}, label->lifetime());\n\n\tlabel->setClickHandlerFilter([=](const auto &...) {\n\t\tif (args.filter && args.filter()) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto changed = args.changed;\n\n\t\tstate->menu = std::make_unique<Ui::PopupMenu>(label);\n\t\tconst auto menu = state->menu.get();\n\n\t\tmenu->setDestroyedCallback(crl::guard(label, [=] {\n\t\t\tif (state->menu.get() == menu) {\n\t\t\t\tstate->menu.release();\n\t\t\t}\n\t\t}));\n\t\tfor (const auto &entry : map) {\n\t\t\tconst auto value = entry.value;\n\t\t\tmenu->addAction(entry.text, [=] {\n\t\t\t\tstate->value = value;\n\t\t\t\tchanged(value);\n\t\t\t});\n\t\t}\n\t\tmenu->popup(QCursor::pos());\n\t\treturn false;\n\t});\n\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/choose_date_time.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n\nnamespace style {\nstruct FlatLabel;\nstruct InputField;\nstruct CalendarColors;\n} // namespace style\n\nnamespace Ui {\n\nclass RoundButton;\n\nstruct ChooseDateTimeBoxDescriptor {\n\tQPointer<RoundButton> submit;\n\tFn<TimeId()> collect;\n\trpl::producer<TimeId> values;\n\trpl::producer<int> width;\n};\n\nstruct ChooseDateTimeStyleArgs {\n\tChooseDateTimeStyleArgs();\n\tconst style::FlatLabel *labelStyle;\n\tconst style::InputField *dateFieldStyle;\n\tconst style::InputField *timeFieldStyle;\n\tconst style::FlatLabel *separatorStyle;\n\tconst style::FlatLabel *atStyle;\n\tconst style::CalendarColors *calendarStyle;\n};\n\nstruct ChooseDateTimeBoxArgs {\n\trpl::producer<QString> title;\n\trpl::producer<QString> submit;\n\tFn<void(TimeId)> done;\n\tFn<TimeId()> min;\n\tTimeId time = 0;\n\tFn<TimeId()> max;\n\trpl::producer<QString> description;\n\tChooseDateTimeStyleArgs style;\n};\n\nChooseDateTimeBoxDescriptor ChooseDateTimeBox(\n\tnot_null<GenericBox*> box,\n\tChooseDateTimeBoxArgs &&args);\n\nstruct ChooseRepeatPeriodArgs {\n\tTimeId value = 0;\n\trpl::variable<bool> locked;\n\tFn<bool()> filter;\n\tFn<void(TimeId)> changed;\n\tbool test = false;\n};\n\n[[nodiscard]] object_ptr<Ui::RpWidget> ChooseRepeatPeriod(\n\tnot_null<Ui::RpWidget*> parent,\n\tChooseRepeatPeriodArgs &&args);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/choose_font_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/choose_font_box.h\"\n\n#include \"base/event_filter.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/style/style_core_font.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/multi_select.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_window.h\"\n\n#include <QtGui/QFontDatabase>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kMinTextWidth = 120;\nconstexpr auto kMaxTextWidth = 320;\nconstexpr auto kMaxTextLines = 3;\n\nstruct PreviewRequest {\n\tQString family;\n\tQColor msgBg;\n\tQColor msgShadow;\n\tQColor replyBar;\n\tQColor replyNameFg;\n\tQColor textFg;\n\tQImage bubbleTail;\n};\n\nclass PreviewPainter {\npublic:\n\tPreviewPainter(const QImage &bg, PreviewRequest request);\n\n\tQImage takeResult();\n\nprivate:\n\tvoid layout();\n\n\tvoid paintBubble(Painter &p);\n\tvoid paintContent(Painter &p);\n\tvoid paintReply(Painter &p);\n\tvoid paintMessage(Painter &p);\n\n\tvoid validateBubbleCache();\n\n\tconst PreviewRequest _request;\n\tconst style::owned_color _msgBg;\n\tconst style::owned_color _msgShadow;\n\n\tstyle::owned_font _nameFontOwned;\n\tstyle::font _nameFont;\n\tstyle::TextStyle _nameStyle;\n\tstyle::owned_font _textFontOwned;\n\tstyle::font _textFont;\n\tstyle::TextStyle _textStyle;\n\n\tUi::Text::String _nameText;\n\tUi::Text::String _replyText;\n\tUi::Text::String _messageText;\n\n\tQRect _replyRect;\n\tQRect _name;\n\tQRect _reply;\n\tQRect _message;\n\tQRect _content;\n\tQRect _bubble;\n\tQSize _outer;\n\n\tUi::CornersPixmaps _bubbleCorners;\n\tQPixmap _bubbleShadowBottomRight;\n\n\tQImage _result;\n\n};\n\nclass Selector final : public Ui::RpWidget {\npublic:\n\tSelector(\n\t\tnot_null<QWidget*> parent,\n\t\tconst QString &now,\n\t\trpl::producer<QString> filter,\n\t\trpl::producer<> submits,\n\t\tFn<void(QString)> chosen,\n\t\tFn<void(Ui::ScrollToRequest, anim::type)> scrollTo);\n\n\tvoid initScroll(anim::type animated);\n\tvoid setMinHeight(int height);\n\tvoid selectSkip(Qt::Key direction);\n\nprivate:\n\tstruct Entry {\n\t\tQString id;\n\t\tQString key;\n\t\tQString text;\n\t\tQStringList keywords;\n\t\tQImage cache;\n\t\tstd::unique_ptr<Ui::RadioView> check;\n\t\tstd::unique_ptr<Ui::RippleAnimation> ripple;\n\t\tint paletteVersion = 0;\n\t};\n\t[[nodiscard]] static std::vector<Entry> FullList(const QString &now);\n\n\tint resizeGetHeight(int newWidth) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\n\t[[nodiscard]] bool searching() const;\n\t[[nodiscard]] int shownRowsCount() const;\n\t[[nodiscard]] Entry &shownRowAt(int index);\n\n\tvoid applyFilter(const QString &query);\n\tvoid updateSelected(int selected);\n\tvoid updatePressed(int pressed);\n\tvoid updateRow(int index);\n\tvoid updateRow(not_null<Entry*> row, int hint);\n\tvoid addRipple(int index, QPoint position);\n\tvoid validateCache(Entry &row);\n\tvoid choose(Entry &row);\n\n\tconst style::SettingsButton &_st;\n\tstd::vector<Entry> _rows;\n\tstd::vector<not_null<Entry*>> _filtered;\n\tQString _chosen;\n\tint _selected = -1;\n\tint _pressed = -1;\n\n\tstd::optional<QPoint> _lastGlobalPoint;\n\tbool _selectedByKeyboard = false;\n\n\tFn<void(QString)> _callback;\n\tFn<void(Ui::ScrollToRequest, anim::type)> _scrollTo;\n\n\tint _rowsSkip = 0;\n\tint _rowHeight = 0;\n\tint _minHeight = 0;\n\n\tQString _query;\n\tQStringList _queryWords;\n\n\trpl::lifetime _lifetime;\n\n};\n\nSelector::Selector(\n\tnot_null<QWidget*> parent,\n\tconst QString &now,\n\trpl::producer<QString> filter,\n\trpl::producer<> submits,\n\tFn<void(QString)> chosen,\n\tFn<void(Ui::ScrollToRequest, anim::type)> scrollTo)\n: RpWidget(parent)\n, _st(st::settingsButton)\n, _rows(FullList(now))\n, _chosen(now)\n, _callback(std::move(chosen))\n, _scrollTo(std::move(scrollTo))\n, _rowsSkip(st::settingsInfoPhotoSkip)\n, _rowHeight(_st.height + _st.padding.top() + _st.padding.bottom()) {\n\tsetMouseTracking(true);\n\n\tstd::move(filter) | rpl::on_next([=](const QString &query) {\n\t\tapplyFilter(query);\n\t}, _lifetime);\n\n\tstd::move(\n\t\tsubmits\n\t) | rpl::on_next([=] {\n\t\tif (_selected >= 0) {\n\t\t\tchoose(shownRowAt(_selected));\n\t\t} else if (searching() && !_filtered.empty()) {\n\t\t\tchoose(*_filtered.front());\n\t\t}\n\t}, _lifetime);\n}\n\nvoid Selector::applyFilter(const QString &query) {\n\tif (_query == query) {\n\t\treturn;\n\t}\n\t_query = query;\n\n\tupdateSelected(-1);\n\tupdatePressed(-1);\n\n\t_queryWords = TextUtilities::PrepareSearchWords(_query);\n\n\tconst auto skip = [](\n\t\t\tconst QStringList &haystack,\n\t\t\tconst QStringList &needles) {\n\t\tconst auto find = [](\n\t\t\t\tconst QStringList &haystack,\n\t\t\t\tconst QString &needle) {\n\t\t\tfor (const auto &item : haystack) {\n\t\t\t\tif (item.startsWith(needle)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\t\tfor (const auto &needle : needles) {\n\t\t\tif (!find(haystack, needle)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\n\t_filtered.clear();\n\tif (!_queryWords.isEmpty()) {\n\t\t_filtered.reserve(_rows.size());\n\t\tfor (auto &row : _rows) {\n\t\t\tif (!skip(row.keywords, _queryWords)) {\n\t\t\t\t_filtered.push_back(&row);\n\t\t\t} else {\n\t\t\t\trow.ripple = nullptr;\n\t\t\t}\n\t\t}\n\t}\n\n\tresizeToWidth(width());\n\tUi::SendPendingMoveResizeEvents(this);\n\tupdate();\n}\n\nvoid Selector::updateSelected(int selected) {\n\tif (_selected == selected) {\n\t\treturn;\n\t}\n\tconst auto was = (_selected >= 0);\n\tupdateRow(_selected);\n\t_selected = selected;\n\tupdateRow(_selected);\n\tconst auto now = (_selected >= 0);\n\tif (was != now) {\n\t\tsetCursor(now ? style::cur_pointer : style::cur_default);\n\t}\n\tif (_selectedByKeyboard) {\n\t\tconst auto top = (_selected > 0)\n\t\t\t? (_rowsSkip + _selected * _rowHeight)\n\t\t\t: 0;\n\t\tconst auto bottom = (_selected > 0)\n\t\t\t? (top + _rowHeight)\n\t\t\t: _selected\n\t\t\t? 0\n\t\t\t: _rowHeight;\n\t\t_scrollTo({ top, bottom }, anim::type::instant);\n\t}\n}\n\nvoid Selector::updatePressed(int pressed) {\n\tif (_pressed == pressed) {\n\t\treturn;\n\t} else if (_pressed >= 0) {\n\t\tif (auto &ripple = shownRowAt(_pressed).ripple) {\n\t\t\tripple->lastStop();\n\t\t}\n\t}\n\tupdateRow(_pressed);\n\t_pressed = pressed;\n\tupdateRow(_pressed);\n}\n\nvoid Selector::updateRow(int index) {\n\tif (index >= 0) {\n\t\tupdate(0, _rowsSkip + index * _rowHeight, width(), _rowHeight);\n\t}\n}\n\nvoid Selector::updateRow(not_null<Entry*> row, int hint) {\n\tif (hint >= 0 && hint < shownRowsCount() && &shownRowAt(hint) == row) {\n\t\tupdateRow(hint);\n\t} else if (searching()) {\n\t\tconst auto i = ranges::find(_filtered, row);\n\t\tif (i != end(_filtered)) {\n\t\t\tupdateRow(int(i - begin(_filtered)));\n\t\t}\n\t} else {\n\t\tconst auto index = int(row.get() - &_rows[0]);\n\t\tAssert(index >= 0 && index < _rows.size());\n\t\tupdateRow(index);\n\t}\n}\n\nvoid Selector::validateCache(Entry &row) {\n\tconst auto version = style::PaletteVersion();\n\tif (row.cache.isNull()) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\trow.cache = QImage(\n\t\t\tQSize(width(), _rowHeight) * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\trow.cache.setDevicePixelRatio(ratio);\n\t} else if (row.paletteVersion == version) {\n\t\treturn;\n\t}\n\trow.paletteVersion = version;\n\trow.cache.fill(Qt::transparent);\n\tauto owned = style::owned_font(row.id, 0, st::boxFontSize);\n\tconst auto font = owned.font();\n\tauto p = QPainter(&row.cache);\n\tp.setFont(font);\n\tp.setPen(st::windowFg);\n\n\tconst auto textw = width() - _st.padding.left() - _st.padding.right();\n\tconst auto textt = (_rowHeight - font->height) / 2.;\n\tp.drawText(\n\t\t_st.padding.left(),\n\t\ttextt + font->ascent,\n\t\tfont->elided(row.text, textw));\n}\n\nbool Selector::searching() const {\n\treturn !_queryWords.isEmpty();\n}\n\nint Selector::shownRowsCount() const {\n\treturn searching() ? int(_filtered.size()) : int(_rows.size());\n}\n\nSelector::Entry &Selector::shownRowAt(int index) {\n\treturn searching() ? *_filtered[index] : _rows[index];\n}\n\nvoid Selector::setMinHeight(int height) {\n\t_minHeight = height;\n\tif (_minHeight > 0) {\n\t\tresizeToWidth(width());\n\t}\n}\n\nvoid Selector::selectSkip(Qt::Key key) {\n\tconst auto count = shownRowsCount();\n\tif (key == Qt::Key_Down) {\n\t\tif (_selected + 1 < count) {\n\t\t\t_selectedByKeyboard = true;\n\t\t\tupdateSelected(_selected + 1);\n\t\t}\n\t} else if (key == Qt::Key_Up) {\n\t\tif (_selected >= 0) {\n\t\t\t_selectedByKeyboard = true;\n\t\t\tupdateSelected(_selected - 1);\n\t\t}\n\t} else if (key == Qt::Key_PageDown) {\n\t\tconst auto change = _minHeight / _rowHeight;\n\t\tif (_selected + 1 < count) {\n\t\t\t_selectedByKeyboard = true;\n\t\t\tupdateSelected(std::min(_selected + change, count - 1));\n\t\t}\n\t} else if (key == Qt::Key_PageUp) {\n\t\tconst auto change = _minHeight / _rowHeight;\n\t\tif (_selected > 0) {\n\t\t\t_selectedByKeyboard = true;\n\t\t\tupdateSelected(std::max(_selected - change, 0));\n\t\t} else if (!_selected) {\n\t\t\t_selectedByKeyboard = true;\n\t\t\tupdateSelected(-1);\n\t\t}\n\t}\n}\n\nvoid Selector::initScroll(anim::type animated) {\n\tconst auto index = [&] {\n\t\tif (searching()) {\n\t\t\tconst auto i = ranges::find(_filtered, _chosen, &Entry::id);\n\t\t\tif (i != end(_filtered)) {\n\t\t\t\treturn int(i - begin(_filtered));\n\t\t\t}\n\t\t\treturn -1;\n\t\t}\n\t\tconst auto i = ranges::find(_rows, _chosen, &Entry::id);\n\t\tAssert(i != end(_rows));\n\t\treturn int(i - begin(_rows));\n\t}();\n\tif (index >= 0) {\n\t\tconst auto top = _rowsSkip + index * _rowHeight;\n\t\tconst auto use = std::max(top - (_minHeight - _rowHeight) / 2, 0);\n\t\t_scrollTo({ use, use + _minHeight }, animated);\n\t}\n}\n\nint Selector::resizeGetHeight(int newWidth) {\n\tconst auto added = 2 * _rowsSkip;\n\treturn std::max(added + shownRowsCount() * _rowHeight, _minHeight);\n}\n\nvoid Selector::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto rows = shownRowsCount();\n\tif (!rows) {\n\t\tp.setFont(st::normalFont);\n\t\tp.setPen(st::windowSubTextFg);\n\t\tp.drawText(\n\t\t\tQRect(0, 0, width(), height() * 2 / 3),\n\t\t\ttr::lng_font_not_found(tr::now),\n\t\t\tstyle::al_center);\n\t\treturn;\n\t}\n\tconst auto clip = e->rect();\n\tconst auto clipped = std::max(clip.y() - _rowsSkip, 0);\n\tconst auto from = std::min(clipped / _rowHeight, rows);\n\tconst auto till = std::min(\n\t\t(clip.y() + clip.height() - _rowsSkip + _rowHeight - 1) / _rowHeight,\n\t\trows);\n\tconst auto active = (_pressed >= 0) ? _pressed : _selected;\n\tfor (auto i = from; i != till; ++i) {\n\t\tauto &row = shownRowAt(i);\n\t\tconst auto y = _rowsSkip + i * _rowHeight;\n\t\tconst auto bg = (i == active) ? st::windowBgOver : st::windowBg;\n\t\tconst auto rect = QRect(0, y, width(), _rowHeight);\n\t\tp.fillRect(rect, bg);\n\n\t\tif (row.ripple) {\n\t\t\trow.ripple->paint(p, 0, y, width());\n\t\t\tif (row.ripple->empty()) {\n\t\t\t\trow.ripple = nullptr;\n\t\t\t}\n\t\t}\n\n\t\tvalidateCache(row);\n\t\tp.drawImage(0, y, row.cache);\n\n\t\tif (!row.check) {\n\t\t\trow.check = std::make_unique<Ui::RadioView>(\n\t\t\t\tst::langsRadio,\n\t\t\t\t(row.id == _chosen),\n\t\t\t\t[=, row = &row] { updateRow(row, i); });\n\t\t}\n\t\trow.check->paint(\n\t\t\tp,\n\t\t\t_st.iconLeft,\n\t\t\ty + (_rowHeight - st::langsRadio.diameter) / 2,\n\t\t\twidth());\n\t}\n}\n\nvoid Selector::leaveEventHook(QEvent *e) {\n\t_lastGlobalPoint = std::nullopt;\n\tif (!_selectedByKeyboard) {\n\t\tupdateSelected(-1);\n\t}\n}\n\nvoid Selector::mouseMoveEvent(QMouseEvent *e) {\n\tif (!_lastGlobalPoint) {\n\t\t_lastGlobalPoint = e->globalPos();\n\t\tif (_selectedByKeyboard) {\n\t\t\treturn;\n\t\t}\n\t} else if (*_lastGlobalPoint == e->globalPos() && _selectedByKeyboard) {\n\t\treturn;\n\t} else {\n\t\t_lastGlobalPoint = e->globalPos();\n\t}\n\t_selectedByKeyboard = false;\n\tconst auto y = e->y() - _rowsSkip;\n\tconst auto index = (y >= 0) ? (y / _rowHeight) : -1;\n\tupdateSelected((index >= 0 && index < shownRowsCount()) ? index : -1);\n}\n\nvoid Selector::mousePressEvent(QMouseEvent *e) {\n\tupdatePressed(_selected);\n\tif (_pressed >= 0) {\n\t\taddRipple(_pressed, e->pos());\n\t}\n}\n\nvoid Selector::mouseReleaseEvent(QMouseEvent *e) {\n\tconst auto pressed = _pressed;\n\tupdatePressed(-1);\n\tif (pressed >= 0 && pressed == _selected) {\n\t\tchoose(shownRowAt(pressed));\n\t}\n}\n\nvoid Selector::choose(Entry &row) {\n\tconst auto id = row.id;\n\tif (_chosen != id) {\n\t\tconst auto i = ranges::find(_rows, _chosen, &Entry::id);\n\t\tAssert(i != end(_rows));\n\t\tif (i->check) {\n\t\t\ti->check->setChecked(false, anim::type::normal);\n\t\t}\n\t\t_chosen = id;\n\t\tif (row.check) {\n\t\t\trow.check->setChecked(true, anim::type::normal);\n\t\t}\n\t}\n\tconst auto animated = searching()\n\t\t? anim::type::instant\n\t\t: anim::type::normal;\n\t_callback(id);\n\tinitScroll(animated);\n}\n\nvoid Selector::addRipple(int index, QPoint position) {\n\tExpects(index >= 0 && index < shownRowsCount());\n\n\tconst auto row = &shownRowAt(index);\n\tif (!row->ripple) {\n\t\trow->ripple = std::make_unique<Ui::RippleAnimation>(\n\t\t\tst::defaultRippleAnimation,\n\t\t\tUi::RippleAnimation::RectMask({ width(), _rowHeight }),\n\t\t\t[=] { updateRow(row, index); });\n\t}\n\trow->ripple->add(position - QPoint(0, _rowsSkip + index * _rowHeight));\n}\n\nstd::vector<Selector::Entry> Selector::FullList(const QString &now) {\n\tusing namespace TextUtilities;\n\n\tauto database = QFontDatabase();\n\tauto families = database.families();\n\tauto result = std::vector<Entry>();\n\tresult.reserve(families.size() + 3);\n\tconst auto add = [&](const QString &text, const QString &id = {}) {\n\t\tresult.push_back({\n\t\t\t.id = id,\n\t\t\t.text = text,\n\t\t\t.keywords = PrepareSearchWords(text),\n\t\t});\n\t};\n\tadd(tr::lng_font_default(tr::now));\n\tadd(tr::lng_font_system(tr::now), style::SystemFontTag());\n\tfor (const auto &family : families) {\n\t\tif (database.isScalable(family)) {\n\t\t\tresult.push_back({ .id = family });\n\t\t}\n\t}\n\tauto nowIt = ranges::find(result, now, &Entry::id);\n\tif (nowIt == end(result)) {\n\t\tresult.push_back({ .id = now });\n\t\tnowIt = end(result) - 1;\n\t}\n\tfor (auto i = begin(result) + 2; i != end(result); ++i) {\n\t\ti->key = TextUtilities::RemoveAccents(i->id).toLower();\n\t\ti->text = i->id;\n\t\ti->keywords = TextUtilities::PrepareSearchWords(i->id);\n\t}\n\tauto skip = 2;\n\tif (nowIt - begin(result) >= skip) {\n\t\tstd::swap(result[2], *nowIt);\n\t\t++skip;\n\t}\n\tranges::sort(\n\t\tbegin(result) + skip, end(result),\n\t\tstd::less<>(),\n\t\t&Entry::key);\n\treturn result;\n}\n\n[[nodiscard]] PreviewRequest PrepareRequest(const QString &family) {\n\treturn {\n\t\t.family = family,\n\t\t.msgBg = st::msgInBg->c,\n\t\t.msgShadow = st::msgInShadow->c,\n\t\t.replyBar = st::msgInReplyBarColor->c,\n\t\t.replyNameFg = st::msgInServiceFg->c,\n\t\t.textFg = st::historyTextInFg->c,\n\t\t.bubbleTail = st::historyBubbleTailInLeft.instance(st::msgInBg->c),\n\t};\n}\n\nPreviewPainter::PreviewPainter(const QImage &bg, PreviewRequest request)\n: _request(request)\n, _msgBg(_request.msgBg)\n, _msgShadow(_request.msgShadow)\n, _nameFontOwned(_request.family, style::FontFlag::Semibold, st::fsize)\n, _nameFont(_nameFontOwned.font())\n, _nameStyle(st::semiboldTextStyle)\n, _textFontOwned(_request.family, 0, st::fsize)\n, _textFont(_textFontOwned.font())\n, _textStyle(st::defaultTextStyle) {\n\t_nameStyle.font = _nameFont;\n\t_textStyle.font = _textFont;\n\n\tlayout();\n\n\tconst auto ratio = style::DevicePixelRatio();\n\t_result = QImage(\n\t\t_outer * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_result.setDevicePixelRatio(ratio);\n\n\tauto p = Painter(&_result);\n\tp.drawImage(0, 0, bg);\n\n\tp.translate(_bubble.topLeft());\n\tpaintBubble(p);\n}\n\nvoid PreviewPainter::paintBubble(Painter &p) {\n\tvalidateBubbleCache();\n\tconst auto bubble = QRect(QPoint(), _bubble.size());\n\tconst auto cornerShadow = _bubbleShadowBottomRight.size()\n\t\t/ _bubbleShadowBottomRight.devicePixelRatio();\n\tp.drawPixmap(\n\t\tbubble.width() - cornerShadow.width(),\n\t\tbubble.height() + st::msgShadow - cornerShadow.height(),\n\t\t_bubbleShadowBottomRight);\n\tUi::FillRoundRect(p, bubble, _msgBg.color(), _bubbleCorners);\n\tconst auto &bubbleTail = _request.bubbleTail;\n\tconst auto tail = bubbleTail.size() / bubbleTail.devicePixelRatio();\n\tp.drawImage(-tail.width(), bubble.height() - tail.height(), bubbleTail);\n\tp.fillRect(\n\t\t-tail.width(),\n\t\tbubble.height(),\n\t\ttail.width() + bubble.width() - cornerShadow.width(),\n\t\tst::msgShadow,\n\t\t_request.msgShadow);\n\tp.translate(_content.topLeft());\n\tconst auto local = _content.translated(-_content.topLeft());\n\tp.setClipRect(local);\n\tpaintContent(p);\n}\n\nvoid PreviewPainter::validateBubbleCache() {\n\tif (!_bubbleCorners.p[0].isNull()) {\n\t\treturn;\n\t}\n\tconst auto radius = st::bubbleRadiusLarge;\n\t_bubbleCorners = Ui::PrepareCornerPixmaps(radius, _msgBg.color());\n\t_bubbleCorners.p[2] = {};\n\t_bubbleShadowBottomRight\n\t\t= Ui::PrepareCornerPixmaps(radius, _msgShadow.color()).p[3];\n}\n\nvoid PreviewPainter::paintContent(Painter &p) {\n\tpaintReply(p);\n\n\tp.translate(_message.topLeft());\n\tconst auto local = _message.translated(-_message.topLeft());\n\tp.setClipRect(local);\n\tpaintMessage(p);\n}\n\nvoid PreviewPainter::paintReply(Painter &p) {\n\t{\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(_request.replyBar);\n\n\t\tconst auto outline = st::messageTextStyle.blockquote.outline;\n\t\tconst auto radius = st::messageTextStyle.blockquote.radius;\n\t\tp.setOpacity(Ui::kDefaultOutline1Opacity);\n\t\tp.setClipRect(\n\t\t\t_replyRect.x(),\n\t\t\t_replyRect.y(),\n\t\t\toutline,\n\t\t\t_replyRect.height());\n\t\tp.drawRoundedRect(_replyRect, radius, radius);\n\t\tp.setOpacity(Ui::kDefaultBgOpacity);\n\t\tp.setClipRect(\n\t\t\t_replyRect.x() + outline,\n\t\t\t_replyRect.y(),\n\t\t\t_replyRect.width() - outline,\n\t\t\t_replyRect.height());\n\t\tp.drawRoundedRect(_replyRect, radius, radius);\n\t}\n\tp.setOpacity(1.);\n\tp.setClipping(false);\n\n\tp.setPen(_request.replyNameFg);\n\t_nameText.drawLeftElided(\n\t\tp,\n\t\t_name.x(),\n\t\t_name.y(),\n\t\t_name.width(),\n\t\t_outer.width());\n\n\tp.setPen(_request.textFg);\n\t_replyText.drawLeftElided(\n\t\tp,\n\t\t_reply.x(),\n\t\t_reply.y(),\n\t\t_reply.width(),\n\t\t_outer.width());\n}\n\nvoid PreviewPainter::paintMessage(Painter &p) {\n\tp.setPen(_request.textFg);\n\t_messageText.drawLeft(p, 0, 0, _message.width(), _message.width());\n}\n\nQImage PreviewPainter::takeResult() {\n\treturn std::move(_result);\n}\n\nvoid PreviewPainter::layout() {\n\tconst auto skip = st::boxRowPadding.left();\n\tconst auto minTextWidth = style::ConvertScale(kMinTextWidth);\n\tconst auto maxTextWidth = st::boxWidth\n\t\t- 2 * skip\n\t\t- st::msgPadding.left()\n\t\t- st::msgPadding.right();\n\n\t_nameText = Ui::Text::String(\n\t\t_nameStyle,\n\t\ttr::lng_settings_chat_message_reply_from(tr::now));\n\t_replyText = Ui::Text::String(\n\t\t_textStyle,\n\t\ttr::lng_background_text2(tr::now));\n\t_messageText = Ui::Text::String(\n\t\t_textStyle,\n\t\ttr::lng_background_text1(tr::now),\n\t\tkDefaultTextOptions,\n\t\tst::msgMinWidth / 2);\n\n\tconst auto namePosition = QPoint(\n\t\tst::historyReplyPadding.left(),\n\t\tst::historyReplyPadding.top());\n\tconst auto replyPosition = QPoint(\n\t\tst::historyReplyPadding.left(),\n\t\t(st::historyReplyPadding.top() + _nameFont->height));\n\tconst auto paddingRight = st::historyReplyPadding.right();\n\n\tconst auto wantedWidth = std::max({\n\t\tnamePosition.x() + _nameText.maxWidth() + paddingRight,\n\t\treplyPosition.x() + _replyText.maxWidth() + paddingRight,\n\t\t_messageText.maxWidth()\n\t});\n\n\tconst auto messageWidth = std::clamp(\n\t\twantedWidth,\n\t\tminTextWidth,\n\t\tmaxTextWidth);\n\tconst auto messageHeight = _messageText.countHeight(messageWidth);\n\n\t_replyRect = QRect(\n\t\tst::msgReplyBarPos.x(),\n\t\tst::historyReplyTop,\n\t\tmessageWidth,\n\t\t(st::historyReplyPadding.top()\n\t\t\t+ _nameFont->height\n\t\t\t+ _textFont->height\n\t\t\t+ st::historyReplyPadding.bottom()));\n\n\t_name = QRect(\n\t\t_replyRect.topLeft() + namePosition,\n\t\tQSize(messageWidth - namePosition.x(), _nameFont->height));\n\t_reply = QRect(\n\t\t_replyRect.topLeft() + replyPosition,\n\t\tQSize(messageWidth - replyPosition.x(), _textFont->height));\n\t_message = QRect(0, 0, messageWidth, messageHeight);\n\n\tconst auto replySkip = _replyRect.y()\n\t\t+ _replyRect.height()\n\t\t+ st::historyReplyBottom;\n\t_message.moveTop(replySkip);\n\n\t_content = QRect(0, 0, messageWidth, replySkip + messageHeight);\n\n\tconst auto msgPadding = st::msgPadding;\n\t_bubble = _content.marginsAdded(msgPadding);\n\t_content.moveTopLeft(-_bubble.topLeft());\n\t_bubble.moveTopLeft({});\n\n\t_outer = QSize(st::boxWidth, st::boxWidth / 2);\n\n\t_bubble.moveTopLeft({ skip, std::max(\n\t\t(_outer.height() - _bubble.height()) / 2,\n\t\tst::msgMargin.top()) });\n}\n\n[[nodiscard]] QImage GeneratePreview(\n\t\tconst QImage &bg,\n\t\tPreviewRequest request) {\n\treturn PreviewPainter(bg, request).takeResult();\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakePreview(\n\t\tnot_null<QWidget*> parent,\n\t\tFn<QImage()> generatePreviewBg,\n\t\trpl::producer<QString> family) {\n\tauto result = object_ptr<Ui::RpWidget>(parent.get());\n\tconst auto raw = result.data();\n\n\tstruct State {\n\t\tQImage preview;\n\t\tQImage bg;\n\t\tQString family;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>();\n\n\tstate->bg = generatePreviewBg();\n\tstyle::PaletteChanged() | rpl::on_next([=] {\n\t\tstate->bg = generatePreviewBg();\n\t}, raw->lifetime());\n\n\trpl::combine(\n\t\trpl::single(rpl::empty) | rpl::then(style::PaletteChanged()),\n\t\tstd::move(family)\n\t) | rpl::on_next([=](const auto &, QString family) {\n\t\tstate->family = family;\n\t\tif (state->preview.isNull()) {\n\t\t\tstate->preview = GeneratePreview(\n\t\t\t\tstate->bg,\n\t\t\t\tPrepareRequest(family));\n\t\t\tconst auto ratio = state->preview.devicePixelRatio();\n\t\t\traw->resize(state->preview.size() / int(ratio));\n\t\t} else {\n\t\t\tconst auto weak = base::make_weak(raw);\n\t\t\tconst auto request = PrepareRequest(family);\n\t\t\tcrl::async([=, bg = state->bg] {\n\t\t\t\tcrl::on_main([\n\t\t\t\t\tweak,\n\t\t\t\t\tstate,\n\t\t\t\t\tpreview = GeneratePreview(bg, request)\n\t\t\t\t]() mutable {\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstate->preview = std::move(preview);\n\t\t\t\t\t\tconst auto ratio = state->preview.devicePixelRatio();\n\t\t\t\t\t\tstrong->resize(\n\t\t\t\t\t\t\tstrong->width(),\n\t\t\t\t\t\t\t(state->preview.height() / int(ratio)));\n\t\t\t\t\t\tstrong->update();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t}, raw->lifetime());\n\n\traw->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tQPainter(raw).drawImage(0, 0, state->preview);\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\n} // namespace\n\nvoid ChooseFontBox(\n\t\tnot_null<GenericBox*> box,\n\t\tFn<QImage()> generatePreviewBg,\n\t\tconst QString &family,\n\t\tFn<void(QString)> save) {\n\tbox->setTitle(tr::lng_font_box_title());\n\n\tstruct State {\n\t\trpl::variable<QString> family;\n\t\trpl::variable<QString> query;\n\t\trpl::event_stream<> submits;\n\t};\n\tconst auto state = box->lifetime().make_state<State>(State{\n\t\t.family = family,\n\t});\n\n\tconst auto top = box->setPinnedToTopContent(\n\t\tobject_ptr<Ui::VerticalLayout>(box));\n\ttop->add(MakePreview(top, generatePreviewBg, state->family.value()));\n\tconst auto filter = top->add(object_ptr<Ui::MultiSelect>(\n\t\ttop,\n\t\tst::defaultMultiSelect,\n\t\ttr::lng_participant_filter()));\n\ttop->resizeToWidth(st::boxWidth);\n\n\tfilter->setSubmittedCallback([=](Qt::KeyboardModifiers) {\n\t\tstate->submits.fire({});\n\t});\n\tfilter->setQueryChangedCallback([=](const QString &query) {\n\t\tstate->query = query;\n\t});\n\tfilter->setCancelledCallback([=] {\n\t\tfilter->clearQuery();\n\t});\n\n\tconst auto chosen = [=](const QString &value) {\n\t\tstate->family = value;\n\t\tfilter->clearQuery();\n\t};\n\tconst auto scrollTo = [=](\n\t\t\tUi::ScrollToRequest request,\n\t\t\tanim::type animated) {\n\t\tbox->scrollTo(request, animated);\n\t};\n\tconst auto selector = box->addRow(\n\t\tobject_ptr<Selector>(\n\t\t\tbox,\n\t\t\tstate->family.current(),\n\t\t\tstate->query.value(),\n\t\t\tstate->submits.events(),\n\t\t\tchosen,\n\t\t\tscrollTo),\n\t\tQMargins());\n\tbox->setMinHeight(st::boxMaxListHeight);\n\tbox->setMaxHeight(st::boxMaxListHeight);\n\n\tbase::install_event_filter(filter, [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::KeyPress) {\n\t\t\tconst auto key = static_cast<QKeyEvent*>(e.get())->key();\n\t\t\tif (key == Qt::Key_Up\n\t\t\t\t|| key == Qt::Key_Down\n\t\t\t\t|| key == Qt::Key_PageUp\n\t\t\t\t|| key == Qt::Key_PageDown) {\n\t\t\t\tselector->selectSkip(Qt::Key(key));\n\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t}\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\trpl::combine(\n\t\tbox->heightValue(),\n\t\ttop->heightValue()\n\t) | rpl::on_next([=](int box, int top) {\n\t\tselector->setMinHeight(box - top);\n\t}, selector->lifetime());\n\n\tconst auto apply = [=](QString chosen) {\n\t\tif (chosen == family) {\n\t\t\tbox->closeBox();\n\t\t\treturn;\n\t\t}\n\t\tbox->getDelegate()->show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_settings_need_restart(),\n\t\t\t.confirmed = [=] { save(chosen); },\n\t\t\t.confirmText = tr::lng_settings_restart_now(),\n\t\t}));\n\t};\n\tconst auto refreshButtons = [=](QString chosen) {\n\t\tbox->clearButtons();\n\t\t// Doesn't fit in most languages.\n\t\t//if (!chosen.isEmpty()) {\n\t\t//\tbox->addLeftButton(tr::lng_background_reset_default(), [=] {\n\t\t//\t\tapply(QString());\n\t\t//\t});\n\t\t//}\n\t\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\t\tapply(chosen);\n\t\t});\n\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t};\n\tstate->family.value(\n\t) | rpl::on_next(refreshButtons, box->lifetime());\n\n\tbox->setFocusCallback([=] {\n\t\tfilter->setInnerFocus();\n\t});\n\tbox->setInitScrollCallback([=] {\n\t\tSendPendingMoveResizeEvents(box);\n\t\tselector->initScroll(anim::type::instant);\n\t});\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/choose_font_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nclass GenericBox;\n\nvoid ChooseFontBox(\n\tnot_null<GenericBox*> box,\n\tFn<QImage()> generatePreviewBg,\n\tconst QString &family,\n\tFn<void(QString)> save);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/choose_language_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/choose_language_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"spellcheck/spellcheck_types.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/multi_select.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/painter.h\"\n#include \"base/debug_log.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Ui {\nnamespace {\n\nconst auto kLanguageNamePrefix = \"cloud_lng_language_\";\nconst auto kTranslateToPrefix = \"cloud_lng_translate_to_\";\n\n[[nodiscard]] std::vector<LanguageId> TranslationLanguagesList() {\n\t// If adding some languages here you need to check that it is\n\t// supported on the server. Right now server supports those:\n\t//\n\t// 'af', 'sq', 'am', 'ar', 'hy', 'az', 'eu', 'be', 'bn', 'bs', 'bg',\n\t// 'ca', 'ceb', 'zh-CN', 'zh', 'zh-TW', 'co', 'hr', 'cs', 'da', 'nl',\n\t// 'en', 'eo', 'et', 'fi', 'fr', 'fy', 'gl', 'ka', 'de', 'el', 'gu',\n\t// 'ht', 'ha', 'haw', 'he', 'iw', 'hi', 'hmn', 'hu', 'is', 'ig', 'id',\n\t// 'ga', 'it', 'ja', 'jv', 'kn', 'kk', 'km', 'rw', 'ko', 'ku', 'ky',\n\t// 'lo', 'la', 'lv', 'lt', 'lb', 'mk', 'mg', 'ms', 'ml', 'mt', 'mi',\n\t// 'mr', 'mn', 'my', 'ne', 'no', 'ny', 'or', 'ps', 'fa', 'pl', 'pt',\n\t// 'pa', 'ro', 'ru', 'sm', 'gd', 'sr', 'st', 'sn', 'sd', 'si', 'sk',\n\t// 'sl', 'so', 'es', 'su', 'sw', 'sv', 'tl', 'tg', 'ta', 'tt', 'te',\n\t// 'th', 'tr', 'tk', 'uk', 'ur', 'ug', 'uz', 'vi', 'cy', 'xh', 'yi',\n\t// 'yo', 'zu',\n\treturn {\n\t\t{ QLocale::English },\n\t\t{ QLocale::Arabic },\n\t\t{ QLocale::Belarusian },\n\t\t{ QLocale::Catalan },\n\t\t{ QLocale::Chinese },\n\t\t{ QLocale::Dutch },\n\t\t{ QLocale::French },\n\t\t{ QLocale::German },\n\t\t{ QLocale::Indonesian },\n\t\t{ QLocale::Italian },\n\t\t{ QLocale::Japanese },\n\t\t{ QLocale::Korean },\n\t\t{ QLocale::Polish },\n\t\t{ QLocale::Portuguese },\n\t\t{ QLocale::Russian },\n\t\t{ QLocale::Spanish },\n\t\t{ QLocale::Ukrainian },\n\n\t\t{ QLocale::Afrikaans },\n\t\t{ QLocale::Albanian },\n\t\t{ QLocale::Amharic },\n\t\t{ QLocale::Armenian },\n\t\t{ QLocale::Azerbaijani },\n\t\t{ QLocale::Basque },\n\t\t{ QLocale::Bosnian },\n\t\t{ QLocale::Bulgarian },\n\t\t{ QLocale::Burmese },\n\t\t{ QLocale::Croatian },\n\t\t{ QLocale::Czech },\n\t\t{ QLocale::Danish },\n\t\t{ QLocale::Esperanto },\n\t\t{ QLocale::Estonian },\n\t\t{ QLocale::Finnish },\n\t\t{ QLocale::Gaelic },\n\t\t{ QLocale::Galician },\n\t\t{ QLocale::Georgian },\n\t\t{ QLocale::Greek },\n\t\t{ QLocale::Gusii },\n\t\t{ QLocale::Hausa },\n\t\t{ QLocale::Hebrew },\n\t\t{ QLocale::Hungarian },\n\t\t{ QLocale::Icelandic },\n\t\t{ QLocale::Igbo },\n\t\t{ QLocale::Irish },\n\t\t{ QLocale::Kazakh },\n\t\t{ QLocale::Kinyarwanda },\n\t\t{ QLocale::Kurdish },\n\t\t{ QLocale::Lao },\n\t\t{ QLocale::Latvian },\n\t\t{ QLocale::Lithuanian },\n\t\t{ QLocale::Luxembourgish },\n\t\t{ QLocale::Macedonian },\n\t\t{ QLocale::Malagasy },\n\t\t{ QLocale::Malay },\n\t\t{ QLocale::Maltese },\n\t\t{ QLocale::Maori },\n\t\t{ QLocale::Mongolian },\n\t\t{ QLocale::Nepali },\n\t\t{ QLocale::Pashto },\n\t\t{ QLocale::Persian },\n\t\t{ QLocale::Romanian },\n\t\t{ QLocale::Serbian },\n\t\t{ QLocale::Shona },\n\t\t{ QLocale::Sindhi },\n\t\t{ QLocale::Sinhala },\n\t\t{ QLocale::Slovak },\n\t\t{ QLocale::Slovenian },\n\t\t{ QLocale::Somali },\n\t\t{ QLocale::Sundanese },\n\t\t{ QLocale::Swahili },\n\t\t{ QLocale::Swedish },\n\t\t{ QLocale::Tajik },\n\t\t{ QLocale::Tatar },\n\t\t{ QLocale::Teso },\n\t\t{ QLocale::Thai },\n\t\t{ QLocale::Turkish },\n\t\t{ QLocale::Turkmen },\n\t\t{ QLocale::Urdu },\n\t\t{ QLocale::Uzbek },\n\t\t{ QLocale::Vietnamese },\n\t\t{ QLocale::Welsh },\n\t\t{ QLocale::WesternFrisian },\n\t\t{ QLocale::Xhosa },\n\t\t{ QLocale::Yiddish },\n\t};\n}\n\nclass Row final : public SettingsButton {\npublic:\n\tRow(not_null<RpWidget*> parent, LanguageId id);\n\n\t[[nodiscard]] bool filtered(const QString &query) const;\n\t[[nodiscard]] LanguageId id() const;\n\n\tint resizeGetHeight(int newWidth) override;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tconst style::PeerListItem &_st;\n\tconst LanguageId _id;\n\tconst QString _status;\n\tconst QString _titleText;\n\tText::String _title;\n\n};\n\nRow::Row(not_null<RpWidget*> parent, LanguageId id)\n: SettingsButton(parent, rpl::never<QString>())\n, _st(st::inviteLinkListItem)\n, _id(id)\n, _status(LanguageName(id))\n, _titleText(LanguageNameNative(id))\n, _title(_st.nameStyle, _titleText) {\n}\n\nLanguageId Row::id() const {\n\treturn _id;\n}\n\nbool Row::filtered(const QString &query) const {\n\treturn _status.startsWith(query, Qt::CaseInsensitive)\n\t\t|| _titleText.startsWith(query, Qt::CaseInsensitive);\n}\n\nint Row::resizeGetHeight(int newWidth) {\n\treturn _st.height;\n}\n\nvoid Row::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\n\tconst auto paintOver = (isOver() || isDown()) && !isDisabled();\n\tSettingsButton::paintBg(p, e->rect(), paintOver);\n\tSettingsButton::paintRipple(p, 0, 0);\n\tSettingsButton::paintToggle(p, width());\n\n\tconst auto &color = st::windowSubTextFg;\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(color);\n\n\tconst auto left = st::defaultSubsectionTitlePadding.left();\n\tconst auto toggleRect = SettingsButton::maybeToggleRect();\n\tconst auto right = left\n\t\t+ (toggleRect.isEmpty() ? 0 : (width() - toggleRect.x()));\n\n\tconst auto availableWidth = std::min(\n\t\t_title.maxWidth(),\n\t\twidth() - left - right);\n\tp.setPen(_st.nameFg);\n\t_title.drawLeft(\n\t\tp,\n\t\tleft,\n\t\t_st.namePosition.y(),\n\t\tavailableWidth,\n\t\twidth() - left - right);\n\n\tp.setPen(paintOver ? _st.statusFgOver : _st.statusFg);\n\tp.setFont(st::contactsStatusFont);\n\tp.drawTextLeft(\n\t\tleft,\n\t\t_st.statusPosition.y(),\n\t\twidth() - left - right,\n\t\t_status);\n}\n\n} // namespace\n\nQString LanguageNameTranslated(const QString &twoLetterCode) {\n\treturn Lang::GetNonDefaultValue(\n\t\tkLanguageNamePrefix + twoLetterCode.toUtf8());\n}\n\nQString LanguageNameLocal(LanguageId id) {\n\treturn QLocale::languageToString(id.language());\n}\n\nQString LanguageName(LanguageId id) {\n\tconst auto translated = LanguageNameTranslated(id.twoLetterCode());\n\treturn translated.isEmpty() ? LanguageNameLocal(id) : translated;\n}\n\nQString LanguageNameNative(LanguageId id) {\n\tconst auto locale = id.locale();\n\tif (locale.language() == QLocale::English\n\t\t\t&& (locale.country() == QLocale::UnitedStates\n\t\t\t\t|| locale.country() == QLocale::AnyCountry)) {\n\t\treturn u\"English\"_q;\n\t} else if (locale.language() == QLocale::Spanish) {\n\t\treturn QString::fromUtf8(\"\\x45\\x73\\x70\\x61\\xc3\\xb1\\x6f\\x6c\");\n\t} else {\n\t\tconst auto name = locale.nativeLanguageName();\n\t\treturn name.left(1).toUpper() + name.mid(1);\n\t}\n}\n\nrpl::producer<QString> TranslateBarTo(LanguageId id) {\n\tconst auto translated = Lang::GetNonDefaultValue(\n\t\tkTranslateToPrefix + id.twoLetterCode().toUtf8());\n\treturn (translated.isEmpty()\n\t\t? tr::lng_translate_bar_to_other\n\t\t: tr::lng_translate_bar_to)(\n\t\t\tlt_name,\n\t\t\trpl::single(translated.isEmpty()\n\t\t\t\t? LanguageNameLocal(id)\n\t\t\t\t: translated));\n}\n\nQString TranslateMenuDont(tr::now_t, LanguageId id) {\n\tconst auto translated = Lang::GetNonDefaultValue(\n\t\tkTranslateToPrefix + id.twoLetterCode().toUtf8());\n\treturn (translated.isEmpty()\n\t\t? tr::lng_translate_menu_dont_other\n\t\t: tr::lng_translate_menu_dont)(\n\t\t\ttr::now,\n\t\t\tlt_name,\n\t\t\ttranslated.isEmpty() ? LanguageNameLocal(id) : translated);\n}\n\nvoid ChooseLanguageBox(\n\t\tnot_null<GenericBox*> box,\n\t\trpl::producer<QString> title,\n\t\tFn<void(std::vector<LanguageId>)> callback,\n\t\tstd::vector<LanguageId> selected,\n\t\tbool multiselect,\n\t\tFn<bool(LanguageId)> toggleCheck) {\n\tbox->setMinHeight(st::boxWidth);\n\tbox->setMaxHeight(st::boxWidth);\n\tbox->setTitle(std::move(title));\n\n\tconst auto multiSelect = box->setPinnedToTopContent(\n\t\tobject_ptr<MultiSelect>(\n\t\t\tbox,\n\t\t\tst::defaultMultiSelect,\n\t\t\ttr::lng_participant_filter()));\n\tbox->setFocusCallback([=] { multiSelect->setInnerFocus(); });\n\n\tconst auto container = box->verticalLayout();\n\tconst auto langs = [&] {\n\t\tauto list = TranslationLanguagesList();\n\t\tfor (const auto id : list) {\n\t\t\tLOG((\"cloud_lng_language_%1\").arg(id.twoLetterCode()));\n\t\t}\n\t\tconst auto current = LanguageId{ QLocale(\n\t\t\tLang::LanguageIdOrDefault(Lang::Id())).language() };\n\t\tif (const auto i = ranges::find(list, current); i != end(list)) {\n\t\t\tbase::reorder(list, std::distance(begin(list), i), 0);\n\t\t}\n\t\tranges::stable_partition(list, [&](LanguageId id) {\n\t\t\treturn ranges::contains(selected, id);\n\t\t});\n\t\treturn list;\n\t}();\n\tstruct ToggleOne {\n\t\tLanguageId id;\n\t\tbool selected = false;\n\t};\n\tstruct State {\n\t\trpl::event_stream<ToggleOne> toggles;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tauto rows = std::vector<not_null<SlideWrap<Row>*>>();\n\trows.reserve(langs.size());\n\tfor (const auto &id : langs) {\n\t\tconst auto button = container->add(\n\t\t\tobject_ptr<SlideWrap<Row>>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<Row>(container, id)));\n\t\tif (multiselect) {\n\t\t\tbutton->entity()->toggleOn(rpl::single(\n\t\t\t\tranges::contains(selected, id)\n\t\t\t) | rpl::then(state->toggles.events(\n\t\t\t) | rpl::filter([=](ToggleOne one) {\n\t\t\t\treturn one.id == id;\n\t\t\t}) | rpl::map([=](ToggleOne one) {\n\t\t\t\treturn one.selected;\n\t\t\t})));\n\n\t\t\tbutton->entity()->toggledChanges(\n\t\t\t) | rpl::on_next([=](bool value) {\n\t\t\t\tif (toggleCheck && !toggleCheck(id)) {\n\t\t\t\t\tstate->toggles.fire({ .id = id, .selected = !value });\n\t\t\t\t}\n\t\t\t}, button->lifetime());\n\t\t} else {\n\t\t\tbutton->entity()->setClickedCallback([=] {\n\t\t\t\tcallback({ id });\n\t\t\t\tbox->closeBox();\n\t\t\t});\n\t\t}\n\t\trows.push_back(button);\n\t}\n\n\tmultiSelect->setQueryChangedCallback([=](const QString &query) {\n\t\tfor (const auto &row : rows) {\n\t\t\tconst auto toggled = row->entity()->filtered(query);\n\t\t\tif (toggled != row->toggled()) {\n\t\t\t\trow->toggle(toggled, anim::type::instant);\n\t\t\t}\n\t\t}\n\t});\n\n\t{\n\t\tconst auto label = CreateChild<FlatLabel>(\n\t\t\tbox.get(),\n\t\t\ttr::lng_languages_none(),\n\t\t\tst::membersAbout);\n\t\tbox->verticalLayout()->geometryValue(\n\t\t) | rpl::on_next([=](const QRect &geometry) {\n\t\t\tconst auto shown = (geometry.height() <= 0);\n\t\t\tlabel->setVisible(shown);\n\t\t\tif (shown) {\n\t\t\t\tlabel->moveToLeft(\n\t\t\t\t\t(geometry.width() - label->width()) / 2,\n\t\t\t\t\tgeometry.y() + st::membersAbout.style.font->height * 4);\n\t\t\t\tlabel->stackUnder(box->verticalLayout());\n\t\t\t}\n\t\t}, label->lifetime());\n\t}\n\n\tif (multiselect) {\n\t\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\t\tauto result = ranges::views::all(\n\t\t\t\trows\n\t\t\t) | ranges::views::filter([](const auto &row) {\n\t\t\t\treturn row->entity()->toggled();\n\t\t\t}) | ranges::views::transform([](const auto &row) {\n\t\t\t\treturn row->entity()->id();\n\t\t\t}) | ranges::to_vector;\n\t\t\tif (!result.empty()) {\n\t\t\t\tcallback(std::move(result));\n\t\t\t}\n\t\t\tbox->closeBox();\n\t\t});\n\t}\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/choose_language_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nstruct LanguageId;\n\nnamespace tr {\nstruct now_t;\n} // namespace tr\n\nnamespace Ui {\n\nclass GenericBox;\n\n[[nodiscard]] QString LanguageNameTranslated(const QString &twoLetterCode);\n[[nodiscard]] QString LanguageNameLocal(LanguageId id);\n[[nodiscard]] QString LanguageName(LanguageId id);\n[[nodiscard]] QString LanguageNameNative(LanguageId id);\n\n[[nodiscard]] rpl::producer<QString> TranslateBarTo(LanguageId id);\n[[nodiscard]] QString TranslateMenuDont(tr::now_t, LanguageId id);\n\nvoid ChooseLanguageBox(\n\tnot_null<GenericBox*> box,\n\trpl::producer<QString> title,\n\tFn<void(std::vector<LanguageId>)> callback,\n\tstd::vector<LanguageId> selected,\n\tbool multiselect,\n\tFn<bool(LanguageId)> toggleCheck);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/choose_time.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/choose_time.h\"\n\n#include \"base/qt_signal_producer.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/fields/time_part_input_with_placeholder.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Ui {\n\nChooseTimeResult ChooseTimeWidget(\n\t\tnot_null<RpWidget*> parent,\n\t\tTimeId startSeconds,\n\t\tbool hiddenDaysInput) {\n\tusing TimeField = Ui::TimePartWithPlaceholder;\n\tconst auto putNext = [](not_null<TimeField*> field, QChar ch) {\n\t\tfield->setCursorPosition(0);\n\t\tif (ch.unicode()) {\n\t\t\tfield->setText(ch + field->getLastText());\n\t\t\tfield->setCursorPosition(1);\n\t\t}\n\t\tfield->onTextEdited();\n\t\tfield->setFocus();\n\t};\n\n\tconst auto erasePrevious = [](not_null<TimeField*> field) {\n\t\tconst auto text = field->getLastText();\n\t\tif (!text.isEmpty()) {\n\t\t\tfield->setCursorPosition(text.size() - 1);\n\t\t\tfield->setText(text.mid(0, text.size() - 1));\n\t\t}\n\t\tfield->setFocus();\n\t};\n\n\tstruct State {\n\t\tnot_null<TimeField*> day;\n\t\tnot_null<TimeField*> hour;\n\t\tnot_null<TimeField*> minute;\n\n\t\trpl::variable<int> valueInSeconds = 0;\n\t};\n\n\tauto content = object_ptr<Ui::FixedHeightWidget>(\n\t\tparent,\n\t\tst::scheduleHeight);\n\n\tconst auto startDays = startSeconds / 86400;\n\tstartSeconds -= startDays * 86400;\n\tconst auto startHours = startSeconds / 3600;\n\tstartSeconds -= startHours * 3600;\n\tconst auto startMinutes = startSeconds / 60;\n\n\tconst auto state = content->lifetime().make_state<State>(State{\n\t\t.day = Ui::CreateChild<TimeField>(\n\t\t\tcontent.data(),\n\t\t\tst::muteBoxTimeField,\n\t\t\trpl::never<QString>(),\n\t\t\tQString::number(startDays)),\n\t\t.hour = Ui::CreateChild<TimeField>(\n\t\t\tcontent.data(),\n\t\t\tst::muteBoxTimeField,\n\t\t\trpl::never<QString>(),\n\t\t\tQString::number(startHours)),\n\t\t.minute = Ui::CreateChild<TimeField>(\n\t\t\tcontent.data(),\n\t\t\tst::muteBoxTimeField,\n\t\t\trpl::never<QString>(),\n\t\t\tQString::number(startMinutes)),\n\t});\n\n\tconst auto day = base::make_weak(state->day);\n\tconst auto hour = base::make_weak(state->hour);\n\tconst auto minute = base::make_weak(state->minute);\n\n\tif (hiddenDaysInput) {\n\t\tday->setVisible(false);\n\t}\n\n\tday->setPhrase(tr::lng_days);\n\tday->setMaxValue(31);\n\tday->setWheelStep(1);\n\tday->putNext() | rpl::on_next([=](QChar ch) {\n\t\tputNext(hour.get(), ch);\n\t}, content->lifetime());\n\n\thour->setPhrase(tr::lng_hours);\n\thour->setMaxValue(23);\n\thour->setWheelStep(1);\n\thour->putNext() | rpl::on_next([=](QChar ch) {\n\t\tputNext(minute.get(), ch);\n\t}, content->lifetime());\n\thour->erasePrevious() | rpl::on_next([=] {\n\t\terasePrevious(day.get());\n\t}, content->lifetime());\n\n\tminute->setPhrase(tr::lng_minutes);\n\tminute->setMaxValue(59);\n\tminute->setWheelStep(10);\n\tminute->erasePrevious() | rpl::on_next([=] {\n\t\terasePrevious(hour.get());\n\t}, content->lifetime());\n\n\tcontent->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tconst auto inputWidth = s.width() / (hiddenDaysInput ? 2 : 3);\n\t\tauto rect = QRect(\n\t\t\t0,\n\t\t\t(s.height() - day->height()) / 2,\n\t\t\tinputWidth,\n\t\t\tday->height());\n\t\tfor (const auto &input : { day, hour, minute }) {\n\t\t\tif (input->isHidden()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tinput->setGeometry(rect - st::muteBoxTimeFieldPadding);\n\t\t\trect.translate(inputWidth, 0);\n\t\t}\n\t}, content->lifetime());\n\n\trpl::merge(\n\t\trpl::single(rpl::empty),\n\t\tbase::qt_signal_producer(day.get(), &MaskedInputField::changed),\n\t\tbase::qt_signal_producer(hour.get(), &MaskedInputField::changed),\n\t\tbase::qt_signal_producer(minute.get(), &MaskedInputField::changed)\n\t) | rpl::on_next([=] {\n\t\tstate->valueInSeconds = 0\n\t\t\t+ day->getLastText().toUInt() * 3600 * 24\n\t\t\t+ hour->getLastText().toUInt() * 3600\n\t\t\t+ minute->getLastText().toUInt() * 60;\n\t}, content->lifetime());\n\treturn {\n\t\tobject_ptr<Ui::RpWidget>::fromRaw(content.release()),\n\t\tstate->valueInSeconds.value(),\n\t};\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/choose_time.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\n\nclass RpWidget;\n\nstruct ChooseTimeResult {\n\tobject_ptr<RpWidget> widget;\n\trpl::producer<TimeId> secondsValue;\n};\n\nChooseTimeResult ChooseTimeWidget(\n\tnot_null<RpWidget*> parent,\n\tTimeId startSeconds,\n\tbool hiddenDaysInput = false);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/collectible_info_box.h\"\n\n#include \"base/unixtime.h\"\n#include \"core/file_utilities.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"info/channel_statistics/earn/earn_format.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/painter.h\"\n#include \"settings/settings_common.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_layers.h\"\n\n#include <QtCore/QRegularExpression>\n#include <QtGui/QGuiApplication>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kTonMultiplier = uint64(1000000000);\n\n[[nodiscard]] QString FormatEntity(CollectibleType type, QString entity) {\n\tswitch (type) {\n\tcase CollectibleType::Phone: {\n\t\tstatic const auto kNonDigits = QRegularExpression(u\"[^\\\\d]\"_q);\n\t\tentity.replace(kNonDigits, QString());\n\t} return Ui::FormatPhone(entity);\n\tcase CollectibleType::Username:\n\t\treturn entity.startsWith('@') ? entity : ('@' + entity);\n\t}\n\tUnexpected(\"CollectibleType in FormatEntity.\");\n}\n\n[[nodiscard]] QString FormatDate(TimeId date) {\n\treturn langDateTime(base::unixtime::parse(date));\n}\n\n[[nodiscard]] TextWithEntities FormatPrice(const CollectibleInfo &info) {\n\tauto minor = Info::ChannelEarn::MinorPart(info.cryptoAmount);\n\tif (minor.size() == 1 && minor.at(0) == '.') {\n\t\tminor += '0';\n\t}\n\tauto price = (info.cryptoCurrency == u\"TON\"_q)\n\t\t? Ui::Text::IconEmoji(\n\t\t\t&st::tonIconEmoji\n\t\t).append(\n\t\t\tInfo::ChannelEarn::MajorPart(info.cryptoAmount)\n\t\t).append(minor)\n\t\t: TextWithEntities{ ('{'\n\t\t\t+ info.cryptoCurrency + ':' + QString::number(info.cryptoAmount)\n\t\t\t+ '}') };\n\tconst auto fiat = Ui::FillAmountAndCurrency(info.amount, info.currency);\n\treturn Ui::Text::Wrapped(\n\t\tprice,\n\t\tEntityType::Bold\n\t).append(u\" (\"_q + fiat + ')');\n}\n\n[[nodiscard]] object_ptr<Ui::RpWidget> MakeOwnerCell(\n\t\tnot_null<QWidget*> parent,\n\t\tconst CollectibleInfo &info) {\n\tconst auto st = &st::defaultMultiSelectItem;\n\tconst auto size = st->height;\n\tauto result = object_ptr<Ui::FixedHeightWidget>(parent.get(), size);\n\tconst auto raw = result.data();\n\n\tconst auto name = info.ownerName;\n\tconst auto userpic = info.ownerUserpic;\n\tconst auto nameWidth = st->style.font->width(name);\n\tconst auto added = size + st->padding.left() + st->padding.right();\n\tconst auto subscribed = std::make_shared<bool>(false);\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tconst auto use = std::min(nameWidth + added, raw->width());\n\t\tconst auto x = (raw->width() - use) / 2;\n\t\tif (const auto available = use - added; available > 0) {\n\t\t\tauto p = QPainter(raw);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(st->textBg);\n\t\t\tp.drawRoundedRect(x, 0, use, size, size / 2., size / 2.);\n\n\t\t\tif (!*subscribed) {\n\t\t\t\t*subscribed = true;\n\t\t\t\tuserpic->subscribeToUpdates([=] { raw->update(); });\n\t\t\t}\n\t\t\tp.drawImage(QRect(x, 0, size, size), userpic->image(size));\n\n\t\t\tconst auto textx = x + size + st->padding.left();\n\t\t\tconst auto texty = st->padding.top() + st->style.font->ascent;\n\t\t\tconst auto text = (use == nameWidth + added)\n\t\t\t\t? name\n\t\t\t\t: st->style.font->elided(name, available);\n\t\t\tp.setPen(st->textFg);\n\t\t\tp.setFont(st->style.font);\n\t\t\tp.drawText(textx, texty, text);\n\t\t}\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\n} // namespace\n\nCollectibleType DetectCollectibleType(const QString &entity) {\n\treturn entity.startsWith('+')\n\t\t? CollectibleType::Phone\n\t\t: CollectibleType::Username;\n}\n\nvoid CollectibleInfoBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tCollectibleInfo info) {\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setStyle(st::collectibleBox);\n\n\tconst auto type = DetectCollectibleType(info.entity);\n\n\tconst auto icon = box->addRow(\n\t\tobject_ptr<Ui::FixedHeightWidget>(box, st::collectibleIconDiameter),\n\t\tst::collectibleIconPadding);\n\ticon->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tconst auto size = icon->height();\n\t\tconst auto inner = QRect(\n\t\t\t(icon->width() - size) / 2,\n\t\t\t0,\n\t\t\tsize,\n\t\t\tsize);\n\t\tif (!inner.intersects(clip)) {\n\t\t\treturn;\n\t\t}\n\t\tauto p = QPainter(icon);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setBrush(st::defaultActiveButton.textBg);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawEllipse(inner);\n\t}, icon->lifetime());\n\tconst auto lottieSize = st::collectibleIcon;\n\tauto lottie = Settings::CreateLottieIcon(\n\t\ticon,\n\t\t{\n\t\t\t.name = (type == CollectibleType::Phone\n\t\t\t\t? u\"collectible_phone\"_q\n\t\t\t\t: u\"collectible_username\"_q),\n\t\t\t.color = &st::defaultActiveButton.textFg,\n\t\t\t.sizeOverride = { lottieSize, lottieSize },\n\t\t},\n\t\tQMargins());\n\tbox->showFinishes(\n\t) | rpl::on_next([animate = std::move(lottie.animate)] {\n\t\tanimate(anim::repeat::once);\n\t}, box->lifetime());\n\tconst auto animation = lottie.widget.release();\n\ticon->sizeValue() | rpl::on_next([=](QSize size) {\n\t\tconst auto skip = (type == CollectibleType::Phone)\n\t\t\t? style::ConvertScale(2)\n\t\t\t: 0;\n\t\tanimation->move(\n\t\t\t(size.width() - animation->width()) / 2,\n\t\t\tskip + (size.height() - animation->height()) / 2);\n\t}, animation->lifetime());\n\n\tconst auto formatted = FormatEntity(type, info.entity);\n\tconst auto header = (type == CollectibleType::Phone)\n\t\t? tr::lng_collectible_phone_title(\n\t\t\ttr::now,\n\t\t\tlt_phone,\n\t\t\ttr::link(formatted),\n\t\t\ttr::marked)\n\t\t: tr::lng_collectible_username_title(\n\t\t\ttr::now,\n\t\t\tlt_username,\n\t\t\ttr::link(formatted),\n\t\t\ttr::marked);\n\tconst auto copyCallback = [box, type, formatted, text = info.copyText](\n\t\t\tbool copyLink) {\n\t\tQGuiApplication::clipboard()->setText((text.isEmpty() || !copyLink)\n\t\t\t? formatted\n\t\t\t: text);\n\t\tbox->uiShow()->showToast((type == CollectibleType::Phone)\n\t\t\t? tr::lng_collectible_phone_copied(tr::now)\n\t\t\t: copyLink\n\t\t\t? tr::lng_username_copied(tr::now)\n\t\t\t: tr::lng_username_text_copied(tr::now));\n\t};\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\trpl::single(header),\n\t\t\tst::collectibleHeader),\n\t\tst::collectibleHeaderPadding,\n\t\tstyle::al_top\n\t)->setClickHandlerFilter([copyCallback](const auto &...) {\n\t\tcopyCallback(false);\n\t\treturn false;\n\t});\n\n\tbox->addRow(MakeOwnerCell(box, info), st::collectibleOwnerPadding);\n\n\tconst auto text = ((type == CollectibleType::Phone)\n\t\t? tr::lng_collectible_phone_info\n\t\t: tr::lng_collectible_username_info)(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\tTextWithEntities{ FormatDate(info.date) },\n\t\t\tlt_price,\n\t\t\tFormatPrice(info),\n\t\t\ttr::rich);\n\tconst auto label = box->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(box, st::collectibleInfo),\n\t\tst::collectibleInfoPadding,\n\t\tstyle::al_top);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tlabel->setMarkedText(text);\n\n\tconst auto more = box->addRow(\n\t\tobject_ptr<Ui::RoundButton>(\n\t\t\tbox,\n\t\t\ttr::lng_collectible_learn_more(),\n\t\t\tst::collectibleMore),\n\t\tst::collectibleMorePadding);\n\tmore->setClickedCallback([url = info.url] {\n\t\tFile::OpenUrl(url);\n\t});\n\n\tconst auto phrase = (type == CollectibleType::Phone)\n\t\t? tr::lng_collectible_phone_copy\n\t\t: tr::lng_collectible_username_copy;\n\tauto owned = object_ptr<Ui::RoundButton>(\n\t\tbox,\n\t\tphrase(),\n\t\tst::collectibleCopy);\n\tconst auto copy = owned.data();\n\tcopy->setClickedCallback([copyCallback] {\n\t\tcopyCallback(true);\n\t});\n\tbox->addButton(std::move(owned));\n\n\tbox->setNoContentMargin(true);\n\tconst auto buttonsParent = box->verticalLayout().get();\n\tconst auto close = Ui::CreateChild<Ui::IconButton>(\n\t\tbuttonsParent,\n\t\tst::boxTitleClose);\n\tclose->setClickedCallback([=] {\n\t\tbox->closeBox();\n\t});\n\tbox->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tclose->moveToRight(0, 0);\n\t}, box->lifetime());\n\n\tbox->widthValue() | rpl::on_next([=](int width) {\n\t\tmore->setFullWidth(width\n\t\t\t- st::collectibleMorePadding.left()\n\t\t\t- st::collectibleMorePadding.right());\n\t\tcopy->setFullWidth(width\n\t\t\t- st::collectibleBox.buttonPadding.left()\n\t\t\t- st::collectibleBox.buttonPadding.right());\n\t}, box->lifetime());\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/collectible_info_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nclass GenericBox;\nclass DynamicImage;\n\nenum class CollectibleType {\n\tPhone,\n\tUsername,\n};\n\n[[nodiscard]] CollectibleType DetectCollectibleType(const QString &entity);\n\nstruct CollectibleInfo {\n\tQString entity;\n\tQString copyText;\n\tstd::shared_ptr<DynamicImage> ownerUserpic;\n\tQString ownerName;\n\tuint64 cryptoAmount = 0;\n\tuint64 amount = 0;\n\tQString cryptoCurrency;\n\tQString currency;\n\tQString url;\n\tTimeId date = 0;\n};\n\nvoid CollectibleInfoBox(not_null<Ui::GenericBox*> box, CollectibleInfo info);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/confirm_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/confirm_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/rect.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Ui {\n\nvoid ConfirmBox(not_null<Ui::GenericBox*> box, ConfirmBoxArgs &&args) {\n\tconst auto weak = base::make_weak(box);\n\tconst auto lifetime = box->lifetime().make_state<rpl::lifetime>();\n\n\tconst auto withTitle = !v::is_null(args.title);\n\tif (withTitle) {\n\t\tbox->setTitle(v::text::take_marked(std::move(args.title)));\n\t}\n\n\tif (!v::is_null(args.text)) {\n\t\tconst auto padding = st::boxPadding;\n\t\tconst auto use = args.labelPadding\n\t\t\t? *args.labelPadding\n\t\t\t: withTitle\n\t\t\t? QMargins(padding.left(), 0, padding.right(), padding.bottom())\n\t\t\t: padding;\n\t\tconst auto label = box->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox.get(),\n\t\t\t\tv::text::take_marked(std::move(args.text)),\n\t\t\t\targs.labelStyle ? *args.labelStyle : st::boxLabel),\n\t\t\tuse);\n\t\tif (args.labelFilter) {\n\t\t\tlabel->setClickHandlerFilter(std::move(args.labelFilter));\n\t\t}\n\t}\n\n\tconst auto prepareCallback = [&](ConfirmBoxArgs::Callback &callback) {\n\t\treturn [=, confirmed = std::move(callback)]() {\n\t\t\tif (const auto callbackPtr = std::get_if<1>(&confirmed)) {\n\t\t\t\tif (auto callback = (*callbackPtr)) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t} else if (const auto callbackPtr = std::get_if<2>(&confirmed)) {\n\t\t\t\tif (auto callback = (*callbackPtr)) {\n\t\t\t\t\tcallback(crl::guard(weak, [=] { weak->closeBox(); }));\n\t\t\t\t}\n\t\t\t} else if (weak) {\n\t\t\t\tweak->closeBox();\n\t\t\t}\n\t\t};\n\t};\n\n\tconst auto &defaultButtonStyle = box->getDelegate()->style().button;\n\tconst auto confirmTextPlain = v::is_null(args.confirmText)\n\t\t|| v::is<rpl::producer<QString>>(args.confirmText)\n\t\t|| v::is<QString>(args.confirmText);\n\tconst auto confirmButton = box->addButton(\n\t\t(confirmTextPlain\n\t\t\t? v::text::take_plain(\n\t\t\t\tstd::move(args.confirmText),\n\t\t\t\ttr::lng_box_ok())\n\t\t\t: rpl::single(QString())),\n\t\t[=, c = prepareCallback(args.confirmed)]() {\n\t\t\tlifetime->destroy();\n\t\t\tc();\n\t\t},\n\t\targs.confirmStyle ? *args.confirmStyle : defaultButtonStyle);\n\tif (!confirmTextPlain) {\n\t\tconfirmButton->setText(\n\t\t\tv::text::take_marked(std::move(args.confirmText)));\n\t}\n\n\tbox->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif ((e->type() != QEvent::KeyPress) || !confirmButton) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto k = static_cast<QKeyEvent*>(e.get());\n\t\tif (k->key() == Qt::Key_Enter || k->key() == Qt::Key_Return) {\n\t\t\tconfirmButton->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);\n\t\t}\n\t}, box->lifetime());\n\n\tif (!args.inform) {\n\t\tconst auto cancelButton = box->addButton(\n\t\t\tv::text::take_plain(std::move(args.cancelText), tr::lng_cancel()),\n\t\t\tcrl::guard(weak, [=, c = prepareCallback(args.cancelled)]() {\n\t\t\t\tlifetime->destroy();\n\t\t\t\tc();\n\t\t\t}),\n\t\t\targs.cancelStyle ? *args.cancelStyle : defaultButtonStyle);\n\n\t\tbox->boxClosing(\n\t\t) | rpl::on_next(crl::guard(cancelButton, [=] {\n\t\t\tcancelButton->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);\n\t\t}), *lifetime);\n\t}\n\n\tif (args.strictCancel) {\n\t\tlifetime->destroy();\n\t}\n}\n\nobject_ptr<Ui::GenericBox> MakeConfirmBox(ConfirmBoxArgs &&args) {\n\treturn Box(ConfirmBox, std::move(args));\n}\n\nvoid IconWithTitle(\n\t\tnot_null<VerticalLayout*> container,\n\t\tnot_null<RpWidget*> icon,\n\t\tnot_null<RpWidget*> title,\n\t\tRpWidget *subtitle) {\n\tconst auto line = container->add(\n\t\tobject_ptr<RpWidget>(container),\n\t\tst::boxRowPadding);\n\ticon->setParent(line);\n\ttitle->setParent(line);\n\tif (subtitle) {\n\t\tsubtitle->setParent(line);\n\t}\n\n\ticon->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tline->resize(line->width(), height);\n\t}, icon->lifetime());\n\n\tline->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\ticon->moveToLeft(0, 0);\n\t\tconst auto skip = st::defaultBoxCheckbox.textPosition.x();\n\t\ttitle->resizeToWidth(width - rect::right(icon) - skip);\n\t\tif (subtitle) {\n\t\t\tsubtitle->resizeToWidth(title->width());\n\t\t\ttitle->moveToLeft(rect::right(icon) + skip, icon->y());\n\t\t\tsubtitle->moveToLeft(\n\t\t\t\ttitle->x(),\n\t\t\t\ticon->y() + icon->height() - subtitle->height());\n\t\t} else {\n\t\t\ttitle->moveToLeft(\n\t\t\t\trect::right(icon) + skip,\n\t\t\t\t((icon->height() - title->height()) / 2));\n\t\t}\n\t}, title->lifetime());\n\n\ticon->setAttribute(Qt::WA_TransparentForMouseEvents);\n\ttitle->setAttribute(Qt::WA_TransparentForMouseEvents);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/confirm_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_variant.h\"\n\nnamespace Ui {\n\nstruct ConfirmBoxArgs {\n\tusing Callback = std::variant<\n\t\tv::null_t,\n\t\tFn<void()>,\n\t\tFn<void(Fn<void()>)>>;\n\n\tv::text::data text = v::null;\n\tCallback confirmed = v::null;\n\tCallback cancelled = v::null;\n\n\tv::text::data confirmText;\n\tv::text::data cancelText;\n\n\tconst style::RoundButton *confirmStyle = nullptr;\n\tconst style::RoundButton *cancelStyle = nullptr;\n\n\tconst style::FlatLabel *labelStyle = nullptr;\n\tFn<bool(const ClickHandlerPtr&, Qt::MouseButton)> labelFilter;\n\tstd::optional<QMargins> labelPadding;\n\n\tv::text::data title = v::null;\n\n\tbool inform = false;\n\t// If strict cancel is set the cancel.callback() is only called\n\t// if the cancel button was pressed.\n\tbool strictCancel = false;\n};\n\nvoid ConfirmBox(not_null<GenericBox*> box, ConfirmBoxArgs &&args);\n\ninline void InformBox(not_null<GenericBox*> box, ConfirmBoxArgs &&args) {\n\targs.inform = true;\n\tConfirmBox(box, std::move(args));\n}\n\n[[nodiscard]] object_ptr<GenericBox> MakeConfirmBox(ConfirmBoxArgs &&args);\n\n[[nodiscard]] inline object_ptr<GenericBox> MakeInformBox(\n\t\tConfirmBoxArgs &&args) {\n\targs.inform = true;\n\treturn MakeConfirmBox(std::move(args));\n}\n\n[[nodiscard]] inline object_ptr<GenericBox> MakeInformBox(\n\t\tv::text::data text) {\n\treturn MakeInformBox({ .text = std::move(text) });\n}\n\nvoid IconWithTitle(\n\tnot_null<VerticalLayout*> container,\n\tnot_null<RpWidget*> icon,\n\tnot_null<RpWidget*> title,\n\tRpWidget *subtitle = nullptr);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/confirm_phone_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/confirm_phone_box.h\"\n\n#include \"core/file_utilities.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/text/format_values.h\" // Ui::FormatPhone\n#include \"ui/text/text_utilities.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace Ui {\n\nConfirmPhoneBox::ConfirmPhoneBox(\n\tQWidget*,\n\tconst QString &phone,\n\tint codeLength,\n\tconst QString &openUrl,\n\tstd::optional<int> timeout)\n: _phone(phone)\n, _sentCodeLength(codeLength)\n, _call([=] { sendCall(); }, [=] { update(); }) {\n\tif (!openUrl.isEmpty()) {\n\t\t_fragment.create(\n\t\t\tthis,\n\t\t\ttr::lng_intro_fragment_button(),\n\t\t\tst::fragmentBoxButton);\n\t\t_fragment->setClickedCallback([=] { File::OpenUrl(openUrl); });\n\t}\n\tif (timeout) {\n\t\t_call.setStatus({ Ui::SentCodeCall::State::Waiting, *timeout });\n\t}\n}\n\nvoid ConfirmPhoneBox::sendCall() {\n\t_resendRequests.fire({});\n}\n\nvoid ConfirmPhoneBox::prepare() {\n\t_about.create(\n\t\tthis,\n\t\ttr::lng_confirm_phone_about(\n\t\t\tlt_phone,\n\t\t\trpl::single(tr::bold(Ui::FormatPhone(_phone))),\n\t\t\ttr::marked),\n\t\tst::confirmPhoneAboutLabel);\n\n\t_code.create(this, st::confirmPhoneCodeField, tr::lng_code_ph());\n\t_code->setAutoSubmit(_sentCodeLength, [=] { sendCode(); });\n\t_code->setChangedCallback([=] { showError(QString()); });\n\n\tsetTitle(tr::lng_confirm_phone_title());\n\n\taddButton(tr::lng_confirm_phone_send(), [=] { sendCode(); });\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\n\tsetDimensions(\n\t\tst::boxWidth,\n\t\tst::usernamePadding.top()\n\t\t\t+ _code->height()\n\t\t\t+ st::usernameSkip\n\t\t\t+ _about->height()\n\t\t\t+ st::usernameSkip\n\t\t\t+ (_fragment ? (_fragment->height() + fragmentSkip()) : 0));\n\n\t_code->submits(\n\t) | rpl::on_next([=] { sendCode(); }, _code->lifetime());\n\n\tshowChildren();\n}\n\nvoid ConfirmPhoneBox::sendCode() {\n\tif (_isWaitingCheck) {\n\t\treturn;\n\t}\n\tconst auto code = _code->getDigitsOnly();\n\tif (code.isEmpty()) {\n\t\t_code->showError();\n\t\treturn;\n\t}\n\n\t_code->setDisabled(true);\n\tsetFocus();\n\n\tshowError(QString());\n\n\t_checkRequests.fire_copy(code);\n\t_isWaitingCheck = true;\n}\n\nvoid ConfirmPhoneBox::showError(const QString &error) {\n\t_error = error;\n\tif (!_error.isEmpty()) {\n\t\t_code->showError();\n\t}\n\tupdate();\n}\n\nvoid ConfirmPhoneBox::paintEvent(QPaintEvent *e) {\n\tBoxContent::paintEvent(e);\n\n\tauto p = QPainter(this);\n\n\tp.setFont(st::boxTextFont);\n\tconst auto callText = _call.getText();\n\tif (!callText.isEmpty()) {\n\t\tp.setPen(st::usernameDefaultFg);\n\t\tconst auto callTextRect = QRect(\n\t\t\tst::usernamePadding.left(),\n\t\t\t_about->y() + _about->height(),\n\t\t\twidth() - 2 * st::usernamePadding.left(),\n\t\t\tst::usernameSkip);\n\t\tp.drawText(callTextRect, callText, style::al_left);\n\t}\n\tauto errorText = _error;\n\tif (errorText.isEmpty()) {\n\t\tp.setPen(st::usernameDefaultFg);\n\t\terrorText = tr::lng_confirm_phone_enter_code(tr::now);\n\t} else {\n\t\tp.setPen(st::boxTextFgError);\n\t}\n\tconst auto errorTextRect = QRect(\n\t\tst::usernamePadding.left(),\n\t\t_code->y() + _code->height(),\n\t\twidth() - 2 * st::usernamePadding.left(),\n\t\tst::usernameSkip);\n\tp.drawText(errorTextRect, errorText, style::al_left);\n}\n\nvoid ConfirmPhoneBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\t_code->resize(\n\t\twidth() - st::usernamePadding.left() - st::usernamePadding.right(),\n\t\t_code->height());\n\t_code->moveToLeft(st::usernamePadding.left(), st::usernamePadding.top());\n\n\tif (_fragment) {\n\t\t_fragment->setFullWidth(_code->width());\n\t\t_fragment->moveToLeft(\n\t\t\t(width() - _fragment->width()) / 2,\n\t\t\t_code->y() + _code->height() + st::usernameSkip);\n\t}\n\n\tconst auto aboutTop = _fragment\n\t\t? (_fragment->y() + _fragment->height() + fragmentSkip())\n\t\t: (_code->y() + _code->height() + st::usernameSkip);\n\t_about->moveToLeft(st::usernamePadding.left(), aboutTop);\n}\n\nvoid ConfirmPhoneBox::setInnerFocus() {\n\t_code->setFocusFast();\n}\n\nint ConfirmPhoneBox::fragmentSkip() const {\n\treturn st::usernamePadding.bottom();\n}\n\nrpl::producer<QString> ConfirmPhoneBox::checkRequests() const {\n\treturn _checkRequests.events();\n}\n\nrpl::producer<> ConfirmPhoneBox::resendRequests() const {\n\treturn _resendRequests.events();\n}\n\nvoid ConfirmPhoneBox::callDone() {\n\t_call.callDone();\n}\n\nvoid ConfirmPhoneBox::showServerError(const QString &text) {\n\t_isWaitingCheck = false;\n\t_code->setDisabled(false);\n\t_code->setFocus();\n\tshowError(text);\n}\n\nQString ConfirmPhoneBox::getPhone() const {\n\treturn _phone;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/confirm_phone_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n#include \"ui/widgets/sent_code_field.h\"\n\nnamespace Ui {\n\nclass FlatLabel;\nclass RoundButton;\n\nclass ConfirmPhoneBox final : public Ui::BoxContent {\npublic:\n\tConfirmPhoneBox(\n\t\tQWidget*,\n\t\tconst QString &phone,\n\t\tint codeLength,\n\t\tconst QString &openUrl,\n\t\tstd::optional<int> timeout);\n\n\t[[nodiscard]] rpl::producer<QString> checkRequests() const;\n\t[[nodiscard]] rpl::producer<> resendRequests() const;\n\n\tvoid callDone();\n\tvoid showServerError(const QString &text);\n\nprotected:\n\tvoid prepare() override;\n\tvoid setInnerFocus() override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid sendCode();\n\tvoid sendCall();\n\tvoid checkPhoneAndHash();\n\n\t[[nodiscard]] int fragmentSkip() const;\n\n\tQString getPhone() const;\n\tvoid showError(const QString &error);\n\n\t// _hash from the link for account.sendConfirmPhoneCode call.\n\t// _phoneHash from auth.sentCode for account.confirmPhone call.\n\tconst QString _phone;\n\n\t// If we receive the code length, we autosubmit _code field when enough symbols is typed.\n\tconst int _sentCodeLength = 0;\n\n\tbool _isWaitingCheck = false;\n\n\tobject_ptr<Ui::FlatLabel> _about = { nullptr };\n\tobject_ptr<Ui::SentCodeField> _code = { nullptr };\n\tobject_ptr<Ui::RoundButton> _fragment = { nullptr };\n\n\tQString _error;\n\tUi::SentCodeCall _call;\n\n\trpl::event_stream<QString> _checkRequests;\n\trpl::event_stream<> _resendRequests;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/country_select_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/country_select_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/multi_select.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"countries/countries_instance.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_intro.h\"\n\n#include <QtCore/QRegularExpression>\n\nnamespace Ui {\nnamespace {\n\nQString LastValidISO;\n\n} // namespace\n\nclass CountrySelectBox::Inner : public RpWidget {\npublic:\n\tInner(QWidget *parent, const QString &iso, Type type);\n\t~Inner();\n\n\tvoid updateFilter(QString filter = QString());\n\n\tvoid selectSkip(int32 dir);\n\tvoid selectSkipPage(int32 h, int32 dir);\n\n\tvoid chooseCountry();\n\n\tvoid refresh();\n\n\t[[nodiscard]] rpl::producer<Entry> countryChosen() const {\n\t\treturn _countryChosen.events();\n\t}\n\n\t[[nodiscard]] rpl::producer<ScrollToRequest> mustScrollTo() const {\n\t\treturn _mustScrollTo.events();\n\t}\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\nprivate:\n\tvoid init();\n\tvoid updateSelected() {\n\t\tupdateSelected(mapFromGlobal(QCursor::pos()));\n\t}\n\tvoid updateSelected(QPoint localPos);\n\tvoid updateSelectedRow();\n\tvoid updateRow(int index);\n\tvoid setPressed(int pressed);\n\tconst std::vector<Entry> &current() const;\n\n\tType _type = Type::Phones;\n\tint _rowHeight = 0;\n\n\tint _selected = -1;\n\tint _pressed = -1;\n\tQString _filter;\n\tbool _mouseSelection = false;\n\n\tstd::vector<std::unique_ptr<RippleAnimation>> _ripples;\n\n\tstd::vector<Entry> _list;\n\tstd::vector<Entry> _filtered;\n\tbase::flat_map<QChar, std::vector<int>> _byLetter;\n\tstd::vector<std::vector<QString>> _namesList;\n\n\trpl::event_stream<Entry> _countryChosen;\n\trpl::event_stream<ScrollToRequest> _mustScrollTo;\n\n};\n\nCountrySelectBox::CountrySelectBox(QWidget*)\n: CountrySelectBox(nullptr, QString(), Type::Phones) {\n}\n\nCountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type)\n: _select(this, st::defaultMultiSelect, tr::lng_country_ph())\n, _ownedInner(this, iso, type) {\n}\n\nrpl::producer<QString> CountrySelectBox::countryChosen() const {\n\tExpects(_ownedInner != nullptr || _inner != nullptr);\n\n\treturn (_ownedInner\n\t\t? _ownedInner.data()\n\t\t: _inner.data())->countryChosen() | rpl::map([](const Entry &e) {\n\t\t\treturn e.iso2;\n\t\t});\n}\n\nrpl::producer<CountrySelectBox::Entry> CountrySelectBox::entryChosen() const {\n\tExpects(_ownedInner != nullptr || _inner != nullptr);\n\n\treturn (_ownedInner\n\t\t? _ownedInner.data()\n\t\t: _inner.data())->countryChosen();\n}\n\nvoid CountrySelectBox::prepare() {\n\tsetTitle(tr::lng_country_select());\n\n\t_select->resizeToWidth(st::boxWidth);\n\t_select->setQueryChangedCallback([=](const QString &query) {\n\t\tapplyFilterUpdate(query);\n\t});\n\t_select->setSubmittedCallback([=](Qt::KeyboardModifiers) {\n\t\tsubmit();\n\t});\n\n\t_inner = setInnerWidget(\n\t\tstd::move(_ownedInner),\n\t\tst::countriesScroll,\n\t\t_select->height());\n\n\taddButton(tr::lng_close(), [=] { closeBox(); });\n\n\tsetDimensions(st::boxWidth, st::boxMaxListHeight);\n\n\t_inner->mustScrollTo(\n\t) | rpl::on_next([=](ScrollToRequest request) {\n\t\tscrollToY(request.ymin, request.ymax);\n\t}, lifetime());\n}\n\nvoid CountrySelectBox::submit() {\n\t_inner->chooseCountry();\n}\n\nvoid CountrySelectBox::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Down) {\n\t\t_inner->selectSkip(1);\n\t} else if (e->key() == Qt::Key_Up) {\n\t\t_inner->selectSkip(-1);\n\t} else if (e->key() == Qt::Key_PageDown) {\n\t\t_inner->selectSkipPage(height() - _select->height(), 1);\n\t} else if (e->key() == Qt::Key_PageUp) {\n\t\t_inner->selectSkipPage(height() - _select->height(), -1);\n\t} else {\n\t\tBoxContent::keyPressEvent(e);\n\t}\n}\n\nvoid CountrySelectBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\t_select->resizeToWidth(width());\n\t_select->moveToLeft(0, 0);\n\n\t_inner->resizeToWidth(width());\n}\n\nvoid CountrySelectBox::applyFilterUpdate(const QString &query) {\n\tscrollToY(0);\n\t_inner->updateFilter(query);\n}\n\nvoid CountrySelectBox::setInnerFocus() {\n\t_select->setInnerFocus();\n}\n\nCountrySelectBox::Inner::Inner(\n\tQWidget *parent,\n\tconst QString &iso,\n\tType type)\n: RpWidget(parent)\n, _type(type)\n, _rowHeight(st::countryRowHeight) {\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\n\tconst auto &byISO2 = Countries::Instance().byISO2();\n\n\tif (byISO2.contains(iso)) {\n\t\tLastValidISO = iso;\n\t}\n\n\trpl::single(\n\t) | rpl::then(\n\t\tCountries::Instance().updated()\n\t) | rpl::on_next([=] {\n\t\t_mustScrollTo.fire(ScrollToRequest(0, 0));\n\t\t_list.clear();\n\t\t_namesList.clear();\n\t\tinit();\n\t\tconst auto filter = _filter;\n\t\t_filter = u\"a\"_q;\n\t\tupdateFilter(filter);\n\t}, lifetime());\n}\n\nvoid CountrySelectBox::Inner::init() {\n\tconst auto &byISO2 = Countries::Instance().byISO2();\n\n\tconst auto extractEntries = [&](const Countries::Info &info) {\n\t\tfor (const auto &code : info.codes) {\n\t\t\t_list.push_back(Entry{\n\t\t\t\t.country = info.name,\n\t\t\t\t.iso2 = info.iso2,\n\t\t\t\t.code = code.callingCode,\n\t\t\t\t.alternativeName = info.alternativeName,\n\t\t\t});\n\t\t}\n\t};\n\n\t_list.reserve(byISO2.size());\n\t_namesList.reserve(byISO2.size());\n\n\tconst auto l = byISO2.constFind(LastValidISO);\n\tconst auto lastValid = (l != byISO2.cend()) ? (*l) : nullptr;\n\tif (lastValid) {\n\t\textractEntries(*lastValid);\n\t}\n\tfor (const auto &entry : Countries::Instance().list()) {\n\t\tif (&entry != lastValid) {\n\t\t\textractEntries(entry);\n\t\t}\n\t}\n\tauto index = 0;\n\tfor (const auto &info : _list) {\n\t\tstatic const auto RegExp = QRegularExpression(\"[\\\\s\\\\-]\");\n\t\tauto full = info.country\n\t\t\t+ ' '\n\t\t\t+ (!info.alternativeName.isEmpty()\n\t\t\t\t? info.alternativeName\n\t\t\t\t: QString());\n\t\tconst auto namesList = std::move(full).toLower().split(\n\t\t\tRegExp,\n\t\t\tQt::SkipEmptyParts);\n\t\tauto &names = _namesList.emplace_back();\n\t\tnames.reserve(namesList.size());\n\t\tfor (const auto &name : namesList) {\n\t\t\tconst auto part = name.trimmed();\n\t\t\tif (part.isEmpty()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst auto ch = part[0];\n\t\t\tauto &byLetter = _byLetter[ch];\n\t\t\tif (byLetter.empty() || byLetter.back() != index) {\n\t\t\t\tbyLetter.push_back(index);\n\t\t\t}\n\t\t\tnames.push_back(part);\n\t\t}\n\t\t++index;\n\t}\n}\n\nvoid CountrySelectBox::Inner::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\tQRect r(e->rect());\n\tp.setClipRect(r);\n\n\tconst auto &list = current();\n\tif (list.empty()) {\n\t\tp.fillRect(r, st::boxBg);\n\t\tp.setFont(st::noContactsFont);\n\t\tp.setPen(st::noContactsColor);\n\t\tp.drawText(QRect(0, 0, width(), st::noContactsHeight), tr::lng_country_none(tr::now), style::al_center);\n\t\treturn;\n\t}\n\tconst auto l = int(list.size());\n\tif (r.intersects(QRect(0, 0, width(), st::countriesSkip))) {\n\t\tp.fillRect(r.intersected(QRect(0, 0, width(), st::countriesSkip)), st::countryRowBg);\n\t}\n\tint32 from = std::clamp((r.y() - st::countriesSkip) / _rowHeight, 0, l);\n\tint32 to = std::clamp((r.y() + r.height() - st::countriesSkip + _rowHeight - 1) / _rowHeight, 0, l);\n\tfor (int32 i = from; i < to; ++i) {\n\t\tauto selected = (i == (_pressed >= 0 ? _pressed : _selected));\n\t\tauto y = st::countriesSkip + i * _rowHeight;\n\n\t\tp.fillRect(0, y, width(), _rowHeight, selected ? st::countryRowBgOver : st::countryRowBg);\n\t\tif (_ripples.size() > i && _ripples[i]) {\n\t\t\t_ripples[i]->paint(p, 0, y, width());\n\t\t\tif (_ripples[i]->empty()) {\n\t\t\t\t_ripples[i].reset();\n\t\t\t}\n\t\t}\n\n\t\tauto code = QString(\"+\") + list[i].code;\n\t\tauto codeWidth = st::countryRowCodeFont->width(code);\n\n\t\tauto name = list[i].country;\n\t\tauto nameWidth = st::countryRowNameFont->width(name);\n\t\tauto availWidth = width() - st::countryRowPadding.left() - st::countryRowPadding.right() - codeWidth - st::boxScroll.width;\n\t\tif (nameWidth > availWidth) {\n\t\t\tname = st::countryRowNameFont->elided(name, availWidth);\n\t\t\tnameWidth = st::countryRowNameFont->width(name);\n\t\t}\n\n\t\tp.setFont(st::countryRowNameFont);\n\t\tp.setPen(st::countryRowNameFg);\n\t\tp.drawTextLeft(st::countryRowPadding.left(), y + st::countryRowPadding.top(), width(), name);\n\n\t\tif (_type == Type::Phones) {\n\t\t\tp.setFont(st::countryRowCodeFont);\n\t\t\tp.setPen(selected ? st::countryRowCodeFgOver : st::countryRowCodeFg);\n\t\t\tp.drawTextLeft(st::countryRowPadding.left() + nameWidth + st::countryRowPadding.right(), y + st::countryRowPadding.top(), width(), code);\n\t\t}\n\t}\n}\n\nvoid CountrySelectBox::Inner::enterEventHook(QEnterEvent *e) {\n\tsetMouseTracking(true);\n}\n\nvoid CountrySelectBox::Inner::leaveEventHook(QEvent *e) {\n\t_mouseSelection = false;\n\tsetMouseTracking(false);\n\tif (_selected >= 0) {\n\t\tupdateSelectedRow();\n\t\t_selected = -1;\n\t}\n}\n\nvoid CountrySelectBox::Inner::mouseMoveEvent(QMouseEvent *e) {\n\t_mouseSelection = true;\n\tupdateSelected(e->pos());\n}\n\nvoid CountrySelectBox::Inner::mousePressEvent(QMouseEvent *e) {\n\t_mouseSelection = true;\n\tupdateSelected(e->pos());\n\n\tsetPressed(_selected);\n\tconst auto &list = current();\n\tif (_pressed >= 0 && _pressed < list.size()) {\n\t\tif (_ripples.size() <= _pressed) {\n\t\t\t_ripples.reserve(_pressed + 1);\n\t\t\twhile (_ripples.size() <= _pressed) {\n\t\t\t\t_ripples.push_back(nullptr);\n\t\t\t}\n\t\t}\n\t\tif (!_ripples[_pressed]) {\n\t\t\tauto mask = RippleAnimation::RectMask(QSize(width(), _rowHeight));\n\t\t\t_ripples[_pressed] = std::make_unique<RippleAnimation>(st::countryRipple, std::move(mask), [this, index = _pressed] {\n\t\t\t\tupdateRow(index);\n\t\t\t});\n\t\t\t_ripples[_pressed]->add(e->pos() - QPoint(0, st::countriesSkip + _pressed * _rowHeight));\n\t\t}\n\t}\n}\n\nvoid CountrySelectBox::Inner::mouseReleaseEvent(QMouseEvent *e) {\n\tauto pressed = _pressed;\n\tsetPressed(-1);\n\tupdateSelectedRow();\n\tif (e->button() == Qt::LeftButton) {\n\t\tif ((pressed >= 0) && pressed == _selected) {\n\t\t\tchooseCountry();\n\t\t}\n\t}\n}\n\nvoid CountrySelectBox::Inner::updateFilter(QString filter) {\n\tconst auto words = TextUtilities::PrepareSearchWords(filter);\n\tfilter = words.isEmpty() ? QString() : words.join(' ');\n\tif (_filter == filter) {\n\t\treturn;\n\t}\n\t_filter = filter;\n\n\tconst auto findWord = [&](\n\t\t\tconst std::vector<QString> &names,\n\t\t\tconst QString &word) {\n\t\tfor (const auto &name : names) {\n\t\t\tif (name.startsWith(word)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\tconst auto hasAllWords = [&](const std::vector<QString> &names) {\n\t\tfor (const auto &word : words) {\n\t\t\tif (!findWord(names, word)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t};\n\tif (!_filter.isEmpty()) {\n\t\t_filtered.clear();\n\t\tfor (const auto index : _byLetter[_filter[0].toLower()]) {\n\t\t\tif (hasAllWords(_namesList[index])) {\n\t\t\t\t_filtered.push_back(_list[index]);\n\t\t\t}\n\t\t}\n\t}\n\trefresh();\n\t_selected = current().empty() ? -1 : 0;\n\tupdate();\n}\n\nvoid CountrySelectBox::Inner::selectSkip(int32 dir) {\n\t_mouseSelection = false;\n\n\tconst auto &list = current();\n\tint cur = (_selected >= 0) ? _selected : -1;\n\tcur += dir;\n\tif (cur <= 0) {\n\t\t_selected = list.empty() ? -1 : 0;\n\t} else if (cur >= list.size()) {\n\t\t_selected = -1;\n\t} else {\n\t\t_selected = cur;\n\t}\n\tif (_selected >= 0) {\n\t\t_mustScrollTo.fire(ScrollToRequest(\n\t\t\tst::countriesSkip + _selected * _rowHeight,\n\t\t\tst::countriesSkip + (_selected + 1) * _rowHeight));\n\t}\n\tupdate();\n}\n\nvoid CountrySelectBox::Inner::selectSkipPage(int32 h, int32 dir) {\n\tint32 points = h / _rowHeight;\n\tif (!points) return;\n\tselectSkip(points * dir);\n}\n\nvoid CountrySelectBox::Inner::chooseCountry() {\n\tconst auto &list = current();\n\t_countryChosen.fire_copy((_selected >= 0 && _selected < list.size())\n\t\t? list[_selected]\n\t\t: Entry());\n}\n\nvoid CountrySelectBox::Inner::refresh() {\n\tconst auto &list = current();\n\tresize(width(), list.empty() ? st::noContactsHeight : (list.size() * _rowHeight + st::countriesSkip));\n}\n\nvoid CountrySelectBox::Inner::updateSelected(QPoint localPos) {\n\tif (!_mouseSelection) return;\n\n\tauto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(QCursor::pos()));\n\n\tconst auto &list = current();\n\tauto selected = (in && localPos.y() >= st::countriesSkip && localPos.y() < st::countriesSkip + list.size() * _rowHeight) ? ((localPos.y() - st::countriesSkip) / _rowHeight) : -1;\n\tif (_selected != selected) {\n\t\tupdateSelectedRow();\n\t\t_selected = selected;\n\t\tupdateSelectedRow();\n\t}\n}\n\nauto CountrySelectBox::Inner::current() const\n-> const std::vector<CountrySelectBox::Entry> & {\n\treturn _filter.isEmpty() ? _list : _filtered;\n}\n\nvoid CountrySelectBox::Inner::updateSelectedRow() {\n\tupdateRow(_selected);\n}\n\nvoid CountrySelectBox::Inner::updateRow(int index) {\n\tif (index >= 0) {\n\t\tupdate(0, st::countriesSkip + index * _rowHeight, width(), _rowHeight);\n\t}\n}\n\nvoid CountrySelectBox::Inner::setPressed(int pressed) {\n\tif (_pressed >= 0 && _pressed < _ripples.size() && _ripples[_pressed]) {\n\t\t_ripples[_pressed]->lastStop();\n\t}\n\t_pressed = pressed;\n}\n\nCountrySelectBox::Inner::~Inner() = default;\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/country_select_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n\nnamespace Countries {\nstruct Info;\n} // namespace Countries\n\nnamespace Ui {\n\nclass MultiSelect;\nclass RippleAnimation;\n\nclass CountrySelectBox : public BoxContent {\npublic:\n\tenum class Type {\n\t\tPhones,\n\t\tCountries,\n\t};\n\tstruct Entry {\n\t\tQString country;\n\t\tQString iso2;\n\t\tQString code;\n\t\tQString alternativeName;\n\t};\n\n\tCountrySelectBox(QWidget*);\n\tCountrySelectBox(QWidget*, const QString &iso, Type type);\n\n\t[[nodiscard]] rpl::producer<QString> countryChosen() const;\n\t[[nodiscard]] rpl::producer<Entry> entryChosen() const;\n\nprotected:\n\tvoid prepare() override;\n\tvoid setInnerFocus() override;\n\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid submit();\n\tvoid applyFilterUpdate(const QString &query);\n\n\tobject_ptr<MultiSelect> _select;\n\n\tclass Inner;\n\tobject_ptr<Inner> _ownedInner;\n\tQPointer<Inner> _inner;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/edit_birthday_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/edit_birthday_box.h\"\n\n#include \"base/event_filter.h\"\n#include \"data/data_birthday.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/vertical_drum_picker.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n\n#include <QtCore/QDate>\n\nnamespace Ui {\n\nclass GenericBox;\n\nvoid EditBirthdayBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tData::Birthday current,\n\t\tFn<void(Data::Birthday)> save,\n\t\tEditBirthdayType type) {\n\tif (type != EditBirthdayType::Suggest) {\n\t\tbox->setTitle(tr::lng_settings_birthday_title());\n\t}\n\tbox->setWidth(st::boxWideWidth);\n\tconst auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>(\n\t\tbox,\n\t\tst::settingsWorkingHoursPicker));\n\n\tconst auto font = st::boxTextFont;\n\tconst auto itemHeight = st::settingsWorkingHoursPickerItemHeight;\n\tconst auto picker = [=](\n\t\t\tint count,\n\t\t\tint startIndex,\n\t\t\tFn<void(QPainter &p, QRectF rect, int index)> paint) {\n\t\treturn Ui::CreateChild<Ui::VerticalDrumPicker>(\n\t\t\tcontent,\n\t\t\tUi::VerticalDrumPicker::DefaultPaintCallback(\n\t\t\t\tfont,\n\t\t\t\titemHeight,\n\t\t\t\tpaint),\n\t\t\tcount,\n\t\t\titemHeight,\n\t\t\tstartIndex);\n\t};\n\n\tconst auto nowDate = QDate::currentDate();\n\tconst auto nowYear = nowDate.year();\n\tconst auto nowMonth = nowDate.month();\n\tconst auto nowDay = nowDate.day();\n\tconst auto now = Data::Birthday(nowDay, nowMonth, nowYear);\n\tconst auto max = current.year() ? std::max(now, current) : now;\n\tconst auto maxYear = max.year();\n\tconst auto minYear = Data::Birthday::kYearMin;\n\tconst auto yearsCount = (maxYear - minYear + 2); // Last - not set.\n\tconst auto yearsStartIndex = current.year()\n\t\t? (current.year() - minYear)\n\t\t: (yearsCount - 1);\n\tconst auto yearsPaint = [=](QPainter &p, QRectF rect, int index) {\n\t\tp.drawText(\n\t\t\trect,\n\t\t\t(index < yearsCount - 1\n\t\t\t\t? QString::number(minYear + index)\n\t\t\t\t: QString::fromUtf8(\"\\xe2\\x80\\x94\")),\n\t\t\tstyle::al_center);\n\t};\n\tconst auto years = picker(yearsCount, yearsStartIndex, yearsPaint);\n\n\tstruct State {\n\t\trpl::variable<Ui::VerticalDrumPicker*> months;\n\t\trpl::variable<Ui::VerticalDrumPicker*> days;\n\t};\n\tconst auto state = content->lifetime().make_state<State>();\n\n\t// years->value() is valid only after size is set.\n\trpl::combine(\n\t\tcontent->sizeValue(),\n\t\tstate->months.value(),\n\t\tstate->days.value()\n\t) | rpl::on_next([=](\n\t\t\tQSize s,\n\t\t\tUi::VerticalDrumPicker *months,\n\t\t\tUi::VerticalDrumPicker *days) {\n\t\tconst auto half = s.width() / 2;\n\t\tyears->setGeometry(half * 3 / 2, 0, half / 2, s.height());\n\t\tif (months) {\n\t\t\tmonths->setGeometry(half / 2, 0, half, s.height());\n\t\t}\n\t\tif (days) {\n\t\t\tdays->setGeometry(0, 0, half / 2, s.height());\n\t\t}\n\t}, content->lifetime());\n\n\tUi::SendPendingMoveResizeEvents(years);\n\n\tyears->value() | rpl::on_next([=](int yearsIndex) {\n\t\tconst auto year = (yearsIndex == yearsCount - 1)\n\t\t\t? 0\n\t\t\t: minYear + yearsIndex;\n\t\tconst auto monthsCount = (year == maxYear)\n\t\t\t? max.month()\n\t\t\t: 12;\n\t\tconst auto monthsStartIndex = std::clamp(\n\t\t\t(state->months.current()\n\t\t\t\t? state->months.current()->index()\n\t\t\t\t: current.month()\n\t\t\t\t? (current.month() - 1)\n\t\t\t\t: (now.month() - 1)),\n\t\t\t0,\n\t\t\tmonthsCount - 1);\n\t\tconst auto monthsPaint = [=](QPainter &p, QRectF rect, int index) {\n\t\t\tp.drawText(\n\t\t\t\trect,\n\t\t\t\tLang::Month(index + 1)(tr::now),\n\t\t\t\tstyle::al_center);\n\t\t};\n\t\tconst auto updated = picker(\n\t\t\tmonthsCount,\n\t\t\tmonthsStartIndex,\n\t\t\tmonthsPaint);\n\t\tdelete state->months.current();\n\t\tstate->months = updated;\n\t\tstate->months.current()->show();\n\t}, years->lifetime());\n\n\tUi::SendPendingMoveResizeEvents(state->months.current());\n\n\tstate->months.value() | rpl::map([=](Ui::VerticalDrumPicker *picker) {\n\t\treturn picker ? picker->value() : rpl::single(current.month()\n\t\t\t? (current.month() - 1)\n\t\t\t: (now.month() - 1));\n\t}) | rpl::flatten_latest() | rpl::on_next([=](int monthIndex) {\n\t\tconst auto month = monthIndex + 1;\n\t\tconst auto yearsIndex = years->index();\n\t\tconst auto year = (yearsIndex == yearsCount - 1)\n\t\t\t? 0\n\t\t\t: minYear + yearsIndex;\n\t\tconst auto daysCount = (year == maxYear && month == max.month())\n\t\t\t? max.day()\n\t\t\t: (month == 2)\n\t\t\t? ((!year || (!(year % 4) && ((year % 100) || !(year % 400))))\n\t\t\t\t? 29\n\t\t\t\t: 28)\n\t\t\t: ((month == 4) || (month == 6) || (month == 9) || (month == 11))\n\t\t\t? 30\n\t\t\t: 31;\n\t\tconst auto daysStartIndex = std::clamp(\n\t\t\t(state->days.current()\n\t\t\t\t? state->days.current()->index()\n\t\t\t\t: current.day()\n\t\t\t\t? (current.day() - 1)\n\t\t\t\t: (now.day() - 1)),\n\t\t\t0,\n\t\t\tdaysCount - 1);\n\t\tconst auto daysPaint = [=](QPainter &p, QRectF rect, int index) {\n\t\t\tp.drawText(rect, QString::number(index + 1), style::al_center);\n\t\t};\n\t\tconst auto updated = picker(\n\t\t\tdaysCount,\n\t\t\tdaysStartIndex,\n\t\t\tdaysPaint);\n\t\tdelete state->days.current();\n\t\tstate->days = updated;\n\t\tstate->days.current()->show();\n\t}, years->lifetime());\n\n\tcontent->paintRequest(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tauto p = QPainter(content);\n\n\t\tp.fillRect(r, Qt::transparent);\n\n\t\tconst auto lineRect = QRect(\n\t\t\t0,\n\t\t\tcontent->height() / 2,\n\t\t\tcontent->width(),\n\t\t\tst::defaultInputField.borderActive);\n\t\tp.fillRect(lineRect.translated(0, itemHeight / 2), st::activeLineFg);\n\t\tp.fillRect(lineRect.translated(0, -itemHeight / 2), st::activeLineFg);\n\t}, content->lifetime());\n\n\tbase::install_event_filter(box, [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::KeyPress) {\n\t\t\tyears->handleKeyEvent(static_cast<QKeyEvent*>(e.get()));\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\tauto confirmText = (type == EditBirthdayType::Suggest)\n\t\t? tr::lng_suggest_birthday_box_confirm()\n\t\t: tr::lng_settings_save();\n\tbox->addButton(std::move(confirmText), [=] {\n\t\tconst auto result = Data::Birthday(\n\t\t\tstate->days.current()->index() + 1,\n\t\t\tstate->months.current()->index() + 1,\n\t\t\t((years->index() == yearsCount - 1)\n\t\t\t\t? 0\n\t\t\t\t: minYear + years->index()));\n\t\tbox->closeBox();\n\t\tsave(result);\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n\tif (current && type == EditBirthdayType::Edit) {\n\t\tbox->addLeftButton(tr::lng_settings_birthday_reset(), [=] {\n\t\t\tbox->closeBox();\n\t\t\tsave(Data::Birthday());\n\t\t});\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/edit_birthday_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nclass Birthday;\n} // namespace Data\n\nnamespace Ui {\n\nclass GenericBox;\n\nenum class EditBirthdayType {\n\tEdit,\n\tSuggest,\n\tConfirmSuggestion,\n};\n\nvoid EditBirthdayBox(\n\tnot_null<Ui::GenericBox*> box,\n\tData::Birthday current,\n\tFn<void(Data::Birthday)> save,\n\tEditBirthdayType type = EditBirthdayType::Edit);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/edit_factcheck_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/edit_factcheck_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n\nvoid EditFactcheckBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tTextWithEntities current,\n\t\tint limit,\n\t\tFn<void(TextWithEntities)> save,\n\t\tFn<void(not_null<Ui::InputField*>)> initField) {\n\tbox->setTitle(tr::lng_factcheck_title());\n\n\tconst auto field = box->addRow(object_ptr<Ui::InputField>(\n\t\tbox,\n\t\tst::factcheckField,\n\t\tUi::InputField::Mode::NoNewlines,\n\t\ttr::lng_factcheck_placeholder(),\n\t\tTextWithTags{\n\t\t\tcurrent.text,\n\t\t\tTextUtilities::ConvertEntitiesToTextTags(current.entities)\n\t\t}));\n\tAddLengthLimitLabel(field, limit);\n\tinitField(field);\n\n\tenum class State {\n\t\tInitial,\n\t\tChanged,\n\t\tRemoved,\n\t};\n\tconst auto state = box->lifetime().make_state<rpl::variable<State>>(\n\t\tState::Initial);\n\tfield->changes() | rpl::on_next([=] {\n\t\tconst auto now = field->getLastText().trimmed();\n\t\t*state = !now.isEmpty()\n\t\t\t? State::Changed\n\t\t\t: current.empty()\n\t\t\t? State::Initial\n\t\t\t: State::Removed;\n\t}, field->lifetime());\n\n\tstate->value() | rpl::on_next([=](State state) {\n\t\tbox->clearButtons();\n\t\tif (state == State::Removed) {\n\t\t\tbox->addButton(tr::lng_box_remove(), [=] {\n\t\t\t\tbox->closeBox();\n\t\t\t\tsave({});\n\t\t\t}, st::attentionBoxButton);\n\t\t} else if (state == State::Initial) {\n\t\t\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\t\t\tif (current.empty()) {\n\t\t\t\t\tfield->showError();\n\t\t\t\t} else {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t}\n\t\t\t});\n\t\t} else {\n\t\t\tbox->addButton(tr::lng_settings_save(), [=] {\n\t\t\t\tauto result = field->getTextWithAppliedMarkdown();\n\t\t\t\tif (result.text.size() > limit) {\n\t\t\t\t\tfield->showError();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tbox->closeBox();\n\t\t\t\tsave({\n\t\t\t\t\tresult.text,\n\t\t\t\t\tTextUtilities::ConvertTextTagsToEntities(result.tags)\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t}, box->lifetime());\n\n\tbox->setFocusCallback([=] {\n\t\tfield->setFocusFast();\n\t});\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/edit_factcheck_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n\nnamespace Ui {\nclass InputField;\n} // namespace Ui\n\nvoid EditFactcheckBox(\n\tnot_null<Ui::GenericBox*> box,\n\tTextWithEntities current,\n\tint limit,\n\tFn<void(TextWithEntities)> save,\n\tFn<void(not_null<Ui::InputField*>)> initField);\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/edit_invite_link.h\"\n\n#include \"base/unixtime.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/boxes/choose_date_time.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/fields/number_input.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_info.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kMaxLimit = std::numeric_limits<int>::max();\nconstexpr auto kHour = 3600;\nconstexpr auto kDay = 86400;\nconstexpr auto kMaxLabelLength = 32;\n\n[[nodiscard]] QString FormatExpireDate(TimeId date) {\n\tif (date > 0) {\n\t\treturn langDateTime(base::unixtime::parse(date));\n\t} else if (-date < kDay) {\n\t\treturn tr::lng_hours(tr::now, lt_count, (-date / kHour));\n\t} else if (-date < 7 * kDay) {\n\t\treturn tr::lng_days(tr::now, lt_count, (-date / kDay));\n\t} else {\n\t\treturn tr::lng_weeks(tr::now, lt_count, (-date / (7 * kDay)));\n\t}\n}\n\n} // namespace\n\nvoid EditInviteLinkBox(\n\t\tnot_null<GenericBox*> box,\n\t\tFn<InviteLinkSubscriptionToggle()> fillSubscription,\n\t\tconst InviteLinkFields &data,\n\t\tFn<void(InviteLinkFields)> done) {\n\tusing namespace rpl::mappers;\n\n\tconst auto link = data.link;\n\tconst auto isGroup = data.isGroup;\n\tconst auto isPublic = data.isPublic;\n\tconst auto subscriptionLocked = data.subscriptionCredits > 0;\n\tbox->setTitle(link.isEmpty()\n\t\t? tr::lng_group_invite_new_title()\n\t\t: tr::lng_group_invite_edit_title());\n\n\tconst auto container = box->verticalLayout();\n\tconst auto addTitle = [&](\n\t\t\tnot_null<VerticalLayout*> container,\n\t\t\trpl::producer<QString> text) {\n\t\tcontainer->add(\n\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\tcontainer,\n\t\t\t\tstd::move(text),\n\t\t\t\tst::defaultSubsectionTitle),\n\t\t\t(st::defaultSubsectionTitlePadding\n\t\t\t\t+ style::margins(0, st::defaultVerticalListSkip, 0, 0)));\n\t};\n\tconst auto addDivider = [&](\n\t\t\tnot_null<VerticalLayout*> container,\n\t\t\trpl::producer<QString> text,\n\t\t\tstyle::margins margins = style::margins()) {\n\t\tcontainer->add(\n\t\t\tobject_ptr<DividerLabel>(\n\t\t\t\tcontainer,\n\t\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\t\tcontainer,\n\t\t\t\t\tstd::move(text),\n\t\t\t\t\tst::boxDividerLabel),\n\t\t\t\tst::defaultBoxDividerLabelPadding),\n\t\t\tmargins);\n\t};\n\n\tconst auto now = base::unixtime::now();\n\tconst auto expire = data.expireDate ? data.expireDate : kMaxLimit;\n\tconst auto expireGroup = std::make_shared<RadiobuttonGroup>(expire);\n\tconst auto usage = data.usageLimit ? data.usageLimit : kMaxLimit;\n\tconst auto usageGroup = std::make_shared<RadiobuttonGroup>(usage);\n\n\tusing Buttons = base::flat_map<int, base::unique_qptr<Radiobutton>>;\n\tstruct State {\n\t\tButtons expireButtons;\n\t\tButtons usageButtons;\n\t\tint expireValue = 0;\n\t\tint usageValue = 0;\n\t\trpl::variable<bool> requestApproval = false;\n\t\trpl::variable<bool> subscription = false;\n\t};\n\tconst auto state = box->lifetime().make_state<State>(State{\n\t\t.expireValue = expire,\n\t\t.usageValue = usage,\n\t\t.requestApproval = (data.requestApproval && !isPublic),\n\t\t.subscription = false,\n\t});\n\n\tconst auto requestApproval = (isPublic || subscriptionLocked)\n\t\t? nullptr\n\t\t: container->add(\n\t\t\tobject_ptr<SettingsButton>(\n\t\t\t\tcontainer,\n\t\t\t\ttr::lng_group_invite_request_approve(),\n\t\t\t\tst::settingsButtonNoIcon),\n\t\t\tstyle::margins{ 0, 0, 0, st::defaultVerticalListSkip });\n\tif (requestApproval) {\n\t\trequestApproval->toggleOn(state->requestApproval.value(), true);\n\t\trequestApproval->setClickedCallback([=] {\n\t\t\tstate->requestApproval.force_assign(!requestApproval->toggled());\n\t\t\tstate->subscription.force_assign(false);\n\t\t});\n\t\taddDivider(container, rpl::conditional(\n\t\t\tstate->requestApproval.value(),\n\t\t\t(isGroup\n\t\t\t\t? tr::lng_group_invite_about_approve()\n\t\t\t\t: tr::lng_group_invite_about_approve_channel()),\n\t\t\t(isGroup\n\t\t\t\t? tr::lng_group_invite_about_no_approve()\n\t\t\t\t: tr::lng_group_invite_about_no_approve_channel())));\n\t}\n\tauto credits = (Ui::NumberInput*)(nullptr);\n\tif (!isPublic && fillSubscription) {\n\t\tUi::AddSkip(container);\n\t\tconst auto &[subscription, input] = fillSubscription();\n\t\tcredits = input.get();\n\t\tsubscription->toggleOn(state->subscription.value(), true);\n\t\tif (subscriptionLocked) {\n\t\t\tinput->setText(QString::number(data.subscriptionCredits));\n\t\t\tinput->setReadOnly(true);\n\t\t\tstate->subscription.force_assign(true);\n\t\t\tstate->requestApproval.force_assign(false);\n\t\t\tsubscription->setToggleLocked(true);\n\t\t\tsubscription->finishAnimating();\n\t\t}\n\t\tsubscription->setClickedCallback([=, show = box->uiShow()] {\n\t\t\tif (subscriptionLocked) {\n\t\t\t\tshow->showToast(\n\t\t\t\t\ttr::lng_group_invite_subscription_toast(tr::now));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->subscription.force_assign(!subscription->toggled());\n\t\t\tstate->requestApproval.force_assign(false);\n\t\t});\n\t}\n\n\tconst auto labelField = container->add(\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tcontainer,\n\t\t\tst::defaultInputField,\n\t\t\ttr::lng_group_invite_label_header(),\n\t\t\tdata.label),\n\t\tstyle::margins(\n\t\t\tst::defaultSubsectionTitlePadding.left(),\n\t\t\tst::defaultVerticalListSkip,\n\t\t\tst::defaultSubsectionTitlePadding.right(),\n\t\t\tst::defaultVerticalListSkip * 2));\n\tlabelField->setMaxLength(kMaxLabelLength);\n\taddDivider(container, tr::lng_group_invite_label_about());\n\n\tconst auto &saveLabel = link.isEmpty()\n\t\t? tr::lng_formatting_link_create\n\t\t: tr::lng_settings_save;\n\tbox->addButton(saveLabel(), [=] {\n\t\tconst auto label = labelField->getLastText();\n\t\tconst auto expireDate = (state->expireValue == kMaxLimit)\n\t\t\t? 0\n\t\t\t: (state->expireValue < 0)\n\t\t\t? (base::unixtime::now() - state->expireValue)\n\t\t\t: state->expireValue;\n\t\tconst auto usageLimit = (state->usageValue == kMaxLimit)\n\t\t\t? 0\n\t\t\t: state->usageValue;\n\t\tdone(InviteLinkFields{\n\t\t\t.link = link,\n\t\t\t.label = label,\n\t\t\t.expireDate = expireDate,\n\t\t\t.usageLimit = usageLimit,\n\t\t\t.subscriptionCredits = credits\n\t\t\t\t? credits->getLastText().toInt()\n\t\t\t\t: 0,\n\t\t\t.requestApproval = state->requestApproval.current(),\n\t\t\t.isGroup = isGroup,\n\t\t\t.isPublic = isPublic,\n\t\t});\n\t});\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\n\tif (subscriptionLocked) {\n\t\treturn;\n\t}\n\taddTitle(container, tr::lng_group_invite_expire_title());\n\tconst auto expiresWrap = container->add(\n\t\tobject_ptr<VerticalLayout>(container),\n\t\tstyle::margins(0, 0, 0, st::defaultVerticalListSkip));\n\taddDivider(\n\t\tcontainer,\n\t\ttr::lng_group_invite_expire_about());\n\n\tconst auto usagesSlide = container->add(\n\t\tobject_ptr<SlideWrap<VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<VerticalLayout>(container)));\n\tconst auto usagesInner = usagesSlide->entity();\n\taddTitle(usagesInner, tr::lng_group_invite_usage_title());\n\tconst auto usagesWrap = usagesInner->add(\n\t\tobject_ptr<VerticalLayout>(usagesInner),\n\t\tstyle::margins(0, 0, 0, st::defaultVerticalListSkip));\n\taddDivider(usagesInner, tr::lng_group_invite_usage_about());\n\n\tstatic const auto addButton = [](\n\t\t\tnot_null<VerticalLayout*> container,\n\t\t\tconst std::shared_ptr<RadiobuttonGroup> &group,\n\t\t\tint value,\n\t\t\tconst QString &text) {\n\t\treturn container->add(\n\t\t\tobject_ptr<Radiobutton>(\n\t\t\t\tcontainer,\n\t\t\t\tgroup,\n\t\t\t\tvalue,\n\t\t\t\ttext),\n\t\t\tst::inviteLinkLimitMargin);\n\t};\n\n\tconst auto regenerate = [=] {\n\t\texpireGroup->setValue(state->expireValue);\n\t\tusageGroup->setValue(state->usageValue);\n\n\t\tauto expires = std::vector{ kMaxLimit, -kHour, -kDay, -kDay * 7, 0 };\n\t\tauto usages = std::vector{ kMaxLimit, 1, 10, 100, 0 };\n\t\tauto defaults = State();\n\t\tfor (auto i = begin(expires); i != end(expires); ++i) {\n\t\t\tif (*i == state->expireValue) {\n\t\t\t\tbreak;\n\t\t\t} else if (*i == kMaxLimit) {\n\t\t\t\tcontinue;\n\t\t\t} else if (!*i || (now - *i >= state->expireValue)) {\n\t\t\t\texpires.insert(i, state->expireValue);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tfor (auto i = begin(usages); i != end(usages); ++i) {\n\t\t\tif (*i == state->usageValue) {\n\t\t\t\tbreak;\n\t\t\t} else if (*i == kMaxLimit) {\n\t\t\t\tcontinue;\n\t\t\t} else if (!*i || *i > state->usageValue) {\n\t\t\t\tusages.insert(i, state->usageValue);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tstate->expireButtons.clear();\n\t\tstate->usageButtons.clear();\n\t\tfor (const auto limit : expires) {\n\t\t\tconst auto text = (limit == kMaxLimit)\n\t\t\t\t? tr::lng_group_invite_expire_never(tr::now)\n\t\t\t\t: !limit\n\t\t\t\t? tr::lng_group_invite_expire_custom(tr::now)\n\t\t\t\t: FormatExpireDate(limit);\n\t\t\tstate->expireButtons.emplace(\n\t\t\t\tlimit,\n\t\t\t\taddButton(expiresWrap, expireGroup, limit, text));\n\t\t}\n\t\tfor (const auto limit : usages) {\n\t\t\tconst auto text = (limit == kMaxLimit)\n\t\t\t\t? tr::lng_group_invite_usage_any(tr::now)\n\t\t\t\t: !limit\n\t\t\t\t? tr::lng_group_invite_usage_custom(tr::now)\n\t\t\t\t: Lang::FormatCountDecimal(limit);\n\t\t\tstate->usageButtons.emplace(\n\t\t\t\tlimit,\n\t\t\t\taddButton(usagesWrap, usageGroup, limit, text));\n\t\t}\n\t};\n\n\tconst auto guard = base::make_weak(box);\n\texpireGroup->setChangedCallback([=](int value) {\n\t\tif (value) {\n\t\t\tstate->expireValue = value;\n\t\t\treturn;\n\t\t}\n\t\texpireGroup->setValue(state->expireValue);\n\t\tbox->getDelegate()->show(Box([=](not_null<GenericBox*> box) {\n\t\t\tconst auto save = [=](TimeId result) {\n\t\t\t\tif (!result) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (guard) {\n\t\t\t\t\tstate->expireValue = result;\n\t\t\t\t\tregenerate();\n\t\t\t\t}\n\t\t\t\tbox->closeBox();\n\t\t\t};\n\t\t\tconst auto now = base::unixtime::now();\n\t\t\tconst auto time = (state->expireValue == kMaxLimit)\n\t\t\t\t? (now + kDay)\n\t\t\t\t: (state->expireValue > now)\n\t\t\t\t? state->expireValue\n\t\t\t\t: (state->expireValue < 0)\n\t\t\t\t? (now - state->expireValue)\n\t\t\t\t: (now + kDay);\n\t\t\tChooseDateTimeBox(box, {\n\t\t\t\t.title = tr::lng_group_invite_expire_after(),\n\t\t\t\t.submit = tr::lng_settings_save(),\n\t\t\t\t.done = save,\n\t\t\t\t.time = time,\n\t\t\t});\n\t\t}));\n\t});\n\tusageGroup->setChangedCallback([=](int value) {\n\t\tif (value) {\n\t\t\tstate->usageValue = value;\n\t\t\treturn;\n\t\t}\n\t\tusageGroup->setValue(state->usageValue);\n\t\tbox->getDelegate()->show(Box([=](not_null<GenericBox*> box) {\n\t\t\tconst auto height = st::boxPadding.bottom()\n\t\t\t\t+ st::defaultInputField.heightMin\n\t\t\t\t+ st::boxPadding.bottom();\n\t\t\tbox->setTitle(tr::lng_group_invite_expire_after());\n\t\t\tconst auto wrap = box->addRow(object_ptr<FixedHeightWidget>(\n\t\t\t\tbox,\n\t\t\t\theight));\n\t\t\tconst auto input = CreateChild<NumberInput>(\n\t\t\t\twrap,\n\t\t\t\tst::defaultInputField,\n\t\t\t\ttr::lng_group_invite_custom_limit(),\n\t\t\t\t(state->usageValue == kMaxLimit\n\t\t\t\t\t? QString()\n\t\t\t\t\t: QString::number(state->usageValue)),\n\t\t\t\t200'000);\n\t\t\twrap->widthValue(\n\t\t\t) | rpl::on_next([=](int width) {\n\t\t\t\tinput->resize(width, input->height());\n\t\t\t\tinput->moveToLeft(0, st::boxPadding.bottom());\n\t\t\t}, input->lifetime());\n\t\t\tbox->setFocusCallback([=] {\n\t\t\t\tinput->setFocusFast();\n\t\t\t});\n\n\t\t\tconst auto save = [=] {\n\t\t\t\tconst auto value = input->getLastText().toInt();\n\t\t\t\tif (value <= 0) {\n\t\t\t\t\tinput->showError();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (guard) {\n\t\t\t\t\tstate->usageValue = value;\n\t\t\t\t\tregenerate();\n\t\t\t\t}\n\t\t\t\tbox->closeBox();\n\t\t\t};\n\t\t\tQObject::connect(input, &NumberInput::submitted, save);\n\t\t\tbox->addButton(tr::lng_settings_save(), save);\n\t\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t\t}));\n\t});\n\n\tregenerate();\n\n\tusagesSlide->toggleOn(state->requestApproval.value() | rpl::map(!_1));\n\tusagesSlide->finishAnimating();\n}\n\nvoid CreateInviteLinkBox(\n\t\tnot_null<GenericBox*> box,\n\t\tFn<InviteLinkSubscriptionToggle()> fillSubscription,\n\t\tbool isGroup,\n\t\tbool isPublic,\n\t\tFn<void(InviteLinkFields)> done) {\n\tEditInviteLinkBox(\n\t\tbox,\n\t\tstd::move(fillSubscription),\n\t\tInviteLinkFields{ .isGroup = isGroup, .isPublic = isPublic },\n\t\tstd::move(done));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/edit_invite_link.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nclass GenericBox;\nclass NumberInput;\nclass SettingsButton;\n\nstruct InviteLinkFields {\n\tQString link;\n\tQString label;\n\tTimeId expireDate = 0;\n\tint usageLimit = 0;\n\tint subscriptionCredits = 0;\n\tbool requestApproval = false;\n\tbool isGroup = false;\n\tbool isPublic = false;\n};\n\nstruct InviteLinkSubscriptionToggle final {\n\tnot_null<Ui::SettingsButton*> button;\n\tnot_null<Ui::NumberInput*> amount;\n};\n\nvoid EditInviteLinkBox(\n\tnot_null<Ui::GenericBox*> box,\n\tFn<InviteLinkSubscriptionToggle()> fillSubscription,\n\tconst InviteLinkFields &data,\n\tFn<void(InviteLinkFields)> done);\n\nvoid CreateInviteLinkBox(\n\tnot_null<Ui::GenericBox*> box,\n\tFn<InviteLinkSubscriptionToggle()> fillSubscription,\n\tbool isGroup,\n\tbool isPublic,\n\tFn<void(InviteLinkFields)> done);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/edit_invite_link_session.h\"\n\n#include \"core/ui_integration.h\" // TextContext\n#include \"data/components/credits.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"ui/boxes/edit_invite_link.h\" // InviteLinkSubscriptionToggle\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/number_input.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_channel_earn.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_info.h\"\n\nnamespace Ui {\n\nInviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<PeerData*> peer) {\n\tstruct State final {\n\t\trpl::variable<float64> usdRate = 0;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tconst auto currency = u\"USD\"_q;\n\n\tconst auto container = box->verticalLayout();\n\tconst auto toggle = container->add(\n\t\tobject_ptr<SettingsButton>(\n\t\t\tcontainer,\n\t\t\ttr::lng_group_invite_subscription(),\n\t\t\tst::settingsButtonNoIconLocked),\n\t\tstyle::margins{ 0, 0, 0, st::defaultVerticalListSkip });\n\n\tconst auto maxCredits = peer->session().appConfig().get<int>(\n\t\tu\"stars_subscription_amount_max\"_q,\n\t\t2500);\n\n\tconst auto &st = st::inviteLinkCreditsField;\n\tconst auto skip = st.textMargins.top() / 2;\n\tconst auto wrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::VerticalLayout>(container)));\n\tbox->setShowFinishedCallback([=] {\n\t\twrap->toggleOn(toggle->toggledValue());\n\t\twrap->finishAnimating();\n\t});\n\tconst auto inputContainer = wrap->entity()->add(\n\t\tCreateSkipWidget(container, st.heightMin - skip));\n\tconst auto input = CreateChild<NumberInput>(\n\t\tinputContainer,\n\t\tst,\n\t\ttr::lng_group_invite_subscription_ph(),\n\t\tQString(),\n\t\tstd::pow(QString::number(maxCredits).size(), 10));\n\twrap->toggledValue() | rpl::on_next([=](bool shown) {\n\t\tif (shown) {\n\t\t\tinput->setFocus();\n\t\t}\n\t}, input->lifetime());\n\tconst auto icon = CreateSingleStarWidget(\n\t\tinputContainer,\n\t\tst.style.font->height);\n\tconst auto priceOverlay = Ui::CreateChild<Ui::RpWidget>(inputContainer);\n\tpriceOverlay->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tinputContainer->sizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\tinput->resize(\n\t\t\tsize.width() - rect::m::sum::h(st::boxRowPadding),\n\t\t\tst.heightMin);\n\t\tinput->moveToLeft(st::boxRowPadding.left(), -skip);\n\t\ticon->moveToLeft(\n\t\t\tst::boxRowPadding.left(),\n\t\t\tinput->pos().y() + st.textMargins.top());\n\t\tpriceOverlay->resize(size);\n\t}, input->lifetime());\n\tToggleChildrenVisibility(inputContainer, true);\n\tQObject::connect(input, &Ui::MaskedInputField::changed, [=] {\n\t\tconst auto amount = input->getLastText().toDouble();\n\t\tif (amount > maxCredits) {\n\t\t\tinput->setText(QString::number(maxCredits));\n\t\t}\n\t\tpriceOverlay->update();\n\t});\n\tpriceOverlay->paintRequest(\n\t) | rpl::on_next([=, right = st::boxRowPadding.right()] {\n\t\tif (state->usdRate.current() <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto amount = input->getLastText().toDouble();\n\t\tif (amount <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto text = tr::lng_group_invite_subscription_price(\n\t\t\ttr::now,\n\t\t\tlt_cost,\n\t\t\tUi::FillAmountAndCurrency(\n\t\t\t\tamount * state->usdRate.current(),\n\t\t\t\tcurrency));\n\t\tauto p = QPainter(priceOverlay);\n\t\tp.setFont(st.placeholderFont);\n\t\tp.setPen(st.placeholderFg);\n\t\tp.setBrush(Qt::NoBrush);\n\t\tconst auto m = QMargins(0, skip, right, 0);\n\t\tp.drawText(priceOverlay->rect() - m, text, style::al_right);\n\t}, priceOverlay->lifetime());\n\n\tstate->usdRate = peer->session().credits().rateValue(peer);\n\n\tauto about = object_ptr<Ui::FlatLabel>(\n\t\tcontainer,\n\t\ttr::lng_group_invite_subscription_about(\n\t\t\tlt_link,\n\t\t\ttr::lng_group_invite_subscription_about_link(\n\t\t\t\tlt_emoji,\n\t\t\t\trpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),\n\t\t\t\ttr::rich\n\t\t\t) | rpl::map([](TextWithEntities text) {\n\t\t\t\treturn tr::link(\n\t\t\t\t\tstd::move(text),\n\t\t\t\t\ttr::lng_group_invite_subscription_about_url(tr::now));\n\t\t\t}),\n\t\t\ttr::rich),\n\t\tst::boxDividerLabel);\n\tUi::AddSkip(wrap->entity());\n\tUi::AddSkip(wrap->entity());\n\tcontainer->add(object_ptr<Ui::DividerLabel>(\n\t\tcontainer,\n\t\tstd::move(about),\n\t\tst::defaultBoxDividerLabelPadding));\n\treturn { toggle, input };\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/edit_invite_link_session.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\n\nnamespace Ui {\n\nclass GenericBox;\nclass SettingsButton;\n\nstruct InviteLinkSubscriptionToggle;\n\nInviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<PeerData*> peer);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/emoji_stake_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/emoji_stake_box.h\"\n\n#include \"base/event_filter.h\"\n#include \"base/object_ptr.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"data/components/credits.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"info/channel_statistics/earn/earn_format.h\"\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\" // CreateLottieIcon\n#include \"settings/settings_credits_graphics.h\" // AddBalanceWidget\n#include \"ui/controls/ton_common.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/custom_emoji_text_badge.h\"\n#include \"ui/text/text_lottie_custom_emoji.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/fields/number_input.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/basic_click_handlers.h\" // UrlClickHandler\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/vertical_list.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_calls.h\" // confcallLinkFooterOr\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_credits.h\" // creditsHistoryRightSkip\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\" // settingsCloudPasswordIconSize\n#include \"styles/style_widgets.h\"\n\n#include <QtSvg/QSvgRenderer>\n\nnamespace Ui {\nnamespace {\n\n[[nodiscard]] not_null<RpWidget*> AddMoneyInputIcon(\n\t\tnot_null<QWidget*> parent,\n\t\tText::PaletteDependentEmoji emoji) {\n\tauto helper = Text::CustomEmojiHelper();\n\tauto text = helper.paletteDependent(std::move(emoji));\n\treturn CreateChild<FlatLabel>(\n\t\tparent,\n\t\trpl::single(std::move(text)),\n\t\tst::defaultFlatLabel,\n\t\tst::defaultPopupMenu,\n\t\thelper.context());\n}\n\n[[nodiscard]] QImage MakeEmojiFrame(int index, int size) {\n\tconst auto path = u\":/gui/dice/dice%1.svg\"_q.arg(index);\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto svg = QSvgRenderer(path);\n\tauto result = QImage(\n\t\tQSize(size, size) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\tresult.fill(Qt::transparent);\n\n\tauto p = QPainter(&result);\n\tsvg.render(&p, QRect(0, 0, size, size));\n\tp.end();\n\n\treturn result;\n}\n\n[[nodiscard]] object_ptr<RpWidget> MakeLogo(not_null<GenericBox*> box) {\n\tconst auto &size = st::settingsCloudPasswordIconSize;\n\tauto icon = Settings::CreateLottieIcon(\n\t\tbox->verticalLayout(),\n\t\t{ .name = u\"dice_6\"_q, .sizeOverride = { size, size } },\n\t\tQMargins());\n\tconst auto animate = std::move(icon.animate);\n\tbox->showFinishes() | rpl::take(1) | rpl::on_next([=] {\n\t\tanimate(anim::repeat::once);\n\t}, box->lifetime());\n\treturn std::move(icon.widget);\n}\n\n[[nodiscard]] object_ptr<RpWidget> MakeTable(\n\t\tnot_null<RpWidget*> parent,\n\t\tconst EmojiGameStakeArgs &args) {\n\tauto result = object_ptr<RpWidget>(parent);\n\tconst auto raw = result.data();\n\n\tstruct State {\n\t\tstd::array<QImage, 6> dices;\n\t\tstd::array<Text::String, 7> multiplicators;\n\t};\n\tconst auto state = raw->lifetime().make_state<State>();\n\tconst auto serialize = [&](int milliReward) {\n\t\treturn QString(QChar(0xD7)) + QString::number(milliReward / 1000.);\n\t};\n\tconst auto esize = st::stakeEmojiSize;\n\tfor (auto i = 0; i != 6; ++i) {\n\t\tstate->dices[i] = MakeEmojiFrame(i + 1, esize);\n\n\t\tconst auto value = (i < args.milliRewards.size())\n\t\t\t? args.milliRewards[i]\n\t\t\t: 0;\n\t\tstate->multiplicators[i].setText(\n\t\t\tst::semiboldTextStyle,\n\t\t\tserialize(value));\n\t}\n\tstate->multiplicators[6].setText(\n\t\tst::semiboldTextStyle,\n\t\tserialize(args.jackpotMilliReward));\n\n\traw->paintOn([=](QPainter &p) {\n\t\tauto path = QPainterPath();\n\t\tconst auto &st = st::defaultTable;\n\t\tconst auto border = st.border;\n\t\tconst auto half = border / 2.;\n\t\tconst auto add = QMargins(border, border, border, border);\n\t\tconst auto inner = raw->rect().marginsRemoved(add);\n\t\tpath.addRoundedRect(inner, st.radius, st.radius);\n\t\t{\n\t\t\tconst auto y = border + inner.height() / 2.;\n\t\t\tpath.moveTo(border + half, y);\n\t\t\tpath.lineTo(border + inner.width() - half, y);\n\t\t}\n\t\t{\n\t\t\tconst auto x = border + inner.width() / 2.;\n\t\t\tpath.moveTo(x, border + half);\n\t\t\tpath.lineTo(x, border + inner.height() - half);\n\t\t}\n\t\t{\n\t\t\tconst auto x = border + (inner.width() - border) / 4.;\n\t\t\tpath.moveTo(x, border + half);\n\t\t\tpath.lineTo(x, border + inner.height() - half);\n\t\t}\n\t\t{\n\t\t\tconst auto x = border\n\t\t\t\t+ (inner.width() / 2.)\n\t\t\t\t+ (inner.width() - border) / 4.;\n\t\t\tpath.moveTo(x, border + half);\n\t\t\tpath.lineTo(x, border + (inner.height() / 2.) - half);\n\t\t}\n\t\tauto pen = st.borderFg->p;\n\t\tpen.setWidth(st.border);\n\t\tp.setPen(pen);\n\t\tp.setBrush(Qt::NoBrush);\n\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.drawPath(path);\n\n\t\tconst auto etop = st::stakeEmojiTop;\n\t\tconst auto ttop = etop + esize;\n\t\t{\n\t\t\tauto left = border + half;\n\t\t\tconst auto width = (inner.width() - 3 * border) / 4.;\n\t\t\tauto top = border + half;\n\t\t\tfor (auto i = 0; i != 7; ++i, left += width + border) {\n\t\t\t\tif (i == 4) {\n\t\t\t\t\tleft -= 4 * (width + border);\n\t\t\t\t\ttop += border + half + inner.height() / 2.;\n\t\t\t\t}\n\t\t\t\tconst auto x = int(base::SafeRound(left));\n\t\t\t\tconst auto y = int(base::SafeRound(top));\n\t\t\t\tconst auto right = (i < 6)\n\t\t\t\t\t? int(base::SafeRound(left + width))\n\t\t\t\t\t: int(base::SafeRound(left + 2 * width + border));\n\t\t\t\tconst auto w = (right - x);\n\n\t\t\t\tif (i < 6) {\n\t\t\t\t\tp.drawImage(\n\t\t\t\t\t\tx + (w - esize) / 2,\n\t\t\t\t\t\ty + etop,\n\t\t\t\t\t\tstate->dices[i]);\n\t\t\t\t} else {\n\t\t\t\t\tconst auto &last = state->dices.back();\n\t\t\t\t\tfor (auto j = 0; j != 3; ++j) {\n\t\t\t\t\t\tp.drawImage(\n\t\t\t\t\t\t\tx + (w - 3 * esize) / 2 + (esize * j),\n\t\t\t\t\t\t\ty + etop,\n\t\t\t\t\t\t\tlast);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tp.setPen(st::windowBoldFg);\n\t\t\t\tconst auto &mul = state->multiplicators[i];\n\t\t\t\tmul.draw(p, {\n\t\t\t\t\t.position = { x + (w - mul.maxWidth()) / 2, y + ttop },\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t});\n\traw->widthValue() | rpl::on_next([=](int width) {\n\t\traw->resize(width, width / 3);\n\t}, raw->lifetime());\n\treturn result;\n}\n\n} // namespace\n\nnot_null<NumberInput*> AddStarsInputField(\n\t\tnot_null<VerticalLayout*> container,\n\t\tStarsInputFieldArgs &&args) {\n\tconst auto wrap = container->add(\n\t\tobject_ptr<FixedHeightWidget>(\n\t\t\tcontainer,\n\t\t\tst::editTagField.heightMin),\n\t\tst::boxRowPadding);\n\tconst auto result = CreateChild<NumberInput>(\n\t\twrap,\n\t\tst::editTagField,\n\t\trpl::single(u\"0\"_q),\n\t\targs.value ? QString::number(*args.value) : QString(),\n\t\targs.max ? args.max : std::numeric_limits<int>::max());\n\tconst auto icon = AddMoneyInputIcon(\n\t\tresult,\n\t\tEarn::IconCreditsEmoji());\n\n\twrap->widthValue() | rpl::on_next([=](int width) {\n\t\ticon->move(st::starsFieldIconPosition);\n\t\tresult->move(0, 0);\n\t\tresult->resize(width, result->height());\n\t\twrap->resize(width, result->height());\n\t}, wrap->lifetime());\n\n\treturn result;\n}\n\nnot_null<InputField*> AddTonInputField(\n\t\tnot_null<VerticalLayout*> container,\n\t\tTonInputFieldArgs &&args) {\n\tconst auto wrap = container->add(\n\t\tobject_ptr<FixedHeightWidget>(\n\t\t\tcontainer,\n\t\t\tst::editTagField.heightMin),\n\t\tst::boxRowPadding);\n\tconst auto result = CreateTonAmountInput(\n\t\twrap,\n\t\trpl::single('0' + TonAmountSeparator() + '0'),\n\t\targs.value);\n\tconst auto icon = AddMoneyInputIcon(\n\t\tresult,\n\t\tEarn::IconCurrencyEmoji());\n\n\twrap->widthValue() | rpl::on_next([=](int width) {\n\t\ticon->move(st::tonFieldIconPosition);\n\t\tresult->move(0, 0);\n\t\tresult->resize(width, result->height());\n\t\twrap->resize(width, result->height());\n\t}, wrap->lifetime());\n\n\treturn result;\n}\n\nvoid AddApproximateUsd(\n\t\tnot_null<QWidget*> field,\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<CreditsAmount> price) {\n\tauto value = std::move(price) | rpl::map([=](CreditsAmount amount) {\n\t\tif (!amount) {\n\t\t\treturn QString();\n\t\t}\n\t\tconst auto appConfig = &session->appConfig();\n\t\tconst auto rate = amount.ton()\n\t\t\t? appConfig->currencySellRate()\n\t\t\t: (appConfig->starsSellRate() / 100.);\n\t\treturn Info::ChannelEarn::ToUsd(amount, rate, 2);\n\t});\n\tconst auto usd = Ui::CreateChild<Ui::FlatLabel>(\n\t\tfield,\n\t\tstd::move(value),\n\t\tst::suggestPriceEstimate);\n\tconst auto move = [=] {\n\t\tusd->moveToRight(0, st::suggestPriceEstimateTop);\n\t};\n\tbase::install_event_filter(field, [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::Resize) {\n\t\t\tmove();\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\tusd->widthValue() | rpl::on_next(move, usd->lifetime());\n}\n\nvoid InsufficientTonBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Main::Session*> session,\n\t\tCreditsAmount required) {\n\tbox->setStyle(st::suggestPriceBox);\n\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\tbox->closeBox();\n\t});\n\n\tauto icon = Settings::CreateLottieIcon(\n\t\tbox->verticalLayout(),\n\t\t{\n\t\t\t.name = u\"diamond\"_q,\n\t\t\t.sizeOverride = st::normalBoxLottieSize,\n\t\t},\n\t\t{});\n\tbox->setShowFinishedCallback([animate = std::move(icon.animate)] {\n\t\tanimate(anim::repeat::loop);\n\t});\n\tbox->addRow(std::move(icon.widget), st::lowTonIconPadding);\n\tconst auto add = required - session->credits().tonBalance();\n\tconst auto nano = add.whole() * Ui::kNanosInOne + add.nano();\n\tconst auto amount = Ui::FormatTonAmount(nano).full;\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_suggest_low_ton_title(tr::now, lt_amount, amount),\n\t\t\tst::boxTitle),\n\t\tst::boxRowPadding + st::lowTonTitlePadding,\n\t\tstyle::al_top);\n\tconst auto label = box->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_suggest_low_ton_text(tr::rich),\n\t\t\tst::lowTonText),\n\t\tst::boxRowPadding + st::lowTonTextPadding,\n\t\tstyle::al_top);\n\tlabel->setTryMakeSimilarLines(true);\n\tlabel->resizeToWidth(\n\t\tst::boxWidth - st::boxRowPadding.left() - st::boxRowPadding.right());\n\n\tconst auto url = tr::lng_suggest_low_ton_fragment_url(tr::now);\n\tconst auto button = box->addButton(\n\t\ttr::lng_suggest_low_ton_fragment(),\n\t\t[=] { UrlClickHandler::Open(url); });\n\tconst auto buttonWidth = st::boxWidth\n\t\t- rect::m::sum::h(st::suggestPriceBox.buttonPadding);\n\tbutton->widthValue() | rpl::filter([=] {\n\t\treturn (button->widthNoMargins() != buttonWidth);\n\t}) | rpl::on_next([=] {\n\t\tbutton->resizeToWidth(buttonWidth);\n\t}, button->lifetime());\n}\n\nvoid AddStakePresets(\n\t\tnot_null<VerticalLayout*> container,\n\t\tnot_null<Main::Session*> session,\n\t\tFn<void(int64)> callback) {\n\tconst auto presets = session->appConfig().stakeDiceNanoTonSuggested();\n\tif (presets.empty()) {\n\t\treturn;\n\t}\n\tconstexpr auto kPerRow = 4;\n\tconst auto count = int(presets.size());\n\tconst auto rows = (count + kPerRow - 1) / kPerRow;\n\tconst auto smallrow = count / rows;\n\tconst auto bigrow = smallrow + 1;\n\tconst auto bigrows = count % rows;\n\tstruct State {\n\t\tstd::vector<std::vector<not_null<RoundButton*>>> buttons;\n\t};\n\tconst auto wrap = container->add(\n\t\tobject_ptr<FixedHeightWidget>(\n\t\t\tcontainer,\n\t\t\t(rows * st::stakePresetButton.height\n\t\t\t\t+ (rows - 1) * st::stakePresetButtonSkip.y())),\n\t\t(st::boxRowPadding\n\t\t\t+ QMargins(0, st::stakeBox.buttonPadding.top(), 0, 0)));\n\tconst auto diamond = QString::fromUtf8(\" \\xf0\\x9f\\x92\\x8e\");\n\tconst auto state = wrap->lifetime().make_state<State>();\n\tfor (auto i = 0; i != rows; ++i) {\n\t\tconst auto big = (i < bigrows);\n\t\tconst auto cols = big ? bigrow : smallrow;\n\t\tauto &row = state->buttons.emplace_back();\n\t\trow.reserve(cols);\n\t\tfor (auto j = 0; j != cols; ++j) {\n\t\t\tconst auto index = i * bigrow + j - std::max(0, i - bigrows);\n\t\t\tconst auto nanoTon = presets[index];\n\t\t\tconst auto button = CreateChild<RoundButton>(\n\t\t\t\twrap,\n\t\t\t\trpl::single(FormatTonAmount(nanoTon).full + diamond),\n\t\t\t\tst::stakePresetButton);\n\t\t\tbutton->setTextTransform(RoundButtonTextTransform::ToUpper);\n\t\t\tbutton->setClickedCallback([=] {\n\t\t\t\tcallback(nanoTon);\n\t\t\t});\n\t\t\trow.push_back(button);\n\t\t}\n\t}\n\twrap->widthValue() | rpl::on_next([=](int width) {\n\t\tauto y = 0;\n\t\tconst auto xskip = st::stakePresetButtonSkip.x();\n\t\tconst auto yskip = st::stakePresetButtonSkip.y();\n\t\tfor (auto i = 0; i != state->buttons.size(); ++i) {\n\t\t\tconst auto &row = state->buttons[i];\n\t\t\tconst auto cols = int(row.size());\n\t\t\tconst auto singlew = (width - (cols - 1) * xskip) / cols;\n\t\t\tauto x = 0;\n\t\t\tfor (const auto &button : row) {\n\t\t\t\tbutton->setFullWidth(singlew);\n\t\t\t\tbutton->move(x, y);\n\t\t\t\tx += singlew + xskip;\n\t\t\t}\n\t\t\ty += st::stakePresetButton.height + yskip;\n\t\t}\n\t}, wrap->lifetime());\n}\n\nvoid EmojiGameStakeBox(\n\t\tnot_null<GenericBox*> box,\n\t\tEmojiGameStakeArgs &&args) {\n\tbox->setStyle(st::stakeBox);\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setNoContentMargin(true);\n\tconst auto container = box->verticalLayout();\n\n\tconst auto skip = st::confcallLinkHeaderIconPadding.bottom();\n\tbox->addRow(\n\t\tMakeLogo(box),\n\t\tst::boxRowPadding + QMargins(0, 0, 0, skip),\n\t\tstyle::al_top);\n\n\tauto helper = Text::CustomEmojiHelper();\n\tconst auto beta = helper.paletteDependent(\n\t\tText::CustomEmojiTextBadge(\n\t\t\ttr::lng_stake_game_beta(tr::now).toUpper(),\n\t\t\tst::customEmojiTextBadge));\n\tconst auto sixText = helper.image({\n\t\t.image = MakeEmojiFrame(6, st::emojiSize),\n\t\t.textColor = false,\n\t});\n\tauto title = tr::lng_stake_game_title(\n\t\ttr::marked\n\t) | rpl::map([=](TextWithEntities &&text) {\n\t\treturn text.append(' ').append(beta);\n\t});\n\tbox->addRow(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(title),\n\t\t\tst::boxTitle,\n\t\t\tst::defaultPopupMenu,\n\t\t\thelper.context()),\n\t\tst::boxRowPadding + st::confcallLinkTitlePadding,\n\t\tstyle::al_top);\n\tbox->addRow(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_stake_game_about(tr::rich),\n\t\t\tst::confcallLinkCenteredText),\n\t\tst::boxRowPadding + QMargins(0, 0, 0, skip),\n\t\tstyle::al_top\n\t)->setTryMakeSimilarLines(true);\n\n\tconst auto sub = st::boxRowPadding - st::defaultSubsectionTitlePadding;\n\tconst auto subtitlePadding = QMargins(sub.left(), 0, sub.right(), 0);\n\tAddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_stake_game_results(),\n\t\tsubtitlePadding);\n\n\tconst auto half = (st::defaultTable.border + 1) / 2;\n\tbox->addRow(\n\t\tMakeTable(box, args),\n\t\tst::boxRowPadding + QMargins(-half, 0, -half, skip / 2),\n\t\tstyle::al_top);\n\n\tconst auto sixContext = helper.context();\n\tconst auto factory = [=](\n\t\t\tQStringView data,\n\t\t\tconst Text::MarkedContext &context) {\n\t\tif (auto result = sixContext.customEmojiFactory(data, context)) {\n\t\t\treturn std::make_unique<Text::LimitedLoopsEmoji>(\n\t\t\t\tstd::move(result),\n\t\t\t\t0,\n\t\t\t\ttrue);\n\t\t}\n\t\treturn std::unique_ptr<Text::LimitedLoopsEmoji>();\n\t};\n\tconst auto context = Text::MarkedContext{\n\t\t.customEmojiFactory = factory,\n\t};\n\tconst auto resets = box->addRow(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_stake_game_resets(\n\t\t\t\tlt_emoji,\n\t\t\t\trpl::single(sixText),\n\t\t\t\ttr::rich),\n\t\t\tst::confcallLinkFooterOr,\n\t\t\tst::defaultPopupMenu,\n\t\t\tcontext),\n\t\tst::boxRowPadding,\n\t\tstyle::al_top);\n\tresets->setTryMakeSimilarLines(true);\n\tresets->setAnimationsPausedCallback([=] {\n\t\treturn FlatLabel::WhichAnimationsPaused::All;\n\t});\n\n\tAddSubsectionTitle(\n\t\tcontainer,\n\t\ttr::lng_stake_game_your(),\n\t\tsubtitlePadding + QMargins(0, skip, 0, sub.bottom()));\n\tconst auto field = AddTonInputField(container, {\n\t\t.value = args.currentStake,\n\t});\n\tbox->setFocusCallback([=] {\n\t\tfield->setFocusFast();\n\t});\n\n\tconst auto fromNanoTon = [](int64 nanoton) {\n\t\treturn CreditsAmount(\n\t\t\tnanoton / Ui::kNanosInOne,\n\t\t\tnanoton % Ui::kNanosInOne,\n\t\t\tCreditsType::Ton);\n\t};\n\tconst auto price = field->lifetime().make_state<\n\t\trpl::variable<CreditsAmount>\n\t>(rpl::single(\n\t\targs.currentStake\n\t) | rpl::then(field->changes() | rpl::map([=] {\n\t\treturn Ui::ParseTonAmountString(field->getLastText()).value_or(0);\n\t})) | rpl::map(fromNanoTon));\n\tAddApproximateUsd(\n\t\tfield,\n\t\targs.session,\n\t\tprice->value());\n\n\tAddStakePresets(container, args.session, [=](int64 nanoTon) {\n\t\tfield->setText(Ui::FormatTonAmount(nanoTon).full);\n\t});\n\n\tconst auto submit = args.submit;\n\tconst auto callback = [=] {\n\t\tconst auto text = field->getLastText();\n\t\tconst auto now = price->current();\n\t\tconst auto credits = &args.session->credits();\n\t\tconst auto appConfig = &args.session->appConfig();\n\t\tconst auto min = fromNanoTon(appConfig->stakeDiceNanoTonMin());\n\t\tconst auto max = fromNanoTon(appConfig->stakeDiceNanoTonMax());\n\t\tif (!now || now < min || now > max) {\n\t\t\tfield->showError();\n\t\t} else if (credits->tonBalance() < now) {\n\t\t\tbox->uiShow()->show(Box(InsufficientTonBox, args.session, now));\n\t\t} else {\n\t\t\tbox->closeBox();\n\t\t\tsubmit(now.whole() * Ui::kNanosInOne + now.nano());\n\t\t}\n\t};\n\tfield->submits() | rpl::on_next(callback, field->lifetime());\n\tconst auto button = box->addButton(rpl::single(QString()), callback);\n\n\tbutton->setText(tr::lng_stake_game_save_and_roll(\n\t) | rpl::map([=](QString text) {\n\t\treturn Text::IconEmoji(\n\t\t\t&st::stakeEmojiIcon,\n\t\t\tQString::fromUtf8(\"\\xf0\\x9f\\x8e\\xb2\")\n\t\t).append(' ').append(text);\n\t}));\n\n\tconst auto close = CreateChild<IconButton>(\n\t\tcontainer,\n\t\tst::boxTitleClose);\n\tclose->setClickedCallback([=] { box->closeBox(); });\n\tcontainer->widthValue() | rpl::on_next([=](int) {\n\t\tclose->moveToRight(0, 0);\n\t}, close->lifetime());\n\n\tconst auto session = args.session;\n\tsession->credits().tonLoad(true);\n\tconst auto balance = Settings::AddBalanceWidget(\n\t\tcontainer,\n\t\tsession,\n\t\tsession->credits().tonBalanceValue(),\n\t\tfalse);\n\trpl::combine(\n\t\tbalance->sizeValue(),\n\t\tcontainer->sizeValue()\n\t) | rpl::on_next([=](const QSize &, const QSize &) {\n\t\tbalance->moveToLeft(\n\t\t\tst::creditsHistoryRightSkip * 2,\n\t\t\tst::creditsHistoryRightSkip);\n\t\tbalance->update();\n\t}, balance->lifetime());\n}\n\nToast::Config MakeEmojiGameStakeToast(\n\t\tstd::shared_ptr<Show> show,\n\t\tEmojiGameStakeArgs &&args) {\n\tauto config = Toast::Config{\n\t\t.st = &st::historyDiceToast,\n\t\t.duration = Ui::Toast::kDefaultDuration * 2,\n\t};\n\tauto helper = Text::CustomEmojiHelper();\n\tstatic const auto makeBg = [] {\n\t\tauto result = st::mediaviewTextLinkFg->c;\n\t\tresult.setAlphaF(0.12);\n\t\treturn result;\n\t};\n\tstruct State {\n\t\tState() : bg(makeBg), badge(st::stakeChangeBadge) {\n\t\t\tbadge.textBg = badge.textBgOver = bg.color();\n\t\t}\n\n\t\tstyle::complex_color bg;\n\t\tstyle::RoundButton badge;\n\t};\n\tauto state = std::make_shared<State>();\n\tconst auto badge = helper.paletteDependent(\n\t\tUi::Text::CustomEmojiTextBadge(\n\t\t\ttr::lng_about_random_stake_change(tr::now),\n\t\t\tstate->badge,\n\t\t\tst::stateChangeBadgeMargin));\n\tconst auto diamond = QString::fromUtf8(\"\\xf0\\x9f\\x92\\x8e\");\n\tconfig.text.append(\n\t\ttr::lng_about_random_stake(\n\t\t\ttr::now,\n\t\t\tlt_amount,\n\t\t\ttr::bold(diamond + Ui::FormatTonAmount(args.currentStake).full),\n\t\t\ttr::marked)\n\t).append(' ').append(\n\t\ttr::link(badge, u\"internal:stake_change\"_q)\n\t).append(u\" \"_q.repeated(10)).append(tr::link(\n\t\ttr::semibold(tr::lng_about_random_send(tr::now)),\n\t\tu\"internal:stake_send\"_q));\n\tconfig.textContext = helper.context();\n\n\tconfig.filter = [=, state = std::move(state)](\n\t\t\tconst ClickHandlerPtr &handler,\n\t\t\tQt::MouseButton button) {\n\t\tif (button == Qt::LeftButton) {\n\t\t\tconst auto url = handler ? handler->url() : QString();\n\t\t\tif (url == u\"internal:stake_change\"_q) {\n\t\t\t\tshow->show(Ui::MakeEmojiGameStakeBox(base::duplicate(args)));\n\t\t\t} else if (url == u\"internal:stake_send\"_q) {\n\t\t\t\targs.submit(args.currentStake);\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\n\treturn config;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/emoji_stake_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui::Toast {\nstruct Config;\n} // namespace Ui::Toast\n\nnamespace Ui {\n\nclass Show;\nclass InputField;\nclass NumberInput;\nclass VerticalLayout;\n\nstruct StarsInputFieldArgs {\n\tstd::optional<int64> value;\n\tint64 max = 0;\n};\n[[nodiscard]] not_null<NumberInput*> AddStarsInputField(\n\tnot_null<VerticalLayout*> container,\n\tStarsInputFieldArgs &&args);\n\nstruct TonInputFieldArgs {\n\tint64 value = 0;\n};\n[[nodiscard]] not_null<InputField*> AddTonInputField(\n\tnot_null<VerticalLayout*> container,\n\tTonInputFieldArgs &&args);\n\nvoid AddApproximateUsd(\n\tnot_null<QWidget*> field,\n\tnot_null<Main::Session*> session,\n\trpl::producer<CreditsAmount> price);\n\nvoid InsufficientTonBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Main::Session*> session,\n\tCreditsAmount required);\n\nstruct EmojiGameStakeArgs {\n\tnot_null<Main::Session*> session;\n\tint64 currentStake = 0;\n\tstd::array<int, 6> milliRewards;\n\tint jackpotMilliReward = 0;\n\tFn<void(int64)> submit;\n};\nvoid EmojiGameStakeBox(not_null<GenericBox*> box, EmojiGameStakeArgs &&args);\n[[nodiscard]] inline object_ptr<GenericBox> MakeEmojiGameStakeBox(\n\t\tEmojiGameStakeArgs &&args) {\n\treturn Box(EmojiGameStakeBox, std::move(args));\n}\n\n[[nodiscard]] Toast::Config MakeEmojiGameStakeToast(\n\tstd::shared_ptr<Show> show,\n\tEmojiGameStakeArgs &&args);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/peer_qr_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/peer_qr_box.h\"\n\n#include \"core/application.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"info/channel_statistics/boosts/giveaway/boost_badge.h\" // InfiniteRadialAnimationWidget.\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"qr/qr_generate.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/box_content_divider.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/continuous_sliders.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_intro.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_window.h\"\n\n#include <QtCore/QMimeData>\n#include <QtGui/QGuiApplication>\n#include <QtSvg/QSvgRenderer>\n\nnamespace Ui {\nnamespace {\n\nusing Colors = std::vector<QColor>;\n\n[[nodiscard]] QMargins NoPhotoBackgroundMargins() {\n\treturn QMargins(\n\t\tst::profileQrBackgroundMargins.left(),\n\t\tst::profileQrBackgroundMargins.left(),\n\t\tst::profileQrBackgroundMargins.right(),\n\t\tst::profileQrBackgroundMargins.bottom());\n}\n\n[[nodiscard]] style::font CreateFont(int size, int scale) {\n\treturn style::font(\n\t\tstyle::ConvertScale(size, scale),\n\t\tst::profileQrFont->flags(),\n\t\tst::profileQrFont->family());\n}\n\n[[nodiscard]] QImage TelegramQr(\n\t\tconst Qr::Data &data,\n\t\tint pixel,\n\t\tint max,\n\t\tbool hasWhiteBackground) {\n\tExpects(data.size > 0);\n\n\tconstexpr auto kCenterRatio = 0.175;\n\n\tif (max > 0 && data.size * pixel > max) {\n\t\tpixel = std::max(max / data.size, 1);\n\t}\n\tauto qr = Qr::Generate(\n\t\tdata,\n\t\tpixel * style::DevicePixelRatio(),\n\t\thasWhiteBackground ? Qt::transparent : Qt::black,\n\t\thasWhiteBackground ? Qt::white : Qt::transparent);\n\t{\n\t\tauto p = QPainter(&qr);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tauto svg = QSvgRenderer(u\":/gui/plane_white.svg\"_q);\n\t\tconst auto size = qr.rect().size();\n\t\tconst auto centerRect = Rect(size)\n\t\t\t- Margins((size.width() - (size.width() * kCenterRatio)) / 2);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(Qt::white);\n\t\tif (hasWhiteBackground) {\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_Clear);\n\t\t\tp.drawEllipse(centerRect);\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\t\tsvg.render(&p, centerRect);\n\t\t} else {\n\t\t\tp.drawEllipse(centerRect);\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_Clear);\n\t\t\tsvg.render(&p, centerRect);\n\t\t}\n\t}\n\treturn qr;\n}\n\n[[nodiscard]] QMargins RoundedMargins(\n\t\tconst QMargins &backgroundMargins,\n\t\tint photoSize,\n\t\tint textMaxHeight) {\n\treturn (textMaxHeight\n\t\t? (backgroundMargins + QMargins(0, photoSize / 2, 0, textMaxHeight))\n\t\t: photoSize\n\t\t? backgroundMargins + QMargins(0, photoSize / 2, 0, photoSize / 2)\n\t\t: Margins(backgroundMargins.left()));\n}\n\nvoid Paint(\n\t\tQPainter &p,\n\t\tconst style::font &font,\n\t\tconst QString &text,\n\t\tconst Colors &backgroundColors,\n\t\tconst QMargins &backgroundMargins,\n\t\tconst QImage &qrImage,\n\t\tconst QRect &qrRect,\n\t\tint qrMaxSize,\n\t\tint qrPixel,\n\t\tint radius,\n\t\tint textMaxHeight,\n\t\tint photoSize,\n\t\tbool hasWhiteBackground) {\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(hasWhiteBackground ? Qt::white : Qt::transparent);\n\tconst auto roundedRect = qrRect\n\t\t+ RoundedMargins(backgroundMargins, photoSize, textMaxHeight);\n\tp.drawRoundedRect(roundedRect, radius, radius);\n\tif (!qrImage.isNull() && !backgroundColors.empty()) {\n\t\tconstexpr auto kDuration = crl::time(10000);\n\t\tconst auto angle = (crl::now() % kDuration)\n\t\t\t/ float64(kDuration) * 360.0;\n\t\tconst auto gradientRotation = int(angle / 45.) * 45;\n\t\tconst auto gradientRotationAdd = angle - gradientRotation;\n\n\t\tconst auto textAdditionalWidth = backgroundMargins.left();\n\t\tauto back = Images::GenerateGradient(\n\t\t\tqrRect.size() + QSize(textAdditionalWidth, 0),\n\t\t\tbackgroundColors,\n\t\t\tgradientRotation,\n\t\t\t1. - (gradientRotationAdd / 45.));\n\t\tif (hasWhiteBackground) {\n\t\t\tp.drawImage(qrRect, back);\n\t\t}\n\t\tconst auto coloredSize = QSize(back.width(), textMaxHeight);\n\t\tauto colored = QImage(\n\t\t\tcoloredSize * style::DevicePixelRatio(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tcolored.setDevicePixelRatio(style::DevicePixelRatio());\n\t\tcolored.fill(Qt::transparent);\n\t\tif (textMaxHeight) {\n\t\t\t// '@' + QString(32, 'W');\n\t\t\tauto p = QPainter(&colored);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::black);\n\t\t\tp.setFont(font);\n\t\t\tauto option = QTextOption(style::al_center);\n\t\t\toption.setWrapMode(QTextOption::WrapAnywhere);\n\t\t\tp.drawText(Rect(coloredSize), text, option);\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_SourceIn);\n\t\t\tp.drawImage(0, -back.height() + textMaxHeight, back);\n\t\t}\n\t\tif (!hasWhiteBackground) {\n\t\t\tauto copy = qrImage;\n\t\t\t{\n\t\t\t\tauto p = QPainter(&copy);\n\t\t\t\tp.setCompositionMode(QPainter::CompositionMode_SourceIn);\n\t\t\t\tp.drawImage(Rect(copy.size()), back);\n\t\t\t}\n\t\t\tp.drawImage(qrRect, copy);\n\t\t} else {\n\t\t\tp.drawImage(qrRect, qrImage);\n\t\t}\n\t\tif (textMaxHeight) {\n\t\t\tp.drawImage(\n\t\t\t\tqrRect.x() - textAdditionalWidth / 2,\n\t\t\t\trect::bottom(qrRect)\n\t\t\t\t\t+ ((rect::bottom(roundedRect) - rect::bottom(qrRect))\n\t\t\t\t\t\t- textMaxHeight) / 2,\n\t\t\t\tcolored);\n\t\t}\n\t}\n}\n\nnot_null<Ui::RpWidget*> PrepareQrWidget(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Ui::RpWidget*> topWidget,\n\t\trpl::producer<int> fontSizeValue,\n\t\trpl::producer<bool> userpicToggled,\n\t\trpl::producer<bool> backgroundToggled,\n\t\trpl::producer<QString> username,\n\t\trpl::producer<QString> links,\n\t\trpl::producer<Colors> bgs,\n\t\trpl::producer<QString> about) {\n\tconst auto divider = container->add(\n\t\tobject_ptr<Ui::BoxContentDivider>(container));\n\tstruct State final {\n\t\texplicit State(Fn<void()> callback) : updating(callback) {\n\t\t\tupdating.start();\n\t\t}\n\n\t\tUi::Animations::Basic updating;\n\t\tstyle::font font;\n\t\tQImage qrImage;\n\t\tColors backgroundColors;\n\t\tQString text;\n\t\tQMargins backgroundMargins;\n\t\tint textWidth = 0;\n\t\tint textMaxHeight = 0;\n\t\tint photoSize = 0;\n\t\tbool backgroundToggled = false;\n\t};\n\tconst auto result = Ui::CreateChild<Ui::RpWidget>(divider);\n\ttopWidget->setParent(result);\n\ttopWidget->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tconst auto state = result->lifetime().make_state<State>(\n\t\t[=] { result->update(); });\n\tconst auto qrMaxSize = st::boxWideWidth\n\t\t- rect::m::sum::h(st::boxRowPadding)\n\t\t- rect::m::sum::h(st::profileQrBackgroundMargins);\n\tconst auto aboutLabel = Ui::CreateChild<Ui::FlatLabel>(\n\t\tdivider,\n\t\tst::creditsBoxAboutDivider);\n\trpl::combine(\n\t\tstd::move(fontSizeValue),\n\t\tstd::move(userpicToggled),\n\t\tstd::move(backgroundToggled),\n\t\tstd::move(username),\n\t\tstd::move(bgs),\n\t\tstd::move(links),\n\t\tstd::move(about),\n\t\trpl::single(rpl::empty) | rpl::then(style::PaletteChanged())\n\t) | rpl::on_next([=](\n\t\t\tint fontSize,\n\t\t\tbool userpicToggled,\n\t\t\tbool backgroundToggled,\n\t\t\tconst QString &username,\n\t\t\tconst Colors &backgroundColors,\n\t\t\tconst QString &link,\n\t\t\tconst QString &about,\n\t\t\tconst auto &) {\n\t\tstate->font = CreateFont(fontSize, style::Scale());\n\t\tstate->backgroundToggled = backgroundToggled;\n\t\tstate->backgroundMargins = userpicToggled\n\t\t\t? st::profileQrBackgroundMargins\n\t\t\t: NoPhotoBackgroundMargins();\n\t\tstate->photoSize = userpicToggled\n\t\t\t? st::defaultUserpicButton.photoSize\n\t\t\t: 0;\n\t\tstate->backgroundColors = backgroundColors;\n\t\tstate->text = username.toUpper();\n\t\tstate->textWidth = state->font->width(state->text);\n\t\tif (!link.isEmpty()) {\n\t\t\tconst auto remainder = qrMaxSize % st::introQrPixel;\n\t\t\tconst auto downTo = remainder\n\t\t\t\t? qrMaxSize - remainder\n\t\t\t\t: qrMaxSize;\n\t\t\tstate->qrImage = TelegramQr(\n\t\t\t\tQr::Encode(link.toUtf8(), Qr::Redundancy::Default),\n\t\t\t\tst::introQrPixel,\n\t\t\t\tdownTo,\n\t\t\t\tbackgroundToggled).scaled(\n\t\t\t\t\tSize(qrMaxSize * style::DevicePixelRatio()),\n\t\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\t\tQt::SmoothTransformation);\n\t\t} else {\n\t\t\tauto image = QImage(\n\t\t\t\tSize(qrMaxSize * style::DevicePixelRatio()),\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\timage.fill(Qt::white);\n\t\t\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\tstate->qrImage = std::move(image);\n\t\t}\n\t\tconst auto resultWidth = qrMaxSize\n\t\t\t+ rect::m::sum::h(state->backgroundMargins);\n\t\t{\n\t\t\taboutLabel->setText(about);\n\t\t\taboutLabel->resizeToWidth(resultWidth);\n\t\t}\n\t\tconst auto textMaxWidth = state->backgroundMargins.left()\n\t\t\t+ (state->qrImage.width() / style::DevicePixelRatio());\n\t\tconst auto lines = int(state->textWidth / textMaxWidth) + 1;\n\t\tstate->textMaxHeight = state->textWidth\n\t\t\t? (state->font->height * lines)\n\t\t\t: 0;\n\t\tconst auto whiteMargins = RoundedMargins(\n\t\t\tstate->backgroundMargins,\n\t\t\tstate->photoSize,\n\t\t\tstate->textMaxHeight);\n\t\tresult->resize(\n\t\t\tqrMaxSize + rect::m::sum::h(whiteMargins),\n\t\t\tqrMaxSize\n\t\t\t\t+ rect::m::sum::v(whiteMargins) // White.\n\t\t\t\t+ rect::m::sum::v(st::profileQrBackgroundPadding) // Gray.\n\t\t\t\t+ state->photoSize / 2\n\t\t\t\t+ aboutLabel->height());\n\n\t\tdivider->resize(container->width(), result->height());\n\t\tresult->moveToLeft((container->width() - result->width()) / 2, 0);\n\t\ttopWidget->setVisible(userpicToggled);\n\t\ttopWidget->moveToLeft(0, std::numeric_limits<int>::min());\n\t\ttopWidget->raise();\n\n\t\taboutLabel->raise();\n\t\taboutLabel->moveToLeft(\n\t\t\tresult->x(),\n\t\t\tdivider->height()\n\t\t\t\t- aboutLabel->height()\n\t\t\t\t- st::defaultBoxDividerLabelPadding.top());\n\t}, container->lifetime());\n\tresult->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(result);\n\t\tconst auto size = (state->qrImage.size() / style::DevicePixelRatio());\n\t\tconst auto qrRect = Rect(\n\t\t\t(result->width() - size.width()) / 2,\n\t\t\tstate->backgroundMargins.top() + state->photoSize / 2,\n\t\t\tsize);\n\t\tp.translate(\n\t\t\t0,\n\t\t\tst::profileQrBackgroundPadding.top() + state->photoSize / 2);\n\t\tPaint(\n\t\t\tp,\n\t\t\tstate->font,\n\t\t\tstate->text,\n\t\t\tstate->backgroundColors,\n\t\t\tstate->backgroundMargins,\n\t\t\tstate->qrImage,\n\t\t\tqrRect,\n\t\t\tqrMaxSize,\n\t\t\tst::introQrPixel,\n\t\t\tst::profileQrBackgroundRadius,\n\t\t\tstate->textMaxHeight,\n\t\t\tstate->photoSize,\n\t\t\tstate->backgroundToggled);\n\t\tif (!state->photoSize) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto photoSize = state->photoSize;\n\t\tconst auto top = Ui::GrabWidget(\n\t\t\ttopWidget,\n\t\t\tQRect(),\n\t\t\tQt::transparent).scaled(\n\t\t\t\tSize(photoSize * style::DevicePixelRatio()),\n\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\tQt::SmoothTransformation);\n\t\tp.drawPixmap((result->width() - photoSize) / 2, -photoSize / 2, top);\n\t}, result->lifetime());\n\treturn result;\n}\n\n[[nodiscard]] Fn<void(int)> AddDotsToSlider(\n\t\tnot_null<Ui::ContinuousSlider*> slider,\n\t\tconst style::MediaSlider &st,\n\t\tint count) {\n\tconst auto lineWidth = st::lineWidth;\n\tconst auto smallSize = Size(st.seekSize.height() - st.width);\n\tauto smallDots = std::vector<not_null<Ui::RpWidget*>>();\n\tsmallDots.reserve(count - 1);\n\tconst auto paintSmall = [=](QPainter &p, const QBrush &brush) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tauto pen = st::boxBg->p;\n\t\tpen.setWidth(st.width);\n\t\tp.setPen(pen);\n\t\tp.setBrush(brush);\n\t\tp.drawEllipse(Rect(smallSize) - Margins(lineWidth));\n\t};\n\tfor (auto i = 0; i < count - 1; i++) {\n\t\tsmallDots.push_back(\n\t\t\tUi::CreateChild<Ui::RpWidget>(slider->parentWidget()));\n\t\tconst auto dot = smallDots.back();\n\t\tdot->resize(smallSize);\n\t\tdot->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tdot->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(dot);\n\t\t\tconst auto fg = (slider->value() > (i / float64(count - 1)))\n\t\t\t\t? st.activeFg\n\t\t\t\t: st.inactiveFg;\n\t\t\tpaintSmall(p, fg);\n\t\t}, dot->lifetime());\n\t}\n\tconst auto bigDot = Ui::CreateChild<Ui::RpWidget>(slider->parentWidget());\n\tbigDot->resize(st.seekSize);\n\tbigDot->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tbigDot->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(bigDot);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tauto pen = st::boxBg->p;\n\t\tpen.setWidth(st.width);\n\t\tp.setPen(pen);\n\t\tp.setBrush(st.activeFg);\n\t\tp.drawEllipse(Rect(st.seekSize) - Margins(lineWidth));\n\t}, bigDot->lifetime());\n\n\treturn [=](int index) {\n\t\tconst auto g = slider->geometry();\n\t\tconst auto bigTop = g.y() + (g.height() - bigDot->height()) / 2;\n\t\tconst auto smallTop = g.y()\n\t\t\t+ (g.height() - smallSize.height()) / 2;\n\t\tfor (auto i = 0; i < count; ++i) {\n\t\t\tif (index == i) {\n\t\t\t\tconst auto x = ((g.width() - bigDot->width()) * i)\n\t\t\t\t\t/ float64(count - 1);\n\t\t\t\tbigDot->move(g.x() + std::round(x), bigTop);\n\t\t\t} else {\n\t\t\t\tconst auto k = (i < index) ? i : i - 1;\n\t\t\t\tconst auto w = smallDots[k]->width();\n\t\t\t\tsmallDots[k]->move(\n\t\t\t\t\tg.x() + ((g.width() - w) * i) / (count - 1),\n\t\t\t\t\tsmallTop);\n\t\t\t}\n\t\t}\n\t};\n}\n\n} // namespace\n\nvoid FillPeerQrBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tPeerData *peer,\n\t\tstd::optional<QString> customLink,\n\t\trpl::producer<QString> about) {\n\tconst auto window = Core::App().findWindow(box);\n\tconst auto controller = window ? window->sessionController() : nullptr;\n\tif (!controller) {\n\t\treturn;\n\t}\n\tbox->setStyle(st::giveawayGiftCodeBox);\n\tbox->setNoContentMargin(true);\n\tbox->setWidth(st::aboutWidth);\n\tbox->setTitle(tr::lng_group_invite_context_qr());\n\tbox->verticalLayout()->resizeToWidth(box->width());\n\tstruct State {\n\t\tUi::RpWidget* saveButton = nullptr;\n\t\trpl::variable<bool> saveButtonBusy = false;\n\t\trpl::variable<bool> userpicToggled = true;\n\t\trpl::variable<bool> backgroundToggled = true;\n\t\trpl::variable<Colors> bgs;\n\t\tUi::Animations::Simple animation;\n\t\trpl::variable<int> chosen = 0;\n\t\trpl::variable<int> scaleValue = 0;\n\t\trpl::variable<int> fontSizeValue = 28;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tstate->userpicToggled = !(customLink || !peer);\n\n\tconst auto usernameValue = [=] {\n\t\treturn (customLink || !peer)\n\t\t\t? (rpl::single(QString()) | rpl::type_erased)\n\t\t\t: Info::Profile::UsernameValue(peer, true) | rpl::map(\n\t\t\t\t[](const auto &username) { return username.text; });\n\t};\n\tconst auto linkValue = [=] {\n\t\treturn customLink\n\t\t\t? rpl::single(*customLink)\n\t\t\t: peer\n\t\t\t? Info::Profile::LinkValue(peer, true) | rpl::map(\n\t\t\t\t[](const auto &link) { return link.text; })\n\t\t\t: (rpl::single(QString()) | rpl::type_erased);\n\t};\n\n\tconst auto userpic = Ui::CreateChild<Ui::RpWidget>(box);\n\tconst auto userpicSize = st::defaultUserpicButton.photoSize;\n\tuserpic->resize(Size(userpicSize));\n\tconst auto userpicMedia = Ui::MakeUserpicThumbnail(peer\n\t\t? peer\n\t\t: controller->session().user().get());\n\tuserpicMedia->subscribeToUpdates(\n\t\tcrl::guard(userpic, [=] { userpic->update(); }));\n\tuserpic->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(userpic);\n\t\tp.drawImage(0, 0, userpicMedia->image(userpicSize));\n\t}, userpic->lifetime());\n\n\tlinkValue() | rpl::on_next([=](const QString &link) {\n\t\tif (link.isEmpty()) {\n\t\t\tbox->showFinishes() | rpl::on_next([=] {\n\t\t\t\tbox->closeBox();\n\t\t\t}, box->lifetime());\n\t\t\tbox->closeBox();\n\t\t}\n\t}, box->lifetime());\n\n\tuserpic->setVisible(peer != nullptr);\n\tPrepareQrWidget(\n\t\tbox->verticalLayout(),\n\t\tuserpic,\n\t\tstate->fontSizeValue.value(),\n\t\tstate->userpicToggled.value(),\n\t\tstate->backgroundToggled.value(),\n\t\tusernameValue(),\n\t\tlinkValue(),\n\t\tstate->bgs.value(),\n\t\tabout ? std::move(about) : rpl::single(QString()));\n\n\tUi::AddSkip(box->verticalLayout());\n\tUi::AddSubsectionTitle(\n\t\tbox->verticalLayout(),\n\t\ttr::lng_userpic_builder_color_subtitle());\n\n\tconst auto themesContainer = box->addRow(\n\t\tobject_ptr<Ui::VerticalLayout>(box));\n\n\tconst auto activewidth = int(\n\t\t(st::defaultInputField.borderActive + st::lineWidth) * 0.9);\n\tconst auto size = st::chatThemePreviewSize.width();\n\n\tconst auto fill = [=](const std::vector<Data::CloudTheme> &cloudThemes) {\n\t\twhile (themesContainer->count()) {\n\t\t\tdelete themesContainer->widgetAt(0);\n\t\t}\n\n\t\tstruct State {\n\t\t\tColors colors;\n\t\t\tQImage image;\n\t\t};\n\t\tconstexpr auto kMaxInRow = 4;\n\t\tconstexpr auto kMaxColors = 4;\n\t\tauto row = (Ui::RpWidget*)(nullptr);\n\t\tauto counter = 0;\n\t\tconst auto spacing = (0\n\t\t\t+ (box->width() - rect::m::sum::h(st::boxRowPadding))\n\t\t\t- (kMaxInRow * size)) / (kMaxInRow + 1);\n\n\t\tauto colorsCollection = ranges::views::all(\n\t\t\tcloudThemes\n\t\t) | ranges::views::transform([](const auto &cloudTheme) -> Colors {\n\t\t\tconst auto it = cloudTheme.settings.find(\n\t\t\t\tData::CloudThemeType::Light);\n\t\t\tif (it == end(cloudTheme.settings)) {\n\t\t\t\treturn Colors();\n\t\t\t}\n\t\t\tconst auto colors = it->second.paper\n\t\t\t\t? it->second.paper->backgroundColors()\n\t\t\t\t: Colors();\n\t\t\tif (colors.size() != kMaxColors) {\n\t\t\t\treturn Colors();\n\t\t\t}\n\t\t\treturn colors;\n\t\t}) | ranges::views::filter([](const Colors &colors) {\n\t\t\treturn !colors.empty();\n\t\t}) | ranges::to_vector;\n\t\tExpects(!colorsCollection.empty());\n\t\tcolorsCollection[0] = Colors{\n\t\t\tst::premiumButtonBg1->c,\n\t\t\tst::premiumButtonBg1->c,\n\t\t\tst::premiumButtonBg2->c,\n\t\t\tst::premiumButtonBg3->c,\n\t\t};\n\t\t// colorsCollection.push_back(Colors{\n\t\t// \tst::creditsBg1->c,\n\t\t// \tst::creditsBg2->c,\n\t\t// \tst::creditsBg1->c,\n\t\t// \tst::creditsBg2->c,\n\t\t// });\n\n\t\tfor (const auto &colors : colorsCollection) {\n\t\t\tif (state->bgs.current().empty()) {\n\t\t\t\tstate->bgs = colors;\n\t\t\t}\n\n\t\t\tif (counter % kMaxInRow == 0) {\n\t\t\t\tUi::AddSkip(themesContainer);\n\t\t\t\trow = themesContainer->add(\n\t\t\t\t\tobject_ptr<Ui::RpWidget>(themesContainer));\n\t\t\t\trow->resize(size, size);\n\t\t\t}\n\t\t\tconst auto widget = Ui::CreateChild<Ui::AbstractButton>(row);\n\t\t\twidget->setClickedCallback([=] {\n\t\t\t\tstate->chosen = counter;\n\t\t\t\twidget->update();\n\t\t\t\tstate->animation.stop();\n\t\t\t\tstate->animation.start([=](float64 value) {\n\t\t\t\t\tconst auto was = state->bgs.current();\n\t\t\t\t\tconst auto &now = colors;\n\t\t\t\t\tif (was.size() == now.size()\n\t\t\t\t\t\t&& was.size() == kMaxColors) {\n\t\t\t\t\t\tstate->bgs = Colors({\n\t\t\t\t\t\t\tanim::color(was[0], now[0], value),\n\t\t\t\t\t\t\tanim::color(was[1], now[1], value),\n\t\t\t\t\t\t\tanim::color(was[2], now[2], value),\n\t\t\t\t\t\t\tanim::color(was[3], now[3], value),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t0.,\n\t\t\t\t1.,\n\t\t\t\tst::shakeDuration);\n\t\t\t});\n\t\t\tstate->chosen.value() | rpl::combine_previous(\n\t\t\t) | rpl::filter([=](int i, int k) {\n\t\t\t\treturn i == counter || k == counter;\n\t\t\t}) | rpl::on_next([=] {\n\t\t\t\twidget->update();\n\t\t\t}, widget->lifetime());\n\t\t\twidget->resize(size, size);\n\t\t\twidget->moveToLeft(\n\t\t\t\tspacing + ((counter % kMaxInRow) * (size + spacing)),\n\t\t\t\t0);\n\t\t\twidget->show();\n\n\t\t\tconst auto cornersMask = Images::CornersMask(\n\t\t\t\tst::roundRadiusLarge * style::DevicePixelRatio());\n\t\t\tconst auto back = [&] {\n\t\t\t\tauto gradient = Images::GenerateGradient(\n\t\t\t\t\tSize(size - activewidth * 5) * style::DevicePixelRatio(),\n\t\t\t\t\tcolors,\n\t\t\t\t\t0,\n\t\t\t\t\t0);\n\t\t\t\tgradient.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\t\tauto result = Images::Round(std::move(gradient), cornersMask);\n\t\t\t\tconst auto rect = Rect(\n\t\t\t\t\tresult.size() / style::DevicePixelRatio());\n\t\t\t\tauto colored = result;\n\t\t\t\tcolored.fill(Qt::transparent);\n\t\t\t\t{\n\t\t\t\t\tauto p = QPainter(&colored);\n\t\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\t\tst::profileQrIcon.paintInCenter(p, rect);\n\t\t\t\t\tp.setCompositionMode(QPainter::CompositionMode_SourceIn);\n\t\t\t\t\tp.drawImage(0, 0, result);\n\t\t\t\t}\n\t\t\t\tauto temp = result;\n\t\t\t\ttemp.fill(Qt::transparent);\n\t\t\t\t{\n\t\t\t\t\tauto p = QPainter(&temp);\n\t\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\t\tp.setPen(st::premiumButtonFg);\n\t\t\t\t\tp.setBrush(st::premiumButtonFg);\n\t\t\t\t\tconst auto size = st::profileQrIcon.width() * 1.5;\n\t\t\t\t\tconst auto margins = Margins((rect.width() - size) / 2);\n\t\t\t\t\tconst auto inner = rect - margins;\n\t\t\t\t\tp.drawRoundedRect(\n\t\t\t\t\t\tinner,\n\t\t\t\t\t\tst::roundRadiusLarge,\n\t\t\t\t\t\tst::roundRadiusLarge);\n\t\t\t\t\tp.drawImage(0, 0, colored);\n\t\t\t\t}\n\t\t\t\t{\n\t\t\t\t\tauto p = QPainter(&result);\n\t\t\t\t\tp.drawImage(0, 0, temp);\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t}();\n\t\t\twidget->paintRequest() | rpl::on_next([=] {\n\t\t\t\tauto p = QPainter(widget);\n\t\t\t\tconst auto rect = widget->rect() - Margins(activewidth * 2.5);\n\t\t\t\tp.drawImage(rect.x(), rect.y(), back);\n\t\t\t\tif (state->chosen.current() == counter) {\n\t\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\t\tauto pen = st::activeLineFg->p;\n\t\t\t\t\tpen.setWidth(st::defaultInputField.borderActive);\n\t\t\t\t\tp.setPen(pen);\n\t\t\t\t\tconst auto r = st::roundRadiusLarge\n\t\t\t\t\t\t+ activewidth * 2.1 * style::DevicePixelRatio();\n\t\t\t\t\tp.drawRoundedRect(\n\t\t\t\t\t\twidget->rect() - Margins(pen.width()),\n\t\t\t\t\t\tr,\n\t\t\t\t\t\tr);\n\t\t\t\t}\n\t\t\t}, widget->lifetime());\n\t\t\tcounter++;\n\t\t}\n\t\tUi::AddSkip(themesContainer);\n\t\tUi::AddSkip(themesContainer);\n\t\tthemesContainer->resizeToWidth(box->width());\n\t};\n\n\tconst auto themes = &controller->session().data().cloudThemes();\n\tconst auto &list = themes->chatThemes();\n\tif (!list.empty()) {\n\t\tfill(list);\n\t} else {\n\t\tthemes->refreshChatThemes();\n\t\tthemes->chatThemesUpdated(\n\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\tfill(themes->chatThemes());\n\t\t}, box->lifetime());\n\t}\n\n\tUi::AddSkip(box->verticalLayout());\n\tUi::AddDivider(box->verticalLayout());\n\tUi::AddSkip(box->verticalLayout());\n\tUi::AddSubsectionTitle(\n\t\tbox->verticalLayout(),\n\t\ttr::lng_qr_box_quality());\n\tUi::AddSkip(box->verticalLayout());\n\tconstexpr auto kMaxQualities = 3;\n\t{\n\t\tconst auto seekSize = st::settingsScale.seekSize.height();\n\t\tconst auto &labelSt = st::defaultFlatLabel;\n\t\tconst auto labels = box->verticalLayout()->add(\n\t\t\tUi::CreateSkipWidget(\n\t\t\t\tbox,\n\t\t\t\tlabelSt.style.font->height + labelSt.style.font->descent),\n\t\t\tst::boxRowPadding);\n\t\tconst auto left = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tlabels,\n\t\t\ttr::lng_qr_box_quality1(),\n\t\t\tlabelSt);\n\t\tconst auto middle = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tlabels,\n\t\t\ttr::lng_qr_box_quality2(),\n\t\t\tlabelSt);\n\t\tconst auto right = Ui::CreateChild<Ui::FlatLabel>(\n\t\t\tlabels,\n\t\t\ttr::lng_qr_box_quality3(),\n\t\t\tlabelSt);\n\t\tlabels->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &size) {\n\t\t\tleft->moveToLeft(0, 0);\n\t\t\tmiddle->moveToLeft((size.width() - middle->width()) / 2, 0);\n\t\t\tright->moveToRight(0, 0);\n\t\t}, labels->lifetime());\n\n\t\tconst auto slider = box->verticalLayout()->add(\n\t\t\tobject_ptr<Ui::MediaSliderWheelless>(\n\t\t\t\tbox->verticalLayout(),\n\t\t\t\tst::settingsScale),\n\t\t\tst::boxRowPadding);\n\t\tslider->resize(slider->width(), seekSize);\n\t\tconst auto active = st::windowActiveTextFg->c;\n\t\tconst auto inactive = st::windowSubTextFg->c;\n\t\tconst auto colorize = [=](int index) {\n\t\t\tif (index == 0) {\n\t\t\t\tleft->setTextColorOverride(active);\n\t\t\t\tmiddle->setTextColorOverride(inactive);\n\t\t\t\tright->setTextColorOverride(inactive);\n\t\t\t} else if (index == 1) {\n\t\t\t\tleft->setTextColorOverride(inactive);\n\t\t\t\tmiddle->setTextColorOverride(active);\n\t\t\t\tright->setTextColorOverride(inactive);\n\t\t\t} else if (index == 2) {\n\t\t\t\tleft->setTextColorOverride(inactive);\n\t\t\t\tmiddle->setTextColorOverride(inactive);\n\t\t\t\tright->setTextColorOverride(active);\n\t\t\t}\n\t\t};\n\t\tconst auto updateGeometry = AddDotsToSlider(\n\t\t\tslider,\n\t\t\tst::settingsScale,\n\t\t\tkMaxQualities);\n\t\tslider->geometryValue(\n\t\t) | rpl::on_next([=](const QRect &rect) {\n\t\t\tupdateGeometry(int(slider->value() * (kMaxQualities - 1)));\n\t\t}, box->lifetime());\n\n\t\tbox->setShowFinishedCallback([=] {\n\t\t\tcolorize(0);\n\t\t\tupdateGeometry(0);\n\t\t});\n\t\tslider->setPseudoDiscrete(\n\t\t\tkMaxQualities,\n\t\t\t[=](int index) { return index; },\n\t\t\t0,\n\t\t\t[=](int scale) {\n\t\t\t\tstate->scaleValue = scale;\n\t\t\t\tcolorize(scale);\n\t\t\t\tupdateGeometry(scale);\n\t\t\t},\n\t\t\t[](int) {});\n\t}\n\t{\n\t\tUi::AddSkip(box->verticalLayout());\n\t\tUi::AddSkip(box->verticalLayout());\n\t\tUi::AddSubsectionTitle(\n\t\t\tbox->verticalLayout(),\n\t\t\ttr::lng_qr_box_font_size());\n\t\tUi::AddSkip(box->verticalLayout());\n\t\tconst auto seekSize = st::settingsScale.seekSize.height();\n\n\t\tconst auto slider = box->verticalLayout()->add(\n\t\t\tobject_ptr<Ui::MediaSliderWheelless>(\n\t\t\t\tbox->verticalLayout(),\n\t\t\t\tst::settingsScale),\n\t\t\tst::boxRowPadding);\n\t\tslider->resize(slider->width(), seekSize);\n\t\tconst auto kSizeAmount = 8;\n\t\tconst auto kMinSize = 20;\n\t\tconst auto kMaxSize = 36;\n\t\tconst auto kStep = (kMaxSize - kMinSize) / (kSizeAmount - 1);\n\t\tconst auto updateGeometry = AddDotsToSlider(\n\t\t\tslider,\n\t\t\tst::settingsScale,\n\t\t\tkSizeAmount);\n\t\tconst auto fontSizeToIndex = [=](int fontSize) {\n\t\t\treturn (fontSize - kMinSize) / kStep;\n\t\t};\n\t\tconst auto indexToFontSize = [=](int index) {\n\t\t\treturn kMinSize + index * kStep;\n\t\t};\n\t\tslider->geometryValue(\n\t\t) | rpl::on_next([=](const QRect &rect) {\n\t\t\tupdateGeometry(fontSizeToIndex(state->fontSizeValue.current()));\n\t\t}, box->lifetime());\n\n\t\tbox->setShowFinishedCallback([=] {\n\t\t\tupdateGeometry(fontSizeToIndex(state->fontSizeValue.current()));\n\t\t});\n\t\tslider->setPseudoDiscrete(\n\t\t\tkSizeAmount,\n\t\t\t[=](int index) { return indexToFontSize(index); },\n\t\t\tstate->fontSizeValue.current(),\n\t\t\t[=](int fontSize) {\n\t\t\t\tstate->fontSizeValue = fontSize;\n\t\t\t\tupdateGeometry(fontSizeToIndex(fontSize));\n\t\t\t},\n\t\t\t[](int) {});\n\t}\n\tUi::AddSkip(box->verticalLayout());\n\tUi::AddSkip(box->verticalLayout());\n\tif (peer) {\n\t\tconst auto userpicToggle = box->verticalLayout()->add(\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tbox->verticalLayout(),\n\t\t\t\t(peer->isUser()\n\t\t\t\t\t? tr::lng_mediaview_profile_photo\n\t\t\t\t\t: (peer->isChannel() && !peer->isMegagroup())\n\t\t\t\t\t? tr::lng_mediaview_channel_photo\n\t\t\t\t\t: tr::lng_mediaview_group_photo)(),\n\t\t\t\tst::settingsButtonNoIcon));\n\t\tuserpicToggle->toggleOn(state->userpicToggled.value(), true);\n\t\tuserpicToggle->setClickedCallback([=] {\n\t\t\tstate->userpicToggled = !state->userpicToggled.current();\n\t\t});\n\t}\n\t{\n\t\tconst auto backgroundToggle = box->verticalLayout()->add(\n\t\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\t\tbox->verticalLayout(),\n\t\t\t\ttr::lng_qr_box_transparent_background(),\n\t\t\t\tst::settingsButtonNoIcon));\n\t\tbackgroundToggle->toggleOn(\n\t\t\tstate->backgroundToggled.value() | rpl::map(!rpl::mappers::_1),\n\t\t\ttrue);\n\t\tbackgroundToggle->setClickedCallback([=] {\n\t\t\tstate->backgroundToggled = !state->backgroundToggled.current();\n\t\t});\n\t}\n\tUi::AddSkip(box->verticalLayout());\n\tUi::AddSkip(box->verticalLayout());\n\n\tauto buttonText = rpl::conditional(\n\t\tstate->saveButtonBusy.value() | rpl::map(rpl::mappers::_1),\n\t\trpl::single(QString()),\n\t\ttr::lng_chat_link_copy());\n\tconst auto show = controller->uiShow();\n\tstate->saveButton = box->addButton(std::move(buttonText), [=] {\n\t\tif (state->saveButtonBusy.current()) {\n\t\t\treturn;\n\t\t}\n\t\tstate->saveButtonBusy = true;\n\t\tconst auto userpicToggled = state->userpicToggled.current();\n\t\tconst auto backgroundToggled = state->backgroundToggled.current();\n\t\tconst auto scale = style::kScaleDefault\n\t\t\t* (kMaxQualities + int(state->scaleValue.current() * 2));\n\t\tconst auto divider = std::max(100, style::Scale())\n\t\t\t/ style::kScaleDefault;\n\t\tconst auto profileQrBackgroundRadius = style::ConvertScale(\n\t\t\tst::profileQrBackgroundRadius / divider,\n\t\t\tscale);\n\t\tconst auto introQrPixel = style::ConvertScale(\n\t\t\tst::introQrPixel / divider,\n\t\t\tscale);\n\t\tconst auto lineWidth = style::ConvertScale(\n\t\t\tst::lineWidth / divider,\n\t\t\tscale);\n\t\tconst auto boxWideWidth = style::ConvertScale(\n\t\t\tst::boxWideWidth / divider,\n\t\t\tscale);\n\t\tconst auto createMargins = [&](const style::margins &margins) {\n\t\t\treturn QMargins(\n\t\t\t\tstyle::ConvertScale(margins.left() / divider, scale),\n\t\t\t\tstyle::ConvertScale(margins.top() / divider, scale),\n\t\t\t\tstyle::ConvertScale(margins.right() / divider, scale),\n\t\t\t\tstyle::ConvertScale(margins.bottom() / divider, scale));\n\t\t};\n\t\tconst auto boxRowPadding = createMargins(st::boxRowPadding);\n\t\tconst auto backgroundMargins = userpicToggled\n\t\t\t? createMargins(st::profileQrBackgroundMargins)\n\t\t\t: createMargins(NoPhotoBackgroundMargins());\n\t\tconst auto qrMaxSize = boxWideWidth\n\t\t\t- rect::m::sum::h(boxRowPadding)\n\t\t\t- rect::m::sum::h(backgroundMargins);\n\t\tconst auto photoSize = userpicToggled\n\t\t\t? style::ConvertScale(\n\t\t\t\tst::defaultUserpicButton.photoSize / divider,\n\t\t\t\tscale)\n\t\t\t: 0;\n\n\t\tconst auto font = CreateFont(state->fontSizeValue.current(), scale);\n\t\tconst auto username = rpl::variable<QString>(\n\t\t\tusernameValue()).current().toUpper();\n\t\tconst auto link = rpl::variable<QString>(linkValue());\n\t\tconst auto textWidth = font->width(username);\n\t\tconst auto top = photoSize\n\t\t\t? userpicMedia->image(photoSize)\n\t\t\t: QImage();\n\t\tconst auto weak = base::make_weak(box);\n\n\t\tcrl::async([=] {\n\t\t\tconst auto qrImage = TelegramQr(\n\t\t\t\tQr::Encode(\n\t\t\t\t\tlink.current().toUtf8(),\n\t\t\t\t\tQr::Redundancy::Default),\n\t\t\t\tintroQrPixel,\n\t\t\t\tqrMaxSize,\n\t\t\t\tbackgroundToggled);\n\t\t\tconst auto textMaxWidth = backgroundMargins.left()\n\t\t\t\t+ (qrImage.width() / style::DevicePixelRatio());\n\t\t\tconst auto lines = int(textWidth / textMaxWidth) + 1;\n\t\t\tconst auto textMaxHeight = textWidth ? font->height * lines : 0;\n\n\t\t\tconst auto whiteMargins = RoundedMargins(\n\t\t\t\tbackgroundMargins,\n\t\t\t\tphotoSize,\n\t\t\t\ttextMaxHeight);\n\t\t\tconst auto resultSize = QSize(\n\t\t\t\tqrMaxSize + rect::m::sum::h(whiteMargins),\n\t\t\t\tqrMaxSize + rect::m::sum::v(whiteMargins) + photoSize / 2);\n\n\t\t\tconst auto qrImageSize = qrImage.size()\n\t\t\t\t/ style::DevicePixelRatio();\n\t\t\tconst auto qrRect = Rect(\n\t\t\t\t(resultSize.width() - qrImageSize.width()) / 2,\n\t\t\t\twhiteMargins.top() + photoSize / 2,\n\t\t\t\tqrImageSize);\n\n\t\t\tauto image = QImage(\n\t\t\t\tresultSize * style::DevicePixelRatio(),\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\timage.fill(Qt::transparent);\n\t\t\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\t{\n\t\t\t\tauto p = QPainter(&image);\n\t\t\t\tp.translate(0, lineWidth); // Bad.\n\t\t\t\tPaint(\n\t\t\t\t\tp,\n\t\t\t\t\tfont,\n\t\t\t\t\tusername,\n\t\t\t\t\tstate->bgs.current(),\n\t\t\t\t\tbackgroundMargins,\n\t\t\t\t\tqrImage,\n\t\t\t\t\tqrRect,\n\t\t\t\t\tqrMaxSize,\n\t\t\t\t\tintroQrPixel,\n\t\t\t\t\tprofileQrBackgroundRadius,\n\t\t\t\t\ttextMaxHeight,\n\t\t\t\t\tphotoSize,\n\t\t\t\t\tbackgroundToggled);\n\n\t\t\t\tif (userpicToggled) {\n\t\t\t\t\tp.drawImage((resultSize.width() - photoSize) / 2, 0, top);\n\t\t\t\t}\n\t\t\t}\n\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\tstate->saveButtonBusy = false;\n\t\t\t\tauto mime = std::make_unique<QMimeData>();\n\t\t\t\tmime->setImageData(std::move(image));\n\t\t\t\tQGuiApplication::clipboard()->setMimeData(mime.release());\n\t\t\t\tshow->showToast(tr::lng_group_invite_qr_copied(tr::now));\n\t\t\t});\n\t\t});\n\t});\n\n\tif (const auto saveButton = state->saveButton) {\n\t\tusing namespace Info::Statistics;\n\t\tconst auto loadingAnimation = InfiniteRadialAnimationWidget(\n\t\t\tsaveButton,\n\t\t\tsaveButton->height() / 2);\n\t\tAddChildToWidgetCenter(saveButton, loadingAnimation);\n\t\tloadingAnimation->showOn(state->saveButtonBusy.value());\n\n\t\tbox->showFinishes(\n\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\tif (const auto window = Core::App().findWindow(box)) {\n\t\t\t\twindow->checkHighlightControl(\n\t\t\t\t\tu\"self-qr-code/copy\"_q,\n\t\t\t\t\tsaveButton,\n\t\t\t\t\t{\n\t\t\t\t\t\t.color = &st::activeButtonFg,\n\t\t\t\t\t\t.opacity = 0.6,\n\t\t\t\t\t\t.rippleShape = true,\n\t\t\t\t\t\t.scroll = false,\n\t\t\t\t\t});\n\t\t\t}\n\t\t}, box->lifetime());\n\t}\n\n\tbox->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });\n}\n\nvoid DefaultShowFillPeerQrBoxCallback(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tPeerData *peer) {\n\tif (peer && !peer->username().isEmpty()) {\n\t\tshow->show(Box(Ui::FillPeerQrBox, peer, std::nullopt, nullptr));\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/peer_qr_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\n\nnamespace Ui {\n\nclass GenericBox;\nclass Show;\n\nvoid DefaultShowFillPeerQrBoxCallback(\n\tstd::shared_ptr<Ui::Show> show,\n\tPeerData *peer);\n\nvoid FillPeerQrBox(\n\tnot_null<Ui::GenericBox*> box,\n\tPeerData *peer,\n\tstd::optional<QString> customLink,\n\trpl::producer<QString> about);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/rate_call_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/rate_call_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_calls.h\"\n\nnamespace Ui {\n\nnamespace {\n\nconstexpr auto kMaxRating = 5;\nconstexpr auto kRateCallCommentLengthMax = 200;\n\n} // namespace\n\nRateCallBox::RateCallBox(QWidget*, InputSubmitSettings sendWay)\n: _sendWay(sendWay) {\n}\n\nvoid RateCallBox::prepare() {\n\tsetTitle(tr::lng_call_rate_label());\n\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\n\tfor (auto i = 0; i < kMaxRating; ++i) {\n\t\t_stars.emplace_back(this, st::callRatingStar);\n\t\t_stars.back()->setClickedCallback([this, value = i + 1] {\n\t\t\tratingChanged(value);\n\t\t});\n\t\t_stars.back()->show();\n\t}\n\n\tupdateMaxHeight();\n}\n\nvoid RateCallBox::resizeEvent(QResizeEvent *e) {\n\tBoxContent::resizeEvent(e);\n\n\tconst auto starsWidth = (_stars.size() * st::callRatingStar.width);\n\tauto starLeft = (width() - starsWidth) / 2;\n\tconst auto starTop = st::callRatingStarTop;\n\tfor (auto &star : _stars) {\n\t\tstar->moveToLeft(starLeft, starTop);\n\t\tstarLeft += star->width();\n\t}\n\tif (_comment) {\n\t\t_comment->moveToLeft(\n\t\t\tst::callRatingPadding.left(),\n\t\t\t_stars.back()->bottomNoMargins() + st::callRatingCommentTop);\n\t}\n}\n\nvoid RateCallBox::ratingChanged(int value) {\n\tExpects(value > 0 && value <= kMaxRating);\n\tif (!_rating) {\n\t\tclearButtons();\n\t\taddButton(tr::lng_send_button(), [=] { send(); });\n\t\taddButton(tr::lng_cancel(), [=] { closeBox(); });\n\t}\n\t_rating = value;\n\n\tfor (auto i = 0; i < kMaxRating; ++i) {\n\t\t_stars[i]->setIconOverride((i < value)\n\t\t\t? &st::callRatingStarFilled\n\t\t\t: nullptr);\n\t\t_stars[i]->setRippleColorOverride((i < value)\n\t\t\t? &st::lightButtonBgOver\n\t\t\t: nullptr);\n\t}\n\tif (value < kMaxRating) {\n\t\tif (!_comment) {\n\t\t\t_comment.create(\n\t\t\t\tthis,\n\t\t\t\tst::callRatingComment,\n\t\t\t\tUi::InputField::Mode::MultiLine,\n\t\t\t\ttr::lng_call_rate_comment());\n\t\t\t_comment->show();\n\t\t\t_comment->setSubmitSettings(_sendWay);\n\t\t\t_comment->setMaxLength(kRateCallCommentLengthMax);\n\t\t\t_comment->resize(\n\t\t\t\twidth()\n\t\t\t\t\t- st::callRatingPadding.left()\n\t\t\t\t\t- st::callRatingPadding.right(),\n\t\t\t\t_comment->height());\n\n\t\t\tupdateMaxHeight();\n\t\t\t_comment->heightChanges(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tcommentResized();\n\t\t\t}, _comment->lifetime());\n\t\t\t_comment->submits(\n\t\t\t) | rpl::on_next([=] { send(); }, _comment->lifetime());\n\t\t\t_comment->cancelled(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tcloseBox();\n\t\t\t}, _comment->lifetime());\n\t\t}\n\t\t_comment->setFocusFast();\n\t} else if (_comment) {\n\t\t_comment.destroy();\n\t\tupdateMaxHeight();\n\t}\n}\n\nvoid RateCallBox::setInnerFocus() {\n\tif (_comment) {\n\t\t_comment->setFocusFast();\n\t} else {\n\t\tBoxContent::setInnerFocus();\n\t}\n}\n\nvoid RateCallBox::commentResized() {\n\tupdateMaxHeight();\n\tupdate();\n}\n\nvoid RateCallBox::send() {\n\tExpects(_rating > 0 && _rating <= kMaxRating);\n\t_sends.fire({\n\t\t.rating = _rating,\n\t\t.comment = _comment ? _comment->getLastText().trimmed() : QString(),\n\t});\n}\n\nvoid RateCallBox::updateMaxHeight() {\n\tauto newHeight = st::callRatingPadding.top()\n\t\t+ st::callRatingStarTop\n\t\t+ _stars.back()->heightNoMargins()\n\t\t+ st::callRatingPadding.bottom();\n\tif (_comment) {\n\t\tnewHeight += st::callRatingCommentTop + _comment->height();\n\t}\n\tsetDimensions(st::boxWideWidth, newHeight);\n}\n\nrpl::producer<RateCallBox::Result> RateCallBox::sends() const {\n\treturn _sends.events();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/rate_call_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/box_content.h\"\n\nnamespace Ui {\n\nclass InputField;\nclass IconButton;\nenum class InputSubmitSettings;\n\nclass RateCallBox : public Ui::BoxContent {\npublic:\n\tRateCallBox(QWidget*, InputSubmitSettings sendWay);\n\n\tstruct Result {\n\t\tint rating = 0;\n\t\tQString comment;\n\t};\n\n\t[[nodiscard]] rpl::producer<Result> sends() const;\n\nprotected:\n\tvoid prepare() override;\n\tvoid setInnerFocus() override;\n\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid updateMaxHeight();\n\tvoid ratingChanged(int value);\n\tvoid send();\n\tvoid commentResized();\n\n\tconst InputSubmitSettings _sendWay;\n\tint _rating = 0;\n\n\tstd::vector<object_ptr<Ui::IconButton>> _stars;\n\tobject_ptr<Ui::InputField> _comment = { nullptr };\n\n\trpl::event_stream<Result> _sends;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/report_box_graphics.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/report_box_graphics.h\"\n\n#include \"info/profile/info_profile_icon.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_channel_earn.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kReportReasonLengthMax = 512;\n\nusing Source = ReportSource;\nusing Reason = ReportReason;\n\n} // namespace\n\nvoid ReportReasonBox(\n\t\tnot_null<GenericBox*> box,\n\t\tconst style::ReportBox &st,\n\t\tReportSource source,\n\t\tFn<void(Reason)> done) {\n\tbox->setTitle([&] {\n\t\tswitch (source) {\n\t\tcase Source::Message: return tr::lng_report_message_title();\n\t\tcase Source::Channel: return tr::lng_report_title();\n\t\tcase Source::Group: return tr::lng_report_group_title();\n\t\tcase Source::Bot: return tr::lng_report_bot_title();\n\t\tcase Source::ProfilePhoto:\n\t\t\treturn tr::lng_report_profile_photo_title();\n\t\tcase Source::ProfileVideo:\n\t\t\treturn tr::lng_report_profile_video_title();\n\t\tcase Source::GroupPhoto: return tr::lng_report_group_photo_title();\n\t\tcase Source::GroupVideo: return tr::lng_report_group_video_title();\n\t\tcase Source::ChannelPhoto:\n\t\t\treturn tr::lng_report_channel_photo_title();\n\t\tcase Source::ChannelVideo:\n\t\t\treturn tr::lng_report_channel_video_title();\n\t\tcase Source::Story:\n\t\t\treturn tr::lng_report_story();\n\t\t}\n\t\tUnexpected(\"'source' in ReportReasonBox.\");\n\t}());\n\tauto margin = style::margins{ 0, st::reportReasonTopSkip, 0, 0 };\n\tconst auto add = [&](\n\t\t\tReason reason,\n\t\t\ttr::phrase<> text,\n\t\t\tconst style::icon &icon) {\n\t\tconst auto layout = box->verticalLayout();\n\t\tconst auto button = layout->add(\n\t\t\tobject_ptr<Ui::SettingsButton>(layout.get(), text(), st.button),\n\t\t\tmargin);\n\t\tmargin = {};\n\t\tbutton->setClickedCallback([=] {\n\t\t\tdone(reason);\n\t\t});\n\t\tconst auto height = st.button.padding.top()\n\t\t\t+ st.button.height\n\t\t\t+ st.button.padding.bottom();\n\t\tobject_ptr<Info::Profile::FloatingIcon>(\n\t\t\tbutton,\n\t\t\ticon,\n\t\t\tQPoint{\n\t\t\t\tst::infoSharedMediaButtonIconPosition.x(),\n\t\t\t\t(height - icon.height()) / 2,\n\t\t\t});\n\t};\n\tadd(Reason::Spam, tr::lng_report_reason_spam, st.spam);\n\tif (source == Source::Channel\n\t\t|| source == Source::Group\n\t\t|| source == Source::Bot) {\n\t\tadd(Reason::Fake, tr::lng_report_reason_fake, st.fake);\n\t}\n\tadd(\n\t\tReason::Violence,\n\t\ttr::lng_report_reason_violence,\n\t\tst.violence);\n\tadd(\n\t\tReason::ChildAbuse,\n\t\ttr::lng_report_reason_child_abuse,\n\t\tst.children);\n\tadd(\n\t\tReason::Pornography,\n\t\ttr::lng_report_reason_pornography,\n\t\tst.pornography);\n\tadd(\n\t\tReason::Copyright,\n\t\ttr::lng_report_reason_copyright,\n\t\tst.copyright);\n\tif (source == Source::Message || source == Source::Story) {\n\t\tadd(\n\t\t\tReason::IllegalDrugs,\n\t\t\ttr::lng_report_reason_illegal_drugs,\n\t\t\tst.drugs);\n\t\tadd(\n\t\t\tReason::PersonalDetails,\n\t\t\ttr::lng_report_reason_personal_details,\n\t\t\tst.personal);\n\t}\n\tadd(Reason::Other, tr::lng_report_reason_other, st.other);\n\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\nvoid ReportDetailsBox(\n\t\tnot_null<GenericBox*> box,\n\t\tconst style::ReportBox &st,\n\t\tFn<void(QString)> done) {\n\tbox->setTitle(tr::lng_profile_report());\n\tAddReportDetailsIconButton(box);\n\tUi::AddSkip(\n\t\tbox->verticalLayout(),\n\t\tst::settingsBlockedListIconPadding.bottom());\n\n\tbox->addRow(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tbox, // #TODO reports\n\t\t\ttr::lng_report_details_about(),\n\t\t\tst.label),\n\t\t{\n\t\t\tst::boxRowPadding.left(),\n\t\t\tst::boxPadding.top(),\n\t\t\tst::boxRowPadding.right(),\n\t\t\tst::boxPadding.bottom(),\n\t\t});\n\tconst auto details = box->addRow(\n\t\tobject_ptr<InputField>(\n\t\t\tbox,\n\t\t\tst.field,\n\t\t\tInputField::Mode::MultiLine,\n\t\t\ttr::lng_report_details(),\n\t\t\tQString()));\n\tdetails->setMaxLength(kReportReasonLengthMax);\n\tbox->setFocusCallback([=] {\n\t\tdetails->setFocusFast();\n\t});\n\n\tconst auto submit = [=] {\n\t\tconst auto text = details->getLastText();\n\t\tdone(text);\n\t};\n\tdetails->submits() | rpl::on_next(submit, details->lifetime());\n\tbox->addButton(tr::lng_report_button(), submit);\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\nnot_null<Ui::AbstractButton*> AddReportOptionButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst QString &text,\n\t\tconst style::ReportBox *stOverride) {\n\tconst auto button = container->add(\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\tcontainer,\n\t\t\trpl::single(QString()),\n\t\t\t(stOverride ? stOverride : &st::defaultReportBox)->noIconButton));\n\tconst auto textFg = (stOverride\n\t\t? stOverride->label\n\t\t: st::sponsoredReportLabel).textFg->c;\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton,\n\t\trpl::single(text),\n\t\tst::sponsoredReportLabel);\n\tlabel->setTextColorOverride(textFg);\n\tconst auto icon = Ui::CreateChild<Ui::RpWidget>(button);\n\ticon->resize(st::settingsPremiumArrow.size());\n\ticon->paintRequest() | rpl::on_next([=, w = icon->width()] {\n\t\tauto p = Painter(icon);\n\t\tst::settingsPremiumArrow.paint(p, 0, 0, w, textFg);\n\t}, icon->lifetime());\n\tbutton->sizeValue() | rpl::on_next([=](const QSize &size) {\n\t\tconst auto left = button->st().padding.left();\n\t\tconst auto right = button->st().padding.right();\n\t\ticon->moveToRight(right, (size.height() - icon->height()) / 2);\n\t\tlabel->resizeToWidth(size.width()\n\t\t\t- icon->width()\n\t\t\t- left\n\t\t\t- st::settingsButtonRightSkip\n\t\t\t- right);\n\t\tlabel->moveToLeft(left, (size.height() - label->height()) / 2);\n\t\tbutton->resize(\n\t\t\tbutton->width(),\n\t\t\trect::m::sum::v(button->st().padding) + label->height());\n\t}, button->lifetime());\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\ticon->setAttribute(Qt::WA_TransparentForMouseEvents);\n\treturn button;\n}\n\nvoid AddReportDetailsIconButton(not_null<GenericBox*> box) {\n\tauto icon = Settings::CreateLottieIcon(\n\t\tbox->verticalLayout(),\n\t\t{\n\t\t\t.name = u\"blocked_peers_empty\"_q,\n\t\t\t.sizeOverride = st::normalBoxLottieSize,\n\t\t},\n\t\t{});\n\tbox->setShowFinishedCallback([animate = std::move(icon.animate)] {\n\t\tanimate(anim::repeat::once);\n\t});\n\tbox->addRow(std::move(icon.widget));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/report_box_graphics.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace style {\nstruct ReportBox;\n} // namespace style\n\nnamespace Ui {\n\nclass AbstractButton;\nclass GenericBox;\nclass VerticalLayout;\n\nenum class ReportSource {\n\tMessage,\n\tChannel,\n\tGroup,\n\tBot,\n\tProfilePhoto,\n\tProfileVideo,\n\tGroupPhoto,\n\tGroupVideo,\n\tChannelPhoto,\n\tChannelVideo,\n\tStory,\n};\n\nenum class ReportReason {\n\tSpam,\n\tFake,\n\tViolence,\n\tChildAbuse,\n\tPornography,\n\tCopyright,\n\tIllegalDrugs,\n\tPersonalDetails,\n\tOther,\n};\n\nvoid ReportReasonBox(\n\tnot_null<GenericBox*> box,\n\tconst style::ReportBox &st,\n\tReportSource source,\n\tFn<void(ReportReason)> done);\n\nvoid ReportDetailsBox(\n\tnot_null<GenericBox*> box,\n\tconst style::ReportBox &st,\n\tFn<void(QString)> done);\n\n[[nodiscard]] not_null<Ui::AbstractButton*> AddReportOptionButton(\n\tnot_null<Ui::VerticalLayout*> container,\n\tconst QString &text,\n\tconst style::ReportBox *stOverride);\n\nvoid AddReportDetailsIconButton(not_null<GenericBox*> box);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/show_or_premium_box.h\"\n\n#include \"base/object_ptr.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/gradient_round_button.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/vertical_list.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kShowOrLineOpacity = 0.3;\n\n} // namespace\n\nobject_ptr<RpWidget> MakeShowOrLabel(\n\t\tnot_null<RpWidget*> parent,\n\t\trpl::producer<QString> text) {\n\tauto result = object_ptr<FlatLabel>(\n\t\tparent,\n\t\tstd::move(text),\n\t\tst::showOrLabel);\n\tconst auto raw = result.data();\n\n\traw->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(raw);\n\n\t\tconst auto full = st::showOrLineWidth;\n\t\tconst auto left = (raw->width() - full) / 2;\n\t\tconst auto text = raw->naturalWidth() + 2 * st::showOrLabelSkip;\n\t\tconst auto fill = (full - text) / 2;\n\t\tconst auto stroke = st::lineWidth;\n\t\tconst auto top = st::showOrLineTop;\n\t\tp.setOpacity(kShowOrLineOpacity);\n\t\tp.fillRect(left, top, fill, stroke, st::windowSubTextFg);\n\t\tconst auto start = left + full - fill;\n\t\tp.fillRect(start, top, fill, stroke, st::windowSubTextFg);\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\nvoid ShowOrPremiumBox(\n\t\tnot_null<GenericBox*> box,\n\t\tShowOrPremium type,\n\t\tQString shortName,\n\t\tFn<void()> justShow,\n\t\tFn<void()> toPremium) {\n\tstruct Skin {\n\t\trpl::producer<QString> showTitle;\n\t\trpl::producer<TextWithEntities> showAbout;\n\t\trpl::producer<QString> showButton;\n\t\trpl::producer<QString> orPremium;\n\t\trpl::producer<QString> premiumTitle;\n\t\trpl::producer<TextWithEntities> premiumAbout;\n\t\trpl::producer<QString> premiumButton;\n\t\tQString toast;\n\t\tQString lottie;\n\t};\n\tauto skin = (type == ShowOrPremium::LastSeen)\n\t\t? Skin{\n\t\t\ttr::lng_lastseen_show_title(),\n\t\t\ttr::lng_lastseen_show_about(\n\t\t\t\tlt_user,\n\t\t\t\trpl::single(TextWithEntities{ shortName }),\n\t\t\t\ttr::rich),\n\t\t\ttr::lng_lastseen_show_button(),\n\t\t\ttr::lng_lastseen_or(),\n\t\t\ttr::lng_lastseen_premium_title(),\n\t\t\ttr::lng_lastseen_premium_about(\n\t\t\t\tlt_user,\n\t\t\t\trpl::single(TextWithEntities{ shortName }),\n\t\t\t\ttr::rich),\n\t\t\ttr::lng_lastseen_premium_button(),\n\t\t\ttr::lng_lastseen_shown_toast(tr::now),\n\t\t\tu\"show_or_premium_lastseen\"_q,\n\t\t}\n\t\t: Skin{\n\t\t\ttr::lng_readtime_show_title(),\n\t\t\ttr::lng_readtime_show_about(\n\t\t\t\tlt_user,\n\t\t\t\trpl::single(TextWithEntities{ shortName }),\n\t\t\t\ttr::rich),\n\t\t\ttr::lng_readtime_show_button(),\n\t\t\ttr::lng_readtime_or(),\n\t\t\ttr::lng_readtime_premium_title(),\n\t\t\ttr::lng_readtime_premium_about(\n\t\t\t\tlt_user,\n\t\t\t\trpl::single(TextWithEntities{ shortName }),\n\t\t\t\ttr::rich),\n\t\t\ttr::lng_readtime_premium_button(),\n\t\t\ttr::lng_readtime_shown_toast(tr::now),\n\t\t\tu\"show_or_premium_readtime\"_q,\n\t\t};\n\n\tbox->setStyle(st::showOrBox);\n\tbox->setWidth(st::boxWideWidth);\n\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\tbox->closeBox();\n\t});\n\n\tconst auto buttonPadding = QMargins(\n\t\tst::showOrBox.buttonPadding.left(),\n\t\t0,\n\t\tst::showOrBox.buttonPadding.right(),\n\t\t0);\n\n\tauto icon = Settings::CreateLottieIcon(\n\t\tbox,\n\t\t{\n\t\t\t.name = skin.lottie,\n\t\t\t.sizeOverride = st::normalBoxLottieSize\n\t\t\t\t- Size(st::showOrTitleIconMargin * 2),\n\t\t},\n\t\t{ 0, st::showOrTitleIconMargin, 0, st::showOrTitleIconMargin });\n\tSettings::AddLottieIconWithCircle(\n\t\tbox->verticalLayout(),\n\t\tstd::move(icon.widget),\n\t\tst::settingsBlockedListIconPadding,\n\t\tst::normalBoxLottieSize);\n\tUi::AddSkip(box->verticalLayout());\n\tbox->addRow(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(skin.showTitle),\n\t\t\tst::boostCenteredTitle),\n\t\tst::showOrTitlePadding + buttonPadding,\n\t\tstyle::al_top);\n\tbox->addRow(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(skin.showAbout),\n\t\t\tst::boostText),\n\t\tst::showOrAboutPadding + buttonPadding,\n\t\tstyle::al_top);\n\tconst auto show = box->addRow(\n\t\tobject_ptr<RoundButton>(\n\t\t\tbox,\n\t\t\tstd::move(skin.showButton),\n\t\t\tst::showOrShowButton),\n\t\tbuttonPadding);\n\tbox->addRow(\n\t\tMakeShowOrLabel(box, std::move(skin.orPremium)),\n\t\tst::showOrLabelPadding + buttonPadding,\n\t\tstyle::al_justify);\n\tbox->addRow(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(skin.premiumTitle),\n\t\t\tst::boostCenteredTitle),\n\t\tst::showOrTitlePadding + buttonPadding,\n\t\tstyle::al_top);\n\tbox->addRow(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(skin.premiumAbout),\n\t\t\tst::boostText),\n\t\tst::showOrPremiumAboutPadding + buttonPadding,\n\t\tstyle::al_top);\n\n\tconst auto premium = CreateChild<GradientButton>(\n\t\tbox.get(),\n\t\tPremium::ButtonGradientStops());\n\n\tpremium->resize(st::showOrShowButton.width, st::showOrShowButton.height);\n\n\tconst auto label = CreateChild<FlatLabel>(\n\t\tpremium,\n\t\tstd::move(skin.premiumButton),\n\t\tst::premiumPreviewButtonLabel);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\trpl::combine(\n\t\tpremium->widthValue(),\n\t\tlabel->widthValue()\n\t) | rpl::on_next([=](int outer, int width) {\n\t\tlabel->moveToLeft(\n\t\t\t(outer - width) / 2,\n\t\t\tst::premiumPreviewBox.button.textTop,\n\t\t\touter);\n\t}, label->lifetime());\n\n\tbox->setShowFinishedCallback([=, animate = std::move(icon.animate)] {\n\t\tpremium->startGlareAnimation();\n\t\tanimate(anim::repeat::once);\n\t});\n\n\tbox->addButton(\n\t\tobject_ptr<AbstractButton>::fromRaw(premium));\n\n\tshow->setClickedCallback([box, justShow, toast = skin.toast] {\n\t\tjustShow();\n\t\tbox->uiShow()->showToast(toast);\n\t\tbox->closeBox();\n\t});\n\tpremium->setClickedCallback(std::move(toPremium));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/show_or_premium_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\n\nclass RpWidget;\nclass GenericBox;\n\nenum class ShowOrPremium : uchar {\n\tLastSeen,\n\tReadTime,\n};\nvoid ShowOrPremiumBox(\n\tnot_null<GenericBox*> box,\n\tShowOrPremium type,\n\tQString shortName,\n\tFn<void()> justShow,\n\tFn<void()> toPremium);\n\n[[nodiscard]] object_ptr<RpWidget> MakeShowOrLabel(\n\tnot_null<RpWidget*> parent,\n\trpl::producer<QString> text);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/single_choice_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/single_choice_box.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n\nvoid SingleChoiceBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tSingleChoiceBoxArgs &&args) {\n\tbox->setTitle(std::move(args.title));\n\n\tbox->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });\n\n\tconst auto group = std::make_shared<Ui::RadiobuttonGroup>(\n\t\targs.initialSelection);\n\n\tconst auto layout = box->verticalLayout();\n\tlayout->add(object_ptr<Ui::FixedHeightWidget>(\n\t\tlayout,\n\t\tst::boxOptionListPadding.top() + st::autolockButton.margin.top()));\n\tauto &&ints = ranges::views::ints(0, ranges::unreachable);\n\tfor (const auto &[i, text] : ranges::views::zip(ints, args.options)) {\n\t\tlayout->add(\n\t\t\tobject_ptr<Ui::Radiobutton>(\n\t\t\t\tlayout,\n\t\t\t\tgroup,\n\t\t\t\ti,\n\t\t\t\ttext,\n\t\t\t\targs.st ? *args.st : st::defaultBoxCheckbox,\n\t\t\t\targs.radioSt ? *args.radioSt : st::defaultRadio),\n\t\t\tQMargins(\n\t\t\t\tst::boxPadding.left() + st::boxOptionListPadding.left(),\n\t\t\t\t0,\n\t\t\t\tst::boxPadding.right(),\n\t\t\t\tst::boxOptionListSkip));\n\t}\n\tconst auto callback = args.callback.value();\n\tgroup->setChangedCallback([=](int value) {\n\t\tconst auto weak = base::make_weak(box);\n\t\tcallback(value);\n\t\tif (weak) {\n\t\t\tbox->closeBox();\n\t\t}\n\t});\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/single_choice_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n#include \"base/required.h\"\n\nnamespace style {\nstruct Checkbox;\nstruct Radio;\n} // namespace style\n\nstruct SingleChoiceBoxArgs {\n\ttemplate <typename T>\n\tusing required = base::required<T>;\n\n\trequired<rpl::producer<QString>> title;\n\tconst std::vector<QString> &options;\n\tint initialSelection = 0;\n\trequired<Fn<void(int)>> callback;\n\tconst style::Checkbox *st = nullptr;\n\tconst style::Radio *radioSt = nullptr;\n};\n\nvoid SingleChoiceBox(\n\tnot_null<Ui::GenericBox*> box,\n\tSingleChoiceBoxArgs &&args);\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/time_picker_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/boxes/time_picker_box.h\"\n\n#include \"base/event_filter.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/vertical_drum_picker.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Ui {\n\nstd::vector<TimeId> DefaultTimePickerValues() {\n\treturn {\n\t\t(60 * 15),\n\t\t(60 * 30),\n\t\t(3600 * 1),\n\t\t(3600 * 2),\n\t\t(3600 * 3),\n\t\t(3600 * 4),\n\t\t(3600 * 8),\n\t\t(3600 * 12),\n\t\t(86400 * 1),\n\t\t(86400 * 2),\n\t\t(86400 * 3),\n\t\t(86400 * 7 * 1),\n\t\t(86400 * 7 * 2),\n\t\t(86400 * 31 * 1),\n\t\t(86400 * 31 * 2),\n\t\t(86400 * 31 * 3),\n\t};\n}\n\nFn<TimeId()> TimePickerBox(\n\t\tnot_null<GenericBox*> box,\n\t\tstd::vector<TimeId> values,\n\t\tstd::vector<QString> phrases,\n\t\tTimeId startValue) {\n\tExpects(phrases.size() == values.size());\n\n\tconst auto startIndex = [&, &v = startValue] {\n\t\tconst auto it = ranges::lower_bound(values, v);\n\t\tif (it == begin(values)) {\n\t\t\treturn 0;\n\t\t}\n\t\tconst auto left = *(it - 1);\n\t\tconst auto right = *it;\n\t\tconst auto shift = (std::abs(v - left) < std::abs(v - right))\n\t\t\t? -1\n\t\t\t: 0;\n\t\treturn int(std::distance(begin(values), it - shift));\n\t}();\n\n\tconst auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>(\n\t\tbox,\n\t\tst::historyMessagesTTLPickerHeight));\n\n\tconst auto font = st::boxTextFont;\n\tconst auto maxPhraseWidth = [&] {\n\t\t// We have to use QFontMetricsF instead of\n\t\t// FontData::width for more precise calculation.\n\t\tconst auto mf = QFontMetricsF(font->f);\n\t\tconst auto maxPhrase = ranges::max_element(\n\t\t\tphrases,\n\t\t\tstd::less<>(),\n\t\t\t[&](const QString &s) { return mf.horizontalAdvance(s); });\n\t\treturn std::ceil(mf.horizontalAdvance(*maxPhrase));\n\t}();\n\tconst auto itemHeight = st::historyMessagesTTLPickerItemHeight;\n\tauto paintCallback = Ui::VerticalDrumPicker::DefaultPaintCallback(\n\t\tfont,\n\t\titemHeight,\n\t\t[=](QPainter &p, QRectF r, int index) {\n\t\t\tp.drawText(r, phrases[index], style::al_center);\n\t\t});\n\n\tconst auto picker = Ui::CreateChild<Ui::VerticalDrumPicker>(\n\t\tcontent,\n\t\tstd::move(paintCallback),\n\t\tphrases.size(),\n\t\titemHeight,\n\t\tstartIndex);\n\n\tcontent->sizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\tpicker->resize(maxPhraseWidth, s.height());\n\t\tpicker->moveToLeft((s.width() - picker->width()) / 2, 0);\n\t}, content->lifetime());\n\n\tcontent->paintRequest(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tauto p = QPainter(content);\n\n\t\tp.fillRect(r, Qt::transparent);\n\n\t\tconst auto lineRect = QRect(\n\t\t\t0,\n\t\t\tcontent->height() / 2,\n\t\t\tcontent->width(),\n\t\t\tst::defaultInputField.borderActive);\n\t\tp.fillRect(lineRect.translated(0, itemHeight / 2), st::activeLineFg);\n\t\tp.fillRect(lineRect.translated(0, -itemHeight / 2), st::activeLineFg);\n\t}, content->lifetime());\n\n\tbase::install_event_filter(content, [=](not_null<QEvent*> e) {\n\t\tif ((e->type() == QEvent::MouseButtonPress)\n\t\t\t|| (e->type() == QEvent::MouseButtonRelease)\n\t\t\t|| (e->type() == QEvent::MouseMove)) {\n\t\t\tpicker->handleMouseEvent(static_cast<QMouseEvent*>(e.get()));\n\t\t} else if (e->type() == QEvent::Wheel) {\n\t\t\tpicker->handleWheelEvent(static_cast<QWheelEvent*>(e.get()));\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\tbase::install_event_filter(box, [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::KeyPress) {\n\t\t\tpicker->handleKeyEvent(static_cast<QKeyEvent*>(e.get()));\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\treturn [=] { return values[picker->index()]; };\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/boxes/time_picker_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nclass GenericBox;\n\n[[nodiscard]] std::vector<TimeId> DefaultTimePickerValues();\n\n[[nodiscard]] Fn<TimeId()> TimePickerBox(\n\tnot_null<GenericBox*> box,\n\tstd::vector<TimeId> values,\n\tstd::vector<QString> phrases,\n\tTimeId startValue);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/cached_round_corners.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/cached_round_corners.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_overview.h\"\n#include \"styles/style_media_view.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kCachedCornerRadiusCount = int(CachedCornerRadius::kCount);\n\nstd::vector<CornersPixmaps> Corners;\nQImage CornersMaskLarge[4], CornersMaskSmall[4];\nrpl::lifetime PaletteChangedLifetime;\n\nstd::array<std::array<QImage, 4>, kCachedCornerRadiusCount> CachedMasks;\n\n[[nodiscard]] std::array<QImage, 4> PrepareCorners(int32 radius, const QBrush &brush, const style::color *shadow = nullptr) {\n\tint32 r = radius * style::DevicePixelRatio(), s = st::msgShadow * style::DevicePixelRatio();\n\tQImage rect(r * 3, r * 3 + (shadow ? s : 0), QImage::Format_ARGB32_Premultiplied);\n\trect.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&rect);\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tp.setPen(Qt::NoPen);\n\t\tif (shadow) {\n\t\t\tp.setBrush((*shadow)->b);\n\t\t\tp.drawRoundedRect(0, s, r * 3, r * 3, r, r);\n\t\t}\n\t\tp.setBrush(brush);\n\t\tp.drawRoundedRect(0, 0, r * 3, r * 3, r, r);\n\t}\n\tauto result = std::array<QImage, 4>();\n\tresult[0] = rect.copy(0, 0, r, r);\n\tresult[1] = rect.copy(r * 2, 0, r, r);\n\tresult[2] = rect.copy(0, r * 2, r, r + (shadow ? s : 0));\n\tresult[3] = rect.copy(r * 2, r * 2, r, r + (shadow ? s : 0));\n\treturn result;\n}\n\nvoid PrepareCorners(CachedRoundCorners index, int32 radius, const QBrush &brush, const style::color *shadow = nullptr) {\n\tExpects(index < Corners.size());\n\n\tauto images = PrepareCorners(radius, brush, shadow);\n\tfor (int i = 0; i < 4; ++i) {\n\t\tCorners[index].p[i] = PixmapFromImage(std::move(images[i]));\n\t\tCorners[index].p[i].setDevicePixelRatio(style::DevicePixelRatio());\n\t}\n}\n\nvoid CreateMaskCorners() {\n\tauto mask = PrepareCorners(st::roundRadiusSmall, QColor(255, 255, 255), nullptr);\n\tfor (int i = 0; i < 4; ++i) {\n\t\tCornersMaskSmall[i] = mask[i].convertToFormat(QImage::Format_ARGB32_Premultiplied);\n\t\tCornersMaskSmall[i].setDevicePixelRatio(style::DevicePixelRatio());\n\t}\n\tmask = PrepareCorners(st::roundRadiusLarge, QColor(255, 255, 255), nullptr);\n\tfor (int i = 0; i < 4; ++i) {\n\t\tCornersMaskLarge[i] = mask[i].convertToFormat(QImage::Format_ARGB32_Premultiplied);\n\t\tCornersMaskLarge[i].setDevicePixelRatio(style::DevicePixelRatio());\n\t}\n}\n\nvoid CreatePaletteCorners() {\n\tPrepareCorners(MenuCorners, st::innerDropdownRadius, st::menuBg);\n\tPrepareCorners(BoxCorners, st::boxRadius, st::boxBg);\n\tPrepareCorners(DateCorners, st::dateRadius, st::msgDateImgBg);\n\tPrepareCorners(OverviewVideoCorners, st::overviewVideoStatusRadius, st::msgDateImgBg);\n\tPrepareCorners(OverviewVideoSelectedCorners, st::overviewVideoStatusRadius, st::msgDateImgBgSelected);\n\tPrepareCorners(ForwardCorners, st::roundRadiusLarge, st::historyForwardChooseBg);\n\tPrepareCorners(MediaviewSaveCorners, st::mediaviewControllerRadius, st::mediaviewSaveMsgBg);\n\tPrepareCorners(StickerHoverCorners, st::roundRadiusSmall, st::emojiPanHover);\n\tPrepareCorners(BotKeyboardCorners, st::roundRadiusSmall, st::botKbBg);\n\n\tPrepareCorners(Doc1Corners, st::roundRadiusSmall, st::msgFile1Bg);\n\tPrepareCorners(Doc2Corners, st::roundRadiusSmall, st::msgFile2Bg);\n\tPrepareCorners(Doc3Corners, st::roundRadiusSmall, st::msgFile3Bg);\n\tPrepareCorners(Doc4Corners, st::roundRadiusSmall, st::msgFile4Bg);\n}\n\n} // namespace\n\nvoid StartCachedCorners() {\n\tCorners.resize(RoundCornersCount);\n\tCreateMaskCorners();\n\tCreatePaletteCorners();\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tCreatePaletteCorners();\n\t}, PaletteChangedLifetime);\n}\n\nvoid FinishCachedCorners() {\n\tCorners.clear();\n\tPaletteChangedLifetime.destroy();\n}\n\nvoid FillRoundRect(QPainter &p, int32 x, int32 y, int32 w, int32 h, style::color bg, const CornersPixmaps &corners) {\n\tusing namespace Images;\n\n\tconst auto fillBg = [&](QRect rect) {\n\t\tp.fillRect(rect, bg);\n\t};\n\tconst auto fillCorner = [&](int x, int y, int index) {\n\t\tif (const auto &pix = corners.p[index]; !pix.isNull()) {\n\t\t\tp.drawPixmap(x, y, pix);\n\t\t}\n\t};\n\n\tif (corners.p[kTopLeft].isNull()\n\t\t&& corners.p[kTopRight].isNull()\n\t\t&& corners.p[kBottomLeft].isNull()\n\t\t&& corners.p[kBottomRight].isNull()) {\n\t\tp.fillRect(x, y, w, h, bg);\n\t\treturn;\n\t}\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto cornerSize = [&](int index) {\n\t\treturn corners.p[index].isNull()\n\t\t\t? 0\n\t\t\t: (corners.p[index].width() / ratio);\n\t};\n\tconst auto verticalSkip = [&](int left, int right) {\n\t\treturn std::max(cornerSize(left), cornerSize(right));\n\t};\n\tconst auto top = verticalSkip(kTopLeft, kTopRight);\n\tconst auto bottom = verticalSkip(kBottomLeft, kBottomRight);\n\tif (top) {\n\t\tconst auto left = cornerSize(kTopLeft);\n\t\tconst auto right = cornerSize(kTopRight);\n\t\tif (left) {\n\t\t\tfillCorner(x, y, kTopLeft);\n\t\t\tif (const auto add = top - left) {\n\t\t\t\tfillBg({ x, y + left, left, add });\n\t\t\t}\n\t\t}\n\t\tif (const auto fill = w - left - right; fill > 0) {\n\t\t\tfillBg({ x + left, y, fill, top });\n\t\t}\n\t\tif (right) {\n\t\t\tfillCorner(x + w - right, y, kTopRight);\n\t\t\tif (const auto add = top - right) {\n\t\t\t\tfillBg({ x + w - right, y + right, right, add });\n\t\t\t}\n\t\t}\n\t}\n\tif (const auto fill = h - top - bottom; fill > 0) {\n\t\tfillBg({ x, y + top, w, fill });\n\t}\n\tif (bottom) {\n\t\tconst auto left = cornerSize(kBottomLeft);\n\t\tconst auto right = cornerSize(kBottomRight);\n\t\tif (left) {\n\t\t\tfillCorner(x, y + h - left, kBottomLeft);\n\t\t\tif (const auto add = bottom - left) {\n\t\t\t\tfillBg({ x, y + h - bottom, left, add });\n\t\t\t}\n\t\t}\n\t\tif (const auto fill = w - left - right; fill > 0) {\n\t\t\tfillBg({ x + left, y + h - bottom, fill, bottom });\n\t\t}\n\t\tif (right) {\n\t\t\tfillCorner(x + w - right, y + h - right, kBottomRight);\n\t\t\tif (const auto add = bottom - right) {\n\t\t\t\tfillBg({ x + w - right, y + h - bottom, right, add });\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid FillRoundRect(QPainter &p, int32 x, int32 y, int32 w, int32 h, style::color bg, CachedRoundCorners index) {\n\tFillRoundRect(p, x, y, w, h, bg, CachedCornerPixmaps(index));\n}\n\nvoid FillRoundShadow(QPainter &p, int32 x, int32 y, int32 w, int32 h, style::color shadow, const CornersPixmaps &corners) {\n\tconstexpr auto kLeft = 2;\n\tconstexpr auto kRight = 3;\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto size = [&](int index) {\n\t\tconst auto &pix = corners.p[index];\n\t\treturn pix.isNull() ? 0 : (pix.width() / ratio);\n\t};\n\tconst auto fillCorner = [&](int left, int bottom, int index) {\n\t\tconst auto &pix = corners.p[index];\n\t\tif (pix.isNull()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto size = pix.width() / ratio;\n\t\tp.drawPixmap(left, bottom - size, pix);\n\t};\n\tconst auto left = size(kLeft);\n\tconst auto right = size(kRight);\n\tconst auto from = x + left;\n\tfillCorner(x, y + h + st::msgShadow, kLeft);\n\tif (const auto width = w - left - right; width > 0) {\n\t\tp.fillRect(from, y + h, width, st::msgShadow, shadow);\n\t}\n\tfillCorner(x + w - right, y + h + st::msgShadow, kRight);\n}\n\nconst CornersPixmaps &CachedCornerPixmaps(CachedRoundCorners index) {\n\tExpects(index >= 0 && index < RoundCornersCount);\n\n\treturn Corners[index];\n}\n\nCornersPixmaps PrepareCornerPixmaps(int radius, style::color bg, const style::color *sh) {\n\tauto images = PrepareCorners(radius, bg, sh);\n\tauto result = CornersPixmaps();\n\tfor (int j = 0; j < 4; ++j) {\n\t\tresult.p[j] = PixmapFromImage(std::move(images[j]));\n\t\tresult.p[j].setDevicePixelRatio(style::DevicePixelRatio());\n\t}\n\treturn result;\n}\n\nCornersPixmaps PrepareCornerPixmaps(ImageRoundRadius radius, style::color bg, const style::color *sh) {\n\tswitch (radius) {\n\tcase ImageRoundRadius::Small:\n\t\treturn PrepareCornerPixmaps(st::roundRadiusSmall, bg, sh);\n\tcase ImageRoundRadius::Large:\n\t\treturn PrepareCornerPixmaps(st::roundRadiusLarge, bg, sh);\n\t}\n\tUnexpected(\"Image round radius in PrepareCornerPixmaps.\");\n}\n\nCornersPixmaps PrepareInvertedCornerPixmaps(int radius, style::color bg) {\n\tconst auto size = radius * style::DevicePixelRatio();\n\tauto circle = style::colorizeImage(\n\t\tstyle::createInvertedCircleMask(radius * 2),\n\t\tbg);\n\tcircle.setDevicePixelRatio(style::DevicePixelRatio());\n\tauto result = CornersPixmaps();\n\tconst auto fill = [&](int index, int xoffset, int yoffset) {\n\t\tresult.p[index] = PixmapFromImage(\n\t\t\tcircle.copy(QRect(xoffset, yoffset, size, size)));\n\t};\n\tfill(0, 0, 0);\n\tfill(1, size, 0);\n\tfill(2, size, size);\n\tfill(3, 0, size);\n\treturn result;\n}\n\n[[nodiscard]] int CachedCornerRadiusValue(CachedCornerRadius tag) {\n\tusing Radius = CachedCornerRadius;\n\tswitch (tag) {\n\tcase Radius::Small: return st::roundRadiusSmall;\n\tcase Radius::ThumbSmall: return MsgFileThumbRadiusSmall();\n\tcase Radius::ThumbLarge: return MsgFileThumbRadiusLarge();\n\tcase Radius::BubbleSmall: return BubbleRadiusSmall();\n\tcase Radius::BubbleLarge: return BubbleRadiusLarge();\n\t}\n\tUnexpected(\"Radius tag in CachedCornerRadiusValue.\");\n}\n\n[[nodiscard]] const std::array<QImage, 4> &CachedCornersMasks(\n\t\tCachedCornerRadius radius) {\n\tconst auto index = static_cast<int>(radius);\n\tAssert(index >= 0 && index < kCachedCornerRadiusCount);\n\n\tif (CachedMasks[index][0].isNull()) {\n\t\tCachedMasks[index] = Images::CornersMask(\n\t\t\tCachedCornerRadiusValue(CachedCornerRadius(index)));\n\t}\n\treturn CachedMasks[index];\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/cached_round_corners.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rect_part.h\"\n\nenum class ImageRoundRadius;\n\nnamespace Ui {\n\nstruct CornersPixmaps {\n\tQPixmap p[4];\n};\n\nenum CachedRoundCorners : int {\n\tBoxCorners,\n\tMenuCorners,\n\tDateCorners,\n\tOverviewVideoCorners,\n\tOverviewVideoSelectedCorners,\n\tForwardCorners,\n\tMediaviewSaveCorners,\n\tStickerHoverCorners,\n\tBotKeyboardCorners,\n\n\tDoc1Corners,\n\tDoc2Corners,\n\tDoc3Corners,\n\tDoc4Corners,\n\n\tRoundCornersCount\n};\n\nvoid FillRoundRect(QPainter &p, int x, int y, int w, int h, style::color bg, CachedRoundCorners index);\ninline void FillRoundRect(QPainter &p, const QRect &rect, style::color bg, CachedRoundCorners index) {\n\tFillRoundRect(p, rect.x(), rect.y(), rect.width(), rect.height(), bg, index);\n}\n\n[[nodiscard]] const CornersPixmaps &CachedCornerPixmaps(CachedRoundCorners index);\n[[nodiscard]] CornersPixmaps PrepareCornerPixmaps(\n\tint radius,\n\tstyle::color bg,\n\tconst style::color *sh = nullptr);\n[[nodiscard]] CornersPixmaps PrepareCornerPixmaps(\n\tImageRoundRadius radius,\n\tstyle::color bg,\n\tconst style::color *sh = nullptr);\n[[nodiscard]] CornersPixmaps PrepareInvertedCornerPixmaps(\n\tint radius,\n\tstyle::color bg);\nvoid FillRoundRect(QPainter &p, int x, int y, int w, int h, style::color bg, const CornersPixmaps &corners);\ninline void FillRoundRect(QPainter &p, const QRect &rect, style::color bg, const CornersPixmaps &corners) {\n\treturn FillRoundRect(p, rect.x(), rect.y(), rect.width(), rect.height(), bg, corners);\n}\nvoid FillRoundShadow(QPainter &p, int x, int y, int w, int h, style::color shadow, const CornersPixmaps &corners);\ninline void FillRoundShadow(QPainter &p, const QRect &rect, style::color shadow, const CornersPixmaps &corners) {\n\tFillRoundShadow(p, rect.x(), rect.y(), rect.width(), rect.height(), shadow, corners);\n}\n\nenum class CachedCornerRadius {\n\tSmall,\n\tThumbSmall,\n\tThumbLarge,\n\tBubbleSmall,\n\tBubbleLarge,\n\n\tkCount,\n};\n[[nodiscard]] int CachedCornerRadiusValue(CachedCornerRadius tag);\n\n[[nodiscard]] const std::array<QImage, 4> &CachedCornersMasks(\n\tCachedCornerRadius radius);\n\nvoid StartCachedCorners();\nvoid FinishCachedCorners();\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/attach/attach_abstract_single_file_preview.h\"\n\n#include \"base/timer_rpl.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Ui {\n\nAbstractSingleFilePreview::AbstractSingleFilePreview(\n\tQWidget *parent,\n\tconst style::ComposeControls &st,\n\tAttachControls::Type type,\n\tconst Text::MarkedContext &captionContext)\n: AbstractSinglePreview(parent)\n, _st(st)\n, _type(type)\n, _captionContext(captionContext)\n, _editMedia(this, _st.files.buttonFile)\n, _deleteMedia(this, _st.files.buttonFile) {\n\tconst auto repaint = _captionContext.repaint;\n\t_captionContext.repaint = [=] {\n\t\tif (repaint) {\n\t\t\trepaint();\n\t\t}\n\t\tconst auto rect = captionRect();\n\t\tif (rect.isEmpty()) {\n\t\t\tupdate();\n\t\t} else {\n\t\t\tupdate(rect);\n\t\t}\n\t};\n\n\t_editMedia->setIconOverride(&_st.files.buttonFileEdit);\n\t_deleteMedia->setIconOverride(&_st.files.buttonFileDelete);\n\n\tif (type == AttachControls::Type::Full) {\n\t\t_deleteMedia->show();\n\t\t_editMedia->show();\n\t} else if (type == AttachControls::Type::EditOnly) {\n\t\t_deleteMedia->hide();\n\t\t_editMedia->show();\n\t} else if (type == AttachControls::Type::None) {\n\t\t_deleteMedia->hide();\n\t\t_editMedia->hide();\n\t}\n}\n\nAbstractSingleFilePreview::~AbstractSingleFilePreview() = default;\n\nrpl::producer<> AbstractSingleFilePreview::editRequests() const {\n\treturn _editMedia->clicks() | rpl::map([] {\n\t\treturn base::timer_once(st::historyAttach.ripple.hideDuration);\n\t}) | rpl::flatten_latest();\n}\n\nrpl::producer<> AbstractSingleFilePreview::deleteRequests() const {\n\treturn _deleteMedia->clicks() | rpl::to_empty;\n}\n\nrpl::producer<> AbstractSingleFilePreview::modifyRequests() const {\n\treturn rpl::never<>();\n}\n\nvoid AbstractSingleFilePreview::setDisplayName(const QString &displayName) {\n\t_data.name = displayName;\n\tupdateTextWidthFor(_data);\n\tupdateDataGeometry();\n\tupdate();\n}\n\nvoid AbstractSingleFilePreview::setCaption(const TextWithTags &caption) {\n\tauto marked = TextWithEntities{\n\t\tcaption.text,\n\t\tTextUtilities::ConvertTextTagsToEntities(caption.tags),\n\t};\n\tmarked = TextUtilities::SingleLine(marked);\n\t_data.caption.setMarkedText(\n\t\tst::defaultTextStyle,\n\t\tmarked,\n\t\tkMarkupTextOptions,\n\t\t_captionContext);\n\tupdateTextWidthFor(_data);\n\tupdateDataGeometry();\n\tupdate();\n}\n\nvoid AbstractSingleFilePreview::prepareThumbFor(\n\t\tData &data,\n\t\tconst QImage &preview) {\n\tif (preview.isNull()) {\n\t\treturn;\n\t}\n\n\tauto originalWidth = preview.width();\n\tauto originalHeight = preview.height();\n\tconst auto &st = st::attachPreviewThumbLayout;\n\tauto thumbWidth = st.thumbSize;\n\tif (originalWidth > originalHeight) {\n\t\tthumbWidth = (originalWidth * st.thumbSize) / originalHeight;\n\t}\n\tconst auto options = Images::Option::RoundSmall;\n\tdata.fileThumb = PixmapFromImage(Images::Prepare(\n\t\tpreview,\n\t\tthumbWidth * style::DevicePixelRatio(),\n\t\t{ .options = options, .outer = { st.thumbSize, st.thumbSize } }));\n}\n\nvoid AbstractSingleFilePreview::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tconst auto w = width()\n\t\t- st::boxPhotoPadding.left()\n\t\t- st::boxPhotoPadding.right();\n\tconst auto &st = !isThumbedLayout(_data)\n\t\t? st::attachPreviewLayout\n\t\t: st::attachPreviewThumbLayout;\n\tconst auto nameleft = st.thumbSize + st.thumbSkip;\n\tconst auto nametop = st.nameTop;\n\tconst auto statustop = st.statusTop;\n\tconst auto x = (width() - w) / 2, y = 0;\n\n\tif (!isThumbedLayout(_data)) {\n\t\tQRect inner(\n\t\t\tstyle::rtlrect(x, y, st.thumbSize, st.thumbSize, width()));\n\t\tp.setPen(Qt::NoPen);\n\n\t\tif (_data.fileIsAudio && !_data.fileThumb.isNull()) {\n\t\t\tp.drawPixmap(inner.topLeft(), _data.fileThumb);\n\t\t} else {\n\t\t\tp.setBrush(_st.files.iconBg);\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(inner);\n\t\t}\n\t\tauto &icon = _data.fileIsAudio\n\t\t\t? (_data.fileThumb.isNull()\n\t\t\t\t? _st.files.iconPlay\n\t\t\t\t: st::historyFileThumbPlay)\n\t\t\t: _data.fileIsImage\n\t\t\t? _st.files.iconImage\n\t\t\t: _st.files.iconDocument;\n\t\ticon.paintInCenter(p, inner);\n\t} else {\n\t\tQRect rthumb(\n\t\t\tstyle::rtlrect(x, y, st.thumbSize, st.thumbSize, width()));\n\t\tp.drawPixmap(rthumb.topLeft(), _data.fileThumb);\n\t}\n\tp.setFont(st::semiboldFont);\n\tp.setPen(_st.files.nameFg);\n\tp.drawTextLeft(\n\t\tx + nameleft,\n\t\ty + nametop, width(),\n\t\t_data.name,\n\t\t_data.nameWidth);\n\n\tp.setFont(st::normalFont);\n\tp.setPen(_st.files.statusFg);\n\tp.drawTextLeft(\n\t\tx + nameleft,\n\t\ty + statustop,\n\t\twidth(),\n\t\t_data.statusText,\n\t\t_data.statusWidth);\n\tif (!_data.caption.isEmpty()) {\n\t\tp.setPen(_st.files.nameFg);\n\t\tconst auto captionTop = y\n\t\t\t+ st.thumbSize\n\t\t\t+ st::attachPreviewCaptionTopOffset;\n\t\t_data.caption.draw(p, {\n\t\t\t.position = {\n\t\t\t\tx,\n\t\t\t\tcaptionTop,\n\t\t\t},\n\t\t\t.outerWidth = width(),\n\t\t\t.availableWidth = _data.captionAvailableWidth,\n\t\t\t.align = style::al_left,\n\t\t\t.elisionLines = 1,\n\t\t\t.elisionBreakEverywhere = true,\n\t\t});\n\t}\n}\n\nvoid AbstractSingleFilePreview::resizeEvent(QResizeEvent *e) {\n\tconst auto w = width()\n\t\t- st::boxPhotoPadding.left()\n\t\t- st::boxPhotoPadding.right();\n\tconst auto x = (width() - w) / 2;\n\tconst auto top = st::sendBoxFileGroupSkipTop;\n\tauto right = st::sendBoxFileGroupSkipRight + x;\n\tif (_type != AttachControls::Type::EditOnly) {\n\t\t_deleteMedia->moveToRight(right, top);\n\t\tright += st::sendBoxFileGroupEditInternalSkip + _deleteMedia->width();\n\t}\n\t_editMedia->moveToRight(right, top);\n}\n\nbool AbstractSingleFilePreview::isThumbedLayout(const Data &data) const {\n\treturn (!data.fileThumb.isNull() && !data.fileIsAudio);\n}\n\nvoid AbstractSingleFilePreview::updateTextWidthFor(Data &data) {\n\tconst auto &st = !isThumbedLayout(data)\n\t\t? st::attachPreviewLayout\n\t\t: st::attachPreviewThumbLayout;\n\tconst auto buttonsCount = (_type == AttachControls::Type::EditOnly)\n\t\t? 1\n\t\t: (_type == AttachControls::Type::Full)\n\t\t? 2\n\t\t: 0;\n\tconst auto availableFileWidth = st::sendMediaPreviewSize\n\t\t- st.thumbSize\n\t\t- st.thumbSkip\n\t\t// Right buttons.\n\t\t- _st.files.buttonFile.width * buttonsCount\n\t\t- st::sendBoxAlbumGroupEditInternalSkip * buttonsCount\n\t\t- st::sendBoxAlbumGroupSkipRight;\n\tconst auto availableCaptionWidth = st::sendMediaPreviewSize\n\t\t- _st.files.buttonFile.width * buttonsCount\n\t\t- st::sendBoxAlbumGroupEditInternalSkip * buttonsCount\n\t\t- st::sendBoxAlbumGroupSkipRight;\n\tdata.nameWidth = st::semiboldFont->width(data.name);\n\tif (data.nameWidth > availableFileWidth) {\n\t\tdata.name = st::semiboldFont->elided(\n\t\t\tdata.name,\n\t\t\tavailableFileWidth,\n\t\t\tQt::ElideMiddle);\n\t\tdata.nameWidth = st::semiboldFont->width(data.name);\n\t}\n\tdata.statusWidth = st::normalFont->width(data.statusText);\n\tdata.captionAvailableWidth = availableCaptionWidth;\n}\n\nvoid AbstractSingleFilePreview::updateDataGeometry() {\n\tconst auto &st = !isThumbedLayout(_data)\n\t\t? st::attachPreviewLayout\n\t\t: st::attachPreviewThumbLayout;\n\tconst auto height = st.thumbSize + (_data.caption.isEmpty()\n\t\t? 0\n\t\t: (st::attachPreviewCaptionTopOffset + _data.caption.lineHeight()));\n\tresize(width(), height);\n}\n\nQRect AbstractSingleFilePreview::captionRect() const {\n\tif (_data.caption.isEmpty()) {\n\t\treturn {};\n\t}\n\tconst auto w = width()\n\t\t- st::boxPhotoPadding.left()\n\t\t- st::boxPhotoPadding.right();\n\tconst auto &st = !isThumbedLayout(_data)\n\t\t? st::attachPreviewLayout\n\t\t: st::attachPreviewThumbLayout;\n\tconst auto x = (width() - w) / 2;\n\tconst auto captionLineHeight = _data.caption.lineHeight();\n\tconst auto top = st.thumbSize\n\t\t+ st::attachPreviewCaptionTopOffset;\n\treturn QRect(\n\t\tx,\n\t\ttop,\n\t\t_data.captionAvailableWidth,\n\t\tcaptionLineHeight) + st::attachPreviewCaptionRepaintMargin;\n}\n\nvoid AbstractSingleFilePreview::setData(Data data) {\n\t_data = std::move(data);\n\tupdateTextWidthFor(_data);\n\tupdateDataGeometry();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/chat/attach/attach_abstract_single_preview.h\"\n#include \"ui/chat/attach/attach_controls.h\"\n#include \"base/object_ptr.h\"\n\nnamespace style {\nstruct ComposeControls;\n} // namespace style\n\nnamespace Ui {\n\nclass IconButton;\n\nclass AbstractSingleFilePreview : public AbstractSinglePreview {\npublic:\n\tAbstractSingleFilePreview(\n\t\tQWidget *parent,\n\t\tconst style::ComposeControls &st,\n\t\tAttachControls::Type type,\n\t\tconst Text::MarkedContext &captionContext);\n\t~AbstractSingleFilePreview();\n\n\t[[nodiscard]] rpl::producer<> deleteRequests() const override;\n\t[[nodiscard]] rpl::producer<> editRequests() const override;\n\t[[nodiscard]] rpl::producer<> modifyRequests() const override;\n\tvirtual void setDisplayName(const QString &displayName);\n\tvirtual void setCaption(const TextWithTags &caption);\n\nprotected:\n\tstruct Data {\n\t\tQPixmap fileThumb;\n\t\tQString name;\n\t\tQString statusText;\n\t\tText::String caption;\n\t\tint nameWidth = 0;\n\t\tint statusWidth = 0;\n\t\tint captionAvailableWidth = 0;\n\t\tbool fileIsAudio = false;\n\t\tbool fileIsImage = false;\n\t};\n\n\tvoid prepareThumbFor(Data &data, const QImage &preview);\n\tbool isThumbedLayout(const Data &data) const;\n\t[[nodiscard]] const Text::MarkedContext &captionContext() const {\n\t\treturn _captionContext;\n\t}\n\n\tvoid setData(Data data);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\tvoid updateTextWidthFor(Data &data);\n\tvoid updateDataGeometry();\n\t[[nodiscard]] QRect captionRect() const;\n\n\tconst style::ComposeControls &_st;\n\tconst AttachControls::Type _type;\n\tText::MarkedContext _captionContext;\n\n\tData _data;\n\n\tobject_ptr<IconButton> _editMedia = { nullptr };\n\tobject_ptr<IconButton> _deleteMedia = { nullptr };\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/attach/attach_abstract_single_media_preview.h\"\n\n#include \"editor/photo_editor_common.h\"\n#include \"ui/chat/attach/attach_controls.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kMinPreviewWidth = 20;\n\n} // namespace\n\nAbstractSingleMediaPreview::AbstractSingleMediaPreview(\n\tQWidget *parent,\n\tconst style::ComposeControls &st,\n\tAttachControls::Type type)\n: AbstractSinglePreview(parent)\n, _st(st)\n, _minThumbH(st::sendBoxAlbumGroupSize.height()\n\t+ st::sendBoxAlbumGroupSkipTop * 2)\n, _controls(base::make_unique_q<AttachControlsWidget>(this, type)) {\n}\n\nAbstractSingleMediaPreview::~AbstractSingleMediaPreview() = default;\n\nrpl::producer<> AbstractSingleMediaPreview::deleteRequests() const {\n\treturn _controls->deleteRequests();\n}\n\nrpl::producer<> AbstractSingleMediaPreview::editRequests() const {\n\treturn _controls->editRequests();\n}\n\nrpl::producer<> AbstractSingleMediaPreview::modifyRequests() const {\n\treturn _photoEditorRequests.events();\n}\n\nvoid AbstractSingleMediaPreview::setSendWay(SendFilesWay way) {\n\t_sendWay = way;\n\tupdate();\n}\n\nSendFilesWay AbstractSingleMediaPreview::sendWay() const {\n\treturn _sendWay;\n}\n\nvoid AbstractSingleMediaPreview::setSpoiler(bool spoiler) {\n\t_spoiler = spoiler\n\t\t? std::make_unique<SpoilerAnimation>([=] { update(); })\n\t\t: nullptr;\n\tupdate();\n}\n\nvoid AbstractSingleMediaPreview::setCanShowHighQualityBadge(bool value) {\n\t_canShowHighQualityBadge = value;\n\tupdate();\n}\n\nbool AbstractSingleMediaPreview::hasSpoiler() const {\n\treturn _spoiler != nullptr;\n}\n\nbool AbstractSingleMediaPreview::canHaveSpoiler() const {\n\treturn supportsSpoilers();\n}\n\nQImage AbstractSingleMediaPreview::generatePriceTagBackground() const {\n\treturn (_previewBlurred.isNull() ? _preview : _previewBlurred).toImage();\n}\n\nvoid AbstractSingleMediaPreview::preparePreview(QImage preview) {\n\tauto maxW = 0;\n\tauto maxH = 0;\n\tif (_animated && drawBackground()) {\n\t\tauto limitW = st::sendMediaPreviewSize;\n\t\tauto limitH = st::confirmMaxHeight;\n\t\tmaxW = qMax(preview.width(), 1);\n\t\tmaxH = qMax(preview.height(), 1);\n\t\tif (maxW * limitH > maxH * limitW) {\n\t\t\tif (maxW < limitW) {\n\t\t\t\tmaxH = maxH * limitW / maxW;\n\t\t\t\tmaxW = limitW;\n\t\t\t}\n\t\t} else {\n\t\t\tif (maxH < limitH) {\n\t\t\t\tmaxW = maxW * limitH / maxH;\n\t\t\t\tmaxH = limitH;\n\t\t\t}\n\t\t}\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tpreview = Images::Prepare(\n\t\t\tstd::move(preview),\n\t\t\tQSize(maxW, maxH) * ratio,\n\t\t\t{ .outer = { maxW, maxH } });\n\t}\n\tauto originalWidth = preview.width();\n\tauto originalHeight = preview.height();\n\tif (!originalWidth || !originalHeight) {\n\t\toriginalWidth = originalHeight = 1;\n\t}\n\t_previewWidth = st::sendMediaPreviewSize;\n\tif (preview.width() < _previewWidth) {\n\t\t_previewWidth = qMax(preview.width(), kMinPreviewWidth);\n\t}\n\tauto maxthumbh = qMin(qRound(1.5 * _previewWidth), st::confirmMaxHeight);\n\t_previewHeight = qRound(originalHeight\n\t\t* float64(_previewWidth)\n\t\t/ originalWidth);\n\tif (_previewHeight > maxthumbh) {\n\t\t_previewWidth = qRound(_previewWidth\n\t\t\t* float64(maxthumbh)\n\t\t\t/ _previewHeight);\n\t\taccumulate_max(_previewWidth, kMinPreviewWidth);\n\t\t_previewHeight = maxthumbh;\n\t}\n\t_previewLeft = (st::boxWideWidth - _previewWidth) / 2;\n\tif (_previewHeight < _minThumbH) {\n\t\t_previewTop = (_minThumbH - _previewHeight) / 2;\n\t}\n\n\tpreview = std::move(preview).scaled(\n\t\t_previewWidth * style::DevicePixelRatio(),\n\t\t_previewHeight * style::DevicePixelRatio(),\n\t\tQt::IgnoreAspectRatio,\n\t\tQt::SmoothTransformation);\n\tpreview = Images::Opaque(std::move(preview));\n\t_preview = PixmapFromImage(std::move(preview));\n\t_preview.setDevicePixelRatio(style::DevicePixelRatio());\n\t_previewBlurred = QPixmap();\n\n\tresize(width(), std::max(_previewHeight, _minThumbH));\n}\n\nbool AbstractSingleMediaPreview::isOverPreview(QPoint position) const {\n\treturn QRect(\n\t\t_previewLeft,\n\t\t_previewTop,\n\t\t_previewWidth,\n\t\t_previewHeight).contains(position);\n}\n\nvoid AbstractSingleMediaPreview::resizeEvent(QResizeEvent *e) {\n\t_controls->moveToRight(\n\t\tst::boxPhotoPadding.right() + st::sendBoxAlbumGroupSkipRight,\n\t\tst::sendBoxAlbumGroupSkipTop,\n\t\twidth());\n}\n\nvoid AbstractSingleMediaPreview::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tauto takenSpoiler = supportsSpoilers()\n\t\t? nullptr\n\t\t: base::take(_spoiler);\n\tconst auto guard = gsl::finally([&] {\n\t\tif (takenSpoiler) {\n\t\t\t_spoiler = base::take(takenSpoiler);\n\t\t}\n\t});\n\n\tif (drawBackground()) {\n\t\tconst auto &padding = st::boxPhotoPadding;\n\t\tif (_previewLeft > padding.left()) {\n\t\t\tp.fillRect(\n\t\t\t\tpadding.left(),\n\t\t\t\t_previewTop,\n\t\t\t\t_previewLeft - padding.left(),\n\t\t\t\t_previewHeight,\n\t\t\t\t_st.files.confirmBg);\n\t\t}\n\t\tif ((_previewLeft + _previewWidth) < (width() - padding.right())) {\n\t\t\tp.fillRect(\n\t\t\t\t_previewLeft + _previewWidth,\n\t\t\t\t_previewTop,\n\t\t\t\twidth() - padding.right() - _previewLeft - _previewWidth,\n\t\t\t\t_previewHeight,\n\t\t\t\t_st.files.confirmBg);\n\t\t}\n\t\tif (_previewTop > 0) {\n\t\t\tp.fillRect(\n\t\t\t\tpadding.left(),\n\t\t\t\t0,\n\t\t\t\twidth() - padding.right() - padding.left(),\n\t\t\t\theight(),\n\t\t\t\t_st.files.confirmBg);\n\t\t}\n\t}\n\n\tif (_spoiler && _previewBlurred.isNull()) {\n\t\t_previewBlurred = BlurredPreviewFromPixmap(_preview, RectPart::None);\n\t}\n\tif (_spoiler || !tryPaintAnimation(p)) {\n\t\tconst auto &pixmap = _spoiler ? _previewBlurred : _preview;\n\t\tconst auto position = QPoint(_previewLeft, _previewTop);\n\t\tp.drawPixmap(position, pixmap);\n\t\tif (_spoiler) {\n\t\t\tconst auto paused = On(PowerSaving::kChatSpoiler);\n\t\t\tFillSpoilerRect(\n\t\t\t\tp,\n\t\t\t\tQRect(position, pixmap.size() / pixmap.devicePixelRatio()),\n\t\t\t\tDefaultImageSpoiler().frame(\n\t\t\t\t\t_spoiler->index(crl::now(), paused)));\n\t\t}\n\t}\n\tif (_animated && !isAnimatedPreviewReady() && !_spoiler) {\n\t\tconst auto innerSize = st::msgFileLayout.thumbSize;\n\t\tauto inner = QRect(\n\t\t\t_previewLeft + (_previewWidth - innerSize) / 2,\n\t\t\t_previewTop + (_previewHeight - innerSize) / 2,\n\t\t\tinnerSize,\n\t\t\tinnerSize);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::msgDateImgBg);\n\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(inner);\n\t\t}\n\n\t\tauto icon = &st::historyFileInPlay;\n\t\ticon->paintInCenter(p, inner);\n\t}\n\tif (_canShowHighQualityBadge && _sendWay.sendLargePhotos()) {\n\t\tPaintHighQualityBadge(\n\t\t\tp,\n\t\t\t_st,\n\t\t\tQRect(_previewLeft, _previewTop, _previewWidth, _previewHeight));\n\t}\n}\n\nvoid AbstractSingleMediaPreview::mousePressEvent(QMouseEvent *e) {\n\tif (isOverPreview(e->pos())) {\n\t\t_pressed = true;\n\t}\n}\n\nvoid AbstractSingleMediaPreview::mouseMoveEvent(QMouseEvent *e) {\n\tapplyCursor((isPhoto() && isOverPreview(e->pos()))\n\t\t? style::cur_pointer\n\t\t: style::cur_default);\n}\n\nvoid AbstractSingleMediaPreview::mouseReleaseEvent(QMouseEvent *e) {\n\tif (base::take(_pressed) && isOverPreview(e->pos())) {\n\t\tif (e->button() == Qt::LeftButton && isPhoto()) {\n\t\t\t_photoEditorRequests.fire({});\n\t\t}\n\t}\n}\n\nvoid AbstractSingleMediaPreview::applyCursor(style::cursor cursor) {\n\tif (_cursor != cursor) {\n\t\t_cursor = cursor;\n\t\tsetCursor(_cursor);\n\t}\n}\n\nint AbstractSingleMediaPreview::previewLeft() const {\n\treturn _previewLeft;\n}\n\nint AbstractSingleMediaPreview::previewTop() const {\n\treturn _previewTop;\n}\n\nint AbstractSingleMediaPreview::previewWidth() const {\n\treturn _previewWidth;\n}\n\nint AbstractSingleMediaPreview::previewHeight() const {\n\treturn _previewHeight;\n}\n\nvoid AbstractSingleMediaPreview::setAnimated(bool animated) {\n\t_animated = animated;\n}\n\nbool AbstractSingleMediaPreview::isPhoto() const {\n\treturn drawBackground()\n\t\t&& !isAnimatedPreviewReady()\n\t\t&& !_animated;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/chat/attach/attach_abstract_single_preview.h\"\n#include \"ui/chat/attach/attach_controls.h\"\n#include \"ui/chat/attach/attach_send_files_way.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/abstract_button.h\"\n\nnamespace style {\nstruct ComposeControls;\n} // namespace style\n\nnamespace Ui {\n\nclass AbstractSingleMediaPreview : public AbstractSinglePreview {\npublic:\n\tAbstractSingleMediaPreview(\n\t\tQWidget *parent,\n\t\tconst style::ComposeControls &st,\n\t\tAttachControls::Type type);\n\t~AbstractSingleMediaPreview();\n\n\tvoid setSendWay(SendFilesWay way);\n\t[[nodiscard]] SendFilesWay sendWay() const;\n\n\t[[nodiscard]] rpl::producer<> deleteRequests() const override;\n\t[[nodiscard]] rpl::producer<> editRequests() const override;\n\t[[nodiscard]] rpl::producer<> modifyRequests() const override;\n\n\t[[nodiscard]] bool isPhoto() const;\n\n\tvoid setSpoiler(bool spoiler);\n\tvoid setCanShowHighQualityBadge(bool value);\n\t[[nodiscard]] bool hasSpoiler() const;\n\t[[nodiscard]] bool canHaveSpoiler() const;\n\n\t[[nodiscard]] QImage generatePriceTagBackground() const;\n\nprotected:\n\tvirtual bool supportsSpoilers() const = 0;\n\tvirtual bool drawBackground() const = 0;\n\tvirtual bool tryPaintAnimation(QPainter &p) = 0;\n\tvirtual bool isAnimatedPreviewReady() const = 0;\n\n\tvoid preparePreview(QImage preview);\n\n\tint previewLeft() const;\n\tint previewTop() const;\n\tint previewWidth() const;\n\tint previewHeight() const;\n\n\tvoid setAnimated(bool animated);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\n\t[[nodiscard]] bool isOverPreview(QPoint position) const;\n\tvoid applyCursor(style::cursor cursor);\n\n\tconst style::ComposeControls &_st;\n\tSendFilesWay _sendWay;\n\tbool _animated = false;\n\tQPixmap _preview;\n\tQPixmap _previewBlurred;\n\tint _previewLeft = 0;\n\tint _previewTop = 0;\n\tint _previewWidth = 0;\n\tint _previewHeight = 0;\n\n\tstd::unique_ptr<SpoilerAnimation> _spoiler;\n\tbool _canShowHighQualityBadge = false;\n\n\tconst int _minThumbH;\n\tconst base::unique_qptr<AttachControlsWidget> _controls;\n\trpl::event_stream<> _photoEditorRequests;\n\n\tstyle::cursor _cursor = style::cur_default;\n\tbool _pressed = false;\n\n\trpl::event_stream<> _modifyRequests;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\n\nclass AbstractSinglePreview : public RpWidget {\npublic:\n\tusing RpWidget::RpWidget;\n\n\t[[nodiscard]] virtual rpl::producer<> deleteRequests() const = 0;\n\t[[nodiscard]] virtual rpl::producer<> editRequests() const = 0;\n\t[[nodiscard]] virtual rpl::producer<> modifyRequests() const = 0;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/attach/attach_album_preview.h\"\n\n#include \"ui/chat/attach/attach_album_thumbnail.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace Media::Streaming {\n\n[[nodiscard]] QImage PrepareBlurredBackground(QSize outer, QImage frame);\n\n} // namespace Media::Streaming\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kDragDuration = crl::time(200);\n\n} // namespace\n\nAlbumPreview::AlbumPreview(\n\tQWidget *parent,\n\tconst style::ComposeControls &st,\n\tgsl::span<Ui::PreparedFile> items,\n\tconst Text::MarkedContext &captionContext,\n\tSendFilesWay way)\n: RpWidget(parent)\n, _st(st)\n, _captionContext(captionContext)\n, _sendWay(way)\n, _dragTimer([=] { switchToDrag(); }) {\n\tsetMouseTracking(true);\n\tprepareThumbs(items);\n\tupdateSize();\n\tupdateFileRows();\n}\n\nAlbumPreview::~AlbumPreview() = default;\n\nvoid AlbumPreview::setSendWay(SendFilesWay way) {\n\tif (_sendWay != way) {\n\t\tcancelDrag();\n\t\t_sendWay = way;\n\t}\n\tupdateSize();\n\tupdateFileRows();\n\tupdate();\n}\n\nvoid AlbumPreview::setCaption(int index, const TextWithTags &caption) {\n\tif (index < 0 || index >= _thumbs.size()) {\n\t\treturn;\n\t}\n\tconst auto realIndex = _order[index];\n\tconst auto oldHeight = _thumbs[realIndex]->fileHeight();\n\t_thumbs[realIndex]->setCaption(caption);\n\tconst auto newHeight = _thumbs[realIndex]->fileHeight();\n\tif (oldHeight == newHeight) {\n\t\treturn;\n\t}\n\tconst auto firstFileHeight = _thumbs.front()->fileHeight();\n\t_hasMixedFileHeights = ranges::any_of(\n\t\t_thumbs,\n\t\t[=](const auto &thumb) {\n\t\t\treturn thumb->fileHeight() != firstFileHeight;\n\t\t});\n\t_filesHeight = ranges::accumulate(ranges::views::all(\n\t\t_thumbs\n\t) | ranges::views::transform([](const auto &thumb) {\n\t\treturn thumb->fileHeight();\n\t}), 0) + (int(_thumbs.size()) - 1) * st::sendMediaRowSkip;\n\tupdateSize();\n\tupdateFileRows();\n}\n\nint AlbumPreview::indexFromPoint(QPoint position) const {\n\tconst auto thumb = findThumb(position);\n\treturn thumb ? orderIndex(thumb) : -1;\n}\n\nvoid AlbumPreview::updateFileRows() {\n\tExpects(_order.size() == _thumbs.size());\n\n\tconst auto isFile = !_sendWay.sendImagesAsPhotos();\n\tauto top = 0;\n\tfor (auto i = 0; i < _order.size(); i++) {\n\t\tconst auto &thumb = _thumbs[_order[i]];\n\t\tthumb->setButtonVisible(isFile && !thumb->isCompressedSticker());\n\t\tthumb->moveButtons(top);\n\t\ttop += thumb->fileHeight() + st::sendMediaRowSkip;\n\t}\n}\n\nbase::flat_set<int> AlbumPreview::collectSpoileredIndices() {\n\tauto result = base::flat_set<int>();\n\tresult.reserve(_thumbs.size());\n\tauto i = 0;\n\tfor (const auto &thumb : _thumbs) {\n\t\tif (thumb->hasSpoiler()) {\n\t\t\tresult.emplace(i);\n\t\t}\n\t\t++i;\n\t}\n\treturn result;\n}\n\nbool AlbumPreview::canHaveSpoiler(int index) const {\n\treturn _sendWay.sendImagesAsPhotos();\n}\n\nvoid AlbumPreview::toggleSpoilers(bool enabled) {\n\tfor (auto &thumb : _thumbs) {\n\t\tthumb->setSpoiler(enabled);\n\t}\n}\n\nstd::vector<int> AlbumPreview::takeOrder() {\n\t//Expects(_thumbs.size() == _order.size());\n\t//Expects(_itemsShownDimensions.size() == _order.size());\n\n\tauto reordered = std::vector<std::unique_ptr<AlbumThumbnail>>();\n\tauto reorderedShownDimensions = std::vector<QSize>();\n\treordered.reserve(_thumbs.size());\n\treorderedShownDimensions.reserve(_itemsShownDimensions.size());\n\tfor (auto index : _order) {\n\t\treordered.push_back(std::move(_thumbs[index]));\n\t\treorderedShownDimensions.push_back(_itemsShownDimensions[index]);\n\t}\n\t_thumbs = std::move(reordered);\n\t_itemsShownDimensions = std::move(reorderedShownDimensions);\n\treturn std::exchange(_order, defaultOrder());\n}\n\nauto AlbumPreview::generateOrderedLayout() const\n-> std::vector<GroupMediaLayout> {\n\tauto layout = LayoutMediaGroup(\n\t\t_itemsShownDimensions,\n\t\tst::sendMediaPreviewSize,\n\t\tst::historyGroupWidthMin / 2,\n\t\tst::historyGroupSkip / 2);\n\tAssert(layout.size() == _order.size());\n\treturn layout;\n}\n\nstd::vector<int> AlbumPreview::defaultOrder(int count) const {\n\tif (count < 0) {\n\t\tcount = _order.size();\n\t}\n\treturn ranges::views::ints(0, count) | ranges::to_vector;\n}\n\nvoid AlbumPreview::prepareThumbs(gsl::span<Ui::PreparedFile> items) {\n\t_order = defaultOrder(items.size());\n\t_itemsShownDimensions = ranges::views::all(\n\t\t_order\n\t) | ranges::views::transform([&](int index) {\n\t\treturn items[index].shownDimensions;\n\t}) | ranges::to_vector;\n\n\tconst auto count = int(_order.size());\n\tconst auto layout = generateOrderedLayout();\n\t_thumbs.reserve(count);\n\tfor (auto i = 0; i != count; ++i) {\n\t\t_thumbs.push_back(std::make_unique<AlbumThumbnail>(\n\t\t\t_st,\n\t\t\titems[i],\n\t\t\t_captionContext,\n\t\t\tlayout[i],\n\t\t\tthis,\n\t\t\t[=] { update(); },\n\t\t\t[=](QRect rect) { update(rect); },\n\t\t\t[=] { changeThumbByIndex(orderIndex(thumbUnderCursor())); },\n\t\t\t[=] { deleteThumbByIndex(orderIndex(thumbUnderCursor())); }));\n\t\tif (_thumbs.back()->isCompressedSticker()) {\n\t\t\t_hasMixedFileHeights = true;\n\t\t}\n\t}\n\t_thumbsHeight = countLayoutHeight(layout);\n\t_photosHeight = ranges::accumulate(ranges::views::all(\n\t\t_thumbs\n\t) | ranges::views::transform([](const auto &thumb) {\n\t\treturn thumb->photoHeight();\n\t}), 0) + (count - 1) * st::sendMediaRowSkip;\n\n\tconst auto firstFileHeight = _thumbs.front()->fileHeight();\n\t_hasMixedFileHeights = _hasMixedFileHeights || ranges::any_of(\n\t\t_thumbs,\n\t\t[=](const auto &thumb) {\n\t\t\treturn thumb->fileHeight() != firstFileHeight;\n\t\t});\n\t_filesHeight = ranges::accumulate(ranges::views::all(\n\t\t_thumbs\n\t) | ranges::views::transform([](const auto &thumb) {\n\t\treturn thumb->fileHeight();\n\t}), 0) + (count - 1) * st::sendMediaRowSkip;\n}\n\nint AlbumPreview::contentLeft() const {\n\treturn (st::boxWideWidth - st::sendMediaPreviewSize) / 2;\n}\n\nint AlbumPreview::contentTop() const {\n\treturn 0;\n}\n\nAlbumThumbnail *AlbumPreview::findThumb(QPoint position) const {\n\tposition -= QPoint(contentLeft(), contentTop());\n\n\tauto top = 0;\n\tconst auto isPhotosWay = _sendWay.sendImagesAsPhotos();\n\tconst auto skip = st::sendMediaRowSkip;\n\tauto find = [&](const auto &thumb) {\n\t\tif (_sendWay.groupFiles() && _sendWay.sendImagesAsPhotos()) {\n\t\t\treturn thumb->containsPoint(position);\n\t\t} else {\n\t\t\tconst auto bottom = top + (isPhotosWay\n\t\t\t\t? thumb->photoHeight()\n\t\t\t\t: thumb->fileHeight());\n\t\t\tconst auto isUnderTop = (position.y() > top);\n\t\t\ttop = bottom + skip;\n\t\t\treturn isUnderTop && (position.y() < bottom);\n\t\t}\n\t\treturn false;\n\t};\n\n\tconst auto i = ranges::find_if(_thumbs, std::move(find));\n\treturn (i == _thumbs.end()) ? nullptr : i->get();\n}\n\nnot_null<AlbumThumbnail*> AlbumPreview::findClosestThumb(\n\tQPoint position) const {\n\tExpects(_draggedThumb != nullptr);\n\n\tif (const auto exact = findThumb(position)) {\n\t\treturn exact;\n\t}\n\tauto result = _draggedThumb;\n\tauto distance = _draggedThumb->distanceTo(position);\n\tfor (const auto &thumb : _thumbs) {\n\t\tconst auto check = thumb->distanceTo(position);\n\t\tif (check < distance) {\n\t\t\tdistance = check;\n\t\t\tresult = thumb.get();\n\t\t}\n\t}\n\treturn result;\n}\n\nint AlbumPreview::orderIndex(not_null<AlbumThumbnail*> thumb) const {\n\tconst auto i = ranges::find_if(_order, [&](int index) {\n\t\treturn (_thumbs[index].get() == thumb);\n\t});\n\tAssert(i != _order.end());\n\treturn int(i - _order.begin());\n}\n\nvoid AlbumPreview::cancelDrag() {\n\t_thumbsHeightAnimation.stop();\n\t_finishDragAnimation.stop();\n\t_shrinkAnimation.stop();\n\tif (_draggedThumb) {\n\t\t_draggedThumb->moveInAlbum({ 0, 0 });\n\t\t_draggedThumb = nullptr;\n\t}\n\tif (_suggestedThumb) {\n\t\tconst auto suggestedIndex = orderIndex(_suggestedThumb);\n\t\tif (suggestedIndex > 0) {\n\t\t\t_thumbs[_order[suggestedIndex - 1]]->suggestMove(0., [] {});\n\t\t}\n\t\tif (suggestedIndex < int(_order.size() - 1)) {\n\t\t\t_thumbs[_order[suggestedIndex + 1]]->suggestMove(0., [] {});\n\t\t}\n\t\t_suggestedThumb->suggestMove(0., [] {});\n\t\t_suggestedThumb->finishAnimations();\n\t\t_suggestedThumb = nullptr;\n\t}\n\t_paintedAbove = nullptr;\n\tupdate();\n}\n\nvoid AlbumPreview::finishDrag() {\n\tExpects(_draggedThumb != nullptr);\n\tExpects(_suggestedThumb != nullptr);\n\n\tif (_suggestedThumb != _draggedThumb) {\n\t\tconst auto currentIndex = orderIndex(_draggedThumb);\n\t\tconst auto newIndex = orderIndex(_suggestedThumb);\n\t\tconst auto delta = (currentIndex < newIndex) ? 1 : -1;\n\t\tconst auto realIndex = _order[currentIndex];\n\t\tfor (auto i = currentIndex; i != newIndex; i += delta) {\n\t\t\t_order[i] = _order[i + delta];\n\t\t}\n\t\t_order[newIndex] = realIndex;\n\t\tconst auto layout = generateOrderedLayout();\n\t\tfor (auto i = 0, count = int(_order.size()); i != count; ++i) {\n\t\t\t_thumbs[_order[i]]->moveToLayout(layout[i]);\n\t\t}\n\t\t_finishDragAnimation.start([=] { update(); }, 0., 1., kDragDuration);\n\n\t\tupdateSizeAnimated(layout);\n\t\t_orderUpdated.fire({});\n\t} else {\n\t\tfor (const auto &thumb : _thumbs) {\n\t\t\tthumb->resetLayoutAnimation();\n\t\t}\n\t\t_draggedThumb->animateLayoutToInitial();\n\t\t_finishDragAnimation.start([=] { update(); }, 0., 1., kDragDuration);\n\t}\n}\n\nint AlbumPreview::countLayoutHeight(\n\tconst std::vector<GroupMediaLayout> &layout) const {\n\tconst auto accumulator = [](int current, const auto &item) {\n\t\treturn std::max(current, item.geometry.y() + item.geometry.height());\n\t};\n\treturn ranges::accumulate(layout, 0, accumulator);\n}\n\nvoid AlbumPreview::updateSizeAnimated(\n\t\tconst std::vector<GroupMediaLayout> &layout) {\n\tconst auto newHeight = countLayoutHeight(layout);\n\tif (newHeight != _thumbsHeight) {\n\t\t_thumbsHeightAnimation.start(\n\t\t\t[=] { updateSize(); },\n\t\t\t_thumbsHeight,\n\t\t\tnewHeight,\n\t\t\tkDragDuration);\n\t\t_thumbsHeight = newHeight;\n\t}\n}\n\nvoid AlbumPreview::updateSize() {\n\tconst auto newHeight = [&] {\n\t\tif (!_sendWay.sendImagesAsPhotos()) {\n\t\t\treturn _filesHeight;\n\t\t} else if (!_sendWay.groupFiles()) {\n\t\t\treturn _photosHeight;\n\t\t} else {\n\t\t\treturn int(base::SafeRound(_thumbsHeightAnimation.value(\n\t\t\t\t_thumbsHeight)));\n\t\t}\n\t}();\n\tif (height() != newHeight) {\n\t\tresize(st::boxWideWidth, newHeight);\n\t}\n}\n\nvoid AlbumPreview::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tif (!_sendWay.sendImagesAsPhotos()) {\n\t\tpaintFiles(p, e->rect());\n\t} else if (!_sendWay.groupFiles()) {\n\t\tpaintPhotos(p, e->rect());\n\t} else {\n\t\tpaintAlbum(p);\n\t}\n}\n\nvoid AlbumPreview::paintAlbum(Painter &p) const {\n\tconst auto shrink = _shrinkAnimation.value(_draggedThumb ? 1. : 0.);\n\tconst auto moveProgress = _finishDragAnimation.value(1.);\n\tconst auto left = contentLeft();\n\tconst auto top = contentTop();\n\tfor (const auto &thumb : _thumbs) {\n\t\tif (thumb.get() != _paintedAbove) {\n\t\t\tthumb->paintInAlbum(\n\t\t\t\tp,\n\t\t\t\tleft,\n\t\t\t\ttop,\n\t\t\t\tshrink,\n\t\t\t\tmoveProgress,\n\t\t\t\t_sendWay.sendLargePhotos());\n\t\t}\n\t}\n\tif (_paintedAbove) {\n\t\t_paintedAbove->paintInAlbum(\n\t\t\tp,\n\t\t\tleft,\n\t\t\ttop,\n\t\t\tshrink,\n\t\t\tmoveProgress,\n\t\t\t_sendWay.sendLargePhotos());\n\t}\n}\n\nvoid AlbumPreview::paintPhotos(Painter &p, QRect clip) const {\n\tconst auto left = (st::boxWideWidth - st::sendMediaPreviewSize) / 2;\n\tauto top = 0;\n\tconst auto outerWidth = width();\n\tfor (const auto &thumb : _thumbs) {\n\t\tconst auto bottom = top + thumb->photoHeight();\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\ttop = bottom + st::sendMediaRowSkip;\n\t\t});\n\t\tif (top >= clip.y() + clip.height()) {\n\t\t\tbreak;\n\t\t} else if (bottom <= clip.y()) {\n\t\t\tcontinue;\n\t\t}\n\t\tthumb->paintPhoto(\n\t\t\tp,\n\t\t\tleft,\n\t\t\ttop,\n\t\t\touterWidth,\n\t\t\t_sendWay.sendLargePhotos());\n\t}\n}\n\nvoid AlbumPreview::paintFiles(Painter &p, QRect clip) const {\n\tconst auto left = (st::boxWideWidth - st::sendMediaPreviewSize) / 2;\n\tconst auto outerWidth = width();\n\tif (!_hasMixedFileHeights) {\n\t\tconst auto fileHeight = _thumbs.front()->fileHeight()\n\t\t\t+ st::sendMediaRowSkip;\n\t\tconst auto bottom = clip.y() + clip.height();\n\t\tconst auto from = std::clamp(\n\t\t\tclip.y() / fileHeight,\n\t\t\t0,\n\t\t\tint(_thumbs.size()));\n\t\tconst auto till = std::clamp(\n\t\t\t(bottom + fileHeight - 1) / fileHeight,\n\t\t\t0,\n\t\t\tint(_thumbs.size()));\n\n\t\tauto top = from * fileHeight;\n\t\tfor (auto i = from; i != till; ++i) {\n\t\t\t_thumbs[i]->paintFile(p, left, top, outerWidth);\n\t\t\ttop += fileHeight;\n\t\t}\n\t} else {\n\t\tauto top = 0;\n\t\tfor (const auto &thumb : _thumbs) {\n\t\t\tconst auto bottom = top + thumb->fileHeight();\n\t\t\tconst auto guard = gsl::finally([&] {\n\t\t\t\ttop = bottom + st::sendMediaRowSkip;\n\t\t\t});\n\t\t\tif (top >= clip.y() + clip.height()) {\n\t\t\t\tbreak;\n\t\t\t} else if (bottom <= clip.y()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthumb->paintFile(p, left, top, outerWidth);\n\t\t}\n\t}\n}\n\nAlbumThumbnail *AlbumPreview::thumbUnderCursor() {\n\treturn findThumb(mapFromGlobal(QCursor::pos()));\n}\n\nvoid AlbumPreview::deleteThumbByIndex(int index) {\n\tif (index < 0) {\n\t\treturn;\n\t}\n\t_thumbDeleted.fire(std::move(index));\n}\n\nvoid AlbumPreview::changeThumbByIndex(int index) {\n\tif (index < 0) {\n\t\treturn;\n\t}\n\t_thumbChanged.fire(std::move(index));\n}\n\nvoid AlbumPreview::modifyThumbByIndex(int index) {\n\tif (index < 0) {\n\t\treturn;\n\t}\n\t_thumbModified.fire(std::move(index));\n}\n\nvoid AlbumPreview::thumbButtonsCallback(\n\t\tnot_null<AlbumThumbnail*> thumb,\n\t\tAttachButtonType type) {\n\tconst auto index = orderIndex(thumb);\n\n\tswitch (type) {\n\tcase AttachButtonType::None: return;\n\tcase AttachButtonType::Edit: changeThumbByIndex(index); break;\n\tcase AttachButtonType::Delete: deleteThumbByIndex(index); break;\n\tcase AttachButtonType::Modify:\n\t\tcancelDrag();\n\t\tmodifyThumbByIndex(index);\n\t\tbreak;\n\t}\n}\n\nvoid AlbumPreview::mousePressEvent(QMouseEvent *e) {\n\tif (_finishDragAnimation.animating()) {\n\t\treturn;\n\t}\n\tconst auto position = e->pos();\n\tcancelDrag();\n\tif (const auto thumb = findThumb(position)) {\n\t\t_draggedStartPosition = position;\n\t\t_pressedThumb = thumb;\n\t\t_pressedButtonType = thumb->buttonTypeFromPoint(position);\n\n\t\tconst auto isAlbum = _sendWay.sendImagesAsPhotos()\n\t\t\t&& _sendWay.groupFiles();\n\t\tif (!isAlbum || e->button() != Qt::LeftButton) {\n\t\t\t_dragTimer.cancel();\n\t\t\treturn;\n\t\t}\n\n\t\tif (_pressedButtonType == AttachButtonType::None) {\n\t\t\tswitchToDrag();\n\t\t} else if (_pressedButtonType == AttachButtonType::Modify) {\n\t\t\t_dragTimer.callOnce(QApplication::startDragTime());\n\t\t}\n\t}\n}\n\nvoid AlbumPreview::mouseMoveEvent(QMouseEvent *e) {\n\tif (!_sendWay.sendImagesAsPhotos() && !_hasMixedFileHeights) {\n\t\tapplyCursor(style::cur_default);\n\t\treturn;\n\t}\n\tif (_dragTimer.isActive()) {\n\t\t_dragTimer.cancel();\n\t\tswitchToDrag();\n\t}\n\tconst auto isAlbum = _sendWay.sendImagesAsPhotos()\n\t\t&& _sendWay.groupFiles();\n\tif (isAlbum && _draggedThumb) {\n\t\tconst auto position = e->pos();\n\t\t_draggedThumb->moveInAlbum(position - _draggedStartPosition);\n\t\tupdateSuggestedDrag(_draggedThumb->center());\n\t\tupdate();\n\t} else {\n\t\tconst auto thumb = findThumb(e->pos());\n\t\tconst auto regularCursor = isAlbum\n\t\t\t? style::cur_pointer\n\t\t\t: style::cur_default;\n\t\tconst auto cursor = thumb\n\t\t\t? (thumb->buttonsContainPoint(e->pos())\n\t\t\t\t? style::cur_pointer\n\t\t\t\t: regularCursor)\n\t\t\t: style::cur_default;\n\t\tapplyCursor(cursor);\n\t}\n}\n\nvoid AlbumPreview::applyCursor(style::cursor cursor) {\n\tif (_cursor != cursor) {\n\t\t_cursor = cursor;\n\t\tsetCursor(_cursor);\n\t}\n}\n\nvoid AlbumPreview::updateSuggestedDrag(QPoint position) {\n\tauto closest = findClosestThumb(position);\n\tauto closestIndex = orderIndex(closest);\n\n\tconst auto draggedIndex = orderIndex(_draggedThumb);\n\tconst auto closestIsBeforePoint = closest->isPointAfter(position);\n\tif (closestIndex < draggedIndex && closestIsBeforePoint) {\n\t\tclosest = _thumbs[_order[++closestIndex]].get();\n\t} else if (closestIndex > draggedIndex && !closestIsBeforePoint) {\n\t\tclosest = _thumbs[_order[--closestIndex]].get();\n\t}\n\n\tif (_suggestedThumb == closest) {\n\t\treturn;\n\t}\n\n\tconst auto last = int(_order.size()) - 1;\n\tif (_suggestedThumb) {\n\t\tconst auto suggestedIndex = orderIndex(_suggestedThumb);\n\t\tif (suggestedIndex < draggedIndex && suggestedIndex > 0) {\n\t\t\tconst auto previous = _thumbs[_order[suggestedIndex - 1]].get();\n\t\t\tprevious->suggestMove(0., [=] { update(); });\n\t\t} else if (suggestedIndex > draggedIndex && suggestedIndex < last) {\n\t\t\tconst auto next = _thumbs[_order[suggestedIndex + 1]].get();\n\t\t\tnext->suggestMove(0., [=] { update(); });\n\t\t}\n\t\t_suggestedThumb->suggestMove(0., [=] { update(); });\n\t}\n\t_suggestedThumb = closest;\n\tconst auto suggestedIndex = closestIndex;\n\tif (_suggestedThumb != _draggedThumb) {\n\t\tconst auto delta = (suggestedIndex < draggedIndex) ? 1. : -1.;\n\t\tif (delta > 0. && suggestedIndex > 0) {\n\t\t\tconst auto previous = _thumbs[_order[suggestedIndex - 1]].get();\n\t\t\tprevious->suggestMove(-delta, [=] { update(); });\n\t\t} else if (delta < 0. && suggestedIndex < last) {\n\t\t\tconst auto next = _thumbs[_order[suggestedIndex + 1]].get();\n\t\t\tnext->suggestMove(-delta, [=] { update(); });\n\t\t}\n\t\t_suggestedThumb->suggestMove(delta, [=] { update(); });\n\t}\n}\n\nvoid AlbumPreview::mouseReleaseEvent(QMouseEvent *e) {\n\tif (_draggedThumb) {\n\t\tfinishDrag();\n\t\t_shrinkAnimation.start(\n\t\t\t[=] { update(); },\n\t\t\t1.,\n\t\t\t0.,\n\t\t\tAlbumThumbnail::kShrinkDuration);\n\t\t_draggedThumb = nullptr;\n\t\t_suggestedThumb = nullptr;\n\t\tupdate();\n\t} else if (const auto thumb = base::take(_pressedThumb)) {\n\t\tconst auto was = _pressedButtonType;\n\t\tconst auto now = thumb->buttonTypeFromPoint(e->pos());\n\t\tif (e->button() == Qt::LeftButton && was == now) {\n\t\t\tthumbButtonsCallback(thumb, now);\n\t\t}\n\t}\n\t_pressedButtonType = AttachButtonType::None;\n}\n\nvoid AlbumPreview::switchToDrag() {\n\t_paintedAbove\n\t\t= _suggestedThumb\n\t\t= _draggedThumb\n\t\t= base::take(_pressedThumb);\n\t_shrinkAnimation.start(\n\t\t[=] { update(); },\n\t\t0.,\n\t\t1.,\n\t\tAlbumThumbnail::kShrinkDuration);\n\tapplyCursor(style::cur_sizeall);\n\tupdate();\n}\n\nQImage AlbumPreview::generatePriceTagBackground() const {\n\tauto wmax = 0;\n\tauto hmax = 0;\n\tfor (auto &thumb : _thumbs) {\n\t\tconst auto geometry = thumb->geometry();\n\t\taccumulate_max(wmax, geometry.x() + geometry.width());\n\t\taccumulate_max(hmax, geometry.y() + geometry.height());\n\t}\n\tconst auto size = QSize(wmax, hmax);\n\tif (size.isEmpty()) {\n\t\treturn {};\n\t}\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto full = size * ratio;\n\tconst auto skip = st::historyGroupSkip;\n\tauto result = QImage(full, QImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\tresult.fill(Qt::black);\n\tauto p = QPainter(&result);\n\tauto hq = PainterHighQualityEnabler(p);\n\tfor (auto &thumb : _thumbs) {\n\t\tconst auto geometry = thumb->geometry();\n\t\tif (geometry.isEmpty()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto w = geometry.width();\n\t\tconst auto h = geometry.height();\n\t\tconst auto wscale = (w + skip) / float64(w);\n\t\tconst auto hscale = (h + skip) / float64(h);\n\t\tp.save();\n\t\tp.translate(geometry.center());\n\t\tp.scale(wscale, hscale);\n\t\tp.translate(-geometry.center());\n\t\tthumb->paintInAlbum(p, 0, 0, 1., 1., _sendWay.sendLargePhotos());\n\t\tp.restore();\n\t}\n\tp.end();\n\n\treturn ::Media::Streaming::PrepareBlurredBackground(\n\t\tfull,\n\t\tstd::move(result));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/chat/attach/attach_send_files_way.h\"\n#include \"ui/text/text.h\"\n#include \"base/timer.h\"\n\nnamespace style {\nstruct ComposeControls;\n} // namespace style\n\nnamespace Ui {\n\nstruct PreparedFile;\nstruct GroupMediaLayout;\nclass AlbumThumbnail;\n\nclass AlbumPreview final : public RpWidget {\npublic:\n\tAlbumPreview(\n\t\tQWidget *parent,\n\t\tconst style::ComposeControls &st,\n\t\tgsl::span<Ui::PreparedFile> items,\n\t\tconst Text::MarkedContext &captionContext,\n\t\tSendFilesWay way);\n\t~AlbumPreview();\n\n\tvoid setSendWay(SendFilesWay way);\n\tvoid setCaption(int index, const TextWithTags &caption);\n\t[[nodiscard]] int indexFromPoint(QPoint position) const;\n\n\t[[nodiscard]] base::flat_set<int> collectSpoileredIndices();\n\t[[nodiscard]] bool canHaveSpoiler(int index) const;\n\tvoid toggleSpoilers(bool enabled);\n\t[[nodiscard]] std::vector<int> takeOrder();\n\n\t[[nodiscard]] rpl::producer<int> thumbDeleted() const {\n\t\treturn _thumbDeleted.events();\n\t}\n\t[[nodiscard]] rpl::producer<int> thumbChanged() const {\n\t\treturn _thumbChanged.events();\n\t}\n\t[[nodiscard]] rpl::producer<int> thumbModified() const {\n\t\treturn _thumbModified.events();\n\t}\n\t[[nodiscard]] rpl::producer<> orderUpdated() const {\n\t\treturn _orderUpdated.events();\n\t}\n\n\t[[nodiscard]] QImage generatePriceTagBackground() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\nprivate:\n\tint countLayoutHeight(\n\t\tconst std::vector<GroupMediaLayout> &layout) const;\n\tstd::vector<GroupMediaLayout> generateOrderedLayout() const;\n\tstd::vector<int> defaultOrder(int count = -1) const;\n\tvoid prepareThumbs(gsl::span<Ui::PreparedFile> items);\n\tvoid updateSizeAnimated(const std::vector<GroupMediaLayout> &layout);\n\tvoid updateSize();\n\tvoid updateFileRows();\n\n\tAlbumThumbnail *thumbUnderCursor();\n\tvoid deleteThumbByIndex(int index);\n\tvoid changeThumbByIndex(int index);\n\tvoid modifyThumbByIndex(int index);\n\tvoid thumbButtonsCallback(\n\t\tnot_null<AlbumThumbnail*> thumb,\n\t\tAttachButtonType type);\n\n\tvoid switchToDrag();\n\n\tvoid paintAlbum(Painter &p) const;\n\tvoid paintPhotos(Painter &p, QRect clip) const;\n\tvoid paintFiles(Painter &p, QRect clip) const;\n\n\tvoid applyCursor(style::cursor cursor);\n\tint contentLeft() const;\n\tint contentTop() const;\n\tAlbumThumbnail *findThumb(QPoint position) const;\n\tnot_null<AlbumThumbnail*> findClosestThumb(QPoint position) const;\n\tvoid updateSuggestedDrag(QPoint position);\n\tint orderIndex(not_null<AlbumThumbnail*> thumb) const;\n\tvoid cancelDrag();\n\tvoid finishDrag();\n\n\tconst style::ComposeControls &_st;\n\tconst Text::MarkedContext _captionContext;\n\tSendFilesWay _sendWay;\n\tstyle::cursor _cursor = style::cur_default;\n\tstd::vector<int> _order;\n\tstd::vector<QSize> _itemsShownDimensions;\n\tstd::vector<std::unique_ptr<AlbumThumbnail>> _thumbs;\n\tint _thumbsHeight = 0;\n\tint _photosHeight = 0;\n\tint _filesHeight = 0;\n\n\tbool _hasMixedFileHeights = false;\n\n\tAlbumThumbnail *_draggedThumb = nullptr;\n\tAlbumThumbnail *_suggestedThumb = nullptr;\n\tAlbumThumbnail *_paintedAbove = nullptr;\n\tAlbumThumbnail *_pressedThumb = nullptr;\n\tQPoint _draggedStartPosition;\n\n\tbase::Timer _dragTimer;\n\tAttachButtonType _pressedButtonType = AttachButtonType::None;\n\n\trpl::event_stream<int> _thumbDeleted;\n\trpl::event_stream<int> _thumbChanged;\n\trpl::event_stream<int> _thumbModified;\n\trpl::event_stream<> _orderUpdated;\n\n\tmutable Animations::Simple _thumbsHeightAnimation;\n\tmutable Animations::Simple _shrinkAnimation;\n\tmutable Animations::Simple _finishDragAnimation;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/attach/attach_album_thumbnail.h\"\n\n#include \"core/mime_type.h\" // Core::IsMimeSticker.\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"base/call_delayed.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_boxes.h\"\n\n#include <QtCore/QFileInfo>\n\nnamespace Ui {\n\nAlbumThumbnail::AlbumThumbnail(\n\tconst style::ComposeControls &st,\n\tconst PreparedFile &file,\n\tconst Text::MarkedContext &captionContext,\n\tconst GroupMediaLayout &layout,\n\tQWidget *parent,\n\tFn<void()> repaint,\n\tFn<void(QRect)> repaintRect,\n\tFn<void()> editCallback,\n\tFn<void()> deleteCallback)\n: _st(st)\n, _layout(layout)\n, _fullPreview(file.videoCover ? file.videoCover->preview : file.preview)\n, _shrinkSize(int(std::ceil(st::roundRadiusLarge / 1.4)))\n, _isPhoto(file.type == PreparedFile::Type::Photo)\n, _isVideo(file.type == PreparedFile::Type::Video)\n, _canShowHighQualityBadge(file.canUseHighQualityPhoto())\n, _isCompressedSticker(Core::IsMimeSticker(file.information->filemime))\n, _repaint(std::move(repaint))\n, _repaintRect(std::move(repaintRect)) {\n\tExpects(!_fullPreview.isNull());\n\n\tmoveToLayout(layout);\n\n\tusing Option = Images::Option;\n\tconst auto previewWidth = _fullPreview.width();\n\tconst auto previewHeight = _fullPreview.height();\n\tconst auto imageWidth = std::max(\n\t\tpreviewWidth / style::DevicePixelRatio(),\n\t\tst::minPhotoSize);\n\tconst auto imageHeight = std::max(\n\t\tpreviewHeight / style::DevicePixelRatio(),\n\t\tst::minPhotoSize);\n\t_photo = PixmapFromImage(Images::Prepare(\n\t\t_fullPreview,\n\t\tQSize(previewWidth, previewHeight),\n\t\t{\n\t\t\t.options = Option::RoundLarge,\n\t\t\t.outer = { imageWidth, imageHeight },\n\t\t}));\n\n\tconst auto &layoutSt = st::attachPreviewThumbLayout;\n\tconst auto idealSize = layoutSt.thumbSize * style::DevicePixelRatio();\n\tconst auto fileThumbSize = (previewWidth > previewHeight)\n\t\t? QSize(previewWidth * idealSize / previewHeight, idealSize)\n\t\t: QSize(idealSize, previewHeight * idealSize / previewWidth);\n\t_fileThumb = PixmapFromImage(Images::Prepare(\n\t\t_fullPreview,\n\t\tfileThumbSize,\n\t\t{\n\t\t\t.options = Option::RoundSmall,\n\t\t\t.outer = { layoutSt.thumbSize, layoutSt.thumbSize },\n\t\t}));\n\n\tconst auto availableFileWidth = st::sendMediaPreviewSize\n\t\t- layoutSt.thumbSize\n\t\t- layoutSt.thumbSkip\n\t\t// Right buttons.\n\t\t- st::sendBoxAlbumGroupButtonFile.width * 2\n\t\t- st::sendBoxAlbumGroupEditInternalSkip * 2\n\t\t- st::sendBoxAlbumGroupSkipRight;\n\tconst auto availableCaptionWidth = st::sendMediaPreviewSize\n\t\t- st::sendBoxAlbumGroupButtonFile.width * 2\n\t\t- st::sendBoxAlbumGroupEditInternalSkip * 2\n\t\t- st::sendBoxAlbumGroupSkipRight;\n\t_captionAvailableWidth = availableCaptionWidth;\n\tconst auto filepath = file.path;\n\tif (filepath.isEmpty()) {\n\t\t_name = file.displayName.isEmpty()\n\t\t\t? \"image.png\"\n\t\t\t: file.displayName;\n\t\t_status = FormatImageSizeText(file.originalDimensions);\n\t} else {\n\t\tauto fileinfo = QFileInfo(filepath);\n\t\t_name = file.displayName.isEmpty()\n\t\t\t? fileinfo.fileName()\n\t\t\t: file.displayName;\n\t\t_status = FormatSizeText(fileinfo.size());\n\t}\n\t_nameWidth = st::semiboldFont->width(_name);\n\tif (_nameWidth > availableFileWidth) {\n\t\t_name = st::semiboldFont->elided(\n\t\t\t_name,\n\t\t\tavailableFileWidth,\n\t\t\tQt::ElideMiddle);\n\t\t_nameWidth = st::semiboldFont->width(_name);\n\t}\n\t_statusWidth = st::normalFont->width(_status);\n\tauto caption = TextWithEntities{\n\t\tfile.caption.text,\n\t\tTextUtilities::ConvertTextTagsToEntities(file.caption.tags),\n\t};\n\tcaption = TextUtilities::SingleLine(caption);\n\tauto context = captionContext;\n\tconst auto repaintCaption = context.repaint;\n\tcontext.repaint = [=] {\n\t\tif (repaintCaption) {\n\t\t\trepaintCaption();\n\t\t}\n\t\tif (!_lastRectOfCaption.isEmpty() && _repaintRect) {\n\t\t\t_repaintRect(_lastRectOfCaption);\n\t\t} else {\n\t\t\t_repaint();\n\t\t}\n\t};\n\t_captionContext = context;\n\t_caption.setMarkedText(\n\t\tst::defaultTextStyle,\n\t\tcaption,\n\t\tkMarkupTextOptions,\n\t\t_captionContext);\n\n\t_editMedia.create(parent, _st.files.buttonFile);\n\t_deleteMedia.create(parent, _st.files.buttonFile);\n\n\tconst auto duration = st::historyAttach.ripple.hideDuration;\n\t_editMedia->setClickedCallback([=] {\n\t\tbase::call_delayed(duration, parent, editCallback);\n\t});\n\t_deleteMedia->setClickedCallback(deleteCallback);\n\n\t_editMedia->setIconOverride(&_st.files.buttonFileEdit);\n\t_deleteMedia->setIconOverride(&_st.files.buttonFileDelete);\n\n\tsetSpoiler(file.spoiler);\n\tsetButtonVisible(false);\n}\n\nvoid AlbumThumbnail::setSpoiler(bool spoiler) {\n\tExpects(_repaint != nullptr);\n\n\t_spoiler = spoiler\n\t\t? std::make_unique<SpoilerAnimation>(_repaint)\n\t\t: nullptr;\n\t_repaint();\n}\n\nvoid AlbumThumbnail::setCaption(const TextWithTags &caption) {\n\tauto marked = TextWithEntities{\n\t\tcaption.text,\n\t\tTextUtilities::ConvertTextTagsToEntities(caption.tags),\n\t};\n\tmarked = TextUtilities::SingleLine(marked);\n\t_caption.setMarkedText(\n\t\tst::defaultTextStyle,\n\t\tmarked,\n\t\tkMarkupTextOptions,\n\t\t_captionContext);\n\t_repaint();\n}\n\nbool AlbumThumbnail::hasSpoiler() const {\n\treturn _spoiler != nullptr;\n}\n\nvoid AlbumThumbnail::setButtonVisible(bool value) {\n\t_editMedia->setVisible(value);\n\t_deleteMedia->setVisible(value);\n}\n\nvoid AlbumThumbnail::moveButtons(int thumbTop) {\n\tconst auto top = thumbTop + st::sendBoxFileGroupSkipTop;\n\n\tauto right = st::sendBoxFileGroupSkipRight + st::boxPhotoPadding.right();\n\t_deleteMedia->moveToRight(right, top);\n\tright += st::sendBoxFileGroupEditInternalSkip + _deleteMedia->width();\n\t_editMedia->moveToRight(right, top);\n}\n\nvoid AlbumThumbnail::resetLayoutAnimation() {\n\t_animateFromGeometry = std::nullopt;\n}\n\nvoid AlbumThumbnail::animateLayoutToInitial() {\n\t_animateFromGeometry = countRealGeometry();\n\t_suggestedMove = 0.;\n\t_albumPosition = QPoint(0, 0);\n}\n\nvoid AlbumThumbnail::moveToLayout(const GroupMediaLayout &layout) {\n\tusing namespace Images;\n\n\tanimateLayoutToInitial();\n\t_layout = layout;\n\n\tconst auto width = _layout.geometry.width();\n\tconst auto height = _layout.geometry.height();\n\t_albumCorners = GetCornersFromSides(_layout.sides);\n\tconst auto pixSize = GetImageScaleSizeForGeometry(\n\t\t{ _fullPreview.width(), _fullPreview.height() },\n\t\t{ width, height });\n\tconst auto pixWidth = pixSize.width() * style::DevicePixelRatio();\n\tconst auto pixHeight = pixSize.height() * style::DevicePixelRatio();\n\n\t_albumImage = PixmapFromImage(Prepare(\n\t\t_fullPreview,\n\t\tQSize(pixWidth, pixHeight),\n\t\t{\n\t\t\t.options = RoundOptions(ImageRoundRadius::Large, _albumCorners),\n\t\t\t.outer = { width, height },\n\t\t}));\n\t_albumImageBlurred = QPixmap();\n}\n\nint AlbumThumbnail::photoHeight() const {\n\treturn _photo.height() / style::DevicePixelRatio();\n}\n\nint AlbumThumbnail::fileHeight() const {\n\treturn _isCompressedSticker\n\t\t? photoHeight()\n\t\t: st::attachPreviewThumbLayout.thumbSize + (_caption.isEmpty()\n\t\t\t? 0\n\t\t\t: (st::attachPreviewCaptionTopOffset + _caption.lineHeight()));\n}\n\nbool AlbumThumbnail::isCompressedSticker() const {\n\treturn _isCompressedSticker;\n}\n\nvoid AlbumThumbnail::paintInAlbum(\n\t\tQPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tfloat64 shrinkProgress,\n\t\tfloat64 moveProgress,\n\t\tbool showHighQualityBadge) {\n\tconst auto shrink = anim::interpolate(0, _shrinkSize, shrinkProgress);\n\t_lastShrinkValue = shrink;\n\tconst auto geometry = countCurrentGeometry(\n\t\tmoveProgress\n\t).translated(left, top);\n\tauto paintedTo = geometry;\n\tconst auto revealed = _spoiler ? shrinkProgress : 1.;\n\tif (revealed > 0.) {\n\t\tif (shrink > 0 || moveProgress < 1.) {\n\t\t\tconst auto size = geometry.size();\n\t\t\tpaintedTo = geometry.marginsRemoved(\n\t\t\t\t{ shrink, shrink, shrink, shrink }\n\t\t\t);\n\t\t\tif (shrinkProgress < 1 && _albumCorners != RectPart::None) {\n\t\t\t\tprepareCache(size, shrink);\n\t\t\t\tp.drawImage(geometry.topLeft(), _albumCache);\n\t\t\t} else {\n\t\t\t\tdrawSimpleFrame(p, paintedTo, size);\n\t\t\t}\n\t\t} else {\n\t\t\tp.drawPixmap(geometry.topLeft(), _albumImage);\n\t\t}\n\t\tif (_isVideo) {\n\t\t\tpaintPlayVideo(p, geometry);\n\t\t}\n\t}\n\tif (revealed < 1.) {\n\t\tauto corners = Images::CornersMaskRef(\n\t\t\tImages::CornersMask(ImageRoundRadius::Large));\n\t\tif (!(_albumCorners & RectPart::TopLeft)) {\n\t\t\tcorners.p[0] = nullptr;\n\t\t}\n\t\tif (!(_albumCorners & RectPart::TopRight)) {\n\t\t\tcorners.p[1] = nullptr;\n\t\t}\n\t\tif (!(_albumCorners & RectPart::BottomLeft)) {\n\t\t\tcorners.p[2] = nullptr;\n\t\t}\n\t\tif (!(_albumCorners & RectPart::BottomRight)) {\n\t\t\tcorners.p[3] = nullptr;\n\t\t}\n\t\tp.setOpacity(1. - revealed);\n\t\tif (_albumImageBlurred.isNull()) {\n\t\t\t_albumImageBlurred = BlurredPreviewFromPixmap(\n\t\t\t\t_albumImage,\n\t\t\t\t_albumCorners);\n\t\t}\n\t\tp.drawPixmap(paintedTo, _albumImageBlurred);\n\t\tconst auto paused = On(PowerSaving::kChatSpoiler);\n\t\tFillSpoilerRect(\n\t\t\tp,\n\t\t\tpaintedTo,\n\t\t\tcorners,\n\t\t\tDefaultImageSpoiler().frame(_spoiler->index(crl::now(), paused)),\n\t\t\t_cornerCache);\n\t\tp.setOpacity(1.);\n\t}\n\n\t_lastRectOfButtons = paintButtons(\n\t\tp,\n\t\tgeometry,\n\t\tshrinkProgress);\n\t_lastRectOfModify = geometry;\n\tif (showHighQualityBadge && _canShowHighQualityBadge) {\n\t\tPaintHighQualityBadge(p, _st, paintedTo);\n\t}\n}\n\nvoid AlbumThumbnail::paintPlayVideo(QPainter &p, QRect geometry) {\n\tconst auto innerSize = st::msgFileLayout.thumbSize;\n\tconst auto inner = QRect(\n\t\tgeometry.x() + (geometry.width() - innerSize) / 2,\n\t\tgeometry.y() + (geometry.height() - innerSize) / 2,\n\t\tinnerSize,\n\t\tinnerSize);\n\t{\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::msgDateImgBg);\n\t\tp.drawEllipse(inner);\n\t}\n\tst::historyFileThumbPlay.paintInCenter(p, inner);\n}\n\nvoid AlbumThumbnail::prepareCache(QSize size, int shrink) {\n\tconst auto width = std::max(\n\t\t_layout.geometry.width(),\n\t\t_animateFromGeometry ? _animateFromGeometry->width() : 0);\n\tconst auto height = std::max(\n\t\t_layout.geometry.height(),\n\t\t_animateFromGeometry ? _animateFromGeometry->height() : 0);\n\tconst auto cacheSize = QSize(width, height) * style::DevicePixelRatio();\n\n\tif (_albumCache.width() < cacheSize.width()\n\t\t|| _albumCache.height() < cacheSize.height()) {\n\t\t_albumCache = QImage(cacheSize, QImage::Format_ARGB32_Premultiplied);\n\t\t_albumCache.setDevicePixelRatio(style::DevicePixelRatio());\n\t}\n\t_albumCache.fill(Qt::transparent);\n\t{\n\t\tPainter p(&_albumCache);\n\t\tconst auto to = QRect(QPoint(), size).marginsRemoved(\n\t\t\t{ shrink, shrink, shrink, shrink }\n\t\t);\n\t\tdrawSimpleFrame(p, to, size);\n\t}\n\t_albumCache = Images::Round(\n\t\tstd::move(_albumCache),\n\t\tImageRoundRadius::Large,\n\t\t_albumCorners,\n\t\tQRect(QPoint(), size * style::DevicePixelRatio()));\n}\n\nvoid AlbumThumbnail::drawSimpleFrame(QPainter &p, QRect to, QSize size) const {\n\tconst auto fullWidth = _fullPreview.width();\n\tconst auto fullHeight = _fullPreview.height();\n\tconst auto previewSize = GetImageScaleSizeForGeometry(\n\t\t{ fullWidth, fullHeight },\n\t\t{ size.width(), size.height() });\n\tconst auto previewWidth = previewSize.width() * style::DevicePixelRatio();\n\tconst auto previewHeight = previewSize.height() * style::DevicePixelRatio();\n\tconst auto width = size.width() * style::DevicePixelRatio();\n\tconst auto height = size.height() * style::DevicePixelRatio();\n\tconst auto scaleWidth = to.width() / float64(width);\n\tconst auto scaleHeight = to.height() / float64(height);\n\tconst auto Round = [](float64 value) {\n\t\treturn int(base::SafeRound(value));\n\t};\n\tconst auto &[from, fillBlack] = [&] {\n\t\tif (previewWidth < width && previewHeight < height) {\n\t\t\tconst auto toWidth = Round(previewWidth * scaleWidth);\n\t\t\tconst auto toHeight = Round(previewHeight * scaleHeight);\n\t\t\treturn std::make_pair(\n\t\t\t\tQRect(0, 0, fullWidth, fullHeight),\n\t\t\t\tQMargins(\n\t\t\t\t\t(to.width() - toWidth) / 2,\n\t\t\t\t\t(to.height() - toHeight) / 2,\n\t\t\t\t\tto.width() - toWidth - (to.width() - toWidth) / 2,\n\t\t\t\t\tto.height() - toHeight - (to.height() - toHeight) / 2));\n\t\t} else if (previewWidth * height > previewHeight * width) {\n\t\t\tif (previewHeight >= height) {\n\t\t\t\tconst auto takeWidth = previewWidth * height / previewHeight;\n\t\t\t\tconst auto useWidth = fullWidth * width / takeWidth;\n\t\t\t\treturn std::make_pair(\n\t\t\t\t\tQRect(\n\t\t\t\t\t\t(fullWidth - useWidth) / 2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tuseWidth,\n\t\t\t\t\t\tfullHeight),\n\t\t\t\t\tQMargins(0, 0, 0, 0));\n\t\t\t} else {\n\t\t\t\tconst auto takeWidth = previewWidth;\n\t\t\t\tconst auto useWidth = fullWidth * width / takeWidth;\n\t\t\t\tconst auto toHeight = Round(previewHeight * scaleHeight);\n\t\t\t\tconst auto toSkip = (to.height() - toHeight) / 2;\n\t\t\t\treturn std::make_pair(\n\t\t\t\t\tQRect(\n\t\t\t\t\t\t(fullWidth - useWidth) / 2,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tuseWidth,\n\t\t\t\t\t\tfullHeight),\n\t\t\t\t\tQMargins(\n\t\t\t\t\t\t0,\n\t\t\t\t\t\ttoSkip,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tto.height() - toHeight - toSkip));\n\t\t\t}\n\t\t} else {\n\t\t\tif (previewWidth >= width) {\n\t\t\t\tconst auto takeHeight = previewHeight * width / previewWidth;\n\t\t\t\tconst auto useHeight = fullHeight * height / takeHeight;\n\t\t\t\treturn std::make_pair(\n\t\t\t\t\tQRect(\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t(fullHeight - useHeight) / 2,\n\t\t\t\t\t\tfullWidth,\n\t\t\t\t\t\tuseHeight),\n\t\t\t\t\tQMargins(0, 0, 0, 0));\n\t\t\t} else {\n\t\t\t\tconst auto takeHeight = previewHeight;\n\t\t\t\tconst auto useHeight = fullHeight * height / takeHeight;\n\t\t\t\tconst auto toWidth = Round(previewWidth * scaleWidth);\n\t\t\t\tconst auto toSkip = (to.width() - toWidth) / 2;\n\t\t\t\treturn std::make_pair(\n\t\t\t\t\tQRect(\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t(fullHeight - useHeight) / 2,\n\t\t\t\t\t\tfullWidth,\n\t\t\t\t\t\tuseHeight),\n\t\t\t\t\tQMargins(\n\t\t\t\t\t\ttoSkip,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tto.width() - toWidth - toSkip,\n\t\t\t\t\t\t0));\n\t\t\t}\n\t\t}\n\t}();\n\n\tp.drawImage(to.marginsRemoved(fillBlack), _fullPreview, from);\n\tif (fillBlack.top() > 0) {\n\t\tp.fillRect(to.x(), to.y(), to.width(), fillBlack.top(), st::imageBg);\n\t}\n\tif (fillBlack.bottom() > 0) {\n\t\tp.fillRect(\n\t\t\tto.x(),\n\t\t\tto.y() + to.height() - fillBlack.bottom(),\n\t\t\tto.width(),\n\t\t\tfillBlack.bottom(),\n\t\t\tst::imageBg);\n\t}\n\tif (fillBlack.left() > 0) {\n\t\tp.fillRect(\n\t\t\tto.x(),\n\t\t\tto.y() + fillBlack.top(),\n\t\t\tfillBlack.left(),\n\t\t\tto.height() - fillBlack.top() - fillBlack.bottom(),\n\t\t\tst::imageBg);\n\t}\n\tif (fillBlack.right() > 0) {\n\t\tp.fillRect(\n\t\t\tto.x() + to.width() - fillBlack.right(),\n\t\t\tto.y() + fillBlack.top(),\n\t\t\tfillBlack.right(),\n\t\t\tto.height() - fillBlack.top() - fillBlack.bottom(),\n\t\t\tst::imageBg);\n\t}\n}\n\nvoid AlbumThumbnail::paintPhoto(\n\t\tPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint outerWidth,\n\t\tbool showHighQualityBadge) {\n\tconst auto size = _photo.size() / style::DevicePixelRatio();\n\tif (_spoiler && _photoBlurred.isNull()) {\n\t\t_photoBlurred = BlurredPreviewFromPixmap(\n\t\t\t_photo,\n\t\t\tRectPart::AllCorners);\n\t}\n\tconst auto &pixmap = _spoiler ? _photoBlurred : _photo;\n\tconst auto rect = QRect(\n\t\tleft + (st::sendMediaPreviewSize - size.width()) / 2,\n\t\ttop,\n\t\tpixmap.width() / pixmap.devicePixelRatio(),\n\t\tpixmap.height() / pixmap.devicePixelRatio());\n\tp.drawPixmapLeft(\n\t\tleft + (st::sendMediaPreviewSize - size.width()) / 2,\n\t\ttop,\n\t\touterWidth,\n\t\tpixmap);\n\tif (_spoiler) {\n\t\tconst auto paused = On(PowerSaving::kChatSpoiler);\n\t\tFillSpoilerRect(\n\t\t\tp,\n\t\t\trect,\n\t\t\tImages::CornersMaskRef(\n\t\t\t\tImages::CornersMask(ImageRoundRadius::Large)),\n\t\t\tDefaultImageSpoiler().frame(_spoiler->index(crl::now(), paused)),\n\t\t\t_cornerCache);\n\t} else if (_isVideo) {\n\t\tpaintPlayVideo(p, rect);\n\t}\n\n\tconst auto topLeft = QPoint{ left, top };\n\n\t_lastRectOfButtons = paintButtons(\n\t\tp,\n\t\tQRect(left, top, st::sendMediaPreviewSize, size.height()),\n\t\t0);\n\n\t_lastRectOfModify = QRect(topLeft, size);\n\tif (showHighQualityBadge && _canShowHighQualityBadge) {\n\t\tPaintHighQualityBadge(p, _st, rect);\n\t}\n}\n\nvoid AlbumThumbnail::paintFile(\n\t\tPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint outerWidth) {\n\n\tif (isCompressedSticker()) {\n\t\tauto spoiler = base::take(_spoiler);\n\t\tpaintPhoto(p, left, top, outerWidth, false);\n\t\t_spoiler = base::take(spoiler);\n\t\treturn;\n\t}\n\tconst auto &st = st::attachPreviewThumbLayout;\n\tconst auto textLeft = left + st.thumbSize + st.thumbSkip;\n\n\tp.drawPixmap(left, top, _fileThumb);\n\tp.setFont(st::semiboldFont);\n\tp.setPen(_st.files.nameFg);\n\tp.drawTextLeft(\n\t\ttextLeft,\n\t\ttop + st.nameTop,\n\t\touterWidth,\n\t\t_name,\n\t\t_nameWidth);\n\tp.setFont(st::normalFont);\n\tp.setPen(_st.files.statusFg);\n\tp.drawTextLeft(\n\t\ttextLeft,\n\t\ttop + st.statusTop,\n\t\touterWidth,\n\t\t_status,\n\t\t_statusWidth);\n\tif (!_caption.isEmpty()) {\n\t\tp.setPen(_st.files.nameFg);\n\t\tconst auto captionLineHeight = _caption.lineHeight();\n\t\tconst auto captionTop = top\n\t\t\t+ st.thumbSize\n\t\t\t+ st::attachPreviewCaptionTopOffset;\n\t\t_lastRectOfCaption = QRect(\n\t\t\tleft,\n\t\t\tcaptionTop,\n\t\t\t_captionAvailableWidth,\n\t\t\tcaptionLineHeight) + st::attachPreviewCaptionRepaintMargin;\n\t\t_caption.draw(p, {\n\t\t\t.position = { left, captionTop },\n\t\t\t.outerWidth = outerWidth,\n\t\t\t.availableWidth = _captionAvailableWidth,\n\t\t\t.align = style::al_left,\n\t\t\t.elisionLines = 1,\n\t\t\t.elisionBreakEverywhere = true,\n\t\t});\n\t} else {\n\t\t_lastRectOfCaption = {};\n\t}\n\n\t_lastRectOfModify = QRect(\n\t\tQPoint(left, top),\n\t\t_fileThumb.size() / style::DevicePixelRatio());\n}\n\nQRect AlbumThumbnail::geometry() const {\n\treturn _layout.geometry;\n}\n\nbool AlbumThumbnail::containsPoint(QPoint position) const {\n\treturn _layout.geometry.contains(position);\n}\n\nbool AlbumThumbnail::buttonsContainPoint(QPoint position) const {\n\treturn ((_isPhoto && !_isCompressedSticker)\n\t\t? _lastRectOfModify\n\t\t: _lastRectOfButtons).contains(position);\n}\n\nAttachButtonType AlbumThumbnail::buttonTypeFromPoint(QPoint position) const {\n\tif (!buttonsContainPoint(position)) {\n\t\treturn AttachButtonType::None;\n\t}\n\treturn (!_lastRectOfButtons.contains(position) && !_isCompressedSticker)\n\t\t? AttachButtonType::Modify\n\t\t: (_buttons.vertical()\n\t\t\t? (position.y() < _lastRectOfButtons.center().y())\n\t\t\t: (position.x() < _lastRectOfButtons.center().x()))\n\t\t? AttachButtonType::Edit\n\t\t: AttachButtonType::Delete;\n}\n\nint AlbumThumbnail::distanceTo(QPoint position) const {\n\tconst auto delta = (_layout.geometry.center() - position);\n\treturn QPoint::dotProduct(delta, delta);\n}\n\nbool AlbumThumbnail::isPointAfter(QPoint position) const {\n\treturn position.x() > _layout.geometry.center().x();\n}\n\nvoid AlbumThumbnail::moveInAlbum(QPoint to) {\n\t_albumPosition = to;\n}\n\nQPoint AlbumThumbnail::center() const {\n\tauto realGeometry = _layout.geometry;\n\trealGeometry.moveTopLeft(realGeometry.topLeft() + _albumPosition);\n\treturn realGeometry.center();\n}\n\nvoid AlbumThumbnail::suggestMove(float64 delta, Fn<void()> callback) {\n\tif (_suggestedMove != delta) {\n\t\t_suggestedMoveAnimation.start(\n\t\t\tstd::move(callback),\n\t\t\t_suggestedMove,\n\t\t\tdelta,\n\t\t\tkShrinkDuration);\n\t\t_suggestedMove = delta;\n\t}\n}\n\nQRect AlbumThumbnail::countRealGeometry() const {\n\tconst auto addLeft = int(base::SafeRound(\n\t\t_suggestedMoveAnimation.value(_suggestedMove) * _lastShrinkValue));\n\tconst auto current = _layout.geometry;\n\tconst auto realTopLeft = current.topLeft()\n\t\t+ _albumPosition\n\t\t+ QPoint(addLeft, 0);\n\treturn { realTopLeft, current.size() };\n}\n\nQRect AlbumThumbnail::countCurrentGeometry(float64 progress) const {\n\tconst auto now = countRealGeometry();\n\tif (_animateFromGeometry && progress < 1.) {\n\t\treturn {\n\t\t\tanim::interpolate(_animateFromGeometry->x(), now.x(), progress),\n\t\t\tanim::interpolate(_animateFromGeometry->y(), now.y(), progress),\n\t\t\tanim::interpolate(_animateFromGeometry->width(), now.width(), progress),\n\t\t\tanim::interpolate(_animateFromGeometry->height(), now.height(), progress)\n\t\t};\n\t}\n\treturn now;\n}\n\nvoid AlbumThumbnail::finishAnimations() {\n\t_suggestedMoveAnimation.stop();\n}\n\nQRect AlbumThumbnail::paintButtons(\n\t\tQPainter &p,\n\t\tQRect geometry,\n\t\tfloat64 shrinkProgress) {\n\tconst auto &skipRight = st::sendBoxAlbumGroupSkipRight;\n\tconst auto &skipTop = st::sendBoxAlbumGroupSkipTop;\n\tconst auto outerWidth = geometry.width();\n\tconst auto outerHeight = geometry.height();\n\tif (st::sendBoxAlbumGroupSize.width() <= outerWidth) {\n\t\t_buttons.setVertical(false);\n\t} else if (st::sendBoxAlbumGroupSize.height() <= outerHeight) {\n\t\t_buttons.setVertical(true);\n\t} else {\n\t\t// If the size is tiny, skip the buttons.\n\t\treturn QRect();\n\t}\n\tconst auto groupWidth = _buttons.width();\n\tconst auto groupHeight = _buttons.height();\n\n\t// If the width is too small,\n\t// it would be better to display the buttons in the center.\n\tconst auto groupX = geometry.x() + ((groupWidth + skipRight * 2 > outerWidth)\n\t\t? (outerWidth - groupWidth) / 2\n\t\t: outerWidth - skipRight - groupWidth);\n\tconst auto groupY = geometry.y() + ((groupHeight + skipTop * 2 > outerHeight)\n\t\t? (outerHeight - groupHeight) / 2\n\t\t: skipTop);\n\n\tconst auto opacity = p.opacity();\n\tp.setOpacity(1.0 - shrinkProgress);\n\t_buttons.paint(p, groupX, groupY);\n\tp.setOpacity(opacity);\n\n\treturn QRect(groupX, groupY, groupWidth, _buttons.height());\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/chat/attach/attach_controls.h\"\n#include \"ui/chat/attach/attach_send_files_way.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/grouped_layout.h\"\n#include \"ui/round_rect.h\"\n#include \"base/object_ptr.h\"\n\nnamespace style {\nstruct ComposeControls;\n} // namespace style\n\nnamespace Ui {\n\nstruct PreparedFile;\nclass IconButton;\nclass SpoilerAnimation;\n\nclass AlbumThumbnail final {\npublic:\n\tAlbumThumbnail(\n\t\tconst style::ComposeControls &st,\n\t\tconst PreparedFile &file,\n\t\tconst Text::MarkedContext &captionContext,\n\t\tconst GroupMediaLayout &layout,\n\t\tQWidget *parent,\n\t\tFn<void()> repaint,\n\t\tFn<void(QRect)> repaintRect,\n\t\tFn<void()> editCallback,\n\t\tFn<void()> deleteCallback);\n\n\tvoid moveToLayout(const GroupMediaLayout &layout);\n\tvoid animateLayoutToInitial();\n\tvoid resetLayoutAnimation();\n\n\tvoid setSpoiler(bool spoiler);\n\tvoid setCaption(const TextWithTags &caption);\n\t[[nodiscard]] bool hasSpoiler() const;\n\n\t[[nodiscard]] int photoHeight() const;\n\t[[nodiscard]] int fileHeight() const;\n\n\tvoid paintInAlbum(\n\t\tQPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tfloat64 shrinkProgress,\n\t\tfloat64 moveProgress,\n\t\tbool showHighQualityBadge);\n\tvoid paintPhoto(\n\t\tPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint outerWidth,\n\t\tbool showHighQualityBadge);\n\tvoid paintFile(Painter &p, int left, int top, int outerWidth);\n\n\t[[nodiscard]] QRect geometry() const;\n\t[[nodiscard]] bool containsPoint(QPoint position) const;\n\t[[nodiscard]] bool buttonsContainPoint(QPoint position) const;\n\t[[nodiscard]] AttachButtonType buttonTypeFromPoint(\n\t\tQPoint position) const;\n\t[[nodiscard]] int distanceTo(QPoint position) const;\n\t[[nodiscard]] bool isPointAfter(QPoint position) const;\n\tvoid moveInAlbum(QPoint to);\n\t[[nodiscard]] QPoint center() const;\n\tvoid suggestMove(float64 delta, Fn<void()> callback);\n\tvoid finishAnimations();\n\n\tvoid setButtonVisible(bool value);\n\tvoid moveButtons(int thumbTop);\n\n\t[[nodiscard]] bool isCompressedSticker() const;\n\n\tstatic constexpr auto kShrinkDuration = crl::time(150);\n\nprivate:\n\tQRect countRealGeometry() const;\n\tQRect countCurrentGeometry(float64 progress) const;\n\tvoid prepareCache(QSize size, int shrink);\n\tvoid drawSimpleFrame(QPainter &p, QRect to, QSize size) const;\n\tQRect paintButtons(\n\t\tQPainter &p,\n\t\tQRect geometry,\n\t\tfloat64 shrinkProgress);\n\tvoid paintPlayVideo(QPainter &p, QRect geometry);\n\n\tconst style::ComposeControls &_st;\n\tGroupMediaLayout _layout;\n\tstd::optional<QRect> _animateFromGeometry;\n\tconst QImage _fullPreview;\n\tconst int _shrinkSize;\n\tconst bool _isPhoto;\n\tconst bool _isVideo;\n\tconst bool _canShowHighQualityBadge;\n\tQPixmap _albumImage;\n\tQPixmap _albumImageBlurred;\n\tQImage _albumCache;\n\tQPoint _albumPosition;\n\tRectParts _albumCorners = RectPart::None;\n\tQPixmap _photo;\n\tQPixmap _photoBlurred;\n\tQPixmap _fileThumb;\n\tQString _name;\n\tQString _status;\n\tText::String _caption;\n\tText::MarkedContext _captionContext;\n\tint _nameWidth = 0;\n\tint _statusWidth = 0;\n\tint _captionAvailableWidth = 0;\n\tfloat64 _suggestedMove = 0.;\n\tAnimations::Simple _suggestedMoveAnimation;\n\tint _lastShrinkValue = 0;\n\tAttachControls _buttons;\n\n\tbool _isCompressedSticker = false;\n\tstd::unique_ptr<SpoilerAnimation> _spoiler;\n\tQImage _cornerCache;\n\tFn<void()> _repaint;\n\tFn<void(QRect)> _repaintRect;\n\n\tQRect _lastRectOfModify;\n\tQRect _lastRectOfButtons;\n\tQRect _lastRectOfCaption;\n\n\tobject_ptr<IconButton> _editMedia = { nullptr };\n\tobject_ptr<IconButton> _deleteMedia = { nullptr };\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/attach/attach_bot_downloads.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/menu/menu_item_base.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_chat.h\"\n\nnamespace Ui::BotWebView {\nnamespace {\n\nclass Action final : public Menu::ItemBase {\npublic:\n\tAction(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tconst DownloadsEntry &entry,\n\t\tFn<void(DownloadsAction)> callback);\n\n\tbool isEnabled() const override;\n\tnot_null<QAction*> action() const override { return _dummyAction; }\n\tvoid handleKeyPress(not_null<QKeyEvent*> e) override;\n\n\tvoid refresh(const DownloadsEntry &entry);\n\nprivate:\n\tQPoint prepareRippleStartPosition() const override {\n\t\treturn mapFromGlobal(QCursor::pos());\n\t}\n\tQImage prepareRippleMask() const override {\n\t\treturn Ui::RippleAnimation::RectMask(size());\n\t}\n\tint contentHeight() const override { return _height; }\n\n\tvoid prepare();\n\tvoid paint(Painter &p);\n\n\tconst not_null<QAction*> _dummyAction;\n\tconst style::Menu &_st = st::defaultMenu;\n\n\tDownloadsEntry _entry;\n\tText::String _name;\n\tFlatLabel _progress;\n\tIconButton _cancel;\n\tint _textWidth = 0;\n\tconst int _height;\n};\n\nAction::Action(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tconst DownloadsEntry &entry,\n\tFn<void(DownloadsAction)> callback)\n: ItemBase(parent, st::defaultMenu)\n, _dummyAction(new QAction(parent))\n, _progress(this, st::botDownloadProgress)\n, _cancel(this, st::botDownloadCancel)\n, _height(st::ttlItemPadding.top()\n\t+ _st.itemStyle.font->height\n\t+ st::ttlItemTimerFont->height\n\t+ st::ttlItemPadding.bottom()) {\n\tsetAcceptBoth(true);\n\tfitToMenuWidth();\n\tsetActionTriggered([=] {\n\t\tif (isEnabled()) {\n\t\t\tcallback(DownloadsAction::Open);\n\t\t}\n\t});\n\t_cancel.setClickedCallback([=] {\n\t\tcallback(DownloadsAction::Cancel);\n\t});\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(this);\n\t\tpaint(p);\n\t}, lifetime());\n\n\twidthValue() | rpl::on_next([=](int width) {\n\t\t_progress.moveToLeft(\n\t\t\t_st.itemPadding.left(),\n\t\t\tst::ttlItemPadding.top() + _st.itemStyle.font->height,\n\t\t\twidth);\n\n\t\t_cancel.moveToRight(\n\t\t\t_st.itemPadding.right(),\n\t\t\t(_height - _cancel.height()) / 2,\n\t\t\twidth);\n\t}, lifetime());\n\n\t_progress.setClickHandlerFilter([=](const auto &...) {\n\t\tcallback(DownloadsAction::Retry);\n\t\treturn false;\n\t});\n\n\tenableMouseSelecting();\n\trefresh(entry);\n}\n\nvoid Action::paint(Painter &p) {\n\tconst auto selected = isSelected();\n\tif (selected && _st.itemBgOver->c.alpha() < 255) {\n\t\tp.fillRect(0, 0, width(), _height, _st.itemBg);\n\t}\n\tp.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg);\n\tif (isEnabled()) {\n\t\tpaintRipple(p, 0, 0);\n\t}\n\n\tp.setPen(selected ? _st.itemFgOver : _st.itemFg);\n\t_name.drawLeftElided(\n\t\tp,\n\t\t_st.itemPadding.left(),\n\t\tst::ttlItemPadding.top(),\n\t\t_textWidth,\n\t\twidth());\n\n\t_progress.setTextColorOverride(\n\t\tselected ? _st.itemFgShortcutOver->c : _st.itemFgShortcut->c);\n}\n\nvoid Action::prepare() {\n\tconst auto filenameWidth = _name.maxWidth();\n\tconst auto progressWidth = _progress.textMaxWidth();\n\tconst auto &padding = _st.itemPadding;\n\n\tconst auto goodWidth = std::max(filenameWidth, progressWidth);\n\n\t// Example max width: \"4000 / 4000 MB\"\n\tconst auto countWidth = [&](const QString &text) {\n\t\treturn st::ttlItemTimerFont->width(text);\n\t};\n\tconst auto maxProgressWidth = countWidth(tr::lng_media_save_progress(\n\t\ttr::now,\n\t\tlt_ready,\n\t\t\"0000\",\n\t\tlt_total,\n\t\t\"0000\",\n\t\tlt_mb,\n\t\t\"MB\"));\n\tconst auto maxStartingWidth = countWidth(\n\t\ttr::lng_bot_download_starting(tr::now));\n\tconst auto maxFailedWidth = countWidth(tr::lng_bot_download_failed(\n\t\ttr::now,\n\t\tlt_retry,\n\t\ttr::lng_bot_download_retry(tr::now)));\n\n\tconst auto cancel = _cancel.width() + padding.right();\n\tconst auto paddings = padding.left() + padding.right() + cancel;\n\tconst auto w = std::clamp(\n\t\tpaddings + std::max({\n\t\t\tgoodWidth,\n\t\t\tmaxProgressWidth,\n\t\t\tmaxStartingWidth,\n\t\t\tmaxFailedWidth,\n\t\t}),\n\t\t_st.widthMin,\n\t\t_st.widthMax);\n\t_textWidth = w - paddings;\n\t_progress.resizeToWidth(_textWidth);\n\tsetMinWidth(w);\n\tupdate();\n}\n\nbool Action::isEnabled() const {\n\treturn _entry.total > 0 && _entry.ready == _entry.total;\n}\n\nvoid Action::handleKeyPress(not_null<QKeyEvent*> e) {\n\tif (!isSelected()) {\n\t\treturn;\n\t}\n\tconst auto key = e->key();\n\tif (key == Qt::Key_Enter || key == Qt::Key_Return) {\n\t\tsetClicked(Menu::TriggeredSource::Keyboard);\n\t}\n}\n\nvoid Action::refresh(const DownloadsEntry &entry) {\n\t_entry = entry;\n\tconst auto filename = entry.path.split('/').last();\n\t_name.setMarkedText(_st.itemStyle, { filename }, kDefaultTextOptions);\n\n\tconst auto progressText = (entry.total && entry.total == entry.ready)\n\t\t? TextWithEntities{ FormatSizeText(entry.total) }\n\t\t: entry.loading\n\t\t? (entry.total\n\t\t\t? TextWithEntities{\n\t\t\t\tFormatProgressText(entry.ready, entry.total),\n\t\t\t}\n\t\t\t: tr::lng_bot_download_starting(tr::now, tr::marked))\n\t\t: tr::lng_bot_download_failed(\n\t\t\ttr::now,\n\t\t\tlt_retry,\n\t\t\tText::Link(tr::lng_bot_download_retry(tr::now)),\n\t\t\ttr::marked);\n\t_progress.setMarkedText(progressText);\n\n\tconst auto enabled = isEnabled();\n\tsetCursor(enabled ? style::cur_pointer : style::cur_default);\n\t_cancel.setVisible(!enabled && _entry.loading);\n\t_progress.setAttribute(Qt::WA_TransparentForMouseEvents, enabled);\n\n\tprepare();\n}\n\n} // namespace\n\nFnMut<void(not_null<PopupMenu*>)> FillAttachBotDownloadsSubmenu(\n\t\trpl::producer<std::vector<DownloadsEntry>> content,\n\t\tFn<void(uint32, DownloadsAction)> callback) {\n\treturn [callback, moved = std::move(content)](\n\t\t\tnot_null<PopupMenu*> menu) mutable {\n\t\tstruct Row {\n\t\t\tnot_null<Action*> action;\n\t\t\tuint32 id = 0;\n\t\t};\n\t\tstruct State {\n\t\t\tstd::vector<Row> rows;\n\t\t};\n\t\tconst auto state = menu->lifetime().make_state<State>();\n\t\tstd::move(\n\t\t\tmoved\n\t\t) | rpl::on_next([=](\n\t\t\tconst std::vector<DownloadsEntry> &entries) {\n\t\t\tauto found = base::flat_set<uint32>();\n\t\t\tfor (const auto &entry : entries | ranges::views::reverse) {\n\t\t\t\tconst auto id = entry.id;\n\t\t\t\tconst auto path = entry.path;\n\t\t\t\tconst auto i = ranges::find(state->rows, id, &Row::id);\n\t\t\t\tfound.emplace(id);\n\n\t\t\t\tif (i != end(state->rows)) {\n\t\t\t\t\ti->action->refresh(entry);\n\t\t\t\t} else {\n\t\t\t\t\tauto action = base::make_unique_q<Action>(\n\t\t\t\t\t\tmenu->menu(),\n\t\t\t\t\t\tentry,\n\t\t\t\t\t\t[=](DownloadsAction type) { callback(id, type); });\n\t\t\t\t\tstate->rows.push_back({\n\t\t\t\t\t\t.action = action.get(),\n\t\t\t\t\t\t.id = id,\n\t\t\t\t\t\t});\n\t\t\t\t\tmenu->addAction(std::move(action));\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (auto i = begin(state->rows); i != end(state->rows);) {\n\t\t\t\tif (!found.contains(i->id)) {\n\t\t\t\t\tmenu->removeAction(i - begin(state->rows));\n\t\t\t\t\ti = state->rows.erase(i);\n\t\t\t\t} else {\n\t\t\t\t\t++i;\n\t\t\t\t}\n\t\t\t}\n\t\t}, menu->lifetime());\n\t};\n}\n\n} // namespace Ui::BotWebView\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Ui::BotWebView {\n\nstruct DownloadsProgress {\n\tuint64 ready = 0;\n\tuint64 total : 63 = 0;\n\tuint64 loading : 1 = 0;\n\n\tfriend inline bool operator==(\n\t\tconst DownloadsProgress &a,\n\t\tconst DownloadsProgress &b) = default;\n};\n\nstruct DownloadsEntry {\n\tuint32 id = 0;\n\tQString url;\n\tQString path;\n\tuint64 ready : 63 = 0;\n\tuint64 loading : 1 = 0;\n\tuint64 total : 63 = 0;\n\tuint64 failed : 1 = 0;\n};\n\nenum class DownloadsAction {\n\tOpen,\n\tRetry,\n\tCancel,\n};\n\n[[nodiscard]] auto FillAttachBotDownloadsSubmenu(\n\trpl::producer<std::vector<DownloadsEntry>> content,\n\tFn<void(uint32, DownloadsAction)> callback)\n-> FnMut<void(not_null<PopupMenu*>)>;\n\n} // namespace Ui::BotWebView\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/attach/attach_bot_webview.h\"\n\n#include \"core/file_utilities.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/attach/attach_bot_downloads.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/layers/box_content.h\"\n#include \"ui/style/style_core_palette.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/separate_panel.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/integration.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"lang/lang_keys.h\"\n#include \"webview/webview_embed.h\"\n#include \"webview/webview_dialog.h\"\n#include \"webview/webview_interface.h\"\n#include \"base/debug_log.h\"\n#include \"base/invoke_queued.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_payments.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonObject>\n#include <QtCore/QJsonArray>\n#include <QtGui/QGuiApplication>\n#include <QtGui/QClipboard>\n#include <QtGui/QWindow>\n#include <QtGui/QScreen>\n#include <QtGui/qpa/qplatformscreen.h>\n\nnamespace Ui::BotWebView {\nnamespace {\n\nconstexpr auto kProcessClickTimeout = crl::time(1000);\nconstexpr auto kClipboardReadTimeout = crl::time(10000);\nconstexpr auto kProgressDuration = crl::time(200);\nconstexpr auto kProgressOpacity = 0.3;\nconstexpr auto kLightnessThreshold = 128;\nconstexpr auto kLightnessDelta = 32;\n\nstruct ButtonArgs {\n\tbool isActive = false;\n\tbool isVisible = false;\n\tbool isProgressVisible = false;\n\tuint64 iconCustomEmojiId = 0;\n\tQString text;\n};\n\n[[nodiscard]] RectPart ParsePosition(const QString &position) {\n\tif (position == u\"left\"_q) {\n\t\treturn RectPart::Left;\n\t} else if (position == u\"top\"_q) {\n\t\treturn RectPart::Top;\n\t} else if (position == u\"right\"_q) {\n\t\treturn RectPart::Right;\n\t} else if (position == u\"bottom\"_q) {\n\t\treturn RectPart::Bottom;\n\t}\n\treturn RectPart::Left;\n}\n\n[[nodiscard]] QJsonObject ParseMethodArgs(const QString &json) {\n\tif (json.isEmpty()) {\n\t\treturn {};\n\t}\n\tauto error = QJsonParseError();\n\tconst auto dictionary = QJsonDocument::fromJson(json.toUtf8(), &error);\n\tif (error.error != QJsonParseError::NoError) {\n\t\tLOG((\"BotWebView Error: Could not parse \\\"%1\\\".\").arg(json));\n\t\treturn {};\n\t}\n\treturn dictionary.object();\n}\n\n[[nodiscard]] std::optional<QColor> ParseColor(const QString &text) {\n\tif (!text.startsWith('#') || text.size() != 7) {\n\t\treturn {};\n\t}\n\tconst auto data = text.data() + 1;\n\tconst auto hex = [&](int from) -> std::optional<int> {\n\t\tconst auto parse = [](QChar ch) -> std::optional<int> {\n\t\t\tconst auto code = ch.unicode();\n\t\t\treturn (code >= 'a' && code <= 'f')\n\t\t\t\t? std::make_optional(10 + (code - 'a'))\n\t\t\t\t: (code >= 'A' && code <= 'F')\n\t\t\t\t? std::make_optional(10 + (code - 'A'))\n\t\t\t\t: (code >= '0' && code <= '9')\n\t\t\t\t? std::make_optional(code - '0')\n\t\t\t\t: std::nullopt;\n\t\t};\n\t\tconst auto h = parse(data[from]), l = parse(data[from + 1]);\n\t\treturn (h && l) ? std::make_optional(*h * 16 + *l) : std::nullopt;\n\t};\n\tconst auto r = hex(0), g = hex(2), b = hex(4);\n\treturn (r && g && b) ? QColor(*r, *g, *b) : std::optional<QColor>();\n}\n\n[[nodiscard]] QColor ResolveRipple(QColor background) {\n\tauto hue = 0;\n\tauto saturation = 0;\n\tauto lightness = 0;\n\tauto alpha = 0;\n\tbackground.getHsv(&hue, &saturation, &lightness, &alpha);\n\treturn QColor::fromHsv(\n\t\thue,\n\t\tsaturation,\n\t\tlightness - (lightness > kLightnessThreshold\n\t\t\t? kLightnessDelta\n\t\t\t: -kLightnessDelta),\n\t\talpha);\n}\n\n[[nodiscard]] const style::color *LookupNamedColor(const QString &key) {\n\tif (key == u\"secondary_bg_color\"_q) {\n\t\treturn &st::boxDividerBg;\n\t} else if (key == u\"bottom_bar_bg_color\"_q) {\n\t\treturn &st::windowBg;\n\t}\n\treturn nullptr;\n}\n\n} // namespace\n\nclass Panel::Button final : public RippleButton {\npublic:\n\tButton(\n\t\tQWidget *parent,\n\t\tconst style::RoundButton &st,\n\t\tText::MarkedContext textContext);\n\t~Button();\n\n\tvoid updateBg(QColor bg);\n\tvoid updateBg(not_null<const style::color*> paletteBg);\n\tvoid updateFg(QColor fg);\n\tvoid updateFg(not_null<const style::color*> paletteFg);\n\n\tvoid updateArgs(ButtonArgs &&args);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\n\tvoid toggleProgress(bool shown);\n\tvoid setupProgressGeometry();\n\tvoid updateText(uint64 iconCustomEmojiId, const QString &text);\n\n\tstd::unique_ptr<Progress> _progress;\n\tUi::Text::String _text;\n\n\tText::MarkedContext _textContext;\n\tconst style::RoundButton &_st;\n\tQColor _fg;\n\tstyle::owned_color _bg;\n\tRoundRect _roundRect;\n\n\trpl::lifetime _bgLifetime;\n\trpl::lifetime _fgLifetime;\n\n};\n\nstruct Panel::Progress {\n\tProgress(QWidget *parent, Fn<QRect()> rect);\n\n\tRpWidget widget;\n\tInfiniteRadialAnimation animation;\n\tAnimations::Simple shownAnimation;\n\tbool shown = true;\n\trpl::lifetime geometryLifetime;\n};\n\nstruct Panel::WebviewWithLifetime {\n\tWebviewWithLifetime(\n\t\tQWidget *parent = nullptr,\n\t\tWebview::WindowConfig config = Webview::WindowConfig());\n\n\tWebview::Window window;\n\tstd::vector<QPointer<RpWidget>> boxes;\n\trpl::lifetime boxesLifetime;\n\trpl::lifetime lifetime;\n};\n\nPanel::Button::Button(\n\tQWidget *parent,\n\tconst style::RoundButton &st,\n\tText::MarkedContext textContext)\n: RippleButton(parent, st.ripple)\n, _textContext(std::move(textContext))\n, _st(st)\n, _bg(st::windowBgActive->c)\n, _roundRect(st::callRadius, st::windowBgActive) {\n\tresize(\n\t\t_st.padding.left() + _text.maxWidth() + _st.padding.right(),\n\t\t_st.padding.top() + _st.height + _st.padding.bottom());\n}\n\nPanel::Button::~Button() = default;\n\nvoid Panel::Button::updateText(\n\t\tuint64 iconCustomEmojiId,\n\t\tconst QString &text) {\n\tif (iconCustomEmojiId) {\n\t\tauto result = Text::SingleCustomEmoji(\n\t\t\tQString::number(iconCustomEmojiId));\n\t\tif (!text.isEmpty()) {\n\t\t\tresult.append(' ').append(text);\n\t\t}\n\t\tauto context = _textContext;\n\t\tcontext.repaint = [=] { update(); };\n\t\t_text.setMarkedText(\n\t\t\tst::semiboldTextStyle,\n\t\t\tresult,\n\t\t\tkMarkupTextOptions,\n\t\t\tcontext);\n\t} else {\n\t\t_text.setText(st::semiboldTextStyle, text);\n\t}\n}\n\nvoid Panel::Button::updateBg(QColor bg) {\n\t_bg.update(bg);\n\t_roundRect.setColor(_bg.color());\n\t_bgLifetime.destroy();\n\tupdate();\n}\n\nvoid Panel::Button::updateBg(not_null<const style::color*> paletteBg) {\n\tupdateBg((*paletteBg)->c);\n\t_bgLifetime = style::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tupdateBg((*paletteBg)->c);\n\t});\n}\n\nvoid Panel::Button::updateFg(QColor fg) {\n\t_fg = fg;\n\t_fgLifetime.destroy();\n\tupdate();\n}\n\nvoid Panel::Button::updateFg(not_null<const style::color*> paletteFg) {\n\tupdateFg((*paletteFg)->c);\n\t_fgLifetime = style::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tupdateFg((*paletteFg)->c);\n\t});\n}\n\nvoid Panel::Button::updateArgs(ButtonArgs &&args) {\n\tupdateText(args.iconCustomEmojiId, args.text);\n\tsetDisabled(!args.isActive);\n\tsetPointerCursor(false);\n\tsetCursor(args.isActive ? style::cur_pointer : Qt::ForbiddenCursor);\n\tsetVisible(args.isVisible);\n\ttoggleProgress(args.isProgressVisible);\n\tupdate();\n}\n\nvoid Panel::Button::toggleProgress(bool shown) {\n\tif (!_progress) {\n\t\tif (!shown) {\n\t\t\treturn;\n\t\t}\n\t\t_progress = std::make_unique<Progress>(\n\t\t\tthis,\n\t\t\t[=] { return _progress->widget.rect(); });\n\t\t_progress->widget.paintRequest(\n\t\t) | rpl::on_next([=](QRect clip) {\n\t\t\tauto p = QPainter(&_progress->widget);\n\t\t\tp.setOpacity(\n\t\t\t\t_progress->shownAnimation.value(_progress->shown ? 1. : 0.));\n\t\t\tauto thickness = st::paymentsLoading.thickness;\n\t\t\tconst auto rect = _progress->widget.rect().marginsRemoved(\n\t\t\t\t{ thickness, thickness, thickness, thickness });\n\t\t\tInfiniteRadialAnimation::Draw(\n\t\t\t\tp,\n\t\t\t\t_progress->animation.computeState(),\n\t\t\t\trect.topLeft(),\n\t\t\t\trect.size() - QSize(),\n\t\t\t\t_progress->widget.width(),\n\t\t\t\t_fg,\n\t\t\t\tthickness);\n\t\t}, _progress->widget.lifetime());\n\t\t_progress->widget.show();\n\t\t_progress->animation.start();\n\t} else if (_progress->shown == shown) {\n\t\treturn;\n\t}\n\tconst auto callback = [=] {\n\t\tif (!_progress->shownAnimation.animating() && !_progress->shown) {\n\t\t\t_progress = nullptr;\n\t\t} else {\n\t\t\t_progress->widget.update();\n\t\t}\n\t};\n\t_progress->shown = shown;\n\t_progress->shownAnimation.start(\n\t\tcallback,\n\t\tshown ? 0. : 1.,\n\t\tshown ? 1. : 0.,\n\t\tkProgressDuration);\n\tif (shown) {\n\t\tsetupProgressGeometry();\n\t}\n}\n\nvoid Panel::Button::setupProgressGeometry() {\n\tif (!_progress || !_progress->shown) {\n\t\treturn;\n\t}\n\t_progress->geometryLifetime.destroy();\n\tsizeValue(\n\t) | rpl::on_next([=](QSize outer) {\n\t\tconst auto height = outer.height();\n\t\tconst auto size = st::paymentsLoading.size;\n\t\tconst auto skip = (height - size.height()) / 2;\n\t\tconst auto right = outer.width();\n\t\tconst auto top = outer.height() - height;\n\t\t_progress->widget.setGeometry(QRect{\n\t\t\tQPoint(right - skip - size.width(), top + skip),\n\t\t\tsize });\n\t}, _progress->geometryLifetime);\n\n\t_progress->widget.show();\n\t_progress->widget.raise();\n\tif (_progress->shown\n\t\t&& Ui::AppInFocus()\n\t\t&& Ui::InFocusChain(_progress->widget.window())) {\n\t\t_progress->widget.setFocus();\n\t}\n}\n\nvoid Panel::Button::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\t_roundRect.paint(p, rect());\n\n\tif (!isDisabled()) {\n\t\tconst auto ripple = ResolveRipple(_bg.color()->c);\n\t\tpaintRipple(p, rect().topLeft(), &ripple);\n\t}\n\n\tp.setFont(_st.style.font);\n\n\tconst auto height = rect().height();\n\tconst auto progress = st::paymentsLoading.size;\n\tconst auto minPad = (height - progress.height()) / 2;\n\tconst auto rightReserved = _progress\n\t\t? (minPad + progress.width() + minPad)\n\t\t: minPad;\n\tconst auto maxTextEnd = width() - rightReserved;\n\tconst auto maxSpace = std::max(maxTextEnd - minPad, 0);\n\tconst auto textWidth = std::min(_text.maxWidth(), maxSpace);\n\tconst auto centered = (width() - textWidth) / 2;\n\tconst auto textLeft = std::max(\n\t\tstd::min(centered, maxTextEnd - textWidth),\n\t\tminPad);\n\tconst auto textTop = _st.padding.top() + _st.textTop;\n\tp.setPen(_fg);\n\t_text.drawLeftElided(p, textLeft, textTop, textWidth, width());\n}\n\nQImage Panel::Button::prepareRippleMask() const {\n\treturn RippleAnimation::RoundRectMask(size(), st::callRadius);\n}\n\nQPoint Panel::Button::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos())\n\t\t- QPoint(_st.padding.left(), _st.padding.top());\n}\n\nPanel::WebviewWithLifetime::WebviewWithLifetime(\n\tQWidget *parent,\n\tWebview::WindowConfig config)\n: window(parent, std::move(config)) {\n}\n\nPanel::Progress::Progress(QWidget *parent, Fn<QRect()> rect)\n: widget(parent)\n, animation(\n\t[=] { if (!anim::Disabled()) widget.update(rect()); },\n\tst::paymentsLoading) {\n}\n\nPanel::Panel(Args &&args)\n: _storageId(args.storageId)\n, _delegate(args.delegate)\n, _menuButtons(args.menuButtons)\n, _widget(std::make_unique<SeparatePanel>(Ui::SeparatePanelArgs{\n\t.menuSt = &st::botWebViewMenu,\n}))\n, _fullscreen(args.fullscreen)\n, _allowClipboardRead(args.allowClipboardRead) {\n\t_widget->setWindowFlag(Qt::WindowStaysOnTopHint, false);\n\t_widget->setInnerSize(st::botWebViewPanelSize, true);\n\n\tconst auto panel = _widget.get();\n\trpl::duplicate(\n\t\targs.title\n\t) | rpl::on_next([=](const QString &title) {\n\t\tconst auto value = tr::lng_credits_box_history_entry_miniapp(tr::now)\n\t\t\t+ u\": \"_q\n\t\t\t+ title;\n\t\tpanel->window()->setWindowTitle(value);\n\t}, panel->lifetime());\n\n\tconst auto params = _delegate->botThemeParams();\n\tupdateColorOverrides(params);\n\n\t_fullscreen.value(\n\t) | rpl::on_next([=](bool fullscreen) {\n\t\t_widget->toggleFullScreen(fullscreen);\n\t\tlayoutButtons();\n\t\tsendFullScreen();\n\t\tsendSafeArea();\n\t\tsendContentSafeArea();\n\t}, _widget->lifetime());\n\n\t_widget->fullScreenValue(\n\t) | rpl::on_next([=](bool fullscreen) {\n\t\t_fullscreen = fullscreen;\n\t}, _widget->lifetime());\n\n\t_widget->closeRequests(\n\t) | rpl::on_next([=] {\n\t\tif (_closeNeedConfirmation) {\n\t\t\tscheduleCloseWithConfirmation();\n\t\t} else {\n\t\t\t_delegate->botClose();\n\t\t}\n\t}, _widget->lifetime());\n\n\t_widget->closeEvents(\n\t) | rpl::filter([=] {\n\t\treturn !_hiddenForPayment;\n\t}) | rpl::on_next([=] {\n\t\t_delegate->botClose();\n\t}, _widget->lifetime());\n\n\t_widget->backRequests(\n\t) | rpl::on_next([=] {\n\t\tpostEvent(\"back_button_pressed\");\n\t}, _widget->lifetime());\n\n\trpl::merge(\n\t\tstyle::PaletteChanged(),\n\t\t_themeUpdateForced.events()\n\t) | rpl::filter([=] {\n\t\treturn !_themeUpdateScheduled;\n\t}) | rpl::on_next([=] {\n\t\t_themeUpdateScheduled = true;\n\t\tcrl::on_main(_widget.get(), [=] {\n\t\t\t_themeUpdateScheduled = false;\n\t\t\tupdateThemeParams(_delegate->botThemeParams());\n\t\t});\n\t}, _widget->lifetime());\n\n\tsetTitle(std::move(args.title));\n\t_widget->setTitleBadge(std::move(args.titleBadge));\n\n\tif (!showWebview(std::move(args), params)) {\n\t\tconst auto available = Webview::Availability();\n\t\tif (available.error != Webview::Available::Error::None) {\n\t\t\tshowWebviewError(tr::lng_bot_no_webview(tr::now), available);\n\t\t} else {\n\t\t\tshowCriticalError({ \"Error: Could not initialize WebView.\" });\n\t\t}\n\t}\n}\n\nPanel::~Panel() {\n\tbase::take(_webview);\n\t_progress = nullptr;\n\t_widget = nullptr;\n}\n\nvoid Panel::setupDownloadsProgress(\n\t\tnot_null<RpWidget*> button,\n\t\trpl::producer<DownloadsProgress> progress,\n\t\tbool fullscreen) {\n\tconst auto widget = Ui::CreateChild<RpWidget>(button.get());\n\twidget->show();\n\twidget->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tbutton->sizeValue() | rpl::on_next([=](QSize size) {\n\t\twidget->setGeometry(QRect(QPoint(), size));\n\t}, widget->lifetime());\n\n\tstruct State {\n\t\tState(QWidget *parent)\n\t\t: animation([=](crl::time now) {\n\t\t\tconst auto total = progress.total;\n\t\t\tconst auto current = total\n\t\t\t\t? (progress.ready / float64(total))\n\t\t\t\t: 0.;\n\t\t\tconst auto updated = animation.update(current, false, now);\n\t\t\tif (!anim::Disabled() || updated) {\n\t\t\t\tparent->update();\n\t\t\t}\n\t\t}) {\n\t\t}\n\n\t\tDownloadsProgress progress;\n\t\tRadialAnimation animation;\n\t\tAnimations::Simple fade;\n\t\tbool shown = false;\n\t};\n\tconst auto state = widget->lifetime().make_state<State>(widget);\n\tstd::move(\n\t\tprogress\n\t) | rpl::on_next([=](DownloadsProgress progress) {\n\t\tconst auto toggle = [&](bool shown) {\n\t\t\tif (state->shown == shown) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->shown = shown;\n\t\t\tif (shown && !state->fade.animating()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->fade.start([=] {\n\t\t\t\twidget->update();\n\t\t\t\tif (!state->shown\n\t\t\t\t\t&& !state->fade.animating()\n\t\t\t\t\t&& (!state->progress.total\n\t\t\t\t\t\t|| (state->progress.ready\n\t\t\t\t\t\t\t== state->progress.total))) {\n\t\t\t\t\tstate->animation.stop();\n\t\t\t\t}\n\t\t\t}, shown ? 0. : 2., shown ? 2. : 0., st::radialDuration * 2);\n\t\t};\n\t\tif (!state->shown && progress.loading) {\n\t\t\tif (!state->animation.animating()) {\n\t\t\t\tstate->animation.start(0.);\n\t\t\t}\n\t\t\ttoggle(true);\n\t\t} else if ((state->progress.total && !progress.total)\n\t\t\t|| (state->progress.ready < state->progress.total\n\t\t\t\t&& progress.ready == progress.total)) {\n\t\t\tstate->animation.update(1., false, crl::now());\n\t\t\ttoggle(false);\n\t\t}\n\t\tstate->progress = progress;\n\t}, widget->lifetime());\n\n\twidget->paintRequest() | rpl::on_next([=] {\n\t\tconst auto opacity = std::clamp(\n\t\t\tstate->fade.value(state->shown ? 2. : 0.) - 1.,\n\t\t\t0.,\n\t\t\t1.);\n\t\tif (!opacity) {\n\t\t\treturn;\n\t\t}\n\t\tauto p = QPainter(widget);\n\t\tp.setOpacity(opacity);\n\t\tconst auto palette = _widget->titleOverridePalette();\n\t\tconst auto color = fullscreen\n\t\t\t? st::radialFg\n\t\t\t: palette\n\t\t\t? palette->boxTitleCloseFg()\n\t\t\t: st::paymentsLoading.color;\n\t\tconst auto &st = fullscreen\n\t\t\t? st::fullScreenPanelMenu\n\t\t\t: st::separatePanelMenu;\n\t\tconst auto size = st.rippleAreaSize;\n\t\tconst auto rect = QRect(st.rippleAreaPosition, QSize(size, size));\n\t\tconst auto stroke = st::botWebViewRadialStroke;\n\t\tconst auto shift = stroke * 1.5;\n\t\tconst auto inner = QRectF(rect).marginsRemoved(\n\t\t\tQMarginsF{ shift, shift, shift, shift });\n\t\tstate->animation.draw(p, inner, stroke, color);\n\t}, widget->lifetime());\n}\n\nvoid Panel::requestActivate() {\n\t_widget->showAndActivate();\n\tif (const auto widget = _webview ? _webview->window.widget() : nullptr) {\n\t\tInvokeQueued(widget, [=] {\n\t\t\tif (widget->isVisible()) {\n\t\t\t\t_webview->window.focus();\n\t\t\t}\n\t\t});\n\t}\n}\n\nvoid Panel::toggleProgress(bool shown) {\n\tif (!_progress) {\n\t\tif (!shown) {\n\t\t\treturn;\n\t\t}\n\t\t_progress = std::make_unique<Progress>(\n\t\t\t_widget.get(),\n\t\t\t[=] { return progressRect(); });\n\t\t_progress->widget.paintRequest(\n\t\t) | rpl::on_next([=](QRect clip) {\n\t\t\tauto p = QPainter(&_progress->widget);\n\t\t\tp.setOpacity(\n\t\t\t\t_progress->shownAnimation.value(_progress->shown ? 1. : 0.));\n\t\t\tconst auto thickness = st::paymentsLoading.thickness;\n\t\t\tif (progressWithBackground()) {\n\t\t\t\tauto color = st::windowBg->c;\n\t\t\t\tcolor.setAlphaF(kProgressOpacity);\n\t\t\t\tp.fillRect(clip, color);\n\t\t\t}\n\t\t\tconst auto rect = progressRect() - Margins(thickness);\n\t\t\tInfiniteRadialAnimation::Draw(\n\t\t\t\tp,\n\t\t\t\t_progress->animation.computeState(),\n\t\t\t\trect.topLeft(),\n\t\t\t\trect.size() - QSize(),\n\t\t\t\t_progress->widget.width(),\n\t\t\t\tst::paymentsLoading.color,\n\t\t\t\tanim::Disabled() ? (thickness / 2.) : thickness);\n\t\t}, _progress->widget.lifetime());\n\t\t_progress->widget.show();\n\t\t_progress->animation.start();\n\t} else if (_progress->shown == shown) {\n\t\treturn;\n\t}\n\tconst auto callback = [=] {\n\t\tif (!_progress->shownAnimation.animating() && !_progress->shown) {\n\t\t\t_progress = nullptr;\n\t\t} else {\n\t\t\t_progress->widget.update();\n\t\t}\n\t};\n\t_progress->shown = shown;\n\t_progress->shownAnimation.start(\n\t\tcallback,\n\t\tshown ? 0. : 1.,\n\t\tshown ? 1. : 0.,\n\t\tkProgressDuration);\n\tif (shown) {\n\t\tsetupProgressGeometry();\n\t}\n}\n\nbool Panel::progressWithBackground() const {\n\treturn (_progress->widget.width() == _widget->innerGeometry().width());\n}\n\nQRect Panel::progressRect() const {\n\tconst auto rect = _progress->widget.rect();\n\tif (!progressWithBackground()) {\n\t\treturn rect;\n\t}\n\tconst auto size = st::defaultBoxButton.height;\n\treturn QRect(\n\t\trect.x() + (rect.width() - size) / 2,\n\t\trect.y() + (rect.height() - size) / 2,\n\t\tsize,\n\t\tsize);\n}\n\nvoid Panel::setupProgressGeometry() {\n\tif (!_progress || !_progress->shown) {\n\t\treturn;\n\t}\n\t_progress->geometryLifetime.destroy();\n\tif (_webviewBottom) {\n\t\t_webviewBottom->geometryValue(\n\t\t) | rpl::on_next([=](QRect bottom) {\n\t\t\tconst auto height = bottom.height();\n\t\t\tconst auto size = st::paymentsLoading.size;\n\t\t\tconst auto skip = (height - size.height()) / 2;\n\t\t\tconst auto inner = _widget->innerGeometry();\n\t\t\tconst auto right = inner.x() + inner.width();\n\t\t\tconst auto top = inner.y() + inner.height() - height;\n\t\t\t// This doesn't work, because first we get the correct bottom\n\t\t\t// geometry and after that we get the previous event (which\n\t\t\t// triggered the 'fire' of correct geometry before getting here).\n\t\t\t//const auto right = bottom.x() + bottom.width();\n\t\t\t//const auto top = bottom.y();\n\t\t\t_progress->widget.setGeometry(QRect{\n\t\t\t\tQPoint(right - skip - size.width(), top + skip),\n\t\t\t\tsize });\n\t\t}, _progress->geometryLifetime);\n\t}\n\t_progress->widget.show();\n\t_progress->widget.raise();\n\tif (_progress->shown) {\n\t\t_progress->widget.setFocus();\n\t}\n}\n\nvoid Panel::showWebviewProgress() {\n\tif (_webviewProgress && _progress && _progress->shown) {\n\t\treturn;\n\t}\n\t_webviewProgress = true;\n\ttoggleProgress(true);\n}\n\nvoid Panel::hideWebviewProgress() {\n\tif (!_webviewProgress) {\n\t\treturn;\n\t}\n\t_webviewProgress = false;\n\ttoggleProgress(false);\n}\n\nbool Panel::showWebview(Args &&args, const Webview::ThemeParams &params) {\n\t_bottomText = std::move(args.bottom);\n\tif (!_webview && !createWebview(params)) {\n\t\treturn false;\n\t}\n\tconst auto allowBack = false;\n\tshowWebviewProgress();\n\t_widget->hideLayer(anim::type::instant);\n\tupdateThemeParams(params);\n\tconst auto url = args.url;\n\t_webview->window.navigate(url);\n\t_widget->setBackAllowed(allowBack);\n\n\trpl::duplicate(args.downloadsProgress) | rpl::on_next([=] {\n\t\t_downloadsUpdated.fire({});\n\t}, lifetime());\n\n\tif (args.forkAdditionalButtons) {\n\t\tconst auto refreshButton = Ui::CreateChild<Ui::LinkButton>(\n\t\t\t_widget.get(),\n\t\t\tu\"refresh\"_q);\n\t\trefreshButton->setClickedCallback([=] {\n\t\t\tif (_webview && _webview->window.widget()) {\n\t\t\t\t_webview->window.reload();\n\t\t\t} else if (const auto params = _delegate->botThemeParams()\n\t\t\t\t; createWebview(params)) {\n\t\t\t\tshowWebviewProgress();\n\t\t\t\tupdateThemeParams(params);\n\t\t\t\t_webview->window.navigate(url);\n\t\t\t}\n\t\t});\n\t\t_widget->sizeValue() | rpl::on_next([=](const QSize &s) {\n\t\t\trefreshButton->move(\n\t\t\t\ts.width() - refreshButton->width() - 20,\n\t\t\t\ts.height() - 40);\n\t\t\trefreshButton->show();\n\t\t\trefreshButton->raise();\n\t\t}, refreshButton->lifetime());\n\t}\n\n\t_widget->setMenuAllowed([=](\n\t\t\tconst Ui::Menu::MenuCallback &callback) {\n\t\tauto list = _delegate->botDownloads(true);\n\t\tif (!list.empty()) {\n\t\t\tauto value = rpl::single(\n\t\t\t\tstd::move(list)\n\t\t\t) | rpl::then(_downloadsUpdated.events(\n\t\t\t) | rpl::map([=] {\n\t\t\t\treturn _delegate->botDownloads();\n\t\t\t}));\n\t\t\tconst auto action = [=](uint32 id, DownloadsAction type) {\n\t\t\t\t_delegate->botDownloadsAction(id, type);\n\t\t\t};\n\t\t\tcallback(Ui::Menu::MenuCallback::Args{\n\t\t\t\t.text = tr::lng_downloads_section(tr::now),\n\t\t\t\t.icon = &st::menuIconDownload,\n\t\t\t\t.fillSubmenu = FillAttachBotDownloadsSubmenu(\n\t\t\t\t\tstd::move(value),\n\t\t\t\t\taction),\n\t\t\t});\n\t\t\tcallback({\n\t\t\t\t.separatorSt = &st::expandedMenuSeparator,\n\t\t\t\t.isSeparator = true,\n\t\t\t});\n\t\t}\n\t\tif (_webview && _webview->window.widget() && _hasSettingsButton) {\n\t\t\tcallback(tr::lng_bot_settings(tr::now), [=] {\n\t\t\t\tpostEvent(\"settings_button_pressed\");\n\t\t\t}, &st::menuIconSettings);\n\t\t}\n\t\tif (_menuButtons & MenuButton::OpenBot) {\n\t\t\tcallback(tr::lng_bot_open(tr::now), [=] {\n\t\t\t\t_delegate->botHandleMenuButton(MenuButton::OpenBot);\n\t\t\t}, &st::menuIconLeave);\n\t\t}\n\t\tcallback(tr::lng_bot_reload_page(tr::now), [=] {\n\t\t\tif (_webview && _webview->window.widget()) {\n\t\t\t\t_webview->window.reload();\n\t\t\t} else if (const auto params = _delegate->botThemeParams()\n\t\t\t\t; createWebview(params)) {\n\t\t\t\tshowWebviewProgress();\n\t\t\t\tupdateThemeParams(params);\n\t\t\t\t_webview->window.navigate(url);\n\t\t\t}\n\t\t}, &st::menuIconRestore);\n\t\tif (_menuButtons & MenuButton::ShareGame) {\n\t\t\tcallback(tr::lng_iv_share(tr::now), [=] {\n\t\t\t\t_delegate->botHandleMenuButton(MenuButton::ShareGame);\n\t\t\t}, &st::menuIconShare);\n\t\t} else {\n\t\t\tcallback(tr::lng_bot_terms(tr::now), [=] {\n\t\t\t\tFile::OpenUrl(tr::lng_mini_apps_tos_url(tr::now));\n\t\t\t}, &st::menuIconGroupLog);\n\t\t\tcallback(tr::lng_bot_privacy(tr::now), [=] {\n\t\t\t\t_delegate->botOpenPrivacyPolicy();\n\t\t\t}, &st::menuIconAntispam);\n\t\t}\n\t\tconst auto main = (_menuButtons & MenuButton::RemoveFromMainMenu);\n\t\tif (main || (_menuButtons & MenuButton::RemoveFromMenu)) {\n\t\t\tconst auto handler = [=] {\n\t\t\t\t_delegate->botHandleMenuButton(main\n\t\t\t\t\t? MenuButton::RemoveFromMainMenu\n\t\t\t\t\t: MenuButton::RemoveFromMenu);\n\t\t\t};\n\t\t\tcallback({\n\t\t\t\t.text = (main\n\t\t\t\t\t? tr::lng_bot_remove_from_side_menu\n\t\t\t\t\t: tr::lng_bot_remove_from_menu)(tr::now),\n\t\t\t\t.handler = handler,\n\t\t\t\t.icon = &st::menuIconDeleteAttention,\n\t\t\t\t.isAttention = true,\n\t\t\t});\n\t\t}\n\t}, [=, progress = std::move(args.downloadsProgress)](\n\t\t\tnot_null<RpWidget*> button,\n\t\t\tbool fullscreen) {\n\t\tsetupDownloadsProgress(\n\t\t\tbutton,\n\t\t\trpl::duplicate(progress),\n\t\t\tfullscreen);\n\t});\n\n\treturn true;\n}\n\nvoid Panel::createWebviewBottom() {\n\t_webviewBottom = std::make_unique<RpWidget>(_widget.get());\n\tconst auto bottom = _webviewBottom.get();\n\tbottom->setVisible(!_fullscreen.current());\n\n\tconst auto &padding = st::paymentsPanelPadding;\n\tconst auto label = CreateChild<FlatLabel>(\n\t\t_webviewBottom.get(),\n\t\t_bottomText.value(),\n\t\tst::paymentsWebviewBottom);\n\t_webviewBottomLabel = label;\n\n\tconst auto height = padding.top()\n\t\t+ label->heightNoMargins()\n\t\t+ padding.bottom();\n\trpl::combine(\n\t\t_webviewBottom->widthValue(),\n\t\tlabel->widthValue()\n\t) | rpl::on_next([=](int outerWidth, int width) {\n\t\tlabel->move((outerWidth - width) / 2, padding.top());\n\t}, label->lifetime());\n\tlabel->show();\n\t_webviewBottom->resize(_webviewBottom->width(), height);\n\n\trpl::combine(\n\t\t_webviewParent->geometryValue() | rpl::map([=] {\n\t\t\treturn _widget->innerGeometry();\n\t\t}),\n\t\tbottom->heightValue()\n\t) | rpl::on_next([=](QRect inner, int height) {\n\t\tbottom->move(inner.x(), inner.y() + inner.height() - height);\n\t\tbottom->resizeToWidth(inner.width());\n\t\tlayoutButtons();\n\t}, bottom->lifetime());\n}\n\nbool Panel::createWebview(const Webview::ThemeParams &params) {\n\tauto outer = base::make_unique_q<RpWidget>(_widget.get());\n\tconst auto container = outer.get();\n\t_widget->showInner(std::move(outer));\n\t_webviewParent = container;\n\n\t_headerColorReceived = false;\n\t_bodyColorReceived = false;\n\t_bottomColorReceived = false;\n\tupdateColorOverrides(params);\n\tcreateWebviewBottom();\n\n\tcontainer->show();\n\t_webview = std::make_unique<WebviewWithLifetime>(\n\t\tcontainer,\n\t\tWebview::WindowConfig{\n\t\t\t.opaqueBg = params.bodyBg,\n\t\t\t.storageId = _storageId,\n\t\t});\n\tconst auto raw = &_webview->window;\n\n\tconst auto bottom = _webviewBottom.get();\n\tQObject::connect(container, &QObject::destroyed, [=] {\n\t\tif (_webview && &_webview->window == raw) {\n\t\t\tbase::take(_webview);\n\t\t\tif (_webviewProgress) {\n\t\t\t\thideWebviewProgress();\n\t\t\t\tif (_progress && !_progress->shown) {\n\t\t\t\t\t_progress = nullptr;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (_webviewBottom.get() == bottom) {\n\t\t\t_webviewBottomLabel = nullptr;\n\t\t\t_webviewBottom = nullptr;\n\t\t\t_secondaryButton = nullptr;\n\t\t\t_mainButton = nullptr;\n\t\t\t_bottomButtonsBg = nullptr;\n\t\t}\n\t});\n\tif (!raw->widget()) {\n\t\treturn false;\n\t}\n\n#if !defined Q_OS_WIN && !defined Q_OS_MAC\n\t_widget->allowChildFullScreenControls(\n\t\t!raw->widget()->inherits(\"QWindowContainer\"));\n#endif // !Q_OS_WIN && !Q_OS_MAC\n\n\traw->setInteractionHandler([=] {\n\t\t_lastWebviewInteraction = crl::now();\n\t});\n\n\tQObject::connect(raw->widget(), &QObject::destroyed, [=] {\n\t\tconst auto parent = _webviewParent.data();\n\t\tif (!_webview\n\t\t\t|| &_webview->window != raw\n\t\t\t|| !parent\n\t\t\t|| _widget->inner() != parent) {\n\t\t\t// If we destroyed _webview ourselves,\n\t\t\t// or if we changed _widget->inner ourselves,\n\t\t\t// we don't show any message, nothing crashed.\n\t\t\treturn;\n\t\t}\n\t\tcrl::on_main(this, [=] {\n\t\t\tshowCriticalError({ \"Error: WebView has crashed.\" });\n\t\t});\n\t});\n\n\trpl::combine(\n\t\tcontainer->geometryValue(),\n\t\t_footerHeight.value()\n\t) | rpl::on_next([=](QRect geometry, int footer) {\n\t\tif (const auto view = raw->widget()) {\n\t\t\tview->setGeometry(geometry.marginsRemoved({ 0, 0, 0, footer }));\n\t\t\tcrl::on_main(view, [=] {\n\t\t\t\tsendViewport();\n\t\t\t\tInvokeQueued(view, [=] { sendViewport(); });\n\t\t\t});\n\t\t}\n\t}, _webview->lifetime);\n\n\traw->setMessageHandler([=](const QJsonDocument &message) {\n\t\tif (!message.isArray()) {\n\t\t\tLOG((\"BotWebView Error: \"\n\t\t\t\t\"Not an array received in buy_callback arguments.\"));\n\t\t\treturn;\n\t\t}\n\t\tconst auto list = message.array();\n\t\tconst auto command = list.at(0).toString();\n\t\tconst auto arguments = ParseMethodArgs(list.at(1).toString());\n\t\tif (command == \"web_app_close\") {\n\t\t\t_delegate->botClose();\n\t\t} else if (command == \"web_app_data_send\") {\n\t\t\tsendDataMessage(arguments);\n\t\t} else if (command == \"web_app_switch_inline_query\") {\n\t\t\tswitchInlineQueryMessage(arguments);\n\t\t} else if (command == \"web_app_setup_main_button\") {\n\t\t\tprocessButtonMessage(_mainButton, arguments);\n\t\t} else if (command == \"web_app_setup_secondary_button\") {\n\t\t\tprocessButtonMessage(_secondaryButton, arguments);\n\t\t} else if (command == \"web_app_setup_back_button\") {\n\t\t\tprocessBackButtonMessage(arguments);\n\t\t} else if (command == \"web_app_setup_settings_button\") {\n\t\t\tprocessSettingsButtonMessage(arguments);\n\t\t} else if (command == \"web_app_request_theme\") {\n\t\t\t_themeUpdateForced.fire({});\n\t\t} else if (command == \"web_app_request_viewport\") {\n\t\t\tsendViewport();\n\t\t} else if (command == \"web_app_request_safe_area\") {\n\t\t\tsendSafeArea();\n\t\t} else if (command == \"web_app_request_content_safe_area\") {\n\t\t\tsendContentSafeArea();\n\t\t} else if (command == \"web_app_request_fullscreen\") {\n\t\t\tif (!_fullscreen.current()) {\n\t\t\t\t_fullscreen = true;\n\t\t\t} else {\n\t\t\t\tsendFullScreen();\n\t\t\t}\n\t\t} else if (command == \"web_app_request_file_download\") {\n\t\t\tprocessDownloadRequest(arguments);\n\t\t} else if (command == \"web_app_exit_fullscreen\") {\n\t\t\tif (_fullscreen.current()) {\n\t\t\t\t_fullscreen = false;\n\t\t\t} else {\n\t\t\t\tsendFullScreen();\n\t\t\t}\n\t\t} else if (command == \"web_app_check_home_screen\") {\n\t\t\tpostEvent(\"home_screen_checked\", \"{ status: \\\"unsupported\\\" }\");\n\t\t} else if (command == \"web_app_start_accelerometer\") {\n\t\t\tpostEvent(\"accelerometer_failed\", \"{ error: \\\"UNSUPPORTED\\\" }\");\n\t\t} else if (command == \"web_app_start_device_orientation\") {\n\t\t\tpostEvent(\n\t\t\t\t\"device_orientation_failed\",\n\t\t\t\t\"{ error: \\\"UNSUPPORTED\\\" }\");\n\t\t} else if (command == \"web_app_start_gyroscope\") {\n\t\t\tpostEvent(\"gyroscope_failed\", \"{ error: \\\"UNSUPPORTED\\\" }\");\n\t\t} else if (command == \"web_app_check_location\") {\n\t\t\tpostEvent(\"location_checked\", \"{ available: false }\");\n\t\t} else if (command == \"web_app_request_location\") {\n\t\t\tpostEvent(\"location_requested\", \"{ available: false }\");\n\t\t} else if (command == \"web_app_biometry_get_info\") {\n\t\t\tpostEvent(\"biometry_info_received\", \"{ available: false }\");\n\t\t} else if (command == \"web_app_open_tg_link\") {\n\t\t\topenTgLink(arguments);\n\t\t} else if (command == \"web_app_open_link\") {\n\t\t\topenExternalLink(arguments);\n\t\t} else if (command == \"web_app_open_invoice\") {\n\t\t\topenInvoice(arguments);\n\t\t} else if (command == \"web_app_open_popup\") {\n\t\t\topenPopup(arguments);\n\t\t} else if (command == \"web_app_open_scan_qr_popup\") {\n\t\t\topenScanQrPopup(arguments);\n\t\t} else if (command == \"web_app_share_to_story\") {\n\t\t\topenShareStory(arguments);\n\t\t} else if (command == \"web_app_request_write_access\") {\n\t\t\trequestWriteAccess();\n\t\t} else if (command == \"web_app_request_phone\") {\n\t\t\trequestPhone();\n\t\t} else if (command == \"web_app_invoke_custom_method\") {\n\t\t\tinvokeCustomMethod(arguments);\n\t\t} else if (command == \"web_app_setup_closing_behavior\") {\n\t\t\tsetupClosingBehaviour(arguments);\n\t\t} else if (command == \"web_app_read_text_from_clipboard\") {\n\t\t\trequestClipboardText(arguments);\n\t\t} else if (command == \"web_app_set_header_color\") {\n\t\t\tprocessHeaderColor(arguments);\n\t\t} else if (command == \"web_app_set_background_color\") {\n\t\t\tprocessBackgroundColor(arguments);\n\t\t} else if (command == \"web_app_set_bottom_bar_color\") {\n\t\t\tprocessBottomBarColor(arguments);\n\t\t} else if (command == \"web_app_send_prepared_message\") {\n\t\t\tprocessSendMessageRequest(arguments);\n\t\t} else if (command == \"web_app_request_chat\") {\n\t\t\tprocessRequestChat(arguments);\n\t\t} else if (command == \"web_app_set_emoji_status\") {\n\t\t\tprocessEmojiStatusRequest(arguments);\n\t\t} else if (command == \"web_app_request_emoji_status_access\") {\n\t\t\tprocessEmojiStatusAccessRequest();\n\t\t} else if (command == \"web_app_device_storage_save_key\") {\n\t\t\tprocessStorageSaveKey(arguments);\n\t\t} else if (command == \"web_app_device_storage_get_key\") {\n\t\t\tprocessStorageGetKey(arguments);\n\t\t} else if (command == \"web_app_device_storage_clear\") {\n\t\t\tprocessStorageClear(arguments);\n\t\t} else if (command == \"web_app_secure_storage_save_key\") {\n\t\t\tsecureStorageFailed(arguments);\n\t\t} else if (command == \"web_app_secure_storage_get_key\") {\n\t\t\tsecureStorageFailed(arguments);\n\t\t} else if (command == \"web_app_secure_storage_restore_key\") {\n\t\t\tsecureStorageFailed(arguments);\n\t\t} else if (command == \"web_app_secure_storage_clear\") {\n\t\t\tsecureStorageFailed(arguments);\n\t\t} else if (command == \"web_app_verify_age\") {\n\t\t\tconst auto passed = arguments[\"passed\"];\n\t\t\tconst auto detected = arguments[\"age\"];\n\t\t\tconst auto valid = passed.isBool()\n\t\t\t\t&& passed.toBool()\n\t\t\t\t&& detected.isDouble();\n\t\t\tconst auto age = valid\n\t\t\t\t? int(std::floor(detected.toDouble()))\n\t\t\t\t: 0;\n\t\t\t_delegate->botVerifyAge(age);\n\t\t} else if (command == \"share_score\") {\n\t\t\t_delegate->botHandleMenuButton(MenuButton::ShareGame);\n\t\t}\n\t});\n\n\traw->setNavigationStartHandler([=](const QString &uri, bool newWindow) {\n\t\tif (_delegate->botHandleLocalUri(uri, false)) {\n\t\t\treturn false;\n\t\t} else if (newWindow) {\n\t\t\treturn true;\n\t\t}\n\t\tshowWebviewProgress();\n\t\treturn true;\n\t});\n\traw->setNavigationDoneHandler([=](bool success) {\n\t\thideWebviewProgress();\n\t});\n\n\traw->init(R\"(\nwindow.TelegramWebviewProxy = {\npostEvent: function(eventType, eventData) {\n\tif (window.external && window.external.invoke) {\n\t\twindow.external.invoke(JSON.stringify([eventType, eventData]));\n\t}\n}\n};)\");\n\n\tif (!_webview) {\n\t\treturn false;\n\t}\n\n\tlayoutButtons();\n\tsetupProgressGeometry();\n\n\tbase::qt_signal_producer(\n\t\tqApp,\n\t\t&QGuiApplication::focusWindowChanged\n\t) | rpl::filter([=](QWindow *focused) {\n\t\tconst auto handle = _widget->window()->windowHandle();\n\t\tconst auto widget = _webview ? _webview->window.widget() : nullptr;\n\t\treturn widget\n\t\t\t&& !widget->isHidden()\n\t\t\t&& handle\n\t\t\t&& (focused == handle);\n\t}) | rpl::on_next([=] {\n\t\t_webview->window.focus();\n\t}, _webview->lifetime);\n\n\treturn true;\n}\n\nvoid Panel::sendViewport() {\n\tpostEvent(\"viewport_changed\", \"{ \"\n\t\t\"height: window.innerHeight, \"\n\t\t\"is_state_stable: true, \"\n\t\t\"is_expanded: true }\");\n}\n\nvoid Panel::sendFullScreen() {\n\tpostEvent(\"fullscreen_changed\", _fullscreen.current()\n\t\t? \"{ is_fullscreen: true }\"\n\t\t: \"{ is_fullscreen: false }\");\n}\n\nvoid Panel::sendSafeArea() {\n\tpostEvent(\"safe_area_changed\",\n\t\t\"{ top: 0, right: 0, bottom: 0, left: 0 }\");\n}\n\nvoid Panel::sendContentSafeArea() {\n\tconst auto shift = st::separatePanelClose.rippleAreaPosition.y();\n\tconst auto top = _fullscreen.current()\n\t\t? (shift + st::fullScreenPanelClose.height + (shift / 2))\n\t\t: 0;\n\tconst auto scaled = top * style::DevicePixelRatio();\n\tauto report = 0;\n\tif (const auto screen = QGuiApplication::primaryScreen()) {\n\t\tconst auto dpi = screen->logicalDotsPerInch();\n\t\tconst auto ratio = screen->devicePixelRatio();\n\t\tconst auto basePair = screen->handle()->logicalBaseDpi();\n\t\tconst auto base = (basePair.first + basePair.second) * 0.5;\n\t\tconst auto systemScreenScale = dpi * ratio / base;\n\t\treport = int(base::SafeRound(scaled / systemScreenScale));\n\t}\n\tpostEvent(\"content_safe_area_changed\",\n\t\tu\"{ top: %1, right: 0, bottom: 0, left: 0 }\"_q.arg(report));\n}\n\nvoid Panel::setTitle(rpl::producer<QString> title) {\n\t_widget->setTitle(std::move(title));\n}\n\nvoid Panel::sendDataMessage(const QJsonObject &args) {\n\tif (args.isEmpty()) {\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\tconst auto data = args[\"data\"].toString();\n\tif (data.isEmpty()) {\n\t\tLOG((\"BotWebView Error: Bad 'data' in sendDataMessage.\"));\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\t_delegate->botSendData(data.toUtf8());\n}\n\nvoid Panel::switchInlineQueryMessage(const QJsonObject &args) {\n\tif (args.isEmpty()) {\n\t\t_delegate->botClose();\n\t\treturn;\n\t} else if (!args.contains(\"query\")) {\n\t\tLOG((\"BotWebView Error: No 'query' in switchInlineQueryMessage.\"));\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\tconst auto query = args[\"query\"].toString();\n\tconst auto valid = base::flat_set<QString>{\n\t\tu\"users\"_q,\n\t\tu\"bots\"_q,\n\t\tu\"groups\"_q,\n\t\tu\"channels\"_q,\n\t};\n\tconst auto typeArray = args[\"chat_types\"].toArray();\n\tauto types = std::vector<QString>();\n\tfor (const auto &value : typeArray) {\n\t\tconst auto type = value.toString();\n\t\tif (valid.contains(type)) {\n\t\t\ttypes.push_back(type);\n\t\t} else {\n\t\t\tLOG((\"BotWebView Error: \"\n\t\t\t\t\"Bad chat type in switchInlineQueryMessage: %1.\").arg(type));\n\t\t\ttypes.clear();\n\t\t\tbreak;\n\t\t}\n\t}\n\t_delegate->botSwitchInlineQuery(types, query);\n}\n\nvoid Panel::processSendMessageRequest(const QJsonObject &args) {\n\tif (args.isEmpty()) {\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\tconst auto id = args[\"id\"].toString();\n\tauto callback = crl::guard(this, [=](QString error) {\n\t\tif (error.isEmpty()) {\n\t\t\tpostEvent(\"prepared_message_sent\");\n\t\t} else {\n\t\t\tpostEvent(\n\t\t\t\t\"prepared_message_failed\",\n\t\t\t\tu\"{ error: \\\"%1\\\" }\"_q.arg(error));\n\t\t}\n\t});\n\t_delegate->botSendPreparedMessage({\n\t\t.id = id,\n\t\t.callback = std::move(callback),\n\t});\n}\n\nvoid Panel::processRequestChat(const QJsonObject &args) {\n\tif (args.isEmpty()) {\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\tconst auto requestId = args[\"req_id\"].toString();\n\tif (requestId.isEmpty()) {\n\t\treturn;\n\t}\n\tauto callback = crl::guard(this, [=](QString error) {\n\t\tif (error.isEmpty()) {\n\t\t\tpostEvent(\n\t\t\t\t\"requested_chat_sent\",\n\t\t\t\tu\"{ req_id: \\\"%1\\\" }\"_q.arg(requestId));\n\t\t} else {\n\t\t\tpostEvent(\n\t\t\t\t\"requested_chat_failed\",\n\t\t\t\tu\"{ req_id: \\\"%1\\\", error: \\\"%2\\\" }\"_q.arg(\n\t\t\t\t\trequestId,\n\t\t\t\t\terror));\n\t\t}\n\t});\n\t_delegate->botRequestChat({\n\t\t.requestId = requestId,\n\t\t.callback = std::move(callback),\n\t});\n}\n\nvoid Panel::processEmojiStatusRequest(const QJsonObject &args) {\n\tif (args.isEmpty()) {\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\tconst auto emojiId = args[\"custom_emoji_id\"].toString().toULongLong();\n\tconst auto duration = TimeId(base::SafeRound(\n\t\targs[\"duration\"].toDouble()));\n\tif (!emojiId) {\n\t\tpostEvent(\n\t\t\t\"emoji_status_failed\",\n\t\t\t\"{ error: \\\"SUGGESTED_EMOJI_INVALID\\\" }\");\n\t\treturn;\n\t} else if (duration < 0) {\n\t\tpostEvent(\n\t\t\t\"emoji_status_failed\",\n\t\t\t\"{ error: \\\"DURATION_INVALID\\\" }\");\n\t\treturn;\n\t}\n\tauto callback = crl::guard(this, [=](QString error) {\n\t\tif (error.isEmpty()) {\n\t\t\tpostEvent(\"emoji_status_set\");\n\t\t} else {\n\t\t\tpostEvent(\n\t\t\t\t\"emoji_status_failed\",\n\t\t\t\tu\"{ error: \\\"%1\\\" }\"_q.arg(error));\n\t\t}\n\t});\n\t_delegate->botSetEmojiStatus({\n\t\t.customEmojiId = emojiId,\n\t\t.duration = duration,\n\t\t.callback = std::move(callback),\n\t});\n}\n\nvoid Panel::processEmojiStatusAccessRequest() {\n\tauto callback = crl::guard(this, [=](bool allowed) {\n\t\tpostEvent(\"emoji_status_access_requested\", allowed\n\t\t\t? \"{ status: \\\"allowed\\\" }\"\n\t\t\t: \"{ status: \\\"cancelled\\\" }\");\n\t});\n\t_delegate->botRequestEmojiStatusAccess(std::move(callback));\n}\n\nvoid Panel::processStorageSaveKey(const QJsonObject &args) {\n\tconst auto keyObject = args[\"key\"];\n\tconst auto valueObject = args[\"value\"];\n\tconst auto key = keyObject.toString();\n\tif (!keyObject.isString()) {\n\t\tdeviceStorageFailed(args, u\"KEY_INVALID\"_q);\n\t} else if (valueObject.isNull()) {\n\t\t_delegate->botStorageWrite(key, std::nullopt);\n\t\treplyDeviceStorage(args, u\"device_storage_key_saved\"_q, {});\n\t} else if (!valueObject.isString()) {\n\t\tdeviceStorageFailed(args, u\"VALUE_INVALID\"_q);\n\t} else if (_delegate->botStorageWrite(key, valueObject.toString())) {\n\t\treplyDeviceStorage(args, u\"device_storage_key_saved\"_q, {});\n\t} else {\n\t\tdeviceStorageFailed(args, u\"QUOTA_EXCEEDED\"_q);\n\t}\n}\n\nvoid Panel::processStorageGetKey(const QJsonObject &args) {\n\tconst auto keyObject = args[\"key\"];\n\tconst auto key = keyObject.toString();\n\tif (!keyObject.isString()) {\n\t\tdeviceStorageFailed(args, u\"KEY_INVALID\"_q);\n\t} else {\n\t\tconst auto value = _delegate->botStorageRead(key);\n\t\treplyDeviceStorage(args, u\"device_storage_key_received\"_q, {\n\t\t\t{ u\"value\"_q, value ? QJsonValue(*value) : QJsonValue::Null },\n\t\t});\n\t}\n}\n\nvoid Panel::processStorageClear(const QJsonObject &args) {\n\t_delegate->botStorageClear();\n\treplyDeviceStorage(args, u\"device_storage_cleared\"_q, {});\n}\n\nvoid Panel::replyDeviceStorage(\n\t\tconst QJsonObject &args,\n\t\tconst QString &event,\n\t\tQJsonObject response) {\n\tresponse[u\"req_id\"_q] = args[u\"req_id\"_q];\n\tpostEvent(event, response);\n}\n\nvoid Panel::deviceStorageFailed(const QJsonObject &args, QString error) {\n\treplyDeviceStorage(args, u\"device_storage_failed\"_q, {\n\t\t{ u\"error\"_q, error },\n\t});\n}\n\nvoid Panel::secureStorageFailed(const QJsonObject &args) {\n\tpostEvent(u\"secure_storage_failed\"_q, QJsonObject{\n\t\t{ u\"req_id\"_q, args[\"req_id\"] },\n\t\t{ u\"error\"_q, u\"UNSUPPORTED\"_q },\n\t});\n}\n\nvoid Panel::openTgLink(const QJsonObject &args) {\n\tif (args.isEmpty()) {\n\t\tLOG((\"BotWebView Error: Bad arguments in 'web_app_open_tg_link'.\"));\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\tconst auto path = args[\"path_full\"].toString();\n\tif (path.isEmpty()) {\n\t\tLOG((\"BotWebView Error: Bad 'path_full' in 'web_app_open_tg_link'.\"));\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\t_delegate->botHandleLocalUri(\"https://t.me\" + path, true);\n}\n\nvoid Panel::openExternalLink(const QJsonObject &args) {\n\tif (args.isEmpty()) {\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\tconst auto iv = args[\"try_instant_view\"].toBool();\n\tconst auto url = args[\"url\"].toString();\n\tif (!_delegate->botValidateExternalLink(url)) {\n\t\tLOG((\"BotWebView Error: Bad url in openExternalLink: %1\").arg(url));\n\t\t// _delegate->botClose();\n\t\treturn;\n\t} else if (!allowOpenLink()) {\n\t\treturn;\n\t} else if (iv) {\n\t\t_delegate->botOpenIvLink(url);\n\t} else {\n\t\tFile::OpenUrl(url);\n\t}\n}\n\nvoid Panel::openInvoice(const QJsonObject &args) {\n\tif (args.isEmpty()) {\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\tconst auto slug = args[\"slug\"].toString();\n\tif (slug.isEmpty()) {\n\t\tLOG((\"BotWebView Error: Bad 'slug' in openInvoice.\"));\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\t_delegate->botHandleInvoice(slug);\n}\n\nvoid Panel::openPopup(const QJsonObject &args) {\n\tif (args.isEmpty()) {\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\tusing Button = Webview::PopupArgs::Button;\n\tusing Type = Button::Type;\n\tconst auto message = args[\"message\"].toString();\n\tconst auto types = base::flat_map<QString, Button::Type>{\n\t\t{ \"default\", Type::Default },\n\t\t{ \"ok\", Type::Ok },\n\t\t{ \"close\", Type::Close },\n\t\t{ \"cancel\", Type::Cancel },\n\t\t{ \"destructive\", Type::Destructive },\n\t};\n\tconst auto buttonArray = args[\"buttons\"].toArray();\n\tauto buttons = std::vector<Webview::PopupArgs::Button>();\n\tfor (const auto button : buttonArray) {\n\t\tconst auto fields = button.toObject();\n\t\tconst auto i = types.find(fields[\"type\"].toString());\n\t\tif (i == end(types)) {\n\t\t\tLOG((\"BotWebView Error: Bad 'type' in openPopup buttons.\"));\n\t\t\t_delegate->botClose();\n\t\t\treturn;\n\t\t}\n\t\tbuttons.push_back({\n\t\t\t.id = fields[\"id\"].toString(),\n\t\t\t.text = fields[\"text\"].toString(),\n\t\t\t.type = i->second,\n\t\t});\n\t}\n\tif (message.isEmpty()) {\n\t\tLOG((\"BotWebView Error: Bad 'message' in openPopup.\"));\n\t\t_delegate->botClose();\n\t\treturn;\n\t} else if (buttons.empty()) {\n\t\tLOG((\"BotWebView Error: Bad 'buttons' in openPopup.\"));\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\tconst auto widget = _webview->window.widget();\n\tconst auto weak = base::make_weak(this);\n\tconst auto result = Webview::ShowBlockingPopup({\n\t\t.parent = widget ? widget->window() : nullptr,\n\t\t.title = args[\"title\"].toString(),\n\t\t.text = message,\n\t\t.buttons = std::move(buttons),\n\t});\n\tif (weak) {\n\t\tpostEvent(\"popup_closed\", result.id\n\t\t\t? QJsonObject{ { u\"button_id\"_q, *result.id } }\n\t\t\t: EventData());\n\t}\n}\n\nvoid Panel::openScanQrPopup(const QJsonObject &args) {\n\tconst auto widget = _webview->window.widget();\n\t[[maybe_unused]] const auto ok = Webview::ShowBlockingPopup({\n\t\t.parent = widget ? widget->window() : nullptr,\n\t\t.text = tr::lng_bot_no_scan_qr(tr::now),\n\t\t.buttons = { {\n\t\t\t.id = \"ok\",\n\t\t\t.text = tr::lng_box_ok(tr::now),\n\t\t\t.type = Webview::PopupArgs::Button::Type::Ok,\n\t\t}},\n\t});\n}\n\nvoid Panel::openShareStory(const QJsonObject &args) {\n\tconst auto widget = _webview->window.widget();\n\t[[maybe_unused]] const auto ok = Webview::ShowBlockingPopup({\n\t\t.parent = widget ? widget->window() : nullptr,\n\t\t.text = tr::lng_bot_no_share_story(tr::now),\n\t\t.buttons = { {\n\t\t\t.id = \"ok\",\n\t\t\t.text = tr::lng_box_ok(tr::now),\n\t\t\t.type = Webview::PopupArgs::Button::Type::Ok,\n\t\t}},\n\t});\n}\n\nvoid Panel::requestWriteAccess() {\n\tif (_inBlockingRequest) {\n\t\treplyRequestWriteAccess(false);\n\t\treturn;\n\t}\n\t_inBlockingRequest = true;\n\tconst auto finish = [=](bool allowed) {\n\t\t_inBlockingRequest = false;\n\t\treplyRequestWriteAccess(allowed);\n\t};\n\tconst auto weak = base::make_weak(this);\n\t_delegate->botCheckWriteAccess([=](bool allowed) {\n\t\tif (!weak) {\n\t\t\treturn;\n\t\t} else if (allowed) {\n\t\t\tfinish(true);\n\t\t\treturn;\n\t\t}\n\t\tusing Button = Webview::PopupArgs::Button;\n\t\tconst auto widget = _webview->window.widget();\n\t\tconst auto integration = &Ui::Integration::Instance();\n\t\tconst auto result = Webview::ShowBlockingPopup({\n\t\t\t.parent = widget ? widget->window() : nullptr,\n\t\t\t.title = integration->phraseBotAllowWriteTitle(),\n\t\t\t.text = integration->phraseBotAllowWrite(),\n\t\t\t.buttons = {\n\t\t\t\t{\n\t\t\t\t\t.id = \"allow\",\n\t\t\t\t\t.text = integration->phraseBotAllowWriteConfirm(),\n\t\t\t\t},\n\t\t\t\t{ .id = \"cancel\", .type = Button::Type::Cancel },\n\t\t\t},\n\t\t});\n\t\tif (!weak) {\n\t\t\treturn;\n\t\t} else if (result.id == \"allow\") {\n\t\t\t_delegate->botAllowWriteAccess(crl::guard(this, finish));\n\t\t} else {\n\t\t\tfinish(false);\n\t\t}\n\t});\n}\n\nvoid Panel::replyRequestWriteAccess(bool allowed) {\n\tpostEvent(\"write_access_requested\", QJsonObject{\n\t\t{ u\"status\"_q, allowed ? u\"allowed\"_q : u\"cancelled\"_q }\n\t});\n}\n\nvoid Panel::requestPhone() {\n\tif (_inBlockingRequest) {\n\t\treplyRequestPhone(false);\n\t\treturn;\n\t}\n\t_inBlockingRequest = true;\n\tconst auto finish = [=](bool shared) {\n\t\t_inBlockingRequest = false;\n\t\treplyRequestPhone(shared);\n\t};\n\tusing Button = Webview::PopupArgs::Button;\n\tconst auto widget = _webview->window.widget();\n\tconst auto weak = base::make_weak(this);\n\tconst auto integration = &Ui::Integration::Instance();\n\tconst auto result = Webview::ShowBlockingPopup({\n\t\t.parent = widget ? widget->window() : nullptr,\n\t\t.title = integration->phraseBotSharePhoneTitle(),\n\t\t.text = integration->phraseBotSharePhone(),\n\t\t.buttons = {\n\t\t\t{\n\t\t\t\t.id = \"share\",\n\t\t\t\t.text = integration->phraseBotSharePhoneConfirm(),\n\t\t\t},\n\t\t\t{ .id = \"cancel\", .type = Button::Type::Cancel },\n\t\t},\n\t});\n\tif (!weak) {\n\t\treturn;\n\t} else if (result.id == \"share\") {\n\t\t_delegate->botSharePhone(crl::guard(this, finish));\n\t} else {\n\t\tfinish(false);\n\t}\n}\n\nvoid Panel::replyRequestPhone(bool shared) {\n\tpostEvent(\"phone_requested\", QJsonObject{\n\t\t{ u\"status\"_q, shared ? u\"sent\"_q : u\"cancelled\"_q }\n\t});\n}\n\nvoid Panel::invokeCustomMethod(const QJsonObject &args) {\n\tconst auto requestId = args[\"req_id\"];\n\tif (requestId.isUndefined()) {\n\t\treturn;\n\t}\n\tconst auto finish = [=](QJsonObject response) {\n\t\treplyCustomMethod(requestId, std::move(response));\n\t};\n\tauto callback = crl::guard(this, [=](CustomMethodResult result) {\n\t\tif (result) {\n\t\t\tauto error = QJsonParseError();\n\t\t\tconst auto parsed = QJsonDocument::fromJson(\n\t\t\t\t\"{ \\\"result\\\": \" + *result + '}',\n\t\t\t\t&error);\n\t\t\tif (error.error != QJsonParseError::NoError\n\t\t\t\t|| !parsed.isObject()\n\t\t\t\t|| parsed.object().size() != 1) {\n\t\t\t\tfinish({ { u\"error\"_q, u\"Could not parse response.\"_q } });\n\t\t\t} else {\n\t\t\t\tfinish(parsed.object());\n\t\t\t}\n\t\t} else {\n\t\t\tfinish({ { u\"error\"_q, result.error() } });\n\t\t}\n\t});\n\tconst auto params = QJsonDocument(\n\t\targs[\"params\"].toObject()\n\t).toJson(QJsonDocument::Compact);\n\t_delegate->botInvokeCustomMethod({\n\t\t.method = args[\"method\"].toString(),\n\t\t.params = params,\n\t\t.callback = std::move(callback),\n\t});\n}\n\nvoid Panel::replyCustomMethod(QJsonValue requestId, QJsonObject response) {\n\tresponse[\"req_id\"] = requestId;\n\tpostEvent(u\"custom_method_invoked\"_q, response);\n}\n\nvoid Panel::requestClipboardText(const QJsonObject &args) {\n\tconst auto requestId = args[\"req_id\"];\n\tif (requestId.isUndefined()) {\n\t\treturn;\n\t}\n\tauto result = QJsonObject();\n\tresult[\"req_id\"] = requestId;\n\tif (allowClipboardQuery()) {\n\t\tresult[\"data\"] = QGuiApplication::clipboard()->text();\n\t}\n\tpostEvent(u\"clipboard_text_received\"_q, result);\n}\n\nbool Panel::allowOpenLink() const {\n\t//const auto now = crl::now();\n\t//if (_mainButtonLastClick\n\t//\t&& _mainButtonLastClick + kProcessClickTimeout >= now) {\n\t//\t_mainButtonLastClick = 0;\n\t//\treturn true;\n\t//}\n\treturn true;\n}\n\nbool Panel::allowClipboardQuery() const {\n\tif (!_allowClipboardRead) {\n\t\treturn false;\n\t}\n\tconst auto now = crl::now();\n\treturn _lastWebviewInteraction\n\t\t&& (_lastWebviewInteraction + kClipboardReadTimeout >= now);\n}\n\nvoid Panel::scheduleCloseWithConfirmation() {\n\tif (!_closeWithConfirmationScheduled) {\n\t\t_closeWithConfirmationScheduled = true;\n\t\tInvokeQueued(_widget.get(), [=] { closeWithConfirmation(); });\n\t}\n}\n\nvoid Panel::closeWithConfirmation() {\n\tusing Button = Webview::PopupArgs::Button;\n\tconst auto widget = _webview->window.widget();\n\tconst auto weak = base::make_weak(this);\n\tconst auto integration = &Ui::Integration::Instance();\n\tconst auto result = Webview::ShowBlockingPopup({\n\t\t.parent = widget ? widget->window() : nullptr,\n\t\t.title = integration->phrasePanelCloseWarning(),\n\t\t.text = integration->phrasePanelCloseUnsaved(),\n\t\t.buttons = {\n\t\t\t{\n\t\t\t\t.id = \"close\",\n\t\t\t\t.text = integration->phrasePanelCloseAnyway(),\n\t\t\t\t.type = Button::Type::Destructive,\n\t\t\t},\n\t\t\t{ .id = \"cancel\", .type = Button::Type::Cancel },\n\t\t},\n\t\t.ignoreFloodCheck = true,\n\t});\n\tif (!weak) {\n\t\treturn;\n\t} else if (result.id == \"close\") {\n\t\t_delegate->botClose();\n\t} else {\n\t\t_closeWithConfirmationScheduled = false;\n\t}\n}\n\nvoid Panel::setupClosingBehaviour(const QJsonObject &args) {\n\t_closeNeedConfirmation = args[\"need_confirmation\"].toBool();\n}\n\nvoid Panel::processButtonMessage(\n\t\tstd::unique_ptr<Button> &button,\n\t\tconst QJsonObject &args) {\n\tif (args.isEmpty()) {\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\n\tconst auto shown = [&] {\n\t\treturn button && !button->isHidden();\n\t};\n\tconst auto wasShown = shown();\n\tconst auto guard = gsl::finally([&] {\n\t\tif (shown() != wasShown) {\n\t\t\tcrl::on_main(this, [=] {\n\t\t\t\tsendViewport();\n\t\t\t});\n\t\t}\n\t});\n\n\tconst auto text = args[\"text\"].toString().trimmed();\n\tconst auto iconCustomEmojiId\n\t\t= args[\"icon_custom_emoji_id\"].toString().toULongLong();\n\tconst auto visible = args[\"is_visible\"].toBool()\n\t\t&& (!text.isEmpty() || iconCustomEmojiId);\n\tif (!button) {\n\t\tif (visible) {\n\t\t\tcreateButton(button);\n\t\t\t_bottomButtonsBg->show();\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tif (const auto bg = ParseColor(args[\"color\"].toString())) {\n\t\tbutton->updateBg(*bg);\n\t} else {\n\t\tbutton->updateBg(&st::windowBgActive);\n\t}\n\n\tif (const auto fg = ParseColor(args[\"text_color\"].toString())) {\n\t\tbutton->updateFg(*fg);\n\t} else {\n\t\tbutton->updateFg(&st::windowFgActive);\n\t}\n\n\tbutton->updateArgs({\n\t\t.isActive = args[\"is_active\"].toBool(),\n\t\t.isVisible = visible,\n\t\t.isProgressVisible = args[\"is_progress_visible\"].toBool(),\n\t\t.iconCustomEmojiId = iconCustomEmojiId,\n\t\t.text = args[\"text\"].toString(),\n\t});\n\tif (button.get() == _secondaryButton.get()) {\n\t\tconst auto position = ParsePosition(args[\"position\"].toString());\n\t\tif (_secondaryPosition != position) {\n\t\t\t_secondaryPosition = position;\n\t\t\tlayoutButtons();\n\t\t}\n\t}\n}\n\nvoid Panel::processBackButtonMessage(const QJsonObject &args) {\n\t_widget->setBackAllowed(args[\"is_visible\"].toBool());\n}\n\nvoid Panel::processSettingsButtonMessage(const QJsonObject &args) {\n\t_hasSettingsButton = args[\"is_visible\"].toBool();\n}\n\nvoid Panel::processHeaderColor(const QJsonObject &args) {\n\t_headerColorReceived = true;\n\tif (const auto color = ParseColor(args[\"color\"].toString())) {\n\t\t_widget->overrideTitleColor(color);\n\t\t_headerColorLifetime.destroy();\n\t} else if (const auto color = LookupNamedColor(\n\t\t\targs[\"color_key\"].toString())) {\n\t\t_widget->overrideTitleColor((*color)->c);\n\t\t_headerColorLifetime = style::PaletteChanged(\n\t\t) | rpl::on_next([=] {\n\t\t\t_widget->overrideTitleColor((*color)->c);\n\t\t});\n\t} else {\n\t\t_widget->overrideTitleColor(std::nullopt);\n\t\t_headerColorLifetime.destroy();\n\t}\n}\n\nvoid Panel::overrideBodyColor(std::optional<QColor> color) {\n\t_widget->overrideBodyColor(color);\n\tconst auto raw = _webviewBottomLabel.data();\n\tif (!raw) {\n\t\treturn;\n\t} else if (!color) {\n\t\traw->setTextColorOverride(std::nullopt);\n\t\treturn;\n\t}\n\tconst auto contrast = 2.5;\n\tconst auto luminance = 0.2126 * color->redF()\n\t\t+ 0.7152 * color->greenF()\n\t\t+ 0.0722 * color->blueF();\n\tconst auto textColor = (luminance > 0.5)\n\t\t? QColor(0, 0, 0)\n\t\t: QColor(255, 255, 255);\n\tconst auto textLuminance = (luminance > 0.5) ? 0 : 1;\n\tconst auto adaptiveOpacity = (luminance - textLuminance + contrast)\n\t\t/ contrast;\n\tconst auto opacity = std::clamp(adaptiveOpacity, 0.5, 0.64);\n\tauto buttonColor = textColor;\n\tbuttonColor.setAlphaF(opacity);\n\traw->setTextColorOverride(buttonColor);\n}\n\nvoid Panel::processBackgroundColor(const QJsonObject &args) {\n\t_bodyColorReceived = true;\n\tif (const auto color = ParseColor(args[\"color\"].toString())) {\n\t\toverrideBodyColor(*color);\n\t\t_bodyColorLifetime.destroy();\n\t} else if (const auto color = LookupNamedColor(\n\t\t\targs[\"color_key\"].toString())) {\n\t\toverrideBodyColor((*color)->c);\n\t\t_bodyColorLifetime = style::PaletteChanged(\n\t\t) | rpl::on_next([=] {\n\t\t\toverrideBodyColor((*color)->c);\n\t\t});\n\t} else {\n\t\toverrideBodyColor(std::nullopt);\n\t\t_bodyColorLifetime.destroy();\n\t}\n\tif (const auto raw = _bottomButtonsBg.get()) {\n\t\traw->update();\n\t}\n\tif (const auto raw = _webviewBottom.get()) {\n\t\traw->update();\n\t}\n}\n\nvoid Panel::processBottomBarColor(const QJsonObject &args) {\n\t_bottomColorReceived = true;\n\tif (const auto color = ParseColor(args[\"color\"].toString())) {\n\t\t_widget->overrideBottomBarColor(color);\n\t\t_bottomBarColor = color;\n\t\t_bottomBarColorLifetime.destroy();\n\t} else if (const auto color = LookupNamedColor(\n\t\t\targs[\"color_key\"].toString())) {\n\t\t_widget->overrideBottomBarColor((*color)->c);\n\t\t_bottomBarColor = (*color)->c;\n\t\t_bottomBarColorLifetime = style::PaletteChanged(\n\t\t) | rpl::on_next([=] {\n\t\t\t_widget->overrideBottomBarColor((*color)->c);\n\t\t\t_bottomBarColor = (*color)->c;\n\t\t});\n\t} else {\n\t\t_widget->overrideBottomBarColor(std::nullopt);\n\t\t_bottomBarColor = std::nullopt;\n\t\t_bottomBarColorLifetime.destroy();\n\t}\n\tif (const auto raw = _bottomButtonsBg.get()) {\n\t\traw->update();\n\t}\n}\n\nvoid Panel::processDownloadRequest(const QJsonObject &args) {\n\tif (args.isEmpty()) {\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\tconst auto url = args[\"url\"].toString();\n\tconst auto name = args[\"file_name\"].toString();\n\tif (url.isEmpty()) {\n\t\tLOG((\"BotWebView Error: Bad 'url' in download request.\"));\n\t\t_delegate->botClose();\n\t\treturn;\n\t} else if (name.isEmpty()) {\n\t\tLOG((\"BotWebView Error: Bad 'file_name' in download request.\"));\n\t\t_delegate->botClose();\n\t\treturn;\n\t}\n\tconst auto done = crl::guard(this, [=](bool started) {\n\t\tpostEvent(\"file_download_requested\", started\n\t\t\t? \"{ status: \\\"downloading\\\" }\"\n\t\t\t: \"{ status: \\\"cancelled\\\" }\");\n\t});\n\t_delegate->botDownloadFile({\n\t\t.url = url,\n\t\t.name = name,\n\t\t.callback = done,\n\t});\n}\n\nvoid Panel::createButton(std::unique_ptr<Button> &button) {\n\tif (!_bottomButtonsBg) {\n\t\t_bottomButtonsBg = std::make_unique<RpWidget>(_widget.get());\n\n\t\tconst auto raw = _bottomButtonsBg.get();\n\t\traw->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(raw);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(_bottomBarColor.value_or(st::windowBg->c));\n\t\t\tp.drawRoundedRect(\n\t\t\t\traw->rect().marginsAdded({ 0, 2 * st::callRadius, 0, 0 }),\n\t\t\t\tst::callRadius,\n\t\t\t\tst::callRadius);\n\t\t}, raw->lifetime());\n\t}\n\tbutton = std::make_unique<Button>(\n\t\t_bottomButtonsBg.get(),\n\t\tst::botWebViewBottomButton,\n\t\t_delegate->botTextContext());\n\tconst auto raw = button.get();\n\n\traw->setClickedCallback([=] {\n\t\tif (!raw->isDisabled()) {\n\t\t\tif (raw == _mainButton.get()) {\n\t\t\t\tpostEvent(\"main_button_pressed\");\n\t\t\t} else if (raw == _secondaryButton.get()) {\n\t\t\t\tpostEvent(\"secondary_button_pressed\");\n\t\t\t}\n\t\t}\n\t});\n\traw->hide();\n\n\trpl::combine(\n\t\traw->shownValue(),\n\t\traw->heightValue()\n\t) | rpl::on_next([=] {\n\t\tlayoutButtons();\n\t}, raw->lifetime());\n}\n\nvoid Panel::layoutButtons() {\n\tif (!_webviewBottom) {\n\t\treturn;\n\t}\n\tconst auto inner = _widget->innerGeometry();\n\tconst auto shown = [](std::unique_ptr<Button> &button) {\n\t\treturn button && !button->isHidden();\n\t};\n\tconst auto any = shown(_mainButton) || shown(_secondaryButton);\n\t_webviewBottom->setVisible(!any\n\t\t&& !_fullscreen.current()\n\t\t&& !_layerShown);\n\tif (any) {\n\t\t_bottomButtonsBg->setVisible(!_layerShown);\n\n\t\tconst auto one = shown(_mainButton)\n\t\t\t? _mainButton.get()\n\t\t\t: _secondaryButton.get();\n\t\tconst auto both = shown(_mainButton) && shown(_secondaryButton);\n\t\tconst auto vertical = both\n\t\t\t&& ((_secondaryPosition == RectPart::Top)\n\t\t\t\t|| (_secondaryPosition == RectPart::Bottom));\n\t\tconst auto padding = st::botWebViewBottomPadding;\n\t\tconst auto height = padding.top()\n\t\t\t+ (vertical\n\t\t\t\t? (_mainButton->height()\n\t\t\t\t\t+ st::botWebViewBottomSkip.y()\n\t\t\t\t\t+ _secondaryButton->height())\n\t\t\t\t: one->height())\n\t\t\t+ padding.bottom();\n\t\t_bottomButtonsBg->setGeometry(\n\t\t\tinner.x(),\n\t\t\tinner.y() + inner.height() - height,\n\t\t\tinner.width(),\n\t\t\theight);\n\t\tauto left = padding.left();\n\t\tauto bottom = height - padding.bottom();\n\t\tauto available = inner.width() - padding.left() - padding.right();\n\t\tif (!both) {\n\t\t\tone->resizeToWidth(available);\n\t\t\tone->move(left, bottom - one->height());\n\t\t} else if (_secondaryPosition == RectPart::Top) {\n\t\t\t_mainButton->resizeToWidth(available);\n\t\t\tbottom -= _mainButton->height();\n\t\t\t_mainButton->move(left, bottom);\n\t\t\tbottom -= st::botWebViewBottomSkip.y();\n\t\t\t_secondaryButton->resizeToWidth(available);\n\t\t\tbottom -= _secondaryButton->height();\n\t\t\t_secondaryButton->move(left, bottom);\n\t\t} else if (_secondaryPosition == RectPart::Bottom) {\n\t\t\t_secondaryButton->resizeToWidth(available);\n\t\t\tbottom -= _secondaryButton->height();\n\t\t\t_secondaryButton->move(left, bottom);\n\t\t\tbottom -= st::botWebViewBottomSkip.y();\n\t\t\t_mainButton->resizeToWidth(available);\n\t\t\tbottom -= _mainButton->height();\n\t\t\t_mainButton->move(left, bottom);\n\t\t} else if (_secondaryPosition == RectPart::Left) {\n\t\t\tavailable = (available - st::botWebViewBottomSkip.x()) / 2;\n\t\t\t_secondaryButton->resizeToWidth(available);\n\t\t\tbottom -= _secondaryButton->height();\n\t\t\t_secondaryButton->move(left, bottom);\n\t\t\t_mainButton->resizeToWidth(available);\n\t\t\t_mainButton->move(\n\t\t\t\tinner.width() - padding.right() - available,\n\t\t\t\tbottom);\n\t\t} else {\n\t\t\tavailable = (available - st::botWebViewBottomSkip.x()) / 2;\n\t\t\t_mainButton->resizeToWidth(available);\n\t\t\tbottom -= _mainButton->height();\n\t\t\t_mainButton->move(left, bottom);\n\t\t\t_secondaryButton->resizeToWidth(available);\n\t\t\t_secondaryButton->move(\n\t\t\t\tinner.width() - padding.right() - available,\n\t\t\t\tbottom);\n\t\t}\n\t} else if (_bottomButtonsBg) {\n\t\t_bottomButtonsBg->hide();\n\t}\n\tconst auto footer = _layerShown\n\t\t? 0\n\t\t: any\n\t\t? _bottomButtonsBg->height()\n\t\t: _fullscreen.current()\n\t\t? 0\n\t\t: _webviewBottom->height();\n\t_widget->setBottomBarHeight((!_layerShown && any) ? footer : 0);\n\t_footerHeight = footer;\n}\n\nvoid Panel::showBox(object_ptr<BoxContent> box) {\n\tshowBox(std::move(box), LayerOption::KeepOther, anim::type::normal);\n}\n\nvoid Panel::showBox(\n\t\tobject_ptr<BoxContent> box,\n\t\tLayerOptions options,\n\t\tanim::type animated) {\n\tif (const auto widget = _webview ? _webview->window.widget() : nullptr) {\n\t\t_layerShown = true;\n\t\tconst auto hideNow = !widget->isHidden();\n\t\tconst auto raw = box.data();\n\t\t_webview->boxes.push_back(raw);\n\t\traw->boxClosing(\n\t\t) | rpl::filter([=] {\n\t\t\treturn _webview != nullptr;\n\t\t}) | rpl::on_next([=] {\n\t\t\tauto &list = _webview->boxes;\n\t\t\tlist.erase(ranges::remove_if(list, [&](QPointer<RpWidget> b) {\n\t\t\t\treturn !b || (b == raw);\n\t\t\t}), end(list));\n\t\t\tif (list.empty()) {\n\t\t\t\t_webview->boxesLifetime.destroy();\n\t\t\t\t_layerShown = false;\n\t\t\t\tconst auto widget = _webview\n\t\t\t\t\t? _webview->window.widget()\n\t\t\t\t\t: nullptr;\n\t\t\t\tif (widget && widget->isHidden()) {\n\t\t\t\t\twidget->show();\n\t\t\t\t\tlayoutButtons();\n\t\t\t\t}\n\t\t\t}\n\t\t}, _webview->boxesLifetime);\n\n\t\tif (hideNow) {\n\t\t\twidget->hide();\n\t\t\tlayoutButtons();\n\t\t}\n\t}\n\tconst auto raw = box.data();\n\n\tInvokeQueued(raw, [=] {\n\t\tif (raw->window()->isActiveWindow()) {\n\t\t\t// In case focus is somewhat in a native child window,\n\t\t\t// like a webview, Qt glitches here with input fields showing\n\t\t\t// focused state, but not receiving any keyboard input:\n\t\t\t//\n\t\t\t// window()->windowHandle()->isActive() == false.\n\t\t\t//\n\t\t\t// Steps were: SeparatePanel with a WebView2 child,\n\t\t\t// some interaction with mouse inside the WebView2,\n\t\t\t// so that WebView2 gets focus and active window state,\n\t\t\t// then we call setSearchAllowed() and after animation\n\t\t\t// is finished try typing -> nothing happens.\n\t\t\t//\n\t\t\t// With this workaround it works fine.\n\t\t\t_widget->activateWindow();\n\t\t}\n\t});\n\n\t_widget->showBox(\n\t\tstd::move(box),\n\t\tLayerOption::KeepOther,\n\t\tanim::type::normal);\n}\n\nvoid Panel::showToast(TextWithEntities &&text) {\n\t_widget->showToast(std::move(text));\n}\n\nnot_null<QWidget*> Panel::toastParent() const {\n\treturn _widget->uiShow()->toastParent();\n}\n\nvoid Panel::hideLayer(anim::type animated) {\n\t_widget->hideLayer(animated);\n}\n\nvoid Panel::showCriticalError(const TextWithEntities &text) {\n\t_progress = nullptr;\n\t_webviewProgress = false;\n\tauto wrap = base::make_unique_q<RpWidget>(_widget.get());\n\tconst auto raw = wrap.get();\n\n\tconst auto error = CreateChild<PaddingWrap<FlatLabel>>(\n\t\traw,\n\t\tobject_ptr<FlatLabel>(\n\t\t\traw,\n\t\t\trpl::single(text),\n\t\t\tst::paymentsCriticalError),\n\t\tst::paymentsCriticalErrorPadding);\n\terror->entity()->setClickHandlerFilter([=](\n\t\t\tconst ClickHandlerPtr &handler,\n\t\t\tQt::MouseButton) {\n\t\tconst auto entity = handler->getTextEntity();\n\t\tif (entity.type != EntityType::CustomUrl) {\n\t\t\treturn true;\n\t\t}\n\t\tFile::OpenUrl(entity.data);\n\t\treturn false;\n\t});\n\n\traw->widthValue() | rpl::on_next([=](int width) {\n\t\terror->resizeToWidth(width);\n\t\traw->resize(width, error->height());\n\t}, raw->lifetime());\n\n\t_widget->showInner(std::move(wrap));\n}\n\nvoid Panel::updateThemeParams(const Webview::ThemeParams &params) {\n\tupdateColorOverrides(params);\n\tif (!_webview || !_webview->window.widget()) {\n\t\treturn;\n\t}\n\t_webview->window.updateTheme(\n\t\tparams.bodyBg,\n\t\tparams.scrollBg,\n\t\tparams.scrollBgOver,\n\t\tparams.scrollBarBg,\n\t\tparams.scrollBarBgOver);\n\tpostEvent(\"theme_changed\", \"{\\\"theme_params\\\": \" + params.json + \"}\");\n}\n\nvoid Panel::updateColorOverrides(const Webview::ThemeParams &params) {\n\tif (!_headerColorReceived && params.titleBg.alpha() == 255) {\n\t\t_widget->overrideTitleColor(params.titleBg);\n\t}\n\tif (!_bodyColorReceived && params.bodyBg.alpha() == 255) {\n\t\toverrideBodyColor(params.bodyBg);\n\t}\n}\n\nvoid Panel::invoiceClosed(const QString &slug, const QString &status) {\n\tif (!_webview || !_webview->window.widget()) {\n\t\treturn;\n\t}\n\tpostEvent(\"invoice_closed\", QJsonObject{\n\t\t{ u\"slug\"_q, slug },\n\t\t{ u\"status\"_q, status },\n\t});\n\tif (_hiddenForPayment) {\n\t\t_hiddenForPayment = false;\n\t\t_widget->showAndActivate();\n\t}\n}\n\nvoid Panel::hideForPayment() {\n\t_hiddenForPayment = true;\n\t_widget->hideGetDuration();\n}\n\nvoid Panel::postEvent(const QString &event) {\n\tpostEvent(event, {});\n}\n\nvoid Panel::postEvent(const QString &event, EventData data) {\n\tif (!_webview) {\n\t\tLOG((\"BotWebView Error: Post event \\\"%1\\\" on crashed webview.\"\n\t\t\t).arg(event));\n\t\treturn;\n\t}\n\tauto written = v::is<QString>(data)\n\t\t? v::get<QString>(data).toUtf8()\n\t\t: QJsonDocument(\n\t\t\tv::get<QJsonObject>(data)).toJson(QJsonDocument::Compact);\n\t_webview->window.eval(R\"(\nif (window.TelegramGameProxy) {\n\twindow.TelegramGameProxy.receiveEvent(\n\t\t\")\"\n\t\t+ event.toUtf8()\n\t\t+ '\"' + (written.isEmpty() ? QByteArray() : \", \" + written)\n\t\t+ R\"();\n}\n)\");\n}\n\nTextWithEntities ErrorText(const Webview::Available &info) {\n\tExpects(info.error != Webview::Available::Error::None);\n\n\tusing Error = Webview::Available::Error;\n\tswitch (info.error) {\n\tcase Error::NoWebview2:\n\t\treturn tr::lng_payments_webview_install_edge(\n\t\t\ttr::now,\n\t\t\tlt_link,\n\t\t\tText::Link(\n\t\t\t\t\"Microsoft Edge WebView2 Runtime\",\n\t\t\t\t\"https://go.microsoft.com/fwlink/p/?LinkId=2124703\"),\n\t\t\ttr::marked);\n\tcase Error::NoWebKitGTK:\n\t\treturn { tr::lng_payments_webview_install_webkit(tr::now) };\n\tcase Error::OldWindows:\n\t\treturn { tr::lng_payments_webview_update_windows(tr::now) };\n\tdefault:\n\t\treturn { QString::fromStdString(info.details) };\n\t}\n}\n\nvoid Panel::showWebviewError(\n\t\tconst QString &text,\n\t\tconst Webview::Available &information) {\n\tshowCriticalError(TextWithEntities{ text }.append(\n\t\t\"\\n\\n\"\n\t).append(ErrorText(information)));\n}\n\nrpl::lifetime &Panel::lifetime() {\n\treturn _widget->lifetime();\n}\n\nstd::unique_ptr<Panel> Show(Args &&args) {\n\treturn std::make_unique<Panel>(std::move(args));\n}\n\n} // namespace Ui::BotWebView\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/expected.h\"\n#include \"base/object_ptr.h\"\n#include \"base/weak_ptr.h\"\n#include \"base/flags.h\"\n#include \"ui/rect_part.h\"\n#include \"ui/round_rect.h\"\n#include \"webview/webview_common.h\"\n#include <crl/crl_time.h>\n\nclass QJsonObject;\nclass QJsonValue;\n\nnamespace Ui {\nclass FlatLabel;\nclass BoxContent;\nclass RpWidget;\nclass SeparatePanel;\nclass IconButton;\nenum class LayerOption;\nusing LayerOptions = base::flags<LayerOption>;\n} // namespace Ui\n\nnamespace Webview {\nstruct Available;\n} // namespace Webview\n\nnamespace Ui::Text {\nstruct MarkedContext;\n} // namespace Ui::Text\n\nnamespace Ui::BotWebView {\n\nstruct DownloadsProgress;\nstruct DownloadsEntry;\nenum class DownloadsAction;\n\n[[nodiscard]] TextWithEntities ErrorText(const Webview::Available &info);\n\nenum class MenuButton {\n\tNone               = 0x00,\n\tOpenBot            = 0x01,\n\tRemoveFromMenu     = 0x02,\n\tRemoveFromMainMenu = 0x04,\n\tShareGame          = 0x08,\n};\ninline constexpr bool is_flag_type(MenuButton) { return true; }\nusing MenuButtons = base::flags<MenuButton>;\n\nusing CustomMethodResult = base::expected<QByteArray, QString>;\nstruct CustomMethodRequest {\n\tQString method;\n\tQByteArray params;\n\tFn<void(CustomMethodResult)> callback;\n};\n\nstruct SetEmojiStatusRequest {\n\tuint64 customEmojiId = 0;\n\tTimeId duration = 0;\n\tFn<void(QString)> callback;\n};\n\nstruct DownloadFileRequest {\n\tQString url;\n\tQString name;\n\tFn<void(bool)> callback;\n};\n\nstruct SendPreparedMessageRequest {\n\tQString id = 0;\n\tFn<void(QString)> callback;\n};\n\nstruct RequestChatRequest {\n\tQString requestId;\n\tFn<void(QString)> callback;\n};\n\nclass Delegate {\npublic:\n\t[[nodiscard]] virtual Webview::ThemeParams botThemeParams() = 0;\n\t[[nodiscard]] virtual Ui::Text::MarkedContext botTextContext() = 0;\n\t[[nodiscard]] virtual auto botDownloads(bool forceCheck = false)\n\t\t-> const std::vector<DownloadsEntry> & = 0;\n\tvirtual void botDownloadsAction(uint32 id, DownloadsAction type) = 0;\n\tvirtual bool botHandleLocalUri(QString uri, bool keepOpen) = 0;\n\tvirtual void botHandleInvoice(QString slug) = 0;\n\tvirtual void botHandleMenuButton(MenuButton button) = 0;\n\tvirtual bool botValidateExternalLink(QString uri) = 0;\n\tvirtual void botOpenIvLink(QString uri) = 0;\n\tvirtual void botSendData(QByteArray data) = 0;\n\tvirtual void botSwitchInlineQuery(\n\t\tstd::vector<QString> chatTypes,\n\t\tQString query) = 0;\n\tvirtual void botCheckWriteAccess(Fn<void(bool allowed)> callback) = 0;\n\tvirtual void botAllowWriteAccess(Fn<void(bool allowed)> callback) = 0;\n\tvirtual bool botStorageWrite(\n\t\tQString key,\n\t\tstd::optional<QString> value) = 0;\n\t[[nodiscard]] virtual std::optional<QString> botStorageRead(\n\t\tQString key) = 0;\n\tvirtual void botStorageClear() = 0;\n\tvirtual void botRequestEmojiStatusAccess(\n\t\tFn<void(bool allowed)> callback) = 0;\n\tvirtual void botSharePhone(Fn<void(bool shared)> callback) = 0;\n\tvirtual void botInvokeCustomMethod(CustomMethodRequest request) = 0;\n\tvirtual void botSetEmojiStatus(SetEmojiStatusRequest request) = 0;\n\tvirtual void botDownloadFile(DownloadFileRequest request) = 0;\n\tvirtual void botSendPreparedMessage(\n\t\tSendPreparedMessageRequest request) = 0;\n\tvirtual void botRequestChat(RequestChatRequest request) = 0;\n\tvirtual void botVerifyAge(int age) = 0;\n\tvirtual void botOpenPrivacyPolicy() = 0;\n\tvirtual void botClose() = 0;\n};\n\nstruct Args {\n\tQString url;\n\tWebview::StorageId storageId;\n\trpl::producer<QString> title;\n\tobject_ptr<Ui::RpWidget> titleBadge = { nullptr };\n\trpl::producer<QString> bottom;\n\tnot_null<Delegate*> delegate;\n\tMenuButtons menuButtons;\n\tbool fullscreen = false;\n\tbool allowClipboardRead = false;\n\tbool forkAdditionalButtons = false;\n\trpl::producer<DownloadsProgress> downloadsProgress;\n};\n\nclass Panel final : public base::has_weak_ptr {\npublic:\n\texplicit Panel(Args &&args);\n\t~Panel();\n\n\tvoid requestActivate();\n\tvoid toggleProgress(bool shown);\n\n\tvoid showBox(object_ptr<BoxContent> box);\n\tvoid showBox(\n\t\tobject_ptr<BoxContent> box,\n\t\tLayerOptions options,\n\t\tanim::type animated);\n\tvoid hideLayer(anim::type animated);\n\tvoid showToast(TextWithEntities &&text);\n\tnot_null<QWidget*> toastParent() const;\n\tvoid showCriticalError(const TextWithEntities &text);\n\tvoid showWebviewError(\n\t\tconst QString &text,\n\t\tconst Webview::Available &information);\n\n\tvoid updateThemeParams(const Webview::ThemeParams &params);\n\n\tvoid hideForPayment();\n\tvoid invoiceClosed(const QString &slug, const QString &status);\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tclass Button;\n\tstruct Progress;\n\tstruct WebviewWithLifetime;\n\n\tbool showWebview(Args &&args, const Webview::ThemeParams &params);\n\n\tbool createWebview(const Webview::ThemeParams &params);\n\tvoid createWebviewBottom();\n\tvoid showWebviewProgress();\n\tvoid hideWebviewProgress();\n\tvoid setupDownloadsProgress(\n\t\tnot_null<RpWidget*> button,\n\t\trpl::producer<DownloadsProgress> progress,\n\t\tbool fullscreen);\n\tvoid setTitle(rpl::producer<QString> title);\n\tvoid sendDataMessage(const QJsonObject &args);\n\tvoid switchInlineQueryMessage(const QJsonObject &args);\n\tvoid processSendMessageRequest(const QJsonObject &args);\n\tvoid processRequestChat(const QJsonObject &args);\n\tvoid processEmojiStatusRequest(const QJsonObject &args);\n\tvoid processEmojiStatusAccessRequest();\n\tvoid processStorageSaveKey(const QJsonObject &args);\n\tvoid processStorageGetKey(const QJsonObject &args);\n\tvoid processStorageClear(const QJsonObject &args);\n\tvoid processButtonMessage(\n\t\tstd::unique_ptr<Button> &button,\n\t\tconst QJsonObject &args);\n\tvoid processBackButtonMessage(const QJsonObject &args);\n\tvoid processSettingsButtonMessage(const QJsonObject &args);\n\tvoid processHeaderColor(const QJsonObject &args);\n\tvoid processBackgroundColor(const QJsonObject &args);\n\tvoid processBottomBarColor(const QJsonObject &args);\n\tvoid processDownloadRequest(const QJsonObject &args);\n\tvoid openTgLink(const QJsonObject &args);\n\tvoid openExternalLink(const QJsonObject &args);\n\tvoid openInvoice(const QJsonObject &args);\n\tvoid openPopup(const QJsonObject &args);\n\tvoid openScanQrPopup(const QJsonObject &args);\n\tvoid openShareStory(const QJsonObject &args);\n\tvoid requestWriteAccess();\n\tvoid replyRequestWriteAccess(bool allowed);\n\tvoid requestPhone();\n\tvoid replyRequestPhone(bool shared);\n\tvoid invokeCustomMethod(const QJsonObject &args);\n\tvoid replyCustomMethod(QJsonValue requestId, QJsonObject response);\n\tvoid requestClipboardText(const QJsonObject &args);\n\tvoid setupClosingBehaviour(const QJsonObject &args);\n\tvoid replyDeviceStorage(\n\t\tconst QJsonObject &args,\n\t\tconst QString &event,\n\t\tQJsonObject response);\n\tvoid deviceStorageFailed(const QJsonObject &args, QString error);\n\tvoid secureStorageFailed(const QJsonObject &args);\n\tvoid createButton(std::unique_ptr<Button> &button);\n\tvoid scheduleCloseWithConfirmation();\n\tvoid closeWithConfirmation();\n\tvoid sendViewport();\n\tvoid sendSafeArea();\n\tvoid sendContentSafeArea();\n\tvoid sendFullScreen();\n\n\tvoid updateColorOverrides(const Webview::ThemeParams &params);\n\tvoid overrideBodyColor(std::optional<QColor> color);\n\n\tusing EventData = std::variant<QString, QJsonObject>;\n\tvoid postEvent(const QString &event);\n\tvoid postEvent(const QString &event, EventData data);\n\n\t[[nodiscard]] bool allowOpenLink() const;\n\t[[nodiscard]] bool allowClipboardQuery() const;\n\t[[nodiscard]] bool progressWithBackground() const;\n\t[[nodiscard]] QRect progressRect() const;\n\tvoid setupProgressGeometry();\n\tvoid layoutButtons();\n\n\tWebview::StorageId _storageId;\n\tconst not_null<Delegate*> _delegate;\n\tbool _closeNeedConfirmation = false;\n\tbool _hasSettingsButton = false;\n\tMenuButtons _menuButtons = {};\n\tstd::unique_ptr<SeparatePanel> _widget;\n\tstd::unique_ptr<WebviewWithLifetime> _webview;\n\tstd::unique_ptr<RpWidget> _webviewBottom;\n\tQPointer<FlatLabel> _webviewBottomLabel;\n\trpl::variable<QString> _bottomText;\n\tQPointer<RpWidget> _webviewParent;\n\tstd::unique_ptr<RpWidget> _bottomButtonsBg;\n\tstd::unique_ptr<Button> _mainButton;\n\tstd::unique_ptr<Button> _secondaryButton;\n\tRectPart _secondaryPosition = RectPart::Left;\n\trpl::variable<int> _footerHeight = 0;\n\tstd::unique_ptr<Progress> _progress;\n\trpl::event_stream<> _themeUpdateForced;\n\tstd::optional<QColor> _bottomBarColor;\n\trpl::lifetime _headerColorLifetime;\n\trpl::lifetime _bodyColorLifetime;\n\trpl::lifetime _bottomBarColorLifetime;\n\trpl::event_stream<> _downloadsUpdated;\n\trpl::variable<bool> _fullscreen = false;\n\tcrl::time _lastWebviewInteraction = 0;\n\tbool _layerShown : 1 = false;\n\tbool _webviewProgress : 1 = false;\n\tbool _themeUpdateScheduled : 1 = false;\n\tbool _hiddenForPayment : 1 = false;\n\tbool _closeWithConfirmationScheduled : 1 = false;\n\tbool _allowClipboardRead : 1 = false;\n\tbool _inBlockingRequest : 1 = false;\n\tbool _headerColorReceived : 1 = false;\n\tbool _bodyColorReceived : 1 = false;\n\tbool _bottomColorReceived : 1 = false;\n\n};\n\n[[nodiscard]] std::unique_ptr<Panel> Show(Args &&args);\n\n} // namespace Ui::BotWebView\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_controls.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/attach/attach_controls.h\"\n\n#include \"ui/painter.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Ui {\n\nvoid AttachControls::paint(QPainter &p, int x, int y) {\n\tconst auto groupWidth = width();\n\tconst auto groupHeight = height();\n\tconst auto full = (_type == Type::Full);\n\n\tconst auto groupRect = QRect(x, y, groupWidth, groupHeight);\n\t{\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::roundedBg);\n\t\tif (_type == Type::EditOnly) {\n\t\t\tconst auto desired = st::sendBoxAlbumSmallGroupCircleSize;\n\t\t\tconst auto available = std::min(groupWidth, groupHeight);\n\t\t\tconst auto side = std::min(desired, available);\n\t\t\tconst auto circleRect = QRect(\n\t\t\t\tx + ((groupWidth - side) / 2),\n\t\t\t\ty + ((groupHeight - side) / 2),\n\t\t\t\tside,\n\t\t\t\tside);\n\t\t\tp.drawEllipse(circleRect);\n\t\t} else {\n\t\t\tconst auto radius = std::min(groupWidth, groupHeight) / 2.;\n\t\t\tp.drawRoundedRect(groupRect, radius, radius);\n\t\t}\n\t}\n\n\tif (full) {\n\t\tconst auto groupHalfWidth = groupWidth / 2;\n\t\tconst auto groupHalfHeight = groupHeight / 2;\n\t\tconst auto editRect = _vertical\n\t\t\t? QRect(x, y, groupWidth, groupHalfHeight)\n\t\t\t: QRect(x, y, groupHalfWidth, groupHeight);\n\t\tst::sendBoxAlbumGroupButtonMediaMore.paintInCenter(p, editRect);\n\t\tconst auto deleteRect = _vertical\n\t\t\t? QRect(x, y + groupHalfHeight, groupWidth, groupHalfHeight)\n\t\t\t: QRect(x + groupHalfWidth, y, groupHalfWidth, groupHeight);\n\t\tst::sendBoxAlbumGroupButtonMediaDelete.paintInCenter(p, deleteRect);\n\t} else if (_type == Type::EditOnly) {\n\t\tst::sendBoxAlbumButtonMediaMore.paintInCenter(p, groupRect);\n\t}\n}\n\nint AttachControls::width() const {\n\treturn (_type == Type::Full)\n\t\t? (_vertical\n\t\t\t? st::sendBoxAlbumGroupSizeVertical.width()\n\t\t\t: st::sendBoxAlbumGroupSize.width())\n\t\t: (_type == Type::EditOnly)\n\t\t? ((st::sendBoxAlbumSmallGroupSize.width()\n\t\t\t> st::sendBoxAlbumSmallGroupCircleSize)\n\t\t\t? st::sendBoxAlbumSmallGroupSize.width()\n\t\t\t: st::sendBoxAlbumSmallGroupCircleSize)\n\t\t: 0;\n}\n\nint AttachControls::height() const {\n\treturn (_type == Type::Full)\n\t\t? (_vertical\n\t\t\t? st::sendBoxAlbumGroupSizeVertical.height()\n\t\t\t: st::sendBoxAlbumGroupSize.height())\n\t\t: (_type == Type::EditOnly)\n\t\t? ((st::sendBoxAlbumSmallGroupSize.height()\n\t\t\t> st::sendBoxAlbumSmallGroupCircleSize)\n\t\t\t? st::sendBoxAlbumSmallGroupSize.height()\n\t\t\t: st::sendBoxAlbumSmallGroupCircleSize)\n\t\t: 0;\n}\n\nAttachControls::Type AttachControls::type() const {\n\treturn _type;\n}\n\nbool AttachControls::vertical() const {\n\treturn _vertical;\n}\n\nvoid AttachControls::setType(Type type) {\n\tif (_type != type) {\n\t\t_type = type;\n\t}\n}\n\nvoid AttachControls::setVertical(bool vertical) {\n\t_vertical = vertical;\n}\n\nAttachControlsWidget::AttachControlsWidget(\n\tnot_null<RpWidget*> parent,\n\tAttachControls::Type type)\n: RpWidget(parent)\n, _edit(base::make_unique_q<AbstractButton>(this))\n, _delete(base::make_unique_q<AbstractButton>(this)) {\n\t_controls.setType(type);\n\n\tconst auto w = _controls.width();\n\tconst auto h = _controls.height();\n\tresize(w, h);\n\n\tif (type == AttachControls::Type::Full) {\n\t\tconst auto leftWidth = w / 2;\n\t\tconst auto rightWidth = w - leftWidth;\n\t\t_edit->setGeometryToLeft(0, 0, leftWidth, h, w);\n\t\t_delete->setGeometryToLeft(leftWidth, 0, rightWidth, h, w);\n\t} else if (type == AttachControls::Type::EditOnly) {\n\t\t_edit->setGeometryToLeft(0, 0, w, h, w);\n\t}\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(this);\n\t\t_controls.paint(p, 0, 0);\n\t}, lifetime());\n}\n\nrpl::producer<> AttachControlsWidget::editRequests() const {\n\treturn _edit->clicks() | rpl::to_empty;\n}\n\nrpl::producer<> AttachControlsWidget::deleteRequests() const {\n\treturn _delete->clicks() | rpl::to_empty;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_controls.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/abstract_button.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\n\nclass AttachControls final {\npublic:\n\tenum class Type {\n\t\tFull,\n\t\tEditOnly,\n\t\tNone,\n\t};\n\n\tvoid paint(QPainter &p, int x, int y);\n\tvoid setType(Type type);\n\tvoid setVertical(bool vertical);\n\n\t[[nodiscard]] int width() const;\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] Type type() const;\n\t[[nodiscard]] bool vertical() const;\n\nprivate:\n\tType _type = Type::Full;\n\tbool _vertical = false;\n\n};\n\nclass AttachControlsWidget final : public RpWidget {\npublic:\n\tAttachControlsWidget(\n\t\tnot_null<RpWidget*> parent,\n\t\tAttachControls::Type type = AttachControls::Type::Full);\n\n\t[[nodiscard]] rpl::producer<> editRequests() const;\n\t[[nodiscard]] rpl::producer<> deleteRequests() const;\n\nprivate:\n\tconst base::unique_qptr<AbstractButton> _edit;\n\tconst base::unique_qptr<AbstractButton> _delete;\n\tAttachControls _controls;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_extensions.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/attach/attach_extensions.h\"\n\n#include <QtCore/QMimeDatabase>\n#include <QtGui/QImageReader>\n\nnamespace Ui {\n\nconst QStringList &ImageExtensions() {\n\tstatic const auto result = [] {\n\t\tconst auto formats = QImageReader::supportedImageFormats();\n\t\treturn formats | ranges::views::transform([](const auto &format) {\n\t\t\treturn '.' + format.toLower();\n\t\t}) | ranges::views::filter([](const auto &format) {\n\t\t\tconst auto mimes = QMimeDatabase().mimeTypesForFileName(\n\t\t\t\tu\"test\"_q + format);\n\t\t\treturn !mimes.isEmpty()\n\t\t\t\t&& mimes.front().name().startsWith(u\"image/\"_q);\n\t\t}) | ranges::to<QStringList>;\n\t}();\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_extensions.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\n[[nodiscard]] const QStringList &ImageExtensions();\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_item_single_file_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/attach/attach_item_single_file_preview.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"history/history_item.h\"\n#include \"history/view/media/history_view_document.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/text/format_song_name.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_chat.h\"\n\nnamespace Ui {\nnamespace {\n\nAttachControls::Type CheckControlsType(\n\t\tnot_null<HistoryItem*> item,\n\t\tAttachControls::Type type) {\n\tconst auto media = item->media();\n\tAssert(media != nullptr);\n\treturn media->allowsEditMedia()\n\t\t? type\n\t\t: AttachControls::Type::None;\n}\n\n} // namespace\n\nItemSingleFilePreview::ItemSingleFilePreview(\n\tQWidget *parent,\n\tconst style::ComposeControls &st,\n\tnot_null<HistoryItem*> item,\n\tAttachControls::Type type)\n: AbstractSingleFilePreview(parent, st, CheckControlsType(item, type), {}) {\n\tconst auto media = item->media();\n\tAssert(media != nullptr);\n\tconst auto document = media->document();\n\tAssert(document != nullptr);\n\n\t_documentMedia = document->createMediaView();\n\t_documentMedia->thumbnailWanted(item->fullId());\n\n\trpl::single(rpl::empty) | rpl::then(\n\t\tdocument->session().downloaderTaskFinished()\n\t) | rpl::on_next([=] {\n\t\tif (_documentMedia->thumbnail()) {\n\t\t\t_lifetimeDownload.destroy();\n\t\t}\n\t\tpreparePreview(document);\n\t}, _lifetimeDownload);\n}\n\nvoid ItemSingleFilePreview::preparePreview(not_null<DocumentData*> document) {\n\tAbstractSingleFilePreview::Data data;\n\n\tconst auto preview = _documentMedia->thumbnail()\n\t\t? _documentMedia->thumbnail()->original()\n\t\t: QImage();\n\n\tprepareThumbFor(data, preview);\n\tdata.fileIsImage = document->isImage();\n\tdata.fileIsAudio = document->isAudioFile() || document->isVoiceMessage();\n\n\tif (data.fileIsImage) {\n\t\tdata.name = document->filename();\n\t\t// data.statusText = FormatImageSizeText(preview.size()\n\t\t// \t/ preview.devicePixelRatio());\n\t} else if (data.fileIsAudio) {\n\t\tauto filename = document->filename();\n\n\t\tauto songTitle = QString();\n\t\tauto songPerformer = QString();\n\t\tif (const auto song = document->song()) {\n\t\t\tsongTitle = song->title;\n\t\t\tsongPerformer = song->performer;\n\n\t\t\tif (document->isSongWithCover()) {\n\t\t\t\tconst auto size = QSize(\n\t\t\t\t\tst::attachPreviewLayout.thumbSize,\n\t\t\t\t\tst::attachPreviewLayout.thumbSize);\n\t\t\t\tauto thumb = QPixmap(size);\n\t\t\t\tthumb.fill(Qt::transparent);\n\t\t\t\tPainter p(&thumb);\n\n\t\t\t\tHistoryView::DrawThumbnailAsSongCover(\n\t\t\t\t\tp,\n\t\t\t\t\tst::songCoverOverlayFg,\n\t\t\t\t\t_documentMedia,\n\t\t\t\t\tQRect(QPoint(), size));\n\t\t\t\tdata.fileThumb = std::move(thumb);\n\t\t\t}\n\t\t} else if (document->isVoiceMessage()) {\n\t\t\tsongTitle = tr::lng_media_audio(tr::now);\n\t\t}\n\n\t\tdata.name = Text::FormatSongName(filename, songTitle, songPerformer)\n\t\t\t.string();\n\t} else {\n\t\tdata.name = document->filename();\n\t}\n\tdata.statusText = FormatSizeText(document->size);\n\n\tsetData(std::move(data));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_item_single_file_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/chat/attach/attach_abstract_single_file_preview.h\"\n\nclass HistoryItem;\nclass DocumentData;\n\nnamespace Data {\nclass DocumentMedia;\n} // namespace Data\n\nnamespace Ui {\n\nstruct PreparedFile;\nclass IconButton;\n\nclass ItemSingleFilePreview final : public AbstractSingleFilePreview {\npublic:\n\tItemSingleFilePreview(\n\t\tQWidget *parent,\n\t\tconst style::ComposeControls &st,\n\t\tnot_null<HistoryItem*> item,\n\t\tAttachControls::Type type);\n\nprivate:\n\tvoid preparePreview(not_null<DocumentData*> document);\n\n\tstd::shared_ptr<::Data::DocumentMedia> _documentMedia;\n\n\trpl::lifetime _lifetimeDownload;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/attach/attach_item_single_media_preview.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"data/data_streaming.h\"\n#include \"history/history_item.h\"\n#include \"history/view/media/history_view_document.h\"\n#include \"main/main_session.h\"\n#include \"media/streaming/media_streaming_document.h\"\n#include \"media/streaming/media_streaming_instance.h\"\n#include \"media/streaming/media_streaming_loader_local.h\"\n#include \"media/streaming/media_streaming_player.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace Ui {\nnamespace {\n\nusing namespace ::Media::Streaming;\n\n} // namespace\n\nItemSingleMediaPreview::ItemSingleMediaPreview(\n\tQWidget *parent,\n\tconst style::ComposeControls &st,\n\tFn<bool()> gifPaused,\n\tnot_null<HistoryItem*> item,\n\tAttachControls::Type type)\n: AbstractSingleMediaPreview(parent, st, type)\n, _gifPaused(std::move(gifPaused))\n, _fullId(item->fullId()) {\n\tconst auto media = item->media();\n\tAssert(media != nullptr);\n\n\tMain::Session *session = nullptr;\n\n\tif (const auto photo = media->photo()) {\n\t\t_photoMedia = photo->createMediaView();\n\t\t_photoMedia->wanted(Data::PhotoSize::Large, item->fullId());\n\n\t\tsession = &photo->session();\n\t} else if (const auto document = media->document()) {\n\t\t_documentMedia = document->createMediaView();\n\t\t_documentMedia->thumbnailWanted(item->fullId());\n\n\t\tsession = &document->session();\n\t\tif (document->isAnimation() || document->isVideoFile()) {\n\t\t\tsetAnimated(true);\n\t\t\tprepareStreamedPreview();\n\t\t}\n\t} else {\n\t\tUnexpected(\"Photo or document should be set.\");\n\t}\n\n\tstruct ThumbInfo {\n\t\tbool loaded = false;\n\t\tImage *image = nullptr;\n\t};\n\n\tconst auto computeThumbInfo = [=]() -> ThumbInfo {\n\t\tusing Size = Data::PhotoSize;\n\t\tif (_documentMedia) {\n\t\t\treturn { true, _documentMedia->thumbnail() };\n\t\t} else if (const auto large = _photoMedia->image(Size::Large)) {\n\t\t\treturn { true, large };\n\t\t} else if (const auto thumbnail = _photoMedia->image(\n\t\t\t\tSize::Thumbnail)) {\n\t\t\treturn { false, thumbnail };\n\t\t} else if (const auto small = _photoMedia->image(Size::Small)) {\n\t\t\treturn { false, small };\n\t\t} else {\n\t\t\treturn { false, _photoMedia->thumbnailInline() };\n\t\t}\n\t};\n\n\trpl::single(rpl::empty) | rpl::then(\n\t\tsession->downloaderTaskFinished()\n\t) | rpl::on_next([=] {\n\t\tconst auto computed = computeThumbInfo();\n\t\tif (!computed.image) {\n\t\t\tif (_documentMedia && !_documentMedia->owner()->hasThumbnail()) {\n\t\t\t\tconst auto size = _documentMedia->owner()->dimensions.scaled(\n\t\t\t\t\tst::sendMediaPreviewSize,\n\t\t\t\t\tst::confirmMaxHeight,\n\t\t\t\t\tQt::KeepAspectRatio);\n\t\t\t\tif (!size.isEmpty()) {\n\t\t\t\t\tauto empty = QImage(\n\t\t\t\t\t\tsize,\n\t\t\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t\t\tempty.fill(Qt::black);\n\t\t\t\t\tpreparePreview(empty);\n\t\t\t\t}\n\t\t\t\t_lifetimeDownload.destroy();\n\t\t\t}\n\t\t\treturn;\n\t\t} else if (computed.loaded) {\n\t\t\t_lifetimeDownload.destroy();\n\t\t}\n\t\tpreparePreview(computed.image->original());\n\t}, _lifetimeDownload);\n}\n\nvoid ItemSingleMediaPreview::prepareStreamedPreview() {\n\tif (_streamed || !_documentMedia) {\n\t\treturn;\n\t}\n\tconst auto document = _documentMedia\n\t\t? _documentMedia->owner().get()\n\t\t: nullptr;\n\tif (document && document->isAnimation()) {\n\t\tsetupStreamedPreview(\n\t\t\tdocument->owner().streaming().sharedDocument(\n\t\t\t\tdocument,\n\t\t\t\t_fullId));\n\t}\n}\n\nvoid ItemSingleMediaPreview::setupStreamedPreview(\n\t\tstd::shared_ptr<Document> shared) {\n\tif (!shared) {\n\t\treturn;\n\t}\n\t_streamed = std::make_unique<Instance>(\n\t\tstd::move(shared),\n\t\t[=] { update(); });\n\t_streamed->lockPlayer();\n\t_streamed->player().updates(\n\t) | rpl::on_next_error([=](Update &&update) {\n\t\thandleStreamingUpdate(std::move(update));\n\t}, [=](Error &&error) {\n\t\thandleStreamingError(std::move(error));\n\t}, _streamed->lifetime());\n\n\tif (_streamed->ready()) {\n\t\tstreamingReady(base::duplicate(_streamed->info()));\n\t}\n\tcheckStreamedIsStarted();\n}\n\nvoid ItemSingleMediaPreview::handleStreamingUpdate(Update &&update) {\n\tv::match(update.data, [&](Information &update) {\n\t\tstreamingReady(std::move(update));\n\t}, [](PreloadedVideo) {\n\t}, [&](UpdateVideo) {\n\t\tthis->update();\n\t}, [](PreloadedAudio) {\n\t}, [](UpdateAudio) {\n\t}, [](WaitingForData) {\n\t}, [](SpeedEstimate) {\n\t}, [](MutedByOther) {\n\t}, [](Finished) {\n\t});\n}\n\nvoid ItemSingleMediaPreview::handleStreamingError(Error &&error) {\n}\n\nvoid ItemSingleMediaPreview::streamingReady(Information &&info) {\n}\n\nvoid ItemSingleMediaPreview::checkStreamedIsStarted() {\n\tif (!_streamed) {\n\t\treturn;\n\t} else if (_streamed->paused()) {\n\t\t_streamed->resume();\n\t}\n\tif (!_streamed->active() && !_streamed->failed()) {\n\t\tstartStreamedPlayer();\n\t}\n}\n\nvoid ItemSingleMediaPreview::startStreamedPlayer() {\n\tauto options = ::Media::Streaming::PlaybackOptions();\n\toptions.audioId = _documentMedia\n\t\t? AudioMsgId(_documentMedia->owner(), _fullId)\n\t\t: AudioMsgId();\n\toptions.waitForMarkAsShown = true;\n\t//if (!_streamed->withSound) {\n\toptions.mode = ::Media::Streaming::Mode::Video;\n\toptions.loop = true;\n\t//}\n\t_streamed->play(options);\n}\n\nbool ItemSingleMediaPreview::supportsSpoilers() const {\n\treturn false; // We are not allowed to change existing spoiler setting.\n}\n\nbool ItemSingleMediaPreview::drawBackground() const {\n\treturn true; // A sticker can't be here.\n}\n\nbool ItemSingleMediaPreview::tryPaintAnimation(QPainter &p) {\n\tcheckStreamedIsStarted();\n\tif (_streamed\n\t\t&& _streamed->player().ready()\n\t\t&& !_streamed->player().videoSize().isEmpty()) {\n\t\tconst auto s = QSize(previewWidth(), previewHeight());\n\t\tconst auto paused = _gifPaused();\n\n\t\tauto request = ::Media::Streaming::FrameRequest();\n\t\trequest.outer = s * style::DevicePixelRatio();\n\t\trequest.resize = s * style::DevicePixelRatio();\n\t\tp.drawImage(\n\t\t\tQRect(\n\t\t\t\tpreviewLeft(),\n\t\t\t\tpreviewTop(),\n\t\t\t\tpreviewWidth(),\n\t\t\t\tpreviewHeight()),\n\t\t\t_streamed->frame(request));\n\t\tif (!paused) {\n\t\t\t_streamed->markFrameShown();\n\t\t}\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool ItemSingleMediaPreview::isAnimatedPreviewReady() const {\n\treturn _streamed != nullptr;\n}\n\nauto ItemSingleMediaPreview::sharedPhotoMedia() const\n-> std::shared_ptr<::Data::PhotoMedia> {\n\treturn _photoMedia;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/chat/attach/attach_abstract_single_media_preview.h\"\n\nnamespace Data {\nclass DocumentMedia;\nclass PhotoMedia;\n} // namespace Data\n\nnamespace Media {\nnamespace Streaming {\nclass Instance;\nclass Document;\nstruct Update;\nenum class Error;\nstruct Information;\n} // namespace Streaming\n} // namespace Media\n\nclass HistoryItem;\n\nnamespace Ui {\n\nclass ItemSingleMediaPreview final : public AbstractSingleMediaPreview {\npublic:\n\tItemSingleMediaPreview(\n\t\tQWidget *parent,\n\t\tconst style::ComposeControls &st,\n\t\tFn<bool()> gifPaused,\n\t\tnot_null<HistoryItem*> item,\n\t\tAttachControls::Type type);\n\n\tstd::shared_ptr<::Data::PhotoMedia> sharedPhotoMedia() const;\n\nprotected:\n\tbool supportsSpoilers() const override;\n\tbool drawBackground() const override;\n\tbool tryPaintAnimation(QPainter &p) override;\n\tbool isAnimatedPreviewReady() const override;\n\nprivate:\n\tvoid prepareStreamedPreview();\n\tvoid checkStreamedIsStarted();\n\tvoid setupStreamedPreview(\n\t\tstd::shared_ptr<::Media::Streaming::Document> shared);\n\tvoid handleStreamingUpdate(::Media::Streaming::Update &&update);\n\tvoid handleStreamingError(::Media::Streaming::Error &&error);\n\tvoid streamingReady(::Media::Streaming::Information &&info);\n\tvoid startStreamedPlayer();\n\n\tconst Fn<bool()> _gifPaused;\n\tconst FullMsgId _fullId;\n\n\tstd::shared_ptr<::Data::PhotoMedia> _photoMedia;\n\tstd::shared_ptr<::Data::DocumentMedia> _documentMedia;\n\n\tstd::unique_ptr<::Media::Streaming::Instance> _streamed;\n\n\n\trpl::lifetime _lifetimeDownload;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/attach/attach_prepare.h\"\n\n#include \"ui/rp_widget.h\"\n#include \"ui/widgets/popup_menu.h\"\n\n#include \"ui/chat/attach/attach_send_files_way.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"core/mime_type.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_media_player.h\"\n\n#include <QFileInfo>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kMaxAlbumCount = 10;\nconstexpr auto kStandardPhotoSideLimit = 1280;\n\nstruct GroupRange {\n\tint from = 0;\n\tint till = 0;\n\tAlbumType type = AlbumType::None;\n\n\t[[nodiscard]] int size() const {\n\t\treturn till - from;\n\t}\n};\n\nstruct HighQualityBadgeCache {\n\tQRgb bg = 0;\n\tQRgb fg = 0;\n\tqreal ratio = 0.;\n\tint width = 0;\n\tint height = 0;\n\tQImage image;\n};\n\n[[nodiscard]] const QImage &HighQualityBadgeImage(\n\t\tconst style::ComposeControls &st) {\n\tstatic auto cache = HighQualityBadgeCache();\n\tconst auto text = u\"HD\"_q;\n\tconst auto &font = st::mediaPlayerSpeedButton.font;\n\tconst auto xpadding = style::ConvertScale(2.);\n\tconst auto ypadding = 0;\n\tconst auto stroke = style::ConvertScaleExact(1.);\n\tconst auto width = font->width(text);\n\tconst auto height = font->height;\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto bg = st::roundedBg->c.rgba();\n\tconst auto fg = st::roundedFg->c.rgba();\n\tif (cache.image.isNull()\n\t\t|| (cache.bg != bg)\n\t\t|| (cache.fg != fg)\n\t\t|| (cache.ratio != ratio)\n\t\t|| (cache.width != width)\n\t\t|| (cache.height != height)) {\n\t\tcache.bg = bg;\n\t\tcache.fg = fg;\n\t\tcache.ratio = ratio;\n\t\tcache.width = width;\n\t\tcache.height = height;\n\t\tcache.image = QImage(\n\t\t\t(width + 2 * xpadding + stroke) * ratio,\n\t\t\t(height + 2 * ypadding + stroke) * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tcache.image.setDevicePixelRatio(ratio);\n\t\tcache.image.fill(Qt::transparent);\n\t\tauto painter = QPainter(&cache.image);\n\t\tauto hq = PainterHighQualityEnabler(painter);\n\t\tpainter.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tpainter.setPen(QPen(Qt::transparent, stroke));\n\t\tpainter.setBrush(st::roundedBg);\n\t\tpainter.setFont(font);\n\t\tpainter.drawRoundedRect(\n\t\t\tQRectF(\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\twidth + 2 * xpadding + stroke,\n\t\t\t\theight + 2 * ypadding + stroke),\n\t\t\theight / 3.,\n\t\t\theight / 3.);\n\t\tpainter.setPen(st::roundedFg);\n\t\tpainter.drawText(\n\t\t\tQPointF(\n\t\t\t\txpadding + stroke / 2.,\n\t\t\t\typadding + font->ascent + stroke / 2.),\n\t\t\ttext);\n\t}\n\treturn cache.image;\n}\n\n[[nodiscard]] AlbumType GroupTypeForFile(\n\t\tPreparedFile::Type type,\n\t\tbool groupFiles,\n\t\tbool sendImagesAsPhotos) {\n\tusing Type = PreparedFile::Type;\n\treturn (type == Type::Music)\n\t\t? (groupFiles ? AlbumType::Music : AlbumType::None)\n\t\t: (type == Type::Video || type == Type::Photo)\n\t\t? ((groupFiles && sendImagesAsPhotos)\n\t\t\t? AlbumType::PhotoVideo\n\t\t\t: (groupFiles && !sendImagesAsPhotos)\n\t\t\t? AlbumType::File\n\t\t\t: AlbumType::None)\n\t\t: (type == Type::File)\n\t\t? (groupFiles ? AlbumType::File : AlbumType::None)\n\t\t: AlbumType::None;\n}\n\n[[nodiscard]] std::vector<GroupRange> GroupRanges(\n\t\tconst std::vector<PreparedFile> &files,\n\t\tSendFilesWay way,\n\t\tbool slowmode) {\n\tconst auto sendImagesAsPhotos = way.sendImagesAsPhotos();\n\tconst auto groupFiles = way.groupFiles() || slowmode;\n\n\tauto result = std::vector<GroupRange>();\n\tif (files.empty()) {\n\t\treturn result;\n\t}\n\tauto from = 0;\n\tauto groupType = AlbumType::None;\n\tfor (auto i = 0; i != int(files.size()); ++i) {\n\t\tconst auto fileGroupType = GroupTypeForFile(\n\t\t\tfiles[i].type,\n\t\t\tgroupFiles,\n\t\t\tsendImagesAsPhotos);\n\t\tconst auto count = (i - from);\n\t\tif ((i > from && groupType != fileGroupType)\n\t\t\t|| ((groupType != AlbumType::None) && (count == kMaxAlbumCount))) {\n\t\t\tresult.push_back(GroupRange{\n\t\t\t\t.from = from,\n\t\t\t\t.till = i,\n\t\t\t\t.type = (count > 1) ? groupType : AlbumType::None,\n\t\t\t});\n\t\t\tfrom = i;\n\t\t}\n\t\tgroupType = fileGroupType;\n\t}\n\tconst auto till = int(files.size());\n\tconst auto count = (till - from);\n\tresult.push_back(GroupRange{\n\t\t.from = from,\n\t\t.till = till,\n\t\t.type = (count > 1) ? groupType : AlbumType::None,\n\t});\n\treturn result;\n}\n\n} // namespace\n\nPreparedFile::PreparedFile(const QString &path) : path(path) {\n\tconst auto fileInfo = QFileInfo(path);\n\tdisplayName = fileInfo.fileName();\n}\n\nPreparedFile::PreparedFile(PreparedFile &&other) = default;\n\nPreparedFile &PreparedFile::operator=(PreparedFile &&other) = default;\n\nPreparedFile::~PreparedFile() = default;\n\nbool PreparedFile::canBeInAlbumType(AlbumType album) const {\n\treturn CanBeInAlbumType(type, album);\n}\n\nbool PreparedFile::isSticker() const {\n\tExpects(information != nullptr);\n\n\treturn (type == PreparedFile::Type::Photo)\n\t\t&& Core::IsMimeSticker(information->filemime);\n}\n\nbool PreparedFile::isVideoFile() const {\n\tExpects(information != nullptr);\n\n\tusing Video = Ui::PreparedFileInformation::Video;\n\treturn (type == PreparedFile::Type::Video)\n\t\t&& v::is<Video>(information->media)\n\t\t&& !v::get<Video>(information->media).isGifv;\n}\n\nbool PreparedFile::isGifv() const {\n\tExpects(information != nullptr);\n\n\tusing Video = Ui::PreparedFileInformation::Video;\n\treturn (type == PreparedFile::Type::Video)\n\t\t&& v::is<Video>(information->media)\n\t\t&& v::get<Video>(information->media).isGifv;\n}\n\nbool PreparedFile::canUseHighQualityPhoto() const {\n\treturn (type == PreparedFile::Type::Photo)\n\t\t&& (information != nullptr)\n\t\t&& !isSticker()\n\t\t&& ((originalDimensions.width() > kStandardPhotoSideLimit)\n\t\t\t|| (originalDimensions.height() > kStandardPhotoSideLimit));\n}\n\nAlbumType PreparedFile::albumType(bool sendImagesAsPhotos) const {\n\tswitch (type) {\n\tcase Type::Photo:\n\t\treturn sendImagesAsPhotos ? AlbumType::PhotoVideo : AlbumType::File;\n\tcase Type::Video:\n\t\treturn AlbumType::PhotoVideo;\n\tcase Type::Music:\n\t\treturn AlbumType::Music;\n\tcase Type::File:\n\t\treturn AlbumType::File;\n\tcase Type::None:\n\t\treturn AlbumType::None;\n\t}\n\tUnexpected(\"PreparedFile::type in PreparedFile::albumType().\");\n}\n\nbool CanBeInAlbumType(PreparedFile::Type type, AlbumType album) {\n\tExpects(album != AlbumType::None);\n\n\tusing Type = PreparedFile::Type;\n\tswitch (album) {\n\tcase AlbumType::PhotoVideo:\n\t\treturn (type == Type::Photo) || (type == Type::Video);\n\tcase AlbumType::Music:\n\t\treturn (type == Type::Music);\n\tcase AlbumType::File:\n\t\treturn (type == Type::Photo)\n\t\t\t|| (type == Type::Video)\n\t\t\t|| (type == Type::File);\n\t}\n\tUnexpected(\"AlbumType in CanBeInAlbumType.\");\n}\n\nbool InsertTextOnImageCancel(const QString &text) {\n\treturn !text.isEmpty() && !text.startsWith(u\"data:image\"_q);\n}\n\nPreparedList PreparedList::Reordered(\n\t\tPreparedList &&list,\n\t\tstd::vector<int> order) {\n\tExpects(list.error == PreparedList::Error::None);\n\tExpects(list.files.size() == order.size());\n\n\tauto result = PreparedList(list.error, list.errorData);\n\tresult.files.reserve(list.files.size());\n\tfor (auto index : order) {\n\t\tresult.files.push_back(std::move(list.files[index]));\n\t}\n\treturn result;\n}\n\nvoid PreparedList::mergeToEnd(PreparedList &&other, bool cutToAlbumSize) {\n\tif (error != Error::None) {\n\t\treturn;\n\t}\n\tif (other.error != Error::None) {\n\t\terror = other.error;\n\t\terrorData = other.errorData;\n\t\treturn;\n\t}\n\tfiles.reserve(std::min(\n\t\tsize_t(cutToAlbumSize ? kMaxAlbumCount : INT_MAX),\n\t\tfiles.size() + other.files.size()));\n\tfor (auto &file : other.files) {\n\t\tif (cutToAlbumSize && files.size() == kMaxAlbumCount) {\n\t\t\tbreak;\n\t\t}\n\t\tfiles.push_back(std::move(file));\n\t}\n}\n\nbool PreparedList::canBeSentInSlowmode() const {\n\treturn canBeSentInSlowmodeWith(PreparedList());\n}\n\nbool PreparedList::canBeSentInSlowmodeWith(const PreparedList &other) const {\n\tif (!filesToProcess.empty() || !other.filesToProcess.empty()) {\n\t\treturn false;\n\t} else if (files.size() + other.files.size() < 2) {\n\t\treturn true;\n\t} else if (files.size() + other.files.size() > kMaxAlbumCount) {\n\t\treturn false;\n\t}\n\n\tusing Type = PreparedFile::Type;\n\tauto &&all = ranges::views::concat(files, other.files);\n\tconst auto has = [&](Type type) {\n\t\treturn ranges::contains(all, type, &PreparedFile::type);\n\t};\n\tconst auto hasNonGrouping = has(Type::None);\n\tconst auto hasPhotos = has(Type::Photo);\n\tconst auto hasFiles = has(Type::File);\n\tconst auto hasVideos = has(Type::Video);\n\tconst auto hasMusic = has(Type::Music);\n\n\t// File-s and Video-s never can be grouped.\n\t// Music-s can be grouped only with themselves.\n\tif (hasNonGrouping) {\n\t\treturn false;\n\t} else if (hasFiles) {\n\t\treturn !hasMusic && !hasVideos;\n\t} else if (hasVideos) {\n\t\treturn !hasMusic && !hasFiles;\n\t} else if (hasMusic) {\n\t\treturn !hasVideos && !hasFiles && !hasPhotos;\n\t}\n\treturn !hasNonGrouping && (!hasFiles || !hasVideos);\n}\n\nbool PreparedList::canAddCaption(bool compress) const {\n\tif (files.empty()) {\n\t\treturn false;\n\t}\n\tconst auto &last = files.back();\n\tconst auto isSticker = last.path.endsWith(u\".tgs\"_q, Qt::CaseInsensitive)\n\t\t|| (!compress\n\t\t\t&& last.information\n\t\t\t&& Core::IsMimeSticker(last.information->filemime));\n\treturn !isSticker;\n}\n\nbool PreparedList::canMoveCaption(bool sendingAlbum, bool compress) const {\n\tif (!canAddCaption(compress)) {\n\t\treturn false;\n\t} else if (files.size() > kMaxAlbumCount) {\n\t\treturn false;\n\t} else if (!sendingAlbum || !compress) {\n\t\treturn (files.size() == 1);\n\t}\n\tfor (const auto &file : files) {\n\t\tif (file.type != PreparedFile::Type::Photo\n\t\t\t&& file.type != PreparedFile::Type::Video) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nbool PreparedList::canChangePrice(bool sendingAlbum, bool compress) const {\n\treturn canMoveCaption(sendingAlbum, compress);\n}\n\nbool PreparedList::hasGroupOption(bool slowmode) const {\n\tif (slowmode || files.size() < 2) {\n\t\treturn false;\n\t}\n\tusing Type = PreparedFile::Type;\n\tauto lastType = Type::None;\n\tfor (const auto &file : files) {\n\t\tif ((file.type == lastType)\n\t\t\t|| (file.type == Type::Video && lastType == Type::Photo)\n\t\t\t|| (file.type == Type::Photo && lastType == Type::Video)\n\t\t\t|| (file.type == Type::File && lastType == Type::Photo)\n\t\t\t|| (file.type == Type::Photo && lastType == Type::File)) {\n\t\t\tif (lastType != Type::None) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\tlastType = file.type;\n\t}\n\treturn false;\n}\n\nbool PreparedList::hasSendImagesAsPhotosOption(bool slowmode) const {\n\tusing Type = PreparedFile::Type;\n\tif (slowmode) {\n\t\tconst auto t = files.front().type;\n\t\treturn (files.size() == 1)\n\t\t\t&& (t == Type::Photo || t == Type::Video);\n\t}\n\treturn ranges::contains(files, Type::Photo, &PreparedFile::type)\n\t\t|| ranges::contains(files, Type::Video, &PreparedFile::type);\n}\n\nbool PreparedList::canHaveEditorHintLabel() const {\n\tfor (const auto &file : files) {\n\t\tif ((file.type == PreparedFile::Type::Photo)\n\t\t\t&& !Core::IsMimeSticker(file.information->filemime)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool PreparedList::hasSticker() const {\n\treturn ranges::any_of(files, &PreparedFile::isSticker);\n}\n\nbool PreparedList::hasSpoilerMenu(bool compress) const {\n\tconst auto allAreVideo = !ranges::any_of(files, [](const auto &f) {\n\t\tusing Type = Ui::PreparedFile::Type;\n\t\treturn (f.type != Type::Video);\n\t});\n\tconst auto allAreMedia = !ranges::any_of(files, [](const auto &f) {\n\t\tusing Type = Ui::PreparedFile::Type;\n\t\treturn (f.type != Type::Photo) && (f.type != Type::Video);\n\t});\n\treturn allAreVideo || (allAreMedia && compress);\n}\n\nbool PreparedList::hasSendLargePhotosOption(bool compress) const {\n\treturn compress\n\t\t&& ranges::any_of(files, &PreparedFile::canUseHighQualityPhoto);\n}\n\nstd::shared_ptr<PreparedBundle> PrepareFilesBundle(\n\t\tstd::vector<PreparedGroup> groups,\n\t\tSendFilesWay way,\n\t\tbool ctrlShiftEnter) {\n\tauto totalCount = 0;\n\tfor (const auto &group : groups) {\n\t\ttotalCount += group.list.files.size();\n\t}\n\treturn std::make_shared<PreparedBundle>(PreparedBundle{\n\t\t.groups = std::move(groups),\n\t\t.way = way,\n\t\t.totalCount = totalCount,\n\t\t.ctrlShiftEnter = ctrlShiftEnter,\n\t});\n}\n\nint MaxAlbumItems() {\n\treturn kMaxAlbumCount;\n}\n\nbool ValidateThumbDimensions(int width, int height) {\n\treturn (width > 0)\n\t\t&& (height > 0)\n\t\t&& (width <= 20 * height)\n\t\t&& (height <= 20 * width);\n}\n\nstd::vector<PreparedGroup> DivideByGroups(\n\t\tPreparedList &&list,\n\t\tSendFilesWay way,\n\t\tbool slowmode) {\n\tconst auto ranges = GroupRanges(list.files, way, slowmode);\n\tauto result = std::vector<PreparedGroup>();\n\tresult.reserve(ranges.size());\n\tfor (const auto &range : ranges) {\n\t\tauto grouped = Ui::PreparedList();\n\t\tgrouped.files.reserve(range.size());\n\t\tfor (auto i = range.from; i != range.till; ++i) {\n\t\t\tgrouped.files.push_back(std::move(list.files[i]));\n\t\t}\n\t\tresult.push_back(PreparedGroup{\n\t\t\t.list = std::move(grouped),\n\t\t\t.type = range.type,\n\t\t});\n\t}\n\treturn result;\n}\n\nQPixmap PrepareSongCoverForThumbnail(QImage image, int size) {\n\tconst auto scaledSize = image.size().scaled(\n\t\tsize,\n\t\tsize,\n\t\tQt::KeepAspectRatioByExpanding);\n\tusing Option = Images::Option;\n\tconst auto ratio = style::DevicePixelRatio();\n\treturn PixmapFromImage(Images::Prepare(\n\t\tstd::move(image),\n\t\tscaledSize * ratio,\n\t\t{\n\t\t\t.colored = &st::songCoverOverlayFg,\n\t\t\t.options = Option::RoundCircle,\n\t\t\t.outer = { size, size },\n\t\t}));\n}\n\nQPixmap BlurredPreviewFromPixmap(QPixmap pixmap, RectParts corners) {\n\tconst auto image = pixmap.toImage();\n\tconst auto skip = st::roundRadiusLarge * image.devicePixelRatio();\n\tauto small = image.copy(\n\t\tskip,\n\t\tskip,\n\t\timage.width() - 2 * skip,\n\t\timage.height() - 2 * skip\n\t).scaled(\n\t\t40,\n\t\t40,\n\t\tQt::KeepAspectRatioByExpanding,\n\t\tQt::SmoothTransformation);\n\n\tusing namespace Images;\n\treturn PixmapFromImage(Prepare(\n\t\tBlur(std::move(small), true),\n\t\timage.size(),\n\t\t{ .options = RoundOptions(ImageRoundRadius::Large, corners) }));\n}\n\nvoid PaintHighQualityBadge(\n\t\tQPainter &p,\n\t\tconst style::ComposeControls &st,\n\t\tQRect rect,\n\t\tRectPart origin) {\n\tconst auto outerSkip = st.photoQualityBadgeOuterSkip;\n\tconst auto &badge = HighQualityBadgeImage(st);\n\tconst auto size = badge.size() / badge.devicePixelRatio();\n\tconst auto left = (origin == RectPart::TopLeft)\n\t\t|| (origin == RectPart::BottomLeft);\n\tconst auto top = (origin == RectPart::TopLeft)\n\t\t|| (origin == RectPart::TopRight);\n\tconst auto x = left\n\t\t? (rect.x() + outerSkip)\n\t\t: (rect.x() + rect.width() - size.width() - outerSkip);\n\tconst auto y = top\n\t\t? (rect.y() + outerSkip)\n\t\t: (rect.y() + rect.height() - size.height() - outerSkip);\n\tp.drawImage(QPointF(x, y), badge);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_prepare.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"editor/photo_editor_common.h\"\n#include \"ui/chat/attach/attach_send_files_way.h\"\n#include \"ui/rect_part.h\"\n\n#include <QtCore/QSemaphore>\n#include <deque>\n\nclass QPainter;\n\nnamespace style {\nstruct ComposeControls;\n} // namespace style\n\nnamespace Ui {\n\nclass RpWidget;\nclass SendFilesWay;\n\nstruct PreparedFileInformation {\n\tstruct Image {\n\t\tQImage data;\n\t\tQByteArray bytes;\n\t\tQByteArray format;\n\t\tbool animated = false;\n\t\tEditor::PhotoModifications modifications;\n\t};\n\tstruct Song {\n\t\tcrl::time duration = -1;\n\t\tQString title;\n\t\tQString performer;\n\t\tQImage cover;\n\t};\n\tstruct Video {\n\t\tbool isGifv = false;\n\t\tbool isWebmSticker = false;\n\t\tbool supportsStreaming = false;\n\t\tcrl::time duration = -1;\n\t\tQImage thumbnail;\n\t};\n\n\tQString filemime;\n\tstd::variant<v::null_t, Image, Song, Video> media;\n};\n\nenum class AlbumType {\n\tNone,\n\tPhotoVideo,\n\tMusic,\n\tFile,\n};\n\nstruct PreparedFile {\n\t// File-s can be grouped if 'groupFiles'.\n\t// File-s + Photo-s can be grouped if 'groupFiles && !sendImagesAsPhotos'.\n\t// Photo-s can be grouped if 'groupFiles'.\n\t// Photo-s + Video-s can be grouped if 'groupFiles && sendImagesAsPhotos'.\n\t// Video-s can be grouped if 'groupFiles'.\n\t// Music-s can be grouped if 'groupFiles'.\n\tenum class Type {\n\t\tNone,\n\t\tPhoto,\n\t\tVideo,\n\t\tMusic,\n\t\tFile,\n\t};\n\n\tPreparedFile(const QString &path);\n\tPreparedFile(PreparedFile &&other);\n\tPreparedFile &operator=(PreparedFile &&other);\n\t~PreparedFile();\n\n\t[[nodiscard]] bool canBeInAlbumType(AlbumType album) const;\n\t[[nodiscard]] AlbumType albumType(bool sendImagesAsPhotos) const;\n\t[[nodiscard]] bool isSticker() const;\n\t[[nodiscard]] bool isVideoFile() const;\n\t[[nodiscard]] bool isGifv() const;\n\t[[nodiscard]] bool canUseHighQualityPhoto() const;\n\n\tQString path;\n\tQString displayName;\n\tTextWithTags caption;\n\tQByteArray content;\n\tint64 size = 0;\n\tstd::unique_ptr<PreparedFileInformation> information;\n\tstd::unique_ptr<PreparedFile> videoCover;\n\tQImage preview;\n\tQSize shownDimensions;\n\tQSize originalDimensions;\n\tType type = Type::File;\n\tbool spoiler = false;\n\tbool sendLargePhotos = false;\n};\n\n[[nodiscard]] bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album);\n[[nodiscard]] bool InsertTextOnImageCancel(const QString &text);\n\nstruct PreparedList {\n\tenum class Error {\n\t\tNone,\n\t\tNonLocalUrl,\n\t\tDirectory,\n\t\tEmptyFile,\n\t\tTooLargeFile,\n\t};\n\n\tPreparedList() = default;\n\tPreparedList(Error error, QString errorData)\n\t: error(error)\n\t, errorData(errorData) {\n\t}\n\tPreparedList(PreparedList &&other) = default;\n\tPreparedList &operator=(PreparedList &&other) = default;\n\n\t[[nodiscard]] static PreparedList Reordered(\n\t\tPreparedList &&list,\n\t\tstd::vector<int> order);\n\tvoid mergeToEnd(PreparedList &&other, bool cutToAlbumSize = false);\n\n\t[[nodiscard]] bool canAddCaption(bool compress) const;\n\t[[nodiscard]] bool canMoveCaption(\n\t\tbool sendingAlbum,\n\t\tbool compress) const;\n\t[[nodiscard]] bool canChangePrice(\n\t\tbool sendingAlbum,\n\t\tbool compress) const;\n\t[[nodiscard]] bool canBeSentInSlowmode() const;\n\t[[nodiscard]] bool canBeSentInSlowmodeWith(\n\t\tconst PreparedList &other) const;\n\n\t[[nodiscard]] bool hasGroupOption(bool slowmode) const;\n\t[[nodiscard]] bool hasSendImagesAsPhotosOption(bool slowmode) const;\n\t[[nodiscard]] bool canHaveEditorHintLabel() const;\n\t[[nodiscard]] bool hasSticker() const;\n\t[[nodiscard]] bool hasSpoilerMenu(bool compress) const;\n\t[[nodiscard]] bool hasSendLargePhotosOption(bool compress) const;\n\n\tError error = Error::None;\n\tQString errorData;\n\tstd::vector<PreparedFile> files;\n\tstd::deque<PreparedFile> filesToProcess;\n\tstd::optional<bool> overrideSendImagesAsPhotos;\n};\n\nstruct PreparedGroup {\n\tPreparedList list;\n\tAlbumType type = AlbumType::None;\n};\n\n[[nodiscard]] std::vector<PreparedGroup> DivideByGroups(\n\tPreparedList &&list,\n\tSendFilesWay way,\n\tbool slowmode);\n\nstruct PreparedBundle {\n\tstd::vector<PreparedGroup> groups;\n\tSendFilesWay way;\n\tint totalCount = 0;\n\tbool ctrlShiftEnter = false;\n};\n[[nodiscard]] std::shared_ptr<PreparedBundle> PrepareFilesBundle(\n\tstd::vector<PreparedGroup> groups,\n\tSendFilesWay way,\n\tbool ctrlShiftEnter);\n\n[[nodiscard]] int MaxAlbumItems();\n[[nodiscard]] bool ValidateThumbDimensions(int width, int height);\n\n[[nodiscard]] QPixmap PrepareSongCoverForThumbnail(QImage image, int size);\n\n[[nodiscard]] QPixmap BlurredPreviewFromPixmap(\n\tQPixmap pixmap,\n\tRectParts corners);\n\nvoid PaintHighQualityBadge(\n\tQPainter &p,\n\tconst style::ComposeControls &st,\n\tQRect rect,\n\tRectPart origin = RectPart::BottomLeft);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_send_files_way.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/attach/attach_send_files_way.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kSerializedSendLargePhotos = int32(4);\n\n} // namespace\n\nvoid SendFilesWay::setSendImagesAsPhotos(bool value) {\n\tif (value) {\n\t\t_flags |= Flag::SendImagesAsPhotos;\n\t} else {\n\t\tif (hasCompressedStickers()) {\n\t\t\tsetGroupFiles(false);\n\t\t}\n\t\t_flags &= ~Flag::SendImagesAsPhotos;\n\t}\n}\n\nvoid SendFilesWay::setGroupFiles(bool value) {\n\tif (value) {\n\t\t_flags |= Flag::GroupFiles;\n\t\tif (hasCompressedStickers()) {\n\t\t\tsetSendImagesAsPhotos(true);\n\t\t}\n\t} else {\n\t\t_flags &= ~Flag::GroupFiles;\n\t}\n}\n\nvoid SendFilesWay::setSendLargePhotos(bool value) {\n\tif (value) {\n\t\t_flags |= Flag::SendLargePhotos;\n\t} else {\n\t\t_flags &= ~Flag::SendLargePhotos;\n\t}\n}\n\nvoid SendFilesWay::setHasCompressedStickers(bool value) {\n\tif (value) {\n\t\t_flags |= Flag::HasCompressedStickers;\n\t} else {\n\t\t_flags &= ~Flag::HasCompressedStickers;\n\t}\n}\n\n//enum class SendFilesWay { // Old way. Serialize should be compatible.\n//\tAlbum,\n//\tPhotos,\n//\tFiles,\n//};\n\nint32 SendFilesWay::serialize() const {\n\tauto result = (sendImagesAsPhotos() && groupFiles())\n\t\t? int32(0)\n\t\t: sendImagesAsPhotos()\n\t\t? int32(1)\n\t\t: groupFiles()\n\t\t? int32(3)\n\t\t: int32(2);\n\tif (sendLargePhotos()) {\n\t\tresult |= kSerializedSendLargePhotos;\n\t}\n\treturn result;\n}\n\nstd::optional<SendFilesWay> SendFilesWay::FromSerialized(int32 value) {\n\tif (value < 0 || value > 7) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto sendLargePhotos = (value & kSerializedSendLargePhotos) != 0;\n\tvalue &= ~kSerializedSendLargePhotos;\n\tauto result = SendFilesWay();\n\tresult.setGroupFiles((value == 0) || (value == 3));\n\tresult.setSendImagesAsPhotos((value == 0) || (value == 1));\n\tresult.setSendLargePhotos(sendLargePhotos);\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_send_files_way.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n\nnamespace Ui {\n\nenum class AttachButtonType {\n\tEdit,\n\tDelete,\n\tModify,\n\tNone,\n};\n\nclass SendFilesWay final {\npublic:\n\t[[nodiscard]] bool groupFiles() const {\n\t\treturn (_flags & Flag::GroupFiles) != 0;\n\t}\n\t[[nodiscard]] bool sendImagesAsPhotos() const {\n\t\treturn (_flags & Flag::SendImagesAsPhotos) != 0;\n\t}\n\t[[nodiscard]] bool sendLargePhotos() const {\n\t\treturn (_flags & Flag::SendLargePhotos) != 0;\n\t}\n\tvoid setGroupFiles(bool value);\n\tvoid setSendImagesAsPhotos(bool value);\n\tvoid setSendLargePhotos(bool value);\n\tvoid setHasCompressedStickers(bool value);\n\n\t[[nodiscard]] inline bool operator<(const SendFilesWay &other) const {\n\t\treturn _flags < other._flags;\n\t}\n\t[[nodiscard]] inline bool operator>(const SendFilesWay &other) const {\n\t\treturn other < *this;\n\t}\n\t[[nodiscard]] inline bool operator<=(const SendFilesWay &other) const {\n\t\treturn !(other < *this);\n\t}\n\t[[nodiscard]] inline bool operator>=(const SendFilesWay &other) const {\n\t\treturn !(*this < other);\n\t}\n\t[[nodiscard]] inline bool operator==(const SendFilesWay &other) const {\n\t\treturn _flags == other._flags;\n\t}\n\t[[nodiscard]] inline bool operator!=(const SendFilesWay &other) const {\n\t\treturn !(*this == other);\n\t}\n\n\t[[nodiscard]] int32 serialize() const;\n\t[[nodiscard]] static std::optional<SendFilesWay> FromSerialized(\n\t\tint32 value);\n\n\tbool asVoice = false;\n\nprivate:\n\t[[nodiscard]] bool hasCompressedStickers() const {\n\t\treturn (_flags & Flag::HasCompressedStickers) != 0;\n\t}\n\n\tenum class Flag : uchar {\n\t\tGroupFiles = (1 << 0),\n\t\tSendImagesAsPhotos = (1 << 1),\n\t\tHasCompressedStickers = (1 << 2),\n\t\tSendLargePhotos = (1 << 3),\n\n\t\tDefault = GroupFiles | SendImagesAsPhotos,\n\t};\n\tfriend inline constexpr bool is_flag_type(Flag) { return true; };\n\n\tbase::flags<Flag> _flags = Flag::Default;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/attach/attach_single_file_preview.h\"\n\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/text/format_song_name.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/ui_utility.h\"\n#include \"core/mime_type.h\"\n#include \"styles/style_chat.h\"\n\n#include <QtCore/QFileInfo>\n\nnamespace Ui {\n\nSingleFilePreview::SingleFilePreview(\n\tQWidget *parent,\n\tconst style::ComposeControls &st,\n\tconst PreparedFile &file,\n\tconst Text::MarkedContext &captionContext,\n\tAttachControls::Type type)\n: AbstractSingleFilePreview(parent, st, type, captionContext) {\n\tpreparePreview(file);\n}\n\nSingleFilePreview::SingleFilePreview(\n\tQWidget *parent,\n\tconst style::ComposeControls &st,\n\tconst PreparedFile &file,\n\tAttachControls::Type type)\n: SingleFilePreview(parent, st, file, {}, type) {\n}\n\nvoid SingleFilePreview::setDisplayName(const QString &displayName) {\n\tAbstractSingleFilePreview::setDisplayName(displayName);\n}\n\nvoid SingleFilePreview::setCaption(const TextWithTags &caption) {\n\tAbstractSingleFilePreview::setCaption(caption);\n}\n\nvoid SingleFilePreview::preparePreview(const PreparedFile &file) {\n\tAbstractSingleFilePreview::Data data;\n\n\tauto preview = QImage();\n\tif (const auto image = std::get_if<PreparedFileInformation::Image>(\n\t\t&file.information->media)) {\n\t\tpreview = file.preview.isNull() ? image->data : file.preview;\n\t} else if (const auto video = std::get_if<PreparedFileInformation::Video>(\n\t\t&file.information->media)) {\n\t\tpreview = video->thumbnail;\n\t}\n\tprepareThumbFor(data, preview);\n\tconst auto filepath = file.path;\n\tif (filepath.isEmpty()) {\n\t\tconst auto fallbackName = u\"image.png\"_q;\n\t\tconst auto displayName = file.displayName.isEmpty()\n\t\t\t? fallbackName\n\t\t\t: file.displayName;\n\t\tdata.name = displayName;\n\t\tif (file.originalDimensions.isValid()) {\n\t\t\tdata.statusText = FormatImageSizeText(file.originalDimensions);\n\t\t\tdata.fileIsImage = true;\n\t\t} else {\n\t\t\tdata.statusText = FormatSizeText(file.size);\n\t\t\tdata.fileIsImage = false;\n\t\t}\n\t} else {\n\t\tauto fileinfo = QFileInfo(filepath);\n\t\tauto filename = file.displayName.isEmpty()\n\t\t\t? fileinfo.fileName()\n\t\t\t: file.displayName;\n\t\tdata.fileIsImage = Core::FileIsImage(\n\t\t\tfilename,\n\t\t\tCore::MimeTypeForFile(fileinfo).name());\n\n\t\tauto songTitle = QString();\n\t\tauto songPerformer = QString();\n\t\tif (file.information) {\n\t\t\tif (const auto song = std::get_if<PreparedFileInformation::Song>(\n\t\t\t\t\t&file.information->media)) {\n\t\t\t\tsongTitle = song->title;\n\t\t\t\tsongPerformer = song->performer;\n\t\t\t\tdata.fileIsAudio = true;\n\n\t\t\t\tif (auto cover = song->cover; !cover.isNull()) {\n\t\t\t\t\tdata.fileThumb = Ui::PrepareSongCoverForThumbnail(\n\t\t\t\t\t\tcover,\n\t\t\t\t\t\tst::attachPreviewLayout.thumbSize);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tdata.name = Text::FormatSongName(filename, songTitle, songPerformer)\n\t\t\t.string();\n\t\tdata.statusText = FormatSizeText(fileinfo.size());\n\t}\n\tauto caption = TextWithEntities{\n\t\tfile.caption.text,\n\t\tTextUtilities::ConvertTextTagsToEntities(file.caption.tags),\n\t};\n\tcaption = TextUtilities::SingleLine(caption);\n\tdata.caption.setMarkedText(\n\t\tst::defaultTextStyle,\n\t\tcaption,\n\t\tkMarkupTextOptions,\n\t\tcaptionContext());\n\n\tsetData(std::move(data));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/chat/attach/attach_abstract_single_file_preview.h\"\n\nnamespace Ui {\n\nstruct PreparedFile;\n\nclass SingleFilePreview final : public AbstractSingleFilePreview {\npublic:\n\tSingleFilePreview(\n\t\tQWidget *parent,\n\t\tconst style::ComposeControls &st,\n\t\tconst PreparedFile &file,\n\t\tconst Text::MarkedContext &captionContext,\n\t\tAttachControls::Type type = AttachControls::Type::Full);\n\tSingleFilePreview(\n\t\tQWidget *parent,\n\t\tconst style::ComposeControls &st,\n\t\tconst PreparedFile &file,\n\t\tAttachControls::Type type = AttachControls::Type::Full);\n\tvoid setDisplayName(const QString &displayName) override;\n\tvoid setCaption(const TextWithTags &caption) override;\n\nprivate:\n\tvoid preparePreview(const PreparedFile &file);\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/attach/attach_single_media_preview.h\"\n\n#include \"editor/photo_editor_common.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"core/mime_type.h\"\n#include \"lottie/lottie_single_player.h\"\n\nnamespace Ui {\n\nSingleMediaPreview *SingleMediaPreview::Create(\n\t\tQWidget *parent,\n\t\tconst style::ComposeControls &st,\n\t\tFn<bool()> gifPaused,\n\t\tconst PreparedFile &file,\n\t\tAttachControls::Type type) {\n\tauto preview = QImage();\n\tauto animated = false;\n\tauto animationPreview = false;\n\tauto hasModifications = false;\n\tif (const auto image = std::get_if<PreparedFileInformation::Image>(\n\t\t\t&file.information->media)) {\n\t\tpreview = Editor::ImageModified(image->data, image->modifications);\n\t\tanimated = animationPreview = image->animated;\n\t\thasModifications = !image->modifications.empty();\n\t} else if (const auto video = std::get_if<PreparedFileInformation::Video>(\n\t\t\t&file.information->media)) {\n\t\tpreview = file.videoCover\n\t\t\t? file.videoCover->preview\n\t\t\t: video->thumbnail;\n\t\tanimated = true;\n\t\tanimationPreview = video->isGifv;\n\t}\n\tif (preview.isNull()) {\n\t\treturn nullptr;\n\t} else if (!animated\n\t\t&& !ValidateThumbDimensions(preview.width(), preview.height())\n\t\t&& !hasModifications) {\n\t\treturn nullptr;\n\t}\n\tconst auto result = CreateChild<SingleMediaPreview>(\n\t\tparent,\n\t\tst,\n\t\tstd::move(gifPaused),\n\t\tpreview,\n\t\tanimated,\n\t\tCore::IsMimeSticker(file.information->filemime),\n\t\tfile.spoiler,\n\t\tanimationPreview ? file.path : QString(),\n\t\ttype);\n\tresult->setCanShowHighQualityBadge(file.canUseHighQualityPhoto());\n\treturn result;\n}\n\nSingleMediaPreview::SingleMediaPreview(\n\tQWidget *parent,\n\tconst style::ComposeControls &st,\n\tFn<bool()> gifPaused,\n\tQImage preview,\n\tbool animated,\n\tbool sticker,\n\tbool spoiler,\n\tconst QString &animatedPreviewPath,\n\tAttachControls::Type type)\n: AbstractSingleMediaPreview(parent, st, type)\n, _gifPaused(std::move(gifPaused))\n, _sticker(sticker) {\n\tExpects(!preview.isNull());\n\tsetAnimated(animated);\n\n\tpreparePreview(preview);\n\tprepareAnimatedPreview(animatedPreviewPath, animated);\n\tsetSpoiler(spoiler);\n}\n\nbool SingleMediaPreview::supportsSpoilers() const {\n\treturn !_sticker || sendWay().sendImagesAsPhotos();\n}\n\nbool SingleMediaPreview::drawBackground() const {\n\treturn !_sticker;\n}\n\nbool SingleMediaPreview::tryPaintAnimation(QPainter &p) {\n\tif (_gifPreview && _gifPreview->started()) {\n\t\tconst auto paused = _gifPaused();\n\t\tconst auto frame = _gifPreview->current({\n\t\t\t.frame = QSize(previewWidth(), previewHeight()),\n\t\t}, paused ? 0 : crl::now());\n\t\tp.drawImage(previewLeft(), previewTop(), frame);\n\t\treturn true;\n\t} else if (_lottiePreview && _lottiePreview->ready()) {\n\t\tconst auto frame = _lottiePreview->frame();\n\t\tconst auto size = frame.size() / style::DevicePixelRatio();\n\t\tp.drawImage(\n\t\t\tQRect(\n\t\t\t\tpreviewLeft() + (previewWidth() - size.width()) / 2,\n\t\t\t\t(previewHeight() - size.height()) / 2,\n\t\t\t\tsize.width(),\n\t\t\t\tsize.height()),\n\t\t\tframe);\n\t\t_lottiePreview->markFrameShown();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nbool SingleMediaPreview::isAnimatedPreviewReady() const {\n\treturn _gifPreview || _lottiePreview;\n}\n\nvoid SingleMediaPreview::prepareAnimatedPreview(\n\t\tconst QString &animatedPreviewPath,\n\t\tbool animated) {\n\tif (_sticker && animated) {\n\t\tconst auto box = QSize(previewWidth(), previewHeight())\n\t\t\t* style::DevicePixelRatio();\n\t\t_lottiePreview = std::make_unique<Lottie::SinglePlayer>(\n\t\t\tLottie::ReadContent(QByteArray(), animatedPreviewPath),\n\t\t\tLottie::FrameRequest{ box });\n\t\t_lottiePreview->updates(\n\t\t) | rpl::on_next([=] {\n\t\t\tupdate();\n\t\t}, lifetime());\n\t} else if (!animatedPreviewPath.isEmpty()) {\n\t\tauto callback = [=](Media::Clip::Notification notification) {\n\t\t\tclipCallback(notification);\n\t\t};\n\t\t_gifPreview = Media::Clip::MakeReader(\n\t\t\tanimatedPreviewPath,\n\t\t\tstd::move(callback));\n\t}\n}\n\nvoid SingleMediaPreview::clipCallback(\n\t\tMedia::Clip::Notification notification) {\n\tusing namespace Media::Clip;\n\tswitch (notification) {\n\tcase Notification::Reinit: {\n\t\tif (_gifPreview && _gifPreview->state() == State::Error) {\n\t\t\t_gifPreview.setBad();\n\t\t}\n\n\t\tif (_gifPreview && _gifPreview->ready() && !_gifPreview->started()) {\n\t\t\t_gifPreview->start({\n\t\t\t\t.frame = QSize(previewWidth(), previewHeight()),\n\t\t\t});\n\t\t}\n\n\t\tupdate();\n\t} break;\n\n\tcase Notification::Repaint: {\n\t\tif (_gifPreview && !_gifPreview->currentDisplayed()) {\n\t\t\tupdate();\n\t\t}\n\t} break;\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/chat/attach/attach_abstract_single_media_preview.h\"\n#include \"media/clip/media_clip_reader.h\"\n\nnamespace Lottie {\nclass SinglePlayer;\n} // namespace Lottie\n\nnamespace Ui {\n\nstruct PreparedFile;\n\nclass SingleMediaPreview final : public AbstractSingleMediaPreview {\npublic:\n\tstatic SingleMediaPreview *Create(\n\t\tQWidget *parent,\n\t\tconst style::ComposeControls &st,\n\t\tFn<bool()> gifPaused,\n\t\tconst PreparedFile &file,\n\t\tAttachControls::Type type = AttachControls::Type::Full);\n\n\tSingleMediaPreview(\n\t\tQWidget *parent,\n\t\tconst style::ComposeControls &st,\n\t\tFn<bool()> gifPaused,\n\t\tQImage preview,\n\t\tbool animated,\n\t\tbool sticker,\n\t\tbool spoiler,\n\t\tconst QString &animatedPreviewPath,\n\t\tAttachControls::Type type);\n\nprotected:\n\tbool supportsSpoilers() const override;\n\tbool drawBackground() const override;\n\tbool tryPaintAnimation(QPainter &p) override;\n\tbool isAnimatedPreviewReady() const override;\n\nprivate:\n\tvoid prepareAnimatedPreview(\n\t\tconst QString &animatedPreviewPath,\n\t\tbool animated);\n\tvoid clipCallback(Media::Clip::Notification notification);\n\n\tconst Fn<bool()> _gifPaused;\n\tconst bool _sticker = false;\n\tMedia::Clip::ReaderPointer _gifPreview;\n\tstd::unique_ptr<Lottie::SinglePlayer> _lottiePreview;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/chat.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\nusing \"dialogs/dialogs.style\";\nusing \"ui/widgets/widgets.style\";\nusing \"ui/menu_icons.style\";\nusing \"chat_helpers/chat_helpers.style\"; // GroupCallUserpics\n\nmsgMaxWidth: 430px;\nmsgFont: font(fsize);\nmsgNameFont: semiboldFont;\nmsgNameStyle: semiboldTextStyle;\nmsgServiceFont: semiboldFont;\nmsgServiceNameFont: semiboldFont;\nmsgServicePhotoWidth: 100px;\nmsgDateFont: font(13px);\nmsgMinWidth: 160px;\nmsgPhotoSize: 33px;\nmsgPhotoSkip: 40px;\nmsgPadding: margins(11px, 8px, 11px, 8px);\nmsgTagBadgePadding: margins(5px, -1px, 5px, 0px);\nmsgTagBadgeBoostSkip: 2px;\n\ntagInfoIcon: icon {{ \"chat/large_user_tag-48x48\", windowFgActive }};\ntagInfoIconPadding: margins(8px, 8px, 8px, 8px);\ntagInfoPreviewHeight: 80px;\ntagInfoPreviewGap: 8px;\ntagInfoPreviewBubbleRadius: 8px;\ntagInfoPreviewLineHeight: 6px;\ntagInfoPreviewLineSpacing: 4px;\ntagInfoPreviewBubblePadding: margins(10px, 8px, 10px, 8px);\ntagInfoPreviewRadius: 8px;\ntagInfoPreviewBubbleRightMargin: 10px;\ntagInfoPreviewFadeWidth: 24px;\ntagInfoPreviewBadgeTop: 6px;\ntagInfoPreviewPadding: margins(0px, 16px, 0px, 0px);\ntagInfoAdminsOnlyPadding: margins(0px, 8px, 0px, 0px);\ntagInfoAdminsOnlyLabel: FlatLabel(defaultFlatLabel) {\n\talign: align(top);\n\tminWidth: 40px;\n\ttextFg: windowSubTextFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px);\n\t}\n}\n\nmsgMargin: margins(16px, 6px, 56px, 2px);\nmsgMarginTopAttached: 0px;\nmsgShadow: 2px;\nmsgSelectionOffset: 30px;\nmsgSelectionBottomSkip: 5px;\n\nitemRevealDuration: 150;\ntextRevealGradient: 24px;\nmiddleClickAutoscrollSpeedScale: 30px;\nmiddleClickAutoscrollMaxSpeed: 7200px;\n\nhistoryReplyTop: 2px;\nhistoryReplyBottom: 2px;\nhistoryReplyPreview: 32px;\nhistoryReplyPreviewMargin: margins(7px, 4px, 4px, 4px);\nhistoryReplyPadding: margins(11px, 2px, 6px, 2px);\nhistoryReplyUser: IconEmoji {\n\ticon: icon {{ \"chat/reply_type_user\", windowFg }};\n\tpadding: margins(0px, 4px, 4px, 0px);\n}\nhistoryReplyGroup: IconEmoji {\n\ticon: icon {{ \"chat/reply_type_group\", windowFg }};\n\tpadding: margins(0px, 4px, 4px, 0px);\n}\nhistoryReplyChannel: IconEmoji {\n\ticon: icon {{ \"chat/reply_type_channel\", windowFg }};\n\tpadding: margins(0px, 5px, 4px, 0px);\n}\nhistoryReplyForward: IconEmoji {\n\ticon: icon {{ \"mini_forward\", windowFg }};\n\tpadding: margins(0px, 2px, 2px, 0px);\n}\n\nmsgReplyPadding: margins(6px, 6px, 11px, 6px);\nmsgReplyBarPos: point(1px, 0px);\nmsgReplyBarSize: size(2px, 36px);\nmsgReplyBarSkip: 10px;\nmsgServicePadding: margins(12px, 3px, 12px, 4px);\nmsgServiceMargin: margins(10px, 10px, 10px, 2px);\nmonoforumBarUserpicSkip: 2px;\n\nmsgDateSpace: 12px;\nmsgDateDelta: point(2px, 5px);\n\nmsgDateImgDelta: 4px;\nmsgDateImgPadding: point(8px, 2px);\n\nmessageQuoteStyle: historyQuoteStyle;\nmessageTextStyle: historyTextStyle;\nhistoryPagePreview: QuoteStyle(messageQuoteStyle) {\n\tpadding: margins(10px, 5px, 7px, 7px);\n}\nmsgDateTextStyle: defaultTextStyle;\nserviceTextPalette: TextPalette(defaultTextPalette) {\n\tlinkFg: msgServiceFg;\n\tmonoFg: msgServiceFg;\n\tspoilerFg: msgServiceFg;\n\tselectBg: msgServiceBgSelected;\n\tselectFg: msgServiceFg;\n\tselectLinkFg: msgServiceFg;\n\tselectMonoFg: msgServiceFg;\n\tselectSpoilerFg: msgServiceFg;\n\tselectOverlay: msgServiceBgSelected;\n}\nserviceTextStyle: TextStyle(defaultTextStyle) {\n\tfont: msgServiceFont;\n}\ninTextPalette: TextPalette(defaultTextPalette) {\n\tlinkFg: historyLinkInFg;\n\tmonoFg: msgInMonoFg;\n\tspoilerFg: msgInDateFg;\n\tselectBg: msgInBgSelected;\n\tselectFg: historyTextInFgSelected;\n\tselectLinkFg: historyLinkInFgSelected;\n\tselectMonoFg: msgInMonoFgSelected;\n\tselectSpoilerFg: msgInDateFgSelected;\n\tselectOverlay: msgSelectOverlay;\n}\ninTextPaletteSelected: TextPalette(inTextPalette) {\n\tlinkFg: historyLinkInFgSelected;\n\tmonoFg: msgInMonoFgSelected;\n\tspoilerFg: msgInDateFgSelected;\n}\noutTextPalette: TextPalette(defaultTextPalette) {\n\tlinkFg: historyLinkOutFg;\n\tmonoFg: msgOutMonoFg;\n\tspoilerFg: msgOutDateFg;\n\tselectBg: msgOutBgSelected;\n\tselectFg: historyTextOutFgSelected;\n\tselectLinkFg: historyLinkOutFgSelected;\n\tselectMonoFg: msgOutMonoFgSelected;\n\tselectSpoilerFg: msgOutDateFgSelected;\n\tselectOverlay: msgSelectOverlay;\n}\noutTextPaletteSelected: TextPalette(outTextPalette) {\n\tlinkFg: historyLinkOutFgSelected;\n\tmonoFg: msgOutMonoFgSelected;\n\tspoilerFg: msgOutDateFgSelected;\n}\npriceTagTextPalette: TextPalette(defaultTextPalette) {\n\tlinkFg: creditsBg1;\n}\nfwdTextUserpicPadding: margins(0px, 1px, 3px, 0px);\nnameRippleRadius: 4px;\nnameRipplePadding: margins(3px, 1px, 3px, 1px);\nlinkRippleRadius: 3px;\nlinkRipplePadding: margins(3px, 0px, 3px, 0px);\nfwdTextStyle: TextStyle(semiboldTextStyle) {\n\tlinkUnderline: kLinkUnderlineNever;\n}\ninFwdTextPalette: TextPalette(defaultTextPalette) {\n\tlinkFg: msgInServiceFg;\n}\noutFwdTextPalette: TextPalette(defaultTextPalette) {\n\tlinkFg: msgOutServiceFg;\n}\ninFwdTextPaletteSelected: TextPalette(defaultTextPalette) {\n\tlinkFg: msgInServiceFgSelected;\n}\noutFwdTextPaletteSelected: TextPalette(defaultTextPalette) {\n\tlinkFg: msgOutServiceFgSelected;\n}\ninReplyTextPalette: TextPalette(inTextPalette) {\n\tlinkFg: msgInDateFg;\n}\ninReplyTextPaletteSelected: TextPalette(inTextPaletteSelected) {\n\tlinkFg: msgInDateFgSelected;\n}\noutReplyTextPalette: TextPalette(outTextPalette) {\n\tlinkFg: msgOutDateFg;\n}\noutReplyTextPaletteSelected: TextPalette(outTextPaletteSelected) {\n\tlinkFg: msgOutDateFgSelected;\n}\nimgReplyTextPalette: TextPalette(defaultTextPalette) {\n\tlinkFg: msgImgReplyBarColor;\n\tmonoFg: msgImgReplyBarColor;\n}\ninSemiboldPalette: TextPalette(inTextPalette) {\n\tlinkFg: msgInServiceFg;\n\tselectFg: msgInServiceFgSelected;\n\tselectLinkFg: msgInServiceFgSelected;\n}\noutSemiboldPalette: TextPalette(outTextPalette) {\n\tlinkFg: msgOutServiceFg;\n\tselectFg: msgOutServiceFgSelected;\n\tselectLinkFg: msgOutServiceFgSelected;\n}\n\nmediaCaptionSkip: 5px;\nmediaInBubbleSkip: 5px;\nmediaUnreadSize: 7px;\nmediaUnreadSkip: 5px;\nmediaUnreadTop: 6px;\n\nmediaInPalette: TextPalette(defaultTextPalette) {\n\tlinkFg: mediaInFg;\n}\nmediaInPaletteSelected: TextPalette(defaultTextPalette) {\n\tlinkFg: mediaInFgSelected;\n}\n\nminPhotoSize: 100px;\nmaxMediaSize: 430px;\nmaxStickerSize: 224px;\nmaxAnimatedEmojiSize: 112px;\nmaxGifSize: 320px;\nmaxVideoMessageSize: 240px;\nmaxSignatureSize: 144px;\nmaxWallPaperWidth: 160px;\nmaxWallPaperHeight: 240px;\nhistoryThemeSize: size(272px, 176px);\n\nextendedPreviewButtonPadding: margins(20px, 10px, 20px, 10px);\nextendedPreviewButtonMargin: 20px;\n\nhistoryMinimalWidth: 380px;\n\nreactionMenu: PopupMenu(defaultPopupMenu) {\n\tmenu: Menu(defaultMenu) {\n\t\twidthMin: 30px;\n\t}\n}\n\nhistoryScroll: ScrollArea(defaultScrollArea) {\n\tbg: historyScrollBg;\n\tbgOver: historyScrollBgOver;\n\tbarBg: historyScrollBarBg;\n\tbarBgOver: historyScrollBarBgOver;\n\n\tround: 3px;\n\n\twidth: 12px;\n\tdeltax: 3px;\n\tdeltat: 3px;\n\tdeltab: 3px;\n\n\tbottomsh: -1px;\n}\n\nhistoryResizeWidth: 6px;\n\nhistoryPaddingBottom: 8px;\n\nhistoryPremiumToast: Toast(defaultToast) {\n\tminWidth: msgMinWidth;\n\tmaxWidth: 380px;\n\tpadding: margins(19px, 13px, 19px, 12px);\n}\nhistoryPremiumViewSet: RoundButton(defaultActiveButton) {\n\twidth: -24px;\n\theight: 44px;\n\ttextTop: 13px;\n\ttextFg: mediaviewTextLinkFg;\n\ttextFgOver: mediaviewTextLinkFg;\n\ttextBg: transparent;\n\ttextBgOver: transparent;\n\tripple: emptyRippleAnimation;\n}\n\nmembersInnerWidth: 310px;\nmembersInnerHeightMax: 360px;\nmembersInnerDropdown: InnerDropdown(defaultInnerDropdown) {\n\tscroll: ScrollArea(defaultSolidScroll) {\n\t\tdeltat: 0px;\n\t\tdeltab: 0px;\n\t\tround: 1px;\n\t\twidth: 8px;\n\t\tdeltax: 3px;\n\t}\n\tscrollMargin: margins(0px, 5px, 0px, 5px);\n\tscrollPadding: margins(0px, 3px, 0px, 3px);\n}\nmembersInnerItem: defaultPeerListItem;\n\nhistoryFileOutImage: icon {{ \"history_file_image\", historyFileOutIconFg }};\nhistoryFileOutImageSelected: icon {{ \"history_file_image\", historyFileOutIconFgSelected }};\nhistoryFileInImage: icon {{ \"history_file_image\", historyFileInIconFg }};\nhistoryFileInImageSelected: icon {{ \"history_file_image\", historyFileInIconFgSelected }};\nhistoryFileOutDocument: icon {{ \"history_file_document\", historyFileOutIconFg }};\nhistoryFileOutDocumentSelected: icon {{ \"history_file_document\", historyFileOutIconFgSelected }};\nhistoryFileInDocument: icon {{ \"history_file_document\", historyFileInIconFg }};\nhistoryFileInDocumentSelected: icon {{ \"history_file_document\", historyFileInIconFgSelected }};\nhistoryFileOutDownload: icon {{ \"history_file_download\", historyFileOutIconFg }};\nhistoryFileOutDownloadSelected: icon {{ \"history_file_download\", historyFileOutIconFgSelected }};\nhistoryFileInDownload: icon {{ \"history_file_download\", historyFileInIconFg }};\nhistoryFileInDownloadSelected: icon {{ \"history_file_download\", historyFileInIconFgSelected }};\nhistoryFileOutCancel: icon {{ \"history_file_cancel\", historyFileOutIconFg }};\nhistoryFileOutCancelSelected: icon {{ \"history_file_cancel\", historyFileOutIconFgSelected }};\nhistoryFileInCancel: icon {{ \"history_file_cancel\", historyFileInIconFg }};\nhistoryFileInCancelSelected: icon {{ \"history_file_cancel\", historyFileInIconFgSelected }};\nhistoryFileOutPause: icon {{ \"history_file_pause\", historyFileOutIconFg }};\nhistoryFileOutPauseSelected: icon {{ \"history_file_pause\", historyFileOutIconFgSelected }};\nhistoryFileInPause: icon {{ \"history_file_pause\", historyFileInIconFg }};\nhistoryFileInPauseSelected: icon {{ \"history_file_pause\", historyFileInIconFgSelected }};\nhistoryFileOutPlay: icon {{ \"history_file_play\", historyFileOutIconFg }};\nhistoryFileOutPlaySelected: icon {{ \"history_file_play\", historyFileOutIconFgSelected }};\nhistoryFileInPlay: icon {{ \"history_file_play\", historyFileInIconFg }};\nhistoryFileInPlaySelected: icon {{ \"history_file_play\", historyFileInIconFgSelected }};\nhistoryFileOutWaiting: icon {{ \"mediaview/save_check\", historyFileOutIconFg }};\nhistoryFileOutWaitingSelected: icon {{ \"mediaview/save_check\", historyFileOutIconFgSelected }};\nhistoryFileInWaiting: icon {{ \"mediaview/save_check\", historyFileInIconFg }};\nhistoryFileInWaitingSelected: icon {{ \"mediaview/save_check\", historyFileInIconFgSelected }};\n\nhistoryFileThumbPause: icon {{ \"history_file_pause\", historyFileThumbIconFg }};\nhistoryFileThumbPauseSelected: icon {{ \"history_file_pause\", historyFileThumbIconFgSelected }};\nhistoryFileThumbDownload: icon {{ \"history_file_download\", historyFileThumbIconFg }};\nhistoryFileThumbDownloadSelected: icon {{ \"history_file_download\", historyFileThumbIconFgSelected }};\nhistoryFileThumbCancel: icon {{ \"history_file_cancel\", historyFileThumbIconFg }};\nhistoryFileThumbCancelSelected: icon {{ \"history_file_cancel\", historyFileThumbIconFgSelected }};\nhistoryFileThumbPlay: icon {{ \"history_file_play\", historyFileThumbIconFg }};\nhistoryFileThumbPlaySelected: icon {{ \"history_file_play\", historyFileThumbIconFgSelected }};\nhistoryFileThumbWaiting: icon {{ \"mediaview/save_check\", historyFileThumbIconFg }};\nhistoryFileThumbWaitingSelected: icon {{ \"mediaview/save_check\", historyFileThumbIconFgSelected }};\n\nhistorySendStateSpace: 24px;\nhistorySendStatePosition: point(-17px, -19px);\nhistorySentIcon: icon {{ \"history_sent\", historyOutIconFg, point(2px, 4px) }};\nhistorySentSelectedIcon: icon {{ \"history_sent\", historyOutIconFgSelected, point(2px, 4px) }};\nhistorySentInvertedIcon: icon {{ \"history_sent\", historyIconFgInverted, point(2px, 4px) }};\nhistoryReceivedIcon: icon {{ \"history_received\", historyOutIconFg, point(2px, 4px) }};\nhistoryReceivedSelectedIcon: icon {{ \"history_received\", historyOutIconFgSelected, point(2px, 4px) }};\nhistoryReceivedInvertedIcon: icon {{ \"history_received\", historyIconFgInverted, point(2px, 4px) }};\nhistoryShortcutStateSpace: 18px;\n\nhistoryViewsSpace: 8px;\nhistoryViewsWidth: 20px;\nhistoryPinWidth: 24px;\nhistoryViewsTop: -15px;\nhistoryViewsInIcon: icon {{ \"history_views\", msgInDateFg }};\nhistoryViewsInSelectedIcon: icon {{ \"history_views\", msgInDateFgSelected }};\nhistoryViewsOutIcon: icon {{ \"history_views\", historyOutIconFg }};\nhistoryViewsOutSelectedIcon: icon {{ \"history_views\", historyOutIconFgSelected }};\nhistoryViewsInvertedIcon: icon {{ \"history_views\", historySendingInvertedIconFg }};\nhistoryRepliesInIcon: icon {{ \"history_replies\", msgInDateFg }};\nhistoryRepliesInSelectedIcon: icon {{ \"history_replies\", msgInDateFgSelected }};\nhistoryRepliesOutIcon: icon {{ \"history_replies\", historyOutIconFg }};\nhistoryRepliesOutSelectedIcon: icon {{ \"history_replies\", historyOutIconFgSelected }};\nhistoryRepliesInvertedIcon: icon {{ \"history_replies\", historySendingInvertedIconFg }};\nhistoryPinTop: -18px;\nhistoryPinInIcon: icon {{ \"history_pin\", msgInDateFg }};\nhistoryPinInSelectedIcon: icon {{ \"history_pin\", msgInDateFgSelected }};\nhistoryPinOutIcon: icon {{ \"history_pin\", historyOutIconFg }};\nhistoryPinOutSelectedIcon: icon {{ \"history_pin\", historyOutIconFgSelected }};\nhistoryPinInvertedIcon: icon {{ \"history_pin\", historySendingInvertedIconFg }};\n\nhistoryBotMenuSkip: 8px;\nhistoryBotMenuMaxWidth: 160px;\nhistoryBotMenuButton: RoundButton(defaultActiveButton) {\n\twidth: -24px;\n\theight: 30px;\n\ttextTop: 6px;\n}\n\ntopicButtonSkip: 3px;\ntopicButtonPadding: margins(6px, 3px, 8px, 3px);\ntopicButtonArrowSkip: 8px;\ntopicButtonArrowPosition: point(3px, 3px);\ntopicButtonArrow: icon{{ \"dialogs/dialogs_topic_arrow\", historyReplyIconFg }};\n\ntextMoreIconEmoji: IconEmoji {\n\ticon: topicButtonArrow;\n\tpadding: margins(-2px, 5px, 0px, 0px);\n}\ntextBackIconEmoji: IconEmoji {\n\ticon: icon{{ \"dialogs/dialogs_topic_arrow-flip_horizontal\", historyReplyIconFg }};\n\tpadding: margins(0px, 5px, -2px, 0px);\n}\n\nmsgBotKbIconPadding: 4px;\nmsgBotKbUrlIcon: icon {{ \"inline_button_url\", msgBotKbIconFg }};\nmsgBotKbSwitchPmIcon: icon {{ \"inline_button_switch\", msgBotKbIconFg }};\nmsgBotKbPaymentIcon: icon {{ \"inline_button_card\", msgBotKbIconFg }};\nmsgBotKbWebviewIcon: icon {{ \"inline_button_web\", msgBotKbIconFg }};\nmsgBotKbCopyIcon: icon {{ \"inline_button_copy\", msgBotKbIconFg }};\nmsgBotKbButton: BotKeyboardButton {\n\tmargin: 2px;\n\tpadding: 10px;\n\theight: 36px;\n\ttextTop: 8px;\n\tripple: defaultRippleAnimation;\n}\n\nbotDescSkip: 8px;\n\nbotKbDuration: 200;\nbotKbStyle: TextStyle(defaultTextStyle) {\n\tfont: font(15px semibold);\n}\nbotKbButton: BotKeyboardButton {\n\tmargin: 10px;\n\tpadding: 10px;\n\theight: 38px;\n\ttextTop: 9px;\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: botKbDownBg;\n\t}\n}\nbotKbTinyButton: BotKeyboardButton {\n\tmargin: 4px;\n\tpadding: 3px;\n\theight: 25px;\n\ttextTop: 2px;\n\tripple: defaultRippleAnimation;\n}\nbotKbScroll: defaultSolidScroll;\n\nhistoryScrollDateHideTimeout: 1000;\nhistoryDateFadeDuration: 200;\nhistoryDiceToast: Toast(defaultToast) {\n\tminWidth: msgMinWidth;\n\tmaxWidth: 640px;\n\tdurationFadeOut: 200;\n}\nhistoryInfoToast: Toast(defaultToast) {\n\tminWidth: msgMinWidth;\n\tmaxWidth: 380px;\n\tpadding: margins(13px, 13px, 19px, 12px);\n}\nhistoryInfoToastIcon: icon {{ \"toast/info\", toastFg }};\n\nbubbleRadiusSmall: roundRadiusLarge;\nbubbleRadiusLarge: 16px;\n\nhistoryPhotoLeft: 14px;\nhistoryPhotoBubbleMinWidth: 200px;\nhistoryBubbleTailInLeft: icon {{ \"bubble_tail\", msgInBg }};\nhistoryBubbleTailInLeftSelected: icon {{ \"bubble_tail\", msgInBgSelected }};\nhistoryBubbleTailOutLeft: icon {{ \"bubble_tail\", msgOutBg }};\nhistoryBubbleTailOutLeftSelected: icon {{ \"bubble_tail\", msgOutBgSelected }};\nhistoryBubbleTailInRight: icon {{ \"bubble_tail-flip_horizontal\", msgInBg }};\nhistoryBubbleTailInRightSelected: icon {{ \"bubble_tail-flip_horizontal\", msgInBgSelected }};\nhistoryBubbleTailOutRight: icon {{ \"bubble_tail-flip_horizontal\", msgOutBg }};\nhistoryBubbleTailOutRightSelected: icon {{ \"bubble_tail-flip_horizontal\", msgOutBgSelected }};\n\nhistoryPeerUserpicFont: semiboldFont;\n\nhistoryPsaIconIn: icon {{ \"message_psa_tooltip\", msgFileThumbLinkInFg }};\nhistoryPsaIconInSelected: icon {{ \"message_psa_tooltip\", msgFileThumbLinkInFgSelected }};\nhistoryPsaIconOut: icon {{ \"message_psa_tooltip\", msgFileThumbLinkOutFg }};\nhistoryPsaIconOutSelected: icon {{ \"message_psa_tooltip\", msgFileThumbLinkOutFgSelected }};\nhistoryPsaIconSkip1: 23px;\nhistoryPsaIconSkip2: 23px;\nhistoryPsaIconPosition1: point(-5px, 0px);\nhistoryPsaIconPosition2: point(-5px, 0px);\n\nhistoryStatusFg: windowSubTextFg;\nhistoryStatusFgActive: windowActiveTextFg;\nhistoryStatusFgTyping: historyStatusFgActive;\n\nhistoryUnreadBarHeight: 32px;\nhistoryUnreadBarMargin: 8px;\nhistoryUnreadBarFont: semiboldFont;\n\nhistoryForwardChooseMargins: margins(30px, 10px, 30px, 10px);\nhistoryForwardChooseFont: font(16px);\n\nhistoryCallArrowIn: icon {{ \"calls/call_arrow_in\", historyCallArrowInFg }};\nhistoryCallArrowInSelected: icon {{ \"calls/call_arrow_in\", historyCallArrowInFgSelected }};\nhistoryCallArrowMissedIn: icon {{ \"calls/call_arrow_in\", historyCallArrowMissedInFg }};\nhistoryCallArrowMissedInSelected: icon {{ \"calls/call_arrow_in\", historyCallArrowMissedInFgSelected }};\nhistoryCallArrowOut: icon {{ \"calls/call_arrow_out\", historyCallArrowOutFg }};\nhistoryCallArrowOutSelected: icon {{ \"calls/call_arrow_out\", historyCallArrowOutFgSelected }};\nhistoryCallWidth: 240px;\nhistoryCallHeight: 56px;\nhistoryCallInIcon: icon {{ \"calls/call_answer\", msgFileInBg }};\nhistoryCallInIconSelected: icon {{ \"calls/call_answer\", msgFileInBgSelected }};\nhistoryCallOutIcon: icon {{ \"calls/call_answer\", msgFileOutBg }};\nhistoryCallOutIconSelected: icon {{ \"calls/call_answer\", msgFileOutBgSelected }};\nhistoryCallCameraInIcon: icon {{ \"calls/call_camera_active\", msgFileInBg }};\nhistoryCallCameraInIconSelected: icon {{ \"calls/call_camera_active\", msgFileInBgSelected }};\nhistoryCallCameraOutIcon: icon {{ \"calls/call_camera_active\", msgFileOutBg }};\nhistoryCallCameraOutIconSelected: icon {{ \"calls/call_camera_active\", msgFileOutBgSelected }};\nhistoryCallGroupInIcon: icon {{ \"calls/call_group\", msgFileInBg }};\nhistoryCallGroupInIconSelected: icon {{ \"calls/call_group\", msgFileInBgSelected }};\nhistoryCallGroupOutIcon: icon {{ \"calls/call_group\", msgFileOutBg }};\nhistoryCallGroupOutIconSelected: icon {{ \"calls/call_group\", msgFileOutBgSelected }};\nhistoryCallIconPosition: point(12px, 10px);\nhistoryCallLeft: 16px;\nhistoryCallTop: 9px;\nhistoryCallStatusTop: 29px;\nhistoryCallStatusSkip: 4px;\nhistoryCallArrowPosition: point(-1px, 1px);\n\nHistoryFileLayout {\n\tpadding: margins;\n\tnameTop: pixels;\n\tstatusTop: pixels;\n\tlinkTop: pixels;\n\tthumbSize: pixels;\n\tthumbSkip: pixels;\n}\n\nmsgFileThumbRadiusSmall: 4px;\nmsgFileThumbRadiusLarge: 12px;\nmsgFileLayout: HistoryFileLayout {\n\tpadding: margins(12px, 8px, 10px, 8px);\n\tnameTop: 12px;\n\tstatusTop: 34px;\n\tthumbSize: 44px;\n\tthumbSkip: 11px;\n}\nmsgFileThumbLayout: HistoryFileLayout {\n\tpadding: margins(6px, 6px, 10px, 6px);\n\tnameTop: 11px;\n\tstatusTop: 31px;\n\tlinkTop: 55px;\n\tthumbSize: 72px;\n\tthumbSkip: 14px;\n}\nmsgFileLayoutGrouped: HistoryFileLayout(msgFileLayout) {\n\tpadding: margins(12px, 5px, 10px, 5px);\n\tnameTop: 9px;\n\tstatusTop: 31px;\n}\nmsgFileThumbLayoutGrouped: HistoryFileLayout(msgFileThumbLayout) {\n\tpadding: margins(6px, 3px, 10px, 3px);\n\tnameTop: 8px;\n\tstatusTop: 28px;\n\tlinkTop: 52px;\n}\nattachPreviewLayout: HistoryFileLayout {\n\tpadding: margins(0px, 0px, 0px, 0px);\n\tnameTop: 6px;\n\tstatusTop: 27px;\n\tthumbSize: 44px;\n\tthumbSkip: 11px;\n}\nattachPreviewThumbLayout: HistoryFileLayout {\n\tpadding: margins(0px, 0px, 0px, 0px);\n\tnameTop: 7px;\n\tstatusTop: 37px;\n\tthumbSize: 64px;\n\tthumbSkip: 10px;\n}\nattachPreviewCaptionTopOffset: 6px;\nattachPreviewCaptionRepaintMargin: margins(0px, 2px, 0px, 2px);\n\nmsgFileMinWidth: 268px;\nmsgFileTopMinus: 6px;\n\nmsgFileOverDuration: 200;\nmsgFileRadialLine: 3px;\n\nmsgWaveformBar: 2px;\nmsgWaveformSkip: 1px;\nmsgWaveformMin: 3px;\nmsgWaveformMax: 17px;\n\nhistoryVoiceMessageInTTL: icon {{ \"chat/mini_media_once\", historyFileInIconFg }};\nhistoryVoiceMessageInTTLSelected: icon {{ \"chat/mini_media_once\", historyFileInIconFgSelected }};\nhistoryVoiceMessageOutTTL: icon {{ \"chat/mini_media_once\", historyFileOutIconFg }};\nhistoryVoiceMessageOutTTLSelected: icon {{ \"chat/mini_media_once\", historyFileOutIconFgSelected }};\n\nhistoryTranscribeSkip: 10px;\nhistoryTranscribeSize: size(28px, 22px);\nhistoryTranscribeRadius: 4px;\nhistoryTranscribeLoadingSize: 24px;\nhistoryTranscribeInIcon: icon {{ \"chat/voice_to_text\", msgFileInBg }};\nhistoryTranscribeInIconSelected: icon {{ \"chat/voice_to_text\", msgFileInBgSelected }};\nhistoryTranscribeOutIcon: icon {{ \"chat/voice_to_text\", msgFileOutBg }};\nhistoryTranscribeOutIconSelected: icon {{ \"chat/voice_to_text\", msgFileOutBgSelected }};\nhistoryTranscribeInHide: icon {{ \"chat/voice_to_text_collapse\", msgFileInBg }};\nhistoryTranscribeInHideSelected: icon {{ \"chat/voice_to_text_collapse\", msgFileInBgSelected }};\nhistoryTranscribeOutHide: icon {{ \"chat/voice_to_text_collapse\", msgFileOutBg }};\nhistoryTranscribeOutHideSelected: icon {{ \"chat/voice_to_text_collapse\", msgFileOutBgSelected }};\nhistoryTranscribeInLock: icon {{ \"chat/mini_lock\", msgFileInBg }};\nhistoryTranscribeInLockSelected: icon {{ \"chat/mini_lock\", msgFileInBgSelected }};\nhistoryTranscribeOutLock: icon {{ \"chat/mini_lock\", msgFileOutBg }};\nhistoryTranscribeOutLockSelected: icon {{ \"chat/mini_lock\", msgFileOutBgSelected }};\nhistoryTranscribeLockPos: point(17px, 9px);\nhistoryTranscribeLockOverlayPos: point(19px, 11px);\nhistoryTranscribeLockOverlaySize: size(5px, 10px);\nhistoryTranscribeRadialAnimation: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {\n\tlinearPeriod: 700;\n\tsinePeriod: 2400;\n\tsineDuration: 700;\n\tsineShift: 1100;\n\tarcMin: 0.0625;\n\tarcMax: 0.35;\n}\n\nhistoryVideoMessageMute: icon {{ \"volume_mute\", historyFileThumbIconFg }};\nhistoryVideoMessageMuteSelected: icon {{ \"volume_mute\", historyFileThumbIconFgSelected }};\nhistoryVideoMessageMuteSize: 25px;\nhistoryVideoMessageProgressOpacity: 0.72;\nhistoryVideoMessageTtlIcon: icon {{ \"chat/audio_once\", historyFileThumbIconFg }};\nhistoryVideoMessageTtlIconSelected: icon {{ \"chat/audio_once\", historyFileThumbIconFgSelected }};\n\nhistoryAdminLogEmptyWidth: 260px;\nhistoryAdminLogEmptyPadding: margins(10px, 12px, 10px, 12px);\nhistoryAdminLogCancelSearch: CrossButton {\n\twidth: 40px;\n\theight: 54px;\n\n\tcross: CrossAnimation {\n\t\tsize: 32px;\n\t\tskip: 10px;\n\t\tstroke: 1.5;\n\t\tminScale: 0.3;\n\t}\n\tcrossFg: menuIconFg;\n\tcrossFgOver: menuIconFgOver;\n\tcrossPosition: point(6px, 11px);\n\n\tduration: 150;\n\tloadingPeriod: 1000;\n\tripple: defaultRippleAnimationBgOver;\n}\nhistoryAdminLogSearchTop: 11px;\nhistoryAdminLogSearchSlideDuration: 150;\nhistoryAdminLogTopBarLeft: 17px;\nhistoryAdminLogTopBarUserpicSkip: 35px;\nhistoryAdminLogWhatIsThis: IconButton(historyAttach) {\n\ticon: icon {{ \"menu/faq\", windowActiveTextFg }};\n\ticonOver: icon {{ \"menu/faq\", windowActiveTextFg }};\n}\n\nhistoryFastShareSize: topPeersSelectorUserpicSize;\nhistoryFastShareLeft: 13px;\nhistoryFastShareBottom: 5px;\nhistoryFastShareIcon: icon {{ \"fast_share\", msgServiceFg }};\nhistoryGoToOriginalIcon: icon {{ \"filled_go_to_message\", msgServiceFg, point(0px, 0px) }};\nhistoryFastCommentsIcon: icon {{ \"fast_comments\", msgServiceFg }};\nhistoryFastCloseSize: 30px;\nhistoryFastCloseIcon: icon {{ \"box_button_close\", msgServiceFg }};\nhistoryFastMoreIcon: icon {{ \"title_menu_dots\", msgServiceFg, point(0px, -2px) }};\nhistoryFastTranscribeIcon: icon {{ \"chat/voice_to_text\", msgServiceFg }};\nhistoryFastTranscribeLock: icon {{ \"chat/mini_lock\", msgServiceFg }};\nhistoryFastTranscribeLockPos: point(18px, 13px);\nhistoryFastSummaryLockPos: point(13px, 13px);\nhistoryFastTranscribeLockOverlayPos: point(21px, 13px);\nhistoryFastTranscribeLockOverlaySize: size(6px, 10px);\n\nhistorySwipeIconSkip: 80px;\n\nhistorySavedFont: font(semibold 14px);\n\nhistoryGroupWidthMax: maxMediaSize;\nhistoryGroupWidthMin: minPhotoSize;\nhistoryGroupSkip: 4px;\nhistoryGroupRadialSize: 44px;\nhistoryGroupRadialLine: 3px;\n\nhistoryMapPoint: icon {{ \"map_point\", mapPointDrop }};\nhistoryMapPointInner: icon {{ \"map_point_inner\", mapPointDot }};\n\nhistoryMapPinRadius: 28px;\nhistoryMapPinUserpicSize: 52px;\nhistoryMapPinTailHeight: 12px;\nhistoryMapPinTailHalfWidth: 12px;\nhistoryMapPinDotRadius: 4px;\nhistoryMapPinDotStroke: 2px;\nhistoryMapPinShadow: BoxShadow {\n\tblurRadius: 6px;\n\toffset: point(0px, 2px);\n\topacity: 0.29;\n}\n\nhistoryPsaForwardPalette: TextPalette(defaultTextPalette) {\n\tlinkFg: boxTextFgGood;\n}\n\nwebPageTitleFont: semiboldFont;\nwebPageTitleStyle: semiboldTextStyle;\nwebPageSponsoredHintFont: font(10px);\nwebPageDescriptionFont: normalFont;\nwebPageDescriptionStyle: defaultTextStyle;\nwebPagePhotoDelta: 8px;\nwebPageAuctionTimerPadding: margins(8px, 4px, 8px, 4px);\nwebPageAuctionTimeFont: font(11px);\nwebPageAuctionSubtextFont: font(12px);\nwebPageAuctionPreviewPadding: margins(10px, 30px, 10px, 5px);\n\nhistoryChecklistTaskPadding: margins(32px, 12px, 0px, 12px);\nhistoryChecklistCheckedTop: 4px;\nhistoryChecklistBottomTop: -4px;\n\nhistoryViewButtonHeight: 48px;\nhistoryViewButtonMargins: margins(10px, 5px, 10px, 10px);\nhistoryViewButtonTextStyle: semiboldTextStyle;\n\nhistoryPageButtonLine: 1px;\nhistoryPageButtonHeight: 36px;\nhistoryPageButtonPadding: margins(13px, 8px, 13px, 8px);\n\nhistoryPageEnlarge: icon{{ \"chat/link_photo_enlarge\", historyFileThumbRadialFg }};\nhistoryPageEnlargeSelected: icon{{ \"chat/link_photo_enlarge\", historyFileThumbRadialFgSelected }};\nhistoryPageEnlargeSize: 36px;\nhistoryPageEnlargeSkip: 4px;\nhistoryPageEnlargeRadius: 8px;\n\nhistoryCommentsButtonHeight: 40px;\nhistoryCommentsSkipLeft: 9px;\nhistoryCommentsSkipText: 10px;\nhistoryCommentsSkipRight: 8px;\nhistoryCommentsUserpics: GroupCallUserpics {\n\tsize: 25px;\n\tshift: 6px;\n\tstroke: 2px;\n\talign: align(left);\n}\n\nhistoryGroupAboutMargin: 16px;\nhistoryGroupAboutPadding: margins(24px, 16px, 24px, 16px);\nhistoryGroupAboutBulletSkip: 16px;\nhistoryGroupAboutHeaderSkip: 10px;\nhistoryGroupAboutTextSkip: 10px;\nhistoryGroupAboutSkip: 8px;\n\nhistoryVideoDownloadSize: 44px;\nhistoryVideoMuteSize: 22px;\nhistoryVideoCancel: icon {{ \"player/playlist_cancel\", historyFileThumbIconFg }};\nhistoryVideoCancelSelected: icon {{ \"player/playlist_cancel\", historyFileThumbIconFgSelected }};\nhistoryVideoDownload: icon {{ \"player/playlist_download\", historyFileThumbIconFg }};\nhistoryVideoDownloadSelected: icon {{ \"player/playlist_download\", historyFileThumbIconFgSelected }};\nhistoryVideoRadialLine: msgFileRadialLine;\nhistoryVideoTimestampProgressLine: 4px;\n\nhistoryAudioDownloadSize: 20px;\nhistoryAudioRadialLine: 2px;\nhistoryAudioDownloadShift: 28px;\nhistoryAudioInCancel: icon {{ \"history_audio_cancel\", historyFileInIconFg }};\nhistoryAudioInCancelSelected: icon {{ \"history_audio_cancel\", historyFileInIconFgSelected }};\nhistoryAudioOutCancel: icon {{ \"history_audio_cancel\", historyFileOutIconFg }};\nhistoryAudioOutCancelSelected: icon {{ \"history_audio_cancel\", historyFileOutIconFgSelected }};\nhistoryAudioInDownload: icon {{ \"history_audio_download\", historyFileInIconFg }};\nhistoryAudioInDownloadSelected: icon {{ \"history_audio_download\", historyFileInIconFgSelected }};\nhistoryAudioOutDownload: icon {{ \"history_audio_download\", historyFileOutIconFg }};\nhistoryAudioOutDownloadSelected: icon {{ \"history_audio_download\", historyFileOutIconFgSelected }};\n\nhistoryQuizExplainIn: icon {{ \"quiz_explain\", msgFileThumbLinkInFg }};\nhistoryQuizExplainInSelected: icon {{ \"quiz_explain\", msgFileThumbLinkInFgSelected }};\nhistoryQuizExplainOut: icon {{ \"quiz_explain\", msgFileThumbLinkOutFg }};\nhistoryQuizExplainOutSelected: icon {{ \"quiz_explain\", msgFileThumbLinkOutFgSelected }};\nhistoryQuizTimerIn: icon {{ \"quiz_timer\", msgFileThumbLinkInFg }};\nhistoryQuizTimerInSelected: icon {{ \"quiz_timer\", msgFileThumbLinkInFgSelected }};\nhistoryQuizTimerOut: icon {{ \"quiz_timer\", msgFileThumbLinkOutFg }};\nhistoryQuizTimerOutSelected: icon {{ \"quiz_timer\", msgFileThumbLinkOutFgSelected }};\n\nhistoryCommentsIn: icon {{ \"history_comments\", msgFileThumbLinkInFg }};\nhistoryCommentsInSelected: icon {{ \"history_comments\", msgFileThumbLinkInFgSelected }};\nhistoryCommentsOut: icon {{ \"history_comments\", msgFileThumbLinkOutFg }};\nhistoryCommentsOutSelected: icon {{ \"history_comments\", msgFileThumbLinkOutFgSelected }};\nhistoryCommentsOpenIn: icon {{ \"history_comments_open\", msgFileThumbLinkInFg }};\nhistoryCommentsOpenInSelected: icon {{ \"history_comments_open\", msgFileThumbLinkInFgSelected }};\nhistoryCommentsOpenOut: icon {{ \"history_comments_open\", msgFileThumbLinkOutFg }};\nhistoryCommentsOpenOutSelected: icon {{ \"history_comments_open\", msgFileThumbLinkOutFgSelected }};\n\nhistoryGroupCallUserpics: GroupCallUserpics {\n\tsize: 32px;\n\tshift: 12px;\n\tstroke: 4px;\n\talign: align(top);\n}\nhistoryGroupCallBlobMinRadius: 23px;\nhistoryGroupCallBlobMaxRadius: 25px;\n\nlargeEmojiSize: 36px;\nlargeEmojiOutline: 1px;\nlargeEmojiPadding: margins(0px, 0px, 0px, 0px);\nlargeEmojiSkip: 4px;\n\nyoutubeIcon: icon {\n\t{ \"media_youtube_play_bg\", youtubePlayIconBg },\n\t{ \"media_youtube_play\", youtubePlayIconFg, point(24px, 12px) },\n};\nvideoIcon: icon {\n\t{ \"media_video_play_bg\", videoPlayIconBg },\n\t{ \"media_video_play\", videoPlayIconFg, point(12px, 12px) },\n};\n\nSliderForTTL {\n\tfont: font;\n\ttextFg: color;\n\tpointSize: pixels;\n\tchosenSize: pixels;\n\tskip: pixels;\n\tstroke: pixels;\n\tactiveFg: color;\n\tinactiveFg: color;\n\tdashOn: pixels;\n\tdashOff: pixels;\n}\n\ndefaultSliderForTTL: SliderForTTL {\n\tfont: normalFont;\n\ttextFg: windowSubTextFg;\n\tpointSize: 6px;\n\tchosenSize: 12px;\n\tskip: 8px;\n\tstroke: 2px;\n\tactiveFg: mediaPlayerActiveFg;\n\tinactiveFg: mediaPlayerInactiveFg;\n\tdashOn: 8px;\n\tdashOff: 5px;\n}\nttlDividerLabelPadding: margins(22px, 10px, 22px, 19px);\n\nttlItemPadding: margins(0px, 4px, 0px, 4px);\nttlItemTimerFont: font(12px);\n\nexpandedMenuSeparator: MenuSeparator(defaultMenuSeparator) {\n\tpadding: margins(0px, 4px, 0px, 4px);\n\twidth: 6px;\n}\npopupMenuExpandedSeparator: PopupMenu(popupMenuWithIcons) {\n\tmenu: Menu(menuWithIcons) {\n\t\tseparator: expandedMenuSeparator;\n\t}\n}\n\nmenuTimecodeFont: font(7px semibold);\n\nwhoReadMenu: PopupMenu(popupMenuExpandedSeparator) {\n\tscrollPadding: margins(0px, 6px, 0px, 4px);\n\tmaxHeight: 400px;\n}\nwhoReadNameWithDateTop: 3px;\nwhoReadDateTop: 20px;\nwhoReadDateSkip: 15px;\nwhoReadDateChecks: icon{{ \"menu/read_ticks_s\", windowSubTextFg }};\nwhoReadDateChecksOver: icon{{ \"menu/read_ticks_s\", windowSubTextFgOver }};\nwhoLikedDateHeart: icon{{ \"menu/read_react_s\", windowSubTextFg }};\nwhoLikedDateHeartOver: icon{{ \"menu/read_react_s\", windowSubTextFgOver }};\nwhoRepostedDateHeart: icon{{ \"mediaview/mini_repost\", groupCallMemberActiveIcon, point(4px, 4px) }};\nwhoRepostedDateHeartOver: icon{{ \"mediaview/mini_repost\", groupCallMemberActiveIcon, point(4px, 4px) }};\nwhoForwardedDateHeart: icon{{ \"statistics/mini_stats_share\", groupCallMemberActiveIcon, point(4px, 4px) }};\nwhoForwardedDateHeartOver: icon{{ \"statistics/mini_stats_share\", groupCallMemberActiveIcon, point(4px, 4px) }};\nwhoReadDateChecksPosition: point(-7px, -4px);\nwhoReadDateStyle: TextStyle(defaultTextStyle) {\n\tfont: font(12px);\n}\nwhoReadChecks: icon{{ \"menu/read_ticks\", windowBoldFg }};\nwhoReadChecksOver: icon{{ \"menu/read_ticks\", windowBoldFg }};\nwhoReadChecksDisabled: icon{{ \"menu/read_ticks\", menuFgDisabled }};\nwhoReadPlayed: icon{{ \"menu/read_audio\", windowBoldFg }};\nwhoReadPlayedOver: icon{{ \"menu/read_audio\", windowBoldFg }};\nwhoReadPlayedDisabled: icon {{ \"menu/read_audio\", menuFgDisabled }};\nwhoReadReactions: icon{{ \"menu/read_reactions\", windowBoldFg }};\nwhoReadReactionsOver: icon{{ \"menu/read_reactions\", windowBoldFg }};\nwhoReadReactionsDisabled: icon{{ \"menu/read_reactions\", menuFgDisabled }};\nwhenEdited: icon {{ \"menu/edited_status\", windowBoldFg }};\nwhenEditedOver: icon {{ \"menu/edited_status\", windowBoldFg }};\nwhenOriginal: icon {{ \"menu/forwarded_status\", windowBoldFg }};\nwhenOriginalOver: icon {{ \"menu/forwarded_status\", windowBoldFg }};\n\nreactionsTabAll: icon {{ \"menu/read_reactions\", windowFg }};\nreactionsTabAllSelected: icon {{ \"menu/read_reactions\", activeButtonFg }};\nreactionsTabPlayed: icon {{ \"menu/read_audio\", windowFg }};\nreactionsTabPlayedSelected: icon {{ \"menu/read_audio\", activeButtonFg }};\nreactionsTabChecks: icon {{ \"menu/read_ticks\", windowFg }};\nreactionsTabChecksSelected: icon {{ \"menu/read_ticks\", activeButtonFg }};\nreactionsTabs: MultiSelect(defaultMultiSelect) {\n\tpadding: margins(12px, 10px, 12px, 10px);\n}\nreactionsTabIconSkip: 3px;\n\nreactionInlinePadding: margins(5px, 2px, 7px, 2px);\nreactionInlineSize: 18px;\nreactionInlineImage: 32px;\nreactionInlineSkip: 3px;\nreactionInlineTagSkip: 6px;\nreactionInlineTagLeftRadius: 6px;\nreactionInlineTagRightRadius: 3px;\nreactionInlineTagArrow: 5px;\nreactionInlineTagDot: 5px;\nreactionInlineTagDotSkip: 2px;\nreactionInlineEmptySkip: 2px;\nreactionInlineTagFont: font(12px);\nreactionInlineTagNamePosition: point(26px, 2px);\nreactionInlineTagPromoPosition: point(20px, 2px);\nreactionInlineBetween: 4px;\nreactionInlineInBubbleLeft: -3px;\nreactionInlineUserpicsPadding: margins(1px, 1px, 1px, 1px);\nreactionInlineUserpics: GroupCallUserpics {\n\tsize: 20px;\n\tshift: 7px;\n\tstroke: 1px;\n\talign: align(left);\n}\n\nreactionInfoSize: 15px;\nreactionInfoImage: 30px;\nreactionInfoSkip: 3px;\nreactionInfoDigitSkip: 6px;\nreactionInfoBetween: 3px;\n\nreactionCornerSize: size(36px, 32px);\nreactionCornerCenter: point(7px, -9px);\nreactionCornerImage: 22px;\nreactionCornerShadow: margins(4px, 8px, 4px, 8px);\nreactionCornerActiveAreaPadding: margins(10px, 10px, 10px, 10px);\nreactionCornerAddedHeightMax: 100px;\n\nreactionCornerSkip: -4px;\nreactionExpandedSkip: 2px;\nreactionGradientStart: 8px;\nreactionGradientSize: 24px;\nreactionGradientFadeSize: 24px;\nreactionAppearStartSkip: 2px;\nreactionMainAppearShift: 20px;\nreactionCollapseFadeThreshold: 40px;\nreactionFlyUp: 50px;\n\nreplyCornerHeight: 20px;\nreplyCornerShadow: margins(4px, 8px, 4px, 8px);\nreplyCornerCenter: point(7px, -2px);\nreplyCornerTextPadding: margins(6px, 0px, 6px, 0px);\n\neffectInfoImage: 12px;\n\nsearchInChatMultiSelectItem: MultiSelectItem(defaultMultiSelectItem) {\n\tmaxWidth: 200px;\n}\nsearchInChatMultiSelect: MultiSelect(defaultMultiSelect) {\n\titem: searchInChatMultiSelectItem;\n}\nsearchInChatPeerListItem: PeerListItem(defaultPeerListItem) {\n\theight: dialogsRowHeight;\n}\nsearchInChatPeerList: PeerList(defaultPeerList) {\n\titem: searchInChatPeerListItem;\n}\nsearchInChatTagsPadding: margins(6px, 0px, 6px, 0px);\n\nmsgServiceGiftPreview: 172px;\nmsgServiceGiftBoxSize: size(236px, 231px); // Plus msgServiceGiftBoxTopSkip.\nmsgServiceGiftBoxRadius: 20px;\nmsgServiceGiftBoxTopSkip: 4px;\nmsgServiceGiftBoxButtonHeight: 30px;\nmsgServiceGiftBoxButtonPadding: margins(1px, 0px, 1px, 0px);\nmsgServiceGiftBoxButtonMargins: margins(0px, 10px, 0px, 20px);\nmsgServiceGiftBoxTitlePadding: margins(0px, 24px, 0px, 7px);\nmsgServiceGiftBoxBadgeFont: font(11px semibold);\nmsgServiceGiftBoxBadgePadding: margins(3px, 2px, 3px, 2px);\nmsgServiceGiftBoxStickerTop: -19px;\nmsgServiceGiftBoxStickerSize: 140px;\nmsgServiceStarGiftBoxWidth: 224px;\nmsgServiceStarGiftStickerTop: 24px;\nmsgServiceStarGiftStickerSize: 100px;\nmsgServiceGiftThemeStickerSize: 64px;\nmsgServiceGiftThemeStickerPadding: margins(18px, 18px, 18px, 18px);\n\nhistorySponsorInfoItem: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px);\n\t}\n\tminWidth: 136px;\n\tmaxHeight: 120px;\n}\nhistorySponsorInfoItemDark: FlatLabel(historySponsorInfoItem) {\n\ttextFg: mediaviewControlFg;\n}\nhistoryHasCustomEmoji: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(11px);\n\t}\n\tminWidth: 80px;\n}\nhistoryHasCustomEmojiPosition: point(12px, 4px);\nhistoryBankCardMenuMultilinePosition: point(18px, 4px);\n\nhistoryTranslateLabel: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n\ttextFg: windowActiveTextFg;\n\tminWidth: 80px;\n}\nhistoryTranslateIcon: icon{{ \"menu/translate\", windowActiveTextFg }};\nhistoryTranslateBarHeight: 36px;\nhistoryTranslateSettings: IconButton(defaultIconButton) {\n\twidth: 46px;\n\theight: 36px;\n\ticon: icon{{ \"menu/customize\", windowActiveTextFg }};\n\ticonOver: icon{{ \"menu/customize\", windowActiveTextFg }};\n\trippleAreaPosition: point(6px, 2px);\n\trippleAreaSize: 32px;\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: lightButtonBgOver;\n\t}\n}\nhistoryTranslateMenuPosition: point(-6px, 30px);\nhistoryTranslateCocoonLabel: FlatLabel(historyHasCustomEmoji) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px);\n\t}\n\tmargin: margins(4px, 8px, 4px, 8px);\n}\n\ncocoonLogoTop: 32px;\ncocoonLogoSize: 96px;\ncocoonTitleFont: font(36px semibold);\ncocoonTitleTop: 12px;\ncocoonSubtitle: FlatLabel(defaultFlatLabel) {\n\tminWidth: 120px;\n\talign: align(top);\n}\ncocoonSubtitleTop: 8px;\ncocoonSubtitleBottom: 16px;\ncocoonJoinSeparatorPadding: margins(24px, 4px, 24px, 12px);\ncocoonJoinLabel: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px);\n\t}\n\ttextFg: windowSubTextFg;\n\tminWidth: 120px;\n\talign: align(top);\n}\n\nhistorySponsoredAboutMenuLabelPosition: point(54px, 4px);\n\nhistorySendDisabled: FlatLabel(defaultFlatLabel) {\n\tminWidth: 10px;\n\tmaxHeight: 20px;\n\ttextFg: placeholderFg;\n}\nhistorySendDisabledIcon: icon {{ \"emoji/premium_lock\", placeholderFgActive }};\nhistorySendDisabledIconSkip: 20px;\nhistorySendDisabledPosition: point(0px, 0px);\nhistorySendPremiumRequired: FlatLabel(historySendDisabled) {\n\talign: align(top);\n}\n\nbackgroundSwitchToDark: IconButton(defaultIconButton) {\n\twidth: 48px;\n\theight: 48px;\n\n\ticon: icon {{ \"menu/header_mode_night\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"menu/header_mode_night\", boxTitleCloseFgOver }};\n\n\trippleAreaPosition: point(4px, 4px);\n\trippleAreaSize: 40px;\n\tripple: defaultRippleAnimationBgOver;\n}\nbackgroundSwitchToLight: IconButton(backgroundSwitchToDark) {\n\ticon: icon {{ \"menu/header_mode_day\", boxTitleCloseFg }};\n\ticonOver: icon {{ \"menu/header_mode_day\", boxTitleCloseFgOver }};\n}\n\nstoryMentionSize: 80px;\nstoryMentionUnreadSkipTwice: 8px;\nstoryMentionUnreadStrokeTwice: 6px;\nstoryMentionReadSkipTwice: 7px;\nstoryMentionReadStrokeTwice: 3px;\nstoryMentionButtonSkip: 5px;\n\nchatGiveawayWidth: 292px;\nchatGiveawayStickerTop: -16px;\nchatGiveawayStickerPadding: margins(14px, -2px, 14px, 14px);\nchatGiveawayWinnersTopSkip: 25px;\nchatGiveawayBadgeFont: font(12px bold);\nchatGiveawayBadgeTop: 118px;\nchatGiveawayBadgePadding: margins(7px, 1px, 5px, 3px);\nchatGiveawayBadgeStroke: 2px;\nchatGiveawayPrizesTitleMargin: margins(11px, 16px, 11px, 4px);\nchatGiveawayPrizesMargin: margins(11px, 0px, 11px, 0px);\nchatGiveawayPrizesWithPadding: margins(22px, 2px, 22px, 2px);\nchatGiveawayPrizesWithSkip: 8px;\nchatGiveawayPrizesWithLineTop: 9px;\nchatGiveawayParticipantsMargin: margins(11px, 0px, 11px, 6px);\nchatGiveawayNoCountriesTitleMargin: margins(11px, 6px, 11px, 4px);\nchatGiveawayEndDateMargin: margins(11px, 0px, 11px, 16px);\nchatGiveawayPeerSize: 32px;\nchatGiveawayPeerPadding: margins(5px, 7px, 12px, 0px);\nchatGiveawayPeerSkip: 8px;\nchatGiveawayCreditsIconHeight: 19px;\n\nchatSimilarRadius: 12px;\nchatSimilarArrowSize: 6px;\nchatSimilarTitle: semiboldFont;\nchatSimilarTitlePosition: point(15px, 9px);\nchatSimilarPadding: margins(8px, 32px, 8px, 4px);\nchatSimilarChannelPadding: margins(8px, 5px, 8px, 37px);\nchatSimilarChannelPhoto: 50px;\nchatSimilarBadgePadding: margins(2px, 0px, 3px, 1px);\nchatSimilarBadgeTop: 43px;\nchatSimilarBadgeIcon: icon{{ \"chat/mini_subscribers\", premiumButtonFg }};\nchatSimilarBadgeIconPosition: point(0px, 1px);\nchatSimilarLockedIcon: icon{{ \"chat/mini_lock\", premiumButtonFg }};\nchatSimilarLockedIconPosition: point(0px, 1px);\nchatSimilarBadgeFont: font(10px bold);\nchatSimilarNameTop: 59px;\nchatSimilarName: TextStyle(defaultTextStyle) {\n\tfont: font(12px);\n\tlineHeight: 14px;\n}\nchatSimilarWidthMax: 424px;\nchatSimilarSkip: 12px;\n\nchatSuggestWidth: 236px;\nchatSuggestInfoWidth: 272px;\nchatSuggestInfoTitleMargin: margins(16px, 16px, 16px, 6px);\nchatSuggestInfoMiddleMargin: margins(16px, 4px, 16px, 4px);\nchatSuggestInfoLastMargin: margins(16px, 4px, 16px, 16px);\nchatSuggestBulletLeftExtra: 16px;\nchatSuggestTableMiddleMargin: margins(8px, 4px, 8px, 4px);\nchatSuggestTableLastMargin: margins(8px, 4px, 8px, 16px);\nchatSuggestInfoFullMargin: margins(16px, 16px, 16px, 16px);\nchatSuggestAcceptIcon: IconEmoji {\n\ticon: icon{{ \"chat/paid_approve\", windowFg }};\n\tpadding: margins(0px, -2px, 0px, 0px);\n}\nchatSuggestDeclineIcon: IconEmoji {\n\ticon: icon{{ \"chat/paid_decline\", windowFg }};\n\tpadding: margins(0px, -2px, 0px, 0px);\n}\nchatSuggestChangeIcon: IconEmoji {\n\ticon: icon{{ \"chat/paid_edit\", windowFg }};\n\tpadding: margins(0px, -2px, 0px, 0px);\n}\n\npremiumRequiredWidth: 186px;\npremiumRequiredIcon: icon{{ \"chat/large_lockedchat\", msgServiceFg }};\npremiumRequiredCircle: 60px;\ndirectMessagesIcon: icon{{ \"chat/large_messages\", msgServiceFg }};\nstarsPerMessageWidth: 226px;\n\nrepliesEmptyIcon: icon{{ \"chat/large_quickreply\", msgServiceFg }};\ngreetingEmptyIcon: icon{{ \"chat/large_greeting\", msgServiceFg }};\nawayEmptyIcon: icon{{ \"chat/large_away\", msgServiceFg }};\nrepliesEmptyWidth: 264px;\nrepliesEmptySkip: 16px;\nrepliesEmptyPadding: margins(10px, 20px, 10px, 16px);\nrepliesComposeControls: ComposeControls(defaultComposeControls) {\n\ttabbedHeightMin: 220px;\n}\n\nboostMessageIcon: IconEmoji {\n\ticon: icon {{ \"stories/boost_mini\", windowFg }};\n\tpadding: margins(0px, 2px, 0px, 0px);\n}\nboostsMessageIcon: IconEmoji {\n\ticon: icon {{ \"stories/boosts_mini\", windowFg }};\n\tpadding: margins(0px, 2px, 0px, 0px);\n}\nhistoryIvIcon: IconEmoji {\n\ticon: icon {{ \"boosts/boost_mini2\", windowFg }};\n\tpadding: margins(2px, 2px, 2px, 0px);\n}\n\nchatIntroStickerSize: 96px;\nchatIntroWidth: 224px;\nchatIntroTitleMargin: margins(11px, 16px, 11px, 4px);\nchatIntroMargin: margins(11px, 0px, 11px, 0px);\nchatIntroStickerPadding: margins(10px, 8px, 10px, 16px);\nchatGiftPreviewWidth: 224px;\n\nliveLocationLongInIcon: icon {{ \"chat/live_location_long\", msgInServiceFg }};\nliveLocationLongInIconSelected: icon {{ \"chat/live_location_long\", msgInServiceFgSelected }};\nliveLocationLongOutIcon: icon {{ \"chat/live_location_long\", msgOutServiceFg }};\nliveLocationLongOutIconSelected: icon {{ \"chat/live_location_long\", msgOutServiceFgSelected }};\nliveLocationRemainingSize: 28px;\n\npreviewMenu: PopupMenu(defaultPopupMenu) {\n\tscrollPadding: margins(0px, 0px, 0px, 0px);\n\tmenu: Menu(defaultMenu) {\n\t\twidthMin: 380px;\n\t\twidthMax: 380px;\n\t}\n\tmaxHeight: 420px;\n\tradius: boxRadius;\n}\npreviewTop: PeerListItem(defaultPeerListItem) {\n\theight: 52px;\n\tphotoPosition: point(10px, 6px);\n\tnamePosition: point(60px, 9px);\n\tstatusPosition: point(60px, 27px);\n\tphotoSize: 40px;\n}\npreviewMarkRead: FlatButton(historyComposeButton) {\n\theight: 39px;\n\ttextTop: 10px;\n}\npreviewName: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n\ttextFg: windowFg;\n\tmaxHeight: 30px;\n}\npreviewStatus: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n}\npreviewUserpic: UserpicButton(defaultUserpicButton) {\n\tsize: size(40px, 40px);\n\tphotoSize: 40px;\n}\n\neffectPreviewSend: FlatButton(previewMarkRead) {\n\tbgColor: transparent;\n\toverBgColor: transparent;\n}\neffectPreviewPromoLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 64px;\n\talign: align(top);\n\ttextFg: windowSubTextFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(11px);\n\t}\n}\neffectPreviewPromoPadding: margins(4px, 6px, 4px, 6px);\neffectPreviewLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {\n\tthickness: 2px;\n}\n\nfactcheckFooterSkip: 16px;\nfactcheckFooterStyle: TextStyle(defaultTextStyle) {\n\tfont: font(11px);\n}\nfactcheckPage: QuoteStyle(historyPagePreview) {\n\tpadding: margins(10px, 5px, 22px, 7px);\n\texpand: icon {{ \"intro_country_dropdown\", historyPeer1NameFg }};\n\texpandPosition: point(6px, 4px);\n\tcollapse: icon {{ \"intro_country_dropdown-flip_vertical\", historyPeer1NameFg }};\n\tcollapsePosition: point(6px, 4px);\n}\nfactcheckField: InputField(defaultInputField) {\n\ttextBg: transparent;\n\ttextMargins: margins(0px, 0px, 0px, 4px);\n\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(2px, 0px, 2px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n\n\theightMin: 24px;\n\n\tstyle: defaultTextStyle;\n}\npurchasedTagPadding: margins(3px, 2px, 6px, 2px);\n\nmsgSelectionCheck: RoundCheckbox(defaultPeerListCheck) {\n\tbgActive: boxTextFgGood;\n}\n\nsponsoredMessageBarMaxHeight: 156px;\n\nbotEmojiStatusPreviewHeight: 148px;\nbotEmojiStatusTitle: FlatLabel(boxTitle) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(16px semibold);\n\t\tlineHeight: 20px;\n\t}\n\tminWidth: 256px;\n\tmaxHeight: 0px;\n\talign: align(top);\n}\nbotEmojiStatusText: FlatLabel(defaultFlatLabel) {\n\tstyle: boxTextStyle;\n\tminWidth: 256px;\n\tmaxHeight: 0px;\n\talign: align(top);\n}\nbotEmojiStatusUserpic: UserpicButton(defaultUserpicButton) {\n\tsize: size(chatGiveawayPeerSize, chatGiveawayPeerSize);\n\tphotoSize: chatGiveawayPeerSize;\n}\nbotEmojiStatusName: FlatLabel(defaultFlatLabel) {\n\tminWidth: 32px;\n\tmaxHeight: 20px;\n}\nbotEmojiStatusEmoji: FlatLabel(botEmojiStatusName) {\n\tmargin: margins(4px, 4px, 4px, 4px);\n\ttextFg: profileVerifiedCheckBg;\n}\n\nbotDownloadLabel: boxLabel;\nbotDownloadProgress: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: ttlItemTimerFont;\n\t}\n}\nbotDownloadCancel: IconButton {\n\twidth: 20px;\n\theight: 20px;\n\n\ticon: smallCloseIcon;\n\ticonOver: smallCloseIconOver;\n\ticonPosition: point(-1px, -1px);\n\n\trippleAreaPosition: point(0px, 0px);\n\trippleAreaSize: 20px;\n\tripple: defaultRippleAnimationBgOver;\n}\nchatUniqueGiftBorder: 4px;\nchatUniqueGiftBadgePadding: margins(12px, 0px, 12px, 2px);\n\nchatUniqueStickerPadding: margins(10px, 40px, 10px, 9px);\nchatUniquePreviewPadding: margins(10px, 30px, 10px, 32px);\nchatUniqueTitle: TextStyle(defaultTextStyle) {\n\tfont: font(15px semibold);\n}\nchatUniqueTitlePadding: margins(12px, 8px, 12px, 0px);\nchatUniqueTextPadding: margins(12px, 2px, 12px, 7px);\nchatUniqueTextStyle: TextStyle(defaultTextStyle) {\n\tfont: font(12px);\n}\nchatUniqueAuthorSkip: 6px;\nchatUniqueTableSkip: 9px;\nchatUniqueRowSkip: 2px;\nchatUniqueButtonPadding: margins(12px, 5px, 12px, 26px);\n\nmarkupWebview: icon {{ \"chat/markup_webview\", windowFg }};\n\nnewPeerTitleMargin: margins(11px, 16px, 11px, 6px);\nnewPeerSubtitleMargin: margins(11px, 0px, 11px, 16px);\nnewPeerNonOfficial: IconEmoji {\n\ticon: icon{{ \"chat/mini_info_alert\", windowFg }};\n\tpadding: margins(0px, 2px, 0px, 0px);\n}\nnewPeerUserpics: GroupCallUserpics {\n\tsize: 16px;\n\tshift: 5px;\n\tstroke: 1px;\n\talign: align(left);\n}\nnewPeerUserpicsPadding: margins(0px, 3px, 0px, 0px);\nnewPeerWidth: 320px;\n\nswipeBackSize: 150px;\n\nchatTabsToggle: IconButton(defaultIconButton) {\n\twidth: 64px;\n\theight: 36px;\n\ticon: icon {{ \"top_bar_profile-flip_horizontal\", menuIconFg }};\n\ticonOver: icon {{ \"top_bar_profile-flip_horizontal\", menuIconFgOver }};\n\tripple: emptyRippleAnimation;\n}\nchatTabsToggleIconTop: icon {{ \"chat/menu_sidebar2-24x24\", menuIconFg }};\nchatTabsToggleIconTopOver: icon {{ \"chat/menu_sidebar2-24x24\", menuIconFgOver }};\nchatTabsToggleIconLeft: icon {{ \"chat/menu_sidebar3-24x24\", menuIconFg }};\nchatTabsToggleIconLeftOver: icon {{ \"chat/menu_sidebar3-24x24\", menuIconFgOver }};\nchatTabsToggleIconBottom: icon {{ \"chat/menu_sidebar1-24x24\", menuIconFg }};\nchatTabsToggleIconBottomOver: icon {{ \"chat/menu_sidebar1-24x24\", menuIconFgOver }};\nchatTabsScroll: ScrollArea(defaultScrollArea) {\n\tbarHidden: true;\n}\nchatTabsSlider: SettingsSlider(defaultSettingsSlider) {\n\tpadding: 0px;\n\theight: 36px;\n\tbarTop: 33px;\n\tbarSkip: 0px;\n\tbarStroke: 6px;\n\tbarRadius: 2px;\n\tbarFg: transparent;\n\tbarSnapToLabel: true;\n\tstrictSkip: 18px;\n\tlabelTop: 9px;\n\tlabelStyle: semiboldTextStyle;\n\tlabelFg: windowSubTextFg;\n\tlabelFgActive: lightButtonFg;\n\trippleBottomSkip: 1px;\n\trippleBg: windowBgOver;\n\trippleBgActive: lightButtonBgOver;\n\tripple: defaultRippleAnimation;\n}\n\nChatTabsOutline {\n\tradius: pixels;\n\tstroke: pixels;\n\tfg: color;\n\tskip: pixels;\n}\n\nChatTabsVertical {\n\tbarStroke: pixels;\n\tbarRadius: pixels;\n\tbarFg: color;\n\tnameStyle: TextStyle;\n\tnameWidth: pixels;\n\tnameTop: pixels;\n\tnameFg: color;\n\tnameFgActive: color;\n\tuserpicTop: pixels;\n\tuserpicSize: pixels;\n\tbaseHeight: pixels;\n\twidth: pixels;\n\tripple: RippleAnimation;\n\trippleBg: color;\n\trippleBgActive: color;\n\tduration: int;\n}\n\nchatTabsVertical: ChatTabsVertical {\n\tbarStroke: 8px;\n\tbarRadius: 4px;\n\tbarFg: sliderBgActive;\n\tnameStyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(10px);\n\t}\n\tnameWidth: 54px;\n\tnameTop: 42px;\n\tnameFg: windowSubTextFg;\n\tnameFgActive: lightButtonFg;\n\tuserpicTop: 8px;\n\tuserpicSize: 28px;\n\tbaseHeight: 50px;\n\twidth: 64px;\n\tripple: defaultRippleAnimation;\n\trippleBg: windowBgOver;\n\trippleBgActive: lightButtonBgOver;\n\tduration: 150;\n}\n\nchatTabsOutlineHorizontal: ChatTabsOutline {\n\tstroke: 8px;\n\tradius: 4px;\n\tfg: sliderBgActive;\n\tskip: 8px;\n}\n\nchatTabsOutlineVertical: ChatTabsOutline(chatTabsOutlineHorizontal) {\n}\n\nmessageSendingAnimationTextFromOffset: point(-6px, 9px);\n\nsuggestPriceBox: Box(defaultBox) {\n\tbuttonPadding: margins(22px, 22px, 22px, 22px);\n\tbuttonHeight: 42px;\n\tbutton: RoundButton(defaultActiveButton) {\n\t\theight: 42px;\n\t\ttextTop: 12px;\n\t\tstyle: semiboldTextStyle;\n\t}\n\tshadowIgnoreTopSkip: true;\n}\nsuggestPriceEstimate: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n}\nsuggestPriceEstimateTop: 12px;\ntonInput: InputField(defaultInputField) {\n\ttextBg: transparent;\n\ttextMargins: margins(0px, 7px, 0px, 7px);\n\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(0px, 0px, 0px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderFont: boxTextFont;\n\n\theightMin: 34px;\n\theightMax: 100px;\n}\nstarsFieldIconPosition: point(0px, 10px);\ntonFieldIconSize: 16px;\ntonFieldIconPosition: point(2px, 11px);\n\nsuggestBarTonIconSize: 14px;\nsuggestBarTonIconMargins: margins(0px, 3px, 0px, 0px);\n\nlowTonIconPadding: margins(12px, 20px, 12px, 0px);\nlowTonTitlePadding: margins(0px, 12px, 0px, 12px);\nlowTonTextPadding: margins(0px, 0px, 0px, 8px);\nlowTonText: FlatLabel(defaultFlatLabel) {\n\tminWidth: 100px;\n\talign: align(top);\n}\n\ngroupMembersWidgetList: PeerList(defaultPeerList) {\n\titem: PeerListItem(defaultPeerListItem) {\n\t\tphotoPosition: point(12px, 6px);\n\t\tnamePosition: point(68px, 11px);\n\t\tstatusPosition: point(68px, 31px);\n\t\tcheckbox: RoundImageCheckbox(defaultPeerListCheckbox) {\n\t\t\tselectExtendTwice: 1px;\n\t\t\timageRadius: 21px;\n\t\t\timageSmallRadius: 19px;\n\t\t\tcheck: RoundCheckbox(defaultPeerListCheck) {\n\t\t\t\tsize: 0px;\n\t\t\t}\n\t\t}\n\t\tnameFgChecked: contactsNameFg;\n\t}\n}\n\nsaveMusicInfoMenu: Menu(defaultMenu) {\n\titemBgOver: windowBg;\n\titemFgOver: windowFg;\n\titemPadding: margins(17px, 4px, 17px, 3px);\n}\n\nnewBotThreadDown: icon {{ \"history_down_arrow\", msgServiceFg }};\nnewBotThreadTopSkip: 24px;\n\nbirthdaySuggestStickerWidth: 236px;\nbirthdaySuggestStickerSize: 96px;\nbirthdaySuggestStickerPadding: margins(10px, 16px, 10px, 4px);\nbirthdaySuggestTextPadding: margins(10px, 4px, 10px, 8px);\nbirthdaySuggestTablePadding: margins(2px, 4px, 2px, 12px);\nbirthdaySuggestTableLastPadding: margins(2px, 4px, 2px, 24px);\nbirthdaySuggestTableSkip: 2px;\n\nmessageGiftIconSkip: 30px;\n\nnewChatIcon: icon{{ \"chat/new_topic\", windowFgActive }};\nnewChatIconPadding: margins(2px, 2px, 2px, 2px);\n\nnewThreadAboutMaxWidth: 180px;\nnewThreadAboutIconSkip: chatIntroTitleMargin.top;\nnewThreadAboutIconOuter: 60px;\nnewThreadAboutIcon: icon{{ \"chat/new_thread-40x40\", windowFgActive }};\n\nmanagedBotCodeIcon: icon{{ \"chat/code_tags\", windowFg }};\nmanagedBotImageWidth: 280px;\nmanagedBotImageHeight: 140px;\n\nstakeBox: Box(defaultBox) {\n\tbuttonPadding: margins(24px, 12px, 24px, 24px);\n\tbuttonHeight: 42px;\n\tbuttonWide: true;\n\tbutton: RoundButton(defaultActiveButton) {\n\t\theight: 42px;\n\t\ttextTop: 12px;\n\t\tstyle: semiboldTextStyle;\n\t}\n\tshadowIgnoreTopSkip: true;\n}\nstakeEmojiSize: 20px;\nstakeEmojiTop: 8px;\nstakeEmojiIcon: IconEmoji {\n\ticon: icon{{ \"chat/mini_roll-18x18\", activeButtonFg }};\n\tpadding: margins(1px, 2px, 1px, 0px);\n}\nstakeIconEmojiSize: 12px;\nstakeIconEmojiTop: 3px;\nstakePresetButton: RoundButton(defaultLightButton) {\n\ttextBg: lightButtonBgOver;\n}\nstakePresetButtonSkip: point(8px, 8px);\nstakeChangeBadge: RoundButton(defaultTableSmallButton) {\n\ttextFg: mediaviewTextLinkFg;\n\ttextFgOver: mediaviewTextLinkFg;\n}\nstateChangeBadgeMargin: margins(0px, 1px, 0px, 0px);\n\nhistorySummaryHeaderIconSize: 30px;\nhistorySummaryHeaderIconSizeInner: 5px;\n\ntoastCheckIcon: icon {{ \"toast/check-36x36\", toastFg }};\ntoastCheckIconPadding: margins(12px, 8px, 12px, 8px);\n\nhistorySummaryStars: icon {{ \"chat/summary_stars-24x24\", msgServiceFg }};\nhistorySummaryArrows: icon {{ \"chat/summary_arrows-24x24\", msgServiceFg }};\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/chat_style.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/chat_style.h\"\n\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/image/image_prepare.h\" // ImageRoundRadius\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/color_contrast.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_polls.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Ui {\nnamespace {\n\nvoid EnsureCorners(\n\t\tCornersPixmaps &corners,\n\t\tint radius,\n\t\tconst style::color &color,\n\t\tconst style::color *shadow = nullptr) {\n\tif (corners.p[0].isNull()) {\n\t\tcorners = PrepareCornerPixmaps(radius, color, shadow);\n\t}\n}\n\nvoid EnsureBlockquoteCache(\n\t\tstd::unique_ptr<Text::QuotePaintCache> &cache,\n\t\tFn<ColorIndexValues()> values) {\n\tif (cache) {\n\t\treturn;\n\t}\n\tcache = std::make_unique<Text::QuotePaintCache>();\n\tconst auto &colors = values();\n\tcache->bg = colors.bg;\n\tcache->outlines = colors.outlines;\n\tcache->icon = colors.name;\n}\n\nvoid EnsurePreCache(\n\t\tstd::unique_ptr<Text::QuotePaintCache> &cache,\n\t\tconst style::color &color,\n\t\tFn<std::optional<QColor>()> bgOverride) {\n\tif (cache) {\n\t\treturn;\n\t}\n\tcache = std::make_unique<Text::QuotePaintCache>();\n\tconst auto bg = bgOverride();\n\tcache->bg = bg.value_or(color->c);\n\tif (!bg) {\n\t\tcache->bg.setAlpha(kDefaultBgOpacity * 255);\n\t}\n\tcache->outlines[0] = color->c;\n\tcache->outlines[0].setAlpha(kDefaultOutline1Opacity * 255);\n\tcache->outlines[1] = cache->outlines[2] = QColor(0, 0, 0, 0);\n\tcache->header = color->c;\n\tcache->header.setAlpha(kDefaultOutline2Opacity * 255);\n\tcache->icon = cache->outlines[0];\n\tcache->icon.setAlpha(kDefaultOutline3Opacity * 255);\n}\n\n} // namespace\n\nnot_null<const MessageStyle*> ChatPaintContext::messageStyle() const {\n\treturn &st->messageStyle(outbg, selected());\n}\n\nnot_null<const MessageImageStyle*> ChatPaintContext::imageStyle() const {\n\treturn &st->imageStyle(selected());\n}\n\nnot_null<Text::QuotePaintCache*> ChatPaintContext::quoteCache(\n\t\tconst std::shared_ptr<ColorCollectible> &colorCollectible,\n\t\tuint8 colorIndex) const {\n\treturn outbg\n\t\t? messageStyle()->quoteCache[colorCollectible\n\t\t\t? 2\n\t\t\t: st->colorPatternIndex(colorIndex)].get()\n\t\t: colorCollectible\n\t\t? st->collectibleQuoteCache(selected(), colorCollectible).get()\n\t\t: st->coloredQuoteCache(selected(), colorIndex).get();\n}\n\nint HistoryServiceMsgRadius() {\n\tstatic const auto result = [] {\n\t\tconst auto minMessageHeight = st::msgServicePadding.top()\n\t\t\t+ st::msgServiceFont->height\n\t\t\t+ st::msgServicePadding.bottom();\n\t\treturn minMessageHeight / 2;\n\t}();\n\treturn result;\n}\n\nint HistoryServiceMsgInvertedRadius() {\n\tstatic const auto result = [] {\n\t\tconst auto minRowHeight = st::msgServiceFont->height;\n\t\treturn minRowHeight - HistoryServiceMsgRadius();\n\t}();\n\treturn result;\n}\n\nint HistoryServiceMsgInvertedShrink() {\n\tstatic const auto result = [] {\n\t\treturn (HistoryServiceMsgInvertedRadius() * 2) / 3;\n\t}();\n\treturn result;\n}\n\nColorIndexValues SimpleColorIndexValues(QColor color, int patternIndex) {\n\tauto bg = color;\n\tbg.setAlpha(kDefaultBgOpacity * 255);\n\tauto result = ColorIndexValues{\n\t\t.name = color,\n\t\t.bg = bg,\n\t};\n\tresult.outlines[0] = color;\n\tresult.outlines[0].setAlpha(kDefaultOutline1Opacity * 255);\n\tif (patternIndex > 1) {\n\t\tresult.outlines[1] = result.outlines[0];\n\t\tresult.outlines[1].setAlpha(kDefaultOutline2Opacity * 255);\n\t\tresult.outlines[2] = result.outlines[0];\n\t\tresult.outlines[2].setAlpha(kDefaultOutline3Opacity * 255);\n\t} else if (patternIndex > 0) {\n\t\tresult.outlines[1] = result.outlines[0];\n\t\tresult.outlines[1].setAlpha(kDefaultOutlineOpacitySecond * 255);\n\t\tresult.outlines[2] = QColor(0, 0, 0, 0);\n\t} else {\n\t\tresult.outlines[1] = result.outlines[2] = QColor(0, 0, 0, 0);\n\t}\n\treturn result;\n}\n\nint BackgroundEmojiData::CacheIndex(\n\t\tbool selected,\n\t\tbool outbg,\n\t\tbool inbubble,\n\t\tuint8 colorIndexPlusOne) {\n\tconst auto base = colorIndexPlusOne\n\t\t? (colorIndexPlusOne - 1)\n\t\t: (kColorIndexCount + (!inbubble ? 0 : outbg ? 1 : 2));\n\treturn (base * 2) + (selected ? 1 : 0);\n};\n\nint ColorPatternIndex(\n\t\tconst ColorIndicesCompressed &indices,\n\t\tuint8 colorIndex,\n\t\tbool dark) {\n\tExpects(colorIndex >= 0 && colorIndex < kColorIndexCount);\n\n\tif (!indices.colors\n\t\t|| colorIndex < kSimpleColorIndexCount) {\n\t\treturn 0;\n\t}\n\tauto &data = (*indices.colors)[colorIndex];\n\tauto &colors = dark ? data.dark : data.light;\n\treturn colors[2] ? 2 : colors[1] ? 1 : 0;\n}\n\nChatStyle::ColoredPalette::ColoredPalette() = default;\nChatStyle::ColoredPalette::ColoredPalette(const ColoredPalette &other)\n: linkFg(other.linkFg)\n, data(other.data) {\n\tif (linkFg) {\n\t\tdata.linkFg = linkFg->color();\n\t\tdata.selectLinkFg = data.linkFg;\n\t}\n}\n\nChatStyle::ColoredPalette &ChatStyle::ColoredPalette::operator=(\n\t\tconst ColoredPalette &other) {\n\tlinkFg = other.linkFg;\n\tdata = other.data;\n\tif (linkFg) {\n\t\tdata.linkFg = linkFg->color();\n\t\tdata.selectLinkFg = data.linkFg;\n\t}\n\treturn *this;\n}\n\nChatStyle::ChatStyle(rpl::producer<ColorIndicesCompressed> colorIndices) {\n\tif (colorIndices) {\n\t\t_colorIndicesLifetime = std::move(\n\t\t\tcolorIndices\n\t\t) | rpl::on_next([=](ColorIndicesCompressed &&indices) {\n\t\t\t_colorIndices = std::move(indices);\n\t\t});\n\t}\n\n\tfinalize();\n\tmake(_historyPsaForwardPalette, st::historyPsaForwardPalette);\n\tmake(_imgReplyTextPalette, st::imgReplyTextPalette);\n\tmake(_serviceTextPalette, st::serviceTextPalette);\n\tmake(_priceTagTextPalette, st::priceTagTextPalette);\n\tmake(_historyRepliesInvertedIcon, st::historyRepliesInvertedIcon);\n\tmake(_historyViewsInvertedIcon, st::historyViewsInvertedIcon);\n\tmake(_historyViewsSendingIcon, st::historyViewsSendingIcon);\n\tmake(\n\t\t_historyViewsSendingInvertedIcon,\n\t\tst::historyViewsSendingInvertedIcon);\n\tmake(_historyPinInvertedIcon, st::historyPinInvertedIcon);\n\tmake(_historySendingIcon, st::historySendingIcon);\n\tmake(_historySendingInvertedIcon, st::historySendingInvertedIcon);\n\tmake(_historySentInvertedIcon, st::historySentInvertedIcon);\n\tmake(_historyReceivedInvertedIcon, st::historyReceivedInvertedIcon);\n\tmake(_msgBotKbUrlIcon, st::msgBotKbUrlIcon);\n\tmake(_msgBotKbPaymentIcon, st::msgBotKbPaymentIcon);\n\tmake(_msgBotKbSwitchPmIcon, st::msgBotKbSwitchPmIcon);\n\tmake(_msgBotKbWebviewIcon, st::msgBotKbWebviewIcon);\n\tmake(_msgBotKbCopyIcon, st::msgBotKbCopyIcon);\n\tmake(_historyFastCommentsIcon, st::historyFastCommentsIcon);\n\tmake(_historyFastShareIcon, st::historyFastShareIcon);\n\tmake(_historyFastTranscribeIcon, st::historyFastTranscribeIcon);\n\tmake(_historyFastTranscribeLock, st::historyFastTranscribeLock);\n\tmake(_historyGoToOriginalIcon, st::historyGoToOriginalIcon);\n\tmake(_historyFastCloseIcon, st::historyFastCloseIcon);\n\tmake(_historyFastMoreIcon, st::historyFastMoreIcon);\n\tmake(_historyMapPoint, st::historyMapPoint);\n\tmake(_historyMapPointInner, st::historyMapPointInner);\n\tmake(_youtubeIcon, st::youtubeIcon);\n\tmake(_videoIcon, st::videoIcon);\n\tmake(_historyPollChoiceRight, st::historyPollChoiceRight);\n\tmake(_historyPollChoiceWrong, st::historyPollChoiceWrong);\n\tmake(\n\t\t&MessageStyle::msgBg,\n\t\tst::msgInBg,\n\t\tst::msgInBgSelected,\n\t\tst::msgOutBg,\n\t\tst::msgOutBgSelected);\n\tmake(\n\t\t&MessageStyle::msgShadow,\n\t\tst::msgInShadow,\n\t\tst::msgInShadowSelected,\n\t\tst::msgOutShadow,\n\t\tst::msgOutShadowSelected);\n\tmake(\n\t\t&MessageStyle::msgServiceFg,\n\t\tst::msgInServiceFg,\n\t\tst::msgInServiceFgSelected,\n\t\tst::msgOutServiceFg,\n\t\tst::msgOutServiceFgSelected);\n\tmake(\n\t\t&MessageStyle::msgDateFg,\n\t\tst::msgInDateFg,\n\t\tst::msgInDateFgSelected,\n\t\tst::msgOutDateFg,\n\t\tst::msgOutDateFgSelected);\n\tmake(\n\t\t&MessageStyle::msgFileThumbLinkFg,\n\t\tst::msgFileThumbLinkInFg,\n\t\tst::msgFileThumbLinkInFgSelected,\n\t\tst::msgFileThumbLinkOutFg,\n\t\tst::msgFileThumbLinkOutFgSelected);\n\tmake(\n\t\t&MessageStyle::msgFileBg,\n\t\tst::msgFileInBg,\n\t\tst::msgFileInBgSelected,\n\t\tst::msgFileOutBg,\n\t\tst::msgFileOutBgSelected);\n\tmake(\n\t\t&MessageStyle::msgReplyBarColor,\n\t\tst::msgInReplyBarColor,\n\t\tst::msgInReplyBarSelColor,\n\t\tst::msgOutReplyBarColor,\n\t\tst::msgOutReplyBarSelColor);\n\tmake(\n\t\t&MessageStyle::msgWaveformActive,\n\t\tst::msgWaveformInActive,\n\t\tst::msgWaveformInActiveSelected,\n\t\tst::msgWaveformOutActive,\n\t\tst::msgWaveformOutActiveSelected);\n\tmake(\n\t\t&MessageStyle::msgWaveformInactive,\n\t\tst::msgWaveformInInactive,\n\t\tst::msgWaveformInInactiveSelected,\n\t\tst::msgWaveformOutInactive,\n\t\tst::msgWaveformOutInactiveSelected);\n\tmake(\n\t\t&MessageStyle::historyTextFg,\n\t\tst::historyTextInFg,\n\t\tst::historyTextInFgSelected,\n\t\tst::historyTextOutFg,\n\t\tst::historyTextOutFgSelected);\n\tmake(\n\t\t&MessageStyle::historyFileNameFg,\n\t\tst::historyFileNameInFg,\n\t\tst::historyFileNameInFgSelected,\n\t\tst::historyFileNameOutFg,\n\t\tst::historyFileNameOutFgSelected);\n\tmake(\n\t\t&MessageStyle::historyFileRadialFg,\n\t\tst::historyFileInRadialFg,\n\t\tst::historyFileInRadialFgSelected,\n\t\tst::historyFileOutRadialFg,\n\t\tst::historyFileOutRadialFgSelected);\n\tmake(\n\t\t&MessageStyle::mediaFg,\n\t\tst::mediaInFg,\n\t\tst::mediaInFgSelected,\n\t\tst::mediaOutFg,\n\t\tst::mediaOutFgSelected);\n\tmake(\n\t\t&MessageStyle::textPalette,\n\t\tst::inTextPalette,\n\t\tst::inTextPaletteSelected,\n\t\tst::outTextPalette,\n\t\tst::outTextPaletteSelected);\n\tmake(\n\t\t&MessageStyle::semiboldPalette,\n\t\tst::inSemiboldPalette,\n\t\tst::inTextPaletteSelected,\n\t\tst::outSemiboldPalette,\n\t\tst::outTextPaletteSelected);\n\tmake(\n\t\t&MessageStyle::fwdTextPalette,\n\t\tst::inFwdTextPalette,\n\t\tst::inFwdTextPaletteSelected,\n\t\tst::outFwdTextPalette,\n\t\tst::outFwdTextPaletteSelected);\n\tmake(\n\t\t&MessageStyle::replyTextPalette,\n\t\tst::inReplyTextPalette,\n\t\tst::inReplyTextPaletteSelected,\n\t\tst::outReplyTextPalette,\n\t\tst::outReplyTextPaletteSelected);\n\tmake(\n\t\t&MessageStyle::tailLeft,\n\t\tst::historyBubbleTailInLeft,\n\t\tst::historyBubbleTailInLeftSelected,\n\t\tst::historyBubbleTailOutLeft,\n\t\tst::historyBubbleTailOutLeftSelected);\n\tmake(\n\t\t&MessageStyle::tailRight,\n\t\tst::historyBubbleTailInRight,\n\t\tst::historyBubbleTailInRightSelected,\n\t\tst::historyBubbleTailOutRight,\n\t\tst::historyBubbleTailOutRightSelected);\n\tmake(\n\t\t&MessageStyle::historyRepliesIcon,\n\t\tst::historyRepliesInIcon,\n\t\tst::historyRepliesInSelectedIcon,\n\t\tst::historyRepliesOutIcon,\n\t\tst::historyRepliesOutSelectedIcon);\n\tmake(\n\t\t&MessageStyle::historyViewsIcon,\n\t\tst::historyViewsInIcon,\n\t\tst::historyViewsInSelectedIcon,\n\t\tst::historyViewsOutIcon,\n\t\tst::historyViewsOutSelectedIcon);\n\tmake(\n\t\t&MessageStyle::historyPinIcon,\n\t\tst::historyPinInIcon,\n\t\tst::historyPinInSelectedIcon,\n\t\tst::historyPinOutIcon,\n\t\tst::historyPinOutSelectedIcon);\n\tmake(\n\t\t&MessageStyle::historySentIcon,\n\t\tst::historySentIcon,\n\t\tst::historySentSelectedIcon,\n\t\tst::historySentIcon,\n\t\tst::historySentSelectedIcon);\n\tmake(\n\t\t&MessageStyle::historyReceivedIcon,\n\t\tst::historyReceivedIcon,\n\t\tst::historyReceivedSelectedIcon,\n\t\tst::historyReceivedIcon,\n\t\tst::historyReceivedSelectedIcon);\n\tmake(\n\t\t&MessageStyle::historyPsaIcon,\n\t\tst::historyPsaIconIn,\n\t\tst::historyPsaIconInSelected,\n\t\tst::historyPsaIconOut,\n\t\tst::historyPsaIconOutSelected);\n\tmake(\n\t\t&MessageStyle::historyCommentsOpen,\n\t\tst::historyCommentsOpenIn,\n\t\tst::historyCommentsOpenInSelected,\n\t\tst::historyCommentsOpenOut,\n\t\tst::historyCommentsOpenOutSelected);\n\tmake(\n\t\t&MessageStyle::historyComments,\n\t\tst::historyCommentsIn,\n\t\tst::historyCommentsInSelected,\n\t\tst::historyCommentsOut,\n\t\tst::historyCommentsOutSelected);\n\tmake(\n\t\t&MessageStyle::historyCallArrow,\n\t\tst::historyCallArrowIn,\n\t\tst::historyCallArrowInSelected,\n\t\tst::historyCallArrowOut,\n\t\tst::historyCallArrowOutSelected);\n\tmake(\n\t\t&MessageStyle::historyCallArrowMissed,\n\t\tst::historyCallArrowMissedIn,\n\t\tst::historyCallArrowMissedInSelected,\n\t\tst::historyCallArrowMissedIn,\n\t\tst::historyCallArrowMissedInSelected);\n\tmake(\n\t\t&MessageStyle::historyCallIcon,\n\t\tst::historyCallInIcon,\n\t\tst::historyCallInIconSelected,\n\t\tst::historyCallOutIcon,\n\t\tst::historyCallOutIconSelected);\n\tmake(\n\t\t&MessageStyle::historyCallCameraIcon,\n\t\tst::historyCallCameraInIcon,\n\t\tst::historyCallCameraInIconSelected,\n\t\tst::historyCallCameraOutIcon,\n\t\tst::historyCallCameraOutIconSelected);\n\tmake(\n\t\t&MessageStyle::historyCallGroupIcon,\n\t\tst::historyCallGroupInIcon,\n\t\tst::historyCallGroupInIconSelected,\n\t\tst::historyCallGroupOutIcon,\n\t\tst::historyCallGroupOutIconSelected);\n\tmake(\n\t\t&MessageStyle::historyFilePlay,\n\t\tst::historyFileInPlay,\n\t\tst::historyFileInPlaySelected,\n\t\tst::historyFileOutPlay,\n\t\tst::historyFileOutPlaySelected);\n\tmake(\n\t\t&MessageStyle::historyFileWaiting,\n\t\tst::historyFileInWaiting,\n\t\tst::historyFileInWaitingSelected,\n\t\tst::historyFileOutWaiting,\n\t\tst::historyFileOutWaitingSelected);\n\tmake(\n\t\t&MessageStyle::historyFileDownload,\n\t\tst::historyFileInDownload,\n\t\tst::historyFileInDownloadSelected,\n\t\tst::historyFileOutDownload,\n\t\tst::historyFileOutDownloadSelected);\n\tmake(\n\t\t&MessageStyle::historyFileCancel,\n\t\tst::historyFileInCancel,\n\t\tst::historyFileInCancelSelected,\n\t\tst::historyFileOutCancel,\n\t\tst::historyFileOutCancelSelected);\n\tmake(\n\t\t&MessageStyle::historyFilePause,\n\t\tst::historyFileInPause,\n\t\tst::historyFileInPauseSelected,\n\t\tst::historyFileOutPause,\n\t\tst::historyFileOutPauseSelected);\n\tmake(\n\t\t&MessageStyle::historyFileImage,\n\t\tst::historyFileInImage,\n\t\tst::historyFileInImageSelected,\n\t\tst::historyFileOutImage,\n\t\tst::historyFileOutImageSelected);\n\tmake(\n\t\t&MessageStyle::historyFileDocument,\n\t\tst::historyFileInDocument,\n\t\tst::historyFileInDocumentSelected,\n\t\tst::historyFileOutDocument,\n\t\tst::historyFileOutDocumentSelected);\n\tmake(\n\t\t&MessageStyle::historyAudioDownload,\n\t\tst::historyAudioInDownload,\n\t\tst::historyAudioInDownloadSelected,\n\t\tst::historyAudioOutDownload,\n\t\tst::historyAudioOutDownloadSelected);\n\tmake(\n\t\t&MessageStyle::historyAudioCancel,\n\t\tst::historyAudioInCancel,\n\t\tst::historyAudioInCancelSelected,\n\t\tst::historyAudioOutCancel,\n\t\tst::historyAudioOutCancelSelected);\n\tmake(\n\t\t&MessageStyle::historyQuizTimer,\n\t\tst::historyQuizTimerIn,\n\t\tst::historyQuizTimerInSelected,\n\t\tst::historyQuizTimerOut,\n\t\tst::historyQuizTimerOutSelected);\n\tmake(\n\t\t&MessageStyle::historyQuizExplain,\n\t\tst::historyQuizExplainIn,\n\t\tst::historyQuizExplainInSelected,\n\t\tst::historyQuizExplainOut,\n\t\tst::historyQuizExplainOutSelected);\n\tmake(\n\t\t&MessageStyle::historyPollChosen,\n\t\tst::historyPollInChosen,\n\t\tst::historyPollInChosenSelected,\n\t\tst::historyPollOutChosen,\n\t\tst::historyPollOutChosenSelected);\n\tmake(\n\t\t&MessageStyle::historyPollChoiceRight,\n\t\tst::historyPollInChoiceRight,\n\t\tst::historyPollInChoiceRightSelected,\n\t\tst::historyPollOutChoiceRight,\n\t\tst::historyPollOutChoiceRightSelected);\n\tmake(\n\t\t&MessageStyle::historyTranscribeIcon,\n\t\tst::historyTranscribeInIcon,\n\t\tst::historyTranscribeInIconSelected,\n\t\tst::historyTranscribeOutIcon,\n\t\tst::historyTranscribeOutIconSelected);\n\tmake(\n\t\t&MessageStyle::historyTranscribeLock,\n\t\tst::historyTranscribeInLock,\n\t\tst::historyTranscribeInLockSelected,\n\t\tst::historyTranscribeOutLock,\n\t\tst::historyTranscribeOutLockSelected);\n\tmake(\n\t\t&MessageStyle::historyTranscribeHide,\n\t\tst::historyTranscribeInHide,\n\t\tst::historyTranscribeInHideSelected,\n\t\tst::historyTranscribeOutHide,\n\t\tst::historyTranscribeOutHideSelected);\n\tmake(\n\t\t&MessageImageStyle::msgDateImgBg,\n\t\tst::msgDateImgBg,\n\t\tst::msgDateImgBgSelected);\n\tmake(\n\t\t&MessageImageStyle::msgServiceBg,\n\t\tst::msgServiceBg,\n\t\tst::msgServiceBgSelected);\n\tmake(\n\t\t&MessageImageStyle::msgShadow,\n\t\tst::msgInShadow,\n\t\tst::msgInShadowSelected);\n\tmake(\n\t\t&MessageImageStyle::historyFileThumbRadialFg,\n\t\tst::historyFileThumbRadialFg,\n\t\tst::historyFileThumbRadialFgSelected);\n\tmake(\n\t\t&MessageImageStyle::historyFileThumbPlay,\n\t\tst::historyFileThumbPlay,\n\t\tst::historyFileThumbPlaySelected);\n\tmake(\n\t\t&MessageImageStyle::historyFileThumbWaiting,\n\t\tst::historyFileThumbWaiting,\n\t\tst::historyFileThumbWaitingSelected);\n\tmake(\n\t\t&MessageImageStyle::historyFileThumbDownload,\n\t\tst::historyFileThumbDownload,\n\t\tst::historyFileThumbDownloadSelected);\n\tmake(\n\t\t&MessageImageStyle::historyFileThumbCancel,\n\t\tst::historyFileThumbCancel,\n\t\tst::historyFileThumbCancelSelected);\n\tmake(\n\t\t&MessageImageStyle::historyFileThumbPause,\n\t\tst::historyFileThumbPause,\n\t\tst::historyFileThumbPauseSelected);\n\tmake(\n\t\t&MessageImageStyle::historyVideoDownload,\n\t\tst::historyVideoDownload,\n\t\tst::historyVideoDownloadSelected);\n\tmake(\n\t\t&MessageImageStyle::historyVideoCancel,\n\t\tst::historyVideoCancel,\n\t\tst::historyVideoCancelSelected);\n\tmake(\n\t\t&MessageImageStyle::historyVideoMessageMute,\n\t\tst::historyVideoMessageMute,\n\t\tst::historyVideoMessageMuteSelected);\n\tmake(\n\t\t&MessageImageStyle::historyVideoMessageTtlIcon,\n\t\tst::historyVideoMessageTtlIcon,\n\t\tst::historyVideoMessageTtlIconSelected);\n\tmake(\n\t\t&MessageImageStyle::historyPageEnlarge,\n\t\tst::historyPageEnlarge,\n\t\tst::historyPageEnlargeSelected);\n\tmake(\n\t\t&MessageStyle::historyVoiceMessageTTL,\n\t\tst::historyVoiceMessageInTTL,\n\t\tst::historyVoiceMessageInTTLSelected,\n\t\tst::historyVoiceMessageOutTTL,\n\t\tst::historyVoiceMessageOutTTLSelected);\n\tmake(\n\t\t&MessageStyle::liveLocationLongIcon,\n\t\tst::liveLocationLongInIcon,\n\t\tst::liveLocationLongInIconSelected,\n\t\tst::liveLocationLongOutIcon,\n\t\tst::liveLocationLongOutIconSelected);\n\n\tupdateDarkValue();\n}\n\nChatStyle::ChatStyle(not_null<const style::palette*> isolated)\n: ChatStyle(rpl::producer<ColorIndicesCompressed>()) {\n\tassignPalette(isolated);\n}\n\nChatStyle::~ChatStyle() = default;\n\nvoid ChatStyle::apply(not_null<ChatTheme*> theme) {\n\tapplyCustomPalette(theme->palette());\n}\n\nvoid ChatStyle::updateDarkValue() {\n\tconst auto withBg = [&](const QColor &color) {\n\t\treturn CountContrast(windowBg()->c, color);\n\t};\n\tconst auto dark = (withBg({ 0, 0, 0 }) < withBg({ 255, 255, 255 }));\n\tif (_dark != dark) {\n\t\t_dark = dark;\n\t\t_collectibleCaches.clear();\n\t}\n}\n\nvoid ChatStyle::applyCustomPalette(const style::palette *palette) {\n\tassignPalette(palette ? palette : style::main_palette::get().get());\n\tif (palette) {\n\t\t_defaultPaletteChangeLifetime.destroy();\n\t} else {\n\t\tstyle::PaletteChanged(\n\t\t) | rpl::on_next([=] {\n\t\t\tassignPalette(style::main_palette::get());\n\t\t}, _defaultPaletteChangeLifetime);\n\t}\n}\n\nvoid ChatStyle::applyAdjustedServiceBg(QColor serviceBg) {\n\tauto r = 0, g = 0, b = 0, a = 0;\n\tserviceBg.getRgb(&r, &g, &b, &a);\n\tmsgServiceBg().set(uchar(r), uchar(g), uchar(b), uchar(a));\n}\n\nstd::span<Text::SpecialColor> ChatStyle::highlightColors() const {\n\tif (_highlightColors.empty()) {\n\t\tconst auto push = [&](const style::color &color) {\n\t\t\t_highlightColors.push_back({ &color->p, &color->p });\n\t\t};\n\n\t\t// comment, block-comment, prolog, doctype, cdata\n\t\tpush(statisticsChartLineLightblue());\n\n\t\t// punctuation\n\t\tpush(statisticsChartLineRed());\n\n\t\t// property, tag, boolean, number,\n\t\t// constant, symbol, deleted\n\t\tpush(statisticsChartLineRed());\n\n\t\t// selector, attr-name, string, char, builtin\n\t\tpush(statisticsChartLineOrange());\n\n\t\t// operator, entity, url\n\t\tpush(statisticsChartLineRed());\n\n\t\t// atrule, attr-value, keyword, function\n\t\tpush(statisticsChartLineBlue());\n\n\t\t// class-name\n\t\tpush(statisticsChartLinePurple());\n\n\t\t// inserted\n\t\tpush(statisticsChartLineGreen());\n\t\t//push(statisticsChartLineLightgreen());\n\t\t//push(statisticsChartLineGolden());\n\t}\n\treturn _highlightColors;\n}\n\nvoid ChatStyle::clearColorIndexCaches() {\n\tfor (auto &style : _messageStyles) {\n\t\tfor (auto &cache : style.quoteCache) {\n\t\t\tcache = nullptr;\n\t\t}\n\t\tfor (auto &cache : style.replyCache) {\n\t\t\tcache = nullptr;\n\t\t}\n\t}\n\tfor (auto &values : _coloredValues) {\n\t\tvalues.reset();\n\t}\n\tfor (auto &palette : _coloredTextPalettes) {\n\t\tpalette.linkFg.reset();\n\t}\n\tfor (auto &cache : _coloredReplyCaches) {\n\t\tcache = nullptr;\n\t}\n\tfor (auto &cache : _coloredQuoteCaches) {\n\t\tcache = nullptr;\n\t}\n}\n\nvoid ChatStyle::assignPalette(not_null<const style::palette*> palette) {\n\t*static_cast<style::palette*>(this) = *palette;\n\tstyle::internal::ResetIcons();\n\n\tclearColorIndexCaches();\n\tfor (auto &style : _messageStyles) {\n\t\tstyle.msgBgCornersSmall = {};\n\t\tstyle.msgBgCornersLarge = {};\n\t\tstyle.preCache = nullptr;\n\t\tstyle.textPalette.linkAlwaysActive\n\t\t\t= style.semiboldPalette.linkAlwaysActive\n\t\t\t= (style.textPalette.linkFg->c == style.historyTextFg->c);\n\t}\n\tfor (auto &style : _imageStyles) {\n\t\tstyle.msgDateImgBgCorners = {};\n\t\tstyle.msgServiceBgCornersSmall = {};\n\t\tstyle.msgServiceBgCornersLarge = {};\n\t\tstyle.msgShadowCornersSmall = {};\n\t\tstyle.msgShadowCornersLarge = {};\n\t}\n\t_serviceBgCornersNormal = {};\n\t_serviceBgCornersInverted = {};\n\t_msgBotKbOverBgAddCornersSmall = {};\n\t_msgBotKbOverBgAddCornersLarge = {};\n\tfor (auto &corners : _msgSelectOverlayCorners) {\n\t\tcorners = {};\n\t}\n\tupdateDarkValue();\n\n\t_paletteChanged.fire({});\n}\n\nconst CornersPixmaps &ChatStyle::serviceBgCornersNormal() const {\n\tEnsureCorners(\n\t\t_serviceBgCornersNormal,\n\t\tHistoryServiceMsgRadius(),\n\t\tmsgServiceBg());\n\treturn _serviceBgCornersNormal;\n}\n\nconst CornersPixmaps &ChatStyle::serviceBgCornersInverted() const {\n\tif (_serviceBgCornersInverted.p[0].isNull()) {\n\t\t_serviceBgCornersInverted = PrepareInvertedCornerPixmaps(\n\t\t\tHistoryServiceMsgInvertedRadius(),\n\t\t\tmsgServiceBg());\n\t}\n\treturn _serviceBgCornersInverted;\n}\n\nconst MessageStyle &ChatStyle::messageStyle(bool outbg, bool selected) const {\n\tauto &result = messageStyleRaw(outbg, selected);\n\tEnsureCorners(\n\t\tresult.msgBgCornersSmall,\n\t\tBubbleRadiusSmall(),\n\t\tresult.msgBg,\n\t\t&result.msgShadow);\n\tEnsureCorners(\n\t\tresult.msgBgCornersLarge,\n\t\tBubbleRadiusLarge(),\n\t\tresult.msgBg,\n\t\t&result.msgShadow);\n\tconst auto &replyBar = result.msgReplyBarColor->c;\n\tfor (auto i = 0; i != kColorPatternsCount; ++i) {\n\t\tEnsureBlockquoteCache(\n\t\t\tresult.replyCache[i],\n\t\t\t[&] { return SimpleColorIndexValues(replyBar, i); });\n\t\tif (!result.quoteCache[i]) {\n\t\t\tresult.quoteCache[i] = std::make_unique<Text::QuotePaintCache>(\n\t\t\t\t*result.replyCache[i]);\n\t\t}\n\t}\n\n\tconst auto preBgOverride = [&] {\n\t\treturn _dark ? QColor(0, 0, 0, 192) : std::optional<QColor>();\n\t};\n\tEnsurePreCache(\n\t\tresult.preCache,\n\t\t(selected\n\t\t\t? result.textPalette.selectMonoFg\n\t\t\t: result.textPalette.monoFg),\n\t\tpreBgOverride);\n\treturn result;\n}\n\nconst MessageImageStyle &ChatStyle::imageStyle(bool selected) const {\n\tauto &result = imageStyleRaw(selected);\n\tEnsureCorners(\n\t\tresult.msgDateImgBgCorners,\n\t\t(st::msgDateImgPadding.y() * 2 + st::normalFont->height) / 2,\n\t\tresult.msgDateImgBg);\n\tEnsureCorners(\n\t\tresult.msgServiceBgCornersSmall,\n\t\tBubbleRadiusSmall(),\n\t\tresult.msgServiceBg);\n\tEnsureCorners(\n\t\tresult.msgServiceBgCornersLarge,\n\t\tBubbleRadiusLarge(),\n\t\tresult.msgServiceBg);\n\tEnsureCorners(\n\t\tresult.msgShadowCornersSmall,\n\t\tBubbleRadiusSmall(),\n\t\tresult.msgShadow);\n\tEnsureCorners(\n\t\tresult.msgShadowCornersLarge,\n\t\tBubbleRadiusLarge(),\n\t\tresult.msgShadow);\n\n\treturn result;\n}\n\nint ChatStyle::colorPatternIndex(uint8 colorIndex) const {\n\tExpects(colorIndex >= 0 && colorIndex < kColorIndexCount);\n\n\tif (!_colorIndices.colors\n\t\t|| colorIndex < kSimpleColorIndexCount) {\n\t\treturn 0;\n\t}\n\tauto &data = (*_colorIndices.colors)[colorIndex];\n\tauto &colors = _dark ? data.dark : data.light;\n\treturn colors[2] ? 2 : colors[1] ? 1 : 0;\n}\n\nint ChatStyle::collectiblePatternIndex(\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const {\n\tconst auto &strip = (_dark && !collectible->darkStrip.empty())\n\t\t? collectible->darkStrip\n\t\t: collectible->strip;\n\treturn std::clamp(int(strip.size()), 1, 3) - 1;\n}\n\nColorIndexValues ChatStyle::computeColorIndexValues(\n\t\tbool selected,\n\t\tuint8 colorIndex) const {\n\tif (!_colorIndices.colors) {\n\t\tcolorIndex %= kSimpleColorIndexCount;\n\t}\n\tif (colorIndex < kSimpleColorIndexCount) {\n\t\tconst auto list = std::array{\n\t\t\t&historyPeer1NameFg(),\n\t\t\t&historyPeer2NameFg(),\n\t\t\t&historyPeer3NameFg(),\n\t\t\t&historyPeer4NameFg(),\n\t\t\t&historyPeer5NameFg(),\n\t\t\t&historyPeer6NameFg(),\n\t\t\t&historyPeer7NameFg(),\n\t\t\t&historyPeer8NameFg(),\n\t\t};\n\t\tconst auto listSelected = std::array{\n\t\t\t&historyPeer1NameFgSelected(),\n\t\t\t&historyPeer2NameFgSelected(),\n\t\t\t&historyPeer3NameFgSelected(),\n\t\t\t&historyPeer4NameFgSelected(),\n\t\t\t&historyPeer5NameFgSelected(),\n\t\t\t&historyPeer6NameFgSelected(),\n\t\t\t&historyPeer7NameFgSelected(),\n\t\t\t&historyPeer8NameFgSelected(),\n\t\t};\n\t\tconst auto paletteIndex = ColorIndexToPaletteIndex(colorIndex);\n\t\tauto result = ColorIndexValues{\n\t\t\t.name = (*(selected ? listSelected : list)[paletteIndex])->c,\n\t\t};\n\t\tresult.bg = result.name;\n\t\tresult.bg.setAlpha(kDefaultBgOpacity * 255);\n\t\tresult.outlines[0] = result.name;\n\t\tresult.outlines[0].setAlpha(kDefaultOutline1Opacity * 255);\n\t\tresult.outlines[1] = result.outlines[2] = QColor(0, 0, 0, 0);\n\t\treturn result;\n\t}\n\tauto &data = (*_colorIndices.colors)[colorIndex];\n\tauto &colors = _dark ? data.dark : data.light;\n\tif (!colors[0]) {\n\t\treturn computeColorIndexValues(\n\t\t\tselected,\n\t\t\tcolorIndex % kSimpleColorIndexCount);\n\t}\n\tconst auto color = [&](int index) {\n\t\tconst auto v = colors[index];\n\t\treturn v ? Ui::ColorFromSerialized(v) : QColor(0, 0, 0, 0);\n\t};\n\tauto result = ColorIndexValues{\n\t\t.outlines = { color(0), color(1), color(2) }\n\t};\n\tresult.bg = result.outlines[0];\n\tresult.bg.setAlpha(kDefaultBgOpacity * 255);\n\tresult.name = result.outlines[0];\n\treturn result;\n}\n\nnot_null<Text::QuotePaintCache*> ChatStyle::serviceQuoteCache(\n\t\tbool twoColored) const {\n\tconst auto index = (twoColored ? 1 : 0);\n\tconst auto &service = msgServiceFg()->c;\n\tEnsureBlockquoteCache(\n\t\t_serviceQuoteCache[index],\n\t\t[&] { return SimpleColorIndexValues(service, twoColored); });\n\treturn _serviceQuoteCache[index].get();\n}\n\nnot_null<Text::QuotePaintCache*> ChatStyle::serviceReplyCache(\n\t\tbool twoColored) const {\n\tconst auto index = (twoColored ? 1 : 0);\n\tconst auto &service = msgServiceFg()->c;\n\tEnsureBlockquoteCache(\n\t\t_serviceReplyCache[index],\n\t\t[&] { return SimpleColorIndexValues(service, twoColored); });\n\treturn _serviceReplyCache[index].get();\n}\n\nconst ColorIndexValues &ChatStyle::coloredValues(\n\t\tbool selected,\n\t\tuint8 colorIndex) const {\n\tExpects(colorIndex >= 0 && colorIndex < kColorIndexCount);\n\n\tconst auto shift = (selected ? kColorIndexCount : 0);\n\tauto &result = _coloredValues[shift + colorIndex];\n\tif (!result) {\n\t\tresult.emplace(computeColorIndexValues(selected, colorIndex));\n\t}\n\treturn *result;\n}\n\nQColor ChatStyle::collectibleNameColor(\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const {\n\treturn (_dark && collectible->darkAccentColor.alpha() > 0)\n\t\t? collectible->darkAccentColor\n\t\t: collectible->accentColor;\n}\n\nconst style::TextPalette &ChatStyle::coloredTextPalette(\n\t\tbool selected,\n\t\tuint8 colorIndex) const {\n\tExpects(colorIndex >= 0 && colorIndex < kColorIndexCount);\n\n\tconst auto shift = (selected ? kColorIndexCount : 0);\n\tauto &result = _coloredTextPalettes[shift + colorIndex];\n\tif (!result.linkFg) {\n\t\tresult.linkFg.emplace(coloredValues(selected, colorIndex).name);\n\t\tmake(\n\t\t\tresult.data,\n\t\t\t(selected\n\t\t\t\t? st::inReplyTextPaletteSelected\n\t\t\t\t: st::inReplyTextPalette));\n\t\tresult.data.linkFg = result.linkFg->color();\n\t\tresult.data.selectLinkFg = result.data.linkFg;\n\t}\n\treturn result.data;\n}\n\nconst style::TextPalette &ChatStyle::collectibleTextPalette(\n\t\tbool selected,\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const {\n\tauto &entry = resolveCollectibleCaches(collectible);\n\tauto &result = selected ? entry.paletteSelected : entry.palette;\n\tif (!result.linkFg) {\n\t\tresult.linkFg.emplace(collectibleNameColor(collectible));\n\t\tmake(\n\t\t\tresult.data,\n\t\t\t(selected\n\t\t\t\t? st::inReplyTextPaletteSelected\n\t\t\t\t: st::inReplyTextPalette));\n\t\tresult.data.linkFg = result.linkFg->color();\n\t\tresult.data.selectLinkFg = result.data.linkFg;\n\t}\n\treturn result.data;\n}\n\nnot_null<BackgroundEmojiData*> ChatStyle::backgroundEmojiData(\n\t\tuint64 emojiId,\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const {\n\tconst auto id = collectible ? collectible->collectibleId : emojiId;\n\treturn &_backgroundEmojis[id];\n}\n\nnot_null<Text::QuotePaintCache*> ChatStyle::coloredQuoteCache(\n\t\tbool selected,\n\t\tuint8 colorIndex) const {\n\treturn coloredCache(_coloredQuoteCaches, selected, colorIndex);\n}\n\nnot_null<Text::QuotePaintCache*> ChatStyle::coloredReplyCache(\n\t\tbool selected,\n\t\tuint8 colorIndex) const {\n\treturn coloredCache(_coloredReplyCaches, selected, colorIndex);\n}\n\nnot_null<Text::QuotePaintCache*> ChatStyle::collectibleQuoteCache(\n\t\tbool selected,\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const {\n\tauto &entry = resolveCollectibleCaches(collectible);\n\treturn collectibleCache(\n\t\tselected ? entry.quoteSelected : entry.quote,\n\t\tcollectible);\n}\n\nnot_null<Text::QuotePaintCache*> ChatStyle::collectibleReplyCache(\n\t\tbool selected,\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const {\n\tauto &entry = resolveCollectibleCaches(collectible);\n\treturn collectibleCache(\n\t\tselected ? entry.replySelected : entry.reply,\n\t\tcollectible);\n}\n\nnot_null<Text::QuotePaintCache*> ChatStyle::coloredCache(\n\t\tColoredQuotePaintCaches &caches,\n\t\tbool selected,\n\t\tuint8 colorIndex) const {\n\tExpects(colorIndex >= 0 && colorIndex < kColorIndexCount);\n\n\tconst auto shift = (selected ? kColorIndexCount : 0);\n\tauto &cache = caches[shift + colorIndex];\n\tEnsureBlockquoteCache(cache, [&] {\n\t\treturn coloredValues(selected, colorIndex);\n\t});\n\treturn cache.get();\n}\n\nnot_null<Text::QuotePaintCache*> ChatStyle::collectibleCache(\n\t\tstd::unique_ptr<Text::QuotePaintCache> &cache,\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const {\n\tEnsureBlockquoteCache(cache, [&] {\n\t\tconst auto name = collectibleNameColor(collectible);\n\t\tauto bg = name;\n\t\tbg.setAlpha(kDefaultBgOpacity * 255);\n\n\t\tconst auto &strip = (_dark && !collectible->darkStrip.empty())\n\t\t\t? collectible->darkStrip\n\t\t\t: collectible->strip;\n\t\treturn ColorIndexValues{\n\t\t\t.outlines = {\n\t\t\t\tstrip.empty() ? name : strip[0],\n\t\t\t\t(strip.size() < 2) ? QColor(0, 0, 0, 0) : strip[1],\n\t\t\t\t(strip.size() < 3) ? QColor(0, 0, 0, 0) : strip[2],\n\t\t\t},\n\t\t\t.name = name,\n\t\t\t.bg = bg,\n\t\t};\n\t});\n\treturn cache.get();\n}\n\nChatStyle::CollectibleColors &ChatStyle::resolveCollectibleCaches(\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const {\n\tconst auto i = _collectibleCaches.find(collectible);\n\tif (i != end(_collectibleCaches)) {\n\t\treturn i->second;\n\t}\n\tfor (auto i = begin(_collectibleCaches); i != end(_collectibleCaches);) {\n\t\tif (i->first.expired()) {\n\t\t\ti = _collectibleCaches.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\treturn _collectibleCaches.emplace(collectible).first->second;\n}\n\nconst CornersPixmaps &ChatStyle::msgBotKbOverBgAddCornersSmall() const {\n\tEnsureCorners(\n\t\t_msgBotKbOverBgAddCornersSmall,\n\t\tBubbleRadiusSmall(),\n\t\tmsgBotKbOverBgAdd());\n\treturn _msgBotKbOverBgAddCornersSmall;\n}\n\nconst CornersPixmaps &ChatStyle::msgBotKbOverBgAddCornersLarge() const {\n\tEnsureCorners(\n\t\t_msgBotKbOverBgAddCornersLarge,\n\t\tBubbleRadiusLarge(),\n\t\tmsgBotKbOverBgAdd());\n\treturn _msgBotKbOverBgAddCornersLarge;\n}\n\nconst CornersPixmaps &ChatStyle::msgSelectOverlayCorners(\n\t\tCachedCornerRadius radius) const {\n\tconst auto index = static_cast<int>(radius);\n\tAssert(index >= 0 && index < int(CachedCornerRadius::kCount));\n\n\tEnsureCorners(\n\t\t_msgSelectOverlayCorners[index],\n\t\tCachedCornerRadiusValue(radius),\n\t\tmsgSelectOverlay());\n\treturn _msgSelectOverlayCorners[index];\n}\n\nMessageStyle &ChatStyle::messageStyleRaw(bool outbg, bool selected) const {\n\treturn _messageStyles[(outbg ? 2 : 0) + (selected ? 1 : 0)];\n}\n\nMessageStyle &ChatStyle::messageIn() {\n\treturn messageStyleRaw(false, false);\n}\n\nMessageStyle &ChatStyle::messageInSelected() {\n\treturn messageStyleRaw(false, true);\n}\n\nMessageStyle &ChatStyle::messageOut() {\n\treturn messageStyleRaw(true, false);\n}\n\nMessageStyle &ChatStyle::messageOutSelected() {\n\treturn messageStyleRaw(true, true);\n}\n\nMessageImageStyle &ChatStyle::imageStyleRaw(bool selected) const {\n\treturn _imageStyles[selected ? 1 : 0];\n}\n\nMessageImageStyle &ChatStyle::image() {\n\treturn imageStyleRaw(false);\n}\n\nMessageImageStyle &ChatStyle::imageSelected() {\n\treturn imageStyleRaw(true);\n}\n\nvoid ChatStyle::make(style::color &my, const style::color &original) const {\n\tmy = _colors[style::main_palette::indexOfColor(original)];\n}\n\nvoid ChatStyle::make(style::icon &my, const style::icon &original) const {\n\tmy = original.withPalette(*this);\n}\n\nvoid ChatStyle::make(\n\t\tstyle::TextPalette &my,\n\t\tconst style::TextPalette &original) const {\n\tmy.linkAlwaysActive = original.linkAlwaysActive;\n\tmake(my.linkFg, original.linkFg);\n\tmake(my.monoFg, original.monoFg);\n\tmake(my.spoilerFg, original.spoilerFg);\n\tmake(my.selectBg, original.selectBg);\n\tmake(my.selectFg, original.selectFg);\n\tmake(my.selectLinkFg, original.selectLinkFg);\n\tmake(my.selectMonoFg, original.selectMonoFg);\n\tmake(my.selectSpoilerFg, original.selectSpoilerFg);\n\tmake(my.selectOverlay, original.selectOverlay);\n}\n\nvoid ChatStyle::make(\n\t\tstyle::TwoIconButton &my,\n\t\tconst style::TwoIconButton &original) const {\n\tmy = original;\n\tmake(my.iconBelow, original.iconBelow);\n\tmake(my.iconAbove, original.iconAbove);\n\tmake(my.iconBelowOver, original.iconBelowOver);\n\tmake(my.iconAboveOver, original.iconAboveOver);\n\tmake(my.ripple.color, original.ripple.color);\n}\n\nvoid ChatStyle::make(\n\t\tstyle::ScrollArea &my,\n\t\tconst style::ScrollArea &original) const {\n\tmy = original;\n\tmake(my.bg, original.bg);\n\tmake(my.bgOver, original.bgOver);\n\tmake(my.barBg, original.barBg);\n\tmake(my.barBgOver, original.barBgOver);\n\tmake(my.shColor, original.shColor);\n}\n\ntemplate <typename Type>\nvoid ChatStyle::make(\n\t\tType MessageStyle::*my,\n\t\tconst Type &originalIn,\n\t\tconst Type &originalInSelected,\n\t\tconst Type &originalOut,\n\t\tconst Type &originalOutSelected) {\n\tmake(messageIn().*my, originalIn);\n\tmake(messageInSelected().*my, originalInSelected);\n\tmake(messageOut().*my, originalOut);\n\tmake(messageOutSelected().*my, originalOutSelected);\n}\n\ntemplate <typename Type>\nvoid ChatStyle::make(\n\t\tType MessageImageStyle::*my,\n\t\tconst Type &original,\n\t\tconst Type &originalSelected) {\n\tmake(image().*my, original);\n\tmake(imageSelected().*my, originalSelected);\n}\n\nuint8 DecideColorIndex(uint64 id) {\n\treturn id % kSimpleColorIndexCount;\n}\n\nuint8 ColorIndexToPaletteIndex(uint8 colorIndex) {\n\tExpects(colorIndex >= 0 && colorIndex < kColorIndexCount);\n\n\tconst int8 map[] = { 0, 7, 4, 1, 6, 3, 5 };\n\treturn map[colorIndex % kSimpleColorIndexCount];\n}\n\nQColor FromNameFg(\n\t\tnot_null<const ChatStyle*> st,\n\t\tbool selected,\n\t\tuint8 colorIndex,\n\t\tconst std::shared_ptr<Ui::ColorCollectible> &colorCollectible) {\n\treturn !colorCollectible\n\t\t? st->coloredValues(selected, colorIndex).name\n\t\t: (st->dark() && (colorCollectible->darkAccentColor.alpha() > 0))\n\t\t? colorCollectible->darkAccentColor\n\t\t: colorCollectible->accentColor;\n}\n\nvoid FillComplexOverlayRect(\n\t\tQPainter &p,\n\t\tQRect rect,\n\t\tconst style::color &color,\n\t\tconst CornersPixmaps &corners) {\n\tusing namespace Images;\n\n\tconst auto pix = corners.p;\n\tconst auto fillRect = [&](QRect rect) {\n\t\tp.fillRect(rect, color);\n\t};\n\tif (pix[kTopLeft].isNull()\n\t\t&& pix[kTopRight].isNull()\n\t\t&& pix[kBottomLeft].isNull()\n\t\t&& pix[kBottomRight].isNull()) {\n\t\tfillRect(rect);\n\t\treturn;\n\t}\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto fillCorner = [&](int left, int top, int index) {\n\t\tp.drawPixmap(left, top, pix[index]);\n\t};\n\tconst auto cornerSize = [&](int index) {\n\t\tconst auto &p = pix[index];\n\t\treturn p.isNull() ? 0 : p.width() / ratio;\n\t};\n\tconst auto verticalSkip = [&](int left, int right) {\n\t\treturn std::max(cornerSize(left), cornerSize(right));\n\t};\n\tconst auto top = verticalSkip(kTopLeft, kTopRight);\n\tconst auto bottom = verticalSkip(kBottomLeft, kBottomRight);\n\tif (top) {\n\t\tconst auto left = cornerSize(kTopLeft);\n\t\tconst auto right = cornerSize(kTopRight);\n\t\tif (left) {\n\t\t\tfillCorner(rect.left(), rect.top(), kTopLeft);\n\t\t\tif (const auto add = top - left) {\n\t\t\t\tfillRect({ rect.left(), rect.top() + left, left, add });\n\t\t\t}\n\t\t}\n\t\tif (const auto fill = rect.width() - left - right; fill > 0) {\n\t\t\tfillRect({ rect.left() + left, rect.top(), fill, top });\n\t\t}\n\t\tif (right) {\n\t\t\tfillCorner(\n\t\t\t\trect.left() + rect.width() - right,\n\t\t\t\trect.top(),\n\t\t\t\tkTopRight);\n\t\t\tif (const auto add = top - right) {\n\t\t\t\tfillRect({\n\t\t\t\t\trect.left() + rect.width() - right,\n\t\t\t\t\trect.top() + right,\n\t\t\t\t\tright,\n\t\t\t\t\tadd,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\tif (const auto h = rect.height() - top - bottom; h > 0) {\n\t\tfillRect({ rect.left(), rect.top() + top, rect.width(), h });\n\t}\n\tif (bottom) {\n\t\tconst auto left = cornerSize(kBottomLeft);\n\t\tconst auto right = cornerSize(kBottomRight);\n\t\tif (left) {\n\t\t\tfillCorner(\n\t\t\t\trect.left(),\n\t\t\t\trect.top() + rect.height() - left,\n\t\t\t\tkBottomLeft);\n\t\t\tif (const auto add = bottom - left) {\n\t\t\t\tfillRect({\n\t\t\t\t\trect.left(),\n\t\t\t\t\trect.top() + rect.height() - bottom,\n\t\t\t\t\tleft,\n\t\t\t\t\tadd,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tif (const auto fill = rect.width() - left - right; fill > 0) {\n\t\t\tfillRect({\n\t\t\t\trect.left() + left,\n\t\t\t\trect.top() + rect.height() - bottom,\n\t\t\t\tfill,\n\t\t\t\tbottom,\n\t\t\t});\n\t\t}\n\t\tif (right) {\n\t\t\tfillCorner(\n\t\t\t\trect.left() + rect.width() - right,\n\t\t\t\trect.top() + rect.height() - right,\n\t\t\t\tkBottomRight);\n\t\t\tif (const auto add = bottom - right) {\n\t\t\t\tfillRect({\n\t\t\t\t\trect.left() + rect.width() - right,\n\t\t\t\t\trect.top() + rect.height() - bottom,\n\t\t\t\t\tright,\n\t\t\t\t\tadd,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid FillComplexEllipse(\n\t\tQPainter &p,\n\t\tnot_null<const ChatStyle*> st,\n\t\tQRect rect) {\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(st->msgSelectOverlay());\n\tp.drawEllipse(rect);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/chat_style.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/cached_round_corners.h\"\n#include \"ui/chat/message_bubble.h\"\n#include \"ui/chat/chat_style_radius.h\"\n#include \"ui/controls/swipe_handler_data.h\"\n#include \"ui/style/style_core_palette.h\"\n#include \"layout/layout_selection.h\"\n#include \"styles/style_basic.h\"\n\nenum class ImageRoundRadius;\n\nnamespace style {\nstruct TwoIconButton;\nstruct ScrollArea;\n} // namespace style\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nnamespace Ui {\n\nclass ChatTheme;\nclass ChatStyle;\nstruct BubblePattern;\n\ninline constexpr auto kColorPatternsCount = Text::kMaxQuoteOutlines;\ninline constexpr auto kColorIndexCount = uint8(1 << 6);\ninline constexpr auto kSimpleColorIndexCount = uint8(7);\n\ninline constexpr auto kDefaultBgOpacity = 0.12;\ninline constexpr auto kDefaultOutline1Opacity = 0.9;\ninline constexpr auto kDefaultOutline2Opacity = 0.3;\ninline constexpr auto kDefaultOutline3Opacity = 0.6;\ninline constexpr auto kDefaultOutlineOpacitySecond = 0.5;\n\nstruct MessageStyle {\n\tCornersPixmaps msgBgCornersSmall;\n\tCornersPixmaps msgBgCornersLarge;\n\tstyle::color msgBg;\n\tstyle::color msgShadow;\n\tstyle::color msgServiceFg;\n\tstyle::color msgDateFg;\n\tstyle::color msgFileThumbLinkFg;\n\tstyle::color msgFileBg;\n\tstyle::color msgReplyBarColor;\n\tstyle::color msgWaveformActive;\n\tstyle::color msgWaveformInactive;\n\tstyle::color historyTextFg;\n\tstyle::color historyFileNameFg;\n\tstyle::color historyFileRadialFg;\n\tstyle::color mediaFg;\n\tstyle::TextPalette textPalette;\n\tstyle::TextPalette semiboldPalette;\n\tstyle::TextPalette fwdTextPalette;\n\tstyle::TextPalette replyTextPalette;\n\tstyle::icon tailLeft = { Qt::Uninitialized };\n\tstyle::icon tailRight = { Qt::Uninitialized };\n\tstyle::icon historyRepliesIcon = { Qt::Uninitialized };\n\tstyle::icon historyViewsIcon = { Qt::Uninitialized };\n\tstyle::icon historyPinIcon = { Qt::Uninitialized };\n\tstyle::icon historySentIcon = { Qt::Uninitialized };\n\tstyle::icon historyReceivedIcon = { Qt::Uninitialized };\n\tstyle::icon historyPsaIcon = { Qt::Uninitialized };\n\tstyle::icon historyCommentsOpen = { Qt::Uninitialized };\n\tstyle::icon historyComments = { Qt::Uninitialized };\n\tstyle::icon historyCallArrow = { Qt::Uninitialized };\n\tstyle::icon historyCallArrowMissed = { Qt::Uninitialized };\n\tstyle::icon historyCallIcon = { Qt::Uninitialized };\n\tstyle::icon historyCallCameraIcon = { Qt::Uninitialized };\n\tstyle::icon historyCallGroupIcon = { Qt::Uninitialized };\n\tstyle::icon historyFilePlay = { Qt::Uninitialized };\n\tstyle::icon historyFileWaiting = { Qt::Uninitialized };\n\tstyle::icon historyFileDownload = { Qt::Uninitialized };\n\tstyle::icon historyFileCancel = { Qt::Uninitialized };\n\tstyle::icon historyFilePause = { Qt::Uninitialized };\n\tstyle::icon historyFileImage = { Qt::Uninitialized };\n\tstyle::icon historyFileDocument = { Qt::Uninitialized };\n\tstyle::icon historyAudioDownload = { Qt::Uninitialized };\n\tstyle::icon historyAudioCancel = { Qt::Uninitialized };\n\tstyle::icon historyQuizTimer = { Qt::Uninitialized };\n\tstyle::icon historyQuizExplain = { Qt::Uninitialized };\n\tstyle::icon historyPollChosen = { Qt::Uninitialized };\n\tstyle::icon historyPollChoiceRight = { Qt::Uninitialized };\n\tstyle::icon historyTranscribeIcon = { Qt::Uninitialized };\n\tstyle::icon historyTranscribeLock = { Qt::Uninitialized };\n\tstyle::icon historyTranscribeHide = { Qt::Uninitialized };\n\tstyle::icon historyVoiceMessageTTL = { Qt::Uninitialized };\n\tstyle::icon liveLocationLongIcon = { Qt::Uninitialized };\n\tstd::array<\n\t\tstd::unique_ptr<Text::QuotePaintCache>,\n\t\tkColorPatternsCount> quoteCache;\n\tstd::array<\n\t\tstd::unique_ptr<Text::QuotePaintCache>,\n\t\tkColorPatternsCount> replyCache;\n\tstd::unique_ptr<Text::QuotePaintCache> preCache;\n};\n\nstruct MessageImageStyle {\n\tCornersPixmaps msgDateImgBgCorners;\n\tCornersPixmaps msgServiceBgCornersSmall;\n\tCornersPixmaps msgServiceBgCornersLarge;\n\tCornersPixmaps msgShadowCornersSmall;\n\tCornersPixmaps msgShadowCornersLarge;\n\tstyle::color msgServiceBg;\n\tstyle::color msgDateImgBg;\n\tstyle::color msgShadow;\n\tstyle::color historyFileThumbRadialFg;\n\tstyle::icon historyFileThumbPlay = { Qt::Uninitialized };\n\tstyle::icon historyFileThumbWaiting = { Qt::Uninitialized };\n\tstyle::icon historyFileThumbDownload = { Qt::Uninitialized };\n\tstyle::icon historyFileThumbCancel = { Qt::Uninitialized };\n\tstyle::icon historyFileThumbPause = { Qt::Uninitialized };\n\tstyle::icon historyVideoDownload = { Qt::Uninitialized };\n\tstyle::icon historyVideoCancel = { Qt::Uninitialized };\n\tstyle::icon historyVideoMessageMute = { Qt::Uninitialized };\n\tstyle::icon historyVideoMessageTtlIcon = { Qt::Uninitialized };\n\tstyle::icon historyPageEnlarge = { Qt::Uninitialized };\n};\n\nstruct ColorCollectible {\n\tuint64 collectibleId = 0;\n\tuint64 giftEmojiId = 0;\n\tuint64 backgroundEmojiId = 0;\n\tQColor accentColor;\n\tstd::vector<QColor> strip;\n\tQColor darkAccentColor;\n\tstd::vector<QColor> darkStrip;\n\n\tfriend inline bool operator==(\n\t\tconst ColorCollectible &,\n\t\tconst ColorCollectible &) = default;\n};\n\nstruct ColorCollectiblePtrCompare {\n\tbool operator()(\n\t\t\tconst std::weak_ptr<ColorCollectible> &a,\n\t\t\tconst std::weak_ptr<ColorCollectible> &b) const {\n\t\treturn a.owner_before(b);\n\t}\n};\n\nstruct ReactionPaintInfo {\n\tQPoint position;\n\tQPoint effectOffset;\n\tFn<QRect(QPainter&)> effectPaint;\n};\n\nstruct BackgroundEmojiCache {\n\tQColor color;\n\tstd::array<QImage, 3> frames;\n};\n\nstruct BackgroundEmojiData {\n\tstd::unique_ptr<Text::CustomEmoji> emoji;\n\tQImage firstFrameMask;\n\tbase::flat_map<int, BackgroundEmojiCache> caches;\n\tbase::flat_map<\n\t\tstd::weak_ptr<ColorCollectible>,\n\t\tBackgroundEmojiCache,\n\t\tColorCollectiblePtrCompare> collectibleCaches;\n\tstd::unique_ptr<Text::CustomEmoji> gift;\n\tQImage firstGiftFrame;\n\n\t[[nodiscard]] static int CacheIndex(\n\t\tbool selected,\n\t\tbool outbg,\n\t\tbool inbubble,\n\t\tuint8 colorIndexPlusOne);\n};\n\nstruct ChatPaintHighlight {\n\tfloat64 opacity = 0.;\n\tfloat64 collapsion = 0.;\n\tTextSelection range;\n\tint todoItemId = 0;\n\tQByteArray pollOption;\n};\n\nstruct ChatPaintContext {\n\tnot_null<const ChatStyle*> st;\n\tconst BubblePattern *bubblesPattern = nullptr;\n\tReactionPaintInfo *reactionInfo = nullptr;\n\tQRect viewport;\n\tQRect area;\n\tQRect clip;\n\tTextSelection selection;\n\tChatPaintHighlight highlight;\n\tQPainterPath *highlightPathCache = nullptr;\n\tmutable QRect highlightInterpolateTo;\n\tcrl::time now = 0;\n\tUi::Controls::SwipeContextData gestureHorizontal;\n\n\tvoid translate(int x, int y) {\n\t\tviewport.translate(x, y);\n\t\tarea.translate(x, y);\n\t\tclip.translate(x, y);\n\t\thighlightInterpolateTo.translate(x, y);\n\t}\n\tvoid translate(QPoint point) {\n\t\ttranslate(point.x(), point.y());\n\t}\n\n\t[[nodiscard]] bool selected() const {\n\t\treturn (selection == FullSelection);\n\t}\n\t[[nodiscard]] not_null<const MessageStyle*> messageStyle() const;\n\t[[nodiscard]] not_null<const MessageImageStyle*> imageStyle() const;\n\t[[nodiscard]] not_null<Text::QuotePaintCache*> quoteCache(\n\t\tconst std::shared_ptr<ColorCollectible> &colorCollectible,\n\t\tuint8 colorIndex) const;\n\n\t[[nodiscard]] ChatPaintContext translated(int x, int y) const {\n\t\tauto result = *this;\n\t\tresult.translate(x, y);\n\t\treturn result;\n\t}\n\t[[nodiscard]] ChatPaintContext translated(QPoint point) const {\n\t\treturn translated(point.x(), point.y());\n\t}\n\t[[nodiscard]] ChatPaintContext withSelection(\n\t\t\tTextSelection selection) const {\n\t\tauto result = *this;\n\t\tresult.selection = selection;\n\t\treturn result;\n\t}\n\t[[nodiscard]] auto computeHighlightCache() const\n\t-> std::optional<Ui::Text::HighlightInfoRequest> {\n\t\tif (highlight.range.empty() || highlight.collapsion <= 0.) {\n\t\t\treturn {};\n\t\t}\n\t\treturn Ui::Text::HighlightInfoRequest{\n\t\t\t.range = highlight.range,\n\t\t\t.interpolateTo = highlightInterpolateTo,\n\t\t\t.interpolateProgress = (1. - highlight.collapsion),\n\t\t\t.outPath = highlightPathCache,\n\t\t};\n\t};\n\n\n\t// This is supported only in unwrapped media and text messages for now.\n\tenum class SkipDrawingParts {\n\t\tNone,\n\t\tContent,\n\t\tSurrounding,\n\t\tBubble,\n\t};\n\tSkipDrawingParts skipDrawingParts = SkipDrawingParts::None;\n\n\tbool skipSelectionCheck = false;\n\tbool outbg = false;\n\tbool paused = false;\n\n};\n\nstruct ChatPaintContextArgs {\n\tnot_null<ChatTheme*> theme;\n\tQRect clip;\n\tQPoint visibleAreaPositionGlobal;\n\tint visibleAreaTop = 0;\n\tint visibleAreaWidth = 0;\n\tint visibleAreaHeight = 0;\n};\n\n[[nodiscard]] int HistoryServiceMsgRadius();\n[[nodiscard]] int HistoryServiceMsgInvertedRadius();\n[[nodiscard]] int HistoryServiceMsgInvertedShrink();\n\nstruct ColorIndexData {\n\tstd::array<uint32, kColorPatternsCount> light = {};\n\tstd::array<uint32, kColorPatternsCount> dark = {};\n\n\tfriend inline bool operator==(\n\t\tconst ColorIndexData&,\n\t\tconst ColorIndexData&) = default;\n};\n\nstruct ColorIndicesCompressed {\n\tstd::shared_ptr<std::array<ColorIndexData, kColorIndexCount>> colors;\n};\n\n[[nodiscard]] int ColorPatternIndex(\n\tconst ColorIndicesCompressed &indices,\n\tuint8 colorIndex,\n\tbool dark);\n\nstruct ColorIndexValues {\n\tstd::array<QColor, kColorPatternsCount> outlines;\n\tQColor name;\n\tQColor bg;\n};\n\n[[nodiscard]] ColorIndexValues SimpleColorIndexValues(\n\tQColor color,\n\tint patternIndex);\n\nclass ChatStyle final : public style::palette {\npublic:\n\texplicit ChatStyle(rpl::producer<ColorIndicesCompressed> colorIndices);\n\texplicit ChatStyle(not_null<const style::palette*> isolated);\n\tChatStyle(const ChatStyle &other) = delete;\n\tChatStyle &operator=(const ChatStyle &other) = delete;\n\t~ChatStyle();\n\n\tvoid apply(not_null<ChatTheme*> theme);\n\tvoid applyCustomPalette(const style::palette *palette);\n\tvoid applyAdjustedServiceBg(QColor serviceBg);\n\n\t[[nodiscard]] bool dark() const {\n\t\treturn _dark;\n\t}\n\n\t[[nodiscard]] std::span<Text::SpecialColor> highlightColors() const;\n\n\t[[nodiscard]] rpl::producer<> paletteChanged() const {\n\t\treturn _paletteChanged.events();\n\t}\n\n\ttemplate <typename Type>\n\t[[nodiscard]] Type value(const Type &original) const {\n\t\tauto my = Type();\n\t\tmake(my, original);\n\t\treturn my;\n\t}\n\n\ttemplate <typename Type>\n\t[[nodiscard]] const Type &value(\n\t\t\trpl::lifetime &parentLifetime,\n\t\t\tconst Type &original) const {\n\t\tconst auto my = parentLifetime.make_state<Type>();\n\t\tmake(*my, original);\n\t\treturn *my;\n\t}\n\n\t[[nodiscard]] const CornersPixmaps &serviceBgCornersNormal() const;\n\t[[nodiscard]] const CornersPixmaps &serviceBgCornersInverted() const;\n\n\t[[nodiscard]] const MessageStyle &messageStyle(\n\t\tbool outbg,\n\t\tbool selected) const;\n\t[[nodiscard]] const MessageImageStyle &imageStyle(bool selected) const;\n\n\t[[nodiscard]] int colorPatternIndex(uint8 colorIndex) const;\n\t[[nodiscard]] int collectiblePatternIndex(\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const;\n\t[[nodiscard]] ColorIndexValues computeColorIndexValues(\n\t\tbool selected,\n\t\tuint8 colorIndex) const;\n\n\t[[nodiscard]] auto serviceQuoteCache(bool twoColored) const\n\t\t-> not_null<Text::QuotePaintCache*>;\n\t[[nodiscard]] auto serviceReplyCache(bool twoColored) const\n\t\t-> not_null<Text::QuotePaintCache*>;\n\t[[nodiscard]] const ColorIndexValues &coloredValues(\n\t\tbool selected,\n\t\tuint8 colorIndex) const;\n\t[[nodiscard]] QColor collectibleNameColor(\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const;\n\t[[nodiscard]] not_null<Text::QuotePaintCache*> coloredQuoteCache(\n\t\tbool selected,\n\t\tuint8 colorIndex) const;\n\t[[nodiscard]] not_null<Text::QuotePaintCache*> coloredReplyCache(\n\t\tbool selected,\n\t\tuint8 colorIndex) const;\n\t[[nodiscard]] not_null<Text::QuotePaintCache*> collectibleQuoteCache(\n\t\tbool selected,\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const;\n\t[[nodiscard]] not_null<Text::QuotePaintCache*> collectibleReplyCache(\n\t\tbool selected,\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const;\n\n\t[[nodiscard]] const style::TextPalette &coloredTextPalette(\n\t\tbool selected,\n\t\tuint8 colorIndex) const;\n\t[[nodiscard]] const style::TextPalette &collectibleTextPalette(\n\t\tbool selected,\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const;\n\n\t[[nodiscard]] not_null<BackgroundEmojiData*> backgroundEmojiData(\n\t\tuint64 emojiId,\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const;\n\n\t[[nodiscard]] const CornersPixmaps &msgBotKbOverBgAddCornersSmall() const;\n\t[[nodiscard]] const CornersPixmaps &msgBotKbOverBgAddCornersLarge() const;\n\t[[nodiscard]] const CornersPixmaps &msgSelectOverlayCorners(\n\t\tCachedCornerRadius radius) const;\n\n\t[[nodiscard]] const style::TextPalette &historyPsaForwardPalette() const {\n\t\treturn _historyPsaForwardPalette;\n\t}\n\t[[nodiscard]] const style::TextPalette &imgReplyTextPalette() const {\n\t\treturn _imgReplyTextPalette;\n\t}\n\t[[nodiscard]] const style::TextPalette &serviceTextPalette() const {\n\t\treturn _serviceTextPalette;\n\t}\n\t[[nodiscard]] const style::TextPalette &priceTagTextPalette() const {\n\t\treturn _priceTagTextPalette;\n\t}\n\t[[nodiscard]] const style::icon &historyRepliesInvertedIcon() const {\n\t\treturn _historyRepliesInvertedIcon;\n\t}\n\t[[nodiscard]] const style::icon &historyViewsInvertedIcon() const {\n\t\treturn _historyViewsInvertedIcon;\n\t}\n\t[[nodiscard]] const style::icon &historyViewsSendingIcon() const {\n\t\treturn _historyViewsSendingIcon;\n\t}\n\t[[nodiscard]] const style::icon &historyViewsSendingInvertedIcon() const {\n\t\treturn _historyViewsSendingInvertedIcon;\n\t}\n\t[[nodiscard]] const style::icon &historyPinInvertedIcon() const {\n\t\treturn _historyPinInvertedIcon;\n\t}\n\t[[nodiscard]] const style::icon &historySendingIcon() const {\n\t\treturn _historySendingIcon;\n\t}\n\t[[nodiscard]] const style::icon &historySendingInvertedIcon() const {\n\t\treturn _historySendingInvertedIcon;\n\t}\n\t[[nodiscard]] const style::icon &historySentInvertedIcon() const {\n\t\treturn _historySentInvertedIcon;\n\t}\n\t[[nodiscard]] const style::icon &historyReceivedInvertedIcon() const {\n\t\treturn _historyReceivedInvertedIcon;\n\t}\n\t[[nodiscard]] const style::icon &msgBotKbUrlIcon() const {\n\t\treturn _msgBotKbUrlIcon;\n\t}\n\t[[nodiscard]] const style::icon &msgBotKbPaymentIcon() const {\n\t\treturn _msgBotKbPaymentIcon;\n\t}\n\t[[nodiscard]] const style::icon &msgBotKbSwitchPmIcon() const {\n\t\treturn _msgBotKbSwitchPmIcon;\n\t}\n\t[[nodiscard]] const style::icon &msgBotKbWebviewIcon() const {\n\t\treturn _msgBotKbWebviewIcon;\n\t}\n\t[[nodiscard]] const style::icon &msgBotKbCopyIcon() const {\n\t\treturn _msgBotKbCopyIcon;\n\t}\n\t[[nodiscard]] const style::icon &historyFastCommentsIcon() const {\n\t\treturn _historyFastCommentsIcon;\n\t}\n\t[[nodiscard]] const style::icon &historyFastShareIcon() const {\n\t\treturn _historyFastShareIcon;\n\t}\n\t[[nodiscard]] const style::icon &historyFastTranscribeIcon() const {\n\t\treturn _historyFastTranscribeIcon;\n\t}\n\t[[nodiscard]] const style::icon &historyFastTranscribeLock() const {\n\t\treturn _historyFastTranscribeLock;\n\t}\n\t[[nodiscard]] const style::icon &historyGoToOriginalIcon() const {\n\t\treturn _historyGoToOriginalIcon;\n\t}\n\t[[nodiscard]] const style::icon &historyFastCloseIcon() const {\n\t\treturn _historyFastCloseIcon;\n\t}\n\t[[nodiscard]] const style::icon &historyFastMoreIcon() const {\n\t\treturn _historyFastMoreIcon;\n\t}\n\t[[nodiscard]] const style::icon &historyMapPoint() const {\n\t\treturn _historyMapPoint;\n\t}\n\t[[nodiscard]] const style::icon &historyMapPointInner() const {\n\t\treturn _historyMapPointInner;\n\t}\n\t[[nodiscard]] const style::icon &youtubeIcon() const {\n\t\treturn _youtubeIcon;\n\t}\n\t[[nodiscard]] const style::icon &videoIcon() const {\n\t\treturn _videoIcon;\n\t}\n\t[[nodiscard]] const style::icon &historyPollChoiceRight() const {\n\t\treturn _historyPollChoiceRight;\n\t}\n\t[[nodiscard]] const style::icon &historyPollChoiceWrong() const {\n\t\treturn _historyPollChoiceWrong;\n\t}\n\nprivate:\n\tusing ColoredQuotePaintCaches = std::array<\n\t\tstd::unique_ptr<Text::QuotePaintCache>,\n\t\tkColorIndexCount * 2>;\n\n\tstruct ColoredPalette {\n\t\tColoredPalette();\n\t\tColoredPalette(const ColoredPalette &other);\n\t\tColoredPalette &operator=(const ColoredPalette &other);\n\n\t\tstd::optional<style::owned_color> linkFg;\n\t\tstyle::TextPalette data;\n\t};\n\n\tstruct CollectibleColors {\n\t\tstd::unique_ptr<Text::QuotePaintCache> quote;\n\t\tstd::unique_ptr<Text::QuotePaintCache> quoteSelected;\n\t\tstd::unique_ptr<Text::QuotePaintCache> reply;\n\t\tstd::unique_ptr<Text::QuotePaintCache> replySelected;\n\t\tColoredPalette palette;\n\t\tColoredPalette paletteSelected;\n\t};\n\n\tvoid assignPalette(not_null<const style::palette*> palette);\n\tvoid clearColorIndexCaches();\n\tvoid updateDarkValue();\n\n\t[[nodiscard]] not_null<Text::QuotePaintCache*> coloredCache(\n\t\tColoredQuotePaintCaches &caches,\n\t\tbool selected,\n\t\tuint8 colorIndex) const;\n\t[[nodiscard]] not_null<Text::QuotePaintCache*> collectibleCache(\n\t\tstd::unique_ptr<Text::QuotePaintCache> &cache,\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const;\n\t[[nodiscard]] CollectibleColors &resolveCollectibleCaches(\n\t\tconst std::shared_ptr<ColorCollectible> &collectible) const;\n\n\tvoid make(style::color &my, const style::color &original) const;\n\tvoid make(style::icon &my, const style::icon &original) const;\n\tvoid make(\n\t\tstyle::TextPalette &my,\n\t\tconst style::TextPalette &original) const;\n\tvoid make(\n\t\tstyle::TwoIconButton &my,\n\t\tconst style::TwoIconButton &original) const;\n\tvoid make(\n\t\tstyle::ScrollArea &my,\n\t\tconst style::ScrollArea &original) const;\n\n\t[[nodiscard]] MessageStyle &messageStyleRaw(\n\t\tbool outbg,\n\t\tbool selected) const;\n\t[[nodiscard]] MessageStyle &messageIn();\n\t[[nodiscard]] MessageStyle &messageInSelected();\n\t[[nodiscard]] MessageStyle &messageOut();\n\t[[nodiscard]] MessageStyle &messageOutSelected();\n\n\t[[nodiscard]] MessageImageStyle &imageStyleRaw(bool selected) const;\n\t[[nodiscard]] MessageImageStyle &image();\n\t[[nodiscard]] MessageImageStyle &imageSelected();\n\n\ttemplate <typename Type>\n\tvoid make(\n\t\tType MessageStyle::*my,\n\t\tconst Type &originalIn,\n\t\tconst Type &originalInSelected,\n\t\tconst Type &originalOut,\n\t\tconst Type &originalOutSelected);\n\n\ttemplate <typename Type>\n\tvoid make(\n\t\tType MessageImageStyle::*my,\n\t\tconst Type &original,\n\t\tconst Type &originalSelected);\n\n\tmutable CornersPixmaps _serviceBgCornersNormal;\n\tmutable CornersPixmaps _serviceBgCornersInverted;\n\n\tmutable std::array<MessageStyle, 4> _messageStyles;\n\tmutable std::array<MessageImageStyle, 2> _imageStyles;\n\n\tmutable CornersPixmaps _msgBotKbOverBgAddCornersSmall;\n\tmutable CornersPixmaps _msgBotKbOverBgAddCornersLarge;\n\tmutable CornersPixmaps _msgSelectOverlayCorners[\n\t\tint(CachedCornerRadius::kCount)];\n\n\tmutable std::vector<Text::SpecialColor> _highlightColors;\n\tmutable std::array<\n\t\tstd::unique_ptr<Text::QuotePaintCache>,\n\t\t2> _serviceQuoteCache;\n\tmutable std::array<\n\t\tstd::unique_ptr<Text::QuotePaintCache>,\n\t\t2> _serviceReplyCache;\n\tmutable std::array<\n\t\tstd::optional<ColorIndexValues>,\n\t\t2 * kColorIndexCount> _coloredValues;\n\tmutable ColoredQuotePaintCaches _coloredQuoteCaches;\n\tmutable ColoredQuotePaintCaches _coloredReplyCaches;\n\tmutable std::array<\n\t\tColoredPalette,\n\t\t2 * kColorIndexCount> _coloredTextPalettes;\n\tmutable base::flat_map<\n\t\tstd::weak_ptr<ColorCollectible>,\n\t\tCollectibleColors,\n\t\tColorCollectiblePtrCompare> _collectibleCaches;\n\tmutable base::flat_map<uint64, BackgroundEmojiData> _backgroundEmojis;\n\n\tstyle::TextPalette _historyPsaForwardPalette;\n\tstyle::TextPalette _imgReplyTextPalette;\n\tstyle::TextPalette _serviceTextPalette;\n\tstyle::TextPalette _priceTagTextPalette;\n\tstyle::icon _historyRepliesInvertedIcon = { Qt::Uninitialized };\n\tstyle::icon _historyViewsInvertedIcon = { Qt::Uninitialized };\n\tstyle::icon _historyViewsSendingIcon = { Qt::Uninitialized };\n\tstyle::icon _historyViewsSendingInvertedIcon = { Qt::Uninitialized };\n\tstyle::icon _historyPinInvertedIcon = { Qt::Uninitialized };\n\tstyle::icon _historySendingIcon = { Qt::Uninitialized };\n\tstyle::icon _historySendingInvertedIcon = { Qt::Uninitialized };\n\tstyle::icon _historySentInvertedIcon = { Qt::Uninitialized };\n\tstyle::icon _historyReceivedInvertedIcon = { Qt::Uninitialized };\n\tstyle::icon _msgBotKbUrlIcon = { Qt::Uninitialized };\n\tstyle::icon _msgBotKbPaymentIcon = { Qt::Uninitialized };\n\tstyle::icon _msgBotKbSwitchPmIcon = { Qt::Uninitialized };\n\tstyle::icon _msgBotKbWebviewIcon = { Qt::Uninitialized };\n\tstyle::icon _msgBotKbCopyIcon = { Qt::Uninitialized };\n\tstyle::icon _historyFastCommentsIcon = { Qt::Uninitialized };\n\tstyle::icon _historyFastShareIcon = { Qt::Uninitialized };\n\tstyle::icon _historyFastMoreIcon = { Qt::Uninitialized };\n\tstyle::icon _historyFastTranscribeIcon = { Qt::Uninitialized };\n\tstyle::icon _historyFastTranscribeLock = { Qt::Uninitialized };\n\tstyle::icon _historyGoToOriginalIcon = { Qt::Uninitialized };\n\tstyle::icon _historyFastCloseIcon = { Qt::Uninitialized };\n\tstyle::icon _historyMapPoint = { Qt::Uninitialized };\n\tstyle::icon _historyMapPointInner = { Qt::Uninitialized };\n\tstyle::icon _youtubeIcon = { Qt::Uninitialized };\n\tstyle::icon _videoIcon = { Qt::Uninitialized };\n\tstyle::icon _historyPollChoiceRight = { Qt::Uninitialized };\n\tstyle::icon _historyPollChoiceWrong = { Qt::Uninitialized };\n\n\tColorIndicesCompressed _colorIndices;\n\n\tbool _dark = false;\n\n\trpl::event_stream<> _paletteChanged;\n\n\trpl::lifetime _defaultPaletteChangeLifetime;\n\trpl::lifetime _colorIndicesLifetime;\n\n};\n\n[[nodiscard]] uint8 DecideColorIndex(uint64 id);\n[[nodiscard]] uint8 ColorIndexToPaletteIndex(uint8 colorIndex);\n\n[[nodiscard]] QColor FromNameFg(\n\tnot_null<const ChatStyle*> st,\n\tbool selected,\n\tuint8 colorIndex,\n\tconst std::shared_ptr<Ui::ColorCollectible> &colorCollectible);\n\n[[nodiscard]] inline QColor FromNameFg(\n\t\tconst ChatPaintContext &context,\n\t\tuint8 colorIndex,\n\t\tconst std::shared_ptr<Ui::ColorCollectible> &colorCollectible) {\n\treturn context.outbg\n\t\t? context.messageStyle()->msgServiceFg->c\n\t\t: FromNameFg(\n\t\t\tcontext.st,\n\t\t\tcontext.selected(),\n\t\t\tcolorIndex,\n\t\t\tcolorCollectible);\n}\n\nvoid FillComplexOverlayRect(\n\tQPainter &p,\n\tQRect rect,\n\tconst style::color &color,\n\tconst CornersPixmaps &corners);\n\nvoid FillComplexEllipse(\n\tQPainter &p,\n\tnot_null<const ChatStyle*> st,\n\tQRect rect);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/chat_style_radius.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/chat_style_radius.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"base/options.h\"\n\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_chat.h\"\n\nnamespace Ui {\nnamespace {\n\nbase::options::toggle UseSmallMsgBubbleRadius({\n\t.id = kOptionUseSmallMsgBubbleRadius,\n\t.name = \"Use small message bubble radius\",\n\t.description = \"Makes most message bubbles square-ish.\",\n\t.restartRequired = true,\n});\n\n} // namespace\n\nconst char kOptionUseSmallMsgBubbleRadius[] = \"use-small-msg-bubble-radius\";\n\nint BubbleRadiusSmall() {\n\treturn st::bubbleRadiusSmall;\n}\n\nint BubbleRadiusLarge() {\n\tstatic const auto result = [] {\n\t\tif (UseSmallMsgBubbleRadius.value()) {\n\t\t\treturn st::bubbleRadiusSmall;\n\t\t} else {\n\t\t\treturn st::bubbleRadiusLarge;\n\t\t}\n\t}();\n\treturn result;\n}\n\nint MsgFileThumbRadiusSmall() {\n\treturn st::msgFileThumbRadiusSmall;\n}\n\nint MsgFileThumbRadiusLarge() {\n\tstatic const auto result = [] {\n\t\tif (UseSmallMsgBubbleRadius.value()) {\n\t\t\treturn st::msgFileThumbRadiusSmall;\n\t\t} else {\n\t\t\treturn st::msgFileThumbRadiusLarge;\n\t\t}\n\t}();\n\treturn result;\n}\n\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/chat_style_radius.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\n[[nodiscard]] int BubbleRadiusSmall();\n[[nodiscard]] int BubbleRadiusLarge();\n\n[[nodiscard]] int MsgFileThumbRadiusSmall();\n[[nodiscard]] int MsgFileThumbRadiusLarge();\n\nextern const char kOptionUseSmallMsgBubbleRadius[];\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/chat_theme.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/chat_theme.h\"\n\n#include \"ui/color_contrast.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/chat/message_bubble.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/style/style_core_palette.h\"\n#include \"ui/style/style_palette_colorizer.h\"\n\n#include <crl/crl_async.h>\n#include <QtGui/QGuiApplication>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kCacheBackgroundTimeout = 1 * crl::time(1000);\nconstexpr auto kCacheBackgroundFastTimeout = crl::time(200);\nconstexpr auto kBackgroundFadeDuration = crl::time(200);\nconstexpr auto kMinimumTiledSize = 512;\nconstexpr auto kMaxSize = 2960;\nconstexpr auto kMaxContrastValue = 21.;\nconstexpr auto kMinAcceptableContrast = 1.14;// 4.5;\n\n[[nodiscard]] QColor DefaultBackgroundColor() {\n\treturn QColor(213, 223, 233);\n}\n\n[[nodiscard]] int ComputeRealRotation(const CacheBackgroundRequest &request) {\n\tif (request.background.colors.size() < 3) {\n\t\treturn request.background.gradientRotation;\n\t}\n\tconst auto doubled = (request.background.gradientRotation\n\t\t+ request.gradientRotationAdd) % 720;\n\treturn (((doubled % 2) ? (doubled - 45) : doubled) / 2) % 360;\n}\n\n[[nodiscard]] double ComputeRealProgress(\n\t\tconst CacheBackgroundRequest &request) {\n\tif (request.background.colors.size() < 3) {\n\t\treturn 1.;\n\t}\n\tconst auto doubled = (request.background.gradientRotation\n\t\t+ request.gradientRotationAdd) % 720;\n\treturn (doubled % 2) ? 0.5 : 1.;\n}\n\n[[nodiscard]] int ChooseGiftSymbolSkip(\n\t\tconst std::vector<ChatThemeGiftSymbol> &symbols) {\n\tif (symbols.empty()) {\n\t\treturn -1;\n\t}\n\tauto maxIndex = -1;\n\tauto maxValue = 0.;\n\tfor (auto i = 0, count = int(symbols.size()); i != count; ++i) {\n\t\tconst auto &symbol = symbols[i];\n\t\tconst auto center = symbol.area.center();\n\t\tconst auto value = std::min(center.x(), 1. - center.x())\n\t\t\t* std::min(center.y(), 1. - center.y())\n\t\t\t* std::min(symbol.area.width(), symbol.area.height());\n\t\tif (maxIndex < 0 || maxValue < value) {\n\t\t\tmaxIndex = i;\n\t\t\tmaxValue = value;\n\t\t}\n\t}\n\treturn maxIndex;\n}\n\n[[nodiscard]] CacheBackgroundResult CacheBackgroundByRequest(\n\t\tconst CacheBackgroundRequest &request) {\n\tExpects(!request.area.isEmpty());\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto gradient = request.background.gradientForFill.isNull()\n\t\t? QImage()\n\t\t: (request.gradientRotationAdd != 0)\n\t\t? Images::GenerateGradient(\n\t\t\trequest.background.gradientForFill.size(),\n\t\t\trequest.background.colors,\n\t\t\tComputeRealRotation(request),\n\t\t\tComputeRealProgress(request))\n\t\t: request.background.gradientForFill;\n\tif (request.background.isPattern\n\t\t|| request.background.tile\n\t\t|| request.background.prepared.isNull()) {\n\t\tauto result = gradient.isNull()\n\t\t\t? QImage(\n\t\t\t\trequest.area * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied)\n\t\t\t: gradient.scaled(\n\t\t\t\trequest.area * ratio,\n\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\tQt::SmoothTransformation);\n\t\tresult.setDevicePixelRatio(ratio);\n\t\tauto giftArea = QRect();\n\t\tfloat64 giftRotation = 0.;\n\t\tif (!request.background.prepared.isNull()) {\n\t\t\tQPainter p(&result);\n\t\t\tif (!gradient.isNull()) {\n\t\t\t\tif (request.background.patternOpacity >= 0.) {\n\t\t\t\t\tp.setCompositionMode(\n\t\t\t\t\t\tQPainter::CompositionMode_SoftLight);\n\t\t\t\t\tp.setOpacity(request.background.patternOpacity);\n\t\t\t\t} else {\n\t\t\t\t\tp.setCompositionMode(\n\t\t\t\t\t\tQPainter::CompositionMode_DestinationIn);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto hasGiftSymbols = request.background.isPattern\n\t\t\t\t&& !request.background.giftSymbols.empty();\n\t\t\tauto tiled = request.background.isPattern\n\t\t\t\t? request.background.prepared.scaled(\n\t\t\t\t\trequest.area.height() * ratio,\n\t\t\t\t\trequest.area.height() * ratio,\n\t\t\t\t\tQt::KeepAspectRatio,\n\t\t\t\t\tQt::SmoothTransformation)\n\t\t\t\t: request.background.preparedForTiled;\n\t\t\tconst auto w = tiled.width() / float(ratio);\n\t\t\tconst auto h = tiled.height() / float(ratio);\n\n\t\t\tconst auto &giftSymbols = request.background.giftSymbols;\n\t\t\tconst auto giftSymbolsCount = int(giftSymbols.size());\n\t\t\tconst auto giftSymbolSkip = ChooseGiftSymbolSkip(giftSymbols);\n\t\t\tconst auto &giftSymbolFrame = request.background.giftSymbolFrame;\n\t\t\tconst auto giftSymbolRect = [&](const QRectF &symbol) {\n\t\t\t\treturn QRectF(\n\t\t\t\t\tsymbol.x() * w,\n\t\t\t\t\tsymbol.y() * h,\n\t\t\t\t\tsymbol.width() * w,\n\t\t\t\t\tsymbol.height() * h);\n\t\t\t};\n\t\t\tconst auto giftSymbolPaint = [&](\n\t\t\t\t\tQPainter &p,\n\t\t\t\t\tChatThemeGiftSymbol symbol) {\n\t\t\t\tconst auto rect = giftSymbolRect(symbol.area);\n\n\t\t\t\tconst auto esize = Ui::Emoji::GetSizeLarge() / ratio;\n\t\t\t\tconst auto custom = Ui::Text::AdjustCustomEmojiSize(esize);\n\t\t\t\tconst auto coef = rect.width() / float64(custom);\n\n\t\t\t\tp.save();\n\t\t\t\tp.translate(rect.center());\n\t\t\t\tp.rotate(symbol.rotation);\n\t\t\t\tp.translate(-rect.center());\n\t\t\t\tp.translate(rect.topLeft());\n\t\t\t\tp.scale(coef, coef);\n\t\t\t\tp.translate(-rect.topLeft());\n\t\t\t\tp.drawImage(rect.topLeft(), giftSymbolFrame);\n\t\t\t\tp.restore();\n\t\t\t};\n\t\t\tif (hasGiftSymbols) {\n\t\t\t\tauto q = QPainter(&tiled);\n\t\t\t\tauto hq = PainterHighQualityEnabler(q);\n\t\t\t\tq.setOpacity(0.8);\n\t\t\t\tfor (auto i = 0; i != giftSymbolsCount; ++i) {\n\t\t\t\t\tif (i != giftSymbolSkip) {\n\t\t\t\t\t\tgiftSymbolPaint(q, giftSymbols[i]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto cx = int(std::ceil(request.area.width() / w));\n\t\t\tconst auto cy = int(std::ceil(request.area.height() / h));\n\t\t\tconst auto rows = cy;\n\t\t\tconst auto cols = request.background.isPattern\n\t\t\t\t? (((cx / 2) * 2) + 1)\n\t\t\t\t: cx;\n\t\t\tconst auto xshift = request.background.isPattern\n\t\t\t\t? (request.area.width() * ratio - cols * tiled.width()) / 2\n\t\t\t\t: 0;\n\t\t\tconst auto useshift = xshift / float(ratio);\n\t\t\tconst auto drawTile = [&](int x, int y) {\n\t\t\t\tconst auto position = QPointF(useshift + x * w, y * h);\n\t\t\t\tp.drawImage(position, tiled);\n\t\t\t};\n\n\t\t\t// Skip a symbol in the center for the gift itself.\n\t\t\tdrawTile(cols / 2, 0);\n\t\t\tif (hasGiftSymbols) {\n\t\t\t\tconst auto &gift = giftSymbols[giftSymbolSkip];\n\t\t\t\tauto q = QPainter(&tiled);\n\t\t\t\tauto hq = PainterHighQualityEnabler(q);\n\t\t\t\tq.setOpacity(0.8);\n\t\t\t\tgiftSymbolPaint(q, gift);\n\t\t\t\tconst auto exact = giftSymbolRect(gift.area);\n\t\t\t\tgiftArea = QRect(\n\t\t\t\t\tuseshift + (cols / 2) * w + exact.x(),\n\t\t\t\t\texact.y(),\n\t\t\t\t\texact.width(),\n\t\t\t\t\texact.height());\n\t\t\t\tgiftRotation = gift.rotation;\n\t\t\t}\n\n\t\t\tfor (auto y = 0; y != rows; ++y) {\n\t\t\t\tfor (auto x = 0; x != cols; ++x) {\n\t\t\t\t\tif (y || x != (cols / 2)) {\n\t\t\t\t\t\tdrawTile(x, y);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!gradient.isNull()\n\t\t\t\t&& request.background.patternOpacity < 0.\n\t\t\t\t&& request.background.patternOpacity > -1.) {\n\t\t\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\t\t\tp.setOpacity(1. + request.background.patternOpacity);\n\t\t\t\tp.fillRect(QRect(QPoint(), request.area), Qt::black);\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\t.image = std::move(result).convertToFormat(\n\t\t\t\tQImage::Format_ARGB32_Premultiplied),\n\t\t\t.gradient = gradient,\n\t\t\t.area = request.area,\n\t\t\t.giftArea = giftArea,\n\t\t\t.giftRotation = giftRotation,\n\t\t\t.waitingForNegativePattern\n\t\t\t\t= request.background.waitingForNegativePattern()\n\t\t};\n\t} else {\n\t\tconst auto rects = ComputeChatBackgroundRects(\n\t\t\trequest.area,\n\t\t\trequest.background.prepared.size());\n\t\tauto result = request.background.prepared.copy(rects.from).scaled(\n\t\t\trects.to.width() * style::DevicePixelRatio(),\n\t\t\trects.to.height() * style::DevicePixelRatio(),\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\t\treturn {\n\t\t\t.image = std::move(result).convertToFormat(\n\t\t\t\tQImage::Format_ARGB32_Premultiplied),\n\t\t\t.gradient = gradient,\n\t\t\t.area = request.area,\n\t\t\t.x = rects.to.x(),\n\t\t\t.y = rects.to.y(),\n\t\t};\n\t}\n}\n\n[[nodiscard]] QImage PrepareBubblesBackground(\n\t\tconst ChatThemeBubblesData &data) {\n\tif (data.colors.size() < 2) {\n\t\treturn QImage();\n\t}\n\tconstexpr auto kSize = 512;\n\treturn Images::GenerateLinearGradient(QSize(kSize, kSize), data.colors);\n}\n\n} // namespace\n\nbool operator==(const ChatThemeBackground &a, const ChatThemeBackground &b) {\n\treturn (a.key == b.key)\n\t\t&& (a.prepared.cacheKey() == b.prepared.cacheKey())\n\t\t&& (a.gradientForFill.cacheKey() == b.gradientForFill.cacheKey())\n\t\t&& (a.tile == b.tile)\n\t\t&& (a.patternOpacity == b.patternOpacity);\n}\n\nbool operator!=(const ChatThemeBackground &a, const ChatThemeBackground &b) {\n\treturn !(a == b);\n}\n\nbool operator==(\n\t\tconst CacheBackgroundRequest &a,\n\t\tconst CacheBackgroundRequest &b) {\n\treturn (a.background == b.background)\n\t\t&& (a.area == b.area)\n\t\t&& (a.gradientRotationAdd == b.gradientRotationAdd)\n\t\t&& (a.gradientProgress == b.gradientProgress);\n}\n\nbool operator!=(\n\t\tconst CacheBackgroundRequest &a,\n\t\tconst CacheBackgroundRequest &b) {\n\treturn !(a == b);\n}\n\nCacheBackgroundResult CacheBackground(\n\t\tconst CacheBackgroundRequest &request) {\n\treturn CacheBackgroundByRequest(request);\n}\n\nCachedBackground::CachedBackground(CacheBackgroundResult &&result)\n: pixmap(PixmapFromImage(std::move(result.image)))\n, area(result.area)\n, x(result.x)\n, y(result.y)\n, giftArea(result.giftArea)\n, giftRotation(result.giftRotation)\n, waitingForNegativePattern(result.waitingForNegativePattern) {\n}\n\nChatTheme::ChatTheme() {\n}\n\n// Runs from background thread.\nChatTheme::ChatTheme(ChatThemeDescriptor &&descriptor)\n: _key(descriptor.key)\n, _palette(std::make_unique<style::palette>()) {\n\tdescriptor.preparePalette(*_palette);\n\tsetBackground(PrepareBackgroundImage(descriptor.backgroundData));\n\tsetBubblesBackground(PrepareBubblesBackground(descriptor.bubblesData));\n\tadjustPalette(descriptor);\n}\n\nChatTheme::~ChatTheme() = default;\n\nvoid ChatTheme::adjustPalette(const ChatThemeDescriptor &descriptor) {\n\tauto &p = *_palette;\n\tconst auto overrideOutBg = (descriptor.bubblesData.colors.size() == 1);\n\tif (overrideOutBg) {\n\t\tset(p.msgOutBg(), descriptor.bubblesData.colors.front());\n\t}\n\tconst auto &data = descriptor.backgroundData;\n\tconst auto &background = data.colors;\n\tconst auto useImage = !data.isPattern\n\t\t&& (!data.path.isEmpty() || !data.bytes.isEmpty());\n\tif (useImage || !background.empty()) {\n\t\tconst auto average = useImage\n\t\t\t? Ui::CountAverageColor(_mutableBackground.prepared)\n\t\t\t: CountAverageColor(background);\n\t\tadjust(p.msgServiceBg(), average);\n\t\tadjust(p.msgServiceBgSelected(), average);\n\t\tadjust(p.historyScrollBg(), average);\n\t\tadjust(p.historyScrollBgOver(), average);\n\t\tadjust(p.historyScrollBarBg(), average);\n\t\tadjust(p.historyScrollBarBgOver(), average);\n\t}\n\tconst auto bubblesAccent = descriptor.bubblesData.accent\n\t\t? descriptor.bubblesData.accent\n\t\t: (!descriptor.bubblesData.colors.empty())\n\t\t? ThemeAdjustedColor(\n\t\t\tp.msgOutReplyBarColor()->c,\n\t\t\tCountAverageColor(descriptor.bubblesData.colors))\n\t\t: std::optional<QColor>();\n\tif (bubblesAccent) {\n\t\t// First set hue/saturation the same for all those colors from accent.\n\t\tconst auto by = *bubblesAccent;\n\t\tif (!overrideOutBg) {\n\t\t\tadjust(p.msgOutBg(), by);\n\t\t}\n\t\tadjust(p.msgOutShadow(), by);\n\t\tadjust(p.msgOutServiceFg(), by);\n\t\tadjust(p.msgOutDateFg(), by);\n\t\tadjust(p.msgFileThumbLinkOutFg(), by);\n\t\tadjust(p.msgFileOutBg(), by);\n\t\tadjust(p.msgOutReplyBarColor(), by);\n\t\tadjust(p.msgWaveformOutActive(), by);\n\t\tadjust(p.msgWaveformOutInactive(), by);\n\t\tadjust(p.historyFileOutRadialFg(), by); // historyFileOutIconFg\n\t\tadjust(p.mediaOutFg(), by);\n\n\t\tadjust(p.historyLinkOutFg(), by);\n\t\tadjust(p.msgOutMonoFg(), by);\n\t\tadjust(p.historyOutIconFg(), by);\n\t\tadjust(p.historySendingOutIconFg(), by);\n\t\tadjust(p.historyCallArrowOutFg(), by);\n\t\tadjust(p.historyFileOutIconFg(), by); // msgOutBg\n\n\t\t// After make msgFileOutBg exact accent and adjust some others.\n\t\tconst auto colorizer = bubblesAccentColorizer(by);\n\t\tadjust(p.msgOutServiceFg(), colorizer);\n\t\tadjust(p.msgOutDateFg(), colorizer);\n\t\tadjust(p.msgFileThumbLinkOutFg(), colorizer);\n\t\tadjust(p.msgFileOutBg(), colorizer);\n\t\tadjust(p.msgOutReplyBarColor(), colorizer);\n\t\tadjust(p.msgWaveformOutActive(), colorizer);\n\t\tadjust(p.msgWaveformOutInactive(), colorizer);\n\t\tadjust(p.mediaOutFg(), colorizer);\n\t\tadjust(p.historyLinkOutFg(), colorizer);\n\t\tadjust(p.historyOutIconFg(), colorizer);\n\t\tadjust(p.historySendingOutIconFg(), colorizer);\n\t\tadjust(p.historyCallArrowOutFg(), colorizer);\n\n\t\tif (!descriptor.basedOnDark) {\n\t\t\tadjust(p.msgOutBgSelected(), by);\n\t\t\tadjust(p.msgOutShadowSelected(), by);\n\t\t\tadjust(p.msgOutServiceFgSelected(), by);\n\t\t\tadjust(p.msgOutDateFgSelected(), by);\n\t\t\tadjust(p.msgFileThumbLinkOutFgSelected(), by);\n\t\t\tadjust(p.msgFileOutBgSelected(), by);\n\t\t\tadjust(p.msgOutReplyBarSelColor(), by);\n\t\t\tadjust(p.msgWaveformOutActiveSelected(), by);\n\t\t\tadjust(p.msgWaveformOutInactiveSelected(), by);\n\t\t\tadjust(p.historyFileOutRadialFgSelected(), by);\n\t\t\tadjust(p.mediaOutFgSelected(), by);\n\n\t\t\tadjust(p.historyLinkOutFgSelected(), by);\n\t\t\tadjust(p.msgOutMonoFgSelected(), by);\n\t\t\tadjust(p.historyOutIconFgSelected(), by);\n\t\t\t// adjust(p.historySendingOutIconFgSelected(), by);\n\t\t\tadjust(p.historyCallArrowOutFgSelected(), by);\n\t\t\tadjust(p.historyFileOutIconFgSelected(), by); // msgOutBg\n\n\t\t\tadjust(p.msgOutServiceFgSelected(), colorizer);\n\t\t\tadjust(p.msgOutDateFgSelected(), colorizer);\n\t\t\tadjust(p.msgFileThumbLinkOutFgSelected(), colorizer);\n\t\t\tadjust(p.msgFileOutBgSelected(), colorizer);\n\t\t\tadjust(p.msgOutReplyBarSelColor(), colorizer);\n\t\t\tadjust(p.msgWaveformOutActiveSelected(), colorizer);\n\t\t\tadjust(p.msgWaveformOutInactiveSelected(), colorizer);\n\t\t\tadjust(p.mediaOutFgSelected(), colorizer);\n\t\t\tadjust(p.historyLinkOutFgSelected(), colorizer);\n\t\t\tadjust(p.historyOutIconFgSelected(), colorizer);\n\t\t\t//adjust(p.historySendingOutIconFgSelected(), colorizer);\n\t\t\tadjust(p.historyCallArrowOutFgSelected(), colorizer);\n\t\t}\n\t}\n\tauto outBgColors = descriptor.bubblesData.colors;\n\tif (outBgColors.empty()) {\n\t\toutBgColors.push_back(p.msgOutBg()->c);\n\t}\n\tconst auto colors = {\n\t\tp.msgOutServiceFg(),\n\t\tp.msgOutDateFg(),\n\t\tp.msgFileThumbLinkOutFg(),\n\t\tp.msgFileOutBg(),\n\t\tp.msgOutReplyBarColor(),\n\t\tp.msgWaveformOutActive(),\n\t\tp.historyTextOutFg(),\n\t\tp.mediaOutFg(),\n\t\tp.historyLinkOutFg(),\n\t\tp.msgOutMonoFg(),\n\t\tp.historyOutIconFg(),\n\t\tp.historyCallArrowOutFg(),\n\t};\n\tconst auto minimal = [&](const QColor &with) {\n\t\tauto result = kMaxContrastValue;\n\t\tfor (const auto &color : colors) {\n\t\t\tresult = std::min(result, Ui::CountContrast(color->c, with));\n\t\t}\n\t\treturn result;\n\t};\n\tconst auto withBg = [&](auto &&count) {\n\t\tauto result = kMaxContrastValue;\n\t\tfor (const auto &bg : outBgColors) {\n\t\t\tresult = std::min(result, count(bg));\n\t\t}\n\t\treturn result;\n\t};\n\t//const auto singleWithBg = [&](const QColor &c) {\n\t//\treturn withBg([&](const QColor &with) {\n\t//\t\treturn Ui::CountContrast(c, with);\n\t//\t});\n\t//};\n\tif (withBg(minimal) < kMinAcceptableContrast) {\n\t\tconst auto white = QColor(255, 255, 255);\n\t\tconst auto black = QColor(0, 0, 0);\n\t\t// This one always gives black :)\n\t\t//const auto now = (singleWithBg(white) >= singleWithBg(black))\n\t\t//\t? white\n\t\t//\t: black;\n\t\tconst auto now = descriptor.basedOnDark ? white : black;\n\t\tfor (const auto &color : colors) {\n\t\t\tset(color, now);\n\t\t}\n\t}\n}\n\nstyle::colorizer ChatTheme::bubblesAccentColorizer(\n\t\tconst QColor &accent) const {\n\tconst auto color = [](const QColor &value) {\n\t\tauto hue = 0;\n\t\tauto saturation = 0;\n\t\tauto lightness = 0;\n\t\tvalue.getHsv(&hue, &saturation, &lightness);\n\t\treturn style::colorizer::Color{ hue, saturation, lightness };\n\t};\n\treturn {\n\t\t.hueThreshold = 255,\n\t\t.was = color(_palette->msgFileOutBg()->c),\n\t\t.now = color(accent),\n\t};\n}\n\nvoid ChatTheme::set(const style::color &my, const QColor &color) {\n\tauto r = 0, g = 0, b = 0, a = 0;\n\tcolor.getRgb(&r, &g, &b, &a);\n\tmy.set(uchar(r), uchar(g), uchar(b), uchar(a));\n}\n\nvoid ChatTheme::adjust(const style::color &my, const QColor &by) {\n\tset(my, ThemeAdjustedColor(my->c, by));\n}\n\nvoid ChatTheme::adjust(const style::color &my, const style::colorizer &by) {\n\tif (const auto adjusted = style::colorize(my->c, by)) {\n\t\tset(my, *adjusted);\n\t}\n}\n\nvoid ChatTheme::setBackground(ChatThemeBackground &&background) {\n\t_mutableBackground = std::move(background);\n\t_backgroundState = {};\n\t_backgroundNext = {};\n\t_backgroundFade.stop();\n\tif (_cacheBackgroundTimer) {\n\t\t_cacheBackgroundTimer->cancel();\n\t}\n\t_repaintBackgroundRequests.fire({});\n}\n\nvoid ChatTheme::updateBackgroundImageFrom(ChatThemeBackground &&background) {\n\t_mutableBackground.key = background.key;\n\t_mutableBackground.prepared = std::move(background.prepared);\n\t_mutableBackground.giftSymbols = std::move(background.giftSymbols);\n\t_mutableBackground.giftId = background.giftId;\n\t_mutableBackground.preparedForTiled = std::move(\n\t\tbackground.preparedForTiled);\n\tif (!_backgroundState.now.pixmap.isNull()) {\n\t\tif (_cacheBackgroundTimer) {\n\t\t\t_cacheBackgroundTimer->cancel();\n\t\t}\n\t\tcacheBackgroundNow();\n\t} else {\n\t\t_repaintBackgroundRequests.fire({});\n\t}\n}\n\nChatThemeKey ChatTheme::key() const {\n\treturn _key;\n}\n\nvoid ChatTheme::setBubblesBackground(QImage image) {\n\tif (image.isNull() && _bubblesBackgroundPrepared.isNull()) {\n\t\treturn;\n\t}\n\t_bubblesBackgroundPrepared = std::move(image);\n\tif (_bubblesBackgroundPrepared.isNull()) {\n\t\t_bubblesBackgroundPattern = nullptr;\n\t\t// setBubblesBackground called only from background thread.\n\t\t//_repaintBackgroundRequests.fire({});\n\t\treturn;\n\t}\n\t_bubblesBackground = CacheBackground({\n\t\t.background = {\n\t\t\t.prepared = _bubblesBackgroundPrepared,\n\t\t},\n\t\t.area = (_bubblesBackground.area.isEmpty()\n\t\t\t? _bubblesBackgroundPrepared.size()\n\t\t\t: _bubblesBackground.area),\n\t});\n\tif (!_bubblesBackgroundPattern) {\n\t\t_bubblesBackgroundPattern = PrepareBubblePattern(palette());\n\t}\n\t_bubblesBackgroundPattern->pixmap = _bubblesBackground.pixmap;\n\t// setBubblesBackground called only from background thread.\n\t//_repaintBackgroundRequests.fire({});\n}\n\nvoid ChatTheme::finishCreateOnMain() {\n\tif (_bubblesBackgroundPattern) {\n\t\tFinishBubblePatternOnMain(_bubblesBackgroundPattern.get());\n\t}\n}\n\nChatPaintContext ChatTheme::preparePaintContext(\n\t\tnot_null<const ChatStyle*> st,\n\t\tQRect viewport,\n\t\tQRect area,\n\t\tQRect clip,\n\t\tbool paused) {\n\tconst auto size = viewport.size();\n\tconst auto now = crl::now();\n\tif (!_bubblesBackgroundPrepared.isNull()\n\t\t&& _bubblesBackground.area != size) {\n\t\tif (!_cacheBubblesTimer) {\n\t\t\t_cacheBubblesTimer.emplace([=] { cacheBubbles(); });\n\t\t}\n\t\tif (_cacheBubblesArea != size\n\t\t\t|| (!_cacheBubblesTimer->isActive()\n\t\t\t\t&& !_bubblesCachingRequest)) {\n\t\t\t_cacheBubblesArea = size;\n\t\t\t_lastBubblesAreaChangeTime = now;\n\t\t\t_cacheBubblesTimer->callOnce(kCacheBackgroundFastTimeout);\n\t\t}\n\t}\n\treturn {\n\t\t.st = st,\n\t\t.bubblesPattern = _bubblesBackgroundPattern.get(),\n\t\t.viewport = viewport,\n\t\t.area = area,\n\t\t.clip = clip,\n\t\t.now = now,\n\t\t.paused = paused,\n\t};\n}\n\nconst BackgroundState &ChatTheme::backgroundState(QSize area) {\n\tif (!_cacheBackgroundTimer) {\n\t\t_cacheBackgroundTimer.emplace([=] { cacheBackground(); });\n\t}\n\t_backgroundState.shown = _backgroundFade.value(1.);\n\tif (_backgroundState.now.pixmap.isNull()\n\t\t&& !background().gradientForFill.isNull()) {\n\t\t// We don't support direct painting of patterned gradients.\n\t\t// So we need to sync-generate cache image here.\n\t\t_cacheBackgroundArea = area;\n\t\tsetCachedBackground(CacheBackground(cacheBackgroundRequest(area)));\n\t\t_cacheBackgroundTimer->cancel();\n\t} else if (_backgroundState.now.area != area) {\n\t\tif (_cacheBackgroundArea != area\n\t\t\t|| (!_cacheBackgroundTimer->isActive()\n\t\t\t\t&& !_backgroundCachingRequest)) {\n\t\t\t_cacheBackgroundArea = area;\n\t\t\t_lastBackgroundAreaChangeTime = crl::now();\n\t\t\t_cacheBackgroundTimer->callOnce(kCacheBackgroundFastTimeout);\n\t\t}\n\t}\n\tgenerateNextBackgroundRotation();\n\treturn _backgroundState;\n}\n\nvoid ChatTheme::clearBackgroundState() {\n\t_backgroundState = BackgroundState();\n\t_backgroundFade.stop();\n}\n\nbool ChatTheme::readyForBackgroundRotation() const {\n\tExpects(_cacheBackgroundTimer.has_value());\n\n\treturn !On(PowerSaving::kChatBackground)\n\t\t&& !_backgroundFade.animating()\n\t\t&& !_cacheBackgroundTimer->isActive()\n\t\t&& !_backgroundState.now.pixmap.isNull();\n}\n\nvoid ChatTheme::generateNextBackgroundRotation() {\n\tif (_nextCachingRequest\n\t\t|| _backgroundCachingRequest\n\t\t|| !_backgroundNext.image.isNull()\n\t\t|| !readyForBackgroundRotation()\n\t\t|| background().colors.size() < 3) {\n\t\treturn;\n\t}\n\tconstexpr auto kAddRotationDoubled = (720 - 45);\n\tconst auto request = cacheBackgroundRequest(\n\t\t_backgroundState.now.area,\n\t\tkAddRotationDoubled);\n\tif (!request) {\n\t\treturn;\n\t}\n\t_nextCachingRequest = request;\n\tcacheBackgroundAsync(request, [=](CacheBackgroundResult &&result) {\n\t\tconst auto forRequest = base::take(_nextCachingRequest);\n\t\tif (!readyForBackgroundRotation()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto request = cacheBackgroundRequest(\n\t\t\t_backgroundState.now.area,\n\t\t\tkAddRotationDoubled);\n\t\tif (forRequest == request) {\n\t\t\t_mutableBackground.gradientRotation\n\t\t\t\t= (_mutableBackground.gradientRotation\n\t\t\t\t\t+ kAddRotationDoubled) % 720;\n\t\t\t_backgroundNext = std::move(result);\n\t\t}\n\t});\n}\n\nauto ChatTheme::cacheBackgroundRequest(QSize area, int addRotation) const\n-> CacheBackgroundRequest {\n\tif (background().colorForFill) {\n\t\treturn {};\n\t}\n\treturn {\n\t\t.background = background(),\n\t\t.area = area,\n\t\t.gradientRotationAdd = addRotation,\n\t};\n}\n\nvoid ChatTheme::cacheBackground() {\n\tExpects(_cacheBackgroundTimer.has_value());\n\n\tconst auto now = crl::now();\n\tif (now - _lastBackgroundAreaChangeTime < kCacheBackgroundTimeout\n\t\t&& QGuiApplication::mouseButtons() != 0) {\n\t\t_cacheBackgroundTimer->callOnce(kCacheBackgroundFastTimeout);\n\t\treturn;\n\t}\n\tcacheBackgroundNow();\n}\n\nvoid ChatTheme::cacheBackgroundNow() {\n\tif (!_backgroundCachingRequest) {\n\t\tif (const auto request = cacheBackgroundRequest(\n\t\t\t\t_cacheBackgroundArea)) {\n\t\t\tcacheBackgroundAsync(request);\n\t\t}\n\t}\n}\n\nvoid ChatTheme::cacheBackgroundAsync(\n\t\tconst CacheBackgroundRequest &request,\n\t\tFn<void(CacheBackgroundResult&&)> done) {\n\tif (!done) {\n\t\t_backgroundCachingRequest = request;\n\t}\n\tconst auto weak = base::make_weak(this);\n\tcrl::async([=] {\n\t\tif (!weak) {\n\t\t\treturn;\n\t\t}\n\t\tcrl::on_main(weak, [=, result = CacheBackground(request)]() mutable {\n\t\t\tif (done) {\n\t\t\t\tdone(std::move(result));\n\t\t\t} else if (const auto request = cacheBackgroundRequest(\n\t\t\t\t\t_cacheBackgroundArea)) {\n\t\t\t\tif (_backgroundCachingRequest != request) {\n\t\t\t\t\tcacheBackgroundAsync(request);\n\t\t\t\t} else {\n\t\t\t\t\t_backgroundCachingRequest = {};\n\t\t\t\t\tsetCachedBackground(std::move(result));\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n}\n\nvoid ChatTheme::setCachedBackground(CacheBackgroundResult &&cached) {\n\t_backgroundNext = {};\n\n\tif (background().gradientForFill.isNull()\n\t\t|| _backgroundState.now.pixmap.isNull()\n\t\t|| anim::Disabled()) {\n\t\t_backgroundFade.stop();\n\t\t_backgroundState.shown = 1.;\n\t\t_backgroundState.now = std::move(cached);\n\t\treturn;\n\t}\n\t// #TODO themes compose several transitions.\n\t_backgroundState.was = std::move(_backgroundState.now);\n\t_backgroundState.now = std::move(cached);\n\t_backgroundState.shown = 0.;\n\tconst auto callback = [=] {\n\t\tif (!_backgroundFade.animating()) {\n\t\t\t_backgroundState.was = {};\n\t\t\t_backgroundState.shown = 1.;\n\t\t}\n\t\t_repaintBackgroundRequests.fire({});\n\t};\n\t_backgroundFade.start(\n\t\tcallback,\n\t\t0.,\n\t\t1.,\n\t\tkBackgroundFadeDuration);\n}\n\nauto ChatTheme::cacheBubblesRequest(QSize area) const\n-> CacheBackgroundRequest {\n\tif (_bubblesBackgroundPrepared.isNull()) {\n\t\treturn {};\n\t}\n\treturn {\n\t\t.background = {\n\t\t\t.gradientForFill = _bubblesBackgroundPrepared,\n\t\t},\n\t\t.area = area,\n\t};\n}\n\nvoid ChatTheme::cacheBubbles() {\n\tExpects(_cacheBubblesTimer.has_value());\n\n\tconst auto now = crl::now();\n\tif (now - _lastBubblesAreaChangeTime < kCacheBackgroundTimeout\n\t\t&& QGuiApplication::mouseButtons() != 0) {\n\t\t_cacheBubblesTimer->callOnce(kCacheBackgroundFastTimeout);\n\t\treturn;\n\t}\n\tcacheBubblesNow();\n}\n\nvoid ChatTheme::cacheBubblesNow() {\n\tif (!_bubblesCachingRequest) {\n\t\tif (const auto request = cacheBackgroundRequest(\n\t\t\t\t_cacheBubblesArea)) {\n\t\t\tcacheBubblesAsync(request);\n\t\t}\n\t}\n}\n\nvoid ChatTheme::cacheBubblesAsync(\n\t\tconst CacheBackgroundRequest &request) {\n\t_bubblesCachingRequest = request;\n\tconst auto weak = base::make_weak(this);\n\tcrl::async([=] {\n\t\tif (!weak) {\n\t\t\treturn;\n\t\t}\n\t\tcrl::on_main(weak, [=, result = CacheBackground(request)]() mutable {\n\t\t\tif (const auto request = cacheBubblesRequest(\n\t\t\t\t\t_cacheBubblesArea)) {\n\t\t\t\tif (_bubblesCachingRequest != request) {\n\t\t\t\t\tcacheBubblesAsync(request);\n\t\t\t\t} else {\n\t\t\t\t\t_bubblesCachingRequest = {};\n\t\t\t\t\t_bubblesBackground = std::move(result);\n\t\t\t\t\t_bubblesBackgroundPattern->pixmap\n\t\t\t\t\t\t= _bubblesBackground.pixmap;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n}\n\nrpl::producer<> ChatTheme::repaintBackgroundRequests() const {\n\treturn _repaintBackgroundRequests.events();\n}\n\nvoid ChatTheme::rotateComplexGradientBackground() {\n\tif (!_backgroundFade.animating() && !_backgroundNext.image.isNull()) {\n\t\tif (_mutableBackground.gradientForFill.size()\n\t\t\t== _backgroundNext.gradient.size()) {\n\t\t\t_mutableBackground.gradientForFill\n\t\t\t\t= std::move(_backgroundNext.gradient);\n\t\t}\n\t\tsetCachedBackground(base::take(_backgroundNext));\n\t}\n}\n\nChatBackgroundRects ComputeChatBackgroundRects(\n\t\tQSize fillSize,\n\t\tQSize imageSize) {\n\tif (uint64(imageSize.width()) * fillSize.height()\n\t\t> uint64(imageSize.height()) * fillSize.width()) {\n\t\tconst auto pxsize = fillSize.height() / float64(imageSize.height());\n\t\tauto takewidth = int(std::ceil(fillSize.width() / pxsize));\n\t\tif (takewidth > imageSize.width()) {\n\t\t\ttakewidth = imageSize.width();\n\t\t} else if ((imageSize.width() % 2) != (takewidth % 2)) {\n\t\t\t++takewidth;\n\t\t}\n\t\treturn {\n\t\t\t.from = QRect(\n\t\t\t\t(imageSize.width() - takewidth) / 2,\n\t\t\t\t0,\n\t\t\t\ttakewidth,\n\t\t\t\timageSize.height()),\n\t\t\t.to = QRect(\n\t\t\t\tint((fillSize.width() - takewidth * pxsize) / 2.),\n\t\t\t\t0,\n\t\t\t\tint(std::ceil(takewidth * pxsize)),\n\t\t\t\tfillSize.height()),\n\t\t};\n\t} else {\n\t\tconst auto pxsize = fillSize.width() / float64(imageSize.width());\n\t\tauto takeheight = int(std::ceil(fillSize.height() / pxsize));\n\t\tif (takeheight > imageSize.height()) {\n\t\t\ttakeheight = imageSize.height();\n\t\t} else if ((imageSize.height() % 2) != (takeheight % 2)) {\n\t\t\t++takeheight;\n\t\t}\n\t\treturn {\n\t\t\t.from = QRect(\n\t\t\t\t0,\n\t\t\t\t(imageSize.height() - takeheight) / 2,\n\t\t\t\timageSize.width(),\n\t\t\t\ttakeheight),\n\t\t\t.to = QRect(\n\t\t\t\t0,\n\t\t\t\tint((fillSize.height() - takeheight * pxsize) / 2.),\n\t\t\t\tfillSize.width(),\n\t\t\t\tint(std::ceil(takeheight * pxsize))),\n\t\t};\n\t}\n}\n\nQColor CountAverageColor(const QImage &image) {\n\tExpects(!image.isNull());\n\tExpects(image.format() == QImage::Format_ARGB32_Premultiplied\n\t\t|| image.format() == QImage::Format_RGB32);\n\n\tuint64 components[3] = { 0 };\n\tconst auto w = image.width();\n\tconst auto h = image.height();\n\tconst auto size = w * h;\n\tif (const auto pix = image.constBits()) {\n\t\tfor (auto i = 0, l = size * 4; i != l; i += 4) {\n\t\t\tcomponents[2] += pix[i + 0];\n\t\t\tcomponents[1] += pix[i + 1];\n\t\t\tcomponents[0] += pix[i + 2];\n\t\t}\n\t}\n\tif (size) {\n\t\tfor (auto &component : components) {\n\t\t\tcomponent /= size;\n\t\t}\n\t}\n\treturn QColor(components[0], components[1], components[2]);\n}\n\nQColor CountAverageColor(const std::vector<QColor> &colors) {\n\tExpects(colors.size() < (std::numeric_limits<int>::max() / 256));\n\n\tint components[3] = { 0 };\n\tauto r = 0;\n\tauto g = 0;\n\tauto b = 0;\n\tfor (const auto &color : colors) {\n\t\tcolor.getRgb(&r, &g, &b);\n\t\tcomponents[0] += r;\n\t\tcomponents[1] += g;\n\t\tcomponents[2] += b;\n\t}\n\tif (const auto size = colors.size()) {\n\t\tfor (auto &component : components) {\n\t\t\tcomponent /= size;\n\t\t}\n\t}\n\treturn QColor(components[0], components[1], components[2]);\n}\n\nbool IsPatternInverted(\n\t\tconst std::vector<QColor> &background,\n\t\tfloat64 patternOpacity) {\n\treturn (patternOpacity > 0.)\n\t\t&& (CountAverageColor(background).toHsv().valueF() <= 0.3);\n}\n\nQColor ThemeAdjustedColor(QColor original, QColor background) {\n\treturn QColor::fromHslF(\n\t\tbackground.hslHueF(),\n\t\tbackground.hslSaturationF(),\n\t\toriginal.lightnessF(),\n\t\toriginal.alphaF()\n\t).toRgb();\n}\n\nQImage PreprocessBackgroundImage(QImage image) {\n\tif (image.isNull()) {\n\t\treturn image;\n\t}\n\tif (image.format() != QImage::Format_ARGB32_Premultiplied) {\n\t\timage = std::move(image).convertToFormat(\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t}\n\tif (image.width() > 40 * image.height()) {\n\t\tconst auto width = 40 * image.height();\n\t\tconst auto height = image.height();\n\t\timage = image.copy((image.width() - width) / 2, 0, width, height);\n\t} else if (image.height() > 40 * image.width()) {\n\t\tconst auto width = image.width();\n\t\tconst auto height = 40 * image.width();\n\t\timage = image.copy(0, (image.height() - height) / 2, width, height);\n\t}\n\tif (image.width() > kMaxSize || image.height() > kMaxSize) {\n\t\timage = image.scaled(\n\t\t\tkMaxSize,\n\t\t\tkMaxSize,\n\t\t\tQt::KeepAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t}\n\treturn image;\n}\n\nstd::optional<QColor> CalculateImageMonoColor(const QImage &image) {\n\tExpects(image.bytesPerLine() == 4 * image.width());\n\n\tif (image.isNull()) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto bits = reinterpret_cast<const uint32*>(image.constBits());\n\tconst auto first = bits[0];\n\tfor (auto i = 0; i < image.width() * image.height(); i++) {\n\t\tif (first != bits[i]) {\n\t\t\treturn std::nullopt;\n\t\t}\n\t}\n\treturn image.pixelColor(QPoint());\n}\n\nQImage PrepareImageForTiled(const QImage &prepared) {\n\tconst auto width = prepared.width();\n\tconst auto height = prepared.height();\n\tconst auto isSmallForTiled = (width > 0 && height > 0)\n\t\t&& (width < kMinimumTiledSize || height < kMinimumTiledSize);\n\tif (!isSmallForTiled) {\n\t\treturn prepared;\n\t}\n\tconst auto repeatTimesX = (kMinimumTiledSize + width - 1) / width;\n\tconst auto repeatTimesY = (kMinimumTiledSize + height - 1) / height;\n\tauto result = QImage(\n\t\twidth * repeatTimesX,\n\t\theight * repeatTimesY,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(prepared.devicePixelRatio());\n\tauto imageForTiledBytes = result.bits();\n\tauto bytesInLine = width * sizeof(uint32);\n\tfor (auto timesY = 0; timesY != repeatTimesY; ++timesY) {\n\t\tauto imageBytes = prepared.constBits();\n\t\tfor (auto y = 0; y != height; ++y) {\n\t\t\tfor (auto timesX = 0; timesX != repeatTimesX; ++timesX) {\n\t\t\t\tmemcpy(imageForTiledBytes, imageBytes, bytesInLine);\n\t\t\t\timageForTiledBytes += bytesInLine;\n\t\t\t}\n\t\t\timageBytes += prepared.bytesPerLine();\n\t\t\timageForTiledBytes += result.bytesPerLine() - (repeatTimesX * bytesInLine);\n\t\t}\n\t}\n\treturn result;\n}\n\nstd::vector<ChatThemeGiftSymbol> ParseGiftSymbols(\n\t\tconst QByteArray &rects,\n\t\tQSize size,\n\t\tfloat64 scale) {\n\tif (size.isEmpty()) {\n\t\treturn {};\n\t}\n\n\tauto result = std::vector<ChatThemeGiftSymbol>();\n\tauto offset = 0;\n\tconst auto jumpPast = [&](const QByteArray &what) {\n\t\tconst auto pos = rects.indexOf(what, offset);\n\t\tif (pos < 0) {\n\t\t\treturn false;\n\t\t}\n\t\toffset = pos + what.size();\n\t\treturn true;\n\t};\n\tconst auto takeDouble = [&](float64 &value, char finish = '\"') {\n\t\tconst auto end = rects.indexOf(finish, offset);\n\t\tif (end < 0) {\n\t\t\treturn false;\n\t\t}\n\t\tvalue = rects.mid(offset, end - offset).toDouble();\n\t\toffset = end + 1;\n\t\treturn true;\n\t};\n\tconst auto maybeTakeRotation = [&](float64 &value) {\n\t\tconst auto end = rects.indexOf(\"/>\"_q, offset);\n\t\tconst auto pos = rects.indexOf(\"rotate(\"_q, offset);\n\t\tif (pos > offset && end > offset && pos < end) {\n\t\t\toffset = pos + 7;\n\t\t\treturn takeDouble(value, ')');\n\t\t}\n\t\treturn true;\n\t};\n\tconst auto cw = scale / float64(size.width());\n\tconst auto ch = scale / float64(size.height());\n\twhile (true) {\n\t\tauto x = float64(), y = float64(), w = float64(), h = float64();\n\t\tauto rotation = float64();\n\t\tif (!jumpPast(\"<rect \"_q)\n\t\t\t|| !jumpPast(\"x=\\\"\"_q)\n\t\t\t|| !takeDouble(x)\n\t\t\t|| !jumpPast(\"y=\\\"\"_q)\n\t\t\t|| !takeDouble(y)\n\t\t\t|| !jumpPast(\"width=\\\"\"_q)\n\t\t\t|| !takeDouble(w)\n\t\t\t|| !jumpPast(\"height=\\\"\"_q)\n\t\t\t|| !takeDouble(h)\n\t\t\t|| !maybeTakeRotation(rotation)\n\t\t\t|| !jumpPast(\"/>\"_q)) {\n\t\t\tbreak;\n\t\t}\n\t\tresult.push_back({ .area = {\n\t\t\tx * cw,\n\t\t\ty * ch,\n\t\t\tw * cw,\n\t\t\th * ch,\n\t\t}, .rotation = rotation });\n\t}\n\treturn result;\n}\n\nBackgroundImageFields ReadBackgroundImage(\n\t\tconst QString &path,\n\t\tconst QByteArray &content,\n\t\tbool gzipSvg,\n\t\tbool findGiftSymbols) {\n\tauto read = Images::Read({\n\t\t.path = path,\n\t\t.content = content,\n\t\t.svgCutOutId = findGiftSymbols ? \"GiftPatterns\"_q : QByteArray(),\n\t\t.maxSize = QSize(kMaxSize, kMaxSize),\n\t\t.gzipSvg = gzipSvg,\n\t});\n\tauto result = BackgroundImageFields{\n\t\t.image = std::move(read.image),\n\t};\n\tif (const auto &rects = read.svgCutOutContent; !rects.isEmpty()) {\n\t\tresult.giftSymbols = ParseGiftSymbols(\n\t\t\trects,\n\t\t\tresult.image.size(),\n\t\t\tread.scale);\n\t}\n\treturn result;\n}\n\nQImage GenerateBackgroundImage(\n\t\tQSize size,\n\t\tconst std::vector<QColor> &bg,\n\t\tint gradientRotation,\n\t\tfloat64 patternOpacity,\n\t\tFn<void(QPainter&,bool)> drawPattern) {\n\tauto result = bg.empty()\n\t\t? Images::GenerateGradient(size, { DefaultBackgroundColor() })\n\t\t: Images::GenerateGradient(size, bg, gradientRotation);\n\tif (bg.size() > 1 && (!drawPattern || patternOpacity >= 0.)) {\n\t\tresult = Images::DitherImage(std::move(result));\n\t}\n\tif (drawPattern) {\n\t\tauto p = QPainter(&result);\n\t\tif (patternOpacity >= 0.) {\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_SoftLight);\n\t\t\tp.setOpacity(patternOpacity);\n\t\t} else {\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\t}\n\t\tdrawPattern(p, IsPatternInverted(bg, patternOpacity));\n\t\tif (patternOpacity < 0. && patternOpacity > -1.) {\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\t\tp.setOpacity(1. + patternOpacity);\n\t\t\tp.fillRect(QRect{ QPoint(), size }, Qt::black);\n\t\t}\n\t}\n\n\treturn std::move(result).convertToFormat(\n\t\tQImage::Format_ARGB32_Premultiplied);\n}\n\nQImage PreparePatternImage(\n\t\tQImage pattern,\n\t\tconst std::vector<QColor> &bg,\n\t\tint gradientRotation,\n\t\tfloat64 patternOpacity) {\n\tauto result = GenerateBackgroundImage(\n\t\tpattern.size(),\n\t\tbg,\n\t\tgradientRotation,\n\t\tpatternOpacity,\n\t\t[&](QPainter &p, bool inverted) {\n\t\t\tif (inverted) {\n\t\t\t\tpattern = InvertPatternImage(std::move(pattern));\n\t\t\t}\n\t\t\tp.drawImage(QRect(QPoint(), pattern.size()), pattern);\n\t\t});\n\n\tpattern = QImage();\n\treturn result;\n}\n\nQImage InvertPatternImage(QImage pattern) {\n\tpattern = std::move(pattern).convertToFormat(\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tconst auto w = pattern.bytesPerLine() / 4;\n\tauto ints = reinterpret_cast<uint32*>(pattern.bits());\n\tfor (auto y = 0, h = pattern.height(); y != h; ++y) {\n\t\tfor (auto x = 0; x != w; ++x) {\n\t\t\tconst auto value = (*ints >> 24);\n\t\t\t*ints++ = (value << 24)\n\t\t\t\t| (value << 16)\n\t\t\t\t| (value << 8)\n\t\t\t\t| value;\n\t\t}\n\t}\n\treturn pattern;\n}\n\nQImage PrepareBlurredBackground(QImage image) {\n\tconstexpr auto kSize = 900;\n\tconstexpr auto kRadius = 24;\n\tif (image.width() > kSize || image.height() > kSize) {\n\t\timage = image.scaled(\n\t\t\tkSize,\n\t\t\tkSize,\n\t\t\tQt::KeepAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t}\n\treturn Images::BlurLargeImage(std::move(image), kRadius);\n}\n\nQImage GenerateDitheredGradient(\n\t\tconst std::vector<QColor> &colors,\n\t\tint rotation) {\n\tconstexpr auto kSize = 512;\n\tconst auto size = QSize(kSize, kSize);\n\tif (colors.empty()) {\n\t\treturn Images::GenerateGradient(size, { DefaultBackgroundColor() });\n\t}\n\tauto result = Images::GenerateGradient(size, colors, rotation);\n\tif (colors.size() > 1) {\n\t\tresult = Images::DitherImage(std::move(result));\n\t}\n\treturn result;\n}\n\nChatThemeBackground PrepareBackgroundImage(\n\t\tconst ChatThemeBackgroundData &data) {\n\tconst auto needBackground = (data.isPattern || data.colors.empty());\n\tauto read = needBackground\n\t\t? ReadBackgroundImage(\n\t\t\tdata.path,\n\t\t\tdata.bytes,\n\t\t\tdata.gzipSvg,\n\t\t\t!data.giftSymbolFrame.isNull())\n\t\t: BackgroundImageFields();\n\tauto prepared = needBackground\n\t\t? PreprocessBackgroundImage(std::move(read.image))\n\t\t: QImage();\n\tif (data.isPattern && !prepared.isNull()) {\n\t\tif (data.colors.size() < 2) {\n\t\t\tprepared = PreparePatternImage(\n\t\t\t\tstd::move(prepared),\n\t\t\t\tdata.colors,\n\t\t\t\tdata.gradientRotation,\n\t\t\t\tdata.patternOpacity);\n\t\t} else if (IsPatternInverted(data.colors, data.patternOpacity)) {\n\t\t\tprepared = InvertPatternImage(std::move(prepared));\n\t\t}\n\t\tprepared.setDevicePixelRatio(style::DevicePixelRatio());\n\t} else if (data.colors.empty()) {\n\t\tprepared.setDevicePixelRatio(style::DevicePixelRatio());\n\t}\n\tif (!prepared.isNull()\n\t\t&& !data.isPattern\n\t\t&& data.forDarkMode\n\t\t&& data.darkModeDimming > 0) {\n\t\tconst auto ratio = int(prepared.devicePixelRatio());\n\t\tauto p = QPainter(&prepared);\n\t\tp.fillRect(\n\t\t\tQRect(0, 0, prepared.width() / ratio, prepared.height() / ratio),\n\t\t\tQColor(0, 0, 0, 255 * data.darkModeDimming / 100));\n\t}\n\tconst auto imageMonoColor = (data.colors.size() < 2)\n\t\t? CalculateImageMonoColor(prepared)\n\t\t: std::nullopt;\n\tif (!prepared.isNull() && !data.isPattern && data.isBlurred) {\n\t\tprepared = PrepareBlurredBackground(std::move(prepared));\n\t}\n\tauto gradientForFill = (data.generateGradient && data.colors.size() > 1)\n\t\t? Ui::GenerateDitheredGradient(data.colors, data.gradientRotation)\n\t\t: QImage();\n\treturn ChatThemeBackground{\n\t\t.key = data.key,\n\t\t.prepared = prepared,\n\t\t.preparedForTiled = PrepareImageForTiled(prepared),\n\t\t.gradientForFill = std::move(gradientForFill),\n\t\t.colorForFill = (!prepared.isNull()\n\t\t\t? imageMonoColor\n\t\t\t: (data.colors.size() > 1 || data.colors.empty())\n\t\t\t? std::nullopt\n\t\t\t: std::make_optional(data.colors.front())),\n\t\t.colors = data.colors,\n\t\t.giftSymbols = std::move(read.giftSymbols),\n\t\t.giftSymbolFrame = data.giftSymbolFrame,\n\t\t.giftId = data.giftId,\n\t\t.patternOpacity = data.patternOpacity,\n\t\t.gradientRotation = data.generateGradient ? data.gradientRotation : 0,\n\t\t.isPattern = data.isPattern,\n\t};\n}\n\n[[nodiscard]] QImage PrepareGiftSymbol(\n\t\tconst std::unique_ptr<Text::CustomEmoji> &emoji) {\n\tif (!emoji || !emoji->ready()) {\n\t\treturn QImage();\n\t}\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto esize = Ui::Emoji::GetSizeLarge() / ratio;\n\tconst auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);\n\tauto result = QImage(\n\t\tQSize(customSize, customSize) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\tresult.fill(Qt::transparent);\n\tauto p = QPainter(&result);\n\temoji->paint(p, {\n\t\t.textColor = QColor(0, 0, 0),\n\t});\n\treturn result;\n}\n\n} // namespace Window::Theme\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/chat_theme.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n\nnamespace style {\nclass palette;\nstruct colorizer;\n} // namespace style\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nnamespace Ui {\n\nclass ChatStyle;\nstruct ChatPaintContext;\nstruct BubblePattern;\n\nstruct ChatThemeGiftSymbol {\n\tQRectF area;\n\tfloat64 rotation = 0.;\n};\n\nstruct ChatThemeBackground {\n\tQString key;\n\tQImage prepared;\n\tQImage preparedForTiled;\n\tQImage gradientForFill;\n\tstd::optional<QColor> colorForFill;\n\tstd::vector<QColor> colors;\n\tstd::vector<ChatThemeGiftSymbol> giftSymbols;\n\tQImage giftSymbolFrame;\n\tuint64_t giftId = 0;\n\tfloat64 patternOpacity = 1.;\n\tint gradientRotation = 0;\n\tbool isPattern = false;\n\tbool tile = false;\n\n\t[[nodiscard]] bool waitingForNegativePattern() const {\n\t\treturn isPattern && prepared.isNull() && (patternOpacity < 0.);\n\t}\n};\n\nbool operator==(const ChatThemeBackground &a, const ChatThemeBackground &b);\nbool operator!=(const ChatThemeBackground &a, const ChatThemeBackground &b);\n\nstruct ChatThemeBackgroundData {\n\tQString key;\n\tQString path;\n\tQByteArray bytes;\n\tQImage giftSymbolFrame;\n\tuint64 giftId = 0;\n\tbool gzipSvg = false;\n\tstd::vector<QColor> colors;\n\tbool isPattern = false;\n\tfloat64 patternOpacity = 0.;\n\tint darkModeDimming = 0;\n\tbool isBlurred = false;\n\tbool forDarkMode = false;\n\tbool generateGradient = false;\n\tint gradientRotation = 0;\n};\n\nstruct ChatThemeBubblesData {\n\tstd::vector<QColor> colors;\n\tstd::optional<QColor> accent;\n};\n\nstruct CacheBackgroundRequest {\n\tChatThemeBackground background;\n\tQSize area;\n\tint gradientRotationAdd = 0;\n\tfloat64 gradientProgress = 1.;\n\n\texplicit operator bool() const {\n\t\treturn !background.prepared.isNull()\n\t\t\t|| !background.gradientForFill.isNull();\n\t}\n};\n\nbool operator==(\n\tconst CacheBackgroundRequest &a,\n\tconst CacheBackgroundRequest &b);\nbool operator!=(\n\tconst CacheBackgroundRequest &a,\n\tconst CacheBackgroundRequest &b);\n\nstruct CacheBackgroundResult {\n\tQImage image;\n\tQImage gradient;\n\tQSize area;\n\tint x = 0;\n\tint y = 0;\n\tQRect giftArea;\n\tfloat64 giftRotation = 0;\n\tbool waitingForNegativePattern = false;\n};\n\n[[nodiscard]] CacheBackgroundResult CacheBackground(\n\tconst CacheBackgroundRequest &request);\n\nstruct CachedBackground {\n\tCachedBackground() = default;\n\tCachedBackground(CacheBackgroundResult &&result);\n\n\tQPixmap pixmap;\n\tQSize area;\n\tint x = 0;\n\tint y = 0;\n\tQRect giftArea;\n\tfloat64 giftRotation = 0.;\n\tmutable std::unique_ptr<Text::CustomEmoji> gift;\n\tbool waitingForNegativePattern = false;\n};\n\nstruct BackgroundState {\n\tCachedBackground was;\n\tCachedBackground now;\n\tfloat64 shown = 1.;\n};\n\nstruct ChatThemeKey {\n\tuint64 id = 0;\n\tbool dark = false;\n\n\texplicit operator bool() const {\n\t\treturn (id != 0);\n\t}\n\n\tfriend inline auto operator<=>(ChatThemeKey, ChatThemeKey) = default;\n\tfriend inline bool operator==(ChatThemeKey, ChatThemeKey) = default;\n};\n\nstruct ChatThemeDescriptor {\n\tChatThemeKey key;\n\tFn<void(style::palette&)> preparePalette;\n\tChatThemeBackgroundData backgroundData;\n\tChatThemeBubblesData bubblesData;\n\tbool basedOnDark = false;\n};\n\nclass ChatTheme final : public base::has_weak_ptr {\npublic:\n\tChatTheme();\n\n\t// Expected to be invoked on a background thread. Invokes callbacks there.\n\tChatTheme(ChatThemeDescriptor &&descriptor);\n\n\t~ChatTheme();\n\n\t[[nodiscard]] ChatThemeKey key() const;\n\t[[nodiscard]] const style::palette *palette() const {\n\t\treturn _palette.get();\n\t}\n\n\tvoid setBackground(ChatThemeBackground &&background);\n\tvoid updateBackgroundImageFrom(ChatThemeBackground &&background);\n\t[[nodiscard]] const ChatThemeBackground &background() const {\n\t\treturn _mutableBackground;\n\t}\n\n\tvoid setBubblesBackground(QImage image);\n\t[[nodiscard]] const BubblePattern *bubblesBackgroundPattern() const {\n\t\treturn _bubblesBackgroundPattern.get();\n\t}\n\tvoid finishCreateOnMain(); // Called on_main after setBubblesBackground.\n\n\t[[nodiscard]] ChatPaintContext preparePaintContext(\n\t\tnot_null<const ChatStyle*> st,\n\t\tQRect viewport,\n\t\tQRect area,\n\t\tQRect clip,\n\t\tbool paused);\n\t[[nodiscard]] const BackgroundState &backgroundState(QSize area);\n\tvoid clearBackgroundState();\n\t[[nodiscard]] rpl::producer<> repaintBackgroundRequests() const;\n\tvoid rotateComplexGradientBackground();\n\n\t[[nodiscard]] CacheBackgroundRequest cacheBackgroundRequest(\n\t\tQSize area,\n\t\tint addRotation = 0) const;\n\nprivate:\n\tvoid cacheBackground();\n\tvoid cacheBackgroundNow();\n\tvoid cacheBackgroundAsync(\n\t\tconst CacheBackgroundRequest &request,\n\t\tFn<void(CacheBackgroundResult&&)> done = nullptr);\n\tvoid setCachedBackground(CacheBackgroundResult &&cached);\n\t[[nodiscard]] bool readyForBackgroundRotation() const;\n\tvoid generateNextBackgroundRotation();\n\n\tvoid cacheBubbles();\n\tvoid cacheBubblesNow();\n\tvoid cacheBubblesAsync(\n\t\tconst CacheBackgroundRequest &request);\n\t[[nodiscard]] CacheBackgroundRequest cacheBubblesRequest(\n\t\tQSize area) const;\n\n\t[[nodiscard]] style::colorizer bubblesAccentColorizer(\n\t\tconst QColor &accent) const;\n\tvoid adjustPalette(const ChatThemeDescriptor &descriptor);\n\tvoid set(const style::color &my, const QColor &color);\n\tvoid adjust(const style::color &my, const QColor &by);\n\tvoid adjust(const style::color &my, const style::colorizer &by);\n\n\tChatThemeKey _key;\n\tstd::unique_ptr<style::palette> _palette;\n\tChatThemeBackground _mutableBackground;\n\tBackgroundState _backgroundState;\n\tAnimations::Simple _backgroundFade;\n\tCacheBackgroundRequest _backgroundCachingRequest;\n\tCacheBackgroundRequest _nextCachingRequest;\n\tCacheBackgroundResult _backgroundNext;\n\tQSize _cacheBackgroundArea;\n\tcrl::time _lastBackgroundAreaChangeTime = 0;\n\tstd::optional<base::Timer> _cacheBackgroundTimer;\n\n\tCachedBackground _bubblesBackground;\n\tQImage _bubblesBackgroundPrepared;\n\tCacheBackgroundRequest _bubblesCachingRequest;\n\tQSize _cacheBubblesArea;\n\tcrl::time _lastBubblesAreaChangeTime = 0;\n\tstd::optional<base::Timer> _cacheBubblesTimer;\n\tstd::unique_ptr<BubblePattern> _bubblesBackgroundPattern;\n\n\trpl::event_stream<> _repaintBackgroundRequests;\n\n\trpl::lifetime _lifetime;\n\n};\n\nstruct ChatBackgroundRects {\n\tQRect from;\n\tQRect to;\n};\n[[nodiscard]] ChatBackgroundRects ComputeChatBackgroundRects(\n\tQSize fillSize,\n\tQSize imageSize);\n\n[[nodiscard]] QColor CountAverageColor(const QImage &image);\n[[nodiscard]] QColor CountAverageColor(const std::vector<QColor> &colors);\n[[nodiscard]] bool IsPatternInverted(\n\tconst std::vector<QColor> &background,\n\tfloat64 patternOpacity);\n[[nodiscard]] QColor ThemeAdjustedColor(QColor original, QColor background);\n[[nodiscard]] QImage PreprocessBackgroundImage(QImage image);\n[[nodiscard]] std::optional<QColor> CalculateImageMonoColor(\n\tconst QImage &image);\n[[nodiscard]] QImage PrepareImageForTiled(const QImage &prepared);\n\nstruct BackgroundImageFields {\n\tQImage image;\n\tstd::vector<ChatThemeGiftSymbol> giftSymbols;\n};\n[[nodiscard]] BackgroundImageFields ReadBackgroundImage(\n\tconst QString &path,\n\tconst QByteArray &content,\n\tbool gzipSvg,\n\tbool findGiftSymbols = false);\n[[nodiscard]] QImage GenerateBackgroundImage(\n\tQSize size,\n\tconst std::vector<QColor> &bg,\n\tint gradientRotation,\n\tfloat64 patternOpacity = 1.,\n\tFn<void(QPainter&,bool)> drawPattern = nullptr);\n[[nodiscard]] QImage InvertPatternImage(QImage pattern);\n[[nodiscard]] QImage PreparePatternImage(\n\tQImage pattern,\n\tconst std::vector<QColor> &bg,\n\tint gradientRotation,\n\tfloat64 patternOpacity);\n[[nodiscard]] QImage PrepareBlurredBackground(QImage image);\n[[nodiscard]] QImage GenerateDitheredGradient(\n\tconst std::vector<QColor> &colors,\n\tint rotation);\n[[nodiscard]] ChatThemeBackground PrepareBackgroundImage(\n\tconst ChatThemeBackgroundData &data);\n[[nodiscard]] QImage PrepareGiftSymbol(\n\tconst std::unique_ptr<Text::CustomEmoji> &emoji);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/chats_filter_tag.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/chats_filter_tag.h\"\n\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/integration.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Ui {\nnamespace {\n\nclass ScaledSimpleEmoji final : public Ui::Text::CustomEmoji {\npublic:\n\tScaledSimpleEmoji(EmojiPtr emoji);\n\n\tint width() override;\n\tQString entityData() override;\n\tvoid paint(QPainter &p, const Context &context) override;\n\tvoid unload() override;\n\tbool ready() override;\n\tbool readyInDefaultState() override;\n\nprivate:\n\tconst EmojiPtr _emoji;\n\tQImage _frame;\n\tQPoint _shift;\n\n};\n\nclass ScaledCustomEmoji final : public Ui::Text::CustomEmoji {\npublic:\n\tScaledCustomEmoji(std::unique_ptr<Ui::Text::CustomEmoji> wrapped);\n\n\tint width() override;\n\tQString entityData() override;\n\tvoid paint(QPainter &p, const Context &context) override;\n\tvoid unload() override;\n\tbool ready() override;\n\tbool readyInDefaultState() override;\n\nprivate:\n\tconst std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;\n\tQImage _frame;\n\tQPoint _shift;\n\n};\n\n[[nodiscard]] int ScaledSize() {\n\treturn st::dialogRowFilterTagStyle.font->height - 2 * st::lineWidth;\n}\n\nScaledSimpleEmoji::ScaledSimpleEmoji(EmojiPtr emoji)\n: _emoji(emoji) {\n}\n\nint ScaledSimpleEmoji::width() {\n\treturn ScaledSize();\n}\n\nQString ScaledSimpleEmoji::entityData() {\n\treturn u\"scaled-simple:\"_q + _emoji->text();\n}\n\nvoid ScaledSimpleEmoji::paint(QPainter &p, const Context &context) {\n\tif (_frame.isNull()) {\n\t\tconst auto adjusted = Text::AdjustCustomEmojiSize(st::emojiSize);\n\t\tconst auto xskip = (st::emojiSize - adjusted) / 2;\n\t\tconst auto yskip = xskip + (width() - st::emojiSize) / 2;\n\t\t_shift = { xskip, yskip };\n\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto large = Emoji::GetSizeLarge();\n\t\tconst auto size = QSize(large, large);\n\t\t_frame = QImage(size, QImage::Format_ARGB32_Premultiplied);\n\t\t_frame.setDevicePixelRatio(ratio);\n\t\t_frame.fill(Qt::transparent);\n\n\t\tauto p = QPainter(&_frame);\n\t\tEmoji::Draw(p, _emoji, large, 0, 0);\n\t\tp.end();\n\n\t\t_frame = _frame.scaled(\n\t\t\tQSize(width(), width()) * ratio,\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t}\n\n\tp.drawImage(context.position - _shift, _frame);\n}\n\nvoid ScaledSimpleEmoji::unload() {\n}\n\nbool ScaledSimpleEmoji::ready() {\n\treturn true;\n}\n\nbool ScaledSimpleEmoji::readyInDefaultState() {\n\treturn true;\n}\n\nScaledCustomEmoji::ScaledCustomEmoji(\n\tstd::unique_ptr<Ui::Text::CustomEmoji> wrapped)\n: _wrapped(std::move(wrapped)) {\n}\n\nint ScaledCustomEmoji::width() {\n\treturn ScaledSize();\n}\n\nQString ScaledCustomEmoji::entityData() {\n\treturn u\"scaled-custom:\"_q + _wrapped->entityData();\n}\n\nvoid ScaledCustomEmoji::paint(QPainter &p, const Context &context) {\n\tif (_frame.isNull()) {\n\t\tif (!_wrapped->ready()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto large = Emoji::GetSizeLarge();\n\t\tconst auto largeadjust = Text::AdjustCustomEmojiSize(large / ratio);\n\t\tconst auto size = QSize(largeadjust, largeadjust) * ratio;\n\t\t_frame = QImage(size, QImage::Format_ARGB32_Premultiplied);\n\t\t_frame.setDevicePixelRatio(ratio);\n\t\t_frame.fill(Qt::transparent);\n\n\t\tauto p = QPainter(&_frame);\n\t\tp.translate(-context.position);\n\t\tconst auto was = context.internal.forceFirstFrame;\n\t\tcontext.internal.forceFirstFrame = true;\n\t\t_wrapped->paint(p, context);\n\t\tcontext.internal.forceFirstFrame = was;\n\t\tp.end();\n\n\t\tconst auto smalladjust = Text::AdjustCustomEmojiSize(width());\n\t\t_frame = _frame.scaled(\n\t\t\tQSize(smalladjust, smalladjust) * ratio,\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t\t_wrapped->unload();\n\n\t\tconst auto adjusted = Text::AdjustCustomEmojiSize(st::emojiSize);\n\t\tconst auto xskip = (st::emojiSize - adjusted) / 2;\n\t\tconst auto yskip = xskip + (width() - st::emojiSize) / 2;\n\n\t\tconst auto add = (width() - smalladjust) / 2;\n\t\t_shift = QPoint(xskip, yskip) - QPoint(add, add);\n\t}\n\tp.drawImage(context.position - _shift, _frame);\n}\n\nvoid ScaledCustomEmoji::unload() {\n\t_wrapped->unload();\n}\n\nbool ScaledCustomEmoji::ready() {\n\treturn !_frame.isNull() || _wrapped->ready();\n}\n\nbool ScaledCustomEmoji::readyInDefaultState() {\n\treturn !_frame.isNull() || _wrapped->ready();\n}\n\n[[nodiscard]] TextWithEntities PrepareSmallEmojiText(\n\t\tTextWithEntities text,\n\t\tChatsFilterTagContext &context) {\n\tauto i = text.entities.begin();\n\tauto ch = text.text.constData();\n\tcontext.loading = false;\n\tconst auto end = text.text.constData() + text.text.size();\n\tconst auto adjust = [&](EntityInText &entity) {\n\t\tif (entity.type() != EntityType::CustomEmoji) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto data = entity.data();\n\t\tif (data.startsWith(u\"scaled-simple:\"_q)) {\n\t\t\treturn;\n\t\t}\n\t\tauto &emoji = context.emoji[data];\n\t\tif (!emoji) {\n\t\t\temoji = Text::MakeCustomEmoji(data, context.textContext);\n\t\t}\n\t\tif (!emoji->ready()) {\n\t\t\tcontext.loading = true;\n\t\t}\n\t\tentity = EntityInText(\n\t\t\tentity.type(),\n\t\t\tentity.offset(),\n\t\t\tentity.length(),\n\t\t\tu\"scaled-custom:\"_q + entity.data());\n\t};\n\tconst auto till = [](EntityInText &entity) {\n\t\treturn entity.offset() + entity.length();\n\t};\n\twhile (ch != end) {\n\t\tauto emojiLength = 0;\n\t\tif (const auto emoji = Ui::Emoji::Find(ch, end, &emojiLength)) {\n\t\t\tconst auto f = int(ch - text.text.constData());\n\t\t\tconst auto l = f + emojiLength;\n\t\t\twhile (i != text.entities.end() && till(*i) <= f) {\n\t\t\t\tadjust(*i);\n\t\t\t\t++i;\n\t\t\t}\n\n\t\t\tch += emojiLength;\n\t\t\tif (i != text.entities.end() && i->offset() < l) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ti = text.entities.insert(i, EntityInText{\n\t\t\t\tEntityType::CustomEmoji,\n\t\t\t\tf,\n\t\t\t\temojiLength,\n\t\t\t\tu\"scaled-simple:\"_q + emoji->text(),\n\t\t\t});\n\t\t} else {\n\t\t\t++ch;\n\t\t}\n\t}\n\tfor (; i != text.entities.end(); ++i) {\n\t\tadjust(*i);\n\t}\n\treturn text;\n}\n\n} // namespace\n\nQImage ChatsFilterTag(\n\t\tconst TextWithEntities &text,\n\t\tChatsFilterTagContext &context) {\n\tconst auto &roundedFont = st::dialogRowFilterTagStyle.font;\n\tconst auto additionalWidth = roundedFont->spacew * 3;\n\tauto rich = Text::String(\n\t\tst::dialogRowFilterTagStyle,\n\t\tPrepareSmallEmojiText(text, context),\n\t\tkMarkupTextOptions,\n\t\tkQFixedMax,\n\t\tcontext.textContext);\n\tconst auto roundedWidth = rich.maxWidth() + additionalWidth;\n\tconst auto rect = QRect(0, 0, roundedWidth, roundedFont->height);\n\tauto cache = QImage(\n\t\trect.size() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tcache.setDevicePixelRatio(style::DevicePixelRatio());\n\tcache.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&cache);\n\t\tconst auto pen = QPen(context.active\n\t\t\t? st::dialogsBgActive->c\n\t\t\t: context.color);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(context.active\n\t\t\t? st::dialogsTextFgActive->c\n\t\t\t: anim::with_alpha(pen.color(), .15));\n\t\t{\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tconst auto radius = roundedFont->height / 3.;\n\t\t\tp.drawRoundedRect(rect, radius, radius);\n\t\t}\n\t\tp.setPen(pen);\n\t\tp.setFont(roundedFont);\n\t\tconst auto dx = (rect.width() - rich.maxWidth()) / 2;\n\t\tconst auto dy = (rect.height() - roundedFont->height) / 2;\n\t\trich.draw(p, {\n\t\t\t.position = rect.topLeft() + QPoint(dx, dy),\n\t\t\t.availableWidth = rich.maxWidth(),\n\t\t});\n\t}\n\treturn cache;\n}\n\nstd::unique_ptr<Text::CustomEmoji> MakeScaledSimpleEmoji(EmojiPtr emoji) {\n\treturn std::make_unique<ScaledSimpleEmoji>(emoji);\n}\n\nstd::unique_ptr<Text::CustomEmoji> MakeScaledCustomEmoji(\n\t\tstd::unique_ptr<Text::CustomEmoji> wrapped) {\n\treturn std::make_unique<ScaledCustomEmoji>(std::move(wrapped));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/chats_filter_tag.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"emoji.h\"\n#include \"ui/text/text.h\"\n\nnamespace Ui {\n\nstruct ChatsFilterTagContext {\n\tbase::flat_map<QString, std::unique_ptr<Text::CustomEmoji>> emoji;\n\tText::MarkedContext textContext;\n\tQColor color;\n\tbool active = false;\n\tbool loading = false;\n};\n\n[[nodiscard]] QImage ChatsFilterTag(\n\tconst TextWithEntities &text,\n\tChatsFilterTagContext &context);\n\n[[nodiscard]] std::unique_ptr<Text::CustomEmoji> MakeScaledSimpleEmoji(\n\tEmojiPtr emoji);\n\n[[nodiscard]] std::unique_ptr<Text::CustomEmoji> MakeScaledCustomEmoji(\n\tstd::unique_ptr<Text::CustomEmoji> wrapped);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/choose_send_as.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/choose_send_as.h\"\n\n#include \"boxes/peer_list_box.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_peer_values.h\"\n#include \"history/history.h\"\n#include \"ui/controls/send_as_button.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"window/window_session_controller.h\"\n#include \"main/main_session.h\"\n#include \"main/session/send_as_peers.h\"\n#include \"lang/lang_keys.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Ui {\nnamespace {\n\nclass Row final : public PeerListRow {\npublic:\n\texplicit Row(const Main::SendAsPeer &sendAsPeer);\n\n\tint paintNameIconGetWidth(\n\t\tPainter &p,\n\t\tFn<void()> repaint,\n\t\tcrl::time now,\n\t\tint nameLeft,\n\t\tint nameTop,\n\t\tint nameWidth,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) override;\n\nprivate:\n\tbool _premiumRequired = false;\n\n};\n\nclass ListController final : public PeerListController {\npublic:\n\tListController(\n\t\tstd::vector<Main::SendAsPeer> list,\n\t\tnot_null<PeerData*> selected);\n\n\tMain::Session &session() const override;\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\n\t[[nodiscard]] rpl::producer<not_null<PeerData*>> clicked() const;\n\nprivate:\n\tstd::unique_ptr<Row> createRow(const Main::SendAsPeer &sendAsPeer);\n\n\tstd::vector<Main::SendAsPeer> _list;\n\tnot_null<PeerData*> _selected;\n\trpl::event_stream<not_null<PeerData*>> _clicked;\n\n};\n\nRow::Row(const Main::SendAsPeer &sendAsPeer)\n: PeerListRow(sendAsPeer.peer)\n, _premiumRequired(sendAsPeer.premiumRequired) {\n}\n\nint Row::paintNameIconGetWidth(\n\t\tPainter &p,\n\t\tFn<void()> repaint,\n\t\tcrl::time now,\n\t\tint nameLeft,\n\t\tint nameTop,\n\t\tint nameWidth,\n\t\tint availableWidth,\n\t\tint outerWidth,\n\t\tbool selected) {\n\tif (_premiumRequired && !peer()->session().premium()) {\n\t\tconst auto &icon = st::emojiPremiumRequired;\n\t\tavailableWidth -= icon.width();\n\t\tconst auto x = nameLeft + std::min(nameWidth, availableWidth);\n\t\ticon.paint(p, x, nameTop, outerWidth);\n\t\treturn icon.width();\n\t}\n\treturn PeerListRow::paintNameIconGetWidth(\n\t\tp,\n\t\tstd::move(repaint),\n\t\tnow,\n\t\tnameLeft,\n\t\tnameTop,\n\t\tnameWidth,\n\t\tavailableWidth,\n\t\touterWidth,\n\t\tselected);\n}\n\nListController::ListController(\n\tstd::vector<Main::SendAsPeer> list,\n\tnot_null<PeerData*> selected)\n: PeerListController()\n, _list(std::move(list))\n, _selected(selected) {\n\tData::AmPremiumValue(\n\t\t&selected->session()\n\t) | rpl::skip(1) | rpl::on_next([=] {\n\t\tconst auto count = delegate()->peerListFullRowsCount();\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tdelegate()->peerListUpdateRow(\n\t\t\t\tdelegate()->peerListRowAt(i));\n\t\t}\n\t}, lifetime());\n}\n\nMain::Session &ListController::session() const {\n\treturn _selected->session();\n}\n\nstd::unique_ptr<Row> ListController::createRow(\n\t\tconst Main::SendAsPeer &sendAsPeer) {\n\tauto result = std::make_unique<Row>(sendAsPeer);\n\tif (sendAsPeer.peer->isSelf()) {\n\t\tresult->setCustomStatus(\n\t\t\ttr::lng_group_call_join_as_personal(tr::now));\n\t} else if (sendAsPeer.peer->isMegagroup()) {\n\t\tresult->setCustomStatus(tr::lng_send_as_anonymous_admin(tr::now));\n\t} else if (const auto channel = sendAsPeer.peer->asChannel()) {\n\t\tresult->setCustomStatus(tr::lng_chat_status_subscribers(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tchannel->membersCount()));\n\t}\n\treturn result;\n}\n\nvoid ListController::prepare() {\n\tdelegate()->peerListSetSearchMode(PeerListSearchMode::Disabled);\n\tfor (const auto &sendAsPeer : _list) {\n\t\tauto row = createRow(sendAsPeer);\n\t\tconst auto raw = row.get();\n\t\tdelegate()->peerListAppendRow(std::move(row));\n\t\tif (sendAsPeer.peer == _selected) {\n\t\t\tdelegate()->peerListSetRowChecked(raw, true);\n\t\t\traw->finishCheckedAnimation();\n\t\t}\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid ListController::rowClicked(not_null<PeerListRow*> row) {\n\tconst auto peer = row->peer();\n\tif (peer == _selected) {\n\t\treturn;\n\t}\n\t_clicked.fire_copy(peer);\n}\n\nrpl::producer<not_null<PeerData*>> ListController::clicked() const {\n\treturn _clicked.events();\n}\n\n} // namespace\n\nvoid ChooseSendAsBox(\n\t\tnot_null<GenericBox*> box,\n\t\tconst style::ChooseSendAs &st,\n\t\tstd::vector<Main::SendAsPeer> list,\n\t\tnot_null<PeerData*> chosen,\n\t\tFn<bool(not_null<PeerData*>)> done) {\n\tExpects(ranges::contains(list, chosen, &Main::SendAsPeer::peer));\n\tExpects(done != nullptr);\n\n\tbox->setWidth(st::groupCallJoinAsWidth);\n\tbox->setTitle(tr::lng_send_as_title());\n\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\tbox,\n\t\ttr::lng_group_call_join_as_about(),\n\t\tst.label));\n\n\tauto &lifetime = box->lifetime();\n\tconst auto delegate = lifetime.make_state<\n\t\tPeerListContentDelegateSimple\n\t>();\n\tconst auto controller = lifetime.make_state<ListController>(\n\t\tlist,\n\t\tchosen);\n\tcontroller->setStyleOverrides(&st.list, nullptr);\n\n\tcontroller->clicked(\n\t) | rpl::on_next([=](not_null<PeerData*> peer) {\n\t\tconst auto weak = base::make_weak(box);\n\t\tif (done(peer) && weak) {\n\t\t\tbox->closeBox();\n\t\t}\n\t}, box->lifetime());\n\n\tconst auto content = box->addRow(\n\t\tobject_ptr<PeerListContent>(box, controller),\n\t\tstyle::margins());\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\tbox->addButton(tr::lng_box_done(), [=] { box->closeBox(); });\n}\n\nvoid SetupSendAsButton(\n\t\tnot_null<SendAsButton*> button,\n\t\tconst style::ChooseSendAs &st,\n\t\trpl::producer<PeerData*> active,\n\t\tstd::shared_ptr<ChatHelpers::Show> show) {\n\tusing namespace rpl::mappers;\n\tconst auto current = button->lifetime().make_state<\n\t\trpl::variable<PeerData*>\n\t>(std::move(active));\n\tbutton->setClickedCallback([=, &st] {\n\t\tconst auto peer = current->current();\n\t\tif (!peer) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto key = Main::SendAsKey{ peer, Main::SendAsType::Message };\n\t\tconst auto session = &peer->session();\n\t\tconst auto &list = session->sendAsPeers().list(key);\n\t\tif (list.size() < 2) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto done = [=](not_null<PeerData*> sendAs) {\n\t\t\tconst auto i = ranges::find(\n\t\t\t\tlist,\n\t\t\t\tsendAs,\n\t\t\t\t&Main::SendAsPeer::peer);\n\t\t\tif (i != end(list)\n\t\t\t\t&& i->premiumRequired\n\t\t\t\t&& !sendAs->session().premium()) {\n\t\t\t\tSettings::ShowPremiumPromoToast(\n\t\t\t\t\tshow,\n\t\t\t\t\ttr::lng_send_as_premium_required(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_link,\n\t\t\t\t\t\ttr::link(\n\t\t\t\t\t\t\ttr::bold(\n\t\t\t\t\t\t\t\ttr::lng_send_as_premium_required_link(\n\t\t\t\t\t\t\t\t\ttr::now))),\n\t\t\t\t\t\ttr::marked),\n\t\t\t\t\tu\"send_as\"_q);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tsession->sendAsPeers().saveChosen(peer, sendAs);\n\t\t\treturn true;\n\t\t};\n\t\tshow->show(Box(\n\t\t\tUi::ChooseSendAsBox,\n\t\t\tst,\n\t\t\tlist,\n\t\t\tsession->sendAsPeers().resolveChosen(peer),\n\t\t\tdone));\n\t});\n\n\tconst auto size = st.button.size;\n\tauto userpic = current->value(\n\t) | rpl::filter([=](PeerData *peer) {\n\t\treturn peer && peer->isChannel();\n\t}) | rpl::map([=](not_null<PeerData*> peer) {\n\t\tconst auto channel = peer->asChannel();\n\n\t\tauto updates = rpl::single(\n\t\t\trpl::empty\n\t\t) | rpl::then(channel->session().sendAsPeers().updated(\n\t\t) | rpl::filter(\n\t\t\t_1 == Main::SendAsKey(channel, Main::SendAsType::Message)\n\t\t) | rpl::to_empty);\n\n\t\treturn rpl::combine(\n\t\t\tstd::move(updates),\n\t\t\tchannel->adminRightsValue()\n\t\t) | rpl::map([=] {\n\t\t\treturn channel->session().sendAsPeers().resolveChosen(channel);\n\t\t}) | rpl::distinct_until_changed(\n\t\t) | rpl::map([=](not_null<PeerData*> chosen) {\n\t\t\treturn Data::PeerUserpicImageValue(\n\t\t\t\tchosen,\n\t\t\t\tsize * style::DevicePixelRatio());\n\t\t}) | rpl::flatten_latest();\n\t}) | rpl::flatten_latest();\n\n\tstd::move(\n\t\tuserpic\n\t) | rpl::on_next([=](QImage &&userpic) {\n\t\tbutton->setUserpic(std::move(userpic));\n\t}, button->lifetime());\n}\n\nvoid SetupSendAsButton(\n\t\tnot_null<SendAsButton*> button,\n\t\tconst style::ChooseSendAs &st,\n\t\tstd::shared_ptr<Data::GroupCall> videoStream,\n\t\tstd::shared_ptr<ChatHelpers::Show> show) {\n\tconst auto peer = videoStream->peer();\n\tconst auto type = Main::SendAsType::VideoStream;\n\tconst auto key = Main::SendAsKey{ peer, type };\n\tbutton->setClickedCallback([=, &st] {\n\t\tconst auto session = &peer->session();\n\t\tconst auto &list = session->sendAsPeers().list(key);\n\t\tif (list.size() < 2) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto done = [=](not_null<PeerData*> sendAs) {\n\t\t\tvideoStream->saveSendAs(sendAs);\n\t\t\treturn true;\n\t\t};\n\t\tshow->show(Box(\n\t\t\tUi::ChooseSendAsBox,\n\t\t\tst,\n\t\t\tlist,\n\t\t\tvideoStream->resolveSendAs(),\n\t\t\tdone));\n\t});\n\n\tconst auto size = st.button.size;\n\tauto userpic = videoStream->sendAsValue(\n\t) | rpl::distinct_until_changed(\n\t) | rpl::map([=](not_null<PeerData*> chosen) {\n\t\treturn Data::PeerUserpicImageValue(\n\t\t\tchosen,\n\t\t\tsize * style::DevicePixelRatio());\n\t}) | rpl::flatten_latest();\n\n\tstd::move(\n\t\tuserpic\n\t) | rpl::on_next([=](QImage &&userpic) {\n\t\tbutton->setUserpic(std::move(userpic));\n\t}, button->lifetime());\n}\n\nvoid SetupSendAsButton(\n\t\tnot_null<SendAsButton*> button,\n\t\tconst style::ChooseSendAs &st,\n\t\tnot_null<Window::SessionController*> window) {\n\tauto active = window->activeChatValue(\n\t) | rpl::map([=](Dialogs::Key key) {\n\t\treturn key.history() ? key.history()->peer.get() : nullptr;\n\t});\n\tSetupSendAsButton(button, st, std::move(active), window->uiShow());\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/choose_send_as.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"ui/layers/generic_box.h\"\n\nclass PeerData;\n\nnamespace style {\nstruct ChooseSendAs;\n} // namespace style\n\nnamespace Data {\nclass GroupCall;\n} // namespace Data\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Main {\nstruct SendAsPeer;\n} // namespace Main\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\n\nclass SendAsButton;\n\nvoid ChooseSendAsBox(\n\tnot_null<GenericBox*> box,\n\tconst style::ChooseSendAs &st,\n\tstd::vector<Main::SendAsPeer> list,\n\tnot_null<PeerData*> chosen,\n\tFn<bool(not_null<PeerData*>)> done);\n\nvoid SetupSendAsButton(\n\tnot_null<SendAsButton*> button,\n\tconst style::ChooseSendAs &st,\n\trpl::producer<PeerData*> active,\n\tstd::shared_ptr<ChatHelpers::Show> show);\n\nvoid SetupSendAsButton(\n\tnot_null<SendAsButton*> button,\n\tconst style::ChooseSendAs &st,\n\tstd::shared_ptr<Data::GroupCall> videoStream,\n\tstd::shared_ptr<ChatHelpers::Show> show);\n\nvoid SetupSendAsButton(\n\tnot_null<SendAsButton*> button,\n\tconst style::ChooseSendAs &st,\n\tnot_null<Window::SessionController*> window);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/choose_theme_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/choose_theme_controller.h\"\n\n#include \"boxes/background_box.h\"\n#include \"boxes/transfer_gift_box.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/chat/message_bubble.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/painter.h\"\n#include \"main/main_session.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/themes/window_theme.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_session.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"lang/lang_keys.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_layers.h\" // boxTitle.\n#include \"styles/style_settings.h\"\n#include \"styles/style_window.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace Ui {\nnamespace {\n\nconst auto kDisableElement = [] { return u\"disable\"_q; };\n\nstruct Preview {\n\tQImage preview;\n\tQRect userpic;\n};\n\n[[nodiscard]] Preview GeneratePreview(\n\t\tnot_null<Ui::ChatTheme*> theme,\n\t\tconst std::shared_ptr<Ui::DynamicImage> &takenUserpic) {\n\tconst auto &background = theme->background();\n\tconst auto &colors = background.colors;\n\tconst auto size = st::chatThemePreviewSize;\n\tauto prepared = background.prepared;\n\tconst auto paintPattern = [&](QPainter &p, bool inverted) {\n\t\tif (prepared.isNull()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto w = prepared.width();\n\t\tconst auto h = prepared.height();\n\t\tconst auto scaled = size.scaled(\n\t\t\tst::windowMinWidth / 2,\n\t\t\tst::windowMinHeight / 2,\n\t\t\tQt::KeepAspectRatio);\n\t\tconst auto use = (scaled.width() > w || scaled.height() > h)\n\t\t\t? scaled.scaled({ w, h }, Qt::KeepAspectRatio)\n\t\t\t: scaled;\n\t\tconst auto good = QSize(\n\t\t\tstd::max(use.width(), 1),\n\t\t\tstd::max(use.height(), 1));\n\t\tauto small = prepared.copy(QRect(\n\t\t\tQPoint(\n\t\t\t\t(w - good.width()) / 2,\n\t\t\t\t(h - good.height()) / 2),\n\t\t\tgood));\n\t\tif (inverted) {\n\t\t\tsmall = Ui::InvertPatternImage(std::move(small));\n\t\t}\n\t\tp.drawImage(\n\t\t\tQRect(QPoint(), size * style::DevicePixelRatio()),\n\t\t\tsmall);\n\t};\n\tauto userpic = QRect();\n\tconst auto fullsize = size * style::DevicePixelRatio();\n\tauto result = background.waitingForNegativePattern()\n\t\t? QImage(\n\t\t\tfullsize,\n\t\t\tQImage::Format_ARGB32_Premultiplied)\n\t\t: Ui::GenerateBackgroundImage(\n\t\t\tfullsize,\n\t\t\tcolors.empty() ? std::vector{ 1, QColor(0, 0, 0) } : colors,\n\t\t\tbackground.gradientRotation,\n\t\t\tbackground.patternOpacity,\n\t\t\tpaintPattern);\n\tif (background.waitingForNegativePattern()) {\n\t\tresult.fill(Qt::black);\n\t}\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\t{\n\t\tauto p = QPainter(&result);\n\t\tconst auto sent = QRect(\n\t\t\tQPoint(\n\t\t\t\t(size.width()\n\t\t\t\t\t- st::chatThemeBubbleSize.width()\n\t\t\t\t\t- st::chatThemeBubblePosition.x()),\n\t\t\t\tst::chatThemeBubblePosition.y()),\n\t\t\tst::chatThemeBubbleSize);\n\t\tconst auto received = QRect(\n\t\t\tst::chatThemeBubblePosition.x(),\n\t\t\tsent.y() + sent.height() + st::chatThemeBubbleSkip,\n\t\t\tsent.width(),\n\t\t\tsent.height());\n\t\tconst auto radius = st::chatThemeBubbleRadius;\n\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tif (const auto pattern = theme->bubblesBackgroundPattern()) {\n\t\t\tauto bubble = pattern->pixmap.toImage().scaled(\n\t\t\t\tsent.size() * style::DevicePixelRatio(),\n\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\tQt::SmoothTransformation\n\t\t\t).convertToFormat(QImage::Format_ARGB32_Premultiplied);\n\t\t\tconst auto corners = Images::CornersMask(radius);\n\t\t\tp.drawImage(sent, Images::Round(std::move(bubble), corners));\n\t\t} else {\n\t\t\tp.setBrush(theme->palette()->msgOutBg()->c);\n\t\t\tp.drawRoundedRect(sent, radius, radius);\n\t\t}\n\t\tp.setBrush(theme->palette()->msgInBg()->c);\n\t\tp.drawRoundedRect(received, radius, radius);\n\n\t\tif (takenUserpic) {\n\t\t\tconst auto border = 2 * st::lineWidth;\n\t\t\tconst auto inner = received.marginsRemoved(\n\t\t\t\t{ border, border, border, border });\n\t\t\tuserpic = inner;\n\t\t\tuserpic.setWidth(userpic.height());\n\n\t\t\tst::chatThemeGiftTaken.paintInCenter(\n\t\t\t\tp,\n\t\t\t\tQRect(\n\t\t\t\t\tinner.x() + inner.width() - inner.height() - border,\n\t\t\t\t\tinner.y(),\n\t\t\t\t\tinner.height(),\n\t\t\t\t\tinner.height()),\n\t\t\t\ttheme->palette()->msgFileInBg()->c);\n\t\t}\n\t}\n\treturn {\n\t\t.preview = Images::Round(std::move(result), ImageRoundRadius::Large),\n\t\t.userpic = userpic,\n\t};\n}\n\n[[nodiscard]] QImage GenerateEmptyPreview() {\n\tauto result = QImage(\n\t\tst::chatThemePreviewSize * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.fill(st::settingsThemeNotSupportedBg->c);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\t{\n\t\tauto p = QPainter(&result);\n\t\tp.setPen(st::menuIconFg);\n\t\tp.setFont(st::semiboldFont);\n\t\tconst auto top = st::chatThemeEmptyPreviewTop;\n\t\tconst auto width = st::chatThemePreviewSize.width();\n\t\tconst auto height = st::chatThemePreviewSize.height() - top;\n\t\tp.drawText(\n\t\t\tQRect(0, top, width, height),\n\t\t\ttr::lng_chat_theme_none(tr::now),\n\t\t\tstyle::al_top);\n\t}\n\treturn Images::Round(std::move(result), ImageRoundRadius::Large);\n}\n\n} // namespace\n\nstruct ChooseThemeController::Entry {\n\tQString token;\n\tUi::ChatThemeKey key;\n\tstd::shared_ptr<Ui::ChatTheme> theme;\n\tstd::shared_ptr<Data::DocumentMedia> media;\n\tstd::shared_ptr<Data::UniqueGift> gift;\n\tstd::shared_ptr<Ui::DynamicImage> takenUserpic;\n\tstd::unique_ptr<Ui::Text::CustomEmoji> custom;\n\tEmojiPtr emoji = nullptr;\n\tQImage preview;\n\tQRect userpic;\n\tQRect geometry;\n\tbool chosen = false;\n};\n\nChooseThemeController::ChooseThemeController(\n\tnot_null<RpWidget*> parent,\n\tnot_null<Window::SessionController*> window,\n\tnot_null<PeerData*> peer)\n: _controller(window)\n, _peer(peer)\n, _wrap(std::make_unique<VerticalLayout>(parent))\n, _topShadow(std::make_unique<PlainShadow>(parent))\n, _content(_wrap->add(object_ptr<RpWidget>(_wrap.get())))\n, _inner(CreateChild<RpWidget>(_content.get()))\n, _disabledEmoji(Ui::Emoji::Find(QString::fromUtf8(\"\\xe2\\x9d\\x8c\")))\n, _dark(Window::Theme::IsThemeDarkValue()) {\n\tinit(parent->sizeValue());\n}\n\nChooseThemeController::~ChooseThemeController() {\n\t_controller->clearPeerThemeOverride(_peer);\n}\n\nvoid ChooseThemeController::init(rpl::producer<QSize> outer) {\n\tusing namespace rpl::mappers;\n\n\tconst auto themes = &_controller->session().data().cloudThemes();\n\tif (themes->myGiftThemesTokens().empty()) {\n\t\tthemes->myGiftThemesLoadMore();\n\t}\n\n\tconst auto &list = themes->chatThemes();\n\tif (!list.empty()) {\n\t\tfill(list);\n\t} else {\n\t\tthemes->refreshChatThemes();\n\t\tthemes->chatThemesUpdated(\n\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\tfill(themes->chatThemes());\n\t\t}, lifetime());\n\t}\n\n\tconst auto titleWrap = _wrap->insert(\n\t\t0,\n\t\tobject_ptr<FixedHeightWidget>(\n\t\t\t_wrap.get(),\n\t\t\tst::boxTitle.style.font->height),\n\t\tst::chatThemeTitlePadding);\n\tauto title = CreateChild<FlatLabel>(\n\t\ttitleWrap,\n\t\ttr::lng_chat_theme_title(),\n\t\tst::boxTitle);\n\t_wrap->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tQPainter(_wrap.get()).fillRect(clip, st::windowBg);\n\t}, lifetime());\n\n\tconst auto close = Ui::CreateChild<Ui::IconButton>(\n\t\t_wrap.get(),\n\t\tst::boxTitleClose);\n\tclose->setClickedCallback([=] { this->close(); });\n\trpl::combine(\n\t\t_wrap->widthValue(),\n\t\ttitleWrap->positionValue()\n\t) | rpl::on_next([=](int width, QPoint position) {\n\t\tclose->moveToRight(0, 0, width);\n\t}, close->lifetime());\n\n\tinitButtons();\n\tinitList();\n\n\t_inner->positionValue(\n\t) | rpl::on_next([=](QPoint position) {\n\t\ttitle->move(std::max(position.x(), 0), 0);\n\t}, title->lifetime());\n\n\tstd::move(\n\t\touter\n\t) | rpl::on_next([=](QSize outer) {\n\t\t_wrap->resizeToWidth(outer.width());\n\t\t_wrap->move(0, outer.height() - _wrap->height());\n\t\tconst auto line = st::lineWidth;\n\t\t_topShadow->setGeometry(0, _wrap->y() - line, outer.width(), line);\n\t}, lifetime());\n\n\trpl::combine(\n\t\t_shouldBeShown.value(),\n\t\t_forceHidden.value(),\n\t\t_1 && !_2\n\t) | rpl::on_next([=](bool shown) {\n\t\t_wrap->setVisible(shown);\n\t\t_topShadow->setVisible(shown);\n\t}, lifetime());\n}\n\nvoid ChooseThemeController::initButtons() {\n\tconst auto controls = _wrap->add(object_ptr<RpWidget>(_wrap.get()));\n\tconst auto apply = CreateChild<RoundButton>(\n\t\tcontrols,\n\t\ttr::lng_chat_theme_apply(),\n\t\tst::defaultLightButton);\n\tconst auto choose = CreateChild<RoundButton>(\n\t\tcontrols,\n\t\ttr::lng_chat_theme_change_wallpaper(),\n\t\tst::defaultLightButton);\n\n\tconst auto &margin = st::chatThemeButtonMargin;\n\tcontrols->resize(\n\t\tmargin.left() + choose->width() + margin.right(),\n\t\tmargin.top() + choose->height() + margin.bottom());\n\trpl::combine(\n\t\tcontrols->widthValue(),\n\t\tapply->widthValue(),\n\t\tchoose->widthValue(),\n\t\t_chosen.value()\n\t) | rpl::on_next([=](\n\t\t\tint outer,\n\t\t\tint applyWidth,\n\t\t\tint chooseWidth,\n\t\t\tQString chosen) {\n\t\tconst auto was = _peer->themeToken();\n\t\tconst auto now = (chosen == kDisableElement()) ? QString() : chosen;\n\t\tconst auto changed = (now != was);\n\t\tapply->setVisible(changed);\n\t\tchoose->setVisible(!changed);\n\t\tconst auto shown = changed ? apply : choose;\n\t\tconst auto shownWidth = changed ? applyWidth : chooseWidth;\n\t\tconst auto inner = margin.left() + shownWidth + margin.right();\n\t\tconst auto left = (outer - inner) / 2;\n\t\tshown->moveToLeft(left, margin.top());\n\t}, controls->lifetime());\n\n\tconst auto setTheme = crl::guard(apply, [=](\n\t\t\tconst QString &token,\n\t\t\tconst std::shared_ptr<Ui::ChatTheme> &theme) {\n\t\tSetPeerTheme(_controller, _peer, token, theme);\n\t\t_controller->toggleChooseChatTheme(_peer);\n\t});\n\tconst auto confirmTakeGiftTheme = crl::guard(apply, [=](\n\t\t\tconst QString &token,\n\t\t\tconst std::shared_ptr<Ui::ChatTheme> &theme,\n\t\t\tnot_null<PeerData*> nowHasTheme) {\n\t\t_controller->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tconst auto confirmed = [=](Fn<void()> close) {\n\t\t\t\tsetTheme(token, theme);\n\t\t\t\tclose();\n\t\t\t};\n\t\t\tUi::ConfirmBox(box, {\n\t\t\t\t.text = tr::lng_chat_theme_gift_replace(\n\t\t\t\t\tlt_name,\n\t\t\t\t\trpl::single(tr::bold(nowHasTheme->shortName())),\n\t\t\t\t\ttr::marked),\n\t\t\t\t.confirmed = confirmed,\n\t\t\t\t.confirmText = tr::lng_box_yes(),\n\t\t\t});\n\t\t}));\n\t});\n\tapply->setClickedCallback([=] {\n\t\tif (const auto chosen = findChosen()) {\n\t\t\tconst auto was = _peer->themeToken();\n\t\t\tconst auto now = chosen->key ? _chosen.current() : QString();\n\t\t\tconst auto user = chosen->gift\n\t\t\t\t? chosen->gift->themeUser\n\t\t\t\t: nullptr;\n\t\t\tif (was != now) {\n\t\t\t\tif (!user || user == _peer) {\n\t\t\t\t\tsetTheme(now, chosen->theme);\n\t\t\t\t} else {\n\t\t\t\t\tconfirmTakeGiftTheme(now, chosen->theme, user);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t_controller->toggleChooseChatTheme(_peer);\n\t\t\t}\n\t\t} else {\n\t\t\t_controller->toggleChooseChatTheme(_peer);\n\t\t}\n\t});\n\tchoose->setClickedCallback([=] {\n\t\t_controller->show(Box<BackgroundBox>(_controller, _peer));\n\t});\n}\n\nvoid ChooseThemeController::paintEntry(QPainter &p, const Entry &entry) {\n\tconst auto geometry = entry.geometry;\n\tp.drawImage(geometry, entry.preview);\n\tif (const auto userpic = entry.takenUserpic.get()) {\n\t\tuserpic->subscribeToUpdates([=] {\n\t\t\t_inner->update();\n\t\t});\n\t\tp.drawImage(\n\t\t\tentry.userpic.translated(geometry.topLeft()),\n\t\t\tuserpic->image(entry.userpic.height()));\n\t}\n\n\tconst auto size = Ui::Emoji::GetSizeLarge();\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto esize = size / factor;\n\tconst auto emojiLeft = geometry.x() + (geometry.width() - esize) / 2;\n\tconst auto emojiTop = geometry.y()\n\t\t+ geometry.height()\n\t\t- esize\n\t\t- st::chatThemeEmojiBottom;\n\tconst auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);\n\tconst auto customSkip = (esize - customSize) / 2;\n\n\tif (const auto emoji = entry.emoji) {\n\t\tUi::Emoji::Draw(p, emoji, size, emojiLeft, emojiTop);\n\t} else if (const auto custom = entry.custom.get()) {\n\t\tcustom->paint(p, {\n\t\t\t.textColor = st::windowFg->c,\n\t\t\t.position = { emojiLeft + customSkip, emojiTop + customSkip },\n\t\t});\n\t}\n\n\tif (entry.chosen) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tauto pen = st::activeLineFg->p;\n\t\tconst auto width = st::defaultInputField.borderActive;\n\t\tpen.setWidth(width);\n\t\tp.setPen(pen);\n\t\tconst auto add = st::lineWidth + width;\n\t\tp.drawRoundedRect(\n\t\t\tentry.geometry.marginsAdded({ add, add, add, add }),\n\t\t\tst::roundRadiusLarge + add,\n\t\t\tst::roundRadiusLarge + add);\n\t}\n}\n\nvoid ChooseThemeController::initList() {\n\t_content->resize(\n\t\t_content->width(),\n\t\t(st::chatThemeEntryMargin.top()\n\t\t\t+ st::chatThemePreviewSize.height()\n\t\t\t+ st::chatThemeEntryMargin.bottom()));\n\t_inner->setMouseTracking(true);\n\n\t_inner->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(_inner.get());\n\t\tfor (const auto &entry : _entries) {\n\t\t\tif (entry.preview.isNull() || !clip.intersects(entry.geometry)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tpaintEntry(p, entry);\n\t\t}\n\t}, lifetime());\n\tconst auto byPoint = [=](QPoint position) -> Entry* {\n\t\tfor (auto &entry : _entries) {\n\t\t\tif (entry.geometry.contains(position)) {\n\t\t\t\treturn &entry;\n\t\t\t}\n\t\t}\n\t\treturn nullptr;\n\t};\n\tconst auto chosenText = [=](const Entry *entry) {\n\t\tif (!entry) {\n\t\t\treturn QString();\n\t\t} else if (entry->key) {\n\t\t\treturn entry->token;\n\t\t} else {\n\t\t\treturn kDisableElement();\n\t\t}\n\t};\n\t_inner->events(\n\t) | rpl::on_next([=](not_null<QEvent*> event) {\n\t\tconst auto type = event->type();\n\t\tif (type == QEvent::MouseMove) {\n\t\t\tconst auto mouse = static_cast<QMouseEvent*>(event.get());\n\t\t\tconst auto skip = _inner->width() - _content->width();\n\t\t\tif (skip <= 0) {\n\t\t\t\t_dragStartPosition = _pressPosition = std::nullopt;\n\t\t\t} else if (_pressPosition.has_value()\n\t\t\t\t&& ((mouse->globalPos() - *_pressPosition).manhattanLength()\n\t\t\t\t\t>= QApplication::startDragDistance())) {\n\t\t\t\t_dragStartPosition = base::take(_pressPosition);\n\t\t\t\t_dragStartInnerLeft = _inner->x();\n\t\t\t}\n\t\t\tif (_dragStartPosition.has_value()) {\n\t\t\t\tconst auto shift = mouse->globalPos().x()\n\t\t\t\t\t- _dragStartPosition->x();\n\t\t\t\tupdateInnerLeft(_dragStartInnerLeft + shift);\n\t\t\t} else {\n\t\t\t\t_inner->setCursor(byPoint(mouse->pos())\n\t\t\t\t\t? style::cur_pointer\n\t\t\t\t\t: style::cur_default);\n\t\t\t}\n\t\t} else if (type == QEvent::MouseButtonPress) {\n\t\t\tconst auto mouse = static_cast<QMouseEvent*>(event.get());\n\t\t\tif (mouse->button() == Qt::LeftButton) {\n\t\t\t\t_pressPosition = mouse->globalPos();\n\t\t\t}\n\t\t\t_pressed = chosenText(byPoint(mouse->pos()));\n\t\t} else if (type == QEvent::MouseButtonRelease) {\n\t\t\t_pressPosition = _dragStartPosition = std::nullopt;\n\t\t\tconst auto mouse = static_cast<QMouseEvent*>(event.get());\n\t\t\tconst auto entry = byPoint(mouse->pos());\n\t\t\tconst auto chosen = chosenText(entry);\n\t\t\tif (entry && chosen == _pressed && chosen != _chosen.current()) {\n\t\t\t\tclearCurrentBackgroundState();\n\t\t\t\tif (const auto was = findChosen()) {\n\t\t\t\t\twas->chosen = false;\n\t\t\t\t}\n\t\t\t\t_chosen = chosen;\n\t\t\t\tentry->chosen = true;\n\t\t\t\tif (entry->theme || !entry->key) {\n\t\t\t\t\t_controller->overridePeerTheme(\n\t\t\t\t\t\t_peer,\n\t\t\t\t\t\tentry->theme,\n\t\t\t\t\t\tentry->token);\n\t\t\t\t}\n\t\t\t\t_inner->update();\n\t\t\t}\n\t\t\t_pressed = QString();\n\t\t} else if (type == QEvent::Wheel) {\n\t\t\tconst auto wheel = static_cast<QWheelEvent*>(event.get());\n\t\t\tconst auto was = _inner->x();\n\t\t\tupdateInnerLeft((wheel->angleDelta().x() != 0)\n\t\t\t\t? (was + (wheel->pixelDelta().x()\n\t\t\t\t\t? wheel->pixelDelta().x()\n\t\t\t\t\t: wheel->angleDelta().x()))\n\t\t\t\t: (wheel->angleDelta().y() != 0)\n\t\t\t\t? (was + (wheel->pixelDelta().y()\n\t\t\t\t\t? wheel->pixelDelta().y()\n\t\t\t\t\t: wheel->angleDelta().y()))\n\t\t\t\t: was);\n\t\t}\n\t}, lifetime());\n\n\t_content->events(\n\t) | rpl::on_next([=](not_null<QEvent*> event) {\n\t\tconst auto type = event->type();\n\t\tif (type == QEvent::KeyPress) {\n\t\t\tconst auto key = static_cast<QKeyEvent*>(event.get());\n\t\t\tif (key->key() == Qt::Key_Escape) {\n\t\t\t\tclose();\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\trpl::combine(\n\t\t_content->widthValue(),\n\t\t_inner->widthValue()\n\t) | rpl::on_next([=](int content, int inner) {\n\t\tif (!content || !inner) {\n\t\t\treturn;\n\t\t} else if (!_entries.empty() && !_initialInnerLeftApplied) {\n\t\t\tapplyInitialInnerLeft();\n\t\t} else {\n\t\t\tupdateInnerLeft(_inner->x());\n\t\t}\n\t}, lifetime());\n}\n\nvoid ChooseThemeController::applyInitialInnerLeft() {\n\tif (const auto chosen = findChosen()) {\n\t\tupdateInnerLeft(\n\t\t\t_content->width() / 2 - chosen->geometry.center().x());\n\t}\n\t_initialInnerLeftApplied = true;\n}\n\nvoid ChooseThemeController::updateInnerLeft(int now) {\n\tconst auto skip = _content->width() - _inner->width();\n\tconst auto clamped = (skip >= 0)\n\t\t? (skip / 2)\n\t\t: std::clamp(now, skip, 0);\n\t_inner->move(clamped, 0);\n\tconst auto visibleTill = -clamped + _content->width();\n\tif (_giftsFinishAt - visibleTill < _content->width()) {\n\t\t_peer->owner().cloudThemes().myGiftThemesLoadMore();\n\t}\n}\n\nvoid ChooseThemeController::close() {\n\tif (const auto chosen = findChosen()) {\n\t\tif (_peer->themeToken() != chosen->token) {\n\t\t\tclearCurrentBackgroundState();\n\t\t}\n\t}\n\t_controller->toggleChooseChatTheme(_peer);\n}\n\nvoid ChooseThemeController::clearCurrentBackgroundState() {\n\tif (const auto entry = findChosen()) {\n\t\tif (entry->theme) {\n\t\t\tentry->theme->clearBackgroundState();\n\t\t}\n\t}\n}\n\nauto ChooseThemeController::findChosen() -> Entry* {\n\tconst auto chosen = _chosen.current();\n\tif (chosen.isEmpty()) {\n\t\treturn nullptr;\n\t}\n\tfor (auto &entry : _entries) {\n\t\tif (!entry.key && chosen == kDisableElement()) {\n\t\t\treturn &entry;\n\t\t} else if (chosen == entry.token) {\n\t\t\treturn &entry;\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nauto ChooseThemeController::findChosen() const -> const Entry* {\n\treturn const_cast<ChooseThemeController*>(this)->findChosen();\n}\n\nvoid ChooseThemeController::fill(\n\t\tconst std::vector<Data::CloudTheme> &themes) {\n\tif (themes.empty()) {\n\t\treturn;\n\t}\n\tconst auto single = st::chatThemePreviewSize;\n\tconst auto skip = st::chatThemeEntrySkip;\n\tconst auto &margin = st::chatThemeEntryMargin;\n\tconst auto initial = _peer->themeToken();\n\tif (initial.isEmpty()) {\n\t\t_chosen = kDisableElement();\n\t}\n\n\tconst auto cloudThemes = &_controller->session().data().cloudThemes();\n\trpl::combine(\n\t\t_dark.value(),\n\t\trpl::single(\n\t\t\trpl::empty\n\t\t) | rpl::then(cloudThemes->myGiftThemesUpdated())\n\t) | rpl::on_next([=](bool dark, auto) {\n\t\tif (!cloudThemes->myGiftThemesReady()) {\n\t\t\treturn;\n\t\t}\n\t\tclearCurrentBackgroundState();\n\t\tif (_chosen.current().isEmpty() && !initial.isEmpty()) {\n\t\t\t_chosen = initial;\n\t\t}\n\n\t\t_cachingLifetime.destroy();\n\t\tconst auto old = base::take(_entries);\n\t\tauto x = margin.left();\n\t\t_entries.push_back({\n\t\t\t.emoji = _disabledEmoji,\n\t\t\t.preview = GenerateEmptyPreview(),\n\t\t\t.geometry = QRect(QPoint(x, margin.top()), single),\n\t\t\t.chosen = (_chosen.current() == kDisableElement()),\n\t\t});\n\t\tAssert(_entries.front().emoji != nullptr);\n\t\tstyle::PaletteChanged(\n\t\t) | rpl::on_next([=] {\n\t\t\t_entries.front().preview = GenerateEmptyPreview();\n\t\t}, _cachingLifetime);\n\n\t\tconst auto type = dark\n\t\t\t? Data::CloudThemeType::Dark\n\t\t\t: Data::CloudThemeType::Light;\n\n\t\tx += single.width() + skip;\n\n\t\tconst auto owner = &_controller->session().data();\n\t\tconst auto manager = &owner->customEmojiManager();\n\t\tconst auto push = [&](\n\t\t\t\tconst Data::CloudTheme &theme,\n\t\t\t\tconst QString &token) {\n\t\t\tif (token.isEmpty() || !theme.settings.contains(type)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto key = ChatThemeKey{ theme.id, dark };\n\t\t\tconst auto isChosen = (_chosen.current() == token);\n\t\t\tconst auto themeUser = theme.unique\n\t\t\t\t? theme.unique->themeUser\n\t\t\t\t: nullptr;\n\t\t\t_entries.push_back({\n\t\t\t\t.token = token,\n\t\t\t\t.key = key,\n\t\t\t\t.gift = theme.unique,\n\t\t\t\t.takenUserpic = (themeUser\n\t\t\t\t\t? Ui::MakeUserpicThumbnail(themeUser, true)\n\t\t\t\t\t: nullptr),\n\t\t\t\t.custom = (theme.unique\n\t\t\t\t\t? manager->create(\n\t\t\t\t\t\ttheme.unique->model.document,\n\t\t\t\t\t\t[=] { _inner->update(); },\n\t\t\t\t\t\tData::CustomEmojiSizeTag::Large)\n\t\t\t\t\t: nullptr),\n\t\t\t\t.emoji = (theme.emoticon.isEmpty()\n\t\t\t\t\t? nullptr\n\t\t\t\t\t: Ui::Emoji::Find(theme.emoticon)),\n\t\t\t\t.geometry = QRect(QPoint(x, skip), single),\n\t\t\t\t.chosen = isChosen,\n\t\t\t});\n\t\t\t_controller->cachedChatThemeValue(\n\t\t\t\ttheme,\n\t\t\t\tData::WallPaper(0),\n\t\t\t\ttype\n\t\t\t) | rpl::filter([=](const std::shared_ptr<ChatTheme> &data) {\n\t\t\t\treturn data && (data->key() == key);\n\t\t\t}) | rpl::take(\n\t\t\t\t1\n\t\t\t) | rpl::on_next([=](std::shared_ptr<ChatTheme> &&data) {\n\t\t\t\tconst auto key = data->key();\n\t\t\t\tconst auto i = ranges::find(_entries, key, &Entry::key);\n\t\t\t\tif (i == end(_entries)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto theme = data.get();\n\t\t\t\tconst auto token = i->token;\n\t\t\t\ti->theme = std::move(data);\n\t\t\t\tauto generated = GeneratePreview(theme, i->takenUserpic);\n\t\t\t\ti->preview = std::move(generated.preview);\n\t\t\t\ti->userpic = generated.userpic;\n\t\t\t\tif (_chosen.current() == token) {\n\t\t\t\t\t_controller->overridePeerTheme(_peer, i->theme, token);\n\t\t\t\t}\n\t\t\t\t_inner->update();\n\n\t\t\t\tif (!theme->background().isPattern\n\t\t\t\t\t|| !theme->background().prepared.isNull()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Subscribe to pattern loading if needed.\n\t\t\t\ttheme->repaintBackgroundRequests(\n\t\t\t\t) | rpl::filter([=] {\n\t\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\t\t_entries,\n\t\t\t\t\t\tkey,\n\t\t\t\t\t\t&Entry::key);\n\t\t\t\t\treturn (i == end(_entries))\n\t\t\t\t\t\t|| !i->theme->background().prepared.isNull();\n\t\t\t\t}) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\t\t_entries,\n\t\t\t\t\t\tkey,\n\t\t\t\t\t\t&Entry::key);\n\t\t\t\t\tif (i == end(_entries)) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tauto generated = GeneratePreview(theme, i->takenUserpic);\n\t\t\t\t\ti->preview = std::move(generated.preview);\n\t\t\t\t\ti->userpic = generated.userpic;\n\t\t\t\t\t_inner->update();\n\t\t\t\t}, _cachingLifetime);\n\t\t\t}, _cachingLifetime);\n\t\t\tx += single.width() + skip;\n\t\t};\n\n\t\t_giftsFinishAt = 0;\n\t\tif (const auto now = cloudThemes->themeForToken(initial)) {\n\t\t\tpush(*now, initial);\n\t\t}\n\t\tfor (const auto &token : cloudThemes->myGiftThemesTokens()) {\n\t\t\tif (const auto found = cloudThemes->themeForToken(token)) {\n\t\t\t\tif (token != initial) {\n\t\t\t\t\tpush(*found, token);\n\t\t\t\t\t_giftsFinishAt = x;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor (const auto &theme : themes) {\n\t\t\tif (const auto emoji = Ui::Emoji::Find(theme.emoticon)) {\n\t\t\t\tconst auto token = emoji->text();\n\t\t\t\tif (token != initial) {\n\t\t\t\t\tpush(theme, token);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst auto full = x - skip + margin.right();\n\t\t_inner->resize(\n\t\t\tfull,\n\t\t\tmargin.top() + single.height() + margin.bottom());\n\n\t\tif (!_initialInnerLeftApplied && _content->width() > 0) {\n\t\t\tapplyInitialInnerLeft();\n\t\t}\n\t}, lifetime());\n\t_shouldBeShown = true;\n}\n\nbool ChooseThemeController::shouldBeShown() const {\n\treturn _shouldBeShown.current();\n}\n\nrpl::producer<bool> ChooseThemeController::shouldBeShownValue() const {\n\treturn _shouldBeShown.value();\n}\n\nint ChooseThemeController::height() const {\n\treturn shouldBeShown() ? _wrap->height() : 0;\n}\n\nvoid ChooseThemeController::hide() {\n\t_forceHidden = true;\n}\n\nvoid ChooseThemeController::show() {\n\t_forceHidden = false;\n}\n\nvoid ChooseThemeController::raise() {\n\t_wrap->raise();\n\t_topShadow->raise();\n}\n\nvoid ChooseThemeController::setFocus() {\n\t_content->setFocus();\n}\n\nrpl::lifetime &ChooseThemeController::lifetime() {\n\treturn _wrap->lifetime();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/choose_theme_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Data {\nstruct CloudTheme;\n} // namespace Data\n\nnamespace Ui {\n\nclass RpWidget;\nclass PlainShadow;\nclass VerticalLayout;\n\nclass ChooseThemeController final {\npublic:\n\tChooseThemeController(\n\t\tnot_null<RpWidget*> parent,\n\t\tnot_null<Window::SessionController*> window,\n\t\tnot_null<PeerData*> peer);\n\t~ChooseThemeController();\n\n\t[[nodiscard]] bool shouldBeShown() const;\n\t[[nodiscard]] rpl::producer<bool> shouldBeShownValue() const;\n\t[[nodiscard]] int height() const;\n\n\tvoid hide();\n\tvoid show();\n\tvoid raise();\n\tvoid setFocus();\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tstruct Entry;\n\n\tvoid init(rpl::producer<QSize> outer);\n\tvoid initButtons();\n\tvoid initList();\n\tvoid fill(const std::vector<Data::CloudTheme> &themes);\n\tvoid close();\n\n\tvoid clearCurrentBackgroundState();\n\tvoid paintEntry(QPainter &p, const Entry &entry);\n\tvoid applyInitialInnerLeft();\n\tvoid updateInnerLeft(int now);\n\n\t[[nodiscard]] Entry *findChosen();\n\t[[nodiscard]] const Entry *findChosen() const;\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst not_null<PeerData*> _peer;\n\tconst std::unique_ptr<VerticalLayout> _wrap;\n\tconst std::unique_ptr<PlainShadow> _topShadow;\n\n\tconst not_null<RpWidget*> _content;\n\tconst not_null<RpWidget*> _inner;\n\tconst EmojiPtr _disabledEmoji = nullptr;\n\tstd::vector<Entry> _entries;\n\tQString _pressed;\n\trpl::variable<QString> _chosen;\n\tstd::optional<QPoint> _pressPosition;\n\tstd::optional<QPoint> _dragStartPosition;\n\tint _dragStartInnerLeft = 0;\n\tint _giftsFinishAt = 0;\n\tbool _initialInnerLeftApplied = false;\n\n\trpl::variable<bool> _shouldBeShown = false;\n\trpl::variable<bool> _forceHidden = false;\n\trpl::variable<bool> _dark = false;\n\trpl::lifetime _cachingLifetime;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/continuous_scroll.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/continuous_scroll.h\"\n\n#include <QScrollBar>\n#include <QWheelEvent>\n\nnamespace Ui {\n\nvoid ContinuousScroll::wheelEvent(QWheelEvent *e) {\n\tif (_tracking\n\t\t&& !e->angleDelta().isNull()\n\t\t&& (e->angleDelta().y() < 0)\n\t\t&& (scrollTopMax() == scrollTop())) {\n\t\t_addContentRequests.fire({});\n\t\tif (base::take(_contentAdded)) {\n\t\t\tviewportEvent(e);\n\t\t}\n\t\treturn;\n\t}\n\tScrollArea::wheelEvent(e);\n}\n\nvoid ContinuousScroll::setTrackingContent(bool value) {\n\tif (_tracking == value) {\n\t\treturn;\n\t}\n\t_tracking = value;\n\treconnect();\n}\n\nvoid ContinuousScroll::reconnect() {\n\tif (!_tracking) {\n\t\t_connection.release();\n\t\treturn;\n\t}\n\tconst auto handleAction = [=](int action) {\n\t\tconst auto scroll = verticalScrollBar();\n\t\tconst auto step = (action == QAbstractSlider::SliderSingleStepAdd)\n\t\t\t? scroll->singleStep()\n\t\t\t: (action == QAbstractSlider::SliderPageStepAdd)\n\t\t\t? scroll->pageStep()\n\t\t\t: 0;\n\t\tif (!action) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto newTop = scrollTop() + step;\n\t\tif (newTop > scrollTopMax()) {\n\t\t\t_addContentRequests.fire({});\n\t\t\tif (base::take(_contentAdded)) {\n\t\t\t\tscroll->setSliderPosition(newTop);\n\t\t\t}\n\t\t}\n\t};\n\t_connection = QObject::connect(\n\t\tverticalScrollBar(),\n\t\t&QAbstractSlider::actionTriggered,\n\t\thandleAction);\n}\n\nvoid ContinuousScroll::contentAdded() {\n\t_contentAdded = true;\n}\n\nrpl::producer<> ContinuousScroll::addContentRequests() const {\n\treturn _addContentRequests.events();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/continuous_scroll.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/scroll_area.h\"\n#include \"base/qt_connection.h\"\n\nnamespace Ui {\n\n// This class is designed for seamless scrolling of\n// on-demand augmented content.\nclass ContinuousScroll final : public ScrollArea {\npublic:\n\tusing ScrollArea::ScrollArea;\n\n\t[[nodiscard]] rpl::producer<> addContentRequests() const;\n\tvoid contentAdded();\n\n\tvoid setTrackingContent(bool value);\n\nprotected:\n\tvoid wheelEvent(QWheelEvent *e) override;\n\nprivate:\n\tvoid reconnect();\n\n\tbase::qt_connection _connection;\n\n\tbool _contentAdded = false;\n\tbool _tracking = false;\n\n\trpl::event_stream<> _addContentRequests;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/forward_options_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/forward_options_box.h\"\n\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/labels.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace Ui {\n\nvoid FillForwardOptions(\n\t\tFn<not_null<AbstractCheckView*>(\n\t\t\trpl::producer<QString> &&,\n\t\t\tbool)> createView,\n\t\tForwardOptions options,\n\t\tFn<void(ForwardOptions)> optionsChanged,\n\t\trpl::lifetime &lifetime) {\n\tExpects(optionsChanged != nullptr);\n\n\tconst auto names = createView(\n\t\t(options.sendersCount == 1\n\t\t\t? tr::lng_forward_show_sender\n\t\t\t: tr::lng_forward_show_senders)(),\n\t\t!options.dropNames);\n\tconst auto captions = options.captionsCount\n\t\t? createView(\n\t\t\t(options.captionsCount == 1\n\t\t\t\t? tr::lng_forward_show_caption\n\t\t\t\t: tr::lng_forward_show_captions)(),\n\t\t\t!options.dropCaptions).get()\n\t\t: nullptr;\n\n\tconst auto notify = [=] {\n\t\toptionsChanged({\n\t\t\t.sendersCount = options.sendersCount,\n\t\t\t.captionsCount = options.captionsCount,\n\t\t\t.dropNames = !names->checked(),\n\t\t\t.dropCaptions = (captions && !captions->checked()),\n\t\t});\n\t};\n\tnames->checkedChanges(\n\t) | rpl::on_next([=](bool showNames) {\n\t\tif (showNames && captions && !captions->checked()) {\n\t\t\tcaptions->setChecked(true, anim::type::normal);\n\t\t} else {\n\t\t\tnotify();\n\t\t}\n\t}, lifetime);\n\tif (captions) {\n\t\tcaptions->checkedChanges(\n\t\t) | rpl::on_next([=](bool showCaptions) {\n\t\t\tif (!showCaptions && names->checked()) {\n\t\t\t\tnames->setChecked(false, anim::type::normal);\n\t\t\t} else {\n\t\t\t\tnotify();\n\t\t\t}\n\t\t}, lifetime);\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/forward_options_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n\nnamespace Ui {\n\nclass AbstractCheckView;\n\nstruct ForwardOptions {\n\tint sendersCount = 0;\n\tint captionsCount = 0;\n\tbool dropNames = false;\n\tbool dropCaptions = false;\n};\n\nvoid FillForwardOptions(\n\tFn<not_null<AbstractCheckView*>(\n\t\trpl::producer<QString> &&,\n\t\tbool)> createView,\n\tForwardOptions options,\n\tFn<void(ForwardOptions)> optionsChanged,\n\trpl::lifetime &lifetime);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/group_call_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/group_call_bar.h\"\n\n#include \"ui/chat/group_call_userpics.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/painter.h\"\n#include \"lang/lang_keys.h\"\n#include \"base/unixtime.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_info.h\" // st::topBarArrowPadding, like TopBarWidget.\n#include \"styles/style_window.h\" // st::columnMinimalWidthLeft\n#include \"styles/palette.h\"\n\n#include <QtGui/QtEvents>\n#include <QtCore/QLocale>\n\nnamespace Ui {\n\nGroupCallScheduledLeft::GroupCallScheduledLeft(TimeId date)\n: _date(date)\n, _datePrecise(computePreciseDate())\n, _timer([=] { update(); }) {\n\tupdate();\n\tbase::unixtime::updates(\n\t) | rpl::on_next([=] {\n\t\trestart();\n\t}, _lifetime);\n}\n\ncrl::time GroupCallScheduledLeft::computePreciseDate() const {\n\treturn crl::now() + (_date - base::unixtime::now()) * crl::time(1000);\n}\n\nvoid GroupCallScheduledLeft::setDate(TimeId date) {\n\tif (_date == date) {\n\t\treturn;\n\t}\n\t_date = date;\n\trestart();\n}\n\nvoid GroupCallScheduledLeft::restart() {\n\t_datePrecise = computePreciseDate();\n\t_timer.cancel();\n\tupdate();\n}\n\nrpl::producer<QString> GroupCallScheduledLeft::text(Negative negative) const {\n\treturn (negative == Negative::Show)\n\t\t? _text.value()\n\t\t: _textNonNegative.value();\n}\n\nrpl::producer<bool> GroupCallScheduledLeft::late() const {\n\treturn _late.value();\n}\n\nvoid GroupCallScheduledLeft::update() {\n\tconst auto now = crl::now();\n\tconst auto duration = (_datePrecise - now);\n\tconst auto left = crl::time(base::SafeRound(std::abs(duration) / 1000.));\n\tconst auto late = (duration < 0) && (left > 0);\n\t_late = late;\n\tconstexpr auto kDay = 24 * 60 * 60;\n\tif (left >= kDay) {\n\t\tconst auto days = (left / kDay);\n\t\t_textNonNegative = tr::lng_days(tr::now, lt_count, days);\n\t\t_text = late\n\t\t\t? tr::lng_days(tr::now, lt_count, -days)\n\t\t\t: _textNonNegative.current();\n\t} else {\n\t\tconst auto hours = left / (60 * 60);\n\t\tconst auto minutes = (left % (60 * 60)) / 60;\n\t\tconst auto seconds = (left % 60);\n\t\t_textNonNegative = (hours > 0)\n\t\t\t? (u\"%1:%2:%3\"_q\n\t\t\t\t.arg(hours, 2, 10, QChar('0'))\n\t\t\t\t.arg(minutes, 2, 10, QChar('0'))\n\t\t\t\t.arg(seconds, 2, 10, QChar('0')))\n\t\t\t: (u\"%1:%2\"_q\n\t\t\t\t.arg(minutes, 2, 10, QChar('0'))\n\t\t\t\t.arg(seconds, 2, 10, QChar('0')));\n\t\t_text = (late ? QString(QChar(0x2212)) : QString())\n\t\t\t+ _textNonNegative.current();\n\t}\n\tif (left >= kDay) {\n\t\t_timer.callOnce((left % kDay) * crl::time(1000));\n\t} else {\n\t\tconst auto fraction = (std::abs(duration) + 500) % 1000;\n\t\tif (fraction < 400 || fraction > 600) {\n\t\t\tconst auto next = std::abs(duration) % 1000;\n\t\t\t_timer.callOnce((duration < 0) ? (1000 - next) : next);\n\t\t} else if (!_timer.isActive()) {\n\t\t\t_timer.callEach(1000);\n\t\t}\n\t}\n}\n\nGroupCallBar::GroupCallBar(\n\tnot_null<QWidget*> parent,\n\trpl::producer<GroupCallBarContent> content,\n\trpl::producer<bool> &&hideBlobs)\n: _wrap(parent, object_ptr<RpWidget>(parent))\n, _inner(_wrap.entity())\n, _shadow(std::make_unique<PlainShadow>(_wrap.parentWidget()))\n, _userpics(std::make_unique<GroupCallUserpics>(\n\t\tst::historyGroupCallUserpics,\n\t\tstd::move(hideBlobs),\n\t\t[=] { updateUserpics(); })) {\n\t_wrap.hide(anim::type::instant);\n\t_shadow->hide();\n\n\t_wrap.entity()->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tQPainter(_wrap.entity()).fillRect(clip, st::historyPinnedBg);\n\t}, lifetime());\n\t_wrap.setAttribute(Qt::WA_OpaquePaintEvent);\n\n\tauto copy = std::move(\n\t\tcontent\n\t) | rpl::start_spawning(_wrap.lifetime());\n\n\trpl::duplicate(\n\t\tcopy\n\t) | rpl::on_next([=](GroupCallBarContent &&content) {\n\t\t_content = content;\n\t\t_userpics->update(_content.users, !_wrap.isHidden());\n\t\t_inner->update();\n\t\trefreshScheduledProcess();\n\t}, lifetime());\n\tif (!_open && !_join) {\n\t\trefreshScheduledProcess();\n\t}\n\n\tstd::move(\n\t\tcopy\n\t) | rpl::map([=](const GroupCallBarContent &content) {\n\t\treturn !content.shown;\n\t}) | rpl::on_next_done([=](bool hidden) {\n\t\t_shouldBeShown = !hidden;\n\t\tif (!_forceHidden) {\n\t\t\t_wrap.toggle(_shouldBeShown, anim::type::normal);\n\t\t}\n\t}, [=] {\n\t\t_forceHidden = true;\n\t\t_wrap.toggle(false, anim::type::normal);\n\t}, lifetime());\n\n\tsetupInner();\n}\n\nGroupCallBar::~GroupCallBar() = default;\n\nvoid GroupCallBar::refreshOpenBrush() {\n\tExpects(_open != nullptr);\n\n\tconst auto width = _open->width();\n\tif (_openBrushForWidth == width) {\n\t\treturn;\n\t}\n\tauto gradient = QLinearGradient(QPoint(width, 0), QPoint(0, 0));\n\tgradient.setStops(QGradientStops{\n\t\t{ 0.0, st::groupCallForceMutedBar1->c },\n\t\t{ .7, st::groupCallForceMutedBar2->c },\n\t\t{ 1.0, st::groupCallForceMutedBar3->c }\n\t});\n\t_openBrushOverride = QBrush(std::move(gradient));\n\t_openBrushForWidth = width;\n\t_open->setBrushOverride(_openBrushOverride);\n}\n\nvoid GroupCallBar::refreshScheduledProcess() {\n\tconst auto date = _content.scheduleDate;\n\tif (!date) {\n\t\tif (_scheduledProcess) {\n\t\t\t_scheduledProcess = nullptr;\n\t\t\t_open = nullptr;\n\t\t\t_openBrushForWidth = 0;\n\t\t}\n\t\tif (!_join) {\n\t\t\t_join = std::make_unique<RoundButton>(\n\t\t\t\t_inner.get(),\n\t\t\t\ttr::lng_group_call_join(),\n\t\t\t\tst::groupCallTopBarJoin);\n\t\t\tsetupRightButton(_join.get());\n\t\t}\n\t} else if (!_scheduledProcess) {\n\t\t_scheduledProcess = std::make_unique<GroupCallScheduledLeft>(date);\n\t\t_join = nullptr;\n\t\t_open = std::make_unique<RoundButton>(\n\t\t\t_inner.get(),\n\t\t\t_scheduledProcess->text(GroupCallScheduledLeft::Negative::Show),\n\t\t\tst::groupCallTopBarOpen);\n\t\t_open->setTextTransform(RoundButtonTextTransform::ToUpper);\n\t\tsetupRightButton(_open.get());\n\t\t_open->widthValue(\n\t\t) | rpl::on_next([=] {\n\t\t\trefreshOpenBrush();\n\t\t}, _open->lifetime());\n\t} else {\n\t\t_scheduledProcess->setDate(date);\n\t}\n}\n\nvoid GroupCallBar::setupInner() {\n\t_inner->resize(0, st::historyReplyHeight);\n\t_inner->paintRequest(\n\t) | rpl::on_next([=](QRect rect) {\n\t\tauto p = Painter(_inner);\n\t\tpaint(p);\n\t}, _inner->lifetime());\n\n\t// Clicks.\n\t_inner->setCursor(style::cur_pointer);\n\t_inner->events(\n\t) | rpl::filter([=](not_null<QEvent*> event) {\n\t\treturn (event->type() == QEvent::MouseButtonPress)\n\t\t\t&& (static_cast<QMouseEvent*>(event.get())->button()\n\t\t\t\t== Qt::LeftButton);\n\t}) | rpl::map([=] {\n\t\treturn _inner->events(\n\t\t) | rpl::filter([=](not_null<QEvent*> event) {\n\t\t\treturn (event->type() == QEvent::MouseButtonRelease);\n\t\t}) | rpl::take(1) | rpl::filter([=](not_null<QEvent*> event) {\n\t\t\treturn _inner->rect().contains(\n\t\t\t\tstatic_cast<QMouseEvent*>(event.get())->pos());\n\t\t});\n\t}) | rpl::flatten_latest(\n\t) | rpl::to_empty | rpl::start_to_stream(_barClicks, _inner->lifetime());\n\n\t_wrap.geometryValue(\n\t) | rpl::on_next([=](QRect rect) {\n\t\tupdateShadowGeometry(rect);\n\t\tupdateControlsGeometry(rect);\n\t}, _inner->lifetime());\n}\n\nvoid GroupCallBar::setupRightButton(not_null<RoundButton*> button) {\n\tbutton->setFullRadius(true);\n\trpl::combine(\n\t\t_inner->widthValue(),\n\t\tbutton->widthValue()\n\t) | rpl::on_next([=](int outerWidth, int buttonWidth) {\n\t\t// Skip shadow of the bar above.\n\t\tconst auto top = (st::historyReplyHeight\n\t\t\t- st::lineWidth\n\t\t\t- button->height()) / 2 + st::lineWidth;\n\t\tconst auto narrow = (outerWidth < st::columnMinimalWidthLeft / 2);\n\t\tif (narrow) {\n\t\t\tbutton->moveToLeft(\n\t\t\t\t(outerWidth - buttonWidth) / 2,\n\t\t\t\ttop,\n\t\t\t\touterWidth);\n\t\t} else {\n\t\t\tbutton->moveToRight(top, top, outerWidth);\n\t\t}\n\t}, button->lifetime());\n\n\tbutton->clicks() | rpl::start_to_stream(_joinClicks, button->lifetime());\n}\n\nvoid GroupCallBar::paint(Painter &p) {\n\tp.fillRect(_inner->rect(), st::historyComposeAreaBg);\n\n\tconst auto narrow = (_inner->width() < st::columnMinimalWidthLeft / 2);\n\tif (!narrow) {\n\t\tpaintTitleAndStatus(p);\n\t\tpaintUserpics(p);\n\t}\n}\n\nvoid GroupCallBar::paintTitleAndStatus(Painter &p) {\n\tconst auto left = st::topBarArrowPadding.right();\n\tconst auto titleTop = st::msgReplyPadding.top();\n\tconst auto textTop = titleTop + st::msgServiceNameFont->height;\n\tconst auto width = _inner->width();\n\tconst auto &font = st::defaultMessageBar.title.font;\n\tp.setPen(st::defaultMessageBar.textFg);\n\tp.setFont(font);\n\n\tconst auto available = (_join ? _join->x() : _open->x()) - left;\n\tconst auto titleWidth = font->width(_content.title);\n\tp.drawTextLeft(\n\t\tleft,\n\t\ttitleTop,\n\t\twidth,\n\t\t(!_content.scheduleDate\n\t\t\t? (_content.livestream\n\t\t\t\t? tr::lng_group_call_title_channel\n\t\t\t\t: tr::lng_group_call_title)(tr::now)\n\t\t\t: _content.title.isEmpty()\n\t\t\t? (_content.livestream\n\t\t\t\t? tr::lng_group_call_scheduled_title_channel\n\t\t\t\t: tr::lng_group_call_scheduled_title)(tr::now)\n\t\t\t: (titleWidth > available)\n\t\t\t? font->elided(_content.title, available)\n\t\t\t: _content.title));\n\tp.setPen(st::historyStatusFg);\n\tp.setFont(st::defaultMessageBar.text.font);\n\tconst auto when = [&] {\n\t\tif (!_content.scheduleDate) {\n\t\t\treturn QString();\n\t\t}\n\t\tconst auto parsed = base::unixtime::parse(_content.scheduleDate);\n\t\tconst auto date = parsed.date();\n\t\tconst auto time = QLocale().toString(\n\t\t\tparsed.time(),\n\t\t\tQLocale::ShortFormat);\n\t\tconst auto today = QDate::currentDate();\n\t\tif (date == today) {\n\t\t\treturn tr::lng_group_call_starts_today(tr::now, lt_time, time);\n\t\t} else if (date == today.addDays(1)) {\n\t\t\treturn tr::lng_group_call_starts_tomorrow(\n\t\t\t\ttr::now,\n\t\t\t\tlt_time,\n\t\t\t\ttime);\n\t\t} else {\n\t\t\treturn tr::lng_group_call_starts_date(\n\t\t\t\ttr::now,\n\t\t\t\tlt_date,\n\t\t\t\tlangDayOfMonthFull(date),\n\t\t\t\tlt_time,\n\t\t\t\ttime);\n\t\t}\n\t}();\n\tp.drawTextLeft(\n\t\tleft,\n\t\ttextTop,\n\t\twidth,\n\t\t(_content.scheduleDate\n\t\t\t? (_content.title.isEmpty()\n\t\t\t\t? tr::lng_group_call_starts_short\n\t\t\t\t: _content.livestream\n\t\t\t\t? tr::lng_group_call_starts_channel\n\t\t\t\t: tr::lng_group_call_starts)(tr::now, lt_when, when)\n\t\t\t: _content.count > 0\n\t\t\t? tr::lng_group_call_members(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\t_content.count)\n\t\t\t: tr::lng_group_call_no_members(tr::now)));\n}\n\nvoid GroupCallBar::paintUserpics(Painter &p) {\n\tconst auto size = st::historyGroupCallUserpics.size;\n\t// Skip shadow of the bar above.\n\tconst auto top = (st::historyReplyHeight - st::lineWidth - size) / 2\n\t\t+ st::lineWidth;\n\t_userpics->paint(p, _inner->width() / 2, top, size);\n}\n\nvoid GroupCallBar::updateControlsGeometry(QRect wrapGeometry) {\n\tconst auto hidden = _wrap.isHidden() || !wrapGeometry.height();\n\tif (_shadow->isHidden() != hidden) {\n\t\t_shadow->setVisible(!hidden);\n\t}\n}\n\nvoid GroupCallBar::setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess) {\n\t_shadowGeometryPostprocess = std::move(postprocess);\n\tupdateShadowGeometry(_wrap.geometry());\n}\n\nvoid GroupCallBar::updateShadowGeometry(QRect wrapGeometry) {\n\tconst auto regular = QRect(\n\t\twrapGeometry.x(),\n\t\twrapGeometry.y() + wrapGeometry.height(),\n\t\twrapGeometry.width(),\n\t\tst::lineWidth);\n\t_shadow->setGeometry(_shadowGeometryPostprocess\n\t\t? _shadowGeometryPostprocess(regular)\n\t\t: regular);\n}\n\nvoid GroupCallBar::updateUserpics() {\n\tconst auto widget = _wrap.entity();\n\tconst auto middle = widget->width() / 2;\n\tconst auto width = _userpics->maxWidth();\n\twidget->update(\n\t\t(middle - width / 2),\n\t\t0,\n\t\twidth,\n\t\twidget->height());\n}\n\nvoid GroupCallBar::show() {\n\tif (!_forceHidden) {\n\t\treturn;\n\t}\n\t_forceHidden = false;\n\tif (_shouldBeShown) {\n\t\t_wrap.show(anim::type::instant);\n\t\t_shadow->show();\n\t}\n}\n\nvoid GroupCallBar::hide() {\n\tif (_forceHidden) {\n\t\treturn;\n\t}\n\t_forceHidden = true;\n\t_wrap.hide(anim::type::instant);\n\t_shadow->hide();\n}\n\nvoid GroupCallBar::raise() {\n\t_wrap.raise();\n\t_shadow->raise();\n}\n\nvoid GroupCallBar::finishAnimating() {\n\t_wrap.finishAnimating();\n}\n\nvoid GroupCallBar::move(int x, int y) {\n\t_wrap.move(x, y);\n}\n\nvoid GroupCallBar::resizeToWidth(int width) {\n\t_wrap.entity()->resizeToWidth(width);\n\t_inner->resizeToWidth(width);\n}\n\nint GroupCallBar::height() const {\n\treturn !_forceHidden\n\t\t? _wrap.height()\n\t\t: _shouldBeShown\n\t\t? st::historyReplyHeight\n\t\t: 0;\n}\n\nrpl::producer<int> GroupCallBar::heightValue() const {\n\treturn _wrap.heightValue();\n}\n\nrpl::producer<> GroupCallBar::barClicks() const {\n\treturn _barClicks.events();\n}\n\nrpl::producer<> GroupCallBar::joinClicks() const {\n\tusing namespace rpl::mappers;\n\treturn _joinClicks.events()\n\t\t| rpl::filter(_1 == Qt::LeftButton)\n\t\t| rpl::to_empty;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/group_call_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/effects/animations.h\"\n#include \"base/object_ptr.h\"\n#include \"base/timer.h\"\n\nclass Painter;\n\nnamespace Ui {\n\nclass PlainShadow;\nclass RoundButton;\nstruct GroupCallUser;\nclass GroupCallUserpics;\n\nstruct GroupCallBarContent {\n\tQString title;\n\tTimeId scheduleDate = 0;\n\tint count = 0;\n\tbool shown = false;\n\tbool livestream = false;\n\tstd::vector<GroupCallUser> users;\n};\n\nclass GroupCallScheduledLeft final {\npublic:\n\tenum class Negative {\n\t\tShow,\n\t\tIgnore,\n\t};\n\texplicit GroupCallScheduledLeft(TimeId date);\n\n\tvoid setDate(TimeId date);\n\n\t[[nodiscard]] rpl::producer<QString> text(Negative negative) const;\n\t[[nodiscard]] rpl::producer<bool> late() const;\n\nprivate:\n\t[[nodiscard]] crl::time computePreciseDate() const;\n\tvoid restart();\n\tvoid update();\n\n\trpl::variable<QString> _text;\n\trpl::variable<QString> _textNonNegative;\n\trpl::variable<bool> _late;\n\tTimeId _date = 0;\n\tcrl::time _datePrecise = 0;\n\tbase::Timer _timer;\n\trpl::lifetime _lifetime;\n\n};\n\nclass GroupCallBar final {\npublic:\n\tGroupCallBar(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<GroupCallBarContent> content,\n\t\trpl::producer<bool> &&hideBlobs);\n\t~GroupCallBar();\n\n\tvoid show();\n\tvoid hide();\n\tvoid raise();\n\tvoid finishAnimating();\n\n\tvoid setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess);\n\n\tvoid move(int x, int y);\n\tvoid resizeToWidth(int width);\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] rpl::producer<int> heightValue() const;\n\t[[nodiscard]] rpl::producer<> barClicks() const;\n\t[[nodiscard]] rpl::producer<> joinClicks() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _wrap.lifetime();\n\t}\n\nprivate:\n\tusing User = GroupCallUser;\n\n\tvoid refreshOpenBrush();\n\tvoid refreshScheduledProcess();\n\tvoid updateShadowGeometry(QRect wrapGeometry);\n\tvoid updateControlsGeometry(QRect wrapGeometry);\n\tvoid updateUserpics();\n\tvoid setupInner();\n\tvoid setupRightButton(not_null<RoundButton*> button);\n\tvoid paint(Painter &p);\n\tvoid paintTitleAndStatus(Painter &p);\n\tvoid paintUserpics(Painter &p);\n\n\tSlideWrap<> _wrap;\n\tnot_null<RpWidget*> _inner;\n\tstd::unique_ptr<RoundButton> _join;\n\tstd::unique_ptr<RoundButton> _open;\n\trpl::event_stream<Qt::MouseButton> _joinClicks;\n\tQBrush _openBrushOverride;\n\tint _openBrushForWidth = 0;\n\tstd::unique_ptr<PlainShadow> _shadow;\n\trpl::event_stream<> _barClicks;\n\tFn<QRect(QRect)> _shadowGeometryPostprocess;\n\tbool _shouldBeShown = false;\n\tbool _forceHidden = false;\n\n\tGroupCallBarContent _content;\n\tstd::unique_ptr<GroupCallScheduledLeft> _scheduledProcess;\n\tstd::unique_ptr<GroupCallUserpics> _userpics;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/group_call_userpics.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/group_call_userpics.h\"\n\n#include \"ui/paint/blobs.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"base/random.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kDuration = 160;\nconstexpr auto kMaxUserpics = 4;\nconstexpr auto kWideScale = 5;\n\nconstexpr auto kBlobsEnterDuration = crl::time(250);\nconstexpr auto kLevelDuration = 100. + 500. * 0.23;\nconstexpr auto kBlobScale = 0.605;\nconstexpr auto kMinorBlobFactor = 0.9f;\nconstexpr auto kUserpicMinScale = 0.8;\nconstexpr auto kMaxLevel = 1.;\nconstexpr auto kSendRandomLevelInterval = crl::time(100);\n\nauto Blobs()->std::array<Ui::Paint::Blobs::BlobData, 2> {\n\treturn { {\n\t\t{\n\t\t\t.segmentsCount = 6,\n\t\t\t.minScale = kBlobScale * kMinorBlobFactor,\n\t\t\t.minRadius = st::historyGroupCallBlobMinRadius * kMinorBlobFactor,\n\t\t\t.maxRadius = st::historyGroupCallBlobMaxRadius * kMinorBlobFactor,\n\t\t\t.speedScale = 1.,\n\t\t\t.alpha = .5,\n\t\t},\n\t\t{\n\t\t\t.segmentsCount = 8,\n\t\t\t.minScale = kBlobScale,\n\t\t\t.minRadius = (float)st::historyGroupCallBlobMinRadius,\n\t\t\t.maxRadius = (float)st::historyGroupCallBlobMaxRadius,\n\t\t\t.speedScale = 1.,\n\t\t\t.alpha = .2,\n\t\t},\n\t} };\n}\n\n} // namespace\n\nstruct GroupCallUserpics::BlobsAnimation {\n\tBlobsAnimation(\n\t\tstd::vector<Ui::Paint::Blobs::BlobData> blobDatas,\n\t\tfloat levelDuration,\n\t\tfloat maxLevel)\n\t: blobs(std::move(blobDatas), levelDuration, maxLevel) {\n\t}\n\n\tUi::Paint::Blobs blobs;\n\tcrl::time lastTime = 0;\n\tcrl::time lastSpeakingUpdateTime = 0;\n\tfloat64 enter = 0.;\n};\n\nstruct GroupCallUserpics::Userpic {\n\tUser data;\n\tstd::pair<uint64, uint64> cacheKey;\n\tcrl::time speakingStarted = 0;\n\tQImage cache;\n\tAnimations::Simple leftAnimation;\n\tAnimations::Simple shownAnimation;\n\tstd::unique_ptr<BlobsAnimation> blobsAnimation;\n\tint left = 0;\n\tbool positionInited = false;\n\tbool topMost = false;\n\tbool hiding = false;\n\tbool cacheMasked = false;\n};\n\nGroupCallUserpics::GroupCallUserpics(\n\tconst style::GroupCallUserpics &st,\n\trpl::producer<bool> &&hideBlobs,\n\tFn<void()> repaint)\n: _st(st)\n, _randomSpeakingTimer([=] { sendRandomLevels(); })\n, _repaint(std::move(repaint)) {\n\tconst auto limit = kMaxUserpics;\n\tconst auto single = _st.size;\n\tconst auto shift = _st.shift;\n\t// + 1 * single for the blobs.\n\t_maxWidth = 2 * single + (limit - 1) * (single - shift);\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tfor (auto &userpic : _list) {\n\t\t\tuserpic.cache = QImage();\n\t\t}\n\t}, lifetime());\n\n\t_speakingAnimation.init([=](crl::time now) {\n\t\tif (const auto &last = _speakingAnimationHideLastTime; (last > 0)\n\t\t\t&& (now - last >= kBlobsEnterDuration)) {\n\t\t\t_speakingAnimation.stop();\n\t\t}\n\t\tfor (auto &userpic : _list) {\n\t\t\tif (const auto blobs = userpic.blobsAnimation.get()) {\n\t\t\t\tblobs->blobs.updateLevel(now - blobs->lastTime);\n\t\t\t\tblobs->lastTime = now;\n\t\t\t}\n\t\t}\n\t\tif (const auto onstack = _repaint) {\n\t\t\tonstack();\n\t\t}\n\t});\n\n\trpl::combine(\n\t\tPowerSaving::OnValue(PowerSaving::kCalls),\n\t\tstd::move(hideBlobs)\n\t) | rpl::on_next([=](bool disabled, bool deactivated) {\n\t\tconst auto hide = disabled || deactivated;\n\n\t\tif (!(hide && _speakingAnimationHideLastTime)) {\n\t\t\t_speakingAnimationHideLastTime = hide ? crl::now() : 0;\n\t\t}\n\t\t_skipLevelUpdate = hide;\n\t\tfor (auto &userpic : _list) {\n\t\t\tif (const auto blobs = userpic.blobsAnimation.get()) {\n\t\t\t\tblobs->blobs.setLevel(0.);\n\t\t\t}\n\t\t}\n\t\tif (!hide && !_speakingAnimation.animating()) {\n\t\t\t_speakingAnimation.start();\n\t\t}\n\t\t_skipLevelUpdate = hide;\n\t}, lifetime());\n}\n\nGroupCallUserpics::~GroupCallUserpics() = default;\n\nvoid GroupCallUserpics::paint(QPainter &p, int x, int y, int size) {\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto &minScale = kUserpicMinScale;\n\tfor (auto &userpic : ranges::views::reverse(_list)) {\n\t\tconst auto shown = userpic.shownAnimation.value(\n\t\t\tuserpic.hiding ? 0. : 1.);\n\t\tif (shown == 0.) {\n\t\t\tcontinue;\n\t\t}\n\t\tvalidateCache(userpic);\n\t\tp.setOpacity(shown);\n\t\tconst auto left = x + userpic.leftAnimation.value(userpic.left);\n\t\tconst auto blobs = userpic.blobsAnimation.get();\n\t\tconst auto shownScale = 0.5 + shown / 2.;\n\t\tconst auto scale = shownScale * (!blobs\n\t\t\t? 1.\n\t\t\t: (minScale\n\t\t\t\t+ (1. - minScale) * (_speakingAnimationHideLastTime\n\t\t\t\t\t? (1. - blobs->blobs.currentLevel())\n\t\t\t\t\t: blobs->blobs.currentLevel())));\n\t\tif (blobs) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\t\tconst auto shift = QPointF(left + size / 2., y + size / 2.);\n\t\t\tp.translate(shift);\n\t\t\tblobs->blobs.paint(p, st::windowActiveTextFg);\n\t\t\tp.translate(-shift);\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t\tif (std::abs(scale - 1.) < 0.001) {\n\t\t\tconst auto skip = ((kWideScale - 1) / 2) * size * factor;\n\t\t\tp.drawImage(\n\t\t\t\tQRect(left, y, size, size),\n\t\t\t\tuserpic.cache,\n\t\t\t\tQRect(skip, skip, size * factor, size * factor));\n\t\t} else {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\t\tauto target = QRect(\n\t\t\t\tleft + (1 - kWideScale) / 2 * size,\n\t\t\t\ty + (1 - kWideScale) / 2 * size,\n\t\t\t\tkWideScale * size,\n\t\t\t\tkWideScale * size);\n\t\t\tauto shrink = anim::interpolate(\n\t\t\t\t(1 - kWideScale) / 2 * size,\n\t\t\t\t0,\n\t\t\t\tscale);\n\t\t\tauto margins = QMargins(shrink, shrink, shrink, shrink);\n\t\t\tp.drawImage(target.marginsAdded(margins), userpic.cache);\n\t\t}\n\t}\n\tp.setOpacity(1.);\n\n\tconst auto hidden = [](const Userpic &userpic) {\n\t\treturn userpic.hiding && !userpic.shownAnimation.animating();\n\t};\n\t_list.erase(ranges::remove_if(_list, hidden), end(_list));\n}\n\nint GroupCallUserpics::maxWidth() const {\n\treturn _maxWidth;\n}\n\nrpl::producer<int> GroupCallUserpics::widthValue() const {\n\treturn _width.value();\n}\n\nbool GroupCallUserpics::needCacheRefresh(Userpic &userpic) {\n\tif (userpic.cache.isNull()) {\n\t\treturn true;\n\t} else if (userpic.hiding) {\n\t\treturn false;\n\t} else if (userpic.cacheKey != userpic.data.userpicKey) {\n\t\treturn true;\n\t}\n\tconst auto shouldBeMasked = !userpic.topMost;\n\tif (userpic.cacheMasked == shouldBeMasked || !shouldBeMasked) {\n\t\treturn true;\n\t}\n\treturn !userpic.leftAnimation.animating();\n}\n\nvoid GroupCallUserpics::ensureBlobsAnimation(Userpic &userpic) {\n\tif (userpic.blobsAnimation) {\n\t\treturn;\n\t}\n\tuserpic.blobsAnimation = std::make_unique<BlobsAnimation>(\n\t\tBlobs() | ranges::to_vector,\n\t\tkLevelDuration,\n\t\tkMaxLevel);\n\tuserpic.blobsAnimation->lastTime = crl::now();\n}\n\nvoid GroupCallUserpics::sendRandomLevels() {\n\tif (_skipLevelUpdate) {\n\t\treturn;\n\t}\n\tfor (auto &userpic : _list) {\n\t\tif (const auto blobs = userpic.blobsAnimation.get()) {\n\t\t\tconst auto value = 30 + base::RandomIndex(70);\n\t\t\tblobs->blobs.setLevel(float64(value) / 100.);\n\t\t}\n\t}\n}\n\nvoid GroupCallUserpics::validateCache(Userpic &userpic) {\n\tif (!needCacheRefresh(userpic)) {\n\t\treturn;\n\t}\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto size = _st.size;\n\tconst auto shift = _st.shift;\n\tconst auto full = QSize(size, size) * kWideScale * factor;\n\tif (userpic.cache.isNull()) {\n\t\tuserpic.cache = QImage(full, QImage::Format_ARGB32_Premultiplied);\n\t\tuserpic.cache.setDevicePixelRatio(factor);\n\t}\n\tuserpic.cacheKey = userpic.data.userpicKey;\n\tuserpic.cacheMasked = !userpic.topMost;\n\tuserpic.cache.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&userpic.cache);\n\t\tconst auto skip = (kWideScale - 1) / 2 * size;\n\t\tp.drawImage(QRect(skip, skip, size, size), userpic.data.userpic);\n\n\t\tif (userpic.cacheMasked) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tauto pen = QPen(Qt::transparent);\n\t\t\tpen.setWidth(_st.stroke);\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\t\tp.setBrush(Qt::transparent);\n\t\t\tp.setPen(pen);\n\t\t\tp.drawEllipse(skip - size + shift, skip, size, size);\n\t\t}\n\t}\n}\n\nvoid GroupCallUserpics::update(\n\t\tconst std::vector<GroupCallUser> &users,\n\t\tbool visible) {\n\tconst auto idFromUserpic = [](const Userpic &userpic) {\n\t\treturn userpic.data.id;\n\t};\n\n\t// Use \"topMost\" as \"willBeHidden\" flag.\n\tfor (auto &userpic : _list) {\n\t\tuserpic.topMost = true;\n\t}\n\tfor (const auto &user : users) {\n\t\tconst auto i = ranges::find(_list, user.id, idFromUserpic);\n\t\tif (i == end(_list)) {\n\t\t\t_list.push_back(Userpic{ user });\n\t\t\ttoggle(_list.back(), true);\n\t\t\tcontinue;\n\t\t}\n\t\ti->topMost = false;\n\n\t\tif (i->hiding) {\n\t\t\ttoggle(*i, true);\n\t\t}\n\t\ti->data = user;\n\n\t\t// Put this one after the last we are not hiding.\n\t\tfor (auto j = end(_list) - 1; j != i; --j) {\n\t\t\tif (!j->topMost) {\n\t\t\t\tranges::rotate(i, i + 1, j + 1);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Hide the ones that \"willBeHidden\" (currently having \"topMost\" flag).\n\t// Set correct real values of \"topMost\" flag.\n\tconst auto userpicsBegin = begin(_list);\n\tconst auto userpicsEnd = end(_list);\n\tauto markedTopMost = userpicsEnd;\n\tauto hasBlobs = false;\n\tfor (auto i = userpicsBegin; i != userpicsEnd; ++i) {\n\t\tauto &userpic = *i;\n\t\tif (userpic.data.speaking) {\n\t\t\tensureBlobsAnimation(userpic);\n\t\t\thasBlobs = true;\n\t\t} else {\n\t\t\tuserpic.blobsAnimation = nullptr;\n\t\t}\n\t\tif (userpic.topMost) {\n\t\t\ttoggle(userpic, false);\n\t\t\tuserpic.topMost = false;\n\t\t} else if (markedTopMost == userpicsEnd) {\n\t\t\tuserpic.topMost = true;\n\t\t\tmarkedTopMost = i;\n\t\t}\n\t}\n\tif (markedTopMost != userpicsEnd && markedTopMost != userpicsBegin) {\n\t\t// Bring the topMost userpic to the very beginning, above all hiding.\n\t\tstd::rotate(userpicsBegin, markedTopMost, markedTopMost + 1);\n\t}\n\tupdatePositions();\n\n\tif (!hasBlobs) {\n\t\t_randomSpeakingTimer.cancel();\n\t\t_speakingAnimation.stop();\n\t} else if (!_randomSpeakingTimer.isActive()) {\n\t\t_randomSpeakingTimer.callEach(kSendRandomLevelInterval);\n\t\t_speakingAnimation.start();\n\t}\n\n\tif (visible) {\n\t\trecountAndRepaint();\n\t} else {\n\t\tfinishAnimating();\n\t}\n}\n\nvoid GroupCallUserpics::finishAnimating() {\n\tfor (auto &userpic : _list) {\n\t\tuserpic.shownAnimation.stop();\n\t\tuserpic.leftAnimation.stop();\n\t}\n\trecountAndRepaint();\n}\n\nvoid GroupCallUserpics::toggle(Userpic &userpic, bool shown) {\n\tif (userpic.hiding == !shown && !userpic.shownAnimation.animating()) {\n\t\treturn;\n\t}\n\tuserpic.hiding = !shown;\n\tuserpic.shownAnimation.start(\n\t\t[=] { recountAndRepaint(); },\n\t\tshown ? 0. : 1.,\n\t\tshown ? 1. : 0.,\n\t\tkDuration);\n}\n\nvoid GroupCallUserpics::updatePositions() {\n\tconst auto shownCount = ranges::count(_list, false, &Userpic::hiding);\n\tif (!shownCount) {\n\t\treturn;\n\t}\n\tconst auto single = _st.size;\n\tconst auto shift = _st.shift;\n\t// + 1 * single for the blobs.\n\tconst auto fullWidth = single + (shownCount - 1) * (single - shift);\n\tauto left = (_st.align & Qt::AlignLeft)\n\t\t? 0\n\t\t: (_st.align & Qt::AlignHCenter)\n\t\t? (-fullWidth / 2)\n\t\t: -fullWidth;\n\tfor (auto &userpic : _list) {\n\t\tif (userpic.hiding) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!userpic.positionInited) {\n\t\t\tuserpic.positionInited = true;\n\t\t\tuserpic.left = left;\n\t\t} else if (userpic.left != left) {\n\t\t\tuserpic.leftAnimation.start(\n\t\t\t\t_repaint,\n\t\t\t\tuserpic.left,\n\t\t\t\tleft,\n\t\t\t\tkDuration);\n\t\t\tuserpic.left = left;\n\t\t}\n\t\tleft += (single - shift);\n\t}\n}\n\nvoid GroupCallUserpics::recountAndRepaint() {\n\tauto width = 0;\n\tauto maxShown = 0.;\n\tfor (const auto &userpic : _list) {\n\t\tconst auto shown = userpic.shownAnimation.value(\n\t\t\tuserpic.hiding ? 0. : 1.);\n\t\tif (shown > maxShown) {\n\t\t\tmaxShown = shown;\n\t\t}\n\t\twidth += anim::interpolate(0, _st.size - _st.shift, shown);\n\t}\n\t_width = width + anim::interpolate(0, _st.shift, maxShown);\n\tif (_repaint) {\n\t\t_repaint();\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/group_call_userpics.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n\nnamespace style {\nstruct GroupCallUserpics;\n} // namespace style\n\nnamespace Ui {\n\nstruct GroupCallUser {\n\tQImage userpic;\n\tstd::pair<uint64, uint64> userpicKey = {};\n\tuint64 id = 0;\n\tbool speaking = false;\n};\n\nclass GroupCallUserpics final {\npublic:\n\tGroupCallUserpics(\n\t\tconst style::GroupCallUserpics &st,\n\t\trpl::producer<bool> &&hideBlobs,\n\t\tFn<void()> repaint);\n\t~GroupCallUserpics();\n\n\tvoid update(\n\t\tconst std::vector<GroupCallUser> &users,\n\t\tbool visible);\n\tvoid paint(QPainter &p, int x, int y, int size);\n\tvoid finishAnimating();\n\n\t[[nodiscard]] int maxWidth() const;\n\t[[nodiscard]] rpl::producer<int> widthValue() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tusing User = GroupCallUser;\n\tstruct BlobsAnimation;\n\tstruct Userpic;\n\n\tvoid toggle(Userpic &userpic, bool shown);\n\tvoid updatePositions();\n\tvoid validateCache(Userpic &userpic);\n\t[[nodiscard]] bool needCacheRefresh(Userpic &userpic);\n\tvoid ensureBlobsAnimation(Userpic &userpic);\n\tvoid sendRandomLevels();\n\tvoid recountAndRepaint();\n\n\tconst style::GroupCallUserpics &_st;\n\tstd::vector<Userpic> _list;\n\tbase::Timer _randomSpeakingTimer;\n\tFn<void()> _repaint;\n\tUi::Animations::Basic _speakingAnimation;\n\tint _maxWidth = 0;\n\tbool _skipLevelUpdate = false;\n\tcrl::time _speakingAnimationHideLastTime = 0;\n\n\trpl::variable<int> _width;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/message_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/message_bar.h\"\n\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/palette.h\"\n\nnamespace Ui {\nnamespace {\n\n[[nodiscard]] int SameFirstPartLength(const QString &a, const QString &b) {\n\tconst auto &[i, j] = ranges::mismatch(a, b);\n\treturn (i - a.begin());\n}\n\n[[nodiscard]] bool MuchDifferent(\n\t\tint same,\n\t\tconst QString &a,\n\t\tconst QString &b) {\n\treturn (same * 2 < a.size()) || (same * 2 < b.size());\n}\n\n[[nodiscard]] bool MuchDifferent(const QString &a, const QString &b) {\n\treturn MuchDifferent(SameFirstPartLength(a, b), a, b);\n}\n\n[[nodiscard]] bool ComplexTitleAnimation(\n\t\tint same,\n\t\tconst QString &a,\n\t\tconst QString &b) {\n\treturn !MuchDifferent(same, a, b)\n\t\t&& (same != a.size() || same != b.size());\n}\n\n} // namespace\n\nMessageBar::MessageBar(\n\tnot_null<QWidget*> parent,\n\tconst style::MessageBar &st,\n\tFn<bool()> customEmojiPaused)\n: _st(st)\n, _widget(parent)\n, _customEmojiPaused(std::move(customEmojiPaused)) {\n\tsetup();\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_topBarGradient = _bottomBarGradient = QPixmap();\n\t}, _widget.lifetime());\n}\n\nvoid MessageBar::customEmojiRepaint() {\n\tif (_customEmojiRepaintScheduled) {\n\t\treturn;\n\t}\n\t_customEmojiRepaintScheduled = true;\n\t_widget.update();\n}\n\nvoid MessageBar::setup() {\n\t_widget.resize(0, st::historyReplyHeight);\n\t_widget.paintRequest(\n\t) | rpl::on_next([=](QRect rect) {\n\t\tauto p = Painter(&_widget);\n\t\tp.setInactive(_customEmojiPaused());\n\t\t_customEmojiRepaintScheduled = false;\n\t\tpaint(p);\n\t}, _widget.lifetime());\n}\n\nvoid MessageBar::set(MessageBarContent &&content) {\n\t_contentLifetime.destroy();\n\ttweenTo(std::move(content));\n}\n\nvoid MessageBar::set(rpl::producer<MessageBarContent> content) {\n\t_contentLifetime.destroy();\n\tstd::move(\n\t\tcontent\n\t) | rpl::on_next([=](MessageBarContent &&content) {\n\t\ttweenTo(std::move(content));\n\t}, _contentLifetime);\n}\n\nMessageBar::BodyAnimation MessageBar::DetectBodyAnimationType(\n\t\tAnimation *currentAnimation,\n\t\tconst MessageBarContent &currentContent,\n\t\tconst MessageBarContent &nextContent) {\n\tconst auto now = currentAnimation\n\t\t? currentAnimation->bodyAnimation\n\t\t: BodyAnimation::None;\n\tconst auto somethingChanged = (currentContent.text != nextContent.text)\n\t\t|| (currentContent.title != nextContent.title)\n\t\t|| (currentContent.index != nextContent.index)\n\t\t|| (currentContent.count != nextContent.count);\n\treturn (now == BodyAnimation::Full\n\t\t|| MuchDifferent(currentContent.title, nextContent.title)\n\t\t|| (currentContent.title.isEmpty() && somethingChanged))\n\t\t? BodyAnimation::Full\n\t\t: (now == BodyAnimation::Text || somethingChanged)\n\t\t? BodyAnimation::Text\n\t\t: BodyAnimation::None;\n}\n\nvoid MessageBar::tweenTo(MessageBarContent &&content) {\n\tExpects(content.count > 0);\n\tExpects(content.index >= 0 && content.index < content.count);\n\n\t_widget.update();\n\tif (!_st.duration || anim::Disabled() || _widget.size().isEmpty()) {\n\t\tupdateFromContent(std::move(content));\n\t\treturn;\n\t}\n\tconst auto hasImageChanged = (_content.preview.isNull()\n\t\t!= content.preview.isNull());\n\tconst auto bodyChanged = (_content.index != content.index\n\t\t|| _content.count != content.count\n\t\t|| _content.title != content.title\n\t\t|| _content.text != content.text\n\t\t|| _content.preview.constBits() != content.preview.constBits());\n\tconst auto barCountChanged = (_content.count != content.count);\n\tconst auto barFrom = _content.index;\n\tconst auto barTo = content.index;\n\tauto animation = Animation();\n\tanimation.bodyAnimation = DetectBodyAnimationType(\n\t\t_animation.get(),\n\t\t_content,\n\t\tcontent);\n\tanimation.movingTo = (content.index > _content.index)\n\t\t? RectPart::Top\n\t\t: (content.index < _content.index)\n\t\t? RectPart::Bottom\n\t\t: RectPart::None;\n\tanimation.imageFrom = grabImagePart();\n\tanimation.spoilerFrom = std::move(_spoiler);\n\tanimation.bodyOrTextFrom = grabBodyOrTextPart(animation.bodyAnimation);\n\tconst auto sameLength = SameFirstPartLength(\n\t\t_content.title,\n\t\tcontent.title);\n\tif (animation.bodyAnimation == BodyAnimation::Text\n\t\t&& ComplexTitleAnimation(sameLength, _content.title, content.title)) {\n\t\tanimation.titleSame = grabTitleBase(sameLength);\n\t\tanimation.titleFrom = grabTitlePart(sameLength);\n\t}\n\tauto was = std::move(_animation);\n\tupdateFromContent(std::move(content));\n\tanimation.imageTo = grabImagePart();\n\tanimation.bodyOrTextTo = grabBodyOrTextPart(animation.bodyAnimation);\n\tif (!animation.titleSame.isNull()) {\n\t\tanimation.titleTo = grabTitlePart(sameLength);\n\t}\n\tif (was) {\n\t\t_animation = std::move(was);\n\t\tstd::swap(*_animation, animation);\n\t\t_animation->imageShown = std::move(animation.imageShown);\n\t\t_animation->barScroll = std::move(animation.barScroll);\n\t\t_animation->barTop = std::move(animation.barTop);\n\t} else {\n\t\t_animation = std::make_unique<Animation>(std::move(animation));\n\t}\n\n\tif (hasImageChanged) {\n\t\t_animation->imageShown.start(\n\t\t\t[=] { _widget.update(); },\n\t\t\t_image.isNull() ? 1. : 0.,\n\t\t\t_image.isNull() ? 0. : 1.,\n\t\t\t_st.duration);\n\t}\n\tif (bodyChanged) {\n\t\t_animation->bodyMoved.start(\n\t\t\t[=] { _widget.update(); },\n\t\t\t0.,\n\t\t\t1.,\n\t\t\t_st.duration);\n\t}\n\tif (barCountChanged) {\n\t\t_animation->barScroll.stop();\n\t\t_animation->barTop.stop();\n\t} else if (barFrom != barTo) {\n\t\tconst auto wasState = countBarState(barFrom);\n\t\tconst auto nowState = countBarState(barTo);\n\t\t_animation->barScroll.start(\n\t\t\t[=] { _widget.update(); },\n\t\t\twasState.scroll,\n\t\t\tnowState.scroll,\n\t\t\t_st.duration);\n\t\t_animation->barTop.start(\n\t\t\t[] {},\n\t\t\twasState.offset,\n\t\t\tnowState.offset,\n\t\t\t_st.duration);\n\t}\n}\n\nvoid MessageBar::updateFromContent(MessageBarContent &&content) {\n\t_content = std::move(content);\n\t_title.setText(_st.title, _content.title);\n\t_text.setMarkedText(\n\t\t_st.text,\n\t\t_content.text,\n\t\tUi::DialogTextOptions(),\n\t\t_content.context);\n\t_image = prepareImage(_content.preview);\n\tif (!_content.spoilerRepaint) {\n\t\t_spoiler = nullptr;\n\t} else if (!_spoiler) {\n\t\t_spoiler = std::make_unique<SpoilerAnimation>(\n\t\t\t_content.spoilerRepaint);\n\t}\n}\n\nQRect MessageBar::imageRect() const {\n\tconst auto left = st::msgReplyBarSkip + st::msgReplyBarSkip;\n\tconst auto top = (st::historyReplyHeight - st::historyReplyPreview) / 2;\n\tconst auto size = st::historyReplyPreview;\n\treturn QRect(left, top, size, size);\n}\n\nQRect MessageBar::titleRangeRect(int from, int till) const {\n\tauto result = bodyRect();\n\tresult.setHeight(st::msgServiceNameFont->height);\n\tconst auto left = from\n\t\t? st::msgServiceNameFont->width(_content.title.mid(0, from))\n\t\t: 0;\n\tconst auto right = (till <= _content.title.size())\n\t\t? st::msgServiceNameFont->width(_content.title.mid(0, till))\n\t\t: result.width();\n\tresult.setLeft(result.left() + left);\n\tresult.setWidth(right - left);\n\treturn result;\n}\n\nQRect MessageBar::bodyRect(bool withImage) const {\n\tconst auto innerLeft = st::msgReplyBarSkip + st::msgReplyBarSkip;\n\tconst auto imageSkip = st::historyReplyPreview + st::msgReplyBarSkip;\n\tconst auto left = innerLeft + (withImage ? imageSkip : 0);\n\tconst auto top = st::msgReplyPadding.top();\n\tconst auto width = _widget.width() - left - st::msgReplyPadding.right();\n\tconst auto height = (st::historyReplyHeight - 2 * top);\n\treturn QRect(left, top, width, height) - _content.margins;\n}\n\nQRect MessageBar::bodyRect() const {\n\treturn bodyRect(!_image.isNull());\n}\n\nQRect MessageBar::textRect() const {\n\tauto result = bodyRect();\n\tresult.setTop(result.top() + st::msgServiceNameFont->height);\n\treturn result;\n}\n\nauto MessageBar::makeGrabGuard() {\n\tauto imageShown = _animation\n\t\t? std::move(_animation->imageShown)\n\t\t: Ui::Animations::Simple();\n\tauto spoiler = std::move(_spoiler);\n\tauto fromSpoiler = _animation\n\t\t? std::move(_animation->spoilerFrom)\n\t\t: nullptr;\n\treturn gsl::finally([\n\t\t&,\n\t\tshown = std::move(imageShown),\n\t\tspoiler = std::move(spoiler),\n\t\tfromSpoiler = std::move(fromSpoiler)\n\t]() mutable {\n\t\tif (_animation) {\n\t\t\t_animation->imageShown = std::move(shown);\n\t\t\t_animation->spoilerFrom = std::move(fromSpoiler);\n\t\t}\n\t\t_spoiler = std::move(spoiler);\n\t});\n}\n\nQPixmap MessageBar::grabBodyOrTextPart(BodyAnimation type) {\n\treturn (type == BodyAnimation::Full)\n\t\t? grabBodyPart()\n\t\t: (type == BodyAnimation::Text)\n\t\t? grabTextPart()\n\t\t: QPixmap();\n}\n\nQPixmap MessageBar::grabTitleBase(int till) {\n\treturn grabTitleRange(0, till);\n}\n\nQPixmap MessageBar::grabTitlePart(int from) {\n\tExpects(from <= _content.title.size());\n\n\treturn grabTitleRange(from, _content.title.size());\n}\n\nQPixmap MessageBar::grabTitleRange(int from, int till) {\n\tconst auto guard = makeGrabGuard();\n\treturn GrabWidget(widget(), titleRangeRect(from, till));\n}\n\nQPixmap MessageBar::grabBodyPart() {\n\tconst auto guard = makeGrabGuard();\n\treturn GrabWidget(widget(), bodyRect());\n}\n\nQPixmap MessageBar::grabTextPart() {\n\tconst auto guard = makeGrabGuard();\n\treturn GrabWidget(widget(), textRect());\n}\n\nQPixmap MessageBar::grabImagePart() {\n\tif (!_animation) {\n\t\treturn _image;\n\t}\n\tconst auto guard = makeGrabGuard();\n\treturn (_animation->bodyMoved.animating()\n\t\t&& !_animation->imageFrom.isNull()\n\t\t&& !_animation->imageTo.isNull())\n\t\t? GrabWidget(widget(), imageRect())\n\t\t: _animation->imageFrom;\n}\n\nvoid MessageBar::finishAnimating() {\n\tif (_animation) {\n\t\t_animation = nullptr;\n\t\t_widget.update();\n\t}\n}\n\nQPixmap MessageBar::prepareImage(const QImage &preview) {\n\treturn QPixmap::fromImage(preview, Qt::ColorOnly);\n}\n\nvoid MessageBar::paint(Painter &p) {\n\tconst auto progress = _animation ? _animation->bodyMoved.value(1.) : 1.;\n\tconst auto imageFinal = _image.isNull() ? 0. : 1.;\n\tconst auto imageShown = _animation\n\t\t? _animation->imageShown.value(imageFinal)\n\t\t: imageFinal;\n\tif (progress == 1. && imageShown == imageFinal && _animation) {\n\t\t_animation = nullptr;\n\t}\n\tconst auto body = [&] {\n\t\tif (!_animation || !_animation->imageShown.animating()) {\n\t\t\treturn bodyRect();\n\t\t}\n\t\tconst auto noImage = bodyRect(false);\n\t\tconst auto withImage = bodyRect(true);\n\t\treturn QRect(\n\t\t\tanim::interpolate(noImage.x(), withImage.x(), imageShown),\n\t\t\tnoImage.y(),\n\t\t\tanim::interpolate(noImage.width(), withImage.width(), imageShown),\n\t\t\tnoImage.height());\n\t}();\n\tconst auto text = textRect();\n\tconst auto image = imageRect();\n\tconst auto width = _widget.width();\n\tconst auto noShift = !_animation\n\t\t|| (_animation->movingTo == RectPart::None);\n\tconst auto shiftFull = st::msgReplyBarSkip;\n\tconst auto shiftTo = noShift\n\t\t? 0\n\t\t: (_animation->movingTo == RectPart::Top)\n\t\t? anim::interpolate(shiftFull, 0, progress)\n\t\t: anim::interpolate(-shiftFull, 0, progress);\n\tconst auto shiftFrom = noShift\n\t\t? 0\n\t\t: (_animation->movingTo == RectPart::Top)\n\t\t? (shiftTo - shiftFull)\n\t\t: (shiftTo + shiftFull);\n\tconst auto now = crl::now();\n\tconst auto paused = p.inactive();\n\tconst auto pausedSpoiler = paused || On(PowerSaving::kChatSpoiler);\n\n\tpaintLeftBar(p);\n\n\tif (!_animation) {\n\t\tif (!_image.isNull()) {\n\t\t\tpaintImageWithSpoiler(\n\t\t\t\tp,\n\t\t\t\timage,\n\t\t\t\t_image,\n\t\t\t\t_spoiler.get(),\n\t\t\t\tnow,\n\t\t\t\tpausedSpoiler);\n\t\t}\n\t} else if (!_animation->imageTo.isNull()\n\t\t|| (!_animation->imageFrom.isNull()\n\t\t\t&& _animation->imageShown.animating())) {\n\t\tconst auto rect = [&] {\n\t\t\tif (!_animation->imageShown.animating()) {\n\t\t\t\treturn image;\n\t\t\t}\n\t\t\tconst auto size = anim::interpolate(0, image.width(), imageShown);\n\t\t\treturn QRect(\n\t\t\t\timage.x(),\n\t\t\t\timage.y() + (image.height() - size) / 2,\n\t\t\t\tsize,\n\t\t\t\tsize);\n\t\t}();\n\t\tif (_animation->bodyMoved.animating()) {\n\t\t\tp.setOpacity(1. - progress);\n\t\t\tpaintImageWithSpoiler(\n\t\t\t\tp,\n\t\t\t\trect.translated(0, shiftFrom),\n\t\t\t\t_animation->imageFrom,\n\t\t\t\t_animation->spoilerFrom.get(),\n\t\t\t\tnow,\n\t\t\t\tpausedSpoiler);\n\t\t\tp.setOpacity(progress);\n\t\t\tpaintImageWithSpoiler(\n\t\t\t\tp,\n\t\t\t\trect.translated(0, shiftTo),\n\t\t\t\t_animation->imageTo,\n\t\t\t\t_spoiler.get(),\n\t\t\t\tnow,\n\t\t\t\tpausedSpoiler);\n\t\t\tp.setOpacity(1.);\n\t\t} else {\n\t\t\tpaintImageWithSpoiler(\n\t\t\t\tp,\n\t\t\t\trect,\n\t\t\t\t_image,\n\t\t\t\t_spoiler.get(),\n\t\t\t\tnow,\n\t\t\t\tpausedSpoiler);\n\t\t}\n\t}\n\tif (!_animation || _animation->bodyAnimation == BodyAnimation::None) {\n\t\tif (_title.isEmpty()) {\n\t\t\t// \"Loading...\" state.\n\t\t\tp.setPen(st::historyComposeAreaFgService);\n\t\t\t_text.draw(p, {\n\t\t\t\t.position = {\n\t\t\t\t\tbody.x(),\n\t\t\t\t\tbody.y() + (body.height() - st::normalFont->height) / 2,\n\t\t\t\t},\n\t\t\t\t.outerWidth = width,\n\t\t\t\t.availableWidth = body.width(),\n\t\t\t\t.elisionLines = 1,\n\t\t\t});\n\t\t} else {\n\t\t\tp.setPen(_st.textFg);\n\t\t\t_text.draw(p, {\n\t\t\t\t.position = { body.x(), text.y() },\n\t\t\t\t.outerWidth = width,\n\t\t\t\t.availableWidth = body.width(),\n\t\t\t\t.palette = &_st.textPalette,\n\t\t\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t\t\t.now = now,\n\t\t\t\t.pausedEmoji = paused || On(PowerSaving::kEmojiChat),\n\t\t\t\t.pausedSpoiler = pausedSpoiler,\n\t\t\t\t.elisionLines = 1,\n\t\t\t});\n\t\t}\n\t} else if (_animation->bodyAnimation == BodyAnimation::Text) {\n\t\tp.setOpacity(1. - progress);\n\t\tp.drawPixmap(\n\t\t\tbody.x(),\n\t\t\ttext.y() + shiftFrom,\n\t\t\t_animation->bodyOrTextFrom);\n\t\tp.setOpacity(progress);\n\t\tp.drawPixmap(body.x(), text.y() + shiftTo, _animation->bodyOrTextTo);\n\t\tp.setOpacity(1.);\n\t}\n\tif (!_animation || _animation->bodyAnimation != BodyAnimation::Full) {\n\t\tif (_animation && !_animation->titleSame.isNull()) {\n\t\t\tconst auto factor = style::DevicePixelRatio();\n\t\t\tp.drawPixmap(body.x(), body.y(), _animation->titleSame);\n\t\t\tp.setOpacity(1. - progress);\n\t\t\tp.drawPixmap(\n\t\t\t\tbody.x() + (_animation->titleSame.width() / factor),\n\t\t\t\tbody.y() + shiftFrom,\n\t\t\t\t_animation->titleFrom);\n\t\t\tp.setOpacity(progress);\n\t\t\tp.drawPixmap(\n\t\t\t\tbody.x() + (_animation->titleSame.width() / factor),\n\t\t\t\tbody.y() + shiftTo,\n\t\t\t\t_animation->titleTo);\n\t\t\tp.setOpacity(1.);\n\t\t} else {\n\t\t\tp.setPen(_st.titleFg);\n\t\t\t_title.drawLeftElided(p, body.x(), body.y(), body.width(), width);\n\t\t}\n\t} else {\n\t\tp.setOpacity(1. - progress);\n\t\tp.drawPixmap(\n\t\t\tbody.x(),\n\t\t\tbody.y() + shiftFrom,\n\t\t\t_animation->bodyOrTextFrom);\n\t\tp.setOpacity(progress);\n\t\tp.drawPixmap(body.x(), body.y() + shiftTo, _animation->bodyOrTextTo);\n\t\tp.setOpacity(1.);\n\t}\n}\n\nauto MessageBar::countBarState(int index) const -> BarState {\n\tExpects(index >= 0 && index < _content.count);\n\n\tauto result = BarState();\n\tconst auto line = st::msgReplyBarSize.width();\n\tconst auto height = st::msgReplyBarSize.height();\n\tconst auto count = _content.count;\n\tconst auto shownCount = std::min(count, 4);\n\tconst auto dividers = (shownCount - 1) * line;\n\tconst auto size = float64(st::msgReplyBarSize.height() - dividers)\n\t\t/ shownCount;\n\tconst auto fullHeight = count * size + (count - 1) * line;\n\tconst auto topByIndex = [&](int index) {\n\t\treturn index * (size + line);\n\t};\n\tresult.scroll = (count < 5 || index < 2)\n\t\t? 0\n\t\t: (index >= count - 2)\n\t\t? (fullHeight - height)\n\t\t: (topByIndex(index) - (height - size) / 2);\n\tresult.size = size;\n\tresult.skip = line;\n\tresult.offset = topByIndex(index);\n\treturn result;\n}\n\nauto MessageBar::countBarState() const -> BarState {\n\treturn countBarState(_content.index);\n}\n\nvoid MessageBar::ensureGradientsCreated(int size) {\n\tif (!_topBarGradient.isNull()) {\n\t\treturn;\n\t}\n\tconst auto rows = size * style::DevicePixelRatio() - 2;\n\tauto bottomMask = QImage(\n\t\tQSize(1, size) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tconst auto step = ((1ULL << 24) - 1) / rows;\n\tconst auto limit = step * rows;\n\tauto bits = bottomMask.bits();\n\tconst auto perLine = bottomMask.bytesPerLine();\n\tfor (auto counter = uint32(0); counter != limit; counter += step) {\n\t\tconst auto value = (counter >> 16);\n\t\tmemset(bits, int(value), perLine);\n\t\tbits += perLine;\n\t}\n\tmemset(bits, 255, perLine * 2);\n\tauto bottom = style::colorizeImage(bottomMask, st::historyPinnedBg);\n\tbottom.setDevicePixelRatio(style::DevicePixelRatio());\n\tauto top = bottom.mirrored();\n\t_bottomBarGradient = Images::PixmapFast(std::move(bottom));\n\t_topBarGradient = Images::PixmapFast(std::move(top));\n}\n\nvoid MessageBar::paintImageWithSpoiler(\n\t\tQPainter &p,\n\t\tQRect rect,\n\t\tconst QPixmap &image,\n\t\tSpoilerAnimation *spoiler,\n\t\tcrl::time now,\n\t\tbool paused) const {\n\tp.drawPixmap(rect, image);\n\tif (spoiler) {\n\t\tconst auto frame = DefaultImageSpoiler().frame(\n\t\t\tspoiler->index(now, paused));\n\t\tFillSpoilerRect(p, rect, frame);\n\t}\n}\n\nvoid MessageBar::paintLeftBar(Painter &p) {\n\tconst auto state = countBarState();\n\tconst auto gradientSize = int(std::ceil(state.size * 2.5));\n\tif (_content.count > 4) {\n\t\tensureGradientsCreated(gradientSize);\n\t}\n\n\tconst auto scroll = _animation\n\t\t? _animation->barScroll.value(state.scroll)\n\t\t: state.scroll;\n\tconst auto offset = _animation\n\t\t? _animation->barTop.value(state.offset)\n\t\t: state.offset;\n\tconst auto line = st::msgReplyBarSize.width();\n\tconst auto height = st::msgReplyBarSize.height();\n\tconst auto activeFrom = offset - scroll;\n\tconst auto activeTill = activeFrom + state.size;\n\tconst auto single = state.size + state.skip;\n\n\tconst auto barSkip = st::msgReplyPadding.top() + st::msgReplyBarPos.y();\n\tconst auto fullHeight = barSkip + height + barSkip;\n\tconst auto bar = QRect(\n\t\tst::msgReplyBarSkip + st::msgReplyBarPos.x(),\n\t\tbarSkip,\n\t\tline,\n\t\tstate.size);\n\tconst auto paintFromScroll = std::max(scroll - barSkip, 0.);\n\tconst auto paintFrom = int(std::floor(paintFromScroll / single));\n\tconst auto paintTillScroll = (scroll + height + barSkip);\n\tconst auto paintTill = std::min(\n\t\tint(std::floor(paintTillScroll / single)) + 1,\n\t\t_content.count);\n\n\tp.setPen(Qt::NoPen);\n\tconst auto activeBrush = QBrush(st::msgInReplyBarColor);\n\tconst auto inactiveBrush = QBrush(QColor(\n\t\tst::msgInReplyBarColor->c.red(),\n\t\tst::msgInReplyBarColor->c.green(),\n\t\tst::msgInReplyBarColor->c.blue(),\n\t\tst::msgInReplyBarColor->c.alpha() / 3));\n\tconst auto radius = line / 2.;\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setClipRect(bar.x(), 0, bar.width(), fullHeight);\n\tfor (auto i = paintFrom; i != paintTill; ++i) {\n\t\tconst auto top = i * single - scroll;\n\t\tconst auto bottom = top + state.size;\n\t\tconst auto active = (top == activeFrom);\n\t\tp.setBrush(active ? activeBrush : inactiveBrush);\n\t\tp.drawRoundedRect(bar.translated(0, top), radius, radius);\n\t\tif (active\n\t\t\t|| bottom - line <= activeFrom\n\t\t\t|| top + line >= activeTill) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto partFrom = std::max(top, activeFrom);\n\t\tconst auto partTill = std::min(bottom, activeTill);\n\t\tp.setBrush(activeBrush);\n\t\tp.drawRoundedRect(\n\t\t\tQRect(bar.x(), bar.y() + partFrom, line, partTill - partFrom),\n\t\t\tradius,\n\t\t\tradius);\n\t}\n\tp.setClipping(false);\n\tif (_content.count > 4) {\n\t\tconst auto firstScroll = countBarState(2).scroll;\n\t\tconst auto gradientTop = (scroll >= firstScroll)\n\t\t\t? 0\n\t\t\t: anim::interpolate(-gradientSize, 0, scroll / firstScroll);\n\t\tconst auto lastScroll = countBarState(_content.count - 3).scroll;\n\t\tconst auto largestScroll = countBarState(_content.count - 1).scroll;\n\t\tconst auto gradientBottom = (scroll <= lastScroll)\n\t\t\t? fullHeight\n\t\t\t: anim::interpolate(\n\t\t\t\tfullHeight,\n\t\t\t\tfullHeight + gradientSize,\n\t\t\t\t(scroll - lastScroll) / (largestScroll - lastScroll));\n\t\tif (gradientTop > -gradientSize) {\n\t\t\tp.drawPixmap(\n\t\t\t\tQRect(bar.x(), gradientTop, bar.width(), gradientSize),\n\t\t\t\t_topBarGradient);\n\t\t}\n\t\tif (gradientBottom < fullHeight + gradientSize) {\n\t\t\tp.drawPixmap(\n\t\t\t\tQRect(\n\t\t\t\t\tbar.x(),\n\t\t\t\t\tgradientBottom - gradientSize,\n\t\t\t\t\tbar.width(),\n\t\t\t\t\tgradientSize),\n\t\t\t\t_bottomBarGradient);\n\t\t}\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/message_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n#include \"ui/rect_part.h\"\n#include \"ui/rp_widget.h\"\n\nclass Painter;\n\nnamespace style {\nstruct MessageBar;\n} // namespace style\n\nnamespace Ui {\n\nclass SpoilerAnimation;\n\nstruct MessageBarContent {\n\tint index = 0;\n\tint count = 1;\n\tQString title;\n\tTextWithEntities text;\n\tText::MarkedContext context;\n\tQImage preview;\n\tFn<void()> spoilerRepaint;\n\tstyle::margins margins;\n};\n\nclass MessageBar final {\npublic:\n\tMessageBar(\n\t\tnot_null<QWidget*> parent,\n\t\tconst style::MessageBar &st,\n\t\tFn<bool()> customEmojiPaused);\n\n\tvoid set(MessageBarContent &&content);\n\tvoid set(rpl::producer<MessageBarContent> content);\n\n\t[[nodiscard]] not_null<RpWidget*> widget() {\n\t\treturn &_widget;\n\t}\n\n\tvoid customEmojiRepaint();\n\tvoid finishAnimating();\n\nprivate:\n\tenum class BodyAnimation : char {\n\t\tFull,\n\t\tText,\n\t\tNone,\n\t};\n\tstruct Animation {\n\t\tAnimations::Simple bodyMoved;\n\t\tAnimations::Simple imageShown;\n\t\tAnimations::Simple barScroll;\n\t\tAnimations::Simple barTop;\n\t\tQPixmap bodyOrTextFrom;\n\t\tQPixmap bodyOrTextTo;\n\t\tQPixmap titleSame;\n\t\tQPixmap titleFrom;\n\t\tQPixmap titleTo;\n\t\tQPixmap imageFrom;\n\t\tQPixmap imageTo;\n\t\tstd::unique_ptr<SpoilerAnimation> spoilerFrom;\n\t\tBodyAnimation bodyAnimation = BodyAnimation::None;\n\t\tRectPart movingTo = RectPart::None;\n\t};\n\tstruct BarState {\n\t\tfloat64 scroll = 0.;\n\t\tfloat64 size = 0.;\n\t\tfloat64 skip = 0.;\n\t\tfloat64 offset = 0.;\n\t};\n\tvoid setup();\n\tvoid paint(Painter &p);\n\tvoid paintLeftBar(Painter &p);\n\tvoid tweenTo(MessageBarContent &&content);\n\tvoid updateFromContent(MessageBarContent &&content);\n\t[[nodiscard]] QPixmap prepareImage(const QImage &preview);\n\n\t[[nodiscard]] QRect imageRect() const;\n\t[[nodiscard]] QRect titleRangeRect(int from, int till) const;\n\t[[nodiscard]] QRect bodyRect(bool withImage) const;\n\t[[nodiscard]] QRect bodyRect() const;\n\t[[nodiscard]] QRect textRect() const;\n\n\tauto makeGrabGuard();\n\t[[nodiscard]] QPixmap grabBodyOrTextPart(BodyAnimation type);\n\t[[nodiscard]] QPixmap grabTitleBase(int till);\n\t[[nodiscard]] QPixmap grabTitlePart(int from);\n\t[[nodiscard]] QPixmap grabTitleRange(int from, int till);\n\t[[nodiscard]] QPixmap grabImagePart();\n\t[[nodiscard]] QPixmap grabBodyPart();\n\t[[nodiscard]] QPixmap grabTextPart();\n\n\t[[nodiscard]] BarState countBarState(int index) const;\n\t[[nodiscard]] BarState countBarState() const;\n\tvoid ensureGradientsCreated(int size);\n\n\tvoid paintImageWithSpoiler(\n\t\tQPainter &p,\n\t\tQRect rect,\n\t\tconst QPixmap &image,\n\t\tSpoilerAnimation *spoiler,\n\t\tcrl::time now,\n\t\tbool paused) const;\n\n\t[[nodiscard]] static BodyAnimation DetectBodyAnimationType(\n\t\tAnimation *currentAnimation,\n\t\tconst MessageBarContent &currentContent,\n\t\tconst MessageBarContent &nextContent);\n\n\tconst style::MessageBar &_st;\n\tRpWidget _widget;\n\tFn<bool()> _customEmojiPaused;\n\tMessageBarContent _content;\n\trpl::lifetime _contentLifetime;\n\tText::String _title, _text;\n\tQPixmap _image, _topBarGradient, _bottomBarGradient;\n\tstd::unique_ptr<Animation> _animation;\n\tstd::unique_ptr<SpoilerAnimation> _spoiler;\n\tbool _customEmojiRepaintScheduled = false;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/message_bubble.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/message_bubble.h\"\n\n#include \"ui/cached_round_corners.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"styles/style_chat.h\"\n\nnamespace Ui {\nnamespace {\n\nusing Corner = BubbleCornerRounding;\n\ntemplate <\n\ttypename FillBg, // fillBg(QRect rect)\n\ttypename FillSh, // fillSh(QRect rect)\n\ttypename FillCorner, // fillCorner(int x, int y, int index, Corner size)\n\ttypename PaintTail> // paintTail(QPoint bottomPosition) -> tailWidth\nvoid PaintBubbleGeneric(\n\t\tconst SimpleBubble &args,\n\t\tFillBg &&fillBg,\n\t\tFillSh &&fillSh,\n\t\tFillCorner &&fillCorner,\n\t\tPaintTail &&paintTail) {\n\tusing namespace Images;\n\n\tconst auto topLeft = args.rounding.topLeft;\n\tconst auto topRight = args.rounding.topRight;\n\tconst auto bottomWithTailLeft = args.rounding.bottomLeft;\n\tconst auto bottomWithTailRight = args.rounding.bottomRight;\n\tif (topLeft == Corner::None\n\t\t&& topRight == Corner::None\n\t\t&& bottomWithTailLeft == Corner::None\n\t\t&& bottomWithTailRight == Corner::None) {\n\t\tfillBg(args.geometry);\n\t\treturn;\n\t}\n\tconst auto bottomLeft = (bottomWithTailLeft == Corner::Tail)\n\t\t? Corner::None\n\t\t: bottomWithTailLeft;\n\tconst auto bottomRight = (bottomWithTailRight == Corner::Tail)\n\t\t? Corner::None\n\t\t: bottomWithTailRight;\n\tconst auto rect = args.geometry;\n\tconst auto small = BubbleRadiusSmall();\n\tconst auto large = BubbleRadiusLarge();\n\tconst auto cornerSize = [&](Corner corner) {\n\t\treturn (corner == Corner::Large)\n\t\t\t? large\n\t\t\t: (corner == Corner::Small)\n\t\t\t? small\n\t\t\t: 0;\n\t};\n\tconst auto verticalSkip = [&](Corner left, Corner right) {\n\t\treturn std::max(cornerSize(left), cornerSize(right));\n\t};\n\tconst auto top = verticalSkip(topLeft, topRight);\n\tconst auto bottom = verticalSkip(bottomLeft, bottomRight);\n\tif (top) {\n\t\tconst auto left = cornerSize(topLeft);\n\t\tconst auto right = cornerSize(topRight);\n\t\tif (left) {\n\t\t\tfillCorner(rect.left(), rect.top(), kTopLeft, topLeft);\n\t\t\tif (const auto add = top - left) {\n\t\t\t\tfillBg({ rect.left(), rect.top() + left, left, add });\n\t\t\t}\n\t\t}\n\t\tif (const auto fill = rect.width() - left - right; fill > 0) {\n\t\t\tfillBg({ rect.left() + left, rect.top(), fill, top });\n\t\t}\n\t\tif (right) {\n\t\t\tfillCorner(\n\t\t\t\trect.left() + rect.width() - right,\n\t\t\t\trect.top(),\n\t\t\t\tkTopRight,\n\t\t\t\ttopRight);\n\t\t\tif (const auto add = top - right) {\n\t\t\t\tfillBg({\n\t\t\t\t\trect.left() + rect.width() - right,\n\t\t\t\t\trect.top() + right,\n\t\t\t\t\tright,\n\t\t\t\t\tadd,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\tif (const auto fill = rect.height() - top - bottom; fill > 0) {\n\t\tfillBg({ rect.left(), rect.top() + top, rect.width(), fill });\n\t}\n\tif (bottom) {\n\t\tconst auto left = cornerSize(bottomLeft);\n\t\tconst auto right = cornerSize(bottomRight);\n\t\tif (left) {\n\t\t\tfillCorner(\n\t\t\t\trect.left(),\n\t\t\t\trect.top() + rect.height() - left,\n\t\t\t\tkBottomLeft,\n\t\t\t\tbottomLeft);\n\t\t\tif (const auto add = bottom - left) {\n\t\t\t\tfillBg({\n\t\t\t\t\trect.left(),\n\t\t\t\t\trect.top() + rect.height() - bottom,\n\t\t\t\t\tleft,\n\t\t\t\t\tadd,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tif (const auto fill = rect.width() - left - right; fill > 0) {\n\t\t\tfillBg({\n\t\t\t\trect.left() + left,\n\t\t\t\trect.top() + rect.height() - bottom,\n\t\t\t\tfill,\n\t\t\t\tbottom,\n\t\t\t});\n\t\t}\n\t\tif (right) {\n\t\t\tfillCorner(\n\t\t\t\trect.left() + rect.width() - right,\n\t\t\t\trect.top() + rect.height() - right,\n\t\t\t\tkBottomRight,\n\t\t\t\tbottomRight);\n\t\t\tif (const auto add = bottom - right) {\n\t\t\t\tfillBg({\n\t\t\t\t\trect.left() + rect.width() - right,\n\t\t\t\t\trect.top() + rect.height() - bottom,\n\t\t\t\t\tright,\n\t\t\t\t\tadd,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\tconst auto leftTail = (bottomWithTailLeft == Corner::Tail)\n\t\t? paintTail({ rect.x(), rect.y() + rect.height() })\n\t\t: 0;\n\tconst auto rightTail = (bottomWithTailRight == Corner::Tail)\n\t\t? paintTail({ rect.x() + rect.width(), rect.y() + rect.height() })\n\t\t: 0;\n\tif (!args.shadowed) {\n\t\treturn;\n\t}\n\tconst auto shLeft = rect.x() + cornerSize(bottomLeft) - leftTail;\n\tconst auto shWidth = rect.x()\n\t\t+ rect.width()\n\t\t- cornerSize(bottomRight)\n\t\t+ rightTail\n\t\t- shLeft;\n\tif (shWidth > 0) {\n\t\tfillSh({ shLeft, rect.y() + rect.height(), shWidth, st::msgShadow });\n\t}\n}\n\nvoid PaintPatternBubble(QPainter &p, const SimpleBubble &args) {\n\tconst auto wasOpacity = p.opacity();\n\tconst auto opacity = args.st->msgOutBg()->c.alphaF() * wasOpacity;\n\tconst auto shadowOpacity = opacity * args.st->msgOutShadow()->c.alphaF();\n\tconst auto pattern = args.pattern;\n\tconst auto &tail = (args.rounding.bottomRight == Corner::Tail)\n\t\t? pattern->tailRight\n\t\t: pattern->tailLeft;\n\tconst auto tailShift = (args.rounding.bottomRight == Corner::Tail\n\t\t? QPoint(0, tail.height())\n\t\t: QPoint(tail.width(), tail.height())) / int(tail.devicePixelRatio());\n\tconst auto fillBg = [&](const QRect &rect) {\n\t\tconst auto fill = rect.intersected(args.patternViewport);\n\t\tif (!fill.isEmpty()) {\n\t\t\tPaintPatternBubblePart(\n\t\t\t\tp,\n\t\t\t\targs.patternViewport,\n\t\t\t\tpattern->pixmap,\n\t\t\t\tfill);\n\t\t}\n\t};\n\tconst auto fillSh = [&](const QRect &rect) {\n\t\tp.setOpacity(shadowOpacity);\n\t\tfillBg(rect);\n\t\tp.setOpacity(opacity);\n\t};\n\tconst auto fillPattern = [&](\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tconst QImage &mask,\n\t\t\tQImage &cache) {\n\t\tPaintPatternBubblePart(\n\t\t\tp,\n\t\t\targs.patternViewport,\n\t\t\tpattern->pixmap,\n\t\t\tQRect(QPoint(x, y), mask.size() / int(mask.devicePixelRatio())),\n\t\t\tmask,\n\t\t\tcache);\n\t};\n\tconst auto fillCorner = [&](int x, int y, int index, Corner size) {\n\t\tauto &corner = (size == Corner::Large)\n\t\t\t? pattern->cornersLarge[index]\n\t\t\t: pattern->cornersSmall[index];\n\t\tauto &cache = (size == Corner::Large)\n\t\t\t? (index < 2\n\t\t\t\t? pattern->cornerTopLargeCache\n\t\t\t\t: pattern->cornerBottomLargeCache)\n\t\t\t: (index < 2\n\t\t\t\t? pattern->cornerTopSmallCache\n\t\t\t\t: pattern->cornerBottomSmallCache);\n\t\tfillPattern(x, y, corner, cache);\n\t};\n\tconst auto paintTail = [&](QPoint bottomPosition) {\n\t\tconst auto position = bottomPosition - tailShift;\n\t\tfillPattern(position.x(), position.y(), tail, pattern->tailCache);\n\t\treturn tail.width() / int(tail.devicePixelRatio());\n\t};\n\n\tp.setOpacity(opacity);\n\tPaintBubbleGeneric(args, fillBg, fillSh, fillCorner, paintTail);\n\tp.setOpacity(wasOpacity);\n}\n\nvoid PaintSolidBubble(QPainter &p, const SimpleBubble &args) {\n\tconst auto &st = args.st->messageStyle(args.outbg, args.selected);\n\tconst auto &bg = st.msgBg;\n\tconst auto sh = (args.rounding.bottomRight == Corner::None)\n\t\t? nullptr\n\t\t: &st.msgShadow;\n\tconst auto &tail = (args.rounding.bottomRight == Corner::Tail)\n\t\t? st.tailRight\n\t\t: st.tailLeft;\n\tconst auto tailShift = (args.rounding.bottomRight == Corner::Tail)\n\t\t? QPoint(0, tail.height())\n\t\t: QPoint(tail.width(), tail.height());\n\n\tPaintBubbleGeneric(args, [&](const QRect &rect) {\n\t\tp.fillRect(rect, bg);\n\t}, [&](const QRect &rect) {\n\t\tp.fillRect(rect, *sh);\n\t}, [&](int x, int y, int index, Corner size) {\n\t\tauto &corners = (size == Corner::Large)\n\t\t\t? st.msgBgCornersLarge\n\t\t\t: st.msgBgCornersSmall;\n\t\tp.drawPixmap(x, y, corners.p[index]);\n\t}, [&](const QPoint &bottomPosition) {\n\t\ttail.paint(p, bottomPosition - tailShift, args.outerWidth);\n\t\treturn tail.width();\n\t});\n}\n\n} // namespace\n\nstd::unique_ptr<BubblePattern> PrepareBubblePattern(\n\t\tnot_null<const style::palette*> st) {\n\tauto result = std::make_unique<Ui::BubblePattern>();\n\tresult->cornersSmall = Images::CornersMask(BubbleRadiusSmall());\n\tresult->cornersLarge = Images::CornersMask(BubbleRadiusLarge());\n\tconst auto addShadow = [&](QImage &bottomCorner) {\n\t\tauto result = QImage(\n\t\t\tbottomCorner.width(),\n\t\t\t(bottomCorner.height()\n\t\t\t\t+ st::msgShadow * int(bottomCorner.devicePixelRatio())),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tresult.fill(Qt::transparent);\n\t\tresult.setDevicePixelRatio(bottomCorner.devicePixelRatio());\n\t\tauto p = QPainter(&result);\n\t\tp.setOpacity(st->msgInShadow()->c.alphaF());\n\t\tp.drawImage(0, st::msgShadow, bottomCorner);\n\t\tp.setOpacity(1.);\n\t\tp.drawImage(0, 0, bottomCorner);\n\t\tp.end();\n\n\t\tbottomCorner = std::move(result);\n\t};\n\taddShadow(result->cornersSmall[2]);\n\taddShadow(result->cornersSmall[3]);\n\tresult->cornerTopSmallCache = QImage(\n\t\tresult->cornersSmall[0].size(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult->cornerTopLargeCache = QImage(\n\t\tresult->cornersLarge[0].size(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult->cornerBottomSmallCache = QImage(\n\t\tresult->cornersSmall[2].size(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult->cornerBottomLargeCache = QImage(\n\t\tresult->cornersLarge[2].size(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\treturn result;\n}\n\nvoid FinishBubblePatternOnMain(not_null<BubblePattern*> pattern) {\n\tpattern->tailLeft = st::historyBubbleTailOutLeft.instance(Qt::white);\n\tpattern->tailRight = st::historyBubbleTailOutRight.instance(Qt::white);\n\tpattern->tailCache = QImage(\n\t\tpattern->tailLeft.size(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n}\n\nvoid PaintBubble(QPainter &p, const SimpleBubble &args) {\n\tif (!args.selected\n\t\t&& args.outbg\n\t\t&& args.pattern\n\t\t&& !args.patternViewport.isEmpty()\n\t\t&& !args.pattern->pixmap.size().isEmpty()) {\n\t\tPaintPatternBubble(p, args);\n\t} else {\n\t\tPaintSolidBubble(p, args);\n\t}\n}\n\nvoid PaintBubble(QPainter &p, const ComplexBubble &args) {\n\tif (args.selection.empty()) {\n\t\tPaintBubble(p, args.simple);\n\t\treturn;\n\t}\n\tconst auto rect = args.simple.geometry;\n\tconst auto left = rect.x();\n\tconst auto width = rect.width();\n\tconst auto top = rect.y();\n\tconst auto bottom = top + rect.height();\n\tconst auto paintOne = [&](\n\t\t\tQRect geometry,\n\t\t\tbool selected,\n\t\t\tbool fromTop,\n\t\t\tbool tillBottom) {\n\t\tauto simple = args.simple;\n\t\tsimple.geometry = geometry;\n\t\tsimple.selected = selected;\n\t\tif (!fromTop) {\n\t\t\tsimple.rounding.topLeft\n\t\t\t\t= simple.rounding.topRight\n\t\t\t\t= Corner::None;\n\t\t}\n\t\tif (!tillBottom) {\n\t\t\tsimple.rounding.bottomLeft\n\t\t\t\t= simple.rounding.bottomRight\n\t\t\t\t= Corner::None;\n\t\t\tsimple.shadowed = false;\n\t\t}\n\t\tPaintBubble(p, simple);\n\t};\n\tauto from = top;\n\tfor (const auto &selected : args.selection) {\n\t\tif (selected.top > from) {\n\t\t\tpaintOne(\n\t\t\t\tQRect(left, from, width, selected.top - from),\n\t\t\t\tfalse,\n\t\t\t\t(from <= top),\n\t\t\t\tfalse);\n\t\t}\n\t\tpaintOne(\n\t\t\tQRect(left, selected.top, width, selected.height),\n\t\t\ttrue,\n\t\t\t(selected.top <= top),\n\t\t\t(selected.top + selected.height >= bottom));\n\t\tfrom = selected.top + selected.height;\n\t}\n\tif (from < bottom) {\n\t\tpaintOne(\n\t\t\tQRect(left, from, width, bottom - from),\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\ttrue);\n\t}\n}\n\nvoid PaintPatternBubblePart(\n\t\tQPainter &p,\n\t\tconst QRect &viewport,\n\t\tconst QPixmap &pixmap,\n\t\tconst QRect &target) {\n\tconst auto factor = pixmap.devicePixelRatio();\n\tif (viewport.size() * factor == pixmap.size()) {\n\t\tconst auto fill = target.intersected(viewport);\n\t\tif (fill.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tp.drawPixmap(fill, pixmap, QRect(\n\t\t\t(fill.topLeft() - viewport.topLeft()) * factor,\n\t\t\tfill.size() * factor));\n\t} else {\n\t\tconst auto to = viewport;\n\t\tconst auto from = QRect(QPoint(), pixmap.size());\n\t\tconst auto deviceRect = QRect(\n\t\t\tQPoint(),\n\t\t\tQSize(p.device()->width(), p.device()->height()));\n\t\tconst auto clip = (target != deviceRect);\n\t\tif (clip) {\n\t\t\tp.setClipRect(target);\n\t\t}\n\t\tp.drawPixmap(to, pixmap, from);\n\t\tif (clip) {\n\t\t\tp.setClipping(false);\n\t\t}\n\t}\n}\n\nvoid PaintPatternBubblePart(\n\t\tQPainter &p,\n\t\tconst QRect &viewport,\n\t\tconst QPixmap &pixmap,\n\t\tconst QRect &target,\n\t\tconst QImage &mask,\n\t\tQImage &cache) {\n\tExpects(mask.bytesPerLine() == mask.width() * 4);\n\tExpects(mask.format() == QImage::Format_ARGB32_Premultiplied);\n\n\tif (cache.size() != mask.size()) {\n\t\tcache = QImage(\n\t\t\tmask.size(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t}\n\tcache.setDevicePixelRatio(mask.devicePixelRatio());\n\tAssert(cache.bytesPerLine() == cache.width() * 4);\n\tmemcpy(cache.bits(), mask.constBits(), mask.sizeInBytes());\n\n\tauto q = QPainter(&cache);\n\tq.setCompositionMode(QPainter::CompositionMode_SourceIn);\n\tPaintPatternBubblePart(\n\t\tq,\n\t\tviewport.translated(-target.topLeft()),\n\t\tpixmap,\n\t\tQRect(QPoint(), cache.size() / int(cache.devicePixelRatio())));\n\tq.end();\n\n\tp.drawImage(target, cache);\n}\n\nvoid PaintPatternBubblePart(\n\t\tQPainter &p,\n\t\tconst QRect &viewport,\n\t\tconst QPixmap &pixmap,\n\t\tconst QRect &target,\n\t\tFn<void(QPainter&)> paintContent,\n\t\tQImage &cache) {\n\tExpects(paintContent != nullptr);\n\n\tconst auto targetOrigin = target.topLeft();\n\tconst auto targetSize = target.size();\n\tif (cache.size() != targetSize * style::DevicePixelRatio()) {\n\t\tcache = QImage(\n\t\t\ttarget.size() * style::DevicePixelRatio(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tcache.setDevicePixelRatio(style::DevicePixelRatio());\n\t}\n\tcache.fill(Qt::transparent);\n\tauto q = QPainter(&cache);\n\tq.translate(-targetOrigin);\n\tpaintContent(q);\n\tq.translate(targetOrigin);\n\tq.setCompositionMode(QPainter::CompositionMode_SourceIn);\n\tPaintPatternBubblePart(\n\t\tq,\n\t\tviewport.translated(-targetOrigin),\n\t\tpixmap,\n\t\tQRect(QPoint(), targetSize));\n\tq.end();\n\n\tp.drawImage(target, cache);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/message_bubble.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nclass ChatTheme;\nclass ChatStyle;\n\nenum class BubbleCornerRounding : uchar {\n\tNone,\n\tTail,\n\tSmall,\n\tLarge,\n};\n\nstruct BubbleRounding {\n\tBubbleCornerRounding topLeft : 2 = BubbleCornerRounding();\n\tBubbleCornerRounding topRight : 2 = BubbleCornerRounding();\n\tBubbleCornerRounding bottomLeft : 2 = BubbleCornerRounding();\n\tBubbleCornerRounding bottomRight : 2 = BubbleCornerRounding();\n\n\tstruct ConstProxy {\n\t\tconstexpr ConstProxy(\n\t\t\tnot_null<const BubbleRounding*> that,\n\t\t\tint index) noexcept\n\t\t: that(that)\n\t\t, index(index) {\n\t\t\tExpects(index >= 0 && index < 4);\n\t\t}\n\n\t\tconstexpr operator BubbleCornerRounding() const noexcept {\n\t\t\tswitch (index) {\n\t\t\tcase 0: return that->topLeft;\n\t\t\tcase 1: return that->topRight;\n\t\t\tcase 2: return that->bottomLeft;\n\t\t\tcase 3: return that->bottomRight;\n\t\t\t}\n\t\t\tUnexpected(\"Index value in BubbleRounding::ConstProxy.\");\n\t\t}\n\n\t\tnot_null<const BubbleRounding*> that;\n\t\tint index = 0;\n\t};\n\tstruct Proxy : ConstProxy {\n\t\tconstexpr Proxy(not_null<BubbleRounding*> that, int index) noexcept\n\t\t: ConstProxy(that, index) {\n\t\t}\n\n\t\tusing ConstProxy::operator BubbleCornerRounding;\n\n\t\tconstexpr Proxy &operator=(BubbleCornerRounding value) noexcept {\n\t\t\tconst auto nonconst = const_cast<BubbleRounding*>(that.get());\n\t\t\tswitch (index) {\n\t\t\tcase 0: nonconst->topLeft = value; break;\n\t\t\tcase 1: nonconst->topRight = value; break;\n\t\t\tcase 2: nonconst->bottomLeft = value; break;\n\t\t\tcase 3: nonconst->bottomRight = value; break;\n\t\t\t}\n\t\t\treturn *this;\n\t\t}\n\t};\n\t[[nodiscard]] constexpr ConstProxy operator[](int index) const {\n\t\treturn { this, index };\n\t}\n\t[[nodiscard]] constexpr Proxy operator[](int index) {\n\t\treturn { this, index };\n\t}\n\n\t[[nodiscard]] uchar key() const {\n\t\tstatic_assert(sizeof(*this) == sizeof(uchar));\n\t\treturn uchar(*reinterpret_cast<const std::byte*>(this));\n\t}\n\n\tinline friend constexpr auto operator<=>(\n\t\tBubbleRounding,\n\t\tBubbleRounding) = default;\n};\n\nstruct BubbleSelectionInterval {\n\tint top = 0;\n\tint height = 0;\n};\n\nstruct BubblePattern {\n\tQPixmap pixmap;\n\tstd::array<QImage, 4> cornersSmall;\n\tstd::array<QImage, 4> cornersLarge;\n\tQImage tailLeft;\n\tQImage tailRight;\n\tmutable QImage cornerTopSmallCache;\n\tmutable QImage cornerTopLargeCache;\n\tmutable QImage cornerBottomSmallCache;\n\tmutable QImage cornerBottomLargeCache;\n\tmutable QImage tailCache;\n};\n\n[[nodiscard]] std::unique_ptr<BubblePattern> PrepareBubblePattern(\n\tnot_null<const style::palette*> st);\nvoid FinishBubblePatternOnMain(not_null<BubblePattern*> pattern);\n\nstruct SimpleBubble {\n\tnot_null<const ChatStyle*> st;\n\tQRect geometry;\n\tconst BubblePattern *pattern = nullptr;\n\tQRect patternViewport;\n\tint outerWidth = 0;\n\tbool selected = false;\n\tbool shadowed = true;\n\tbool outbg = false;\n\tBubbleRounding rounding;\n};\n\nstruct ComplexBubble {\n\tSimpleBubble simple;\n\tconst std::vector<BubbleSelectionInterval> &selection;\n};\n\nvoid PaintBubble(QPainter &p, const SimpleBubble &args);\nvoid PaintBubble(QPainter &p, const ComplexBubble &args);\n\nvoid PaintPatternBubblePart(\n\tQPainter &p,\n\tconst QRect &viewport,\n\tconst QPixmap &pixmap,\n\tconst QRect &target);\n\nvoid PaintPatternBubblePart(\n\tQPainter &p,\n\tconst QRect &viewport,\n\tconst QPixmap &pixmap,\n\tconst QRect &target,\n\tconst QImage &mask,\n\tQImage &cache);\n\nvoid PaintPatternBubblePart(\n\tQPainter &p,\n\tconst QRect &viewport,\n\tconst QPixmap &pixmap,\n\tconst QRect &target,\n\tFn<void(QPainter&)> paintContent,\n\tQImage &cache);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/more_chats_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/more_chats_bar.h\"\n\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/painter.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_window.h\" // st::columnMinimalWidthLeft\n\nnamespace Ui {\n\nMoreChatsBar::MoreChatsBar(\n\tnot_null<QWidget*> parent,\n\trpl::producer<MoreChatsBarContent> content)\n: _wrap(parent, object_ptr<RpWidget>(parent))\n, _inner(_wrap.entity())\n, _shadow(std::make_unique<PlainShadow>(_wrap.parentWidget()))\n, _close(_inner.get(), st::moreChatsBarClose) {\n\t_wrap.hide(anim::type::instant);\n\t_shadow->hide();\n\n\t_wrap.entity()->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tQPainter(_wrap.entity()).fillRect(clip, st::historyPinnedBg);\n\t}, lifetime());\n\t_wrap.setAttribute(Qt::WA_OpaquePaintEvent);\n\n\tauto copy = std::move(\n\t\tcontent\n\t) | rpl::start_spawning(_wrap.lifetime());\n\n\trpl::duplicate(\n\t\tcopy\n\t) | rpl::on_next([=](MoreChatsBarContent &&content) {\n\t\t_content = content;\n\t\tif (_content.count > 0) {\n\t\t\t_text.setText(\n\t\t\t\tst::defaultMessageBar.title,\n\t\t\t\ttr::lng_filters_bar_you_can(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\t_content.count),\n\t\t\t\tUi::NameTextOptions());\n\t\t\t_status.setText(\n\t\t\t\tst::defaultMessageBar.text,\n\t\t\t\ttr::lng_filters_bar_view(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\t_content.count),\n\t\t\t\tUi::NameTextOptions());\n\t\t}\n\t\t_inner->update();\n\t}, lifetime());\n\n\tstd::move(\n\t\tcopy\n\t) | rpl::map([=](const MoreChatsBarContent &content) {\n\t\treturn !content.count;\n\t}) | rpl::on_next_done([=](bool hidden) {\n\t\t_shouldBeShown = !hidden;\n\t\tif (!_forceHidden) {\n\t\t\t_wrap.toggle(_shouldBeShown, anim::type::normal);\n\t\t}\n\t}, [=] {\n\t\t_forceHidden = true;\n\t\t_wrap.toggle(false, anim::type::normal);\n\t}, lifetime());\n\n\tsetupInner();\n}\n\nMoreChatsBar::~MoreChatsBar() = default;\n\nvoid MoreChatsBar::setupInner() {\n\t_inner->resize(0, st::moreChatsBarHeight);\n\t_inner->paintRequest(\n\t) | rpl::on_next([=](QRect rect) {\n\t\tauto p = Painter(_inner);\n\t\tpaint(p);\n\t}, _inner->lifetime());\n\n\t// Clicks.\n\t_inner->setCursor(style::cur_pointer);\n\t_inner->events(\n\t) | rpl::filter([=](not_null<QEvent*> event) {\n\t\treturn (event->type() == QEvent::MouseButtonPress);\n\t}) | rpl::map([=] {\n\t\treturn _inner->events(\n\t\t) | rpl::filter([=](not_null<QEvent*> event) {\n\t\t\treturn (event->type() == QEvent::MouseButtonRelease);\n\t\t}) | rpl::take(1) | rpl::filter([=](not_null<QEvent*> event) {\n\t\t\treturn _inner->rect().contains(\n\t\t\t\tstatic_cast<QMouseEvent*>(event.get())->pos());\n\t\t});\n\t}) | rpl::flatten_latest(\n\t) | rpl::to_empty | rpl::start_to_stream(_barClicks, _inner->lifetime());\n\n\t_wrap.geometryValue(\n\t) | rpl::on_next([=](QRect rect) {\n\t\tupdateShadowGeometry(rect);\n\t\tupdateControlsGeometry(rect);\n\t}, _inner->lifetime());\n}\n\nvoid MoreChatsBar::paint(Painter &p) {\n\tp.fillRect(_inner->rect(), st::historyComposeAreaBg);\n\n\tconst auto width = std::max(\n\t\t_inner->width(),\n\t\tst::columnMinimalWidthLeft);\n\tconst auto available = width\n\t\t- st::moreChatsBarTextPosition.x()\n\t\t- st::moreChatsBarClose.width;\n\n\tp.setPen(st::defaultMessageBar.titleFg);\n\t_text.drawElided(\n\t\tp,\n\t\tst::moreChatsBarTextPosition.x(),\n\t\tst::moreChatsBarTextPosition.y(),\n\t\tavailable);\n\n\tp.setPen(st::defaultMessageBar.textFg);\n\t_status.drawElided(\n\t\tp,\n\t\tst::moreChatsBarStatusPosition.x(),\n\t\tst::moreChatsBarStatusPosition.y(),\n\t\tavailable);\n}\n\nvoid MoreChatsBar::updateControlsGeometry(QRect wrapGeometry) {\n\tconst auto hidden = _wrap.isHidden() || !wrapGeometry.height();\n\tif (_shadow->isHidden() != hidden) {\n\t\t_shadow->setVisible(!hidden);\n\t}\n\tconst auto width = std::max(\n\t\twrapGeometry.width(),\n\t\tst::columnMinimalWidthLeft);\n\t_close->move(width - _close->width(), 0);\n}\n\nvoid MoreChatsBar::setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess) {\n\t_shadowGeometryPostprocess = std::move(postprocess);\n\tupdateShadowGeometry(_wrap.geometry());\n}\n\nvoid MoreChatsBar::updateShadowGeometry(QRect wrapGeometry) {\n\tconst auto regular = QRect(\n\t\twrapGeometry.x(),\n\t\twrapGeometry.y() + wrapGeometry.height(),\n\t\twrapGeometry.width(),\n\t\tst::lineWidth);\n\t_shadow->setGeometry(_shadowGeometryPostprocess\n\t\t? _shadowGeometryPostprocess(regular)\n\t\t: regular);\n}\n\nvoid MoreChatsBar::show() {\n\tif (!_forceHidden) {\n\t\treturn;\n\t}\n\t_forceHidden = false;\n\tif (_shouldBeShown) {\n\t\t_wrap.show(anim::type::instant);\n\t\t_shadow->show();\n\t}\n}\n\nvoid MoreChatsBar::hide() {\n\tif (_forceHidden) {\n\t\treturn;\n\t}\n\t_forceHidden = true;\n\t_wrap.hide(anim::type::instant);\n\t_shadow->hide();\n}\n\nvoid MoreChatsBar::raise() {\n\t_wrap.raise();\n\t_shadow->raise();\n}\n\nvoid MoreChatsBar::finishAnimating() {\n\t_wrap.finishAnimating();\n}\n\nvoid MoreChatsBar::move(int x, int y) {\n\t_wrap.move(x, y);\n}\n\nvoid MoreChatsBar::resizeToWidth(int width) {\n\t_wrap.entity()->resizeToWidth(width);\n\t_inner->resizeToWidth(width);\n}\n\nint MoreChatsBar::height() const {\n\treturn !_forceHidden\n\t\t? _wrap.height()\n\t\t: _shouldBeShown\n\t\t? st::moreChatsBarHeight\n\t\t: 0;\n}\n\nrpl::producer<int> MoreChatsBar::heightValue() const {\n\treturn _wrap.heightValue();\n}\n\nrpl::producer<> MoreChatsBar::barClicks() const {\n\treturn _barClicks.events();\n}\n\nrpl::producer<> MoreChatsBar::closeClicks() const {\n\treturn _close->clicks() | rpl::to_empty;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/more_chats_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/text/text.h\"\n#include \"base/object_ptr.h\"\n#include \"base/timer.h\"\n\nclass Painter;\n\nnamespace Ui {\n\nclass PlainShadow;\nclass IconButton;\n\nstruct MoreChatsBarContent {\n\tint count = 0;\n};\n\nclass MoreChatsBar final {\npublic:\n\tMoreChatsBar(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<MoreChatsBarContent> content);\n\t~MoreChatsBar();\n\n\t[[nodiscard]] not_null<RpWidget*> wrap() {\n\t\treturn &_wrap;\n\t}\n\n\tvoid show();\n\tvoid hide();\n\tvoid raise();\n\tvoid finishAnimating();\n\n\tvoid setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess);\n\n\tvoid move(int x, int y);\n\tvoid resizeToWidth(int width);\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] rpl::producer<int> heightValue() const;\n\t[[nodiscard]] rpl::producer<> barClicks() const;\n\t[[nodiscard]] rpl::producer<> closeClicks() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _wrap.lifetime();\n\t}\n\nprivate:\n\tvoid updateShadowGeometry(QRect wrapGeometry);\n\tvoid updateControlsGeometry(QRect wrapGeometry);\n\tvoid setupInner();\n\tvoid paint(Painter &p);\n\n\tSlideWrap<> _wrap;\n\tnot_null<RpWidget*> _inner;\n\tstd::unique_ptr<PlainShadow> _shadow;\n\tobject_ptr<IconButton> _close;\n\trpl::event_stream<> _barClicks;\n\tFn<QRect(QRect)> _shadowGeometryPostprocess;\n\tbool _shouldBeShown = false;\n\tbool _forceHidden = false;\n\n\tMoreChatsBarContent _content;\n\tUi::Text::String _text;\n\tUi::Text::String _status;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/pinned_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/pinned_bar.h\"\n\n#include \"ui/chat/message_bar.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/palette.h\"\n\n#include <QtGui/QtEvents>\n\nnamespace Ui {\n\nPinnedBar::PinnedBar(\n\tnot_null<QWidget*> parent,\n\tFn<bool()> customEmojiPaused,\n\trpl::producer<> customEmojiPausedChanges)\n: _wrap(parent, object_ptr<RpWidget>(parent))\n, _shadow(std::make_unique<PlainShadow>(_wrap.parentWidget()))\n, _customEmojiPaused(std::move(customEmojiPaused)) {\n\t_wrap.hide(anim::type::instant);\n\t_shadow->hide();\n\n\t_shadow->showOn(rpl::combine(\n\t\t_wrap.shownValue(),\n\t\t_wrap.heightValue(),\n\t\trpl::mappers::_1 && rpl::mappers::_2 > 0\n\t) | rpl::filter([=](bool shown) {\n\t\treturn (shown == _shadow->isHidden());\n\t}));\n\n\t_wrap.entity()->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tQPainter(_wrap.entity()).fillRect(clip, st::historyPinnedBg);\n\t}, lifetime());\n\t_wrap.setAttribute(Qt::WA_OpaquePaintEvent);\n\n\tif (customEmojiPausedChanges) {\n\t\tstd::move(\n\t\t\tcustomEmojiPausedChanges\n\t\t) | rpl::on_next([=] {\n\t\t\t_wrap.entity()->update();\n\t\t}, lifetime());\n\t}\n}\n\nPinnedBar::~PinnedBar() {\n\t_right.button.destroy();\n}\n\nvoid PinnedBar::setContent(rpl::producer<Ui::MessageBarContent> content) {\n\t_contentLifetime.destroy();\n\n\tauto copy = std::move(\n\t\tcontent\n\t) | rpl::start_spawning(_contentLifetime);\n\n\trpl::duplicate(\n\t\tcopy\n\t) | rpl::filter([=](const MessageBarContent &content) {\n\t\treturn !content.title.isEmpty() || !content.text.text.isEmpty();\n\t}) | rpl::on_next([=](MessageBarContent &&content) {\n\t\tconst auto creating = !_bar;\n\t\tif (creating) {\n\t\t\tcreateControls();\n\t\t}\n\n\t\t// In most cases the new right button should arrive\n\t\t// before we want to get its width.\n\t\tconst auto right = _right.button ? _right.button->width() : 0;\n\t\tcontent.margins = { 0, 0, right, 0 };\n\n\t\t_bar->set(std::move(content));\n\t\tif (creating) {\n\t\t\t_bar->finishAnimating();\n\t\t}\n\t}, _contentLifetime);\n\n\tstd::move(\n\t\tcopy\n\t) | rpl::map([=](const MessageBarContent &content) {\n\t\treturn content.title.isEmpty() || content.text.text.isEmpty();\n\t}) | rpl::on_next_done([=](bool hidden) {\n\t\t_shouldBeShown = !hidden;\n\t\tif (!_forceHidden) {\n\t\t\t_wrap.toggle(_shouldBeShown, anim::type::normal);\n\t\t} else if (!_shouldBeShown) {\n\t\t\t_bar = nullptr;\n\t\t}\n\t}, [=] {\n\t\t_forceHidden = true;\n\t\t_wrap.toggle(false, anim::type::normal);\n\t}, _contentLifetime);\n}\n\nvoid PinnedBar::setRightButton(object_ptr<Ui::RpWidget> button) {\n\tconst auto hasPrevious = (_right.button != nullptr);\n\tif (auto previous = _right.button.release()) {\n\t\tusing Unique = base::unique_qptr<Ui::FadeWrapScaled<Ui::RpWidget>>;\n\t\t_right.previousButtonLifetime = previous->shownValue(\n\t\t) | rpl::filter(!rpl::mappers::_1) | rpl::on_next([=] {\n\t\t\t_right.previousButtonLifetime.destroy();\n\t\t});\n\t\tprevious->hide(anim::type::normal);\n\t\t_right.previousButtonLifetime.make_state<Unique>(Unique{ previous });\n\t}\n\t_right.button.create(_wrap.entity(), std::move(button));\n\tif (_right.button) {\n\t\t_right.button->setParent(_wrap.entity());\n\t\tif (hasPrevious) {\n\t\t\t_right.button->setDuration(st::defaultMessageBar.duration);\n\t\t\t_right.button->show(anim::type::normal);\n\t\t} else {\n\t\t\t_right.button->setDuration(0);\n\t\t\t_right.button->show(anim::type::instant);\n\t\t}\n\t}\n\tif (_bar) {\n\t\tupdateControlsGeometry(_wrap.geometry());\n\t}\n}\n\nvoid PinnedBar::updateControlsGeometry(QRect wrapGeometry) {\n\t_bar->widget()->resizeToWidth(wrapGeometry.width());\n\tif (_right.button) {\n\t\t_right.button->moveToRight(0, 0);\n\t}\n}\n\nvoid PinnedBar::setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess) {\n\t_shadowGeometryPostprocess = std::move(postprocess);\n\tupdateShadowGeometry(_wrap.geometry());\n}\n\nvoid PinnedBar::updateShadowGeometry(QRect wrapGeometry) {\n\tconst auto regular = QRect(\n\t\twrapGeometry.x(),\n\t\twrapGeometry.y() + wrapGeometry.height(),\n\t\twrapGeometry.width(),\n\t\tst::lineWidth);\n\t_shadow->setGeometry(_shadowGeometryPostprocess\n\t\t? _shadowGeometryPostprocess(regular)\n\t\t: regular);\n}\n\nvoid PinnedBar::createControls() {\n\tExpects(!_bar);\n\n\t_bar = std::make_unique<MessageBar>(\n\t\t_wrap.entity(),\n\t\tst::defaultMessageBar,\n\t\t_customEmojiPaused);\n\tif (_right.button) {\n\t\t_right.button->raise();\n\t}\n\n\t// Clicks.\n\t_bar->widget()->setCursor(style::cur_pointer);\n\t_bar->widget()->events(\n\t) | rpl::filter([=](not_null<QEvent*> event) {\n\t\treturn (event->type() == QEvent::MouseButtonPress)\n\t\t\t&& (static_cast<QMouseEvent*>(event.get())->button()\n\t\t\t\t\t== Qt::LeftButton);\n\t}) | rpl::map([=] {\n\t\treturn _bar->widget()->events(\n\t\t) | rpl::filter([](not_null<QEvent*> event) {\n\t\t\treturn (event->type() == QEvent::MouseButtonRelease);\n\t\t}) | rpl::take(1) | rpl::filter([=](not_null<QEvent*> event) {\n\t\t\treturn _bar->widget()->rect().contains(\n\t\t\t\tstatic_cast<QMouseEvent*>(event.get())->pos());\n\t\t});\n\t}) | rpl::flatten_latest(\n\t) | rpl::to_empty | rpl::start_to_stream(\n\t\t_barClicks,\n\t\t_bar->widget()->lifetime());\n\n\t_bar->widget()->move(0, 0);\n\t_bar->widget()->show();\n\t_wrap.entity()->resize(\n\t\t_wrap.entity()->width(),\n\t\t_bar->widget()->height());\n\n\t_wrap.geometryValue(\n\t) | rpl::on_next([=](QRect rect) {\n\t\tupdateShadowGeometry(rect);\n\t\tupdateControlsGeometry(rect);\n\t}, _bar->widget()->lifetime());\n\n\t_wrap.shownValue(\n\t) | rpl::skip(\n\t\t1\n\t) | rpl::filter([=](bool shown) {\n\t\treturn !shown && !_forceHidden;\n\t}) | rpl::on_next([=] {\n\t\t_bar = nullptr;\n\t}, _bar->widget()->lifetime());\n\n\tEnsures(_bar != nullptr);\n}\n\nvoid PinnedBar::show() {\n\tif (!_forceHidden) {\n\t\treturn;\n\t}\n\t_forceHidden = false;\n\tif (_shouldBeShown) {\n\t\t_wrap.show(anim::type::instant);\n\t} else if (!_wrap.isHidden() && !_wrap.animating()) {\n\t\t_wrap.hide(anim::type::instant);\n\t}\n}\n\nvoid PinnedBar::hide() {\n\tif (_forceHidden) {\n\t\treturn;\n\t}\n\t_forceHidden = true;\n\t_wrap.hide(anim::type::instant);\n}\n\nvoid PinnedBar::raise() {\n\t_wrap.raise();\n\t_shadow->raise();\n}\n\nvoid PinnedBar::customEmojiRepaint() {\n\tif (_bar) {\n\t\t_bar->customEmojiRepaint();\n\t}\n}\n\nvoid PinnedBar::finishAnimating() {\n\t_wrap.finishAnimating();\n}\n\nvoid PinnedBar::move(int x, int y) {\n\t_wrap.move(x, y);\n}\n\nvoid PinnedBar::resizeToWidth(int width) {\n\t_wrap.entity()->resizeToWidth(width);\n\tif (!_wrap.width()) {\n\t\t_wrap.resizeToWidth(width);\n\t}\n}\n\nint PinnedBar::height() const {\n\treturn !_forceHidden\n\t\t? _wrap.height()\n\t\t: _shouldBeShown\n\t\t? st::historyReplyHeight\n\t\t: 0;\n}\n\nrpl::producer<int> PinnedBar::heightValue() const {\n\treturn _wrap.heightValue();\n}\n\nrpl::producer<> PinnedBar::barClicks() const {\n\treturn _barClicks.events();\n}\n\nrpl::producer<> PinnedBar::contextMenuRequested() const {\n\treturn _wrap.entity()->paintRequest(\n\t) | rpl::filter([=] {\n\t\treturn _bar && _bar->widget();\n\t}) | rpl::map([=] {\n\t\treturn _bar->widget()->events(\n\t\t) | rpl::filter([](not_null<QEvent*> event) {\n\t\t\treturn (event->type() == QEvent::ContextMenu);\n\t\t}) | rpl::to_empty;\n\t}) | rpl::flatten_latest();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/pinned_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/wrap/slide_wrap.h\"\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\n\nstruct MessageBarContent;\ntemplate <typename Widget>\nclass FadeWrapScaled;\nclass MessageBar;\nclass IconButton;\nclass PlainShadow;\nclass RpWidget;\n\nclass PinnedBar final {\npublic:\n\tPinnedBar(\n\t\tnot_null<QWidget*> parent,\n\t\tFn<bool()> customEmojiPaused,\n\t\trpl::producer<> customEmojiPausedChanges);\n\t~PinnedBar();\n\n\tvoid show();\n\tvoid hide();\n\tvoid raise();\n\tvoid customEmojiRepaint();\n\tvoid finishAnimating();\n\n\tvoid setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess);\n\n\tvoid setContent(rpl::producer<Ui::MessageBarContent> content);\n\tvoid setRightButton(object_ptr<Ui::RpWidget> button);\n\n\tvoid move(int x, int y);\n\tvoid resizeToWidth(int width);\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] rpl::producer<int> heightValue() const;\n\t[[nodiscard]] rpl::producer<> barClicks() const;\n\t[[nodiscard]] rpl::producer<> contextMenuRequested() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _wrap.lifetime();\n\t}\n\nprivate:\n\tusing RightButton = object_ptr<Ui::FadeWrapScaled<Ui::RpWidget>>;\n\tvoid createControls();\n\tvoid updateShadowGeometry(QRect wrapGeometry);\n\tvoid updateControlsGeometry(QRect wrapGeometry);\n\n\tUi::SlideWrap<> _wrap;\n\tstd::unique_ptr<Ui::MessageBar> _bar;\n\n\tstruct {\n\t\tRightButton button = { nullptr };\n\t\trpl::lifetime previousButtonLifetime;\n\t} _right;\n\n\tstd::unique_ptr<Ui::PlainShadow> _shadow;\n\tFn<bool()> _customEmojiPaused;\n\trpl::event_stream<> _barClicks;\n\trpl::event_stream<> _contextMenuRequested;\n\tFn<QRect(QRect)> _shadowGeometryPostprocess;\n\tbool _shouldBeShown = false;\n\tbool _forceHidden = false;\n\n\trpl::lifetime _contentLifetime;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/requests_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/requests_bar.h\"\n\n#include \"ui/chat/group_call_userpics.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/painter.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_info.h\" // st::topBarArrowPadding, like TopBarWidget.\n#include \"styles/style_window.h\" // st::columnMinimalWidthLeft\n#include \"styles/palette.h\"\n\n#include <QtGui/QtEvents>\n\nnamespace Ui {\n\nRequestsBar::RequestsBar(\n\tnot_null<QWidget*> parent,\n\trpl::producer<RequestsBarContent> content)\n: _wrap(parent, object_ptr<RpWidget>(parent))\n, _inner(_wrap.entity())\n, _shadow(std::make_unique<PlainShadow>(_wrap.parentWidget()))\n, _userpics(std::make_unique<GroupCallUserpics>(\n\t\tst::historyRequestsUserpics,\n\t\trpl::single(false),\n\t\t[=] { _inner->update(); })) {\n\t_wrap.hide(anim::type::instant);\n\t_shadow->hide();\n\n\t_wrap.entity()->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tQPainter(_wrap.entity()).fillRect(clip, st::historyPinnedBg);\n\t}, lifetime());\n\t_wrap.setAttribute(Qt::WA_OpaquePaintEvent);\n\n\tauto copy = std::move(\n\t\tcontent\n\t) | rpl::start_spawning(_wrap.lifetime());\n\n\trpl::duplicate(\n\t\tcopy\n\t) | rpl::on_next([=](RequestsBarContent &&content) {\n\t\t_content = content;\n\t\tif (_content.count > 0) {\n\t\t\tif (_content.count == 1 && !_content.nameFull.isEmpty()) {\n\t\t\t\t_textFull.setText(\n\t\t\t\t\tst::defaultMessageBar.title,\n\t\t\t\t\ttr::lng_group_requests_pending_user(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\t_content.nameFull),\n\t\t\t\t\tUi::NameTextOptions());\n\t\t\t\t_textShort.setText(\n\t\t\t\t\tst::defaultMessageBar.title,\n\t\t\t\t\ttr::lng_group_requests_pending_user(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_user,\n\t\t\t\t\t\t_content.nameShort),\n\t\t\t\t\tUi::NameTextOptions());\n\t\t\t} else {\n\t\t\t\t_textShort.setText(\n\t\t\t\t\tst::defaultMessageBar.title,\n\t\t\t\t\ttr::lng_group_requests_pending(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count_decimal,\n\t\t\t\t\t\t_content.count),\n\t\t\t\t\tUi::NameTextOptions());\n\t\t\t\t_textFull.clear();\n\t\t\t}\n\t\t}\n\t\t_userpics->update(_content.users, !_wrap.isHidden());\n\t\t_inner->update();\n\t}, lifetime());\n\n\tstd::move(\n\t\tcopy\n\t) | rpl::map([=](const RequestsBarContent &content) {\n\t\treturn !content.count;\n\t}) | rpl::on_next_done([=](bool hidden) {\n\t\t_shouldBeShown = !hidden;\n\t\tif (!_forceHidden) {\n\t\t\t_wrap.toggle(_shouldBeShown, anim::type::normal);\n\t\t}\n\t}, [=] {\n\t\t_forceHidden = true;\n\t\t_wrap.toggle(false, anim::type::normal);\n\t}, lifetime());\n\n\t_userpics->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_userpicsWidth = width;\n\t}, lifetime());\n\n\tsetupInner();\n}\n\nRequestsBar::~RequestsBar() = default;\n\nvoid RequestsBar::setupInner() {\n\t_inner->resize(0, st::historyRequestsHeight);\n\t_inner->paintRequest(\n\t) | rpl::on_next([=](QRect rect) {\n\t\tauto p = Painter(_inner);\n\t\tpaint(p);\n\t}, _inner->lifetime());\n\n\t// Clicks.\n\t_inner->setCursor(style::cur_pointer);\n\t_inner->events(\n\t) | rpl::filter([=](not_null<QEvent*> event) {\n\t\treturn (event->type() == QEvent::MouseButtonPress);\n\t}) | rpl::map([=] {\n\t\treturn _inner->events(\n\t\t) | rpl::filter([=](not_null<QEvent*> event) {\n\t\t\treturn (event->type() == QEvent::MouseButtonRelease);\n\t\t}) | rpl::take(1) | rpl::filter([=](not_null<QEvent*> event) {\n\t\t\treturn _inner->rect().contains(\n\t\t\t\tstatic_cast<QMouseEvent*>(event.get())->pos());\n\t\t});\n\t}) | rpl::flatten_latest(\n\t) | rpl::to_empty | rpl::start_to_stream(_barClicks, _inner->lifetime());\n\n\t_wrap.geometryValue(\n\t) | rpl::on_next([=](QRect rect) {\n\t\tupdateShadowGeometry(rect);\n\t\tupdateControlsGeometry(rect);\n\t}, _inner->lifetime());\n}\n\nvoid RequestsBar::paint(Painter &p) {\n\tp.fillRect(_inner->rect(), st::historyComposeAreaBg);\n\n\tconst auto userpicsSize = st::historyRequestsUserpics.size;\n\tconst auto userpicsTop = st::lineWidth + (st::historyRequestsHeight\n\t\t- st::lineWidth\n\t\t- userpicsSize) / 2;\n\tconst auto userpicsLeft = userpicsTop * 2;\n\tconst auto textTop = st::lineWidth + (st::historyRequestsHeight\n\t\t- st::lineWidth\n\t\t- st::semiboldFont->height) / 2;\n\tconst auto width = _inner->width();\n\tconst auto &font = st::defaultMessageBar.title.font;\n\tp.setPen(st::defaultMessageBar.titleFg);\n\tp.setFont(font);\n\n\tif (width >= st::columnMinimalWidthLeft / 2) {\n\t\tconst auto textLeft = userpicsLeft + _userpicsWidth + userpicsLeft;\n\t\tconst auto available = width - textLeft - userpicsLeft;\n\t\tif (_textFull.isEmpty() || available < _textFull.maxWidth()) {\n\t\t\t_textShort.drawElided(p, textLeft, textTop, available);\n\t\t} else {\n\t\t\t_textFull.drawElided(p, textLeft, textTop, available);\n\t\t}\n\t}\n\n\t// Skip shadow of the bar above.\n\t_userpics->paint(p, userpicsLeft, userpicsTop, userpicsSize);\n}\n\nvoid RequestsBar::updateControlsGeometry(QRect wrapGeometry) {\n\tconst auto hidden = _wrap.isHidden() || !wrapGeometry.height();\n\tif (_shadow->isHidden() != hidden) {\n\t\t_shadow->setVisible(!hidden);\n\t}\n}\n\nvoid RequestsBar::setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess) {\n\t_shadowGeometryPostprocess = std::move(postprocess);\n\tupdateShadowGeometry(_wrap.geometry());\n}\n\nvoid RequestsBar::updateShadowGeometry(QRect wrapGeometry) {\n\tconst auto regular = QRect(\n\t\twrapGeometry.x(),\n\t\twrapGeometry.y() + wrapGeometry.height(),\n\t\twrapGeometry.width(),\n\t\tst::lineWidth);\n\t_shadow->setGeometry(_shadowGeometryPostprocess\n\t\t? _shadowGeometryPostprocess(regular)\n\t\t: regular);\n}\n\nvoid RequestsBar::show() {\n\tif (!_forceHidden) {\n\t\treturn;\n\t}\n\t_forceHidden = false;\n\tif (_shouldBeShown) {\n\t\t_wrap.show(anim::type::instant);\n\t\t_shadow->show();\n\t}\n}\n\nvoid RequestsBar::hide() {\n\tif (_forceHidden) {\n\t\treturn;\n\t}\n\t_forceHidden = true;\n\t_wrap.hide(anim::type::instant);\n\t_shadow->hide();\n}\n\nvoid RequestsBar::raise() {\n\t_wrap.raise();\n\t_shadow->raise();\n}\n\nvoid RequestsBar::finishAnimating() {\n\t_wrap.finishAnimating();\n}\n\nvoid RequestsBar::move(int x, int y) {\n\t_wrap.move(x, y);\n}\n\nvoid RequestsBar::resizeToWidth(int width) {\n\t_wrap.entity()->resizeToWidth(width);\n\t_inner->resizeToWidth(width);\n}\n\nint RequestsBar::height() const {\n\treturn !_forceHidden\n\t\t? _wrap.height()\n\t\t: _shouldBeShown\n\t\t? st::historyRequestsHeight\n\t\t: 0;\n}\n\nrpl::producer<int> RequestsBar::heightValue() const {\n\treturn _wrap.heightValue();\n}\n\nrpl::producer<> RequestsBar::barClicks() const {\n\treturn _barClicks.events();\n}\n\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/requests_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/text/text.h\"\n#include \"base/object_ptr.h\"\n#include \"base/timer.h\"\n\nclass Painter;\n\nnamespace Ui {\n\nclass PlainShadow;\nstruct GroupCallUser;\nclass GroupCallUserpics;\n\nstruct RequestsBarContent {\n\tstd::vector<GroupCallUser> users;\n\tQString nameFull;\n\tQString nameShort;\n\tint count = 0;\n\tbool isGroup = false;\n};\n\nclass RequestsBar final {\npublic:\n\tRequestsBar(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<RequestsBarContent> content);\n\t~RequestsBar();\n\n\tvoid show();\n\tvoid hide();\n\tvoid raise();\n\tvoid finishAnimating();\n\n\tvoid setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess);\n\n\tvoid move(int x, int y);\n\tvoid resizeToWidth(int width);\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] rpl::producer<int> heightValue() const;\n\t[[nodiscard]] rpl::producer<> barClicks() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _wrap.lifetime();\n\t}\n\nprivate:\n\tusing User = GroupCallUser;\n\n\tvoid updateShadowGeometry(QRect wrapGeometry);\n\tvoid updateControlsGeometry(QRect wrapGeometry);\n\tvoid updateUserpics();\n\tvoid setupInner();\n\tvoid paint(Painter &p);\n\n\tSlideWrap<> _wrap;\n\tnot_null<RpWidget*> _inner;\n\tstd::unique_ptr<PlainShadow> _shadow;\n\trpl::event_stream<> _barClicks;\n\tFn<QRect(QRect)> _shadowGeometryPostprocess;\n\tbool _shouldBeShown = false;\n\tbool _forceHidden = false;\n\n\tRequestsBarContent _content;\n\tstd::unique_ptr<GroupCallUserpics> _userpics;\n\tint _userpicsWidth = 0;\n\tUi::Text::String _textShort;\n\tUi::Text::String _textFull;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/sponsored_message_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/chat/sponsored_message_bar.h\"\n\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/ui_integration.h\" // TextContext\n#include \"data/components/sponsored_messages.h\"\n#include \"data/data_session.h\"\n#include \"history/history_item_helpers.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"menu/menu_sponsored.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/rect.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"window/section_widget.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Ui {\nnamespace {\n\nstruct Colors final {\n\tQColor bg;\n\tQColor fg;\n};\n\nusing ColorFactory = Fn<Colors()>;\n\nclass BadgeButton final : public Ui::RippleButton {\npublic:\n\tBadgeButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\ttr::phrase<> text,\n\t\tColorFactory cache)\n\t: Ui::RippleButton(parent, st::defaultRippleAnimation) {\n\t\ttext(\n\t\t) | rpl::on_next([this](const QString &t) {\n\t\t\tconst auto height = st::stickersHeaderBadgeFont->height;\n\t\t\tresize(\n\t\t\t\tst::stickersHeaderBadgeFont->width(t) + height,\n\t\t\t\theight);\n\t\t\tupdate();\n\t\t}, lifetime());\n\t\tpaintRequest() | rpl::on_next([this, cache, text] {\n\t\t\tauto p = QPainter(this);\n\t\t\tconst auto colors = cache();\n\t\t\tconst auto r = rect();\n\t\t\tconst auto rippleColor = anim::with_alpha(colors.fg, .15);\n\t\t\tUi::RippleButton::paintRipple(\n\t\t\t\tp,\n\t\t\t\tQPoint(),\n\t\t\t\t&rippleColor);\n\t\t\tp.setBrush(colors.bg);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.drawRoundedRect(r, r.height() / 2, r.height() / 2);\n\t\t\tp.setFont(st::stickersHeaderBadgeFont);\n\t\t\tp.setPen(colors.fg);\n\t\t\tp.drawText(r, text(tr::now), style::al_center);\n\t\t}, lifetime());\n\t}\n\n\tQImage prepareRippleMask() const override {\n\t\treturn Ui::RippleAnimation::RoundRectMask(size(), height() / 2);\n\t}\n\n};\n\n[[nodiscard]] Window::SessionController *FindSessionController(\n\t\tnot_null<RpWidget*> widget) {\n\tconst auto window = Core::App().findWindow(widget);\n\treturn window ? window->sessionController() : nullptr;\n}\n\n[[nodiscard]] ColorFactory GenerateReplyColorCallback(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<RpWidget*> widget,\n\t\tFullMsgId fullId,\n\t\tint colorIndex) {\n\tconst auto peer = controller->session().data().peer(fullId.peer);\n\tstruct State final {\n\t\tstd::shared_ptr<Ui::ChatTheme> theme;\n\t};\n\tconst auto state = widget->lifetime().make_state<State>();\n\tWindow::ChatThemeValueFromPeer(\n\t\tcontroller,\n\t\tpeer\n\t) | rpl::on_next([=](std::shared_ptr<Ui::ChatTheme> &&theme) {\n\t\tstate->theme = std::move(theme);\n\t}, widget->lifetime());\n\n\treturn [=]() -> Colors {\n\t\tif (!state->theme) {\n\t\t\treturn {\n\t\t\t\tanim::with_alpha(st::windowBgActive->c, .15),\n\t\t\t\tst::windowActiveTextFg->c,\n\t\t\t};\n\t\t}\n\t\tconst auto context = controller->preparePaintContext({\n\t\t\t.theme = state->theme.get(),\n\t\t});\n\t\tconst auto selected = false;\n\t\tconst auto cache = context.st->coloredReplyCache(\n\t\t\tselected,\n\t\t\tcolorIndex);\n\t\treturn { cache->bg, cache->icon };\n\t};\n}\n\n[[nodiscard]] ColorFactory GenerateReplyColorCallback(\n\t\tnot_null<RpWidget*> widget,\n\t\tFullMsgId fullId,\n\t\tint colorIndex) {\n\tif (const auto window = FindSessionController(widget)) {\n\t\treturn GenerateReplyColorCallback(window, widget, fullId, colorIndex);\n\t}\n\tconst auto window\n\t\t= widget->lifetime().make_state<Window::SessionController*>();\n\tconst auto callback = widget->lifetime().make_state<ColorFactory>();\n\treturn [=, color = colorIndex]() -> Colors {\n\t\tif (*callback) {\n\t\t\treturn (*callback)();\n\t\t}\n\t\t*window = FindSessionController(widget);\n\t\tif (const auto w = (*window)) {\n\t\t\t*callback = GenerateReplyColorCallback(w, widget, fullId, color);\n\t\t\treturn (*callback)();\n\t\t} else {\n\t\t\treturn {\n\t\t\t\tanim::with_alpha(st::windowBgActive->c, .15),\n\t\t\t\tst::windowActiveTextFg->c,\n\t\t\t};\n\t\t}\n\t};\n}\n\n} // namespace\n\nvoid FillSponsoredMessageBar(\n\t\tnot_null<RpWidget*> container,\n\t\tnot_null<Main::Session*> session,\n\t\tFullMsgId fullId,\n\t\tData::SponsoredFrom from,\n\t\tconst TextWithEntities &textWithEntities) {\n\tconst auto widget = CreateSimpleRectButton(\n\t\tcontainer,\n\t\tst::defaultRippleAnimationBgOver);\n\twidget->show();\n\tcontainer->sizeValue() | rpl::on_next([=](const QSize &s) {\n\t\twidget->resize(s);\n\t}, widget->lifetime());\n\twidget->setAcceptBoth();\n\n\twidget->addClickHandler([=](Qt::MouseButton button) {\n\t\tif (button == Qt::RightButton) {\n\t\t\tif (const auto controller = FindSessionController(widget)) {\n\t\t\t\t::Menu::ShowSponsored(widget, controller->uiShow(), fullId);\n\t\t\t}\n\t\t} else if (button == Qt::LeftButton) {\n\t\t\tsession->sponsoredMessages().clicked(fullId, false, false);\n\t\t\tUrlClickHandler::Open(from.link);\n\t\t}\n\t});\n\n\tstruct State final {\n\t\tUi::Text::String title;\n\t\tUi::Text::String contentTitle;\n\t\tUi::Text::String contentText;\n\t\trpl::variable<int> lastPaintedContentLineAmount = 0;\n\t\trpl::variable<int> lastPaintedContentTop = 0;\n\n\t\tstd::shared_ptr<Ui::DynamicImage> rightPhoto;\n\t\tQImage rightPhotoImage;\n\t};\n\tconst auto state = widget->lifetime().make_state<State>();\n\tconst auto &titleSt = st::semiboldTextStyle;\n\tconst auto &contentTitleSt = st::semiboldTextStyle;\n\tconst auto &contentTextSt = st::defaultTextStyle;\n\tstate->title.setText(\n\t\ttitleSt,\n\t\tfrom.isRecommended\n\t\t\t? tr::lng_recommended_message_title(tr::now)\n\t\t\t: tr::lng_sponsored_message_title(tr::now));\n\tstate->contentTitle.setText(contentTitleSt, from.title);\n\tstate->contentText.setMarkedText(\n\t\tcontentTextSt,\n\t\ttextWithEntities,\n\t\tkMarkupTextOptions,\n\t\tCore::TextContext({\n\t\t\t.session = session,\n\t\t\t.repaint = [=] { widget->update(); },\n\t\t}));\n\tconst auto hostedClick = [=](ClickHandlerPtr handler) {\n\t\treturn [=] {\n\t\t\tif (const auto controller = FindSessionController(widget)) {\n\t\t\t\tActivateClickHandler(widget, handler, {\n\t\t\t\t\t.other = QVariant::fromValue(ClickHandlerContext{\n\t\t\t\t\t\t.itemId = fullId,\n\t\t\t\t\t\t.sessionWindow = base::make_weak(controller),\n\t\t\t\t\t\t.show = controller->uiShow(),\n\t\t\t\t\t})\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\t};\n\tconst auto paused = [=]() -> Fn<bool()> {\n\t\tif (const auto c = FindSessionController(widget)) {\n\t\t\tusing Gif = Window::GifPauseReason;\n\t\t\treturn [=] { return c->isGifPausedAtLeastFor(Gif::Any); };\n\t\t}\n\t\treturn [] { return false; };\n\t};\n\tconst auto kLinesForPhoto = 3;\n\tconst auto rightPhotoSize = titleSt.font->ascent * kLinesForPhoto;\n\tconst auto rightPhotoPlaceholder = titleSt.font->height * kLinesForPhoto;\n\tconst auto hasRightPhoto = from.photoId > 0;\n\tif (hasRightPhoto) {\n\t\tstate->rightPhoto = Ui::MakePhotoThumbnail(\n\t\t\tsession->data().photo(from.photoId),\n\t\t\tfullId);\n\t\tconst auto callback = [=] {\n\t\t\tstate->rightPhotoImage = Images::Round(\n\t\t\t\tstate->rightPhoto->image(rightPhotoSize),\n\t\t\t\tImageRoundRadius::Small);\n\t\t\twidget->update();\n\t\t};\n\t\tstate->rightPhoto->subscribeToUpdates(callback);\n\t\tcallback();\n\t}\n\tconst auto rightHide = hasRightPhoto\n\t\t? nullptr\n\t\t: Ui::CreateChild<Ui::IconButton>(\n\t\t\tcontainer,\n\t\t\tst::dialogsCancelSearchInPeer);\n\tif (rightHide) {\n\t\tcontainer->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &s) {\n\t\t\trightHide->moveToRight(st::buttonRadius, st::lineWidth);\n\t\t}, rightHide->lifetime());\n\t\trightHide->setClickedCallback(\n\t\t\thostedClick(HideSponsoredClickHandler()));\n\t}\n\tconst auto badgeButton = Ui::CreateChild<BadgeButton>(\n\t\twidget,\n\t\tfrom.canReport\n\t\t\t? tr::lng_sponsored_message_revenue_button\n\t\t\t: tr::lng_sponsored_top_bar_hide,\n\t\tGenerateReplyColorCallback(\n\t\t\twidget,\n\t\t\tfullId,\n\t\t\tfrom.colorIndex ? from.colorIndex : 4/*blue*/));\n\tbadgeButton->setClickedCallback(\n\t\thostedClick(from.canReport\n\t\t\t? AboutSponsoredClickHandler()\n\t\t\t: HideSponsoredClickHandler()));\n\tbadgeButton->show();\n\n\tconst auto draw = [=](QPainter &p) {\n\t\tconst auto r = widget->rect();\n\t\tp.fillRect(r, st::historyPinnedBg);\n\t\twidget->paintRipple(p, 0, 0);\n\t\tconst auto leftPadding = st::msgReplyBarSkip + st::msgReplyBarSkip;\n\t\tconst auto rightPadding = st::msgReplyBarSkip;\n\t\tconst auto topPadding = st::msgReplyPadding.top();\n\t\tconst auto availableWidthNoPhoto = r.width()\n\t\t\t- leftPadding\n\t\t\t- rightPadding;\n\t\tconst auto availableWidth = availableWidthNoPhoto\n\t\t\t- (hasRightPhoto ? (rightPadding + rightPhotoSize) : 0)\n\t\t\t- (rightHide ? rightHide->width() : 0);\n\t\tconst auto titleRight = leftPadding\n\t\t\t+ state->title.maxWidth()\n\t\t\t+ titleSt.font->spacew * 2;\n\t\tconst auto hasSecondLineTitle = (titleRight\n\t\t\t> (availableWidth\n\t\t\t\t- state->contentTitle.maxWidth()\n\t\t\t\t- badgeButton->width()));\n\t\tp.setPen(st::windowActiveTextFg);\n\t\tstate->title.draw(p, {\n\t\t\t.position = QPoint(leftPadding, topPadding),\n\t\t\t.outerWidth = availableWidth,\n\t\t\t.availableWidth = availableWidth,\n\t\t});\n\t\tbadgeButton->moveToLeft(\n\t\t\thasSecondLineTitle\n\t\t\t\t? titleRight\n\t\t\t\t: std::min(\n\t\t\t\t\ttitleRight\n\t\t\t\t\t\t+ state->contentTitle.maxWidth()\n\t\t\t\t\t\t+ titleSt.font->spacew * 2,\n\t\t\t\t\tr.width()\n\t\t\t\t\t\t- (hasRightPhoto\n\t\t\t\t\t\t\t? (rightPadding + rightPhotoSize)\n\t\t\t\t\t\t\t: 0)\n\t\t\t\t\t\t- (rightHide ? rightHide->width() : 0)\n\t\t\t\t\t\t- rightPadding),\n\t\t\ttopPadding\n\t\t\t\t+ (titleSt.font->height - badgeButton->height()) / 2);\n\t\tp.setPen(st::windowFg);\n\t\t{\n\t\t\tconst auto left = hasSecondLineTitle ? leftPadding : titleRight;\n\t\t\tconst auto top = hasSecondLineTitle\n\t\t\t\t? (topPadding + titleSt.font->height)\n\t\t\t\t: topPadding;\n\t\t\tstate->contentTitle.draw(p, {\n\t\t\t\t.position = QPoint(left, top),\n\t\t\t\t.outerWidth = hasSecondLineTitle\n\t\t\t\t\t? availableWidth\n\t\t\t\t\t: (availableWidth - titleRight),\n\t\t\t\t.availableWidth = availableWidth,\n\t\t\t\t.elisionLines = 1,\n\t\t\t});\n\t\t}\n\t\t{\n\t\t\tconst auto left = leftPadding;\n\t\t\tconst auto top = hasSecondLineTitle\n\t\t\t\t? (topPadding\n\t\t\t\t\t+ titleSt.font->height\n\t\t\t\t\t+ contentTitleSt.font->height)\n\t\t\t\t: topPadding + titleSt.font->height;\n\t\t\tauto lastContentLineAmount = 0;\n\t\t\tconst auto lineHeight = contentTextSt.font->height;\n\t\t\tconst auto lineLayout = [&](int line) -> Ui::Text::LineGeometry {\n\t\t\t\tline++;\n\t\t\t\tlastContentLineAmount = line;\n\t\t\t\tconst auto diff = (st::sponsoredMessageBarMaxHeight)\n\t\t\t\t\t- line * lineHeight;\n\t\t\t\tif (diff < 3 * lineHeight) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\t.width = availableWidthNoPhoto,\n\t\t\t\t\t\t.elided = true,\n\t\t\t\t\t};\n\t\t\t\t} else if (diff < 2 * lineHeight) {\n\t\t\t\t\treturn {};\n\t\t\t\t}\n\t\t\t\tline += (hasSecondLineTitle ? 2 : 1)\n\t\t\t\t\t+ (hasRightPhoto ? 0 : 1);\n\t\t\t\treturn {\n\t\t\t\t\t.width = (line > kLinesForPhoto)\n\t\t\t\t\t\t? availableWidthNoPhoto\n\t\t\t\t\t\t: availableWidth,\n\t\t\t\t};\n\t\t\t};\n\t\t\tstate->contentText.draw(p, {\n\t\t\t\t.position = QPoint(left, top),\n\t\t\t\t.outerWidth = availableWidth,\n\t\t\t\t.availableWidth = availableWidth,\n\t\t\t\t.geometry = Ui::Text::GeometryDescriptor{\n\t\t\t\t\t.layout = std::move(lineLayout),\n\t\t\t\t},\n\t\t\t\t.pausedEmoji = On(PowerSaving::kEmojiChat) || paused(),\n\t\t\t\t.pausedSpoiler = On(PowerSaving::kChatSpoiler) || paused(),\n\t\t\t});\n\t\t\tstate->lastPaintedContentTop = top;\n\t\t\tstate->lastPaintedContentLineAmount = lastContentLineAmount;\n\t\t}\n\t\tif (hasRightPhoto) {\n\t\t\tp.drawImage(\n\t\t\t\tr.width() - rightPadding - rightPhotoSize,\n\t\t\t\ttopPadding + (rightPhotoPlaceholder - rightPhotoSize) / 2,\n\t\t\t\tstate->rightPhotoImage);\n\t\t}\n\t};\n\twidget->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(widget);\n\t\tdraw(p);\n\t}, widget->lifetime());\n\trpl::combine(\n\t\tstate->lastPaintedContentTop.value(),\n\t\tstate->lastPaintedContentLineAmount.value()\n\t) | rpl::distinct_until_changed() | rpl::on_next([=](\n\t\t\tint lastTop,\n\t\t\tint lastLines) {\n\t\tconst auto bottomPadding = st::msgReplyPadding.top();\n\t\tconst auto desiredHeight = lastTop\n\t\t\t+ (lastLines * contentTextSt.font->height)\n\t\t\t+ bottomPadding;\n\t\tconst auto minHeight = hasRightPhoto\n\t\t\t? (rightPhotoPlaceholder + bottomPadding * 2)\n\t\t\t: desiredHeight;\n\t\tcontainer->resize(\n\t\t\twidget->width(),\n\t\t\tstd::clamp(\n\t\t\t\tdesiredHeight,\n\t\t\t\tminHeight,\n\t\t\t\tst::sponsoredMessageBarMaxHeight));\n\t}, widget->lifetime());\n\t{ // Calculate a good size for container.\n\t\tauto dummy = QImage(1, 1, QImage::Format_ARGB32);\n\t\tauto p = QPainter(&dummy);\n\t\tdraw(p);\n\t}\n\n\t{\n\t\tconst auto top = Ui::CreateChild<PlainShadow>(widget);\n\t\tconst auto bottom = Ui::CreateChild<PlainShadow>(widget);\n\t\twidget->sizeValue() | rpl::on_next([=] (const QSize &s) {\n\t\t\ttop->show();\n\t\t\ttop->raise();\n\t\t\ttop->resizeToWidth(s.width());\n\t\t\tbottom->show();\n\t\t\tbottom->raise();\n\t\t\tbottom->resizeToWidth(s.width());\n\t\t\tbottom->moveToLeft(0, s.height() - bottom->height());\n\t\t}, top->lifetime());\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/chat/sponsored_message_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nstruct SponsoredFrom;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\n\nclass RpWidget;\n\nvoid FillSponsoredMessageBar(\n\tnot_null<RpWidget*> container,\n\tnot_null<Main::Session*> session,\n\tFullMsgId fullId,\n\tData::SponsoredFrom from,\n\tconst TextWithEntities &textWithEntities);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/color_contrast.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/color_contrast.h\"\n\nnamespace Ui {\n\n// https://stackoverflow.com/a/9733420\nfloat64 CountContrast(const QColor &a, const QColor &b) {\n\tconst auto luminance = [](const QColor &c) {\n\t\tconst auto map = [](double value) {\n\t\t\treturn (value <= 0.03928)\n\t\t\t\t? (value / 12.92)\n\t\t\t\t: std::pow((value + 0.055) / 1.055, 2.4);\n\t\t};\n\t\treturn map(c.redF()) * 0.2126\n\t\t\t+ map(c.greenF()) * 0.7152\n\t\t\t+ map(c.blueF()) * 0.0722;\n\t};\n\tconst auto luminance1 = luminance(a);\n\tconst auto luminance2 = luminance(b);\n\tconst auto brightest = std::max(luminance1, luminance2);\n\tconst auto darkest = std::min(luminance1, luminance2);\n\treturn (brightest + 0.05) / (darkest + 0.05);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/color_contrast.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass QColor;\n\nnamespace Ui {\n\n[[nodiscard]] float64 CountContrast(const QColor &a, const QColor &b);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/color_indices.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\ncolorIndexRed: 0;\ncolorIndexOrange: 1;\ncolorIndexGreen: 2;\ncolorIndexSea: 3;\ncolorIndexBlue: 4;\ncolorIndexPurple: 5;\ncolorIndexPink: 6;\ncolorIndexYellow: 7;\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/color_int_conversion.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/color_int_conversion.h\"\n\nnamespace Ui {\n\nQColor ColorFromSerialized(quint32 serialized) {\n\treturn QColor(\n\t\tint((serialized >> 16) & 0xFFU),\n\t\tint((serialized >> 8) & 0xFFU),\n\t\tint(serialized & 0xFFU));\n}\n\nstd::optional<QColor> MaybeColorFromSerialized(quint32 serialized) {\n\treturn (serialized == quint32(-1))\n\t\t? std::nullopt\n\t\t: std::make_optional(ColorFromSerialized(serialized));\n}\n\nQColor Color32FromSerialized(quint32 serialized) {\n\treturn QColor(\n\t\tint(serialized & 0xFFU),\n\t\tint((serialized >> 8) & 0xFFU),\n\t\tint((serialized >> 16) & 0xFFU),\n\t\tint((serialized >> 24) & 0xFFU));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/color_int_conversion.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\n[[nodiscard]] QColor ColorFromSerialized(quint32 serialized);\n[[nodiscard]] std::optional<QColor> MaybeColorFromSerialized(\n\tquint32 serialized);\n[[nodiscard]] QColor Color32FromSerialized(quint32 serialized);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/button_labels.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/button_labels.h\"\n\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n\nnamespace Ui {\n\nvoid SetButtonTwoLabels(\n\t\tnot_null<Ui::RoundButton*> button,\n\t\trpl::producer<TextWithEntities> title,\n\t\trpl::producer<TextWithEntities> subtitle,\n\t\tconst style::FlatLabel &st,\n\t\tconst style::FlatLabel &subst,\n\t\tconst style::color *textFg) {\n\tSetButtonTwoLabels(\n\t\tbutton,\n\t\tbutton->st().textTop,\n\t\tstd::move(title),\n\t\tstd::move(subtitle),\n\t\tst,\n\t\tsubst,\n\t\ttextFg);\n}\n\nvoid SetButtonTwoLabels(\n\t\tnot_null<Ui::RpWidget*> button,\n\t\tint singleLineTextTop,\n\t\trpl::producer<TextWithEntities> title,\n\t\trpl::producer<TextWithEntities> subtitle,\n\t\tconst style::FlatLabel &st,\n\t\tconst style::FlatLabel &subst,\n\t\tconst style::color *textFg) {\n\tconst auto buttonTitle = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton,\n\t\tstd::move(title),\n\t\tst);\n\tbuttonTitle->show();\n\tconst auto buttonSubtitle = Ui::CreateChild<Ui::FlatLabel>(\n\t\tbutton,\n\t\trpl::duplicate(\n\t\t\tsubtitle\n\t\t) | rpl::filter([](const TextWithEntities &text) {\n\t\t\treturn !text.empty();\n\t\t}),\n\t\tsubst);\n\tbuttonSubtitle->setOpacity(0.6);\n\tif (textFg) {\n\t\tbuttonTitle->setTextColorOverride((*textFg)->c);\n\t\tbuttonSubtitle->setTextColorOverride((*textFg)->c);\n\t\tstyle::PaletteChanged() | rpl::on_next([=] {\n\t\t\tbuttonTitle->setTextColorOverride((*textFg)->c);\n\t\t\tbuttonSubtitle->setTextColorOverride((*textFg)->c);\n\t\t}, buttonTitle->lifetime());\n\t}\n\trpl::combine(\n\t\tbutton->sizeValue(),\n\t\tbuttonTitle->sizeValue(),\n\t\tbuttonSubtitle->sizeValue(),\n\t\tstd::move(subtitle)\n\t) | rpl::on_next([=](\n\t\t\tQSize outer,\n\t\t\tQSize title,\n\t\t\tQSize subtitle,\n\t\t\tconst TextWithEntities &subtitleText) {\n\t\tconst auto withSubtitle = !subtitleText.empty();\n\t\tbuttonSubtitle->setVisible(withSubtitle);\n\n\t\tconst auto two = title.height() + subtitle.height();\n\t\tconst auto titleTop = withSubtitle\n\t\t\t? (outer.height() - two) / 2\n\t\t\t: singleLineTextTop;\n\t\tconst auto subtitleTop = titleTop + title.height();\n\t\tbuttonTitle->moveToLeft(\n\t\t\t(outer.width() - title.width()) / 2,\n\t\t\ttitleTop);\n\t\tbuttonSubtitle->moveToLeft(\n\t\t\t(outer.width() - subtitle.width()) / 2,\n\t\t\tsubtitleTop);\n\t}, buttonTitle->lifetime());\n\tbuttonTitle->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tbuttonSubtitle->setAttribute(Qt::WA_TransparentForMouseEvents);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/button_labels.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace style {\nstruct FlatLabel;\n} // namespace style\n\nnamespace Ui {\n\nclass RoundButton;\nclass RpWidget;\n\nvoid SetButtonTwoLabels(\n    not_null<Ui::RoundButton*> button,\n    rpl::producer<TextWithEntities> title,\n    rpl::producer<TextWithEntities> subtitle,\n    const style::FlatLabel &st,\n    const style::FlatLabel &subst,\n    const style::color *textFg = nullptr);\n\nvoid SetButtonTwoLabels(\n    not_null<Ui::RpWidget*> button,\n    int singleLineTextTop,\n    rpl::producer<TextWithEntities> title,\n    rpl::producer<TextWithEntities> subtitle,\n    const style::FlatLabel &st,\n    const style::FlatLabel &subst,\n    const style::color *textFg = nullptr);\n\n} // namespace Ui"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/call_mute_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/call_mute_button.h\"\n\n#include \"base/flat_map.h\"\n#include \"ui/abstract_button.h\"\n#include \"ui/effects/shake_animation.h\"\n#include \"ui/paint/blobs.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/widgets/call_button.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/ui_utility.h\"\n#include \"base/random.h\"\n#include \"styles/palette.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_calls.h\"\n\n#include <QtCore/QtMath>\n#include <QtCore/QCoreApplication>\n\nnamespace Ui {\nnamespace {\n\nusing Radiuses = Paint::Blob::Radiuses;\n\nconstexpr auto kMaxLevel = 1.;\n\nconstexpr auto kLevelDuration = 100. + 500. * 0.33;\n\nconstexpr auto kScaleBig = 0.807 - 0.1;\nconstexpr auto kScaleSmall = 0.704 - 0.1;\n\nconstexpr auto kScaleBigMin = 0.878;\nconstexpr auto kScaleSmallMin = 0.926;\n\nconstexpr auto kScaleBigMax = (float)(kScaleBigMin + kScaleBig);\nconstexpr auto kScaleSmallMax = (float)(kScaleSmallMin + kScaleSmall);\n\nconstexpr auto kMainRadiusFactor = (float)(50. / 57.);\n\nconstexpr auto kGlowPaddingFactor = 1.2;\nconstexpr auto kGlowMinScale = 0.6;\nconstexpr auto kGlowAlpha = 150;\n\nconstexpr auto kOverrideColorBgAlpha = 76;\nconstexpr auto kOverrideColorRippleAlpha = 50;\n\nconstexpr auto kSwitchStateDuration = crl::time(120);\nconstexpr auto kSwitchLabelDuration = crl::time(180);\n\n// Switch state from Connecting animation.\nconstexpr auto kSwitchRadialDuration = crl::time(350);\nconstexpr auto kSwitchCirclelDuration = crl::time(275);\nconstexpr auto kBlobsScaleEnterDuration = crl::time(400);\nconstexpr auto kSwitchStateFromConnectingDuration = kSwitchRadialDuration\n\t+ kSwitchCirclelDuration\n\t+ kBlobsScaleEnterDuration;\n\nconstexpr auto kRadialEndPartAnimation = float(kSwitchRadialDuration)\n\t/ kSwitchStateFromConnectingDuration;\nconstexpr auto kBlobsWidgetPartAnimation = 1. - kRadialEndPartAnimation;\nconstexpr auto kFillCirclePartAnimation = float(kSwitchCirclelDuration)\n\t/ (kSwitchCirclelDuration + kBlobsScaleEnterDuration);\nconstexpr auto kBlobPartAnimation = float(kBlobsScaleEnterDuration)\n\t/ (kSwitchCirclelDuration + kBlobsScaleEnterDuration);\n\nconstexpr auto kOverlapProgressRadialHide = 1.2;\n\nconstexpr auto kRadialFinishArcShift = 1200;\n\n[[nodiscard]] CallMuteButtonType TypeForIcon(CallMuteButtonType type) {\n\treturn (type == CallMuteButtonType::Connecting\n\t\t|| type == CallMuteButtonType::ConferenceForceMuted)\n\t\t? CallMuteButtonType::Muted\n\t\t: (type == CallMuteButtonType::RaisedHand)\n\t\t? CallMuteButtonType::ForceMuted\n\t\t: type;\n};\n\n[[nodiscard]] QSize AdjustedLottieSize(\n\t\tnot_null<const style::CallMuteButton*> st) {\n\tconst auto &button = st->active.button;\n\tconst auto left = (button.width - st->lottieSize.width()) / 2;\n\tconst auto size = button.width - 2 * left;\n\treturn QSize(size, size);\n}\n\n[[nodiscard]] int AdjustedBgSize(\n\t\tnot_null<const style::CallMuteButton*> st) {\n\tconst auto &button = st->active.button;\n\tconst auto left = (button.width - st->active.bgSize) / 2;\n\treturn button.width - 2 * left;\n}\n\n[[nodiscard]] int AdjustedBgSkip(\n\t\tnot_null<const style::CallMuteButton*> st) {\n\tconst auto &button = st->active.button;\n\tconst auto bgSize = AdjustedBgSize(st);\n\treturn (button.width - bgSize) / 2;\n}\n\nauto MuteBlobs() {\n\treturn std::vector<Paint::Blobs::BlobData>{\n\t\t{\n\t\t\t.segmentsCount = 9,\n\t\t\t.minScale = kScaleSmallMin / kScaleSmallMax,\n\t\t\t.minRadius = st::callMuteMinorBlobMinRadius\n\t\t\t\t* kScaleSmallMax\n\t\t\t\t* kMainRadiusFactor,\n\t\t\t.maxRadius = st::callMuteMinorBlobMaxRadius\n\t\t\t\t* kScaleSmallMax\n\t\t\t\t* kMainRadiusFactor,\n\t\t\t.speedScale = 1.,\n\t\t\t.alpha = (76. / 255.),\n\t\t},\n\t\t{\n\t\t\t.segmentsCount = 12,\n\t\t\t.minScale = kScaleBigMin / kScaleBigMax,\n\t\t\t.minRadius = st::callMuteMajorBlobMinRadius\n\t\t\t\t* kScaleBigMax\n\t\t\t\t* kMainRadiusFactor,\n\t\t\t.maxRadius = st::callMuteMajorBlobMaxRadius\n\t\t\t\t* kScaleBigMax\n\t\t\t\t* kMainRadiusFactor,\n\t\t\t.speedScale = 1.,\n\t\t\t.alpha = (76. / 255.),\n\t\t},\n\t};\n}\n\nauto Colors() {\n\tusing Vector = std::vector<QColor>;\n\tusing Colors = anim::gradient_colors;\n\tauto result = base::flat_map<CallMuteButtonType, Colors>{\n\t\t{\n\t\t\tCallMuteButtonType::Active,\n\t\t\tColors(Vector{ st::groupCallLive1->c, st::groupCallLive2->c })\n\t\t},\n\t\t{\n\t\t\tCallMuteButtonType::Connecting,\n\t\t\tColors(st::callIconBg->c)\n\t\t},\n\t\t{\n\t\t\tCallMuteButtonType::Muted,\n\t\t\tColors(Vector{ st::groupCallMuted1->c, st::groupCallMuted2->c })\n\t\t},\n\t};\n\tconst auto forceMutedColors = Colors(QGradientStops{\n\t\t{ .0, st::groupCallForceMuted3->c },\n\t\t{ .5, st::groupCallForceMuted2->c },\n\t\t{ 1., st::groupCallForceMuted1->c } });\n\tconst auto forceMutedTypes = {\n\t\tCallMuteButtonType::ForceMuted,\n\t\tCallMuteButtonType::RaisedHand,\n\t\tCallMuteButtonType::ConferenceForceMuted,\n\t\tCallMuteButtonType::ScheduledCanStart,\n\t\tCallMuteButtonType::ScheduledNotify,\n\t\tCallMuteButtonType::ScheduledSilent,\n\t};\n\tfor (const auto type : forceMutedTypes) {\n\t\tresult.emplace(type, forceMutedColors);\n\t}\n\treturn result;\n}\n\nbool IsConnecting(CallMuteButtonType type) {\n\treturn (type == CallMuteButtonType::Connecting);\n}\n\nbool IsInactive(CallMuteButtonType type) {\n\treturn IsConnecting(type);\n}\n\nauto Clamp(float64 value) {\n\treturn std::clamp(value, 0., 1.);\n}\n\nvoid ComputeRadialFinish(\n\t\tint &value,\n\t\tfloat64 progress,\n\t\tint to = -RadialState::kFull) {\n\tvalue = anim::interpolate(value, to, Clamp(progress));\n}\n\n} // namespace\n\nclass AnimatedLabel final : public RpWidget {\npublic:\n\tAnimatedLabel(\n\t\tQWidget *parent,\n\t\trpl::producer<QString> &&text,\n\t\tcrl::time duration,\n\t\tint additionalHeight,\n\t\tconst style::FlatLabel &st = st::defaultFlatLabel);\n\n\tint contentHeight() const;\n\tvoid setMaxWidth(int width);\n\nprivate:\n\tvoid setText(const QString &text);\n\n\tconst style::FlatLabel &_st;\n\tconst crl::time _duration;\n\tconst int _additionalHeight;\n\tconst TextParseOptions _options;\n\n\tText::String _text;\n\tText::String _previousText;\n\n\tint _maxLabelWidth = 0;\n\n\tAnimations::Simple _animation;\n\n};\n\nAnimatedLabel::AnimatedLabel(\n\tQWidget *parent,\n\trpl::producer<QString> &&text,\n\tcrl::time duration,\n\tint additionalHeight,\n\tconst style::FlatLabel &st)\n: RpWidget(parent)\n, _st(st)\n, _duration(duration)\n, _additionalHeight(additionalHeight)\n, _options({ 0, 0, 0, Qt::LayoutDirectionAuto }) {\n\tstd::move(\n\t\ttext\n\t) | rpl::on_next([=](const QString &value) {\n\t\tsetText(value);\n\t}, lifetime());\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(this);\n\t\tconst auto progress = _animation.value(1.);\n\n\t\tp.setFont(_st.style.font);\n\t\tp.setPen(_st.textFg);\n\t\tp.setTextPalette(_st.palette);\n\n\t\tconst auto textHeight = contentHeight();\n\t\tconst auto diffHeight = height() - textHeight;\n\t\tconst auto center = diffHeight / 2;\n\n\t\tp.setOpacity(1. - progress);\n\t\tif (p.opacity()) {\n\t\t\t_previousText.drawElided(\n\t\t\t\tp,\n\t\t\t\t0,\n\t\t\t\tanim::interpolate(center, diffHeight, progress),\n\t\t\t\twidth(),\n\t\t\t\t1,\n\t\t\t\tstyle::al_center);\n\t\t}\n\n\t\tp.setOpacity(progress);\n\t\tif (p.opacity()) {\n\t\t\t_text.drawElided(\n\t\t\t\tp,\n\t\t\t\t0,\n\t\t\t\tanim::interpolate(0, center, progress),\n\t\t\t\twidth(),\n\t\t\t\t1,\n\t\t\t\tstyle::al_center);\n\t\t}\n\t}, lifetime());\n}\n\nint AnimatedLabel::contentHeight() const {\n\tconst auto lineHeight = _st.style.font->height;\n\tif (!_maxLabelWidth) {\n\t\treturn lineHeight;\n\t}\n\tconst auto textHeight = _text.countHeight(_maxLabelWidth);\n\tconst auto previousHeight = _previousText.countHeight(_maxLabelWidth);\n\treturn std::min(std::max(textHeight, previousHeight), lineHeight * 2);\n}\n\nvoid AnimatedLabel::setText(const QString &text) {\n\tif (_text.toString() == text) {\n\t\treturn;\n\t}\n\t_previousText = std::move(_text);\n\t_text = Ui::Text::String(_st.style, text, _options);\n\n\tconst auto width = _maxLabelWidth\n\t\t? _maxLabelWidth\n\t\t: std::max(\n\t\t\t_st.style.font->width(_text.toString()),\n\t\t\t_st.style.font->width(_previousText.toString()));\n\tresize(\n\t\twidth + _additionalHeight,\n\t\tcontentHeight() + _additionalHeight * 2);\n\n\t_animation.stop();\n\t_animation.start([=] { update(); }, 0., 1., _duration);\n}\n\nvoid AnimatedLabel::setMaxWidth(int width) {\n\t_maxLabelWidth = width;\n\tif (!_text.isEmpty()) {\n\t\tresize(\n\t\t\t_maxLabelWidth + _additionalHeight,\n\t\t\tcontentHeight() + _additionalHeight * 2);\n\t\tupdate();\n\t}\n}\n\nclass BlobsWidget final : public RpWidget {\npublic:\n\tBlobsWidget(\n\t\tnot_null<RpWidget*> parent,\n\t\tint diameter,\n\t\trpl::producer<bool> &&hideBlobs);\n\n\tvoid setDiameter(int diameter);\n\tvoid setLevel(float level);\n\tvoid setBlobBrush(QBrush brush);\n\tvoid setGlowBrush(QBrush brush);\n\n\t[[nodiscard]] QRectF innerRect() const;\n\n\t[[nodiscard]] float64 switchConnectingProgress() const;\n\tvoid setSwitchConnectingProgress(float64 progress);\n\nprivate:\n\tvoid init(int diameter);\n\tvoid computeCircleRect();\n\n\tPaint::Blobs _blobs;\n\n\tfloat _circleRadius = 0.;\n\tQBrush _blobBrush;\n\tQBrush _glowBrush;\n\tint _center = 0;\n\tQRectF _circleRect;\n\n\tfloat64 _switchConnectingProgress = 0.;\n\n\tcrl::time _blobsLastTime = 0;\n\tcrl::time _blobsHideLastTime = 0;\n\n\tfloat64 _blobsScaleEnter = 0.;\n\tcrl::time _blobsScaleLastTime = 0;\n\n\tbool _hideBlobs = true;\n\n\tAnimations::Basic _animation;\n\n};\n\nBlobsWidget::BlobsWidget(\n\tnot_null<RpWidget*> parent,\n\tint diameter,\n\trpl::producer<bool> &&hideBlobs)\n: RpWidget(parent)\n, _blobs(MuteBlobs(), kLevelDuration, kMaxLevel)\n, _blobBrush(Qt::transparent)\n, _glowBrush(Qt::transparent)\n, _blobsLastTime(crl::now())\n, _blobsScaleLastTime(crl::now()) {\n\tinit(diameter);\n\n\tstd::move(\n\t\thideBlobs\n\t) | rpl::on_next([=](bool hide) {\n\t\tif (_hideBlobs != hide) {\n\t\t\tconst auto now = crl::now();\n\t\t\tif ((now - _blobsScaleLastTime) >= kBlobsScaleEnterDuration) {\n\t\t\t\t_blobsScaleLastTime = now;\n\t\t\t}\n\t\t\t_hideBlobs = hide;\n\t\t}\n\t\tif (hide) {\n\t\t\tsetLevel(0.);\n\t\t}\n\t\t_blobsHideLastTime = hide ? crl::now() : 0;\n\t\tif (!hide && !_animation.animating()) {\n\t\t\t_animation.start();\n\t\t}\n\t}, lifetime());\n}\n\nvoid BlobsWidget::setDiameter(int diameter) {\n\t_circleRadius = diameter / 2.;\n\tconst auto defaultSize = _blobs.maxRadius() * 2 * kGlowPaddingFactor;\n\tconst auto s = int(std::ceil((defaultSize * diameter)\n\t\t/ float64(st::callMuteBlobRadiusForDiameter)));\n\tconst auto size = QSize{ s, s };\n\tif (this->size() != size) {\n\t\tresize(size);\n\t}\n\tcomputeCircleRect();\n}\n\nvoid BlobsWidget::computeCircleRect() {\n\tconst auto &r = _circleRadius;\n\tconst auto left = (size().width() - r * 2.) / 2.;\n\tconst auto add = st::callConnectingRadial.thickness / 2;\n\t_circleRect = QRectF(left, left, r * 2, r * 2).marginsAdded(\n\t\tstyle::margins(add, add, add, add));\n}\n\nvoid BlobsWidget::init(int diameter) {\n\tsetAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto cutRect = [](Painter &p, const QRectF &r) {\n\t\tp.save();\n\t\tp.setOpacity(1.);\n\t\tp.setBrush(st::groupCallBg);\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tp.drawEllipse(r);\n\t\tp.restore();\n\t};\n\n\tsetDiameter(diameter);\n\n\tsizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\t_center = size.width() / 2;\n\t\tcomputeCircleRect();\n\t}, lifetime());\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(this);\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tp.setPen(Qt::NoPen);\n\n\t\t// Glow.\n\t\tconst auto s = kGlowMinScale\n\t\t\t+ (1. - kGlowMinScale) * _blobs.currentLevel();\n\t\tp.translate(_center, _center);\n\t\tp.scale(s, s);\n\t\tp.translate(-_center, -_center);\n\t\tp.fillRect(rect(), _glowBrush);\n\t\tp.resetTransform();\n\n\t\t// Blobs.\n\t\tp.translate(_center, _center);\n\t\tconst auto scale = (_switchConnectingProgress > 0.)\n\t\t\t? anim::easeOutBack(\n\t\t\t\t1.,\n\t\t\t\t_blobsScaleEnter * (1. - Clamp(\n\t\t\t\t\t_switchConnectingProgress / kBlobPartAnimation)))\n\t\t\t: _blobsScaleEnter;\n\t\tconst auto sizeScale = (2. * _circleRadius)\n\t\t\t/ st::callMuteBlobRadiusForDiameter;\n\t\t_blobs.paint(p, _blobBrush, scale * sizeScale);\n\t\tp.translate(-_center, -_center);\n\n\t\tif (scale < 1.) {\n\t\t\tcutRect(p, _circleRect);\n\t\t}\n\n\t\t// Main circle.\n\t\tconst auto circleProgress\n\t\t\t= Clamp(_switchConnectingProgress - kBlobPartAnimation)\n\t\t\t\t/ kFillCirclePartAnimation;\n\t\tconst auto skipColoredCircle = (circleProgress == 1.);\n\n\t\tif (!skipColoredCircle) {\n\t\t\tp.setBrush(_blobBrush);\n\t\t\tp.drawEllipse(_circleRect);\n\t\t}\n\n\t\tif (_switchConnectingProgress > 0.) {\n\t\t\tp.resetTransform();\n\n\t\t\tconst auto mF = (_circleRect.width() / 2) * (1. - circleProgress);\n\t\t\tconst auto cutOutRect = _circleRect.marginsRemoved(\n\t\t\t\tQMarginsF(mF, mF, mF, mF));\n\n\t\t\tif (!skipColoredCircle) {\n\t\t\t\tp.setBrush(st::callConnectingRadial.color);\n\t\t\t\tp.setOpacity(circleProgress);\n\t\t\t\tp.drawEllipse(_circleRect);\n\t\t\t}\n\n\t\t\tp.setOpacity(1.);\n\n\t\t\tcutRect(p, cutOutRect);\n\n\t\t\tp.setBrush(st::callIconBg);\n\t\t\tp.drawEllipse(cutOutRect);\n\t\t}\n\t}, lifetime());\n\n\t_animation.init([=](crl::time now) {\n\t\tif (const auto &last = _blobsHideLastTime; (last > 0)\n\t\t\t&& (now - last >= kBlobsScaleEnterDuration)) {\n\t\t\t_animation.stop();\n\t\t\treturn false;\n\t\t}\n\t\t_blobs.updateLevel(now - _blobsLastTime);\n\t\t_blobsLastTime = now;\n\n\t\tconst auto dt = Clamp(\n\t\t\t(now - _blobsScaleLastTime) / float64(kBlobsScaleEnterDuration));\n\t\t_blobsScaleEnter = _hideBlobs\n\t\t\t? (1. - anim::easeInCirc(1., dt))\n\t\t\t: anim::easeOutBack(1., dt);\n\n\t\tupdate();\n\t\treturn true;\n\t});\n\tshownValue(\n\t) | rpl::on_next([=](bool shown) {\n\t\tif (shown) {\n\t\t\t_animation.start();\n\t\t} else {\n\t\t\t_animation.stop();\n\t\t}\n\t}, lifetime());\n}\n\nQRectF BlobsWidget::innerRect() const {\n\treturn _circleRect;\n}\n\nvoid BlobsWidget::setBlobBrush(QBrush brush) {\n\tif (_blobBrush == brush) {\n\t\treturn;\n\t}\n\t_blobBrush = brush;\n}\n\nvoid BlobsWidget::setGlowBrush(QBrush brush) {\n\tif (_glowBrush == brush) {\n\t\treturn;\n\t}\n\t_glowBrush = brush;\n}\n\nvoid BlobsWidget::setLevel(float level) {\n\tif (_blobsHideLastTime) {\n\t\t return;\n\t}\n\t_blobs.setLevel(level);\n}\n\nfloat64 BlobsWidget::switchConnectingProgress() const {\n\treturn _switchConnectingProgress;\n}\n\nvoid BlobsWidget::setSwitchConnectingProgress(float64 progress) {\n\t_switchConnectingProgress = progress;\n}\n\nCallMuteButton::CallMuteButton(\n\tnot_null<RpWidget*> parent,\n\tconst style::CallMuteButton &st,\n\trpl::producer<bool> &&hideBlobs,\n\tCallMuteButtonState initial)\n: _state(initial)\n, _st(&st)\n, _lottieSize(AdjustedLottieSize(_st))\n, _bgSize(AdjustedBgSize(_st))\n, _bgSkip(AdjustedBgSkip(_st))\n, _blobs(base::make_unique_q<BlobsWidget>(\n\tparent,\n\t_bgSize,\n\trpl::combine(\n\t\tPowerSaving::OnValue(PowerSaving::kCalls),\n\t\tstd::move(hideBlobs),\n\t\t_state.value(\n\t\t) | rpl::map([](const CallMuteButtonState &state) {\n\t\t\treturn IsInactive(state.type);\n\t\t})\n\t) | rpl::map(rpl::mappers::_1 || rpl::mappers::_2 || rpl::mappers::_3)))\n, _content(base::make_unique_q<AbstractButton>(parent))\n, _colors(Colors())\n, _iconState(iconStateFrom(initial.type)) {\n\tinit();\n}\n\nvoid CallMuteButton::refreshLabels() {\n\t_centerLabel = base::make_unique_q<AnimatedLabel>(\n\t\t_content->parentWidget(),\n\t\t_state.value(\n\t\t) | rpl::map([](const CallMuteButtonState &state) {\n\t\t\treturn state.subtext.isEmpty() ? state.text : QString();\n\t\t}),\n\t\tkSwitchLabelDuration,\n\t\t_st->labelAdditional,\n\t\t_st->active.label);\n\t_label = base::make_unique_q<AnimatedLabel>(\n\t\t_content->parentWidget(),\n\t\t_state.value(\n\t\t) | rpl::map([](const CallMuteButtonState &state) {\n\t\t\treturn state.subtext.isEmpty() ? QString() : state.text;\n\t\t}),\n\t\tkSwitchLabelDuration,\n\t\t_st->labelAdditional,\n\t\t_st->active.label);\n\t_sublabel = base::make_unique_q<AnimatedLabel>(\n\t\t_content->parentWidget(),\n\t\t_state.value(\n\t\t) | rpl::map([](const CallMuteButtonState &state) {\n\t\t\treturn state.subtext;\n\t\t}),\n\t\tkSwitchLabelDuration,\n\t\t_st->labelAdditional,\n\t\t_st->sublabel);\n\n\t_label->show();\n\trpl::combine(\n\t\t_content->geometryValue(),\n\t\t_label->sizeValue()\n\t) | rpl::on_next([=](QRect my, QSize size) {\n\t\tupdateLabelGeometry(my, size);\n\t}, _label->lifetime());\n\t_label->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t_sublabel->show();\n\trpl::combine(\n\t\t_content->geometryValue(),\n\t\t_sublabel->sizeValue()\n\t) | rpl::on_next([=](QRect my, QSize size) {\n\t\tupdateSublabelGeometry(my, size);\n\t}, _sublabel->lifetime());\n\t_sublabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t_centerLabel->show();\n\trpl::combine(\n\t\t_content->geometryValue(),\n\t\t_centerLabel->sizeValue()\n\t) | rpl::on_next([=](QRect my, QSize size) {\n\t\tupdateCenterLabelGeometry(my, size);\n\t}, _centerLabel->lifetime());\n\t_centerLabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t_content->sizeValue(\n\t) | rpl::map([](QSize size) {\n\t\treturn size.width();\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](int w) {\n\t\t_centerLabel->setMaxWidth(w);\n\t\t_label->setMaxWidth(w);\n\t\t_sublabel->setMaxWidth(w);\n\t}, _centerLabel->lifetime());\n}\n\nvoid CallMuteButton::refreshIcons() {\n\t_icons[0].emplace(Lottie::IconDescriptor{\n\t\t.path = u\":/icons/calls/voice.lottie\"_q,\n\t\t.color = &st::groupCallIconFg,\n\t\t.sizeOverride = _lottieSize,\n\t\t.frame = (_iconState.index ? 0 : _iconState.frameTo),\n\t});\n\t_icons[1].emplace(Lottie::IconDescriptor{\n\t\t.path = u\":/icons/calls/hands.lottie\"_q,\n\t\t.color = &st::groupCallIconFg,\n\t\t.sizeOverride = _lottieSize,\n\t\t.frame = (_iconState.index ? _iconState.frameTo : 0),\n\t});\n\n}\n\nauto CallMuteButton::iconStateAnimated(CallMuteButtonType previous)\n-> IconState {\n\tusing Type = CallMuteButtonType;\n\tusing Key = std::pair<Type, Type>;\n\tstruct Animation {\n\t\tint from = 0;\n\t\tint to = 0;\n\t};\n\tstatic const auto kAnimations = std::vector<std::pair<Key, Animation>>{\n\t\t{ { Type::ForceMuted, Type::Muted }, { 0, 35 } },\n\t\t{ { Type::Muted, Type::Active }, { 36, 68 } },\n\t\t{ { Type::Active, Type::Muted }, { 69, 98 } },\n\t\t{ { Type::Muted, Type::ForceMuted }, { 99, 135 } },\n\t\t{ { Type::Active, Type::ForceMuted }, { 136, 172 } },\n\t\t{ { Type::ScheduledSilent, Type::ScheduledNotify }, { 173, 201 } },\n\t\t{ { Type::ScheduledSilent, Type::Muted }, { 202, 236 } },\n\t\t{ { Type::ScheduledSilent, Type::ForceMuted }, { 237, 273 } },\n\t\t{ { Type::ScheduledNotify, Type::ForceMuted }, { 274, 310 } },\n\t\t{ { Type::ScheduledNotify, Type::ScheduledSilent }, { 311, 343 } },\n\t\t{ { Type::ScheduledNotify, Type::Muted }, { 344, 375 } },\n\t\t{ { Type::ScheduledCanStart, Type::Muted }, { 376, 403 } },\n\t};\n\tstatic const auto kMap = [] {\n\t\t// flat_multi_map_pair_type lacks some required constructors :(\n\t\tauto &&list = kAnimations | ranges::views::transform([](auto &&pair) {\n\t\t\treturn base::flat_multi_map_pair_type<Key, Animation>(\n\t\t\t\tpair.first,\n\t\t\t\tpair.second);\n\t\t});\n\t\treturn base::flat_map<Key, Animation>(begin(list), end(list));\n\t}();\n\tconst auto was = TypeForIcon(previous);\n\tconst auto now = TypeForIcon(_state.current().type);\n\tif (was == now) {\n\t\treturn {};\n\t}\n\n\tif (const auto i = kMap.find(Key{ was, now }); i != end(kMap)) {\n\t\treturn { 0, i->second.from, i->second.to };\n\t}\n\treturn {};\n}\n\nCallMuteButton::IconState CallMuteButton::iconStateFrom(\n\t\tCallMuteButtonType previous) {\n\tif (const auto animated = iconStateAnimated(previous)) {\n\t\treturn animated;\n\t}\n\n\tusing Type = CallMuteButtonType;\n\tstatic const auto kFinal = base::flat_map<Type, int>{\n\t\t{ Type::ForceMuted, 0 },\n\t\t{ Type::Muted, 36 },\n\t\t{ Type::Active, 69 },\n\t\t{ Type::ScheduledSilent, 173 },\n\t\t{ Type::ScheduledNotify, 274 },\n\t\t{ Type::ScheduledCanStart, 376 },\n\t};\n\n\tconst auto now = TypeForIcon(_state.current().type);\n\tconst auto i = kFinal.find(now);\n\n\tEnsures(i != end(kFinal));\n\treturn { 0, i->second, i->second };\n}\n\nCallMuteButton::IconState CallMuteButton::randomWavingState() {\n\tstruct Animation {\n\t\tint from = 0;\n\t\tint to = 0;\n\t};\n\tstatic const auto kAnimations = std::vector<Animation>{\n\t\t{ 0, 119 },\n\t\t{ 120, 239 },\n\t\t{ 240, 419 },\n\t\t{ 420, 539 },\n\t};\n\tconst auto index = base::RandomIndex(kAnimations.size());\n\treturn { 1, kAnimations[index].from, kAnimations[index].to };\n}\n\nvoid CallMuteButton::init() {\n\trefreshLabels();\n\trefreshIcons();\n\n\tconst auto &button = _st->active.button;\n\t_content->resize(button.width, button.height);\n\n\t_content->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::MouseMove) {\n\t\t\tif (!_state.current().tooltip.isEmpty()) {\n\t\t\t\tUi::Tooltip::Show(1000, this);\n\t\t\t}\n\t\t} else if (e->type() == QEvent::Leave) {\n\t\t\tUi::Tooltip::Hide();\n\t\t}\n\t}, _content->lifetime());\n\n\trpl::combine(\n\t\t_radialInfo.rawShowProgress.value(),\n\t\tanim::Disables()\n\t) | rpl::on_next([=](float64 value, bool disabled) {\n\t\tauto &info = _radialInfo;\n\t\tinfo.realShowProgress = (1. - value) / kRadialEndPartAnimation;\n\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\t_content->update();\n\t\t});\n\n\t\tif (((value == 0.) || disabled) && _radial) {\n\t\t\t_radial->stop();\n\t\t\t_radial = nullptr;\n\t\t\treturn;\n\t\t}\n\t\tif ((value > 0.) && !disabled && !_radial) {\n\t\t\t_radial = std::make_unique<InfiniteRadialAnimation>(\n\t\t\t\t[=] { _content->update(); },\n\t\t\t\t_radialInfo.st);\n\t\t\t_radial->start();\n\t\t}\n\t\tif ((info.realShowProgress < 1.) && !info.isDirectionToShow) {\n\t\t\tif (_radial) {\n\t\t\t\t_radial->stop(anim::type::instant);\n\t\t\t\t_radial->start();\n\t\t\t}\n\t\t\tinfo.state = std::nullopt;\n\t\t\treturn;\n\t\t}\n\n\t\tif (value == 1.) {\n\t\t\tinfo.state = std::nullopt;\n\t\t} else {\n\t\t\tif (_radial && !info.state.has_value()) {\n\t\t\t\tinfo.state = _radial->computeState();\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\t// State type.\n\tconst auto previousType\n\t\t= lifetime().make_state<CallMuteButtonType>(_state.current().type);\n\tsetHandleMouseState(HandleMouseState::Disabled);\n\n\trefreshGradients();\n\n\t_state.value(\n\t) | rpl::map([](const CallMuteButtonState &state) {\n\t\treturn state.type;\n\t}) | rpl::on_next([=](CallMuteButtonType type) {\n\t\tconst auto previous = *previousType;\n\t\t*previousType = type;\n\n\t\tconst auto mouseState = HandleMouseStateFromType(type);\n\t\tsetHandleMouseState(HandleMouseState::Disabled);\n\t\tif (mouseState != HandleMouseState::Enabled) {\n\t\t\tsetHandleMouseState(mouseState);\n\t\t}\n\n\t\tconst auto fromConnecting = IsConnecting(previous);\n\t\tconst auto toConnecting = IsConnecting(type);\n\n\t\tconst auto radialShowFrom = fromConnecting ? 1. : 0.;\n\t\tconst auto radialShowTo = toConnecting ? 1. : 0.;\n\n\t\tconst auto from = (_switchAnimation.animating() && !fromConnecting)\n\t\t\t? (1. - _switchAnimation.value(0.))\n\t\t\t: 0.;\n\t\tconst auto to = 1.;\n\n\t\t_radialInfo.isDirectionToShow = fromConnecting;\n\n\t\tscheduleIconState(iconStateFrom(previous));\n\n\t\tauto callback = [=](float64 value) {\n\t\t\tconst auto brushProgress = fromConnecting ? 1. : value;\n\t\t\t_blobs->setBlobBrush(QBrush(\n\t\t\t\t_linearGradients.gradient(previous, type, brushProgress)));\n\t\t\t_blobs->setGlowBrush(QBrush(\n\t\t\t\t_glowGradients.gradient(previous, type, value)));\n\t\t\t_blobs->update();\n\n\t\t\tconst auto radialShowProgress = (radialShowFrom == radialShowTo)\n\t\t\t\t? radialShowTo\n\t\t\t\t: anim::interpolateToF(radialShowFrom, radialShowTo, value);\n\t\t\tif (radialShowProgress != _radialInfo.rawShowProgress.current()) {\n\t\t\t\t_radialInfo.rawShowProgress = radialShowProgress;\n\t\t\t\t_blobs->setSwitchConnectingProgress(Clamp(\n\t\t\t\t\tradialShowProgress / kBlobsWidgetPartAnimation));\n\t\t\t}\n\n\t\t\toverridesColors(previous, type, value);\n\n\t\t\tif (value == to) {\n\t\t\t\tsetHandleMouseState(mouseState);\n\t\t\t}\n\t\t};\n\n\t\t_switchAnimation.stop();\n\t\tconst auto duration = (1. - from) * ((fromConnecting || toConnecting)\n\t\t\t? kSwitchStateFromConnectingDuration\n\t\t\t: kSwitchStateDuration);\n\t\t_switchAnimation.start(std::move(callback), from, to, duration);\n\t}, lifetime());\n\n\t// Icon rect.\n\t_content->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tconst auto icon = _lottieSize;\n\t\t_muteIconRect = QRect(\n\t\t\t(size.width() - icon.width()) / 2,\n\t\t\t_st->lottieTop,\n\t\t\ticon.width(),\n\t\t\ticon.height());\n\t}, lifetime());\n\n\t// Paint.\n\t_content->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tPainter p(_content);\n\n\t\tconst auto expand = _state.current().expandType;\n\t\tif (expand == CallMuteButtonExpandType::Expanded) {\n\t\t\tst::callMuteFromFullScreen.paintInCenter(p, _muteIconRect);\n\t\t} else if (expand == CallMuteButtonExpandType::Normal) {\n\t\t\tst::callMuteToFullScreen.paintInCenter(p, _muteIconRect);\n\t\t} else {\n\t\t\t_icons[_iconState.index]->paint(\n\t\t\t\tp,\n\t\t\t\t_muteIconRect.x(),\n\t\t\t\t_muteIconRect.y());\n\t\t}\n\n\t\tif (_radialInfo.state.has_value() && _switchAnimation.animating()) {\n\t\t\tconst auto radialProgress = _radialInfo.realShowProgress;\n\n\t\t\tauto r = *_radialInfo.state;\n\t\t\tr.shown = 1.;\n\t\t\tif (_radialInfo.isDirectionToShow) {\n\t\t\t\tconst auto to = r.arcFrom - kRadialFinishArcShift;\n\t\t\t\tComputeRadialFinish(r.arcFrom, radialProgress, to);\n\t\t\t\tComputeRadialFinish(r.arcLength, radialProgress);\n\t\t\t} else {\n\t\t\t\tr.arcLength = RadialState::kFull;\n\t\t\t}\n\n\t\t\tconst auto opacity = (radialProgress > kOverlapProgressRadialHide)\n\t\t\t\t? 0.\n\t\t\t\t: _blobs->switchConnectingProgress();\n\t\t\tp.setOpacity(opacity);\n\t\t\tInfiniteRadialAnimation::Draw(\n\t\t\t\tp,\n\t\t\t\tr,\n\t\t\t\tQPoint(_bgSkip, _bgSkip),\n\t\t\t\tQSize(_bgSize, _bgSize),\n\t\t\t\t_content->width(),\n\t\t\t\tQPen(_radialInfo.st.color),\n\t\t\t\t_radialInfo.st.thickness);\n\t\t} else if (_radial) {\n\t\t\tauto state = _radial->computeState();\n\t\t\tstate.shown = 1.;\n\n\t\t\tInfiniteRadialAnimation::Draw(\n\t\t\t\tp,\n\t\t\t\tstd::move(state),\n\t\t\t\tQPoint(_bgSkip, _bgSkip),\n\t\t\t\tQSize(_bgSize, _bgSize),\n\t\t\t\t_content->width(),\n\t\t\t\tQPen(_radialInfo.st.color),\n\t\t\t\t_radialInfo.st.thickness);\n\t\t}\n\t}, _content->lifetime());\n}\n\nvoid CallMuteButton::refreshGradients() {\n\tconst auto blobsInner = [&] {\n\t\t// The point of the circle at 45 degrees.\n\t\tconst auto w = _blobs->innerRect().width();\n\t\tconst auto mF = (1 - std::cos(M_PI / 4.)) * (w / 2.);\n\t\treturn _blobs->innerRect().marginsRemoved(QMarginsF(mF, mF, mF, mF));\n\t}();\n\n\t_linearGradients = anim::linear_gradients<CallMuteButtonType>(\n\t\t_colors,\n\t\tQPointF(blobsInner.x() + blobsInner.width(), blobsInner.y()),\n\t\tQPointF(blobsInner.x(), blobsInner.y() + blobsInner.height()));\n\n\tauto glowColors = [&] {\n\t\tauto copy = _colors;\n\t\tfor (auto &[type, stops] : copy) {\n\t\t\tauto firstColor = IsInactive(type)\n\t\t\t\t? st::groupCallBg->c\n\t\t\t\t: stops.stops[(stops.stops.size() - 1) / 2].second;\n\t\t\tfirstColor.setAlpha(kGlowAlpha);\n\t\t\tstops.stops = QGradientStops{\n\t\t\t\t{ 0., std::move(firstColor) },\n\t\t\t\t{ 1., QColor(Qt::transparent) }\n\t\t\t};\n\t\t}\n\t\treturn copy;\n\t}();\n\t_glowGradients = anim::radial_gradients<CallMuteButtonType>(\n\t\tstd::move(glowColors),\n\t\tblobsInner.center(),\n\t\t_blobs->width() / 2);\n}\n\nvoid CallMuteButton::scheduleIconState(const IconState &state) {\n\tif (_iconState != state) {\n\t\tif (_icons[_iconState.index]->animating()) {\n\t\t\t_scheduledState = state;\n\t\t} else {\n\t\t\tstartIconState(state);\n\t\t}\n\t} else if (_scheduledState) {\n\t\t_scheduledState = std::nullopt;\n\t}\n}\n\nvoid CallMuteButton::startIconState(const IconState &state) {\n\t_iconState = state;\n\t_scheduledState = std::nullopt;\n\t_icons[_iconState.index]->animate(\n\t\t[=] { iconAnimationCallback(); },\n\t\t_iconState.frameFrom,\n\t\t_iconState.frameTo);\n}\n\nvoid CallMuteButton::iconAnimationCallback() {\n\t_content->update(_muteIconRect);\n\tif (!_icons[_iconState.index]->animating() && _scheduledState) {\n\t\tstartIconState(*_scheduledState);\n\t}\n}\n\nQString CallMuteButton::tooltipText() const {\n\treturn _state.current().tooltip;\n}\n\nQPoint CallMuteButton::tooltipPos() const {\n\treturn QCursor::pos();\n}\n\nbool CallMuteButton::tooltipWindowActive() const {\n\treturn Ui::AppInFocus()\n\t\t&& Ui::InFocusChain(_content->window())\n\t\t&& _content->mapToGlobal(_content->rect()).contains(QCursor::pos());\n}\n\nconst style::Tooltip *CallMuteButton::tooltipSt() const {\n\treturn &st::groupCallTooltip;\n}\n\nvoid CallMuteButton::updateLabelsGeometry() {\n\tupdateLabelGeometry(_content->geometry(), _label->size());\n\tupdateCenterLabelGeometry(_content->geometry(), _centerLabel->size());\n\tupdateSublabelGeometry(_content->geometry(), _sublabel->size());\n}\n\nvoid CallMuteButton::updateLabelGeometry(QRect my, QSize size) {\n\tconst auto skip = _st->sublabelSkip + _st->labelsSkip;\n\tconst auto contentHeight = _label->contentHeight();\n\tconst auto contentTop = my.y() + my.height() - contentHeight - skip;\n\t_label->moveToLeft(\n\t\tmy.x() + (my.width() - size.width()) / 2 + _labelShakeShift,\n\t\tcontentTop - (size.height() - contentHeight) / 2,\n\t\tmy.width());\n}\n\nvoid CallMuteButton::updateCenterLabelGeometry(QRect my, QSize size) {\n\tconst auto skip = (_st->sublabelSkip / 2) + _st->labelsSkip;\n\tconst auto contentHeight = _centerLabel->contentHeight();\n\tconst auto contentTop = my.y() + my.height() - contentHeight - skip;\n\t_centerLabel->moveToLeft(\n\t\tmy.x() + (my.width() - size.width()) / 2 + _labelShakeShift,\n\t\tcontentTop - (size.height() - contentHeight) / 2,\n\t\tmy.width());\n}\n\nvoid CallMuteButton::updateSublabelGeometry(QRect my, QSize size) {\n\tconst auto skip = _st->labelsSkip;\n\tconst auto contentHeight = _sublabel->contentHeight();\n\tconst auto contentTop = my.y() + my.height() - contentHeight - skip;\n\t_sublabel->moveToLeft(\n\t\tmy.x() + (my.width() - size.width()) / 2 + _labelShakeShift,\n\t\tcontentTop - (size.height() - contentHeight) / 2,\n\t\tmy.width());\n}\n\nvoid CallMuteButton::shake() {\n\tif (_shakeAnimation.animating()) {\n\t\treturn;\n\t}\n\t_shakeAnimation.start(DefaultShakeCallback([=](int shift) {\n\t\t_labelShakeShift = shift;\n\t\tupdateLabelsGeometry();\n\t}), 0., 1., st::shakeDuration);\n}\n\nCallMuteButton::HandleMouseState CallMuteButton::HandleMouseStateFromType(\n\t\tCallMuteButtonType type) {\n\tswitch (type) {\n\tcase CallMuteButtonType::Active:\n\tcase CallMuteButtonType::Muted:\n\t\treturn HandleMouseState::Enabled;\n\tcase CallMuteButtonType::Connecting:\n\t\treturn HandleMouseState::Disabled;\n\tcase CallMuteButtonType::ScheduledCanStart:\n\tcase CallMuteButtonType::ScheduledNotify:\n\tcase CallMuteButtonType::ScheduledSilent:\n\tcase CallMuteButtonType::ConferenceForceMuted:\n\tcase CallMuteButtonType::ForceMuted:\n\tcase CallMuteButtonType::RaisedHand:\n\t\treturn HandleMouseState::Enabled;\n\t}\n\tUnexpected(\"Type in HandleMouseStateFromType.\");\n}\n\nvoid CallMuteButton::setStyle(const style::CallMuteButton &st) {\n\tif (_st == &st) {\n\t\treturn;\n\t}\n\t_st = &st;\n\t_lottieSize = AdjustedLottieSize(_st);\n\t_bgSize = AdjustedBgSize(_st);\n\t_bgSkip = AdjustedBgSkip(_st);\n\tconst auto &button = _st->active.button;\n\t_content->resize(button.width, button.height);\n\t_blobs->setDiameter(_st->active.bgSize);\n\n\trefreshIcons();\n\trefreshLabels();\n\tupdateLabelsGeometry();\n\trefreshGradients();\n}\n\nvoid CallMuteButton::setState(const CallMuteButtonState &state) {\n\t_state = state;\n}\n\nvoid CallMuteButton::setLevel(float level) {\n\t_level = level;\n\t_blobs->setLevel(level);\n}\n\nrpl::producer<Qt::MouseButton> CallMuteButton::clicks() {\n\treturn _content->clicks() | rpl::before_next([=] {\n\t\tconst auto type = _state.current().type;\n\t\tif (type == CallMuteButtonType::ForceMuted\n\t\t\t|| type == CallMuteButtonType::RaisedHand) {\n\t\t\tscheduleIconState(randomWavingState());\n\t\t}\n\t});\n}\n\nQSize CallMuteButton::innerSize() const {\n\treturn QSize(\n\t\t_content->width() - 2 * _bgSkip,\n\t\t_content->width() - 2 * _bgSkip);\n}\n\nvoid CallMuteButton::moveInner(QPoint position) {\n\t_content->move(position - QPoint(_bgSkip, _bgSkip));\n\n\t{\n\t\tconst auto offset = QPoint(\n\t\t\t(_blobs->width() - _content->width()) / 2,\n\t\t\t(_blobs->height() - _content->width()) / 2);\n\t\t_blobs->move(_content->pos() - offset);\n\t}\n}\n\nvoid CallMuteButton::setVisible(bool visible) {\n\t_centerLabel->setVisible(visible);\n\t_label->setVisible(visible);\n\t_sublabel->setVisible(visible);\n\t_content->setVisible(visible);\n\t_blobs->setVisible(visible);\n}\n\nbool CallMuteButton::isHidden() const {\n\treturn _content->isHidden();\n}\n\nvoid CallMuteButton::raise() {\n\t_blobs->raise();\n\t_content->raise();\n\t_centerLabel->raise();\n\t_label->raise();\n\t_sublabel->raise();\n}\n\nvoid CallMuteButton::lower() {\n\t_centerLabel->lower();\n\t_label->lower();\n\t_sublabel->lower();\n\t_content->lower();\n\t_blobs->lower();\n}\n\nvoid CallMuteButton::setHandleMouseState(HandleMouseState state) {\n\tif (_handleMouseState == state) {\n\t\treturn;\n\t}\n\t_handleMouseState = state;\n\tconst auto handle = (_handleMouseState != HandleMouseState::Disabled);\n\tconst auto pointer = (_handleMouseState == HandleMouseState::Enabled);\n\t_content->setAttribute(Qt::WA_TransparentForMouseEvents, !handle);\n\t_content->setPointerCursor(pointer);\n}\n\nvoid CallMuteButton::overridesColors(\n\t\tCallMuteButtonType fromType,\n\t\tCallMuteButtonType toType,\n\t\tfloat64 progress) {\n\tconst auto toInactive = IsInactive(toType);\n\tconst auto fromInactive = IsInactive(fromType);\n\tif (toInactive && (progress == 1)) {\n\t\t_colorOverrides = CallButtonColors();\n\t\treturn;\n\t}\n\tconst auto &fromStops = _colors.find(fromType)->second.stops;\n\tconst auto &toStops = _colors.find(toType)->second.stops;\n\tauto from = fromStops[(fromStops.size() - 1) / 2].second;\n\tauto to = toStops[(toStops.size() - 1) / 2].second;\n\tauto fromRipple = from;\n\tauto toRipple = to;\n\tif (!toInactive) {\n\t\ttoRipple.setAlpha(kOverrideColorRippleAlpha);\n\t\tto.setAlpha(kOverrideColorBgAlpha);\n\t}\n\tif (!fromInactive) {\n\t\tfromRipple.setAlpha(kOverrideColorRippleAlpha);\n\t\tfrom.setAlpha(kOverrideColorBgAlpha);\n\t}\n\tconst auto resultBg = anim::color(from, to, progress);\n\tconst auto resultRipple = anim::color(fromRipple, toRipple, progress);\n\t_colorOverrides = CallButtonColors{ resultBg, resultRipple };\n}\n\nrpl::producer<CallButtonColors> CallMuteButton::colorOverrides() const {\n\treturn _colorOverrides.value();\n}\n\nnot_null<RpWidget*> CallMuteButton::outer() const {\n\treturn _content.get();\n}\n\nrpl::lifetime &CallMuteButton::lifetime() {\n\treturn _blobs->lifetime();\n}\n\nCallMuteButton::~CallMuteButton() = default;\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/call_mute_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/cross_line.h\"\n#include \"ui/effects/gradient.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/widgets/call_button.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"lottie/lottie_icon.h\"\n\nnamespace style {\nstruct CallMuteButton;\n} // namespace style\n\nnamespace st {\nextern const style::InfiniteRadialAnimation &callConnectingRadial;\n} // namespace st\n\nnamespace Ui {\n\nclass BlobsWidget;\n\nclass AbstractButton;\nclass FlatLabel;\nclass RpWidget;\nclass AnimatedLabel;\n\nenum class CallMuteButtonType {\n\tConnecting,\n\tActive,\n\tMuted,\n\tForceMuted,\n\tRaisedHand,\n\tConferenceForceMuted,\n\tScheduledCanStart,\n\tScheduledSilent,\n\tScheduledNotify,\n};\n\nenum class CallMuteButtonExpandType {\n\tNone,\n\tNormal,\n\tExpanded,\n};\n\nstruct CallMuteButtonState {\n\tQString text;\n\tQString subtext;\n\tQString tooltip;\n\tCallMuteButtonType type = CallMuteButtonType::Connecting;\n\tCallMuteButtonExpandType expandType = CallMuteButtonExpandType::None;\n};\n\nclass CallMuteButton final : private AbstractTooltipShower {\npublic:\n\texplicit CallMuteButton(\n\t\tnot_null<RpWidget*> parent,\n\t\tconst style::CallMuteButton &st,\n\t\trpl::producer<bool> &&hideBlobs,\n\t\tCallMuteButtonState initial = CallMuteButtonState());\n\t~CallMuteButton();\n\n\tvoid setState(const CallMuteButtonState &state);\n\tvoid setStyle(const style::CallMuteButton &st);\n\tvoid setLevel(float level);\n\t[[nodiscard]] rpl::producer<Qt::MouseButton> clicks();\n\n\t[[nodiscard]] QSize innerSize() const;\n\tvoid moveInner(QPoint position);\n\n\tvoid shake();\n\n\tvoid setVisible(bool visible);\n\tvoid show() {\n\t\tsetVisible(true);\n\t}\n\tvoid hide() {\n\t\tsetVisible(false);\n\t}\n\t[[nodiscard]] bool isHidden() const;\n\tvoid raise();\n\tvoid lower();\n\n\t[[nodiscard]] not_null<RpWidget*> outer() const;\n\t[[nodiscard]] rpl::producer<CallButtonColors> colorOverrides() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tenum class HandleMouseState {\n\t\tEnabled,\n\t\tBlocked,\n\t\tDisabled,\n\t};\n\tstruct RadialInfo {\n\t\tstd::optional<RadialState> state = std::nullopt;\n\t\tbool isDirectionToShow = false;\n\t\trpl::variable<float64> rawShowProgress = 0.;\n\t\tfloat64 realShowProgress = 0.;\n\t\tconst style::InfiniteRadialAnimation &st = st::callConnectingRadial;\n\t};\n\tstruct IconState {\n\t\tint index = -1;\n\t\tint frameFrom = 0;\n\t\tint frameTo = 0;\n\n\t\tinline bool operator==(const IconState &other) const {\n\t\t\treturn (index == other.index)\n\t\t\t\t&& (frameFrom == other.frameFrom)\n\t\t\t\t&& (frameTo == other.frameTo);\n\t\t}\n\t\tinline bool operator!=(const IconState &other) const {\n\t\t\treturn !(*this == other);\n\t\t}\n\n\t\tbool valid() const {\n\t\t\treturn (index >= 0);\n\t\t}\n\t\texplicit operator bool() const {\n\t\t\treturn valid();\n\t\t}\n\t};\n\n\tvoid init();\n\tvoid refreshIcons();\n\tvoid refreshGradients();\n\tvoid refreshLabels();\n\tvoid overridesColors(\n\t\tCallMuteButtonType fromType,\n\t\tCallMuteButtonType toType,\n\t\tfloat64 progress);\n\n\tvoid setHandleMouseState(HandleMouseState state);\n\tvoid updateCenterLabelGeometry(QRect my, QSize size);\n\tvoid updateLabelGeometry(QRect my, QSize size);\n\tvoid updateSublabelGeometry(QRect my, QSize size);\n\tvoid updateLabelsGeometry();\n\n\t[[nodiscard]] IconState iconStateFrom(CallMuteButtonType previous);\n\t[[nodiscard]] IconState randomWavingState();\n\t[[nodiscard]] IconState iconStateAnimated(CallMuteButtonType previous);\n\tvoid scheduleIconState(const IconState &state);\n\tvoid startIconState(const IconState &state);\n\tvoid iconAnimationCallback();\n\n\tQString tooltipText() const override;\n\tQPoint tooltipPos() const override;\n\tbool tooltipWindowActive() const override;\n\tconst style::Tooltip *tooltipSt() const override;\n\n\t[[nodiscard]] static HandleMouseState HandleMouseStateFromType(\n\t\tCallMuteButtonType type);\n\n\trpl::variable<CallMuteButtonState> _state;\n\tfloat _level = 0.;\n\tQRect _muteIconRect;\n\tHandleMouseState _handleMouseState = HandleMouseState::Enabled;\n\n\tnot_null<const style::CallMuteButton*> _st;\n\tQSize _lottieSize;\n\tint _bgSize = 0;\n\tint _bgSkip = 0;\n\n\tconst base::unique_qptr<BlobsWidget> _blobs;\n\tconst base::unique_qptr<AbstractButton> _content;\n\tbase::unique_qptr<AnimatedLabel> _centerLabel;\n\tbase::unique_qptr<AnimatedLabel> _label;\n\tbase::unique_qptr<AnimatedLabel> _sublabel;\n\tint _labelShakeShift = 0;\n\n\tRadialInfo _radialInfo;\n\tstd::unique_ptr<InfiniteRadialAnimation> _radial;\n\tconst base::flat_map<CallMuteButtonType, anim::gradient_colors> _colors;\n\tanim::linear_gradients<CallMuteButtonType> _linearGradients;\n\tanim::radial_gradients<CallMuteButtonType> _glowGradients;\n\n\tstd::array<std::optional<Lottie::Icon>, 2> _icons;\n\tIconState _iconState;\n\tstd::optional<IconState> _scheduledState;\n\n\tAnimations::Simple _switchAnimation;\n\tAnimations::Simple _shakeAnimation;\n\n\trpl::variable<CallButtonColors> _colorOverrides;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/chat_service_checkbox.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/chat_service_checkbox.h\"\n\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_layers.h\"\n\n#include <QCoreApplication>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kAnimationTimerDelta = crl::time(7);\n\nclass ServiceCheck final : public AbstractCheckView {\npublic:\n\tServiceCheck(const style::ServiceCheck &st, bool checked);\n\n\tQSize getSize() const override;\n\tvoid paint(\n\t\tQPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint outerWidth) override;\n\tQImage prepareRippleMask() const override;\n\tbool checkRippleStartPosition(QPoint position) const override;\n\nprivate:\n\tclass Generator {\n\tpublic:\n\t\tGenerator();\n\n\t\tvoid paintFrame(\n\t\t\tQPainter &p,\n\t\t\tint left,\n\t\t\tint top,\n\t\t\tnot_null<const style::ServiceCheck*> st,\n\t\t\tfloat64 toggled);\n\t\tvoid invalidate();\n\n\tprivate:\n\t\tstruct Frames {\n\t\t\tQImage image;\n\t\t\tstd::vector<bool> ready;\n\t\t};\n\n\t\tnot_null<Frames*> framesForStyle(\n\t\t\tnot_null<const style::ServiceCheck*> st);\n\t\tstatic void FillFrame(\n\t\t\tQImage &image,\n\t\t\tnot_null<const style::ServiceCheck*> st,\n\t\t\tint index,\n\t\t\tint count);\n\t\tstatic void PaintFillingFrame(\n\t\t\tQPainter &p,\n\t\t\tnot_null<const style::ServiceCheck*> st,\n\t\t\tfloat64 progress);\n\t\tstatic void PaintCheckingFrame(\n\t\t\tQPainter &p,\n\t\t\tnot_null<const style::ServiceCheck*> st,\n\t\t\tfloat64 progress);\n\n\t\tbase::flat_map<not_null<const style::ServiceCheck*>, Frames> _data;\n\t\trpl::lifetime _lifetime;\n\n\t};\n\tstatic Generator &Frames();\n\n\tconst style::ServiceCheck &_st;\n\n};\n\nServiceCheck::Generator::Generator() {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tinvalidate();\n\t}, _lifetime);\n}\n\nauto ServiceCheck::Generator::framesForStyle(\n\t\tnot_null<const style::ServiceCheck*> st) -> not_null<Frames*> {\n\tif (const auto i = _data.find(st); i != _data.end()) {\n\t\treturn &i->second;\n\t}\n\tconst auto result = &_data.emplace(st, Frames()).first->second;\n\tconst auto size = st->diameter;\n\tconst auto count = (st->duration / kAnimationTimerDelta) + 2;\n\tresult->image = QImage(\n\t\tQSize(count * size, size) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult->image.fill(Qt::transparent);\n\tresult->image.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult->ready.resize(count);\n\treturn result;\n}\n\nvoid ServiceCheck::Generator::FillFrame(\n\t\tQImage &image,\n\t\tnot_null<const style::ServiceCheck*> st,\n\t\tint index,\n\t\tint count) {\n\tExpects(count > 1);\n\tExpects(index >= 0 && index < count);\n\n\tauto p = QPainter(&image);\n\tPainterHighQualityEnabler hq(p);\n\n\tp.translate(index * st->diameter, 0);\n\tconst auto progress = index / float64(count - 1);\n\tif (progress > 0.5) {\n\t\tPaintCheckingFrame(p, st, (progress - 0.5) * 2);\n\t} else {\n\t\tPaintFillingFrame(p, st, progress * 2);\n\t}\n}\n\nvoid ServiceCheck::Generator::PaintFillingFrame(\n\t\tQPainter &p,\n\t\tnot_null<const style::ServiceCheck*> st,\n\t\tfloat64 progress) {\n\tconst auto shift = progress * st->shift;\n\tp.setBrush(st->color);\n\tp.setPen(Qt::NoPen);\n\tp.drawEllipse(QRectF(\n\t\tshift,\n\t\tshift,\n\t\tst->diameter - 2 * shift,\n\t\tst->diameter - 2 * shift));\n\tif (progress < 1.) {\n\t\tconst auto remove = progress * (st->diameter / 2. - st->thickness);\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(Qt::transparent);\n\t\tp.drawEllipse(QRectF(\n\t\t\tst->thickness + remove,\n\t\t\tst->thickness + remove,\n\t\t\tst->diameter - 2 * (st->thickness + remove),\n\t\t\tst->diameter - 2 * (st->thickness + remove)));\n\t}\n}\n\nvoid ServiceCheck::Generator::PaintCheckingFrame(\n\t\tQPainter &p,\n\t\tnot_null<const style::ServiceCheck*> st,\n\t\tfloat64 progress) {\n\tconst auto shift = (1. - progress) * st->shift;\n\tp.setBrush(st->color);\n\tp.setPen(Qt::NoPen);\n\tp.drawEllipse(QRectF(\n\t\tshift,\n\t\tshift,\n\t\tst->diameter - 2 * shift,\n\t\tst->diameter - 2 * shift));\n\tif (progress > 0.) {\n\t\tconst auto tip = QPointF(st->tip.x(), st->tip.y());\n\t\tconst auto left = tip - QPointF(st->small, st->small) * progress;\n\t\tconst auto right = tip - QPointF(-st->large, st->large) * progress;\n\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tp.setBrush(Qt::NoBrush);\n\t\tauto pen = QPen(Qt::transparent);\n\t\tpen.setWidth(st->stroke);\n\t\tpen.setCapStyle(Qt::RoundCap);\n\t\tpen.setJoinStyle(Qt::RoundJoin);\n\t\tp.setPen(pen);\n\t\tauto path = QPainterPath();\n\t\tpath.moveTo(left);\n\t\tpath.lineTo(tip);\n\t\tpath.lineTo(right);\n\t\tp.drawPath(path);\n\t}\n}\n\nvoid ServiceCheck::Generator::paintFrame(\n\t\tQPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tnot_null<const style::ServiceCheck*> st,\n\t\tfloat64 toggled) {\n\tconst auto frames = framesForStyle(st);\n\tauto &image = frames->image;\n\tconst auto count = int(frames->ready.size());\n\tconst auto index = int(base::SafeRound(toggled * (count - 1)));\n\tAssert(index >= 0 && index < count);\n\tif (!frames->ready[index]) {\n\t\tframes->ready[index] = true;\n\t\tFillFrame(image, st, index, count);\n\t}\n\tconst auto size = st->diameter;\n\tconst auto part = size * style::DevicePixelRatio();\n\tp.drawImage(\n\t\tQPoint(left, top),\n\t\timage,\n\t\tQRect(index * part, 0, part, part));\n}\n\nvoid ServiceCheck::Generator::invalidate() {\n\t_data.clear();\n}\n\nServiceCheck::Generator &ServiceCheck::Frames() {\n\tstatic const auto Instance = Ui::CreateChild<Generator>(\n\t\tQCoreApplication::instance());\n\treturn *Instance;\n}\n\nServiceCheck::ServiceCheck(\n\tconst style::ServiceCheck &st,\n\tbool checked)\n: AbstractCheckView(st.duration, checked, nullptr)\n, _st(st) {\n}\n\nQSize ServiceCheck::getSize() const {\n\tconst auto inner = QRect(0, 0, _st.diameter, _st.diameter);\n\treturn inner.marginsAdded(_st.margin).size();\n}\n\nvoid ServiceCheck::paint(\n\t\tQPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint outerWidth) {\n\tFrames().paintFrame(\n\t\tp,\n\t\tleft + _st.margin.left(),\n\t\ttop + _st.margin.top(),\n\t\t&_st,\n\t\tcurrentAnimationValue());\n}\n\nQImage ServiceCheck::prepareRippleMask() const {\n\treturn QImage();\n}\n\nbool ServiceCheck::checkRippleStartPosition(QPoint position) const {\n\treturn false;\n}\n\nvoid SetupBackground(not_null<Checkbox*> checkbox, Fn<QColor()> bg) {\n\tcheckbox->paintRequest(\n\t) | rpl::map(\n\t\tbg ? bg : [] { return st::msgServiceBg->c; }\n\t) | rpl::filter([=](const QColor &color) {\n\t\treturn color.alpha() > 0;\n\t}) | rpl::on_next([=](const QColor &color) {\n\t\tauto p = QPainter(checkbox);\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(color);\n\t\tconst auto radius = checkbox->height() / 2.;\n\t\tp.drawRoundedRect(checkbox->rect(), radius, radius);\n\t}, checkbox->lifetime());\n}\n\n} // namespace\n\n[[nodiscard]] object_ptr<Checkbox> MakeChatServiceCheckbox(\n\t\tQWidget *parent,\n\t\tconst QString &text,\n\t\tconst style::Checkbox &st,\n\t\tconst style::ServiceCheck &stCheck,\n\t\tbool checked,\n\t\tFn<QColor()> bg) {\n\tauto result = object_ptr<Checkbox>(\n\t\tparent,\n\t\ttext,\n\t\tst,\n\t\tstd::make_unique<ServiceCheck>(stCheck, checked));\n\tSetupBackground(result.data(), std::move(bg));\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/chat_service_checkbox.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nnamespace style {\nstruct Checkbox;\nstruct ServiceCheck;\n} // namespace style\n\nnamespace Ui {\n\nclass Checkbox;\n\n[[nodiscard]] object_ptr<Checkbox> MakeChatServiceCheckbox(\n\tQWidget *parent,\n\tconst QString &text,\n\tconst style::Checkbox &st,\n\tconst style::ServiceCheck &stCheck,\n\tbool checked,\n\tFn<QColor()> bg = nullptr);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/compose_ai_button_factory.h\"\n\n#include \"base/options.h\"\n#include \"boxes/compose_ai_box.h\"\n#include \"config.h\"\n#include \"core/mime_type.h\"\n#include \"history/view/controls/history_view_compose_ai_button.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"ui/chat/attach/attach_prepare.h\"\n#include \"ui/text/text.h\"\n#include \"ui/text/text_entity.h\"\n#include \"ui/widgets/fields/input_field.h\"\n\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Ui {\n\nconst char kOptionHideAiButton[] = \"hide-ai-button\";\n\nbase::options::toggle HideAiButtonOption({\n\t.id = kOptionHideAiButton,\n\t.name = \"Hide AI button\",\n\t.description = \"Hide the AI Tools button in message compose fields.\",\n});\n\nbool HasEnoughLinesForAi(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<Ui::InputField*> field) {\n\tif (HideAiButtonOption.value()\n\t\t|| session->appConfig().aiComposeStyles().empty()) {\n\t\treturn false;\n\t}\n\tconst auto &style = field->st().style;\n\tconst auto lineHeight = style.lineHeight\n\t\t? style.lineHeight\n\t\t: style.font->height;\n\tconst auto margins = field->fullTextMargins();\n\tconst auto contentHeight = field->height()\n\t\t- margins.top()\n\t\t- margins.bottom();\n\tif (contentHeight < (3 * lineHeight)) {\n\t\treturn false;\n\t}\n\tconst auto &text = field->getLastText();\n\tif (text.size() > MaxMessageSize) {\n\t\treturn false;\n\t}\n\tfor (const auto &ch : text) {\n\t\tif (!Text::IsTrimmed(ch) && !Text::IsReplacedBySpace(ch)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nPreparedList PrepareTextAsFile(const QString &text) {\n\tauto content = text.toUtf8();\n\tauto result = PreparedList();\n\tauto file = PreparedFile(QString());\n\tfile.content = content;\n\tfile.displayName = u\"message.txt\"_q;\n\tfile.size = content.size();\n\tfile.information = std::make_unique<PreparedFileInformation>();\n\tfile.information->filemime = u\"text/plain\"_q;\n\tresult.files.push_back(std::move(file));\n\treturn result;\n}\n\nconstexpr auto kSendAsFilePasteMultiplier = 8;\n\nint SendAsFilePasteThreshold() {\n\treturn kSendAsFilePasteMultiplier * MaxMessageSize;\n}\n\nLargeTextPasteResult CheckLargeTextPaste(\n\t\tnot_null<Ui::InputField*> field,\n\t\tnot_null<const QMimeData*> data) {\n\tconst auto pasteText = Core::ReadMimeText(data);\n\tif (pasteText.isEmpty()) {\n\t\treturn {};\n\t}\n\tconst auto cursor = field->textCursor();\n\tconst auto currentText = field->getLastText();\n\tconst auto selStart = cursor.selectionStart();\n\tconst auto selEnd = cursor.selectionEnd();\n\tconst auto resultingSize = currentText.size()\n\t\t- (selEnd - selStart)\n\t\t+ pasteText.size();\n\tif (resultingSize < SendAsFilePasteThreshold()) {\n\t\treturn {};\n\t}\n\treturn {\n\t\t.exceeds = true,\n\t\t.resultingText = currentText.mid(0, selStart)\n\t\t\t+ pasteText\n\t\t\t+ currentText.mid(selEnd),\n\t};\n}\n\nvoid UpdateCaptionAiButtonGeometry(\n\t\tnot_null<HistoryView::Controls::ComposeAiButton*> button,\n\t\tnot_null<Ui::InputField*> field) {\n\tif (button->isHidden()) {\n\t\treturn;\n\t}\n\tconst auto &pos = st::boxAiComposeButtonPosition;\n\tconst auto x = field->x()\n\t\t+ field->width()\n\t\t- button->width()\n\t\t+ pos.x();\n\tconst auto y = field->y()\n\t\t+ field->height()\n\t\t- button->height()\n\t\t+ pos.y();\n\tbutton->moveToLeft(x, y);\n}\n\nauto SetupCaptionAiButton(SetupCaptionAiButtonArgs &&args)\n-> not_null<HistoryView::Controls::ComposeAiButton*> {\n\tconst auto button = Ui::CreateChild<\n\t\tHistoryView::Controls::ComposeAiButton\n\t>(args.parent.get(), st::historyAiComposeButton);\n\tconst auto field = args.field;\n\tconst auto session = args.session;\n\tconst auto show = std::move(args.show);\n\tconst auto chatStyle = std::move(args.chatStyle);\n\n\tbutton->hide();\n\tbutton->setAccessibleName(tr::lng_ai_compose_title(tr::now));\n\n\tbutton->setClickedCallback(crl::guard(field, [=] {\n\t\tconst auto textWithTags = field->getTextWithAppliedMarkdown();\n\t\tif (textWithTags.text.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tauto text = TextWithEntities{\n\t\t\ttextWithTags.text,\n\t\t\tTextUtilities::ConvertTextTagsToEntities(textWithTags.tags),\n\t\t};\n\t\tHistoryView::Controls::ShowComposeAiBox(show, {\n\t\t\t.session = session,\n\t\t\t.text = std::move(text),\n\t\t\t.chatStyle = chatStyle,\n\t\t\t.apply = crl::guard(field, [=](TextWithEntities result) {\n\t\t\t\tfield->setTextWithTags(\n\t\t\t\t\t{\n\t\t\t\t\t\tresult.text,\n\t\t\t\t\t\tTextUtilities::ConvertEntitiesToTextTags(\n\t\t\t\t\t\t\tresult.entities),\n\t\t\t\t\t},\n\t\t\t\t\tUi::InputField::HistoryAction::NewEntry);\n\t\t\t}),\n\t\t});\n\t}));\n\n\tconst auto updateVisibility = [=] {\n\t\tconst auto visible = !field->isHidden()\n\t\t\t&& HasEnoughLinesForAi(session, field);\n\t\tbutton->setVisible(visible);\n\t\tif (visible) {\n\t\t\tUpdateCaptionAiButtonGeometry(button, field);\n\t\t\tbutton->raise();\n\t\t}\n\t};\n\n\trpl::merge(\n\t\tfield->heightChanges() | rpl::to_empty,\n\t\tfield->changes() | rpl::to_empty,\n\t\tfield->shownValue() | rpl::to_empty\n\t) | rpl::on_next([=] {\n\t\tupdateVisibility();\n\t}, button->lifetime());\n\n\treturn button;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/compose_ai_button_factory.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass QMimeData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass ChatStyle;\nclass InputField;\nclass Show;\n} // namespace Ui\n\nnamespace HistoryView::Controls {\nclass ComposeAiButton;\n} // namespace HistoryView::Controls\n\nnamespace Ui {\n\nstruct PreparedList;\n\nextern const char kOptionHideAiButton[];\n\n[[nodiscard]] bool HasEnoughLinesForAi(\n\tnot_null<Main::Session*> session,\n\tnot_null<Ui::InputField*> field);\n\nstruct SetupCaptionAiButtonArgs {\n\tnot_null<QWidget*> parent;\n\tnot_null<Ui::InputField*> field;\n\tnot_null<Main::Session*> session;\n\tstd::shared_ptr<Ui::Show> show;\n\tstd::shared_ptr<Ui::ChatStyle> chatStyle;\n};\n\n[[nodiscard]] auto SetupCaptionAiButton(SetupCaptionAiButtonArgs &&args)\n-> not_null<HistoryView::Controls::ComposeAiButton*>;\n\nvoid UpdateCaptionAiButtonGeometry(\n\tnot_null<HistoryView::Controls::ComposeAiButton*> button,\n\tnot_null<Ui::InputField*> field);\n\n[[nodiscard]] PreparedList PrepareTextAsFile(const QString &text);\n[[nodiscard]] int SendAsFilePasteThreshold();\n\nstruct LargeTextPasteResult {\n\tbool exceeds = false;\n\tQString resultingText;\n};\n\n[[nodiscard]] LargeTextPasteResult CheckLargeTextPaste(\n\tnot_null<Ui::InputField*> field,\n\tnot_null<const QMimeData*> data);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/delete_message_context_action.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/delete_message_context_action.h\"\n\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"lang/lang_keys.h\"\n#include \"base/call_delayed.h\"\n#include \"base/unixtime.h\"\n#include \"base/timer.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Ui {\nnamespace {\n\nclass ActionWithTimer final : public Menu::ItemBase {\npublic:\n\tActionWithTimer(\n\t\tnot_null<Menu::Menu*> parent,\n\t\tconst style::Menu &st,\n\t\tTimeId destroyAt,\n\t\tFn<void()> callback,\n\t\tFn<void()> destroyByTimerCallback);\n\n\tbool isEnabled() const override;\n\tnot_null<QAction*> action() const override;\n\n\tvoid handleKeyPress(not_null<QKeyEvent*> e) override;\n\nprivate:\n\tQPoint prepareRippleStartPosition() const override;\n\tQImage prepareRippleMask() const override;\n\n\tint contentHeight() const override;\n\n\tvoid prepare();\n\tvoid refreshAutoDeleteText();\n\tvoid paint(Painter &p);\n\n\tconst not_null<QAction*> _dummyAction;\n\tconst style::Menu &_st;\n\tconst TimeId _destroyAt = 0;\n\tconst Fn<void()> _destroyByTimerCallback;\n\tconst crl::time _startedAt = 0;\n\tbase::Timer _refreshTimer;\n\n\tText::String _text;\n\tint _textWidth = 0;\n\tQString _autoDeleteText;\n\tconst int _height;\n\n};\n\nTextParseOptions MenuTextOptions = {\n\tTextParseLinks, // flags\n\t0, // maxw\n\t0, // maxh\n\tQt::LayoutDirectionAuto, // dir\n};\n\nActionWithTimer::ActionWithTimer(\n\tnot_null<Menu::Menu*> parent,\n\tconst style::Menu &st,\n\tTimeId destroyAt,\n\tFn<void()> callback,\n\tFn<void()> destroyByTimerCallback)\n: ItemBase(parent, st)\n, _dummyAction(new QAction(parent))\n, _st(st)\n, _destroyAt(destroyAt)\n, _destroyByTimerCallback(destroyByTimerCallback)\n, _startedAt(crl::now())\n, _refreshTimer([=] { refreshAutoDeleteText(); })\n, _height(st::ttlItemPadding.top()\n\t\t+ _st.itemStyle.font->height\n\t\t+ st::ttlItemTimerFont->height\n\t\t+ st::ttlItemPadding.bottom()) {\n\tsetAcceptBoth(true);\n\tfitToMenuWidth();\n\tsetActionTriggered(std::move(callback));\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(this);\n\t\tpaint(p);\n\t}, lifetime());\n\n\tenableMouseSelecting();\n\tprepare();\n}\n\nvoid ActionWithTimer::paint(Painter &p) {\n\tconst auto selected = isSelected();\n\tif (selected && _st.itemBgOver->c.alpha() < 255) {\n\t\tp.fillRect(0, 0, width(), _height, _st.itemBg);\n\t}\n\tp.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg);\n\tif (isEnabled()) {\n\t\tpaintRipple(p, 0, 0);\n\t}\n\n\tconst auto normalHeight = _st.itemPadding.top()\n\t\t+ _st.itemStyle.font->height\n\t\t+ _st.itemPadding.bottom();\n\tconst auto deltaHeight = _height - normalHeight;\n\tst::menuIconDelete.paint(\n\t\tp,\n\t\t_st.itemIconPosition + QPoint(0, deltaHeight / 2),\n\t\twidth());\n\n\tp.setPen(selected ? _st.itemFgOver : _st.itemFg);\n\t_text.drawLeftElided(\n\t\tp,\n\t\t_st.itemPadding.left(),\n\t\tst::ttlItemPadding.top(),\n\t\t_textWidth,\n\t\twidth());\n\n\tp.setFont(st::ttlItemTimerFont);\n\tp.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);\n\tp.drawTextLeft(\n\t\t_st.itemPadding.left(),\n\t\tst::ttlItemPadding.top() + _st.itemStyle.font->height,\n\t\twidth(),\n\t\t_autoDeleteText);\n}\n\nvoid ActionWithTimer::refreshAutoDeleteText() {\n\tconst auto now = base::unixtime::now();\n\tconst auto left = (_destroyAt > now) ? (_destroyAt - now) : 0;\n\tconst auto text = [&] {\n\t\tconst auto duration = (left >= 86400)\n\t\t\t? tr::lng_days(tr::now, lt_count, ((left + 43200) / 86400))\n\t\t\t: (left >= 3600)\n\t\t\t? QString(\"%1:%2:%3\"\n\t\t\t).arg(left / 3600\n\t\t\t).arg((left % 3600) / 60, 2, 10, QChar('0')\n\t\t\t).arg(left % 60, 2, 10, QChar('0'))\n\t\t\t: QString(\"%1:%2\"\n\t\t\t).arg(left / 60\n\t\t\t).arg(left % 60, 2, 10, QChar('0'));\n\t\treturn tr::lng_context_auto_delete_in(\n\t\t\ttr::now,\n\t\t\tlt_duration,\n\t\t\tduration);\n\t}();\n\tif (_autoDeleteText != text) {\n\t\t_autoDeleteText = text;\n\t\tupdate();\n\t}\n\n\tif (!left) {\n\t\tbase::call_delayed(crl::time(100), this, _destroyByTimerCallback);\n\t\treturn;\n\t}\n\tconst auto nextCall = (left >= 86400)\n\t\t? ((left % 43200) + 1) * crl::time(1000)\n\t\t: crl::time(500) - ((crl::now() - _startedAt) % 500);\n\t_refreshTimer.callOnce(nextCall);\n}\n\nvoid ActionWithTimer::prepare() {\n\trefreshAutoDeleteText();\n\n\t_text.setMarkedText(\n\t\t_st.itemStyle,\n\t\t{ tr::lng_context_delete_msg(tr::now) },\n\t\tMenuTextOptions);\n\tconst auto textWidth = _text.maxWidth();\n\tconst auto &padding = _st.itemPadding;\n\n\tconst auto goodWidth = padding.left()\n\t\t+ textWidth\n\t\t+ padding.right();\n\tconst auto ttlMaxWidth = [&](const QString &duration) {\n\t\treturn padding.left()\n\t\t\t+ st::ttlItemTimerFont->width(tr::lng_context_auto_delete_in(\n\t\t\t\ttr::now,\n\t\t\t\tlt_duration,\n\t\t\t\tduration))\n\t\t\t+ padding.right();\n\t};\n\tconst auto maxWidth1 = ttlMaxWidth(\"23:59:59\");\n\tconst auto maxWidth2 = ttlMaxWidth(tr::lng_days(tr::now, lt_count, 7));\n\n\tconst auto w = std::clamp(\n\t\tstd::max({ goodWidth, maxWidth1, maxWidth2 }),\n\t\t_st.widthMin,\n\t\t_st.widthMax);\n\t_textWidth = w - (goodWidth - textWidth);\n\tsetMinWidth(w);\n\tupdate();\n}\n\nbool ActionWithTimer::isEnabled() const {\n\treturn true;\n}\n\nnot_null<QAction*> ActionWithTimer::action() const {\n\treturn _dummyAction;\n}\n\nQPoint ActionWithTimer::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos());\n}\n\nQImage ActionWithTimer::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::RectMask(size());\n}\n\nint ActionWithTimer::contentHeight() const {\n\treturn _height;\n}\n\nvoid ActionWithTimer::handleKeyPress(not_null<QKeyEvent*> e) {\n\tif (!isSelected()) {\n\t\treturn;\n\t}\n\tconst auto key = e->key();\n\tif (key == Qt::Key_Enter || key == Qt::Key_Return) {\n\t\tsetClicked(Menu::TriggeredSource::Keyboard);\n\t}\n}\n\n} // namespace\n\nbase::unique_qptr<Menu::ItemBase> DeleteMessageContextAction(\n\t\tnot_null<Menu::Menu*> menu,\n\t\tFn<void()> callback,\n\t\tTimeId destroyAt,\n\t\tFn<void()> destroyByTimerCallback) {\n\tif (destroyAt <= 0) {\n\t\treturn base::make_unique_q<Menu::Action>(\n\t\t\tmenu,\n\t\t\tmenu->st(),\n\t\t\tMenu::CreateAction(\n\t\t\t\tmenu,\n\t\t\t\ttr::lng_context_delete_msg(tr::now),\n\t\t\t\tstd::move(callback)),\n\t\t\t&st::menuIconDelete,\n\t\t\t&st::menuIconDelete);\n\t}\n\treturn base::make_unique_q<ActionWithTimer>(\n\t\tmenu,\n\t\tmenu->st(),\n\t\tdestroyAt,\n\t\tstd::move(callback),\n\t\tstd::move(destroyByTimerCallback));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/delete_message_context_action.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n\nnamespace Ui {\nnamespace Menu {\nclass Menu;\nclass ItemBase;\n} // namespace Menu\n\nclass PopupMenu;\n\n[[nodiscard]] base::unique_qptr<Menu::ItemBase> DeleteMessageContextAction(\n\tnot_null<Menu::Menu*> menu,\n\tFn<void()> callback,\n\tTimeId destroyAt,\n\tFn<void()> destroyByTimerCallback);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/download_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/download_bar.h\"\n\n#include \"ui/widgets/buttons.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Ui {\nnamespace {\n\n[[nodiscard]] QImage Make(const QImage &image, int size) {\n\tif (image.isNull()) {\n\t\treturn QImage();\n\t}\n\tauto result = image.scaledToWidth(\n\t\tsize * style::DevicePixelRatio(),\n\t\tQt::SmoothTransformation);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\treturn result;\n}\n\n} // namespace\n\nDownloadBar::DownloadBar(\n\tnot_null<QWidget*> parent,\n\trpl::producer<DownloadBarProgress> progress)\n: _button(\n\tparent,\n\tobject_ptr<RippleButton>(parent, st::dialogsMenuToggle.ripple))\n, _shadow(parent)\n, _progress(std::move(progress))\n, _radial([=](crl::time now) { radialAnimationCallback(now); }) {\n\t_button.hide(anim::type::instant);\n\t_shadow.showOn(_button.shownValue());\n\t_button.setDirectionUp(false);\n\t_button.entity()->resize(0, st::downloadBarHeight);\n\t_button.entity()->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = Painter(_button.entity());\n\t\tpaint(p, clip);\n\t}, lifetime());\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\trefreshIcon();\n\t}, lifetime());\n\trefreshIcon();\n\n\t_progress.value(\n\t) | rpl::on_next([=](const DownloadBarProgress &progress) {\n\t\trefreshInfo(progress);\n\t}, lifetime());\n}\n\nDownloadBar::~DownloadBar() = default;\n\nvoid DownloadBar::show(DownloadBarContent &&content) {\n\t_button.toggle(content.count > 0, anim::type::normal);\n\tif (!content.count) {\n\t\treturn;\n\t}\n\tif (!_radial.animating()) {\n\t\t_radial.start(computeProgress());\n\t}\n\t_content = content;\n\tconst auto finished = (_content.done == _content.count);\n\tif (_finished != finished) {\n\t\t_finished = finished;\n\t\t_finishedAnimation.start(\n\t\t\t[=] { _button.update(); },\n\t\t\t_finished ? 0. : 1.,\n\t\t\t_finished ? 1. : 0.,\n\t\t\tst::widgetFadeDuration);\n\t}\n\trefreshThumbnail();\n\t_title.setMarkedText(\n\t\tst::defaultTextStyle,\n\t\t(content.count > 1\n\t\t\t? tr::bold(\n\t\t\t\ttr::lng_profile_files(tr::now, lt_count, content.count))\n\t\t\t: content.singleName));\n\trefreshInfo(_progress.current());\n}\n\nvoid DownloadBar::refreshThumbnail() {\n\tif (_content.singleThumbnail.isNull()) {\n\t\t_thumbnail = _thumbnailDone = QImage();\n\t\t_thumbnailCacheKey = 0;\n\t\treturn;\n\t}\n\tconst auto cacheKey = _content.singleThumbnail.cacheKey();\n\tif (_thumbnailCacheKey == cacheKey) {\n\t\treturn;\n\t}\n\t_thumbnailCacheKey = cacheKey;\n\t_thumbnailLarge = _content.singleThumbnail;\n\t_thumbnailLarge.detach();\n\tconst auto width = _thumbnailLarge.width();\n\tconst auto height = _thumbnailLarge.height();\n\tif (width != height) {\n\t\tconst auto size = std::min(width, height);\n\t\t_thumbnailLarge = _thumbnailLarge.copy(\n\t\t\t(width - size) / 2,\n\t\t\t(height - size) / 2,\n\t\t\tsize,\n\t\t\tsize);\n\t}\n\tconst auto size = st::downloadLoadingSize;\n\tconst auto added = 3 * st::downloadLoadingLine;\n\tconst auto loadingsize = size;\n\tconst auto donesize = size + (added - st::downloadLoadingLine) * 2;\n\tconst auto make = [&](int size) {\n\t\treturn Images::Circle(Make(_thumbnailLarge, size));\n\t};\n\t_thumbnail = make(loadingsize);\n\t_thumbnailDone = make(donesize);\n\t_thumbnailLarge = Images::Circle(std::move(_thumbnailLarge));\n}\n\nvoid DownloadBar::refreshIcon() {\n\t_documentIconLarge = st::downloadIconDocument.instance(\n\t\tst::windowFgActive->c,\n\t\tstyle::kScaleMax / style::DevicePixelRatio());\n\t_documentIcon = Make(_documentIconLarge, st::downloadIconSize);\n\t_documentIconDone = Make(_documentIconLarge, st::downloadIconSizeDone);\n}\n\nvoid DownloadBar::refreshInfo(const DownloadBarProgress &progress) {\n\t_info.setMarkedText(\n\t\tst::downloadInfoStyle,\n\t\t(progress.ready < progress.total\n\t\t\t? tr::marked(\n\t\t\t\tFormatDownloadText(progress.ready, progress.total))\n\t\t\t: Text::Link((_content.count > 1)\n\t\t\t\t? tr::lng_downloads_view_in_section(tr::now)\n\t\t\t\t: tr::lng_downloads_view_in_chat(tr::now))));\n\t_button.entity()->update();\n}\n\nbool DownloadBar::isHidden() const {\n\treturn _button.isHidden();\n}\n\nint DownloadBar::height() const {\n\treturn _button.height();\n}\n\nrpl::producer<int> DownloadBar::heightValue() const {\n\treturn _button.heightValue();\n}\n\nrpl::producer<bool> DownloadBar::shownValue() const {\n\treturn _button.shownValue();\n}\n\nvoid DownloadBar::setGeometry(int left, int top, int width, int height) {\n\t_button.resizeToWidth(width);\n\t_button.moveToLeft(left, top);\n\t_shadow.setGeometry(left, top - st::lineWidth, width, st::lineWidth);\n}\n\nrpl::producer<> DownloadBar::clicks() const {\n\treturn _button.entity()->clicks() | rpl::to_empty;\n}\n\nrpl::lifetime &DownloadBar::lifetime() {\n\treturn _button.lifetime();\n}\n\nvoid DownloadBar::paint(Painter &p, QRect clip) {\n\tconst auto button = _button.entity();\n\tconst auto outerw = button->width();\n\tconst auto over = button->isOver() || button->isDown();\n\tconst auto &icon = over ? st::downloadArrowOver : st::downloadArrow;\n\tp.fillRect(clip, st::windowBg);\n\tbutton->paintRipple(p, 0, 0);\n\n\tconst auto finished = _finishedAnimation.value(_finished ? 1. : 0.);\n\tconst auto size = st::downloadLoadingSize;\n\tconst auto added = 3 * st::downloadLoadingLine;\n\tconst auto skipx = st::downloadLoadingLeft;\n\tconst auto skipy = (button->height() - size) / 2;\n\tconst auto full = QRect(\n\t\tskipx - added,\n\t\tskipy - added,\n\t\tsize + added * 2,\n\t\tsize + added * 2);\n\tif (full.intersects(clip)) {\n\t\tconst auto done = (finished == 1.);\n\t\tconst auto loading = _radial.computeState();\n\t\tif (loading.shown > 0) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setOpacity(loading.shown);\n\t\t\tauto pen = st::windowBgActive->p;\n\t\t\tpen.setWidth(st::downloadLoadingLine);\n\t\t\tp.setPen(pen);\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\tconst auto m = added / 2.;\n\t\t\tauto rect = QRectF(full).marginsRemoved({ m, m, m, m });\n\t\t\tif (loading.arcLength < arc::kFullLength) {\n\t\t\t\tp.drawArc(rect, loading.arcFrom, loading.arcLength);\n\t\t\t} else {\n\t\t\t\tp.drawEllipse(rect);\n\t\t\t}\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t\tconst auto shift = st::downloadLoadingLine\n\t\t\t+ (1. - finished) * (added - st::downloadLoadingLine);\n\t\tconst auto ellipse = QRectF(full).marginsRemoved(\n\t\t\t{ shift, shift, shift, shift });\n\t\tif (_thumbnail.isNull()) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(st::windowBgActive);\n\t\t\tp.drawEllipse(ellipse);\n\t\t\tconst auto sizeLoading = st::downloadIconSize;\n\t\t\tif (finished == 0. || done) {\n\t\t\t\tconst auto size = done\n\t\t\t\t\t? st::downloadIconSizeDone\n\t\t\t\t\t: sizeLoading;\n\t\t\t\tconst auto image = done ? _documentIconDone : _documentIcon;\n\t\t\t\tp.drawImage(\n\t\t\t\t\tfull.x() + (full.width() - size) / 2,\n\t\t\t\t\tfull.y() + (full.height() - size) / 2,\n\t\t\t\t\timage);\n\t\t\t} else {\n\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\tconst auto size = sizeLoading\n\t\t\t\t\t+ (st::downloadIconSizeDone - sizeLoading) * finished;\n\t\t\t\tp.drawImage(\n\t\t\t\t\tQRectF(\n\t\t\t\t\t\tfull.x() + (full.width() - size) / 2.,\n\t\t\t\t\t\tfull.y() + (full.height() - size) / 2.,\n\t\t\t\t\t\tsize,\n\t\t\t\t\t\tsize),\n\t\t\t\t\t_documentIconLarge);\n\t\t\t}\n\t\t} else if (finished == 0. || done) {\n\t\t\tp.drawImage(\n\t\t\t\tbase::SafeRound(ellipse.x()),\n\t\t\t\tbase::SafeRound(ellipse.y()),\n\t\t\t\tdone ? _thumbnailDone : _thumbnail);\n\t\t} else {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.drawImage(ellipse, _thumbnailLarge);\n\t\t}\n\t}\n\n\tconst auto minleft = std::min(\n\t\tst::downloadTitleLeft,\n\t\tst::downloadInfoLeft);\n\tconst auto maxwidth = outerw - minleft;\n\tif (!clip.intersects({ minleft, 0, maxwidth, st::downloadBarHeight })) {\n\t\treturn;\n\t}\n\tconst auto right = st::downloadArrowRight + icon.width();\n\tconst auto available = button->width() - st::downloadTitleLeft - right;\n\tp.setPen(st::windowBoldFg);\n\t_title.drawLeftElided(\n\t\tp,\n\t\tst::downloadTitleLeft,\n\t\tst::downloadTitleTop,\n\t\tavailable,\n\t\touterw);\n\n\tp.setPen(st::windowSubTextFg);\n\tp.setTextPalette(st::defaultTextPalette);\n\t_info.drawLeftElided(\n\t\tp,\n\t\tst::downloadInfoLeft,\n\t\tst::downloadInfoTop,\n\t\tavailable,\n\t\touterw);\n\n\tconst auto iconTop = (st::downloadBarHeight - icon.height()) / 2;\n\ticon.paint(p, outerw - right, iconTop, outerw);\n}\n\nfloat64 DownloadBar::computeProgress() const {\n\tconst auto now = _progress.current();\n\treturn now.total ? (now.ready / float64(now.total)) : 0.;\n}\n\nvoid DownloadBar::radialAnimationCallback(crl::time now) {\n\tconst auto finished = (_content.done == _content.count);\n\tconst auto updated = _radial.update(computeProgress(), finished, now);\n\tif (!anim::Disabled() || updated) {\n\t\tconst auto button = _button.entity();\n\t\tconst auto size = st::downloadLoadingSize;\n\t\tconst auto added = 3 * st::downloadLoadingLine;\n\t\tconst auto skipx = st::downloadLoadingLeft;\n\t\tconst auto skipy = (button->height() - size) / 2;\n\t\tconst auto full = QRect(\n\t\t\tskipx - added,\n\t\t\tskipy - added,\n\t\t\tsize + added * 2,\n\t\t\tsize + added * 2);\n\t\tbutton->update(full);\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/download_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/text/text.h\"\n\nnamespace Ui {\n\nclass RippleButton;\n\nstruct DownloadBarProgress {\n\tint64 ready = 0;\n\tint64 total = 0;\n};\n\nstruct DownloadBarContent {\n\tTextWithEntities singleName;\n\tQImage singleThumbnail;\n\tint count = 0;\n\tint done = 0;\n};\n\nclass DownloadBar final {\npublic:\n\tDownloadBar(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<DownloadBarProgress> progress);\n\t~DownloadBar();\n\n\tvoid show(DownloadBarContent &&content);\n\n\t[[nodiscard]] bool isHidden() const;\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] rpl::producer<int> heightValue() const;\n\t[[nodiscard]] rpl::producer<bool> shownValue() const;\n\tvoid setGeometry(int left, int top, int width, int height);\n\n\t[[nodiscard]] rpl::producer<> clicks() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tvoid paint(Painter &p, QRect clip);\n\tvoid refreshIcon();\n\tvoid refreshThumbnail();\n\tvoid refreshInfo(const DownloadBarProgress &progress);\n\tvoid radialAnimationCallback(crl::time now);\n\t[[nodiscard]] float64 computeProgress() const;\n\n\tSlideWrap<RippleButton> _button;\n\tPlainShadow _shadow;\n\tDownloadBarContent _content;\n\trpl::variable<DownloadBarProgress> _progress;\n\tUi::Animations::Simple _finishedAnimation;\n\tbool _finished = false;\n\tQImage _documentIconLarge;\n\tQImage _documentIcon;\n\tQImage _documentIconDone;\n\tqint64 _thumbnailCacheKey = 0;\n\tQImage _thumbnailLarge;\n\tQImage _thumbnail;\n\tQImage _thumbnailDone;\n\tText::String _title;\n\tText::String _info;\n\tRadialAnimation _radial;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp",
    "content": "// This file is part of Telegram Desktop,\n// the official desktop application for the Telegram messaging service.\n//\n// For license and copyright information please follow this link:\n// https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n//\n#include \"ui/controls/dynamic_images_strip.h\"\n\n#include \"ui/dynamic_image.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kDimmedAlpha = 0.6;\nconstexpr auto kHoverScaleIncrease = st::topPeersSelectorUserpicExpand;\n\n} // namespace\n\nDynamicImagesStrip::DynamicImagesStrip(\n\tQWidget *parent,\n\tstd::vector<std::shared_ptr<DynamicImage>> thumbnails,\n\tint userpicSize,\n\tint gap)\n: RpWidget(parent)\n, _thumbnails(std::move(thumbnails))\n, _userpicSize(userpicSize)\n, _gap(gap)\n, _scales(_thumbnails.size(), 0.)\n, _alphas(_thumbnails.size(), 1.)\n, _scaleTargets(_thumbnails.size(), 0.)\n, _alphaTargets(_thumbnails.size(), 1.)\n, _animation([=](crl::time now) {\n\tconst auto dt = (now - _animation.started())\n\t\t/ float64(st::universalDuration);\n\tauto finished = true;\n\tfor (auto i = 0; i < int(_thumbnails.size()); ++i) {\n\t\tconst auto progress = std::clamp(dt, 0., 1.);\n\t\t_scales[i] += (_scaleTargets[i] - _scales[i]) * progress;\n\t\t_alphas[i] += (_alphaTargets[i] - _alphas[i]) * progress;\n\t\tif (std::abs(_scales[i] - _scaleTargets[i]) > 0.01\n\t\t\t|| std::abs(_alphas[i] - _alphaTargets[i]) > 0.01) {\n\t\t\tfinished = false;\n\t\t}\n\t}\n\tupdate();\n\treturn !finished;\n}) {\n\tfor (const auto &thumbnail : _thumbnails) {\n\t\tthumbnail->subscribeToUpdates([=] { update(); });\n\t}\n\tsetMouseTracking(true);\n}\n\nvoid DynamicImagesStrip::setProgress(float64 progress) {\n\t_progress = progress;\n\tupdate();\n}\n\nvoid DynamicImagesStrip::setClickCallback(Fn<void(int)> callback) {\n\t_clickCallback = std::move(callback);\n}\n\nbool DynamicImagesStrip::hasMouseMoved() const {\n\treturn _motions > 6;\n}\n\nvoid DynamicImagesStrip::mouseMoved() {\n\t_motions++;\n}\n\nrpl::producer<HoveredItemInfo> DynamicImagesStrip::hoveredItemValue() const {\n\treturn _hoveredItem.events();\n}\n\nvoid DynamicImagesStrip::handleKeyPressEvent(QKeyEvent *e) {\n\tkeyPressEvent(e);\n}\n\nvoid DynamicImagesStrip::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\t{\n\t\tconst auto shift = (height() - _userpicSize) / 2;\n\t\tp.translate(shift, shift);\n\t}\n\tauto x = 0;\n\tconst auto count = int(_thumbnails.size());\n\tconst auto duration = 0.25;\n\tconst auto overlap = 0.15;\n\tauto hq = PainterHighQualityEnabler(p);\n\tfor (auto i = 0; i < count; ++i) {\n\t\tconst auto center = count / 2;\n\t\tconst auto offset = (i <= center)\n\t\t\t? (center - i)\n\t\t\t: (i - center);\n\t\tconst auto start = offset * (duration - overlap);\n\t\tconst auto scale = std::clamp(\n\t\t\t(_progress - start) / duration,\n\t\t\t0.,\n\t\t\t1.);\n\t\tif (scale > 0.) {\n\t\t\tconst auto hoverScale = 1. + kHoverScaleIncrease * _scales[i];\n\t\t\tconst auto alpha = _alphas[i];\n\t\t\tconst auto cx = x + _userpicSize / 2;\n\t\t\tconst auto cy = height() / 2;\n\t\t\tp.save();\n\t\t\tp.setOpacity(alpha);\n\t\t\tp.translate(cx, cy);\n\t\t\tp.scale(scale * hoverScale, scale * hoverScale);\n\t\t\tp.translate(-cx, -cy);\n\t\t\tconst auto image = _thumbnails[i]->image(_userpicSize);\n\t\t\tp.drawImage(x, 0, image);\n\t\t\tp.restore();\n\t\t}\n\t\tx += _userpicSize + _gap;\n\t}\n}\n\nvoid DynamicImagesStrip::mouseMoveEvent(QMouseEvent *e) {\n\tmouseMoved();\n\tif (!hasMouseMoved()) {\n\t\treturn;\n\t}\n\tconst auto pos = e->pos().x();\n\tconst auto step = _userpicSize + _gap;\n\tconst auto count = int(_thumbnails.size());\n\tauto newIndex = -1;\n\tfor (auto i = 0; i < count; ++i) {\n\t\tconst auto start = i * step;\n\t\tconst auto end = start + _userpicSize + _gap / 2;\n\t\tif (pos >= start && pos < end) {\n\t\t\tnewIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tsetSelectedIndex(newIndex);\n}\n\nvoid DynamicImagesStrip::mousePressEvent(QMouseEvent *e) {\n\tif (!hasMouseMoved()) {\n\t\treturn;\n\t}\n\t_pressed = true;\n\tif (_hoveredIndex >= 0 && _clickCallback) {\n\t\t_clickCallback(_hoveredIndex);\n\t}\n}\n\nvoid DynamicImagesStrip::mouseReleaseEvent(QMouseEvent *e) {\n\tif (!hasMouseMoved()) {\n\t\treturn;\n\t}\n\tif (!_pressed && _hoveredIndex >= 0 && _clickCallback) {\n\t\t_clickCallback(_hoveredIndex);\n\t}\n}\n\nvoid DynamicImagesStrip::leaveEventHook(QEvent *e) {\n\tsetSelectedIndex(-1);\n}\n\nvoid DynamicImagesStrip::startAnimation() {\n\tif (!_animation.animating()) {\n\t\t_animation.start();\n\t}\n}\n\nvoid DynamicImagesStrip::updateHoveredItem(int index) {\n\tif (index < 0) {\n\t\t_hoveredItem.fire({ .index = -1, .globalPos = {} });\n\t\treturn;\n\t}\n\tconst auto step = _userpicSize + _gap;\n\tconst auto shift = (height() - _userpicSize) / 2;\n\tconst auto x = index * step + shift;\n\tconst auto avatarRect = QRect(x, shift, _userpicSize, _userpicSize);\n\tconst auto globalRect = QRect(\n\t\tmapToGlobal(avatarRect.topLeft()),\n\t\tavatarRect.size());\n\t_hoveredItem.fire({\n\t\t.index = index,\n\t\t.globalPos = globalRect.center(),\n\t});\n}\n\nvoid DynamicImagesStrip::keyPressEvent(QKeyEvent *e) {\n\tconst auto count = int(_thumbnails.size());\n\tif (count == 0) {\n\t\treturn;\n\t}\n\tconst auto key = e->key();\n\tif (key == Qt::Key_Left || key == Qt::Key_Up) {\n\t\tconst auto newIndex = (_hoveredIndex < 0)\n\t\t\t? (count - 1)\n\t\t\t: ((_hoveredIndex - 1 + count) % count);\n\t\tsetSelectedIndex(newIndex);\n\t} else if (key == Qt::Key_Right || key == Qt::Key_Down) {\n\t\tconst auto newIndex = (_hoveredIndex < 0)\n\t\t\t? 0\n\t\t\t: ((_hoveredIndex + 1) % count);\n\t\tsetSelectedIndex(newIndex);\n\t} else if ((key == Qt::Key_Return\n\t\t\t|| key == Qt::Key_Enter\n\t\t\t|| key == Qt::Key_Space)\n\t\t&& _hoveredIndex >= 0 && _clickCallback) {\n\t\t_clickCallback(_hoveredIndex);\n\t}\n}\n\nvoid DynamicImagesStrip::setSelectedIndex(int index) {\n\tif (_hoveredIndex == index) {\n\t\treturn;\n\t}\n\tconst auto prev = _hoveredIndex;\n\tconst auto count = int(_thumbnails.size());\n\t_hoveredIndex = index;\n\tif (prev >= 0) {\n\t\t_scaleTargets[prev] = 0.;\n\t\t_alphaTargets[prev] = kDimmedAlpha;\n\t}\n\tif (index >= 0) {\n\t\t_scaleTargets[index] = 1.;\n\t\t_alphaTargets[index] = 1.;\n\t\tfor (auto i = 0; i < count; ++i) {\n\t\t\tif (i != index && _alphaTargets[i] > kDimmedAlpha) {\n\t\t\t\t_alphaTargets[i] = kDimmedAlpha;\n\t\t\t}\n\t\t}\n\t\tupdateHoveredItem(index);\n\t} else {\n\t\tfor (auto i = 0; i < count; ++i) {\n\t\t\t_scaleTargets[i] = 0.;\n\t\t\t_alphaTargets[i] = 1.;\n\t\t}\n\t\tupdateHoveredItem(-1);\n\t}\n\tstartAnimation();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/dynamic_images_strip.h",
    "content": "// This file is part of Telegram Desktop,\n// the official desktop application for the Telegram messaging service.\n//\n// For license and copyright information please follow this link:\n// https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n//\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace Ui {\n\nclass DynamicImage;\n\nstruct HoveredItemInfo {\n\tint index = -1;\n\tQPoint globalPos;\n};\n\nclass DynamicImagesStrip final : public RpWidget {\npublic:\n\tDynamicImagesStrip(\n\t\tQWidget *parent,\n\t\tstd::vector<std::shared_ptr<DynamicImage>> thumbnails,\n\t\tint userpicSize,\n\t\tint gap);\n\n\tvoid setProgress(float64 progress);\n\tvoid setClickCallback(Fn<void(int)> callback);\n\t[[nodiscard]] rpl::producer<HoveredItemInfo> hoveredItemValue() const;\n\n\tvoid handleKeyPressEvent(QKeyEvent *e);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tvoid startAnimation();\n\tvoid updateHoveredItem(int index);\n\tvoid setSelectedIndex(int index);\n\n\t[[nodiscard]] bool hasMouseMoved() const;\n\tvoid mouseMoved();\n\n\tstd::vector<std::shared_ptr<DynamicImage>> _thumbnails;\n\tint _userpicSize = 0;\n\tint _gap = 0;\n\tfloat64 _progress = 0.;\n\tint _hoveredIndex = -1;\n\tint _motions = 0;\n\tstd::vector<float64> _scales;\n\tstd::vector<float64> _alphas;\n\tstd::vector<float64> _scaleTargets;\n\tstd::vector<float64> _alphaTargets;\n\tAnimations::Basic _animation;\n\tFn<void(int)> _clickCallback;\n\trpl::event_stream<HoveredItemInfo> _hoveredItem;\n\tbool _pressed = false;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/emoji_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/emoji_button.h\"\n\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Ui {\n\nEmojiButton::EmojiButton(QWidget *parent, const style::EmojiButton &st)\n: RippleButton(parent, st.inner.ripple)\n, _st(st) {\n\tresize(_st.inner.width, _st.inner.height);\n\tsetCursor(style::cur_pointer);\n}\n\nvoid EmojiButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tp.fillRect(e->rect(), _st.bg);\n\tconst auto &st = _st.inner;\n\tpaintRipple(p, st.rippleAreaPosition.x(), st.rippleAreaPosition.y(), _rippleOverride ? &(*_rippleOverride)->c : nullptr);\n\n\tconst auto over = isOver();\n\tconst auto loadingState = _loading\n\t\t? _loading->computeState()\n\t\t: RadialState{ 0., 0, RadialState::kFull };\n\tconst auto icon = _iconOverride ? _iconOverride : &(over ? st.iconOver : st.icon);\n\tauto position = st.iconPosition;\n\tif (position.x() < 0) {\n\t\tposition.setX((width() - icon->width()) / 2);\n\t}\n\tif (position.y() < 0) {\n\t\tposition.setY((height() - icon->height()) / 2);\n\t}\n\tconst auto skipx = icon->width() / 4;\n\tconst auto skipy = icon->height() / 4;\n\tconst auto inner = QRect(\n\t\tposition + QPoint(skipx, skipy),\n\t\tQSize(icon->width() - 2 * skipx, icon->height() - 2 * skipy));\n\n\tif (loadingState.shown < 1.) {\n\t\tp.setOpacity(1. - loadingState.shown);\n\t\ticon->paint(p, position, width());\n\t\tp.setOpacity(1.);\n\t}\n\n\tconst auto color = (_colorOverride\n\t\t? *_colorOverride\n\t\t: (over ? _st.lineFgOver : _st.lineFg));\n\tconst auto line = style::ConvertScaleExact(st::historyEmojiCircleLine);\n\tif (anim::Disabled() && _loading && _loading->animating()) {\n\t\tanim::DrawStaticLoading(p, inner, line, color);\n\t} else {\n\t\tauto pen = color->p;\n\t\tpen.setWidthF(line);\n\t\tpen.setCapStyle(Qt::RoundCap);\n\t\tp.setPen(pen);\n\t\tp.setBrush(Qt::NoBrush);\n\n\t\tPainterHighQualityEnabler hq(p);\n\t\tif (loadingState.arcLength < RadialState::kFull) {\n\t\t\tp.drawArc(inner, loadingState.arcFrom, loadingState.arcLength);\n\t\t} else {\n\t\t\tp.drawEllipse(inner);\n\t\t}\n\t}\n}\n\nvoid EmojiButton::loadingAnimationCallback() {\n\tif (!anim::Disabled()) {\n\t\tupdate();\n\t}\n}\n\nvoid EmojiButton::setLoading(bool loading) {\n\tif (loading && !_loading) {\n\t\t_loading = std::make_unique<InfiniteRadialAnimation>(\n\t\t\t[=] { loadingAnimationCallback(); },\n\t\t\tst::defaultInfiniteRadialAnimation);\n\t}\n\tif (loading) {\n\t\t_loading->start();\n\t\tupdate();\n\t} else if (_loading) {\n\t\t_loading->stop();\n\t\tupdate();\n\t}\n}\n\nvoid EmojiButton::setColorOverrides(const style::icon *iconOverride, const style::color *colorOverride, const style::color *rippleOverride) {\n\t_iconOverride = iconOverride;\n\t_colorOverride = colorOverride;\n\t_rippleOverride = rippleOverride;\n\tupdate();\n}\n\nvoid EmojiButton::onStateChanged(State was, StateChangeSource source) {\n\tRippleButton::onStateChanged(was, source);\n\tauto wasOver = static_cast<bool>(was & StateFlag::Over);\n\tif (isOver() != wasOver) {\n\t\tupdate();\n\t}\n}\n\nQPoint EmojiButton::prepareRippleStartPosition() const {\n\tif (!_st.inner.rippleAreaSize) {\n\t\treturn DisabledRippleStartPosition();\n\t}\n\treturn mapFromGlobal(QCursor::pos()) - _st.inner.rippleAreaPosition;\n}\n\nQImage EmojiButton::prepareRippleMask() const {\n\tconst auto size = _st.inner.rippleAreaSize;\n\treturn RippleAnimation::EllipseMask(QSize(size, size));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/emoji_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/buttons.h\"\n\nnamespace style {\nstruct EmojiButton;\n} // namespace style\n\nnamespace Ui {\n\nclass InfiniteRadialAnimation;\n\nclass EmojiButton final : public RippleButton {\npublic:\n\tEmojiButton(QWidget *parent, const style::EmojiButton &st);\n\n\tvoid setLoading(bool loading);\n\tvoid setColorOverrides(\n\t\tconst style::icon *iconOverride,\n\t\tconst style::color *colorOverride,\n\t\tconst style::color *rippleOverride);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid onStateChanged(State was, StateChangeSource source) override;\n\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\nprivate:\n\tvoid loadingAnimationCallback();\n\n\tconst style::EmojiButton &_st;\n\n\tstd::unique_ptr<Ui::InfiniteRadialAnimation> _loading;\n\n\tconst style::icon *_iconOverride = nullptr;\n\tconst style::color *_colorOverride = nullptr;\n\tconst style::color *_rippleOverride = nullptr;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/emoji_button_factory.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/emoji_button_factory.h\"\n\n#include \"base/event_filter.h\"\n#include \"chat_helpers/emoji_suggestions_widget.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"chat_helpers/tabbed_panel.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"ui/controls/emoji_button.h\"\n#include \"ui/effects/fade_animation.h\"\n#include \"ui/layers/box_content.h\"\n#include \"ui/rect.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat_helpers.h\" // defaultComposeFiles.\n#include \"styles/style_settings.h\"\n\nnamespace Ui {\n\n[[nodiscard]] not_null<Ui::EmojiButton*> AddEmojiToggleToField(\n\t\tnot_null<Ui::InputField*> field,\n\t\tnot_null<Ui::BoxContent*> box,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<ChatHelpers::TabbedPanel*> emojiPanel,\n\t\tQPoint shift,\n\t\tbool fadeOnFocusChange) {\n\tconst auto emojiToggle = Ui::CreateChild<Ui::EmojiButton>(\n\t\tfield->parentWidget(),\n\t\tst::defaultComposeFiles.emoji);\n\tconst auto fade = Ui::CreateChild<Ui::FadeAnimation>(\n\t\temojiToggle,\n\t\temojiToggle,\n\t\t0.5);\n\t{\n\t\tconst auto fadeTarget = Ui::CreateChild<Ui::RpWidget>(emojiToggle);\n\t\tfadeTarget->resize(emojiToggle->size());\n\t\tfadeTarget->paintRequest(\n\t\t) | rpl::on_next([=](const QRect &rect) {\n\t\t\tauto p = QPainter(fadeTarget);\n\t\t\tif (fade->animating()) {\n\t\t\t\tp.fillRect(fadeTarget->rect(), st::boxBg);\n\t\t\t}\n\t\t\tfade->paint(p);\n\t\t}, fadeTarget->lifetime());\n\t\tif (fadeOnFocusChange) {\n\t\t\trpl::single(false) | rpl::then(\n\t\t\t\tfield->focusedChanges()\n\t\t\t) | rpl::on_next([=](bool shown) {\n\t\t\t\tcrl::on_main(emojiToggle, [=] {\n\t\t\t\t\tif (shown) {\n\t\t\t\t\t\tfade->fadeIn(st::universalDuration);\n\t\t\t\t\t} else if (emojiToggle->isVisible()) {\n\t\t\t\t\t\tfade->fadeOut(st::universalDuration);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}, emojiToggle->lifetime());\n\t\t}\n\t\tfade->fadeOut(1);\n\t\tfade->finish();\n\t}\n\n\n\tconst auto outer = box->getDelegate()->outerContainer();\n\tconst auto allow = [](not_null<DocumentData*>) { return true; };\n\tInitMessageFieldHandlers(\n\t\tcontroller,\n\t\tfield,\n\t\tWindow::GifPauseReason::Layer,\n\t\tallow);\n\tUi::Emoji::SuggestionsController::Init(\n\t\touter,\n\t\tfield,\n\t\t&controller->session(),\n\t\tUi::Emoji::SuggestionsController::Options{\n\t\t\t.suggestCustomEmoji = true,\n\t\t\t.allowCustomWithoutPremium = allow,\n\t\t});\n\tconst auto updateEmojiPanelGeometry = [=] {\n\t\tconst auto parent = emojiPanel->parentWidget();\n\t\tconst auto global = emojiToggle->mapToGlobal({ 0, 0 });\n\t\tconst auto local = parent->mapFromGlobal(global);\n\t\tconst auto right = local.x() + emojiToggle->width() * 3;\n\t\tconst auto isDropDown = local.y() < parent->height() / 2;\n\t\temojiPanel->setDropDown(isDropDown);\n\t\tif (isDropDown) {\n\t\t\temojiPanel->moveTopRight(\n\t\t\t\tlocal.y() + emojiToggle->height(),\n\t\t\t\tright);\n\t\t} else {\n\t\t\temojiPanel->moveBottomRight(local.y(), right);\n\t\t}\n\t};\n\trpl::combine(\n\t\tbox->sizeValue(),\n\t\tfield->geometryValue()\n\t) | rpl::on_next([=](QSize outer, QRect inner) {\n\t\temojiToggle->moveToLeft(\n\t\t\trect::right(inner) + shift.x(),\n\t\t\tinner.y() + shift.y());\n\t\temojiToggle->update();\n\t}, emojiToggle->lifetime());\n\n\temojiToggle->installEventFilter(emojiPanel);\n\temojiToggle->addClickHandler([=] {\n\t\tupdateEmojiPanelGeometry();\n\t\temojiPanel->toggleAnimated();\n\t});\n\tconst auto filterCallback = [=](not_null<QEvent*> event) {\n\t\tif (event->type() == QEvent::Enter) {\n\t\t\tupdateEmojiPanelGeometry();\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t};\n\tbase::install_event_filter(emojiToggle, filterCallback);\n\n\treturn emojiToggle;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/emoji_button_factory.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace ChatHelpers {\nclass TabbedPanel;\n} // namespace ChatHelpers\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\n\nclass BoxContent;\nclass EmojiButton;\nclass InputField;\n\n[[nodiscard]] not_null<Ui::EmojiButton*> AddEmojiToggleToField(\n\tnot_null<Ui::InputField*> field,\n\tnot_null<Ui::BoxContent*> box,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<ChatHelpers::TabbedPanel*> emojiPanel,\n\tQPoint shift,\n\tbool fadeOnFocusChange = true);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/feature_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/feature_list.h\"\n\n#include \"base/object_ptr.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"ui/widgets/labels.h\"\n#include \"styles/style_info.h\"\n\nnamespace Ui {\n\nobject_ptr<RpWidget> MakeFeatureListEntry(\n\t\tQWidget *parent,\n\t\tFeatureListEntry feature,\n\t\tconst Text::MarkedContext &context,\n\t\tconst style::FlatLabel &stTitle,\n\t\tconst style::FlatLabel &stAbout) {\n\tauto result = object_ptr<PaddingWrap<>>(\n\t\tparent,\n\t\tobject_ptr<RpWidget>(parent),\n\t\tst::infoStarsFeatureMargin);\n\tconst auto widget = result->entity();\n\tconst auto icon = CreateChild<Info::Profile::FloatingIcon>(\n\t\twidget,\n\t\tfeature.icon,\n\t\tst::infoStarsFeatureIconPosition);\n\tconst auto title = CreateChild<FlatLabel>(\n\t\twidget,\n\t\tfeature.title,\n\t\tstTitle);\n\tconst auto about = CreateChild<FlatLabel>(\n\t\twidget,\n\t\trpl::single(feature.about),\n\t\tstAbout,\n\t\tst::defaultPopupMenu,\n\t\tcontext);\n\ticon->show();\n\ttitle->show();\n\tabout->show();\n\tabout->setLinksTrusted();\n\twidget->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto left = st::infoStarsFeatureLabelLeft;\n\t\tconst auto available = width - left;\n\t\ttitle->resizeToWidth(available);\n\t\tabout->resizeToWidth(available);\n\t\tauto top = 0;\n\t\ttitle->move(left, top);\n\t\ttop += title->height() + st::infoStarsFeatureSkip;\n\t\tabout->move(left, top);\n\t\ttop += about->height();\n\t\twidget->resize(width, top);\n\t}, widget->lifetime());\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/feature_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace style {\nstruct FlatLabel;\n} // namespace style\n\nnamespace st {\nextern const style::FlatLabel &infoStarsFeatureTitle;\nextern const style::FlatLabel &infoStarsFeatureAbout;\n} // namespace st\n\nnamespace Ui::Text {\nstruct MarkedContext;\n} // namespace Ui::Text\n\nnamespace Ui {\n\nclass RpWidget;\n\nstruct FeatureListEntry {\n\tconst style::icon &icon;\n\tQString title;\n\tTextWithEntities about;\n};\n\n[[nodiscard]] object_ptr<RpWidget> MakeFeatureListEntry(\n\tQWidget *parent,\n\tFeatureListEntry feature,\n\tconst Text::MarkedContext &context = {},\n\tconst style::FlatLabel &stTitle = st::infoStarsFeatureTitle,\n\tconst style::FlatLabel &stAbout = st::infoStarsFeatureAbout);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/filter_link_header.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/filter_link_header.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/chat_filters_tabs_slider.h\"\n#include \"ui/widgets/labels.h\"\n#include \"styles/style_chat_helpers.h\" // defaultEmojiSuggestions\n#include \"styles/style_dialogs.h\" // dialogsSearchTabs\n#include \"styles/style_filter_icons.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_window.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kBodyAnimationPart = 0.90;\nconstexpr auto kTitleAdditionalScale = 0.05;\n\nstruct PreviewState {\n\tFn<QImage()> frame;\n\trpl::lifetime lifetime;\n};\n\nclass Widget final : public RpWidget {\npublic:\n\tWidget(\n\t\tnot_null<QWidget*> parent,\n\t\tFilterLinkHeaderDescriptor &&descriptor);\n\n\tvoid setTitlePosition(int x, int y);\n\n\t[[nodiscard]] rpl::producer<not_null<QWheelEvent*>> wheelEvents() const;\n\t[[nodiscard]] rpl::producer<> closeRequests() const;\n\nprivate:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid wheelEvent(QWheelEvent *e) override;\n\n\t[[nodiscard]] QRectF previewRect(\n\t\tfloat64 topProgress,\n\t\tfloat64 sizeProgress) const;\n\tvoid refreshTitleText();\n\n\tconst not_null<FlatLabel*> _about;\n\tconst not_null<IconButton*> _close;\n\tQMargins _aboutPadding;\n\n\tstruct {\n\t\tfloat64 top = 0.;\n\t\tfloat64 body = 0.;\n\t\tfloat64 title = 0.;\n\t\tfloat64 scaleTitle = 0.;\n\t} _progress;\n\n\trpl::variable<int> _badge;\n\tPreviewState _preview;\n\tQRectF _previewRect;\n\n\tQString _titleText;\n\tstyle::font _titleFont;\n\tQMargins _titlePadding;\n\tQPoint _titlePosition;\n\tQPainterPath _titlePath;\n\n\tTextWithEntities _folderTitle;\n\tText::MarkedContext _aboutContext;\n\tnot_null<const style::icon*> _folderIcon;\n\tbool _horizontalFilters = false;\n\n\tint _maxHeight = 0;\n\n\trpl::event_stream<not_null<QWheelEvent*>> _wheelEvents;\n\n};\n\n[[nodiscard]] PreviewState GeneratePreview(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst TextWithEntities &title,\n\t\tText::MarkedContext aboutContext,\n\t\tint badge) {\n\tusing Tabs = Ui::ChatsFiltersTabs;\n\tauto preview = PreviewState();\n\n\tstruct State {\n\t\tState(not_null<Ui::RpWidget*> parent)\n\t\t: tabs(parent, st::dialogsSearchTabs) {\n\t\t}\n\n\t\tTabs tabs;\n\t\tQImage cache;\n\t\tbool dirty = true;\n\t};\n\tconst auto state = preview.lifetime.make_state<State>(parent);\n\tpreview.frame = [=] {\n\t\tif (state->dirty) {\n\t\t\tconst auto raw = &state->tabs;\n\t\t\tstate->cache.fill(st::windowBg->c);\n\n\t\t\tauto p = QPainter(&state->cache);\n\t\t\tUi::RenderWidget(p, raw, QPoint(), raw->rect());\n\n\t\t\tconst auto &r = st::defaultEmojiSuggestions.fadeRight;\n\t\t\tconst auto &l = st::defaultEmojiSuggestions.fadeLeft;\n\t\t\tconst auto padding = st::filterLinkSubsectionTitlePadding.top();\n\t\t\tconst auto w = raw->width();\n\t\t\tconst auto h = raw->height();\n\t\t\tr.fill(p, QRect(w - r.width() - padding, 0, r.width(), h));\n\t\t\tl.fill(p, QRect(padding, 0, l.width(), h));\n\t\t\tp.fillRect(0, 0, padding, h, st::windowBg);\n\t\t\tp.fillRect(w - padding, 0, padding, raw->height(), st::windowBg);\n\t\t}\n\t\treturn state->cache;\n\t};\n\tconst auto raw = &state->tabs;\n\tconst auto repaint = [=] { state->dirty = true; };\n\tauto context = aboutContext;\n\tcontext.repaint = repaint;\n\traw->setSections({\n\t\tTextWithEntities{ tr::lng_filters_name_people(tr::now) },\n\t\ttitle,\n\t\tTextWithEntities{ tr::lng_filters_name_unread(tr::now) },\n\t}, context);\n\traw->fitWidthToSections();\n\traw->setActiveSectionFast(1);\n\traw->stopAnimation();\n\n\tauto &result = state->cache;\n\tresult = QImage(\n\t\traw->size() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\traw->hide();\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next(repaint, preview.lifetime);\n\n\treturn preview;\n}\n\n[[nodiscard]] PreviewState GeneratePreview(\n\t\tconst TextWithEntities &title,\n\t\tText::MarkedContext context,\n\t\tnot_null<const style::icon*> icon,\n\t\tint badge) {\n\tauto preview = PreviewState();\n\n\tstruct State {\n\t\tQImage bg;\n\t\tQImage composed;\n\t\tUi::Text::String string;\n\t\tbool dirty = true;\n\t};\n\tconst auto state = preview.lifetime.make_state<State>();\n\tcontext.repaint = [=] {\n\t\tstate->dirty = true;\n\t};\n\n\tconst auto size = st::filterLinkPreview;\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto radius = st::filterLinkPreviewRadius;\n\tconst auto full = QSize(size, size) * ratio;\n\tauto &result = state->bg;\n\tresult = QImage(full, QImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\tresult.fill(st::windowBg->c);\n\n\tauto p = QPainter(&result);\n\n\tconst auto column = st::filterLinkPreviewColumn;\n\tp.fillRect(0, 0, column, size, st::sideBarBg);\n\tp.fillRect(column, 0, size - column, size, st::emojiPanCategories);\n\n\tconst auto &st = st::windowFiltersButton;\n\tconst auto skip = st.style.font->spacew;\n\tconst auto available = column - 2 * skip;\n\tconst auto iconWidth = st::foldersAll.width();\n\tconst auto iconHeight = st::foldersAll.height();\n\tconst auto iconLeft = (column - iconWidth) / 2;\n\tconst auto allIconTop = st::filterLinkPreviewAllBottom - iconHeight;\n\tst::foldersAll.paint(p, iconLeft, allIconTop, size);\n\tconst auto myIconTop = st::filterLinkPreviewMyBottom - iconHeight;\n\ticon->paint(p, iconLeft, myIconTop, size);\n\n\tconst auto fillName = [=](const TextWithEntities &text) {\n\t\tstate->string = Ui::Text::String(\n\t\t\tst::windowFiltersButton.style,\n\t\t\ttext,\n\t\t\tkMarkupTextOptions,\n\t\t\tavailable,\n\t\t\tcontext);\n\t};\n\tconst auto paintName = [=](QPainter &p, int top) {\n\t\tstate->string.draw(p, {\n\t\t\t.position = QPoint(\n\t\t\t\tstd::max(\n\t\t\t\t\t(column - state->string.maxWidth()) / 2,\n\t\t\t\t\tskip),\n\t\t\t\ttop),\n\t\t\t.outerWidth = available,\n\t\t\t.availableWidth = available,\n\t\t\t.align = style::al_left,\n\t\t\t.elisionLines = 1,\n\t\t});\n\t};\n\tp.setFont(st.style.font);\n\tp.setPen(st.textFg);\n\tfillName({ tr::lng_filters_all(tr::now) });\n\tpaintName(p, st::filterLinkPreviewAllTop);\n\tfillName(title);\n\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tconst auto chatSize = st::filterLinkPreviewChatSize;\n\tconst auto chatLeft = size + st::lineWidth - (chatSize / 2);\n\tconst auto paintChat = [&](int top, const style::color &bg) {\n\t\tp.setBrush(bg);\n\t\tp.drawEllipse(chatLeft, top, chatSize, chatSize);\n\t};\n\tconst auto chatSkip = st::filterLinkPreviewChatSkip;\n\tconst auto chat1Top = (size - 2 * chatSize - chatSkip) / 2;\n\tconst auto chat2Top = size - chat1Top - chatSize;\n\tp.setPen(Qt::NoPen);\n\tpaintChat(chat1Top, st::historyPeer4UserpicBg);\n\tpaintChat(chat2Top, st::historyPeer8UserpicBg);\n\n\tif (badge > 0) {\n\t\tconst auto font = st.badgeStyle.font;\n\t\tconst auto badgeHeight = st.badgeHeight;\n\t\tconst auto countBadgeWidth = [&](const QString &text) {\n\t\t\treturn std::max(\n\t\t\t\tfont->width(text) + 2 * st.badgeSkip,\n\t\t\t\tbadgeHeight);\n\t\t};\n\t\tconst auto defaultBadgeWidth = countBadgeWidth(u\"+3\"_q);\n\t\tconst auto badgeText = '+' + QString::number(badge);\n\t\tconst auto badgeWidth = countBadgeWidth(badgeText);\n\t\tconst auto defaultBadgeLeft = st::filterLinkPreviewBadgeLeft;\n\t\tconst auto badgeLeft = defaultBadgeLeft\n\t\t\t+ (defaultBadgeWidth - badgeWidth) / 2;\n\t\tconst auto badgeTop = st::filterLinkPreviewBadgeTop;\n\n\t\tconst auto add = st::lineWidth;\n\t\tauto pen = st.textBg->p;\n\t\tpen.setWidthF(add * 2.);\n\t\tp.setPen(pen);\n\t\tp.setBrush(st.badgeBg);\n\t\tconst auto radius = (badgeHeight / 2) + add;\n\t\tconst auto rect = QRect(badgeLeft, badgeTop, badgeWidth, badgeHeight)\n\t\t\t+ QMargins(add, add, add, add);\n\t\tp.drawRoundedRect(rect, radius, radius);\n\n\t\tp.setPen(st.badgeFg);\n\t\tp.setFont(st.badgeStyle.font);\n\t\tp.drawText(rect, badgeText, style::al_center);\n\t}\n\n\tauto pen = st::shadowFg->p;\n\tpen.setWidthF(st::lineWidth * 2.);\n\tp.setPen(pen);\n\tp.setBrush(Qt::NoBrush);\n\tp.drawRoundedRect(0, 0, size, size, radius, radius);\n\tp.end();\n\n\tresult = Images::Round(std::move(result), Images::CornersMask(radius));\n\n\tpreview.frame = [=] {\n\t\tif (state->dirty) {\n\t\t\tstate->composed = state->bg;\n\t\t\tauto p = QPainter(&state->composed);\n\t\t\tp.setPen(st.textFgActive);\n\t\t\tpaintName(p, st::filterLinkPreviewMyTop);\n\t\t}\n\t\treturn state->composed;\n\t};\n\n\treturn preview;\n}\n\nWidget::Widget(\n\tnot_null<QWidget*> parent,\n\tFilterLinkHeaderDescriptor &&descriptor)\n: RpWidget(parent)\n, _about(CreateChild<FlatLabel>(\n\tthis,\n\trpl::single(descriptor.about.value()),\n\tst::filterLinkAbout,\n\tst::defaultPopupMenu,\n\tdescriptor.aboutContext))\n, _close(CreateChild<IconButton>(this, st::boxTitleClose))\n, _aboutPadding(st::boxRowPadding)\n, _badge(std::move(descriptor.badge))\n, _titleText(descriptor.title)\n, _titleFont(st::boxTitle.style.font)\n, _titlePadding(st::filterLinkTitlePadding)\n, _folderTitle(descriptor.folderTitle)\n, _aboutContext(descriptor.aboutContext)\n, _folderIcon(descriptor.folderIcon)\n, _horizontalFilters(descriptor.horizontalFilters) {\n\tsetMinimumHeight(st::boxTitleHeight);\n\trefreshTitleText();\n\tsetTitlePosition(st::boxTitlePosition.x(), st::boxTitlePosition.y());\n\n\t_badge.changes() | rpl::on_next([this] {\n\t\t_preview = PreviewState();\n\t\tupdate();\n\t}, lifetime());\n}\n\nvoid Widget::refreshTitleText() {\n\t_titlePath = QPainterPath();\n\t_titlePath.addText(0, _titleFont->ascent, _titleFont, _titleText);\n\tupdate();\n}\n\nvoid Widget::setTitlePosition(int x, int y) {\n\t_titlePosition = { x, y };\n}\n\nrpl::producer<not_null<QWheelEvent*>> Widget::wheelEvents() const {\n\treturn _wheelEvents.events();\n}\n\nrpl::producer<> Widget::closeRequests() const {\n\treturn _close->clicks() | rpl::to_empty;\n}\n\nvoid Widget::resizeEvent(QResizeEvent *e) {\n\tconst auto &padding = _aboutPadding;\n\tconst auto availableWidth = width() - padding.left() - padding.right();\n\tif (availableWidth <= 0) {\n\t\treturn;\n\t}\n\t_about->resizeToWidth(availableWidth);\n\n\tconst auto minHeight = minimumHeight();\n\tconst auto maxHeight = (_horizontalFilters\n\t\t\t? (st::filterLinkAboutTop * 0.8)\n\t\t\t: st::filterLinkAboutTop)\n\t\t+ _about->height()\n\t\t+ st::filterLinkAboutBottom;\n\tif (maxHeight <= minHeight) {\n\t\treturn;\n\t} else if (_maxHeight != maxHeight) {\n\t\t_maxHeight = maxHeight;\n\t\tsetMaximumHeight(maxHeight);\n\t}\n\n\tconst auto progress = (height() - minHeight)\n\t\t/ float64(_maxHeight - minHeight);\n\t_progress.top = 1. -\n\t\tstd::clamp(\n\t\t\t(1. - progress) / kBodyAnimationPart,\n\t\t\t0.,\n\t\t\t1.);\n\t_progress.body = _progress.top;\n\t_progress.title = 1. - progress;\n\t_progress.scaleTitle = 1. + kTitleAdditionalScale * progress;\n\n\t_previewRect = previewRect(_progress.top, _progress.body);\n\n\tconst auto titleTop = _previewRect.top()\n\t\t+ _previewRect.height()\n\t\t+ _titlePadding.top();\n\tconst auto titlePathRect = _titlePath.boundingRect();\n\tconst auto aboutTop = titleTop\n\t\t+ titlePathRect.height()\n\t\t+ _titlePadding.bottom();\n\t_about->moveToLeft(_aboutPadding.left(), aboutTop);\n\t_about->setOpacity(_progress.body);\n\n\t_close->moveToRight(0, 0);\n\n\tupdate();\n}\n\nQRectF Widget::previewRect(\n\t\tfloat64 topProgress,\n\t\tfloat64 sizeProgress) const {\n\tif (_horizontalFilters) {\n\t\tconst auto size = (_preview.frame\n\t\t\t? (_preview.frame().size() / style::DevicePixelRatio())\n\t\t\t: QSize()) * sizeProgress;\n\t\treturn QRectF(\n\t\t\t(width() - size.width()) / 2.,\n\t\t\tst::filterLinkPreviewTop * 1.5 * topProgress,\n\t\t\tsize.width(),\n\t\t\tsize.height());\n\t} else {\n\t\tconst auto size = st::filterLinkPreview * sizeProgress;\n\t\treturn QRectF(\n\t\t\t(width() - size) / 2.,\n\t\t\tst::filterLinkPreviewTop * topProgress,\n\t\t\tsize,\n\t\t\tsize);\n\t}\n};\n\nvoid Widget::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tp.setOpacity(_progress.body);\n\tif (_progress.top) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tif (!_preview.frame) {\n\t\t\tconst auto badge = _badge.current();\n\t\t\tauto context = _aboutContext;\n\t\t\tcontext.repaint = [this, copy = context.repaint] {\n\t\t\t\tif (const auto &repaint = copy) {\n\t\t\t\t\trepaint();\n\t\t\t\t}\n\t\t\t\tupdate();\n\t\t\t};\n\t\t\tif (_horizontalFilters) {\n\t\t\t\t_preview = GeneratePreview(\n\t\t\t\t\tthis,\n\t\t\t\t\t_folderTitle,\n\t\t\t\t\tcontext,\n\t\t\t\t\tbadge);\n\t\t\t\tWidget::resizeEvent(nullptr);\n\t\t\t} else {\n\t\t\t\t_preview = GeneratePreview(\n\t\t\t\t\t_folderTitle,\n\t\t\t\t\tcontext,\n\t\t\t\t\t_folderIcon,\n\t\t\t\t\tbadge);\n\t\t\t}\n\t\t}\n\t\tp.drawImage(_previewRect, _preview.frame());\n\t}\n\tp.resetTransform();\n\n\tconst auto titlePathRect = _titlePath.boundingRect();\n\n\t// Title.\n\tPainterHighQualityEnabler hq(p);\n\tp.setOpacity(1.);\n\tp.setFont(_titleFont);\n\tp.setPen(st::boxTitleFg);\n\tconst auto fullPreviewRect = previewRect(1., 1.);\n\tconst auto fullTitleTop = fullPreviewRect.top()\n\t\t+ fullPreviewRect.height()\n\t\t+ _titlePadding.top();\n\tp.translate(\n\t\tanim::interpolate(\n\t\t\t(width() - titlePathRect.width()) / 2,\n\t\t\t_titlePosition.x(),\n\t\t\t_progress.title),\n\t\tanim::interpolate(fullTitleTop, _titlePosition.y(), _progress.title));\n\n\tp.translate(titlePathRect.center());\n\tp.scale(_progress.scaleTitle, _progress.scaleTitle);\n\tp.translate(-titlePathRect.center());\n\tp.fillPath(_titlePath, st::boxTitleFg);\n}\n\nvoid Widget::wheelEvent(QWheelEvent *e) {\n\t_wheelEvents.fire(e);\n}\n\n} // namespace\n\n[[nodiscard]] FilterLinkHeader MakeFilterLinkHeader(\n\t\tnot_null<QWidget*> parent,\n\t\tFilterLinkHeaderDescriptor &&descriptor) {\n\tconst auto result = CreateChild<Widget>(\n\t\tparent.get(),\n\t\tstd::move(descriptor));\n\treturn {\n\t\t.widget = result,\n\t\t.wheelEvents = result->wheelEvents(),\n\t\t.closeRequests = result->closeRequests(),\n\t};\n}\n\nobject_ptr<RoundButton> FilterLinkProcessButton(\n\t\tnot_null<QWidget*> parent,\n\t\tFilterLinkHeaderType type,\n\t\tTextWithEntities title,\n\t\tText::MarkedContext context,\n\t\trpl::producer<int> badge) {\n\tconst auto st = &st::filterInviteBox.button;\n\tconst auto badgeSt = &st::filterInviteButtonBadgeStyle;\n\tauto result = object_ptr<RoundButton>(parent, rpl::single(u\"\"_q), *st);\n\tresult->setTextTransform(RoundButtonTextTransform::ToUpper);\n\n\tstruct Data {\n\t\tTextWithEntities text;\n\t\tQString badge;\n\t};\n\tauto data = std::move(\n\t\tbadge\n\t) | rpl::map([=](int count) {\n\t\tconst auto badge = count ? QString::number(count) : QString();\n\t\tconst auto with = [&](QString badge) {\n\t\t\treturn rpl::map([=](TextWithEntities text) {\n\t\t\t\treturn Data{ text, badge };\n\t\t\t});\n\t\t};\n\t\tswitch (type) {\n\t\tcase FilterLinkHeaderType::AddingFilter:\n\t\t\treturn badge.isEmpty()\n\t\t\t\t? tr::lng_filters_by_link_add_no(\n\t\t\t\t\ttr::marked\n\t\t\t\t) | with(QString())\n\t\t\t\t: tr::lng_filters_by_link_add_button(\n\t\t\t\t\tlt_folder,\n\t\t\t\t\trpl::single(title),\n\t\t\t\t\ttr::marked\n\t\t\t\t) | with(badge);\n\t\tcase FilterLinkHeaderType::AddingChats:\n\t\t\treturn badge.isEmpty()\n\t\t\t\t? tr::lng_filters_by_link_join_no(\n\t\t\t\t\ttr::marked\n\t\t\t\t) | with(QString())\n\t\t\t\t: tr::lng_filters_by_link_and_join_button(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(float64(count)),\n\t\t\t\t\ttr::marked) | with(badge);\n\t\tcase FilterLinkHeaderType::AllAdded:\n\t\t\treturn tr::lng_box_ok(tr::marked) | with(QString());\n\t\tcase FilterLinkHeaderType::Removing:\n\t\t\treturn badge.isEmpty()\n\t\t\t\t? tr::lng_filters_by_link_remove_button(\n\t\t\t\t\ttr::marked\n\t\t\t\t) | with(QString())\n\t\t\t\t: tr::lng_filters_by_link_and_quit_button(\n\t\t\t\t\tlt_count,\n\t\t\t\t\trpl::single(float64(count)),\n\t\t\t\t\ttr::marked) | with(badge);\n\t\t}\n\t\tUnexpected(\"Type in FilterLinkProcessButton.\");\n\t}) | rpl::flatten_latest();\n\n\tstruct Label : RpWidget {\n\t\tusing RpWidget::RpWidget;\n\n\t\tText::String text;\n\t\tText::String badge;\n\t};\n\tconst auto label = result->lifetime().make_state<Label>(result.data());\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tresult->sizeValue() | rpl::on_next([=](QSize size) {\n\t\tconst auto xskip = st->style.font->spacew;\n\t\tconst auto yskip = xskip / 2;\n\t\tlabel->setGeometry(QRect(QPoint(), size).marginsRemoved(\n\t\t\t{ xskip, yskip, xskip, yskip }));\n\t}, label->lifetime());\n\tlabel->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = Painter(label);\n\t\tconst auto width = label->width();\n\t\tconst auto hasBadge = !label->badge.isEmpty();\n\t\tconst auto &badgePadding = st::filterInviteButtonBadgePadding;\n\t\tconst auto badgeInnerWidth = label->badge.maxWidth();\n\t\tconst auto badgeInnerHeight = badgeSt->font->height;\n\t\tconst auto badgeSize = QRect(\n\t\t\t0,\n\t\t\t0,\n\t\t\tbadgeInnerWidth,\n\t\t\tbadgeInnerHeight\n\t\t).marginsAdded(badgePadding).size();\n\t\tconst auto skip = st::filterInviteButtonBadgeSkip;\n\t\tconst auto badgeWithSkip = hasBadge ? (skip + badgeSize.width()) : 0;\n\t\tconst auto full = label->text.maxWidth() + badgeWithSkip;\n\t\tconst auto use = std::min(full, width);\n\t\tconst auto left = (width - use) / 2;\n\t\tconst auto top = st->textTop - label->y();\n\t\tconst auto available = use - badgeWithSkip;\n\n\t\tp.setPen(st->textFg);\n\t\tlabel->text.drawLeftElided(p, left, top, available + skip, width);\n\t\tif (hasBadge) {\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(st->textFg);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tconst auto radius = badgeSize.height() / 2;\n\t\t\tconst auto badgePosition = QPoint(\n\t\t\t\tleft + available + skip,\n\t\t\t\ttop - badgePadding.top());\n\t\t\tp.drawRoundedRect(\n\t\t\t\tQRect(badgePosition, badgeSize),\n\t\t\t\tradius,\n\t\t\t\tradius);\n\t\t\tp.setPen(st->textBg);\n\t\t\tlabel->badge.drawLeftElided(\n\t\t\t\tp,\n\t\t\t\tbadgePosition.x() + badgePadding.left(),\n\t\t\t\tbadgePosition.y() + badgePadding.top(),\n\t\t\t\tbadgeInnerWidth + skip,\n\t\t\t\twidth);\n\t\t}\n\t}, label->lifetime());\n\n\tcontext.repaint = [=] { label->update(); };\n\tstd::move(data) | rpl::on_next([=](Data data) {\n\t\tlabel->text.setMarkedText(\n\t\t\tst::filterInviteButtonStyle,\n\t\t\tdata.text,\n\t\t\tkMarkupTextOptions,\n\t\t\tcontext);\n\t\tlabel->badge.setText(st::filterInviteButtonBadgeStyle, data.badge);\n\t\tlabel->update();\n\t}, label->lifetime());\n\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/filter_link_header.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"base/required.h\"\n\nnamespace Ui {\n\nclass RpWidget;\nclass RoundButton;\n\nenum class FilterLinkHeaderType {\n\tAddingFilter,\n\tAddingChats,\n\tAllAdded,\n\tRemoving,\n};\n\nstruct FilterLinkHeaderDescriptor {\n\tbase::required<FilterLinkHeaderType> type;\n\tbase::required<QString> title;\n\tbase::required<TextWithEntities> about;\n\tText::MarkedContext aboutContext;\n\tbase::required<TextWithEntities> folderTitle;\n\tnot_null<const style::icon*> folderIcon;\n\trpl::producer<int> badge;\n\tbool horizontalFilters = false;\n};\n\nstruct FilterLinkHeader {\n\tnot_null<RpWidget*> widget;\n\trpl::producer<not_null<QWheelEvent*>> wheelEvents;\n\trpl::producer<> closeRequests;\n};\n\n[[nodiscard]] FilterLinkHeader MakeFilterLinkHeader(\n\tnot_null<QWidget*> parent,\n\tFilterLinkHeaderDescriptor &&descriptor);\n\n[[nodiscard]] object_ptr<RoundButton> FilterLinkProcessButton(\n\tnot_null<QWidget*> parent,\n\tFilterLinkHeaderType type,\n\tTextWithEntities title,\n\tText::MarkedContext context,\n\trpl::producer<int> badge);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/invite_link_buttons.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/invite_link_buttons.h\"\n\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_info.h\"\n\nnamespace Ui {\nnamespace {\n\nclass JoinedCountButton final : public AbstractButton {\npublic:\n\tusing AbstractButton::AbstractButton;\n\n\tvoid onStateChanged(State was, StateChangeSource source) override {\n\t\tupdate();\n\t}\n};\n\n} // namespace\n\nvoid AddCopyShareLinkButtons(\n\t\tnot_null<VerticalLayout*> container,\n\t\tFn<void()> copyLink,\n\t\tFn<void()> shareLink) {\n\tconst auto wrap = container->add(\n\t\tobject_ptr<FixedHeightWidget>(\n\t\t\tcontainer,\n\t\t\tst::inviteLinkButton.height),\n\t\tst::inviteLinkButtonsPadding);\n\tconst auto copy = CreateChild<RoundButton>(\n\t\twrap,\n\t\ttr::lng_group_invite_copy(),\n\t\tst::inviteLinkCopy);\n\tcopy->setClickedCallback(copyLink);\n\tconst auto share = CreateChild<RoundButton>(\n\t\twrap,\n\t\ttr::lng_group_invite_share(),\n\t\tst::inviteLinkShare);\n\tshare->setClickedCallback(shareLink);\n\n\twrap->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto buttonWidth = (width - st::inviteLinkButtonsSkip) / 2;\n\t\tcopy->setFullWidth(buttonWidth);\n\t\tshare->setFullWidth(buttonWidth);\n\t\tcopy->moveToLeft(0, 0, width);\n\t\tshare->moveToRight(0, 0, width);\n\t}, wrap->lifetime());\n}\n\n\nvoid AddReactivateLinkButton(\n\t\tnot_null<VerticalLayout*> container,\n\t\tFn<void()> editLink) {\n\tconst auto button = container->add(\n\t\tobject_ptr<RoundButton>(\n\t\t\tcontainer,\n\t\t\ttr::lng_group_invite_reactivate(),\n\t\t\tst::inviteLinkReactivate),\n\t\tst::inviteLinkButtonsPadding);\n\tbutton->setClickedCallback(editLink);\n}\n\nvoid AddDeleteLinkButton(\n\t\tnot_null<VerticalLayout*> container,\n\t\tFn<void()> deleteLink) {\n\tconst auto button = container->add(\n\t\tobject_ptr<RoundButton>(\n\t\t\tcontainer,\n\t\t\ttr::lng_group_invite_delete(),\n\t\t\tst::inviteLinkDelete),\n\t\tst::inviteLinkButtonsPadding);\n\tbutton->setClickedCallback(deleteLink);\n}\n\nnot_null<AbstractButton*> AddJoinedCountButton(\n\t\tnot_null<VerticalLayout*> container,\n\t\trpl::producer<JoinedCountContent> content,\n\t\tstyle::margins padding) {\n\tstruct State {\n\t\tJoinedCountContent content;\n\t\tQString phrase;\n\t\tint addedWidth = 0;\n\t};\n\tconst auto wrap = container->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::FixedHeightWidget>>(\n\t\t\tcontainer,\n\t\t\tobject_ptr<Ui::FixedHeightWidget>(\n\t\t\t\tcontainer,\n\t\t\t\tst::inviteLinkUserpics.size),\n\t\t\tQMargins(padding.left(), padding.top(), padding.right(), 0)),\n\t\tQMargins(0, 0, 0, padding.bottom()));\n\tconst auto result = CreateChild<JoinedCountButton>(wrap->entity());\n\tconst auto state = result->lifetime().make_state<State>();\n\tstd::move(\n\t\tcontent\n\t) | rpl::on_next([=](JoinedCountContent &&content) {\n\t\tstate->content = std::move(content);\n\t\twrap->toggle(state->content.count > 0, anim::type::instant);\n\t\tif (state->content.count <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tresult->setAttribute(\n\t\t\tQt::WA_TransparentForMouseEvents,\n\t\t\t!state->content.count);\n\t\tif (!state->content.count) {\n\t\t\tresult->clearState();\n\t\t}\n\t\tconst auto &st = st::inviteLinkUserpics;\n\t\tconst auto imageWidth = !state->content.userpics.isNull()\n\t\t\t? state->content.userpics.width() / style::DevicePixelRatio()\n\t\t\t: !state->content.count\n\t\t\t? 0\n\t\t\t: ((std::min(state->content.count, 3) - 1) * (st.size - st.shift)\n\t\t\t\t+ st.size);\n\t\tstate->addedWidth = imageWidth\n\t\t\t? (imageWidth + st::inviteLinkUserpicsSkip)\n\t\t\t: 0;\n\t\tstate->phrase = tr::lng_group_invite_joined(\n\t\t\ttr::now,\n\t\t\tlt_count_decimal,\n\t\t\tstate->content.count);\n\t\tconst auto fullWidth = st::inviteLinkJoinedFont->width(state->phrase)\n\t\t\t+ state->addedWidth;\n\t\tresult->resize(fullWidth, st.size);\n\t\tresult->move((wrap->width() - fullWidth) / 2, 0);\n\t\tresult->update();\n\t}, result->lifetime());\n\n\tresult->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(result);\n\t\tif (!state->content.userpics.isNull()) {\n\t\t\tp.drawImage(0, 0, state->content.userpics);\n\t\t}\n\t\tconst auto &font = st::inviteLinkJoinedFont;\n\t\tp.setPen(st::defaultLinkButton.color);\n\t\tp.setFont((result->isOver() || result->isDown())\n\t\t\t? font->underline()\n\t\t\t: font);\n\t\tconst auto top = (result->height() - font->height) / 2;\n\t\tp.drawText(\n\t\t\tstate->addedWidth,\n\t\t\ttop + font->ascent,\n\t\t\tstate->phrase);\n\t}, result->lifetime());\n\n\twrap->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tresult->move((width - result->width()) / 2, 0);\n\t}, wrap->lifetime());\n\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/invite_link_buttons.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nclass AbstractButton;\nclass VerticalLayout;\n\nvoid AddCopyShareLinkButtons(\n\tnot_null<VerticalLayout*> container,\n\tFn<void()> copyLink,\n\tFn<void()> shareLink);\n\nvoid AddReactivateLinkButton(\n\tnot_null<VerticalLayout*> container,\n\tFn<void()> editLink);\n\nvoid AddDeleteLinkButton(\n\tnot_null<VerticalLayout*> container,\n\tFn<void()> deleteLink);\n\nstruct JoinedCountContent {\n\tint count = 0;\n\tQImage userpics;\n};\n\nnot_null<AbstractButton*> AddJoinedCountButton(\n\tnot_null<VerticalLayout*> container,\n\trpl::producer<JoinedCountContent> content,\n\tstyle::margins padding);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/invite_link_label.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/invite_link_label.h\"\n\n#include \"ui/painter.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"styles/style_info.h\"\n\nnamespace Ui {\n\nInviteLinkLabel::InviteLinkLabel(\n\tnot_null<QWidget*> parent,\n\trpl::producer<QString> text,\n\tFn<base::unique_qptr<PopupMenu>()> createMenu)\n: _outer(std::in_place, parent) {\n\t_outer->resize(_outer->width(), st::inviteLinkFieldHeight);\n\tconst auto label = CreateChild<FlatLabel>(\n\t\t_outer.get(),\n\t\tstd::move(text),\n\t\tcreateMenu ? st::defaultFlatLabel : st::inviteLinkFieldLabel);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto button = createMenu\n\t\t? CreateChild<IconButton>(_outer.get(), st::inviteLinkThreeDots)\n\t\t: (IconButton*)(nullptr);\n\n\t_outer->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto margin = st::inviteLinkFieldMargin;\n\t\tconst auto labelWidth = width - margin.left() - margin.right();\n\t\tlabel->resizeToWidth(labelWidth);\n\t\tlabel->moveToLeft(\n\t\t\tcreateMenu\n\t\t\t\t? margin.left()\n\t\t\t\t: (width - labelWidth) / 2,\n\t\t\tmargin.top());\n\t\tif (button) {\n\t\t\tbutton->moveToRight(0, 0);\n\t\t}\n\t}, _outer->lifetime());\n\n\t_outer->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(_outer.get());\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::filterInputInactiveBg);\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawRoundedRect(\n\t\t\t\t_outer->rect(),\n\t\t\t\tst::inviteLinkFieldRadius,\n\t\t\t\tst::inviteLinkFieldRadius);\n\t\t}\n\t}, _outer->lifetime());\n\n\t_outer->setCursor(style::cur_pointer);\n\n\tif (createMenu) {\n\t\trpl::merge(\n\t\t\tbutton->clicks() | rpl::to_empty,\n\t\t\t_outer->events(\n\t\t\t) | rpl::filter([=](not_null<QEvent*> event) {\n\t\t\t\treturn (event->type() == QEvent::MouseButtonPress)\n\t\t\t\t\t&& (static_cast<QMouseEvent*>(event.get())->button()\n\t\t\t\t\t\t== Qt::RightButton);\n\t\t\t}) | rpl::to_empty\n\t\t) | rpl::on_next([=] {\n\t\t\tif (_menu) {\n\t\t\t\t_menu = nullptr;\n\t\t\t} else if ((_menu = createMenu())) {\n\t\t\t\t_menu->popup(QCursor::pos());\n\t\t\t}\n\t\t}, _outer->lifetime());\n\t}\n}\n\nobject_ptr<RpWidget> InviteLinkLabel::take() {\n\treturn object_ptr<RpWidget>::fromRaw(_outer.get());\n}\n\nrpl::producer<> InviteLinkLabel::clicks() {\n\treturn _outer->events(\n\t) | rpl::filter([=](not_null<QEvent*> event) {\n\t\treturn (event->type() == QEvent::MouseButtonPress)\n\t\t\t&& (static_cast<QMouseEvent*>(event.get())->button()\n\t\t\t\t== Qt::LeftButton);\n\t}) | rpl::map([=](not_null<QEvent*> event) {\n\t\treturn _outer->events(\n\t\t) | rpl::filter([=](not_null<QEvent*> event) {\n\t\t\treturn (event->type() == QEvent::MouseButtonRelease)\n\t\t\t\t&& (static_cast<QMouseEvent*>(event.get())->button()\n\t\t\t\t\t== Qt::LeftButton);\n\t\t}) | rpl::take(1) | rpl::filter([=](not_null<QEvent*> event) {\n\t\t\treturn (_outer->rect().contains(\n\t\t\t\tstatic_cast<QMouseEvent*>(event.get())->pos()));\n\t\t});\n\t}) | rpl::flatten_latest() | rpl::to_empty;\n}\n\nrpl::lifetime &InviteLinkLabel::lifetime() {\n\treturn _outer->lifetime();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/invite_link_label.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\ntemplate <typename Object>\nclass object_ptr;\n\n#include \"base/unique_qptr.h\"\n\nnamespace Ui {\n\nclass RpWidget;\nclass PopupMenu;\n\nclass InviteLinkLabel final {\npublic:\n\tInviteLinkLabel(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<QString> text,\n\t\tFn<base::unique_qptr<PopupMenu>()> createMenu);\n\n\t[[nodiscard]] object_ptr<RpWidget> take();\n\n\t[[nodiscard]] rpl::producer<> clicks();\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tconst base::unique_qptr<RpWidget> _outer;\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/jump_down_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/jump_down_button.h\"\n\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/unread_badge_paint.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Ui {\n\nJumpDownButton::JumpDownButton(\n\tQWidget *parent,\n\tconst style::TwoIconButton &st)\n: RippleButton(parent, st.ripple)\n, _st(st) {\n\tresize(_st.width, _st.height);\n\tsetCursor(style::cur_pointer);\n\n\thide();\n}\n\nQImage JumpDownButton::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::EllipseMask(\n\t\tQSize(_st.rippleAreaSize, _st.rippleAreaSize));\n}\n\nQPoint JumpDownButton::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos()) - _st.rippleAreaPosition;\n}\n\nvoid JumpDownButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto over = isOver();\n\tconst auto down = isDown();\n\t((over || down)\n\t\t? _st.iconBelowOver\n\t\t: _st.iconBelow).paint(p, _st.iconPosition, width());\n\tpaintRipple(p, _st.rippleAreaPosition.x(), _st.rippleAreaPosition.y());\n\t((over || down)\n\t\t? _st.iconAboveOver\n\t\t: _st.iconAbove).paint(p, _st.iconPosition, width());\n\tif (_unreadCount > 0) {\n\t\tauto unreadString = QString::number(_unreadCount);\n\n\t\tUi::UnreadBadgeStyle st;\n\t\tst.align = style::al_center;\n\t\tst.font = st::historyToDownBadgeFont;\n\t\tst.size = st::historyToDownBadgeSize;\n\t\tst.sizeId = Ui::UnreadBadgeSize::HistoryToDown;\n\t\tUi::PaintUnreadBadge(p, unreadString, width(), 0, st, 4);\n\t}\n}\n\nvoid JumpDownButton::setUnreadCount(int unreadCount) {\n\tif (_unreadCount != unreadCount) {\n\t\t_unreadCount = unreadCount;\n\t\tupdate();\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/jump_down_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/buttons.h\"\n\nnamespace Ui {\n\nclass JumpDownButton final : public RippleButton {\npublic:\n\tJumpDownButton(QWidget *parent, const style::TwoIconButton &st);\n\n\tvoid setUnreadCount(int unreadCount);\n\t[[nodiscard]] int unreadCount() const {\n\t\treturn _unreadCount;\n\t}\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\nprivate:\n\tconst style::TwoIconButton &_st;\n\n\tint _unreadCount = 0;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/labeled_emoji_tabs.h\"\n\n#include \"base/object_ptr.h\"\n#include \"ui/abstract_button.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"styles/style_basic.h\"\n#include \"styles/style_boxes.h\"\n\n#include <algorithm>\n#include <cmath>\n\n#include <QtGui/QMouseEvent>\n#include <QtWidgets/QApplication>\n\nnamespace Ui {\nnamespace {\n\n[[nodiscard]] QColor ColorWithAlpha(\n\t\tconst style::color &color,\n\t\tfloat64 alpha) {\n\tauto result = color->c;\n\tresult.setAlphaF(result.alphaF() * alpha);\n\treturn result;\n}\n\n[[nodiscard]] QColor ActiveBackgroundColor(const style::color &color) {\n\treturn ColorWithAlpha(color, st::aiComposeButtonBgActiveOpacity);\n}\n\n[[nodiscard]] QColor RippleColor(\n\t\tconst style::RippleAnimation &ripple,\n\t\tfloat64 opacity) {\n\treturn ColorWithAlpha(ripple.color, opacity);\n}\n\n[[nodiscard]] qreal TabsRadius() {\n\treturn st::aiComposeStyleTabsRadius;\n}\n\n} // namespace\n\nclass LabeledEmojiTabs::Button final : public RippleButton {\npublic:\n\tButton(\n\t\tQWidget *parent,\n\t\tLabeledEmojiTab descriptor,\n\t\tText::CustomEmojiFactory factory);\n\n\tvoid setSelected(bool selected);\n\tvoid setExtraPadding(int extra);\n\t[[nodiscard]] const QString &id() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\t[[nodiscard]] QImage prepareRippleMask() const override;\n\nprivate:\n\tconst LabeledEmojiTab _descriptor;\n\tstd::unique_ptr<Text::CustomEmoji> _custom;\n\tbool _selected = false;\n\tint _extraPadding = 0;\n\n};\n\nclass LabeledEmojiScrollTabs::DragScroll final : public QObject {\npublic:\n\tDragScroll(\n\t\tnot_null<QObject*> parent,\n\t\tnot_null<ScrollArea*> scroll,\n\t\tFn<void()> beforeScroll);\n\n\tvoid add(not_null<LabeledEmojiTabs::Button*> button);\n\nprotected:\n\tbool eventFilter(QObject *watched, QEvent *event) override;\n\nprivate:\n\tconst not_null<ScrollArea*> _scroll;\n\tconst Fn<void()> _beforeScroll;\n\tQPointer<LabeledEmojiTabs::Button> _pressed;\n\tQPoint _pressGlobal;\n\tint _scrollStart = 0;\n\tbool _dragging = false;\n\n};\n\nLabeledEmojiTabs::Button::Button(\n\tQWidget *parent,\n\tLabeledEmojiTab descriptor,\n\tText::CustomEmojiFactory factory)\n: RippleButton(parent, st::aiComposeButtonRippleInactive)\n, _descriptor(std::move(descriptor))\n, _custom(!_descriptor.customEmojiData.isEmpty() && factory\n\t? factory(\n\t\t_descriptor.customEmojiData,\n\t\t{ .repaint = [this] { update(); } })\n\t: nullptr) {\n\tsetCursor(style::cur_pointer);\n\tsetNaturalWidth([&] {\n\t\tconst auto padding = st::aiComposeStyleButtonPadding;\n\t\tconst auto labelWidth = st::aiComposeStyleLabelFont->width(\n\t\t\t_descriptor.label);\n\t\tconst auto emojiWidth = (_custom || _descriptor.emoji)\n\t\t\t? (Emoji::GetSizeLarge() / style::DevicePixelRatio())\n\t\t\t: 0;\n\t\treturn padding.left()\n\t\t\t+ std::max(labelWidth, emojiWidth)\n\t\t\t+ padding.right();\n\t}());\n\tsetExtraPadding(0);\n}\n\nvoid LabeledEmojiTabs::Button::setSelected(bool selected) {\n\tif (_selected == selected) {\n\t\treturn;\n\t}\n\t_selected = selected;\n\tupdate();\n}\n\nvoid LabeledEmojiTabs::Button::setExtraPadding(int extra) {\n\t_extraPadding = extra;\n\tresize(naturalWidth() + 2 * extra, height());\n}\n\nconst QString &LabeledEmojiTabs::Button::id() const {\n\treturn _descriptor.id;\n}\n\nvoid LabeledEmojiTabs::Button::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\tPainterHighQualityEnabler hq(p);\n\n\tconst auto radius = TabsRadius();\n\tif (_selected) {\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(ActiveBackgroundColor(st::aiComposeStyleButtonBgActive));\n\t\tp.drawRoundedRect(rect(), radius, radius);\n\t}\n\tconst auto ripple = RippleColor(\n\t\t_selected\n\t\t\t? st::aiComposeButtonRippleActive\n\t\t\t: st::aiComposeButtonRippleInactive,\n\t\t_selected\n\t\t\t? st::aiComposeButtonRippleActiveOpacity\n\t\t\t: st::aiComposeButtonRippleInactiveOpacity);\n\tpaintRipple(p, 0, 0, &ripple);\n\n\tif (_custom) {\n\t\tconst auto size = Emoji::GetSizeLarge() / style::DevicePixelRatio();\n\t\tconst auto adjusted = Text::AdjustCustomEmojiSize(size);\n\t\tconst auto skip = (size - adjusted) / 2;\n\t\tconst auto left = (width() - size) / 2;\n\t\t_custom->paint(p, {\n\t\t\t.textColor = (_selected\n\t\t\t\t? st::aiComposeStyleLabelFgActive\n\t\t\t\t: st::aiComposeStyleLabelFg)->c,\n\t\t\t.now = crl::now(),\n\t\t\t.position = {\n\t\t\t\tleft + skip,\n\t\t\t\tst::aiComposeStyleEmojiTop + skip,\n\t\t\t},\n\t\t});\n\t} else if (_descriptor.emoji) {\n\t\tconst auto size = Emoji::GetSizeLarge() / style::DevicePixelRatio();\n\t\tconst auto left = (width() - size) / 2;\n\t\tEmoji::Draw(\n\t\t\tp,\n\t\t\t_descriptor.emoji,\n\t\t\tEmoji::GetSizeLarge(),\n\t\t\tleft,\n\t\t\tst::aiComposeStyleEmojiTop);\n\t}\n\n\tp.setPen(_selected\n\t\t? st::aiComposeStyleLabelFgActive\n\t\t: st::aiComposeStyleLabelFg);\n\tp.setFont(st::aiComposeStyleLabelFont);\n\tp.drawText(\n\t\tQRect(\n\t\t\t0,\n\t\t\tst::aiComposeStyleLabelTop,\n\t\t\twidth(),\n\t\t\theight() - st::aiComposeStyleLabelTop),\n\t\tQt::AlignHCenter | Qt::AlignTop,\n\t\t_descriptor.label);\n}\n\nQImage LabeledEmojiTabs::Button::prepareRippleMask() const {\n\treturn RippleAnimation::MaskByDrawer(size(), false, [&](QPainter &p) {\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(Qt::white);\n\t\tconst auto radius = TabsRadius();\n\t\tp.drawRoundedRect(rect(), radius, radius);\n\t});\n}\n\nLabeledEmojiScrollTabs::DragScroll::DragScroll(\n\tnot_null<QObject*> parent,\n\tnot_null<ScrollArea*> scroll,\n\tFn<void()> beforeScroll)\n: QObject(parent)\n, _scroll(scroll)\n, _beforeScroll(std::move(beforeScroll)) {\n}\n\nvoid LabeledEmojiScrollTabs::DragScroll::add(\n\t\tnot_null<LabeledEmojiTabs::Button*> button) {\n\tbutton->installEventFilter(this);\n}\n\nbool LabeledEmojiScrollTabs::DragScroll::eventFilter(\n\t\tQObject *watched,\n\t\tQEvent *event) {\n\tconst auto button = dynamic_cast<LabeledEmojiTabs::Button*>(watched);\n\tif (!button) {\n\t\treturn QObject::eventFilter(watched, event);\n\t}\n\tswitch (event->type()) {\n\tcase QEvent::MouseButtonPress: {\n\t\tconst auto mouse = static_cast<QMouseEvent*>(event);\n\t\tif (mouse->button() != Qt::LeftButton) {\n\t\t\tbreak;\n\t\t}\n\t\t_pressed = button;\n\t\t_pressGlobal = mouse->globalPos();\n\t\t_scrollStart = _scroll->scrollLeft();\n\t\t_dragging = false;\n\t\tbreak;\n\t}\n\tcase QEvent::MouseMove: {\n\t\tconst auto mouse = static_cast<QMouseEvent*>(event);\n\t\tif (_pressed != button || !(mouse->buttons() & Qt::LeftButton)) {\n\t\t\tbreak;\n\t\t}\n\t\tconst auto delta = mouse->globalPos() - _pressGlobal;\n\t\tif (!_dragging) {\n\t\t\tif (!_scroll->scrollLeftMax()) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (std::abs(delta.x()) < QApplication::startDragDistance()) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (std::abs(delta.x()) <= std::abs(delta.y())) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t_dragging = true;\n\t\t\tbutton->setSynteticOver(false);\n\t\t}\n\t\tif (_beforeScroll) {\n\t\t\t_beforeScroll();\n\t\t}\n\t\tbutton->setSynteticOver(false);\n\t\t_scroll->scrollToX(_scrollStart - delta.x());\n\t\treturn true;\n\t}\n\tcase QEvent::MouseButtonRelease: {\n\t\tconst auto mouse = static_cast<QMouseEvent*>(event);\n\t\tif (_pressed != button || mouse->button() != Qt::LeftButton) {\n\t\t\tbreak;\n\t\t}\n\t\tconst auto dragging = std::exchange(_dragging, false);\n\t\t_pressed = nullptr;\n\t\tif (dragging) {\n\t\t\tbutton->setSynteticOver(false);\n\t\t}\n\t\tbreak;\n\t}\n\tcase QEvent::Hide:\n\t\tif (_pressed == button) {\n\t\t\t_pressed = nullptr;\n\t\t\t_dragging = false;\n\t\t}\n\t\tbreak;\n\tdefault:\n\t\tbreak;\n\t}\n\treturn QObject::eventFilter(watched, event);\n}\n\nLabeledEmojiTabs::LabeledEmojiTabs(\n\tQWidget *parent,\n\tstd::vector<LabeledEmojiTab> descriptors,\n\tText::CustomEmojiFactory factory)\n: RpWidget(parent) {\n\t_buttons.reserve(descriptors.size());\n\tfor (auto &descriptor : descriptors) {\n\t\tconst auto button = CreateChild<Button>(\n\t\t\tthis,\n\t\t\tstd::move(descriptor),\n\t\t\tfactory);\n\t\tbutton->setClickedCallback([=] {\n\t\t\tconst auto i = ranges::find(_buttons, not_null(button));\n\t\t\tconst auto index = int(i - begin(_buttons));\n\t\t\tif (index == _buttons.size()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsetActive(index);\n\t\t\t_requestShown.fire({\n\t\t\t\t_buttons[index]->x(),\n\t\t\t\t_buttons[index]->x() + _buttons[index]->width(),\n\t\t\t});\n\t\t\tif (_changed) {\n\t\t\t\t_changed(index);\n\t\t\t}\n\t\t});\n\t\t_buttons.push_back(button);\n\t}\n\tsetNaturalWidth([&] {\n\t\tconst auto padding = st::aiComposeStyleTabsPadding;\n\t\tconst auto skip = st::aiComposeStyleTabsSkip;\n\t\tauto total = padding.left();\n\t\tfor (const auto &button : _buttons) {\n\t\t\ttotal += button->naturalWidth() + skip;\n\t\t}\n\t\treturn total - (_buttons.empty() ? 0 : skip) + padding.right();\n\t}());\n\tsetActive(-1);\n}\n\nvoid LabeledEmojiTabs::setChangedCallback(Fn<void(int)> callback) {\n\t_changed = std::move(callback);\n}\n\nvoid LabeledEmojiTabs::setActive(int index) {\n\tif (index < -1 || index >= int(_buttons.size())) {\n\t\treturn;\n\t}\n\t_active = index;\n\tfor (auto i = 0; i != int(_buttons.size()); ++i) {\n\t\t_buttons[i]->setSelected(i == index);\n\t}\n}\n\nvoid LabeledEmojiTabs::resizeForOuterWidth(int outerWidth) {\n\tconst auto count = int(_buttons.size());\n\tconst auto padding = st::aiComposeStyleTabsPadding;\n\tconst auto skip = st::aiComposeStyleTabsSkip;\n\tconst auto buttonHeight = st::aiComposeStyleTabsHeight\n\t\t- padding.top()\n\t\t- padding.bottom();\n\tconst auto height = st::aiComposeStyleTabsHeight;\n\tauto left = padding.left();\n\tconst auto guard = gsl::finally([&] {\n\t\tresize(left - (count ? skip : 0) + padding.right(), height);\n\t});\n\tif (!count) {\n\t\treturn;\n\t}\n\tconst auto setExtraPaddingFor = [&](not_null<Button*> button, int value) {\n\t\tbutton->setExtraPadding(value);\n\t\tconst auto width = button->width();\n\t\tbutton->setGeometry(left, padding.top(), width, buttonHeight);\n\t\tleft += width + skip;\n\t};\n\tconst auto diff = naturalWidth() - outerWidth;\n\tif (diff > 0) {\n\t\tauto total = left;\n\t\tfor (auto fit = 0; fit != count;) {\n\t\t\tconst auto width = _buttons[fit]->naturalWidth();\n\t\t\tconst auto tooLarge = (total + (width / 2) > outerWidth);\n\t\t\tif (!tooLarge) {\n\t\t\t\t++fit;\n\t\t\t\ttotal += width + skip;\n\t\t\t}\n\t\t\tif (tooLarge || (fit == count)) {\n\t\t\t\tif (fit > 0) {\n\t\t\t\t\tconst auto width = _buttons[fit - 1]->naturalWidth();\n\t\t\t\t\tconst auto desired = total - skip - (width / 2);\n\t\t\t\t\tconst auto add = outerWidth - desired;\n\t\t\t\t\tconst auto extra = add / ((fit - 1) * 2 + 1);\n\t\t\t\t\tfor (const auto &button : _buttons) {\n\t\t\t\t\t\tsetExtraPaddingFor(button, extra);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tfor (const auto &button : _buttons) {\n\t\t\t\t\t\tsetExtraPaddingFor(button, 0);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tUnexpected(\"Tabs width inconsistency.\");\n\t} else {\n\t\tconst auto add = -diff / 2;\n\t\tconst auto each = add / _buttons.size();\n\t\tconst auto more = add - (each * _buttons.size());\n\t\tfor (auto i = 0; i < more; ++i) {\n\t\t\tsetExtraPaddingFor(_buttons[i], each + 1);\n\t\t}\n\t\tfor (auto i = more; i < count; ++i) {\n\t\t\tsetExtraPaddingFor(_buttons[i], each);\n\t\t}\n\t}\n}\n\nQString LabeledEmojiTabs::currentId() const {\n\treturn (_active >= 0 && _active < int(_buttons.size()))\n\t\t? _buttons[_active]->id()\n\t\t: QString();\n}\n\nint LabeledEmojiTabs::buttonCount() const {\n\treturn int(_buttons.size());\n}\n\nrpl::producer<ScrollToRequest> LabeledEmojiTabs::requestShown() const {\n\treturn _requestShown.events();\n}\n\nvoid LabeledEmojiTabs::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(st::aiComposeStyleTabsBg);\n\tconst auto radius = TabsRadius();\n\tp.drawRoundedRect(rect(), radius, radius);\n}\n\nLabeledEmojiScrollTabs::LabeledEmojiScrollTabs(\n\tQWidget *parent,\n\tstd::vector<LabeledEmojiTab> descriptors,\n\tText::CustomEmojiFactory factory)\n: RpWidget(parent)\n, _scroll(CreateChild<ScrollArea>(this, st::aiComposeStyleTabsScroll))\n, _inner(_scroll->setOwnedWidget(\n\tobject_ptr<LabeledEmojiTabs>(\n\t\tthis,\n\t\tstd::move(descriptors),\n\t\tstd::move(factory))))\n, _fadeLeft(CreateChild<RpWidget>(this))\n, _fadeRight(CreateChild<RpWidget>(this))\n, _cornerLeft(CreateChild<RpWidget>(this))\n, _cornerRight(CreateChild<RpWidget>(this))\n, _dragScroll(std::make_unique<DragScroll>(\n\tthis,\n\t_scroll,\n\t[=] { _scrollAnimation.stop(); })) {\n\tfor (const auto &button : _inner->_buttons) {\n\t\t_dragScroll->add(button);\n\t}\n\t_scroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) {\n\t\tconst auto pixelDelta = e->pixelDelta();\n\t\tconst auto angleDelta = e->angleDelta();\n\t\tif (std::abs(pixelDelta.x()) + std::abs(angleDelta.x())) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto y = pixelDelta.y() ? pixelDelta.y() : angleDelta.y();\n\t\t_scrollAnimation.stop();\n\t\t_scroll->scrollToX(_scroll->scrollLeft() - y);\n\t\treturn true;\n\t});\n\n\tconst auto setupFade = [&](not_null<RpWidget*> fade, bool left) {\n\t\tfade->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tfade->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(fade);\n\t\t\tconst auto width = fade->width();\n\t\t\tconst auto bg = st::aiComposeStyleTabsBg->c;\n\t\t\tauto transparent = bg;\n\t\t\ttransparent.setAlpha(0);\n\t\t\tauto gradient = QLinearGradient(0, 0, width, 0);\n\t\t\tif (left) {\n\t\t\t\tgradient.setColorAt(0., bg);\n\t\t\t\tgradient.setColorAt(1., transparent);\n\t\t\t} else {\n\t\t\t\tgradient.setColorAt(0., transparent);\n\t\t\t\tgradient.setColorAt(1., bg);\n\t\t\t}\n\t\t\tp.fillRect(fade->rect(), gradient);\n\t\t}, fade->lifetime());\n\t\tfade->hide();\n\t};\n\tsetupFade(_fadeLeft, true);\n\tsetupFade(_fadeRight, false);\n\n\tconst auto setupCorner = [&](not_null<RpWidget*> corner, bool left) {\n\t\tcorner->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tcorner->paintRequest() | rpl::on_next([=] {\n\t\t\tauto p = QPainter(corner);\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tconst auto width = corner->width();\n\t\t\tconst auto height = corner->height();\n\t\t\tauto mask = QPainterPath();\n\t\t\tmask.addRect(0, 0, width, height);\n\t\t\tauto rounded = QPainterPath();\n\t\t\tif (left) {\n\t\t\t\trounded.addRoundedRect(0, 0, width * 2, height, width, width);\n\t\t\t} else {\n\t\t\t\trounded.addRoundedRect(-width, 0, width * 2, height, width, width);\n\t\t\t}\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(st::boxDividerBg);\n\t\t\tp.drawPath(mask.subtracted(rounded));\n\t\t}, corner->lifetime());\n\t};\n\tsetupCorner(_cornerLeft, true);\n\tsetupCorner(_cornerRight, false);\n\n\t_scroll->scrolls() | rpl::on_next([=] {\n\t\tupdateFades();\n\t}, lifetime());\n\n\trpl::combine(\n\t\twidthValue(),\n\t\t_scroll->widthValue(),\n\t\t_inner->widthValue()\n\t) | rpl::on_next([=] {\n\t\tupdateFades();\n\t}, lifetime());\n\n\t_inner->requestShown() | rpl::on_next([=](ScrollToRequest request) {\n\t\tscrollToButton(request.ymin, request.ymax, true);\n\t}, lifetime());\n}\n\nLabeledEmojiScrollTabs::~LabeledEmojiScrollTabs() = default;\n\nvoid LabeledEmojiScrollTabs::setChangedCallback(Fn<void(int)> callback) {\n\t_inner->setChangedCallback(std::move(callback));\n}\n\nvoid LabeledEmojiScrollTabs::setActive(int index) {\n\t_inner->setActive(index);\n}\n\nvoid LabeledEmojiScrollTabs::setPaintOuterCorners(bool paint) {\n\tif (_paintOuterCorners == paint) {\n\t\treturn;\n\t}\n\t_paintOuterCorners = paint;\n\t_cornerLeft->setVisible(paint);\n\t_cornerRight->setVisible(paint);\n}\n\nvoid LabeledEmojiScrollTabs::scrollToActive() {\n\tconst auto index = _inner->_active;\n\tif (index < 0 || index >= int(_inner->_buttons.size())) {\n\t\t_scrollToActivePending = false;\n\t\treturn;\n\t}\n\tconst auto button = _inner->_buttons[index];\n\tif (_scroll->width() <= 0 || button->width() <= 0) {\n\t\t_scrollToActivePending = true;\n\t\treturn;\n\t}\n\t_scrollToActivePending = false;\n\tscrollToButton(button->x(), button->x() + button->width(), false);\n}\n\nQString LabeledEmojiScrollTabs::currentId() const {\n\treturn _inner->currentId();\n}\n\nint LabeledEmojiScrollTabs::buttonCount() const {\n\treturn _inner->buttonCount();\n}\n\nrpl::producer<ScrollToRequest> LabeledEmojiScrollTabs::requestShown() const {\n\treturn _inner->requestShown();\n}\n\nint LabeledEmojiScrollTabs::resizeGetHeight(int newWidth) {\n\t_scroll->setGeometry(0, 0, newWidth, st::aiComposeStyleTabsHeight);\n\t_inner->resizeForOuterWidth(newWidth);\n\n\tconst auto fadeWidth = st::aiComposeStyleFadeWidth;\n\tconst auto fadeHeight = st::aiComposeStyleTabsHeight;\n\t_fadeLeft->setGeometry(0, 0, fadeWidth, fadeHeight);\n\t_fadeRight->setGeometry(newWidth - fadeWidth, 0, fadeWidth, fadeHeight);\n\t_fadeLeft->raise();\n\t_fadeRight->raise();\n\n\tconst auto radius = st::aiComposeStyleTabsRadius;\n\tif (_paintOuterCorners) {\n\t\t_cornerLeft->setGeometry(0, 0, radius, fadeHeight);\n\t\t_cornerRight->setGeometry(newWidth - radius, 0, radius, fadeHeight);\n\t\t_cornerLeft->raise();\n\t\t_cornerRight->raise();\n\t\t_cornerLeft->show();\n\t\t_cornerRight->show();\n\t} else {\n\t\t_cornerLeft->hide();\n\t\t_cornerRight->hide();\n\t}\n\tif (_scrollToActivePending) {\n\t\tscrollToActive();\n\t}\n\n\tupdateFades();\n\treturn st::aiComposeStyleTabsHeight;\n}\n\nvoid LabeledEmojiScrollTabs::updateFades() {\n\tconst auto scrollLeft = _scroll->scrollLeft();\n\tconst auto scrollMax = _scroll->scrollLeftMax();\n\t_fadeLeft->setVisible(scrollLeft > 0);\n\t_fadeRight->setVisible(scrollLeft < scrollMax);\n}\n\nvoid LabeledEmojiScrollTabs::scrollToButton(\n\t\tint buttonLeft,\n\t\tint buttonRight,\n\t\tbool animated) {\n\tconst auto full = _scroll->width();\n\tconst auto tab = buttonRight - buttonLeft;\n\tif (tab < full) {\n\t\tconst auto add = std::min(full - tab, tab) / 2;\n\t\tbuttonRight += add;\n\t\tbuttonLeft -= add;\n\t}\n\tconst auto scrollLeft = _scroll->scrollLeft();\n\tconst auto needed = (buttonLeft < scrollLeft)\n\t\t|| (buttonRight > scrollLeft + full);\n\tif (!needed) {\n\t\treturn;\n\t}\n\tconst auto target = (buttonLeft < scrollLeft)\n\t\t? buttonLeft\n\t\t: std::min(buttonLeft, buttonRight - full);\n\t_scrollAnimation.stop();\n\tif (!animated) {\n\t\t_scroll->scrollToX(target);\n\t\treturn;\n\t}\n\t_scrollAnimation.start([=] {\n\t\t_scroll->scrollToX(qRound(_scrollAnimation.value(target)));\n\t}, scrollLeft, target, st::slideDuration, anim::sineInOut);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/emoji_config.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/text/text_custom_emoji.h\"\n\n#include <memory>\n#include <vector>\n\nnamespace Ui {\n\nclass RpWidget;\nclass ScrollArea;\nstruct ScrollToRequest;\n\nstruct LabeledEmojiTab {\n\tQString id;\n\tQString label;\n\tEmojiPtr emoji = nullptr;\n\tQString customEmojiData;\n};\n\nclass LabeledEmojiScrollTabs;\n\nclass LabeledEmojiTabs final : public RpWidget {\n\tfriend class LabeledEmojiScrollTabs;\n\npublic:\n\tLabeledEmojiTabs(\n\t\tQWidget *parent,\n\t\tstd::vector<LabeledEmojiTab> descriptors,\n\t\tText::CustomEmojiFactory factory);\n\n\tvoid setChangedCallback(Fn<void(int)> callback);\n\tvoid setActive(int index);\n\tvoid resizeForOuterWidth(int outerWidth);\n\t[[nodiscard]] QString currentId() const;\n\t[[nodiscard]] int buttonCount() const;\n\t[[nodiscard]] rpl::producer<ScrollToRequest> requestShown() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tclass Button;\n\n\tstd::vector<not_null<Button*>> _buttons;\n\tFn<void(int)> _changed;\n\tint _active = -1;\n\trpl::event_stream<ScrollToRequest> _requestShown;\n\n};\n\nclass LabeledEmojiScrollTabs final : public RpWidget {\npublic:\n\tLabeledEmojiScrollTabs(\n\t\tQWidget *parent,\n\t\tstd::vector<LabeledEmojiTab> descriptors,\n\t\tText::CustomEmojiFactory factory);\n\t~LabeledEmojiScrollTabs();\n\n\tvoid setChangedCallback(Fn<void(int)> callback);\n\tvoid setActive(int index);\n\tvoid setPaintOuterCorners(bool paint);\n\tvoid scrollToActive();\n\t[[nodiscard]] QString currentId() const;\n\t[[nodiscard]] int buttonCount() const;\n\t[[nodiscard]] rpl::producer<ScrollToRequest> requestShown() const;\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tclass DragScroll;\n\n\tvoid updateFades();\n\tvoid scrollToButton(int buttonLeft, int buttonRight, bool animated);\n\n\tconst not_null<ScrollArea*> _scroll;\n\tconst not_null<LabeledEmojiTabs*> _inner;\n\tconst not_null<RpWidget*> _fadeLeft;\n\tconst not_null<RpWidget*> _fadeRight;\n\tconst not_null<RpWidget*> _cornerLeft;\n\tconst not_null<RpWidget*> _cornerRight;\n\tAnimations::Simple _scrollAnimation;\n\tstd::unique_ptr<DragScroll> _dragScroll;\n\tbool _paintOuterCorners = true;\n\tbool _scrollToActivePending = false;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/location_picker.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/location_picker.h\"\n\n#include \"apiwrap.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"boxes/peer_list_box.h\"\n#include \"core/current_geo_location.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_location.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"dialogs/ui/chat_search_empty.h\" // Dialogs::SearchEmpty.\n#include \"lang/lang_instance.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/session/session_show.h\"\n#include \"main/main_session.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/separate_panel.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/painter.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/webview_helpers.h\"\n#include \"webview/webview_data_stream_memory.h\"\n#include \"webview/webview_embed.h\"\n#include \"webview/webview_interface.h\"\n#include \"core/cached_webview_availability.h\"\n#include \"window/themes/window_theme.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_settings.h\" // settingsCloudPasswordIconSize\n#include \"styles/style_layers.h\" // boxDividerHeight\n\n#include <QtCore/QFile>\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonObject>\n#include <QtCore/QJsonValue>\n#include <QtGui/QGuiApplication>\n#include <QtGui/QScreen>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kResolveAddressDelay = 3 * crl::time(1000);\nconstexpr auto kSearchDebounceDelay = crl::time(900);\n\nCore::GeoLocation LastExactLocation;\n\nusing VenueData = Data::InputVenue;\n\nclass VenueRowDelegate {\npublic:\n\tvirtual void rowPaintIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tconst QString &type) = 0;\n};\n\nclass VenueRow final : public PeerListRow {\npublic:\n\tVenueRow(not_null<VenueRowDelegate*> delegate, const VenueData &data);\n\n\tvoid update(const VenueData &data);\n\n\t[[nodiscard]] VenueData data() const;\n\n\tQString generateName() override;\n\tQString generateShortName() override;\n\tPaintRoundImageCallback generatePaintUserpicCallback(\n\t\tbool forceRound) override;\n\nprivate:\n\tconst not_null<VenueRowDelegate*> _delegate;\n\tVenueData _data;\n\n};\n\nVenueRow::VenueRow(\n\tnot_null<VenueRowDelegate*> delegate,\n\tconst VenueData &data)\n: PeerListRow(UniqueRowIdFromString(data.id))\n, _delegate(delegate)\n, _data(data) {\n\tsetCustomStatus(data.address);\n}\n\nvoid VenueRow::update(const VenueData &data) {\n\t_data = data;\n\tsetCustomStatus(data.address);\n\trefreshName(st::pickLocationVenueItem);\n}\n\nVenueData VenueRow::data() const {\n\treturn _data;\n}\n\nQString VenueRow::generateName() {\n\treturn _data.title;\n}\n\nQString VenueRow::generateShortName() {\n\treturn generateName();\n}\n\nPaintRoundImageCallback VenueRow::generatePaintUserpicCallback(\n\t\tbool forceRound) {\n\treturn [=](\n\t\t\tQPainter &p,\n\t\t\tint x,\n\t\t\tint y,\n\t\t\tint outerWidth,\n\t\t\tint size) {\n\t\t_delegate->rowPaintIcon(p, x, y, size, _data.venueType);\n\t};\n}\n\nclass VenuesController final\n\t: public PeerListController\n\t, public VenueRowDelegate\n\t, public base::has_weak_ptr {\npublic:\n\tVenuesController(\n\t\tnot_null<Main::Session*> session,\n\t\trpl::producer<std::vector<VenueData>> content,\n\t\tFn<void(VenueData)> callback);\n\n\tvoid prepare() override;\n\tvoid rowClicked(not_null<PeerListRow*> row) override;\n\tvoid rowRightActionClicked(not_null<PeerListRow*> row) override;\n\tMain::Session &session() const override;\n\n\tvoid rowPaintIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tconst QString &type) override;\n\nprivate:\n\tstruct VenueIcon {\n\t\tnot_null<DocumentData*> document;\n\t\tstd::shared_ptr<Data::DocumentMedia> media;\n\t\tuint32 paletteVersion : 31 = 0;\n\t\tuint32 iconLoaded : 1 = 0;\n\t\tQImage image;\n\t\tQImage icon;\n\t};\n\n\tvoid appendRow(const VenueData &data);\n\n\tvoid rebuild(const std::vector<VenueData> &rows);\n\n\tconst not_null<Main::Session*> _session;\n\tconst Fn<void(VenueData)> _callback;\n\trpl::variable<std::vector<VenueData>> _rows;\n\n\tbase::flat_map<QString, VenueIcon> _icons;\n\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] QString NormalizeVenuesQuery(QString query) {\n\treturn query.trimmed().toLower();\n}\n\n[[nodiscard]] object_ptr<RpWidget> MakeFoursquarePromo() {\n\tauto result = object_ptr<RpWidget>((QWidget*)nullptr);\n\tconst auto skip = st::defaultVerticalListSkip;\n\tconst auto raw = result.data();\n\traw->resize(0, skip + st::pickLocationPromoHeight);\n\tconst auto shadow = CreateChild<PlainShadow>(raw);\n\traw->widthValue() | rpl::on_next([=](int width) {\n\t\tshadow->setGeometry(0, skip, width, st::lineWidth);\n\t}, raw->lifetime());\n\traw->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(raw);\n\t\tp.fillRect(clip, st::windowBg);\n\t\tp.setPen(st::windowSubTextFg);\n\t\tp.setFont(st::normalFont);\n\t\tp.drawText(\n\t\t\traw->rect().marginsRemoved({ 0, skip, 0, 0 }),\n\t\t\ttr::lng_maps_venues_source(tr::now),\n\t\t\tstyle::al_center);\n\t}, raw->lifetime());\n\treturn result;\n}\n\nVenuesController::VenuesController(\n\tnot_null<Main::Session*> session,\n\trpl::producer<std::vector<VenueData>> content,\n\tFn<void(VenueData)> callback)\n: _session(session)\n, _callback(std::move(callback))\n, _rows(std::move(content)) {\n}\n\nvoid VenuesController::prepare() {\n\t_rows.value(\n\t) | rpl::on_next([=](const std::vector<VenueData> &rows) {\n\t\trebuild(rows);\n\t}, _lifetime);\n}\n\nvoid VenuesController::rebuild(const std::vector<VenueData> &rows) {\n\tauto i = 0;\n\tauto count = delegate()->peerListFullRowsCount();\n\twhile (i < rows.size()) {\n\t\tif (i < count) {\n\t\t\tconst auto row = delegate()->peerListRowAt(i);\n\t\t\tstatic_cast<VenueRow*>(row.get())->update(rows[i]);\n\t\t} else {\n\t\t\tappendRow(rows[i]);\n\t\t}\n\t\t++i;\n\t}\n\twhile (i < count) {\n\t\tdelegate()->peerListRemoveRow(delegate()->peerListRowAt(i));\n\t\t--count;\n\t}\n\tif (i > 0) {\n\t\tdelegate()->peerListSetBelowWidget(MakeFoursquarePromo());\n\t} else {\n\t\tdelegate()->peerListSetBelowWidget({ nullptr });\n\t}\n\tdelegate()->peerListRefreshRows();\n}\n\nvoid VenuesController::rowClicked(not_null<PeerListRow*> row) {\n\t_callback(static_cast<VenueRow*>(row.get())->data());\n}\n\nvoid VenuesController::rowRightActionClicked(not_null<PeerListRow*> row) {\n\tdelegate()->peerListShowRowMenu(row, true);\n}\n\nMain::Session &VenuesController::session() const {\n\treturn *_session;\n}\n\nvoid VenuesController::appendRow(const VenueData &data) {\n\tdelegate()->peerListAppendRow(std::make_unique<VenueRow>(this, data));\n}\n\nvoid VenuesController::rowPaintIcon(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tconst QString &icon) {\n\tauto i = _icons.find(icon);\n\tif (i == end(_icons)) {\n\t\ti = _icons.emplace(icon, VenueIcon{\n\t\t\t.document = _session->data().venueIconDocument(icon),\n\t\t}).first;\n\t\ti->second.media = i->second.document->createMediaView();\n\t\ti->second.document->forceToCache(true);\n\t\ti->second.document->save({}, QString(), LoadFromCloudOrLocal, true);\n\t}\n\tauto &data = i->second;\n\tconst auto version = uint32(style::PaletteVersion());\n\tconst auto loaded = (!data.media || data.media->loaded()) ? 1 : 0;\n\tconst auto prepare = data.image.isNull()\n\t\t|| (data.iconLoaded != loaded)\n\t\t|| (data.paletteVersion != version);\n\tif (prepare) {\n\t\tconst auto skip = st::pickLocationIconSkip;\n\t\tconst auto inner = size - skip * 2;\n\t\tconst auto ratio = style::DevicePixelRatio();\n\n\t\tif (loaded && data.media) {\n\t\t\tconst auto bytes = base::take(data.media)->bytes();\n\t\t\tdata.icon = Images::Read({ .content = bytes }).image;\n\t\t\tif (!data.icon.isNull()) {\n\t\t\t\tdata.icon = data.icon.scaled(\n\t\t\t\t\tQSize(inner, inner) * ratio,\n\t\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\t\tQt::SmoothTransformation);\n\t\t\t\tif (!data.icon.isNull()) {\n\t\t\t\t\tdata.icon = data.icon.convertToFormat(\n\t\t\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst auto full = QSize(size, size) * ratio;\n\t\tauto image = (data.image.size() == full)\n\t\t\t? base::take(data.image)\n\t\t\t: QImage(full, QImage::Format_ARGB32_Premultiplied);\n\t\timage.fill(Qt::transparent);\n\t\timage.setDevicePixelRatio(ratio);\n\n\t\tconst auto bg = EmptyUserpic::UserpicColor(\n\t\t\tEmptyUserpic::ColorIndex(UniqueRowIdFromString(icon)));\n\t\tauto p = QPainter(&image);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t{\n\t\t\tauto gradient = QLinearGradient(0, 0, 0, size);\n\t\t\tgradient.setStops({\n\t\t\t\t{ 0., bg.color1->c },\n\t\t\t\t{ 1., bg.color2->c }\n\t\t\t});\n\t\t\tp.setBrush(gradient);\n\t\t}\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawEllipse(QRect(0, 0, size, size));\n\t\tif (!data.icon.isNull()) {\n\t\t\tp.drawImage(\n\t\t\t\tQRect(skip, skip, inner, inner),\n\t\t\t\tstyle::colorizeImage(data.icon, st::historyPeerUserpicFg));\n\t\t}\n\t\tp.end();\n\n\t\tdata.paletteVersion = version;\n\t\tdata.iconLoaded = loaded;\n\t\tdata.image = std::move(image);\n\t}\n\tp.drawImage(x, y, data.image);\n}\n\n[[nodiscard]] QByteArray DefaultCenter(Core::GeoLocation initial) {\n\tconst auto &use = initial.exact() ? initial : LastExactLocation;\n\tif (!use) {\n\t\treturn \"null\";\n\t}\n\treturn \"[\"_q\n\t\t+ QByteArray::number(use.point.x())\n\t\t+ \",\"_q\n\t\t+ QByteArray::number(use.point.y())\n\t\t+ \"]\"_q;\n}\n\n[[nodiscard]] QByteArray DefaultBounds() {\n\tconst auto country = Core::ResolveCurrentCountryLocation();\n\tif (!country) {\n\t\treturn \"null\";\n\t}\n\treturn \"[[\"_q\n\t\t+ QByteArray::number(country.bounds.x())\n\t\t+ \",\"_q\n\t\t+ QByteArray::number(country.bounds.y())\n\t\t+ \"],[\"_q\n\t\t+ QByteArray::number(country.bounds.x() + country.bounds.width())\n\t\t+ \",\"_q\n\t\t+ QByteArray::number(country.bounds.y() + country.bounds.height())\n\t\t+ \"]]\"_q;\n}\n\n[[nodiscard]] QByteArray ComputeStyles() {\n\tstatic const auto map = base::flat_map<QByteArray, const style::color*>{\n\t\t{ \"window-bg\", &st::windowBg },\n\t\t{ \"window-bg-over\", &st::windowBgOver },\n\t\t{ \"window-bg-ripple\", &st::windowBgRipple },\n\t\t{ \"window-active-text-fg\", &st::windowActiveTextFg },\n\t\t{ \"history-to-down-shadow\", &st::historyToDownShadow },\n\t};\n\tstatic const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{\n\t\t{ \"maps-places-in-area\", tr::lng_maps_places_in_area },\n\t};\n\treturn Ui::ComputeStyles(map, phrases, 100, Window::Theme::IsNightMode());\n}\n\n[[nodiscard]] QByteArray ReadResource(const QString &name) {\n\tauto file = QFile(u\":/picker/\"_q + name);\n\treturn file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray();\n}\n\n[[nodiscard]] QByteArray PickerContent() {\n\treturn R\"(<!DOCTYPE html>\n<html style=\")\"\n+ EscapeForAttribute(ComputeStyles())\n+ R\"(\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"robots\" content=\"noindex, nofollow\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t\t<script src=\"/location/picker.js\"></script>\n\t\t<link rel=\"stylesheet\" href=\"/location/picker.css\" />\n\t\t<script src='https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.js'></script>\n\t\t<link href='https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.css' rel='stylesheet' />\n\t</head>\n\t<body>\n\t\t<div id=\"search_venues\">\n\t\t\t<div id=\"search_venues_inner\"><span id=\"search_venues_content\"></span></div>\n\t\t</div>\n\t\t<div id=\"marker\">\n\t\t\t<div id=\"marker_shadow\" style=\"transform: translate(0px, -14px);\">\n<svg display=\"block\" height=\"41px\" width=\"27px\" viewBox=\"0 0 27 41\">\n\t<defs>\n\t\t<radialGradient id=\"shadowGradient\">\n\t\t\t<stop offset=\"10%\" stop-opacity=\"0.4\"></stop>\n\t\t\t<stop offset=\"100%\" stop-opacity=\"0.05\"></stop>\n\t\t</radialGradient>\n\t</defs>\n\t<ellipse\n\t\tcx=\"13.5\"\n\t\tcy=\"34.8\"\n\t\trx=\"10.5\"\n\t\try=\"5.25\"\n\t\tfill=\")\" + \"url(#shadowGradient)\" + R\"(\"></ellipse>\n</svg>\n\t\t\t</div>\n\t\t\t<div id=\"marker_drop\" style=\"transform: translate(0px, -14px);\">\n<svg display=\"block\" height=\"41px\" width=\"27px\" viewBox=\"0 0 27 41\">\n\t<path fill=\"#3FB1CE\" d=\"M27,13.5C27,19.07 20.25,27 14.75,34.5C14.02,35.5 12.98,35.5 12.25,34.5C6.75,27 0,19.22 0,13.5C0,6.04 6.04,0 13.5,0C20.96,0 27,6.04 27,13.5Z\"></path><path opacity=\"0.25\" d=\"M13.5,0C6.04,0 0,6.04 0,13.5C0,19.22 6.75,27 12.25,34.5C13,35.52 14.02,35.5 14.75,34.5C20.25,27 27,19.07 27,13.5C27,6.04 20.96,0 13.5,0ZM13.5,1C20.42,1 26,6.58 26,13.5C26,15.9 24.5,19.18 22.22,22.74C19.95,26.3 16.71,30.14 13.94,33.91C13.74,34.18 13.61,34.32 13.5,34.44C13.39,34.32 13.26,34.18 13.06,33.91C10.28,30.13 7.41,26.31 5.02,22.77C2.62,19.23 1,15.95 1,13.5C1,6.58 6.58,1 13.5,1Z\"></path>\n\t<circle fill=\"white\" cx=\"13.5\" cy=\"13.5\" r=\"5.5\"></circle>\n</svg>\n\t\t\t</div>\n\t\t</div>\n\t\t<div id=\"map\"></div>\n\t\t<script>LocationPicker.notify({ event: 'ready' });</script>\n\t</body>\n</html>\n)\"_q;\n}\n\n[[nodiscard]] object_ptr<AbstractButton> MakeChooseLocationButton(\n\t\tQWidget *parent,\n\t\trpl::producer<QString> label,\n\t\trpl::producer<QString> address) {\n\tauto result = object_ptr<FlatButton>(\n\t\tparent,\n\t\tQString(),\n\t\tst::pickLocationButton);\n\tconst auto raw = result.data();\n\n\tconst auto st = &st::pickLocationVenueItem;\n\tconst auto icon = CreateChild<RpWidget>(raw);\n\ticon->setGeometry(\n\t\tst->photoPosition.x(),\n\t\tst->photoPosition.y(),\n\t\tst->photoSize,\n\t\tst->photoSize);\n\ticon->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(icon);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::windowBgActive);\n\t\tp.drawEllipse(icon->rect());\n\t\tst::pickLocationSendIcon.paintInCenter(p, icon->rect());\n\t}, icon->lifetime());\n\ticon->show();\n\n\tconst auto hadAddress = std::make_shared<bool>(false);\n\tauto statusText = std::move(\n\t\taddress\n\t) | rpl::map([=](const QString &text) {\n\t\tif (!text.isEmpty()) {\n\t\t\t*hadAddress = true;\n\t\t\treturn text;\n\t\t}\n\t\treturn *hadAddress ? tr::lng_contacts_loading(tr::now) : QString();\n\t});\n\tconst auto name = CreateChild<FlatLabel>(\n\t\traw,\n\t\tstd::move(label),\n\t\tst::pickLocationButtonText);\n\tname->show();\n\tconst auto status = CreateChild<FlatLabel>(\n\t\traw,\n\t\trpl::duplicate(statusText),\n\t\tst::pickLocationButtonStatus);\n\tstatus->showOn(rpl::duplicate(\n\t\tstatusText\n\t) | rpl::map([](const QString &text) {\n\t\treturn !text.isEmpty();\n\t}) | rpl::distinct_until_changed());\n\trpl::combine(\n\t\tresult->widthValue(),\n\t\tstd::move(statusText)\n\t) | rpl::on_next([=](int width, const QString &statusText) {\n\t\tconst auto available = width\n\t\t\t- st->namePosition.x()\n\t\t\t- st->button.padding.right();\n\t\tconst auto namePosition = st->namePosition;\n\t\tconst auto statusPosition = st->statusPosition;\n\t\tname->resizeToWidth(available);\n\t\tconst auto nameTop = statusText.isEmpty()\n\t\t\t? ((st->height - name->height()) / 2)\n\t\t\t: namePosition.y();\n\t\tname->moveToLeft(namePosition.x(), nameTop, width);\n\t\tstatus->resizeToNaturalWidth(available);\n\t\tstatus->moveToLeft(statusPosition.x(), statusPosition.y(), width);\n\t}, name->lifetime());\n\n\ticon->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tname->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tstatus->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\treturn result;\n}\n\nvoid SetupLoadingView(not_null<RpWidget*> container) {\n\tclass Loading final : public RpWidget {\n\tpublic:\n\t\texplicit Loading(QWidget *parent)\n\t\t: RpWidget(parent)\n\t\t, animation(\n\t\t\t\t[=] { if (!anim::Disabled()) update(); },\n\t\t\t\tst::pickLocationLoading) {\n\t\t\tanimation.start(st::pickLocationLoading.sineDuration);\n\t\t}\n\n\tprivate:\n\t\tvoid paintEvent(QPaintEvent *e) override {\n\t\t\tauto p = QPainter(this);\n\t\t\tconst auto size = st::pickLocationLoading.size;\n\t\t\tconst auto inner = QRect(QPoint(), size);\n\t\t\tconst auto positioned = style::centerrect(rect(), inner);\n\t\t\tanimation.draw(p, positioned.topLeft(), size, width());\n\t\t}\n\n\t\tInfiniteRadialAnimation animation;\n\n\t};\n\n\tconst auto view = CreateChild<Loading>(container);\n\tview->resize(container->width(), st::recentPeersEmptyHeightMin);\n\tview->show();\n\n\tResizeFitChild(container, view);\n}\n\nvoid SetupEmptyView(\n\t\tnot_null<RpWidget*> container,\n\t\tstd::optional<QString> query) {\n\tusing Icon = Dialogs::SearchEmptyIcon;\n\tconst auto view = CreateChild<Dialogs::SearchEmpty>(\n\t\tcontainer,\n\t\t(query ? Icon::NoResults : Icon::Search),\n\t\t(query\n\t\t\t? tr::lng_maps_no_places\n\t\t\t: tr::lng_maps_choose_to_search)(tr::marked));\n\tview->setMinimalHeight(st::recentPeersEmptyHeightMin);\n\tview->show();\n\n\tResizeFitChild(container, view);\n\n\tInvokeQueued(view, [=] { view->animate(); });\n}\n\nvoid SetupVenues(\n\t\tnot_null<VerticalLayout*> container,\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\trpl::producer<PickerVenueState> value,\n\t\tFn<void(VenueData)> callback) {\n\tconst auto otherWrap = container->add(object_ptr<SlideWrap<RpWidget>>(\n\t\tcontainer,\n\t\tobject_ptr<RpWidget>(container)));\n\tconst auto other = otherWrap->entity();\n\trpl::duplicate(\n\t\tvalue\n\t) | rpl::on_next([=](const PickerVenueState &state) {\n\t\twhile (!other->children().isEmpty()) {\n\t\t\tdelete other->children()[0];\n\t\t}\n\t\tif (v::is<PickerVenueList>(state)) {\n\t\t\totherWrap->hide(anim::type::instant);\n\t\t\treturn;\n\t\t} else if (v::is<PickerVenueLoading>(state)) {\n\t\t\tSetupLoadingView(other);\n\t\t} else {\n\t\t\tconst auto n = std::get_if<PickerVenueNothingFound>(&state);\n\t\t\tSetupEmptyView(other, n ? n->query : std::optional<QString>());\n\t\t}\n\t\totherWrap->show(anim::type::instant);\n\t}, otherWrap->lifetime());\n\n\tauto &lifetime = container->lifetime();\n\tauto venuesList = rpl::duplicate(\n\t\tvalue\n\t) | rpl::map([=](PickerVenueState &&state) {\n\t\treturn v::is<PickerVenueList>(state)\n\t\t\t? std::move(v::get<PickerVenueList>(state).list)\n\t\t\t: std::vector<VenueData>();\n\t});\n\tconst auto delegate = lifetime.make_state<PeerListContentDelegateShow>(\n\t\tshow);\n\tconst auto controller = lifetime.make_state<VenuesController>(\n\t\t&show->session(),\n\t\tstd::move(venuesList),\n\t\tstd::move(callback));\n\tcontroller->setStyleOverrides(&st::pickLocationVenueList);\n\tconst auto content = container->add(object_ptr<PeerListContent>(\n\t\tcontainer,\n\t\tcontroller));\n\tdelegate->setContent(content);\n\tcontroller->setDelegate(delegate);\n\n\tshow->session().downloaderTaskFinished() | rpl::on_next([=] {\n\t\tcontent->update();\n\t}, content->lifetime());\n}\n\n[[nodiscard]] PickerVenueList ParseVenues(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPmessages_BotResults &venues) {\n\tconst auto &data = venues.data();\n\tsession->data().processUsers(data.vusers());\n\n\tauto &list = data.vresults().v;\n\tauto result = PickerVenueList();\n\tresult.list.reserve(list.size());\n\tfor (const auto &found : list) {\n\t\tfound.match([&](const auto &data) {\n\t\t\tdata.vsend_message().match([&](\n\t\t\t\t\tconst MTPDbotInlineMessageMediaVenue &data) {\n\t\t\t\tdata.vgeo().match([&](const MTPDgeoPoint &geo) {\n\t\t\t\t\tresult.list.push_back({\n\t\t\t\t\t\t.lat = geo.vlat().v,\n\t\t\t\t\t\t.lon = geo.vlong().v,\n\t\t\t\t\t\t.title = qs(data.vtitle()),\n\t\t\t\t\t\t.address = qs(data.vaddress()),\n\t\t\t\t\t\t.provider = qs(data.vprovider()),\n\t\t\t\t\t\t.id = qs(data.vvenue_id()),\n\t\t\t\t\t\t.venueType = qs(data.vvenue_type()),\n\t\t\t\t\t});\n\t\t\t\t}, [](const auto &) {});\n\t\t\t}, [](const auto &) {});\n\t\t});\n\t}\n\treturn result;\n}\n\nnot_null<RpWidget*> SetupMapPlaceholder(\n\t\tnot_null<RpWidget*> parent,\n\t\tint minHeight,\n\t\tint maxHeight,\n\t\tFn<void()> choose) {\n\tconst auto result = CreateChild<RpWidget>(parent);\n\n\tconst auto top = CreateChild<BoxContentDivider>(result);\n\tconst auto bottom = CreateChild<BoxContentDivider>(result);\n\n\tconst auto icon = CreateChild<RpWidget>(result);\n\tconst auto iconSize = st::settingsCloudPasswordIconSize;\n\tauto ownedLottie = Lottie::MakeIcon({\n\t\t.name = u\"location\"_q,\n\t\t.sizeOverride = { iconSize, iconSize },\n\t\t.limitFps = true,\n\t});\n\tconst auto lottie = ownedLottie.get();\n\ticon->lifetime().add([kept = std::move(ownedLottie)] {});\n\n\ticon->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(icon);\n\t\tconst auto left = (icon->width() - iconSize) / 2;\n\t\tconst auto scale = icon->height() / float64(iconSize);\n\t\tauto hq = std::optional<PainterHighQualityEnabler>();\n\t\tif (scale < 1.) {\n\t\t\tconst auto center = QPointF(\n\t\t\t\ticon->width() / 2.,\n\t\t\t\ticon->height() / 2.);\n\t\t\thq.emplace(p);\n\t\t\tp.translate(center);\n\t\t\tp.scale(scale, scale);\n\t\t\tp.translate(-center);\n\t\t\tp.setOpacity(scale);\n\t\t}\n\t\tlottie->paint(p, left, 0);\n\t}, icon->lifetime());\n\n\tInvokeQueued(icon, [=] {\n\t\tconst auto till = lottie->framesCount() - 1;\n\t\tlottie->animate([=] { icon->update(); }, 0, till);\n\t});\n\n\tconst auto button = CreateChild<RoundButton>(\n\t\tresult,\n\t\ttr::lng_maps_select_on_map(),\n\t\tst::pickLocationChooseOnMap);\n\tbutton->setFullRadius(true);\n\tbutton->setClickedCallback(choose);\n\n\tparent->sizeValue() | rpl::on_next([=](QSize size) {\n\t\tresult->setGeometry(QRect(QPoint(), size));\n\n\t\tconst auto width = size.width();\n\t\ttop->setGeometry(0, 0, width, top->height());\n\t\tbottom->setGeometry(QRect(\n\t\t\tQPoint(0, size.height() - bottom->height()),\n\t\t\tQSize(width, bottom->height())));\n\t\tconst auto dividers = top->height() + bottom->height();\n\n\t\tconst auto ratio = (size.height() - minHeight)\n\t\t\t/ float64(maxHeight - minHeight);\n\t\tconst auto iconHeight = int(base::SafeRound(ratio * iconSize));\n\n\t\tconst auto available = size.height() - dividers;\n\t\tconst auto maxDelta = (maxHeight\n\t\t\t- dividers\n\t\t\t- iconSize\n\t\t\t- button->height()) / 2;\n\t\tconst auto minDelta = (minHeight - dividers - button->height()) / 2;\n\n\t\tconst auto delta = anim::interpolate(minDelta, maxDelta, ratio);\n\t\tbutton->move(\n\t\t\t(width - button->width()) / 2,\n\t\t\tsize.height() - bottom->height() - delta - button->height());\n\t\tconst auto wide = available - delta - button->height();\n\t\tconst auto skip = (wide - iconHeight) / 2;\n\t\ticon->setGeometry(0, top->height() + skip, width, iconHeight);\n\t}, result->lifetime());\n\n\ttop->show();\n\ticon->show();\n\tbottom->show();\n\tresult->show();\n\n\treturn result;\n}\n\n} // namespace\n\nLocationPicker::LocationPicker(Descriptor &&descriptor)\n: _config(std::move(descriptor.config))\n, _callback(std::move(descriptor.callback))\n, _quit(std::move(descriptor.quit))\n, _window(std::make_unique<SeparatePanel>())\n, _body((_window->setInnerSize(st::pickLocationWindow)\n\t, _window->showInner(base::make_unique_q<RpWidget>(_window.get()))\n\t, _window->inner()))\n, _chooseButtonLabel(std::move(descriptor.chooseLabel))\n, _webviewStorageId(descriptor.storageId)\n, _updateStyles([=] {\n\tconst auto str = EscapeForScriptString(ComputeStyles());\n\tif (_webview) {\n\t\t_webview->eval(\"LocationPicker.updateStyles('\" + str + \"');\");\n\t}\n})\n, _geocoderResolveTimer([=] { resolveAddressByTimer(); })\n, _venueState(PickerVenueLoading())\n, _session(descriptor.session)\n, _venuesSearchDebounceTimer([=] {\n\tExpects(_venuesSearchLocation.has_value());\n\tExpects(_venuesSearchQuery.has_value());\n\n\tvenuesRequest(*_venuesSearchLocation, *_venuesSearchQuery);\n})\n, _api(&_session->mtp())\n, _venueRecipient(descriptor.recipient) {\n\tstd::move(\n\t\tdescriptor.closeRequests\n\t) | rpl::on_next([=] {\n\t\t_window = nullptr;\n\t\tdelete this;\n\t}, _lifetime);\n\n\tsetup(descriptor);\n}\n\nstd::shared_ptr<Main::SessionShow> LocationPicker::uiShow() {\n\treturn Main::MakeSessionShow(nullptr, _session);\n}\n\nbool LocationPicker::Available(const LocationPickerConfig &config) {\n#ifdef _DEBUG\n\treturn true;\n#endif\n\tconst auto &availability = Core::CachedWebviewAvailability();\n\treturn availability.customSchemeRequests\n\t\t&& availability.customReferer\n\t\t&& !config.mapsToken.isEmpty();\n}\n\nvoid LocationPicker::setup(const Descriptor &descriptor) {\n\tsetupWindow(descriptor);\n\n\t_initialProvided = descriptor.initial;\n\tconst auto initial = _initialProvided.exact()\n\t\t? _initialProvided\n\t\t: LastExactLocation;\n\tif (initial) {\n\t\tvenuesRequest(initial);\n\t\tresolveAddress(initial);\n\t\tvenuesSearchEnableAt(initial);\n\t}\n\tif (!_initialProvided) {\n\t\tresolveCurrentLocation();\n\t}\n}\n\nvoid LocationPicker::setupWindow(const Descriptor &descriptor) {\n\tconst auto window = _window.get();\n\n\twindow->setWindowFlag(Qt::WindowStaysOnTopHint, false);\n\twindow->closeRequests() | rpl::on_next([=] {\n\t\tclose();\n\t}, _lifetime);\n\n\tconst auto parent = descriptor.parent\n\t\t? descriptor.parent->window()->geometry()\n\t\t: QGuiApplication::primaryScreen()->availableGeometry();\n\twindow->setTitle(tr::lng_maps_point());\n\twindow->move(\n\t\tparent.x() + (parent.width() - window->width()) / 2,\n\t\tparent.y() + (parent.height() - window->height()) / 2);\n\n\t_container = CreateChild<RpWidget>(_body.get());\n\t_mapPlaceholderAdded = st::pickLocationButtonSkip\n\t\t+ st::pickLocationButton.height\n\t\t+ st::pickLocationButtonSkip\n\t\t+ st::boxDividerHeight;\n\tconst auto min = st::pickLocationCollapsedHeight + _mapPlaceholderAdded;\n\tconst auto max = st::pickLocationMapHeight + _mapPlaceholderAdded;\n\t_mapPlaceholder = SetupMapPlaceholder(_container, min, max, [=] {\n\t\tsetupWebview();\n\t});\n\t_scroll = CreateChild<ScrollArea>(_body.get());\n\tconst auto controls = _scroll->setOwnedWidget(\n\t\tobject_ptr<VerticalLayout>(_scroll));\n\n\t_mapControlsWrap = controls->add(\n\t\tobject_ptr<SlideWrap<VerticalLayout>>(\n\t\t\tcontrols,\n\t\t\tobject_ptr<VerticalLayout>(controls)));\n\t_mapControlsWrap->show(anim::type::instant);\n\tconst auto mapControls = _mapControlsWrap->entity();\n\n\tconst auto toppad = mapControls->add(object_ptr<RpWidget>(controls));\n\n\tAddSkip(mapControls);\n\tAddSubsectionTitle(mapControls, tr::lng_maps_or_choose());\n\n\tauto state = _venueState.value();\n\tSetupVenues(controls, uiShow(), std::move(state), [=](VenueData info) {\n\t\t_callback(std::move(info));\n\t\tclose();\n\t});\n\n\trpl::combine(\n\t\t_body->sizeValue(),\n\t\t_scroll->scrollTopValue(),\n\t\t_venuesSearchShown.value()\n\t) | rpl::on_next([=](QSize size, int scrollTop, bool search) {\n\t\tconst auto width = size.width();\n\t\tconst auto height = size.height();\n\t\tconst auto sub = std::min(\n\t\t\t(st::pickLocationMapHeight - st::pickLocationCollapsedHeight),\n\t\t\tscrollTop);\n\t\tconst auto mapHeight = st::pickLocationMapHeight\n\t\t\t- sub\n\t\t\t+ (_mapPlaceholder ? _mapPlaceholderAdded : 0);\n\t\t_container->setGeometry(0, 0, width, mapHeight);\n\t\tconst auto scrollWidgetTop = search ? 0 : mapHeight;\n\t\tconst auto scrollHeight = height - scrollWidgetTop;\n\t\t_scroll->setGeometry(0, scrollWidgetTop, width, scrollHeight);\n\t\tcontrols->resizeToWidth(width);\n\t\ttoppad->resize(width, sub);\n\t}, _container->lifetime());\n\n\t_container->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tQPainter(_container).fillRect(clip, st::windowBg);\n\t}, _container->lifetime());\n\n\t_container->show();\n\t_scroll->show();\n\tcontrols->show();\n\twindow->show();\n}\n\nvoid LocationPicker::setupWebview() {\n\tExpects(!_webview);\n\n\tdelete base::take(_mapPlaceholder);\n\n\tconst auto mapControls = _mapControlsWrap->entity();\n\tmapControls->insert(\n\t\t1,\n\t\tobject_ptr<BoxContentDivider>(mapControls)\n\t)->show();\n\n\t_mapButton = mapControls->insert(\n\t\t1,\n\t\tMakeChooseLocationButton(\n\t\t\tmapControls,\n\t\t\t_chooseButtonLabel.value(),\n\t\t\t_geocoderAddress.value()),\n\t\t{ 0, st::pickLocationButtonSkip, 0, st::pickLocationButtonSkip });\n\t_mapButton->setClickedCallback([=] {\n\t\t_webview->eval(\"LocationPicker.send();\");\n\t});\n\t_mapButton->hide();\n\n\t_scroll->scrollToY(0);\n\t_venuesSearchShown.force_assign(_venuesSearchShown.current());\n\n\t_mapLoading = CreateChild<RpWidget>(_body.get());\n\n\t_container->geometryValue() | rpl::on_next([=](QRect rect) {\n\t\t_mapLoading->setGeometry(rect);\n\t}, _mapLoading->lifetime());\n\n\tSetupLoadingView(_mapLoading);\n\t_mapLoading->show();\n\n\tconst auto window = _window.get();\n\t_webview = std::make_unique<Webview::Window>(\n\t\t_container,\n\t\tWebview::WindowConfig{\n\t\t\t.opaqueBg = st::windowBg->c,\n\t\t\t.storageId = _webviewStorageId,\n\t\t\t.safe = true,\n\t\t});\n\tconst auto raw = _webview.get();\n\n\twindow->lifetime().add([=] {\n\t\t_webview = nullptr;\n\t});\n\n\twindow->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::Close) {\n\t\t\tclose();\n\t\t} else if (e->type() == QEvent::KeyPress) {\n\t\t\tconst auto event = static_cast<QKeyEvent*>(e.get());\n\t\t\tif (event->key() == Qt::Key_Escape && !_venuesSearchQuery) {\n\t\t\t\tclose();\n\t\t\t}\n\t\t}\n\t}, window->lifetime());\n\traw->widget()->show();\n\n\t_container->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\traw->widget()->setGeometry(QRect(QPoint(), size));\n\t}, _container->lifetime());\n\n\traw->setNavigationStartHandler([=](const QString &uri, bool newWindow) {\n\t\treturn true;\n\t});\n\traw->setNavigationDoneHandler([=](bool success) {\n\t});\n\traw->setMessageHandler([=](const QJsonDocument &message) {\n\t\tcrl::on_main(_window.get(), [=] {\n\t\t\tconst auto object = message.object();\n\t\t\tconst auto event = object.value(\"event\").toString();\n\t\t\tif (event == u\"ready\"_q) {\n\t\t\t\tmapReady();\n\t\t\t} else if (event == u\"keydown\"_q) {\n\t\t\t\tconst auto key = object.value(\"key\").toString();\n\t\t\t\tconst auto modifier = object.value(\"modifier\").toString();\n\t\t\t\tprocessKey(key, modifier);\n\t\t\t} else if (event == u\"send\"_q) {\n\t\t\t\tconst auto lat = object.value(\"latitude\").toDouble();\n\t\t\t\tconst auto lon = object.value(\"longitude\").toDouble();\n\t\t\t\t_callback({\n\t\t\t\t\t.lat = lat,\n\t\t\t\t\t.lon = lon,\n\t\t\t\t\t.address = _geocoderAddress.current(),\n\t\t\t\t});\n\t\t\t\tclose();\n\t\t\t} else if (event == u\"move_start\"_q) {\n\t\t\t\tif (const auto now = _geocoderAddress.current()\n\t\t\t\t\t; !now.isEmpty()) {\n\t\t\t\t\t_geocoderSavedAddress = now;\n\t\t\t\t\t_geocoderAddress = QString();\n\t\t\t\t}\n\t\t\t\tbase::take(_geocoderResolvePostponed);\n\t\t\t\t_geocoderResolveTimer.cancel();\n\t\t\t} else if (event == u\"move_end\"_q) {\n\t\t\t\tconst auto lat = object.value(\"latitude\").toDouble();\n\t\t\t\tconst auto lon = object.value(\"longitude\").toDouble();\n\t\t\t\tconst auto location = Core::GeoLocation{\n\t\t\t\t\t.point = { lat, lon },\n\t\t\t\t\t.accuracy = Core::GeoLocationAccuracy::Exact,\n\t\t\t\t};\n\t\t\t\tif (AreTheSame(_geocoderResolvingFor, location)\n\t\t\t\t\t&& !_geocoderSavedAddress.isEmpty()) {\n\t\t\t\t\t_geocoderAddress = base::take(_geocoderSavedAddress);\n\t\t\t\t\t_geocoderResolveTimer.cancel();\n\t\t\t\t} else {\n\t\t\t\t\t_geocoderResolvePostponed = location;\n\t\t\t\t\t_geocoderResolveTimer.callOnce(kResolveAddressDelay);\n\t\t\t\t}\n\t\t\t\tif (!AreTheSame(_venuesRequestLocation, location)) {\n\t\t\t\t\t_webview->eval(\n\t\t\t\t\t\t\"LocationPicker.toggleSearchVenues(true);\");\n\t\t\t\t}\n\t\t\t\tvenuesSearchEnableAt(location);\n\t\t\t} else if (event == u\"search_venues\"_q) {\n\t\t\t\tconst auto lat = object.value(\"latitude\").toDouble();\n\t\t\t\tconst auto lon = object.value(\"longitude\").toDouble();\n\t\t\t\tvenuesRequest({\n\t\t\t\t\t.point = { lat, lon },\n\t\t\t\t\t.accuracy = Core::GeoLocationAccuracy::Exact,\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t});\n\traw->setDataRequestHandler([=](Webview::DataRequest request) {\n\t\tconst auto pos = request.id.find('#');\n\t\tif (pos != request.id.npos) {\n\t\t\trequest.id = request.id.substr(0, pos);\n\t\t}\n\t\tif (!request.id.starts_with(\"location/\")) {\n\t\t\treturn Webview::DataResult::Failed;\n\t\t}\n\t\tconst auto finishWith = [&](QByteArray data, std::string mime) {\n\t\t\trequest.done({\n\t\t\t\t.stream = std::make_unique<Webview::DataStreamFromMemory>(\n\t\t\t\t\tstd::move(data),\n\t\t\t\t\tstd::move(mime)),\n\t\t\t\t});\n\t\t\treturn Webview::DataResult::Done;\n\t\t};\n\t\tif (!_subscribedToColors) {\n\t\t\t_subscribedToColors = true;\n\n\t\t\trpl::merge(\n\t\t\t\tLang::Updated(),\n\t\t\t\tstyle::PaletteChanged()\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\t_updateStyles.call();\n\t\t\t}, _webview->lifetime());\n\t\t}\n\t\tconst auto id = std::string_view(request.id).substr(9);\n\t\tif (id == \"picker.html\") {\n\t\t\treturn finishWith(PickerContent(), \"text/html; charset=utf-8\");\n\t\t}\n\t\tconst auto css = id.ends_with(\".css\");\n\t\tconst auto js = !css && id.ends_with(\".js\");\n\t\tif (!css && !js) {\n\t\t\treturn Webview::DataResult::Failed;\n\t\t}\n\t\tconst auto qstring = QString::fromUtf8(id.data(), id.size());\n\t\tconst auto pattern = u\"^[a-zA-Z\\\\.\\\\-_0-9]+$\"_q;\n\t\tif (QRegularExpression(pattern).match(qstring).hasMatch()) {\n\t\t\tconst auto bytes = ReadResource(qstring);\n\t\t\tif (!bytes.isEmpty()) {\n\t\t\t\tconst auto mime = css ? \"text/css\" : \"text/javascript\";\n\t\t\t\treturn finishWith(bytes, mime);\n\t\t\t}\n\t\t}\n\t\treturn Webview::DataResult::Failed;\n\t});\n\n\traw->init(R\"()\");\n\traw->navigateToData(\"location/picker.html\");\n}\n\nvoid LocationPicker::resolveAddressByTimer() {\n\tif (const auto location = base::take(_geocoderResolvePostponed)) {\n\t\tresolveAddress(location);\n\t}\n}\n\nvoid LocationPicker::resolveAddress(Core::GeoLocation location) {\n\tif (AreTheSame(_geocoderResolvingFor, location)) {\n\t\treturn;\n\t}\n\t_geocoderResolvingFor = location;\n\tconst auto done = [=](Core::GeoAddress address) {\n\t\tif (!AreTheSame(_geocoderResolvingFor, location)) {\n\t\t\treturn;\n\t\t} else if (address) {\n\t\t\t_geocoderAddress = address.name;\n\t\t} else {\n\t\t\t_geocoderAddress = u\"(%1, %2)\"_q\n\t\t\t\t.arg(location.point.x(), 0, 'f')\n\t\t\t\t.arg(location.point.y(), 0, 'f');\n\t\t}\n\t};\n\tconst auto baseLangId = Lang::GetInstance().baseId();\n\tconst auto langId = baseLangId.isEmpty()\n\t\t? Lang::GetInstance().id()\n\t\t: baseLangId;\n\tconst auto nonEmptyId = langId.isEmpty() ? u\"en\"_q : langId;\n\tCore::ResolveLocationAddress(\n\t\tlocation,\n\t\tlangId,\n\t\t_config.geoToken,\n\t\tcrl::guard(this, done));\n}\n\nvoid LocationPicker::mapReady() {\n\tExpects(_scroll != nullptr);\n\n\tdelete base::take(_mapLoading);\n\n\tconst auto token = _config.mapsToken.toUtf8();\n\tconst auto center = DefaultCenter(_initialProvided);\n\tconst auto bounds = DefaultBounds();\n\tconst auto params = \"token: '\" + token + \"'\"\n\t\t+ \", center: \" + center\n\t\t+ \", bounds: \" + bounds;\n\t_webview->eval(\"LocationPicker.init({ \" + params + \" });\");\n\n\tconst auto handle = _window->window()->windowHandle();\n\tif (handle && QGuiApplication::focusWindow() == handle) {\n\t\t_webview->focus();\n\t}\n\t_mapButton->show();\n}\n\nbool LocationPicker::venuesFromCache(\n\t\tCore::GeoLocation location,\n\t\tQString query) {\n\tconst auto normalized = NormalizeVenuesQuery(query);\n\tauto &cache = _venuesCache[normalized];\n\tconst auto i = ranges::find_if(cache, [&](const VenuesCacheEntry &v) {\n\t\treturn AreTheSame(v.location, location);\n\t});\n\tif (i == end(cache)) {\n\t\treturn false;\n\t}\n\t_venuesRequestLocation = location;\n\t_venuesRequestQuery = normalized;\n\t_venuesInitialQuery = query;\n\tvenuesApplyResults(i->result);\n\treturn true;\n}\n\nvoid LocationPicker::venuesRequest(\n\t\tCore::GeoLocation location,\n\t\tQString query) {\n\tconst auto normalized = NormalizeVenuesQuery(query);\n\tif (AreTheSame(_venuesRequestLocation, location)\n\t\t&& _venuesRequestQuery == normalized) {\n\t\treturn;\n\t} else if (const auto oldRequestId = base::take(_venuesRequestId)) {\n\t\t_api.request(oldRequestId).cancel();\n\t}\n\t_venueState = PickerVenueLoading();\n\t_venuesRequestLocation = location;\n\t_venuesRequestQuery = normalized;\n\t_venuesInitialQuery = query;\n\tif (_venuesBot) {\n\t\tvenuesSendRequest();\n\t} else if (_venuesBotRequestId) {\n\t\treturn;\n\t}\n\tconst auto username = _session->serverConfig().venueSearchUsername;\n\t_venuesBotRequestId = _api.request(MTPcontacts_ResolveUsername(\n\t\tMTP_flags(0),\n\t\tMTP_string(username),\n\t\tMTP_string()\n\t)).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\tauto &data = result.data();\n\t\t_session->data().processUsers(data.vusers());\n\t\t_session->data().processChats(data.vchats());\n\t\tconst auto peer = _session->data().peerLoaded(\n\t\t\tpeerFromMTP(data.vpeer()));\n\t\tconst auto user = peer ? peer->asUser() : nullptr;\n\t\tif (user && user->isBotInlineGeo()) {\n\t\t\t_venuesBot = user;\n\t\t\tvenuesSendRequest();\n\t\t} else {\n\t\t\tLOG((\"API Error: Bad peer returned by: %1\").arg(username));\n\t\t}\n\t}).fail([=] {\n\t\tLOG((\"API Error: Error returned on lookup: %1\").arg(username));\n\t}).send();\n}\n\nvoid LocationPicker::venuesSendRequest() {\n\tExpects(_venuesBot != nullptr);\n\n\tif (_venuesRequestId || !_venuesRequestLocation) {\n\t\treturn;\n\t}\n\t_venuesRequestId = _api.request(MTPmessages_GetInlineBotResults(\n\t\tMTP_flags(MTPmessages_GetInlineBotResults::Flag::f_geo_point),\n\t\t_venuesBot->inputUser(),\n\t\t(_venueRecipient ? _venueRecipient->input() : MTP_inputPeerEmpty()),\n\t\tMTP_inputGeoPoint(\n\t\t\tMTP_flags(0),\n\t\t\tMTP_double(_venuesRequestLocation.point.x()),\n\t\t\tMTP_double(_venuesRequestLocation.point.y()),\n\t\t\tMTP_int(0)), // accuracy_radius\n\t\tMTP_string(_venuesRequestQuery),\n\t\tMTP_string() // offset\n\t)).done([=](const MTPmessages_BotResults &result) {\n\t\tauto parsed = ParseVenues(_session, result);\n\t\t_venuesCache[_venuesRequestQuery].push_back({\n\t\t\t.location = _venuesRequestLocation,\n\t\t\t.result = parsed,\n\t\t});\n\t\tvenuesApplyResults(std::move(parsed));\n\t}).fail([=] {\n\t\tvenuesApplyResults({});\n\t}).send();\n}\n\nvoid LocationPicker::venuesApplyResults(PickerVenueList venues) {\n\t_venuesRequestId = 0;\n\tif (venues.list.empty()) {\n\t\t_venueState = PickerVenueNothingFound{ _venuesInitialQuery };\n\t} else {\n\t\t_venueState = std::move(venues);\n\t}\n}\n\nvoid LocationPicker::venuesSearchEnableAt(Core::GeoLocation location) {\n\tif (!_venuesSearchLocation) {\n\t\t_window->setSearchAllowed(\n\t\t\ttr::lng_dlg_filter(),\n\t\t\t[=](std::optional<QString> query) {\n\t\t\t\tvenuesSearchChanged(query);\n\t\t\t});\n\t}\n\t_venuesSearchLocation = location;\n}\n\nvoid LocationPicker::venuesSearchChanged(\n\t\tconst std::optional<QString> &query) {\n\t_venuesSearchQuery = query;\n\n\tconst auto shown = query && !query->trimmed().isEmpty();\n\t_venuesSearchShown = shown;\n\tif (_container->isHidden() != shown) {\n\t\t_container->setVisible(!shown);\n\t\t_mapControlsWrap->toggle(!shown, anim::type::instant);\n\t\tif (shown) {\n\t\t\t_venuesNoSearchLocation = _venuesRequestLocation;\n\t\t} else if (_venuesNoSearchLocation) {\n\t\t\tif (!venuesFromCache(_venuesNoSearchLocation)) {\n\t\t\t\tvenuesRequest(_venuesNoSearchLocation);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (shown\n\t\t&& !venuesFromCache(\n\t\t\t*_venuesSearchLocation,\n\t\t\t*_venuesSearchQuery)) {\n\t\t_venueState = PickerVenueLoading();\n\t\t_venuesSearchDebounceTimer.callOnce(kSearchDebounceDelay);\n\t} else {\n\t\t_venuesSearchDebounceTimer.cancel();\n\t}\n}\n\nvoid LocationPicker::resolveCurrentLocation() {\n\tusing namespace Core;\n\tconst auto window = _window.get();\n\tResolveCurrentGeoLocation(crl::guard(window, [=](GeoLocation location) {\n\t\tconst auto changed = !AreTheSame(LastExactLocation, location);\n\t\tif (location.accuracy != GeoLocationAccuracy::Exact || !changed) {\n\t\t\tif (!_venuesSearchLocation) {\n\t\t\t\t_venueState = PickerVenueWaitingForLocation();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tLastExactLocation = location;\n\t\tif (location) {\n\t\t\tif (_venuesSearchQuery.value_or(QString()).isEmpty()) {\n\t\t\t\tvenuesRequest(location);\n\t\t\t}\n\t\t\tresolveAddress(location);\n\t\t}\n\t\tif (_webview) {\n\t\t\tconst auto point = QByteArray::number(location.point.x())\n\t\t\t\t+ \",\"_q\n\t\t\t\t+ QByteArray::number(location.point.y());\n\t\t\t_webview->eval(\"LocationPicker.narrowTo([\" + point + \"]);\");\n\t\t}\n\t}));\n}\n\nvoid LocationPicker::processKey(\n\t\tconst QString &key,\n\t\tconst QString &modifier) {\n\tconst auto ctrl = ::Platform::IsMac() ? u\"cmd\"_q : u\"ctrl\"_q;\n\tif (key == u\"escape\"_q) {\n\t\tif (!_window->closeSearch()) {\n\t\t\tclose();\n\t\t}\n\t} else if (key == u\"w\"_q && modifier == ctrl) {\n\t\tclose();\n\t} else if (key == u\"m\"_q && modifier == ctrl) {\n\t\tminimize();\n\t} else if (key == u\"q\"_q && modifier == ctrl) {\n\t\tquit();\n\t}\n}\n\nvoid LocationPicker::activate() {\n\tif (_window) {\n\t\t_window->activateWindow();\n\t}\n}\n\nvoid LocationPicker::close() {\n\tcrl::on_main(this, [=] {\n\t\t_window = nullptr;\n\t\tdelete this;\n\t});\n}\n\nvoid LocationPicker::minimize() {\n\tif (_window) {\n\t\t_window->setWindowState(_window->windowState()\n\t\t\t| Qt::WindowMinimized);\n\t}\n}\n\nvoid LocationPicker::quit() {\n\tif (const auto onstack = _quit) {\n\t\tonstack();\n\t}\n}\n\nnot_null<LocationPicker*> LocationPicker::Show(Descriptor &&descriptor) {\n\treturn new LocationPicker(std::move(descriptor));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/location_picker.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/invoke_queued.h\"\n#include \"base/timer.h\"\n#include \"base/weak_ptr.h\"\n#include \"core/current_geo_location.h\"\n#include \"mtproto/sender.h\"\n#include \"webview/webview_common.h\"\n\nnamespace Data {\nstruct InputVenue;\n} // namespace Data\n\nnamespace Main {\nclass Session;\nclass SessionShow;\n} // namespace Main\n\nnamespace Webview {\nclass Window;\n} // namespace Webview\n\nnamespace Ui {\n\nclass AbstractButton;\nclass SeparatePanel;\nclass RpWidget;\nclass ScrollArea;\nclass VerticalLayout;\ntemplate <typename Widget>\nclass SlideWrap;\n\nstruct PickerVenueLoading {\n\tfriend inline bool operator==(\n\t\tPickerVenueLoading,\n\t\tPickerVenueLoading) = default;\n};\n\nstruct PickerVenueNothingFound {\n\tQString query;\n\n\tfriend inline bool operator==(\n\t\tconst PickerVenueNothingFound&,\n\t\tconst PickerVenueNothingFound&) = default;\n};\n\nstruct PickerVenueWaitingForLocation {\n\tfriend inline bool operator==(\n\t\tPickerVenueWaitingForLocation,\n\t\tPickerVenueWaitingForLocation) = default;\n};\n\nstruct PickerVenueList {\n\tstd::vector<Data::InputVenue> list;\n\n\tfriend inline bool operator==(\n\t\tconst PickerVenueList&,\n\t\tconst PickerVenueList&) = default;\n};\n\nusing PickerVenueState = std::variant<\n\tPickerVenueLoading,\n\tPickerVenueNothingFound,\n\tPickerVenueWaitingForLocation,\n\tPickerVenueList>;\n\nstruct LocationPickerConfig {\n\tQString mapsToken;\n\tQString geoToken;\n};\n\nclass LocationPicker final : public base::has_weak_ptr {\npublic:\n\tstruct Descriptor {\n\t\tRpWidget *parent = nullptr;\n\t\tLocationPickerConfig config;\n\t\trpl::producer<QString> chooseLabel;\n\t\tPeerData *recipient = nullptr;\n\t\tnot_null<Main::Session*> session;\n\t\tCore::GeoLocation initial;\n\t\tFn<void(Data::InputVenue)> callback;\n\t\tFn<void()> quit;\n\t\tWebview::StorageId storageId;\n\t\trpl::producer<> closeRequests;\n\t};\n\n\t[[nodiscard]] static bool Available(const LocationPickerConfig &config);\n\tstatic not_null<LocationPicker*> Show(Descriptor &&descriptor);\n\n\tvoid activate();\n\tvoid close();\n\tvoid minimize();\n\tvoid quit();\n\nprivate:\n\tstruct VenuesCacheEntry {\n\t\tCore::GeoLocation location;\n\t\tPickerVenueList result;\n\t};\n\n\texplicit LocationPicker(Descriptor &&descriptor);\n\n\t[[nodiscard]] std::shared_ptr<Main::SessionShow> uiShow();\n\n\tvoid setup(const Descriptor &descriptor);\n\tvoid setupWindow(const Descriptor &descriptor);\n\tvoid setupWebview();\n\tvoid processKey(const QString &key, const QString &modifier);\n\tvoid resolveCurrentLocation();\n\tvoid resolveAddressByTimer();\n\tvoid resolveAddress(Core::GeoLocation location);\n\tvoid mapReady();\n\n\tbool venuesFromCache(Core::GeoLocation location, QString query = {});\n\tvoid venuesRequest(Core::GeoLocation location, QString query = {});\n\tvoid venuesSendRequest();\n\tvoid venuesApplyResults(PickerVenueList venues);\n\tvoid venuesSearchEnableAt(Core::GeoLocation location);\n\tvoid venuesSearchChanged(const std::optional<QString> &query);\n\n\tLocationPickerConfig _config;\n\tFn<void(Data::InputVenue)> _callback;\n\tFn<void()> _quit;\n\tstd::unique_ptr<SeparatePanel> _window;\n\tnot_null<RpWidget*> _body;\n\tRpWidget *_container = nullptr;\n\tRpWidget *_mapPlaceholder = nullptr;\n\tRpWidget *_mapLoading = nullptr;\n\tAbstractButton *_mapButton = nullptr;\n\tSlideWrap<VerticalLayout> *_mapControlsWrap = nullptr;\n\trpl::variable<QString> _chooseButtonLabel;\n\tScrollArea *_scroll = nullptr;\n\tWebview::StorageId _webviewStorageId;\n\tstd::unique_ptr<Webview::Window> _webview;\n\tSingleQueuedInvokation _updateStyles;\n\tCore::GeoLocation _initialProvided;\n\tint _mapPlaceholderAdded = 0;\n\tbool _subscribedToColors = false;\n\n\tbase::Timer _geocoderResolveTimer;\n\tCore::GeoLocation _geocoderResolvePostponed;\n\tCore::GeoLocation _geocoderResolvingFor;\n\tQString _geocoderSavedAddress;\n\trpl::variable<QString> _geocoderAddress;\n\n\trpl::variable<PickerVenueState> _venueState;\n\n\tconst not_null<Main::Session*> _session;\n\tstd::optional<Core::GeoLocation> _venuesSearchLocation;\n\tstd::optional<QString> _venuesSearchQuery;\n\tbase::Timer _venuesSearchDebounceTimer;\n\tMTP::Sender _api;\n\tPeerData *_venueRecipient = nullptr;\n\tUserData *_venuesBot = nullptr;\n\tmtpRequestId _venuesBotRequestId = 0;\n\tmtpRequestId _venuesRequestId = 0;\n\tCore::GeoLocation _venuesRequestLocation;\n\tQString _venuesRequestQuery;\n\tQString _venuesInitialQuery;\n\tbase::flat_map<QString, std::vector<VenuesCacheEntry>> _venuesCache;\n\tCore::GeoLocation _venuesNoSearchLocation;\n\trpl::variable<bool> _venuesSearchShown = false;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/peer_list_dummy.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/peer_list_dummy.h\"\n\n#include \"ui/painter.h\"\n#include \"styles/style_widgets.h\"\n\nPeerListDummy::PeerListDummy(\n\tQWidget *parent,\n\tint count,\n\tconst style::PeerList &st)\n: _st(st)\n, _count(count) {\n\tresize(width(), _count * _st.item.height);\n}\n\nvoid PeerListDummy::paintEvent(QPaintEvent *e) {\n\tQPainter p(this);\n\n\tPainterHighQualityEnabler hq(p);\n\n\tconst auto fill = e->rect();\n\tconst auto bottom = fill.top() + fill.height();\n\tconst auto from = std::clamp(fill.top() / _st.item.height, 0, _count);\n\tconst auto till = std::clamp(\n\t\t(bottom + _st.item.height - 1) / _st.item.height,\n\t\t0,\n\t\t_count);\n\tp.translate(0, _st.item.height * from);\n\tp.setPen(Qt::NoPen);\n\tfor (auto i = from; i != till; ++i) {\n\t\tp.setBrush(st::windowBgOver);\n\t\tp.drawEllipse(\n\t\t\t_st.item.photoPosition.x(),\n\t\t\t_st.item.photoPosition.y(),\n\t\t\t_st.item.photoSize,\n\t\t\t_st.item.photoSize);\n\n\t\tconst auto small = int(1.5 * _st.item.photoSize);\n\t\tconst auto large = 2 * small;\n\t\tconst auto second = (i % 2) ? large : small;\n\t\tconst auto height = _st.item.nameStyle.font->height / 2;\n\t\tconst auto radius = height / 2;\n\t\tconst auto left = _st.item.namePosition.x();\n\t\tconst auto top = _st.item.namePosition.y()\n\t\t\t+ (_st.item.nameStyle.font->height - height) / 2;\n\t\tconst auto skip = _st.item.namePosition.x()\n\t\t\t- _st.item.photoPosition.x()\n\t\t\t- _st.item.photoSize;\n\t\tconst auto next = left + small + skip;\n\t\tp.drawRoundedRect(left, top, small, height, radius, radius);\n\t\tp.drawRoundedRect(next, top, second, height, radius, radius);\n\n\t\tp.translate(0, _st.item.height);\n\t}\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/peer_list_dummy.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace style {\nstruct PeerList;\n} // namespace style\n\nclass PeerListDummy final : public Ui::RpWidget {\npublic:\n\tPeerListDummy(QWidget *parent, int count, const style::PeerList &st);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tconst style::PeerList &_st;\n\tint _count = 0;\n\n\tstd::vector<Ui::Animations::Simple> _animations;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/popup_selector.cpp",
    "content": "// This file is part of Telegram Desktop,\n// the official desktop application for the Telegram messaging service.\n//\n// For license and copyright information please follow this link:\n// https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n//\n#include \"ui/controls/popup_selector.h\"\n\n#include \"ui/painter.h\"\n#include \"ui/style/style_core.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/platform/ui_platform_utility.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_widgets.h\"\n\n#include <QtGui/QScreen>\n#include <QtWidgets/QApplication>\n#include <private/qapplication_p.h>\n\nnamespace Ui {\n\nPopupSelector::PopupSelector(\n\tnot_null<QWidget*> parent,\n\tQSize size,\n\tPopupAppearType appearType)\n: RpWidget(parent)\n, _innerSize(size)\n, _cachedRound(\n\tsize,\n\tst::reactionCornerShadow,\n\tstd::min(size.width(), size.height()))\n, _appearType(appearType) {\n\t_useTransparency = Platform::TranslucentWindowsSupported();\n\tconst auto margins = marginsForShadow();\n\tresize(size + QSize(\n\t\tmargins.left() + margins.right(),\n\t\tmargins.top() + margins.bottom()));\n\n\tsetWindowFlags(Qt::WindowFlags(Qt::FramelessWindowHint)\n\t\t| Qt::BypassWindowManagerHint\n\t\t| Qt::Popup\n\t\t| Qt::NoDropShadowWindowHint);\n\tsetMouseTracking(true);\n\tsetAttribute(Qt::WA_NoSystemBackground, true);\n\n\tif (_useTransparency) {\n\t\tsetAttribute(Qt::WA_TranslucentBackground, true);\n\t} else {\n\t\tsetAttribute(Qt::WA_TranslucentBackground, false);\n\t\tsetAttribute(Qt::WA_OpaquePaintEvent, true);\n\t}\n\n\tinstallEventFilter(this);\n\n\thide();\n}\n\nvoid PopupSelector::popup(const QPoint &p) {\n\tconst auto screen = QGuiApplication::screenAt(p);\n\tcreateWinId();\n\twindowHandle()->removeEventFilter(this);\n\twindowHandle()->installEventFilter(this);\n\tif (screen) {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)\n\t\tsetScreen(screen);\n#else\n\t\twindowHandle()->setScreen(screen);\n#endif\n\t}\n\n\tauto w = p;\n\tconst auto r = screen ? screen->availableGeometry() : QRect();\n\tif (!r.isNull()) {\n\t\tif (w.x() + width() > r.x() + r.width()) {\n\t\t\tw.setX(r.x() + r.width() - width());\n\t\t}\n\t\tif (w.x() < r.x()) {\n\t\t\tw.setX(r.x());\n\t\t}\n\t\tif (w.y() + height() > r.y() + r.height()) {\n\t\t\tw.setY(r.y() + r.height() - height());\n\t\t}\n\t\tif (w.y() < r.y()) {\n\t\t\tw.setY(r.y());\n\t\t}\n\t}\n\tmove(w);\n\tshow();\n\tPlatform::ShowOverAll(this);\n\traise();\n\tactivateWindow();\n}\n\nQMargins PopupSelector::marginsForShadow() const {\n\tconst auto line = st::lineWidth;\n\treturn _useTransparency\n\t\t? st::reactionCornerShadow\n\t\t: QMargins(line, line, line, line);\n}\n\nvoid PopupSelector::updateShowState(\n\t\tfloat64 progress,\n\t\tfloat64 opacity,\n\t\tbool appearing) {\n\tif (_appearing && !appearing && !_paintBuffer.isNull()) {\n\t\tpaintBackgroundToBuffer();\n\t}\n\t_appearing = appearing;\n\t_appearProgress = progress;\n\t_appearOpacity = opacity;\n\tif (_appearing && isHidden()) {\n\t\tshow();\n\t} else if (!_appearing && _appearOpacity == 0. && !isHidden()) {\n\t\thide();\n\t}\n\tupdate();\n}\n\nvoid PopupSelector::hideAnimated() {\n\tif (/*!isHidden() && */!_hiding) {\n\t\t_hiding = true;\n\t\t_hideAnimation.start([=] {\n\t\t\tconst auto progress = 1. - _hideAnimation.value(0.);\n\t\t\tupdateShowState(progress, progress, false);\n\t\t\tif (!_hideAnimation.animating()) {\n\t\t\t\t_hiding = false;\n\t\t\t\tif (_hideFinishedCallback) {\n\t\t\t\t\t_hideFinishedCallback();\n\t\t\t\t}\n\t\t\t}\n\t\t}, _appearProgress, 0., st::defaultPopupMenu.duration);\n\t}\n}\n\nvoid PopupSelector::setHideFinishedCallback(Fn<void()> callback) {\n\t_hideFinishedCallback = std::move(callback);\n}\n\nvoid PopupSelector::paintBackgroundToBuffer() {\n\tconst auto factor = style::DevicePixelRatio();\n\tif (_paintBuffer.size() != size() * factor) {\n\t\t_paintBuffer = _cachedRound.PrepareImage(size());\n\t}\n\t_paintBuffer.fill(Qt::transparent);\n\n\t_cachedRound.setBackgroundColor(st::boxBg->c);\n\t_cachedRound.setShadowColor(st::shadowFg->c);\n\n\tauto p = QPainter(&_paintBuffer);\n\tconst auto radius = std::min(_innerSize.width(), _innerSize.height()) / 2.;\n\tconst auto frame = _cachedRound.validateFrame(0, 1., radius);\n\tconst auto fill = _cachedRound.FillWithImage(p, rect(), frame);\n\tif (!fill.isEmpty()) {\n\t\tp.fillRect(fill, st::boxBg);\n\t}\n}\n\nvoid PopupSelector::paintAppearing(QPainter &p) {\n\tp.setOpacity(_appearOpacity);\n\tconst auto factor = style::DevicePixelRatio();\n\tif (_paintBuffer.size() != size() * factor) {\n\t\t_paintBuffer = _cachedRound.PrepareImage(size());\n\t}\n\t_paintBuffer.fill(st::boxBg->c);\n\n\tauto q = QPainter(&_paintBuffer);\n\tconst auto margins = marginsForShadow();\n\n\tif (_appearType == PopupAppearType::CenterExpand) {\n\t\tconst auto appearedWidth = anim::interpolate(\n\t\t\t_innerSize.height(),\n\t\t\t_innerSize.width(),\n\t\t\t_appearProgress);\n\t\tconst auto fullWidth = margins.left() + appearedWidth + margins.right();\n\t\tconst auto drawSize = QSize(fullWidth, height());\n\t\tconst auto offsetX = (width() - fullWidth) / 2;\n\n\t\t_cachedRound.setBackgroundColor(st::boxBg->c);\n\t\t_cachedRound.setShadowColor(st::shadowFg->c);\n\t\tconst auto radius\n\t\t\t= std::min(_innerSize.width(), _innerSize.height()) / 2.;\n\t\t_cachedRound.overlayExpandedBorder(\n\t\t\tq,\n\t\t\tdrawSize,\n\t\t\t_appearProgress,\n\t\t\tradius,\n\t\t\tradius,\n\t\t\t1.);\n\t\tq.end();\n\n\t\tp.drawImage(\n\t\t\tQRect(QPoint(offsetX, 0), drawSize),\n\t\t\t_paintBuffer,\n\t\t\tQRect(QPoint(), drawSize * factor));\n\t} else if (_appearType == PopupAppearType::RightToLeft) {\n\t\tconst auto appearedWidth = anim::interpolate(\n\t\t\t_innerSize.height(),\n\t\t\t_innerSize.width(),\n\t\t\t_appearProgress);\n\t\tconst auto fullWidth = margins.left()\n\t\t\t+ appearedWidth\n\t\t\t+ margins.right();\n\t\tconst auto drawSize = QSize(fullWidth, height());\n\t\tconst auto offsetX = width() - fullWidth;\n\n\t\t_cachedRound.setBackgroundColor(st::boxBg->c);\n\t\t_cachedRound.setShadowColor(st::shadowFg->c);\n\t\tconst auto radius\n\t\t\t= std::min(_innerSize.width(), _innerSize.height()) / 2.;\n\t\t_cachedRound.overlayExpandedBorder(\n\t\t\tq,\n\t\t\tdrawSize,\n\t\t\t_appearProgress,\n\t\t\tradius,\n\t\t\tradius,\n\t\t\t1.);\n\t\tq.end();\n\n\t\tp.drawImage(\n\t\t\tQRect(QPoint(offsetX, 0), drawSize),\n\t\t\t_paintBuffer,\n\t\t\tQRect(QPoint(), drawSize * factor));\n\t} else {\n\t\tconst auto appearedWidth = anim::interpolate(\n\t\t\t_innerSize.height(),\n\t\t\t_innerSize.width(),\n\t\t\t_appearProgress);\n\t\tconst auto fullWidth = margins.left() + appearedWidth + margins.right();\n\t\tconst auto drawSize = QSize(fullWidth, height());\n\n\t\t_cachedRound.setBackgroundColor(st::boxBg->c);\n\t\t_cachedRound.setShadowColor(st::shadowFg->c);\n\t\tconst auto radius\n\t\t\t= std::min(_innerSize.width(), _innerSize.height()) / 2.;\n\t\t_cachedRound.overlayExpandedBorder(\n\t\t\tq,\n\t\t\tdrawSize,\n\t\t\t_appearProgress,\n\t\t\tradius,\n\t\t\tradius,\n\t\t\t1.);\n\t\tq.end();\n\n\t}\n}\n\nvoid PopupSelector::paintCollapsed(QPainter &p) {\n\tif (_paintBuffer.isNull()) {\n\t\tpaintBackgroundToBuffer();\n\t}\n\tp.drawImage(0, 0, _paintBuffer);\n}\n\nvoid PopupSelector::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\tif (!_useTransparency) {\n\t\tp.fillRect(rect(), st::boxBg);\n\t}\n\tif (_appearing) {\n\t\tpaintAppearing(p);\n\t} else {\n\t\tpaintCollapsed(p);\n\t}\n}\n\nbool PopupSelector::eventFilter(QObject *obj, QEvent *e) {\n\tconst auto type = e->type();\n\tif (type == QEvent::TouchBegin\n\t\t|| type == QEvent::TouchUpdate\n\t\t|| type == QEvent::TouchEnd) {\n\t\tif (obj == windowHandle() && isActiveWindow()) {\n\t\t\tconst auto event = static_cast<QTouchEvent*>(e);\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n\t\t\te->setAccepted(\n\t\t\t\tQApplicationPrivate::translateRawTouchEvent(\n\t\t\t\t\tthis,\n\t\t\t\t\tevent->device(),\n\t\t\t\t\tevent->touchPoints(),\n\t\t\t\t\tevent->timestamp()));\n#elif QT_VERSION < QT_VERSION_CHECK(6, 2, 0)\n\t\t\te->setAccepted(\n\t\t\t\tQApplicationPrivate::translateRawTouchEvent(\n\t\t\t\t\tthis,\n\t\t\t\t\tevent->pointingDevice(),\n\t\t\t\t\tconst_cast<QList<QEventPoint> &>(event->points()),\n\t\t\t\t\tevent->timestamp()));\n#else\n\t\t\te->setAccepted(\n\t\t\t\tQApplicationPrivate::translateRawTouchEvent(this, event));\n#endif\n\t\t\treturn e->isAccepted();\n\t\t}\n\t}\n\treturn RpWidget::eventFilter(obj, e);\n}\n\nvoid PopupSelector::mousePressEvent(QMouseEvent *e) {\n\tif (!rect().contains(e->pos())) {\n\t\thideAnimated();\n\t}\n}\n\nvoid PopupSelector::focusOutEvent(QFocusEvent *e) {\n\thideAnimated();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/popup_selector.h",
    "content": "// This file is part of Telegram Desktop,\n// the official desktop application for the Telegram messaging service.\n//\n// For license and copyright information please follow this link:\n// https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n//\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/round_area_with_shadow.h\"\n\nnamespace Ui {\n\nenum class PopupAppearType {\n\tLeftToRight,\n\tRightToLeft,\n\tCenterExpand,\n};\n\nclass PopupSelector final : public RpWidget {\npublic:\n\tPopupSelector(\n\t\tnot_null<QWidget*> parent,\n\t\tQSize size,\n\t\tPopupAppearType appearType = PopupAppearType::CenterExpand);\n\n\tvoid updateShowState(\n\t\tfloat64 progress,\n\t\tfloat64 opacity,\n\t\tbool appearing);\n\tvoid hideAnimated();\n\tvoid popup(const QPoint &p);\n\tvoid setHideFinishedCallback(Fn<void()> callback);\n\n\t[[nodiscard]] QMargins marginsForShadow() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tbool eventFilter(QObject *obj, QEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid focusOutEvent(QFocusEvent *e) override;\n\nprivate:\n\tvoid paintBackgroundToBuffer();\n\tvoid paintAppearing(QPainter &p);\n\tvoid paintCollapsed(QPainter &p);\n\n\tconst QSize _innerSize;\n\tRoundAreaWithShadow _cachedRound;\n\tQImage _paintBuffer;\n\tPopupAppearType _appearType = PopupAppearType::CenterExpand;\n\n\tfloat64 _appearProgress = 0.;\n\tfloat64 _appearOpacity = 0.;\n\tbool _appearing = false;\n\tbool _useTransparency = false;\n\tbool _hiding = false;\n\tAnimations::Simple _hideAnimation;\n\tFn<void()> _hideFinishedCallback;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/round_video_recorder.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/round_video_recorder.h\"\n\n#include \"base/concurrent_timer.h\"\n#include \"base/debug_log.h\"\n#include \"ffmpeg/ffmpeg_bytes_io_wrap.h\"\n#include \"ffmpeg/ffmpeg_utility.h\"\n#include \"media/audio/media_audio_capture.h\"\n#include \"ui/controls/round_video_recorder_data.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/arc_angles.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/painter.h\"\n#include \"ui/rp_widget.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kSide = 400;\nconstexpr auto kUpdateEach = crl::time(100);\nconstexpr auto kAudioFrequency = 48'000;\nconstexpr auto kAudioBitRate = 64 * 1024;\nconstexpr auto kVideoBitRate = 2 * 1024 * 1024;\nconstexpr auto kMinDuration = crl::time(200);\nconstexpr auto kMaxDuration = 60 * crl::time(1000);\nconstexpr auto kInitTimeout = 5 * crl::time(1000);\nconstexpr auto kBlurredSize = 64;\nconstexpr auto kMinithumbsPerSecond = 5;\nconstexpr auto kMinithumbsInRow = 16;\nconstexpr auto kFadeDuration = crl::time(150);\nconstexpr auto kSkipFrames = 8;\nconstexpr auto kMinScale = 0.7;\nconstexpr auto &kPlainLogoFrames = RoundVideoData::kLogoFrames;\nconstexpr auto kLogoSize = RoundVideoData::kLogoSize;\nconstexpr auto kLogoXShift = -10;\nconstexpr auto kLogoYShift = 10;\nconstexpr auto kOverlayOpacity = 0.1;\nconstexpr auto kOverlayOpaque = 1. - kOverlayOpacity;\nconstexpr auto kOverlayUVOpaque = 128 * kOverlayOpaque;\n\nusing namespace FFmpeg;\n\n[[nodiscard]] int MinithumbSize() {\n\tconst auto full = st::historySendSize.height();\n\tconst auto margin = st::historyRecordWaveformBgMargins;\n\tconst auto outer = full - margin.top() - margin.bottom();\n\tconst auto inner = outer - 2 * st::msgWaveformMin;\n\treturn inner * style::DevicePixelRatio();\n}\n\n[[nodiscard]] QImage CircularTextImage(\n\t\tconst QString &text,\n\t\tint width,\n\t\tint height,\n\t\tint radius,\n\t\tfloat64 startAngle = 0.0,\n\t\tfloat64 endAngle = 360.0,\n\t\tconst QColor &textColor = Qt::black,\n\t\tconst QColor &bgColor = Qt::white,\n\t\tconst QFont &font = QFont(),\n\t\tbool reverseDirection = false) {\n\tauto image = QImage(width, height, QImage::Format_ARGB32);\n\timage.fill(bgColor);\n\n\tauto painter = QPainter(&image);\n\tpainter.setRenderHint(QPainter::Antialiasing, true);\n\tpainter.setPen(textColor);\n\tpainter.setFont(font);\n\n\tauto center = QPoint(width / 2, height / 2);\n\tpainter.translate(center);\n\n\tif (endAngle < startAngle) {\n\t\tstd::swap(startAngle, endAngle);\n\t}\n\n\tconst auto startRad = float64(startAngle - 90) * M_PI / 180.0;\n\tconst auto endRad = float64(endAngle - 90) * M_PI / 180.0;\n\tconst auto angleRange = float64(endRad) - float64(startRad);\n\n\tconst auto &metrics = QFontMetrics(font);\n\n\tfor (auto i = 0; i < text.length(); ++i) {\n\t\tconst auto ratio = (text.length() <= 1)\n\t\t\t? 0.5\n\t\t\t: reverseDirection\n\t\t\t? 1.0 - static_cast<float64>(i) / (text.length() - 1)\n\t\t\t: static_cast<float64>(i) / (text.length() - 1);\n\n\t\tconst auto angle = startRad + ratio * angleRange;\n\n\t\tconst auto x = radius * std::cos(angle);\n\t\tconst auto y = radius * std::sin(angle);\n\n\t\tconst auto degrees = (angle * 180.0 / M_PI) - 90;\n\t\tpainter.save();\n\t\tpainter.translate(x, y);\n\t\tpainter.rotate(degrees);\n\t\tconst auto offset = (i == text.length() - 1) ? 2. : 0.;\n\t\tpainter.drawText(\n\t\t\t-metrics.horizontalAdvance(text[i]) / 2 + offset,\n\t\t\tmetrics.ascent() / 2,\n\t\t\tQString(text[i]));\n\t\tpainter.restore();\n\t}\n\n\treturn image;\n}\n\nusing PrecomputedLogo = std::array<std::array<float, kLogoSize>, kLogoSize>;\n[[nodiscard]] const std::vector<PrecomputedLogo> &PrecomputedLogos() {\n\tstatic std::vector<PrecomputedLogo> precomputedLogos;\n\n\tif (!precomputedLogos.empty()) {\n\t\treturn precomputedLogos;\n\t}\n\tconstexpr auto kAntialiasRadius = 0.4;\n\tprecomputedLogos.resize(kPlainLogoFrames.size());\n\tconst auto antialiasFactor = 1.0\n\t\t/ (2. * kAntialiasRadius * kAntialiasRadius);\n\n\tfor (auto index = size_t(0); index < kPlainLogoFrames.size(); ++index) {\n\t\tuint8_t logoFrame[kLogoSize][kLogoSize] = {{ 0 }};\n\t\tRoundVideoData::DecompressLogoRLEFrame(\n\t\t\tkPlainLogoFrames[index],\n\t\t\tlogoFrame);\n\n\t\tfor (auto y = 0; y < kLogoSize; ++y) {\n\t\t\tfor (auto x = 0; x < kLogoSize; ++x) {\n\t\t\t\tauto blendedValue = 0.;\n\t\t\t\tauto weightSum = 0.;\n\n\t\t\t\tconst auto minY = std::max(0, y - 1);\n\t\t\t\tconst auto maxY = std::min(kLogoSize - 1, y + 1);\n\t\t\t\tconst auto minX = std::max(0, x - 1);\n\t\t\t\tconst auto maxX = std::min(kLogoSize - 1, x + 1);\n\n\t\t\t\tfor (auto sampleY = minY; sampleY <= maxY; ++sampleY) {\n\t\t\t\t\tconst auto dy = sampleY - y;\n\t\t\t\t\tfor (auto sampleX = minX; sampleX <= maxX; ++sampleX) {\n\t\t\t\t\t\tconst auto dx = sampleX - x;\n\t\t\t\t\t\tconst auto distanceSq = dx * dx + dy * dy;\n\t\t\t\t\t\tconst auto weight\n\t\t\t\t\t\t\t= std::exp(-distanceSq * antialiasFactor);\n\n\t\t\t\t\t\tblendedValue += logoFrame[sampleY][sampleX] * weight;\n\t\t\t\t\t\tweightSum += weight;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tprecomputedLogos[index][y][x] = (weightSum > 0)\n\t\t\t\t\t? (blendedValue / weightSum)\n\t\t\t\t\t: 0;\n\t\t\t}\n\t\t}\n\t}\n\treturn precomputedLogos;\n}\n\n} // namespace\n\nclass RoundVideoRecorder::Private final {\npublic:\n\tPrivate(crl::weak_on_queue<Private> weak, int minithumbSize);\n\t~Private();\n\n\tvoid push(int64 mcstimestamp, const QImage &frame);\n\tvoid push(const Media::Capture::Chunk &chunk);\n\n\tusing Update = Media::Capture::Update;\n\t[[nodiscard]] rpl::producer<Update, Error> updated() const;\n\n\t[[nodiscard]] RoundVideoResult finish();\n\tvoid restart(RoundVideoPartial partial);\n\nprivate:\n\tstatic constexpr auto kMaxStreams = 2;\n\n\tstruct CopyContext {\n\t\tCopyContext();\n\n\t\tstd::array<int64, kMaxStreams> lastPts = { 0 };\n\t\tstd::array<int64, kMaxStreams> lastDts = { 0 };\n\t};\n\n\tvoid initEncoding();\n\tvoid initCircleMask();\n\tvoid initCircularTextImage();\n\tvoid initMinithumbsCanvas();\n\tvoid maybeSaveMinithumb(\n\t\tnot_null<AVFrame*> frame,\n\t\tconst QImage &original,\n\t\tQRect crop);\n\n\tbool initVideo();\n\tbool initAudio();\n\tvoid notifyFinished();\n\tvoid deinitEncoding();\n\tvoid finishEncoding();\n\tvoid fail(Error error);\n\tvoid timeout();\n\n\tvoid encodeVideoFrame(int64 mcstimestamp, const QImage &frame);\n\tvoid encodeAudioFrame(const Media::Capture::Chunk &chunk);\n\tbool writeFrame(\n\t\tconst FramePointer &frame,\n\t\tconst CodecPointer &codec,\n\t\tAVStream *stream);\n\n\tvoid updateMaxLevel(const Media::Capture::Chunk &chunk);\n\tvoid updateResultDuration(int64 pts, AVRational timeBase);\n\n\tvoid mirrorYUV420P(not_null<AVFrame*> frame);\n\tvoid drawLogoOnYUV420P(not_null<AVFrame*> frame);\n\n\t[[nodiscard]] RoundVideoResult appendToPrevious(RoundVideoResult video);\n\t[[nodiscard]] static FormatPointer OpenInputContext(\n\t\tnot_null<const QByteArray*> data,\n\t\tnot_null<ReadBytesWrap*> wrap);\n\t[[nodiscard]] bool copyPackets(\n\t\tnot_null<AVFormatContext*> input,\n\t\tnot_null<AVFormatContext*> output,\n\t\tCopyContext &context,\n\t\tcrl::time offset = 0);\n\n\tconst crl::weak_on_queue<Private> _weak;\n\n\tFormatPointer _format;\n\n\tAVStream *_videoStream = nullptr;\n\tCodecPointer _videoCodec;\n\tFramePointer _videoFrame;\n\tSwscalePointer _swsContext;\n\tint64_t _videoPts = 0;\n\n\t// This is the first recorded frame timestamp in microseconds.\n\tint64_t _videoFirstTimestamp = -1;\n\n\t// Audio-related members\n\tAVStream *_audioStream = nullptr;\n\tCodecPointer _audioCodec;\n\tFramePointer _audioFrame;\n\tSwresamplePointer _swrContext;\n\tQByteArray _audioTail;\n\tint64_t _audioPts = 0;\n\tint _audioChannels = 0;\n\n\t// Those timestamps are in 'ms' used for sync between audio and video.\n\tcrl::time _firstAudioChunkFinished = 0;\n\tcrl::time _firstVideoFrameTime = 0;\n\n\tWriteBytesWrap _result;\n\tcrl::time _resultDuration = 0;\n\tbool _finished = false;\n\n\tushort _maxLevelSinceLastUpdate = 0;\n\tcrl::time _lastUpdateDuration = 0;\n\trpl::event_stream<Update, Error> _updates;\n\n\tcrl::time _minithumbNextTimestamp = 0;\n\tconst int _minithumbSize = 0;\n\tint _minithumbsCount = 0;\n\tQImage _minithumbs;\n\n\tcrl::time _maxDuration = 0;\n\tRoundVideoResult _previous;\n\n\tReadBytesWrap _forConcat1, _forConcat2;\n\n\tuint8_t _logoFrameCounter = 0;\n\tQImage _circularTextImage;\n\n\tstd::vector<bool> _circleMask; // Always nice to use vector<bool>! :D\n\n\tbase::ConcurrentTimer _timeoutTimer;\n\n};\n\nRoundVideoRecorder::Private::CopyContext::CopyContext() {\n\tranges::fill(lastPts, std::numeric_limits<int64>::min());\n\tranges::fill(lastDts, std::numeric_limits<int64>::min());\n}\n\nRoundVideoRecorder::Private::Private(\n\tcrl::weak_on_queue<Private> weak,\n\tint minithumbSize)\n: _weak(std::move(weak))\n, _minithumbSize(minithumbSize)\n, _maxDuration(kMaxDuration)\n, _timeoutTimer(_weak, [=] { timeout(); }) {\n\tinitEncoding();\n\tinitCircleMask();\n\tinitCircularTextImage();\n\tinitMinithumbsCanvas();\n\n\t_timeoutTimer.callOnce(kInitTimeout);\n}\n\nRoundVideoRecorder::Private::~Private() {\n\tfinishEncoding();\n}\n\nvoid RoundVideoRecorder::Private::initEncoding() {\n\t_format = MakeWriteFormatPointer(\n\t\tstatic_cast<void*>(&_result),\n\t\tnullptr,\n\t\t&WriteBytesWrap::Write,\n\t\t&WriteBytesWrap::Seek,\n\t\t\"mp4\"_q);\n\n\tif (!initVideo()) {\n\t\tfail(Error::VideoInit);\n\t\treturn;\n\t} else if (!initAudio()) {\n\t\tfail(Error::AudioInit);\n\t\treturn;\n\t}\n\n\tconst auto error = AvErrorWrap(avformat_write_header(\n\t\t_format.get(),\n\t\tnullptr));\n\tif (error) {\n\t\tLogError(\"avformat_write_header\", error);\n\t\tfail(Error::Encoding);\n\t}\n}\n\nbool RoundVideoRecorder::Private::initVideo() {\n\tif (!_format) {\n\t\treturn false;\n\t}\n\n\tauto videoCodec = avcodec_find_encoder_by_name(\"libopenh264\");\n\tif (!videoCodec) {\n\t\tvideoCodec = avcodec_find_encoder(AV_CODEC_ID_H264);\n\t\tif (!videoCodec) {\n\t\t\tLogError(\"avcodec_find_encoder\", \"AV_CODEC_ID_H264\");\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t_videoStream = avformat_new_stream(_format.get(), videoCodec);\n\tif (!_videoStream) {\n\t\tLogError(\"avformat_new_stream\", \"libopenh264\");\n\t\treturn false;\n\t}\n\n\t_videoCodec = CodecPointer(avcodec_alloc_context3(videoCodec));\n\tif (!_videoCodec) {\n\t\tLogError(\"avcodec_alloc_context3\", \"libopenh264\");\n\t\treturn false;\n\t}\n\n\t_videoCodec->codec_id = videoCodec->id;\n\t_videoCodec->codec_type = AVMEDIA_TYPE_VIDEO;\n\t_videoCodec->width = kSide;\n\t_videoCodec->height = kSide;\n\t_videoCodec->time_base = AVRational{ 1, 1'000'000 }; // Microseconds.\n\t_videoCodec->framerate = AVRational{ 0, 1 }; // Variable frame rate.\n\t_videoCodec->pix_fmt = AV_PIX_FMT_YUV420P;\n\t_videoCodec->bit_rate = kVideoBitRate;\n\n\tauto error = AvErrorWrap(avcodec_open2(\n\t\t_videoCodec.get(),\n\t\tvideoCodec,\n\t\tnullptr));\n\tif (error) {\n\t\tLogError(\"avcodec_open2\", error, \"libopenh264\");\n\t\treturn false;\n\t}\n\n\terror = AvErrorWrap(avcodec_parameters_from_context(\n\t\t_videoStream->codecpar,\n\t\t_videoCodec.get()));\n\tif (error) {\n\t\tLogError(\"avcodec_parameters_from_context\", error, \"libopenh264\");\n\t\treturn false;\n\t}\n\n\t_videoFrame = MakeFramePointer();\n\tif (!_videoFrame) {\n\t\treturn false;\n\t}\n\n\t_videoFrame->format = _videoCodec->pix_fmt;\n\t_videoFrame->width = _videoCodec->width;\n\t_videoFrame->height = _videoCodec->height;\n\n\terror = AvErrorWrap(av_frame_get_buffer(_videoFrame.get(), 0));\n\tif (error) {\n\t\tLogError(\"av_frame_get_buffer\", error, \"libopenh264\");\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\nbool RoundVideoRecorder::Private::initAudio() {\n\tif (!_format) {\n\t\treturn false;\n\t}\n\n\tconst auto audioCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);\n\tif (!audioCodec) {\n\t\tLogError(\"avcodec_find_encoder\", \"AAC\");\n\t\treturn false;\n\t}\n\n\t_audioStream = avformat_new_stream(_format.get(), audioCodec);\n\tif (!_audioStream) {\n\t\tLogError(\"avformat_new_stream\", \"AAC\");\n\t\treturn false;\n\t}\n\n\t_audioCodec = CodecPointer(avcodec_alloc_context3(audioCodec));\n\tif (!_audioCodec) {\n\t\tLogError(\"avcodec_alloc_context3\", \"AAC\");\n\t\treturn false;\n\t}\n\n\t_audioChannels = 1;\n\t_audioCodec->sample_fmt = AV_SAMPLE_FMT_FLTP;\n\t_audioCodec->bit_rate = kAudioBitRate;\n\t_audioCodec->sample_rate = kAudioFrequency;\n\t_audioCodec->ch_layout = AV_CHANNEL_LAYOUT_MONO;\n\n\tauto error = AvErrorWrap(avcodec_open2(\n\t\t_audioCodec.get(),\n\t\taudioCodec,\n\t\tnullptr));\n\tif (error) {\n\t\tLogError(\"avcodec_open2\", error, \"AAC\");\n\t\treturn false;\n\t}\n\n\terror = AvErrorWrap(avcodec_parameters_from_context(\n\t\t_audioStream->codecpar,\n\t\t_audioCodec.get()));\n\tif (error) {\n\t\tLogError(\"avcodec_parameters_from_context\", error, \"AAC\");\n\t\treturn false;\n\t}\n\n\t_swrContext = MakeSwresamplePointer(\n\t\t&_audioCodec->ch_layout,\n\t\tAV_SAMPLE_FMT_S16,\n\t\t_audioCodec->sample_rate,\n\t\t&_audioCodec->ch_layout,\n\t\t_audioCodec->sample_fmt,\n\t\t_audioCodec->sample_rate,\n\t\t&_swrContext);\n\tif (!_swrContext) {\n\t\treturn false;\n\t}\n\n\t_audioFrame = MakeFramePointer();\n\tif (!_audioFrame) {\n\t\treturn false;\n\t}\n\n\t_audioFrame->nb_samples = _audioCodec->frame_size;\n\t_audioFrame->format = _audioCodec->sample_fmt;\n\t_audioFrame->sample_rate = _audioCodec->sample_rate;\n\tav_channel_layout_copy(&_audioFrame->ch_layout, &_audioCodec->ch_layout);\n\n\terror = AvErrorWrap(av_frame_get_buffer(_audioFrame.get(), 0));\n\tif (error) {\n\t\tLogError(\"av_frame_get_buffer\", error, \"AAC\");\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\nvoid RoundVideoRecorder::Private::finishEncoding() {\n\tif (_format\n\t\t&& writeFrame(nullptr, _videoCodec, _videoStream)\n\t\t&& writeFrame(nullptr, _audioCodec, _audioStream)) {\n\t\tconst auto error = AvErrorWrap(av_write_trailer(_format.get()));\n\t\tif (error) {\n\t\t\tLogError(\"av_write_trailer\", error);\n\t\t\tfail(Error::Encoding);\n\t\t}\n\t}\n\tdeinitEncoding();\n}\n\nauto RoundVideoRecorder::Private::updated() const\n-> rpl::producer<Update, Error> {\n\treturn _updates.events();\n}\n\nRoundVideoResult RoundVideoRecorder::Private::finish() {\n\tif (!_format) {\n\t\treturn {};\n\t}\n\tfinishEncoding();\n\tauto result = appendToPrevious({\n\t\t.content = base::take(_result.content),\n\t\t.duration = base::take(_resultDuration),\n\t\t//.waveform = {},\n\t\t.minithumbs = base::take(_minithumbs),\n\t\t.minithumbsCount = base::take(_minithumbsCount),\n\t\t.minithumbSize = _minithumbSize,\n\t});\n\tif (result.duration < kMinDuration) {\n\t\treturn {};\n\t}\n\treturn result;\n}\n\nRoundVideoResult RoundVideoRecorder::Private::appendToPrevious(\n\t\tRoundVideoResult video) {\n\tif (!_previous.duration) {\n\t\treturn video;\n\t}\n\tconst auto cleanup = gsl::finally([&] {\n\t\t_forConcat1 = {};\n\t\t_forConcat2 = {};\n\t\tdeinitEncoding();\n\t});\n\n\tauto input1 = OpenInputContext(&_previous.content, &_forConcat1);\n\tauto input2 = OpenInputContext(&video.content, &_forConcat2);\n\tif (!input1 || !input2) {\n\t\treturn video;\n\t}\n\n\tauto output = MakeWriteFormatPointer(\n\t\tstatic_cast<void*>(&_result),\n\t\tnullptr,\n\t\t&WriteBytesWrap::Write,\n\t\t&WriteBytesWrap::Seek,\n\t\t\"mp4\"_q);\n\n\tfor (auto i = 0; i != input1->nb_streams; ++i) {\n\t\tAVStream *inStream = input1->streams[i];\n\t\tAVStream *outStream = avformat_new_stream(output.get(), nullptr);\n\t\tif (!outStream) {\n\t\t\tLogError(\"avformat_new_stream\");\n\t\t\tfail(Error::Encoding);\n\t\t\treturn {};\n\t\t}\n\t\tconst auto error = AvErrorWrap(avcodec_parameters_copy(\n\t\t\toutStream->codecpar,\n\t\t\tinStream->codecpar));\n\t\tif (error) {\n\t\t\tLogError(\"avcodec_parameters_copy\", error);\n\t\t\tfail(Error::Encoding);\n\t\t\treturn {};\n\t\t}\n\t\toutStream->time_base = inStream->time_base;\n\t}\n\n\tconst auto offset = _previous.duration;\n\tauto context = CopyContext();\n\tauto error = AvErrorWrap(avformat_write_header(\n\t\toutput.get(),\n\t\tnullptr));\n\tif (error) {\n\t\tLogError(\"avformat_write_header\", error);\n\t\tfail(Error::Encoding);\n\t\treturn {};\n\t} else if (!copyPackets(input1.get(), output.get(), context)\n\t\t|| !copyPackets(input2.get(), output.get(), context, offset)) {\n\t\treturn {};\n\t}\n\terror = AvErrorWrap(av_write_trailer(output.get()));\n\tif (error) {\n\t\tLogError(\"av_write_trailer\", error);\n\t\tfail(Error::Encoding);\n\t\treturn {};\n\t}\n\tvideo.content = base::take(_result.content);\n\tvideo.duration += _previous.duration;\n\treturn video;\n}\n\nFormatPointer RoundVideoRecorder::Private::OpenInputContext(\n\t\tnot_null<const QByteArray*> data,\n\t\tnot_null<ReadBytesWrap*> wrap) {\n\t*wrap = ReadBytesWrap{\n\t\t.size = data->size(),\n\t\t.data = reinterpret_cast<const uchar*>(data->constData()),\n\t};\n\treturn MakeFormatPointer(\n\t\twrap.get(),\n\t\t&ReadBytesWrap::Read,\n\t\tnullptr,\n\t\t&ReadBytesWrap::Seek);\n}\n\nbool RoundVideoRecorder::Private::copyPackets(\n\t\tnot_null<AVFormatContext*> input,\n\t\tnot_null<AVFormatContext*> output,\n\t\tCopyContext &context,\n\t\tcrl::time offset) {\n\tAVPacket packet;\n\tav_init_packet(&packet);\n\n\tauto offsets = std::array<int64, kMaxStreams>{ 0 };\n\twhile (av_read_frame(input, &packet) >= 0) {\n\t\tconst auto index = packet.stream_index;\n\t\tAssert(index >= 0 && index < kMaxStreams);\n\t\tAssert(index < output->nb_streams);\n\n\t\tif (offset) {\n\t\t\tauto &scaled = offsets[index];\n\t\t\tif (!scaled) {\n\t\t\t\tscaled = av_rescale_q(\n\t\t\t\t\toffset,\n\t\t\t\t\tAVRational{ 1, 1000 },\n\t\t\t\t\tinput->streams[index]->time_base);\n\t\t\t}\n\t\t\tif (packet.pts != AV_NOPTS_VALUE) {\n\t\t\t\tpacket.pts += scaled;\n\t\t\t}\n\t\t\tif (packet.dts != AV_NOPTS_VALUE) {\n\t\t\t\tpacket.dts += scaled;\n\t\t\t}\n\t\t}\n\n\t\tif (packet.pts <= context.lastPts[index]) {\n\t\t\tpacket.pts = context.lastPts[index] + 1;\n\t\t}\n\t\tcontext.lastPts[index] = packet.pts;\n\n\t\tif (packet.dts <= context.lastDts[index]) {\n\t\t\tpacket.dts = context.lastDts[index] + 1;\n\t\t}\n\t\tcontext.lastDts[index] = packet.dts;\n\n\t\tconst auto error = AvErrorWrap(av_interleaved_write_frame(\n\t\t\toutput,\n\t\t\t&packet));\n\t\tif (error) {\n\t\t\tLogError(\"av_interleaved_write_frame\", error);\n\t\t\tav_packet_unref(&packet);\n\t\t\treturn false;\n\t\t}\n\t\tav_packet_unref(&packet);\n\t}\n\treturn true;\n}\n\nvoid RoundVideoRecorder::Private::restart(RoundVideoPartial partial) {\n\tif (_format) {\n\t\treturn;\n\t} else if (_maxDuration <= 0) {\n\t\tnotifyFinished();\n\t\treturn;\n\t}\n\t_previous = std::move(partial.video);\n\t_minithumbs = std::move(_previous.minithumbs);\n\t_minithumbsCount = _previous.minithumbsCount;\n\tAssert(_minithumbSize == _previous.minithumbSize);\n\t_maxDuration = kMaxDuration - _previous.duration;\n\t_minithumbNextTimestamp = 0;\n\t_finished = false;\n\tinitEncoding();\n\t_timeoutTimer.callOnce(kInitTimeout);\n}\n\nvoid RoundVideoRecorder::Private::fail(Error error) {\n\tdeinitEncoding();\n\t_updates.fire_error_copy(error);\n}\n\nvoid RoundVideoRecorder::Private::timeout() {\n\tif (!_firstAudioChunkFinished) {\n\t\tfail(Error::AudioTimeout);\n\t} else if (!_firstVideoFrameTime) {\n\t\tfail(Error::VideoTimeout);\n\t}\n}\n\nvoid RoundVideoRecorder::Private::deinitEncoding() {\n\t_swsContext = nullptr;\n\t_videoCodec = nullptr;\n\t_videoStream = nullptr;\n\t_videoFrame = nullptr;\n\t_swrContext = nullptr;\n\t_audioCodec = nullptr;\n\t_audioStream = nullptr;\n\t_audioFrame = nullptr;\n\t_format = nullptr;\n\n\t_videoFirstTimestamp = -1;\n\t_videoPts = 0;\n\t_audioTail = QByteArray();\n\t_audioPts = 0;\n\t_audioChannels = 0;\n\n\t_firstAudioChunkFinished = 0;\n\t_firstVideoFrameTime = 0;\n\n\t_result.offset = 0;\n\n\t_maxLevelSinceLastUpdate = 0;\n\t_lastUpdateDuration = 0;\n}\n\nvoid RoundVideoRecorder::Private::push(\n\t\tint64 mcstimestamp,\n\t\tconst QImage &frame) {\n\tif (!_format || _finished) {\n\t\treturn;\n\t} else if (!_firstAudioChunkFinished) {\n\t\t// Skip frames while we didn't start receiving audio.\n\t\treturn;\n\t} else if (!_firstVideoFrameTime) {\n\t\t_firstVideoFrameTime = crl::now();\n\t}\n\tencodeVideoFrame(mcstimestamp, frame);\n}\n\nvoid RoundVideoRecorder::Private::push(const Media::Capture::Chunk &chunk) {\n\tif (!_format || _finished) {\n\t\treturn;\n\t} else if (!_firstAudioChunkFinished || !_firstVideoFrameTime) {\n\t\t_firstAudioChunkFinished = chunk.finished;\n\t\treturn;\n\t}\n\t// We get a chunk roughly every 50ms and need to encode it interleaved.\n\tencodeAudioFrame(chunk);\n}\n\nvoid RoundVideoRecorder::Private::encodeVideoFrame(\n\t\tint64 mcstimestamp,\n\t\tconst QImage &frame) {\n\tExpects(!_finished);\n\n\tif (_videoFirstTimestamp == -1) {\n\t\t_videoFirstTimestamp = mcstimestamp;\n\t}\n\tconst auto fwidth = frame.width();\n\tconst auto fheight = frame.height();\n\tconst auto fmin = std::min(fwidth, fheight);\n\tconst auto fx = (fwidth > fheight) ? (fwidth - fheight) / 2 : 0;\n\tconst auto fy = (fwidth < fheight) ? (fheight - fwidth) / 2 : 0;\n\tconst auto crop = QRect(fx, fy, fmin, fmin);\n\n\t_swsContext = MakeSwscalePointer(\n\t\tQSize(fmin, fmin),\n\t\tAV_PIX_FMT_BGRA,\n\t\tQSize(kSide, kSide),\n\t\tAV_PIX_FMT_YUV420P,\n\t\t&_swsContext);\n\tif (!_swsContext) {\n\t\tfail(Error::Encoding);\n\t\treturn;\n\t}\n\n\tconst auto cdata = frame.constBits()\n\t\t+ (frame.bytesPerLine() * fy)\n\t\t+ (fx * frame.depth() / 8);\n\n\tconst uint8_t *srcSlice[1] = { cdata };\n\tint srcStride[1] = { int(frame.bytesPerLine()) };\n\n\tsws_scale(\n\t\t_swsContext.get(),\n\t\tsrcSlice,\n\t\tsrcStride,\n\t\t0,\n\t\tfmin,\n\t\t_videoFrame->data,\n\t\t_videoFrame->linesize);\n\n\tmirrorYUV420P(_videoFrame.get());\n\tdrawLogoOnYUV420P(_videoFrame.get());\n\n\t_videoFrame->pts = mcstimestamp - _videoFirstTimestamp;\n\tmaybeSaveMinithumb(_videoFrame.get(), frame, crop);\n\tif (_videoFrame->pts >= _maxDuration * int64(1000)) {\n\t\tnotifyFinished();\n\t\treturn;\n\t} else if (!writeFrame(_videoFrame, _videoCodec, _videoStream)) {\n\t\treturn;\n\t}\n}\n\nvoid RoundVideoRecorder::Private::maybeSaveMinithumb(\n\t\tnot_null<AVFrame*> frame,\n\t\tconst QImage &original,\n\t\tQRect crop) {\n\tif (frame->pts < _minithumbNextTimestamp * 1000) {\n\t\treturn;\n\t}\n\t_minithumbNextTimestamp += crl::time(1000) / kMinithumbsPerSecond;\n\tconst auto perline = original.bytesPerLine();\n\tconst auto perpixel = original.depth() / 8;\n\tconst auto cropped = QImage(\n\t\toriginal.constBits() + (crop.y() * perline) + (crop.x() * perpixel),\n\t\tcrop.width(),\n\t\tcrop.height(),\n\t\tperline,\n\t\toriginal.format()\n\t).scaled(\n\t\t_minithumbSize,\n\t\t_minithumbSize,\n\t\tQt::IgnoreAspectRatio,\n\t\tQt::SmoothTransformation);\n\n\tconst auto row = _minithumbsCount / kMinithumbsInRow;\n\tconst auto column = _minithumbsCount % kMinithumbsInRow;\n\tconst auto fromPerLine = cropped.bytesPerLine();\n\tauto from = cropped.constBits();\n\tconst auto toPerLine = _minithumbs.bytesPerLine();\n\tconst auto toPerPixel = _minithumbs.depth() / 8;\n\tauto to = _minithumbs.bits()\n\t\t+ (row * _minithumbSize * toPerLine)\n\t\t+ (column * _minithumbSize * toPerPixel);\n\n\tAssert(toPerPixel == perpixel);\n\tfor (auto y = 0; y != _minithumbSize; ++y) {\n\t\tAssert(to + toPerLine - _minithumbs.constBits()\n\t\t\t<= _minithumbs.bytesPerLine() * _minithumbs.height());\n\t\tmemcpy(to, from, _minithumbSize * toPerPixel);\n\t\tfrom += fromPerLine;\n\t\tto += toPerLine;\n\t}\n\t++_minithumbsCount;\n}\n\nvoid RoundVideoRecorder::Private::initCircleMask() {\n\tconst auto width = kSide;\n\tconst auto height = kSide;\n\tconst auto centerX = width / 2;\n\tconst auto centerY = height / 2;\n\tconst auto radius = std::min(centerX, centerY) + 3; // Add some padding.\n\tconst auto radiusSquared = radius * radius;\n\n\t_circleMask.resize(width * height);\n\tauto index = 0;\n\tfor (auto y = 0; y != height; ++y) {\n\t\tfor (auto x = 0; x != width; ++x) {\n\t\t\tconst auto dx = x - centerX;\n\t\t\tconst auto dy = y - centerY;\n\t\t\t_circleMask[index++] = (dx * dx + dy * dy > radiusSquared);\n\t\t}\n\t}\n}\n\nvoid RoundVideoRecorder::Private::initCircularTextImage() {\n\tconstexpr auto kCircularTextRadius = kSide / 2 + 17;\n\tconstexpr auto kCircularTextStartAngle = 125;\n\tconstexpr auto kCircularTextEndAngle = 145;\n\t_circularTextImage = CircularTextImage(\n\t\tu\"Telegram\"_q.toUpper(),\n\t\tkSide,\n\t\tkSide,\n\t\tkCircularTextRadius,\n\t\tkCircularTextStartAngle,\n\t\tkCircularTextEndAngle,\n\t\tQt::white,\n\t\tQt::transparent,\n\t\tst::roundVideoFont,\n\t\ttrue);\n}\n\nvoid RoundVideoRecorder::Private::initMinithumbsCanvas() {\n\tconst auto width = kMinithumbsInRow * _minithumbSize;\n\tconst auto seconds = (kMaxDuration + 999) / 1000;\n\tconst auto persecond = kMinithumbsPerSecond;\n\tconst auto frames = (seconds + persecond - 1) * persecond;\n\tconst auto rows = (frames + kMinithumbsInRow - 1) / kMinithumbsInRow;\n\tconst auto height = rows * _minithumbSize;\n\t_minithumbs = QImage(width, height, QImage::Format_ARGB32_Premultiplied);\n}\n\nvoid RoundVideoRecorder::Private::mirrorYUV420P(not_null<AVFrame*> frame) {\n\tfor (auto p = 0; p < 3; ++p) {\n\t\tconst auto size = p ? (kSide / 2) : kSide;\n\t\tconst auto linesize = _videoFrame->linesize[p];\n\t\tauto data = _videoFrame->data[p];\n\t\tfor (auto y = 0; y != size; ++y) {\n\t\t\tauto left = data + y * linesize;\n\t\t\tauto right = left + size - 1;\n\t\t\twhile (left < right) {\n\t\t\t\tstd::swap(*left++, *right--);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid RoundVideoRecorder::Private::drawLogoOnYUV420P(\n\t\tnot_null<AVFrame*> frame) {\n\tconst auto width = frame->width;\n\tconst auto height = frame->height;\n\n\tconst auto logoBottom = height - kLogoSize + kLogoYShift;\n\tconst auto logoStartX = kLogoXShift;\n\tconst auto logoEndX = logoStartX + kLogoSize;\n\tconst auto logoStartY = logoBottom;\n\tconst auto logoEndY = logoBottom + kLogoSize;\n\n\tconst auto &currentLogo = PrecomputedLogos()[_logoFrameCounter];\n\t_logoFrameCounter = (_logoFrameCounter + 1) % kPlainLogoFrames.size();\n\n\tauto yData = frame->data[0];\n\tconst auto ySkip = frame->linesize[0] - width;\n\n\tconst auto uvWidth = width / 2;\n\tauto uData = frame->data[1];\n\tauto vData = frame->data[2];\n\tconst auto uvSkip = frame->linesize[1] - uvWidth;\n\tauto yMaskIndex = 0;\n\n\tfor (auto y = 0; y < height; ++y) {\n\t\tfor (auto x = 0; x < width; ++x) {\n\t\t\tif (_circleMask[yMaskIndex]) {\n\t\t\t\t*yData = static_cast<uint8_t>(*yData * kOverlayOpacity\n\t\t\t\t\t+ 16 * kOverlayOpaque);\n\t\t\t}\n\n\t\t\tif ((x >= logoStartX && x < logoEndX)\n\t\t\t\t&& (y >= logoStartY && y < logoEndY)) {\n\t\t\t\tconst auto logoX = x - kLogoXShift;\n\t\t\t\tconst auto logoY = y - logoBottom;\n\n\t\t\t\tconst auto blendedValue = currentLogo[logoX][logoY];\n\t\t\t\tif (blendedValue > 0) {\n\t\t\t\t\tconst auto logoFactor = blendedValue / 255.0f;\n\t\t\t\t\t*yData = static_cast<uint8_t>(*yData * (1 - logoFactor)\n\t\t\t\t\t\t+ 255 * logoFactor);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst auto textAlpha = qAlpha(_circularTextImage.pixel(x, y))\n\t\t\t\t/ 255.;\n\t\t\t*yData = std::min(\n\t\t\t\t*yData + static_cast<uint8_t>(textAlpha * 100),\n\t\t\t\t255);\n\n\t\t\t++yData;\n\t\t\t++yMaskIndex;\n\t\t}\n\t\tyData += ySkip;\n\n\t\tif (y % 2 == 0) {\n\t\t\tfor (auto x = 0; x < uvWidth; ++x) {\n\t\t\t\tif (_circleMask[(y * width) + (x * 2)]) {\n\t\t\t\t\t*uData = static_cast<uint8_t>(*uData * kOverlayOpacity\n\t\t\t\t\t\t+ kOverlayUVOpaque);\n\t\t\t\t\t*vData = static_cast<uint8_t>(*vData * kOverlayOpacity\n\t\t\t\t\t\t+ kOverlayUVOpaque);\n\t\t\t\t}\n\t\t\t\t++uData;\n\t\t\t\t++vData;\n\t\t\t}\n\t\t\tuData += uvSkip;\n\t\t\tvData += uvSkip;\n\t\t}\n\t}\n}\n\nvoid RoundVideoRecorder::Private::encodeAudioFrame(\n\t\tconst Media::Capture::Chunk &chunk) {\n\tExpects(!_finished);\n\n\tupdateMaxLevel(chunk);\n\n\tif (_audioTail.isEmpty()) {\n\t\t_audioTail = chunk.samples;\n\t} else {\n\t\t_audioTail.append(chunk.samples);\n\t}\n\n\tconst auto inSamples = int(_audioTail.size() / sizeof(int16_t));\n\tconst auto inData = reinterpret_cast<const uint8_t*>(\n\t\t_audioTail.constData());\n\tauto samplesProcessed = 0;\n\n\twhile (samplesProcessed + _audioCodec->frame_size <= inSamples) {\n\t\tconst auto remainingSamples = inSamples - samplesProcessed;\n\t\tauto outSamples = int(av_rescale_rnd(\n\t\t\tswr_get_delay(_swrContext.get(), kAudioFrequency) + remainingSamples,\n\t\t\t_audioCodec->sample_rate,\n\t\t\tkAudioFrequency,\n\t\t\tAV_ROUND_UP));\n\n\t\t// Ensure we don't exceed the frame's capacity\n\t\toutSamples = std::min(outSamples, _audioCodec->frame_size);\n\n\t\tconst auto process = std::min(remainingSamples, outSamples);\n\t\tauto dataptr = inData + samplesProcessed * sizeof(int16_t);\n\t\tauto error = AvErrorWrap(swr_convert(\n\t\t\t_swrContext.get(),\n\t\t\t_audioFrame->data,\n\t\t\toutSamples,\n\t\t\t&dataptr,\n\t\t\tprocess));\n\n\t\tif (error) {\n\t\t\tLogError(\"swr_convert\", error);\n\t\t\tfail(Error::Encoding);\n\t\t\treturn;\n\t\t}\n\n\t\t// Update the actual number of samples in the frame\n\t\t_audioFrame->nb_samples = error.code();\n\n\t\t_audioFrame->pts = _audioPts;\n\t\t_audioPts += _audioFrame->nb_samples;\n\t\tif (_audioPts >= _maxDuration * int64(kAudioFrequency) / 1000) {\n\t\t\tnotifyFinished();\n\t\t\treturn;\n\t\t} else if (!writeFrame(_audioFrame, _audioCodec, _audioStream)) {\n\t\t\treturn;\n\t\t}\n\n\t\tsamplesProcessed += process;\n\t}\n\tconst auto left = inSamples - samplesProcessed;\n\tif (left > 0) {\n\t\tmemmove(_audioTail.data(), _audioTail.data() + samplesProcessed * sizeof(int16_t), left * sizeof(int16_t));\n\t\t_audioTail.resize(left * sizeof(int16_t));\n\t} else {\n\t\t_audioTail.clear();\n\t}\n}\n\nvoid RoundVideoRecorder::Private::notifyFinished() {\n\t_finished = true;\n\t_updates.fire({\n\t\t.samples = int((_previous.duration + _resultDuration) * 48),\n\t\t.level = base::take(_maxLevelSinceLastUpdate),\n\t\t.finished = true,\n\t});\n}\n\nbool RoundVideoRecorder::Private::writeFrame(\n\t\tconst FramePointer &frame,\n\t\tconst CodecPointer &codec,\n\t\tAVStream *stream) {\n\t_timeoutTimer.cancel();\n\n\tif (frame) {\n\t\tupdateResultDuration(frame->pts, codec->time_base);\n\t}\n\n\tauto error = AvErrorWrap(avcodec_send_frame(codec.get(), frame.get()));\n\tif (error) {\n\t\tLogError(\"avcodec_send_frame\", error);\n\t\tfail(Error::Encoding);\n\t\treturn false;\n\t}\n\n\tauto pkt = av_packet_alloc();\n\tconst auto guard = gsl::finally([&] {\n\t\tav_packet_free(&pkt);\n\t});\n\twhile (true) {\n\t\terror = AvErrorWrap(avcodec_receive_packet(codec.get(), pkt));\n\t\tif (error.code() == AVERROR(EAGAIN)) {\n\t\t\treturn true; // Need more input\n\t\t} else if (error.code() == AVERROR_EOF) {\n\t\t\treturn true; // Encoding finished\n\t\t} else if (error) {\n\t\t\tLogError(\"avcodec_receive_packet\", error);\n\t\t\tfail(Error::Encoding);\n\t\t\treturn false;\n\t\t}\n\n\t\tpkt->stream_index = stream->index;\n\t\tav_packet_rescale_ts(pkt, codec->time_base, stream->time_base);\n\n\t\tupdateResultDuration(pkt->pts, stream->time_base);\n\n\t\terror = AvErrorWrap(av_interleaved_write_frame(_format.get(), pkt));\n\t\tif (error) {\n\t\t\tLogError(\"av_interleaved_write_frame\", error);\n\t\t\tfail(Error::Encoding);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n\nvoid RoundVideoRecorder::Private::updateMaxLevel(\n\t\tconst Media::Capture::Chunk &chunk) {\n\tconst auto &list = chunk.samples;\n\tconst auto samples = int(list.size() / sizeof(ushort));\n\tconst auto data = reinterpret_cast<const ushort*>(list.constData());\n\tfor (const auto value : gsl::make_span(data, samples)) {\n\t\taccumulate_max(_maxLevelSinceLastUpdate, value);\n\t}\n}\n\nvoid RoundVideoRecorder::Private::updateResultDuration(\n\t\tint64 pts,\n\t\tAVRational timeBase) {\n\taccumulate_max(_resultDuration, PtsToTimeCeil(pts, timeBase));\n\n\tconst auto initial = !_lastUpdateDuration;\n\tif (initial) {\n\t\taccumulate_max(_resultDuration, crl::time(1));\n\t}\n\tif (initial || (_lastUpdateDuration + kUpdateEach < _resultDuration)) {\n\t\t_lastUpdateDuration = _resultDuration;\n\t\t_updates.fire({\n\t\t\t.samples = int((_previous.duration + _resultDuration) * 48),\n\t\t\t.level = base::take(_maxLevelSinceLastUpdate),\n\t\t});\n\t}\n}\n\nRoundVideoRecorder::RoundVideoRecorder(\n\tRoundVideoRecorderDescriptor &&descriptor)\n: _descriptor(std::move(descriptor))\n, _gradientBg(QColor(255, 255, 255, 0))\n, _gradientFg(QColor(255, 255, 255, 48))\n, _gradient(\n\t_gradientBg.color(),\n\t_gradientFg.color(),\n\t[=] { _preview->update(); })\n, _preview(std::make_unique<RpWidget>(_descriptor.container))\n, _private(MinithumbSize()) {\n\tsetup();\n}\n\nRoundVideoRecorder::~RoundVideoRecorder() = default;\n\nFn<void(Media::Capture::Chunk)> RoundVideoRecorder::audioChunkProcessor() {\n\treturn [weak = _private.weak()](Media::Capture::Chunk chunk) {\n\t\tweak.with([copy = std::move(chunk)](Private &that) {\n\t\t\tthat.push(copy);\n\t\t});\n\t};\n}\n\nrpl::producer<QImage> RoundVideoRecorder::placeholderUpdates() const {\n\treturn _placeholderUpdates.events();\n}\n\nint RoundVideoRecorder::previewSize() const {\n\treturn _side;\n}\n\nauto RoundVideoRecorder::updated() -> rpl::producer<Update, Error> {\n\treturn _private.producer_on_main([](const Private &that) {\n\t\treturn that.updated();\n\t}) | rpl::before_next(crl::guard(this, [=](const Update &update) {\n\t\tconst auto progress = (update.samples * crl::time(1000))\n\t\t\t/ float64(kAudioFrequency * kMaxDuration);\n\t\tprogressTo(progress);\n\t}));\n}\n\nvoid RoundVideoRecorder::hide(Fn<void(RoundVideoResult)> done) {\n\tif (const auto onstack = _descriptor.hiding) {\n\t\tonstack(this);\n\t}\n\tpause(std::move(done));\n\tfade(false);\n}\n\nvoid RoundVideoRecorder::progressTo(float64 progress) {\n\tif (_progress == progress || _paused) {\n\t\treturn;\n\t} else if (_progressReceived) {\n\t\t_progressAnimation.start(\n\t\t\t[=] { _preview->update(); },\n\t\t\t_progress,\n\t\t\tprogress,\n\t\t\tkUpdateEach * 1.1);\n\t} else {\n\t\t_progressReceived = true;\n\t\t_fadeContentAnimation.start(\n\t\t\t[=] { _preview->update(); },\n\t\t\t0.,\n\t\t\t1.,\n\t\t\tkFadeDuration);\n\t}\n\t_progress = progress;\n\t_preview->update();\n}\n\nvoid RoundVideoRecorder::preparePlaceholder(const QImage &placeholder) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto full = QSize(_side, _side) * ratio;\n\t_framePlaceholder = Images::Circle(\n\t\t(placeholder.isNull()\n\t\t\t? QImage(u\":/gui/art/round_placeholder.jpg\"_q)\n\t\t\t: placeholder).scaled(\n\t\t\t\tfull,\n\t\t\t\tQt::KeepAspectRatio,\n\t\t\t\tQt::SmoothTransformation));\n\t_framePlaceholder.setDevicePixelRatio(ratio);\n}\n\nvoid RoundVideoRecorder::prepareFrame(bool blurred) {\n\tif (_frameOriginal.isNull()) {\n\t\treturn;\n\t} else if (!blurred) {\n\t\tif (_preparedIndex == _lastAddedIndex) {\n\t\t\treturn;\n\t\t}\n\t\t_preparedIndex = _lastAddedIndex;\n\t}\n\n\tconst auto owidth = _frameOriginal.width();\n\tconst auto oheight = _frameOriginal.height();\n\tconst auto omin = std::min(owidth, oheight);\n\tconst auto ox = (owidth > oheight) ? (owidth - oheight) / 2 : 0;\n\tconst auto oy = (owidth < oheight) ? (oheight - owidth) / 2 : 0;\n\tconst auto from = QRect(ox, oy, omin, omin);\n\tconst auto bytesPerLine = _frameOriginal.bytesPerLine();\n\tconst auto depth = _frameOriginal.depth() / 8;\n\tconst auto shift = (bytesPerLine * from.y()) + (from.x() * depth);\n\tauto copy = QImage(\n\t\t_frameOriginal.constBits() + shift,\n\t\tomin, omin,\n\t\tbytesPerLine,\n\t\t_frameOriginal.format());\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (blurred) {\n\t\tstatic constexpr auto kRadius = 16;\n\t\tauto image = Images::BlurLargeImage(\n\t\t\tcopy.scaled(\n\t\t\t\tQSize(kBlurredSize, kBlurredSize),\n\t\t\t\tQt::KeepAspectRatio,\n\t\t\t\tQt::FastTransformation),\n\t\t\tkRadius).mirrored(true, false);\n\t\tpreparePlaceholder(image);\n\t\t_placeholderUpdates.fire(std::move(image));\n\t} else {\n\t\tauto scaled = copy.scaled(\n\t\t\tQSize(_side, _side) * ratio,\n\t\t\tQt::KeepAspectRatio,\n\t\t\tQt::SmoothTransformation).mirrored(true, false);\n\t\t_framePrepared = Images::Circle(std::move(scaled));\n\t\t_framePrepared.setDevicePixelRatio(ratio);\n\t}\n}\n\nvoid RoundVideoRecorder::createImages() {\n\tconst auto ratio = style::DevicePixelRatio();\n\tpreparePlaceholder(_descriptor.placeholder);\n\n\tconst auto side = _side + 2 * _extent;\n\t_shadow = QImage(\n\t\tQSize(side, side) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_shadow.fill(Qt::transparent);\n\t_shadow.setDevicePixelRatio(ratio);\n\n\tauto sp = QPainter(&_shadow);\n\tauto shq = PainterHighQualityEnabler(sp);\n\n\tQRadialGradient gradient(\n\t\tQPointF(_extent + _side / 2, _extent + _side / 2),\n\t\t_side / 2 + _extent);\n\tgradient.setColorAt(0, QColor(0, 0, 0, 128));\n\tgradient.setColorAt(0.8, QColor(0, 0, 0, 64));\n\tgradient.setColorAt(1, QColor(0, 0, 0, 0));\n\n\tsp.setPen(Qt::NoPen);\n\tsp.fillRect(0, 0, side, side, gradient);\n\tsp.end();\n}\n\nvoid RoundVideoRecorder::setup() {\n\tconst auto raw = _preview.get();\n\n\t_side = style::ConvertScale(kSide * 3 / 4);\n\t_progressStroke = st::radialLine;\n\t_extent = _progressStroke * 8;\n\tcreateImages();\n\n\t_descriptor.container->sizeValue(\n\t) | rpl::on_next([=](QSize outer) {\n\t\tconst auto side = _side + 2 * _extent;\n\t\traw->setGeometry(\n\t\t\tstyle::centerrect(\n\t\t\t\tQRect(QPoint(), outer),\n\t\t\t\tQRect(0, 0, side, side)));\n\t}, raw->lifetime());\n\n\tconst auto paintPlaceholder = [=](QPainter &p, QRect inner) {\n\t\tp.drawImage(inner, _framePlaceholder);\n\t\tif (_paused) {\n\t\t\treturn;\n\t\t}\n\n\t\t_gradient.startFrame(\n\t\t\t0,\n\t\t\traw->width(),\n\t\t\traw->width() * 2 / 3);\n\t\t_gradient.paint([&](const Ui::PathShiftGradient::Background &b) {\n\t\t\tif (!v::is<QLinearGradient*>(b)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tconst auto gradient = v::get<QLinearGradient*>(b);\n\n\t\t\tauto copy = *gradient;\n\t\t\tauto stops = copy.stops();\n\t\t\tfor (auto &pair : stops) {\n\t\t\t\tif (pair.second.alpha() > 0) {\n\t\t\t\t\tpair.second.setAlpha(255);\n\t\t\t\t}\n\t\t\t}\n\t\t\tcopy.setStops(stops);\n\n\t\t\tconst auto stroke = style::ConvertScaleExact(1.);\n\t\t\tconst auto sub = stroke / 2.;\n\t\t\tp.setPen(QPen(QBrush(copy), stroke));\n\n\t\t\tp.setBrush(*gradient);\n\t\t\tconst auto innerf = QRectF(inner);\n\t\t\tp.drawEllipse(innerf.marginsRemoved({ sub, sub, sub, sub }));\n\t\t\treturn true;\n\t\t});\n\t};\n\n\traw->paintRequest() | rpl::on_next([=] {\n\t\tprepareFrame();\n\n\t\tauto p = QPainter(raw);\n\t\tconst auto faded = _fadeAnimation.value(_visible ? 1. : 0.);\n\t\tif (_fadeAnimation.animating()) {\n\t\t\tp.setOpacity(faded * faded);\n\n\t\t\tconst auto center = raw->rect().center();\n\t\t\tp.translate(center);\n\t\t\tconst auto scale = kMinScale + (1. - kMinScale) * faded;\n\t\t\tp.scale(scale, scale);\n\t\t\tp.translate(-center);\n\t\t} else if (!_visible) {\n\t\t\treturn;\n\t\t}\n\n\t\tp.drawImage(raw->rect(), _shadow);\n\t\tconst auto inner = QRect(_extent, _extent, _side, _side);\n\t\tconst auto fading = _fadeContentAnimation.animating();\n\t\tif (!_progressReceived && !fading) {\n\t\t\tpaintPlaceholder(p, inner);\n\t\t} else {\n\t\t\tif (fading) {\n\t\t\t\tpaintPlaceholder(p, inner);\n\n\t\t\t\tconst auto to = _progressReceived ? 1. : 0.;\n\t\t\t\tp.setOpacity(faded * _fadeContentAnimation.value(to));\n\t\t\t}\n\t\t\tp.drawImage(inner, _framePrepared);\n\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(QPen(\n\t\t\t\tQt::white,\n\t\t\t\t_progressStroke,\n\t\t\t\tQt::SolidLine,\n\t\t\t\tQt::RoundCap));\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\tconst auto add = _progressStroke * 3 / 2.;\n\t\t\tconst auto full = arc::kFullLength;\n\t\t\tconst auto length = int(base::SafeRound(\n\t\t\t\t_progressAnimation.value(_progress) * full));\n\t\t\tp.drawArc(\n\t\t\t\tQRectF(inner).marginsAdded({ add, add, add, add }),\n\t\t\t\t(full / 4) - length,\n\t\t\t\tlength);\n\t\t}\n\n\t\tconst auto preview = _fadePreviewAnimation.value(\n\t\t\t_silentPreview ? 1. : 0.);\n\t\tconst auto frame = _silentPreview\n\t\t\t? lookupPreviewFrame()\n\t\t\t: _cachedPreviewFrame;\n\t\tif (preview > 0. && !frame.image.isNull()) {\n\t\t\tp.setOpacity(preview);\n\t\t\tp.drawImage(inner, frame.image);\n\t\t\tif (frame.silent) {\n\t\t\t\tconst auto iconSize = st::historyVideoMessageMuteSize;\n\t\t\t\tconst auto iconRect = style::rtlrect(\n\t\t\t\t\tinner.x() + (inner.width() - iconSize) / 2,\n\t\t\t\t\tinner.y() + st::msgDateImgDelta,\n\t\t\t\t\ticonSize,\n\t\t\t\t\ticonSize,\n\t\t\t\t\traw->width());\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(st::msgDateImgBg);\n\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\tp.drawEllipse(iconRect);\n\t\t\t\tst::historyVideoMessageMute.paintInCenter(p, iconRect);\n\t\t\t}\n\t\t}\n\t}, raw->lifetime());\n\n\t// Skip some frames, they are sometimes black :(\n\t_skipFrames = kSkipFrames;\n\t_descriptor.track->setState(Webrtc::VideoState::Active);\n\n\t_descriptor.track->renderNextFrame() | rpl::on_next([=] {\n\t\tconst auto info = _descriptor.track->frameWithInfo(true);\n\t\tif (!info.original.isNull() && _lastAddedIndex != info.index) {\n\t\t\t_lastAddedIndex = info.index;\n\t\t\tif (_skipFrames > 0) {\n\t\t\t\t--_skipFrames;\n\t\t\t} else {\n\t\t\t\t_frameOriginal = info.original;\n\t\t\t\tconst auto ts = info.mcstimestamp;\n\t\t\t\t_private.with([copy = info.original, ts](Private &that) {\n\t\t\t\t\tthat.push(ts, copy);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\t_descriptor.track->markFrameShown();\n\t\traw->update();\n\t}, raw->lifetime());\n\t_descriptor.track->markFrameShown();\n\n\tfade(true);\n\n\traw->show();\n\traw->raise();\n}\n\nvoid RoundVideoRecorder::fade(bool visible) {\n\tif (_visible == visible) {\n\t\treturn;\n\t}\n\t_visible = visible;\n\tconst auto from = visible ? 0. : 1.;\n\tconst auto to = visible ? 1. : 0.;\n\t_fadeAnimation.start([=] {\n\t\tif (_fadeAnimation.animating() || _visible) {\n\t\t\t_preview->update();\n\t\t} else {\n\t\t\t_preview->hide();\n\t\t\tif (const auto onstack = _descriptor.hidden) {\n\t\t\t\tonstack(this);\n\t\t\t}\n\t\t}\n\t}, from, to, kFadeDuration);\n}\n\nauto RoundVideoRecorder::lookupPreviewFrame() const -> PreviewFrame {\n\tauto sounded = _soundedPreview\n\t\t? _soundedPreview->image(_side)\n\t\t: QImage();\n\tconst auto silent = (_silentPreview && sounded.isNull());\n\treturn {\n\t\t.image = silent ? _silentPreview->image(_side) : std::move(sounded),\n\t\t.silent = silent,\n\t};\n}\n\nFn<void()> RoundVideoRecorder::updater() const {\n\treturn [=] {\n\t\t_preview->update();\n\t};\n}\n\nvoid RoundVideoRecorder::pause(Fn<void(RoundVideoResult)> done) {\n\tif (_paused) {\n\t\treturn;\n\t} else if (done) {\n\t\t_private.with([done = std::move(done)](Private &that) {\n\t\t\tdone(that.finish());\n\t\t});\n\t}\n\t_paused = true;\n\tprepareFrame(true);\n\t_progressReceived = false;\n\t_fadeContentAnimation.start(updater(), 1., 0., kFadeDuration);\n\t_descriptor.track->setState(Webrtc::VideoState::Inactive);\n\t_preview->update();\n}\n\nvoid RoundVideoRecorder::showPreview(\n\t\tstd::shared_ptr<Ui::DynamicImage> silent,\n\t\tstd::shared_ptr<Ui::DynamicImage> sounded) {\n\t_silentPreview = std::move(silent);\n\t_soundedPreview = std::move(sounded);\n\t_silentPreview->subscribeToUpdates(updater());\n\t_soundedPreview->subscribeToUpdates(updater());\n\t_fadePreviewAnimation.start(updater(), 0., 1., kFadeDuration);\n\t_preview->update();\n}\n\nvoid RoundVideoRecorder::resume(RoundVideoPartial partial) {\n\tif (!_paused) {\n\t\treturn;\n\t}\n\t_private.with([partial = std::move(partial)](Private &that) mutable {\n\t\tthat.restart(std::move(partial));\n\t});\n\t_paused = false;\n\t_cachedPreviewFrame = lookupPreviewFrame();\n\tif (const auto preview = base::take(_silentPreview)) {\n\t\tpreview->subscribeToUpdates(nullptr);\n\t}\n\tif (const auto preview = base::take(_soundedPreview)) {\n\t\tpreview->subscribeToUpdates(nullptr);\n\t}\n\tif (!_cachedPreviewFrame.image.isNull()) {\n\t\t_fadePreviewAnimation.start(updater(), 1., 0., kFadeDuration);\n\t}\n\t// Skip some frames, they are sometimes black :(\n\t_skipFrames = kSkipFrames;\n\t_descriptor.track->setState(Webrtc::VideoState::Active);\n\t_preview->update();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/round_video_recorder.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/path_shift_gradient.h\"\n\n#include <crl/crl_object_on_queue.h>\n\nnamespace Media::Capture {\nstruct Chunk;\nstruct Update;\nenum class Error : uchar;\n} // namespace Media::Capture\n\nnamespace tgcalls {\nclass VideoCaptureInterface;\n} // namespace tgcalls\n\nnamespace Webrtc {\nclass VideoTrack;\n} // namespace Webrtc\n\nnamespace Ui {\n\nclass RpWidget;\nclass DynamicImage;\nclass RoundVideoRecorder;\n\nstruct RoundVideoRecorderDescriptor {\n\tnot_null<RpWidget*> container;\n\tFn<void(not_null<RoundVideoRecorder*>)> hiding;\n\tFn<void(not_null<RoundVideoRecorder*>)> hidden;\n\tstd::shared_ptr<tgcalls::VideoCaptureInterface> capturer;\n\tstd::shared_ptr<Webrtc::VideoTrack> track;\n\tQImage placeholder;\n};\n\nstruct RoundVideoResult {\n\tQByteArray content;\n\tQVector<signed char> waveform;\n\tcrl::time duration = 0;\n\tQImage minithumbs;\n\tint minithumbsCount = 0;\n\tint minithumbSize = 0;\n};\n\nstruct RoundVideoPartial {\n\tRoundVideoResult video;\n\tcrl::time from = 0;\n\tcrl::time till = 0;\n};\n\nclass RoundVideoRecorder final : public base::has_weak_ptr {\npublic:\n\texplicit RoundVideoRecorder(RoundVideoRecorderDescriptor &&descriptor);\n\t~RoundVideoRecorder();\n\n\t[[nodiscard]] int previewSize() const;\n\t[[nodiscard]] Fn<void(Media::Capture::Chunk)> audioChunkProcessor();\n\t[[nodiscard]] rpl::producer<QImage> placeholderUpdates() const;\n\n\tvoid pause(Fn<void(RoundVideoResult)> done = nullptr);\n\tvoid resume(RoundVideoPartial partial);\n\tvoid hide(Fn<void(RoundVideoResult)> done = nullptr);\n\n\tvoid showPreview(\n\t\tstd::shared_ptr<Ui::DynamicImage> silent,\n\t\tstd::shared_ptr<Ui::DynamicImage> sounded);\n\n\tusing Update = Media::Capture::Update;\n\tusing Error = Media::Capture::Error;\n\t[[nodiscard]] rpl::producer<Update, Error> updated();\n\nprivate:\n\tclass Private;\n\tstruct PreviewFrame {\n\t\tQImage image;\n\t\tbool silent = false;\n\t};\n\n\tvoid setup();\n\tvoid prepareFrame(bool blurred = false);\n\tvoid preparePlaceholder(const QImage &placeholder);\n\tvoid createImages();\n\tvoid progressTo(float64 progress);\n\tvoid fade(bool visible);\n\n\t[[nodiscard]] Fn<void()> updater() const;\n\t[[nodiscard]] PreviewFrame lookupPreviewFrame() const;\n\n\tconst RoundVideoRecorderDescriptor _descriptor;\n\tstyle::owned_color _gradientBg;\n\tstyle::owned_color _gradientFg;\n\tPathShiftGradient _gradient;\n\tstd::unique_ptr<RpWidget> _preview;\n\tcrl::object_on_queue<Private> _private;\n\tUi::Animations::Simple _progressAnimation;\n\tUi::Animations::Simple _fadeAnimation;\n\tUi::Animations::Simple _fadeContentAnimation;\n\trpl::event_stream<QImage> _placeholderUpdates;\n\n\tstd::shared_ptr<Ui::DynamicImage> _silentPreview;\n\tstd::shared_ptr<Ui::DynamicImage> _soundedPreview;\n\tUi::Animations::Simple _fadePreviewAnimation;\n\tPreviewFrame _cachedPreviewFrame;\n\n\tfloat64 _progress = 0.;\n\tQImage _frameOriginal;\n\tQImage _framePlaceholder;\n\tQImage _framePrepared;\n\tQImage _shadow;\n\tint _lastAddedIndex = 0;\n\tint _preparedIndex = 0;\n\tint _side = 0;\n\tint _progressStroke = 0;\n\tint _extent = 0;\n\tint _skipFrames = 0;\n\tbool _progressReceived = false;\n\tbool _visible = false;\n\tbool _paused = false;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/round_video_recorder_data.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace RoundVideoData {\n\nconstexpr auto kLogoSize = 80;\n\nstruct LogoRLENode final {\n\tuint16_t count;\n\tuint8_t value;\n};\nusing LogoRLEFrame = std::vector<LogoRLENode>;\n\nvoid DecompressLogoRLEFrame(\n\t\tconst std::vector<LogoRLENode> &rleFrame,\n\t\tuint8_t outFrame[kLogoSize][kLogoSize]) {\n\tauto pos = size_t(0);\n\tfor (const auto &node : rleFrame) {\n\t\tfor (auto i = uint16_t(0); i < node.count; ++i) {\n\t\t\tif (pos >= kLogoSize * kLogoSize) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst auto y = int(pos / kLogoSize);\n\t\t\tconst auto x = int(pos % kLogoSize);\n\t\t\toutFrame[y][x] = node.value;\n\t\t\tpos++;\n\t\t}\n\t}\n}\n\nconst auto kLogoFrames = std::array<LogoRLEFrame, 27>{ {\n\n\tLogoRLEFrame{ { 997, 0 }, { 1, 9 }, { 1, 4 }, { 77, 0 }, { 1, 1 }, { 1, 27 }, { 78, 0 }, { 1, 32 }, { 1, 11 }, { 77, 0 }, { 1, 14 }, { 1, 44 }, { 77, 0 }, { 1, 2 }, { 1, 54 }, { 1, 17 }, { 77, 0 }, { 1, 33 }, { 1, 47 }, { 77, 0 }, { 1, 9 }, { 1, 61 }, { 1, 13 }, { 77, 0 }, { 1, 45 }, { 1, 40 }, { 77, 0 }, { 1, 19 }, { 1, 60 }, { 1, 6 }, { 76, 0 }, { 1, 2 }, { 1, 54 }, { 1, 31 }, { 77, 0 }, { 1, 27 }, { 1, 54 }, { 1, 2 }, { 76, 0 }, { 1, 1 }, { 1, 55 }, { 1, 15 }, { 9, 0 }, { 1, 53 }, { 1, 196 }, { 1, 65 }, { 65, 0 }, { 1, 24 }, { 1, 35 }, { 9, 0 }, { 1, 32 }, { 1, 238 }, { 1, 255 }, { 1, 115 }, { 65, 0 }, { 1, 41 }, { 1, 2 }, { 8, 0 }, { 1, 4 }, { 1, 199 }, { 2, 255 }, { 1, 121 }, { 20, 0 }, { 1, 2 }, { 1, 4 }, { 42, 0 }, { 1, 16 }, { 1, 9 }, { 9, 0 }, { 1, 135 }, { 3, 255 }, { 1, 98 }, { 19, 0 }, { 2, 19 }, { 43, 0 }, { 1, 7 }, { 9, 0 }, { 1, 68 }, { 1, 253 }, { 3, 255 }, { 1, 65 }, { 17, 0 }, { 1, 8 }, { 1, 43 }, { 1, 20 }, { 53, 0 }, { 1, 22 }, { 1, 231 }, { 3, 255 }, { 1, 217 }, { 1, 3 }, { 16, 0 }, { 1, 30 }, { 1, 56 }, { 1, 15 }, { 53, 0 }, { 1, 1 }, { 1, 183 }, { 3, 255 }, { 1, 250 }, { 1, 67 }, { 15, 0 }, { 1, 8 }, { 1, 48 }, { 1, 52 }, { 1, 10 }, { 54, 0 }, { 1, 115 }, { 4, 255 }, { 1, 91 }, { 15, 0 }, { 1, 23 }, { 1, 59 }, { 1, 39 }, { 1, 2 }, { 54, 0 }, { 1, 52 }, { 1, 250 }, { 3, 255 }, { 1, 114 }, { 14, 0 }, { 1, 4 }, { 1, 41 }, { 1, 60 }, { 1, 22 }, { 55, 0 }, { 1, 13 }, { 1, 219 }, { 3, 255 }, { 1, 139 }, { 14, 0 }, { 1, 10 }, { 1, 54 }, { 1, 51 }, { 1, 9 }, { 56, 0 }, { 1, 164 }, { 3, 255 }, { 1, 162 }, { 1, 1 }, { 1, 0 }, { 1, 102 }, { 1, 222 }, { 1, 236 }, { 1, 85 }, { 8, 0 }, { 1, 15 }, { 1, 54 }, { 1, 27 }, { 57, 0 }, { 1, 95 }, { 3, 255 }, { 1, 183 }, { 1, 5 }, { 1, 2 }, { 1, 152 }, { 3, 255 }, { 1, 164 }, { 7, 0 }, { 1, 22 }, { 1, 36 }, { 1, 4 }, { 57, 0 }, { 1, 38 }, { 1, 243 }, { 2, 255 }, { 1, 201 }, { 1, 11 }, { 1, 7 }, { 1, 172 }, { 4, 255 }, { 1, 188 }, { 6, 0 }, { 1, 16 }, { 1, 12 }, { 58, 0 }, { 1, 6 }, { 1, 206 }, { 2, 255 }, { 1, 217 }, { 1, 20 }, { 1, 14 }, { 1, 190 }, { 5, 255 }, { 1, 213 }, { 5, 0 }, { 1, 1 }, { 60, 0 }, { 1, 144 }, { 2, 255 }, { 1, 230 }, { 1, 32 }, { 1, 23 }, { 1, 207 }, { 6, 255 }, { 1, 238 }, { 65, 0 }, { 1, 76 }, { 2, 255 }, { 1, 240 }, { 1, 46 }, { 1, 35 }, { 1, 220 }, { 8, 255 }, { 1, 8 }, { 63, 0 }, { 1, 26 }, { 1, 235 }, { 1, 255 }, { 1, 248 }, { 1, 64 }, { 1, 48 }, { 1, 232 }, { 9, 255 }, { 1, 31 }, { 62, 0 }, { 1, 2 }, { 1, 191 }, { 1, 255 }, { 1, 253 }, { 1, 83 }, { 1, 64 }, { 1, 241 }, { 10, 255 }, { 1, 56 }, { 62, 0 }, { 1, 124 }, { 2, 255 }, { 1, 106 }, { 1, 83 }, { 1, 248 }, { 11, 255 }, { 1, 81 }, { 61, 0 }, { 1, 59 }, { 1, 252 }, { 1, 255 }, { 1, 130 }, { 1, 103 }, { 1, 253 }, { 12, 255 }, { 1, 105 }, { 60, 0 }, { 1, 17 }, { 1, 225 }, { 1, 255 }, { 1, 226 }, { 1, 143 }, { 14, 255 }, { 1, 103 }, { 60, 0 }, { 1, 173 }, { 17, 255 }, { 1, 223 }, { 1, 12 }, { 59, 0 }, { 1, 105 }, { 17, 255 }, { 1, 206 }, { 1, 30 }, { 59, 0 }, { 1, 44 }, { 1, 247 }, { 15, 255 }, { 1, 204 }, { 1, 95 }, { 1, 4 }, { 59, 0 }, { 1, 9 }, { 1, 213 }, { 12, 255 }, { 1, 252 }, { 1, 191 }, { 1, 110 }, { 1, 30 }, { 62, 0 }, { 1, 154 }, { 10, 255 }, { 1, 247 }, { 1, 178 }, { 1, 97 }, { 1, 20 }, { 64, 0 }, { 1, 85 }, { 8, 255 }, { 1, 240 }, { 1, 165 }, { 1, 84 }, { 1, 12 }, { 66, 0 }, { 1, 32 }, { 1, 240 }, { 5, 255 }, { 1, 232 }, { 1, 153 }, { 1, 71 }, { 1, 6 }, { 68, 0 }, { 1, 1 }, { 1, 196 }, { 3, 255 }, { 1, 221 }, { 1, 140 }, { 1, 59 }, { 1, 2 }, { 71, 0 }, { 1, 49 }, { 1, 253 }, { 1, 207 }, { 1, 127 }, { 1, 46 }, { 76, 0 }, { 1, 6 }, { 508, 0 }, { 1, 6 }, { 1, 1 }, { 76, 0 }, { 1, 7 }, { 1, 29 }, { 1, 9 }, { 75, 0 }, { 1, 6 }, { 1, 36 }, { 1, 41 }, { 1, 3 }, { 74, 0 }, { 1, 1 }, { 1, 31 }, { 1, 59 }, { 1, 29 }, { 75, 0 }, { 1, 17 }, { 1, 53 }, { 1, 51 }, { 1, 13 }, { 74, 0 }, { 1, 6 }, { 1, 42 }, { 1, 60 }, { 1, 29 }, { 75, 0 }, { 1, 25 }, { 1, 60 }, { 1, 47 }, { 1, 9 }, { 74, 0 }, { 1, 3 }, { 1, 41 }, { 1, 52 }, { 1, 21 }, { 75, 0 }, { 1, 12 }, { 1, 43 }, { 1, 21 }, { 76, 0 }, { 1, 15 }, { 1, 18 }, { 77, 0 }, { 1, 1 }, { 843, 0 } }, \n\tLogoRLEFrame{ { 600, 0 }, { 1, 1 }, { 79, 0 }, { 1, 7 }, { 78, 0 }, { 1, 14 }, { 1, 1 }, { 77, 0 }, { 1, 7 }, { 1, 16 }, { 77, 0 }, { 1, 1 }, { 1, 26 }, { 1, 3 }, { 77, 0 }, { 1, 18 }, { 1, 19 }, { 77, 0 }, { 1, 6 }, { 1, 29 }, { 1, 3 }, { 77, 0 }, { 1, 24 }, { 1, 16 }, { 77, 0 }, { 1, 12 }, { 1, 28 }, { 1, 1 }, { 76, 0 }, { 1, 2 }, { 1, 28 }, { 1, 12 }, { 77, 0 }, { 1, 17 }, { 1, 25 }, { 77, 0 }, { 1, 3 }, { 1, 29 }, { 1, 7 }, { 77, 0 }, { 1, 16 }, { 1, 17 }, { 77, 0 }, { 1, 1 }, { 1, 24 }, { 1, 1 }, { 77, 0 }, { 1, 13 }, { 1, 5 }, { 77, 0 }, { 1, 1 }, { 1, 8 }, { 78, 0 }, { 1, 1 }, { 90, 0 }, { 1, 14 }, { 1, 167 }, { 1, 127 }, { 76, 0 }, { 1, 2 }, { 1, 189 }, { 1, 255 }, { 1, 196 }, { 76, 0 }, { 1, 124 }, { 2, 255 }, { 1, 204 }, { 75, 0 }, { 1, 58 }, { 1, 252 }, { 2, 255 }, { 1, 185 }, { 74, 0 }, { 1, 16 }, { 1, 224 }, { 3, 255 }, { 1, 155 }, { 74, 0 }, { 1, 171 }, { 4, 255 }, { 1, 64 }, { 73, 0 }, { 1, 101 }, { 4, 255 }, { 1, 167 }, { 73, 0 }, { 1, 42 }, { 1, 245 }, { 3, 255 }, { 1, 192 }, { 1, 7 }, { 72, 0 }, { 1, 8 }, { 1, 210 }, { 3, 255 }, { 1, 209 }, { 1, 15 }, { 57, 0 }, { 1, 5 }, { 15, 0 }, { 1, 148 }, { 3, 255 }, { 1, 223 }, { 1, 25 }, { 57, 0 }, { 1, 14 }, { 1, 1 }, { 14, 0 }, { 1, 80 }, { 3, 255 }, { 1, 235 }, { 1, 38 }, { 1, 0 }, { 1, 26 }, { 1, 159 }, { 1, 214 }, { 1, 142 }, { 52, 0 }, { 1, 4 }, { 1, 20 }, { 14, 0 }, { 1, 28 }, { 1, 237 }, { 2, 255 }, { 1, 245 }, { 1, 54 }, { 1, 0 }, { 1, 48 }, { 1, 231 }, { 3, 255 }, { 1, 7 }, { 51, 0 }, { 1, 29 }, { 1, 5 }, { 13, 0 }, { 1, 3 }, { 1, 193 }, { 2, 255 }, { 1, 251 }, { 1, 74 }, { 1, 0 }, { 1, 64 }, { 1, 241 }, { 4, 255 }, { 1, 32 }, { 50, 0 }, { 1, 14 }, { 1, 30 }, { 14, 0 }, { 1, 126 }, { 3, 255 }, { 1, 95 }, { 1, 0 }, { 1, 83 }, { 1, 248 }, { 5, 255 }, { 1, 59 }, { 49, 0 }, { 1, 1 }, { 1, 37 }, { 1, 12 }, { 13, 0 }, { 1, 60 }, { 1, 252 }, { 2, 255 }, { 1, 119 }, { 1, 0 }, { 1, 105 }, { 1, 253 }, { 6, 255 }, { 1, 85 }, { 49, 0 }, { 1, 21 }, { 1, 32 }, { 13, 0 }, { 1, 17 }, { 1, 225 }, { 2, 255 }, { 1, 144 }, { 1, 0 }, { 1, 128 }, { 8, 255 }, { 1, 111 }, { 48, 0 }, { 1, 3 }, { 1, 40 }, { 1, 11 }, { 13, 0 }, { 1, 173 }, { 2, 255 }, { 1, 167 }, { 1, 4 }, { 1, 152 }, { 9, 255 }, { 1, 138 }, { 48, 0 }, { 1, 25 }, { 1, 31 }, { 13, 0 }, { 1, 104 }, { 2, 255 }, { 1, 188 }, { 1, 14 }, { 1, 174 }, { 10, 255 }, { 1, 164 }, { 47, 0 }, { 1, 6 }, { 1, 41 }, { 1, 9 }, { 12, 0 }, { 1, 43 }, { 1, 246 }, { 1, 255 }, { 1, 206 }, { 1, 29 }, { 1, 192 }, { 11, 255 }, { 1, 191 }, { 47, 0 }, { 1, 28 }, { 1, 29 }, { 12, 0 }, { 1, 9 }, { 1, 211 }, { 1, 255 }, { 1, 221 }, { 1, 49 }, { 1, 209 }, { 12, 255 }, { 1, 218 }, { 46, 0 }, { 1, 5 }, { 1, 40 }, { 1, 5 }, { 12, 0 }, { 1, 151 }, { 2, 255 }, { 1, 125 }, { 1, 223 }, { 13, 255 }, { 1, 223 }, { 46, 0 }, { 1, 22 }, { 1, 20 }, { 12, 0 }, { 1, 82 }, { 18, 255 }, { 1, 121 }, { 45, 0 }, { 1, 1 }, { 1, 32 }, { 12, 0 }, { 1, 30 }, { 1, 238 }, { 17, 255 }, { 1, 139 }, { 1, 2 }, { 45, 0 }, { 1, 15 }, { 1, 8 }, { 11, 0 }, { 1, 3 }, { 1, 195 }, { 15, 255 }, { 1, 253 }, { 1, 186 }, { 1, 63 }, { 47, 0 }, { 1, 11 }, { 12, 0 }, { 1, 128 }, { 13, 255 }, { 1, 247 }, { 1, 179 }, { 1, 99 }, { 1, 22 }, { 48, 0 }, { 1, 1 }, { 12, 0 }, { 1, 62 }, { 1, 252 }, { 10, 255 }, { 1, 238 }, { 1, 163 }, { 1, 83 }, { 1, 12 }, { 63, 0 }, { 1, 18 }, { 1, 227 }, { 8, 255 }, { 1, 226 }, { 1, 146 }, { 1, 66 }, { 1, 4 }, { 66, 0 }, { 1, 175 }, { 6, 255 }, { 1, 209 }, { 1, 130 }, { 1, 50 }, { 69, 0 }, { 1, 100 }, { 3, 255 }, { 1, 252 }, { 1, 193 }, { 1, 113 }, { 1, 34 }, { 72, 0 }, { 1, 216 }, { 1, 246 }, { 1, 176 }, { 1, 97 }, { 1, 21 }, { 75, 0 }, { 1, 17 }, { 1, 4 }, { 349, 0 }, { 1, 18 }, { 1, 13 }, { 76, 0 }, { 1, 19 }, { 1, 42 }, { 1, 11 }, { 75, 0 }, { 1, 15 }, { 1, 49 }, { 1, 43 }, { 1, 3 }, { 74, 0 }, { 1, 5 }, { 1, 40 }, { 1, 59 }, { 1, 27 }, { 75, 0 }, { 1, 26 }, { 1, 59 }, { 1, 45 }, { 1, 7 }, { 74, 0 }, { 1, 12 }, { 1, 50 }, { 1, 57 }, { 1, 22 }, { 75, 0 }, { 1, 27 }, { 1, 60 }, { 1, 38 }, { 1, 4 }, { 74, 0 }, { 1, 4 }, { 1, 42 }, { 1, 38 }, { 1, 8 }, { 75, 0 }, { 1, 12 }, { 1, 29 }, { 1, 7 }, { 76, 0 }, { 1, 2 }, { 1, 5 }, { 999, 0 } }, \n\tLogoRLEFrame{ { 2042, 0 }, { 1, 2 }, { 1, 144 }, { 1, 220 }, { 1, 25 }, { 1, 4 }, { 75, 0 }, { 1, 132 }, { 2, 255 }, { 1, 64 }, { 75, 0 }, { 1, 62 }, { 1, 253 }, { 2, 255 }, { 1, 55 }, { 74, 0 }, { 1, 16 }, { 1, 225 }, { 3, 255 }, { 1, 43 }, { 74, 0 }, { 1, 169 }, { 4, 255 }, { 1, 19 }, { 55, 0 }, { 2, 5 }, { 16, 0 }, { 1, 96 }, { 4, 255 }, { 1, 201 }, { 56, 0 }, { 1, 26 }, { 16, 0 }, { 1, 35 }, { 1, 243 }, { 3, 255 }, { 1, 252 }, { 1, 66 }, { 55, 0 }, { 1, 20 }, { 1, 21 }, { 15, 0 }, { 1, 4 }, { 1, 201 }, { 4, 255 }, { 1, 105 }, { 55, 0 }, { 1, 3 }, { 1, 50 }, { 1, 1 }, { 15, 0 }, { 1, 132 }, { 4, 255 }, { 1, 134 }, { 56, 0 }, { 1, 37 }, { 1, 32 }, { 15, 0 }, { 1, 62 }, { 1, 253 }, { 3, 255 }, { 1, 160 }, { 56, 0 }, { 1, 12 }, { 1, 60 }, { 1, 6 }, { 14, 0 }, { 1, 16 }, { 1, 225 }, { 3, 255 }, { 1, 183 }, { 1, 4 }, { 1, 0 }, { 1, 28 }, { 1, 132 }, { 1, 143 }, { 1, 31 }, { 51, 0 }, { 1, 46 }, { 1, 34 }, { 15, 0 }, { 1, 169 }, { 3, 255 }, { 1, 202 }, { 1, 11 }, { 1, 0 }, { 1, 73 }, { 1, 243 }, { 2, 255 }, { 1, 165 }, { 50, 0 }, { 1, 17 }, { 1, 59 }, { 1, 5 }, { 14, 0 }, { 1, 95 }, { 3, 255 }, { 1, 219 }, { 1, 21 }, { 1, 0 }, { 1, 94 }, { 1, 251 }, { 3, 255 }, { 1, 197 }, { 50, 0 }, { 1, 51 }, { 1, 32 }, { 14, 0 }, { 1, 35 }, { 1, 242 }, { 2, 255 }, { 1, 232 }, { 1, 34 }, { 1, 0 }, { 1, 118 }, { 5, 255 }, { 1, 229 }, { 49, 0 }, { 1, 24 }, { 1, 58 }, { 1, 3 }, { 13, 0 }, { 1, 4 }, { 1, 200 }, { 2, 255 }, { 1, 243 }, { 1, 51 }, { 1, 1 }, { 1, 143 }, { 7, 255 }, { 1, 7 }, { 48, 0 }, { 1, 55 }, { 1, 29 }, { 14, 0 }, { 1, 132 }, { 2, 255 }, { 1, 250 }, { 1, 70 }, { 1, 6 }, { 1, 167 }, { 8, 255 }, { 1, 38 }, { 47, 0 }, { 1, 20 }, { 1, 52 }, { 1, 1 }, { 13, 0 }, { 1, 61 }, { 1, 253 }, { 2, 255 }, { 1, 93 }, { 1, 13 }, { 1, 187 }, { 9, 255 }, { 1, 70 }, { 47, 0 }, { 1, 47 }, { 1, 15 }, { 13, 0 }, { 1, 16 }, { 1, 225 }, { 2, 255 }, { 1, 118 }, { 1, 23 }, { 1, 205 }, { 10, 255 }, { 1, 102 }, { 46, 0 }, { 1, 11 }, { 1, 37 }, { 14, 0 }, { 1, 168 }, { 2, 255 }, { 1, 145 }, { 1, 36 }, { 1, 221 }, { 11, 255 }, { 1, 134 }, { 46, 0 }, { 1, 29 }, { 1, 2 }, { 13, 0 }, { 1, 95 }, { 2, 255 }, { 1, 170 }, { 1, 53 }, { 1, 233 }, { 12, 255 }, { 1, 166 }, { 45, 0 }, { 1, 3 }, { 1, 10 }, { 13, 0 }, { 1, 35 }, { 1, 242 }, { 1, 255 }, { 1, 226 }, { 1, 89 }, { 1, 243 }, { 13, 255 }, { 1, 187 }, { 59, 0 }, { 1, 4 }, { 1, 200 }, { 18, 255 }, { 1, 136 }, { 59, 0 }, { 1, 131 }, { 18, 255 }, { 1, 190 }, { 1, 9 }, { 58, 0 }, { 1, 61 }, { 1, 253 }, { 16, 255 }, { 1, 237 }, { 1, 125 }, { 1, 4 }, { 58, 0 }, { 1, 16 }, { 1, 225 }, { 14, 255 }, { 1, 235 }, { 1, 161 }, { 1, 84 }, { 1, 11 }, { 60, 0 }, { 1, 168 }, { 12, 255 }, { 1, 213 }, { 1, 136 }, { 1, 59 }, { 1, 2 }, { 62, 0 }, { 1, 94 }, { 9, 255 }, { 1, 250 }, { 1, 188 }, { 1, 111 }, { 1, 34 }, { 65, 0 }, { 1, 34 }, { 1, 242 }, { 6, 255 }, { 1, 236 }, { 1, 162 }, { 1, 85 }, { 1, 14 }, { 67, 0 }, { 1, 1 }, { 1, 198 }, { 4, 255 }, { 1, 214 }, { 1, 137 }, { 1, 60 }, { 1, 3 }, { 70, 0 }, { 1, 67 }, { 1, 255 }, { 1, 251 }, { 1, 189 }, { 1, 112 }, { 1, 35 }, { 74, 0 }, { 1, 9 }, { 1, 57 }, { 1, 14 }, { 111, 0 }, { 1, 6 }, { 1, 11 }, { 76, 0 }, { 1, 5 }, { 1, 32 }, { 1, 20 }, { 75, 0 }, { 1, 4 }, { 1, 33 }, { 1, 51 }, { 1, 11 }, { 75, 0 }, { 1, 24 }, { 1, 57 }, { 1, 42 }, { 1, 3 }, { 74, 0 }, { 1, 11 }, { 1, 48 }, { 1, 56 }, { 1, 20 }, { 74, 0 }, { 1, 3 }, { 1, 35 }, { 1, 61 }, { 1, 38 }, { 1, 3 }, { 74, 0 }, { 1, 14 }, { 1, 55 }, { 1, 53 }, { 1, 15 }, { 75, 0 }, { 1, 28 }, { 1, 52 }, { 1, 24 }, { 75, 0 }, { 1, 4 }, { 1, 34 }, { 1, 23 }, { 76, 0 }, { 1, 4 }, { 1, 15 }, { 1156, 0 } }, \n\tLogoRLEFrame{ { 1807, 0 }, { 2, 3 }, { 77, 0 }, { 1, 5 }, { 1, 34 }, { 74, 0 }, { 1, 15 }, { 1, 88 }, { 1, 9 }, { 1, 7 }, { 1, 52 }, { 1, 10 }, { 73, 0 }, { 1, 14 }, { 1, 210 }, { 1, 255 }, { 1, 94 }, { 1, 51 }, { 1, 33 }, { 57, 0 }, { 1, 2 }, { 16, 0 }, { 1, 160 }, { 2, 255 }, { 1, 151 }, { 1, 47 }, { 57, 0 }, { 1, 13 }, { 1, 1 }, { 15, 0 }, { 1, 80 }, { 3, 255 }, { 1, 178 }, { 1, 5 }, { 56, 0 }, { 1, 2 }, { 1, 27 }, { 15, 0 }, { 1, 21 }, { 1, 233 }, { 3, 255 }, { 1, 157 }, { 57, 0 }, { 1, 35 }, { 1, 9 }, { 15, 0 }, { 1, 175 }, { 4, 255 }, { 1, 143 }, { 56, 0 }, { 1, 13 }, { 1, 45 }, { 15, 0 }, { 1, 94 }, { 5, 255 }, { 1, 73 }, { 55, 0 }, { 1, 1 }, { 1, 52 }, { 1, 20 }, { 14, 0 }, { 1, 29 }, { 1, 240 }, { 4, 255 }, { 1, 199 }, { 1, 1 }, { 55, 0 }, { 1, 26 }, { 1, 52 }, { 14, 0 }, { 1, 1 }, { 1, 189 }, { 4, 255 }, { 1, 227 }, { 1, 24 }, { 55, 0 }, { 1, 3 }, { 1, 57 }, { 1, 20 }, { 14, 0 }, { 1, 109 }, { 4, 255 }, { 1, 241 }, { 1, 46 }, { 56, 0 }, { 1, 32 }, { 1, 50 }, { 14, 0 }, { 1, 39 }, { 1, 246 }, { 3, 255 }, { 1, 250 }, { 1, 68 }, { 56, 0 }, { 1, 6 }, { 1, 60 }, { 1, 18 }, { 13, 0 }, { 1, 3 }, { 1, 201 }, { 4, 255 }, { 1, 93 }, { 2, 0 }, { 1, 24 }, { 1, 97 }, { 1, 81 }, { 1, 1 }, { 51, 0 }, { 1, 38 }, { 1, 48 }, { 14, 0 }, { 1, 124 }, { 4, 255 }, { 1, 122 }, { 2, 0 }, { 1, 83 }, { 1, 240 }, { 2, 255 }, { 1, 113 }, { 50, 0 }, { 1, 7 }, { 1, 61 }, { 1, 14 }, { 13, 0 }, { 1, 50 }, { 1, 250 }, { 3, 255 }, { 1, 153 }, { 2, 0 }, { 1, 109 }, { 1, 253 }, { 3, 255 }, { 1, 164 }, { 50, 0 }, { 1, 33 }, { 1, 37 }, { 13, 0 }, { 1, 7 }, { 1, 212 }, { 3, 255 }, { 1, 180 }, { 1, 3 }, { 1, 1 }, { 1, 136 }, { 5, 255 }, { 1, 206 }, { 49, 0 }, { 1, 2 }, { 1, 52 }, { 1, 4 }, { 13, 0 }, { 1, 139 }, { 3, 255 }, { 1, 203 }, { 1, 11 }, { 1, 5 }, { 1, 163 }, { 6, 255 }, { 1, 247 }, { 1, 2 }, { 48, 0 }, { 1, 24 }, { 1, 20 }, { 13, 0 }, { 1, 61 }, { 1, 253 }, { 2, 255 }, { 1, 221 }, { 1, 22 }, { 1, 12 }, { 1, 186 }, { 8, 255 }, { 1, 35 }, { 48, 0 }, { 1, 27 }, { 13, 0 }, { 1, 12 }, { 1, 222 }, { 2, 255 }, { 1, 236 }, { 1, 38 }, { 1, 23 }, { 1, 205 }, { 9, 255 }, { 1, 78 }, { 47, 0 }, { 1, 6 }, { 1, 2 }, { 13, 0 }, { 1, 154 }, { 2, 255 }, { 1, 247 }, { 1, 58 }, { 1, 37 }, { 1, 222 }, { 10, 255 }, { 1, 120 }, { 61, 0 }, { 1, 74 }, { 2, 255 }, { 1, 253 }, { 1, 82 }, { 1, 55 }, { 1, 235 }, { 11, 255 }, { 1, 162 }, { 60, 0 }, { 1, 19 }, { 1, 230 }, { 2, 255 }, { 1, 110 }, { 1, 75 }, { 1, 245 }, { 12, 255 }, { 1, 205 }, { 60, 0 }, { 1, 169 }, { 2, 255 }, { 1, 180 }, { 1, 103 }, { 1, 251 }, { 13, 255 }, { 1, 242 }, { 59, 0 }, { 1, 89 }, { 19, 255 }, { 1, 232 }, { 58, 0 }, { 1, 26 }, { 1, 238 }, { 19, 255 }, { 1, 87 }, { 58, 0 }, { 1, 183 }, { 18, 255 }, { 1, 233 }, { 1, 78 }, { 58, 0 }, { 1, 104 }, { 16, 255 }, { 1, 243 }, { 1, 176 }, { 1, 98 }, { 1, 8 }, { 58, 0 }, { 1, 35 }, { 1, 244 }, { 13, 255 }, { 1, 209 }, { 1, 136 }, { 1, 64 }, { 1, 6 }, { 60, 0 }, { 1, 2 }, { 1, 196 }, { 10, 255 }, { 1, 238 }, { 1, 169 }, { 1, 97 }, { 1, 26 }, { 64, 0 }, { 1, 119 }, { 7, 255 }, { 1, 253 }, { 1, 201 }, { 1, 129 }, { 1, 57 }, { 1, 3 }, { 66, 0 }, { 1, 39 }, { 1, 249 }, { 4, 255 }, { 1, 232 }, { 1, 161 }, { 1, 89 }, { 1, 20 }, { 70, 0 }, { 1, 139 }, { 1, 255 }, { 1, 251 }, { 1, 193 }, { 1, 121 }, { 1, 49 }, { 1, 1 }, { 31, 0 }, { 1, 7 }, { 41, 0 }, { 1, 23 }, { 1, 62 }, { 1, 14 }, { 33, 0 }, { 1, 18 }, { 1, 24 }, { 1, 1 }, { 75, 0 }, { 1, 16 }, { 1, 46 }, { 1, 22 }, { 75, 0 }, { 1, 10 }, { 1, 46 }, { 1, 53 }, { 1, 11 }, { 74, 0 }, { 1, 2 }, { 1, 33 }, { 1, 60 }, { 1, 35 }, { 1, 2 }, { 74, 0 }, { 1, 19 }, { 1, 55 }, { 1, 51 }, { 1, 13 }, { 74, 0 }, { 1, 5 }, { 1, 43 }, { 1, 61 }, { 1, 30 }, { 1, 1 }, { 74, 0 }, { 1, 15 }, { 1, 55 }, { 1, 40 }, { 1, 9 }, { 75, 0 }, { 1, 29 }, { 1, 38 }, { 1, 9 }, { 75, 0 }, { 1, 4 }, { 1, 22 }, { 1, 8 }, { 77, 0 }, { 1, 2 }, { 1234, 0 } }, \n\tLogoRLEFrame{ { 1649, 0 }, { 1, 23 }, { 1, 2 }, { 77, 0 }, { 1, 33 }, { 1, 24 }, { 77, 0 }, { 1, 33 }, { 1, 48 }, { 56, 0 }, { 2, 2 }, { 15, 0 }, { 1, 17 }, { 3, 0 }, { 1, 24 }, { 1, 59 }, { 1, 9 }, { 56, 0 }, { 1, 18 }, { 15, 0 }, { 1, 152 }, { 1, 255 }, { 1, 98 }, { 1, 0 }, { 1, 15 }, { 1, 61 }, { 1, 19 }, { 56, 0 }, { 1, 12 }, { 1, 20 }, { 14, 0 }, { 1, 98 }, { 2, 255 }, { 1, 169 }, { 1, 5 }, { 1, 58 }, { 1, 32 }, { 57, 0 }, { 1, 45 }, { 1, 1 }, { 13, 0 }, { 1, 24 }, { 1, 239 }, { 2, 255 }, { 1, 216 }, { 1, 43 }, { 1, 39 }, { 57, 0 }, { 1, 29 }, { 1, 33 }, { 14, 0 }, { 1, 173 }, { 3, 255 }, { 1, 250 }, { 1, 36 }, { 57, 0 }, { 1, 8 }, { 1, 59 }, { 1, 8 }, { 13, 0 }, { 1, 82 }, { 5, 255 }, { 1, 2 }, { 57, 0 }, { 1, 41 }, { 1, 39 }, { 13, 0 }, { 1, 16 }, { 1, 230 }, { 5, 255 }, { 1, 7 }, { 56, 0 }, { 1, 12 }, { 1, 61 }, { 1, 7 }, { 13, 0 }, { 1, 156 }, { 5, 255 }, { 1, 198 }, { 57, 0 }, { 1, 47 }, { 1, 36 }, { 13, 0 }, { 1, 66 }, { 6, 255 }, { 1, 85 }, { 56, 0 }, { 1, 18 }, { 1, 60 }, { 1, 6 }, { 12, 0 }, { 1, 10 }, { 1, 221 }, { 5, 255 }, { 1, 152 }, { 57, 0 }, { 1, 52 }, { 1, 34 }, { 13, 0 }, { 1, 140 }, { 5, 255 }, { 1, 192 }, { 1, 6 }, { 56, 0 }, { 1, 19 }, { 1, 57 }, { 1, 4 }, { 12, 0 }, { 1, 52 }, { 1, 252 }, { 4, 255 }, { 1, 216 }, { 1, 17 }, { 57, 0 }, { 1, 46 }, { 1, 22 }, { 12, 0 }, { 1, 5 }, { 1, 209 }, { 4, 255 }, { 1, 235 }, { 1, 35 }, { 3, 0 }, { 1, 23 }, { 1, 7 }, { 52, 0 }, { 1, 10 }, { 1, 46 }, { 13, 0 }, { 1, 123 }, { 4, 255 }, { 1, 247 }, { 1, 58 }, { 2, 0 }, { 1, 48 }, { 1, 206 }, { 1, 255 }, { 1, 246 }, { 1, 77 }, { 51, 0 }, { 1, 33 }, { 1, 7 }, { 12, 0 }, { 1, 40 }, { 1, 248 }, { 4, 255 }, { 1, 87 }, { 2, 0 }, { 1, 73 }, { 1, 244 }, { 3, 255 }, { 1, 173 }, { 50, 0 }, { 1, 4 }, { 1, 18 }, { 12, 0 }, { 1, 1 }, { 1, 196 }, { 4, 255 }, { 1, 121 }, { 2, 0 }, { 1, 100 }, { 1, 251 }, { 4, 255 }, { 1, 233 }, { 50, 0 }, { 1, 5 }, { 13, 0 }, { 1, 107 }, { 4, 255 }, { 1, 157 }, { 2, 0 }, { 1, 131 }, { 7, 255 }, { 1, 36 }, { 62, 0 }, { 1, 29 }, { 1, 242 }, { 3, 255 }, { 1, 188 }, { 2, 5 }, { 1, 160 }, { 8, 255 }, { 1, 95 }, { 62, 0 }, { 1, 181 }, { 3, 255 }, { 1, 213 }, { 1, 15 }, { 1, 14 }, { 1, 187 }, { 9, 255 }, { 1, 154 }, { 61, 0 }, { 1, 90 }, { 3, 255 }, { 1, 232 }, { 1, 32 }, { 1, 27 }, { 1, 209 }, { 10, 255 }, { 1, 214 }, { 60, 0 }, { 1, 20 }, { 1, 235 }, { 2, 255 }, { 1, 246 }, { 1, 54 }, { 1, 44 }, { 1, 227 }, { 12, 255 }, { 1, 18 }, { 59, 0 }, { 1, 165 }, { 3, 255 }, { 1, 82 }, { 1, 65 }, { 1, 240 }, { 13, 255 }, { 1, 76 }, { 58, 0 }, { 1, 74 }, { 3, 255 }, { 1, 166 }, { 1, 94 }, { 1, 249 }, { 14, 255 }, { 1, 134 }, { 57, 0 }, { 1, 13 }, { 1, 226 }, { 20, 255 }, { 1, 165 }, { 57, 0 }, { 1, 148 }, { 21, 255 }, { 1, 89 }, { 56, 0 }, { 1, 60 }, { 21, 255 }, { 1, 137 }, { 56, 0 }, { 1, 7 }, { 1, 215 }, { 18, 255 }, { 1, 252 }, { 1, 185 }, { 1, 68 }, { 57, 0 }, { 1, 132 }, { 15, 255 }, { 1, 253 }, { 1, 205 }, { 1, 140 }, { 1, 75 }, { 1, 14 }, { 58, 0 }, { 1, 46 }, { 1, 250 }, { 12, 255 }, { 1, 209 }, { 1, 144 }, { 1, 79 }, { 1, 17 }, { 61, 0 }, { 1, 3 }, { 1, 203 }, { 9, 255 }, { 1, 212 }, { 1, 148 }, { 1, 83 }, { 1, 20 }, { 26, 0 }, { 1, 1 }, { 38, 0 }, { 1, 105 }, { 6, 255 }, { 1, 216 }, { 1, 151 }, { 1, 86 }, { 1, 22 }, { 28, 0 }, { 1, 5 }, { 1, 21 }, { 1, 4 }, { 38, 0 }, { 1, 183 }, { 2, 255 }, { 1, 220 }, { 1, 155 }, { 1, 90 }, { 1, 26 }, { 30, 0 }, { 1, 3 }, { 1, 32 }, { 1, 33 }, { 1, 1 }, { 39, 0 }, { 1, 33 }, { 1, 74 }, { 1, 29 }, { 32, 0 }, { 1, 1 }, { 1, 29 }, { 1, 56 }, { 1, 22 }, { 75, 0 }, { 1, 17 }, { 1, 53 }, { 1, 50 }, { 1, 10 }, { 74, 0 }, { 1, 6 }, { 1, 42 }, { 1, 60 }, { 1, 28 }, { 75, 0 }, { 1, 28 }, { 1, 59 }, { 1, 45 }, { 1, 8 }, { 74, 0 }, { 1, 6 }, { 1, 46 }, { 1, 55 }, { 1, 22 }, { 75, 0 }, { 1, 16 }, { 1, 49 }, { 1, 24 }, { 1, 1 }, { 75, 0 }, { 2, 23 }, { 77, 0 }, { 1, 8 }, { 1391, 0 } }, \n\tLogoRLEFrame{ { 1411, 0 }, { 1, 8 }, { 1, 2 }, { 77, 0 }, { 1, 11 }, { 1, 33 }, { 77, 0 }, { 1, 13 }, { 1, 55 }, { 1, 5 }, { 55, 0 }, { 1, 6 }, { 20, 0 }, { 1, 7 }, { 1, 57 }, { 1, 24 }, { 56, 0 }, { 1, 21 }, { 19, 0 }, { 1, 3 }, { 1, 51 }, { 1, 39 }, { 56, 0 }, { 1, 27 }, { 1, 9 }, { 19, 0 }, { 1, 44 }, { 1, 50 }, { 1, 1 }, { 55, 0 }, { 1, 7 }, { 1, 43 }, { 15, 0 }, { 1, 8 }, { 3, 0 }, { 1, 26 }, { 1, 57 }, { 1, 7 }, { 56, 0 }, { 1, 45 }, { 1, 20 }, { 13, 0 }, { 1, 15 }, { 1, 200 }, { 1, 249 }, { 1, 54 }, { 1, 0 }, { 1, 7 }, { 1, 52 }, { 1, 9 }, { 56, 0 }, { 1, 21 }, { 1, 55 }, { 14, 0 }, { 1, 158 }, { 2, 255 }, { 1, 150 }, { 1, 0 }, { 1, 36 }, { 1, 7 }, { 56, 0 }, { 1, 1 }, { 1, 54 }, { 1, 24 }, { 13, 0 }, { 1, 55 }, { 3, 255 }, { 1, 216 }, { 1, 13 }, { 1, 5 }, { 57, 0 }, { 1, 27 }, { 1, 53 }, { 13, 0 }, { 1, 2 }, { 1, 203 }, { 4, 255 }, { 1, 25 }, { 57, 0 }, { 1, 3 }, { 1, 58 }, { 1, 22 }, { 13, 0 }, { 1, 100 }, { 5, 255 }, { 1, 60 }, { 57, 0 }, { 1, 33 }, { 1, 51 }, { 13, 0 }, { 1, 17 }, { 1, 235 }, { 5, 255 }, { 1, 88 }, { 56, 0 }, { 1, 6 }, { 1, 60 }, { 1, 19 }, { 13, 0 }, { 1, 147 }, { 6, 255 }, { 1, 109 }, { 56, 0 }, { 1, 32 }, { 1, 45 }, { 13, 0 }, { 1, 46 }, { 1, 252 }, { 6, 255 }, { 1, 54 }, { 55, 0 }, { 1, 1 }, { 1, 55 }, { 1, 8 }, { 13, 0 }, { 1, 193 }, { 6, 255 }, { 1, 211 }, { 1, 1 }, { 55, 0 }, { 1, 23 }, { 1, 29 }, { 13, 0 }, { 1, 90 }, { 6, 255 }, { 1, 251 }, { 1, 50 }, { 56, 0 }, { 1, 36 }, { 13, 0 }, { 1, 12 }, { 1, 229 }, { 6, 255 }, { 1, 106 }, { 56, 0 }, { 1, 11 }, { 1, 7 }, { 13, 0 }, { 1, 137 }, { 6, 255 }, { 1, 148 }, { 57, 0 }, { 1, 2 }, { 13, 0 }, { 1, 38 }, { 1, 249 }, { 5, 255 }, { 1, 187 }, { 1, 4 }, { 71, 0 }, { 1, 183 }, { 5, 255 }, { 1, 217 }, { 1, 16 }, { 2, 0 }, { 1, 4 }, { 1, 109 }, { 1, 172 }, { 1, 145 }, { 1, 26 }, { 64, 0 }, { 1, 79 }, { 5, 255 }, { 1, 238 }, { 1, 37 }, { 2, 0 }, { 1, 22 }, { 1, 199 }, { 3, 255 }, { 1, 194 }, { 63, 0 }, { 1, 8 }, { 1, 222 }, { 4, 255 }, { 1, 251 }, { 1, 67 }, { 2, 0 }, { 1, 41 }, { 1, 222 }, { 4, 255 }, { 1, 253 }, { 1, 22 }, { 62, 0 }, { 1, 126 }, { 5, 255 }, { 1, 106 }, { 2, 0 }, { 1, 67 }, { 1, 240 }, { 6, 255 }, { 1, 100 }, { 61, 0 }, { 1, 31 }, { 1, 246 }, { 4, 255 }, { 1, 148 }, { 2, 0 }, { 1, 99 }, { 1, 250 }, { 7, 255 }, { 1, 181 }, { 61, 0 }, { 1, 173 }, { 4, 255 }, { 1, 187 }, { 1, 4 }, { 1, 1 }, { 1, 136 }, { 9, 255 }, { 1, 249 }, { 1, 13 }, { 59, 0 }, { 1, 69 }, { 4, 255 }, { 1, 217 }, { 1, 16 }, { 1, 9 }, { 1, 171 }, { 11, 255 }, { 1, 87 }, { 58, 0 }, { 1, 5 }, { 1, 215 }, { 3, 255 }, { 1, 238 }, { 1, 37 }, { 1, 22 }, { 1, 200 }, { 12, 255 }, { 1, 168 }, { 58, 0 }, { 1, 115 }, { 3, 255 }, { 1, 251 }, { 1, 67 }, { 1, 42 }, { 1, 223 }, { 13, 255 }, { 1, 242 }, { 1, 7 }, { 56, 0 }, { 1, 24 }, { 1, 242 }, { 3, 255 }, { 1, 173 }, { 1, 72 }, { 1, 240 }, { 15, 255 }, { 1, 74 }, { 56, 0 }, { 1, 162 }, { 22, 255 }, { 1, 144 }, { 55, 0 }, { 1, 59 }, { 23, 255 }, { 1, 164 }, { 54, 0 }, { 1, 2 }, { 1, 206 }, { 22, 255 }, { 1, 251 }, { 1, 54 }, { 54, 0 }, { 1, 105 }, { 22, 255 }, { 1, 236 }, { 1, 69 }, { 54, 0 }, { 1, 19 }, { 1, 237 }, { 19, 255 }, { 1, 224 }, { 1, 168 }, { 1, 98 }, { 1, 9 }, { 16, 0 }, { 1, 12 }, { 1, 4 }, { 37, 0 }, { 1, 151 }, { 15, 255 }, { 1, 243 }, { 1, 190 }, { 1, 134 }, { 1, 79 }, { 1, 24 }, { 18, 0 }, { 1, 15 }, { 1, 34 }, { 1, 7 }, { 37, 0 }, { 1, 49 }, { 1, 253 }, { 10, 255 }, { 1, 253 }, { 1, 211 }, { 1, 155 }, { 1, 100 }, { 1, 45 }, { 1, 2 }, { 20, 0 }, { 1, 13 }, { 1, 46 }, { 1, 37 }, { 1, 1 }, { 38, 0 }, { 1, 179 }, { 7, 255 }, { 1, 232 }, { 1, 177 }, { 1, 121 }, { 1, 66 }, { 1, 13 }, { 4, 0 }, { 1, 2 }, { 1, 8 }, { 17, 0 }, { 1, 5 }, { 1, 39 }, { 1, 59 }, { 1, 23 }, { 40, 0 }, { 1, 223 }, { 2, 255 }, { 1, 248 }, { 1, 198 }, { 1, 142 }, { 1, 87 }, { 1, 32 }, { 8, 0 }, { 1, 9 }, { 1, 20 }, { 17, 0 }, { 1, 26 }, { 1, 59 }, { 1, 43 }, { 1, 6 }, { 41, 0 }, { 1, 39 }, { 1, 88 }, { 1, 53 }, { 1, 6 }, { 11, 0 }, { 1, 21 }, { 1, 25 }, { 16, 0 }, { 1, 13 }, { 1, 50 }, { 1, 56 }, { 1, 20 }, { 56, 0 }, { 1, 5 }, { 1, 32 }, { 1, 26 }, { 16, 0 }, { 1, 32 }, { 1, 62 }, { 1, 38 }, { 1, 3 }, { 56, 0 }, { 1, 10 }, { 1, 38 }, { 1, 24 }, { 15, 0 }, { 1, 6 }, { 1, 47 }, { 1, 41 }, { 1, 11 }, { 57, 0 }, { 1, 17 }, { 1, 40 }, { 1, 18 }, { 15, 0 }, { 1, 17 }, { 1, 35 }, { 1, 10 }, { 58, 0 }, { 1, 24 }, { 1, 40 }, { 1, 12 }, { 15, 0 }, { 1, 11 }, { 1, 9 }, { 58, 0 }, { 1, 1 }, { 1, 31 }, { 1, 37 }, { 1, 8 }, { 75, 0 }, { 1, 2 }, { 1, 32 }, { 1, 31 }, { 1, 4 }, { 75, 0 }, { 1, 2 }, { 1, 31 }, { 1, 20 }, { 76, 0 }, { 1, 2 }, { 1, 25 }, { 1, 9 }, { 76, 0 }, { 1, 1 }, { 1, 11 }, { 1171, 0 } }, \n\tLogoRLEFrame{ { 1174, 0 }, { 1, 1 }, { 78, 0 }, { 1, 29 }, { 77, 0 }, { 1, 1 }, { 1, 43 }, { 1, 17 }, { 55, 0 }, { 1, 11 }, { 21, 0 }, { 1, 43 }, { 1, 42 }, { 55, 0 }, { 1, 7 }, { 1, 18 }, { 20, 0 }, { 1, 33 }, { 1, 54 }, { 1, 4 }, { 55, 0 }, { 1, 38 }, { 1, 1 }, { 19, 0 }, { 1, 24 }, { 1, 60 }, { 1, 12 }, { 55, 0 }, { 1, 21 }, { 1, 33 }, { 19, 0 }, { 1, 11 }, { 1, 61 }, { 1, 23 }, { 55, 0 }, { 1, 4 }, { 1, 57 }, { 1, 8 }, { 19, 0 }, { 1, 50 }, { 1, 29 }, { 56, 0 }, { 1, 36 }, { 1, 42 }, { 19, 0 }, { 1, 29 }, { 1, 25 }, { 56, 0 }, { 1, 9 }, { 1, 61 }, { 1, 10 }, { 13, 0 }, { 1, 44 }, { 1, 19 }, { 3, 0 }, { 1, 9 }, { 1, 20 }, { 57, 0 }, { 1, 42 }, { 1, 40 }, { 13, 0 }, { 1, 125 }, { 1, 255 }, { 1, 232 }, { 1, 19 }, { 2, 0 }, { 1, 5 }, { 57, 0 }, { 1, 14 }, { 1, 61 }, { 1, 9 }, { 12, 0 }, { 1, 38 }, { 1, 251 }, { 2, 255 }, { 1, 107 }, { 60, 0 }, { 1, 48 }, { 1, 38 }, { 13, 0 }, { 1, 170 }, { 3, 255 }, { 1, 191 }, { 59, 0 }, { 1, 17 }, { 1, 60 }, { 1, 7 }, { 12, 0 }, { 1, 51 }, { 4, 255 }, { 1, 252 }, { 1, 22 }, { 58, 0 }, { 1, 45 }, { 1, 30 }, { 13, 0 }, { 1, 186 }, { 5, 255 }, { 1, 85 }, { 57, 0 }, { 1, 9 }, { 1, 52 }, { 1, 1 }, { 12, 0 }, { 1, 66 }, { 6, 255 }, { 1, 132 }, { 57, 0 }, { 1, 35 }, { 1, 14 }, { 13, 0 }, { 1, 202 }, { 6, 255 }, { 1, 180 }, { 56, 0 }, { 1, 3 }, { 1, 29 }, { 13, 0 }, { 1, 83 }, { 7, 255 }, { 1, 210 }, { 56, 0 }, { 1, 14 }, { 13, 0 }, { 1, 3 }, { 1, 216 }, { 7, 255 }, { 1, 167 }, { 70, 0 }, { 1, 99 }, { 8, 255 }, { 1, 86 }, { 69, 0 }, { 1, 8 }, { 1, 227 }, { 7, 255 }, { 1, 192 }, { 70, 0 }, { 1, 116 }, { 7, 255 }, { 1, 235 }, { 1, 31 }, { 69, 0 }, { 1, 15 }, { 1, 237 }, { 6, 255 }, { 1, 251 }, { 1, 66 }, { 70, 0 }, { 1, 132 }, { 7, 255 }, { 1, 112 }, { 70, 0 }, { 1, 24 }, { 1, 244 }, { 6, 255 }, { 1, 163 }, { 4, 0 }, { 1, 7 }, { 1, 45 }, { 1, 27 }, { 64, 0 }, { 1, 149 }, { 6, 255 }, { 1, 205 }, { 1, 9 }, { 3, 0 }, { 1, 81 }, { 1, 231 }, { 2, 255 }, { 1, 173 }, { 1, 2 }, { 61, 0 }, { 1, 35 }, { 1, 250 }, { 5, 255 }, { 1, 234 }, { 1, 30 }, { 2, 0 }, { 1, 1 }, { 1, 129 }, { 5, 255 }, { 1, 76 }, { 61, 0 }, { 1, 165 }, { 5, 255 }, { 1, 251 }, { 1, 64 }, { 2, 0 }, { 1, 11 }, { 1, 173 }, { 6, 255 }, { 1, 184 }, { 60, 0 }, { 1, 47 }, { 6, 255 }, { 1, 110 }, { 2, 0 }, { 1, 31 }, { 1, 208 }, { 8, 255 }, { 1, 37 }, { 59, 0 }, { 1, 181 }, { 5, 255 }, { 1, 161 }, { 2, 0 }, { 1, 60 }, { 1, 234 }, { 9, 255 }, { 1, 143 }, { 58, 0 }, { 1, 62 }, { 5, 255 }, { 1, 204 }, { 1, 8 }, { 1, 0 }, { 1, 99 }, { 1, 249 }, { 10, 255 }, { 1, 240 }, { 1, 11 }, { 57, 0 }, { 1, 198 }, { 4, 255 }, { 1, 233 }, { 1, 29 }, { 1, 3 }, { 1, 145 }, { 13, 255 }, { 1, 102 }, { 56, 0 }, { 1, 78 }, { 4, 255 }, { 1, 251 }, { 1, 63 }, { 1, 17 }, { 1, 186 }, { 14, 255 }, { 1, 210 }, { 55, 0 }, { 1, 2 }, { 1, 212 }, { 4, 255 }, { 1, 209 }, { 1, 49 }, { 1, 218 }, { 16, 255 }, { 1, 61 }, { 54, 0 }, { 1, 95 }, { 24, 255 }, { 1, 169 }, { 53, 0 }, { 1, 7 }, { 1, 224 }, { 24, 255 }, { 1, 250 }, { 1, 12 }, { 14, 0 }, { 1, 3 }, { 1, 2 }, { 36, 0 }, { 1, 111 }, { 25, 255 }, { 1, 237 }, { 1, 8 }, { 12, 0 }, { 1, 3 }, { 1, 26 }, { 1, 13 }, { 36, 0 }, { 1, 13 }, { 1, 234 }, { 25, 255 }, { 1, 109 }, { 11, 0 }, { 1, 2 }, { 1, 29 }, { 1, 45 }, { 1, 7 }, { 37, 0 }, { 1, 128 }, { 24, 255 }, { 1, 222 }, { 1, 92 }, { 11, 0 }, { 1, 24 }, { 1, 57 }, { 1, 37 }, { 1, 1 }, { 37, 0 }, { 1, 21 }, { 1, 243 }, { 19, 255 }, { 1, 240 }, { 1, 195 }, { 1, 150 }, { 1, 105 }, { 1, 57 }, { 1, 1 }, { 10, 0 }, { 1, 11 }, { 1, 48 }, { 1, 55 }, { 1, 18 }, { 39, 0 }, { 1, 144 }, { 14, 255 }, { 1, 250 }, { 1, 211 }, { 1, 183 }, { 1, 139 }, { 1, 74 }, { 1, 29 }, { 14, 0 }, { 1, 3 }, { 1, 35 }, { 1, 61 }, { 1, 36 }, { 1, 3 }, { 39, 0 }, { 1, 2 }, { 1, 239 }, { 9, 255 }, { 1, 225 }, { 1, 180 }, { 1, 134 }, { 1, 89 }, { 1, 44 }, { 1, 23 }, { 1, 59 }, { 1, 31 }, { 16, 0 }, { 1, 18 }, { 1, 56 }, { 1, 52 }, { 1, 14 }, { 41, 0 }, { 1, 3 }, { 1, 231 }, { 3, 255 }, { 1, 240 }, { 1, 195 }, { 1, 149 }, { 1, 104 }, { 1, 59 }, { 1, 15 }, { 4, 0 }, { 1, 31 }, { 1, 61 }, { 1, 22 }, { 15, 0 }, { 1, 1 }, { 1, 34 }, { 1, 56 }, { 1, 27 }, { 1, 1 }, { 43, 0 }, { 1, 30 }, { 1, 92 }, { 1, 74 }, { 1, 29 }, { 8, 0 }, { 1, 2 }, { 1, 42 }, { 1, 58 }, { 1, 15 }, { 15, 0 }, { 1, 7 }, { 1, 42 }, { 1, 26 }, { 1, 1 }, { 56, 0 }, { 1, 2 }, { 1, 47 }, { 1, 52 }, { 1, 9 }, { 15, 0 }, { 1, 12 }, { 1, 21 }, { 1, 1 }, { 57, 0 }, { 1, 2 }, { 1, 47 }, { 1, 38 }, { 1, 2 }, { 16, 0 }, { 1, 1 }, { 58, 0 }, { 1, 3 }, { 1, 44 }, { 1, 21 }, { 76, 0 }, { 1, 3 }, { 1, 27 }, { 1, 5 }, { 77, 0 }, { 1, 4 }, { 1407, 0 } }, \n\tLogoRLEFrame{ { 1015, 0 }, { 1, 6 }, { 1, 1 }, { 54, 0 }, { 1, 1 }, { 22, 0 }, { 1, 9 }, { 1, 14 }, { 55, 0 }, { 1, 14 }, { 21, 0 }, { 1, 10 }, { 1, 26 }, { 1, 1 }, { 54, 0 }, { 1, 19 }, { 1, 9 }, { 20, 0 }, { 1, 7 }, { 1, 29 }, { 1, 8 }, { 54, 0 }, { 1, 3 }, { 1, 39 }, { 20, 0 }, { 1, 3 }, { 1, 28 }, { 1, 14 }, { 55, 0 }, { 1, 38 }, { 1, 20 }, { 19, 0 }, { 1, 1 }, { 1, 25 }, { 1, 21 }, { 55, 0 }, { 1, 15 }, { 1, 56 }, { 20, 0 }, { 1, 17 }, { 1, 24 }, { 1, 1 }, { 55, 0 }, { 1, 50 }, { 1, 28 }, { 19, 0 }, { 1, 6 }, { 1, 23 }, { 1, 1 }, { 55, 0 }, { 1, 22 }, { 1, 56 }, { 1, 2 }, { 19, 0 }, { 1, 17 }, { 1, 1 }, { 55, 0 }, { 1, 1 }, { 1, 55 }, { 1, 26 }, { 19, 0 }, { 1, 6 }, { 57, 0 }, { 1, 29 }, { 1, 55 }, { 1, 1 }, { 76, 0 }, { 1, 4 }, { 1, 58 }, { 1, 23 }, { 11, 0 }, { 1, 53 }, { 1, 218 }, { 1, 196 }, { 1, 20 }, { 62, 0 }, { 1, 31 }, { 1, 51 }, { 11, 0 }, { 1, 2 }, { 1, 217 }, { 2, 255 }, { 1, 155 }, { 61, 0 }, { 1, 1 }, { 1, 56 }, { 1, 14 }, { 11, 0 }, { 1, 87 }, { 3, 255 }, { 1, 243 }, { 1, 12 }, { 60, 0 }, { 1, 22 }, { 1, 37 }, { 12, 0 }, { 1, 207 }, { 4, 255 }, { 1, 99 }, { 60, 0 }, { 1, 42 }, { 1, 4 }, { 11, 0 }, { 1, 73 }, { 5, 255 }, { 1, 198 }, { 59, 0 }, { 1, 13 }, { 1, 15 }, { 12, 0 }, { 1, 194 }, { 6, 255 }, { 1, 20 }, { 58, 0 }, { 1, 10 }, { 12, 0 }, { 1, 59 }, { 7, 255 }, { 1, 82 }, { 71, 0 }, { 1, 180 }, { 7, 255 }, { 1, 146 }, { 70, 0 }, { 1, 47 }, { 8, 255 }, { 1, 206 }, { 70, 0 }, { 1, 167 }, { 8, 255 }, { 1, 198 }, { 69, 0 }, { 1, 35 }, { 1, 252 }, { 8, 255 }, { 1, 162 }, { 69, 0 }, { 1, 153 }, { 9, 255 }, { 1, 53 }, { 68, 0 }, { 1, 25 }, { 1, 248 }, { 8, 255 }, { 1, 167 }, { 69, 0 }, { 1, 139 }, { 8, 255 }, { 1, 218 }, { 1, 14 }, { 68, 0 }, { 1, 17 }, { 1, 243 }, { 7, 255 }, { 1, 246 }, { 1, 46 }, { 69, 0 }, { 1, 126 }, { 8, 255 }, { 1, 95 }, { 69, 0 }, { 1, 11 }, { 1, 236 }, { 7, 255 }, { 1, 154 }, { 5, 0 }, { 1, 20 }, { 1, 50 }, { 1, 15 }, { 62, 0 }, { 1, 112 }, { 7, 255 }, { 1, 204 }, { 1, 7 }, { 3, 0 }, { 1, 8 }, { 1, 144 }, { 1, 252 }, { 1, 255 }, { 1, 253 }, { 1, 148 }, { 60, 0 }, { 1, 6 }, { 1, 227 }, { 6, 255 }, { 1, 238 }, { 1, 33 }, { 3, 0 }, { 1, 31 }, { 1, 204 }, { 5, 255 }, { 1, 83 }, { 59, 0 }, { 1, 98 }, { 7, 255 }, { 1, 76 }, { 3, 0 }, { 1, 69 }, { 1, 236 }, { 6, 255 }, { 1, 215 }, { 1, 3 }, { 57, 0 }, { 1, 2 }, { 1, 217 }, { 6, 255 }, { 1, 133 }, { 2, 0 }, { 1, 1 }, { 1, 121 }, { 1, 253 }, { 8, 255 }, { 1, 97 }, { 57, 0 }, { 1, 85 }, { 6, 255 }, { 1, 189 }, { 1, 3 }, { 1, 0 }, { 1, 14 }, { 1, 175 }, { 10, 255 }, { 1, 225 }, { 1, 7 }, { 56, 0 }, { 1, 205 }, { 5, 255 }, { 1, 229 }, { 1, 22 }, { 1, 0 }, { 1, 42 }, { 1, 217 }, { 12, 255 }, { 1, 111 }, { 55, 0 }, { 1, 71 }, { 5, 255 }, { 1, 251 }, { 1, 59 }, { 1, 0 }, { 1, 86 }, { 1, 243 }, { 13, 255 }, { 1, 234 }, { 1, 12 }, { 54, 0 }, { 1, 192 }, { 5, 255 }, { 1, 150 }, { 1, 4 }, { 1, 141 }, { 16, 255 }, { 1, 125 }, { 53, 0 }, { 1, 58 }, { 6, 255 }, { 1, 228 }, { 1, 202 }, { 17, 255 }, { 1, 241 }, { 1, 19 }, { 52, 0 }, { 1, 179 }, { 26, 255 }, { 1, 140 }, { 51, 0 }, { 1, 45 }, { 27, 255 }, { 1, 245 }, { 1, 13 }, { 50, 0 }, { 1, 165 }, { 28, 255 }, { 1, 25 }, { 49, 0 }, { 1, 34 }, { 1, 252 }, { 27, 255 }, { 1, 179 }, { 50, 0 }, { 1, 151 }, { 27, 255 }, { 1, 170 }, { 1, 11 }, { 49, 0 }, { 1, 24 }, { 1, 248 }, { 21, 255 }, { 1, 248 }, { 1, 214 }, { 1, 178 }, { 1, 142 }, { 1, 105 }, { 1, 31 }, { 51, 0 }, { 1, 111 }, { 15, 255 }, { 1, 248 }, { 1, 219 }, { 1, 176 }, { 1, 139 }, { 1, 103 }, { 1, 67 }, { 1, 31 }, { 1, 2 }, { 56, 0 }, { 1, 127 }, { 8, 255 }, { 1, 245 }, { 1, 209 }, { 1, 173 }, { 1, 137 }, { 1, 101 }, { 1, 66 }, { 1, 69 }, { 1, 29 }, { 63, 0 }, { 1, 24 }, { 1, 184 }, { 1, 227 }, { 1, 207 }, { 1, 171 }, { 1, 134 }, { 1, 98 }, { 1, 62 }, { 1, 26 }, { 4, 0 }, { 1, 3 }, { 1, 36 }, { 1, 12 }, { 76, 0 }, { 1, 1 }, { 1, 15 }, { 1722, 0 } }, \n\tLogoRLEFrame{ { 833, 0 }, { 1, 1 }, { 78, 0 }, { 1, 1 }, { 1, 7 }, { 78, 0 }, { 1, 15 }, { 78, 0 }, { 1, 7 }, { 1, 16 }, { 78, 0 }, { 1, 25 }, { 1, 3 }, { 77, 0 }, { 1, 15 }, { 1, 22 }, { 77, 0 }, { 1, 3 }, { 1, 29 }, { 1, 6 }, { 77, 0 }, { 1, 18 }, { 1, 21 }, { 77, 0 }, { 1, 5 }, { 1, 30 }, { 1, 5 }, { 77, 0 }, { 1, 21 }, { 1, 20 }, { 77, 0 }, { 1, 7 }, { 1, 30 }, { 1, 4 }, { 77, 0 }, { 1, 22 }, { 1, 18 }, { 77, 0 }, { 1, 4 }, { 1, 27 }, { 1, 1 }, { 9, 0 }, { 1, 104 }, { 1, 242 }, { 1, 191 }, { 1, 15 }, { 64, 0 }, { 1, 17 }, { 1, 10 }, { 9, 0 }, { 1, 23 }, { 1, 249 }, { 2, 255 }, { 1, 156 }, { 63, 0 }, { 1, 1 }, { 1, 19 }, { 10, 0 }, { 1, 126 }, { 3, 255 }, { 1, 248 }, { 1, 22 }, { 62, 0 }, { 1, 9 }, { 1, 2 }, { 9, 0 }, { 1, 4 }, { 1, 229 }, { 4, 255 }, { 1, 125 }, { 62, 0 }, { 1, 3 }, { 10, 0 }, { 1, 86 }, { 5, 255 }, { 1, 230 }, { 1, 5 }, { 72, 0 }, { 1, 194 }, { 6, 255 }, { 1, 82 }, { 71, 0 }, { 1, 47 }, { 7, 255 }, { 1, 158 }, { 71, 0 }, { 1, 155 }, { 7, 255 }, { 1, 233 }, { 1, 1 }, { 69, 0 }, { 1, 17 }, { 1, 246 }, { 8, 255 }, { 1, 53 }, { 69, 0 }, { 1, 115 }, { 9, 255 }, { 1, 117 }, { 68, 0 }, { 1, 2 }, { 1, 221 }, { 9, 255 }, { 1, 114 }, { 68, 0 }, { 1, 76 }, { 10, 255 }, { 1, 90 }, { 68, 0 }, { 1, 184 }, { 9, 255 }, { 1, 236 }, { 1, 9 }, { 67, 0 }, { 1, 38 }, { 10, 255 }, { 1, 119 }, { 68, 0 }, { 1, 145 }, { 9, 255 }, { 1, 190 }, { 1, 2 }, { 51, 0 }, { 1, 7 }, { 1, 4 }, { 14, 0 }, { 1, 12 }, { 1, 241 }, { 8, 255 }, { 1, 234 }, { 1, 25 }, { 52, 0 }, { 1, 27 }, { 15, 0 }, { 1, 105 }, { 9, 255 }, { 1, 72 }, { 52, 0 }, { 1, 12 }, { 1, 29 }, { 15, 0 }, { 1, 213 }, { 8, 255 }, { 1, 138 }, { 53, 0 }, { 1, 31 }, { 1, 18 }, { 14, 0 }, { 1, 66 }, { 8, 255 }, { 1, 200 }, { 1, 5 }, { 4, 0 }, { 1, 27 }, { 1, 158 }, { 1, 209 }, { 1, 192 }, { 1, 113 }, { 1, 5 }, { 42, 0 }, { 1, 6 }, { 1, 41 }, { 1, 3 }, { 14, 0 }, { 1, 174 }, { 7, 255 }, { 1, 240 }, { 1, 32 }, { 4, 0 }, { 1, 80 }, { 1, 239 }, { 4, 255 }, { 1, 163 }, { 42, 0 }, { 1, 23 }, { 1, 29 }, { 14, 0 }, { 1, 30 }, { 1, 252 }, { 7, 255 }, { 1, 84 }, { 3, 0 }, { 1, 5 }, { 1, 142 }, { 7, 255 }, { 1, 68 }, { 40, 0 }, { 1, 1 }, { 1, 39 }, { 1, 14 }, { 14, 0 }, { 1, 134 }, { 7, 255 }, { 1, 150 }, { 3, 0 }, { 1, 31 }, { 1, 200 }, { 8, 255 }, { 1, 219 }, { 1, 8 }, { 2, 0 }, { 1, 4 }, { 36, 0 }, { 1, 10 }, { 1, 37 }, { 1, 1 }, { 13, 0 }, { 1, 8 }, { 1, 235 }, { 6, 255 }, { 1, 209 }, { 1, 8 }, { 2, 0 }, { 1, 77 }, { 1, 238 }, { 10, 255 }, { 1, 131 }, { 1, 8 }, { 1, 25 }, { 37, 0 }, { 1, 22 }, { 1, 15 }, { 14, 0 }, { 1, 95 }, { 6, 255 }, { 1, 244 }, { 1, 40 }, { 1, 0 }, { 1, 4 }, { 1, 139 }, { 12, 255 }, { 1, 250 }, { 1, 72 }, { 38, 0 }, { 1, 26 }, { 15, 0 }, { 1, 204 }, { 6, 255 }, { 1, 96 }, { 1, 0 }, { 1, 29 }, { 1, 197 }, { 14, 255 }, { 1, 195 }, { 1, 1 }, { 36, 0 }, { 1, 3 }, { 1, 11 }, { 14, 0 }, { 1, 56 }, { 6, 255 }, { 1, 228 }, { 1, 0 }, { 1, 74 }, { 1, 236 }, { 16, 255 }, { 1, 99 }, { 36, 0 }, { 1, 4 }, { 15, 0 }, { 1, 164 }, { 7, 255 }, { 1, 196 }, { 18, 255 }, { 1, 238 }, { 1, 22 }, { 50, 0 }, { 1, 23 }, { 1, 249 }, { 27, 255 }, { 1, 163 }, { 50, 0 }, { 1, 124 }, { 29, 255 }, { 1, 61 }, { 48, 0 }, { 1, 4 }, { 1, 228 }, { 29, 255 }, { 1, 154 }, { 48, 0 }, { 1, 85 }, { 30, 255 }, { 1, 112 }, { 48, 0 }, { 1, 193 }, { 29, 255 }, { 1, 204 }, { 1, 17 }, { 47, 0 }, { 1, 45 }, { 27, 255 }, { 1, 247 }, { 1, 205 }, { 1, 119 }, { 1, 11 }, { 48, 0 }, { 1, 107 }, { 19, 255 }, { 1, 231 }, { 1, 201 }, { 1, 171 }, { 1, 141 }, { 1, 111 }, { 1, 81 }, { 1, 51 }, { 1, 21 }, { 52, 0 }, { 1, 84 }, { 10, 255 }, { 1, 245 }, { 1, 215 }, { 1, 185 }, { 1, 155 }, { 1, 125 }, { 1, 95 }, { 1, 65 }, { 1, 35 }, { 1, 7 }, { 60, 0 }, { 1, 3 }, { 1, 140 }, { 1, 224 }, { 1, 228 }, { 1, 198 }, { 1, 168 }, { 1, 138 }, { 1, 108 }, { 1, 78 }, { 1, 48 }, { 1, 19 }, { 1805, 0 } }, \n\tLogoRLEFrame{ { 1717, 0 }, { 1, 74 }, { 1, 216 }, { 1, 178 }, { 1, 22 }, { 75, 0 }, { 1, 6 }, { 1, 234 }, { 2, 255 }, { 1, 190 }, { 75, 0 }, { 1, 84 }, { 4, 255 }, { 1, 62 }, { 74, 0 }, { 1, 182 }, { 4, 255 }, { 1, 182 }, { 73, 0 }, { 1, 28 }, { 1, 253 }, { 5, 255 }, { 1, 48 }, { 72, 0 }, { 1, 123 }, { 6, 255 }, { 1, 165 }, { 71, 0 }, { 1, 1 }, { 1, 220 }, { 6, 255 }, { 1, 247 }, { 1, 12 }, { 70, 0 }, { 1, 63 }, { 8, 255 }, { 1, 88 }, { 70, 0 }, { 1, 161 }, { 8, 255 }, { 1, 174 }, { 69, 0 }, { 1, 14 }, { 1, 245 }, { 8, 255 }, { 1, 247 }, { 1, 12 }, { 68, 0 }, { 1, 101 }, { 10, 255 }, { 1, 58 }, { 68, 0 }, { 1, 200 }, { 10, 255 }, { 1, 58 }, { 52, 0 }, { 1, 17 }, { 14, 0 }, { 1, 42 }, { 11, 255 }, { 1, 33 }, { 51, 0 }, { 1, 8 }, { 1, 31 }, { 14, 0 }, { 1, 140 }, { 10, 255 }, { 1, 191 }, { 52, 0 }, { 1, 42 }, { 1, 17 }, { 13, 0 }, { 1, 5 }, { 1, 233 }, { 10, 255 }, { 1, 68 }, { 51, 0 }, { 1, 9 }, { 1, 61 }, { 1, 3 }, { 13, 0 }, { 1, 80 }, { 10, 255 }, { 1, 142 }, { 52, 0 }, { 1, 34 }, { 1, 42 }, { 14, 0 }, { 1, 178 }, { 9, 255 }, { 1, 209 }, { 1, 7 }, { 51, 0 }, { 1, 1 }, { 1, 58 }, { 1, 19 }, { 13, 0 }, { 1, 25 }, { 1, 252 }, { 8, 255 }, { 1, 247 }, { 1, 43 }, { 52, 0 }, { 1, 22 }, { 1, 58 }, { 1, 1 }, { 13, 0 }, { 1, 118 }, { 9, 255 }, { 1, 108 }, { 53, 0 }, { 1, 41 }, { 1, 33 }, { 14, 0 }, { 1, 216 }, { 8, 255 }, { 1, 182 }, { 5, 0 }, { 1, 2 }, { 1, 102 }, { 1, 171 }, { 1, 174 }, { 1, 118 }, { 1, 13 }, { 7, 0 }, { 1, 4 }, { 1, 15 }, { 34, 0 }, { 1, 53 }, { 1, 3 }, { 13, 0 }, { 1, 59 }, { 8, 255 }, { 1, 233 }, { 1, 23 }, { 4, 0 }, { 1, 29 }, { 1, 194 }, { 4, 255 }, { 1, 226 }, { 1, 20 }, { 5, 0 }, { 1, 15 }, { 1, 32 }, { 34, 0 }, { 1, 13 }, { 1, 28 }, { 14, 0 }, { 1, 157 }, { 8, 255 }, { 1, 75 }, { 4, 0 }, { 1, 80 }, { 1, 237 }, { 6, 255 }, { 1, 175 }, { 3, 0 }, { 1, 1 }, { 1, 34 }, { 1, 39 }, { 35, 0 }, { 1, 22 }, { 1, 1 }, { 13, 0 }, { 1, 12 }, { 1, 243 }, { 7, 255 }, { 1, 148 }, { 3, 0 }, { 1, 8 }, { 1, 148 }, { 9, 255 }, { 1, 99 }, { 1, 0 }, { 1, 8 }, { 1, 50 }, { 1, 40 }, { 36, 0 }, { 1, 7 }, { 14, 0 }, { 1, 97 }, { 7, 255 }, { 1, 214 }, { 1, 9 }, { 2, 0 }, { 1, 40 }, { 1, 209 }, { 10, 255 }, { 1, 244 }, { 1, 48 }, { 1, 58 }, { 1, 35 }, { 52, 0 }, { 1, 195 }, { 6, 255 }, { 1, 249 }, { 1, 48 }, { 2, 0 }, { 1, 98 }, { 1, 245 }, { 12, 255 }, { 1, 212 }, { 1, 29 }, { 52, 0 }, { 1, 38 }, { 7, 255 }, { 1, 117 }, { 1, 0 }, { 1, 14 }, { 1, 167 }, { 15, 255 }, { 1, 128 }, { 52, 0 }, { 1, 135 }, { 7, 255 }, { 1, 58 }, { 1, 54 }, { 1, 221 }, { 16, 255 }, { 1, 252 }, { 1, 56 }, { 50, 0 }, { 1, 3 }, { 1, 230 }, { 7, 255 }, { 1, 242 }, { 1, 252 }, { 18, 255 }, { 1, 220 }, { 1, 12 }, { 49, 0 }, { 1, 76 }, { 29, 255 }, { 1, 156 }, { 49, 0 }, { 1, 174 }, { 30, 255 }, { 1, 75 }, { 47, 0 }, { 1, 22 }, { 1, 250 }, { 30, 255 }, { 1, 189 }, { 47, 0 }, { 1, 114 }, { 31, 255 }, { 1, 180 }, { 47, 0 }, { 1, 212 }, { 30, 255 }, { 1, 250 }, { 1, 73 }, { 46, 0 }, { 1, 48 }, { 29, 255 }, { 1, 252 }, { 1, 189 }, { 1, 67 }, { 47, 0 }, { 1, 82 }, { 20, 255 }, { 1, 251 }, { 1, 226 }, { 1, 200 }, { 1, 174 }, { 1, 147 }, { 1, 121 }, { 1, 95 }, { 1, 68 }, { 1, 42 }, { 1, 14 }, { 49, 0 }, { 1, 24 }, { 1, 248 }, { 10, 255 }, { 1, 233 }, { 1, 207 }, { 1, 180 }, { 1, 154 }, { 1, 128 }, { 1, 101 }, { 1, 75 }, { 1, 49 }, { 1, 23 }, { 1, 2 }, { 59, 0 }, { 1, 62 }, { 1, 187 }, { 1, 208 }, { 1, 187 }, { 1, 161 }, { 1, 135 }, { 1, 108 }, { 1, 82 }, { 1, 56 }, { 1, 29 }, { 1, 5 }, { 1804, 0 } }, \n\tLogoRLEFrame{ { 1717, 0 }, { 1, 16 }, { 1, 160 }, { 1, 162 }, { 1, 39 }, { 76, 0 }, { 1, 154 }, { 2, 255 }, { 1, 226 }, { 1, 19 }, { 74, 0 }, { 1, 9 }, { 1, 242 }, { 3, 255 }, { 1, 134 }, { 74, 0 }, { 1, 86 }, { 4, 255 }, { 1, 242 }, { 1, 18 }, { 73, 0 }, { 1, 177 }, { 5, 255 }, { 1, 132 }, { 72, 0 }, { 1, 18 }, { 1, 249 }, { 5, 255 }, { 1, 241 }, { 1, 17 }, { 71, 0 }, { 1, 102 }, { 7, 255 }, { 1, 115 }, { 71, 0 }, { 1, 193 }, { 7, 255 }, { 1, 206 }, { 54, 0 }, { 1, 2 }, { 1, 12 }, { 14, 0 }, { 1, 29 }, { 9, 255 }, { 1, 43 }, { 53, 0 }, { 1, 31 }, { 1, 6 }, { 14, 0 }, { 1, 118 }, { 9, 255 }, { 1, 134 }, { 52, 0 }, { 1, 6 }, { 1, 52 }, { 15, 0 }, { 1, 209 }, { 9, 255 }, { 1, 222 }, { 52, 0 }, { 1, 34 }, { 1, 39 }, { 14, 0 }, { 1, 44 }, { 10, 255 }, { 1, 250 }, { 51, 0 }, { 1, 1 }, { 1, 57 }, { 1, 17 }, { 14, 0 }, { 1, 135 }, { 11, 255 }, { 1, 3 }, { 50, 0 }, { 1, 22 }, { 1, 56 }, { 14, 0 }, { 1, 1 }, { 1, 224 }, { 10, 255 }, { 1, 205 }, { 51, 0 }, { 1, 47 }, { 1, 34 }, { 14, 0 }, { 1, 60 }, { 11, 255 }, { 1, 113 }, { 50, 0 }, { 1, 4 }, { 1, 61 }, { 1, 9 }, { 14, 0 }, { 1, 151 }, { 10, 255 }, { 1, 215 }, { 1, 8 }, { 19, 0 }, { 1, 5 }, { 30, 0 }, { 1, 21 }, { 1, 37 }, { 14, 0 }, { 1, 5 }, { 1, 236 }, { 9, 255 }, { 1, 251 }, { 1, 52 }, { 18, 0 }, { 1, 9 }, { 1, 27 }, { 31, 0 }, { 1, 35 }, { 1, 5 }, { 14, 0 }, { 1, 76 }, { 10, 255 }, { 1, 127 }, { 18, 0 }, { 1, 24 }, { 1, 37 }, { 32, 0 }, { 1, 24 }, { 15, 0 }, { 1, 167 }, { 9, 255 }, { 1, 203 }, { 1, 4 }, { 16, 0 }, { 1, 4 }, { 1, 43 }, { 1, 40 }, { 32, 0 }, { 1, 4 }, { 1, 2 }, { 14, 0 }, { 1, 12 }, { 1, 245 }, { 8, 255 }, { 1, 246 }, { 1, 40 }, { 16, 0 }, { 1, 11 }, { 1, 55 }, { 1, 39 }, { 49, 0 }, { 1, 93 }, { 9, 255 }, { 1, 110 }, { 5, 0 }, { 1, 11 }, { 1, 140 }, { 1, 216 }, { 1, 227 }, { 1, 181 }, { 1, 69 }, { 5, 0 }, { 1, 20 }, { 1, 59 }, { 1, 31 }, { 50, 0 }, { 1, 183 }, { 8, 255 }, { 1, 189 }, { 1, 1 }, { 4, 0 }, { 1, 52 }, { 1, 218 }, { 5, 255 }, { 1, 96 }, { 3, 0 }, { 1, 31 }, { 1, 60 }, { 1, 22 }, { 50, 0 }, { 1, 22 }, { 1, 251 }, { 7, 255 }, { 1, 240 }, { 1, 30 }, { 3, 0 }, { 1, 1 }, { 1, 117 }, { 1, 250 }, { 6, 255 }, { 1, 246 }, { 1, 45 }, { 1, 2 }, { 1, 42 }, { 1, 58 }, { 1, 14 }, { 51, 0 }, { 1, 109 }, { 8, 255 }, { 1, 94 }, { 3, 0 }, { 1, 25 }, { 1, 187 }, { 9, 255 }, { 1, 217 }, { 1, 57 }, { 1, 51 }, { 1, 8 }, { 52, 0 }, { 1, 199 }, { 7, 255 }, { 1, 174 }, { 3, 0 }, { 1, 76 }, { 1, 235 }, { 11, 255 }, { 1, 180 }, { 53, 0 }, { 1, 35 }, { 7, 255 }, { 1, 233 }, { 1, 21 }, { 1, 0 }, { 1, 8 }, { 1, 147 }, { 14, 255 }, { 1, 107 }, { 52, 0 }, { 1, 125 }, { 7, 255 }, { 1, 99 }, { 1, 0 }, { 1, 43 }, { 1, 210 }, { 15, 255 }, { 1, 249 }, { 1, 53 }, { 51, 0 }, { 1, 215 }, { 7, 255 }, { 1, 159 }, { 1, 112 }, { 1, 246 }, { 17, 255 }, { 1, 224 }, { 1, 17 }, { 49, 0 }, { 1, 50 }, { 29, 255 }, { 1, 179 }, { 1, 1 }, { 48, 0 }, { 1, 141 }, { 30, 255 }, { 1, 119 }, { 47, 0 }, { 1, 2 }, { 1, 229 }, { 30, 255 }, { 1, 252 }, { 1, 45 }, { 46, 0 }, { 1, 67 }, { 32, 255 }, { 1, 139 }, { 46, 0 }, { 1, 157 }, { 32, 255 }, { 1, 108 }, { 45, 0 }, { 1, 7 }, { 1, 240 }, { 31, 255 }, { 1, 212 }, { 1, 21 }, { 45, 0 }, { 1, 58 }, { 29, 255 }, { 1, 244 }, { 1, 208 }, { 1, 127 }, { 1, 17 }, { 46, 0 }, { 1, 57 }, { 19, 255 }, { 1, 238 }, { 1, 213 }, { 1, 188 }, { 1, 163 }, { 1, 138 }, { 1, 113 }, { 1, 88 }, { 1, 63 }, { 1, 38 }, { 1, 13 }, { 51, 0 }, { 1, 191 }, { 7, 255 }, { 1, 253 }, { 1, 232 }, { 1, 207 }, { 1, 182 }, { 1, 157 }, { 1, 132 }, { 1, 107 }, { 1, 82 }, { 1, 57 }, { 1, 32 }, { 1, 8 }, { 61, 0 }, { 1, 6 }, { 1, 103 }, { 1, 139 }, { 1, 126 }, { 1, 101 }, { 1, 76 }, { 1, 51 }, { 1, 26 }, { 1, 3 }, { 1725, 0 } }, \n\tLogoRLEFrame{ { 1718, 0 }, { 1, 88 }, { 1, 155 }, { 1, 71 }, { 76, 0 }, { 1, 47 }, { 1, 252 }, { 2, 255 }, { 1, 85 }, { 75, 0 }, { 1, 140 }, { 3, 255 }, { 1, 230 }, { 1, 9 }, { 74, 0 }, { 1, 225 }, { 4, 255 }, { 1, 115 }, { 57, 0 }, { 1, 14 }, { 15, 0 }, { 1, 55 }, { 5, 255 }, { 1, 234 }, { 1, 12 }, { 55, 0 }, { 1, 1 }, { 1, 35 }, { 15, 0 }, { 1, 141 }, { 6, 255 }, { 1, 121 }, { 55, 0 }, { 1, 28 }, { 1, 29 }, { 14, 0 }, { 1, 1 }, { 1, 226 }, { 6, 255 }, { 1, 236 }, { 1, 7 }, { 53, 0 }, { 1, 1 }, { 1, 57 }, { 1, 14 }, { 14, 0 }, { 1, 56 }, { 8, 255 }, { 1, 84 }, { 53, 0 }, { 1, 21 }, { 1, 54 }, { 15, 0 }, { 1, 142 }, { 8, 255 }, { 1, 181 }, { 53, 0 }, { 1, 47 }, { 1, 32 }, { 14, 0 }, { 1, 1 }, { 1, 227 }, { 8, 255 }, { 1, 252 }, { 1, 25 }, { 51, 0 }, { 1, 9 }, { 1, 62 }, { 1, 9 }, { 14, 0 }, { 1, 57 }, { 10, 255 }, { 1, 118 }, { 51, 0 }, { 1, 29 }, { 1, 46 }, { 15, 0 }, { 1, 143 }, { 10, 255 }, { 1, 170 }, { 51, 0 }, { 1, 46 }, { 1, 13 }, { 14, 0 }, { 1, 1 }, { 1, 227 }, { 10, 255 }, { 1, 185 }, { 21, 0 }, { 1, 4 }, { 1, 17 }, { 27, 0 }, { 1, 2 }, { 1, 40 }, { 15, 0 }, { 1, 58 }, { 11, 255 }, { 1, 167 }, { 20, 0 }, { 1, 16 }, { 1, 33 }, { 28, 0 }, { 1, 17 }, { 1, 8 }, { 15, 0 }, { 1, 144 }, { 11, 255 }, { 1, 84 }, { 18, 0 }, { 1, 1 }, { 1, 34 }, { 1, 40 }, { 29, 0 }, { 1, 9 }, { 15, 0 }, { 1, 1 }, { 1, 228 }, { 10, 255 }, { 1, 219 }, { 1, 8 }, { 17, 0 }, { 1, 8 }, { 1, 51 }, { 1, 40 }, { 46, 0 }, { 1, 59 }, { 10, 255 }, { 1, 253 }, { 1, 60 }, { 17, 0 }, { 1, 16 }, { 1, 58 }, { 1, 35 }, { 47, 0 }, { 1, 145 }, { 10, 255 }, { 1, 141 }, { 17, 0 }, { 1, 27 }, { 1, 61 }, { 1, 26 }, { 47, 0 }, { 1, 1 }, { 1, 229 }, { 9, 255 }, { 1, 217 }, { 1, 9 }, { 15, 0 }, { 1, 1 }, { 1, 38 }, { 1, 59 }, { 1, 18 }, { 48, 0 }, { 1, 60 }, { 9, 255 }, { 1, 253 }, { 1, 57 }, { 15, 0 }, { 1, 2 }, { 1, 47 }, { 1, 55 }, { 1, 11 }, { 49, 0 }, { 1, 146 }, { 9, 255 }, { 1, 137 }, { 6, 0 }, { 1, 42 }, { 1, 108 }, { 1, 118 }, { 1, 73 }, { 1, 3 }, { 4, 0 }, { 1, 2 }, { 1, 47 }, { 1, 44 }, { 1, 4 }, { 49, 0 }, { 1, 2 }, { 1, 229 }, { 8, 255 }, { 1, 214 }, { 1, 7 }, { 4, 0 }, { 1, 4 }, { 1, 129 }, { 1, 251 }, { 3, 255 }, { 1, 215 }, { 1, 35 }, { 2, 0 }, { 1, 3 }, { 1, 46 }, { 1, 27 }, { 51, 0 }, { 1, 61 }, { 8, 255 }, { 1, 252 }, { 1, 53 }, { 4, 0 }, { 1, 33 }, { 1, 198 }, { 6, 255 }, { 1, 211 }, { 1, 12 }, { 1, 3 }, { 1, 34 }, { 1, 10 }, { 52, 0 }, { 1, 147 }, { 8, 255 }, { 1, 133 }, { 4, 0 }, { 1, 90 }, { 1, 241 }, { 8, 255 }, { 1, 172 }, { 1, 10 }, { 53, 0 }, { 1, 2 }, { 1, 230 }, { 7, 255 }, { 1, 211 }, { 1, 6 }, { 2, 0 }, { 1, 13 }, { 1, 162 }, { 11, 255 }, { 1, 121 }, { 53, 0 }, { 1, 62 }, { 7, 255 }, { 1, 251 }, { 1, 50 }, { 2, 0 }, { 1, 54 }, { 1, 220 }, { 12, 255 }, { 1, 253 }, { 1, 73 }, { 52, 0 }, { 1, 148 }, { 7, 255 }, { 1, 133 }, { 1, 0 }, { 1, 2 }, { 1, 121 }, { 1, 251 }, { 14, 255 }, { 1, 239 }, { 1, 36 }, { 50, 0 }, { 1, 2 }, { 1, 231 }, { 7, 255 }, { 1, 100 }, { 1, 28 }, { 1, 191 }, { 17, 255 }, { 1, 212 }, { 1, 12 }, { 49, 0 }, { 1, 63 }, { 8, 255 }, { 1, 249 }, { 1, 245 }, { 19, 255 }, { 1, 172 }, { 1, 1 }, { 48, 0 }, { 1, 149 }, { 30, 255 }, { 1, 122 }, { 47, 0 }, { 1, 2 }, { 1, 232 }, { 30, 255 }, { 1, 253 }, { 1, 68 }, { 46, 0 }, { 1, 64 }, { 32, 255 }, { 1, 205 }, { 46, 0 }, { 1, 150 }, { 32, 255 }, { 1, 229 }, { 45, 0 }, { 1, 3 }, { 1, 232 }, { 32, 255 }, { 1, 150 }, { 45, 0 }, { 1, 58 }, { 31, 255 }, { 1, 248 }, { 1, 154 }, { 1, 5 }, { 45, 0 }, { 1, 77 }, { 23, 255 }, { 1, 249 }, { 1, 224 }, { 1, 199 }, { 1, 174 }, { 1, 149 }, { 1, 124 }, { 1, 99 }, { 1, 72 }, { 1, 13 }, { 47, 0 }, { 1, 18 }, { 1, 241 }, { 12, 255 }, { 1, 244 }, { 1, 219 }, { 1, 194 }, { 1, 169 }, { 1, 144 }, { 1, 118 }, { 1, 93 }, { 1, 68 }, { 1, 43 }, { 1, 18 }, { 57, 0 }, { 1, 63 }, { 1, 213 }, { 1, 250 }, { 1, 239 }, { 1, 214 }, { 1, 188 }, { 1, 163 }, { 1, 138 }, { 1, 113 }, { 1, 88 }, { 1, 63 }, { 1, 38 }, { 1, 13 }, { 490, 0 }, { 1, 1 }, { 1, 20 }, { 1, 4 }, { 75, 0 }, { 1, 1 }, { 1, 24 }, { 1, 45 }, { 1, 8 }, { 75, 0 }, { 1, 15 }, { 1, 53 }, { 1, 46 }, { 1, 4 }, { 74, 0 }, { 1, 4 }, { 1, 39 }, { 1, 60 }, { 1, 26 }, { 75, 0 }, { 1, 17 }, { 1, 57 }, { 1, 47 }, { 1, 8 }, { 75, 0 }, { 1, 28 }, { 1, 50 }, { 1, 21 }, { 75, 0 }, { 1, 1 }, { 1, 24 }, { 1, 17 }, { 77, 0 }, { 1, 2 }, { 678, 0 } }, \n\tLogoRLEFrame{ { 1637, 0 }, { 1, 4 }, { 1, 151 }, { 1, 183 }, { 1, 74 }, { 61, 0 }, { 1, 12 }, { 14, 0 }, { 1, 111 }, { 2, 255 }, { 1, 251 }, { 1, 68 }, { 59, 0 }, { 1, 17 }, { 1, 18 }, { 14, 0 }, { 1, 199 }, { 3, 255 }, { 1, 212 }, { 1, 2 }, { 58, 0 }, { 1, 51 }, { 1, 4 }, { 13, 0 }, { 1, 27 }, { 5, 255 }, { 1, 92 }, { 57, 0 }, { 1, 21 }, { 1, 51 }, { 14, 0 }, { 1, 109 }, { 5, 255 }, { 1, 221 }, { 1, 5 }, { 56, 0 }, { 1, 47 }, { 1, 30 }, { 14, 0 }, { 1, 191 }, { 6, 255 }, { 1, 104 }, { 55, 0 }, { 1, 9 }, { 1, 62 }, { 1, 7 }, { 13, 0 }, { 1, 21 }, { 1, 252 }, { 6, 255 }, { 1, 224 }, { 1, 2 }, { 54, 0 }, { 1, 34 }, { 1, 46 }, { 14, 0 }, { 1, 100 }, { 8, 255 }, { 1, 70 }, { 54, 0 }, { 1, 55 }, { 1, 21 }, { 14, 0 }, { 1, 183 }, { 8, 255 }, { 1, 169 }, { 53, 0 }, { 1, 9 }, { 1, 50 }, { 14, 0 }, { 1, 16 }, { 1, 250 }, { 8, 255 }, { 1, 249 }, { 1, 19 }, { 52, 0 }, { 1, 26 }, { 1, 17 }, { 14, 0 }, { 1, 92 }, { 10, 255 }, { 1, 111 }, { 52, 0 }, { 1, 27 }, { 15, 0 }, { 1, 175 }, { 10, 255 }, { 1, 162 }, { 52, 0 }, { 1, 8 }, { 14, 0 }, { 1, 11 }, { 1, 246 }, { 10, 255 }, { 1, 180 }, { 67, 0 }, { 1, 84 }, { 11, 255 }, { 1, 161 }, { 67, 0 }, { 1, 167 }, { 11, 255 }, { 1, 79 }, { 66, 0 }, { 1, 7 }, { 1, 242 }, { 10, 255 }, { 1, 215 }, { 1, 7 }, { 66, 0 }, { 1, 76 }, { 10, 255 }, { 1, 253 }, { 1, 57 }, { 67, 0 }, { 1, 158 }, { 10, 255 }, { 1, 141 }, { 67, 0 }, { 1, 4 }, { 1, 237 }, { 9, 255 }, { 1, 219 }, { 1, 9 }, { 67, 0 }, { 1, 68 }, { 10, 255 }, { 1, 61 }, { 68, 0 }, { 1, 150 }, { 9, 255 }, { 1, 147 }, { 6, 0 }, { 1, 36 }, { 1, 110 }, { 1, 128 }, { 1, 88 }, { 1, 10 }, { 57, 0 }, { 1, 2 }, { 1, 231 }, { 8, 255 }, { 1, 222 }, { 1, 11 }, { 4, 0 }, { 1, 1 }, { 1, 112 }, { 1, 247 }, { 3, 255 }, { 1, 233 }, { 1, 64 }, { 56, 0 }, { 1, 59 }, { 9, 255 }, { 1, 66 }, { 4, 0 }, { 1, 22 }, { 1, 183 }, { 6, 255 }, { 1, 237 }, { 1, 34 }, { 55, 0 }, { 1, 142 }, { 8, 255 }, { 1, 152 }, { 4, 0 }, { 1, 72 }, { 1, 232 }, { 8, 255 }, { 1, 212 }, { 1, 13 }, { 54, 0 }, { 1, 224 }, { 7, 255 }, { 1, 226 }, { 1, 14 }, { 2, 0 }, { 1, 6 }, { 1, 142 }, { 11, 255 }, { 1, 177 }, { 1, 1 }, { 52, 0 }, { 1, 51 }, { 8, 255 }, { 1, 72 }, { 2, 0 }, { 1, 40 }, { 1, 207 }, { 13, 255 }, { 1, 132 }, { 52, 0 }, { 1, 134 }, { 7, 255 }, { 1, 162 }, { 2, 0 }, { 1, 101 }, { 1, 245 }, { 15, 255 }, { 1, 88 }, { 51, 0 }, { 1, 217 }, { 7, 255 }, { 1, 137 }, { 1, 17 }, { 1, 172 }, { 17, 255 }, { 1, 246 }, { 1, 51 }, { 49, 0 }, { 1, 43 }, { 9, 255 }, { 1, 242 }, { 19, 255 }, { 1, 227 }, { 1, 24 }, { 48, 0 }, { 1, 126 }, { 30, 255 }, { 1, 198 }, { 1, 7 }, { 47, 0 }, { 1, 208 }, { 31, 255 }, { 1, 151 }, { 46, 0 }, { 1, 35 }, { 33, 255 }, { 1, 34 }, { 45, 0 }, { 1, 118 }, { 33, 255 }, { 1, 50 }, { 45, 0 }, { 1, 200 }, { 32, 255 }, { 1, 219 }, { 1, 2 }, { 44, 0 }, { 1, 22 }, { 31, 255 }, { 1, 252 }, { 1, 184 }, { 1, 25 }, { 45, 0 }, { 1, 39 }, { 23, 255 }, { 1, 247 }, { 1, 221 }, { 1, 195 }, { 1, 169 }, { 1, 143 }, { 1, 117 }, { 1, 91 }, { 1, 66 }, { 1, 20 }, { 47, 0 }, { 1, 4 }, { 1, 219 }, { 12, 255 }, { 1, 250 }, { 1, 225 }, { 1, 199 }, { 1, 173 }, { 1, 147 }, { 1, 121 }, { 1, 95 }, { 1, 69 }, { 1, 43 }, { 1, 17 }, { 57, 0 }, { 1, 45 }, { 1, 206 }, { 1, 253 }, { 1, 252 }, { 1, 228 }, { 1, 202 }, { 1, 176 }, { 1, 150 }, { 1, 125 }, { 1, 99 }, { 1, 73 }, { 1, 47 }, { 1, 21 }, { 1, 1 }, { 68, 0 }, { 1, 5 }, { 1, 2 }, { 343, 0 }, { 1, 6 }, { 77, 0 }, { 1, 13 }, { 1, 35 }, { 1, 7 }, { 75, 0 }, { 1, 10 }, { 1, 44 }, { 1, 46 }, { 1, 3 }, { 74, 0 }, { 1, 1 }, { 1, 31 }, { 1, 60 }, { 1, 32 }, { 75, 0 }, { 1, 15 }, { 1, 53 }, { 1, 52 }, { 1, 12 }, { 75, 0 }, { 1, 28 }, { 1, 58 }, { 1, 31 }, { 1, 1 }, { 74, 0 }, { 1, 1 }, { 1, 34 }, { 1, 30 }, { 1, 3 }, { 76, 0 }, { 1, 13 }, { 1, 2 }, { 834, 0 } }, \n\tLogoRLEFrame{ { 1383, 0 }, { 1, 8 }, { 1, 2 }, { 78, 0 }, { 1, 35 }, { 78, 0 }, { 1, 15 }, { 1, 41 }, { 13, 0 }, { 1, 16 }, { 1, 183 }, { 1, 198 }, { 1, 75 }, { 61, 0 }, { 1, 46 }, { 1, 26 }, { 13, 0 }, { 1, 142 }, { 2, 255 }, { 1, 250 }, { 1, 59 }, { 59, 0 }, { 1, 9 }, { 1, 61 }, { 1, 5 }, { 13, 0 }, { 1, 226 }, { 3, 255 }, { 1, 200 }, { 59, 0 }, { 1, 34 }, { 1, 44 }, { 13, 0 }, { 1, 53 }, { 5, 255 }, { 1, 80 }, { 57, 0 }, { 1, 1 }, { 1, 58 }, { 1, 21 }, { 13, 0 }, { 1, 135 }, { 5, 255 }, { 1, 213 }, { 1, 3 }, { 56, 0 }, { 1, 17 }, { 1, 57 }, { 1, 1 }, { 13, 0 }, { 1, 216 }, { 6, 255 }, { 1, 95 }, { 56, 0 }, { 1, 35 }, { 1, 27 }, { 13, 0 }, { 1, 42 }, { 7, 255 }, { 1, 216 }, { 56, 0 }, { 1, 43 }, { 1, 1 }, { 13, 0 }, { 1, 124 }, { 8, 255 }, { 1, 60 }, { 54, 0 }, { 1, 6 }, { 1, 21 }, { 14, 0 }, { 1, 205 }, { 8, 255 }, { 1, 161 }, { 54, 0 }, { 1, 10 }, { 14, 0 }, { 1, 32 }, { 9, 255 }, { 1, 246 }, { 1, 16 }, { 68, 0 }, { 1, 113 }, { 10, 255 }, { 1, 105 }, { 68, 0 }, { 1, 194 }, { 10, 255 }, { 1, 147 }, { 67, 0 }, { 1, 23 }, { 1, 253 }, { 10, 255 }, { 1, 165 }, { 67, 0 }, { 1, 102 }, { 11, 255 }, { 1, 135 }, { 67, 0 }, { 1, 183 }, { 11, 255 }, { 1, 54 }, { 66, 0 }, { 1, 15 }, { 1, 250 }, { 10, 255 }, { 1, 188 }, { 67, 0 }, { 1, 91 }, { 10, 255 }, { 1, 244 }, { 1, 33 }, { 67, 0 }, { 1, 173 }, { 10, 255 }, { 1, 109 }, { 67, 0 }, { 1, 9 }, { 1, 245 }, { 9, 255 }, { 1, 196 }, { 1, 2 }, { 67, 0 }, { 1, 80 }, { 9, 255 }, { 1, 247 }, { 1, 39 }, { 68, 0 }, { 1, 162 }, { 9, 255 }, { 1, 119 }, { 6, 0 }, { 1, 68 }, { 1, 147 }, { 1, 163 }, { 1, 123 }, { 1, 28 }, { 57, 0 }, { 1, 5 }, { 1, 238 }, { 8, 255 }, { 1, 204 }, { 1, 3 }, { 4, 0 }, { 1, 9 }, { 1, 152 }, { 4, 255 }, { 1, 248 }, { 1, 83 }, { 56, 0 }, { 1, 69 }, { 8, 255 }, { 1, 250 }, { 1, 46 }, { 4, 0 }, { 1, 45 }, { 1, 213 }, { 6, 255 }, { 1, 245 }, { 1, 49 }, { 55, 0 }, { 1, 151 }, { 8, 255 }, { 1, 129 }, { 4, 0 }, { 1, 105 }, { 1, 247 }, { 8, 255 }, { 1, 226 }, { 1, 23 }, { 53, 0 }, { 1, 1 }, { 1, 230 }, { 7, 255 }, { 1, 211 }, { 1, 6 }, { 2, 0 }, { 1, 18 }, { 1, 175 }, { 11, 255 }, { 1, 198 }, { 1, 7 }, { 52, 0 }, { 1, 58 }, { 7, 255 }, { 1, 252 }, { 1, 53 }, { 2, 0 }, { 1, 62 }, { 1, 226 }, { 13, 255 }, { 1, 160 }, { 52, 0 }, { 1, 140 }, { 7, 255 }, { 1, 146 }, { 1, 0 }, { 1, 3 }, { 1, 128 }, { 1, 252 }, { 15, 255 }, { 1, 116 }, { 51, 0 }, { 1, 221 }, { 7, 255 }, { 1, 144 }, { 1, 29 }, { 1, 194 }, { 17, 255 }, { 1, 253 }, { 1, 75 }, { 49, 0 }, { 1, 47 }, { 9, 255 }, { 1, 252 }, { 19, 255 }, { 1, 241 }, { 1, 42 }, { 48, 0 }, { 1, 129 }, { 30, 255 }, { 1, 220 }, { 1, 18 }, { 47, 0 }, { 1, 211 }, { 31, 255 }, { 1, 180 }, { 46, 0 }, { 1, 37 }, { 33, 255 }, { 1, 44 }, { 45, 0 }, { 1, 118 }, { 33, 255 }, { 1, 36 }, { 45, 0 }, { 1, 200 }, { 32, 255 }, { 1, 188 }, { 45, 0 }, { 1, 20 }, { 31, 255 }, { 1, 227 }, { 1, 138 }, { 1, 8 }, { 45, 0 }, { 1, 34 }, { 22, 255 }, { 1, 239 }, { 1, 212 }, { 1, 185 }, { 1, 158 }, { 1, 131 }, { 1, 104 }, { 1, 77 }, { 1, 50 }, { 1, 23 }, { 1, 1 }, { 47, 0 }, { 1, 2 }, { 1, 211 }, { 11, 255 }, { 1, 250 }, { 1, 225 }, { 1, 198 }, { 1, 171 }, { 1, 144 }, { 1, 117 }, { 1, 91 }, { 1, 64 }, { 1, 37 }, { 1, 10 }, { 58, 0 }, { 1, 36 }, { 1, 190 }, { 1, 243 }, { 1, 239 }, { 1, 212 }, { 1, 185 }, { 1, 158 }, { 1, 131 }, { 1, 104 }, { 1, 77 }, { 1, 50 }, { 1, 23 }, { 1, 1 }, { 415, 0 }, { 1, 5 }, { 1, 23 }, { 1, 2 }, { 75, 0 }, { 1, 4 }, { 1, 33 }, { 1, 42 }, { 1, 3 }, { 75, 0 }, { 1, 25 }, { 1, 58 }, { 1, 35 }, { 75, 0 }, { 1, 10 }, { 1, 48 }, { 1, 55 }, { 1, 16 }, { 75, 0 }, { 1, 27 }, { 1, 61 }, { 1, 38 }, { 1, 3 }, { 74, 0 }, { 1, 1 }, { 1, 38 }, { 1, 42 }, { 1, 12 }, { 75, 0 }, { 1, 4 }, { 1, 25 }, { 1, 9 }, { 77, 0 }, { 1, 1 }, { 913, 0 } }, \n\tLogoRLEFrame{ { 1478, 0 }, { 1, 38 }, { 1, 100 }, { 1, 33 }, { 76, 0 }, { 1, 17 }, { 1, 233 }, { 1, 255 }, { 1, 246 }, { 1, 62 }, { 75, 0 }, { 1, 102 }, { 3, 255 }, { 1, 223 }, { 1, 6 }, { 74, 0 }, { 1, 184 }, { 4, 255 }, { 1, 107 }, { 73, 0 }, { 1, 16 }, { 1, 250 }, { 4, 255 }, { 1, 231 }, { 1, 10 }, { 72, 0 }, { 1, 94 }, { 6, 255 }, { 1, 121 }, { 72, 0 }, { 1, 176 }, { 6, 255 }, { 1, 239 }, { 1, 11 }, { 70, 0 }, { 1, 12 }, { 1, 247 }, { 7, 255 }, { 1, 96 }, { 70, 0 }, { 1, 86 }, { 8, 255 }, { 1, 196 }, { 70, 0 }, { 1, 169 }, { 9, 255 }, { 1, 40 }, { 68, 0 }, { 1, 8 }, { 1, 243 }, { 9, 255 }, { 1, 138 }, { 68, 0 }, { 1, 78 }, { 10, 255 }, { 1, 206 }, { 68, 0 }, { 1, 161 }, { 10, 255 }, { 1, 223 }, { 67, 0 }, { 1, 5 }, { 1, 238 }, { 10, 255 }, { 1, 218 }, { 67, 0 }, { 1, 70 }, { 11, 255 }, { 1, 142 }, { 67, 0 }, { 1, 153 }, { 10, 255 }, { 1, 251 }, { 1, 46 }, { 66, 0 }, { 1, 2 }, { 1, 233 }, { 10, 255 }, { 1, 134 }, { 67, 0 }, { 1, 62 }, { 10, 255 }, { 1, 215 }, { 1, 7 }, { 67, 0 }, { 1, 145 }, { 9, 255 }, { 1, 253 }, { 1, 57 }, { 67, 0 }, { 1, 1 }, { 1, 226 }, { 9, 255 }, { 1, 142 }, { 68, 0 }, { 1, 54 }, { 9, 255 }, { 1, 221 }, { 1, 10 }, { 5, 0 }, { 1, 10 }, { 1, 76 }, { 1, 103 }, { 1, 70 }, { 1, 7 }, { 58, 0 }, { 1, 137 }, { 9, 255 }, { 1, 65 }, { 5, 0 }, { 1, 55 }, { 1, 218 }, { 3, 255 }, { 1, 228 }, { 1, 70 }, { 57, 0 }, { 1, 219 }, { 8, 255 }, { 1, 151 }, { 4, 0 }, { 1, 1 }, { 1, 118 }, { 1, 251 }, { 5, 255 }, { 1, 241 }, { 1, 41 }, { 55, 0 }, { 1, 46 }, { 8, 255 }, { 1, 226 }, { 1, 13 }, { 3, 0 }, { 1, 22 }, { 1, 184 }, { 8, 255 }, { 1, 219 }, { 1, 17 }, { 54, 0 }, { 1, 129 }, { 8, 255 }, { 1, 72 }, { 3, 0 }, { 1, 68 }, { 1, 231 }, { 10, 255 }, { 1, 186 }, { 1, 3 }, { 46, 0 }, { 1, 5 }, { 6, 0 }, { 1, 211 }, { 7, 255 }, { 1, 160 }, { 2, 0 }, { 1, 4 }, { 1, 133 }, { 1, 253 }, { 12, 255 }, { 1, 142 }, { 45, 0 }, { 1, 1 }, { 1, 7 }, { 5, 0 }, { 1, 38 }, { 7, 255 }, { 1, 231 }, { 1, 17 }, { 1, 0 }, { 1, 30 }, { 1, 197 }, { 15, 255 }, { 1, 96 }, { 44, 0 }, { 1, 12 }, { 1, 1 }, { 5, 0 }, { 1, 121 }, { 7, 255 }, { 1, 165 }, { 1, 0 }, { 1, 82 }, { 1, 238 }, { 16, 255 }, { 1, 248 }, { 1, 57 }, { 42, 0 }, { 1, 4 }, { 1, 14 }, { 6, 0 }, { 1, 204 }, { 7, 255 }, { 1, 250 }, { 1, 187 }, { 19, 255 }, { 1, 231 }, { 1, 28 }, { 41, 0 }, { 1, 16 }, { 1, 7 }, { 5, 0 }, { 1, 31 }, { 30, 255 }, { 1, 203 }, { 1, 9 }, { 39, 0 }, { 1, 6 }, { 1, 18 }, { 6, 0 }, { 1, 113 }, { 31, 255 }, { 1, 159 }, { 39, 0 }, { 1, 17 }, { 1, 9 }, { 6, 0 }, { 1, 196 }, { 32, 255 }, { 1, 44 }, { 37, 0 }, { 1, 7 }, { 1, 19 }, { 6, 0 }, { 1, 24 }, { 1, 253 }, { 32, 255 }, { 1, 66 }, { 37, 0 }, { 1, 17 }, { 1, 10 }, { 6, 0 }, { 1, 105 }, { 32, 255 }, { 1, 232 }, { 1, 9 }, { 36, 0 }, { 1, 7 }, { 1, 19 }, { 1, 1 }, { 6, 0 }, { 1, 187 }, { 31, 255 }, { 1, 203 }, { 1, 37 }, { 37, 0 }, { 1, 16 }, { 1, 10 }, { 7, 0 }, { 1, 226 }, { 23, 255 }, { 1, 251 }, { 1, 224 }, { 1, 196 }, { 1, 168 }, { 1, 140 }, { 1, 112 }, { 1, 84 }, { 1, 36 }, { 38, 0 }, { 1, 4 }, { 1, 18 }, { 8, 0 }, { 1, 194 }, { 14, 255 }, { 1, 248 }, { 1, 220 }, { 1, 192 }, { 1, 164 }, { 1, 136 }, { 1, 108 }, { 1, 80 }, { 1, 52 }, { 1, 24 }, { 1, 2 }, { 45, 0 }, { 1, 12 }, { 1, 5 }, { 8, 0 }, { 1, 50 }, { 1, 230 }, { 4, 255 }, { 1, 245 }, { 1, 216 }, { 1, 188 }, { 1, 160 }, { 1, 132 }, { 1, 104 }, { 1, 76 }, { 1, 48 }, { 1, 20 }, { 55, 0 }, { 1, 12 }, { 10, 0 }, { 1, 15 }, { 1, 64 }, { 1, 72 }, { 1, 44 }, { 1, 16 }, { 63, 0 }, { 1, 6 }, { 1, 2 }, { 78, 0 }, { 1, 3 }, { 122, 0 }, { 1, 8 }, { 77, 0 }, { 1, 22 }, { 1, 35 }, { 1, 3 }, { 75, 0 }, { 1, 17 }, { 1, 52 }, { 1, 36 }, { 75, 0 }, { 1, 5 }, { 1, 41 }, { 1, 58 }, { 1, 22 }, { 75, 0 }, { 1, 24 }, { 1, 58 }, { 1, 43 }, { 1, 6 }, { 74, 0 }, { 1, 1 }, { 1, 38 }, { 1, 53 }, { 1, 22 }, { 75, 0 }, { 1, 5 }, { 1, 36 }, { 1, 21 }, { 77, 0 }, { 1, 11 }, { 1070, 0 } }, \n\tLogoRLEFrame{ { 1479, 0 }, { 1, 41 }, { 1, 68 }, { 1, 5 }, { 76, 0 }, { 1, 52 }, { 1, 251 }, { 1, 255 }, { 1, 199 }, { 1, 11 }, { 75, 0 }, { 1, 164 }, { 3, 255 }, { 1, 137 }, { 74, 0 }, { 1, 8 }, { 1, 242 }, { 3, 255 }, { 1, 245 }, { 1, 24 }, { 73, 0 }, { 1, 80 }, { 5, 255 }, { 1, 145 }, { 73, 0 }, { 1, 166 }, { 5, 255 }, { 1, 248 }, { 1, 29 }, { 71, 0 }, { 1, 8 }, { 1, 243 }, { 6, 255 }, { 1, 147 }, { 71, 0 }, { 1, 81 }, { 7, 255 }, { 1, 239 }, { 1, 8 }, { 70, 0 }, { 1, 167 }, { 8, 255 }, { 1, 87 }, { 69, 0 }, { 1, 9 }, { 1, 244 }, { 8, 255 }, { 1, 184 }, { 69, 0 }, { 1, 83 }, { 9, 255 }, { 1, 253 }, { 1, 28 }, { 68, 0 }, { 1, 169 }, { 10, 255 }, { 1, 82 }, { 67, 0 }, { 1, 10 }, { 1, 244 }, { 10, 255 }, { 1, 95 }, { 67, 0 }, { 1, 84 }, { 11, 255 }, { 1, 77 }, { 67, 0 }, { 1, 170 }, { 10, 255 }, { 1, 240 }, { 1, 7 }, { 66, 0 }, { 1, 11 }, { 1, 245 }, { 10, 255 }, { 1, 133 }, { 67, 0 }, { 1, 86 }, { 10, 255 }, { 1, 212 }, { 1, 7 }, { 67, 0 }, { 1, 172 }, { 9, 255 }, { 1, 252 }, { 1, 53 }, { 67, 0 }, { 1, 12 }, { 1, 246 }, { 9, 255 }, { 1, 135 }, { 62, 0 }, { 1, 2 }, { 5, 0 }, { 1, 88 }, { 9, 255 }, { 1, 214 }, { 1, 7 }, { 61, 0 }, { 1, 5 }, { 1, 9 }, { 5, 0 }, { 1, 173 }, { 8, 255 }, { 1, 252 }, { 1, 55 }, { 5, 0 }, { 1, 11 }, { 1, 117 }, { 1, 162 }, { 1, 144 }, { 1, 77 }, { 52, 0 }, { 1, 29 }, { 5, 0 }, { 1, 12 }, { 1, 247 }, { 8, 255 }, { 1, 137 }, { 5, 0 }, { 1, 54 }, { 1, 221 }, { 4, 255 }, { 1, 182 }, { 1, 3 }, { 49, 0 }, { 1, 13 }, { 1, 29 }, { 5, 0 }, { 1, 89 }, { 8, 255 }, { 1, 215 }, { 1, 8 }, { 4, 0 }, { 1, 113 }, { 1, 250 }, { 6, 255 }, { 1, 137 }, { 49, 0 }, { 1, 49 }, { 1, 7 }, { 5, 0 }, { 1, 175 }, { 7, 255 }, { 1, 253 }, { 1, 56 }, { 3, 0 }, { 1, 18 }, { 1, 178 }, { 9, 255 }, { 1, 84 }, { 47, 0 }, { 1, 24 }, { 1, 46 }, { 5, 0 }, { 1, 13 }, { 1, 247 }, { 7, 255 }, { 1, 139 }, { 3, 0 }, { 1, 58 }, { 1, 225 }, { 10, 255 }, { 1, 243 }, { 1, 42 }, { 45, 0 }, { 1, 1 }, { 1, 56 }, { 1, 19 }, { 5, 0 }, { 1, 91 }, { 7, 255 }, { 1, 216 }, { 1, 8 }, { 1, 0 }, { 1, 1 }, { 1, 118 }, { 1, 251 }, { 12, 255 }, { 1, 217 }, { 1, 14 }, { 44, 0 }, { 1, 26 }, { 1, 52 }, { 6, 0 }, { 1, 177 }, { 6, 255 }, { 1, 253 }, { 1, 57 }, { 1, 0 }, { 1, 20 }, { 1, 182 }, { 15, 255 }, { 1, 176 }, { 1, 1 }, { 42, 0 }, { 1, 1 }, { 1, 56 }, { 1, 23 }, { 5, 0 }, { 1, 14 }, { 1, 248 }, { 6, 255 }, { 1, 243 }, { 1, 8 }, { 1, 62 }, { 1, 228 }, { 17, 255 }, { 1, 123 }, { 42, 0 }, { 1, 27 }, { 1, 55 }, { 6, 0 }, { 1, 92 }, { 8, 255 }, { 1, 222 }, { 1, 252 }, { 18, 255 }, { 1, 253 }, { 1, 72 }, { 40, 0 }, { 1, 2 }, { 1, 56 }, { 1, 26 }, { 6, 0 }, { 1, 178 }, { 29, 255 }, { 1, 238 }, { 1, 34 }, { 39, 0 }, { 1, 25 }, { 1, 55 }, { 1, 1 }, { 5, 0 }, { 1, 15 }, { 1, 249 }, { 30, 255 }, { 1, 203 }, { 1, 2 }, { 38, 0 }, { 1, 49 }, { 1, 19 }, { 6, 0 }, { 1, 94 }, { 32, 255 }, { 1, 64 }, { 37, 0 }, { 1, 11 }, { 1, 43 }, { 7, 0 }, { 1, 180 }, { 32, 255 }, { 1, 49 }, { 37, 0 }, { 1, 33 }, { 1, 7 }, { 6, 0 }, { 1, 16 }, { 1, 249 }, { 31, 255 }, { 1, 193 }, { 1, 1 }, { 36, 0 }, { 1, 1 }, { 1, 24 }, { 7, 0 }, { 1, 94 }, { 30, 255 }, { 1, 226 }, { 1, 137 }, { 1, 9 }, { 37, 0 }, { 1, 10 }, { 8, 0 }, { 1, 129 }, { 21, 255 }, { 1, 251 }, { 1, 225 }, { 1, 196 }, { 1, 167 }, { 1, 138 }, { 1, 109 }, { 1, 80 }, { 1, 51 }, { 1, 22 }, { 49, 0 }, { 1, 87 }, { 12, 255 }, { 1, 253 }, { 1, 230 }, { 1, 201 }, { 1, 172 }, { 1, 143 }, { 1, 114 }, { 1, 85 }, { 1, 56 }, { 1, 27 }, { 1, 2 }, { 57, 0 }, { 1, 1 }, { 1, 151 }, { 3, 255 }, { 1, 235 }, { 1, 206 }, { 1, 177 }, { 1, 148 }, { 1, 119 }, { 1, 90 }, { 1, 61 }, { 1, 32 }, { 1, 5 }, { 68, 0 }, { 1, 14 }, { 1, 32 }, { 1, 9 }, { 188, 0 }, { 1, 11 }, { 1, 23 }, { 1, 1 }, { 75, 0 }, { 1, 10 }, { 1, 42 }, { 1, 35 }, { 75, 0 }, { 1, 2 }, { 1, 34 }, { 1, 59 }, { 1, 25 }, { 75, 0 }, { 1, 18 }, { 1, 55 }, { 1, 48 }, { 1, 9 }, { 74, 0 }, { 1, 1 }, { 1, 37 }, { 1, 60 }, { 1, 28 }, { 75, 0 }, { 1, 5 }, { 1, 44 }, { 1, 33 }, { 1, 5 }, { 75, 0 }, { 1, 7 }, { 1, 22 }, { 1, 3 }, { 1226, 0 } }, \n\tLogoRLEFrame{ { 1479, 0 }, { 1, 67 }, { 1, 135 }, { 1, 53 }, { 76, 0 }, { 1, 37 }, { 1, 247 }, { 1, 255 }, { 1, 248 }, { 1, 59 }, { 75, 0 }, { 1, 135 }, { 3, 255 }, { 1, 204 }, { 74, 0 }, { 1, 1 }, { 1, 224 }, { 4, 255 }, { 1, 75 }, { 73, 0 }, { 1, 61 }, { 5, 255 }, { 1, 203 }, { 73, 0 }, { 1, 152 }, { 6, 255 }, { 1, 74 }, { 71, 0 }, { 1, 6 }, { 1, 237 }, { 6, 255 }, { 1, 184 }, { 71, 0 }, { 1, 79 }, { 7, 255 }, { 1, 252 }, { 1, 23 }, { 70, 0 }, { 1, 170 }, { 8, 255 }, { 1, 110 }, { 69, 0 }, { 1, 14 }, { 1, 247 }, { 8, 255 }, { 1, 202 }, { 69, 0 }, { 1, 96 }, { 10, 255 }, { 1, 27 }, { 68, 0 }, { 1, 187 }, { 10, 255 }, { 1, 41 }, { 67, 0 }, { 1, 25 }, { 1, 253 }, { 10, 255 }, { 1, 40 }, { 67, 0 }, { 1, 114 }, { 10, 255 }, { 1, 221 }, { 62, 0 }, { 1, 2 }, { 5, 0 }, { 1, 205 }, { 10, 255 }, { 1, 121 }, { 62, 0 }, { 1, 15 }, { 4, 0 }, { 1, 40 }, { 10, 255 }, { 1, 210 }, { 1, 6 }, { 61, 0 }, { 1, 13 }, { 1, 15 }, { 4, 0 }, { 1, 131 }, { 9, 255 }, { 1, 250 }, { 1, 48 }, { 62, 0 }, { 1, 42 }, { 5, 0 }, { 1, 221 }, { 9, 255 }, { 1, 125 }, { 62, 0 }, { 1, 25 }, { 1, 32 }, { 4, 0 }, { 1, 57 }, { 9, 255 }, { 1, 203 }, { 1, 4 }, { 61, 0 }, { 1, 4 }, { 1, 57 }, { 1, 9 }, { 4, 0 }, { 1, 148 }, { 8, 255 }, { 1, 248 }, { 1, 42 }, { 5, 0 }, { 1, 18 }, { 1, 85 }, { 1, 104 }, { 1, 63 }, { 1, 1 }, { 52, 0 }, { 1, 32 }, { 1, 45 }, { 4, 0 }, { 1, 4 }, { 1, 235 }, { 8, 255 }, { 1, 116 }, { 5, 0 }, { 1, 76 }, { 1, 232 }, { 3, 255 }, { 1, 208 }, { 1, 27 }, { 50, 0 }, { 1, 4 }, { 1, 59 }, { 1, 15 }, { 4, 0 }, { 1, 75 }, { 8, 255 }, { 1, 196 }, { 1, 2 }, { 3, 0 }, { 1, 4 }, { 1, 137 }, { 6, 255 }, { 1, 194 }, { 1, 3 }, { 49, 0 }, { 1, 32 }, { 1, 48 }, { 5, 0 }, { 1, 166 }, { 7, 255 }, { 1, 245 }, { 1, 36 }, { 3, 0 }, { 1, 26 }, { 1, 194 }, { 8, 255 }, { 1, 131 }, { 48, 0 }, { 1, 4 }, { 1, 59 }, { 1, 18 }, { 4, 0 }, { 1, 12 }, { 1, 245 }, { 7, 255 }, { 1, 107 }, { 3, 0 }, { 1, 69 }, { 1, 234 }, { 9, 255 }, { 1, 253 }, { 1, 68 }, { 47, 0 }, { 1, 33 }, { 1, 51 }, { 5, 0 }, { 1, 92 }, { 7, 255 }, { 1, 188 }, { 1, 1 }, { 1, 0 }, { 1, 2 }, { 1, 128 }, { 1, 253 }, { 11, 255 }, { 1, 232 }, { 1, 24 }, { 45, 0 }, { 1, 2 }, { 1, 59 }, { 1, 20 }, { 5, 0 }, { 1, 183 }, { 6, 255 }, { 1, 241 }, { 1, 30 }, { 1, 0 }, { 1, 22 }, { 1, 187 }, { 14, 255 }, { 1, 188 }, { 1, 2 }, { 44, 0 }, { 1, 24 }, { 1, 44 }, { 5, 0 }, { 1, 22 }, { 1, 252 }, { 6, 255 }, { 1, 130 }, { 1, 0 }, { 1, 61 }, { 1, 229 }, { 16, 255 }, { 1, 124 }, { 44, 0 }, { 1, 46 }, { 1, 8 }, { 5, 0 }, { 1, 110 }, { 7, 255 }, { 1, 207 }, { 1, 134 }, { 1, 252 }, { 17, 255 }, { 1, 252 }, { 1, 63 }, { 42, 0 }, { 1, 10 }, { 1, 30 }, { 6, 0 }, { 1, 201 }, { 28, 255 }, { 1, 229 }, { 1, 21 }, { 41, 0 }, { 1, 24 }, { 1, 1 }, { 5, 0 }, { 1, 36 }, { 30, 255 }, { 1, 180 }, { 40, 0 }, { 1, 1 }, { 1, 10 }, { 6, 0 }, { 1, 127 }, { 31, 255 }, { 1, 56 }, { 47, 0 }, { 1, 218 }, { 31, 255 }, { 1, 69 }, { 46, 0 }, { 1, 54 }, { 31, 255 }, { 1, 226 }, { 1, 7 }, { 46, 0 }, { 1, 145 }, { 30, 255 }, { 1, 190 }, { 1, 29 }, { 47, 0 }, { 1, 216 }, { 23, 255 }, { 1, 232 }, { 1, 202 }, { 1, 171 }, { 1, 140 }, { 1, 109 }, { 1, 79 }, { 1, 28 }, { 49, 0 }, { 1, 220 }, { 14, 255 }, { 1, 251 }, { 1, 223 }, { 1, 192 }, { 1, 161 }, { 1, 130 }, { 1, 99 }, { 1, 69 }, { 1, 38 }, { 1, 8 }, { 56, 0 }, { 1, 104 }, { 6, 255 }, { 1, 244 }, { 1, 213 }, { 1, 182 }, { 1, 151 }, { 1, 120 }, { 1, 90 }, { 1, 59 }, { 1, 28 }, { 1, 2 }, { 65, 0 }, { 1, 61 }, { 1, 116 }, { 1, 111 }, { 1, 80 }, { 1, 49 }, { 1, 18 }, { 109, 0 }, { 1, 3 }, { 1, 9 }, { 76, 0 }, { 1, 3 }, { 2, 31 }, { 76, 0 }, { 1, 26 }, { 1, 57 }, { 1, 26 }, { 75, 0 }, { 1, 12 }, { 1, 49 }, { 1, 53 }, { 1, 14 }, { 74, 0 }, { 1, 1 }, { 1, 34 }, { 1, 61 }, { 1, 34 }, { 1, 1 }, { 74, 0 }, { 1, 5 }, { 1, 47 }, { 1, 46 }, { 1, 14 }, { 75, 0 }, { 1, 11 }, { 1, 35 }, { 1, 13 }, { 76, 0 }, { 1, 1 }, { 1, 7 }, { 1305, 0 } }, \n\tLogoRLEFrame{ { 1400, 0 }, { 1, 1 }, { 77, 0 }, { 1, 1 }, { 1, 167 }, { 1, 246 }, { 1, 150 }, { 1, 1 }, { 75, 0 }, { 1, 82 }, { 3, 255 }, { 1, 102 }, { 75, 0 }, { 1, 182 }, { 3, 255 }, { 1, 222 }, { 1, 3 }, { 73, 0 }, { 1, 27 }, { 1, 252 }, { 4, 255 }, { 1, 89 }, { 73, 0 }, { 1, 123 }, { 5, 255 }, { 1, 209 }, { 72, 0 }, { 1, 1 }, { 1, 221 }, { 6, 255 }, { 1, 65 }, { 71, 0 }, { 1, 65 }, { 7, 255 }, { 1, 151 }, { 71, 0 }, { 1, 164 }, { 7, 255 }, { 1, 233 }, { 1, 3 }, { 69, 0 }, { 1, 16 }, { 1, 247 }, { 8, 255 }, { 1, 65 }, { 65, 0 }, { 1, 2 }, { 3, 0 }, { 1, 106 }, { 9, 255 }, { 1, 142 }, { 64, 0 }, { 1, 11 }, { 1, 3 }, { 3, 0 }, { 1, 205 }, { 9, 255 }, { 1, 152 }, { 64, 0 }, { 1, 28 }, { 3, 0 }, { 1, 48 }, { 10, 255 }, { 1, 143 }, { 63, 0 }, { 1, 25 }, { 1, 18 }, { 3, 0 }, { 1, 147 }, { 10, 255 }, { 1, 61 }, { 62, 0 }, { 1, 4 }, { 1, 52 }, { 3, 0 }, { 1, 7 }, { 1, 238 }, { 9, 255 }, { 1, 206 }, { 63, 0 }, { 1, 36 }, { 1, 35 }, { 3, 0 }, { 1, 89 }, { 9, 255 }, { 1, 249 }, { 1, 46 }, { 62, 0 }, { 1, 8 }, { 1, 61 }, { 1, 9 }, { 3, 0 }, { 1, 188 }, { 9, 255 }, { 1, 116 }, { 63, 0 }, { 1, 38 }, { 1, 41 }, { 3, 0 }, { 1, 32 }, { 9, 255 }, { 1, 192 }, { 1, 2 }, { 62, 0 }, { 1, 8 }, { 1, 61 }, { 1, 11 }, { 3, 0 }, { 1, 129 }, { 8, 255 }, { 1, 240 }, { 1, 30 }, { 63, 0 }, { 1, 39 }, { 1, 44 }, { 3, 0 }, { 1, 2 }, { 1, 226 }, { 8, 255 }, { 1, 92 }, { 5, 0 }, { 1, 48 }, { 1, 144 }, { 1, 166 }, { 1, 125 }, { 1, 23 }, { 53, 0 }, { 1, 8 }, { 1, 61 }, { 1, 15 }, { 3, 0 }, { 1, 71 }, { 8, 255 }, { 1, 169 }, { 4, 0 }, { 1, 1 }, { 1, 119 }, { 1, 251 }, { 3, 255 }, { 1, 241 }, { 1, 38 }, { 52, 0 }, { 1, 36 }, { 1, 46 }, { 4, 0 }, { 1, 170 }, { 7, 255 }, { 1, 228 }, { 1, 17 }, { 3, 0 }, { 1, 15 }, { 1, 176 }, { 6, 255 }, { 1, 200 }, { 1, 3 }, { 50, 0 }, { 1, 2 }, { 1, 58 }, { 1, 11 }, { 3, 0 }, { 1, 20 }, { 1, 249 }, { 7, 255 }, { 1, 68 }, { 3, 0 }, { 1, 45 }, { 1, 219 }, { 8, 255 }, { 1, 121 }, { 50, 0 }, { 1, 23 }, { 1, 37 }, { 4, 0 }, { 1, 112 }, { 7, 255 }, { 1, 144 }, { 3, 0 }, { 1, 92 }, { 1, 245 }, { 9, 255 }, { 1, 249 }, { 1, 47 }, { 49, 0 }, { 1, 43 }, { 1, 5 }, { 4, 0 }, { 1, 211 }, { 6, 255 }, { 1, 213 }, { 1, 8 }, { 1, 0 }, { 1, 6 }, { 1, 149 }, { 12, 255 }, { 1, 209 }, { 1, 6 }, { 47, 0 }, { 1, 9 }, { 1, 21 }, { 4, 0 }, { 1, 54 }, { 6, 255 }, { 1, 250 }, { 1, 49 }, { 1, 0 }, { 1, 29 }, { 1, 200 }, { 14, 255 }, { 1, 133 }, { 47, 0 }, { 1, 14 }, { 5, 0 }, { 1, 153 }, { 6, 255 }, { 1, 144 }, { 1, 0 }, { 1, 68 }, { 1, 235 }, { 15, 255 }, { 1, 252 }, { 1, 56 }, { 51, 0 }, { 1, 10 }, { 1, 241 }, { 6, 255 }, { 1, 204 }, { 1, 136 }, { 1, 253 }, { 17, 255 }, { 1, 217 }, { 1, 9 }, { 50, 0 }, { 1, 95 }, { 28, 255 }, { 1, 145 }, { 50, 0 }, { 1, 194 }, { 29, 255 }, { 1, 57 }, { 48, 0 }, { 1, 37 }, { 30, 255 }, { 1, 156 }, { 48, 0 }, { 1, 136 }, { 30, 255 }, { 1, 124 }, { 47, 0 }, { 1, 3 }, { 1, 230 }, { 29, 255 }, { 1, 221 }, { 1, 27 }, { 47, 0 }, { 1, 77 }, { 28, 255 }, { 1, 231 }, { 1, 143 }, { 1, 24 }, { 48, 0 }, { 1, 161 }, { 20, 255 }, { 1, 251 }, { 1, 220 }, { 1, 186 }, { 1, 153 }, { 1, 119 }, { 1, 85 }, { 1, 51 }, { 1, 18 }, { 51, 0 }, { 1, 180 }, { 13, 255 }, { 1, 234 }, { 1, 200 }, { 1, 167 }, { 1, 133 }, { 1, 99 }, { 1, 65 }, { 1, 32 }, { 1, 3 }, { 58, 0 }, { 1, 79 }, { 1, 252 }, { 4, 255 }, { 1, 247 }, { 1, 214 }, { 1, 181 }, { 1, 147 }, { 1, 113 }, { 1, 79 }, { 1, 46 }, { 1, 13 }, { 67, 0 }, { 1, 48 }, { 1, 102 }, { 1, 93 }, { 1, 59 }, { 1, 26 }, { 1, 1 }, { 112, 0 }, { 1, 19 }, { 1, 22 }, { 76, 0 }, { 1, 18 }, { 1, 49 }, { 1, 25 }, { 75, 0 }, { 1, 7 }, { 1, 43 }, { 1, 56 }, { 1, 16 }, { 75, 0 }, { 1, 27 }, { 1, 59 }, { 1, 40 }, { 1, 4 }, { 74, 0 }, { 1, 4 }, { 1, 47 }, { 1, 56 }, { 1, 19 }, { 56, 0 }, { 1, 1 }, { 1, 7 }, { 1, 1 }, { 16, 0 }, { 1, 11 }, { 1, 46 }, { 1, 25 }, { 1, 1 }, { 56, 0 }, { 1, 8 }, { 1, 12 }, { 17, 0 }, { 1, 9 }, { 1, 19 }, { 57, 0 }, { 1, 5 }, { 1, 17 }, { 1, 11 }, { 76, 0 }, { 1, 11 }, { 1, 19 }, { 1, 7 }, { 75, 0 }, { 1, 4 }, { 1, 17 }, { 1, 16 }, { 1, 3 }, { 75, 0 }, { 1, 10 }, { 1, 20 }, { 1, 11 }, { 75, 0 }, { 1, 2 }, { 1, 16 }, { 1, 18 }, { 1, 5 }, { 75, 0 }, { 1, 4 }, { 1, 18 }, { 1, 10 }, { 1, 1 }, { 75, 0 }, { 1, 7 }, { 1, 12 }, { 1, 2 }, { 76, 0 }, { 1, 5 }, { 1, 4 }, { 852, 0 } }, \n\tLogoRLEFrame{ { 1479, 0 }, { 1, 2 }, { 1, 131 }, { 1, 159 }, { 1, 33 }, { 76, 0 }, { 1, 115 }, { 2, 255 }, { 1, 209 }, { 1, 1 }, { 74, 0 }, { 1, 4 }, { 1, 227 }, { 3, 255 }, { 1, 75 }, { 74, 0 }, { 1, 84 }, { 4, 255 }, { 1, 186 }, { 69, 0 }, { 1, 2 }, { 4, 0 }, { 1, 193 }, { 5, 255 }, { 1, 43 }, { 68, 0 }, { 1, 14 }, { 3, 0 }, { 1, 46 }, { 6, 255 }, { 1, 143 }, { 67, 0 }, { 1, 23 }, { 1, 5 }, { 3, 0 }, { 1, 155 }, { 6, 255 }, { 1, 219 }, { 66, 0 }, { 1, 3 }, { 1, 39 }, { 3, 0 }, { 1, 18 }, { 1, 246 }, { 7, 255 }, { 1, 38 }, { 65, 0 }, { 1, 36 }, { 1, 21 }, { 3, 0 }, { 1, 118 }, { 8, 255 }, { 1, 113 }, { 64, 0 }, { 1, 11 }, { 1, 58 }, { 1, 2 }, { 2, 0 }, { 1, 2 }, { 1, 224 }, { 8, 255 }, { 1, 161 }, { 64, 0 }, { 1, 44 }, { 1, 34 }, { 3, 0 }, { 1, 80 }, { 9, 255 }, { 1, 146 }, { 63, 0 }, { 1, 12 }, { 1, 61 }, { 1, 6 }, { 3, 0 }, { 1, 189 }, { 9, 255 }, { 1, 100 }, { 63, 0 }, { 1, 44 }, { 1, 37 }, { 3, 0 }, { 1, 42 }, { 9, 255 }, { 1, 234 }, { 1, 8 }, { 62, 0 }, { 1, 13 }, { 1, 61 }, { 1, 8 }, { 3, 0 }, { 1, 151 }, { 9, 255 }, { 1, 100 }, { 63, 0 }, { 1, 45 }, { 1, 40 }, { 3, 0 }, { 1, 15 }, { 1, 244 }, { 8, 255 }, { 1, 170 }, { 63, 0 }, { 1, 11 }, { 1, 62 }, { 1, 10 }, { 3, 0 }, { 1, 113 }, { 8, 255 }, { 1, 225 }, { 1, 16 }, { 63, 0 }, { 2, 36 }, { 3, 0 }, { 1, 1 }, { 1, 220 }, { 7, 255 }, { 1, 252 }, { 1, 59 }, { 63, 0 }, { 1, 1 }, { 1, 54 }, { 1, 4 }, { 3, 0 }, { 1, 75 }, { 8, 255 }, { 1, 127 }, { 5, 0 }, { 1, 54 }, { 1, 111 }, { 1, 96 }, { 1, 26 }, { 55, 0 }, { 1, 22 }, { 1, 26 }, { 4, 0 }, { 1, 184 }, { 7, 255 }, { 1, 194 }, { 1, 3 }, { 3, 0 }, { 1, 7 }, { 1, 153 }, { 3, 255 }, { 1, 244 }, { 1, 73 }, { 54, 0 }, { 1, 31 }, { 4, 0 }, { 1, 38 }, { 7, 255 }, { 1, 238 }, { 1, 29 }, { 3, 0 }, { 1, 27 }, { 1, 201 }, { 5, 255 }, { 1, 224 }, { 1, 10 }, { 52, 0 }, { 1, 5 }, { 1, 8 }, { 4, 0 }, { 1, 146 }, { 7, 255 }, { 1, 83 }, { 3, 0 }, { 1, 59 }, { 1, 231 }, { 7, 255 }, { 1, 131 }, { 57, 0 }, { 1, 13 }, { 1, 242 }, { 6, 255 }, { 1, 154 }, { 3, 0 }, { 1, 103 }, { 1, 250 }, { 8, 255 }, { 1, 248 }, { 1, 37 }, { 56, 0 }, { 1, 108 }, { 6, 255 }, { 1, 214 }, { 1, 10 }, { 1, 0 }, { 1, 6 }, { 1, 153 }, { 11, 255 }, { 1, 184 }, { 55, 0 }, { 1, 1 }, { 1, 216 }, { 5, 255 }, { 1, 248 }, { 1, 47 }, { 1, 0 }, { 1, 24 }, { 1, 197 }, { 13, 255 }, { 1, 80 }, { 54, 0 }, { 1, 70 }, { 6, 255 }, { 1, 110 }, { 1, 0 }, { 1, 55 }, { 1, 229 }, { 14, 255 }, { 1, 224 }, { 1, 10 }, { 53, 0 }, { 1, 179 }, { 5, 255 }, { 1, 250 }, { 1, 6 }, { 1, 99 }, { 1, 248 }, { 16, 255 }, { 1, 131 }, { 52, 0 }, { 1, 34 }, { 1, 253 }, { 6, 255 }, { 1, 228 }, { 18, 255 }, { 1, 248 }, { 1, 37 }, { 51, 0 }, { 1, 141 }, { 27, 255 }, { 1, 181 }, { 50, 0 }, { 1, 11 }, { 1, 239 }, { 28, 255 }, { 1, 33 }, { 49, 0 }, { 1, 103 }, { 28, 255 }, { 1, 249 }, { 1, 18 }, { 49, 0 }, { 1, 212 }, { 28, 255 }, { 1, 142 }, { 49, 0 }, { 1, 66 }, { 27, 255 }, { 1, 221 }, { 1, 110 }, { 50, 0 }, { 1, 175 }, { 21, 255 }, { 1, 231 }, { 1, 192 }, { 1, 154 }, { 1, 116 }, { 1, 77 }, { 1, 39 }, { 1, 1 }, { 50, 0 }, { 1, 2 }, { 1, 249 }, { 14, 255 }, { 1, 243 }, { 1, 205 }, { 1, 167 }, { 1, 128 }, { 1, 90 }, { 1, 51 }, { 1, 14 }, { 57, 0 }, { 1, 7 }, { 1, 241 }, { 7, 255 }, { 1, 251 }, { 1, 218 }, { 1, 179 }, { 1, 141 }, { 1, 102 }, { 1, 64 }, { 1, 26 }, { 65, 0 }, { 1, 83 }, { 1, 215 }, { 1, 225 }, { 1, 192 }, { 1, 153 }, { 1, 115 }, { 1, 77 }, { 1, 38 }, { 1, 5 }, { 33, 0 }, { 1, 4 }, { 1, 3 }, { 76, 0 }, { 1, 4 }, { 1, 18 }, { 1, 11 }, { 75, 0 }, { 1, 1 }, { 1, 17 }, { 1, 28 }, { 1, 8 }, { 56, 0 }, { 1, 11 }, { 1, 1 }, { 17, 0 }, { 1, 9 }, { 1, 27 }, { 1, 22 }, { 1, 3 }, { 55, 0 }, { 1, 10 }, { 1, 31 }, { 1, 5 }, { 16, 0 }, { 1, 2 }, { 1, 21 }, { 1, 29 }, { 1, 12 }, { 55, 0 }, { 1, 3 }, { 1, 35 }, { 1, 41 }, { 1, 2 }, { 16, 0 }, { 1, 5 }, { 1, 25 }, { 1, 19 }, { 1, 3 }, { 55, 0 }, { 1, 20 }, { 1, 57 }, { 1, 34 }, { 17, 0 }, { 1, 8 }, { 1, 15 }, { 1, 3 }, { 55, 0 }, { 1, 4 }, { 1, 41 }, { 1, 58 }, { 1, 20 }, { 17, 0 }, { 1, 1 }, { 1, 2 }, { 56, 0 }, { 1, 18 }, { 1, 56 }, { 1, 47 }, { 1, 7 }, { 74, 0 }, { 1, 2 }, { 1, 38 }, { 1, 61 }, { 1, 30 }, { 75, 0 }, { 1, 6 }, { 2, 50 }, { 1, 13 }, { 75, 0 }, { 1, 13 }, { 1, 49 }, { 1, 23 }, { 76, 0 }, { 1, 18 }, { 1, 29 }, { 1, 2 }, { 76, 0 }, { 1, 8 }, { 1, 6 }, { 1088, 0 } }, \n\tLogoRLEFrame{ { 1396, 0 }, { 1, 1 }, { 78, 0 }, { 1, 14 }, { 78, 0 }, { 1, 3 }, { 1, 25 }, { 5, 0 }, { 1, 82 }, { 1, 174 }, { 1, 71 }, { 70, 0 }, { 1, 35 }, { 1, 7 }, { 4, 0 }, { 1, 43 }, { 1, 250 }, { 1, 255 }, { 1, 243 }, { 1, 18 }, { 68, 0 }, { 1, 11 }, { 1, 46 }, { 5, 0 }, { 1, 164 }, { 3, 255 }, { 1, 109 }, { 68, 0 }, { 1, 47 }, { 1, 24 }, { 4, 0 }, { 1, 33 }, { 1, 252 }, { 3, 255 }, { 1, 209 }, { 67, 0 }, { 1, 18 }, { 1, 57 }, { 1, 2 }, { 4, 0 }, { 1, 151 }, { 5, 255 }, { 1, 51 }, { 66, 0 }, { 1, 49 }, { 1, 30 }, { 4, 0 }, { 1, 24 }, { 1, 248 }, { 5, 255 }, { 1, 124 }, { 65, 0 }, { 1, 18 }, { 1, 59 }, { 1, 3 }, { 4, 0 }, { 1, 138 }, { 6, 255 }, { 1, 187 }, { 65, 0 }, { 1, 49 }, { 1, 33 }, { 4, 0 }, { 1, 16 }, { 1, 242 }, { 6, 255 }, { 1, 245 }, { 1, 5 }, { 63, 0 }, { 1, 18 }, { 1, 60 }, { 1, 5 }, { 4, 0 }, { 1, 124 }, { 8, 255 }, { 1, 40 }, { 63, 0 }, { 1, 48 }, { 1, 34 }, { 4, 0 }, { 1, 10 }, { 1, 235 }, { 8, 255 }, { 1, 15 }, { 62, 0 }, { 1, 9 }, { 1, 56 }, { 1, 3 }, { 4, 0 }, { 1, 111 }, { 8, 255 }, { 1, 214 }, { 63, 0 }, { 1, 34 }, { 1, 20 }, { 4, 0 }, { 1, 5 }, { 1, 226 }, { 8, 255 }, { 1, 85 }, { 62, 0 }, { 1, 1 }, { 1, 39 }, { 5, 0 }, { 1, 98 }, { 8, 255 }, { 1, 182 }, { 1, 1 }, { 62, 0 }, { 1, 17 }, { 1, 8 }, { 4, 0 }, { 1, 2 }, { 1, 217 }, { 7, 255 }, { 1, 227 }, { 1, 20 }, { 63, 0 }, { 1, 11 }, { 5, 0 }, { 1, 84 }, { 7, 255 }, { 1, 251 }, { 1, 60 }, { 70, 0 }, { 1, 205 }, { 7, 255 }, { 1, 119 }, { 70, 0 }, { 1, 71 }, { 7, 255 }, { 1, 182 }, { 1, 1 }, { 3, 0 }, { 1, 22 }, { 1, 153 }, { 1, 209 }, { 1, 187 }, { 1, 82 }, { 62, 0 }, { 1, 192 }, { 6, 255 }, { 1, 227 }, { 1, 20 }, { 3, 0 }, { 1, 56 }, { 1, 232 }, { 4, 255 }, { 1, 58 }, { 60, 0 }, { 1, 58 }, { 6, 255 }, { 1, 251 }, { 1, 60 }, { 3, 0 }, { 1, 91 }, { 1, 248 }, { 5, 255 }, { 1, 187 }, { 60, 0 }, { 1, 179 }, { 6, 255 }, { 1, 119 }, { 2, 0 }, { 1, 1 }, { 1, 133 }, { 8, 255 }, { 1, 62 }, { 58, 0 }, { 1, 46 }, { 6, 255 }, { 1, 182 }, { 1, 1 }, { 1, 0 }, { 1, 11 }, { 1, 174 }, { 9, 255 }, { 1, 192 }, { 58, 0 }, { 1, 166 }, { 5, 255 }, { 1, 227 }, { 1, 20 }, { 1, 0 }, { 1, 28 }, { 1, 207 }, { 11, 255 }, { 1, 65 }, { 56, 0 }, { 1, 34 }, { 1, 252 }, { 4, 255 }, { 1, 251 }, { 1, 59 }, { 1, 0 }, { 1, 55 }, { 1, 231 }, { 12, 255 }, { 1, 195 }, { 56, 0 }, { 1, 152 }, { 5, 255 }, { 1, 119 }, { 1, 0 }, { 1, 89 }, { 1, 247 }, { 14, 255 }, { 1, 68 }, { 54, 0 }, { 1, 25 }, { 1, 248 }, { 4, 255 }, { 1, 227 }, { 1, 2 }, { 1, 131 }, { 16, 255 }, { 1, 198 }, { 54, 0 }, { 1, 139 }, { 6, 255 }, { 1, 216 }, { 18, 255 }, { 1, 71 }, { 52, 0 }, { 1, 17 }, { 1, 243 }, { 25, 255 }, { 1, 196 }, { 52, 0 }, { 1, 126 }, { 26, 255 }, { 1, 239 }, { 51, 0 }, { 1, 11 }, { 1, 236 }, { 26, 255 }, { 1, 153 }, { 51, 0 }, { 1, 112 }, { 26, 255 }, { 1, 183 }, { 1, 11 }, { 50, 0 }, { 1, 6 }, { 1, 228 }, { 22, 255 }, { 1, 246 }, { 1, 203 }, { 1, 155 }, { 1, 65 }, { 1, 1 }, { 51, 0 }, { 1, 99 }, { 17, 255 }, { 1, 253 }, { 1, 218 }, { 1, 173 }, { 1, 128 }, { 1, 83 }, { 1, 37 }, { 1, 2 }, { 55, 0 }, { 1, 202 }, { 12, 255 }, { 1, 233 }, { 1, 188 }, { 1, 142 }, { 1, 97 }, { 1, 52 }, { 1, 10 }, { 61, 0 }, { 1, 232 }, { 6, 255 }, { 1, 246 }, { 1, 202 }, { 1, 157 }, { 1, 112 }, { 1, 67 }, { 1, 22 }, { 10, 0 }, { 1, 1 }, { 56, 0 }, { 1, 94 }, { 1, 219 }, { 1, 214 }, { 1, 172 }, { 1, 127 }, { 1, 82 }, { 1, 37 }, { 1, 2 }, { 13, 0 }, { 1, 1 }, { 1, 20 }, { 1, 6 }, { 76, 0 }, { 1, 18 }, { 1, 39 }, { 1, 5 }, { 75, 0 }, { 1, 9 }, { 1, 44 }, { 1, 42 }, { 1, 2 }, { 75, 0 }, { 1, 27 }, { 1, 60 }, { 1, 32 }, { 75, 0 }, { 1, 7 }, { 1, 46 }, { 1, 56 }, { 1, 16 }, { 75, 0 }, { 1, 24 }, { 1, 59 }, { 1, 43 }, { 1, 4 }, { 74, 0 }, { 1, 2 }, { 1, 42 }, { 1, 60 }, { 1, 25 }, { 75, 0 }, { 1, 6 }, { 1, 50 }, { 1, 41 }, { 1, 8 }, { 75, 0 }, { 1, 13 }, { 1, 43 }, { 1, 14 }, { 76, 0 }, { 1, 14 }, { 1, 19 }, { 77, 0 }, { 2, 1 }, { 1324, 0 } }, \n\tLogoRLEFrame{ { 998, 0 }, { 1, 2 }, { 78, 0 }, { 1, 3 }, { 1, 10 }, { 78, 0 }, { 1, 28 }, { 78, 0 }, { 1, 11 }, { 1, 32 }, { 78, 0 }, { 1, 47 }, { 1, 10 }, { 77, 0 }, { 1, 21 }, { 1, 49 }, { 78, 0 }, { 1, 54 }, { 1, 22 }, { 77, 0 }, { 1, 23 }, { 1, 54 }, { 78, 0 }, { 1, 54 }, { 1, 26 }, { 6, 0 }, { 1, 21 }, { 1, 145 }, { 1, 100 }, { 68, 0 }, { 1, 24 }, { 1, 57 }, { 1, 1 }, { 6, 0 }, { 1, 192 }, { 2, 255 }, { 1, 46 }, { 66, 0 }, { 1, 1 }, { 1, 54 }, { 1, 29 }, { 6, 0 }, { 1, 79 }, { 3, 255 }, { 1, 133 }, { 66, 0 }, { 1, 22 }, { 1, 57 }, { 1, 3 }, { 5, 0 }, { 1, 2 }, { 1, 211 }, { 3, 255 }, { 1, 217 }, { 66, 0 }, { 1, 47 }, { 1, 22 }, { 6, 0 }, { 1, 93 }, { 5, 255 }, { 1, 38 }, { 64, 0 }, { 1, 8 }, { 1, 46 }, { 6, 0 }, { 1, 5 }, { 1, 222 }, { 5, 255 }, { 1, 87 }, { 64, 0 }, { 1, 31 }, { 1, 9 }, { 6, 0 }, { 1, 107 }, { 6, 255 }, { 1, 133 }, { 64, 0 }, { 1, 25 }, { 6, 0 }, { 1, 10 }, { 1, 231 }, { 6, 255 }, { 1, 174 }, { 63, 0 }, { 1, 9 }, { 1, 1 }, { 6, 0 }, { 1, 120 }, { 7, 255 }, { 1, 142 }, { 70, 0 }, { 1, 16 }, { 1, 238 }, { 7, 255 }, { 1, 74 }, { 70, 0 }, { 1, 134 }, { 7, 255 }, { 1, 185 }, { 70, 0 }, { 1, 24 }, { 1, 245 }, { 6, 255 }, { 1, 237 }, { 1, 32 }, { 70, 0 }, { 1, 148 }, { 6, 255 }, { 1, 253 }, { 1, 73 }, { 70, 0 }, { 1, 32 }, { 1, 249 }, { 6, 255 }, { 1, 127 }, { 71, 0 }, { 1, 161 }, { 6, 255 }, { 1, 182 }, { 1, 1 }, { 3, 0 }, { 1, 15 }, { 1, 54 }, { 1, 23 }, { 64, 0 }, { 1, 43 }, { 1, 253 }, { 5, 255 }, { 1, 222 }, { 1, 17 }, { 3, 0 }, { 1, 108 }, { 1, 245 }, { 1, 255 }, { 1, 253 }, { 1, 122 }, { 63, 0 }, { 1, 175 }, { 5, 255 }, { 1, 247 }, { 1, 49 }, { 2, 0 }, { 1, 3 }, { 1, 151 }, { 4, 255 }, { 1, 246 }, { 1, 17 }, { 61, 0 }, { 1, 55 }, { 6, 255 }, { 1, 96 }, { 2, 0 }, { 1, 12 }, { 1, 182 }, { 6, 255 }, { 1, 112 }, { 61, 0 }, { 1, 189 }, { 5, 255 }, { 1, 152 }, { 2, 0 }, { 1, 27 }, { 1, 207 }, { 7, 255 }, { 1, 216 }, { 60, 0 }, { 1, 67 }, { 5, 255 }, { 1, 202 }, { 1, 7 }, { 1, 0 }, { 1, 46 }, { 1, 227 }, { 9, 255 }, { 1, 65 }, { 59, 0 }, { 1, 201 }, { 4, 255 }, { 1, 235 }, { 1, 30 }, { 1, 0 }, { 1, 71 }, { 1, 242 }, { 10, 255 }, { 1, 169 }, { 58, 0 }, { 1, 81 }, { 4, 255 }, { 1, 253 }, { 1, 69 }, { 1, 0 }, { 1, 101 }, { 1, 251 }, { 11, 255 }, { 1, 250 }, { 1, 23 }, { 56, 0 }, { 1, 2 }, { 1, 213 }, { 4, 255 }, { 1, 122 }, { 1, 1 }, { 1, 135 }, { 14, 255 }, { 1, 122 }, { 56, 0 }, { 1, 95 }, { 4, 255 }, { 1, 199 }, { 1, 9 }, { 1, 168 }, { 15, 255 }, { 1, 224 }, { 1, 2 }, { 54, 0 }, { 1, 6 }, { 1, 223 }, { 4, 255 }, { 1, 239 }, { 1, 212 }, { 17, 255 }, { 1, 75 }, { 54, 0 }, { 1, 108 }, { 24, 255 }, { 1, 158 }, { 53, 0 }, { 1, 11 }, { 1, 232 }, { 24, 255 }, { 1, 113 }, { 53, 0 }, { 1, 122 }, { 24, 255 }, { 1, 200 }, { 1, 11 }, { 52, 0 }, { 1, 17 }, { 1, 239 }, { 22, 255 }, { 1, 237 }, { 1, 137 }, { 1, 9 }, { 53, 0 }, { 1, 136 }, { 19, 255 }, { 1, 216 }, { 1, 163 }, { 1, 110 }, { 1, 56 }, { 1, 7 }, { 1, 0 }, { 1, 8 }, { 1, 2 }, { 51, 0 }, { 1, 25 }, { 1, 245 }, { 14, 255 }, { 1, 227 }, { 1, 174 }, { 1, 120 }, { 1, 67 }, { 1, 15 }, { 4, 0 }, { 1, 4 }, { 1, 29 }, { 1, 9 }, { 52, 0 }, { 1, 144 }, { 10, 255 }, { 1, 237 }, { 1, 184 }, { 1, 131 }, { 1, 78 }, { 1, 25 }, { 8, 0 }, { 1, 27 }, { 1, 45 }, { 1, 5 }, { 53, 0 }, { 1, 215 }, { 5, 255 }, { 1, 245 }, { 1, 195 }, { 1, 142 }, { 1, 88 }, { 1, 35 }, { 11, 0 }, { 1, 13 }, { 1, 51 }, { 1, 43 }, { 1, 2 }, { 54, 0 }, { 1, 117 }, { 1, 227 }, { 1, 205 }, { 1, 152 }, { 1, 99 }, { 1, 46 }, { 1, 3 }, { 13, 0 }, { 1, 1 }, { 1, 32 }, { 1, 60 }, { 1, 29 }, { 75, 0 }, { 1, 11 }, { 1, 50 }, { 1, 53 }, { 1, 13 }, { 75, 0 }, { 1, 29 }, { 1, 61 }, { 1, 38 }, { 1, 2 }, { 74, 0 }, { 1, 2 }, { 1, 42 }, { 1, 57 }, { 1, 20 }, { 75, 0 }, { 1, 7 }, { 1, 50 }, { 1, 36 }, { 1, 3 }, { 75, 0 }, { 1, 14 }, { 1, 39 }, { 1, 12 }, { 76, 0 }, { 1, 7 }, { 1, 13 }, { 1639, 0 } }, \n\tLogoRLEFrame{ { 680, 0 }, { 1, 7 }, { 78, 0 }, { 1, 5 }, { 1, 8 }, { 78, 0 }, { 1, 20 }, { 78, 0 }, { 1, 10 }, { 1, 17 }, { 77, 0 }, { 1, 1 }, { 1, 27 }, { 1, 6 }, { 77, 0 }, { 1, 14 }, { 1, 23 }, { 77, 0 }, { 1, 1 }, { 1, 28 }, { 1, 9 }, { 77, 0 }, { 1, 14 }, { 1, 25 }, { 77, 0 }, { 1, 1 }, { 1, 28 }, { 1, 10 }, { 77, 0 }, { 1, 15 }, { 1, 26 }, { 78, 0 }, { 1, 28 }, { 1, 11 }, { 77, 0 }, { 1, 10 }, { 1, 23 }, { 78, 0 }, { 1, 21 }, { 1, 5 }, { 7, 0 }, { 1, 85 }, { 1, 118 }, { 1, 1 }, { 67, 0 }, { 1, 3 }, { 1, 16 }, { 7, 0 }, { 1, 93 }, { 2, 255 }, { 1, 79 }, { 67, 0 }, { 1, 11 }, { 1, 1 }, { 6, 0 }, { 1, 16 }, { 1, 235 }, { 2, 255 }, { 1, 149 }, { 67, 0 }, { 1, 5 }, { 7, 0 }, { 1, 144 }, { 3, 255 }, { 1, 216 }, { 74, 0 }, { 1, 41 }, { 1, 251 }, { 4, 255 }, { 1, 10 }, { 73, 0 }, { 1, 185 }, { 5, 255 }, { 1, 38 }, { 72, 0 }, { 1, 78 }, { 6, 255 }, { 1, 67 }, { 71, 0 }, { 1, 6 }, { 1, 219 }, { 6, 255 }, { 1, 37 }, { 71, 0 }, { 1, 119 }, { 6, 255 }, { 1, 212 }, { 71, 0 }, { 1, 24 }, { 1, 242 }, { 5, 255 }, { 1, 253 }, { 1, 60 }, { 71, 0 }, { 1, 160 }, { 6, 255 }, { 1, 129 }, { 71, 0 }, { 1, 54 }, { 6, 255 }, { 1, 177 }, { 1, 1 }, { 70, 0 }, { 1, 1 }, { 1, 200 }, { 5, 255 }, { 1, 214 }, { 1, 13 }, { 71, 0 }, { 1, 93 }, { 5, 255 }, { 1, 239 }, { 1, 37 }, { 3, 0 }, { 1, 77 }, { 1, 130 }, { 1, 91 }, { 1, 2 }, { 64, 0 }, { 1, 12 }, { 1, 229 }, { 4, 255 }, { 1, 253 }, { 1, 72 }, { 2, 0 }, { 1, 11 }, { 1, 177 }, { 3, 255 }, { 1, 131 }, { 64, 0 }, { 1, 134 }, { 5, 255 }, { 1, 118 }, { 2, 0 }, { 1, 22 }, { 1, 203 }, { 4, 255 }, { 1, 217 }, { 63, 0 }, { 1, 34 }, { 1, 248 }, { 4, 255 }, { 1, 167 }, { 2, 0 }, { 1, 36 }, { 1, 220 }, { 6, 255 }, { 1, 42 }, { 62, 0 }, { 1, 175 }, { 4, 255 }, { 1, 206 }, { 1, 9 }, { 1, 0 }, { 1, 53 }, { 1, 234 }, { 7, 255 }, { 1, 123 }, { 51, 0 }, { 1, 4 }, { 9, 0 }, { 1, 68 }, { 4, 255 }, { 1, 234 }, { 1, 30 }, { 1, 0 }, { 1, 74 }, { 1, 244 }, { 8, 255 }, { 1, 204 }, { 50, 0 }, { 1, 1 }, { 1, 9 }, { 8, 0 }, { 1, 4 }, { 1, 212 }, { 3, 255 }, { 1, 250 }, { 1, 62 }, { 1, 0 }, { 1, 98 }, { 1, 251 }, { 10, 255 }, { 1, 30 }, { 49, 0 }, { 1, 13 }, { 1, 2 }, { 8, 0 }, { 1, 109 }, { 4, 255 }, { 1, 106 }, { 1, 0 }, { 1, 125 }, { 12, 255 }, { 1, 110 }, { 48, 0 }, { 1, 7 }, { 1, 13 }, { 8, 0 }, { 1, 19 }, { 1, 238 }, { 3, 255 }, { 1, 155 }, { 1, 3 }, { 1, 153 }, { 13, 255 }, { 1, 191 }, { 47, 0 }, { 1, 2 }, { 1, 19 }, { 1, 4 }, { 8, 0 }, { 1, 150 }, { 3, 255 }, { 1, 204 }, { 1, 16 }, { 1, 177 }, { 14, 255 }, { 1, 252 }, { 1, 20 }, { 46, 0 }, { 2, 13 }, { 8, 0 }, { 1, 46 }, { 1, 252 }, { 3, 255 }, { 1, 213 }, { 1, 206 }, { 16, 255 }, { 1, 89 }, { 45, 0 }, { 1, 4 }, { 1, 20 }, { 1, 2 }, { 8, 0 }, { 1, 190 }, { 22, 255 }, { 1, 116 }, { 45, 0 }, { 1, 17 }, { 1, 11 }, { 8, 0 }, { 1, 83 }, { 22, 255 }, { 1, 243 }, { 1, 24 }, { 44, 0 }, { 1, 8 }, { 1, 19 }, { 1, 1 }, { 7, 0 }, { 1, 8 }, { 1, 223 }, { 21, 255 }, { 1, 231 }, { 1, 53 }, { 5, 0 }, { 1, 16 }, { 1, 8 }, { 37, 0 }, { 1, 1 }, { 1, 19 }, { 1, 8 }, { 8, 0 }, { 1, 124 }, { 19, 255 }, { 1, 247 }, { 1, 191 }, { 1, 108 }, { 1, 11 }, { 4, 0 }, { 1, 10 }, { 1, 38 }, { 1, 10 }, { 38, 0 }, { 1, 10 }, { 1, 16 }, { 8, 0 }, { 1, 28 }, { 1, 245 }, { 15, 255 }, { 1, 241 }, { 1, 182 }, { 1, 120 }, { 1, 59 }, { 1, 6 }, { 5, 0 }, { 1, 3 }, { 1, 35 }, { 1, 49 }, { 1, 6 }, { 38, 0 }, { 1, 1 }, { 1, 18 }, { 1, 3 }, { 8, 0 }, { 1, 165 }, { 12, 255 }, { 1, 234 }, { 1, 173 }, { 1, 111 }, { 1, 50 }, { 1, 3 }, { 8, 0 }, { 1, 17 }, { 1, 56 }, { 1, 42 }, { 1, 2 }, { 39, 0 }, { 2, 9 }, { 8, 0 }, { 1, 58 }, { 9, 255 }, { 1, 226 }, { 1, 164 }, { 1, 102 }, { 1, 40 }, { 11, 0 }, { 1, 2 }, { 1, 37 }, { 1, 60 }, { 1, 25 }, { 41, 0 }, { 1, 12 }, { 9, 0 }, { 1, 168 }, { 5, 255 }, { 1, 216 }, { 1, 155 }, { 1, 93 }, { 1, 31 }, { 14, 0 }, { 1, 15 }, { 1, 54 }, { 1, 50 }, { 1, 9 }, { 41, 0 }, { 1, 5 }, { 1, 1 }, { 9, 0 }, { 1, 133 }, { 1, 243 }, { 1, 207 }, { 1, 146 }, { 1, 84 }, { 1, 23 }, { 17, 0 }, { 1, 32 }, { 1, 62 }, { 1, 34 }, { 1, 1 }, { 42, 0 }, { 1, 1 }, { 31, 0 }, { 1, 2 }, { 1, 43 }, { 1, 49 }, { 1, 14 }, { 75, 0 }, { 1, 7 }, { 1, 43 }, { 1, 22 }, { 76, 0 }, { 1, 10 }, { 1, 24 }, { 1, 1 }, { 77, 0 }, { 1, 3 }, { 1875, 0 } }, \n\tLogoRLEFrame{ { 1643, 0 }, { 1, 62 }, { 1, 116 }, { 1, 1 }, { 76, 0 }, { 1, 66 }, { 1, 251 }, { 1, 255 }, { 1, 64 }, { 75, 0 }, { 1, 10 }, { 1, 223 }, { 2, 255 }, { 1, 116 }, { 75, 0 }, { 1, 138 }, { 3, 255 }, { 1, 165 }, { 74, 0 }, { 1, 47 }, { 1, 251 }, { 3, 255 }, { 1, 184 }, { 73, 0 }, { 1, 2 }, { 1, 200 }, { 4, 255 }, { 1, 195 }, { 73, 0 }, { 1, 107 }, { 5, 255 }, { 1, 180 }, { 72, 0 }, { 1, 26 }, { 1, 241 }, { 5, 255 }, { 1, 93 }, { 72, 0 }, { 1, 173 }, { 5, 255 }, { 1, 194 }, { 1, 3 }, { 71, 0 }, { 1, 77 }, { 5, 255 }, { 1, 231 }, { 1, 26 }, { 71, 0 }, { 1, 12 }, { 1, 226 }, { 4, 255 }, { 1, 247 }, { 1, 54 }, { 72, 0 }, { 1, 143 }, { 5, 255 }, { 1, 89 }, { 72, 0 }, { 1, 50 }, { 1, 252 }, { 4, 255 }, { 1, 129 }, { 2, 0 }, { 1, 3 }, { 1, 102 }, { 1, 161 }, { 1, 117 }, { 1, 5 }, { 56, 0 }, { 1, 5 }, { 8, 0 }, { 1, 3 }, { 1, 204 }, { 4, 255 }, { 1, 170 }, { 1, 1 }, { 1, 0 }, { 1, 17 }, { 1, 194 }, { 3, 255 }, { 1, 100 }, { 55, 0 }, { 1, 4 }, { 1, 16 }, { 8, 0 }, { 1, 112 }, { 4, 255 }, { 1, 204 }, { 1, 9 }, { 1, 0 }, { 1, 27 }, { 1, 212 }, { 4, 255 }, { 1, 161 }, { 55, 0 }, { 1, 34 }, { 8, 0 }, { 1, 29 }, { 1, 243 }, { 3, 255 }, { 1, 229 }, { 1, 26 }, { 1, 0 }, { 1, 40 }, { 1, 225 }, { 5, 255 }, { 1, 223 }, { 54, 0 }, { 1, 22 }, { 1, 28 }, { 8, 0 }, { 1, 178 }, { 3, 255 }, { 1, 246 }, { 1, 52 }, { 1, 0 }, { 1, 56 }, { 1, 236 }, { 7, 255 }, { 1, 29 }, { 52, 0 }, { 1, 7 }, { 1, 55 }, { 1, 3 }, { 7, 0 }, { 1, 82 }, { 4, 255 }, { 1, 85 }, { 1, 0 }, { 1, 74 }, { 1, 245 }, { 8, 255 }, { 1, 90 }, { 52, 0 }, { 1, 45 }, { 1, 34 }, { 7, 0 }, { 1, 14 }, { 1, 229 }, { 3, 255 }, { 1, 125 }, { 1, 0 }, { 1, 94 }, { 1, 251 }, { 9, 255 }, { 1, 152 }, { 51, 0 }, { 1, 20 }, { 1, 58 }, { 1, 4 }, { 7, 0 }, { 1, 148 }, { 3, 255 }, { 1, 167 }, { 1, 0 }, { 1, 117 }, { 11, 255 }, { 1, 213 }, { 50, 0 }, { 1, 2 }, { 1, 55 }, { 1, 26 }, { 7, 0 }, { 1, 55 }, { 1, 253 }, { 2, 255 }, { 1, 201 }, { 1, 10 }, { 1, 141 }, { 13, 255 }, { 1, 20 }, { 49, 0 }, { 1, 31 }, { 1, 53 }, { 7, 0 }, { 1, 4 }, { 1, 208 }, { 2, 255 }, { 1, 227 }, { 1, 30 }, { 1, 164 }, { 14, 255 }, { 1, 80 }, { 48, 0 }, { 1, 8 }, { 1, 61 }, { 1, 18 }, { 7, 0 }, { 1, 117 }, { 3, 255 }, { 1, 177 }, { 1, 187 }, { 15, 255 }, { 1, 129 }, { 11, 0 }, { 1, 4 }, { 1, 3 }, { 35, 0 }, { 1, 42 }, { 1, 46 }, { 7, 0 }, { 1, 32 }, { 1, 245 }, { 20, 255 }, { 1, 116 }, { 9, 0 }, { 1, 1 }, { 1, 25 }, { 1, 14 }, { 35, 0 }, { 1, 10 }, { 1, 59 }, { 1, 8 }, { 7, 0 }, { 1, 183 }, { 20, 255 }, { 1, 212 }, { 1, 9 }, { 8, 0 }, { 1, 18 }, { 1, 46 }, { 1, 11 }, { 36, 0 }, { 1, 40 }, { 1, 26 }, { 7, 0 }, { 1, 87 }, { 20, 255 }, { 1, 171 }, { 1, 15 }, { 7, 0 }, { 1, 6 }, { 1, 44 }, { 1, 50 }, { 1, 6 }, { 36, 0 }, { 1, 7 }, { 1, 45 }, { 7, 0 }, { 1, 16 }, { 1, 231 }, { 16, 255 }, { 1, 251 }, { 1, 195 }, { 1, 126 }, { 1, 40 }, { 8, 0 }, { 1, 23 }, { 1, 58 }, { 1, 38 }, { 1, 2 }, { 37, 0 }, { 1, 30 }, { 1, 5 }, { 7, 0 }, { 1, 153 }, { 14, 255 }, { 1, 217 }, { 1, 147 }, { 1, 78 }, { 1, 14 }, { 9, 0 }, { 1, 5 }, { 1, 43 }, { 1, 58 }, { 1, 20 }, { 38, 0 }, { 1, 4 }, { 1, 12 }, { 7, 0 }, { 1, 59 }, { 11, 255 }, { 1, 236 }, { 1, 168 }, { 1, 99 }, { 1, 30 }, { 12, 0 }, { 1, 19 }, { 1, 57 }, { 1, 47 }, { 1, 7 }, { 39, 0 }, { 1, 1 }, { 7, 0 }, { 1, 5 }, { 1, 212 }, { 7, 255 }, { 1, 249 }, { 1, 190 }, { 1, 120 }, { 1, 51 }, { 1, 2 }, { 14, 0 }, { 1, 33 }, { 1, 60 }, { 1, 29 }, { 49, 0 }, { 1, 106 }, { 5, 255 }, { 1, 211 }, { 1, 141 }, { 1, 72 }, { 1, 10 }, { 16, 0 }, { 1, 2 }, { 1, 43 }, { 1, 39 }, { 1, 6 }, { 50, 0 }, { 1, 156 }, { 1, 255 }, { 1, 231 }, { 1, 163 }, { 1, 93 }, { 1, 25 }, { 19, 0 }, { 1, 7 }, { 1, 36 }, { 1, 12 }, { 52, 0 }, { 1, 4 }, { 1, 24 }, { 22, 0 }, { 1, 5 }, { 1, 14 }, { 2190, 0 } }, \n\tLogoRLEFrame{ { 1724, 0 }, { 1, 99 }, { 1, 81 }, { 77, 0 }, { 1, 129 }, { 1, 255 }, { 1, 228 }, { 76, 0 }, { 1, 57 }, { 1, 253 }, { 2, 255 }, { 1, 9 }, { 74, 0 }, { 1, 8 }, { 1, 216 }, { 3, 255 }, { 1, 35 }, { 74, 0 }, { 1, 140 }, { 4, 255 }, { 1, 31 }, { 73, 0 }, { 1, 58 }, { 1, 253 }, { 4, 255 }, { 1, 23 }, { 72, 0 }, { 1, 9 }, { 1, 217 }, { 4, 255 }, { 1, 201 }, { 73, 0 }, { 1, 141 }, { 5, 255 }, { 1, 72 }, { 61, 0 }, { 2, 5 }, { 9, 0 }, { 1, 58 }, { 1, 253 }, { 4, 255 }, { 1, 123 }, { 62, 0 }, { 1, 25 }, { 9, 0 }, { 1, 9 }, { 1, 217 }, { 4, 255 }, { 1, 162 }, { 62, 0 }, { 1, 24 }, { 1, 16 }, { 9, 0 }, { 1, 142 }, { 4, 255 }, { 1, 192 }, { 1, 6 }, { 61, 0 }, { 1, 8 }, { 1, 47 }, { 9, 0 }, { 1, 59 }, { 1, 253 }, { 3, 255 }, { 1, 216 }, { 1, 17 }, { 2, 0 }, { 1, 78 }, { 1, 131 }, { 1, 73 }, { 57, 0 }, { 1, 47 }, { 1, 23 }, { 8, 0 }, { 1, 9 }, { 1, 218 }, { 3, 255 }, { 1, 235 }, { 1, 35 }, { 1, 0 }, { 1, 10 }, { 1, 178 }, { 3, 255 }, { 1, 39 }, { 55, 0 }, { 1, 25 }, { 1, 53 }, { 1, 1 }, { 8, 0 }, { 1, 143 }, { 3, 255 }, { 1, 248 }, { 1, 58 }, { 1, 0 }, { 1, 18 }, { 1, 199 }, { 4, 255 }, { 1, 87 }, { 54, 0 }, { 1, 4 }, { 1, 58 }, { 1, 19 }, { 8, 0 }, { 1, 60 }, { 1, 253 }, { 3, 255 }, { 1, 88 }, { 1, 0 }, { 1, 28 }, { 1, 213 }, { 5, 255 }, { 1, 133 }, { 54, 0 }, { 1, 37 }, { 1, 47 }, { 8, 0 }, { 1, 10 }, { 1, 219 }, { 3, 255 }, { 1, 121 }, { 1, 0 }, { 1, 40 }, { 1, 225 }, { 6, 255 }, { 1, 180 }, { 1, 0 }, { 1, 18 }, { 1, 5 }, { 50, 0 }, { 1, 12 }, { 1, 61 }, { 1, 12 }, { 8, 0 }, { 1, 144 }, { 3, 255 }, { 1, 157 }, { 1, 0 }, { 1, 54 }, { 1, 235 }, { 7, 255 }, { 1, 227 }, { 1, 38 }, { 1, 6 }, { 51, 0 }, { 1, 49 }, { 1, 39 }, { 8, 0 }, { 1, 61 }, { 3, 255 }, { 1, 188 }, { 1, 5 }, { 1, 69 }, { 1, 243 }, { 9, 255 }, { 1, 20 }, { 51, 0 }, { 1, 21 }, { 1, 59 }, { 1, 6 }, { 7, 0 }, { 1, 10 }, { 1, 220 }, { 2, 255 }, { 1, 214 }, { 1, 16 }, { 1, 87 }, { 1, 249 }, { 10, 255 }, { 1, 63 }, { 16, 0 }, { 1, 11 }, { 1, 10 }, { 33, 0 }, { 1, 50 }, { 1, 23 }, { 8, 0 }, { 1, 145 }, { 2, 255 }, { 1, 233 }, { 1, 32 }, { 1, 107 }, { 1, 253 }, { 11, 255 }, { 1, 110 }, { 14, 0 }, { 1, 4 }, { 1, 34 }, { 1, 17 }, { 33, 0 }, { 1, 17 }, { 1, 44 }, { 8, 0 }, { 1, 62 }, { 2, 255 }, { 1, 246 }, { 1, 55 }, { 1, 128 }, { 13, 255 }, { 1, 157 }, { 13, 0 }, { 1, 27 }, { 1, 51 }, { 1, 11 }, { 34, 0 }, { 1, 42 }, { 1, 6 }, { 7, 0 }, { 1, 11 }, { 1, 221 }, { 2, 255 }, { 1, 124 }, { 1, 150 }, { 14, 255 }, { 1, 193 }, { 11, 0 }, { 1, 10 }, { 1, 49 }, { 1, 50 }, { 1, 6 }, { 34, 0 }, { 1, 13 }, { 1, 15 }, { 8, 0 }, { 1, 147 }, { 19, 255 }, { 1, 171 }, { 10, 0 }, { 1, 28 }, { 1, 60 }, { 1, 34 }, { 1, 1 }, { 35, 0 }, { 1, 11 }, { 8, 0 }, { 1, 63 }, { 19, 255 }, { 1, 232 }, { 1, 33 }, { 8, 0 }, { 1, 8 }, { 1, 47 }, { 1, 56 }, { 1, 16 }, { 45, 0 }, { 1, 11 }, { 1, 221 }, { 18, 255 }, { 1, 189 }, { 1, 29 }, { 8, 0 }, { 1, 22 }, { 1, 59 }, { 1, 43 }, { 1, 4 }, { 46, 0 }, { 1, 148 }, { 16, 255 }, { 1, 214 }, { 1, 139 }, { 1, 53 }, { 9, 0 }, { 1, 33 }, { 1, 55 }, { 1, 22 }, { 47, 0 }, { 1, 64 }, { 13, 255 }, { 1, 247 }, { 1, 182 }, { 1, 107 }, { 1, 33 }, { 10, 0 }, { 1, 2 }, { 1, 40 }, { 1, 30 }, { 1, 2 }, { 47, 0 }, { 1, 11 }, { 1, 222 }, { 10, 255 }, { 1, 226 }, { 1, 151 }, { 1, 76 }, { 1, 10 }, { 12, 0 }, { 1, 7 }, { 1, 26 }, { 1, 5 }, { 49, 0 }, { 1, 149 }, { 7, 255 }, { 1, 252 }, { 1, 195 }, { 1, 120 }, { 1, 45 }, { 16, 0 }, { 1, 5 }, { 50, 0 }, { 1, 61 }, { 5, 255 }, { 1, 236 }, { 1, 163 }, { 1, 89 }, { 1, 18 }, { 70, 0 }, { 1, 173 }, { 2, 255 }, { 1, 207 }, { 1, 132 }, { 1, 57 }, { 1, 2 }, { 73, 0 }, { 1, 55 }, { 1, 86 }, { 1, 27 }, { 2213, 0 } }, \n\tLogoRLEFrame{ { 1804, 0 }, { 1, 34 }, { 1, 136 }, { 1, 20 }, { 76, 0 }, { 1, 26 }, { 1, 229 }, { 1, 255 }, { 1, 94 }, { 63, 0 }, { 1, 1 }, { 11, 0 }, { 1, 1 }, { 1, 185 }, { 2, 255 }, { 1, 116 }, { 63, 0 }, { 1, 14 }, { 11, 0 }, { 1, 108 }, { 3, 255 }, { 1, 124 }, { 62, 0 }, { 1, 23 }, { 1, 6 }, { 10, 0 }, { 1, 40 }, { 1, 246 }, { 3, 255 }, { 1, 104 }, { 61, 0 }, { 1, 8 }, { 1, 36 }, { 10, 0 }, { 1, 5 }, { 1, 204 }, { 4, 255 }, { 1, 67 }, { 61, 0 }, { 1, 48 }, { 1, 11 }, { 10, 0 }, { 1, 132 }, { 4, 255 }, { 1, 209 }, { 61, 0 }, { 1, 29 }, { 1, 46 }, { 10, 0 }, { 1, 58 }, { 1, 252 }, { 3, 255 }, { 1, 241 }, { 1, 44 }, { 60, 0 }, { 1, 7 }, { 1, 60 }, { 1, 13 }, { 9, 0 }, { 1, 12 }, { 1, 221 }, { 3, 255 }, { 1, 251 }, { 1, 71 }, { 61, 0 }, { 1, 42 }, { 1, 41 }, { 10, 0 }, { 1, 157 }, { 4, 255 }, { 1, 98 }, { 61, 0 }, { 1, 17 }, { 1, 60 }, { 1, 7 }, { 9, 0 }, { 1, 79 }, { 4, 255 }, { 1, 128 }, { 2, 0 }, { 1, 6 }, { 1, 38 }, { 1, 6 }, { 56, 0 }, { 1, 1 }, { 1, 53 }, { 1, 32 }, { 9, 0 }, { 1, 23 }, { 1, 234 }, { 3, 255 }, { 1, 158 }, { 2, 0 }, { 1, 85 }, { 1, 235 }, { 1, 255 }, { 1, 221 }, { 1, 4 }, { 5, 0 }, { 1, 9 }, { 1, 3 }, { 48, 0 }, { 1, 28 }, { 1, 57 }, { 1, 3 }, { 9, 0 }, { 1, 180 }, { 3, 255 }, { 1, 185 }, { 1, 4 }, { 1, 0 }, { 1, 114 }, { 4, 255 }, { 1, 35 }, { 3, 0 }, { 1, 4 }, { 1, 30 }, { 1, 10 }, { 48, 0 }, { 1, 2 }, { 1, 58 }, { 1, 22 }, { 9, 0 }, { 1, 104 }, { 3, 255 }, { 1, 207 }, { 1, 13 }, { 1, 0 }, { 1, 136 }, { 5, 255 }, { 1, 71 }, { 2, 0 }, { 1, 23 }, { 1, 47 }, { 1, 7 }, { 17, 0 }, { 1, 1 }, { 1, 2 }, { 30, 0 }, { 1, 27 }, { 1, 43 }, { 9, 0 }, { 1, 37 }, { 1, 245 }, { 2, 255 }, { 1, 225 }, { 1, 25 }, { 1, 3 }, { 1, 157 }, { 6, 255 }, { 1, 107 }, { 1, 8 }, { 1, 47 }, { 1, 48 }, { 1, 4 }, { 17, 0 }, { 2, 9 }, { 30, 0 }, { 1, 1 }, { 1, 51 }, { 1, 5 }, { 8, 0 }, { 1, 4 }, { 1, 201 }, { 2, 255 }, { 1, 239 }, { 1, 42 }, { 1, 8 }, { 1, 176 }, { 7, 255 }, { 1, 152 }, { 1, 59 }, { 1, 36 }, { 1, 1 }, { 16, 0 }, { 1, 5 }, { 1, 21 }, { 1, 9 }, { 31, 0 }, { 1, 24 }, { 1, 18 }, { 9, 0 }, { 1, 128 }, { 2, 255 }, { 1, 249 }, { 1, 64 }, { 1, 15 }, { 1, 193 }, { 8, 255 }, { 1, 195 }, { 1, 20 }, { 16, 0 }, { 1, 1 }, { 1, 17 }, { 1, 27 }, { 1, 5 }, { 32, 0 }, { 1, 23 }, { 9, 0 }, { 1, 55 }, { 1, 251 }, { 2, 255 }, { 1, 89 }, { 1, 24 }, { 1, 208 }, { 9, 255 }, { 1, 215 }, { 16, 0 }, { 1, 7 }, { 1, 26 }, { 1, 23 }, { 1, 3 }, { 32, 0 }, { 1, 4 }, { 9, 0 }, { 1, 11 }, { 1, 218 }, { 2, 255 }, { 1, 118 }, { 1, 35 }, { 1, 221 }, { 10, 255 }, { 1, 248 }, { 1, 2 }, { 14, 0 }, { 1, 16 }, { 1, 30 }, { 1, 14 }, { 44, 0 }, { 1, 152 }, { 2, 255 }, { 1, 149 }, { 1, 48 }, { 1, 232 }, { 12, 255 }, { 1, 29 }, { 12, 0 }, { 1, 6 }, { 1, 25 }, { 1, 26 }, { 1, 6 }, { 44, 0 }, { 1, 76 }, { 2, 255 }, { 1, 178 }, { 1, 66 }, { 1, 241 }, { 13, 255 }, { 1, 62 }, { 11, 0 }, { 1, 11 }, { 1, 30 }, { 1, 19 }, { 1, 1 }, { 44, 0 }, { 1, 21 }, { 1, 232 }, { 2, 255 }, { 1, 185 }, { 1, 248 }, { 14, 255 }, { 1, 59 }, { 10, 0 }, { 1, 16 }, { 1, 23 }, { 1, 6 }, { 46, 0 }, { 1, 176 }, { 18, 255 }, { 1, 184 }, { 9, 0 }, { 1, 1 }, { 1, 17 }, { 1, 10 }, { 47, 0 }, { 1, 99 }, { 18, 255 }, { 1, 168 }, { 1, 10 }, { 8, 0 }, { 1, 1 }, { 1, 8 }, { 48, 0 }, { 1, 34 }, { 1, 243 }, { 15, 255 }, { 1, 242 }, { 1, 169 }, { 1, 59 }, { 59, 0 }, { 1, 3 }, { 1, 198 }, { 13, 255 }, { 1, 227 }, { 1, 149 }, { 1, 71 }, { 1, 6 }, { 61, 0 }, { 1, 123 }, { 11, 255 }, { 1, 206 }, { 1, 128 }, { 1, 50 }, { 64, 0 }, { 1, 51 }, { 1, 250 }, { 7, 255 }, { 1, 250 }, { 1, 185 }, { 1, 107 }, { 1, 30 }, { 66, 0 }, { 1, 9 }, { 1, 215 }, { 5, 255 }, { 1, 238 }, { 1, 164 }, { 1, 86 }, { 1, 14 }, { 69, 0 }, { 1, 131 }, { 3, 255 }, { 1, 222 }, { 1, 143 }, { 1, 65 }, { 1, 4 }, { 72, 0 }, { 1, 155 }, { 1, 191 }, { 1, 122 }, { 1, 44 }, { 2212, 0 } }, \n\tLogoRLEFrame{ { 1633, 0 }, { 1, 4 }, { 78, 0 }, { 1, 18 }, { 78, 0 }, { 1, 9 }, { 1, 25 }, { 78, 0 }, { 1, 45 }, { 1, 3 }, { 10, 0 }, { 1, 9 }, { 1, 171 }, { 1, 197 }, { 64, 0 }, { 1, 31 }, { 1, 34 }, { 11, 0 }, { 1, 167 }, { 1, 255 }, { 1, 248 }, { 63, 0 }, { 1, 11 }, { 1, 60 }, { 1, 7 }, { 10, 0 }, { 1, 96 }, { 3, 255 }, { 1, 3 }, { 62, 0 }, { 1, 47 }, { 1, 34 }, { 10, 0 }, { 1, 37 }, { 1, 243 }, { 2, 255 }, { 1, 240 }, { 62, 0 }, { 1, 22 }, { 1, 57 }, { 1, 3 }, { 9, 0 }, { 1, 5 }, { 1, 203 }, { 3, 255 }, { 1, 211 }, { 61, 0 }, { 1, 3 }, { 1, 56 }, { 1, 26 }, { 10, 0 }, { 1, 136 }, { 4, 255 }, { 1, 115 }, { 61, 0 }, { 1, 34 }, { 1, 52 }, { 10, 0 }, { 1, 66 }, { 1, 253 }, { 3, 255 }, { 1, 208 }, { 1, 9 }, { 60, 0 }, { 1, 9 }, { 1, 61 }, { 1, 18 }, { 9, 0 }, { 1, 19 }, { 1, 229 }, { 3, 255 }, { 1, 225 }, { 1, 26 }, { 13, 0 }, { 1, 2 }, { 47, 0 }, { 1, 38 }, { 1, 41 }, { 10, 0 }, { 1, 176 }, { 3, 255 }, { 1, 238 }, { 1, 41 }, { 13, 0 }, { 1, 19 }, { 1, 12 }, { 46, 0 }, { 1, 6 }, { 1, 55 }, { 1, 4 }, { 9, 0 }, { 1, 104 }, { 3, 255 }, { 1, 247 }, { 1, 59 }, { 12, 0 }, { 1, 10 }, { 1, 41 }, { 1, 13 }, { 47, 0 }, { 1, 34 }, { 1, 19 }, { 9, 0 }, { 1, 42 }, { 1, 246 }, { 2, 255 }, { 1, 253 }, { 1, 80 }, { 1, 0 }, { 1, 10 }, { 1, 136 }, { 1, 221 }, { 1, 194 }, { 1, 24 }, { 5, 0 }, { 1, 2 }, { 1, 33 }, { 1, 52 }, { 1, 8 }, { 47, 0 }, { 1, 4 }, { 1, 32 }, { 9, 0 }, { 1, 7 }, { 1, 209 }, { 3, 255 }, { 1, 104 }, { 1, 0 }, { 1, 19 }, { 1, 200 }, { 3, 255 }, { 1, 85 }, { 4, 0 }, { 1, 12 }, { 1, 52 }, { 1, 47 }, { 1, 5 }, { 48, 0 }, { 1, 17 }, { 1, 1 }, { 9, 0 }, { 1, 144 }, { 3, 255 }, { 1, 131 }, { 1, 0 }, { 1, 29 }, { 1, 214 }, { 4, 255 }, { 1, 113 }, { 3, 0 }, { 1, 29 }, { 1, 60 }, { 1, 32 }, { 50, 0 }, { 1, 2 }, { 9, 0 }, { 1, 74 }, { 3, 255 }, { 1, 157 }, { 1, 0 }, { 1, 41 }, { 1, 226 }, { 5, 255 }, { 1, 142 }, { 1, 0 }, { 1, 7 }, { 1, 47 }, { 1, 57 }, { 1, 16 }, { 60, 0 }, { 1, 23 }, { 1, 233 }, { 2, 255 }, { 1, 181 }, { 1, 4 }, { 1, 55 }, { 1, 236 }, { 6, 255 }, { 1, 171 }, { 1, 16 }, { 1, 58 }, { 1, 46 }, { 1, 5 }, { 60, 0 }, { 1, 1 }, { 1, 184 }, { 2, 255 }, { 1, 201 }, { 1, 10 }, { 1, 71 }, { 1, 244 }, { 7, 255 }, { 1, 204 }, { 1, 56 }, { 1, 24 }, { 62, 0 }, { 1, 113 }, { 2, 255 }, { 1, 218 }, { 1, 20 }, { 1, 90 }, { 1, 250 }, { 8, 255 }, { 1, 231 }, { 1, 3 }, { 62, 0 }, { 1, 48 }, { 1, 249 }, { 1, 255 }, { 1, 232 }, { 1, 33 }, { 1, 110 }, { 10, 255 }, { 1, 253 }, { 1, 4 }, { 61, 0 }, { 1, 10 }, { 1, 215 }, { 1, 255 }, { 1, 243 }, { 1, 50 }, { 1, 132 }, { 12, 255 }, { 1, 29 }, { 61, 0 }, { 1, 153 }, { 1, 255 }, { 1, 250 }, { 1, 72 }, { 1, 154 }, { 13, 255 }, { 1, 58 }, { 60, 0 }, { 1, 82 }, { 2, 255 }, { 2, 175 }, { 14, 255 }, { 1, 64 }, { 59, 0 }, { 1, 28 }, { 1, 237 }, { 17, 255 }, { 1, 209 }, { 1, 5 }, { 58, 0 }, { 1, 2 }, { 1, 191 }, { 17, 255 }, { 1, 204 }, { 1, 28 }, { 59, 0 }, { 1, 121 }, { 16, 255 }, { 1, 219 }, { 1, 106 }, { 1, 7 }, { 59, 0 }, { 1, 54 }, { 1, 251 }, { 13, 255 }, { 1, 205 }, { 1, 125 }, { 1, 45 }, { 61, 0 }, { 1, 13 }, { 1, 220 }, { 10, 255 }, { 1, 252 }, { 1, 190 }, { 1, 110 }, { 1, 30 }, { 64, 0 }, { 1, 162 }, { 8, 255 }, { 1, 246 }, { 1, 175 }, { 1, 95 }, { 1, 19 }, { 66, 0 }, { 1, 90 }, { 6, 255 }, { 1, 237 }, { 1, 160 }, { 1, 80 }, { 1, 10 }, { 68, 0 }, { 1, 27 }, { 1, 241 }, { 3, 255 }, { 1, 225 }, { 1, 145 }, { 1, 65 }, { 1, 4 }, { 71, 0 }, { 1, 126 }, { 1, 255 }, { 1, 211 }, { 1, 130 }, { 1, 50 }, { 75, 0 }, { 1, 7 }, { 1, 19 }, { 2135, 0 } }, \n\tLogoRLEFrame{ { 1315, 0 }, { 1, 8 }, { 78, 0 }, { 1, 10 }, { 1, 13 }, { 77, 0 }, { 1, 1 }, { 1, 38 }, { 78, 0 }, { 1, 32 }, { 1, 22 }, { 77, 0 }, { 1, 13 }, { 1, 55 }, { 1, 1 }, { 77, 0 }, { 1, 52 }, { 1, 27 }, { 77, 0 }, { 1, 28 }, { 1, 53 }, { 1, 1 }, { 76, 0 }, { 1, 5 }, { 1, 59 }, { 1, 19 }, { 9, 0 }, { 1, 4 }, { 1, 141 }, { 1, 147 }, { 65, 0 }, { 1, 39 }, { 1, 47 }, { 10, 0 }, { 1, 153 }, { 1, 255 }, { 1, 224 }, { 64, 0 }, { 1, 14 }, { 1, 62 }, { 1, 12 }, { 9, 0 }, { 1, 86 }, { 2, 255 }, { 1, 232 }, { 64, 0 }, { 1, 48 }, { 1, 38 }, { 9, 0 }, { 1, 32 }, { 1, 240 }, { 2, 255 }, { 1, 212 }, { 63, 0 }, { 1, 15 }, { 1, 56 }, { 1, 3 }, { 8, 0 }, { 1, 4 }, { 1, 199 }, { 3, 255 }, { 1, 179 }, { 63, 0 }, { 1, 45 }, { 1, 17 }, { 9, 0 }, { 1, 135 }, { 4, 255 }, { 1, 83 }, { 16, 0 }, { 2, 9 }, { 44, 0 }, { 1, 12 }, { 1, 37 }, { 9, 0 }, { 1, 67 }, { 1, 253 }, { 3, 255 }, { 1, 182 }, { 15, 0 }, { 1, 2 }, { 1, 31 }, { 1, 18 }, { 45, 0 }, { 1, 29 }, { 1, 1 }, { 8, 0 }, { 1, 21 }, { 1, 230 }, { 3, 255 }, { 1, 201 }, { 1, 11 }, { 14, 0 }, { 1, 20 }, { 1, 51 }, { 1, 14 }, { 45, 0 }, { 1, 5 }, { 1, 7 }, { 8, 0 }, { 1, 1 }, { 1, 182 }, { 3, 255 }, { 1, 217 }, { 1, 20 }, { 13, 0 }, { 1, 4 }, { 1, 43 }, { 1, 55 }, { 1, 10 }, { 56, 0 }, { 1, 114 }, { 3, 255 }, { 1, 230 }, { 1, 32 }, { 13, 0 }, { 1, 17 }, { 1, 56 }, { 1, 43 }, { 1, 4 }, { 56, 0 }, { 1, 51 }, { 1, 249 }, { 2, 255 }, { 1, 241 }, { 1, 47 }, { 1, 0 }, { 1, 27 }, { 1, 167 }, { 1, 230 }, { 1, 169 }, { 1, 2 }, { 6, 0 }, { 1, 1 }, { 1, 35 }, { 1, 61 }, { 1, 27 }, { 57, 0 }, { 1, 12 }, { 1, 218 }, { 2, 255 }, { 1, 248 }, { 1, 64 }, { 1, 0 }, { 1, 46 }, { 1, 230 }, { 3, 255 }, { 1, 23 }, { 5, 0 }, { 1, 10 }, { 1, 52 }, { 1, 54 }, { 1, 12 }, { 58, 0 }, { 1, 162 }, { 2, 255 }, { 1, 253 }, { 1, 85 }, { 1, 0 }, { 1, 62 }, { 1, 240 }, { 4, 255 }, { 1, 48 }, { 4, 0 }, { 1, 16 }, { 1, 58 }, { 1, 38 }, { 1, 3 }, { 58, 0 }, { 1, 93 }, { 3, 255 }, { 1, 108 }, { 1, 0 }, { 1, 79 }, { 1, 247 }, { 5, 255 }, { 1, 73 }, { 3, 0 }, { 1, 24 }, { 1, 47 }, { 1, 12 }, { 59, 0 }, { 1, 36 }, { 1, 243 }, { 2, 255 }, { 1, 133 }, { 1, 0 }, { 1, 100 }, { 1, 252 }, { 6, 255 }, { 1, 98 }, { 2, 0 }, { 1, 26 }, { 1, 22 }, { 60, 0 }, { 1, 6 }, { 1, 204 }, { 2, 255 }, { 1, 158 }, { 1, 0 }, { 1, 123 }, { 8, 255 }, { 1, 123 }, { 1, 0 }, { 1, 11 }, { 1, 2 }, { 61, 0 }, { 1, 141 }, { 2, 255 }, { 1, 180 }, { 1, 6 }, { 1, 146 }, { 9, 255 }, { 1, 148 }, { 63, 0 }, { 1, 73 }, { 2, 255 }, { 1, 199 }, { 1, 16 }, { 1, 167 }, { 10, 255 }, { 1, 174 }, { 62, 0 }, { 1, 24 }, { 1, 234 }, { 1, 255 }, { 1, 215 }, { 1, 31 }, { 1, 186 }, { 11, 255 }, { 1, 199 }, { 61, 0 }, { 1, 1 }, { 1, 187 }, { 1, 255 }, { 1, 229 }, { 1, 52 }, { 1, 203 }, { 12, 255 }, { 1, 223 }, { 61, 0 }, { 1, 120 }, { 2, 255 }, { 1, 138 }, { 1, 217 }, { 13, 255 }, { 1, 225 }, { 60, 0 }, { 1, 56 }, { 1, 251 }, { 17, 255 }, { 1, 111 }, { 59, 0 }, { 1, 15 }, { 1, 222 }, { 16, 255 }, { 1, 253 }, { 1, 122 }, { 60, 0 }, { 1, 168 }, { 15, 255 }, { 1, 247 }, { 1, 168 }, { 1, 47 }, { 60, 0 }, { 1, 99 }, { 13, 255 }, { 1, 240 }, { 1, 164 }, { 1, 83 }, { 1, 11 }, { 61, 0 }, { 1, 40 }, { 1, 245 }, { 10, 255 }, { 1, 230 }, { 1, 151 }, { 1, 70 }, { 1, 5 }, { 63, 0 }, { 1, 8 }, { 1, 209 }, { 8, 255 }, { 1, 219 }, { 1, 138 }, { 1, 57 }, { 1, 1 }, { 66, 0 }, { 1, 147 }, { 6, 255 }, { 1, 206 }, { 1, 125 }, { 1, 44 }, { 69, 0 }, { 1, 73 }, { 3, 255 }, { 1, 252 }, { 1, 193 }, { 1, 112 }, { 1, 32 }, { 72, 0 }, { 1, 190 }, { 1, 247 }, { 1, 180 }, { 1, 99 }, { 1, 21 }, { 75, 0 }, { 1, 13 }, { 1, 5 }, { 664, 0 }, { 1, 7 }, { 1, 1 }, { 76, 0 }, { 1, 11 }, { 1, 16 }, { 1, 1 }, { 75, 0 }, { 1, 10 }, { 1, 25 }, { 1, 14 }, { 75, 0 }, { 1, 5 }, { 1, 23 }, { 1, 27 }, { 1, 7 }, { 74, 0 }, { 1, 1 }, { 1, 16 }, { 1, 30 }, { 1, 18 }, { 1, 1 }, { 74, 0 }, { 1, 9 }, { 1, 27 }, { 1, 25 }, { 1, 7 }, { 74, 0 }, { 1, 1 }, { 1, 19 }, { 1, 30 }, { 1, 15 }, { 75, 0 }, { 1, 5 }, { 1, 25 }, { 1, 17 }, { 1, 3 }, { 75, 0 }, { 1, 11 }, { 1, 16 }, { 1, 3 }, { 76, 0 }, { 1, 6 }, { 1, 2 }, { 764, 0 } }, \n\n} };\n\t\n} // namespace RoundVideoData\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/send_as_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/send_as_button.h\"\n\n#include \"ui/effects/cross_animation.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Ui {\n\nSendAsButton::SendAsButton(QWidget *parent, const style::SendAsButton &st)\n: AbstractButton(parent)\n, _st(st) {\n\tresize(_st.width, _st.height);\n}\n\nvoid SendAsButton::setUserpic(QImage userpic) {\n\t_userpic = std::move(userpic);\n\tupdate();\n}\n\nvoid SendAsButton::setActive(bool active) {\n\tif (_active == active) {\n\t\treturn;\n\t}\n\t_active = active;\n\t_activeAnimation.start(\n\t\t[=] { update(); },\n\t\t_active ? 0. : 1.,\n\t\t_active ? 1. : 0.,\n\t\t_st.duration);\n}\n\nvoid SendAsButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto left = (width() - _st.size) / 2;\n\tconst auto top = (height() - _st.size) / 2;\n\n\tconst auto active = _activeAnimation.value(_active ? 1. : 0.);\n\tif (active < 1. && !_userpic.isNull()) {\n\t\tp.drawImage(QRect(left, top, _st.size, _st.size), _userpic);\n\t}\n\tif (active > 0.) {\n\t\tp.setOpacity(active);\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(_st.activeBg);\n\t\t{\n\t\t\tPainterHighQualityEnabler hq(p);\n\t\t\tp.drawEllipse(left, top, _st.size, _st.size);\n\t\t}\n\n\t\tCrossAnimation::paint(\n\t\t\tp,\n\t\t\t_st.cross,\n\t\t\t_st.activeFg,\n\t\t\tleft,\n\t\t\ttop,\n\t\t\twidth(),\n\t\t\tactive);\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/send_as_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/buttons.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace style {\nstruct SendAsButton;\n} // namespace style\n\nnamespace Ui {\n\nclass SendAsButton final : public AbstractButton {\npublic:\n\tSendAsButton(QWidget *parent, const style::SendAsButton &st);\n\n\tvoid setUserpic(QImage userpic);\n\n\tvoid setActive(bool active);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tconst style::SendAsButton &_st;\n\n\tAnimations::Simple _activeAnimation;\n\tbool _active = false;\n\n\tQImage _userpic;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/send_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/send_button.h\"\n\n#include \"lang/lang_tag.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_credits.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/text/format_values.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kWideScale = 5;\nconstexpr auto kVoiceToRoundIndex = 0;\nconstexpr auto kRoundToVoiceIndex = 1;\nconstexpr auto kForbiddenOpacity = 0.5;\n\n} // namespace\n\nSendButton::SendButton(QWidget *parent, const style::SendButton &st)\n: RippleButton(parent, st.inner.ripple)\n, _st(st)\n, _lastRippleShape(currentRippleShape()) {\n\tupdateSize();\n}\n\nSendButton::~SendButton() = default;\n\nvoid SendButton::setState(State state) {\n\tif (_state == state) {\n\t\treturn;\n\t}\n\n\tconst auto previousType = _state.type;\n\tconst auto newType = state.type;\n\tconst auto voiceRoundTransition = isVoiceRoundTransition(\n\t\tpreviousType,\n\t\tnewType);\n\n\tconst auto hasSlowmode = (_state.slowmodeDelay > 0);\n\tconst auto hasSlowmodeChanged = hasSlowmode != (state.slowmodeDelay > 0);\n\tauto withSameSlowmode = state;\n\twithSameSlowmode.slowmodeDelay = _state.slowmodeDelay;\n\tconst auto animate = hasSlowmodeChanged\n\t\t|| (!hasSlowmode && withSameSlowmode != _state);\n\n\tif (animate && !voiceRoundTransition) {\n\t\t_contentFrom = grabContent();\n\t}\n\n\tif (_voiceRoundAnimating && !voiceRoundTransition) {\n\t\t_voiceRoundAnimating = false;\n\t}\n\n\tif (_state.slowmodeDelay != state.slowmodeDelay) {\n\t\tconst auto seconds = state.slowmodeDelay;\n\t\tconst auto minutes = seconds / 60;\n\t\t_slowmodeDelayText = seconds\n\t\t\t? u\"%1:%2\"_q.arg(minutes).arg(seconds % 60, 2, 10, QChar('0'))\n\t\t\t: QString();\n\t}\n\tif (!state.starsToSend || state.type != Type::Send) {\n\t\t_starsToSendText = Text::String();\n\t} else if (_starsToSendText.isEmpty()\n\t\t|| _state.starsToSend != state.starsToSend) {\n\t\t_starsToSendText.setMarkedText(\n\t\t\t_st.stars.style,\n\t\t\tText::IconEmoji(&st::starIconEmoji).append(\n\t\t\t\tLang::FormatCountToShort(state.starsToSend).string),\n\t\t\tkMarkupTextOptions);\n\t}\n\t_state = state;\n\n\tconst auto newShape = currentRippleShape();\n\tif (_lastRippleShape != newShape) {\n\t\t_lastRippleShape = newShape;\n\t\tRippleButton::finishAnimating();\n\t}\n\n\tsetAccessibleName([&] {\n\t\tswitch (_state.type) {\n\t\tcase Type::Send: return tr::lng_send_button(tr::now);\n\t\tcase Type::Record:\n\t\t\treturn tr::lng_shortcuts_record_voice_message(tr::now);\n\t\tcase Type::Round:\n\t\t\treturn tr::lng_shortcuts_record_round_message(tr::now);\n\t\tcase Type::Cancel: return tr::lng_cancel(tr::now);\n\t\tcase Type::Save: return tr::lng_settings_save(tr::now);\n\t\tcase Type::Slowmode:\n\t\t\treturn tr::lng_slowmode_enabled(\n\t\t\t\ttr::now,\n\t\t\t\tlt_left,\n\t\t\t\tUi::FormatDurationWordsSlowmode(_state.slowmodeDelay));\n\t\tcase Type::Schedule: return tr::lng_schedule_button(tr::now);\n\t\tcase Type::EditPrice:\n\t\t\treturn tr::lng_suggest_menu_edit_price(tr::now);\n\t\t}\n\t\tUnexpected(\"Send button type.\");\n\t}());\n\n\tif (voiceRoundTransition) {\n\t\t_voiceRoundAnimating = true;\n\n\t\tconst auto toRound = (newType == Type::Round);\n\t\tconst auto index = toRound ? kVoiceToRoundIndex : kRoundToVoiceIndex;\n\t\tauto &icon = _voiceRoundIcons[index];\n\t\tif (!icon) {\n\t\t\tinitVoiceRoundIcon(index);\n\t\t}\n\t\ticon->animate([=, raw = icon.get()] {\n\t\t\tupdate();\n\t\t\tif (!raw->animating()) {\n\t\t\t\t_voiceRoundAnimating = false;\n\t\t\t}\n\t\t}, 0, icon->framesCount() - 1);\n\t\tauto &after = _voiceRoundIcons[1 - index];\n\t\tif (!after) {\n\t\t\tinitVoiceRoundIcon(1 - index);\n\t\t} else if (after->frameIndex() != 0) {\n\t\t\tafter->jumpTo(0, nullptr);\n\t\t}\n\t} else if (animate) {\n\t\t_stateChangeFromWidth = width();\n\t\t_stateChangeAnimation.stop();\n\t\tupdateSize();\n\t\t_contentTo = grabContent();\n\t\t_stateChangeAnimation.start(\n\t\t\t[=] { updateSize(); update(); },\n\t\t\t0.,\n\t\t\t1.,\n\t\t\tst::universalDuration);\n\t\tsetPointerCursor(_state.type != Type::Slowmode);\n\t\tupdateSize();\n\t}\n\tupdate();\n}\n\nvoid SendButton::finishAnimating() {\n\t_stateChangeAnimation.stop();\n\tupdateSize();\n\tupdate();\n}\n\nvoid SendButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tauto over = (isDown() || isOver());\n\n\tif (_voiceRoundAnimating) {\n\t\tpaintVoiceRoundIcon(p, over);\n\t\treturn;\n\t}\n\n\tauto changed = _stateChangeAnimation.value(1.);\n\tif (changed < 1.) {\n\t\tPainterHighQualityEnabler hq(p);\n\t\tconst auto ratio = style::DevicePixelRatio();\n\n\t\tp.setOpacity(1. - changed);\n\t\tconst auto fromSize = _contentFrom.size() / (kWideScale * ratio);\n\t\tconst auto fromShift = QPoint(\n\t\t\t(width() - fromSize.width()) / 2,\n\t\t\t(height() - fromSize.height()) / 2);\n\t\tauto fromRect = QRect(\n\t\t\t(1 - kWideScale) / 2 * fromSize.width(),\n\t\t\t(1 - kWideScale) / 2 * fromSize.height(),\n\t\t\tkWideScale * fromSize.width(),\n\t\t\tkWideScale * fromSize.height()\n\t\t).translated(fromShift);\n\t\tauto hiddenWidth = anim::interpolate(0, (1 - kWideScale) / 2 * fromSize.width(), changed);\n\t\tauto hiddenHeight = anim::interpolate(0, (1 - kWideScale) / 2 * fromSize.height(), changed);\n\t\tp.drawPixmap(\n\t\t\tfromRect.marginsAdded(\n\t\t\t\t{ hiddenWidth, hiddenHeight, hiddenWidth, hiddenHeight }),\n\t\t\t_contentFrom);\n\n\t\tp.setOpacity(changed);\n\t\tconst auto toSize = _contentTo.size() / (kWideScale * ratio);\n\t\tconst auto toShift = QPoint(\n\t\t\t(width() - toSize.width()) / 2,\n\t\t\t(height() - toSize.height()) / 2);\n\t\tauto toRect = QRect(\n\t\t\t(1 - kWideScale) / 2 * toSize.width(),\n\t\t\t(1 - kWideScale) / 2 * toSize.height(),\n\t\t\tkWideScale * toSize.width(),\n\t\t\tkWideScale * toSize.height()\n\t\t).translated(toShift);\n\t\tauto shownWidth = anim::interpolate((1 - kWideScale) / 2 * width(), 0, changed);\n\t\tauto shownHeight = anim::interpolate((1 - kWideScale) / 2 * toSize.height(), 0, changed);\n\t\tp.drawPixmap(\n\t\t\ttoRect.marginsAdded(\n\t\t\t\t{ shownWidth, shownHeight, shownWidth, shownHeight }),\n\t\t\t_contentTo);\n\t\treturn;\n\t}\n\tswitch (_state.type) {\n\tcase Type::Record: paintRecord(p, over); break;\n\tcase Type::Round: paintRound(p, over); break;\n\tcase Type::Save: paintSave(p, over); break;\n\tcase Type::Cancel: paintCancel(p, over); break;\n\tcase Type::Send:\n\t\tif (_starsToSendText.isEmpty()) {\n\t\t\tpaintSend(p, over);\n\t\t} else {\n\t\t\tpaintStarsToSend(p, over);\n\t\t}\n\t\tbreak;\n\tcase Type::Schedule: paintSchedule(p, over); break;\n\tcase Type::Slowmode: paintSlowmode(p); break;\n\tcase Type::EditPrice: break;\n\t}\n}\n\nvoid SendButton::paintRecord(QPainter &p, bool over) {\n\tif (!isDisabled() && !_state.forbidden) {\n\t\tpaintRipple(\n\t\t\tp,\n\t\t\t(width() - _st.inner.rippleAreaSize) / 2,\n\t\t\t_st.inner.rippleAreaPosition.y());\n\t}\n\tif (_state.forbidden) {\n\t\tp.setOpacity(kForbiddenOpacity);\n\t}\n\tpaintLottieIcon(p, kVoiceToRoundIndex, over);\n\tif (_state.forbidden) {\n\t\tp.setOpacity(1.);\n\t}\n}\n\nvoid SendButton::paintRound(QPainter &p, bool over) {\n\tif (!isDisabled() && !_state.forbidden) {\n\t\tpaintRipple(\n\t\t\tp,\n\t\t\t(width() - _st.inner.rippleAreaSize) / 2,\n\t\t\t_st.inner.rippleAreaPosition.y());\n\t}\n\tif (_state.forbidden) {\n\t\tp.setOpacity(kForbiddenOpacity);\n\t}\n\tpaintLottieIcon(p, kRoundToVoiceIndex, over);\n\tif (_state.forbidden) {\n\t\tp.setOpacity(1.);\n\t}\n}\n\nvoid SendButton::paintLottieIcon(QPainter &p, int index, bool over) {\n\tauto &icon = _voiceRoundIcons[index];\n\tif (!icon) {\n\t\tinitVoiceRoundIcon(index);\n\t} else if (!_voiceRoundAnimating && icon->frameIndex() != 0) {\n\t\ticon->jumpTo(0, [=] { update(); });\n\t}\n\tconst auto color = (isDisabled() || !over)\n\t\t? st::historyRecordVoiceFg->c\n\t\t: st::historyRecordVoiceFgOver->c;\n\ticon->paintInCenter(p, rect(), color);\n}\n\nvoid SendButton::paintSave(QPainter &p, bool over) {\n\tif (!isDisabled()) {\n\t\tauto color = _st.sendIconFg->c;\n\t\tcolor.setAlpha(25);\n\t\tpaintRipple(\n\t\t\tp,\n\t\t\t(width() - _st.inner.rippleAreaSize) / 2,\n\t\t\t_st.inner.rippleAreaPosition.y(),\n\t\t\t&color);\n\t}\n\tconst auto &saveIcon = over\n\t\t? st::historyEditSaveIconOver\n\t\t: st::historyEditSaveIcon;\n\tsaveIcon.paintInCenter(p, rect());\n}\n\nvoid SendButton::paintCancel(QPainter &p, bool over) {\n\tpaintRipple(\n\t\tp,\n\t\t(width() - _st.inner.rippleAreaSize) / 2,\n\t\t_st.inner.rippleAreaPosition.y());\n\n\tconst auto &cancelIcon = over\n\t\t? st::historyReplyCancelIconOver\n\t\t: st::historyReplyCancelIcon;\n\tcancelIcon.paintInCenter(p, rect());\n}\n\nvoid SendButton::paintSend(QPainter &p, bool over) {\n\tconst auto &sendIcon = over ? _st.inner.iconOver : _st.inner.icon;\n\tif (const auto padding = _st.sendIconFillPadding; padding > 0) {\n\t\tconst auto ellipse = sendEllipseRect();\n\t\t{\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tif (_state.fillBgOverride.isValid()) {\n\t\t\t\tp.setBrush(_state.fillBgOverride);\n\t\t\t} else {\n\t\t\t\tp.setBrush(st::windowBgActive);\n\t\t\t}\n\t\t\tp.drawEllipse(ellipse);\n\t\t}\n\t\tif (!isDisabled()) {\n\t\t\tauto color = _st.sendIconFg->c;\n\t\t\tcolor.setAlpha(25);\n\t\t\tpaintRipple(p, ellipse.topLeft(), &color);\n\t\t}\n\t} else if (!isDisabled()) {\n\t\tauto color = _st.sendIconFg->c;\n\t\tcolor.setAlpha(25);\n\t\tpaintRipple(\n\t\t\tp,\n\t\t\t(width() - _st.inner.rippleAreaSize) / 2,\n\t\t\t_st.inner.rippleAreaPosition.y(),\n\t\t\t&color);\n\t}\n\tif (isDisabled()) {\n\t\tconst auto color = st::historyRecordVoiceFg->c;\n\t\tsendIcon.paint(p, _st.sendIconPosition, width(), color);\n\t} else {\n\t\tsendIcon.paint(p, _st.sendIconPosition, width());\n\t}\n}\n\nvoid SendButton::paintStarsToSend(QPainter &p, bool over) {\n\tconst auto geometry = starsGeometry();\n\t{\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tif (_state.fillBgOverride.isValid()) {\n\t\t\tp.setBrush(_state.fillBgOverride);\n\t\t} else {\n\t\t\tp.setBrush(over ? _st.stars.textBgOver : _st.stars.textBg);\n\t\t}\n\t\tconst auto radius = geometry.rounded.height() / 2;\n\t\tp.drawRoundedRect(geometry.rounded, radius, radius);\n\t}\n\tif (!isDisabled()) {\n\t\tauto color = _st.stars.textFg->c;\n\t\tcolor.setAlpha(25);\n\t\tpaintRipple(p, geometry.rounded.topLeft(), &color);\n\t}\n\tp.setPen(over ? _st.stars.textFgOver : _st.stars.textFg);\n\t_starsToSendText.draw(p, {\n\t\t.position = geometry.inner.topLeft(),\n\t\t.outerWidth = width(),\n\t\t.availableWidth = geometry.inner.width(),\n\t});\n}\n\nvoid SendButton::paintSchedule(QPainter &p, bool over) {\n\tconst auto ellipse = scheduleEllipseRect();\n\t{\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(over ? st::historySendIconFgOver : st::historySendIconFg);\n\t\tp.drawEllipse(ellipse);\n\t}\n\tif (!isDisabled()) {\n\t\tauto color = st::historyComposeAreaBg->c;\n\t\tcolor.setAlpha(25);\n\t\tpaintRipple(p, ellipse.topLeft(), &color);\n\t}\n\tst::historyScheduleIcon.paint(\n\t\tp,\n\t\tst::historyScheduleIconPosition,\n\t\twidth());\n}\n\nvoid SendButton::paintSlowmode(QPainter &p) {\n\tp.setFont(st::normalFont);\n\tp.setPen(st::windowSubTextFg);\n\tp.drawText(\n\t\trect().marginsRemoved(st::historySlowmodeCounterMargins),\n\t\t_slowmodeDelayText,\n\t\tstyle::al_center);\n}\n\nSendButton::StarsGeometry SendButton::starsGeometry() const {\n\tconst auto &st = _st.stars;\n\tconst auto inner = QRect(\n\t\t0,\n\t\t0,\n\t\t_starsToSendText.maxWidth(),\n\t\tst.style.font->height);\n\tconst auto rounded = inner.marginsAdded(QMargins(\n\t\tst.padding.left() - st.width / 2,\n\t\tst.padding.top() + st.textTop,\n\t\tst.padding.right() - st.width / 2,\n\t\tst.height - st.padding.top() - st.textTop - st.style.font->height));\n\tconst auto add = (_st.inner.height - rounded.height()) / 2;\n\tconst auto outer = rounded.marginsAdded(QMargins(\n\t\tadd,\n\t\tadd,\n\t\tadd,\n\t\t_st.inner.height - add - rounded.height()));\n\tconst auto shift = -outer.topLeft();\n\treturn {\n\t\t.inner = inner.translated(shift),\n\t\t.rounded = rounded.translated(shift),\n\t\t.outer = outer.translated(shift),\n\t};\n}\n\nSendButton::RippleShape SendButton::currentRippleShape() const {\n\tswitch (_state.type) {\n\tcase Type::Send:\n\t\tif (!_starsToSendText.isEmpty()) {\n\t\t\treturn RippleShape::StarsRoundRect;\n\t\t} else if (_st.sendIconFillPadding > 0) {\n\t\t\treturn RippleShape::SendEllipse;\n\t\t}\n\t\treturn RippleShape::InnerEllipse;\n\tcase Type::Schedule:\n\t\treturn RippleShape::ScheduleEllipse;\n\tcase Type::Save:\n\tcase Type::Record:\n\tcase Type::Round:\n\tcase Type::Cancel:\n\tcase Type::Slowmode:\n\tcase Type::EditPrice:\n\t\treturn RippleShape::InnerEllipse;\n\t}\n\tUnexpected(\"Type in SendButton::currentRippleShape.\");\n}\n\nQRect SendButton::sendEllipseRect() const {\n\tconst auto &sendIcon = _st.inner.icon;\n\tconst auto padding = _st.sendIconFillPadding;\n\treturn QRect(_st.sendIconPosition, sendIcon.size()).marginsAdded(\n\t\t{ padding, padding, padding, padding });\n}\n\nQRect SendButton::scheduleEllipseRect() const {\n\treturn QRect(\n\t\tst::historyScheduleIconPosition,\n\t\tQSize(\n\t\t\tst::historyScheduleIcon.width(),\n\t\t\tst::historyScheduleIcon.height()));\n}\n\nvoid SendButton::updateSize() {\n\tif (_state.type == Type::EditPrice) {\n\t\tresize(0, _st.inner.height);\n\t\treturn;\n\t}\n\tconst auto finalWidth = _starsToSendText.isEmpty()\n\t\t? _st.inner.width\n\t\t: starsGeometry().outer.width();\n\tconst auto progress = _stateChangeAnimation.value(1.);\n\tresize(\n\t\tanim::interpolate(_stateChangeFromWidth, finalWidth, progress),\n\t\t_st.inner.height);\n}\n\nQPixmap SendButton::grabContent() {\n\tauto result = QImage(\n\t\tkWideScale * size() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&result);\n\t\tp.drawPixmap(\n\t\t\t(kWideScale - 1) / 2 * width(),\n\t\t\t(kWideScale - 1) / 2 * height(),\n\t\t\tGrabWidget(this));\n\t}\n\treturn PixmapFromImage(std::move(result));\n}\n\nQImage SendButton::prepareRippleMask() const {\n\tswitch (_lastRippleShape) {\n\tcase RippleShape::InnerEllipse: {\n\t\tconst auto size = _st.inner.rippleAreaSize;\n\t\treturn RippleAnimation::EllipseMask(QSize(size, size));\n\t}\n\tcase RippleShape::SendEllipse: {\n\t\tconst auto r = sendEllipseRect();\n\t\treturn RippleAnimation::EllipseMask(r.size());\n\t}\n\tcase RippleShape::StarsRoundRect: {\n\t\tconst auto r = starsGeometry().rounded;\n\t\tconst auto radius = r.height() / 2;\n\t\treturn RippleAnimation::RoundRectMask(r.size(), radius);\n\t}\n\tcase RippleShape::ScheduleEllipse: {\n\t\tconst auto r = scheduleEllipseRect();\n\t\treturn RippleAnimation::EllipseMask(r.size());\n\t}\n\t}\n\tUnexpected(\"RippleShape in SendButton::prepareRippleMask.\");\n}\n\nQPoint SendButton::prepareRippleStartPosition() const {\n\tconst auto real = mapFromGlobal(QCursor::pos());\n\tswitch (_lastRippleShape) {\n\tcase RippleShape::InnerEllipse: {\n\t\tconst auto size = _st.inner.rippleAreaSize;\n\t\tconst auto y = (height() - size) / 2;\n\t\treturn real - QPoint((width() - size) / 2, y);\n\t}\n\tcase RippleShape::SendEllipse:\n\t\treturn real - sendEllipseRect().topLeft();\n\tcase RippleShape::StarsRoundRect:\n\t\treturn real - starsGeometry().rounded.topLeft();\n\tcase RippleShape::ScheduleEllipse:\n\t\treturn real - scheduleEllipseRect().topLeft();\n\t}\n\tUnexpected(\"RippleShape in SendButton::prepareRippleStartPosition.\");\n}\n\nvoid SendButton::initVoiceRoundIcon(int index) {\n\tExpects(index >= 0 && index < 2);\n\n\t_voiceRoundIcons[index] = Lottie::MakeIcon({\n\t\t.path = ((index == kVoiceToRoundIndex)\n\t\t\t? u\":/animations/chat/voice_to_video.tgs\"_q\n\t\t\t: u\":/animations/chat/video_to_voice.tgs\"_q),\n\t\t.sizeOverride = _st.recordSize,\n\t\t.colorizeUsingAlpha = true,\n\t});\n}\n\nvoid SendButton::paintVoiceRoundIcon(QPainter &p, bool over) {\n\tif (!isDisabled() && !_state.forbidden) {\n\t\tpaintRipple(\n\t\t\tp,\n\t\t\t(width() - _st.inner.rippleAreaSize) / 2,\n\t\t\t_st.inner.rippleAreaPosition.y());\n\t}\n\n\tif (_state.forbidden) {\n\t\tp.setOpacity(kForbiddenOpacity);\n\t}\n\tconst auto color = (isDisabled() || !over)\n\t\t? st::historyRecordVoiceFg->c\n\t\t: st::historyRecordVoiceFgOver->c;\n\tconst auto toVideo = (_state.type == Type::Round);\n\tconst auto index = toVideo ? kVoiceToRoundIndex : kRoundToVoiceIndex;\n\t_voiceRoundIcons[index]->paintInCenter(p, rect(), color);\n\tif (_state.forbidden) {\n\t\tp.setOpacity(1.);\n\t}\n}\n\nbool SendButton::isVoiceRoundTransition(Type from, Type to) {\n\treturn (from == Type::Record && to == Type::Round)\n\t\t|| (from == Type::Round && to == Type::Record);\n}\n\nSendStarButton::SendStarButton(\n\tQWidget *parent,\n\tconst style::IconButton &st,\n\tconst style::RoundButton &counterSt,\n\trpl::producer<SendStarButtonState> state)\n: RippleButton(parent, st.ripple)\n, _st(st)\n, _counterSt(counterSt) {\n\tresize(_st.width, _st.height);\n\n\tstd::move(state) | rpl::on_next([=](SendStarButtonState value) {\n\t\tsetCount(value.count);\n\t\thighlight(value.highlight);\n\t}, lifetime());\n}\n\nvoid SendStarButton::paintEvent(QPaintEvent *e) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto fullSize = size() * ratio;\n\tif (_frame.size() != fullSize) {\n\t\t_frame = QImage(fullSize, QImage::Format_ARGB32_Premultiplied);\n\t\t_frame.setDevicePixelRatio(ratio);\n\t}\n\t_frame.fill(Qt::transparent);\n\n\tauto p = QPainter(&_frame);\n\n\tconst auto highlighted = _highlight.value(_highlighted ? 1. : 0.);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(_counterSt.textBg);\n\tp.drawEllipse(rect());\n\tpaintRipple(p, QPoint());\n\tif (highlighted > 0.) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setBrush(st::creditsBg3);\n\t\tp.setOpacity(highlighted);\n\t\tp.drawEllipse(rect());\n\t\tp.setOpacity(1.);\n\t}\n\n\tst::starIconEmoji.icon.paintInCenter(p, rect(), st::premiumButtonFg->c);\n\n\tif (!_starsText.isEmpty()) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tauto pen = st::transparent->p;\n\t\tpen.setWidthF(_counterSt.numbersSkip);\n\t\tp.setPen(pen);\n\t\tp.setBrush(\n\t\t\tanim::brush(_counterSt.textBg, st::creditsBg3, highlighted));\n\t\tconst auto size = QSize(\n\t\t\t_starsText.maxWidth(),\n\t\t\t_counterSt.style.font->height);\n\t\tconst auto larger = size.grownBy(_counterSt.padding);\n\t\tconst auto left = (width() - larger.width());\n\t\tconst auto top = 0;\n\t\tconst auto r = larger.height() / 2.;\n\t\tp.drawRoundedRect(left, top, larger.width(), larger.height(), r, r);\n\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\tp.setPen(\n\t\t\tanim::pen(_counterSt.textFg, st::premiumButtonFg, highlighted));\n\t\t_starsText.draw(p, {\n\t\t\t.position = QPoint(\n\t\t\t\tleft + _counterSt.padding.left(),\n\t\t\t\ttop + _counterSt.padding.top()),\n\t\t\t.availableWidth = _starsText.maxWidth(),\n\t\t});\n\t}\n\n\tQPainter(this).drawImage(0, 0, _frame);\n}\n\nvoid SendStarButton::setCount(int count) {\n\tif (_count == count) {\n\t\treturn;\n\t}\n\t_count = count;\n\tif (_count) {\n\t\t_starsText.setText(\n\t\t\t_counterSt.style,\n\t\t\tLang::FormatCountDecimal(_count));\n\t\tconst auto sub = _counterSt.padding.left()\n\t\t\t+ _counterSt.padding.right();\n\t\tif (_starsText.maxWidth() > width() - sub) {\n\t\t\t_starsText.setText(\n\t\t\t\t_counterSt.style,\n\t\t\t\tLang::FormatCountToShort(_count).string);\n\t\t}\n\t} else {\n\t\t_starsText = Text::String();\n\t}\n\tupdate();\n}\n\nvoid SendStarButton::highlight(bool enabled) {\n\tif (_highlighted == enabled) {\n\t\treturn;\n\t}\n\t_highlighted = enabled;\n\t_highlight.start(\n\t\t[=] { update(); },\n\t\tenabled ? 0. : 1.,\n\t\tenabled ? 1. : 0.,\n\t\t360,\n\t\tanim::easeOutCirc);\n}\n\nQImage SendStarButton::prepareRippleMask() const {\n\treturn RippleAnimation::EllipseMask(size());\n}\n\nQPoint SendStarButton::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos());\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/send_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/buttons.h\"\n\n#include <memory>\n\nnamespace Lottie {\nclass Icon;\n} // namespace Lottie\n\nnamespace style {\nstruct SendButton;\nstruct IconButton;\nstruct RoundButton;\n} // namespace style\n\nnamespace Ui {\n\nclass SendButton final : public RippleButton {\npublic:\n\tSendButton(QWidget *parent, const style::SendButton &st);\n\t~SendButton();\n\n\tstatic constexpr auto kSlowmodeDelayLimit = 100 * 60;\n\n\tenum class Type {\n\t\tSend,\n\t\tSchedule,\n\t\tSave,\n\t\tRecord,\n\t\tRound,\n\t\tCancel,\n\t\tSlowmode,\n\t\tEditPrice,\n\t};\n\tstruct State {\n\t\tType type = Type::Send;\n\t\tQColor fillBgOverride;\n\t\tint slowmodeDelay = 0;\n\t\tint starsToSend = 0;\n\t\tbool forbidden = false;\n\n\t\tfriend inline bool operator==(State, State) = default;\n\t};\n\t[[nodiscard]] Type type() const {\n\t\treturn _state.type;\n\t}\n\t[[nodiscard]] State state() const {\n\t\treturn _state;\n\t}\n\tvoid setState(State state);\n\tvoid finishAnimating();\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\nprivate:\n\tstruct StarsGeometry {\n\t\tQRect inner;\n\t\tQRect rounded;\n\t\tQRect outer;\n\t};\n\tenum class RippleShape : uchar {\n\t\tInnerEllipse,\n\t\tSendEllipse,\n\t\tStarsRoundRect,\n\t\tScheduleEllipse,\n\t};\n\n\t[[nodiscard]] QPixmap grabContent();\n\tvoid updateSize();\n\n\t[[nodiscard]] StarsGeometry starsGeometry() const;\n\n\t[[nodiscard]] RippleShape currentRippleShape() const;\n\t[[nodiscard]] QRect sendEllipseRect() const;\n\t[[nodiscard]] QRect scheduleEllipseRect() const;\n\n\tvoid paintRecord(QPainter &p, bool over);\n\tvoid paintRound(QPainter &p, bool over);\n\tvoid paintSave(QPainter &p, bool over);\n\tvoid paintCancel(QPainter &p, bool over);\n\tvoid paintSend(QPainter &p, bool over);\n\tvoid paintSchedule(QPainter &p, bool over);\n\tvoid paintSlowmode(QPainter &p);\n\tvoid paintStarsToSend(QPainter &p, bool over);\n\n\tvoid initVoiceRoundIcon(int index);\n\tvoid paintVoiceRoundIcon(QPainter &p, bool over);\n\t[[nodiscard]] static bool isVoiceRoundTransition(Type from, Type to);\n\tvoid paintLottieIcon(QPainter &p, int index, bool over);\n\n\tconst style::SendButton &_st;\n\n\tState _state;\n\tQPixmap _contentFrom, _contentTo;\n\n\tUi::Animations::Simple _stateChangeAnimation;\n\tint _stateChangeFromWidth = 0;\n\n\tQString _slowmodeDelayText;\n\tUi::Text::String _starsToSendText;\n\n\tstd::array<std::unique_ptr<Lottie::Icon>, 2> _voiceRoundIcons;\n\tbool _voiceRoundAnimating = false;\n\tRippleShape _lastRippleShape = RippleShape::SendEllipse;\n\n};\n\nstruct SendStarButtonState {\n\tint count = 0;\n\tbool highlight = false;\n};\n\nclass SendStarButton final : public RippleButton {\npublic:\n\tSendStarButton(\n\t\tQWidget *parent,\n\t\tconst style::IconButton &st,\n\t\tconst style::RoundButton &counterSt,\n\t\trpl::producer<SendStarButtonState> state);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\nprivate:\n\tvoid setCount(int count);\n\tvoid highlight(bool enabled);\n\n\tconst style::IconButton &_st;\n\tconst style::RoundButton &_counterSt;\n\n\tQImage _frame;\n\tUi::Text::String _starsText;\n\tUi::Animations::Simple _highlight;\n\tint _count = 0;\n\tbool _highlighted = false;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/silent_toggle.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/silent_toggle.h\"\n\n#include \"ui/effects/ripple_animation.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/data_session.h\"\n#include \"data/data_channel.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Ui {\n\nSilentToggle::SilentToggle(QWidget *parent, not_null<ChannelData*> channel)\n: RippleButton(parent, st::historySilentToggle.ripple)\n, _st(st::historySilentToggle)\n, _channel(channel)\n, _checked(channel->owner().notifySettings().silentPosts(_channel)) {\n\tExpects(!channel->owner().notifySettings().silentPostsUnknown(_channel));\n\n\tresize(_st.width, _st.height);\n\n\tsetMouseTracking(true);\n\tsetAccessibleName(tr::lng_broadcast_silent(tr::now));\n\n\tclicks(\n\t) | rpl::on_next([=] {\n\t\tsetChecked(!_checked);\n\t\tUi::Tooltip::Show(0, this);\n\t\t_channel->owner().notifySettings().update(_channel, {}, _checked);\n\t}, lifetime());\n}\n\nvoid SilentToggle::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tpaintRipple(p, _st.rippleAreaPosition, nullptr);\n\n\t//const auto checked = _crossLineAnimation.value(_checked ? 1. : 0.);\n\tconst auto over = isOver();\n\t(_checked\n\t\t? (over\n\t\t\t? st::historySilentToggleOnOver\n\t\t\t: st::historySilentToggleOn)\n\t\t: (over\n\t\t\t? st::historySilentToggle.iconOver\n\t\t\t: st::historySilentToggle.icon)).paintInCenter(p, rect());\n}\n\nvoid SilentToggle::mouseMoveEvent(QMouseEvent *e) {\n\tRippleButton::mouseMoveEvent(e);\n\tif (rect().contains(e->pos())) {\n\t\tUi::Tooltip::Show(1000, this);\n\t} else {\n\t\tUi::Tooltip::Hide();\n\t}\n}\n\nvoid SilentToggle::setChecked(bool checked) {\n\tif (_checked != checked) {\n\t\t_checked = checked;\n\t\tupdate();\n        accessibilityStateChanged({ .checked = true });\n\t\t// _crossLineAnimation.start(\n\t\t// \t[=] { update(); },\n\t\t// \t_checked ? 0. : 1.,\n\t\t// \t_checked ? 1. : 0.,\n\t\t// \tkAnimationDuration);\n\t}\n}\n\nvoid SilentToggle::leaveEventHook(QEvent *e) {\n\tRippleButton::leaveEventHook(e);\n\tUi::Tooltip::Hide();\n}\n\nQString SilentToggle::tooltipText() const {\n\treturn _checked\n\t\t? tr::lng_wont_be_notified(tr::now)\n\t\t: tr::lng_will_be_notified(tr::now);\n}\n\nQPoint SilentToggle::tooltipPos() const {\n\treturn QCursor::pos();\n}\n\nbool SilentToggle::tooltipWindowActive() const {\n\treturn Ui::AppInFocus() && InFocusChain(window());\n}\n\nQPoint SilentToggle::prepareRippleStartPosition() const {\n\tconst auto result = mapFromGlobal(QCursor::pos())\n\t\t- _st.rippleAreaPosition;\n\tconst auto rect = QRect(0, 0, _st.rippleAreaSize, _st.rippleAreaSize);\n\treturn rect.contains(result)\n\t\t? result\n\t\t: DisabledRippleStartPosition();\n}\n\nQImage SilentToggle::prepareRippleMask() const {\n\treturn RippleAnimation::EllipseMask(\n\t\tQSize(_st.rippleAreaSize, _st.rippleAreaSize));\n}\n\nQAccessible::Role SilentToggle::accessibilityRole() {\n    return QAccessible::Role::CheckBox;\n}\n\nUi::AccessibilityState SilentToggle::accessibilityState() const {\n\treturn { .checkable = true, .checked = _checked };\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/silent_toggle.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/tooltip.h\"\n\nnamespace Ui {\n\nclass SilentToggle final\n\t: public RippleButton\n\t, public AbstractTooltipShower {\npublic:\n\tSilentToggle(QWidget *parent, not_null<ChannelData*> channel);\n\n\tvoid setChecked(bool checked);\n\tbool checked() const {\n\t\treturn _checked;\n\t}\n\n\t// AbstractTooltipShower interface\n\tQString tooltipText() const override;\n\tQPoint tooltipPos() const override;\n\tbool tooltipWindowActive() const override;\n\t[[nodiscard]] QAccessible::Role accessibilityRole() override;\n\t[[nodiscard]] AccessibilityState accessibilityState() const override;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\nprivate:\n\tconst style::IconButton &_st;\n\n\tnot_null<ChannelData*> _channel;\n\tbool _checked = false;\n\n\t// Animations::Simple _crossLineAnimation;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/stars_rating.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/stars_rating.h\"\n\n#include \"base/unixtime.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/controls/feature_list.h\"\n#include \"ui/effects/premium_bubble.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/layers/show.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/custom_emoji_text_badge.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_chat.h\" // textMoreIconEmoji\n#include \"styles/style_info.h\"\n#include \"styles/style_info_levels.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_media_view.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kAutoCollapseTimeout = 4 * crl::time(1000);\n\nusing Counters = Data::StarsRating;\n\n[[nodiscard]] Counters AdjustByReached(Counters data) {\n\tif (data.stars < 0) {\n\t\treturn data;\n\t}\n\tconst auto reached = !data.nextLevelStars;\n\tif (reached) {\n\t\t--data.level;\n\t\tdata.stars = data.nextLevelStars = std::max({\n\t\t\tdata.stars,\n\t\t\tdata.thisLevelStars,\n\t\t\t1\n\t\t});\n\t\tdata.thisLevelStars = 0;\n\t} else {\n\t\tdata.stars = std::max(data.thisLevelStars, data.stars);\n\t\tdata.nextLevelStars = std::max(\n\t\t\tdata.nextLevelStars,\n\t\t\tdata.stars + 1);\n\t}\n\treturn data;\n}\n\n[[nodiscard]] Fn<Premium::BubbleText(int)> BubbleTextFactory(\n\t\tint countForScale,\n\t\tint nextLevelCounter) {\n\treturn [=](int count) {\n\t\tconst auto counter = [&](int count) {\n\t\t\treturn (countForScale < 10'000)\n\t\t\t\t? Lang::FormatCountDecimal(count)\n\t\t\t\t: (countForScale < 10'000'000)\n\t\t\t\t? (Lang::FormatCountDecimal((count / 100) / 10.) + 'K')\n\t\t\t\t: (Lang::FormatCountDecimal((count / 100'000) / 10.) + 'M');\n\t\t};\n\t\treturn Premium::BubbleText{\n\t\t\t.counter = counter(count),\n\t\t\t.additional = (nextLevelCounter\n\t\t\t\t? (u\"/\"_q + counter(nextLevelCounter))\n\t\t\t\t: QString()),\n\t\t};\n\t};\n}\n\nvoid FillRatingLimit(\n\t\trpl::producer<> showFinished,\n\t\tnot_null<VerticalLayout*> container,\n\t\trpl::producer<Counters> data,\n\t\tPremium::BubbleType type,\n\t\tstyle::margins limitLinePadding,\n\t\tint starsForScale,\n\t\tint nextLevelStars,\n\t\tbool hideCount) {\n\tconst auto addSkip = [&](int skip) {\n\t\tcontainer->add(object_ptr<FixedHeightWidget>(container, skip));\n\t};\n\n\tconst auto negative = (type == Premium::BubbleType::NegativeRating);\n\tconst auto ratio = [=](Counters rating) {\n\t\tif (negative) {\n\t\t\treturn 0.5;\n\t\t}\n\t\tconst auto min = rating.thisLevelStars;\n\t\tconst auto max = rating.nextLevelStars;\n\n\t\tAssert(rating.stars >= min && rating.stars <= max);\n\t\tconst auto count = (max - min);\n\t\tconst auto index = (rating.stars - min);\n\t\tif (!index) {\n\t\t\treturn 0.;\n\t\t} else if (index == count) {\n\t\t\treturn 1.;\n\t\t} else if (count == 2) {\n\t\t\treturn 0.5;\n\t\t}\n\t\tconst auto available = st::boxWideWidth\n\t\t\t- st::boxPadding.left()\n\t\t\t- st::boxPadding.right();\n\t\tconst auto average = available / float64(count);\n\t\tconst auto levelWidth = [&](int add) {\n\t\t\treturn st::normalFont->width(\n\t\t\t\ttr::lng_boost_level(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\trating.level + add));\n\t\t};\n\t\tconst auto paddings = 2 * st::premiumLineTextSkip;\n\t\tconst auto labelLeftWidth = paddings + levelWidth(0);\n\t\tconst auto labelRightWidth = paddings + levelWidth(1);\n\t\tconst auto first = std::max(average, labelLeftWidth * 1.);\n\t\tconst auto last = std::max(average, labelRightWidth * 1.);\n\t\tconst auto other = (available - first - last) / (count - 2);\n\t\treturn (first + (index - 1) * other) / available;\n\t};\n\n\tauto adjustedData = rpl::duplicate(data) | rpl::map(AdjustByReached);\n\n\tauto bubbleRowState = rpl::duplicate(\n\t\tadjustedData\n\t) | rpl::combine_previous(\n\t\tCounters()\n\t) | rpl::map([=](Counters previous, Counters counters) {\n\t\treturn Premium::BubbleRowState{\n\t\t\t.counter = counters.stars,\n\t\t\t.ratio = ratio(counters),\n\t\t\t.animateFromZero = (counters.level != previous.level),\n\t\t\t.dynamic = true,\n\t\t};\n\t});\n\tPremium::AddBubbleRow(\n\t\tcontainer,\n\t\t(hideCount ? st::iconOnlyPremiumBubble : st::starRatingBubble),\n\t\tstd::move(showFinished),\n\t\trpl::duplicate(bubbleRowState),\n\t\ttype,\n\t\t(hideCount\n\t\t\t? [](int) { return Premium::BubbleText(); }\n\t\t\t: BubbleTextFactory(starsForScale, nextLevelStars)),\n\t\tnegative ? &st::levelNegativeBubble : &st::infoStarsCrown,\n\t\tlimitLinePadding);\n\taddSkip(st::premiumLineTextSkip);\n\n\tconst auto level = [](int level) {\n\t\treturn tr::lng_boost_level(tr::now, lt_count, level);\n\t};\n\tauto limitState = std::move(\n\t\tbubbleRowState\n\t) | rpl::map([negative](const Premium::BubbleRowState &state) {\n\t\treturn Premium::LimitRowState{\n\t\t\t.ratio = negative ? 0.5 : state.ratio,\n\t\t\t.animateFromZero = !negative && state.animateFromZero,\n\t\t\t.dynamic = state.dynamic\n\t\t};\n\t});\n\tauto left = rpl::duplicate(\n\t\tadjustedData\n\t) | rpl::map([=](Counters counters) {\n\t\treturn (counters.level < 0) ? QString() : level(counters.level);\n\t});\n\tauto right = rpl::duplicate(\n\t\tadjustedData\n\t) | rpl::map([=](Counters counters) {\n\t\treturn (counters.level < 0)\n\t\t\t? tr::lng_stars_rating_negative_label(tr::now)\n\t\t\t: level(counters.level + 1);\n\t});\n\tPremium::AddLimitRow(\n\t\tcontainer,\n\t\t(negative ? st::negativeStarsLimits : st::boostLimits),\n\t\tPremium::LimitRowLabels{\n\t\t\t.leftLabel = std::move(left),\n\t\t\t.rightLabel = std::move(right),\n\t\t\t.activeLineBg = [=] { return negative\n\t\t\t\t? st::attentionButtonFg->b\n\t\t\t\t: st::windowBgActive->b;\n\t\t\t},\n\t\t},\n\t\tstd::move(limitState),\n\t\tlimitLinePadding);\n}\n\nvoid AboutRatingBox(\n\t\tnot_null<GenericBox*> box,\n\t\tconst QString &name,\n\t\tCounters data,\n\t\tData::StarsRatingPending pending) {\n\tbox->setWidth(st::boxWideWidth);\n\tbox->setStyle(st::boostBox);\n\n\tstruct State {\n\t\trpl::variable<Counters> data;\n\t\trpl::variable<bool> pending;\n\t\trpl::variable<bool> full;\n\t};\n\tconst auto state = box->lifetime().make_state<State>();\n\tstate->data = data;\n\n\tFillRatingLimit(\n\t\tBoxShowFinishes(box),\n\t\tbox->verticalLayout(),\n\t\tstate->data.value(),\n\t\t(data.level < 0\n\t\t\t? Premium::BubbleType::NegativeRating\n\t\t\t: Premium::BubbleType::StarRating),\n\t\tst::boxRowPadding,\n\t\tdata.stars,\n\t\tdata.nextLevelStars,\n\t\t(data.level < 0 && !data.stars));\n\n\tbox->setMaxHeight(st::boostBoxMaxHeight);\n\n\tauto title = rpl::conditional(\n\t\tstate->pending.value(),\n\t\ttr::lng_stars_rating_future(),\n\t\ttr::lng_stars_rating_title());\n\n\tauto text = !name.isEmpty()\n\t\t? tr::lng_stars_rating_about(\n\t\t\tlt_name,\n\t\t\trpl::single(TextWithEntities{ name }),\n\t\t\ttr::rich) | rpl::type_erased\n\t\t: tr::lng_stars_rating_about_your(\n\t\t\ttr::rich) | rpl::type_erased;\n\n\tif (data.level < 0) {\n\t\tauto text = (data.stars < 0)\n\t\t\t? tr::lng_stars_rating_negative_your(\n\t\t\t\tlt_count_decimal,\n\t\t\t\trpl::single(-data.stars * 1.),\n\t\t\t\ttr::rich)\n\t\t\t: tr::lng_stars_rating_negative(\n\t\t\t\tlt_name,\n\t\t\t\trpl::single(TextWithEntities{ name }),\n\t\t\t\ttr::rich);\n\t\tbox->addRow(\n\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\tstd::move(text),\n\t\t\t\tst::boostTextNegative),\n\t\t\t(st::boxRowPadding\n\t\t\t\t+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)),\n\t\t\tstyle::al_top\n\t\t)->setTryMakeSimilarLines(true);\n\t}\n\n\tbox->addRow(\n\t\tobject_ptr<FlatLabel>(box, std::move(title), st::infoStarsTitle),\n\t\tst::boxRowPadding + QMargins(0, st::boostTitleSkip / 2, 0, 0),\n\t\tstyle::al_top);\n\n\tif (pending) {\n\t\tconst auto now = base::unixtime::now();\n\t\tconst auto days = std::max((pending.date - now + 43200) / 86400, 1);\n\t\tauto text = state->pending.value(\n\t\t) | rpl::map([=](bool value) {\n\t\t\treturn tr::lng_stars_rating_pending(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_decimal,\n\t\t\t\tpending.value.stars - data.stars,\n\t\t\t\tlt_when,\n\t\t\t\tTextWithEntities{\n\t\t\t\t\ttr::lng_stars_rating_updates(tr::now, lt_count, days),\n\t\t\t\t},\n\t\t\t\tlt_link,\n\t\t\t\tText::Link((value\n\t\t\t\t\t? tr::lng_stars_rating_pending_back\n\t\t\t\t\t: tr::lng_stars_rating_pending_preview)(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_arrow,\n\t\t\t\t\t\tText::IconEmoji(&st::textMoreIconEmoji),\n\t\t\t\t\t\ttr::marked)),\n\t\t\t\ttr::rich);\n\t\t});\n\t\tconst auto aboutPending = box->addRow(\n\t\t\tobject_ptr<FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\tstd::move(text),\n\t\t\t\tst::boostTextPending),\n\t\t\t(st::boxRowPadding\n\t\t\t\t+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)),\n\t\t\tstyle::al_top);\n\t\taboutPending->setTryMakeSimilarLines(true);\n\t\taboutPending->setClickHandlerFilter([=](const auto &...) {\n\t\t\tstate->pending = !state->pending.current();\n\t\t\tstate->data = state->pending.current()\n\t\t\t\t? pending.value\n\t\t\t\t: data;\n\t\t\tbox->verticalLayout()->resizeToWidth(box->width());\n\t\t\treturn false;\n\t\t});\n\t}\n\n\tbox->addRow(\n\t\tobject_ptr<FlatLabel>(\n\t\t\tbox,\n\t\t\tstd::move(text),\n\t\t\tst::boostText),\n\t\t(st::boxRowPadding\n\t\t\t+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)),\n\t\tstyle::al_top\n\t)->setTryMakeSimilarLines(true);\n\n\tauto helper = Text::CustomEmojiHelper();\n\tconst auto makeBadge = [&](\n\t\t\tconst QString &text,\n\t\t\tconst style::RoundButton &st) {\n\t\treturn helper.paletteDependent(\n\t\t\tText::CustomEmojiTextBadge(text.toUpper(), st));\n\t};\n\tconst auto makeActive = [&](const QString &text) {\n\t\treturn makeBadge(text, st::customEmojiTextBadge);\n\t};\n\tconst auto makeInactive = [&](const QString &text) {\n\t\treturn makeBadge(text, st::infoRatingDeductedBadge);\n\t};\n\tconst auto features = std::vector<FeatureListEntry>{\n\t\t{\n\t\t\tst::menuIconRatingGifts,\n\t\t\ttr::lng_stars_title_gifts_telegram(tr::now),\n\t\t\ttr::lng_stars_about_gifts_telegram(\n\t\t\t\ttr::now,\n\t\t\t\tlt_emoji,\n\t\t\t\tmakeActive(tr::lng_stars_rating_added(tr::now)),\n\t\t\t\ttr::rich),\n\t\t},\n\t\t{\n\t\t\tst::menuIconRatingUsers,\n\t\t\ttr::lng_stars_title_gifts_users(tr::now),\n\t\t\ttr::lng_stars_about_gifts_users(\n\t\t\t\ttr::now,\n\t\t\t\tlt_emoji,\n\t\t\t\tmakeActive(tr::lng_stars_rating_added(tr::now)),\n\t\t\t\ttr::rich),\n\t\t},\n\t\t{\n\t\t\tst::menuIconRatingRefund,\n\t\t\ttr::lng_stars_title_refunds(tr::now),\n\t\t\ttr::lng_stars_about_refunds(\n\t\t\t\ttr::now,\n\t\t\t\tlt_emoji,\n\t\t\t\tmakeInactive(tr::lng_stars_rating_deducted(tr::now)),\n\t\t\t\ttr::rich),\n\t\t},\n\t};\n\tconst auto context = helper.context();\n\tfor (const auto &feature : features) {\n\t\tbox->addRow(MakeFeatureListEntry(box, feature, context));\n\t}\n\tbox->addButton(rpl::single(QString()), [=] {\n\t\tbox->closeBox();\n\t})->setText(rpl::single(Text::IconEmoji(\n\t\t&st::infoStarsUnderstood\n\t).append(' ').append(tr::lng_stars_rating_understood(tr::now))));\n}\n\n[[nodiscard]] not_null<const style::LevelShape*> SelectShape(int level) {\n\tif (level < 0) {\n\t\treturn &st::levelNegative;\n\t}\n\tstruct Shape {\n\t\tint level = 0;\n\t\tnot_null<const style::LevelShape*> shape;\n\t};\n\tconst auto list = std::vector<Shape>{\n\t\t{ 1, &st::level1 },\n\t\t{ 2, &st::level2 },\n\t\t{ 3, &st::level3 },\n\t\t{ 4, &st::level4 },\n\t\t{ 5, &st::level5 },\n\t\t{ 6, &st::level6 },\n\t\t{ 7, &st::level7 },\n\t\t{ 8, &st::level8 },\n\t\t{ 9, &st::level9 },\n\t\t{ 10, &st::level10 },\n\t\t{ 20, &st::level20 },\n\t\t{ 30, &st::level30 },\n\t\t{ 40, &st::level40 },\n\t\t{ 50, &st::level50 },\n\t\t{ 60, &st::level60 },\n\t\t{ 70, &st::level70 },\n\t\t{ 80, &st::level80 },\n\t\t{ 90, &st::level90 },\n\t};\n\tconst auto i = ranges::lower_bound(\n\t\tlist,\n\t\tlevel + 1,\n\t\tranges::less(),\n\t\t&Shape::level);\n\treturn (i != begin(list)) ? (i - 1)->shape : list.front().shape;\n}\n\n} // namespace\n\nStarsRating::StarsRating(\n\tQWidget *parent,\n\tstd::shared_ptr<Show> show,\n\tconst QString &name,\n\trpl::producer<Counters> value,\n\tFn<Data::StarsRatingPending()> pending)\n: _widget(std::make_unique<AbstractButton>(parent))\n, _show(std::move(show))\n, _name(name)\n, _value(std::move(value))\n, _pending(std::move(pending)) {\n\tinit();\n}\n\nStarsRating::~StarsRating() = default;\n\nvoid StarsRating::init() {\n\t_widget->setPointerCursor(true);\n\n\t_widget->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(_widget.get());\n\t\tpaint(p);\n\t}, lifetime());\n\n\t_widget->setClickedCallback([=] {\n\t\tif (!_value.current()) {\n\t\t\treturn;\n\t\t}\n\t\t_show->show(Box(AboutRatingBox, _name, _value.current(), _pending\n\t\t\t? _pending()\n\t\t\t: Data::StarsRatingPending()));\n\t});\n\n\t_widget->resize(_widget->width(), st::level1.icon.height());\n\n\t_value.value() | rpl::on_next([=](Counters rating) {\n\t\tupdateData(rating);\n\t}, lifetime());\n}\n\nvoid StarsRating::updateData(Data::StarsRating rating) {\n\tif (!rating) {\n\t\t_shape = nullptr;\n\t\t_widthValue = 0;\n\t\t_currentLevel = 0;\n\t} else {\n\t\t_shape = SelectShape(rating.level);\n\t\t_collapsedText = (rating.level < 0\n\t\t\t? QString()\n\t\t\t: Lang::FormatCountDecimal(rating.level));\n\t\tconst auto &margin = st::levelMargin;\n\t\t_widthValue = _shape->icon.width() + margin.right() - margin.left();\n\t\t_currentLevel = rating.level;\n\t}\n\tupdateWidth();\n\t_widget->setAccessibleName(tr::lng_boost_level(tr::now, lt_count, rating.level));\n}\n\nvoid StarsRating::updateWidth() {\n\tif (const auto widthToRight = _widthValue.current()) {\n\t\tconst auto &margin = st::levelMargin;\n\t\t_widget->resize(margin.left() + widthToRight, _widget->height());\n\t\t_widget->update();\n\t} else {\n\t\t_widget->resize(0, _widget->height());\n\t}\n}\n\nvoid StarsRating::raise() {\n\t_widget->raise();\n}\n\nvoid StarsRating::moveTo(int x, int y) {\n\t_widget->move(x - st::levelMargin.left(), y - st::levelMargin.top());\n}\n\nvoid StarsRating::setOpacity(float64 opacity) {\n\t_opacity = opacity;\n\t_widget->update();\n}\n\nvoid StarsRating::setCustomColors(\n\t\tstd::optional<QColor> textColor,\n\t\tstd::optional<QColor> shapeColor) {\n\t_customTextColor = textColor;\n\t_customShapeColor = shapeColor;\n\t_cachedLevel = std::numeric_limits<int>::min();\n\t_widget->update();\n}\n\nvoid StarsRating::paint(QPainter &p) {\n\tp.setOpacity(_opacity);\n\tif (!_shape) {\n\t\treturn;\n\t}\n\n\tif (_cachedLevel != _currentLevel) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto size = _widget->size() * ratio;\n\t\t_cache = QImage(size, QImage::Format_ARGB32_Premultiplied);\n\t\t_cache.setDevicePixelRatio(ratio);\n\t\t_cache.fill(Qt::transparent);\n\n\t\tauto q = QPainter(&_cache);\n\t\tif (_customShapeColor) {\n\t\t\t_shape->icon.paint(q, 0, 0, _widget->width(), *_customShapeColor);\n\t\t} else {\n\t\t\t_shape->icon.paint(q, 0, 0, _widget->width());\n\t\t}\n\n\t\tif (!_collapsedText.isEmpty()) {\n\t\t\tq.setPen(_customTextColor\n\t\t\t\t? *_customTextColor\n\t\t\t\t: st::levelTextFg->c);\n\t\t\tq.setFont(st::levelStyle.font);\n\t\t\tq.drawText(\n\t\t\t\tRect(_shape->icon.size()),\n\t\t\t\tQt::AlignCenter,\n\t\t\t\t_collapsedText);\n\t\t}\n\n\t\t_cachedLevel = _currentLevel;\n\t}\n\n\tp.setOpacity(_opacity);\n\tp.drawImage(0, 0, _cache);\n}\n\nrpl::producer<int> StarsRating::widthValue() const {\n\treturn _widthValue.value();\n}\n\nint StarsRating::width() const {\n\treturn _widthValue.current();\n}\n\nrpl::lifetime &StarsRating::lifetime() {\n\treturn _widget->lifetime();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/stars_rating.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n#include \"data/data_peer_common.h\"\n\nnamespace style {\nstruct Toast;\nstruct LevelShape;\n} // namespace style\n\nnamespace Ui::Toast {\nclass Instance;\n} // namespace Ui::Toast\n\nnamespace Ui {\n\nclass ImportantTooltip;\nclass AbstractButton;\nclass FlatLabel;\nclass Show;\n\nclass StarsRating final {\npublic:\n\tStarsRating(\n\t\tQWidget *parent,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tconst QString &name,\n\t\trpl::producer<Data::StarsRating> value,\n\t\tFn<Data::StarsRatingPending()> pending);\n\t~StarsRating();\n\n\tvoid raise();\n\tvoid moveTo(int x, int y);\n\tvoid setOpacity(float64 opacity);\n\tvoid setCustomColors(\n\t\tstd::optional<QColor> textColor,\n\t\tstd::optional<QColor> shapeColor);\n\n\t[[nodiscard]] rpl::producer<int> widthValue() const;\n\t[[nodiscard]] int width() const;\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tvoid init();\n\tvoid paint(QPainter &p);\n\tvoid updateData(Data::StarsRating rating);\n\tvoid updateWidth();\n\n\tconst std::unique_ptr<Ui::AbstractButton> _widget;\n\tconst std::shared_ptr<Ui::Show> _show;\n\tconst QString _name;\n\n\tQString _collapsedText;\n\n\trpl::variable<Data::StarsRating> _value;\n\tFn<Data::StarsRatingPending()> _pending;\n\trpl::variable<int> _widthValue;\n\tconst style::LevelShape *_shape = nullptr;\n\n\tQImage _cache;\n\tint _cachedLevel = std::numeric_limits<int>::min();\n\n\tint _currentLevel = 0;\n\tfloat64 _opacity = 1.;\n\n\tstd::optional<QColor> _customTextColor;\n\tstd::optional<QColor> _customShapeColor;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/sub_tabs.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/sub_tabs.h\"\n\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/effects/animation_value_f.h\"\n#include \"styles/style_basic.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_info.h\"\n\n#include <QApplication>\n\nnamespace Ui {\n\nSubTabs::SubTabs(\n\tQWidget *parent,\n\tconst style::SubTabs &st,\n\tOptions options,\n\tstd::vector<Tab> tabs,\n\tText::MarkedContext context)\n: RpWidget(parent)\n, _st(st)\n, _centered(options.centered) {\n\tsetMouseTracking(true);\n\t_reorderScrollAnimation.init([this] { updateScrollCallback(); });\n\tsetTabs(std::move(tabs), context);\n\tif (!options.selected.isEmpty()) {\n\t\tsetActiveTab(options.selected);\n\t}\n}\n\nvoid SubTabs::setTabs(\n\t\tstd::vector<Tab> tabs,\n\t\tText::MarkedContext context) {\n\tauto x = st::giftBoxTabsMargin.left();\n\tauto y = st::giftBoxTabsMargin.top();\n\n\tsetSelected(-1);\n\t_buttons.resize(tabs.size());\n\tconst auto padding = st::giftBoxTabPadding;\n\tauto activeId = (_active >= 0\n\t\t&& ranges::contains(tabs, _buttons[_active].tab.id, &Tab::id))\n\t\t? _buttons[_active].tab.id\n\t\t: QString();\n\t_active = -1;\n\tcontext.repaint = [=] { update(); };\n\tfor (auto i = 0, count = int(tabs.size()); i != count; ++i) {\n\t\tauto &tab = tabs[i];\n\t\tAssert(!tab.id.isEmpty());\n\n\t\tauto &button = _buttons[i];\n\t\tbutton.active = (tab.id == activeId);\n\t\tif (button.tab != tab) {\n\t\t\tbutton.text = Text::String();\n\t\t\tbutton.text.setMarkedText(\n\t\t\t\tst::semiboldTextStyle,\n\t\t\t\ttab.text,\n\t\t\t\tkMarkupTextOptions,\n\t\t\t\tcontext);\n\t\t\tbutton.tab = std::move(tab);\n\t\t}\n\t\tif (button.active) {\n\t\t\t_active = i;\n\t\t}\n\t\tconst auto width = button.text.maxWidth();\n\t\tconst auto height = st::giftBoxTabStyle.font->height;\n\t\tconst auto r = QRect(0, 0, width, height).marginsAdded(padding);\n\t\tbutton.geometry = QRect(QPoint(x, y), r.size());\n\t\tx += r.width() + st::giftBoxTabSkip;\n\t}\n\tconst auto width = x\n\t\t- st::giftBoxTabSkip\n\t\t+ st::giftBoxTabsMargin.right();\n\t_fullWidth = width;\n\tresizeToWidth(this->width());\n\tupdate();\n}\n\nvoid SubTabs::setActiveTab(const QString &id) {\n\tif (id.isEmpty()) {\n\t\tsetActive(-1);\n\t\treturn;\n\t}\n\tconst auto i = ranges::find(\n\t\t_buttons,\n\t\tid,\n\t\t[](const Button &button) { return button.tab.id; });\n\tAssert(i != end(_buttons));\n\tsetActive(i - begin(_buttons));\n}\n\nrpl::producer<QString> SubTabs::activated() const {\n\treturn _activated.events();\n}\n\nrpl::producer<QString> SubTabs::contextMenuRequests() const {\n\treturn _contextMenuRequests.events();\n}\n\nrpl::producer<SubTabs::ReorderUpdate> SubTabs::reorderUpdates() const {\n\treturn _reorderUpdates.events();\n}\n\nvoid SubTabs::setReorderEnabled(bool enabled) {\n\t_reorderEnable = enabled;\n\tif (enabled) {\n\t\t_shakeAnimation.init([=] { update(); });\n\t\t_shakeAnimation.start();\n\t} else {\n\t\t_shakeAnimation.stop();\n\t\tupdate();\n\t}\n}\n\nbool SubTabs::reorderEnabled() const {\n\treturn _reorderEnable;\n}\n\nvoid SubTabs::setPinnedInterval(int from, int to) {\n\t_pinnedIntervals.push_back({ from, to });\n}\n\nvoid SubTabs::clearPinnedIntervals() {\n\t_pinnedIntervals.clear();\n}\n\nbool SubTabs::isIndexPinned(int index) const {\n\tfor (const auto &interval : _pinnedIntervals) {\n\t\tif (index >= interval.from && index < interval.to) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid SubTabs::setSelected(int index) {\n\tconst auto was = (_selected >= 0);\n\tconst auto now = (index >= 0);\n\t_selected = index;\n\tif (was != now) {\n\t\tsetCursor(now ? style::cur_pointer : style::cur_default);\n\t}\n}\n\nvoid SubTabs::setActive(int index) {\n\tconst auto was = _active;\n\tif (was == index) {\n\t\treturn;\n\t}\n\tif (was >= 0 && was < _buttons.size()) {\n\t\t_buttons[was].active = false;\n\t}\n\t_active = index;\n\t_buttons[index].active = true;\n\tconst auto geometry = _buttons[index].geometry;\n\tif (width() > 0\n\t\t&& _fullWidth > width()\n\t\t&& _scrollMax > 0\n\t\t&& !geometry.isEmpty()) {\n\t\tconst auto added = std::max(\n\t\t\tstd::min(width() / 8, (width() - geometry.width()) / 2),\n\t\t\t0);\n\t\tconst auto visibleFrom = int(base::SafeRound(_scroll));\n\t\tconst auto visibleTill = visibleFrom + width();\n\t\tif ((visibleTill < geometry.x() + geometry.width() + added)\n\t\t\t|| (visibleFrom + added > geometry.x())) {\n\t\t\t_scrollTo = std::clamp(\n\t\t\t\tgeometry.x() + (geometry.width() / 2) - (width() / 2),\n\t\t\t\t0,\n\t\t\t\t_scrollMax);\n\t\t\t_scrollAnimation.start([=] {\n\t\t\t\t_scroll = _scrollAnimation.value(_scrollTo);\n\t\t\t\tupdate();\n\t\t\t}, _scroll, _scrollTo, crl::time(150), anim::easeOutCirc);\n\t\t}\n\t}\n\tupdate();\n}\n\nint SubTabs::resizeGetHeight(int newWidth) {\n\tif (_centered) {\n\t\tupdate();\n\t\tconst auto fullWidth = _fullWidth;\n\t\t_fullShift = (fullWidth < newWidth) ? (newWidth - fullWidth) / 2 : 0;\n\t}\n\t_scrollMax = (_fullWidth > newWidth) ? (_fullWidth - newWidth) : 0;\n\treturn _buttons.empty()\n\t\t? 0\n\t\t: (st::giftBoxTabsMargin.top()\n\t\t\t+ _buttons.back().geometry.height()\n\t\t\t+ st::giftBoxTabsMargin.bottom());\n}\n\nbool SubTabs::eventHook(QEvent *e) {\n\tif (e->type() == QEvent::Leave) {\n\t\tsetSelected(-1);\n\t}\n\treturn RpWidget::eventHook(e);\n}\n\nvoid SubTabs::mouseMoveEvent(QMouseEvent *e) {\n\tconst auto mousex = e->pos().x();\n\tconst auto drag = QApplication::startDragDistance();\n\n\tif (_reorderEnable && _reorderIndex >= 0) {\n\t\tif (_reorderState != SubTabsReorderUpdate::State::Started) {\n\t\t\tconst auto shift = e->globalPos().x() - _reorderStart;\n\t\t\tif (std::abs(shift) > drag) {\n\t\t\t\t_reorderState = SubTabsReorderUpdate::State::Started;\n\t\t\t\t_reorderStart += (shift > 0) ? drag : -drag;\n\t\t\t\t_reorderDesiredIndex = _reorderIndex;\n\t\t\t\t_reorderUpdates.fire({\n\t\t\t\t\t_buttons[_reorderIndex].tab.id,\n\t\t\t\t\t_reorderIndex,\n\t\t\t\t\t_reorderIndex,\n\t\t\t\t\t_reorderState\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\tupdateReorder(e->globalPos());\n\t\t}\n\t\treturn;\n\t}\n\n\tif (!_reorderEnable) {\n\t\tif (_dragx > 0) {\n\t\t\t_scrollAnimation.stop();\n\t\t\t_scroll = std::clamp(\n\t\t\t\t_dragscroll + _dragx - mousex,\n\t\t\t\t0.,\n\t\t\t\t_scrollMax * 1.);\n\t\t\tupdate();\n\t\t\treturn;\n\t\t} else if (_pressx > 0 && std::abs(_pressx - mousex) > drag) {\n\t\t\t_dragx = _pressx;\n\t\t\t_dragscroll = _scroll;\n\t\t}\n\t}\n\tauto selected = -1;\n\tconst auto position = e->pos() + scroll();\n\tfor (auto i = 0, c = int(_buttons.size()); i != c; ++i) {\n\t\tif (_buttons[i].geometry.contains(position)) {\n\t\t\tselected = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tsetSelected(selected);\n}\n\nvoid SubTabs::wheelEvent(QWheelEvent *e) {\n\tif (_reorderEnable) {\n\t\te->ignore();\n\t\treturn;\n\t}\n\tconst auto delta = ScrollDeltaF(e);\n\n\tconst auto phase = e->phase();\n\tconst auto horizontal = (std::abs(delta.x()) > std::abs(delta.y()));\n\tif (phase == Qt::NoScrollPhase) {\n\t\t_locked = std::nullopt;\n\t} else if (phase == Qt::ScrollBegin) {\n\t\t_locked = std::nullopt;\n\t} else if (!_locked) {\n\t\t_locked = horizontal ? Qt::Horizontal : Qt::Vertical;\n\t}\n\tif (horizontal) {\n\t\tif (_locked == Qt::Vertical) {\n\t\t\treturn;\n\t\t}\n\t\te->accept();\n\t} else {\n\t\tif (_locked == Qt::Horizontal) {\n\t\t\te->accept();\n\t\t} else {\n\t\t\te->ignore();\n\t\t}\n\t\treturn;\n\t}\n\t_scrollAnimation.stop();\n\t_scroll = std::clamp(_scroll - delta.x(), 0., _scrollMax * 1.);\n\tupdate();\n}\n\nvoid SubTabs::mousePressEvent(QMouseEvent *e) {\n\tif (e->button() != Qt::LeftButton) {\n\t\treturn;\n\t}\n\t_pressed = _selected;\n\t_pressx = e->pos().x();\n\n\tif (_reorderEnable && _selected >= 0 && !isIndexPinned(_selected)) {\n\t\tstartReorder(_selected, e->globalPos());\n\t}\n}\n\nvoid SubTabs::mouseReleaseEvent(QMouseEvent *e) {\n\tif (e->button() != Qt::LeftButton) {\n\t\treturn;\n\t}\n\n\tif (_reorderEnable && _reorderIndex >= 0) {\n\t\tfinishReorder();\n\t\treturn;\n\t}\n\n\tconst auto dragx = std::exchange(_dragx, 0);\n\tconst auto pressed = std::exchange(_pressed, -1);\n\t_pressx = 0;\n\tif (!dragx\n\t\t&& pressed >= 0\n\t\t&& _selected == pressed\n\t\t&& pressed < _buttons.size()) {\n\t\t_activated.fire_copy(_buttons[pressed].tab.id);\n\t}\n}\n\nvoid SubTabs::contextMenuEvent(QContextMenuEvent *e) {\n\tif (_selected >= 0 && _selected < _buttons.size()) {\n\t\t_contextMenuRequests.fire_copy(_buttons[_selected].tab.id);\n\t}\n}\n\nvoid SubTabs::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto padding = st::giftBoxTabPadding;\n\tconst auto shift = -scroll();\n\tconst auto now = crl::now();\n\tconst auto hasShake = _shakeAnimation.animating();\n\tfor (auto i = 0; i < _buttons.size(); ++i) {\n\t\tconst auto &button = _buttons[i];\n\t\tconst auto geometry = button.geometry.translated(shift);\n\n\t\tif (hasShake && _reorderEnable && !isIndexPinned(i)) {\n\t\t\tshakeTransform(p, i, geometry.topLeft(), now);\n\t\t}\n\n\t\tconst auto shiftedGeometry = geometry.translated(\n\t\t\tbase::SafeRound(button.shift),\n\t\t\t0);\n\t\tif (button.active) {\n\t\t\tp.setBrush(st::giftBoxTabBgActive);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tconst auto radius = shiftedGeometry.height() / 2.;\n\t\t\tp.drawRoundedRect(shiftedGeometry, radius, radius);\n\t\t\tp.setPen(st::giftBoxTabFgActive);\n\t\t} else {\n\t\t\tp.setPen(st::giftBoxTabFg);\n\t\t}\n\t\tbutton.text.draw(p, {\n\t\t\t.position = shiftedGeometry.marginsRemoved(padding).topLeft(),\n\t\t\t.availableWidth = button.text.maxWidth(),\n\t\t});\n\n\t\tif (hasShake && _reorderEnable && !isIndexPinned(i)) {\n\t\t\tp.resetTransform();\n\t\t}\n\t}\n\tif (_fullWidth > width()) {\n\t\tconst auto &icon = st::defaultEmojiSuggestions;\n\t\tconst auto w = icon.fadeRight.width();\n\t\tconst auto &c = _st.bg->c;\n\t\tconst auto r = QRect(0, 0, w, height());\n\t\tconst auto s = std::abs(float64(shift.x()));\n\t\tconstexpr auto kF = 0.5;\n\t\tconst auto opacityRight = (_scrollMax - s)\n\t\t\t/ (icon.fadeRight.width() * kF);\n\t\tp.setOpacity(std::clamp(std::abs(opacityRight), 0., 1.));\n\t\ticon.fadeRight.fill(p, r.translated(width() - w, 0), c);\n\n\t\tconst auto opacityLeft = s / (icon.fadeLeft.width() * kF);\n\t\tp.setOpacity(std::clamp(std::abs(opacityLeft), 0., 1.));\n\t\ticon.fadeLeft.fill(p, r, c);\n\t}\n\n}\n\nQPoint SubTabs::scroll() const {\n\treturn QPoint(int(base::SafeRound(_scroll)) - _fullShift, 0);\n}\n\nvoid SubTabs::startReorder(int index, QPoint globalPos) {\n\tcancelReorder();\n\t_reorderIndex = index;\n\t_reorderStart = globalPos.x();\n\t_reorderState = SubTabsReorderUpdate::State::Cancelled;\n}\n\nvoid SubTabs::updateReorder(QPoint globalPos) {\n\tif (_reorderIndex < 0 || isIndexPinned(_reorderIndex)) {\n\t\treturn;\n\t}\n\n\t_reorderMousePos = mapFromGlobal(globalPos);\n\tconst auto shift = globalPos.x() - _reorderStart;\n\tauto &current = _buttons[_reorderIndex];\n\tcurrent.shiftAnimation.stop();\n\tcurrent.shift = current.finalShift = shift;\n\n\tcheckForScrollAnimation();\n\n\tconst auto count = _buttons.size();\n\tconst auto currentWidth = current.geometry.width();\n\tconst auto currentMiddle = current.geometry.x()\n\t\t+ shift\n\t\t+ currentWidth / 2;\n\t_reorderDesiredIndex = _reorderIndex;\n\n\tif (shift > 0) {\n\t\tfor (auto next = _reorderIndex + 1; next < count; ++next) {\n\t\t\tif (isIndexPinned(next)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst auto &e = _buttons[next];\n\t\t\tif (currentMiddle < e.geometry.x() + e.geometry.width() / 2) {\n\t\t\t\tmoveToShift(next, 0);\n\t\t\t} else {\n\t\t\t\t_reorderDesiredIndex = next;\n\t\t\t\tmoveToShift(next, -currentWidth);\n\t\t\t}\n\t\t}\n\t\tfor (auto prev = _reorderIndex - 1; prev >= 0; --prev) {\n\t\t\tmoveToShift(prev, 0);\n\t\t}\n\t} else {\n\t\tfor (auto next = _reorderIndex + 1; next < count; ++next) {\n\t\t\tmoveToShift(next, 0);\n\t\t}\n\t\tfor (auto prev = _reorderIndex - 1; prev >= 0; --prev) {\n\t\t\tif (isIndexPinned(prev)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst auto &e = _buttons[prev];\n\t\t\tif (currentMiddle >= e.geometry.x() + e.geometry.width() / 2) {\n\t\t\t\tmoveToShift(prev, 0);\n\t\t\t} else {\n\t\t\t\t_reorderDesiredIndex = prev;\n\t\t\t\tmoveToShift(prev, currentWidth);\n\t\t\t}\n\t\t}\n\t}\n\tupdate();\n}\n\nvoid SubTabs::finishReorder() {\n\t_reorderScrollAnimation.stop();\n\tif (_reorderIndex < 0) {\n\t\treturn;\n\t}\n\n\tconst auto index = _reorderIndex;\n\tconst auto result = _reorderDesiredIndex;\n\tconst auto id = _buttons[index].tab.id;\n\n\tif (result == index\n\t\t|| _reorderState != SubTabsReorderUpdate::State::Started) {\n\t\tcancelReorder();\n\t\treturn;\n\t}\n\n\t_reorderState = SubTabsReorderUpdate::State::Applied;\n\t_reorderIndex = -1;\n\t_dragx = 0;\n\t_pressx = 0;\n\t_dragscroll = 0.;\n\n\tauto &current = _buttons[index];\n\tconst auto width = current.geometry.width();\n\n\tif (index < result) {\n\t\tauto sum = 0;\n\t\tfor (auto i = index; i < result; ++i) {\n\t\t\tauto &entry = _buttons[i + 1];\n\t\t\tentry.deltaShift += width;\n\t\t\tupdateShift(i + 1);\n\t\t\tsum += entry.geometry.width();\n\t\t}\n\t\tcurrent.finalShift -= sum;\n\t} else if (index > result) {\n\t\tauto sum = 0;\n\t\tfor (auto i = result; i < index; ++i) {\n\t\t\tauto &entry = _buttons[i];\n\t\t\tentry.deltaShift -= width;\n\t\t\tupdateShift(i);\n\t\t\tsum += entry.geometry.width();\n\t\t}\n\t\tcurrent.finalShift += sum;\n\t}\n\n\tif (!(current.finalShift + current.deltaShift)) {\n\t\tcurrent.shift = 0;\n\t}\n\n\tbase::reorder(_buttons, index, result);\n\n\tauto x = st::giftBoxTabsMargin.left();\n\tconst auto y = st::giftBoxTabsMargin.top();\n\tconst auto padding = st::giftBoxTabPadding;\n\tfor (auto i = 0; i < _buttons.size(); ++i) {\n\t\tauto &button = _buttons[i];\n\t\tconst auto width = button.text.maxWidth();\n\t\tconst auto height = st::giftBoxTabStyle.font->height;\n\t\tconst auto r = QRect(0, 0, width, height).marginsAdded(padding);\n\t\tbutton.geometry = QRect(QPoint(x, y), r.size());\n\t\tx += r.width() + st::giftBoxTabSkip;\n\t}\n\n\tfor (auto i = 0; i < _buttons.size(); ++i) {\n\t\tmoveToShift(i, 0);\n\t}\n\n\t_reorderUpdates.fire(\n\t\t{ id, index, result, ReorderUpdate::State::Applied });\n}\n\nvoid SubTabs::cancelReorder() {\n\t_reorderScrollAnimation.stop();\n\tif (_reorderIndex < 0) {\n\t\treturn;\n\t}\n\n\tconst auto index = _reorderIndex;\n\tconst auto id = _buttons[index].tab.id;\n\n\tif (_reorderState == SubTabsReorderUpdate::State::Started) {\n\t\t_reorderState = SubTabsReorderUpdate::State::Cancelled;\n\t\t_reorderUpdates.fire({ id, index, index, _reorderState });\n\t}\n\n\t_reorderIndex = -1;\n\t_dragx = 0;\n\t_pressx = 0;\n\t_dragscroll = 0.;\n\tfor (auto i = 0; i < _buttons.size(); ++i) {\n\t\tmoveToShift(i, 0);\n\t}\n}\n\nvoid SubTabs::moveToShift(int index, float64 shift) {\n\tif (index < 0 || index >= _buttons.size()) {\n\t\treturn;\n\t}\n\n\tauto &entry = _buttons[index];\n\tif (entry.finalShift + entry.deltaShift == shift) {\n\t\treturn;\n\t}\n\n\tentry.shiftAnimation.start(\n\t\t[=, this] { updateShift(index); },\n\t\tentry.finalShift,\n\t\tshift - entry.deltaShift,\n\t\t150);\n\tentry.finalShift = shift - entry.deltaShift;\n}\n\nvoid SubTabs::updateShift(int index) {\n\tif (index < 0 || index >= _buttons.size()) {\n\t\treturn;\n\t}\n\n\tauto &entry = _buttons[index];\n\tentry.shift = entry.shiftAnimation.value(entry.finalShift)\n\t\t+ entry.deltaShift;\n\n\tif (entry.deltaShift && !entry.shiftAnimation.animating()) {\n\t\tentry.finalShift += entry.deltaShift;\n\t\tentry.deltaShift = 0;\n\t}\n\n\tupdate();\n}\n\nvoid SubTabs::shakeTransform(\n\t\tQPainter &p,\n\t\tint index,\n\t\tQPoint position,\n\t\tcrl::time now) const {\n\tconstexpr auto kShakeADuration = crl::time(400);\n\tconstexpr auto kShakeXDuration = crl::time(kShakeADuration * 1.2);\n\tconstexpr auto kShakeYDuration = kShakeADuration;\n\tconst auto diff = ((index % 2) ? 0 : kShakeYDuration / 2)\n\t\t+ (now - _shakeAnimation.started());\n\tconst auto pX = (diff % kShakeXDuration)\n\t\t/ float64(kShakeXDuration);\n\tconst auto pY = (diff % kShakeYDuration)\n\t\t/ float64(kShakeYDuration);\n\n\tconstexpr auto kMaxTranslation = .5;\n\tconstexpr auto kXStep = 1. / 5;\n\tconstexpr auto kYStep = 1. / 4;\n\n\t// 0, kMaxTranslation, 0, -kMaxTranslation, 0.\n\tconst auto x = (pX < kXStep)\n\t\t? anim::interpolateF(0., kMaxTranslation, pX / kXStep)\n\t\t: (pX < kXStep * 2.)\n\t\t? anim::interpolateF(kMaxTranslation, 0, (pX - kXStep) / kXStep)\n\t\t: (pX < kXStep * 3.)\n\t\t? anim::interpolateF(0, -kMaxTranslation, (pX - kXStep * 2.) / kXStep)\n\t\t: (pX < kXStep * 4.)\n\t\t? anim::interpolateF(-kMaxTranslation, 0, (pX - kXStep * 3.) / kXStep)\n\t\t: anim::interpolateF(0, 0., (pX - kXStep * 4.) / kXStep);\n\n\t// 0, kMaxTranslation, -kMaxTranslation, 0.\n\tconst auto y = (pY < kYStep)\n\t\t? anim::interpolateF(0., kMaxTranslation, pY / kYStep)\n\t\t: (pY < kYStep * 2.)\n\t\t? anim::interpolateF(kMaxTranslation, 0, (pY - kYStep) / kYStep)\n\t\t: (pY < kYStep * 3.)\n\t\t? anim::interpolateF(0, -kMaxTranslation, (pY - kYStep * 2.) / kYStep)\n\t\t: anim::interpolateF(-kMaxTranslation, 0, (pY - kYStep * 3) / kYStep);\n\n\tp.translate(x, y);\n}\n\nvoid SubTabs::checkForScrollAnimation() {\n\tif (_reorderIndex < 0\n\t\t|| !deltaFromEdge()\n\t\t|| _reorderScrollAnimation.animating()) {\n\t\treturn;\n\t}\n\t_reorderScrollAnimation.start();\n}\n\nvoid SubTabs::updateScrollCallback() {\n\tconst auto delta = deltaFromEdge();\n\tif (!delta) {\n\t\treturn;\n\t}\n\n\tconst auto oldScroll = _scroll;\n\t_scroll = std::clamp(_scroll + delta * 0.1, 0., float64(_scrollMax));\n\n\tconst auto scrollDelta = oldScroll - _scroll;\n\t_reorderStart += scrollDelta;\n\n\tif (_reorderIndex >= 0) {\n\t\tauto &current = _buttons[_reorderIndex];\n\t\tcurrent.shift = current.finalShift -= scrollDelta;\n\t}\n\n\tif (_scroll == 0. || _scroll == _scrollMax) {\n\t\t_reorderScrollAnimation.stop();\n\t}\n\tupdate();\n}\n\nint SubTabs::deltaFromEdge() {\n\tif (_reorderIndex < 0) {\n\t\treturn 0;\n\t}\n\n\tconst auto mouseX = _reorderMousePos.x();\n\tconst auto isLeftEdge = (mouseX < 0);\n\tconst auto isRightEdge = (mouseX > width());\n\n\tif (!isLeftEdge && !isRightEdge) {\n\t\t_reorderScrollAnimation.stop();\n\t\treturn 0;\n\t}\n\n\tconst auto delta = isRightEdge ? (mouseX - width()) : mouseX;\n\treturn std::clamp(delta, -50, 50);\n}\n\n} // namespace Ui\n\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/sub_tabs.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace style {\nstruct SubTabs;\n} // namespace style\n\nnamespace Ui {\n\nstruct SubTabsOptions {\n\tQString selected;\n\tbool centered = false;\n};\n\nstruct SubTabsTab {\n\tQString id;\n\tTextWithEntities text;\n\n\tfriend inline bool operator==(\n\t\tconst SubTabsTab &,\n\t\tconst SubTabsTab &) = default;\n};\n\nstruct SubTabsReorderUpdate {\n\tQString id;\n\tint oldPosition = 0;\n\tint newPosition = 0;\n\tenum class State : uchar {\n\t\tStarted,\n\t\tApplied,\n\t\tCancelled,\n\t} state = State::Started;\n};\n\nclass SubTabs : public RpWidget {\npublic:\n\tusing Options = SubTabsOptions;\n\tusing Tab = SubTabsTab;\n\n\texplicit SubTabs(\n\t\tQWidget *parent,\n\t\tconst style::SubTabs &st,\n\t\tOptions options = {},\n\t\tstd::vector<Tab> tabs = {},\n\t\tText::MarkedContext context = {});\n\n\tvoid setTabs(\n\t\tstd::vector<Tab> tabs,\n\t\tText::MarkedContext context = {});\n\n\tvoid setActiveTab(const QString &id);\n\n\tvoid setReorderEnabled(bool enabled);\n\t[[nodiscard]] bool reorderEnabled() const;\n\n\tvoid setPinnedInterval(int from, int to);\n\tvoid clearPinnedIntervals();\n\n\t[[nodiscard]] rpl::producer<QString> activated() const;\n\t[[nodiscard]] rpl::producer<QString> contextMenuRequests() const;\n\n\tusing ReorderUpdate = SubTabsReorderUpdate;\n\t[[nodiscard]] rpl::producer<ReorderUpdate> reorderUpdates() const;\n\nprivate:\n\tstruct Button {\n\t\tTab tab;\n\t\tQRect geometry;\n\t\tText::String text;\n\t\tbool active = false;\n\t\tUi::Animations::Simple shiftAnimation;\n\t\tfloat64 shift = 0.;\n\t\tfloat64 finalShift = 0.;\n\t\tfloat64 deltaShift = 0.;\n\t};\n\n\tint resizeGetHeight(int newWidth) override;\n\tvoid wheelEvent(QWheelEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tbool eventHook(QEvent *e) override;\n\n\tvoid setSelected(int index);\n\tvoid setActive(int index);\n\t[[nodiscard]] QPoint scroll() const;\n\tvoid shakeTransform(QPainter &p, int index, QPoint position, crl::time now) const;\n\t[[nodiscard]] bool isIndexPinned(int index) const;\n\n\tvoid startReorder(int index, QPoint globalPos);\n\tvoid updateReorder(QPoint globalPos);\n\tvoid finishReorder();\n\tvoid cancelReorder();\n\tvoid moveToShift(int index, float64 shift);\n\tvoid updateShift(int index);\n\n\tvoid checkForScrollAnimation();\n\tvoid updateScrollCallback();\n\t[[nodiscard]] int deltaFromEdge();\n\n\tconst style::SubTabs &_st;\n\tstd::vector<Button> _buttons;\n\trpl::event_stream<QString> _activated;\n\trpl::event_stream<QString> _contextMenuRequests;\n\trpl::event_stream<ReorderUpdate> _reorderUpdates;\n\tstd::optional<Qt::Orientation> _locked;\n\tint _dragx = 0;\n\tint _pressx = 0;\n\tfloat64 _dragscroll = 0.;\n\tfloat64 _scroll = 0.;\n\tfloat64 _scrollTo = 0.;\n\tUi::Animations::Simple _scrollAnimation;\n\tUi::Animations::Basic _reorderScrollAnimation;\n\tint _scrollMax = 0;\n\tint _fullShift = 0;\n\tint _fullWidth = 0;\n\tint _selected = -1;\n\tint _pressed = -1;\n\tint _active = -1;\n\tbool _centered = false;\n\tbool _reorderEnable = false;\n\tUi::Animations::Basic _shakeAnimation;\n\tstruct PinnedInterval {\n\t\tint from;\n\t\tint to;\n\t};\n\tstd::vector<PinnedInterval> _pinnedIntervals;\n\n\tint _reorderIndex = -1;\n\tfloat64 _reorderStart = 0.;\n\tint _reorderDesiredIndex = 0;\n\tSubTabsReorderUpdate::State _reorderState\n\t\t= SubTabsReorderUpdate::State::Cancelled;\n\tQPoint _reorderMousePos;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/subsection_tabs_slider.h\"\n\n#include \"dialogs/dialogs_three_state_icon.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/unread_badge_paint.h\"\n#include \"ui/unread_counter_format.h\"\n#include \"ui/painter.h\"\n#include \"ui/round_rect.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_filter_icons.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kMaxNameLines = 3;\nconstexpr auto kVerticalScale = 0.6;\nconstexpr auto kHorizontalScale = 0.5;\n\nvoid PaintPinnedIcon(\n\t\tQPainter &p,\n\t\tint width,\n\t\tint backgroundMargin,\n\t\tfloat64 scale = kVerticalScale,\n\t\tbool isHorizontal = false) {\n\tconstexpr auto kOffset = 5;\n\tp.scale(scale, scale);\n\tif (isHorizontal) {\n\t\tp.translate(\n\t\t\tst::lineWidth * kOffset,\n\t\t\tst::lineWidth * kOffset + backgroundMargin);\n\t} else {\n\t\tp.translate(\n\t\t\tst::lineWidth * kOffset + backgroundMargin,\n\t\t\tst::lineWidth * kOffset);\n\t}\n\tst::dialogsPinnedIcon.icon.paint(p, 0, 0, width);\n}\n\nclass VerticalButton final : public SubsectionButton {\npublic:\n\tVerticalButton(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<SubsectionButtonDelegate*> delegate,\n\t\tSubsectionTab &&data);\n\t~VerticalButton();\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid dataUpdatedHook() override;\n\tvoid invalidateCache() override;\n\tQImage prepareRippleMask() const override final {\n\t\treturn isPinned()\n\t\t\t? _rippleMask\n\t\t\t: Ui::RippleButton::prepareRippleMask();\n\t}\n\n\tvoid updateSize();\n\tvoid paintPinnedBackground(QPainter &p, const QRect &bgRect);\n\tQPainterPath createClipPath(const QRect &rect) const;\n\t[[nodiscard]] const QPainterPath &cachedClipPath(const QRect &rect);\n\n\tconst style::ChatTabsVertical &_st;\n\tText::String _text;\n\tbool _subscribed = false;\n\tRoundRect _roundRect;\n\tQImage _rippleMask;\n\tQPainterPath _clipPathCache;\n\tQRect _clipPathRect;\n\tbool _clipPathValid = false;\n\n};\n\nclass HorizontalButton final : public SubsectionButton {\npublic:\n\tHorizontalButton(\n\t\tnot_null<QWidget*> parent,\n\t\tconst style::SettingsSlider &st,\n\t\tnot_null<SubsectionButtonDelegate*> delegate,\n\t\tSubsectionTab &&data);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid dataUpdatedHook() override;\n\tvoid invalidateCache() override;\n\tQImage prepareRippleMask() const override final {\n\t\treturn isPinned()\n\t\t\t? _rippleMask\n\t\t\t: Ui::RippleButton::prepareRippleMask();\n\t}\n\tvoid updateSize();\n\tvoid paintPinnedBackground(QPainter &p, const QRect &bgRect);\n\tQPainterPath createClipPath(const QRect &rect) const;\n\t[[nodiscard]] const QPainterPath &cachedClipPath(const QRect &rect);\n\n\tconst style::SettingsSlider &_st;\n\tText::String _text;\n\tRoundRect _roundRect;\n\tQImage _rippleMask;\n\tQPainterPath _clipPathCache;\n\tQRect _clipPathRect;\n\tbool _clipPathValid = false;\n\n};\n\nVerticalButton::VerticalButton(\n\tnot_null<QWidget*> parent,\n\tnot_null<SubsectionButtonDelegate*> delegate,\n\tSubsectionTab &&data)\n: SubsectionButton(parent, delegate, std::move(data))\n, _st(st::chatTabsVertical)\n, _text(_st.nameStyle, _data.text, kDefaultTextOptions, _st.nameWidth)\n, _roundRect(st::boxRadius, st::windowBgOver) {\n\tupdateSize();\n}\n\nVerticalButton::~VerticalButton() {\n\tif (_subscribed) {\n\t\t_data.userpic->subscribeToUpdates(nullptr);\n\t}\n}\n\nvoid VerticalButton::dataUpdatedHook() {\n\t_text.setMarkedText(_st.nameStyle, _data.text, kDefaultTextOptions);\n\tupdateSize();\n}\n\nvoid VerticalButton::invalidateCache() {\n\t_roundRect.setColor(st::white);\n\tif (isPinned()) {\n\t\tconst auto bgRect = rect()\n\t\t\t- QMargins(_backgroundMargin, 0, _backgroundMargin, 0);\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t_rippleMask = QImage(\n\t\t\tbgRect.size() * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t_rippleMask.setDevicePixelRatio(ratio);\n\t\t_rippleMask.fill(Qt::transparent);\n\t\t{\n\t\t\tauto p = QPainter(&_rippleMask);\n\t\t\t_roundRect.paintSomeRounded(p, QRect(QPoint(), bgRect.size()), 0);\n\t\t}\n\t} else {\n\t\t_rippleMask = QImage();\n\t}\n\t_roundRect.setColor(st::shadowFg);\n\t_clipPathValid = false;\n}\n\nvoid VerticalButton::updateSize() {\n\tresize(_st.width, _st.baseHeight + std::min(\n\t\t_st.nameStyle.font->height * kMaxNameLines,\n\t\t_text.countHeight(_st.nameWidth, true)));\n\t_clipPathValid = false;\n}\n\nvoid VerticalButton::paintPinnedBackground(QPainter &p, const QRect &bgRect) {\n\tif (isFirstPinned() && isLastPinned()) {\n\t\t_roundRect.paint(p, bgRect);\n\t} else if (isFirstPinned()) {\n\t\t_roundRect.paintSomeRounded(\n\t\t\tp,\n\t\t\tbgRect,\n\t\t\tRectPart::TopLeft | RectPart::TopRight);\n\t} else if (isLastPinned()) {\n\t\t_roundRect.paintSomeRounded(\n\t\t\tp,\n\t\t\tbgRect,\n\t\t\tRectPart::BottomLeft | RectPart::BottomRight);\n\t} else {\n\t\t_roundRect.paintSomeRounded(p, bgRect, 0);\n\t}\n}\n\nQPainterPath VerticalButton::createClipPath(const QRect &rect) const {\n\tQPainterPath path;\n\tpath.setFillRule(Qt::WindingFill);\n\tconst auto radius = st::boxRadius;\n\tif (isFirstPinned() && isLastPinned()) {\n\t\tpath.addRoundedRect(rect, radius, radius);\n\t} else if (isFirstPinned()) {\n\t\tpath.addRoundedRect(rect, radius, radius);\n\t\tpath.addRect(rect.adjusted(0, rect.height() / 2, 0, 0));\n\t} else if (isLastPinned()) {\n\t\tpath.addRoundedRect(rect, radius, radius);\n\t\tpath.addRect(rect.adjusted(0, 0, 0, -rect.height() / 2));\n\t}\n\treturn path;\n}\n\nconst QPainterPath &VerticalButton::cachedClipPath(const QRect &rect) {\n\tif (!_clipPathValid || _clipPathRect != rect) {\n\t\t_clipPathCache = createClipPath(rect);\n\t\t_clipPathRect = rect;\n\t\t_clipPathValid = true;\n\t}\n\treturn _clipPathCache;\n}\n\nvoid VerticalButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto active = _delegate->buttonActive(this);\n\tconst auto color = anim::color(\n\t\t_st.rippleBg,\n\t\t_st.rippleBgActive,\n\t\tactive);\n\n\tif (isPinned()) {\n\t\tconst auto bgRect = rect()\n\t\t\t- QMargins(_backgroundMargin, 0, _backgroundMargin, 0);\n\t\tif (isFirstPinned() || isLastPinned()) {\n\t\t\tp.setClipPath(cachedClipPath(bgRect));\n\t\t}\n\t\tpaintPinnedBackground(p, bgRect);\n\t\tpaintRipple(p, QPoint(_backgroundMargin, 0), &color);\n\t} else {\n\t\tpaintRipple(p, QPoint(0, 0), &color);\n\t}\n\n\tif (!_subscribed) {\n\t\t_subscribed = true;\n\t\t_data.userpic->subscribeToUpdates([=] { update(); });\n\t}\n\tconst auto &image = _data.userpic->image(_st.userpicSize);\n\tconst auto userpicLeft = (width() - _st.userpicSize) / 2;\n\tp.drawImage(userpicLeft, _st.userpicTop, image);\n\tp.setPen(anim::pen(_st.nameFg, _st.nameFgActive, active));\n\n\tconst auto textLeft = (width() - _st.nameWidth) / 2;\n\t_text.draw(p, {\n\t\t.position = QPoint(textLeft, _st.nameTop),\n\t\t.outerWidth = width(),\n\t\t.availableWidth = _st.nameWidth,\n\t\t.align = style::al_top,\n\t\t.paused = _delegate->buttonPaused(),\n\t\t.elisionLines = kMaxNameLines,\n\t});\n\n\tconst auto &state = _data.badges;\n\tconst auto top = _st.userpicTop / 2;\n\tauto right = width() - textLeft;\n\tUnreadBadgeStyle st;\n\tif (state.unread) {\n\t\tst.muted = state.unreadMuted;\n\t\tconst auto counter = FormatUnreadCounter(\n\t\t\tstate.unreadCounter,\n\t\t\tstate.mention || state.reaction,\n\t\t\ttrue);\n\t\tconst auto badge = PaintUnreadBadge(p, counter, right, top, st);\n\t\tright -= badge.width() + st.padding;\n\t}\n\tif (state.mention || state.reaction) {\n\t\tconst auto muted = state.mention\n\t\t\t? state.mentionMuted\n\t\t\t: state.reactionMuted;\n\t\tconst auto &icon = state.mention\n\t\t\t? (muted\n\t\t\t\t? st::dialogsUnreadMentionMuted.icon\n\t\t\t\t: st::dialogsUnreadMention.icon)\n\t\t\t: (muted\n\t\t\t\t? st::dialogsUnreadReactionMuted.icon\n\t\t\t\t: st::dialogsUnreadReaction.icon);\n\t\ticon.paint(p, right - icon.width(), top, width());\n\t\tright -= icon.width() + st::dialogsUnreadPadding;\n\t}\n\tif (isPinned() && isFirstPinned()) {\n\t\tPaintPinnedIcon(p, width(), _backgroundMargin);\n\t}\n}\n\nHorizontalButton::HorizontalButton(\n\tnot_null<QWidget*> parent,\n\tconst style::SettingsSlider &st,\n\tnot_null<SubsectionButtonDelegate*> delegate,\n\tSubsectionTab &&data)\n: SubsectionButton(parent, delegate, std::move(data))\n, _st(st)\n, _roundRect(st::boxRadius, st::windowBgOver) {\n\tdataUpdatedHook();\n}\n\nvoid HorizontalButton::updateSize() {\n\tauto width = _st.strictSkip + _text.maxWidth();\n\n\tconst auto &state = _data.badges;\n\tUnreadBadgeStyle st;\n\tif (state.unread) {\n\t\tconst auto counter = FormatUnreadCounter(\n\t\t\tstate.unreadCounter,\n\t\t\tfalse,\n\t\t\tfalse);\n\t\tconst auto badge = CountUnreadBadgeSize(counter, st);\n\t\twidth += badge.width() + st.padding;\n\t}\n\tif (state.mention || state.reaction) {\n\t\tst.sizeId = state.mention\n\t\t\t? UnreadBadgeSize::Dialogs\n\t\t\t: UnreadBadgeSize::ReactionInDialogs;\n\t\tst.padding = 0;\n\t\tst.textTop = 0;\n\t\tconst auto counter = QString();\n\t\tconst auto badge = CountUnreadBadgeSize(counter, st);\n\t\twidth += badge.width() + st.padding + st::dialogsUnreadPadding;\n\t}\n\tresize(width, _st.height);\n\t_clipPathValid = false;\n}\n\nvoid HorizontalButton::paintPinnedBackground(\n\t\tQPainter &p,\n\t\tconst QRect &bgRect) {\n\tif (isFirstPinned() && isLastPinned()) {\n\t\t_roundRect.paint(p, bgRect);\n\t} else if (isFirstPinned()) {\n\t\t_roundRect.paintSomeRounded(\n\t\t\tp,\n\t\t\tbgRect,\n\t\t\tRectPart::TopLeft | RectPart::BottomLeft);\n\t} else if (isLastPinned()) {\n\t\t_roundRect.paintSomeRounded(\n\t\t\tp,\n\t\t\tbgRect,\n\t\t\tRectPart::TopRight | RectPart::BottomRight);\n\t} else {\n\t\t_roundRect.paintSomeRounded(p, bgRect, 0);\n\t}\n}\n\nQPainterPath HorizontalButton::createClipPath(const QRect &rect) const {\n\tQPainterPath path;\n\tpath.setFillRule(Qt::WindingFill);\n\tconst auto radius = st::boxRadius;\n\tif (isFirstPinned() && isLastPinned()) {\n\t\tpath.addRoundedRect(rect, radius, radius);\n\t} else if (isFirstPinned()) {\n\t\tpath.addRoundedRect(rect, radius, radius);\n\t\tpath.addRect(rect.adjusted(rect.width() / 2, 0, 0, 0));\n\t} else if (isLastPinned()) {\n\t\tpath.addRoundedRect(rect, radius, radius);\n\t\tpath.addRect(rect.adjusted(0, 0, -rect.width() / 2, 0));\n\t}\n\treturn path;\n}\n\nconst QPainterPath &HorizontalButton::cachedClipPath(const QRect &rect) {\n\tif (!_clipPathValid || _clipPathRect != rect) {\n\t\t_clipPathCache = createClipPath(rect);\n\t\t_clipPathRect = rect;\n\t\t_clipPathValid = true;\n\t}\n\treturn _clipPathCache;\n}\n\nvoid HorizontalButton::dataUpdatedHook() {\n\tauto context = _delegate->buttonContext();\n\tcontext.repaint = [=] { update(); };\n\t_text.setMarkedText(\n\t\t_st.labelStyle,\n\t\t_data.text,\n\t\tkDefaultTextOptions,\n\t\tcontext);\n\tupdateSize();\n}\n\nvoid HorizontalButton::invalidateCache() {\n\t_roundRect.setColor(st::white);\n\tif (isPinned()) {\n\t\tconst auto bgRect = rect()\n\t\t\t- QMargins(0, _backgroundMargin, 0, _backgroundMargin);\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t_rippleMask = QImage(\n\t\t\tbgRect.size() * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t_rippleMask.setDevicePixelRatio(ratio);\n\t\t_rippleMask.fill(Qt::transparent);\n\t\t{\n\t\t\tauto p = QPainter(&_rippleMask);\n\t\t\t_roundRect.paintSomeRounded(p, QRect(QPoint(), bgRect.size()), 0);\n\t\t}\n\t} else {\n\t\t_rippleMask = QImage();\n\t}\n\t_roundRect.setColor(st::shadowFg);\n\t_clipPathValid = false;\n}\n\nvoid HorizontalButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tconst auto active = _delegate->buttonActive(this);\n\n\tconst auto color = anim::color(\n\t\t_st.rippleBg,\n\t\t_st.rippleBgActive,\n\t\tactive);\n\n\tif (isPinned()) {\n\t\tconst auto bgRect = rect()\n\t\t\t- QMargins(0, _backgroundMargin, 0, _backgroundMargin);\n\t\tif (isFirstPinned() || isLastPinned()) {\n\t\t\tp.setClipPath(cachedClipPath(bgRect));\n\t\t}\n\t\tpaintPinnedBackground(p, bgRect);\n\t\tpaintRipple(p, QPoint(0, _backgroundMargin), &color);\n\t} else {\n\t\tpaintRipple(p, QPoint(0, 0), &color);\n\t}\n\n\tp.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active));\n\t_text.draw(p, {\n\t\t.position = QPoint(_st.strictSkip / 2, _st.labelTop),\n\t\t.outerWidth = width(),\n\t\t.availableWidth = _text.maxWidth(),\n\t\t.paused = _delegate->buttonPaused(),\n\t});\n\n\tauto right = width() - _st.strictSkip + (_st.strictSkip / 2);\n\tUnreadBadgeStyle st;\n\tconst auto &state = _data.badges;\n\tconst auto badgeTop = (height() - st.size) / 2;\n\tif (state.unread) {\n\t\tst.muted = state.unreadMuted;\n\t\tconst auto counter = FormatUnreadCounter(\n\t\t\tstate.unreadCounter,\n\t\t\tfalse,\n\t\t\tfalse);\n\t\tconst auto badge = PaintUnreadBadge(p, counter, right, badgeTop, st);\n\t\tright -= badge.width() + st.padding;\n\t}\n\tif (state.mention || state.reaction) {\n\t\tconst auto muted = state.mention\n\t\t\t? state.mentionMuted\n\t\t\t: state.reactionMuted;\n\t\tconst auto &icon = state.mention\n\t\t\t? (muted\n\t\t\t\t? st::dialogsUnreadMentionMuted.icon\n\t\t\t\t: st::dialogsUnreadMention.icon)\n\t\t\t: (muted\n\t\t\t\t? st::dialogsUnreadReactionMuted.icon\n\t\t\t\t: st::dialogsUnreadReaction.icon);\n\t\ticon.paint(p, right - icon.width(), badgeTop, width());\n\t\tright -= icon.width() + st::dialogsUnreadPadding;\n\t}\n\n\tif (isPinned() && isFirstPinned()) {\n\t\tPaintPinnedIcon(\n\t\t\tp,\n\t\t\twidth(),\n\t\t\t_backgroundMargin,\n\t\t\tkHorizontalScale,\n\t\t\ttrue);\n\t}\n}\n\n} // namespace\n\nSubsectionButton::SubsectionButton(\n\tnot_null<QWidget*> parent,\n\tnot_null<SubsectionButtonDelegate*> delegate,\n\tSubsectionTab &&data)\n: RippleButton(parent, st::defaultRippleAnimationBgOver)\n, _delegate(delegate)\n, _data(std::move(data)) {\n}\n\nSubsectionButton::~SubsectionButton() = default;\n\nvoid SubsectionButton::setData(SubsectionTab &&data) {\n\tExpects(_data.userpic.get() == data.userpic.get());\n\n\t_data = std::move(data);\n\tRippleButton::finishAnimating();\n\tdataUpdatedHook();\n\tupdate();\n}\n\nDynamicImage *SubsectionButton::userpic() const {\n\treturn _data.userpic.get();\n}\n\nvoid SubsectionButton::setActiveShown(float64 activeShown) {\n\tif (_activeShown != activeShown) {\n\t\t_activeShown = activeShown;\n\t\tupdate();\n\t}\n}\n\nvoid SubsectionButton::setIsPinned(bool pinned) {\n\tif (_isPinned != pinned) {\n\t\t_isPinned = pinned;\n\t\tinvalidateCache();\n\t\tupdate();\n\t}\n}\n\nbool SubsectionButton::isPinned() const {\n\treturn _isPinned;\n}\n\nvoid SubsectionButton::setPinnedPosition(bool isFirst, bool isLast) {\n\tif (_isFirstPinned != isFirst || _isLastPinned != isLast) {\n\t\t_isFirstPinned = isFirst;\n\t\t_isLastPinned = isLast;\n\t\tinvalidateCache();\n\t\tupdate();\n\t}\n}\n\nbool SubsectionButton::isFirstPinned() const {\n\treturn _isFirstPinned;\n}\n\nbool SubsectionButton::isLastPinned() const {\n\treturn _isLastPinned;\n}\n\nvoid SubsectionButton::setBackgroundMargin(int margin) {\n\t_backgroundMargin = margin;\n\tinvalidateCache();\n}\n\nvoid SubsectionButton::setShift(int shift) {\n\t_shift = shift;\n}\n\nvoid SubsectionButton::contextMenuEvent(QContextMenuEvent *e) {\n\t_delegate->buttonContextMenu(this, e);\n}\n\nSubsectionSlider::SubsectionSlider(not_null<QWidget*> parent, bool vertical)\n: RpWidget(parent)\n, _vertical(vertical)\n, _barSt(vertical\n\t? st::chatTabsOutlineVertical\n\t: st::chatTabsOutlineHorizontal)\n, _bar(CreateChild<RpWidget>(this))\n, _barRect(_barSt.radius, _barSt.fg) {\n\tsetupBar();\n}\n\nSubsectionSlider::~SubsectionSlider() = default;\n\nvoid SubsectionSlider::setupBar() {\n\t_bar->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tsizeValue() | rpl::on_next([=](QSize size) {\n\t\tconst auto thickness = _barSt.stroke - (_barSt.stroke / 2);\n\t\t_bar->setGeometry(\n\t\t\t0,\n\t\t\t_vertical ? 0 : (size.height() - thickness),\n\t\t\t_vertical ? thickness : size.width(),\n\t\t\t_vertical ? size.height() : thickness);\n\t}, _bar->lifetime());\n\t_bar->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tconst auto start = -_barSt.stroke / 2;\n\t\tconst auto currentRange = getCurrentActiveRange();\n\t\tconst auto from = currentRange.from + _barSt.skip;\n\t\tconst auto size = currentRange.size - 2 * _barSt.skip;\n\t\tif (size <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto rect = myrtlrect(\n\t\t\t_vertical ? start : from,\n\t\t\t_vertical ? from : 0,\n\t\t\t_vertical ? _barSt.stroke : size,\n\t\t\t_vertical ? size : _barSt.stroke);\n\t\tif (rect.intersects(clip)) {\n\t\t\tauto p = QPainter(_bar);\n\t\t\t_barRect.paint(p, rect);\n\t\t}\n\t}, _bar->lifetime());\n}\n\nvoid SubsectionSlider::setSections(\n\t\tSubsectionTabs sections,\n\t\tFn<bool()> paused) {\n\tExpects(!sections.tabs.empty());\n\n\t_context = sections.context;\n\t_paused = std::move(paused);\n\t_fixedCount = sections.fixed;\n\t_pinnedCount = sections.pinned;\n\t_reorderAllowed = sections.reorder;\n\n\tauto old = base::take(_tabs);\n\t_tabs.reserve(sections.tabs.size());\n\n\tauto size = 0;\n\tfor (auto &data : sections.tabs) {\n\t\tconst auto i = data.userpic\n\t\t\t? ranges::find(\n\t\t\t\told,\n\t\t\t\tdata.userpic.get(),\n\t\t\t\t&SubsectionButton::userpic)\n\t\t\t: old.empty()\n\t\t\t? end(old)\n\t\t\t: (end(old) - 1);\n\t\tif (i != end(old)) {\n\t\t\t_tabs.push_back(std::move(*i));\n\t\t\told.erase(i);\n\t\t\t_tabs.back()->setData(std::move(data));\n\t\t} else {\n\t\t\t_tabs.push_back(makeButton(std::move(data)));\n\t\t\t_tabs.back()->show();\n\t\t}\n\t\t_tabs.back()->setBackgroundMargin(_barSt.radius);\n\t\t_tabs.back()->move(_vertical ? 0 : size, _vertical ? size : 0);\n\n\t\tconst auto index = int(_tabs.size()) - 1;\n\t\tconst auto isPinned = (index >= _fixedCount)\n\t\t\t&& (index < _fixedCount + _pinnedCount);\n\t\t_tabs.back()->setIsPinned(isPinned);\n\t\tif (isPinned) {\n\t\t\tconst auto isFirst = (index == _fixedCount);\n\t\t\tconst auto isLast = (index == _fixedCount + _pinnedCount - 1);\n\t\t\t_tabs.back()->setPinnedPosition(isFirst, isLast);\n\t\t}\n\t\t_tabs.back()->setClickedCallback([=, raw = _tabs.back().get()] {\n\t\t\tif (_tabsReorderedOnce) {\n\t\t\t\tconst auto i = ranges::find(\n\t\t\t\t\t_tabs,\n\t\t\t\t\traw,\n\t\t\t\t\t&std::unique_ptr<SubsectionButton>::get);\n\t\t\t\tif (i != end(_tabs)) {\n\t\t\t\t\tactivate(int(i - begin(_tabs)));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tactivate(index);\n\t\t\t}\n\t\t});\n\t\tsize += _vertical ? _tabs.back()->height() : _tabs.back()->width();\n\t}\n\n\tfor (auto i = 0; i < int(_tabs.size()); ++i) {\n\t\tconst auto isPinned = (i >= _fixedCount)\n\t\t\t&& (i < _fixedCount + _pinnedCount);\n\t\tif (isPinned) {\n\t\t\tconst auto isFirst = (i == _fixedCount);\n\t\t\tconst auto isLast = (i == _fixedCount + _pinnedCount - 1);\n\t\t\t_tabs[i]->setPinnedPosition(isFirst, isLast);\n\t\t}\n\t}\n\n\tif (!_tabs.empty()) {\n\t\tresize(\n\t\t\t_vertical ? _tabs.front()->width() : size,\n\t\t\t_vertical ? size : _tabs.front()->height());\n\t}\n\n\t_bar->raise();\n}\n\nvoid SubsectionSlider::activate(int index) {\n\tif (_isReorderingCallback && _isReorderingCallback()) {\n\t\treturn;\n\t}\n\tconst auto old = _active;\n\tconst auto was = getFinalActiveRange();\n\t_active = index;\n\tconst auto now = getFinalActiveRange();\n\tconst auto callback = [=] {\n\t\t_bar->update();\n\t\tfor (auto i = std::min(old, index); i != std::max(old, index); ++i) {\n\t\t\tif (i >= 0 && i < int(_tabs.size())) {\n\t\t\t\t_tabs[i]->update();\n\t\t\t}\n\t\t}\n\t};\n\tconst auto weak = base::make_weak(_bar);\n\t_sectionActivated.fire_copy(index);\n\tif (weak) {\n\t\tconst auto duration = st::chatTabsSlider.duration;\n\t\t_activeFrom.start(callback, was.from, now.from, duration);\n\t\t_activeSize.start(callback, was.size, now.size, duration);\n\t\t_requestShown.fire_copy({ now.from, now.from + now.size });\n\t}\n}\n\nvoid SubsectionSlider::setActiveSectionFast(int active, bool ignoreScroll) {\n\tExpects(active < int(_tabs.size()));\n\n\tif (_active == active) {\n\t\treturn;\n\t}\n\t_active = active;\n\t_activeFrom.stop();\n\t_activeSize.stop();\n\tif (_active >= 0 && !ignoreScroll) {\n\t\tconst auto now = getFinalActiveRange();\n\t\t_requestShown.fire({ now.from, now.from + now.size });\n\t}\n\t_bar->update();\n}\n\nrpl::producer<ScrollToRequest> SubsectionSlider::requestShown() const {\n\treturn _requestShown.events();\n}\n\nvoid SubsectionSlider::setIsReorderingCallback(Fn<bool()> callback) {\n\t_isReorderingCallback = std::move(callback);\n}\n\nint SubsectionSlider::sectionsCount() const {\n\treturn int(_tabs.size());\n}\n\nrpl::producer<int> SubsectionSlider::sectionActivated() const {\n\treturn _sectionActivated.events();\n}\n\nrpl::producer<int> SubsectionSlider::sectionContextMenu() const {\n\treturn _sectionContextMenu.events();\n}\n\nint SubsectionSlider::lookupSectionPosition(int index) const {\n\tExpects(!_tabs.empty());\n\tExpects(index >= 0 && index < _tabs.size());\n\n\treturn _vertical ? _tabs[index]->y() : _tabs[index]->x();\n}\n\nvoid SubsectionSlider::paintEvent(QPaintEvent *e) {\n}\n\nint SubsectionSlider::lookupSectionIndex(QPoint position) const {\n\tExpects(!_tabs.empty());\n\n\tconst auto count = sectionsCount();\n\tif (_vertical) {\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tconst auto tab = _tabs[i].get();\n\t\t\tif (position.y() < tab->y() + tab->height()) {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tconst auto tab = _tabs[i].get();\n\t\t\tif (position.x() < tab->x() + tab->width()) {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t}\n\treturn count - 1;\n}\n\nSubsectionSlider::Range SubsectionSlider::getFinalActiveRange() const {\n\tif (_active < 0 || _active >= _tabs.size()) {\n\t\treturn {};\n\t}\n\tconst auto tab = _tabs[_active].get();\n\treturn Range{\n\t\t.from = _vertical ? tab->y() : tab->x(),\n\t\t.size = _vertical ? tab->height() : tab->width(),\n\t};\n}\n\nSubsectionSlider::Range SubsectionSlider::getCurrentActiveRange() const {\n\tconst auto finalRange = getFinalActiveRange();\n\treturn {\n\t\t.from = int(base::SafeRound(_activeFrom.value(finalRange.from))),\n\t\t.size = int(base::SafeRound(_activeSize.value(finalRange.size))),\n\t};\n}\n\nbool SubsectionSlider::buttonPaused() {\n\treturn _paused && _paused();\n}\n\nfloat64 SubsectionSlider::buttonActive(not_null<SubsectionButton*> button) {\n\tconst auto currentRange = getCurrentActiveRange();\n\tconst auto from = _vertical ? button->y() : button->x();\n\tconst auto size = _vertical ? button->height() : button->width();\n\tconst auto checkSize = std::min(size, currentRange.size);\n\treturn (checkSize > 0)\n\t\t? (1. - (std::abs(currentRange.from - from) / float64(checkSize)))\n\t\t: 0.;\n}\n\nvoid SubsectionSlider::buttonContextMenu(\n\t\tnot_null<SubsectionButton*> button,\n\t\tnot_null<QContextMenuEvent*> e) {\n\tconst auto i = ranges::find(\n\t\t_tabs,\n\t\tbutton.get(),\n\t\t&std::unique_ptr<SubsectionButton>::get);\n\tAssert(i != end(_tabs));\n\n\t_sectionContextMenu.fire(int(i - begin(_tabs)));\n\te->accept();\n}\n\nText::MarkedContext SubsectionSlider::buttonContext() {\n\treturn _context;\n}\n\nnot_null<SubsectionButton*> SubsectionSlider::buttonAt(int index) {\n\tExpects(index >= 0 && index < _tabs.size());\n\n\treturn _tabs[index].get();\n}\n\nvoid SubsectionSlider::setButtonShift(int index, int shift) {\n\tExpects(index >= 0 && index < _tabs.size());\n\n\tauto position = 0;\n\tfor (auto i = 0; i < index; ++i) {\n\t\tposition += _vertical ? _tabs[i]->height() : _tabs[i]->width();\n\t}\n\n\tconst auto targetPos = position + shift;\n\n\t_tabs[index]->move(\n\t\t_vertical ? 0 : targetPos,\n\t\t_vertical ? targetPos : 0);\n\trecalculatePinnedPositionsByUI();\n}\n\nvoid SubsectionSlider::reorderButtons(int from, int to) {\n\tExpects(from >= 0 && from < _tabs.size());\n\tExpects(to >= 0 && to < _tabs.size());\n\tif (from == to) {\n\t\treturn;\n\t}\n\n\t_active = base::reorder_index(_active, from, to);\n\tbase::reorder(_tabs, from, to);\n\n\tauto position = 0;\n\tfor (auto i = 0; i < int(_tabs.size()); ++i) {\n\t\t_tabs[i]->move(_vertical ? 0 : position, _vertical ? position : 0);\n\t\tposition += _vertical ? _tabs[i]->height() : _tabs[i]->width();\n\t}\n\t_tabsReorderedOnce = true;\n}\n\nvoid SubsectionSlider::recalculatePinnedPositions() {\n\tfor (auto i = 0; i < int(_tabs.size()); ++i) {\n\t\tconst auto isPinned = (i >= _fixedCount)\n\t\t\t&& (i < _fixedCount + _pinnedCount);\n\t\t_tabs[i]->setIsPinned(isPinned);\n\t\tif (isPinned) {\n\t\t\tconst auto isFirst = (i == _fixedCount);\n\t\t\tconst auto isLast = (i == _fixedCount + _pinnedCount - 1);\n\t\t\t_tabs[i]->setPinnedPosition(isFirst, isLast);\n\t\t}\n\t}\n}\n\nvoid SubsectionSlider::recalculatePinnedPositionsByUI() {\n\tif (_pinnedCount == 0) {\n\t\treturn;\n\t}\n\n\tauto pinnedIndices = std::vector<int>();\n\tfor (auto i = 0; i < int(_tabs.size()); ++i) {\n\t\tif (_tabs[i]->isPinned()) {\n\t\t\tpinnedIndices.push_back(i);\n\t\t}\n\t}\n\n\tif (pinnedIndices.empty()) {\n\t\treturn;\n\t}\n\n\tranges::sort(pinnedIndices, [&](int a, int b) {\n\t\tconst auto posA = _vertical ? _tabs[a]->y() : _tabs[a]->x();\n\t\tconst auto posB = _vertical ? _tabs[b]->y() : _tabs[b]->x();\n\t\treturn posA < posB;\n\t});\n\n\tfor (auto i = 0; i < int(pinnedIndices.size()); ++i) {\n\t\tconst auto index = pinnedIndices[i];\n\t\tconst auto isFirst = (i == 0);\n\t\tconst auto isLast = (i == int(pinnedIndices.size()) - 1);\n\t\t_tabs[index]->setPinnedPosition(isFirst, isLast);\n\t}\n}\n\nVerticalSlider::VerticalSlider(not_null<QWidget*> parent)\n: SubsectionSlider(parent, true) {\n}\n\nVerticalSlider::~VerticalSlider() = default;\n\nstd::unique_ptr<SubsectionButton> VerticalSlider::makeButton(\n\t\tSubsectionTab &&data) {\n\treturn std::make_unique<VerticalButton>(\n\t\tthis,\n\t\tstatic_cast<SubsectionButtonDelegate*>(this),\n\t\tstd::move(data));\n}\n\nHorizontalSlider::HorizontalSlider(not_null<QWidget*> parent)\n: SubsectionSlider(parent, false)\n, _st(st::chatTabsSlider) {\n}\n\nHorizontalSlider::~HorizontalSlider() = default;\n\nstd::unique_ptr<SubsectionButton> HorizontalSlider::makeButton(\n\t\tSubsectionTab &&data) {\n\treturn std::make_unique<HorizontalButton>(\n\t\tthis,\n\t\t_st,\n\t\tstatic_cast<SubsectionButtonDelegate*>(this),\n\t\tstd::move(data));\n}\n\nstd::shared_ptr<DynamicImage> MakeIconSubsectionsThumbnail(\n\t\tconst style::icon &icon,\n\t\tFn<QColor()> textColor,\n\t\tstd::optional<QMargins> invertedPadding = {}) {\n\tclass Image final : public DynamicImage {\n\tpublic:\n\t\tImage(\n\t\t\tconst style::icon &icon,\n\t\t\tFn<QColor()> textColor,\n\t\t\tstd::optional<QMargins> invertedPadding)\n\t\t: _icon(icon)\n\t\t, _textColor(std::move(textColor))\n\t\t, _invertedPadding(invertedPadding) {\n\t\t\tExpects(_textColor != nullptr);\n\t\t}\n\n\t\tstd::shared_ptr<DynamicImage> clone() override {\n\t\t\treturn std::make_shared<Image>(\n\t\t\t\t_icon,\n\t\t\t\t_textColor,\n\t\t\t\t_invertedPadding);\n\t\t}\n\n\t\tQImage image(int size) override {\n\t\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t\tconst auto full = size * ratio;\n\t\t\tconst auto color = _textColor();\n\t\t\tif (_cache.size() != QSize(full, full)) {\n\t\t\t\t_cache = QImage(\n\t\t\t\t\tQSize(full, full),\n\t\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t\t_cache.setDevicePixelRatio(ratio);\n\t\t\t} else if (_color == color) {\n\t\t\t\treturn _cache;\n\t\t\t}\n\t\t\t_color = color;\n\t\t\tif (_invertedPadding) {\n\t\t\t\t_cache.fill(Qt::transparent);\n\t\t\t\tauto p = QPainter(&_cache);\n\t\t\t\tconst auto fill = QRect(QPoint(), _icon.size()).marginsAdded(\n\t\t\t\t\t*_invertedPadding).size();\n\t\t\t\tconst auto inner = QRect(\n\t\t\t\t\t(size - fill.width()) / 2,\n\t\t\t\t\t(size - fill.height()) / 2,\n\t\t\t\t\tfill.width(),\n\t\t\t\t\tfill.height());\n\t\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\t\tconst auto radius = fill.width() / 6.;\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(color);\n\t\t\t\tp.drawRoundedRect(inner, radius, radius);\n\t\t\t\t_icon.paint(\n\t\t\t\t\tp,\n\t\t\t\t\t(inner.topLeft()\n\t\t\t\t\t\t+ QPoint(\n\t\t\t\t\t\t\t_invertedPadding->left(),\n\t\t\t\t\t\t\t_invertedPadding->top())),\n\t\t\t\t\tsize);\n\t\t\t\treturn _cache;\n\t\t\t}\n\n\t\t\tif (_mask.isNull()) {\n\t\t\t\t_mask = _icon.instance(QColor(255, 255, 255));\n\t\t\t}\n\t\t\tconst auto position = ratio * QPoint(\n\t\t\t\t(size - (_mask.width() / ratio)) / 2,\n\t\t\t\t(size - (_mask.height() / ratio)) / 2);\n\t\t\tif (_mask.width() <= full && _mask.height() <= full) {\n\t\t\t\tstyle::colorizeImage(\n\t\t\t\t\t_mask,\n\t\t\t\t\tcolor,\n\t\t\t\t\t&_cache,\n\t\t\t\t\tQRect(),\n\t\t\t\t\tposition);\n\t\t\t} else {\n\t\t\t\t_cache = style::colorizeImage(_mask, color).scaled(\n\t\t\t\t\tfull,\n\t\t\t\t\tfull,\n\t\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\t\tQt::SmoothTransformation);\n\t\t\t\t_cache.setDevicePixelRatio(ratio);\n\t\t\t}\n\t\t\treturn _cache;\n\t\t}\n\t\tvoid subscribeToUpdates(Fn<void()> callback) override {\n\t\t\tif (!callback) {\n\t\t\t\t_cache = QImage();\n\t\t\t\t_mask = QImage();\n\t\t\t}\n\t\t}\n\n\tprivate:\n\t\tconst style::icon &_icon;\n\t\tFn<QColor()> _textColor;\n\t\tQImage _mask;\n\t\tQImage _cache;\n\t\tQColor _color;\n\t\tstd::optional<QMargins> _invertedPadding;\n\n\t};\n\treturn std::make_shared<Image>(\n\t\ticon,\n\t\tstd::move(textColor),\n\t\tinvertedPadding);\n}\n\nstd::shared_ptr<DynamicImage> MakeAllSubsectionsThumbnail(\n\t\tFn<QColor()> textColor) {\n\treturn MakeIconSubsectionsThumbnail(\n\t\tst::foldersAll,\n\t\tstd::move(textColor));\n}\n\nstd::shared_ptr<DynamicImage> MakeNewChatSubsectionsThumbnail(\n\t\tFn<QColor()> textColor) {\n\treturn MakeIconSubsectionsThumbnail(\n\t\tst::newChatIcon,\n\t\tstd::move(textColor),\n\t\tst::newChatIconPadding);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"dialogs/dialogs_common.h\"\n#include \"ui/round_rect.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/widgets/buttons.h\"\n\nnamespace style {\nstruct ChatTabsVertical;\nstruct ChatTabsOutline;\n} // namespace style\n\nnamespace Ui {\n\nclass DynamicImage;\nclass RippleAnimation;\nclass SubsectionButton;\nstruct ScrollToRequest;\n\nstruct SubsectionTab {\n\tTextWithEntities text;\n\tstd::shared_ptr<DynamicImage> userpic;\n\tDialogs::BadgesState badges;\n};\n\nstruct SubsectionTabs {\n\tstd::vector<SubsectionTab> tabs;\n\tText::MarkedContext context;\n\tint fixed = 0;\n\tint pinned = 0;\n\tbool reorder = false;\n};\n\nclass SubsectionButtonDelegate {\npublic:\n\tvirtual bool buttonPaused() = 0;\n\tvirtual float64 buttonActive(not_null<SubsectionButton*> button) = 0;\n\tvirtual Text::MarkedContext buttonContext() = 0;\n\tvirtual void buttonContextMenu(\n\t\tnot_null<SubsectionButton*> button,\n\t\tnot_null<QContextMenuEvent*> e) = 0;\n};\n\nclass SubsectionButton : public RippleButton {\npublic:\n\tSubsectionButton(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<SubsectionButtonDelegate*> delegate,\n\t\tSubsectionTab &&data);\n\t~SubsectionButton();\n\n\tvoid setData(SubsectionTab &&data);\n\t[[nodiscard]] DynamicImage *userpic() const;\n\n\tvoid setActiveShown(float64 activeShown);\n\tvoid setIsPinned(bool pinned);\n\t[[nodiscard]] bool isPinned() const;\n\tvoid setPinnedPosition(bool isFirst, bool isLast);\n\t[[nodiscard]] bool isFirstPinned() const;\n\t[[nodiscard]] bool isLastPinned() const;\n\tvirtual void setBackgroundMargin(int margin);\n\tvoid setShift(int shift);\n\nprotected:\n\tvirtual void dataUpdatedHook() = 0;\n\tvirtual void invalidateCache() = 0;\n\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\n\tconst not_null<SubsectionButtonDelegate*> _delegate;\n\tSubsectionTab _data;\n\tfloat64 _activeShown = 0.;\n\tbool _isPinned = false;\n\tbool _isFirstPinned = false;\n\tbool _isLastPinned = false;\n\tint _backgroundMargin = 0;\n\tint _shift = 0;\n\n};\n\nclass SubsectionSlider\n\t: public RpWidget\n\t, public SubsectionButtonDelegate {\npublic:\n\t~SubsectionSlider();\n\n\tvoid setSections(\n\t\tSubsectionTabs sections,\n\t\tFn<bool()> paused);\n\tvoid setActiveSectionFast(int active, bool ignoreScroll = false);\n\n\t[[nodiscard]] int sectionsCount() const;\n\t[[nodiscard]] rpl::producer<int> sectionActivated() const;\n\t[[nodiscard]] rpl::producer<int> sectionContextMenu() const;\n\t[[nodiscard]] int lookupSectionPosition(int index) const;\n\n\tbool buttonPaused() override;\n\tfloat64 buttonActive(not_null<SubsectionButton*> button) override;\n\tvoid buttonContextMenu(\n\t\tnot_null<SubsectionButton*> button,\n\t\tnot_null<QContextMenuEvent*> e) override;\n\tText::MarkedContext buttonContext() override;\n\t[[nodiscard]] not_null<SubsectionButton*> buttonAt(int index);\n\tvoid setButtonShift(int index, int shift);\n\tvoid reorderButtons(int from, int to);\n\tvoid recalculatePinnedPositions();\n\tvoid recalculatePinnedPositionsByUI();\n\n\t[[nodiscard]] rpl::producer<ScrollToRequest> requestShown() const;\n\n\tvoid setIsReorderingCallback(Fn<bool()> callback);\n\n\t[[nodiscard]] bool isVertical() const {\n\t\treturn _vertical;\n\t}\n\nprotected:\n\tstruct Range {\n\t\tint from = 0;\n\t\tint size = 0;\n\t};\n\n\tSubsectionSlider(not_null<QWidget*> parent, bool vertical);\n\tvoid setupBar();\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\t[[nodiscard]] int lookupSectionIndex(QPoint position) const;\n\t[[nodiscard]] Range getFinalActiveRange() const;\n\t[[nodiscard]] Range getCurrentActiveRange() const;\n\tvoid activate(int index);\n\n\t[[nodiscard]] virtual std::unique_ptr<SubsectionButton> makeButton(\n\t\tSubsectionTab &&data) = 0;\n\n\tconst bool _vertical = false;\n\n\tconst style::ChatTabsOutline &_barSt;\n\tRpWidget *_bar = nullptr;\n\tRoundRect _barRect;\n\n\tstd::vector<std::unique_ptr<SubsectionButton>> _tabs;\n\tbool _tabsReorderedOnce = false;\n\tint _active = -1;\n\tint _pressed = -1;\n\tAnimations::Simple _activeFrom;\n\tAnimations::Simple _activeSize;\n\n\t//int _buttonIndexHint = 0;\n\n\tText::MarkedContext _context;\n\tint _fixedCount = 0;\n\tint _pinnedCount = 0;\n\tbool _reorderAllowed = false;\n\n\trpl::event_stream<int> _sectionActivated;\n\trpl::event_stream<int> _sectionContextMenu;\n\tFn<bool()> _paused;\n\n\trpl::event_stream<ScrollToRequest> _requestShown;\n\n\tFn<bool()> _isReorderingCallback;\n\n};\n\nclass VerticalSlider final : public SubsectionSlider {\npublic:\n\texplicit VerticalSlider(not_null<QWidget*> parent);\n\t~VerticalSlider();\n\nprivate:\n\tstd::unique_ptr<SubsectionButton> makeButton(\n\t\tSubsectionTab &&data) override;\n\n};\n\nclass HorizontalSlider final : public SubsectionSlider {\npublic:\n\texplicit HorizontalSlider(not_null<QWidget*> parent);\n\t~HorizontalSlider();\n\nprivate:\n\tstd::unique_ptr<SubsectionButton> makeButton(\n\t\tSubsectionTab &&data) override;\n\n\tconst style::SettingsSlider &_st;\n\n};\n\n[[nodiscard]] std::shared_ptr<DynamicImage> MakeAllSubsectionsThumbnail(\n\tFn<QColor()> textColor);\n[[nodiscard]] std::shared_ptr<DynamicImage> MakeNewChatSubsectionsThumbnail(\n\tFn<QColor()> textColor);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/subsection_tabs_slider_reorder.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/subsection_tabs_slider_reorder.h\"\n\n#include \"ui/controls/subsection_tabs_slider.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/controls/subsection_tabs_slider.h\"\n#include \"styles/style_basic.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace Ui {\n\nnamespace {\n\nconstexpr auto kScrollFactor = 0.05;\n\n} // namespace\n\nSubsectionSliderReorder::SubsectionSliderReorder(\n\tnot_null<SubsectionSlider*> slider,\n\tnot_null<ScrollArea*> scroll)\n: _slider(slider)\n, _scroll(scroll)\n, _scrollAnimation([=] { updateScrollCallback(); }) {\n}\n\nSubsectionSliderReorder::SubsectionSliderReorder(\n\tnot_null<SubsectionSlider*> slider)\n: _slider(slider) {\n}\n\nSubsectionSliderReorder::~SubsectionSliderReorder() {\n\tcancel();\n}\n\nvoid SubsectionSliderReorder::cancel() {\n\tif (_currentButton) {\n\t\tcancelCurrent(indexOf(_currentButton));\n\t}\n\t_lifetime.destroy();\n\t_entries.clear();\n}\n\nvoid SubsectionSliderReorder::start() {\n\tconst auto count = _slider->sectionsCount();\n\tif (count < 2) {\n\t\treturn;\n\t}\n\tfor (auto i = 0; i != count; ++i) {\n\t\tconst auto button = _slider->buttonAt(i);\n\t\tconst auto eventsProducer = _proxyButtonCallback\n\t\t\t? _proxyButtonCallback(i)\n\t\t\t: button;\n\t\teventsProducer->events(\n\t\t) | rpl::on_next_done([=](not_null<QEvent*> e) {\n\t\t\tswitch (e->type()) {\n\t\t\tcase QEvent::MouseMove:\n\t\t\t\tmouseMove(\n\t\t\t\t\tbutton,\n\t\t\t\t\tstatic_cast<QMouseEvent*>(e.get())->globalPos());\n\t\t\t\tbreak;\n\t\t\tcase QEvent::MouseButtonPress: {\n\t\t\t\tconst auto event = static_cast<QMouseEvent*>(e.get());\n\t\t\t\tmousePress(button, event->button(), event->globalPos());\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase QEvent::MouseButtonRelease:\n\t\t\t\tmouseRelease(static_cast<QMouseEvent*>(e.get())->button());\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}, [=] {\n\t\t\tcancel();\n\t\t}, _lifetime);\n\t\t_entries.push_back({ button });\n\t}\n}\n\nvoid SubsectionSliderReorder::addPinnedInterval(int from, int length) {\n\t_pinnedIntervals.push_back({ from, length });\n}\n\nvoid SubsectionSliderReorder::clearPinnedIntervals() {\n\t_pinnedIntervals.clear();\n}\n\nvoid SubsectionSliderReorder::setMouseEventProxy(ProxyCallback callback) {\n\t_proxyButtonCallback = std::move(callback);\n}\n\nbool SubsectionSliderReorder::Interval::isIn(int index) const {\n\treturn (index >= from) && (index < (from + length));\n}\n\nbool SubsectionSliderReorder::isIndexPinned(int index) const {\n\treturn ranges::any_of(_pinnedIntervals, [&](const Interval &i) {\n\t\treturn i.isIn(index);\n\t});\n}\n\nvoid SubsectionSliderReorder::mouseMove(\n\t\tnot_null<SubsectionButton*> button,\n\t\tQPoint position) {\n\tif (_currentButton != button) {\n\t\treturn;\n\t} else if (_currentState != State::Started) {\n\t\tcheckForStart(position);\n\t} else {\n\t\tupdateOrder(indexOf(_currentButton), position);\n\n\t\tcheckForScrollAnimation();\n\t}\n}\n\nvoid SubsectionSliderReorder::checkForStart(QPoint position) {\n\tconst auto shift = _slider->isVertical()\n\t\t? (position.y() - _currentStart)\n\t\t: (position.x() - _currentStart);\n\tconst auto delta = QApplication::startDragDistance();\n\tif (std::abs(shift) <= delta) {\n\t\treturn;\n\t}\n\t_currentButton->raise();\n\t_currentState = State::Started;\n\t_currentStart += (shift > 0) ? delta : -delta;\n\n\tconst auto index = indexOf(_currentButton);\n\t_currentDesiredIndex = index;\n\t_updates.fire({ _currentButton, index, index, _currentState });\n\n\tupdateOrder(index, position);\n}\n\nvoid SubsectionSliderReorder::updateOrder(int index, QPoint position) {\n\tif (isIndexPinned(index)) {\n\t\treturn;\n\t}\n\tconst auto shift = _slider->isVertical()\n\t\t? (position.y() - _currentStart)\n\t\t: (position.x() - _currentStart);\n\tauto &current = _entries[index];\n\tcurrent.shiftAnimation.stop();\n\tcurrent.shift = current.finalShift = shift;\n\t_slider->setButtonShift(index, shift);\n\n\tconst auto count = _entries.size();\n\tconst auto currentSize = _slider->isVertical()\n\t\t? _currentButton->height()\n\t\t: _currentButton->width();\n\tconst auto currentMiddle = _slider->isVertical()\n\t\t? (_currentButton->y() + currentSize / 2)\n\t\t: (_currentButton->x() + currentSize / 2);\n\t_currentDesiredIndex = index;\n\tif (shift > 0) {\n\t\tauto top = _slider->isVertical()\n\t\t\t? (_currentButton->y() - shift)\n\t\t\t: (_currentButton->x() - shift);\n\t\tfor (auto next = index + 1; next != count; ++next) {\n\t\t\tif (isIndexPinned(next)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto &entry = _entries[next];\n\t\t\ttop += _slider->isVertical()\n\t\t\t\t? entry.button->height()\n\t\t\t\t: entry.button->width();\n\t\t\tif (currentMiddle < top) {\n\t\t\t\tmoveToShift(next, 0);\n\t\t\t} else {\n\t\t\t\t_currentDesiredIndex = next;\n\t\t\t\tmoveToShift(next, -currentSize);\n\t\t\t}\n\t\t}\n\t\tfor (auto prev = index - 1; prev >= 0; --prev) {\n\t\t\tmoveToShift(prev, 0);\n\t\t}\n\t} else {\n\t\tfor (auto next = index + 1; next != count; ++next) {\n\t\t\tmoveToShift(next, 0);\n\t\t}\n\t\tfor (auto prev = index - 1; prev >= 0; --prev) {\n\t\t\tif (isIndexPinned(prev)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto &entry = _entries[prev];\n\t\t\tconst auto entryPos = _slider->isVertical()\n\t\t\t\t? (entry.button->y() - entry.shift)\n\t\t\t\t: (entry.button->x() - entry.shift);\n\t\t\tif (currentMiddle >= entryPos + currentSize) {\n\t\t\t\tmoveToShift(prev, 0);\n\t\t\t} else {\n\t\t\t\t_currentDesiredIndex = prev;\n\t\t\t\tmoveToShift(prev, currentSize);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid SubsectionSliderReorder::mousePress(\n\t\tnot_null<SubsectionButton*> button,\n\t\tQt::MouseButton btn,\n\t\tQPoint position) {\n\tif (btn != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tcancelCurrent();\n\t_currentButton = button;\n\t_currentStart = _slider->isVertical() ? position.y() : position.x();\n}\n\nvoid SubsectionSliderReorder::mouseRelease(Qt::MouseButton button) {\n\tif (button != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tfinishReordering();\n}\n\nvoid SubsectionSliderReorder::cancelCurrent() {\n\tif (_currentButton) {\n\t\tcancelCurrent(indexOf(_currentButton));\n\t}\n}\n\nvoid SubsectionSliderReorder::cancelCurrent(int index) {\n\tExpects(_currentButton != nullptr);\n\n\tif (_currentState == State::Started) {\n\t\t_currentState = State::Cancelled;\n\t\t_updates.fire({ _currentButton, index, index, _currentState });\n\t}\n\t_currentButton = nullptr;\n\tfor (auto i = 0, count = int(_entries.size()); i != count; ++i) {\n\t\tmoveToShift(i, 0);\n\t}\n}\n\nvoid SubsectionSliderReorder::finishReordering() {\n\tif (_scroll) {\n\t\t_scrollAnimation.stop();\n\t}\n\tfinishCurrent();\n}\n\nvoid SubsectionSliderReorder::finishCurrent() {\n\tif (!_currentButton) {\n\t\treturn;\n\t}\n\tconst auto index = indexOf(_currentButton);\n\tif (_currentDesiredIndex == index || _currentState != State::Started) {\n\t\tcancelCurrent(index);\n\t\treturn;\n\t}\n\tconst auto result = _currentDesiredIndex;\n\tconst auto button = _currentButton;\n\t_currentState = State::Cancelled;\n\t_currentButton = nullptr;\n\n\tauto &current = _entries[index];\n\tconst auto size = _slider->isVertical()\n\t\t? button->height()\n\t\t: button->width();\n\tif (index < result) {\n\t\tauto sum = 0;\n\t\tfor (auto i = index; i != result; ++i) {\n\t\t\tauto &entry = _entries[i + 1];\n\t\t\tconst auto btn = entry.button;\n\t\t\tentry.deltaShift += size;\n\t\t\tupdateShift(btn, i + 1);\n\t\t\tsum += _slider->isVertical() ? btn->height() : btn->width();\n\t\t}\n\t\tcurrent.finalShift -= sum;\n\t} else if (index > result) {\n\t\tauto sum = 0;\n\t\tfor (auto i = result; i != index; ++i) {\n\t\t\tauto &entry = _entries[i];\n\t\t\tconst auto btn = entry.button;\n\t\t\tentry.deltaShift -= size;\n\t\t\tupdateShift(btn, i);\n\t\t\tsum += _slider->isVertical() ? btn->height() : btn->width();\n\t\t}\n\t\tcurrent.finalShift += sum;\n\t}\n\tif (!(current.finalShift + current.deltaShift)) {\n\t\tcurrent.shift = 0;\n\t\t_slider->setButtonShift(index, 0);\n\t}\n\tbase::reorder(_entries, index, result);\n\t_slider->reorderButtons(index, result);\n\tfor (auto i = 0, count = int(_entries.size()); i != count; ++i) {\n\t\tmoveToShift(i, 0);\n\t}\n\n\t_updates.fire({ button, index, result, State::Applied });\n}\n\nvoid SubsectionSliderReorder::moveToShift(int index, int shift) {\n\tauto &entry = _entries[index];\n\tif (entry.finalShift + entry.deltaShift == shift) {\n\t\treturn;\n\t}\n\tconst auto button = entry.button;\n\tentry.shiftAnimation.start(\n\t\t[=] { updateShift(button, index); },\n\t\tentry.finalShift,\n\t\tshift - entry.deltaShift,\n\t\tst::slideWrapDuration);\n\tentry.finalShift = shift - entry.deltaShift;\n}\n\nvoid SubsectionSliderReorder::updateShift(\n\t\tnot_null<SubsectionButton*> button,\n\t\tint indexHint) {\n\tExpects(indexHint >= 0 && indexHint < _entries.size());\n\n\tconst auto index = (_entries[indexHint].button == button)\n\t\t? indexHint\n\t\t: indexOf(button);\n\tauto &entry = _entries[index];\n\tentry.shift = base::SafeRound(\n\t\tentry.shiftAnimation.value(entry.finalShift)\n\t) + entry.deltaShift;\n\tif (entry.deltaShift && !entry.shiftAnimation.animating()) {\n\t\tentry.finalShift += entry.deltaShift;\n\t\tentry.deltaShift = 0;\n\t}\n\t_slider->setButtonShift(index, entry.shift);\n}\n\nauto SubsectionSliderReorder::updates() const\n-> rpl::producer<SubsectionSliderReorder::Single> {\n\treturn _updates.events();\n}\n\nint SubsectionSliderReorder::indexOf(\n\t\tnot_null<SubsectionButton*> button) const {\n\tconst auto i = ranges::find(_entries, button, &Entry::button);\n\tAssert(i != end(_entries));\n\treturn i - begin(_entries);\n}\n\nvoid SubsectionSliderReorder::updateScrollCallback() {\n\tif (!_scroll) {\n\t\treturn;\n\t}\n\tconst auto delta = deltaFromEdge();\n\tif (_slider->isVertical()) {\n\t\tconst auto oldTop = _scroll->scrollTop();\n\t\t_scroll->scrollToY(oldTop + delta);\n\t\tconst auto newTop = _scroll->scrollTop();\n\t\t_currentStart += oldTop - newTop;\n\t\tif (newTop == 0 || newTop == _scroll->scrollTopMax()) {\n\t\t\t_scrollAnimation.stop();\n\t\t}\n\t} else {\n\t\tconst auto oldLeft = _scroll->scrollLeft();\n\t\t_scroll->scrollToX(oldLeft + delta);\n\t\tconst auto newLeft = _scroll->scrollLeft();\n\t\t_currentStart += oldLeft - newLeft;\n\t\tif (newLeft == 0 || newLeft == _scroll->scrollLeftMax()) {\n\t\t\t_scrollAnimation.stop();\n\t\t}\n\t}\n}\n\nvoid SubsectionSliderReorder::checkForScrollAnimation() {\n\tif (!_scroll || !deltaFromEdge() || _scrollAnimation.animating()) {\n\t\treturn;\n\t}\n\t_scrollAnimation.start();\n}\n\nint SubsectionSliderReorder::deltaFromEdge() {\n\tExpects(_currentButton != nullptr);\n\tExpects(_scroll);\n\n\tconst auto globalPosition = _currentButton->mapToGlobal(QPoint(0, 0));\n\tconst auto localPos = _scroll->mapFromGlobal(globalPosition);\n\tconst auto localTop = _slider->isVertical() ? localPos.y() : localPos.x();\n\tconst auto buttonSize = _slider->isVertical()\n\t\t? _currentButton->height()\n\t\t: _currentButton->width();\n\tconst auto scrollSize = _slider->isVertical()\n\t\t? _scroll->height()\n\t\t: _scroll->width();\n\tconst auto localBottom = localTop + buttonSize - scrollSize;\n\n\tconst auto isTopEdge = (localTop < 0);\n\tconst auto isBottomEdge = (localBottom > 0);\n\tif (!isTopEdge && !isBottomEdge) {\n\t\t_scrollAnimation.stop();\n\t\treturn 0;\n\t}\n\treturn int((isBottomEdge ? localBottom : localTop) * kScrollFactor);\n}\n\n} // namespace Ui"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/subsection_tabs_slider_reorder.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n\nnamespace Ui {\n\nclass RpWidget;\nclass ScrollArea;\nclass SubsectionButton;\nclass SubsectionSlider;\n\nclass SubsectionSliderReorder final {\npublic:\n\tusing ProxyCallback = Fn<not_null<Ui::RpWidget*>(int)>;\n\tenum class State : uchar {\n\t\tStarted,\n\t\tApplied,\n\t\tCancelled,\n\t};\n\tstruct Single {\n\t\tnot_null<SubsectionButton*> widget;\n\t\tint oldPosition = 0;\n\t\tint newPosition = 0;\n\t\tState state = State::Started;\n\t};\n\n\tSubsectionSliderReorder(\n\t\tnot_null<SubsectionSlider*> slider,\n\t\tnot_null<ScrollArea*> scroll);\n\tSubsectionSliderReorder(not_null<SubsectionSlider*> slider);\n\t~SubsectionSliderReorder();\n\n\tvoid start();\n\tvoid cancel();\n\tvoid finishReordering();\n\tvoid addPinnedInterval(int from, int length);\n\tvoid clearPinnedIntervals();\n\tvoid setMouseEventProxy(ProxyCallback callback);\n\t[[nodiscard]] rpl::producer<Single> updates() const;\n\nprivate:\n\tstruct Entry {\n\t\tnot_null<SubsectionButton*> button;\n\t\tUi::Animations::Simple shiftAnimation;\n\t\tint shift = 0;\n\t\tint finalShift = 0;\n\t\tint deltaShift = 0;\n\t};\n\tstruct Interval {\n\t\t[[nodiscard]] bool isIn(int index) const;\n\t\tint from = 0;\n\t\tint length = 0;\n\t};\n\n\tvoid mouseMove(not_null<SubsectionButton*> button, QPoint position);\n\tvoid mousePress(\n\t\tnot_null<SubsectionButton*> button,\n\t\tQt::MouseButton mouseButton,\n\t\tQPoint position);\n\tvoid mouseRelease(Qt::MouseButton button);\n\n\tvoid checkForStart(QPoint position);\n\tvoid updateOrder(int index, QPoint position);\n\tvoid cancelCurrent();\n\tvoid finishCurrent();\n\tvoid cancelCurrent(int index);\n\n\t[[nodiscard]] int indexOf(not_null<SubsectionButton*> button) const;\n\tvoid moveToShift(int index, int shift);\n\tvoid updateShift(not_null<SubsectionButton*> button, int indexHint);\n\n\tvoid updateScrollCallback();\n\tvoid checkForScrollAnimation();\n\t[[nodiscard]] int deltaFromEdge();\n\n\t[[nodiscard]] bool isIndexPinned(int index) const;\n\n\tconst not_null<Ui::SubsectionSlider*> _slider;\n\tUi::ScrollArea *_scroll = nullptr;\n\n\tUi::Animations::Basic _scrollAnimation;\n\n\tstd::vector<Interval> _pinnedIntervals;\n\n\tProxyCallback _proxyButtonCallback = nullptr;\n\n\tSubsectionButton *_currentButton = nullptr;\n\tint _currentStart = 0;\n\tint _currentDesiredIndex = 0;\n\tState _currentState = State::Cancelled;\n\tstd::vector<Entry> _entries;\n\trpl::event_stream<Single> _updates;\n\trpl::lifetime _lifetime;\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/swipe_handler.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/swipe_handler.h\"\n\n#include \"base/platform/base_platform_haptic.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/qt/qt_common_adapters.h\"\n#include \"base/event_filter.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/controls/swipe_handler_data.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/elastic_scroll.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"styles/style_chat.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace Ui::Controls {\nnamespace {\n\nconstexpr auto kSwipeSlow = 0.2;\n\nconstexpr auto kMsgBareIdSwipeBack = std::numeric_limits<int64>::max() - 77;\nconstexpr auto kSwipedBackSpeedRatio = 0.35;\n\nfloat64 InterpolationRatio(float64 from, float64 to, float64 result) {\n\treturn (result - from) / (to - from);\n};\n\nclass RatioRange final {\npublic:\n\t[[nodiscard]] float64 calcRatio(float64 value) {\n\t\tif (value < _min) {\n\t\t\tconst auto shift = _min - value;\n\t\t\t_min -= shift;\n\t\t\t_max -= shift;\n\t\t\t_max = _min + 1;\n\t\t} else if (value > _max) {\n\t\t\tconst auto shift = value - _max;\n\t\t\t_min += shift;\n\t\t\t_max += shift;\n\t\t\t_max = _min + 1;\n\t\t}\n\t\treturn InterpolationRatio(_min, _max, value);\n\t}\n\nprivate:\n\tfloat64 _min = 0;\n\tfloat64 _max = 1;\n\n};\n\n} // namespace\n\nvoid SetupSwipeHandler(SwipeHandlerArgs &&args) {\n\tstatic constexpr auto kThresholdWidth = 50;\n\tstatic constexpr auto kMaxRatio = 1.5;\n\n\tconst auto widget = std::move(args.widget);\n\tconst auto scroll = std::move(args.scroll);\n\tconst auto update = std::move(args.update);\n\n\tstruct UpdateArgs {\n\t\tQPoint globalCursor;\n\t\tQPointF position;\n\t\tQPointF delta;\n\t\tbool touch = false;\n\t};\n\tstruct State {\n\t\tbase::unique_qptr<QObject> filter;\n\t\tUi::Animations::Simple animationReach;\n\t\tUi::Animations::Simple animationEnd;\n\t\tSwipeContextData data;\n\t\tSwipeHandlerFinishData finishByTopData;\n\t\tstd::optional<Qt::Orientation> orientation;\n\t\tstd::optional<Qt::LayoutDirection> direction;\n\t\tfloat64 threshold = style::ConvertFloatScale(kThresholdWidth);\n\t\tRatioRange ratioRange;\n\t\tint directionInt = 1.;\n\t\tQPointF startAt;\n\t\tQPointF delta;\n\t\tint cursorTop = 0;\n\t\tbool dontStart = false;\n\t\tbool started = false;\n\t\tbool reached = false;\n\t\tbool touch = false;\n\n\t\trpl::lifetime lifetime;\n\t};\n\tauto &useLifetime = args.onLifetime\n\t\t? *(args.onLifetime)\n\t\t: args.widget->lifetime();\n\tconst auto state = useLifetime.make_state<State>();\n\tif (args.dontStart) {\n\t\tstd::move(\n\t\t\targs.dontStart\n\t\t) | rpl::on_next([=](bool dontStart) {\n\t\t\tstate->dontStart = dontStart;\n\t\t}, state->lifetime);\n\t} else {\n\t\tv::match(scroll, [](v::null_t) {\n\t\t}, [&](const auto &scroll) {\n\t\t\tscroll->touchMaybePressing(\n\t\t\t) | rpl::on_next([=](bool maybePressing) {\n\t\t\t\tstate->dontStart = maybePressing;\n\t\t\t}, state->lifetime);\n\t\t});\n\t}\n\n\tconst auto updateRatio = [=](float64 ratio) {\n\t\tratio = std::max(ratio, 0.);\n\t\tstate->data.ratio = ratio;\n\t\tconst auto overscrollRatio = std::max(ratio - 1., 0.);\n\t\tconst auto translation = int(\n\t\t\tbase::SafeRound(-std::min(ratio, 1.) * state->threshold)\n\t\t) + Ui::OverscrollFromAccumulated(int(\n\t\t\tbase::SafeRound(-overscrollRatio * state->threshold)\n\t\t));\n\t\tstate->data.msgBareId = state->finishByTopData.msgBareId;\n\t\tstate->data.translation = translation\n\t\t\t* state->directionInt;\n\t\tstate->data.cursorTop = state->cursorTop;\n\t\tupdate(state->data);\n\t};\n\tconst auto setOrientation = [=](std::optional<Qt::Orientation> o) {\n\t\tstate->orientation = o;\n\t\tconst auto isHorizontal = (o == Qt::Horizontal);\n\t\tv::match(scroll, [](v::null_t) {\n\t\t}, [&](const auto &scroll) {\n\t\t\tif (const auto viewport = scroll->viewport()) {\n\t\t\t\tif (viewport != widget) {\n\t\t\t\t\tviewport->setAttribute(\n\t\t\t\t\t\tQt::WA_AcceptTouchEvents,\n\t\t\t\t\t\t!isHorizontal);\n\t\t\t\t}\n\t\t\t}\n\t\t\tscroll->disableScroll(isHorizontal);\n\t\t});\n\t};\n\tconst auto processEnd = [=](std::optional<QPointF> delta = {}) {\n\t\tif (state->orientation == Qt::Horizontal) {\n\t\t\tconst auto rawRatio = delta.value_or(state->delta).x()\n\t\t\t\t/ state->threshold\n\t\t\t\t* state->directionInt;\n\t\t\tconst auto ratio = std::clamp(\n\t\t\t\tstate->finishByTopData.keepRatioWithinRange\n\t\t\t\t\t? state->ratioRange.calcRatio(rawRatio)\n\t\t\t\t\t: rawRatio,\n\t\t\t\t0.,\n\t\t\t\tkMaxRatio);\n\t\t\tif ((ratio >= 1) && state->finishByTopData.callback) {\n\t\t\t\tUi::PostponeCall(\n\t\t\t\t\twidget,\n\t\t\t\t\tstate->finishByTopData.callback);\n\t\t\t}\n\t\t\tstate->animationEnd.stop();\n\t\t\tstate->animationEnd.start(\n\t\t\t\tupdateRatio,\n\t\t\t\tratio,\n\t\t\t\t0.,\n\t\t\t\tstd::min(1., ratio) * st::slideWrapDuration);\n\t\t}\n\t\tsetOrientation(std::nullopt);\n\t\tstate->started = false;\n\t\tstate->reached = false;\n\t\tstate->direction = std::nullopt;\n\t\tstate->startAt = {};\n\t\tstate->delta = {};\n\t};\n\tv::match(scroll, [](v::null_t) {\n\t}, [&](const auto &scroll) {\n\t\tscroll->scrolls() | rpl::on_next([=] {\n\t\t\tif (state->orientation != Qt::Vertical) {\n\t\t\t\tprocessEnd();\n\t\t\t}\n\t\t}, state->lifetime);\n\t});\n\tconst auto animationReachCallback = [=](float64 value) {\n\t\tstate->data.reachRatio = value;\n\t\tupdate(state->data);\n\t};\n\tconst auto updateWith = [=, generateFinish = args.init](UpdateArgs args) {\n\t\tconst auto fillFinishByTop = [&] {\n\t\t\tif (!args.delta.x()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->direction = (args.delta.x() < 0)\n\t\t\t\t? Qt::RightToLeft\n\t\t\t\t: Qt::LeftToRight;\n\t\t\tstate->directionInt = (state->direction == Qt::LeftToRight)\n\t\t\t\t? 1\n\t\t\t\t: -1;\n\t\t\tstate->finishByTopData = generateFinish(\n\t\t\t\tstate->cursorTop,\n\t\t\t\t*state->direction);\n\t\t\tstate->threshold = style::ConvertFloatScale(kThresholdWidth)\n\t\t\t\t* state->finishByTopData.speedRatio;\n\t\t\tif (!state->finishByTopData.callback\n\t\t\t\t|| (state->finishByTopData.msgBareId == kMsgBareIdSwipeBack\n\t\t\t\t\t&& !base::Platform::IsSwipeBackEnabled())) {\n\t\t\t\tsetOrientation(Qt::Vertical);\n\t\t\t}\n\t\t};\n\t\tif (!state->started || state->touch != args.touch) {\n\t\t\tstate->started = true;\n\t\t\tstate->data.reachRatio = 0.;\n\t\t\tstate->touch = args.touch;\n\t\t\tstate->startAt = args.position;\n\t\t\tstate->cursorTop = widget->mapFromGlobal(args.globalCursor).y();\n\t\t\tif (!state->touch) {\n\t\t\t\t// args.delta already is valid.\n\t\t\t\tfillFinishByTop();\n\t\t\t} else {\n\t\t\t\t// args.delta depends on state->startAt, so it's invalid.\n\t\t\t\tstate->direction = std::nullopt;\n\t\t\t}\n\t\t\tstate->delta = QPointF();\n\t\t} else if (!state->direction) {\n\t\t\tfillFinishByTop();\n\t\t} else if (!state->orientation) {\n\t\t\tstate->delta = args.delta;\n\t\t\tconst auto diffXtoY = std::abs(args.delta.x())\n\t\t\t\t- std::abs(args.delta.y());\n\t\t\tconstexpr auto kOrientationThreshold = 1.;\n\t\t\tif (diffXtoY > kOrientationThreshold) {\n\t\t\t\tif (!state->dontStart) {\n\t\t\t\t\tsetOrientation(Qt::Horizontal);\n\t\t\t\t}\n\t\t\t} else if (diffXtoY < -kOrientationThreshold) {\n\t\t\t\tsetOrientation(Qt::Vertical);\n\t\t\t} else {\n\t\t\t\tsetOrientation(std::nullopt);\n\t\t\t}\n\t\t} else if (*state->orientation == Qt::Horizontal) {\n\t\t\tstate->delta = args.delta;\n\t\t\tconst auto rawRatio = 0\n\t\t\t\t+ args.delta.x() * state->directionInt / state->threshold;\n\t\t\tconst auto ratio = state->finishByTopData.keepRatioWithinRange\n\t\t\t\t? state->ratioRange.calcRatio(rawRatio)\n\t\t\t\t: rawRatio;\n\t\t\tupdateRatio(ratio);\n\t\t\tconstexpr auto kResetReachedOn = 0.95;\n\t\t\tconstexpr auto kBounceDuration = crl::time(500);\n\t\t\tif (!state->reached && ratio >= 1.) {\n\t\t\t\tstate->reached = true;\n\t\t\t\tstate->animationReach.stop();\n\t\t\t\tstate->animationReach.start(\n\t\t\t\t\tanimationReachCallback,\n\t\t\t\t\t0.,\n\t\t\t\t\t1.,\n\t\t\t\t\tstate->finishByTopData.reachRatioDuration\n\t\t\t\t\t\t? state->finishByTopData.reachRatioDuration\n\t\t\t\t\t\t: kBounceDuration);\n\t\t\t\tbase::Platform::Haptic();\n\t\t\t} else if (state->reached\n\t\t\t\t&& ratio < kResetReachedOn) {\n\t\t\t\tif (state->finishByTopData.provideReachOutRatio) {\n\t\t\t\t\tstate->animationReach.stop();\n\t\t\t\t\tstate->animationReach.start(\n\t\t\t\t\t\tanimationReachCallback,\n\t\t\t\t\t\t1.,\n\t\t\t\t\t\t0.,\n\t\t\t\t\t\tstate->finishByTopData.reachRatioDuration\n\t\t\t\t\t\t\t? state->finishByTopData.reachRatioDuration\n\t\t\t\t\t\t\t: kBounceDuration);\n\t\t\t\t}\n\t\t\t\tstate->reached = false;\n\t\t\t}\n\t\t}\n\t};\n\tconst auto filter = [=](not_null<QEvent*> e) {\n\t\tif (!widget->testAttribute(Qt::WA_AcceptTouchEvents)) {\n\t\t\t[[maybe_unused]] int a = 0;\n\t\t}\n\t\tconst auto type = e->type();\n\t\tswitch (type) {\n\t\tcase QEvent::Leave: {\n\t\t\tif (state->orientation == Qt::Horizontal) {\n\t\t\t\tprocessEnd();\n\t\t\t}\n\t\t} break;\n\t\tcase QEvent::MouseMove: {\n\t\t\tif (state->orientation == Qt::Horizontal) {\n\t\t\t\tconst auto m = static_cast<QMouseEvent*>(e.get());\n\t\t\t\tif (std::abs(m->pos().y() - state->cursorTop)\n\t\t\t\t\t> QApplication::startDragDistance()) {\n\t\t\t\t\tprocessEnd();\n\t\t\t\t}\n\t\t\t}\n\t\t} break;\n\t\tcase QEvent::TouchBegin:\n\t\tcase QEvent::TouchUpdate:\n\t\tcase QEvent::TouchEnd:\n\t\tcase QEvent::TouchCancel: {\n\t\t\tconst auto t = static_cast<QTouchEvent*>(e.get());\n\t\t\tconst auto touchscreen = t->device()\n\t\t\t\t&& (t->device()->type() == base::TouchDevice::TouchScreen);\n\t\t\tif (!touchscreen && type != QEvent::TouchCancel) {\n\t\t\t\tbreak;\n\t\t\t} else if (type == QEvent::TouchBegin) {\n\t\t\t\t// Reset state in case we lost some TouchEnd.\n\t\t\t\tprocessEnd();\n\t\t\t}\n\t\t\tconst auto &touches = t->touchPoints();\n\t\t\tconst auto released = [&](int index) {\n\t\t\t\treturn (touches.size() > index)\n\t\t\t\t\t&& (int(touches.at(index).state())\n\t\t\t\t\t\t& int(Qt::TouchPointReleased));\n\t\t\t};\n\t\t\tconst auto cancel = released(0)\n\t\t\t\t|| released(1)\n\t\t\t\t|| (touches.size() != (touchscreen ? 1 : 2))\n\t\t\t\t|| (type == QEvent::TouchEnd)\n\t\t\t\t|| (type == QEvent::TouchCancel);\n\t\t\tif (cancel) {\n\t\t\t\tprocessEnd(touches.empty()\n\t\t\t\t\t? std::optional<QPointF>()\n\t\t\t\t\t: (state->startAt - touches[0].pos()));\n\t\t\t} else {\n\t\t\t\tconst auto args = UpdateArgs{\n\t\t\t\t\t.globalCursor = (touchscreen\n\t\t\t\t\t\t? touches[0].screenPos().toPoint()\n\t\t\t\t\t\t: QCursor::pos()),\n\t\t\t\t\t.position = touches[0].pos(),\n\t\t\t\t\t.delta = state->startAt - touches[0].pos(),\n\t\t\t\t\t.touch = true,\n\t\t\t\t};\n\t\t\t\tupdateWith(args);\n\t\t\t}\n\t\t\treturn (touchscreen && state->orientation != Qt::Horizontal)\n\t\t\t\t? base::EventFilterResult::Continue\n\t\t\t\t: base::EventFilterResult::Cancel;\n\t\t} break;\n\t\tcase QEvent::Wheel: {\n\t\t\tconst auto w = static_cast<QWheelEvent*>(e.get());\n\t\t\tconst auto phase = w->phase();\n\t\t\tif (phase == Qt::NoScrollPhase) {\n\t\t\t\tbreak;\n\t\t\t} else if (phase == Qt::ScrollBegin) {\n\t\t\t\t// Reset state in case we lost some TouchEnd.\n\t\t\t\tprocessEnd();\n\t\t\t}\n\t\t\tconst auto cancel = w->buttons()\n\t\t\t\t|| (phase == Qt::ScrollEnd)\n\t\t\t\t|| (phase == Qt::ScrollMomentum);\n\t\t\tif (cancel) {\n\t\t\t\tprocessEnd();\n\t\t\t} else {\n\t\t\t\tconst auto invert = (w->inverted() ? -1 : 1);\n\t\t\t\tconst auto delta = Ui::ScrollDeltaF(w) * invert;\n\t\t\t\tupdateWith({\n\t\t\t\t\t.globalCursor = w->globalPosition().toPoint(),\n\t\t\t\t\t.position = QPointF(),\n\t\t\t\t\t.delta = state->delta + delta * kSwipeSlow,\n\t\t\t\t\t.touch = false,\n\t\t\t\t});\n\t\t\t}\n\t\t} break;\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t};\n\twidget->setAttribute(Qt::WA_AcceptTouchEvents);\n\tstate->filter = base::unique_qptr<QObject>(\n\t\tbase::install_event_filter(widget, filter));\n}\n\nSwipeBackResult SetupSwipeBack(\n\t\tnot_null<Ui::RpWidget*> widget,\n\t\tFn<std::pair<QColor, QColor>()> colors,\n\t\tbool mirrored,\n\t\tbool iconMirrored) {\n\tstruct State {\n\t\tbase::unique_qptr<Ui::RpWidget> back;\n\t\tSwipeContextData data;\n\t};\n\n\tconstexpr auto kMaxInnerOffset = 0.5;\n\tconstexpr auto kMaxOuterOffset = 0.8;\n\tconstexpr auto kIdealSize = 100;\n\tconst auto maxOffset = st::swipeBackSize * kMaxInnerOffset;\n\tconst auto sizeRatio = st::swipeBackSize\n\t\t/ style::ConvertFloatScale(kIdealSize);\n\n\tauto lifetime = rpl::lifetime();\n\tconst auto state = lifetime.make_state<State>();\n\n\tconst auto paintCallback = [=] {\n\t\tconst auto [bg, fg] = colors();\n\t\tconst auto arrowPen = QPen(\n\t\t\tfg,\n\t\t\tst::lineWidth * 3 * sizeRatio,\n\t\t\tQt::SolidLine,\n\t\t\tQt::RoundCap);\n\t\treturn [=] {\n\t\t\tauto p = QPainter(state->back);\n\n\t\t\tconstexpr auto kBouncePart = 0.25;\n\t\t\tconstexpr auto kStrokeWidth = 2.;\n\t\t\tconstexpr auto kWaveWidth = 10.;\n\t\t\tconst auto ratio = std::min(state->data.ratio, 1.);\n\t\t\tconst auto reachRatio = state->data.reachRatio;\n\t\t\tconst auto rect = state->back->rect()\n\t\t\t\t- Margins(state->back->width() / 4);\n\t\t\tconst auto center = rect::center(rect);\n\t\t\tconst auto strokeWidth = style::ConvertFloatScale(kStrokeWidth)\n\t\t\t\t* sizeRatio;\n\n\t\t\tconst auto reachScale = std::clamp(\n\t\t\t\t(reachRatio > kBouncePart)\n\t\t\t\t\t? (kBouncePart * 2 - reachRatio)\n\t\t\t\t\t: reachRatio,\n\t\t\t\t0.,\n\t\t\t\t1.);\n\t\t\tauto pen = QPen(bg);\n\t\t\tpen.setWidthF(strokeWidth - (1. * (reachScale / kBouncePart)));\n\t\t\tconst auto arcRect = rect - Margins(strokeWidth);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setOpacity(ratio);\n\t\t\tif (reachScale || mirrored) {\n\t\t\t\tconst auto scale = (1. + 1. * reachScale);\n\t\t\t\tp.translate(center);\n\t\t\t\tp.scale(scale * (mirrored ? -1 : 1), scale);\n\t\t\t\tp.translate(-center);\n\t\t\t}\n\t\t\t{\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(bg);\n\t\t\t\tp.drawEllipse(rect);\n\t\t\t\tp.drawEllipse(rect);\n\t\t\t\tp.setPen(arrowPen);\n\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t\tconst auto halfSize = rect.width() / 2;\n\t\t\t\tconst auto arrowSize = halfSize / 2;\n\t\t\t\tconst auto arrowHalf = arrowSize / 2;\n\t\t\t\tconst auto arrowX = st::swipeBackSize / 8\n\t\t\t\t\t+ rect.x()\n\t\t\t\t\t+ halfSize;\n\t\t\t\tconst auto arrowY = rect.y() + halfSize;\n\n\t\t\t\tauto arrowPath = QPainterPath();\n\t\t\t\tconst auto direction = iconMirrored ? -1 : 1;\n\n\t\t\t\tarrowPath.moveTo(arrowX + direction * arrowSize, arrowY);\n\t\t\t\tarrowPath.lineTo(arrowX, arrowY);\n\t\t\t\tarrowPath.lineTo(\n\t\t\t\t\tarrowX + direction * arrowHalf,\n\t\t\t\t\tarrowY - arrowHalf);\n\t\t\t\tarrowPath.moveTo(arrowX, arrowY);\n\t\t\t\tarrowPath.lineTo(\n\t\t\t\t\tarrowX + direction * arrowHalf,\n\t\t\t\t\tarrowY + arrowHalf);\n\t\t\t\tarrowPath.translate(-direction * arrowHalf, 0);\n\t\t\t\tp.drawPath(arrowPath);\n\t\t\t}\n\t\t\tif (reachRatio) {\n\t\t\t\tp.setPen(pen);\n\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t\tconst auto w = style::ConvertFloatScale(kWaveWidth)\n\t\t\t\t\t* sizeRatio;\n\t\t\t\tp.setOpacity(ratio - reachRatio);\n\t\t\t\tp.drawArc(\n\t\t\t\t\tarcRect + Margins(reachRatio * reachRatio * w),\n\t\t\t\t\tarc::kQuarterLength,\n\t\t\t\t\tarc::kFullLength);\n\t\t\t}\n\t\t};\n\t};\n\n\tconst auto callback = ([=](SwipeContextData data) {\n\t\tconst auto ratio = std::min(1.0, data.ratio);\n\t\tstate->data = std::move(data);\n\t\tif (ratio > 0) {\n\t\t\tif (!state->back) {\n\t\t\t\tstate->back = base::make_unique_q<Ui::RpWidget>(widget);\n\t\t\t\tconst auto raw = state->back.get();\n\t\t\t\traw->paintRequest(\n\t\t\t\t) | rpl::on_next(paintCallback(), raw->lifetime());\n\t\t\t\traw->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\t\traw->resize(Size(st::swipeBackSize));\n\t\t\t\traw->show();\n\t\t\t\traw->raise();\n\t\t\t}\n\t\t\tif (!mirrored) {\n\t\t\t\tstate->back->moveToLeft(\n\t\t\t\t\tanim::interpolate(\n\t\t\t\t\t\t-st::swipeBackSize * kMaxOuterOffset,\n\t\t\t\t\t\tmaxOffset - st::swipeBackSize,\n\t\t\t\t\t\tratio),\n\t\t\t\t\t(widget->height() - state->back->height()) / 2);\n\t\t\t} else {\n\t\t\t\tstate->back->moveToLeft(\n\t\t\t\t\tanim::interpolate(\n\t\t\t\t\t\twidget->width() + st::swipeBackSize * kMaxOuterOffset,\n\t\t\t\t\t\twidget->width() - maxOffset,\n\t\t\t\t\t\tratio),\n\t\t\t\t\t(widget->height() - state->back->height()) / 2);\n\t\t\t}\n\t\t\tstate->back->update();\n\t\t} else if (state->back) {\n\t\t\tstate->back = nullptr;\n\t\t}\n\t});\n\treturn { std::move(lifetime), std::move(callback) };\n}\n\nSwipeHandlerFinishData DefaultSwipeBackHandlerFinishData(\n\t\tFn<void(void)> callback) {\n\treturn {\n\t\t.callback = std::move(callback),\n\t\t.msgBareId = kMsgBareIdSwipeBack,\n\t\t.speedRatio = kSwipedBackSpeedRatio,\n\t\t.keepRatioWithinRange = true,\n\t};\n}\n\n} // namespace Ui::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/swipe_handler.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass ElasticScroll;\nclass RpWidget;\nclass ScrollArea;\n} // namespace Ui\n\nnamespace Ui::Controls {\n\nstruct SwipeContextData;\nstruct SwipeBackResult;\n\nstruct SwipeHandlerFinishData {\n\tFn<void(void)> callback;\n\tint64 msgBareId = 0;\n\tfloat64 speedRatio = 1.0;\n\tcrl::time reachRatioDuration = 0;\n\tbool keepRatioWithinRange = false;\n\tbool provideReachOutRatio = false;\n};\n\nusing Scroll = std::variant<\n\tv::null_t,\n\tnot_null<Ui::ScrollArea*>,\n\tnot_null<Ui::ElasticScroll*>>;\n\nstruct SwipeHandlerArgs {\n\tnot_null<Ui::RpWidget*> widget;\n\tScroll scroll;\n\tFn<void(SwipeContextData)> update;\n\tFn<SwipeHandlerFinishData(int, Qt::LayoutDirection)> init;\n\trpl::producer<bool> dontStart = nullptr;\n\trpl::lifetime *onLifetime = nullptr;\n};\n\nvoid SetupSwipeHandler(SwipeHandlerArgs &&args);\n\n[[nodiscard]] SwipeBackResult SetupSwipeBack(\n\tnot_null<Ui::RpWidget*> widget,\n\tFn<std::pair<QColor, QColor>()> colors,\n\tbool mirrored = false,\n\tbool iconMirrored = false);\n\n[[nodiscard]] SwipeHandlerFinishData DefaultSwipeBackHandlerFinishData(\n\tFn<void(void)> callback);\n\n} // namespace Ui::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/swipe_handler_data.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui::Controls {\n\nstruct SwipeContextData final {\n\t[[nodiscard]] bool empty() const {\n\t\treturn !ratio\n\t\t\t&& !reachRatio\n\t\t\t&& !translation\n\t\t\t&& !cursorTop;\n\t}\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn !empty();\n\t}\n\n\tfloat64 ratio = 0.;\n\tfloat64 reachRatio = 0.;\n\tint64 msgBareId = 0;\n\tint translation = 0;\n\tint cursorTop = 0;\n};\n\nstruct SwipeBackResult final {\n\trpl::lifetime lifetime;\n\tFn<void(SwipeContextData)> callback;\n};\n\n} // namespace Ui::Controls\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/tabbed_search.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/tabbed_search.h\"\n\n#include \"base/qt_signal_producer.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_chat_helpers.h\"\n\n#include <QtWidgets/QApplication>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kDebounceTimeout = crl::time(400);\nconstexpr auto kCategoryIconSizeOverride = 22;\n\nclass GroupsStrip final : public RpWidget {\npublic:\n\tGroupsStrip(\n\t\tQWidget *parent,\n\t\tconst style::TabbedSearch &st,\n\t\trpl::producer<std::vector<EmojiGroup>> groups,\n\t\tText::CustomEmojiFactory factory);\n\n\tvoid scrollByWheel(QWheelEvent *e);\n\n\tstruct Chosen {\n\t\tnot_null<const EmojiGroup*> group;\n\t\tint iconLeft = 0;\n\t\tint iconRight = 0;\n\t};\n\t[[nodiscard]] rpl::producer<Chosen> chosen() const;\n\tvoid clearChosen();\n\n\t[[nodiscard]] rpl::producer<int> moveRequests() const;\n\nprivate:\n\tstruct Button {\n\t\tEmojiGroup group;\n\t\tQString iconId;\n\t\tstd::unique_ptr<Text::CustomEmoji> icon;\n\t};\n\n\tvoid init(rpl::producer<std::vector<EmojiGroup>> groups);\n\tvoid set(std::vector<EmojiGroup> list);\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\n\tvoid fireChosenGroup();\n\n\tstatic inline auto FindById(auto &&buttons, QStringView id) {\n\t\treturn ranges::find(buttons, id, &Button::iconId);\n\t}\n\n\tconst style::TabbedSearch &_st;\n\tconst Text::CustomEmojiFactory _factory;\n\n\tstd::vector<Button> _buttons;\n\trpl::event_stream<Chosen> _chosenGroup;\n\trpl::event_stream<int> _moveRequests;\n\tQPoint _globalPressPoint, _globalLastPoint;\n\tbool _dragging = false;\n\tint _pressed = -1;\n\tint _chosen = -1;\n\n};\n\n[[nodiscard]] std::vector<QString> FieldQuery(not_null<InputField*> field) {\n\tif (const auto last = field->getLastText(); !last.isEmpty()) {\n\t\treturn { last };\n\t}\n\treturn {};\n}\n\nGroupsStrip::GroupsStrip(\n\tQWidget *parent,\n\tconst style::TabbedSearch &st,\n\trpl::producer<std::vector<EmojiGroup>> groups,\n\tText::CustomEmojiFactory factory)\n: RpWidget(parent)\n, _st(st)\n, _factory(std::move(factory)) {\n\tinit(std::move(groups));\n}\n\nrpl::producer<GroupsStrip::Chosen> GroupsStrip::chosen() const {\n\treturn _chosenGroup.events();\n}\n\nrpl::producer<int> GroupsStrip::moveRequests() const {\n\treturn _moveRequests.events();\n}\n\nvoid GroupsStrip::clearChosen() {\n\tif (const auto chosen = std::exchange(_chosen, -1); chosen >= 0) {\n\t\tupdate();\n\t}\n}\n\nvoid GroupsStrip::init(rpl::producer<std::vector<EmojiGroup>> groups) {\n\tstd::move(\n\t\tgroups\n\t) | rpl::on_next([=](std::vector<EmojiGroup> &&list) {\n\t\tset(std::move(list));\n\t}, lifetime());\n\n\tsetCursor(style::cur_pointer);\n}\n\nvoid GroupsStrip::set(std::vector<EmojiGroup> list) {\n\tconst auto chosen = (_chosen >= 0)\n\t\t? _buttons[_chosen].group.iconId\n\t\t: QString();\n\tauto existing = std::move(_buttons);\n\tconst auto updater = [=](const QString &iconId) {\n\t\treturn [=] {\n\t\t\tconst auto i = FindById(_buttons, iconId);\n\t\t\tif (i != end(_buttons)) {\n\t\t\t\tconst auto index = i - begin(_buttons);\n\t\t\t\tconst auto single = _st.groupWidth;\n\t\t\t\tupdate(index * single, 0, single, height());\n\t\t\t}\n\t\t};\n\t};\n\tfor (auto &group : list) {\n\t\tconst auto i = FindById(existing, group.iconId);\n\t\tif (i != end(existing)) {\n\t\t\t_buttons.push_back(std::move(*i));\n\t\t\texisting.erase(i);\n\t\t} else {\n\t\t\tconst auto loopCount = 1;\n\t\t\tconst auto stopAtLastFrame = true;\n\t\t\t_buttons.push_back({\n\t\t\t\t.iconId = group.iconId,\n\t\t\t\t.icon = std::make_unique<Text::LimitedLoopsEmoji>(\n\t\t\t\t\t_factory(\n\t\t\t\t\t\tgroup.iconId,\n\t\t\t\t\t\t{ .repaint = updater(group.iconId) }),\n\t\t\t\t\tloopCount,\n\t\t\t\t\tstopAtLastFrame),\n\t\t\t});\n\t\t}\n\t\t_buttons.back().group = std::move(group);\n\t}\n\tresize(_buttons.size() * _st.groupWidth, height());\n\tif (!chosen.isEmpty()) {\n\t\tconst auto i = FindById(_buttons, chosen);\n\t\tif (i != end(_buttons)) {\n\t\t\t_chosen = (i - begin(_buttons));\n\t\t\tfireChosenGroup();\n\t\t} else {\n\t\t\t_chosen = -1;\n\t\t}\n\t}\n\tupdate();\n}\n\nvoid GroupsStrip::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tauto index = 0;\n\tconst auto single = _st.groupWidth;\n\tconst auto skip = _st.groupSkip;\n\tconst auto height = this->height();\n\tconst auto clip = e->rect();\n\tconst auto now = crl::now();\n\tfor (const auto &button : _buttons) {\n\t\tconst auto left = index * single;\n\t\tconst auto top = 0;\n\t\tconst auto size = SearchWithGroups::IconSizeOverride();\n\t\tif (_chosen == index) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(_st.bgActive);\n\t\t\tp.drawEllipse(\n\t\t\t\tleft + skip,\n\t\t\t\ttop + (height - single) / 2 + skip,\n\t\t\t\tsingle - 2 * skip,\n\t\t\t\tsingle - 2 * skip);\n\t\t}\n\t\tif (QRect(left, top, single, height).intersects(clip)) {\n\t\t\tbutton.icon->paint(p, {\n\t\t\t\t.textColor = (_chosen == index ? _st.fgActive : _st.fg)->c,\n\t\t\t\t.now = now,\n\t\t\t\t.position = QPoint(left, top) + QPoint(\n\t\t\t\t\t(single - size) / 2,\n\t\t\t\t\t(height - size) / 2),\n\t\t\t\t});\n\t\t}\n\t\t++index;\n\t}\n}\n\nvoid GroupsStrip::scrollByWheel(QWheelEvent *e) {\n\tauto horizontal = (e->angleDelta().x() != 0);\n\tauto vertical = (e->angleDelta().y() != 0);\n\tif (!horizontal && !vertical) {\n\t\treturn;\n\t}\n\tconst auto delta = horizontal\n\t\t? ((style::RightToLeft() ? -1 : 1) * (e->pixelDelta().x()\n\t\t\t? e->pixelDelta().x()\n\t\t\t: e->angleDelta().x()))\n\t\t: (e->pixelDelta().y()\n\t\t\t? e->pixelDelta().y()\n\t\t\t: e->angleDelta().y());\n\t_moveRequests.fire_copy(delta);\n}\n\nvoid GroupsStrip::mouseMoveEvent(QMouseEvent *e) {\n\tconst auto point = e->globalPos();\n\tif (!_dragging) {\n\t\tconst auto distance = (point - _globalPressPoint).manhattanLength();\n\t\tif (distance >= QApplication::startDragDistance()) {\n\t\t\t_dragging = true;\n\t\t\t_globalLastPoint = _globalPressPoint;\n\t\t}\n\t}\n\tif (_dragging) {\n\t\tconst auto delta = (point - _globalLastPoint).x();\n\t\t_globalLastPoint = point;\n\t\t_moveRequests.fire_copy(delta);\n\t}\n}\n\nvoid GroupsStrip::mousePressEvent(QMouseEvent *e) {\n\tconst auto index = e->pos().x() / _st.groupWidth;\n\tconst auto chosen = (index < 0 || index >= _buttons.size())\n\t\t? -1\n\t\t: index;\n\t_pressed = chosen;\n\t_globalPressPoint = e->globalPos();\n}\n\nvoid GroupsStrip::mouseReleaseEvent(QMouseEvent *e) {\n\tconst auto pressed = std::exchange(_pressed, -1);\n\tif (_dragging) {\n\t\t_dragging = false;\n\t\treturn;\n\t}\n\tconst auto index = e->pos().x() / _st.groupWidth;\n\tconst auto chosen = (index < 0 || index >= _buttons.size())\n\t\t? -1\n\t\t: index;\n\tif (pressed == chosen && chosen >= 0) {\n\t\t_chosen = pressed;\n\t\tfireChosenGroup();\n\t\tupdate();\n\t}\n}\n\nvoid GroupsStrip::fireChosenGroup() {\n\tExpects(_chosen >= 0 && _chosen < _buttons.size());\n\n\t_chosenGroup.fire({\n\t\t.group = &_buttons[_chosen].group,\n\t\t.iconLeft = _chosen * _st.groupWidth,\n\t\t.iconRight = (_chosen + 1) * _st.groupWidth,\n\t});\n}\n\n} // namespace\n\nconst QString &PremiumGroupFakeEmoticon() {\n\tstatic const auto result = u\"*premium\"_q;\n\treturn result;\n}\n\nSearchWithGroups::SearchWithGroups(\n\tQWidget *parent,\n\tSearchDescriptor descriptor)\n: RpWidget(parent)\n, _st(descriptor.st)\n, _search(CreateChild<FadeWrapScaled<IconButton>>(\n\tthis,\n\tobject_ptr<IconButton>(this, _st.search)))\n, _back(CreateChild<FadeWrapScaled<IconButton>>(\n\tthis,\n\tobject_ptr<IconButton>(this, _st.back)))\n, _cancel(CreateChild<CrossButton>(this, _st.cancel))\n, _field(CreateChild<InputField>(this, _st.field, tr::lng_dlg_filter()))\n, _groups(CreateChild<FadeWrapScaled<RpWidget>>(\n\tthis,\n\tobject_ptr<GroupsStrip>(\n\t\tthis,\n\t\t_st,\n\t\tstd::move(descriptor.groups),\n\t\tstd::move(descriptor.customEmojiFactory))))\n, _fade(CreateChild<RpWidget>(this))\n, _debounceTimer([=] { _debouncedQuery = _query.current(); }) {\n\tinitField();\n\tinitGroups();\n\tinitButtons();\n\tinitEdges();\n\t_inited = true;\n}\n\nanim::type SearchWithGroups::animated() const {\n\treturn _inited ? anim::type::normal : anim::type::instant;\n}\n\nvoid SearchWithGroups::initField() {\n\t_field->changes(\n\t) | rpl::on_next([=] {\n\t\tconst auto last = FieldQuery(_field);\n\t\t_query = last;\n\t\tconst auto empty = last.empty();\n\t\t_fieldEmpty = empty;\n\t\tif (empty) {\n\t\t\t_debounceTimer.cancel();\n\t\t\t_debouncedQuery = last;\n\t\t} else {\n\t\t\t_debounceTimer.callOnce(kDebounceTimeout);\n\t\t\t_chosenGroup = QString();\n\t\t\tscrollGroupsToStart();\n\t\t}\n\t}, _field->lifetime());\n\n\t_fieldPlaceholderWidth = tr::lng_dlg_filter(\n\t) | rpl::map([=](const QString &value) {\n\t\treturn _st.field.placeholderFont->width(value);\n\t}) | rpl::after_next([=] {\n\t\tresizeToWidth(width());\n\t});\n\n\tconst auto last = FieldQuery(_field);\n\t_query = last;\n\t_debouncedQuery = last;\n\t_fieldEmpty = last.empty();\n\t_fieldEmpty.value(\n\t) | rpl::on_next([=](bool empty) {\n\t\t_cancel->toggle(!empty, animated());\n\t\t_groups->toggle(empty, animated());\n\t\tresizeToWidth(width());\n\t}, lifetime());\n}\n\nvoid SearchWithGroups::initGroups() {\n\tconst auto widget = static_cast<GroupsStrip*>(_groups->entity());\n\n\tconst auto &search = _st.search;\n\t_fadeLeftStart = search.iconPosition.x() + search.icon.width();\n\t_groups->move(_fadeLeftStart + _st.defaultFieldWidth, 0);\n\twidget->resize(widget->width(), _st.height);\n\twidget->widthValue(\n\t) | rpl::filter([=] {\n\t\treturn (width() > 0);\n\t}) | rpl::on_next([=] {\n\t\tresizeToWidth(width());\n\t}, widget->lifetime());\n\n\twidget->chosen(\n\t) | rpl::on_next([=](const GroupsStrip::Chosen &chosen) {\n\t\t_chosenGroup = chosen.group->iconId;\n\t\t_query = (chosen.group->type == EmojiGroupType::Premium)\n\t\t\t? std::vector{ PremiumGroupFakeEmoticon() }\n\t\t\t: chosen.group->emoticons;\n\t\t_debouncedQuery = chosen.group->emoticons;\n\t\t_debounceTimer.cancel();\n\t\tscrollGroupsToIcon(chosen.iconLeft, chosen.iconRight);\n\t}, lifetime());\n\n\twidget->moveRequests(\n\t) | rpl::on_next([=](int delta) {\n\t\tmoveGroupsBy(width(), delta);\n\t}, lifetime());\n\n\t_chosenGroup.value(\n\t) | rpl::map([=](const QString &id) {\n\t\treturn id.isEmpty();\n\t}) | rpl::on_next([=](bool empty) {\n\t\t_search->toggle(empty, animated());\n\t\t_back->toggle(!empty, animated());\n\t\tif (empty) {\n\t\t\twidget->clearChosen();\n\t\t\tif (_field->getLastText().isEmpty()) {\n\t\t\t\t_query = {};\n\t\t\t\t_debouncedQuery = {};\n\t\t\t\t_debounceTimer.cancel();\n\t\t\t}\n\t\t} else {\n\t\t\t_field->setText({});\n\t\t}\n\t}, lifetime());\n}\n\nvoid SearchWithGroups::scrollGroupsToIcon(int iconLeft, int iconRight) {\n\tconst auto single = _st.groupWidth;\n\tconst auto fadeRight = _fadeLeftStart + _st.fadeLeft.width();\n\tif (_groups->x() < fadeRight + single - iconLeft) {\n\t\tscrollGroupsTo(fadeRight + single - iconLeft);\n\t} else if (_groups->x() > width() - single - iconRight) {\n\t\tscrollGroupsTo(width() - single - iconRight);\n\t} else {\n\t\t_groupsLeftAnimation.stop();\n\t}\n}\n\nvoid SearchWithGroups::scrollGroupsToStart() {\n\tscrollGroupsTo(width());\n}\n\nvoid SearchWithGroups::scrollGroupsTo(int left) {\n\tleft = clampGroupsLeft(width(), left);\n\t_groupsLeftTo = left;\n\tconst auto delta = _groupsLeftTo - _groups->x();\n\tif (!delta) {\n\t\t_groupsLeftAnimation.stop();\n\t\treturn;\n\t}\n\t_groupsLeftAnimation.start([=] {\n\t\tconst auto d = int(base::SafeRound(_groupsLeftAnimation.value(0)));\n\t\tmoveGroupsTo(width(), _groupsLeftTo - d);\n\t}, delta, 0, st::slideWrapDuration, anim::sineInOut);\n}\n\nvoid SearchWithGroups::initEdges() {\n\tpaintRequest() | rpl::on_next([=](QRect clip) {\n\t\tQPainter(this).fillRect(clip, _st.bg);\n\t}, lifetime());\n\n\tconst auto makeEdge = [&](bool left) {\n\t\tconst auto edge = CreateChild<RpWidget>(this);\n\t\tconst auto size = QSize(height() / 2, height());\n\t\tedge->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tedge->resize(size);\n\t\tif (left) {\n\t\t\tedge->move(0, 0);\n\t\t} else {\n\t\t\twidthValue(\n\t\t\t) | rpl::on_next([=](int width) {\n\t\t\t\tedge->move(width - edge->width(), 0);\n\t\t\t}, edge->lifetime());\n\t\t}\n\t\tedge->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tconst auto ratio = edge->devicePixelRatioF();\n\t\t\tensureRounding(height(), ratio);\n\t\t\tconst auto size = _rounding.height();\n\t\t\tconst auto half = size / 2;\n\t\t\tQPainter(edge).drawImage(\n\t\t\t\tQPoint(),\n\t\t\t\t_rounding,\n\t\t\t\tQRect(left ? 0 : _rounding.width() - half, 0, half, size));\n\t\t}, edge->lifetime());\n\t};\n\tmakeEdge(true);\n\tmakeEdge(false);\n\n\t_fadeOpacity.changes(\n\t) | rpl::on_next([=] {\n\t\t_fade->update();\n\t}, _fade->lifetime());\n\n\t_fade->paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(_fade);\n\t\tp.setOpacity(_fadeOpacity.current());\n\t\tconst auto fill = QRect(0, 0, _fadeLeftStart, _st.height);\n\t\tif (fill.intersects(clip)) {\n\t\t\tp.fillRect(fill, _st.bg);\n\t\t}\n\t\tconst auto icon = QRect(\n\t\t\t_fadeLeftStart,\n\t\t\t0,\n\t\t\t_st.fadeLeft.width(),\n\t\t\t_st.height);\n\t\tif (clip.intersects(icon)) {\n\t\t\t_st.fadeLeft.fill(p, icon);\n\t\t}\n\t}, _fade->lifetime());\n\t_fade->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\t_rounding = QImage();\n\t}, lifetime());\n}\n\nvoid SearchWithGroups::initButtons() {\n\t_cancel->setClickedCallback([=] {\n\t\t_field->setText(QString());\n\t});\n\t_back->entity()->setClickedCallback([=] {\n\t\t_chosenGroup = QString();\n\t\tscrollGroupsToStart();\n\t});\n\t_search->entity()->setClickedCallback([=] {\n\t\t_field->setFocus();\n\t\tscrollGroupsToStart();\n\t});\n\t_field->focusedChanges(\n\t) | rpl::filter(rpl::mappers::_1) | rpl::on_next([=] {\n\t\tscrollGroupsToStart();\n\t}, _field->lifetime());\n\t_field->raise();\n\t_fade->raise();\n\t_search->raise();\n\t_back->raise();\n\t_cancel->raise();\n}\n\nvoid SearchWithGroups::ensureRounding(int size, float64 ratio) {\n\tconst auto rounded = qRound(size * ratio);\n\tconst auto full = QSize(rounded + 4, rounded);\n\tif (_rounding.size() != full) {\n\t\t_rounding = QImage(full, QImage::Format_ARGB32_Premultiplied);\n\t\t_rounding.fill(_st.outer->c);\n\t\tauto p = QPainter(&_rounding);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tp.setBrush(Qt::transparent);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawRoundedRect(QRect(QPoint(), full), rounded / 2., rounded / 2.);\n\t}\n\t_rounding.setDevicePixelRatio(ratio);\n}\n\nrpl::producer<> SearchWithGroups::escapes() const {\n\treturn _field->cancelled();\n}\n\nrpl::producer<std::vector<QString>> SearchWithGroups::queryValue() const {\n\treturn _query.value();\n}\n\nauto SearchWithGroups::debouncedQueryValue() const\n-> rpl::producer<std::vector<QString>> {\n\treturn _debouncedQuery.value();\n}\n\nvoid SearchWithGroups::cancel() {\n\t_field->setText(QString());\n\t_chosenGroup = QString();\n\tscrollGroupsToStart();\n}\n\nvoid SearchWithGroups::setLoading(bool loading) {\n\t_cancel->setLoadingAnimation(loading);\n}\n\nvoid SearchWithGroups::stealFocus() {\n\tif (!_focusTakenFrom) {\n\t\t_focusTakenFrom = QApplication::focusWidget();\n\t}\n\t_field->setFocus();\n}\n\nvoid SearchWithGroups::returnFocus() {\n\tif (_field && _focusTakenFrom) {\n\t\tif (_field->hasFocus()) {\n\t\t\t_focusTakenFrom->setFocus();\n\t\t}\n\t\t_focusTakenFrom = nullptr;\n\t}\n}\n\nint SearchWithGroups::IconSizeOverride() {\n\treturn style::ConvertScale(kCategoryIconSizeOverride);\n}\n\nint SearchWithGroups::resizeGetHeight(int newWidth) {\n\tif (!newWidth) {\n\t\treturn _st.height;\n\t}\n\t_back->moveToLeft(0, 0, newWidth);\n\t_search->moveToLeft(0, 0, newWidth);\n\t_cancel->moveToRight(0, 0, newWidth);\n\n\tmoveGroupsBy(newWidth, 0);\n\n\tconst auto fadeWidth = _fadeLeftStart + _st.fadeLeft.width();\n\tconst auto fade = QRect(0, 0, fadeWidth, _st.height);\n\t_fade->setGeometry(fade);\n\n\treturn _st.height;\n}\n\nvoid SearchWithGroups::wheelEvent(QWheelEvent *e) {\n\tstatic_cast<GroupsStrip*>(_groups->entity())->scrollByWheel(e);\n}\n\nint SearchWithGroups::clampGroupsLeft(int width, int desiredLeft) const {\n\tconst auto groupsLeftDefault = _fadeLeftStart + _st.defaultFieldWidth;\n\tconst auto groupsLeftMin = width - _groups->entity()->width();\n\tconst auto groupsLeftMax = std::max(groupsLeftDefault, groupsLeftMin);\n\treturn std::clamp(desiredLeft, groupsLeftMin, groupsLeftMax);\n}\n\nvoid SearchWithGroups::moveGroupsBy(int width, int delta) {\n\tmoveGroupsTo(width, _groups->x() + delta);\n}\n\nvoid SearchWithGroups::moveGroupsTo(int width, int to) {\n\tconst auto groupsLeft = clampGroupsLeft(width, to);\n\t_groups->move(groupsLeft, 0);\n\n\tconst auto placeholderMargins = _st.field.textMargins\n\t\t+ _st.field.placeholderMargins;\n\tconst auto placeholderWidth = _fieldPlaceholderWidth.current();\n\tconst auto fieldWidthMin = std::min(\n\t\trect::m::sum::h(placeholderMargins) + placeholderWidth,\n\t\t_st.defaultFieldWidth);\n\tconst auto fieldWidth = _fieldEmpty.current()\n\t\t? std::max(groupsLeft - _st.search.width, fieldWidthMin)\n\t\t: (width - _fadeLeftStart - _st.cancel.width);\n\t_field->resizeToWidth(fieldWidth);\n\tconst auto fieldLeft = _fieldEmpty.current()\n\t\t? (groupsLeft - fieldWidth)\n\t\t: _fadeLeftStart;\n\t_field->moveToLeft(fieldLeft, 0);\n\n\tif (fieldLeft >= _fadeLeftStart) {\n\t\tif (!_fade->isHidden()) {\n\t\t\t_fade->hide();\n\t\t}\n\t} else {\n\t\tif (_fade->isHidden()) {\n\t\t\t_fade->show();\n\t\t}\n\t\t_fadeOpacity = (fieldLeft < _fadeLeftStart / 2)\n\t\t\t? 1.\n\t\t\t: (_fadeLeftStart - fieldLeft) / float64(_fadeLeftStart / 2);\n\t}\n}\n\nTabbedSearch::TabbedSearch(\n\tnot_null<RpWidget*> parent,\n\tconst style::EmojiPan &st,\n\tSearchDescriptor &&descriptor)\n: _st(st)\n, _search(parent, std::move(descriptor)) {\n\t_search.move(_st.searchMargin.left(), _st.searchMargin.top());\n\n\tparent->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_search.resizeToWidth(width - rect::m::sum::h(_st.searchMargin));\n\t}, _search.lifetime());\n}\n\nint TabbedSearch::height() const {\n\treturn _search.height() + rect::m::sum::v(_st.searchMargin);\n}\n\nQImage TabbedSearch::grab() {\n\treturn Ui::GrabWidgetToImage(&_search);\n}\n\nvoid TabbedSearch::cancel() {\n\t_search.cancel();\n}\n\nvoid TabbedSearch::setLoading(bool loading) {\n\t_search.setLoading(loading);\n}\n\nvoid TabbedSearch::stealFocus() {\n\t_search.stealFocus();\n}\n\nvoid TabbedSearch::returnFocus() {\n\t_search.returnFocus();\n}\n\nrpl::producer<> TabbedSearch::escapes() const {\n\treturn _search.escapes();\n}\n\nrpl::producer<std::vector<QString>> TabbedSearch::queryValue() const {\n\treturn _search.queryValue();\n}\n\nauto TabbedSearch::debouncedQueryValue() const\n-> rpl::producer<std::vector<QString>> {\n\treturn _search.debouncedQueryValue();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/tabbed_search.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"base/qt/qt_compare.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/text/text_custom_emoji.h\"\n\nnamespace style {\nstruct EmojiPan;\nstruct TabbedSearch;\n} // namespace style\n\nnamespace anim {\nenum class type : uchar;\n} // namespace anim\n\nnamespace Ui {\n\nclass InputField;\nclass IconButton;\nclass CrossButton;\nclass RpWidget;\ntemplate <typename Widget>\nclass FadeWrap;\n\nenum class EmojiGroupType {\n\tNormal,\n\tGreeting,\n\tPremium,\n};\n\nstruct EmojiGroup {\n\tQString iconId;\n\tstd::vector<QString> emoticons;\n\tEmojiGroupType type = EmojiGroupType::Normal;\n\n\tfriend inline auto operator<=>(\n\t\tconst EmojiGroup &a,\n\t\tconst EmojiGroup &b) = default;\n};\n\n[[nodiscard]] const QString &PremiumGroupFakeEmoticon();\n\nstruct SearchDescriptor {\n\tconst style::TabbedSearch &st;\n\trpl::producer<std::vector<EmojiGroup>> groups;\n\tText::CustomEmojiFactory customEmojiFactory;\n};\n\nclass SearchWithGroups final : public RpWidget {\npublic:\n\tSearchWithGroups(QWidget *parent, SearchDescriptor descriptor);\n\n\t[[nodiscard]] rpl::producer<> escapes() const;\n\t[[nodiscard]] rpl::producer<std::vector<QString>> queryValue() const;\n\t[[nodiscard]] auto debouncedQueryValue() const\n\t\t-> rpl::producer<std::vector<QString>>;\n\n\tvoid cancel();\n\tvoid setLoading(bool loading);\n\tvoid stealFocus();\n\tvoid returnFocus();\n\n\t[[nodiscard]] static int IconSizeOverride();\n\nprivate:\n\tint resizeGetHeight(int newWidth) override;\n\tvoid wheelEvent(QWheelEvent *e) override;\n\n\t[[nodiscard]] int clampGroupsLeft(int width, int desiredLeft) const;\n\tvoid moveGroupsBy(int width, int delta);\n\tvoid moveGroupsTo(int width, int to);\n\tvoid scrollGroupsToIcon(int iconLeft, int iconRight);\n\tvoid scrollGroupsToStart();\n\tvoid scrollGroupsTo(int left);\n\n\t[[nodiscard]] anim::type animated() const;\n\tvoid initField();\n\tvoid initGroups();\n\tvoid initEdges();\n\tvoid initButtons();\n\n\tvoid ensureRounding(int size, float64 rounding);\n\n\tconst style::TabbedSearch &_st;\n\tnot_null<FadeWrap<IconButton>*> _search;\n\tnot_null<FadeWrap<IconButton>*> _back;\n\tnot_null<CrossButton*> _cancel;\n\tnot_null<InputField*> _field;\n\tQPointer<QWidget> _focusTakenFrom;\n\tnot_null<FadeWrap<RpWidget>*> _groups;\n\tnot_null<RpWidget*> _fade;\n\trpl::variable<float64> _fadeOpacity = 0.;\n\tint _fadeLeftStart = 0;\n\n\trpl::variable<int> _fieldPlaceholderWidth;\n\trpl::variable<bool> _fieldEmpty = true;\n\tUi::Animations::Simple _groupsLeftAnimation;\n\tint _groupsLeftTo = 0;\n\n\tQImage _rounding;\n\n\trpl::variable<std::vector<QString>> _query;\n\trpl::variable<std::vector<QString>> _debouncedQuery;\n\trpl::variable<QString> _chosenGroup;\n\tbase::Timer _debounceTimer;\n\tbool _inited = false;\n\n};\n\nclass TabbedSearch final {\npublic:\n\tTabbedSearch(\n\t\tnot_null<RpWidget*> parent,\n\t\tconst style::EmojiPan &st,\n\t\tSearchDescriptor &&descriptor);\n\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] QImage grab();\n\n\t[[nodiscard]] rpl::producer<> escapes() const;\n\t[[nodiscard]] rpl::producer<std::vector<QString>> queryValue() const;\n\t[[nodiscard]] auto debouncedQueryValue() const\n\t\t->rpl::producer<std::vector<QString>>;\n\n\tvoid cancel();\n\tvoid setLoading(bool loading);\n\tvoid stealFocus();\n\tvoid returnFocus();\n\nprivate:\n\tconst style::EmojiPan &_st;\n\tSearchWithGroups _search;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/table_rows.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/table_rows.h\"\n\n#include \"base/event_filter.h\"\n#include \"base/timer_rpl.h\"\n#include \"boxes/peers/prepare_short_info_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"data/data_session.h\"\n#include \"data/data_peer.h\"\n#include \"info/profile/info_profile_badge.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/wrap/table_layout.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/rect.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_giveaway.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kTooltipDuration = 6 * crl::time(1000);\n\n} // namespace\n\nvoid AddTableRow(\n\t\tnot_null<TableLayout*> table,\n\t\trpl::producer<QString> label,\n\t\tobject_ptr<RpWidget> value,\n\t\tstyle::margins valueMargins) {\n\ttable->addRow(\n\t\t(label\n\t\t\t? object_ptr<FlatLabel>(\n\t\t\t\ttable,\n\t\t\t\tstd::move(label),\n\t\t\t\ttable->st().defaultLabel)\n\t\t\t: object_ptr<FlatLabel>(nullptr)),\n\t\tstd::move(value),\n\t\tst::giveawayGiftCodeLabelMargin,\n\t\tvalueMargins);\n}\n\nnot_null<FlatLabel*> AddTableRow(\n\t\tnot_null<TableLayout*> table,\n\t\trpl::producer<QString> label,\n\t\trpl::producer<TextWithEntities> value,\n\t\tconst Text::MarkedContext &context) {\n\tauto widget = object_ptr<FlatLabel>(\n\t\ttable,\n\t\tstd::move(value),\n\t\ttable->st().defaultValue,\n\t\tst::defaultPopupMenu,\n\t\tcontext);\n\tconst auto result = widget.data();\n\tAddTableRow(table, std::move(label), std::move(widget));\n\treturn result;\n}\n\nvoid AddTableRow(\n\t\tnot_null<TableLayout*> table,\n\t\trpl::producer<QString> label,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tPeerId id) {\n\tif (!id) {\n\t\treturn;\n\t}\n\tAddTableRow(\n\t\ttable,\n\t\tstd::move(label),\n\t\tMakePeerTableValue(table, show, id),\n\t\tst::giveawayGiftCodePeerMargin);\n}\n\nValueWithSmallButton MakeValueWithSmallButton(\n\t\tnot_null<TableLayout*> table,\n\t\tnot_null<RpWidget*> value,\n\t\trpl::producer<QString> buttonText,\n\t\tFn<void(not_null<RpWidget*> button)> handler,\n\t\tint topSkip) {\n\tclass MarginedWidget final : public RpWidget {\n\tpublic:\n\t\tusing RpWidget::RpWidget;\n\t\tQMargins getMargins() const override {\n\t\t\treturn { 0, 0, 0, st::giveawayGiftCodePeerMargin.bottom() };\n\t\t}\n\t};\n\tauto result = object_ptr<MarginedWidget>(table);\n\tconst auto raw = result.data();\n\n\tvalue->setParent(raw);\n\tvalue->show();\n\n\tconst auto button = CreateChild<RoundButton>(\n\t\traw,\n\t\tstd::move(buttonText),\n\t\ttable->st().smallButton);\n\tif (handler) {\n\t\tbutton->setClickedCallback([button, handler = std::move(handler)] {\n\t\t\thandler(button);\n\t\t});\n\t} else {\n\t\tbutton->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t}\n\trpl::combine(\n\t\traw->widthValue(),\n\t\tbutton->widthValue(),\n\t\tvalue->naturalWidthValue()\n\t) | rpl::on_next([=](int width, int buttonWidth, int) {\n\t\tconst auto buttonSkip = st::normalFont->spacew + buttonWidth;\n\t\tvalue->resizeToNaturalWidth(width - buttonSkip);\n\t\tvalue->moveToLeft(0, 0, width);\n\t\tbutton->moveToLeft(\n\t\t\trect::right(value) + st::normalFont->spacew,\n\t\t\t(topSkip\n\t\t\t\t+ (table->st().defaultValue.style.font->ascent\n\t\t\t\t\t- table->st().smallButton.style.font->ascent)),\n\t\t\twidth);\n\t}, value->lifetime());\n\n\tvalue->heightValue() | rpl::on_next([=](int height) {\n\t\tconst auto bottom = st::giveawayGiftCodePeerMargin.bottom();\n\t\traw->resize(raw->width(), height + bottom);\n\t}, raw->lifetime());\n\n\treturn {\n\t\t.widget = std::move(result),\n\t\t.button = button,\n\t};\n}\n\nobject_ptr<RpWidget> MakePeerTableValue(\n\t\tnot_null<TableLayout*> table,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tPeerId id,\n\t\trpl::producer<QString> button,\n\t\tFn<void()> handler) {\n\tauto result = object_ptr<AbstractButton>(table);\n\tconst auto raw = result.data();\n\n\tconst auto &st = st::giveawayGiftCodeUserpic;\n\traw->resize(raw->width(), st.photoSize);\n\n\tconst auto peer = show->session().data().peer(id);\n\tconst auto userpic = CreateChild<UserpicButton>(raw, peer, st);\n\tconst auto label = CreateChild<FlatLabel>(\n\t\traw,\n\t\t(button && handler) ? peer->shortName() : peer->name(),\n\t\ttable->st().defaultValue);\n\n\traw->widthValue() | rpl::on_next([=](int width) {\n\t\tconst auto position = st::giveawayGiftCodeNamePosition;\n\t\tlabel->resizeToNaturalWidth(width - position.x());\n\t\tlabel->moveToLeft(position.x(), position.y(), width);\n\t\tconst auto top = (raw->height() - userpic->height()) / 2;\n\t\tuserpic->moveToLeft(0, top, width);\n\t}, label->lifetime());\n\n\tlabel->naturalWidthValue() | rpl::on_next([=](int width) {\n\t\traw->setNaturalWidth(st::giveawayGiftCodeNamePosition.x() + width);\n\t}, label->lifetime());\n\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\trpl::single(\n\t\trpl::empty_value()\n\t) | rpl::then(style::PaletteChanged()) | rpl::on_next([=] {\n\t\tlabel->setTextColorOverride(\n\t\t\ttable->st().defaultValue.palette.linkFg->c);\n\t}, label->lifetime());\n\n\traw->setClickedCallback([=] {\n\t\tshow->showBox(PrepareShortInfoBox(peer, show));\n\t});\n\n\tif (!button || !handler) {\n\t\treturn result;\n\t}\n\treturn MakeValueWithSmallButton(\n\t\ttable,\n\t\tresult.release(),\n\t\tstd::move(button),\n\t\t[=](not_null<RpWidget*> button) { handler(); },\n\t\tst::giveawayGiftCodeNamePosition.y()).widget;\n}\n\nobject_ptr<RpWidget> MakePeerWithStatusValue(\n\t\tnot_null<TableLayout*> table,\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tPeerId id,\n\t\tFn<void(not_null<RpWidget*>, EmojiStatusId)> pushStatusId) {\n\tauto result = object_ptr<RpWidget>(table);\n\tconst auto raw = result.data();\n\n\tconst auto peerLabel = MakePeerTableValue(table, show, id).release();\n\tpeerLabel->setParent(raw);\n\tpeerLabel->show();\n\n\traw->resize(raw->width(), peerLabel->height());\n\n\tusing namespace Info::Profile;\n\tstruct State {\n\t\trpl::variable<Badge::Content> content;\n\t};\n\tconst auto peer = show->session().data().peer(id);\n\tconst auto state = peerLabel->lifetime().make_state<State>();\n\tstate->content = EmojiStatusIdValue(\n\t\tpeer\n\t) | rpl::map([=](EmojiStatusId emojiStatusId) {\n\t\tif (!peer->session().premium()\n\t\t\t|| (!peer->isSelf() && !emojiStatusId)) {\n\t\t\treturn Badge::Content();\n\t\t}\n\t\treturn Badge::Content{\n\t\t\t.badge = BadgeType::Premium,\n\t\t\t.emojiStatusId = emojiStatusId,\n\t\t};\n\t});\n\tconst auto badge = peerLabel->lifetime().make_state<Badge>(\n\t\traw,\n\t\tst::infoPeerBadge,\n\t\t&peer->session(),\n\t\tstate->content.value(),\n\t\tnullptr,\n\t\t[=] { return show->paused(ChatHelpers::PauseReason::Layer); });\n\tstate->content.value(\n\t) | rpl::on_next([=](const Badge::Content &content) {\n\t\tif (const auto widget = badge->widget()) {\n\t\t\tpushStatusId(widget, content.emojiStatusId);\n\t\t}\n\t}, raw->lifetime());\n\n\trpl::combine(\n\t\traw->widthValue(),\n\t\trpl::single(rpl::empty) | rpl::then(badge->updated())\n\t) | rpl::on_next([=](int width, const auto &) {\n\t\tconst auto badgeWidget = badge->widget();\n\t\tconst auto badgeSkip = badgeWidget\n\t\t\t? (st::normalFont->spacew + badgeWidget->width())\n\t\t\t: 0;\n\t\tpeerLabel->resizeToNaturalWidth(width - badgeSkip);\n\t\tpeerLabel->moveToLeft(0, 0, width);\n\t\tif (badgeWidget) {\n\t\t\tbadgeWidget->moveToLeft(\n\t\t\t\tpeerLabel->width() + st::normalFont->spacew,\n\t\t\t\tst::giftBoxByStarsStarTop,\n\t\t\t\twidth);\n\t\t}\n\t}, raw->lifetime());\n\n\treturn result;\n}\n\nobject_ptr<RpWidget> MakeHiddenPeerTableValue(\n\t\tnot_null<TableLayout*> table) {\n\tauto result = object_ptr<RpWidget>(table);\n\tconst auto raw = result.data();\n\n\tconst auto &st = st::giveawayGiftCodeUserpic;\n\traw->resize(raw->width(), st.photoSize);\n\n\tconst auto userpic = CreateChild<RpWidget>(raw);\n\tconst auto usize = st.photoSize;\n\tuserpic->resize(usize, usize);\n\tuserpic->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(userpic);\n\t\tEmptyUserpic::PaintHiddenAuthor(p, 0, 0, usize, usize);\n\t}, userpic->lifetime());\n\n\tconst auto label = CreateChild<FlatLabel>(\n\t\traw,\n\t\ttr::lng_gift_from_hidden(),\n\t\ttable->st().defaultValue);\n\traw->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto position = st::giveawayGiftCodeNamePosition;\n\t\tlabel->resizeToNaturalWidth(width - position.x());\n\t\tlabel->moveToLeft(position.x(), position.y(), width);\n\t\tconst auto top = (raw->height() - userpic->height()) / 2;\n\t\tuserpic->moveToLeft(0, top, width);\n\t}, label->lifetime());\n\n\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\trpl::single(\n\t\trpl::empty_value()\n\t) | rpl::then(style::PaletteChanged()) | rpl::on_next([=] {\n\t\tlabel->setTextColorOverride(st::windowFg->c);\n\t}, label->lifetime());\n\n\treturn result;\n}\n\nvoid ShowTableRowTooltip(\n\t\tstd::shared_ptr<TableRowTooltipData> data,\n\t\tnot_null<QWidget*> target,\n\t\trpl::producer<TextWithEntities> text,\n\t\tint duration,\n\t\tconst Text::MarkedContext &context) {\n\tif (data->raw) {\n\t\tdata->raw->toggleAnimated(false);\n\t}\n\tconst auto parent = data->parent;\n\tconst auto tooltip = CreateChild<ImportantTooltip>(\n\t\tparent,\n\t\tMakeNiceTooltipLabel(\n\t\t\tparent,\n\t\t\tstd::move(text),\n\t\t\tst::boxWideWidth,\n\t\t\tst::defaultImportantTooltipLabel,\n\t\t\tst::defaultPopupMenu,\n\t\t\tcontext),\n\t\tst::defaultImportantTooltip);\n\ttooltip->toggleFast(false);\n\n\tbase::install_event_filter(tooltip, qApp, [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::MouseButtonPress) {\n\t\t\ttooltip->toggleAnimated(false);\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\tconst auto update = [=] {\n\t\tconst auto geometry = MapFrom(parent, target, target->rect());\n\t\tconst auto countPosition = [=](QSize size) {\n\t\t\tconst auto left = geometry.x()\n\t\t\t\t+ (geometry.width() - size.width()) / 2;\n\t\t\tconst auto right = parent->width()\n\t\t\t\t- st::normalFont->spacew;\n\t\t\treturn QPoint(\n\t\t\t\tstd::max(std::min(left, right - size.width()), 0),\n\t\t\t\tgeometry.y() - size.height() - st::normalFont->descent);\n\t\t};\n\t\ttooltip->pointAt(geometry, RectPart::Top, countPosition);\n\t};\n\tparent->widthValue(\n\t) | rpl::on_next(update, tooltip->lifetime());\n\n\tupdate();\n\ttooltip->toggleAnimated(true);\n\n\tdata->raw = tooltip;\n\ttooltip->shownValue() | rpl::filter(\n\t\t!rpl::mappers::_1\n\t) | rpl::on_next([=] {\n\t\tcrl::on_main(tooltip, [=] {\n\t\t\tif (tooltip->isHidden()) {\n\t\t\t\tif (data->raw == tooltip) {\n\t\t\t\t\tdata->raw = nullptr;\n\t\t\t\t}\n\t\t\t\tdelete tooltip;\n\t\t\t}\n\t\t});\n\t}, tooltip->lifetime());\n\n\tbase::timer_once(\n\t\tduration\n\t) | rpl::on_next([=] {\n\t\ttooltip->toggleAnimated(false);\n\t}, tooltip->lifetime());\n}\n\nValueWithSmallButton MakeTableValueWithTooltip(\n\t\tnot_null<TableLayout*> table,\n\t\tstd::shared_ptr<TableRowTooltipData> data,\n\t\tTextWithEntities price,\n\t\tTextWithEntities tooltip,\n\t\tconst Text::MarkedContext &context) {\n\tconst auto label = CreateChild<FlatLabel>(\n\t\ttable,\n\t\trpl::single(price),\n\t\ttable->st().defaultValue,\n\t\tst::defaultPopupMenu,\n\t\tcontext);\n\tlabel->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto handler = [=](not_null<RpWidget*> button) {\n\t\tShowTableRowTooltip(\n\t\t\tdata,\n\t\t\tbutton,\n\t\t\trpl::single(tooltip),\n\t\t\tkTooltipDuration,\n\t\t\tcontext);\n\t};\n\tauto text = rpl::single(u\"?\"_q);\n\treturn MakeValueWithSmallButton(table, label, std::move(text), handler);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/table_rows.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nnamespace st {\nextern const style::margins &giveawayGiftCodeLabelMargin;\nextern const style::margins &giveawayGiftCodeValueMargin;\n} // namespace st\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Ui {\n\nclass RpWidget;\nclass TableLayout;\nclass FlatLabel;\nclass RoundButton;\nclass ImportantTooltip;\n\nvoid AddTableRow(\n\tnot_null<TableLayout*> table,\n\trpl::producer<QString> label,\n\tobject_ptr<RpWidget> value,\n\tstyle::margins valueMargins = st::giveawayGiftCodeValueMargin);\n\nnot_null<FlatLabel*> AddTableRow(\n\tnot_null<TableLayout*> table,\n\trpl::producer<QString> label,\n\trpl::producer<TextWithEntities> value,\n\tconst Text::MarkedContext &context = {});\n\nvoid AddTableRow(\n\tnot_null<TableLayout*> table,\n\trpl::producer<QString> label,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tPeerId id);\n\n\nstruct ValueWithSmallButton {\n\tobject_ptr<RpWidget> widget;\n\tnot_null<RoundButton*> button;\n};\n[[nodiscard]] ValueWithSmallButton MakeValueWithSmallButton(\n\tnot_null<TableLayout*> table,\n\tnot_null<RpWidget*> value,\n\trpl::producer<QString> buttonText,\n\tFn<void(not_null<RpWidget*> button)> handler = nullptr,\n\tint topSkip = 0);\n[[nodiscard]] object_ptr<RpWidget> MakePeerTableValue(\n\tnot_null<TableLayout*> table,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tPeerId id,\n\trpl::producer<QString> button = nullptr,\n\tFn<void()> handler = nullptr);\n[[nodiscard]] object_ptr<RpWidget> MakePeerWithStatusValue(\n\tnot_null<TableLayout*> table,\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tPeerId id,\n\tFn<void(not_null<RpWidget*>, EmojiStatusId)> pushStatusId);\n[[nodiscard]] object_ptr<RpWidget> MakeHiddenPeerTableValue(\n\tnot_null<TableLayout*> table);\n\nstruct TableRowTooltipData {\n\tnot_null<RpWidget*> parent;\n\tImportantTooltip *raw = nullptr;\n};\nvoid ShowTableRowTooltip(\n\tstd::shared_ptr<TableRowTooltipData> data,\n\tnot_null<QWidget*> target,\n\trpl::producer<TextWithEntities> text,\n\tint duration,\n\tconst Text::MarkedContext &context = {});\n\n[[nodiscard]] ValueWithSmallButton MakeTableValueWithTooltip(\n\tnot_null<TableLayout*> table,\n\tstd::shared_ptr<TableRowTooltipData> data,\n\tTextWithEntities price,\n\tTextWithEntities tooltip,\n\tconst Text::MarkedContext &context = {});\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/ton_common.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/ton_common.h\"\n\n#include \"base/qthelp_url.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n//#include \"styles/style_wallet.h\"\n\n#include <QtCore/QLocale>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kOneTon = kNanosInOne;\nconstexpr auto kNanoDigits = 9;\n\nstruct FixedAmount {\n\tQString text;\n\tint position = 0;\n};\n\nstd::optional<int64> ParseAmountTons(const QString &trimmed) {\n\tauto ok = false;\n\tconst auto grams = int64(trimmed.toLongLong(&ok));\n\treturn (ok\n\t\t&& (grams <= std::numeric_limits<int64>::max() / kOneTon)\n\t\t&& (grams >= std::numeric_limits<int64>::min() / kOneTon))\n\t\t? std::make_optional(grams * kOneTon)\n\t\t: std::nullopt;\n}\n\nstd::optional<int64> ParseAmountNano(QString trimmed) {\n\twhile (trimmed.size() < kNanoDigits) {\n\t\ttrimmed.append('0');\n\t}\n\tauto zeros = 0;\n\tfor (const auto ch : trimmed) {\n\t\tif (ch == '0') {\n\t\t\t++zeros;\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (zeros == trimmed.size()) {\n\t\treturn 0;\n\t} else if (trimmed.size() > kNanoDigits) {\n\t\treturn std::nullopt;\n\t}\n\tauto ok = false;\n\tconst auto value = trimmed.mid(zeros).toLongLong(&ok);\n\treturn (ok && value > 0 && value < kOneTon)\n\t\t? std::make_optional(value)\n\t\t: std::nullopt;\n}\n\n[[nodiscard]] FixedAmount FixTonAmountInput(\n\t\tconst QString &was,\n\t\tconst QString &text,\n\t\tint position) {\n\tconstexpr auto kMaxDigitsCount = 9;\n\tconst auto separator = FormatTonAmount(1).separator;\n\n\tauto result = FixedAmount{ text, position };\n\tif (text.isEmpty()) {\n\t\treturn result;\n\t} else if (text.startsWith('.')\n\t\t|| text.startsWith(',')\n\t\t|| text.startsWith(separator)) {\n\t\tresult.text.prepend('0');\n\t\t++result.position;\n\t}\n\tauto separatorFound = false;\n\tauto digitsCount = 0;\n\tfor (auto i = 0; i != result.text.size();) {\n\t\tconst auto ch = result.text[i];\n\t\tconst auto atSeparator = QStringView(result.text).mid(i).startsWith(separator);\n\t\tif (ch >= '0' && ch <= '9' && digitsCount < kMaxDigitsCount) {\n\t\t\t++i;\n\t\t\t++digitsCount;\n\t\t\tcontinue;\n\t\t} else if (!separatorFound\n\t\t\t&& (atSeparator || ch == '.' || ch == ',')) {\n\t\t\tseparatorFound = true;\n\t\t\tif (!atSeparator) {\n\t\t\t\tresult.text.replace(i, 1, separator);\n\t\t\t}\n\t\t\tdigitsCount = 0;\n\t\t\ti += separator.size();\n\t\t\tcontinue;\n\t\t}\n\t\tresult.text.remove(i, 1);\n\t\tif (result.position > i) {\n\t\t\t--result.position;\n\t\t}\n\t}\n\tif (result.text == \"0\" && result.position > 0) {\n\t\tif (was.startsWith('0')) {\n\t\t\tresult.text = QString();\n\t\t\tresult.position = 0;\n\t\t} else {\n\t\t\tresult.text += separator;\n\t\t\tresult.position += separator.size();\n\t\t}\n\t}\n\treturn result;\n}\n\n} // namespace\n\nFormattedTonAmount FormatTonAmount(int64 amount, TonFormatFlags flags) {\n\tauto result = FormattedTonAmount();\n\tconst auto grams = amount / kOneTon;\n\tconst auto preciseNanos = std::abs(amount) % kOneTon;\n\tauto roundedNanos = preciseNanos;\n\tif (flags & TonFormatFlag::Rounded) {\n\t\tif (std::abs(grams) >= 1'000'000 && (roundedNanos % 1'000'000)) {\n\t\t\troundedNanos -= (roundedNanos % 1'000'000);\n\t\t} else if (std::abs(grams) >= 1'000 && (roundedNanos % 1'000)) {\n\t\t\troundedNanos -= (roundedNanos % 1'000);\n\t\t}\n\t}\n\tconst auto precise = (roundedNanos == preciseNanos);\n\tauto nanos = preciseNanos;\n\tauto zeros = 0;\n\twhile (zeros < kNanoDigits && nanos % 10 == 0) {\n\t\tnanos /= 10;\n\t\t++zeros;\n\t}\n\tconst auto system = QLocale::system();\n\tconst auto locale = (flags & TonFormatFlag::Simple)\n\t\t? QLocale::c()\n\t\t: system;\n\tconst auto separator = system.decimalPoint();\n\n\tresult.wholeString = locale.toString(grams);\n\tif ((flags & TonFormatFlag::Signed) && amount > 0) {\n\t\tresult.wholeString = locale.positiveSign() + result.wholeString;\n\t} else if (amount < 0 && grams == 0) {\n\t\tresult.wholeString = locale.negativeSign() + result.wholeString;\n\t}\n\tresult.full = result.wholeString;\n\tif (zeros < kNanoDigits) {\n\t\tresult.separator = separator;\n\t\tresult.nanoString = QString(\"%1\"\n\t\t).arg(nanos, kNanoDigits - zeros, 10, QChar('0'));\n\t\tif (!precise) {\n\t\t\tconst auto nanoLength = (std::abs(grams) >= 1'000'000)\n\t\t\t\t? 3\n\t\t\t\t: (std::abs(grams) >= 1'000)\n\t\t\t\t? 6\n\t\t\t\t: 9;\n\t\t\tresult.nanoString = result.nanoString.mid(0, nanoLength);\n\t\t}\n\t\tresult.full += separator + result.nanoString;\n\t}\n\treturn result;\n}\n\nstd::optional<int64> ParseTonAmountString(const QString &amount) {\n\tconst auto trimmed = amount.trimmed();\n\tconst auto separator = QString(QLocale::system().decimalPoint());\n\tconst auto index1 = trimmed.indexOf('.');\n\tconst auto index2 = trimmed.indexOf(',');\n\tconst auto index3 = (separator == \".\" || separator == \",\")\n\t\t? -1\n\t\t: trimmed.indexOf(separator);\n\tconst auto found = (index1 >= 0 ? 1 : 0)\n\t\t+ (index2 >= 0 ? 1 : 0)\n\t\t+ (index3 >= 0 ? 1 : 0);\n\tif (found > 1) {\n\t\treturn std::nullopt;\n\t}\n\tconst auto index = (index1 >= 0)\n\t\t? index1\n\t\t: (index2 >= 0)\n\t\t? index2\n\t\t: index3;\n\tconst auto used = (index1 >= 0)\n\t\t? \".\"\n\t\t: (index2 >= 0)\n\t\t? \",\"\n\t\t: separator;\n\tconst auto grams = ParseAmountTons(trimmed.mid(0, index));\n\tconst auto nano = ParseAmountNano(trimmed.mid(index + used.size()));\n\tif (index < 0 || index == trimmed.size() - used.size()) {\n\t\treturn grams;\n\t} else if (index == 0) {\n\t\treturn nano;\n\t} else if (!nano || !grams) {\n\t\treturn std::nullopt;\n\t}\n\treturn *grams + (*grams < 0 ? (-*nano) : (*nano));\n}\n\nQString TonAmountSeparator() {\n\treturn FormatTonAmount(1).separator;\n}\n\nnot_null<Ui::InputField*> CreateTonAmountInput(\n\t\tnot_null<QWidget*> parent,\n\t\trpl::producer<QString> placeholder,\n\t\tint64 amount) {\n\tconst auto result = Ui::CreateChild<Ui::InputField>(\n\t\tparent.get(),\n\t\tst::editTagField,\n\t\tUi::InputField::Mode::SingleLine,\n\t\tstd::move(placeholder),\n\t\t(amount > 0\n\t\t\t? FormatTonAmount(amount, TonFormatFlag::Simple).full\n\t\t\t: QString()));\n\tconst auto lastAmountValue = std::make_shared<QString>();\n\tresult->changes() | rpl::on_next([=] {\n\t\tUi::PostponeCall(result, [=] {\n\t\t\tconst auto position = result->textCursor().position();\n\t\t\tconst auto now = result->getLastText();\n\t\t\tconst auto fixed = FixTonAmountInput(\n\t\t\t\t*lastAmountValue,\n\t\t\t\tnow,\n\t\t\t\tposition);\n\t\t\t*lastAmountValue = fixed.text;\n\t\t\tif (fixed.text == now) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresult->setText(fixed.text);\n\t\t\tresult->setFocusFast();\n\t\t\tresult->setCursorPosition(fixed.position);\n\t\t});\n\t}, result->lifetime());\n\treturn result;\n}\n\n} // namespace Wallet\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/ton_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flags.h\"\n\nnamespace Ui {\n\nclass InputField;\n\ninline constexpr auto kNanosInOne = 1'000'000'000LL;\n\nstruct FormattedTonAmount {\n\tQString wholeString;\n\tQString separator;\n\tQString nanoString;\n\tQString full;\n};\n\nenum class TonFormatFlag {\n\tSigned = 0x01,\n\tRounded = 0x02,\n\tSimple = 0x04,\n};\nconstexpr bool is_flag_type(TonFormatFlag) { return true; };\nusing TonFormatFlags = base::flags<TonFormatFlag>;\n\n[[nodiscard]] FormattedTonAmount FormatTonAmount(\n\tint64 amount,\n\tTonFormatFlags flags = TonFormatFlags());\n[[nodiscard]] std::optional<int64> ParseTonAmountString(\n\tconst QString &amount);\n\n[[nodiscard]] QString TonAmountSeparator();\n\n[[nodiscard]] not_null<Ui::InputField*> CreateTonAmountInput(\n\tnot_null<QWidget*> parent,\n\trpl::producer<QString> placeholder,\n\tint64 amount = 0);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/userpic_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/userpic_button.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_peer_photo.h\"\n#include \"ui/effects/upload_progress_overlay.h\"\n#include \"api/api_user_privacy.h\"\n#include \"base/call_delayed.h\"\n#include \"boxes/edit_privacy_box.h\"\n#include \"boxes/peers/edit_peer_info_box.h\" // EditPeerInfoBox::Available.\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/empty_userpic.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_user.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_streaming.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_photo_media.h\"\n#include \"history/history.h\"\n#include \"calls/calls_instance.h\"\n#include \"core/application.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"editor/photo_editor_common.h\"\n#include \"editor/photo_editor_layer_widget.h\"\n#include \"info/userpic/info_userpic_emoji_builder_common.h\"\n#include \"info/userpic/info_userpic_emoji_builder_menu_item.h\"\n#include \"media/streaming/media_streaming_instance.h\"\n#include \"media/streaming/media_streaming_player.h\"\n#include \"media/streaming/media_streaming_document.h\"\n#include \"settings/sections/settings_calls.h\" // AddCameraSubsection.\n#include \"settings/settings_privacy_controllers.h\"\n#include \"webrtc/webrtc_environment.h\"\n#include \"webrtc/webrtc_video_track.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"api/api_peer_photo.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_premium.h\"\n\n#include <QtGui/QClipboard>\n#include <QtGui/QGuiApplication>\n\nnamespace Ui {\nnamespace {\n\n[[nodiscard]] bool IsCameraAvailable() {\n\treturn (Core::App().calls().currentCall() == nullptr)\n\t\t&& !Core::App().mediaDevices().defaultId(\n\t\t\tWebrtc::DeviceType::Camera).isEmpty();\n}\n\nvoid CameraBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::Controller*> controller,\n\t\tPeerData *peer,\n\t\tbool forceForumShape,\n\t\tFn<void(QImage &&image)> &&doneCallback) {\n\tusing namespace Webrtc;\n\n\tconst auto track = Settings::AddCameraSubsection(\n\t\tbox->uiShow(),\n\t\tbox->verticalLayout(),\n\t\tfalse);\n\tif (!track) {\n\t\tbox->closeBox();\n\t\treturn;\n\t}\n\ttrack->stateValue(\n\t) | rpl::on_next([=](const VideoState &state) {\n\t\tif (state == VideoState::Inactive) {\n\t\t\tbox->closeBox();\n\t\t}\n\t}, box->lifetime());\n\n\tauto done = [=, done = std::move(doneCallback)]() mutable {\n\t\tusing namespace Editor;\n\t\tauto callback = [=, done = std::move(done)](QImage &&image) {\n\t\t\tbox->closeBox();\n\t\t\tdone(std::move(image));\n\t\t};\n\t\tconst auto useForumShape = forceForumShape\n\t\t\t|| (peer && peer->isForum() && !peer->isBot());\n\t\tPrepareProfilePhoto(\n\t\t\tbox,\n\t\t\tcontroller,\n\t\t\t{\n\t\t\t\t.confirm = tr::lng_profile_set_photo_button(tr::now),\n\t\t\t\t.cropType = (useForumShape\n\t\t\t\t\t? EditorData::CropType::RoundedRect\n\t\t\t\t\t: EditorData::CropType::Ellipse),\n\t\t\t\t.keepAspectRatio = true,\n\t\t\t},\n\t\t\tstd::move(callback),\n\t\t\ttrack->frame(FrameRequest()).mirrored(true, false));\n\t};\n\n\tbox->setTitle(tr::lng_profile_camera_title());\n\tbox->addButton(tr::lng_continue(), std::move(done));\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\ntemplate <typename Callback>\nQPixmap CreateSquarePixmap(int width, Callback &&paintCallback) {\n\tconst auto size = QSize(width, width) * style::DevicePixelRatio();\n\tauto image = QImage(size, QImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\timage.fill(Qt::transparent);\n\t{\n\t\tPainter p(&image);\n\t\tpaintCallback(p);\n\t}\n\treturn Ui::PixmapFromImage(std::move(image));\n};\n\nvoid SetupSubButtonBackground(\n\t\tnot_null<Ui::UserpicButton*> upload,\n\t\tnot_null<Ui::RpWidget*> background) {\n\tconst auto border = st::uploadUserpicButtonBorder;\n\tconst auto size = upload->rect().marginsAdded(\n\t\t{ border, border, border, border }\n\t).size();\n\n\tbackground->resize(size);\n\tbackground->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(background);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setBrush(st::boxBg);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.drawEllipse(background->rect());\n\t}, background->lifetime());\n\n\tupload->positionValue(\n\t) | rpl::on_next([=](QPoint position) {\n\t\tbackground->move(position - QPoint(border, border));\n\t}, background->lifetime());\n}\n\n[[nodiscard]] QBrush CreateDefaultGradientBrush(int size) {\n\tauto gradient = QLinearGradient(0, 0, 0, size);\n\tgradient.setStops({\n\t\t{ 0.0, st::historyPeer4UserpicBg->c },\n\t\t{ 1.0, st::historyPeer4UserpicBg2->c },\n\t});\n\treturn QBrush(std::move(gradient));\n}\n\n} // namespace\n\nUserpicButton::UserpicButton(\n\tQWidget *parent,\n\tnot_null<Window::Controller*> window,\n\tRole role,\n\tconst style::UserpicButton &st,\n\tPeerUserpicShape shape)\n: RippleButton(parent, st.changeButton.ripple)\n, _st(st)\n, _controller(window->sessionController())\n, _window(window)\n, _shape(shape)\n, _role(role) {\n\tExpects(_role == Role::ChangePhoto || _role == Role::ChoosePhoto);\n\n\tshowCustom({});\n\t_waiting = false;\n\tprepare();\n}\n\nUserpicButton::UserpicButton(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\tRole role,\n\tSource source,\n\tconst style::UserpicButton &st,\n\tPeerUserpicShape shape)\n: RippleButton(parent, st.changeButton.ripple)\n, _st(st)\n, _controller(controller)\n, _window(&controller->window())\n, _peer(peer)\n, _shape(shape)\n, _role(role)\n, _source(source) {\n\tif (_source == Source::Custom) {\n\t\tshowCustom({});\n\t} else {\n\t\tprocessPeerPhoto();\n\t\tsetupPeerViewers();\n\t}\n\tprepare();\n}\n\nUserpicButton::UserpicButton(\n\tQWidget *parent,\n\tnot_null<PeerData*> peer,\n\tconst style::UserpicButton &st,\n\tPeerUserpicShape shape)\n: RippleButton(parent, st.changeButton.ripple)\n, _st(st)\n, _peer(peer)\n, _shape(shape)\n, _role(Role::Custom)\n, _source(Source::PeerPhoto) {\n\tExpects(_role != Role::OpenPhoto);\n\n\tprocessPeerPhoto();\n\tsetupPeerViewers();\n\tprepare();\n}\n\nUserpicButton::~UserpicButton() = default;\n\nvoid UserpicButton::prepare() {\n\tresize(_st.size);\n\tsetNaturalWidth(_st.size.width());\n\t_notShownYet = _waiting;\n\tif (!_waiting) {\n\t\tprepareUserpicPixmap();\n\t}\n\tsetClickHandlerByRole();\n\n\tif (_role == Role::OpenPhoto) {\n\t\tsetAccessibleName(tr::lng_mediaview_profile_photo(tr::now));\n\t} else if (_role == Role::ChangePhoto || _role == Role::ChoosePhoto) {\n\t\tsetAccessibleName(tr::lng_profile_set_photo_for(tr::now));\n\t}\n}\n\nvoid UserpicButton::showCustomOnChosen() {\n\tchosenImages(\n\t) | rpl::on_next([=](ChosenImage &&chosen) {\n\t\tshowCustom(std::move(chosen.image));\n\t}, lifetime());\n}\n\nvoid UserpicButton::requestSuggestAvailability() {\n\tif (const auto user = _peer ? _peer->asUser() : nullptr) {\n\t\tif (!user->isSelf()) {\n\t\t\tconst auto history = user->owner().history(user);\n\t\t\tif (!history->lastServerMessageKnown()) {\n\t\t\t\t// Server allows suggesting photos only in non-empty chats.\n\t\t\t\tuser->owner().histories().requestDialogEntry(history);\n\t\t\t}\n\t\t}\n\t}\n}\n\nbool UserpicButton::canSuggestPhoto(not_null<UserData*> user) const {\n\t// Server allows suggesting photos only in non-empty chats.\n\treturn !user->isSelf()\n\t\t&& !user->isBot()\n\t\t&& !user->starsPerMessageChecked()\n\t\t&& (user->owner().history(user)->lastServerMessage() != nullptr);\n}\n\nbool UserpicButton::hasPersonalPhotoLocally() const {\n\tif (const auto user = _peer->asUser()) {\n\t\treturn _overrideHasPersonalPhoto.value_or(user->hasPersonalPhoto());\n\t}\n\treturn false;\n}\n\nvoid UserpicButton::setClickHandlerByRole() {\n\trequestSuggestAvailability();\n\n\tswitch (_role) {\n\tcase Role::ChoosePhoto:\n\tcase Role::ChangePhoto:\n\t\taddClickHandler([=] { choosePhotoLocally(); });\n\t\tbreak;\n\n\tcase Role::OpenPhoto:\n\t\taddClickHandler([=] { openPeerPhoto(); });\n\t\tbreak;\n\t}\n}\n\nvoid UserpicButton::choosePhotoLocally() {\n\tif (_uploadOverlay && _uploadOverlay->uploading()) {\n\t\t_peer->session().api().peerPhoto().cancelUpload(_peer);\n\t\treturn;\n\t}\n\tif (!_window) {\n\t\treturn;\n\t} else if (const auto controller = _window->sessionController()) {\n\t\tif (controller->showFrozenError()) {\n\t\t\treturn;\n\t\t}\n\t}\n\tconst auto callback = [=](ChosenType type) {\n\t\treturn [=](QImage &&image) {\n\t\t\t_chosenImages.fire({ std::move(image), type });\n\t\t};\n\t};\n\tconst auto editorData = [=](ChosenType type) {\n\t\tconst auto user = _peer ? _peer->asUser() : nullptr;\n\t\tconst auto name = (user && !user->firstName.isEmpty())\n\t\t\t? user->firstName\n\t\t\t: _peer\n\t\t\t? _peer->name()\n\t\t\t: QString();\n\t\tconst auto phrase = (type == ChosenType::Suggest)\n\t\t\t? &tr::lng_profile_suggest_sure\n\t\t\t: (user && EditPeerInfoBox::Available(user))\n\t\t\t? nullptr\n\t\t\t: (user && !user->isSelf())\n\t\t\t? &tr::lng_profile_set_personal_sure\n\t\t\t: nullptr;\n\t\treturn Editor::EditorData{\n\t\t\t.about = (phrase\n\t\t\t\t? (*phrase)(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_user,\n\t\t\t\t\ttr::bold(name),\n\t\t\t\t\ttr::marked)\n\t\t\t\t: TextWithEntities()),\n\t\t\t.confirm = ((type == ChosenType::Suggest)\n\t\t\t\t? tr::lng_profile_suggest_button(tr::now)\n\t\t\t\t: tr::lng_profile_set_photo_button(tr::now)),\n\t\t\t.cropType = (useForumShape()\n\t\t\t\t? Editor::EditorData::CropType::RoundedRect\n\t\t\t\t: Editor::EditorData::CropType::Ellipse),\n\t\t\t.keepAspectRatio = true,\n\t\t};\n\t};\n\tconst auto chooseFile = [=](ChosenType type) {\n\t\tbase::call_delayed(\n\t\t\t_st.changeButton.ripple.hideDuration,\n\t\t\tcrl::guard(this, [=] {\n\t\t\t\tPrepareProfilePhotoFromFile(\n\t\t\t\t\tthis,\n\t\t\t\t\t_window,\n\t\t\t\t\teditorData(type),\n\t\t\t\t\tcallback(type));\n\t\t\t}));\n\t};\n\tconst auto user = _peer ? _peer->asUser() : nullptr;\n\tconst auto addUserpicBuilder = [&](ChosenType type) {\n\t\tif (!_controller) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto done = [=](UserpicBuilder::Result data) {\n\t\t\tauto result = ChosenImage{ base::take(data.image), type };\n\t\t\tresult.markup.documentId = data.id;\n\t\t\tresult.markup.colors = base::take(data.colors);\n\t\t\t_chosenImages.fire(std::move(result));\n\t\t};\n\t\tUserpicBuilder::AddEmojiBuilderAction(\n\t\t\t_controller,\n\t\t\t_menu,\n\t\t\t_controller->session().api().peerPhoto().emojiListValue(user\n\t\t\t\t? Api::PeerPhoto::EmojiListType::Profile\n\t\t\t\t: Api::PeerPhoto::EmojiListType::Group),\n\t\t\tdone,\n\t\t\t_peer ? (_peer->isForum() && !_peer->isBot()) : false);\n\t};\n\tconst auto addFromClipboard = [=](ChosenType type, tr::phrase<> text) {\n\t\tif (const auto data = QGuiApplication::clipboard()->mimeData()) {\n\t\t\tif (data->hasImage()) {\n\t\t\t\tauto openEditor = crl::guard(this, [=, this] {\n\t\t\t\t\tEditor::PrepareProfilePhoto(\n\t\t\t\t\t\tthis,\n\t\t\t\t\t\t_window,\n\t\t\t\t\t\teditorData(type),\n\t\t\t\t\t\tcallback(type),\n\t\t\t\t\t\tqvariant_cast<QImage>(data->imageData()));\n\t\t\t\t});\n\t\t\t\t_menu->addAction(\n\t\t\t\t\tstd::move(text)(tr::now),\n\t\t\t\t\tstd::move(openEditor),\n\t\t\t\t\t&st::menuIconPhoto);\n\t\t\t}\n\t\t}\n\t};\n\t_menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tthis,\n\t\tst::popupMenuWithIcons);\n\tif (user && !user->isSelf()) {\n\t\t_menu->addAction(\n\t\t\ttr::lng_profile_set_photo_for(tr::now),\n\t\t\t[=] { chooseFile(ChosenType::Set); },\n\t\t\t&st::menuIconPhotoSet);\n\t\taddFromClipboard(\n\t\t\tChosenType::Set,\n\t\t\ttr::lng_profile_set_photo_for_from_clipboard);\n\t\tif (canSuggestPhoto(user)) {\n\t\t\t_menu->addAction(\n\t\t\t\ttr::lng_profile_suggest_photo(tr::now),\n\t\t\t\t[=] { chooseFile(ChosenType::Suggest); },\n\t\t\t\t&st::menuIconPhotoSuggest);\n\t\t\taddFromClipboard(\n\t\t\t\tChosenType::Suggest,\n\t\t\t\ttr::lng_profile_suggest_photo_from_clipboard);\n\t\t}\n\t\taddUserpicBuilder(ChosenType::Set);\n\t\tif (hasPersonalPhotoLocally()) {\n\t\t\t_menu->addSeparator(&st::expandedMenuSeparator);\n\t\t\t_menu->addAction(makeResetToOriginalAction());\n\t\t}\n\t} else {\n\t\tconst auto hasCamera = IsCameraAvailable();\n\t\tif (hasCamera || _controller) {\n\t\t\t_menu->addAction(tr::lng_attach_file(tr::now), [=] {\n\t\t\t\tchooseFile(ChosenType::Set);\n\t\t\t}, &st::menuIconPhoto);\n\t\t\tif (hasCamera) {\n\t\t\t\t_menu->addAction(tr::lng_attach_camera(tr::now), [=] {\n\t\t\t\t\t_window->show(Box(\n\t\t\t\t\t\tCameraBox,\n\t\t\t\t\t\t_window,\n\t\t\t\t\t\t_peer,\n\t\t\t\t\t\t(_shape == PeerUserpicShape::Forum),\n\t\t\t\t\t\tcallback(ChosenType::Set)));\n\t\t\t\t}, &st::menuIconPhotoSet);\n\t\t\t}\n\t\t\taddFromClipboard(\n\t\t\t\tChosenType::Set,\n\t\t\t\ttr::lng_profile_photo_from_clipboard);\n\t\t\taddUserpicBuilder(ChosenType::Set);\n\t\t} else {\n\t\t\tchooseFile(ChosenType::Set);\n\t\t}\n\t\tif (user && user->isSelf()) {\n\t\t\tconst auto key = Api::UserPrivacy::Key::ProfilePhoto;\n\t\t\tconst auto text = tr::lng_edit_privacy_profile_photo_public_set(\n\t\t\t\ttr::now);\n\t\t\tuser->session().api().userPrivacy().reload(key);\n\t\t\t_menu->addAction(std::move(text), [=] {\n\t\t\t\tusing namespace Api;\n\t\t\t\tuser->session().api().userPrivacy().value(\n\t\t\t\t\tkey\n\t\t\t\t) | rpl::take(\n\t\t\t\t\t1\n\t\t\t\t) | rpl::on_next([=](const UserPrivacy::Rule &value) {\n\t\t\t\t\tusing namespace Settings;\n\t\t\t\t\t_window->show(Box<EditPrivacyBox>(\n\t\t\t\t\t\t_window->sessionController(),\n\t\t\t\t\t\tstd::make_unique<ProfilePhotoPrivacyController>(),\n\t\t\t\t\t\tvalue));\n\t\t\t\t}, _menu->lifetime());\n\t\t\t}, &st::menuIconProfile);\n\t\t}\n\t}\n\tconst auto position = rect().contains(mapFromGlobal(QCursor::pos()))\n\t\t? QCursor::pos()\n\t\t: mapToGlobal(rect().center());\n\t_menu->popup(position);\n}\n\nauto UserpicButton::makeResetToOriginalAction()\n-> base::unique_qptr<Menu::ItemBase> {\n\tauto item = base::make_unique_q<Menu::Action>(\n\t\t_menu->menu(),\n\t\t_menu->st().menu,\n\t\tMenu::CreateAction(\n\t\t\t_menu.get(),\n\t\t\ttr::lng_profile_photo_reset(tr::now),\n\t\t\t[=] { _resetPersonalRequests.fire({}); }),\n\t\tnullptr,\n\t\tnullptr);\n\tconst auto icon = CreateChild<UserpicButton>(\n\t\titem.get(),\n\t\t_controller,\n\t\t_peer,\n\t\tUi::UserpicButton::Role::Custom,\n\t\tUi::UserpicButton::Source::NonPersonalIfHasPersonal,\n\t\tst::restoreUserpicIcon);\n\tif (_source == Source::Custom) {\n\t\ticon->showCustom(base::duplicate(_result));\n\t}\n\ticon->setAttribute(Qt::WA_TransparentForMouseEvents);\n\ticon->move(_menu->st().menu.itemIconPosition\n\t\t+ QPoint(\n\t\t\t(st::menuIconRemove.width() - icon->width()) / 2,\n\t\t\t(st::menuIconRemove.height() - icon->height()) / 2));\n\treturn item;\n}\n\nPopupMenu *UserpicButton::showChangePhotoMenu() {\n\tchoosePhotoLocally();\n\treturn _menu.get();\n}\n\nvoid UserpicButton::openPeerPhoto() {\n\tExpects(_peer != nullptr);\n\tExpects(_controller != nullptr);\n\n\tif (_uploadOverlay && _uploadOverlay->uploading()) {\n\t\t_peer->session().api().peerPhoto().cancelUpload(_peer);\n\t\treturn;\n\t}\n\tif (_changeOverlayEnabled && _cursorInChangeOverlay) {\n\t\tchoosePhotoLocally();\n\t\treturn;\n\t}\n\n\tconst auto id = _peer->userpicPhotoId();\n\tif (!id) {\n\t\treturn;\n\t}\n\tconst auto photo = _peer->owner().photo(id);\n\tif (photo->date() && _controller) {\n\t\t_controller->openPhoto(photo, _peer);\n\t}\n}\n\nvoid UserpicButton::setupPeerViewers() {\n\tconst auto user = _peer->asUser();\n\tif (user\n\t\t&& (_source == Source::NonPersonalPhoto\n\t\t\t|| _source == Source::NonPersonalIfHasPersonal)) {\n\t\tuser->session().changes().peerFlagsValue(\n\t\t\tuser,\n\t\t\tData::PeerUpdate::Flag::FullInfo\n\t\t) | rpl::map([=] {\n\t\t\treturn std::pair(\n\t\t\t\tuser->session().api().peerPhoto().nonPersonalPhoto(user),\n\t\t\t\tuser->hasPersonalPhoto());\n\t\t}) | rpl::distinct_until_changed() | rpl::skip(\n\t\t\t1\n\t\t) | rpl::on_next([=] {\n\t\t\tprocessNewPeerPhoto();\n\t\t\tupdate();\n\t\t}, _sourceLifetime);\n\t}\n\tif (!user\n\t\t|| _source == Source::PeerPhoto\n\t\t|| _source == Source::NonPersonalIfHasPersonal) {\n\t\t_peer->session().changes().peerUpdates(\n\t\t\t_peer,\n\t\t\tData::PeerUpdate::Flag::Photo\n\t\t) | rpl::on_next([=] {\n\t\t\tprocessNewPeerPhoto();\n\t\t\tupdate();\n\t\t}, _sourceLifetime);\n\t}\n\t_peer->session().downloaderTaskFinished(\n\t) | rpl::filter([=] {\n\t\treturn _waiting;\n\t}) | rpl::on_next([=] {\n\t\tconst auto loading = _showPeerUserpic\n\t\t\t? Ui::PeerUserpicLoading(_userpicView)\n\t\t\t: (_nonPersonalView && !_nonPersonalView->loaded());\n\t\tif (!loading) {\n\t\t\t_waiting = false;\n\t\t\tstartNewPhotoShowing();\n\t\t}\n\t}, _sourceLifetime);\n}\n\nvoid UserpicButton::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tif (!_waiting && _notShownYet) {\n\t\t_notShownYet = false;\n\t\tstartAnimation();\n\t}\n\n\tauto photoPosition = countPhotoPosition();\n\tauto photoLeft = photoPosition.x();\n\tauto photoTop = photoPosition.y();\n\n\tif (showSavedMessages()) {\n\t\tUi::EmptyUserpic::PaintSavedMessages(\n\t\t\tp,\n\t\t\tphotoPosition.x(),\n\t\t\tphotoPosition.y(),\n\t\t\twidth(),\n\t\t\t_st.photoSize);\n\t} else if (showRepliesMessages()) {\n\t\tUi::EmptyUserpic::PaintRepliesMessages(\n\t\t\tp,\n\t\t\tphotoPosition.x(),\n\t\t\tphotoPosition.y(),\n\t\t\twidth(),\n\t\t\t_st.photoSize);\n\t} else if (showMyNotes()) {\n\t\tUi::EmptyUserpic::PaintMyNotes(\n\t\t\tp,\n\t\t\tphotoPosition.x(),\n\t\t\tphotoPosition.y(),\n\t\t\twidth(),\n\t\t\t_st.photoSize);\n\t} else if (showAuthorHidden()) {\n\t\tUi::EmptyUserpic::PaintHiddenAuthor(\n\t\t\tp,\n\t\t\tphotoPosition.x(),\n\t\t\tphotoPosition.y(),\n\t\t\twidth(),\n\t\t\t_st.photoSize);\n\t} else {\n\t\tif (_a_appearance.animating()) {\n\t\t\tp.drawPixmapLeft(photoPosition, width(), _oldUserpic);\n\t\t\tp.setOpacity(_a_appearance.value(1.));\n\t\t}\n\t\tpaintUserpicFrame(p, photoPosition);\n\t}\n\n\tconst auto fillTranslatedShape = [&](QBrush brush) {\n\t\tp.translate(photoLeft, photoTop);\n\t\tfillShape(p, std::move(brush));\n\t\tp.translate(-photoLeft, -photoTop);\n\t};\n\n\tconst auto uploadShown = _uploadOverlay\n\t\t&& _uploadOverlay->shown();\n\tif (!uploadShown\n\t\t&& (_role == Role::ChangePhoto || _role == Role::ChoosePhoto)) {\n\t\tauto over = isOver() || isDown();\n\t\tif (over) {\n\t\t\tfillTranslatedShape(_userpicHasImage\n\t\t\t\t? st::msgDateImgBg->b\n\t\t\t\t: st::shadowFg->b);\n\t\t}\n\t\tpaintRipple(p, photoLeft, photoTop, &st::shadowFg->c);\n\t\tif (over || !_userpicHasImage) {\n\t\t\tauto iconLeft = (_st.changeIconPosition.x() < 0)\n\t\t\t\t? (_st.photoSize - _st.changeIcon.width()) / 2\n\t\t\t\t: _st.changeIconPosition.x();\n\t\t\tauto iconTop = (_st.changeIconPosition.y() < 0)\n\t\t\t\t? (_st.photoSize - _st.changeIcon.height()) / 2\n\t\t\t\t: _st.changeIconPosition.y();\n\t\t\t_st.changeIcon.paint(\n\t\t\t\tp,\n\t\t\t\tphotoLeft + iconLeft,\n\t\t\t\tphotoTop + iconTop,\n\t\t\t\twidth());\n\t\t}\n\t} else if (!uploadShown && _changeOverlayEnabled) {\n\t\tauto current = _changeOverlayShown.value(\n\t\t\t(isOver() || isDown()) ? 1. : 0.);\n\t\tauto barHeight = anim::interpolate(\n\t\t\t0,\n\t\t\t_st.uploadHeight,\n\t\t\tcurrent);\n\t\tif (barHeight > 0) {\n\t\t\tauto barLeft = photoLeft;\n\t\t\tauto barTop = photoTop + _st.photoSize - barHeight;\n\t\t\tauto rect = QRect(\n\t\t\t\tbarLeft,\n\t\t\t\tbarTop,\n\t\t\t\t_st.photoSize,\n\t\t\t\tbarHeight);\n\t\t\tp.setClipRect(rect);\n\t\t\tfillTranslatedShape(_st.uploadBg);\n\t\t\tauto iconLeft = (_st.uploadIconPosition.x() < 0)\n\t\t\t\t? (_st.photoSize - _st.uploadIcon.width()) / 2\n\t\t\t\t: _st.uploadIconPosition.x();\n\t\t\tauto iconTop = (_st.uploadIconPosition.y() < 0)\n\t\t\t\t? (_st.uploadHeight - _st.uploadIcon.height()) / 2\n\t\t\t\t: _st.uploadIconPosition.y();\n\t\t\tif (iconTop < barHeight) {\n\t\t\t\t_st.uploadIcon.paint(\n\t\t\t\t\tp,\n\t\t\t\t\tbarLeft + iconLeft,\n\t\t\t\t\tbarTop + iconTop,\n\t\t\t\t\twidth());\n\t\t\t}\n\t\t}\n\t}\n\tif (uploadShown) {\n\t\t_uploadOverlay->paint(p, QRect(photoPosition, Size(_st.photoSize)), {\n\t\t\t.lineWidth = _st.uploadProgressLine,\n\t\t\t.margin = _st.uploadProgressMargin,\n\t\t\t.progressFg = st::historyFileThumbRadialFg,\n\t\t\t.overlayFg = st::songCoverOverlayFg,\n\t\t\t.cancelIcon = &st::userpicUploadCancel,\n\t\t\t.roundRadius = useForumShape()\n\t\t\t\t? (_st.photoSize * ForumUserpicRadiusMultiplier())\n\t\t\t\t: 0.,\n\t\t});\n\t}\n}\n\nvoid UserpicButton::paintUserpicFrame(Painter &p, QPoint photoPosition) {\n\tcheckStreamedIsStarted();\n\tif (_streamed\n\t\t&& _streamed->player().ready()\n\t\t&& !_streamed->player().videoSize().isEmpty()) {\n\t\tconst auto paused = _controller\n\t\t\t? _controller->isGifPausedAtLeastFor(\n\t\t\t\tWindow::GifPauseReason::RoundPlaying)\n\t\t\t: false;\n\t\tauto request = Media::Streaming::FrameRequest();\n\t\tauto size = QSize{ _st.photoSize, _st.photoSize };\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\trequest.outer = request.resize = size * ratio;\n\t\tif (_shape == PeerUserpicShape::Monoforum) {\n\t\t} else if (useForumShape()) {\n\t\t\tconst auto radius = int(_st.photoSize\n\t\t\t\t* Ui::ForumUserpicRadiusMultiplier());\n\t\t\tif (_roundingCorners[0].width() != radius * ratio) {\n\t\t\t\t_roundingCorners = Images::CornersMask(radius);\n\t\t\t}\n\t\t\trequest.rounding = Images::CornersMaskRef(_roundingCorners);\n\t\t} else {\n\t\t\tif (_ellipseMask.size() != request.outer) {\n\t\t\t\t_ellipseMask = Images::EllipseMask(size);\n\t\t\t}\n\t\t\trequest.mask = _ellipseMask;\n\t\t}\n\t\tauto frame = _streamed->frame(request);\n\n\t\tif (_shape == PeerUserpicShape::Monoforum) {\n\t\t\tif (_monoforumMask.isNull()) {\n\t\t\t\t_monoforumMask = MonoforumShapeMask(request.resize);\n\t\t\t}\n\t\t\tconstexpr auto format = QImage::Format_ARGB32_Premultiplied;\n\t\t\tif (frame.format() != format) {\n\t\t\t\tframe = std::move(frame).convertToFormat(format);\n\t\t\t}\n\t\t\tauto q = QPainter(&frame);\n\t\t\tq.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\t\tq.drawImage(\n\t\t\t\tQRect(QPoint(), frame.size() / frame.devicePixelRatio()),\n\t\t\t\t_monoforumMask);\n\t\t\tq.end();\n\t\t}\n\t\tp.drawImage(QRect(photoPosition, size), frame);\n\t\tif (!paused) {\n\t\t\t_streamed->markFrameShown();\n\t\t}\n\t} else {\n\t\tp.drawPixmapLeft(photoPosition, width(), _userpic);\n\t}\n}\n\nQPoint UserpicButton::countPhotoPosition() const {\n\tauto photoLeft = (_st.photoPosition.x() < 0)\n\t\t? (width() - _st.photoSize) / 2\n\t\t: _st.photoPosition.x();\n\tauto photoTop = (_st.photoPosition.y() < 0)\n\t\t? (height() - _st.photoSize) / 2\n\t\t: _st.photoPosition.y();\n\treturn { photoLeft, photoTop };\n}\n\nQImage UserpicButton::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::EllipseMask(QSize(\n\t\t_st.photoSize,\n\t\t_st.photoSize));\n}\n\nQPoint UserpicButton::prepareRippleStartPosition() const {\n\treturn (_role == Role::ChangePhoto)\n\t\t? mapFromGlobal(QCursor::pos()) - countPhotoPosition()\n\t\t: DisabledRippleStartPosition();\n}\n\nvoid UserpicButton::processPeerPhoto() {\n\tExpects(_peer != nullptr);\n\n\tconst auto user = _peer->asUser();\n\tconst auto nonPersonal = (user && _source != Source::PeerPhoto)\n\t\t? _peer->session().api().peerPhoto().nonPersonalPhoto(user)\n\t\t: nullptr;\n\t_showPeerUserpic = (_source == Source::PeerPhoto)\n\t\t|| (user\n\t\t\t&& !user->hasPersonalPhoto()\n\t\t\t&& (_source == Source::NonPersonalPhoto\n\t\t\t\t|| (_source == Source::NonPersonalIfHasPersonal\n\t\t\t\t\t&& hasPersonalPhotoLocally())));\n\tconst auto showNonPersonal = _showPeerUserpic ? nullptr : nonPersonal;\n\n\t_userpicView = _showPeerUserpic\n\t\t? _peer->createUserpicView()\n\t\t: PeerUserpicView();\n\t_nonPersonalView = showNonPersonal\n\t\t? showNonPersonal->createMediaView()\n\t\t: nullptr;\n\t_waiting = _showPeerUserpic\n\t\t? Ui::PeerUserpicLoading(_userpicView)\n\t\t: (_nonPersonalView && !_nonPersonalView->loaded());\n\tif (_waiting) {\n\t\tif (_showPeerUserpic) {\n\t\t\t_peer->loadUserpic();\n\t\t} else if (_nonPersonalView) {\n\t\t\tshowNonPersonal->load(Data::FileOriginFullUser{\n\t\t\t\tpeerToUser(user->id),\n\t\t\t});\n\t\t}\n\t}\n\tif (_role == Role::OpenPhoto) {\n\t\tif (_peer->userpicPhotoUnknown()) {\n\t\t\t_peer->updateFullForced();\n\t\t}\n\t\t_canOpenPhoto = (_peer->userpicPhotoId() != 0);\n\t\tupdateCursor();\n\t\tupdateVideo();\n\t}\n}\n\nvoid UserpicButton::updateCursor() {\n\tExpects(_role == Role::OpenPhoto);\n\n\tconst auto pointer = _canOpenPhoto\n\t\t|| (_changeOverlayEnabled && _cursorInChangeOverlay);\n\tsetPointerCursor(pointer);\n}\n\nbool UserpicButton::createStreamingObjects(not_null<PhotoData*> photo) {\n\tExpects(_peer != nullptr);\n\n\tusing namespace Media::Streaming;\n\n\tconst auto origin = _peer->isUser()\n\t\t? Data::FileOriginUserPhoto(peerToUser(_peer->id), photo->id)\n\t\t: Data::FileOrigin(Data::FileOriginPeerPhoto(_peer->id));\n\t_streamed = std::make_unique<Instance>(\n\t\tphoto->owner().streaming().sharedDocument(photo, origin),\n\t\tnullptr);\n\t_streamed->lockPlayer();\n\t_streamed->player().updates(\n\t) | rpl::on_next_error([=](Update &&update) {\n\t\thandleStreamingUpdate(std::move(update));\n\t}, [=](Error &&error) {\n\t\thandleStreamingError(std::move(error));\n\t}, _streamed->lifetime());\n\tif (_streamed->ready()) {\n\t\tstreamingReady(base::duplicate(_streamed->info()));\n\t}\n\tif (!_streamed->valid()) {\n\t\tclearStreaming();\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nvoid UserpicButton::clearStreaming() {\n\t_streamed = nullptr;\n\t_streamedPhoto = nullptr;\n}\n\nvoid UserpicButton::handleStreamingUpdate(Media::Streaming::Update &&update) {\n\tusing namespace Media::Streaming;\n\n\tv::match(update.data, [&](Information &update) {\n\t\tstreamingReady(std::move(update));\n\t}, [](PreloadedVideo) {\n\t}, [&](UpdateVideo) {\n\t\tthis->update();\n\t}, [](PreloadedAudio) {\n\t}, [](UpdateAudio) {\n\t}, [](WaitingForData) {\n\t}, [](SpeedEstimate) {\n\t}, [](MutedByOther) {\n\t}, [](Finished) {\n\t});\n}\n\nvoid UserpicButton::handleStreamingError(Media::Streaming::Error &&error) {\n\tExpects(_peer != nullptr);\n\n\t_streamedPhoto->setVideoPlaybackFailed();\n\t_streamedPhoto = nullptr;\n\t_streamed = nullptr;\n}\n\nvoid UserpicButton::streamingReady(Media::Streaming::Information &&info) {\n\tupdate();\n}\n\nvoid UserpicButton::updateVideo() {\n\tExpects(_role == Role::OpenPhoto);\n\n\tconst auto id = _peer->userpicPhotoId();\n\tif (!id) {\n\t\tclearStreaming();\n\t\treturn;\n\t}\n\tconst auto photo = _peer->owner().photo(id);\n\tif (!photo->date() || !photo->videoCanBePlayed()) {\n\t\tclearStreaming();\n\t\treturn;\n\t} else if (_streamed && _streamedPhoto == photo) {\n\t\treturn;\n\t}\n\tif (!createStreamingObjects(photo)) {\n\t\tphoto->setVideoPlaybackFailed();\n\t\treturn;\n\t}\n\t_streamedPhoto = photo;\n\tcheckStreamedIsStarted();\n}\n\nvoid UserpicButton::checkStreamedIsStarted() {\n\tExpects(!_streamed || _streamedPhoto);\n\n\tif (!_streamed) {\n\t\treturn;\n\t} else if (_streamed->paused()) {\n\t\t_streamed->resume();\n\t}\n\tif (_streamed && !_streamed->active() && !_streamed->failed()) {\n\t\tconst auto position = _streamedPhoto->videoStartPosition();\n\t\tauto options = Media::Streaming::PlaybackOptions();\n\t\toptions.position = position;\n\t\toptions.mode = Media::Streaming::Mode::Video;\n\t\toptions.loop = true;\n\t\t_streamed->play(options);\n\t}\n}\n\nvoid UserpicButton::mouseMoveEvent(QMouseEvent *e) {\n\tRippleButton::mouseMoveEvent(e);\n\tif (_role == Role::OpenPhoto) {\n\t\tupdateCursorInChangeOverlay(e->pos());\n\t}\n}\n\nvoid UserpicButton::updateCursorInChangeOverlay(QPoint localPos) {\n\tauto photoPosition = countPhotoPosition();\n\tauto overlayRect = QRect(\n\t\tphotoPosition.x(),\n\t\tphotoPosition.y() + _st.photoSize - _st.uploadHeight,\n\t\t_st.photoSize,\n\t\t_st.uploadHeight);\n\tauto inOverlay = overlayRect.contains(localPos);\n\tsetCursorInChangeOverlay(inOverlay);\n}\n\nvoid UserpicButton::leaveEventHook(QEvent *e) {\n\tif (_role == Role::OpenPhoto) {\n\t\tsetCursorInChangeOverlay(false);\n\t}\n\treturn RippleButton::leaveEventHook(e);\n}\n\nvoid UserpicButton::setCursorInChangeOverlay(bool inOverlay) {\n\tExpects(_role == Role::OpenPhoto);\n\n\tif (_cursorInChangeOverlay != inOverlay) {\n\t\t_cursorInChangeOverlay = inOverlay;\n\t\tupdateCursor();\n\t}\n}\n\nvoid UserpicButton::processNewPeerPhoto() {\n\tif (_source == Source::Custom) {\n\t\treturn;\n\t}\n\tprocessPeerPhoto();\n\tif (!_waiting) {\n\t\tgrabOldUserpic();\n\t\tstartNewPhotoShowing();\n\t}\n}\n\nbool UserpicButton::useForumShape() const {\n\treturn (_shape == PeerUserpicShape::Forum)\n\t\t|| (_peer\n\t\t\t&& _peer->isForum()\n\t\t\t&& _shape == PeerUserpicShape::Auto\n\t\t\t&& !_peer->isBot());\n}\n\nvoid UserpicButton::grabOldUserpic() {\n\tauto photoRect = QRect(\n\t\tcountPhotoPosition(),\n\t\tQSize(_st.photoSize, _st.photoSize)\n\t);\n\t_oldUserpic = GrabWidget(this, photoRect);\n}\n\nvoid UserpicButton::startNewPhotoShowing() {\n\tconst auto oldUniqueKey = _userpicUniqueKey;\n\tprepareUserpicPixmap();\n\tupdate();\n\n\tif (_notShownYet) {\n\t\treturn;\n\t} else if (oldUniqueKey != _userpicUniqueKey\n\t\t|| _a_appearance.animating()) {\n\t\tstartAnimation();\n\t}\n}\n\nvoid UserpicButton::startAnimation() {\n\t_a_appearance.stop();\n\t_a_appearance.start([this] { update(); }, 0, 1, _st.duration);\n}\n\nvoid UserpicButton::switchChangePhotoOverlay(\n\t\tbool enabled,\n\t\tFn<void(ChosenImage)> chosen) {\n\tExpects(_role == Role::OpenPhoto);\n\n\tif (_changeOverlayEnabled != enabled) {\n\t\t_changeOverlayEnabled = enabled;\n\t\tif (enabled) {\n\t\t\tif (isOver()) {\n\t\t\t\tstartChangeOverlayAnimation();\n\t\t\t}\n\t\t\tupdateCursorInChangeOverlay(\n\t\t\t\tmapFromGlobal(QCursor::pos()));\n\t\t\tif (chosen) {\n\t\t\t\tchosenImages() | rpl::on_next(chosen, lifetime());\n\t\t\t}\n\t\t} else {\n\t\t\t_changeOverlayShown.stop();\n\t\t\tupdate();\n\t\t}\n\t}\n}\n\nvoid UserpicButton::overrideShape(PeerUserpicShape shape) {\n\t_shape = shape;\n\tprepare();\n}\n\nvoid UserpicButton::showSavedMessagesOnSelf(bool enabled) {\n\tif (_showSavedMessagesOnSelf != enabled) {\n\t\t_showSavedMessagesOnSelf = enabled;\n\t\tupdate();\n\t}\n}\n\nvoid UserpicButton::showMyNotesOnSelf(bool enabled) {\n\tif (_showMyNotesOnSelf != enabled) {\n\t\t_showMyNotesOnSelf = enabled;\n\t\tupdate();\n\t}\n}\n\nbool UserpicButton::showSavedMessages() const {\n\treturn _showSavedMessagesOnSelf && _peer && _peer->isSelf();\n}\n\nbool UserpicButton::showRepliesMessages() const {\n\treturn _showSavedMessagesOnSelf && _peer && _peer->isRepliesChat();\n}\n\nbool UserpicButton::showMyNotes() const {\n\treturn _showMyNotesOnSelf && _peer && _peer->isSelf();\n}\n\nbool UserpicButton::showAuthorHidden() const {\n\treturn _showMyNotesOnSelf && _peer && _peer->isSavedHiddenAuthor();\n}\n\nvoid UserpicButton::startChangeOverlayAnimation() {\n\tauto over = isOver() || isDown();\n\t_changeOverlayShown.start(\n\t\t[this] { update(); },\n\t\tover ? 0. : 1.,\n\t\tover ? 1. : 0.,\n\t\tst::slideWrapDuration);\n\tupdate();\n}\n\nvoid UserpicButton::onStateChanged(\n\t\tState was,\n\t\tStateChangeSource source) {\n\tRippleButton::onStateChanged(was, source);\n\tif (_changeOverlayEnabled) {\n\t\tauto mask = (StateFlag::Over | StateFlag::Down);\n\t\tauto wasOver = (was & mask) != 0;\n\t\tauto nowOver = (state() & mask) != 0;\n\t\tif (wasOver != nowOver) {\n\t\t\tstartChangeOverlayAnimation();\n\t\t}\n\t}\n\tif (_uploadOverlay && _uploadOverlay->uploading()) {\n\t\tconst auto over = isOver() || isDown();\n\t\tconst auto wasOver = (was & StateFlag::Over)\n\t\t\t|| (was & StateFlag::Down);\n\t\tif (over != wasOver) {\n\t\t\t_uploadOverlay->setOver(over);\n\t\t}\n\t}\n}\n\nvoid UserpicButton::showCustom(QImage &&image) {\n\tif (!_notShownYet) {\n\t\tgrabOldUserpic();\n\t}\n\n\tclearStreaming();\n\t_sourceLifetime.destroy();\n\t_source = Source::Custom;\n\n\t_userpicHasImage = !image.isNull();\n\tif (_userpicHasImage) {\n\t\tauto size = QSize(_st.photoSize, _st.photoSize);\n\t\tauto small = image.scaled(\n\t\t\tsize * style::DevicePixelRatio(),\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t\t_userpic = Ui::PixmapFromImage(useForumShape()\n\t\t\t? Images::Round(\n\t\t\t\tstd::move(small),\n\t\t\t\tImages::CornersMask(_st.photoSize\n\t\t\t\t\t* Ui::ForumUserpicRadiusMultiplier()))\n\t\t\t: Images::Circle(std::move(small)));\n\t} else {\n\t\t_userpic = CreateSquarePixmap(_st.photoSize, [&](Painter &p) {\n\t\t\tfillShape(p, CreateDefaultGradientBrush(_st.photoSize));\n\t\t});\n\t}\n\t_userpic.setDevicePixelRatio(style::DevicePixelRatio());\n\t_userpicUniqueKey = {};\n\t_result = std::move(image);\n\n\tstartNewPhotoShowing();\n}\n\nvoid UserpicButton::showSource(Source source) {\n\tExpects(_peer != nullptr);\n\tExpects(source != Source::Custom); // Show this using showCustom().\n\tExpects(source == Source::PeerPhoto || _peer->isUser());\n\n\tif (_source != source) {\n\t\tclearStreaming();\n\t}\n\n\t_sourceLifetime.destroy();\n\t_source = source;\n\n\t_result = QImage();\n\n\tprocessPeerPhoto();\n\tsetupPeerViewers();\n\n\tprepareUserpicPixmap();\n\tupdate();\n}\n\nvoid UserpicButton::overrideHasPersonalPhoto(bool has) {\n\tExpects(_peer && _peer->isUser());\n\n\t_overrideHasPersonalPhoto = has;\n}\n\nrpl::producer<> UserpicButton::resetPersonalRequests() const {\n\treturn _resetPersonalRequests.events();\n}\n\nvoid UserpicButton::showUploadProgress() {\n\tif (_uploadOverlay && _uploadOverlay->uploading()) {\n\t\treturn;\n\t}\n\tExpects(_peer != nullptr);\n\n\t_uploadOverlay = std::make_unique<UploadProgressOverlay>(\n\t\tthis,\n\t\t[=] { update(); });\n\t_uploadOverlay->start();\n\n\t_peer->session().api().peerPhoto().subscribeToUpload(\n\t\t_peer,\n\t\t_uploadLifetime,\n\t\t{\n\t\t\t.progress = [=](float64 value) {\n\t\t\t\t_uploadOverlay->setProgress(value);\n\t\t\t},\n\t\t\t.done = [=] {\n\t\t\t\t_uploadOverlay->stop([=] {\n\t\t\t\t\t_uploadLifetime.destroy();\n\t\t\t\t\t_uploadOverlay = nullptr;\n\t\t\t\t});\n\t\t\t},\n\t\t\t.failed = [=] {\n\t\t\t\t_uploadOverlay->fail([=] {\n\t\t\t\t\t_uploadLifetime.destroy();\n\t\t\t\t\t_uploadOverlay = nullptr;\n\t\t\t\t\tshowSource(Source::PeerPhoto);\n\t\t\t\t});\n\t\t\t},\n\t\t});\n}\n\nvoid UserpicButton::fillShape(QPainter &p, QBrush brush) const {\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(brush);\n\tconst auto size = _st.photoSize;\n\tif (useForumShape()) {\n\t\tconst auto radius = size * Ui::ForumUserpicRadiusMultiplier();\n\t\tp.drawRoundedRect(0, 0, size, size, radius, radius);\n\t} else {\n\t\tp.drawEllipse(0, 0, size, size);\n\t}\n}\n\nvoid UserpicButton::prepareUserpicPixmap() {\n\tif (_source == Source::Custom) {\n\t\treturn;\n\t}\n\tconst auto size = _st.photoSize;\n\t_userpicHasImage = _showPeerUserpic\n\t\t? (_peer\n\t\t\t&& (_peer->userpicCloudImage(_userpicView)\n\t\t\t\t|| _role != Role::ChangePhoto))\n\t\t: (_source == Source::NonPersonalPhoto\n\t\t\t|| (_source == Source::NonPersonalIfHasPersonal\n\t\t\t\t&& hasPersonalPhotoLocally()));\n\t_userpic = CreateSquarePixmap(size, [&](Painter &p) {\n\t\tif (_userpicHasImage) {\n\t\t\tif (_showPeerUserpic) {\n\t\t\t\t_peer->paintUserpic(p, _userpicView, {\n\t\t\t\t\t.position = QPoint(),\n\t\t\t\t\t.size = size,\n\t\t\t\t\t.shape = _shape,\n\t\t\t\t});\n\t\t\t} else if (_nonPersonalView) {\n\t\t\t\tusing Size = Data::PhotoSize;\n\t\t\t\tif (const auto full = _nonPersonalView->image(Size::Large)) {\n\t\t\t\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t\t\t\tauto image = full->original().scaled(\n\t\t\t\t\t\tQSize(size, size) * ratio,\n\t\t\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\t\t\tQt::SmoothTransformation);\n\t\t\t\t\timage = useForumShape()\n\t\t\t\t\t\t? Images::Round(\n\t\t\t\t\t\t\tstd::move(image),\n\t\t\t\t\t\t\tImages::CornersMask(size\n\t\t\t\t\t\t\t\t* Ui::ForumUserpicRadiusMultiplier()))\n\t\t\t\t\t\t: Images::Circle(std::move(image));\n\t\t\t\t\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\t\t\tp.drawImage(0, 0, image);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst auto user = _peer->asUser();\n\t\t\t\tauto empty = Ui::EmptyUserpic(\n\t\t\t\t\tUi::EmptyUserpic::UserpicColor(_peer->colorIndex()),\n\t\t\t\t\t((user && user->isInaccessible())\n\t\t\t\t\t\t? Ui::EmptyUserpic::InaccessibleName()\n\t\t\t\t\t\t: _peer->name()));\n\t\t\t\tif (useForumShape()) {\n\t\t\t\t\tempty.paintRounded(\n\t\t\t\t\t\tp,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tsize,\n\t\t\t\t\t\tsize,\n\t\t\t\t\t\tsize * Ui::ForumUserpicRadiusMultiplier());\n\t\t\t\t} else {\n\t\t\t\t\tempty.paintCircle(p, 0, 0, size, size);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfillShape(p, CreateDefaultGradientBrush(_st.photoSize));\n\t\t}\n\t});\n\t_userpicUniqueKey = _userpicHasImage\n\t\t? (_showPeerUserpic\n\t\t\t? _peer->userpicUniqueKey(_userpicView)\n\t\t\t: _nonPersonalView\n\t\t\t? InMemoryKey{ _nonPersonalView->owner()->id, 0 }\n\t\t\t: InMemoryKey{ _peer->id.value, _peer->id.value })\n\t\t: InMemoryKey();\n}\n\nnot_null<Ui::UserpicButton*> CreateUploadSubButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto background = Ui::CreateChild<Ui::RpWidget>(parent.get());\n\tconst auto upload = Ui::CreateChild<Ui::UserpicButton>(\n\t\tparent.get(),\n\t\t&controller->window(),\n\t\tUi::UserpicButton::Role::ChoosePhoto,\n\t\tst::uploadUserpicButton);\n\tSetupSubButtonBackground(upload, background);\n\treturn upload;\n}\n\nnot_null<Ui::UserpicButton*> CreateUploadSubButton(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<UserData*> contact,\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto background = Ui::CreateChild<Ui::RpWidget>(parent.get());\n\tconst auto upload = Ui::CreateChild<Ui::UserpicButton>(\n\t\tparent.get(),\n\t\tcontroller,\n\t\tcontact,\n\t\tUi::UserpicButton::Role::ChoosePhoto,\n\t\tUi::UserpicButton::Source::NonPersonalIfHasPersonal,\n\t\tst::uploadUserpicButton);\n\tSetupSubButtonBackground(upload, background);\n\treturn upload;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/userpic_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/buttons.h\"\n#include \"ui/userpic_view.h\"\n\nclass PeerData;\n\nnamespace Data {\nclass PhotoMedia;\n} // namespace Data\n\nnamespace Window {\nclass Controller;\nclass SessionController;\n} // namespace Window\n\nnamespace Media {\nnamespace Streaming {\nclass Instance;\nstruct Update;\nenum class Error;\nstruct Information;\n} // namespace Streaming\n} // namespace Media\n\nnamespace style {\nstruct UserpicButton;\n} // namespace style\n\nnamespace Ui::Menu {\nclass ItemBase;\n} // namespace Ui::Menu\n\nnamespace Ui {\n\nclass UploadProgressOverlay;\n\nclass PopupMenu;\n\nclass UserpicButton final : public RippleButton {\npublic:\n\tenum class Role {\n\t\tChoosePhoto,\n\t\tChangePhoto,\n\t\tOpenPhoto,\n\t\tCustom,\n\t};\n\tenum class Source {\n\t\tPeerPhoto,\n\t\tNonPersonalPhoto,\n\t\tNonPersonalIfHasPersonal,\n\t\tCustom,\n\t};\n\n\tUserpicButton(\n\t\tQWidget *parent,\n\t\tnot_null<::Window::Controller*> window,\n\t\tRole role,\n\t\tconst style::UserpicButton &st,\n\t\tPeerUserpicShape shape = PeerUserpicShape::Auto);\n\tUserpicButton(\n\t\tQWidget *parent,\n\t\tnot_null<::Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tRole role,\n\t\tSource source,\n\t\tconst style::UserpicButton &st,\n\t\tPeerUserpicShape shape = PeerUserpicShape::Auto);\n\tUserpicButton(\n\t\tQWidget *parent,\n\t\tnot_null<PeerData*> peer, // Role::Custom, Source::PeerPhoto\n\t\tconst style::UserpicButton &st,\n\t\tPeerUserpicShape shape = PeerUserpicShape::Auto);\n\t~UserpicButton();\n\n\tenum class ChosenType {\n\t\tSet,\n\t\tSuggest,\n\t};\n\tstruct ChosenImage {\n\t\tQImage image;\n\t\tChosenType type = ChosenType::Set;\n\t\tstruct {\n\t\t\tDocumentId documentId = 0;\n\t\t\tstd::vector<QColor> colors;\n\t\t} markup;\n\t};\n\n\t// Role::OpenPhoto\n\tvoid switchChangePhotoOverlay(\n\t\tbool enabled,\n\t\tFn<void(ChosenImage)> chosen);\n\tvoid showSavedMessagesOnSelf(bool enabled);\n\tvoid showMyNotesOnSelf(bool enabled);\n\tvoid overrideShape(PeerUserpicShape shape);\n\n\t// Role::ChoosePhoto or Role::ChangePhoto\n\t[[nodiscard]] rpl::producer<ChosenImage> chosenImages() const {\n\t\treturn _chosenImages.events();\n\t}\n\t[[nodiscard]] QImage takeResultImage() {\n\t\treturn std::move(_result);\n\t}\n\n\tvoid showCustom(QImage &&image);\n\tvoid showSource(Source source);\n\tvoid showCustomOnChosen();\n\n\tvoid showUploadProgress();\n\n\t[[nodiscard]] PopupMenu *showChangePhotoMenu();\n\n\tvoid overrideHasPersonalPhoto(bool has);\n\t[[nodiscard]] rpl::producer<> resetPersonalRequests() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\n\tvoid onStateChanged(State was, StateChangeSource source) override;\n\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\nprivate:\n\tvoid prepare();\n\tvoid setupPeerViewers();\n\tvoid startAnimation();\n\tvoid processPeerPhoto();\n\tvoid processNewPeerPhoto();\n\tvoid startNewPhotoShowing();\n\tvoid prepareUserpicPixmap();\n\tvoid fillShape(QPainter &p, QBrush brush) const;\n\t[[nodiscard]] QPoint countPhotoPosition() const;\n\tvoid startChangeOverlayAnimation();\n\tvoid updateCursorInChangeOverlay(QPoint localPos);\n\tvoid setCursorInChangeOverlay(bool inOverlay);\n\tvoid updateCursor();\n\tvoid updateVideo();\n\t[[nodiscard]] bool showSavedMessages() const;\n\t[[nodiscard]] bool showRepliesMessages() const;\n\t[[nodiscard]] bool showMyNotes() const;\n\t[[nodiscard]] bool showAuthorHidden() const;\n\tvoid checkStreamedIsStarted();\n\tbool createStreamingObjects(not_null<PhotoData*> photo);\n\tvoid clearStreaming();\n\tvoid handleStreamingUpdate(Media::Streaming::Update &&update);\n\tvoid handleStreamingError(Media::Streaming::Error &&error);\n\tvoid streamingReady(Media::Streaming::Information &&info);\n\tvoid paintUserpicFrame(Painter &p, QPoint photoPosition);\n\n\t[[nodiscard]] bool useForumShape() const;\n\tvoid grabOldUserpic();\n\tvoid setClickHandlerByRole();\n\tvoid requestSuggestAvailability();\n\tvoid openPeerPhoto();\n\tvoid choosePhotoLocally();\n\t[[nodiscard]] bool canSuggestPhoto(not_null<UserData*> user) const;\n\t[[nodiscard]] bool hasPersonalPhotoLocally() const;\n\t[[nodiscard]] auto makeResetToOriginalAction()\n\t\t-> base::unique_qptr<Menu::ItemBase>;\n\n\tconst style::UserpicButton &_st;\n\t::Window::SessionController *_controller = nullptr;\n\t::Window::Controller *_window = nullptr;\n\tPeerData *_peer = nullptr;\n\tPeerUserpicShape _shape = PeerUserpicShape::Auto;\n\tPeerUserpicView _userpicView;\n\tQImage _monoforumMask;\n\tstd::shared_ptr<Data::PhotoMedia> _nonPersonalView;\n\tRole _role = Role::ChangePhoto;\n\tbool _notShownYet = true;\n\tbool _waiting = false;\n\tQPixmap _userpic, _oldUserpic;\n\tbool _userpicHasImage = false;\n\tbool _showPeerUserpic = false;\n\tInMemoryKey _userpicUniqueKey;\n\tAnimations::Simple _a_appearance;\n\tQImage _result;\n\tQImage _ellipseMask;\n\tstd::array<QImage, 4> _roundingCorners;\n\tstd::unique_ptr<Media::Streaming::Instance> _streamed;\n\tPhotoData *_streamedPhoto = nullptr;\n\n\tbase::unique_qptr<PopupMenu> _menu;\n\n\tbool _showSavedMessagesOnSelf = false;\n\tbool _showMyNotesOnSelf = false;\n\tbool _canOpenPhoto = false;\n\tbool _cursorInChangeOverlay = false;\n\tbool _changeOverlayEnabled = false;\n\tAnimations::Simple _changeOverlayShown;\n\n\trpl::event_stream<ChosenImage> _chosenImages;\n\n\tSource _source = Source::Custom;\n\tstd::optional<bool> _overrideHasPersonalPhoto;\n\trpl::event_stream<> _resetPersonalRequests;\n\trpl::lifetime _sourceLifetime;\n\n\tstd::unique_ptr<UploadProgressOverlay> _uploadOverlay;\n\trpl::lifetime _uploadLifetime;\n\n};\n\n[[nodiscard]] not_null<Ui::UserpicButton*> CreateUploadSubButton(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Window::SessionController*> controller);\n\n[[nodiscard]] not_null<Ui::UserpicButton*> CreateUploadSubButton(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<UserData*> contact,\n\tnot_null<Window::SessionController*> controller);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/who_reacted_context_action.h\"\n\n#include \"base/call_delayed.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/chat/group_call_userpics.h\"\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtCore/QLocale>\n\nnamespace Lang {\nnamespace {\n\nstruct StringWithReacted {\n\tQString text;\n\tint seen = 0;\n};\n\n} // namespace\n\ntemplate <typename ResultString>\nstruct StartReplacements;\n\ntemplate <>\nstruct StartReplacements<StringWithReacted> {\n\tstatic inline StringWithReacted Call(QString &&langString) {\n\t\treturn { std::move(langString) };\n\t}\n};\n\ntemplate <typename ResultString>\nstruct ReplaceTag;\n\ntemplate <>\nstruct ReplaceTag<StringWithReacted> {\n\tstatic StringWithReacted Call(\n\t\tStringWithReacted &&original,\n\t\tushort tag,\n\t\tconst StringWithReacted &replacement);\n};\n\nStringWithReacted ReplaceTag<StringWithReacted>::Call(\n\t\tStringWithReacted &&original,\n\t\tushort tag,\n\t\tconst StringWithReacted &replacement) {\n\tconst auto offset = FindTagReplacementPosition(original.text, tag);\n\tif (offset < 0) {\n\t\treturn std::move(original);\n\t}\n\toriginal.text = ReplaceTag<QString>::Call(\n\t\tstd::move(original.text),\n\t\ttag,\n\t\treplacement.text + '/' + QString::number(original.seen));\n\treturn std::move(original);\n}\n\n} // namespace Lang\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kPreloaderAlpha = 0.2;\n\nusing Text::CustomEmojiFactory;\n\nclass Action final : public Menu::ItemBase {\npublic:\n\tAction(\n\t\tnot_null<PopupMenu*> parentMenu,\n\t\trpl::producer<WhoReadContent> content,\n\t\tCustomEmojiFactory factory,\n\t\tFn<void(WhoReadParticipant)> participantChosen,\n\t\tFn<void()> showAllChosen);\n\n\tbool isEnabled() const override;\n\tnot_null<QAction*> action() const override;\n\n\tvoid handleKeyPress(not_null<QKeyEvent*> e) override;\n\nprotected:\n\tQPoint prepareRippleStartPosition() const override;\n\tQImage prepareRippleMask() const override;\n\n\tint contentHeight() const override;\n\nprivate:\n\tvoid paint(Painter &p);\n\n\tvoid updateUserpicsFromContent();\n\tvoid resolveMinWidth();\n\tvoid refreshText();\n\tvoid refreshDimensions();\n\tvoid populateSubmenu();\n\n\tconst not_null<PopupMenu*> _parentMenu;\n\tconst not_null<QAction*> _dummyAction;\n\tconst Fn<void(WhoReadParticipant)> _participantChosen;\n\tconst Fn<void()> _showAllChosen;\n\tconst std::unique_ptr<GroupCallUserpics> _userpics;\n\tconst style::Menu &_st;\n\tconst CustomEmojiFactory _customEmojiFactory;\n\n\tWhoReactedListMenu _submenu;\n\n\tText::String _text;\n\tstd::unique_ptr<Ui::Text::CustomEmoji> _custom;\n\tint _textWidth = 0;\n\tconst int _height = 0;\n\tint _userpicsWidth = 0;\n\tbool _appeared = false;\n\n\tWhoReadContent _content;\n\n};\n\nclass WhenAction final : public Menu::ItemBase {\npublic:\n\tWhenAction(\n\t\tnot_null<PopupMenu*> parentMenu,\n\t\trpl::producer<WhoReadContent> content,\n\t\tFn<void()> showOrPremium);\n\n\tbool isEnabled() const override;\n\tnot_null<QAction*> action() const override;\n\nprotected:\n\tQPoint prepareRippleStartPosition() const override;\n\tQImage prepareRippleMask() const override;\n\n\tint contentHeight() const override;\n\nprivate:\n\tvoid paint(Painter &p);\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\tvoid resolveMinWidth();\n\tvoid refreshText();\n\tvoid refreshDimensions();\n\n\tconst not_null<PopupMenu*> _parentMenu;\n\tconst not_null<QAction*> _dummyAction;\n\tconst Fn<void()> _showOrPremium;\n\tconst style::Menu &_st;\n\n\tText::String _text;\n\tText::String _show;\n\tQRect _showRect;\n\tint _textWidth = 0;\n\tconst int _height = 0;\n\n\tWhoReadContent _content;\n\n};\n\nTextParseOptions MenuTextOptions = {\n\tTextParseLinks, // flags\n\t0, // maxw\n\t0, // maxh\n\tQt::LayoutDirectionAuto, // dir\n};\n\n[[nodiscard]] QString FormatReactedString(int reacted, int seen) {\n\tconst auto projection = [&](const QString &text) {\n\t\treturn Lang::StringWithReacted{ text, seen };\n\t};\n\treturn tr::lng_context_seen_reacted(\n\t\ttr::now,\n\t\tlt_count_short,\n\t\treacted,\n\t\tprojection\n\t).text;\n}\n\n[[nodiscard]] QString FormatReactionsCountString(int count) {\n\treturn tr::lng_context_seen_reactions_count(\n\t\ttr::now,\n\t\tlt_count_short,\n\t\tcount);\n}\n\nAction::Action(\n\tnot_null<PopupMenu*> parentMenu,\n\trpl::producer<WhoReadContent> content,\n\tText::CustomEmojiFactory factory,\n\tFn<void(WhoReadParticipant)> participantChosen,\n\tFn<void()> showAllChosen)\n: ItemBase(parentMenu->menu(), parentMenu->menu()->st())\n, _parentMenu(parentMenu)\n, _dummyAction(CreateChild<QAction>(parentMenu->menu().get()))\n, _participantChosen(std::move(participantChosen))\n, _showAllChosen(std::move(showAllChosen))\n, _userpics(std::make_unique<GroupCallUserpics>(\n\tst::defaultWhoRead.userpics,\n\trpl::never<bool>(),\n\t[=] { update(); }))\n, _st(parentMenu->menu()->st())\n, _customEmojiFactory(std::move(factory))\n, _submenu(_customEmojiFactory, _participantChosen, _showAllChosen)\n, _height(st::defaultWhoRead.itemPadding.top()\n\t\t+ _st.itemStyle.font->height\n\t\t+ st::defaultWhoRead.itemPadding.bottom()) {\n\tconst auto delay = anim::Disabled() ? 0 : parentMenu->st().duration;\n\tconst auto checkAppeared = [=, now = crl::now()](bool force = false) {\n\t\t_appeared = force || ((crl::now() - now) >= delay);\n\t};\n\n\tsetAcceptBoth(true);\n\tfitToMenuWidth();\n\n\tstd::move(\n\t\tcontent\n\t) | rpl::on_next([=](WhoReadContent &&content) {\n\t\tcheckAppeared();\n\t\tconst auto changed = (_content.participants != content.participants)\n\t\t\t|| (_content.state != content.state);\n\t\t_content = content;\n\t\tif (changed) {\n\t\t\tPostponeCall(this, [=] { populateSubmenu(); });\n\t\t}\n\t\tupdateUserpicsFromContent();\n\t\trefreshText();\n\t\trefreshDimensions();\n\t\tsetPointerCursor(isEnabled());\n\t\t_dummyAction->setEnabled(isEnabled());\n\t\tif (!isEnabled()) {\n\t\t\tsetSelected(false);\n\t\t}\n\t\tupdate();\n\t}, lifetime());\n\n\tresolveMinWidth();\n\n\t_userpics->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\t_userpicsWidth = width;\n\t\trefreshDimensions();\n\t\tupdate();\n\t}, lifetime());\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(this);\n\t\tpaint(p);\n\t}, lifetime());\n\n\tclicks(\n\t) | rpl::on_next([=] {\n\t\tif (_content.participants.size() == 1) {\n\t\t\tif (const auto onstack = _participantChosen) {\n\t\t\t\tonstack(_content.participants.front());\n\t\t\t}\n\t\t} else if (_content.fullReactionsCount > 0) {\n\t\t\tif (const auto onstack = _showAllChosen) {\n\t\t\t\tonstack();\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\tenableMouseSelecting();\n\n\tbase::call_delayed(parentMenu->st().duration, this, [=] {\n\t\tif (!_appeared) {\n\t\t\tcheckAppeared(true);\n\t\t\tupdateUserpicsFromContent();\n\t\t}\n\t});\n}\n\nvoid Action::resolveMinWidth() {\n\tconst auto maxIconWidth = 0;\n\tconst auto width = [&](const QString &text) {\n\t\treturn _st.itemStyle.font->width(text);\n\t};\n\tconst auto maxText = (_content.type == WhoReadType::Listened)\n\t\t? tr::lng_context_seen_listened(tr::now, lt_count, 999)\n\t\t: (_content.type == WhoReadType::Watched)\n\t\t? tr::lng_context_seen_watched(tr::now, lt_count, 999)\n\t\t: (_content.type == WhoReadType::Seen)\n\t\t? tr::lng_context_seen_text(tr::now, lt_count, 999)\n\t\t: QString();\n\tconst auto maxReacted = (_content.fullReactionsCount > 0)\n\t\t? (!maxText.isEmpty()\n\t\t\t? FormatReactedString(_content.fullReactionsCount, 999)\n\t\t\t: tr::lng_context_seen_reacted(\n\t\t\t\ttr::now,\n\t\t\t\tlt_count_short,\n\t\t\t\t_content.fullReactionsCount))\n\t\t: QString();\n\tconst auto maxReactionsCount = (_content.fullReactionsCount\n\t\t\t> _content.fullReadCount)\n\t\t? FormatReactionsCountString(_content.fullReactionsCount)\n\t\t: QString();\n\tconst auto maxTextWidth = std::max({\n\t\twidth(maxText),\n\t\twidth(maxReacted),\n\t\twidth(maxReactionsCount),\n\t});\n\tconst auto maxWidth = st::defaultWhoRead.itemPadding.left()\n\t\t+ maxIconWidth\n\t\t+ maxTextWidth\n\t\t+ _userpics->maxWidth()\n\t\t+ st::defaultWhoRead.itemPadding.right();\n\tsetMinWidth(maxWidth);\n}\n\nvoid Action::updateUserpicsFromContent() {\n\tif (!_appeared) {\n\t\treturn;\n\t}\n\tauto users = std::vector<GroupCallUser>();\n\tif (!_content.participants.empty()) {\n\t\tconst auto count = std::min(\n\t\t\tint(_content.participants.size()),\n\t\t\tWhoReadParticipant::kMaxSmallUserpics);\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tusers.reserve(count);\n\t\tfor (auto i = 0; i != count; ++i) {\n\t\t\tauto &participant = _content.participants[i];\n\t\t\tparticipant.userpicSmall.setDevicePixelRatio(factor);\n\t\t\tusers.push_back({\n\t\t\t\t.userpic = participant.userpicSmall,\n\t\t\t\t.userpicKey = participant.userpicKey,\n\t\t\t\t.id = participant.id,\n\t\t\t});\n\t\t}\n\t}\n\t_userpics->update(users, true);\n}\n\nvoid Action::populateSubmenu() {\n\tif (_content.participants.size() < 1) {\n\t\t_submenu.clear();\n\t\t_parentMenu->removeSubmenu(action());\n\t\tif (!isEnabled()) {\n\t\t\tsetSelected(false);\n\t\t}\n\t\treturn;\n\t}\n\n\tconst auto submenu = _parentMenu->ensureSubmenu(\n\t\taction(),\n\t\tst::whoReadMenu);\n\t_submenu.populate(submenu, _content);\n\t_parentMenu->checkSubmenuShow();\n}\n\nvoid Action::paint(Painter &p) {\n\tconst auto enabled = isEnabled();\n\tconst auto selected = isSelected();\n\tif (selected && _st.itemBgOver->c.alpha() < 255) {\n\t\tp.fillRect(0, 0, width(), _height, _st.itemBg);\n\t}\n\tconst auto &bg = selected ? _st.itemBgOver : _st.itemBg;\n\tp.fillRect(0, 0, width(), _height, bg);\n\tif (enabled) {\n\t\tpaintRipple(p, 0, 0);\n\t}\n\tif (!_custom && !_content.singleCustomEntityData.isEmpty()) {\n\t\t_custom = _customEmojiFactory(\n\t\t\t_content.singleCustomEntityData,\n\t\t\t{ .repaint = [=] { update(); } });\n\t}\n\tif (_custom) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto size = Emoji::GetSizeNormal() / ratio;\n\t\tconst auto adjusted = Text::AdjustCustomEmojiSize(size);\n\t\tconst auto x = st::defaultWhoRead.iconPosition.x()\n\t\t\t+ (st::whoReadChecks.width() - adjusted) / 2;\n\t\tconst auto y = (_height - adjusted) / 2;\n\t\t_custom->paint(p, {\n\t\t\t.textColor = (selected ? _st.itemFgOver : _st.itemFg)->c,\n\t\t\t.now = crl::now(),\n\t\t\t.position = { x, y },\n\t\t});\n\t} else {\n\t\tconst auto &icon = (_content.fullReactionsCount)\n\t\t\t? (!enabled\n\t\t\t\t? st::whoReadReactionsDisabled\n\t\t\t\t: selected\n\t\t\t\t? st::whoReadReactionsOver\n\t\t\t\t: st::whoReadReactions)\n\t\t\t: (_content.type == WhoReadType::Seen)\n\t\t\t? (!enabled\n\t\t\t\t? st::whoReadChecksDisabled\n\t\t\t\t: selected\n\t\t\t\t? st::whoReadChecksOver\n\t\t\t\t: st::whoReadChecks)\n\t\t\t: (!enabled\n\t\t\t\t? st::whoReadPlayedDisabled\n\t\t\t\t: selected\n\t\t\t\t? st::whoReadPlayedOver\n\t\t\t\t: st::whoReadPlayed);\n\t\ticon.paint(p, st::defaultWhoRead.iconPosition, width());\n\t}\n\tp.setPen(!enabled\n\t\t? _st.itemFgDisabled\n\t\t: selected\n\t\t? _st.itemFgOver\n\t\t: _st.itemFg);\n\t_text.drawLeftElided(\n\t\tp,\n\t\tst::defaultWhoRead.itemPadding.left(),\n\t\tst::defaultWhoRead.itemPadding.top(),\n\t\t_textWidth,\n\t\twidth());\n\tif (_appeared) {\n\t\t_userpics->paint(\n\t\t\tp,\n\t\t\twidth() - st::defaultWhoRead.itemPadding.right(),\n\t\t\t(height() - st::defaultWhoRead.userpics.size) / 2,\n\t\t\tst::defaultWhoRead.userpics.size);\n\t}\n}\n\nvoid Action::refreshText() {\n\tconst auto usersCount = int(_content.participants.size());\n\tconst auto onlySeenCount = ranges::count(\n\t\t_content.participants,\n\t\tQString(),\n\t\t&WhoReadParticipant::customEntityData);\n\tconst auto count = std::max(_content.fullReactionsCount, usersCount);\n\t_text.setMarkedText(\n\t\t_st.itemStyle,\n\t\t{ ((_content.state == WhoReadState::Unknown)\n\t\t\t? tr::lng_context_seen_loading(tr::now)\n\t\t\t: (usersCount == 1)\n\t\t\t? _content.participants.front().name\n\t\t\t: (_content.fullReactionsCount > 0\n\t\t\t\t&& _content.fullReactionsCount <= _content.fullReadCount)\n\t\t\t? FormatReactedString(\n\t\t\t\t_content.fullReactionsCount,\n\t\t\t\t_content.fullReadCount)\n\t\t\t: (_content.type == WhoReadType::Reacted\n\t\t\t\t|| (count > 0 && _content.fullReactionsCount > usersCount)\n\t\t\t\t|| (count > 0 && onlySeenCount == 0))\n\t\t\t? (count\n\t\t\t\t? ((_content.fullReactionsCount > _content.fullReadCount)\n\t\t\t\t\t? FormatReactionsCountString(_content.fullReactionsCount)\n\t\t\t\t\t: tr::lng_context_seen_reacted(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count_short,\n\t\t\t\t\t\tcount))\n\t\t\t\t: tr::lng_context_seen_reacted_none(tr::now))\n\t\t\t: (_content.type == WhoReadType::Watched)\n\t\t\t? (count\n\t\t\t\t? tr::lng_context_seen_watched(tr::now, lt_count, count)\n\t\t\t\t: tr::lng_context_seen_watched_none(tr::now))\n\t\t\t: (_content.type == WhoReadType::Listened)\n\t\t\t? (count\n\t\t\t\t? tr::lng_context_seen_listened(tr::now, lt_count, count)\n\t\t\t\t: tr::lng_context_seen_listened_none(tr::now))\n\t\t\t: (count\n\t\t\t\t? tr::lng_context_seen_text(tr::now, lt_count, count)\n\t\t\t\t: tr::lng_context_seen_text_none(tr::now))) },\n\t\tMenuTextOptions);\n}\n\nvoid Action::refreshDimensions() {\n\tif (!minWidth()) {\n\t\treturn;\n\t}\n\tconst auto textWidth = _text.maxWidth();\n\tconst auto &padding = st::defaultWhoRead.itemPadding;\n\n\tconst auto goodWidth = padding.left()\n\t\t+ textWidth\n\t\t+ (_userpicsWidth ? (_st.itemStyle.font->spacew + _userpicsWidth) : 0)\n\t\t+ padding.right();\n\n\tconst auto w = std::clamp(\n\t\tgoodWidth,\n\t\t_st.widthMin,\n\t\tstd::max(minWidth(), _st.widthMin));\n\t_textWidth = w - (goodWidth - textWidth);\n}\n\nbool Action::isEnabled() const {\n\treturn !_content.participants.empty()\n\t\t|| (_content.state == WhoReadState::MyHidden);\n}\n\nnot_null<QAction*> Action::action() const {\n\treturn _dummyAction;\n}\n\nQPoint Action::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos());\n}\n\nQImage Action::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::RectMask(size());\n}\n\nint Action::contentHeight() const {\n\treturn _height;\n}\n\nvoid Action::handleKeyPress(not_null<QKeyEvent*> e) {\n\tif (!isSelected()) {\n\t\treturn;\n\t}\n\tconst auto key = e->key();\n\tif (key == Qt::Key_Enter || key == Qt::Key_Return) {\n\t\tsetClicked(Menu::TriggeredSource::Keyboard);\n\t}\n}\n\nWhenAction::WhenAction(\n\tnot_null<PopupMenu*> parentMenu,\n\trpl::producer<WhoReadContent> content,\n\tFn<void()> showOrPremium)\n: ItemBase(parentMenu->menu(), parentMenu->menu()->st())\n, _parentMenu(parentMenu)\n, _dummyAction(CreateChild<QAction>(parentMenu->menu().get()))\n, _showOrPremium(std::move(showOrPremium))\n, _st(parentMenu->menu()->st())\n, _height(st::whenReadPadding.top()\n\t\t+ st::whenReadStyle.font->height\n\t\t+ st::whenReadPadding.bottom()) {\n\tsetAcceptBoth(true);\n\tfitToMenuWidth();\n\n\tstd::move(\n\t\tcontent\n\t) | rpl::on_next([=](WhoReadContent &&content) {\n\t\t_content = content;\n\t\trefreshText();\n\t\trefreshDimensions();\n\t\tsetPointerCursor(isEnabled());\n\t\t_dummyAction->setEnabled(isEnabled());\n\t\tif (!isEnabled()) {\n\t\t\tsetSelected(false);\n\t\t}\n\t\tupdate();\n\t}, lifetime());\n\n\tresolveMinWidth();\n\trefreshDimensions();\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tPainter p(this);\n\t\tpaint(p);\n\t}, lifetime());\n\n\tclicks(\n\t) | rpl::on_next([=] {\n\t\tif (_content.state == WhoReadState::MyHidden) {\n\t\t\tif (const auto onstack = _showOrPremium) {\n\t\t\t\tonstack();\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\tenableMouseSelecting();\n}\n\nvoid WhenAction::resolveMinWidth() {\n\tconst auto width = [&](const QString &text) {\n\t\treturn st::whenReadStyle.font->width(text);\n\t};\n\tconst auto added = st::whenReadShowPadding.left()\n\t\t+ st::whenReadShowPadding.right();\n\n\tconst auto sampleDate = QDate::currentDate();\n\tconst auto sampleTime = QLocale().toString(\n\t\tQTime::currentTime(),\n\t\tQLocale::ShortFormat);\n\tconst auto maxTextWidth = added + std::max({\n\t\twidth(tr::lng_contacts_loading(tr::now)),\n\t\t(width(tr::lng_context_read_hidden(tr::now))\n\t\t\t+ st::whenReadSkip\n\t\t\t+ width(tr::lng_context_read_show(tr::now))),\n\t\twidth(tr::lng_mediaview_today(tr::now, lt_time, sampleTime)),\n\t\twidth(tr::lng_mediaview_yesterday(tr::now, lt_time, sampleTime)),\n\t\twidth(tr::lng_mediaview_date_time(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\ttr::lng_month_day(\n\t\t\t\ttr::now,\n\t\t\t\tlt_month,\n\t\t\t\tLang::MonthDay(sampleDate.month())(tr::now),\n\t\t\t\tlt_day,\n\t\t\t\tQString::number(sampleDate.day())),\n\t\t\tlt_time,\n\t\t\tsampleTime)),\n\t});\n\n\tconst auto maxWidth = st::whenReadPadding.left()\n\t\t+ maxTextWidth\n\t\t+ st::whenReadPadding.right();\n\tsetMinWidth(maxWidth);\n}\n\nvoid WhenAction::paint(Painter &p) {\n\tconst auto loading = !isEnabled() && _content.participants.empty();\n\tconst auto selected = isSelected();\n\tif (selected && _st.itemBgOver->c.alpha() < 255) {\n\t\tp.fillRect(0, 0, width(), _height, _st.itemBg);\n\t}\n\tp.fillRect(0, 0, width(), _height, _st.itemBg);\n\tconst auto &icon = (_content.type == WhoReadType::Edited)\n\t\t? (selected ? st::whenEditedOver : st::whenEdited)\n\t\t: (_content.type == WhoReadType::Original)\n\t\t? (selected ? st::whenOriginalOver : st::whenOriginal)\n\t\t: loading\n\t\t? st::whoReadChecksDisabled\n\t\t: selected\n\t\t? st::whoReadChecksOver\n\t\t: st::whoReadChecks;\n\ticon.paint(p, st::whenReadIconPosition, width());\n\tp.setPen(loading ? _st.itemFgDisabled : _st.itemFg);\n\t_text.drawLeftElided(\n\t\tp,\n\t\tst::whenReadPadding.left(),\n\t\tst::whenReadPadding.top(),\n\t\t_textWidth,\n\t\twidth());\n\tif (!_show.isEmpty()) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(_st.itemBgOver);\n\t\tconst auto radius = _showRect.height() / 2.;\n\t\tp.drawRoundedRect(_showRect, radius, radius);\n\t\tpaintRipple(p, 0, 0);\n\t\tconst auto inner = _showRect.marginsRemoved(st::whenReadShowPadding);\n\t\tp.setPen(_st.itemFgOver);\n\t\t_show.drawLeftElided(\n\t\t\tp,\n\t\t\tinner.x(),\n\t\t\tinner.y(),\n\t\t\tinner.width(),\n\t\t\twidth());\n\t}\n}\n\nvoid WhenAction::refreshText() {\n\t_text.setMarkedText(\n\t\tst::whenReadStyle,\n\t\t{ ((_content.state == WhoReadState::Unknown)\n\t\t\t? tr::lng_context_seen_loading(tr::now)\n\t\t\t: _content.participants.empty()\n\t\t\t? tr::lng_context_read_hidden(tr::now)\n\t\t\t: _content.participants.front().date) },\n\t\tMenuTextOptions);\n\tif (_content.state == WhoReadState::MyHidden) {\n\t\t_show.setMarkedText(\n\t\t\tst::whenReadStyle,\n\t\t\t{ tr::lng_context_read_show(tr::now) },\n\t\t\tMenuTextOptions);\n\t} else {\n\t\t_show = Text::String();\n\t}\n}\n\nvoid WhenAction::resizeEvent(QResizeEvent *e) {\n\tItemBase::resizeEvent(e);\n\trefreshDimensions();\n}\n\nvoid WhenAction::refreshDimensions() {\n\tif (!minWidth()) {\n\t\treturn;\n\t}\n\tconst auto textWidth = _text.maxWidth();\n\tconst auto showWidth = _show.isEmpty() ? 0 : _show.maxWidth();\n\tconst auto &padding = st::whenReadPadding;\n\n\tconst auto goodWidth = padding.left()\n\t\t+ textWidth\n\t\t+ (showWidth\n\t\t\t? (st::whenReadSkip\n\t\t\t\t+ st::whenReadShowPadding.left()\n\t\t\t\t+ showWidth\n\t\t\t\t+ st::whenReadShowPadding.right())\n\t\t\t: 0)\n\t\t+ padding.right();\n\n\tconst auto w = std::clamp(\n\t\tgoodWidth,\n\t\t_st.widthMin,\n\t\tstd::max(width(), _st.widthMin));\n\t_textWidth = std::min(w - (goodWidth - textWidth), textWidth);\n\tif (showWidth) {\n\t\t_showRect = QRect(\n\t\t\tpadding.left() + _textWidth + st::whenReadSkip,\n\t\t\tpadding.top() - st::whenReadShowPadding.top(),\n\t\t\t(st::whenReadShowPadding.left()\n\t\t\t\t+ showWidth\n\t\t\t\t+ st::whenReadShowPadding.right()),\n\t\t\t(st::whenReadShowPadding.top()\n\t\t\t\t+ st::whenReadStyle.font->height\n\t\t\t\t+ st::whenReadShowPadding.bottom()));\n\t}\n}\n\nbool WhenAction::isEnabled() const {\n\treturn (_content.state == WhoReadState::MyHidden);\n}\n\nnot_null<QAction*> WhenAction::action() const {\n\treturn _dummyAction;\n}\n\nQPoint WhenAction::prepareRippleStartPosition() const {\n\tconst auto result = mapFromGlobal(QCursor::pos());\n\treturn _showRect.contains(result)\n\t\t? result\n\t\t: Ui::RippleButton::DisabledRippleStartPosition();\n}\n\nQImage WhenAction::prepareRippleMask() const {\n\treturn Ui::RippleAnimation::MaskByDrawer(size(), false, [&](QPainter &p) {\n\t\tconst auto radius = _showRect.height() / 2.;\n\t\tp.drawRoundedRect(_showRect, radius, radius);\n\t});\n}\n\nint WhenAction::contentHeight() const {\n\treturn _height;\n}\n\n} // namespace\n\nWhoReactedEntryAction::WhoReactedEntryAction(\n\tnot_null<Ui::Menu::Menu*> parent,\n\tCustomEmojiFactory customEmojiFactory,\n\tconst style::Menu &st,\n\tData &&data)\n: ItemBase(parent, st)\n, _dummyAction(CreateChild<QAction>(parent.get()))\n, _customEmojiFactory(std::move(customEmojiFactory))\n, _st(st)\n, _height(st::defaultWhoRead.photoSkip * 2 + st::defaultWhoRead.photoSize) {\n\tsetAcceptBoth(true);\n\n\tfitToMenuWidth();\n\tsetData(std::move(data));\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tpaint(Painter(this));\n\t}, lifetime());\n\n\tenableMouseSelecting();\n}\n\nnot_null<QAction*> WhoReactedEntryAction::action() const {\n\treturn _dummyAction.get();\n}\n\nbool WhoReactedEntryAction::isEnabled() const {\n\treturn true;\n}\n\nint WhoReactedEntryAction::contentHeight() const {\n\treturn _height;\n}\n\nvoid WhoReactedEntryAction::setData(Data &&data) {\n\tsetActionTriggered(std::move(data.callback));\n\t_userpic = std::move(data.userpic);\n\t_text.setMarkedText(_st.itemStyle, { data.text }, MenuTextOptions);\n\tif (data.date.isEmpty()) {\n\t\t_date = Text::String();\n\t} else {\n\t\t_date.setMarkedText(\n\t\t\tst::whoReadDateStyle,\n\t\t\t{ data.date },\n\t\t\tMenuTextOptions);\n\t}\n\t_type = data.type;\n\t_custom = _customEmojiFactory\n\t\t? _customEmojiFactory(\n\t\t\tdata.customEntityData,\n\t\t\t{ .repaint = [=] { update(); } })\n\t\t: nullptr;\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto size = Emoji::GetSizeNormal() / ratio;\n\t_customSize = Text::AdjustCustomEmojiSize(size);\n\tconst auto textWidth = std::max(\n\t\t_text.maxWidth(),\n\t\tst::whoReadDateSkip + _date.maxWidth());\n\tconst auto &padding = _st.itemPadding;\n\tconst auto rightSkip = padding.right()\n\t\t+ (_custom ? (size + padding.right()) : 0);\n\tconst auto goodWidth = st::defaultWhoRead.nameLeft\n\t\t+ textWidth\n\t\t+ rightSkip;\n\tconst auto w = std::clamp(goodWidth, _st.widthMin, _st.widthMax);\n\t_textWidth = w - (goodWidth - textWidth);\n\tsetMinWidth(w);\n\tupdate();\n}\n\nvoid WhoReactedEntryAction::paint(Painter &&p) {\n\tconst auto enabled = isEnabled();\n\tconst auto selected = isSelected();\n\tif (selected && _st.itemBgOver->c.alpha() < 255) {\n\t\tp.fillRect(0, 0, width(), _height, _st.itemBg);\n\t}\n\tconst auto bg = selected ? _st.itemBgOver : _st.itemBg;\n\tp.fillRect(0, 0, width(), _height, bg);\n\tif (enabled) {\n\t\tpaintRipple(p, 0, 0);\n\t}\n\tconst auto photoSize = st::defaultWhoRead.photoSize;\n\tconst auto photoLeft = st::defaultWhoRead.photoLeft;\n\tconst auto photoTop = (height() - photoSize) / 2;\n\tconst auto preloader = (_type == WhoReactedType::Preloader);\n\tconst auto preloaderBrush = preloader\n\t\t? [&] {\n\t\t\tauto color = _st.itemFg->c;\n\t\t\tcolor.setAlphaF(color.alphaF() * kPreloaderAlpha);\n\t\t\treturn QBrush(color);\n\t\t}() : QBrush();\n\tif (preloader) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(preloaderBrush);\n\t\tp.drawEllipse(photoLeft, photoTop, photoSize, photoSize);\n\t} else if (!_userpic.isNull()) {\n\t\tp.drawImage(photoLeft, photoTop, _userpic);\n\t\tif (_type == WhoReactedType::RefRecipientNow) {\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\tauto bgPen = bg->p;\n\t\t\tbgPen.setWidthF(st::lineWidth * 6.);\n\t\t\tp.setPen(bgPen);\n\t\t\tp.drawEllipse(photoLeft, photoTop, photoSize, photoSize);\n\t\t\tauto fgPen = st::windowBgActive->p;\n\t\t\tfgPen.setWidthF(st::lineWidth * 2.);\n\t\t\tp.setPen(fgPen);\n\t\t\tp.drawEllipse(photoLeft, photoTop, photoSize, photoSize);\n\t\t}\n\t} else if (!_custom) {\n\t\tst::menuIconReactions.paintInCenter(\n\t\t\tp,\n\t\t\tQRect(photoLeft, photoTop, photoSize, photoSize));\n\t}\n\n\tconst auto withDate = !_date.isEmpty();\n\tconst auto textTop = withDate\n\t\t? st::whoReadNameWithDateTop\n\t\t: (height() - _st.itemStyle.font->height) / 2;\n\tif (_type == WhoReactedType::Preloader) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(preloaderBrush);\n\t\tconst auto height = _st.itemStyle.font->height / 2;\n\t\tp.drawRoundedRect(\n\t\t\tst::defaultWhoRead.nameLeft,\n\t\t\ttextTop + (_st.itemStyle.font->height - height) / 2,\n\t\t\t_textWidth,\n\t\t\theight,\n\t\t\theight / 2.,\n\t\t\theight / 2.);\n\t} else {\n\t\tp.setPen(selected\n\t\t\t? _st.itemFgOver\n\t\t\t: enabled\n\t\t\t? _st.itemFg\n\t\t\t: _st.itemFgDisabled);\n\t\t_text.drawLeftElided(\n\t\t\tp,\n\t\t\tst::defaultWhoRead.nameLeft,\n\t\t\ttextTop,\n\t\t\t_textWidth,\n\t\t\twidth());\n\t}\n\tif (_type == WhoReactedType::RefRecipient\n\t\t|| _type == WhoReactedType::RefRecipientNow) {\n\t\tp.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);\n\t\t_date.drawLeftElided(\n\t\t\tp,\n\t\t\tst::defaultWhoRead.nameLeft,\n\t\t\tst::whoReadDateTop,\n\t\t\t_textWidth,\n\t\t\twidth());\n\t} else if (withDate) {\n\t\tconst auto iconPosition = QPoint(\n\t\t\tst::defaultWhoRead.nameLeft,\n\t\t\tst::whoReadDateTop) + st::whoReadDateChecksPosition;\n\t\tconst auto icon = [&] {\n\t\t\tswitch (_type) {\n\t\t\tcase WhoReactedType::Viewed:\n\t\t\t\treturn &(selected\n\t\t\t\t\t? st::whoReadDateChecksOver\n\t\t\t\t\t: st::whoReadDateChecks);\n\t\t\tcase WhoReactedType::Reacted:\n\t\t\t\treturn &(selected\n\t\t\t\t\t? st::whoLikedDateHeartOver\n\t\t\t\t\t: st::whoLikedDateHeart);\n\t\t\tcase WhoReactedType::Reposted:\n\t\t\t\treturn &(selected\n\t\t\t\t\t? st::whoRepostedDateHeartOver\n\t\t\t\t\t: st::whoRepostedDateHeart);\n\t\t\tcase WhoReactedType::Forwarded:\n\t\t\t\treturn &(selected\n\t\t\t\t\t? st::whoForwardedDateHeartOver\n\t\t\t\t\t: st::whoForwardedDateHeart);\n\t\t\t}\n\t\t\tUnexpected(\"Type in WhoReactedEntryAction::paint.\");\n\t\t}();\n\t\ticon->paint(p, iconPosition, width());\n\t\tp.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);\n\t\t_date.drawLeftElided(\n\t\t\tp,\n\t\t\tst::defaultWhoRead.nameLeft + st::whoReadDateSkip,\n\t\t\tst::whoReadDateTop,\n\t\t\t_textWidth - st::whoReadDateSkip,\n\t\t\twidth());\n\t}\n\tif (_custom) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto size = Emoji::GetSizeNormal() / ratio;\n\t\tconst auto skip = (size - _customSize) / 2;\n\t\t_custom->paint(p, {\n\t\t\t.textColor = (selected ? _st.itemFgOver : _st.itemFg)->c,\n\t\t\t.now = crl::now(),\n\t\t\t.position = QPoint(\n\t\t\t\twidth() - _st.itemPadding.right() - size + skip,\n\t\t\t\t(height() - _customSize) / 2),\n\t\t});\n\t}\n}\n\nbool operator==(const WhoReadParticipant &a, const WhoReadParticipant &b) {\n\treturn (a.id == b.id)\n\t\t&& (a.name == b.name)\n\t\t&& (a.date == b.date)\n\t\t&& (a.userpicKey == b.userpicKey);\n}\n\nbool operator!=(const WhoReadParticipant &a, const WhoReadParticipant &b) {\n\treturn !(a == b);\n}\n\nbase::unique_qptr<Menu::ItemBase> WhoReactedContextAction(\n\t\tnot_null<PopupMenu*> menu,\n\t\trpl::producer<WhoReadContent> content,\n\t\tCustomEmojiFactory factory,\n\t\tFn<void(WhoReadParticipant)> participantChosen,\n\t\tFn<void()> showAllChosen) {\n\treturn base::make_unique_q<Action>(\n\t\tmenu,\n\t\tstd::move(content),\n\t\tstd::move(factory),\n\t\tstd::move(participantChosen),\n\t\tstd::move(showAllChosen));\n}\n\nbase::unique_qptr<Menu::ItemBase> WhenReadContextAction(\n\t\tnot_null<PopupMenu*> menu,\n\t\trpl::producer<WhoReadContent> content,\n\t\tFn<void()> showOrPremium) {\n\treturn base::make_unique_q<WhenAction>(\n\t\tmenu,\n\t\tstd::move(content),\n\t\tstd::move(showOrPremium));\n}\n\nWhoReactedListMenu::WhoReactedListMenu(\n\tCustomEmojiFactory factory,\n\tFn<void(WhoReadParticipant)> participantChosen,\n\tFn<void()> showAllChosen)\n: _customEmojiFactory(std::move(factory))\n, _participantChosen(std::move(participantChosen))\n, _showAllChosen(std::move(showAllChosen)) {\n}\n\nvoid WhoReactedListMenu::clear() {\n\t_actions.clear();\n}\n\nvoid WhoReactedListMenu::populate(\n\t\tnot_null<PopupMenu*> menu,\n\t\tconst WhoReadContent &content,\n\t\tFn<void()> refillTopActions,\n\t\tint addedToBottom,\n\t\tFn<void()> appendBottomActions) {\n\tconst auto reactions = ranges::count_if(\n\t\tcontent.participants,\n\t\t[](const auto &p) { return !p.customEntityData.isEmpty(); });\n\tconst auto addShowAll = (content.fullReactionsCount > reactions);\n\tconst auto actionsCount = int(content.participants.size())\n\t\t+ (addShowAll ? 1 : 0);\n\tif (_actions.size() > actionsCount) {\n\t\t_actions.clear();\n\t\tmenu->clearActions();\n\t\tif (refillTopActions) {\n\t\t\trefillTopActions();\n\t\t}\n\t\taddedToBottom = 0;\n\t}\n\tauto index = 0;\n\tconst auto append = [&](WhoReactedEntryData &&data) {\n\t\tif (index < _actions.size()) {\n\t\t\t_actions[index]->setData(std::move(data));\n\t\t} else {\n\t\t\tauto item = base::make_unique_q<WhoReactedEntryAction>(\n\t\t\t\tmenu->menu(),\n\t\t\t\t_customEmojiFactory,\n\t\t\t\tmenu->menu()->st(),\n\t\t\t\tstd::move(data));\n\t\t\t_actions.push_back(item.get());\n\t\t\tconst auto count = int(menu->actions().size());\n\t\t\tif (addedToBottom > 0 && addedToBottom <= count) {\n\t\t\t\tmenu->insertAction(count - addedToBottom, std::move(item));\n\t\t\t} else {\n\t\t\t\tmenu->addAction(std::move(item));\n\t\t\t}\n\t\t}\n\t\t++index;\n\t};\n\tfor (const auto &participant : content.participants) {\n\t\tconst auto chosen = [call = _participantChosen, participant] {\n\t\t\tcall(participant);\n\t\t};\n\t\tappend({\n\t\t\t.text = participant.name,\n\t\t\t.date = participant.date,\n\t\t\t.type = (participant.dateReacted\n\t\t\t\t? WhoReactedType::Reacted\n\t\t\t\t: WhoReactedType::Viewed),\n\t\t\t.customEntityData = participant.customEntityData,\n\t\t\t.userpic = participant.userpicLarge,\n\t\t\t.callback = chosen,\n\t\t});\n\t}\n\tif (addShowAll) {\n\t\tappend({\n\t\t\t.text = tr::lng_context_seen_reacted_all(tr::now),\n\t\t\t.callback = _showAllChosen,\n\t\t});\n\t}\n\tif (!addedToBottom && appendBottomActions) {\n\t\tappendBottomActions();\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/who_reacted_context_action.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n#include \"ui/widgets/menu/menu_item_base.h\"\n#include \"ui/text/text_custom_emoji.h\"\n\nnamespace Ui {\n\nclass PopupMenu;\n\nstruct WhoReadParticipant {\n\tQString name;\n\tQString date;\n\tbool dateReacted = false;\n\tQString customEntityData;\n\tQImage userpicSmall;\n\tQImage userpicLarge;\n\tstd::pair<uint64, uint64> userpicKey = {};\n\tuint64 id = 0;\n\n\tstatic constexpr auto kMaxSmallUserpics = 3;\n};\n\nbool operator==(const WhoReadParticipant &a, const WhoReadParticipant &b);\nbool operator!=(const WhoReadParticipant &a, const WhoReadParticipant &b);\n\nenum class WhoReadType {\n\tSeen,\n\tListened,\n\tWatched,\n\tReacted,\n\tEdited,\n\tOriginal,\n};\n\nenum class WhoReadState : uchar {\n\tEmpty,\n\tUnknown,\n\tMyHidden,\n\tHisHidden,\n\tTooOld,\n};\n\nstruct WhoReadContent {\n\tstd::vector<WhoReadParticipant> participants;\n\tWhoReadType type = WhoReadType::Seen;\n\tQString singleCustomEntityData;\n\tint fullReactionsCount = 0;\n\tint fullReadCount = 0;\n\tWhoReadState state = WhoReadState::Empty;\n};\n\n[[nodiscard]] base::unique_qptr<Menu::ItemBase> WhoReactedContextAction(\n\tnot_null<PopupMenu*> menu,\n\trpl::producer<WhoReadContent> content,\n\tText::CustomEmojiFactory factory,\n\tFn<void(WhoReadParticipant)> participantChosen,\n\tFn<void()> showAllChosen);\n\n[[nodiscard]] base::unique_qptr<Menu::ItemBase> WhenReadContextAction(\n\tnot_null<PopupMenu*> menu,\n\trpl::producer<WhoReadContent> content,\n\tFn<void()> showOrPremium = nullptr);\n\nenum class WhoReactedType : uchar {\n\tViewed,\n\tReacted,\n\tReposted,\n\tForwarded,\n\tPreloader,\n\tRefRecipient,\n\tRefRecipientNow,\n};\n\nstruct WhoReactedEntryData {\n\tQString text;\n\tQString date;\n\tWhoReactedType type = WhoReactedType::Viewed;\n\tQString customEntityData;\n\tQImage userpic;\n\tFn<void()> callback;\n};\n\nclass WhoReactedEntryAction final : public Menu::ItemBase {\npublic:\n\tusing Data = WhoReactedEntryData;\n\n\tWhoReactedEntryAction(\n\t\tnot_null<Ui::Menu::Menu*> parent,\n\t\tText::CustomEmojiFactory factory,\n\t\tconst style::Menu &st,\n\t\tData &&data);\n\n\tvoid setData(Data &&data);\n\n\tnot_null<QAction*> action() const override;\n\tbool isEnabled() const override;\n\nprivate:\n\tint contentHeight() const override;\n\n\tvoid paint(Painter &&p);\n\n\tconst not_null<QAction*> _dummyAction;\n\tconst Text::CustomEmojiFactory _customEmojiFactory;\n\tconst style::Menu &_st;\n\tconst int _height = 0;\n\n\tText::String _text;\n\tText::String _date;\n\tstd::unique_ptr<Ui::Text::CustomEmoji> _custom;\n\tQImage _userpic;\n\tint _textWidth = 0;\n\tint _customSize = 0;\n\tWhoReactedType _type = WhoReactedType::Viewed;\n\n};\n\nclass WhoReactedListMenu final {\npublic:\n\tWhoReactedListMenu(\n\t\tText::CustomEmojiFactory factory,\n\t\tFn<void(WhoReadParticipant)> participantChosen,\n\t\tFn<void()> showAllChosen);\n\n\tvoid clear();\n\tvoid populate(\n\t\tnot_null<PopupMenu*> menu,\n\t\tconst WhoReadContent &content,\n\t\tFn<void()> refillTopActions = nullptr,\n\t\tint addedToBottom = 0,\n\t\tFn<void()> appendBottomActions = nullptr);\n\nprivate:\n\tconst Text::CustomEmojiFactory _customEmojiFactory;\n\tconst Fn<void(WhoReadParticipant)> _participantChosen;\n\tconst Fn<void()> _showAllChosen;\n\n\tstd::vector<not_null<WhoReactedEntryAction*>> _actions;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/window_outdated_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/window_outdated_bar.h\"\n\n#include \"ui/widgets/labels.h\" // Ui::FlatLabel\n#include \"ui/widgets/buttons.h\" // Ui::IconButton\n#include \"ui/wrap/slide_wrap.h\" // Ui::SlideWrap\n#include \"base/platform/base_platform_info.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_window.h\"\n\n#include <QtCore/QFile>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kMinimalSkip = 7;\nconstexpr auto kSoonSkip = 30;\nconstexpr auto kNowSkip = 90;\n\nclass Bar final : public RpWidget {\npublic:\n\tBar(not_null<QWidget*> parent, QDate date);\n\n\tint resizeGetHeight(int newWidth) override;\n\n\t[[nodiscard]] rpl::producer<> hideClicks() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tQDate _date;\n\tobject_ptr<FlatLabel> _title;\n\tobject_ptr<FlatLabel> _details;\n\tobject_ptr<IconButton> _close;\n\tbool _soon = false;\n\n};\n\n[[nodiscard]] rpl::producer<QString> OutdatedReasonPhrase() {\n\tconst auto why = Platform::WhySystemBecomesOutdated();\n\treturn (why == Platform::OutdateReason::Is32Bit)\n\t\t? tr::lng_outdated_title_bits(tr::upper)\n\t\t: tr::lng_outdated_title(tr::upper);\n}\n\nBar::Bar(not_null<QWidget*> parent, QDate date)\n: _date(date)\n, _title(\n\tthis,\n\tOutdatedReasonPhrase(),\n\tst::windowOutdatedTitle)\n, _details(this,\n\tQString(),\n\tst::windowOutdatedDetails)\n, _close(this, st::windowOutdatedClose)\n, _soon(_date >= QDate::currentDate()) {\n\t_title->setTryMakeSimilarLines(true);\n\t_details->setTryMakeSimilarLines(true);\n\t_details->setText(_soon\n\t\t? tr::lng_outdated_soon(tr::now, lt_date, langDayOfMonthFull(date))\n\t\t: tr::lng_outdated_now(tr::now));\n}\n\nrpl::producer<> Bar::hideClicks() const {\n\treturn _close->clicks() | rpl::to_empty;\n}\n\nint Bar::resizeGetHeight(int newWidth) {\n\tconst auto padding = st::windowOutdatedPadding;\n\tconst auto skip = _close->width();\n\tconst auto available = newWidth - 2 * skip;\n\n\t_title->resizeToWidth(available);\n\t_title->moveToLeft(skip, padding.top(), newWidth);\n\n\t_details->resizeToWidth(available);\n\t_details->moveToLeft(\n\t\tskip,\n\t\t_title->y() + _title->height() + st::windowOutdatedSkip,\n\t\tnewWidth);\n\n\t_close->moveToRight(0, 0, newWidth);\n\n\treturn _details->y() + _details->height() + padding.bottom();\n}\n\nvoid Bar::paintEvent(QPaintEvent *e) {\n\tQPainter(this).fillRect(\n\t\te->rect(),\n\t\t_soon ? st::outdateSoonBg : st::outdatedBg);\n}\n\n[[nodiscard]] QString LastHiddenPath(const QString &workingDir) {\n\treturn workingDir + u\"tdata/outdated_hidden\"_q;\n}\n\n[[nodiscard]] bool Skip(const QDate &date, const QString &workingDir) {\n\tauto file = QFile(LastHiddenPath(workingDir));\n\tif (!file.open(QIODevice::ReadOnly) || file.size() != sizeof(qint32)) {\n\t\treturn false;\n\t}\n\tconst auto content = file.readAll();\n\tif (content.size() != sizeof(qint32)) {\n\t\treturn false;\n\t}\n\tconst auto value = *reinterpret_cast<const qint32*>(content.constData());\n\tconst auto year = (value / 10000);\n\tconst auto month = (value % 10000) / 100;\n\tconst auto day = (value % 100);\n\tconst auto last = QDate(year, month, day);\n\tif (!last.isValid()) {\n\t\treturn false;\n\t}\n\tconst auto today = QDate::currentDate();\n\tif (last > today) {\n\t\treturn false;\n\t}\n\tconst auto skipped = last.daysTo(today);\n\tif (today > date && last <= date) {\n\t\treturn (skipped < kMinimalSkip);\n\t} else if (today <= date) {\n\t\treturn (skipped < kSoonSkip);\n\t} else {\n\t\treturn (skipped < kNowSkip);\n\t}\n}\n\nvoid Closed(const QString &workingDir) {\n\tauto file = QFile(LastHiddenPath(workingDir));\n\tif (!file.open(QIODevice::WriteOnly)) {\n\t\treturn;\n\t}\n\tconst auto today = QDate::currentDate();\n\tconst auto value = qint32(0\n\t\t+ today.year() * 10000\n\t\t+ today.month() * 100\n\t\t+ today.day());\n\tfile.write(QByteArray::fromRawData(\n\t\treinterpret_cast<const char*>(&value),\n\t\tsizeof(qint32)));\n}\n\n} // namespace\n\nobject_ptr<RpWidget> CreateOutdatedBar(\n\t\tnot_null<QWidget*> parent,\n\t\tconst QString &workingPath) {\n\tconst auto date = Platform::WhenSystemBecomesOutdated();\n\tif (date.isNull()) {\n\t\treturn { nullptr };\n\t} else if (Skip(date, workingPath)) {\n\t\treturn { nullptr };\n\t}\n\n\tauto result = object_ptr<SlideWrap<Bar>>(\n\t\tparent.get(),\n\t\tobject_ptr<Bar>(parent.get(), date));\n\tconst auto wrap = result.data();\n\n\twrap->entity()->hideClicks(\n\t) | rpl::on_next([=] {\n\t\twrap->toggle(false, anim::type::normal);\n\t\tClosed(workingPath);\n\t}, wrap->lifetime());\n\n\twrap->entity()->resizeToWidth(st::windowMinWidth);\n\twrap->show(anim::type::instant);\n\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/window_outdated_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\n\nclass RpWidget;\n\n[[nodiscard]] object_ptr<RpWidget> CreateOutdatedBar(\n\tnot_null<QWidget*> parent,\n\tconst QString &workingPath);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/window_outdated_bar_dummy.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/window_outdated_bar.h\"\n\nnamespace Ui {\n\nobject_ptr<RpWidget> CreateOutdatedBar(\n\t\tnot_null<QWidget*> parent,\n\t\tconst QString &workingPath) {\n\treturn { nullptr };\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/window_screen_reader_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/controls/window_screen_reader_bar.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/screen_reader_mode.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"styles/style_window.h\"\n\nnamespace Ui {\nnamespace {\n\nclass DisableButton final : public RippleButton {\npublic:\n\texplicit DisableButton(QWidget *parent);\n\n\tQAccessible::Role accessibilityRole() override;\n\tQString accessibilityName() override;\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\n\tQString _text;\n\tint _textWidth = 0;\n\n};\n\nclass InaccessibleLabel final : public FlatLabel {\npublic:\n\tusing FlatLabel::FlatLabel;\n\n\tQAccessible::Role accessibilityRole() override;\n\tQString accessibilityName() override;\n\n};\n\nclass Bar final : public RpWidget {\npublic:\n\texplicit Bar(not_null<QWidget*> parent);\n\n\tQAccessible::Role accessibilityRole() override;\n\tQString accessibilityName() override;\n\n\tint resizeGetHeight(int newWidth) override;\n\n\t[[nodiscard]] rpl::producer<> disableClicks() const;\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tint accessibilityChildCount() const override;\n\n\tobject_ptr<InaccessibleLabel> _label;\n\tobject_ptr<DisableButton> _disable;\n\n};\n\nDisableButton::DisableButton(QWidget *parent)\n: RippleButton(parent, st::windowScreenReaderButtonRipple) {\n\t_text = tr::lng_screen_reader_bar_disable(tr::now);\n\t_textWidth = st::windowScreenReaderDisableTextStyle.font->width(_text);\n\tconst auto padding = st::windowScreenReaderButtonPadding;\n\tsetFixedSize(\n\t\tpadding.left() + _textWidth + padding.right(),\n\t\tst::windowScreenReaderButtonHeight);\n}\n\nvoid DisableButton::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tp.fillRect(e->rect(), st::activeButtonBg);\n\tpaintRipple(p, 0, 0);\n\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto border = st::windowScreenReaderButtonBorderWidth;\n\tconst auto half = border / 2.;\n\tconst auto radius = st::windowScreenReaderButtonRadius;\n\n\tp.setPen(QPen(st::activeButtonFg, border));\n\tp.setBrush(Qt::NoBrush);\n\tp.drawRoundedRect(\n\t\tQRectF(rect()).marginsRemoved(QMarginsF(half, half, half, half)),\n\t\tradius,\n\t\tradius);\n\n\tconst auto &font = st::windowScreenReaderDisableTextStyle.font;\n\tp.setFont(font);\n\tp.setPen(st::activeButtonFg);\n\tp.drawText(\n\t\t(width() - _textWidth) / 2,\n\t\t(height() - font->height) / 2 + font->ascent,\n\t\t_text);\n}\n\nQImage DisableButton::prepareRippleMask() const {\n\treturn RippleAnimation::RoundRectMask(\n\t\tsize(),\n\t\tst::windowScreenReaderButtonRadius);\n}\n\nQPoint DisableButton::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos());\n}\n\nQAccessible::Role DisableButton::accessibilityRole() {\n\treturn QAccessible::NoRole;\n}\n\nQString DisableButton::accessibilityName() {\n\treturn QString();\n}\n\nQAccessible::Role InaccessibleLabel::accessibilityRole() {\n\treturn QAccessible::Role::NoRole;\n}\n\nQString InaccessibleLabel::accessibilityName() {\n\treturn QString();\n}\n\nBar::Bar(not_null<QWidget*> parent)\n: RpWidget(parent)\n, _label(\n\tthis,\n\ttr::lng_screen_reader_bar_text(),\n\tst::windowScreenReaderLabel)\n, _disable(object_ptr<DisableButton>(this)) {\n}\n\nint Bar::resizeGetHeight(int newWidth) {\n\tconst auto padding = st::windowScreenReaderPadding;\n\tconst auto available = newWidth\n\t\t- padding.left()\n\t\t- padding.right()\n\t\t- _disable->width()\n\t\t- padding.left();\n\n\t_label->resizeToWidth(available);\n\n\tconst auto contentHeight = std::max(\n\t\t_label->height(),\n\t\t_disable->height());\n\tconst auto height = contentHeight + padding.top() + padding.bottom();\n\n\t_label->moveToLeft(\n\t\tpadding.left(),\n\t\tpadding.top() + (contentHeight - _label->height()) / 2,\n\t\tnewWidth);\n\n\t_disable->moveToRight(\n\t\tpadding.right(),\n\t\tpadding.top() + (contentHeight - _disable->height()) / 2,\n\t\tnewWidth);\n\n\treturn height;\n}\n\nvoid Bar::paintEvent(QPaintEvent *e) {\n\tQPainter(this).fillRect(e->rect(), st::activeButtonBg);\n}\n\nrpl::producer<> Bar::disableClicks() const {\n\treturn _disable->clicks() | rpl::to_empty;\n}\n\nQAccessible::Role Bar::accessibilityRole() {\n\treturn QAccessible::NoRole;\n}\n\nQString Bar::accessibilityName() {\n\treturn QString();\n}\n\nint Bar::accessibilityChildCount() const {\n\treturn 0;\n}\n\n} // namespace\n\nobject_ptr<RpWidget> CreateScreenReaderBar(\n\t\tnot_null<QWidget*> parent,\n\t\tFn<void()> disableCallback) {\n\tauto result = object_ptr<SlideWrap<Bar>>(\n\t\tparent.get(),\n\t\tobject_ptr<Bar>(parent.get()));\n\tconst auto wrap = result.data();\n\n\twrap->entity()->disableClicks(\n\t) | rpl::on_next([=] {\n\t\tif (disableCallback) {\n\t\t\tdisableCallback();\n\t\t}\n\t}, wrap->lifetime());\n\n\tUi::ScreenReaderModeActiveValue(\n\t) | rpl::on_next([=](bool active) {\n\t\twrap->toggle(active, anim::type::normal);\n\t}, wrap->lifetime());\n\n\twrap->toggle(Ui::ScreenReaderModeActive(), anim::type::instant);\n\n\twrap->entity()->resizeToWidth(st::windowMinWidth);\n\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/controls/window_screen_reader_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n\nnamespace Ui {\nclass RpWidget;\n\n[[nodiscard]] object_ptr<RpWidget> CreateScreenReaderBar(\n\tnot_null<QWidget*> parent,\n\tFn<void()> disableCallback);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/countryinput.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/countryinput.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/multi_select.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/boxes/country_select_box.h\"\n#include \"ui/painter.h\"\n#include \"countries/countries_instance.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_intro.h\"\n#include \"styles/style_widgets.h\"\n\nCountryInput::CountryInput(\n\tQWidget *parent,\n\tstd::shared_ptr<Ui::Show> show,\n\tconst style::InputField &st)\n: RpWidget(parent)\n, _show(show)\n, _st(st)\n, _text(tr::lng_country_code(tr::now)) {\n\tresize(_st.width, _st.heightMin);\n\tsetFocusPolicy(Qt::StrongFocus);\n}\n\nvoid CountryInput::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tQRect r(rect().intersected(e->rect()));\n\tif (_st.textBg->c.alphaF() > 0.) {\n\t\tp.fillRect(r, _st.textBg);\n\t}\n\tif (_st.border) {\n\t\tp.fillRect(\n\t\t\t0,\n\t\t\theight() - _st.border,\n\t\t\twidth(),\n\t\t\t_st.border,\n\t\t\t_st.borderFg);\n\t}\n\n\tst::introCountryIcon.paint(\n\t\tp,\n\t\twidth()\n\t\t\t- st::introCountryIcon.width()\n\t\t\t- st::introCountryIconPosition.x(),\n\t\tst::introCountryIconPosition.y(),\n\t\twidth());\n\n\tp.setFont(_st.style.font);\n\tp.setPen(_st.textFg);\n\tp.drawText(rect().marginsRemoved(_st.textMargins), _text, _st.textAlign);\n}\n\nvoid CountryInput::mouseMoveEvent(QMouseEvent *e) {\n\tbool newActive = rect().contains(e->pos());\n\tif (_active != newActive) {\n\t\t_active = newActive;\n\t\tsetCursor(_active ? style::cur_pointer : style::cur_default);\n\t}\n}\n\nvoid CountryInput::mousePressEvent(QMouseEvent *e) {\n\tmouseMoveEvent(e);\n\tif (_active) {\n\t\tauto object = Box<Ui::CountrySelectBox>();\n\t\tconst auto box = base::make_weak(object.data());\n\t\t_show->showBox(std::move(object), Ui::LayerOption::CloseOther);\n\t\tbox->entryChosen(\n\t\t) | rpl::on_next([=](\n\t\t\t\tconst Ui::CountrySelectBox::Entry &entry) {\n\t\t\tif (box) {\n\t\t\t\tbox->closeBox();\n\t\t\t}\n\n\t\t\tconst auto &list = Countries::Instance().list();\n\t\t\tconst auto infoIt = ranges::find(\n\t\t\t\tlist,\n\t\t\t\tentry.iso2,\n\t\t\t\t&Countries::Info::iso2);\n\t\t\tif (infoIt == end(list)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto info = *infoIt;\n\t\t\tconst auto it = ranges::find(\n\t\t\t\tinfo.codes,\n\t\t\t\tentry.code,\n\t\t\t\t&Countries::CallingCodeInfo::callingCode);\n\t\t\tif (it != end(info.codes)) {\n\t\t\t\tchooseCountry(\n\t\t\t\t\t&info,\n\t\t\t\t\tstd::distance(begin(info.codes), it));\n\t\t\t}\n\t\t}, lifetime());\n\t}\n}\n\nvoid CountryInput::keyPressEvent(QKeyEvent* e) {\n\tif (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return || e->key() == Qt::Key_Space) {\n\t\tauto object = Box<Ui::CountrySelectBox>();\n\t\tconst auto box = base::make_weak(object.data());\n\t\t_show->showBox(std::move(object), Ui::LayerOption::CloseOther);\n\t\tbox->entryChosen(\n\t\t) | rpl::on_next([=](\n\t\t\tconst Ui::CountrySelectBox::Entry& entry) {\n\t\t\t\tif (box) {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t}\n\n\t\t\t\tconst auto& list = Countries::Instance().list();\n\t\t\t\tconst auto infoIt = ranges::find(\n\t\t\t\t\tlist,\n\t\t\t\t\tentry.iso2,\n\t\t\t\t\t&Countries::Info::iso2);\n\t\t\t\tif (infoIt == end(list)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto info = *infoIt;\n\t\t\t\tconst auto it = ranges::find(\n\t\t\t\t\tinfo.codes,\n\t\t\t\t\tentry.code,\n\t\t\t\t\t&Countries::CallingCodeInfo::callingCode);\n\t\t\t\tif (it != end(info.codes)) {\n\t\t\t\t\tchooseCountry(\n\t\t\t\t\t\t&info,\n\t\t\t\t\t\tstd::distance(begin(info.codes), it));\n\t\t\t\t}\n\t\t\t}, lifetime());\n\t} else {\n\t\tRpWidget::keyPressEvent(e);\n\t}\n}\n\nvoid CountryInput::enterEventHook(QEnterEvent *e) {\n\tsetMouseTracking(true);\n}\n\nvoid CountryInput::leaveEventHook(QEvent *e) {\n\tsetMouseTracking(false);\n\t_active = false;\n\tsetCursor(style::cur_default);\n}\n\nvoid CountryInput::onChooseCode(const QString &code) {\n\t_show->hideLayer();\n\t_chosenIso = QString();\n\tif (code.length()) {\n\t\tconst auto &byCode = Countries::Instance().byCode();\n\t\tconst auto i = byCode.constFind(code);\n\t\tif (i != byCode.cend()) {\n\t\t\tconst auto info = *i;\n\t\t\t_chosenIso = info->iso2;\n\t\t\tsetText(info->name);\n\t\t} else {\n\t\t\tsetText(tr::lng_bad_country_code(tr::now));\n\t\t}\n\t} else {\n\t\tsetText(tr::lng_country_code(tr::now));\n\t}\n\tupdate();\n}\n\nbool CountryInput::chooseCountry(const QString &iso) {\n\tconst auto &byISO2 = Countries::Instance().byISO2();\n\tconst auto i = byISO2.constFind(iso);\n\tconst auto info = (i != byISO2.cend()) ? (*i) : nullptr;\n\n\t_chosenIso = QString();\n\tif (info) {\n\t\tchooseCountry(info, 0);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid CountryInput::chooseCountry(\n\t\tnot_null<const Countries::Info*> info,\n\t\tint codeIndex) {\n\t_chosenIso = info->iso2;\n\tsetText(info->name);\n\t_codeChanged.fire_copy(info->codes[codeIndex].callingCode);\n\tupdate();\n}\n\nrpl::producer<QString> CountryInput::codeChanged() const {\n\treturn _codeChanged.events();\n}\n\nvoid CountryInput::setText(const QString &newText) {\n\t_text = _st.style.font->elided(\n\t\tnewText,\n\t\twidth() - _st.textMargins.left() - _st.textMargins.right());\n\taccessibilityNameChanged();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/countryinput.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace style {\nstruct InputField;\n} // namespace style\n\nnamespace Countries {\nstruct Info;\n} // namespace Countries\n\nnamespace Ui {\nclass Show;\n} // namespace Ui\n\nclass CountryInput : public Ui::RpWidget {\npublic:\n\tCountryInput(\n\t\tQWidget *parent,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tconst style::InputField &st);\n\n\tQAccessible::Role accessibilityRole() override {\n\t\treturn QAccessible::Role::PushButton;\n\t}\n\tQString accessibilityName() override {\n\t\treturn _text;\n\t}\n\n\t[[nodiscard]] QString iso() const {\n\t\treturn _chosenIso;\n\t}\n\tbool chooseCountry(const QString &country);\n\n\tvoid onChooseCode(const QString &code);\n\n\trpl::producer<QString> codeChanged() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent* e) override;\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\nprivate:\n\tvoid chooseCountry(not_null<const Countries::Info*> info, int codeIndex);\n\tvoid setText(const QString &newText);\n\n\tconst std::shared_ptr<Ui::Show> _show;\n\tconst style::InputField &_st;\n\tbool _active = false;\n\tQString _text;\n\tQString _chosenIso;\n\n\trpl::event_stream<QString> _codeChanged;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/dynamic_thumbnails.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/dynamic_thumbnails.h\"\n\n#include \"data/data_changes.h\"\n#include \"data/data_cloud_file.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_story.h\"\n#include \"layout/layout_document_generic_preview.h\"\n#include \"main/main_session.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/dynamic_image.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/userpic_view.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_overview.h\"\n\nnamespace Ui {\nnamespace {\n\nclass PeerUserpic final : public DynamicImage {\npublic:\n\tPeerUserpic(not_null<PeerData*> peer, bool forceRound);\n\n\tstd::shared_ptr<DynamicImage> clone() override;\n\n\tQImage image(int size) override;\n\tvoid subscribeToUpdates(Fn<void()> callback) override;\n\nprivate:\n\tstruct Subscribed {\n\t\texplicit Subscribed(Fn<void()> callback)\n\t\t: callback(std::move(callback)) {\n\t\t}\n\n\t\tUi::PeerUserpicView view;\n\t\tFn<void()> callback;\n\t\tInMemoryKey key;\n\t\tint paletteVersion = 0;\n\t\trpl::lifetime photoLifetime;\n\t\trpl::lifetime downloadLifetime;\n\t};\n\n\t[[nodiscard]] bool waitingUserpicLoad() const;\n\tvoid processNewPhoto();\n\n\tconst not_null<PeerData*> _peer;\n\tQImage _frame;\n\tstd::unique_ptr<Subscribed> _subscribed;\n\tbool _forceRound = false;\n\n};\n\nclass MediaThumbnail : public DynamicImage {\npublic:\n\texplicit MediaThumbnail(\n\t\tData::FileOrigin origin,\n\t\tbool forceRound,\n\t\tbool centerCrop);\n\n\tQImage image(int size) override;\n\tvoid subscribeToUpdates(Fn<void()> callback) override;\n\nprotected:\n\tstruct Thumb {\n\t\tImage *image = nullptr;\n\t\tbool blurred = false;\n\t};\n\n\t[[nodiscard]] Data::FileOrigin origin() const;\n\t[[nodiscard]] bool forceRound() const;\n\t[[nodiscard]] bool centerCrop() const;\n\n\t[[nodiscard]] virtual Main::Session &session() = 0;\n\t[[nodiscard]] virtual Thumb loaded(Data::FileOrigin origin) = 0;\n\tvirtual void clear() = 0;\n\nprivate:\n\tconst Data::FileOrigin _origin;\n\tconst bool _forceRound;\n\tconst bool _centerCrop;\n\tQImage _full;\n\trpl::lifetime _subscription;\n\tQImage _prepared;\n\tbool _blurred = false;\n\n};\n\nclass PhotoThumbnail final : public MediaThumbnail {\npublic:\n\tPhotoThumbnail(\n\t\tnot_null<PhotoData*> photo,\n\t\tData::FileOrigin origin,\n\t\tbool forceRound,\n\t\tbool centerCrop);\n\n\tstd::shared_ptr<DynamicImage> clone() override;\n\nprivate:\n\tMain::Session &session() override;\n\tThumb loaded(Data::FileOrigin origin) override;\n\tvoid clear() override;\n\n\tconst not_null<PhotoData*> _photo;\n\tstd::shared_ptr<Data::PhotoMedia> _media;\n\n};\n\nclass VideoThumbnail final : public MediaThumbnail {\npublic:\n\tVideoThumbnail(\n\t\tnot_null<DocumentData*> video,\n\t\tData::FileOrigin origin,\n\t\tbool forceRound,\n\t\tbool centerCrop);\n\n\tstd::shared_ptr<DynamicImage> clone() override;\n\nprivate:\n\tMain::Session &session() override;\n\tThumb loaded(Data::FileOrigin origin) override;\n\tvoid clear() override;\n\n\tconst not_null<DocumentData*> _video;\n\tstd::shared_ptr<Data::DocumentMedia> _media;\n\n};\n\nclass CallThumbnail final : public DynamicImage {\npublic:\n\tCallThumbnail();\n\n\tstd::shared_ptr<DynamicImage> clone() override;\n\n\tQImage image(int size) override;\n\tvoid subscribeToUpdates(Fn<void()> callback) override;\n\nprivate:\n\tQImage _prepared;\n\n};\n\nclass EmptyThumbnail final : public DynamicImage {\npublic:\n\tstd::shared_ptr<DynamicImage> clone() override;\n\n\tQImage image(int size) override;\n\tvoid subscribeToUpdates(Fn<void()> callback) override;\n\nprivate:\n\tQImage _cached;\n\n};\n\nclass SavedMessagesUserpic final : public DynamicImage {\npublic:\n\tstd::shared_ptr<DynamicImage> clone() override;\n\n\tQImage image(int size) override;\n\tvoid subscribeToUpdates(Fn<void()> callback) override;\n\nprivate:\n\tQImage _frame;\n\tint _paletteVersion = 0;\n\n};\n\nclass RepliesUserpic final : public DynamicImage {\npublic:\n\tstd::shared_ptr<DynamicImage> clone() override;\n\n\tQImage image(int size) override;\n\tvoid subscribeToUpdates(Fn<void()> callback) override;\n\nprivate:\n\tQImage _frame;\n\tint _paletteVersion = 0;\n\n};\n\nclass HiddenAuthorUserpic final : public DynamicImage {\npublic:\n\tstd::shared_ptr<DynamicImage> clone() override;\n\n\tQImage image(int size) override;\n\tvoid subscribeToUpdates(Fn<void()> callback) override;\n\nprivate:\n\tQImage _frame;\n\tint _paletteVersion = 0;\n\n};\n\nclass IconThumbnail final : public DynamicImage {\npublic:\n\texplicit IconThumbnail(const style::icon &icon);\n\n\tstd::shared_ptr<DynamicImage> clone() override;\n\n\tQImage image(int size) override;\n\tvoid subscribeToUpdates(Fn<void()> callback) override;\n\nprivate:\n\tconst style::icon &_icon;\n\tint _paletteVersion = 0;\n\tQImage _frame;\n\n};\n\nclass EmojiThumbnail final : public DynamicImage {\npublic:\n\tEmojiThumbnail(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst QString &data,\n\t\tint loopLimit,\n\t\tFn<bool()> paused,\n\t\tFn<QColor()> textColor);\n\n\tstd::shared_ptr<DynamicImage> clone() override;\n\n\tQImage image(int size) override;\n\tvoid subscribeToUpdates(Fn<void()> callback) override;\n\nprivate:\n\tconst not_null<Data::Session*> _owner;\n\tconst QString _data;\n\tconst int _loopLimit = 0;\n\tstd::unique_ptr<Ui::Text::CustomEmoji> _emoji;\n\tFn<bool()> _paused;\n\tFn<QColor()> _textColor;\n\tQImage _frame;\n\n};\n\nclass GeoThumbnail final : public DynamicImage {\npublic:\n\tGeoThumbnail(\n\t\tnot_null<Data::CloudImage*> data,\n\t\tnot_null<Main::Session*> session,\n\t\tData::FileOrigin origin,\n\t\tbool drawPin);\n\n\tstd::shared_ptr<DynamicImage> clone() override;\n\n\tQImage image(int size) override;\n\tvoid subscribeToUpdates(Fn<void()> callback) override;\n\nprivate:\n\tconst not_null<Data::CloudImage*> _data;\n\tconst not_null<Main::Session*> _session;\n\tconst Data::FileOrigin _origin;\n\tconst bool _drawPin;\n\tstd::shared_ptr<QImage> _view;\n\tQImage _prepared;\n\tint _paletteVersion = 0;\n\trpl::lifetime _subscription;\n\n};\n\nclass DocumentFilePreviewThumbnail final : public DynamicImage {\npublic:\n\tDocumentFilePreviewThumbnail(\n\t\tnot_null<DocumentData*> document,\n\t\tData::FileOrigin origin);\n\n\tstd::shared_ptr<DynamicImage> clone() override;\n\n\tQImage image(int size) override;\n\tvoid subscribeToUpdates(Fn<void()> callback) override;\n\nprivate:\n\t[[nodiscard]] QImage prepareThumbImage(int size);\n\t[[nodiscard]] QImage prepareGenericImage(int size);\n\n\tconst not_null<DocumentData*> _document;\n\tconst Data::FileOrigin _origin;\n\tconst ::Layout::DocumentGenericPreview _generic;\n\tstd::shared_ptr<Data::DocumentMedia> _media;\n\tQImage _prepared;\n\tbool _thumbLoaded = false;\n\tint _paletteVersion = 0;\n\trpl::lifetime _subscription;\n\n};\n\nPeerUserpic::PeerUserpic(not_null<PeerData*> peer, bool forceRound)\n: _peer(peer)\n, _forceRound(forceRound) {\n}\n\nstd::shared_ptr<DynamicImage> PeerUserpic::clone() {\n\treturn std::make_shared<PeerUserpic>(_peer, _forceRound);\n}\n\nQImage PeerUserpic::image(int size) {\n\tExpects(_subscribed != nullptr);\n\n\tconst auto good = (_frame.width() == size * _frame.devicePixelRatio());\n\tconst auto key = _peer->userpicUniqueKey(_subscribed->view);\n\tconst auto paletteVersion = style::PaletteVersion();\n\tif (!good\n\t\t|| (_subscribed->paletteVersion != paletteVersion\n\t\t\t&& _peer->useEmptyUserpic(_subscribed->view))\n\t\t|| (_subscribed->key != key && !waitingUserpicLoad())) {\n\t\t_subscribed->key = key;\n\t\t_subscribed->paletteVersion = paletteVersion;\n\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tif (!good) {\n\t\t\t_frame = QImage(\n\t\t\t\tQSize(size, size) * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_frame.setDevicePixelRatio(ratio);\n\t\t}\n\t\t_frame.fill(Qt::transparent);\n\n\t\tauto p = Painter(&_frame);\n\t\tauto &view = _subscribed->view;\n\t\t_peer->paintUserpic(p, view, {\n\t\t\t.position = QPoint(),\n\t\t\t.size = size,\n\t\t\t.shape = (_forceRound\n\t\t\t\t? Ui::PeerUserpicShape::Circle\n\t\t\t\t: Ui::PeerUserpicShape::Auto),\n\t\t});\n\t}\n\treturn _frame;\n}\n\nbool PeerUserpic::waitingUserpicLoad() const {\n\treturn _peer->hasUserpic() && _peer->useEmptyUserpic(_subscribed->view);\n}\n\nvoid PeerUserpic::subscribeToUpdates(Fn<void()> callback) {\n\tif (!callback) {\n\t\t_subscribed = nullptr;\n\t\treturn;\n\t}\n\tconst auto old = std::exchange(\n\t\t_subscribed,\n\t\tstd::make_unique<Subscribed>(std::move(callback)));\n\n\t_peer->session().changes().peerUpdates(\n\t\t_peer,\n\t\tData::PeerUpdate::Flag::Photo\n\t) | rpl::on_next([=] {\n\t\t_subscribed->callback();\n\t\tprocessNewPhoto();\n\t}, _subscribed->photoLifetime);\n\n\tprocessNewPhoto();\n}\n\nvoid PeerUserpic::processNewPhoto() {\n\tExpects(_subscribed != nullptr);\n\n\tif (!waitingUserpicLoad()) {\n\t\t_subscribed->downloadLifetime.destroy();\n\t\treturn;\n\t}\n\t_peer->session().downloaderTaskFinished(\n\t) | rpl::filter([=] {\n\t\treturn !waitingUserpicLoad();\n\t}) | rpl::on_next([=] {\n\t\t_subscribed->callback();\n\t\t_subscribed->downloadLifetime.destroy();\n\t}, _subscribed->downloadLifetime);\n}\n\nMediaThumbnail::MediaThumbnail(\n\t\tData::FileOrigin origin,\n\t\tbool forceRound,\n\t\tbool centerCrop)\n: _origin(origin)\n, _forceRound(forceRound)\n, _centerCrop(centerCrop) {\n}\n\nQImage MediaThumbnail::image(int size) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (_prepared.width() != size * ratio) {\n\t\tif (_full.isNull()) {\n\t\t\t_prepared = QImage(\n\t\t\t\tQSize(size, size) * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_prepared.fill(Qt::black);\n\t\t} else {\n\t\t\tauto source = QRect();\n\t\t\tif (_centerCrop) {\n\t\t\t\tconst auto side = std::min(_full.width(), _full.height());\n\t\t\t\tconst auto x = (_full.width() - side) / 2;\n\t\t\t\tconst auto y = (_full.height() - side) / 2;\n\t\t\t\tsource = QRect(x, y, side, side);\n\t\t\t} else {\n\t\t\t\tconst auto width = _full.width();\n\t\t\t\tconst auto skip = std::max((_full.height() - width) / 2, 0);\n\t\t\t\tsource = QRect(0, skip, width, width);\n\t\t\t}\n\t\t\t_prepared = _full.copy(source).scaled(\n\t\t\t\tQSize(size, size) * ratio,\n\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\tQt::SmoothTransformation);\n\t\t}\n\t\tif (_forceRound) {\n\t\t\t_prepared = Images::Circle(std::move(_prepared));\n\t\t}\n\t\t_prepared.setDevicePixelRatio(ratio);\n\t}\n\treturn _prepared;\n}\n\nvoid MediaThumbnail::subscribeToUpdates(Fn<void()> callback) {\n\t_subscription.destroy();\n\tif (!callback) {\n\t\tclear();\n\t\treturn;\n\t} else if (!_full.isNull() && !_blurred) {\n\t\treturn;\n\t}\n\tconst auto thumbnail = loaded(_origin);\n\tif (const auto image = thumbnail.image) {\n\t\t_full = image->original();\n\t}\n\t_blurred = thumbnail.blurred;\n\tif (!_blurred) {\n\t\t_prepared = QImage();\n\t} else {\n\t\t_subscription = session().downloaderTaskFinished(\n\t\t) | rpl::filter([=] {\n\t\t\tconst auto thumbnail = loaded(_origin);\n\t\t\tif (!thumbnail.blurred) {\n\t\t\t\t_full = thumbnail.image->original();\n\t\t\t\t_prepared = QImage();\n\t\t\t\t_blurred = false;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t}) | rpl::take(1) | rpl::on_next(callback);\n\t}\n}\n\nData::FileOrigin MediaThumbnail::origin() const {\n\treturn _origin;\n}\n\nbool MediaThumbnail::forceRound() const {\n\treturn _forceRound;\n}\n\nbool MediaThumbnail::centerCrop() const {\n\treturn _centerCrop;\n}\n\nPhotoThumbnail::PhotoThumbnail(\n\tnot_null<PhotoData*> photo,\n\tData::FileOrigin origin,\n\tbool forceRound,\n\tbool centerCrop)\n: MediaThumbnail(origin, forceRound, centerCrop)\n, _photo(photo) {\n}\n\nstd::shared_ptr<DynamicImage> PhotoThumbnail::clone() {\n\treturn std::make_shared<PhotoThumbnail>(\n\t\t_photo,\n\t\torigin(),\n\t\tforceRound(),\n\t\tcenterCrop());\n}\n\nMain::Session &PhotoThumbnail::session() {\n\treturn _photo->session();\n}\n\nMediaThumbnail::Thumb PhotoThumbnail::loaded(Data::FileOrigin origin) {\n\tif (!_media) {\n\t\t_media = _photo->createMediaView();\n\t\t_media->wanted(Data::PhotoSize::Small, origin);\n\t}\n\tif (const auto small = _media->image(Data::PhotoSize::Small)) {\n\t\treturn { .image = small };\n\t}\n\treturn { .image = _media->thumbnailInline(), .blurred = true };\n}\n\nvoid PhotoThumbnail::clear() {\n\t_media = nullptr;\n}\n\nVideoThumbnail::VideoThumbnail(\n\tnot_null<DocumentData*> video,\n\tData::FileOrigin origin,\n\tbool forceRound,\n\tbool centerCrop)\n: MediaThumbnail(origin, forceRound, centerCrop)\n, _video(video) {\n}\n\nstd::shared_ptr<DynamicImage> VideoThumbnail::clone() {\n\treturn std::make_shared<VideoThumbnail>(\n\t\t_video,\n\t\torigin(),\n\t\tforceRound(),\n\t\tcenterCrop());\n}\n\nMain::Session &VideoThumbnail::session() {\n\treturn _video->session();\n}\n\nMediaThumbnail::Thumb VideoThumbnail::loaded(Data::FileOrigin origin) {\n\tif (!_media) {\n\t\t_media = _video->createMediaView();\n\t\t_media->thumbnailWanted(origin);\n\t}\n\tif (const auto small = _media->thumbnail()) {\n\t\treturn { .image = small };\n\t}\n\treturn { .image = _media->thumbnailInline(), .blurred = true };\n}\n\nvoid VideoThumbnail::clear() {\n\t_media = nullptr;\n}\n\nCallThumbnail::CallThumbnail() = default;\n\nstd::shared_ptr<DynamicImage> CallThumbnail::clone() {\n\treturn std::make_shared<CallThumbnail>();\n}\n\nQImage CallThumbnail::image(int size) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto full = QSize(size, size) * ratio;\n\tif (_prepared.size() != full) {\n\t\t_prepared = QImage(full, QImage::Format_ARGB32_Premultiplied);\n\t\t_prepared.fill(Qt::black);\n\t\t_prepared.setDevicePixelRatio(ratio);\n\n\t\t_prepared = Images::Circle(std::move(_prepared));\n\t}\n\treturn _prepared;\n}\n\nvoid CallThumbnail::subscribeToUpdates(Fn<void()> callback) {\n}\n\nstd::shared_ptr<DynamicImage> EmptyThumbnail::clone() {\n\treturn std::make_shared<EmptyThumbnail>();\n}\n\nQImage EmptyThumbnail::image(int size) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (_cached.width() != size * ratio) {\n\t\t_cached = QImage(\n\t\t\tQSize(size, size) * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t_cached.fill(Qt::black);\n\t\t_cached.setDevicePixelRatio(ratio);\n\t}\n\treturn _cached;\n}\n\nvoid EmptyThumbnail::subscribeToUpdates(Fn<void()> callback) {\n}\n\nstd::shared_ptr<DynamicImage> SavedMessagesUserpic::clone() {\n\treturn std::make_shared<SavedMessagesUserpic>();\n}\n\nQImage SavedMessagesUserpic::image(int size) {\n\tconst auto good = (_frame.width() == size * _frame.devicePixelRatio());\n\tconst auto paletteVersion = style::PaletteVersion();\n\tif (!good || _paletteVersion != paletteVersion) {\n\t\t_paletteVersion = paletteVersion;\n\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tif (!good) {\n\t\t\t_frame = QImage(\n\t\t\t\tQSize(size, size) * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_frame.setDevicePixelRatio(ratio);\n\t\t}\n\t\t_frame.fill(Qt::transparent);\n\n\t\tauto p = Painter(&_frame);\n\t\tUi::EmptyUserpic::PaintSavedMessages(p, 0, 0, size, size);\n\t}\n\treturn _frame;\n}\n\nvoid SavedMessagesUserpic::subscribeToUpdates(Fn<void()> callback) {\n\tif (!callback) {\n\t\t_frame = {};\n\t}\n}\n\nstd::shared_ptr<DynamicImage> RepliesUserpic::clone() {\n\treturn std::make_shared<RepliesUserpic>();\n}\n\nQImage RepliesUserpic::image(int size) {\n\tconst auto good = (_frame.width() == size * _frame.devicePixelRatio());\n\tconst auto paletteVersion = style::PaletteVersion();\n\tif (!good || _paletteVersion != paletteVersion) {\n\t\t_paletteVersion = paletteVersion;\n\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tif (!good) {\n\t\t\t_frame = QImage(\n\t\t\t\tQSize(size, size) * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_frame.setDevicePixelRatio(ratio);\n\t\t}\n\t\t_frame.fill(Qt::transparent);\n\n\t\tauto p = Painter(&_frame);\n\t\tUi::EmptyUserpic::PaintRepliesMessages(p, 0, 0, size, size);\n\t}\n\treturn _frame;\n}\n\nvoid RepliesUserpic::subscribeToUpdates(Fn<void()> callback) {\n\tif (!callback) {\n\t\t_frame = {};\n\t}\n}\n\nstd::shared_ptr<DynamicImage> HiddenAuthorUserpic::clone() {\n\treturn std::make_shared<HiddenAuthorUserpic>();\n}\n\nQImage HiddenAuthorUserpic::image(int size) {\n\tconst auto good = (_frame.width() == size * _frame.devicePixelRatio());\n\tconst auto paletteVersion = style::PaletteVersion();\n\tif (!good || _paletteVersion != paletteVersion) {\n\t\t_paletteVersion = paletteVersion;\n\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tif (!good) {\n\t\t\t_frame = QImage(\n\t\t\t\tQSize(size, size) * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_frame.setDevicePixelRatio(ratio);\n\t\t}\n\t\t_frame.fill(Qt::transparent);\n\n\t\tauto p = Painter(&_frame);\n\t\tUi::EmptyUserpic::PaintHiddenAuthor(p, 0, 0, size, size);\n\t}\n\treturn _frame;\n}\n\nvoid HiddenAuthorUserpic::subscribeToUpdates(Fn<void()> callback) {\n\tif (!callback) {\n\t\t_frame = {};\n\t}\n}\n\nIconThumbnail::IconThumbnail(const style::icon &icon) : _icon(icon) {\n}\n\nstd::shared_ptr<DynamicImage> IconThumbnail::clone() {\n\treturn std::make_shared<IconThumbnail>(_icon);\n}\n\nQImage IconThumbnail::image(int size) {\n\tconst auto good = (_frame.width() == size * _frame.devicePixelRatio());\n\tconst auto paletteVersion = style::PaletteVersion();\n\tif (!good || _paletteVersion != paletteVersion) {\n\t\t_paletteVersion = paletteVersion;\n\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tif (!good) {\n\t\t\t_frame = QImage(\n\t\t\t\tQSize(size, size) * ratio,\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_frame.setDevicePixelRatio(ratio);\n\t\t}\n\t\t_frame.fill(Qt::transparent);\n\n\t\tauto p = Painter(&_frame);\n\t\t_icon.paintInCenter(p, QRect(0, 0, size, size));\n\t}\n\treturn _frame;\n}\n\nvoid IconThumbnail::subscribeToUpdates(Fn<void()> callback) {\n\tif (!callback) {\n\t\t_frame = {};\n\t}\n}\n\nEmojiThumbnail::EmojiThumbnail(\n\tnot_null<Data::Session*> owner,\n\tconst QString &data,\n\tint loopLimit,\n\tFn<bool()> paused,\n\tFn<QColor()> textColor)\n: _owner(owner)\n, _data(data)\n, _loopLimit(loopLimit)\n, _paused(std::move(paused))\n, _textColor(std::move(textColor)) {\n}\n\nvoid EmojiThumbnail::subscribeToUpdates(Fn<void()> callback) {\n\tif (!callback) {\n\t\t_emoji = nullptr;\n\t\treturn;\n\t}\n\tauto emoji = _owner->customEmojiManager().create(\n\t\t_data,\n\t\tstd::move(callback),\n\t\tData::CustomEmojiSizeTag::Large);\n\t_emoji = (_loopLimit > 0)\n\t\t? std::make_unique<Ui::Text::LimitedLoopsEmoji>(\n\t\t\tstd::move(emoji),\n\t\t\t_loopLimit)\n\t\t: std::move(emoji);\n\n\tEnsures(_emoji != nullptr);\n}\n\nstd::shared_ptr<DynamicImage> EmojiThumbnail::clone() {\n\treturn std::make_shared<EmojiThumbnail>(\n\t\t_owner,\n\t\t_data,\n\t\t_loopLimit,\n\t\t_paused,\n\t\t_textColor);\n}\n\nQImage EmojiThumbnail::image(int size) {\n\tExpects(_emoji != nullptr);\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto good = (_frame.width() == size * _frame.devicePixelRatio());\n\tif (!good) {\n\t\t_frame = QImage(\n\t\t\tQSize(size, size) * ratio,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t_frame.setDevicePixelRatio(ratio);\n\t}\n\t_frame.fill(Qt::transparent);\n\n\tconst auto esize = Text::AdjustCustomEmojiSize(\n\t\tEmoji::GetSizeLarge() / style::DevicePixelRatio());\n\tconst auto eskip = (size - esize) / 2;\n\n\tauto p = Painter(&_frame);\n\t_emoji->paint(p, {\n\t\t.textColor = _textColor ? _textColor() : st::windowBoldFg->c,\n\t\t.now = crl::now(),\n\t\t.position = QPoint(eskip, eskip),\n\t\t.paused = _paused && _paused(),\n\t});\n\tp.end();\n\n\treturn _frame;\n}\n\nGeoThumbnail::GeoThumbnail(\n\tnot_null<Data::CloudImage*> data,\n\tnot_null<Main::Session*> session,\n\tData::FileOrigin origin,\n\tbool drawPin)\n: _data(data)\n, _session(session)\n, _origin(origin)\n, _drawPin(drawPin) {\n}\n\nstd::shared_ptr<DynamicImage> GeoThumbnail::clone() {\n\treturn std::make_shared<GeoThumbnail>(\n\t\t_data,\n\t\t_session,\n\t\t_origin,\n\t\t_drawPin);\n}\n\nQImage GeoThumbnail::image(int size) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto full = QSize(size, size) * ratio;\n\tconst auto paletteVersion = style::PaletteVersion();\n\tif (_prepared.size() == full && _paletteVersion == paletteVersion) {\n\t\treturn _prepared;\n\t}\n\t_paletteVersion = paletteVersion;\n\n\tconst auto loaded = _view ? *_view : QImage();\n\tif (loaded.isNull()) {\n\t\t_prepared = QImage(full, QImage::Format_ARGB32_Premultiplied);\n\t\t_prepared.fill(Qt::black);\n\t} else {\n\t\tconst auto w = loaded.width();\n\t\tconst auto h = loaded.height();\n\t\tconst auto side = std::min(w, h);\n\t\tconst auto x = (w - side) / 2;\n\t\tconst auto y = (h - side) / 2;\n\t\t_prepared = loaded.copy(x, y, side, side).scaled(\n\t\t\tfull,\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t}\n\t_prepared = Images::Round(\n\t\tstd::move(_prepared),\n\t\tImageRoundRadius::Small);\n\t_prepared.setDevicePixelRatio(ratio);\n\tif (_drawPin && !loaded.isNull()) {\n\t\tauto p = Painter(&_prepared);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto pinScale = std::min(\n\t\t\t1.0,\n\t\t\tsize / (st::historyMapPoint.height() * 2.5));\n\t\tconst auto center = QPointF(size / 2.0, size / 2.0);\n\t\tp.translate(center);\n\t\tp.scale(pinScale, pinScale);\n\t\tp.translate(-center);\n\t\tconst auto paintMarker = [&](const style::icon &icon) {\n\t\t\ticon.paint(\n\t\t\t\tp,\n\t\t\t\t(size - icon.width()) / 2,\n\t\t\t\t(size / 2) - icon.height(),\n\t\t\t\tsize);\n\t\t};\n\t\tpaintMarker(st::historyMapPoint);\n\t\tpaintMarker(st::historyMapPointInner);\n\t}\n\treturn _prepared;\n}\n\nvoid GeoThumbnail::subscribeToUpdates(Fn<void()> callback) {\n\t_subscription.destroy();\n\tif (!callback) {\n\t\t_view = nullptr;\n\t\t_prepared = QImage();\n\t\treturn;\n\t}\n\t_view = _data->createView();\n\t_data->load(_session, _origin);\n\tif (!_view->isNull()) {\n\t\treturn;\n\t}\n\t_subscription = _session->downloaderTaskFinished(\n\t) | rpl::filter([=] {\n\t\treturn !_view->isNull();\n\t}) | rpl::take(1) | rpl::on_next([=] {\n\t\t_prepared = QImage();\n\t\tcallback();\n\t});\n}\n\nDocumentFilePreviewThumbnail::DocumentFilePreviewThumbnail(\n\tnot_null<DocumentData*> document,\n\tData::FileOrigin origin)\n: _document(document)\n, _origin(origin)\n, _generic(::Layout::DocumentGenericPreview::Create(document)) {\n}\n\nstd::shared_ptr<DynamicImage> DocumentFilePreviewThumbnail::clone() {\n\treturn std::make_shared<DocumentFilePreviewThumbnail>(\n\t\t_document,\n\t\t_origin);\n}\n\nQImage DocumentFilePreviewThumbnail::image(int size) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto paletteVersion = style::PaletteVersion();\n\tconst auto good = (_prepared.width() == size * ratio);\n\tif (good && _paletteVersion == paletteVersion) {\n\t\treturn _prepared;\n\t}\n\t_paletteVersion = paletteVersion;\n\n\tif (_media) {\n\t\tconst auto thumbnail = _media->thumbnail();\n\t\tconst auto blurred = _media->thumbnailInline();\n\t\tif (thumbnail || blurred) {\n\t\t\t_prepared = prepareThumbImage(size);\n\t\t\tif (!_prepared.isNull()) {\n\t\t\t\treturn _prepared;\n\t\t\t}\n\t\t}\n\t}\n\t_prepared = prepareGenericImage(size);\n\treturn _prepared;\n}\n\nQImage DocumentFilePreviewThumbnail::prepareThumbImage(int size) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto thumbnail = _media->thumbnail();\n\tconst auto blurred = _media->thumbnailInline();\n\tconst auto image = thumbnail ? thumbnail : blurred;\n\tif (!image) {\n\t\treturn {};\n\t}\n\tauto full = image->original();\n\tconst auto side = std::min(full.width(), full.height());\n\tconst auto x = (full.width() - side) / 2;\n\tconst auto y = (full.height() - side) / 2;\n\tauto result = full.copy(x, y, side, side).scaled(\n\t\tQSize(size, size) * ratio,\n\t\tQt::IgnoreAspectRatio,\n\t\tQt::SmoothTransformation);\n\tresult = Images::Round(\n\t\tstd::move(result),\n\t\tImageRoundRadius::Small);\n\tresult.setDevicePixelRatio(ratio);\n\treturn result;\n}\n\nQImage DocumentFilePreviewThumbnail::prepareGenericImage(int size) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto result = QImage(\n\t\tQSize(size, size) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::transparent);\n\tresult.setDevicePixelRatio(ratio);\n\n\tconst auto radius = size / 6;\n\tconst auto foldSize = size / 4;\n\n\tauto p = QPainter(&result);\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setPen(Qt::NoPen);\n\n\t// Rounded rect with top-right corner cut for fold.\n\tauto path = QPainterPath();\n\tpath.moveTo(radius, 0);\n\tpath.lineTo(size - foldSize, 0);\n\tpath.lineTo(size, foldSize);\n\tpath.lineTo(size, size - radius);\n\tpath.arcTo(\n\t\tsize - 2 * radius,\n\t\tsize - 2 * radius,\n\t\t2 * radius,\n\t\t2 * radius,\n\t\t0,\n\t\t-90);\n\tpath.lineTo(radius, size);\n\tpath.arcTo(0, size - 2 * radius, 2 * radius, 2 * radius, 270, -90);\n\tpath.lineTo(0, radius);\n\tpath.arcTo(0, 0, 2 * radius, 2 * radius, 180, -90);\n\tpath.closeSubpath();\n\n\tp.setBrush(_generic.color);\n\tp.drawPath(path);\n\n\t// Fold triangle.\n\tauto fold = QPainterPath();\n\tfold.moveTo(size - foldSize, 0);\n\tfold.lineTo(size, foldSize);\n\tfold.lineTo(size - foldSize, foldSize);\n\tfold.closeSubpath();\n\tp.setBrush(_generic.dark);\n\tp.drawPath(fold);\n\n\tif (!_generic.ext.isEmpty()) {\n\t\t// Reference: overview uses 18px font in a 70px square.\n\t\tconst auto refSize = st::overviewFileLayout.fileThumbSize;\n\t\tconst auto fontSize = std::max(\n\t\t\tsize * st::overviewFileExtFont->f.pixelSize() / refSize,\n\t\t\t8);\n\t\tconst auto font = style::font(\n\t\t\tfontSize,\n\t\t\tst::overviewFileExtFont->flags(),\n\t\t\tst::overviewFileExtFont->family());\n\t\tconst auto padding = size * st::overviewFileExtPadding / refSize;\n\t\tconst auto maxw = size - padding * 2;\n\t\tauto ext = _generic.ext;\n\t\tauto extw = font->width(ext);\n\t\tif (extw > maxw) {\n\t\t\text = font->elided(ext, maxw, Qt::ElideMiddle);\n\t\t\textw = font->width(ext);\n\t\t}\n\t\tp.setFont(font);\n\t\tp.setPen(st::overviewFileExtFg);\n\t\tp.drawText(\n\t\t\t(size - extw) / 2,\n\t\t\t(size - font->height) / 2 + font->ascent,\n\t\t\text);\n\t}\n\tp.end();\n\treturn result;\n}\n\nvoid DocumentFilePreviewThumbnail::subscribeToUpdates(Fn<void()> callback) {\n\t_subscription.destroy();\n\tif (!callback) {\n\t\t_media = nullptr;\n\t\t_prepared = QImage();\n\t\treturn;\n\t}\n\tif (!_document->hasThumbnail()) {\n\t\treturn;\n\t}\n\tif (!_media) {\n\t\t_media = _document->createMediaView();\n\t\t_media->thumbnailWanted(_origin);\n\t}\n\tif (_media->thumbnail()) {\n\t\t_thumbLoaded = true;\n\t\treturn;\n\t}\n\tif (!_thumbLoaded) {\n\t\t_subscription = _document->session().downloaderTaskFinished(\n\t\t) | rpl::filter([=] {\n\t\t\treturn _media && _media->thumbnail();\n\t\t}) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t_thumbLoaded = true;\n\t\t\t_prepared = QImage();\n\t\t\tcallback();\n\t\t});\n\t}\n}\n\n} // namespace\n\nstd::shared_ptr<DynamicImage> MakeUserpicThumbnail(\n\t\tnot_null<PeerData*> peer,\n\t\tbool forceRound) {\n\treturn std::make_shared<PeerUserpic>(peer, forceRound);\n}\n\nstd::shared_ptr<DynamicImage> MakeSavedMessagesThumbnail() {\n\treturn std::make_shared<SavedMessagesUserpic>();\n}\n\nstd::shared_ptr<DynamicImage> MakeRepliesThumbnail() {\n\treturn std::make_shared<RepliesUserpic>();\n}\n\nstd::shared_ptr<DynamicImage> MakeHiddenAuthorThumbnail() {\n\treturn std::make_shared<HiddenAuthorUserpic>();\n}\n\nstd::shared_ptr<DynamicImage> MakeStoryThumbnail(\n\t\tnot_null<Data::Story*> story) {\n\tusing Result = std::shared_ptr<DynamicImage>;\n\tconst auto id = story->fullId();\n\treturn v::match(story->media().data, [](v::null_t) -> Result {\n\t\treturn std::make_shared<EmptyThumbnail>();\n\t}, [](const std::shared_ptr<Data::GroupCall> &call) -> Result {\n\t\treturn std::make_shared<CallThumbnail>();\n\t}, [&](not_null<PhotoData*> photo) -> Result {\n\t\treturn std::make_shared<PhotoThumbnail>(\n\t\t\tphoto,\n\t\t\tid,\n\t\t\ttrue,\n\t\t\tfalse);\n\t}, [&](not_null<DocumentData*> video) -> Result {\n\t\treturn std::make_shared<VideoThumbnail>(\n\t\t\tvideo,\n\t\t\tid,\n\t\t\ttrue,\n\t\t\tfalse);\n\t});\n}\n\nstd::shared_ptr<DynamicImage> MakeIconThumbnail(const style::icon &icon) {\n\treturn std::make_shared<IconThumbnail>(icon);\n}\n\nstd::shared_ptr<DynamicImage> MakeEmojiThumbnail(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst QString &data,\n\t\tFn<bool()> paused,\n\t\tFn<QColor()> textColor,\n\t\tint loopLimit) {\n\treturn std::make_shared<EmojiThumbnail>(\n\t\towner,\n\t\tdata,\n\t\tloopLimit,\n\t\tstd::move(paused),\n\t\tstd::move(textColor));\n}\n\nstd::shared_ptr<DynamicImage> MakePhotoThumbnail(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId fullId) {\n\treturn std::make_shared<PhotoThumbnail>(\n\t\tphoto,\n\t\tfullId,\n\t\tfalse,\n\t\tfalse);\n}\n\nstd::shared_ptr<DynamicImage> MakePhotoThumbnailCenterCrop(\n\t\tnot_null<PhotoData*> photo,\n\t\tFullMsgId fullId) {\n\treturn std::make_shared<PhotoThumbnail>(\n\t\tphoto,\n\t\tfullId,\n\t\tfalse,\n\t\ttrue);\n}\n\nstd::shared_ptr<DynamicImage> MakeDocumentThumbnail(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId fullId) {\n\treturn std::make_shared<VideoThumbnail>(\n\t\tdocument,\n\t\tfullId,\n\t\tfalse,\n\t\tfalse);\n}\n\nstd::shared_ptr<DynamicImage> MakeDocumentThumbnailCenterCrop(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId fullId) {\n\treturn std::make_shared<VideoThumbnail>(\n\t\tdocument,\n\t\tfullId,\n\t\tfalse,\n\t\ttrue);\n}\n\nstd::shared_ptr<DynamicImage> MakeDocumentFilePreviewThumbnail(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId fullId) {\n\treturn std::make_shared<DocumentFilePreviewThumbnail>(\n\t\tdocument,\n\t\tfullId);\n}\n\nstd::shared_ptr<DynamicImage> MakeGeoThumbnail(\n\t\tnot_null<Data::CloudImage*> data,\n\t\tnot_null<Main::Session*> session,\n\t\tData::FileOrigin origin) {\n\treturn std::make_shared<GeoThumbnail>(\n\t\tdata,\n\t\tsession,\n\t\tstd::move(origin),\n\t\tfalse);\n}\n\nstd::shared_ptr<DynamicImage> MakeGeoThumbnailWithPin(\n\t\tnot_null<Data::CloudImage*> data,\n\t\tnot_null<Main::Session*> session,\n\t\tData::FileOrigin origin) {\n\treturn std::make_shared<GeoThumbnail>(\n\t\tdata,\n\t\tsession,\n\t\tstd::move(origin),\n\t\ttrue);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/dynamic_thumbnails.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass DocumentData;\nclass PeerData;\nclass PhotoData;\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Data {\nclass CloudImage;\nclass Story;\nclass Session;\nstruct FileOrigin;\n} // namespace Data\n\nnamespace Ui {\n\nclass DynamicImage;\n\n[[nodiscard]] std::shared_ptr<DynamicImage> MakeUserpicThumbnail(\n\tnot_null<PeerData*> peer,\n\tbool forceRound = false);\n[[nodiscard]] std::shared_ptr<DynamicImage> MakeSavedMessagesThumbnail();\n[[nodiscard]] std::shared_ptr<DynamicImage> MakeRepliesThumbnail();\n[[nodiscard]] std::shared_ptr<DynamicImage> MakeHiddenAuthorThumbnail();\n[[nodiscard]] std::shared_ptr<DynamicImage> MakeStoryThumbnail(\n\tnot_null<Data::Story*> story);\n[[nodiscard]] std::shared_ptr<DynamicImage> MakeIconThumbnail(\n\tconst style::icon &icon);\n[[nodiscard]] std::shared_ptr<DynamicImage> MakeEmojiThumbnail(\n\tnot_null<Data::Session*> owner,\n\tconst QString &data,\n\tFn<bool()> paused = nullptr,\n\tFn<QColor()> textColor = nullptr,\n\tint loopLimit = 0);\n[[nodiscard]] std::shared_ptr<DynamicImage> MakePhotoThumbnail(\n\tnot_null<PhotoData*> photo,\n\tFullMsgId fullId);\n[[nodiscard]] std::shared_ptr<DynamicImage> MakePhotoThumbnailCenterCrop(\n\tnot_null<PhotoData*> photo,\n\tFullMsgId fullId);\n[[nodiscard]] std::shared_ptr<DynamicImage> MakeDocumentThumbnail(\n\tnot_null<DocumentData*> document,\n\tFullMsgId fullId);\n[[nodiscard]] std::shared_ptr<DynamicImage> MakeDocumentThumbnailCenterCrop(\n\tnot_null<DocumentData*> document,\n\tFullMsgId fullId);\n[[nodiscard]] std::shared_ptr<DynamicImage> MakeDocumentFilePreviewThumbnail(\n\tnot_null<DocumentData*> document,\n\tFullMsgId fullId);\n[[nodiscard]] std::shared_ptr<DynamicImage> MakeGeoThumbnail(\n\tnot_null<Data::CloudImage*> data,\n\tnot_null<Main::Session*> session,\n\tData::FileOrigin origin);\n[[nodiscard]] std::shared_ptr<DynamicImage> MakeGeoThumbnailWithPin(\n\tnot_null<Data::CloudImage*> data,\n\tnot_null<Main::Session*> session,\n\tData::FileOrigin origin);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/credits.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\nusing \"boxes/boxes.style\";\nusing \"ui/widgets/widgets.style\";\nusing \"ui/effects/premium.style\";\nusing \"settings/settings.style\";\n\ncreditsSettingsBigBalance: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(24px semibold);\n\t}\n}\ncreditsSettingsBigBalanceSkip: 4px;\ncreditsSettingsBigBalanceButton: RoundButton(defaultActiveButton) {\n\twidth: 240px;\n\theight: 40px;\n\ttextTop: 11px;\n\tstyle: semiboldTextStyle;\n}\ncreditsSettingsBigBalanceButtonGift: RoundButton(defaultLightButton) {\n\theight: 42px;\n\ttextTop: 12px;\n\tstyle: defaultTextStyle;\n}\n\ncreditsPremiumCover: PremiumCover(defaultPremiumCover) {\n\tstarTopSkip: 39px;\n\ttitleFont: font(15px semibold);\n\tabout: FlatLabel(userPremiumCoverAbout) {\n\t\ttextFg: boxTitleFg;\n\t}\n\taboutMaxWidth: 236px;\n\tadditionalShadowForDarkThemes: false;\n}\ncreditsLowBalancePremiumCover: PremiumCover(creditsPremiumCover) {\n\tstarSize: size(64px, 62px);\n\tstarTopSkip: 30px;\n\tabout: FlatLabel(userPremiumCoverAbout) {\n\t\ttextFg: boxTitleFg;\n\t\tminWidth: 200px;\n\t}\n}\ncreditsLowBalancePremiumCoverHeight: 162px;\ncreditsTopupButton: SettingsButton(settingsButton) {\n\tstyle: semiboldTextStyle;\n}\ncreditsTopupPrice: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n}\n\ncreditsHistoryRightSkip: 10px;\ncreditsBalanceStarHeight: 20px;\n\ncreditsBoxAbout: FlatLabel(defaultFlatLabel) {\n\tminWidth: 256px;\n\talign: align(top);\n}\ncreditsBoxAboutTitle: FlatLabel(settingsPremiumUserTitle) {\n\tminWidth: 256px;\n}\ncreditsBoxAboutDivider: FlatLabel(boxDividerLabel) {\n\talign: align(top);\n}\ncreditsBoxButtonLabel: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n}\ncreditsReleasedByLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n}\n\nstarIconEmoji: IconEmoji {\n\ticon: icon{{ \"payments/premium_emoji\", creditsBg1 }};\n\tpadding: margins(4px, 1px, 4px, 0px);\n}\nstarIconEmojiColored: IconEmoji(starIconEmoji) {\n\tuseIconColor: true;\n}\nstarIconEmojiSmall: IconEmoji {\n\ticon: icon{{ \"chat/mini_stars\", creditsBg1 }};\n\tpadding: margins(0px, 4px, 0px, 0px);\n}\nstarIconEmojiLarge: IconEmoji {\n\ticon: icon {{ \"settings/premium/star\", settingsIconFg }};\n\tpadding: margins(0px, -2px, 0px, 0px);\n}\nstarIconEmojiInline: IconEmoji(starIconEmojiSmall) {\n\tpadding: margins(0px, 3px, 0px, 0px);\n}\nbuttonStarIconEmoji: IconEmoji(starIconEmoji) {\n\tpadding: margins(4px, 2px, 4px, 0px);\n}\n\ntonIconEmoji: IconEmoji {\n\ticon: icon{{ \"chat/mini_ton_bold\", currencyFg }};\n\tpadding: margins(1px, 2px, 1px, 0px);\n}\ntonIconEmojiLarge: IconEmoji {\n\ticon: icon{{ \"payments/ton_emoji-18x18\", currencyFg }};\n\tpadding: margins(0px, 1px, 0px, 0px);\n}\ntonIconEmojiInSmall: IconEmoji(tonIconEmoji) {\n\tpadding: margins(0px, 2px, 0px, 0px);\n}\nbuttonTonIconEmoji: IconEmoji(tonIconEmoji) {\n\tpadding: margins(1px, 3px, 1px, 0px);\n}\n\ncreditsHistoryEntryTypeAds: icon {{ \"folders/folders_channels\", premiumButtonFg }};\ncreditsHistorySearchPostsIcon: icon {{ \"box_search\", historyPeerUserpicFg }};\n\ncreditsHistoryEntryGiftStickerSkip: -20px;\ncreditsHistoryEntryGiftStickerSize: 150px;\ncreditsHistoryEntryGiftStickerSpace: 105px;\n\ncreditsHistoryEntryStarGiftSkip: 10px;\ncreditsHistoryEntryStarGiftSize: 120px;\ncreditsHistoryEntryStarGiftSpace: 125px;\n\ncreditsGiftBox: Box(defaultBox) {\n\tshadowIgnoreTopSkip: true;\n}\n\ngiftBox: Box(defaultBox) {\n\tbuttonPadding: margins(22px, 11px, 22px, 12px);\n\tbuttonHeight: 42px;\n\tbuttonWide: true;\n\tbutton: RoundButton(defaultActiveButton) {\n\t\theight: 42px;\n\t\ttextTop: 12px;\n\t\tstyle: semiboldTextStyle;\n\t}\n}\ngiftLimitedBox: Box(giftBox) {\n\tbuttonPadding: margins(22px, 48px, 22px, 12px);\n}\ngiftCraftBox: Box(giftBox) {\n\tbuttonPadding: margins(0px, 0px, 0px, 0px);\n\tbuttonHeight: 0px;\n}\ngiftLimitedPadding: margins(8px, 4px, 8px, 4px);\ngiftBoxSubtitle: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: boxTitleFont;\n\t}\n\ttextFg: windowBoldFg;\n\talign: align(top);\n\tminWidth: 200px;\n}\ngiftBoxSubtitleMargin: margins(22px, 4px, 22px, 4px);\ngiftBoxAbout: FlatLabel(defaultFlatLabel) {\n\talign: align(top);\n\tminWidth: 200px;\n}\ngiftBoxAboutMargin: margins(20px, 4px, 20px, 4px);\ngiftBoxTabsMargin: margins(20px, 4px, 20px, 4px);\ngiftBoxTabPadding: margins(8px, 2px, 8px, 2px);\ngiftBoxTabSkip: 2px;\ngiftBoxTabStyle: semiboldTextStyle;\ngiftBoxTabFg: windowSubTextFg;\ngiftBoxTabFgActive: windowBoldFg;\ngiftBoxTabBgActive: windowBgRipple;\ngiftBoxPillTabs: PillTabs(defaultPillTabs) {\n\tborderWidth: 6px;\n\tfg: windowSubTextFg;\n\tfgActive: windowBoldFg;\n\tbgActive: windowBgRipple;\n}\ngiftBoxResaleTabsMargin: margins(11px, 10px, 11px, 8px);\ngiftBoxResaleTabSkip: 8px;\ngiftBoxResaleTabsDropdown: IconEmoji {\n\ticon: icon{{ \"intro_country_dropdown\", lightButtonFg }};\n\tpadding: margins(4px, 6px, 0px, 0px);\n}\ngiftBoxResaleMiniNumber: IconEmoji {\n\ticon: icon{{ \"settings/mini_gift_order_number-16x16\", lightButtonFg }};\n\tpadding: margins(0px, 2px, 4px, 0px);\n}\ngiftBoxResaleMiniPrice: IconEmoji {\n\ticon: icon{{ \"settings/mini_gift_order_price-16x16\", lightButtonFg }};\n\tpadding: margins(0px, 2px, 4px, 0px);\n}\ngiftBoxResaleMiniDate: IconEmoji {\n\ticon: icon{{ \"settings/mini_gift_order_date-16x16\", lightButtonFg }};\n\tpadding: margins(0px, 2px, 4px, 0px);\n}\ngiftBoxResaleFilter: PopupMenu(popupMenuWithIcons) {\n\tmenu: Menu(menuWithIcons) {\n\t\titemPadding: margins(54px, 8px, 48px, 8px);\n\t\titemRightSkip: 12px;\n\t}\n\tmaxHeight: 320px;\n}\ngiftBoxResaleColorSize: 18px;\ngiftBoxResaleColorTop: 1px;\ngiftBoxPadding: margins(11px, 4px, 11px, 24px);\ngiftBoxGiftSkip: point(10px, 8px);\ngiftBoxGiftHeight: 164px;\ngiftBoxGiftSmall: 108px;\ngiftBoxGiftTiny: 72px;\ngiftBoxGiftRadius: 12px;\ngiftBoxGiftBadgeFont: font(10px semibold);\ngiftBoxByStarsStyle: TextStyle(defaultTextStyle) {\n\tfont: font(10px);\n}\ngiftBoxByStarsSkip: 2px;\ngiftBoxByStarsStarTop: 3px;\ngiftBoxPremiumIconSize: 64px;\ngiftBoxPremiumIconTop: 10px;\ngiftBoxPremiumTextTop: 84px;\ngiftBoxPremiumTextTopByStars: 78px;\ngiftBoxButtonBottomSmall: 4px;\ngiftBoxButtonBottom: 12px;\ngiftBoxButtonBottomByStars: 18px;\ngiftBoxButtonPadding: margins(8px, 4px, 8px, 4px);\ngiftBoxPreviewStickerPadding: margins(10px, 12px, 10px, 16px);\ngiftBoxPreviewTitlePadding: margins(12px, 4px, 12px, 4px);\ngiftBoxReleasedByMargin: margins(6px, 1px, 6px, 2px);\ngiftBoxPreviewTextPadding: margins(12px, 4px, 12px, 4px);\ngiftBoxButtonMargin: margins(12px, 8px, 12px, 12px);\ngiftBoxStickerTop: 0px;\ngiftBoxStickerStarTop: 24px;\ngiftBoxStickerUniqueTop: 32px;\ngiftBoxSmallStickerTop: 16px;\ngiftBoxStickerTopByStars: -4px;\ngiftBoxStickerSize: size(80px, 80px);\ngiftBoxStickerTiny: size(48px, 48px);\ngiftBoxSelectSkip: 5px;\ngiftBoxUserpicSize: 24px;\ngiftBoxUserpicSkip: 2px;\ngiftBoxTextField: InputField(defaultInputField) {\n\ttextBg: transparent;\n\ttextMargins: margins(2px, 0px, 32px, 0px);\n\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(2px, 0px, 2px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n\n\tborder: 0px;\n\tborderActive: 0px;\n\n\theightMin: 20px;\n\theightMax: 48px;\n\n\tstyle: defaultTextStyle;\n}\ngiftBoxTextPadding: margins(20px, 15px, 20px, 11px);\ngiftBoxHiddenMark: icon{{ \"chat/mini_gift_hidden\", premiumButtonFg, margins(4px, 4px, 4px, 4px) }};\ngiftListAbout: FlatLabel(defaultFlatLabel) {\n\tminWidth: 240px;\n\ttextFg: windowSubTextFg;\n\talign: align(top);\n\tstyle: boxLabelStyle;\n}\ngiftListAboutMargin: margins(12px, 24px, 12px, 24px);\ngiftListAboutToggle: FlatLabel(giftListAbout) {\n\tstyle: semiboldTextStyle;\n}\ngiftListAboutToggleSkip: 4px;\ngiftBoxEmojiToggleTop: 7px;\ngiftBoxLimitTop: 28px;\ngiftBoxLock: IconEmoji {\n\ticon: icon {{ \"emoji/premium_lock\", placeholderFgActive }};\n\tpadding: margins(-2px, 1px, 0px, 0px);\n}\ngiftBoxPinIcon: icon {{ \"dialogs/dialogs_pinned\", premiumButtonFg }};\ngiftBoxLockIcon: icon{{ \"limits/mini_gift_lock-18x18\", attentionButtonFg }};\ngiftBoxLockIconPosition: point(4px, 4px);\n\ngiftBoxGiftEmptyLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 156px;\n\talign: align(top);\n}\n\ncreditsHistoryEntriesList: PeerList(defaultPeerList) {\n\tpadding: margins(0px, 7px, 0px, 7px);\n\titem: PeerListItem(defaultPeerListItem) {\n\t\theight: 72px;\n\t\tphotoPosition: point(18px, 7px);\n\t\tnamePosition: point(70px, 6px);\n\t\tstatusPosition: point(70px, 46px);\n\t\tphotoSize: 42px;\n\t}\n}\n\nsubscriptionCreditsBadgePadding: margins(10px, 1px, 8px, 3px);\n\nuniqueGiftModelTop: 20px;\nuniqueGiftPretitle: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n\tmargin: margins(12px, 4px, 12px, 4px);\n}\nuniqueGiftTitle: FlatLabel(boxTitle) {\n\talign: align(top);\n}\nuniqueGiftNumber: FlatLabel(uniqueGiftTitle) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: semiboldFont;\n\t}\n}\nuniqueGiftResalePrice: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n}\nuniqueGiftResalePadding: margins(4px, 4px, 8px, 4px);\nuniqueGiftResaleMargin: margins(10px, 10px, 10px, 10px);\nuniqueGiftTitleTop: 140px;\nuniqueGiftSubtitle: FlatLabel(defaultFlatLabel) {\n\tminWidth: 64px;\n\talign: align(top);\n}\nuniqueGiftReleasedBy: FlatLabel(uniqueGiftSubtitle) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(10px);\n\t}\n}\nuniqueGiftOnSale: FlatLabel(uniqueGiftSubtitle) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(10px semibold);\n\t}\n}\nuniqueGiftSubtitleTop: 170px;\nuniqueGiftUserpicTop: 48px;\nuniqueGiftUserpicSize: 80px;\nuniqueGiftBottom: 20px;\nuniqueCloseButton: IconButton(boxTitleClose) {\n\ticon: icon {{ \"box_button_close\", videoPlayIconFg }};\n\ticonOver: icon {{ \"box_button_close\", videoPlayIconFg }};\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: shadowFg;\n\t}\n}\nuniqueMenuButton: IconButton(uniqueCloseButton) {\n\twidth: 40px;\n\trippleAreaPosition: point(0px, 4px);\n\n\ticon: icon {{ \"title_menu_dots\", videoPlayIconFg }};\n\ticonOver: icon {{ \"title_menu_dots\", videoPlayIconFg }};\n}\nuniqueCraftButton: IconButton(uniqueMenuButton) {\n\ticon: icon{{ \"chat/filled_forge-24x24\", videoPlayIconFg }};\n\ticonOver: icon{{ \"chat/filled_forge-24x24\", videoPlayIconFg }};\n}\nuniqueGiftSenderBadge: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n\tpalette: TextPalette(defaultTextPalette) {\n\t\tlinkFg: radialFg;\n\t\tmonoFg: radialFg;\n\t}\n\tmargin: margins(12px, 3px, 12px, 4px);\n\tminWidth: 128px;\n\talign: align(top);\n\ttextFg: radialFg;\n}\n\nuniqueAttributeTop: 10px;\nuniqueAttributeSkip: 8px;\nuniqueAttributePadding: margins(6px, 8px, 6px, 8px);\nuniqueAttributeName: TextStyle(semiboldTextStyle) {\n\tfont: font(12px semibold);\n}\nuniqueAttributeType: TextStyle(defaultTextStyle) {\n\tfont: font(12px);\n}\nuniqueAttributePercent: uniqueAttributeType;\nuniqueAttributePercentPadding: margins(4px, 0px, 4px, 0px);\nuniqueAttributeModel: RoundButton(defaultLightButton) {\n\ttextFg: windowBoldFg;\n\ttextFgOver: windowBoldFg;\n\ttextBg: windowBgOver;\n\ttextBgOver: windowBgOver;\n\tripple: defaultRippleAnimation;\n\tradius: 8px;\n\n\twidth: 107px;\n\theight: 34px;\n\ttextTop: 7px;\n\tstyle: defaultBoxButtonTextStyle;\n\ticon: icon {{ \"menu/unique\", windowBoldFg }};\n\ticonOver: icon {{ \"menu/unique\", windowBoldFg }};\n\ticonPosition: point(-1px, 4px);\n}\nuniqueAttributeModelActive: icon{{ \"menu/unique\", activeButtonFg }};\nuniqueAttributeBackdrop: RoundButton(uniqueAttributeModel) {\n\ticon: icon {{ \"menu/palette\", windowBoldFg }};\n\ticonOver: icon {{ \"menu/palette\", windowBoldFg }};\n}\nuniqueAttributeBackdropActive: icon{{ \"menu/palette\", activeButtonFg }};\nuniqueAttributeSymbol: RoundButton(uniqueAttributeModel) {\n\ticon: icon {{ \"menu/all_media\", windowBoldFg }};\n\ticonOver: icon {{ \"menu/all_media\", windowBoldFg }};\n}\nuniqueAttributeSymbolActive: icon{{ \"menu/all_media\", activeButtonFg }};\nuniqueAttributesBox: Box(giftBox) {\n\tbuttonWide: false;\n\tbuttonHeight: 34px;\n\tbuttonPadding: margins(10px, 10px, 11px, 10px);\n}\nuniqueAttributeStickerSize: size(64px, 64px);\nupgradeGiftBox: Box(giftBox) {\n\tbuttonPadding: margins(22px, 3px, 22px, 22px);\n}\nupgradeGiftWithPricesBox: Box(giftBox) {\n\tbuttonPadding: margins(22px, 3px, 22px, 46px);\n}\nupgradeGiftSubtext: FlatLabel(boxDividerLabel) {\n\tminWidth: 200px;\n}\ndarkUpgradeGiftTitle: FlatLabel(uniqueGiftTitle) {\n\ttextFg: groupCallMembersFg;\n}\ndarkUpgradeGiftSubtitle: FlatLabel(uniqueGiftSubtitle) {\n\ttextFg: groupCallMembersFg;\n}\ndarkUpgradeGiftBox: Box(upgradeGiftBox) {\n\tbg: groupCallMembersBg;\n\ttitle: darkUpgradeGiftTitle;\n\ttitleAdditionalFg: groupCallMemberNotJoinedStatus;\n}\ndarkUpgradeGiftRadiant: icon{{ \"menu/unique\", groupCallMembersFg }};\ndarkUpgradeGiftProfile: icon{{ \"settings/premium/features/feature_profile_cover\", groupCallMembersFg }};\ndarkUpgradeGiftProof: icon{{ \"menu/factcheck\", groupCallMembersFg }};\ndarkUpgradeGiftInfoTitle: FlatLabel(defaultFlatLabel) {\n\ttextFg: groupCallMembersFg;\n}\ndarkUpgradeGiftInfoAbout: FlatLabel(upgradeGiftSubtext) {\n\ttextFg: groupCallMemberNotJoinedStatus;\n}\n\ngiftTooManyPinnedBox: Box(giftBox) {\n\tbuttonPadding: margins(11px, 11px, 11px, 11px);\n}\ngiftTooManyPinnedChoose: FlatLabel(giftBoxAbout) {\n\ttextFg: windowSubTextFg;\n}\n\ncreditsHistoryTabsSlider: SettingsSlider(defaultTabsSlider) {\n\theight: 39px;\n\tlabelTop: 7px;\n\tbarTop: 36px;\n\tbarSkip: 0px;\n\trippleBottomSkip: 0px;\n}\ncreditsHistoryTabsSliderPadding: margins(14px, 0px, 24px, 0px);\ncreditsHistoryRowDescriptionSkip: 20px;\ncreditsHistoryRowRightTop: 16px;\ncreditsHistoryRowRightMinorTop: 18px;\ncreditsHistoryRowRightStyle: TextStyle(defaultTextStyle) {\n\tfont: font(fsize);\n}\nresaleButtonTitle: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n\ttextFg: activeButtonFg;\n\tmaxHeight: 20px;\n}\nresaleButtonSubtitle: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px semibold);\n\t}\n\ttextFg: activeButtonFg;\n\tmaxHeight: 20px;\n}\nresalePriceTableLink: FlatLabel(defaultFlatLabel) {\n\tminWidth: 200px;\n\talign: align(top);\n}\nresalePriceAbout: FlatLabel(boxLabel) {\n\ttextFg: windowSubTextFg;\n\talign: align(top);\n}\n\nresaleConfirmTonOnly: FlatLabel(boxLabel) {\n\ttextFg: windowSubTextFg;\n}\nresaleConfirmTonOnlyMargin: margins(0px, 12px, 0px, 12px);\n\nofferValuePadding: margins(0px, 8px, 0px, 0px);\nofferValueGood: FlatLabel(boxLabel) {\n\tminWidth: 120px;\n\ttextFg: windowSubTextFg;\n\talign: align(top);\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px);\n\t}\n}\nofferValueBad: FlatLabel(offerValueGood) {\n\ttextFg: attentionButtonFg;\n}\n\nuniqueGiftValuePrice: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(20px semibold);\n\t}\n\tmargin: margins(12px, 6px, 12px, 6px);\n\ttextFg: windowFgActive;\n}\nuniqueGiftValueAbout: FlatLabel(defaultFlatLabel) {\n\tminWidth: 128px;\n\talign: align(top);\n}\nuniqueGiftValueAvailableLink: FlatLabel(boxLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px);\n\t}\n}\nuniqueGiftValueAvailableMargin: margins(0px, 4px, 0px, 4px);\n\nvideoStreamTopupRipple: RippleAnimation(defaultRippleAnimation) {\n\tcolor: groupCallMembersBgRipple;\n}\nvideoStreamTopupButton: SettingsButton(creditsTopupButton) {\n\ttextFg: groupCallMemberNotJoinedStatus;\n\ttextFgOver: groupCallMemberNotJoinedStatus;\n\ttextBg: groupCallMembersBg;\n\ttextBgOver: groupCallMembersBgOver;\n\tripple: videoStreamTopupRipple;\n}\nvideoStreamTopupPrice: FlatLabel(creditsTopupPrice) {\n\ttextFg: groupCallMemberNotJoinedStatus;\n}\nvideoStreamShowMoreButton: SettingsButton(defaultSettingsButton) {\n\ttextFg: groupCallActiveFg;\n\ttextFgOver: groupCallActiveFg;\n\ttextBg: groupCallMembersBg;\n\ttextBgOver: groupCallMembersBgOver;\n\n\tpadding: margins(70px, 10px, 22px, 8px);\n\n\tripple: videoStreamTopupRipple;\n}\nvideoStreamStarsCover: PremiumCover(creditsLowBalancePremiumCover) {\n\tbg: groupCallMembersBg;\n\ttitleFg: groupCallMembersFg;\n\tabout: FlatLabel(defaultFlatLabel) {\n\t\tstyle: premiumAboutTextStyle;\n\t\tpalette: TextPalette(defaultTextPalette) {\n\t\t\tlinkFg: groupCallMembersFg;\n\t\t}\n\t\talign: align(top);\n\t\ttextFg: groupCallMembersFg;\n\t\tminWidth: 190px;\n\t}\n}\n\nauctionInfoPreviewMargin: margins(0px, 24px, 0px, 8px);\nauctionInfoSubtitleSkip: 8px;\nauctionInfoTableMargin: margins(0px, 12px, 0px, 12px);\nauctionInfoValueMultiline: FlatLabel(defaultTableValue) {\n\tminWidth: 96px;\n\tmaxHeight: 96px;\n}\nauctionBidEmoji: IconEmoji {\n\ticon: icon {{ \"settings/button_auction\", windowFg }};\n\tpadding: margins(-4px, -2px, -4px, 0px);\n}\nauctionBidToast: Toast(defaultToast) {\n\tpadding: margins(19px, 13px, 19px, 12px);\n}\nauctionBidToastIcon: icon {{ \"toast/auction\", toastFg }};\nauctionAboutLogo: icon {{ \"settings/large_auctions\", windowFgActive }};\nauctionAboutLogoPadding: margins(8px, 8px, 8px, 8px);\nauctionAboutTextPadding: margins(0px, 6px, 0px, 8px);\nauctionCenteredSubtitle: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(10px);\n\t}\n}\nauctionChangeRecipientPadding: margins(0px, 0px, 0px, 10px);\n\nauctionBidPlace: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n\ttextFg: windowActiveTextFg;\n}\nauctionBidUserpic: UserpicButton(defaultUserpicButton) {\n\tsize: size(28px, 36px);\n\tphotoSize: 28px;\n\tphotoPosition: point(0px, 4px);\n}\nauctionBidName: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n}\nauctionBidStars: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowSubTextFg;\n}\nauctionBidSkip: 10px;\n\nauctionListEntrySkip: 12px;\nauctionListTitle: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(15px semibold);\n\t}\n\ttextFg: windowBoldFg;\n}\nauctionListTitlePadding: margins(50px, 0px, 0px, 0px);\nauctionListText: FlatLabel(defaultFlatLabel) {\n}\nauctionListTextPadding: margins(50px, 4px, 0px, 0px);\nauctionListRaise: RoundButton(defaultActiveButton) {\n\twidth: 0px;\n}\nauctionListRaisePadding: margins(0px, 8px, 0px, 0px);\n\nCraftRadialPercent {\n\tstroke: pixels;\n\tfont: font;\n}\n\ncraftTitleMargin: margins(64px, 16px, 64px, 4px);\ncraftAbout: FlatLabel(defaultFlatLabel) {\n\talign: align(top);\n\tminWidth: 128px;\n}\ncraftAboutMargin: margins(24px, 12px, 24px, 4px);\ncraftChance: FlatLabel(defaultFlatLabel) {\n\tstyle: semiboldTextStyle;\n\ttextFg: attentionButtonFg;\n\tmargin: margins(8px, 4px, 8px, 4px);\n}\ncraftPattern: IconEmoji {\n\ticon: icon{{ \"chat/filled_forge-36x36\", windowFg }};\n\tpadding: margins(0px, 0px, 0px, 0px);\n}\ncraftAddIcon: icon {{ \"settings/plus\", windowFgActive }};\ncraftReplaceIcon: icon {{ \"mini_replace-12x12\", windowFgActive }};\ncraftPreviewPadding: margins(24px, 8px, 40px, 8px);\ncraftBoxButtonPadding: margins(22px, 12px, 22px, 22px);\ncraftAttributesMargin: margins(22px, 12px, 22px, 16px);\ncraftPreviewAllPadding: margins(6px, 4px, 6px, 4px);\ncraftPreviewAllMargin: margins(22px, 0px, 22px, 8px);\ncraftProgressTitleMargin: margins(0px, 12px, 0px, 16px);\ncraftProgressAboutMargin: margins(0px, 4px, 0px, 16px);\ncraftProgressWarningMargin: margins(0px, 4px, 0px, 16px);\ncraftProgressChanceMargin: margins(0px, 40px, 0px, 16px);\ncraftProgressIconSize: size(24px, 24px);\ncraftProgressIconSkip: 4px;\ncraftFailureThumbsTop: 36px;\ncraftFailureTitleMargin: margins(0px, 88px, 0px, 12px);\ncraftFailureAboutMargin: margins(0px, 0px, 0px, 16px);\n\ncraftForge: icon{{ \"chat/filled_forge-48x48\", windowFg }};\ncraftFailLottieSize: size(36px, 36px);\ncraftForgePadding: 12px;\ncraftForgePercent: CraftRadialPercent {\n\tstroke: 4px;\n\tfont: font(14px semibold);\n}\ncraftForgeLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {\n\tcolor: windowFgActive;\n\tthickness: 4px;\n}\ncraftPercentLabel: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(11px semibold);\n\t}\n\tmargin: margins(5px, 2px, 5px, 2px);\n}\ncraftAttributeSkip: 12px;\ncraftAttributePadding: 7px;\ncraftAttributeSize: 48px;\ncraftAttributePercent: CraftRadialPercent {\n\tstroke: 3px;\n\tfont: font(10px semibold);\n}\ncraftAttributeRowSkip: 8px;\ncraftYourListEmpty: FlatLabel(defaultFlatLabel) {\n\tminWidth: 128px;\n\talign: align(top);\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/credits_graphics.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/credits_graphics.h\"\n\n#include <QtCore/QDateTime>\n\n#include \"data/data_credits.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_session.h\"\n#include \"history/view/media/history_view_sticker_player.h\"\n#include \"info/bot/starref/info_bot_starref_common.h\"\n#include \"info/userpic/info_userpic_emoji_builder_preview.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/effects/spoiler_mess.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/custom_emoji_instance.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/fields/number_input.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_channel_earn.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_intro.h\" // introFragmentIcon.\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_widgets.h\"\n\n#include <QtSvg/QSvgRenderer>\n\nnamespace Ui {\nnamespace {\n\nPaintRoundImageCallback MultiThumbnail(\n\t\tPaintRoundImageCallback first,\n\t\tPaintRoundImageCallback second,\n\t\tint totalCount) {\n\tconst auto cache = std::make_shared<QImage>();\n\treturn [=](Painter &p, int x, int y, int outerWidth, int size) {\n\t\tconst auto stroke = st::lineWidth * 2;\n\t\tconst auto shift = stroke * 3;\n\t\tif (size <= 2 * shift) {\n\t\t\tfirst(p, x, y, outerWidth, size);\n\t\t\treturn;\n\t\t}\n\t\tconst auto smaller = size - shift;\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto full = QSize(size, size) * ratio;\n\t\tif (cache->size() != full) {\n\t\t\t*cache = QImage(full, QImage::Format_ARGB32_Premultiplied);\n\t\t\tcache->setDevicePixelRatio(ratio);\n\t\t}\n\t\tcache->fill(Qt::transparent);\n\t\tauto q = Painter(cache.get());\n\t\tsecond(q, shift, 0, outerWidth, smaller);\n\t\tq.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tq.setPen(QPen(Qt::transparent, 2 * stroke));\n\t\tq.setBrush(Qt::NoBrush);\n\t\tconst auto radius = st::roundRadiusLarge;\n\t\tauto hq = PainterHighQualityEnabler(q);\n\t\tq.drawRoundedRect(QRect(0, shift, smaller, smaller), radius, radius);\n\t\tq.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\tfirst(q, 0, shift, outerWidth, smaller);\n\t\tq.setPen(Qt::NoPen);\n\t\tq.setBrush(st::shadowFg);\n\t\tq.drawRoundedRect(QRect(0, shift, smaller, smaller), radius, radius);\n\t\tq.setPen(st::toastFg);\n\t\tq.setFont(style::font(smaller / 2, style::FontFlag::Semibold, 0));\n\t\tq.drawText(\n\t\t\tQRect(0, shift, smaller, smaller),\n\t\t\tQString::number(totalCount),\n\t\t\tstyle::al_center);\n\t\tq.end();\n\n\t\tp.drawImage(x, y, *cache);\n\t};\n}\n\nQByteArray CreditsIconSvg(int strokeWidth) {\n\tauto colorized = qs(Premium::ColorizedSvg(\n\t\tPremium::CreditsIconGradientStops()));\n\tcolorized.replace(\n\t\tu\"stroke=\\\"none\\\"\"_q,\n\t\tu\"stroke=\\\"%1\\\"\"_q.arg(st::creditsStroke->c.name()));\n\tcolorized.replace(\n\t\tu\"stroke-width=\\\"1\\\"\"_q,\n\t\tu\"stroke-width=\\\"%1\\\"\"_q.arg(strokeWidth));\n\treturn colorized.toUtf8();\n}\n\n} // namespace\n\nQImage GenerateStars(int height, int count) {\n\tconstexpr auto kOutlineWidth = .6;\n\tconstexpr auto kStrokeWidth = 3;\n\tconstexpr auto kShift = 3;\n\n\tauto svg = QSvgRenderer(CreditsIconSvg(kStrokeWidth));\n\tsvg.setViewBox(svg.viewBox() + Margins(kStrokeWidth));\n\n\tconst auto starSize = Size(height - kOutlineWidth * 2);\n\n\tauto frame = QImage(\n\t\tQSize(\n\t\t\t(height + kShift * (count - 1)) * style::DevicePixelRatio(),\n\t\t\theight * style::DevicePixelRatio()),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tframe.setDevicePixelRatio(style::DevicePixelRatio());\n\tframe.fill(Qt::transparent);\n\tconst auto drawSingle = [&](QPainter &q) {\n\t\tconst auto s = kOutlineWidth;\n\t\tq.save();\n\t\tq.translate(s, s);\n\t\tq.setCompositionMode(QPainter::CompositionMode_Clear);\n\t\tsvg.render(&q, QRectF(QPointF(s, 0), starSize));\n\t\tsvg.render(&q, QRectF(QPointF(s, s), starSize));\n\t\tsvg.render(&q, QRectF(QPointF(0, s), starSize));\n\t\tsvg.render(&q, QRectF(QPointF(-s, s), starSize));\n\t\tsvg.render(&q, QRectF(QPointF(-s, 0), starSize));\n\t\tsvg.render(&q, QRectF(QPointF(-s, -s), starSize));\n\t\tsvg.render(&q, QRectF(QPointF(0, -s), starSize));\n\t\tsvg.render(&q, QRectF(QPointF(s, -s), starSize));\n\t\tq.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\tsvg.render(&q, Rect(starSize));\n\t\tq.restore();\n\t};\n\t{\n\t\tauto q = QPainter(&frame);\n\t\tq.translate(frame.width() / style::DevicePixelRatio() - height, 0);\n\t\tfor (auto i = count; i > 0; --i) {\n\t\t\tdrawSingle(q);\n\t\t\tq.translate(-kShift, 0);\n\t\t}\n\t}\n\treturn frame;\n}\n\nnot_null<RpWidget*> CreateSingleStarWidget(\n\t\tnot_null<RpWidget*> parent,\n\t\tint height) {\n\tconst auto widget = CreateChild<RpWidget>(parent);\n\tconst auto image = GenerateStars(height, 1);\n\twidget->resize(image.size() / style::DevicePixelRatio());\n\twidget->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(widget);\n\t\tp.drawImage(0, 0, image);\n\t}, widget->lifetime());\n\twidget->setAttribute(Qt::WA_TransparentForMouseEvents);\n\treturn widget;\n}\n\nnot_null<MaskedInputField*> AddInputFieldForCredits(\n\t\tnot_null<VerticalLayout*> container,\n\t\trpl::producer<CreditsAmount> value) {\n\tconst auto &st = st::botEarnInputField;\n\tconst auto inputContainer = container->add(\n\t\tCreateSkipWidget(container, st.heightMin));\n\tconst auto currentValue = rpl::variable<CreditsAmount>(\n\t\trpl::duplicate(value));\n\tconst auto input = CreateChild<NumberInput>(\n\t\tinputContainer,\n\t\tst,\n\t\ttr::lng_bot_earn_out_ph_max(\n\t\t\tlt_amount,\n\t\t\tcurrentValue.value() | rpl::map([](CreditsAmount amount) {\n\t\t\t\treturn QString::number(amount.whole());\n\t\t\t})),\n\t\tQString::number(currentValue.current().whole()),\n\t\tcurrentValue.current().whole());\n\trpl::duplicate(\n\t\tvalue\n\t) | rpl::on_next([=](CreditsAmount v) {\n\t\tinput->changeLimit(v.whole());\n\t\tinput->setText(QString::number(v.whole()));\n\t}, input->lifetime());\n\tconst auto icon = CreateSingleStarWidget(\n\t\tinputContainer,\n\t\tst.style.font->height);\n\tinputContainer->sizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\tinput->resize(\n\t\t\tsize.width() - rect::m::sum::h(st::boxRowPadding),\n\t\t\tst.heightMin);\n\t\tinput->moveToLeft(st::boxRowPadding.left(), 0);\n\t\ticon->moveToLeft(\n\t\t\tst::boxRowPadding.left(),\n\t\t\tst.textMargins.top());\n\t}, input->lifetime());\n\tToggleChildrenVisibility(inputContainer, true);\n\treturn input;\n}\n\nPaintRoundImageCallback GenerateCreditsPaintUserpicCallback(\n\t\tconst Data::CreditsHistoryEntry &entry) {\n\tusing PeerType = Data::CreditsHistoryEntry::PeerType;\n\tif (entry.peerType == PeerType::PremiumBot) {\n\t\tconst auto svg = std::make_shared<QSvgRenderer>(Ui::Premium::Svg());\n\t\treturn [=](Painter &p, int x, int y, int, int size) mutable {\n\t\t\tconst auto hq = PainterHighQualityEnabler(p);\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\t{\n\t\t\t\tauto gradient = QLinearGradient(x + size, y + size, x, y);\n\t\t\t\tgradient.setStops(Ui::Premium::ButtonGradientStops());\n\t\t\t\tp.setBrush(gradient);\n\t\t\t}\n\t\t\tp.drawEllipse(x, y, size, size);\n\t\t\tsvg->render(&p, QRectF(x, y, size, size) - Margins(size / 5.));\n\t\t};\n\t}\n\tconst auto bg = [&]() -> EmptyUserpic::BgColors {\n\t\tif (entry.postsSearch) {\n\t\t\treturn {\n\t\t\t\tst::historyPeerSavedMessagesBg,\n\t\t\t\tst::historyPeerSavedMessagesBg2,\n\t\t\t};\n\t\t}\n\t\tswitch (entry.peerType) {\n\t\tcase Data::CreditsHistoryEntry::PeerType::API:\n\t\t\treturn { st::historyPeer2UserpicBg, st::historyPeer2UserpicBg2 };\n\t\tcase Data::CreditsHistoryEntry::PeerType::Peer:\n\t\t\treturn EmptyUserpic::UserpicColor(0);\n\t\tcase Data::CreditsHistoryEntry::PeerType::AppStore:\n\t\t\treturn { st::historyPeer7UserpicBg, st::historyPeer7UserpicBg2 };\n\t\tcase Data::CreditsHistoryEntry::PeerType::PlayMarket:\n\t\t\treturn { st::historyPeer2UserpicBg, st::historyPeer2UserpicBg2 };\n\t\tcase Data::CreditsHistoryEntry::PeerType::Fragment:\n\t\t\treturn { st::windowSubTextFg, st::imageBg };\n\t\tcase Data::CreditsHistoryEntry::PeerType::PremiumBot:\n\t\t\treturn { st::historyPeer8UserpicBg, st::historyPeer8UserpicBg2 };\n\t\tcase Data::CreditsHistoryEntry::PeerType::Ads:\n\t\t\treturn { st::historyPeer6UserpicBg, st::historyPeer6UserpicBg2 };\n\t\tcase Data::CreditsHistoryEntry::PeerType::Unsupported:\n\t\t\treturn {\n\t\t\t\tst::historyPeerArchiveUserpicBg,\n\t\t\t\tst::historyPeerArchiveUserpicBg,\n\t\t\t};\n\t\t}\n\t\tUnexpected(\"Unknown peer type.\");\n\t}();\n\tconst auto userpic = std::make_shared<EmptyUserpic>(bg, QString());\n\tif (entry.peerType == PeerType::API) {\n\t\tconst auto svg = std::make_shared<QSvgRenderer>(Ui::Premium::Svg());\n\t\tconst auto image = std::make_shared<QImage>();\n\t\treturn [=](Painter &p, int x, int y, int outer, int size) mutable {\n\t\t\tuserpic->paintCircle(p, x, y, outer, size);\n\t\t\tif (image->isNull()) {\n\t\t\t\t*image = QImage(\n\t\t\t\t\tSize(size) * style::DevicePixelRatio(),\n\t\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t\timage->setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\t\timage->fill(Qt::transparent);\n\t\t\t\tconstexpr auto kSize = 126.;\n\t\t\t\tconstexpr auto kBubbleRatio = kSize / ((kSize - 70) / 2.);\n\t\t\t\tconst auto rect = QRectF(0, 0, size, size)\n\t\t\t\t\t- Margins(size / kBubbleRatio);\n\n\t\t\t\tauto q = QPainter(image.get());\n\t\t\t\tconst auto hq = PainterHighQualityEnabler(q);\n\t\t\t\tq.setPen(Qt::NoPen);\n\t\t\t\tq.setBrush(st::historyPeerUserpicFg);\n\t\t\t\tq.drawEllipse(rect);\n\t\t\t\tconstexpr auto kTailX1 = 4;\n\t\t\t\tconstexpr auto kTailY1 = 8;\n\t\t\t\tconstexpr auto kTailX2 = 2;\n\t\t\t\tconstexpr auto kTailY2 = 0;\n\t\t\t\tconstexpr auto kTailX3 = 9;\n\t\t\t\tconstexpr auto kTailY3 = 4;\n\t\t\t\tauto path = QPainterPath();\n\t\t\t\tpath.moveTo(\n\t\t\t\t\tst::lineWidth * kTailX1,\n\t\t\t\t\trect.height() - st::lineWidth * kTailY1);\n\t\t\t\tpath.lineTo(\n\t\t\t\t\tst::lineWidth * kTailX2,\n\t\t\t\t\trect.height() - st::lineWidth * kTailY2);\n\t\t\t\tpath.lineTo(\n\t\t\t\t\tst::lineWidth * kTailX3,\n\t\t\t\t\trect.height() - st::lineWidth * kTailY3);\n\t\t\t\tpath.translate(rect.x(), rect.y());\n\t\t\t\tq.strokePath(\n\t\t\t\t\tpath,\n\t\t\t\t\tQPen(\n\t\t\t\t\t\tst::historyPeerUserpicFg,\n\t\t\t\t\t\tst::lineWidth * 2,\n\t\t\t\t\t\tQt::SolidLine,\n\t\t\t\t\t\tQt::RoundCap,\n\t\t\t\t\t\tQt::RoundJoin));\n\t\t\t\tq.fillPath(path, st::historyPeerUserpicFg);\n\t\t\t\tq.setCompositionMode(QPainter::CompositionMode_Clear);\n\t\t\t\tconstexpr auto kStarRatio = kSize / ((kSize - 44) / 2.);\n\t\t\t\tsvg->render(\n\t\t\t\t\t&q,\n\t\t\t\t\tQRectF(0, 0, size, size) - Margins(size / kStarRatio));\n\t\t\t}\n\t\t\tp.drawImage(x, y, *image);\n\t\t};\n\t}\n\treturn [=](Painter &p, int x, int y, int outerWidth, int size) mutable {\n\t\tuserpic->paintCircle(p, x, y, outerWidth, size);\n\t\tconst auto rect = QRect(x, y, size, size);\n\t\t(entry.postsSearch\n\t\t\t? st::creditsHistorySearchPostsIcon\n\t\t\t: (entry.peerType == PeerType::AppStore)\n\t\t\t? st::sessionIconiPhone\n\t\t\t: (entry.peerType == PeerType::PlayMarket)\n\t\t\t? st::sessionIconAndroid\n\t\t\t: (entry.peerType == PeerType::Fragment)\n\t\t\t? st::introFragmentIcon\n\t\t\t: (entry.peerType == PeerType::Ads)\n\t\t\t? st::creditsHistoryEntryTypeAds\n\t\t\t: st::dialogsInaccessibleUserpic).paintInCenter(p, rect);\n\t};\n}\n\nPaintRoundImageCallback GenerateCreditsPaintEntryCallback(\n\t\tnot_null<PhotoData*> photo,\n\t\tFn<void()> update) {\n\tstruct State {\n\t\tstd::shared_ptr<Data::PhotoMedia> view;\n\t\tImage *imagePtr = nullptr;\n\t\tQImage image;\n\t\trpl::lifetime downloadLifetime;\n\t\tbool entryImageLoaded = false;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tstate->view = photo->createMediaView();\n\tphoto->load(Data::PhotoSize::Large, {});\n\n\trpl::single(rpl::empty_value()) | rpl::then(\n\t\tphoto->session().downloaderTaskFinished()\n\t) | rpl::on_next([=] {\n\t\tusing Size = Data::PhotoSize;\n\t\tif (const auto large = state->view->image(Size::Large)) {\n\t\t\tstate->imagePtr = large;\n\t\t} else if (const auto small = state->view->image(Size::Small)) {\n\t\t\tstate->imagePtr = small;\n\t\t} else if (const auto t = state->view->image(Size::Thumbnail)) {\n\t\t\tstate->imagePtr = t;\n\t\t}\n\t\tupdate();\n\t\tif (state->view->loaded()) {\n\t\t\tstate->entryImageLoaded = true;\n\t\t\tstate->downloadLifetime.destroy();\n\t\t}\n\t}, state->downloadLifetime);\n\n\treturn [=](Painter &p, int x, int y, int outerWidth, int size) {\n\t\tif (state->imagePtr\n\t\t\t&& (!state->entryImageLoaded || state->image.isNull())) {\n\t\t\tconst auto image = state->imagePtr->original();\n\t\t\tconst auto minSize = std::min(image.width(), image.height());\n\t\t\tstate->image = Images::Prepare(\n\t\t\t\timage.copy(\n\t\t\t\t\t(image.width() - minSize) / 2,\n\t\t\t\t\t(image.height() - minSize) / 2,\n\t\t\t\t\tminSize,\n\t\t\t\t\tminSize),\n\t\t\t\tsize * style::DevicePixelRatio(),\n\t\t\t\t{ .options = Images::Option::RoundLarge });\n\t\t}\n\t\tp.drawImage(x, y, state->image);\n\t};\n}\n\nPaintRoundImageCallback GenerateCreditsPaintEntryCallback(\n\t\tnot_null<DocumentData*> video,\n\t\tFn<void()> update) {\n\tstruct State {\n\t\tstd::shared_ptr<Data::DocumentMedia> view;\n\t\tImage *imagePtr = nullptr;\n\t\tQImage image;\n\t\trpl::lifetime downloadLifetime;\n\t\tbool entryImageLoaded = false;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tstate->view = video->createMediaView();\n\tvideo->loadThumbnail({});\n\n\trpl::single(rpl::empty_value()) | rpl::then(\n\t\tvideo->session().downloaderTaskFinished()\n\t) | rpl::on_next([=] {\n\t\tif (const auto thumbnail = state->view->thumbnail()) {\n\t\t\tstate->imagePtr = thumbnail;\n\t\t}\n\t\tupdate();\n\t\tif (state->imagePtr) {\n\t\t\tstate->entryImageLoaded = true;\n\t\t\tstate->downloadLifetime.destroy();\n\t\t}\n\t}, state->downloadLifetime);\n\n\treturn [=](Painter &p, int x, int y, int outerWidth, int size) {\n\t\tif (state->imagePtr\n\t\t\t&& (!state->entryImageLoaded || state->image.isNull())) {\n\t\t\tconst auto image = state->imagePtr->original();\n\t\t\tconst auto minSize = std::min(image.width(), image.height());\n\t\t\tstate->image = Images::Prepare(\n\t\t\t\timage.copy(\n\t\t\t\t\t(image.width() - minSize) / 2,\n\t\t\t\t\t(image.height() - minSize) / 2,\n\t\t\t\t\tminSize,\n\t\t\t\t\tminSize),\n\t\t\t\tsize * style::DevicePixelRatio(),\n\t\t\t\t{ .options = Images::Option::RoundLarge });\n\t\t}\n\t\tp.drawImage(x, y, state->image);\n\t};\n}\n\nPaintRoundImageCallback GenerateCreditsPaintEntryCallback(\n\t\tnot_null<Main::Session*> session,\n\t\tData::CreditsHistoryMedia media,\n\t\tFn<void()> update) {\n\treturn (media.type == Data::CreditsHistoryMediaType::Photo)\n\t\t? GenerateCreditsPaintEntryCallback(\n\t\t\tsession->data().photo(media.id),\n\t\t\tstd::move(update))\n\t\t: GenerateCreditsPaintEntryCallback(\n\t\t\tsession->data().document(media.id),\n\t\t\tstd::move(update));\n}\n\nPaintRoundImageCallback GenerateCreditsPaintEntryCallback(\n\t\tnot_null<Main::Session*> session,\n\t\tconst std::vector<Data::CreditsHistoryMedia> &media,\n\t\tFn<void()> update) {\n\tif (media.size() == 1) {\n\t\treturn GenerateCreditsPaintEntryCallback(\n\t\t\tsession,\n\t\t\tmedia.front(),\n\t\t\tstd::move(update));\n\t}\n\treturn MultiThumbnail(\n\t\tGenerateCreditsPaintEntryCallback(session, media[0], update),\n\t\tGenerateCreditsPaintEntryCallback(session, media[1], update),\n\t\tmedia.size());\n}\n\nPaintRoundImageCallback GeneratePaidPhotoPaintCallback(\n\t\tnot_null<PhotoData*> photo,\n\t\tFn<void()> update) {\n\tstruct State {\n\t\texplicit State(Fn<void()> update) : spoiler(std::move(update)) {\n\t\t}\n\n\t\tQImage image;\n\t\tQImage spoilerCornerCache;\n\t\tSpoilerAnimation spoiler;\n\t};\n\tconst auto state = std::make_shared<State>(update);\n\n\treturn [=](Painter &p, int x, int y, int outerWidth, int size) {\n\t\tif (state->image.isNull()) {\n\t\t\tconst auto media = photo->createMediaView();\n\t\t\tconst auto thumbnail = media->thumbnailInline();\n\t\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t\tconst auto scaled = QSize(size, size) * ratio;\n\t\t\tauto image = thumbnail\n\t\t\t\t? Images::Blur(thumbnail->original(), true)\n\t\t\t\t: QImage(scaled, QImage::Format_ARGB32_Premultiplied);\n\t\t\tif (!thumbnail) {\n\t\t\t\timage.fill(Qt::black);\n\t\t\t\timage.setDevicePixelRatio(ratio);\n\t\t\t}\n\t\t\tconst auto minSize = std::min(image.width(), image.height());\n\t\t\tstate->image = Images::Prepare(\n\t\t\t\timage.copy(\n\t\t\t\t\t(image.width() - minSize) / 2,\n\t\t\t\t\t(image.height() - minSize) / 2,\n\t\t\t\t\tminSize,\n\t\t\t\t\tminSize),\n\t\t\t\tsize * ratio,\n\t\t\t\t{ .options = Images::Option::RoundLarge });\n\t\t}\n\t\tp.drawImage(x, y, state->image);\n\t\tFillSpoilerRect(\n\t\t\tp,\n\t\t\tQRect(x, y, size, size),\n\t\t\tImages::CornersMaskRef(\n\t\t\t\tImages::CornersMask(ImageRoundRadius::Large)),\n\t\t\tDefaultImageSpoiler().frame(\n\t\t\t\tstate->spoiler.index(crl::now(), false)),\n\t\t\tstate->spoilerCornerCache);\n\t};\n}\n\nPaintRoundImageCallback GeneratePaidMediaPaintCallback(\n\t\tnot_null<PhotoData*> photo,\n\t\tPhotoData *second,\n\t\tint totalCount,\n\t\tFn<void()> update) {\n\tif (!second) {\n\t\treturn GeneratePaidPhotoPaintCallback(photo, update);\n\t}\n\treturn MultiThumbnail(\n\t\tGeneratePaidPhotoPaintCallback(photo, update),\n\t\tGeneratePaidPhotoPaintCallback(second, update),\n\t\ttotalCount);\n}\n\nPaintRoundImageCallback GenerateGiftStickerUserpicCallback(\n\t\tnot_null<Main::Session*> session,\n\t\tuint64 stickerId,\n\t\tFn<void()> update) {\n\tstruct State {\n\t\tstd::optional<UserpicBuilder::PreviewPainter> painter;\n\t\tint size = 0;\n\t};\n\tconst auto state = std::make_shared<State>();\n\t//const auto document = session->data().document(stickerId);\n\treturn [=](Painter &p, int x, int y, int outerWidth, int size) {\n\t\tif (state->size != size || !state->painter) {\n\t\t\tstate->size = size;\n\t\t\tstate->painter.emplace(size * M_SQRT2);\n\t\t\tstate->painter->setDocument(\n\t\t\t\tsession->data().document(stickerId),\n\t\t\t\tupdate);\n\t\t}\n\t\tconst auto skip = int(base::SafeRound((size * (M_SQRT2 - 1.)) / 2.));\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.translate(x - skip, y - skip);\n\t\tstate->painter->paintForeground(p);\n\t\tp.translate(skip - x, skip - y);\n\t};\n}\n\nFn<PaintRoundImageCallback(Fn<void()>)> PaintPreviewCallback(\n\t\tnot_null<Main::Session*> session,\n\t\tconst Data::CreditsHistoryEntry &entry) {\n\tconst auto &extended = entry.extended;\n\tif (!extended.empty()) {\n\t\treturn [=](Fn<void()> update) {\n\t\t\treturn GenerateCreditsPaintEntryCallback(\n\t\t\t\tsession,\n\t\t\t\textended,\n\t\t\t\tstd::move(update));\n\t\t};\n\t} else if (entry.photoId && entry.subscriptionUntil.isNull()) {\n\t\tconst auto photo = session->data().photo(entry.photoId);\n\t\treturn [=](Fn<void()> update) {\n\t\t\treturn GenerateCreditsPaintEntryCallback(\n\t\t\t\tphoto,\n\t\t\t\tstd::move(update));\n\t\t};\n\t}\n\treturn nullptr;\n}\n\nTextWithEntities GenerateEntryName(const Data::CreditsHistoryEntry &entry) {\n\treturn (entry.starrefCommission && !entry.starrefAmount)\n\t\t? tr::lng_credits_commission(\n\t\t\ttr::now,\n\t\t\tlt_amount,\n\t\t\tTextWithEntities{\n\t\t\t\tInfo::BotStarRef::FormatCommission(entry.starrefCommission)\n\t\t\t},\n\t\t\tTextWithEntities::Simple)\n\t\t: entry.isLiveStoryReaction()\n\t\t? tr::lng_credits_paid_messages_fee_live_reaction(\n\t\t\ttr::now,\n\t\t\tTextWithEntities::Simple)\n\t\t: entry.paidMessagesCount\n\t\t? tr::lng_credits_paid_messages_fee(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tentry.paidMessagesCount,\n\t\t\tTextWithEntities::Simple)\n\t\t: (entry.premiumMonthsForStars\n\t\t? tr::lng_premium_summary_title\n\t\t: entry.floodSkip\n\t\t? tr::lng_credits_box_history_entry_api\n\t\t: entry.reaction\n\t\t? tr::lng_credits_box_history_entry_reaction_name\n\t\t: entry.giftResale\n\t\t? (entry.in\n\t\t\t? tr::lng_credits_box_history_entry_gift_sold\n\t\t\t: tr::lng_credits_box_history_entry_gift_bought)\n\t\t: entry.bareGiveawayMsgId\n\t\t? tr::lng_credits_box_history_entry_giveaway_name\n\t\t: entry.converted\n\t\t? tr::lng_credits_box_history_entry_gift_converted\n\t\t: (entry.gift && !entry.in && entry.uniqueGift)\n\t\t? tr::lng_credits_box_history_entry_gift_transfer\n\t\t: (entry.starsConverted || (entry.gift && !entry.in))\n\t\t? tr::lng_credits_box_history_entry_gift_sent\n\t\t: entry.gift\n\t\t? tr::lng_credits_box_history_entry_gift_name\n\t\t: (entry.peerType == Data::CreditsHistoryEntry::PeerType::Fragment)\n\t\t? tr::lng_credits_box_history_entry_fragment\n\t\t: (entry.peerType == Data::CreditsHistoryEntry::PeerType::PremiumBot)\n\t\t? tr::lng_credits_box_history_entry_premium_bot\n\t\t: (entry.peerType == Data::CreditsHistoryEntry::PeerType::Ads)\n\t\t? tr::lng_credits_box_history_entry_ads\n\t\t: tr::lng_credits_summary_history_entry_inner_in)(\n\t\t\ttr::now,\n\t\t\tTextWithEntities::Simple);\n}\n\nFn<void(QPainter &)> PaintOutlinedColoredCreditsIconCallback(\n\t\tint size,\n\t\tfloat64 outlineRatio) {\n\t// constexpr auto kIdealSize = 42;\n\tconstexpr auto kPoints = uint(16);\n\tconstexpr auto kAngleStep = 2. * M_PI / kPoints;\n\tconstexpr auto kOutlineWidth = 1.6;\n\t// constexpr auto kStarShift = 3.8;\n\tconstexpr auto kStrokeWidth = 3;\n\tconst auto starSize = Size(size);\n\n\tauto svg = std::make_shared<QSvgRenderer>(CreditsIconSvg(kStrokeWidth));\n\tsvg->setViewBox(svg->viewBox() + Margins(kStrokeWidth));\n\tconst auto s = style::ConvertFloatScale(kOutlineWidth * outlineRatio);\n\treturn [=](QPainter &q) {\n\t\tq.save();\n\t\tq.setCompositionMode(QPainter::CompositionMode_Clear);\n\t\tfor (auto i = 0; i < kPoints; ++i) {\n\t\t\tconst auto angle = i * kAngleStep;\n\t\t\tconst auto x = s * std::cos(angle);\n\t\t\tconst auto y = s * std::sin(angle);\n\t\t\tsvg->render(&q, QRectF(QPointF(x, y), starSize));\n\t\t}\n\t\tq.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\tsvg->render(&q, Rect(starSize));\n\t\tq.restore();\n\t};\n}\n\nQImage CreditsWhiteDoubledIcon(int size, float64 outlineRatio) {\n\tauto svg = QSvgRenderer(Ui::Premium::Svg());\n\tauto result = QImage(\n\t\tSize(size) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::transparent);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\n\t// constexpr auto kIdealSize = 42;\n\tconstexpr auto kPoints = uint(16);\n\tconstexpr auto kAngleStep = 2. * M_PI / kPoints;\n\tconstexpr auto kOutlineWidth = 1.6;\n\tconstexpr auto kStarShift = 3.8;\n\tconst auto userpicRect = Rect(Size(size));\n\tconst auto starRect = userpicRect - Margins(userpicRect.width() / 4.);\n\tconst auto starSize = starRect.size();\n\tconst auto drawSingle = [&](QPainter &q) {\n\t\tconst auto s = style::ConvertFloatScale(kOutlineWidth * outlineRatio);\n\t\tq.save();\n\t\tq.setCompositionMode(QPainter::CompositionMode_Clear);\n\t\tfor (auto i = 0; i < kPoints; ++i) {\n\t\t\tconst auto angle = i * kAngleStep;\n\t\t\tconst auto x = s * std::cos(angle);\n\t\t\tconst auto y = s * std::sin(angle);\n\t\t\tsvg.render(&q, QRectF(QPointF(x, y), starSize));\n\t\t}\n\t\tq.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\tsvg.render(&q, Rect(starSize));\n\t\tq.restore();\n\t};\n\t{\n\t\tauto p = QPainter(&result);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::lightButtonFg);\n\t\tp.translate(starRect.topLeft());\n\t\tp.translate(\n\t\t\tstyle::ConvertFloatScale(kStarShift * outlineRatio) / 2.,\n\t\t\t0);\n\t\tdrawSingle(p);\n\t\t{\n\t\t\t// Remove the previous star at bottom.\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_Clear);\n\t\t\tp.save();\n\t\t\tp.resetTransform();\n\t\t\tp.fillRect(\n\t\t\t\tuserpicRect.x(),\n\t\t\t\tuserpicRect.y(),\n\t\t\t\tuserpicRect.width() / 2.,\n\t\t\t\tuserpicRect.height(),\n\t\t\t\tQt::transparent);\n\t\t\tp.restore();\n\t\t}\n\t\tp.translate(\n\t\t\t-style::ConvertFloatScale(kStarShift * outlineRatio),\n\t\t\t0);\n\t\tdrawSingle(p);\n\t}\n\treturn result;\n}\n\nstd::unique_ptr<Ui::Text::CustomEmoji> MakeCreditsIconEmoji(\n\t\tint height,\n\t\tint count) {\n\treturn std::make_unique<Ui::CustomEmoji::Internal>(\n\t\tu\"credits_icon:%1:%2\"_q.arg(height).arg(count),\n\t\tGenerateStars(height, count));\n}\n\nUi::Text::MarkedContext MakeCreditsIconContext(int height, int count) {\n\tauto customEmojiFactory = [=](\n\t\tQStringView data,\n\t\tconst Ui::Text::MarkedContext &context\n\t) -> std::unique_ptr<Ui::Text::CustomEmoji> {\n\t\treturn MakeCreditsIconEmoji(height, count);\n\t};\n\treturn { .customEmojiFactory = std::move(customEmojiFactory) };\n}\n\nTextWithEntities MakeCreditsIconEntity() {\n\treturn Ui::Text::SingleCustomEmoji(Ui::kCreditsCurrency);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/credits_graphics.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PhotoData;\nclass DocumentData;\n\nnamespace Data {\nstruct CreditsHistoryEntry;\nstruct CreditsHistoryMedia;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui::Text {\nclass CustomEmoji;\nstruct MarkedContext;\n} // namespace Ui::Text\n\nnamespace Ui {\n\nclass MaskedInputField;\nclass RpWidget;\nclass VerticalLayout;\n\nusing PaintRoundImageCallback = Fn<void(\n\tPainter &p,\n\tint x,\n\tint y,\n\tint outerWidth,\n\tint size)>;\n\n[[nodiscard]] QImage GenerateStars(int height, int count);\n\n[[nodiscard]] not_null<Ui::RpWidget*> CreateSingleStarWidget(\n\tnot_null<Ui::RpWidget*> parent,\n\tint height);\n\n[[nodiscard]] not_null<Ui::MaskedInputField*> AddInputFieldForCredits(\n\tnot_null<Ui::VerticalLayout*> container,\n\trpl::producer<CreditsAmount> value);\n\nPaintRoundImageCallback GenerateCreditsPaintUserpicCallback(\n\tconst Data::CreditsHistoryEntry &entry);\n\nPaintRoundImageCallback GenerateCreditsPaintEntryCallback(\n\tnot_null<PhotoData*> photo,\n\tFn<void()> update);\n\nPaintRoundImageCallback GenerateCreditsPaintEntryCallback(\n\tnot_null<DocumentData*> video,\n\tFn<void()> update);\n\nPaintRoundImageCallback GenerateCreditsPaintEntryCallback(\n\tnot_null<Main::Session*> session,\n\tData::CreditsHistoryMedia media,\n\tFn<void()> update);\n\nPaintRoundImageCallback GeneratePaidMediaPaintCallback(\n\tnot_null<PhotoData*> photo,\n\tPhotoData *second,\n\tint totalCount,\n\tFn<void()> update);\n\nPaintRoundImageCallback GenerateGiftStickerUserpicCallback(\n\tnot_null<Main::Session*> session,\n\tuint64 stickerId,\n\tFn<void()> update);\n\nFn<PaintRoundImageCallback(Fn<void()>)> PaintPreviewCallback(\n\tnot_null<Main::Session*> session,\n\tconst Data::CreditsHistoryEntry &entry);\n\n[[nodiscard]] TextWithEntities GenerateEntryName(\n\tconst Data::CreditsHistoryEntry &entry);\n\nFn<void(QPainter &)> PaintOutlinedColoredCreditsIconCallback(\n\tint size,\n\tfloat64 outlineRatio);\n\n[[nodiscard]] QImage CreditsWhiteDoubledIcon(int size, float64 outlineRatio);\n\n[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> MakeCreditsIconEmoji(\n\tint height,\n\tint count);\n[[nodiscard]] Ui::Text::MarkedContext MakeCreditsIconContext(\n\tint height,\n\tint count);\n[[nodiscard]] TextWithEntities MakeCreditsIconEntity();\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/emoji_fly_animation.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/emoji_fly_animation.h\"\n\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/animated_icon.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_chat.h\"\n\nnamespace Ui {\nnamespace {\n\n[[nodiscard]] int ComputeFlySize(Data::CustomEmojiSizeTag tag) {\n\tusing Tag = Data::CustomEmojiSizeTag;\n\tif (tag == Tag::Normal) {\n\t\treturn st::reactionInlineImage;\n\t}\n\treturn int(base::SafeRound(\n\t\t(st::reactionInlineImage * Data::FrameSizeFromTag(tag)\n\t\t\t/ float64(Data::FrameSizeFromTag(Tag::Normal)))));\n}\n\n} // namespace\n\nEmojiFlyAnimation::EmojiFlyAnimation(\n\tnot_null<Ui::RpWidget*> body,\n\tnot_null<Data::Reactions*> owner,\n\tReactionFlyAnimationArgs &&args,\n\tFn<void()> repaint,\n\tFn<QColor()> textColor,\n\tData::CustomEmojiSizeTag tag)\n: _flySize(ComputeFlySize(tag))\n, _textColor(std::move(textColor))\n, _fly(\n\towner,\n\tstd::move(args),\n\tstd::move(repaint),\n\t_flySize,\n\ttag)\n, _layer(body) {\n\tbody->sizeValue() | rpl::on_next([=](QSize size) {\n\t\t_layer.setGeometry(QRect(QPoint(), size));\n\t}, _layer.lifetime());\n\n\t_layer.paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tconst auto target = _target.data();\n\t\tif (!target || !target->isVisible()) {\n\t\t\treturn;\n\t\t}\n\t\tauto p = QPainter(&_layer);\n\n\t\tconst auto rect = Ui::MapFrom(&_layer, target, target->rect());\n\t\tconst auto skipx = (rect.width() - _flySize) / 2;\n\t\tconst auto skipy = (rect.height() - _flySize) / 2;\n\t\tconst auto area = _fly.paintGetArea(\n\t\t\tp,\n\t\t\tQPoint(),\n\t\t\tQRect(\n\t\t\t\trect.topLeft() + QPoint(skipx, skipy),\n\t\t\t\tQSize(_flySize, _flySize)),\n\t\t\t(_textColor ? _textColor() : st::infoPeerBadge.premiumFg->c),\n\t\t\tclip,\n\t\t\tcrl::now());\n\t\tif (_areaUpdated || _area.isEmpty()) {\n\t\t\t_area = area;\n\t\t} else {\n\t\t\t_area = _area.united(area);\n\t\t}\n\t}, _layer.lifetime());\n\n\t_layer.setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_layer.show();\n}\n\nnot_null<Ui::RpWidget*> EmojiFlyAnimation::layer() {\n\treturn &_layer;\n}\n\nbool EmojiFlyAnimation::finished() const {\n\tif (const auto target = _target.data()) {\n\t\treturn _fly.finished() || !target->isVisible();\n\t}\n\treturn true;\n}\n\nvoid EmojiFlyAnimation::repaint() {\n\tif (_area.isEmpty()) {\n\t\t_layer.update();\n\t} else {\n\t\t_layer.update(_area);\n\t\t_areaUpdated = true;\n\t}\n}\n\nbool EmojiFlyAnimation::paintBadgeFrame(not_null<QWidget*> widget) {\n\t_target = widget;\n\treturn !_fly.finished();\n}\n\nReactionFlyCenter EmojiFlyAnimation::grabBadgeCenter() {\n\tauto result = _fly.takeCenter();\n\tresult.size = _flySize;\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/emoji_fly_animation.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/reaction_fly_animation.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\n\nstruct ReactionFlyCenter;\n\nclass EmojiFlyAnimation {\npublic:\n\tEmojiFlyAnimation(\n\t\tnot_null<RpWidget*> body,\n\t\tnot_null<Data::Reactions*> owner,\n\t\tUi::ReactionFlyAnimationArgs &&args,\n\t\tFn<void()> repaint,\n\t\tFn<QColor()> textColor,\n\t\tData::CustomEmojiSizeTag tag);\n\n\t[[nodiscard]] not_null<Ui::RpWidget*> layer();\n\t[[nodiscard]] bool finished() const;\n\n\tvoid repaint();\n\tbool paintBadgeFrame(not_null<QWidget*> widget);\n\t[[nodiscard]] ReactionFlyCenter grabBadgeCenter();\n\nprivate:\n\tconst int _flySize = 0;\n\tFn<QColor()> _textColor;\n\tUi::ReactionFlyAnimation _fly;\n\tUi::RpWidget _layer;\n\tQRect _area;\n\tbool _areaUpdated = false;\n\tQPointer<QWidget> _target;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/fireworks_animation.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/fireworks_animation.h\"\n\n#include \"base/random.h\"\n#include \"ui/painter.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kParticlesCount = 60;\nconstexpr auto kFallCount = 30;\nconstexpr auto kFirstUpdateTime = crl::time(16);\nconstexpr auto kFireworkWidth = 480;\nconstexpr auto kFireworkHeight = 320;\n\nstd::vector<QBrush> PrepareBrushes() {\n\treturn {\n\t\tQBrush{ Ui::ColorFromSerialized(0xE8BC2C) },\n\t\tQBrush{ Ui::ColorFromSerialized(0xD0049E) },\n\t\tQBrush{ Ui::ColorFromSerialized(0x02CBFE) },\n\t\tQBrush{ Ui::ColorFromSerialized(0x5723FD) },\n\t\tQBrush{ Ui::ColorFromSerialized(0xFE8C27) },\n\t\tQBrush{ Ui::ColorFromSerialized(0x6CB859) },\n\t};\n}\n\n[[nodiscard]] float64 RandomFloat01() {\n\treturn base::RandomValue<uint32>()\n\t\t/ float64(std::numeric_limits<uint32>::max());\n}\n\n} // namespace\n\nFireworksAnimation::FireworksAnimation(Fn<void()> repaint)\n: _brushes(PrepareBrushes())\n, _animation([=](crl::time now) { update(now); })\n, _repaint(std::move(repaint)) {\n\t_smallSide = style::ConvertScale(2);\n\t_particles.reserve(kParticlesCount + kFallCount);\n\tfor (auto i = 0; i != kParticlesCount; ++i) {\n\t\tinitParticle(_particles.emplace_back(), false);\n\t}\n\t_animation.start();\n}\n\nvoid FireworksAnimation::update(crl::time now) {\n\tconst auto passed = _lastUpdate ? (now - _lastUpdate) : kFirstUpdateTime;\n\t_lastUpdate = now;\n\tauto allFinished = true;\n\tfor (auto &particle : _particles) {\n\t\tupdateParticle(particle, passed);\n\t\tif (!particle.finished) {\n\t\t\tallFinished = false;\n\t\t}\n\t}\n\tif (allFinished) {\n\t\t_animation.stop();\n\t} else if (_fallingDown >= kParticlesCount / 2 && _speedCoef > 0.2) {\n\t\tstartFall();\n\t\t_speedCoef -= passed / 16.0 * 0.15;\n\t\tif (_speedCoef < 0.2) {\n\t\t\t_speedCoef = 0.2;\n\t\t}\n\t}\n\t_repaint();\n}\n\nbool FireworksAnimation::paint(QPainter &p, const QRect &rect) {\n\tif (rect.isEmpty()) {\n\t\treturn false;\n\t}\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tp.setClipRect(rect);\n\tfor (auto &particle : _particles) {\n\t\tif (!particle.finished) {\n\t\t\tpaintParticle(p, particle, rect);\n\t\t}\n\t}\n\tp.setClipping(false);\n\treturn _animation.animating();\n}\n\nvoid FireworksAnimation::paintParticle(\n\t\tQPainter &p,\n\t\tconst Particle &particle,\n\t\tconst QRect &rect) {\n\tconst auto size = particle.size;\n\tconst auto x = rect.x() + (particle.x * rect.width() / kFireworkWidth);\n\tconst auto y = rect.y() + (particle.y * rect.height() / kFireworkHeight);\n\tp.setBrush(_brushes[particle.color]);\n\tif (particle.type == Particle::Type::Circle) {\n\t\tp.drawEllipse(x, y, size, size);\n\t} else {\n\t\tconst auto rect = QRect(-size, -_smallSide, size, _smallSide);\n\t\tp.save();\n\t\tp.translate(x, y);\n\t\tp.rotate(particle.rotation);\n\t\tp.drawRoundedRect(rect, _smallSide, _smallSide);\n\t\tp.restore();\n\t}\n}\n\nvoid FireworksAnimation::updateParticle(Particle &particle, crl::time dt) {\n\tif (particle.finished) {\n\t\treturn;\n\t}\n\tconst auto moveCoef = dt / 16.;\n\tparticle.x += particle.moveX * moveCoef;\n\tparticle.y += particle.moveY * moveCoef;\n\tif (particle.xFinished != 0) {\n\t\tconst auto dp = 0.5;\n\t\tif (particle.xFinished == 1) {\n\t\t\tparticle.moveX += dp * moveCoef * 0.05;\n\t\t\tif (particle.moveX >= dp) {\n\t\t\t\tparticle.xFinished = 2;\n\t\t\t}\n\t\t} else {\n\t\t\tparticle.moveX -= dp * moveCoef * 0.05f;\n\t\t\tif (particle.moveX <= -dp) {\n\t\t\t\tparticle.xFinished = 1;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (particle.right) {\n\t\t\tif (particle.moveX < 0) {\n\t\t\t\tparticle.moveX += moveCoef * 0.05f;\n\t\t\t\tif (particle.moveX >= 0) {\n\t\t\t\t\tparticle.moveX = 0;\n\t\t\t\t\tparticle.xFinished = particle.finishedStart;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif (particle.moveX > 0) {\n\t\t\t\tparticle.moveX -= moveCoef * 0.05f;\n\t\t\t\tif (particle.moveX <= 0) {\n\t\t\t\t\tparticle.moveX = 0;\n\t\t\t\t\tparticle.xFinished = particle.finishedStart;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tconst auto yEdge = -0.5;\n\tconst auto wasNegative = (particle.moveY < yEdge);\n\tif (particle.moveY > yEdge) {\n\t\tparticle.moveY += (1. / 3.) * moveCoef * _speedCoef;\n\t} else {\n\t\tparticle.moveY += (1. / 3.) * moveCoef;\n\t}\n\tif (wasNegative && particle.moveY > yEdge) {\n\t\t++_fallingDown;\n\t}\n\tif (particle.type == Particle::Type::Rectangle) {\n\t\tparticle.rotation += moveCoef * 10;\n\t\tif (particle.rotation > 360) {\n\t\t\tparticle.rotation -= 360;\n\t\t}\n\t}\n\tif (particle.y >= kFireworkHeight) {\n\t\tparticle.finished = true;\n\t}\n}\n\nvoid FireworksAnimation::startFall() {\n\tif (_startedFall) {\n\t\treturn;\n\t}\n\t_startedFall = true;\n\tfor (auto i = 0; i != kFallCount; ++i) {\n\t\tinitParticle(_particles.emplace_back(), true);\n\t}\n}\n\nvoid FireworksAnimation::initParticle(Particle &particle, bool falling) {\n\tusing Type = Particle::Type;\n\tusing base::RandomIndex;\n\n\tparticle.color = RandomIndex(_brushes.size());\n\tparticle.type = RandomIndex(2) ? Type::Rectangle : Type::Circle;\n\tparticle.right = (RandomIndex(2) == 1);\n\tparticle.finishedStart = 1 + RandomIndex(2);\n\tif (particle.type == Type::Circle) {\n\t\tparticle.size = style::ConvertScale(6 + RandomFloat01() * 3);\n\t} else {\n\t\tparticle.size = style::ConvertScale(6 + RandomFloat01() * 6);\n\t\tparticle.rotation = RandomIndex(360);\n\t}\n\tif (falling) {\n\t\tparticle.y = -RandomFloat01() * kFireworkHeight * 1.2f;\n\t\tparticle.x = 5 + RandomIndex(kFireworkWidth - 10);\n\t\tparticle.xFinished = particle.finishedStart;\n\t} else {\n\t\tconst auto xOffset = 4 + RandomIndex(10);\n\t\tconst auto yOffset = kFireworkHeight / 4;\n\t\tif (particle.right) {\n\t\t\tparticle.x = kFireworkWidth + xOffset;\n\t\t} else {\n\t\t\tparticle.x = -xOffset;\n\t\t}\n\t\tparticle.moveX = (particle.right ? -1 : 1) * (1.2 + RandomFloat01() * 4);\n\t\tparticle.moveY = -(4 + RandomFloat01() * 4);\n\t\tparticle.y = yOffset / 2 + RandomIndex(yOffset * 2);\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/fireworks_animation.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n\nnamespace Ui {\n\nclass FireworksAnimation final {\npublic:\n\texplicit FireworksAnimation(Fn<void()> repaint);\n\n\tbool paint(QPainter &p, const QRect &rect);\n\nprivate:\n\tstruct Particle {\n\t\tenum class Type : uchar {\n\t\t\tCircle,\n\t\t\tRectangle\n\t\t};\n\n\t\tfloat64 x = 0.;\n\t\tfloat64 y = 0.;\n\t\tfloat64 moveX = 0.;\n\t\tfloat64 moveY = 0.;\n\t\tuint16 rotation = 0;\n\n\t\tType type = Type::Circle;\n\t\tuchar color = 0;\n\t\tbool right = false;\n\t\tuchar size = 0;\n\t\tuchar xFinished = 0;\n\t\tuchar finishedStart = 0;\n\t\tbool finished = false;\n\t};\n\n\tvoid update(crl::time now);\n\tvoid startFall();\n\tvoid paintParticle(\n\t\tQPainter &p,\n\t\tconst Particle &particle,\n\t\tconst QRect &rect);\n\tvoid initParticle(Particle &particle, bool falling);\n\tvoid updateParticle(Particle &particle, crl::time dt);\n\n\tstd::vector<Particle> _particles;\n\tstd::vector<QBrush> _brushes;\n\tUi::Animations::Basic _animation;\n\tFn<void()> _repaint;\n\tcrl::time _lastUpdate = 0;\n\tfloat64 _speedCoef = 1.;\n\tint _fallingDown = 0;\n\tint _smallSide = 0;\n\tbool _startedFall = false;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/glare.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/glare.h\"\n\n#include \"styles/style_boxes.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kMaxGlareOpaque = 0.5;\n\n} // namespace\n\nfloat64 GlareEffect::progress(crl::time now) const {\n\treturn (now - glare.birthTime)\n\t\t/ float64(glare.deathTime - glare.birthTime);\n}\n\nQLinearGradient GlareEffect::computeGradient(const QColor &color) const {\n\tauto gradient = QLinearGradient(\n\t\tQPointF(0, 0),\n\t\tQPointF(width, 0));\n\n\tauto tempColor = color;\n\ttempColor.setAlphaF(0);\n\tconst auto edge = tempColor;\n\ttempColor.setAlphaF(kMaxGlareOpaque);\n\tconst auto middle = tempColor;\n\tgradient.setStops({\n\t\t{ 0., edge },\n\t\t{ .5, middle },\n\t\t{ 1., edge },\n\t});\n\treturn gradient;\n}\n\nvoid GlareEffect::validate(\n\t\tconst QColor &color,\n\t\tFn<void()> updateCallback,\n\t\tcrl::time timeout,\n\t\tcrl::time duration) {\n\tif (anim::Disabled()) {\n\t\treturn;\n\t}\n\tif (!width) {\n\t\twidth = st::gradientButtonGlareWidth;\n\t}\n\tanimation.init([=](crl::time now) {\n\t\tif (const auto diff = (now - glare.deathTime); diff > 0) {\n\t\t\tif (diff > timeout && !paused) {\n\t\t\t\tglare = {\n\t\t\t\t\t.birthTime = now,\n\t\t\t\t\t.deathTime = now + duration,\n\t\t\t\t};\n\t\t\t\tupdateCallback();\n\t\t\t}\n\t\t} else {\n\t\t\tupdateCallback();\n\t\t}\n\t});\n\tanimation.start();\n\t{\n\t\tauto newPixmap = QPixmap(QSize(width, 1)\n\t\t\t* style::DevicePixelRatio());\n\t\tnewPixmap.setDevicePixelRatio(style::DevicePixelRatio());\n\t\tnewPixmap.fill(Qt::transparent);\n\t\t{\n\t\t\tauto p = QPainter(&newPixmap);\n\t\t\tp.fillRect(newPixmap.rect(), QBrush(computeGradient(color)));\n\t\t}\n\t\tpixmap = std::move(newPixmap);\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/glare.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nstruct GlareEffect final {\n\tvoid validate(\n\t\tconst QColor &color,\n\t\tFn<void()> updateCallback,\n\t\tcrl::time timeout,\n\t\tcrl::time duration);\n\t[[nodiscard]] float64 progress(crl::time now) const;\n\t[[nodiscard]] QLinearGradient computeGradient(const QColor &color) const;\n\n\tUi::Animations::Basic animation;\n\tstruct {\n\t\tcrl::time birthTime = 0;\n\t\tcrl::time deathTime = 0;\n\t} glare;\n\tQPixmap pixmap;\n\tint width = 0;\n\tbool paused = false;\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/loading_element.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/loading_element.h\"\n\n#include \"base/object_ptr.h\"\n#include \"base/random.h\"\n#include \"styles/palette.h\"\n#include \"ui/effects/glare.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/rp_widget.h\"\n#include \"styles/style_basic.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Ui {\nnamespace {\n\nclass LoadingElement {\npublic:\n\tLoadingElement() = default;\n\n\t[[nodiscard]] virtual int height() const = 0;\n\tvirtual void paint(QPainter &p, int width) = 0;\n};\n\nclass LoadingText final : public LoadingElement {\npublic:\n\tLoadingText(const style::TextStyle &st);\n\n\t[[nodiscard]] int height() const override;\n\tvoid paint(QPainter &p, int width) override;\n\nprivate:\n\tconst style::TextStyle &_st;\n\n};\n\nLoadingText::LoadingText(const style::TextStyle &st) : _st(st) {\n}\n\nint LoadingText::height() const {\n\treturn _st.lineHeight;\n}\n\nvoid LoadingText::paint(QPainter &p, int width) {\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tp.setPen(Qt::NoPen);\n\n\tp.setBrush(st::windowBgOver);\n\tconst auto h = _st.font->ascent;\n\tp.drawRoundedRect(\n\t\t0,\n\t\theight() - h - (height() - _st.font->height),\n\t\twidth,\n\t\th,\n\t\th / 2,\n\t\th / 2);\n}\n\n[[nodiscard]] const style::PeerListItem &PeerListItemFromDialogRow(\n\t\trpl::lifetime &lifetime,\n\t\tconst style::DialogRow &st) {\n\tusing namespace style;\n\tconst auto button = lifetime.make_state<OutlineButton>(OutlineButton{\n\t\t.textBgOver = st::windowBgOver,\n\t});\n\tconst auto item = lifetime.make_state<PeerListItem>(PeerListItem{\n\t\t.height = st.height,\n\t\t.photoPosition = QPoint(st.padding.left(), st.padding.top()),\n\t\t.namePosition = QPoint(st.nameLeft, st.nameTop),\n\t\t.nameStyle = st::semiboldTextStyle,\n\t\t.statusPosition = QPoint(st.textLeft, st.textTop),\n\t\t.photoSize = st.photoSize,\n\t\t.button = { *button },\n\t});\n\treturn *item;\n}\n\nclass LoadingPeerListItem final : public LoadingElement {\npublic:\n\tLoadingPeerListItem(const style::PeerListItem &st) : _st(st) {\n\t}\n\tLoadingPeerListItem(const style::DialogRow &st)\n\t: _st(PeerListItemFromDialogRow(_lifetime, st)) {\n\t}\n\n\t[[nodiscard]] int height() const override {\n\t\treturn _st.height;\n\t}\n\tvoid paint(QPainter &p, int width) override {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tconst auto &style = _st.nameStyle;\n\t\tconst auto offset = -style.font->ascent\n\t\t\t- (style.lineHeight - style.font->height);\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(_st.button.textBgOver);\n\n\t\tp.drawEllipse(\n\t\t\t_st.photoPosition.x(),\n\t\t\t_st.photoPosition.y(),\n\t\t\t_st.photoSize,\n\t\t\t_st.photoSize);\n\n\t\tconstexpr auto kNameWidth = 60;\n\t\tconstexpr auto kStatusWidth = 100;\n\n\t\tconst auto h1 = st::semiboldTextStyle.font->ascent;\n\t\tp.drawRoundedRect(\n\t\t\t_st.namePosition.x(),\n\t\t\t_st.namePosition.y() + offset,\n\t\t\tkNameWidth,\n\t\t\th1,\n\t\t\th1 / 2,\n\t\t\th1 / 2);\n\n\t\t{\n\t\t\tconst auto h2 = st::defaultTextStyle.font->ascent;\n\t\t\tconst auto radius = h2 / 2;\n\t\t\tconst auto rect = QRect(\n\t\t\t\t_st.statusPosition.x(),\n\t\t\t\t_st.statusPosition.y() + offset,\n\t\t\t\tkStatusWidth,\n\t\t\t\th2);\n\t\t\tif (rect::bottom(rect) < height()) {\n\t\t\t\tp.drawRoundedRect(rect, radius, radius);\n\t\t\t}\n\t\t}\n\t}\n\nprivate:\n\trpl::lifetime _lifetime;\n\tconst style::PeerListItem &_st;\n\n};\n\ntemplate <typename Element, typename ...ElementArgs>\nobject_ptr<Ui::RpWidget> CreateLoadingElementWidget(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tint lines,\n\t\tQColor bg,\n\t\trpl::producer<bool> rtl,\n\t\tElementArgs &&...args) {\n\tauto widget = object_ptr<Ui::RpWidget>(parent);\n\tconst auto raw = widget.data();\n\n\tstruct State {\n\t\tGlareEffect glare;\n\t\tUi::Animations::Simple animation;\n\t\tint lastLineWidth = 0;\n\t\trpl::variable<bool> rtl;\n\t};\n\n\tconst auto state = widget->lifetime().make_state<State>();\n\tstate->rtl = std::move(rtl);\n\tstate->rtl.value(\n\t) | rpl::on_next([=] { raw->update(); }, raw->lifetime());\n\traw->resize(\n\t\traw->width(),\n\t\tElement(std::forward<ElementArgs>(args)...).height() * lines);\n\n\tconst auto draw = [=](QPainter &p) {\n\t\tauto loading = Element(std::forward<ElementArgs>(args)...);\n\t\tconst auto countRows = lines;\n\t\tfor (auto i = 0; i < countRows; i++) {\n\t\t\tconst auto w = (i == countRows - 1)\n\t\t\t\t? state->lastLineWidth\n\t\t\t\t: raw->width();\n\t\t\tloading.paint(p, w);\n\t\t\tp.translate(0, loading.height());\n\t\t}\n\t\tp.resetTransform();\n\n\t\tauto &_glare = state->glare;\n\t\tif (_glare.glare.birthTime) {\n\t\t\tconst auto progress = _glare.progress(crl::now());\n\t\t\tconst auto x = (-_glare.width)\n\t\t\t\t+ (raw->width() + _glare.width * 2) * progress;\n\t\t\tconst auto h = raw->height();\n\n\t\t\tp.drawTiledPixmap(x, 0, _glare.width, h, _glare.pixmap, 0, 0);\n\t\t}\n\t};\n\n\twidget->paintRequest(\n\t) | rpl::on_next([=](const QRect &r) {\n\t\tauto p = QPainter(raw);\n\t\tif (state->rtl.current()) {\n\t\t\tconst auto r = raw->rect();\n\t\t\tp.translate(r.center());\n\t\t\tp.scale(-1., 1.);\n\t\t\tp.translate(-r.center());\n\t\t}\n\t\tdraw(p);\n\t}, widget->lifetime());\n\n\tconstexpr auto kTimeout = crl::time(1000);\n\tconstexpr auto kDuration = crl::time(1000);\n\twidget->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tstate->glare.width = width;\n\t\tstate->glare.validate(\n\t\t\tbg,\n\t\t\t[=] { raw->update(); },\n\t\t\tkTimeout,\n\t\t\tkDuration);\n\t\tif (width) {\n\t\t\tstate->lastLineWidth = (width / 4) + base::RandomIndex(width / 2);\n\t\t}\n\t}, widget->lifetime());\n\n\treturn widget;\n}\n\n} // namespace\n\nobject_ptr<Ui::RpWidget> CreateLoadingTextWidget(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::TextStyle &st,\n\t\tint lines,\n\t\trpl::producer<bool> rtl) {\n\treturn CreateLoadingElementWidget<LoadingText>(\n\t\tparent,\n\t\tlines,\n\t\tst::dialogsBg->c,\n\t\tstd::move(rtl),\n\t\tst);\n}\n\nobject_ptr<Ui::RpWidget> CreateLoadingPeerListItemWidget(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::PeerListItem &st,\n\t\tint lines,\n\t\tstd::optional<QColor> bgOverride) {\n\treturn CreateLoadingElementWidget<LoadingPeerListItem>(\n\t\tparent,\n\t\tlines,\n\t\tbgOverride.value_or(st::dialogsBg->c),\n\t\trpl::single(false),\n\t\tst);\n}\n\nobject_ptr<Ui::RpWidget> CreateLoadingDialogRowWidget(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::DialogRow &st,\n\t\tint lines) {\n\treturn CreateLoadingElementWidget<LoadingPeerListItem>(\n\t\tparent,\n\t\tlines,\n\t\tst::dialogsBg->c,\n\t\trpl::single(false),\n\t\tst);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/loading_element.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\ntemplate <typename Object>\nclass object_ptr;\n\nnamespace style {\nstruct TextStyle;\nstruct PeerListItem;\nstruct DialogRow;\n} // namespace style\n\nnamespace Ui {\n\nclass RpWidget;\n\nobject_ptr<Ui::RpWidget> CreateLoadingTextWidget(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst style::TextStyle &st,\n\tint lines,\n\trpl::producer<bool> rtl);\n\nobject_ptr<Ui::RpWidget> CreateLoadingPeerListItemWidget(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst style::PeerListItem &st,\n\tint lines,\n\tstd::optional<QColor> bgOverride);\n\nobject_ptr<Ui::RpWidget> CreateLoadingDialogRowWidget(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst style::DialogRow &st,\n\tint lines);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/message_sending_animation_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nstruct MessageSendingAnimationFrom {\n\tenum class Type {\n\t\tNone,\n\t\tSticker,\n\t\tGif,\n\t\tEmoji,\n\t\tText,\n\t};\n\tType type = Type::None;\n\tstd::optional<MsgId> localId;\n\tQRect globalStartGeometry;\n\tQImage frame;\n\tbool crop = false;\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/message_sending_animation_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/message_sending_animation_controller.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_session.h\"\n#include \"history/history_item.h\"\n#include \"history/view/history_view_element.h\"\n#include \"history/view/media/history_view_media.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/effects/animation_value_f.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/text/text_isolated_emoji.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kSurroundingProgress = 0.5;\n\ninline float64 OffsetMid(int value, float64 min, float64 max = 1.) {\n\treturn ((value * max) - (value * min)) / 2.;\n}\n\nclass Content final : public RpWidget {\npublic:\n\tContent(\n\t\tnot_null<RpWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tMessageSendingAnimationFrom &&fromInfo,\n\t\tMessageSendingAnimationController::SendingInfoTo &&to);\n\n\t[[nodiscard]] rpl::producer<> destroyRequests() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n \tusing Context = Ui::ChatPaintContext;\n\n\tvoid createSurrounding();\n\tvoid createBubble();\n\tHistoryView::Element *maybeView() const;\n\tbool checkView(HistoryView::Element *currentView) const;\n\tvoid drawContent(Painter &p, float64 progress) const;\n\n\tconst not_null<Window::SessionController*> _controller;\n\tconst bool _crop;\n\tconst bool _isText;\n\tMessageSendingAnimationController::SendingInfoTo _toInfo;\n\tQRect _from;\n\tQPoint _to;\n\tQRect _innerContentRect;\n\n\tAnimations::Simple _animation;\n\tfloat64 _minScale = 0;\n\n\tstruct {\n\t\tbase::unique_qptr<Ui::RpWidget> widget;\n\t\tQPoint offsetFromContent;\n\t} _bubble;\n\tbase::unique_qptr<Ui::RpWidget> _surrounding;\n\n\trpl::event_stream<> _destroyRequests;\n\n};\n\nContent::Content(\n\tnot_null<RpWidget*> parent,\n\tnot_null<Window::SessionController*> controller,\n\tMessageSendingAnimationFrom &&fromInfo,\n\tMessageSendingAnimationController::SendingInfoTo &&to)\n: RpWidget(parent)\n, _controller(controller)\n, _crop(fromInfo.crop)\n, _isText(fromInfo.type == MessageSendingAnimationFrom::Type::Text)\n, _toInfo(std::move(to))\n, _from(parent->mapFromGlobal(fromInfo.globalStartGeometry))\n, _innerContentRect(_isText\n\t? [&] {\n\t\tconst auto g = maybeView()->innerGeometry();\n\t\treturn Rect(g.size()) + QMargins(0, g.top(), 0, 0);\n\t}()\n\t: maybeView()->media()->contentRectForReactions())\n, _minScale(!_isText\n\t? float64(_from.height()) / _innerContentRect.height()\n\t: 1.) {\n\tExpects(_toInfo.view != nullptr);\n\tExpects(_toInfo.paintContext != nullptr);\n\n\tshow();\n\tsetAttribute(Qt::WA_TransparentForMouseEvents);\n\traise();\n\n\tbase::take(\n\t\t_toInfo.globalEndTopLeft\n\t) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](const std::optional<QPoint> &p) {\n\t\tif (p) {\n\t\t\t_to = parent->mapFromGlobal(*p);\n\t\t} else {\n\t\t\t_destroyRequests.fire({});\n\t\t}\n\t}, lifetime());\n\n\t_controller->session().downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\tresize(_innerContentRect.size());\n\n\tconst auto innerGeometry = maybeView()->innerGeometry();\n\n\tauto animationCallback = [=](float64 value) {\n\t\tauto resultFrom = rect();\n\t\tif (_isText) {\n\t\t\tresultFrom.moveTo(\n\t\t\t\tstyle::RightToLeft()\n\t\t\t\t\t? (rect::right(_from) - resultFrom.width())\n\t\t\t\t\t: _from.left(),\n\t\t\t\t_from.y());\n\t\t\t// Calculating relative values for all possible cases\n\t\t\t// is too complex, so we manually set the position\n\t\t\t// for the simplest case.\n\t\t\tresultFrom.translate(st::messageSendingAnimationTextFromOffset);\n\t\t} else {\n\t\t\tresultFrom.moveCenter(_from.center());\n\t\t}\n\n\t\tconst auto resultTo = _isText\n\t\t\t? _to + innerGeometry.topLeft()\n\t\t\t: _to + innerGeometry.topLeft() + _innerContentRect.topLeft();\n\t\tconst auto xEase = anim::easeOutQuint(1.0, value);\n\t\tconst auto x = anim::interpolate(resultFrom.x(), resultTo.x(), xEase);\n\t\tconst auto y = anim::interpolate(resultFrom.y(), resultTo.y(), value);\n\t\tif (!_isText) {\n\t\t\t// Text-only messages are drawing only in _bubble.widget.\n\t\t\tmoveToLeft(x, y);\n\t\t\tupdate();\n\t\t} else {\n\t\t\tsetUpdatesEnabled(false);\n\t\t}\n\n\t\tif ((value > kSurroundingProgress || _isText)\n\t\t\t&& !_surrounding\n\t\t\t&& !_bubble.widget) {\n\t\t\tconst auto currentView = maybeView();\n\t\t\tif (!checkView(currentView)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (_isText || currentView->hasBubble()) {\n\t\t\t\tcreateBubble();\n\t\t\t} else {\n\t\t\t\tcreateSurrounding();\n\t\t\t}\n\t\t}\n\t\tif (_surrounding) {\n\t\t\t_surrounding->moveToLeft(\n\t\t\t\tx - _innerContentRect.x(),\n\t\t\t\ty - _innerContentRect.y());\n\t\t}\n\t\tif (_bubble.widget) {\n\t\t\t_bubble.widget->moveToLeft(\n\t\t\t\tx - _bubble.offsetFromContent.x(),\n\t\t\t\ty - _bubble.offsetFromContent.y());\n\t\t}\n\n\t\tif (value == 1.) {\n\t\t\tconst auto currentView = maybeView();\n\t\t\tif (!checkView(currentView)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto controller = _controller;\n\t\t\t_destroyRequests.fire({});\n\t\t\tcontroller->session().data().requestViewRepaint(currentView);\n\t\t}\n\t};\n\tanimationCallback(0.);\n\t_animation.start(\n\t\tstd::move(animationCallback),\n\t\t0.,\n\t\t1.,\n\t\tst::itemRevealDuration,\n\t\tanim::easeOutQuint);\n}\n\nHistoryView::Element *Content::maybeView() const {\n\treturn _toInfo.view();\n}\n\nbool Content::checkView(HistoryView::Element *currentView) const {\n\tif (!currentView) {\n\t\t_destroyRequests.fire({});\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nvoid Content::paintEvent(QPaintEvent *e) {\n\tconst auto progress = _animation.value(_animation.animating() ? 0. : 1.);\n\n\tif (!_crop) {\n\t\tPainter p(this);\n\t\tp.fillRect(e->rect(), Qt::transparent);\n\t\tdrawContent(p, progress);\n\t} else {\n\t\t// Use QImage to make CompositionMode_Clear work.\n\t\tauto image = QImage(\n\t\t\tsize() * style::DevicePixelRatio(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\t\timage.fill(Qt::transparent);\n\n\t\tconst auto scaledFromSize = _from.size().scaled(\n\t\t\t_innerContentRect.size(),\n\t\t\tQt::KeepAspectRatio);\n\t\tconst auto cropW = std::ceil(\n\t\t\t(_innerContentRect.width() - scaledFromSize.width())\n\t\t\t\t/ 2.\n\t\t\t\t* (1. - std::clamp(progress / kSurroundingProgress, 0., 1.)));\n\n\t\t{\n\t\t\tPainter p(&image);\n\t\t\tdrawContent(p, progress);\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_Clear);\n\t\t\tp.fillRect(\n\t\t\t\tQRect(0, 0, cropW, _innerContentRect.height()),\n\t\t\t\tQt::black);\n\t\t\tp.fillRect(\n\t\t\t\tQRect(\n\t\t\t\t\t_innerContentRect.width() - cropW,\n\t\t\t\t\t0,\n\t\t\t\t\tcropW,\n\t\t\t\t\t_innerContentRect.height()),\n\t\t\t\tQt::black);\n\t\t}\n\n\t\tauto p = QPainter(this);\n\t\tp.drawImage(QPoint(), std::move(image));\n\t}\n}\n\nvoid Content::drawContent(Painter &p, float64 progress) const {\n\tconst auto scale = anim::interpolateF(_minScale, 1., progress);\n\n\tp.translate(\n\t\t(1 - progress) * OffsetMid(width(), _minScale),\n\t\t(1 - progress) * OffsetMid(height(), _minScale));\n\tp.scale(scale, scale);\n\n\tconst auto currentView = maybeView();\n\tif (!checkView(currentView)) {\n\t\treturn;\n\t}\n\n\tauto context = _toInfo.paintContext();\n\tcontext.skipDrawingParts = Context::SkipDrawingParts::Surrounding;\n\tcontext.outbg = currentView->hasOutLayout();\n\tp.translate(-_innerContentRect.topLeft());\n\tcurrentView->media()->draw(p, context);\n}\n\nrpl::producer<> Content::destroyRequests() const {\n\treturn _destroyRequests.events();\n}\n\nvoid Content::createSurrounding() {\n\t_surrounding = base::make_unique_q<Ui::RpWidget>(parentWidget());\n\t_surrounding->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto currentView = maybeView();\n\tif (!checkView(currentView)) {\n\t\treturn;\n\t}\n\tconst auto surroundingSize = currentView->innerGeometry().size();\n\tconst auto offset = _innerContentRect.topLeft();\n\n\t_surrounding->resize(surroundingSize);\n\t_surrounding->show();\n\n\t// Do not raise.\n\t_surrounding->stackUnder(this);\n\tstackUnder(_surrounding.get());\n\n\t_surrounding->paintRequest(\n\t) | rpl::on_next([=, size = surroundingSize](const QRect &r) {\n\t\tPainter p(_surrounding);\n\n\t\tp.fillRect(r, Qt::transparent);\n\n\t\tconst auto progress = _animation.value(0.);\n\t\tconst auto revProgress = 1. - progress;\n\n\t\tconst auto divider = 1. - kSurroundingProgress;\n\t\tconst auto alpha = (divider - revProgress) / divider;\n\t\tp.setOpacity(alpha);\n\n\t\tconst auto scale = anim::interpolateF(_minScale, 1., progress);\n\n\t\tp.translate(\n\t\t\trevProgress * OffsetMid(size.width() + offset.x(), _minScale),\n\t\t\trevProgress * OffsetMid(size.height() + offset.y(), _minScale));\n\t\tp.scale(scale, scale);\n\n\t\tconst auto currentView = maybeView();\n\t\tif (!checkView(currentView)) {\n\t\t\treturn;\n\t\t}\n\n\t\tauto context = _toInfo.paintContext();\n\t\tcontext.skipDrawingParts = Context::SkipDrawingParts::Content;\n\t\tcontext.outbg = currentView->hasOutLayout();\n\n\t\tcurrentView->media()->draw(p, context);\n\t}, _surrounding->lifetime());\n}\n\nvoid Content::createBubble() {\n\t_bubble.widget = base::make_unique_q<Ui::RpWidget>(parentWidget());\n\t_bubble.widget->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\tconst auto currentView = maybeView();\n\tif (!checkView(currentView)) {\n\t\treturn;\n\t}\n\tconst auto innerGeometry = currentView->innerGeometry();\n\n\tconst auto tailWidth = st::historyBubbleTailOutLeft.width();\n\t_bubble.offsetFromContent = QPoint(\n\t\t(currentView->hasOutLayout()\n\t\t\t&& (currentView->delegate()->elementChatMode()\n\t\t\t\t!= HistoryView::ElementChatMode::Wide))\n\t\t\t? 0\n\t\t\t: tailWidth,\n\t\tinnerGeometry.y());\n\n\tconst auto scaleOffset = QPoint(0, innerGeometry.y());\n\tconst auto paintOffsetLeft = innerGeometry.x()\n\t\t- _bubble.offsetFromContent.x();\n\n\tconst auto hasCommentsButton = currentView->data()->repliesAreComments()\n\t\t|| currentView->data()->externalReply();\n\t_bubble.widget->resize(innerGeometry.size()\n\t\t+ QSize(\n\t\t\t(currentView->hasOutLayout() ? tailWidth : 0)\n\t\t\t\t+ (_isText && currentView->data()->isPost()\n\t\t\t\t\t? rect::m::sum::h(st::msgPadding)\n\t\t\t\t\t\t+ st::historyFastShareSize\n\t\t\t\t\t: 0),\n\t\t\t(hasCommentsButton || _isText) ? innerGeometry.y() : 0));\n\t_bubble.widget->show();\n\n\t_bubble.widget->stackUnder(this);\n\n\t_bubble.widget->paintRequest(\n\t) | rpl::on_next([=, raw = _bubble.widget.get()](const QRect &r) {\n\t\tauto p = Painter(raw);\n\n\t\tp.fillRect(r, Qt::transparent);\n\n\t\tconst auto progress = _animation.value(0.);\n\t\tconst auto revProgress = 1. - progress;\n\n\t\tconst auto divider = 1. - (_isText ? 0. : kSurroundingProgress);\n\t\tconst auto alpha = (divider - revProgress) / divider;\n\t\tp.setOpacity(alpha);\n\n\t \tconst auto scale = anim::interpolateF(_minScale, 1., progress);\n\n\t \tp.translate(\n\t \t\trevProgress * OffsetMid(width() + scaleOffset.x(), _minScale),\n\t \t\trevProgress * OffsetMid(height() + scaleOffset.y(), _minScale));\n\t \tp.scale(scale, scale);\n\n\t\tconst auto currentView = maybeView();\n\t\tif (!checkView(currentView)) {\n\t\t\treturn;\n\t\t}\n\n\t\tauto context = _toInfo.paintContext();\n\t\tcontext.skipDrawingParts = Context::SkipDrawingParts::Content;\n\t\tcontext.outbg = currentView->hasOutLayout();\n\n\t\tconst auto diff = context.viewport.height() - raw->height();\n\t\tauto bottom = anim::interpolate(_from.y(), _to.y(), progress);\n\t\tif (bottom > diff) {\n\t\t\tbottom = diff;\n\t\t}\n\t\tif (bottom < raw->height()) {\n\t\t\tbottom = raw->height();\n\t\t}\n\t\tcontext.translate(paintOffsetLeft, -context.viewport.y() - bottom);\n\t\tp.translate(-paintOffsetLeft, 0);\n\n\t\tcurrentView->draw(p, context);\n\t\tif (_isText) {\n\t\t\tp.setOpacity(1.);\n\t\t\tp.setClipRect(QRect(0, 0, 0, 0));\n\t\t\tcontext.skipDrawingParts = Context::SkipDrawingParts::Bubble;\n\t\t\tcurrentView->draw(p, context);\n\t\t}\n\t}, _bubble.widget->lifetime());\n}\n\n} // namespace\n\nMessageSendingAnimationController::MessageSendingAnimationController(\n\tnot_null<Window::SessionController*> controller)\n: _controller(controller) {\n\tsubscribeToDestructions();\n}\n\nvoid MessageSendingAnimationController::subscribeToDestructions() {\n\t_controller->session().data().itemIdChanged(\n\t) | rpl::on_next([=](Data::Session::IdChange change) {\n\t\t_itemSendPending.remove(change.oldId);\n\t}, _lifetime);\n\n\t_controller->session().data().itemRemoved(\n\t) | rpl::on_next([=](not_null<const HistoryItem*> item) {\n\t\t_itemSendPending.remove(item->id);\n\t\t_processing.remove(item);\n\t}, _lifetime);\n}\n\nvoid MessageSendingAnimationController::appendSending(\n\t\tMessageSendingAnimationFrom from) {\n\tif (anim::Disabled()) {\n\t\treturn;\n\t}\n\tif (from.localId) {\n\t\t_itemSendPending[*from.localId] = std::move(from);\n\t}\n}\n\nvoid MessageSendingAnimationController::startAnimation(SendingInfoTo &&to) {\n\tif (anim::Disabled()\n\t\t|| PowerSaving::On(PowerSaving::Flag::kChatEffects)) {\n\t\treturn;\n\t}\n\tconst auto container = _controller->content();\n\tconst auto view = to.view();\n\tconst auto item = view->data();\n\n\tconst auto it = _itemSendPending.find(item->fullId().msg);\n\tif (it == end(_itemSendPending)) {\n\t\treturn;\n\t}\n\tauto from = std::move(it->second);\n\t_itemSendPending.erase(it);\n\n\tif (view->isolatedEmoji() || view->onlyCustomEmoji()) {\n\t\treturn;\n\t}\n\n\tauto content = base::make_unique_q<Content>(\n\t\tcontainer,\n\t\t_controller,\n\t\tstd::move(from),\n\t\tstd::move(to));\n\n\tcontent->destroyRequests(\n\t) | rpl::on_next([=] {\n\t\t_processing.erase(item);\n\t}, content->lifetime());\n\n\t_processing.emplace(item, std::move(content));\n}\n\nbool MessageSendingAnimationController::hasAnimatedMessage(\n\t\tnot_null<HistoryItem*> item) const {\n\treturn _processing.contains(item);\n}\n\nvoid MessageSendingAnimationController::clear() {\n\t_itemSendPending.clear();\n\t_processing.clear();\n}\n\nbool MessageSendingAnimationController::checkExpectedType(\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto it = _itemSendPending.find(item->id);\n\tif (it == end(_itemSendPending)) {\n\t\treturn false;\n\t}\n\tconst auto type = it->second.type;\n\tconst auto isSticker = type == MessageSendingAnimationFrom::Type::Sticker;\n\tconst auto isGif = type == MessageSendingAnimationFrom::Type::Gif;\n\tconst auto isText = type == MessageSendingAnimationFrom::Type::Text;\n\tif (isSticker || isGif) {\n\t\tif (item->emptyText()) {\n\t\t\tif (const auto media = item->media()) {\n\t\t\t\tif (const auto document = media->document()) {\n\t\t\t\t\tif ((isGif && document->isGifv())\n\t\t\t\t\t\t|| (isSticker && document->sticker())) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if (isText && !item->media()) {\n\t\treturn true;\n\t}\n\t_itemSendPending.erase(it);\n\treturn false;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/message_sending_animation_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n#include \"ui/effects/message_sending_animation_common.h\"\n\nnamespace HistoryView {\nclass Element;\n} // namespace HistoryView\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\n\nclass RpWidget;\nstruct ChatPaintContext;\n\nclass MessageSendingAnimationController final {\npublic:\n\texplicit MessageSendingAnimationController(\n\t\tnot_null<Window::SessionController*> controller);\n\n\tstruct SendingInfoTo {\n\t\trpl::producer<std::optional<QPoint>> globalEndTopLeft;\n\t\tFn<HistoryView::Element*()> view;\n\t\tFn<Ui::ChatPaintContext()> paintContext;\n\t};\n\n\tvoid appendSending(MessageSendingAnimationFrom from);\n\tvoid startAnimation(SendingInfoTo &&to);\n\n\t[[nodiscard]] bool hasAnimatedMessage(not_null<HistoryItem*> item) const;\n\t[[nodiscard]] bool checkExpectedType(not_null<HistoryItem*> item);\n\n\tvoid clear();\n\nprivate:\n\tvoid subscribeToDestructions();\n\n\tconst not_null<Window::SessionController*> _controller;\n\tbase::flat_map<MsgId, MessageSendingAnimationFrom> _itemSendPending;\n\n\tbase::flat_map<\n\t\tnot_null<HistoryItem*>,\n\t\tbase::unique_qptr<RpWidget>> _processing;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/ministar_particles.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/ministar_particles.h\"\n\n#include \"ui/painter.h\"\n#include \"ui/color_int_conversion.h\"\n#include \"base/random.h\"\n\n#include <QtGui/QPainterPath>\n#include <QtMath>\n\nnamespace Ui {\n\nStarParticles::StarParticles(Type type, int count, int size)\n: _type(type)\n, _count(count)\n, _starSize(size) {\n\tgenerate();\n}\n\nvoid StarParticles::setSpeed(float speed) {\n\t_speed = speed;\n}\n\nvoid StarParticles::setVisible(float visible) {\n\t_visible = visible;\n}\n\nvoid StarParticles::setColor(const QColor &color) {\n\tsetColors({ color });\n}\n\nvoid StarParticles::setColors(std::vector<QColor> colors) {\n\tExpects(!colors.empty());\n\n\tconst auto was = int(_colors.size());\n\t_colors = std::move(colors);\n\tconst auto now = int(_colors.size());\n\n\t_paintedCaches.clear();\n\n\tif (now != was) {\n\t\tif (now > 1) {\n\t\t\tauto buffered = base::BufferedRandom<uint32>(_particles.size());\n\t\t\tfor (auto &p : _particles) {\n\t\t\t\tp.colorIndex = base::RandomIndex(now, buffered);\n\t\t\t}\n\t\t} else {\n\t\t\tfor (auto &p : _particles) {\n\t\t\t\tp.colorIndex = 0;\n\t\t\t}\n\t\t}\n\n\t}\n}\n\nQImage StarParticles::generateStarCache(int size, QColor color) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto image = QImage(\n\t\tsize * ratio,\n\t\tsize * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(ratio);\n\timage.fill(Qt::transparent);\n\n\tauto p = QPainter(&image);\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setBrush(color);\n\tp.setPen(Qt::NoPen);\n\n\tauto star = QPainterPath();\n\tconst auto center = size / 2.;\n\tconst auto outerRadius = size / 2.;\n\tconst auto innerRadius = size / 5.;\n\tconst auto points = 4;\n\n\tfor (auto i = 0; i < points * 2; ++i) {\n\t\tconst auto angle = M_PI * i / points - M_PI / 2;\n\t\tconst auto radius = (i % 2 == 0) ? outerRadius : innerRadius;\n\t\tconst auto x = center + radius * std::cos(angle);\n\t\tconst auto y = center + radius * std::sin(angle);\n\t\tif (i == 0) {\n\t\t\tstar.moveTo(x, y);\n\t\t} else {\n\t\t\tstar.lineTo(x, y);\n\t\t}\n\t}\n\tstar.closeSubpath();\n\tp.drawPath(star);\n\n\treturn image;\n}\n\nvoid StarParticles::generate() {\n\t_particles.clear();\n\t_particles.reserve(_count);\n\n\tconstexpr auto kRandomPerParticle = 13;\n\tauto random = bytes::vector(_count * kRandomPerParticle);\n\tbase::RandomFill(random.data(), random.size());\n\tauto idx = 0;\n\tconst auto next = [&] { return uchar(random[idx++]) / 255.; };\n\n\tconst auto colors = int(_colors.size());\n\n\tfor (auto i = 0; i < _count; ++i) {\n\t\tauto p = Particle();\n\t\tp.x = next();\n\t\tp.y = next();\n\t\tp.size = 0.3 + next() * 0.7;\n\t\tp.rotation = next() * 360.;\n\t\tp.vrotation = (next() - 0.5) * 180.;\n\t\tp.alpha = 0.3 + next() * 0.7;\n\n\t\tif (_type == Type::Right) {\n\t\t\tp.vx = 0.5 + next() * 1.5;\n\t\t\tp.vy = (next() - 0.5) * 1.5;\n\t\t} else if (_type == Type::Radial) {\n\t\t\tconst auto angle = next() * 2 * M_PI;\n\t\t\tconst auto speed = 0.5 + next();\n\t\t\tp.vx = std::cos(angle) * speed;\n\t\t\tp.vy = std::sin(angle) * speed;\n\t\t} else {\n\t\t\tconst auto angle = next() * 2 * M_PI;\n\t\t\tconst auto speed = 0.3 + next() * 0.5;\n\t\t\tp.vx = -std::cos(angle) * speed;\n\t\t\tp.vy = -std::sin(angle) * speed;\n\t\t}\n\n\t\tif (colors > 1) {\n\t\t\tp.colorIndex = int(base::SafeRound(next() * (colors - 1)));\n\t\t}\n\n\t\t_particles.push_back(p);\n\t}\n}\n\nvoid StarParticles::paint(\n\t\tQPainter &p,\n\t\tconst QRect &rect,\n\t\tcrl::time now,\n\t\tbool paused) {\n\tif (_lastTime == 0) {\n\t\t_lastTime = now;\n\t\treturn;\n\t}\n\n\tif (paused) {\n\t\tif (!_pausedAt) {\n\t\t\t_pausedAt = now;\n\t\t}\n\t\tnow = _pausedAt - _pauseOffset;\n\t} else {\n\t\tif (_pausedAt) {\n\t\t\t_pauseOffset += now - _pausedAt;\n\t\t\t_pausedAt = 0;\n\t\t}\n\t\tnow = now - _pauseOffset;\n\t}\n\n\tconst auto validate = [&](int colorIndex) {\n\t\tExpects(colorIndex >= 0 && colorIndex < _colors.size());\n\n\t\tif (_paintedCaches.empty()) {\n\t\t\t_paintedCaches.resize(_colors.size());\n\t\t} else if (const auto result = _paintedCaches[colorIndex]\n\t\t\t; !result.isNull()) {\n\t\t\treturn result;\n\t\t}\n\t\tconst auto &color = _colors[colorIndex];\n\t\tconst auto colorKey = color.rgba();\n\t\tconst auto i = _starCache.find(colorKey);\n\t\tconst auto &cache = (i != end(_starCache))\n\t\t\t? i->second\n\t\t\t: _starCache.emplace(\n\t\t\t\tcolorKey,\n\t\t\t\tgenerateStarCache(_starSize, color)).first->second;\n\t\t_paintedCaches[colorIndex] = cache;\n\t\treturn cache;\n\t};\n\n\tconst auto dt = (now - _lastTime) / 1000.;\n\t_lastTime = now;\n\n\tconst auto visibleCount = int(_count * _visible);\n\tfor (auto i = 0; i < visibleCount; ++i) {\n\t\tauto &particle = _particles[i];\n\n\t\tparticle.x += particle.vx * _speed * dt;\n\t\tparticle.y += particle.vy * _speed * dt;\n\t\tparticle.rotation += particle.vrotation * dt;\n\n\t\tif (particle.x < -0.1 || particle.x > 1.1\n\t\t\t|| particle.y < -0.1 || particle.y > 1.1) {\n\t\t\tauto random = bytes::vector(1);\n\t\t\tbase::RandomFill(random.data(), random.size());\n\t\t\tparticle.x = (_type == Type::Right) ? 0. : 0.5;\n\t\t\tparticle.y = (_type == Type::Right)\n\t\t\t\t? (uchar(random[0]) / 255.)\n\t\t\t\t: 0.5;\n\t\t}\n\n\t\tconst auto x = rect.x() + particle.x * rect.width();\n\t\tconst auto y = rect.y() + particle.y * rect.height();\n\t\tconst auto cache = validate(particle.colorIndex);\n\t\tconst auto size = cache.width()\n\t\t\t/ cache.devicePixelRatio() * particle.size;\n\n\t\tp.save();\n\t\tp.setOpacity(particle.alpha);\n\t\tp.translate(x, y);\n\t\tp.rotate(particle.rotation);\n\t\tp.drawImage(\n\t\t\tQRectF(-size / 2, -size / 2, size, size),\n\t\t\tcache);\n\t\tp.restore();\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/ministar_particles.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\n\nclass StarParticles final {\npublic:\n\tenum class Type {\n\t\tRight,\n\t\tRadial,\n\t\tRadialInside,\n\t};\n\n\tStarParticles(Type type, int count, int size);\n\n\tvoid setSpeed(float speed);\n\tvoid setVisible(float visible);\n\tvoid setColor(const QColor &color);\n\tvoid setColors(std::vector<QColor> colors);\n\tvoid paint(QPainter &p, const QRect &rect, crl::time now, bool paused);\n\nprivate:\n\tstruct Particle {\n\t\tint colorIndex = 0;\n\t\tfloat x = 0.;\n\t\tfloat y = 0.;\n\t\tfloat vx = 0.;\n\t\tfloat vy = 0.;\n\t\tfloat rotation = 0.;\n\t\tfloat vrotation = 0.;\n\t\tfloat size = 0.;\n\t\tfloat alpha = 0.;\n\t\tcrl::time born = 0;\n\t};\n\n\tvoid generate();\n\t[[nodiscard]] QImage generateStarCache(int size, QColor color);\n\n\tType _type;\n\tint _count = 0;\n\tint _starSize = 0;\n\tfloat _speed = 1.f;\n\tfloat _visible = 1.f;\n\tstd::vector<QColor> _colors = { QColor(255, 200, 70) };\n\tstd::vector<QImage> _paintedCaches;\n\tstd::vector<Particle> _particles;\n\tbase::flat_map<QRgb, QImage> _starCache;\n\tcrl::time _lastTime = 0;\n\n\tcrl::time _pausedAt = 0;\n\tcrl::time _pauseOffset = 0;\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/outline_segments.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/outline_segments.h\"\n\n#include <QtCore/QtMath> // M_PI\n\nnamespace Ui {\nnamespace {\n\nconstexpr std::array<float, 90> Radiuses = {{ 1, 1, 1, 1.001, 1.002, 1.003, 1.005, 1.007, 1.009, 1.012, 1.015, 1.018, 1.022, 1.026, 1.03, 1.035, 1.04, 1.045, 1.051, 1.057, 1.064, 1.071, 1.078, 1.086, 1.094, 1.103, 1.11201, 1.12201, 1.13201, 1.14301, 1.15401, 1.16601, 1.17801, 1.19201, 1.20601, 1.22001, 1.23501, 1.25101, 1.26801, 1.28601, 1.30501, 1.32402, 1.34502, 1.36602, 1.38902, 1.41302, 1.38902, 1.36602, 1.34502, 1.32402, 1.30501, 1.28601, 1.26801, 1.25101, 1.23501, 1.22001, 1.20601, 1.19201, 1.17801, 1.16601, 1.15401, 1.14301, 1.13201, 1.12201, 1.11201, 1.103, 1.094, 1.086, 1.078, 1.071, 1.064, 1.057, 1.051, 1.045, 1.04, 1.035, 1.03, 1.026, 1.022, 1.018, 1.015, 1.012, 1.009, 1.007, 1.005, 1.003, 1.002, 1.001, 1, 1 }};\nstruct Corner {\n\tshort xSign = 0;\n\tshort ySign = 0;\n\tint angle = 0;\n};\nconstexpr std::array<Corner, 4> Corners = {{\n\t{ -1, -1, 225 },\n\t{ -1 ,1, 315 },\n\t{ 1, 1, 405 },\n\t{ 1, -1, 495 },\n}};\n\nvoid DrawArcFork(\n\t\tQPainter &p,\n\t\tconst QRectF &oval,\n\t\tint startAngle,\n\t\tint spanAngle) {\n\tstartAngle /= 16;\n\tspanAngle /= 16;\n\tstartAngle += 90;\n\tconst auto a = float64(startAngle);\n\tconst auto b = float64(startAngle + spanAngle);\n\tconst auto r = oval.width() / 2.;\n\n\tconst auto aRad = M_PI / 2. - a * M_PI / 180.0;\n\tconst auto bRad = M_PI / 2. - b * M_PI / 180.0;\n\n\tconst auto aR = Radiuses[(int)(a - ((int)(a / 90.0) * 90))] * r;\n\tconst auto aX = aR * std::cos(aRad);\n\tconst auto aY = aR * std::sin(aRad);\n\n\tconst auto bR = Radiuses[(int)(b - ((int)(b / 90.0) * 90))] * r;\n\tconst auto bX = bR * std::cos(bRad);\n\tconst auto bY = bR * std::sin(bRad);\n\n\tauto path = QPainterPath();\n\n\tconst auto shiftX = oval.x() + r;\n\tconst auto shiftY = oval.y() + r;\n\n\tpath.moveTo(shiftX + aX, shiftY + aY);\n\tfor (const auto &corner : Corners) {\n\t\tif (corner.angle > a && corner.angle < b) {\n\t\t\tpath.lineTo(shiftX + corner.xSign * r, shiftY + corner.ySign * r);\n\t\t}\n\t}\n\tpath.lineTo(shiftX + bX, shiftY + bY);\n\n\tp.drawPath(path);\n}\n\n} // namespace\n\nvoid PaintOutlineSegments(\n\t\tQPainter &p,\n\t\tQRectF ellipse,\n\t\tconst std::vector<OutlineSegment> &segments,\n\t\tfloat64 fromFullProgress) {\n\tExpects(!segments.empty());\n\n\tp.setBrush(Qt::NoBrush);\n\tconst auto count = std::min(int(segments.size()), kOutlineSegmentsMax);\n\tif (count == 1) {\n\t\tp.setPen(QPen(segments.front().brush, segments.front().width));\n\t\tif (style::SquareUserpics()) {\n\t\tp.drawRect(ellipse);\n\t\t} else {\n\t\tp.drawEllipse(ellipse);\n\t\t}\n\t\treturn;\n\t}\n\tconst auto small = 160;\n\tconst auto full = arc::kFullLength;\n\tconst auto separator = (full > 1.1 * small * count)\n\t\t? small\n\t\t: (full / (count * 1.1));\n\tconst auto left = full - (separator * count);\n\tconst auto length = left / float64(count);\n\tconst auto spin = separator * (1. - fromFullProgress);\n\n\tauto start = 0. + (arc::kQuarterLength + (separator / 2)) + (3. * spin);\n\tauto pen = QPen(\n\t\tsegments.back().brush,\n\t\tsegments.back().width,\n\t\tQt::SolidLine,\n\t\tQt::RoundCap);\n\tp.setPen(pen);\n\tfor (auto i = 0; i != count;) {\n\t\tconst auto &segment = segments[count - (++i)];\n\t\tif (!segment.width) {\n\t\t\tstart += length + separator;\n\t\t\tcontinue;\n\t\t} else if (pen.brush() != segment.brush\n\t\t\t|| pen.widthF() != segment.width) {\n\t\t\tpen = QPen(\n\t\t\t\tsegment.brush,\n\t\t\t\tsegment.width,\n\t\t\t\tQt::SolidLine,\n\t\t\t\tQt::RoundCap);\n\t\t\tp.setPen(pen);\n\t\t}\n\t\tconst auto from = int(base::SafeRound(start));\n\t\tconst auto till = start + length;\n\t\tauto added = spin;\n\t\tfor (; i != count;) {\n\t\t\tstart += length + separator;\n\t\t\tconst auto &next = segments[count - (++i)];\n\t\t\tif (next.width) {\n\t\t\t\t--i;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tadded += (separator + length) * (1. - fromFullProgress);\n\t\t}\n\t\tif (style::SquareUserpics()) {\n\t\tDrawArcFork(p, ellipse, from, int(base::SafeRound(till + added)) - from);\n\t\t} else {\n\t\tp.drawArc(ellipse, from, int(base::SafeRound(till + added)) - from);\n\t\t}\n\t}\n}\n\nvoid PaintOutlineSegments(\n\t\tQPainter &p,\n\t\tQRectF rect,\n\t\tfloat64 radius,\n\t\tconst std::vector<OutlineSegment> &segments) {\n\tExpects(!segments.empty());\n\n\tp.setBrush(Qt::NoBrush);\n\tconst auto count = std::min(int(segments.size()), kOutlineSegmentsMax);\n\tif (count == 1 || true) {\n\t\tp.setPen(QPen(segments.back().brush, segments.back().width));\n\t\tp.drawRoundedRect(rect, radius, radius);\n\t\treturn;\n\t}\n}\n\nQLinearGradient UnreadStoryOutlineGradient(\n\t\tQRectF rect,\n\t\tconst QColor &c1,\n\t\tconst QColor &c2) {\n\tauto result = QLinearGradient(rect.topRight(), rect.bottomLeft());\n\tresult.setStops({ { 0., c1 }, { 1., c2 } });\n\treturn result;\n}\n\nQLinearGradient UnreadStoryOutlineGradient(QRectF rect) {\n\treturn UnreadStoryOutlineGradient(\n\t\tstd::move(rect),\n\t\tst::groupCallLive1->c,\n\t\tst::groupCallMuted1->c);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/outline_segments.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\ninline constexpr auto kOutlineSegmentsMax = 50;\n\nstruct OutlineSegment {\n\tQBrush brush;\n\tfloat64 width = 0.;\n};\n\nvoid PaintOutlineSegments(\n\tQPainter &p,\n\tQRectF ellipse,\n\tconst std::vector<OutlineSegment> &segments,\n\tfloat64 fromFullProgress = 1.);\n\nvoid PaintOutlineSegments(\n\tQPainter &p,\n\tQRectF rect,\n\tfloat64 radius,\n\tconst std::vector<OutlineSegment> &segments);\n\n[[nodiscard]] QLinearGradient UnreadStoryOutlineGradient(\n\tQRectF rect,\n\tconst QColor &c1,\n\tconst QColor &c2);\n[[nodiscard]] QLinearGradient UnreadStoryOutlineGradient(QRectF rect = {});\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/premium.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\nusing \"boxes/boxes.style\";\nusing \"ui/widgets/widgets.style\";\nusing \"ui/layers/layers.style\";\n\nPremiumLimits {\n\tboxLabel: FlatLabel;\n\tnonPremiumBg: color;\n\tnonPremiumFg: color;\n\tgradientFromLeft: bool;\n}\nPremiumBubble {\n\twidthLimit: pixels;\n\theight: pixels;\n\tpadding: margins;\n\tskip: pixels;\n\tpenWidth: pixels;\n\ttextSkip: pixels;\n\ttailSize: size;\n\tfont: font;\n\tadditionalStyle: TextStyle;\n\tadditionalSkip: pixels;\n\tsubtextStyle: TextStyle;\n\tsubtextPadding: margins;\n}\nPremiumCover {\n\tbg: color;\n\tstarSize: size;\n\tstarTopSkip: pixels;\n\ttitlePadding: margins;\n\ttitleFont: font;\n\ttitleFg: color;\n\tabout: FlatLabel;\n\taboutMaxWidth: pixels;\n\tadditionalShadowForDarkThemes: bool;\n}\nComposePremiumRequired {\n\tlabel: FlatLabel;\n\tbutton: RoundButton;\n\tbuttonSkip: pixels;\n\tbuttonTop: pixels;\n\tposition: point;\n\ticon: icon;\n}\n\npremiumAboutTextStyle: TextStyle(defaultTextStyle) {\n\tfont: font(12px);\n\tlinkUnderline: kLinkUnderlineAlways;\n\tlineHeight: 18px;\n}\ndefaultPremiumCover: PremiumCover {\n\tbg: boxBg;\n\tstarSize: size(84px, 81px);\n\tstarTopSkip: 37px;\n\ttitlePadding: margins(0px, 18px, 0px, 11px);\n\ttitleFont: boxTitleFont;\n\ttitleFg: windowBoldFg;\n\tabout: FlatLabel(defaultFlatLabel) {\n\t\tstyle: premiumAboutTextStyle;\n\t\tpalette: TextPalette(defaultTextPalette) {\n\t\t\tlinkFg: premiumButtonFg;\n\t\t}\n\t\talign: align(top);\n\t\ttextFg: premiumButtonFg;\n\t\tminWidth: 190px;\n\t}\n\taboutMaxWidth: 0px;\n\tadditionalShadowForDarkThemes: true;\n}\nuserPremiumCoverAbout: FlatLabel(boxDividerLabel) {\n\tstyle: premiumAboutTextStyle;\n\tminWidth: 315px;\n\tmaxHeight: 0px;\n\talign: align(top);\n}\nuserPremiumCover: PremiumCover(defaultPremiumCover) {\n\tabout: userPremiumCoverAbout;\n}\n\ndefaultPremiumBoxLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 220px;\n\talign: align(topleft);\n\tstyle: TextStyle(boxTextStyle) {\n\t\tlineHeight: 22px;\n\t}\n}\ndefaultPremiumLimits: PremiumLimits {\n\tboxLabel: defaultPremiumBoxLabel;\n\tnonPremiumBg: windowBgOver;\n\tnonPremiumFg: windowFg;\n\tgradientFromLeft: false;\n}\n\n// Preview.\npremiumPreviewBox: Box(defaultBox) {\n\tbuttonPadding: margins(18px, 18px, 18px, 18px);\n\tbuttonHeight: 44px;\n\tbutton: RoundButton(defaultActiveButton) {\n\t\theight: 44px;\n\t\ttextTop: 12px;\n\t\tstyle: semiboldTextStyle;\n\t}\n}\npremiumPreviewDoubledLimitsBox: Box(premiumPreviewBox) {\n\tbuttonPadding: margins(12px, 12px, 12px, 12px);\n}\npremiumPreviewAboutTitlePadding: margins(18px, 19px, 18px, 0px);\npremiumPreviewAboutTitle: FlatLabel(defaultFlatLabel) {\n\tminWidth: 240px;\n\ttextFg: windowBoldFg;\n\talign: align(top);\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(17px semibold);\n\t}\n}\npremiumPreviewAbout: FlatLabel(defaultFlatLabel) {\n\tminWidth: 240px;\n\ttextFg: membersAboutLimitFg;\n\talign: align(top);\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px);\n\t}\n}\npremiumPreviewAboutPadding: margins(18px, 8px, 18px, 8px);\npremiumPreviewButtonLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: premiumButtonFg;\n\tstyle: semiboldTextStyle;\n}\npremiumSlideDuration: 200;\npremiumVideoStarSize: size(77px, 73px);\n\npremiumPreviewHeight: 312px;\n\npremiumDot: 6px;\npremiumDotPadding: margins(4px, 4px, 4px, 4px);\npremiumDotsMargin: margins(0px, 5px, 0px, 6px);\npremiumVideoWidth: 182px;\n\n// Graphics.\n\ndefaultPremiumBubble: PremiumBubble {\n\twidthLimit: 80px;\n\theight: 40px;\n\tpadding: margins(14px, 0px, 14px, 0px);\n\tskip: 8px;\n\tpenWidth: 6px;\n\ttextSkip: 3px;\n\ttailSize: size(21px, 7px);\n\tfont: font(19px);\n}\npremiumLineRadius: 5px;\npremiumLineTextSkip: 11px;\npremiumInfographicPadding: margins(0px, 10px, 0px, 15px);\n\npremiumIconChats: icon {{ \"limits/chats\", settingsIconFg }};\npremiumIconFiles: icon {{ \"limits/files\", settingsIconFg }};\npremiumIconFolders: icon {{ \"limits/folders\", settingsIconFg }};\npremiumIconGroups: icon {{ \"limits/groups\", settingsIconFg }};\npremiumIconLinks: icon {{ \"limits/links\", settingsIconFg }};\npremiumIconPins: icon {{ \"limits/pins\", settingsIconFg }};\npremiumIconAccounts: icon {{ \"limits/accounts\", settingsIconFg }};\npremiumIconBoost: icon {{ \"limits/boost\", settingsIconFg }};\n\npremiumAccountsCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) {\n\timageRadius: 27px;\n\timageSmallRadius: 23px;\n\tcheck: RoundCheckbox(defaultRoundCheckbox) {\n\t\tsize: 0px;\n\t}\n}\npremiumAccountsLabelSize: size(22px, 15px);\npremiumAccountsLabelPadding: margins(2px, 2px, 2px, 2px);\npremiumAccountsLabelRadius: 6;\npremiumAccountsNameTop: 13px;\npremiumAccountsPadding: margins(0px, 20px, 0px, 14px);\npremiumAccountsHeight: 105px;\n\nPremiumOption {\n\trowPadding: margins;\n\trowMargins: margins;\n\trowHeight: pixels;\n\n\tborderWidth: pixels;\n\tborderRadius: pixels;\n\n\tsubtitleTop: pixels;\n\ttextLeft: pixels;\n\n\tbadgeHeight: pixels;\n\tbadgeRadius: pixels;\n\tbadgeMargins: margins;\n\tbadgeShift: point;\n}\n\npremiumSubscriptionOption: PremiumOption {\n\trowPadding: margins(9px, 2px, 17px, 3px);\n\trowMargins: margins(14px, 0px, 5px, 0px);\n\trowHeight: 39px;\n\n\tborderWidth: 0px;\n\tborderRadius: 0px;\n\n\tsubtitleTop: 1px;\n\ttextLeft: 51px;\n\n\tbadgeHeight: 15px;\n\tbadgeRadius: 4px;\n\tbadgeMargins: margins(3px, 1px, 3px, 0px);\n\tbadgeShift: point(9px, 0px);\n}\n\n// Gift.\npremiumGiftOption: PremiumOption {\n\trowPadding: margins(19px, 2px, 17px, 2px);\n\trowMargins: margins(14px, 0px, 15px, 0px);\n\trowHeight: 56px;\n\n\tborderWidth: 2px;\n\tborderRadius: 9px;\n\n\tsubtitleTop: 7px;\n\ttextLeft: 53px;\n\n\tbadgeHeight: 18px;\n\tbadgeRadius: 4px;\n\tbadgeMargins: margins(5px, 1px, 5px, 0px);\n}\n\npremiumGiftBox: Box(premiumPreviewBox) {\n\tbuttonPadding: margins(12px, 12px, 12px, 12px);\n}\n\npremiumGiftsUserpicButton: UserpicButton(defaultUserpicButton) {\n\tsize: size(66px, 66px);\n\tphotoSize: 66px;\n\tphotoPosition: point(-1px, -1px);\n}\n\nboostSkipTop: 37px;\nboostLimits: PremiumLimits(defaultPremiumLimits) {\n\tgradientFromLeft: true;\n\tnonPremiumBg: windowBgRipple;\n}\nnegativeStarsLimits: PremiumLimits(boostLimits) {\n\tgradientFromLeft: false;\n}\nupgradePriceLimits: PremiumLimits(boostLimits) {\n\tgradientFromLeft: false;\n}\nboostBubble: PremiumBubble(defaultPremiumBubble) {\n\theight: 32px;\n\tpadding: margins(7px, 0px, 11px, 0px);\n\tskip: 5px;\n\ttextSkip: 2px;\n\ttailSize: size(14px, 6px);\n\tfont: font(16px);\n\tadditionalStyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(14px semibold);\n\t}\n\tadditionalSkip: 2px;\n\tsubtextStyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(10px);\n\t}\n\tsubtextPadding: margins(11px, -5px, 11px, 0px);\n}\nstarRatingBubble: PremiumBubble(boostBubble) {\n\tfont: font(16px semibold);\n}\niconOnlyPremiumBubble: PremiumBubble(boostBubble) {\n\tpadding: margins(7px, 0px, 7px, 0px);\n\ttextSkip: 0px;\n}\nboostTitleSkip: 32px;\nboostTitle: FlatLabel(defaultFlatLabel) {\n\tminWidth: 40px;\n\ttextFg: windowBoldFg;\n\tmaxHeight: 24px;\n\tstyle: TextStyle(boxTextStyle) {\n\t\tfont: font(17px semibold);\n\t}\n}\nboostTitleBadge: FlatLabel(defaultFlatLabel) {\n\tmargin: margins(4px, 2px, 4px, 2px);\n\tstyle: semiboldTextStyle;\n\ttextFg: premiumButtonFg;\n}\nboostTitleBadgeSkip: 6px;\nboostCenteredTitle: FlatLabel(boostTitle) {\n\talign: align(top);\n}\ndarkEditStarsCenteredTitle: FlatLabel(boostCenteredTitle) {\n\ttextFg: groupCallMembersFg;\n}\nboostTextSkip: 5px;\nboostText: FlatLabel(defaultFlatLabel) {\n\tminWidth: 40px;\n\talign: align(top);\n}\ndarkEditStarsText: FlatLabel(boostText) {\n\ttextFg: groupCallMembersFg;\n}\nboostTextPending: FlatLabel(boostText) {\n\ttextFg: windowSubTextFg;\n}\nboostTextNegative: FlatLabel(boostText) {\n\ttextFg: attentionButtonFg;\n}\nboostReassignText: FlatLabel(defaultFlatLabel) {\n\tminWidth: 40px;\n\talign: align(top);\n}\nboostBottomSkip: 6px;\nboostBox: Box(premiumPreviewDoubledLimitsBox) {\n\tbuttonPadding: margins(16px, 12px, 16px, 12px);\n\tbuttonHeight: 42px;\n\tbuttonWide: true;\n\tbutton: RoundButton(defaultActiveButton) {\n\t\theight: 42px;\n\t\ttextTop: 12px;\n\t\tstyle: semiboldTextStyle;\n\t}\n}\ndarkEditStarsClose: IconButton(boxTitleClose) {\n\ticon: icon {{ \"box_button_close\", groupCallMemberInactiveIcon }};\n\ticonOver: icon {{ \"box_button_close\", groupCallMemberInactiveIcon }};\n\tripple: RippleAnimation(defaultRippleAnimation) {\n\t\tcolor: groupCallMembersBgOver;\n\t}\n}\n\nboostReplaceUserpicsPadding: margins(0px, 18px, 0px, 20px);\nboostReplaceUserpicsSkip: 42px;\nboostReplaceUserpicsShift: 24px;\nboostReplaceUserpic: UserpicButton(defaultUserpicButton) {\n\tsize: size(60px, 60px);\n\tphotoSize: 60px;\n}\nboostReplaceIcon: icon{{ \"stories/boost_mini\", premiumButtonFg }};\nboostReplaceIconSkip: 3px;\nboostReplaceIconOutline: 2px;\nboostReplaceIconAdd: point(4px, 2px);\nboostReplaceArrow: icon{{ \"mediaview/next\", windowSubTextFg }};\nboostReplaceUserpicsRow: UserpicsRow {\n\tbutton: boostReplaceUserpic;\n\tbg: windowBg;\n\tshift: boostReplaceUserpicsShift;\n\tstroke: boostReplaceIconOutline;\n\tcomplex: true;\n}\n\nshowOrTitleIconMargin: 8px;\nshowOrTitlePadding: margins(0px, 0px, 0px, 5px);\nshowOrAboutPadding: margins(0px, 0px, 0px, 16px);\nshowOrShowButton: RoundButton(defaultActiveButton) {\n\twidth: 308px;\n\theight: 42px;\n\ttextTop: 12px;\n\tstyle: semiboldTextStyle;\n}\nshowOrLabel: FlatLabel(boostText) {\n\ttextFg: windowSubTextFg;\n}\nshowOrLineWidth: 190px;\nshowOrLabelSkip: 7px;\nshowOrLineTop: 10px;\nshowOrLabelPadding: margins(0px, 17px, 0px, 13px);\nshowOrPremiumAboutPadding: margins(0px, 0px, 0px, 0px);\nshowOrBox: Box(boostBox) {\n\tbuttonPadding: margins(28px, 16px, 28px, 27px);\n\tbutton: showOrShowButton;\n}\n\nboostBoxMaxHeight: 512px;\nboostLevelBadge: FlatLabel(defaultFlatLabel) {\n\tmargin: margins(12px, 4px, 12px, 5px);\n\tstyle: semiboldTextStyle;\n\ttextFg: premiumButtonFg;\n\talign: align(top);\n}\nboostLevelBadgePadding: margins(30px, 12px, 32px, 12px);\nboostLevelBadgeSkip: 12px;\nboostLevelBadgeLine: 1px;\n\nboostFeatureLabel: FlatLabel(defaultFlatLabel) {\n\tmargin: margins(36px, 4px, 0px, 4px);\n}\nboostFeaturePadding: margins(64px, 6px, 24px, 6px);\nboostFeatureIconPosition: point(0px, 0px);\nboostFeatureBackground: icon{{ \"settings/premium/features/feature_wallpaper\", windowBgActive }};\nboostFeatureCustomBackground: icon{{ \"settings/premium/features/feature_custombg\", windowBgActive }};\nboostFeatureCustomEmoji: icon{{ \"settings/premium/features/feature_emoji_pack\", windowBgActive }};\nboostFeatureCustomLink: icon{{ \"settings/premium/features/feature_links2\", windowBgActive }};\nboostFeatureCustomReactions: icon{{ \"settings/premium/features/feature_reactions\", windowBgActive }};\nboostFeatureEmojiStatus: icon{{ \"settings/premium/features/feature_status\", windowBgActive }};\nboostFeatureLink: icon{{ \"settings/premium/features/feature_links\", windowBgActive }};\nboostFeatureName: icon{{ \"settings/premium/features/feature_color_names\", windowBgActive }};\nboostFeatureStories: icon{{ \"settings/premium/features/feature_stories\", windowBgActive }};\nboostFeatureTranscribe: icon{{ \"settings/premium/features/feature_voice\", windowBgActive }};\nboostFeatureAutoTranslate: icon{{ \"menu/translate\", windowBgActive }};\nboostFeatureOffSponsored: icon{{ \"settings/premium/features/feature_off_sponsored\", windowBgActive }};\nboostFeatureProfileColor: icon{{ \"settings/premium/features/feature_color_profile\", windowBgActive }};\nboostFeatureProfileIcon: icon{{ \"settings/premium/features/feature_profile_cover\", windowBgActive }};\n\npaidReactBox: Box(boostBox) {\n\tbuttonPadding: margins(22px, 22px, 22px, 22px);\n\tbutton: RoundButton(defaultActiveButton) {\n\t\theight: 42px;\n\t\ttextTop: 12px;\n\t\tstyle: semiboldTextStyle;\n\t}\n}\npaidReactBoxCheckbox: Checkbox(defaultCheckbox) {\n\twidth: 0px;\n}\ndarkEditStarsBox: Box(paidReactBox) {\n\tbg: groupCallMembersBg;\n\ttitle: FlatLabel(boxTitle) {\n\t\ttextFg: groupCallMembersFg;\n\t\talign: align(top);\n\t}\n}\npaidReactBubbleIcon: icon{{ \"settings/premium/star\", premiumButtonFg }};\npaidReactBubbleTop: 5px;\npaidReactSliderTop: 5px;\npaidReactSlider: MediaSlider(defaultContinuousSlider) {\n\tactiveFg: windowBgOver;\n\tinactiveFg: windowBgOver;\n\tactiveFgOver: windowBgOver;\n\tinactiveFgOver: windowBgOver;\n\tactiveFgDisabled: windowBgOver;\n\tinactiveFgDisabled: windowBgOver;\n\twidth: 16px;\n\tseekSize: size(16px, 16px);\n\tborderFg: creditsBg3;\n\tborderWidth: 2px;\n}\ndarkEditStarsSlider: MediaSlider(paidReactSlider) {\n\tinactiveFg: groupCallMembersBgOver;\n\tinactiveFgOver: groupCallMembersBgOver;\n\tinactiveFgDisabled: groupCallMembersBgOver;\n}\npaidReactTitleSkip: 23px;\npaidReactTopTitleMargin: margins(10px, 26px, 10px, 12px);\npaidReactTopMargin: margins(0px, 12px, 0px, 11px);\npaidReactTopUserpic: 42px;\npaidReactTopNameSkip: 47px;\npaidReactTopBadgeSkip: 32px;\npaidReactCrown: icon{{ \"calls/filled_stream_crown-24x24\", premiumButtonFg }};\npaidReactCrownTop: 7px;\npaidReactCrownSkip: 12px;\npaidReactCrownOutline: 1px;\npaidReactCrownSmall: icon{{ \"calls/filled_stream_crown-20x20\", premiumButtonFg }};\npaidReactCrownSmallTop: 4px;\npaidReactCrownMargin: margins(0px, 0px, 0px, 0px);\npaidReactToastLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: toastFg;\n\tpalette: defaultToastPalette;\n}\npaidReactTopStarIcon: icon{{ \"chat/mini_stars\", premiumButtonFg }};\npaidReactTopStarIconPosition: point(0px, 1px);\npaidReactTopStarSkip: 4px;\npaidReactChannelArrow: icon{{ \"intro_country_dropdown\", activeButtonFg }};\npaidReactChannelMenu: PopupMenu(popupMenuWithIcons) {\n\tmenu: Menu(menuWithIcons) {\n\t\twidthMax: 240px;\n\t}\n\tmaxHeight: 345px;\n}\n\ntoastUndoStroke: 2px;\ntoastUndoSpace: 8px;\ntoastUndoDiameter: 20px;\ntoastUndoSkip: 20px;\ntoastUndoFont: font(12px bold);\n\nstarrefCover: PremiumCover(userPremiumCover) {\n\tbg: windowBgOver;\n\tstarTopSkip: 24px;\n\ttitlePadding: margins(0px, 12px, 0px, 11px);\n}\nstarrefCoverHeight: 180px;\nstarrefFooterButton: RoundButton(defaultActiveButton) {\n\theight: 42px;\n\ttextTop: 12px;\n\tstyle: semiboldTextStyle;\n}\nstarrefFooterBox: Box(defaultBox) {\n\tbuttonPadding: margins(22px, 11px, 22px, 54px);\n\tbuttonHeight: 42px;\n\tbuttonWide: true;\n\tbutton: starrefFooterButton;\n\tshadowIgnoreTopSkip: true;\n}\nstarrefCopyButton: RoundButton(starrefFooterButton) {\n\ticon: icon {{ \"info/edit/links_copy\", activeButtonFg }};\n\ticonOver: icon {{ \"info/edit/links_copy\", activeButtonFgOver }};\n\ticonPosition: point(-1px, 5px);\n}\n\nstarrefJoinIcon: icon{{ \"payments/small_star\", premiumButtonFg }};\nstarrefJoinUserpicsPadding: margins(0px, 32px, 0px, 10px);\nstarrefJoinTitlePadding: margins(0px, 0px, 0px, 12px);\nstarrefCenteredText: FlatLabel(defaultFlatLabel) {\n\talign: align(top);\n\tminWidth: 40px;\n}\nstarrefJoinFooter: FlatLabel(starrefCenteredText) {\n\ttextFg: windowSubTextFg;\n}\nstarrefRevenueText: FlatLabel(starrefCenteredText) {\n\tpalette: TextPalette(defaultTextPalette) {\n\t\tlinkFg: creditsBg1;\n\t}\n}\n\nstarrefInfoIconPosition: point(16px, 8px);\nstarrefBottomButton: RoundButton(defaultActiveButton) {\n\theight: 44px;\n\ttextTop: 12px;\n\tstyle: semiboldTextStyle;\n}\nstarrefButtonMargin: margins(12px, 6px, 12px, 4px);\nstarrefBottomButtonLabel: FlatLabel(defaultFlatLabel) {\n\ttextFg: windowFgActive;\n\tstyle: semiboldTextStyle;\n\tminWidth: 0px;\n}\nstarrefBottomButtonSublabel: FlatLabel(starrefBottomButtonLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(11px semibold);\n\t}\n}\nstarrefBottomButtonLabelTop: 5px;\nstarrefBottomButtonSublabelTop: 23px;\nstarrefEndBulletSize: 6px;\nstarrefEndBulletTop: 8px;\nstarrefLinkThumbOuter: 64px;\nstarrefLinkThumbInner: 48px;\nstarrefLinkCountAdd: 6px;\nstarrefLinkCountIcon: icon{{ \"chat/mini_subscribers\", historyPeerUserpicFg }};\nstarrefLinkCountIconPosition: point(0px, 1px);\nstarrefLinkCountFont: font(10px bold);\nstarrefLinkCountPadding: margins(2px, 0px, 3px, 1px);\nstarrefRecipientBg: lightButtonBgOver;\nstarrefRecipientBgDisabled: windowBgOver;\nstarrefRecipientArrow: icon{{ \"intro_country_dropdown\", lightButtonFg }};\nstarrefCommissionFont: font(10px semibold);\nstarrefCommissionPadding: margins(3px, 0px, 3px, 0px);\nstarrefLinkBadge: 16px;\nstarrefLinkBadgeSkip: 1px;\n\nstarrefAddForBotIcon: icon {{ \"menu/bot_add\", lightButtonFg }};\nstarrefAddForBotIconPosition: point(23px, 2px);\n\nstarrefPopupMenu: PopupMenu(defaultPopupMenu) {\n\tmaxHeight: 320px;\n\tmenu: Menu(defaultMenu) {\n\t\twidthMin: 156px;\n\t\twidthMax: 200px;\n\t}\n}\n\nstarSelectInfoPadding: margins(12px, 8px, 12px, 8px);\nstarSelectInfoTitle: FlatLabel(defaultFlatLabel) {\n\tminWidth: 40px;\n\ttextFg: windowBoldFg;\n\talign: align(top);\n\tstyle: TextStyle(boxTextStyle) {\n\t\tfont: font(18px semibold);\n\t}\n}\nstarSelectInfoSubtext: FlatLabel(defaultFlatLabel) {\n\tminWidth: 40px;\n\ttextFg: windowFg;\n\talign: align(top);\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(10px);\n\t}\n}\nvideoStreamInfoTitle: FlatLabel(starSelectInfoTitle) {\n\ttextFg: groupCallMembersFg;\n}\nvideoStreamInfoSubtext: FlatLabel(starSelectInfoSubtext) {\n\ttextFg: groupCallMembersFg;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/premium_bubble.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/premium_bubble.h\"\n\n#include \"base/debug_log.h\"\n#include \"base/object_ptr.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/gradient.h\"\n#include \"ui/effects/ministar_particles.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_info_levels.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_premium.h\"\n\nnamespace Ui::Premium {\nnamespace {\n\nconstexpr auto kBubbleRadiusSubtractor = 2;\nconstexpr auto kDeflectionSmall = 20.;\nconstexpr auto kDeflection = 30.;\nconstexpr auto kStepBeforeDeflection = 0.75;\nconstexpr auto kStepAfterDeflection = kStepBeforeDeflection\n+ (1. - kStepBeforeDeflection) / 2.;\nconstexpr auto kSlideDuration = crl::time(1000);\n\n} // namespace\n\nTextFactory ProcessTextFactory(\n\t\tstd::optional<tr::phrase<lngtag_count>> phrase) {\n\treturn phrase\n\t\t? TextFactory([=](int n) -> BubbleText {\n\t\t\treturn { (*phrase)(tr::now, lt_count, n) };\n\t\t})\n\t\t: TextFactory([=](int n) -> BubbleText {\n\t\t\treturn { QString::number(n) };\n\t\t});\n}\n\nBubble::Bubble(\n\tconst style::PremiumBubble &st,\n\tFn<void()> updateCallback,\n\tTextFactory textFactory,\n\tconst style::icon *icon,\n\tbool hasTail)\n: _st(st)\n, _updateCallback(std::move(updateCallback))\n, _textFactory(std::move(textFactory))\n, _icon(icon)\n, _numberAnimation(_st.font, _updateCallback)\n, _height(_st.height + _st.tailSize.height())\n, _hasTail(hasTail) {\n\t_numberAnimation.setDisabledMonospace(true);\n\t_numberAnimation.setWidthChangedCallback([=] {\n\t\t_widthChanges.fire({});\n\t});\n\tconst auto texts = _textFactory(0);\n\t_numberAnimation.setText(texts.counter, 0);\n\t_numberAnimation.finishAnimating();\n\tif (!texts.additional.isEmpty()) {\n\t\t_additional.setText(_st.additionalStyle, texts.additional);\n\t}\n}\n\ncrl::time Bubble::SlideNoDeflectionDuration() {\n\treturn kSlideDuration * kStepBeforeDeflection;\n}\n\nstd::optional<int> Bubble::counter() const {\n\treturn _counter;\n}\n\nint Bubble::height() const {\n\treturn _height;\n}\n\nint Bubble::bubbleRadius() const {\n\treturn (_st.height / 2) - kBubbleRadiusSubtractor;\n}\n\nint Bubble::filledWidth() const {\n\treturn _st.padding.left()\n\t\t+ _icon->width()\n\t\t+ _st.textSkip\n\t\t+ _st.padding.right();\n}\n\nint Bubble::topTextWidth() const {\n\treturn filledWidth()\n\t\t+ _numberAnimation.countWidth()\n\t\t+ (_additional.isEmpty()\n\t\t\t? 0\n\t\t\t: (_st.additionalSkip + _additional.maxWidth()));\n}\n\nint Bubble::bottomTextWidth() const {\n\treturn _subtext.isEmpty()\n\t\t? 0\n\t\t: (_st.subtextPadding.left()\n\t\t\t+ _subtext.maxWidth()\n\t\t\t+ _st.subtextPadding.right());\n}\n\nint Bubble::width() const {\n\treturn std::max(topTextWidth(), bottomTextWidth());\n}\n\nint Bubble::countTargetWidth(int targetCounter) const {\n\tauto numbers = Ui::NumbersAnimation(_st.font, [] {});\n\tnumbers.setDisabledMonospace(true);\n\tnumbers.setDuration(0);\n\tconst auto texts = _textFactory(targetCounter);\n\tnumbers.setText(texts.counter, targetCounter);\n\tnumbers.finishAnimating();\n\tconst auto top = filledWidth()\n\t\t+ numbers.maxWidth()\n\t\t+ (_additional.isEmpty()\n\t\t\t? 0\n\t\t\t: (_st.additionalSkip\n\t\t\t\t+ _st.additionalStyle.font->width(texts.additional)));\n\treturn std::max(top, bottomTextWidth());\n}\n\nvoid Bubble::setCounter(int value) {\n\tif (_counter != value) {\n\t\t_counter = value;\n\t\tconst auto texts = _textFactory(value);\n\t\t_numberAnimation.setText(texts.counter, value);\n\t\tif (!texts.additional.isEmpty()) {\n\t\t\t_additional.setText(_st.additionalStyle, texts.additional);\n\t\t}\n\t}\n}\n\nvoid Bubble::setTailEdge(EdgeProgress edge) {\n\t_tailEdge = std::clamp(edge, 0., 1.);\n}\n\nvoid Bubble::setFlipHorizontal(bool value) {\n\t_flipHorizontal = value;\n}\n\nQRect Bubble::bubbleGeometry(const QRect &r) const {\n\tconst auto penWidth = _st.penWidth;\n\tconst auto penWidthHalf = penWidth / 2;\n\treturn r - style::margins(\n\t\tpenWidthHalf,\n\t\tpenWidthHalf,\n\t\tpenWidthHalf,\n\t\t_st.tailSize.height() + penWidthHalf);\n}\n\nQPainterPath Bubble::bubblePath(const QRect &r) const {\n\tconst auto bubbleRect = bubbleGeometry(r);\n\tconst auto radius = bubbleRadius();\n\tauto pathTail = QPainterPath();\n\n\tconst auto tailWHalf = _st.tailSize.width() / 2.;\n\tconst auto progress = _tailEdge;\n\n\tconst auto tailTop = bubbleRect.y() + bubbleRect.height();\n\tconst auto tailLeftFull = bubbleRect.x()\n\t\t+ (bubbleRect.width() * 0.5)\n\t\t- tailWHalf;\n\tconst auto tailLeft = bubbleRect.x()\n\t\t+ (bubbleRect.width() * 0.5 * (progress + 1.))\n\t\t- tailWHalf;\n\tconst auto tailCenter = tailLeft + tailWHalf;\n\tconst auto tailRight = [&] {\n\t\tconst auto max = bubbleRect.x() + bubbleRect.width();\n\t\tconst auto right = tailLeft + _st.tailSize.width();\n\t\tconst auto bottomMax = max - radius;\n\t\treturn (right > bottomMax)\n\t\t\t? std::max(float64(tailCenter), float64(bottomMax))\n\t\t\t: right;\n\t}();\n\tif (_hasTail) {\n\t\tpathTail.moveTo(tailLeftFull, tailTop);\n\t\tpathTail.lineTo(tailLeft, tailTop);\n\t\tpathTail.lineTo(tailCenter, tailTop + _st.tailSize.height());\n\t\tpathTail.lineTo(tailRight, tailTop);\n\t\tpathTail.lineTo(tailRight, tailTop - radius);\n\t\tpathTail.moveTo(tailLeftFull, tailTop);\n\t}\n\tauto pathBubble = QPainterPath();\n\tpathBubble.setFillRule(Qt::WindingFill);\n\tpathBubble.addRoundedRect(bubbleRect, radius, radius);\n\n\tauto result = pathTail + pathBubble;\n\tif (_flipHorizontal) {\n\t\tauto m = QTransform();\n\t\tconst auto center = QRectF(bubbleRect).center();\n\t\tm.translate(center.x(), center.y());\n\t\tm.scale(-1., 1.);\n\t\tm.translate(-center.x(), -center.y());\n\t\treturn m.map(result);\n\t}\n\treturn result;\n}\n\nvoid Bubble::setSubtext(QString subtext) {\n\tif (_subtext.toString() == subtext) {\n\t\treturn;\n\t} else if (subtext.isEmpty()) {\n\t\t_subtext = {};\n\t} else {\n\t\t_subtext.setText(_st.subtextStyle, subtext);\n\t}\n\t_widthChanges.fire({});\n\t_updateCallback();\n}\n\nvoid Bubble::finishAnimating() {\n\t_numberAnimation.finishAnimating();\n}\n\nvoid Bubble::paintBubble(QPainter &p, const QRect &r, const QBrush &brush) {\n\tif (!_counter.has_value()) {\n\t\treturn;\n\t}\n\n\tconst auto bubbleRect = bubbleGeometry(r);\n\tconst auto penWidth = _st.penWidth;\n\t{\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(QPen(\n\t\t\tbrush,\n\t\t\tpenWidth,\n\t\t\tQt::SolidLine,\n\t\t\tQt::RoundCap,\n\t\t\tQt::RoundJoin));\n\t\tp.setBrush(brush);\n\t\tp.drawPath(bubblePath(r));\n\t}\n\tp.setPen(st::activeButtonFg);\n\tconst auto withSubtext = !_subtext.isEmpty();\n\tconst auto height = withSubtext\n\t\t? (_st.font->height\n\t\t\t+ _st.subtextPadding.top()\n\t\t\t+ _st.subtextStyle.font->height)\n\t\t: _st.font->height;\n\tconst auto topWidth = withSubtext ? topTextWidth() : 0;\n\tconst auto bottomWidth = withSubtext ? bottomTextWidth() : 0;\n\tconst auto iconShift = (topWidth >= bottomWidth)\n\t\t? 0\n\t\t: (bottomWidth - topWidth) / 2;\n\tconst auto iconLeft = r.x() + _st.padding.left() + iconShift;\n\tconst auto topShift = (_st.font->height - height) / 2;\n\tconst auto iconTop = bubbleRect.y()\n\t\t+ topShift\n\t\t+ (bubbleRect.height() - _icon->height()) / 2;\n\t_icon->paint(p, iconLeft, iconTop, bubbleRect.width());\n\tconst auto numberLeft = iconLeft + _icon->width() + _st.textSkip;\n\tconst auto numberTop = r.y() + (_st.height - height) / 2;\n\t_numberAnimation.paint(p, numberLeft, numberTop, width());\n\tif (!_additional.isEmpty()) {\n\t\tp.setOpacity(0.7);\n\t\tconst auto additionalLeft = numberLeft\n\t\t\t+ _numberAnimation.countWidth()\n\t\t\t+ _st.additionalSkip;\n\t\tconst auto additionalTop = numberTop\n\t\t\t+ _st.font->ascent\n\t\t\t- _st.additionalStyle.font->ascent;\n\t\t_additional.draw(p, {\n\t\t\t.position = { additionalLeft, additionalTop },\n\t\t\t.availableWidth = _additional.maxWidth(),\n\t\t});\n\t}\n\tif (withSubtext) {\n\t\tp.setOpacity(1.);\n\t\tconst auto subtextShift = (bottomWidth >= topWidth)\n\t\t\t? 0\n\t\t\t: (topWidth - bottomWidth) / 2;\n\t\tconst auto subtextLeft = r.x()\n\t\t\t+ _st.subtextPadding.left()\n\t\t\t+ subtextShift;\n\t\tconst auto subtextTop = numberTop\n\t\t\t+ _st.font->height\n\t\t\t+ _st.subtextPadding.top();\n\t\t_subtext.draw(p, {\n\t\t\t.position = { subtextLeft, subtextTop },\n\t\t\t.availableWidth = _subtext.maxWidth(),\n\t\t});\n\t}\n}\n\nrpl::producer<> Bubble::widthChanges() const {\n\treturn _widthChanges.events();\n}\n\nBubbleWidget::BubbleWidget(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst style::PremiumBubble &st,\n\tTextFactory textFactory,\n\trpl::producer<BubbleRowState> state,\n\tBubbleType type,\n\trpl::producer<> showFinishes,\n\tconst style::icon *icon,\n\tconst style::margins &outerPadding)\n: AbstractButton(parent)\n, _st(st)\n, _state(std::move(state))\n, _bubble(\n\t_st,\n\t[=] { update(); },\n\tstd::move(textFactory),\n\ticon,\n\t(type != BubbleType::NoPremium))\n, _type(type)\n, _outerPadding(outerPadding)\n, _deflection(kDeflection)\n, _stepBeforeDeflection(kStepBeforeDeflection)\n, _stepAfterDeflection(kStepAfterDeflection) {\n\tif (_type == BubbleType::Credits) {\n\t\tsetupParticles(parent);\n\t}\n\tconst auto resizeTo = [=](int w, int h) {\n\t\t_deflection = (w > _st.widthLimit)\n\t\t\t? kDeflectionSmall\n\t\t\t: kDeflection;\n\t\t_spaceForDeflection = QSize(_st.skip, _st.skip);\n\t\tresize(QSize(w, h) + 2 * _spaceForDeflection);\n\t};\n\n\tresizeTo(_bubble.width(), _bubble.height());\n\t_bubble.widthChanges(\n\t) | rpl::on_next([=] {\n\t\tresizeTo(_bubble.width(), _bubble.height());\n\t}, lifetime());\n\n\tconst auto instant = !showFinishes;\n\tif (instant) {\n\t\tshowFinishes = rpl::single(rpl::empty);\n\t}\n\tstd::move(\n\t\tshowFinishes\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t_state.value(\n\t\t) | rpl::on_next([=](BubbleRowState state) {\n\t\t\tanimateTo(state);\n\t\t}, lifetime());\n\t\tif (instant) {\n\t\t\t_appearanceAnimation.stop();\n\t\t\tif (const auto onstack = base::take(_appearanceCallback)) {\n\t\t\t\tonstack(1.);\n\t\t\t}\n\t\t\t_bubble.finishAnimating();\n\t\t}\n\n\t\tparent->widthValue() | rpl::on_next([=](int w) {\n\t\t\tif (!_appearanceAnimation.animating()) {\n\t\t\t\tconst auto available = w\n\t\t\t\t\t- _outerPadding.left()\n\t\t\t\t\t- _outerPadding.right();\n\t\t\t\tconst auto x = (available * _animatingFromResultRatio);\n\t\t\t\tmoveToLeft(-_spaceForDeflection.width()\n\t\t\t\t\t+ std::max(\n\t\t\t\t\t\tint(base::SafeRound(x\n\t\t\t\t\t\t\t- (_bubble.width() / 2)\n\t\t\t\t\t\t\t+ _outerPadding.left())),\n\t\t\t\t\t\t0),\n\t\t\t\t\t0);\n\t\t\t}\n\t\t}, lifetime());\n\t}, lifetime());\n}\n\nvoid BubbleWidget::setupParticles(not_null<Ui::RpWidget*> parent) {\n\t_particles.emplace(StarParticles::Type::Radial, 50, st::lineWidth * 4);\n\t_particles->setSpeed(0.1);\n\n\t_particlesWidget = Ui::CreateChild<Ui::RpWidget>(parent);\n\t_particlesWidget->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t_particlesWidget->show();\n\t_particlesWidget->raise();\n\n\t_particlesAnimation.init([=] { _particlesWidget->update(); });\n\n\t_particlesWidget->paintRequest() | rpl::on_next([=] {\n\t\tif (!_particlesAnimation.animating()) {\n\t\t\t_particlesAnimation.start();\n\t\t}\n\t\tauto p = QPainter(_particlesWidget);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tconst auto offset = QPoint(\n\t\t\t(_particlesWidget->width() - width()) / 2,\n\t\t\t(_particlesWidget->height() - height()) / 2);\n\t\tconst auto bubbleRect = rect().translated(offset)\n\t\t\t- QMargins(\n\t\t\t\t_spaceForDeflection.width(),\n\t\t\t\t_spaceForDeflection.height(),\n\t\t\t\t_spaceForDeflection.width(),\n\t\t\t\t_spaceForDeflection.height());\n\n\t\tp.save();\n\t\tp.translate(offset);\n\t\tconst auto bubblePath = _bubble.bubblePath(bubbleRect);\n\t\tp.restore();\n\n\t\tauto fullRect = QPainterPath();\n\t\tfullRect.addRect(QRectF(_particlesWidget->rect()));\n\n\t\tp.setClipPath(bubblePath);\n\t\t_particles->setColor(st::premiumButtonFg->c);\n\t\t_particles->paint(p, _particlesWidget->rect(), crl::now(), false);\n\t\tp.setClipping(false);\n\n\t\tp.setClipPath(fullRect.subtracted(bubblePath));\n\t\t_particles->setColor(_brushOverride\n\t\t\t? st::groupCallMemberInactiveIcon->c\n\t\t\t: st::creditsBg3->c);\n\t\t_particles->paint(p, _particlesWidget->rect(), crl::now(), false);\n\t}, _particlesWidget->lifetime());\n\n\tgeometryValue() | rpl::on_next([=](QRect geometry) {\n\t\tconst auto particlesSize = QSize(\n\t\t\tint(geometry.width() * 1.5),\n\t\t\tint(geometry.height() * 1.5));\n\t\tconst auto center = geometry.center();\n\t\t_particlesWidget->setGeometry(\n\t\t\tcenter.x() - particlesSize.width() / 2,\n\t\t\tcenter.y() - particlesSize.height() / 2,\n\t\t\tparticlesSize.width(),\n\t\t\tparticlesSize.height());\n\t}, lifetime());\n}\n\nvoid BubbleWidget::animateTo(BubbleRowState state) {\n\tconst auto targetWidth = _bubble.countTargetWidth(state.counter);\n\tconst auto parent = parentWidget();\n\tconst auto available = parent->width()\n\t\t- _outerPadding.left()\n\t\t- _outerPadding.right();\n\tconst auto halfWidth = (targetWidth / 2);\n\tconst auto computeLeft = [=](float64 pointRatio, float64 animProgress) {\n\t\tconst auto delta = (pointRatio - _animatingFromResultRatio);\n\t\tconst auto center = available\n\t\t\t* (_animatingFromResultRatio + delta * animProgress);\n\t\treturn center - halfWidth + _outerPadding.left();\n\t};\n\tconst auto moveEndPoint = state.ratio;\n\tconst auto computeRightEdge = [=] {\n\t\treturn parent->width() - _outerPadding.right() - targetWidth;\n\t};\n\tstruct Edge final {\n\t\tfloat64 goodPointRatio = 0.;\n\t\tfloat64 bubbleEdge = 0.;\n\t};\n\tconst auto desiredFinish = computeLeft(moveEndPoint, 1.);\n\tconst auto leftEdge = [&]() -> Edge {\n\t\tconst auto edge = _outerPadding.left();\n\t\tif (desiredFinish < edge) {\n\t\t\tconst auto goodPointRatio = float64(halfWidth) / available;\n\t\t\tconst auto bubbleLeftEdge = (desiredFinish - edge)\n\t\t\t\t/ float64(halfWidth);\n\t\t\treturn { goodPointRatio, bubbleLeftEdge };\n\t\t}\n\t\treturn {};\n\t}();\n\tconst auto rightEdge = [&]() -> Edge {\n\t\tconst auto edge = computeRightEdge();\n\t\tif (desiredFinish > edge) {\n\t\t\tconst auto goodPointRatio = 1. - float64(halfWidth) / available;\n\t\t\tconst auto bubbleRightEdge = (desiredFinish - edge)\n\t\t\t\t/ float64(halfWidth);\n\t\t\treturn { goodPointRatio, bubbleRightEdge };\n\t\t}\n\t\treturn {};\n\t}();\n\tconst auto finalEdge = (leftEdge.bubbleEdge < 0.)\n\t\t? leftEdge.bubbleEdge\n\t\t: rightEdge.bubbleEdge;\n\t_ignoreDeflection = !_state.current().dynamic && (finalEdge != 0.);\n\tif (_ignoreDeflection) {\n\t\t_stepBeforeDeflection = 1.;\n\t\t_stepAfterDeflection = 1.;\n\t} else {\n\t\t_stepBeforeDeflection = kStepBeforeDeflection;\n\t\t_stepAfterDeflection = kStepAfterDeflection;\n\t}\n\tconst auto resultMoveEndPoint = (finalEdge < 0)\n\t\t? leftEdge.goodPointRatio\n\t\t: (finalEdge > 0)\n\t\t? rightEdge.goodPointRatio\n\t\t: moveEndPoint;\n\n\tconst auto duration = kSlideDuration\n\t\t* (_ignoreDeflection ? kStepBeforeDeflection : 1.)\n\t\t* ((_state.current().ratio < 0.001) ? 0.5 : 1.);\n\tif (state.animateFromZero) {\n\t\t_animatingFrom.ratio = 0.;\n\t\t_animatingFrom.counter = 0;\n\t\t_animatingFromResultRatio = 0.;\n\t\t_animatingFromBubbleEdge = 0.;\n\t}\n\t_appearanceCallback = [=](float64 value) {\n\t\tif (!_appearanceAnimation.animating()) {\n\t\t\t_animatingFrom = state;\n\t\t\t_animatingFromResultRatio = resultMoveEndPoint;\n\t\t\t_animatingFromBubbleEdge = finalEdge;\n\t\t\t_appearanceCallback = nullptr;\n\t\t}\n\t\tvalue = std::abs(value);\n\t\tconst auto moveProgress = std::clamp(\n\t\t\t(value / _stepBeforeDeflection),\n\t\t\t0.,\n\t\t\t1.);\n\t\tconst auto counterProgress = std::clamp(\n\t\t\t(value / _stepAfterDeflection),\n\t\t\t0.,\n\t\t\t1.);\n\t\tconst auto nowBubbleEdge = _animatingFromBubbleEdge\n\t\t\t+ (finalEdge - _animatingFromBubbleEdge) * moveProgress;\n\t\tmoveToLeft(-_spaceForDeflection.width()\n\t\t\t+ std::max(\n\t\t\t\tint(base::SafeRound(\n\t\t\t\t\tcomputeLeft(resultMoveEndPoint, moveProgress))),\n\t\t\t\t0),\n\t\t\t0);\n\n\t\tconst auto now = _animatingFrom.counter\n\t\t\t+ counterProgress * (state.counter - _animatingFrom.counter);\n\t\t_bubble.setCounter(int(base::SafeRound(now)));\n\n\t\tif (_particles) {\n\t\t\tconst auto progress = now / float(state.counter);\n\t\t\t_particles->setSpeed(0.01 + progress * 0.25);\n\t\t}\n\n\t\t_bubble.setFlipHorizontal(nowBubbleEdge < 0);\n\t\t_bubble.setTailEdge(std::abs(nowBubbleEdge));\n\t\tupdate();\n\t};\n\t_appearanceAnimation.start(\n\t\t_appearanceCallback,\n\t\t0.,\n\t\t(state.ratio >= _animatingFrom.ratio) ? 1. : -1.,\n\t\tduration,\n\t\tanim::easeOutCirc);\n}\n\nvoid BubbleWidget::setBrushOverride(std::optional<QBrush> brushOverride) {\n\t_brushOverride = std::move(brushOverride);\n\tupdate();\n}\n\nvoid BubbleWidget::setSubtext(QString subtext) {\n\t_bubble.setSubtext(subtext);\n}\n\nvoid BubbleWidget::paintEvent(QPaintEvent *e) {\n\tif (!_bubble.counter().has_value()) {\n\t\treturn;\n\t}\n\n\tauto p = QPainter(this);\n\n\tconst auto padding = QMargins(\n\t\t_spaceForDeflection.width(),\n\t\t_spaceForDeflection.height(),\n\t\t_spaceForDeflection.width(),\n\t\t_spaceForDeflection.height());\n\tconst auto bubbleRect = rect() - padding;\n\n\tconst auto params = GradientParams{\n\t\t.left = x() + _spaceForDeflection.width(),\n\t\t.width = bubbleRect.width(),\n\t\t.outer = parentWidget()->parentWidget()->width(),\n\t};\n\tif (_cachedGradientParams != params) {\n\t\t_cachedGradient = ComputeGradient(\n\t\t\tparentWidget(),\n\t\t\tparams.left,\n\t\t\tparams.width);\n\t\t_cachedGradientParams = params;\n\t}\n\tif (_appearanceAnimation.animating()) {\n\t\tconst auto value = _appearanceAnimation.value(1.);\n\t\tconst auto progress = std::abs(value);\n\t\tconst auto finalScale = (_animatingFromResultRatio > 0.)\n\t\t\t|| (_state.current().ratio < 0.001);\n\t\tconst auto scaleProgress = finalScale\n\t\t\t? 1.\n\t\t\t: std::clamp((progress / _stepBeforeDeflection), 0., 1.);\n\t\tconst auto scale = scaleProgress;\n\t\tconst auto rotationProgress = std::clamp(\n\t\t\t(progress - _stepBeforeDeflection) / (1. - _stepBeforeDeflection),\n\t\t\t0.,\n\t\t\t1.);\n\t\tconst auto rotationProgressReverse = std::clamp(\n\t\t\t(progress - _stepAfterDeflection) / (1. - _stepAfterDeflection),\n\t\t\t0.,\n\t\t\t1.);\n\n\t\tconst auto offsetX = bubbleRect.x() + bubbleRect.width() / 2;\n\t\tconst auto offsetY = bubbleRect.y() + bubbleRect.height();\n\t\tp.translate(offsetX, offsetY);\n\t\tp.scale(scale, scale);\n\t\tif (!_ignoreDeflection) {\n\t\t\tp.rotate((rotationProgress - rotationProgressReverse)\n\t\t\t\t* _deflection\n\t\t\t\t* (value < 0. ? -1. : 1.));\n\t\t}\n\t\tp.translate(-offsetX, -offsetY);\n\t}\n\n\n\t_bubble.paintBubble(p, bubbleRect, [&] {\n\t\tif (_brushOverride) {\n\t\t\treturn *_brushOverride;\n\t\t}\n\t\tswitch (_type) {\n\t\tcase BubbleType::NoPremium:\n\t\tcase BubbleType::UpgradePrice:\n\t\tcase BubbleType::StarRating: return st::windowBgActive->b;\n\t\tcase BubbleType::NegativeRating: return st::attentionButtonFg->b;\n\t\tcase BubbleType::Premium: return QBrush(_cachedGradient);\n\t\tcase BubbleType::Credits: return st::creditsBg3->b;\n\t\t}\n\t\tUnexpected(\"Type in Premium::BubbleWidget.\");\n\t}());\n}\n\nvoid BubbleWidget::resizeEvent(QResizeEvent *e) {\n\tRpWidget::resizeEvent(e);\n}\n\nnot_null<BubbleWidget*> AddBubbleRow(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tconst style::PremiumBubble &st,\n\t\trpl::producer<> showFinishes,\n\t\tint min,\n\t\tint current,\n\t\tint max,\n\t\tBubbleType type,\n\t\tstd::optional<tr::phrase<lngtag_count>> phrase,\n\t\tconst style::icon *icon) {\n\treturn AddBubbleRow(\n\t\tparent,\n\t\tst,\n\t\tstd::move(showFinishes),\n\t\trpl::single(BubbleRowState{\n\t\t\t.counter = current,\n\t\t\t.ratio = (current - min) / float64(max - min),\n\t\t}),\n\t\ttype,\n\t\tProcessTextFactory(phrase),\n\t\ticon,\n\t\tst::boxRowPadding);\n}\n\nnot_null<BubbleWidget*> AddBubbleRow(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tconst style::PremiumBubble &st,\n\t\trpl::producer<> showFinishes,\n\t\trpl::producer<BubbleRowState> state,\n\t\tBubbleType type,\n\t\tTextFactory text,\n\t\tconst style::icon *icon,\n\t\tconst style::margins &outerPadding) {\n\tconst auto container = parent->add(\n\t\tobject_ptr<Ui::FixedHeightWidget>(parent, 0));\n\tcontainer->show();\n\n\tconst auto bubble = Ui::CreateChild<BubbleWidget>(\n\t\tcontainer,\n\t\tst,\n\t\ttext ? std::move(text) : ProcessTextFactory(std::nullopt),\n\t\tstd::move(state),\n\t\ttype,\n\t\tstd::move(showFinishes),\n\t\ticon,\n\t\touterPadding);\n\tbubble->setAttribute(Qt::WA_TransparentForMouseEvents);\n\trpl::combine(\n\t\tcontainer->sizeValue(),\n\t\tbubble->sizeValue()\n\t) | rpl::on_next([=](const QSize &parentSize, const QSize &size) {\n\t\tcontainer->resize(parentSize.width(), size.height());\n\t}, bubble->lifetime());\n\tbubble->show();\n\treturn bubble;\n}\n\n} // namespace Ui::Premium\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/premium_bubble.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/numbers_animation.h\"\n#include \"ui/effects/ministar_particles.h\"\n#include \"ui/abstract_button.h\"\n#include \"ui/rp_widget.h\"\n\nenum lngtag_count : int;\n\nnamespace tr {\ntemplate <typename ...Tags>\nstruct phrase;\n} // namespace tr\n\nnamespace style {\nstruct PremiumBubble;\n} // namespace style\n\nnamespace Ui {\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Ui::Premium {\n\nstruct BubbleText {\n\tQString counter;\n\tQString additional;\n};\n\nusing TextFactory = Fn<BubbleText(int)>;\n\n[[nodiscard]] TextFactory ProcessTextFactory(\n\tstd::optional<tr::phrase<lngtag_count>> phrase);\n\nclass Bubble final {\npublic:\n\tusing EdgeProgress = float64;\n\n\tBubble(\n\t\tconst style::PremiumBubble &st,\n\t\tFn<void()> updateCallback,\n\t\tTextFactory textFactory,\n\t\tconst style::icon *icon,\n\t\tbool hasTail);\n\n\t[[nodiscard]] static crl::time SlideNoDeflectionDuration();\n\n\t[[nodiscard]] std::optional<int> counter() const;\n\t[[nodiscard]] int height() const;\n\t[[nodiscard]] int width() const;\n\t[[nodiscard]] int bubbleRadius() const;\n\t[[nodiscard]] int countTargetWidth(int targetCounter) const;\n\t[[nodiscard]] QRect bubbleGeometry(const QRect &r) const;\n\n\tvoid setCounter(int value);\n\tvoid setTailEdge(EdgeProgress edge);\n\tvoid setFlipHorizontal(bool value);\n\tvoid paintBubble(QPainter &p, const QRect &r, const QBrush &brush);\n\t[[nodiscard]] QPainterPath bubblePath(const QRect &r) const;\n\tvoid setSubtext(QString subtext);\n\n\tvoid finishAnimating();\n\n\t[[nodiscard]] rpl::producer<> widthChanges() const;\n\nprivate:\n\t[[nodiscard]] int filledWidth() const;\n\t[[nodiscard]] int topTextWidth() const;\n\t[[nodiscard]] int bottomTextWidth() const;\n\n\tconst style::PremiumBubble &_st;\n\n\tconst Fn<void()> _updateCallback;\n\tconst TextFactory _textFactory;\n\n\tconst style::icon *_icon;\n\tNumbersAnimation _numberAnimation;\n\tText::String _additional;\n\tText::String _subtext;\n\tconst int _height;\n\tconst bool _hasTail;\n\n\tstd::optional<int> _counter;\n\tEdgeProgress _tailEdge = 0.;\n\tbool _flipHorizontal = false;\n\n\trpl::event_stream<> _widthChanges;\n\n};\n\nstruct BubbleRowState {\n\tint counter = 0;\n\tfloat64 ratio = 0.;\n\tbool animateFromZero = false;\n\tbool dynamic = false;\n};\n\nenum class BubbleType : uchar {\n\tUpgradePrice,\n\tStarRating,\n\tNegativeRating,\n\tNoPremium,\n\tPremium,\n\tCredits,\n};\n\nclass BubbleWidget final : public Ui::AbstractButton {\npublic:\n\tBubbleWidget(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::PremiumBubble &st,\n\t\tTextFactory textFactory,\n\t\trpl::producer<BubbleRowState> state,\n\t\tBubbleType type,\n\t\trpl::producer<> showFinishes,\n\t\tconst style::icon *icon,\n\t\tconst style::margins &outerPadding);\n\n\tvoid setBrushOverride(std::optional<QBrush> brushOverride);\n\tvoid setSubtext(QString subtext);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid setupParticles(not_null<Ui::RpWidget*> parent);\n\n\tstruct GradientParams {\n\t\tint left = 0;\n\t\tint width = 0;\n\t\tint outer = 0;\n\n\t\tfriend inline constexpr bool operator==(\n\t\t\tGradientParams,\n\t\t\tGradientParams) = default;\n\t};\n\tvoid animateTo(BubbleRowState state);\n\n\tconst style::PremiumBubble &_st;\n\tBubbleRowState _animatingFrom;\n\tfloat64 _animatingFromResultRatio = 0.;\n\tfloat64 _animatingFromBubbleEdge = 0.;\n\trpl::variable<BubbleRowState> _state;\n\tBubble _bubble;\n\tstd::optional<QBrush> _brushOverride;\n\tconst BubbleType _type;\n\tconst style::margins _outerPadding;\n\n\tUi::Animations::Simple _appearanceAnimation;\n\tFn<void(float64)> _appearanceCallback;\n\tQSize _spaceForDeflection;\n\n\tQLinearGradient _cachedGradient;\n\tstd::optional<GradientParams> _cachedGradientParams;\n\n\tfloat64 _deflection;\n\n\tbool _ignoreDeflection = false;\n\tfloat64 _stepBeforeDeflection;\n\tfloat64 _stepAfterDeflection;\n\n\tRpWidget *_particlesWidget = nullptr;\n\tstd::optional<StarParticles> _particles;\n\tUi::Animations::Basic _particlesAnimation;\n\n};\n\nnot_null<BubbleWidget*> AddBubbleRow(\n\tnot_null<Ui::VerticalLayout*> parent,\n\tconst style::PremiumBubble &st,\n\trpl::producer<> showFinishes,\n\tint min,\n\tint current,\n\tint max,\n\tBubbleType type,\n\tstd::optional<tr::phrase<lngtag_count>> phrase,\n\tconst style::icon *icon);\n\nnot_null<BubbleWidget*> AddBubbleRow(\n\tnot_null<Ui::VerticalLayout*> parent,\n\tconst style::PremiumBubble &st,\n\trpl::producer<> showFinishes,\n\trpl::producer<BubbleRowState> state,\n\tBubbleType type,\n\tTextFactory text,\n\tconst style::icon *icon,\n\tconst style::margins &outerPadding);\n\n} // namespace Ui::Premium\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/premium_graphics.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/premium_graphics.h\"\n\n#include \"data/data_premium_subscription_option.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/abstract_button.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/credits_graphics.h\"\n#include \"ui/effects/gradient.h\"\n#include \"ui/effects/numbers_animation.h\"\n#include \"ui/effects/premium_bubble.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_window.h\"\n\n#include <QtCore/QFile>\n#include <QtGui/QBrush>\n#include <QtSvg/QSvgRenderer>\n\nnamespace Ui {\nnamespace Premium {\nnamespace {\n\nclass GradientRadioView : public Ui::RadioView {\npublic:\n\tGradientRadioView(\n\t\tconst style::Radio &st,\n\t\tbool checked,\n\t\tFn<void()> updateCallback = nullptr);\n\n\tvoid setBrush(std::optional<QBrush> brush);\n\n\tvoid paint(QPainter &p, int left, int top, int outerWidth) override;\n\nprivate:\n\n\tnot_null<const style::Radio*> _st;\n\tstd::optional<QBrush> _brushOverride;\n\n};\n\nGradientRadioView::GradientRadioView(\n\tconst style::Radio &st,\n\tbool checked,\n\tFn<void()> updateCallback)\n: Ui::RadioView(st, checked, updateCallback)\n, _st(&st) {\n}\n\nvoid GradientRadioView::paint(QPainter &p, int left, int top, int outerW) {\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tconst auto toggled = currentAnimationValue();\n\tconst auto toggledFg = _brushOverride\n\t\t? (*_brushOverride)\n\t\t: QBrush(_st->toggledFg);\n\n\t{\n\t\tconst auto skip = (_st->outerSkip / 10.) + (_st->thickness / 2);\n\t\tconst auto rect = QRectF(left, top, _st->diameter, _st->diameter)\n\t\t\t- Margins(skip);\n\n\t\tp.setBrush(_st->bg);\n\t\tif (toggled < 1) {\n\t\t\tp.setPen(QPen(_st->untoggledFg, _st->thickness));\n\t\t\tp.drawEllipse(style::rtlrect(rect, outerW));\n\t\t}\n\t\tif (toggled > 0) {\n\t\t\tp.setOpacity(toggled);\n\t\t\tp.setPen(QPen(toggledFg, _st->thickness));\n\t\t\tp.drawEllipse(style::rtlrect(rect, outerW));\n\t\t}\n\t}\n\n\tif (toggled > 0) {\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(toggledFg);\n\n\t\tconst auto skip0 = _st->diameter / 2.;\n\t\tconst auto skip1 = _st->skip / 10.;\n\t\tconst auto checkSkip = skip0 * (1. - toggled) + skip1 * toggled;\n\t\tconst auto rect = QRectF(left, top, _st->diameter, _st->diameter)\n\t\t\t- Margins(checkSkip);\n\t\tp.drawEllipse(style::rtlrect(rect, outerW));\n\t}\n}\n\nvoid GradientRadioView::setBrush(std::optional<QBrush> brush) {\n\t_brushOverride = brush;\n}\n\nclass PartialGradient final {\npublic:\n\tPartialGradient(int from, int to, QGradientStops stops);\n\n\t[[nodiscard]] QLinearGradient compute(int position, int size) const;\n\nprivate:\n\tconst int _from;\n\tconst int _to;\n\tQLinearGradient _gradient;\n\n};\n\nPartialGradient::PartialGradient(int from, int to, QGradientStops stops)\n: _from(from)\n, _to(to)\n, _gradient(0, 0, 0, to - from) {\n\t_gradient.setStops(std::move(stops));\n}\n\nQLinearGradient PartialGradient::compute(int position, int size) const {\n\tconst auto pointTop = position - _from;\n\tconst auto pointBottom = pointTop + size;\n\tconst auto ratioTop = pointTop / float64(_to - _from);\n\tconst auto ratioBottom = pointBottom / float64(_to - _from);\n\n\tauto resultGradient = QLinearGradient(\n\t\tQPointF(),\n\t\tQPointF(0, pointBottom - pointTop));\n\n\tresultGradient.setColorAt(\n\t\t.0,\n\t\tanim::gradient_color_at(_gradient, ratioTop));\n\tresultGradient.setColorAt(\n\t\t.1,\n\t\tanim::gradient_color_at(_gradient, ratioBottom));\n\treturn resultGradient;\n}\n\nclass Line final : public Ui::RpWidget {\npublic:\n\tLine(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::PremiumLimits &st,\n\t\tint max,\n\t\tTextFactory textFactory,\n\t\tint min,\n\t\tfloat64 ratio);\n\tLine(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::PremiumLimits &st,\n\t\tQString max,\n\t\tQString min,\n\t\tfloat64 ratio);\n\n\tLine(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::PremiumLimits &st,\n\t\tLimitRowLabels labels,\n\t\trpl::producer<LimitRowState> state);\n\n\tvoid setColorOverride(QBrush brush);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *event) override;\n\nprivate:\n\tvoid recache(const QSize &s);\n\n\tconst style::PremiumLimits &_st;\n\n\tQPixmap _leftPixmap;\n\tQPixmap _rightPixmap;\n\n\tfloat64 _ratio = 0.;\n\tUi::Animations::Simple _animation;\n\trpl::event_stream<> _recaches;\n\tUi::Text::String _leftLabel;\n\tUi::Text::String _leftText;\n\tUi::Text::String _rightLabel;\n\tUi::Text::String _rightText;\n\tbool _dynamic = false;\n\n\tstd::optional<QBrush> _overrideBrush;\n\n};\n\nLine::Line(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst style::PremiumLimits &st,\n\tint max,\n\tTextFactory textFactory,\n\tint min,\n\tfloat64 ratio)\n: Line(\n\tparent,\n\tst,\n\tmax ? textFactory(max).counter : QString(),\n\tmin ? textFactory(min).counter : QString(),\n\tratio) {\n}\n\nLine::Line(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst style::PremiumLimits &st,\n\tQString max,\n\tQString min,\n\tfloat64 ratio)\n: Line(parent, st, LimitRowLabels{\n\t.leftLabel = tr::lng_premium_free(),\n\t.leftCount = rpl::single(min),\n\t.rightLabel = tr::lng_premium(),\n\t.rightCount = rpl::single(max),\n}, rpl::single(LimitRowState{ ratio })) {\n}\n\nLine::Line(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst style::PremiumLimits &st,\n\tLimitRowLabels labels,\n\trpl::producer<LimitRowState> state)\n: Ui::RpWidget(parent)\n, _st(st) {\n\tresize(width(), st::requestsAcceptButton.height);\n\n\tconst auto set = [&](\n\t\t\tUi::Text::String &label,\n\t\t\trpl::producer<QString> &text) {\n\t\tstd::move(text) | rpl::on_next([=, &label](QString text) {\n\t\t\tlabel = { st::semiboldTextStyle, text };\n\t\t\t_recaches.fire({});\n\t\t}, lifetime());\n\t};\n\tset(_leftLabel, labels.leftLabel);\n\tset(_leftText, labels.leftCount);\n\tset(_rightLabel, labels.rightLabel);\n\tset(_rightText, labels.rightCount);\n\n\tstd::move(state) | rpl::on_next([=](LimitRowState state) {\n\t\t_dynamic = state.dynamic;\n\t\tif (width() > 0) {\n\t\t\tconst auto from = state.animateFromZero\n\t\t\t\t? 0.\n\t\t\t\t: _animation.value(_ratio);\n\t\t\tconst auto duration = Bubble::SlideNoDeflectionDuration();\n\t\t\t_animation.start([=] {\n\t\t\t\tupdate();\n\t\t\t}, from, state.ratio, duration, anim::easeOutCirc);\n\t\t}\n\t\t_ratio = state.ratio;\n\t}, lifetime());\n\n\trpl::combine(\n\t\tsizeValue(),\n\t\tparent->widthValue(),\n\t\t_recaches.events_starting_with({})\n\t) | rpl::filter([](const QSize &size, int parentWidth, auto) {\n\t\treturn !size.isEmpty() && parentWidth;\n\t}) | rpl::on_next([=](const QSize &size, auto, auto) {\n\t\trecache(size);\n\t\tupdate();\n\t}, lifetime());\n\n}\n\nvoid Line::setColorOverride(QBrush brush) {\n\tif (brush.style() == Qt::NoBrush) {\n\t\t_overrideBrush = std::nullopt;\n\t} else {\n\t\t_overrideBrush = brush;\n\t}\n}\n\nvoid Line::paintEvent(QPaintEvent *event) {\n\tPainter p(this);\n\n\tconst auto ratio = _animation.value(_ratio);\n\tconst auto left = int(base::SafeRound(ratio * width()));\n\tconst auto dpr = int(_leftPixmap.devicePixelRatio());\n\tconst auto height = _leftPixmap.height() / dpr;\n\tp.drawPixmap(\n\t\tQRect(0, 0, left, height),\n\t\t_leftPixmap,\n\t\tQRect(0, 0, left * dpr, height * dpr));\n\tp.drawPixmap(\n\t\tQRect(left, 0, width() - left, height),\n\t\t_rightPixmap,\n\t\tQRect(left * dpr, 0, (width() - left) * dpr, height * dpr));\n\n\tp.setFont(st::normalFont);\n\n\tconst auto textPadding = st::premiumLineTextSkip;\n\tconst auto textTop = (height - _leftLabel.minHeight()) / 2;\n\n\tconst auto leftMinWidth = _leftLabel.maxWidth()\n\t\t+ _leftText.maxWidth()\n\t\t+ 3 * textPadding;\n\tconst auto pen = [&](bool gradient) {\n\t\treturn gradient ? st::activeButtonFg : _st.nonPremiumFg;\n\t};\n\tif (!_dynamic && left >= leftMinWidth) {\n\t\tp.setPen(pen(_st.gradientFromLeft));\n\t\t_leftLabel.drawLeft(\n\t\t\tp,\n\t\t\ttextPadding,\n\t\t\ttextTop,\n\t\t\tleft - textPadding,\n\t\t\tleft);\n\t\t_leftText.drawRight(\n\t\t\tp,\n\t\t\ttextPadding,\n\t\t\ttextTop,\n\t\t\tleft - textPadding,\n\t\t\tleft,\n\t\t\tstyle::al_right);\n\t}\n\tconst auto right = width() - left;\n\tconst auto rightMinWidth = 2 * _rightText.maxWidth()\n\t\t+ 3 * textPadding;\n\tif (!_dynamic && right >= rightMinWidth) {\n\t\tp.setPen(pen(!_st.gradientFromLeft));\n\t\t_rightLabel.drawLeftElided(\n\t\t\tp,\n\t\t\tleft + textPadding,\n\t\t\ttextTop,\n\t\t\t(right - _rightText.countWidth(right) - textPadding * 2),\n\t\t\tright);\n\t\t_rightText.drawRight(\n\t\t\tp,\n\t\t\ttextPadding,\n\t\t\ttextTop,\n\t\t\tright - textPadding,\n\t\t\twidth(),\n\t\t\tstyle::al_right);\n\t}\n}\n\nvoid Line::recache(const QSize &s) {\n\tconst auto r = [&](int width) {\n\t\treturn QRect(0, 0, width, s.height());\n\t};\n\tconst auto pixmap = [&](int width) {\n\t\tauto result = QPixmap(r(width).size() * style::DevicePixelRatio());\n\t\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\t\tresult.fill(Qt::transparent);\n\t\treturn result;\n\t};\n\n\tconst auto pathRound = [&](int width) {\n\t\tauto result = QPainterPath();\n\t\tresult.addRoundedRect(\n\t\t\tr(width),\n\t\t\tst::premiumLineRadius,\n\t\t\tst::premiumLineRadius);\n\t\treturn result;\n\t};\n\tconst auto width = s.width();\n\tconst auto fill = [&](QPainter &p, QPainterPath path, bool gradient) {\n\t\tif (!gradient) {\n\t\t\tp.fillPath(path, _st.nonPremiumBg);\n\t\t} else if (_overrideBrush) {\n\t\t\tp.fillPath(path, *_overrideBrush);\n\t\t} else {\n\t\t\tp.fillPath(path, QBrush(ComputeGradient(this, 0, width)));\n\t\t}\n\t};\n\tconst auto textPadding = st::premiumLineTextSkip;\n\tconst auto rwidth = _rightLabel.maxWidth();\n\tconst auto pen = [&](bool gradient) {\n\t\treturn gradient ? st::activeButtonFg : _st.nonPremiumFg;\n\t};\n\t{\n\t\tauto leftPixmap = pixmap(width);\n\t\tauto p = Painter(&leftPixmap);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tfill(p, pathRound(width), _st.gradientFromLeft);\n\t\tif (_dynamic) {\n\t\t\tp.setFont(st::normalFont);\n\t\t\tp.setPen(pen(_st.gradientFromLeft));\n\t\t\tconst auto leftTop = (s.height() - _leftLabel.minHeight()) / 2;\n\t\t\t_leftLabel.drawLeft(p, textPadding, leftTop, width, width);\n\t\t\tconst auto rightTop = (s.height() - _rightLabel.minHeight()) / 2;\n\t\t\t_rightLabel.drawRight(p, textPadding, rightTop, rwidth, width);\n\t\t}\n\t\t_leftPixmap = std::move(leftPixmap);\n\t}\n\t{\n\t\tauto rightPixmap = pixmap(width);\n\t\tauto p = Painter(&rightPixmap);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tfill(p, pathRound(width), !_st.gradientFromLeft);\n\t\tif (_dynamic) {\n\t\t\tp.setFont(st::normalFont);\n\t\t\tp.setPen(pen(!_st.gradientFromLeft));\n\t\t\tconst auto leftTop = (s.height() - _leftLabel.minHeight()) / 2;\n\t\t\t_leftLabel.drawLeft(p, textPadding, leftTop, width, width);\n\t\t\tconst auto rightTop = (s.height() - _rightLabel.minHeight()) / 2;\n\t\t\t_rightLabel.drawRight(p, textPadding, rightTop, rwidth, width);\n\t\t}\n\t\t_rightPixmap = std::move(rightPixmap);\n\t}\n}\n\n} // namespace\n\nQString Svg() {\n\treturn u\":/gui/icons/settings/star.svg\"_q;\n}\n\nQByteArray ColorizedSvg(const QGradientStops &gradientStops) {\n\tauto f = QFile(Svg());\n\tif (!f.open(QIODevice::ReadOnly)) {\n\t\treturn QByteArray();\n\t}\n\tauto content = QString::fromUtf8(f.readAll());\n\tauto stops = [&] {\n\t\tauto s = QString();\n\t\tfor (const auto &stop : gradientStops) {\n\t\t\ts += QString(\"<stop offset='%1' stop-color='%2'/>\")\n\t\t\t\t.arg(QString::number(stop.first), stop.second.name());\n\t\t}\n\t\treturn s;\n\t}();\n\tconst auto color = QString(\"<linearGradient id='Gradient2' \"\n\t\t\"x1='%1' x2='%2' y1='%3' y2='%4'>%5</linearGradient>\")\n\t\t.arg(0)\n\t\t.arg(1)\n\t\t.arg(1)\n\t\t.arg(0)\n\t\t.arg(std::move(stops));\n\tcontent.replace(u\"gradientPlaceholder\"_q, color);\n\tcontent.replace(u\"#fff\"_q, u\"url(#Gradient2)\"_q);\n\tf.close();\n\treturn content.toUtf8();\n}\n\nQImage GenerateStarForLightTopBar(QRectF rect) {\n\tauto svg = QSvgRenderer(Ui::Premium::Svg());\n\n\tconst auto size = rect.size().toSize();\n\tauto frame = QImage(\n\t\tsize * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tframe.setDevicePixelRatio(style::DevicePixelRatio());\n\n\tauto mask = frame;\n\tmask.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&mask);\n\t\tauto gradient = QLinearGradient(\n\t\t\t0,\n\t\t\tsize.height(),\n\t\t\tsize.width(),\n\t\t\t0);\n\t\tgradient.setStops(Ui::Premium::ButtonGradientStops());\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(gradient);\n\t\tp.drawRect(0, 0, size.width(), size.height());\n\t}\n\tframe.fill(Qt::transparent);\n\t{\n\t\tauto q = QPainter(&frame);\n\t\tsvg.render(&q, QRect(QPoint(), size));\n\t\tq.setCompositionMode(QPainter::CompositionMode_SourceIn);\n\t\tq.drawImage(0, 0, mask);\n\t}\n\treturn frame;\n}\n\nvoid AddLimitRow(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tconst style::PremiumLimits &st,\n\t\tQString max,\n\t\tQString min,\n\t\tfloat64 ratio) {\n\tparent->add(\n\t\tobject_ptr<Line>(parent, st, max, min, ratio),\n\t\tst::boxRowPadding);\n}\n\nvoid AddLimitRow(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tconst style::PremiumLimits &st,\n\t\tint max,\n\t\tstd::optional<tr::phrase<lngtag_count>> phrase,\n\t\tint min,\n\t\tfloat64 ratio) {\n\tconst auto factory = ProcessTextFactory(phrase);\n\tAddLimitRow(\n\t\tparent,\n\t\tst,\n\t\tmax ? factory(max).counter : QString(),\n\t\tmin ? factory(min).counter : QString(),\n\t\tratio);\n}\n\nvoid AddLimitRow(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tconst style::PremiumLimits &st,\n\t\tLimitRowLabels labels,\n\t\trpl::producer<LimitRowState> state,\n\t\tconst style::margins &padding) {\n\tconst auto color = std::move(labels.activeLineBg);\n\tconst auto line = parent->add(\n\t\tobject_ptr<Line>(parent, st, std::move(labels), std::move(state)),\n\t\tpadding);\n\tif (color) {\n\t\tline->setColorOverride(color());\n\n\t\tstyle::PaletteChanged() | rpl::on_next([=] {\n\t\t\tline->setColorOverride(color());\n\t\t}, line->lifetime());\n\t}\n}\n\nvoid AddAccountsRow(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tAccountsRowArgs &&args) {\n\tconst auto container = parent->add(\n\t\tobject_ptr<Ui::FixedHeightWidget>(parent, st::premiumAccountsHeight),\n\t\tst::boxRowPadding);\n\n\tstruct Account {\n\t\tnot_null<Ui::AbstractButton*> widget;\n\t\tUi::RoundImageCheckbox checkbox;\n\t\tUi::Text::String name;\n\t\tQPixmap badge;\n\t};\n\tstruct State {\n\t\tstd::vector<Account> accounts;\n\t};\n\tconst auto state = container->lifetime().make_state<State>();\n\tconst auto group = args.group;\n\n\tconst auto imageRadius = args.st.imageRadius;\n\tconst auto checkSelectWidth = args.st.selectWidth;\n\tconst auto nameFg = args.stNameFg;\n\n\tconst auto cacheBadge = [=](int center) {\n\t\tconst auto &padding = st::premiumAccountsLabelPadding;\n\t\tconst auto size = st::premiumAccountsLabelSize\n\t\t\t+ QSize(\n\t\t\t\tpadding.left() + padding.right(),\n\t\t\t\tpadding.top() + padding.bottom());\n\t\tauto badge = QPixmap(size * style::DevicePixelRatio());\n\t\tbadge.setDevicePixelRatio(style::DevicePixelRatio());\n\t\tbadge.fill(Qt::transparent);\n\n\t\tauto p = QPainter(&badge);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\tp.setPen(Qt::NoPen);\n\t\tconst auto rectOut = QRect(QPoint(), size);\n\t\tconst auto rectIn = rectOut - padding;\n\n\t\tconst auto radius = st::premiumAccountsLabelRadius;\n\t\tp.setBrush(st::premiumButtonFg);\n\t\tp.drawRoundedRect(rectOut, radius, radius);\n\n\t\tconst auto left = center - rectIn.width() / 2;\n\t\tp.setBrush(QBrush(ComputeGradient(container, left, rectIn.width())));\n\t\tp.drawRoundedRect(rectIn, radius / 2, radius / 2);\n\n\t\tp.setPen(st::premiumButtonFg);\n\t\tp.setFont(st::semiboldFont);\n\t\tp.drawText(rectIn, u\"+1\"_q, style::al_center);\n\n\t\treturn badge;\n\t};\n\n\tfor (auto &entry : args.entries) {\n\t\tconst auto widget = Ui::CreateChild<Ui::AbstractButton>(container);\n\t\tauto name = Ui::Text::String(imageRadius * 2);\n\t\tname.setText(args.stName, entry.name, Ui::NameTextOptions());\n\t\tstate->accounts.push_back(Account{\n\t\t\t.widget = widget,\n\t\t\t.checkbox = Ui::RoundImageCheckbox(\n\t\t\t\targs.st,\n\t\t\t\t[=] { widget->update(); },\n\t\t\t\tbase::take(entry.paintRoundImage)),\n\t\t\t.name = std::move(name),\n\t\t});\n\t\tconst auto index = int(state->accounts.size()) - 1;\n\t\tstate->accounts[index].checkbox.setChecked(\n\t\t\tindex == group->current(),\n\t\t\tanim::type::instant);\n\n\t\twidget->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tPainter p(widget);\n\t\t\tconst auto width = widget->width();\n\t\t\tconst auto photoLeft = (width - (imageRadius * 2)) / 2;\n\t\t\tconst auto photoTop = checkSelectWidth;\n\t\t\tconst auto &account = state->accounts[index];\n\t\t\taccount.checkbox.paint(p, photoLeft, photoTop, width);\n\n\t\t\tconst auto &badgeSize = account.badge.size()\n\t\t\t\t/ style::DevicePixelRatio();\n\t\t\tp.drawPixmap(\n\t\t\t\t(width - badgeSize.width()) / 2,\n\t\t\t\tphotoTop + (imageRadius * 2) - badgeSize.height() / 2,\n\t\t\t\taccount.badge);\n\n\t\t\tp.setPen(nameFg);\n\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\taccount.name.drawLeftElided(\n\t\t\t\tp,\n\t\t\t\t0,\n\t\t\t\tphotoTop + imageRadius * 2 + st::premiumAccountsNameTop,\n\t\t\t\twidth,\n\t\t\t\twidth,\n\t\t\t\t2,\n\t\t\t\tstyle::al_top,\n\t\t\t\t0,\n\t\t\t\t-1,\n\t\t\t\t0,\n\t\t\t\ttrue);\n\t\t}, widget->lifetime());\n\n\t\twidget->setClickedCallback([=] {\n\t\t\tgroup->setValue(index);\n\t\t});\n\t}\n\n\tcontainer->sizeValue(\n\t) | rpl::on_next([=](const QSize &size) {\n\t\tconst auto count = state->accounts.size();\n\t\tconst auto columnWidth = size.width() / count;\n\t\tfor (auto i = 0; i < count; i++) {\n\t\t\tauto &account = state->accounts[i];\n\t\t\taccount.widget->resize(columnWidth, size.height());\n\t\t\tconst auto left = columnWidth * i;\n\t\t\taccount.widget->moveToLeft(left, 0);\n\t\t\taccount.badge = cacheBadge(left + columnWidth / 2);\n\n\t\t\tconst auto photoWidth = ((imageRadius + checkSelectWidth) * 2);\n\t\t\taccount.checkbox.setColorOverride(QBrush(\n\t\t\t\tComputeGradient(\n\t\t\t\t\tcontainer,\n\t\t\t\t\tleft + (columnWidth - photoWidth) / 2,\n\t\t\t\t\tphotoWidth)));\n\t\t}\n\t}, container->lifetime());\n\n\tgroup->setChangedCallback([=](int value) {\n\t\tfor (auto i = 0; i < state->accounts.size(); i++) {\n\t\t\tstate->accounts[i].checkbox.setChecked(i == value);\n\t\t}\n\t});\n}\n\nQGradientStops LimitGradientStops() {\n\treturn {\n\t\t{ 0.0, st::premiumButtonBg1->c },\n\t\t{ .25, st::premiumButtonBg1->c },\n\t\t{ .85, st::premiumButtonBg2->c },\n\t\t{ 1.0, st::premiumButtonBg3->c },\n\t};\n}\n\nQGradientStops ButtonGradientStops() {\n\treturn {\n\t\t{ 0., st::premiumButtonBg1->c },\n\t\t{ 0.6, st::premiumButtonBg2->c },\n\t\t{ 1., st::premiumButtonBg3->c },\n\t};\n}\n\nQGradientStops LockGradientStops() {\n\treturn ButtonGradientStops();\n}\n\nQGradientStops FullHeightGradientStops() {\n\treturn {\n\t\t{ 0.0, st::premiumIconBg1->c },\n\t\t{ .28, st::premiumIconBg2->c },\n\t\t{ .55, st::premiumButtonBg2->c },\n\t\t{ .75, st::premiumButtonBg1->c },\n\t\t{ 1.0, st::premiumIconBg3->c },\n\t};\n}\n\nQGradientStops GiftGradientStops() {\n\treturn {\n\t\t{ 0., st::premiumButtonBg1->c },\n\t\t{ 1., st::premiumButtonBg2->c },\n\t};\n}\n\nQGradientStops StoriesIconsGradientStops() {\n\treturn {\n\t\t{ 0., st::premiumButtonBg1->c },\n\t\t{ .33, st::premiumButtonBg2->c },\n\t\t{ .66, st::premiumButtonBg3->c },\n\t\t{ 1., st::premiumIconBg1->c },\n\t};\n}\n\nQGradientStops CreditsIconGradientStops() {\n\treturn {\n\t\t{ 0., st::creditsBg1->c },\n\t\t{ 1., st::creditsBg2->c },\n\t};\n}\n\nQLinearGradient ComputeGradient(\n\t\tnot_null<QWidget*> content,\n\t\tint left,\n\t\tint width) {\n\t// Take a full width of parent box without paddings.\n\tconst auto fullGradientWidth = content->parentWidget()->width();\n\tauto fullGradient = QLinearGradient(0, 0, fullGradientWidth, 0);\n\tfullGradient.setStops(ButtonGradientStops());\n\n\tauto gradient = QLinearGradient(0, 0, width, 0);\n\tconst auto fullFinal = float64(fullGradient.finalStop().x());\n\tleft += ((fullGradientWidth - content->width()) / 2);\n\tgradient.setColorAt(\n\t\t.0,\n\t\tanim::gradient_color_at(fullGradient, left / fullFinal));\n\tgradient.setColorAt(\n\t\t1.,\n\t\tanim::gradient_color_at(\n\t\t\tfullGradient,\n\t\t\tstd::min(1., (left + width) / fullFinal)));\n\n\treturn gradient;\n}\n\nvoid ShowListBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tconst style::PremiumLimits &st,\n\t\tstd::vector<ListEntry> entries) {\n\tbox->setWidth(st::boxWideWidth);\n\n\tconst auto &stLabel = st::defaultFlatLabel;\n\tconst auto &titlePadding = st::settingsPremiumPreviewTitlePadding;\n\tconst auto &aboutPadding = st::settingsPremiumPreviewAboutPadding;\n\tconst auto iconTitlePadding = st::settingsPremiumPreviewIconTitlePadding;\n\tconst auto iconAboutPadding = st::settingsPremiumPreviewIconAboutPadding;\n\n\tauto lines = std::vector<Line*>();\n\tlines.reserve(int(entries.size()));\n\n\tauto icons = std::shared_ptr<std::vector<QColor>>();\n\n\tconst auto content = box->verticalLayout();\n\tfor (auto &entry : entries) {\n\t\tconst auto title = content->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\tbase::take(entry.title) | rpl::map(tr::bold),\n\t\t\t\tstLabel),\n\t\t\tentry.icon ? iconTitlePadding : titlePadding);\n\t\tcontent->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tcontent,\n\t\t\t\tbase::take(entry.about),\n\t\t\t\tst::boxDividerLabel),\n\t\t\tentry.icon ? iconAboutPadding : aboutPadding);\n\t\tif (const auto outlined = entry.icon) {\n\t\t\tif (!icons) {\n\t\t\t\ticons = std::make_shared<std::vector<QColor>>();\n\t\t\t}\n\t\t\tconst auto index = int(icons->size());\n\t\t\ticons->push_back(QColor());\n\t\t\tconst auto icon = Ui::CreateChild<Ui::RpWidget>(content.get());\n\t\t\ticon->resize(outlined->size());\n\t\t\ttitle->topValue() | rpl::on_next([=](int y) {\n\t\t\t\tconst auto shift = st::settingsPremiumPreviewIconPosition;\n\t\t\t\ticon->move(QPoint(0, y) + shift);\n\t\t\t}, icon->lifetime());\n\t\t\ticon->paintRequest() | rpl::on_next([=] {\n\t\t\t\tauto p = QPainter(icon);\n\t\t\t\toutlined->paintInCenter(p, icon->rect(), (*icons)[index]);\n\t\t\t}, icon->lifetime());\n\t\t}\n\t\tif (entry.leftNumber || entry.rightNumber) {\n\t\t\tauto factory = [=, text = ProcessTextFactory({})](int n) {\n\t\t\t\tif (entry.customRightText && (n == entry.rightNumber)) {\n\t\t\t\t\treturn Ui::Premium::BubbleText{ *entry.customRightText };\n\t\t\t\t} else {\n\t\t\t\t\treturn Ui::Premium::BubbleText{ text(n) };\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto limitRow = content->add(\n\t\t\t\tobject_ptr<Line>(\n\t\t\t\t\tcontent,\n\t\t\t\t\tst,\n\t\t\t\t\tentry.rightNumber,\n\t\t\t\t\tTextFactory(std::move(factory)),\n\t\t\t\t\tentry.leftNumber,\n\t\t\t\t\tkLimitRowRatio),\n\t\t\t\tst::settingsPremiumPreviewLinePadding);\n\t\t\tlines.push_back(limitRow);\n\t\t}\n\t}\n\n\tcontent->resizeToWidth(content->height());\n\n\t// Colors for icons.\n\tif (icons) {\n\t\tbox->addSkip(st::settingsPremiumPreviewLinePadding.bottom());\n\n\t\tconst auto stops = Ui::Premium::StoriesIconsGradientStops();\n\t\tfor (auto i = 0, count = int(icons->size()); i != count; ++i) {\n\t\t\t(*icons)[i] = anim::gradient_color_at(\n\t\t\t\tstops,\n\t\t\t\t(count > 1) ? (i / float64(count - 1)) : 0.);\n\t\t}\n\t}\n\n\t// Color lines.\n\tif (!lines.empty()) {\n\t\tbox->addSkip(st::settingsPremiumPreviewLinePadding.bottom());\n\n\t\tconst auto from = lines.front()->y();\n\t\tconst auto to = lines.back()->y() + lines.back()->height();\n\n\t\tconst auto partialGradient = [&] {\n\t\t\tauto stops = Ui::Premium::FullHeightGradientStops();\n\t\t\t// Reverse.\n\t\t\tfor (auto &stop : stops) {\n\t\t\t\tstop.first = std::abs(stop.first - 1.);\n\t\t\t}\n\t\t\treturn PartialGradient(from, to, std::move(stops));\n\t\t}();\n\n\t\tfor (auto i = 0; i < int(lines.size()); i++) {\n\t\t\tconst auto &line = lines[i];\n\n\t\t\tconst auto brush = QBrush(\n\t\t\t\tpartialGradient.compute(line->y(), line->height()));\n\t\t\tline->setColorOverride(brush);\n\t\t}\n\t\tbox->addSkip(st::settingsPremiumPreviewLinePadding.bottom());\n\t}\n}\n\nvoid AddGiftOptions(\n\t\tnot_null<Ui::VerticalLayout*> parent,\n\t\tstd::shared_ptr<Ui::RadiobuttonGroup> group,\n\t\tstd::vector<Data::PremiumSubscriptionOption> gifts,\n\t\tconst style::PremiumOption &st,\n\t\tbool topBadges) {\n\n\tstruct Edges {\n\t\tUi::RpWidget *top = nullptr;\n\t\tUi::RpWidget *bottom = nullptr;\n\t};\n\tconst auto edges = parent->lifetime().make_state<Edges>();\n\tstruct Animation {\n\t\tint nowIndex = 0;\n\t\tUi::Animations::Simple animation;\n\t};\n\tconst auto wasGroupValue = group->current();\n\tconst auto animation = parent->lifetime().make_state<Animation>();\n\tanimation->nowIndex = wasGroupValue;\n\n\tconst auto stops = GiftGradientStops();\n\n\tconst auto addRow = [&](\n\t\t\tconst Data::PremiumSubscriptionOption &info,\n\t\t\tint index) {\n\t\tconst auto row = parent->add(\n\t\t\tobject_ptr<Ui::AbstractButton>(parent),\n\t\t\tst.rowPadding);\n\t\trow->resize(row->width(), st.rowHeight);\n\t\t{\n\t\t\tif (!index) {\n\t\t\t\tedges->top = row;\n\t\t\t}\n\t\t\tedges->bottom = row;\n\t\t}\n\n\t\tconst auto &stCheckbox = st::defaultBoxCheckbox;\n\t\tauto radioView = std::make_unique<GradientRadioView>(\n\t\t\tst::defaultRadio,\n\t\t\t(group->hasValue() && group->current() == index));\n\t\tconst auto radioViewRaw = radioView.get();\n\t\tconst auto radio = Ui::CreateChild<Ui::Radiobutton>(\n\t\t\trow,\n\t\t\tgroup,\n\t\t\tindex,\n\t\t\tQString(),\n\t\t\tstCheckbox,\n\t\t\tstd::move(radioView));\n\t\tradio->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tradio->show();\n\t\t{ // Paint the last frame instantly for the layer animation.\n\t\t\tgroup->setValue(0);\n\t\t\tgroup->setValue(wasGroupValue);\n\t\t\tradio->finishAnimating();\n\t\t}\n\n\t\trow->sizeValue(\n\t\t) | rpl::on_next([=, margins = stCheckbox.margin](\n\t\t\t\tconst QSize &s) {\n\t\t\tconst auto radioHeight = radio->height()\n\t\t\t\t- margins.top()\n\t\t\t\t- margins.bottom();\n\t\t\tradio->moveToLeft(\n\t\t\t\tst.rowMargins.left(),\n\t\t\t\t(s.height() - radioHeight) / 2);\n\t\t}, radio->lifetime());\n\n\t\t{\n\t\t\tauto onceLifetime = std::make_shared<rpl::lifetime>();\n\t\t\trow->paintRequest(\n\t\t\t) | rpl::take(\n\t\t\t\t1\n\t\t\t) | rpl::on_next([=]() mutable {\n\t\t\t\tconst auto from = edges->top->y();\n\t\t\t\tconst auto to = edges->bottom->y() + edges->bottom->height();\n\t\t\t\tauto partialGradient = PartialGradient(from, to, stops);\n\n\t\t\t\tradioViewRaw->setBrush(\n\t\t\t\t\tpartialGradient.compute(row->y(), row->height()));\n\t\t\t\tif (onceLifetime) {\n\t\t\t\t\tbase::take(onceLifetime)->destroy();\n\t\t\t\t}\n\t\t\t}, *onceLifetime);\n\t\t}\n\n\t\tconstexpr auto kStar = QChar(0x2B50);\n\t\tconst auto removedStar = [&](QString s) {\n\t\t\treturn s.replace(kStar, QChar());\n\t\t};\n\t\tconst auto &costPerMonthFont = st::shareBoxListItem.nameStyle.font;\n\t\tconst auto &costPerYearFont = st::normalFont;\n\t\tconst auto costPerMonthIcon = info.costPerMonth.startsWith(kStar)\n\t\t\t? GenerateStars(costPerMonthFont->height, 1)\n\t\t\t: QImage();\n\t\tauto leftText = TextWithEntities();\n\t\tif (!info.costNoDiscount.isEmpty()) {\n\t\t\tleftText.append(Ui::Text::Wrapped(\n\t\t\t\tTextWithEntities{ info.costNoDiscount },\n\t\t\t\tEntityType::StrikeOut));\n\t\t\tleftText.append(' ');\n\t\t}\n\t\tleftText.append(costPerMonthIcon.isNull()\n\t\t\t? info.costPerMonth\n\t\t\t: removedStar(info.costPerMonth));\n\t\tconst auto costPerMonthLabel\n\t\t\t= row->lifetime().make_state<Ui::Text::String>();\n\t\tcostPerMonthLabel->setMarkedText(\n\t\t\tst::shareBoxListItem.nameStyle,\n\t\t\tstd::move(leftText));\n\t\tconst auto rightText = info.total.isEmpty()\n\t\t\t? info.costPerYear\n\t\t\t: info.total;\n\n\t\tconst auto costPerYearEntry = [&] {\n\t\t\tif (!rightText.startsWith(kStar)) {\n\t\t\t\treturn QImage();\n\t\t\t}\n\t\t\tconst auto text = removedStar(rightText);\n\t\t\tconst auto icon = GenerateStars(costPerYearFont->height, 1);\n\t\t\tauto result = QImage(\n\t\t\t\tQSize(costPerYearFont->spacew + costPerYearFont->width(text), 0)\n\t\t\t\t\t* style::DevicePixelRatio()\n\t\t\t\t\t+ icon.size(),\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t\tresult.fill(Qt::transparent);\n\t\t\t{\n\t\t\t\tauto p = QPainter(&result);\n\t\t\t\tp.drawImage(0, 0, icon);\n\t\t\t\tp.setPen(st::windowSubTextFg);\n\t\t\t\tp.setFont(costPerYearFont);\n\t\t\t\tp.drawText(\n\t\t\t\t\tRect(result.size() / style::DevicePixelRatio()),\n\t\t\t\t\ttext,\n\t\t\t\t\tstyle::al_right);\n\t\t\t}\n\t\t\treturn result;\n\t\t}();\n\n\t\trow->paintRequest(\n\t\t) | rpl::on_next([=](const QRect &r) {\n\t\t\tauto p = QPainter(row);\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\n\t\t\tp.fillRect(r, Qt::transparent);\n\n\t\t\tconst auto left = st.textLeft;\n\t\t\tconst auto halfHeight = row->height() / 2;\n\n\t\t\tconst auto titleFont = st::semiboldFont;\n\t\t\tp.setFont(titleFont);\n\t\t\tp.setPen(st::boxTextFg);\n\t\t\tif (info.costPerMonth.isEmpty() && info.discount.isEmpty()) {\n\t\t\t\tconst auto r = row->rect().translated(\n\t\t\t\t\t-row->rect().left() + left,\n\t\t\t\t\t0);\n\t\t\t\tp.drawText(r, info.duration, style::al_left);\n\t\t\t} else {\n\t\t\t\tp.drawText(\n\t\t\t\t\tleft,\n\t\t\t\t\tst.subtitleTop + titleFont->ascent,\n\t\t\t\t\tinfo.duration);\n\t\t\t}\n\n\t\t\tconst auto discountFont = st::windowFiltersButton.badgeStyle.font;\n\t\t\tconst auto discountWidth = discountFont->width(info.discount);\n\t\t\tconst auto &discountMargins = discountWidth\n\t\t\t\t? st.badgeMargins\n\t\t\t\t: style::margins();\n\n\t\t\tconst auto bottomLeftRect = QRect(\n\t\t\t\tleft,\n\t\t\t\thalfHeight + discountMargins.top(),\n\t\t\t\tdiscountWidth\n\t\t\t\t\t+ discountMargins.left()\n\t\t\t\t\t+ discountMargins.right(),\n\t\t\t\tst.badgeHeight);\n\t\t\tconst auto discountRect = topBadges\n\t\t\t\t? bottomLeftRect.translated(\n\t\t\t\t\ttitleFont->width(info.duration) + st.badgeShift.x(),\n\t\t\t\t\t-bottomLeftRect.top()\n\t\t\t\t\t\t+ st.badgeShift.y()\n\t\t\t\t\t\t+ st.subtitleTop\n\t\t\t\t\t\t+ (titleFont->height - bottomLeftRect.height()) / 2)\n\t\t\t\t: bottomLeftRect;\n\t\t\tconst auto from = edges->top->y();\n\t\t\tconst auto to = edges->bottom->y() + edges->bottom->height();\n\t\t\tauto partialGradient = PartialGradient(from, to, stops);\n\t\t\tconst auto partialGradientBrush = partialGradient.compute(\n\t\t\t\trow->y(),\n\t\t\t\trow->height());\n\t\t\t{\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(partialGradientBrush);\n\t\t\t\tconst auto round = st.badgeRadius;\n\t\t\t\tp.drawRoundedRect(discountRect, round, round);\n\t\t\t}\n\n\t\t\tif (st.borderWidth && (animation->nowIndex == index)) {\n\t\t\t\tconst auto progress = animation->animation.value(1.);\n\t\t\t\tconst auto w = row->width();\n\t\t\t\tauto gradient = QLinearGradient(\n\t\t\t\t\tw - w * progress,\n\t\t\t\t\t0,\n\t\t\t\t\tw * 2,\n\t\t\t\t\t0);\n\t\t\t\tgradient.setSpread(QGradient::Spread::RepeatSpread);\n\t\t\t\tgradient.setStops(stops);\n\t\t\t\tconst auto pen = QPen(\n\t\t\t\t\tQBrush(partialGradientBrush),\n\t\t\t\t\tst.borderWidth);\n\t\t\t\tp.setPen(pen);\n\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t\tconst auto borderRect = row->rect()\n\t\t\t\t\t- Margins(pen.width() / 2);\n\t\t\t\tconst auto round = st.borderRadius;\n\t\t\t\tp.drawRoundedRect(borderRect, round, round);\n\t\t\t}\n\n\t\t\tp.setPen(st::premiumButtonFg);\n\t\t\tp.setFont(discountFont);\n\t\t\tp.drawText(discountRect, info.discount, style::al_center);\n\n\t\t\tconst auto perRect = QMargins(0, 0, row->width(), 0)\n\t\t\t\t+ bottomLeftRect.translated(\n\t\t\t\t\ttopBadges\n\t\t\t\t\t\t? 0\n\t\t\t\t\t\t: bottomLeftRect.width() + discountMargins.left(),\n\t\t\t\t\t0);\n\t\t\tp.setPen(st::windowSubTextFg);\n\t\t\tp.setFont(costPerMonthFont);\n\n\t\t\t{\n\t\t\t\tconst auto left = costPerMonthIcon.isNull()\n\t\t\t\t\t? 0\n\t\t\t\t\t: (costPerMonthFont->spacew\n\t\t\t\t\t\t+ costPerMonthIcon.width()\n\t\t\t\t\t\t\t/ style::DevicePixelRatio());\n\t\t\t\tconst auto costPerYearWidth = costPerYearFont->width(\n\t\t\t\t\trightText);\n\t\t\t\tconst auto pos = perRect.translated(left, 0).topLeft();\n\t\t\t\tconst auto availableWidth = row->width()\n\t\t\t\t\t- pos.x()\n\t\t\t\t\t- costPerYearWidth;\n\t\t\t\tcostPerMonthLabel->draw(p, {\n\t\t\t\t\t.position = pos,\n\t\t\t\t\t.outerWidth = availableWidth,\n\t\t\t\t\t.availableWidth = availableWidth,\n\t\t\t\t\t.elisionLines = 1,\n\t\t\t\t});\n\t\t\t}\n\t\t\tp.drawImage(perRect.topLeft(), costPerMonthIcon);\n\n\t\t\tconst auto totalRect = row->rect()\n\t\t\t\t- QMargins(0, 0, st.rowMargins.right(), 0);\n\t\t\tif (costPerYearEntry.isNull()) {\n\t\t\t\tp.setFont(costPerYearFont);\n\t\t\t\tp.drawText(totalRect, rightText, style::al_right);\n\t\t\t} else {\n\t\t\t\tconst auto size = costPerYearEntry.size()\n\t\t\t\t\t/ style::DevicePixelRatio();\n\t\t\t\tp.drawImage(\n\t\t\t\t\ttotalRect.width() - size.width(),\n\t\t\t\t\t(row->height() - size.height()) / 2,\n\t\t\t\t\tcostPerYearEntry);\n\t\t\t}\n\t\t}, row->lifetime());\n\n\t\trow->setClickedCallback([=, duration = st::defaultCheck.duration] {\n\t\t\tgroup->setValue(index);\n\t\t\tanimation->nowIndex = group->current();\n\t\t\tanimation->animation.stop();\n\t\t\tanimation->animation.start(\n\t\t\t\t[=] { parent->update(); },\n\t\t\t\t0.,\n\t\t\t\t1.,\n\t\t\t\tduration);\n\t\t});\n\n\t};\n\tfor (auto i = 0; i < gifts.size(); i++) {\n\t\taddRow(gifts[i], i);\n\t}\n\n\tparent->resizeToWidth(parent->height());\n\tparent->update();\n}\n\n} // namespace Premium\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/premium_graphics.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/round_checkbox.h\"\n\nnamespace style {\nstruct PremiumLimits;\n} // namespace style\n\nnamespace tr {\ntemplate <typename ...>\nstruct phrase;\n} // namespace tr\n\nenum lngtag_count : int;\n\nnamespace Data {\nstruct PremiumSubscriptionOption;\n} // namespace Data\n\nnamespace style {\nstruct RoundImageCheckbox;\nstruct PremiumOption;\nstruct TextStyle;\nstruct PremiumBubble;\n} // namespace style\n\nnamespace Ui {\n\nclass GenericBox;\nclass RadiobuttonGroup;\nclass VerticalLayout;\n\nnamespace Premium {\n\ninline constexpr auto kLimitRowRatio = 0.5;\n\n[[nodiscard]] QString Svg();\n[[nodiscard]] QByteArray ColorizedSvg(const QGradientStops &gradientStops);\n[[nodiscard]] QImage GenerateStarForLightTopBar(QRectF rect);\n\nvoid AddLimitRow(\n\tnot_null<Ui::VerticalLayout*> parent,\n\tconst style::PremiumLimits &st,\n\tQString max,\n\tQString min = {},\n\tfloat64 ratio = kLimitRowRatio);\n\nvoid AddLimitRow(\n\tnot_null<Ui::VerticalLayout*> parent,\n\tconst style::PremiumLimits &st,\n\tint max,\n\tstd::optional<tr::phrase<lngtag_count>> phrase,\n\tint min = 0,\n\tfloat64 ratio = kLimitRowRatio);\n\nstruct LimitRowLabels {\n\trpl::producer<QString> leftLabel;\n\trpl::producer<QString> leftCount;\n\trpl::producer<QString> rightLabel;\n\trpl::producer<QString> rightCount;\n\tFn<QBrush()> activeLineBg;\n};\n\nstruct LimitRowState {\n\tfloat64 ratio = 0.;\n\tbool animateFromZero = false;\n\tbool dynamic = false;\n};\n\nvoid AddLimitRow(\n\tnot_null<Ui::VerticalLayout*> parent,\n\tconst style::PremiumLimits &st,\n\tLimitRowLabels labels,\n\trpl::producer<LimitRowState> state,\n\tconst style::margins &padding);\n\nstruct AccountsRowArgs final {\n\tstd::shared_ptr<Ui::RadiobuttonGroup> group;\n\tconst style::RoundImageCheckbox &st;\n\tconst style::TextStyle &stName;\n\tconst style::color &stNameFg;\n\tstruct Entry final {\n\t\tQString name;\n\t\tUi::RoundImageCheckbox::PaintRoundImage paintRoundImage;\n\t};\n\tstd::vector<Entry> entries;\n};\n\nvoid AddAccountsRow(\n\tnot_null<Ui::VerticalLayout*> parent,\n\tAccountsRowArgs &&args);\n\n[[nodiscard]] QGradientStops LimitGradientStops();\n[[nodiscard]] QGradientStops ButtonGradientStops();\n[[nodiscard]] QGradientStops LockGradientStops();\n[[nodiscard]] QGradientStops FullHeightGradientStops();\n[[nodiscard]] QGradientStops GiftGradientStops();\n[[nodiscard]] QGradientStops CreditsIconGradientStops();\n\n[[nodiscard]] QLinearGradient ComputeGradient(\n\tnot_null<QWidget*> content,\n\tint left,\n\tint width);\n\nstruct ListEntry final {\n\trpl::producer<QString> title;\n\trpl::producer<TextWithEntities> about;\n\tint leftNumber = 0;\n\tint rightNumber = 0;\n\tstd::optional<QString> customRightText;\n\tconst style::icon *icon = nullptr;\n};\nvoid ShowListBox(\n\tnot_null<Ui::GenericBox*> box,\n\tconst style::PremiumLimits &st,\n\tstd::vector<ListEntry> entries);\n\nvoid AddGiftOptions(\n\tnot_null<Ui::VerticalLayout*> parent,\n\tstd::shared_ptr<Ui::RadiobuttonGroup> group,\n\tstd::vector<Data::PremiumSubscriptionOption> gifts,\n\tconst style::PremiumOption &st,\n\tbool topBadges = false);\n\n} // namespace Premium\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/premium_stars.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/premium_stars.h\"\n\n#include \"base/random.h\"\n#include \"ui/effects/animation_value_f.h\"\n\n#include <QtCore/QtMath>\n\nnamespace Ui {\nnamespace Premium {\nnamespace {\n\nusing Type = MiniStarsType;\nconstexpr auto kDeformationMax = 0.1;\nconstexpr auto kIdleLimit = 5;\n\n} // namespace\n\nMiniStars::MiniStars(\n\tFn<void(const QRect &r)> updateCallback,\n\tbool opaque,\n\tType type)\n: _availableAngles((type != Type::SlowStars && type != Type::SlowDiamondStars)\n\t? std::vector<Interval>{\n\t\tInterval{ -10, 40 },\n\t\tInterval{ 180 + 10 - 40, 40 },\n\t\tInterval{ 180 + 15, 50 },\n\t\tInterval{ -15 - 50, 50 },\n\t}\n\t: std::vector<Interval>{ Interval{ -90, 180 }, Interval{ 90, 180 } })\n, _lifeLength((type != Type::SlowStars && type != Type::SlowDiamondStars)\n\t? Interval{ 150 / 5, 200 / 5 }\n\t: Interval{ 150 * 2, 200 * 2 })\n, _deathTime((type != Type::SlowStars && type != Type::SlowDiamondStars)\n\t? Interval{ 1500, 2000 }\n\t: Interval{ 1500 * 2, 2000 * 2 })\n, _size((type != Type::SlowStars)\n\t? Interval{ 5, 10 }\n\t: Interval{ 2, 4 })\n, _alpha({ opaque ? 100 : 40, opaque ? 100 : 60 })\n, _sinFactor({ 10, 190 })\n, _spritesCount({ 0, ((type == Type::MonoStars) ? 1 : 2) })\n, _appearProgressTill((type != Type::SlowStars\n\t\t&& type != Type::SlowDiamondStars)\n\t? 0.2\n\t: 0.01)\n, _disappearProgressAfter(0.8)\n, _distanceProgressStart(0.5)\n, _sprite((type == Type::DiamondStars || type == Type::SlowDiamondStars)\n\t? u\":/gui/icons/settings/starmini.svg\"_q\n\t: u\":/gui/icons/settings/star.svg\"_q)\n, _animation([=](crl::time now) {\n\tif (++_idleCounter >= kIdleLimit) {\n\t\t_animation.stop();\n\t\treturn;\n\t}\n\tif (now > _nextBirthTime && !_paused) {\n\t\tcreateStar(now);\n\t}\n\tif (_rectToUpdate.isValid()) {\n\t\tupdateCallback(base::take(_rectToUpdate));\n\t}\n}) {\n\tif (type == Type::BiStars) {\n\t\t_secondSprite = std::make_unique<QSvgRenderer>(\n\t\t\tu\":/gui/icons/settings/star.svg\"_q);\n\t}\n\tif (anim::Disabled()) {\n\t\tconst auto from = _deathTime.from + _deathTime.length;\n\t\tauto r = bytes::vector(from + 1);\n\t\tbase::RandomFill(r.data(), r.size());\n\n\t\tfor (auto i = -from; i < 0; i += randomInterval(_lifeLength, r[-i])) {\n\t\t\tcreateStar(i);\n\t\t}\n\t\tupdateCallback(_rectToUpdate);\n\t}\n}\n\nint MiniStars::randomInterval(\n\t\tconst Interval &interval,\n\t\tconst bytes::type &random) const {\n\treturn interval.from + (uchar(random) % interval.length);\n}\n\ncrl::time MiniStars::timeNow() const {\n\treturn anim::Disabled() ? 0 : crl::now();\n}\n\nvoid MiniStars::paint(QPainter &p, const QRectF &rect) {\n\t_idleCounter = 0;\n\tif (!_animation.animating()) {\n\t\t_animation.start();\n\t}\n\tconst auto center = rect.center();\n\tconst auto opacity = p.opacity();\n\tconst auto now = timeNow();\n\tfor (const auto &ministar : _ministars) {\n\t\tconst auto progress = (now - ministar.birthTime)\n\t\t\t/ float64(ministar.deathTime - ministar.birthTime);\n\t\tif (progress > 1.) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto appearProgress = std::clamp(\n\t\t\tprogress / _appearProgressTill,\n\t\t\t0.,\n\t\t\t1.);\n\t\tconst auto rsin = float(std::sin(ministar.angle * M_PI / 180.));\n\t\tconst auto rcos = float(std::cos(ministar.angle * M_PI / 180.));\n\t\tconst auto end = QPointF(\n\t\t\trect.width() / kSizeFactor * rcos,\n\t\t\trect.height() / kSizeFactor * rsin);\n\n\t\tconst auto alphaProgress = 1.\n\t\t\t- (std::clamp(progress - _disappearProgressAfter, 0., 1.)\n\t\t\t\t/ (1. - _disappearProgressAfter));\n\t\tp.setOpacity(ministar.alpha\n\t\t\t* alphaProgress\n\t\t\t* appearProgress\n\t\t\t* opacity);\n\n\t\tconst auto deformResult = progress * 360;\n\t\tconst auto rsinDeform = float(\n\t\t\tstd::sin(ministar.sinFactor * deformResult * M_PI / 180.));\n\t\tconst auto deformH = 1. + kDeformationMax * rsinDeform;\n\t\tconst auto deformW = 1. / deformH;\n\n\t\tconst auto distanceProgress = _distanceProgressStart + progress;\n\t\tconst auto starSide = ministar.size * appearProgress;\n\t\tconst auto widthFade = (std::abs(rcos) >= std::abs(rsin));\n\t\tconst auto starWidth = starSide\n\t\t\t* (widthFade ? alphaProgress : 1.)\n\t\t\t* deformW;\n\t\tconst auto starHeight = starSide\n\t\t\t* (!widthFade ? alphaProgress : 1.)\n\t\t\t* deformH;\n\t\tconst auto renderRect = QRectF(\n\t\t\tcenter.x()\n\t\t\t\t+ anim::interpolateF(0, end.x(), distanceProgress)\n\t\t\t\t- starWidth / 2.,\n\t\t\tcenter.y()\n\t\t\t\t+ anim::interpolateF(0, end.y(), distanceProgress)\n\t\t\t\t- starHeight / 2.,\n\t\t\tstarWidth,\n\t\t\tstarHeight);\n\t\tministar.sprite->render(&p, renderRect);\n\t\t_rectToUpdate |= renderRect.toRect();\n\t}\n\tp.setOpacity(opacity);\n}\n\nvoid MiniStars::setPaused(bool paused) {\n\t_paused = paused;\n}\n\nvoid MiniStars::createStar(crl::time now) {\n\tconstexpr auto kRandomSize = 9;\n\tauto random = bytes::vector(kRandomSize);\n\tbase::RandomFill(random.data(), random.size());\n\n\tauto i = 0;\n\tauto next = [&] { return random[i++]; };\n\n\t_nextBirthTime = now + randomInterval(_lifeLength, next());\n\n\tconst auto &angleInterval = _availableAngles[\n\t\tuchar(next()) % _availableAngles.size()];\n\n\tauto ministar = MiniStar{\n\t\t.birthTime = now,\n\t\t.deathTime = now + randomInterval(_deathTime, next()),\n\t\t.angle = randomInterval(angleInterval, next()),\n\t\t.size = float64(randomInterval(_size, next())),\n\t\t.alpha = float64(randomInterval(_alpha, next())) / 100.,\n\t\t.sinFactor = randomInterval(_sinFactor, next()) / 100.\n\t\t\t* ((uchar(next()) % 2) == 1 ? 1. : -1.),\n\t\t.sprite = ((randomInterval(_spritesCount, next()) && _secondSprite)\n\t\t\t? _secondSprite.get()\n\t\t\t: &_sprite),\n\t};\n\tfor (auto i = 0; i < _ministars.size(); i++) {\n\t\tif (ministar.birthTime > _ministars[i].deathTime) {\n\t\t\t_ministars[i] = ministar;\n\t\t\treturn;\n\t\t}\n\t}\n\t_ministars.push_back(ministar);\n}\n\n\n} // namespace Premium\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/premium_stars.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n\n#include <QSvgRenderer>\n\nnamespace Ui {\nnamespace Premium {\n\nenum class MiniStarsType {\n\tMonoStars,\n\tBiStars,\n\tSlowStars,\n\tDiamondStars,\n\tSlowDiamondStars,\n};\n\nclass MiniStars final {\npublic:\n\tMiniStars(\n\t\tFn<void(const QRect &r)> updateCallback,\n\t\tbool opaque = false,\n\t\tMiniStarsType type = MiniStarsType::MonoStars);\n\n\tvoid paint(QPainter &p, const QRectF &rect);\n\tvoid setPaused(bool paused);\n\n\tstatic constexpr auto kSizeFactor = 1.5;\n\nprivate:\n\tstruct MiniStar {\n\t\tcrl::time birthTime = 0;\n\t\tcrl::time deathTime = 0;\n\t\tint angle = 0;\n\t\tfloat64 size = 0.;\n\t\tfloat64 alpha = 0.;\n\t\tfloat64 sinFactor = 0.;\n\t\tnot_null<QSvgRenderer*> sprite;\n\t};\n\n\tstruct Interval {\n\t\tint from = 0;\n\t\tint length = 0;\n\t};\n\n\tvoid createStar(crl::time now);\n\t[[nodiscard]] crl::time timeNow() const;\n\t[[nodiscard]] int randomInterval(\n\t\tconst Interval &interval,\n\t\tconst gsl::byte &random) const;\n\n\tconst std::vector<Interval> _availableAngles;\n\tconst Interval _lifeLength;\n\tconst Interval _deathTime;\n\tconst Interval _size;\n\tconst Interval _alpha;\n\tconst Interval _sinFactor;\n\tconst Interval _spritesCount;\n\n\tconst float64 _appearProgressTill;\n\tconst float64 _disappearProgressAfter;\n\tconst float64 _distanceProgressStart;\n\n\tQSvgRenderer _sprite;\n\tstd::unique_ptr<QSvgRenderer> _secondSprite;\n\n\tUi::Animations::Basic _animation;\n\n\tstd::vector<MiniStar> _ministars;\n\n\tcrl::time _nextBirthTime = 0;\n\tbool _paused = false;\n\tuint8_t _idleCounter : 3 = 0;\n\n\tQRect _rectToUpdate;\n\n};\n\n} // namespace Premium\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/premium_stars_colored.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/premium_stars_colored.h\"\n\n#include \"base/random.h\"\n#include \"ui/effects/premium_graphics.h\" // GiftGradientStops.\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/painter.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Ui::Premium {\nnamespace {\n\nconstexpr auto kStarsCount = 16;\nconstexpr auto kTravelMax = 0.5;\nconstexpr auto kExcludeRadius = 0.7;\nconstexpr auto kFading = crl::time(200);\nconstexpr auto kLifetimeMin = crl::time(1000);\nconstexpr auto kLifetimeMax = 3 * kLifetimeMin;\nconstexpr auto kSizeMin = 0.1;\nconstexpr auto kSizeMax = 0.15;\nconstexpr auto kPauseOnNoPaints = 4;\n\n[[nodiscard]] crl::time ChooseLife(base::BufferedRandom<uint32> &random) {\n\treturn kLifetimeMin\n\t\t+ base::RandomIndex(kLifetimeMax - kLifetimeMin, random);\n}\n\nclass CollectibleEmoji final : public Text::CustomEmoji {\npublic:\n\tCollectibleEmoji(\n\t\tQStringView entityData,\n\t\tQColor centerColor,\n\t\tQColor edgeColor,\n\t\tstd::unique_ptr<Ui::Text::CustomEmoji> inner,\n\t\tFn<void()> update,\n\t\tint size);\n\n\tint width() override;\n\tQString entityData() override;\n\tvoid paint(QPainter &p, const Context &context) override;\n\tvoid unload() override;\n\tbool ready() override;\n\tbool readyInDefaultState() override;\n\nprivate:\n\tstruct Star {\n\t\tQPointF start;\n\t\tQPointF delta;\n\t\tfloat64 size = 0.;\n\t\tcrl::time birthTime = 0;\n\t\tcrl::time deathTime = 0;\n\t};\n\n\tvoid fill();\n\tvoid refill(Star &star, base::BufferedRandom<uint32> &random);\n\tvoid prepareFrame();\n\n\tQString _entityData;\n\tQSvgRenderer _svg;\n\tstd::vector<Star> _stars;\n\tQColor _centerColor;\n\tQColor _edgeColor;\n\tstd::unique_ptr<Text::CustomEmoji> _inner;\n\tUi::Animations::Basic _animation;\n\tQImage _frame;\n\tint _skippedPaints = 0;\n\tint _size = 0;\n\n};\n\nCollectibleEmoji::CollectibleEmoji(\n\tQStringView entityData,\n\tQColor centerColor,\n\tQColor edgeColor,\n\tstd::unique_ptr<Ui::Text::CustomEmoji> inner,\n\tFn<void()> update,\n\tint size)\n: _entityData(entityData.toString())\n, _svg(u\":/gui/icons/settings/starmini.svg\"_q)\n, _centerColor(centerColor)\n, _edgeColor(edgeColor)\n, _inner(std::move(inner))\n, _animation([=] {\n\tif (++_skippedPaints > kPauseOnNoPaints) {\n\t\t_animation.stop();\n\t} else {\n\t\tupdate();\n\t}\n})\n, _size(size) {\n\tfill();\n}\n\nvoid CollectibleEmoji::fill() {\n\t_stars.reserve(kStarsCount);\n\tconst auto now = crl::now();\n\tauto random = base::BufferedRandom<uint32>(kStarsCount * 12);\n\tfor (auto i = 0; i != kStarsCount; ++i) {\n\t\tconst auto life = ChooseLife(random);\n\t\tconst auto shift = base::RandomIndex(life - kFading, random);\n\t\t_stars.push_back({\n\t\t\t.birthTime = now - crl::time(shift),\n\t\t\t.deathTime = now - crl::time(shift) + crl::time(life),\n\t\t});\n\t\trefill(_stars.back(), random);\n\t}\n}\n\nvoid CollectibleEmoji::refill(\n\t\tStar &star,\n\t\tbase::BufferedRandom<uint32> &random) {\n\tconst auto take = [&] {\n\t\treturn base::RandomIndex(_size * 16, random) / (_size * 15.);\n\t};\n\tconst auto stake = [&] {\n\t\treturn take() * 2. - 1.;\n\t};\n\tconst auto exclude = kExcludeRadius * kExcludeRadius;\n\tauto square = 0.;\n\twhile (true) {\n\t\tconst auto start = QPointF(stake(), stake());\n\t\tsquare = start.x() * start.x() + start.y() * start.y();\n\t\tif (square > exclude) {\n\t\t\tstar.start = start * _size;\n\t\t\tbreak;\n\t\t}\n\t}\n\tsquare *= _size * _size;\n\twhile (true) {\n\t\tconst auto delta = QPointF(stake(), stake()) * kTravelMax * _size;\n\t\tconst auto end = star.start + delta;\n\t\tif (end.x() * end.x() + end.y() * end.y() > square) {\n\t\t\tstar.delta = delta;\n\t\t\tbreak;\n\t\t}\n\t}\n\tstar.start = (star.start + QPointF(_size, _size)) / 2.;\n\tstar.size = (kSizeMin + (kSizeMax - kSizeMin) * take()) * _size;\n}\n\nint CollectibleEmoji::width() {\n\treturn _inner->width();\n}\n\nQString CollectibleEmoji::entityData() {\n\treturn _entityData;\n}\n\nvoid CollectibleEmoji::prepareFrame() {\n\tconst auto clip = QSize(_size, _size);\n\tif (_frame.isNull()) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t_frame = QImage(clip * ratio, QImage::Format_ARGB32_Premultiplied);\n\t\t_frame.setDevicePixelRatio(ratio);\n\t}\n\t_frame.fill(Qt::transparent);\n\tauto p = QPainter(&_frame);\n\tauto hq = PainterHighQualityEnabler(p);\n\tauto random = std::optional<base::BufferedRandom<uint32>>();\n\tconst auto now = crl::now();\n\tfor (auto &star : _stars) {\n\t\tif (star.deathTime <= now) {\n\t\t\tif (!random) {\n\t\t\t\trandom.emplace(kStarsCount * 10);\n\t\t\t}\n\t\t\tconst auto life = ChooseLife(*random);\n\t\t\tstar.birthTime = now;\n\t\t\tstar.deathTime = now + crl::time(life);\n\t\t\trefill(star, *random);\n\t\t}\n\t\tAssert(star.birthTime <= now && now <= star.deathTime);\n\n\t\tconst auto time = (now - star.birthTime)\n\t\t\t/ float64(star.deathTime - star.birthTime);\n\t\tconst auto position = star.start + star.delta * time;\n\t\tconst auto scale = ((now - star.birthTime) < kFading)\n\t\t\t? ((now - star.birthTime) / float64(kFading))\n\t\t\t: ((star.deathTime - now) < kFading)\n\t\t\t? ((star.deathTime - now) / float64(kFading))\n\t\t\t: 1.;\n\t\tif (scale > 0.) {\n\t\t\tconst auto size = star.size * scale;\n\t\t\tconst auto rect = QRectF(\n\t\t\t\tposition - QPointF(size, size),\n\t\t\t\tQSizeF(size, size) * 2);\n\t\t\t_svg.render(&p, rect);\n\t\t}\n\t}\n\n\tp.setCompositionMode(QPainter::CompositionMode_SourceIn);\n\tauto gradient = QRadialGradient(\n\t\tQRect(QPoint(), clip).center(),\n\t\tclip.height() / 2);\n\tgradient.setStops({\n\t\t{ 0., _centerColor },\n\t\t{ 1., _edgeColor },\n\t});\n\tp.setBrush(gradient);\n\tp.setPen(Qt::NoPen);\n\tp.drawRect(QRect(QPoint(), clip));\n}\n\nvoid CollectibleEmoji::paint(QPainter &p, const Context &context) {\n\tprepareFrame();\n\tp.drawImage(context.position, _frame);\n\tif (context.paused) {\n\t\t_animation.stop();\n\t} else if (!_animation.animating()) {\n\t\t_animation.start();\n\t}\n\t_inner->paint(p, context);\n\t_skippedPaints = 0;\n}\n\nvoid CollectibleEmoji::unload() {\n\t_inner->unload();\n}\n\nbool CollectibleEmoji::ready() {\n\treturn _inner->ready();\n}\n\nbool CollectibleEmoji::readyInDefaultState() {\n\treturn _inner->readyInDefaultState();\n}\n\n} // namespace\n\nColoredMiniStars::ColoredMiniStars(\n\tnot_null<Ui::RpWidget*> parent,\n\tbool optimizeUpdate,\n\tMiniStarsType type)\n: _ministars(\n\toptimizeUpdate\n\t\t? Fn<void(const QRect &)>([=](const QRect &r) {\n\t\t\tparent->update(r.translated(_position));\n\t\t})\n\t\t: Fn<void(const QRect &)>([=](const QRect &) { parent->update(); }),\n\ttrue,\n\ttype) {\n}\n\nColoredMiniStars::ColoredMiniStars(\n\tFn<void(const QRect &)> update,\n\tMiniStarsType type)\n: _ministars(update, true, type) {\n}\n\nvoid ColoredMiniStars::setSize(const QSize &size) {\n\t_frame = QImage(\n\t\tsize * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\t_frame.setDevicePixelRatio(style::DevicePixelRatio());\n\n\t_mask = _frame;\n\t_mask.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&_mask);\n\t\tif (_stopsOverride && _stopsOverride->size() == 1) {\n\t\t\tconst auto &color = _stopsOverride->front().second;\n\t\t\tp.fillRect(0, 0, size.width(), size.height(), color);\n\t\t} else {\n\t\t\tauto gradient = QLinearGradient(0, 0, size.width(), 0);\n\t\t\tgradient.setStops((_stopsOverride && _stopsOverride->size() > 1)\n\t\t\t\t? (*_stopsOverride)\n\t\t\t\t: Ui::Premium::GiftGradientStops());\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(gradient);\n\t\t\tp.drawRect(0, 0, size.width(), size.height());\n\t\t}\n\t}\n\n\t_size = size;\n\n\t{\n\t\tconst auto s = _size / Ui::Premium::MiniStars::kSizeFactor;\n\t\tconst auto margins = QMarginsF(\n\t\t\ts.width() / 2.,\n\t\t\ts.height() / 2.,\n\t\t\ts.width() / 2.,\n\t\t\ts.height() / 2.);\n\t\t_ministarsRect = QRectF(QPointF(), _size) - margins;\n\t}\n}\n\nvoid ColoredMiniStars::setPosition(QPoint position) {\n\t_position = std::move(position);\n}\n\nvoid ColoredMiniStars::setColorOverride(std::optional<QGradientStops> stops) {\n\t_stopsOverride = stops;\n}\n\nvoid ColoredMiniStars::paint(QPainter &p) {\n\t_frame.fill(Qt::transparent);\n\t{\n\t\tauto q = QPainter(&_frame);\n\t\t_ministars.paint(q, _ministarsRect);\n\t\tq.setCompositionMode(QPainter::CompositionMode_SourceIn);\n\t\tq.drawImage(0, 0, _mask);\n\t}\n\n\tp.drawImage(_position, _frame);\n}\n\nvoid ColoredMiniStars::setPaused(bool paused) {\n\t_ministars.setPaused(paused);\n}\n\nvoid ColoredMiniStars::setCenter(const QRect &rect) {\n\tconst auto center = rect.center();\n\tconst auto size = QSize(\n\t\trect.width() * Ui::Premium::MiniStars::kSizeFactor,\n\t\trect.height());\n\tconst auto ministarsRect = QRect(\n\t\tQPoint(center.x() - size.width(), center.y() - size.height()),\n\t\tQPoint(center.x() + size.width(), center.y() + size.height()));\n\tsetPosition(ministarsRect.topLeft());\n\tsetSize(ministarsRect.size());\n}\n\nstd::unique_ptr<Text::CustomEmoji> MakeCollectibleEmoji(\n\t\tQStringView entityData,\n\t\tQColor centerColor,\n\t\tQColor edgeColor,\n\t\tstd::unique_ptr<Text::CustomEmoji> inner,\n\t\tFn<void()> update,\n\t\tint size) {\n\treturn std::make_unique<CollectibleEmoji>(\n\t\tentityData,\n\t\tcenterColor,\n\t\tedgeColor,\n\t\tstd::move(inner),\n\t\tstd::move(update),\n\t\tsize);\n}\n\n} // namespace Ui::Premium\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/premium_stars_colored.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/premium_stars.h\"\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nnamespace Ui::Premium {\n\nclass ColoredMiniStars final {\npublic:\n\t// optimizeUpdate may cause paint glitch.\n\tColoredMiniStars(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tbool optimizeUpdate,\n\t\tMiniStarsType type = MiniStarsType::MonoStars);\n\tColoredMiniStars(Fn<void(const QRect &)> update, MiniStarsType type);\n\n\tvoid setSize(const QSize &size);\n\tvoid setPosition(QPoint position);\n\tvoid setColorOverride(std::optional<QGradientStops> stops);\n\tvoid setCenter(const QRect &rect);\n\tvoid paint(QPainter &p);\n\n\tvoid setPaused(bool paused);\n\nprivate:\n\tMiniStars _ministars;\n\tQRectF _ministarsRect;\n\tQImage _frame;\n\tQImage _mask;\n\tQSize _size;\n\tQPoint _position;\n\tstd::optional<QGradientStops> _stopsOverride;\n\n};\n\n[[nodiscard]] std::unique_ptr<Text::CustomEmoji> MakeCollectibleEmoji(\n\tQStringView entityData,\n\tQColor centerColor,\n\tQColor edgeColor,\n\tstd::unique_ptr<Text::CustomEmoji> inner,\n\tFn<void()> update,\n\tint size);\n\n} // namespace Ui::Premium\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/premium_top_bar.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/premium_top_bar.h\"\n\n#include \"lottie/lottie_icon.h\"\n#include \"ui/color_contrast.h\"\n#include \"ui/painter.h\"\n#include \"ui/effects/premium_graphics.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/rect.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace Ui::Premium {\nnamespace {\n\nconstexpr auto kBodyAnimationPart = 0.90;\nconstexpr auto kTitleAdditionalScale = 0.15;\nconstexpr auto kMinAcceptableContrast = 4.5; // 1.14;\n\n[[nodiscard]] QImage ScaleTo(QImage image) {\n\tusing namespace style;\n\tconst auto size = image.size();\n\tconst auto scale = DevicePixelRatio() * Scale() / 300.;\n\tconst auto scaled = QSize(\n\t\tint(base::SafeRound(size.width() * scale)),\n\t\tint(base::SafeRound(size.height() * scale)));\n\timage = image.scaled(\n\t\tscaled,\n\t\tQt::IgnoreAspectRatio,\n\t\tQt::SmoothTransformation);\n\timage.setDevicePixelRatio(DevicePixelRatio());\n\treturn image;\n}\n\n} // namespace\n\nTopBarAbstract::TopBarAbstract(\n\tQWidget *parent,\n\tconst style::PremiumCover &st)\n: RpWidget(parent)\n, _st(st) {\n}\n\nvoid TopBarAbstract::setRoundEdges(bool value) {\n\t_roundEdges = value;\n\tupdate();\n}\n\nvoid TopBarAbstract::paintEdges(QPainter &p, const QBrush &brush) const {\n\tconst auto r = rect();\n\tif (_roundEdges) {\n\t\tPainterHighQualityEnabler hq(p);\n\t\tconst auto radius = st::boxRadius;\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(brush);\n\t\tp.drawRoundedRect(\n\t\t\tr + QMargins{ 0, 0, 0, radius + 1 },\n\t\t\tradius,\n\t\t\tradius);\n\t} else {\n\t\tp.fillRect(r, brush);\n\t}\n}\n\nvoid TopBarAbstract::paintEdges(QPainter &p) const {\n\tpaintEdges(p, st().bg);\n\tif (isDark() && st().additionalShadowForDarkThemes) {\n\t\tpaintEdges(p, st::shadowFg);\n\t\tpaintEdges(p, st::shadowFg);\n\t}\n}\n\nQRectF TopBarAbstract::starRect(\n\t\tfloat64 topProgress,\n\t\tfloat64 sizeProgress) const {\n\tconst auto starSize = _st.starSize * sizeProgress;\n\treturn QRectF(\n\t\tQPointF(\n\t\t\t(width() - starSize.width()) / 2,\n\t\t\t_st.starTopSkip * topProgress),\n\t\tstarSize);\n};\n\nbool TopBarAbstract::isDark() const {\n\treturn _isDark;\n}\n\nvoid TopBarAbstract::computeIsDark() {\n\tconst auto contrast = CountContrast(\n\t\tst().bg->c,\n\t\tst::premiumButtonFg->c);\n\t_isDark = (contrast > kMinAcceptableContrast);\n}\n\nTopBar::TopBar(\n\tnot_null<QWidget*> parent,\n\tconst style::PremiumCover &st,\n\tTopBarDescriptor &&descriptor)\n: TopBarAbstract(parent, st)\n, _light(descriptor.light)\n, _logo(descriptor.logo)\n, _titleFont(st.titleFont)\n, _titlePadding(st.titlePadding)\n, _aboutMaxWidth(st.aboutMaxWidth)\n, _about(this, std::move(descriptor.about), st.about)\n, _ministars(\n\t\tthis,\n\t\tdescriptor.optimizeMinistars,\n\t\t(_logo == u\"diamond\"_q)\n\t\t\t? MiniStarsType::DiamondStars\n\t\t\t: MiniStarsType::BiStars) {\n\tstd::move(\n\t\tdescriptor.title\n\t) | rpl::on_next([=](QString text) {\n\t\t_titlePath = QPainterPath();\n\t\t_titlePath.addText(0, _titleFont->ascent, _titleFont, text);\n\t\tupdate();\n\t}, lifetime());\n\n\tif (const auto other = descriptor.clickContextOther) {\n\t\t_about->setClickHandlerFilter([=](\n\t\t\t\tconst ClickHandlerPtr &handler,\n\t\t\t\tQt::MouseButton button) {\n\t\t\tActivateClickHandler(_about, handler, {\n\t\t\t\tbutton,\n\t\t\t\tother()\n\t\t\t});\n\t\t\treturn false;\n\t\t});\n\t}\n\n\trpl::single() | rpl::then(\n\t\tstyle::PaletteChanged()\n\t) | rpl::on_next([=, starSize = st.starSize] {\n\t\tTopBarAbstract::computeIsDark();\n\n\t\tif (_logo == u\"dollar\"_q) {\n\t\t\t_dollar = ScaleTo(QImage(u\":/gui/art/business_logo.png\"_q));\n\t\t\t_ministars.setColorOverride(\n\t\t\t\tQGradientStops{{ 0, st::premiumButtonFg->c }});\n\t\t} else if (_logo == u\"affiliate\"_q) {\n\t\t\t_dollar = ScaleTo(QImage(u\":/gui/art/affiliate_logo.png\"_q));\n\t\t\t_ministars.setColorOverride(descriptor.gradientStops);\n\t\t} else if (_logo == u\"diamond\"_q) {\n\t\t\t_lottie = Lottie::MakeIcon({\n\t\t\t\t.name = u\"diamond\"_q,\n\t\t\t\t.sizeOverride = starSize,\n\t\t\t});\n\t\t\t_lottie->animate(\n\t\t\t\t[=] { update(_starRect.toRect() + Margins(st::lineWidth)); },\n\t\t\t\t0,\n\t\t\t\t_lottie->framesCount() - 1);\n\t\t\t_ministars.setColorOverride(\n\t\t\t\tQGradientStops{{ 0, st::windowActiveTextFg->c }});\n\t\t} else if (!_light && !TopBarAbstract::isDark()) {\n\t\t\t_star.load(Svg());\n\t\t\t_ministars.setColorOverride(\n\t\t\t\tQGradientStops{{ 0, st::premiumButtonFg->c }});\n\t\t} else {\n\t\t\t_star.load(ColorizedSvg(descriptor.gradientStops\n\t\t\t\t? (*descriptor.gradientStops)\n\t\t\t\t: Ui::Premium::ButtonGradientStops()));\n\t\t\t_ministars.setColorOverride(descriptor.gradientStops);\n\t\t}\n\t\tauto event = QResizeEvent(size(), size());\n\t\tresizeEvent(&event);\n\t}, lifetime());\n\n\tif (_light) {\n\t\tconst auto smallTopShadow = CreateChild<FadeShadow>(this);\n\t\tsmallTopShadow->setDuration(st::fadeWrapDuration);\n\t\tsizeValue(\n\t\t) | rpl::on_next([=](QSize size) {\n\t\t\tsmallTopShadow->resizeToWidth(size.width());\n\t\t\tsmallTopShadow->moveToLeft(\n\t\t\t\t0,\n\t\t\t\theight() - smallTopShadow->height());\n\t\t\tconst auto shown = (minimumHeight() * 2 > size.height());\n\t\t\tsmallTopShadow->toggle(shown, anim::type::normal);\n\t\t}, lifetime());\n\t}\n}\n\nTopBar::~TopBar() = default;\n\nvoid TopBar::setPaused(bool paused) {\n\t_ministars.setPaused(paused);\n}\n\nvoid TopBar::setTextPosition(int x, int y) {\n\t_titlePosition = { x, y };\n}\n\nrpl::producer<int> TopBar::additionalHeight() const {\n\treturn _about->heightValue(\n\t) | rpl::map([l = st().about.style.lineHeight](int height) {\n\t\treturn std::max(height - l, 0);\n\t});\n}\n\nvoid TopBar::resizeEvent(QResizeEvent *e) {\n\tconst auto max = maximumHeight();\n\tconst auto min = minimumHeight();\n\tconst auto progress = (max > min)\n\t\t? ((e->size().height() - min) / float64(max - min))\n\t\t: 1.;\n\t_progress.top = 1.\n\t\t- std::clamp((1. - progress) / kBodyAnimationPart, 0., 1.);\n\t_progress.body = _progress.top;\n\t_progress.title = 1. - progress;\n\t_progress.scaleTitle = 1. + kTitleAdditionalScale * progress;\n\n\t_ministars.setCenter(starRect(_progress.top, 1.).toRect());\n\n\t_starRect = starRect(_progress.top, _progress.body);\n\n\tconst auto &padding = st::boxRowPadding;\n\tconst auto availableWidth = width() - padding.left() - padding.right();\n\tconst auto titleTop = _starRect.top()\n\t\t+ _starRect.height()\n\t\t+ _titlePadding.top();\n\tconst auto titlePathRect = _titlePath.boundingRect();\n\tconst auto aboutTop = titleTop\n\t\t+ titlePathRect.height()\n\t\t+ _titlePadding.bottom();\n\t_about->resizeToWidth(_aboutMaxWidth ? _aboutMaxWidth : availableWidth);\n\t_about->moveToLeft(\n\t\tpadding.left()\n\t\t\t+ (_aboutMaxWidth ? (availableWidth - _about->width()) / 2 : 0),\n\t\taboutTop);\n\t_about->setOpacity(_progress.body);\n\n\tRpWidget::resizeEvent(e);\n}\n\nvoid TopBar::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto r = rect();\n\n\tif (!_light && !TopBarAbstract::isDark()) {\n\t\tconst auto gradientPointTop = r.height() / 3. * 2.;\n\t\tauto gradient = QLinearGradient(\n\t\t\tQPointF(0, gradientPointTop),\n\t\t\tQPointF(r.width(), r.height() - gradientPointTop));\n\t\tgradient.setStops(ButtonGradientStops());\n\n\t\tTopBarAbstract::paintEdges(p, gradient);\n\t} else {\n\t\tTopBarAbstract::paintEdges(p);\n\t}\n\n\tp.setOpacity(_progress.body);\n\tp.translate(_starRect.center());\n\tp.scale(_progress.body, _progress.body);\n\tp.translate(-_starRect.center());\n\tif (_progress.top) {\n\t\t_ministars.paint(p);\n\t}\n\tif (_lottie) {\n\t\t_lottie->paint(\n\t\t\tp,\n\t\t\t_starRect.left()\n\t\t\t\t+ (_starRect.width() - _lottie->width()) / 2\n\t\t\t\t- st::lineWidth * 6,\n\t\t\t_starRect.top());\n\t\tif (!_lottie->animating() && _lottie->frameIndex() > 0) {\n\t\t\t_lottie->animate(\n\t\t\t\t[=] { update(_starRect.toRect() + Margins(st::lineWidth)); },\n\t\t\t\t0,\n\t\t\t\t_lottie->framesCount() - 1);\n\t\t}\n\t}\n\tp.resetTransform();\n\n\n\tif (!_dollar.isNull()) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.drawImage(_starRect, _dollar);\n\t} else {\n\t\t_star.render(&p, _starRect);\n\t}\n\n\tconst auto color = _light ? st().titleFg : st::premiumButtonFg;\n\tp.setPen(color);\n\n\tconst auto titlePathRect = _titlePath.boundingRect();\n\n\t// Title.\n\tPainterHighQualityEnabler hq(p);\n\tp.setOpacity(1.);\n\tp.setFont(_titleFont);\n\tconst auto fullStarRect = starRect(1., 1.);\n\tconst auto fullTitleTop = fullStarRect.top()\n\t\t+ fullStarRect.height()\n\t\t+ _titlePadding.top();\n\tp.translate(\n\t\tanim::interpolate(\n\t\t\t(width() - titlePathRect.width()) / 2,\n\t\t\t_titlePosition.x(),\n\t\t\t_progress.title),\n\t\tanim::interpolate(fullTitleTop, _titlePosition.y(), _progress.title));\n\n\tp.translate(titlePathRect.center());\n\tp.scale(_progress.scaleTitle, _progress.scaleTitle);\n\tp.translate(-titlePathRect.center());\n\tp.fillPath(_titlePath, color);\n}\n\n} // namespace Ui::Premium\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/premium_top_bar.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/premium_stars_colored.h\"\n\nnamespace style {\nstruct PremiumCover;\n} // namespace style\n\nnamespace st {\nextern const style::PremiumCover &defaultPremiumCover;\n} // namespace st\n\nnamespace Ui {\nclass FlatLabel;\n} // namespace Ui\n\nnamespace Lottie {\nclass Icon;\n} // namespace Lottie\n\nnamespace Ui::Premium {\n\nclass TopBarAbstract : public RpWidget {\npublic:\n\tTopBarAbstract(\n\t\tQWidget *parent = nullptr,\n\t\tconst style::PremiumCover &st = st::defaultPremiumCover);\n\n\tvoid setRoundEdges(bool value);\n\n\tvirtual void setPaused(bool paused) = 0;\n\tvirtual void setTextPosition(int x, int y) = 0;\n\n\t[[nodiscard]] virtual rpl::producer<int> additionalHeight() const = 0;\n\n\t[[nodiscard]] const style::PremiumCover &st() const {\n\t\treturn _st;\n\t}\n\nprotected:\n\tvoid paintEdges(QPainter &p, const QBrush &brush) const;\n\tvoid paintEdges(QPainter &p) const;\n\n\t[[nodiscard]] QRectF starRect(\n\t\tfloat64 topProgress,\n\t\tfloat64 sizeProgress) const;\n\n\t[[nodiscard]] bool isDark() const;\n\tvoid computeIsDark();\n\nprivate:\n\tconst style::PremiumCover &_st;\n\tbool _roundEdges = true;\n\tbool _isDark = false;\n\n};\n\nstruct TopBarDescriptor {\n\tFn<QVariant()> clickContextOther;\n\tQString logo;\n\trpl::producer<QString> title;\n\trpl::producer<TextWithEntities> about;\n\tbool light = false;\n\tbool optimizeMinistars = true;\n\tstd::optional<QGradientStops> gradientStops;\n};\n\nclass TopBar final : public TopBarAbstract {\npublic:\n\tTopBar(\n\t\tnot_null<QWidget*> parent,\n\t\tconst style::PremiumCover &st,\n\t\tTopBarDescriptor &&descriptor);\n\t~TopBar();\n\n\tvoid setPaused(bool paused) override;\n\tvoid setTextPosition(int x, int y) override;\n\n\trpl::producer<int> additionalHeight() const override;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tconst bool _light = false;\n\tconst QString _logo;\n\tconst style::font &_titleFont;\n\tconst style::margins &_titlePadding;\n\tconst int _aboutMaxWidth = 0;\n\tobject_ptr<FlatLabel> _about;\n\tColoredMiniStars _ministars;\n\tQSvgRenderer _star;\n\tQImage _dollar;\n\tstd::unique_ptr<Lottie::Icon> _lottie;\n\n\tstruct {\n\t\tfloat64 top = 0.;\n\t\tfloat64 body = 0.;\n\t\tfloat64 title = 0.;\n\t\tfloat64 scaleTitle = 0.;\n\t} _progress;\n\n\tQRectF _starRect;\n\n\tQPoint _titlePosition;\n\tQPainterPath _titlePath;\n\n};\n\n} // namespace Ui::Premium\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/reaction_fly_animation.h\"\n\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/animated_icon.h\"\n#include \"ui/painter.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"base/random.h\"\n#include \"styles/style_chat.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kFlyDuration = crl::time(300);\nconstexpr auto kMiniCopies = 7;\nconstexpr auto kMiniCopiesDurationMax = crl::time(1400);\nconstexpr auto kMiniCopiesDurationMin = crl::time(700);\nconstexpr auto kMiniCopiesScaleInDuration = crl::time(200);\nconstexpr auto kMiniCopiesScaleOutDuration = crl::time(200);\nconstexpr auto kMiniCopiesMaxScaleMin = 0.6;\nconstexpr auto kMiniCopiesMaxScaleMax = 0.9;\n\n} // namespace\n\nReactionFlyAnimationArgs ReactionFlyAnimationArgs::translated(QPoint point) const {\n\treturn {\n\t\t.id = id,\n\t\t.flyIcon = flyIcon,\n\t\t.flyFrom = flyFrom.translated(point),\n\t};\n}\n\nauto ReactionFlyAnimation::flyCallback() {\n\treturn [=] {\n\t\tif (!_fly.animating()) {\n\t\t\t_flyIcon = QImage();\n\t\t\tstartAnimations();\n\t\t}\n\t\tif (_repaint) {\n\t\t\t_repaint();\n\t\t}\n\t};\n}\n\nauto ReactionFlyAnimation::callback() {\n\treturn [=] {\n\t\tif (_repaint) {\n\t\t\t_repaint();\n\t\t}\n\t};\n}\n\nReactionFlyAnimation::ReactionFlyAnimation(\n\tnot_null<::Data::Reactions*> owner,\n\tReactionFlyAnimationArgs &&args,\n\tFn<void()> repaint,\n\tint size,\n\tData::CustomEmojiSizeTag customSizeTag)\n: _owner(owner)\n, _repaint(std::move(repaint))\n, _flyFrom(args.flyFrom)\n, _scaleOutDuration(args.scaleOutDuration)\n, _scaleOutTarget(args.scaleOutTarget)\n, _forceFirstFrame(args.forceFirstFrame) {\n\tconst auto &list = owner->list(::Data::Reactions::Type::All);\n\tauto centerIcon = (DocumentData*)nullptr;\n\tauto aroundAnimation = (DocumentData*)nullptr;\n\tif (const auto customId = args.id.custom()) {\n\t\tconst auto esize = Data::FrameSizeFromTag(customSizeTag)\n\t\t\t/ style::DevicePixelRatio();\n\t\tconst auto data = &owner->owner();\n\t\tconst auto document = data->document(customId);\n\t\t_custom = data->customEmojiManager().create(\n\t\t\tdocument,\n\t\t\tcallback(),\n\t\t\tcustomSizeTag);\n\t\t_customSize = esize;\n\t\t_centerSizeMultiplier = _customSize / float64(size);\n\t\taroundAnimation = owner->chooseGenericAnimation(document);\n\t} else if (args.id.paid()) {\n\t\tconst auto fake = owner->lookupPaid();\n\t\tcenterIcon = fake->centerIcon;\n\t\taroundAnimation = owner->choosePaidReactionAnimation();\n\t\t_centerSizeMultiplier = 0.5;\n\t} else {\n\t\tconst auto i = ranges::find(list, args.id, &::Data::Reaction::id);\n\t\tif (i == end(list)/* || !i->centerIcon*/) {\n\t\t\treturn;\n\t\t}\n\t\tcenterIcon = i->centerIcon\n\t\t\t? not_null(i->centerIcon)\n\t\t\t: i->selectAnimation;\n\t\taroundAnimation = i->aroundAnimation;\n\t\t_centerSizeMultiplier = i->centerIcon ? 1. : 0.5;\n\t}\n\tconst auto resolve = [&](\n\t\t\tstd::unique_ptr<AnimatedIcon> &icon,\n\t\t\tDocumentData *document,\n\t\t\tint size) {\n\t\tif (!document) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto media = document->activeMediaView();\n\t\tif (!media || !media->loaded()) {\n\t\t\treturn false;\n\t\t}\n\t\ticon = MakeAnimatedIcon({\n\t\t\t.generator = DocumentIconFrameGenerator(media),\n\t\t\t.sizeOverride = QSize(size, size),\n\t\t\t.colorized = media->owner()->emojiUsesTextColor(),\n\t\t});\n\t\treturn true;\n\t};\n\tgenerateMiniCopies(size + size / 2, args.miniCopyMultiplier);\n\tif (args.effectOnly) {\n\t\t_effectOnly = true;\n\t} else if (!_custom && !resolve(_center, centerIcon, size)) {\n\t\treturn;\n\t}\n\tresolve(_effect, aroundAnimation, size * 2);\n\tif (!args.flyIcon.isNull()) {\n\t\t_flyIcon = std::move(args.flyIcon);\n\t\t_fly.start(flyCallback(), 0., 1., kFlyDuration);\n\t} else if (!_center && !_effect && _miniCopies.empty()) {\n\t\treturn;\n\t} else {\n\t\tstartAnimations();\n\t}\n\t_valid = true;\n}\n\nReactionFlyAnimation::~ReactionFlyAnimation() = default;\n\nQRect ReactionFlyAnimation::paintGetArea(\n\t\tQPainter &p,\n\t\tQPoint origin,\n\t\tQRect target,\n\t\tconst QColor &colored,\n\t\tQRect clip,\n\t\tcrl::time now) const {\n\tconst auto scale = [&] {\n\t\tif (!_scaleOutDuration\n\t\t\t|| (!_effect && !_noEffectScaleStarted)) {\n\t\t\treturn 1.;\n\t\t}\n\t\tauto progress = _noEffectScaleAnimation.value(0.);\n\t\tif (_effect) {\n\t\t\tconst auto rate = _effect->frameRate();\n\t\t\tif (!rate) {\n\t\t\t\treturn 1.;\n\t\t\t}\n\t\t\tconst auto left = _effect->framesCount() - _effect->frameIndex();\n\t\t\tconst auto duration = left * 1000. / rate;\n\t\t\tprogress = (duration < _scaleOutDuration)\n\t\t\t\t? (duration / double(_scaleOutDuration))\n\t\t\t\t: 1.;\n\t\t}\n\t\treturn (1. * progress + _scaleOutTarget * (1. - progress));\n\t}();\n\tauto hq = std::optional<PainterHighQualityEnabler>();\n\tif (scale < 1.) {\n\t\thq.emplace(p);\n\t\tconst auto shift = QRectF(target).center();\n\t\tp.translate(shift);\n\t\tp.scale(scale, scale);\n\t\tp.translate(-shift);\n\t}\n\tif (!_valid) {\n\t\treturn QRect();\n\t} else if (_flyIcon.isNull()) {\n\t\tconst auto wide = QRect(\n\t\t\ttarget.topLeft() - QPoint(target.width(), target.height()) / 2,\n\t\t\ttarget.size() * 2);\n\t\tconst auto area = _miniCopies.empty()\n\t\t\t? wide\n\t\t\t: QRect(\n\t\t\t\ttarget.topLeft() - QPoint(target.width(), target.height()),\n\t\t\t\ttarget.size() * 3);\n\t\tif (clip.isEmpty() || area.intersects(clip)) {\n\t\t\tpaintCenterFrame(p, target, colored, now);\n\t\t\tif (const auto effect = _effect.get()) {\n\t\t\t\tif (effect->animating()) {\n\t\t\t\t\t// Must not be colored to text.\n\t\t\t\t\tp.drawImage(wide, effect->frame(QColor()));\n\t\t\t\t}\n\t\t\t}\n\t\t\tpaintMiniCopies(p, target.center(), colored, now);\n\t\t}\n\t\treturn area;\n\t}\n\tconst auto from = _flyFrom.translated(origin);\n\tconst auto lshift = target.width() / 4;\n\tconst auto rshift = target.width() / 2 - lshift;\n\tconst auto margins = QMargins{ lshift, lshift, rshift, rshift };\n\ttarget = target.marginsRemoved(margins);\n\tconst auto progress = _fly.value(1.);\n\tconst auto rect = QRect(\n\t\tanim::interpolate(from.x(), target.x(), progress),\n\t\tcomputeParabolicTop(\n\t\t\t_cached,\n\t\t\tfrom.y(),\n\t\t\ttarget.y(),\n\t\t\tst::reactionFlyUp,\n\t\t\tprogress),\n\t\tanim::interpolate(from.width(), target.width(), progress),\n\t\tanim::interpolate(from.height(), target.height(), progress));\n\tconst auto wide = rect.marginsAdded(margins);\n\tif (clip.isEmpty() || wide.intersects(clip)) {\n\t\tif (progress < 1.) {\n\t\t\tp.setOpacity(1. - progress);\n\t\t\tp.drawImage(rect, _flyIcon);\n\t\t}\n\t\tif (progress > 0.) {\n\t\t\tp.setOpacity(progress);\n\t\t\tpaintCenterFrame(p, wide, colored, now);\n\t\t}\n\t\tp.setOpacity(1.);\n\t}\n\treturn wide;\n}\n\nvoid ReactionFlyAnimation::paintCenterFrame(\n\t\tQPainter &p,\n\t\tQRect target,\n\t\tconst QColor &colored,\n\t\tcrl::time now) const {\n\tif (_effectOnly) {\n\t\treturn;\n\t}\n\tconst auto size = QSize(\n\t\tint(base::SafeRound(target.width() * _centerSizeMultiplier)),\n\t\tint(base::SafeRound(target.height() * _centerSizeMultiplier)));\n\tif (_center) {\n\t\tconst auto rect = QRect(\n\t\t\ttarget.x() + (target.width() - size.width()) / 2,\n\t\t\ttarget.y() + (target.height() - size.height()) / 2,\n\t\t\tsize.width(),\n\t\t\tsize.height());\n\t\tp.drawImage(rect, _center->frame(st::windowFg->c));\n\t} else if (_custom) {\n\t\tconst auto scaled = (size.width() != _customSize);\n\t\t_custom->paint(p, {\n\t\t\t.textColor = colored,\n\t\t\t.size = { _customSize, _customSize },\n\t\t\t.now = now,\n\t\t\t.scale = (scaled ? (size.width() / float64(_customSize)) : 1.),\n\t\t\t.position = QPoint(\n\t\t\t\ttarget.x() + (target.width() - _customSize) / 2,\n\t\t\t\ttarget.y() + (target.height() - _customSize) / 2),\n\t\t\t.scaled = scaled,\n\t\t\t.internal = { .forceFirstFrame = _forceFirstFrame },\n\t\t});\n\t}\n}\n\nvoid ReactionFlyAnimation::paintMiniCopies(\n\t\tQPainter &p,\n\t\tQPoint center,\n\t\tconst QColor &colored,\n\t\tcrl::time now) const {\n\tExpects(_miniCopies.empty() || _custom != nullptr);\n\n\tif (!_minis.animating()) {\n\t\treturn;\n\t}\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto size = QSize(_customSize, _customSize);\n\tconst auto progress = _minis.value(1.);\n\tconst auto middle = center - QPoint(_customSize / 2, _customSize / 2);\n\tconst auto scaleIn = kMiniCopiesScaleInDuration\n\t\t/ float64(kMiniCopiesDurationMax);\n\tconst auto scaleOut = kMiniCopiesScaleOutDuration\n\t\t/ float64(kMiniCopiesDurationMax);\n\tauto context = Text::CustomEmoji::Context{\n\t\t.textColor = colored,\n\t\t.size = size,\n\t\t.now = now,\n\t\t.scaled = true,\n\t\t.internal = { .forceFirstFrame = _forceFirstFrame },\n\t};\n\tfor (const auto &mini : _miniCopies) {\n\t\tif (progress >= mini.duration) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto value = progress / mini.duration;\n\t\tcontext.scale = (progress < scaleIn)\n\t\t\t? (mini.maxScale * progress / scaleIn)\n\t\t\t: (progress <= mini.duration - scaleOut)\n\t\t\t? mini.maxScale\n\t\t\t: (mini.maxScale * (mini.duration - progress) / scaleOut);\n\t\tcontext.position = middle + QPoint(\n\t\t\tanim::interpolate(0, mini.finalX, value),\n\t\t\tcomputeParabolicTop(\n\t\t\t\tmini.cached,\n\t\t\t\t0,\n\t\t\t\tmini.finalY,\n\t\t\t\tmini.flyUp,\n\t\t\t\tvalue));\n\t\t_custom->paint(p, context);\n\t}\n}\n\nvoid ReactionFlyAnimation::generateMiniCopies(\n\t\tint size,\n\t\tfloat64 miniCopyMultiplier) {\n\tif (!_custom) {\n\t\treturn;\n\t}\n\tconst auto random = [] {\n\t\tconstexpr auto count = 16384;\n\t\treturn base::RandomIndex(count) / float64(count - 1);\n\t};\n\tconst auto between = [](int a, int b) {\n\t\treturn (a > b)\n\t\t\t? (b + base::RandomIndex(a - b + 1))\n\t\t\t: (a + base::RandomIndex(b - a + 1));\n\t};\n\t_miniCopies.reserve(kMiniCopies);\n\tfor (auto i = 0; i != kMiniCopies; ++i) {\n\t\tconst auto scale = kMiniCopiesMaxScaleMin\n\t\t\t+ (kMiniCopiesMaxScaleMax - kMiniCopiesMaxScaleMin) * random();\n\t\tconst auto maxScale = scale * miniCopyMultiplier;\n\t\tconst auto duration = between(\n\t\t\tkMiniCopiesDurationMin,\n\t\t\tkMiniCopiesDurationMax);\n\t\tconst auto maxSize = int(std::ceil(maxScale * _customSize));\n\t\tconst auto maxHalf = (maxSize + 1) / 2;\n\t\tconst auto flyUpTill = std::max(size - maxHalf, size / 4 + 1);\n\t\t_miniCopies.push_back({\n\t\t\t.maxScale = maxScale,\n\t\t\t.duration = duration / float64(kMiniCopiesDurationMax),\n\t\t\t.flyUp = between(size / 4, flyUpTill),\n\t\t\t.finalX = between(-size, size),\n\t\t\t.finalY = between(size - (size / 4), size),\n\t\t});\n\t}\n}\n\nint ReactionFlyAnimation::computeParabolicTop(\n\t\tParabolic &cache,\n\t\tint from,\n\t\tint to,\n\t\tint top,\n\t\tfloat64 progress) const {\n\tconst auto t = progress;\n\n\t// result = a * t * t + b * t + c\n\n\t// y = a * t * t + b * t\n\t// shift = y_1 = y(1) = a + b\n\t// y_0 = y(t_0) = a * t_0 * t_0 + b * t_0\n\t// 0 = 2 * a * t_0 + b\n\t// b = y_1 - a\n\t// a = y_1 / (1 - 2 * t_0)\n\t// b = 2 * t_0 * y_1 / (2 * t_0 - 1)\n\t// t_0 = (y_0 / y_1) +- sqrt((y_0 / y_1) * (y_0 / y_1 - 1))\n\tconst auto y_1 = to - from;\n\tif (cache.key != y_1) {\n\t\tconst auto y_0 = std::min(0, y_1) - top;\n\t\tconst auto ratio = y_1 ? (float64(y_0) / y_1) : 0.;\n\t\tconst auto root = y_1 ? sqrt(ratio * (ratio - 1)) : 0.;\n\t\tconst auto t_0 = !y_1\n\t\t\t? 0.5\n\t\t\t: (y_1 > 0)\n\t\t\t? (ratio + root)\n\t\t\t: (ratio - root);\n\t\tconst auto a = y_1 ? (y_1 / (1 - 2 * t_0)) : (-4 * y_0);\n\t\tconst auto b = y_1 - a;\n\t\tcache.key = y_1;\n\t\tcache.a = a;\n\t\tcache.b = b;\n\t}\n\n\treturn int(base::SafeRound(cache.a * t * t + cache.b * t + from));\n}\n\nvoid ReactionFlyAnimation::startAnimations() {\n\tif (const auto center = _center.get()) {\n\t\tcenter->animate(callback());\n\t}\n\tif (const auto effect = _effect.get()) {\n\t\teffect->animate(callback());\n\t} else if (_scaleOutDuration > 0) {\n\t\t_noEffectScaleStarted = true;\n\t\t_noEffectScaleAnimation.start(callback(), 1, 0, _scaleOutDuration);\n\t}\n\tif (!_miniCopies.empty()) {\n\t\t_minis.start(callback(), 0., 1., kMiniCopiesDurationMax);\n\t}\n}\n\nvoid ReactionFlyAnimation::setRepaintCallback(Fn<void()> repaint) {\n\t_repaint = std::move(repaint);\n}\n\nbool ReactionFlyAnimation::flying() const {\n\treturn !_flyIcon.isNull();\n}\n\nfloat64 ReactionFlyAnimation::flyingProgress() const {\n\treturn _fly.value(1.);\n}\n\nbool ReactionFlyAnimation::finished() const {\n\treturn !_valid\n\t\t|| (_flyIcon.isNull()\n\t\t\t&& (!_center || !_center->animating())\n\t\t\t&& (!_effect || !_effect->animating())\n\t\t\t&& !_noEffectScaleAnimation.animating()\n\t\t\t&& !_minis.animating());\n}\n\nReactionFlyCenter ReactionFlyAnimation::takeCenter() {\n\t_valid = false;\n\treturn {\n\t\t.custom = std::move(_custom),\n\t\t.icon = std::move(_center),\n\t\t.scale = (_scaleOutDuration > 0) ? _scaleOutTarget : 1.,\n\t\t.centerSizeMultiplier = _centerSizeMultiplier,\n\t\t.customSize = _customSize,\n\t};\n}\n\n} // namespace HistoryView::Reactions\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/reaction_fly_animation.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n#include \"data/data_message_reaction_id.h\"\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nnamespace Data {\nclass Reactions;\nenum class CustomEmojiSizeTag : uchar;\n} // namespace Data\n\nnamespace Ui {\n\nclass AnimatedIcon;\n\nstruct ReactionFlyAnimationArgs {\n\t::Data::ReactionId id;\n\tQImage flyIcon;\n\tQRect flyFrom;\n\tcrl::time scaleOutDuration = 0;\n\tfloat64 scaleOutTarget = 0.;\n\tfloat64 miniCopyMultiplier = 1.;\n\tbool effectOnly = false;\n\tbool forceFirstFrame = false;\n\n\t[[nodiscard]] ReactionFlyAnimationArgs translated(QPoint point) const;\n};\n\nstruct ReactionFlyCenter {\n\tstd::unique_ptr<Text::CustomEmoji> custom;\n\tstd::unique_ptr<AnimatedIcon> icon;\n\tfloat64 scale = 0.;\n\tfloat64 centerSizeMultiplier = 0.;\n\tint customSize = 0;\n\tint size = 0;\n\tbool forceFirstFrame = false;\n};\n\nclass ReactionFlyAnimation final {\npublic:\n\tReactionFlyAnimation(\n\t\tnot_null<::Data::Reactions*> owner,\n\t\tReactionFlyAnimationArgs &&args,\n\t\tFn<void()> repaint,\n\t\tint size,\n\t\tData::CustomEmojiSizeTag customSizeTag = {});\n\t~ReactionFlyAnimation();\n\n\tvoid setRepaintCallback(Fn<void()> repaint);\n\tQRect paintGetArea(\n\t\tQPainter &p,\n\t\tQPoint origin,\n\t\tQRect target,\n\t\tconst QColor &colored,\n\t\tQRect clip,\n\t\tcrl::time now) const;\n\n\t[[nodiscard]] bool flying() const;\n\t[[nodiscard]] float64 flyingProgress() const;\n\t[[nodiscard]] bool finished() const;\n\n\t[[nodiscard]] ReactionFlyCenter takeCenter();\n\nprivate:\n\tstruct Parabolic {\n\t\tfloat64 a = 0.;\n\t\tfloat64 b = 0.;\n\t\tstd::optional<int> key;\n\t};\n\tstruct MiniCopy {\n\t\tmutable Parabolic cached;\n\t\tfloat64 maxScale = 1.;\n\t\tfloat64 duration = 1.;\n\t\tint flyUp = 0;\n\t\tint finalX = 0;\n\t\tint finalY = 0;\n\t};\n\n\t[[nodiscard]] auto flyCallback();\n\t[[nodiscard]] auto callback();\n\tvoid startAnimations();\n\tint computeParabolicTop(\n\t\tParabolic &cache,\n\t\tint from,\n\t\tint to,\n\t\tint top,\n\t\tfloat64 progress) const;\n\tvoid paintCenterFrame(\n\t\tQPainter &p,\n\t\tQRect target,\n\t\tconst QColor &colored,\n\t\tcrl::time now) const;\n\tvoid paintMiniCopies(\n\t\tQPainter &p,\n\t\tQPoint center,\n\t\tconst QColor &colored,\n\t\tcrl::time now) const;\n\tvoid generateMiniCopies(int size, float64 miniCopyMultiplier);\n\n\tconst not_null<::Data::Reactions*> _owner;\n\tFn<void()> _repaint;\n\tQImage _flyIcon;\n\tstd::unique_ptr<Text::CustomEmoji> _custom;\n\tstd::unique_ptr<AnimatedIcon> _center;\n\tstd::unique_ptr<AnimatedIcon> _effect;\n\tAnimations::Simple _noEffectScaleAnimation;\n\tstd::vector<MiniCopy> _miniCopies;\n\tAnimations::Simple _fly;\n\tAnimations::Simple _minis;\n\tQRect _flyFrom;\n\tfloat64 _centerSizeMultiplier = 0.;\n\tint _customSize = 0;\n\tcrl::time _scaleOutDuration = 0;\n\tfloat64 _scaleOutTarget = 0.;\n\tbool _noEffectScaleStarted = false;\n\tbool _forceFirstFrame = false;\n\tbool _effectOnly = false;\n\tbool _valid = false;\n\n\tmutable Parabolic _cached;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/round_checkbox.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/round_checkbox.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/painter.h\"\n#include \"ui/effects/outline_segments.h\"\n#include \"styles/style_calls.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_widgets.h\"\n\n#include <QtCore/QCoreApplication>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kAnimationTimerDelta = crl::time(7);\nconstexpr auto kWideScale = 3;\n\n[[nodiscard]] int CountFramesCount(const style::RoundCheckbox *st) {\n\treturn (st->duration / kAnimationTimerDelta) + 1;\n}\n\nclass CheckCaches : public QObject {\npublic:\n\tCheckCaches(QObject *parent) : QObject(parent) {\n\t\tExpects(parent != nullptr);\n\n\t\tstyle::PaletteChanged(\n\t\t) | rpl::on_next([=] {\n\t\t\t_data.clear();\n\t\t}, _lifetime);\n\t}\n\n\tQPixmap frame(\n\t\tconst style::RoundCheckbox *st,\n\t\tbool displayInactive,\n\t\tfloat64 progress);\n\nprivate:\n\tstruct Frames {\n\t\tbool displayInactive = false;\n\t\tstd::vector<QPixmap> list;\n\t\tQPixmap outerWide;\n\t\tQPixmap inner;\n\t\tQPixmap check;\n\t};\n\n\tFrames &framesForStyle(\n\t\tconst style::RoundCheckbox *st,\n\t\tbool displayInactive);\n\tvoid prepareFramesData(\n\t\tconst style::RoundCheckbox *st,\n\t\tbool displayInactive,\n\t\tFrames &frames);\n\tQPixmap paintFrame(\n\t\tconst style::RoundCheckbox *st,\n\t\tconst Frames &frames,\n\t\tfloat64 progress);\n\n\tstd::map<const style::RoundCheckbox *, Frames> _data;\n\trpl::lifetime _lifetime;\n\n};\n\nQPixmap PrepareOuterWide(const style::RoundCheckbox *st) {\n\tconst auto size = st->size;\n\tconst auto wideSize = size * kWideScale;\n\tauto result = QImage(\n\t\tQSize(wideSize, wideSize) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&result);\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st->border);\n\t\tconst auto half = st->width / 2.;\n\t\tp.drawEllipse(QRectF(\n\t\t\t(wideSize - size) / 2 - half,\n\t\t\t(wideSize - size) / 2 - half,\n\t\t\tsize + 2. * half,\n\t\t\tsize + 2. * half));\n\t}\n\treturn Ui::PixmapFromImage(std::move(result));\n}\n\nQPixmap PrepareInner(const style::RoundCheckbox *st, bool displayInactive) {\n\tconst auto size = st->size;\n\tauto result = QImage(\n\t\tQSize(size, size) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&result);\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st->bgActive);\n\t\tconst auto half = st->width / 2.;\n\t\tp.drawEllipse(QRectF(\n\t\t\tdisplayInactive ? 0. : half,\n\t\t\tdisplayInactive ? 0. : half,\n\t\t\tsize - (displayInactive ? 0. : 2. * half),\n\t\t\tsize - (displayInactive ? 0. : 2. * half)));\n\t}\n\treturn Ui::PixmapFromImage(std::move(result));\n}\n\nQPixmap PrepareCheck(const style::RoundCheckbox *st) {\n\tconst auto size = st->size;\n\tauto result = QImage(\n\t\tQSize(size, size) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&result);\n\t\tst->check.paint(p, 0, 0, size);\n\t}\n\treturn Ui::PixmapFromImage(std::move(result));\n}\n\nQRect WideDestRect(\n\t\tconst style::RoundCheckbox *st,\n\t\tint x,\n\t\tint y,\n\t\tfloat64 scale) {\n\tauto iconSizeFull = kWideScale * st->size;\n\tauto iconSize = qRound(iconSizeFull * scale);\n\tif (iconSize % 2 != iconSizeFull % 2) {\n\t\t++iconSize;\n\t}\n\tauto iconShift = (iconSizeFull - iconSize) / 2;\n\tauto iconLeft = x - (kWideScale - 1) * st->size / 2 + iconShift;\n\tauto iconTop = y - (kWideScale - 1) * st->size / 2 + iconShift;\n\treturn QRect(iconLeft, iconTop, iconSize, iconSize);\n}\n\nCheckCaches::Frames &CheckCaches::framesForStyle(\n\t\tconst style::RoundCheckbox *st,\n\t\tbool displayInactive) {\n\tauto i = _data.find(st);\n\tif (i == _data.end()) {\n\t\ti = _data.emplace(st, Frames()).first;\n\t\tprepareFramesData(st, displayInactive, i->second);\n\t} else if (i->second.displayInactive != displayInactive) {\n\t\ti->second = Frames();\n\t\tprepareFramesData(st, displayInactive, i->second);\n\t}\n\treturn i->second;\n}\n\nvoid CheckCaches::prepareFramesData(\n\t\tconst style::RoundCheckbox *st,\n\t\tbool displayInactive,\n\t\tFrames &frames) {\n\tframes.list.resize(CountFramesCount(st));\n\tframes.displayInactive = displayInactive;\n\n\tif (!frames.displayInactive) {\n\t\tframes.outerWide = PrepareOuterWide(st);\n\t}\n\tframes.inner = PrepareInner(st, frames.displayInactive);\n\tframes.check = PrepareCheck(st);\n}\n\nQPixmap CheckCaches::frame(\n\t\tconst style::RoundCheckbox *st,\n\t\tbool displayInactive,\n\t\tfloat64 progress) {\n\tauto &frames = framesForStyle(st, displayInactive);\n\n\tconst auto frameCount = int(frames.list.size());\n\tconst auto frameIndex = int(base::SafeRound(progress * (frameCount - 1)));\n\tAssert(frameIndex >= 0 && frameIndex < frameCount);\n\n\tif (!frames.list[frameIndex]) {\n\t\tconst auto frameProgress = frameIndex / float64(frameCount - 1);\n\t\tframes.list[frameIndex] = paintFrame(st, frames, frameProgress);\n\t}\n\treturn frames.list[frameIndex];\n}\n\nQPixmap CheckCaches::paintFrame(\n\t\tconst style::RoundCheckbox *st,\n\t\tconst Frames &frames,\n\t\tfloat64 progress) {\n\tconst auto size = st->size;\n\tconst auto wideSize = size * kWideScale;\n\tconst auto skip = (wideSize - size) / 2;\n\tauto result = QImage(wideSize * style::DevicePixelRatio(), wideSize * style::DevicePixelRatio(), QImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult.fill(Qt::transparent);\n\n\tconst auto roundProgress = (progress >= st->bgDuration)\n\t\t? 1.\n\t\t: (progress / st->bgDuration);\n\tconst auto checkProgress = (1. - progress >= st->fgDuration)\n\t\t? 0.\n\t\t: (1. - (1. - progress) / st->fgDuration);\n\t{\n\t\tauto p = QPainter(&result);\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tif (!frames.displayInactive) {\n\t\t\tconst auto outerMaxScale = (size - st->width) / float64(size);\n\t\t\tconst auto outerScale = roundProgress\n\t\t\t\t+ (1. - roundProgress) * outerMaxScale;\n\t\t\tconst auto outerTo = WideDestRect(st, skip, skip, outerScale);\n\t\t\tconst auto outerFrom = QRect(\n\t\t\t\tQPoint(0, 0),\n\t\t\t\tQSize(wideSize, wideSize) * style::DevicePixelRatio());\n\t\t\tp.drawPixmap(outerTo, frames.outerWide, outerFrom);\n\t\t}\n\t\tp.drawPixmap(skip, skip, frames.inner);\n\n\t\tconst auto divider = checkProgress * st->size;\n\t\tconst auto checkTo = QRect(skip, skip, divider, st->size);\n\t\tconst auto checkFrom = QRect(\n\t\t\tQPoint(0, 0),\n\t\t\tQSize(divider, st->size) * style::DevicePixelRatio());\n\t\tp.drawPixmap(checkTo, frames.check, checkFrom);\n\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(Qt::transparent);\n\t\tconst auto remove = size * (1. - roundProgress);\n\t\tp.drawEllipse(QRectF(\n\t\t\t(wideSize - remove) / 2.,\n\t\t\t(wideSize - remove) / 2.,\n\t\t\tremove,\n\t\t\tremove));\n\t}\n\treturn Ui::PixmapFromImage(std::move(result));\n}\n\nCheckCaches *FrameCaches() {\n\tstatic QPointer<CheckCaches> Instance;\n\n\tif (const auto instance = Instance.data()) {\n\t\treturn instance;\n\t}\n\tconst auto result = new CheckCaches(QCoreApplication::instance());\n\tInstance = result;\n\treturn result;\n}\n\n} // namespace\n\nRoundCheckbox::RoundCheckbox(const style::RoundCheckbox &st, Fn<void()> updateCallback)\n: _st(st)\n, _updateCallback(updateCallback) {\n}\n\nvoid RoundCheckbox::paint(QPainter &p, int x, int y, int outerWidth, float64 masterScale) const {\n\tif (!_st.size\n\t\t|| (!_checkedProgress.animating()\n\t\t\t&& !_checked\n\t\t\t&& !_displayInactive)) {\n\t\treturn;\n\t}\n\n\tauto cacheSize = kWideScale * _st.size * style::DevicePixelRatio();\n\tauto cacheFrom = QRect(0, 0, cacheSize, cacheSize);\n\tauto inactiveTo = WideDestRect(&_st, x, y, masterScale);\n\n\tPainterHighQualityEnabler hq(p);\n\tif (!_inactiveCacheBg.isNull()) {\n\t\tp.drawPixmap(inactiveTo, _inactiveCacheBg, cacheFrom);\n\t}\n\n\tconst auto progress = _checkedProgress.value(_checked ? 1. : 0.);\n\tif (progress > 0.) {\n\t\tauto frame = FrameCaches()->frame(&_st, _displayInactive, progress);\n\t\tp.drawPixmap(inactiveTo, frame, cacheFrom);\n\t}\n\n\tif (!_inactiveCacheFg.isNull()) {\n\t\tp.drawPixmap(inactiveTo, _inactiveCacheFg, cacheFrom);\n\t}\n}\n\nvoid RoundCheckbox::setChecked(bool newChecked, anim::type animated) {\n\tif (_checked == newChecked) {\n\t\tif (animated == anim::type::instant) {\n\t\t\t_checkedProgress.stop();\n\t\t}\n\t\treturn;\n\t}\n\t_checked = newChecked;\n\tif (animated == anim::type::normal) {\n\t\t_checkedProgress.start(\n\t\t\t_updateCallback,\n\t\t\t_checked ? 0. : 1.,\n\t\t\t_checked ? 1. : 0.,\n\t\t\t_st.duration,\n\t\t\tanim::linear);\n\t} else {\n\t\t_checkedProgress.stop();\n\t}\n}\n\nvoid RoundCheckbox::finishAnimating() {\n\t_checkedProgress.stop();\n}\n\nvoid RoundCheckbox::invalidateCache() {\n\tif (!_inactiveCacheBg.isNull() || !_inactiveCacheFg.isNull()) {\n\t\tprepareInactiveCache();\n\t}\n}\n\nvoid RoundCheckbox::setDisplayInactive(bool displayInactive) {\n\tif (_displayInactive != displayInactive) {\n\t\t_displayInactive = displayInactive;\n\t\tif (_displayInactive) {\n\t\t\tprepareInactiveCache();\n\t\t} else {\n\t\t\t_inactiveCacheBg = _inactiveCacheFg = QPixmap();\n\t\t}\n\t}\n}\n\nvoid RoundCheckbox::prepareInactiveCache() {\n\tauto wideSize = _st.size * kWideScale;\n\tauto ellipse = QRect((wideSize - _st.size) / 2, (wideSize - _st.size) / 2, _st.size, _st.size);\n\n\tauto cacheBg = QImage(wideSize * style::DevicePixelRatio(), wideSize * style::DevicePixelRatio(), QImage::Format_ARGB32_Premultiplied);\n\tcacheBg.setDevicePixelRatio(style::DevicePixelRatio());\n\tcacheBg.fill(Qt::transparent);\n\tauto cacheFg = cacheBg;\n\tif (_st.bgInactive) {\n\t\tauto p = QPainter(&cacheBg);\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(_st.bgInactive);\n\t\tp.drawEllipse(ellipse);\n\t}\n\t_inactiveCacheBg = Ui::PixmapFromImage(std::move(cacheBg));\n\n\t{\n\t\tauto p = QPainter(&cacheFg);\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tauto pen = _st.border->p;\n\t\tpen.setWidth(_st.width);\n\t\tp.setPen(pen);\n\t\tp.setBrush(Qt::NoBrush);\n\t\tp.drawEllipse(ellipse);\n\t}\n\t_inactiveCacheFg = Ui::PixmapFromImage(std::move(cacheFg));\n}\n\nRoundImageCheckbox::RoundImageCheckbox(\n\tconst style::RoundImageCheckbox &st,\n\tFn<void()> updateCallback,\n\tPaintRoundImage &&paintRoundImage,\n\tFn<std::optional<int>(int size)> roundingRadius)\n: _st(st)\n, _updateCallback(updateCallback)\n, _paintRoundImage(std::move(paintRoundImage))\n, _roundingRadius(std::move(roundingRadius))\n, _check(_st.check, _updateCallback) {\n}\n\nRoundImageCheckbox::RoundImageCheckbox(RoundImageCheckbox&&) = default;\n\nRoundImageCheckbox::~RoundImageCheckbox() = default;\n\nvoid RoundImageCheckbox::paint(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth) const {\n\tif (_liveBadge) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tconst auto added = _st.selectWidth;\n\t\tconst auto cacheSize = (added + _st.imageRadius) * 2;\n\t\tconst auto fullCacheSize = cacheSize * ratio;\n\t\tif (_liveBadgeCache.width() != fullCacheSize) {\n\t\t\t_liveBadgeCache = QImage(\n\t\t\t\tQSize(fullCacheSize, fullCacheSize),\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\t_liveBadgeCache.setDevicePixelRatio(ratio);\n\t\t}\n\t\t_liveBadgeCache.fill(Qt::transparent);\n\t\tauto q = Painter(&_liveBadgeCache);\n\t\tpaintFrame(q, added, added, cacheSize);\n\t\tq.end();\n\n\t\tp.drawImage(x - added, y - added, _liveBadgeCache);\n\t} else {\n\t\tpaintFrame(p, x, y, outerWidth);\n\t}\n}\n\nvoid RoundImageCheckbox::paintFrame(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth) const {\n\tauto selectionLevel = _selection.value(checked() ? 1. : 0.);\n\tif (_selection.animating()) {\n\t\tauto userpicRadius = qRound(kWideScale\n\t\t\t* (_st.imageRadius + (_st.imageSmallRadius - _st.imageRadius)\n\t\t\t\t* selectionLevel));\n\t\tauto userpicShift = kWideScale * _st.imageRadius - userpicRadius;\n\t\tauto userpicLeft = x\n\t\t\t- ((kWideScale - 1) * _st.imageRadius)\n\t\t\t+ userpicShift;\n\t\tauto userpicTop = y\n\t\t\t- ((kWideScale - 1) * _st.imageRadius)\n\t\t\t+ userpicShift;\n\t\tauto to = QRect(userpicLeft, userpicTop, userpicRadius * 2, userpicRadius * 2);\n\t\tauto from = QRect(QPoint(0, 0), _wideCache.size());\n\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.drawPixmapLeft(to, outerWidth, _wideCache, from);\n\t} else {\n\t\tauto userpicRadius = checked() ? _st.imageSmallRadius : _st.imageRadius;\n\t\tauto userpicShift = _st.imageRadius - userpicRadius;\n\t\tauto userpicLeft = x + userpicShift;\n\t\tauto userpicTop = y + userpicShift;\n\t\t_paintRoundImage(p, userpicLeft, userpicTop, outerWidth, userpicRadius * 2);\n\t}\n\n\tif (selectionLevel > 0) {\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.setOpacity(std::clamp(selectionLevel, 0., 1.));\n\t\tp.setBrush(Qt::NoBrush);\n\t\tconst auto segments = int(_segments.size());\n\t\tconst auto rect = style::rtlrect(\n\t\t\tx,\n\t\t\ty,\n\t\t\t_st.imageRadius * 2,\n\t\t\t_st.imageRadius * 2,\n\t\t\touterWidth);\n\t\tconst auto add = _st.selectExtendTwice / 2.;\n\t\tconst auto outline = QRectF(rect).marginsAdded({\n\t\t\tadd, add, add, add });\n\t\tif (segments < 2) {\n\t\t\tconst auto radius = _roundingRadius\n\t\t\t\t? _roundingRadius(_st.imageRadius * 2)\n\t\t\t\t: std::optional<int>();\n\t\t\tconst auto pen = QPen(\n\t\t\t\tsegments ? _segments.front().brush : _st.selectFg->b,\n\t\t\t\tsegments ? _segments.front().width : _st.selectWidth);\n\t\t\tp.setPen(pen);\n\t\t\tif (!radius) {\n\t\t\t\tstyle::SquareUserpics() ? p.drawRect(outline) : p.drawEllipse(outline);\n\t\t\t} else {\n\t\t\t\tp.drawRoundedRect(outline, *radius, *radius);\n\t\t\t}\n\t\t} else {\n\t\t\tPaintOutlineSegments(p, outline, _segments);\n\t\t}\n\n\t\tif (_liveBadge) {\n\t\t\tPaintLiveBadge(p, x, y, _st.imageRadius * 2);\n\t\t}\n\n\t\tp.setOpacity(1.);\n\t}\n\tif (_st.check.size > 0) {\n\t\tauto iconLeft = x + 2 * _st.imageRadius + _st.selectWidth - _st.check.size;\n\t\tauto iconTop = y + 2 * _st.imageRadius + _st.selectWidth - _st.check.size;\n\t\t_check.paint(p, iconLeft, iconTop, outerWidth);\n\t}\n}\n\nfloat64 RoundImageCheckbox::checkedAnimationRatio() const {\n\treturn std::clamp(_selection.value(checked() ? 1. : 0.), 0., 1.);\n}\n\nvoid RoundImageCheckbox::setChecked(bool newChecked, anim::type animated) {\n\tauto changed = (checked() != newChecked);\n\t_check.setChecked(newChecked, animated);\n\tif (!changed) {\n\t\tif (animated == anim::type::instant) {\n\t\t\t_selection.stop();\n\t\t\t_wideCache = QPixmap();\n\t\t}\n\t\treturn;\n\t}\n\tif (animated == anim::type::normal) {\n\t\tprepareWideCache();\n\t\tconst auto from = checked() ? 0. : 1.;\n\t\tconst auto to = checked() ? 1. : 0.;\n\t\t_selection.start(\n\t\t\t[=](float64 value) {\n\t\t\t\tif (_updateCallback) {\n\t\t\t\t\t_updateCallback();\n\t\t\t\t}\n\t\t\t\tif (value == to) {\n\t\t\t\t\t_wideCache = QPixmap();\n\t\t\t\t}\n\t\t\t},\n\t\t\tfrom,\n\t\t\tto,\n\t\t\t_st.selectDuration,\n\t\t\tanim::bumpy(1.25));\n\t} else {\n\t\t_selection.stop();\n\t\t_wideCache = QPixmap();\n\t}\n}\n\nvoid RoundImageCheckbox::prepareWideCache() {\n\tif (_wideCache.isNull()) {\n\t\tauto size = _st.imageRadius * 2;\n\t\tauto wideSize = size * kWideScale;\n\t\tQImage cache(wideSize * style::DevicePixelRatio(), wideSize * style::DevicePixelRatio(), QImage::Format_ARGB32_Premultiplied);\n\t\tcache.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t{\n\t\t\tauto p = Painter(&cache);\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\t\tp.fillRect(0, 0, wideSize, wideSize, Qt::transparent);\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\t\t_paintRoundImage(p, (wideSize - size) / 2, (wideSize - size) / 2, wideSize, size);\n\t\t}\n\t\t_wideCache = Ui::PixmapFromImage(std::move(cache));\n\t}\n}\n\nvoid RoundImageCheckbox::setColorOverride(std::optional<QBrush> fg) {\n\tif (fg) {\n\t\tsetCustomizedSegments({\n\t\t\t{ .brush = *fg, .width = float64(_st.selectWidth) }\n\t\t}, false);\n\t} else {\n\t\tsetCustomizedSegments({}, false);\n\t}\n}\n\nvoid RoundImageCheckbox::setCustomizedSegments(\n\t\tstd::vector<Ui::OutlineSegment> segments,\n\t\tbool liveBadge) {\n\t_segments = std::move(segments);\n\t_liveBadge = liveBadge;\n}\n\nvoid PaintLiveBadge(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint photoSize,\n\t\tstd::optional<QColor> outline) {\n\tconst auto &st = st::groupCallMessageBadge;\n\tconst auto text = tr::lng_video_stream_live(tr::now);\n\tauto string = Ui::Text::String(st.style, text);\n\tconst auto size = QSize(string.maxWidth(), string.minHeight());\n\n\tconst auto full = QSize(\n\t\t(st.width < 0) ? (size.width() - st.width) : st.width,\n\t\tst.height);\n\tconst auto left = x + (photoSize - full.width()) / 2;\n\tconst auto top = y + photoSize - full.height();\n\n\tconst auto stroke = st::dialogsStories.lineTwice / 2.;\n\tauto pen = QPen(outline.value_or(QColor(Qt::transparent)));\n\tpen.setWidthF(stroke);\n\n\tconst auto half = stroke / 2.;\n\tif (!outline) {\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t}\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setPen(pen);\n\tp.setBrush(st.textBg);\n\n\tconst auto r = st.radius + half;\n\tconst auto rect = QRectF(left, top, full.width(), full.height());\n\tconst auto sub = QMarginsF(half, half, half, half);\n\tp.drawRoundedRect(rect.marginsAdded(sub), r, r);\n\n\tif (!outline) {\n\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t}\n\tconst auto textLeft = (full.width() - size.width()) / 2;\n\tp.setPen(st.textFg);\n\tstring.draw(p, { .position = { left + textLeft, top + st.textTop } });\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/round_checkbox.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n\nnamespace style {\nstruct RoundCheckbox;\nstruct RoundImageCheckbox;\n} // namespace style\n\nclass Painter;\nenum class ImageRoundRadius;\n\nnamespace Ui {\n\nstruct OutlineSegment;\n\nclass RoundCheckbox {\npublic:\n\tRoundCheckbox(const style::RoundCheckbox &st, Fn<void()> updateCallback);\n\n\tvoid paint(QPainter &p, int x, int y, int outerWidth, float64 masterScale = 1.) const;\n\n\tvoid setDisplayInactive(bool displayInactive);\n\tbool checked() const {\n\t\treturn _checked;\n\t}\n\tvoid setChecked(\n\t\tbool newChecked,\n\t\tanim::type animated = anim::type::normal);\n\tvoid finishAnimating();\n\n\tvoid invalidateCache();\n\nprivate:\n\tvoid prepareInactiveCache();\n\n\tconst style::RoundCheckbox &_st;\n\tFn<void()> _updateCallback;\n\n\tbool _checked = false;\n\tUi::Animations::Simple _checkedProgress;\n\n\tbool _displayInactive = false;\n\tQPixmap _inactiveCacheBg, _inactiveCacheFg;\n\n};\n\nclass RoundImageCheckbox {\npublic:\n\tusing PaintRoundImage = Fn<void(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size)>;\n\tRoundImageCheckbox(\n\t\tconst style::RoundImageCheckbox &st,\n\t\tFn<void()> updateCallback,\n\t\tPaintRoundImage &&paintRoundImage,\n\t\tFn<std::optional<int>(int size)> roundingRadius = nullptr);\n\tRoundImageCheckbox(RoundImageCheckbox&&);\n\t~RoundImageCheckbox();\n\n\tvoid paint(Painter &p, int x, int y, int outerWidth) const;\n\tfloat64 checkedAnimationRatio() const;\n\n\tvoid setColorOverride(std::optional<QBrush> fg);\n\tvoid setCustomizedSegments(\n\t\tstd::vector<OutlineSegment> segments,\n\t\tbool liveBadge);\n\n\tbool checked() const {\n\t\treturn _check.checked();\n\t}\n\tvoid setChecked(\n\t\tbool newChecked,\n\t\tanim::type animated = anim::type::normal);\n\n\tvoid invalidateCache() {\n\t\t_check.invalidateCache();\n\t}\n\nprivate:\n\tvoid prepareWideCache();\n\tvoid paintFrame(Painter &p, int x, int y, int outerWidth) const;\n\n\tconst style::RoundImageCheckbox &_st;\n\tFn<void()> _updateCallback;\n\tPaintRoundImage _paintRoundImage;\n\tFn<std::optional<int>(int size)> _roundingRadius;\n\n\tQPixmap _wideCache;\n\tUi::Animations::Simple _selection;\n\n\tRoundCheckbox _check;\n\n\t//std::optional<QBrush> _fgOverride;\n\tstd::vector<OutlineSegment> _segments;\n\n\tmutable QImage _liveBadgeCache;\n\tbool _liveBadge = false;\n\n};\n\nvoid PaintLiveBadge(\n\tQPainter &p,\n\tint x,\n\tint y,\n\tint photoSize,\n\tstd::optional<QColor> outline = std::nullopt);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/scroll_content_shadow.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/scroll_content_shadow.h\"\n\n#include \"ui/rp_widget.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/fade_wrap.h\"\n#include \"ui/ui_utility.h\"\n\nnamespace Ui {\n\nvoid SetupShadowsToScrollContent(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Ui::ScrollArea*> scroll,\n\t\trpl::producer<int> &&innerHeightValue) {\n\tusing namespace rpl::mappers;\n\n\tconst auto topShadow = Ui::CreateChild<Ui::FadeShadow>(parent.get());\n\tconst auto bottomShadow = Ui::CreateChild<Ui::FadeShadow>(parent.get());\n\tscroll->geometryValue(\n\t) | rpl::on_next_done([=](const QRect &geometry) {\n\t\ttopShadow->resizeToWidth(geometry.width());\n\t\ttopShadow->move(\n\t\t\tgeometry.x(),\n\t\t\tgeometry.y());\n\t\tbottomShadow->resizeToWidth(geometry.width());\n\t\tbottomShadow->move(\n\t\t\tgeometry.x(),\n\t\t\tgeometry.y() + geometry.height() - st::lineWidth);\n\t}, [t = base::make_weak(topShadow), b = base::make_weak(bottomShadow)] {\n\t\tUi::DestroyChild(t.get());\n\t\tUi::DestroyChild(b.get());\n\t}, topShadow->lifetime());\n\n\ttopShadow->toggleOn(scroll->scrollTopValue() | rpl::map(_1 > 0));\n\tbottomShadow->toggleOn(rpl::combine(\n\t\tscroll->scrollTopValue(),\n\t\tscroll->heightValue(),\n\t\tstd::move(innerHeightValue),\n\t\t_1 + _2 < _3));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/scroll_content_shadow.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nclass RpWidget;\nclass ScrollArea;\n\nvoid SetupShadowsToScrollContent(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Ui::ScrollArea*> scroll,\n\trpl::producer<int> &&innerHeightValue);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/send_action_animations.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/send_action_animations.h\"\n\n#include \"api/api_send_progress.h\"\n#include \"base/never_freed_pointer.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr int kTypingDotsCount = 3;\nconstexpr int kRecordArcsCount = 4;\nconstexpr int kUploadArrowsCount = 3;\nconstexpr auto kSpeakingDuration = 3200;\nconstexpr auto kSpeakingFadeDuration = 400;\n\n} // namespace\n\nclass SendActionAnimation::Impl {\npublic:\n\tusing Type = Api::SendProgressType;\n\n\tImpl(int period) : _period(period), _started(crl::now()) {\n\t}\n\n\tstruct MetaData {\n\t\tint index;\n\t\tstd::unique_ptr<Impl>(*creator)();\n\t};\n\tvirtual const MetaData *metaData() const = 0;\n\tbool supports(Type type) const;\n\n\tvirtual int width() const = 0;\n\tvirtual int widthNoMargins() const {\n\t\treturn width();\n\t}\n\tvirtual void paint(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tcrl::time now) = 0;\n\n\tvirtual void restartedAt(crl::time now) {\n\t}\n\tvirtual bool finishNow() {\n\t\treturn true;\n\t}\n\n\tvirtual ~Impl() = default;\n\nprotected:\n\t[[nodiscard]] int period() const {\n\t\treturn _period;\n\t}\n\t[[nodiscard]] crl::time started() const {\n\t\treturn _started;\n\t}\n\t[[nodiscard]] int frameTime(crl::time now) const {\n\t\treturn anim::Disabled()\n\t\t\t? 0\n\t\t\t: (std::max(now - _started, crl::time(0)) % _period);\n\t}\n\nprivate:\n\tint _period = 1;\n\tcrl::time _started = 0;\n\n};\n\nnamespace {\n\nusing ImplementationsMap = QMap<\n\tApi::SendProgressType,\n\tconst SendActionAnimation::Impl::MetaData*>;\nbase::NeverFreedPointer<ImplementationsMap> Implementations;\n\nclass TypingAnimation : public SendActionAnimation::Impl {\npublic:\n\tTypingAnimation() : Impl(st::historySendActionTypingDuration) {\n\t}\n\n\tstatic const MetaData kMeta;\n\tstatic std::unique_ptr<Impl> create() {\n\t\treturn std::make_unique<TypingAnimation>();\n\t}\n\tconst MetaData *metaData() const override {\n\t\treturn &kMeta;\n\t}\n\n\tint width() const override {\n\t\treturn st::historySendActionTypingPosition.x()\n\t\t\t+ kTypingDotsCount * st::historySendActionTypingDelta;\n\t}\n\n\tvoid paint(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tcrl::time now) override;\n\n};\n\nconst TypingAnimation::MetaData TypingAnimation::kMeta = {\n\t0,\n\t&TypingAnimation::create,\n};\n\nvoid TypingAnimation::paint(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tcrl::time now) {\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(color);\n\tauto frameMs = frameTime(now);\n\tauto position = QPointF(x + 0.5, y - 0.5)\n\t\t+ st::historySendActionTypingPosition;\n\tfor (auto i = 0; i != kTypingDotsCount; ++i) {\n\t\tauto r = st::historySendActionTypingSmallNumerator\n\t\t\t/ st::historySendActionTypingDenominator;\n\t\tif (frameMs < 2 * st::historySendActionTypingHalfPeriod) {\n\t\t\tconst auto delta = (st::historySendActionTypingLargeNumerator\n\t\t\t\t\t- st::historySendActionTypingSmallNumerator)\n\t\t\t\t/ st::historySendActionTypingDenominator;\n\t\t\tif (frameMs < st::historySendActionTypingHalfPeriod) {\n\t\t\t\tr += delta\n\t\t\t\t\t* anim::easeOutCirc(\n\t\t\t\t\t\t1.,\n\t\t\t\t\t\tfloat64(frameMs)\n\t\t\t\t\t\t\t/ st::historySendActionTypingHalfPeriod);\n\t\t\t} else {\n\t\t\t\tr += delta\n\t\t\t\t\t* (1. - anim::easeOutCirc(\n\t\t\t\t\t\t1.,\n\t\t\t\t\t\tfloat64(frameMs\n\t\t\t\t\t\t\t\t- st::historySendActionTypingHalfPeriod)\n\t\t\t\t\t\t\t/ st::historySendActionTypingHalfPeriod));\n\t\t\t}\n\t\t}\n\t\tp.drawEllipse(position, r, r);\n\t\tposition.setX(position.x() + st::historySendActionTypingDelta);\n\t\tframeMs = (frameMs + period() - st::historySendActionTypingDeltaTime)\n\t\t\t% period();\n\t}\n}\n\nclass RecordAnimation : public SendActionAnimation::Impl {\npublic:\n\tRecordAnimation() : Impl(st::historySendActionRecordDuration) {\n\t}\n\n\tstatic const MetaData kMeta;\n\tstatic std::unique_ptr<Impl> create() {\n\t\treturn std::make_unique<RecordAnimation>();\n\t}\n\tconst MetaData *metaData() const override {\n\t\treturn &kMeta;\n\t}\n\n\tint width() const override {\n\t\treturn st::historySendActionRecordPosition.x()\n\t\t\t+ (kRecordArcsCount + 1) * st::historySendActionRecordDelta;\n\t}\n\n\tvoid paint(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tcrl::time now) override;\n\n};\n\nconst RecordAnimation::MetaData RecordAnimation::kMeta = {\n\t0,\n\t&RecordAnimation::create,\n};\n\nvoid RecordAnimation::paint(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tcrl::time now) {\n\tPainterHighQualityEnabler hq(p);\n\tconst auto frameMs = frameTime(now);\n\tauto pen = color->p;\n\tpen.setWidth(st::historySendActionRecordStrokeNumerator\n\t\t/ st::historySendActionRecordDenominator);\n\tpen.setJoinStyle(Qt::RoundJoin);\n\tpen.setCapStyle(Qt::RoundCap);\n\tp.setPen(pen);\n\tp.setBrush(Qt::NoBrush);\n\tauto progress = frameMs / float64(period());\n\tauto size = st::historySendActionRecordPosition.x()\n\t\t+ st::historySendActionRecordDelta * progress;\n\ty += st::historySendActionRecordPosition.y();\n\tconstexpr auto kAngleStart = -arc::kFullLength / 24;\n\tconstexpr auto kAngleSpan = arc::kFullLength / 12;\n\tfor (auto i = 0; i != kRecordArcsCount; ++i) {\n\t\tp.setOpacity((i == 0)\n\t\t\t? progress\n\t\t\t: (i == kRecordArcsCount - 1)\n\t\t\t? (1. - progress)\n\t\t\t: 1.);\n\t\tauto rect = QRectF(x - size, y - size, 2 * size, 2 * size);\n\t\tp.drawArc(rect, kAngleStart, kAngleSpan);\n\t\tsize += st::historySendActionRecordDelta;\n\t}\n\tp.setOpacity(1.);\n}\n\nclass UploadAnimation : public SendActionAnimation::Impl {\npublic:\n\tUploadAnimation() : Impl(st::historySendActionUploadDuration) {\n\t}\n\n\tstatic const MetaData kMeta;\n\tstatic std::unique_ptr<Impl> create() {\n\t\treturn std::make_unique<UploadAnimation>();\n\t}\n\tconst MetaData *metaData() const override {\n\t\treturn &kMeta;\n\t}\n\n\tint width() const override {\n\t\treturn st::historySendActionUploadPosition.x()\n\t\t\t+ (kUploadArrowsCount + 1) * st::historySendActionUploadDelta;\n\t}\n\n\tvoid paint(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tcrl::time now) override;\n\n};\n\nconst UploadAnimation::MetaData UploadAnimation::kMeta = {\n\t0,\n\t&UploadAnimation::create,\n};\n\nvoid UploadAnimation::paint(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tcrl::time now) {\n\tPainterHighQualityEnabler hq(p);\n\tconst auto frameMs = frameTime(now);\n\tauto pen = color->p;\n\tpen.setWidth(st::historySendActionUploadStrokeNumerator\n\t\t/ st::historySendActionUploadDenominator);\n\tpen.setJoinStyle(Qt::RoundJoin);\n\tpen.setCapStyle(Qt::RoundCap);\n\tp.setPen(pen);\n\tp.setBrush(Qt::NoBrush);\n\tauto progress = frameMs / float64(period());\n\tauto position = st::historySendActionUploadPosition\n\t\t+ QPointF(x + st::historySendActionUploadDelta * progress, y);\n\tauto path = QPainterPath();\n\tpath.moveTo(\n\t\t0.,\n\t\t-st::historySendActionUploadSizeNumerator\n\t\t\t/ st::historySendActionUploadDenominator);\n\tpath.lineTo(\n\t\tst::historySendActionUploadSizeNumerator\n\t\t\t/ st::historySendActionUploadDenominator,\n\t\t0.);\n\tpath.lineTo(\n\t\t0.,\n\t\tst::historySendActionUploadSizeNumerator\n\t\t\t/ st::historySendActionUploadDenominator);\n\tp.translate(position);\n\tfor (auto i = 0; i != kUploadArrowsCount; ++i) {\n\t\tp.setOpacity((i == 0)\n\t\t\t? progress\n\t\t\t: (i == kUploadArrowsCount - 1)\n\t\t\t? (1. - progress)\n\t\t\t: 1.);\n\t\tp.drawPath(path);\n\t\tposition.setX(position.x() + st::historySendActionUploadDelta);\n\t\tp.translate(st::historySendActionUploadDelta, 0);\n\t}\n\tp.setOpacity(1.);\n\tp.translate(-position);\n}\n\nclass SpeakingAnimation : public SendActionAnimation::Impl {\npublic:\n\tSpeakingAnimation();\n\n\tstatic const MetaData kMeta;\n\tstatic std::unique_ptr<Impl> create() {\n\t\treturn std::make_unique<SpeakingAnimation>();\n\t}\n\tconst MetaData *metaData() const override {\n\t\treturn &kMeta;\n\t}\n\n\tint width() const override {\n\t\tconst auto &numerator = st::dialogsSpeakingStrokeNumerator;\n\t\tconst auto &denominator = st::dialogsSpeakingDenominator;\n\t\treturn 4 * (numerator / denominator);\n\t}\n\n\tvoid restartedAt(crl::time now) override;\n\tbool finishNow() override;\n\n\tstatic void PaintIdle(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth);\n\n\tvoid paint(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tcrl::time now) override;\n\nprivate:\n\tstatic void PaintFrame(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint frameMs,\n\t\tfloat64 started);\n\n\tcrl::time _startStarted = 0;\n\tcrl::time _finishStarted = 0;\n\n};\n\nconst SpeakingAnimation::MetaData SpeakingAnimation::kMeta = {\n\t0,\n\t&SpeakingAnimation::create };\n\nSpeakingAnimation::SpeakingAnimation()\n: Impl(kSpeakingDuration)\n, _startStarted(crl::now()) {\n}\n\nvoid SpeakingAnimation::restartedAt(crl::time now) {\n\tif (!_finishStarted) {\n\t\treturn;\n\t}\n\tconst auto finishFinishes = _finishStarted + kSpeakingFadeDuration;\n\tconst auto leftToFinish = (finishFinishes - now);\n\tif (leftToFinish > 0) {\n\t\t_startStarted = now - leftToFinish;\n\t} else {\n\t\t_startStarted = now;\n\t}\n\t_finishStarted = 0;\n}\n\nbool SpeakingAnimation::finishNow() {\n\tconst auto now = crl::now();\n\tif (_finishStarted) {\n\t\treturn (_finishStarted + kSpeakingFadeDuration <= now);\n\t} else if (_startStarted >= now) {\n\t\treturn true;\n\t}\n\tconst auto startFinishes = _startStarted + kSpeakingFadeDuration;\n\tconst auto leftToStart = (startFinishes - now);\n\tif (leftToStart > 0) {\n\t\t_finishStarted = now - leftToStart;\n\t} else {\n\t\t_finishStarted = now;\n\t}\n\treturn false;\n}\n\nvoid SpeakingAnimation::PaintIdle(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth) {\n\tPaintFrame(p, color, x, y, outerWidth, 0, 0.);\n}\n\nvoid SpeakingAnimation::paint(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tcrl::time now) {\n\tconst auto started = _finishStarted\n\t\t? (1. - ((now - _finishStarted) / float64(kSpeakingFadeDuration)))\n\t\t: (now - _startStarted) / float64(kSpeakingFadeDuration);\n\tconst auto progress = std::clamp(started, 0., 1.);\n\tPaintFrame(p, color, x, y, outerWidth, frameTime(now), progress);\n}\n\nvoid SpeakingAnimation::PaintFrame(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint frameMs,\n\t\tfloat64 started) {\n\tPainterHighQualityEnabler hq(p);\n\n\tconst auto line = st::dialogsSpeakingStrokeNumerator\n\t\t/ (2 * st::dialogsSpeakingDenominator);\n\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(color);\n\n\tconst auto duration = kSpeakingDuration;\n\tconst auto stageDuration = duration / 8;\n\tconst auto fullprogress = frameMs;\n\tconst auto stage = fullprogress / stageDuration;\n\tconst auto progress = (fullprogress - stage * stageDuration)\n\t\t/ float64(stageDuration);\n\tconst auto half = st::dialogsCallBadgeSize / 2.;\n\tconst auto center = QPointF(x + half, y + half);\n\tconst auto middleSize = [&] {\n\t\tif (!started) {\n\t\t\treturn 2 * line;\n\t\t}\n\t\tauto result = line;\n\t\tswitch (stage) {\n\t\tcase 0: result += 4 * line * progress; break;\n\t\tcase 1: result += 4 * line * (1. - progress); break;\n\t\tcase 2: result += 2 * line * progress; break;\n\t\tcase 3: result += 2 * line * (1. - progress); break;\n\t\tcase 4: result += 4 * line * progress; break;\n\t\tcase 5: result += 4 * line * (1. - progress); break;\n\t\tcase 6: result += 4 * line * progress; break;\n\t\tcase 7: result += 4 * line * (1. - progress); break;\n\t\t}\n\t\treturn (started == 1.)\n\t\t\t? result\n\t\t\t: (started * result) + ((1. - started) * 2 * line);\n\t}();\n\tconst auto sideSize = [&] {\n\t\tif (!started) {\n\t\t\treturn 2 * line;\n\t\t}\n\t\tauto result = line;\n\t\tswitch (stage) {\n\t\tcase 0: result += 2 * line * (1. - progress); break;\n\t\tcase 1: result += 4 * line * progress; break;\n\t\tcase 2: result += 4 * line * (1. - progress); break;\n\t\tcase 3: result += 2 * line * progress; break;\n\t\tcase 4: result += 2 * line * (1. - progress); break;\n\t\tcase 5: result += 4 * line * progress; break;\n\t\tcase 6: result += 4 * line * (1. - progress); break;\n\t\tcase 7: result += 2 * line * progress; break;\n\t\t}\n\t\treturn (started == 1.)\n\t\t\t? result\n\t\t\t: (started * result) + ((1. - started) * 2 * line);\n\t}();\n\n\tconst auto drawRoundedRect = [&](float left, float size) {\n\t\tconst auto top = center.y() - size;\n\t\tp.drawRoundedRect(QRectF(left, top, 2 * line, 2 * size), line, line);\n\t};\n\n\tauto left = center.x() - 4 * line;\n\tdrawRoundedRect(left, sideSize);\n\tleft += 3 * line;\n\tdrawRoundedRect(left, middleSize);\n\tleft += 3 * line;\n\tdrawRoundedRect(left, sideSize);\n}\n\nclass ChooseStickerAnimation : public SendActionAnimation::Impl {\npublic:\n\tChooseStickerAnimation()\n\t: Impl(st::historySendActionChooseStickerDuration)\n\t, _eye({\n\t\t.outWidth = float64(st::historySendActionChooseStickerEyeWidth),\n\t\t.outHeight = float64(st::historySendActionChooseStickerEyeHeight),\n\t\t.step = float64(st::historySendActionChooseStickerEyeStep),\n\t\t.inLeftOffset = style::ConvertScale(1.5),\n\t\t.inRightOffset = -style::ConvertScale(2.5)\n\t\t\t+ st::historySendActionChooseStickerEyeWidth,\n\t\t.outXOffset = style::ConvertScale(1.5),\n\t\t.outStrokeWidth = style::ConvertScale(0.8 * 1.3),\n\t\t.inStrokeWidth = style::ConvertScale(1.2 * 1.3),\n\t\t.inSize = style::ConvertScale(2.),\n\t\t.minProgress = 0.3,\n\t\t.outHeightOffset = 1.5,\n\t}) {\n\t}\n\n\tstatic const MetaData kMeta;\n\tstatic std::unique_ptr<Impl> create() {\n\t\treturn std::make_unique<ChooseStickerAnimation>();\n\t}\n\tconst MetaData *metaData() const override {\n\t\treturn &kMeta;\n\t}\n\n\tint width() const override {\n\t\treturn widthNoMargins() + _eye.step * 2;\n\t}\n\n\tint widthNoMargins() const override {\n\t\treturn st::historySendActionChooseStickerPosition.x()\n\t\t\t+ 2 * (_eye.outWidth + _eye.step)\n\t\t\t+ _eye.step;\n\t}\n\n\tvoid paint(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tcrl::time now) override;\n\nprivate:\n\tconst struct {\n\t\tconst float64 outWidth;\n\t\tconst float64 outHeight;\n\t\tconst float64 step;\n\t\tconst float64 inLeftOffset;\n\t\tconst float64 inRightOffset;\n\t\tconst float64 outXOffset;\n\t\tconst float64 outStrokeWidth;\n\t\tconst float64 inStrokeWidth;\n\t\tconst float64 inSize;\n\t\tconst float64 minProgress;\n\t\tconst float64 outHeightOffset;\n\t} _eye;\n\n};\n\nconst ChooseStickerAnimation::MetaData ChooseStickerAnimation::kMeta = {\n\t0,\n\t&ChooseStickerAnimation::create,\n};\n\nvoid ChooseStickerAnimation::paint(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tcrl::time now) {\n\tPainterHighQualityEnabler hq(p);\n\tconst auto frameMs = frameTime(now);\n\tauto pen = color->p;\n\tpen.setJoinStyle(Qt::RoundJoin);\n\tpen.setCapStyle(Qt::RoundCap);\n\n\tconst auto half = float64(period() / 2);\n\tconst auto increment = (frameMs < half) ? true : false;\n\t// A double-progress within a period half.\n\tconst auto progress = (frameMs / (half / 2)) - (increment ? 0 : 2);\n\n\tconst auto animationProgress = std::min(progress, 1.);\n\n\tconst auto k = _eye.minProgress;\n\tconst auto pIn = anim::easeInCirc(1, std::min(animationProgress / k, 1.));\n\tconst auto pInRev = 1. - pIn;\n\tconst auto pOut = anim::easeOutCirc(1., (animationProgress < k)\n\t\t? 0.\n\t\t: (animationProgress - k) / (1. - k));\n\n\tconst auto inX = _eye.inLeftOffset * (increment ? pIn : pInRev)\n\t\t+ _eye.inRightOffset * (increment ? pInRev : pIn);\n\tconst auto inY = (_eye.outHeight - _eye.inSize) / 2.;\n\n\tconst auto outLeft = _eye.outXOffset\n\t\t* (increment\n\t\t\t? (1. - anim::easeOutCirc(1., progress / 2.))\n\t\t\t: anim::easeOutQuint(1., progress / 2.));\n\n\tconst auto outScaleOffset = (pIn - pOut) * _eye.outHeightOffset;\n\tconst auto top = st::historySendActionChooseStickerPosition.y() + y;\n\tconst auto left = st::historySendActionChooseStickerPosition.x()\n\t\t+ x\n\t\t+ outLeft;\n\n\tfor (auto i = 0; i < 2; i++) {\n\t\tconst auto currentLeft = left + (_eye.outWidth + _eye.step) * i;\n\n\t\tpen.setWidthF(_eye.outStrokeWidth);\n\t\tp.setPen(pen);\n\t\tp.setBrush(Qt::NoBrush);\n\t\tp.drawEllipse(QRectF(\n\t\t\tcurrentLeft,\n\t\t\ttop + outScaleOffset,\n\t\t\t_eye.outWidth,\n\t\t\t_eye.outHeight - outScaleOffset));\n\n\t\tpen.setWidthF(_eye.inStrokeWidth);\n\t\tp.setPen(pen);\n\t\tp.setBrush(color->b);\n\t\tp.drawEllipse(QRectF(\n\t\t\tcurrentLeft + inX,\n\t\t\ttop + inY,\n\t\t\t_eye.inSize,\n\t\t\t_eye.inSize));\n\t}\n}\n\nvoid CreateImplementationsMap() {\n\tif (Implementations) {\n\t\treturn;\n\t}\n\tusing Type = Api::SendProgressType;\n\tImplementations.createIfNull();\n\tstatic constexpr auto kRecordTypes = {\n\t\tType::RecordVideo,\n\t\tType::RecordVoice,\n\t\tType::RecordRound,\n\t};\n\tfor (const auto type : kRecordTypes) {\n\t\tImplementations->insert(type, &RecordAnimation::kMeta);\n\t}\n\tstatic constexpr auto kUploadTypes = {\n\t\tType::UploadFile,\n\t\tType::UploadPhoto,\n\t\tType::UploadVideo,\n\t\tType::UploadVoice,\n\t\tType::UploadRound,\n\t};\n\tfor (const auto type : kUploadTypes) {\n\t\tImplementations->insert(type, &UploadAnimation::kMeta);\n\t}\n\tImplementations->insert(Type::Speaking, &SpeakingAnimation::kMeta);\n\tImplementations->insert(\n\t\tType::ChooseSticker,\n\t\t&ChooseStickerAnimation::kMeta);\n}\n\n} // namespace\n\nSendActionAnimation::SendActionAnimation() = default;\n\nSendActionAnimation::~SendActionAnimation() = default;\n\nbool SendActionAnimation::Impl::supports(Type type) const {\n\tCreateImplementationsMap();\n\treturn Implementations->value(type, &TypingAnimation::kMeta)\n\t\t== metaData();\n}\n\nvoid SendActionAnimation::start(Type type) {\n\tif (!_impl || !_impl->supports(type)) {\n\t\t_impl = CreateByType(type);\n\t} else {\n\t\t_impl->restartedAt(crl::now());\n\t}\n}\n\nvoid SendActionAnimation::tryToFinish() {\n\tif (!_impl) {\n\t\treturn;\n\t} else if (_impl->finishNow()) {\n\t\t_impl.reset();\n\t}\n}\n\nint SendActionAnimation::width() const {\n\treturn _impl ? _impl->width() : 0;\n}\n\nint SendActionAnimation::widthNoMargins() const {\n\treturn _impl ? _impl->widthNoMargins() : 0;\n}\n\nvoid SendActionAnimation::paint(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tcrl::time ms) const {\n\tif (_impl) {\n\t\t_impl->paint(p, color, x, y, outerWidth, ms);\n\t}\n}\n\nvoid SendActionAnimation::PaintSpeakingIdle(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth) {\n\tSpeakingAnimation::PaintIdle(p, color, x, y, outerWidth);\n}\n\nauto SendActionAnimation::CreateByType(Type type) -> std::unique_ptr<Impl> {\n\tCreateImplementationsMap();\n\treturn Implementations->value(type, &TypingAnimation::kMeta)->creator();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/send_action_animations.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Api {\nenum class SendProgressType;\n} // namespace Api\n\nnamespace Ui {\n\nclass SendActionAnimation {\npublic:\n\tusing Type = Api::SendProgressType;\n\tclass Impl;\n\n\tSendActionAnimation();\n\t~SendActionAnimation();\n\n\tvoid start(Type type);\n\tvoid tryToFinish();\n\n\tint width() const;\n\tint widthNoMargins() const;\n\tvoid paint(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tcrl::time ms) const;\n\n\texplicit operator bool() const {\n\t\treturn _impl != nullptr;\n\t}\n\n\tstatic void PaintSpeakingIdle(\n\t\tQPainter &p,\n\t\tstyle::color color,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth);\n\nprivate:\n\t[[nodiscard]] static std::unique_ptr<Impl> CreateByType(Type type);\n\n\tstd::unique_ptr<Impl> _impl;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/shake_animation.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/shake_animation.h\"\n\n#include \"styles/style_basic.h\"\n\nnamespace Ui {\n\nFn<void(float64)> DefaultShakeCallback(Fn<void(int)> applyShift) {\n\tconstexpr auto kShiftProgress = 6;\n\tconstexpr auto kSegmentsCount = 5;\n\treturn [=, applyShift = std::move(applyShift)](float64 value) {\n\t\tconst auto fullProgress = value * kShiftProgress;\n\t\tconst auto segment = std::clamp(\n\t\t\tint(std::floor(fullProgress)),\n\t\t\t0,\n\t\t\tkSegmentsCount);\n\t\tconst auto part = fullProgress - segment;\n\t\tconst auto from = (segment == 0)\n\t\t\t? 0.\n\t\t\t: (segment == 1 || segment == 3 || segment == 5)\n\t\t\t? 1.\n\t\t\t: -1.;\n\t\tconst auto to = (segment == 0 || segment == 2 || segment == 4)\n\t\t\t? 1.\n\t\t\t: (segment == 1 || segment == 3)\n\t\t\t? -1.\n\t\t\t: 0.;\n\t\tconst auto shift = from * (1. - part) + to * part;\n\t\tapplyShift(int(base::SafeRound(shift * st::shakeShift)));\n\t};\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/shake_animation.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nFn<void(float64)> DefaultShakeCallback(Fn<void(int)> applyShift);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/skeleton_animation.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/skeleton_animation.h\"\n\n#include \"ui/painter.h\"\n#include \"ui/widgets/labels.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kSlideDuration = crl::time(1000);\nconstexpr auto kWaitDuration = crl::time(1000);\nconstexpr auto kFullDuration = kSlideDuration + kWaitDuration;\nconstexpr auto kBaseAlpha = 0.5;\nconstexpr auto kGradientAlpha = 0.2;\n\n[[nodiscard]] QColor ColorWithAlpha(\n\t\tconst style::color &color,\n\t\tfloat64 alpha) {\n\tauto result = color->c;\n\tresult.setAlphaF(result.alphaF() * alpha);\n\treturn result;\n}\n\n} // namespace\n\nSkeletonAnimation::SkeletonAnimation(not_null<FlatLabel*> label)\n: _label(label)\n, _animation([=] { _label->update(); }) {\n\tbase::install_event_filter(_label, _label, [=](not_null<QEvent*> e) {\n\t\treturn (_active && e->type() == QEvent::Paint)\n\t\t\t? paint()\n\t\t\t: base::EventFilterResult::Continue;\n\t});\n\t_label->sizeValue(\n\t) | rpl::on_next([=](QSize) {\n\t\tif (_active) {\n\t\t\trecompute();\n\t\t}\n\t}, _lifetime);\n}\n\nvoid SkeletonAnimation::start() {\n\t_active = true;\n\trecompute();\n\t_animation.start();\n\t_label->update();\n}\n\nvoid SkeletonAnimation::stop() {\n\t_active = false;\n\t_animation.stop();\n\t_label->update();\n}\n\nbool SkeletonAnimation::active() const {\n\treturn _active;\n}\n\nvoid SkeletonAnimation::recompute() {\n\t_lineWidths = _label->countLineWidths();\n}\n\nbase::EventFilterResult SkeletonAnimation::paint() {\n\tif (_lineWidths.empty()) {\n\t\treturn base::EventFilterResult::Cancel;\n\t}\n\tauto p = QPainter(_label);\n\tPainterHighQualityEnabler hq(p);\n\n\tconst auto &st = _label->st();\n\tconst auto lineHeight = st.style.lineHeight\n\t\t? st.style.lineHeight\n\t\t: st.style.font->height;\n\tconst auto thickness = st.style.font->height / 2;\n\tconst auto radius = thickness / 2.;\n\tconst auto left = st.margin.left();\n\tconst auto textWidth = _label->width()\n\t\t- st.margin.left()\n\t\t- st.margin.right();\n\tconst auto baseColor = ColorWithAlpha(\n\t\tst::windowSubTextFg,\n\t\tkBaseAlpha);\n\n\tconst auto now = crl::now();\n\tconst auto period = now % kFullDuration;\n\tconst auto gradientActive = (period < kSlideDuration);\n\n\tp.setPen(Qt::NoPen);\n\tif (gradientActive) {\n\t\tconst auto progress = period\n\t\t\t/ float64(kSlideDuration);\n\t\tconst auto gradientWidth = textWidth;\n\t\tconst auto gradientStart = anim::interpolate(\n\t\t\tleft - gradientWidth,\n\t\t\tleft + textWidth,\n\t\t\tprogress);\n\t\tauto gradient = QLinearGradient(\n\t\t\tgradientStart,\n\t\t\t0,\n\t\t\tgradientStart + gradientWidth,\n\t\t\t0);\n\t\tconst auto centerColor = ColorWithAlpha(\n\t\t\tst::windowSubTextFg,\n\t\t\tkGradientAlpha);\n\t\tgradient.setStops({\n\t\t\t{ 0., baseColor },\n\t\t\t{ 0.5, centerColor },\n\t\t\t{ 1., baseColor },\n\t\t});\n\t\tp.setBrush(gradient);\n\t} else {\n\t\tp.setBrush(baseColor);\n\t}\n\tauto lineTop = st.margin.top();\n\tfor (const auto lineWidth : _lineWidths) {\n\t\tif (lineWidth <= 0) {\n\t\t\tlineTop += lineHeight;\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto rect = QRectF(\n\t\t\tleft,\n\t\t\tlineTop + (lineHeight - thickness) / 2.,\n\t\t\tlineWidth,\n\t\t\tthickness);\n\t\tp.drawRoundedRect(rect, radius, radius);\n\t\tlineTop += lineHeight;\n\t}\n\treturn base::EventFilterResult::Cancel;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/skeleton_animation.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/event_filter.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace Ui {\n\nclass FlatLabel;\n\nclass SkeletonAnimation final {\npublic:\n\texplicit SkeletonAnimation(not_null<FlatLabel*> label);\n\n\tvoid start();\n\tvoid stop();\n\t[[nodiscard]] bool active() const;\n\nprivate:\n\tvoid recompute();\n\tbase::EventFilterResult paint();\n\n\tconst not_null<FlatLabel*> _label;\n\tUi::Animations::Basic _animation;\n\tstd::vector<int> _lineWidths;\n\tbool _active = false;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/snowflakes.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/snowflakes.h\"\n\n#include \"base/random.h\"\n#include \"ui/effects/animation_value_f.h\"\n#include \"ui/painter.h\"\n\n#include <QtCore/QtMath>\n\nnamespace Ui {\nnamespace {\n\n[[nodiscard]] QImage PrepareSnowflake(QBrush brush) {\n\tconstexpr auto kPenWidth = 1.;\n\tconstexpr auto kTailCount = 6;\n\tconstexpr auto kAngle = (-M_PI / 2.);\n\tconstexpr auto kTailSize = 6.;\n\tconstexpr auto kSubtailPositionRatio = 2 / 3.;\n\tconstexpr auto kSubtailSize = kTailSize / 3;\n\tconstexpr auto kSubtailAngle1 = -M_PI / 6.;\n\tconstexpr auto kSubtailAngle2 = -M_PI - kSubtailAngle1;\n\tconstexpr auto kSpriteSize = (kTailSize + kPenWidth / 2.) * 2;\n\n\tconst auto x = float64(style::ConvertScaleExact(kSpriteSize / 2.));\n\tconst auto y = float64(style::ConvertScaleExact(kSpriteSize / 2.));\n\tconst auto tailSize = style::ConvertScaleExact(kTailSize);\n\tconst auto subtailSize = style::ConvertScaleExact(kSubtailSize);\n\tconst auto endTail = QPointF(\n\t\tstd::cos(kAngle) * tailSize,\n\t\tstd::sin(kAngle) * tailSize);\n\tconst auto startSubtail = endTail * kSubtailPositionRatio;\n\tconst auto endSubtail1 = startSubtail + QPointF(\n\t\tsubtailSize * std::cos(kSubtailAngle1),\n\t\tsubtailSize * std::sin(kSubtailAngle1));\n\tconst auto endSubtail2 = startSubtail + QPointF(\n\t\tsubtailSize * std::cos(kSubtailAngle2),\n\t\tsubtailSize * std::sin(kSubtailAngle2));\n\n\tconst auto pen = QPen(\n\t\tstd::move(brush),\n\t\tstyle::ConvertScaleExact(kPenWidth),\n\t\tQt::SolidLine,\n\t\tQt::RoundCap,\n\t\tQt::RoundJoin);\n\n\tconst auto s = style::ConvertScaleExact(kSpriteSize)\n\t\t* style::DevicePixelRatio();\n\tauto result = QImage(QSize(s, s), QImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&result);\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.setPen(pen);\n\t\tp.setBrush(Qt::NoBrush);\n\t\tp.translate(x, y);\n\t\tconst auto step = 360. / kTailCount;\n\t\tfor (auto i = 0; i < kTailCount; i++) {\n\t\t\tp.rotate(step);\n\t\t\tp.drawLine(QPointF(), endTail);\n\t\t\tp.drawLine(startSubtail, endSubtail1);\n\t\t\tp.drawLine(startSubtail, endSubtail2);\n\t\t}\n\t}\n\treturn result;\n}\n\n} // namespace\n\nSnowflakes::Snowflakes(Fn<void(const QRect &r)> updateCallback)\n: _lifeLength({ 300 * 2, 100 * 2 })\n, _deathTime({ 2000 * 5, 100 * 5 })\n, _scale({ 60, 100 })\n, _velocity({ 20 * 7, 4 * 7 })\n, _angle({ 70, 40 })\n, _relativeX({ 0, 100 })\n, _relativeY({ -10, 70 })\n, _appearProgressTill(200. / _deathTime.from)\n, _disappearProgressAfter(_appearProgressTill)\n, _dotMargins(3., 3., 3., 3.)\n, _renderMargins(1., 1., 1., 1.)\n, _animation([=](crl::time now) {\n\tif (now > _nextBirthTime && !_paused.at) {\n\t\tcreateParticle(now);\n\t}\n\tif (_rectToUpdate.isValid()) {\n\t\tupdateCallback(base::take(_rectToUpdate));\n\t}\n}) {\n\t{\n\t\tconst auto from = _deathTime.from + _deathTime.length;\n\t\tauto r = bytes::vector(from + 1);\n\t\tbase::RandomFill(r.data(), r.size());\n\n\t\tconst auto now = crl::now();\n\t\tfor (auto i = -from; i < 0; i += randomInterval(_lifeLength, r[-i])) {\n\t\t\tcreateParticle(now + i);\n\t\t}\n\t\tupdateCallback(_rectToUpdate);\n\t}\n\tif (!anim::Disabled()) {\n\t\t_animation.start();\n\t}\n}\n\nint Snowflakes::randomInterval(\n\t\tconst Interval &interval,\n\t\tconst bytes::type &random) const {\n\treturn interval.from + (uchar(random) % interval.length);\n}\n\ncrl::time Snowflakes::timeNow() const {\n\treturn _paused.at ? _paused.at : (crl::now() - _paused.diff);\n}\n\nvoid Snowflakes::paint(QPainter &p, const QRectF &rect) {\n\tconst auto opacity = p.opacity();\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(_brush);\n\tconst auto now = timeNow();\n\tfor (const auto &particle : _particles) {\n\t\tconst auto progress = (now - particle.birthTime)\n\t\t\t/ float64(particle.deathTime - particle.birthTime);\n\t\tif (progress > 1.) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto appearProgress = std::clamp(\n\t\t\tprogress / _appearProgressTill,\n\t\t\t0.,\n\t\t\t1.);\n\t\tconst auto dissappearProgress = 1.\n\t\t\t- (std::clamp(progress - _disappearProgressAfter, 0., 1.)\n\t\t\t\t/ (1. - _disappearProgressAfter));\n\n\t\tp.setOpacity(appearProgress * dissappearProgress * opacity);\n\n\t\tconst auto startX = rect.x() + rect.width() * particle.relativeX;\n\t\tconst auto startY = rect.y() + rect.height() * particle.relativeY;\n\t\tconst auto endX = startX + particle.velocityX;\n\t\tconst auto endY = startY + particle.velocityY;\n\n\t\tconst auto x = anim::interpolateF(startX, endX, progress);\n\t\tconst auto y = anim::interpolateF(startY, endY, progress);\n\n\t\tif (particle.type == Type::Dot) {\n\t\t\tconst auto renderRect = QRectF(x, y, 0., 0.)\n\t\t\t\t+ _dotMargins * particle.scale;\n\t\t\tp.drawEllipse(renderRect);\n\t\t\t_rectToUpdate |= renderRect.toRect() + _renderMargins;\n\t\t} else if (particle.type == Type::Snowflake) {\n\t\t\tconst auto s = _sprite.size() / style::DevicePixelRatio();\n\t\t\tconst auto h = s.height() / 2.;\n\t\t\tconst auto pos = QPointF(x - h, y - h);\n\t\t\tp.drawImage(pos, _sprite);\n\t\t\t_rectToUpdate |= QRectF(pos, s).toRect() + _renderMargins;\n\t\t}\n\t}\n\tp.setOpacity(opacity);\n}\n\nvoid Snowflakes::setPaused(bool paused) {\n\tpaused |= anim::Disabled();\n\tif (paused) {\n\t\t_paused.diff = 0;\n\t\t_paused.at = crl::now();\n\t} else {\n\t\t_paused.diff = _paused.at ? (crl::now() - _paused.at) : 0;\n\t\t_paused.at = 0;\n\t}\n}\n\nvoid Snowflakes::setBrush(QBrush brush) {\n\t_brush = std::move(brush);\n\t_sprite = PrepareSnowflake(_brush);\n}\n\nvoid Snowflakes::createParticle(crl::time now) {\n\tconstexpr auto kRandomSize = 9;\n\tauto random = bytes::vector(kRandomSize);\n\tbase::RandomFill(random.data(), random.size());\n\n\tauto i = 0;\n\tauto next = [&] { return random[i++]; };\n\n\t_nextBirthTime = now + randomInterval(_lifeLength, next());\n\n\tconst auto angle = randomInterval(_angle, next());\n\tconst auto velocity = randomInterval(_velocity, next());\n\tauto particle = Particle{\n\t\t.birthTime = now,\n\t\t.deathTime = now + randomInterval(_deathTime, next()),\n\t\t.scale = float64(randomInterval(_scale, next())) / 100.,\n\t\t.relativeX = float64(randomInterval(_relativeX, next())) / 100.,\n\t\t.relativeY = float64(randomInterval(_relativeY, next())) / 100.,\n\t\t.velocityX = std::cos(M_PI / 180. * angle) * velocity,\n\t\t.velocityY = std::sin(M_PI / 180. * angle) * velocity,\n\t\t.type = ((uchar(next()) % 2) == 1 ? Type::Snowflake : Type::Dot),\n\t};\n\tfor (auto i = 0; i < _particles.size(); i++) {\n\t\tif (particle.birthTime > _particles[i].deathTime) {\n\t\t\t_particles[i] = particle;\n\t\t\treturn;\n\t\t}\n\t}\n\t_particles.push_back(particle);\n}\n\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/snowflakes.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n\nnamespace Ui {\n\nclass Snowflakes final {\npublic:\n\tSnowflakes(Fn<void(const QRect &r)> updateCallback);\n\n\tvoid paint(QPainter &p, const QRectF &rect);\n\tvoid setPaused(bool paused);\n\tvoid setBrush(QBrush brush);\n\nprivate:\n\tenum class Type {\n\t\tDot,\n\t\tSnowflake,\n\t};\n\n\tstruct Interval {\n\t\tint from = 0;\n\t\tint length = 0;\n\t};\n\n\tstruct Particle {\n\t\tcrl::time birthTime = 0;\n\t\tcrl::time deathTime = 0;\n\t\tfloat64 scale = 0.;\n\t\tfloat64 alpha = 0.;\n\t\tfloat64 relativeX = 0.; // Relative to a width.\n\t\tfloat64 relativeY = 0.; // Relative to a height.\n\t\tfloat64 velocityX = 0.;\n\t\tfloat64 velocityY = 0.;\n\t\tType type;\n\t};\n\n\tvoid createParticle(crl::time now);\n\t[[nodiscard]] crl::time timeNow() const;\n\t[[nodiscard]] int randomInterval(\n\t\tconst Interval &interval,\n\t\tconst gsl::byte &random) const;\n\n\tconst Interval _lifeLength;\n\tconst Interval _deathTime;\n\tconst Interval _scale;\n\tconst Interval _velocity;\n\tconst Interval _angle;\n\tconst Interval _relativeX;\n\tconst Interval _relativeY;\n\n\tconst float64 _appearProgressTill;\n\tconst float64 _disappearProgressAfter;\n\tconst QMarginsF _dotMargins;\n\tconst QMargins _renderMargins;\n\n\tUi::Animations::Basic _animation;\n\tQImage _sprite;\n\n\tstd::vector<Particle> _particles;\n\n\tcrl::time _nextBirthTime = 0;\n\tstruct {\n\t\tcrl::time diff = 0;\n\t\tcrl::time at = 0;\n\t} _paused;\n\tQBrush _brush;\n\n\tQRect _rectToUpdate;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/thanos_effect.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/thanos_effect.h\"\n\n#include \"ui/effects/thanos_effect_renderer.h\"\n#include \"ui/gl/gl_surface.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/ui_utility.h\"\n#include \"base/debug_log.h\"\n#include \"base/platform/base_platform_info.h\"\n\n#include <QTimer>\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n#include <rhi/qrhi.h>\n#ifdef Q_OS_UNIX\n#include <QOffscreenSurface>\n#include <QSurfaceFormat>\n#endif\n#endif\n\nnamespace Ui {\nnamespace {\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n[[nodiscard]] bool ProbeComputeSupport() {\n\t// Create a throw-away QRhi with the same backend SurfaceRhi will use\n\t// in production, ask whether GPU compute is available, then destroy.\n\t// This is real hardware/driver capability detection — no OS version\n\t// guards. On a Mac with macOS 10.13 + Metal the answer is \"no\",\n\t// which is exactly what makes the surface render uninitialized\n\t// (Y-flipped) garbage and triggers the mirror bug elsewhere.\n\tauto rhi = std::unique_ptr<QRhi>(nullptr);\n#ifdef Q_OS_MAC\n\tif (::Platform::MetalSupported()) {\n\t\tauto params = QRhiMetalInitParams();\n\t\trhi.reset(QRhi::create(QRhi::Metal, &params));\n\t}\n\tif (!rhi) {\n\t\treturn false;\n\t}\n#elif defined(Q_OS_WIN)\n\tauto params = QRhiD3D11InitParams();\n\trhi.reset(QRhi::create(QRhi::D3D11, &params));\n\tif (!rhi) {\n\t\treturn false;\n\t}\n#else\n\t// Linux/Unix: probe the OpenGL backend with an offscreen surface.\n\t// Matches what SurfaceRhi uses in production so the result is\n\t// representative of what the real widget would get. If anything\n\t// here fails (no GL context, software fallback without compute,\n\t// driver bug), treat compute as unsupported and refuse the effect\n\t// rather than show an uninitialized swap-chain.\n\tauto format = QSurfaceFormat::defaultFormat();\n\tauto offscreen = std::unique_ptr<QOffscreenSurface>(\n\t\tQRhiGles2InitParams::newFallbackSurface(format));\n\tif (!offscreen) {\n\t\tLOG((\"ThanosEffect: probe failed — no offscreen surface\"));\n\t\treturn false;\n\t}\n\tauto params = QRhiGles2InitParams();\n\tparams.format = format;\n\tparams.fallbackSurface = offscreen.get();\n\trhi.reset(QRhi::create(QRhi::OpenGLES2, &params));\n\tif (!rhi) {\n\t\tLOG((\"ThanosEffect: probe failed — no GL RHI\"));\n\t\treturn false;\n\t}\n#endif\n\tconst auto supported = rhi->isFeatureSupported(QRhi::Compute);\n\tLOG((\"ThanosEffect: probe backend=%1 device=%2 compute=%3\"\n\t\t).arg(rhi->backendName()\n\t\t).arg(rhi->driverInfo().deviceName\n\t\t).arg(supported ? \"yes\" : \"no\"));\n\t// Destroy the RHI before the offscreen surface goes out of scope.\n\trhi.reset();\n\treturn supported;\n}\n\n[[nodiscard]] bool RhiComputeSupportedCached() {\n\tstatic const auto cached = ProbeComputeSupport();\n\treturn cached;\n}\n#endif // Qt >= 6.7\n\n} // namespace\n\nbool ThanosEffect::Supported() {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\tif (PowerSaving::On(PowerSaving::kChatEffects)) {\n\t\treturn false;\n\t}\n\t// The effect is built around compute shaders. If the driver does\n\t// not expose them (e.g. Metal on older macOS), the surface would\n\t// render an uninitialized swap-chain texture, which on macOS 10.13\n\t// observably mirrors the underlying window. Refuse to create the\n\t// effect at all in that case — the controller falls back to the\n\t// regular collapse animation.\n\treturn RhiComputeSupportedCached();\n#else\n\treturn false;\n#endif\n}\n\nvoid ThanosEffect::WarmUp() {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\t// Triggers the static-local probe inside RhiComputeSupportedCached.\n\t// Called explicitly from an idle spot (e.g. after dialogs finish\n\t// loading) so the probe cost is paid before the first Thanos use.\n\t(void)RhiComputeSupportedCached();\n#endif\n}\n\nThanosEffect::ThanosEffect(not_null<QWidget*> parent)\n: _parent(parent) {\n}\n\nThanosEffect::~ThanosEffect() {\n\tstopUpdateTimer();\n}\n\nvoid ThanosEffect::ensureSurface() {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\tif (_surface) {\n\t\treturn;\n\t}\n\n\tauto renderer = std::make_unique<ThanosEffectRenderer>();\n\t_renderer = renderer.get();\n\n\t_renderer->allDone() | rpl::on_next([=] {\n\t\tcrl::on_main(_parent, [=] {\n\t\t\thideSurface();\n\t\t\t_allDone.fire({});\n\t\t});\n\t}, _lifetime);\n\n\t_surface = GL::CreateSurface(\n\t\t_parent,\n\t\tGL::ChosenRenderer{\n\t\t\t.renderer = std::move(renderer),\n\t\t\t.backend = GL::Backend::QRhi,\n\t\t});\n\n\tif (const auto w = _surface ? _surface->rpWidget() : nullptr) {\n\t\tw->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tw->setAttribute(Qt::WA_AlwaysStackOnTop);\n\t\tw->setGeometry(_parent->rect());\n\t\tw->hide();\n\t}\n#endif\n}\n\nvoid ThanosEffect::showSurface() {\n\tif (const auto w = _surface ? _surface->rpWidget() : nullptr) {\n\t\tw->setGeometry(_parent->rect());\n\t\t// Defer show until the current call stack returns to the event\n\t\t// loop, so that all items from a batch deletion are added\n\t\t// before the first render. Without this, w->show() triggers\n\t\t// an immediate platform compositing pass with only the first\n\t\t// item visible.\n\t\tUi::PostponeCall(w, [w] {\n\t\t\tw->show();\n\t\t\tw->raise();\n\t\t});\n\t\tstartUpdateTimer();\n\t}\n}\n\nvoid ThanosEffect::hideSurface() {\n\tstopUpdateTimer();\n\tif (const auto w = _surface ? _surface->rpWidget() : nullptr) {\n\t\tw->hide();\n\t}\n}\n\nvoid ThanosEffect::addItem(QImage snapshot, QRect rect) {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\tensureSurface();\n\tif (!_renderer) {\n\t\treturn;\n\t}\n\n\tconst auto wasAnimating = _renderer->hasActiveItems();\n\n\t_renderer->addItem({\n\t\t.snapshot = std::move(snapshot),\n\t\t.rect = QRectF(rect),\n\t});\n\n\tif (!wasAnimating) {\n\t\tshowSurface();\n\t}\n#endif\n}\n\nbool ThanosEffect::animating() const {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\treturn _renderer && _renderer->hasActiveItems();\n#else\n\treturn false;\n#endif\n}\n\nrpl::producer<> ThanosEffect::allDone() const {\n\treturn _allDone.events();\n}\n\nvoid ThanosEffect::setGeometry(QRect rect) {\n\tif (const auto w = _surface ? _surface->rpWidget() : nullptr) {\n\t\tif (w->isVisible()) {\n\t\t\tw->setGeometry(rect);\n\t\t}\n\t}\n}\n\nvoid ThanosEffect::raise() {\n\tif (const auto w = _surface ? _surface->rpWidget() : nullptr) {\n\t\tw->raise();\n\t}\n}\n\nvoid ThanosEffect::startUpdateTimer() {\n\tif (_updateTimer) {\n\t\treturn;\n\t}\n\tif (const auto w = _surface ? _surface->rpWidget() : nullptr) {\n\t\t_updateTimer = new QTimer(w);\n\t\t_updateTimer->setInterval(16);\n\t\tQObject::connect(_updateTimer, &QTimer::timeout, w, [w] {\n\t\t\tw->update();\n\t\t});\n\t\t_updateTimer->start();\n\t}\n}\n\nvoid ThanosEffect::stopUpdateTimer() {\n\tif (_updateTimer) {\n\t\t_updateTimer->stop();\n\t\tdelete _updateTimer;\n\t\t_updateTimer = nullptr;\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/thanos_effect.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/unique_qptr.h\"\n\n#include <QImage>\n\nnamespace Ui {\nclass RpWidget;\nclass RpWidgetWrap;\n} // namespace Ui\n\nnamespace Ui {\n\nclass ThanosEffectRenderer;\n\nclass ThanosEffect final {\npublic:\n\texplicit ThanosEffect(not_null<QWidget*> parent);\n\t~ThanosEffect();\n\n\tvoid addItem(QImage snapshot, QRect rect);\n\n\t[[nodiscard]] bool animating() const;\n\n\t[[nodiscard]] rpl::producer<> allDone() const;\n\n\tvoid setGeometry(QRect rect);\n\tvoid raise();\n\n\t[[nodiscard]] static bool Supported();\n\t// Runs the (potentially slow, 10–300ms) QRhi+offscreen-surface probe\n\t// synchronously on the main thread and caches the result. Safe to\n\t// call multiple times. If not called, `Supported()` will lazily\n\t// probe on first use — prefer warming it up during idle time so the\n\t// first Thanos-triggering click doesn't hitch.\n\tstatic void WarmUp();\n\nprivate:\n\tvoid ensureSurface();\n\tvoid showSurface();\n\tvoid hideSurface();\n\tvoid startUpdateTimer();\n\tvoid stopUpdateTimer();\n\n\tconst not_null<QWidget*> _parent;\n\n\tstd::unique_ptr<RpWidgetWrap> _surface;\n\tThanosEffectRenderer *_renderer = nullptr;\n\n\tQTimer *_updateTimer = nullptr;\n\n\trpl::event_stream<> _allDone;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/thanos_effect_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/thanos_effect_controller.h\"\n\n#include \"ui/effects/thanos_effect.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/painter.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"history/history_item.h\"\n#include \"history/view/history_view_element.h\"\n#include \"styles/style_basic.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kBaseMs = 400;\nconstexpr auto kPerPixelMs = 0.5;\nconstexpr auto kMaxMs = 1200;\n\n} // namespace\n\nThanosEffectController::ThanosEffectController(\n\tnot_null<Main::Session*> session,\n\tDelegate delegate,\n\trpl::lifetime &lifetime)\n: _session(session)\n, _delegate(std::move(delegate)) {\n\t_session->data().itemsAboutToBeDestroyed(\n\t) | rpl::on_next([=](const auto &items) {\n\t\tcaptureItemsBatch(items);\n\t}, lifetime);\n}\n\nThanosEffectController::~ThanosEffectController() = default;\n\nvoid ThanosEffectController::captureItemsBatch(\n\t\tconst std::vector<not_null<HistoryItem*>> &items) {\n\tif (!ThanosEffect::Supported()) {\n\t\treturn;\n\t}\n\t_suppressCollapseAnimation = true;\n\tconst auto guard = gsl::finally([&] {\n\t\t_suppressCollapseAnimation = false;\n\t});\n\tfor (const auto &item : items) {\n\t\tif (const auto view = _delegate.viewForItem(item)) {\n\t\t\tconst auto top = _delegate.itemTop(view);\n\t\t\tconst auto height = view->height();\n\t\t\tcaptureView(view, height, top);\n\t\t\t_preCaptured.emplace(view, PreCapturedView{\n\t\t\t\t.height = height,\n\t\t\t\t.top = top,\n\t\t\t});\n\t\t}\n\t}\n}\n\nvoid ThanosEffectController::clearPreCaptured() {\n\t_preCaptured.clear();\n}\n\nvoid ThanosEffectController::captureOnRemoval(\n\t\tnot_null<const HistoryItem*> item) {\n\tif (!ThanosEffect::Supported()) {\n\t\treturn;\n\t}\n\tconst auto view = _delegate.viewForItem(item);\n\tif (!view) {\n\t\treturn;\n\t}\n\tif (const auto it = _preCaptured.find(view);\n\t\tit != end(_preCaptured)) {\n\t\tconst auto saved = it->second;\n\t\t_preCaptured.erase(it);\n\t\tif (saved.top >= 0) {\n\t\t\tstartCollapseAnimation(saved.height, saved.top);\n\t\t}\n\t\treturn;\n\t}\n\tconst auto top = _delegate.itemTop(view);\n\tconst auto height = view->height();\n\tcaptureView(view, height, top);\n\tstartCollapseAnimation(height, top);\n}\n\nvoid ThanosEffectController::captureView(\n\t\tnot_null<const HistoryView::Element*> view,\n\t\tint viewHeight,\n\t\tint viewTop) {\n\tconst auto item = view->data();\n\tif (!item->isRegular() || item->isService()) {\n\t\treturn;\n\t}\n\tif (viewTop < 0) {\n\t\treturn;\n\t}\n\tconst auto viewWidth = _delegate.contentWidth();\n\tif (viewWidth <= 0 || viewHeight <= 0) {\n\t\treturn;\n\t}\n\tconst auto visibleTop = _delegate.visibleAreaTop();\n\tconst auto visibleBottom = _delegate.visibleAreaBottom();\n\tconst auto visibleHeight = visibleBottom - visibleTop;\n\tconst auto screenTop = viewTop - visibleTop;\n\tif (screenTop + viewHeight <= 0 || screenTop >= visibleHeight) {\n\t\treturn;\n\t}\n\tauto gapOffset = 0;\n\tfor (const auto &gap : _renderGaps) {\n\t\tif (gap.absY >= 0 && viewTop >= gap.absY) {\n\t\t\tgapOffset += gap.height;\n\t\t}\n\t}\n\tconst auto adjustedScreenTop = screenTop + gapOffset;\n\tconst auto captureTop = std::clamp(-adjustedScreenTop, 0, viewHeight);\n\tconst auto captureBottom = std::clamp(\n\t\tvisibleHeight - adjustedScreenTop,\n\t\t0,\n\t\tviewHeight);\n\tif (captureTop >= captureBottom) {\n\t\treturn;\n\t}\n\tconst auto captureHeight = captureBottom - captureTop;\n\n\tconst auto dpr = style::DevicePixelRatio();\n\tauto image = QImage(\n\t\tQSize(viewWidth, captureHeight) * dpr,\n\t\tQImage::Format_RGBA8888_Premultiplied);\n\timage.setDevicePixelRatio(dpr);\n\timage.fill(Qt::transparent);\n\t{\n\t\tPainter p(&image);\n\t\tp.translate(0, -captureTop);\n\t\tauto clip = QRect(0, captureTop, viewWidth, captureHeight);\n\t\tauto context = _delegate.preparePaintContext(clip);\n\t\tconst auto renderedTop = viewTop + gapOffset;\n\t\tcontext.translate(0, -renderedTop);\n\t\tcontext.clip = clip;\n\t\tcontext.skipSelectionCheck = true;\n\t\tcontext.outbg = view->hasOutLayout();\n\t\tview->draw(p, context);\n\t}\n\n\tconst auto topLevel = _delegate.window();\n\tif (!topLevel) {\n\t\treturn;\n\t}\n\n\tif (!_thanosEffect) {\n\t\t_thanosEffect = std::make_unique<ThanosEffect>(topLevel);\n\t}\n\t_thanosEffect->setGeometry(QRect(QPoint(), topLevel->size()));\n\t_thanosEffect->raise();\n\n\tconst auto scroll = _delegate.scrollArea();\n\tconst auto globalPos = scroll->mapTo(\n\t\ttopLevel,\n\t\tQPoint(0, adjustedScreenTop + captureTop));\n\t_thanosEffect->addItem(\n\t\tstd::move(image),\n\t\tQRect(globalPos, QSize(viewWidth, captureHeight)));\n}\n\nvoid ThanosEffectController::startCollapseAnimation(\n\t\tint height,\n\t\tint itemTop) {\n\tif (height <= 0) {\n\t\treturn;\n\t}\n\n\tconst auto scroll = _delegate.scrollArea();\n\tconst auto scrollTop = scroll->scrollTop();\n\tconst auto scrollBottom = scrollTop + scroll->height();\n\tconst auto itemBottom = itemTop + height;\n\n\tif (itemBottom >= scrollBottom) {\n\t\treturn;\n\t}\n\n\tif (_collapseAnimation.animating()) {\n\t\tfor (auto &gap : _collapseGaps) {\n\t\t\tgap.startHeight = gap.currentHeight;\n\t\t}\n\t}\n\n\tauto merged = false;\n\tfor (auto &gap : _collapseGaps) {\n\t\tif (gap.absY + gap.originalHeight == itemTop\n\t\t\t|| (gap.absY <= itemTop\n\t\t\t\t&& itemTop <= gap.absY + gap.originalHeight)) {\n\t\t\tgap.startHeight += height;\n\t\t\tgap.currentHeight += height;\n\t\t\tgap.originalHeight += height;\n\t\t\tmerged = true;\n\t\t\tbreak;\n\t\t}\n\t\tif (itemTop + height == gap.absY) {\n\t\t\tgap.absY = itemTop;\n\t\t\tgap.startHeight += height;\n\t\t\tgap.currentHeight += height;\n\t\t\tgap.originalHeight += height;\n\t\t\tmerged = true;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (!merged) {\n\t\tconst auto it = std::lower_bound(\n\t\t\t_collapseGaps.begin(),\n\t\t\t_collapseGaps.end(),\n\t\t\titemTop,\n\t\t\t[](const auto &gap, int top) { return gap.absY < top; });\n\t\t_collapseGaps.insert(it, {\n\t\t\t.absY = itemTop,\n\t\t\t.startHeight = height,\n\t\t\t.currentHeight = height,\n\t\t\t.originalHeight = height,\n\t\t});\n\t}\n\n\tsyncCollapseGapsToHost();\n\n\tconst auto aboveViewport = std::max(0, scrollTop - itemTop);\n\tconst auto scrollAdjust = std::min(height, aboveViewport);\n\tif (scrollAdjust > 0) {\n\t\t_delegate.scrollToY(scrollTop + scrollAdjust);\n\t}\n\n\tauto totalHeight = 0;\n\tfor (const auto &gap : _collapseGaps) {\n\t\ttotalHeight += gap.currentHeight;\n\t}\n\tconst auto duration = int(std::clamp(\n\t\tkBaseMs + totalHeight * kPerPixelMs,\n\t\tdouble(kBaseMs),\n\t\tdouble(kMaxMs)));\n\n\t_collapseAnimation.start(\n\t\t[=] { collapseAnimationCallback(); },\n\t\t0.,\n\t\t1.,\n\t\tduration,\n\t\tanim::easeOutCirc);\n}\n\nvoid ThanosEffectController::collapseAnimationCallback() {\n\tconst auto progress = _collapseAnimation.value(1.);\n\n\tauto totalDelta = 0;\n\tfor (auto &gap : _collapseGaps) {\n\t\tconst auto newHeight = anim::interpolate(\n\t\t\tgap.startHeight,\n\t\t\t0,\n\t\t\tprogress);\n\t\ttotalDelta += (gap.currentHeight - newHeight);\n\t\tgap.currentHeight = newHeight;\n\t}\n\n\tif (totalDelta != 0) {\n\t\tconst auto scroll = _delegate.scrollArea();\n\t\tconst auto scrollTop = scroll->scrollTop();\n\t\tsyncCollapseGapsToHost();\n\t\t_delegate.scrollToY(std::max(scrollTop - totalDelta, 0));\n\t}\n\n\tif (!_collapseAnimation.animating()) {\n\t\t_collapseGaps.clear();\n\t\t_renderGaps.clear();\n\t\t_delegate.setCollapseGaps({});\n\t\t_collapseAnimation = {};\n\t}\n}\n\nvoid ThanosEffectController::syncCollapseGapsToHost() {\n\tauto gaps = std::vector<CollapseGap>();\n\tgaps.reserve(_collapseGaps.size());\n\tauto cumulativeOriginal = 0;\n\tfor (const auto &g : _collapseGaps) {\n\t\tgaps.push_back({\n\t\t\t.absY = g.absY - cumulativeOriginal,\n\t\t\t.height = g.currentHeight,\n\t\t});\n\t\tcumulativeOriginal += g.originalHeight;\n\t}\n\t_renderGaps = gaps;\n\t_delegate.setCollapseGaps(std::move(gaps));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/thanos_effect_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n#include \"base/flat_map.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass ThanosEffect;\nclass ScrollArea;\nstruct ChatPaintContext;\n} // namespace Ui\n\nnamespace HistoryView {\nclass Element;\n} // namespace HistoryView\n\nclass HistoryItem;\n\nnamespace Ui {\n\nstruct CollapseGap {\n\tint absY = -1;\n\tint height = 0;\n\n\tfriend bool operator==(const CollapseGap&, const CollapseGap&) = default;\n};\n\nclass ThanosEffectController final {\npublic:\n\tstruct Delegate {\n\t\tFn<HistoryView::Element*(not_null<const HistoryItem*>)> viewForItem;\n\t\tFn<int(not_null<const HistoryView::Element*>)> itemTop;\n\t\tFn<int()> visibleAreaTop;\n\t\tFn<int()> visibleAreaBottom;\n\t\tFn<int()> contentWidth;\n\t\tFn<ChatPaintContext(QRect)> preparePaintContext;\n\t\tFn<QWidget*()> window;\n\t\tFn<not_null<ScrollArea*>()> scrollArea;\n\t\tFn<void(int scrollTop)> scrollToY;\n\t\tFn<void(std::vector<CollapseGap>)> setCollapseGaps;\n\t};\n\n\tThanosEffectController(\n\t\tnot_null<Main::Session*> session,\n\t\tDelegate delegate,\n\t\trpl::lifetime &lifetime);\n\t~ThanosEffectController();\n\n\tvoid captureOnRemoval(not_null<const HistoryItem*> item);\n\tvoid clearPreCaptured();\n\n\t[[nodiscard]] const std::vector<CollapseGap> &renderGaps() const {\n\t\treturn _renderGaps;\n\t}\n\nprivate:\n\tstruct PreCapturedView {\n\t\tint height = 0;\n\t\tint top = 0;\n\t};\n\n\tstruct CollapseGapState {\n\t\tint absY = -1;\n\t\tint startHeight = 0;\n\t\tint currentHeight = 0;\n\t\tint originalHeight = 0;\n\t};\n\n\tvoid captureItemsBatch(\n\t\tconst std::vector<not_null<HistoryItem*>> &items);\n\tvoid captureView(\n\t\tnot_null<const HistoryView::Element*> view,\n\t\tint viewHeight,\n\t\tint viewTop);\n\tvoid startCollapseAnimation(int height, int itemTop);\n\tvoid collapseAnimationCallback();\n\tvoid syncCollapseGapsToHost();\n\n\tconst not_null<Main::Session*> _session;\n\tconst Delegate _delegate;\n\n\tstd::unique_ptr<ThanosEffect> _thanosEffect;\n\tbase::flat_map<\n\t\tnot_null<const HistoryView::Element*>,\n\t\tPreCapturedView> _preCaptured;\n\tbool _suppressCollapseAnimation = false;\n\tstd::vector<CollapseGap> _renderGaps;\n\n\tstd::vector<CollapseGapState> _collapseGaps;\n\tAnimations::Simple _collapseAnimation;\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/thanos_effect_renderer.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/thanos_effect_renderer.h\"\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\n#include \"ui/rhi/rhi_shader.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_basic.h\"\n#include \"base/debug_log.h\"\n\n#include <rhi/qrhi.h>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kParticleStride = int(24);\nconstexpr auto kQuadVertexCount = int(6);\nconstexpr auto kQuadVertexStride = int(2 * sizeof(float));\nconstexpr auto kComputeWorkgroupSize = int(64);\nconstexpr auto kMaxPhaseDuration = 6.0f;\nconstexpr auto kPhaseSpeed = 1.65f;\nconstexpr auto kTimeStepMultiplier = 1.65f;\nconstexpr auto kAccelerationStartPhase = 1.0f;\nconstexpr auto kAccelerationRampPhase = 2.5f;\nconstexpr auto kAccelerationMaxMultiplier = 2.2f;\nconstexpr auto kDisappearStartPhase = kMaxPhaseDuration * 0.15f;\nconstexpr auto kDisappearDuration\n\t= kMaxPhaseDuration - kDisappearStartPhase;\nconstexpr auto kMaxParticleCount = uint32_t(120000);\n\nconst float kQuadVertices[kQuadVertexCount * 2] = {\n\t0.f, 0.f,\n\t1.f, 0.f,\n\t0.f, 1.f,\n\t1.f, 0.f,\n\t0.f, 1.f,\n\t1.f, 1.f,\n};\n\nstruct alignas(16) ComputeInitUniforms {\n\tuint32_t particleCountX;\n\tuint32_t particleCountY;\n\tuint32_t seed;\n\tuint32_t _pad;\n};\nstatic_assert(sizeof(ComputeInitUniforms) % 16 == 0);\n\nstruct alignas(16) ComputeUpdateUniforms {\n\tuint32_t particleCountX;\n\tuint32_t particleCountY;\n\tfloat phase;\n\tfloat timeStep;\n};\nstatic_assert(sizeof(ComputeUpdateUniforms) % 16 == 0);\n\nstruct alignas(16) RenderUniforms {\n\tfloat rect[4];\n\tfloat size[2];\n\tuint32_t particleResolution[2];\n\tfloat scale[4];\n};\nstatic_assert(sizeof(RenderUniforms) % 16 == 0);\n\n[[nodiscard]] float AnimationSpeedMultiplier(float phase) {\n\tif (phase <= kAccelerationStartPhase) {\n\t\treturn 1.0f;\n\t}\n\tconst auto t = std::clamp(\n\t\t(phase - kAccelerationStartPhase) / kAccelerationRampPhase,\n\t\t0.0f,\n\t\t1.0f);\n\tconst auto smooth = t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f);\n\treturn 1.0f\n\t\t+ ((kAccelerationMaxMultiplier - 1.0f) * smooth);\n}\n\n[[nodiscard]] float DisappearProgress(float phase) {\n\tconst auto t = std::clamp(\n\t\t(phase - kDisappearStartPhase) / kDisappearDuration,\n\t\t0.0f,\n\t\t1.0f);\n\tconst auto oneMinus = 1.0f - t;\n\treturn 1.0f - (oneMinus * oneMinus * oneMinus);\n}\n\n[[nodiscard]] QShader LoadShader(const QString &name) {\n\treturn Rhi::ShaderFromFile(u\":/shaders/\"_q + name + u\".qsb\"_q);\n}\n\n} // namespace\n\nThanosEffectRenderer::ThanosEffectRenderer() {\n\t_elapsed.start();\n}\n\nThanosEffectRenderer::~ThanosEffectRenderer() {\n\treleaseResources();\n}\n\nvoid ThanosEffectRenderer::initialize(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\tif (_initialized && _rhi == rhi) {\n\t\treturn;\n\t}\n\treleaseResources();\n\t_rhi = rhi;\n\n\tif (!rhi->isFeatureSupported(QRhi::Compute)) {\n\t\tLOG((\"ThanosEffect: Compute shaders not supported, disabled\"));\n\t\treturn;\n\t}\n\n\t_quadVertexBuffer = rhi->newBuffer(\n\t\tQRhiBuffer::Immutable,\n\t\tQRhiBuffer::VertexBuffer,\n\t\tsizeof(kQuadVertices));\n\t_quadVertexBuffer->create();\n\n\t_computeInitUniformBuffer = rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::UniformBuffer,\n\t\tsizeof(ComputeInitUniforms));\n\t_computeInitUniformBuffer->create();\n\n\t_computeUpdateUniformBuffer = rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::UniformBuffer,\n\t\tsizeof(ComputeUpdateUniforms));\n\t_computeUpdateUniformBuffer->create();\n\n\t_renderUniformBuffer = rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::UniformBuffer,\n\t\tsizeof(RenderUniforms));\n\t_renderUniformBuffer->create();\n\n\t_placeholderParticleBuffer = rhi->newBuffer(\n\t\tQRhiBuffer::Immutable,\n\t\tQRhiBuffer::VertexBuffer | QRhiBuffer::StorageBuffer,\n\t\tkParticleStride);\n\t_placeholderParticleBuffer->create();\n\n\t_placeholderTexture = rhi->newTexture(\n\t\tQRhiTexture::RGBA8,\n\t\tQSize(1, 1));\n\t_placeholderTexture->create();\n\n\t_placeholderSampler = rhi->newSampler(\n\t\tQRhiSampler::Linear,\n\t\tQRhiSampler::Linear,\n\t\tQRhiSampler::None,\n\t\tQRhiSampler::ClampToEdge,\n\t\tQRhiSampler::ClampToEdge);\n\t_placeholderSampler->create();\n\n\tcreatePipelines(rt);\n\n\tauto *rub = rhi->nextResourceUpdateBatch();\n\trub->uploadStaticBuffer(_quadVertexBuffer, kQuadVertices);\n\tcb->resourceUpdate(rub);\n\n\t_initialized = true;\n\t_lastFrameTime = double(_elapsed.elapsed()) / 1000.0;\n\n\tLOG((\"ThanosEffect: initialized, backend=%1 device=%2\")\n\t\t.arg(rhi->backendName())\n\t\t.arg(rhi->driverInfo().deviceName));\n}\n\nvoid ThanosEffectRenderer::createPipelines(QRhiRenderTarget *rt) {\n\tconst auto initShader = LoadShader(u\"thanos_init.comp\"_q);\n\tconst auto updateShader = LoadShader(u\"thanos_update.comp\"_q);\n\tconst auto vertShader = LoadShader(u\"thanos.vert\"_q);\n\tconst auto fragShader = LoadShader(u\"thanos.frag\"_q);\n\n\t_computeInitSrbLayout = _rhi->newShaderResourceBindings();\n\t_computeInitSrbLayout->setBindings({\n\t\tQRhiShaderResourceBinding::bufferLoadStore(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::ComputeStage,\n\t\t\t_placeholderParticleBuffer),\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::ComputeStage,\n\t\t\t_computeInitUniformBuffer),\n\t});\n\t_computeInitSrbLayout->create();\n\n\t_computeInitPipeline = _rhi->newComputePipeline();\n\t_computeInitPipeline->setShaderStage(\n\t\t{ QRhiShaderStage::Compute, initShader });\n\t_computeInitPipeline->setShaderResourceBindings(_computeInitSrbLayout);\n\t_computeInitPipeline->create();\n\n\t_computeUpdateSrbLayout = _rhi->newShaderResourceBindings();\n\t_computeUpdateSrbLayout->setBindings({\n\t\tQRhiShaderResourceBinding::bufferLoadStore(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::ComputeStage,\n\t\t\t_placeholderParticleBuffer),\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::ComputeStage,\n\t\t\t_computeUpdateUniformBuffer),\n\t});\n\t_computeUpdateSrbLayout->create();\n\n\t_computeUpdatePipeline = _rhi->newComputePipeline();\n\t_computeUpdatePipeline->setShaderStage(\n\t\t{ QRhiShaderStage::Compute, updateShader });\n\t_computeUpdatePipeline->setShaderResourceBindings(\n\t\t_computeUpdateSrbLayout);\n\t_computeUpdatePipeline->create();\n\n\t_renderSrbLayout = _rhi->newShaderResourceBindings();\n\t_renderSrbLayout->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage,\n\t\t\t_renderUniformBuffer),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\t_placeholderTexture,\n\t\t\t_placeholderSampler),\n\t});\n\t_renderSrbLayout->create();\n\n\t_renderPipeline = _rhi->newGraphicsPipeline();\n\t_renderPipeline->setShaderStages({\n\t\t{ QRhiShaderStage::Vertex, vertShader },\n\t\t{ QRhiShaderStage::Fragment, fragShader },\n\t});\n\n\tQRhiVertexInputLayout inputLayout;\n\tinputLayout.setBindings({\n\t\t{ quint32(kQuadVertexStride) },\n\t\t{ quint32(kParticleStride),\n\t\t\tQRhiVertexInputBinding::PerInstance },\n\t});\n\tinputLayout.setAttributes({\n\t\t{ 0, 0, QRhiVertexInputAttribute::Float2, 0 },\n\t\t{ 1, 1, QRhiVertexInputAttribute::Float2, 0 },\n\t\t{ 1, 2, QRhiVertexInputAttribute::Float, 16 },\n\t});\n\t_renderPipeline->setVertexInputLayout(inputLayout);\n\n\tQRhiGraphicsPipeline::TargetBlend blend;\n\tblend.enable = true;\n\tblend.srcColor = QRhiGraphicsPipeline::One;\n\tblend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;\n\tblend.srcAlpha = QRhiGraphicsPipeline::One;\n\tblend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;\n\t_renderPipeline->setTargetBlends({ blend });\n\n\t_renderPipeline->setTopology(QRhiGraphicsPipeline::Triangles);\n\t_renderPipeline->setShaderResourceBindings(_renderSrbLayout);\n\t_renderPipeline->setRenderPassDescriptor(\n\t\trt->renderPassDescriptor());\n\t_renderPipeline->create();\n}\n\nvoid ThanosEffectRenderer::render(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) {\n\tif (!_initialized || !rhi->isFeatureSupported(QRhi::Compute)) {\n\t\treturn;\n\t}\n\t_rhi = rhi;\n\n\tconst auto now = double(_elapsed.elapsed()) / 1000.0;\n\tconst auto dt = float(std::clamp(now - _lastFrameTime, 0.001, 0.1));\n\t_lastFrameTime = now;\n\n\taddPendingItems(cb);\n\n\tif (_items.empty()) {\n\t\treturn;\n\t}\n\n\tconst auto pixelSize = rt->pixelSize();\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto viewW = float(pixelSize.width()) / factor;\n\tconst auto viewH = float(pixelSize.height()) / factor;\n\n\t{\n\t\tauto *rub = rhi->nextResourceUpdateBatch();\n\t\tauto needsInit = false;\n\n\t\tfor (auto &item : _items) {\n\t\t\tconst auto animationTimeStep\n\t\t\t\t= dt * AnimationSpeedMultiplier(item.phase);\n\t\t\titem.phase += animationTimeStep * kPhaseSpeed;\n\t\t\tif (!item.particlesInitialized) {\n\t\t\t\tneedsInit = true;\n\t\t\t\titem.particlesInitialized = true;\n\n\t\t\t\tComputeInitUniforms uni;\n\t\t\t\tuni.particleCountX = item.particleCountX;\n\t\t\t\tuni.particleCountY = item.particleCountY;\n\t\t\t\tuni.seed = _seedCounter++;\n\t\t\t\tuni._pad = 0;\n\t\t\t\trub->updateDynamicBuffer(\n\t\t\t\t\titem.computeInitUniformBuffer,\n\t\t\t\t\t0,\n\t\t\t\t\tsizeof(uni),\n\t\t\t\t\t&uni);\n\t\t\t}\n\n\t\t\tComputeUpdateUniforms updateUni;\n\t\t\tupdateUni.particleCountX = item.particleCountX;\n\t\t\tupdateUni.particleCountY = item.particleCountY;\n\t\t\tupdateUni.phase = item.phase;\n\t\t\tupdateUni.timeStep = animationTimeStep * kTimeStepMultiplier;\n\t\t\trub->updateDynamicBuffer(\n\t\t\t\titem.computeUpdateUniformBuffer,\n\t\t\t\t0,\n\t\t\t\tsizeof(updateUni),\n\t\t\t\t&updateUni);\n\t\t}\n\n\t\tcb->beginComputePass(rub);\n\n\t\tif (needsInit) {\n\t\t\tfor (auto &item : _items) {\n\t\t\t\tif (item.phase <= dt * kPhaseSpeed * 1.1f) {\n\t\t\t\t\tcb->setComputePipeline(_computeInitPipeline);\n\t\t\t\t\tcb->setShaderResources(item.computeInitSrb);\n\t\t\t\t\tconst auto count =\n\t\t\t\t\t\titem.particleCountX * item.particleCountY;\n\t\t\t\t\tconst auto groups =\n\t\t\t\t\t\t(count + kComputeWorkgroupSize - 1)\n\t\t\t\t\t\t/ kComputeWorkgroupSize;\n\t\t\t\t\tcb->dispatch(int(groups), 1, 1);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor (auto &item : _items) {\n\t\t\tcb->setComputePipeline(_computeUpdatePipeline);\n\t\t\tcb->setShaderResources(item.computeUpdateSrb);\n\t\t\tconst auto count = item.particleCountX * item.particleCountY;\n\t\t\tconst auto groups = (count + kComputeWorkgroupSize - 1)\n\t\t\t\t/ kComputeWorkgroupSize;\n\t\t\tcb->dispatch(int(groups), 1, 1);\n\t\t}\n\n\t\tcb->endComputePass();\n\t}\n\n\t{\n\t\tauto *renderRub = rhi->nextResourceUpdateBatch();\n\t\tfor (auto &item : _items) {\n\t\t\tRenderUniforms uni;\n\t\t\tuni.rect[0] = float(item.rect.x()) / viewW;\n\t\t\tuni.rect[1] = (viewH - float(item.rect.y())\n\t\t\t\t- float(item.rect.height())) / viewH;\n\t\t\tuni.rect[2] = float(item.rect.width()) / viewW;\n\t\t\tuni.rect[3] = float(item.rect.height()) / viewH;\n\t\t\tuni.size[0] = float(item.rect.width());\n\t\t\tuni.size[1] = float(item.rect.height());\n\t\t\tuni.particleResolution[0] = item.particleCountX;\n\t\t\tuni.particleResolution[1] = item.particleCountY;\n\t\t\tconst auto disappearProgress = DisappearProgress(item.phase);\n\t\t\tconst auto inverseDisappear = 1.0f - disappearProgress;\n\t\t\tuni.scale[0] = inverseDisappear;\n\t\t\tuni.scale[1] = 0.0f;\n\t\t\tuni.scale[2] = inverseDisappear;\n\t\t\tuni.scale[3] = 0.0f;\n\n\t\t\trenderRub->updateDynamicBuffer(\n\t\t\t\titem.renderUniformBuffer,\n\t\t\t\t0,\n\t\t\t\tsizeof(uni),\n\t\t\t\t&uni);\n\t\t}\n\n\t\tconst auto bg = QColor(0, 0, 0, 0);\n\t\tcb->beginPass(rt, bg, { 1.0f, 0 }, renderRub);\n\n\t\tfor (auto &item : _items) {\n\t\t\tcb->setGraphicsPipeline(_renderPipeline);\n\t\t\tcb->setShaderResources(item.renderSrb);\n\t\t\tcb->setViewport({\n\t\t\t\t0, 0,\n\t\t\t\tfloat(pixelSize.width()),\n\t\t\t\tfloat(pixelSize.height()) });\n\n\t\t\tconst QRhiCommandBuffer::VertexInput vbufs[] = {\n\t\t\t\t{ _quadVertexBuffer, 0 },\n\t\t\t\t{ item.particleBuffer, 0 },\n\t\t\t};\n\t\t\tcb->setVertexInput(0, 2, vbufs);\n\n\t\t\tconst auto instanceCount =\n\t\t\t\titem.particleCountX * item.particleCountY;\n\t\t\tcb->draw(kQuadVertexCount, instanceCount);\n\t\t}\n\n\t\tcb->endPass();\n\t}\n\n\tauto hadItems = !_items.empty();\n\t_items.erase(\n\t\tstd::remove_if(_items.begin(), _items.end(), [&](auto &item) {\n\t\t\tif (item.phase >= kMaxPhaseDuration) {\n\t\t\t\tdestroyAnimatingItem(item);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t}),\n\t\t_items.end());\n\n\tif (hadItems && _items.empty()) {\n\t\t_allDone.fire({});\n\t}\n}\n\nvoid ThanosEffectRenderer::addItem(ThanosItem item) {\n\t_pendingItems.push_back(std::move(item));\n}\n\nbool ThanosEffectRenderer::hasActiveItems() const {\n\treturn !_items.empty() || !_pendingItems.empty();\n}\n\nrpl::producer<> ThanosEffectRenderer::allDone() const {\n\treturn _allDone.events();\n}\n\nvoid ThanosEffectRenderer::addPendingItems(QRhiCommandBuffer *cb) {\n\tif (_pendingItems.empty() || !_rhi) {\n\t\treturn;\n\t}\n\n\tauto *rub = _rhi->nextResourceUpdateBatch();\n\n\tfor (auto &pending : _pendingItems) {\n\t\tauto animating = createAnimatingItem(std::move(pending));\n\t\tif (animating.texture) {\n\t\t\tauto image = animating.uploadImage;\n\t\t\tif (!image.isNull()) {\n\t\t\t\trub->uploadTexture(\n\t\t\t\t\tanimating.texture,\n\t\t\t\t\tQRhiTextureUploadDescription(\n\t\t\t\t\t\tQRhiTextureUploadEntry(\n\t\t\t\t\t\t\t0, 0,\n\t\t\t\t\t\t\tQRhiTextureSubresourceUploadDescription(\n\t\t\t\t\t\t\t\timage))));\n\t\t\t}\n\t\t\tanimating.uploadImage = QImage();\n\t\t\t_items.push_back(std::move(animating));\n\t\t}\n\t}\n\t_pendingItems.clear();\n\n\tcb->resourceUpdate(rub);\n}\n\nThanosEffectRenderer::AnimatingItem ThanosEffectRenderer::createAnimatingItem(\n\t\tThanosItem &&item) {\n\tAnimatingItem result;\n\tresult.rect = item.rect;\n\n\tconst auto w = int(item.rect.width());\n\tconst auto h = int(item.rect.height());\n\tif (w <= 0 || h <= 0 || item.snapshot.isNull()) {\n\t\treturn result;\n\t}\n\n\tconst auto totalPixels = uint32_t(w) * uint32_t(h);\n\tif (totalPixels <= kMaxParticleCount) {\n\t\tresult.particleCountX = uint32_t(w);\n\t\tresult.particleCountY = uint32_t(h);\n\t} else {\n\t\tconst auto aspectRatio = float(w) / float(h);\n\t\tresult.particleCountY = uint32_t(\n\t\t\tstd::sqrt(float(kMaxParticleCount) / aspectRatio));\n\t\tresult.particleCountX = uint32_t(\n\t\t\tfloat(kMaxParticleCount) / float(result.particleCountY));\n\t\tif (result.particleCountX < 1) result.particleCountX = 1;\n\t\tif (result.particleCountY < 1) result.particleCountY = 1;\n\t}\n\tconst auto particleCount =\n\t\tresult.particleCountX * result.particleCountY;\n\n\tauto *tex = _rhi->newTexture(\n\t\tQRhiTexture::RGBA8,\n\t\tQSize(item.snapshot.width(), item.snapshot.height()));\n\ttex->create();\n\tresult.texture = tex;\n\n\tresult.uploadImage = item.snapshot.convertToFormat(\n\t\tQImage::Format_RGBA8888_Premultiplied);\n\n\tauto *sampler = _rhi->newSampler(\n\t\tQRhiSampler::Linear,\n\t\tQRhiSampler::Linear,\n\t\tQRhiSampler::None,\n\t\tQRhiSampler::ClampToEdge,\n\t\tQRhiSampler::ClampToEdge);\n\tsampler->create();\n\tresult.sampler = sampler;\n\n\tauto *particleBuf = _rhi->newBuffer(\n\t\tQRhiBuffer::Static,\n\t\tQRhiBuffer::VertexBuffer | QRhiBuffer::StorageBuffer,\n\t\tparticleCount * kParticleStride);\n\tparticleBuf->create();\n\tresult.particleBuffer = particleBuf;\n\n\tauto *initUbo = _rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::UniformBuffer,\n\t\tsizeof(ComputeInitUniforms));\n\tinitUbo->create();\n\tresult.computeInitUniformBuffer = initUbo;\n\n\tauto *updateUbo = _rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::UniformBuffer,\n\t\tsizeof(ComputeUpdateUniforms));\n\tupdateUbo->create();\n\tresult.computeUpdateUniformBuffer = updateUbo;\n\n\tresult.computeInitSrb = _rhi->newShaderResourceBindings();\n\tresult.computeInitSrb->setBindings({\n\t\tQRhiShaderResourceBinding::bufferLoadStore(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::ComputeStage,\n\t\t\tparticleBuf),\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::ComputeStage,\n\t\t\tinitUbo),\n\t});\n\tresult.computeInitSrb->create();\n\n\tresult.computeUpdateSrb = _rhi->newShaderResourceBindings();\n\tresult.computeUpdateSrb->setBindings({\n\t\tQRhiShaderResourceBinding::bufferLoadStore(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::ComputeStage,\n\t\t\tparticleBuf),\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::ComputeStage,\n\t\t\tupdateUbo),\n\t});\n\tresult.computeUpdateSrb->create();\n\n\tauto *renderUbo = _rhi->newBuffer(\n\t\tQRhiBuffer::Dynamic,\n\t\tQRhiBuffer::UniformBuffer,\n\t\tsizeof(RenderUniforms));\n\trenderUbo->create();\n\tresult.renderUniformBuffer = renderUbo;\n\n\tresult.renderSrb = _rhi->newShaderResourceBindings();\n\tresult.renderSrb->setBindings({\n\t\tQRhiShaderResourceBinding::uniformBuffer(\n\t\t\t0,\n\t\t\tQRhiShaderResourceBinding::VertexStage,\n\t\t\trenderUbo),\n\t\tQRhiShaderResourceBinding::sampledTexture(\n\t\t\t1,\n\t\t\tQRhiShaderResourceBinding::FragmentStage,\n\t\t\ttex,\n\t\t\tsampler),\n\t});\n\tresult.renderSrb->create();\n\n\treturn result;\n}\n\nvoid ThanosEffectRenderer::destroyAnimatingItem(AnimatingItem &item) {\n\tdelete item.renderSrb;\n\tdelete item.computeUpdateSrb;\n\tdelete item.computeInitSrb;\n\tdelete item.renderUniformBuffer;\n\tdelete item.computeUpdateUniformBuffer;\n\tdelete item.computeInitUniformBuffer;\n\tdelete item.particleBuffer;\n\tdelete item.sampler;\n\tdelete item.texture;\n\titem = {};\n}\n\nvoid ThanosEffectRenderer::releaseResources() {\n\tfor (auto &item : _items) {\n\t\tdestroyAnimatingItem(item);\n\t}\n\t_items.clear();\n\n\tdelete _renderPipeline;\n\t_renderPipeline = nullptr;\n\tdelete _renderSrbLayout;\n\t_renderSrbLayout = nullptr;\n\tdelete _computeUpdatePipeline;\n\t_computeUpdatePipeline = nullptr;\n\tdelete _computeUpdateSrbLayout;\n\t_computeUpdateSrbLayout = nullptr;\n\tdelete _computeInitPipeline;\n\t_computeInitPipeline = nullptr;\n\tdelete _computeInitSrbLayout;\n\t_computeInitSrbLayout = nullptr;\n\n\tdelete _placeholderSampler;\n\t_placeholderSampler = nullptr;\n\tdelete _placeholderTexture;\n\t_placeholderTexture = nullptr;\n\tdelete _placeholderParticleBuffer;\n\t_placeholderParticleBuffer = nullptr;\n\n\tdelete _renderUniformBuffer;\n\t_renderUniformBuffer = nullptr;\n\tdelete _computeUpdateUniformBuffer;\n\t_computeUpdateUniformBuffer = nullptr;\n\tdelete _computeInitUniformBuffer;\n\t_computeInitUniformBuffer = nullptr;\n\tdelete _quadVertexBuffer;\n\t_quadVertexBuffer = nullptr;\n\n\t_initialized = false;\n}\n\n} // namespace Ui\n\n#endif // Qt >= 6.7\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/thanos_effect_renderer.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rhi/rhi_renderer.h\"\n#include \"ui/gl/gl_surface.h\"\n\n#include <QElapsedTimer>\n#include <QImage>\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n\nclass QRhi;\nclass QRhiBuffer;\nclass QRhiTexture;\nclass QRhiSampler;\nclass QRhiGraphicsPipeline;\nclass QRhiComputePipeline;\nclass QRhiShaderResourceBindings;\nclass QRhiRenderTarget;\nclass QRhiCommandBuffer;\n\nnamespace Ui {\n\nstruct ThanosItem {\n\tQImage snapshot;\n\tQRectF rect;\n};\n\nclass ThanosEffectRenderer final\n\t: public GL::Renderer\n\t, public Rhi::Renderer {\npublic:\n\tThanosEffectRenderer();\n\t~ThanosEffectRenderer();\n\n\tvoid initialize(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) override;\n\tvoid render(\n\t\tQRhi *rhi,\n\t\tQRhiRenderTarget *rt,\n\t\tQRhiCommandBuffer *cb) override;\n\tvoid releaseResources() override;\n\n\tQColor rhiClearColor() override {\n\t\treturn QColor(0, 0, 0, 0);\n\t}\n\n\tstd::optional<QColor> clearColor() override {\n\t\treturn QColor(0, 0, 0, 0);\n\t}\n\n\tvoid addItem(ThanosItem item);\n\t[[nodiscard]] bool hasActiveItems() const;\n\n\trpl::producer<> allDone() const;\n\nprivate:\n\tstruct AnimatingItem {\n\t\tQRhiTexture *texture = nullptr;\n\t\tQRhiSampler *sampler = nullptr;\n\t\tQRhiBuffer *particleBuffer = nullptr;\n\t\tQRhiBuffer *computeInitUniformBuffer = nullptr;\n\t\tQRhiBuffer *computeUpdateUniformBuffer = nullptr;\n\t\tQRhiBuffer *renderUniformBuffer = nullptr;\n\t\tQRhiShaderResourceBindings *computeInitSrb = nullptr;\n\t\tQRhiShaderResourceBindings *computeUpdateSrb = nullptr;\n\t\tQRhiShaderResourceBindings *renderSrb = nullptr;\n\t\tQImage uploadImage;\n\t\tQRectF rect;\n\t\tuint32_t particleCountX = 0;\n\t\tuint32_t particleCountY = 0;\n\t\tfloat phase = 0.f;\n\t\tbool particlesInitialized = false;\n\t};\n\n\tvoid createPipelines(QRhiRenderTarget *rt);\n\tvoid addPendingItems(QRhiCommandBuffer *cb);\n\tAnimatingItem createAnimatingItem(ThanosItem &&item);\n\tvoid destroyAnimatingItem(AnimatingItem &item);\n\n\tQRhi *_rhi = nullptr;\n\n\tQRhiBuffer *_quadVertexBuffer = nullptr;\n\tQRhiBuffer *_computeInitUniformBuffer = nullptr;\n\tQRhiBuffer *_computeUpdateUniformBuffer = nullptr;\n\tQRhiBuffer *_renderUniformBuffer = nullptr;\n\n\tQRhiBuffer *_placeholderParticleBuffer = nullptr;\n\tQRhiTexture *_placeholderTexture = nullptr;\n\tQRhiSampler *_placeholderSampler = nullptr;\n\n\tQRhiShaderResourceBindings *_computeInitSrbLayout = nullptr;\n\tQRhiShaderResourceBindings *_computeUpdateSrbLayout = nullptr;\n\tQRhiShaderResourceBindings *_renderSrbLayout = nullptr;\n\n\tQRhiComputePipeline *_computeInitPipeline = nullptr;\n\tQRhiComputePipeline *_computeUpdatePipeline = nullptr;\n\tQRhiGraphicsPipeline *_renderPipeline = nullptr;\n\n\tstd::vector<AnimatingItem> _items;\n\tstd::vector<ThanosItem> _pendingItems;\n\n\tQElapsedTimer _elapsed;\n\tdouble _lastFrameTime = 0.;\n\tbool _initialized = false;\n\tuint32_t _seedCounter = 0;\n\n\trpl::event_stream<> _allDone;\n\n};\n\n} // namespace Ui\n\n#endif // Qt >= 6.7\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/thanos_effect_session.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/thanos_effect_session.h\"\n\n#include \"ui/effects/thanos_effect.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n\nnamespace Ui {\n\nvoid ScheduleThanosEffectWarmUp(\n\t\tnot_null<Main::Session*> session,\n\t\trpl::lifetime &lifetime) {\n\tif (session->data().chatsListLoaded(nullptr)) {\n\t\tThanosEffect::WarmUp();\n\t\treturn;\n\t}\n\tsession->data().chatsListLoadedEvents(\n\t) | rpl::filter([](Data::Folder *folder) {\n\t\treturn !folder;\n\t}) | rpl::take(1) | rpl::on_next([] {\n\t\tThanosEffect::WarmUp();\n\t}, lifetime);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/thanos_effect_session.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\n\n// Schedules `ThanosEffect::WarmUp()` to run once the main dialog list\n// has finished its initial load (or immediately if already loaded).\n// Kept in a separate file so the Ui-only `ThanosEffect` does not need\n// to depend on `Main::Session` / `Data::Session`.\nvoid ScheduleThanosEffectWarmUp(\n\tnot_null<Main::Session*> session,\n\trpl::lifetime &lifetime);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/toggle_arrow.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/toggle_arrow.h\"\n\n#include \"ui/rp_widget.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"styles/style_statistics.h\"\n#include \"styles/style_window.h\"\n\n#include <QtCore/QtMath>\n\nnamespace Ui {\n\n[[nodiscard]] QPainterPath ToggleUpDownArrowPath(\n\t\tfloat64 x,\n\t\tfloat64 y,\n\t\tfloat64 size,\n\t\tfloat64 fourStrokes,\n\t\tfloat64 progress) {\n\tconst auto size2 = size / 2.;\n\tconst auto stroke = (fourStrokes / 4.) / M_SQRT2;\n\tconst auto left = x - size;\n\tconst auto right = x + size;\n\tconst auto bottom = y + size2;\n\tconstexpr auto kPointCount = 6;\n\tauto points = std::array<QPointF, kPointCount>{ {\n\t\t{ left - stroke, bottom - stroke },\n\t\t{ x, bottom - stroke - size - stroke },\n\t\t{ right + stroke, bottom - stroke },\n\t\t{ right - stroke, bottom + stroke },\n\t\t{ x, bottom + stroke - size + stroke },\n\t\t{ left + stroke, bottom + stroke }\n\t} };\n\tconst auto alpha = (progress - 1.) * M_PI;\n\tconst auto cosalpha = cos(alpha);\n\tconst auto sinalpha = sin(alpha);\n\tfor (auto &point : points) {\n\t\tauto px = point.x() - x;\n\t\tauto py = point.y() - y;\n\t\tpoint.setX(x + px * cosalpha - py * sinalpha);\n\t\tpoint.setY(y + py * cosalpha + px * sinalpha);\n\t}\n\tauto path = QPainterPath();\n\tpath.moveTo(points.front());\n\tfor (int i = 1; i != kPointCount; ++i) {\n\t\tpath.lineTo(points[i]);\n\t}\n\tpath.lineTo(points.front());\n\n\treturn path;\n}\n\nvoid AddToggleUpDownArrowToMoreButton(not_null<Ui::RpWidget*> parent) {\n\tconst auto arrow = Ui::CreateChild<Ui::RpWidget>(parent.get());\n\tarrow->paintRequest() | rpl::on_next([=](const QRect &r) {\n\t\tauto p = QPainter(arrow);\n\n\t\tconst auto path = ToggleUpDownArrowPath(\n\t\t\tst::statisticsShowMoreButtonArrowSize,\n\t\t\tst::statisticsShowMoreButtonArrowSize,\n\t\t\tst::statisticsShowMoreButtonArrowSize,\n\t\t\tst::mainMenuToggleFourStrokes,\n\t\t\t0.);\n\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.fillPath(path, st::lightButtonFg);\n\t}, arrow->lifetime());\n\tarrow->resize(Size(st::statisticsShowMoreButtonArrowSize * 2));\n\tarrow->move(st::statisticsShowMoreButtonArrowPosition);\n\tarrow->show();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/toggle_arrow.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nclass RpWidget;\n\n[[nodiscard]] QPainterPath ToggleUpDownArrowPath(\n\tfloat64 x,\n\tfloat64 y,\n\tfloat64 size,\n\tfloat64 fourStrokes,\n\tfloat64 progress);\n\nvoid AddToggleUpDownArrowToMoreButton(not_null<Ui::RpWidget*> parent);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/ttl_icon.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/ttl_icon.h\"\n\n#include \"ui/arc_angles.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Ui {\n\nvoid PaintTimerIcon(\n\t\tQPainter &p,\n\t\tconst QRect &innerRect,\n\t\tconst QString &text,\n\t\tconst QColor &color) {\n\tp.setFont(st::dialogsScamFont);\n\tp.setPen(color);\n\tp.drawText(\n\t\tinnerRect,\n\t\t(text.size() > 2) ? text.mid(0, 2) : text,\n\t\tstyle::al_center);\n\n\tconstexpr auto kPenWidth = 1.5;\n\tconst auto penWidth = style::ConvertScaleExact(kPenWidth);\n\tauto pen = QPen(color);\n\tpen.setJoinStyle(Qt::RoundJoin);\n\tpen.setCapStyle(Qt::RoundCap);\n\tpen.setWidthF(penWidth);\n\n\tp.setPen(pen);\n\tp.setBrush(Qt::NoBrush);\n\tp.drawArc(innerRect, arc::kQuarterLength, arc::kHalfLength);\n\n\tp.setClipRect(innerRect\n\t\t- QMargins(innerRect.width() / 2, -penWidth, -penWidth, -penWidth));\n\tpen.setStyle(Qt::DotLine);\n\tp.setPen(pen);\n\tp.drawEllipse(innerRect);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/ttl_icon.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nvoid PaintTimerIcon(\n\tQPainter &p,\n\tconst QRect &innerRect,\n\tconst QString &text,\n\tconst QColor &color);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/upload_progress_overlay.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/effects/upload_progress_overlay.h\"\n\n#include \"base/call_delayed.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_basic.h\"\n\nnamespace Ui {\n\nUploadProgressOverlay::UploadProgressOverlay(\n\tnot_null<QWidget*> parent,\n\tFn<void()> update)\n: _parent(parent)\n, _update(std::move(update))\n, _radial([=](crl::time now) { radialAnimationCallback(now); }) {\n}\n\nvoid UploadProgressOverlay::start() {\n\t_uploading = true;\n\t_stopping = false;\n\t_startedAt = crl::now();\n\t_progress = 0.;\n\t_hiding.stop();\n\t_hidingDone = nullptr;\n\t_radial.start(_progress);\n\t_update();\n}\n\nvoid UploadProgressOverlay::stop(Fn<void()> done) {\n\tif (!_uploading) {\n\t\tif (done) {\n\t\t\tdone();\n\t\t}\n\t\treturn;\n\t}\n\t_hidingDone = std::move(done);\n\t_stopping = true;\n\t_progress = 1.;\n\t_update();\n\n\tconst auto catchUp = st::radialDuration * 2;\n\tbase::call_delayed(catchUp, _parent, [=] {\n\t\tif (!_stopping) {\n\t\t\treturn;\n\t\t}\n\t\tproceedWithStop();\n\t});\n}\n\nvoid UploadProgressOverlay::fail(Fn<void()> done) {\n\tif (!_uploading) {\n\t\tif (done) {\n\t\t\tdone();\n\t\t}\n\t\treturn;\n\t}\n\t_hidingDone = std::move(done);\n\t_stopping = true;\n\t_progress = 0.;\n\t_update();\n\n\tconst auto catchUp = st::radialDuration * 2;\n\tbase::call_delayed(catchUp, _parent, [=] {\n\t\tif (!_stopping) {\n\t\t\treturn;\n\t\t}\n\t\tproceedWithStop();\n\t});\n}\n\nvoid UploadProgressOverlay::proceedWithStop() {\n\t_stopping = false;\n\t_uploading = false;\n\n\tconst auto elapsed = crl::now() - _startedAt;\n\tconst auto minDuration = st::radialDuration * 3;\n\tconst auto delay = std::max(minDuration - elapsed, crl::time(0));\n\tbase::call_delayed(delay, _parent, [=] {\n\t\tif (_uploading) {\n\t\t\treturn;\n\t\t}\n\t\t_hiding.start(\n\t\t\t_update,\n\t\t\t1.,\n\t\t\t0.,\n\t\t\tst::radialDuration);\n\t\t_hiding.setFinishedCallback([=] {\n\t\t\t_radial.stop();\n\t\t\tconst auto callback = base::take(_hidingDone);\n\t\t\t_update();\n\t\t\tif (callback) {\n\t\t\t\tcallback();\n\t\t\t}\n\t\t});\n\t});\n}\n\nvoid UploadProgressOverlay::finish(Fn<void()> done) {\n\t_uploading = false;\n\t_update();\n\tif (done) {\n\t\tdone();\n\t}\n}\n\nvoid UploadProgressOverlay::setProgress(float64 progress) {\n\t_progress = progress;\n\t_update();\n}\n\nvoid UploadProgressOverlay::setOver(bool over) {\n\t_over = over;\n\tif (!_uploading) {\n\t\treturn;\n\t}\n\t_cancelShown.start(\n\t\t_update,\n\t\tover ? 0. : 1.,\n\t\tover ? 1. : 0.,\n\t\tst::universalDuration);\n}\n\nbool UploadProgressOverlay::shown() const {\n\treturn _uploading\n\t\t|| _radial.animating()\n\t\t|| _hiding.animating()\n\t\t|| _hidingDone;\n}\n\nbool UploadProgressOverlay::uploading() const {\n\treturn _uploading && !_stopping;\n}\n\nvoid UploadProgressOverlay::paint(\n\t\tQPainter &p,\n\t\tQRect rect,\n\t\tconst PaintArgs &args) {\n\tconst auto hideOpacity = _hiding.animating()\n\t\t? _hiding.value(0.)\n\t\t: (_uploading || _hidingDone) ? 1. : 0.;\n\tif (hideOpacity <= 0.) {\n\t\treturn;\n\t}\n\n\tauto o = p.opacity();\n\tif (hideOpacity < 1.) {\n\t\tp.setOpacity(o * hideOpacity);\n\t}\n\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(args.overlayFg);\n\tif (args.roundRadius > 0.) {\n\t\tp.drawRoundedRect(rect, args.roundRadius, args.roundRadius);\n\t} else {\n\t\tp.drawEllipse(rect);\n\t}\n\n\tif (_uploading) {\n\t\tconst auto cancelOpacity = _cancelShown.value(\n\t\t\t_over ? 1. : 0.);\n\t\tif (cancelOpacity > 0. && args.cancelIcon) {\n\t\t\tp.setOpacity(o * hideOpacity * cancelOpacity);\n\t\t\targs.cancelIcon->paintInCenter(p, rect);\n\t\t}\n\t}\n\n\tp.setOpacity(o * hideOpacity);\n\tconst auto line = float64(args.lineWidth);\n\tconst auto margin = float64(args.margin);\n\tconst auto arc = QRectF(rect) - QMarginsF(\n\t\tmargin,\n\t\tmargin,\n\t\tmargin,\n\t\tmargin);\n\t_radial.draw(p, arc, line, args.progressFg);\n\n\tp.setOpacity(o);\n}\n\nvoid UploadProgressOverlay::radialAnimationCallback(crl::time now) {\n\tconst auto updated = _radial.update(\n\t\t_progress,\n\t\t!_uploading,\n\t\tnow);\n\tif (!anim::Disabled() || updated || _radial.animating()) {\n\t\t_update();\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/effects/upload_progress_overlay.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace Ui {\n\nclass UploadProgressOverlay final {\npublic:\n\tstruct PaintArgs {\n\t\tint lineWidth = 3;\n\t\tint margin = 8;\n\t\tstyle::color progressFg;\n\t\tstyle::color overlayFg;\n\t\tconst style::icon *cancelIcon = nullptr;\n\t\tfloat64 roundRadius = 0.;\n\t};\n\n\tUploadProgressOverlay(\n\t\tnot_null<QWidget*> parent,\n\t\tFn<void()> update);\n\n\tvoid start();\n\tvoid stop(Fn<void()> done = nullptr);\n\tvoid fail(Fn<void()> done = nullptr);\n\tvoid setProgress(float64 progress);\n\tvoid setOver(bool over);\n\n\t[[nodiscard]] bool shown() const;\n\t[[nodiscard]] bool uploading() const;\n\n\tvoid paint(QPainter &p, QRect rect, const PaintArgs &args);\n\nprivate:\n\tvoid radialAnimationCallback(crl::time now);\n\tvoid finish(Fn<void()> done);\n\tvoid proceedWithStop();\n\n\tconst not_null<QWidget*> _parent;\n\tconst Fn<void()> _update;\n\tRadialAnimation _radial;\n\tAnimations::Simple _cancelShown;\n\tAnimations::Simple _hiding;\n\tFn<void()> _hidingDone;\n\tbool _uploading = false;\n\tbool _stopping = false;\n\tbool _over = false;\n\tfloat64 _progress = 0.;\n\tcrl::time _startedAt = 0;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/empty_userpic.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/empty_userpic.h\"\n\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_widgets.h\" // style::IconButton\n#include \"styles/style_info.h\" // st::topBarCall\n\n#include <QtCore/QMutex>\n#include <QtSvg/QSvgRenderer>\n\nnamespace Ui {\nnamespace {\n\n[[nodiscard]] bool IsExternal(const QString &name) {\n\treturn !name.isEmpty()\n\t\t&& (name.front() == QChar(0))\n\t\t&& QStringView(name).mid(1) == u\"external\"_q;\n}\n\n[[nodiscard]] bool IsInaccessible(const QString &name) {\n\treturn !name.isEmpty()\n\t\t&& (name.front() == QChar(0))\n\t\t&& QStringView(name).mid(1) == u\"inaccessible\"_q;\n}\n\nvoid PaintSavedMessagesInner(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tconst style::color &fg) {\n\t// |<----width----->|\n\t//\n\t// XXXXXXXXXXXXXXXXXX  ---\n\t// X                X   |\n\t// X                X   |\n\t// X                X   |\n\t// X                X height\n\t// X       XX       X   |     ---\n\t// X     XX  XX     X   |      |\n\t// X   XX      XX   X   |     add\n\t// X XX          XX X   |      |\n\t// XX              XX  ---    ---\n\n\tconst auto thinkness = base::SafeRound(size * 0.055);\n\tconst auto increment = int(thinkness) % 2 + (size % 2);\n\tconst auto width = base::SafeRound(size * 0.15) * 2 + increment;\n\tconst auto height = base::SafeRound(size * 0.19) * 2 + increment;\n\tconst auto add = base::SafeRound(size * 0.064);\n\n\tconst auto left = x + (size - width) / 2;\n\tconst auto top = y + (size - height) / 2;\n\tconst auto right = left + width;\n\tconst auto bottom = top + height;\n\tconst auto middle = (left + right) / 2;\n\tconst auto half = (top + bottom) / 2;\n\n\tp.setBrush(Qt::NoBrush);\n\tauto pen = fg->p;\n\tpen.setWidthF(thinkness);\n\tpen.setCapStyle(Qt::FlatCap);\n\n\t{\n\t\t// XXXXXXXXXXXXXXXXXX\n\t\t// X                X\n\t\t// X                X\n\t\t// X                X\n\t\t// X                X\n\t\t// X                X\n\n\t\tpen.setJoinStyle(Qt::RoundJoin);\n\t\tp.setPen(pen);\n\t\tQPainterPath path;\n\t\tpath.moveTo(left, half);\n\t\tpath.lineTo(left, top);\n\t\tpath.lineTo(right, top);\n\t\tpath.lineTo(right, half);\n\t\tp.drawPath(path);\n\t}\n\t{\n\t\t// X                X\n\t\t// X       XX       X\n\t\t// X     XX  XX     X\n\t\t// X   XX      XX   X\n\t\t// X XX          XX X\n\t\t// XX              XX\n\n\t\tpen.setJoinStyle(Qt::MiterJoin);\n\t\tp.setPen(pen);\n\t\tQPainterPath path;\n\t\tpath.moveTo(left, half);\n\t\tpath.lineTo(left, bottom);\n\t\tpath.lineTo(middle, bottom - add);\n\t\tpath.lineTo(right, bottom);\n\t\tpath.lineTo(right, half);\n\t\tp.drawPath(path);\n\t}\n}\n\nvoid PaintIconInner(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tint defaultSize,\n\t\tconst style::icon &icon,\n\t\tconst style::color &fg) {\n\tif (size == defaultSize) {\n\t\tconst auto rect = QRect{ x, y, size, size };\n\t\ticon.paintInCenter(\n\t\t\tp,\n\t\t\trect,\n\t\t\tfg->c);\n\t} else {\n\t\tp.save();\n\t\tconst auto ratio = size / float64(defaultSize);\n\t\tp.translate(x + size / 2., y + size / 2.);\n\t\tp.scale(ratio, ratio);\n\t\tconst auto skip = defaultSize;\n\t\tconst auto rect = QRect{ -skip, -skip, 2 * skip, 2 * skip };\n\t\ticon.paintInCenter(\n\t\t\tp,\n\t\t\trect,\n\t\t\tfg->c);\n\t\tp.restore();\n\t}\n}\n\nvoid PaintRepliesMessagesInner(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tconst style::color &fg) {\n\tPaintIconInner(\n\t\tp,\n\t\tx,\n\t\ty,\n\t\tsize,\n\t\tst::defaultDialogRow.photoSize,\n\t\tst::dialogsRepliesUserpic,\n\t\tfg);\n}\n\nvoid PaintHiddenAuthorInner(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tconst style::color &fg) {\n\tPaintIconInner(\n\t\tp,\n\t\tx,\n\t\ty,\n\t\tsize,\n\t\tst::defaultDialogRow.photoSize,\n\t\tst::dialogsHiddenAuthorUserpic,\n\t\tfg);\n}\n\nvoid PaintMyNotesInner(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tconst style::color &fg) {\n\tPaintIconInner(\n\t\tp,\n\t\tx,\n\t\ty,\n\t\tsize,\n\t\tst::defaultDialogRow.photoSize,\n\t\tst::dialogsMyNotesUserpic,\n\t\tfg);\n}\n\nvoid PaintCurrencyInner(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tconst style::color &fg) {\n\tauto svg = QSvgRenderer(Ui::Earn::CurrencySvgColored(fg->c));\n\tconst auto skip = size / 5;\n\tsvg.render(&p, QRect(x, y, size, size).marginsRemoved(\n\t\t{ skip, skip, skip, skip }));\n}\n\nvoid PaintExternalMessagesInner(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tconst style::color &fg) {\n\tPaintIconInner(\n\t\tp,\n\t\tx,\n\t\ty,\n\t\tsize,\n\t\tst::msgPhotoSize,\n\t\tst::topBarCall.icon,\n\t\tfg);\n}\n\nvoid PaintInaccessibleAccountInner(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint size,\n\t\tconst style::color &fg) {\n\tif (size > st::defaultDialogRow.photoSize) {\n\t\tPaintIconInner(\n\t\t\tp,\n\t\t\tx,\n\t\t\ty,\n\t\t\tsize,\n\t\t\tst::infoProfilePhotoInnerSize,\n\t\t\tst::infoProfileInaccessibleUserpic,\n\t\t\tfg);\n\t} else {\n\t\tPaintIconInner(\n\t\t\tp,\n\t\t\tx,\n\t\t\ty,\n\t\t\tsize,\n\t\t\tst::defaultDialogRow.photoSize,\n\t\t\tst::dialogsInaccessibleUserpic,\n\t\t\tfg);\n\t}\n}\n\n[[nodiscard]] QImage Generate(int size, Fn<void(QPainter&)> callback) {\n\tauto result = QImage(\n\t\tQSize(size, size) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult.fill(Qt::transparent);\n\t{\n\t\tPainter p(&result);\n\t\tcallback(p);\n\t}\n\treturn result;\n}\n\n} // namespace\n\nEmptyUserpic::EmptyUserpic(const BgColors &colors, const QString &name)\n: _colors(colors) {\n\tfillString(name);\n}\n\nQString EmptyUserpic::ExternalName() {\n\treturn QChar(0) + u\"external\"_q;\n}\n\nQString EmptyUserpic::InaccessibleName() {\n\treturn QChar(0) + u\"inaccessible\"_q;\n}\n\nuint8 EmptyUserpic::ColorIndex(uint64 id) {\n\treturn DecideColorIndex(id);\n}\n\nEmptyUserpic::BgColors EmptyUserpic::UserpicColor(uint8 colorIndex) {\n\tconst EmptyUserpic::BgColors colors[] = {\n\t\t{ st::historyPeer1UserpicBg, st::historyPeer1UserpicBg2 },\n\t\t{ st::historyPeer2UserpicBg, st::historyPeer2UserpicBg2 },\n\t\t{ st::historyPeer3UserpicBg, st::historyPeer3UserpicBg2 },\n\t\t{ st::historyPeer4UserpicBg, st::historyPeer4UserpicBg2 },\n\t\t{ st::historyPeer5UserpicBg, st::historyPeer5UserpicBg2 },\n\t\t{ st::historyPeer6UserpicBg, st::historyPeer6UserpicBg2 },\n\t\t{ st::historyPeer7UserpicBg, st::historyPeer7UserpicBg2 },\n\t\t{ st::historyPeer8UserpicBg, st::historyPeer8UserpicBg2 },\n\t};\n\treturn colors[ColorIndexToPaletteIndex(colorIndex)];\n}\n\nvoid EmptyUserpic::paint(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size,\n\t\tFn<void()> paintBackground) const {\n\tx = style::RightToLeft() ? (outerWidth - x - size) : x;\n\n\tconst auto fontsize = (size * 13) / 33;\n\tauto font = st::historyPeerUserpicFont->f;\n\tfont.setPixelSize(fontsize);\n\n\tPainterHighQualityEnabler hq(p);\n\t{\n\t\tauto gradient = QLinearGradient(x, y, x, y + size);\n\t\tgradient.setStops({\n\t\t\t{ 0., _colors.color1->c },\n\t\t\t{ 1., _colors.color2->c }\n\t\t});\n\t\tp.setBrush(gradient);\n\t}\n\tp.setPen(Qt::NoPen);\n\tpaintBackground();\n\n\tif (IsExternal(_string)) {\n\t\tPaintExternalMessagesInner(p, x, y, size, st::historyPeerUserpicFg);\n\t} else if (IsInaccessible(_string)) {\n\t\tPaintInaccessibleAccountInner(\n\t\t\tp,\n\t\t\tx,\n\t\t\ty,\n\t\t\tsize,\n\t\t\tst::historyPeerUserpicFg);\n\t} else {\n\t\tp.setFont(font);\n\t\tp.setBrush(Qt::NoBrush);\n\t\tp.setPen(st::historyPeerUserpicFg);\n\t\tp.drawText(\n\t\t\tQRect(x, y, size, size),\n\t\t\t_string,\n\t\t\tQTextOption(style::al_center));\n\t}\n}\n\nvoid EmptyUserpic::paintCircle(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size) const {\n\tpaint(p, x, y, outerWidth, size, [&] {\n\t\tif (style::SquareUserpics()) {\n\t\t\tp.drawRect(x, y, size, size);\n\t\t} else {\n\t\t\tp.drawEllipse(x, y, size, size);\n\t\t}\n\t});\n}\n\nvoid EmptyUserpic::paintRounded(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size,\n\t\tint radius) const {\n\tpaint(p, x, y, outerWidth, size, [&] {\n\t\tp.drawRoundedRect(x, y, size, size, radius, radius);\n\t});\n}\n\nvoid EmptyUserpic::paintSquare(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size) const {\n\tpaint(p, x, y, outerWidth, size, [&] {\n\t\tp.fillRect(x, y, size, size, p.brush());\n\t});\n}\n\nvoid EmptyUserpic::paintMonoforum(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size) const {\n\tpaint(p, x, y, outerWidth, size, [&] {\n\t\tPaintMonoforumShape(p, QRect(x, y, size, size));\n\t});\n}\n\nvoid EmptyUserpic::PaintSavedMessages(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size) {\n\tauto bg = QLinearGradient(x, y, x, y + size);\n\tbg.setStops({\n\t\t{ 0., st::historyPeerSavedMessagesBg->c },\n\t\t{ 1., st::historyPeerSavedMessagesBg2->c }\n\t});\n\tconst auto &fg = st::historyPeerUserpicFg;\n\tPaintSavedMessages(p, x, y, outerWidth, size, QBrush(bg), fg);\n}\n\nvoid EmptyUserpic::PaintSavedMessages(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size,\n\t\tQBrush bg,\n\t\tconst style::color &fg) {\n\tx = style::RightToLeft() ? (outerWidth - x - size) : x;\n\n\tPainterHighQualityEnabler hq(p);\n\tp.setBrush(std::move(bg));\n\tp.setPen(Qt::NoPen);\n\n\tif (style::SquareUserpics()) {\n\t\tp.drawRect(x, y, size, size);\n\t} else {\n\t\tp.drawEllipse(x, y, size, size);\n\t}\n\n\tPaintSavedMessagesInner(p, x, y, size, fg);\n}\n\nQImage EmptyUserpic::GenerateSavedMessages(int size) {\n\treturn Generate(size, [&](QPainter &p) {\n\t\tPaintSavedMessages(p, 0, 0, size, size);\n\t});\n}\n\nvoid EmptyUserpic::PaintRepliesMessages(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size) {\n\tauto bg = QLinearGradient(x, y, x, y + size);\n\tbg.setStops({\n\t\t{ 0., st::historyPeerSavedMessagesBg->c },\n\t\t{ 1., st::historyPeerSavedMessagesBg2->c }\n\t});\n\tconst auto &fg = st::historyPeerUserpicFg;\n\tPaintRepliesMessages(p, x, y, outerWidth, size, QBrush(bg), fg);\n}\n\nvoid EmptyUserpic::PaintRepliesMessages(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size,\n\t\tQBrush bg,\n\t\tconst style::color &fg) {\n\tx = style::RightToLeft() ? (outerWidth - x - size) : x;\n\n\tPainterHighQualityEnabler hq(p);\n\tp.setBrush(bg);\n\tp.setPen(Qt::NoPen);\n\tif (style::SquareUserpics()) {\n\t\tp.drawRect(x, y, size, size);\n\t} else {\n\t\tp.drawEllipse(x, y, size, size);\n\t}\n\n\tPaintRepliesMessagesInner(p, x, y, size, fg);\n}\n\nQImage EmptyUserpic::GenerateRepliesMessages(int size) {\n\treturn Generate(size, [&](QPainter &p) {\n\t\tPaintRepliesMessages(p, 0, 0, size, size);\n\t});\n}\n\nvoid EmptyUserpic::PaintHiddenAuthor(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size) {\n\tauto bg = QLinearGradient(x, y, x, y + size);\n\tbg.setStops({\n\t\t{ 0., st::premiumButtonBg2->c },\n\t\t{ 1., st::premiumButtonBg3->c },\n\t});\n\tconst auto &fg = st::premiumButtonFg;\n\tPaintHiddenAuthor(p, x, y, outerWidth, size, QBrush(bg), fg);\n}\n\nvoid EmptyUserpic::PaintHiddenAuthor(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size,\n\t\tQBrush bg,\n\t\tconst style::color &fg) {\n\tx = style::RightToLeft() ? (outerWidth - x - size) : x;\n\n\tPainterHighQualityEnabler hq(p);\n\tp.setBrush(bg);\n\tp.setPen(Qt::NoPen);\n\tif (style::SquareUserpics()) {\n\t\tp.drawRect(x, y, size, size);\n\t} else {\n\t\tp.drawEllipse(x, y, size, size);\n\t}\n\n\tPaintHiddenAuthorInner(p, x, y, size, fg);\n}\n\nQImage EmptyUserpic::GenerateHiddenAuthor(int size) {\n\treturn Generate(size, [&](QPainter &p) {\n\t\tPaintHiddenAuthor(p, 0, 0, size, size);\n\t});\n}\n\nvoid EmptyUserpic::PaintMyNotes(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size) {\n\tauto bg = QLinearGradient(x, y, x, y + size);\n\tbg.setStops({\n\t\t{ 0., st::historyPeerSavedMessagesBg->c },\n\t\t{ 1., st::historyPeerSavedMessagesBg2->c }\n\t});\n\tconst auto &fg = st::historyPeerUserpicFg;\n\tPaintMyNotes(p, x, y, outerWidth, size, QBrush(bg), fg);\n}\n\nvoid EmptyUserpic::PaintMyNotes(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size,\n\t\tQBrush bg,\n\t\tconst style::color &fg) {\n\tx = style::RightToLeft() ? (outerWidth - x - size) : x;\n\n\tPainterHighQualityEnabler hq(p);\n\tp.setBrush(bg);\n\tp.setPen(Qt::NoPen);\n\tif (style::SquareUserpics()) {\n\t\tp.drawRect(x, y, size, size);\n\t} else {\n\t\tp.drawEllipse(x, y, size, size);\n\t}\n\n\tPaintMyNotesInner(p, x, y, size, fg);\n}\n\nQImage EmptyUserpic::GenerateMyNotes(int size) {\n\treturn Generate(size, [&](QPainter &p) {\n\t\tPaintMyNotes(p, 0, 0, size, size);\n\t});\n}\n\nvoid EmptyUserpic::PaintCurrency(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size) {\n\tauto bg = QLinearGradient(x, y, x, y + size);\n\tbg.setStops({\n\t\t{ 0., st::historyPeerSavedMessagesBg->c },\n\t\t{ 1., st::historyPeerSavedMessagesBg2->c }\n\t});\n\tconst auto &fg = st::historyPeerUserpicFg;\n\tPaintCurrency(p, x, y, outerWidth, size, QBrush(bg), fg);\n}\n\nvoid EmptyUserpic::PaintCurrency(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size,\n\t\tQBrush bg,\n\t\tconst style::color &fg) {\n\tx = style::RightToLeft() ? (outerWidth - x - size) : x;\n\n\tPainterHighQualityEnabler hq(p);\n\tp.setBrush(bg);\n\tp.setPen(Qt::NoPen);\n\tif (style::SquareUserpics()) {\n\t\tp.drawRect(x, y, size, size);\n\t} else {\n\t\tp.drawEllipse(x, y, size, size);\n\t}\n\n\tPaintCurrencyInner(p, x, y, size, fg);\n}\n\nQImage EmptyUserpic::GenerateCurrency(int size) {\n\treturn Generate(size, [&](QPainter &p) {\n\t\tPaintCurrency(p, 0, 0, size, size);\n\t});\n}\n\nstd::pair<uint64, uint64> EmptyUserpic::uniqueKey() const {\n\tconst auto first = (uint64(0xFFFFFFFFU) << 32)\n\t\t| anim::getPremultiplied(_colors.color1->c);\n\tauto second = uint64(0);\n\tmemcpy(\n\t\t&second,\n\t\t_string.constData(),\n\t\tstd::min(sizeof(second), size_t(_string.size()) * sizeof(QChar)));\n\treturn { first, second };\n}\n\nQPixmap EmptyUserpic::generate(int size) {\n\tauto result = QImage(\n\t\tQSize(size, size) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&result);\n\t\tpaintCircle(p, 0, 0, size, size);\n\t}\n\treturn Ui::PixmapFromImage(std::move(result));\n}\n\nvoid EmptyUserpic::fillString(const QString &name) {\n\tif (IsExternal(name) || IsInaccessible(name)) {\n\t\t_string = name;\n\t\treturn;\n\t}\n\tQList<QString> letters;\n\tQList<int> levels;\n\n\tauto level = 0;\n\tauto letterFound = false;\n\tauto ch = name.constData(), end = ch + name.size();\n\twhile (ch != end) {\n\t\tauto emojiLength = 0;\n\t\tif (Ui::Emoji::Find(ch, end, &emojiLength)) {\n\t\t\tch += emojiLength;\n\t\t} else if (ch->isHighSurrogate()) {\n\t\t\t++ch;\n\t\t\tif (ch != end && ch->isLowSurrogate()) {\n\t\t\t\t++ch;\n\t\t\t}\n\t\t} else if (!letterFound && ch->isLetterOrNumber()) {\n\t\t\tletterFound = true;\n\t\t\tif (ch + 1 != end && Ui::Text::IsDiacritic(*(ch + 1))) {\n\t\t\t\tletters.push_back(QString(ch, 2));\n\t\t\t\tlevels.push_back(level);\n\t\t\t\t++ch;\n\t\t\t} else {\n\t\t\t\tletters.push_back(QString(ch, 1));\n\t\t\t\tlevels.push_back(level);\n\t\t\t}\n\t\t\t++ch;\n\t\t} else {\n\t\t\tif (*ch == ' ') {\n\t\t\t\tlevel = 0;\n\t\t\t\tletterFound = false;\n\t\t\t} else if (letterFound && *ch == '-') {\n\t\t\t\tlevel = 1;\n\t\t\t\tletterFound = true;\n\t\t\t}\n\t\t\t++ch;\n\t\t}\n\t}\n\n\t// We prefer the second letter to be after ' ', but it can also be after '-'.\n\t_string = QString();\n\tif (!letters.isEmpty()) {\n\t\t_string += letters.front();\n\t\tauto bestIndex = 0;\n\t\tauto bestLevel = 2;\n\t\tfor (auto i = letters.size(); i != 1;) {\n\t\t\tif (levels[--i] < bestLevel) {\n\t\t\t\tbestIndex = i;\n\t\t\t\tbestLevel = levels[i];\n\t\t\t}\n\t\t}\n\t\tif (bestIndex > 0) {\n\t\t\t_string += letters[bestIndex];\n\t\t}\n\t}\n\t_string = _string.toUpper();\n}\n\nEmptyUserpic::~EmptyUserpic() = default;\n\nvoid PaintMonoforumShape(QPainter &p, QRect rect) {\n\tif (style::SquareUserpics()) {\n\t\tp.drawRect(rect);\n\t} else {\n\t\tp.drawEllipse(rect);\n\t}\n\n\tauto path = QPainterPath();\n\tpath.moveTo(\n\t\trect.x() + rect.width() * 0.5,\n\t\trect.y() + rect.height() * 0.5);\n\tpath.arcTo(\n\t\tQRectF(\n\t\t\trect.x() - rect.width() * 0.5,\n\t\t\trect.y(),\n\t\t\trect.width(),\n\t\t\trect.height()),\n\t\t0,\n\t\t-90);\n\tpath.arcTo(\n\t\tQRectF(\n\t\t\trect.x() - rect.width() * 0.25,\n\t\t\trect.y() - rect.height() * 2,\n\t\t\trect.width() * 0.5,\n\t\t\trect.height() * 3),\n\t\t-90,\n\t\t45);\n\tpath.lineTo(\n\t\trect.x() + rect.width() * 0.5,\n\t\trect.y() + rect.height() * 0.5);\n\tp.drawPath(path);\n}\n\nQImage MonoforumShapeMask(QSize size) {\n\tauto result = QImage(size, QImage::Format_ARGB32_Premultiplied);\n\tresult.fill(Qt::transparent);\n\n\tQPainter p(&result);\n\tPainterHighQualityEnabler hq(p);\n\tp.setBrush(Qt::white);\n\tp.setPen(Qt::NoPen);\n\n\tPaintMonoforumShape(p, QRect(QPoint(), size));\n\n\tp.end();\n\n\treturn result;\n}\n\nconst QImage &MonoforumShapeMaskCached(QSize size) {\n\tconst auto key = (uint64(uint32(size.width())) << 32)\n\t\t| uint64(uint32(size.height()));\n\n\tstatic auto Masks = base::flat_map<uint64, QImage>();\n\tstatic auto Mutex = QMutex();\n\tauto lock = QMutexLocker(&Mutex);\n\tconst auto i = Masks.find(key);\n\tif (i != end(Masks)) {\n\t\treturn i->second;\n\t}\n\tlock.unlock();\n\n\tauto mask = MonoforumShapeMask(size);\n\n\tlock.relock();\n\treturn Masks.emplace(key, std::move(mask)).first->second;\n}\n\nQImage ApplyMonoforumShape(QImage image) {\n\tconst auto size = image.size();\n\tauto mask = MonoforumShapeMaskCached(size);\n\n\tconstexpr auto format = QImage::Format_ARGB32_Premultiplied;\n\tif (image.format() != format) {\n\t\timage = std::move(image).convertToFormat(format);\n\t}\n\tauto p = QPainter(&image);\n\tp.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\tp.drawImage(\n\t\tQRect(QPoint(), image.size() / image.devicePixelRatio()),\n\t\tmask);\n\tp.end();\n\n\treturn image;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/empty_userpic.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n\nnamespace Ui {\n\nclass EmptyUserpic final : public base::has_weak_ptr {\npublic:\n\tstruct BgColors {\n\t\tconst style::color color1;\n\t\tconst style::color color2;\n\t};\n\n\t[[nodiscard]] static uint8 ColorIndex(uint64 id);\n\t[[nodiscard]] static EmptyUserpic::BgColors UserpicColor(\n\t\tuint8 colorIndex);\n\n\t[[nodiscard]] static QString ExternalName();\n\t[[nodiscard]] static QString InaccessibleName();\n\n\tEmptyUserpic(const BgColors &colors, const QString &name);\n\n\tvoid paintCircle(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size) const;\n\tvoid paintRounded(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size,\n\t\tint radius = 0) const;\n\tvoid paintSquare(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size) const;\n\tvoid paintMonoforum(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size) const;\n\t[[nodiscard]] QPixmap generate(int size);\n\t[[nodiscard]] std::pair<uint64, uint64> uniqueKey() const;\n\n\tstatic void PaintSavedMessages(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size);\n\tstatic void PaintSavedMessages(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size,\n\t\tQBrush bg,\n\t\tconst style::color &fg);\n\t[[nodiscard]] static QImage GenerateSavedMessages(int size);\n\n\tstatic void PaintRepliesMessages(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size);\n\tstatic void PaintRepliesMessages(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size,\n\t\tQBrush bg,\n\t\tconst style::color &fg);\n\t[[nodiscard]] static QImage GenerateRepliesMessages(int size);\n\n\tstatic void PaintHiddenAuthor(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size);\n\tstatic void PaintHiddenAuthor(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size,\n\t\tQBrush bg,\n\t\tconst style::color &fg);\n\t[[nodiscard]] static QImage GenerateHiddenAuthor(int size);\n\n\tstatic void PaintMyNotes(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size);\n\tstatic void PaintMyNotes(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size,\n\t\tQBrush bg,\n\t\tconst style::color &fg);\n\t[[nodiscard]] static QImage GenerateMyNotes(int size);\n\n\tstatic void PaintCurrency(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size);\n\tstatic void PaintCurrency(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size,\n\t\tQBrush bg,\n\t\tconst style::color &fg);\n\t[[nodiscard]] static QImage GenerateCurrency(int size);\n\n\t~EmptyUserpic();\n\nprivate:\n\tvoid paint(\n\t\tQPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tint size,\n\t\tFn<void()> paintBackground) const;\n\n\tvoid fillString(const QString &name);\n\n\tconst BgColors _colors;\n\tQString _string;\n\n};\n\nvoid PaintMonoforumShape(QPainter &p, QRect rect);\n[[nodiscard]] QImage MonoforumShapeMask(QSize size);\n[[nodiscard]] const QImage &MonoforumShapeMaskCached(QSize size);\n[[nodiscard]] QImage ApplyMonoforumShape(QImage image);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/filter_icon_panel.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/filter_icon_panel.h\"\n\n#include \"ui/widgets/shadow.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/effects/panel_animation.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/filter_icons.h\"\n#include \"ui/painter.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"lang/lang_keys.h\"\n#include \"core/application.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_window.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kHideTimeoutMs = crl::time(300);\nconstexpr auto kIconsPerRow = 6;\n\nconstexpr auto kIcons = std::array{\n\tFilterIcon::Cat,\n\tFilterIcon::Book,\n\tFilterIcon::Money,\n\t// FilterIcon::Camera,\n\tFilterIcon::Game,\n\t// FilterIcon::House,\n\tFilterIcon::Light,\n\tFilterIcon::Like,\n\t// FilterIcon::Plus,\n\tFilterIcon::Note,\n\tFilterIcon::Palette,\n\tFilterIcon::Travel,\n\tFilterIcon::Sport,\n\tFilterIcon::Favorite,\n\tFilterIcon::Study,\n\tFilterIcon::Airplane,\n\t// FilterIcon::Microbe,\n\t// FilterIcon::Worker,\n\tFilterIcon::Private,\n\tFilterIcon::Groups,\n\tFilterIcon::All,\n\tFilterIcon::Unread,\n\t// FilterIcon::Check,\n\tFilterIcon::Bots,\n\t// FilterIcon::Folders,\n\tFilterIcon::Crown,\n\tFilterIcon::Flower,\n\tFilterIcon::Home,\n\tFilterIcon::Love,\n\tFilterIcon::Mask,\n\tFilterIcon::Party,\n\tFilterIcon::Trade,\n\tFilterIcon::Work,\n\tFilterIcon::Unmuted,\n\tFilterIcon::Channels,\n\tFilterIcon::Custom,\n\tFilterIcon::Setup,\n\t// FilterIcon::Poo,\n};\n\n} // namespace\n\nFilterIconPanel::FilterIconPanel(QWidget *parent)\n: RpWidget(parent)\n, _inner(Ui::CreateChild<Ui::RpWidget>(this))\n, _innerBg(ImageRoundRadius::Small, st::dialogsBg)\n, _shadow(st::emojiPanAnimation.shadow) {\n\tsetup();\n}\n\nFilterIconPanel::~FilterIconPanel() {\n\thideFast();\n}\n\nrpl::producer<FilterIcon> FilterIconPanel::chosen() const {\n\treturn _chosen.events();\n}\n\nvoid FilterIconPanel::setup() {\n\tsetupInner();\n\tresize(_inner->rect().marginsAdded(innerPadding()).size());\n\t_inner->move(innerRect().topLeft());\n\n\t_hideTimer.setCallback([=] { hideByTimerOrLeave(); });\n\n\tmacWindowDeactivateEvents(\n\t) | rpl::filter([=] {\n\t\treturn !isHidden();\n\t}) | rpl::on_next([=] {\n\t\thideAnimated();\n\t}, lifetime());\n\n\tsetAttribute(Qt::WA_OpaquePaintEvent, false);\n\n\thideChildren();\n\thide();\n}\n\nvoid FilterIconPanel::setupInner() {\n\tconst auto count = kIcons.size();\n\tconst auto rows = (count / kIconsPerRow)\n\t\t+ ((count % kIconsPerRow) ? 1 : 0);\n\tconst auto single = st::windowFilterIconSingle;\n\tconst auto size = QSize(\n\t\tsingle.width() * kIconsPerRow,\n\t\tsingle.height() * rows);\n\tconst auto full = QRect(QPoint(), size).marginsAdded(\n\t\tst::windowFilterIconPadding).size();\n\t_inner->resize(full);\n\n\t_inner->paintRequest(\n\t\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = Painter(_inner);\n\t\t_innerBg.paint(p, _inner->rect());\n\t\tp.setFont(st::emojiPanHeaderFont);\n\t\tp.setPen(st::emojiPanHeaderFg);\n\t\tp.drawTextLeft(\n\t\t\tst::windowFilterIconHeaderPosition.x(),\n\t\t\tst::windowFilterIconHeaderPosition.y(),\n\t\t\t_inner->width(),\n\t\t\ttr::lng_filters_icon_header(tr::now));\n\n\t\tconst auto selected = (_pressed >= 0) ? _pressed : _selected;\n\t\tfor (auto i = 0; i != kIcons.size(); ++i) {\n\t\t\tconst auto rect = countRect(i);\n\t\t\tif (!rect.intersects(clip)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto over = (i == selected);\n\t\t\tif (over) {\n\t\t\t\tUi::FillRoundRect(\n\t\t\t\t\tp,\n\t\t\t\t\trect,\n\t\t\t\t\tst::dialogsBgOver,\n\t\t\t\t\tUi::StickerHoverCorners);\n\t\t\t}\n\t\t\tconst auto icon = LookupFilterIcon(kIcons[i]).normal;\n\t\t\ticon->paintInCenter(\n\t\t\t\tp,\n\t\t\t\trect,\n\t\t\t\t(over\n\t\t\t\t\t? st::dialogsUnreadBgMutedOver\n\t\t\t\t\t: st::dialogsUnreadBgMuted)->c);\n\t\t}\n\t}, _inner->lifetime());\n\n\t_inner->setMouseTracking(true);\n\t_inner->events(\n\t) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tswitch (e->type()) {\n\t\tcase QEvent::Leave: setSelected(-1); break;\n\t\tcase QEvent::MouseMove:\n\t\t\tmouseMove(static_cast<QMouseEvent*>(e.get())->pos());\n\t\t\tbreak;\n\t\tcase QEvent::MouseButtonPress:\n\t\t\tmousePress(static_cast<QMouseEvent*>(e.get())->button());\n\t\t\tbreak;\n\t\tcase QEvent::MouseButtonRelease:\n\t\t\tmouseRelease(static_cast<QMouseEvent*>(e.get())->button());\n\t\t\tbreak;\n\t\t}\n\t}, _inner->lifetime());\n}\n\nvoid FilterIconPanel::setSelected(int selected) {\n\tif (_selected == selected) {\n\t\treturn;\n\t}\n\tconst auto was = (_selected >= 0);\n\tupdateRect(_selected);\n\t_selected = selected;\n\tupdateRect(_selected);\n\tconst auto now = (_selected >= 0);\n\tif (was != now) {\n\t\t_inner->setCursor(now ? style::cur_pointer : style::cur_default);\n\t}\n}\n\nvoid FilterIconPanel::setPressed(int pressed) {\n\tif (_pressed == pressed) {\n\t\treturn;\n\t}\n\tupdateRect(_pressed);\n\t_pressed = pressed;\n\tupdateRect(_pressed);\n}\n\nQRect FilterIconPanel::countRect(int index) const {\n\tExpects(index >= 0);\n\n\tconst auto row = index / kIconsPerRow;\n\tconst auto column = index % kIconsPerRow;\n\tconst auto single = st::windowFilterIconSingle;\n\tconst auto rect = QRect(\n\t\tQPoint(column * single.width(), row * single.height()),\n\t\tsingle);\n\tconst auto padding = st::windowFilterIconPadding;\n\treturn rect.translated(padding.left(), padding.top());\n}\n\nvoid FilterIconPanel::updateRect(int index) {\n\tif (index < 0) {\n\t\treturn;\n\t}\n\t_inner->update(countRect(index));\n}\n\nvoid FilterIconPanel::mouseMove(QPoint position) {\n\tconst auto padding = st::windowFilterIconPadding;\n\tif (!_inner->rect().marginsRemoved(padding).contains(position)) {\n\t\tsetSelected(-1);\n\t} else {\n\t\tconst auto point = position - QPoint(padding.left(), padding.top());\n\t\tconst auto column = point.x() / st::windowFilterIconSingle.width();\n\t\tconst auto row = point.y() / st::windowFilterIconSingle.height();\n\t\tconst auto index = row * kIconsPerRow + column;\n\t\tsetSelected(index < kIcons.size() ? index : -1);\n\t}\n}\n\nvoid FilterIconPanel::mousePress(Qt::MouseButton button) {\n\tif (button != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tsetPressed(_selected);\n}\n\nvoid FilterIconPanel::mouseRelease(Qt::MouseButton button) {\n\tif (button != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tconst auto pressed = _pressed;\n\tsetPressed(-1);\n\tif (pressed == _selected && pressed >= 0) {\n\t\tAssert(pressed < kIcons.size());\n\t\t_chosen.fire_copy(kIcons[pressed]);\n\t}\n}\n\nvoid FilterIconPanel::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\t// This call can finish _a_show animation and destroy _showAnimation.\n\tconst auto opacityAnimating = _a_opacity.animating();\n\n\tconst auto showAnimating = _a_show.animating();\n\tif (_showAnimation && !showAnimating) {\n\t\t_showAnimation.reset();\n\t\tif (!opacityAnimating) {\n\t\t\tshowChildren();\n\t\t}\n\t}\n\n\tif (showAnimating) {\n\t\tAssert(_showAnimation != nullptr);\n\t\tif (auto opacity = _a_opacity.value(_hiding ? 0. : 1.)) {\n\t\t\t_showAnimation->paintFrame(\n\t\t\t\tp,\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\twidth(),\n\t\t\t\t_a_show.value(1.),\n\t\t\t\topacity);\n\t\t}\n\t} else if (opacityAnimating) {\n\t\tp.setOpacity(_a_opacity.value(_hiding ? 0. : 1.));\n\t\tp.drawPixmap(0, 0, _cache);\n\t} else if (_hiding || isHidden()) {\n\t\thideFinished();\n\t} else {\n\t\tif (!_cache.isNull()) _cache = QPixmap();\n\t\t_shadow.paint(p, innerRect(), st::emojiPanRadius);\n\t}\n}\n\nvoid FilterIconPanel::enterEventHook(QEnterEvent *e) {\n\tCore::App().registerLeaveSubscription(this);\n\tshowAnimated();\n}\n\nvoid FilterIconPanel::leaveEventHook(QEvent *e) {\n\tCore::App().unregisterLeaveSubscription(this);\n\tif (_a_show.animating() || _a_opacity.animating()) {\n\t\thideAnimated();\n\t} else {\n\t\t_hideTimer.callOnce(kHideTimeoutMs);\n\t}\n\treturn RpWidget::leaveEventHook(e);\n}\n\nvoid FilterIconPanel::otherEnter() {\n\tshowAnimated();\n}\n\nvoid FilterIconPanel::otherLeave() {\n\tif (_a_opacity.animating()) {\n\t\thideByTimerOrLeave();\n\t} else {\n\t\t_hideTimer.callOnce(0);\n\t}\n}\n\nvoid FilterIconPanel::hideFast() {\n\tif (isHidden()) return;\n\n\t_hideTimer.cancel();\n\t_hiding = false;\n\t_a_opacity.stop();\n\thideFinished();\n}\n\nvoid FilterIconPanel::opacityAnimationCallback() {\n\tupdate();\n\tif (!_a_opacity.animating()) {\n\t\tif (_hiding) {\n\t\t\t_hiding = false;\n\t\t\thideFinished();\n\t\t} else if (!_a_show.animating()) {\n\t\t\tshowChildren();\n\t\t}\n\t}\n}\n\nvoid FilterIconPanel::hideByTimerOrLeave() {\n\tif (isHidden()) {\n\t\treturn;\n\t}\n\n\thideAnimated();\n}\n\nvoid FilterIconPanel::prepareCacheFor(bool hiding) {\n\tif (_a_opacity.animating()) {\n\t\t_hiding = hiding;\n\t\treturn;\n\t}\n\n\tauto showAnimation = base::take(_a_show);\n\tauto showAnimationData = base::take(_showAnimation);\n\t_hiding = false;\n\tshowChildren();\n\n\t_cache = Ui::GrabWidget(this);\n\n\t_a_show = base::take(showAnimation);\n\t_showAnimation = base::take(showAnimationData);\n\t_hiding = hiding;\n\tif (_a_show.animating()) {\n\t\thideChildren();\n\t}\n}\n\nvoid FilterIconPanel::startOpacityAnimation(bool hiding) {\n\tprepareCacheFor(hiding);\n\thideChildren();\n\t_a_opacity.start(\n\t\t[=] { opacityAnimationCallback(); },\n\t\t_hiding ? 1. : 0.,\n\t\t_hiding ? 0. : 1.,\n\t\tst::emojiPanDuration);\n}\n\nvoid FilterIconPanel::startShowAnimation() {\n\tif (!_a_show.animating()) {\n\t\tauto image = grabForAnimation();\n\n\t\t_showAnimation = std::make_unique<Ui::PanelAnimation>(st::emojiPanAnimation, Ui::PanelAnimation::Origin::TopRight);\n\t\tauto inner = rect().marginsRemoved(st::emojiPanMargins);\n\t\t_showAnimation->setFinalImage(\n\t\t\tstd::move(image),\n\t\t\tQRect(\n\t\t\t\tinner.topLeft() * style::DevicePixelRatio(),\n\t\t\t\tinner.size() * style::DevicePixelRatio()),\n\t\t\tst::emojiPanRadius);\n\t\t_showAnimation->setCornerMasks(Images::CornersMask(ImageRoundRadius::Small));\n\t\t_showAnimation->start();\n\t}\n\thideChildren();\n\t_a_show.start([this] { update(); }, 0., 1., st::emojiPanShowDuration);\n}\n\nQImage FilterIconPanel::grabForAnimation() {\n\tauto cache = base::take(_cache);\n\tauto opacityAnimation = base::take(_a_opacity);\n\tauto showAnimationData = base::take(_showAnimation);\n\tauto showAnimation = base::take(_a_show);\n\n\tshowChildren();\n\tUi::SendPendingMoveResizeEvents(this);\n\n\tauto result = QImage(\n\t\tsize() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult.fill(Qt::transparent);\n\tif (_inner) {\n\t\tQPainter p(&result);\n\t\tUi::RenderWidget(p, _inner, _inner->pos());\n\t}\n\n\t_a_show = base::take(showAnimation);\n\t_showAnimation = base::take(showAnimationData);\n\t_a_opacity = base::take(opacityAnimation);\n\t_cache = base::take(cache);\n\n\treturn result;\n}\n\nvoid FilterIconPanel::hideAnimated() {\n\tif (isHidden() || _hiding) {\n\t\treturn;\n\t}\n\n\t_hideTimer.cancel();\n\tstartOpacityAnimation(true);\n}\n\nvoid FilterIconPanel::toggleAnimated() {\n\tif (isHidden() || _hiding) {\n\t\tshowAnimated();\n\t} else {\n\t\thideAnimated();\n\t}\n}\n\nvoid FilterIconPanel::hideFinished() {\n\thide();\n\t_a_show.stop();\n\t_showAnimation.reset();\n\t_cache = QPixmap();\n\t_hiding = false;\n}\n\nvoid FilterIconPanel::showAnimated() {\n\t_hideTimer.cancel();\n\tshowStarted();\n}\n\nvoid FilterIconPanel::showStarted() {\n\tif (isHidden()) {\n\t\traise();\n\t\tshow();\n\t\tstartShowAnimation();\n\t} else if (_hiding) {\n\t\tstartOpacityAnimation(false);\n\t}\n}\n\nbool FilterIconPanel::eventFilter(QObject *obj, QEvent *e) {\n\tif (e->type() == QEvent::Enter) {\n\t\totherEnter();\n\t} else if (e->type() == QEvent::Leave) {\n\t\totherLeave();\n\t}\n\treturn false;\n}\n\nstyle::margins FilterIconPanel::innerPadding() const {\n\treturn st::emojiPanMargins;\n}\n\nQRect FilterIconPanel::innerRect() const {\n\treturn rect().marginsRemoved(innerPadding());\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/filter_icon_panel.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/round_rect.h\"\n#include \"ui/widgets/shadow.h\"\n\nnamespace Ui {\n\nenum class FilterIcon : uchar;\nclass PanelAnimation;\n\nclass FilterIconPanel final : public Ui::RpWidget {\npublic:\n\tFilterIconPanel(QWidget *parent);\n\t~FilterIconPanel();\n\n\tvoid hideFast();\n\t[[nodiscard]] bool hiding() const {\n\t\treturn _hiding || _hideTimer.isActive();\n\t}\n\n\t[[nodiscard]] style::margins innerPadding() const;\n\n\tvoid showAnimated();\n\tvoid hideAnimated();\n\tvoid toggleAnimated();\n\n\t[[nodiscard]] rpl::producer<FilterIcon> chosen() const;\n\nprivate:\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid otherEnter();\n\tvoid otherLeave();\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tbool eventFilter(QObject *obj, QEvent *e) override;\n\n\tvoid setup();\n\tvoid setupInner();\n\tvoid hideByTimerOrLeave();\n\n\t// Rounded rect which has shadow around it.\n\t[[nodiscard]] QRect innerRect() const;\n\n\t[[nodiscard]] QImage grabForAnimation();\n\tvoid startShowAnimation();\n\tvoid startOpacityAnimation(bool hiding);\n\tvoid prepareCacheFor(bool hiding);\n\n\tvoid opacityAnimationCallback();\n\n\tvoid hideFinished();\n\tvoid showStarted();\n\tvoid setSelected(int selected);\n\tvoid setPressed(int pressed);\n\t[[nodiscard]] QRect countRect(int index) const;\n\tvoid updateRect(int index);\n\tvoid mouseMove(QPoint position);\n\tvoid mousePress(Qt::MouseButton button);\n\tvoid mouseRelease(Qt::MouseButton button);\n\n\tconst not_null<Ui::RpWidget*> _inner;\n\trpl::event_stream<FilterIcon> _chosen;\n\tUi::RoundRect _innerBg;\n\n\tint _selected = -1;\n\tint _pressed = -1;\n\n\tstd::unique_ptr<Ui::PanelAnimation> _showAnimation;\n\tUi::Animations::Simple _a_show;\n\n\tUi::BoxShadow _shadow;\n\tbool _hiding = false;\n\tQPixmap _cache;\n\tUi::Animations::Simple _a_opacity;\n\tbase::Timer _hideTimer;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/filter_icons.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/filter_icons.h\"\n\n#include \"ui/emoji_config.h\"\n#include \"data/data_chat_filters.h\"\n#include \"styles/style_filter_icons.h\"\n\nnamespace Ui {\nnamespace {\n\nconst auto kIcons = std::vector<FilterIcons>{\n\t{\n\t\t&st::foldersCat,\n\t\t&st::foldersCatActive,\n\t\t\"\\xF0\\x9F\\x90\\xB1\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersBook,\n\t\t&st::foldersBookActive,\n\t\t\"\\xF0\\x9F\\x93\\x95\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersMoney,\n\t\t&st::foldersMoneyActive,\n\t\t\"\\xF0\\x9F\\x92\\xB0\"_cs.utf16()\n\t},\n\t//{\n\t//\t&st::foldersCamera,\n\t//\t&st::foldersCameraActive,\n\t//\t\"\\xF0\\x9F\\x93\\xB8\"_cs.utf16()\n\t//},\n\t{\n\t\t&st::foldersGame,\n\t\t&st::foldersGameActive,\n\t\t\"\\xF0\\x9F\\x8E\\xAE\"_cs.utf16()\n\t},\n\t//{\n\t//\t&st::foldersHouse,\n\t//\t&st::foldersHouseActive,\n\t//\t\"\\xF0\\x9F\\x8F\\xA1\"_cs.utf16()\n\t//},\n\t{\n\t\t&st::foldersLight,\n\t\t&st::foldersLightActive,\n\t\t\"\\xF0\\x9F\\x92\\xA1\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersLike,\n\t\t&st::foldersLikeActive,\n\t\t\"\\xF0\\x9F\\x91\\x8C\"_cs.utf16()\n\t},\n\t//{\n\t//\t&st::foldersPlus,\n\t//\t&st::foldersPlusActive,\n\t//\t\"\\xE2\\x9E\\x95\"_cs.utf16()\n\t//},\n\t{\n\t\t&st::foldersNote,\n\t\t&st::foldersNoteActive,\n\t\t\"\\xF0\\x9F\\x8E\\xB5\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersPalette,\n\t\t&st::foldersPaletteActive,\n\t\t\"\\xF0\\x9F\\x8E\\xA8\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersTravel,\n\t\t&st::foldersTravelActive,\n\t\t\"\\xE2\\x9C\\x88\\xEF\\xB8\\x8F\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersSport,\n\t\t&st::foldersSportActive,\n\t\t\"\\xE2\\x9A\\xBD\\xEF\\xB8\\x8F\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersFavorite,\n\t\t&st::foldersFavoriteActive,\n\t\t\"\\xE2\\xAD\\x90\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersStudy,\n\t\t&st::foldersStudyActive,\n\t\t\"\\xF0\\x9F\\x8E\\x93\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersAirplane,\n\t\t&st::foldersAirplaneActive,\n\t\t\"\\xF0\\x9F\\x9B\\xAB\"_cs.utf16()\n\t},\n\t//{\n\t//\t&st::foldersMicrobe,\n\t//\t&st::foldersMicrobeActive,\n\t//\t\"\\xF0\\x9F\\xA6\\xA0\"_cs.utf16()\n\t//},\n\t//{\n\t//\t&st::foldersWorker,\n\t//\t&st::foldersWorkerActive,\n\t//\t\"\\xF0\\x9F\\x91\\xA8\\xE2\\x80\\x8D\\xF0\\x9F\\x92\\xBC\"_cs.utf16()\n\t//},\n\t{\n\t\t&st::foldersPrivate,\n\t\t&st::foldersPrivateActive,\n\t\t\"\\xF0\\x9F\\x91\\xA4\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersGroups,\n\t\t&st::foldersGroupsActive,\n\t\t\"\\xF0\\x9F\\x91\\xA5\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersAll,\n\t\t&st::foldersAllActive,\n\t\t\"\\xF0\\x9F\\x92\\xAC\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersUnread,\n\t\t&st::foldersUnreadActive,\n\t\t\"\\xE2\\x9C\\x85\"_cs.utf16()\n\t},\n\t//{\n\t//\t&st::foldersCheck,\n\t//\t&st::foldersCheckActive,\n\t//\t\"\\xE2\\x98\\x91\\xEF\\xB8\\x8F\"_cs.utf16()\n\t//},\n\t{\n\t\t&st::foldersBots,\n\t\t&st::foldersBotsActive,\n\t\t\"\\xF0\\x9F\\xA4\\x96\"_cs.utf16()\n\t},\n\t//{\n\t//\t&st::foldersFolders,\n\t//\t&st::foldersFoldersActive,\n\t//\t\"\\xF0\\x9F\\x97\\x82\"_cs.utf16()\n\t//},\n\t{\n\t\t&st::foldersCrown,\n\t\t&st::foldersCrownActive,\n\t\t\"\\xF0\\x9F\\x91\\x91\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersFlower,\n\t\t&st::foldersFlowerActive,\n\t\t\"\\xF0\\x9F\\x8C\\xB9\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersHome,\n\t\t&st::foldersHomeActive,\n\t\t\"\\xF0\\x9F\\x8F\\xA0\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersLove,\n\t\t&st::foldersLoveActive,\n\t\t\"\\xE2\\x9D\\xA4\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersMask,\n\t\t&st::foldersMaskActive,\n\t\t\"\\xF0\\x9F\\x8E\\xAD\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersParty,\n\t\t&st::foldersPartyActive,\n\t\t\"\\xF0\\x9F\\x8D\\xB8\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersTrade,\n\t\t&st::foldersTradeActive,\n\t\t\"\\xF0\\x9F\\x93\\x88\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersWork,\n\t\t&st::foldersWorkActive,\n\t\t\"\\xF0\\x9F\\x92\\xBC\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersUnmuted,\n\t\t&st::foldersUnmutedActive,\n\t\t\"\\xF0\\x9F\\x94\\x94\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersChannels,\n\t\t&st::foldersChannelsActive,\n\t\t\"\\xF0\\x9F\\x93\\xA2\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersCustom,\n\t\t&st::foldersCustomActive,\n\t\t\"\\xF0\\x9F\\x93\\x81\"_cs.utf16()\n\t},\n\t{\n\t\t&st::foldersSetup,\n\t\t&st::foldersSetupActive,\n\t\t\"\\xF0\\x9F\\x93\\x8B\"_cs.utf16()\n\t},\n\t//{\n\t//\t&st::foldersPoo,\n\t//\t&st::foldersPooActive,\n\t//\t\"\\xF0\\x9F\\x92\\xA9\"_cs.utf16()\n\t//},\n\t{\n\t\t&st::filtersEdit,\n\t\t&st::filtersEdit,\n\t\tQString()\n\t}\n};\n\n} // namespace\n\nconst FilterIcons &LookupFilterIcon(FilterIcon icon) {\n\tExpects(static_cast<int>(icon) < kIcons.size());\n\n\treturn kIcons[static_cast<int>(icon)];\n}\n\nstd::optional<FilterIcon> LookupFilterIconByEmoji(const QString &emoji) {\n\tstatic const auto kMap = [] {\n\t\tauto result = base::flat_map<EmojiPtr, FilterIcon>();\n\t\tauto index = 0;\n\t\tfor (const auto &entry : kIcons) {\n\t\t\tif (entry.emoji.isEmpty()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto emoji = Ui::Emoji::Find(entry.emoji);\n\t\t\tAssert(emoji != nullptr);\n\t\t\tresult.emplace(emoji, static_cast<FilterIcon>(index++));\n\t\t}\n\t\treturn result;\n\t}();\n\tconst auto i = kMap.find(Ui::Emoji::Find(emoji));\n\treturn (i != end(kMap)) ? std::make_optional(i->second) : std::nullopt;\n}\n\nFilterIcon ComputeDefaultFilterIcon(const Data::ChatFilter &filter) {\n\tusing Icon = FilterIcon;\n\tusing Flag = Data::ChatFilter::Flag;\n\n\tconst auto all = Flag::Contacts\n\t\t| Flag::NonContacts\n\t\t| Flag::Groups\n\t\t| Flag::Channels\n\t\t| Flag::Bots;\n\tconst auto removed = Flag::NoRead | Flag::NoMuted;\n\tconst auto people = Flag::Contacts | Flag::NonContacts;\n\tif (!filter.always().empty()\n\t\t|| !filter.never().empty()\n\t\t|| !(filter.flags() & all)) {\n\t\treturn Icon::Custom;\n\t} else if ((filter.flags() & all) == Flag::Contacts\n\t\t|| (filter.flags() & all) == Flag::NonContacts\n\t\t|| (filter.flags() & all) == people) {\n\t\treturn Icon::Private;\n\t} else if ((filter.flags() & all) == Flag::Groups) {\n\t\treturn Icon::Groups;\n\t} else if ((filter.flags() & all) == Flag::Channels) {\n\t\treturn Icon::Channels;\n\t} else if ((filter.flags() & all) == Flag::Bots) {\n\t\treturn Icon::Bots;\n\t} else if ((filter.flags() & removed) == Flag::NoRead) {\n\t\treturn Icon::Unread;\n\t} else if ((filter.flags() & removed) == Flag::NoMuted) {\n\t\treturn Icon::Unmuted;\n\t}\n\treturn Icon::Custom;\n}\n\nFilterIcon ComputeFilterIcon(const Data::ChatFilter &filter) {\n\treturn LookupFilterIconByEmoji(filter.iconEmoji()).value_or(\n\t\tComputeDefaultFilterIcon(filter));\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/filter_icons.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace style {\nnamespace internal {\nclass Icon;\n} // namespace internal\n} // namespace style\n\nnamespace Data {\nclass ChatFilter;\n} // namespace Data\n\nnamespace Ui {\n\nenum class FilterIcon : uchar {\n\tCat,\n\tBook,\n\tMoney,\n\t// Camera,\n\tGame,\n\t// House,\n\tLight,\n\tLike,\n\t// Plus,\n\tNote,\n\tPalette,\n\tTravel,\n\tSport,\n\tFavorite,\n\tStudy,\n\tAirplane,\n\t// Microbe,\n\t// Worker,\n\tPrivate,\n\tGroups,\n\tAll,\n\tUnread,\n\t// Check,\n\tBots,\n\t// Folders,\n\tCrown,\n\tFlower,\n\tHome,\n\tLove,\n\tMask,\n\tParty,\n\tTrade,\n\tWork,\n\tUnmuted,\n\tChannels,\n\tCustom,\n\tSetup,\n\t// Poo,\n\n\tEdit,\n};\n\nstruct FilterIcons {\n\tnot_null<const style::internal::Icon*> normal;\n\tnot_null<const style::internal::Icon*> active;\n\tQString emoji;\n};\n\n[[nodiscard]] const FilterIcons &LookupFilterIcon(FilterIcon icon);\n[[nodiscard]] std::optional<FilterIcon> LookupFilterIconByEmoji(\n\tconst QString &emoji);\n\n[[nodiscard]] FilterIcon ComputeDefaultFilterIcon(\n\tconst Data::ChatFilter &filter);\n[[nodiscard]] FilterIcon ComputeFilterIcon(const Data::ChatFilter &filter);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/filter_icons.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/colors.palette\";\n\nfoldersCat: icon {{ \"folders/folders_cat\", sideBarIconFg }};\nfoldersCatActive: icon {{ \"folders/folders_cat\", sideBarIconFgActive }};\nfoldersBook: icon {{ \"folders/folders_book\", sideBarIconFg }};\nfoldersBookActive: icon {{ \"folders/folders_book\", sideBarIconFgActive }};\nfoldersMoney: icon {{ \"folders/folders_money\", sideBarIconFg }};\nfoldersMoneyActive: icon {{ \"folders/folders_money\", sideBarIconFgActive }};\n// foldersCamera: icon {{ \"folders/folders_camera\", sideBarIconFg }};\n// foldersCameraActive: icon {{ \"folders/folders_camera\", sideBarIconFgActive }};\nfoldersGame: icon {{ \"folders/folders_game\", sideBarIconFg }};\nfoldersGameActive: icon {{ \"folders/folders_game\", sideBarIconFgActive }};\n// foldersHouse: icon {{ \"folders/folders_house\", sideBarIconFg }};\n// foldersHouseActive: icon {{ \"folders/folders_house\", sideBarIconFgActive }};\nfoldersLight: icon {{ \"folders/folders_light\", sideBarIconFg }};\nfoldersLightActive: icon {{ \"folders/folders_light\", sideBarIconFgActive }};\nfoldersLike: icon {{ \"folders/folders_like\", sideBarIconFg }};\nfoldersLikeActive: icon {{ \"folders/folders_like\", sideBarIconFgActive }};\n// foldersPlus: icon {{ \"folders/folders_plus\", sideBarIconFg }};\n// foldersPlusActive: icon {{ \"folders/folders_plus\", sideBarIconFgActive }};\nfoldersNote: icon {{ \"folders/folders_note\", sideBarIconFg }};\nfoldersNoteActive: icon {{ \"folders/folders_note\", sideBarIconFgActive }};\nfoldersPalette: icon {{ \"folders/folders_palette\", sideBarIconFg }};\nfoldersPaletteActive: icon {{ \"folders/folders_palette\", sideBarIconFgActive }};\nfoldersTravel: icon {{ \"folders/folders_travel\", sideBarIconFg }};\nfoldersTravelActive: icon {{ \"folders/folders_travel\", sideBarIconFgActive }};\nfoldersSport: icon {{ \"folders/folders_sport\", sideBarIconFg }};\nfoldersSportActive: icon {{ \"folders/folders_sport\", sideBarIconFgActive }};\nfoldersFavorite: icon {{ \"folders/folders_favorite\", sideBarIconFg }};\nfoldersFavoriteActive: icon {{ \"folders/folders_favorite\", sideBarIconFgActive }};\nfoldersStudy: icon {{ \"folders/folders_study\", sideBarIconFg }};\nfoldersStudyActive: icon {{ \"folders/folders_study\", sideBarIconFgActive }};\nfoldersAirplane: icon {{ \"folders/folders_airplane\", sideBarIconFg }};\nfoldersAirplaneActive: icon {{ \"folders/folders_airplane\", sideBarIconFgActive }};\n// foldersMicrobe: icon {{ \"folders/folders_microbe\", sideBarIconFg }};\n// foldersMicrobeActive: icon {{ \"folders/folders_microbe\", sideBarIconFgActive }};\n// foldersWorker: icon {{ \"folders/folders_worker\", sideBarIconFg }};\n// foldersWorkerActive: icon {{ \"folders/folders_worker\", sideBarIconFgActive }};\nfoldersPrivate: icon {{ \"folders/folders_private\", sideBarIconFg }};\nfoldersPrivateActive: icon {{ \"folders/folders_private\", sideBarIconFgActive }};\nfoldersGroups: icon {{ \"folders/folders_group\", sideBarIconFg }};\nfoldersGroupsActive: icon {{ \"folders/folders_group\", sideBarIconFgActive }};\nfoldersAll: icon {{ \"folders/folders_all\", sideBarIconFg }};\nfoldersAllActive: icon {{ \"folders/folders_all\", sideBarIconFgActive }};\nfoldersUnread: icon {{ \"folders/folders_unread\", sideBarIconFg }};\nfoldersUnreadActive: icon {{ \"folders/folders_unread\", sideBarIconFgActive }};\n// foldersCheck: icon {{ \"folders/folders_check\", sideBarIconFg }};\n// foldersCheckActive: icon {{ \"folders/folders_check\", sideBarIconFgActive }};\nfoldersBots: icon {{ \"folders/folders_bots\", sideBarIconFg }};\nfoldersBotsActive: icon {{ \"folders/folders_bots\", sideBarIconFgActive }};\n// foldersFolders: icon {{ \"folders/folders_folders\", sideBarIconFg }};\n// foldersFoldersActive: icon {{ \"folders/folders_folders\", sideBarIconFgActive }};\nfoldersCrown: icon {{ \"folders/folders_crown\", sideBarIconFg }};\nfoldersCrownActive: icon {{ \"folders/folders_crown\", sideBarIconFgActive }};\nfoldersFlower: icon {{ \"folders/folders_flower\", sideBarIconFg }};\nfoldersFlowerActive: icon {{ \"folders/folders_flower\", sideBarIconFgActive }};\nfoldersHome: icon {{ \"folders/folders_home\", sideBarIconFg }};\nfoldersHomeActive: icon {{ \"folders/folders_home\", sideBarIconFgActive }};\nfoldersLove: icon {{ \"folders/folders_love\", sideBarIconFg }};\nfoldersLoveActive: icon {{ \"folders/folders_love\", sideBarIconFgActive }};\nfoldersMask: icon {{ \"folders/folders_mask\", sideBarIconFg }};\nfoldersMaskActive: icon {{ \"folders/folders_mask\", sideBarIconFgActive }};\nfoldersParty: icon {{ \"folders/folders_party\", sideBarIconFg }};\nfoldersPartyActive: icon {{ \"folders/folders_party\", sideBarIconFgActive }};\nfoldersTrade: icon {{ \"folders/folders_trade\", sideBarIconFg }};\nfoldersTradeActive: icon {{ \"folders/folders_trade\", sideBarIconFgActive }};\nfoldersWork: icon {{ \"folders/folders_work\", sideBarIconFg }};\nfoldersWorkActive: icon {{ \"folders/folders_work\", sideBarIconFgActive }};\nfoldersUnmuted: icon {{ \"folders/folders_unmuted\", sideBarIconFg }};\nfoldersUnmutedActive: icon {{ \"folders/folders_unmuted\", sideBarIconFgActive }};\nfoldersChannels: icon {{ \"folders/folders_channels\", sideBarIconFg }};\nfoldersChannelsActive: icon {{ \"folders/folders_channels\", sideBarIconFgActive }};\nfoldersCustom: icon {{ \"folders/folders_custom\", sideBarIconFg }};\nfoldersCustomActive: icon {{ \"folders/folders_custom\", sideBarIconFgActive }};\nfoldersSetup: icon {{ \"folders/folders_setup\", sideBarIconFg }};\nfoldersSetupActive: icon {{ \"folders/folders_setup\", sideBarIconFgActive }};\nfoldersPoo: icon {{ \"folders/folders_poo\", sideBarIconFg }};\nfoldersPooActive: icon {{ \"folders/folders_poo\", sideBarIconFgActive }};\n\nfiltersEdit: icon {{ \"folders/folders_edit\", sideBarIconFg }};\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/grouped_layout.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/grouped_layout.h\"\n\nnamespace Ui {\nnamespace {\n\nint Round(float64 value) {\n\treturn int(base::SafeRound(value));\n}\n\nclass Layouter {\npublic:\n\tLayouter(\n\t\tconst std::vector<QSize> &sizes,\n\t\tint maxWidth,\n\t\tint minWidth,\n\t\tint spacing);\n\n\tstd::vector<GroupMediaLayout> layout() const;\n\nprivate:\n\tstatic std::vector<float64> CountRatios(const std::vector<QSize> &sizes);\n\tstatic std::string CountProportions(const std::vector<float64> &ratios);\n\n\tstd::vector<GroupMediaLayout> layoutTwo() const;\n\tstd::vector<GroupMediaLayout> layoutThree() const;\n\tstd::vector<GroupMediaLayout> layoutFour() const;\n\n\tstd::vector<GroupMediaLayout> layoutOne() const;\n\tstd::vector<GroupMediaLayout> layoutTwoTopBottom() const;\n\tstd::vector<GroupMediaLayout> layoutTwoLeftRightEqual() const;\n\tstd::vector<GroupMediaLayout> layoutTwoLeftRight() const;\n\tstd::vector<GroupMediaLayout> layoutThreeLeftAndOther() const;\n\tstd::vector<GroupMediaLayout> layoutThreeTopAndOther() const;\n\tstd::vector<GroupMediaLayout> layoutFourLeftAndOther() const;\n\tstd::vector<GroupMediaLayout> layoutFourTopAndOther() const;\n\n\tconst std::vector<QSize> &_sizes;\n\tconst std::vector<float64> _ratios;\n\tconst std::string _proportions;\n\tconst int _count = 0;\n\tconst int _maxWidth = 0;\n\tconst int _maxHeight = 0;\n\tconst int _minWidth = 0;\n\tconst int _spacing = 0;\n\tconst float64 _averageRatio = 1.;\n\tconst float64 _maxSizeRatio = 1.;\n\n};\n\nclass ComplexLayouter {\npublic:\n\tComplexLayouter(\n\t\tconst std::vector<float64> &ratios,\n\t\tfloat64 averageRatio,\n\t\tint maxWidth,\n\t\tint minWidth,\n\t\tint spacing);\n\n\tstd::vector<GroupMediaLayout> layout() const;\n\nprivate:\n\tstruct Attempt {\n\t\tstd::vector<int> lineCounts;\n\t\tstd::vector<float64> heights;\n\t};\n\n\tstatic std::vector<float64> CropRatios(\n\t\tconst std::vector<float64> &ratios,\n\t\tfloat64 averageRatio);\n\n\tconst std::vector<float64> _ratios;\n\tconst int _count = 0;\n\tconst int _maxWidth = 0;\n\tconst int _maxHeight = 0;\n\tconst int _minWidth = 0;\n\tconst int _spacing = 0;\n\tconst float64 _averageRatio = 1.;\n\n};\n\nLayouter::Layouter(\n\tconst std::vector<QSize> &sizes,\n\tint maxWidth,\n\tint minWidth,\n\tint spacing)\n: _sizes(sizes)\n, _ratios(CountRatios(_sizes))\n, _proportions(CountProportions(_ratios))\n, _count(int(_ratios.size()))\n// All apps currently use square max size first.\n// In complex case they use maxWidth * 4 / 3 as maxHeight.\n, _maxWidth(maxWidth)\n, _maxHeight(maxWidth)\n, _minWidth(minWidth)\n, _spacing(spacing)\n, _averageRatio(ranges::accumulate(_ratios, 1.) / _count)\n, _maxSizeRatio(_maxWidth / float64(_maxHeight)) {\n}\n\nstd::vector<float64> Layouter::CountRatios(const std::vector<QSize> &sizes) {\n\treturn ranges::views::all(\n\t\tsizes\n\t) | ranges::views::transform([](const QSize &size) {\n\t\treturn size.width() / float64(size.height());\n\t}) | ranges::to_vector;\n}\n\nstd::string Layouter::CountProportions(const std::vector<float64> &ratios) {\n\treturn ranges::views::all(\n\t\tratios\n\t) | ranges::views::transform([](float64 ratio) {\n\t\treturn (ratio > 1.2) ? 'w' : (ratio < 0.8) ? 'n' : 'q';\n\t}) | ranges::to<std::string>();\n}\n\nstd::vector<GroupMediaLayout> Layouter::layout() const {\n\tif (!_count) {\n\t\treturn {};\n\t} else if (_count == 1) {\n\t\treturn layoutOne();\n\t}\n\n\tusing namespace rpl::mappers;\n\tif (_count >= 5 || ranges::find_if(_ratios, _1 > 2) != _ratios.end()) {\n\t\treturn ComplexLayouter(\n\t\t\t_ratios,\n\t\t\t_averageRatio,\n\t\t\t_maxWidth,\n\t\t\t_minWidth,\n\t\t\t_spacing).layout();\n\t}\n\n\tif (_count == 2) {\n\t\treturn layoutTwo();\n\t} else if (_count == 3) {\n\t\treturn layoutThree();\n\t}\n\treturn layoutFour();\n}\n\nstd::vector<GroupMediaLayout> Layouter::layoutTwo() const {\n\tExpects(_count == 2);\n\n\tif ((_proportions == \"ww\")\n\t\t&& (_averageRatio > 1.4 * _maxSizeRatio)\n\t\t&& (_ratios[1] - _ratios[0] < 0.2)) {\n\t\treturn layoutTwoTopBottom();\n\t} else if (_proportions == \"ww\" || _proportions == \"qq\") {\n\t\treturn layoutTwoLeftRightEqual();\n\t}\n\treturn layoutTwoLeftRight();\n}\n\nstd::vector<GroupMediaLayout> Layouter::layoutThree() const {\n\tExpects(_count == 3);\n\n\tauto result = std::vector<GroupMediaLayout>(_count);\n\tif (_proportions[0] == 'n') {\n\t\treturn layoutThreeLeftAndOther();\n\t}\n\treturn layoutThreeTopAndOther();\n}\n\nstd::vector<GroupMediaLayout> Layouter::layoutFour() const {\n\tExpects(_count == 4);\n\n\tauto result = std::vector<GroupMediaLayout>(_count);\n\tif (_proportions[0] == 'w') {\n\t\treturn layoutFourTopAndOther();\n\t}\n\treturn layoutFourLeftAndOther();\n}\n\nstd::vector<GroupMediaLayout> Layouter::layoutOne() const {\n\tExpects(_count == 1);\n\n\tconst auto width = _maxWidth;\n\tconst auto height = (_sizes[0].height() * width) / _sizes[0].width();\n\n\treturn {\n\t\t{\n\t\t\tQRect(0, 0, width, height),\n\t\t\tRectPart::Left | RectPart::Top | RectPart::Right | RectPart::Bottom\n\t\t},\n\t};\n}\n\nstd::vector<GroupMediaLayout> Layouter::layoutTwoTopBottom() const {\n\tExpects(_count == 2);\n\n\tconst auto width = _maxWidth;\n\tconst auto height = Round(std::min(\n\t\twidth / _ratios[0],\n\t\tstd::min(\n\t\t\twidth / _ratios[1],\n\t\t\t(_maxHeight - _spacing) / 2.)));\n\n\treturn {\n\t\t{\n\t\t\tQRect(0, 0, width, height),\n\t\t\tRectPart::Left | RectPart::Top | RectPart::Right\n\t\t},\n\t\t{\n\t\t\tQRect(0, height + _spacing, width, height),\n\t\t\tRectPart::Left | RectPart::Bottom | RectPart::Right\n\t\t},\n\t};\n}\n\nstd::vector<GroupMediaLayout> Layouter::layoutTwoLeftRightEqual() const {\n\tExpects(_count == 2);\n\n\tconst auto width = (_maxWidth - _spacing) / 2;\n\tconst auto height = Round(std::min(\n\t\twidth / _ratios[0],\n\t\tstd::min(width / _ratios[1], _maxHeight * 1.)));\n\n\treturn {\n\t\t{\n\t\t\tQRect(0, 0, width, height),\n\t\t\tRectPart::Top | RectPart::Left | RectPart::Bottom\n\t\t},\n\t\t{\n\t\t\tQRect(width + _spacing, 0, width, height),\n\t\t\tRectPart::Top | RectPart::Right | RectPart::Bottom\n\t\t},\n\t};\n}\n\nstd::vector<GroupMediaLayout> Layouter::layoutTwoLeftRight() const {\n\tExpects(_count == 2);\n\n\tconst auto minimalWidth = Round(_minWidth * 1.5);\n\tconst auto secondWidth = std::min(\n\t\tRound(std::max(\n\t\t\t0.4 * (_maxWidth - _spacing),\n\t\t\t(_maxWidth - _spacing) / _ratios[0]\n\t\t\t\t/ (1. / _ratios[0] + 1. / _ratios[1]))),\n\t\t_maxWidth - _spacing - minimalWidth);\n\tconst auto firstWidth = _maxWidth\n\t\t- secondWidth\n\t\t- _spacing;\n\tconst auto height = std::min(\n\t\t_maxHeight,\n\t\tRound(std::min(\n\t\t\tfirstWidth / _ratios[0],\n\t\t\tsecondWidth / _ratios[1])));\n\n\treturn {\n\t\t{\n\t\t\tQRect(0, 0, firstWidth, height),\n\t\t\tRectPart::Top | RectPart::Left | RectPart::Bottom\n\t\t},\n\t\t{\n\t\t\tQRect(firstWidth + _spacing, 0, secondWidth, height),\n\t\t\tRectPart::Top | RectPart::Right | RectPart::Bottom\n\t\t},\n\t};\n}\n\nstd::vector<GroupMediaLayout> Layouter::layoutThreeLeftAndOther() const {\n\tExpects(_count == 3);\n\n\tconst auto firstHeight = _maxHeight;\n\tconst auto thirdHeight = Round(std::min(\n\t\t(_maxHeight - _spacing) / 2.,\n\t\t(_ratios[1] * (_maxWidth - _spacing)\n\t\t\t/ (_ratios[2] + _ratios[1]))));\n\tconst auto secondHeight = firstHeight\n\t\t- thirdHeight\n\t\t- _spacing;\n\tconst auto rightWidth = std::max(\n\t\t_minWidth,\n\t\tRound(std::min(\n\t\t\t(_maxWidth - _spacing) / 2.,\n\t\t\tstd::min(\n\t\t\t\tthirdHeight * _ratios[2],\n\t\t\t\tsecondHeight * _ratios[1]))));\n\tconst auto leftWidth = std::min(\n\t\tRound(firstHeight * _ratios[0]),\n\t\t_maxWidth - _spacing - rightWidth);\n\n\treturn {\n\t\t{\n\t\t\tQRect(0, 0, leftWidth, firstHeight),\n\t\t\tRectPart::Top | RectPart::Left | RectPart::Bottom\n\t\t},\n\t\t{\n\t\t\tQRect(leftWidth + _spacing, 0, rightWidth, secondHeight),\n\t\t\tRectPart::Top | RectPart::Right\n\t\t},\n\t\t{\n\t\t\tQRect(leftWidth + _spacing, secondHeight + _spacing, rightWidth, thirdHeight),\n\t\t\tRectPart::Bottom | RectPart::Right\n\t\t},\n\t};\n}\n\nstd::vector<GroupMediaLayout> Layouter::layoutThreeTopAndOther() const {\n\tExpects(_count == 3);\n\n\tconst auto firstWidth = _maxWidth;\n\tconst auto firstHeight = Round(std::min(\n\t\tfirstWidth / _ratios[0],\n\t\t(_maxHeight - _spacing) * 0.66));\n\tconst auto secondWidth = (_maxWidth - _spacing) / 2;\n\tconst auto secondHeight = std::min(\n\t\t_maxHeight - firstHeight - _spacing,\n\t\tRound(std::min(\n\t\t\tsecondWidth / _ratios[1],\n\t\t\tsecondWidth / _ratios[2])));\n\tconst auto thirdWidth = firstWidth - secondWidth - _spacing;\n\n\treturn {\n\t\t{\n\t\t\tQRect(0, 0, firstWidth, firstHeight),\n\t\t\tRectPart::Left | RectPart::Top | RectPart::Right\n\t\t},\n\t\t{\n\t\t\tQRect(0, firstHeight + _spacing, secondWidth, secondHeight),\n\t\t\tRectPart::Bottom | RectPart::Left\n\t\t},\n\t\t{\n\t\t\tQRect(secondWidth + _spacing, firstHeight + _spacing, thirdWidth, secondHeight),\n\t\t\tRectPart::Bottom | RectPart::Right\n\t\t},\n\t};\n}\n\nstd::vector<GroupMediaLayout> Layouter::layoutFourTopAndOther() const {\n\tExpects(_count == 4);\n\n\tconst auto w = _maxWidth;\n\tconst auto h0 = Round(std::min(\n\t\tw / _ratios[0],\n\t\t(_maxHeight - _spacing) * 0.66));\n\tconst auto h = Round(\n\t\t(_maxWidth - 2 * _spacing)\n\t\t\t/ (_ratios[1] + _ratios[2] + _ratios[3]));\n\tconst auto w0 = std::max(\n\t\t_minWidth,\n\t\tRound(std::min(\n\t\t\t(_maxWidth - 2 * _spacing) * 0.4,\n\t\t\th * _ratios[1])));\n\tconst auto w2 = Round(std::max(\n\t\tstd::max(\n\t\t\t_minWidth * 1.,\n\t\t\t(_maxWidth - 2 * _spacing) * 0.33),\n\t\th * _ratios[3]));\n\tconst auto w1 = w - w0 - w2 - 2 * _spacing;\n\tconst auto h1 = std::min(\n\t\t_maxHeight - h0 - _spacing,\n\t\th);\n\n\treturn {\n\t\t{\n\t\t\tQRect(0, 0, w, h0),\n\t\t\tRectPart::Left | RectPart::Top | RectPart::Right\n\t\t},\n\t\t{\n\t\t\tQRect(0, h0 + _spacing, w0, h1),\n\t\t\tRectPart::Bottom | RectPart::Left\n\t\t},\n\t\t{\n\t\t\tQRect(w0 + _spacing, h0 + _spacing, w1, h1),\n\t\t\tRectPart::Bottom,\n\t\t},\n\t\t{\n\t\t\tQRect(w0 + _spacing + w1 + _spacing, h0 + _spacing, w2, h1),\n\t\t\tRectPart::Right | RectPart::Bottom\n\t\t},\n\t};\n}\n\nstd::vector<GroupMediaLayout> Layouter::layoutFourLeftAndOther() const {\n\tExpects(_count == 4);\n\n\tconst auto h = _maxHeight;\n\tconst auto w0 = Round(std::min(\n\t\th * _ratios[0],\n\t\t(_maxWidth - _spacing) * 0.6));\n\n\tconst auto w = Round(\n\t\t(_maxHeight - 2 * _spacing)\n\t\t\t/ (1. / _ratios[1] + 1. / _ratios[2] + 1. / _ratios[3])\n\t);\n\tconst auto h0 = Round(w / _ratios[1]);\n\tconst auto h1 = Round(w / _ratios[2]);\n\tconst auto h2 = h - h0 - h1 - 2 * _spacing;\n\tconst auto w1 = std::max(\n\t\t_minWidth,\n\t\tstd::min(_maxWidth - w0 - _spacing, w));\n\n\treturn {\n\t\t{\n\t\t\tQRect(0, 0, w0, h),\n\t\t\tRectPart::Top | RectPart::Left | RectPart::Bottom\n\t\t},\n\t\t{\n\t\t\tQRect(w0 + _spacing, 0, w1, h0),\n\t\t\tRectPart::Top | RectPart::Right\n\t\t},\n\t\t{\n\t\t\tQRect(w0 + _spacing, h0 + _spacing, w1, h1),\n\t\t\tRectPart::Right\n\t\t},\n\t\t{\n\t\t\tQRect(w0 + _spacing, h0 + h1 + 2 * _spacing, w1, h2),\n\t\t\tRectPart::Bottom | RectPart::Right\n\t\t},\n\t};\n}\n\nComplexLayouter::ComplexLayouter(\n\tconst std::vector<float64> &ratios,\n\tfloat64 averageRatio,\n\tint maxWidth,\n\tint minWidth,\n\tint spacing)\n: _ratios(CropRatios(ratios, averageRatio))\n, _count(int(_ratios.size()))\n// All apps currently use square max size first.\n// In complex case they use maxWidth * 4 / 3 as maxHeight.\n, _maxWidth(maxWidth)\n, _maxHeight(maxWidth * 4 / 3)\n, _minWidth(minWidth)\n, _spacing(spacing)\n, _averageRatio(averageRatio) {\n}\n\nstd::vector<float64> ComplexLayouter::CropRatios(\n\t\tconst std::vector<float64> &ratios,\n\t\tfloat64 averageRatio) {\n\treturn ranges::views::all(\n\t\tratios\n\t) | ranges::views::transform([&](float64 ratio) {\n\t\tconstexpr auto kMaxRatio = 2.75;\n\t\tconstexpr auto kMinRatio = 0.6667;\n\t\treturn (averageRatio > 1.1)\n\t\t\t? std::clamp(ratio, 1., kMaxRatio)\n\t\t\t: std::clamp(ratio, kMinRatio, 1.);\n\t}) | ranges::to_vector;\n}\n\nstd::vector<GroupMediaLayout> ComplexLayouter::layout() const {\n\tExpects(_count > 1);\n\n\tauto result = std::vector<GroupMediaLayout>(_count);\n\n\tauto attempts = std::vector<Attempt>();\n\tconst auto multiHeight = [&](int offset, int count) {\n\t\tconst auto ratios = gsl::make_span(_ratios).subspan(offset, count);\n\t\tconst auto sum = ranges::accumulate(ratios, 0.);\n\t\treturn (_maxWidth - (count - 1) * _spacing) / sum;\n\t};\n\tconst auto pushAttempt = [&](std::vector<int> lineCounts) {\n\t\tauto heights = std::vector<float64>();\n\t\theights.reserve(lineCounts.size());\n\t\tauto offset = 0;\n\t\tfor (auto count : lineCounts) {\n\t\t\theights.push_back(multiHeight(offset, count));\n\t\t\toffset += count;\n\t\t}\n\t\tattempts.push_back({ std::move(lineCounts), std::move(heights) });\n\t};\n\n\tfor (auto first = 1; first != _count; ++first) {\n\t\tconst auto second = _count - first;\n\t\tif (first > 3 || second > 3) {\n\t\t\tcontinue;\n\t\t}\n\t\tpushAttempt({ first, second });\n\t}\n\tfor (auto first = 1; first != _count - 1; ++first) {\n\t\tfor (auto second = 1; second != _count - first; ++second) {\n\t\t\tconst auto third = _count - first - second;\n\t\t\tif ((first > 3)\n\t\t\t\t|| (second > ((_averageRatio < 0.85) ? 4 : 3))\n\t\t\t\t|| (third > 3)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tpushAttempt({ first, second, third });\n\t\t}\n\t}\n\tfor (auto first = 1; first != _count - 1; ++first) {\n\t\tfor (auto second = 1; second != _count - first; ++second) {\n\t\t\tfor (auto third = 1; third != _count - first - second; ++third) {\n\t\t\t\tconst auto fourth = _count - first - second - third;\n\t\t\t\tif (first > 3 || second > 3 || third > 3 || fourth > 3) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tpushAttempt({ first, second, third, fourth });\n\t\t\t}\n\t\t}\n\t}\n\n\tauto optimalAttempt = (const Attempt*)nullptr;\n\tauto optimalDiff = 0.;\n\tfor (const auto &attempt : attempts) {\n\t\tconst auto &heights = attempt.heights;\n\t\tconst auto &counts = attempt.lineCounts;\n\t\tconst auto lineCount = int(counts.size());\n\t\tconst auto totalHeight = ranges::accumulate(heights, 0.)\n\t\t\t+ _spacing * (lineCount - 1);\n\t\tconst auto minLineHeight = ranges::min(heights);\n\t\tconst auto bad1 = (minLineHeight < _minWidth) ? 1.5 : 1.;\n\t\tconst auto bad2 = [&] {\n\t\t\tfor (auto line = 1; line != lineCount; ++line) {\n\t\t\t\tif (counts[line - 1] > counts[line]) {\n\t\t\t\t\treturn 1.5;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn 1.;\n\t\t}();\n\t\tconst auto diff = std::abs(totalHeight - _maxHeight) * bad1 * bad2;\n\t\tif (!optimalAttempt || diff < optimalDiff) {\n\t\t\toptimalAttempt = &attempt;\n\t\t\toptimalDiff = diff;\n\t\t}\n\t}\n\tAssert(optimalAttempt != nullptr);\n\n\tconst auto &optimalCounts = optimalAttempt->lineCounts;\n\tconst auto &optimalHeights = optimalAttempt->heights;\n\tconst auto rowCount = int(optimalCounts.size());\n\n\tauto index = 0;\n\tauto y = 0.;\n\tfor (auto row = 0; row != rowCount; ++row) {\n\t\tconst auto colCount = optimalCounts[row];\n\t\tconst auto lineHeight = optimalHeights[row];\n\t\tconst auto height = Round(lineHeight);\n\n\t\tauto x = 0;\n\t\tfor (auto col = 0; col != colCount; ++col) {\n\t\t\tconst auto sides = RectPart::None\n\t\t\t\t| (row == 0 ? RectPart::Top : RectPart::None)\n\t\t\t\t| (row == rowCount - 1 ? RectPart::Bottom : RectPart::None)\n\t\t\t\t| (col == 0 ? RectPart::Left : RectPart::None)\n\t\t\t\t| (col == colCount - 1 ? RectPart::Right : RectPart::None);\n\n\t\t\tconst auto ratio = _ratios[index];\n\t\t\tconst auto width = (col == colCount - 1)\n\t\t\t\t? (_maxWidth - x)\n\t\t\t\t: Round(ratio * lineHeight);\n\t\t\tresult[index] = {\n\t\t\t\tQRect(x, y, width, height),\n\t\t\t\tsides\n\t\t\t};\n\n\t\t\tx += width + _spacing;\n\t\t\t++index;\n\t\t}\n\t\ty += height + _spacing;\n\t}\n\n\treturn result;\n}\n\n} // namespace\n\nstd::vector<GroupMediaLayout> LayoutMediaGroup(\n\t\tconst std::vector<QSize> &sizes,\n\t\tint maxWidth,\n\t\tint minWidth,\n\t\tint spacing) {\n\treturn Layouter(sizes, maxWidth, minWidth, spacing).layout();\n}\n\nRectParts GetCornersFromSides(RectParts sides) {\n\tconst auto convert = [&](\n\t\t\tRectPart side1,\n\t\t\tRectPart side2,\n\t\t\tRectPart corner) {\n\t\treturn ((sides & side1) && (sides & side2))\n\t\t\t? corner\n\t\t\t: RectPart::None;\n\t};\n\treturn RectPart::None\n\t\t| convert(RectPart::Top, RectPart::Left, RectPart::TopLeft)\n\t\t| convert(RectPart::Top, RectPart::Right, RectPart::TopRight)\n\t\t| convert(RectPart::Bottom, RectPart::Left, RectPart::BottomLeft)\n\t\t| convert(RectPart::Bottom, RectPart::Right, RectPart::BottomRight);\n}\n\nQSize GetImageScaleSizeForGeometry(QSize original, QSize geometry) {\n\tconst auto width = geometry.width();\n\tconst auto height = geometry.height();\n\tauto tw = original.width();\n\tauto th = original.height();\n\tif (tw * height > th * width) {\n\t\tif (th > height || tw * height < 2 * th * width) {\n\t\t\ttw = (height * tw) / th;\n\t\t\tth = height;\n\t\t} else if (tw < width) {\n\t\t\tth = (width * th) / tw;\n\t\t\ttw = width;\n\t\t}\n\t} else {\n\t\tif (tw > width || th * width < 2 * tw * height) {\n\t\t\tth = (width * th) / tw;\n\t\t\ttw = width;\n\t\t} else if (tw > 0 && th < height) {\n\t\t\ttw = (height * tw) / th;\n\t\t\tth = height;\n\t\t}\n\t}\n\tif (tw < 1) tw = 1;\n\tif (th < 1) th = 1;\n\treturn { tw, th };\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/grouped_layout.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rect_part.h\"\n\nnamespace Ui {\n\nstruct GroupMediaLayout {\n\tQRect geometry;\n\tRectParts sides = RectPart::None;\n};\n\nstd::vector<GroupMediaLayout> LayoutMediaGroup(\n\tconst std::vector<QSize> &sizes,\n\tint maxWidth,\n\tint minWidth,\n\tint spacing);\n\nRectParts GetCornersFromSides(RectParts sides);\nQSize GetImageScaleSizeForGeometry(QSize original, QSize geometry);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/image/image.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/image/image.h\"\n\n#include \"storage/cache/storage_cache_database.h\"\n#include \"data/data_session.h\"\n#include \"main/main_session.h\"\n#include \"ui/ui_utility.h\"\n#include \"core/application.h\"\n\nusing namespace Images;\n\nnamespace {\n\n[[nodiscard]] uint64 PixKey(int width, int height, Options options) {\n\treturn static_cast<uint64>(width)\n\t\t| (static_cast<uint64>(height) << 24)\n\t\t| (static_cast<uint64>(options) << 48);\n}\n\n[[nodiscard]] uint64 SinglePixKey(Options options) {\n\treturn PixKey(0, 0, options);\n}\n\n[[nodiscard]] Options OptionsByArgs(const PrepareArgs &args) {\n\treturn args.options | (args.colored ? Option::Colorize : Option::None);\n}\n\n[[nodiscard]] uint64 PixKey(int width, int height, const PrepareArgs &args) {\n\treturn PixKey(width, height, OptionsByArgs(args));\n}\n\n[[nodiscard]] uint64 SinglePixKey(const PrepareArgs &args) {\n\treturn SinglePixKey(OptionsByArgs(args));\n}\n\n} // namespace\n\nImage::Image(const QString &path)\n: Image(Read({ .path = path }).image) {\n}\n\nImage::Image(const QByteArray &content)\n: Image(Read({ .content = content }).image) {\n}\n\nImage::Image(QImage &&data)\n: _data(data.isNull() ? Empty()->original() : std::move(data)) {\n\tExpects(!_data.isNull());\n}\n\nnot_null<Image*> Image::Empty() {\n\tstatic auto result = Image([] {\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tauto data = QImage(\n\t\t\tfactor,\n\t\t\tfactor,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tdata.fill(Qt::transparent);\n\t\tdata.setDevicePixelRatio(style::DevicePixelRatio());\n\t\treturn data;\n\t}());\n\treturn &result;\n}\n\nnot_null<Image*> Image::BlankMedia() {\n\tstatic auto result = Image([] {\n\t\tconst auto factor = style::DevicePixelRatio();\n\t\tauto data = QImage(\n\t\t\tfactor,\n\t\t\tfactor,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tdata.fill(Qt::black);\n\t\tdata.setDevicePixelRatio(style::DevicePixelRatio());\n\t\treturn data;\n\t}());\n\treturn &result;\n}\n\nQImage Image::original() const {\n\treturn _data;\n}\n\nconst QPixmap &Image::cached(\n\t\tint w,\n\t\tint h,\n\t\tconst Images::PrepareArgs &args,\n\t\tbool single) const {\n\tconst auto ratio = style::DevicePixelRatio();\n\tif (w <= 0 || !width() || !height()) {\n\t\tw = width();\n\t} else if (h <= 0) {\n\t\th = std::max(int(int64(height()) * w / width()), 1) * ratio;\n\t\tw *= ratio;\n\t} else {\n\t\tw *= ratio;\n\t\th *= ratio;\n\t}\n\tconst auto outer = args.outer;\n\tconst auto size = outer.isEmpty() ? QSize(w, h) : outer * ratio;\n\tconst auto k = single ? SinglePixKey(args) : PixKey(w, h, args);\n\tconst auto i = _cache.find(k);\n\treturn (i != _cache.cend() && i->second.size() == size)\n\t\t? i->second\n\t\t: _cache.emplace_or_assign(k, prepare(w, h, args)).first->second;\n}\n\nQPixmap Image::prepare(int w, int h, const Images::PrepareArgs &args) const {\n\tif (_data.isNull()) {\n\t\tif (h <= 0 && height() > 0) {\n\t\t\th = qRound(width() * w / float64(height()));\n\t\t}\n\t\treturn Empty()->prepare(w, h, args);\n\t}\n\n\tauto outer = args.outer;\n\tif (!isNull() || outer.isEmpty()) {\n\t\treturn Ui::PixmapFromImage(Prepare(_data, w, h, args));\n\t}\n\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto outerw = outer.width() * ratio;\n\tconst auto outerh = outer.height() * ratio;\n\n\tauto result = QImage(\n\t\tQSize(outerw, outerh),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(ratio);\n\n\tauto p = QPainter(&result);\n\tif (w < outerw) {\n\t\tp.fillRect(0, 0, (outerw - w) / 2, result.height(), Qt::black);\n\t\tp.fillRect(((outerw - w) / 2) + w, 0, result.width() - (((outerw - w) / 2) + w), result.height(), Qt::black);\n\t}\n\tif (h < outerh) {\n\t\tp.fillRect(qMax(0, (outerw - w) / 2), 0, qMin(result.width(), w), (outerh - h) / 2, Qt::black);\n\t\tp.fillRect(qMax(0, (outerw - w) / 2), ((outerh - h) / 2) + h, qMin(result.width(), w), result.height() - (((outerh - h) / 2) + h), Qt::black);\n\t}\n\tp.fillRect(qMax(0, (outerw - w) / 2), qMax(0, (outerh - h) / 2), qMin(result.width(), w), qMin(result.height(), h), Qt::white);\n\tp.end();\n\n\tresult = Round(std::move(result), args.options);\n\tif (args.colored) {\n\t\tresult = Colored(std::move(result), *args.colored);\n\t}\n\treturn Ui::PixmapFromImage(std::move(result));\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/image/image.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/image/image_prepare.h\"\n\nclass QPainterPath;\n\nclass Image final {\npublic:\n\texplicit Image(const QString &path);\n\texplicit Image(const QByteArray &content);\n\texplicit Image(QImage &&data);\n\n\t[[nodiscard]] static not_null<Image*> Empty(); // 1x1 transparent\n\t[[nodiscard]] static not_null<Image*> BlankMedia(); // 1x1 black\n\n\t[[nodiscard]] int width() const {\n\t\treturn _data.width();\n\t}\n\t[[nodiscard]] int height() const {\n\t\treturn _data.height();\n\t}\n\t[[nodiscard]] QSize size() const {\n\t\treturn { width(), height() };\n\t}\n\n\t[[nodiscard]] bool isNull() const {\n\t\treturn (this == Empty());\n\t}\n\n\t[[nodiscard]] QImage original() const;\n\n\t[[nodiscard]] const QPixmap &pix(\n\t\t\tQSize size,\n\t\t\tconst Images::PrepareArgs &args = {}) const {\n\t\treturn cached(size.width(), size.height(), args, false);\n\t}\n\t[[nodiscard]] const QPixmap &pix(\n\t\t\tint w,\n\t\t\tint h,\n\t\t\tconst Images::PrepareArgs &args = {}) const {\n\t\treturn cached(w, h, args, false);\n\t}\n\t[[nodiscard]] const QPixmap &pix(\n\t\t\tint w = 0,\n\t\t\tconst Images::PrepareArgs &args = {}) const {\n\t\treturn cached(w, 0, args, false);\n\t}\n\n\t[[nodiscard]] const QPixmap &pixSingle(\n\t\t\tQSize size,\n\t\t\tconst Images::PrepareArgs &args = {}) const {\n\t\treturn cached(size.width(), size.height(), args, true);\n\t}\n\t[[nodiscard]] const QPixmap &pixSingle(\n\t\t\tint w,\n\t\t\tint h,\n\t\t\tconst Images::PrepareArgs &args = {}) const {\n\t\treturn cached(w, h, args, true);\n\t}\n\t[[nodiscard]] const QPixmap &pixSingle(\n\t\t\tint w = 0,\n\t\t\tconst Images::PrepareArgs &args = {}) const {\n\t\treturn cached(w, 0, args, true);\n\t}\n\n\t[[nodiscard]] QPixmap pixNoCache(\n\t\t\tQSize size,\n\t\t\tconst Images::PrepareArgs &args = {}) const {\n\t\treturn prepare(size.width(), size.height(), args);\n\t}\n\t[[nodiscard]] QPixmap pixNoCache(\n\t\t\tint w,\n\t\t\tint h,\n\t\t\tconst Images::PrepareArgs &args = {}) const {\n\t\treturn prepare(w, h, args);\n\t}\n\t[[nodiscard]] QPixmap pixNoCache(\n\t\t\tint w = 0,\n\t\t\tconst Images::PrepareArgs &args = {}) const {\n\t\treturn prepare(w, 0, args);\n\t}\n\nprivate:\n\t[[nodiscard]] QPixmap prepare(\n\t\tint w,\n\t\tint h,\n\t\tconst Images::PrepareArgs &args) const;\n\t[[nodiscard]] const QPixmap &cached(\n\t\tint w,\n\t\tint h,\n\t\tconst Images::PrepareArgs &args,\n\t\tbool single) const;\n\n\tconst QImage _data;\n\tmutable base::flat_map<uint64, QPixmap> _cache;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/image/image_location.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/image/image_location.h\"\n\n#include \"ui/image/image.h\"\n#include \"platform/platform_specific.h\"\n#include \"storage/cache/storage_cache_types.h\"\n#include \"storage/serialize_common.h\"\n#include \"data/data_file_origin.h\"\n#include \"base/overload.h\"\n#include \"main/main_session.h\"\n\n#include <QtCore/QBuffer>\n\nnamespace {\n\nconstexpr auto kDocumentBaseCacheTag = 0x0000000000010000ULL;\nconstexpr auto kDocumentBaseCacheMask = 0x000000000000FF00ULL;\nconstexpr auto kPhotoBaseCacheTag = 0x0000000000020000ULL;\nconstexpr auto kPhotoBaseCacheMask = 0x000000000000FF00ULL;\n\nconstexpr auto kNonStorageLocationToken = quint8(0x10);\nconstexpr auto kLegacyInMessagePeerIdFlag = quint8(0x08);\nconstexpr auto kModernLocationFlag = quint8(0x20);\nconstexpr auto kInMessageFieldsFlag = quint8(0x40);\n\nenum class NonStorageLocationType : quint8 {\n\tWeb,\n\tGeo,\n\tUrl,\n\tMemory,\n\tAudioAlbumThumb,\n};\n\nMTPInputPeer GenerateInputPeer(\n\t\tPeerId id,\n\t\tuint64 accessHash,\n\t\tPeerId inMessagePeerId,\n\t\tint32 inMessageId,\n\t\tUserId self) {\n\tconst auto bareId = [&] {\n\t\treturn peerToBareMTPInt(id);\n\t};\n\tif (inMessageId && peerIsUser(inMessagePeerId)) {\n\t\treturn MTP_inputPeerUserFromMessage(\n\t\t\tGenerateInputPeer(id, accessHash, 0, 0, self),\n\t\t\tMTP_int(inMessageId),\n\t\t\tMTP_long(peerToUser(inMessagePeerId).bare));\n\t} else if (inMessageId && peerIsChannel(inMessagePeerId)) {\n\t\treturn MTP_inputPeerChannelFromMessage(\n\t\t\tGenerateInputPeer(id, accessHash, 0, 0, self),\n\t\t\tMTP_int(inMessageId),\n\t\t\tMTP_long(peerToChannel(inMessagePeerId).bare));\n\t} else if (!id) {\n\t\treturn MTP_inputPeerEmpty();\n\t} else if (id == peerFromUser(self)) {\n\t\treturn MTP_inputPeerSelf();\n\t} else if (peerIsUser(id)) {\n\t\treturn MTP_inputPeerUser(bareId(), MTP_long(accessHash));\n\t} else if (peerIsChat(id)) {\n\t\treturn MTP_inputPeerChat(bareId());\n\t} else if (peerIsChannel(id)) {\n\t\treturn MTP_inputPeerChannel(bareId(), MTP_long(accessHash));\n\t} else {\n\t\treturn MTP_inputPeerEmpty();\n\t}\n}\n\n} // namespace\n\nWebFileLocation WebFileLocation::Null;\n\nStorageFileLocation::StorageFileLocation(\n\tint32 dcId,\n\tUserId self,\n\tconst MTPInputFileLocation &tl)\n: _dcId(dcId) {\n\ttl.match([&](const MTPDinputFileLocation &data) {\n\t\t_type = Type::Legacy;\n\t\t_volumeId = data.vvolume_id().v;\n\t\t_localId = data.vlocal_id().v;\n\t\t_accessHash = data.vsecret().v;\n\t\t_fileReference = data.vfile_reference().v;\n\t}, [&](const MTPDinputEncryptedFileLocation &data) {\n\t\t_type = Type::Encrypted;\n\t\t_id = data.vid().v;\n\t\t_accessHash = data.vaccess_hash().v;\n\t}, [&](const MTPDinputDocumentFileLocation &data) {\n\t\t_type = Type::Document;\n\t\t_id = data.vid().v;\n\t\t_accessHash = data.vaccess_hash().v;\n\t\t_fileReference = data.vfile_reference().v;\n\t\t_sizeLetter = data.vthumb_size().v.isEmpty()\n\t\t\t? uint8(0)\n\t\t\t: uint8(data.vthumb_size().v[0]);\n\t}, [&](const MTPDinputSecureFileLocation &data) {\n\t\t_type = Type::Secure;\n\t\t_id = data.vid().v;\n\t\t_accessHash = data.vaccess_hash().v;\n\t}, [&](const MTPDinputTakeoutFileLocation &data) {\n\t\t_type = Type::Takeout;\n\t}, [&](const MTPDinputPhotoFileLocation &data) {\n\t\t_type = Type::Photo;\n\t\t_id = data.vid().v;\n\t\t_accessHash = data.vaccess_hash().v;\n\t\t_fileReference = data.vfile_reference().v;\n\t\t_sizeLetter = data.vthumb_size().v.isEmpty()\n\t\t\t? char(0)\n\t\t\t: data.vthumb_size().v[0];\n\t}, [&](const MTPDinputPhotoLegacyFileLocation &data) {\n\t\t_type = Type::Legacy;\n\t\t_volumeId = data.vvolume_id().v;\n\t\t_localId = data.vlocal_id().v;\n\t\t_accessHash = data.vsecret().v;\n\t\t_fileReference = data.vfile_reference().v;\n\t}, [&](const MTPDinputPeerPhotoFileLocation &data) {\n\t\t_type = Type::PeerPhoto;\n\t\tconst auto fillPeer = base::overload([&](\n\t\t\t\tconst MTPDinputPeerEmpty &data) {\n\t\t\t_id = 0;\n\t\t}, [&](const MTPDinputPeerSelf &data) {\n\t\t\t_id = peerFromUser(self).value;\n\t\t}, [&](const MTPDinputPeerChat &data) {\n\t\t\t_id = peerFromChat(data.vchat_id()).value;\n\t\t}, [&](const MTPDinputPeerUser &data) {\n\t\t\t_id = peerFromUser(data.vuser_id()).value;\n\t\t\t_accessHash = data.vaccess_hash().v;\n\t\t}, [&](const MTPDinputPeerChannel &data) {\n\t\t\t_id = peerFromChannel(data.vchannel_id()).value;\n\t\t\t_accessHash = data.vaccess_hash().v;\n\t\t});\n\t\tdata.vpeer().match(fillPeer, [&](\n\t\t\t\tconst MTPDinputPeerUserFromMessage &data) {\n\t\t\tdata.vpeer().match(fillPeer, [&](auto &&) {\n\t\t\t\t// Bad data provided.\n\t\t\t\t_id = _accessHash = 0;\n\t\t\t});\n\t\t\t_inMessagePeerId = peerFromUser(data.vuser_id());\n\t\t\t_inMessageId = data.vmsg_id().v;\n\t\t}, [&](const MTPDinputPeerChannelFromMessage &data) {\n\t\t\tdata.vpeer().match(fillPeer, [&](auto &&) {\n\t\t\t\t// Bad data provided.\n\t\t\t\t_id = _accessHash = 0;\n\t\t\t});\n\t\t\t_inMessagePeerId = peerFromChannel(data.vchannel_id());\n\t\t\t_inMessageId = data.vmsg_id().v;\n\t\t});\n\t\t_volumeId = data.vphoto_id().v;\n\t\t_sizeLetter = data.is_big() ? 'c' : 'a';\n\n\t\t// _localId place is used in serialization.\n\t\tEnsures(_localId == 0);\n\t}, [&](const MTPDinputStickerSetThumb &data) {\n\t\t_type = Type::StickerSetThumb;\n\t\tdata.vstickerset().match([&](const MTPDinputStickerSetEmpty &data) {\n\t\t\t_id = 0;\n\t\t}, [&](const MTPDinputStickerSetID &data) {\n\t\t\t_id = data.vid().v;\n\t\t\t_accessHash = data.vaccess_hash().v;\n\t\t}, [&](const auto &data) {\n\t\t\tUnexpected(\"InputStickerSet type in StorageFileLocation.\");\n\t\t});\n\t\t_volumeId = 0;\n\t\t_localId = data.vthumb_version().v;\n\t}, [&](const MTPDinputGroupCallStream &data) {\n\t\t_type = Type::GroupCallStream;\n\t\tdata.vcall().match([&](const MTPDinputGroupCall &data) {\n\t\t\t_id = data.vid().v;\n\t\t\t_accessHash = data.vaccess_hash().v;\n\t\t}, [](const auto &data) {\n\t\t\tUnexpected(\"slug/msg in inputGroupCallStream.\");\n\t\t});\n\t\t_volumeId = data.vtime_ms().v;\n\t\t_localId = data.vscale().v;\n\t\t_sizeLetter = uint8(data.vvideo_channel().value_or_empty() & 0x3F)\n\t\t\t| uint8((data.vvideo_quality().value_or_empty() & 0x03) << 6);\n\t});\n}\n\nStorageFileLocation StorageFileLocation::convertToModernPeerPhoto(\n\t\tuint64 id,\n\t\tuint64 accessHash,\n\t\tuint64 photoId) const {\n\tif (_type != Type::Legacy && _type != Type::PeerPhoto) {\n\t\treturn *this;\n\t} else if (!photoId) {\n\t\treturn StorageFileLocation();\n\t}\n\n\tauto result = *this;\n\tresult._type = Type::PeerPhoto;\n\tresult._id = id;\n\tresult._accessHash = accessHash;\n\tresult._sizeLetter = uint8('a');\n\tresult._volumeId = photoId;\n\tresult._localId = 0;\n\tresult._inMessagePeerId = 0;\n\tresult._inMessageId = 0;\n\treturn result;\n}\n\nint32 StorageFileLocation::dcId() const {\n\treturn _dcId;\n}\n\nuint64 StorageFileLocation::objectId() const {\n\treturn _id;\n}\n\nMTPInputFileLocation StorageFileLocation::tl(UserId self) const {\n\tswitch (_type) {\n\tcase Type::Legacy:\n\t\treturn MTP_inputFileLocation(\n\t\t\tMTP_long(_volumeId),\n\t\t\tMTP_int(_localId),\n\t\t\tMTP_long(_accessHash),\n\t\t\tMTP_bytes(_fileReference));\n\n\tcase Type::Encrypted:\n\t\treturn MTP_inputSecureFileLocation(\n\t\t\tMTP_long(_id),\n\t\t\tMTP_long(_accessHash));\n\n\tcase Type::Document:\n\t\treturn MTP_inputDocumentFileLocation(\n\t\t\tMTP_long(_id),\n\t\t\tMTP_long(_accessHash),\n\t\t\tMTP_bytes(_fileReference),\n\t\t\tMTP_string(_sizeLetter\n\t\t\t\t? std::string(1, char(_sizeLetter))\n\t\t\t\t: std::string()));\n\n\tcase Type::Secure:\n\t\treturn MTP_inputSecureFileLocation(\n\t\t\tMTP_long(_id),\n\t\t\tMTP_long(_accessHash));\n\n\tcase Type::Takeout:\n\t\treturn MTP_inputTakeoutFileLocation();\n\n\tcase Type::Photo:\n\t\treturn MTP_inputPhotoFileLocation(\n\t\t\tMTP_long(_id),\n\t\t\tMTP_long(_accessHash),\n\t\t\tMTP_bytes(_fileReference),\n\t\t\tMTP_string(std::string(1, char(_sizeLetter))));\n\n\tcase Type::PeerPhoto:\n\t\treturn MTP_inputPeerPhotoFileLocation(\n\t\t\tMTP_flags((_sizeLetter == 'c')\n\t\t\t\t? MTPDinputPeerPhotoFileLocation::Flag::f_big\n\t\t\t\t: MTPDinputPeerPhotoFileLocation::Flag(0)),\n\t\t\tGenerateInputPeer(\n\t\t\t\tPeerId(_id),\n\t\t\t\t_accessHash,\n\t\t\t\t_inMessagePeerId,\n\t\t\t\t_inMessageId,\n\t\t\t\tself),\n\t\t\tMTP_long(_volumeId));\n\n\tcase Type::StickerSetThumb:\n\t\treturn MTP_inputStickerSetThumb(\n\t\t\tMTP_inputStickerSetID(MTP_long(_id), MTP_long(_accessHash)),\n\t\t\tMTP_int(_localId));\n\n\tcase Type::GroupCallStream:\n\t\treturn MTP_inputGroupCallStream(\n\t\t\tMTP_flags((_sizeLetter != 0)\n\t\t\t\t? (MTPDinputGroupCallStream::Flag::f_video_channel\n\t\t\t\t\t| MTPDinputGroupCallStream::Flag::f_video_quality)\n\t\t\t\t: MTPDinputGroupCallStream::Flag(0)),\n\t\t\tMTP_inputGroupCall(MTP_long(_id), MTP_long(_accessHash)),\n\t\t\tMTP_long(_volumeId),\n\t\t\tMTP_int(_localId),\n\t\t\tMTP_int(_sizeLetter & 0x3F),\n\t\t\tMTP_int((_sizeLetter >> 6) & 0x03));\n\n\t}\n\tUnexpected(\"Type in StorageFileLocation::tl.\");\n}\n\nQByteArray StorageFileLocation::serialize() const {\n\tauto result = QByteArray();\n\tif (valid()) {\n\t\tresult.reserve(serializeSize());\n\t\tauto buffer = QBuffer(&result);\n\t\tbuffer.open(QIODevice::WriteOnly);\n\t\tauto stream = QDataStream(&buffer);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\n\t\tAssert(!(quint8(_type) & kModernLocationFlag)\n\t\t\t&& !(quint8(_type) & kInMessageFieldsFlag));\n\t\tauto typeWithFlags = quint8(_type);\n\t\ttypeWithFlags |= kModernLocationFlag;\n\t\tauto field1 = qint32(_localId);\n\t\tauto field2 = qint32(0);\n\t\tif (_inMessagePeerId != 0) {\n\t\t\tAssert(field1 == 0);\n\t\t\ttypeWithFlags |= kInMessageFieldsFlag;\n\t\t\tfield1 = qint32(uint32(_inMessagePeerId.value >> 32));\n\t\t\tfield2 = qint32(uint32(_inMessagePeerId.value & 0xFFFFFFFFULL));\n\t\t}\n\t\tAssert(typeWithFlags != kNonStorageLocationToken);\n\t\tstream\n\t\t\t<< quint16(_dcId)\n\t\t\t<< typeWithFlags\n\t\t\t<< quint8(_sizeLetter)\n\t\t\t<< field1\n\t\t\t<< quint64(_id)\n\t\t\t<< quint64(_accessHash)\n\t\t\t<< quint64(_volumeId)\n\t\t\t<< field2\n\t\t\t<< qint32(_inMessageId)\n\t\t\t<< _fileReference;\n\t}\n\treturn result;\n}\n\nint StorageFileLocation::serializeSize() const {\n\treturn valid()\n\t\t? int(sizeof(uint64) * 5 + Serialize::bytearraySize(_fileReference))\n\t\t: 0;\n}\n\nstd::optional<StorageFileLocation> StorageFileLocation::FromSerialized(\n\t\tconst QByteArray &serialized) {\n\tif (serialized.isEmpty()) {\n\t\treturn StorageFileLocation();\n\t}\n\n\tquint16 dcId = 0;\n\tquint8 typeWithFlags = 0;\n\tquint8 sizeLetter = 0;\n\tqint32 field1 = 0;\n\tquint64 id = 0;\n\tquint64 accessHash = 0;\n\tquint64 volumeId = 0;\n\tqint32 field2 = 0;\n\tqint32 inMessageId = 0;\n\tQByteArray fileReference;\n\tauto stream = QDataStream(serialized);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\tstream\n\t\t>> dcId\n\t\t>> typeWithFlags;\n\tif (typeWithFlags == kNonStorageLocationToken) {\n\t\treturn std::nullopt;\n\t}\n\tstream\n\t\t>> sizeLetter\n\t\t>> field1\n\t\t>> id\n\t\t>> accessHash\n\t\t>> volumeId;\n\tconst auto modern = ((typeWithFlags & kModernLocationFlag) != 0);\n\tconst auto inMessageFields\n\t\t= ((typeWithFlags & kInMessageFieldsFlag) != 0);\n\tif (modern) {\n\t\tstream >> field2 >> inMessageId;\n\t\ttypeWithFlags &= ~kModernLocationFlag;\n\t\tif (inMessageFields) {\n\t\t\ttypeWithFlags &= ~kInMessageFieldsFlag;\n\t\t}\n\t} else if (typeWithFlags & kLegacyInMessagePeerIdFlag) {\n\t\ttypeWithFlags &= ~kLegacyInMessagePeerIdFlag;\n\t\tstream >> field2 >> inMessageId;\n\t}\n\tstream >> fileReference;\n\n\tauto result = StorageFileLocation();\n\tresult._dcId = dcId;\n\tresult._type = Type(typeWithFlags);\n\tresult._sizeLetter = sizeLetter;\n\tresult._accessHash = accessHash;\n\tresult._volumeId = volumeId;\n\tresult._inMessageId = inMessageId;\n\tresult._fileReference = fileReference;\n\n\tif (modern) {\n\t\tresult._id = id;\n\t\tif (inMessageFields) {\n\t\t\tresult._localId = 0;\n\t\t\tresult._inMessagePeerId = PeerId(\n\t\t\t\t(uint64(uint32(field1)) << 32) | uint64(uint32(field2)));\n\t\t} else {\n\t\t\tresult._localId = field1;\n\t\t\tresult._inMessagePeerId = 0;\n\t\t}\n\t} else {\n\t\tresult._id = (result._type == Type::PeerPhoto)\n\t\t\t? DeserializePeerId(id).value\n\t\t\t: id;\n\t\tresult._localId = (result._type == Type::PeerPhoto)\n\t\t\t? 0\n\t\t\t: field1;\n\t\tresult._inMessagePeerId = (field2 && result._type == Type::PeerPhoto)\n\t\t\t? ((field2 > 0)\n\t\t\t\t? peerFromUser(UserId(field2))\n\t\t\t\t: peerFromChannel(ChannelId(-field2)))\n\t\t\t: PeerId();\n\t}\n\tif (result._type == Type::StickerSetThumb && result._volumeId != 0) {\n\t\t// Legacy field values that cannot be converted to modern.\n\t\t// No information about thumb_version, which is required.\n\t\treturn std::nullopt;\n\t}\n\n\treturn (stream.status() == QDataStream::Ok && result.valid())\n\t\t? std::make_optional(result)\n\t\t: std::nullopt;\n}\n\nStorageFileLocation::Type StorageFileLocation::type() const {\n\treturn _type;\n}\n\nbool StorageFileLocation::valid() const {\n\tswitch (_type) {\n\tcase Type::Legacy:\n\t\treturn (_dcId != 0) && (_volumeId != 0) && (_localId != 0);\n\n\tcase Type::Encrypted:\n\tcase Type::Secure:\n\tcase Type::Document:\n\t\treturn (_dcId != 0) && (_id != 0);\n\n\tcase Type::Photo:\n\t\treturn (_dcId != 0) && (_id != 0) && (_sizeLetter != 0);\n\n\tcase Type::Takeout:\n\t\treturn true;\n\n\tcase Type::PeerPhoto:\n\tcase Type::StickerSetThumb:\n\t\treturn (_dcId != 0) && (_id != 0);\n\n\tcase Type::GroupCallStream:\n\t\treturn (_dcId != 0) && (_id != 0) && (_volumeId != 0);\n\t}\n\treturn false;\n}\n\nbool StorageFileLocation::isLegacy() const {\n\treturn (_type == Type::Legacy);\n}\n\nbool StorageFileLocation::isDocumentThumbnail() const {\n\treturn (_type == Type::Document) && (_sizeLetter != 0);\n}\n\nStorage::Cache::Key StorageFileLocation::cacheKey() const {\n\tusing Key = Storage::Cache::Key;\n\n\t// Skip '1' for legacy document cache keys.\n\t// Skip '2' because it is used for good (fullsize) document thumbnails.\n\tconst auto shifted = ((uint64(_type) + 3) << 8);\n\tconst auto sliced = uint64(_dcId) & 0xFFULL;\n\tswitch (_type) {\n\tcase Type::Legacy:\n\tcase Type::PeerPhoto:\n\tcase Type::StickerSetThumb:\n\t\treturn Key{\n\t\t\tshifted | sliced | (uint64(uint32(_localId)) << 16),\n\t\t\t_volumeId };\n\n\tcase Type::Encrypted:\n\tcase Type::Secure:\n\t\treturn Key{ shifted | sliced, _id };\n\n\tcase Type::Document:\n\t\t// Keep old cache keys for documents.\n\t\tif (_sizeLetter == 0) {\n\t\t\treturn Data::DocumentCacheKey(_dcId, _id);\n\t\t\t//return Key{ 0x100ULL | sliced, _id };\n\t\t}\n\t\t[[fallthrough]];\n\tcase Type::Photo:\n\t\treturn Key{ shifted | sliced | (uint64(_sizeLetter) << 16), _id };\n\n\tcase Type::Takeout:\n\t\treturn Key{ shifted, 0 };\n\n\tcase Type::GroupCallStream:\n\t\treturn Key{\n\t\t\t(shifted\n\t\t\t\t| sliced\n\t\t\t\t| (uint32(_localId) << 16)\n\t\t\t\t| (_volumeId << 20)\n\t\t\t\t| (uint64(_sizeLetter) << 56)),\n\t\t\t_id };\n\t}\n\treturn Key();\n}\n\nStorage::Cache::Key StorageFileLocation::bigFileBaseCacheKey() const {\n\tswitch (_type) {\n\tcase Type::Document: {\n\t\tconst auto high = kDocumentBaseCacheTag\n\t\t\t| ((uint64(_dcId) << 16) & kDocumentBaseCacheMask)\n\t\t\t| (_id >> 48);\n\t\tconst auto low = (_id << 16);\n\n\t\tEnsures((low & 0x1FFULL) == 0);\n\t\treturn Storage::Cache::Key{ high, low };\n\t}\n\n\tcase Type::StickerSetThumb: {\n\t\tconst auto high = (uint64(uint32(_localId)) << 24)\n\t\t\t| ((uint64(_type) + 1) << 16)\n\t\t\t| ((uint64(_dcId) & 0xFFULL) << 8)\n\t\t\t| (_volumeId >> 56);\n\t\tconst auto low = (_volumeId << 8);\n\n\t\tEnsures((low & 0xFFULL) == 0);\n\t\treturn Storage::Cache::Key{ high, low };\n\t}\n\n\tcase Type::Photo: {\n\t\tconst auto high = kPhotoBaseCacheTag\n\t\t\t| ((uint64(_dcId) << 16) & kPhotoBaseCacheMask)\n\t\t\t| (_id >> 48);\n\t\tconst auto low = (_id << 16);\n\n\t\tEnsures((low & 0xFFULL) == 0);\n\t\treturn Storage::Cache::Key{ high, low };\n\t}\n\n\tcase Type::Legacy:\n\tcase Type::PeerPhoto:\n\tcase Type::Encrypted:\n\tcase Type::Secure:\n\tcase Type::Takeout:\n\tcase Type::GroupCallStream:\n\t\tUnexpected(\"Not implemented file location type.\");\n\n\t};\n\tUnexpected(\"Invalid file location type.\");\n}\n\nQByteArray StorageFileLocation::fileReference() const {\n\treturn _fileReference;\n}\n\nbool StorageFileLocation::refreshFileReference(\n\t\tconst Data::UpdatedFileReferences &updates) {\n\tconst auto i = (_type == Type::Document)\n\t\t? updates.data.find(Data::DocumentFileLocationId{ _id })\n\t\t: (_type == Type::Photo)\n\t\t? updates.data.find(Data::PhotoFileLocationId{ _id })\n\t\t: end(updates.data);\n\treturn (i != end(updates.data))\n\t\t? refreshFileReference(i->second)\n\t\t: false;\n}\n\nbool StorageFileLocation::refreshFileReference(const QByteArray &data) {\n\tif (data.isEmpty() || _fileReference == data) {\n\t\treturn false;\n\t}\n\t_fileReference = data;\n\treturn true;\n}\n\nconst StorageFileLocation &StorageFileLocation::Invalid() {\n\tstatic auto result = StorageFileLocation();\n\treturn result;\n}\n\nbool operator==(const StorageFileLocation &a, const StorageFileLocation &b) {\n\tconst auto valid = a.valid();\n\tif (valid != b.valid()) {\n\t\treturn false;\n\t} else if (!valid) {\n\t\treturn true;\n\t}\n\tconst auto type = a._type;\n\tif (type != b._type) {\n\t\treturn false;\n\t}\n\n\tusing Type = StorageFileLocation::Type;\n\tswitch (type) {\n\tcase Type::Legacy:\n\t\treturn (a._dcId == b._dcId)\n\t\t\t&& (a._volumeId == b._volumeId)\n\t\t\t&& (a._localId == b._localId);\n\n\tcase Type::Encrypted:\n\tcase Type::Secure:\n\t\treturn (a._dcId == b._dcId) && (a._id == b._id);\n\n\tcase Type::Photo:\n\tcase Type::Document:\n\t\treturn (a._dcId == b._dcId)\n\t\t\t&& (a._id == b._id)\n\t\t\t&& (a._sizeLetter == b._sizeLetter);\n\n\tcase Type::Takeout:\n\t\treturn true;\n\n\tcase Type::PeerPhoto:\n\t\treturn (a._dcId == b._dcId)\n\t\t\t&& (a._volumeId == b._volumeId)\n\t\t\t&& (a._localId == b._localId)\n\t\t\t&& (a._id == b._id)\n\t\t\t&& (a._sizeLetter == b._sizeLetter);\n\n\tcase Type::StickerSetThumb:\n\t\treturn (a._dcId == b._dcId)\n\t\t\t&& (a._volumeId == b._volumeId)\n\t\t\t&& (a._localId == b._localId)\n\t\t\t&& (a._id == b._id);\n\n\tcase Type::GroupCallStream:\n\t\treturn (a._dcId == b._dcId)\n\t\t\t&& (a._id == b._id)\n\t\t\t&& (a._localId == b._localId)\n\t\t\t&& (a._sizeLetter == b._sizeLetter);\n\t};\n\tUnexpected(\"Type in StorageFileLocation::operator==.\");\n}\n\nbool operator<(const StorageFileLocation &a, const StorageFileLocation &b) {\n\tconst auto valid = a.valid();\n\tif (valid != b.valid()) {\n\t\treturn !valid;\n\t} else if (!valid) {\n\t\treturn false;\n\t}\n\tconst auto type = a._type;\n\tif (type != b._type) {\n\t\treturn (type < b._type);\n\t}\n\n\tusing Type = StorageFileLocation::Type;\n\tswitch (type) {\n\tcase Type::Legacy:\n\t\treturn std::tie(a._localId, a._volumeId, a._dcId)\n\t\t\t< std::tie(b._localId, b._volumeId, b._dcId);\n\n\tcase Type::Encrypted:\n\tcase Type::Secure:\n\t\treturn std::tie(a._id, a._dcId) < std::tie(b._id, b._dcId);\n\n\tcase Type::Photo:\n\tcase Type::Document:\n\t\treturn std::tie(a._id, a._dcId, a._sizeLetter)\n\t\t\t< std::tie(b._id, b._dcId, b._sizeLetter);\n\n\tcase Type::Takeout:\n\t\treturn false;\n\n\tcase Type::PeerPhoto:\n\t\treturn std::tie(\n\t\t\ta._id,\n\t\t\ta._sizeLetter,\n\t\t\ta._localId,\n\t\t\ta._volumeId,\n\t\t\ta._dcId)\n\t\t\t< std::tie(\n\t\t\t\tb._id,\n\t\t\t\tb._sizeLetter,\n\t\t\t\tb._localId,\n\t\t\t\tb._volumeId,\n\t\t\t\tb._dcId);\n\n\tcase Type::StickerSetThumb:\n\t\treturn std::tie(a._id, a._localId, a._volumeId, a._dcId)\n\t\t\t< std::tie(b._id, b._localId, b._volumeId, b._dcId);\n\n\tcase Type::GroupCallStream:\n\t\treturn std::tie(a._id, a._localId, a._dcId, a._sizeLetter)\n\t\t\t< std::tie(b._id, b._localId, b._dcId, b._sizeLetter);\n\t};\n\tUnexpected(\"Type in StorageFileLocation::operator==.\");\n}\n\nInMemoryKey inMemoryKey(const StorageFileLocation &location) {\n\tconst auto key = location.cacheKey();\n\treturn { key.high, key.low };\n}\n\nInMemoryKey inMemoryKey(const WebFileLocation &location) {\n\tauto result = InMemoryKey();\n\tconst auto &url = location.url();\n\tconst auto sha = hashSha1(url.data(), url.size());\n\tbytes::copy(\n\t\tbytes::object_as_span(&result),\n\t\tbytes::make_span(sha).subspan(0, sizeof(result)));\n\treturn result;\n}\n\nInMemoryKey inMemoryKey(const GeoPointLocation &location) {\n\treturn InMemoryKey(\n\t\t(uint64(base::SafeRound(\n\t\t\tstd::abs(location.lat + 360.) * 1000000)) << 32)\n\t\t| uint64(base::SafeRound(std::abs(location.lon + 360.) * 1000000)),\n\t\t(uint64(location.width) << 32) | uint64(location.height));\n}\n\nInMemoryKey inMemoryKey(const PlainUrlLocation &location) {\n\tauto result = InMemoryKey();\n\tconst auto &url = location.url;\n\tconst auto sha = hashSha1(url.data(), url.size() * sizeof(QChar));\n\tbytes::copy(\n\t\tbytes::object_as_span(&result),\n\t\tbytes::make_span(sha).subspan(0, sizeof(result)));\n\treturn result;\n}\n\nInMemoryKey inMemoryKey(const AudioAlbumThumbLocation &location) {\n\tconst auto key = Data::AudioAlbumThumbCacheKey(location);\n\treturn { key.high, key.low };\n}\n\nInMemoryKey inMemoryKey(const InMemoryLocation &location) {\n\tauto result = InMemoryKey();\n\tconst auto &data = location.bytes;\n\tconst auto sha = hashSha1(data.data(), data.size());\n\tbytes::copy(\n\t\tbytes::object_as_span(&result),\n\t\tbytes::make_span(sha).subspan(0, sizeof(result)));\n\treturn result;\n}\n\nInMemoryKey inMemoryKey(const DownloadLocation &location) {\n\treturn v::match(location.data, [](const auto &data) {\n\t\treturn inMemoryKey(data);\n\t});\n}\n\nStorageImageLocation::StorageImageLocation(\n\tconst StorageFileLocation &file,\n\tint width,\n\tint height)\n: _file(file)\n, _width(width)\n, _height(height) {\n}\n\nQByteArray StorageImageLocation::serialize() const {\n\tauto result = _file.serialize();\n\tif (!result.isEmpty() || (_width > 0) || (_height > 0)) {\n\t\tresult.reserve(result.size() + 2 * sizeof(qint32));\n\t\tauto buffer = QBuffer(&result);\n\t\tbuffer.open(QIODevice::Append);\n\t\tauto stream = QDataStream(&buffer);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\t\tstream << qint32(_width) << qint32(_height);\n\t}\n\treturn result;\n}\n\nint StorageImageLocation::serializeSize() const {\n\tconst auto partial = _file.serializeSize();\n\treturn (partial > 0 || _width > 0 || _height > 0)\n\t\t? (partial + 2 * sizeof(qint32))\n\t\t: 0;\n}\n\nstd::optional<StorageImageLocation> StorageImageLocation::FromSerialized(\n\t\tconst QByteArray &serialized) {\n\tif (const auto file = StorageFileLocation::FromSerialized(serialized)) {\n\t\tconst auto my = 2 * sizeof(qint32);\n\t\tconst auto full = serialized.size();\n\t\tif (!full) {\n\t\t\treturn StorageImageLocation(*file, 0, 0);\n\t\t} else if (full >= my) {\n\t\t\tqint32 width = 0;\n\t\t\tqint32 height = 0;\n\n\t\t\tconst auto dimensions = QByteArray::fromRawData(\n\t\t\t\tserialized.data() + full - my, my);\n\t\t\tauto stream = QDataStream(dimensions);\n\t\t\tstream.setVersion(QDataStream::Qt_5_1);\n\t\t\tstream >> width >> height;\n\n\t\t\treturn (stream.status() == QDataStream::Ok)\n\t\t\t\t? StorageImageLocation(*file, width, height)\n\t\t\t\t: std::optional<StorageImageLocation>();\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n\nQByteArray DownloadLocation::serialize() const {\n\tif (!valid() || v::is<StorageFileLocation>(data)) {\n\t\treturn v::get<StorageFileLocation>(data).serialize();\n\t}\n\tauto result = QByteArray();\n\tauto buffer = QBuffer(&result);\n\tbuffer.open(QIODevice::WriteOnly);\n\tauto stream = QDataStream(&buffer);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\tstream << quint16(0) << kNonStorageLocationToken;\n\n\tv::match(data, [&](const StorageFileLocation &data) {\n\t\tUnexpected(\"Variant in DownloadLocation::serialize.\");\n\t}, [&](const WebFileLocation &data) {\n\t\tstream\n\t\t\t<< quint8(NonStorageLocationType::Web)\n\t\t\t<< data.url()\n\t\t\t<< quint64(data.accessHash());\n\t}, [&](const GeoPointLocation &data) {\n\t\tstream\n\t\t\t<< quint8(NonStorageLocationType::Geo)\n\t\t\t<< qreal(data.lat)\n\t\t\t<< qreal(data.lon)\n\t\t\t<< quint64(data.access)\n\t\t\t<< qint32(data.width)\n\t\t\t<< qint32(data.height)\n\t\t\t<< qint32(data.zoom)\n\t\t\t<< qint32(data.scale);\n\t}, [&](const AudioAlbumThumbLocation &data) {\n\t\tstream\n\t\t\t<< quint8(NonStorageLocationType::AudioAlbumThumb)\n\t\t\t<< quint64(data.documentId);\n\t}, [&](const PlainUrlLocation &data) {\n\t\tstream << quint8(NonStorageLocationType::Url) << data.url.toUtf8();\n\t}, [&](const InMemoryLocation &data) {\n\t\tstream << quint8(NonStorageLocationType::Memory) << data.bytes;\n\t});\n\tbuffer.close();\n\treturn result;\n}\n\nint DownloadLocation::serializeSize() const {\n\tif (!valid() || v::is<StorageFileLocation>(data)) {\n\t\treturn v::get<StorageFileLocation>(data).serializeSize();\n\t}\n\tauto result = sizeof(quint16) + sizeof(quint8) + sizeof(quint8);\n\tv::match(data, [&](const StorageFileLocation &data) {\n\t\tUnexpected(\"Variant in DownloadLocation::serializeSize.\");\n\t}, [&](const WebFileLocation &data) {\n\t\tresult += Serialize::bytearraySize(data.url()) + sizeof(quint64);\n\t}, [&](const GeoPointLocation &data) {\n\t\tresult += 2 * sizeof(qreal) + sizeof(quint64) + 4 * sizeof(qint32);\n\t}, [&](const PlainUrlLocation &data) {\n\t\tresult += Serialize::bytearraySize(data.url.toUtf8());\n\t}, [&](const AudioAlbumThumbLocation &data) {\n\t\tresult += sizeof(quint64);\n\t}, [&](const InMemoryLocation &data) {\n\t\tresult += Serialize::bytearraySize(data.bytes);\n\t});\n\treturn result;\n}\n\nstd::optional<DownloadLocation> DownloadLocation::FromSerialized(\n\t\tconst QByteArray &serialized) {\n\tquint16 dcId = 0;\n\tquint8 token = 0;\n\tauto stream = QDataStream(serialized);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\tstream >> dcId >> token;\n\tif (dcId != 0 || token != kNonStorageLocationToken) {\n\t\tconst auto storage = StorageFileLocation::FromSerialized(serialized);\n\t\treturn storage\n\t\t\t? std::make_optional(DownloadLocation{ *storage })\n\t\t\t: std::nullopt;\n\t}\n\tquint8 type = 0;\n\tstream >> type;\n\tswitch (NonStorageLocationType(type)) {\n\tcase NonStorageLocationType::Web: {\n\t\tQByteArray url;\n\t\tquint64 accessHash = 0;\n\t\tstream >> url >> accessHash;\n\t\treturn (stream.status() == QDataStream::Ok)\n\t\t\t? std::make_optional(\n\t\t\t\tDownloadLocation{ WebFileLocation(url, accessHash) })\n\t\t\t: std::nullopt;\n\t} break;\n\n\tcase NonStorageLocationType::Geo: {\n\t\tqreal lat = 0.;\n\t\tqreal lon = 0.;\n\t\tquint64 access = 0;\n\t\tqint32 width = 0;\n\t\tqint32 height = 0;\n\t\tqint32 zoom = 0;\n\t\tqint32 scale = 0;\n\t\tstream >> lat >> lon >> access >> width >> height >> zoom >> scale;\n\t\treturn (stream.status() == QDataStream::Ok)\n\t\t\t? std::make_optional(\n\t\t\t\tDownloadLocation{ GeoPointLocation{\n\t\t\t\t\t.lat = lat,\n\t\t\t\t\t.lon = lon,\n\t\t\t\t\t.access = access,\n\t\t\t\t\t.width = width,\n\t\t\t\t\t.height = height,\n\t\t\t\t\t.zoom = zoom,\n\t\t\t\t\t.scale = scale } })\n\t\t\t: std::nullopt;\n\t} break;\n\n\tcase NonStorageLocationType::AudioAlbumThumb: {\n\t\tquint64 id = 0;\n\t\tstream >> id;\n\t\treturn (stream.status() == QDataStream::Ok)\n\t\t\t? std::make_optional(DownloadLocation{\n\t\t\t\tAudioAlbumThumbLocation{ id } })\n\t\t\t: std::nullopt;\n\t} break;\n\n\tcase NonStorageLocationType::Url: {\n\t\tQByteArray utf;\n\t\tstream >> utf;\n\t\tconst auto url = base::FromUtf8Safe(utf);\n\t\treturn (stream.status() == QDataStream::Ok)\n\t\t\t? std::make_optional(DownloadLocation{ PlainUrlLocation{ url } })\n\t\t\t: std::nullopt;\n\t} break;\n\n\tcase NonStorageLocationType::Memory: {\n\t\tQByteArray bytes;\n\t\tstream >> bytes;\n\t\treturn (stream.status() == QDataStream::Ok)\n\t\t\t? std::make_optional(\n\t\t\t\tDownloadLocation{ InMemoryLocation{ bytes } })\n\t\t\t: std::nullopt;\n\t} break;\n\t}\n\treturn std::nullopt;\n}\n\nDownloadLocation DownloadLocation::convertToModernPeerPhoto(\n\t\tuint64 id,\n\t\tuint64 accessHash,\n\t\tuint64 photoId) const {\n\tif (!v::is<StorageFileLocation>(data)) {\n\t\treturn *this;\n\t}\n\tauto &file = v::get<StorageFileLocation>(data);\n\treturn DownloadLocation{\n\t\tfile.convertToModernPeerPhoto(id, accessHash, photoId)\n\t};\n}\n\nStorage::Cache::Key DownloadLocation::cacheKey() const {\n\treturn v::match(data, [](const GeoPointLocation &data) {\n\t\treturn Data::GeoPointCacheKey(data);\n\t}, [](const StorageFileLocation &data) {\n\t\treturn data.valid()\n\t\t\t? data.cacheKey()\n\t\t\t: Storage::Cache::Key();\n\t}, [](const WebFileLocation &data) {\n\t\treturn data.isNull()\n\t\t\t? Storage::Cache::Key()\n\t\t\t: Data::WebDocumentCacheKey(data);\n\t}, [](const PlainUrlLocation &data) {\n\t\treturn data.url.isEmpty()\n\t\t\t? Storage::Cache::Key()\n\t\t\t: Data::UrlCacheKey(data.url);\n\t}, [](const AudioAlbumThumbLocation &data) {\n\t\treturn Data::AudioAlbumThumbCacheKey(data);\n\t}, [](const InMemoryLocation &data) {\n\t\treturn Storage::Cache::Key();\n\t});\n}\n\nStorage::Cache::Key DownloadLocation::bigFileBaseCacheKey() const {\n\treturn v::is<StorageFileLocation>(data)\n\t\t? v::get<StorageFileLocation>(data).bigFileBaseCacheKey()\n\t\t: Storage::Cache::Key();\n}\n\nbool DownloadLocation::valid() const {\n\treturn v::match(data, [](const GeoPointLocation &data) {\n\t\treturn true;\n\t}, [](const StorageFileLocation &data) {\n\t\treturn data.valid();\n\t}, [](const WebFileLocation &data) {\n\t\treturn !data.isNull();\n\t}, [](const PlainUrlLocation &data) {\n\t\treturn !data.url.isEmpty();\n\t}, [](const AudioAlbumThumbLocation &data) {\n\t\treturn data.documentId != 0;\n\t}, [](const InMemoryLocation &data) {\n\t\treturn !data.bytes.isEmpty();\n\t});\n}\n\nbool DownloadLocation::isLegacy() const {\n\treturn v::is<StorageFileLocation>(data)\n\t\t? v::get<StorageFileLocation>(data).isLegacy()\n\t\t: false;\n}\n\nQByteArray DownloadLocation::fileReference() const {\n\tif (!v::is<StorageFileLocation>(data)) {\n\t\treturn QByteArray();\n\t}\n\treturn v::get<StorageFileLocation>(data).fileReference();\n}\n\nbool DownloadLocation::refreshFileReference(const QByteArray &data) {\n\tif (!v::is<StorageFileLocation>(this->data)) {\n\t\treturn false;\n\t}\n\tauto &file = v::get<StorageFileLocation>(this->data);\n\treturn file.refreshFileReference(data);\n}\n\nbool DownloadLocation::refreshFileReference(\n\tconst Data::UpdatedFileReferences &updates) {\n\tif (!v::is<StorageFileLocation>(data)) {\n\t\treturn false;\n\t}\n\tauto &file = v::get<StorageFileLocation>(data);\n\treturn file.refreshFileReference(updates);\n}\n\nImageLocation::ImageLocation(\n\tconst DownloadLocation &file,\n\tint width,\n\tint height)\n: _file(file)\n, _width(width)\n, _height(height) {\n}\n\nQByteArray ImageLocation::serialize() const {\n\tauto result = _file.serialize();\n\tif (!result.isEmpty() || (_width > 0) || (_height > 0)) {\n\t\tresult.reserve(result.size() + 2 * sizeof(qint32));\n\t\tauto buffer = QBuffer(&result);\n\t\tbuffer.open(QIODevice::Append);\n\t\tauto stream = QDataStream(&buffer);\n\t\tstream.setVersion(QDataStream::Qt_5_1);\n\t\tstream << qint32(_width) << qint32(_height);\n\t}\n\treturn result;\n}\n\nint ImageLocation::serializeSize() const {\n\tconst auto partial = _file.serializeSize();\n\treturn (partial > 0 || _width > 0 || _height > 0)\n\t\t? (partial + 2 * sizeof(qint32))\n\t\t: 0;\n}\n\nstd::optional<ImageLocation> ImageLocation::FromSerialized(\n\t\tconst QByteArray &serialized) {\n\tif (const auto file = DownloadLocation::FromSerialized(serialized)) {\n\t\tconst auto my = 2 * sizeof(qint32);\n\t\tconst auto full = serialized.size();\n\t\tif (!full) {\n\t\t\treturn ImageLocation(*file, 0, 0);\n\t\t} else if (full >= my) {\n\t\t\tqint32 width = 0;\n\t\t\tqint32 height = 0;\n\n\t\t\tconst auto dimensions = QByteArray::fromRawData(\n\t\t\t\tserialized.data() + full - my, my);\n\t\t\tauto stream = QDataStream(dimensions);\n\t\t\tstream.setVersion(QDataStream::Qt_5_1);\n\t\t\tstream >> width >> height;\n\n\t\t\treturn (stream.status() == QDataStream::Ok)\n\t\t\t\t? ImageLocation(*file, width, height)\n\t\t\t\t: std::optional<ImageLocation>();\n\t\t}\n\t}\n\treturn std::nullopt;\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/image/image_location.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_peer_id.h\"\n\nclass FileLoader;\n\nnamespace Storage {\nnamespace Cache {\nstruct Key;\n} // namespace Cache\n} // namespace Storage\n\nnamespace Data {\nstruct UpdatedFileReferences;\n} // namespace Data\n\nenum LoadFromCloudSetting {\n\tLoadFromCloudOrLocal,\n\tLoadFromLocalOnly,\n};\n\nenum LoadToCacheSetting {\n\tLoadToFileOnly,\n\tLoadToCacheAsWell,\n};\n\nusing InMemoryKey = std::pair<uint64, uint64>;\n\nnamespace std {\n\ntemplate<>\nstruct hash<InMemoryKey> {\n\tsize_t operator()(InMemoryKey value) const {\n\t\tauto seed = hash<uint64>()(value.first);\n\t\tseed ^= hash<uint64>()(value.second)\n\t\t\t+ std::size_t(0x9e3779b9)\n\t\t\t+ (seed << 6) + (seed >> 2);\n\t\treturn seed;\n\t}\n\n};\n\n} // namespace std\n\nclass StorageFileLocation {\npublic:\n\t// Those are used in serialization, don't change.\n\tenum class Type : uint8 {\n\t\tLegacy          = 0x00,\n\t\tEncrypted       = 0x01,\n\t\tDocument        = 0x02,\n\t\tSecure          = 0x03,\n\t\tTakeout         = 0x04,\n\t\tPhoto           = 0x05,\n\t\tPeerPhoto       = 0x06,\n\t\tStickerSetThumb = 0x07,\n\t\tGroupCallStream = 0x08,\n\t};\n\n\tStorageFileLocation() = default;\n\tStorageFileLocation(\n\t\tint32 dcId,\n\t\tUserId self,\n\t\tconst MTPInputFileLocation &tl);\n\n\t[[nodiscard]] StorageFileLocation convertToModernPeerPhoto(\n\t\tuint64 id,\n\t\tuint64 accessHash,\n\t\tuint64 photoId) const;\n\n\t[[nodiscard]] int32 dcId() const;\n\t[[nodiscard]] uint64 objectId() const;\n\t[[nodiscard]] MTPInputFileLocation tl(UserId self) const;\n\n\t[[nodiscard]] QByteArray serialize() const;\n\t[[nodiscard]] int serializeSize() const;\n\t[[nodiscard]] static std::optional<StorageFileLocation> FromSerialized(\n\t\tconst QByteArray &serialized);\n\n\t[[nodiscard]] Type type() const;\n\t[[nodiscard]] bool valid() const;\n\t[[nodiscard]] bool isLegacy() const;\n\t[[nodiscard]] Storage::Cache::Key cacheKey() const;\n\t[[nodiscard]] Storage::Cache::Key bigFileBaseCacheKey() const;\n\n\t// We have to allow checking this because of a serialization bug.\n\t[[nodiscard]] bool isDocumentThumbnail() const;\n\n\t[[nodiscard]] QByteArray fileReference() const;\n\tbool refreshFileReference(const Data::UpdatedFileReferences &updates);\n\tbool refreshFileReference(const QByteArray &data);\n\n\t[[nodiscard]] static const StorageFileLocation &Invalid();\n\n\tfriend bool operator==(\n\t\tconst StorageFileLocation &a,\n\t\tconst StorageFileLocation &b);\n\tfriend bool operator<(\n\t\tconst StorageFileLocation &a,\n\t\tconst StorageFileLocation &b);\n\nprivate:\n\tuint16 _dcId = 0;\n\tType _type = Type::Legacy;\n\tuint8 _sizeLetter = 0;\n\tint32 _localId = 0;\n\tuint64 _id = 0;\n\tuint64 _accessHash = 0;\n\tuint64 _volumeId = 0;\n\tPeerId _inMessagePeerId = 0;\n\tuint32 _inMessageId = 0;\n\tQByteArray _fileReference;\n\n};\n\ninline bool operator!=(\n\t\tconst StorageFileLocation &a,\n\t\tconst StorageFileLocation &b) {\n\treturn !(a == b);\n}\n\ninline bool operator>(\n\t\tconst StorageFileLocation &a,\n\t\tconst StorageFileLocation &b) {\n\treturn (b < a);\n}\n\ninline bool operator<=(\n\t\tconst StorageFileLocation &a,\n\t\tconst StorageFileLocation &b) {\n\treturn !(b < a);\n}\n\ninline bool operator>=(\n\t\tconst StorageFileLocation &a,\n\t\tconst StorageFileLocation &b) {\n\treturn !(a < b);\n}\n\nclass StorageImageLocation {\npublic:\n\tStorageImageLocation() = default;\n\tStorageImageLocation(\n\t\tconst StorageFileLocation &file,\n\t\tint width,\n\t\tint height);\n\n\t[[nodiscard]] QByteArray serialize() const;\n\t[[nodiscard]] int serializeSize() const;\n\t[[nodiscard]] static std::optional<StorageImageLocation> FromSerialized(\n\t\tconst QByteArray &serialized);\n\n\t[[nodiscard]] StorageImageLocation convertToModernPeerPhoto(\n\t\t\tuint64 id,\n\t\t\tuint64 accessHash,\n\t\t\tuint64 photoId) const {\n\t\treturn StorageImageLocation(\n\t\t\t_file.convertToModernPeerPhoto(id, accessHash, photoId),\n\t\t\t_width,\n\t\t\t_height);\n\t}\n\n\t[[nodiscard]] const StorageFileLocation &file() const {\n\t\treturn _file;\n\t}\n\t[[nodiscard]] int width() const {\n\t\treturn _width;\n\t}\n\t[[nodiscard]] int height() const {\n\t\treturn _height;\n\t}\n\n\tvoid setSize(int width, int height) {\n\t\t_width = width;\n\t\t_height = height;\n\t}\n\n\t[[nodiscard]] StorageFileLocation::Type type() const {\n\t\treturn _file.type();\n\t}\n\t[[nodiscard]] bool valid() const {\n\t\treturn _file.valid();\n\t}\n\t[[nodiscard]] bool isLegacy() const {\n\t\treturn _file.isLegacy();\n\t}\n\t[[nodiscard]] QByteArray fileReference() const {\n\t\treturn _file.fileReference();\n\t}\n\tbool refreshFileReference(const QByteArray &data) {\n\t\treturn _file.refreshFileReference(data);\n\t}\n\tbool refreshFileReference(const Data::UpdatedFileReferences &updates) {\n\t\treturn _file.refreshFileReference(updates);\n\t}\n\n\t[[nodiscard]] static const StorageImageLocation &Invalid() {\n\t\tstatic auto result = StorageImageLocation();\n\t\treturn result;\n\t}\n\n\tfriend inline bool operator==(\n\t\t\tconst StorageImageLocation &a,\n\t\t\tconst StorageImageLocation &b) {\n\t\treturn (a._file == b._file);\n\t}\n\tfriend inline bool operator<(\n\t\t\tconst StorageImageLocation &a,\n\t\t\tconst StorageImageLocation &b) {\n\t\treturn (a._file < b._file);\n\t}\n\nprivate:\n\tStorageFileLocation _file;\n\tint _width = 0;\n\tint _height = 0;\n\n};\n\ninline bool operator!=(\n\t\tconst StorageImageLocation &a,\n\t\tconst StorageImageLocation &b) {\n\treturn !(a == b);\n}\n\ninline bool operator>(\n\t\tconst StorageImageLocation &a,\n\t\tconst StorageImageLocation &b) {\n\treturn (b < a);\n}\n\ninline bool operator<=(\n\t\tconst StorageImageLocation &a,\n\t\tconst StorageImageLocation &b) {\n\treturn !(b < a);\n}\n\ninline bool operator>=(\n\t\tconst StorageImageLocation &a,\n\t\tconst StorageImageLocation &b) {\n\treturn !(a < b);\n}\n\nclass WebFileLocation {\npublic:\n\tWebFileLocation() = default;\n\tWebFileLocation(const QByteArray &url, uint64 accessHash)\n\t: _accessHash(accessHash)\n\t, _url(url) {\n\t}\n\tbool isNull() const {\n\t\treturn _url.isEmpty();\n\t}\n\tuint64 accessHash() const {\n\t\treturn _accessHash;\n\t}\n\tconst QByteArray &url() const {\n\t\treturn _url;\n\t}\n\n\tstatic WebFileLocation Null;\n\nprivate:\n\tuint64 _accessHash = 0;\n\tQByteArray _url;\n\n\tfriend inline bool operator==(\n\t\t\tconst WebFileLocation &a,\n\t\t\tconst WebFileLocation &b) {\n\t\treturn (a._accessHash == b._accessHash)\n\t\t\t&& (a._url == b._url);\n\t}\n\tfriend inline bool operator<(\n\t\t\tconst WebFileLocation &a,\n\t\t\tconst WebFileLocation &b) {\n\t\treturn std::tie(a._accessHash, a._url)\n\t\t\t< std::tie(b._accessHash, b._url);\n\t}\n\n};\n\ninline bool operator!=(const WebFileLocation &a, const WebFileLocation &b) {\n\treturn !(a == b);\n}\n\ninline bool operator>(const WebFileLocation &a, const WebFileLocation &b) {\n\treturn (b < a);\n}\n\ninline bool operator<=(const WebFileLocation &a, const WebFileLocation &b) {\n\treturn !(b < a);\n}\n\ninline bool operator>=(const WebFileLocation &a, const WebFileLocation &b) {\n\treturn !(a < b);\n}\n\nstruct GeoPointLocation {\n\tfloat64 lat = 0.;\n\tfloat64 lon = 0.;\n\tuint64 access = 0;\n\tint32 width = 0;\n\tint32 height = 0;\n\tint32 zoom = 0;\n\tint32 scale = 0;\n};\n\ninline bool operator==(\n\t\tconst GeoPointLocation &a,\n\t\tconst GeoPointLocation &b) {\n\treturn (a.lat == b.lat)\n\t\t&& (a.lon == b.lon)\n\t\t&& (a.access == b.access)\n\t\t&& (a.width == b.width)\n\t\t&& (a.height == b.height)\n\t\t&& (a.zoom == b.zoom)\n\t\t&& (a.scale == b.scale);\n}\n\ninline bool operator<(\n\t\tconst GeoPointLocation &a,\n\t\tconst GeoPointLocation &b) {\n\treturn std::tie(\n\t\ta.access,\n\t\ta.lat,\n\t\ta.lon,\n\t\ta.width,\n\t\ta.height,\n\t\ta.zoom,\n\t\ta.scale)\n\t\t< std::tie(\n\t\t\tb.access,\n\t\t\tb.lat,\n\t\t\tb.lon,\n\t\t\tb.width,\n\t\t\tb.height,\n\t\t\tb.zoom,\n\t\t\tb.scale);\n}\n\ninline bool operator!=(\n\t\tconst GeoPointLocation &a,\n\t\tconst GeoPointLocation &b) {\n\treturn !(a == b);\n}\n\ninline bool operator>(\n\t\tconst GeoPointLocation &a,\n\t\tconst GeoPointLocation &b) {\n\treturn (b < a);\n}\n\ninline bool operator<=(\n\t\tconst GeoPointLocation &a,\n\t\tconst GeoPointLocation &b) {\n\treturn !(b < a);\n}\n\ninline bool operator>=(\n\t\tconst GeoPointLocation &a,\n\t\tconst GeoPointLocation &b) {\n\treturn !(a < b);\n}\n\nstruct PlainUrlLocation {\n\tQString url;\n\n\tfriend inline bool operator==(\n\t\t\tconst PlainUrlLocation &a,\n\t\t\tconst PlainUrlLocation &b) {\n\t\treturn (a.url == b.url);\n\t}\n\tfriend inline bool operator<(\n\t\t\tconst PlainUrlLocation &a,\n\t\t\tconst PlainUrlLocation &b) {\n\t\treturn (a.url < b.url);\n\t}\n};\n\ninline bool operator!=(\n\t\tconst PlainUrlLocation &a,\n\t\tconst PlainUrlLocation &b) {\n\treturn !(a == b);\n}\n\ninline bool operator>(\n\t\tconst PlainUrlLocation &a,\n\t\tconst PlainUrlLocation &b) {\n\treturn (b < a);\n}\n\ninline bool operator<=(\n\t\tconst PlainUrlLocation &a,\n\t\tconst PlainUrlLocation &b) {\n\treturn !(b < a);\n}\n\ninline bool operator>=(\n\t\tconst PlainUrlLocation &a,\n\t\tconst PlainUrlLocation &b) {\n\treturn !(a < b);\n}\n\nstruct AudioAlbumThumbLocation {\n\tuint64 documentId = 0;\n\n\tfriend inline auto operator<=>(\n\t\tAudioAlbumThumbLocation,\n\t\tAudioAlbumThumbLocation) = default;\n};\n\nstruct InMemoryLocation {\n\tQByteArray bytes;\n\n\tfriend inline bool operator==(\n\t\t\tconst InMemoryLocation &a,\n\t\t\tconst InMemoryLocation &b) {\n\t\treturn (a.bytes == b.bytes);\n\t}\n\tfriend inline bool operator<(\n\t\t\tconst InMemoryLocation &a,\n\t\t\tconst InMemoryLocation &b) {\n\t\treturn (a.bytes < b.bytes);\n\t}\n};\n\ninline bool operator!=(\n\t\tconst InMemoryLocation &a,\n\t\tconst InMemoryLocation &b) {\n\treturn !(a == b);\n}\n\ninline bool operator>(\n\t\tconst InMemoryLocation &a,\n\t\tconst InMemoryLocation &b) {\n\treturn (b < a);\n}\n\ninline bool operator<=(\n\t\tconst InMemoryLocation &a,\n\t\tconst InMemoryLocation &b) {\n\treturn !(b < a);\n}\n\ninline bool operator>=(\n\t\tconst InMemoryLocation &a,\n\t\tconst InMemoryLocation &b) {\n\treturn !(a < b);\n}\n\nclass DownloadLocation {\npublic:\n\tstd::variant<\n\t\tStorageFileLocation,\n\t\tWebFileLocation,\n\t\tGeoPointLocation,\n\t\tPlainUrlLocation,\n\t\tAudioAlbumThumbLocation,\n\t\tInMemoryLocation> data;\n\n\t[[nodiscard]] QByteArray serialize() const;\n\t[[nodiscard]] int serializeSize() const;\n\t[[nodiscard]] static std::optional<DownloadLocation> FromSerialized(\n\t\tconst QByteArray &serialized);\n\n\t[[nodiscard]] DownloadLocation convertToModernPeerPhoto(\n\t\tuint64 id,\n\t\tuint64 accessHash,\n\t\tuint64 photoId) const;\n\n\t[[nodiscard]] Storage::Cache::Key cacheKey() const;\n\t[[nodiscard]] Storage::Cache::Key bigFileBaseCacheKey() const;\n\t[[nodiscard]] bool valid() const;\n\t[[nodiscard]] bool isLegacy() const;\n\t[[nodiscard]] QByteArray fileReference() const;\n\tbool refreshFileReference(const QByteArray &data);\n\tbool refreshFileReference(const Data::UpdatedFileReferences &updates);\n\n\tfriend inline bool operator==(\n\t\t\tconst DownloadLocation &a,\n\t\t\tconst DownloadLocation &b) {\n\t\treturn (a.data == b.data);\n\t}\n\tfriend inline bool operator<(\n\t\t\tconst DownloadLocation &a,\n\t\t\tconst DownloadLocation &b) {\n\t\treturn (a.data < b.data);\n\t}\n\n};\n\ninline bool operator!=(\n\t\tconst DownloadLocation &a,\n\t\tconst DownloadLocation &b) {\n\treturn !(a == b);\n}\n\ninline bool operator>(\n\t\tconst DownloadLocation &a,\n\t\tconst DownloadLocation &b) {\n\treturn (b < a);\n}\n\ninline bool operator<=(\n\t\tconst DownloadLocation &a,\n\t\tconst DownloadLocation &b) {\n\treturn !(b < a);\n}\n\ninline bool operator>=(\n\t\tconst DownloadLocation &a,\n\t\tconst DownloadLocation &b) {\n\treturn !(a < b);\n}\n\nclass ImageLocation {\npublic:\n\tImageLocation() = default;\n\tImageLocation(\n\t\tconst DownloadLocation &file,\n\t\tint width,\n\t\tint height);\n\n\t[[nodiscard]] QByteArray serialize() const;\n\t[[nodiscard]] int serializeSize() const;\n\t[[nodiscard]] static std::optional<ImageLocation> FromSerialized(\n\t\tconst QByteArray &serialized);\n\n\t[[nodiscard]] ImageLocation convertToModernPeerPhoto(\n\t\t\tuint64 id,\n\t\t\tuint64 accessHash,\n\t\t\tuint64 photoId) const {\n\t\treturn ImageLocation(\n\t\t\t_file.convertToModernPeerPhoto(id, accessHash, photoId),\n\t\t\t_width,\n\t\t\t_height);\n\t}\n\n\t[[nodiscard]] const DownloadLocation &file() const {\n\t\treturn _file;\n\t}\n\t[[nodiscard]] int width() const {\n\t\treturn _width;\n\t}\n\t[[nodiscard]] int height() const {\n\t\treturn _height;\n\t}\n\n\tvoid setSize(int width, int height) {\n\t\t_width = width;\n\t\t_height = height;\n\t}\n\n\t[[nodiscard]] bool valid() const {\n\t\treturn _file.valid();\n\t}\n\t[[nodiscard]] bool isLegacy() const {\n\t\treturn _file.isLegacy();\n\t}\n\t[[nodiscard]] QByteArray fileReference() const {\n\t\treturn _file.fileReference();\n\t}\n\tbool refreshFileReference(const QByteArray &data) {\n\t\treturn _file.refreshFileReference(data);\n\t}\n\tbool refreshFileReference(const Data::UpdatedFileReferences &updates) {\n\t\treturn _file.refreshFileReference(updates);\n\t}\n\n\t[[nodiscard]] static const ImageLocation &Invalid() {\n\t\tstatic auto result = ImageLocation();\n\t\treturn result;\n\t}\n\n\tfriend inline bool operator==(\n\t\t\tconst ImageLocation &a,\n\t\t\tconst ImageLocation &b) {\n\t\treturn (a._file == b._file);\n\t}\n\tfriend inline bool operator<(\n\t\t\tconst ImageLocation &a,\n\t\t\tconst ImageLocation &b) {\n\t\treturn (a._file < b._file);\n\t}\n\nprivate:\n\tDownloadLocation _file;\n\tint _width = 0;\n\tint _height = 0;\n\n};\n\ninline bool operator!=(\n\t\tconst ImageLocation &a,\n\t\tconst ImageLocation &b) {\n\treturn !(a == b);\n}\n\ninline bool operator>(\n\t\tconst ImageLocation &a,\n\t\tconst ImageLocation &b) {\n\treturn (b < a);\n}\n\ninline bool operator<=(\n\t\tconst ImageLocation &a,\n\t\tconst ImageLocation &b) {\n\treturn !(b < a);\n}\n\ninline bool operator>=(\n\t\tconst ImageLocation &a,\n\t\tconst ImageLocation &b) {\n\treturn !(a < b);\n}\n\nstruct ImageWithLocation {\n\tImageLocation location;\n\tQByteArray bytes;\n\tQImage preloaded;\n\tint bytesCount = 0;\n\tint progressivePartSize = 0;\n};\n\nstruct InlineImageLocation {\n\tQByteArray bytes;\n\tbool isPath = false;\n};\n\nInMemoryKey inMemoryKey(const StorageFileLocation &location);\n\ninline InMemoryKey inMemoryKey(const StorageImageLocation &location) {\n\treturn inMemoryKey(location.file());\n}\n\nInMemoryKey inMemoryKey(const WebFileLocation &location);\nInMemoryKey inMemoryKey(const GeoPointLocation &location);\nInMemoryKey inMemoryKey(const PlainUrlLocation &location);\nInMemoryKey inMemoryKey(const InMemoryLocation &location);\nInMemoryKey inMemoryKey(const DownloadLocation &location);\n\ninline InMemoryKey inMemoryKey(const ImageLocation &location) {\n\treturn inMemoryKey(location.file());\n}\n\ninline QSize shrinkToKeepAspect(int32 width, int32 height, int32 towidth, int32 toheight) {\n\tint32 w = qMax(width, 1), h = qMax(height, 1);\n\tif (w * toheight > h * towidth) {\n\t\th = qRound(h * towidth / float64(w));\n\t\tw = towidth;\n\t} else {\n\t\tw = qRound(w * toheight / float64(h));\n\t\th = toheight;\n\t}\n\treturn QSize(qMax(w, 1), qMax(h, 1));\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/image/image_location_factory.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/image/image_location_factory.h\"\n\n#include \"ui/image/image.h\"\n#include \"main/main_session.h\"\n\n#include <QtCore/QBuffer>\n\nnamespace Images {\nnamespace {\n\nQSize GetSizeForDocument(const QVector<MTPDocumentAttribute> &attributes) {\n\tfor (const auto &attribute : attributes) {\n\t\tif (attribute.type() == mtpc_documentAttributeImageSize) {\n\t\t\tauto &size = attribute.c_documentAttributeImageSize();\n\t\t\treturn QSize(size.vw().v, size.vh().v);\n\t\t}\n\t}\n\treturn QSize();\n}\n\n} // namespace\n\nImageWithLocation FromPhotoSize(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDphoto &photo,\n\t\tconst MTPPhotoSize &size) {\n\tif (!photo.vaccess_hash().v && photo.vfile_reference().v.isEmpty()) {\n\t\t// Locally created fake photo.\n\t\treturn ImageWithLocation();\n\t}\n\treturn size.match([&](const MTPDphotoSize &data) {\n\t\treturn ImageWithLocation{\n\t\t\t.location = ImageLocation(\n\t\t\t\tDownloadLocation{ StorageFileLocation(\n\t\t\t\t\tphoto.vdc_id().v,\n\t\t\t\t\tsession->userId(),\n\t\t\t\t\tMTP_inputPhotoFileLocation(\n\t\t\t\t\t\tphoto.vid(),\n\t\t\t\t\t\tphoto.vaccess_hash(),\n\t\t\t\t\t\tphoto.vfile_reference(),\n\t\t\t\t\t\tdata.vtype())) },\n\t\t\t\tdata.vw().v,\n\t\t\t\tdata.vh().v),\n\t\t\t.bytesCount = data.vsize().v\n\t\t};\n\t}, [&](const MTPDphotoCachedSize &data) {\n\t\tconst auto bytes = qba(data.vbytes());\n\t\treturn ImageWithLocation{\n\t\t\t.location = ImageLocation(\n\t\t\t\tDownloadLocation{ StorageFileLocation(\n\t\t\t\t\tphoto.vdc_id().v,\n\t\t\t\t\tsession->userId(),\n\t\t\t\t\tMTP_inputPhotoFileLocation(\n\t\t\t\t\t\tphoto.vid(),\n\t\t\t\t\t\tphoto.vaccess_hash(),\n\t\t\t\t\t\tphoto.vfile_reference(),\n\t\t\t\t\t\tdata.vtype())) },\n\t\t\t\tdata.vw().v,\n\t\t\t\tdata.vh().v),\n\t\t\t.bytes = bytes,\n\t\t\t.bytesCount = int(bytes.size()),\n\t\t};\n\t}, [&](const MTPDphotoSizeProgressive &data) {\n\t\t// #TODO layer118\n\t\tif (data.vsizes().v.isEmpty()) {\n\t\t\treturn ImageWithLocation();\n\t\t}\n\t\treturn ImageWithLocation{\n\t\t\t.location = ImageLocation(\n\t\t\t\tDownloadLocation{ StorageFileLocation(\n\t\t\t\t\tphoto.vdc_id().v,\n\t\t\t\t\tsession->userId(),\n\t\t\t\t\tMTP_inputPhotoFileLocation(\n\t\t\t\t\t\tphoto.vid(),\n\t\t\t\t\t\tphoto.vaccess_hash(),\n\t\t\t\t\t\tphoto.vfile_reference(),\n\t\t\t\t\t\tdata.vtype())) },\n\t\t\t\tdata.vw().v,\n\t\t\t\tdata.vh().v),\n\t\t\t.bytesCount = data.vsizes().v.back().v\n\t\t};\n\t}, [&](const MTPDphotoStrippedSize &data) {\n\t\treturn ImageWithLocation();\n\t\t//const auto bytes = ExpandInlineBytes(qba(data.vbytes()));\n\t\t//return ImageWithLocation{\n\t\t//\t.location = ImageLocation(\n\t\t//\t\tDownloadLocation{ StorageFileLocation(\n\t\t//\t\t\tphoto.vdc_id().v,\n\t\t//\t\t\tsession->userId(),\n\t\t//\t\t\tMTP_inputPhotoFileLocation(\n\t\t//\t\t\t\tphoto.vid(),\n\t\t//\t\t\t\tphoto.vaccess_hash(),\n\t\t//\t\t\t\tphoto.vfile_reference(),\n\t\t//\t\t\t\tdata.vtype())) },\n\t\t//\t\twidth, // ???\n\t\t//\t\theight), // ???\n\t\t//\t.bytes = bytes,\n\t\t//\t.bytesCount = bytes.size(),\n\t\t//};\n\t}, [&](const MTPDphotoPathSize &) {\n\t\treturn ImageWithLocation();\n\t}, [&](const MTPDphotoSizeEmpty &) {\n\t\treturn ImageWithLocation();\n\t});\n}\n\nImageWithLocation FromProgressiveSize(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPPhotoSize &size,\n\t\tint index) {\n\tExpects(size.type() == mtpc_photoSizeProgressive);\n\n\tconst auto &data = size.c_photoSizeProgressive();\n\tif (data.vsizes().v.size() <= index) {\n\t\treturn ImageWithLocation();\n\t}\n\treturn ImageWithLocation{\n\t\t.progressivePartSize = data.vsizes().v[index].v,\n\t};\n}\n\nImageWithLocation FromPhotoSize(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDdocument &document,\n\t\tconst MTPPhotoSize &size) {\n\treturn size.match([&](const MTPDphotoSize &data) {\n\t\treturn ImageWithLocation{\n\t\t\t.location = ImageLocation(\n\t\t\t\tDownloadLocation{ StorageFileLocation(\n\t\t\t\t\tdocument.vdc_id().v,\n\t\t\t\t\tsession->userId(),\n\t\t\t\t\tMTP_inputDocumentFileLocation(\n\t\t\t\t\t\tdocument.vid(),\n\t\t\t\t\t\tdocument.vaccess_hash(),\n\t\t\t\t\t\tdocument.vfile_reference(),\n\t\t\t\t\t\tdata.vtype())) },\n\t\t\t\tdata.vw().v,\n\t\t\t\tdata.vh().v),\n\t\t\t.bytesCount = data.vsize().v\n\t\t};\n\t}, [&](const MTPDphotoCachedSize &data) {\n\t\tconst auto bytes = qba(data.vbytes());\n\t\treturn ImageWithLocation{\n\t\t\t.location = ImageLocation(\n\t\t\t\tDownloadLocation{ StorageFileLocation(\n\t\t\t\t\tdocument.vdc_id().v,\n\t\t\t\t\tsession->userId(),\n\t\t\t\t\tMTP_inputDocumentFileLocation(\n\t\t\t\t\t\tdocument.vid(),\n\t\t\t\t\t\tdocument.vaccess_hash(),\n\t\t\t\t\t\tdocument.vfile_reference(),\n\t\t\t\t\t\tdata.vtype())) },\n\t\t\t\tdata.vw().v,\n\t\t\t\tdata.vh().v),\n\t\t\t.bytes = bytes,\n\t\t\t.bytesCount = int(bytes.size()),\n\t\t};\n\t}, [&](const MTPDphotoSizeProgressive &data) {\n\t\tif (data.vsizes().v.isEmpty()) {\n\t\t\treturn ImageWithLocation();\n\t\t}\n\t\treturn ImageWithLocation{\n\t\t\t.location = ImageLocation(\n\t\t\t\tDownloadLocation{ StorageFileLocation(\n\t\t\t\t\tdocument.vdc_id().v,\n\t\t\t\t\tsession->userId(),\n\t\t\t\t\tMTP_inputDocumentFileLocation(\n\t\t\t\t\t\tdocument.vid(),\n\t\t\t\t\t\tdocument.vaccess_hash(),\n\t\t\t\t\t\tdocument.vfile_reference(),\n\t\t\t\t\t\tdata.vtype())) },\n\t\t\t\tdata.vw().v,\n\t\t\t\tdata.vh().v),\n\t\t\t.bytesCount = data.vsizes().v.back().v\n\t\t};\n\t}, [&](const MTPDphotoStrippedSize &data) {\n\t\treturn ImageWithLocation();\n\t\t//const auto bytes = ExpandInlineBytes(qba(data.vbytes()));\n\t\t//return ImageWithLocation{\n\t\t//\t.location = ImageLocation(\n\t\t//\t\tDownloadLocation{ StorageFileLocation(\n\t\t//\t\t\tdocument.vdc_id().v,\n\t\t//\t\t\tsession->userId(),\n\t\t//\t\t\tMTP_inputDocumentFileLocation(\n\t\t//\t\t\t\tdocument.vid(),\n\t\t//\t\t\t\tdocument.vaccess_hash(),\n\t\t//\t\t\t\tdocument.vfile_reference(),\n\t\t//\t\t\t\tdata.vtype())) },\n\t\t//\t\twidth, // ???\n\t\t//\t\theight), // ???\n\t\t//\t.bytes = bytes,\n\t\t//\t.bytesCount = bytes.size(),\n\t\t//};\n\t}, [&](const MTPDphotoPathSize &data) {\n\t\treturn ImageWithLocation();\n\t}, [&](const MTPDphotoSizeEmpty &) {\n\t\treturn ImageWithLocation();\n\t});\n}\n\nImageWithLocation FromPhotoSize(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDstickerSet &set,\n\t\tconst MTPPhotoSize &size) {\n\tif (!set.vthumb_dc_id() || !set.vthumb_version()) {\n\t\treturn ImageWithLocation();\n\t}\n\treturn size.match([&](const MTPDphotoSize &data) {\n\t\treturn ImageWithLocation{\n\t\t\t.location = ImageLocation(\n\t\t\t\tDownloadLocation{ StorageFileLocation(\n\t\t\t\t\tset.vthumb_dc_id()->v,\n\t\t\t\t\tsession->userId(),\n\t\t\t\t\tMTP_inputStickerSetThumb(\n\t\t\t\t\t\tMTP_inputStickerSetID(set.vid(), set.vaccess_hash()),\n\t\t\t\t\t\tMTP_int(set.vthumb_version()->v))) },\n\t\t\t\tdata.vw().v,\n\t\t\t\tdata.vh().v),\n\t\t\t.bytesCount = data.vsize().v\n\t\t};\n\t}, [&](const MTPDphotoCachedSize &data) {\n\t\tconst auto bytes = qba(data.vbytes());\n\t\treturn ImageWithLocation{\n\t\t\t.location = ImageLocation(\n\t\t\t\tDownloadLocation{ StorageFileLocation(\n\t\t\t\t\tset.vthumb_dc_id()->v,\n\t\t\t\t\tsession->userId(),\n\t\t\t\t\tMTP_inputStickerSetThumb(\n\t\t\t\t\t\tMTP_inputStickerSetID(set.vid(), set.vaccess_hash()),\n\t\t\t\t\t\tMTP_int(set.vthumb_version()->v))) },\n\t\t\t\tdata.vw().v,\n\t\t\t\tdata.vh().v),\n\t\t\t.bytes = bytes,\n\t\t\t.bytesCount = int(bytes.size()),\n\t\t};\n\t}, [&](const MTPDphotoSizeProgressive &data) {\n\t\tif (data.vsizes().v.isEmpty()) {\n\t\t\treturn ImageWithLocation();\n\t\t}\n\t\treturn ImageWithLocation{\n\t\t\t.location = ImageLocation(\n\t\t\t\tDownloadLocation{ StorageFileLocation(\n\t\t\t\t\tset.vthumb_dc_id()->v,\n\t\t\t\t\tsession->userId(),\n\t\t\t\t\tMTP_inputStickerSetThumb(\n\t\t\t\t\t\tMTP_inputStickerSetID(set.vid(), set.vaccess_hash()),\n\t\t\t\t\t\tMTP_int(set.vthumb_version()->v))) },\n\t\t\t\tdata.vw().v,\n\t\t\t\tdata.vh().v),\n\t\t\t.bytesCount = data.vsizes().v.back().v\n\t\t};\n\t}, [&](const MTPDphotoStrippedSize &data) {\n\t\treturn ImageWithLocation();\n\t\t//const auto bytes = ExpandInlineBytes(qba(data.vbytes()));\n\t\t//return ImageWithLocation{\n\t\t//\t.location = ImageLocation(\n\t\t//\t\tDownloadLocation{ StorageFileLocation(\n\t\t//\t\t\tdocument.vdc_id().v,\n\t\t//\t\t\tsession->userId(),\n\t\t//\t\t\tMTP_inputDocumentFileLocation(\n\t\t//\t\t\t\tdocument.vid(),\n\t\t//\t\t\t\tdocument.vaccess_hash(),\n\t\t//\t\t\t\tdocument.vfile_reference(),\n\t\t//\t\t\t\tdata.vtype())) },\n\t\t//\t\twidth, // ???\n\t\t//\t\theight), // ???\n\t\t//\t.bytes = bytes,\n\t\t//\t.bytesCount = bytes.size(),\n\t\t//};\n\t}, [&](const MTPDphotoPathSize &data) {\n\t\treturn ImageWithLocation();\n\t}, [&](const MTPDphotoSizeEmpty &) {\n\t\treturn ImageWithLocation();\n\t});\n}\n\nImageWithLocation FromImageInMemory(\n\t\tconst QImage &image,\n\t\tconst char *format,\n\t\tQByteArray bytes) {\n\tif (image.isNull()) {\n\t\treturn ImageWithLocation();\n\t}\n\tif (bytes.isEmpty()) {\n\t\tauto buffer = QBuffer(&bytes);\n\t\timage.save(&buffer, format);\n\t}\n\treturn ImageWithLocation{\n\t\t.location = ImageLocation(\n\t\t\tDownloadLocation{ InMemoryLocation{ bytes } },\n\t\t\timage.width(),\n\t\t\timage.height()),\n\t\t.bytes = bytes,\n\t\t.preloaded = image,\n\t\t.bytesCount = int(bytes.size()),\n\t};\n}\n\nImageLocation FromWebDocument(const MTPWebDocument &document) {\n\treturn document.match([](const MTPDwebDocument &data) {\n\t\tconst auto size = GetSizeForDocument(data.vattributes().v);\n\n\t\t// We don't use size from WebDocument, because it is not reliable.\n\t\t// It can be > 0 and different from the real size\n\t\t// that we get in upload.WebFile result.\n\t\t//auto filesize = int64(); // data.vsize().v;\n\t\treturn ImageLocation(\n\t\t\tDownloadLocation{ WebFileLocation(\n\t\t\t\tdata.vurl().v,\n\t\t\t\tdata.vaccess_hash().v) },\n\t\t\tsize.width(),\n\t\t\tsize.height());\n\t}, [](const MTPDwebDocumentNoProxy &data) {\n\t\tconst auto size = GetSizeForDocument(data.vattributes().v);\n\n\t\t// We don't use size from WebDocument, because it is not reliable.\n\t\t// It can be > 0 and different from the real size\n\t\t// that we get in upload.WebFile result.\n\t\t//auto filesize = int64(); // data.vsize().v;\n\t\treturn ImageLocation(\n\t\t\tDownloadLocation{ PlainUrlLocation{ qs(data.vurl()) } },\n\t\t\tsize.width(),\n\t\t\tsize.height());\n\t});\n}\n\nImageWithLocation FromVideoSize(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDdocument &document,\n\t\tconst MTPVideoSize &size) {\n\treturn size.match([&](const MTPDvideoSize &data) {\n\t\treturn ImageWithLocation{\n\t\t\t.location = ImageLocation(\n\t\t\t\tDownloadLocation{ StorageFileLocation(\n\t\t\t\t\tdocument.vdc_id().v,\n\t\t\t\t\tsession->userId(),\n\t\t\t\t\tMTP_inputDocumentFileLocation(\n\t\t\t\t\t\tdocument.vid(),\n\t\t\t\t\t\tdocument.vaccess_hash(),\n\t\t\t\t\t\tdocument.vfile_reference(),\n\t\t\t\t\t\tdata.vtype())) },\n\t\t\t\tdata.vw().v,\n\t\t\t\tdata.vh().v),\n\t\t\t.bytesCount = data.vsize().v,\n\t\t};\n\t}, [](const MTPDvideoSizeEmojiMarkup &) {\n\t\treturn ImageWithLocation();\n\t}, [](const MTPDvideoSizeStickerMarkup &) {\n\t\treturn ImageWithLocation();\n\t});\n}\n\nImageWithLocation FromVideoSize(\n\t\tnot_null<Main::Session*> session,\n\t\tconst MTPDphoto &photo,\n\t\tconst MTPVideoSize &size) {\n\treturn size.match([&](const MTPDvideoSize &data) {\n\t\treturn ImageWithLocation{\n\t\t\t.location = ImageLocation(\n\t\t\t\tDownloadLocation{ StorageFileLocation(\n\t\t\t\t\tphoto.vdc_id().v,\n\t\t\t\t\tsession->userId(),\n\t\t\t\t\tMTP_inputPhotoFileLocation(\n\t\t\t\t\t\tphoto.vid(),\n\t\t\t\t\t\tphoto.vaccess_hash(),\n\t\t\t\t\t\tphoto.vfile_reference(),\n\t\t\t\t\t\tdata.vtype())) },\n\t\t\t\tdata.vw().v,\n\t\t\t\tdata.vh().v),\n\t\t\t.bytesCount = data.vsize().v,\n\t\t};\n\t}, [](const MTPDvideoSizeEmojiMarkup &) {\n\t\treturn ImageWithLocation();\n\t}, [](const MTPDvideoSizeStickerMarkup &) {\n\t\treturn ImageWithLocation();\n\t});\n}\n\n} // namespace Images\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/image/image_location_factory.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/image/image_location.h\"\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Images {\n\n[[nodiscard]] ImageWithLocation FromPhotoSize(\n\tnot_null<Main::Session*> session,\n\tconst MTPDphoto &photo,\n\tconst MTPPhotoSize &size);\n[[nodiscard]] ImageWithLocation FromProgressiveSize(\n\tnot_null<Main::Session*> session,\n\tconst MTPPhotoSize &size,\n\tint index);\n[[nodiscard]] ImageWithLocation FromPhotoSize(\n\tnot_null<Main::Session*> session,\n\tconst MTPDdocument &document,\n\tconst MTPPhotoSize &size);\n[[nodiscard]] ImageWithLocation FromPhotoSize(\n\tnot_null<Main::Session*> session,\n\tconst MTPDstickerSet &set,\n\tconst MTPPhotoSize &size);\n[[nodiscard]] ImageWithLocation FromVideoSize(\n\tnot_null<Main::Session*> session,\n\tconst MTPDdocument &document,\n\tconst MTPVideoSize &size);\n[[nodiscard]] ImageWithLocation FromVideoSize(\n\tnot_null<Main::Session*> session,\n\tconst MTPDphoto &photo,\n\tconst MTPVideoSize &size);\n[[nodiscard]] ImageWithLocation FromImageInMemory(\n\tconst QImage &image,\n\tconst char *format,\n\tQByteArray bytes = QByteArray());\n[[nodiscard]] ImageLocation FromWebDocument(const MTPWebDocument &document);\n\n} // namespace Images\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/image/image_source.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/image/image_source.h\"\n\n#include \"storage/cache/storage_cache_database.h\"\n#include \"storage/file_download_mtproto.h\"\n#include \"storage/file_download_web.h\"\n#include \"data/data_session.h\"\n#include \"data/data_file_origin.h\"\n#include \"history/history_item.h\"\n#include \"history/history.h\"\n#include \"main/main_session.h\"\n#include \"app.h\"\n\n#include <QtCore/QBuffer>\n\nnamespace Images {\nnamespace {\n\n[[nodiscard]] QByteArray ReadContent(const QString &path) {\n\tauto file = QFile(path);\n\tconst auto good = (file.size() <= App::kImageSizeLimit)\n\t\t&& file.open(QIODevice::ReadOnly);\n\treturn good ? file.readAll() : QByteArray();\n}\n\n[[nodiscard]] QImage ReadImage(const QByteArray &content) {\n\treturn App::readImage(content, nullptr, false, nullptr);\n}\n\n} // namespace\n\nImageSource::ImageSource(const QString &path)\n: ImageSource(ReadContent(path)) {\n}\n\nImageSource::ImageSource(const QByteArray &content)\n: ImageSource(ReadImage(content)) {\n}\n\nImageSource::ImageSource(QImage &&data) : _data(std::move(data)) {\n}\n\nvoid ImageSource::load() {\n}\n\nQImage ImageSource::takeLoaded() {\n\treturn _data;\n}\n\nint ImageSource::width() {\n\treturn _data.width();\n}\n\nint ImageSource::height() {\n\treturn _data.height();\n}\n\n} // namespace Images\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/image/svg_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/image/svg_preview.h\"\n\n#include \"base/debug_log.h\"\n\n#include <QtCore/QXmlStreamReader>\n#include <QtGui/QPainter>\n#include <QtSvg/QSvgRenderer>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kMaxSvgSize = 1 * 1024 * 1024;\nconstexpr auto kMaxDefaultRenderSize = 4096;\n\n[[nodiscard]] bool IsName(const QString &value, const char *name) {\n\treturn (value.compare(QLatin1String(name), Qt::CaseInsensitive) == 0);\n}\n\n[[nodiscard]] bool IsDisallowedElement(const QString &name) {\n\treturn IsName(name, \"script\")\n\t\t|| IsName(name, \"foreignObject\")\n\t\t|| IsName(name, \"iframe\")\n\t\t|| IsName(name, \"object\")\n\t\t|| IsName(name, \"embed\")\n\t\t|| IsName(name, \"audio\")\n\t\t|| IsName(name, \"video\")\n\t\t|| IsName(name, \"animate\")\n\t\t|| IsName(name, \"animateColor\")\n\t\t|| IsName(name, \"animateMotion\")\n\t\t|| IsName(name, \"animateTransform\")\n\t\t|| IsName(name, \"set\");\n}\n\n[[nodiscard]] QString TrimAndUnquote(QString value) {\n\tvalue = value.trimmed();\n\tif (value.size() < 2) {\n\t\treturn value;\n\t}\n\tconst auto first = value.at(0);\n\tconst auto last = value.at(value.size() - 1);\n\tif (((first == QLatin1Char('\"')) && (last == QLatin1Char('\"')))\n\t\t|| ((first == QLatin1Char('\\'')) && (last == QLatin1Char('\\'')))) {\n\t\tvalue = value.mid(1, value.size() - 2).trimmed();\n\t}\n\treturn value;\n}\n\n[[nodiscard]] bool IsAllowedReference(QString value) {\n\tvalue = TrimAndUnquote(value);\n\treturn !value.isEmpty() && value.startsWith(QLatin1Char('#'));\n}\n\n[[nodiscard]] bool HasDisallowedCss(const QString &text) {\n\tif (text.contains(QLatin1String(\"@import\"), Qt::CaseInsensitive)) {\n\t\treturn true;\n\t}\n\tif (text.contains(QLatin1String(\"@keyframes\"), Qt::CaseInsensitive)\n\t\t|| text.contains(QLatin1String(\"animation\"), Qt::CaseInsensitive)) {\n\t\treturn true;\n\t}\n\tauto index = 0;\n\twhile (true) {\n\t\tindex = text.indexOf(\n\t\t\tQLatin1String(\"url(\"),\n\t\t\tindex,\n\t\t\tQt::CaseInsensitive);\n\t\tif (index < 0) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto start = index + 4;\n\t\tconst auto end = text.indexOf(QLatin1Char(')'), start);\n\t\tif (end < 0) {\n\t\t\treturn true;\n\t\t}\n\t\tif (!IsAllowedReference(text.mid(start, end - start))) {\n\t\t\treturn true;\n\t\t}\n\t\tindex = end + 1;\n\t}\n}\n\n[[nodiscard]] bool AttributesAreSafe(\n\t\tconst QXmlStreamAttributes &attributes) {\n\tfor (const auto &attribute : attributes) {\n\t\tconst auto name = attribute.name().toString();\n\t\tconst auto value = attribute.value().toString();\n\t\tif (name.startsWith(QLatin1String(\"on\"), Qt::CaseInsensitive)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (IsName(name, \"href\") && !IsAllowedReference(value)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (IsName(name, \"style\") && HasDisallowedCss(value)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n[[nodiscard]] QByteArray SanitizeSvg(const QByteArray &bytes) {\n\tauto reader = QXmlStreamReader(bytes);\n\tauto hasRoot = false;\n\tauto inStyle = 0;\n\twhile (!reader.atEnd()) {\n\t\tswitch (reader.readNext()) {\n\t\tcase QXmlStreamReader::StartElement: {\n\t\t\tconst auto name = reader.name().toString();\n\t\t\tif (!hasRoot) {\n\t\t\t\thasRoot = true;\n\t\t\t\tif (!IsName(name, \"svg\")) {\n\t\t\t\t\tLOG((\"Svg Sanitize: Invalid root element.\"));\n\t\t\t\t\treturn {};\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (IsDisallowedElement(name)\n\t\t\t\t|| !AttributesAreSafe(reader.attributes())) {\n\t\t\t\tLOG((\"Svg Sanitize: Disallowed element or attribute.\"));\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\tif (IsName(name, \"style\")) {\n\t\t\t\t++inStyle;\n\t\t\t}\n\t\t} break;\n\t\tcase QXmlStreamReader::EndElement: {\n\t\t\tif ((inStyle > 0) && IsName(reader.name().toString(), \"style\")) {\n\t\t\t\t--inStyle;\n\t\t\t}\n\t\t} break;\n\t\tcase QXmlStreamReader::Characters: {\n\t\t\tif ((inStyle > 0) && HasDisallowedCss(reader.text().toString())) {\n\t\t\t\tLOG((\"Svg Sanitize: Disallowed CSS.\"));\n\t\t\t\treturn {};\n\t\t\t}\n\t\t} break;\n\t\tcase QXmlStreamReader::DTD:\n\t\tcase QXmlStreamReader::EntityReference:\n\t\tcase QXmlStreamReader::ProcessingInstruction:\n\t\tcase QXmlStreamReader::Invalid:\n\t\t\tLOG((\"Svg Sanitize: Disallowed XML token.\"));\n\t\t\treturn {};\n\t\tdefault: break;\n\t\t}\n\t}\n\tif (reader.hasError() || !hasRoot) {\n\t\tLOG((\"Svg Sanitize: Parse error.\"));\n\t\treturn {};\n\t}\n\treturn bytes;\n}\n\n} // namespace\n\nint SvgPreviewBytesLimit() {\n\treturn kMaxSvgSize;\n}\n\nQImage RenderSvgPreview(const QByteArray &bytes, QSize maxSize) {\n\tif (bytes.isEmpty()) {\n\t\treturn {};\n\t}\n\tif (bytes.size() > kMaxSvgSize) {\n\t\tLOG((\"Svg Error: File too large (%1 bytes).\").arg(bytes.size()));\n\t\treturn {};\n\t}\n\tconst auto sanitized = SanitizeSvg(bytes);\n\tif (sanitized.isEmpty()) {\n\t\treturn {};\n\t}\n\tauto renderer = QSvgRenderer();\n\tif (!renderer.load(sanitized) || !renderer.isValid()) {\n\t\tLOG((\"Svg Error: Invalid data.\"));\n\t\treturn {};\n\t}\n\tauto size = renderer.defaultSize();\n\tif (!maxSize.isEmpty()) {\n\t\tsize = size.scaled(maxSize, Qt::KeepAspectRatio);\n\t} else if ((size.width() > kMaxDefaultRenderSize)\n\t\t|| (size.height() > kMaxDefaultRenderSize)) {\n\t\tsize = size.scaled(\n\t\t\tkMaxDefaultRenderSize,\n\t\t\tkMaxDefaultRenderSize,\n\t\t\tQt::KeepAspectRatio);\n\t}\n\tif ((size.width() <= 0) || (size.height() <= 0)) {\n\t\tLOG((\"Svg Error: Bad size %1x%2.\"\n\t\t\t).arg(renderer.defaultSize().width()\n\t\t\t).arg(renderer.defaultSize().height()));\n\t\treturn {};\n\t}\n\tauto rendered = QImage(size, QImage::Format_ARGB32_Premultiplied);\n\trendered.fill(Qt::transparent);\n\t{\n\t\tQPainter p(&rendered);\n\t\trenderer.render(&p, QRect(QPoint(), size));\n\t}\n\treturn rendered;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/image/svg_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <QtCore/QByteArray>\n#include <QtCore/QSize>\n#include <QtGui/QImage>\n\nnamespace Ui {\n\n[[nodiscard]] int SvgPreviewBytesLimit();\n[[nodiscard]] QImage RenderSvgPreview(\n\tconst QByteArray &bytes,\n\tQSize maxSize);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/item_text_options.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/item_text_options.h\"\n\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n\nnamespace Ui {\nnamespace {\n\nbool UseBotTextOptions(\n\t\tnot_null<History*> history,\n\t\tnot_null<PeerData*> author) {\n\tif (const auto user = history->peer->asUser()) {\n\t\tif (user->isBot()) {\n\t\t\treturn true;\n\t\t}\n\t} else if (const auto chat = history->peer->asChat()) {\n\t\tif (chat->botStatus != Data::BotStatus::NoBots) {\n\t\t\treturn true;\n\t\t}\n\t} else if (const auto group = history->peer->asMegagroup()) {\n\t\tif (group->mgInfo->botStatus != Data::BotStatus::NoBots) {\n\t\t\treturn true;\n\t\t}\n\t}\n\tif (const auto user = author->asUser()) {\n\t\tif (user->isBot()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n} // namespace\n\nconst TextParseOptions &ItemTextOptions(\n\t\tnot_null<History*> history,\n\t\tnot_null<PeerData*> author) {\n\treturn UseBotTextOptions(history, author)\n\t\t? ItemTextBotDefaultOptions()\n\t\t: ItemTextDefaultOptions();\n}\n\nconst TextParseOptions &ItemTextOptions(not_null<const HistoryItem*> item) {\n\treturn ItemTextOptions(item->history(), item->author());\n}\n\nconst TextParseOptions &ItemTextNoMonoOptions(\n\t\tnot_null<History*> history,\n\t\tnot_null<PeerData*> author) {\n\treturn UseBotTextOptions(history, author)\n\t\t? ItemTextBotNoMonoOptions()\n\t\t: ItemTextNoMonoOptions();\n}\n\nconst TextParseOptions &ItemTextNoMonoOptions(\n\t\tnot_null<const HistoryItem*> item) {\n\treturn ItemTextNoMonoOptions(item->history(), item->author());\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/item_text_options.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/text/text_options.h\"\n\nclass History;\nclass PeerData;\nclass HistoryItem;\n\nnamespace Ui {\n\nconst TextParseOptions &ItemTextOptions(\n\tnot_null<History*> history,\n\tnot_null<PeerData*> author);\nconst TextParseOptions &ItemTextNoMonoOptions(\n\tnot_null<History*> history,\n\tnot_null<PeerData*> author);\nconst TextParseOptions &ItemTextOptions(not_null<const HistoryItem*> item);\nconst TextParseOptions &ItemTextNoMonoOptions(not_null<const HistoryItem*> item);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/menu_icons.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/colors.palette\";\nusing \"ui/widgets/widgets.style\";\n\nmenuIconReactions: icon {{ \"menu/read_reactions\", menuIconColor }};\nmenuIconReply: icon {{ \"menu/reply\", menuIconColor }};\nmenuIconViewReplies: icon {{ \"menu/view_replies\", menuIconColor }};\nmenuIconEdit: icon {{ \"menu/edit\", menuIconColor }};\nmenuIconDraw: icon {{ \"mediaview/draw\", menuIconColor, point(2px, 3px) }};\nmenuIconPin: icon {{ \"menu/pin\", menuIconColor }};\nmenuIconUnpin: icon {{ \"menu/unpin\", menuIconColor }};\nmenuIconCopy: icon {{ \"menu/copy\", menuIconColor }};\nmenuIconForward: icon {{ \"menu/forward\", menuIconColor }};\nmenuIconDelete: icon {{ \"menu/delete\", menuIconColor }};\nmenuIconSelect: icon {{ \"menu/select\", menuIconColor }};\nmenuIconSaveImage: icon {{ \"menu/save_image\", menuIconColor }};\nmenuIconStickers: icon {{ \"menu/stickers\", menuIconColor }};\nmenuIconEmoji: icon {{ \"menu/emoji\", menuIconColor }};\nmenuIconCancel: icon {{ \"menu/cancel\", menuIconColor }};\nmenuIconShowInChat: icon {{ \"menu/show_in_chat\", menuIconColor }};\nmenuIconStealth: icon {{ \"menu/stealth\", menuIconColor }};\nmenuIconStealthLocked: icon {{ \"menu/stealth_locked\", menuIconColor }};\nmenuIconGif: icon {{ \"menu/gif\", menuIconColor }};\nmenuIconShowInFolder: icon {{ \"menu/show_in_folder\", menuIconColor }};\nmenuIconDownload: icon {{ \"menu/download\", menuIconColor }};\nmenuIconLink: icon {{ \"menu/link\", menuIconColor }};\nmenuIconBlock: icon {{ \"menu/block\", menuIconColor }};\nmenuIconUnblock: icon {{ \"menu/unblock\", menuIconColor }};\nmenuIconRestartBot: icon {{ \"menu/restart_bot\", menuIconColor }};\nmenuIconReport: icon {{ \"menu/report\", menuIconColor }};\nmenuIconFave: icon {{ \"menu/favorite\", menuIconColor }};\nmenuIconUnfave: icon {{ \"menu/unfavorite\", menuIconColor }};\nmenuIconProfile: icon {{ \"menu/profile\", menuIconColor }};\nmenuIconInfo: icon {{ \"menu/info\", menuIconColor }};\nmenuIconInvite: icon {{ \"menu/invite\", menuIconColor }};\nmenuIconPromote: icon {{ \"menu/admin_promote\", menuIconColor }};\nmenuIconAdmin: icon {{ \"menu/admin\", menuIconColor }};\nmenuIconRemove: icon {{ \"menu/remove\", menuIconColor }};\nmenuIconRetractVote: icon {{ \"menu/retract_vote\", menuIconColor }};\nmenuIconPermissions: icon {{ \"menu/permissions\", menuIconColor }};\nmenuIcon2SV: icon {{ \"menu/2sv_on\", menuIconColor }};\n//menuIconOff2SV: icon {{ \"menu/2sv_off\", menuIconColor }};\nmenuIconShare: icon {{ \"menu/share\", menuIconColor }};\nmenuIconSharing: icon {{ \"menu/share2\", menuIconColor }};\nmenuIconArchive: icon {{ \"menu/archive\", menuIconColor }};\nmenuIconUnarchive: icon {{ \"menu/unarchive\", menuIconColor }};\nmenuIconMarkRead: icon {{ \"menu/read\", menuIconColor }};\nmenuIconMarkUnread: icon {{ \"menu/unread\", menuIconColor }};\nmenuIconChangeColors: icon {{ \"menu/colors\", menuIconColor }};\nmenuIconExport: icon {{ \"menu/export\", menuIconColor }};\nmenuIconClear: icon {{ \"menu/clear\", menuIconColor }};\nmenuIconManage: icon {{ \"menu/manage\", menuIconColor }};\nmenuIconDiscussion: icon {{ \"menu/discussion\", menuIconColor }};\nmenuIconStats: icon {{ \"menu/stats\", menuIconColor }};\nmenuIconBoosts: icon {{ \"menu/boosts\", menuIconColor }};\nmenuIconEarn: icon {{ \"menu/earn\", menuIconColor }};\nmenuIconCancelFee: icon {{ \"menu/cancel_fee\", menuIconColor }};\nmenuIconCreatePoll: icon {{ \"menu/create_poll\", menuIconColor }};\nmenuIconCreateTodoList: icon {{ \"menu/select\", menuIconColor }};\nmenuIconQrCode: icon {{ \"menu/qr_code\", menuIconColor }};\nmenuIconExpand: icon {{ \"menu/expand\", menuIconColor }};\nmenuIconCollapse: icon {{ \"menu/collapse\", menuIconColor }};\nmenuIconToMainMenu: icon {{ \"menu/move_to_menu\", menuIconColor }};\nmenuIconFromMainMenu: icon {{ \"menu/move_from_menu\", menuIconColor }};\nmenuIconAddAccount: icon {{ \"menu/add_account\", menuIconColor }};\nmenuIconMute: icon {{ \"menu/mute\", menuIconColor }};\nmenuIconUnmute: icon {{ \"menu/unmute\", menuIconColor }};\nmenuIconSchedule: icon {{ \"menu/calendar\", menuIconColor }};\nmenuIconReschedule: icon {{ \"menu/reschedule\", menuIconColor }};\nmenuIconSend: icon {{ \"menu/send\", menuIconColor }};\nmenuIconFlip: icon {{ \"menu/flip\", menuIconColor }};\nmenuIconWhenOnline: icon {{ \"menu/send_when_online\", menuIconColor }};\nmenuIconPalette: icon {{ \"menu/palette\", menuIconColor }};\nmenuIconImportTheme: icon {{ \"menu/import_theme\", menuIconColor }};\nmenuIconExportTheme: icon {{ \"menu/export_theme\", menuIconColor }};\nmenuIconRestore: icon {{ \"menu/restore\", menuIconColor }};\nmenuIconSettings: icon {{ \"menu/settings\", menuIconColor }};\nmenuIconCopyright: icon {{ \"menu/copyright\", menuIconColor }};\nmenuIconDrugs: icon {{ \"menu/drugs\", menuIconColor }};\nmenuIconFake: icon {{ \"menu/fake\", menuIconColor }};\nmenuIconPersonal: icon {{ \"menu/personal\", menuIconColor }};\nmenuIconPorn: icon {{ \"menu/porn\", menuIconColor }};\nmenuIconViolence: icon {{ \"menu/violence\", menuIconColor }};\nmenuIconMuteFor: icon {{ \"menu/mute_for\", menuIconColor }};\nmenuIconSilent: icon {{ \"menu/silent\", menuIconColor }};\nmenuIconCustomize: icon {{ \"menu/customize\", menuIconColor }};\nmenuIconSoundOn: icon {{ \"menu/sound_enable\", menuIconColor }};\nmenuIconSoundOff: icon {{ \"menu/sound_disable\", menuIconColor }};\nmenuIconSoundSelect: icon {{ \"menu/sound_select\", menuIconColor }};\nmenuIconSoundAdd: icon {{ \"menu/sound_add\", menuIconColor }};\nmenuIconFile: icon {{ \"menu/file\", menuIconColor }};\nmenuIconPhoto: icon {{ \"menu/image\", menuIconColor }};\nmenuIconAddToFolder: icon {{ \"menu/add_to_folder\", menuIconColor }};\nmenuIconLeave: icon {{ \"menu/leave\", menuIconColor }};\nmenuIconGiftPremium: icon {{ \"menu/gift_premium\", menuIconColor }};\nmenuIconSearch: icon {{ \"menu/search\", menuIconColor }};\nmenuIconStartStream: icon {{ \"menu/start_stream\", menuIconColor }};\nmenuIconStartStreamWith: icon {{ \"menu/start_stream_with\", menuIconColor }};\nmenuIconVideoChat: icon {{ \"menu/video_chat\", menuIconColor }};\nmenuIconTranslate: icon {{ \"menu/translate\", menuIconColor }};\nmenuIconReportAntiSpam: icon {{ \"menu/false_positive\", menuIconColor }};\nmenuIconSpoiler: icon {{ \"menu/spoiler_on\", menuIconColor }};\nmenuIconQualityHigh: icon {{ \"menu/quality_hd\", menuIconColor }};\nmenuIconDisable: icon {{ \"menu/disable\", menuIconColor }};\nmenuIconPhotoSet: icon {{ \"menu/photo_set\", menuIconColor }};\nmenuIconPhotoSuggest: icon {{ \"menu/photo_suggest\", menuIconColor }};\nmenuIconNewWindow: icon {{ \"menu/new_window\", menuIconColor }};\nmenuIconChatBubble: icon {{ \"menu/chat_bubble\", menuIconColor }};\nmenuIconPhone: icon {{ \"menu/phone\", menuIconColor }};\nmenuIconChannel: icon {{ \"menu/channel\", menuIconColor }};\nmenuIconStoriesSavedSection: icon {{ \"menu/stories_saved_section\", menuIconColor }};\nmenuIconStoriesArchiveSection: icon {{ \"menu/stories_archive_section\", menuIconColor }};\nmenuIconStoriesSave: icon {{ \"menu/stories_save\", menuIconColor }};\nmenuIconStoriesArchive: icon {{ \"menu/stories_archive\", menuIconColor }};\nmenuIconArchiveOpen: icon {{ \"menu/archive_open\", menuIconColor }};\nmenuIconGroups: icon {{ \"menu/groups\", menuIconColor }};\nmenuIconSavedMessages: icon {{ \"menu/saved_messages\", menuIconColor }};\nmenuIconNightMode: icon {{ \"menu/night_mode\", menuIconColor }};\nmenuIconNotifications: icon {{ \"menu/info_notifications\", menuIconColor }};\nmenuIconLock: icon {{ \"menu/lock\", menuIconColor }};\nmenuIconPowerUsage: icon {{ \"menu/power_usage\", menuIconColor }};\nmenuIconFaq: icon {{ \"menu/faq\", menuIconColor }};\nmenuIconEmojiObjects: icon {{ \"emoji/emoji_objects\", menuIconColor }};\nmenuIconUsername: icon {{ \"menu/payment_email\", menuIconColor }};\nmenuIconDevices: icon {{ \"menu/devices\", menuIconColor }};\nmenuIconNetwork: icon {{ \"menu/network\", menuIconColor }};\nmenuIconStorage: icon {{ \"menu/storage\", menuIconColor }};\nmenuIconExperimental: icon {{ \"menu/experimental\", menuIconColor }};\nmenuIconCallsReceive: icon {{ \"menu/calls_receive\", menuIconColor }};\nmenuIconDockBounce: icon {{ \"menu/dock_bounce\", menuIconColor }};\nmenuIconRecoveryEmail: icon {{ \"menu/recovery_email\", menuIconColor }};\nmenuIconTimer: icon {{ \"menu/timer\", menuIconColor }};\nmenuIconTopics: icon {{ \"menu/topics\", menuIconColor }};\nmenuIconGroupReactions: icon {{ \"menu/group_reactions\", menuIconColor }};\nmenuIconLinks: icon {{ \"menu/links_profile\", menuIconColor }};\nmenuIconGroupLog: icon {{ \"menu/group_log\", menuIconColor }};\nmenuIconSigned: icon {{ \"menu/signed\", menuIconColor }};\nmenuIconAntispam: icon {{ \"menu/antispam\", menuIconColor }};\nmenuIconChatDiscuss: icon {{ \"menu/chat_discuss\", menuIconColor }};\nmenuIconChats: icon {{ \"menu/chats\", menuIconColor }};\nmenuIconBot: icon {{ \"menu/bot\", menuIconColor }};\nmenuIconBotCommands: icon {{ \"menu/bot_commands\", menuIconColor }};\nmenuIconPremium: icon {{ \"menu/premium\", menuIconColor }};\nmenuIconShop: icon {{ \"menu/shop\", menuIconColor }};\nmenuIconIpAddress: icon {{ \"menu/ip_address\", menuIconColor }};\nmenuIconAddress: icon {{ \"menu/payment_address\", menuIconColor }};\nmenuIconShowAll: icon {{ \"menu/all_media\", menuIconColor }};\nmenuIconReplace: icon {{ \"chat/input_replace\", menuIconColor }};\nmenuIconAbove: icon {{ \"menu/link_above\", menuIconColor }};\nmenuIconBelow: icon {{ \"menu/link_below\", menuIconColor }};\nmenuIconEnlarge: icon {{ \"menu/link_enlarge\", menuIconColor }};\nmenuIconShrink: icon {{ \"menu/link_shrink\", menuIconColor }};\nmenuIconUserShow: icon {{ \"menu/name_show\", menuIconColor }};\nmenuIconUserHide: icon {{ \"menu/name_hide\", menuIconColor }};\nmenuIconCaptionShow: icon {{ \"menu/caption_show\", menuIconColor }};\nmenuIconCaptionHide: icon {{ \"menu/caption_hide\", menuIconColor }};\nmenuIconAsTopics: icon {{ \"menu/mode_topics\", menuIconColor }};\nmenuIconAsMessages: icon {{ \"menu/mode_messages\", menuIconColor }};\nmenuIconTagFilter: icon{{ \"menu/tag_filter\", menuIconColor }};\nmenuIconTagRename: icon{{ \"menu/tag_rename\", menuIconColor }};\nmenuIconTagRemove: icon {{ \"menu/tag_remove\", menuIconColor }};\nmenuIconTagSell: icon {{ \"menu/tag_sell\", menuIconColor }};\nmenuIconGroupsHide: icon {{ \"menu/hide_members\", menuIconColor }};\nmenuIconFont: icon {{ \"menu/fonts\", menuIconColor }};\nmenuIconFactcheck: icon {{ \"menu/factcheck\", menuIconColor }};\nmenuIconWinHello: icon {{ \"menu/passcode_winhello\", menuIconColor }};\nmenuIconTouchID: icon {{ \"menu/passcode_finger\", menuIconColor }};\nmenuIconAppleWatch: icon {{ \"menu/passcode_watch\", menuIconColor }};\nmenuIconSystemPwd: menuIconPermissions;\nmenuIconPlayerFullScreen: icon {{ \"player/player_fullscreen\", menuIconColor }};\nmenuIconPlayerWindowed: icon {{ \"player/player_minimize\", menuIconColor }};\nmenuIconStarRefShare: icon {{ \"menu/stars_share\", menuIconColor }};\nmenuIconStarRefLink: icon {{ \"settings/premium/features/feature_links2\", menuIconColor }};\nmenuIconTransparent: icon {{ \"menu/affiliate_transparent\", menuIconColor }};\nmenuIconLike: icon {{ \"menu/affiliate_simple\", menuIconColor }};\nmenuIconTradable: icon {{ \"menu/tradable\", menuIconColor }};\nmenuIconUnique: icon {{ \"menu/unique\", menuIconColor }};\nmenuIconUniqueProfile: icon {{ \"settings/premium/features/feature_profile_cover\", menuIconColor }};\nmenuIconNftWear: icon {{ \"menu/nft_wear\", menuIconColor }};\nmenuIconNftTakeOff: icon {{ \"menu/nft_takeoff\", menuIconColor }};\nmenuIconShortcut: icon {{ \"menu/shortcut\", menuIconColor }};\nmenuIconHourglass: icon {{ \"menu/hourglass\", menuIconColor }};\nmenuIconPayment: icon {{ \"payments/payment_card\", menuIconColor }};\nmenuIconOrderPrice: icon {{ \"menu/order_price\", menuIconColor }};\nmenuIconOrderDate: icon {{ \"menu/order_date\", menuIconColor }};\nmenuIconOrderNumber: icon {{ \"menu/order_number\", menuIconColor }};\nmenuIconAdd: icon {{ \"menu/add\", menuIconColor }};\nmenuIconRatingGifts: icon {{ \"menu/rating_gifts-24x24\", menuIconColor }};\nmenuIconRatingUsers: icon {{ \"menu/users_stars-24x24\", menuIconColor }};\nmenuIconRatingRefund: icon {{ \"menu/rating_refund-24x24\", menuIconColor }};\nmenuIconAuctionDrop: icon {{ \"menu/auction_drop\", menuIconColor }};\nmenuIconStarsRefund: icon {{ \"menu/auction_refund\", menuIconColor }};\nmenuIconStarsCarryover: icon {{ \"menu/auction_carry\", menuIconColor }};\nmenuIconTon: icon{{ \"payments/ton_emoji-18x18\", menuIconColor, point(3px, 2px) }};\nmenuIconReorder: icon{{ \"menu/reorder-24x24\", menuIconColor }};\nmenuIconTools: icon{{ \"menu/craft_tools-24x24\", menuIconColor }};\nmenuIconCraftTraits: icon{{ \"menu/craft_random-24x24\", menuIconColor }};\nmenuIconCraftChance: icon{{ \"menu/craft_chance-24x24\", menuIconColor }};\nmenuIconCraft: icon{{ \"menu/craft_start-24x24\", menuIconColor }};\nmenuIconTagAdd: icon{{ \"menu/tag_add-24x24\", menuIconColor }};\nmenuIconTagEdit: icon{{ \"menu/tag_edit-24x24\", menuIconColor }};\nmenuIconDownloadOff: icon {{ \"menu/download_off-24x24\", menuIconColor }};\nmenuIconShareOff: icon {{ \"menu/share_off-24x24\", menuIconColor }};\nmenuIconShareOn: icon {{ \"menu/share_on-24x24\", menuIconColor }};\nmenuIconStar: icon {{ \"menu/star\", menuIconColor }};\n\nmenuIconForkSettings: icon {{ \"settings/settings_fork-22x22\", menuIconColor }};\n\nmenuIconTTLAny: icon {{ \"menu/auto_delete_plain\", menuIconColor }};\nmenuIconTTLAnyTextPosition: point(11px, 22px);\nmenuIconTTL: icon {{ \"menu/auto_delete\", menuIconColor }};\n\nmenuIconMuteForAny: icon {{ \"menu/mute_for_plain\", menuIconColor }};\nmenuIconMuteForAnyTextPosition: point(14px, 9px);\nmenuIconMuteForAnyTextFont: font(8px semibold);\n\nmenuBlueIconPhotoSet: icon {{ \"menu/photo_set\", lightButtonFg }};\nmenuBlueIconPhotoSuggest: icon {{ \"menu/photo_suggest\", lightButtonFg }};\nmenuBlueIconPremium: icon{{ \"menu/premium\", lightButtonFg }};\nmenuBlueIconColorNames: icon{{ \"settings/premium/features/feature_color_names\", lightButtonFg }};\nmenuBlueIconWallpaper: icon{{ \"settings/premium/features/feature_wallpaper\", lightButtonFg }};\nmenuBlueIconEmojiStatus: icon{{ \"settings/premium/features/feature_status\", lightButtonFg }};\nmenuBlueIconEmojiPack: icon{{ \"settings/premium/features/feature_emoji_pack\", lightButtonFg }};\nmenuBlueIconGroupCreate: icon {{ \"menu/groups_create\", lightButtonFg }};\n\nmediaMenuIconStickers: icon {{ \"menu/stickers\", mediaviewMenuFg }};\nmediaMenuIconCancel: icon {{ \"menu/cancel\", mediaviewMenuFg }};\nmediaMenuIconChannel: icon {{ \"menu/channel\", mediaviewMenuFg }};\nmediaMenuIconShowInChat: icon {{ \"menu/show_in_chat\", mediaviewMenuFg }};\nmediaMenuIconShowInFolder: icon {{ \"menu/show_in_folder\", mediaviewMenuFg }};\nmediaMenuIconDownload: icon {{ \"menu/download\", mediaviewMenuFg }};\nmediaMenuIconDownloadLocked: icon {{ \"menu/download_locked\", mediaviewMenuFg }};\nmediaMenuIconCopy: icon {{ \"menu/copy\", mediaviewMenuFg }};\nmediaMenuIconForward: icon {{ \"menu/forward\", mediaviewMenuFg }};\nmediaMenuIconShare: icon {{ \"menu/share2\", mediaviewMenuFg }};\nmediaMenuIconDelete: icon {{ \"menu/delete\", mediaviewMenuFg }};\nmediaMenuIconShowAll: icon {{ \"menu/all_media\", mediaviewMenuFg }};\nmediaMenuIconProfile: icon {{ \"menu/profile\", mediaviewMenuFg }};\nmediaMenuIconReport: icon {{ \"menu/report\", mediaviewMenuFg }};\nmediaMenuIconSaveStory: icon {{ \"menu/stories_save\", mediaviewMenuFg }};\nmediaMenuIconArchiveStory: icon {{ \"menu/stories_archive\", mediaviewMenuFg }};\nmediaMenuIconStealthLocked: icon {{ \"menu/stealth_locked\", mediaviewMenuFg }};\nmediaMenuIconStealth: icon {{ \"menu/stealth\", mediaviewMenuFg }};\nmediaMenuIconStats: icon {{ \"menu/stats\", mediaviewMenuFg }};\nmediaMenuIconRemove: icon {{ \"menu/remove\", mediaviewMenuFg }};\nmediaMenuIconRetractVote: icon {{ \"menu/retract_vote\", mediaviewMenuFg }};\n\nmediaMenuIconInfo: icon {{ \"menu/info\", mediaviewMenuFg }};\nmediaMenuIconBlock: icon {{ \"menu/block\", mediaviewMenuFg }};\n\nmenuIconDeleteAttention: icon {{ \"menu/delete\", menuIconAttentionColor }};\nmenuIconLeaveAttention: icon {{ \"menu/leave\", menuIconAttentionColor }};\nmenuIconDisableAttention: icon {{ \"menu/disable\", menuIconAttentionColor }};\nmenuIconReportAttention: icon {{ \"menu/report\", menuIconAttentionColor }};\nmenuIconRestoreAttention: icon {{ \"menu/restore\", menuIconAttentionColor }};\nmenuIconTagRemoveAttention: icon {{ \"menu/tag_remove\", menuIconAttentionColor }};\nmenuIconCancelAttention: icon {{ \"menu/cancel\", menuIconAttentionColor }};\nmenuIconBlockAttention: icon {{ \"menu/block\", menuIconAttentionColor }};\nmenuIconRemoveAttention: icon {{ \"menu/remove\", menuIconAttentionColor }};\n\nmenuIconBlockSettings: icon {{ \"menu/block\", windowBgActive }};\nmenuIconInviteSettings: icon {{ \"menu/invite\", lightButtonFg }};\n\nplayerSpeedSlow: icon {{ \"player/speed/audiospeed_menu_0.5\", menuIconColor }};\nplayerSpeedSlowActive: icon {{ \"player/speed/audiospeed_menu_0.5\", mediaPlayerActiveFg }};\nplayerSpeedNormal: icon {{ \"player/speed/audiospeed_menu_1.0\", menuIconColor }};\nplayerSpeedNormalActive: icon {{ \"player/speed/audiospeed_menu_1.0\", mediaPlayerActiveFg }};\nplayerSpeedMedium: icon {{ \"player/speed/audiospeed_menu_1.2\", menuIconColor }};\nplayerSpeedMediumActive: icon {{ \"player/speed/audiospeed_menu_1.2\", mediaPlayerActiveFg }};\nplayerSpeedFast: icon {{ \"player/speed/audiospeed_menu_1.5\", menuIconColor }};\nplayerSpeedFastActive: icon {{ \"player/speed/audiospeed_menu_1.5\", mediaPlayerActiveFg }};\nplayerSpeedVeryFast: icon {{ \"player/speed/audiospeed_menu_1.7\", menuIconColor }};\nplayerSpeedVeryFastActive: icon {{ \"player/speed/audiospeed_menu_1.7\", mediaPlayerActiveFg }};\nplayerSpeedSuperFast: icon {{ \"player/speed/audiospeed_menu_2.0\", menuIconColor }};\nplayerSpeedSuperFastActive: icon {{ \"player/speed/audiospeed_menu_2.0\", mediaPlayerActiveFg }};\n\nmediaSpeedSlow: icon {{ \"player/speed/audiospeed_menu_0.5\", mediaviewMenuFg }};\nmediaSpeedSlowActive: icon {{ \"player/speed/audiospeed_menu_0.5\", mediaviewTextLinkFg }};\nmediaSpeedNormal: icon {{ \"player/speed/audiospeed_menu_1.0\", mediaviewMenuFg }};\nmediaSpeedNormalActive: icon {{ \"player/speed/audiospeed_menu_1.0\", mediaviewTextLinkFg }};\nmediaSpeedMedium: icon {{ \"player/speed/audiospeed_menu_1.2\", mediaviewMenuFg }};\nmediaSpeedMediumActive: icon {{ \"player/speed/audiospeed_menu_1.2\", mediaviewTextLinkFg }};\nmediaSpeedFast: icon {{ \"player/speed/audiospeed_menu_1.5\", mediaviewMenuFg }};\nmediaSpeedFastActive: icon {{ \"player/speed/audiospeed_menu_1.5\", mediaviewTextLinkFg }};\nmediaSpeedVeryFast: icon {{ \"player/speed/audiospeed_menu_1.7\", mediaviewMenuFg }};\nmediaSpeedVeryFastActive: icon {{ \"player/speed/audiospeed_menu_1.7\", mediaviewTextLinkFg }};\nmediaSpeedSuperFast: icon {{ \"player/speed/audiospeed_menu_2.0\", mediaviewMenuFg }};\nmediaSpeedSuperFastActive: icon {{ \"player/speed/audiospeed_menu_2.0\", mediaviewTextLinkFg }};\n\nmenuIconMakeBig: icon {{ \"player/player_fullscreen\", menuIconColor }};\nmenuIconMakeSmall: icon {{ \"player/player_minimize\", menuIconColor }};\nmenuIconChangeOrder: icon {{ \"player/player_order\", menuIconColor }};\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/new_badges.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/new_badges.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/painter.h\"\n#include \"ui/widgets/labels.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_settings.h\"\n\nnamespace Ui::NewBadge {\n\nnot_null<Ui::RpWidget*> CreateNewBadge(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\trpl::producer<QString> text) {\n\tconst auto badge = Ui::CreateChild<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\tparent.get(),\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tparent,\n\t\t\tstd::move(text),\n\t\t\tst::settingsPremiumNewBadge),\n\t\tst::settingsPremiumNewBadgePadding);\n\tbadge->show();\n\tbadge->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tbadge->paintRequest() | rpl::on_next([=] {\n\t\tauto p = QPainter(badge);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::windowBgActive);\n\t\tconst auto r = st::settingsPremiumNewBadgePadding.left();\n\t\tp.drawRoundedRect(badge->rect(), r, r);\n\t}, badge->lifetime());\n\treturn badge;\n}\n\nvoid AddToRight(not_null<Ui::RpWidget*> parent) {\n\tconst auto badge = CreateNewBadge(parent, tr::lng_bot_side_menu_new());\n\n\tparent->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tbadge->moveToRight(\n\t\t\tst::mainMenuButton.padding.right(),\n\t\t\t(size.height() - badge->height()) / 2,\n\t\t\tsize.width());\n\t}, badge->lifetime());\n}\n\nvoid AddAfterLabel(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Ui::RpWidget*> label) {\n\tconst auto badge = CreateNewBadge(\n\t\tparent,\n\t\ttr::lng_premium_summary_new_badge());\n\n\tlabel->geometryValue(\n\t) | rpl::on_next([=](QRect geometry) {\n\t\tbadge->move(st::settingsPremiumNewBadgePosition\n\t\t\t+ QPoint(label->x() + label->width(), label->y()));\n\t}, badge->lifetime());\n}\n\n} // namespace Ui::NewBadge\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/new_badges.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Ui::NewBadge {\n\n[[nodiscard]] not_null<Ui::RpWidget*> CreateNewBadge(\n\tnot_null<Ui::RpWidget*> parent,\n\trpl::producer<QString> text);\n\nvoid AddToRight(not_null<Ui::RpWidget*> parent);\nvoid AddAfterLabel(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Ui::RpWidget*> label);\n\n} // namespace Ui::NewBadge\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/peer/color_sample.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/peer/color_sample.h\"\n\n#include \"base/algorithm.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/color_contrast.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"styles/style_settings.h\"\n\n#include <QtMath>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kSelectAnimationDuration = crl::time(150);\nconstexpr auto kUnsetColorIndex = uint8(0xFF);\nconstexpr auto kProfileColorIndexCount = uint8(8);\n\n} // namespace\n\nColorSample::ColorSample(\n\tnot_null<QWidget*> parent,\n\tFn<Ui::Text::MarkedContext()> contextProvider,\n\tFn<TextWithEntities(uint64)> emojiProvider,\n\tstd::shared_ptr<Ui::ChatStyle> style,\n\trpl::producer<uint8> colorIndex,\n\trpl::producer<std::shared_ptr<Ui::ColorCollectible>> collectible,\n\trpl::producer<QString> name)\n: AbstractButton(parent)\n, _style(style) {\n\trpl::combine(\n\t\tstd::move(colorIndex),\n\t\tstd::move(collectible),\n\t\tstd::move(name)\n\t) | rpl::on_next([=](\n\t\t\tuint8 index,\n\t\t\tstd::shared_ptr<Ui::ColorCollectible> collectible,\n\t\t\tconst QString &nameValue) {\n\t\t_index = index;\n\t\t_collectible = std::move(collectible);\n\t\tif (const auto raw = _collectible.get()) {\n\t\t\tauto context = contextProvider();\n\t\t\tcontext.repaint = [=] { update(); };\n\t\t\t_name.setMarkedText(\n\t\t\t\tst::semiboldTextStyle,\n\t\t\t\temojiProvider(raw->giftEmojiId),\n\t\t\t\tkMarkupTextOptions,\n\t\t\t\tstd::move(context));\n\t\t} else {\n\t\t\t_name.setText(st::semiboldTextStyle, nameValue);\n\t\t}\n\t\tsetNaturalWidth([&] {\n\t\t\tif (_name.isEmpty() || _style->colorPatternIndex(_index)) {\n\t\t\t\treturn st::settingsColorSampleSize;\n\t\t\t}\n\t\t\tconst auto padding = st::settingsColorSamplePadding;\n\t\t\treturn std::max(\n\t\t\t\tpadding.left() + _name.maxWidth() + padding.right(),\n\t\t\t\tpadding.top() + st::semiboldFont->height + padding.bottom());\n\t\t}());\n\t\tupdate();\n\t}, lifetime());\n}\n\nColorSample::ColorSample(\n\tnot_null<QWidget*> parent,\n\tstd::shared_ptr<Ui::ChatStyle> style,\n\tuint8 colorIndex,\n\tbool selected)\n: AbstractButton(parent)\n, _style(style)\n, _index(colorIndex)\n, _selected(selected)\n, _simple(true) {\n\tsetNaturalWidth(st::settingsColorSampleSize);\n}\n\nColorSample::ColorSample(\n\tnot_null<QWidget*> parent,\n\tFn<Data::ColorProfileSet(uint8)> profileProvider,\n\tuint8 colorIndex,\n\tbool selected)\n: AbstractButton(parent)\n, _index(colorIndex)\n, _selected(selected)\n, _simple(true)\n, _profileProvider(std::move(profileProvider)) {\n\tsetNaturalWidth(st::settingsColorSampleSize);\n}\n\nvoid ColorSample::setSelected(bool selected) {\n\tif (_selected == selected) {\n\t\treturn;\n\t}\n\t_selected = selected;\n\t_selectAnimation.start(\n\t\t[=] { update(); },\n\t\t_selected ? 0. : 1.,\n\t\t_selected ? 1. : 0.,\n\t\tkSelectAnimationDuration);\n}\n\nvoid ColorSample::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tif (_profileProvider) {\n\t\tconst auto size = float64(width());\n\t\tconst auto half = size / 2.;\n\t\tconst auto full = QRectF(-half, -half, size, size);\n\t\tp.translate(size / 2., size / 2.);\n\t\tp.setPen(Qt::NoPen);\n\n\t\tconst auto profile = _profileProvider(_index);\n\t\tif (!profile.palette.empty()) {\n\t\t\tif (profile.palette.size() == 2) {\n\t\t\t\tp.rotate(-45.);\n\t\t\t\tp.setClipRect(-size, 0, 3 * size, size);\n\t\t\t\tp.setBrush(profile.palette[1]);\n\t\t\t\tp.drawEllipse(full);\n\t\t\t\tp.setClipRect(-size, -size, 3 * size, size);\n\t\t\t}\n\t\t\tp.setBrush(profile.palette[0]);\n\t\t\tp.drawEllipse(full);\n\t\t}\n\t\tp.setClipping(false);\n\n\t\tconst auto selected = _selectAnimation.value(_selected ? 1. : 0.);\n\t\tif (selected > 0) {\n\t\t\tconst auto line = st::settingsColorRadioStroke * 1.;\n\t\t\tconst auto thickness = selected * line;\n\t\t\tconst auto skip = 1.5 * line;\n\t\t\tconst auto rect = full - Margins(skip);\n\t\t\tif (_selectionCutout) {\n\t\t\t\tp.save();\n\t\t\t\tp.setCompositionMode(QPainter::CompositionMode_Clear);\n\t\t\t\tauto pen = QPen(Qt::transparent, thickness);\n\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t\tp.setPen(pen);\n\t\t\t\tp.drawEllipse(rect);\n\t\t\t\tp.restore();\n\t\t\t} else {\n\t\t\t\tauto pen = st::boxBg->p;\n\t\t\t\tpen.setWidthF(thickness);\n\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t\tp.setPen(pen);\n\t\t\t\tp.drawEllipse(rect);\n\t\t\t}\n\t\t}\n\t\treturn;\n\t}\n\n\tconst auto colors = _style->coloredValues(false, _index);\n\tif (!_forceCircle && !_simple && !colors.outlines[1].alpha()) {\n\t\tconst auto radius = height() / 2;\n\t\tp.setPen(Qt::NoPen);\n\t\tif (const auto raw = _collectible.get()) {\n\t\t\tconst auto withBg = [&](const QColor &color) {\n\t\t\t\treturn Ui::CountContrast(st::windowBg->c, color);\n\t\t\t};\n\t\t\tconst auto dark = (withBg({ 0, 0, 0 })\n\t\t\t\t< withBg({ 255, 255, 255 }));\n\t\t\tconst auto name = (dark && raw->darkAccentColor.alpha() > 0)\n\t\t\t\t? raw->darkAccentColor\n\t\t\t\t: raw->accentColor;\n\t\t\tauto bg = name;\n\t\t\tbg.setAlpha(0.12 * 255);\n\t\t\tp.setBrush(bg);\n\t\t} else {\n\t\t\tp.setBrush(colors.bg);\n\t\t}\n\t\tp.drawRoundedRect(rect(), radius, radius);\n\n\t\tconst auto padding = st::settingsColorSamplePadding;\n\t\tp.setPen(colors.name);\n\t\tp.setBrush(Qt::NoBrush);\n\t\tp.setFont(st::semiboldFont);\n\t\t_name.drawLeftElided(\n\t\t\tp,\n\t\t\tpadding.left(),\n\t\t\tpadding.top(),\n\t\t\twidth() - padding.left() - padding.right(),\n\t\t\twidth(),\n\t\t\t1,\n\t\t\tstyle::al_top);\n\t} else {\n\t\tconst auto size = float64(width());\n\t\tconst auto half = size / 2.;\n\t\tconst auto full = QRectF(-half, -half, size, size);\n\t\tp.translate(size / 2., size / 2.);\n\t\tp.setPen(Qt::NoPen);\n\n\t\tauto combinedClip = QPainterPath();\n\t\tif (_cutoutPadding > 0) {\n\t\t\tcombinedClip.addRect(QRectF(-half, -half, size, size));\n\t\t\tauto cutout = QPainterPath();\n\t\t\tconst auto cutoutRadius = half;\n\t\t\tconst auto cutoutX = -half - _cutoutPadding;\n\t\t\tconst auto cutoutY = 0.0;\n\t\t\tif (colors.outlines[1].alpha()) {\n\t\t\t\tconst auto angle = M_PI / (180. / 45.);\n\t\t\t\tconst auto rotatedX = cutoutX * std::cos(angle)\n\t\t\t\t\t- cutoutY * std::sin(angle);\n\t\t\t\tconst auto rotatedY = cutoutX * std::sin(angle)\n\t\t\t\t\t+ cutoutY * std::cos(angle);\n\t\t\t\tcutout.addEllipse(\n\t\t\t\t\tQPointF(rotatedX, rotatedY),\n\t\t\t\t\tcutoutRadius,\n\t\t\t\t\tcutoutRadius);\n\t\t\t} else {\n\t\t\t\tcutout.addEllipse(\n\t\t\t\t\tQPointF(cutoutX, cutoutY),\n\t\t\t\t\tcutoutRadius,\n\t\t\t\t\tcutoutRadius);\n\t\t\t}\n\t\t\tcombinedClip = combinedClip.subtracted(cutout);\n\t\t}\n\n\t\tif (colors.outlines[1].alpha()) {\n\t\t\tp.rotate(-45.);\n\t\t\tif (_cutoutPadding > 0) {\n\t\t\t\tauto rectPath = QPainterPath();\n\t\t\t\trectPath.addRect(QRectF(-size, 0, 3 * size, size));\n\t\t\t\tauto clipRect = combinedClip.intersected(rectPath);\n\t\t\t\tp.setClipPath(clipRect);\n\t\t\t} else {\n\t\t\t\tp.setClipRect(-size, 0, 3 * size, size);\n\t\t\t}\n\t\t\tp.setBrush(colors.outlines[1]);\n\t\t\tp.drawEllipse(full);\n\t\t\tif (_cutoutPadding > 0) {\n\t\t\t\tauto rectPath = QPainterPath();\n\t\t\t\trectPath.addRect(QRectF(-size, -size, 3 * size, size));\n\t\t\t\tauto clipRect = combinedClip.intersected(rectPath);\n\t\t\t\tp.setClipPath(clipRect);\n\t\t\t} else {\n\t\t\t\tp.setClipRect(-size, -size, 3 * size, size);\n\t\t\t}\n\t\t} else if (_cutoutPadding > 0) {\n\t\t\tp.setClipPath(combinedClip);\n\t\t}\n\t\tp.setBrush(colors.outlines[0]);\n\t\tp.drawEllipse(full);\n\t\tp.setClipping(false);\n\t\tif (colors.outlines[2].alpha()) {\n\t\t\tconst auto multiplier = size / st::settingsColorSampleSize;\n\t\t\tconst auto center = st::settingsColorSampleCenter * multiplier;\n\t\t\tconst auto radius = st::settingsColorSampleCenterRadius\n\t\t\t\t* multiplier;\n\t\t\tp.setBrush(colors.outlines[2]);\n\t\t\tp.drawRoundedRect(\n\t\t\t\tQRectF(-center / 2., -center / 2., center, center),\n\t\t\t\tradius,\n\t\t\t\tradius);\n\t\t}\n\t\tconst auto selected = _selectAnimation.value(_selected ? 1. : 0.);\n\t\tif (selected > 0) {\n\t\t\tconst auto line = st::settingsColorRadioStroke * 1.;\n\t\t\tconst auto thickness = selected * line;\n\t\t\tconst auto skip = 1.5 * line;\n\t\t\tconst auto rect = full - Margins(skip);\n\t\t\tif (_selectionCutout) {\n\t\t\t\tp.save();\n\t\t\t\tp.setCompositionMode(QPainter::CompositionMode_Clear);\n\t\t\t\tauto pen = QPen(Qt::transparent, thickness);\n\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t\tp.setPen(pen);\n\t\t\t\tp.drawEllipse(rect);\n\t\t\t\tp.restore();\n\t\t\t} else {\n\t\t\t\tauto pen = st::boxBg->p;\n\t\t\t\tpen.setWidthF(thickness);\n\t\t\t\tp.setBrush(Qt::NoBrush);\n\t\t\t\tp.setPen(pen);\n\t\t\t\tp.drawEllipse(rect);\n\t\t\t}\n\t\t}\n\t}\n}\n\nuint8 ColorSample::index() const {\n\treturn _index;\n}\n\nvoid ColorSample::setCutoutPadding(int padding) {\n\tif (_cutoutPadding == padding) {\n\t\treturn;\n\t}\n\t_cutoutPadding = padding;\n\tupdate();\n}\n\nvoid ColorSample::setForceCircle(bool force) {\n\tif (_forceCircle == force) {\n\t\treturn;\n\t}\n\t_forceCircle = force;\n\tupdate();\n}\n\nvoid ColorSample::setSelectionCutout(bool cutout) {\n\tif (_selectionCutout == cutout) {\n\t\treturn;\n\t}\n\t_selectionCutout = cutout;\n\tsetAttribute(Qt::WA_TranslucentBackground, cutout);\n\tupdate();\n}\n\nColorSelector::ColorSelector(\n\tnot_null<QWidget*> parent,\n\tstd::shared_ptr<ChatStyle> style,\n\trpl::producer<std::vector<uint8>> indices,\n\trpl::producer<uint8> index,\n\tFn<void(uint8)> callback)\n: RpWidget(parent)\n, _style(style)\n, _callback(std::move(callback))\n, _index(std::move(index))\n, _isProfileMode(false) {\n\tstd::move(\n\t\tindices\n\t) | rpl::on_next([=](std::vector<uint8> indices) {\n\t\tfillFrom(std::move(indices));\n\t}, lifetime());\n\tsetupSelectionTracking();\n}\n\nColorSelector::ColorSelector(\n\tnot_null<QWidget*> parent,\n\tconst std::vector<uint8> &indices,\n\tuint8 index,\n\tFn<void(uint8)> callback,\n\tFn<Data::ColorProfileSet(uint8)> profileProvider)\n: RpWidget(parent)\n, _callback(std::move(callback))\n, _index(index)\n, _profileProvider(std::move(profileProvider))\n, _isProfileMode(true) {\n\tfillFrom(indices);\n}\n\nvoid ColorSelector::updateSelection(uint8 newIndex) {\n\tif (_index.current() == newIndex) {\n\t\treturn;\n\t}\n\tfor (const auto &sample : _samples) {\n\t\tif (sample->index() == _index.current()) {\n\t\t\tsample->setSelected(false);\n\t\t\tbreak;\n\t\t}\n\t}\n\tfor (const auto &sample : _samples) {\n\t\tif (sample->index() == newIndex) {\n\t\t\tsample->setSelected(true);\n\t\t\tbreak;\n\t\t}\n\t}\n\t_index = newIndex;\n}\n\nvoid ColorSelector::fillFrom(std::vector<uint8> indices) {\n\tauto samples = std::vector<std::unique_ptr<ColorSample>>();\n\tconst auto initial = _index.current();\n\tconst auto add = [&](uint8 index) {\n\t\tauto i = ranges::find(_samples, index, &ColorSample::index);\n\t\tif (i != end(_samples)) {\n\t\t\tsamples.push_back(std::move(*i));\n\t\t\t_samples.erase(i);\n\t\t} else {\n\t\t\tif (_isProfileMode) {\n\t\t\t\tsamples.push_back(std::make_unique<ColorSample>(\n\t\t\t\t\tthis,\n\t\t\t\t\t_profileProvider,\n\t\t\t\t\tindex,\n\t\t\t\t\tindex == initial));\n\t\t\t} else {\n\t\t\t\tsamples.push_back(std::make_unique<ColorSample>(\n\t\t\t\t\tthis,\n\t\t\t\t\t_style,\n\t\t\t\t\tindex,\n\t\t\t\t\tindex == initial));\n\t\t\t}\n\t\t\tsamples.back()->show();\n\t\t\tsamples.back()->setClickedCallback([=] {\n\t\t\t\tif (_isProfileMode) {\n\t\t\t\t\tupdateSelection(index);\n\t\t\t\t}\n\t\t\t\t_callback(index);\n\t\t\t});\n\t\t}\n\t};\n\tfor (const auto index : indices) {\n\t\tadd(index);\n\t}\n\tif (!_isProfileMode\n\t\t\t&& initial != 0xFF\n\t\t\t&& !ranges::contains(indices, initial)) {\n\t\tadd(initial);\n\t}\n\t_samples = std::move(samples);\n\tif (width() > 0) {\n\t\tresizeToWidth(width());\n\t}\n}\n\nvoid ColorSelector::setupSelectionTracking() {\n\tif (_isProfileMode) {\n\t\treturn;\n\t}\n\t_index.value(\n\t) | rpl::combine_previous(\n\t) | rpl::on_next([=](uint8 was, uint8 now) {\n\t\tconst auto i = ranges::find(_samples, was, &ColorSample::index);\n\t\tif (i != end(_samples)) {\n\t\t\ti->get()->setSelected(false);\n\t\t}\n\t\tconst auto j = ranges::find(_samples, now, &ColorSample::index);\n\t\tif (j != end(_samples)) {\n\t\t\tj->get()->setSelected(true);\n\t\t}\n\t}, lifetime());\n}\n\nint ColorSelector::resizeGetHeight(int newWidth) {\n\tif (newWidth <= 0) {\n\t\treturn 0;\n\t}\n\tconst auto count = int(_samples.size());\n\tconst auto columns = _isProfileMode\n\t\t? kProfileColorIndexCount\n\t\t: kSimpleColorIndexCount;\n\tconst auto skip = st::settingsColorRadioSkip;\n\tconst auto size = (newWidth - skip * (columns - 1)) / float64(columns);\n\tconst auto isize = int(base::SafeRound(size));\n\tauto top = 0;\n\tauto left = 0.;\n\tfor (auto i = 0; i != count; ++i) {\n\t\t_samples[i]->resize(isize, isize);\n\t\t_samples[i]->move(int(base::SafeRound(left)), top);\n\t\tleft += size + skip;\n\t\tif (!((i + 1) % columns)) {\n\t\t\ttop += isize + skip;\n\t\t\tleft = 0.;\n\t\t}\n\t}\n\treturn (top - skip) + ((count % columns) ? (isize + skip) : 0);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/peer/color_sample.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/buttons.h\"\n#include \"ui/text/text.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/rp_widget.h\"\n#include \"data/data_peer_colors.h\"\n\nnamespace Ui {\nnamespace Text {\nstruct MarkedContext;\n} // namespace Text\nclass ChatStyle;\nstruct ColorCollectible;\n\nclass ColorSample final : public AbstractButton {\npublic:\n\tColorSample(\n\t\tnot_null<QWidget*> parent,\n\t\tFn<Ui::Text::MarkedContext()> contextProvider,\n\t\tFn<TextWithEntities(uint64)> emojiProvider,\n\t\tstd::shared_ptr<ChatStyle> style,\n\t\trpl::producer<uint8> colorIndex,\n\t\trpl::producer<std::shared_ptr<ColorCollectible>> collectible,\n\t\trpl::producer<QString> name);\n\tColorSample(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::shared_ptr<ChatStyle> style,\n\t\tuint8 colorIndex,\n\t\tbool selected);\n\tColorSample(\n\t\tnot_null<QWidget*> parent,\n\t\tFn<Data::ColorProfileSet(uint8)> profileProvider,\n\t\tuint8 colorIndex,\n\t\tbool selected);\n\n\t[[nodiscard]] uint8 index() const;\n\n\tvoid setSelected(bool selected);\n\tvoid setCutoutPadding(int padding);\n\tvoid setForceCircle(bool force);\n\tvoid setSelectionCutout(bool cutout);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tstd::shared_ptr<ChatStyle> _style;\n\tText::String _name;\n\tuint8 _index = 0;\n\tstd::shared_ptr<ColorCollectible> _collectible;\n\tAnimations::Simple _selectAnimation;\n\tbool _selected = false;\n\tbool _simple = false;\n\tFn<Data::ColorProfileSet(uint8)> _profileProvider;\n\tint _cutoutPadding = 0;\n\tbool _forceCircle = false;\n\tbool _selectionCutout = false;\n\n};\n\nclass ColorSelector final : public RpWidget {\npublic:\n\tColorSelector(\n\t\tnot_null<QWidget*> parent,\n\t\tstd::shared_ptr<ChatStyle> style,\n\t\trpl::producer<std::vector<uint8>> indices,\n\t\trpl::producer<uint8> index,\n\t\tFn<void(uint8)> callback);\n\tColorSelector(\n\t\tnot_null<QWidget*> parent,\n\t\tconst std::vector<uint8> &indices,\n\t\tuint8 index,\n\t\tFn<void(uint8)> callback,\n\t\tFn<Data::ColorProfileSet(uint8)> profileProvider);\n\n\tvoid updateSelection(uint8 newIndex);\n\nprivate:\n\tvoid fillFrom(std::vector<uint8> indices);\n\tvoid setupSelectionTracking();\n\n\tint resizeGetHeight(int newWidth) override;\n\n\tstd::shared_ptr<ChatStyle> _style;\n\tstd::vector<std::unique_ptr<ColorSample>> _samples;\n\tFn<void(uint8)> _callback;\n\trpl::variable<uint8> _index;\n\tFn<Data::ColorProfileSet(uint8)> _profileProvider;\n\tbool _isProfileMode = false;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/peer/video_userpic_player.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/peer/video_userpic_player.h\"\n\n#include \"data/data_peer.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"data/data_streaming.h\"\n#include \"data/data_file_origin.h\"\n#include \"media/streaming/media_streaming_instance.h\"\n#include \"media/streaming/media_streaming_player.h\"\n#include \"media/streaming/media_streaming_document.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Ui {\n\nVideoUserpicPlayer::VideoUserpicPlayer() = default;\n\nVideoUserpicPlayer::~VideoUserpicPlayer() = default;\n\nvoid VideoUserpicPlayer::setup(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<PhotoData*> photo) {\n\tif (_streamedPhoto == photo && _streamed && _peer == peer) {\n\t\treturn;\n\t}\n\tif (!createStreaming(peer, photo)) {\n\t\tphoto->setVideoPlaybackFailed();\n\t\treturn;\n\t}\n\t_streamedPhoto = photo;\n\t_peer = peer;\n\tcheckStarted();\n}\n\nvoid VideoUserpicPlayer::clear() {\n\t_streamed = nullptr;\n\t_streamedPhoto = nullptr;\n\t_peer = nullptr;\n}\n\nQImage VideoUserpicPlayer::frame(QSize size, not_null<PeerData*> peer) {\n\tif (!_streamed\n\t\t|| !_streamed->player().ready()\n\t\t|| _streamed->player().videoSize().isEmpty()\n\t\t|| !_streamedPhoto) {\n\t\treturn {};\n\t}\n\tauto request = ::Media::Streaming::FrameRequest();\n\tconst auto ratio = style::DevicePixelRatio();\n\trequest.outer = request.resize = size * ratio;\n\n\tconst auto broadcast = peer->monoforumBroadcast();\n\n\tif (broadcast) {\n\t\tif (_monoforumMask.isNull()) {\n\t\t\t_monoforumMask = Ui::MonoforumShapeMask(request.resize);\n\t\t}\n\t} else if (peer->isForum()) {\n\t\tconst auto radius = int(\n\t\t\tsize.width() * Ui::ForumUserpicRadiusMultiplier());\n\t\tif (_roundingCorners[0].width() != radius * ratio) {\n\t\t\t_roundingCorners = Images::CornersMask(radius);\n\t\t}\n\t\trequest.rounding = Images::CornersMaskRef(_roundingCorners);\n\t} else {\n\t\tif (_ellipseMask.size() != request.outer) {\n\t\t\t_ellipseMask = Images::EllipseMask(size);\n\t\t}\n\t\trequest.mask = _ellipseMask;\n\t}\n\n\tauto result = _streamed->frame(request);\n\tif (broadcast) {\n\t\tconstexpr auto kFormat = QImage::Format_ARGB32_Premultiplied;\n\t\tif (result.format() != kFormat) {\n\t\t\tresult = std::move(result).convertToFormat(kFormat);\n\t\t}\n\t\tauto q = QPainter(&result);\n\t\tq.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\tq.drawImage(\n\t\t\tQRect(QPoint(), result.size() / result.devicePixelRatio()),\n\t\t\t_monoforumMask);\n\t\tq.end();\n\t}\n\t_streamed->markFrameShown();\n\treturn result;\n}\n\nbool VideoUserpicPlayer::ready() const {\n\treturn _streamed\n\t\t&& _streamed->player().ready()\n\t\t&& !_streamed->player().videoSize().isEmpty();\n}\n\nbool VideoUserpicPlayer::createStreaming(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<PhotoData*> photo) {\n\tusing namespace ::Media::Streaming;\n\tconst auto origin = peer->isUser()\n\t\t? Data::FileOriginUserPhoto(peerToUser(peer->id), photo->id)\n\t\t: Data::FileOrigin(Data::FileOriginPeerPhoto(peer->id));\n\t_streamed = std::make_unique<Instance>(\n\t\tphoto->owner().streaming().sharedDocument(photo, origin),\n\t\tnullptr);\n\t_streamed->lockPlayer();\n\t_streamed->player().updates(\n\t) | rpl::on_next_error([=](Update &&update) {\n\t\thandleUpdate(std::move(update));\n\t}, [=](Error &&error) {\n\t\thandleError(std::move(error));\n\t}, _streamed->lifetime());\n\tif (_streamed->ready()) {\n\t\tstreamingReady(base::duplicate(_streamed->info()));\n\t}\n\tif (!_streamed->valid()) {\n\t\tclear();\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nvoid VideoUserpicPlayer::checkStarted() {\n\tif (!_streamed) {\n\t\treturn;\n\t} else if (_streamed->paused()) {\n\t\t_streamed->resume();\n\t}\n\tif (_streamed && !_streamed->active() && !_streamed->failed()) {\n\t\tconst auto position = _streamedPhoto->videoStartPosition();\n\t\tauto options = ::Media::Streaming::PlaybackOptions();\n\t\toptions.position = position;\n\t\toptions.mode = ::Media::Streaming::Mode::Video;\n\t\toptions.loop = true;\n\t\t_streamed->play(options);\n\t}\n}\n\nvoid VideoUserpicPlayer::handleUpdate(\n\t\t::Media::Streaming::Update &&update) {\n\tusing namespace ::Media::Streaming;\n\tv::match(update.data, [&](Information &update) {\n\t\tstreamingReady(std::move(update));\n\t}, [](PreloadedVideo) {\n\t}, [](UpdateVideo) {\n\t}, [](PreloadedAudio) {\n\t}, [](UpdateAudio) {\n\t}, [](WaitingForData) {\n\t}, [](SpeedEstimate) {\n\t}, [](MutedByOther) {\n\t}, [](Finished) {\n\t});\n}\n\nvoid VideoUserpicPlayer::handleError(::Media::Streaming::Error &&error) {\n\t_streamedPhoto->setVideoPlaybackFailed();\n\tclear();\n}\n\nvoid VideoUserpicPlayer::streamingReady(\n\t\t::Media::Streaming::Information &&info) {\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/peer/video_userpic_player.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Media::Streaming {\nclass Instance;\nstruct Update;\nstruct Information;\nenum class Error;\n} // namespace Media::Streaming\n\nclass PhotoData;\nclass PeerData;\n\nnamespace Ui {\n\nclass VideoUserpicPlayer final {\npublic:\n\tVideoUserpicPlayer();\n\t~VideoUserpicPlayer();\n\n\tvoid setup(not_null<PeerData*> peer, not_null<PhotoData*> photo);\n\tvoid clear();\n\n\t[[nodiscard]] QImage frame(QSize size, not_null<PeerData*> peer);\n\t[[nodiscard]] bool ready() const;\n\nprivate:\n\tbool createStreaming(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<PhotoData*> photo);\n\tvoid checkStarted();\n\tvoid handleUpdate(::Media::Streaming::Update &&update);\n\tvoid handleError(::Media::Streaming::Error &&error);\n\tvoid streamingReady(::Media::Streaming::Information &&info);\n\n\tstd::unique_ptr<::Media::Streaming::Instance> _streamed;\n\tPhotoData *_streamedPhoto = nullptr;\n\tQImage _ellipseMask;\n\tQImage _monoforumMask;\n\tstd::array<QImage, 4> _roundingCorners;\n\tPeerData *_peer = nullptr;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/power_saving.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/power_saving.h\"\n\nnamespace PowerSaving {\nnamespace {\n\nFlags Data/* = {}*/;\nrpl::event_stream<> Events;\nbool AllForced/* = false*/;\n\n} // namespace\n\nvoid Set(Flags flags) {\n\tif (const auto diff = Data ^ flags) {\n\t\tData = flags;\n\t\tif (!AllForced) {\n\t\t\tif (diff & kAnimations) {\n\t\t\t\tanim::SetDisabled(On(kAnimations));\n\t\t\t}\n\t\t\tEvents.fire({});\n\t\t}\n\t}\n}\n\nFlags Current() {\n\treturn Data;\n}\n\nvoid SetForceAll(bool force) {\n\tif (AllForced == force) {\n\t\treturn;\n\t}\n\tAllForced = force;\n\tif (const auto diff = Data ^ kAll) {\n\t\tif (diff & kAnimations) {\n\t\t\tanim::SetDisabled(On(kAnimations));\n\t\t}\n\t\tEvents.fire({});\n\t}\n}\n\nbool ForceAll() {\n\treturn AllForced;\n}\n\nrpl::producer<> Changes() {\n\treturn Events.events();\n}\n\n} // namespace PowerSaving\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/power_saving.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace PowerSaving {\n\nenum Flag : uint32 {\n\tkAnimations = (1U << 0),\n\tkStickersPanel = (1U << 1),\n\tkStickersChat = (1U << 2),\n\tkEmojiPanel = (1U << 3),\n\tkEmojiReactions = (1U << 4),\n\tkEmojiChat = (1U << 5),\n\tkChatBackground = (1U << 6),\n\tkChatSpoiler = (1U << 7),\n\tkCalls = (1U << 8),\n\tkEmojiStatus = (1U << 9),\n\tkChatEffects = (1U << 10),\n\n\tkAll = (1U << 11) - 1,\n};\ninline constexpr bool is_flag_type(Flag) { return true; }\nusing Flags = base::flags<Flag>;\n\nvoid Set(Flags flags);\n[[nodiscard]] Flags Current();\n\nvoid SetForceAll(bool force);\n[[nodiscard]] bool ForceAll();\n\n[[nodiscard]] rpl::producer<> Changes();\n\n[[nodiscard]] inline bool On(Flag flag) {\n\treturn ForceAll() || (Current() & flag);\n}\n[[nodiscard]] inline rpl::producer<bool> OnValue(Flag flag) {\n\treturn rpl::single(On(flag)) | rpl::then(Changes() | rpl::map([=] {\n\t\treturn On(flag);\n\t})) | rpl::distinct_until_changed();\n}\n\n} // namespace PowerSaving\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/resize_area.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\n\nclass ResizeArea : public RpWidget {\npublic:\n\tResizeArea(QWidget *parent) : RpWidget(parent) {\n\t\tsetCursor(style::cur_sizehor);\n\t}\n\n\trpl::producer<int> moveLeft() const {\n\t\treturn _moveLeft.events();\n\t}\n\ttemplate <typename Callback>\n\tvoid addMoveLeftCallback(Callback &&callback) {\n\t\tmoveLeft(\n\t\t) | rpl::on_next(\n\t\t\tstd::forward<Callback>(callback),\n\t\t\tlifetime());\n\t}\n\n\trpl::producer<> moveFinished() const {\n\t\treturn _moveFinished.events();\n\t}\n\ttemplate <typename Callback>\n\tvoid addMoveFinishedCallback(Callback &&callback) {\n\t\tmoveFinished(\n\t\t) | rpl::on_next(\n\t\t\tstd::forward<Callback>(callback),\n\t\t\tlifetime());\n\t}\n\n\t~ResizeArea() {\n\t\tmoveFinish();\n\t}\n\nprotected:\n\tvoid mousePressEvent(QMouseEvent *e) override {\n\t\tif (e->button() == Qt::LeftButton) {\n\t\t\t_moving = true;\n\t\t\t_moveStartLeft = e->pos().x();\n\t\t}\n\t}\n\tvoid mouseReleaseEvent(QMouseEvent *e) override {\n\t\tif (e->button() == Qt::LeftButton) {\n\t\t\tmoveFinish();\n\t\t}\n\t}\n\tvoid mouseMoveEvent(QMouseEvent *e) override {\n\t\tif (_moving) {\n\t\t\t_moveLeft.fire(e->globalPos().x() - _moveStartLeft);\n\t\t}\n\t}\n\nprivate:\n\tvoid moveFinish() {\n\t\tif (base::take(_moving)) {\n\t\t\t_moveFinished.fire({});\n\t\t}\n\t}\n\n\trpl::event_stream<int> _moveLeft;\n\trpl::event_stream<> _moveFinished;\n\tint _moveStartLeft = 0;\n\tbool _moving = false;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/search_field_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/search_field_controller.h\"\n\n#include \"styles/style_widgets.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"lang/lang_keys.h\"\n\nnamespace Ui {\n\nSearchFieldController::SearchFieldController(const QString &query)\n: _query(query) {\n}\n\nauto SearchFieldController::createRowView(\n\t\tQWidget *parent,\n\t\tconst style::SearchFieldRow &st) -> RowView {\n\tauto result = base::make_unique_q<Ui::FixedHeightWidget>(\n\t\tparent,\n\t\tst.height);\n\tauto wrap = result.get();\n\n\tauto field = createField(wrap, st.field).release();\n\tfield->show();\n\n\tauto cancel = CreateChild<Ui::CrossButton>(\n\t\twrap,\n\t\tst.fieldCancel);\n\tcancel->addClickHandler([=] {\n\t\tfield->setText(QString());\n\t});\n\tqueryValue(\n\t) | rpl::map([](const QString &value) {\n\t\treturn !value.isEmpty();\n\t}) | rpl::on_next([cancel](bool shown) {\n\t\tcancel->toggle(shown, anim::type::normal);\n\t\tcancel->setAccessibleName(tr::lng_sr_clear_search(tr::now));\n\t}, cancel->lifetime());\n\tcancel->finishAnimating();\n\n\tauto shadow = CreateChild<Ui::PlainShadow>(wrap);\n\tshadow->show();\n\n\twrap->widthValue(\n\t) | rpl::on_next([=, &st](int newWidth) {\n\t\tauto availableWidth = newWidth\n\t\t\t- st.fieldIconSkip\n\t\t\t- st.fieldCancelSkip;\n\t\tfield->setGeometryToLeft(\n\t\t\tst.padding.left() + st.fieldIconSkip,\n\t\t\tst.padding.top(),\n\t\t\tavailableWidth,\n\t\t\tfield->height());\n\t\tcancel->moveToRight(0, 0);\n\t\tshadow->setGeometry(\n\t\t\t0,\n\t\t\tst.height - st::lineWidth,\n\t\t\tnewWidth,\n\t\t\tst::lineWidth);\n\t}, wrap->lifetime());\n\twrap->paintRequest(\n\t) | rpl::on_next([=, &st] {\n\t\tauto p = QPainter(wrap);\n\t\tst.fieldIcon.paint(\n\t\t\tp,\n\t\t\tst.padding.left(),\n\t\t\tst.padding.top(),\n\t\t\twrap->width());\n\t}, wrap->lifetime());\n\n\t_view.release();\n\t_view.reset(wrap);\n\treturn { std::move(result), field };\n}\n\nQString SearchFieldController::query() const {\n\treturn _query.current();\n}\n\nrpl::producer<QString> SearchFieldController::queryValue() const {\n\treturn _query.value();\n}\n\nrpl::producer<QString> SearchFieldController::queryChanges() const {\n\treturn _query.changes();\n}\n\nvoid SearchFieldController::setQuery(const QString &query) {\n\t_query = query;\n}\n\nbase::unique_qptr<Ui::InputField> SearchFieldController::createField(\n\t\tQWidget *parent,\n\t\tconst style::InputField &st) {\n\tauto result = base::make_unique_q<Ui::InputField>(\n\t\tparent,\n\t\tst,\n\t\ttr::lng_dlg_filter(),\n\t\t_query.current());\n\tauto field = result.get();\n\tfield->changes(\n\t) | rpl::on_next([=] {\n\t\t_query = field->getLastText();\n\t}, field->lifetime());\n\t_view.reset(field);\n\treturn result;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/search_field_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include <rpl/variable.h>\n#include \"ui/rp_widget.h\"\n#include \"base/unique_qptr.h\"\n\nnamespace style {\nstruct SearchFieldRow;\nstruct InputField;\n} // namespace style\n\nnamespace Ui {\n\nclass CrossButton;\nclass InputField;\n\nclass SearchFieldController {\npublic:\n\tSearchFieldController(const QString &query);\n\n\tbase::unique_qptr<Ui::InputField> createField(\n\t\tQWidget *parent,\n\t\tconst style::InputField &st);\n\tstruct RowView {\n\t\tbase::unique_qptr<Ui::RpWidget> wrap;\n\t\tQPointer<Ui::InputField> field;\n\t};\n\tRowView createRowView(\n\t\tQWidget *parent,\n\t\tconst style::SearchFieldRow &st);\n\n\tQString query() const;\n\trpl::producer<QString> queryValue() const;\n\trpl::producer<QString> queryChanges() const;\n\n\tvoid setQuery(const QString &query);\n\n\trpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tbase::unique_qptr<QWidget> _view;\n\trpl::variable<QString> _query;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/td_common.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/colors.palette\";\nusing \"ui/widgets/widgets.style\";\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/text/format_song_document_name.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/text/format_song_document_name.h\"\n\n#include \"data/data_document.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"history/history_item_helpers.h\"\n#include \"history/history_item.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/text/text_utilities.h\"\n\n#include <QtCore/QLocale>\n\nnamespace Ui::Text {\n\nFormatSongName FormatSongNameFor(not_null<DocumentData*> document) {\n\tconst auto song = document->song();\n\n\treturn FormatSongName(\n\t\tdocument->filename(),\n\t\tsong ? song->title : QString(),\n\t\tsong ? song->performer : QString());\n}\n\nTextWithEntities FormatDownloadsName(not_null<DocumentData*> document) {\n\treturn document->isVideoFile()\n\t\t? Bold(tr::lng_in_dlg_video(tr::now))\n\t\t: document->isVoiceMessage()\n\t\t? Bold(tr::lng_in_dlg_audio(tr::now))\n\t\t: document->isVideoMessage()\n\t\t? Bold(tr::lng_in_dlg_video_message(tr::now))\n\t\t: document->sticker()\n\t\t? Bold(document->sticker()->alt.isEmpty()\n\t\t\t? tr::lng_in_dlg_sticker(tr::now)\n\t\t\t: tr::lng_in_dlg_sticker_emoji(\n\t\t\t\ttr::now,\n\t\t\t\tlt_emoji,\n\t\t\t\tdocument->sticker()->alt))\n\t\t: FormatSongNameFor(document).textWithEntities();\n}\n\nFormatSongName FormatVoiceName(\n\t\tnot_null<DocumentData*> document,\n\t\tFullMsgId contextId) {\n\tif (const auto item = document->owner().message(contextId)) {\n\t\tconst auto name = (!item->out() || item->isPost())\n\t\t\t? item->fromOriginal()->name()\n\t\t\t: tr::lng_from_you(tr::now);\n\t\tconst auto date = [item] {\n\t\t\tconst auto parsed = ItemDateTime(item);\n\t\t\tconst auto date = parsed.date();\n\t\t\tconst auto time = QLocale().toString(\n\t\t\t\tparsed.time(),\n\t\t\t\tQLocale::ShortFormat);\n\t\t\tconst auto today = QDateTime::currentDateTime().date();\n\t\t\tif (date == today) {\n\t\t\t\treturn tr::lng_player_message_today(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_time,\n\t\t\t\t\ttime);\n\t\t\t} else if (date.addDays(1) == today) {\n\t\t\t\treturn tr::lng_player_message_yesterday(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_time,\n\t\t\t\t\ttime);\n\t\t\t}\n\t\t\treturn tr::lng_player_message_date(\n\t\t\t\ttr::now,\n\t\t\t\tlt_date,\n\t\t\t\tlangDayOfMonthFull(date),\n\t\t\t\tlt_time,\n\t\t\t\ttime);\n\t\t};\n\t\tauto result = FormatSongName(QString(), date(), name);\n\t\tresult.setNoDash(true);\n\t\treturn result;\n\t} else if (document->isVideoMessage()) {\n\t\treturn FormatSongName(QString(), tr::lng_media_round(tr::now), {});\n\t} else {\n\t\treturn FormatSongName(QString(), tr::lng_media_audio(tr::now), {});\n\t}\n}\n\n} // namespace Ui::Text\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/text/format_song_document_name.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/text/format_song_name.h\"\n\nclass DocumentData;\n\nnamespace Ui::Text {\n\n[[nodiscard]] FormatSongName FormatSongNameFor(\n\tnot_null<DocumentData*> document);\n\n[[nodiscard]] TextWithEntities FormatDownloadsName(\n\tnot_null<DocumentData*> document);\n\n[[nodiscard]] FormatSongName FormatVoiceName(\n\tnot_null<DocumentData*> document,\n\tFullMsgId contextId);\n\n} // namespace Ui::Text\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/text/format_song_name.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/text/format_song_name.h\"\n\nnamespace Ui::Text {\n\nnamespace {\n\nFormatSongName::ComposedName ComputeComposedName(\n\t\tconst QString &filename,\n\t\tconst QString &songTitle,\n\t\tconst QString &songPerformer) {\n\tconst auto unknown = u\"Unknown Track\"_q;\n\tif (songTitle.isEmpty() && songPerformer.isEmpty()) {\n\t\treturn {\n\t\t\t.title = filename.isEmpty() ? unknown : filename,\n\t\t\t.performer = QString(),\n\t\t};\n\t}\n\n\tif (songPerformer.isEmpty()) {\n\t\treturn {\n\t\t\t.title = songTitle,\n\t\t\t.performer = QString(),\n\t\t};\n\t}\n\n\treturn {\n\t\t.title = (songTitle.isEmpty() ? unknown : songTitle),\n\t\t.performer = songPerformer,\n\t};\n}\n\n} // namespace\n\nFormatSongName::FormatSongName(\n\tconst QString &filename,\n\tconst QString &songTitle,\n\tconst QString &songPerformer)\n: _composedName(ComputeComposedName(filename, songTitle, songPerformer)) {\n}\n\nFormatSongName::ComposedName FormatSongName::composedName() const {\n\treturn _composedName;\n}\n\nQString FormatSongName::string() const {\n\tconst auto &[title, performer] = _composedName;\n\tconst auto dash = (title.isEmpty() || performer.isEmpty())\n\t\t? QString()\n\t\t: _noDash\n\t\t? QString(' ')\n\t\t: QString::fromUtf8(\" \\xe2\\x80\\x93 \");\n\treturn performer + dash + title;\n}\n\nvoid FormatSongName::setNoDash(bool noDash) {\n\t_noDash = noDash;\n}\n\nTextWithEntities FormatSongName::textWithEntities(\n\t\tbool boldOnlyPerformer) const {\n\tTextWithEntities result;\n\tresult.text = string();\n\tif (!boldOnlyPerformer || !_composedName.performer.isEmpty()) {\n\t\tresult.entities.push_back({\n\t\t\tEntityType::Semibold,\n\t\t\t0,\n\t\t\t_composedName.performer.isEmpty()\n\t\t\t\t? int(result.text.size())\n\t\t\t\t: int(_composedName.performer.size()),\n\t\t});\n\t}\n\treturn result;\n}\n\n} // namespace Ui::Text\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/text/format_song_name.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui::Text {\n\nclass FormatSongName final {\npublic:\n\tstruct ComposedName {\n\t\tQString title;\n\t\tQString performer;\n\t};\n\n\tFormatSongName(\n\t\tconst QString &filename,\n\t\tconst QString &songTitle,\n\t\tconst QString &songPerformer);\n\n\t[[nodiscard]] ComposedName composedName() const;\n\t[[nodiscard]] QString string() const;\n\t[[nodiscard]] TextWithEntities textWithEntities(\n\t\tbool boldOnlyPerformer = false) const;\n\n\tvoid setNoDash(bool noDash);\n\nprivate:\n\tconst ComposedName _composedName;\n\tbool _noDash = false;\n\n};\n\n} // namespace Ui::Text\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/text/format_values.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/text/format_values.h\"\n\n#include \"base/unixtime.h\"\n#include \"lang/lang_keys.h\"\n#include \"countries/countries_instance.h\"\n\n#include <QtCore/QLocale>\n#include <locale>\n#include <sstream>\n#include <iostream>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kSecondsInYear = 365 * 24 * 60 * 60; // 31536000\n\n[[nodiscard]] QString FormatTextWithReadyAndTotal(\n\t\ttr::phrase<lngtag_ready, lngtag_total, lngtag_mb> phrase,\n\t\tqint64 ready,\n\t\tqint64 total) {\n\tQString readyStr, totalStr, mb;\n\tif (total >= 1024 * 1024) { // more than 1 mb\n\t\tconst qint64 readyTenthMb = (ready * 10 / (1024 * 1024));\n\t\tconst qint64 totalTenthMb = (total * 10 / (1024 * 1024));\n\t\treadyStr = QString::number(readyTenthMb / 10)\n\t\t\t+ '.'\n\t\t\t+ QString::number(readyTenthMb % 10);\n\t\ttotalStr = QString::number(totalTenthMb / 10)\n\t\t\t+ '.'\n\t\t\t+ QString::number(totalTenthMb % 10);\n\t\tmb = u\"MB\"_q;\n\t} else if (total >= 1024) {\n\t\tqint64 readyKb = (ready / 1024), totalKb = (total / 1024);\n\t\treadyStr = QString::number(readyKb);\n\t\ttotalStr = QString::number(totalKb);\n\t\tmb = u\"KB\"_q;\n\t} else {\n\t\treadyStr = QString::number(ready);\n\t\ttotalStr = QString::number(total);\n\t\tmb = u\"B\"_q;\n\t}\n\treturn phrase(tr::now, lt_ready, readyStr, lt_total, totalStr, lt_mb, mb);\n}\n\n} // namespace\n\nQString FormatSizeText(qint64 size) {\n\tif (size >= 1024 * 1024) { // more than 1 mb\n\t\tconst qint64 sizeTenthMb = (size * 10 / (1024 * 1024));\n\t\treturn QString::number(sizeTenthMb / 10)\n\t\t\t+ '.'\n\t\t\t+ QString::number(sizeTenthMb % 10) + u\" MB\"_q;\n\t}\n\tif (size >= 1024) {\n\t\tconst qint64 sizeTenthKb = (size * 10 / 1024);\n\t\treturn QString::number(sizeTenthKb / 10)\n\t\t\t+ '.'\n\t\t\t+ QString::number(sizeTenthKb % 10) + u\" KB\"_q;\n\t}\n\treturn QString::number(size) + u\" B\"_q;\n}\n\nQString FormatDownloadText(qint64 ready, qint64 total) {\n\treturn FormatTextWithReadyAndTotal(\n\t\ttr::lng_save_downloaded,\n\t\tready,\n\t\ttotal);\n}\n\nQString FormatProgressText(qint64 ready, qint64 total) {\n\treturn FormatTextWithReadyAndTotal(\n\t\ttr::lng_media_save_progress,\n\t\tready,\n\t\ttotal);\n}\n\nQString FormatDateTime(QDateTime date) {\n\tconst auto now = QDateTime::currentDateTime();\n\tif (date.date() == now.date()) {\n\t\treturn tr::lng_mediaview_today(\n\t\t\ttr::now,\n\t\t\tlt_time,\n\t\t\tQLocale().toString(date.time(), QLocale::ShortFormat));\n\t} else if (date.date().addDays(1) == now.date()) {\n\t\treturn tr::lng_mediaview_yesterday(\n\t\t\ttr::now,\n\t\t\tlt_time,\n\t\t\tQLocale().toString(date.time(), QLocale::ShortFormat));\n\t} else {\n\t\treturn tr::lng_mediaview_date_time(\n\t\t\ttr::now,\n\t\t\tlt_date,\n\t\t\tQLocale().toString(date.date(), QLocale::ShortFormat),\n\t\t\tlt_time,\n\t\t\tQLocale().toString(date.time(), QLocale::ShortFormat));\n\t}\n}\n\nQString FormatDateTimeSavedFrom(QDateTime dateTime) {\n\tconst auto current = QDate::currentDate();\n\tconst auto date = dateTime.date();\n\tconst auto timeStr = QLocale().toString(\n\t\tdateTime.time(),\n\t\tQLocale::ShortFormat);\n\n\tif (date == current) {\n\t\treturn tr::lng_mediaview_today(tr::now, lt_time, timeStr);\n\t} else if (date == current.addDays(-1)) {\n\t\treturn tr::lng_mediaview_yesterday(tr::now, lt_time, timeStr);\n\t}\n\tconst auto diff = std::abs(\n\t\tbase::unixtime::now() - base::unixtime::serialize(dateTime));\n\tconst auto dateStr = (diff < kSecondsInYear)\n\t\t? tr::lng_month_day(\n\t\t\ttr::now,\n\t\t\tlt_month,\n\t\t\tLang::MonthSmall(date.month())(tr::now),\n\t\t\tlt_day,\n\t\t\tQString::number(date.day()))\n\t\t: langDayOfMonthFull(date);\n\n\treturn tr::lng_mediaview_date_time(\n\t\ttr::now,\n\t\tlt_date,\n\t\tdateStr,\n\t\tlt_time,\n\t\ttimeStr);\n}\n\nQString FormatDurationText(qint64 duration) {\n\tqint64 hours = (duration / 3600), minutes = (duration % 3600) / 60, seconds = duration % 60;\n\treturn (hours ? QString::number(hours) + ':' : QString()) + (minutes >= 10 ? QString() : QString('0')) + QString::number(minutes) + ':' + (seconds >= 10 ? QString() : QString('0')) + QString::number(seconds);\n}\n\nQString FormatDurationWords(qint64 duration) {\n\tif (duration > 59) {\n\t\tauto minutes = (duration / 60);\n\t\tauto minutesCount = tr::lng_duration_minsec_minutes(tr::now, lt_count, minutes);\n\t\tauto seconds = (duration % 60);\n\t\tauto secondsCount = tr::lng_duration_minsec_seconds(tr::now, lt_count, seconds);\n\t\treturn tr::lng_duration_minutes_seconds(tr::now, lt_minutes_count, minutesCount, lt_seconds_count, secondsCount);\n\t}\n\treturn tr::lng_seconds(tr::now, lt_count, duration);\n}\n\nQString FormatDurationWordsSlowmode(qint64 duration) {\n\tif (duration > 59) {\n\t\tauto minutes = (duration / 60);\n\t\tauto minutesCount = tr::lng_duration_minsec_minutes(tr::now, lt_count, minutes);\n\t\tauto seconds = (duration % 60);\n\t\tauto secondsCount = tr::lng_duration_minsec_seconds(tr::now, lt_count, seconds);\n\t\treturn tr::lng_duration_minutes_seconds(tr::now, lt_minutes_count, minutesCount, lt_seconds_count, secondsCount);\n\t}\n\treturn tr::lng_slowmode_seconds(tr::now, lt_count, duration);\n}\n\nQString FormatDurationAndSizeText(qint64 duration, qint64 size) {\n\treturn tr::lng_duration_and_size(tr::now, lt_duration, FormatDurationText(duration), lt_size, FormatSizeText(size));\n}\n\nQString FormatGifAndSizeText(qint64 size) {\n\treturn tr::lng_duration_and_size(tr::now, lt_duration, u\"GIF\"_q, lt_size, FormatSizeText(size));\n}\n\nQString FormatPlayedText(qint64 played, qint64 duration) {\n\treturn tr::lng_duration_played(tr::now, lt_played, FormatDurationText(played), lt_duration, FormatDurationText(duration));\n}\n\nQString FillAmountAndCurrency(\n\t\tint64 amount,\n\t\tconst QString &currency,\n\t\tbool forceStripDotZero) {\n\t// std::abs doesn't work on that one :/\n\tExpects(amount != std::numeric_limits<int64>::min());\n\n\tif (currency == kCreditsCurrency) {\n\t\treturn QChar(0x2B50) + Lang::FormatCountDecimal(std::abs(amount));\n\t}\n\n\tconst auto rule = LookupCurrencyRule(currency);\n\tconst auto prefix = (amount < 0)\n\t\t? QString::fromUtf8(\"\\xe2\\x88\\x92\")\n\t\t: QString();\n\tconst auto value = std::abs(amount) / std::pow(10., rule.exponent);\n\tconst auto name = (*rule.international)\n\t\t? QString::fromUtf8(rule.international)\n\t\t: currency;\n\tauto result = prefix;\n\tif (rule.left) {\n\t\tresult.append(name);\n\t\tif (rule.space) result.append(' ');\n\t}\n\tconst auto precision = ((!rule.stripDotZero && !forceStripDotZero)\n\t\t|| std::floor(value) != value)\n\t\t? rule.exponent\n\t\t: 0;\n\tresult.append(FormatWithSeparators(\n\t\tvalue,\n\t\tprecision,\n\t\trule.decimal,\n\t\trule.thousands));\n\tif (!rule.left) {\n\t\tif (rule.space) result.append(' ');\n\t\tresult.append(name);\n\t}\n\treturn result;\n}\n\nCurrencyRule LookupCurrencyRule(const QString &currency) {\n\tstatic const auto kRules = std::vector<std::pair<QString, CurrencyRule>>{\n\t\t{ u\"AED\"_q, { \"\", ',', '.', true, true } },\n\t\t{ u\"AFN\"_q, {} },\n\t\t{ u\"ALL\"_q, { \"\", '.', ',', false } },\n\t\t{ u\"AMD\"_q, { \"\", ',', '.', false, true } },\n\t\t{ u\"ARS\"_q, { \"\", '.', ',', true, true } },\n\t\t{ u\"AUD\"_q, { \"AU$\" } },\n\t\t{ u\"AZN\"_q, { \"\", ' ', ',', false, true } },\n\t\t{ u\"BAM\"_q, { \"\", '.', ',', false, true } },\n\t\t{ u\"BDT\"_q, { \"\", ',', '.', true, true } },\n\t\t{ u\"BGN\"_q, { \"\", ' ', ',', false, true } },\n\t\t{ u\"BHD\"_q, { \"\", ',', '.', true, true, 3 } },\n\t\t{ u\"BND\"_q, { \"\", '.', ',' } },\n\t\t{ u\"BOB\"_q, { \"\", '.', ',', true, true } },\n\t\t{ u\"BRL\"_q, { \"R$\", '.', ',', true, true } },\n\t\t{ u\"BYN\"_q, { \"\", ' ', ',', false, true } },\n\t\t{ u\"CAD\"_q, { \"CA$\" } },\n\t\t{ u\"CHF\"_q, { \"\", '\\'', '.', false, true } },\n\t\t{ u\"CLP\"_q, { \"\", '.', ',', true, true, 0 } },\n\t\t{ u\"CNY\"_q, { \"\\x43\\x4E\\xC2\\xA5\" } },\n\t\t{ u\"COP\"_q, { \"\", '.', ',', true, true } },\n\t\t{ u\"CRC\"_q, { \"\", '.', ',' } },\n\t\t{ u\"CZK\"_q, { \"\", ' ', ',', false, true } },\n\t\t{ u\"DKK\"_q, { \"\", '\\0', ',', false, true } },\n\t\t{ u\"DOP\"_q, {} },\n\t\t{ u\"DZD\"_q, { \"\", ',', '.', true, true } },\n\t\t{ u\"EGP\"_q, { \"\", ',', '.', true, true } },\n\t\t{ u\"ETB\"_q, {} },\n\t\t{ u\"EUR\"_q, { \"\\xE2\\x82\\xAC\", ' ', ',', false, true } },\n\t\t{ u\"GBP\"_q, { \"\\xC2\\xA3\" } },\n\t\t{ u\"GEL\"_q, { \"\", ' ', ',', false, true } },\n\t\t{ u\"GHS\"_q, {} },\n\t\t{ u\"GTQ\"_q, {} },\n\t\t{ u\"HKD\"_q, { \"HK$\" } },\n\t\t{ u\"HNL\"_q, { \"\", ',', '.', true, true } },\n\t\t{ u\"HRK\"_q, { \"\", '.', ',', false, true } },\n\t\t{ u\"HUF\"_q, { \"\", ' ', ',', false, true } },\n\t\t{ u\"IDR\"_q, { \"\", '.', ',' } },\n\t\t{ u\"ILS\"_q, { \"\\xE2\\x82\\xAA\", ',', '.', true, true } },\n\t\t{ u\"INR\"_q, { \"\\xE2\\x82\\xB9\" } },\n\t\t{ u\"IQD\"_q, { \"\", ',', '.', true, true, 3 } },\n\t\t{ u\"IRR\"_q, { \"\", ',', '/', false, true } },\n\t\t{ u\"ISK\"_q, { \"\", '.', ',', false, true, 0 } },\n\t\t{ u\"JMD\"_q, {} },\n\t\t{ u\"JOD\"_q, { \"\", ',', '.', true, false, 3 } },\n\t\t{ u\"JPY\"_q, { \"\\xC2\\xA5\", ',', '.', true, false, 0 } },\n\t\t{ u\"KES\"_q, {} },\n\t\t{ u\"KGS\"_q, { \"\", ' ', '-', false, true } },\n\t\t{ u\"KRW\"_q, { \"\\xE2\\x82\\xA9\", ',', '.', true, false, 0 } },\n\t\t{ u\"KZT\"_q, { \"\", ' ', '-' } },\n\t\t{ u\"LBP\"_q, { \"\", ',', '.', true, true } },\n\t\t{ u\"LKR\"_q, { \"\", ',', '.', true, true } },\n\t\t{ u\"MAD\"_q, { \"\", ',', '.', true, true } },\n\t\t{ u\"MDL\"_q, { \"\", ',', '.', false, true } },\n\t\t{ u\"MMK\"_q, {} },\n\t\t{ u\"MNT\"_q, { \"\", ' ', ',' } },\n\t\t{ u\"MOP\"_q, {} },\n\t\t{ u\"MUR\"_q, {} },\n\t\t{ u\"MVR\"_q, { \"\", ',', '.', false, true } },\n\t\t{ u\"MXN\"_q, { \"MX$\" } },\n\t\t{ u\"MYR\"_q, {} },\n\t\t{ u\"MZN\"_q, {} },\n\t\t{ u\"NGN\"_q, {} },\n\t\t{ u\"NIO\"_q, { \"\", ',', '.', true, true } },\n\t\t{ u\"NOK\"_q, { \"\", ' ', ',', true, true } },\n\t\t{ u\"NPR\"_q, {} },\n\t\t{ u\"NZD\"_q, { \"NZ$\" } },\n\t\t{ u\"PAB\"_q, { \"\", ',', '.', true, true } },\n\t\t{ u\"PEN\"_q, { \"\", ',', '.', true, true } },\n\t\t{ u\"PHP\"_q, {} },\n\t\t{ u\"PKR\"_q, {} },\n\t\t{ u\"PLN\"_q, { \"\", ' ', ',', false, true } },\n\t\t{ u\"PYG\"_q, { \"\", '.', ',', true, true, 0 } },\n\t\t{ u\"QAR\"_q, { \"\", ',', '.', true, true } },\n\t\t{ u\"RON\"_q, { \"\", '.', ',', false, true } },\n\t\t{ u\"RSD\"_q, { \"\", '.', ',', false, true } },\n\t\t{ u\"RUB\"_q, { \"\", ' ', ',', false, true } },\n\t\t{ u\"SAR\"_q, { \"\", ',', '.', true, true } },\n\t\t{ u\"SEK\"_q, { \"\", '.', ',', false, true } },\n\t\t{ u\"SGD\"_q, {} },\n\t\t{ u\"SYP\"_q, { \"\", ',', '.', true, true } },\n\t\t{ u\"THB\"_q, { \"\\xE0\\xB8\\xBF\" } },\n\t\t{ u\"TJS\"_q, { \"\", ' ', ';', false, true } },\n\t\t{ u\"TRY\"_q, { \"\", '.', ',', false, true } },\n\t\t{ u\"TTD\"_q, {} },\n\t\t{ u\"TWD\"_q, { \"NT$\" } },\n\t\t{ u\"TZS\"_q, {} },\n\t\t{ u\"UAH\"_q, { \"\", ' ', ',', false } },\n\t\t{ u\"UGX\"_q, { \"\", ',', '.', true, false, 0 } },\n\t\t{ u\"USD\"_q, { \"$\" } },\n\t\t{ u\"UYU\"_q, { \"\", '.', ',', true, true } },\n\t\t{ u\"UZS\"_q, { \"\", ' ', ',', false, true } },\n\t\t{ u\"VEF\"_q, { \"\", '.', ',', true, true } },\n\t\t{ u\"VND\"_q, { \"\\xE2\\x82\\xAB\", '.', ',', false, true, 0 } },\n\t\t{ u\"YER\"_q, { \"\", ',', '.', true, true } },\n\t\t{ u\"ZAR\"_q, { \"\", ',', '.', true, true } },\n\n\t\t//{ u\"VUV\"_q, { \"\", ',', '.', false, false, 0 } },\n\t\t//{ u\"WST\"_q, {} },\n\t\t//{ u\"XAF\"_q, { \"FCFA\", ',', '.', false, false, 0 } },\n\t\t//{ u\"XCD\"_q, {} },\n\t\t//{ u\"XOF\"_q, { \"CFA\", ' ', ',', false, false, 0 } },\n\t\t//{ u\"XPF\"_q, { \"\", ',', '.', false, false, 0 } },\n\t\t//{ u\"ZMW\"_q, {} },\n\t\t//{ u\"ANG\"_q, {} },\n\t\t//{ u\"RWF\"_q, { \"\", ' ', ',', true, true, 0 } },\n\t\t//{ u\"PGK\"_q, {} },\n\t\t//{ u\"TOP\"_q, {} },\n\t\t//{ u\"SBD\"_q, {} },\n\t\t//{ u\"SCR\"_q, {} },\n\t\t//{ u\"SHP\"_q, {} },\n\t\t//{ u\"SLL\"_q, {} },\n\t\t//{ u\"SOS\"_q, {} },\n\t\t//{ u\"SRD\"_q, {} },\n\t\t//{ u\"STD\"_q, {} },\n\t\t//{ u\"SVC\"_q, {} },\n\t\t//{ u\"SZL\"_q, {} },\n\t\t//{ u\"AOA\"_q, {} },\n\t\t//{ u\"AWG\"_q, {} },\n\t\t//{ u\"BBD\"_q, {} },\n\t\t//{ u\"BIF\"_q, { \"\", ',', '.', false, false, 0 } },\n\t\t//{ u\"BMD\"_q, {} },\n\t\t//{ u\"BSD\"_q, {} },\n\t\t//{ u\"BWP\"_q, {} },\n\t\t//{ u\"BZD\"_q, {} },\n\t\t//{ u\"CDF\"_q, { \"\", ',', '.', false } },\n\t\t//{ u\"CVE\"_q, { \"\", ',', '.', true, false, 0 } },\n\t\t//{ u\"DJF\"_q, { \"\", ',', '.', false, false, 0 } },\n\t\t//{ u\"FJD\"_q, {} },\n\t\t//{ u\"FKP\"_q, {} },\n\t\t//{ u\"GIP\"_q, {} },\n\t\t//{ u\"GMD\"_q, { \"\", ',', '.', false } },\n\t\t//{ u\"GNF\"_q, { \"\", ',', '.', false, false, 0 } },\n\t\t//{ u\"GYD\"_q, {} },\n\t\t//{ u\"HTG\"_q, {} },\n\t\t//{ u\"KHR\"_q, { \"\", ',', '.', false } },\n\t\t//{ u\"KMF\"_q, { \"\", ',', '.', false, false, 0 } },\n\t\t//{ u\"KYD\"_q, {} },\n\t\t//{ u\"LAK\"_q, { \"\", ',', '.', false } },\n\t\t//{ u\"LRD\"_q, {} },\n\t\t//{ u\"LSL\"_q, { \"\", ',', '.', false } },\n\t\t//{ u\"MGA\"_q, { \"\", ',', '.', true, false, 0 } },\n\t\t//{ u\"MKD\"_q, { \"\", '.', ',', false, true } },\n\t\t//{ u\"MWK\"_q, {} },\n\t\t//{ u\"NAD\"_q, {} },\n\t\t//{ u\"CLF\"_q, { \"\", ',', '.', true, false, 4 } },\n\t\t//{ u\"KWD\"_q, { \"\", ',', '.', true, false, 3 } },\n\t\t//{ u\"LYD\"_q, { \"\", ',', '.', true, false, 3 } },\n\t\t//{ u\"OMR\"_q, { \"\", ',', '.', true, false, 3 } },\n\t\t//{ u\"TND\"_q, { \"\", ',', '.', true, false, 3 } },\n\t\t//{ u\"UYI\"_q, { \"\", ',', '.', true, false, 0 } },\n\t\t//{ u\"MRO\"_q, { \"\", ',', '.', true, false, 1 } },\n\t};\n\tstatic const auto kRulesMap = [] {\n\t\t// flat_multi_map_pair_type lacks some required constructors :(\n\t\tauto &&list = kRules | ranges::views::transform([](auto &&pair) {\n\t\t\treturn base::flat_multi_map_pair_type<QString, CurrencyRule>(\n\t\t\t\tpair.first,\n\t\t\t\tpair.second);\n\t\t});\n\t\treturn base::flat_map<QString, CurrencyRule>(begin(list), end(list));\n\t}();\n\tconst auto i = kRulesMap.find(currency);\n\treturn (i != end(kRulesMap)) ? i->second : CurrencyRule{};\n}\n\n[[nodiscard]] QString FormatWithSeparators(\n\t\tdouble amount,\n\t\tint precision,\n\t\tchar decimal,\n\t\tchar thousands) {\n\tExpects(decimal != 0);\n\n\t// Thanks https://stackoverflow.com/a/5058949\n\tstruct FormattingHelper : std::numpunct<char> {\n\t\tFormattingHelper(char decimal, char thousands)\n\t\t: decimal(decimal)\n\t\t, thousands(thousands) {\n\t\t}\n\n\t\tchar do_decimal_point() const override { return decimal; }\n\t\tchar do_thousands_sep() const override { return thousands; }\n\t\tstd::string do_grouping() const override { return \"\\3\"; }\n\n\t\tchar decimal = '.';\n\t\tchar thousands = ',';\n\t};\n\n\tauto stream = std::ostringstream();\n\tstream.imbue(std::locale(\n\t\tstream.getloc(),\n\t\tnew FormattingHelper(decimal, thousands ? thousands : '?')));\n\tstream.precision(precision);\n\tstream << std::fixed << amount;\n\tauto result = QString::fromStdString(stream.str());\n\tif (!thousands) {\n\t\tresult.replace('?', QString());\n\t}\n\treturn result;\n}\n\nQString FormatImageSizeText(const QSize &size) {\n\treturn QString::number(size.width())\n\t\t+ QChar(215)\n\t\t+ QString::number(size.height());\n}\n\nQString FormatPhone(QString phone) {\n\tif (phone.isEmpty()) {\n\t\treturn QString();\n\t}\n\tif (phone.at(0) == '0') {\n\t\treturn phone;\n\t}\n\tphone = phone.remove(QChar::Space);\n\treturn Countries::Instance().format({\n\t\t.phone = (phone.at(0) == '+') ? phone.mid(1) : phone,\n\t}).formatted;\n}\n\nQString FormatTTL(float64 ttl) {\n\tif (ttl < 86400) {\n\t\treturn tr::lng_hours(tr::now, lt_count, int(ttl / 3600));\n\t} else if (ttl < 86400 * 7) {\n\t\treturn tr::lng_days(tr::now, lt_count, int(ttl / (86400)));\n\t} else if (ttl < 86400 * 31) {\n\t\tconst auto days = int(ttl / 86400);\n\t\tif ((int(ttl) % 7) == 0) {\n\t\t\treturn tr::lng_weeks(tr::now, lt_count, int(days / 7));\n\t\t} else {\n\t\t\treturn tr::lng_weeks(tr::now, lt_count, int(days / 7))\n\t\t\t\t+ ' '\n\t\t\t\t+ tr::lng_days(tr::now, lt_count, int(days % 7));\n\t\t}\n\t} else if (ttl <= (86400 * 31) * 11) {\n\t\treturn tr::lng_months(tr::now, lt_count, int(ttl / (86400 * 31)));\n\t} else {\n\t\treturn tr::lng_years({}, lt_count, std::round(ttl / (86400 * 365)));\n\t}\n}\n\nQString FormatTTLAfter(float64 ttl) {\n\treturn (ttl <= 3600 * 23)\n\t\t? tr::lng_settings_ttl_after_hours(tr::now, lt_count, int(ttl / 3600))\n\t\t: (ttl <= (86400) * 6)\n\t\t? tr::lng_settings_ttl_after_days(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tint(ttl / (86400)))\n\t\t: (ttl <= (86400 * 7) * 3)\n\t\t? tr::lng_settings_ttl_after_weeks(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tint(ttl / (86400 * 7)))\n\t\t: (ttl <= (86400 * 31) * 11)\n\t\t? tr::lng_settings_ttl_after_months(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tint(ttl / (86400 * 31)))\n\t\t: tr::lng_settings_ttl_after_years(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tstd::round(ttl / (86400 * 365)));\n}\n\nQString FormatTTLTiny(float64 ttl) {\n\treturn (ttl <= 3600 * 9)\n\t\t? tr::lng_hours_tiny(tr::now, lt_count, int(ttl / 3600))\n\t\t: (ttl <= (86400) * 6)\n\t\t? tr::lng_days_tiny(tr::now, lt_count, int(ttl / (86400)))\n\t\t: (ttl <= (86400 * 7) * 3)\n\t\t? tr::lng_weeks_tiny(tr::now, lt_count, int(ttl / (86400 * 7)))\n\t\t: (ttl <= (86400 * 31) * 11)\n\t\t? tr::lng_months_tiny({}, lt_count, int(ttl / (86400 * 31)))\n\t\t: (ttl <= 86400 * 366)\n\t\t? tr::lng_years_tiny({}, lt_count, std::round(ttl / (86400 * 365)))\n\t\t: QString();\n}\n\nQString FormatMuteFor(float64 sec) {\n\treturn (sec <= 60)\n\t\t? tr::lng_seconds(tr::now, lt_count, sec)\n\t\t: (sec <= 60 * 59)\n\t\t? tr::lng_minutes(tr::now, lt_count, int(sec / 60))\n\t\t: FormatTTL(sec);\n}\n\nQString FormatMuteForTiny(float64 sec) {\n\treturn (sec <= 60)\n\t\t? QString()\n\t\t: (sec <= 60 * 59)\n\t\t? tr::lng_minutes_tiny(tr::now, lt_count, std::round(sec / 60))\n\t\t: (sec <= 3600 * 23)\n\t\t? tr::lng_hours_tiny(tr::now, lt_count, std::round(sec / 3600))\n\t\t: (sec <= 86400 * 6)\n\t\t? tr::lng_days_tiny(tr::now, lt_count, std::round(sec / 86400))\n\t\t: (sec <= (86400 * 7) * 3)\n\t\t? tr::lng_weeks_tiny(tr::now, lt_count, std::round(sec / (86400 * 7)))\n\t\t: (sec <= (86400 * 31) * 11)\n\t\t? tr::lng_months_tiny({}, lt_count, std::round(sec / (86400 * 31)))\n\t\t: (sec <= 86400 * 366)\n\t\t? tr::lng_years_tiny({}, lt_count, std::round(sec / (86400 * 365)))\n\t\t: QString();\n}\n\nQString FormatResetCloudPasswordIn(float64 sec) {\n\treturn (sec >= 3600) ? FormatTTL(sec) : FormatDurationText(sec);\n}\n\nQString FormatDialogsDate(const QDateTime &lastTime) {\n\t// Show all dates that are in the last 20 hours in time format.\n\tconstexpr int kRecentlyInSeconds = 20 * 3600;\n\n\tconst auto now = QDateTime::currentDateTime();\n\tconst auto nowDate = now.date();\n\tconst auto lastDate = lastTime.date();\n\n\tif ((lastDate == nowDate)\n\t\t|| (std::abs(lastTime.secsTo(now)) < kRecentlyInSeconds)) {\n\t\treturn QLocale().toString(lastTime.time(), QLocale::ShortFormat);\n\t} else if (std::abs(lastDate.daysTo(nowDate)) < 7) {\n\t\treturn langDayOfWeek(lastDate);\n\t} else {\n\t\treturn QLocale().toString(lastDate, QLocale::ShortFormat);\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/text/format_values.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\ninline constexpr auto FileStatusSizeReady = 0xFFFFFFF0LL;\ninline constexpr auto FileStatusSizeLoaded = 0xFFFFFFF1LL;\ninline constexpr auto FileStatusSizeFailed = 0xFFFFFFF2LL;\n\ninline const QString kCreditsCurrency = u\"XTR\"_q;\n\n[[nodiscard]] QString FormatSizeText(qint64 size);\n[[nodiscard]] QString FormatDownloadText(qint64 ready, qint64 total);\n[[nodiscard]] QString FormatProgressText(qint64 ready, qint64 total);\n[[nodiscard]] QString FormatDateTime(QDateTime date);\n[[nodiscard]] QString FormatDateTimeSavedFrom(QDateTime date);\n[[nodiscard]] QString FormatDurationText(qint64 duration);\n[[nodiscard]] QString FormatDurationWords(qint64 duration);\n[[nodiscard]] QString FormatDurationWordsSlowmode(qint64 duration);\n[[nodiscard]] QString FormatDurationAndSizeText(qint64 duration, qint64 size);\n[[nodiscard]] QString FormatGifAndSizeText(qint64 size);\n[[nodiscard]] QString FormatPlayedText(qint64 played, qint64 duration);\n[[nodiscard]] QString FormatImageSizeText(const QSize &size);\n[[nodiscard]] QString FormatPhone(QString phone);\n[[nodiscard]] QString FormatTTL(float64 ttl);\n[[nodiscard]] QString FormatTTLAfter(float64 ttl);\n[[nodiscard]] QString FormatTTLTiny(float64 ttl);\n[[nodiscard]] QString FormatMuteFor(float64 sec);\n[[nodiscard]] QString FormatMuteForTiny(float64 sec);\n[[nodiscard]] QString FormatResetCloudPasswordIn(float64 sec);\n[[nodiscard]] QString FormatDialogsDate(const QDateTime &lastTime);\n\nstruct CurrencyRule {\n\tconst char *international = \"\";\n\tchar thousands = ',';\n\tchar decimal = '.';\n\tbool left = true;\n\tbool space = false;\n\tint exponent = 2;\n\tbool stripDotZero = false;\n};\n\n[[nodiscard]] QString FillAmountAndCurrency(\n\tint64 amount,\n\tconst QString &currency,\n\tbool forceStripDotZero = false);\n[[nodiscard]] CurrencyRule LookupCurrencyRule(const QString &currency);\n[[nodiscard]] QString FormatWithSeparators(\n\tdouble amount,\n\tint precision,\n\tchar decimal,\n\tchar thousands);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/text/text_lottie_custom_emoji.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/text/text_lottie_custom_emoji.h\"\n\n#include \"lottie/lottie_icon.h\"\n#include \"ui/text/text_utilities.h\"\n\nnamespace Ui::Text {\n\nLottieCustomEmoji::LottieCustomEmoji(Lottie::IconDescriptor &&descriptor)\n: LottieCustomEmoji(std::move(descriptor), nullptr) {\n}\n\nLottieCustomEmoji::LottieCustomEmoji(\n\tLottie::IconDescriptor &&descriptor,\n\tFn<void()> repaint)\n: _entityData(!descriptor.name.isEmpty()\n\t? descriptor.name\n\t: !descriptor.path.isEmpty()\n\t? descriptor.path\n\t: (Unexpected(\"LottieCustomEmoji: descriptor.name or descriptor.path\"),\n\t\tQString()))\n, _width(descriptor.sizeOverride.width())\n, _icon(Lottie::MakeIcon(std::move(descriptor)))\n, _repaint(std::move(repaint)) {\n\tif (!_width && _icon && _icon->valid()) {\n\t\t_width = _icon->width();\n\t\tstartAnimation();\n\t}\n}\n\nint LottieCustomEmoji::width() {\n\treturn _width;\n}\n\nQString LottieCustomEmoji::entityData() {\n\treturn _entityData;\n}\n\nvoid LottieCustomEmoji::paint(QPainter &p, const Context &context) {\n\tif (!_icon || !_icon->valid()) {\n\t\treturn;\n\t}\n\n\tconst auto paused = context.paused\n\t\t|| context.internal.forceFirstFrame\n\t\t|| context.internal.overrideFirstWithLastFrame;\n\n\tif (paused) {\n\t\tconst auto frame = context.internal.forceLastFrame\n\t\t\t? _icon->framesCount() - 1\n\t\t\t: 0;\n\t\t_icon->jumpTo(frame, _repaint);\n\t} else if (!_icon->animating()) {\n\t\tstartAnimation();\n\t}\n\n\t_icon->paint(\n\t\tp,\n\t\tcontext.position.x(),\n\t\tcontext.position.y(),\n\t\tcontext.textColor);\n}\n\nvoid LottieCustomEmoji::unload() {\n\tif (_icon) {\n\t\t_icon->jumpTo(0, nullptr);\n\t}\n}\n\nbool LottieCustomEmoji::ready() {\n\treturn _icon && _icon->valid();\n}\n\nbool LottieCustomEmoji::readyInDefaultState() {\n\treturn _icon && _icon->valid() && _icon->frameIndex() == 0;\n}\n\nvoid LottieCustomEmoji::startAnimation() {\n\tif (!_icon || !_icon->valid() || _icon->framesCount() <= 1) {\n\t\treturn;\n\t}\n\n\t_icon->animate(\n\t\t_repaint,\n\t\t0,\n\t\t_icon->framesCount() - 1);\n}\n\nQString LottieEmojiData(Lottie::IconDescriptor descriptor) {\n\treturn !descriptor.name.isEmpty()\n\t\t? descriptor.name\n\t\t: !descriptor.path.isEmpty()\n\t\t? descriptor.path\n\t\t: QString();\n}\n\nTextWithEntities LottieEmoji(Lottie::IconDescriptor descriptor) {\n\treturn SingleCustomEmoji(LottieEmojiData(std::move(descriptor)));\n}\n\nMarkedContext LottieEmojiContext(Lottie::IconDescriptor descriptor) {\n\tauto customEmojiFactory = [descriptor = std::move(descriptor)](\n\t\t\tQStringView data,\n\t\t\tconst MarkedContext &context\n\t\t) mutable -> std::unique_ptr<CustomEmoji> {\n\t\tif (data == LottieEmojiData(descriptor)) {\n\t\t\treturn std::make_unique<LottieCustomEmoji>(\n\t\t\t\tstd::move(descriptor),\n\t\t\t\tcontext.repaint);\n\t\t}\n\t\treturn nullptr;\n\t};\n\treturn { .customEmojiFactory = std::move(customEmojiFactory) };\n}\n\n} // namespace Ui::Text"
  },
  {
    "path": "Telegram/SourceFiles/ui/text/text_lottie_custom_emoji.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/text/text_custom_emoji.h\"\n\nnamespace Lottie {\nstruct IconDescriptor;\nclass Icon;\n} // namespace Lottie\n\nnamespace Ui::Text {\n\nclass LottieCustomEmoji final : public CustomEmoji {\npublic:\n\texplicit LottieCustomEmoji(Lottie::IconDescriptor &&descriptor);\n\tLottieCustomEmoji(\n\t\tLottie::IconDescriptor &&descriptor,\n\t\tFn<void()> repaint);\n\n\tint width() override;\n\tQString entityData() override;\n\tvoid paint(QPainter &p, const Context &context) override;\n\tvoid unload() override;\n\tbool ready() override;\n\tbool readyInDefaultState() override;\n\nprivate:\n\tvoid startAnimation();\n\n\tQString _entityData;\n\tint _width = 0;\n\tstd::unique_ptr<Lottie::Icon> _icon;\n\tFn<void()> _repaint;\n\n};\n\n[[nodiscard]] QString LottieEmojiData(Lottie::IconDescriptor);\n[[nodiscard]] TextWithEntities LottieEmoji(Lottie::IconDescriptor);\n[[nodiscard]] MarkedContext LottieEmojiContext(Lottie::IconDescriptor);\n\n} // namespace Ui::Text"
  },
  {
    "path": "Telegram/SourceFiles/ui/text/text_options.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/text/text_options.h\"\n\n#include \"styles/style_window.h\"\n#include \"styles/style_chat.h\"\n\nnamespace Ui {\nnamespace {\n\nTextParseOptions HistoryTextOptions = {\n\tTextParseLinks\n\t\t| TextParseMentions\n\t\t| TextParseHashtags\n\t\t| TextParseMultiline\n\t\t| TextParseMarkdown, // flags\n\t0, // maxw\n\t0, // maxh\n\tQt::LayoutDirectionAuto, // dir\n};\n\nTextParseOptions HistoryBotOptions = {\n\tTextParseLinks\n\t\t| TextParseMentions\n\t\t| TextParseHashtags\n\t\t| TextParseBotCommands\n\t\t| TextParseMultiline\n\t\t| TextParseMarkdown, // flags\n\t0, // maxw\n\t0, // maxh\n\tQt::LayoutDirectionAuto, // dir\n};\n\nTextParseOptions HistoryServiceOptions = {\n\tTextParseLinks\n\t\t| TextParseMentions\n\t\t//| TextParseMultiline\n\t\t| TextParseHashtags\n\t\t| TextParseMarkdown, // flags\n\t0, // maxw\n\t0, // maxh\n\tQt::LayoutDirectionAuto, // lang-dependent\n};\n\nTextParseOptions HistoryTextNoMonoOptions = {\n\tTextParseLinks\n\t\t| TextParseMentions\n\t\t| TextParseHashtags\n\t\t| TextParseMultiline, // flags\n\t0, // maxw\n\t0, // maxh\n\tQt::LayoutDirectionAuto, // dir\n};\n\nTextParseOptions HistoryBotNoMonoOptions = {\n\tTextParseLinks\n\t\t| TextParseMentions\n\t\t| TextParseHashtags\n\t\t| TextParseBotCommands\n\t\t| TextParseMultiline, // flags\n\t0, // maxw\n\t0, // maxh\n\tQt::LayoutDirectionAuto, // dir\n};\n\nTextParseOptions TextNameOptions = {\n\t0, // flags\n\t4096, // maxw\n\t1, // maxh\n\tQt::LayoutDirectionAuto, // lang-dependent\n};\n\nTextParseOptions TextDialogOptions = {\n\tTextParseColorized | TextParseMarkdown, // flags\n\t0, // maxw is style-dependent\n\t1, // maxh\n\tQt::LayoutDirectionAuto, // lang-dependent\n};\n\nTextParseOptions WebpageTitleOptions = {\n\tTextParseMultiline, // flags\n\t0, // maxw\n\t0, // maxh\n\tQt::LayoutDirectionAuto, // dir\n};\n\nTextParseOptions WebpageDescriptionOptions = {\n\tTextParseLinks\n\t\t| TextParseMentions\n\t\t| TextParseHashtags\n\t\t| TextParseMultiline\n\t\t| TextParseMarkdown, // flags\n\t0, // maxw\n\t0, // maxh\n\tQt::LayoutDirectionAuto, // dir\n};\n\n} // namespace\n\nvoid InitTextOptions() {\n\tTextDialogOptions.maxw = st::columnMaximalWidthLeft * 2;\n\tWebpageTitleOptions.maxh = st::webPageTitleFont->height * 2;\n\tWebpageTitleOptions.maxw\n\t\t= WebpageDescriptionOptions.maxw\n\t\t= st::msgMaxWidth\n\t\t- st::msgPadding.left()\n\t\t- st::messageQuoteStyle.padding.left()\n\t\t- st::messageQuoteStyle.padding.right()\n\t\t- st::msgPadding.right();\n}\n\nconst TextParseOptions &ItemTextDefaultOptions() {\n\treturn HistoryTextOptions;\n}\n\nconst TextParseOptions &ItemTextBotDefaultOptions() {\n\treturn HistoryBotOptions;\n}\n\nconst TextParseOptions &ItemTextNoMonoOptions() {\n\treturn HistoryTextNoMonoOptions;\n}\n\nconst TextParseOptions &ItemTextBotNoMonoOptions() {\n\treturn HistoryBotNoMonoOptions;\n}\n\nconst TextParseOptions &ItemTextServiceOptions() {\n\treturn HistoryServiceOptions;\n}\n\nconst TextParseOptions &WebpageTextTitleOptions() {\n\treturn WebpageTitleOptions;\n}\n\nconst TextParseOptions &WebpageTextDescriptionOptions() {\n\treturn WebpageDescriptionOptions;\n}\n\nconst TextParseOptions &NameTextOptions() {\n\treturn TextNameOptions;\n}\n\nconst TextParseOptions &DialogTextOptions() {\n\treturn TextDialogOptions;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/text/text_options.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nstruct TextParseOptions;\n\nnamespace Ui {\n\nvoid InitTextOptions();\n\nconst TextParseOptions &ItemTextDefaultOptions();\nconst TextParseOptions &ItemTextBotDefaultOptions();\nconst TextParseOptions &ItemTextNoMonoOptions();\nconst TextParseOptions &ItemTextBotNoMonoOptions();\nconst TextParseOptions &ItemTextServiceOptions();\n\nconst TextParseOptions &WebpageTextTitleOptions();\nconst TextParseOptions &WebpageTextDescriptionOptions();\n\nconst TextParseOptions &NameTextOptions();\nconst TextParseOptions &DialogTextOptions();\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/top_background_gradient.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/top_background_gradient.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_peer_colors.h\"\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_credits.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_star_gift.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"main/main_session.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_chat_helpers.h\"\n\nnamespace Ui {\nnamespace {\n\nvoid PrepareImage(\n\t\tQImage &image,\n\t\tnot_null<Ui::Text::CustomEmoji*> emoji,\n\t\tconst PatternPoint &point,\n\t\tconst QColor &patternColor) {\n\tif (!image.isNull() || !emoji->ready()) {\n\t\treturn;\n\t}\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto size = Emoji::GetSizeNormal() / ratio;\n\timage = QImage(\n\t\t2 * QSize(size, size) * ratio,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(ratio);\n\timage.fill(Qt::transparent);\n\tauto p = QPainter(&image);\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.setOpacity(point.opacity);\n\tif (point.scale < 1.) {\n\t\tp.translate(size, size);\n\t\tp.scale(point.scale, point.scale);\n\t\tp.translate(-size, -size);\n\t}\n\tconst auto shift = (2 * size - (Emoji::GetSizeLarge() / ratio)) / 2;\n\temoji->paint(p, {\n\t\t.textColor = patternColor,\n\t\t.position = QPoint(shift, shift),\n\t\t.paused = true,\n\t});\n}\n\n} // namespace\n\nQImage CreateTopBgGradient(\n\t\tQSize size,\n\t\tQColor centerColor,\n\t\tQColor edgeColor,\n\t\tbool rounded,\n\t\tQPoint offset) {\n\tconst auto ratio = style::DevicePixelRatio();\n\tauto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied);\n\tif (!rounded) {\n\t\tresult.fill(Qt::transparent);\n\t}\n\tresult.setDevicePixelRatio(ratio);\n\n\tauto p = QPainter(&result);\n\tauto hq = PainterHighQualityEnabler(p);\n\tauto gradient = QRadialGradient(\n\t\trect::center(QRect(offset, size)),\n\t\tsize.height() / 2);\n\tgradient.setStops({\n\t\t{ 0., centerColor },\n\t\t{ 1., edgeColor },\n\t});\n\tp.setBrush(gradient);\n\tp.setPen(Qt::NoPen);\n\tp.drawRect(Rect(size));\n\tp.end();\n\n\tif (rounded) {\n\t\tconst auto mask = Images::CornersMask(st::boxRadius);\n\t\treturn Images::Round(std::move(result), mask, RectPart::FullTop);\n\t}\n\treturn result;\n}\n\nQImage CreateTopBgGradient(QSize size, const Data::UniqueGift &gift) {\n\treturn CreateTopBgGradient(size, gift.backdrop);\n}\n\nQImage CreateTopBgGradient(\n\t\tQSize size,\n\t\tconst Data::UniqueGiftBackdrop &backdrop) {\n\treturn CreateTopBgGradient(\n\t\tsize,\n\t\tbackdrop.centerColor,\n\t\tbackdrop.edgeColor);\n}\n\nQImage CreateTopBgGradient(\n\t\tQSize size,\n\t\tnot_null<PeerData*> peer,\n\t\tQPoint offset) {\n\tif (const auto collectible = peer->emojiStatusId().collectible) {\n\t\treturn CreateTopBgGradient(\n\t\t\tsize,\n\t\t\tcollectible->centerColor,\n\t\t\tcollectible->edgeColor,\n\t\t\tfalse,\n\t\t\toffset);\n\t}\n\tif (const auto color = peer->session().api().peerColors().colorProfileFor(\n\t\t\tpeer)) {\n\t\tif (color->bg.size() > 1) {\n\t\t\treturn CreateTopBgGradient(\n\t\t\t\tsize,\n\t\t\t\tcolor->bg[1],\n\t\t\t\tcolor->bg[0],\n\t\t\t\tfalse,\n\t\t\t\toffset);\n\t\t}\n\t}\n\treturn QImage();\n}\n\nconst std::vector<PatternPoint> &PatternBgPoints() {\n\tstatic const auto kSmall = 0.7;\n\tstatic const auto kFaded = 0.2;\n\tstatic const auto kLarge = 0.85;\n\tstatic const auto kOpaque = 0.3;\n\tstatic const auto result = std::vector<PatternPoint>{\n\t\t{ { 0.5, 0.066 }, kSmall, kFaded },\n\n\t\t{ { 0.177, 0.168 }, kSmall, kFaded },\n\t\t{ { 0.822, 0.168 }, kSmall, kFaded },\n\n\t\t{ { 0.37, 0.168 }, kLarge, kOpaque },\n\t\t{ { 0.63, 0.168 }, kLarge, kOpaque },\n\n\t\t{ { 0.277, 0.308 }, kSmall, kOpaque },\n\t\t{ { 0.723, 0.308 }, kSmall, kOpaque },\n\n\t\t{ { 0.13, 0.42 }, kSmall, kFaded },\n\t\t{ { 0.87, 0.42 }, kSmall, kFaded },\n\n\t\t{ { 0.27, 0.533 }, kLarge, kOpaque },\n\t\t{ { 0.73, 0.533 }, kLarge, kOpaque },\n\n\t\t{ { 0.2, 0.73 }, kSmall, kFaded },\n\t\t{ { 0.8, 0.73 }, kSmall, kFaded },\n\n\t\t{ { 0.302, 0.825 }, kLarge, kOpaque },\n\t\t{ { 0.698, 0.825 }, kLarge, kOpaque },\n\n\t\t{ { 0.5, 0.876 }, kLarge, kFaded },\n\n\t\t{ { 0.144, 0.936 }, kSmall, kFaded },\n\t\t{ { 0.856, 0.936 }, kSmall, kFaded },\n\t};\n\treturn result;\n}\n\nconst std::vector<PatternPoint> &PatternBgPointsSmall() {\n\tstatic const auto kSmall = 0.45;\n\tstatic const auto kFaded = 0.2;\n\tstatic const auto kLarge = 0.55;\n\tstatic const auto kOpaque = 0.3;\n\tstatic const auto result = std::vector<PatternPoint>{\n\t\t{ { 0.5, 0.066 }, kSmall, kFaded },\n\n\t\t{ { 0.177, 0.168 }, kSmall, kFaded },\n\t\t{ { 0.822, 0.168 }, kSmall, kFaded },\n\n\t\t{ { 0.37, 0.168 }, kLarge, kOpaque },\n\t\t{ { 0.63, 0.168 }, kLarge, kOpaque },\n\n\t\t{ { 0.277, 0.308 }, kSmall, kOpaque },\n\t\t{ { 0.723, 0.308 }, kSmall, kOpaque },\n\n\t\t{ { 0.13, 0.42 }, kSmall, kFaded },\n\t\t{ { 0.87, 0.42 }, kSmall, kFaded },\n\n\t\t{ { 0.27, 0.533 }, kLarge, kOpaque },\n\t\t{ { 0.73, 0.533 }, kLarge, kOpaque },\n\n\t\t{ { 0.2, 0.73 }, kSmall, kFaded },\n\t\t{ { 0.8, 0.73 }, kSmall, kFaded },\n\n\t\t{ { 0.302, 0.825 }, kLarge, kOpaque },\n\t\t{ { 0.698, 0.825 }, kLarge, kOpaque },\n\n\t\t{ { 0.5, 0.876 }, kLarge, kFaded },\n\n\t\t{ { 0.144, 0.936 }, kSmall, kFaded },\n\t\t{ { 0.856, 0.936 }, kSmall, kFaded },\n\t};\n\treturn result;\n}\n\nvoid PaintBgPoints(\n\t\tQPainter &p,\n\t\tconst std::vector<PatternPoint> &points,\n\t\tbase::flat_map<float64, QImage> &cache,\n\t\tnot_null<Ui::Text::CustomEmoji*> emoji,\n\t\tconst Data::UniqueGift &gift,\n\t\tconst QRect &rect,\n\t\tfloat64 shown) {\n\tPaintBgPoints(\n\t\tp,\n\t\tpoints,\n\t\tcache,\n\t\temoji,\n\t\tgift.backdrop,\n\t\trect,\n\t\tshown);\n}\n\nvoid PaintBgPoints(\n\t\tQPainter &p,\n\t\tconst std::vector<PatternPoint> &points,\n\t\tbase::flat_map<float64, QImage> &cache,\n\t\tnot_null<Ui::Text::CustomEmoji*> emoji,\n\t\tconst Data::UniqueGiftBackdrop &backdrop,\n\t\tconst QRect &rect,\n\t\tfloat64 shown) {\n\tPaintBgPoints(\n\t\tp,\n\t\tpoints,\n\t\tcache,\n\t\temoji,\n\t\tbackdrop.patternColor,\n\t\trect,\n\t\tshown);\n}\n\nvoid PaintBgPoints(\n\t\tQPainter &p,\n\t\tconst std::vector<PatternPoint> &points,\n\t\tbase::flat_map<float64, QImage> &cache,\n\t\tnot_null<Ui::Text::CustomEmoji*> emoji,\n\t\tconst QColor &patternColor,\n\t\tconst QRect &rect,\n\t\tfloat64 shown) {\n\tconst auto origin = rect.topLeft();\n\tconst auto width = rect.width();\n\tconst auto height = rect.height();\n\tconst auto ratio = style::DevicePixelRatio();\n\tconst auto paintPoint = [&](const PatternPoint &point) {\n\t\tconst auto key = (1. + point.opacity) * 10. + point.scale;\n\t\tauto &image = cache[key];\n\t\tPrepareImage(image, emoji, point, patternColor);\n\t\tif (!image.isNull()) {\n\t\t\tconst auto position = origin + QPoint(\n\t\t\t\tint(point.position.x() * width),\n\t\t\t\tint(point.position.y() * height));\n\t\t\tif (shown < 1.) {\n\t\t\t\tp.save();\n\t\t\t\tp.translate(position);\n\t\t\t\tp.scale(shown, shown);\n\t\t\t\tp.translate(-position);\n\t\t\t}\n\t\t\tconst auto size = image.size() / ratio;\n\t\t\tp.drawImage(\n\t\t\t\tposition - QPoint(size.width() / 2, size.height() / 2),\n\t\t\t\timage);\n\t\t\tif (shown < 1.) {\n\t\t\t\tp.restore();\n\t\t\t}\n\t\t}\n\t};\n\tfor (const auto &point : points) {\n\t\tpaintPoint(point);\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/top_background_gradient.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Data {\nstruct UniqueGift;\nstruct UniqueGiftBackdrop;\n} // namespace Data\n\nnamespace Ui::Text {\nclass CustomEmoji;\n} // namespace Ui::Text\n\nclass PeerData;\n\nnamespace Ui {\n\nstruct PatternPoint {\n\tQPointF position;\n\tfloat64 scale = 1.;\n\tfloat64 opacity = 1.;\n};\n\n[[nodiscard]] QImage CreateTopBgGradient(\n\tQSize size,\n\tconst Data::UniqueGift &gift);\n\n[[nodiscard]] QImage CreateTopBgGradient(\n\tQSize size,\n\tconst Data::UniqueGiftBackdrop &backdrop);\n\n[[nodiscard]] QImage CreateTopBgGradient(\n\tQSize size,\n\tQColor centerColor,\n\tQColor edgeColor,\n\tbool rounded = true,\n\tQPoint offset = QPoint());\n\n[[nodiscard]] QImage CreateTopBgGradient(\n\tQSize size,\n\tnot_null<PeerData*> peer,\n\tQPoint offset = QPoint());\n\n[[nodiscard]] const std::vector<PatternPoint> &PatternBgPoints();\n[[nodiscard]] const std::vector<PatternPoint> &PatternBgPointsSmall();\n\nvoid PaintBgPoints(\n\tQPainter &p,\n\tconst std::vector<PatternPoint> &points,\n\tbase::flat_map<float64, QImage> &cache,\n\tnot_null<Ui::Text::CustomEmoji*> emoji,\n\tconst Data::UniqueGift &gift,\n\tconst QRect &rect,\n\tfloat64 shown = 1.);\n\nvoid PaintBgPoints(\n\tQPainter &p,\n\tconst std::vector<PatternPoint> &points,\n\tbase::flat_map<float64, QImage> &cache,\n\tnot_null<Ui::Text::CustomEmoji*> emoji,\n\tconst Data::UniqueGiftBackdrop &backdrop,\n\tconst QRect &rect,\n\tfloat64 shown = 1.);\n\nvoid PaintBgPoints(\n\tQPainter &p,\n\tconst std::vector<PatternPoint> &points,\n\tbase::flat_map<float64, QImage> &cache,\n\tnot_null<Ui::Text::CustomEmoji*> emoji,\n\tconst QColor &patternColor,\n\tconst QRect &rect,\n\tfloat64 shown = 1.);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/ui_pch.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n\n#include <QtCore/QString>\n#include <QtCore/QPoint>\n#include <QtCore/QRect>\n#include <QtCore/QSize>\n\n#include <QtGui/QColor>\n#include <QtGui/QPen>\n#include <QtGui/QBrush>\n#include <QtGui/QCursor>\n#include <QtGui/QFont>\n#include <QtGui/QFontMetrics>\n#include <QtGui/QPainter>\n#include <QtGui/QImage>\n#include <QtGui/QPixmap>\n#include <QtGui/QtEvents>\n\n#include <QtWidgets/QWidget>\n\n#include <rpl/rpl.h>\n#include <range/v3/all.hpp>\n#include <crl/crl_time.h>\n#include <crl/crl_on_main.h>\n\n#include \"base/algorithm.h\"\n#include \"base/basic_types.h\"\n#include \"base/flat_map.h\"\n#include \"base/flat_set.h\"\n#include \"base/weak_qptr.h\"\n\n#include \"core/credits_amount.h\"\n\n#include \"ui/arc_angles.h\"\n#include \"ui/color_int_conversion.h\"\n#include \"ui/text/text.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/qt_object_factory.h\"\n#include \"styles/palette.h\"\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/unread_badge.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/unread_badge.h\"\n\n#include \"data/data_emoji_statuses.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"main/main_session.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/unread_badge_paint.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kPlayStatusLimit = 2;\n\n} // namespace\n\nstruct PeerBadge::EmojiStatus {\n\tEmojiStatusId id;\n\tstd::unique_ptr<Ui::Text::CustomEmoji> emoji;\n\tint skip = 0;\n};\n\nstruct PeerBadge::BotVerifiedData {\n\tQImage cache;\n\tstd::unique_ptr<Text::CustomEmoji> icon;\n};\n\nvoid UnreadBadge::setText(const QString &text, bool active) {\n\t_text = text;\n\t_active = active;\n\tconst auto st = Dialogs::Ui::UnreadBadgeStyle();\n\tresize(\n\t\tstd::max(st.font->width(text) + 2 * st.padding, st.size),\n\t\tst.size);\n\tupdate();\n}\n\nint UnreadBadge::textBaseline() const {\n\tconst auto st = Dialogs::Ui::UnreadBadgeStyle();\n\treturn ((st.size - st.font->height) / 2) + st.font->ascent;\n}\n\nvoid UnreadBadge::paintEvent(QPaintEvent *e) {\n\tif (_text.isEmpty()) {\n\t\treturn;\n\t}\n\n\tauto p = QPainter(this);\n\n\tUnreadBadgeStyle unreadSt;\n\tunreadSt.muted = !_active;\n\tauto unreadRight = width();\n\tauto unreadTop = 0;\n\tPaintUnreadBadge(\n\t\tp,\n\t\t_text,\n\t\tunreadRight,\n\t\tunreadTop,\n\t\tunreadSt);\n}\n\nQString TextBadgeText(TextBadgeType type) {\n\tswitch (type) {\n\tcase TextBadgeType::Fake: return tr::lng_fake_badge(tr::now);\n\tcase TextBadgeType::Scam: return tr::lng_scam_badge(tr::now);\n\tcase TextBadgeType::Direct: return tr::lng_direct_badge(tr::now);\n\t}\n\tUnexpected(\"Type in TextBadgeText.\");\n}\n\nQSize TextBadgeSize(TextBadgeType type) {\n\tconst auto phrase = TextBadgeText(type);\n\tconst auto phraseWidth = st::dialogsScamFont->width(phrase);\n\tconst auto width = st::dialogsScamPadding.left()\n\t\t+ phraseWidth\n\t\t+ st::dialogsScamPadding.right();\n\tconst auto height = st::dialogsScamPadding.top()\n\t\t+ st::dialogsScamFont->height\n\t\t+ st::dialogsScamPadding.bottom();\n\treturn { width, height };\n}\n\nvoid DrawTextBadge(\n\t\tPainter &p,\n\t\tQRect rect,\n\t\tint outerWidth,\n\t\tconst style::color &color,\n\t\tconst QString &phrase,\n\t\tint phraseWidth) {\n\tPainterHighQualityEnabler hq(p);\n\tauto pen = color->p;\n\tpen.setWidth(st::lineWidth);\n\tp.setPen(pen);\n\tp.setBrush(Qt::NoBrush);\n\tp.drawRoundedRect(rect, st::dialogsScamRadius, st::dialogsScamRadius);\n\tp.setFont(st::dialogsScamFont);\n\tif (style::DevicePixelRatio() > 1) {\n\t\tp.drawText(\n\t\t\tQRect(\n\t\t\t\trect.x() + st::dialogsScamPadding.left(),\n\t\t\t\trect.y() + st::dialogsScamPadding.top(),\n\t\t\t\trect.width() - rect::m::sum::h(st::dialogsScamPadding),\n\t\t\t\trect.height() - rect::m::sum::v(st::dialogsScamPadding)),\n\t\t\tQt::AlignCenter,\n\t\t\tphrase);\n\t} else {\n\t\tp.drawTextLeft(\n\t\t\trect.x() + st::dialogsScamPadding.left(),\n\t\t\trect.y() + st::dialogsScamPadding.top(),\n\t\t\touterWidth,\n\t\t\tphrase,\n\t\t\tphraseWidth);\n\t}\n}\n\nvoid DrawTextBadge(\n\t\tTextBadgeType type,\n\t\tPainter &p,\n\t\tQRect rect,\n\t\tint outerWidth,\n\t\tconst style::color &color) {\n\tconst auto phrase = TextBadgeText(type);\n\tDrawTextBadge(\n\t\tp,\n\t\trect,\n\t\touterWidth,\n\t\tcolor,\n\t\tphrase,\n\t\tst::dialogsScamFont->width(phrase));\n}\n\nPeerBadge::PeerBadge() = default;\n\nPeerBadge::~PeerBadge() = default;\n\nint PeerBadge::drawGetWidth(Painter &p, Descriptor &&descriptor) {\n\tExpects(descriptor.customEmojiRepaint != nullptr);\n\n\tconst auto peer = descriptor.peer;\n\tif ((descriptor.scam && (peer->isScam() || peer->isFake()))\n\t\t|| (descriptor.direct && peer->isMonoforum())) {\n\t\treturn drawTextBadge(p, descriptor);\n\t}\n\tconst auto verifyCheck = descriptor.verified && peer->isVerified();\n\tconst auto premiumMark = descriptor.premium\n\t\t&& peer->session().premiumBadgesShown();\n\tconst auto emojiStatus = premiumMark\n\t\t&& peer->emojiStatusId()\n\t\t&& (peer->isPremium() || peer->isChannel());\n\tconst auto premiumStar = premiumMark\n\t\t&& !emojiStatus\n\t\t&& peer->isPremium();\n\n\tconst auto paintVerify = verifyCheck\n\t\t&& (descriptor.prioritizeVerification\n\t\t\t|| descriptor.bothVerifyAndStatus\n\t\t\t|| !emojiStatus);\n\tconst auto paintEmoji = emojiStatus\n\t\t&& (!paintVerify || descriptor.bothVerifyAndStatus);\n\tconst auto paintStar = premiumStar && !paintVerify;\n\n\tauto result = 0;\n\tif (paintEmoji) {\n\t\tauto &rectForName = descriptor.rectForName;\n\t\tconst auto verifyWidth = descriptor.verified->width();\n\t\tif (paintVerify) {\n\t\t\trectForName.setWidth(rectForName.width() - verifyWidth);\n\t\t}\n\t\tresult += drawPremiumEmojiStatus(p, descriptor);\n\t\tif (!paintVerify) {\n\t\t\treturn result;\n\t\t}\n\t\trectForName.setWidth(rectForName.width() + verifyWidth);\n\t\tdescriptor.nameWidth += result;\n\t}\n\tif (paintVerify) {\n\t\tresult += drawVerifyCheck(p, descriptor);\n\t\treturn result;\n\t} else if (paintStar) {\n\t\treturn drawPremiumStar(p, descriptor);\n\t}\n\treturn 0;\n}\n\nint PeerBadge::drawTextBadge(Painter &p, const Descriptor &descriptor) {\n\tconst auto type = [&] {\n\t\tif (descriptor.peer->isScam()) {\n\t\t\treturn TextBadgeType::Scam;\n\t\t} else if (descriptor.peer->isFake()) {\n\t\t\treturn TextBadgeType::Fake;\n\t\t}\n\t\treturn TextBadgeType::Direct;\n\t}();\n\tconst auto phrase = TextBadgeText(type);\n\tconst auto phraseWidth = st::dialogsScamFont->width(phrase);\n\tconst auto width = st::dialogsScamPadding.left()\n\t\t+ phraseWidth\n\t\t+ st::dialogsScamPadding.right();\n\tconst auto height = st::dialogsScamPadding.top()\n\t\t+ st::dialogsScamFont->height\n\t\t+ st::dialogsScamPadding.bottom();\n\tconst auto rectForName = descriptor.rectForName;\n\tconst auto rect = QRect(\n\t\t(rectForName.x()\n\t\t\t+ qMin(\n\t\t\t\tdescriptor.nameWidth + st::dialogsScamSkip,\n\t\t\t\trectForName.width() - width)),\n\t\trectForName.y() + (rectForName.height() - height) / 2,\n\t\twidth,\n\t\theight);\n\tDrawTextBadge(\n\t\tp,\n\t\trect,\n\t\tdescriptor.outerWidth,\n\t\t*((type == TextBadgeType::Direct)\n\t\t\t? descriptor.direct\n\t\t\t: descriptor.scam),\n\t\tphrase,\n\t\tphraseWidth);\n\treturn st::dialogsScamSkip + width;\n}\n\nint PeerBadge::drawVerifyCheck(Painter &p, const Descriptor &descriptor) {\n\tconst auto iconw = descriptor.verified->width();\n\tconst auto rectForName = descriptor.rectForName;\n\tconst auto nameWidth = descriptor.nameWidth;\n\tdescriptor.verified->paint(\n\t\tp,\n\t\trectForName.x() + qMin(nameWidth, rectForName.width() - iconw),\n\t\trectForName.y(),\n\t\tdescriptor.outerWidth);\n\treturn iconw;\n}\n\nint PeerBadge::drawPremiumEmojiStatus(\n\t\tPainter &p,\n\t\tconst Descriptor &descriptor) {\n\tconst auto peer = descriptor.peer;\n\tconst auto id = peer->emojiStatusId();\n\tconst auto rectForName = descriptor.rectForName;\n\tconst auto iconw = descriptor.premium->width();\n\tconst auto iconx = rectForName.x()\n\t\t+ qMin(descriptor.nameWidth, rectForName.width() - iconw);\n\tconst auto icony = rectForName.y();\n\tif (!_emojiStatus) {\n\t\t_emojiStatus = std::make_unique<EmojiStatus>();\n\t\tconst auto size = st::emojiSize;\n\t\tconst auto emoji = Ui::Text::AdjustCustomEmojiSize(size);\n\t\t_emojiStatus->skip = (size - emoji) / 2;\n\t}\n\tif (_emojiStatus->id != id) {\n\t\tusing namespace Ui::Text;\n\t\tauto &manager = peer->session().data().customEmojiManager();\n\t\t_emojiStatus->id = id;\n\t\t_emojiStatus->emoji = std::make_unique<LimitedLoopsEmoji>(\n\t\t\tmanager.create(\n\t\t\t\tData::EmojiStatusCustomId(id),\n\t\t\t\tdescriptor.customEmojiRepaint),\n\t\t\tkPlayStatusLimit);\n\t}\n\t_emojiStatus->emoji->paint(p, {\n\t\t.textColor = (*descriptor.premiumFg)->c,\n\t\t.now = descriptor.now,\n\t\t.position = QPoint(\n\t\t\ticonx - 2 * _emojiStatus->skip,\n\t\t\ticony + _emojiStatus->skip),\n\t\t.paused = descriptor.paused || On(PowerSaving::kEmojiStatus),\n\t});\n\treturn iconw - 4 * _emojiStatus->skip;\n}\n\nint PeerBadge::drawPremiumStar(Painter &p, const Descriptor &descriptor) {\n\tconst auto rectForName = descriptor.rectForName;\n\tconst auto iconw = descriptor.premium->width();\n\tconst auto iconx = rectForName.x()\n\t\t+ qMin(descriptor.nameWidth, rectForName.width() - iconw);\n\tconst auto icony = rectForName.y();\n\t_emojiStatus = nullptr;\n\tdescriptor.premium->paint(p, iconx, icony, descriptor.outerWidth);\n\treturn iconw;\n}\n\nvoid PeerBadge::unload() {\n\t_emojiStatus = nullptr;\n}\n\nbool PeerBadge::ready(const BotVerifyDetails *details) const {\n\tif (!details || !*details) {\n\t\t_botVerifiedData = nullptr;\n\t\treturn true;\n\t} else if (!_botVerifiedData) {\n\t\treturn false;\n\t}\n\tif (!details->iconId) {\n\t\t_botVerifiedData->icon = nullptr;\n\t} else if (!_botVerifiedData->icon\n\t\t|| (_botVerifiedData->icon->entityData()\n\t\t\t!= Data::SerializeCustomEmojiId(details->iconId))) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nvoid PeerBadge::set(\n\t\tnot_null<const BotVerifyDetails*> details,\n\t\tUi::Text::CustomEmojiFactory factory,\n\t\tFn<void()> repaint) {\n\tif (!_botVerifiedData) {\n\t\t_botVerifiedData = std::make_unique<BotVerifiedData>();\n\t}\n\tif (details->iconId) {\n\t\t_botVerifiedData->icon = std::make_unique<Ui::Text::FirstFrameEmoji>(\n\t\t\tfactory(\n\t\t\t\tData::SerializeCustomEmojiId(details->iconId),\n\t\t\t\t{ .repaint = repaint }));\n\t}\n}\n\nint PeerBadge::drawVerified(\n\t\tQPainter &p,\n\t\tQPoint position,\n\t\tconst style::VerifiedBadge &st) {\n\tconst auto data = _botVerifiedData.get();\n\tif (!data) {\n\t\treturn 0;\n\t}\n\tif (const auto icon = data->icon.get()) {\n\t\ticon->paint(p, {\n\t\t\t.textColor = st.color->c,\n\t\t\t.now = crl::now(),\n\t\t\t.position = position,\n\t\t});\n\t\treturn icon->width();\n\t}\n\treturn 0;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/unread_badge.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/text/text_custom_emoji.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace style {\nstruct VerifiedBadge;\n} // namespace style\n\nnamespace Ui {\n\nclass UnreadBadge : public RpWidget {\npublic:\n\tusing RpWidget::RpWidget;\n\n\tvoid setText(const QString &text, bool active);\n\tint textBaseline() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tQString _text;\n\tbool _active = false;\n\n};\n\nstruct BotVerifyDetails {\n\tUserId botId = 0;\n\tDocumentId iconId = 0;\n\tTextWithEntities description;\n\n\texplicit operator bool() const {\n\t\treturn iconId != 0;\n\t}\n\tfriend inline bool operator==(\n\t\tconst BotVerifyDetails &,\n\t\tconst BotVerifyDetails &) = default;\n};\n\nclass PeerBadge {\npublic:\n\tPeerBadge();\n\t~PeerBadge();\n\n\tstruct Descriptor {\n\t\tnot_null<PeerData*> peer;\n\t\tQRect rectForName;\n\t\tint nameWidth = 0;\n\t\tint outerWidth = 0;\n\t\tconst style::icon *verified = nullptr;\n\t\tconst style::icon *premium = nullptr;\n\t\tconst style::color *scam = nullptr;\n\t\tconst style::color *direct = nullptr;\n\t\tconst style::color *premiumFg = nullptr;\n\t\tFn<void()> customEmojiRepaint;\n\t\tcrl::time now = 0;\n\t\tbool prioritizeVerification = false;\n\t\tbool bothVerifyAndStatus = false;\n\t\tbool paused = false;\n\t};\n\tint drawGetWidth(Painter &p, Descriptor &&descriptor);\n\tvoid unload();\n\n\t[[nodiscard]] bool ready(const BotVerifyDetails *details) const;\n\tvoid set(\n\t\tnot_null<const BotVerifyDetails*> details,\n\t\tText::CustomEmojiFactory factory,\n\t\tFn<void()> repaint);\n\n\t// How much horizontal space the badge took.\n\tint drawVerified(\n\t\tQPainter &p,\n\t\tQPoint position,\n\t\tconst style::VerifiedBadge &st);\n\nprivate:\n\tstruct EmojiStatus;\n\tstruct BotVerifiedData;\n\n\tint drawTextBadge(Painter &p, const Descriptor &descriptor);\n\tint drawVerifyCheck(Painter &p, const Descriptor &descriptor);\n\tint drawPremiumEmojiStatus(Painter &p, const Descriptor &descriptor);\n\tint drawPremiumStar(Painter &p, const Descriptor &descriptor);\n\n\tstd::unique_ptr<EmojiStatus> _emojiStatus;\n\tmutable std::unique_ptr<BotVerifiedData> _botVerifiedData;\n\n};\n\nenum class TextBadgeType : uchar {\n\tScam,\n\tFake,\n\tDirect,\n};\n\nQSize TextBadgeSize(TextBadgeType type);\nvoid DrawTextBadge(\n\tTextBadgeType,\n\tPainter &p,\n\tQRect rect,\n\tint outerWidth,\n\tconst style::color &color);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/unread_badge_paint.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/unread_badge_paint.h\"\n\n#include \"ui/ui_utility.h\"\n#include \"styles/style_dialogs.h\"\n\nnamespace Ui {\nnamespace {\n\nstruct UnreadBadgeSizeData {\n\tQImage circle;\n\tQPixmap left[6], right[6];\n};\nclass UnreadBadgeStyleData {\npublic:\n\tUnreadBadgeStyleData();\n\n\tUnreadBadgeSizeData sizes[static_cast<int>(UnreadBadgeSize::kCount)];\n\tstyle::color bg[6] = {\n\t\tst::dialogsUnreadBg,\n\t\tst::dialogsUnreadBgOver,\n\t\tst::dialogsUnreadBgActive,\n\t\tst::dialogsUnreadBgMuted,\n\t\tst::dialogsUnreadBgMutedOver,\n\t\tst::dialogsUnreadBgMutedActive\n\t};\n\tstyle::color reactionBg[6] = {\n\t\tst::dialogsDraftFg,\n\t\tst::dialogsDraftFgOver,\n\t\tst::dialogsDraftFgActive,\n\t\tst::dialogsUnreadBgMuted,\n\t\tst::dialogsUnreadBgMutedOver,\n\t\tst::dialogsUnreadBgMutedActive\n\t};\n\tstyle::color pollBg[6] = {\n\t\tst::dialogsPollIconFg,\n\t\tst::dialogsPollIconFg,\n\t\tst::dialogsPollIconFg,\n\t\tst::dialogsUnreadBgMuted,\n\t\tst::dialogsUnreadBgMutedOver,\n\t\tst::dialogsUnreadBgMutedActive\n\t};\n\trpl::lifetime lifetime;\n};\n\nUnreadBadgeStyleData::UnreadBadgeStyleData() {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tfor (auto &data : sizes) {\n\t\t\tfor (auto &left : data.left) {\n\t\t\t\tleft = QPixmap();\n\t\t\t}\n\t\t\tfor (auto &right : data.right) {\n\t\t\t\tright = QPixmap();\n\t\t\t}\n\t\t}\n\t}, lifetime);\n}\n\nUnreadBadgeStyleData &UnreadBadgeStyles() {\n\tstatic auto result = UnreadBadgeStyleData();\n\treturn result;\n}\n\nvoid CreateCircleMask(UnreadBadgeSizeData *data, int size) {\n\tif (!data->circle.isNull()) {\n\t\treturn;\n\t}\n\tdata->circle = style::createCircleMask(size);\n}\n\n[[nodiscard]] QImage ColorizeCircleHalf(\n\t\tUnreadBadgeSizeData *data,\n\t\tint size,\n\t\tint half,\n\t\tint xoffset,\n\t\tstyle::color color) {\n\tauto result = style::colorizeImage(data->circle, color, QRect(xoffset, 0, half, size));\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\treturn result;\n}\n\n[[nodiscard]] QString ComputeUnreadBadgeText(\n\t\tconst QString &unreadCount,\n\t\tint allowDigits) {\n\treturn (allowDigits > 0) && (unreadCount.size() > allowDigits + 1)\n\t\t? u\"..\"_q + unreadCount.mid(unreadCount.size() - allowDigits)\n\t\t: unreadCount;\n}\n\nvoid PaintUnreadBadge(QPainter &p, const QRect &rect, const UnreadBadgeStyle &st) {\n\tAssert(rect.height() == st.size);\n\n\tint index = (st.muted ? 0x03 : 0x00) + (st.active ? 0x02 : (st.selected ? 0x01 : 0x00));\n\tint size = st.size, sizehalf = size / 2;\n\n\tauto &styles = UnreadBadgeStyles();\n\tauto badgeData = styles.sizes;\n\tif (st.sizeId > UnreadBadgeSize()) {\n\t\tAssert(st.sizeId < UnreadBadgeSize::kCount);\n\t\tbadgeData = &styles.sizes[static_cast<int>(st.sizeId)];\n\t}\n\tconst auto bg = (st.sizeId == UnreadBadgeSize::ReactionInDialogs)\n\t\t? styles.reactionBg[index]\n\t\t: (st.sizeId == UnreadBadgeSize::PollInDialogs)\n\t\t? styles.pollBg[index]\n\t\t: styles.bg[index];\n\tif (badgeData->left[index].isNull()) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\tint imgsize = size * ratio, imgsizehalf = sizehalf * ratio;\n\t\tCreateCircleMask(badgeData, size);\n\t\tbadgeData->left[index] = PixmapFromImage(\n\t\t\tColorizeCircleHalf(badgeData, imgsize, imgsizehalf, 0, bg));\n\t\tbadgeData->right[index] = PixmapFromImage(ColorizeCircleHalf(\n\t\t\tbadgeData,\n\t\t\timgsize,\n\t\t\timgsizehalf,\n\t\t\timgsize - imgsizehalf,\n\t\t\tbg));\n\t}\n\n\tint bar = rect.width() - 2 * sizehalf;\n\tp.drawPixmap(rect.x(), rect.y(), badgeData->left[index]);\n\tif (bar) {\n\t\tp.fillRect(rect.x() + sizehalf, rect.y(), bar, rect.height(), bg);\n\t}\n\tp.drawPixmap(rect.x() + sizehalf + bar, rect.y(), badgeData->right[index]);\n}\n\n} // namespace\n\nUnreadBadgeStyle::UnreadBadgeStyle()\n: size(st::dialogsUnreadHeight)\n, padding(st::dialogsUnreadPadding)\n, font(st::dialogsUnreadFont) {\n}\n\nQSize CountUnreadBadgeSize(\n\t\tconst QString &unreadCount,\n\t\tconst UnreadBadgeStyle &st,\n\t\tint allowDigits) {\n\tconst auto text = ComputeUnreadBadgeText(unreadCount, allowDigits);\n\tconst auto unreadRectHeight = st.size;\n\tconst auto unreadWidth = st.font->width(text);\n\treturn {\n\t\tstd::max(unreadWidth + 2 * st.padding, unreadRectHeight),\n\t\tunreadRectHeight,\n\t};\n}\n\nQRect PaintUnreadBadge(\n\t\tQPainter &p,\n\t\tconst QString &unreadCount,\n\t\tint x,\n\t\tint y,\n\t\tconst UnreadBadgeStyle &st,\n\t\tint allowDigits) {\n\tconst auto text = ComputeUnreadBadgeText(unreadCount, allowDigits);\n\tconst auto unreadRectHeight = st.size;\n\tconst auto unreadWidth = st.font->width(text);\n\tconst auto unreadRectWidth = std::max(\n\t\tunreadWidth + 2 * st.padding,\n\t\tunreadRectHeight);\n\n\tconst auto unreadRectLeft = ((st.align & Qt::AlignHorizontal_Mask) & style::al_center)\n\t\t? (x - unreadRectWidth) / 2\n\t\t: ((st.align & Qt::AlignHorizontal_Mask) & style::al_right)\n\t\t? (x - unreadRectWidth)\n\t\t: x;\n\tconst auto unreadRectTop = y;\n\n\tconst auto badge = QRect(unreadRectLeft, unreadRectTop, unreadRectWidth, unreadRectHeight);\n\tPaintUnreadBadge(p, badge, st);\n\n\tconst auto textTop = st.textTop ? st.textTop : (unreadRectHeight - st.font->height) / 2;\n\tp.setFont(st.font);\n\tp.setPen(st.active\n\t\t? st::dialogsUnreadFgActive\n\t\t: st.selected\n\t\t? st::dialogsUnreadFgOver\n\t\t: st::dialogsUnreadFg);\n\tp.drawText(unreadRectLeft + (unreadRectWidth - unreadWidth) / 2, unreadRectTop + textTop + st.font->ascent, text);\n\n\treturn badge;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/unread_badge_paint.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\n\nenum class UnreadBadgeSize {\n\tDialogs,\n\tMainMenu,\n\tHistoryToDown,\n\tStickersPanel,\n\tStickersBox,\n\tTouchBar,\n\tReactionInDialogs,\n\tPollInDialogs,\n\n\tkCount,\n};\nstruct UnreadBadgeStyle {\n\tUnreadBadgeStyle();\n\n\tstyle::align align = style::al_right;\n\tbool active = false;\n\tbool selected = false;\n\tbool muted = false;\n\tint textTop = 0;\n\tint size = 0;\n\tint padding = 0;\n\tUnreadBadgeSize sizeId = UnreadBadgeSize::Dialogs;\n\tstyle::font font;\n};\n\n[[nodiscard]] QSize CountUnreadBadgeSize(\n\tconst QString &unreadCount,\n\tconst UnreadBadgeStyle &st,\n\tint allowDigits = 0);\n\nQRect PaintUnreadBadge(\n\tQPainter &p,\n\tconst QString &t,\n\tint x,\n\tint y,\n\tconst UnreadBadgeStyle &st,\n\tint allowDigits = 0);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/unread_counter_format.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/unread_counter_format.h\"\n\nQString FormatUnreadCounter(\n\t\tint unreadCounter,\n\t\tbool hasMentionOrReaction,\n\t\tbool narrow) {\n\tif (unreadCounter <= 0) {\n\t\treturn QString();\n\t}\n\tif (!narrow) {\n\t\treturn QString::number(unreadCounter);\n\t}\n\tif (hasMentionOrReaction && (unreadCounter > 999)) {\n\t\treturn u\"99+\"_q;\n\t}\n\tif (unreadCounter > 999999) {\n\t\treturn u\"99999+\"_q;\n\t}\n\treturn QString::number(unreadCounter);\n}"
  },
  {
    "path": "Telegram/SourceFiles/ui/unread_counter_format.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n[[nodiscard]] QString FormatUnreadCounter(\n\tint unreadCounter,\n\tbool hasMentionOrReaction,\n\tbool narrow = false);"
  },
  {
    "path": "Telegram/SourceFiles/ui/userpic_view.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/userpic_view.h\"\n\n#include \"ui/empty_userpic.h\"\n#include \"ui/painter.h\"\n#include \"ui/image/image_prepare.h\"\n\nnamespace Ui {\n\nfloat64 ForumUserpicRadiusMultiplier() {\n\treturn 0.3;\n}\n\nbool PeerUserpicLoading(const PeerUserpicView &view) {\n\treturn view.cloud && view.cloud->isNull();\n}\n\nvoid ValidateUserpicCache(\n\t\tPeerUserpicView &view,\n\t\tconst QImage *cloud,\n\t\tconst EmptyUserpic *empty,\n\t\tint size,\n\t\tPeerUserpicShape shape) {\n\tExpects(cloud != nullptr || empty != nullptr);\n\n\tconst auto full = QSize(size, size);\n\tconst auto version = style::PaletteVersion();\n\tconst auto shapeValue = static_cast<uint32>(shape) & 3;\n\tconst auto regenerate = (view.cached.size() != QSize(size, size))\n\t\t|| (view.shape != shapeValue)\n\t\t|| (cloud && !view.empty.null())\n\t\t|| (empty && empty != view.empty.get())\n\t\t|| (empty && view.paletteVersion != version);\n\tif (!regenerate) {\n\t\treturn;\n\t}\n\tview.empty = empty;\n\tview.shape = shapeValue;\n\tview.paletteVersion = version;\n\n\tif (cloud) {\n\t\tview.cached = cloud->scaled(\n\t\t\tfull,\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t\tif (shape == PeerUserpicShape::Monoforum) {\n\t\t\tview.cached = Ui::ApplyMonoforumShape(std::move(view.cached));\n\t\t} else if (shape == PeerUserpicShape::Forum) {\n\t\t\tview.cached = Images::Round(\n\t\t\t\tstd::move(view.cached),\n\t\t\t\tImages::CornersMask(size\n\t\t\t\t\t* Ui::ForumUserpicRadiusMultiplier()\n\t\t\t\t\t/ style::DevicePixelRatio()));\n\t\t} else {\n\t\t\tview.cached = Images::Circle(std::move(view.cached));\n\t\t}\n\t} else {\n\t\tif (view.cached.size() != full) {\n\t\t\tview.cached = QImage(full, QImage::Format_ARGB32_Premultiplied);\n\t\t}\n\t\tview.cached.fill(Qt::transparent);\n\n\t\tauto p = QPainter(&view.cached);\n\t\tif (shape == PeerUserpicShape::Monoforum) {\n\t\t\tempty->paintMonoforum(p, 0, 0, size, size);\n\t\t} else if (shape == PeerUserpicShape::Forum) {\n\t\t\tempty->paintRounded(\n\t\t\t\tp,\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\tsize,\n\t\t\t\tsize,\n\t\t\t\tsize * Ui::ForumUserpicRadiusMultiplier());\n\t\t} else {\n\t\t\tempty->paintCircle(p, 0, 0, size, size);\n\t\t}\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/userpic_view.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/weak_ptr.h\"\n\n#include <QtGui/QImage>\n\nnamespace Ui {\n\nclass EmptyUserpic;\n\n[[nodiscard]] float64 ForumUserpicRadiusMultiplier();\n\nenum class PeerUserpicShape : uint8 {\n\tAuto,\n\tCircle,\n\tForum,\n\tMonoforum,\n};\n\nstruct PeerUserpicView {\n\t[[nodiscard]] bool null() const {\n\t\treturn cached.isNull() && !cloud && empty.null();\n\t}\n\n\tQImage cached;\n\tstd::shared_ptr<QImage> cloud;\n\tbase::weak_ptr<const EmptyUserpic> empty;\n\tuint32 paletteVersion : 30 = 0;\n\tuint32 shape : 2 = 0;\n};\n\n[[nodiscard]] bool PeerUserpicLoading(const PeerUserpicView &view);\n\nvoid ValidateUserpicCache(\n\tPeerUserpicView &view,\n\tconst QImage *cloud,\n\tconst EmptyUserpic *empty,\n\tint size,\n\tPeerUserpicShape shape);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/vertical_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/vertical_list.h\"\n\n#include \"lang/lang_text_entity.h\"\n#include \"ui/widgets/box_content_divider.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/padding_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Ui {\n\nvoid AddSkip(not_null<Ui::VerticalLayout*> container) {\n\tAddSkip(container, st::defaultVerticalListSkip);\n}\n\nvoid AddSkip(not_null<Ui::VerticalLayout*> container, int skip) {\n\tcontainer->add(object_ptr<Ui::FixedHeightWidget>(container, skip));\n}\n\nvoid AddDivider(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tconst style::DividerBar &st) {\n\tcontainer->add(object_ptr<Ui::BoxContentDivider>(\n\t\tcontainer,\n\t\tst::boxDividerHeight,\n\t\tst));\n}\n\nnot_null<Ui::FlatLabel*> AddDividerText(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> text,\n\t\tconst style::margins &margins,\n\t\tconst style::DividerLabel &st,\n\t\tRectParts parts) {\n\treturn AddDividerText(\n\t\tcontainer,\n\t\tstd::move(text) | rpl::map(tr::marked),\n\t\tmargins,\n\t\tst,\n\t\tparts);\n}\n\nnot_null<Ui::FlatLabel*> AddDividerText(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<TextWithEntities> text,\n\t\tconst style::margins &margins,\n\t\tconst style::DividerLabel &st,\n\t\tRectParts parts) {\n\tauto label = object_ptr<Ui::FlatLabel>(\n\t\tcontainer,\n\t\tstd::move(text),\n\t\tst.label);\n\tconst auto result = label.data();\n\tcontainer->add(object_ptr<Ui::DividerLabel>(\n\t\tcontainer,\n\t\tstd::move(label),\n\t\tmargins,\n\t\tst.bar,\n\t\tparts));\n\treturn result;\n}\n\nnot_null<Ui::FlatLabel*> AddSubsectionTitle(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\trpl::producer<QString> text,\n\t\tstyle::margins addPadding,\n\t\tconst style::FlatLabel *st) {\n\treturn container->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tcontainer,\n\t\t\tstd::move(text),\n\t\t\tst ? *st : st::defaultSubsectionTitle),\n\t\tst::defaultSubsectionTitlePadding + addPadding);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/vertical_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rect_part.h\"\n\nnamespace style {\nstruct FlatLabel;\nstruct DividerBar;\nstruct DividerLabel;\n} // namespace style\n\nnamespace st {\nextern const style::margins &defaultBoxDividerLabelPadding;\nextern const style::DividerBar &defaultDividerBar;\nextern const style::DividerLabel &defaultDividerLabel;\n} // namespace st\n\nnamespace Ui {\n\nclass FlatLabel;\nclass VerticalLayout;\n\nvoid AddSkip(not_null<Ui::VerticalLayout*> container);\nvoid AddSkip(not_null<Ui::VerticalLayout*> container, int skip);\nvoid AddDivider(\n\tnot_null<Ui::VerticalLayout*> container,\n\tconst style::DividerBar &st = st::defaultDividerBar);\nnot_null<Ui::FlatLabel*> AddDividerText(\n\tnot_null<Ui::VerticalLayout*> container,\n\trpl::producer<QString> text,\n\tconst style::margins &margins = st::defaultBoxDividerLabelPadding,\n\tconst style::DividerLabel &st = st::defaultDividerLabel,\n\tRectParts parts = RectPart::Top | RectPart::Bottom);\nnot_null<Ui::FlatLabel*> AddDividerText(\n\tnot_null<Ui::VerticalLayout*> container,\n\trpl::producer<TextWithEntities> text,\n\tconst style::margins &margins = st::defaultBoxDividerLabelPadding,\n\tconst style::DividerLabel &st = st::defaultDividerLabel,\n\tRectParts parts = RectPart::Top | RectPart::Bottom);\nnot_null<Ui::FlatLabel*> AddSubsectionTitle(\n\tnot_null<Ui::VerticalLayout*> container,\n\trpl::producer<QString> text,\n\tstyle::margins addPadding = {},\n\tconst style::FlatLabel *st = nullptr);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/webview_helpers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/webview_helpers.h\"\n\n#include \"lang/lang_keys.h\"\n\nnamespace Ui {\nnamespace {\n\n[[nodiscard]] QByteArray Serialize(const QColor &qt) {\n\tif (qt.alpha() == 255) {\n\t\treturn '#'\n\t\t\t+ QByteArray::number(qt.red(), 16).rightJustified(2, '0')\n\t\t\t+ QByteArray::number(qt.green(), 16).rightJustified(2, '0')\n\t\t\t+ QByteArray::number(qt.blue(), 16).rightJustified(2, '0');\n\t}\n\treturn \"rgba(\"\n\t\t+ QByteArray::number(qt.red()) + \",\"\n\t\t+ QByteArray::number(qt.green()) + \",\"\n\t\t+ QByteArray::number(qt.blue()) + \",\"\n\t\t+ QByteArray::number(qt.alpha() / 255.) + \")\";\n}\n\n} // namespace\n\nQByteArray ComputeStyles(\n\t\tconst base::flat_map<QByteArray, const style::color*> &colors,\n\t\tconst base::flat_map<QByteArray, tr::phrase<>> &phrases,\n\t\tint zoom,\n\t\tbool nightTheme) {\n\tstatic const auto serialize = [](const style::color *color) {\n\t\treturn Serialize((*color)->c);\n\t};\n\tstatic const auto escape = [](tr::phrase<> phrase) {\n\t\tconst auto text = phrase(tr::now);\n\n\t\tauto result = QByteArray();\n\t\tfor (auto i = 0; i != text.size(); ++i) {\n\t\t\tuint ucs4 = text[i].unicode();\n\t\t\tif (QChar::isHighSurrogate(ucs4) && i + 1 != text.size()) {\n\t\t\t\tushort low = text[i + 1].unicode();\n\t\t\t\tif (QChar::isLowSurrogate(low)) {\n\t\t\t\t\tucs4 = QChar::surrogateToUcs4(ucs4, low);\n\t\t\t\t\t++i;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (ucs4 == '\\'' || ucs4 == '\\\"' || ucs4 == '\\\\') {\n\t\t\t\tresult.append('\\\\').append(char(ucs4));\n\t\t\t} else if (ucs4 < 32 || ucs4 > 127) {\n\t\t\t\tresult.append('\\\\' + QByteArray::number(ucs4, 16) + ' ');\n\t\t\t} else {\n\t\t\t\tresult.append(char(ucs4));\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t};\n\tauto result = QByteArray();\n\tfor (const auto &[name, phrase] : phrases) {\n\t\tresult += \"--td-lng-\"_q + name + \":'\"_q + escape(phrase) + \"'; \"_q;\n\t}\n\tfor (const auto &[name, color] : colors) {\n\t\tresult += \"--td-\"_q + name + ':' + serialize(color) + ';';\n\t}\n\tresult += \"--td-night:\"_q + (nightTheme ? \"1\" : \"0\") + ';';\n\tresult += \"--td-zoom-percentage:\"_q\n\t\t+ (QString::number(zoom).toUtf8() + '%')\n\t\t+ ';';\n\treturn result;\n}\n\nQByteArray ComputeSemiTransparentOverStyle(\n\t\tconst QByteArray &name,\n\t\tconst style::color &over,\n\t\tconst style::color &bg) {\n\tconst auto result = [&](const QColor &c) {\n\t\treturn \"--td-\"_q + name + ':' + Serialize(c) + ';';\n\t};\n\tif (over->c.alpha() < 255) {\n\t\treturn result(over->c);\n\t}\n\t// The most transparent color that will still give the same result.\n\tconst auto r0 = bg->c.red();\n\tconst auto g0 = bg->c.green();\n\tconst auto b0 = bg->c.blue();\n\tconst auto r1 = over->c.red();\n\tconst auto g1 = over->c.green();\n\tconst auto b1 = over->c.blue();\n\tconst auto mina = [](int c0, int c1) {\n\t\treturn (c0 == c1)\n\t\t\t? 0\n\t\t\t: (c0 > c1)\n\t\t\t? (((c0 - c1) * 255) / c0)\n\t\t\t: (((c1 - c0) * 255) / (255 - c0));\n\t};\n\tconst auto rmina = mina(r0, r1);\n\tconst auto gmina = mina(g0, g1);\n\tconst auto bmina = mina(b0, b1);\n\tconst auto a = std::max({ rmina, gmina, bmina });\n\tconst auto r = (a > 0) ? ((r1 * 255 - r0 * (255 - a)) / a) : r0;\n\tconst auto g = (a > 0) ? ((g1 * 255 - g0 * (255 - a)) / a) : g0;\n\tconst auto b = (a > 0) ? ((b1 * 255 - b0 * (255 - a)) / a) : b0;\n\treturn result(QColor(r, g, b, a));\n}\n\nQByteArray EscapeForAttribute(QByteArray value) {\n\treturn value\n\t\t.replace('&', \"&amp;\")\n\t\t.replace('\"', \"&quot;\")\n\t\t.replace('\\'', \"&#039;\")\n\t\t.replace('<', \"&lt;\")\n\t\t.replace('>', \"&gt;\");\n}\n\nQByteArray EscapeForScriptString(QByteArray value) {\n\treturn value\n\t\t.replace('\\\\', \"\\\\\\\\\")\n\t\t.replace('\"', \"\\\\\\\"\")\n\t\t.replace('\\'', \"\\\\\\'\");\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/webview_helpers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/flat_map.h\"\n\nnamespace tr {\ntemplate <typename ...Tags>\nstruct phrase;\n} // namespace tr\n\nnamespace Ui {\n\n[[nodiscard]] QByteArray ComputeStyles(\n\tconst base::flat_map<QByteArray, const style::color*> &colors,\n\tconst base::flat_map<QByteArray, tr::phrase<>> &phrases,\n\tint zoom,\n\tbool nightTheme = false);\n[[nodiscard]] QByteArray ComputeSemiTransparentOverStyle(\n\tconst QByteArray &name,\n\tconst style::color &over,\n\tconst style::color &bg);\n\n[[nodiscard]] QByteArray EscapeForAttribute(QByteArray value);\n[[nodiscard]] QByteArray EscapeForScriptString(QByteArray value);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/chat_filters_tabs_slider.h\"\n\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/widgets/side_bar_button.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_widgets.h\"\n\n#include <QScrollBar>\n\nnamespace Ui {\n\nChatsFiltersTabs::ChatsFiltersTabs(\n\tnot_null<Ui::RpWidget*> parent,\n\tconst style::SettingsSlider &st)\n: Ui::SettingsSlider(parent, st)\n, _st(st)\n, _unreadSt([&] {\n\tauto st = Ui::UnreadBadgeStyle();\n\tst.align = style::al_left;\n\treturn st;\n}())\n, _unreadMaxString(u\"99+\"_q)\n, _unreadSkip(st::lineWidth * 5) {\n\tExpects(_st.barSnapToLabel && _st.strictSkip);\n\tif (_st.barRadius > 0) {\n\t\t_bar.emplace(_st.barRadius, _st.barFg);\n\t\t_barActive.emplace(_st.barRadius, _st.barFgActive);\n\t}\n\t{\n\t\tconst auto one = Ui::CountUnreadBadgeSize(u\"9\"_q, _unreadSt, 1);\n\t\t_cachedBadgeWidths = {\n\t\t\tone.width(),\n\t\t\tUi::CountUnreadBadgeSize(u\"99\"_q, _unreadSt, 2).width(),\n\t\t\tUi::CountUnreadBadgeSize(u\"999\"_q, _unreadSt, 2).width(),\n\t\t};\n\t\t_cachedBadgeHeight = one.height();\n\t}\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tfor (auto &[index, unread] : _unreadCounts) {\n\t\t\tunread.cache = cacheUnreadCount(unread.count, unread.muted);\n\t\t}\n\t\tupdate();\n\t}, lifetime());\n\tUi::DiscreteSlider::setSelectOnPress(false);\n}\n\nbool ChatsFiltersTabs::setSectionsAndCheckChanged(\n\t\tstd::vector<TextWithEntities> &&sections,\n\t\tconst Text::MarkedContext &context,\n\t\tFn<bool()> paused) {\n\tconst auto &was = sectionsRef();\n\tconst auto changed = [&] {\n\t\tif (was.size() != sections.size()) {\n\t\t\treturn true;\n\t\t}\n\t\tfor (auto i = 0; i < sections.size(); i++) {\n\t\t\tif (was[i].label.toTextWithEntities() != sections[i]) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}();\n\tif (changed) {\n\t\tUi::DiscreteSlider::setSections(std::move(sections), context);\n\t}\n\t_emojiPaused = std::move(paused);\n\treturn changed;\n}\n\nvoid ChatsFiltersTabs::fitWidthToSections() {\n\tSettingsSlider::fitWidthToSections();\n\n\t_lockedFromX = calculateLockedFromX();\n\n\t{\n\t\t_sections.clear();\n\t\tenumerateSections([&](Section &section) {\n\t\t\t_sections.push_back({ not_null{ &section }, 0, false });\n\t\t\treturn true;\n\t\t});\n\t}\n}\n\nvoid ChatsFiltersTabs::setUnreadCount(int index, int unreadCount, bool mute) {\n\tconst auto it = _unreadCounts.find(index);\n\tif (it == _unreadCounts.end()) {\n\t\tif (unreadCount) {\n\t\t\t_unreadCounts.emplace(index, Unread{\n\t\t\t\t.cache = cacheUnreadCount(unreadCount, mute),\n\t\t\t\t.count = ushort(std::clamp(\n\t\t\t\t\tunreadCount,\n\t\t\t\t\t0,\n\t\t\t\t\tint(std::numeric_limits<ushort>::max()))),\n\t\t\t\t.muted = mute,\n\t\t\t});\n\t\t\tupdate();\n\t\t}\n\t} else if (!unreadCount) {\n\t\t_unreadCounts.erase(it);\n\t\tupdate();\n\t} else if (it->second.count != unreadCount || it->second.muted != mute) {\n\t\tit->second.count = unreadCount;\n\t\tit->second.muted = mute;\n\t\tit->second.cache = cacheUnreadCount(unreadCount, mute);\n\t\tupdate();\n\t}\n\tif (unreadCount) {\n\t\tconst auto widthIndex = (unreadCount < 10)\n\t\t\t? 0\n\t\t\t: (unreadCount < 100)\n\t\t\t? 1\n\t\t\t: 2;\n\t\tsetAdditionalContentWidthToSection(\n\t\t\tindex,\n\t\t\t_cachedBadgeWidths[widthIndex] + _unreadSkip);\n\t} else {\n\t\tsetAdditionalContentWidthToSection(index, 0);\n\t}\n}\n\nint ChatsFiltersTabs::calculateLockedFromX() const {\n\tif (!_lockedFrom) {\n\t\treturn std::numeric_limits<int>::max();\n\t}\n\tauto left = 0;\n\tauto index = 0;\n\tenumerateSections([&](const Section &section) {\n\t\tconst auto currentRight = section.left + section.width;\n\t\tif (index == _lockedFrom) {\n\t\t\treturn false;\n\t\t}\n\t\tleft = currentRight;\n\t\tindex++;\n\t\treturn true;\n\t});\n\treturn left ? left : std::numeric_limits<int>::max();\n}\n\nvoid ChatsFiltersTabs::setLockedFrom(int index) {\n\t_lockedFrom = index;\n\t_lockedFromX = calculateLockedFromX();\n\tif (!index) {\n\t\t_paletteLifetime.destroy();\n\t\treturn;\n\t}\n\t_paletteLifetime = style::PaletteChanged(\n\t) | rpl::on_next([this] {\n\t\t_lockCache.emplace(Ui::SideBarLockIcon(_st.labelFg));\n\t});\n}\n\nQImage ChatsFiltersTabs::cacheUnreadCount(int count, bool muted) const {\n\tconst auto widthIndex = (count < 10) ? 0 : (count < 100) ? 1 : 2;\n\tauto image = QImage(\n\t\tQSize(_cachedBadgeWidths[widthIndex], _cachedBadgeHeight)\n\t\t\t* style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\timage.fill(Qt::transparent);\n\tconst auto string = (count > 999)\n\t\t? _unreadMaxString\n\t\t: QString::number(count);\n\t{\n\t\tauto p = QPainter(&image);\n\t\tif (muted) {\n\t\t\tauto copy = _unreadSt;\n\t\t\tcopy.muted = muted;\n\t\t\tUi::PaintUnreadBadge(p, string, 0, 0, copy, 0);\n\t\t} else {\n\t\t\tUi::PaintUnreadBadge(p, string, 0, 0, _unreadSt, 0);\n\t\t}\n\t}\n\treturn image;\n}\n\nvoid ChatsFiltersTabs::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto clip = e->rect();\n\tconst auto range = getCurrentActiveRange();\n\tconst auto activeIndex = activeSection();\n\tconst auto now = crl::now();\n\n\tauto index = 0;\n\tauto raisedIndex = -1;\n\tauto activeHorizontalShift = 0;\n\tconst auto drawSection = [&](Section &section) {\n\t\t// const auto activeWidth = _st.barSnapToLabel\n\t\t// \t? section.contentWidth\n\t\t// \t: section.width;\n\n\t\tconst auto horizontalShift = _sections[index].horizontalShift;\n\t\tconst auto shiftedLeft = section.left + horizontalShift;\n\t\tif (_sections[index].raise) {\n\t\t\traisedIndex = index;\n\t\t}\n\t\tif (index == activeIndex) {\n\t\t\tactiveHorizontalShift = horizontalShift;\n\t\t}\n\n\t\t// const auto activeLeft = shiftedLeft\n\t\t// \t+ (section.width - activeWidth) / 2;\n\t\t// const auto active = 1.\n\t\t// \t- std::clamp(\n\t\t// \t\tstd::abs(range.left - activeLeft) / float64(range.width),\n\t\t// \t\t0.,\n\t\t// \t\t1.);\n\t\tconst auto active = (index == activeIndex) ? 1. : 0.;\n\t\tif (section.ripple) {\n\t\t\tconst auto color = anim::color(\n\t\t\t\t_st.rippleBg,\n\t\t\t\t_st.rippleBgActive,\n\t\t\t\tactive);\n\t\t\tsection.ripple->paint(p, shiftedLeft, 0, width(), &color);\n\t\t\tif (section.ripple->empty()) {\n\t\t\t\tsection.ripple.reset();\n\t\t\t}\n\t\t}\n\t\tconst auto labelLeft = shiftedLeft\n\t\t\t+ (section.width - section.contentWidth) / 2;\n\t\tconst auto rect = myrtlrect(\n\t\t\tlabelLeft,\n\t\t\t_st.labelTop,\n\t\t\tsection.contentWidth,\n\t\t\t_st.labelStyle.font->height);\n\t\tif (rect.intersects(clip)) {\n\t\t\tconst auto locked = (_lockedFrom && (index >= _lockedFrom));\n\t\t\tif (locked) {\n\t\t\t\tconstexpr auto kPremiumLockedOpacity = 0.6;\n\t\t\t\tp.setOpacity(kPremiumLockedOpacity);\n\t\t\t}\n\t\t\tp.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active));\n\t\t\tsection.label.draw(p, {\n\t\t\t\t.position = QPoint(labelLeft, _st.labelTop),\n\t\t\t\t.outerWidth = width(),\n\t\t\t\t.availableWidth = section.label.maxWidth(),\n\t\t\t\t.now = now,\n\t\t\t\t.pausedEmoji = _emojiPaused && _emojiPaused(),\n\t\t\t});\n\t\t\t{\n\t\t\t\tconst auto it = _unreadCounts.find(index);\n\t\t\t\tif (it != _unreadCounts.end()) {\n\t\t\t\t\tp.drawImage(\n\t\t\t\t\t\tlabelLeft\n\t\t\t\t\t\t\t+ _unreadSkip\n\t\t\t\t\t\t\t+ section.label.maxWidth(),\n\t\t\t\t\t\t_st.labelTop,\n\t\t\t\t\t\tit->second.cache);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (locked) {\n\t\t\t\tif (!_lockCache) {\n\t\t\t\t\t_lockCache.emplace(Ui::SideBarLockIcon(_st.labelFg));\n\t\t\t\t}\n\t\t\t\tconst auto size = _lockCache->size()\n\t\t\t\t\t/ style::DevicePixelRatio();\n\t\t\t\tp.drawImage(\n\t\t\t\t\tlabelLeft + (section.label.maxWidth() - size.width()) / 2,\n\t\t\t\t\theight() - size.height() - st::lineWidth,\n\t\t\t\t\t*_lockCache);\n\t\t\t\tp.setOpacity(1.0);\n\t\t\t}\n\t\t}\n\t\tindex++;\n\t\treturn true;\n\t};\n\tenumerateSections(drawSection);\n\tif (raisedIndex >= 0) {\n\t\tindex = raisedIndex;\n\t\tdrawSection(*_sections[raisedIndex].section);\n\t}\n\tif (_st.barSnapToLabel) {\n\t\tconst auto drawRect = [&](QRect rect, bool active) {\n\t\t\tconst auto &bar = active ? _barActive : _bar;\n\t\t\tif (bar) {\n\t\t\t\tbar->paint(p, rect);\n\t\t\t} else {\n\t\t\t\tp.fillRect(rect, active ? _st.barFgActive : _st.barFg);\n\t\t\t}\n\t\t};\n\t\tconst auto add = _st.barStroke / 2;\n\t\tconst auto from = std::max(range.left - add, 0);\n\t\tconst auto till = std::min(range.left + range.width + add, width());\n\t\tif (from < till) {\n\t\t\tdrawRect(\n\t\t\t\tmyrtlrect(\n\t\t\t\t\tfrom,\n\t\t\t\t\t_st.barTop,\n\t\t\t\t\ttill - from,\n\t\t\t\t\t_st.barStroke).translated(activeHorizontalShift, 0),\n\t\t\t\ttrue);\n\t\t}\n\t}\n}\n\nvoid ChatsFiltersTabs::mousePressEvent(QMouseEvent *e) {\n\tconst auto mouseButton = e->button();\n\tif (mouseButton == Qt::MouseButton::LeftButton) {\n\t\t_lockedPressed = (e->pos().x() >= _lockedFromX);\n\t\tif (_lockedPressed) {\n\t\t\tUi::RpWidget::mousePressEvent(e);\n\t\t} else {\n\t\t\tUi::SettingsSlider::mousePressEvent(e);\n\t\t}\n\t} else {\n\t\tUi::RpWidget::mousePressEvent(e);\n\t}\n}\n\nvoid ChatsFiltersTabs::mouseMoveEvent(QMouseEvent *e) {\n\tif (_reordering) {\n\t\tUi::RpWidget::mouseMoveEvent(e);\n\t} else {\n\t\tUi::SettingsSlider::mouseMoveEvent(e);\n\t}\n}\n\nvoid ChatsFiltersTabs::mouseReleaseEvent(QMouseEvent *e) {\n\tconst auto mouseButton = e->button();\n\tif (mouseButton == Qt::MouseButton::LeftButton) {\n\t\tif (base::take(_lockedPressed)) {\n\t\t\t_lockedPressed = false;\n\t\t\t_lockedClicked.fire({});\n\t\t} else {\n\t\t\tif (_reordering) {\n\t\t\t\tfor (const auto &section : _sections) {\n\t\t\t\t\tif (section.section->ripple) {\n\t\t\t\t\t\tsection.section->ripple->lastStop();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tUi::SettingsSlider::mouseReleaseEvent(e);\n\t\t\t}\n\t\t}\n\t} else {\n\t\tUi::RpWidget::mouseReleaseEvent(e);\n\t}\n}\n\nvoid ChatsFiltersTabs::contextMenuEvent(QContextMenuEvent *e) {\n\tconst auto pos = e->pos();\n\tif (pos.x() >= _lockedFromX) {\n\t\treturn;\n\t}\n\tauto left = 0;\n\tauto index = 0;\n\tenumerateSections([&](const Section &section) {\n\t\tconst auto currentRight = section.left + section.width;\n\t\tif (pos.x() > left && pos.x() < currentRight) {\n\t\t\treturn false;\n\t\t}\n\t\tleft = currentRight;\n\t\tindex++;\n\t\treturn true;\n\t});\n\t_contextMenuRequested.fire_copy(index);\n}\n\nrpl::producer<int> ChatsFiltersTabs::contextMenuRequested() const {\n\treturn _contextMenuRequested.events();\n}\n\nrpl::producer<> ChatsFiltersTabs::lockedClicked() const {\n\treturn _lockedClicked.events();\n}\n\nint ChatsFiltersTabs::count() const {\n\treturn _sections.size();\n}\n\nvoid ChatsFiltersTabs::setHorizontalShift(int index, int shift) {\n\tExpects(index >= 0 && index < _sections.size());\n\n\tauto &section = _sections[index];\n\tif (shift - section.horizontalShift) {\n\t\tsection.horizontalShift = shift;\n\t\tupdate();\n\t}\n}\n\nvoid ChatsFiltersTabs::setRaised(int index) {\n\t_sections[index].raise = true;\n\tupdate();\n}\n\nvoid ChatsFiltersTabs::reorderSections(int oldIndex, int newIndex) {\n\tExpects(oldIndex >= 0 && oldIndex < _sections.size());\n\tExpects(newIndex >= 0 && newIndex < _sections.size());\n\t// Expects(!_inResize);\n\tauto lefts = std::vector<int>();\n\tenumerateSections([&](Section &section) {\n\t\tlefts.emplace_back(section.left);\n\t\treturn true;\n\t});\n\tconst auto wasActive = activeSection();\n\n\t{\n\t\tauto unreadCounts = base::flat_map<Index, Unread>();\n\t\tfor (auto &[index, unread] : _unreadCounts) {\n\t\t\tunreadCounts.emplace(\n\t\t\t\tbase::reorder_index(index, oldIndex, newIndex),\n\t\t\t\tstd::move(unread));\n\t\t}\n\t\t_unreadCounts = std::move(unreadCounts);\n\t}\n\n\tbase::reorder(sectionsRef(), oldIndex, newIndex);\n\tUi::DiscreteSlider::setActiveSectionFast(\n\t\tbase::reorder_index(wasActive, oldIndex, newIndex));\n\tUi::DiscreteSlider::stopAnimation();\n\n\t{\n\t\t_sections.clear();\n\t\tauto left = 0;\n\t\tenumerateSections([&](Section &section) {\n\t\t\t_sections.push_back({ not_null{ &section }, 0, false });\n\t\t\tsection.left = left;\n\t\t\tleft += section.width;\n\t\t\treturn true;\n\t\t});\n\t}\n\tupdate();\n}\n\nnot_null<Ui::DiscreteSlider::Section*> ChatsFiltersTabs::widgetAt(\n\t\tint index) const {\n\tExpects(index >= 0 && index < count());\n\n\treturn _sections[index].section;\n}\n\nvoid ChatsFiltersTabs::setReordering(int value) {\n\t_reordering = value;\n}\n\nint ChatsFiltersTabs::reordering() const {\n\treturn _reordering;\n}\n\nvoid ChatsFiltersTabs::stopAnimation() {\n\tUi::DiscreteSlider::stopAnimation();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/unread_badge_paint.h\"\n#include \"ui/widgets/discrete_sliders.h\"\n\nnamespace style {\nstruct SettingsSlider;\n} // namespace style\n\nnamespace Ui {\n\nclass RpWidget;\nclass SettingsSlider;\n\nclass ChatsFiltersTabsReorder;\n\nclass ChatsFiltersTabs final : public Ui::SettingsSlider {\npublic:\n\tChatsFiltersTabs(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tconst style::SettingsSlider &st);\n\n\tbool setSectionsAndCheckChanged(\n\t\tstd::vector<TextWithEntities> &&sections,\n\t\tconst Text::MarkedContext &context,\n\t\tFn<bool()> paused);\n\n\tvoid fitWidthToSections() override;\n\tvoid setUnreadCount(int index, int unreadCount, bool muted);\n\tvoid setLockedFrom(int index);\n\n\t[[nodiscard]] rpl::producer<int> contextMenuRequested() const;\n\t[[nodiscard]] rpl::producer<> lockedClicked() const;\n\n\tvoid setHorizontalShift(int index, int shift);\n\tvoid setRaised(int index);\n\t[[nodiscard]] int count() const;\n\tvoid reorderSections(int oldIndex, int newIndex);\n\t[[nodiscard]] not_null<DiscreteSlider::Section*> widgetAt(int i) const;\n\tvoid setReordering(int value);\n\t[[nodiscard]] int reordering() const;\n\n\tvoid stopAnimation();\n\nprotected:\n\tstruct ShiftedSection {\n\t\tnot_null<Ui::DiscreteSlider::Section*> section;\n\t\tint horizontalShift = 0;\n\t\tbool raise = false;\n\t};\n\tfriend class ChatsFiltersTabsReorder;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid contextMenuEvent(QContextMenuEvent *e) override;\n\n\tstd::vector<ShiftedSection> _sections;\n\nprivate:\n\t[[nodiscard]] QImage cacheUnreadCount(int count, bool muted) const;\n\t[[nodiscard]] int calculateLockedFromX() const;\n\n\tusing Index = int;\n\tstruct Unread final {\n\t\tQImage cache;\n\t\tushort count = 0;\n\t\tbool muted = false;\n\t};\n\tbase::flat_map<Index, Unread> _unreadCounts;\n\tconst style::SettingsSlider &_st;\n\tconst UnreadBadgeStyle _unreadSt;\n\tconst QString _unreadMaxString;\n\tconst int _unreadSkip;\n\tstd::vector<int> _cachedBadgeWidths;\n\tint _cachedBadgeHeight = 0;\n\tint _lockedFrom = 0;\n\tint _lockedFromX = 0;\n\tbool _lockedPressed = false;\n\tstd::optional<Ui::RoundRect> _bar;\n\tstd::optional<Ui::RoundRect> _barActive;\n\tstd::optional<QImage> _lockCache;\n\tFn<bool()> _emojiPaused;\n\n\tint _reordering = 0;\n\n\trpl::lifetime _paletteLifetime;\n\trpl::event_stream<int> _contextMenuRequested;\n\trpl::event_stream<> _lockedClicked;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/chat_filters_tabs_slider_reorder.h\"\n\n#include \"ui/widgets/scroll_area.h\"\n#include \"styles/style_basic.h\"\n\n#include <QScrollBar>\n#include <QtGui/QtEvents>\n#include <QtWidgets/QApplication>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kScrollFactor = 0.05;\n\n} // namespace\n\nChatsFiltersTabsReorder::ChatsFiltersTabsReorder(\n\tnot_null<ChatsFiltersTabs*> layout,\n\tnot_null<ScrollArea*> scroll)\n: _layout(layout)\n, _scroll(scroll)\n, _scrollAnimation([this] { updateScrollCallback(); }) {\n}\n\nChatsFiltersTabsReorder::ChatsFiltersTabsReorder(\n\tnot_null<ChatsFiltersTabs*> layout)\n: _layout(layout) {\n}\n\nvoid ChatsFiltersTabsReorder::cancel() {\n\tif (_currentWidget) {\n\t\tcancelCurrent(indexOf(_currentWidget));\n\t}\n\t_lifetime.destroy();\n\tfor (auto i = 0, count = _layout->count(); i != count; ++i) {\n\t\t_layout->setHorizontalShift(i, 0);\n\t}\n\t_entries.clear();\n}\n\nvoid ChatsFiltersTabsReorder::start() {\n\tconst auto count = _layout->count();\n\tif (count < 2) {\n\t\treturn;\n\t}\n\t_layout->events()\n\t| rpl::on_next_done([this](not_null<QEvent*> e) {\n\t\tswitch (e->type()) {\n\t\tcase QEvent::MouseMove:\n\t\t\tmouseMove(static_cast<QMouseEvent*>(e.get())->globalPos());\n\t\t\tbreak;\n\t\tcase QEvent::MouseButtonPress: {\n\t\t\tconst auto m = static_cast<QMouseEvent*>(e.get());\n\t\t\tmousePress(m->button(), m->pos(), m->globalPos());\n\t\t\tbreak;\n\t\t}\n\t\tcase QEvent::MouseButtonRelease:\n\t\t\tmouseRelease(static_cast<QMouseEvent*>(e.get())->button());\n\t\t\tbreak;\n\t\t}\n\t}, [this] {\n\t\tcancel();\n\t}, _lifetime);\n\n\tfor (auto i = 0; i != count; ++i) {\n\t\tconst auto widget = _layout->widgetAt(i);\n\t\t_entries.push_back({ widget });\n\t}\n}\n\nvoid ChatsFiltersTabsReorder::addPinnedInterval(int from, int length) {\n\t_pinnedIntervals.push_back({ from, length });\n}\n\nvoid ChatsFiltersTabsReorder::clearPinnedIntervals() {\n\t_pinnedIntervals.clear();\n}\n\nbool ChatsFiltersTabsReorder::Interval::isIn(int index) const {\n\treturn (index >= from) && (index < (from + length));\n}\n\nbool ChatsFiltersTabsReorder::isIndexPinned(int index) const {\n\treturn ranges::any_of(_pinnedIntervals, [&](const Interval &i) {\n\t\treturn i.isIn(index);\n\t});\n}\n\nvoid ChatsFiltersTabsReorder::checkForStart(QPoint position) {\n\tconst auto shift = position.x() - _currentStart;\n\tconst auto delta = QApplication::startDragDistance();\n\tif (std::abs(shift) <= delta) {\n\t\treturn;\n\t}\n\t_currentState = State::Started;\n\t_currentStart += (shift > 0) ? delta : -delta;\n\n\tconst auto index = indexOf(_currentWidget);\n\t_layout->setRaised(index);\n\t_currentDesiredIndex = index;\n\t_updates.fire({ _currentWidget, index, index, _currentState });\n\n\tupdateOrder(index, position);\n}\n\nvoid ChatsFiltersTabsReorder::updateOrder(int index, QPoint position) {\n\tif (isIndexPinned(index)) {\n\t\treturn;\n\t}\n\tconst auto shift = position.x() - _currentStart;\n\tauto &current = _entries[index];\n\tcurrent.shiftAnimation.stop();\n\tcurrent.shift = current.finalShift = shift;\n\t_layout->setHorizontalShift(index, shift);\n\n\tcheckForScrollAnimation();\n\n\tconst auto count = _entries.size();\n\tconst auto currentWidth = current.widget->width;\n\tconst auto currentMiddle = current.widget->left\n\t\t+ shift\n\t\t+ currentWidth / 2;\n\t_currentDesiredIndex = index;\n\tif (shift > 0) {\n\t\tfor (auto next = index + 1; next != count; ++next) {\n\t\t\tif (isIndexPinned(next)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto &e = _entries[next];\n\t\t\tif (currentMiddle < e.widget->left + e.widget->width / 2) {\n\t\t\t\tmoveToShift(next, 0);\n\t\t\t} else {\n\t\t\t\t_currentDesiredIndex = next;\n\t\t\t\tmoveToShift(next, -currentWidth);\n\t\t\t}\n\t\t}\n\t\tfor (auto prev = index - 1; prev >= 0; --prev) {\n\t\t\tmoveToShift(prev, 0);\n\t\t}\n\t} else {\n\t\tfor (auto next = index + 1; next != count; ++next) {\n\t\t\tmoveToShift(next, 0);\n\t\t}\n\t\tfor (auto prev = index - 1; prev >= 0; --prev) {\n\t\t\tif (isIndexPinned(prev)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto &e = _entries[prev];\n\t\t\tif (currentMiddle >= e.widget->left + e.widget->width / 2) {\n\t\t\t\tmoveToShift(prev, 0);\n\t\t\t} else {\n\t\t\t\t_currentDesiredIndex = prev;\n\t\t\t\tmoveToShift(prev, currentWidth);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ChatsFiltersTabsReorder::mousePress(\n\t\tQt::MouseButton button,\n\t\tQPoint position,\n\t\tQPoint globalPosition) {\n\tif (button != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tauto widget = (ChatsFiltersTabs::ShiftedSection*)(nullptr);\n\tfor (auto i = 0; i != _layout->_sections.size(); ++i) {\n\t\tauto &section = _layout->_sections[i];\n\t\tif ((position.x() >= section.section->left)\n\t\t\t&& (position.x() < (section.section->left + section.section->width))) {\n\t\t\twidget = &section;\n\t\t\tbreak;\n\t\t}\n\t}\n\tcancelCurrent();\n\tif (!widget) {\n\t\treturn;\n\t}\n\t_currentWidget = widget->section;\n\t_currentShiftedWidget = widget;\n\t_currentStart = globalPosition.x();\n}\n\nvoid ChatsFiltersTabsReorder::mouseMove(QPoint position) {\n\tif (!_currentWidget) {\n\t// if (_currentWidget != widget) {\n\t\treturn;\n\t} else if (_currentState != State::Started) {\n\t\tcheckForStart(position);\n\t} else {\n\t\tupdateOrder(indexOf(_currentWidget), position);\n\t}\n}\n\nvoid ChatsFiltersTabsReorder::mouseRelease(Qt::MouseButton button) {\n\tif (button != Qt::LeftButton) {\n\t\treturn;\n\t}\n\tfinishReordering();\n}\n\nvoid ChatsFiltersTabsReorder::cancelCurrent() {\n\tif (_currentWidget) {\n\t\tcancelCurrent(indexOf(_currentWidget));\n\t}\n}\n\nvoid ChatsFiltersTabsReorder::cancelCurrent(int index) {\n\tExpects(_currentWidget != nullptr);\n\n\tif (_currentState == State::Started) {\n\t\t_currentState = State::Cancelled;\n\t\t_updates.fire({ _currentWidget, index, index, _currentState });\n\t}\n\t_currentWidget = nullptr;\n\t_currentShiftedWidget = nullptr;\n\tfor (auto i = 0, count = int(_entries.size()); i != count; ++i) {\n\t\tmoveToShift(i, 0);\n\t}\n}\n\nvoid ChatsFiltersTabsReorder::finishReordering() {\n\tif (_scroll) {\n\t\t_scrollAnimation.stop();\n\t}\n\tfinishCurrent();\n}\n\nvoid ChatsFiltersTabsReorder::finishCurrent() {\n\tif (!_currentWidget) {\n\t\treturn;\n\t}\n\tconst auto index = indexOf(_currentWidget);\n\tif (_currentDesiredIndex == index || _currentState != State::Started) {\n\t\tcancelCurrent(index);\n\t\treturn;\n\t}\n\tconst auto result = _currentDesiredIndex;\n\tconst auto widget = _currentWidget;\n\t_currentState = State::Cancelled;\n\t_currentWidget = nullptr;\n\t_currentShiftedWidget = nullptr;\n\n\tauto &current = _entries[index];\n\tconst auto width = current.widget->width;\n\tif (index < result) {\n\t\tauto sum = 0;\n\t\tfor (auto i = index; i != result; ++i) {\n\t\t\tauto &entry = _entries[i + 1];\n\t\t\tconst auto widget = entry.widget;\n\t\t\tentry.deltaShift += width;\n\t\t\tupdateShift(widget, i + 1);\n\t\t\tsum += widget->width;\n\t\t}\n\t\tcurrent.finalShift -= sum;\n\t} else if (index > result) {\n\t\tauto sum = 0;\n\t\tfor (auto i = result; i != index; ++i) {\n\t\t\tauto &entry = _entries[i];\n\t\t\tconst auto widget = entry.widget;\n\t\t\tentry.deltaShift -= width;\n\t\t\tupdateShift(widget, i);\n\t\t\tsum += widget->width;\n\t\t}\n\t\tcurrent.finalShift += sum;\n\t}\n\tif (!(current.finalShift + current.deltaShift)) {\n\t\tcurrent.shift = 0;\n\t\t_layout->setHorizontalShift(index, 0);\n\t}\n\tbase::reorder(_entries, index, result);\n\t_layout->reorderSections(index, _currentDesiredIndex);\n\tfor (auto i = 0; i != _layout->sectionsRef().size(); ++i) {\n\t\t_entries[i].widget = &_layout->sectionsRef()[i];\n\t\tmoveToShift(i, 0);\n\t}\n\n\t_updates.fire({ widget, index, result, State::Applied });\n}\n\nvoid ChatsFiltersTabsReorder::moveToShift(int index, int shift) {\n\tauto &entry = _entries[index];\n\tif (entry.finalShift + entry.deltaShift == shift) {\n\t\treturn;\n\t}\n\tconst auto widget = entry.widget;\n\tentry.shiftAnimation.start(\n\t\t[=, this] { updateShift(widget, index); },\n\t\tentry.finalShift,\n\t\tshift - entry.deltaShift,\n\t\tst::slideWrapDuration);\n\tentry.finalShift = shift - entry.deltaShift;\n}\n\nvoid ChatsFiltersTabsReorder::updateShift(\n\t\tnot_null<Section*> widget,\n\t\tint indexHint) {\n\tExpects(indexHint >= 0 && indexHint < _entries.size());\n\n\tconst auto index = (_entries[indexHint].widget == widget)\n\t\t? indexHint\n\t\t: indexOf(widget);\n\tauto &entry = _entries[index];\n\tentry.shift = base::SafeRound(\n\t\tentry.shiftAnimation.value(entry.finalShift)\n\t) + entry.deltaShift;\n\tif (entry.deltaShift && !entry.shiftAnimation.animating()) {\n\t\tentry.finalShift += entry.deltaShift;\n\t\tentry.deltaShift = 0;\n\t}\n\t_layout->setHorizontalShift(index, entry.shift);\n}\n\nint ChatsFiltersTabsReorder::indexOf(not_null<Section*> widget) const {\n\tconst auto i = ranges::find(_entries, widget, &Entry::widget);\n\tAssert(i != end(_entries));\n\treturn i - begin(_entries);\n}\n\nauto ChatsFiltersTabsReorder::updates() const -> rpl::producer<Single> {\n\treturn _updates.events();\n}\n\nvoid ChatsFiltersTabsReorder::updateScrollCallback() {\n\tif (!_scroll) {\n\t\treturn;\n\t}\n\tconst auto delta = deltaFromEdge();\n\tconst auto oldLeft = _scroll->scrollLeft();\n\t_scroll->horizontalScrollBar()->setValue(oldLeft + delta);\n\tconst auto newLeft = _scroll->scrollLeft();\n\n\t_currentStart += oldLeft - newLeft;\n\tif (newLeft == 0 || newLeft == _scroll->scrollLeftMax()) {\n\t\t_scrollAnimation.stop();\n\t}\n}\n\nvoid ChatsFiltersTabsReorder::checkForScrollAnimation() {\n\tif (!_scroll || !deltaFromEdge() || _scrollAnimation.animating()) {\n\t\treturn;\n\t}\n\t_scrollAnimation.start();\n}\n\nint ChatsFiltersTabsReorder::deltaFromEdge() {\n\tExpects(_currentWidget != nullptr);\n\tExpects(_currentShiftedWidget != nullptr);\n\tExpects(_scroll);\n\n\tconst auto globalPosition = _layout->mapToGlobal(\n\t\tQPoint(\n\t\t\t_currentWidget->left + _currentShiftedWidget->horizontalShift,\n\t\t\t0));\n\tconst auto localLeft = _scroll->mapFromGlobal(globalPosition).x();\n\tconst auto localRight = localLeft\n\t\t+ _currentWidget->width\n\t\t- _scroll->width();\n\n\tconst auto isLeftEdge = (localLeft < 0);\n\tconst auto isRightEdge = (localRight > 0);\n\tif (!isLeftEdge && !isRightEdge) {\n\t\t_scrollAnimation.stop();\n\t\treturn 0;\n\t}\n\treturn int((isRightEdge ? localRight : localLeft) * kScrollFactor);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n#include \"ui/widgets/chat_filters_tabs_slider.h\"\n\nnamespace Ui {\n\nclass ScrollArea;\n\nclass ChatsFiltersTabsReorder final {\npublic:\n\tusing Section = ChatsFiltersTabs::Section;\n\tenum class State : uchar {\n\t\tStarted,\n\t\tApplied,\n\t\tCancelled,\n\t};\n\n\tstruct Single {\n\t\tnot_null<Section*> widget;\n\t\tint oldPosition = 0;\n\t\tint newPosition = 0;\n\t\tState state = State::Started;\n\t};\n\n\tChatsFiltersTabsReorder(\n\t\tnot_null<ChatsFiltersTabs*> layout,\n\t\tnot_null<ScrollArea*> scroll);\n\tChatsFiltersTabsReorder(not_null<ChatsFiltersTabs*> layout);\n\n\tvoid start();\n\tvoid cancel();\n\tvoid finishReordering();\n\tvoid addPinnedInterval(int from, int length);\n\tvoid clearPinnedIntervals();\n\t[[nodiscard]] rpl::producer<Single> updates() const;\n\nprivate:\n\tstruct Entry {\n\t\tnot_null<Section*> widget;\n\t\tUi::Animations::Simple shiftAnimation;\n\t\tint shift = 0;\n\t\tint finalShift = 0;\n\t\tint deltaShift = 0;\n\t};\n\tstruct Interval {\n\t\t[[nodiscard]] bool isIn(int index) const;\n\n\t\tint from = 0;\n\t\tint length = 0;\n\t};\n\n\tvoid mousePress(Qt::MouseButton button, QPoint position, QPoint global);\n\tvoid mouseMove(QPoint position);\n\tvoid mouseRelease(Qt::MouseButton button);\n\n\tvoid checkForStart(QPoint position);\n\tvoid updateOrder(int index, QPoint position);\n\tvoid cancelCurrent();\n\tvoid finishCurrent();\n\tvoid cancelCurrent(int index);\n\n\t[[nodiscard]] int indexOf(not_null<Section*> widget) const;\n\tvoid moveToShift(int index, int shift);\n\tvoid updateShift(not_null<Section*> widget, int indexHint);\n\n\tvoid updateScrollCallback();\n\tvoid checkForScrollAnimation();\n\t[[nodiscard]] int deltaFromEdge();\n\n\t[[nodiscard]] bool isIndexPinned(int index) const;\n\n\tconst not_null<ChatsFiltersTabs*> _layout;\n\tUi::ScrollArea *_scroll = nullptr;\n\n\tUi::Animations::Basic _scrollAnimation;\n\n\tstd::vector<Interval> _pinnedIntervals;\n\n\tSection *_currentWidget = nullptr;\n\tChatsFiltersTabs::ShiftedSection *_currentShiftedWidget = nullptr;\n\tint _currentStart = 0;\n\tint _currentDesiredIndex = 0;\n\tState _currentState = State::Cancelled;\n\tstd::vector<Entry> _entries;\n\trpl::event_stream<Single> _updates;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/chat_filters_tabs_strip.h\"\n\n#include \"api/api_chat_filters_remove_manager.h\"\n#include \"boxes/choose_filter_box.h\"\n#include \"boxes/filters/edit_filter_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"core/application.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_peer_values.h\" // Data::AmPremiumValue.\n#include \"data/data_premium_limits.h\"\n#include \"data/data_session.h\"\n#include \"data/data_unread_value.h\"\n#include \"data/data_user.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"settings/sections/settings_folders.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/chat_filters_tabs_slider_reorder.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_dialogs.h\" // dialogsSearchTabs\n#include \"styles/style_media_player.h\" // mediaPlayerMenuCheck\n#include \"styles/style_menu_icons.h\"\n\n#include <QScrollBar>\n\nnamespace Ui {\nnamespace {\n\nstruct State final {\n\tUi::Animations::Simple animation;\n\tstd::optional<FilterId> lastFilterId = std::nullopt;\n\trpl::lifetime rebuildLifetime;\n\trpl::lifetime reorderLifetime;\n\tbase::unique_qptr<Ui::PopupMenu> menu;\n\n\tApi::RemoveComplexChatFilter removeApi;\n\tbool waitingSuggested = false;\n\n\tstd::unique_ptr<Ui::ChatsFiltersTabsReorder> reorder;\n\tbool ignoreRefresh = false;\n};\n\nvoid ShowMenu(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<State*> state,\n\t\tint index) {\n\tconst auto session = &controller->session();\n\n\tauto id = FilterId(0);\n\t{\n\t\tconst auto &list = session->data().chatsFilters().list();\n\t\tif (index < 0 || index >= list.size()) {\n\t\t\treturn;\n\t\t}\n\t\tid = list[index].id();\n\t}\n\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\tconst auto addAction = Ui::Menu::CreateAddActionCallback(\n\t\tstate->menu.get());\n\n\tif (id) {\n\t\taddAction(\n\t\t\ttr::lng_filters_context_edit(tr::now),\n\t\t\t[=] { EditExistingFilter(controller, id); },\n\t\t\t&st::menuIconEdit);\n\n\t\tWindow::MenuAddMarkAsReadChatListAction(\n\t\t\tcontroller,\n\t\t\t[=] { return session->data().chatsFilters().chatsList(id); },\n\t\t\taddAction);\n\n\t\tauto showRemoveBox = [=] {\n\t\t\tstate->removeApi.request(base::make_weak(parent), controller, id);\n\t\t};\n\t\taddAction({\n\t\t\t.text = tr::lng_filters_context_remove(tr::now),\n\t\t\t.handler = std::move(showRemoveBox),\n\t\t\t.icon = &st::menuIconDeleteAttention,\n\t\t\t.isAttention = true,\n\t\t});\n\t} else {\n\t\tauto customUnreadState = [=] {\n\t\t\treturn Data::MainListMapUnreadState(\n\t\t\t\tsession,\n\t\t\t\tsession->data().chatsList()->unreadState());\n\t\t};\n\t\tWindow::MenuAddMarkAsReadChatListAction(\n\t\t\tcontroller,\n\t\t\t[=] { return session->data().chatsList(); },\n\t\t\taddAction,\n\t\t\tstd::move(customUnreadState));\n\n\t\tauto openFiltersSettings = [=] {\n\t\t\tconst auto filters = &session->data().chatsFilters();\n\t\t\tif (filters->suggestedLoaded()) {\n\t\t\t\tcontroller->showSettings(Settings::FoldersId());\n\t\t\t} else if (!state->waitingSuggested) {\n\t\t\t\tstate->waitingSuggested = true;\n\t\t\t\tfilters->requestSuggested();\n\t\t\t\tfilters->suggestedUpdated(\n\t\t\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t\t\tcontroller->showSettings(Settings::FoldersId());\n\t\t\t\t}, parent->lifetime());\n\t\t\t}\n\t\t};\n\t\taddAction(\n\t\t\ttr::lng_filters_setup_menu(tr::now),\n\t\t\tstd::move(openFiltersSettings),\n\t\t\t&st::menuIconEdit);\n\t}\n\tif (state->menu->empty()) {\n\t\tstate->menu = nullptr;\n\t\treturn;\n\t}\n\tstate->menu->popup(QCursor::pos());\n}\n\nvoid ShowFiltersListMenu(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<State*> state,\n\t\tint active,\n\t\tFn<void(int)> changeActive) {\n\tconst auto &list = session->data().chatsFilters().list();\n\n\tstate->menu = base::make_unique_q<Ui::PopupMenu>(\n\t\tparent,\n\t\tst::popupMenuWithIcons);\n\n\tconst auto reorderAll = session->user()->isPremium();\n\tconst auto maxLimit = (reorderAll ? 1 : 0)\n\t\t+ Data::PremiumLimits(session).dialogFiltersCurrent();\n\tconst auto premiumFrom = (reorderAll ? 0 : 1) + maxLimit;\n\n\tfor (auto i = 0; i < list.size(); ++i) {\n\t\tconst auto title = list[i].title();\n\t\tconst auto text = title.text.empty()\n\t\t\t? tr::lng_filters_all_short(tr::now)\n\t\t\t: title.text.text;\n\t\tconst auto callback = [=] {\n\t\t\tif (i != active) {\n\t\t\t\tchangeActive(i);\n\t\t\t}\n\t\t};\n\t\tconst auto icon = (i == active)\n\t\t\t? &st::mediaPlayerMenuCheck\n\t\t\t: nullptr;\n\t\tconst auto action = Ui::Menu::CreateAction(\n\t\t\tstate->menu->menu(),\n\t\t\ttext,\n\t\t\tcallback);\n\t\tauto item = base::make_unique_q<Ui::Menu::Action>(\n\t\t\tstate->menu->menu(),\n\t\t\tstate->menu->st().menu,\n\t\t\taction,\n\t\t\ticon,\n\t\t\ticon);\n\t\taction->setEnabled(i < premiumFrom);\n\t\tif (!title.text.empty()) {\n\t\t\tconst auto context = Core::TextContext({\n\t\t\t\t.session = session,\n\t\t\t\t.repaint = [raw = item.get()] { raw->update(); },\n\t\t\t\t.customEmojiLoopLimit = title.isStatic ? -1 : 0,\n\t\t\t});\n\t\t\titem->setMarkedText(title.text, QString(), context);\n\t\t}\n\t\tstate->menu->addAction(std::move(item));\n\t}\n\tsession->data().chatsFilters().changed() | rpl::on_next([=] {\n\t\tstate->menu->hideMenu();\n\t}, state->menu->lifetime());\n\n\tif (state->menu->empty()) {\n\t\tstate->menu = nullptr;\n\t\treturn;\n\t}\n\tstate->menu->popup(QCursor::pos());\n}\n\n} // namespace\n\nnot_null<Ui::RpWidget*> AddChatFiltersTabsStrip(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Main::Session*> session,\n\t\tFn<void(FilterId)> choose,\n\t\tChatHelpers::PauseReason pauseLevel,\n\t\tWindow::SessionController *controller,\n\t\tbool trackActiveFilterAndUnreadAndReorder) {\n\n\tconst auto wrap = Ui::CreateChild<Ui::SlideWrap<Ui::RpWidget>>(\n\t\tparent,\n\t\tobject_ptr<Ui::RpWidget>(parent));\n\tif (!controller) {\n\t\tconst auto window = Core::App().findWindow(parent);\n\t\tcontroller = window ? window->sessionController() : nullptr;\n\t\tif (!controller) {\n\t\t\treturn wrap;\n\t\t}\n\t}\n\tconst auto container = wrap->entity();\n\tconst auto scroll = Ui::CreateChild<Ui::ScrollArea>(\n\t\tcontainer,\n\t\tst::dialogsTabsScroll,\n\t\ttrue);\n\tconst auto slider = scroll->setOwnedWidget(\n\t\tobject_ptr<Ui::ChatsFiltersTabs>(\n\t\t\tparent,\n\t\t\ttrackActiveFilterAndUnreadAndReorder\n\t\t\t\t? st::dialogsSearchTabs\n\t\t\t\t: st::chatsFiltersTabs));\n\tconst auto state = wrap->lifetime().make_state<State>();\n\tconst auto reassignUnreadValue = [=] {\n\t\tstate->reorderLifetime.destroy();\n\t\tconst auto &list = session->data().chatsFilters().list();\n\t\tauto includeMuted = Data::IncludeMutedCounterFoldersValue();\n\t\tfor (auto i = 0; i < list.size(); i++) {\n\t\t\trpl::combine(\n\t\t\t\tData::UnreadStateValue(session, list[i].id()),\n\t\t\t\trpl::duplicate(includeMuted)\n\t\t\t) | rpl::on_next([=](\n\t\t\t\t\tconst Dialogs::UnreadState &state,\n\t\t\t\t\tbool includeMuted) {\n\t\t\t\tconst auto chats = state.chats;\n\t\t\t\tconst auto chatsMuted = state.chatsMuted;\n\t\t\t\tconst auto muted = (chatsMuted + state.marksMuted);\n\t\t\t\tconst auto count = (chats + state.marks)\n\t\t\t\t\t- (includeMuted ? 0 : muted);\n\t\t\t\tconst auto isMuted = includeMuted && (count == muted);\n\t\t\t\tslider->setUnreadCount(i, count, isMuted);\n\t\t\t\tslider->fitWidthToSections();\n\t\t\t}, state->reorderLifetime);\n\t\t}\n\t};\n\tif (trackActiveFilterAndUnreadAndReorder) {\n\t\tusing Reorder = Ui::ChatsFiltersTabsReorder;\n\t\tstate->reorder = std::make_unique<Reorder>(slider, scroll);\n\t\tconst auto applyReorder = [=](\n\t\t\t\tint oldPosition,\n\t\t\t\tint newPosition) {\n\t\t\tif (newPosition == oldPosition) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst auto filters = &session->data().chatsFilters();\n\t\t\tconst auto &list = filters->list();\n\t\t\tif (!session->user()->isPremium()) {\n\t\t\t\tif (list[0].id() != FilterId()) {\n\t\t\t\t\tfilters->moveAllToFront();\n\t\t\t\t}\n\t\t\t}\n\t\t\tAssert(oldPosition >= 0 && oldPosition < list.size());\n\t\t\tAssert(newPosition >= 0 && newPosition < list.size());\n\n\t\t\tauto order = ranges::views::all(\n\t\t\t\tlist\n\t\t\t) | ranges::views::transform(\n\t\t\t\t&Data::ChatFilter::id\n\t\t\t) | ranges::to_vector;\n\t\t\tbase::reorder(order, oldPosition, newPosition);\n\n\t\t\tstate->ignoreRefresh = true;\n\t\t\tfilters->saveOrder(order);\n\t\t\tstate->ignoreRefresh = false;\n\t\t};\n\n\t\tstate->reorder->updates(\n\t\t) | rpl::on_next([=](const Reorder::Single &data) {\n\t\t\tif (data.state == Reorder::State::Started) {\n\t\t\t\tslider->setReordering(slider->reordering() + 1);\n\t\t\t} else {\n\t\t\t\tUi::PostponeCall(slider, [=] {\n\t\t\t\t\tslider->setReordering(slider->reordering() - 1);\n\t\t\t\t});\n\t\t\t\tif (data.state == Reorder::State::Applied) {\n\t\t\t\t\tapplyReorder(data.oldPosition, data.newPosition);\n\t\t\t\t\treassignUnreadValue();\n\t\t\t\t}\n\t\t\t}\n\t\t}, slider->lifetime());\n\n\t\tSetupFilterDragAndDrop(\n\t\t\tslider,\n\t\t\tsession,\n\t\t\t[=](QPoint pos) -> std::optional<FilterId> {\n\t\t\t\tconst auto local = slider->mapFromGlobal(pos);\n\t\t\t\tconst auto x = local.x();\n\t\t\t\tconst auto count = slider->sectionsCount();\n\t\t\t\tfor (auto i = 0; i < count; ++i) {\n\t\t\t\t\tconst auto left = slider->lookupSectionLeft(i);\n\t\t\t\t\tconst auto right = (i + 1 < count)\n\t\t\t\t\t\t? slider->lookupSectionLeft(i + 1)\n\t\t\t\t\t\t: slider->width();\n\t\t\t\t\tif (x >= left && x < right) {\n\t\t\t\t\t\tconst auto &list\n\t\t\t\t\t\t\t= session->data().chatsFilters().list();\n\t\t\t\t\t\treturn (i < list.size())\n\t\t\t\t\t\t\t? list[i].id()\n\t\t\t\t\t\t\t: FilterId();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn std::nullopt;\n\t\t\t},\n\t\t\t[=] { return state->lastFilterId.value_or(FilterId()); },\n\t\t\t[=](FilterId id) {\n\t\t\t\tconst auto &list = session->data().chatsFilters().list();\n\t\t\t\tfor (auto i = 0; i < list.size(); i++) {\n\t\t\t\t\tif (list[i].id() == id) {\n\t\t\t\t\t\tslider->selectSection(i);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tslider->selectSection(-1);\n\t\t\t});\n\t}\n\twrap->toggle(false, anim::type::instant);\n\tscroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) {\n\t\tconst auto pixelDelta = e->pixelDelta();\n\t\tconst auto angleDelta = e->angleDelta();\n\t\tif (std::abs(pixelDelta.x()) + std::abs(angleDelta.x())) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto bar = scroll->horizontalScrollBar();\n\t\tconst auto y = pixelDelta.y() ? pixelDelta.y() : angleDelta.y();\n\t\tbar->setValue(bar->value() - y);\n\t\treturn true;\n\t});\n\n\tconst auto scrollToIndex = [=](int index, anim::type type) {\n\t\tconst auto to = index\n\t\t\t? (slider->centerOfSection(index) - scroll->width() / 2)\n\t\t\t: 0;\n\t\tconst auto bar = scroll->horizontalScrollBar();\n\t\tstate->animation.stop();\n\t\tif (type == anim::type::instant) {\n\t\t\tbar->setValue(to);\n\t\t} else {\n\t\t\tstate->animation.start(\n\t\t\t\t[=](float64 v) { bar->setValue(v); },\n\t\t\t\tbar->value(),\n\t\t\t\tstd::min(to, bar->maximum()),\n\t\t\t\tst::defaultTabsSlider.duration);\n\t\t}\n\t};\n\n\tconst auto applyFilter = [=](const Data::ChatFilter &filter) {\n\t\tif (slider->reordering()) {\n\t\t\treturn;\n\t\t}\n\t\tchoose(filter.id());\n\t};\n\n\tconst auto filterByIndex = [=](int index) -> const Data::ChatFilter& {\n\t\tconst auto &list = session->data().chatsFilters().list();\n\t\tAssert(index >= 0 && index < list.size());\n\t\treturn list[index];\n\t};\n\n\tconst auto rebuild = [=] {\n\t\tconst auto &list = session->data().chatsFilters().list();\n\t\tif ((list.size() <= 1 && !slider->width()) || state->ignoreRefresh) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto context = Core::TextContext({ .session = session });\n\t\tconst auto paused = [=] {\n\t\t\treturn On(PowerSaving::kEmojiChat)\n\t\t\t\t|| controller->isGifPausedAtLeastFor(pauseLevel);\n\t\t};\n\t\tconst auto sectionsChanged = slider->setSectionsAndCheckChanged(\n\t\t\tranges::views::all(\n\t\t\t\tlist\n\t\t\t) | ranges::views::transform([](const Data::ChatFilter &filter) {\n\t\t\t\tauto title = filter.title();\n\t\t\t\treturn title.text.empty()\n\t\t\t\t\t? TextWithEntities{ tr::lng_filters_all_short(tr::now) }\n\t\t\t\t\t: title.isStatic\n\t\t\t\t\t? Data::ForceCustomEmojiStatic(title.text)\n\t\t\t\t\t: title.text;\n\t\t\t}) | ranges::to_vector, context, paused);\n\t\tif (!sectionsChanged) {\n\t\t\treturn;\n\t\t}\n\t\tstate->rebuildLifetime.destroy();\n\t\tslider->fitWidthToSections();\n\t\t{\n\t\t\tconst auto reorderAll = session->user()->isPremium();\n\t\t\tconst auto maxLimit = (reorderAll ? 1 : 0)\n\t\t\t\t+ Data::PremiumLimits(session).dialogFiltersCurrent();\n\t\t\tconst auto premiumFrom = (reorderAll ? 0 : 1) + maxLimit;\n\t\t\tslider->setLockedFrom((premiumFrom >= list.size())\n\t\t\t\t? 0\n\t\t\t\t: premiumFrom);\n\t\t\tslider->lockedClicked() | rpl::on_next([=] {\n\t\t\t\tcontroller->show(Box(FiltersLimitBox, session, std::nullopt));\n\t\t\t}, state->rebuildLifetime);\n\t\t\tif (state->reorder) {\n\t\t\t\tstate->reorder->cancel();\n\t\t\t\tstate->reorder->clearPinnedIntervals();\n\t\t\t\tif (!reorderAll) {\n\t\t\t\t\tstate->reorder->addPinnedInterval(0, 1);\n\t\t\t\t}\n\t\t\t\tstate->reorder->addPinnedInterval(\n\t\t\t\t\tpremiumFrom,\n\t\t\t\t\tstd::max(1, int(list.size()) - maxLimit));\n\t\t\t}\n\t\t}\n\t\tif (trackActiveFilterAndUnreadAndReorder) {\n\t\t\treassignUnreadValue();\n\t\t}\n\t\t[&] {\n\t\t\tconst auto lookingId = state->lastFilterId.value_or(list[0].id());\n\t\t\tfor (auto i = 0; i < list.size(); i++) {\n\t\t\t\tconst auto &filter = list[i];\n\t\t\t\tif (filter.id() == lookingId) {\n\t\t\t\t\tconst auto wasLast = !!state->lastFilterId;\n\t\t\t\t\tstate->lastFilterId = filter.id();\n\t\t\t\t\tslider->setActiveSectionFast(i);\n\t\t\t\t\tscrollToIndex(\n\t\t\t\t\t\ti,\n\t\t\t\t\t\twasLast ? anim::type::normal : anim::type::instant);\n\t\t\t\t\tapplyFilter(filter);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (list.size()) {\n\t\t\t\tconst auto index = 0;\n\t\t\t\tconst auto &filter = filterByIndex(index);\n\t\t\t\tstate->lastFilterId = filter.id();\n\t\t\t\tslider->setActiveSectionFast(index);\n\t\t\t\tscrollToIndex(index, anim::type::instant);\n\t\t\t\tapplyFilter(filter);\n\t\t\t}\n\t\t}();\n\t\tif (trackActiveFilterAndUnreadAndReorder) {\n\t\t\tcontroller->activeChatsFilter(\n\t\t\t) | rpl::on_next([=](FilterId id) {\n\t\t\t\tconst auto &list = session->data().chatsFilters().list();\n\t\t\t\tfor (auto i = 0; i < list.size(); ++i) {\n\t\t\t\t\tif (list[i].id() == id) {\n\t\t\t\t\t\tslider->setActiveSection(i);\n\t\t\t\t\t\tscrollToIndex(i, anim::type::normal);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstate->reorder->finishReordering();\n\t\t\t}, state->rebuildLifetime);\n\t\t}\n\t\trpl::single(-1) | rpl::then(\n\t\t\tslider->sectionActivated()\n\t\t) | rpl::combine_previous(\n\t\t) | rpl::on_next([=](int was, int index) {\n\t\t\tif (slider->reordering()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto &filter = filterByIndex(index);\n\t\t\tif (was != index) {\n\t\t\t\tstate->lastFilterId = filter.id();\n\t\t\t\tscrollToIndex(index, anim::type::normal);\n\t\t\t}\n\t\t\tapplyFilter(filter);\n\t\t}, state->rebuildLifetime);\n\t\tslider->contextMenuRequested() | rpl::on_next([=](int index) {\n\t\t\tif (trackActiveFilterAndUnreadAndReorder) {\n\t\t\t\tShowMenu(wrap, controller, state, index);\n\t\t\t} else {\n\t\t\t\tShowFiltersListMenu(\n\t\t\t\t\twrap,\n\t\t\t\t\tsession,\n\t\t\t\t\tstate,\n\t\t\t\t\tslider->activeSection(),\n\t\t\t\t\t[=](int i) { slider->setActiveSection(i); });\n\t\t\t}\n\t\t}, state->rebuildLifetime);\n\t\twrap->toggle((list.size() > 1), anim::type::instant);\n\n\t\tif (state->reorder) {\n\t\t\tstate->reorder->start();\n\t\t}\n\t};\n\trpl::combine(\n\t\tsession->data().chatsFilters().changed(),\n\t\tData::AmPremiumValue(session) | rpl::to_empty\n\t) | rpl::on_next(rebuild, wrap->lifetime());\n\trebuild();\n\n\tsession->data().chatsFilters().isChatlistChanged(\n\t) | rpl::on_next([=](FilterId id) {\n\t\tif (!id || !state->lastFilterId || (id != state->lastFilterId)) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const auto &filter : session->data().chatsFilters().list()) {\n\t\t\tif (filter.id() == id) {\n\t\t\t\tapplyFilter(filter);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}, wrap->lifetime());\n\n\trpl::combine(\n\t\tparent->widthValue() | rpl::filter(rpl::mappers::_1 > 0),\n\t\tslider->heightValue() | rpl::filter(rpl::mappers::_1 > 0)\n\t) | rpl::on_next([=](int w, int h) {\n\t\tscroll->resize(w, h);\n\t\tcontainer->resize(w, h);\n\t\twrap->resize(w, h);\n\t}, wrap->lifetime());\n\n\treturn wrap;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace ChatHelpers {\nenum class PauseReason;\n} // namespace ChatHelpers\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Window {\nclass SessionController;\n} // namespace Window\n\nnamespace Ui {\n\nnot_null<Ui::RpWidget*> AddChatFiltersTabsStrip(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Main::Session*> session,\n\tFn<void(FilterId)> choose,\n\tChatHelpers::PauseReason pauseLevel,\n\tWindow::SessionController *controller = nullptr,\n\tbool trackActiveFilterAndUnreadAndReorder = false);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/color_editor.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/color_editor.h\"\n\n#include \"base/platform/base_platform_info.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/widgets/fields/masked_input_field.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_media_view.h\"\n\nclass ColorEditor::Picker : public Ui::RpWidget {\npublic:\n\tPicker(QWidget *parent, Mode mode, QColor color);\n\n\tfloat64 valueX() const {\n\t\treturn _x;\n\t}\n\tfloat64 valueY() const {\n\t\treturn _y;\n\t}\n\n\trpl::producer<> changed() const {\n\t\treturn _changed.events();\n\t}\n\tvoid setHSB(HSB hsb);\n\tvoid setRGB(int red, int green, int blue);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e);\n\n\tvoid mousePressEvent(QMouseEvent *e);\n\tvoid mouseMoveEvent(QMouseEvent *e);\n\tvoid mouseReleaseEvent(QMouseEvent *e);\n\nprivate:\n\tvoid setFromColor(QColor color);\n\tQCursor generateCursor();\n\n\tvoid preparePalette();\n\tvoid preparePaletteRGBA();\n\tvoid preparePaletteHSL();\n\tvoid updateCurrentPoint(QPoint localPosition);\n\n\tMode _mode;\n\tQColor _topleft;\n\tQColor _topright;\n\tQColor _bottomleft;\n\tQColor _bottomright;\n\n\tQImage _palette;\n\tbool _paletteInvalidated = false;\n\tfloat64 _x = 0.;\n\tfloat64 _y = 0.;\n\n\tbool _choosing = false;\n\trpl::event_stream<> _changed;\n\n};\n\nQCursor ColorEditor::Picker::generateCursor() {\n\tconst auto diameter = style::ConvertScale(16);\n\tconst auto line = style::ConvertScale(1);\n\tconst auto size = ((diameter + 2 * line) >= 32) ? 64 : 32;\n\tconst auto diff = (size - diameter) / 2;\n\tauto cursor = QImage(\n\t\tQSize(size, size) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tcursor.setDevicePixelRatio(style::DevicePixelRatio());\n\tcursor.fill(Qt::transparent);\n\t{\n\t\tauto p = QPainter(&cursor);\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tp.setBrush(Qt::NoBrush);\n\t\tauto pen = QPen(Qt::white);\n\t\tpen.setWidth(3 * line);\n\t\tp.setPen(pen);\n\t\tp.drawEllipse(diff, diff, diameter, diameter);\n\t\tpen = QPen(Qt::black);\n\t\tpen.setWidth(line);\n\t\tp.setPen(pen);\n\t\tp.drawEllipse(diff, diff, diameter, diameter);\n\t}\n\treturn QCursor(QPixmap::fromImage(cursor));\n}\n\nColorEditor::Picker::Picker(QWidget *parent, Mode mode, QColor color)\n: RpWidget(parent)\n, _mode(mode) {\n\tsetCursor(generateCursor());\n\n\tconst auto size = QSize(st::colorPickerSize, st::colorPickerSize);\n\tresize(size);\n\n\t_palette = QImage(\n\t\tsize * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\n\tsetFromColor(color);\n}\n\nvoid ColorEditor::Picker::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tpreparePalette();\n\n\tp.drawImage(0, 0, _palette);\n\n\tconst auto left = anim::color(_topleft, _bottomleft, _y);\n\tconst auto right = anim::color(_topright, _bottomright, _y);\n\tconst auto color = anim::color(left, right, _x);\n\tconst auto lightness = 0.2989 * color.redF()\n\t\t+ 0.5870 * color.greenF()\n\t\t+ 0.1140 * color.blueF();\n\tauto pen = QPen((lightness > 0.6)\n\t\t? QColor(0, 0, 0)\n\t\t: QColor(255, 255, 255));\n\tpen.setWidth(st::colorPickerMarkLine);\n\tp.setPen(pen);\n\tp.setBrush(Qt::NoBrush);\n\n\tconst auto x = anim::interpolate(0, width() - 1, _x);\n\tconst auto y = anim::interpolate(0, height() - 1, _y);\n\tPainterHighQualityEnabler hq(p);\n\n\tp.drawEllipse(QRect(x, y, 0, 0) + Margins(st::colorPickerMarkRadius));\n}\n\nvoid ColorEditor::Picker::mousePressEvent(QMouseEvent *e) {\n\t_choosing = true;\n\tupdateCurrentPoint(e->pos());\n}\n\nvoid ColorEditor::Picker::mouseMoveEvent(QMouseEvent *e) {\n\tif (_choosing) {\n\t\tupdateCurrentPoint(e->pos());\n\t}\n}\n\nvoid ColorEditor::Picker::mouseReleaseEvent(QMouseEvent *e) {\n\t_choosing = false;\n}\n\nvoid ColorEditor::Picker::preparePalette() {\n\tif (!_paletteInvalidated) return;\n\t_paletteInvalidated = false;\n\n\tif (_mode == Mode::RGBA) {\n\t\tpreparePaletteRGBA();\n\t} else {\n\t\tpreparePaletteHSL();\n\t}\n\t_palette.setDevicePixelRatio(style::DevicePixelRatio());\n}\n\nvoid ColorEditor::Picker::preparePaletteRGBA() {\n\tconst auto size = _palette.width();\n\tauto ints = reinterpret_cast<uint32*>(_palette.bits());\n\tconst auto intsAddPerLine = (_palette.bytesPerLine()\n\t\t\t- size * sizeof(uint32))\n\t\t/ sizeof(uint32);\n\n\tconstexpr auto kLarge = 1024 * 1024;\n\tconstexpr auto kLargeBit = 20; // n / kLarge == (n >> kLargeBit)\n\tconst auto part = kLarge / size;\n\n\tconst auto topleft = anim::shifted(_topleft);\n\tconst auto topright = anim::shifted(_topright);\n\tconst auto bottomleft = anim::shifted(_bottomleft);\n\tconst auto bottomright = anim::shifted(_bottomright);\n\n\tauto yAccumulated = 0;\n\tfor (auto y = 0; y != size; ++y, yAccumulated += part) {\n\t\t// (yAccumulated * 256) / kLarge;\n\t\tconst auto yRatio = yAccumulated >> (kLargeBit - 8);\n\t\t// 0 <= yAccumulated < kLarge\n\t\t// 0 <= yRatio < 256\n\n\t\tconst auto topRatio = 256 - yRatio;\n\t\tconst auto bottomRatio = yRatio;\n\n\t\tconst auto left = anim::reshifted(bottomleft * bottomRatio\n\t\t\t+ topleft * topRatio);\n\t\tconst auto right = anim::reshifted(bottomright * bottomRatio\n\t\t\t+ topright * topRatio);\n\n\t\tauto xAccumulated = 0;\n\t\tfor (auto x = 0; x != size; ++x, xAccumulated += part) {\n\t\t\t// (xAccumulated * 256) / kLarge;\n\t\t\tconst auto xRatio = xAccumulated >> (kLargeBit - 8);\n\t\t\t// 0 <= xAccumulated < kLarge\n\t\t\t// 0 <= xRatio < 256\n\n\t\t\tconst auto leftRatio = 256 - xRatio;\n\t\t\tconst auto rightRatio = xRatio;\n\n\t\t\t*ints++ = anim::unshifted(left * leftRatio + right * rightRatio);\n\t\t}\n\t\tints += intsAddPerLine;\n\t}\n}\n\nvoid ColorEditor::Picker::preparePaletteHSL() {\n\tconst auto size = _palette.width();\n\tconst auto intsAddPerLine = (_palette.bytesPerLine()\n\t\t\t- size * sizeof(uint32))\n\t\t/ sizeof(uint32);\n\tauto ints = reinterpret_cast<uint32*>(_palette.bits());\n\n\tconstexpr auto kLarge = 1024 * 1024;\n\tconstexpr auto kLargeBit = 20; // n / kLarge == (n >> kLargeBit)\n\tconst auto part = kLarge / size;\n\n\tconst auto lightness = _topleft.lightness();\n\tconst auto right = anim::shifted(_bottomright);\n\n\tfor (auto y = 0; y != size; ++y) {\n\t\tconst auto hue = y * 360 / size;\n\t\tconst auto color = QColor::fromHsl(hue, 255, lightness).toRgb();\n\t\tconst auto left = anim::shifted(anim::getPremultiplied(color));\n\n\t\tauto xAccumulated = 0;\n\t\tfor (auto x = 0; x != size; ++x, xAccumulated += part) {\n\t\t\t// (xAccumulated * 256) / kLarge;\n\t\t\tconst auto xRatio = xAccumulated >> (kLargeBit - 8);\n\t\t\t// 0 <= xAccumulated < kLarge\n\t\t\t// 0 <= xRatio < 256\n\n\t\t\tconst auto leftRatio = 256 - xRatio;\n\t\t\tconst auto rightRatio = xRatio;\n\t\t\t*ints++ = anim::unshifted(left * leftRatio + right * rightRatio);\n\t\t}\n\t\tints += intsAddPerLine;\n\t}\n\n\t_palette = std::move(_palette).transformed(\n\t\tQTransform(0, 1, 1, 0, 0, 0));\n}\n\nvoid ColorEditor::Picker::updateCurrentPoint(QPoint localPosition) {\n\tconst auto x = std::clamp(localPosition.x(), 0, width())\n\t\t/ float64(width());\n\tconst auto y = std::clamp(localPosition.y(), 0, height())\n\t\t/ float64(height());\n\tif (_x != x || _y != y) {\n\t\t_x = x;\n\t\t_y = y;\n\t\tupdate();\n\t\t_changed.fire({});\n\t}\n}\n\nvoid ColorEditor::Picker::setHSB(HSB hsb) {\n\tif (_mode == Mode::RGBA) {\n\t\t_topleft = QColor(255, 255, 255);\n\t\t_topright.setHsv(std::max(0, hsb.hue), 255, 255);\n\t\t_topright = _topright.toRgb();\n\t\t_bottomleft = _bottomright = QColor(0, 0, 0);\n\n\t\t_x = std::clamp(hsb.saturation / 255., 0., 1.);\n\t\t_y = 1. - std::clamp(hsb.brightness / 255., 0., 1.);\n\t} else {\n\t\t_topleft = _topright = QColor::fromHsl(0, 255, hsb.brightness);\n\t\t_bottomleft = _bottomright = QColor::fromHsl(0, 0, hsb.brightness);\n\n\t\t_x = std::clamp(hsb.hue / 360., 0., 1.);\n\t\t_y = 1. - std::clamp(hsb.saturation / 255., 0., 1.);\n\t}\n\n\t_paletteInvalidated = true;\n\tupdate();\n}\n\nvoid ColorEditor::Picker::setRGB(int red, int green, int blue) {\n\tsetFromColor(QColor(red, green, blue));\n}\n\nvoid ColorEditor::Picker::setFromColor(QColor color) {\n\tif (_mode == Mode::RGBA) {\n\t\tsetHSB({ color.hsvHue(), color.hsvSaturation(), color.value() });\n\t} else {\n\t\tsetHSB({ color.hslHue(), color.hslSaturation(), color.lightness() });\n\t}\n}\n\nclass ColorEditor::Slider : public Ui::RpWidget {\npublic:\n\tenum class Direction {\n\t\tHorizontal,\n\t\tVertical,\n\t};\n\tenum class Type {\n\t\tHue,\n\t\tOpacity,\n\t\tLightness\n\t};\n\tSlider(QWidget *parent, Direction direction, Type type, QColor color);\n\n\trpl::producer<> changed() const {\n\t\treturn _changed.events();\n\t}\n\tfloat64 value() const {\n\t\treturn _value;\n\t}\n\tvoid setValue(float64 value) {\n\t\t_value = std::clamp(value, 0., 1.);\n\t\tupdate();\n\t}\n\tvoid setHSB(HSB hsb);\n\tvoid setRGB(int red, int green, int blue);\n\tvoid setAlpha(int alpha);\n\n\tvoid setLightnessLimits(int min, int max);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\nprivate:\n\tfloat64 valueFromColor(QColor color) const;\n\tfloat64 valueFromHue(int hue) const;\n\tbool isHorizontal() const {\n\t\treturn (_direction == Direction::Horizontal);\n\t}\n\tvoid colorUpdated();\n\tvoid prepareMinSize();\n\tvoid generatePixmap();\n\tvoid updatePixmapFromMask();\n\tvoid updateCurrentPoint(QPoint localPosition);\n\t[[nodiscard]] QColor applyLimits(QColor color) const;\n\n\tDirection _direction = Direction::Horizontal;\n\tType _type = Type::Hue;\n\n\tint _lightnessMin = 0;\n\tint _lightnessMax = 255;\n\n\tQColor _color;\n\tfloat64 _value = 0;\n\n\tQImage _mask;\n\tQPixmap _pixmap;\n\tQBrush _transparent;\n\n\tbool _choosing = false;\n\trpl::event_stream<> _changed;\n\n};\n\nColorEditor::Slider::Slider(\n\tQWidget *parent,\n\tDirection direction,\n\tType type,\n\tQColor color)\n: RpWidget(parent)\n, _direction(direction)\n, _type(type)\n, _color(color.red(), color.green(), color.blue())\n, _value(valueFromColor(color))\n, _transparent((_type == Type::Opacity)\n\t\t? style::TransparentPlaceholder()\n\t\t: QBrush()) {\n\tprepareMinSize();\n}\n\nvoid ColorEditor::Slider::prepareMinSize() {\n\tconst auto minSize = st::colorSliderSkip * 2 + st::colorSliderWidth;\n\tresize(minSize, minSize);\n}\n\nvoid ColorEditor::Slider::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tconst auto to = rect() - Margins(st::colorSliderSkip);\n\tUi::Shadow::paint(p, to, width(), st::defaultRoundShadow);\n\tif (_type == Type::Opacity) {\n\t\tp.fillRect(to, _transparent);\n\t}\n\tp.drawPixmap(to, _pixmap, _pixmap.rect());\n\tif (isHorizontal()) {\n\t\tconst auto x = st::colorSliderSkip + std::round(_value * to.width());\n\t\tst::colorSliderArrowTop.paint(\n\t\t\tp,\n\t\t\tx - st::colorSliderArrowTop.width() / 2,\n\t\t\t0,\n\t\t\twidth());\n\t\tst::colorSliderArrowBottom.paint(\n\t\t\tp,\n\t\t\tx - st::colorSliderArrowBottom.width() / 2,\n\t\t\theight() - st::colorSliderArrowBottom.height(),\n\t\t\twidth());\n\t} else {\n\t\tconst auto y = st::colorSliderSkip + std::round(_value * to.height());\n\t\tst::colorSliderArrowLeft.paint(\n\t\t\tp,\n\t\t\t0,\n\t\t\ty - st::colorSliderArrowLeft.height() / 2,\n\t\t\twidth());\n\t\tst::colorSliderArrowRight.paint(\n\t\t\tp,\n\t\t\twidth() - st::colorSliderArrowRight.width(),\n\t\t\ty - st::colorSliderArrowRight.height() / 2,\n\t\t\twidth());\n\t}\n}\n\nvoid ColorEditor::Slider::resizeEvent(QResizeEvent *e) {\n\tgeneratePixmap();\n\tupdate();\n}\n\nvoid ColorEditor::Slider::mousePressEvent(QMouseEvent *e) {\n\t_choosing = true;\n\tupdateCurrentPoint(e->pos());\n}\n\nvoid ColorEditor::Slider::mouseMoveEvent(QMouseEvent *e) {\n\tif (_choosing) {\n\t\tupdateCurrentPoint(e->pos());\n\t}\n}\n\nvoid ColorEditor::Slider::mouseReleaseEvent(QMouseEvent *e) {\n\t_choosing = false;\n}\n\nvoid ColorEditor::Slider::generatePixmap() {\n\tconst auto size = (isHorizontal() ? width() : height())\n\t\t* style::DevicePixelRatio();\n\tauto image = QImage(\n\t\tsize,\n\t\tstyle::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\tauto ints = reinterpret_cast<uint32*>(image.bits());\n\tconst auto intsPerLine = image.bytesPerLine() / sizeof(uint32);\n\tconst auto intsPerLineAdded = intsPerLine - size;\n\n\tconstexpr auto kLarge = 1024 * 1024;\n\tconstexpr auto kLargeBit = 20; // n / kLarge == (n >> kLargeBit)\n\tconst auto part = kLarge / size;\n\n\tif (_type == Type::Hue) {\n\t\tfor (auto x = 0; x != size; ++x) {\n\t\t\tconst auto color = QColor::fromHsv(x * 360 / size, 255, 255);\n\t\t\tconst auto value = anim::getPremultiplied(color.toRgb());\n\t\t\tfor (auto y = 0; y != style::DevicePixelRatio(); ++y) {\n\t\t\t\tints[y * intsPerLine] = value;\n\t\t\t}\n\t\t\t++ints;\n\t\t}\n\t\tif (!isHorizontal()) {\n\t\t\timage = std::move(image).transformed(\n\t\t\t\tQTransform(0, -1, 1, 0, 0, 0));\n\t\t}\n\t\t_pixmap = Ui::PixmapFromImage(std::move(image));\n\t} else if (_type == Type::Opacity) {\n\t\tauto color = anim::shifted(QColor(255, 255, 255, 255));\n\t\tauto transparent = anim::shifted(QColor(255, 255, 255, 0));\n\t\tfor (auto y = 0; y != style::DevicePixelRatio(); ++y) {\n\t\t\tauto xAccumulated = 0;\n\t\t\tfor (auto x = 0; x != size; ++x, xAccumulated += part) {\n\t\t\t\tconst auto xRatio = xAccumulated >> (kLargeBit - 8);\n\t\t\t\t// 0 <= xAccumulated < kLarge\n\t\t\t\t// 0 <= xRatio < 256\n\n\t\t\t\t*ints++ = anim::unshifted(color * xRatio\n\t\t\t\t\t+ transparent * (256 - xRatio));\n\t\t\t}\n\t\t\tints += intsPerLineAdded;\n\t\t}\n\t\tif (!isHorizontal()) {\n\t\t\timage = std::move(image).transformed(\n\t\t\t\tQTransform(0, -1, 1, 0, 0, 0));\n\t\t}\n\t\t_mask = std::move(image);\n\t\tupdatePixmapFromMask();\n\t} else {\n\t\tconst auto range = _lightnessMax - _lightnessMin;\n\t\tfor (auto x = 0; x != size; ++x) {\n\t\t\tconst auto color = QColor::fromHsl(\n\t\t\t\t_color.hslHue(),\n\t\t\t\t_color.hslSaturation(),\n\t\t\t\t_lightnessMin + x * range / size);\n\t\t\tconst auto value = anim::getPremultiplied(color.toRgb());\n\t\t\tfor (auto y = 0; y != style::DevicePixelRatio(); ++y) {\n\t\t\t\tints[y * intsPerLine] = value;\n\t\t\t}\n\t\t\t++ints;\n\t\t}\n\t\tif (!isHorizontal()) {\n\t\t\timage = std::move(image).transformed(\n\t\t\t\tQTransform(0, -1, 1, 0, 0, 0));\n\t\t}\n\t\t_pixmap = Ui::PixmapFromImage(std::move(image));\n\t}\n}\n\nvoid ColorEditor::Slider::setHSB(HSB hsb) {\n\tif (_type == Type::Hue) {\n\t\t// hue == 360 converts to 0 if done in general way\n\t\t_value = valueFromHue(hsb.hue);\n\t\tupdate();\n\t} else if (_type == Type::Opacity) {\n\t\t_color.setHsv(hsb.hue, hsb.saturation, hsb.brightness);\n\t\tcolorUpdated();\n\t} else {\n\t\t_color.setHsl(\n\t\t\thsb.hue,\n\t\t\thsb.saturation,\n\t\t\tstd::clamp(hsb.brightness, _lightnessMin, _lightnessMax));\n\t\tcolorUpdated();\n\t}\n}\n\nvoid ColorEditor::Slider::setRGB(int red, int green, int blue) {\n\t_color = applyLimits(QColor(red, green, blue));\n\tcolorUpdated();\n}\n\nvoid ColorEditor::Slider::colorUpdated() {\n\tif (_type == Type::Hue) {\n\t\t_value = valueFromColor(_color);\n\t} else if (!_mask.isNull()) {\n\t\tupdatePixmapFromMask();\n\t} else {\n\t\t_value = valueFromColor(_color);\n\t\tgeneratePixmap();\n\t}\n\tupdate();\n}\n\nfloat64 ColorEditor::Slider::valueFromColor(QColor color) const {\n\treturn (_type == Type::Hue)\n\t\t? valueFromHue(color.hsvHue())\n\t\t: (_type == Type::Opacity)\n\t\t? color.alphaF()\n\t\t: std::clamp(\n\t\t\t((color.lightness() - _lightnessMin)\n\t\t\t\t/ float64(_lightnessMax - _lightnessMin)),\n\t\t\t0.,\n\t\t\t1.);\n}\n\nfloat64 ColorEditor::Slider::valueFromHue(int hue) const {\n\treturn (1. - std::clamp(hue, 0, 360) / 360.);\n}\n\nvoid ColorEditor::Slider::setAlpha(int alpha) {\n\tif (_type == Type::Opacity) {\n\t\t_value = std::clamp(alpha, 0, 255) / 255.;\n\t\tupdate();\n\t}\n}\n\nvoid ColorEditor::Slider::setLightnessLimits(int min, int max) {\n\tExpects(max > min);\n\n\t_lightnessMin = min;\n\t_lightnessMax = max;\n\t_color = applyLimits(_color);\n\tcolorUpdated();\n}\n\nvoid ColorEditor::Slider::updatePixmapFromMask() {\n\t_pixmap = Ui::PixmapFromImage(style::colorizeImage(_mask, _color));\n}\n\nvoid ColorEditor::Slider::updateCurrentPoint(QPoint localPosition) {\n\tconst auto coord = (isHorizontal()\n\t\t? localPosition.x()\n\t\t: localPosition.y()) - st::colorSliderSkip;\n\tconst auto maximum = (isHorizontal()\n\t\t? width()\n\t\t: height()) - 2 * st::colorSliderSkip;\n\tconst auto value = std::clamp(coord, 0, maximum) / float64(maximum);\n\tif (_value != value) {\n\t\t_value = value;\n\t\tupdate();\n\t\t_changed.fire({});\n\t}\n}\n\nQColor ColorEditor::Slider::applyLimits(QColor color) const {\n\tif (_type != Type::Lightness) {\n\t\treturn color;\n\t}\n\n\tconst auto lightness = color.lightness();\n\tconst auto clamped = std::clamp(lightness, _lightnessMin, _lightnessMax);\n\tif (clamped == lightness) {\n\t\treturn color;\n\t}\n\treturn QColor::fromHsl(color.hslHue(), color.hslSaturation(), clamped);\n}\n\nclass ColorEditor::Field : public Ui::MaskedInputField {\npublic:\n\tField(\n\t\tQWidget *parent,\n\t\tconst style::InputField &st,\n\t\tconst QString &placeholder,\n\t\tint limit,\n\t\tconst QString &units = QString());\n\n\tint value() const {\n\t\treturn getLastText().toInt();\n\t}\n\n\tvoid setTextWithFocus(const QString &text) {\n\t\tsetText(text);\n\t\tif (hasFocus()) {\n\t\t\tselectAll();\n\t\t}\n\t}\n\nprotected:\n\tvoid correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) override;\n\tvoid paintAdditionalPlaceholder(QPainter &p) override;\n\n\tvoid wheelEvent(QWheelEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tvoid changeValue(int delta);\n\n\tQString _placeholder, _units;\n\tint _limit = 0;\n\tint _digitLimit = 1;\n\tint _wheelDelta = 0;\n\n};\n\nColorEditor::Field::Field(\n\tQWidget *parent,\n\tconst style::InputField &st,\n\tconst QString &placeholder,\n\tint limit,\n\tconst QString &units)\n: Ui::MaskedInputField(parent, st)\n, _placeholder(placeholder)\n, _units(units)\n, _limit(limit)\n, _digitLimit(QString::number(_limit).size()) {\n}\n\nvoid ColorEditor::Field::correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) {\n\tconst auto oldPos = nowCursor;\n\tconst auto oldLen = now.length();\n\tauto newText = QString();\n\tauto newPos = -1;\n\n\tnewText.reserve(oldLen);\n\tfor (int i = 0; i < oldLen; ++i) {\n\t\tif (i == oldPos) {\n\t\t\tnewPos = newText.length();\n\t\t}\n\n\t\tconst auto ch = (now[i]);\n\t\tif (ch.isDigit()) {\n\t\t\tnewText += ch;\n\t\t}\n\t\tif (newText.size() >= _digitLimit) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (newPos < 0 || newPos > newText.size()) {\n\t\tnewPos = newText.size();\n\t}\n\tif (newText.toInt() > _limit) {\n\t\tnewText = QString::number(_limit);\n\t\tnewPos = newText.size();\n\t}\n\tif (newText != now) {\n\t\tnow = newText;\n\t\tsetText(now);\n\t\tstartPlaceholderAnimation();\n\t\tnowCursor = -1;\n\t}\n\tif (newPos != nowCursor) {\n\t\tnowCursor = newPos;\n\t\tsetCursorPosition(nowCursor);\n\t}\n}\n\nvoid ColorEditor::Field::paintAdditionalPlaceholder(QPainter &p) {\n\tp.setFont(_st.style.font);\n\tp.setPen(_st.placeholderFg);\n\tconst auto inner = QRect(\n\t\t_st.textMargins.right(),\n\t\t_st.textMargins.top(),\n\t\twidth() - 2 * _st.textMargins.right(),\n\t\theight() - rect::m::sum::v(_st.textMargins));\n\tp.drawText(inner, _placeholder, style::al_topleft);\n\tif (!_units.isEmpty()) {\n\t\tp.drawText(inner, _units, style::al_topright);\n\t}\n}\n\nvoid ColorEditor::Field::wheelEvent(QWheelEvent *e) {\n\tif (!hasFocus()) {\n\t\treturn;\n\t}\n\n\tauto deltaX = e->angleDelta().x();\n\tauto deltaY = e->angleDelta().y();\n\tif (Platform::IsMac()) {\n\t\tdeltaY *= -1;\n\t} else {\n\t\tdeltaX *= -1;\n\t}\n\t_wheelDelta += (std::abs(deltaX) > std::abs(deltaY)) ? deltaX : deltaY;\n\n\tconstexpr auto kStep = 5;\n\tif (const auto delta = _wheelDelta / kStep) {\n\t\t_wheelDelta -= delta * kStep;\n\t\tchangeValue(delta);\n\t}\n}\n\nvoid ColorEditor::Field::changeValue(int delta) {\n\tconst auto currentValue = value();\n\tconst auto newValue = std::clamp(currentValue + delta, 0, _limit);\n\tif (newValue != currentValue) {\n\t\tsetText(QString::number(newValue));\n\t\tsetFocus();\n\t\tselectAll();\n\t\tchanged();\n\t}\n}\n\nvoid ColorEditor::Field::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Up) {\n\t\tchangeValue(1);\n\t} else if (e->key() == Qt::Key_Down) {\n\t\tchangeValue(-1);\n\t} else {\n\t\tMaskedInputField::keyPressEvent(e);\n\t}\n}\n\nclass ColorEditor::ResultField : public Ui::MaskedInputField {\npublic:\n\tResultField(QWidget *parent, const style::InputField &st);\n\n\tvoid setTextWithFocus(const QString &text) {\n\t\tsetText(text);\n\t\tif (hasFocus()) {\n\t\t\tselectAll();\n\t\t}\n\t}\n\nprotected:\n\tvoid correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) override;\n\tvoid paintAdditionalPlaceholder(QPainter &p) override;\n\n};\n\nColorEditor::ResultField::ResultField(\n\tQWidget *parent,\n\tconst style::InputField &st)\n: Ui::MaskedInputField(parent, st) {\n}\n\nvoid ColorEditor::ResultField::correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) {\n\tconst auto oldPos = nowCursor;\n\tconst auto oldLen = now.length();\n\tauto newText = QString();\n\tauto newPos = -1;\n\n\tnewText.reserve(oldLen);\n\tfor (auto i = 0; i < oldLen; ++i) {\n\t\tif (i == oldPos) {\n\t\t\tnewPos = newText.length();\n\t\t}\n\n\t\tconst auto ch = (now[i]);\n\t\tconst auto code = ch.unicode();\n\t\tif ((code >= '0' && code <= '9')\n\t\t\t|| (ch >= 'a' && ch <= 'f')\n\t\t\t|| (ch >= 'A' && ch <= 'F')) {\n\t\t\tnewText += ch;\n\t\t}\n\t\tif (newText.size() >= 8) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (newPos < 0 || newPos > newText.size()) {\n\t\tnewPos = newText.size();\n\t}\n\tif (newText != now) {\n\t\tnow = newText;\n\t\tsetText(now);\n\t\tstartPlaceholderAnimation();\n\t\tnowCursor = -1;\n\t}\n\tif (newPos != nowCursor) {\n\t\tnowCursor = newPos;\n\t\tsetCursorPosition(nowCursor);\n\t}\n}\n\nvoid ColorEditor::ResultField::paintAdditionalPlaceholder(QPainter &p) {\n\tp.setFont(_st.style.font);\n\tp.setPen(_st.placeholderFg);\n\tp.drawText(\n\t\tQRect(\n\t\t\t_st.textMargins.right(),\n\t\t\t_st.textMargins.top(),\n\t\t\twidth(),\n\t\t\theight() - rect::m::sum::v(_st.textMargins)),\n\t\t\"#\",\n\t\tstyle::al_topleft);\n}\n\nColorEditor::ColorEditor(\n\tQWidget *parent,\n\tMode mode,\n\tQColor current)\n: RpWidget()\n, _mode(mode)\n, _picker(this, mode, current)\n, _hueField(this, st::colorValueInput, \"H\", 360, QString(QChar(176))) // degree character\n, _saturationField(this, st::colorValueInput, \"S\", 100, \"%\")\n, _brightnessField(\n\tthis,\n\tst::colorValueInput,\n\t(mode == Mode::RGBA) ? \"B\" : \"L\", 100, \"%\")\n, _redField(this, st::colorValueInput, \"R\", 255)\n, _greenField(this, st::colorValueInput, \"G\", 255)\n, _blueField(this, st::colorValueInput, \"B\", 255)\n, _result(this, st::colorResultInput)\n, _transparent(style::TransparentPlaceholder())\n, _current(current)\n, _new(current) {\n\tif (_mode == Mode::RGBA) {\n\t\t_hueSlider.create(\n\t\t\tthis,\n\t\t\tSlider::Direction::Vertical,\n\t\t\tSlider::Type::Hue,\n\t\t\tcurrent);\n\t\t_opacitySlider.create(\n\t\t\tthis,\n\t\t\tSlider::Direction::Horizontal,\n\t\t\tSlider::Type::Opacity,\n\t\t\tcurrent);\n\t} else if (_mode == Mode::HSL) {\n\t\t_lightnessSlider.create(\n\t\t\tthis,\n\t\t\tSlider::Direction::Horizontal,\n\t\t\tSlider::Type::Lightness,\n\t\t\tcurrent);\n\t}\n\tprepare();\n}\n\nvoid ColorEditor::setLightnessLimits(int min, int max) {\n\tExpects(_mode == Mode::HSL);\n\n\t_lightnessMin = min;\n\t_lightnessMax = max;\n\t_lightnessSlider->setLightnessLimits(min, max);\n\n\tconst auto adjusted = applyLimits(_new);\n\tif (_new != adjusted) {\n\t\tupdateFromColor(adjusted);\n\t}\n}\n\nvoid ColorEditor::prepare() {\n\tconst auto hsbChanged = [=] { updateFromHSBFields(); };\n\tconst auto rgbChanged = [=] { updateFromRGBFields(); };\n\tconnect(_hueField, &Ui::MaskedInputField::changed, hsbChanged);\n\tconnect(_saturationField, &Ui::MaskedInputField::changed, hsbChanged);\n\tconnect(_brightnessField, &Ui::MaskedInputField::changed, hsbChanged);\n\tconnect(_redField, &Ui::MaskedInputField::changed, rgbChanged);\n\tconnect(_greenField, &Ui::MaskedInputField::changed, rgbChanged);\n\tconnect(_blueField, &Ui::MaskedInputField::changed, rgbChanged);\n\tconnect(_result, &Ui::MaskedInputField::changed, [=] {\n\t\tupdateFromResultField();\n\t});\n\n\tconst auto submitted = [=] { fieldSubmitted(); };\n\tconnect(_hueField, &Ui::MaskedInputField::submitted, submitted);\n\tconnect(_saturationField, &Ui::MaskedInputField::submitted, submitted);\n\tconnect(_brightnessField, &Ui::MaskedInputField::submitted, submitted);\n\tconnect(_redField, &Ui::MaskedInputField::submitted, submitted);\n\tconnect(_greenField, &Ui::MaskedInputField::submitted, submitted);\n\tconnect(_blueField, &Ui::MaskedInputField::submitted, submitted);\n\tconnect(_result, &Ui::MaskedInputField::submitted, submitted);\n\n\tsetNaturalWidth(st::colorEditWidth);\n\tconst auto height = st::colorEditSkip\n\t\t+ st::colorPickerSize\n\t\t+ st::colorEditSkip\n\t\t+ st::colorSliderWidth\n\t\t+ st::colorEditSkip;\n\tresize(st::colorEditWidth, height);\n\n\trpl::merge(\n\t\t_picker->changed(),\n\t\t(_hueSlider ? _hueSlider->changed() : rpl::never<>()),\n\t\t(_opacitySlider ? _opacitySlider->changed() : rpl::never<>()),\n\t\t(_lightnessSlider ? _lightnessSlider->changed() : rpl::never<>())\n\t) | rpl::on_next([=] {\n\t\tupdateFromControls();\n\t}, lifetime());\n\n\tupdateRGBFields();\n\tupdateHSBFields();\n\tupdateResultField();\n\tupdate();\n}\n\nvoid ColorEditor::setInnerFocus() const {\n\t_result->setFocus();\n\t_result->selectAll();\n}\n\nQColor ColorEditor::color() const {\n\treturn _new.toRgb();\n}\n\nrpl::producer<QColor> ColorEditor::colorValue() const {\n\treturn _newChanges.events_starting_with_copy(_new);\n}\n\nrpl::producer<> ColorEditor::submitRequests() const {\n\treturn _submitRequests.events();\n}\n\nvoid ColorEditor::fieldSubmitted() {\n\tconst auto fields = std::array<Ui::MaskedInputField*, 7>{ {\n\t\t_hueField,\n\t\t_saturationField,\n\t\t_brightnessField,\n\t\t_redField,\n\t\t_greenField,\n\t\t_blueField,\n\t\t_result,\n\t} };\n\tfor (auto i = 0, count = int(fields.size()); i + 1 != count; ++i) {\n\t\tif (fields[i]->hasFocus()) {\n\t\t\tfields[i + 1]->setFocus();\n\t\t\tfields[i + 1]->selectAll();\n\t\t\treturn;\n\t\t}\n\t}\n\tif (_result->hasFocus()) {\n\t\t_submitRequests.fire({});\n\t}\n}\n\nvoid ColorEditor::updateHSBFields() {\n\tconst auto hsb = hsbFromControls();\n\t_hueField->setTextWithFocus(QString::number(hsb.hue));\n\t_saturationField->setTextWithFocus(\n\t\tQString::number(percentFromByte(hsb.saturation)));\n\t_brightnessField->setTextWithFocus(\n\t\tQString::number(percentFromByte(hsb.brightness)));\n}\n\nvoid ColorEditor::updateRGBFields() {\n\t_redField->setTextWithFocus(QString::number(_new.red()));\n\t_greenField->setTextWithFocus(QString::number(_new.green()));\n\t_blueField->setTextWithFocus(QString::number(_new.blue()));\n}\n\nvoid ColorEditor::updateResultField() {\n\tauto text = QString();\n\tconst auto addHex = [&text](int value) {\n\t\tif (value >= 0 && value <= 9) {\n\t\t\ttext.append(QChar('0' + value));\n\t\t} else if (value >= 10 && value <= 15) {\n\t\t\ttext.append(QChar('a' + (value - 10)));\n\t\t}\n\t};\n\tconst auto addValue = [&](int value) {\n\t\taddHex(value / 16);\n\t\taddHex(value % 16);\n\t};\n\taddValue(_new.red());\n\taddValue(_new.green());\n\taddValue(_new.blue());\n\tif (_new.alpha() != 255) {\n\t\taddValue(_new.alpha());\n\t}\n\t_result->setTextWithFocus(text);\n}\n\nvoid ColorEditor::resizeEvent(QResizeEvent *e) {\n\tconst auto fullwidth = _picker->width()\n\t\t+ ((_mode == Mode::RGBA)\n\t\t\t? (2 * (st::colorEditSkip - st::colorSliderSkip)\n\t\t\t\t+ _hueSlider->width())\n\t\t\t: (2 * st::colorEditSkip))\n\t\t+ st::colorSampleSize.width();\n\tconst auto left = (width() - fullwidth) / 2;\n\t_picker->moveToLeft(left, st::colorEditSkip);\n\tif (_hueSlider) {\n\t\t_hueSlider->setGeometryToLeft(\n\t\t\trect::right(_picker) + st::colorEditSkip - st::colorSliderSkip,\n\t\t\tst::colorEditSkip - st::colorSliderSkip,\n\t\t\t_hueSlider->width(),\n\t\t\tst::colorPickerSize + 2 * st::colorSliderSkip);\n\t}\n\t{\n\t\tconst auto rectSlider = QRect(\n\t\t\t_picker->x() - st::colorSliderSkip,\n\t\t\trect::bottom(_picker) + st::colorEditSkip - st::colorSliderSkip,\n\t\t\t_picker->width() + 2 * st::colorSliderSkip,\n\t\t\t0);\n\t\tif (_opacitySlider) {\n\t\t\t_opacitySlider->setGeometryToLeft(rectSlider\n\t\t\t\t+ QMargins(0, 0, 0, _opacitySlider->height()));\n\t\t}\n\t\tif (_lightnessSlider) {\n\t\t\t_lightnessSlider->setGeometryToLeft(rectSlider\n\t\t\t\t+ QMargins(0, 0, 0, _lightnessSlider->height()));\n\t\t}\n\t}\n\tconst auto fieldLeft = (_mode == Mode::RGBA)\n\t\t? (rect::right(_hueSlider) + st::colorEditSkip - st::colorSliderSkip)\n\t\t: (rect::right(_picker) + st::colorEditSkip);\n\tconst auto addWidth = (_mode == Mode::RGBA) ? 0 : st::colorEditSkip;\n\tconst auto fieldWidth = st::colorSampleSize.width() + addWidth;\n\tconst auto fieldHeight = _hueField->height();\n\t_newRect = QRect(\n\t\tfieldLeft,\n\t\tst::colorEditSkip,\n\t\tfieldWidth,\n\t\tst::colorSampleSize.height());\n\t_currentRect = _newRect.translated(0, st::colorSampleSize.height());\n\t{\n\t\tconst auto fieldRect = QRect(fieldLeft, 0, fieldWidth, fieldHeight);\n\t\t_hueField->setGeometryToLeft(fieldRect.translated(\n\t\t\t0,\n\t\t\trect::bottom(_currentRect) + st::colorFieldSkip));\n\t\t_saturationField->setGeometryToLeft(fieldRect.translated(\n\t\t\t0,\n\t\t\trect::bottom(_hueField)));\n\t\t_brightnessField->setGeometryToLeft(fieldRect.translated(\n\t\t\t0,\n\t\t\trect::bottom(_saturationField)));\n\t\t_redField->setGeometryToLeft(fieldRect.translated(\n\t\t\t0,\n\t\t\trect::bottom(_brightnessField) + st::colorFieldSkip));\n\t\t_greenField->setGeometryToLeft(fieldRect.translated(\n\t\t\t0,\n\t\t\trect::bottom(_redField)));\n\t\t_blueField->setGeometryToLeft(fieldRect.translated(\n\t\t\t0,\n\t\t\trect::bottom(_greenField)));\n\t}\n\tconst auto resultDelta = (_mode == Mode::RGBA)\n\t\t? (st::colorEditSkip + st::colorSliderWidth)\n\t\t: 0;\n\tconst auto resultBottom = (_mode == Mode::RGBA)\n\t\t? (rect::bottom(_opacitySlider))\n\t\t: (rect::bottom(_lightnessSlider));\n\t_result->setGeometryToLeft(\n\t\tfieldLeft - resultDelta,\n\t\tresultBottom - st::colorSliderSkip - _result->height(),\n\t\tfieldWidth + resultDelta,\n\t\tfieldHeight);\n}\n\nvoid ColorEditor::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\tUi::Shadow::paint(\n\t\tp,\n\t\t_picker->geometry(),\n\t\twidth(),\n\t\tst::defaultRoundShadow);\n\n\tUi::Shadow::paint(\n\t\tp,\n\t\t_newRect + QMargins(0, 0, 0, _currentRect.height()),\n\t\twidth(),\n\t\tst::defaultRoundShadow);\n\tif (_new.alphaF() < 1.) {\n\t\tp.fillRect(myrtlrect(_newRect), _transparent);\n\t}\n\tp.fillRect(myrtlrect(_newRect), _new);\n\tif (_current.alphaF() < 1.) {\n\t\tp.fillRect(myrtlrect(_currentRect), _transparent);\n\t}\n\tp.fillRect(myrtlrect(_currentRect), _current);\n}\n\nvoid ColorEditor::mousePressEvent(QMouseEvent *e) {\n\tif (myrtlrect(_currentRect).contains(e->pos())) {\n\t\tupdateFromColor(_current);\n\t}\n}\n\nColorEditor::HSB ColorEditor::hsbFromControls() const {\n\tconst auto hue = (_mode == Mode::RGBA)\n\t\t? std::round((1. - _hueSlider->value()) * 360)\n\t\t: std::round(_picker->valueX() * 360);\n\tconst auto saturation = (_mode == Mode::RGBA)\n\t\t? std::round(_picker->valueX() * 255)\n\t\t: std::round((1. - _picker->valueY()) * 255);\n\tconst auto brightness = (_mode == Mode::RGBA)\n\t\t? std::round((1. - _picker->valueY()) * 255)\n\t\t: (_lightnessMin\n\t\t\t+ std::round(_lightnessSlider->value()\n\t\t\t\t* (_lightnessMax - _lightnessMin)));\n\treturn { int(hue), int(saturation), int(brightness) };\n}\n\nQColor ColorEditor::applyLimits(QColor color) const {\n\tif (_mode != Mode::HSL) {\n\t\treturn color;\n\t}\n\n\tconst auto lightness = color.lightness();\n\tconst auto clamped = std::clamp(lightness, _lightnessMin, _lightnessMax);\n\tif (clamped == lightness) {\n\t\treturn color;\n\t}\n\treturn QColor::fromHsl(color.hslHue(), color.hslSaturation(), clamped);\n}\n\nvoid ColorEditor::updateFromColor(QColor color) {\n\t_new = applyLimits(color);\n\t_newChanges.fire_copy(_new);\n\tupdateControlsFromColor();\n\tupdateRGBFields();\n\tupdateHSBFields();\n\tupdateResultField();\n\tupdate();\n}\n\nvoid ColorEditor::updateFromControls() {\n\tconst auto hsb = hsbFromControls();\n\tconst auto alpha = _opacitySlider\n\t\t? std::round(_opacitySlider->value() * 255)\n\t\t: 255;\n\tsetHSB(hsb, alpha);\n\tupdateHSBFields();\n\tupdateControlsFromHSB(hsb);\n}\n\nvoid ColorEditor::updateFromHSBFields() {\n\tconst auto hue = _hueField->value();\n\tconst auto saturation = percentToByte(_saturationField->value());\n\tconst auto brightness = std::clamp(\n\t\tpercentToByte(_brightnessField->value()),\n\t\t_lightnessMin,\n\t\t_lightnessMax);\n\tconst auto alpha = _opacitySlider\n\t\t? std::round(_opacitySlider->value() * 255)\n\t\t: 255;\n\tsetHSB({ hue, saturation, brightness }, alpha);\n\tupdateControlsFromHSB({ hue, saturation, brightness });\n}\n\nvoid ColorEditor::updateFromRGBFields() {\n\tconst auto red = _redField->value();\n\tconst auto blue = _blueField->value();\n\tconst auto green = _greenField->value();\n\tconst auto alpha = _opacitySlider\n\t\t? std::round(_opacitySlider->value() * 255)\n\t\t: 255;\n\tsetRGB(red, green, blue, alpha);\n\tupdateResultField();\n}\n\nvoid ColorEditor::updateFromResultField() {\n\tconst auto text = _result->getLastText();\n\tif (text.size() != 6 && text.size() != 8) {\n\t\treturn;\n\t}\n\n\tconst auto fromHex = [](QChar hex) {\n\t\tauto code = hex.unicode();\n\t\tif (code >= 'A' && code <= 'F') {\n\t\t\treturn (code - 'A' + 10);\n\t\t} else if (code >= 'a' && code <= 'f') {\n\t\t\treturn (code - 'a' + 10);\n\t\t}\n\t\treturn code - '0';\n\t};\n\tconst auto fromChars = [&](QChar a, QChar b) {\n\t\treturn fromHex(a) * 0x10 + fromHex(b);\n\t};\n\tconst auto red = fromChars(text[0], text[1]);\n\tconst auto green = fromChars(text[2], text[3]);\n\tconst auto blue = fromChars(text[4], text[5]);\n\tconst auto alpha = (text.size() == 8) ? fromChars(text[6], text[7]) : 255;\n\tsetRGB(red, green, blue, alpha);\n\tupdateRGBFields();\n}\n\nvoid ColorEditor::updateControlsFromHSB(HSB hsb) {\n\t_picker->setHSB(hsb);\n\tif (_hueSlider) {\n\t\t_hueSlider->setHSB(hsb);\n\t}\n\tif (_opacitySlider) {\n\t\t_opacitySlider->setHSB(hsb);\n\t}\n\tif (_lightnessSlider) {\n\t\t_lightnessSlider->setHSB(hsb);\n\t}\n}\n\nvoid ColorEditor::updateControlsFromColor() {\n\tconst auto red = _new.red();\n\tconst auto green = _new.green();\n\tconst auto blue = _new.blue();\n\tconst auto alpha = _new.alpha();\n\t_picker->setRGB(red, green, blue);\n\tif (_hueSlider) {\n\t\t_hueSlider->setRGB(red, green, blue);\n\t}\n\tif (_opacitySlider) {\n\t\t_opacitySlider->setRGB(red, green, blue);\n\t\t_opacitySlider->setAlpha(alpha);\n\t}\n\tif (_lightnessSlider) {\n\t\t_lightnessSlider->setRGB(red, green, blue);\n\t}\n}\n\nvoid ColorEditor::setHSB(HSB hsb, int alpha) {\n\tif (_mode == Mode::RGBA) {\n\t\t_new.setHsv(hsb.hue, hsb.saturation, hsb.brightness, alpha);\n\t} else {\n\t\t_new.setHsl(hsb.hue, hsb.saturation, hsb.brightness, alpha);\n\t}\n\t_newChanges.fire_copy(_new);\n\tupdateRGBFields();\n\tupdateResultField();\n\tupdate();\n}\n\nvoid ColorEditor::setRGB(int red, int green, int blue, int alpha) {\n\t_new = applyLimits(QColor(red, green, blue, alpha));\n\t_newChanges.fire_copy(_new);\n\tupdateControlsFromColor();\n\tupdateHSBFields();\n\tupdate();\n}\n\nvoid ColorEditor::showColor(QColor color) {\n\tupdateFromColor(color);\n}\n\nvoid ColorEditor::setCurrent(QColor color) {\n\t_current = color;\n\tupdate();\n}\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/color_editor.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"base/object_ptr.h\"\n\nclass ColorEditor : public Ui::RpWidget {\npublic:\n\tenum class Mode {\n\t\tRGBA,\n\t\tHSL,\n\t};\n\tColorEditor(\n\t\tQWidget *parent,\n\t\tMode mode,\n\t\tQColor current);\n\n\tvoid setLightnessLimits(int min, int max);\n\n\t[[nodiscard]] QColor color() const;\n\t[[nodiscard]] rpl::producer<QColor> colorValue() const;\n\t[[nodiscard]] rpl::producer<> submitRequests() const;\n\n\tvoid showColor(QColor color);\n\tvoid setCurrent(QColor color);\n\n\tvoid setInnerFocus() const;\n\nprotected:\n\tvoid prepare();\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\nprivate:\n\tstruct HSB { // HSV or HSL depending on Mode.\n\t\tint hue = 0;\n\t\tint saturation = 0;\n\t\tint brightness = 0;\n\t};\n\tvoid fieldSubmitted();\n\n\t[[nodiscard]] HSB hsbFromControls() const;\n\tvoid updateFromColor(QColor color);\n\tvoid updateControlsFromColor();\n\tvoid updateControlsFromHSB(HSB hsb);\n\tvoid updateHSBFields();\n\tvoid updateRGBFields();\n\tvoid updateResultField();\n\tvoid updateFromControls();\n\tvoid updateFromHSBFields();\n\tvoid updateFromRGBFields();\n\tvoid updateFromResultField();\n\tvoid setHSB(HSB hsb, int alpha);\n\tvoid setRGB(int red, int green, int blue, int alpha);\n\t[[nodiscard]] QColor applyLimits(QColor color) const;\n\n\tint percentFromByte(int byte) {\n\t\treturn std::clamp(qRound(byte * 100 / 255.), 0, 100);\n\t}\n\tint percentToByte(int percent) {\n\t\treturn std::clamp(qRound(percent * 255 / 100.), 0, 255);\n\t}\n\n\tclass Picker;\n\tclass Slider;\n\tclass Field;\n\tclass ResultField;\n\n\tMode _mode = Mode();\n\n\tobject_ptr<Picker> _picker;\n\tobject_ptr<Slider> _hueSlider = { nullptr };\n\tobject_ptr<Slider> _opacitySlider = { nullptr };\n\tobject_ptr<Slider> _lightnessSlider = { nullptr };\n\n\tobject_ptr<Field> _hueField;\n\tobject_ptr<Field> _saturationField;\n\tobject_ptr<Field> _brightnessField;\n\tobject_ptr<Field> _redField;\n\tobject_ptr<Field> _greenField;\n\tobject_ptr<Field> _blueField;\n\tobject_ptr<ResultField> _result;\n\n\tQBrush _transparent;\n\tQColor _current;\n\tQColor _new;\n\n\tQRect _currentRect;\n\tQRect _newRect;\n\n\tint _lightnessMin = 0;\n\tint _lightnessMax = 255;\n\n\trpl::event_stream<> _submitRequests;\n\trpl::event_stream<QColor> _newChanges;\n\n};\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/continuous_sliders.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/continuous_sliders.h\"\n\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"base/timer.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kByWheelFinishedTimeout = 1000;\n\n} // namespace\n\nContinuousSlider::ContinuousSlider(QWidget *parent) : RpWidget(parent) {\n\tsetCursor(style::cur_pointer);\n}\n\nvoid ContinuousSlider::setDisabled(bool disabled) {\n\tif (_disabled != disabled) {\n\t\t_disabled = disabled;\n\t\tsetCursor(_disabled ? style::cur_default : style::cur_pointer);\n\t\tupdate();\n\t}\n}\n\nvoid ContinuousSlider::setMoveByWheel(bool move) {\n\tif (move != moveByWheel()) {\n\t\tif (move) {\n\t\t\t_byWheelFinished = std::make_unique<base::Timer>([=] {\n\t\t\t\tif (_changeFinishedCallback) {\n\t\t\t\t\t_changeFinishedCallback(getCurrentValue());\n\t\t\t\t}\n\t\t\t});\n\t\t} else {\n\t\t\t_byWheelFinished = nullptr;\n\t\t}\n\t}\n}\n\nQRect ContinuousSlider::getSeekRect() const {\n\tconst auto decrease = getSeekDecreaseSize();\n\treturn isHorizontal()\n\t\t? QRect(decrease.width() / 2, 0, width() - decrease.width(), height())\n\t\t: QRect(0, decrease.height() / 2, width(), height() - decrease.width());\n}\n\nfloat64 ContinuousSlider::value() const {\n\treturn getCurrentValue();\n}\n\nvoid ContinuousSlider::setValue(float64 value) {\n\tsetValue(value, -1);\n}\n\nvoid ContinuousSlider::setValue(float64 value, float64 receivedTill) {\n\tif (_value != value || _receivedTill != receivedTill) {\n\t\t_value = value;\n\t\t_receivedTill = receivedTill;\n\t\tupdate();\n\t}\n}\n\nvoid ContinuousSlider::setFadeOpacity(float64 opacity) {\n\t_fadeOpacity = opacity;\n\tupdate();\n}\n\nvoid ContinuousSlider::mouseMoveEvent(QMouseEvent *e) {\n\tif (_mouseDown) {\n\t\tupdateDownValueFromPos(e->pos());\n\t}\n}\n\nfloat64 ContinuousSlider::computeValue(const QPoint &pos) const {\n\tconst auto seekRect = myrtlrect(getSeekRect());\n\tconst auto result = isHorizontal() ?\n\t\t(pos.x() - seekRect.x()) / float64(seekRect.width()) :\n\t\t(1. - (pos.y() - seekRect.y()) / float64(seekRect.height()));\n\tconst auto snapped = std::clamp(result, 0., 1.);\n\treturn _adjustCallback ? _adjustCallback(snapped) : snapped;\n}\n\nvoid ContinuousSlider::mousePressEvent(QMouseEvent *e) {\n\t_mouseDown = true;\n\t_downValue = computeValue(e->pos());\n\tupdate();\n\tif (_changeProgressCallback) {\n\t\t_changeProgressCallback(_downValue);\n\t}\n}\n\nvoid ContinuousSlider::mouseReleaseEvent(QMouseEvent *e) {\n\tif (_mouseDown) {\n\t\t_mouseDown = false;\n\t\tif (_changeFinishedCallback) {\n\t\t\t_changeFinishedCallback(_downValue);\n\t\t}\n\t\t_value = _downValue;\n\t\tupdate();\n\t}\n}\n\nvoid ContinuousSlider::wheelEvent(QWheelEvent *e) {\n\tif (_mouseDown || !moveByWheel()) {\n\t\treturn;\n\t}\n\tconstexpr auto step = static_cast<int>(QWheelEvent::DefaultDeltasPerStep);\n\tconstexpr auto coef = 1. / (step * 10.);\n\n\tauto deltaX = e->angleDelta().x(), deltaY = e->angleDelta().y();\n\tif (Platform::IsMac()) {\n\t\tdeltaY *= -1;\n\t} else {\n\t\tdeltaX *= -1;\n\t}\n\tauto delta = (qAbs(deltaX) > qAbs(deltaY)) ? deltaX : deltaY;\n\tauto finalValue = std::clamp(_value + delta * coef, 0., 1.);\n\tsetValue(finalValue);\n\tif (_changeProgressCallback) {\n\t\t_changeProgressCallback(finalValue);\n\t}\n\t_byWheelFinished->callOnce(kByWheelFinishedTimeout);\n}\n\nvoid ContinuousSlider::keyPressEvent(QKeyEvent *e) {\n\tconst auto changeBy = [&](float64 step) {\n\t\tExpects(step != 0.);\n\n\t\tauto steps = 0;\n\t\twhile (true) {\n\t\t\t++steps;\n\t\t\tauto result = _value + (steps * step);\n\t\t\tconst auto stopping = (result <= 0.) || (result >= 1.);\n\t\t\tif (_adjustCallback) {\n\t\t\t\tresult = _adjustCallback(result);\n\t\t\t}\n\t\t\tresult = std::clamp(result, 0., 1.);\n\t\t\tif (result != _value || stopping) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t};\n\n\tconst auto newValue = [&] {\n\t\tconstexpr auto kSmallStep = 0.01;\n\t\tconstexpr auto kLargeStep = 0.10;\n\t\tswitch (e->key()) {\n\t\tcase Qt::Key_Right:\n\t\tcase Qt::Key_Up: return changeBy(kSmallStep);\n\t\tcase Qt::Key_Left:\n\t\tcase Qt::Key_Down: return changeBy(-kSmallStep);\n\t\tcase Qt::Key_PageUp: return changeBy(kLargeStep);\n\t\tcase Qt::Key_PageDown: return changeBy(-kLargeStep);\n\t\tcase Qt::Key_Home: return changeBy(-1.);\n\t\tcase Qt::Key_End: return changeBy(1.);\n\t\tdefault: e->ignore();\n\t\t}\n\t\treturn _value;\n\t}();\n\n\tif (_value == newValue) {\n\t\treturn;\n\t}\n\tsetValue(newValue);\n\tif (_changeProgressCallback) {\n\t\t_changeProgressCallback(_value);\n\t}\n\tif (_changeFinishedCallback) {\n\t\t_changeFinishedCallback(_value);\n\t}\n\taccessibilityValueChanged();\n}\n\nvoid ContinuousSlider::updateDownValueFromPos(const QPoint &pos) {\n\t_downValue = computeValue(pos);\n\tupdate();\n\tif (_changeProgressCallback) {\n\t\t_changeProgressCallback(_downValue);\n\t}\n}\n\nvoid ContinuousSlider::enterEventHook(QEnterEvent *e) {\n\tsetOver(true);\n}\n\nvoid ContinuousSlider::leaveEventHook(QEvent *e) {\n\tsetOver(false);\n}\n\nvoid ContinuousSlider::setOver(bool over) {\n\tif (_over == over) return;\n\n\t_over = over;\n\tauto from = _over ? 0. : 1., to = _over ? 1. : 0.;\n\t_overAnimation.start([=] { update(); }, from, to, getOverDuration());\n}\n\nFilledSlider::FilledSlider(QWidget *parent, const style::FilledSlider &st) : ContinuousSlider(parent)\n, _st(st) {\n}\n\nQSize FilledSlider::getSeekDecreaseSize() const {\n\treturn QSize(0, 0);\n}\n\nfloat64 FilledSlider::getOverDuration() const {\n\treturn _st.duration;\n}\n\nvoid FilledSlider::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tPainterHighQualityEnabler hq(p);\n\n\tp.setPen(Qt::NoPen);\n\n\tconst auto masterOpacity = fadeOpacity();\n\tconst auto disabled = isDisabled();\n\tconst auto over = getCurrentOverFactor();\n\tconst auto lineWidth = _st.lineWidth + ((_st.fullWidth - _st.lineWidth) * over);\n\tconst auto lineWidthRounded = std::floor(lineWidth);\n\tconst auto lineWidthPartial = lineWidth - lineWidthRounded;\n\tconst auto seekRect = getSeekRect();\n\tconst auto value = getCurrentValue();\n\tconst auto from = seekRect.x();\n\tconst auto mid = qRound(from + value * seekRect.width());\n\tconst auto end = from + seekRect.width();\n\tif (mid > from) {\n\t\tp.setOpacity(masterOpacity);\n\t\tp.fillRect(from, height() - lineWidthRounded, (mid - from), lineWidthRounded, disabled ? _st.disabledFg : _st.activeFg);\n\t\tif (lineWidthPartial > 0.01) {\n\t\t\tp.setOpacity(masterOpacity * lineWidthPartial);\n\t\t\tp.fillRect(from, height() - lineWidthRounded - 1, (mid - from), 1, disabled ? _st.disabledFg : _st.activeFg);\n\t\t}\n\t}\n\tif (end > mid && over > 0) {\n\t\tp.setOpacity(masterOpacity * over);\n\t\tp.fillRect(mid, height() - lineWidthRounded, (end - mid), lineWidthRounded, _st.inactiveFg);\n\t\tif (lineWidthPartial > 0.01) {\n\t\t\tp.setOpacity(masterOpacity * over * lineWidthPartial);\n\t\t\tp.fillRect(mid, height() - lineWidthRounded - 1, (end - mid), 1, _st.inactiveFg);\n\t\t}\n\t}\n}\n\nMediaSlider::MediaSlider(QWidget *parent, const style::MediaSlider &st) : ContinuousSlider(parent)\n, _st(st) {\n}\n\nQSize MediaSlider::getSeekDecreaseSize() const {\n\treturn _alwaysDisplayMarker ? _st.seekSize : QSize();\n}\n\nfloat64 MediaSlider::getOverDuration() const {\n\treturn _st.duration;\n}\n\nvoid MediaSlider::disablePaint(bool disabled) {\n\t_paintDisabled = disabled;\n}\n\nvoid MediaSlider::addDivider(float64 atValue, const QSize &size) {\n\t_dividers.push_back(Divider{ atValue, size });\n\t_dividerExclusionSize = QSize();\n}\n\nvoid MediaSlider::clearDividers() {\n\t_dividers.clear();\n\t_dividerExclusion = QRegion();\n\t_dividerExclusionSize = QSize();\n}\n\nvoid MediaSlider::rebuildDividerExclusion() {\n\tconst auto currentSize = size();\n\tif (_dividerExclusionSize == currentSize) {\n\t\treturn;\n\t}\n\t_dividerExclusionSize = currentSize;\n\t_dividerExclusion = QRegion();\n\tif (_dividers.empty()) {\n\t\treturn;\n\t}\n\tconst auto horizontal = isHorizontal();\n\tconst auto from = 0;\n\tconst auto length = horizontal ? width() : height();\n\tfor (const auto &divider : _dividers) {\n\t\tconst auto dividerValue = horizontal\n\t\t\t? divider.atValue\n\t\t\t: (1. - divider.atValue);\n\t\tconst auto dividerMid = int(base::SafeRound(\n\t\t\tfrom + dividerValue * length));\n\t\tconst auto &s = divider.size;\n\t\t_dividerExclusion += horizontal\n\t\t\t? QRect(\n\t\t\t\tdividerMid - s.width() / 2,\n\t\t\t\t(height() - s.height()) / 2,\n\t\t\t\ts.width(),\n\t\t\t\ts.height())\n\t\t\t: QRect(\n\t\t\t\t(width() - s.height()) / 2,\n\t\t\t\tdividerMid - s.width() / 2,\n\t\t\t\ts.height(),\n\t\t\t\ts.width());\n\t}\n}\n\nvoid MediaSlider::setDividerStyle(DividerStyle style) {\n\t_dividerStyle = style;\n\t_dividerExclusionSize = QSize();\n\tupdate();\n}\n\nvoid MediaSlider::setColorOverrides(ColorOverrides overrides) {\n\t_overrides = std::move(overrides);\n\tupdate();\n}\n\nvoid MediaSlider::paintEvent(QPaintEvent *e) {\n\tif (_paintDisabled) {\n\t\treturn;\n\t}\n\tauto p = QPainter(this);\n\tPainterHighQualityEnabler hq(p);\n\n\tp.setPen(Qt::NoPen);\n\tp.setOpacity(fadeOpacity());\n\n\tconst auto horizontal = isHorizontal();\n\tconst auto borderWidth = _st.borderWidth;\n\tconst auto borderHalf = borderWidth / 2;\n\tconst auto radius = _st.width / 2;\n\tconst auto disabled = isDisabled();\n\tconst auto over = getCurrentOverFactor();\n\tconst auto seekRect = getSeekRect();\n\n\t// invert colors and value for vertical\n\tconst auto value = horizontal\n\t\t? getCurrentValue()\n\t\t: (1. - getCurrentValue());\n\n\t// receivedTill is not supported for vertical\n\tconst auto receivedTill = horizontal\n\t\t? getCurrentReceivedTill()\n\t\t: value;\n\n\tconst auto markerFrom = (horizontal ? seekRect.x() : seekRect.y());\n\tconst auto markerLength = horizontal\n\t\t? seekRect.width()\n\t\t: seekRect.height();\n\tconst auto from = 0;\n\tconst auto length = (horizontal ? width() : height());\n\tconst auto alwaysSeekSize = horizontal\n\t\t? _st.seekSize.width()\n\t\t: _st.seekSize.height();\n\tconst auto mid = _alwaysDisplayMarker\n\t\t? qRound(from\n\t\t\t+ (alwaysSeekSize / 2.)\n\t\t\t+ value * (length - alwaysSeekSize))\n\t\t: qRound(from + value * length);\n\tconst auto till = horizontal\n\t\t? std::max(mid, qRound(from + receivedTill * length))\n\t\t: mid;\n\tconst auto end = from + length;\n\tconst auto activeFg = disabled\n\t\t? _st.activeFgDisabled\n\t\t: _overrides.activeFg\n\t\t? QBrush(*_overrides.activeFg)\n\t\t: anim::brush(_st.activeFg, _st.activeFgOver, over);\n\tconst auto receivedTillFg = _st.receivedTillFg;\n\tconst auto inactiveFg = disabled\n\t\t? _st.inactiveFgDisabled\n\t\t: _overrides.inactiveFg\n\t\t? QBrush(*_overrides.inactiveFg)\n\t\t: anim::brush(_st.inactiveFg, _st.inactiveFgOver, over);\n\tconst auto borderFg = _st.borderFg;\n\tconst auto gapStyle = (_dividerStyle == DividerStyle::Gaps);\n\tif (gapStyle) {\n\t\trebuildDividerExclusion();\n\t}\n\tconst auto clip = [&](const QRect &rect) {\n\t\tif (!gapStyle || _dividerExclusion.isEmpty()) {\n\t\t\tp.setClipRect(rect);\n\t\t} else {\n\t\t\tp.setClipRegion(QRegion(rect) - _dividerExclusion);\n\t\t}\n\t};\n\tif (mid > from) {\n\t\tconst auto fromClipRect = horizontal\n\t\t\t? QRect(0, 0, mid, height())\n\t\t\t: QRect(0, 0, width(), mid);\n\t\tconst auto till = std::min(mid + radius, end);\n\t\tconst auto fromRect = horizontal\n\t\t\t? QRect(\n\t\t\t\tfrom + borderHalf,\n\t\t\t\t(height() - _st.width) / 2 + borderHalf,\n\t\t\t\ttill - from - borderWidth,\n\t\t\t\t_st.width - borderWidth)\n\t\t\t: QRect(\n\t\t\t\t(width() - _st.width) / 2 + borderHalf,\n\t\t\t\tfrom + borderHalf,\n\t\t\t\t_st.width - borderWidth,\n\t\t\t\ttill - from - borderWidth);\n\t\tclip(fromClipRect);\n\t\tif (borderWidth > 0) {\n\t\t\tconst auto borderPen = _overrides.activeBorder\n\t\t\t\t? QPen(*_overrides.activeBorder, borderWidth)\n\t\t\t\t: QPen(borderFg, borderWidth);\n\t\t\tconst auto bgBrush = _overrides.activeBg\n\t\t\t\t? QBrush(*_overrides.activeBg)\n\t\t\t\t: (horizontal ? borderFg : inactiveFg);\n\t\t\tp.setPen(borderPen);\n\t\t\tp.setBrush(bgBrush);\n\t\t} else {\n\t\t\tp.setPen(Qt::NoPen);\n\t\t\tp.setBrush(horizontal ? activeFg : inactiveFg);\n\t\t}\n\t\tp.drawRoundedRect(fromRect, radius, radius);\n\t}\n\tif (till > mid) {\n\t\tAssert(horizontal);\n\t\tauto clipRect = QRect(mid, 0, till - mid, height());\n\t\tconst auto left = std::max(mid - radius, from);\n\t\tconst auto right = std::min(till + radius, end);\n\t\tconst auto rect = QRect(\n\t\t\tleft,\n\t\t\t(height() - _st.width) / 2,\n\t\t\tright - left,\n\t\t\t_st.width);\n\t\tclip(clipRect);\n\t\tp.setBrush(receivedTillFg);\n\t\tp.drawRoundedRect(rect, radius, radius);\n\t}\n\tif (end > till) {\n\t\tconst auto endClipRect = horizontal\n\t\t\t? QRect(till, 0, width() - till, height())\n\t\t\t: QRect(0, till, width(), height() - till);\n\t\tconst auto begin = std::max(till - radius, from);\n\t\tconst auto endRect = horizontal\n\t\t\t? QRect(\n\t\t\t\tbegin + borderHalf,\n\t\t\t\t(height() - _st.width) / 2 + borderHalf,\n\t\t\t\tend - begin - borderWidth,\n\t\t\t\t_st.width - borderWidth)\n\t\t\t: QRect(\n\t\t\t\t(width() - _st.width) / 2 + borderHalf,\n\t\t\t\tbegin + borderHalf,\n\t\t\t\t_st.width - borderWidth,\n\t\t\t\tend - begin - borderWidth);\n\t\tclip(endClipRect);\n\t\tif (borderWidth > 0) {\n\t\t\tconst auto endBorderPen = _overrides.inactiveBorder\n\t\t\t\t? QPen(*_overrides.inactiveBorder, borderWidth)\n\t\t\t\t: QPen(borderFg, borderWidth);\n\t\t\tp.setPen(endBorderPen);\n\t\t} else {\n\t\t\tp.setPen(Qt::NoPen);\n\t\t}\n\t\tp.setBrush(horizontal ? inactiveFg : activeFg);\n\t\tp.drawRoundedRect(endRect, radius, radius);\n\t}\n\tif (!gapStyle && !_dividers.empty()) {\n\t\tp.setClipRect(rect());\n\t\tfor (const auto &divider : _dividers) {\n\t\t\tconst auto dividerValue = horizontal\n\t\t\t\t? divider.atValue\n\t\t\t\t: (1. - divider.atValue);\n\t\t\tconst auto dividerMid = base::SafeRound(from\n\t\t\t\t+ dividerValue * length);\n\t\t\tconst auto &size = divider.size;\n\t\t\tconst auto rect = horizontal\n\t\t\t\t? QRect(\n\t\t\t\t\tdividerMid - size.width() / 2,\n\t\t\t\t\t(height() - size.height()) / 2,\n\t\t\t\t\tsize.width(),\n\t\t\t\t\tsize.height())\n\t\t\t\t: QRect(\n\t\t\t\t\t(width() - size.height()) / 2,\n\t\t\t\t\tdividerMid - size.width() / 2,\n\t\t\t\t\tsize.height(),\n\t\t\t\t\tsize.width());\n\t\t\tp.setBrush(((value < dividerValue) == horizontal)\n\t\t\t\t? inactiveFg\n\t\t\t\t: activeFg);\n\t\t\tconst auto dividerRadius = size.width() / 2.;\n\t\t\tp.drawRoundedRect(rect, dividerRadius, dividerRadius);\n\t\t}\n\t}\n\tconst auto markerSizeRatio = disabled\n\t\t? 0.\n\t\t: (_alwaysDisplayMarker ? 1. : over);\n\tif (markerSizeRatio > 0) {\n\t\tconst auto position = qRound(markerFrom + value * markerLength)\n\t\t\t- (horizontal\n\t\t\t\t? (_st.seekSize.width() / 2)\n\t\t\t\t: (_st.seekSize.height() / 2));\n\t\tconst auto seekButton = horizontal\n\t\t\t? QRect(\n\t\t\t\tposition,\n\t\t\t\t(height() - _st.seekSize.height()) / 2,\n\t\t\t\t_st.seekSize.width(),\n\t\t\t\t_st.seekSize.height())\n\t\t\t: QRect(\n\t\t\t\t(width() - _st.seekSize.width()) / 2,\n\t\t\t\tposition,\n\t\t\t\t_st.seekSize.width(),\n\t\t\t\t_st.seekSize.height());\n\t\tconst auto size = horizontal\n\t\t\t? _st.seekSize.width()\n\t\t\t: _st.seekSize.height();\n\t\tconst auto remove = static_cast<int>(\n\t\t\t((1. - markerSizeRatio) * size) / 2.);\n\t\tif (remove * 2 < size) {\n\t\t\tp.setClipRect(rect());\n\t\t\tconst auto seekFg = _overrides.seekFg\n\t\t\t\t? QBrush(*_overrides.seekFg)\n\t\t\t\t: activeFg;\n\t\t\tif (borderWidth > 0) {\n\t\t\t\tconst auto seekBorderPen = _overrides.seekBorder\n\t\t\t\t\t? QPen(*_overrides.seekBorder, borderWidth)\n\t\t\t\t\t: QPen(borderFg, borderWidth);\n\t\t\t\tp.setPen(seekBorderPen);\n\t\t\t\tp.setBrush(seekFg);\n\t\t\t} else {\n\t\t\t\tp.setPen(Qt::NoPen);\n\t\t\t\tp.setBrush(seekFg);\n\t\t\t}\n\t\t\tconst auto xshift = horizontal\n\t\t\t\t? std::max(\n\t\t\t\t\tseekButton.x() + seekButton.width() - remove - width(),\n\t\t\t\t\t0) + std::min(seekButton.x() + remove, 0)\n\t\t\t\t: 0;\n\t\t\tconst auto yshift = horizontal\n\t\t\t\t? 0\n\t\t\t\t: std::max(\n\t\t\t\t\tseekButton.y() + seekButton.height() - remove - height(),\n\t\t\t\t\t0) + std::min(seekButton.y() + remove, 0);\n\t\t\tauto ellipseRect = (seekButton - Margins(remove)).translated(\n\t\t\t\t-xshift,\n\t\t\t\t-yshift);\n\t\t\tif (borderWidth > 0) {\n\t\t\t\tellipseRect -= Margins(borderHalf);\n\t\t\t}\n\t\t\tp.drawEllipse(ellipseRect);\n\t\t}\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/continuous_sliders.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace base {\nclass Timer;\n} // namespace base\n\nnamespace style {\nstruct FilledSlider;\nstruct MediaSlider;\n} // namespace style\n\nnamespace Ui {\n\nclass ContinuousSlider : public RpWidget {\npublic:\n\tContinuousSlider(QWidget *parent);\n\n\tenum class Direction {\n\t\tHorizontal,\n\t\tVertical,\n\t};\n\tvoid setDirection(Direction direction) {\n\t\t_direction = direction;\n\t\tupdate();\n\t}\n\n\tfloat64 value() const;\n\tvoid setValue(float64 value);\n\tvoid setValue(float64 value, float64 receivedTill);\n\tvoid setFadeOpacity(float64 opacity);\n\tvoid setDisabled(bool disabled);\n\tbool isDisabled() const {\n\t\treturn _disabled;\n\t}\n\n\tvoid setAdjustCallback(Fn<float64(float64)> callback) {\n\t\t_adjustCallback = std::move(callback);\n\t}\n\tvoid setChangeProgressCallback(Fn<void(float64)> callback) {\n\t\t_changeProgressCallback = std::move(callback);\n\t}\n\tvoid setChangeFinishedCallback(Fn<void(float64)> callback) {\n\t\t_changeFinishedCallback = std::move(callback);\n\t}\n\tbool isChanging() const {\n\t\treturn _mouseDown;\n\t}\n\n\tvoid setMoveByWheel(bool move);\n\n\tQAccessible::Role accessibilityRole() override {\n\t\treturn QAccessible::Role::Slider;\n\t}\n\n\tQString accessibilityValue() const override {\n\t\tconst auto percent = std::clamp(qRound(_value * 100.), 0, 100);\n\t\treturn QString::number(percent) + '%';\n\t}\n\nprotected:\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid wheelEvent(QWheelEvent *e) override;\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\n\tfloat64 fadeOpacity() const {\n\t\treturn _fadeOpacity;\n\t}\n\tfloat64 getCurrentValue() const {\n\t\treturn _mouseDown ? _downValue : _value;\n\t}\n\tfloat64 getCurrentReceivedTill() const {\n\t\treturn _receivedTill;\n\t}\n\tfloat64 getCurrentOverFactor() {\n\t\treturn _disabled ? 0. : _overAnimation.value(_over ? 1. : 0.);\n\t}\n\tDirection getDirection() const {\n\t\treturn _direction;\n\t}\n\tbool isHorizontal() const {\n\t\treturn (_direction == Direction::Horizontal);\n\t}\n\tQRect getSeekRect() const;\n\tvirtual QSize getSeekDecreaseSize() const = 0;\n\nprivate:\n\tvirtual float64 getOverDuration() const = 0;\n\n\tbool moveByWheel() const {\n\t\treturn _byWheelFinished != nullptr;\n\t}\n\n\tvoid setOver(bool over);\n\tfloat64 computeValue(const QPoint &pos) const;\n\tvoid updateDownValueFromPos(const QPoint &pos);\n\n\tDirection _direction = Direction::Horizontal;\n\tbool _disabled = false;\n\n\tstd::unique_ptr<base::Timer> _byWheelFinished;\n\n\tFn<float64(float64)> _adjustCallback;\n\tFn<void(float64)> _changeProgressCallback;\n\tFn<void(float64)> _changeFinishedCallback;\n\n\tbool _over = false;\n\tUi::Animations::Simple _overAnimation;\n\n\tfloat64 _value = 0.;\n\tfloat64 _receivedTill = 0.;\n\n\tbool _mouseDown = false;\n\tfloat64 _downValue = 0.;\n\n\tfloat64 _fadeOpacity = 1.;\n\n};\n\nclass FilledSlider : public ContinuousSlider {\npublic:\n\tFilledSlider(QWidget *parent, const style::FilledSlider &st);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tQSize getSeekDecreaseSize() const override;\n\tfloat64 getOverDuration() const override;\n\n\tconst style::FilledSlider &_st;\n\n};\n\nclass MediaSlider : public ContinuousSlider {\npublic:\n\tstruct ColorOverrides {\n\t\tstd::optional<QColor> activeFg;\n\t\tstd::optional<QColor> activeBg;\n\t\tstd::optional<QColor> activeBorder;\n\t\tstd::optional<QColor> seekFg;\n\t\tstd::optional<QColor> seekBorder;\n\t\tstd::optional<QColor> inactiveFg;\n\t\tstd::optional<QColor> inactiveBorder;\n\t};\n\n\tMediaSlider(QWidget *parent, const style::MediaSlider &st);\n\n\tvoid setAlwaysDisplayMarker(bool alwaysDisplayMarker) {\n\t\t_alwaysDisplayMarker = alwaysDisplayMarker;\n\t\tupdate();\n\t}\n\tvoid disablePaint(bool disabled);\n\n\ttemplate <\n\t\ttypename Value,\n\t\ttypename Convert,\n\t\ttypename Progress,\n\t\ttypename = std::enable_if_t<\n\t\t\trpl::details::is_callable_plain_v<Progress, Value>\n\t\t\t&& std::is_same_v<Value, decltype(std::declval<Convert>()(1))>>>\n\tvoid setPseudoDiscrete(\n\t\t\tint valuesCount,\n\t\t\tConvert &&convert,\n\t\t\tValue current,\n\t\t\tProgress &&progress,\n\t\t\tint indexMin = 0) {\n\t\tExpects(valuesCount > 1);\n\n\t\tsetAlwaysDisplayMarker(true);\n\t\tsetDirection(Ui::ContinuousSlider::Direction::Horizontal);\n\n\t\tconst auto sectionsCount = (valuesCount - 1);\n\t\tsetValue(1.);\n\t\tfor (auto index = index_type(); index != valuesCount; ++index) {\n\t\t\tif (current <= convert(index)) {\n\t\t\t\tsetValue(index / float64(sectionsCount));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tsetAdjustCallback([=](float64 value) {\n\t\t\treturn std::max(\n\t\t\t\tbase::SafeRound(value * sectionsCount),\n\t\t\t\tindexMin * 1.\n\t\t\t) / sectionsCount;\n\t\t});\n\t\tsetChangeProgressCallback([=](float64 value) {\n\t\t\tconst auto index = std::max(\n\t\t\t\tint(base::SafeRound(value * sectionsCount)),\n\t\t\t\tindexMin);\n\t\t\tprogress(convert(index));\n\t\t});\n\t}\n\n\ttemplate <\n\t\ttypename Value,\n\t\ttypename Convert,\n\t\ttypename Progress,\n\t\ttypename Finished,\n\t\ttypename = std::enable_if_t<\n\t\t\trpl::details::is_callable_plain_v<Progress, Value>\n\t\t\t&& rpl::details::is_callable_plain_v<Finished, Value>\n\t\t\t&& std::is_same_v<Value, decltype(std::declval<Convert>()(1))>>>\n\tvoid setPseudoDiscrete(\n\t\t\tint valuesCount,\n\t\t\tConvert &&convert,\n\t\t\tValue current,\n\t\t\tProgress &&progress,\n\t\t\tFinished &&finished,\n\t\t\tint indexMin = 0) {\n\t\tsetPseudoDiscrete(\n\t\t\tvaluesCount,\n\t\t\tstd::forward<Convert>(convert),\n\t\t\tcurrent,\n\t\t\tstd::forward<Progress>(progress),\n\t\t\tindexMin);\n\t\tsetChangeFinishedCallback([=](float64 value) {\n\t\t\tconst auto sectionsCount = (valuesCount - 1);\n\t\t\tconst auto index = std::max(\n\t\t\t\tint(base::SafeRound(value * sectionsCount)),\n\t\t\t\tindexMin);\n\t\t\tfinished(convert(index));\n\t\t});\n\t}\n\n\tenum class DividerStyle : uchar {\n\t\tMarks,\n\t\tGaps,\n\t};\n\n\tvoid setColorOverrides(ColorOverrides overrides);\n\tvoid addDivider(float64 atValue, const QSize &size);\n\tvoid clearDividers();\n\tvoid setDividerStyle(DividerStyle style);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tstruct Divider {\n\t\tconst float64 atValue;\n\t\tconst QSize size;\n\t};\n\n\tQSize getSeekDecreaseSize() const override;\n\tfloat64 getOverDuration() const override;\n\tvoid rebuildDividerExclusion();\n\n\tconst style::MediaSlider &_st;\n\tbool _alwaysDisplayMarker = false;\n\tbool _paintDisabled = false;\n\n\tstd::vector<Divider> _dividers;\n\tDividerStyle _dividerStyle = DividerStyle::Marks;\n\tQRegion _dividerExclusion;\n\tQSize _dividerExclusionSize;\n\tColorOverrides _overrides;\n\n};\n\nclass MediaSliderWheelless : public MediaSlider {\npublic:\n\tusing Ui::MediaSlider::MediaSlider;\n\nprotected:\n\tvoid wheelEvent(QWheelEvent *e) override {\n\t\te->ignore();\n\t}\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/cross_fade_label.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/cross_fade_label.h\"\n\n#include \"styles/style_widgets.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kDuration = crl::time(200);\n\n} // namespace\n\nCrossFadeLabel::CrossFadeLabel(\n\tQWidget *parent,\n\tconst style::LabelSimple &st)\n: RpWidget(parent)\n, _st(st) {\n}\n\nvoid CrossFadeLabel::setText(\n\t\tconst QString &text,\n\t\tanim::type animated) {\n\tif (_current == text) {\n\t\treturn;\n\t}\n\t_previous = _current;\n\t_previousWidth = _currentWidth;\n\t_current = text;\n\t_currentWidth = _st.font->width(_current);\n\tif (!height()) {\n\t\tresize(width(), _st.font->height);\n\t}\n\tif (animated == anim::type::instant) {\n\t\t_animation.stop();\n\t\tupdate();\n\t\treturn;\n\t}\n\t_animation.start(\n\t\t[=] { update(); },\n\t\t0.,\n\t\t1.,\n\t\tkDuration,\n\t\tanim::easeOutCubic);\n}\n\nvoid CrossFadeLabel::setDirection(int direction) {\n\t_direction = direction;\n}\n\nvoid CrossFadeLabel::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tp.setPen(_st.textFg);\n\tp.setFont(_st.font);\n\tconst auto slide = _st.font->height;\n\tconst auto progress = _animation.value(1.);\n\tconst auto y = _st.font->ascent;\n\tif (progress < 1. && !_previous.isEmpty()) {\n\t\tp.setOpacity(1. - progress);\n\t\tconst auto dx = int(slide * _direction * progress);\n\t\tp.drawText(\n\t\t\t(width() - _previousWidth) / 2 + dx,\n\t\t\ty,\n\t\t\t_previous);\n\t}\n\tif (!_current.isEmpty()) {\n\t\tp.setOpacity(progress);\n\t\tconst auto dx = int(slide * -_direction * (1. - progress));\n\t\tp.drawText(\n\t\t\t(width() - _currentWidth) / 2 + dx,\n\t\t\ty,\n\t\t\t_current);\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/cross_fade_label.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace style {\nstruct LabelSimple;\n} // namespace style\n\nnamespace Ui {\n\nclass CrossFadeLabel final : public RpWidget {\npublic:\n\tCrossFadeLabel(\n\t\tQWidget *parent,\n\t\tconst style::LabelSimple &st);\n\n\tvoid setText(const QString &text, anim::type animated = anim::type::normal);\n\tvoid setDirection(int direction);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tconst style::LabelSimple &_st;\n\n\tQString _current;\n\tQString _previous;\n\tint _currentWidth = 0;\n\tint _previousWidth = 0;\n\tint _direction = 1;\n\n\tAnimations::Simple _animation;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/discrete_sliders.h\"\n\n#include \"ui/effects/ripple_animation.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Ui {\n\nDiscreteSlider::DiscreteSlider(QWidget *parent, bool snapToLabel)\n: RpWidget(parent)\n, _snapToLabel(snapToLabel) {\n\tsetCursor(style::cur_pointer);\n}\n\nDiscreteSlider::~DiscreteSlider() = default;\n\nvoid DiscreteSlider::setActiveSection(int index) {\n\t_activeIndex = index;\n\tactivateCallback();\n\tsetSelectedSection(index);\n}\n\nvoid DiscreteSlider::activateCallback() {\n\tif (_timerId >= 0) {\n\t\tkillTimer(_timerId);\n\t\t_timerId = -1;\n\t}\n\tauto ms = crl::now();\n\tif (ms >= _callbackAfterMs) {\n\t\t_sectionActivated.fire_copy(_activeIndex);\n\t} else {\n\t\t_timerId = startTimer(_callbackAfterMs - ms, Qt::PreciseTimer);\n\t}\n}\n\nvoid DiscreteSlider::timerEvent(QTimerEvent *e) {\n\tactivateCallback();\n}\n\nvoid DiscreteSlider::setActiveSectionFast(int index) {\n\tsetActiveSection(index);\n\tfinishAnimating();\n}\n\nvoid DiscreteSlider::finishAnimating() {\n\t_a_left.stop();\n\t_a_width.stop();\n\tupdate();\n\t_callbackAfterMs = 0;\n\tif (_timerId >= 0) {\n\t\tactivateCallback();\n\t}\n}\n\nvoid DiscreteSlider::selectSection(int index) {\n\tif (index < 0 || index >= _sections.size()) {\n\t\tfor (auto &other : _sections) {\n\t\t\tif (other.ripple) {\n\t\t\t\tother.ripple->lastStop();\n\t\t\t}\n\t\t}\n\t\treturn;\n\t}\n\tauto &section = _sections[index];\n\tif (section.ripple && !section.ripple->empty()) {\n\t\treturn;\n\t}\n\tfor (auto &other : _sections) {\n\t\tif (other.ripple) {\n\t\t\tother.ripple->lastStop();\n\t\t}\n\t}\n\tstartRipple(index);\n}\n\nvoid DiscreteSlider::setAdditionalContentWidthToSection(int index, int w) {\n\tif (index >= 0 && index < _sections.size()) {\n\t\tauto &section = _sections[index];\n\t\tsection.contentWidth = section.label.maxWidth() + w;\n\t}\n}\n\nint DiscreteSlider::sectionsCount() const {\n\treturn int(_sections.size());\n}\n\nint DiscreteSlider::lookupSectionLeft(int index) const {\n\tExpects(index >= 0 && index < _sections.size());\n\n\treturn _sections[index].left;\n}\n\nvoid DiscreteSlider::setSelectOnPress(bool selectOnPress) {\n\t_selectOnPress = selectOnPress;\n}\n\nbool DiscreteSlider::paused() const {\n\treturn _paused && _paused();\n}\n\nstd::vector<DiscreteSlider::Section> &DiscreteSlider::sectionsRef() {\n\treturn _sections;\n}\n\nvoid DiscreteSlider::addSection(const QString &label) {\n\t_sections.push_back(Section(label, getLabelStyle()));\n\tresizeToWidth(width());\n}\n\nvoid DiscreteSlider::addSection(\n\t\tconst TextWithEntities &label,\n\t\tText::MarkedContext context) {\n\tcontext.repaint = [this] { update(); };\n\t_sections.push_back(Section(label, getLabelStyle(), context));\n\tresizeToWidth(width());\n}\n\nvoid DiscreteSlider::setSections(const std::vector<QString> &labels) {\n\tAssert(!labels.empty());\n\n\t_sections.clear();\n\tfor (const auto &label : labels) {\n\t\t_sections.push_back(Section(label, getLabelStyle()));\n\t}\n\trefresh();\n}\n\nvoid DiscreteSlider::setSections(\n\t\tconst std::vector<TextWithEntities> &labels,\n\t\tText::MarkedContext context,\n\t\tFn<bool()> paused) {\n\tAssert(!labels.empty());\n\n\tcontext.repaint = [this] { update(); };\n\n\t_sections.clear();\n\tfor (const auto &label : labels) {\n\t\t_sections.push_back(Section(label, getLabelStyle(), context));\n\t}\n\t_paused = std::move(paused);\n\trefresh();\n}\n\nvoid DiscreteSlider::refresh() {\n\tstopAnimation();\n\tif (_activeIndex >= _sections.size()) {\n\t\t_activeIndex = 0;\n\t}\n\tif (_selected >= _sections.size()) {\n\t\t_selected = 0;\n\t}\n\tresizeToWidth(width());\n\tupdate();\n}\n\nDiscreteSlider::Range DiscreteSlider::getFinalActiveRange() const {\n\tconst auto raw = (_sections.empty() || _selected < 0)\n\t\t? nullptr\n\t\t: &_sections[_selected];\n\tif (!raw) {\n\t\treturn { 0, 0 };\n\t}\n\tconst auto width = _snapToLabel\n\t\t? std::min(raw->width, raw->contentWidth)\n\t\t: raw->width;\n\treturn { raw->left + ((raw->width - width) / 2), width };\n}\n\nDiscreteSlider::Range DiscreteSlider::getCurrentActiveRange() const {\n\tconst auto to = getFinalActiveRange();\n\treturn {\n\t\tint(base::SafeRound(_a_left.value(to.left))),\n\t\tint(base::SafeRound(_a_width.value(to.width))),\n\t};\n}\n\nvoid DiscreteSlider::enumerateSections(Fn<bool(Section&)> callback) {\n\tfor (auto &section : _sections) {\n\t\tif (!callback(section)) {\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nvoid DiscreteSlider::enumerateSections(\n\t\tFn<bool(const Section&)> callback) const {\n\tfor (const auto &section : _sections) {\n\t\tif (!callback(section)) {\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nvoid DiscreteSlider::mousePressEvent(QMouseEvent *e) {\n\tconst auto index = getIndexFromPosition(e->pos());\n\tif (_selectOnPress) {\n\t\tsetSelectedSection(index);\n\t}\n\tstartRipple(index);\n\t_pressed = index;\n}\n\nvoid DiscreteSlider::mouseMoveEvent(QMouseEvent *e) {\n\tif (_pressed < 0) {\n\t\treturn;\n\t}\n\tif (_selectOnPress) {\n\t\tsetSelectedSection(getIndexFromPosition(e->pos()));\n\t}\n}\n\nvoid DiscreteSlider::mouseReleaseEvent(QMouseEvent *e) {\n\tconst auto pressed = std::exchange(_pressed, -1);\n\tif (pressed < 0) {\n\t\treturn;\n\t}\n\n\tconst auto index = getIndexFromPosition(e->pos());\n\tif (pressed < _sections.size()) {\n\t\tif (_sections[pressed].ripple) {\n\t\t\t_sections[pressed].ripple->lastStop();\n\t\t}\n\t}\n\tif (_selectOnPress || index == pressed) {\n\t\tsetActiveSection(index);\n\t}\n}\n\nvoid DiscreteSlider::setSelectedSection(int index) {\n\tif (index >= int(_sections.size())) {\n\t\treturn;\n\t}\n\n\tif (_selected != index) {\n\t\tconst auto from = getFinalActiveRange();\n\t\t_selected = index;\n\t\tconst auto to = getFinalActiveRange();\n\t\tconst auto duration = getAnimationDuration();\n\t\tconst auto updater = [this] { update(); };\n\t\t_a_left.start(updater, from.left, to.left, duration);\n\t\t_a_width.start(updater, from.width, to.width, duration);\n\t\t_callbackAfterMs = crl::now() + duration;\n\t}\n}\n\nint DiscreteSlider::getIndexFromPosition(QPoint pos) {\n\tconst auto count = _sections.size();\n\tfor (auto i = 0; i != count; ++i) {\n\t\tif (_sections[i].left + _sections[i].width > pos.x()) {\n\t\t\treturn i;\n\t\t}\n\t}\n\treturn count - 1;\n}\n\nDiscreteSlider::Section::Section(\n\tconst QString &label,\n\tconst style::TextStyle &st)\n: label(st, label)\n, contentWidth(Section::label.maxWidth()) {\n}\n\nDiscreteSlider::Section::Section(\n\t\tconst TextWithEntities &label,\n\t\tconst style::TextStyle &st,\n\t\tconst Text::MarkedContext &context) {\n\tthis->label.setMarkedText(st, label, kMarkupTextOptions, context);\n\tcontentWidth = Section::label.maxWidth();\n}\n\nSettingsSlider::SettingsSlider(\n\tQWidget *parent,\n\tconst style::SettingsSlider &st)\n: DiscreteSlider(parent, st.barSnapToLabel)\n, _st(st) {\n\tif (_st.barRadius > 0) {\n\t\t_bar.emplace(_st.barRadius, _st.barFg);\n\t\t_barActive.emplace(_st.barRadius, _st.barFgActive);\n\t}\n\tsetSelectOnPress(_st.ripple.showDuration == 0);\n}\n\nconst style::SettingsSlider &SettingsSlider::st() const {\n\treturn _st;\n}\n\nint SettingsSlider::centerOfSection(int section) const {\n\tconst auto widths = countSectionsWidths(0);\n\tauto result = 0;\n\tif (section >= 0 && section < widths.size()) {\n\t\tfor (auto i = 0; i < section; i++) {\n\t\t\tresult += widths[i];\n\t\t}\n\t\tresult += widths[section] / 2;\n\t}\n\treturn result;\n}\n\nvoid SettingsSlider::fitWidthToSections() {\n\tconst auto widths = countSectionsWidths(0);\n\tresizeToWidth(ranges::accumulate(widths, .0) + _st.padding * 2);\n}\n\nvoid SettingsSlider::setRippleTopRoundRadius(int radius) {\n\t_rippleTopRoundRadius = radius;\n}\n\nconst style::TextStyle &SettingsSlider::getLabelStyle() const {\n\treturn _st.labelStyle;\n}\n\nint SettingsSlider::getAnimationDuration() const {\n\treturn _st.duration;\n}\n\nvoid SettingsSlider::resizeSections(int newWidth) {\n\tconst auto count = getSectionsCount();\n\tif (!count) {\n\t\treturn;\n\t}\n\n\tconst auto sectionWidths = countSectionsWidths(newWidth);\n\n\tauto skip = 0;\n\tauto x = _st.padding * 1.;\n\tauto sectionWidth = sectionWidths.begin();\n\tenumerateSections([&](Section &section) {\n\t\tExpects(sectionWidth != sectionWidths.end());\n\n\t\tsection.left = std::floor(x) + skip;\n\t\tx += *sectionWidth;\n\t\tsection.width = qRound(x) - (section.left - skip);\n\t\tskip += _st.barSkip;\n\t\t++sectionWidth;\n\t\treturn true;\n\t});\n\tstopAnimation();\n}\n\nstd::vector<float64> SettingsSlider::countSectionsWidths(int newWidth) const {\n\tconst auto count = getSectionsCount();\n\tconst auto sectionsWidth = newWidth\n\t\t- 2 * _st.padding\n\t\t- (count - 1) * _st.barSkip;\n\tconst auto sectionWidth = sectionsWidth / float64(count);\n\n\tauto result = std::vector<float64>(count, sectionWidth);\n\tauto labelsWidth = 0;\n\tauto commonWidth = true;\n\tenumerateSections([&](const Section &section) {\n\t\tlabelsWidth += section.contentWidth;\n\t\tif (section.contentWidth >= sectionWidth) {\n\t\t\tcommonWidth = false;\n\t\t}\n\t\treturn true;\n\t});\n\t// If labelsWidth > sectionsWidth we're screwed anyway.\n\tif (_st.strictSkip || (!commonWidth && labelsWidth <= sectionsWidth)) {\n\t\tauto padding = _st.strictSkip\n\t\t\t? (_st.strictSkip / 2.)\n\t\t\t: (sectionsWidth - labelsWidth) / (2. * count);\n\t\tauto currentWidth = result.begin();\n\t\tenumerateSections([&](const Section &section) {\n\t\t\tExpects(currentWidth != result.end());\n\n\t\t\t*currentWidth = padding + section.contentWidth + padding;\n\t\t\t++currentWidth;\n\t\t\treturn true;\n\t\t});\n\t}\n\treturn result;\n}\n\nint SettingsSlider::resizeGetHeight(int newWidth) {\n\tresizeSections(newWidth);\n\treturn _st.height;\n}\n\nvoid SettingsSlider::startRipple(int sectionIndex) {\n\tif (!_st.ripple.showDuration) {\n\t\treturn;\n\t}\n\tauto index = 0;\n\tenumerateSections([this, &index, sectionIndex](Section &section) {\n\t\tif (index++ == sectionIndex) {\n\t\t\tif (!section.ripple) {\n\t\t\t\tauto mask = prepareRippleMask(sectionIndex, section);\n\t\t\t\tsection.ripple = std::make_unique<RippleAnimation>(\n\t\t\t\t\t_st.ripple,\n\t\t\t\t\tstd::move(mask),\n\t\t\t\t\t[this] { update(); });\n\t\t\t}\n\t\t\tconst auto point = mapFromGlobal(QCursor::pos());\n\t\t\tsection.ripple->add(point - QPoint(section.left, 0));\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n}\n\nQImage SettingsSlider::prepareRippleMask(\n\t\tint sectionIndex,\n\t\tconst Section &section) {\n\tconst auto size = QSize(section.width, height() - _st.rippleBottomSkip);\n\tif (!_rippleTopRoundRadius\n\t\t|| (sectionIndex > 0 && sectionIndex + 1 < getSectionsCount())) {\n\t\treturn RippleAnimation::RectMask(size);\n\t}\n\treturn RippleAnimation::MaskByDrawer(size, false, [&](QPainter &p) {\n\t\tconst auto plusRadius = _rippleTopRoundRadius + 1;\n\t\tp.drawRoundedRect(\n\t\t\t0,\n\t\t\t0,\n\t\t\tsection.width,\n\t\t\theight() + plusRadius,\n\t\t\t_rippleTopRoundRadius,\n\t\t\t_rippleTopRoundRadius);\n\t\tif (sectionIndex > 0) {\n\t\t\tp.fillRect(0, 0, plusRadius, plusRadius, p.brush());\n\t\t}\n\t\tif (sectionIndex + 1 < getSectionsCount()) {\n\t\t\tp.fillRect(\n\t\t\t\tsection.width - plusRadius,\n\t\t\t\t0,\n\t\t\t\tplusRadius,\n\t\t\t\tplusRadius, p.brush());\n\t\t}\n\t});\n}\n\nvoid SettingsSlider::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tconst auto clip = e->rect();\n\tconst auto range = DiscreteSlider::getCurrentActiveRange();\n\n\tconst auto drawRect = [&](QRect rect, bool active = false) {\n\t\tconst auto &bar = active ? _barActive : _bar;\n\t\tif (bar) {\n\t\t\tbar->paint(p, rect);\n\t\t} else {\n\t\t\tp.fillRect(rect, active ? _st.barFgActive : _st.barFg);\n\t\t}\n\t};\n\tenumerateSections([&](Section &section) {\n\t\tconst auto activeWidth = _st.barSnapToLabel\n\t\t\t? section.contentWidth\n\t\t\t: section.width;\n\t\tconst auto activeLeft = section.left\n\t\t\t+ (section.width - activeWidth) / 2;\n\t\tconst auto divider = std::max(std::min(activeWidth, range.width), 1);\n\t\tconst auto active = 1.\n\t\t\t- std::clamp(\n\t\t\t\tstd::abs(range.left - activeLeft) / float64(divider),\n\t\t\t\t0.,\n\t\t\t\t1.);\n\t\tif (section.ripple) {\n\t\t\tconst auto color = anim::color(\n\t\t\t\t_st.rippleBg,\n\t\t\t\t_st.rippleBgActive,\n\t\t\t\tactive);\n\t\t\tsection.ripple->paint(p, section.left, 0, width(), &color);\n\t\t\tif (section.ripple->empty()) {\n\t\t\t\tsection.ripple.reset();\n\t\t\t}\n\t\t}\n\t\tif (!_st.barSnapToLabel) {\n\t\t\tauto from = activeLeft;\n\t\t\tauto tofill = activeWidth;\n\t\t\tif (range.left > from) {\n\t\t\t\tconst auto fill = std::min(tofill, range.left - from);\n\t\t\t\tdrawRect(myrtlrect(from, _st.barTop, fill, _st.barStroke));\n\t\t\t\tfrom += fill;\n\t\t\t\ttofill -= fill;\n\t\t\t}\n\t\t\tif (range.left + activeWidth > from) {\n\t\t\t\tconst auto fill = std::min(\n\t\t\t\t\ttofill,\n\t\t\t\t\trange.left + activeWidth - from);\n\t\t\t\tif (fill) {\n\t\t\t\t\tdrawRect(\n\t\t\t\t\t\tmyrtlrect(from, _st.barTop, fill, _st.barStroke),\n\t\t\t\t\t\ttrue);\n\t\t\t\t\tfrom += fill;\n\t\t\t\t\ttofill -= fill;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (tofill) {\n\t\t\t\tdrawRect(myrtlrect(from, _st.barTop, tofill, _st.barStroke));\n\t\t\t}\n\t\t}\n\t\tconst auto labelLeft = section.left\n\t\t\t+ (section.width - section.contentWidth) / 2;\n\t\tconst auto rect = myrtlrect(\n\t\t\tlabelLeft,\n\t\t\t_st.labelTop,\n\t\t\tsection.contentWidth,\n\t\t\t_st.labelStyle.font->height);\n\t\tif (rect.intersects(clip)) {\n\t\t\tp.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active));\n\t\t\tsection.label.draw(p, {\n\t\t\t\t.position = QPoint(labelLeft, _st.labelTop),\n\t\t\t\t.outerWidth = width(),\n\t\t\t\t.availableWidth = section.label.maxWidth(),\n\t\t\t\t.paused = paused(),\n\t\t\t});\n\t\t}\n\t\treturn true;\n\t});\n\tif (_st.barSnapToLabel) {\n\t\tconst auto add = _st.barStroke / 2;\n\t\tconst auto from = std::max(range.left - add, 0);\n\t\tconst auto till = std::min(range.left + range.width + add, width());\n\t\tif (from < till) {\n\t\t\tdrawRect(\n\t\t\t\tmyrtlrect(from, _st.barTop, till - from, _st.barStroke),\n\t\t\t\ttrue);\n\t\t}\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/discrete_sliders.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/round_rect.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/text/text.h\"\n\nnamespace style {\nstruct TextStyle;\nstruct SettingsSlider;\n} // namespace style\n\nnamespace st {\nextern const style::SettingsSlider &defaultSettingsSlider;\n} // namespace st\n\nnamespace Ui {\n\nclass RippleAnimation;\n\nclass DiscreteSlider : public RpWidget {\npublic:\n\tDiscreteSlider(QWidget *parent, bool snapToLabel);\n\t~DiscreteSlider();\n\n\tvoid addSection(const QString &label);\n\tvoid addSection(\n\t\tconst TextWithEntities &label,\n\t\tText::MarkedContext context = {});\n\tvoid setSections(const std::vector<QString> &labels);\n\tvoid setSections(\n\t\tconst std::vector<TextWithEntities> &labels,\n\t\tText::MarkedContext context = {},\n\t\tFn<bool()> paused = nullptr);\n\tint activeSection() const {\n\t\treturn _activeIndex;\n\t}\n\tvoid setActiveSection(int index);\n\tvoid setActiveSectionFast(int index);\n\tvoid finishAnimating();\n\tvoid selectSection(int index);\n\n\tvoid setAdditionalContentWidthToSection(int index, int width);\n\n\t[[nodiscard]] rpl::producer<int> sectionActivated() const {\n\t\treturn _sectionActivated.events();\n\t}\n\n\t[[nodiscard]] int sectionsCount() const;\n\t[[nodiscard]] int lookupSectionLeft(int index) const;\n\nprotected:\n\tvoid timerEvent(QTimerEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\n\tint resizeGetHeight(int newWidth) override = 0;\n\n\tstruct Section {\n\t\tSection(const QString &label, const style::TextStyle &st);\n\t\tSection(\n\t\t\tconst TextWithEntities &label,\n\t\t\tconst style::TextStyle &st,\n\t\t\tconst Text::MarkedContext &context);\n\n\t\tText::String label;\n\t\tstd::unique_ptr<RippleAnimation> ripple;\n\t\tint left = 0;\n\t\tint width = 0;\n\t\tint contentWidth = 0;\n\t};\n\tstruct Range {\n\t\tint left = 0;\n\t\tint width = 0;\n\t};\n\n\t[[nodiscard]] Range getFinalActiveRange() const;\n\t[[nodiscard]] Range getCurrentActiveRange() const;\n\n\t[[nodiscard]] int getSectionsCount() const {\n\t\treturn _sections.size();\n\t}\n\n\tvoid enumerateSections(Fn<bool(Section&)> callback);\n\tvoid enumerateSections(Fn<bool(const Section&)> callback) const;\n\n\tvirtual void startRipple(int sectionIndex) {\n\t}\n\n\tvoid stopAnimation() {\n\t\t_a_left.stop();\n\t\t_a_width.stop();\n\t}\n\tvoid refresh();\n\n\tvoid setSelectOnPress(bool selectOnPress);\n\n\t[[nodiscard]] std::vector<Section> &sectionsRef();\n\n\t[[nodiscard]] bool paused() const;\n\nprivate:\n\tvoid activateCallback();\n\tvirtual const style::TextStyle &getLabelStyle() const = 0;\n\tvirtual int getAnimationDuration() const = 0;\n\n\tint getIndexFromPosition(QPoint pos);\n\tvoid setSelectedSection(int index);\n\n\tstd::vector<Section> _sections;\n\tFn<bool()> _paused;\n\tint _activeIndex = 0;\n\tbool _selectOnPress = true;\n\tbool _snapToLabel = false;\n\n\trpl::event_stream<int> _sectionActivated;\n\n\tint _pressed = -1;\n\tint _selected = 0;\n\tUi::Animations::Simple _a_left;\n\tUi::Animations::Simple _a_width;\n\n\tint _timerId = -1;\n\tcrl::time _callbackAfterMs = 0;\n\n};\n\nclass SettingsSlider : public DiscreteSlider {\npublic:\n\tSettingsSlider(\n\t\tQWidget *parent,\n\t\tconst style::SettingsSlider &st = st::defaultSettingsSlider);\n\n\t[[nodiscard]] const style::SettingsSlider &st() const;\n\n\t[[nodiscard]] int centerOfSection(int section) const;\n\tvirtual void fitWidthToSections();\n\n\tvoid setRippleTopRoundRadius(int radius);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid startRipple(int sectionIndex) override;\n\n\tstd::vector<float64> countSectionsWidths(int newWidth) const;\n\nprivate:\n\tconst style::TextStyle &getLabelStyle() const override;\n\tint getAnimationDuration() const override;\n\tQImage prepareRippleMask(int sectionIndex, const Section &section);\n\n\tvoid resizeSections(int newWidth);\n\n\tconst style::SettingsSlider &_st;\n\tstd::optional<Ui::RoundRect> _bar;\n\tstd::optional<Ui::RoundRect> _barActive;\n\tint _rippleTopRoundRadius = 0;\n\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/expandable_peer_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/expandable_peer_list.h\"\n\n#include \"data/data_peer.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"lang/lang_text_entity.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/rect.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/participants_check_view.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Ui {\nnamespace {\n\nclass Button final : public Ui::RippleButton {\npublic:\n\tButton(not_null<QWidget*> parent, int count);\n\n\t[[nodiscard]] not_null<Ui::AbstractCheckView*> checkView() const;\n\nprivate:\n\tvoid paintEvent(QPaintEvent *event) override;\n\tQImage prepareRippleMask() const override;\n\tQPoint prepareRippleStartPosition() const override;\n\n\tstd::unique_ptr<Ui::AbstractCheckView> _view;\n\n};\n\nButton::Button(not_null<QWidget*> parent, int count)\n: Ui::RippleButton(parent, st::defaultRippleAnimation)\n, _view(std::make_unique<Ui::ParticipantsCheckView>(\n\tcount,\n\tst::slideWrapDuration,\n\tfalse,\n\t[=] { update(); })) {\n}\n\nnot_null<Ui::AbstractCheckView*> Button::checkView() const {\n\treturn _view.get();\n}\n\nQImage Button::prepareRippleMask() const {\n\treturn _view->prepareRippleMask();\n}\n\nQPoint Button::prepareRippleStartPosition() const {\n\treturn mapFromGlobal(QCursor::pos());\n}\n\nvoid Button::paintEvent(QPaintEvent *event) {\n\tauto p = QPainter(this);\n\tUi::RippleButton::paintRipple(p, QPoint());\n\t_view->paint(p, 0, 0, width());\n}\n\n} // namespace\n\nvoid AddExpandablePeerList(\n\t\tnot_null<Ui::Checkbox*> checkbox,\n\t\tnot_null<ExpandablePeerListController*> controller,\n\t\tnot_null<Ui::VerticalLayout*> inner) {\n\tconst auto &participants = controller->data.participants;\n\tconst auto hideRightButton = controller->data.hideRightButton;\n\tconst auto checkTopOnAllInner = controller->data.checkTopOnAllInner;\n\tconst auto isSingle = controller->data.skipSingle\n\t\t? false\n\t\t: (participants.size() == 1);\n\tif (isSingle) {\n\t\tconst auto p = participants.front();\n\t\tcontroller->collectRequests = [=] { return Participants{ p }; };\n\t\treturn;\n\t}\n\tconst auto count = int(participants.size());\n\tconst auto button = !hideRightButton\n\t\t? Ui::CreateChild<Button>(inner, count)\n\t\t: nullptr;\n\tif (button) {\n\t\tbutton->resize(Ui::ParticipantsCheckView::ComputeSize(count));\n\t}\n\n\tconst auto overlay = Ui::CreateChild<Ui::AbstractButton>(inner);\n\n\tcheckbox->geometryValue(\n\t) | rpl::on_next([=](const QRect &rect) {\n\t\toverlay->setGeometry(rect);\n\t\toverlay->raise();\n\n\t\tif (button) {\n\t\t\tbutton->moveToRight(\n\t\t\t\tst::moderateBoxExpandRight,\n\t\t\t\trect.top() + (rect.height() - button->height()) / 2,\n\t\t\t\tinner->width());\n\t\t\tbutton->raise();\n\t\t}\n\t}, overlay->lifetime());\n\n\tcontroller->toggleRequestsFromInner.events(\n\t) | rpl::on_next([=](bool toggled) {\n\t\tcheckbox->setChecked(toggled);\n\t}, checkbox->lifetime());\n\tif (button) {\n\t\tbutton->setClickedCallback([=] {\n\t\t\tbutton->checkView()->setChecked(\n\t\t\t\t!button->checkView()->checked(),\n\t\t\t\tanim::type::normal);\n\t\t\tcontroller->toggleRequestsFromTop.fire_copy(\n\t\t\t\tbutton->checkView()->checked());\n\t\t});\n\t}\n\toverlay->setClickedCallback([=] {\n\t\tcheckbox->setChecked(!checkbox->checked());\n\t\tcontroller->checkAllRequests.fire_copy(checkbox->checked());\n\t});\n\t{\n\t\tconst auto wrap = inner->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tinner,\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(inner)));\n\t\twrap->toggle(hideRightButton, anim::type::instant);\n\n\t\tcontroller->toggleRequestsFromTop.events(\n\t\t) | rpl::on_next([=](bool toggled) {\n\t\t\twrap->toggle(toggled, anim::type::normal);\n\t\t}, wrap->lifetime());\n\n\t\tconst auto container = wrap->entity();\n\t\tUi::AddSkip(container);\n\n\t\tauto &lifetime = wrap->lifetime();\n\t\tconst auto clicks = lifetime.make_state<rpl::event_stream<>>();\n\t\tconst auto checkboxes = ranges::views::all(\n\t\t\tparticipants\n\t\t) | ranges::views::transform([&](not_null<PeerData*> peer) {\n\t\t\tconst auto line = container->add(\n\t\t\t\tobject_ptr<Ui::AbstractButton>(container));\n\t\t\tconst auto &st = st::moderateBoxUserpic;\n\t\t\tline->resize(line->width(), st.size.height());\n\n\t\t\tusing namespace Info::Profile;\n\t\t\tauto name = controller->data.bold\n\t\t\t\t? (NameValue(peer) | rpl::map(tr::bold) | rpl::type_erased)\n\t\t\t\t: NameValue(peer) | rpl::map(tr::marked);\n\t\t\tconst auto userpic\n\t\t\t\t= Ui::CreateChild<Ui::UserpicButton>(line, peer, st);\n\t\t\tconst auto checkbox = Ui::CreateChild<Ui::Checkbox>(\n\t\t\t\tline,\n\t\t\t\tcontroller->data.messagesCounts\n\t\t\t\t? rpl::combine(\n\t\t\t\t\tstd::move(name),\n\t\t\t\t\trpl::duplicate(controller->data.messagesCounts)\n\t\t\t\t) | rpl::map([=](const auto &richName, const auto &map) {\n\t\t\t\t\tconst auto it = map.find(peer->id);\n\t\t\t\t\treturn (it == map.end() || !it->second)\n\t\t\t\t\t\t? richName\n\t\t\t\t\t\t: TextWithEntities{\n\t\t\t\t\t\t\t(u\"(%1) \"_q).arg(it->second)\n\t\t\t\t\t\t}.append(richName);\n\t\t\t\t})\n\t\t\t\t: std::move(name) | rpl::type_erased,\n\t\t\t\tst::defaultBoxCheckbox,\n\t\t\t\tstd::make_unique<Ui::CheckView>(\n\t\t\t\t\tst::defaultCheck,\n\t\t\t\t\tranges::contains(controller->data.checked, peer->id)));\n\t\t\tcheckbox->setCheckAlignment(style::al_left);\n\t\t\trpl::combine(\n\t\t\t\tline->widthValue(),\n\t\t\t\tcheckbox->widthValue()\n\t\t\t) | rpl::on_next([=](int width, int) {\n\t\t\t\tuserpic->moveToLeft(\n\t\t\t\t\tst::boxRowPadding.left()\n\t\t\t\t\t\t+ checkbox->checkRect().width()\n\t\t\t\t\t\t+ st::defaultBoxCheckbox.textPosition.x(),\n\t\t\t\t\t0);\n\t\t\t\tconst auto skip = st::defaultBoxCheckbox.textPosition.x();\n\t\t\t\tcheckbox->resizeToWidth(width\n\t\t\t\t\t- rect::right(userpic)\n\t\t\t\t\t- skip\n\t\t\t\t\t- st::boxRowPadding.right());\n\t\t\t\tcheckbox->moveToLeft(\n\t\t\t\t\trect::right(userpic) + skip,\n\t\t\t\t\t((userpic->height() - checkbox->height()) / 2)\n\t\t\t\t\t\t+ st::defaultBoxCheckbox.margin.top());\n\t\t\t}, checkbox->lifetime());\n\n\t\t\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\tcheckbox->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t\t\tline->setClickedCallback([=] {\n\t\t\t\tcheckbox->setChecked(!checkbox->checked());\n\t\t\t\tclicks->fire({});\n\t\t\t});\n\n\t\t\treturn checkbox;\n\t\t}) | ranges::to_vector;\n\n\t\tclicks->events(\n\t\t) | rpl::on_next([=] {\n\t\t\tcontroller->toggleRequestsFromInner.fire_copy(\n\t\t\t\tcheckTopOnAllInner\n\t\t\t\t? ranges::all_of(checkboxes, &Ui::Checkbox::checked)\n\t\t\t\t: ranges::any_of(checkboxes, &Ui::Checkbox::checked));\n\t\t}, container->lifetime());\n\n\t\tcontroller->checkAllRequests.events(\n\t\t) | rpl::on_next([=](bool checked) {\n\t\t\tfor (const auto &c : checkboxes) {\n\t\t\t\tc->setChecked(checked);\n\t\t\t}\n\t\t}, container->lifetime());\n\n\t\tcontroller->collectRequests = [=] {\n\t\t\tauto result = Participants();\n\t\t\tfor (auto i = 0; i < checkboxes.size(); i++) {\n\t\t\t\tif (checkboxes[i]->checked()) {\n\t\t\t\t\tresult.push_back(participants[i]);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn result;\n\t\t};\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/expandable_peer_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass PeerData;\n\nusing Participants = std::vector<not_null<PeerData*>>;\n\nnamespace Ui {\n\nclass Checkbox;\nclass VerticalLayout;\n\nstruct ExpandablePeerListController final {\n\tstruct Data final {\n\t\trpl::producer<base::flat_map<PeerId, int>> messagesCounts = nullptr;\n\t\tParticipants participants;\n\t\tstd::vector<PeerId> checked;\n\t\tbool skipSingle = false;\n\t\tbool hideRightButton = false;\n\t\tbool checkTopOnAllInner = false;\n\t\tbool bold = true;\n\t};\n\tExpandablePeerListController(Data &&data) : data(std::move(data)) {\n\t}\n\tconst Data data;\n\trpl::event_stream<bool> toggleRequestsFromTop;\n\trpl::event_stream<bool> toggleRequestsFromInner;\n\trpl::event_stream<bool> checkAllRequests;\n\tFn<Participants()> collectRequests;\n};\n\nvoid AddExpandablePeerList(\n\tnot_null<Ui::Checkbox*> checkbox,\n\tnot_null<ExpandablePeerListController*> controller,\n\tnot_null<Ui::VerticalLayout*> inner);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/fields/special_fields.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/fields/special_fields.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"countries/countries_instance.h\" // Countries::ValidPhoneCode\n#include \"styles/style_widgets.h\"\n\n#include <QtCore/QRegularExpression>\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kMaxUsernameLength = 32;\n\n// Rest of the phone number, without country code (seen 12 at least),\n// need more for service numbers.\nconstexpr auto kMaxPhoneTailLength = 32;\n\n// Max length of country phone code.\nconstexpr auto kMaxPhoneCodeLength = 4;\n\n} // namespace\n\nCountryCodeInput::CountryCodeInput(\n\tQWidget *parent,\n\tconst style::InputField &st)\n: MaskedInputField(parent, st) {\n}\n\nvoid CountryCodeInput::startErasing(QKeyEvent *e) {\n\tsetFocus();\n\tkeyPressEvent(e);\n}\n\nvoid CountryCodeInput::codeSelected(const QString &code) {\n\tauto wasText = getLastText();\n\tauto wasCursor = cursorPosition();\n\tauto newText = '+' + code;\n\tauto newCursor = int(newText.size());\n\tsetText(newText);\n\t_nosignal = true;\n\tcorrectValue(wasText, wasCursor, newText, newCursor);\n\t_nosignal = false;\n\tchanged();\n}\n\nvoid CountryCodeInput::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Space) {\n\t\t_spacePressed.fire({});\n\t} else {\n\t\tMaskedInputField::keyPressEvent(e);\n\t}\n}\n\nvoid CountryCodeInput::correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) {\n\tQString newText, addToNumber;\n\tint oldPos(nowCursor);\n\tint newPos(-1);\n\tint oldLen(now.length());\n\tint start = 0;\n\tint digits = 5;\n\tnewText.reserve(oldLen + 1);\n\tif (oldLen && now[0] == '+') {\n\t\tif (start == oldPos) {\n\t\t\tnewPos = newText.length();\n\t\t}\n\t\t++start;\n\t}\n\tnewText += '+';\n\tfor (int i = start; i < oldLen; ++i) {\n\t\tif (i == oldPos) {\n\t\t\tnewPos = newText.length();\n\t\t}\n\t\tauto ch = now[i];\n\t\tif (ch.isDigit()) {\n\t\t\tif (!digits || !--digits) {\n\t\t\t\taddToNumber += ch;\n\t\t\t} else {\n\t\t\t\tnewText += ch;\n\t\t\t}\n\t\t}\n\t}\n\tif (!addToNumber.isEmpty()) {\n\t\tauto validCode = Countries::Instance().validPhoneCode(newText.mid(1));\n\t\taddToNumber = newText.mid(1 + validCode.length()) + addToNumber;\n\t\tnewText = '+' + validCode;\n\t}\n\tsetCorrectedText(now, nowCursor, newText, newPos);\n\n\tif (!_nosignal && was != newText) {\n\t\t_codeChanged.fire(newText.mid(1));\n\t}\n\tif (!addToNumber.isEmpty()) {\n\t\t_addedToNumber.fire_copy(addToNumber);\n\t}\n}\n\nPhonePartInput::PhonePartInput(\n\tQWidget *parent,\n\tconst style::InputField &st,\n\tPhonePartInput::GroupsCallback groupsCallback)\n: MaskedInputField(parent, st/*, tr::lng_phone_ph(tr::now)*/)\n, _groupsCallback(std::move(groupsCallback)) {\n}\n\nvoid PhonePartInput::paintAdditionalPlaceholder(QPainter &p) {\n\tif (!_pattern.isEmpty()) {\n\t\tauto t = getDisplayedText();\n\t\tauto ph = _additionalPlaceholder.mid(t.size());\n\t\tif (!ph.isEmpty()) {\n\t\t\tp.setClipRect(rect());\n\t\t\tauto phRect = placeholderRect();\n\t\t\tint tw = phFont()->width(t);\n\t\t\tif (tw < phRect.width()) {\n\t\t\t\tphRect.setLeft(phRect.left() + tw);\n\t\t\t\tplaceholderAdditionalPrepare(p);\n\t\t\t\tp.drawText(phRect, ph, style::al_topleft);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid PhonePartInput::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Backspace && cursorPosition() == 0) {\n\t\t_frontBackspaceEvent.fire_copy(e);\n\t} else {\n\t\tMaskedInputField::keyPressEvent(e);\n\t}\n}\n\nvoid PhonePartInput::correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) {\n\tif (!now.isEmpty() && (_lastDigits != now)) {\n\t\t_lastDigits = now;\n\t\t_lastDigits.replace(TextUtilities::RegExpDigitsExclude(), QString());\n\t\tupdatePattern(_groupsCallback(_code + _lastDigits));\n\t}\n\n\tQString newText;\n\tint oldPos(nowCursor), newPos(-1), oldLen(now.length()), digitCount = 0;\n\tfor (int i = 0; i < oldLen; ++i) {\n\t\tif (now[i].isDigit()) {\n\t\t\t++digitCount;\n\t\t}\n\t}\n\tif (digitCount > kMaxPhoneTailLength) {\n\t\tdigitCount = kMaxPhoneTailLength;\n\t}\n\n\tbool inPart = !_pattern.isEmpty();\n\tint curPart = -1, leftInPart = 0;\n\tnewText.reserve(oldLen);\n\tfor (int i = 0; i < oldLen; ++i) {\n\t\tif (i == oldPos && newPos < 0) {\n\t\t\tnewPos = newText.length();\n\t\t}\n\n\t\tauto ch = now[i];\n\t\tif (ch.isDigit()) {\n\t\t\tif (!digitCount--) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (inPart) {\n\t\t\t\tif (leftInPart) {\n\t\t\t\t\t--leftInPart;\n\t\t\t\t} else {\n\t\t\t\t\t++curPart;\n\t\t\t\t\tinPart = curPart < _pattern.size();\n\t\t\t\t\t// Don't add an extra space to the end.\n\t\t\t\t\tif (inPart) {\n\t\t\t\t\t\tnewText += ' ';\n\t\t\t\t\t}\n\n\t\t\t\t\tleftInPart = inPart ? (_pattern.at(curPart) - 1) : 0;\n\n\t\t\t\t\t++oldPos;\n\t\t\t\t}\n\t\t\t}\n\t\t\tnewText += ch;\n\t\t} else if (ch == ' ' || ch == '-' || ch == '(' || ch == ')') {\n\t\t\tif (inPart) {\n\t\t\t\tif (leftInPart) {\n\t\t\t\t} else {\n\t\t\t\t\tnewText += ch;\n\t\t\t\t\t++curPart;\n\t\t\t\t\tinPart = curPart < _pattern.size();\n\t\t\t\t\tleftInPart = inPart ? _pattern.at(curPart) : 0;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tnewText += ch;\n\t\t\t}\n\t\t}\n\t}\n\tauto newlen = newText.size();\n\twhile (newlen > 0 && newText.at(newlen - 1).isSpace()) {\n\t\t--newlen;\n\t}\n\tif (newlen < newText.size()) {\n\t\tnewText = newText.mid(0, newlen);\n\t}\n\tsetCorrectedText(now, nowCursor, newText, newPos);\n}\n\nvoid PhonePartInput::addedToNumber(const QString &added) {\n\tsetFocus();\n\tauto wasText = getLastText();\n\tauto wasCursor = cursorPosition();\n\tauto newText = added + wasText;\n\tauto newCursor = int(newText.size());\n\tsetText(newText);\n\tsetCursorPosition(added.length());\n\tcorrectValue(wasText, wasCursor, newText, newCursor);\n\tstartPlaceholderAnimation();\n}\n\nvoid PhonePartInput::chooseCode(const QString &code) {\n\t_code = code;\n\tupdatePattern(_groupsCallback(_code));\n\n\tauto wasText = getLastText();\n\tauto wasCursor = cursorPosition();\n\tauto newText = getLastText();\n\tauto newCursor = int(newText.size());\n\tcorrectValue(wasText, wasCursor, newText, newCursor);\n\n\tstartPlaceholderAnimation();\n\n\tupdate();\n}\n\nvoid PhonePartInput::updatePattern(QVector<int> &&pattern) {\n\tif (_pattern == pattern) {\n\t\treturn;\n\t}\n\t_pattern = std::move(pattern);\n\tif (!_pattern.isEmpty() && _pattern.at(0) == _code.size()) {\n\t\t_pattern.pop_front();\n\t} else {\n\t\t_pattern.clear();\n\t}\n\t_additionalPlaceholder = QString();\n\tif (!_pattern.isEmpty()) {\n\t\t_additionalPlaceholder.reserve(20);\n\t\tfor (const auto &part : _pattern) {\n\t\t\t_additionalPlaceholder.append(' ');\n\t\t\t_additionalPlaceholder.append(QString(part, QChar(0x2212)));\n\t\t}\n\t}\n\tsetPlaceholderHidden(!_additionalPlaceholder.isEmpty());\n}\n\nUsernameInput::UsernameInput(\n\tQWidget *parent,\n\tconst style::InputField &st,\n\trpl::producer<QString> placeholder,\n\tconst QString &val,\n\tconst QString &linkPlaceholder)\n: MaskedInputField(parent, st, std::move(placeholder), val) {\n\tsetLinkPlaceholder(linkPlaceholder);\n}\n\nvoid UsernameInput::setLinkPlaceholder(const QString &placeholder) {\n\t_linkPlaceholder = placeholder;\n\tif (!_linkPlaceholder.isEmpty()) {\n\t\tsetTextMargins(style::margins(_st.textMargins.left() + _st.style.font->width(_linkPlaceholder), _st.textMargins.top(), _st.textMargins.right(), _st.textMargins.bottom()));\n\t\tsetPlaceholderHidden(true);\n\t}\n}\n\nvoid UsernameInput::setMaxLength(int maxLength) {\n\t_maxLength = maxLength;\n}\n\nvoid UsernameInput::paintAdditionalPlaceholder(QPainter &p) {\n\tif (!_linkPlaceholder.isEmpty()) {\n\t\tp.setFont(_st.style.font);\n\t\tp.setPen(_st.placeholderFg);\n\t\tp.drawText(QRect(_st.textMargins.left(), _st.textMargins.top(), width(), height() - _st.textMargins.top() - _st.textMargins.bottom()), _linkPlaceholder, style::al_topleft);\n\t}\n}\n\nvoid UsernameInput::correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) {\n\tauto newPos = nowCursor;\n\tauto from = 0, len = int(now.size());\n\tfor (; from < len; ++from) {\n\t\tif (!now.at(from).isSpace()) {\n\t\t\tbreak;\n\t\t}\n\t\tif (newPos > 0) --newPos;\n\t}\n\tlen -= from;\n\tconst auto maxLength = (_maxLength > 0)\n\t\t? _maxLength\n\t\t: kMaxUsernameLength;\n\tif (len > maxLength) {\n\t\tlen = maxLength + (now.at(from) == '@' ? 1 : 0);\n\t}\n\tfor (int32 to = from + len; to > from;) {\n\t\t--to;\n\t\tif (!now.at(to).isSpace()) {\n\t\t\tbreak;\n\t\t}\n\t\t--len;\n\t}\n\tsetCorrectedText(now, nowCursor, now.mid(from, len), newPos);\n}\n\nPhoneInput::PhoneInput(\n\tQWidget *parent,\n\tconst style::InputField &st,\n\trpl::producer<QString> placeholder,\n\tconst QString &defaultValue,\n\tQString value,\n\tPhoneInput::GroupsCallback groupsCallback)\n: MaskedInputField(parent, st, std::move(placeholder), value)\n, _defaultValue(defaultValue)\n, _groupsCallback(std::move(groupsCallback)) {\n\tif (value.isEmpty()) {\n\t\tclearText();\n\t} else {\n\t\tauto pos = int(value.size());\n\t\tcorrectValue(QString(), 0, value, pos);\n\t}\n}\n\nvoid PhoneInput::focusInEvent(QFocusEvent *e) {\n\tMaskedInputField::focusInEvent(e);\n\tsetSelection(cursorPosition(), cursorPosition());\n}\n\nvoid PhoneInput::clearText() {\n\tauto value = _defaultValue;\n\tsetText(value);\n\tauto pos = int(value.size());\n\tcorrectValue(QString(), 0, value, pos);\n}\n\nvoid PhoneInput::paintAdditionalPlaceholder(QPainter &p) {\n\tif (!_pattern.isEmpty()) {\n\t\tauto t = getDisplayedText();\n\t\tauto ph = _additionalPlaceholder.mid(t.size());\n\t\tif (!ph.isEmpty()) {\n\t\t\tp.setClipRect(rect());\n\t\t\tauto phRect = placeholderRect();\n\t\t\tint tw = phFont()->width(t);\n\t\t\tif (tw < phRect.width()) {\n\t\t\t\tphRect.setLeft(phRect.left() + tw);\n\t\t\t\tplaceholderAdditionalPrepare(p);\n\t\t\t\tp.drawText(phRect, ph, style::al_topleft);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid PhoneInput::correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) {\n\tauto digits = now;\n\tdigits.replace(TextUtilities::RegExpDigitsExclude(), QString());\n\t_pattern = _groupsCallback(digits);\n\n\tQString newPlaceholder;\n\tif (_pattern.isEmpty()) {\n\t\tnewPlaceholder = QString();\n\t} else if (_pattern.size() == 1 && _pattern.at(0) == digits.size()) {\n\t\tnewPlaceholder = QString(_pattern.at(0) + 2, ' ') + tr::lng_contact_phone(tr::now);\n\t} else {\n\t\tnewPlaceholder.reserve(20);\n\t\tfor (int i = 0, l = _pattern.size(); i < l; ++i) {\n\t\t\tif (i) {\n\t\t\t\tnewPlaceholder.append(' ');\n\t\t\t} else {\n\t\t\t\tnewPlaceholder.append('+');\n\t\t\t}\n\t\t\tnewPlaceholder.append(i ? QString(_pattern.at(i), QChar(0x2212)) : digits.mid(0, _pattern.at(i)));\n\t\t}\n\t}\n\tif (_additionalPlaceholder != newPlaceholder) {\n\t\t_additionalPlaceholder = newPlaceholder;\n\t\tsetPlaceholderHidden(!_additionalPlaceholder.isEmpty());\n\t\tupdate();\n\t}\n\n\tQString newText;\n\tint oldPos(nowCursor), newPos(-1), oldLen(now.length()), digitCount = qMin(digits.size(), kMaxPhoneCodeLength + kMaxPhoneTailLength);\n\n\tbool inPart = !_pattern.isEmpty(), plusFound = false;\n\tint curPart = 0, leftInPart = inPart ? _pattern.at(curPart) : 0;\n\tnewText.reserve(oldLen + 1);\n\tnewText.append('+');\n\tfor (int i = 0; i < oldLen; ++i) {\n\t\tif (i == oldPos && newPos < 0) {\n\t\t\tnewPos = newText.length();\n\t\t}\n\n\t\tQChar ch(now[i]);\n\t\tif (ch.isDigit()) {\n\t\t\tif (!digitCount--) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (inPart) {\n\t\t\t\tif (leftInPart) {\n\t\t\t\t\t--leftInPart;\n\t\t\t\t} else {\n\t\t\t\t\t++curPart;\n\t\t\t\t\tinPart = curPart < _pattern.size();\n\t\t\t\t\t// Don't add an extra space to the end.\n\t\t\t\t\tif (inPart) {\n\t\t\t\t\t\tnewText += ' ';\n\t\t\t\t\t}\n\t\t\t\t\tleftInPart = inPart ? (_pattern.at(curPart) - 1) : 0;\n\n\t\t\t\t\t++oldPos;\n\t\t\t\t}\n\t\t\t}\n\t\t\tnewText += ch;\n\t\t} else if (ch == ' ' || ch == '-' || ch == '(' || ch == ')') {\n\t\t\tif (inPart) {\n\t\t\t\tif (leftInPart) {\n\t\t\t\t} else {\n\t\t\t\t\tnewText += ch;\n\t\t\t\t\t++curPart;\n\t\t\t\t\tinPart = curPart < _pattern.size();\n\t\t\t\t\tleftInPart = inPart ? _pattern.at(curPart) : 0;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tnewText += ch;\n\t\t\t}\n\t\t} else if (ch == '+') {\n\t\t\tplusFound = true;\n\t\t}\n\t}\n\tif (!plusFound && newText == u\"+\"_q) {\n\t\tnewText = QString();\n\t\tnewPos = 0;\n\t}\n\tint32 newlen = newText.size();\n\twhile (newlen > 0 && newText.at(newlen - 1).isSpace()) {\n\t\t--newlen;\n\t}\n\tif (newlen < newText.size()) {\n\t\tnewText = newText.mid(0, newlen);\n\t}\n\tsetCorrectedText(now, nowCursor, newText, newPos);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/fields/special_fields.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/fields/masked_input_field.h\"\n\nnamespace Ui {\n\nclass CountryCodeInput : public MaskedInputField {\npublic:\n\tCountryCodeInput(QWidget *parent, const style::InputField &st);\n\n\tvoid startErasing(QKeyEvent *e);\n\n\t[[nodiscard]] rpl::producer<QString> addedToNumber() const {\n\t\treturn _addedToNumber.events();\n\t}\n\t[[nodiscard]] rpl::producer<QString> codeChanged() const {\n\t\treturn _codeChanged.events();\n\t}\n\t[[nodiscard]] rpl::producer<> spacePressed() const {\n\t\treturn _spacePressed.events();\n\t}\n\n\tvoid codeSelected(const QString &code);\n\nprotected:\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) override;\n\nprivate:\n\tbool _nosignal = false;\n\trpl::event_stream<QString> _addedToNumber;\n\trpl::event_stream<QString> _codeChanged;\n\trpl::event_stream<> _spacePressed;\n\n};\n\nclass PhonePartInput : public MaskedInputField {\npublic:\n\tusing GroupsCallback = Fn<QVector<int>(const QString &)>;\n\n\tPhonePartInput(\n\t\tQWidget *parent,\n\t\tconst style::InputField &st,\n\t\tGroupsCallback groupsCallback);\n\n\t[[nodiscard]] auto frontBackspaceEvent() const\n\t-> rpl::producer<not_null<QKeyEvent*>> {\n\t\treturn _frontBackspaceEvent.events();\n\t}\n\n\tvoid addedToNumber(const QString &added);\n\tvoid chooseCode(const QString &code);\n\nprotected:\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\n\tvoid correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) override;\n\tvoid paintAdditionalPlaceholder(QPainter &p) override;\n\nprivate:\n\tvoid updatePattern(QVector<int> &&pattern);\n\n\tQString _code;\n\tQString _lastDigits;\n\tQVector<int> _pattern;\n\tQString _additionalPlaceholder;\n\trpl::event_stream<not_null<QKeyEvent*>> _frontBackspaceEvent;\n\tGroupsCallback _groupsCallback;\n\n};\n\nclass UsernameInput : public MaskedInputField {\npublic:\n\tUsernameInput(\n\t\tQWidget *parent,\n\t\tconst style::InputField &st,\n\t\trpl::producer<QString> placeholder,\n\t\tconst QString &val,\n\t\tconst QString &linkPlaceholder);\n\n\tvoid setLinkPlaceholder(const QString &placeholder);\n\tvoid setMaxLength(int maxLength);\n\nprotected:\n\tvoid correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) override;\n\tvoid paintAdditionalPlaceholder(QPainter &p) override;\n\nprivate:\n\tQString _linkPlaceholder;\n\tint _maxLength = 0;\n\n};\n\nclass PhoneInput : public MaskedInputField {\npublic:\n\tusing GroupsCallback = Fn<QVector<int>(const QString &)>;\n\n\tPhoneInput(\n\t\tQWidget *parent,\n\t\tconst style::InputField &st,\n\t\trpl::producer<QString> placeholder,\n\t\tconst QString &defaultValue,\n\t\tQString value,\n\t\tGroupsCallback groupsCallback);\n\n\tvoid clearText();\n\nprotected:\n\tvoid focusInEvent(QFocusEvent *e) override;\n\n\tvoid correctValue(\n\t\tconst QString &was,\n\t\tint wasCursor,\n\t\tQString &now,\n\t\tint &nowCursor) override;\n\tvoid paintAdditionalPlaceholder(QPainter &p) override;\n\nprivate:\n\tQString _defaultValue;\n\tQVector<int> _pattern;\n\tQString _additionalPlaceholder;\n\n\tGroupsCallback _groupsCallback;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/fields/time_part_input_with_placeholder.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/fields/time_part_input_with_placeholder.h\"\n\n#include \"lang/lang_numbers_animation.h\"\n\nnamespace Ui {\n\nvoid TimePartWithPlaceholder::setPhrase(\n\t\tconst tr::phrase<lngtag_count> &phrase) {\n\t_phrase = phrase;\n}\n\nvoid TimePartWithPlaceholder::paintAdditionalPlaceholder(QPainter &p) {\n\tmaybeUpdatePlaceholder();\n\n\tp.setClipRect(rect());\n\tconst auto phRect = placeholderRect();\n\n\tif (_lastPlaceholder.width < phRect.width()) {\n\t\tplaceholderAdditionalPrepare(p);\n\t\tp.drawText(\n\t\t\tphRect.translated(-_lastPlaceholder.leftOffset, 0),\n\t\t\t_lastPlaceholder.text,\n\t\t\tstyle::al_left);\n\t}\n\n}\n\nvoid TimePartWithPlaceholder::maybeUpdatePlaceholder() {\n\tconst auto displayedText = getDisplayedText();\n\tif (displayedText == _lastPlaceholder.displayedText) {\n\t\treturn;\n\t}\n\tconst auto count = displayedText.toUInt();\n\tconst auto textWithOffset = _phrase(\n\t\ttr::now,\n\t\tlt_count,\n\t\tcount,\n\t\tUi::StringWithNumbers::FromString);\n\t_lastPlaceholder = {\n\t\t.width = phFont()->width(textWithOffset.text),\n\t\t.text = textWithOffset.text,\n\t\t.leftOffset = phFont()->width(\n\t\t\ttextWithOffset.text.mid(0, textWithOffset.offset)),\n\t\t.displayedText = displayedText,\n\t};\n\tif (displayedText.size() > 1 && displayedText.startsWith(_zero)) {\n\t\t_lastPlaceholder.text.insert(textWithOffset.offset, _zero);\n\t}\n\n\tconst auto leftMargins = (width() - _lastPlaceholder.width) / 2\n\t\t+ _lastPlaceholder.leftOffset;\n\tsetTextMargins({ leftMargins, 0, 0, 0 });\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/fields/time_part_input_with_placeholder.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/fields/time_part_input.h\"\n\n#include \"lang_auto.h\"\n\nnamespace Ui {\n\nclass TimePartWithPlaceholder final : public TimePart {\npublic:\n\tusing Ui::TimePart::TimePart;\n\n\tvoid setPhrase(const tr::phrase<lngtag_count> &phrase);\n\nprotected:\n\tvoid paintAdditionalPlaceholder(QPainter &p) override;\n\nprivate:\n\tvoid maybeUpdatePlaceholder();\n\n\tconst QChar _zero = QChar('0');\n\ttr::phrase<lngtag_count> _phrase;\n\n\tstruct {\n\t\tint width = 0;\n\t\tQString text;\n\t\tint leftOffset = 0;\n\t\tQString displayedText;\n\t} _lastPlaceholder;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/gradient_round_button.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/gradient_round_button.h\"\n\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace Ui {\n\nGradientButton::GradientButton(QWidget *widget, QGradientStops stops)\n: RippleButton(widget, st::defaultRippleAnimation)\n, _stops(std::move(stops)) {\n}\n\nvoid GradientButton::setStops(QGradientStops stops) {\n\tif (_stops != stops) {\n\t\t_stops = std::move(stops);\n\t\t_bg = {};\n\t\tupdate();\n\t}\n}\n\nvoid GradientButton::setFullRadius(bool enabled) {\n\tif (_fullRadius != enabled) {\n\t\t_fullRadius = enabled;\n\t\t_bg = {};\n\t\tupdate();\n\t}\n}\n\nvoid GradientButton::paintEvent(QPaintEvent *e) {\n\tQPainter p(this);\n\n\tvalidateBg();\n\tp.drawImage(0, 0, _bg);\n\tpaintGlare(p);\n\n\tconst auto ripple = QColor(0, 0, 0, 36);\n\tpaintRipple(p, 0, 0, &ripple);\n}\n\nvoid GradientButton::paintGlare(QPainter &p) {\n\tif (!_glare.glare.birthTime) {\n\t\treturn;\n\t}\n\tconst auto progress = _glare.progress(crl::now());\n\tconst auto x = (-_glare.width) + (width() + _glare.width * 2) * progress;\n\tconst auto h = height();\n\n\tconst auto edgeWidth = _glare.width + st::roundRadiusLarge;\n\tif (x > edgeWidth && x < (width() - edgeWidth)) {\n\t\tp.drawTiledPixmap(x, 0, _glare.width, h, _glare.pixmap, 0, 0);\n\t} else {\n\t\tauto frame = QImage(\n\t\t\tQSize(_glare.width, h) * style::DevicePixelRatio(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\tframe.setDevicePixelRatio(style::DevicePixelRatio());\n\t\tframe.fill(Qt::transparent);\n\n\t\t{\n\t\t\tauto q = QPainter(&frame);\n\t\t\tq.drawTiledPixmap(0, 0, _glare.width, h, _glare.pixmap, 0, 0);\n\t\t\tq.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\t\tq.drawImage(-x, 0, _bg, 0, 0);\n\t\t}\n\t\tp.drawImage(x, 0, frame);\n\t}\n}\n\nvoid GradientButton::validateBg() {\n\tconst auto factor = devicePixelRatio();\n\tif (!_bg.isNull()\n\t\t&& (_bg.devicePixelRatio() == factor)\n\t\t&& (_bg.size() == size() * factor)) {\n\t\treturn;\n\t}\n\t_bg = QImage(size() * factor, QImage::Format_ARGB32_Premultiplied);\n\t_bg.setDevicePixelRatio(factor);\n\tif (_fullRadius) {\n\t\t_bg.fill(Qt::transparent);\n\t}\n\tauto p = QPainter(&_bg);\n\tauto gradient = QLinearGradient(\n\t\tQPointF(0, 0),\n\t\tQPointF(width(), height()));\n\tgradient.setStops(_stops);\n\tif (_fullRadius) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(gradient);\n\t\tp.drawRoundedRect(rect(), height() / 2., height() / 2.);\n\t} else {\n\t\tp.fillRect(rect(), gradient);\n\t}\n\tp.end();\n\n\tif (!_fullRadius) {\n\t\t_bg = Images::Round(std::move(_bg), ImageRoundRadius::Large);\n\t}\n}\n\nvoid GradientButton::setGlarePaused(bool paused) {\n\t_glare.paused = paused;\n}\n\nvoid GradientButton::validateGlare() {\n\t_glare.validate(\n\t\tst::premiumButtonFg->c,\n\t\t[=] { update(); },\n\t\tst::gradientButtonGlareTimeout,\n\t\tst::gradientButtonGlareDuration);\n}\n\nQImage GradientButton::prepareRippleMask() const {\n\treturn RippleAnimation::RoundRectMask(\n\t\tsize(),\n\t\t_fullRadius ? height() / 2 : st::roundRadiusLarge);\n}\n\nvoid GradientButton::startGlareAnimation() {\n\tvalidateGlare();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/gradient_round_button.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/buttons.h\"\n#include \"ui/effects/glare.h\"\n\nnamespace Ui {\n\nclass GradientButton final : public Ui::RippleButton {\npublic:\n\tGradientButton(QWidget *widget, QGradientStops stops);\n\n\tvoid setStops(QGradientStops stops);\n\tvoid setFullRadius(bool enabled);\n\n\tvoid startGlareAnimation();\n\tvoid setGlarePaused(bool paused);\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid paintGlare(QPainter &p);\n\tvoid validateBg();\n\tvoid validateGlare();\n\n\tQImage prepareRippleMask() const override;\n\n\tQGradientStops _stops;\n\tQImage _bg;\n\n\tGlareEffect _glare;\n\tbool _fullRadius = false;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/horizontal_fit_container.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/horizontal_fit_container.h\"\n\n#include \"ui/widgets/buttons.h\"\n\nnamespace Ui {\n\nHorizontalFitContainer::HorizontalFitContainer(\n\tnot_null<RpWidget*> parent,\n\tint spacing)\n: RpWidget(parent)\n, _spacing(spacing) {\n\tsizeValue() | rpl::on_next([=](QSize size) {\n\t\tupdateLayout(size);\n\t}, lifetime());\n}\n\nint HorizontalFitContainer::add(not_null<AbstractButton*> button) {\n\tconst auto id = _nextId++;\n\t_buttons.emplace(id, base::unique_qptr<AbstractButton>{ button.get() });\n\tbutton->setParent(this);\n\tbutton->show();\n\tupdateLayout(size());\n\treturn id;\n}\n\nvoid HorizontalFitContainer::remove(int id) {\n\tif (const auto it = _buttons.find(id); it != _buttons.end()) {\n\t\t_buttons.erase(it);\n\t\tupdateLayout(size());\n\t}\n}\n\nvoid HorizontalFitContainer::updateLayout(QSize size) {\n\t_layoutLifetime.destroy();\n\tif (_buttons.empty()) {\n\t\treturn;\n\t}\n\tconst auto count = int(_buttons.size());\n\tconst auto totalSpacing = _spacing * (count - 1);\n\tconst auto buttonWidth = (size.width() - totalSpacing) / count;\n\tconst auto h = size.height();\n\tauto x = 0;\n\tfor (const auto &[id, button] : _buttons) {\n\t\tbutton->setGeometry(x, 0, buttonWidth, h);\n\t\tx += buttonWidth + _spacing;\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/horizontal_fit_container.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"base/unique_qptr.h\"\n\nnamespace Ui {\n\nclass AbstractButton;\n\nclass HorizontalFitContainer final : public RpWidget {\npublic:\n\tHorizontalFitContainer(\n\t\tnot_null<RpWidget*> parent,\n\t\tint spacing);\n\n\tint add(not_null<AbstractButton*> button);\n\tvoid remove(int id);\n\nprivate:\n\tvoid updateLayout(QSize size);\n\n\tconst int _spacing;\n\tstd::map<int, base::unique_qptr<AbstractButton>> _buttons;\n\tint _nextId = 0;\n\trpl::lifetime _layoutLifetime;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/level_meter.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/level_meter.h\"\n\n#include \"ui/painter.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Ui {\n\nLevelMeter::LevelMeter(QWidget *parent, const style::LevelMeter &st)\n: RpWidget(parent)\n, _st(st) {\n}\n\nvoid LevelMeter::setValue(float value) {\n\t_value = value;\n\trepaint();\n}\n\nvoid LevelMeter::paintEvent(QPaintEvent* event) {\n\tauto p = QPainter(this);\n\tauto hq = PainterHighQualityEnabler(p);\n\n\tp.setPen(Qt::NoPen);\n\n\tconst auto activeFg = _st.activeFg;\n\tconst auto inactiveFg = _st.inactiveFg;\n\tconst auto radius = _st.lineWidth / 2;\n\tconst auto rect = QRect(0, 0, _st.lineWidth, height());\n\tp.setBrush(activeFg);\n\tfor (auto i = 0; i < _st.lineCount; ++i) {\n\t\tconst auto valueAtLine = (float64)(i + 1) / _st.lineCount;\n\t\tif (valueAtLine > _value) {\n\t\t\tp.setBrush(inactiveFg);\n\t\t}\n\t\tp.drawRoundedRect(\n\t\t\trect.translated((_st.lineWidth + _st.lineSpacing) * i, 0),\n\t\t\tradius,\n\t\t\tradius);\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/level_meter.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace style {\nstruct LevelMeter;\n} // namespace style\n\nnamespace Ui {\n\nclass LevelMeter : public RpWidget {\npublic:\n\tLevelMeter(QWidget *parent, const style::LevelMeter &st);\n\n\tvoid setValue(float value);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tconst style::LevelMeter &_st;\n\tfloat _value = 0.0f;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/middle_click_autoscroll.h\"\n\n#include \"styles/style_chat.h\"\n#include \"styles/style_widgets.h\"\n#include \"ui/effects/animation_value.h\"\n#include \"ui/rect.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kDelay = 15;\nconstexpr auto kHoldToToggleThreshold = crl::time(220);\n\n[[nodiscard]] int Deadzone() {\n\treturn std::max(1, int(std::lround(style::ConvertFloatScale(6.))));\n}\n\n[[nodiscard]] QImage PaintCursorImage(\n\t\tint size,\n\t\tint factor,\n\t\tbool drawOuter,\n\t\tbool drawTopArrow,\n\t\tbool drawBottomArrow,\n\t\tconst QColor &inner,\n\t\tconst QColor &indicator,\n\t\tconst QColor &topArrow,\n\t\tconst QColor &bottomArrow) {\n\tauto image = QImage(\n\t\tQSize(size, size) * factor,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(factor);\n\timage.fill(Qt::transparent);\n\tauto p = QPainter(&image);\n\tp.setRenderHint(QPainter::Antialiasing);\n\tp.setPen(Qt::NoPen);\n\n\tconst auto outerRect = QRectF(QPointF(0.5, 0.5), Size(size - 1.));\n\tif (drawOuter) {\n\t\tconst auto outerStroke = std::max(1., style::ConvertFloatScale(1.5));\n\t\tauto pen = QPen(indicator);\n\t\tpen.setWidthF(outerStroke);\n\t\tp.setPen(pen);\n\t\tp.setBrush(inner);\n\t\tp.drawEllipse(outerRect.marginsRemoved(Margins(outerStroke * 0.5)));\n\t}\n\tp.setPen(Qt::NoPen);\n\tconst auto centerSkip = style::ConvertFloatScale(10.5);\n\tconst auto centerRect = outerRect - Margins(centerSkip);\n\tp.setBrush(indicator);\n\tp.drawEllipse(centerRect);\n\n\tconst auto middle = size * 0.5;\n\tconst auto arrowGap = style::ConvertFloatScale(5.);\n\tconst auto headHalfWidth = style::ConvertFloatScale(4.5);\n\tconst auto headHeight = style::ConvertFloatScale(3.5);\n\tconst auto stroke = std::max(1., style::ConvertFloatScale(2.));\n\tconst auto drawArrow = [&](bool up, const QColor &color) {\n\t\tauto pen = QPen(color);\n\t\tpen.setWidthF(stroke);\n\t\tpen.setCapStyle(Qt::RoundCap);\n\t\tpen.setJoinStyle(Qt::RoundJoin);\n\t\tp.setPen(pen);\n\t\tconst auto dir = up ? -1. : 1.;\n\t\tconst auto tipY = middle + dir * (arrowGap + headHeight);\n\t\tconst auto neckY = tipY - dir * headHeight;\n\t\tp.drawLine(\n\t\t\tQPointF(middle, tipY),\n\t\t\tQPointF(middle - headHalfWidth, neckY));\n\t\tp.drawLine(\n\t\t\tQPointF(middle, tipY),\n\t\t\tQPointF(middle + headHalfWidth, neckY));\n\t};\n\tif (drawTopArrow) {\n\t\tdrawArrow(true, topArrow);\n\t}\n\tif (drawBottomArrow) {\n\t\tdrawArrow(false, bottomArrow);\n\t}\n\treturn image;\n}\n\n} // namespace\n\nMiddleClickAutoscroll::MiddleClickAutoscroll(\n\tFn<void(int)> scrollBy,\n\tFn<void(const QCursor &)> applyCursor,\n\tFn<void()> restoreCursor,\n\tFn<bool()> shouldContinue)\n: _scrollBy(std::move(scrollBy))\n, _applyCursor(std::move(applyCursor))\n, _restoreCursor(std::move(restoreCursor))\n, _shouldContinue(std::move(shouldContinue))\n, _timer([=] { onTimer(); }) {\n}\n\nvoid MiddleClickAutoscroll::toggleOrBeginHold(const QPoint &globalPosition) {\n\tif (_active) {\n\t\tstop();\n\t\treturn;\n\t}\n\t_middlePressed = true;\n\t_middlePressedAt = crl::now();\n\tstart(globalPosition);\n}\n\nbool MiddleClickAutoscroll::finishHold(Qt::MouseButton button) {\n\tif (!_middlePressed || (button != Qt::MiddleButton)) {\n\t\treturn false;\n\t}\n\t_middlePressed = false;\n\tif ((crl::now() - _middlePressedAt) >= kHoldToToggleThreshold) {\n\t\tstop();\n\t}\n\treturn true;\n}\n\nvoid MiddleClickAutoscroll::start(const QPoint &globalPosition) {\n\tif (_active) {\n\t\tstop();\n\t}\n\t_active = true;\n\t_startPosition = globalPosition;\n\t_time = crl::now();\n\tupdateCursor(QCursor::pos().y() - _startPosition.y());\n\t_timer.callEach(kDelay);\n}\n\nvoid MiddleClickAutoscroll::stop() {\n\tif (!_active) {\n\t\treturn;\n\t}\n\t_middlePressed = false;\n\t_active = false;\n\t_timer.cancel();\n\tif (_restoreCursor) {\n\t\t_restoreCursor();\n\t}\n}\n\nvoid MiddleClickAutoscroll::updateCursor(int delta) {\n\tif (!_applyCursor) {\n\t\treturn;\n\t}\n\tconst auto mode = (std::abs(delta) <= Deadzone())\n\t\t? CursorMode::Neutral\n\t\t: (delta < 0)\n\t\t? CursorMode::Up\n\t\t: CursorMode::Down;\n\t_applyCursor(makeCursor(mode));\n}\n\nQCursor MiddleClickAutoscroll::makeCursor(CursorMode mode) {\n\tstruct CachedCursor {\n\t\tCursorMode mode = CursorMode::Neutral;\n\t\tint size = 0;\n\t\tint factor = 0;\n\t\tQColor inner;\n\t\tQColor indicator;\n\t\tQCursor cursor;\n\t};\n\tconst auto size = std::max(\n\t\t1,\n\t\tint(std::lround(style::ConvertFloatScale(27.))));\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto inner = anim::with_alpha(st::windowBg->c, (240. / 255.));\n\tconst auto active = anim::with_alpha(st::windowFg->c, (210. / 255.));\n\tconst auto passive = anim::with_alpha(\n\t\tst::windowSubTextFg->c,\n\t\t(210. / 255.));\n\tconst auto neutral = (mode == CursorMode::Neutral);\n\tconst auto indicator = neutral ? passive : active;\n\tstatic auto cache = std::vector<CachedCursor>();\n\tif (const auto i = ranges::find_if(\n\t\t\tcache,\n\t\t\t[&](const CachedCursor &entry) {\n\t\t\t\treturn (entry.mode == mode)\n\t\t\t\t\t&& (entry.size == size)\n\t\t\t\t\t&& (entry.factor == factor)\n\t\t\t\t\t&& (entry.inner == inner)\n\t\t\t\t\t&& (entry.indicator == indicator);\n\t\t\t});\n\t\ti != cache.end()) {\n\t\treturn i->cursor;\n\t}\n\tconst auto image = PaintCursorImage(\n\t\tsize,\n\t\tfactor,\n\t\tneutral,\n\t\tmode != CursorMode::Down,\n\t\tmode != CursorMode::Up,\n\t\tinner,\n\t\tindicator,\n\t\tindicator,\n\t\tindicator);\n\tconst auto hot = int(std::floor(size * 0.5));\n\tauto cursor = QCursor(QPixmap::fromImage(image), hot, hot);\n\tcache.push_back(CachedCursor{\n\t\t.mode = mode,\n\t\t.size = size,\n\t\t.factor = factor,\n\t\t.inner = inner,\n\t\t.indicator = indicator,\n\t\t.cursor = cursor,\n\t});\n\treturn cursor;\n}\n\nvoid MiddleClickAutoscroll::onTimer() {\n\tif (!_active) {\n\t\treturn;\n\t} else if (_shouldContinue && !_shouldContinue()) {\n\t\tstop();\n\t\treturn;\n\t}\n\tconst auto now = crl::now();\n\tconst auto elapsed = std::max(now - _time, crl::time(1));\n\t_time = now;\n\tconst auto delta = QCursor::pos().y() - _startPosition.y();\n\tupdateCursor(delta);\n\tconst auto absolute = std::abs(delta) - Deadzone();\n\tif (absolute <= 0) {\n\t\treturn;\n\t}\n\tconst auto speed = std::min(\n\t\tabsolute * float64(st::middleClickAutoscrollSpeedScale),\n\t\tfloat64(st::middleClickAutoscrollMaxSpeed));\n\tauto scroll = int(std::lround((speed * elapsed) / 1000.));\n\tif (scroll <= 0) {\n\t\tscroll = 1;\n\t}\n\tif (delta < 0) {\n\t\tscroll = -scroll;\n\t}\n\t_scrollBy(scroll);\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n\nclass QCursor;\n\nnamespace Ui {\n\nclass MiddleClickAutoscroll final {\npublic:\n\texplicit MiddleClickAutoscroll(\n\t\tFn<void(int)> scrollBy,\n\t\tFn<void(const QCursor &)> applyCursor = nullptr,\n\t\tFn<void()> restoreCursor = nullptr,\n\t\tFn<bool()> shouldContinue = nullptr);\n\n\t[[nodiscard]] bool active() const {\n\t\treturn _active;\n\t}\n\n\tvoid toggleOrBeginHold(const QPoint &globalPosition);\n\t[[nodiscard]] bool finishHold(Qt::MouseButton button);\n\tvoid start(const QPoint &globalPosition);\n\tvoid stop();\n\nprivate:\n\tenum class CursorMode {\n\t\tNeutral,\n\t\tUp,\n\t\tDown,\n\t};\n\n\tvoid updateCursor(int delta);\n\tvoid onTimer();\n\t[[nodiscard]] static QCursor makeCursor(CursorMode mode);\n\n\tFn<void(int)> _scrollBy;\n\tFn<void(const QCursor &)> _applyCursor;\n\tFn<void()> _restoreCursor;\n\tFn<bool()> _shouldContinue;\n\tbool _active = false;\n\tbool _middlePressed = false;\n\tQPoint _startPosition;\n\tcrl::time _middlePressedAt = 0;\n\tcrl::time _time = 0;\n\tbase::Timer _timer;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/multi_select.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/multi_select.h\"\n\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/effects/cross_animation.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"lang/lang_keys.h\"\n\n#include <set>\n\nnamespace Ui {\nnamespace {\n\nconstexpr int kWideScale = 3;\n\nclass Item {\npublic:\n\tItem(\n\t\tconst style::MultiSelectItem &st,\n\t\tuint64 id,\n\t\tconst QString &text,\n\t\tstyle::color color,\n\t\tMultiSelect::PaintRoundImage &&paintRoundImage);\n\n\tuint64 id() const {\n\t\treturn _id;\n\t}\n\tint getWidth() const {\n\t\treturn _width;\n\t}\n\tQRect rect() const {\n\t\treturn QRect(_x, _y, _width, _st.height);\n\t}\n\tbool isOverDelete() const {\n\t\treturn _overDelete;\n\t}\n\tvoid setActive(bool active) {\n\t\t_active = active;\n\t}\n\tvoid setPosition(int x, int y, int outerWidth, int maxVisiblePadding);\n\tQRect paintArea(int outerWidth) const;\n\n\tvoid setUpdateCallback(Fn<void()> updateCallback) {\n\t\t_updateCallback = updateCallback;\n\t}\n\tvoid setText(const QString &text);\n\tvoid paint(Painter &p, int outerWidth);\n\n\tvoid mouseMoveEvent(QPoint point);\n\tvoid leaveEvent();\n\n\tvoid showAnimated() {\n\t\tsetVisibleAnimated(true);\n\t}\n\tvoid hideAnimated() {\n\t\tsetVisibleAnimated(false);\n\t}\n\tbool hideFinished() const {\n\t\treturn (_hiding && !_visibility.animating());\n\t}\n\n\nprivate:\n\tvoid setOver(bool over);\n\tvoid paintOnce(Painter &p, int x, int y, int outerWidth);\n\tvoid paintDeleteButton(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tfloat64 overOpacity);\n\tbool paintCached(Painter &p, int x, int y, int outerWidth);\n\tvoid prepareCache();\n\tvoid setVisibleAnimated(bool visible);\n\n\tconst style::MultiSelectItem &_st;\n\n\tuint64 _id;\n\tstruct SlideAnimation {\n\t\tSlideAnimation(\n\t\t\t\tFn<void()> updateCallback,\n\t\t\t\tint fromX,\n\t\t\t\tint toX,\n\t\t\t\tint y,\n\t\t\t\tfloat64 duration)\n\t\t\t: fromX(fromX)\n\t\t\t, toX(toX)\n\t\t\t, y(y) {\n\t\t\tx.start(updateCallback, fromX, toX, duration);\n\t\t}\n\t\tUi::Animations::Simple x;\n\t\tint fromX, toX;\n\t\tint y;\n\t};\n\tstd::vector<SlideAnimation> _copies;\n\tint _x = -1;\n\tint _y = -1;\n\tint _width = 0;\n\tText::String _text;\n\tstyle::color _color;\n\tbool _over = false;\n\tQPixmap _cache;\n\tUi::Animations::Simple _visibility;\n\tUi::Animations::Simple _overOpacity;\n\tbool _overDelete = false;\n\tbool _active = false;\n\tMultiSelect::PaintRoundImage _paintRoundImage;\n\tFn<void()> _updateCallback;\n\tbool _hiding = false;\n\n};\n\nItem::Item(\n\tconst style::MultiSelectItem &st,\n\tuint64 id,\n\tconst QString &text,\n\tstyle::color color,\n\tMultiSelect::PaintRoundImage &&paintRoundImage)\n: _st(st)\n, _id(id)\n, _color(color)\n, _paintRoundImage(std::move(paintRoundImage)) {\n\tsetText(text);\n}\n\nvoid Item::setText(const QString &text) {\n\t_text.setText(_st.style, text, NameTextOptions());\n\t_width = _st.height\n\t\t+ _st.padding.left()\n\t\t+ _text.maxWidth()\n\t\t+ _st.padding.right();\n\taccumulate_min(_width, _st.maxWidth);\n}\n\nvoid Item::paint(Painter &p, int outerWidth) {\n\tif (!_cache.isNull() && !_visibility.animating()) {\n\t\tif (_hiding) {\n\t\t\treturn;\n\t\t} else {\n\t\t\t_cache = QPixmap();\n\t\t}\n\t}\n\tif (_copies.empty()) {\n\t\tpaintOnce(p, _x, _y, outerWidth);\n\t} else {\n\t\tfor (auto i = _copies.begin(), e = _copies.end(); i != e;) {\n\t\t\tauto x = qRound(i->x.value(_x));\n\t\t\tauto y = i->y;\n\t\t\tauto animating = i->x.animating();\n\t\t\tif (animating || (y == _y)) {\n\t\t\t\tpaintOnce(p, x, y, outerWidth);\n\t\t\t}\n\t\t\tif (animating) {\n\t\t\t\t++i;\n\t\t\t} else {\n\t\t\t\ti = _copies.erase(i);\n\t\t\t\te = _copies.end();\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Item::paintOnce(Painter &p, int x, int y, int outerWidth) {\n\tif (!_cache.isNull()) {\n\t\tpaintCached(p, x, y, outerWidth);\n\t\treturn;\n\t}\n\n\tauto radius = _st.height / 2;\n\tauto inner = style::rtlrect(\n\t\tx + radius,\n\t\ty,\n\t\t_width - radius,\n\t\t_st.height,\n\t\touterWidth);\n\n\tauto clipEnabled = p.hasClipping();\n\tauto clip = clipEnabled ? p.clipRegion() : QRegion();\n\tp.setClipRect(inner);\n\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(_active ? _st.textActiveBg : _st.textBg);\n\t{\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.drawRoundedRect(\n\t\t\tstyle::rtlrect(x, y, _width, _st.height, outerWidth),\n\t\t\tradius,\n\t\t\tradius);\n\t}\n\n\tif (clipEnabled) {\n\t\tp.setClipRegion(clip);\n\t} else {\n\t\tp.setClipping(false);\n\t}\n\n\tauto overOpacity = _overOpacity.value(_over ? 1. : 0.);\n\tif (overOpacity < 1.) {\n\t\t_paintRoundImage(p, x, y, outerWidth, _st.height);\n\t}\n\tif (overOpacity > 0.) {\n\t\tpaintDeleteButton(p, x, y, outerWidth, overOpacity);\n\t}\n\n\tauto textLeft = _st.height + _st.padding.left();\n\tauto textWidth = _width - textLeft - _st.padding.right();\n\tp.setPen(_active ? _st.textActiveFg : _st.textFg);\n\t_text.drawLeftElided(\n\t\tp,\n\t\tx + textLeft,\n\t\ty + _st.padding.top(),\n\t\ttextWidth,\n\t\touterWidth);\n}\n\nvoid Item::paintDeleteButton(\n\t\tPainter &p,\n\t\tint x,\n\t\tint y,\n\t\tint outerWidth,\n\t\tfloat64 overOpacity) {\n\tp.setOpacity(overOpacity);\n\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(_color);\n\t{\n\t\tPainterHighQualityEnabler hq(p);\n\t\tp.drawEllipse(\n\t\t\tstyle::rtlrect(x, y, _st.height, _st.height, outerWidth));\n\t}\n\n\tCrossAnimation::paint(\n\t\tp,\n\t\t_st.deleteCross,\n\t\t_st.deleteFg,\n\t\tx,\n\t\ty,\n\t\touterWidth,\n\t\toverOpacity);\n\n\tp.setOpacity(1.);\n}\n\nbool Item::paintCached(Painter &p, int x, int y, int outerWidth) {\n\tPainterHighQualityEnabler hq(p);\n\n\tauto opacity = _visibility.value(_hiding ? 0. : 1.);\n\tauto height = opacity * _cache.height() / _cache.devicePixelRatio();\n\tauto width = opacity * _cache.width() / _cache.devicePixelRatio();\n\n\tp.setOpacity(opacity);\n\tp.drawPixmap(\n\t\tstyle::rtlrect(\n\t\t\tx + (_width - width) / 2.,\n\t\t\ty + (_st.height - height) / 2.,\n\t\t\twidth,\n\t\t\theight,\n\t\t\touterWidth),\n\t\t_cache);\n\tp.setOpacity(1.);\n\treturn true;\n}\n\nvoid Item::mouseMoveEvent(QPoint point) {\n\tif (!_cache.isNull()) {\n\t\treturn;\n\t}\n\t_overDelete = QRect(0, 0, _st.height, _st.height).contains(point);\n\tsetOver(true);\n}\n\nvoid Item::leaveEvent() {\n\t_overDelete = false;\n\tsetOver(false);\n}\n\nvoid Item::setPosition(int x, int y, int outerWidth, int maxVisiblePadding) {\n\tif (_x >= 0 && _y >= 0 && (_x != x || _y != y)) {\n\t\t// Make an animation if it is not the first setPosition().\n\t\tauto found = false;\n\t\tauto leftHidden = -_width - maxVisiblePadding;\n\t\tauto rightHidden = outerWidth + maxVisiblePadding;\n\t\tfor (auto i = _copies.begin(), e = _copies.end(); i != e;) {\n\t\t\tif (i->x.animating()) {\n\t\t\t\tif (i->y == y) {\n\t\t\t\t\ti->x.start(_updateCallback, i->toX, x, _st.duration);\n\t\t\t\t\tfound = true;\n\t\t\t\t} else {\n\t\t\t\t\ti->x.start(\n\t\t\t\t\t\t_updateCallback,\n\t\t\t\t\t\ti->fromX,\n\t\t\t\t\t\t(i->toX > i->fromX) ? rightHidden : leftHidden,\n\t\t\t\t\t\t_st.duration);\n\t\t\t\t}\n\t\t\t\t++i;\n\t\t\t} else {\n\t\t\t\ti = _copies.erase(i);\n\t\t\t\te = _copies.end();\n\t\t\t}\n\t\t}\n\t\tif (_copies.empty()) {\n\t\t\tif (_y == y) {\n\t\t\t\tauto copy = SlideAnimation(\n\t\t\t\t\t_updateCallback,\n\t\t\t\t\t_x,\n\t\t\t\t\tx,\n\t\t\t\t\t_y,\n\t\t\t\t\t_st.duration);\n\t\t\t\t_copies.push_back(std::move(copy));\n\t\t\t} else {\n\t\t\t\tauto copyHiding = SlideAnimation(\n\t\t\t\t\t_updateCallback,\n\t\t\t\t\t_x,\n\t\t\t\t\t(y > _y) ? rightHidden : leftHidden,\n\t\t\t\t\t_y,\n\t\t\t\t\t_st.duration);\n\t\t\t\t_copies.push_back(std::move(copyHiding));\n\t\t\t\tauto copyShowing = SlideAnimation(\n\t\t\t\t\t_updateCallback,\n\t\t\t\t\t(y > _y) ? leftHidden : rightHidden,\n\t\t\t\t\tx,\n\t\t\t\t\ty,\n\t\t\t\t\t_st.duration);\n\t\t\t\t_copies.push_back(std::move(copyShowing));\n\t\t\t}\n\t\t} else if (!found) {\n\t\t\tauto copy = SlideAnimation(\n\t\t\t\t_updateCallback,\n\t\t\t\t(y > _y) ? leftHidden : rightHidden,\n\t\t\t\tx,\n\t\t\t\ty,\n\t\t\t\t_st.duration);\n\t\t\t_copies.push_back(std::move(copy));\n\t\t}\n\t}\n\t_x = x;\n\t_y = y;\n}\n\nQRect Item::paintArea(int outerWidth) const {\n\tif (_copies.empty()) {\n\t\treturn rect();\n\t}\n\tauto yMin = 0, yMax = 0;\n\tfor (const auto &copy : _copies) {\n\t\taccumulate_max(yMax, copy.y);\n\t\tif (yMin) {\n\t\t\taccumulate_min(yMin, copy.y);\n\t\t} else {\n\t\t\tyMin = copy.y;\n\t\t}\n\t}\n\treturn QRect(0, yMin, outerWidth, yMax - yMin + _st.height);\n}\n\nvoid Item::prepareCache() {\n\tif (!_cache.isNull()) return;\n\n\tAssert(!_visibility.animating());\n\tauto cacheWidth = _width * kWideScale * style::DevicePixelRatio();\n\tauto cacheHeight = _st.height * kWideScale * style::DevicePixelRatio();\n\tauto data = QImage(\n\t\tcacheWidth,\n\t\tcacheHeight,\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tdata.fill(Qt::transparent);\n\tdata.setDevicePixelRatio(style::DevicePixelRatio());\n\t{\n\t\tPainter p(&data);\n\t\tpaintOnce(\n\t\t\tp,\n\t\t\t_width * (kWideScale - 1) / 2,\n\t\t\t_st.height * (kWideScale - 1) / 2,\n\t\t\tcacheWidth);\n\t}\n\t_cache = Ui::PixmapFromImage(std::move(data));\n}\n\nvoid Item::setVisibleAnimated(bool visible) {\n\t_hiding = !visible;\n\tprepareCache();\n\tauto from = visible ? 0. : 1.;\n\tauto to = visible ? 1. : 0.;\n\tauto transition = visible ? anim::bumpy(1.0625) : anim::linear;\n\t_visibility.start(_updateCallback, from, to, _st.duration, transition);\n}\n\nvoid Item::setOver(bool over) {\n\tif (over != _over) {\n\t\t_over = over;\n\t\t_overOpacity.start(\n\t\t\t_updateCallback,\n\t\t\t_over ? 0. : 1.,\n\t\t\t_over ? 1. : 0.,\n\t\t\t_st.duration);\n\t}\n}\n\n} // namespace\n\nclass MultiSelect::Inner : public RpWidget {\npublic:\n\tusing ScrollCallback = Fn<void(int activeTop, int activeBottom)>;\n\tInner(\n\t\tQWidget *parent,\n\t\tconst style::MultiSelect &st,\n\t\trpl::producer<QString> placeholder,\n\t\tconst QString &query,\n\t\tScrollCallback callback);\n\n\t[[nodiscard]] QString getQuery() const;\n\tvoid setQuery(const QString &query);\n\tbool setInnerFocus();\n\tvoid clearQuery();\n\n\tvoid setQueryChangedCallback(Fn<void(const QString &query)> callback);\n\tvoid setSubmittedCallback(Fn<void(Qt::KeyboardModifiers)> callback);\n\tvoid setCancelledCallback(Fn<void()> callback);\n\n\tvoid addItemInBunch(std::unique_ptr<Item> item);\n\tvoid finishItemsBunch(AddItemWay way);\n\tvoid setItemText(uint64 itemId, const QString &text);\n\n\tvoid setItemRemovedCallback(Fn<void(uint64 itemId)> callback);\n\tvoid removeItem(uint64 itemId);\n\n\tint getItemsCount() const;\n\tQVector<uint64> getItems() const;\n\tbool hasItem(uint64 itemId) const;\n\n\tvoid setResizedCallback(Fn<void(int heightDelta)> callback);\n\n\t~Inner();\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tvoid cancelled();\n\tvoid queryChanged();\n\tvoid fieldFocused();\n\tvoid computeItemsGeometry(int newWidth);\n\tvoid updateItemsGeometry();\n\tvoid updateFieldGeometry();\n\tvoid updateHasAnyItems(bool hasAnyItems);\n\tvoid updateSelection(QPoint mousePosition);\n\tvoid clearSelection() {\n\t\tupdateSelection(QPoint(-1, -1));\n\t}\n\tvoid updateCursor();\n\tvoid updateHeightStep();\n\tvoid finishHeightAnimation();\n\tenum class ChangeActiveWay {\n\t\tDefault,\n\t\tSkipSetFocus,\n\t};\n\tvoid setActiveItem(\n\t\tint active,\n\t\tChangeActiveWay skipSetFocus = ChangeActiveWay::Default);\n\tvoid setActiveItemPrevious();\n\tvoid setActiveItemNext();\n\n\tQMargins itemPaintMargins() const;\n\n\tconst style::MultiSelect &_st;\n\tUi::Animations::Simple _iconOpacity;\n\n\tScrollCallback _scrollCallback;\n\n\tstd::set<uint64> _idsMap;\n\tstd::vector<std::unique_ptr<Item>> _items;\n\tstd::set<std::unique_ptr<Item>> _removingItems;\n\n\tint _selected = -1;\n\tint _active = -1;\n\tbool _overDelete = false;\n\n\tint _fieldLeft = 0;\n\tint _fieldTop = 0;\n\tint _fieldWidth = 0;\n\tobject_ptr<Ui::InputField> _field;\n\tobject_ptr<Ui::CrossButton> _cancel;\n\n\tint _newHeight = 0;\n\tUi::Animations::Simple _height;\n\n\tFn<void(const QString &query)> _queryChangedCallback;\n\tFn<void(Qt::KeyboardModifiers)> _submittedCallback;\n\tFn<void()> _cancelledCallback;\n\tFn<void(uint64 itemId)> _itemRemovedCallback;\n\tFn<void(int heightDelta)> _resizedCallback;\n\n};\n\nMultiSelect::MultiSelect(\n\tQWidget *parent,\n\tconst style::MultiSelect &st,\n\trpl::producer<QString> placeholder,\n\tconst QString &query)\n: RpWidget(parent)\n, _st(st)\n, _scroll(this, _st.scroll) {\n\tconst auto scrollCallback = [=](int activeTop, int activeBottom) {\n\t\tscrollTo(activeTop, activeBottom);\n\t};\n\t_inner = _scroll->setOwnedWidget(object_ptr<Inner>(\n\t\tthis,\n\t\tst,\n\t\tstd::move(placeholder),\n\t\tquery,\n\t\tscrollCallback));\n\t_scroll->installEventFilter(this);\n\t_inner->setResizedCallback([this](int innerHeightDelta) {\n\t\tauto newHeight = resizeGetHeight(width());\n\t\tif (innerHeightDelta > 0) {\n\t\t\t_scroll->scrollToY(_scroll->scrollTop() + innerHeightDelta);\n\t\t}\n\t\tif (newHeight != height()) {\n\t\t\tresize(width(), newHeight);\n\t\t\tif (_resizedCallback) {\n\t\t\t\t_resizedCallback();\n\t\t\t}\n\t\t}\n\t});\n\t_inner->setQueryChangedCallback([this](const QString &query) {\n\t\t_scroll->scrollToY(_scroll->scrollTopMax());\n\t\tif (_queryChangedCallback) {\n\t\t\t_queryChangedCallback(query);\n\t\t}\n\t});\n\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\tauto defaultWidth = _st.item.maxWidth + _st.fieldMinWidth + _st.fieldCancelSkip;\n\tresizeToWidth(_st.padding.left() + defaultWidth + _st.padding.right());\n}\n\nbool MultiSelect::eventFilter(QObject *o, QEvent *e) {\n\tif (o == _scroll && e->type() == QEvent::KeyPress) {\n\t\te->ignore();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid MultiSelect::scrollTo(int activeTop, int activeBottom) {\n\tauto scrollTop = _scroll->scrollTop();\n\tauto scrollHeight = _scroll->height();\n\tauto scrollBottom = scrollTop + scrollHeight;\n\tif (scrollTop > activeTop) {\n\t\t_scroll->scrollToY(activeTop);\n\t} else if (scrollBottom < activeBottom) {\n\t\t_scroll->scrollToY(activeBottom - scrollHeight);\n\t}\n}\n\nvoid MultiSelect::setQueryChangedCallback(Fn<void(const QString &query)> callback) {\n\t_queryChangedCallback = std::move(callback);\n}\n\nvoid MultiSelect::setSubmittedCallback(Fn<void(Qt::KeyboardModifiers)> callback) {\n\t_inner->setSubmittedCallback(std::move(callback));\n}\n\nvoid MultiSelect::setCancelledCallback(Fn<void()> callback) {\n\t_inner->setCancelledCallback(std::move(callback));\n}\n\nvoid MultiSelect::setResizedCallback(Fn<void()> callback) {\n\t_resizedCallback = std::move(callback);\n}\n\nvoid MultiSelect::setInnerFocus() {\n\tif (_inner->setInnerFocus()) {\n\t\t_scroll->scrollToY(_scroll->scrollTopMax());\n\t}\n}\n\nvoid MultiSelect::clearQuery() {\n\t_inner->clearQuery();\n}\n\nQString MultiSelect::getQuery() const {\n\treturn _inner->getQuery();\n}\n\nvoid MultiSelect::setQuery(const QString &query) {\n\t_inner->setQuery(query);\n}\n\nvoid MultiSelect::addItem(uint64 itemId, const QString &text, style::color color, PaintRoundImage paintRoundImage, AddItemWay way) {\n\taddItemInBunch(itemId, text, color, std::move(paintRoundImage));\n\t_inner->finishItemsBunch(way);\n}\n\nvoid MultiSelect::addItemInBunch(uint64 itemId, const QString &text, style::color color, PaintRoundImage paintRoundImage) {\n\t_inner->addItemInBunch(std::make_unique<Item>(_st.item, itemId, text, color, std::move(paintRoundImage)));\n}\n\nvoid MultiSelect::finishItemsBunch() {\n\t_inner->finishItemsBunch(AddItemWay::SkipAnimation);\n}\n\nvoid MultiSelect::setItemRemovedCallback(Fn<void(uint64 itemId)> callback) {\n\t_inner->setItemRemovedCallback(std::move(callback));\n}\n\nvoid MultiSelect::removeItem(uint64 itemId) {\n\t_inner->removeItem(itemId);\n}\n\nint MultiSelect::getItemsCount() const {\n\treturn _inner->getItemsCount();\n}\n\nQVector<uint64> MultiSelect::getItems() const {\n\treturn _inner->getItems();\n}\n\nbool MultiSelect::hasItem(uint64 itemId) const {\n\treturn _inner->hasItem(itemId);\n}\n\nint MultiSelect::resizeGetHeight(int newWidth) {\n\tif (newWidth != _inner->width()) {\n\t\t_inner->resizeToWidth(newWidth);\n\t}\n\tauto newHeight = qMin(_inner->height(), _st.maxHeight);\n\t_scroll->setGeometryToLeft(0, 0, newWidth, newHeight);\n\treturn newHeight;\n}\n\nMultiSelect::Inner::Inner(\n\tQWidget *parent,\n\tconst style::MultiSelect &st,\n\trpl::producer<QString> placeholder,\n\tconst QString &query,\n\tScrollCallback callback)\n: RpWidget(parent)\n, _st(st)\n, _scrollCallback(std::move(callback))\n, _field(this, _st.field, std::move(placeholder), query)\n, _cancel(this, _st.fieldCancel) {\n\t_field->customUpDown(true);\n\t_field->focusedChanges(\n\t) | rpl::filter(rpl::mappers::_1) | rpl::on_next([=] {\n\t\tfieldFocused();\n\t}, _field->lifetime());\n\t_field->changes(\n\t) | rpl::on_next([=] {\n\t\tqueryChanged();\n\t}, _field->lifetime());\n\t_field->submits(\n\t) | rpl::on_next([=](Qt::KeyboardModifiers m) {\n\t\tif (_submittedCallback) {\n\t\t\t_submittedCallback(m);\n\t\t}\n\t}, _field->lifetime());\n\t_field->cancelled(\n\t) | rpl::on_next([=] {\n\t\tcancelled();\n\t}, _field->lifetime());\n\t_cancel->setClickedCallback([=] {\n\t\tclearQuery();\n\t\t_field->setFocus();\n\t});\n\tsetMouseTracking(true);\n}\n\nvoid MultiSelect::Inner::queryChanged() {\n\tauto query = getQuery();\n\t_cancel->toggle(!query.isEmpty(), anim::type::normal);\n\tupdateFieldGeometry();\n\tif (_queryChangedCallback) {\n\t\t_queryChangedCallback(query);\n\t}\n}\n\nQString MultiSelect::Inner::getQuery() const {\n\treturn _field->getLastText().trimmed();\n}\n\nvoid MultiSelect::Inner::setQuery(const QString &query) {\n\t_field->setText(query);\n\tif (const auto last = _field->getLastText(); !last.isEmpty()) {\n\t\t_field->setCursorPosition(last.size());\n\t}\n}\n\nbool MultiSelect::Inner::setInnerFocus() {\n\tif (_active >= 0) {\n\t\tsetFocus();\n\t} else if (!_field->hasFocus()) {\n\t\t_field->setFocusFast();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid MultiSelect::Inner::clearQuery() {\n\t_field->setText(QString());\n}\n\nvoid MultiSelect::Inner::setQueryChangedCallback(Fn<void(const QString &query)> callback) {\n\t_queryChangedCallback = std::move(callback);\n}\n\nvoid MultiSelect::Inner::setSubmittedCallback(\n\t\tFn<void(Qt::KeyboardModifiers)> callback) {\n\t_submittedCallback = std::move(callback);\n}\n\nvoid MultiSelect::Inner::setCancelledCallback(Fn<void()> callback) {\n\t_cancelledCallback = std::move(callback);\n}\n\nvoid MultiSelect::Inner::updateFieldGeometry() {\n\tauto fieldFinalWidth = _fieldWidth;\n\tif (_cancel->toggled()) {\n\t\tfieldFinalWidth -= _st.fieldCancelSkip;\n\t}\n\t_field->resizeToWidth(fieldFinalWidth);\n\t_field->moveToLeft(_st.padding.left() + _fieldLeft, _st.padding.top() + _fieldTop);\n}\n\nvoid MultiSelect::Inner::updateHasAnyItems(bool hasAnyItems) {\n\t_field->setPlaceholderHidden(hasAnyItems);\n\tupdateCursor();\n\t_iconOpacity.start([this] {\n\t\trtlupdate(_st.padding.left(), _st.padding.top(), _st.fieldIcon.width(), _st.fieldIcon.height());\n\t}, hasAnyItems ? 1. : 0., hasAnyItems ? 0. : 1., _st.item.duration);\n}\n\nvoid MultiSelect::Inner::updateCursor() {\n\tsetCursor(_items.empty() ? style::cur_text : (_overDelete ? style::cur_pointer : style::cur_default));\n}\n\nvoid MultiSelect::Inner::setActiveItem(int active, ChangeActiveWay skipSetFocus) {\n\tif (_active == active) return;\n\n\tif (_active >= 0) {\n\t\tAssert(_active < _items.size());\n\t\t_items[_active]->setActive(false);\n\t}\n\t_active = active;\n\tif (_active >= 0) {\n\t\tAssert(_active < _items.size());\n\t\t_items[_active]->setActive(true);\n\t}\n\tif (skipSetFocus != ChangeActiveWay::SkipSetFocus) {\n\t\tsetInnerFocus();\n\t}\n\tif (_scrollCallback) {\n\t\tauto rect = (_active >= 0) ? _items[_active]->rect() : _field->geometry().translated(-_st.padding.left(), -_st.padding.top());\n\t\t_scrollCallback(rect.y(), rect.y() + rect.height() + _st.padding.top() + _st.padding.bottom());\n\t}\n\tupdate();\n}\n\nvoid MultiSelect::Inner::setActiveItemPrevious() {\n\tif (_active > 0) {\n\t\tsetActiveItem(_active - 1);\n\t} else if (_active < 0 && !_items.empty()) {\n\t\tsetActiveItem(_items.size() - 1);\n\t}\n}\n\nvoid MultiSelect::Inner::setActiveItemNext() {\n\tif (_active >= 0 && _active + 1 < _items.size()) {\n\t\tsetActiveItem(_active + 1);\n\t} else {\n\t\tsetActiveItem(-1);\n\t}\n}\n\nint MultiSelect::Inner::resizeGetHeight(int newWidth) {\n\tif (newWidth <= 0) {\n\t\treturn height();\n\t}\n\tcomputeItemsGeometry(newWidth);\n\tupdateFieldGeometry();\n\n\tauto cancelLeft = _fieldLeft + _fieldWidth + _st.padding.right() - _cancel->width();\n\tauto cancelTop = _fieldTop - _st.padding.top();\n\t_cancel->moveToLeft(_st.padding.left() + cancelLeft, _st.padding.top() + cancelTop);\n\n\treturn _field->y() + _field->height() + _st.padding.bottom();\n}\n\nvoid MultiSelect::Inner::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tauto paintRect = e->rect();\n\tp.fillRect(paintRect, _st.bg);\n\n\tauto offset = QPoint(\n\t\tstyle::RightToLeft() ? _st.padding.right() : _st.padding.left(),\n\t\t_st.padding.top());\n\tp.translate(offset);\n\tpaintRect.translate(-offset);\n\n\tauto outerWidth = width() - _st.padding.left() - _st.padding.right();\n\tauto iconOpacity = _iconOpacity.value(_items.empty() ? 1. : 0.);\n\tif (iconOpacity > 0.) {\n\t\tp.setOpacity(iconOpacity);\n\t\t_st.fieldIcon.paint(p, 0, 0, outerWidth);\n\t\tp.setOpacity(1.);\n\t}\n\n\tauto checkRect = myrtlrect(paintRect);\n\tauto paintMargins = itemPaintMargins();\n\tfor (auto i = _removingItems.begin(), e = _removingItems.end(); i != e;) {\n\t\tauto &item = *i;\n\t\tauto itemRect = item->paintArea(outerWidth);\n\t\titemRect = itemRect.marginsAdded(paintMargins);\n\t\tif (checkRect.intersects(itemRect)) {\n\t\t\titem->paint(p, outerWidth);\n\t\t}\n\t\tif (item->hideFinished()) {\n\t\t\ti = _removingItems.erase(i);\n\t\t\te = _removingItems.end();\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tfor (const auto &item : _items) {\n\t\tauto itemRect = item->paintArea(outerWidth);\n\t\titemRect = itemRect.marginsAdded(paintMargins);\n\t\tif (checkRect.y() + checkRect.height() <= itemRect.y()) {\n\t\t\tbreak;\n\t\t} else if (checkRect.intersects(itemRect)) {\n\t\t\titem->paint(p, outerWidth);\n\t\t}\n\t}\n}\n\nQMargins MultiSelect::Inner::itemPaintMargins() const {\n\treturn {\n\t\tqMax(_st.itemSkip, _st.padding.left()),\n\t\t_st.itemSkip,\n\t\tqMax(_st.itemSkip, _st.padding.right()),\n\t\t_st.itemSkip,\n\t};\n}\n\nvoid MultiSelect::Inner::leaveEventHook(QEvent *e) {\n\tclearSelection();\n}\n\nvoid MultiSelect::Inner::mouseMoveEvent(QMouseEvent *e) {\n\tupdateSelection(e->pos());\n}\n\nvoid MultiSelect::Inner::keyPressEvent(QKeyEvent *e) {\n\tif (_active >= 0) {\n\t\tExpects(_active < _items.size());\n\t\tif (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace) {\n\t\t\tauto itemId = _items[_active]->id();\n\t\t\tsetActiveItemNext();\n\t\t\tremoveItem(itemId);\n\t\t} else if (e->key() == Qt::Key_Left) {\n\t\t\tsetActiveItemPrevious();\n\t\t} else if (e->key() == Qt::Key_Right) {\n\t\t\tsetActiveItemNext();\n\t\t} else if (e->key() == Qt::Key_Escape) {\n\t\t\tsetActiveItem(-1);\n\t\t} else {\n\t\t\te->ignore();\n\t\t}\n\t} else if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Backspace) {\n\t\tsetActiveItemPrevious();\n\t} else {\n\t\te->ignore();\n\t}\n}\n\nvoid MultiSelect::Inner::cancelled() {\n\tif (_cancelledCallback) {\n\t\t_cancelledCallback();\n\t}\n}\n\nvoid MultiSelect::Inner::fieldFocused() {\n\tsetActiveItem(-1, ChangeActiveWay::SkipSetFocus);\n}\n\nvoid MultiSelect::Inner::updateSelection(QPoint mousePosition) {\n\tauto point = myrtlpoint(mousePosition) - QPoint(_st.padding.left(), _st.padding.right());\n\tauto selected = -1;\n\tfor (auto i = 0, count = int(_items.size()); i != count; ++i) {\n\t\tauto itemRect = _items[i]->rect();\n\t\tif (itemRect.y() > point.y()) {\n\t\t\tbreak;\n\t\t} else if (itemRect.contains(point)) {\n\t\t\tpoint -= itemRect.topLeft();\n\t\t\tselected = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (_selected != selected) {\n\t\tif (_selected >= 0) {\n\t\t\tAssert(_selected < _items.size());\n\t\t\t_items[_selected]->leaveEvent();\n\t\t}\n\t\t_selected = selected;\n\t\tupdate();\n\t}\n\tauto overDelete = false;\n\tif (_selected >= 0) {\n\t\t_items[_selected]->mouseMoveEvent(point);\n\t\toverDelete = _items[_selected]->isOverDelete();\n\t}\n\tif (_overDelete != overDelete) {\n\t\t_overDelete = overDelete;\n\t\tupdateCursor();\n\t}\n}\n\nvoid MultiSelect::Inner::mousePressEvent(QMouseEvent *e) {\n\tif (_overDelete) {\n\t\tExpects(_selected >= 0);\n\t\tExpects(_selected < _items.size());\n\t\tremoveItem(_items[_selected]->id());\n\t} else if (_selected >= 0) {\n\t\tsetActiveItem(_selected);\n\t} else {\n\t\tsetInnerFocus();\n\t}\n}\n\nvoid MultiSelect::Inner::addItemInBunch(std::unique_ptr<Item> item) {\n\tauto wasEmpty = _items.empty();\n\titem->setUpdateCallback([this, item = item.get()] {\n\t\tauto itemRect = item->paintArea(width() - _st.padding.left() - _st.padding.top());\n\t\titemRect = itemRect.translated(_st.padding.left(), _st.padding.top());\n\t\titemRect = itemRect.marginsAdded(itemPaintMargins());\n\t\trtlupdate(itemRect);\n\t});\n\t_idsMap.insert(item->id());\n\t_items.push_back(std::move(item));\n\tif (wasEmpty) {\n\t\tupdateHasAnyItems(true);\n\t}\n}\n\nvoid MultiSelect::Inner::finishItemsBunch(AddItemWay way) {\n\tupdateItemsGeometry();\n\tif (way != AddItemWay::SkipAnimation) {\n\t\t_items.back()->showAnimated();\n\t} else {\n\t\t_field->finishAnimating();\n\t\tfinishHeightAnimation();\n\t}\n}\n\nvoid MultiSelect::Inner::computeItemsGeometry(int newWidth) {\n\tnewWidth -= _st.padding.left() + _st.padding.right();\n\n\tauto itemLeft = 0;\n\tauto itemTop = 0;\n\tauto widthLeft = newWidth;\n\tauto maxVisiblePadding = qMax(_st.padding.left(), _st.padding.right());\n\tfor (const auto &item : _items) {\n\t\tauto itemWidth = item->getWidth();\n\t\tAssert(itemWidth <= newWidth);\n\t\tif (itemWidth > widthLeft) {\n\t\t\titemLeft = 0;\n\t\t\titemTop += _st.item.height + _st.itemSkip;\n\t\t\twidthLeft = newWidth;\n\t\t}\n\t\titem->setPosition(itemLeft, itemTop, newWidth, maxVisiblePadding);\n\t\titemLeft += itemWidth + _st.itemSkip;\n\t\twidthLeft -= itemWidth + _st.itemSkip;\n\t}\n\n\tauto fieldMinWidth = _st.fieldMinWidth + _st.fieldCancelSkip;\n\tAssert(fieldMinWidth <= newWidth);\n\tif (fieldMinWidth > widthLeft) {\n\t\t_fieldLeft = 0;\n\t\t_fieldTop = itemTop + _st.item.height + _st.itemSkip;\n\t} else {\n\t\t_fieldLeft = itemLeft + (_items.empty() ? _st.fieldIconSkip : 0);\n\t\t_fieldTop = itemTop;\n\t}\n\t_fieldWidth = newWidth - _fieldLeft;\n}\n\nvoid MultiSelect::Inner::updateItemsGeometry() {\n\tauto newHeight = resizeGetHeight(width());\n\tif (newHeight == _newHeight) return;\n\n\t_newHeight = newHeight;\n\t_height.start([this] { updateHeightStep(); }, height(), _newHeight, _st.item.duration);\n}\n\nvoid MultiSelect::Inner::updateHeightStep() {\n\tauto newHeight = qRound(_height.value(_newHeight));\n\tif (auto heightDelta = newHeight - height()) {\n\t\tresize(width(), newHeight);\n\t\tif (_resizedCallback) {\n\t\t\t_resizedCallback(heightDelta);\n\t\t}\n\t\tupdate();\n\t}\n}\n\nvoid MultiSelect::Inner::finishHeightAnimation() {\n\t_height.stop();\n\tupdateHeightStep();\n}\n\nvoid MultiSelect::Inner::setItemText(uint64 itemId, const QString &text) {\n\tfor (const auto &item : _items) {\n\t\tif (item->id() == itemId) {\n\t\t\titem->setText(text);\n\t\t\tupdateItemsGeometry();\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nvoid MultiSelect::Inner::setItemRemovedCallback(Fn<void(uint64 itemId)> callback) {\n\t_itemRemovedCallback = std::move(callback);\n}\n\nvoid MultiSelect::Inner::setResizedCallback(Fn<void(int heightDelta)> callback) {\n\t_resizedCallback = std::move(callback);\n}\n\nvoid MultiSelect::Inner::removeItem(uint64 itemId) {\n\tauto found = false;\n\tfor (auto i = 0, count = int(_items.size()); i != count; ++i) {\n\t\tauto &item = _items[i];\n\t\tif (item->id() == itemId) {\n\t\t\tfound = true;\n\t\t\tclearSelection();\n\n\t\t\titem->hideAnimated();\n\t\t\t_idsMap.erase(item->id());\n\t\t\t_removingItems.insert(std::move(item));\n\t\t\t_items.erase(_items.begin() + i);\n\n\t\t\tif (_active == i) {\n\t\t\t\t_active = -1;\n\t\t\t} else if (_active > i) {\n\t\t\t\t--_active;\n\t\t\t}\n\n\t\t\tupdateItemsGeometry();\n\t\t\tif (_items.empty()) {\n\t\t\t\tupdateHasAnyItems(false);\n\t\t\t}\n\t\t\tauto point = QCursor::pos();\n\t\t\tif (auto parent = parentWidget()) {\n\t\t\t\tif (parent->rect().contains(parent->mapFromGlobal(point))) {\n\t\t\t\t\tupdateSelection(mapFromGlobal(point));\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (found && _itemRemovedCallback) {\n\t\t_itemRemovedCallback(itemId);\n\t}\n\tsetInnerFocus();\n}\n\nint MultiSelect::Inner::getItemsCount() const {\n\treturn _items.size();\n}\n\nQVector<uint64> MultiSelect::Inner::getItems() const {\n\tauto result = QVector<uint64>();\n\tresult.reserve(_items.size());\n\tfor (const auto &item : _items) {\n\t\tresult.push_back(item->id());\n\t}\n\treturn result;\n}\n\nbool MultiSelect::Inner::hasItem(uint64 itemId) const {\n\treturn _idsMap.find(itemId) != _idsMap.cend();\n}\n\nMultiSelect::Inner::~Inner() = default;\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/multi_select.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/object_ptr.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace style {\nstruct MultiSelect;\n} // namespace style\n\nnamespace Ui {\n\nclass ScrollArea;\n\nclass MultiSelect : public RpWidget {\npublic:\n\tMultiSelect(\n\t\tQWidget *parent,\n\t\tconst style::MultiSelect &st,\n\t\trpl::producer<QString> placeholder = nullptr,\n\t\tconst QString &query = QString());\n\n\t[[nodiscard]] QString getQuery() const;\n\tvoid setQuery(const QString &query);\n\tvoid setInnerFocus();\n\tvoid clearQuery();\n\n\tvoid setQueryChangedCallback(Fn<void(const QString &query)> callback);\n\tvoid setSubmittedCallback(Fn<void(Qt::KeyboardModifiers)> callback);\n\tvoid setCancelledCallback(Fn<void()> callback);\n\tvoid setResizedCallback(Fn<void()> callback);\n\n\tenum class AddItemWay {\n\t\tDefault,\n\t\tSkipAnimation,\n\t};\n\tusing PaintRoundImage = Fn<void(Painter &p, int x, int y, int outerWidth, int size)>;\n\tvoid addItem(uint64 itemId, const QString &text, style::color color, PaintRoundImage paintRoundImage, AddItemWay way = AddItemWay::Default);\n\tvoid addItemInBunch(uint64 itemId, const QString &text, style::color color, PaintRoundImage paintRoundImage);\n\tvoid finishItemsBunch();\n\tvoid setItemText(uint64 itemId, const QString &text);\n\n\tvoid setItemRemovedCallback(Fn<void(uint64 itemId)> callback);\n\tvoid removeItem(uint64 itemId);\n\n\tint getItemsCount() const;\n\tQVector<uint64> getItems() const;\n\tbool hasItem(uint64 itemId) const;\n\nprotected:\n\tint resizeGetHeight(int newWidth) override;\n\tbool eventFilter(QObject *o, QEvent *e) override;\n\nprivate:\n\tvoid scrollTo(int activeTop, int activeBottom);\n\n\tconst style::MultiSelect &_st;\n\n\tobject_ptr<Ui::ScrollArea> _scroll;\n\n\tclass Inner;\n\tQPointer<Inner> _inner;\n\n\tFn<void()> _resizedCallback;\n\tFn<void(const QString &query)> _queryChangedCallback;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/participants_check_view.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/participants_check_view.h\"\n\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/effects/toggle_arrow.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace Ui {\n\nParticipantsCheckView::ParticipantsCheckView(\n\tint count,\n\tint duration,\n\tbool checked,\n\tFn<void()> updateCallback)\n: Ui::AbstractCheckView(duration, checked, std::move(updateCallback))\n, _text(QString::number(std::abs(count)))\n, _count(count) {\n}\n\nQSize ParticipantsCheckView::ComputeSize(int count) {\n\treturn QSize(\n\t\tst::moderateBoxExpandHeight\n\t\t\t+ st::moderateBoxExpand.width()\n\t\t\t+ st::moderateBoxExpandInnerSkip * 4\n\t\t\t+ st::moderateBoxExpandFont->width(\n\t\t\t\tQString::number(std::abs(count)))\n\t\t\t+ st::moderateBoxExpandToggleSize,\n\t\tst::moderateBoxExpandHeight);\n}\n\nQSize ParticipantsCheckView::getSize() const {\n\treturn ComputeSize(_count);\n}\n\nvoid ParticipantsCheckView::paint(\n\t\tQPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint outerWidth) {\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto size = getSize();\n\tconst auto radius = size.height() / 2;\n\tp.setPen(Qt::NoPen);\n\tst::moderateBoxExpand.paint(\n\t\tp,\n\t\tradius,\n\t\tleft + (size.height() - st::moderateBoxExpand.height()) / 2,\n\t\ttop + size.width());\n\n\tconst auto innerSkip = st::moderateBoxExpandInnerSkip;\n\n\tp.setBrush(Qt::NoBrush);\n\tp.setPen(st::boxTextFg);\n\tp.setFont(st::moderateBoxExpandFont);\n\tp.drawText(\n\t\tQRect(\n\t\t\tleft + innerSkip + radius + st::moderateBoxExpand.width(),\n\t\t\ttop,\n\t\t\tsize.width(),\n\t\t\tsize.height()),\n\t\t_text,\n\t\tstyle::al_left);\n\n\tconst auto path = Ui::ToggleUpDownArrowPath(\n\t\tleft + size.width() - st::moderateBoxExpandToggleSize - radius,\n\t\ttop + size.height() / 2,\n\t\tst::moderateBoxExpandToggleSize,\n\t\tst::moderateBoxExpandToggleFourStrokes,\n\t\tUi::AbstractCheckView::currentAnimationValue());\n\tp.fillPath(path, st::boxTextFg);\n}\n\nQImage ParticipantsCheckView::prepareRippleMask() const {\n\tconst auto size = getSize();\n\treturn Ui::RippleAnimation::RoundRectMask(size, size.height() / 2);\n}\n\nbool ParticipantsCheckView::checkRippleStartPosition(QPoint position) const {\n\treturn Rect(getSize()).contains(position);\n}\n\nvoid ParticipantsCheckView::checkedChangedHook(anim::type animated) {\n}\n\nParticipantsCheckView::~ParticipantsCheckView() = default;\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/participants_check_view.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/checkbox.h\"\n\nnamespace Ui {\n\nclass ParticipantsCheckView : public Ui::AbstractCheckView {\npublic:\n\tParticipantsCheckView(\n\t\tint count,\n\t\tint duration,\n\t\tbool checked,\n\t\tFn<void()> updateCallback);\n\n\t[[nodiscard]] static QSize ComputeSize(int count);\n\n\tQSize getSize() const override;\n\n\tvoid paint(QPainter &p, int left, int top, int outerWidth) override;\n\tQImage prepareRippleMask() const override;\n\tbool checkRippleStartPosition(QPoint position) const override;\n\n\t~ParticipantsCheckView();\n\nprivate:\n\tconst QString _text;\n\tconst int _count;\n\tvoid checkedChangedHook(anim::type animated) override;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/peer_bubble.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/peer_bubble.h\"\n\n#include \"data/data_peer.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/widgets/labels.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_channel_earn.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Ui {\n\nobject_ptr<Ui::RpWidget> CreatePeerBubble(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<PeerData*> peer) {\n\tauto owned = object_ptr<Ui::RpWidget>(parent);\n\tconst auto peerBubble = owned.data();\n\tpeerBubble->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tconst auto left = Ui::CreateChild<Ui::UserpicButton>(\n\t\tpeerBubble,\n\t\tpeer,\n\t\tst::uploadUserpicButton);\n\tconst auto right = Ui::CreateChild<Ui::FlatLabel>(\n\t\tpeerBubble,\n\t\tInfo::Profile::NameValue(peer),\n\t\tst::channelEarnSemiboldLabel);\n\tconst auto padding = st::chatGiveawayPeerPadding\n\t\t+ QMargins(st::chatGiveawayPeerPadding.left(), 0, 0, 0);\n\trpl::combine(\n\t\tleft->sizeValue(),\n\t\tright->sizeValue()\n\t) | rpl::on_next([=](\n\t\t\tconst QSize &leftSize,\n\t\t\tconst QSize &rightSize) {\n\t\tpeerBubble->setNaturalWidth(\n\t\t\tleftSize.width() + rightSize.width() + rect::m::sum::h(padding));\n\t\tpeerBubble->resize(peerBubble->naturalWidth(), leftSize.height());\n\t\tleft->moveToLeft(0, 0);\n\t\tright->moveToRight(padding.right() + st::lineWidth, padding.top());\n\t\tconst auto maxRightSize = parent->width()\n\t\t\t- rect::m::sum::h(st::boxRowPadding)\n\t\t\t- rect::m::sum::h(padding)\n\t\t\t- leftSize.width();\n\t\tif ((rightSize.width() > maxRightSize) && (maxRightSize > 0)) {\n\t\t\tright->resizeToWidth(maxRightSize);\n\t\t}\n\t}, peerBubble->lifetime());\n\tpeerBubble->paintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(peerBubble);\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::windowBgOver);\n\t\tconst auto rect = peerBubble->rect();\n\t\tconst auto radius = rect.height() / 2;\n\t\tp.drawRoundedRect(rect, radius, radius);\n\t}, peerBubble->lifetime());\n\n\treturn owned;\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/peer_bubble.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\ntemplate <typename Object>\nclass object_ptr;\n\nclass PeerData;\n\nnamespace Ui {\nclass RpWidget;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Ui {\n\n[[nodiscard]] object_ptr<Ui::RpWidget> CreatePeerBubble(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<PeerData*> peer);\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/sent_code_field.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/sent_code_field.h\"\n\n#include \"lang/lang_keys.h\"\n\n#include <QRegularExpression>\n\nnamespace Ui {\n\nSentCodeField::SentCodeField(\n\tQWidget *parent,\n\tconst style::InputField &st,\n\trpl::producer<QString> placeholder,\n\tconst QString &val)\n: Ui::InputField(parent, st, std::move(placeholder), val) {\n\tchanges() | rpl::on_next([=] { fix(); }, lifetime());\n}\n\nvoid SentCodeField::setAutoSubmit(int length, Fn<void()> submitCallback) {\n\t_autoSubmitLength = length;\n\t_submitCallback = std::move(submitCallback);\n}\n\nvoid SentCodeField::setChangedCallback(Fn<void()> changedCallback) {\n\t_changedCallback = std::move(changedCallback);\n}\n\nQString SentCodeField::getDigitsOnly() const {\n\treturn QString(\n\t\tgetLastText()\n\t).remove(TextUtilities::RegExpDigitsExclude());\n}\n\nvoid SentCodeField::fix() {\n\tif (_fixing) return;\n\n\t_fixing = true;\n\tauto newText = QString();\n\tconst auto now = getLastText();\n\tauto oldPos = textCursor().position();\n\tauto newPos = -1;\n\tauto oldLen = now.size();\n\tauto digitCount = 0;\n\tfor (const auto &ch : now) {\n\t\tif (ch.isDigit()) {\n\t\t\t++digitCount;\n\t\t}\n\t}\n\n\tif (_autoSubmitLength > 0 && digitCount > _autoSubmitLength) {\n\t\tdigitCount = _autoSubmitLength;\n\t}\n\tconst auto strict = (_autoSubmitLength > 0)\n\t\t&& (digitCount == _autoSubmitLength);\n\n\tnewText.reserve(oldLen);\n\tint i = 0;\n\tfor (const auto &ch : now) {\n\t\tif (i++ == oldPos) {\n\t\t\tnewPos = newText.length();\n\t\t}\n\t\tif (ch.isDigit()) {\n\t\t\tif (!digitCount--) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tnewText += ch;\n\t\t\tif (strict && !digitCount) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t} else if (ch == '-') {\n\t\t\tnewText += ch;\n\t\t}\n\t}\n\tif (newPos < 0) {\n\t\tnewPos = newText.length();\n\t}\n\tif (newText != now) {\n\t\tsetText(newText);\n\t\tsetCursorPosition(newPos);\n\t}\n\t_fixing = false;\n\n\tif (_changedCallback) {\n\t\t_changedCallback();\n\t}\n\tif (strict && _submitCallback) {\n\t\t_submitCallback();\n\t}\n}\n\nSentCodeCall::SentCodeCall(\n\tFnMut<void()> callCallback,\n\tFn<void()> updateCallback)\n: _call(std::move(callCallback))\n, _update(std::move(updateCallback)) {\n\t_timer.setCallback([=] {\n\t\tif (_status.state == State::Waiting) {\n\t\t\tif (--_status.timeout <= 0) {\n\t\t\t\t_status.state = State::Calling;\n\t\t\t\t_timer.cancel();\n\t\t\t\tif (_call) {\n\t\t\t\t\t_call();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (_update) {\n\t\t\t_update();\n\t\t}\n\t});\n}\n\nvoid SentCodeCall::setStatus(const Status &status) {\n\t_status = status;\n\tif (_status.state == State::Waiting) {\n\t\t_timer.callEach(1000);\n\t}\n}\n\nQString SentCodeCall::getText() const {\n\tswitch (_status.state) {\n\tcase State::Waiting: {\n\t\tif (_status.timeout >= 3600) {\n\t\t\treturn tr::lng_code_call(\n\t\t\t\ttr::now,\n\t\t\t\tlt_minutes,\n\t\t\t\t(u\"%1:%2\"_q)\n\t\t\t\t\t.arg(_status.timeout / 3600)\n\t\t\t\t\t.arg((_status.timeout / 60) % 60, 2, 10, QChar('0')),\n\t\t\t\tlt_seconds,\n\t\t\t\t(u\"%1\"_q).arg(_status.timeout % 60, 2, 10, QChar('0')));\n\t\t}\n\t\treturn tr::lng_code_call(\n\t\t\ttr::now,\n\t\t\tlt_minutes,\n\t\t\tQString::number(_status.timeout / 60),\n\t\t\tlt_seconds,\n\t\t\t(u\"%1\"_q).arg(_status.timeout % 60, 2, 10, QChar('0')));\n\t} break;\n\tcase State::Calling: return tr::lng_code_calling(tr::now);\n\tcase State::Called: return tr::lng_code_called(tr::now);\n\t}\n\treturn QString();\n}\n\nvoid SentCodeCall::callDone() {\n\tif (_status.state == State::Calling) {\n\t\t_status.state = State::Called;\n\t\tif (_update) {\n\t\t\t_update();\n\t\t}\n\t}\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/sent_code_field.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"ui/widgets/fields/input_field.h\"\n\nnamespace Ui {\n\nclass SentCodeField final : public Ui::InputField {\npublic:\n\tSentCodeField(\n\t\tQWidget *parent,\n\t\tconst style::InputField &st,\n\t\trpl::producer<QString> placeholder = nullptr,\n\t\tconst QString &val = QString());\n\n\tvoid setAutoSubmit(int length, Fn<void()> submitCallback);\n\tvoid setChangedCallback(Fn<void()> changedCallback);\n\t[[nodiscard]] QString getDigitsOnly() const;\n\nprivate:\n\tvoid fix();\n\n\t// Flag for not calling onTextChanged() recursively.\n\tbool _fixing = false;\n\n\tint _autoSubmitLength = 0;\n\tFn<void()> _submitCallback;\n\tFn<void()> _changedCallback;\n\n};\n\nclass SentCodeCall final {\npublic:\n\tSentCodeCall(\n\t\tFnMut<void()> callCallback,\n\t\tFn<void()> updateCallback);\n\n\tenum class State {\n\t\tWaiting,\n\t\tCalling,\n\t\tCalled,\n\t\tDisabled,\n\t};\n\tstruct Status {\n\t\tStatus() {\n\t\t}\n\t\tStatus(State state, int timeout) : state(state), timeout(timeout) {\n\t\t}\n\n\t\tState state = State::Disabled;\n\t\tint timeout = 0;\n\t};\n\tvoid setStatus(const Status &status);\n\n\tvoid callDone();\n\n\t[[nodiscard]] QString getText() const;\n\nprivate:\n\tStatus _status;\n\tbase::Timer _timer;\n\tFnMut<void()> _call;\n\tFn<void()> _update;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/slider_natural_width.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/discrete_sliders.h\"\n\nnamespace Ui {\n\nclass CustomWidthSlider final : public SettingsSlider {\npublic:\n\tusing Ui::SettingsSlider::SettingsSlider;\n\tusing SettingsSlider::setNaturalWidth;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"ui/widgets/vertical_drum_picker.h\"\n\n#include \"ui/effects/animation_value_f.h\"\n#include \"ui/ui_utility.h\"\n#include \"styles/style_basic.h\"\n#include \"styles/style_widgets.h\"\n\nnamespace Ui {\nnamespace {\n\nconstexpr auto kAlmostIndex = float64(.99);\nconstexpr auto kMinYScale = 0.2;\n\nusing PaintItemCallback = VerticalDrumPicker::PaintItemCallback;\n\n} // namespace\n\nPaintItemCallback VerticalDrumPicker::DefaultPaintCallback(\n\t\tconst style::font &font,\n\t\tint itemHeight,\n\t\tFn<void(QPainter&, QRectF, int)> paintContent) {\n\treturn [=](\n\t\t\tQPainter &p,\n\t\t\tint index,\n\t\t\tfloat64 y,\n\t\t\tfloat64 distanceFromCenter,\n\t\t\tint outerWidth) {\n\t\tconst auto r = QRectF(0, y, outerWidth, itemHeight);\n\t\tconst auto progress = std::abs(distanceFromCenter);\n\t\tconst auto revProgress = 1. - progress;\n\t\tp.save();\n\t\tp.translate(r.center());\n\t\tconst auto yScale = kMinYScale\n\t\t\t+ (1. - kMinYScale) * anim::easeOutCubic(1., revProgress);\n\t\tp.scale(1., yScale);\n\t\tp.translate(-r.center());\n\t\tp.setOpacity(revProgress);\n\t\tp.setFont(font);\n\t\tp.setPen(st::defaultFlatLabel.textFg);\n\t\tpaintContent(p, r, index);\n\t\tp.restore();\n\t};\n}\n\nPickerAnimation::PickerAnimation() = default;\n\nvoid PickerAnimation::jumpToOffset(int offset) {\n\t_result.from = _result.current;\n\t_result.to += offset;\n\t_animation.stop();\n\tauto callback = [=](float64 value) {\n\t\tconst auto was = _result.current;\n\t\t_result.current = anim::interpolateF(\n\t\t\t_result.from,\n\t\t\t_result.to,\n\t\t\tvalue);\n\t\t_updates.fire(_result.current - was);\n\t};\n\tif (anim::Disabled()) {\n\t\tauto value = float64(0.);\n\t\tconst auto diff = _result.to - _result.from;\n\t\tconst auto step = std::min(\n\t\t\tkAlmostIndex,\n\t\t\t1. / (std::max(1. - kAlmostIndex, std::abs(diff) + 1)));\n\t\twhile (true) {\n\t\t\tvalue += step;\n\t\t\tif (value >= 1.) {\n\t\t\t\tcallback(1.);\n\t\t\t\tbreak;\n\t\t\t} else {\n\t\t\t\tcallback(value);\n\t\t\t}\n\t\t}\n\t\treturn;\n\t}\n\t_animation.start(\n\t\tstd::move(callback),\n\t\t0.,\n\t\t1.,\n\t\tst::fadeWrapDuration);\n}\n\nvoid PickerAnimation::setResult(float64 from, float64 current, float64 to) {\n\t_result = { from, current, to };\n}\n\nrpl::producer<PickerAnimation::Shift> PickerAnimation::updates() const {\n\treturn _updates.events();\n}\n\nVerticalDrumPicker::VerticalDrumPicker(\n\tnot_null<Ui::RpWidget*> parent,\n\tPaintItemCallback &&paintCallback,\n\tint itemsCount,\n\tint itemHeight,\n\tint startIndex,\n\tbool looped)\n: RpWidget(parent)\n, _itemsCount(itemsCount)\n, _itemHeight(itemHeight)\n, _paintCallback(std::move(paintCallback))\n, _pendingStartIndex(startIndex)\n, _loopData({ .looped = looped }) {\n\tExpects(_paintCallback != nullptr);\n\n\tsizeValue(\n\t) | rpl::on_next([=](const QSize &s) {\n\t\t_itemsVisible.count = std::ceil(float64(s.height()) / _itemHeight);\n\t\t_itemsVisible.centerOffset = _itemsVisible.count / 2;\n\t\tif ((_pendingStartIndex >= 0) && _itemsVisible.count) {\n\t\t\t_index = normalizedIndex(_pendingStartIndex\n\t\t\t\t- _itemsVisible.centerOffset);\n\t\t\t_pendingStartIndex = -1;\n\t\t}\n\n\t\tif (!_loopData.looped) {\n\t\t\t_loopData.minIndex = -_itemsVisible.centerOffset;\n\t\t\t_loopData.maxIndex = _itemsCount - 1 - _itemsVisible.centerOffset;\n\t\t}\n\n\t\t_changes.fire({});\n\t}, lifetime());\n\n\tpaintRequest(\n\t) | rpl::on_next([=] {\n\t\tauto p = QPainter(this);\n\n\t\tconst auto outerWidth = width();\n\t\tconst auto centerY = height() / 2.;\n\t\tconst auto shiftedY = _itemHeight * _shift;\n\t\tfor (auto i = -1; i < (_itemsVisible.count + 1); i++) {\n\t\t\tconst auto index = normalizedIndex(i + _index);\n\t\t\tif (!isIndexInRange(index)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto y = (_itemHeight * i + shiftedY);\n\t\t\t_paintCallback(\n\t\t\t\tp,\n\t\t\t\tindex,\n\t\t\t\ty,\n\t\t\t\t((y + _itemHeight / 2.) - centerY) / centerY,\n\t\t\t\touterWidth);\n\t\t}\n\t}, lifetime());\n\n\t_animation.updates(\n\t) | rpl::on_next([=](PickerAnimation::Shift shift) {\n\t\tincreaseShift(shift);\n\t}, lifetime());\n}\n\nvoid VerticalDrumPicker::increaseShift(float64 by) {\n\t{\n\t\t// Guard input.\n\t\tif (by >= 1.) {\n\t\t\tby = kAlmostIndex;\n\t\t} else if (by <= -1.) {\n\t\t\tby = -kAlmostIndex;\n\t\t}\n\t}\n\n\tauto shift = _shift;\n\tauto index = _index;\n\tshift += by;\n\tif (shift >= 1.) {\n\t\tshift -= 1.;\n\t\tindex--;\n\t\tindex = normalizedIndex(index);\n\t} else if (shift <= -1.) {\n\t\tshift += 1.;\n\t\tindex++;\n\t\tindex = normalizedIndex(index);\n\t}\n\tif (_loopData.minIndex == _loopData.maxIndex) {\n\t\t_shift = 0.;\n\t} else if (!_loopData.looped && (index <= _loopData.minIndex)) {\n\t\t_shift = std::min(0., shift);\n\t\t_index = _loopData.minIndex;\n\t} else if (!_loopData.looped && (index >= _loopData.maxIndex)) {\n\t\t_shift = std::max(0., shift);\n\t\t_index = _loopData.maxIndex;\n\t} else {\n\t\t_shift = shift;\n\t\t_index = index;\n\t}\n\t_changes.fire({});\n\tupdate();\n}\n\nvoid VerticalDrumPicker::handleWheelEvent(not_null<QWheelEvent*> e) {\n\tconst auto direction = Ui::WheelDirection(e);\n\tif (direction) {\n\t\t_animation.jumpToOffset(direction);\n\t} else {\n\t\tif (const auto delta = e->pixelDelta().y(); delta) {\n\t\t\tincreaseShift(delta / float64(_itemHeight));\n\t\t} else if (e->phase() == Qt::ScrollEnd) {\n\t\t\tanimationDataFromIndex();\n\t\t\t_animation.jumpToOffset(0);\n\t\t} else {\n\t\t\tconstexpr auto step = int(QWheelEvent::DefaultDeltasPerStep);\n\n\t\t\t_touch.verticalDelta += e->angleDelta().y();\n\t\t\twhile (std::abs(_touch.verticalDelta) >= step) {\n\t\t\t\tif (_touch.verticalDelta < 0) {\n\t\t\t\t\t_touch.verticalDelta += step;\n\t\t\t\t\t_animation.jumpToOffset(1);\n\t\t\t\t} else {\n\t\t\t\t\t_touch.verticalDelta -= step;\n\t\t\t\t\t_animation.jumpToOffset(-1);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid VerticalDrumPicker::handleKeyEvent(not_null<QKeyEvent*> e) {\n\tif (e->key() == Qt::Key_Left || e->key() == Qt::Key_Up) {\n\t\t_animation.jumpToOffset(1);\n\t} else if (e->key() == Qt::Key_PageUp && !e->isAutoRepeat()) {\n\t\t_animation.jumpToOffset(_itemsVisible.count);\n\t} else if (e->key() == Qt::Key_Right || e->key() == Qt::Key_Down) {\n\t\t_animation.jumpToOffset(-1);\n\t} else if (e->key() == Qt::Key_PageDown && !e->isAutoRepeat()) {\n\t\t_animation.jumpToOffset(-_itemsVisible.count);\n\t}\n}\n\nvoid VerticalDrumPicker::handleMouseEvent(not_null<QMouseEvent*> e) {\n\tif (e->type() == QEvent::MouseButtonPress) {\n\t\t_mouse.pressed = true;\n\t\t_mouse.lastPositionY = e->pos().y();\n\t} else if (e->type() == QEvent::MouseMove) {\n\t\tif (_mouse.pressed) {\n\t\t\tconst auto was = _mouse.lastPositionY;\n\t\t\t_mouse.lastPositionY = e->pos().y();\n\t\t\tconst auto diff = _mouse.lastPositionY - was;\n\t\t\tincreaseShift(float64(diff) / _itemHeight);\n\t\t\t_mouse.clickDisabled = true;\n\t\t}\n\t} else if (e->type() == QEvent::MouseButtonRelease) {\n\t\tif (_mouse.clickDisabled) {\n\t\t\tanimationDataFromIndex();\n\t\t\t_animation.jumpToOffset(0);\n\t\t} else {\n\t\t\t_mouse.lastPositionY = e->pos().y();\n\t\t\tconst auto toOffset = _itemsVisible.centerOffset\n\t\t\t\t- (_mouse.lastPositionY / _itemHeight);\n\t\t\t_animation.jumpToOffset(toOffset);\n\t\t}\n\t\t_mouse = {};\n\t}\n}\n\n\nvoid VerticalDrumPicker::wheelEvent(QWheelEvent *e) {\n\thandleWheelEvent(e);\n}\n\nvoid VerticalDrumPicker::mousePressEvent(QMouseEvent *e) {\n\thandleMouseEvent(e);\n}\n\nvoid VerticalDrumPicker::mouseMoveEvent(QMouseEvent *e) {\n\thandleMouseEvent(e);\n}\n\nvoid VerticalDrumPicker::mouseReleaseEvent(QMouseEvent *e) {\n\thandleMouseEvent(e);\n}\n\nvoid VerticalDrumPicker::keyPressEvent(QKeyEvent *e) {\n\thandleKeyEvent(e);\n}\n\nvoid VerticalDrumPicker::animationDataFromIndex() {\n\t_animation.setResult(\n\t\t_index,\n\t\t_index + _shift,\n\t\tstd::round(_index + _shift));\n}\n\nbool VerticalDrumPicker::isIndexInRange(int index) const {\n\treturn (index >= 0) && (index < _itemsCount);\n}\n\nint VerticalDrumPicker::normalizedIndex(int index) const {\n\tif (!_loopData.looped) {\n\t\treturn index;\n\t}\n\tif (index < 0) {\n\t\tindex += _itemsCount;\n\t} else if (index >= _itemsCount) {\n\t\tindex -= _itemsCount;\n\t}\n\treturn index;\n}\n\nint VerticalDrumPicker::index() const {\n\treturn normalizedIndex(_index + _itemsVisible.centerOffset);\n}\n\nrpl::producer<int> VerticalDrumPicker::changes() const {\n\treturn _changes.events() | rpl::map([=] { return index(); });\n}\n\nrpl::producer<int> VerticalDrumPicker::value() const {\n\treturn rpl::single(index())\n\t\t| rpl::then(changes())\n\t\t| rpl::distinct_until_changed();\n}\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\n#include \"ui/effects/animations.h\"\n\nnamespace Ui {\n\nclass PickerAnimation final {\npublic:\n\tusing Shift = float64;\n\tPickerAnimation();\n\n\tvoid jumpToOffset(int offset);\n\tvoid setResult(float64 from, float64 current, float64 to);\n\n\t[[nodiscard]] rpl::producer<Shift> updates() const;\n\nprivate:\n\tUi::Animations::Simple _animation;\n\tstruct {\n\t\tfloat64 from = 0.;\n\t\tfloat64 current = 0.;\n\t\tfloat64 to = 0.;\n\t} _result;\n\n\trpl::event_stream<Shift> _updates;\n};\n\nclass VerticalDrumPicker final : public Ui::RpWidget {\npublic:\n\tusing PaintItemCallback = Fn<void(\n\t\tQPainter &p,\n\t\tint index,\n\t\tfloat y,\n\t\tfloat64 distanceFromCenter,\n\t\tint outerWidth)>;\n\n\tVerticalDrumPicker(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tPaintItemCallback &&paintCallback,\n\t\tint itemsCount,\n\t\tint itemHeight,\n\t\tint startIndex = 0,\n\t\tbool looped = false);\n\n\t[[nodiscard]] int index() const;\n\t[[nodiscard]] rpl::producer<int> changes() const;\n\t[[nodiscard]] rpl::producer<int> value() const;\n\n\tvoid handleWheelEvent(not_null<QWheelEvent*> e);\n\tvoid handleMouseEvent(not_null<QMouseEvent*> e);\n\tvoid handleKeyEvent(not_null<QKeyEvent*> e);\n\n\tstatic PaintItemCallback DefaultPaintCallback(\n\t\tconst style::font &font,\n\t\tint itemHeight,\n\t\tFn<void(QPainter&, QRectF, int)> paintContent);\n\nprotected:\n\tvoid wheelEvent(QWheelEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tvoid increaseShift(float64 by);\n\tvoid animationDataFromIndex();\n\t[[nodiscard]] int normalizedIndex(int index) const;\n\t[[nodiscard]] bool isIndexInRange(int index) const;\n\n\tconst int _itemsCount;\n\tconst int _itemHeight;\n\n\tPaintItemCallback _paintCallback;\n\n\tint _pendingStartIndex = -1;\n\n\tstruct {\n\t\tint count = 0;\n\t\tint centerOffset = 0;\n\t} _itemsVisible;\n\n\tint _index = 0;\n\tfloat64 _shift = 0.;\n\trpl::event_stream<> _changes;\n\n\tstruct {\n\t\tconst bool looped;\n\t\tint minIndex = 0;\n\t\tint maxIndex = 0;\n\t} _loopData;\n\n\tPickerAnimation _animation;\n\n\tstruct {\n\t\tbool pressed = false;\n\t\tint lastPositionY;\n\t\tbool clickDisabled = false;\n\t} _mouse;\n\n\tstruct {\n\t\tint verticalDelta = 0;\n\t} _touch;\n\n};\n\n} // namespace Ui\n"
  },
  {
    "path": "Telegram/SourceFiles/window/main_window.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/main_window.h\"\n\n#include \"api/api_updates.h\"\n#include \"storage/localstorage.h\"\n#include \"platform/platform_specific.h\"\n#include \"ui/platform/ui_platform_window.h\"\n#include \"platform/platform_window_title.h\"\n#include \"history/history.h\"\n#include \"info/media/info_media_widget.h\" // SharedMediaTitle.\n#include \"window/window_separate_id.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_lock_widgets.h\"\n#include \"window/window_controller.h\"\n#include \"main/main_account.h\" // Account::sessionValue.\n#include \"main/main_domain.h\"\n#include \"core/application.h\"\n#include \"core/sandbox.h\"\n#include \"core/shortcuts.h\"\n#include \"lang/lang_keys.h\"\n#include \"data/data_session.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_user.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"base/options.h\"\n#include \"base/crc32hash.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/controls/window_outdated_bar.h\"\n#include \"ui/controls/window_screen_reader_bar.h\"\n#include \"ui/painter.h\"\n#include \"ui/screen_reader_mode.h\"\n#include \"ui/ui_utility.h\"\n#include \"apiwrap.h\"\n#include \"mainwidget.h\" // session->content()->windowShown().\n#include \"tray.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_dialogs.h\" // ChildSkip().x() for new child windows.\n\n#include <QtCore/QMimeData>\n#include <QtGui/QWindow>\n#include <QtGui/QScreen>\n#include <QtGui/QDrag>\n\n#include <kurlmimedata.h>\n\nnamespace Window {\nnamespace {\n\nconstexpr auto kSaveWindowPositionTimeout = crl::time(1000);\n\nusing Core::WindowPosition;\n\n[[nodiscard]] QPoint ChildSkip() {\n\tconst auto skipx = st::defaultDialogRow.padding.left()\n\t\t+ st::defaultDialogRow.photoSize\n\t\t+ st::defaultDialogRow.padding.left();\n\tconst auto skipy = st::windowTitleHeight;\n\treturn { skipx, skipy };\n}\n\nconst QImage &LogoSquareNoMargin() {\n\tstatic const auto result = QImage(u\":/gui/art/logo_256_square.png\"_q);\n\treturn result;\n}\n\nconst QImage &LogoForkgramNoMargin() {\n\tstatic const auto result =\n\t\tQImage(u\":/gui/art/forkgram/logo_256_no_margin.png\"_q);\n\treturn result;\n}\n\nconst QImage &GetForkIconOr(const QImage &l) {\n\tif (!Core::IsAppLaunched()) {\n\t\treturn l;\n\t}\n\tconst auto isSquare = Core::App().settings().fork().squareUserpics();\n\tconst auto isOriginal =\n\t\tCore::App().settings().fork().useOriginalTrayIcon();\n\treturn isOriginal\n\t\t? l\n\t\t: isSquare\n\t\t\t? LogoSquareNoMargin()\n\t\t\t: LogoForkgramNoMargin();\n}\n\n[[nodiscard]] QImage &OverridenIcon() {\n\tstatic auto result = QImage();\n\treturn result;\n}\n\nbase::options::toggle OptionNewWindowsSizeAsFirst({\n\t.id = kOptionNewWindowsSizeAsFirst,\n\t.name = \"Adjust size of new chat windows\",\n\t.description = \"Open new windows with a size of the main window.\",\n});\n\nbase::options::toggle OptionDisableTouchbar({\n\t.id = kOptionDisableTouchbar,\n\t.name = \"Disable Touch Bar (macOS only).\",\n\t.scope = [] {\n#ifdef Q_OS_MAC\n\t\treturn true;\n#else // !Q_OS_MAC\n\t\treturn false;\n#endif // !Q_OS_MAC\n\t},\n\t.restartRequired = true,\n});\n\n[[nodiscard]] QString TitleFromSeparateSharedMedia(\n\t\tconst Core::WindowTitleContent &settings,\n\t\tconst SeparateId &id) {\n\tif (id.type != SeparateType::SharedMedia) {\n\t\treturn QString();\n\t}\n\tconst auto type = id.sharedMediaType;\n\tconst auto result = Info::Media::SharedMediaTitle(type)(tr::now);\n\tif (settings.hideChatName) {\n\t\treturn result;\n\t}\n\tconst auto thread = id.thread;\n\tconst auto topic = thread->asTopic();\n\tconst auto name = topic\n\t\t? topic->title()\n\t\t: thread->peer()->isSelf()\n\t\t? tr::lng_saved_messages(tr::now)\n\t\t: thread->peer()->name();\n\tconst auto wrapped = st::wrap_rtl(name);\n\treturn name + u\" @ \"_q + result;\n}\n\n} // namespace\n\nconst char kOptionNewWindowsSizeAsFirst[] = \"new-windows-size-as-first\";\nconst char kOptionDisableTouchbar[] = \"touchbar-disabled\";\n\nconst QImage &Logo() {\n\tstatic const auto result = QImage(u\":/gui/art/logo_256.png\"_q);\n\treturn GetForkIconOr(result);\n}\n\nconst QImage &LogoNoMargin() {\n\tstatic const auto result = QImage(u\":/gui/art/logo_256_no_margin.png\"_q);\n\treturn GetForkIconOr(result);\n}\n\nvoid ConvertIconToBlack(QImage &image) {\n\tif (image.format() != QImage::Format_ARGB32_Premultiplied) {\n\t\timage = std::move(image).convertToFormat(\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t}\n\t//const auto gray = red * 0.299 + green * 0.587 + blue * 0.114;\n\t//const auto result = (gray - 100 < 0) ? 0 : (gray - 100) * 255 / 155;\n\tconstexpr auto scale = 255 / 155.;\n\tconstexpr auto red = 0.299;\n\tconstexpr auto green = 0.587;\n\tconstexpr auto blue = 0.114;\n\tstatic constexpr auto shift = (1 << 24);\n\tauto shifter = [](double value) {\n\t\treturn uint32(value * shift);\n\t};\n\tconstexpr auto iscale = shifter(scale);\n\tconstexpr auto ired = shifter(red);\n\tconstexpr auto igreen = shifter(green);\n\tconstexpr auto iblue = shifter(blue);\n\tconstexpr auto threshold = 100;\n\n\tconst auto width = image.width();\n\tconst auto height = image.height();\n\tconst auto data = reinterpret_cast<uint32*>(image.bits());\n\tconst auto intsPerLine = image.bytesPerLine() / 4;\n\tconst auto intsPerLineAdded = intsPerLine - width;\n\n\tauto pixel = data;\n\tfor (auto j = 0; j != height; ++j) {\n\t\tfor (auto i = 0; i != width; ++i) {\n\t\t\tconst auto value = *pixel;\n\t\t\tconst auto gray = (((value >> 16) & 0xFF) * ired\n\t\t\t\t+ ((value >> 8) & 0xFF) * igreen\n\t\t\t\t+ (value & 0xFF) * iblue) >> 24;\n\t\t\tconst auto small = gray - threshold;\n\t\t\tconst auto test = ~small;\n\t\t\tconst auto result = (test >> 31) * small * iscale;\n\t\t\tconst auto component = (result >> 24) & 0xFF;\n\t\t\t*pixel++ = (value & 0xFF000000U)\n\t\t\t\t| (component << 16)\n\t\t\t\t| (component << 8)\n\t\t\t\t| component;\n\t\t}\n\t\tpixel += intsPerLineAdded;\n\t}\n}\n\nvoid OverrideApplicationIcon(QImage image) {\n\tOverridenIcon() = std::move(image);\n}\n\nQIcon CreateOfficialIcon(Main::Session *session) {\n\tconst auto support = (session && session->supportMode());\n\tconst auto useBlack = Core::IsAppLaunched()\n\t\t&& Core::App().settings().fork().useBlackTrayIcon();\n\tif (!support && !useBlack) {\n\t\treturn QIcon();\n\t}\n\tauto overriden = OverridenIcon();\n\tauto image = overriden.isNull()\n\t\t? Platform::DefaultApplicationIcon()\n\t\t: overriden;\n\tConvertIconToBlack(image);\n\treturn QIcon(Ui::PixmapFromImage(std::move(image)));\n}\n\nQIcon CreateIcon(Main::Session *session, bool returnNullIfDefault) {\n\tconst auto officialIcon = CreateOfficialIcon(session);\n\tif (!officialIcon.isNull() || returnNullIfDefault) {\n\t\treturn officialIcon;\n\t}\n\n\tauto result = QIcon(Ui::PixmapFromImage(base::duplicate(Logo())));\n\n\tif constexpr (!Platform::IsLinux()) {\n\t\treturn result;\n\t}\n\n\tconst auto iconFromTheme = QIcon::fromTheme(\n\t\tPlatform::ApplicationIconName(),\n\t\tresult);\n\n\tresult = QIcon();\n\n\tstatic const auto iconSizes = {\n\t\t16,\n\t\t22,\n\t\t32,\n\t\t48,\n\t\t64,\n\t\t128,\n\t\t256,\n\t};\n\n\t// Qt's standard QIconLoaderEngine sets availableSizes\n\t// to XDG directories sizes, since svg icons are scalable,\n\t// they could be only in one XDG folder (like 48x48)\n\t// and Qt will set only a 48px icon to the window\n\t// even though the icon could be scaled to other sizes.\n\t// Thus, scale it manually to the most widespread sizes.\n\tfor (const auto iconSize : iconSizes) {\n\t\t// We can't use QIcon::actualSize here\n\t\t// since it works incorrectly with svg icon themes\n\t\tconst auto iconPixmap = iconFromTheme.pixmap(iconSize);\n\n\t\tconst auto iconPixmapSize = iconPixmap.size()\n\t\t\t/ iconPixmap.devicePixelRatio();\n\n\t\t// Not a svg icon, don't scale it\n\t\tif (iconPixmapSize.width() != iconSize) {\n\t\t\treturn iconFromTheme;\n\t\t}\n\n\t\tresult.addPixmap(iconPixmap);\n\t}\n\n\treturn result;\n}\n\nQImage GenerateCounterLayer(CounterLayerArgs &&args) {\n\tconst auto count = args.count.value();\n\tconst auto text = (count < 1000)\n\t\t? QString::number(count)\n\t\t: u\"..%1\"_q.arg(count % 100, 2, 10, QChar('0'));\n\tconst auto textSize = text.size();\n\n\tstruct Dimensions {\n\t\tint size = 0;\n\t\tint font = 0;\n\t\tint delta = 0;\n\t\tint radius = 0;\n\t};\n\tconst auto d = [&]() -> Dimensions {\n\t\tswitch (args.size.value()) {\n\t\tcase 16:\n\t\t\treturn {\n\t\t\t\t.size = 16,\n\t\t\t\t.font = ((textSize < 2) ? 11 : (textSize < 3) ? 11 : 8),\n\t\t\t\t.delta = ((textSize < 2) ? 5 : (textSize < 3) ? 2 : 1),\n\t\t\t\t.radius = ((textSize < 2) ? 8 : (textSize < 3) ? 7 : 3),\n\t\t\t};\n\t\tcase 20:\n\t\t\treturn {\n\t\t\t\t.size = 20,\n\t\t\t\t.font = ((textSize < 2) ? 14 : (textSize < 3) ? 13 : 10),\n\t\t\t\t.delta = ((textSize < 2) ? 6 : (textSize < 3) ? 2 : 1),\n\t\t\t\t.radius = ((textSize < 2) ? 10 : (textSize < 3) ? 9 : 5),\n\t\t\t};\n\t\tcase 24:\n\t\t\treturn {\n\t\t\t\t.size = 24,\n\t\t\t\t.font = ((textSize < 2) ? 17 : (textSize < 3) ? 16 : 12),\n\t\t\t\t.delta = ((textSize < 2) ? 7 : (textSize < 3) ? 3 : 1),\n\t\t\t\t.radius = ((textSize < 2) ? 12 : (textSize < 3) ? 11 : 6),\n\t\t\t};\n\t\tdefault:\n\t\t\treturn {\n\t\t\t\t.size = 32,\n\t\t\t\t.font = ((textSize < 2) ? 22 : (textSize < 3) ? 20 : 16),\n\t\t\t\t.delta = ((textSize < 2) ? 9 : (textSize < 3) ? 4 : 2),\n\t\t\t\t.radius = ((textSize < 2) ? 16 : (textSize < 3) ? 14 : 8),\n\t\t\t};\n\t\t}\n\t}();\n\n\tauto result = QImage(\n\t\tQSize(d.size, d.size) * args.devicePixelRatio,\n\t\tQImage::Format_ARGB32);\n\tresult.setDevicePixelRatio(args.devicePixelRatio);\n\tresult.fill(Qt::transparent);\n\n\tauto p = QPainter(&result);\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto f = style::font{ d.font, 0, 0 };\n\tconst auto w = f->width(text);\n\n\tp.setBrush(args.bg.value());\n\tp.setPen(Qt::NoPen);\n\tp.drawRoundedRect(\n\t\tQRect(\n\t\t\td.size - w - d.delta * 2,\n\t\t\td.size - f->height,\n\t\t\tw + d.delta * 2,\n\t\t\tf->height),\n\t\td.radius,\n\t\td.radius);\n\n\tp.setFont(f);\n\tp.setPen(args.fg.value());\n\tp.drawText(d.size - w - d.delta, d.size - f->height + f->ascent, text);\n\tp.end();\n\n\treturn result;\n}\n\nQImage WithSmallCounter(QImage image, CounterLayerArgs &&args) {\n\t// platform/linux/tray_linux depends on count used the same\n\t// way for all the same (count % 100) values.\n\tconst auto count = args.count.value();\n\tconst auto text = (count < 100)\n\t\t? QString::number(count)\n\t\t: QString(\"..%1\").arg(count % 10, 1, 10, QChar('0'));\n\tconst auto textSize = text.size();\n\n\tstruct Dimensions {\n\t\tint size = 0;\n\t\tint font = 0;\n\t\tint delta = 0;\n\t\tint radius = 0;\n\t};\n\tconst auto d = Dimensions{\n\t\t.size = args.size.value(),\n\t\t.font = args.size.value() / 2,\n\t\t.delta = args.size.value() / ((textSize < 2) ? 8 : 16),\n\t\t.radius = args.size.value() / ((textSize < 2) ? 4 : 5),\n\t};\n\n\tauto p = QPainter(&image);\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto f = style::font{ d.font, 0, 0 };\n\tconst auto w = f->width(text);\n\n\tp.setBrush(args.bg.value());\n\tp.setPen(Qt::NoPen);\n\tp.drawRoundedRect(\n\t\tQRect(\n\t\t\td.size - w - d.delta * 2,\n\t\t\td.size - f->height,\n\t\t\tw + d.delta * 2,\n\t\t\tf->height),\n\t\td.radius,\n\t\td.radius);\n\n\tp.setFont(f);\n\tp.setPen(args.fg.value());\n\tp.drawText(d.size - w - d.delta, d.size - f->height + f->ascent, text);\n\tp.end();\n\n\treturn image;\n}\n\nMainWindow::MainWindow(not_null<Controller*> controller)\n: _controller(controller)\n, _positionUpdatedTimer([=] { savePosition(); })\n, _outdated(Ui::CreateOutdatedBar(body(), cWorkingDir()))\n, _screenReaderBar(Ui::CreateScreenReaderBar(body(), [=] {\n\tcontroller->show(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_screen_reader_confirm_text(tr::now),\n\t\t.confirmed = [=](Fn<void()> close) {\n\t\t\tCore::App().settings().writePref<bool>(\n\t\t\t\tCore::kScreenReaderModeDisabledKey,\n\t\t\t\ttrue);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\tUi::SetScreenReaderModeDisabled(true);\n\t\t\tclose();\n\t\t},\n\t\t.confirmText = tr::lng_screen_reader_confirm_disable(),\n\t}));\n}))\n, _body(body()) {\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tupdatePalette();\n\t}, lifetime());\n\n\tCore::App().unreadBadgeChanges(\n\t) | rpl::on_next([=] {\n\t\tupdateTitle();\n\t\tunreadCounterChangedHook();\n\t\tCore::App().tray().updateIconCounters();\n\t}, lifetime());\n\n\tCore::App().settings().workModeChanges(\n\t) | rpl::on_next([=](Core::Settings::WorkMode mode) {\n\t\tworkmodeUpdated(mode);\n\t}, lifetime());\n\n\tif (isPrimary()) {\n\t\tUi::Toast::SetDefaultParent(_body.data());\n\t}\n\n\twindowActiveValue(\n\t) | rpl::skip(1) | rpl::on_next([=](bool active) {\n\t\tInvokeQueued(this, [=] {\n\t\t\thandleActiveChanged(active);\n\t\t});\n\t}, lifetime());\n\n\tshownValue(\n\t) | rpl::skip(1) | rpl::on_next([=](bool visible) {\n\t\tInvokeQueued(this, [=] {\n\t\t\thandleVisibleChanged(visible);\n\t\t});\n\t}, lifetime());\n\n\tbody()->sizeValue(\n\t) | rpl::on_next([=](QSize size) {\n\t\tupdateControlsGeometry();\n\t}, lifetime());\n\n\tif (_outdated) {\n\t\t_outdated->heightValue(\n\t\t) | rpl::on_next([=](int height) {\n\t\t\tif (!height) {\n\t\t\t\tcrl::on_main(this, [=] { _outdated.destroy(); });\n\t\t\t}\n\t\t\tupdateControlsGeometry();\n\t\t}, _outdated->lifetime());\n\t}\n\n\tif (_screenReaderBar) {\n\t\t_screenReaderBar->heightValue(\n\t\t) | rpl::on_next([=](int height) {\n\t\t\tupdateControlsGeometry();\n\t\t}, _screenReaderBar->lifetime());\n\t}\n\n\tShortcuts::Listen(this);\n}\n\nMain::Account &MainWindow::account() const {\n\treturn _controller->account();\n}\n\nWindow::SeparateId MainWindow::id() const {\n\treturn _controller->id();\n}\n\nbool MainWindow::isPrimary() const {\n\treturn _controller->isPrimary();\n}\n\nWindow::SessionController *MainWindow::sessionController() const {\n\treturn _controller->sessionController();\n}\n\nbool MainWindow::hideNoQuit() {\n\tif (Core::Quitting()) {\n\t\treturn false;\n\t}\n\tconst auto workMode = Core::App().settings().workMode();\n\tusing Mode = Core::Settings::WorkMode;\n\tif (workMode == Mode::TrayOnly || workMode == Mode::WindowAndTray) {\n\t\tif (minimizeToTray()) {\n\t\t\tif (const auto controller = sessionController()) {\n\t\t\t\tcontroller->clearSectionStack();\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t}\n\tusing Behavior = Core::Settings::CloseBehavior;\n\tconst auto behavior = Platform::IsMac()\n\t\t? Behavior::RunInBackground\n\t\t: Core::App().settings().closeBehavior();\n\tif (behavior == Behavior::RunInBackground) {\n\t\tcloseWithoutDestroy();\n\t} else if (behavior == Behavior::CloseToTaskbar) {\n\t\tsetWindowState(window()->windowState() | Qt::WindowMinimized);\n\t} else {\n\t\treturn false;\n\t}\n\tcontroller().updateIsActiveBlur();\n\tupdateGlobalMenu();\n\tif (const auto controller = sessionController()) {\n\t\tcontroller->clearSectionStack();\n\t}\n\treturn true;\n}\n\nvoid MainWindow::clearWidgets() {\n\tclearWidgetsHook();\n\tupdateGlobalMenu();\n}\n\nvoid MainWindow::updateIsActive() {\n\tconst auto isActive = computeIsActive();\n\tif (_isActive != isActive) {\n\t\t_isActive = isActive;\n\t}\n}\n\nbool MainWindow::computeIsActive() const {\n\treturn isActiveWindow() && isVisible() && !(windowState() & Qt::WindowMinimized);\n}\n\nQRect MainWindow::desktopRect() const {\n\tconst auto now = crl::now();\n\tif (!_monitorLastGot || now >= _monitorLastGot + crl::time(1000)) {\n\t\t_monitorLastGot = now;\n\t\t_monitorRect = computeDesktopRect();\n\t}\n\treturn _monitorRect;\n}\n\nvoid MainWindow::init() {\n\tinitHook();\n\n\tupdatePalette();\n\n\tif (Ui::Platform::NativeWindowFrameSupported()) {\n\t\tCore::App().settings().nativeWindowFrameChanges(\n\t\t) | rpl::on_next([=](bool native) {\n\t\t\trefreshTitleWidget();\n\t\t\trecountGeometryConstraints();\n\t\t}, lifetime());\n\t}\n\trefreshTitleWidget();\n\n\tupdateTitle();\n\tupdateWindowIcon();\n\n\n\n\t// Forkgram.\n\tconst auto iconUpdated = std::make_shared<bool>(false);\n\tconnect(\n\t\twindowHandle(),\n\t\t&QWindow::activeChanged,\n\t\tthis,\n\t\t[=] {\n\t\t\tif (!isActiveWindow()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t*iconUpdated = false;\n\t\t\tInvokeQueued(this, [=] {\n\t\t\t\tif (!(*iconUpdated)) {\n\t\t\t\t\tupdateWindowIcon();\n\t\t\t\t\t*iconUpdated = true;\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t\tQt::QueuedConnection);\n}\n\nvoid MainWindow::handleStateChanged(Qt::WindowState state) {\n\tstateChangedHook(state);\n\tupdateControlsGeometry();\n\tif (state == Qt::WindowMinimized) {\n\t\tcontroller().updateIsActiveBlur();\n\t} else {\n\t\tcontroller().updateIsActiveFocus();\n\t}\n\tCore::App().updateNonIdle();\n\tusing WorkMode = Core::Settings::WorkMode;\n\tif (state == Qt::WindowMinimized\n\t\t&& (Core::App().settings().workMode() == WorkMode::TrayOnly)) {\n\t\tminimizeToTray();\n\t}\n\tsavePosition(state);\n}\n\nvoid MainWindow::handleActiveChanged(bool active) {\n\tcheckActivation();\n\tif (active) {\n\t\tCore::App().windowActivated(&controller());\n\t}\n\tif (const auto controller = sessionController()) {\n\t\tcontroller->session().updates().updateOnline();\n\t}\n}\n\nvoid MainWindow::handleVisibleChanged(bool visible) {\n\tif (visible) {\n\t\tif (_maximizedBeforeHide) {\n\t\t\tDEBUG_LOG((\"Window Pos: Window was maximized before hidding, setting maximized.\"));\n\t\t\tsetWindowState(Qt::WindowMaximized);\n\t\t}\n\t} else {\n\t\t_maximizedBeforeHide = Core::App().settings().windowPosition().maximized;\n\t}\n\n\thandleVisibleChangedHook(visible);\n}\n\nvoid MainWindow::showFromTray() {\n\tInvokeQueued(this, [=] {\n\t\tupdateGlobalMenu();\n\t});\n\tactivate();\n\tunreadCounterChangedHook();\n\tCore::App().tray().updateIconCounters();\n}\n\nvoid MainWindow::quitFromTray() {\n\tCore::Quit();\n}\n\nvoid MainWindow::activate() {\n\tbool wasHidden = !isVisible();\n\tsetWindowState(windowState() & ~Qt::WindowMinimized);\n\tsetVisible(true);\n\tPlatform::ActivateThisProcess();\n\traise();\n\tactivateWindow();\n\tcontroller().updateIsActiveFocus();\n\tif (wasHidden) {\n\t\tif (const auto session = sessionController()) {\n\t\t\tsession->content()->windowShown();\n\t\t}\n\t}\n}\n\nvoid MainWindow::updatePalette() {\n\tUi::ForceFullRepaint(this);\n\n\tauto p = palette();\n\tp.setColor(QPalette::Window, st::windowBg->c);\n\tsetPalette(p);\n}\n\nint MainWindow::computeMinWidth() const {\n\tauto result = st::windowMinWidth;\n\tif (_rightColumn) {\n\t\tresult += _rightColumn->width();\n\t}\n\treturn result;\n}\n\nint MainWindow::computeMinHeight() const {\n\tconst auto outdated = [&] {\n\t\tif (!_outdated) {\n\t\t\treturn 0;\n\t\t}\n\t\t_outdated->resizeToWidth(st::windowMinWidth);\n\t\treturn _outdated->height();\n\t}();\n\tconst auto screenReader = [&] {\n\t\tif (!_screenReaderBar) {\n\t\t\treturn 0;\n\t\t}\n\t\t_screenReaderBar->resizeToWidth(st::windowMinWidth);\n\t\treturn _screenReaderBar->height();\n\t}();\n\treturn outdated + screenReader + st::windowMinHeight;\n}\n\nvoid MainWindow::refreshTitleWidget() {\n\tif (Ui::Platform::NativeWindowFrameSupported()\n\t\t&& Core::App().settings().nativeWindowFrame()) {\n\t\tsetNativeFrame(true);\n\t\tif (Platform::NativeTitleRequiresShadow()) {\n\t\t\t_titleShadow.create(this);\n\t\t\t_titleShadow->show();\n\t\t}\n\t} else {\n\t\tsetNativeFrame(false);\n\t\t_titleShadow.destroy();\n\t}\n}\n\nvoid MainWindow::updateMinimumSize() {\n\tsetMinimumSize(QSize(computeMinWidth(), computeMinHeight()));\n}\n\nvoid MainWindow::recountGeometryConstraints() {\n\tupdateMinimumSize();\n\tupdateControlsGeometry();\n\tfixOrder();\n}\n\nWindowPosition MainWindow::initialPosition() const {\n\tconst auto active = Core::App().activeWindow();\n\treturn (!active || active == &controller())\n\t\t? Core::AdjustToScale(\n\t\t\tCore::App().settings().windowPosition(),\n\t\t\tu\"Window\"_q)\n\t\t: active->widget()->nextInitialChildPosition(id());\n}\n\nWindowPosition MainWindow::nextInitialChildPosition(SeparateId childId) {\n\tconst auto rect = geometry().marginsRemoved(frameMargins());\n\tconst auto position = rect.topLeft();\n\tconst auto adjust = [&](int value) {\n\t\treturn (value * 3 / 4);\n\t};\n\tconst auto width = OptionNewWindowsSizeAsFirst.value()\n\t\t? Core::App().settings().windowPosition().w\n\t\t: childId.primary()\n\t\t? st::windowDefaultWidth\n\t\t: childId.hasChatsList()\n\t\t? (st::columnMinimalWidthLeft + adjust(st::windowDefaultWidth))\n\t\t: adjust(st::windowDefaultWidth);\n\tconst auto height = OptionNewWindowsSizeAsFirst.value()\n\t\t? Core::App().settings().windowPosition().h\n\t\t: childId.primary()\n\t\t? st::windowDefaultHeight\n\t\t: adjust(st::windowDefaultHeight);\n\tconst auto skip = ChildSkip();\n\tconst auto delta = _lastChildIndex\n\t\t? (_lastMyChildCreatePosition - position)\n\t\t: skip;\n\tif (qAbs(delta.x()) >= skip.x() || qAbs(delta.y()) >= skip.y()) {\n\t\t_lastChildIndex = 1;\n\t} else {\n\t\t++_lastChildIndex;\n\t}\n\n\t_lastMyChildCreatePosition = position;\n\tconst auto use = position + (skip * _lastChildIndex);\n\treturn withScreenInPosition({\n\t\t.scale = cScale(),\n\t\t.x = use.x(),\n\t\t.y = use.y(),\n\t\t.w = width,\n\t\t.h = height,\n\t});\n}\n\nQRect MainWindow::countInitialGeometry(WindowPosition position) {\n\tconst auto primaryScreen = QGuiApplication::primaryScreen();\n\tconst auto primaryAvailable = primaryScreen\n\t\t? primaryScreen->availableGeometry()\n\t\t: QRect(0, 0, st::windowDefaultWidth, st::windowDefaultHeight);\n\tconst auto initialWidth = Core::Settings::ThirdColumnByDefault()\n\t\t? st::windowBigDefaultWidth\n\t\t: st::windowDefaultWidth;\n\tconst auto initialHeight = Core::Settings::ThirdColumnByDefault()\n\t\t? st::windowBigDefaultHeight\n\t\t: st::windowDefaultHeight;\n\tconst auto initial = WindowPosition{\n\t\t.x = (primaryAvailable.x()\n\t\t\t+ std::max((primaryAvailable.width() - initialWidth) / 2, 0)),\n\t\t.y = (primaryAvailable.y()\n\t\t\t+ std::max((primaryAvailable.height() - initialHeight) / 2, 0)),\n\t\t.w = initialWidth,\n\t\t.h = initialHeight,\n\t};\n\treturn CountInitialGeometry(\n\t\tthis,\n\t\tposition,\n\t\tinitial,\n\t\t{ st::windowMinWidth, st::windowMinHeight },\n\t\tu\"Window\"_q);\n}\n\nvoid MainWindow::firstShow() {\n\tupdateMinimumSize();\n\tif (initGeometryFromSystem()) {\n\t\tshow();\n\t\treturn;\n\t}\n\tconst auto geometry = countInitialGeometry(initialPosition());\n\tDEBUG_LOG((\"Window Pos: Setting first %1, %2, %3, %4\"\n\t\t).arg(geometry.x()\n\t\t).arg(geometry.y()\n\t\t).arg(geometry.width()\n\t\t).arg(geometry.height()));\n\tsetGeometry(geometry);\n\tshow();\n}\n\nvoid MainWindow::positionUpdated() {\n\t_positionUpdatedTimer.callOnce(kSaveWindowPositionTimeout);\n}\n\nvoid MainWindow::setPositionInited() {\n\t_positionInited = true;\n}\n\nvoid MainWindow::imeCompositionStartReceived() {\n\t_imeCompositionStartReceived.fire({});\n}\n\nrpl::producer<> MainWindow::leaveEvents() const {\n\treturn _leaveEvents.events();\n}\n\nrpl::producer<> MainWindow::imeCompositionStarts() const {\n\treturn _imeCompositionStartReceived.events();\n}\n\nvoid MainWindow::leaveEventHook(QEvent *e) {\n\t_leaveEvents.fire({});\n}\n\nvoid MainWindow::updateControlsGeometry() {\n\tconst auto inner = body()->rect();\n\tauto bodyLeft = inner.x();\n\tauto bodyTop = inner.y();\n\tauto bodyWidth = inner.width();\n\tif (_titleShadow) {\n\t\t_titleShadow->setGeometry(inner.x(), bodyTop, inner.width(), st::lineWidth);\n\t}\n\tif (_outdated) {\n\t\tUi::SendPendingMoveResizeEvents(_outdated.data());\n\t\t_outdated->resizeToWidth(inner.width());\n\t\t_outdated->moveToLeft(inner.x(), bodyTop);\n\t\tbodyTop += _outdated->height();\n\t}\n\tif (_screenReaderBar) {\n\t\tUi::SendPendingMoveResizeEvents(_screenReaderBar.data());\n\t\t_screenReaderBar->resizeToWidth(inner.width());\n\t\t_screenReaderBar->moveToLeft(inner.x(), bodyTop);\n\t\tbodyTop += _screenReaderBar->height();\n\t}\n\tif (_rightColumn) {\n\t\tbodyWidth -= _rightColumn->width();\n\t\t_rightColumn->setGeometry(bodyWidth, bodyTop, inner.width() - bodyWidth, inner.height() - (bodyTop - inner.y()));\n\t}\n\t_body->setGeometry(bodyLeft, bodyTop, bodyWidth, inner.height() - (bodyTop - inner.y()));\n}\n\nvoid MainWindow::updateTitle() {\n\tif (Core::Quitting()) {\n\t\treturn;\n\t}\n\n\tconst auto settings = Core::App().settings().windowTitleContent();\n\tconst auto locked = Core::App().passcodeLocked();\n\tconst auto counter = settings.hideTotalUnread\n\t\t? 0\n\t\t: Core::App().unreadBadge();\n\tconst auto added = (counter > 0) ? u\" (%1)\"_q.arg(counter) : QString();\n\tconst auto session = locked ? nullptr : _controller->sessionController();\n\tconst auto user = (session\n\t\t&& !settings.hideAccountName\n\t\t&& Core::App().domain().accountsAuthedCount() > 1)\n\t\t? st::wrap_rtl(session->authedName())\n\t\t: QString();\n\tconst auto separateSharedMediaTitle = session\n\t\t? TitleFromSeparateSharedMedia(settings, session->windowId())\n\t\t: QString();\n\tif (!separateSharedMediaTitle.isEmpty()) {\n\t\tsetTitle(separateSharedMediaTitle);\n\t\treturn;\n\t}\n\tconst auto key = (session && !settings.hideChatName)\n\t\t? session->activeChatCurrent()\n\t\t: Dialogs::Key();\n\tconst auto thread = key ? key.thread() : nullptr;\n\tif (!thread) {\n\t\tsetTitle((user.isEmpty() ? u\"Telegram\"_q : user) + added);\n\t\treturn;\n\t}\n\tconst auto history = thread->owningHistory();\n\tconst auto topic = thread->asTopic();\n\tconst auto name = topic\n\t\t? topic->title()\n\t\t: history->peer->isSelf()\n\t\t? tr::lng_saved_messages(tr::now)\n\t\t: history->peer->name();\n\tconst auto wrapped = st::wrap_rtl(name);\n\tconst auto threadCounter = thread->chatListBadgesState().unreadCounter;\n\tconst auto primary = (threadCounter > 0)\n\t\t? u\"(%1) %2\"_q.arg(threadCounter).arg(wrapped)\n\t\t: wrapped;\n\tconst auto middle = !user.isEmpty()\n\t\t? (u\" @ \"_q + user)\n\t\t: !added.isEmpty()\n\t\t? u\" \\u2013\"_q\n\t\t: QString();\n\tsetTitle(primary + middle + added);\n}\n\nQRect MainWindow::computeDesktopRect() const {\n\treturn screen()->availableGeometry();\n}\n\nvoid MainWindow::savePosition(Qt::WindowState state) {\n\tif (state == Qt::WindowActive) {\n\t\tstate = windowHandle()->windowState();\n\t}\n\n\tif (state == Qt::WindowMinimized\n\t\t|| !isVisible()\n\t\t|| !Core::App().savingPositionFor(&controller())\n\t\t|| !positionInited()) {\n\t\treturn;\n\t}\n\n\tconst auto &savedPosition = Core::App().settings().windowPosition();\n\tauto realPosition = savedPosition;\n\n\tif (state == Qt::WindowMaximized) {\n\t\trealPosition.maximized = 1;\n\t\tDEBUG_LOG((\"Window Pos: Saving maximized position.\"));\n\t} else {\n\t\tauto r = body()->mapToGlobal(body()->rect());\n\t\trealPosition.x = r.x();\n\t\trealPosition.y = r.y();\n\t\trealPosition.w = r.width() - (_rightColumn ? _rightColumn->width() : 0);\n\t\trealPosition.h = r.height();\n\t\trealPosition.scale = cScale();\n\t\trealPosition.maximized = 0;\n\t\trealPosition.moncrc = 0;\n\n\t\tDEBUG_LOG((\"Window Pos: Saving non-maximized position: %1, %2, %3, %4\").arg(realPosition.x).arg(realPosition.y).arg(realPosition.w).arg(realPosition.h));\n\t\trealPosition = withScreenInPosition(realPosition);\n\t}\n\tif (realPosition.w >= st::windowMinWidth && realPosition.h >= st::windowMinHeight) {\n\t\tif (realPosition.x != savedPosition.x\n\t\t\t|| realPosition.y != savedPosition.y\n\t\t\t|| realPosition.w != savedPosition.w\n\t\t\t|| realPosition.h != savedPosition.h\n\t\t\t|| realPosition.scale != savedPosition.scale\n\t\t\t|| realPosition.moncrc != savedPosition.moncrc\n\t\t\t|| realPosition.maximized != savedPosition.maximized) {\n\t\t\tDEBUG_LOG((\"Window Pos: Writing: %1, %2, %3, %4 (scale %5%, maximized %6)\")\n\t\t\t\t.arg(realPosition.x)\n\t\t\t\t.arg(realPosition.y)\n\t\t\t\t.arg(realPosition.w)\n\t\t\t\t.arg(realPosition.h)\n\t\t\t\t.arg(realPosition.scale)\n\t\t\t\t.arg(Logs::b(realPosition.maximized)));\n\t\t\tCore::App().settings().setWindowPosition(realPosition);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t}\n\t}\n}\n\nWindowPosition MainWindow::withScreenInPosition(\n\t\tWindowPosition position) const {\n\treturn PositionWithScreen(\n\t\tposition,\n\t\tthis,\n\t\t{ st::windowMinWidth, st::windowMinHeight },\n\t\tu\"Window\"_q);\n}\n\nbool MainWindow::minimizeToTray() {\n\tif (Core::Quitting() || !Core::App().tray().has()) {\n\t\treturn false;\n\t}\n\n\tcloseWithoutDestroy();\n\tcontroller().updateIsActiveBlur();\n\tupdateGlobalMenu();\n\treturn true;\n}\n\nvoid MainWindow::showRightColumn(object_ptr<Ui::RpWidget> widget) {\n\tconst auto wasWidth = width();\n\tconst auto wasRightWidth = _rightColumn ? _rightColumn->width() : 0;\n\t_rightColumn = std::move(widget);\n\tif (_rightColumn) {\n\t\t_rightColumn->setParent(body());\n\t\t_rightColumn->show();\n\t\t_rightColumn->setFocus();\n\t} else {\n\t\tsetInnerFocus();\n\t}\n\tconst auto nowRightWidth = _rightColumn ? _rightColumn->width() : 0;\n\tconst auto wasMinimumWidth = minimumWidth();\n\tconst auto nowMinimumWidth = computeMinWidth();\n\tconst auto firstResize = (nowMinimumWidth < wasMinimumWidth);\n\tif (firstResize) {\n\t\tupdateMinimumSize();\n\t}\n\tif (!isMaximized()) {\n\t\ttryToExtendWidthBy(wasWidth + nowRightWidth - wasRightWidth - width());\n\t} else {\n\t\tupdateControlsGeometry();\n\t}\n\tif (!firstResize) {\n\t\tupdateMinimumSize();\n\t}\n}\n\nint MainWindow::maximalExtendBy() const {\n\tauto desktop = screen()->availableGeometry();\n\treturn std::max(desktop.width() - body()->width(), 0);\n}\n\nbool MainWindow::canExtendNoMove(int extendBy) const {\n\tauto desktop = screen()->availableGeometry();\n\tauto inner = body()->mapToGlobal(body()->rect());\n\tauto innerRight = (inner.x() + inner.width() + extendBy);\n\tauto desktopRight = (desktop.x() + desktop.width());\n\treturn innerRight <= desktopRight;\n}\n\nint MainWindow::tryToExtendWidthBy(int addToWidth) {\n\tauto desktop = screen()->availableGeometry();\n\tauto inner = body()->mapToGlobal(body()->rect());\n\taccumulate_min(\n\t\taddToWidth,\n\t\tstd::max(desktop.width() - inner.width(), 0));\n\tauto newWidth = inner.width() + addToWidth;\n\tauto newLeft = std::min(\n\t\tinner.x(),\n\t\tdesktop.x() + desktop.width() - newWidth);\n\tif (inner.x() != newLeft || inner.width() != newWidth) {\n\t\tsetGeometry(QRect(newLeft, inner.y(), newWidth, inner.height()));\n\t} else {\n\t\tupdateControlsGeometry();\n\t}\n\treturn addToWidth;\n}\n\nvoid MainWindow::launchDrag(\n\t\tstd::unique_ptr<QMimeData> data,\n\t\tFn<void()> &&callback,\n\t\tQPixmap pixmap) {\n\t// Qt destroys this QDrag automatically after the drag is finished\n\t// We must not delete this at the end of this function, as this breaks DnD on Linux\n\tauto drag = new QDrag(this);\n\tKUrlMimeData::exportUrlsToPortal(data.get());\n\tdrag->setMimeData(data.release());\n\tif (!pixmap.isNull()) {\n\t\tdrag->setPixmap(std::move(pixmap));\n\t}\n\tdrag->exec(Qt::CopyAction);\n\n\t// We don't receive mouseReleaseEvent when drag is finished.\n\tClickHandler::unpressed();\n\tcallback();\n}\n\nMainWindow::~MainWindow() {\n\t// Otherwise:\n\t// ~QWidget\n\t// QWidgetPrivate::close_helper\n\t// QWidgetPrivate::setVisible\n\t// QWidgetPrivate::hide_helper\n\t// QWidgetPrivate::hide_sys\n\t// QWindowPrivate::setVisible\n\t// QMetaObject::activate\n\t// Window::MainWindow::handleVisibleChanged on a destroyed MainWindow.\n\thide();\n}\n\nint32 DefaultScreenNameChecksum(const QString &name) {\n\tconst auto bytes = name.toUtf8();\n\treturn base::crc32(bytes.constData(), bytes.size());\n}\n\nWindowPosition PositionWithScreen(\n\t\tWindowPosition position,\n\t\tconst QScreen *chosen,\n\t\tQSize minimal,\n\t\tconst QString &name) {\n\tif (!chosen) {\n\t\treturn position;\n\t}\n\tconst auto available = chosen->availableGeometry();\n\tif (available.width() < minimal.width()\n\t\t|| available.height() < minimal.height()) {\n\t\treturn position;\n\t}\n\taccumulate_min(position.w, available.width());\n\taccumulate_min(position.h, available.height());\n\tif (position.x + position.w > available.x() + available.width()) {\n\t\tposition.x = available.x() + available.width() - position.w;\n\t}\n\tif (position.y + position.h > available.y() + available.height()) {\n\t\tposition.y = available.y() + available.height() - position.h;\n\t}\n\tconst auto geometry = chosen->geometry();\n\tDEBUG_LOG((\"%1 Pos: Screen found, geometry: %2, %3, %4, %5\"\n\t\t).arg(name\n\t\t).arg(geometry.x()\n\t\t).arg(geometry.y()\n\t\t).arg(geometry.width()\n\t\t).arg(geometry.height()));\n\treturn position;\n}\n\nWindowPosition PositionWithScreen(\n\t\tWindowPosition position,\n\t\tnot_null<const QWidget*> widget,\n\t\tQSize minimal,\n\t\tconst QString &name) {\n\tconst auto screen = widget->screen();\n\treturn PositionWithScreen(\n\t\tposition,\n\t\tscreen ? screen : QGuiApplication::primaryScreen(),\n\t\tminimal,\n\t\tname);\n}\n\nQRect CountInitialGeometry(\n\t\tnot_null<const Ui::RpWindow*> widget,\n\t\tWindowPosition position,\n\t\tWindowPosition initial,\n\t\tQSize minSize,\n\t\tconst QString &name) {\n\tif (!position.w || !position.h) {\n\t\treturn initial.rect();\n\t}\n\tconst auto screen = [&]() -> QScreen* {\n\t\tfor (const auto screen : QGuiApplication::screens()) {\n\t\t\tconst auto sum = Platform::ScreenNameChecksum(screen->name());\n\t\t\tif (position.moncrc == sum) {\n\t\t\t\treturn screen;\n\t\t\t}\n\t\t}\n\t\treturn QGuiApplication::screenAt(position.rect().center());\n\t}();\n\tif (!screen) {\n\t\treturn initial.rect();\n\t}\n\tconst auto frame = widget->frameMargins();\n\tconst auto screenGeometry = screen->geometry();\n\tconst auto availableGeometry = screen->availableGeometry();\n\tconst auto spaceForInner = availableGeometry.marginsRemoved(frame);\n\tDEBUG_LOG((\"%1 Pos: \"\n\t\t\"Screen found, screen geometry: %2, %3, %4, %5, \"\n\t\t\"available: %6, %7, %8, %9\"\n\t\t).arg(name\n\t\t).arg(screenGeometry.x()\n\t\t).arg(screenGeometry.y()\n\t\t).arg(screenGeometry.width()\n\t\t).arg(screenGeometry.height()\n\t\t).arg(availableGeometry.x()\n\t\t).arg(availableGeometry.y()\n\t\t).arg(availableGeometry.width()\n\t\t).arg(availableGeometry.height()));\n\tDEBUG_LOG((\"%1 Pos: \"\n\t\t\"Window frame margins: %2, %3, %4, %5, \"\n\t\t\"available space for inner geometry: %6, %7, %8, %9\"\n\t\t).arg(name\n\t\t).arg(frame.left()\n\t\t).arg(frame.top()\n\t\t).arg(frame.right()\n\t\t).arg(frame.bottom()\n\t\t).arg(spaceForInner.x()\n\t\t).arg(spaceForInner.y()\n\t\t).arg(spaceForInner.width()\n\t\t).arg(spaceForInner.height()));\n\n\tconst auto x = spaceForInner.x()\n\t\t- (position.moncrc ? screenGeometry.x() : 0);\n\tconst auto y = spaceForInner.y()\n\t\t- (position.moncrc ? screenGeometry.y() : 0);\n\tconst auto w = spaceForInner.width();\n\tconst auto h = spaceForInner.height();\n\tif (w < st::windowMinWidth || h < st::windowMinHeight) {\n\t\treturn initial.rect();\n\t}\n\tif (position.x < x) position.x = x;\n\tif (position.y < y) position.y = y;\n\tif (position.w > w) position.w = w;\n\tif (position.h > h) position.h = h;\n\tconst auto rightPoint = position.x + position.w;\n\tconst auto screenRightPoint = x + w;\n\tif (rightPoint > screenRightPoint) {\n\t\tconst auto distance = rightPoint - screenRightPoint;\n\t\tconst auto newXPos = position.x - distance;\n\t\tif (newXPos >= x) {\n\t\t\tposition.x = newXPos;\n\t\t} else {\n\t\t\tposition.x = x;\n\t\t\tconst auto newRightPoint = position.x + position.w;\n\t\t\tconst auto newDistance = newRightPoint - screenRightPoint;\n\t\t\tposition.w -= newDistance;\n\t\t}\n\t}\n\tconst auto bottomPoint = position.y + position.h;\n\tconst auto screenBottomPoint = y + h;\n\tif (bottomPoint > screenBottomPoint) {\n\t\tconst auto distance = bottomPoint - screenBottomPoint;\n\t\tconst auto newYPos = position.y - distance;\n\t\tif (newYPos >= y) {\n\t\t\tposition.y = newYPos;\n\t\t} else {\n\t\t\tposition.y = y;\n\t\t\tconst auto newBottomPoint = position.y + position.h;\n\t\t\tconst auto newDistance = newBottomPoint - screenBottomPoint;\n\t\t\tposition.h -= newDistance;\n\t\t}\n\t}\n\tif (position.moncrc) {\n\t\tposition.x += screenGeometry.x();\n\t\tposition.y += screenGeometry.y();\n\t}\n\tif ((position.x + st::windowMinWidth\n\t\t> screenGeometry.x() + screenGeometry.width())\n\t\t|| (position.y + st::windowMinHeight\n\t\t\t> screenGeometry.y() + screenGeometry.height())) {\n\t\treturn initial.rect();\n\t}\n\tDEBUG_LOG((\"%1 Pos: Resulting geometry is %2, %3, %4, %5\"\n\t\t).arg(name\n\t\t).arg(position.x\n\t\t).arg(position.y\n\t\t).arg(position.w\n\t\t).arg(position.h));\n\treturn position.rect();\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/main_window.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/widgets/rp_window.h\"\n#include \"base/timer.h\"\n#include \"base/object_ptr.h\"\n#include \"core/core_settings.h\"\n\nnamespace Main {\nclass Session;\nclass Account;\n} // namespace Main\n\nnamespace Ui {\nclass BoxContent;\nclass PlainShadow;\n} // namespace Ui\n\nnamespace Core {\nstruct WindowPosition;\nenum class QuitReason;\n} // namespace Core\n\nnamespace Window {\n\nclass Controller;\nclass SessionController;\nclass TitleWidget;\nstruct TermsLock;\nstruct SeparateId;\n\n[[nodiscard]] const QImage &Logo();\n[[nodiscard]] const QImage &LogoNoMargin();\nvoid OverrideApplicationIcon(QImage image);\n[[nodiscard]] QIcon CreateIcon(\n\tMain::Session *session = nullptr,\n\tbool returnNullIfDefault = false);\nvoid ConvertIconToBlack(QImage &image);\n\nstruct CounterLayerArgs {\n\ttemplate <typename T>\n\tusing required = base::required<T>;\n\n\trequired<int> size = 16;\n\tdouble devicePixelRatio = 1.;\n\trequired<int> count = 1;\n\trequired<style::color> bg;\n\trequired<style::color> fg;\n};\n\nextern const char kOptionNewWindowsSizeAsFirst[];\nextern const char kOptionDisableTouchbar[];\n\n[[nodiscard]] QImage GenerateCounterLayer(CounterLayerArgs &&args);\n[[nodiscard]] QImage WithSmallCounter(QImage image, CounterLayerArgs &&args);\n\nclass MainWindow : public Ui::RpWindow {\npublic:\n\texplicit MainWindow(not_null<Controller*> controller);\n\tvirtual ~MainWindow();\n\n\t[[nodiscard]] Window::Controller &controller() const {\n\t\treturn *_controller;\n\t}\n\t[[nodiscard]] Window::SeparateId id() const;\n\t[[nodiscard]] bool isPrimary() const;\n\t[[nodiscard]] Main::Account &account() const;\n\t[[nodiscard]] Window::SessionController *sessionController() const;\n\n\tbool hideNoQuit();\n\n\tvoid showFromTray();\n\tvoid quitFromTray();\n\tvoid activate();\n\n\t[[nodiscard]] QRect desktopRect() const;\n\t[[nodiscard]] Core::WindowPosition withScreenInPosition(\n\t\tCore::WindowPosition position) const;\n\n\tvoid init();\n\n\tvoid updateIsActive();\n\n\t[[nodiscard]] bool isActive() const {\n\t\treturn !isHidden() && _isActive;\n\t}\n\t[[nodiscard]] virtual bool isActiveForTrayMenu() {\n\t\tupdateIsActive();\n\t\treturn isActive();\n\t}\n\n\tbool positionInited() const {\n\t\treturn _positionInited;\n\t}\n\tvoid positionUpdated();\n\n\tvoid showRightColumn(object_ptr<Ui::RpWidget> widget);\n\tint maximalExtendBy() const;\n\tbool canExtendNoMove(int extendBy) const;\n\n\t// Returns how much could the window get extended.\n\tint tryToExtendWidthBy(int addToWidth);\n\n\tvirtual void fixOrder() {\n\t}\n\tvirtual void setInnerFocus() {\n\t\tsetFocus();\n\t}\n\n\tUi::RpWidget *bodyWidget() {\n\t\treturn _body.data();\n\t}\n\n\tvoid launchDrag(\n\t\tstd::unique_ptr<QMimeData> data,\n\t\tFn<void()> &&callback,\n\t\tQPixmap pixmap = QPixmap());\n\n\t[[nodiscard]] rpl::producer<> leaveEvents() const;\n\t[[nodiscard]] rpl::producer<> imeCompositionStarts() const;\n\n\tvirtual void updateWindowIcon() = 0;\n\tvoid updateTitle();\n\n\tvoid clearWidgets();\n\n\tint computeMinWidth() const;\n\tint computeMinHeight() const;\n\n\tvoid recountGeometryConstraints();\n\tvirtual void updateControlsGeometry();\n\n\tvoid firstShow();\n\tbool minimizeToTray();\n\tvoid updateGlobalMenu() {\n\t\tupdateGlobalMenuHook();\n\t}\n\n\t[[nodiscard]] virtual rpl::producer<QPoint> globalForceClicks() {\n\t\treturn rpl::never<QPoint>();\n\t}\n\nprotected:\n\tvoid leaveEventHook(QEvent *e) override;\n\n\tvoid savePosition(Qt::WindowState state = Qt::WindowActive);\n\tvoid handleStateChanged(Qt::WindowState state);\n\tvoid handleActiveChanged(bool active);\n\tvoid handleVisibleChanged(bool visible);\n\n\tvirtual void checkActivation() {\n\t}\n\tvirtual void initHook() {\n\t}\n\n\tvirtual void handleVisibleChangedHook(bool visible) {\n\t}\n\n\tvirtual void clearWidgetsHook() {\n\t}\n\n\tvirtual void stateChangedHook(Qt::WindowState state) {\n\t}\n\n\tvirtual void unreadCounterChangedHook() {\n\t}\n\n\tvirtual void closeWithoutDestroy() {\n\t\thide();\n\t}\n\n\tvirtual void updateGlobalMenuHook() {\n\t}\n\n\tvirtual void workmodeUpdated(Core::Settings::WorkMode mode) {\n\t}\n\n\tvirtual void createGlobalMenu() {\n\t}\n\n\tvirtual bool initGeometryFromSystem() {\n\t\treturn false;\n\t}\n\n\tvoid imeCompositionStartReceived();\n\tvoid setPositionInited();\n\n\tvirtual QRect computeDesktopRect() const;\n\nprivate:\n\tvoid refreshTitleWidget();\n\tvoid updateMinimumSize();\n\tvoid updatePalette();\n\n\t[[nodiscard]] Core::WindowPosition initialPosition() const;\n\t[[nodiscard]] Core::WindowPosition nextInitialChildPosition(\n\t\tSeparateId childId);\n\t[[nodiscard]] QRect countInitialGeometry(Core::WindowPosition position);\n\n\tbool computeIsActive() const;\n\n\tnot_null<Window::Controller*> _controller;\n\n\tbase::Timer _positionUpdatedTimer;\n\tbool _positionInited = false;\n\n\tobject_ptr<Ui::PlainShadow> _titleShadow = { nullptr };\n\tobject_ptr<Ui::RpWidget> _outdated;\n\tobject_ptr<Ui::RpWidget> _screenReaderBar;\n\tobject_ptr<Ui::RpWidget> _body;\n\tobject_ptr<Ui::RpWidget> _rightColumn = { nullptr };\n\n\tbool _isActive = false;\n\n\trpl::event_stream<> _leaveEvents;\n\trpl::event_stream<> _imeCompositionStartReceived;\n\n\tbool _maximizedBeforeHide = false;\n\n\tQPoint _lastMyChildCreatePosition;\n\tint _lastChildIndex = 0;\n\n\tmutable QRect _monitorRect;\n\tmutable crl::time _monitorLastGot = 0;\n\n};\n\n[[nodiscard]] int32 DefaultScreenNameChecksum(const QString &name);\n\n[[nodiscard]] Core::WindowPosition PositionWithScreen(\n\tCore::WindowPosition position,\n\tconst QScreen *chosen,\n\tQSize minimal,\n\tconst QString &name);\n[[nodiscard]] Core::WindowPosition PositionWithScreen(\n\tCore::WindowPosition position,\n\tnot_null<const QWidget*> widget,\n\tQSize minimal,\n\tconst QString &name);\n\n[[nodiscard]] QRect CountInitialGeometry(\n\tnot_null<const Ui::RpWindow*> widget,\n\tCore::WindowPosition position,\n\tCore::WindowPosition initial,\n\tQSize minSize,\n\tconst QString &name);\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/notifications_manager.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/notifications_manager.h\"\n\n#include \"base/options.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"platform/platform_notifications_manager.h\"\n#include \"window/notifications_manager_default.h\"\n#include \"media/audio/media_audio_track.h\"\n#include \"media/audio/media_audio.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"history/history.h\"\n#include \"history/history_item_components.h\"\n#include \"history/view/history_view_chat_section.h\"\n#include \"lang/lang_keys.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_user.h\"\n#include \"data/data_document.h\"\n#include \"data/data_poll.h\"\n#include \"base/unixtime.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"core/application.h\"\n#include \"mainwindow.h\"\n#include \"api/api_reactions_notify_settings.h\"\n#include \"api/api_updates.h\"\n#include \"apiwrap.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"platform/platform_specific.h\"\n\n#include <QtGui/QWindow>\n#include <QtGui/QGuiApplication>\n#include <QtGui/QScreen>\n\n#if __has_include(<gio/gio.hpp>)\n#include <gio/gio.hpp>\n#endif // __has_include(<gio/gio.hpp>)\n\nnamespace Window {\nnamespace Notifications {\nnamespace {\n\n// not more than one sound in 500ms from one peer - grouping\nconstexpr auto kMinimalDelay = crl::time(100);\nconstexpr auto kMinimalForwardDelay = crl::time(500);\nconstexpr auto kMinimalAlertDelay = crl::time(500);\nconstexpr auto kWaitingForAllGroupedDelay = crl::time(1000);\nconstexpr auto kReactionNotificationEach = 60 * 60 * crl::time(1000);\n\n#ifdef Q_OS_MAC\nconstexpr auto kSystemAlertDuration = crl::time(1000);\n#else // !Q_OS_MAC\nconstexpr auto kSystemAlertDuration = crl::time(0);\n#endif // Q_OS_MAC\n\n[[nodiscard]] QString PlaceholderReactionText() {\n\tstatic const auto result = QString::fromUtf8(\"\\xf0\\x9f\\x92\\xad\");\n\treturn result;\n}\n\n[[nodiscard]] QString TextWithForwardedChar(\n\t\tconst QString &text,\n\t\tbool forwarded) {\n\tstatic const auto result = QString::fromUtf8(\"\\xE2\\x9E\\xA1\\xEF\\xB8\\x8F\");\n\treturn forwarded ? result + text : text;\n}\n\n[[nodiscard]] QString TextWithPermanentSpoiler(\n\t\tconst TextWithEntities &textWithEntities) {\n\tauto text = textWithEntities.text;\n\tfor (const auto &e : textWithEntities.entities) {\n\t\tif (e.type() == EntityType::Spoiler) {\n\t\t\tauto replacement = QString().fill(QChar(0x259A), e.length());\n\t\t\ttext = text.replace(\n\t\t\t\te.offset(),\n\t\t\t\te.length(),\n\t\t\t\tstd::move(replacement));\n\t\t}\n\t}\n\treturn text;\n}\n\n[[nodiscard]] QByteArray ReadRingtoneBytes(\n\t\tconst std::shared_ptr<Data::DocumentMedia> &media) {\n\tconst auto result = media->bytes();\n\tif (!result.isEmpty()) {\n\t\treturn result;\n\t}\n\tconst auto &location = media->owner()->location();\n\tif (!location.isEmpty() && location.accessEnable()) {\n\t\tconst auto guard = gsl::finally([&] {\n\t\t\tlocation.accessDisable();\n\t\t});\n\t\tauto f = QFile(location.name());\n\t\tif (f.open(QIODevice::ReadOnly)) {\n\t\t\treturn f.readAll();\n\t\t}\n\t}\n\treturn {};\n}\n\n[[nodiscard]] std::optional<DocumentId> MaybeSoundFor(\n\t\tnot_null<Data::Thread*> thread,\n\t\tPeerData *from) {\n\tconst auto notifySettings = &thread->owner().notifySettings();\n\tconst auto threadUnknown = notifySettings->muteUnknown(thread);\n\tconst auto threadAlert = !threadUnknown\n\t\t&& !notifySettings->isMuted(thread);\n\tconst auto fromUnknown = (!from\n\t\t|| notifySettings->muteUnknown(from));\n\tconst auto fromAlert = !fromUnknown\n\t\t&& !notifySettings->isMuted(from);\n\tconst auto &sound = notifySettings->sound(thread);\n\treturn ((threadAlert || fromAlert) && !sound.none)\n\t\t? sound.id\n\t\t: std::optional<DocumentId>();\n}\n\n[[nodiscard]] bool AllowNotificationActions(not_null<PeerData*> peer) {\n\treturn Platform::IsMac() && peer->isNotificationsUser();\n}\n\n} // namespace\n\nconst char kOptionCustomNotification[] = \"custom-notification\";\n\nbase::options::toggle OptionCustomNotification({\n\t.id = kOptionCustomNotification,\n\t.name = \"Force non-native notifications availability\",\n\t.description = \"Allow to disable native notifications\"\n\t\t\" even if custom notifications are broken on this platform\",\n\t.scope = [] {\n\t\treturn Platform::Notifications::Enforced();\n\t},\n\t.restartRequired = true,\n});\n\nconst char kOptionGNotification[] = \"gnotification\";\nconst char kOptionHideReplyButton[] = \"hide-reply-button\";\n\nbase::options::toggle OptionGNotification({\n\t.id = kOptionGNotification,\n\t.name = \"GNotification\",\n\t.description = \"Force enable GLib's GNotification.\"\n\t\t\" When disabled, autodetect is used.\",\n\t.scope = [] {\n#if __has_include(<gio/gio.hpp>)\n\t\tusing namespace gi::repository;\n\t\treturn bool(Gio::Application::get_default());\n#else // __has_include(<gio/gio.hpp>)\n\t\treturn false;\n#endif // __has_include(<gio/gio.hpp>)\n\t},\n\t.restartRequired = true,\n});\n\nbase::options::toggle HideReplyButtonOption({\n\t.id = kOptionHideReplyButton,\n\t.name = \"Hide reply button\",\n\t.description = \"Hide reply button in notifications.\",\n});\n\nstruct System::Waiter {\n\tNotificationInHistoryKey key;\n\tUserData *reactionOrVoteSender = nullptr;\n\tData::ItemNotificationType type = Data::ItemNotificationType::Message;\n\tcrl::time when = 0;\n};\n\nSystem::NotificationInHistoryKey::NotificationInHistoryKey(\n\tData::ItemNotification notification)\n: NotificationInHistoryKey(notification.item->id, notification.type) {\n}\n\nSystem::NotificationInHistoryKey::NotificationInHistoryKey(\n\tMsgId messageId,\n\tData::ItemNotificationType type)\n: messageId(messageId)\n, type(type) {\n}\n\nSystem::System()\n: _waitTimer([=] { showNext(); })\n, _waitForAllGroupedTimer([=] { showGrouped(); })\n, _manager(std::make_unique<DummyManager>(this)) {\n\tsettingsChanged(\n\t) | rpl::on_next([=](ChangeType type) {\n\t\tif (type == ChangeType::DesktopEnabled) {\n\t\t\tclearAll();\n\t\t} else if (type == ChangeType::ViewParams) {\n\t\t\tupdateAll();\n\t\t} else if (type == ChangeType::IncludeMuted\n\t\t\t|| type == ChangeType::CountMessages) {\n\t\t\tCore::App().domain().notifyUnreadBadgeChanged();\n\t\t}\n\t}, lifetime());\n}\n\nvoid System::createManager() {\n\tPlatform::Notifications::Create(this);\n}\n\nvoid System::setManager(Fn<std::unique_ptr<Manager>()> create) {\n\tExpects(_manager != nullptr);\n\tconst auto oldManager = _manager.get();\n\tconst auto guard = gsl::finally([&] {\n\t\tEnsures(_manager != nullptr);\n\t\tif (oldManager != _manager.get()) {\n\t\t\t_managerChanged.fire({});\n\t\t}\n\t});\n\n\tif ((Core::App().settings().nativeNotifications()\n\t\t\t\t|| nativeEnforced())\n\t\t\t&& Platform::Notifications::Supported()) {\n\t\tif (_manager->type() == ManagerType::Native) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (auto manager = create()) {\n\t\t\t_manager = std::move(manager);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tif (nativeEnforced()) {\n\t\tif (_manager->type() != ManagerType::Dummy) {\n\t\t\t_manager = std::make_unique<DummyManager>(this);\n\t\t}\n\t} else if (_manager->type() != ManagerType::Default) {\n\t\t_manager = std::make_unique<Default::Manager>(this);\n\t}\n}\n\nManager &System::manager() const {\n\tExpects(_manager != nullptr);\n\treturn *_manager;\n}\n\nrpl::producer<> System::managerChanged() const {\n\treturn _managerChanged.events();\n}\n\nbool System::nativeEnforced() const {\n\treturn !OptionCustomNotification.value() && Platform::Notifications::Enforced();\n}\n\nMain::Session *System::findSession(uint64 sessionId) const {\n\tfor (const auto &[index, account] : Core::App().domain().accounts()) {\n\t\tif (const auto session = account->maybeSession()) {\n\t\t\tif (session->uniqueId() == sessionId) {\n\t\t\t\treturn session;\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nbool System::skipSentNotification(\n\t\tnot_null<HistoryItem*> item,\n\t\tbase::flat_map<SentNotificationId, crl::time> &already) const {\n\tconst auto id = SentNotificationId{\n\t\t.itemId = item->fullId(),\n\t\t.sessionId = item->history()->session().uniqueId(),\n\t};\n\tconst auto now = crl::now();\n\tconst auto clearBefore = now - kReactionNotificationEach;\n\tfor (auto i = begin(already); i != end(already);) {\n\t\tif (i->second <= clearBefore) {\n\t\t\ti = already.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\treturn !already.emplace(id, now).second;\n}\n\nSystem::SkipState System::skipNotification(\n\t\tData::ItemNotification notification) const {\n\tconst auto item = notification.item;\n\tconst auto type = notification.type;\n\tconst auto messageType = (type == Data::ItemNotificationType::Message);\n\tconst auto thread = item->maybeNotificationThread();\n\tif (!thread\n\t\t|| !thread->currentNotification()\n\t\t|| (messageType && item->skipNotification())\n\t\t|| (type == Data::ItemNotificationType::Reaction\n\t\t\t&& skipSentNotification(item, _sentReactionNotifications))\n\t\t|| (type == Data::ItemNotificationType::PollVote\n\t\t\t&& skipSentNotification(item, _sentPollVoteNotifications))) {\n\t\treturn { SkipState::Skip };\n\t}\n\treturn computeSkipState(notification);\n}\n\nSystem::SkipState System::computeSkipState(\n\t\tData::ItemNotification notification) const {\n\tconst auto type = notification.type;\n\tconst auto item = notification.item;\n\tconst auto thread = item->notificationThread();\n\tconst auto notifySettings = &thread->owner().notifySettings();\n\tconst auto messageType = (type == Data::ItemNotificationType::Message);\n\tconst auto withSilent = [&](\n\t\t\tSkipState::Value value,\n\t\t\tbool forceSilent = false) {\n\t\treturn SkipState{\n\t\t\t.value = value,\n\t\t\t.silent = (forceSilent\n\t\t\t\t|| !messageType\n\t\t\t\t|| item->isSilent()\n\t\t\t\t|| notifySettings->sound(thread).none),\n\t\t};\n\t};\n\tconst auto showForMuted = messageType\n\t\t&& item->out()\n\t\t&& item->isFromScheduled();\n\tconst auto notifyBy = messageType\n\t\t? item->specialNotificationPeer()\n\t\t: notification.reactionOrVoteSender;\n\tif (Core::Quitting()) {\n\t\treturn { SkipState::Skip };\n\t} else if (!Core::App().settings().notifyFromAll()\n\t\t&& &thread->session().account() != &Core::App().domain().active()) {\n\t\treturn { SkipState::Skip };\n\t}\n\n\tif (messageType) {\n\t\tnotifySettings->request(thread);\n\t} else if (notifyBy->blockStatus() == PeerData::BlockStatus::Unknown) {\n\t\tnotifyBy->updateFull();\n\t}\n\tif (notifyBy) {\n\t\tnotifySettings->request(notifyBy);\n\t}\n\n\tif (messageType && notifySettings->muteUnknown(thread)) {\n\t\treturn { SkipState::Unknown };\n\t} else if (messageType && !notifySettings->isMuted(thread)) {\n\t\treturn withSilent(SkipState::DontSkip);\n\t} else if (!notifyBy) {\n\t\treturn withSilent(\n\t\t\tshowForMuted ? SkipState::DontSkip : SkipState::Skip,\n\t\t\tshowForMuted);\n\t} else if (notifySettings->muteUnknown(notifyBy)\n\t\t|| (!messageType\n\t\t\t&& notifyBy->blockStatus() == PeerData::BlockStatus::Unknown)) {\n\t\treturn withSilent(SkipState::Unknown);\n\t} else if (!notifySettings->isMuted(notifyBy)\n\t\t&& (messageType || !notifyBy->isBlocked())) {\n\t\treturn withSilent(SkipState::DontSkip);\n\t} else {\n\t\treturn withSilent(\n\t\t\tshowForMuted ? SkipState::DontSkip : SkipState::Skip,\n\t\t\tshowForMuted);\n\t}\n}\n\nSystem::Timing System::countTiming(\n\t\tnot_null<Data::Thread*> thread,\n\t\tcrl::time minimalDelay) const {\n\tauto delay = minimalDelay;\n\tconst auto t = base::unixtime::now();\n\tconst auto ms = crl::now();\n\tconst auto &updates = thread->session().updates();\n\tconst auto &config = thread->session().serverConfig();\n\tconst bool isOnline = updates.lastWasOnline();\n\tconst auto otherNotOld = ((cOtherOnline() * 1000LL) + config.onlineCloudTimeout > t * 1000LL);\n\tconst bool otherLaterThanMe = (cOtherOnline() * 1000LL + (ms - updates.lastSetOnline()) > t * 1000LL);\n\tif (!isOnline && otherNotOld && otherLaterThanMe) {\n\t\tdelay = config.notifyCloudDelay;\n\t} else if (cOtherOnline() >= t) {\n\t\tdelay = config.notifyDefaultDelay;\n\t}\n\treturn {\n\t\t.delay = delay,\n\t\t.when = ms + delay,\n\t};\n}\n\nvoid System::registerThread(not_null<Data::Thread*> thread) {\n\tif (const auto topic = thread->asTopic()) {\n\t\tconst auto &[i, ok] = _watchedTopics.emplace(topic, rpl::lifetime());\n\t\tif (ok) {\n\t\t\ttopic->destroyed() | rpl::on_next([=] {\n\t\t\t\tclearFromTopic(topic);\n\t\t\t}, i->second);\n\t\t}\n\t} else if (const auto sublist = thread->asSublist()) {\n\t\tconst auto &[i, ok] = _watchedSublists.emplace(\n\t\t\tsublist,\n\t\t\trpl::lifetime());\n\t\tif (ok) {\n\t\t\tsublist->destroyed() | rpl::on_next([=] {\n\t\t\t\tclearFromSublist(sublist);\n\t\t\t}, i->second);\n\t\t}\n\t}\n}\n\nvoid System::schedule(Data::ItemNotification notification) {\n\tExpects(_manager != nullptr);\n\n\tconst auto item = notification.item;\n\tconst auto type = notification.type;\n\tconst auto thread = item->notificationThread();\n\tconst auto skip = skipNotification(notification);\n\tif (skip.value == SkipState::Skip) {\n\t\tthread->popNotification(notification);\n\t\treturn;\n\t}\n\tconst auto ready = (skip.value != SkipState::Unknown)\n\t\t&& item->notificationReady();\n\n\tconst auto minimalDelay = (type == Data::ItemNotificationType::Reaction\n\t\t|| type == Data::ItemNotificationType::PollVote)\n\t\t? kMinimalDelay\n\t\t: item->Has<HistoryMessageForwarded>()\n\t\t? kMinimalForwardDelay\n\t\t: kMinimalDelay;\n\tconst auto timing = countTiming(thread, minimalDelay);\n\tconst auto notifyBy = (type == Data::ItemNotificationType::Message)\n\t\t? item->specialNotificationPeer()\n\t\t: notification.reactionOrVoteSender;\n\tif (!skip.silent) {\n\t\tregisterThread(thread);\n\t\t_whenAlerts[thread].emplace(timing.when, notifyBy);\n\t}\n\tif (const auto user = item->history()->peer->asUser()) {\n\t\tif (user->hasStarsPerMessage()\n\t\t\t&& !user->messageMoneyRestrictionsKnown()) {\n\t\t\tuser->updateFull();\n\t\t}\n\t}\n\tif (Core::App().settings().desktopNotify()\n\t\t&& !_manager->skipToast()) {\n\t\tregisterThread(thread);\n\t\tconst auto key = NotificationInHistoryKey(notification);\n\t\tauto &whenMap = _whenMaps[thread];\n\t\tif (whenMap.find(key) == whenMap.end()) {\n\t\t\twhenMap.emplace(key, timing.when);\n\t\t}\n\n\t\tauto &addTo = ready ? _waiters : _settingWaiters;\n\t\tconst auto it = addTo.find(thread);\n\t\tif (it == addTo.end() || it->second.when > timing.when) {\n\t\t\taddTo.emplace(thread, Waiter{\n\t\t\t\t.key = key,\n\t\t\t\t.reactionOrVoteSender = notification.reactionOrVoteSender,\n\t\t\t\t.type = notification.type,\n\t\t\t\t.when = timing.when,\n\t\t\t});\n\t\t}\n\t}\n\tif (ready) {\n\t\tif (!_waitTimer.isActive()\n\t\t\t|| _waitTimer.remainingTime() > timing.delay) {\n\t\t\t_waitTimer.callOnce(timing.delay);\n\t\t}\n\t}\n}\n\nvoid System::clearAll() {\n\tif (_manager) {\n\t\t_manager->clearAll();\n\t}\n\n\tfor (const auto &[thread, _] : _whenMaps) {\n\t\tthread->clearNotifications();\n\t}\n\t_whenMaps.clear();\n\t_whenAlerts.clear();\n\t_waiters.clear();\n\t_settingWaiters.clear();\n\t_watchedTopics.clear();\n\t_watchedSublists.clear();\n}\n\nvoid System::clearFromTopic(not_null<Data::ForumTopic*> topic) {\n\tif (_manager) {\n\t\t_manager->clearFromTopic(topic);\n\t}\n\n\ttopic->clearNotifications();\n\t_whenMaps.remove(topic);\n\t_whenAlerts.remove(topic);\n\t_waiters.remove(topic);\n\t_settingWaiters.remove(topic);\n\n\t_watchedTopics.remove(topic);\n\n\t_waitTimer.cancel();\n\tshowNext();\n}\n\nvoid System::clearFromSublist(not_null<Data::SavedSublist*> sublist) {\n\tif (_manager) {\n\t\t_manager->clearFromSublist(sublist);\n\t}\n\n\tsublist->clearNotifications();\n\t_whenMaps.remove(sublist);\n\t_whenAlerts.remove(sublist);\n\t_waiters.remove(sublist);\n\t_settingWaiters.remove(sublist);\n\n\t_watchedSublists.remove(sublist);\n\n\t_waitTimer.cancel();\n\tshowNext();\n}\n\nvoid System::clearForThreadIf(Fn<bool(not_null<Data::Thread*>)> predicate) {\n\tfor (auto i = _whenMaps.begin(); i != _whenMaps.end();) {\n\t\tconst auto thread = i->first;\n\t\tif (!predicate(thread)) {\n\t\t\t++i;\n\t\t\tcontinue;\n\t\t}\n\t\ti = _whenMaps.erase(i);\n\n\t\tthread->clearNotifications();\n\t\t_whenAlerts.remove(thread);\n\t\t_waiters.remove(thread);\n\t\t_settingWaiters.remove(thread);\n\t\tif (const auto topic = thread->asTopic()) {\n\t\t\t_watchedTopics.remove(topic);\n\t\t} else if (const auto sublist = thread->asSublist()) {\n\t\t\t_watchedSublists.remove(sublist);\n\t\t}\n\t}\n\tconst auto clearFrom = [&](auto &map) {\n\t\tfor (auto i = map.begin(); i != map.end();) {\n\t\t\tconst auto thread = i->first;\n\t\t\tif (predicate(thread)) {\n\t\t\t\tif (const auto topic = thread->asTopic()) {\n\t\t\t\t\t_watchedTopics.remove(topic);\n\t\t\t\t} else if (const auto sublist = thread->asSublist()) {\n\t\t\t\t\t_watchedSublists.remove(sublist);\n\t\t\t\t}\n\t\t\t\ti = map.erase(i);\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t};\n\tclearFrom(_whenAlerts);\n\tclearFrom(_waiters);\n\tclearFrom(_settingWaiters);\n\n\t_waitTimer.cancel();\n\tshowNext();\n}\n\nvoid System::clearFromHistory(not_null<History*> history) {\n\tif (_manager) {\n\t\t_manager->clearFromHistory(history);\n\t}\n\tclearForThreadIf([&](not_null<Data::Thread*> thread) {\n\t\treturn (thread->owningHistory() == history);\n\t});\n}\n\nvoid System::clearFromSession(not_null<Main::Session*> session) {\n\tif (_manager) {\n\t\t_manager->clearFromSession(session);\n\t}\n\tclearForThreadIf([&](not_null<Data::Thread*> thread) {\n\t\treturn (&thread->session() == session);\n\t});\n}\n\nvoid System::clearIncomingFromHistory(not_null<History*> history) {\n\tif (_manager) {\n\t\t_manager->clearFromHistory(history);\n\t}\n\thistory->clearIncomingNotifications();\n\t_whenAlerts.remove(history);\n}\n\nvoid System::clearIncomingFromTopic(not_null<Data::ForumTopic*> topic) {\n\tif (_manager) {\n\t\t_manager->clearFromTopic(topic);\n\t}\n\ttopic->clearIncomingNotifications();\n\t_whenAlerts.remove(topic);\n}\n\nvoid System::clearIncomingFromSublist(\n\t\tnot_null<Data::SavedSublist*> sublist) {\n\tif (_manager) {\n\t\t_manager->clearFromSublist(sublist);\n\t}\n\tsublist->clearIncomingNotifications();\n\t_whenAlerts.remove(sublist);\n}\n\nvoid System::clearFromItem(not_null<HistoryItem*> item) {\n\tif (_manager) {\n\t\t_manager->clearFromItem(item);\n\t}\n}\n\nvoid System::clearAllFast() {\n\tif (_manager) {\n\t\t_manager->clearAllFast();\n\t}\n\n\t_whenMaps.clear();\n\t_whenAlerts.clear();\n\t_waiters.clear();\n\t_settingWaiters.clear();\n\t_watchedTopics.clear();\n\t_watchedSublists.clear();\n}\n\nvoid System::checkDelayed() {\n\tfor (auto i = _settingWaiters.begin(); i != _settingWaiters.end();) {\n\t\tconst auto remove = [&] {\n\t\t\tconst auto thread = i->first;\n\t\t\tconst auto peer = thread->peer();\n\t\t\tconst auto fullId = FullMsgId(peer->id, i->second.key.messageId);\n\t\t\tconst auto item = thread->owner().message(fullId);\n\t\t\tif (!item) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst auto state = computeSkipState({\n\t\t\t\t.item = item,\n\t\t\t\t.reactionOrVoteSender = i->second.reactionOrVoteSender,\n\t\t\t\t.type = i->second.type,\n\t\t\t});\n\t\t\tif (state.value == SkipState::Skip) {\n\t\t\t\treturn true;\n\t\t\t} else if (state.value == SkipState::Unknown\n\t\t\t\t|| !item->notificationReady()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t_waiters.emplace(i->first, i->second);\n\t\t\treturn true;\n\t\t}();\n\t\tif (remove) {\n\t\t\ti = _settingWaiters.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\t_waitTimer.cancel();\n\tshowNext();\n}\n\nvoid System::showGrouped() {\n\tExpects(_manager != nullptr);\n\n\tif (const auto session = findSession(_lastHistorySessionId)) {\n\t\tif (const auto lastItem = session->data().message(_lastHistoryItemId)) {\n\t\t\t_waitForAllGroupedTimer.cancel();\n\t\t\t_manager->showNotification({\n\t\t\t\t.item = lastItem,\n\t\t\t\t.forwardedCount = _lastForwardedCount,\n\t\t\t\t.soundId = _lastSoundId,\n\t\t\t});\n\t\t\t_lastForwardedCount = 0;\n\t\t\t_lastHistoryItemId = FullMsgId();\n\t\t\t_lastHistorySessionId = 0;\n\t\t\t_lastSoundId = {};\n\t\t}\n\t}\n}\n\nvoid System::showNext() {\n\tExpects(_manager != nullptr);\n\n\tif (Core::Quitting()) {\n\t\treturn;\n\t}\n\n\tconst auto isSameGroup = [=](HistoryItem *item) {\n\t\tif (!_lastHistorySessionId || !_lastHistoryItemId || !item) {\n\t\t\treturn false;\n\t\t} else if (item->history()->session().uniqueId()\n\t\t\t!= _lastHistorySessionId) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto lastItem = item->history()->owner().message(\n\t\t\t_lastHistoryItemId);\n\t\tif (lastItem) {\n\t\t\treturn (lastItem->groupId() == item->groupId())\n\t\t\t\t|| (lastItem->author() == item->author());\n\t\t}\n\t\treturn false;\n\t};\n\tauto ms = crl::now(), nextAlert = crl::time(0);\n\tauto alertThread = (Data::Thread*)nullptr;\n\tauto alertSoundId = std::optional<DocumentId>();\n\tfor (auto i = _whenAlerts.begin(); i != _whenAlerts.end();) {\n\t\twhile (!i->second.empty() && i->second.begin()->first <= ms) {\n\t\t\tconst auto thread = i->first;\n\t\t\tconst auto from = i->second.begin()->second;\n\t\t\tif (const auto soundId = MaybeSoundFor(thread, from)) {\n\t\t\t\talertThread = thread;\n\t\t\t\talertSoundId = soundId;\n\t\t\t}\n\t\t\twhile (!i->second.empty()\n\t\t\t\t&& i->second.begin()->first <= ms + kMinimalAlertDelay) {\n\t\t\t\ti->second.erase(i->second.begin());\n\t\t\t}\n\t\t}\n\t\tif (i->second.empty()) {\n\t\t\ti = _whenAlerts.erase(i);\n\t\t} else {\n\t\t\tif (!nextAlert || nextAlert > i->second.begin()->first) {\n\t\t\t\tnextAlert = i->second.begin()->first;\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t}\n\tconst auto &settings = Core::App().settings();\n\tif (alertThread) {\n\t\tif (settings.flashBounceNotify()) {\n\t\t\tconst auto peer = alertThread->peer();\n\t\t\tif (const auto window = Core::App().windowFor(peer)) {\n\t\t\t\tif (const auto controller = window->sessionController()) {\n\t\t\t\t\t_manager->maybeFlashBounce(crl::guard(controller, [=] {\n\t\t\t\t\t\tif (const auto handle = window->widget()->windowHandle()) {\n\t\t\t\t\t\t\thandle->alert(kSystemAlertDuration);\n\t\t\t\t\t\t\t// (handle, SLOT(_q_clearAlert())); in the future.\n\t\t\t\t\t\t}\n\t\t\t\t\t}));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (settings.soundNotify()) {\n\t\t\tconst auto owner = &alertThread->owner();\n\t\t\tconst auto id = owner->notifySettings().sound(alertThread).id;\n\t\t\tauto volume\n\t\t\t\t= owner->session().settings().ringtoneVolume(\n\t\t\t\t\talertThread->peer()->id,\n\t\t\t\t\talertThread->topicRootId(),\n\t\t\t\t\talertThread->monoforumPeerId());\n\t\t\tif (!volume) {\n\t\t\t\tvolume = owner->session().settings().ringtoneVolume(\n\t\t\t\t\tData::DefaultNotifyType(alertThread->peer()));\n\t\t\t}\n\t\t\t_manager->maybePlaySound(crl::guard(&owner->session(), [=] {\n\t\t\t\tconst auto track = lookupSound(owner, id);\n\t\t\t\ttrack->playOnce(volume ? volume * 0.01 : 0);\n\t\t\t\tMedia::Player::mixer()->suppressAll(track->getLengthMs());\n\t\t\t\tMedia::Player::mixer()->scheduleFaderCallback();\n\t\t\t}));\n\t\t}\n\t}\n\n\tif (_waiters.empty()\n\t\t|| !settings.desktopNotify()\n\t\t|| _manager->skipToast()) {\n\t\tif (nextAlert) {\n\t\t\t_waitTimer.callOnce(nextAlert - ms);\n\t\t}\n\t\treturn;\n\t}\n\n\twhile (true) {\n\t\tauto next = 0LL;\n\t\tauto notify = std::optional<Data::ItemNotification>();\n\t\tauto notifyThread = (Data::Thread*)nullptr;\n\t\tfor (auto i = _waiters.begin(); i != _waiters.end();) {\n\t\t\tconst auto thread = i->first;\n\t\t\tauto current = thread->currentNotification();\n\t\t\tif (current && current->item->id != i->second.key.messageId) {\n\t\t\t\tauto j = _whenMaps.find(thread);\n\t\t\t\tif (j == _whenMaps.end()) {\n\t\t\t\t\tthread->clearNotifications();\n\t\t\t\t\ti = _waiters.erase(i);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tdo {\n\t\t\t\t\tauto k = j->second.find(*current);\n\t\t\t\t\tif (k != j->second.cend()) {\n\t\t\t\t\t\ti->second.key = k->first;\n\t\t\t\t\t\ti->second.when = k->second;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tthread->skipNotification();\n\t\t\t\t\tcurrent = thread->currentNotification();\n\t\t\t\t} while (current);\n\t\t\t}\n\t\t\tif (!current) {\n\t\t\t\t_whenMaps.remove(thread);\n\t\t\t\ti = _waiters.erase(i);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tauto when = i->second.when;\n\t\t\tif (!notify || next > when) {\n\t\t\t\tnext = when;\n\t\t\t\tnotify = current,\n\t\t\t\tnotifyThread = thread;\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t\tif (!notify) {\n\t\t\tbreak;\n\t\t} else if (next > ms) {\n\t\t\tif (nextAlert && nextAlert < next) {\n\t\t\t\tnext = nextAlert;\n\t\t\t\tnextAlert = 0;\n\t\t\t}\n\t\t\t_waitTimer.callOnce(next - ms);\n\t\t\tbreak;\n\t\t}\n\t\tconst auto notifyItem = notify->item;\n\t\tconst auto notifySilent = computeSkipState(*notify).silent;\n\t\tconst auto messageType = (notify->type\n\t\t\t== Data::ItemNotificationType::Message);\n\t\tconst auto isForwarded = messageType\n\t\t\t&& notifyItem->Has<HistoryMessageForwarded>();\n\t\tconst auto isAlbum = messageType\n\t\t\t&& notifyItem->groupId();\n\n\t\t// Forwarded and album notify grouping.\n\t\tauto groupedItem = (isForwarded || isAlbum)\n\t\t\t? notifyItem.get()\n\t\t\t: nullptr;\n\t\tauto forwardedCount = isForwarded ? 1 : 0;\n\n\t\tconst auto thread = notifyItem->notificationThread();\n\t\tconst auto j = _whenMaps.find(thread);\n\t\tif (j == _whenMaps.cend()) {\n\t\t\tthread->clearNotifications();\n\t\t} else {\n\t\t\twhile (true) {\n\t\t\t\tauto nextNotify = std::optional<Data::ItemNotification>();\n\t\t\t\tthread->skipNotification();\n\t\t\t\tif (!thread->hasNotification()) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tj->second.remove({\n\t\t\t\t\t(groupedItem ? groupedItem : notifyItem.get())->id,\n\t\t\t\t\tnotify->type,\n\t\t\t\t});\n\t\t\t\tdo {\n\t\t\t\t\tconst auto k = j->second.find(\n\t\t\t\t\t\tthread->currentNotification());\n\t\t\t\t\tif (k != j->second.cend()) {\n\t\t\t\t\t\tnextNotify = thread->currentNotification();\n\t\t\t\t\t\t_waiters.emplace(notifyThread, Waiter{\n\t\t\t\t\t\t\t.key = k->first,\n\t\t\t\t\t\t\t.when = k->second\n\t\t\t\t\t\t});\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tthread->skipNotification();\n\t\t\t\t} while (thread->hasNotification());\n\t\t\t\tif (!nextNotify || !groupedItem) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tconst auto nextMessageNotification\n\t\t\t\t\t= (nextNotify->type\n\t\t\t\t\t\t== Data::ItemNotificationType::Message);\n\t\t\t\tconst auto canNextBeGrouped = nextMessageNotification\n\t\t\t\t\t&& ((isForwarded\n\t\t\t\t\t\t&& nextNotify->item->Has<HistoryMessageForwarded>())\n\t\t\t\t\t\t|| (isAlbum && nextNotify->item->groupId()));\n\t\t\t\tconst auto nextItem = canNextBeGrouped\n\t\t\t\t\t? nextNotify->item.get()\n\t\t\t\t\t: nullptr;\n\t\t\t\tif (nextItem\n\t\t\t\t\t&& qAbs(int64(nextItem->date()) - int64(groupedItem->date())) < 2) {\n\t\t\t\t\tif (isForwarded\n\t\t\t\t\t\t&& groupedItem->author() == nextItem->author()) {\n\t\t\t\t\t\t++forwardedCount;\n\t\t\t\t\t\tgroupedItem = nextItem;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (isAlbum\n\t\t\t\t\t\t&& groupedItem->groupId() == nextItem->groupId()) {\n\t\t\t\t\t\tgroupedItem = nextItem;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (!_lastHistoryItemId && groupedItem) {\n\t\t\t_lastHistorySessionId = groupedItem->history()->session().uniqueId();\n\t\t\t_lastHistoryItemId = groupedItem->fullId();\n\t\t\t_lastSoundId = notifySilent ? std::nullopt : MaybeSoundFor(\n\t\t\t\tnotifyThread,\n\t\t\t\tgroupedItem->specialNotificationPeer());\n\t\t}\n\n\t\t// If the current notification is grouped.\n\t\tif (isAlbum || isForwarded) {\n\t\t\t// If the previous notification is grouped\n\t\t\t// then reset the timer.\n\t\t\tif (_waitForAllGroupedTimer.isActive()) {\n\t\t\t\t_waitForAllGroupedTimer.cancel();\n\t\t\t\t// If this is not the same group\n\t\t\t\t// then show the previous group immediately.\n\t\t\t\tif (!isSameGroup(groupedItem)) {\n\t\t\t\t\tshowGrouped();\n\t\t\t\t}\n\t\t\t}\n\t\t\t// We have to wait until all the messages in this group are loaded.\n\t\t\t_lastForwardedCount += forwardedCount;\n\t\t\t_lastHistorySessionId = groupedItem->history()->session().uniqueId();\n\t\t\t_lastHistoryItemId = groupedItem->fullId();\n\t\t\t_lastSoundId = notifySilent ? std::nullopt : MaybeSoundFor(\n\t\t\t\tnotifyThread,\n\t\t\t\tgroupedItem->specialNotificationPeer());\n\t\t\t_waitForAllGroupedTimer.callOnce(kWaitingForAllGroupedDelay);\n\t\t} else {\n\t\t\t// If the current notification is not grouped\n\t\t\t// then there is no reason to wait for the timer\n\t\t\t// to show the previous notification.\n\t\t\tshowGrouped();\n\t\t\tconst auto reactionNotification\n\t\t\t\t= (notify->type == Data::ItemNotificationType::Reaction);\n\t\t\tconst auto pollVoteNotification\n\t\t\t\t= (notify->type == Data::ItemNotificationType::PollVote);\n\t\t\tconst auto reaction = reactionNotification\n\t\t\t\t? notify->item->lookupUnreadReaction(notify->reactionOrVoteSender)\n\t\t\t\t: Data::ReactionId();\n\t\t\tconst auto pollVoteOption = pollVoteNotification\n\t\t\t\t? notify->item->lookupUnreadPollVote(\n\t\t\t\t\tnotify->reactionOrVoteSender)\n\t\t\t\t: QByteArray();\n\t\t\tconst auto soundFrom = (reactionNotification\n\t\t\t\t|| pollVoteNotification)\n\t\t\t\t? notify->reactionOrVoteSender\n\t\t\t\t: notify->item->specialNotificationPeer();\n\t\t\tif ((!reactionNotification || !reaction.empty())\n\t\t\t\t&& (!pollVoteNotification || !pollVoteOption.isEmpty())) {\n\t\t\t\t_manager->showNotification({\n\t\t\t\t\t.item = notify->item,\n\t\t\t\t\t.forwardedCount = forwardedCount,\n\t\t\t\t\t.reactionFrom = notify->reactionOrVoteSender,\n\t\t\t\t\t.reactionId = reaction,\n\t\t\t\t\t.pollVoteOption = pollVoteOption,\n\t\t\t\t\t.soundId = (notifySilent\n\t\t\t\t\t\t? std::nullopt\n\t\t\t\t\t\t: MaybeSoundFor(notifyThread, soundFrom)),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tif (!thread->hasNotification()) {\n\t\t\t_waiters.remove(thread);\n\t\t\t_whenMaps.remove(thread);\n\t\t}\n\t}\n\tif (nextAlert) {\n\t\t_waitTimer.callOnce(nextAlert - ms);\n\t}\n}\n\nQByteArray System::lookupSoundBytes(\n\t\tnot_null<Data::Session*> owner,\n\t\tDocumentId id) {\n\tif (id) {\n\t\tconst auto &notifySettings = owner->notifySettings();\n\t\tconst auto custom = notifySettings.lookupRingtone(id);\n\t\treturn custom ? ReadRingtoneBytes(custom) : QByteArray();\n\t}\n\tauto f = QFile(Core::App().settings().getSoundPath(u\"msg_incoming\"_q));\n\tif (f.open(QIODevice::ReadOnly)) {\n\t\treturn f.readAll();\n\t}\n\tauto fallback = QFile(u\":/sounds/msg_incoming.mp3\"_q);\n\tif (fallback.open(QIODevice::ReadOnly)) {\n\t\treturn fallback.readAll();\n\t}\n\tUnexpected(\"Embedded sound not found!\");\n}\n\nnot_null<Media::Audio::Track*> System::lookupSound(\n\t\tnot_null<Data::Session*> owner,\n\t\tDocumentId id) {\n\tif (!id) {\n\t\tensureSoundCreated();\n\t\treturn _soundTrack.get();\n\t}\n\tconst auto i = _customSoundTracks.find(id);\n\tif (i != end(_customSoundTracks)) {\n\t\treturn i->second.get();\n\t}\n\tconst auto bytes = lookupSoundBytes(owner, id);\n\tif (!bytes.isEmpty()) {\n\t\tconst auto j = _customSoundTracks.emplace(\n\t\t\tid,\n\t\t\tMedia::Audio::Current().createTrack()\n\t\t).first;\n\t\tj->second->fillFromData(bytes::make_vector(bytes));\n\t\treturn j->second.get();\n\t}\n\tensureSoundCreated();\n\treturn _soundTrack.get();\n}\n\nvoid System::ensureSoundCreated() {\n\tif (_soundTrack) {\n\t\treturn;\n\t}\n\n\t_soundTrack = Media::Audio::Current().createTrack();\n\t_soundTrack->fillFromFile(\n\t\tCore::App().settings().getSoundPath(u\"msg_incoming\"_q));\n}\n\nvoid System::updateAll() {\n\tif (_manager) {\n\t\t_manager->updateAll();\n\t}\n}\n\nrpl::producer<ChangeType> System::settingsChanged() const {\n\treturn _settingsChanged.events();\n}\n\nvoid System::notifySettingsChanged(ChangeType type) {\n\treturn _settingsChanged.fire(std::move(type));\n}\n\nbool System::volumeSupported() const {\n\t// Play through native notification system if toasts are enabled.\n\treturn Core::App().settings().soundNotify()\n\t\t&& (!Core::App().settings().desktopNotify()\n\t\t\t|| _manager->type() != ManagerType::Native\n\t\t\t|| Platform::Notifications::VolumeSupported());\n}\n\nrpl::producer<bool> System::volumeSupportedValue() const {\n\treturn rpl::single(\n\t\trpl::empty_value()\n\t) | rpl::then(\n\t\trpl::merge(settingsChanged() | rpl::to_empty, managerChanged())\n\t) | rpl::map([=] {\n\t\treturn volumeSupported();\n\t}) | rpl::distinct_until_changed();\n}\n\nvoid System::playSound(\n\t\tnot_null<Main::Session*> session,\n\t\tDocumentId id,\n\t\tfloat64 volumeOverride) {\n\tlookupSound(&session->data(), id)->playOnce(volumeOverride);\n}\n\nManager::DisplayOptions Manager::getNotificationOptions(\n\t\tHistoryItem *item,\n\t\tData::ItemNotificationType type) const {\n\tconst auto hideEverything = Core::App().passcodeLocked()\n\t\t|| forceHideDetails();\n\tconst auto view = Core::App().settings().notifyView();\n\tconst auto peer = item ? item->history()->peer.get() : nullptr;\n\tconst auto topic = item ? item->topic() : nullptr;\n\n\tauto result = DisplayOptions();\n\tresult.hideNameAndPhoto = hideEverything\n\t\t|| (view > Core::Settings::NotifyView::ShowName);\n\tresult.hideMessageText = hideEverything\n\t\t|| (view > Core::Settings::NotifyView::ShowPreview);\n\tresult.hideMarkAsRead = result.hideMessageText\n\t\t|| (type != Data::ItemNotificationType::Message)\n\t\t|| !item\n\t\t|| ((item->out() || peer->isSelf()) && item->isFromScheduled());\n\tresult.hideReplyButton = result.hideMarkAsRead\n\t\t|| (!Data::CanSendTexts(peer)\n\t\t\t&& (!topic || !Data::CanSendTexts(topic)))\n\t\t|| peer->isBroadcast()\n\t\t|| (peer->slowmodeSecondsLeft() > 0)\n\t\t|| (peer->starsPerMessageChecked() > 0)\n\t\t|| HideReplyButtonOption.value();\n\tresult.spoilerLoginCode = item\n\t\t&& !item->out()\n\t\t&& (peer->isNotificationsUser()\n\t\t\t|| peer->isVerifyCodes());\n\treturn result;\n}\n\nTextWithEntities Manager::ComposeReactionEmoji(\n\t\tnot_null<Main::Session*> session,\n\t\tconst Data::ReactionId &reaction) {\n\tif (const auto emoji = std::get_if<QString>(&reaction.data)) {\n\t\treturn TextWithEntities{ *emoji };\n\t}\n\tconst auto id = v::get<DocumentId>(reaction.data);\n\tconst auto document = session->data().document(id);\n\tconst auto sticker = document->sticker();\n\tconst auto text = sticker ? sticker->alt : PlaceholderReactionText();\n\treturn TextWithEntities{\n\t\ttext,\n\t\t{\n\t\t\tEntityInText(\n\t\t\t\tEntityType::CustomEmoji,\n\t\t\t\t0,\n\t\t\t\ttext.size(),\n\t\t\t\tData::SerializeCustomEmojiId(id))\n\t\t}\n\t};\n}\n\nTextWithEntities Manager::ComposeReactionNotification(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst Data::ReactionId &reaction,\n\t\tbool hideContent) {\n\tconst auto reactionWithEntities = ComposeReactionEmoji(\n\t\t&item->history()->session(),\n\t\treaction);\n\tconst auto simple = [&](const auto &phrase) {\n\t\treturn phrase(\n\t\t\ttr::now,\n\t\t\tlt_reaction,\n\t\t\treactionWithEntities,\n\t\t\ttr::marked);\n\t};\n\tif (hideContent) {\n\t\treturn simple(tr::lng_reaction_notext);\n\t}\n\tconst auto media = item->media();\n\tconst auto text = [&] {\n\t\treturn tr::lng_reaction_text(\n\t\t\ttr::now,\n\t\t\tlt_reaction,\n\t\t\treactionWithEntities,\n\t\t\tlt_text,\n\t\t\titem->notificationText(),\n\t\t\ttr::marked);\n\t};\n\tif (!media || media->webpage()) {\n\t\treturn text();\n\t} else if (media->photo()) {\n\t\treturn simple(tr::lng_reaction_photo);\n\t} else if (const auto document = media->document()) {\n\t\tif (document->isVoiceMessage()) {\n\t\t\treturn simple(tr::lng_reaction_voice_message);\n\t\t} else if (document->isVideoMessage()) {\n\t\t\treturn simple(tr::lng_reaction_video_message);\n\t\t} else if (document->isAnimation()) {\n\t\t\treturn simple(tr::lng_reaction_gif);\n\t\t} else if (document->isVideoFile()) {\n\t\t\treturn simple(tr::lng_reaction_video);\n\t\t} else if (const auto sticker = document->sticker()) {\n\t\t\treturn tr::lng_reaction_sticker(\n\t\t\t\ttr::now,\n\t\t\t\tlt_reaction,\n\t\t\t\treactionWithEntities,\n\t\t\t\tlt_emoji,\n\t\t\t\ttr::marked(sticker->alt),\n\t\t\t\ttr::marked);\n\t\t}\n\t\treturn simple(tr::lng_reaction_document);\n\t} else if (const auto contact = media->sharedContact()) {\n\t\tconst auto name = contact->firstName.isEmpty()\n\t\t\t? contact->lastName\n\t\t\t: contact->lastName.isEmpty()\n\t\t\t? contact->firstName\n\t\t\t: tr::lng_full_name(\n\t\t\t\ttr::now,\n\t\t\t\tlt_first_name,\n\t\t\t\tcontact->firstName,\n\t\t\t\tlt_last_name,\n\t\t\t\tcontact->lastName);\n\t\treturn tr::lng_reaction_contact(\n\t\t\ttr::now,\n\t\t\tlt_reaction,\n\t\t\treactionWithEntities,\n\t\t\tlt_name,\n\t\t\ttr::marked(name),\n\t\t\ttr::marked);\n\t} else if (media->location()) {\n\t\treturn simple(tr::lng_reaction_location);\n\t\t// lng_reaction_live_location not used right now :(\n\t} else if (const auto poll = media->poll()) {\n\t\treturn (poll->quiz()\n\t\t\t? tr::lng_reaction_quiz\n\t\t\t: tr::lng_reaction_poll)(\n\t\t\t\ttr::now,\n\t\t\t\tlt_reaction,\n\t\t\t\treactionWithEntities,\n\t\t\t\tlt_title,\n\t\t\t\tpoll->question,\n\t\t\t\ttr::marked);\n\t} else if (media->game()) {\n\t\treturn simple(tr::lng_reaction_game);\n\t} else if (media->invoice()) {\n\t\treturn simple(tr::lng_reaction_invoice);\n\t}\n\treturn text();\n}\n\nTextWithEntities Manager::ComposePollVoteNotification(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst QByteArray &option,\n\t\tbool hideContent) {\n\tif (hideContent) {\n\t\treturn tr::lng_poll_vote_notext(tr::now, tr::marked);\n\t}\n\tconst auto media = item->media();\n\tconst auto poll = media ? media->poll() : nullptr;\n\tif (!poll) {\n\t\treturn tr::lng_poll_vote_notext(tr::now, tr::marked);\n\t}\n\tif (const auto answer = poll->answerByOption(option)) {\n\t\treturn tr::lng_poll_vote_option(\n\t\t\ttr::now,\n\t\t\tlt_option,\n\t\t\tanswer->text,\n\t\t\ttr::marked);\n\t}\n\treturn tr::lng_poll_vote(\n\t\ttr::now,\n\t\tlt_title,\n\t\tpoll->question,\n\t\ttr::marked);\n}\n\nTextWithEntities Manager::addTargetAccountName(\n\t\tTextWithEntities title,\n\t\tnot_null<Main::Session*> session) {\n\tconst auto add = [&] {\n\t\tfor (const auto &[index, account] : Core::App().domain().accounts()) {\n\t\t\tif (const auto other = account->maybeSession()) {\n\t\t\t\tif (other != session) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}();\n\tif (!add) {\n\t\treturn title;\n\t}\n\treturn title.append(accountNameSeparator()).append(\n\t\t(session->user()->username().isEmpty()\n\t\t\t? session->user()->name()\n\t\t\t: session->user()->username()));\n}\n\nQString Manager::addTargetAccountName(\n\t\tconst QString &title,\n\t\tnot_null<Main::Session*> session) {\n\treturn addTargetAccountName(TextWithEntities{ title }, session).text;\n}\n\nQString Manager::accountNameSeparator() {\n\treturn QString::fromUtf8(\" \\xE2\\x9E\\x9C \");\n}\n\nvoid Manager::notificationActivated(\n\t\tNotificationId id,\n\t\tActivateOptions &&options) {\n\tonBeforeNotificationActivated(id);\n\tif (const auto session = system()->findSession(id.contextId.sessionId)) {\n\t\tconst auto history = session->data().history(\n\t\t\tid.contextId.peerId);\n\t\tconst auto item = history->owner().message(\n\t\t\thistory->peer,\n\t\t\tid.msgId);\n\t\tconst auto topic = item ? item->topic() : nullptr;\n\t\tconst auto sublist = item ? item->savedSublist() : nullptr;\n\t\tif (!options.draft.text.isEmpty()) {\n\t\t\tconst auto topicRootId = topic\n\t\t\t\t? topic->rootId()\n\t\t\t\t: id.contextId.topicRootId;\n\t\t\tconst auto monoforumPeerId = (sublist && sublist->parentChat())\n\t\t\t\t? sublist->sublistPeer()->id\n\t\t\t\t: id.contextId.monoforumPeerId;\n\t\t\tconst auto replyToId = (id.msgId > 0\n\t\t\t\t&& !history->peer->isUser()\n\t\t\t\t&& id.msgId != topicRootId)\n\t\t\t\t? FullMsgId(history->peer->id, id.msgId)\n\t\t\t\t: FullMsgId();\n\t\t\tconst auto length = int(options.draft.text.size());\n\t\t\tauto draft = std::make_unique<Data::Draft>(\n\t\t\t\tstd::move(options.draft),\n\t\t\t\tFullReplyTo{\n\t\t\t\t\t.messageId = replyToId,\n\t\t\t\t\t.topicRootId = topicRootId,\n\t\t\t\t\t.monoforumPeerId = monoforumPeerId,\n\t\t\t\t},\n\t\t\t\tSuggestOptions(),\n\t\t\t\tMessageCursor{\n\t\t\t\t\tlength,\n\t\t\t\t\tlength,\n\t\t\t\t\tUi::kQFixedMax,\n\t\t\t\t},\n\t\t\t\tData::WebPageDraft());\n\t\t\thistory->setLocalDraft(std::move(draft));\n\t\t}\n\t\tconst auto openSeparated = options.allowNewWindow\n\t\t\t&& base::IsCtrlPressed();\n\t\tconst auto window = openNotificationMessage(\n\t\t\thistory,\n\t\t\tid.msgId,\n\t\t\topenSeparated);\n\t\tonAfterNotificationActivated(id, window);\n\t}\n}\n\nWindow::SessionController *Manager::openNotificationMessage(\n\t\tnot_null<History*> history,\n\t\tMsgId messageId,\n\t\tbool openSeparated) {\n\tif (Core::App().passcodeLocked()) {\n\t\tconst auto window = history->session().tryResolveWindow();\n\t\tif (window) {\n\t\t\twindow->widget()->showFromTray();\n\t\t\twindow->widget()->setInnerFocus();\n\t\t\tsystem()->clearAll();\n\t\t}\n\t\treturn window;\n\t}\n\tconst auto item = history->owner().message(history->peer, messageId);\n\tconst auto openExactlyMessage = !history->peer->isBroadcast()\n\t\t&& item\n\t\t&& item->isRegular()\n\t\t&& (item->out() || (item->mentionsMe() && !history->peer->isUser()));\n\tconst auto topic = item ? item->topic() : nullptr;\n\tconst auto sublist = item ? item->savedSublist() : nullptr;\n\n\tconst auto guard = gsl::finally([&] {\n\t\tif (topic) {\n\t\t\tsystem()->clearFromTopic(topic);\n\t\t} else if (sublist && sublist->parentChat()) {\n\t\t\tsystem()->clearFromSublist(sublist);\n\t\t} else {\n\t\t\tsystem()->clearFromHistory(history);\n\t\t}\n\t});\n\n\tconst auto separateId = !topic\n\t\t? Window::SeparateId(history->peer)\n\t\t: history->peer->useSubsectionTabs()\n\t\t? Window::SeparateId(Window::SeparateType::Chat, topic)\n\t\t: Window::SeparateId(Window::SeparateType::Forum, history);\n\tconst auto separate = Core::App().separateWindowFor(separateId);\n\tconst auto itemId = openExactlyMessage ? messageId : ShowAtUnreadMsgId;\n\tif (openSeparated && !separate && !topic) {\n\t\t// In case we're opening a chat history we first try to open it like\n\t\t// it is done from the main window context menu (that checks if the\n\t\t// chat isn't restricted and also closes the chat we're opening\n\t\t// in the window itself). If this couldn't be done, we open normally.\n\t\tconst auto tryInExisting = [&](bool primary) {\n\t\t\tfor (const auto &window : history->session().windows()) {\n\t\t\t\tif (primary && !window->window().id().primary()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (!primary && !window->window().id().folder()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\twindow->showInNewWindow(separateId, itemId);\n\t\t\t\tconst auto shown = Core::App().separateWindowFor(\n\t\t\t\t\tseparateId);\n\t\t\t\treturn shown ? shown->sessionController() : window.get();\n\t\t\t}\n\t\t\treturn (Window::SessionController*)nullptr;\n\t\t};\n\t\tconst auto shownPrimary = tryInExisting(true);\n\t\tconst auto shown = shownPrimary\n\t\t\t? shownPrimary\n\t\t\t: tryInExisting(false);\n\t\tif (shown) {\n\t\t\treturn shown;\n\t\t}\n\t}\n\tconst auto window = separate\n\t\t? separate->sessionController()\n\t\t: openSeparated\n\t\t? [&] {\n\t\t\tconst auto window = Core::App().ensureSeparateWindowFor(\n\t\t\t\tseparateId,\n\t\t\t\titemId);\n\t\t\treturn window ? window->sessionController() : nullptr;\n\t\t}()\n\t\t: history->session().tryResolveWindow();\n\tif (window) {\n\t\twindow->widget()->showFromTray();\n\t\tif (topic) {\n\t\t\twindow->showTopic(topic, itemId, SectionShow::Way::Forward);\n\t\t} else if (sublist) {\n\t\t\twindow->showSublist(sublist, itemId, SectionShow::Way::Forward);\n\t\t} else {\n\t\t\twindow->showPeerHistory(\n\t\t\t\thistory->peer->id,\n\t\t\t\tSectionShow::Way::Forward,\n\t\t\t\titemId);\n\t\t}\n\t}\n\treturn window;\n}\n\nvoid Manager::notificationReplied(\n\t\tNotificationId id,\n\t\tconst TextWithTags &reply) {\n\tif (!id.contextId.sessionId || !id.contextId.peerId) {\n\t\treturn;\n\t}\n\n\tconst auto session = system()->findSession(id.contextId.sessionId);\n\tif (!session) {\n\t\treturn;\n\t}\n\tconst auto history = session->data().history(id.contextId.peerId);\n\tconst auto item = history->owner().message(history->peer, id.msgId);\n\tconst auto topic = item ? item->topic() : nullptr;\n\tconst auto topicRootId = topic\n\t\t? topic->rootId()\n\t\t: id.contextId.topicRootId;\n\tconst auto sublist = item ? item->savedSublist() : nullptr;\n\tconst auto monoforumPeerId = (sublist && sublist->parentChat())\n\t\t? sublist->sublistPeer()->id\n\t\t: id.contextId.monoforumPeerId;\n\n\tauto message = Api::MessageToSend(Api::SendAction(history));\n\tmessage.textWithTags = reply;\n\tconst auto replyToId = (id.msgId > 0 && !history->peer->isUser()\n\t\t&& id.msgId != topicRootId)\n\t\t? id.msgId\n\t\t: history->peer->isForum()\n\t\t? topicRootId\n\t\t: MsgId(0);\n\tmessage.action.replyTo = {\n\t\t.messageId = { replyToId ? history->peer->id : 0, replyToId },\n\t\t.topicRootId = topic ? topic->rootId() : 0,\n\t\t.monoforumPeerId = monoforumPeerId,\n\t};\n\tmessage.action.clearDraft = false;\n\thistory->session().api().sendMessage(std::move(message));\n\n\tif (item && item->isUnreadMention() && !item->isIncomingUnreadMedia()) {\n\t\thistory->session().api().markContentsRead(item);\n\t}\n}\n\nvoid Manager::notificationActionActivated(\n\t\tNotificationId id,\n\t\tconst QString &actionId) {\n\tif (actionId == u\"markAsRead\"_q) {\n\t\tnotificationReplied(id, {});\n\t\treturn;\n\t}\n\tif (!actionId.startsWith(u\"callbackButton:\"_q)) {\n\t\treturn;\n\t}\n\tconst auto payload = QStringView(actionId).mid(15);\n\tconst auto sep = payload.indexOf(':');\n\tif (sep < 0) {\n\t\treturn;\n\t}\n\tconst auto row = payload.left(sep).toInt();\n\tconst auto column = payload.mid(sep + 1).toInt();\n\tif (!id.contextId.sessionId || !id.contextId.peerId) {\n\t\treturn;\n\t}\n\tconst auto session = system()->findSession(id.contextId.sessionId);\n\tif (!session) {\n\t\treturn;\n\t}\n\tconst auto history = session->data().history(id.contextId.peerId);\n\tconst auto item = history->owner().message(history->peer, id.msgId);\n\tif (!item || !item->isRegular()) {\n\t\treturn;\n\t}\n\tconst auto owner = &history->owner();\n\tconst auto fullId = item->fullId();\n\tconst auto button = HistoryMessageMarkupButton::Get(\n\t\towner,\n\t\tfullId,\n\t\trow,\n\t\tcolumn);\n\tif (!button || button->requestId) {\n\t\treturn;\n\t}\n\tusing ButtonType = HistoryMessageMarkupButton::Type;\n\tif (button->type != ButtonType::Callback) {\n\t\treturn;\n\t}\n\tauto flags = MTPmessages_GetBotCallbackAnswer::Flags(0);\n\tflags |= MTPmessages_GetBotCallbackAnswer::Flag::f_data;\n\tconst auto sendData = button->data;\n\tbutton->requestId = session->api().request(\n\t\tMTPmessages_GetBotCallbackAnswer(\n\t\t\tMTP_flags(flags),\n\t\t\thistory->peer->input(),\n\t\t\tMTP_int(item->id),\n\t\t\tMTP_bytes(sendData),\n\t\t\tMTP_inputCheckPasswordEmpty())\n\t).done([=](const MTPmessages_BotCallbackAnswer &result) {\n\t\tconst auto item = owner->message(fullId);\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\t\tif (const auto button = HistoryMessageMarkupButton::Get(\n\t\t\t\towner,\n\t\t\t\tfullId,\n\t\t\t\trow,\n\t\t\t\tcolumn)) {\n\t\t\tbutton->requestId = 0;\n\t\t\towner->requestItemRepaint(item);\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto item = owner->message(fullId);\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\t\tif (const auto button = HistoryMessageMarkupButton::Get(\n\t\t\t\towner,\n\t\t\t\tfullId,\n\t\t\t\trow,\n\t\t\t\tcolumn)) {\n\t\t\tbutton->requestId = 0;\n\t\t}\n\t}).send();\n}\n\nvoid Manager::maybePlaySound(Fn<void()> playSound) {\n\tif (_system->volumeSupported()) {\n\t\tdoMaybePlaySound(std::move(playSound));\n\t}\n}\n\nvoid NativeManager::doShowNotification(NotificationFields &&fields) {\n\tconst auto pollVote = !fields.pollVoteOption.isEmpty();\n\tconst auto type = fields.reactionFrom\n\t\t? (pollVote\n\t\t\t? Data::ItemNotificationType::PollVote\n\t\t\t: Data::ItemNotificationType::Reaction)\n\t\t: Data::ItemNotificationType::Message;\n\tconst auto options = getNotificationOptions(fields.item, type);\n\tconst auto item = fields.item;\n\tconst auto peer = item->history()->peer;\n\tconst auto reactionFrom = fields.reactionFrom;\n\tif (reactionFrom && options.hideNameAndPhoto) {\n\t\treturn;\n\t}\n\tconst auto scheduled = !options.hideNameAndPhoto\n\t\t&& !reactionFrom\n\t\t&& (item->out() || peer->isSelf())\n\t\t&& item->isFromScheduled();\n\tconst auto subWithChat = [&] {\n\t\tconst auto name = peer->name();\n\t\tconst auto topic = item->topic();\n\t\tconst auto sublist = item->savedSublist();\n\t\treturn topic\n\t\t\t? (topic->title() + u\" (\"_q + name + ')')\n\t\t\t: (sublist && sublist->parentChat())\n\t\t\t? (sublist->sublistPeer()->shortName() + u\" (\"_q + name + ')')\n\t\t\t: name;\n\t};\n\tconst auto title = options.hideNameAndPhoto\n\t\t? AppNameF.utf16()\n\t\t: (scheduled && peer->isSelf())\n\t\t? tr::lng_notification_reminder(tr::now)\n\t\t: subWithChat();\n\tconst auto fullTitle = addTargetAccountName(title, &peer->session());\n\tconst auto hideReactionSender = reactionFrom\n\t\t&& !peer->session().api().reactionsNotifySettings()\n\t\t\t.showPreviewsCurrent();\n\tconst auto subtitle = reactionFrom\n\t\t? ((!hideReactionSender && reactionFrom != peer)\n\t\t\t? reactionFrom->name()\n\t\t\t: QString())\n\t\t: options.hideNameAndPhoto\n\t\t? QString()\n\t\t: item->notificationHeader();\n\tconst auto text = pollVote\n\t\t? TextWithPermanentSpoiler(ComposePollVoteNotification(\n\t\t\titem,\n\t\t\tfields.pollVoteOption,\n\t\t\toptions.hideMessageText))\n\t\t: reactionFrom\n\t\t? TextWithPermanentSpoiler(ComposeReactionNotification(\n\t\t\titem,\n\t\t\tfields.reactionId,\n\t\t\toptions.hideMessageText))\n\t\t: options.hideMessageText\n\t\t? tr::lng_notification_preview(tr::now)\n\t\t: (fields.forwardedCount > 1)\n\t\t? tr::lng_forward_messages(tr::now, lt_count, fields.forwardedCount)\n\t\t: item->groupId()\n\t\t? tr::lng_in_dlg_album(tr::now)\n\t\t: TextWithForwardedChar(\n\t\t\tTextWithPermanentSpoiler(item->notificationText({\n\t\t\t\t.spoilerLoginCode = options.spoilerLoginCode,\n\t\t\t})),\n\t\t\t(fields.forwardedCount == 1));\n\n\t// #TODO optimize\n\tauto userpicView = item->history()->peer->createUserpicView();\n\tconst auto owner = &item->history()->owner();\n\tconst auto withSound = fields.soundId\n\t\t&& Core::App().settings().soundNotify();\n\tconst auto sound = withSound ? [=, id = *fields.soundId] {\n\t\treturn _localSoundCache.sound(id, [=] {\n\t\t\treturn Core::App().notifications().lookupSoundBytes(owner, id);\n\t\t}, [=] {\n\t\t\treturn Core::App().notifications().lookupSoundBytes(owner, 0);\n\t\t});\n\t} : Fn<NotificationSound()>();\n\tauto actions = std::vector<NotificationAction>();\n\tif (AllowNotificationActions(peer)) {\n\t\tif (const auto markup = item->inlineReplyMarkup()) {\n\t\t\tusing ButtonType = HistoryMessageMarkupButton::Type;\n\t\t\tconst auto &rows = markup->data.rows;\n\t\t\tconst auto rowsCount = int(rows.size());\n\t\t\tfor (auto row = 0; row != rowsCount; ++row) {\n\t\t\t\tconst auto &buttons = rows[row];\n\t\t\t\tconst auto colsCount = int(buttons.size());\n\t\t\t\tfor (auto col = 0; col != colsCount; ++col) {\n\t\t\t\t\tconst auto &button = buttons[col];\n\t\t\t\t\tif (button.type == ButtonType::Callback) {\n\t\t\t\t\t\tactions.push_back({\n\t\t\t\t\t\t\t.id = u\"callbackButton:%1:%2\"_q.arg(row).arg(col),\n\t\t\t\t\t\t\t.text = button.text,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tdoShowNativeNotification({\n\t\t.peer = item->history()->peer,\n\t\t.topicRootId = item->topicRootId(),\n\t\t.monoforumPeerId = (item->history()->amMonoforumAdmin()\n\t\t\t? item->sublistPeerId()\n\t\t\t: PeerId()),\n\t\t.itemId = item->id,\n\t\t.title = scheduled ? WrapFromScheduled(fullTitle) : fullTitle,\n\t\t.subtitle = subtitle,\n\t\t.message = text,\n\t\t.sound = sound,\n\t\t.options = options,\n\t\t.actions = std::move(actions),\n\t}, userpicView);\n}\n\nbool NativeManager::forceHideDetails() const {\n\treturn Core::App().screenIsLocked();\n}\n\nSystem::~System() = default;\n\nQString WrapFromScheduled(const QString &text) {\n\treturn QString::fromUtf8(\"\\xF0\\x9F\\x93\\x85 \") + text;\n}\n\nQRect NotificationDisplayRect(Window::Controller *controller) {\n\tconst auto displayChecksum\n\t\t= Core::App().settings().notificationsDisplayChecksum();\n\n\tauto screen = (QScreen*)(nullptr);\n\tif (displayChecksum) {\n\t\tusing namespace Platform;\n\t\tfor (const auto candidateScreen : QGuiApplication::screens()) {\n\t\t\tif (ScreenNameChecksum(candidateScreen) == displayChecksum) {\n\t\t\t\tscreen = candidateScreen;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn screen\n\t\t? screen->availableGeometry()\n\t\t: controller\n\t\t? controller->widget()->desktopRect()\n\t\t: QGuiApplication::primaryScreen()->availableGeometry();\n}\n\n} // namespace Notifications\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/notifications_manager.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_message_reaction_id.h\"\n#include \"base/timer.h\"\n#include \"base/type_traits.h\"\n#include \"media/audio/media_audio_local_cache.h\"\n\nclass History;\n\nnamespace Data {\nclass Session;\nclass ForumTopic;\nclass SavedSublist;\nclass Thread;\nstruct ItemNotification;\nenum class ItemNotificationType;\n} // namespace Data\n\nnamespace Ui {\nstruct PeerUserpicView;\n} // namespace Ui\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Platform {\nnamespace Notifications {\nclass Manager;\n} // namespace Notifications\n} // namespace Platform\n\nnamespace Media::Audio {\nclass Track;\n} // namespace Media::Audio\n\nnamespace Window {\nclass Controller;\nclass SessionController;\n} // namespace Window\n\nnamespace Window::Notifications {\n\nenum class ManagerType {\n\tDummy,\n\tDefault,\n\tNative,\n};\n\nenum class ChangeType {\n\tSoundEnabled,\n\tFlashBounceEnabled,\n\tIncludeMuted,\n\tCountMessages,\n\tDesktopEnabled,\n\tViewParams,\n\tMaxCount,\n\tCorner,\n\tDemoIsShown,\n\tDemoIsHidden,\n};\n\n} // namespace Window::Notifications\n\nnamespace base {\n\ntemplate <>\nstruct custom_is_fast_copy_type<Window::Notifications::ChangeType> : std::true_type {\n};\n\n} // namespace base\n\nnamespace base::options {\n\ntemplate <typename Type>\nclass option;\n\nusing toggle = option<bool>;\n\n} // namespace base::options\n\nnamespace Window::Notifications {\n\nextern const char kOptionCustomNotification[];\nextern const char kOptionGNotification[];\nextern base::options::toggle OptionGNotification;\n\nextern const char kOptionHideReplyButton[];\n\nclass Manager;\n\nstruct ActivateOptions {\n\tTextWithTags draft;\n\tbool allowNewWindow = false;\n};\n\nclass System final {\npublic:\n\tSystem();\n\t~System();\n\n\t[[nodiscard]] Main::Session *findSession(uint64 sessionId) const;\n\n\tvoid createManager();\n\tvoid setManager(Fn<std::unique_ptr<Manager>()> create);\n\t[[nodiscard]] Manager &manager() const;\n\t[[nodiscard]] rpl::producer<> managerChanged() const;\n\t[[nodiscard]] bool nativeEnforced() const;\n\n\tvoid checkDelayed();\n\tvoid schedule(Data::ItemNotification notification);\n\tvoid clearFromTopic(not_null<Data::ForumTopic*> topic);\n\tvoid clearFromSublist(not_null<Data::SavedSublist*> sublist);\n\tvoid clearFromHistory(not_null<History*> history);\n\tvoid clearIncomingFromTopic(not_null<Data::ForumTopic*> topic);\n\tvoid clearIncomingFromSublist(not_null<Data::SavedSublist*> sublist);\n\tvoid clearIncomingFromHistory(not_null<History*> history);\n\tvoid clearFromSession(not_null<Main::Session*> session);\n\tvoid clearFromItem(not_null<HistoryItem*> item);\n\tvoid clearAll();\n\tvoid clearAllFast();\n\tvoid updateAll();\n\n\t[[nodiscard]] rpl::producer<ChangeType> settingsChanged() const;\n\tvoid notifySettingsChanged(ChangeType type);\n\n\t[[nodiscard]] bool volumeSupported() const;\n\t[[nodiscard]] rpl::producer<bool> volumeSupportedValue() const;\n\n\tvoid playSound(\n\t\tnot_null<Main::Session*> session,\n\t\tDocumentId id,\n\t\tfloat64 volumeOverride = -1);\n\t[[nodiscard]] QByteArray lookupSoundBytes(\n\t\tnot_null<Data::Session*> owner,\n\t\tDocumentId id);\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tstruct Waiter;\n\n\tstruct SkipState {\n\t\tenum Value {\n\t\t\tUnknown,\n\t\t\tSkip,\n\t\t\tDontSkip\n\t\t};\n\t\tValue value = Value::Unknown;\n\t\tbool silent = false;\n\t};\n\tstruct NotificationInHistoryKey {\n\t\tNotificationInHistoryKey(Data::ItemNotification notification);\n\t\tNotificationInHistoryKey(\n\t\t\tMsgId messageId,\n\t\t\tData::ItemNotificationType type);\n\n\t\tMsgId messageId = 0;\n\t\tData::ItemNotificationType type = Data::ItemNotificationType();\n\n\t\tfriend inline auto operator<=>(\n\t\t\tNotificationInHistoryKey a,\n\t\t\tNotificationInHistoryKey b) = default;\n\t};\n\tstruct Timing {\n\t\tcrl::time delay = 0;\n\t\tcrl::time when = 0;\n\t};\n\tstruct SentNotificationId {\n\t\tFullMsgId itemId;\n\t\tuint64 sessionId = 0;\n\n\t\tfriend inline bool operator<(\n\t\t\t\tSentNotificationId a,\n\t\t\t\tSentNotificationId b) {\n\t\t\treturn std::pair(a.itemId, a.sessionId)\n\t\t\t\t< std::pair(b.itemId, b.sessionId);\n\t\t}\n\t};\n\n\tvoid clearForThreadIf(Fn<bool(not_null<Data::Thread*>)> predicate);\n\n\t[[nodiscard]] SkipState skipNotification(\n\t\tData::ItemNotification notification) const;\n\t[[nodiscard]] SkipState computeSkipState(\n\t\tData::ItemNotification notification) const;\n\t[[nodiscard]] Timing countTiming(\n\t\tnot_null<Data::Thread*> thread,\n\t\tcrl::time minimalDelay) const;\n\t[[nodiscard]] bool skipSentNotification(\n\t\tnot_null<HistoryItem*> item,\n\t\tbase::flat_map<SentNotificationId, crl::time> &already) const;\n\n\tvoid showNext();\n\tvoid showGrouped();\n\tvoid ensureSoundCreated();\n\t[[nodiscard]] not_null<Media::Audio::Track*> lookupSound(\n\t\tnot_null<Data::Session*> owner,\n\t\tDocumentId id);\n\n\tvoid registerThread(not_null<Data::Thread*> thread);\n\n\tbase::flat_map<\n\t\tnot_null<Data::Thread*>,\n\t\tbase::flat_map<NotificationInHistoryKey, crl::time>> _whenMaps;\n\n\tbase::flat_map<not_null<Data::Thread*>, Waiter> _waiters;\n\tbase::flat_map<not_null<Data::Thread*>, Waiter> _settingWaiters;\n\tbase::Timer _waitTimer;\n\tbase::Timer _waitForAllGroupedTimer;\n\n\tbase::flat_map<\n\t\tnot_null<Data::Thread*>,\n\t\tbase::flat_map<crl::time, PeerData*>> _whenAlerts;\n\n\tmutable base::flat_map<\n\t\tSentNotificationId,\n\t\tcrl::time> _sentReactionNotifications;\n\tmutable base::flat_map<\n\t\tSentNotificationId,\n\t\tcrl::time> _sentPollVoteNotifications;\n\n\tstd::unique_ptr<Manager> _manager;\n\trpl::event_stream<> _managerChanged;\n\n\trpl::event_stream<ChangeType> _settingsChanged;\n\n\tstd::unique_ptr<Media::Audio::Track> _soundTrack;\n\tbase::flat_map<\n\t\tDocumentId,\n\t\tstd::unique_ptr<Media::Audio::Track>> _customSoundTracks;\n\n\tbase::flat_map<\n\t\tnot_null<Data::ForumTopic*>,\n\t\trpl::lifetime> _watchedTopics;\n\tbase::flat_map<\n\t\tnot_null<Data::SavedSublist*>,\n\t\trpl::lifetime> _watchedSublists;\n\n\tint _lastForwardedCount = 0;\n\tuint64 _lastHistorySessionId = 0;\n\tFullMsgId _lastHistoryItemId;\n\tstd::optional<DocumentId> _lastSoundId;\n\n\trpl::lifetime _lifetime;\n\n};\n\nclass Manager {\npublic:\n\tstruct ContextId {\n\t\tuint64 sessionId = 0;\n\t\tPeerId peerId = 0;\n\t\tMsgId topicRootId = 0;\n\t\tPeerId monoforumPeerId = 0;\n\n\t\tfriend inline auto operator<=>(\n\t\t\tconst ContextId&,\n\t\t\tconst ContextId&) = default;\n\t};\n\tstruct NotificationId {\n\t\tContextId contextId;\n\t\tMsgId msgId = 0;\n\n\t\tfriend inline auto operator<=>(\n\t\t\tconst NotificationId&,\n\t\t\tconst NotificationId&) = default;\n\t};\n\tstruct NotificationFields {\n\t\tnot_null<HistoryItem*> item;\n\t\tint forwardedCount = 0;\n\t\tPeerData *reactionFrom = nullptr;\n\t\tData::ReactionId reactionId;\n\t\tQByteArray pollVoteOption;\n\t\tstd::optional<DocumentId> soundId;\n\t};\n\n\texplicit Manager(not_null<System*> system) : _system(system) {\n\t}\n\n\tvoid showNotification(NotificationFields fields) {\n\t\tdoShowNotification(std::move(fields));\n\t}\n\tvoid updateAll() {\n\t\tdoUpdateAll();\n\t}\n\tvoid clearAll() {\n\t\tdoClearAll();\n\t}\n\tvoid clearAllFast() {\n\t\tdoClearAllFast();\n\t}\n\tvoid clearFromItem(not_null<HistoryItem*> item) {\n\t\tdoClearFromItem(item);\n\t}\n\tvoid clearFromTopic(not_null<Data::ForumTopic*> topic) {\n\t\tdoClearFromTopic(topic);\n\t}\n\tvoid clearFromSublist(not_null<Data::SavedSublist*> sublist) {\n\t\tdoClearFromSublist(sublist);\n\t}\n\tvoid clearFromHistory(not_null<History*> history) {\n\t\tdoClearFromHistory(history);\n\t}\n\tvoid clearFromSession(not_null<Main::Session*> session) {\n\t\tdoClearFromSession(session);\n\t}\n\n\tvoid notificationActivated(\n\t\tNotificationId id,\n\t\tActivateOptions &&options = {});\n\tvoid notificationReplied(NotificationId id, const TextWithTags &reply);\n\tvoid notificationActionActivated(\n\t\tNotificationId id,\n\t\tconst QString &actionId);\n\n\tstruct DisplayOptions {\n\t\tbool hideNameAndPhoto : 1 = false;\n\t\tbool hideMessageText : 1 = false;\n\t\tbool hideMarkAsRead : 1 = false;\n\t\tbool hideReplyButton : 1 = false;\n\t\tbool spoilerLoginCode : 1 = false;\n\t};\n\t[[nodiscard]] DisplayOptions getNotificationOptions(\n\t\tHistoryItem *item,\n\t\tData::ItemNotificationType type) const;\n\t[[nodiscard]] static TextWithEntities ComposeReactionEmoji(\n\t\tnot_null<Main::Session*> session,\n\t\tconst Data::ReactionId &reaction);\n\t[[nodiscard]] static TextWithEntities ComposeReactionNotification(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst Data::ReactionId &reaction,\n\t\tbool hideContent);\n\t[[nodiscard]] static TextWithEntities ComposePollVoteNotification(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst QByteArray &option,\n\t\tbool hideContent);\n\n\t[[nodiscard]] TextWithEntities addTargetAccountName(\n\t\tTextWithEntities title,\n\t\tnot_null<Main::Session*> session);\n\t[[nodiscard]] QString addTargetAccountName(\n\t\tconst QString &title,\n\t\tnot_null<Main::Session*> session);\n\n\t[[nodiscard]] virtual ManagerType type() const = 0;\n\n\t[[nodiscard]] bool skipToast() const {\n\t\treturn doSkipToast();\n\t}\n\tvoid maybePlaySound(Fn<void()> playSound);\n\tvoid maybeFlashBounce(Fn<void()> flashBounce) {\n\t\tdoMaybeFlashBounce(std::move(flashBounce));\n\t}\n\n\tvirtual ~Manager() = default;\n\nprotected:\n\t[[nodiscard]] not_null<System*> system() const {\n\t\treturn _system;\n\t}\n\n\tvirtual void doUpdateAll() = 0;\n\tvirtual void doShowNotification(NotificationFields &&fields) = 0;\n\tvirtual void doClearAll() = 0;\n\tvirtual void doClearAllFast() = 0;\n\tvirtual void doClearFromItem(not_null<HistoryItem*> item) = 0;\n\tvirtual void doClearFromTopic(not_null<Data::ForumTopic*> topic) = 0;\n\tvirtual void doClearFromSublist(\n\t\tnot_null<Data::SavedSublist*> sublist) = 0;\n\tvirtual void doClearFromHistory(not_null<History*> history) = 0;\n\tvirtual void doClearFromSession(not_null<Main::Session*> session) = 0;\n\t[[nodiscard]] virtual bool doSkipToast() const = 0;\n\tvirtual void doMaybePlaySound(Fn<void()> playSound) = 0;\n\tvirtual void doMaybeFlashBounce(Fn<void()> flashBounce) = 0;\n\t[[nodiscard]] virtual bool forceHideDetails() const {\n\t\treturn false;\n\t}\n\tvirtual void onBeforeNotificationActivated(NotificationId id) {\n\t}\n\tvirtual void onAfterNotificationActivated(\n\t\tNotificationId id,\n\t\tnot_null<SessionController*> window) {\n\t}\n\t[[nodiscard]] virtual QString accountNameSeparator();\n\nprivate:\n\tWindow::SessionController *openNotificationMessage(\n\t\tnot_null<History*> history,\n\t\tMsgId messageId,\n\t\tbool openSeparated);\n\n\tconst not_null<System*> _system;\n\n};\n\nclass NativeManager : public Manager {\npublic:\n\t[[nodiscard]] ManagerType type() const override {\n\t\treturn ManagerType::Native;\n\t}\n\n\tusing NotificationSound = Media::Audio::LocalSound;\n\tstruct NotificationAction {\n\t\tQString id;\n\t\tQString text;\n\t};\n\tstruct NotificationInfo {\n\t\tnot_null<PeerData*> peer;\n\t\tMsgId topicRootId = 0;\n\t\tPeerId monoforumPeerId = 0;\n\t\tMsgId itemId = 0;\n\t\tQString title;\n\t\tQString subtitle;\n\t\tQString message;\n\t\tFn<NotificationSound()> sound;\n\t\tDisplayOptions options;\n\t\tstd::vector<NotificationAction> actions;\n\t};\n\nprotected:\n\tusing Manager::Manager;\n\n\tvoid doUpdateAll() override {\n\t\tdoClearAllFast();\n\t}\n\tvoid doClearAll() override {\n\t\tdoClearAllFast();\n\t}\n\tvoid doShowNotification(NotificationFields &&fields) override;\n\n\tbool forceHideDetails() const override;\n\n\tvirtual void doShowNativeNotification(\n\t\tNotificationInfo &&info,\n\t\tUi::PeerUserpicView &userpicView) = 0;\n\nprivate:\n\tMedia::Audio::LocalCache _localSoundCache;\n\n};\n\nclass DummyManager : public NativeManager {\npublic:\n\tusing NativeManager::NativeManager;\n\n\t[[nodiscard]] ManagerType type() const override {\n\t\treturn ManagerType::Dummy;\n\t}\n\nprotected:\n\tvoid doShowNativeNotification(\n\t\tNotificationInfo &&info,\n\t\tUi::PeerUserpicView &userpicView) override {\n\t}\n\tvoid doClearAllFast() override {\n\t}\n\tvoid doClearFromItem(not_null<HistoryItem*> item) override {\n\t}\n\tvoid doClearFromTopic(not_null<Data::ForumTopic*> topic) override {\n\t}\n\tvoid doClearFromSublist(not_null<Data::SavedSublist*> sublist) override {\n\t}\n\tvoid doClearFromHistory(not_null<History*> history) override {\n\t}\n\tvoid doClearFromSession(not_null<Main::Session*> session) override {\n\t}\n\tbool doSkipToast() const override {\n\t\treturn false;\n\t}\n\tvoid doMaybePlaySound(Fn<void()> playSound) override {\n\t\tplaySound();\n\t}\n\tvoid doMaybeFlashBounce(Fn<void()> flashBounce) override {\n\t\tflashBounce();\n\t}\n\n};\n\n[[nodiscard]] QString WrapFromScheduled(const QString &text);\n\n[[nodiscard]] QRect NotificationDisplayRect(Window::Controller *controller);\n\n} // namespace Window::Notifications\n"
  },
  {
    "path": "Telegram/SourceFiles/window/notifications_manager_default.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/notifications_manager_default.h\"\n\n#include \"platform/platform_notifications_manager.h\"\n#include \"platform/platform_specific.h\"\n#include \"core/application.h\"\n#include \"core/ui_integration.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/platform/ui_platform_utility.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/painter.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/ui_utility.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"dialogs/ui/dialogs_layout.h\"\n#include \"window/window_controller.h\"\n#include \"storage/file_download.h\"\n#include \"main/main_session.h\"\n#include \"main/main_account.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/history_view_item_preview.h\"\n#include \"base/platform/base_platform_last_input.h\"\n#include \"base/call_delayed.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_window.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QScreen>\n\nnamespace Window {\nnamespace Notifications {\nnamespace Default {\nnamespace {\n\n[[nodiscard]] QPoint notificationStartPosition() {\n\tconst auto corner = Core::App().settings().notificationsCorner();\n\tconst auto r = NotificationDisplayRect(Core::App().activePrimaryWindow());\n\tconst auto isLeft = Core::Settings::IsLeftCorner(corner);\n\tconst auto isTop = Core::Settings::IsTopCorner(corner);\n\tconst auto x = (isLeft == rtl())\n\t\t? (r.x() + r.width() - st::notifyWidth - st::notifyDeltaX)\n\t\t: (r.x() + st::notifyDeltaX);\n\tconst auto y = isTop ? r.y() : (r.y() + r.height());\n\treturn QPoint(x, y);\n}\n\ninternal::Widget::Direction notificationShiftDirection() {\n\tauto isTop = Core::Settings::IsTopCorner(Core::App().settings().notificationsCorner());\n\treturn isTop ? internal::Widget::Direction::Down : internal::Widget::Direction::Up;\n}\n\n} // namespace\n\nstd::unique_ptr<Manager> Create(System *system) {\n\treturn std::make_unique<Manager>(system);\n}\n\nManager::Manager(System *system)\n: Notifications::Manager(system)\n, _inputCheckTimer([=] { checkLastInput(); }) {\n\tsystem->settingsChanged(\n\t) | rpl::on_next([=](ChangeType change) {\n\t\tsettingsChanged(change);\n\t}, _lifetime);\n}\n\nManager::QueuedNotification::QueuedNotification(NotificationFields &&fields)\n: history(fields.item->history())\n, topicRootId(fields.item->topicRootId())\n, peer(history->peer)\n, reaction(fields.reactionId)\n, author(!fields.reactionFrom\n\t? fields.item->notificationHeader()\n\t: (fields.reactionFrom != peer)\n\t? fields.reactionFrom->name()\n\t: QString())\n, item((fields.forwardedCount < 2) ? fields.item.get() : nullptr)\n, forwardedCount(fields.forwardedCount)\n, fromScheduled(reaction.empty() && (fields.item->out() || peer->isSelf())\n\t&& fields.item->isFromScheduled()) {\n}\n\nQPixmap Manager::hiddenUserpicPlaceholder() const {\n\tif (_hiddenUserpicPlaceholder.isNull()) {\n\t\tconst auto ratio = style::DevicePixelRatio();\n\t\t_hiddenUserpicPlaceholder = Ui::PixmapFromImage(\n\t\t\tLogoNoMargin().scaled(\n\t\t\t\tst::notifyPhotoSize * ratio,\n\t\t\t\tst::notifyPhotoSize * ratio,\n\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\tQt::SmoothTransformation));\n\t\t_hiddenUserpicPlaceholder.setDevicePixelRatio(ratio);\n\t}\n\treturn _hiddenUserpicPlaceholder;\n}\n\nbool Manager::hasReplyingNotification() const {\n\tfor (const auto &notification : _notifications) {\n\t\tif (notification->isReplying()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Manager::settingsChanged(ChangeType change) {\n\tif (change == ChangeType::Corner) {\n\t\tauto startPosition = notificationStartPosition();\n\t\tauto shiftDirection = notificationShiftDirection();\n\t\tfor (const auto &notification : _notifications) {\n\t\t\tnotification->updatePosition(startPosition, shiftDirection);\n\t\t}\n\t\tif (_hideAll) {\n\t\t\t_hideAll->updatePosition(startPosition, shiftDirection);\n\t\t}\n\t} else if (change == ChangeType::MaxCount) {\n\t\tint allow = Core::App().settings().notificationsCount();\n\t\tfor (int i = _notifications.size(); i != 0;) {\n\t\t\tauto &notification = _notifications[--i];\n\t\t\tif (notification->isUnlinked()) continue;\n\t\t\tif (--allow < 0) {\n\t\t\t\tnotification->unlinkHistory();\n\t\t\t}\n\t\t}\n\t\tif (allow > 0) {\n\t\t\tfor (int i = 0; i != allow; ++i) {\n\t\t\t\tshowNextFromQueue();\n\t\t\t}\n\t\t}\n\t} else if ((change == ChangeType::DemoIsShown)\n\t\t\t|| (change == ChangeType::DemoIsHidden)) {\n\t\t_demoIsShown = (change == ChangeType::DemoIsShown);\n\t\t_demoMasterOpacity.start(\n\t\t\t[=] { demoMasterOpacityCallback(); },\n\t\t\t_demoIsShown ? 1. : 0.,\n\t\t\t_demoIsShown ? 0. : 1.,\n\t\t\tst::notifyFastAnim);\n\t}\n}\n\nvoid Manager::demoMasterOpacityCallback() {\n\tfor (const auto &notification : _notifications) {\n\t\tnotification->updateOpacity();\n\t}\n\tif (_hideAll) {\n\t\t_hideAll->updateOpacity();\n\t}\n}\n\nfloat64 Manager::demoMasterOpacity() const {\n\treturn _demoMasterOpacity.value(_demoIsShown ? 0. : 1.);\n}\n\nvoid Manager::checkLastInput() {\n\tauto replying = hasReplyingNotification();\n\tauto waiting = false;\n\tconst auto lastInputTime = base::Platform::LastUserInputTimeSupported()\n\t\t? std::make_optional(Core::App().lastNonIdleTime())\n\t\t: std::nullopt;\n\tfor (const auto &notification : _notifications) {\n\t\tif (!notification->checkLastInput(replying, lastInputTime)) {\n\t\t\twaiting = true;\n\t\t}\n\t}\n\tif (waiting) {\n\t\t_inputCheckTimer.callOnce(300);\n\t}\n}\n\nvoid Manager::startAllHiding() {\n\tif (!hasReplyingNotification()) {\n\t\tfor (const auto &notification : _notifications) {\n\t\t\tnotification->startHiding();\n\t\t}\n\t\tif (_hideAll && _queuedNotifications.size() < 2) {\n\t\t\t_hideAll->startHiding();\n\t\t}\n\t}\n}\n\nvoid Manager::stopAllHiding() {\n\tfor (const auto &notification : _notifications) {\n\t\tnotification->stopHiding();\n\t}\n\tif (_hideAll) {\n\t\t_hideAll->stopHiding();\n\t}\n}\n\nvoid Manager::showNextFromQueue() {\n\tauto guard = gsl::finally([this] {\n\t\tif (_positionsOutdated) {\n\t\t\tmoveWidgets();\n\t\t}\n\t});\n\tif (_queuedNotifications.empty()) {\n\t\treturn;\n\t}\n\tint count = Core::App().settings().notificationsCount();\n\tfor (const auto &notification : _notifications) {\n\t\tif (notification->isUnlinked()) continue;\n\t\t--count;\n\t}\n\tif (count <= 0) {\n\t\treturn;\n\t}\n\n\tauto startPosition = notificationStartPosition();\n\tauto startShift = 0;\n\tauto shiftDirection = notificationShiftDirection();\n\tdo {\n\t\tauto queued = _queuedNotifications.front();\n\t\t_queuedNotifications.pop_front();\n\n\t\tsubscribeToSession(&queued.history->session());\n\t\t_notifications.push_back(std::make_unique<Notification>(\n\t\t\tthis,\n\t\t\tqueued.history,\n\t\t\tqueued.topicRootId,\n\t\t\tqueued.monoforumPeerId,\n\t\t\tqueued.peer,\n\t\t\tqueued.author,\n\t\t\tqueued.item,\n\t\t\tqueued.reaction,\n\t\t\tqueued.forwardedCount,\n\t\t\tqueued.fromScheduled,\n\t\t\tstartPosition,\n\t\t\tstartShift,\n\t\t\tshiftDirection));\n\t\t--count;\n\t} while (count > 0 && !_queuedNotifications.empty());\n\n\t_positionsOutdated = true;\n\tcheckLastInput();\n}\n\nvoid Manager::subscribeToSession(not_null<Main::Session*> session) {\n\tauto i = _subscriptions.find(session);\n\tif (i == _subscriptions.end()) {\n\t\ti = _subscriptions.emplace(session).first;\n\t\tsession->account().sessionChanges(\n\t\t) | rpl::on_next([=] {\n\t\t\t_subscriptions.remove(session);\n\t\t}, i->second.lifetime);\n\t} else if (i->second.subscription) {\n\t\treturn;\n\t}\n\tsession->downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tauto found = false;\n\t\tfor (const auto &notification : _notifications) {\n\t\t\tif (const auto history = notification->maybeHistory()) {\n\t\t\t\tif (&history->session() == session) {\n\t\t\t\t\tnotification->updatePeerPhoto();\n\t\t\t\t\tfound = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!found) {\n\t\t\t_subscriptions[session].subscription.destroy();\n\t\t}\n\t}, i->second.subscription);\n}\n\nvoid Manager::moveWidgets() {\n\tauto shift = st::notifyDeltaY;\n\tint lastShift = 0, lastShiftCurrent = 0, count = 0;\n\tfor (int i = _notifications.size(); i != 0;) {\n\t\tauto &notification = _notifications[--i];\n\t\tif (notification->isUnlinked()) continue;\n\n\t\tnotification->changeShift(shift);\n\t\tshift += notification->height() + st::notifyDeltaY;\n\n\t\tlastShiftCurrent = notification->currentShift();\n\t\tlastShift = shift;\n\n\t\t++count;\n\t}\n\n\tif (count > 1 || !_queuedNotifications.empty()) {\n\t\tif (!_hideAll) {\n\t\t\t_hideAll = std::make_unique<HideAllButton>(this, notificationStartPosition(), lastShiftCurrent, notificationShiftDirection());\n\t\t}\n\t\t_hideAll->changeShift(lastShift);\n\t\t_hideAll->stopHiding();\n\t} else if (_hideAll) {\n\t\t_hideAll->startHidingFast();\n\t}\n}\n\nvoid Manager::changeNotificationHeight(Notification *notification, int newHeight) {\n\tauto deltaHeight = newHeight - notification->height();\n\tif (!deltaHeight) return;\n\n\tnotification->addToHeight(deltaHeight);\n\tauto it = std::find_if(_notifications.cbegin(), _notifications.cend(), [notification](auto &item) {\n\t\treturn (item.get() == notification);\n\t});\n\tif (it != _notifications.cend()) {\n\t\tfor (auto i = _notifications.cbegin(); i != it; ++i) {\n\t\t\tauto &notification = *i;\n\t\t\tif (notification->isUnlinked()) continue;\n\n\t\t\tnotification->addToShift(deltaHeight);\n\t\t}\n\t}\n\tif (_hideAll) {\n\t\t_hideAll->addToShift(deltaHeight);\n\t}\n}\n\nvoid Manager::unlinkFromShown(Notification *remove) {\n\tif (remove) {\n\t\tif (remove->unlinkHistory()) {\n\t\t\t_positionsOutdated = true;\n\t\t}\n\t}\n\tshowNextFromQueue();\n}\n\nvoid Manager::removeWidget(internal::Widget *remove) {\n\tif (remove == _hideAll.get()) {\n\t\t_hideAll.reset();\n\t} else if (remove) {\n\t\tconst auto it = ranges::find(\n\t\t\t_notifications,\n\t\t\tremove,\n\t\t\t&std::unique_ptr<Notification>::get);\n\t\tif (it != end(_notifications)) {\n\t\t\t_notifications.erase(it);\n\t\t\t_positionsOutdated = true;\n\t\t}\n\t}\n\tshowNextFromQueue();\n}\n\nvoid Manager::doShowNotification(NotificationFields &&fields) {\n\t_queuedNotifications.emplace_back(std::move(fields));\n\tshowNextFromQueue();\n}\n\nvoid Manager::doClearAll() {\n\t_queuedNotifications.clear();\n\tfor (const auto &notification : _notifications) {\n\t\tnotification->unlinkHistory();\n\t}\n\tshowNextFromQueue();\n}\n\nvoid Manager::doClearAllFast() {\n\t_queuedNotifications.clear();\n\tbase::take(_notifications);\n\tbase::take(_hideAll);\n}\n\nvoid Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) {\n\tconst auto history = topic->history();\n\tconst auto topicRootId = topic->rootId();\n\tfor (auto i = _queuedNotifications.begin(); i != _queuedNotifications.cend();) {\n\t\tif (i->history == history && i->topicRootId == topicRootId) {\n\t\t\ti = _queuedNotifications.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tfor (const auto &notification : _notifications) {\n\t\tif (notification->unlinkHistory(history, topicRootId, PeerId())) {\n\t\t\t_positionsOutdated = true;\n\t\t}\n\t}\n\tshowNextFromQueue();\n}\n\nvoid Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) {\n\tconst auto history = sublist->owningHistory();\n\tconst auto sublistPeerId = sublist->sublistPeer()->id;\n\tfor (auto i = _queuedNotifications.begin(); i != _queuedNotifications.cend();) {\n\t\tif (i->history == history && i->monoforumPeerId == sublistPeerId) {\n\t\t\ti = _queuedNotifications.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tfor (const auto &notification : _notifications) {\n\t\tif (notification->unlinkHistory(history, MsgId(), sublistPeerId)) {\n\t\t\t_positionsOutdated = true;\n\t\t}\n\t}\n\tshowNextFromQueue();\n}\n\nvoid Manager::doClearFromHistory(not_null<History*> history) {\n\tfor (auto i = _queuedNotifications.begin(); i != _queuedNotifications.cend();) {\n\t\tif (i->history == history) {\n\t\t\ti = _queuedNotifications.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tfor (const auto &notification : _notifications) {\n\t\tif (notification->unlinkHistory(history)) {\n\t\t\t_positionsOutdated = true;\n\t\t}\n\t}\n\tshowNextFromQueue();\n}\n\nvoid Manager::doClearFromSession(not_null<Main::Session*> session) {\n\tfor (auto i = _queuedNotifications.begin(); i != _queuedNotifications.cend();) {\n\t\tif (&i->history->session() == session) {\n\t\t\ti = _queuedNotifications.erase(i);\n\t\t} else {\n\t\t\t++i;\n\t\t}\n\t}\n\tfor (const auto &notification : _notifications) {\n\t\tif (notification->unlinkSession(session)) {\n\t\t\t_positionsOutdated = true;\n\t\t}\n\t}\n\tshowNextFromQueue();\n}\n\nvoid Manager::doClearFromItem(not_null<HistoryItem*> item) {\n\t_queuedNotifications.erase(std::remove_if(_queuedNotifications.begin(), _queuedNotifications.end(), [&](auto &queued) {\n\t\treturn (queued.item == item);\n\t}), _queuedNotifications.cend());\n\n\tauto showNext = false;\n\tfor (const auto &notification : _notifications) {\n\t\tif (notification->unlinkItem(item)) {\n\t\t\tshowNext = true;\n\t\t}\n\t}\n\tif (showNext) {\n\t\t// This call invalidates _notifications iterators.\n\t\tshowNextFromQueue();\n\t}\n}\n\nbool Manager::doSkipToast() const {\n\treturn Platform::Notifications::SkipToastForCustom();\n}\n\nvoid Manager::doMaybePlaySound(Fn<void()> playSound) {\n\tPlatform::Notifications::MaybePlaySoundForCustom(std::move(playSound));\n}\n\nvoid Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {\n\tPlatform::Notifications::MaybeFlashBounceForCustom(std::move(flashBounce));\n}\n\nvoid Manager::doUpdateAll() {\n\tfor (const auto &notification : _notifications) {\n\t\tnotification->updateNotifyDisplay();\n\t}\n}\n\nManager::~Manager() {\n\tclearAllFast();\n}\n\nnamespace internal {\n\nWidget::Widget(\n\tnot_null<Manager*> manager,\n\tQPoint startPosition,\n\tint shift,\n\tDirection shiftDirection)\n: _manager(manager)\n, _startPosition(startPosition)\n, _direction(shiftDirection)\n, _shift(shift)\n, _shiftAnimation([=](crl::time now) {\n\treturn shiftAnimationCallback(now);\n}) {\n\tsetWindowOpacity(0.);\n\n\tsetWindowFlags(Qt::WindowFlags(Qt::FramelessWindowHint)\n\t\t| Qt::WindowStaysOnTopHint\n\t\t| Qt::BypassWindowManagerHint\n\t\t| Qt::NoDropShadowWindowHint\n\t\t| Qt::Tool);\n\tsetAttribute(Qt::WA_MacAlwaysShowToolWindow);\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\n\tUi::Platform::InitOnTopPanel(this);\n\n\t_a_opacity.start([this] { opacityAnimationCallback(); }, 0., 1., st::notifyFastAnim);\n}\n\nvoid Widget::opacityAnimationCallback() {\n\tupdateOpacity();\n\tupdate();\n\tif (!_a_opacity.animating() && _hiding) {\n\t\tif (underMouse()) {\n\t\t\t// The notification is leaving from under the cursor, but in such case leave hook is not\n\t\t\t// triggered automatically. But we still want the manager to start hiding notifications\n\t\t\t// (see #28813).\n\t\t\tmanager()->startAllHiding();\n\t\t}\n\t\tmanager()->removeWidget(this);  // Deletes `this`\n\t}\n}\n\nbool Widget::shiftAnimationCallback(crl::time now) {\n\tif (anim::Disabled()) {\n\t\tnow += st::notifyFastAnim;\n\t}\n\tconst auto dt = (now - _shiftAnimation.started())\n\t\t/ float64(st::notifyFastAnim);\n\tif (dt >= 1.) {\n\t\t_shift.finish();\n\t} else {\n\t\t_shift.update(dt, anim::linear);\n\t}\n\tmoveByShift();\n\treturn (dt < 1.);\n}\n\nvoid Widget::hideSlow() {\n\tif (anim::Disabled()) {\n\t\t_hiding = true;\n\t\tbase::call_delayed(\n\t\t\tst::notifySlowHide,\n\t\t\tthis,\n\t\t\t[=, guard = _hidingDelayed.make_guard()] {\n\t\t\t\tif (guard && _hiding) {\n\t\t\t\t\thideFast();\n\t\t\t\t}\n\t\t\t});\n\t} else {\n\t\thideAnimated(st::notifySlowHide, anim::easeInCirc);\n\t}\n}\n\nvoid Widget::hideFast() {\n\thideAnimated(st::notifyFastAnim, anim::linear);\n}\n\nvoid Widget::hideStop() {\n\tif (_hiding) {\n\t\t_hiding = false;\n\t\t_hidingDelayed = {};\n\t\t_a_opacity.start([this] { opacityAnimationCallback(); }, 0., 1., st::notifyFastAnim);\n\t}\n}\n\nvoid Widget::hideAnimated(float64 duration, const anim::transition &func) {\n\t_hiding = true;\n\t// Stop the previous animation so as to make sure that the notification\n\t// is fully restored before hiding it again.\n\t// Relates to https://github.com/telegramdesktop/tdesktop/issues/28811.\n\t_a_opacity.stop();\n\t_a_opacity.start([this] { opacityAnimationCallback(); }, 1., 0., duration, func);\n}\n\nvoid Widget::updateOpacity() {\n\tsetWindowOpacity(_a_opacity.value(_hiding ? 0. : 1.) * _manager->demoMasterOpacity());\n}\n\nvoid Widget::changeShift(int top) {\n\t_shift.start(top);\n\t_shiftAnimation.start();\n}\n\nvoid Widget::updatePosition(QPoint startPosition, Direction shiftDirection) {\n\t_startPosition = startPosition;\n\t_direction = shiftDirection;\n\tmoveByShift();\n}\n\nvoid Widget::addToHeight(int add) {\n\tauto newHeight = height() + add;\n\tauto newPosition = computePosition(newHeight);\n\tupdateGeometry(newPosition.x(), newPosition.y(), width(), newHeight);\n\tUi::ForceFullRepaintSync(this);\n}\n\nvoid Widget::updateGeometry(int x, int y, int width, int height) {\n\tmove(x, y);\n\tsetFixedSize(width, height);\n\tupdate();\n}\n\nvoid Widget::addToShift(int add) {\n\t_shift.add(add);\n\tmoveByShift();\n}\n\nvoid Widget::moveByShift() {\n\tmove(computePosition(height()));\n}\n\nQPoint Widget::computePosition(int height) const {\n\tauto realShift = qRound(_shift.current());\n\tif (_direction == Direction::Up) {\n\t\trealShift = -realShift - height;\n\t}\n\treturn QPoint(_startPosition.x(), _startPosition.y() + realShift);\n}\n\nBackground::Background(QWidget *parent) : RpWidget(parent) {\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n}\n\nvoid Background::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tp.fillRect(rect(), st::notificationBg);\n\tp.fillRect(0, 0, st::notifyBorderWidth, height(), st::notifyBorder);\n\tp.fillRect(width() - st::notifyBorderWidth, 0, st::notifyBorderWidth, height(), st::notifyBorder);\n\tp.fillRect(st::notifyBorderWidth, height() - st::notifyBorderWidth, width() - 2 * st::notifyBorderWidth, st::notifyBorderWidth, st::notifyBorder);\n}\n\nNotification::Notification(\n\tnot_null<Manager*> manager,\n\tnot_null<History*> history,\n\tMsgId topicRootId,\n\tPeerId monoforumPeerId,\n\tnot_null<PeerData*> peer,\n\tconst QString &author,\n\tHistoryItem *item,\n\tconst Data::ReactionId &reaction,\n\tint forwardedCount,\n\tbool fromScheduled,\n\tQPoint startPosition,\n\tint shift,\n\tDirection shiftDirection)\n: Widget(manager, startPosition, shift, shiftDirection)\n, _peer(peer)\n, _started(crl::now())\n, _history(history)\n, _topic(history->peer->forumTopicFor(topicRootId))\n, _topicRootId(topicRootId)\n, _sublist(history->peer->monoforumSublistFor(monoforumPeerId))\n, _monoforumPeerId(monoforumPeerId)\n, _userpicView(_peer->userpicPaintingPeer()->createUserpicView())\n, _author(author)\n, _reaction(reaction)\n, _item(item)\n, _forwardedCount(forwardedCount)\n, _fromScheduled(fromScheduled)\n, _close(this, st::notifyClose)\n, _reply(this, tr::lng_notification_reply(), st::defaultBoxButton) {\n\t_reply->setTextTransform(Ui::RoundButtonTextTransform::ToUpper);\n\n\tLang::Updated(\n\t) | rpl::on_next([=] {\n\t\trefreshLang();\n\t}, lifetime());\n\n\tif (_topic) {\n\t\t_topic->destroyed(\n\t\t) | rpl::on_next([=] {\n\t\t\tunlinkHistory();\n\t\t}, lifetime());\n\t}\n\n\tauto position = computePosition(st::notifyMinHeight);\n\tupdateGeometry(position.x(), position.y(), st::notifyWidth, st::notifyMinHeight);\n\n\t_userpicLoaded = !Ui::PeerUserpicLoading(_userpicView);\n\tupdateNotifyDisplay();\n\n\t_hideTimer.setSingleShot(true);\n\tconnect(&_hideTimer, &QTimer::timeout, [=] { startHiding(); });\n\n\t_close->setClickedCallback([this] {\n\t\tunlinkHistoryInManager();\n\t});\n\t_close->setAcceptBoth(true);\n\t_close->moveToRight(st::notifyClosePos.x(), st::notifyClosePos.y());\n\t_close->show();\n\n\t_reply->setClickedCallback([this] {\n\t\tshowReplyField();\n\t});\n\t_replyPadding = st::notifyMinHeight - st::notifyPhotoPos.y() - st::notifyPhotoSize;\n\tupdateReplyGeometry();\n\t_reply->hide();\n\n\tprepareActionsCache();\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tupdateNotifyDisplay();\n\t\tif (!_buttonsCache.isNull()) {\n\t\t\tprepareActionsCache();\n\t\t}\n\t\tupdate();\n\t\tif (_background) {\n\t\t\t_background->update();\n\t\t}\n\t}, lifetime());\n\n\tshow();\n}\n\nvoid Notification::updateReplyGeometry() {\n\t_reply->moveToRight(_replyPadding, height() - _reply->height() - _replyPadding);\n}\n\nvoid Notification::refreshLang() {\n\tInvokeQueued(this, [this] { updateReplyGeometry(); });\n}\n\nvoid Notification::prepareActionsCache() {\n\tauto replyCache = Ui::GrabWidget(_reply);\n\tauto fadeWidth = st::notifyFadeRight.width();\n\tauto actionsTop = st::notifyTextTop + st::semiboldFont->height;\n\tauto replyRight = _replyPadding - st::notifyBorderWidth;\n\tauto actionsCacheWidth = _reply->width() + replyRight + fadeWidth;\n\tauto actionsCacheHeight = height() - actionsTop - st::notifyBorderWidth;\n\tauto actionsCacheImg = QImage(\n\t\tQSize(actionsCacheWidth, actionsCacheHeight)\n\t\t\t* style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tactionsCacheImg.setDevicePixelRatio(style::DevicePixelRatio());\n\tactionsCacheImg.fill(Qt::transparent);\n\t{\n\t\tPainter p(&actionsCacheImg);\n\t\tst::notifyFadeRight.fill(p, style::rtlrect(0, 0, fadeWidth, actionsCacheHeight, actionsCacheWidth));\n\t\tp.fillRect(style::rtlrect(fadeWidth, 0, actionsCacheWidth - fadeWidth, actionsCacheHeight, actionsCacheWidth), st::notificationBg);\n\t\tp.drawPixmapRight(replyRight, _reply->y() - actionsTop, actionsCacheWidth, replyCache);\n\t}\n\t_buttonsCache = Ui::PixmapFromImage(std::move(actionsCacheImg));\n}\n\nbool Notification::checkLastInput(\n\t\tbool hasReplyingNotifications,\n\t\tstd::optional<crl::time> lastInputTime) {\n\tif (!_waitingForInput) return true;\n\n\tusing namespace Platform::Notifications;\n\tconst auto waitForUserInput = WaitForInputForCustom()\n\t\t&& lastInputTime.has_value()\n\t\t&& (*lastInputTime <= _started);\n\n\tif (!waitForUserInput) {\n\t\t_waitingForInput = false;\n\t\tif (!hasReplyingNotifications) {\n\t\t\t_hideTimer.start(st::notifyWaitLongHide);\n\t\t}\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Notification::replyResized() {\n\tchangeHeight(st::notifyMinHeight + _replyArea->height() + st::notifyBorderWidth);\n}\n\nvoid Notification::replyCancel() {\n\tunlinkHistoryInManager();\n}\n\nvoid Notification::updateGeometry(int x, int y, int width, int height) {\n\tif (height > st::notifyMinHeight) {\n\t\tif (!_background) {\n\t\t\t_background.create(this);\n\t\t}\n\t\t_background->setGeometry(0, st::notifyMinHeight, width, height - st::notifyMinHeight);\n\t} else if (_background) {\n\t\t_background.destroy();\n\t}\n\tWidget::updateGeometry(x, y, width, height);\n}\n\nvoid Notification::paintEvent(QPaintEvent *e) {\n\trepaintText();\n\n\tPainter p(this);\n\tp.setClipRect(e->rect());\n\tp.drawImage(0, 0, _cache);\n\n\tauto buttonsTop = st::notifyTextTop + st::semiboldFont->height;\n\tif (a_actionsOpacity.animating()) {\n\t\tp.setOpacity(a_actionsOpacity.value(1.));\n\t\tp.drawPixmapRight(st::notifyBorderWidth, buttonsTop, width(), _buttonsCache);\n\t} else if (_actionsVisible) {\n\t\tp.drawPixmapRight(st::notifyBorderWidth, buttonsTop, width(), _buttonsCache);\n\t}\n}\n\nvoid Notification::actionsOpacityCallback() {\n\tupdate();\n\tif (!a_actionsOpacity.animating() && _actionsVisible) {\n\t\t_reply->show();\n\t}\n}\n\nvoid Notification::customEmojiCallback() {\n\tif (_textsRepaintScheduled) {\n\t\treturn;\n\t}\n\t_textsRepaintScheduled = true;\n\tcrl::on_main(this, [=] { repaintText(); });\n}\n\nvoid Notification::repaintText() {\n\tif (!_textsRepaintScheduled) {\n\t\treturn;\n\t}\n\t_textsRepaintScheduled = false;\n\tif (_cache.isNull()) {\n\t\treturn;\n\t}\n\tPainter p(&_cache);\n\tconst auto adjusted = Ui::Text::AdjustCustomEmojiSize(st::emojiSize);\n\tconst auto skip = (adjusted - st::emojiSize + 1) / 2;\n\tconst auto margin = QMargins{ skip, skip, skip, skip };\n\tp.fillRect(_titleRect.marginsAdded(margin), st::notificationBg);\n\tp.fillRect(_textRect.marginsAdded(margin), st::notificationBg);\n\tpaintTitle(p);\n\tpaintText(p);\n\tupdate();\n}\n\nvoid Notification::paintTitle(Painter &p) {\n\tp.setPen(st::dialogsNameFg);\n\tp.setFont(st::semiboldFont);\n\t_titleCache.draw(p, {\n\t\t.position = _titleRect.topLeft(),\n\t\t.availableWidth = _titleRect.width(),\n\t\t.palette = &st::dialogsTextPalette,\n\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t.pausedEmoji = On(PowerSaving::kEmojiChat),\n\t\t.pausedSpoiler = On(PowerSaving::kChatSpoiler),\n\t\t.elisionLines = 1,\n\t});\n}\n\nvoid Notification::paintText(Painter &p) {\n\tp.setPen(st::dialogsTextFg);\n\tp.setFont(st::dialogsTextFont);\n\t_textCache.draw(p, {\n\t\t.position = _textRect.topLeft(),\n\t\t.availableWidth = _textRect.width(),\n\t\t.palette = &st::dialogsTextPalette,\n\t\t.spoiler = Ui::Text::DefaultSpoilerCache(),\n\t\t.pausedEmoji = On(PowerSaving::kEmojiChat),\n\t\t.pausedSpoiler = On(PowerSaving::kChatSpoiler),\n\t\t.elisionHeight = _textRect.height(),\n\t});\n}\n\nvoid Notification::updateNotifyDisplay() {\n\tif (!_history || (!_item && _forwardedCount < 2)) {\n\t\treturn;\n\t}\n\n\tconst auto options = manager()->getNotificationOptions(\n\t\t_item,\n\t\t(_reaction.empty()\n\t\t\t? Data::ItemNotificationType::Message\n\t\t\t: Data::ItemNotificationType::Reaction));\n\t_hideReplyButton = options.hideReplyButton;\n\n\tint32 w = width(), h = height();\n\tauto img = QImage(\n\t\tsize() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timg.setDevicePixelRatio(style::DevicePixelRatio());\n\timg.fill(st::notificationBg->c);\n\n\t{\n\t\tPainter p(&img);\n\t\tp.fillRect(0, 0, w - st::notifyBorderWidth, st::notifyBorderWidth, st::notifyBorder);\n\t\tp.fillRect(w - st::notifyBorderWidth, 0, st::notifyBorderWidth, h - st::notifyBorderWidth, st::notifyBorder);\n\t\tp.fillRect(st::notifyBorderWidth, h - st::notifyBorderWidth, w - st::notifyBorderWidth, st::notifyBorderWidth, st::notifyBorder);\n\t\tp.fillRect(0, st::notifyBorderWidth, st::notifyBorderWidth, h - st::notifyBorderWidth, st::notifyBorder);\n\n\t\tif (!options.hideNameAndPhoto) {\n\t\t\tif (_fromScheduled && _history->peer->isSelf()) {\n\t\t\t\tUi::EmptyUserpic::PaintSavedMessages(p, st::notifyPhotoPos.x(), st::notifyPhotoPos.y(), width(), st::notifyPhotoSize);\n\t\t\t\t_userpicLoaded = true;\n\t\t\t} else if (_history->peer->isRepliesChat()) {\n\t\t\t\tUi::EmptyUserpic::PaintRepliesMessages(p, st::notifyPhotoPos.x(), st::notifyPhotoPos.y(), width(), st::notifyPhotoSize);\n\t\t\t\t_userpicLoaded = true;\n\t\t\t} else {\n\t\t\t\t_userpicView = _history->peer->createUserpicView();\n\t\t\t\t_history->peer->loadUserpic();\n\t\t\t\t_history->peer->paintUserpicLeft(p, _userpicView, st::notifyPhotoPos.x(), st::notifyPhotoPos.y(), width(), st::notifyPhotoSize);\n\t\t\t}\n\t\t} else {\n\t\t\tp.drawPixmap(st::notifyPhotoPos.x(), st::notifyPhotoPos.y(), manager()->hiddenUserpicPlaceholder());\n\t\t\t_userpicLoaded = true;\n\t\t}\n\n\t\tint32 itemWidth = w - st::notifyPhotoPos.x() - st::notifyPhotoSize - st::notifyTextLeft - st::notifyClosePos.x() - st::notifyClose.width;\n\n\t\tQRect rectForName(st::notifyPhotoPos.x() + st::notifyPhotoSize + st::notifyTextLeft, st::notifyTextTop, itemWidth, st::semiboldFont->height);\n\t\tconst auto reminder = _fromScheduled && _history->peer->isSelf();\n\t\tif (!options.hideNameAndPhoto) {\n\t\t\tif (_fromScheduled) {\n\t\t\t\tstatic const auto emoji = Ui::Emoji::Find(QString::fromUtf8(\"\\xF0\\x9F\\x93\\x85\"));\n\t\t\t\tconst auto size = Ui::Emoji::GetSizeNormal()\n\t\t\t\t\t/ style::DevicePixelRatio();\n\t\t\t\tconst auto top = rectForName.top() + (st::semiboldFont->height - size) / 2;\n\t\t\t\tUi::Emoji::Draw(p, emoji, Ui::Emoji::GetSizeNormal(), rectForName.left(), top);\n\t\t\t\trectForName.setLeft(rectForName.left() + size + st::semiboldFont->spacew);\n\t\t\t}\n\t\t\tconst auto chatTypeIcon = _topic\n\t\t\t\t? nullptr\n\t\t\t\t: Dialogs::Ui::ChatTypeIcon(_history->peer);\n\t\t\tif (chatTypeIcon) {\n\t\t\t\tchatTypeIcon->paint(p, rectForName.topLeft(), w);\n\t\t\t\trectForName.setLeft(rectForName.left()\n\t\t\t\t\t+ chatTypeIcon->width()\n\t\t\t\t\t+ st::dialogsChatTypeSkip);\n\t\t\t}\n\t\t}\n\n\t\tconst auto composeText = !options.hideMessageText\n\t\t\t|| (!_reaction.empty() && !options.hideNameAndPhoto);\n\t\tif (composeText) {\n\t\t\tauto old = base::take(_textCache);\n\t\t\t_textCache = Ui::Text::String(itemWidth);\n\t\t\tauto r = QRect(\n\t\t\t\tst::notifyPhotoPos.x() + st::notifyPhotoSize + st::notifyTextLeft,\n\t\t\t\tst::notifyItemTop + st::semiboldFont->height,\n\t\t\t\titemWidth,\n\t\t\t\t2 * st::dialogsTextFont->height);\n\t\t\tconst auto text = !_reaction.empty()\n\t\t\t\t? (!_author.isEmpty()\n\t\t\t\t\t? Ui::Text::Colorized(_author).append(' ')\n\t\t\t\t\t: TextWithEntities()\n\t\t\t\t).append(Manager::ComposeReactionNotification(\n\t\t\t\t\t_item,\n\t\t\t\t\t_reaction,\n\t\t\t\t\toptions.hideMessageText))\n\t\t\t\t: _item\n\t\t\t\t? _item->toPreview({\n\t\t\t\t\t.hideSender = reminder,\n\t\t\t\t\t.generateImages = false,\n\t\t\t\t\t.spoilerLoginCode = options.spoilerLoginCode,\n\t\t\t\t}).text\n\t\t\t\t: ((!_author.isEmpty()\n\t\t\t\t\t\t? Ui::Text::Colorized(_author)\n\t\t\t\t\t\t: TextWithEntities()\n\t\t\t\t\t).append(_forwardedCount > 1\n\t\t\t\t\t\t? ('\\n' + tr::lng_forward_messages(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\t_forwardedCount))\n\t\t\t\t\t\t: QString()));\n\t\t\tconst auto options = TextParseOptions{\n\t\t\t\t(TextParseColorized\n\t\t\t\t\t| TextParseMarkdown\n\t\t\t\t\t| (_forwardedCount > 1 ? TextParseMultiline : 0)),\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\tQt::LayoutDirectionAuto,\n\t\t\t};\n\t\t\tconst auto context = Core::TextContext({\n\t\t\t\t.session = &_history->session(),\n\t\t\t\t.repaint = [=] { customEmojiCallback(); },\n\t\t\t});\n\t\t\t_textCache.setMarkedText(\n\t\t\t\tst::dialogsTextStyle,\n\t\t\t\ttext,\n\t\t\t\toptions,\n\t\t\t\tcontext);\n\t\t\t_textRect = r;\n\t\t\tpaintText(p);\n\t\t\tif (!_textCache.hasPersistentAnimation() && !_topic) {\n\t\t\t\t_textCache = Ui::Text::String();\n\t\t\t}\n\t\t} else {\n\t\t\tp.setFont(st::dialogsTextFont);\n\t\t\tp.setPen(st::dialogsTextFgService);\n\t\t\tp.drawText(\n\t\t\t\tst::notifyPhotoPos.x() + st::notifyPhotoSize + st::notifyTextLeft,\n\t\t\t\tst::notifyItemTop + st::semiboldFont->height + st::dialogsTextFont->ascent,\n\t\t\t\tst::dialogsTextFont->elided(\n\t\t\t\t\ttr::lng_notification_preview(tr::now),\n\t\t\t\t\titemWidth));\n\t\t}\n\n\t\tconst auto topicWithChat = [&]() -> TextWithEntities {\n\t\t\tconst auto name = st::wrap_rtl(_history->peer->name());\n\t\t\treturn _topic\n\t\t\t\t? _topic->titleWithIcon().append(u\" (\"_q + name + ')')\n\t\t\t\t: TextWithEntities{ name };\n\t\t};\n\t\tauto title = options.hideNameAndPhoto\n\t\t\t? TextWithEntities{ u\"Forkgram Desktop\"_q }\n\t\t\t: reminder\n\t\t\t? tr::lng_notification_reminder(tr::now, tr::marked)\n\t\t\t: topicWithChat();\n\t\tconst auto fullTitle = manager()->addTargetAccountName(\n\t\t\tstd::move(title),\n\t\t\t&_history->session());\n\t\tconst auto context = Core::TextContext({\n\t\t\t.session = &_history->session(),\n\t\t\t.repaint = [=] { customEmojiCallback(); },\n\t\t});\n\t\t_titleCache.setMarkedText(\n\t\t\tst::semiboldTextStyle,\n\t\t\tfullTitle,\n\t\t\tUi::NameTextOptions(),\n\t\t\tcontext);\n\t\t_titleRect = rectForName;\n\t\tpaintTitle(p);\n\t}\n\n\t_cache = std::move(img);\n\tif (!canReply()) {\n\t\ttoggleActionButtons(false);\n\t}\n\tupdate();\n}\n\nvoid Notification::updatePeerPhoto() {\n\tif (_userpicLoaded) {\n\t\treturn;\n\t}\n\t_userpicView = _peer->createUserpicView();\n\tif (Ui::PeerUserpicLoading(_userpicView)) {\n\t\treturn;\n\t}\n\t_userpicLoaded = true;\n\n\tPainter p(&_cache);\n\tp.fillRect(\n\t\tstyle::rtlrect(\n\t\t\tQRect(\n\t\t\t\tst::notifyPhotoPos,\n\t\t\t\tQSize(st::notifyPhotoSize, st::notifyPhotoSize)),\n\t\t\twidth()),\n\t\tst::notificationBg);\n\t_peer->paintUserpicLeft(\n\t\tp,\n\t\t_userpicView,\n\t\tst::notifyPhotoPos.x(),\n\t\tst::notifyPhotoPos.y(),\n\t\twidth(),\n\t\tst::notifyPhotoSize);\n\t_userpicView = {};\n\tupdate();\n}\n\nbool Notification::unlinkItem(HistoryItem *deleted) {\n\tauto unlink = (_item && _item == deleted);\n\tif (unlink) {\n\t\t_item = nullptr;\n\t\tunlinkHistory();\n\t}\n\treturn unlink;\n}\n\nbool Notification::canReply() const {\n\treturn !_hideReplyButton\n\t\t&& (_item != nullptr)\n\t\t&& !Core::App().passcodeLocked()\n\t\t&& (Core::App().settings().notifyView()\n\t\t\t<= Core::Settings::NotifyView::ShowPreview);\n}\n\nvoid Notification::unlinkHistoryInManager() {\n\tmanager()->unlinkFromShown(this);\n}\n\nvoid Notification::toggleActionButtons(bool visible) {\n\tif (_actionsVisible != visible) {\n\t\t_actionsVisible = visible;\n\t\ta_actionsOpacity.start([this] { actionsOpacityCallback(); }, _actionsVisible ? 0. : 1., _actionsVisible ? 1. : 0., st::notifyActionsDuration);\n\t\t_reply->clearState();\n\t\t_reply->hide();\n\t}\n}\n\nvoid Notification::showReplyField() {\n\tif (!_item) {\n\t\treturn;\n\t}\n\traise();\n\tactivateWindow();\n\n\tif (_replyArea) {\n\t\t_replyArea->setFocus();\n\t\treturn;\n\t}\n\tstopHiding();\n\n\t_background.create(this);\n\t_background->setGeometry(0, st::notifyMinHeight, width(), st::notifySendReply.height + st::notifyBorderWidth);\n\t_background->show();\n\n\t_replyArea.create(\n\t\tthis,\n\t\tst::notifyReplyArea,\n\t\tUi::InputField::Mode::MultiLine,\n\t\ttr::lng_message_ph());\n\t_replyArea->resize(width() - st::notifySendReply.width - 2 * st::notifyBorderWidth, st::notifySendReply.height);\n\t_replyArea->moveToLeft(st::notifyBorderWidth, st::notifyMinHeight);\n\t_replyArea->show();\n\t_replyArea->setFocus();\n\t_replyArea->setMaxLength(MaxMessageSize);\n\t_replyArea->setSubmitSettings(Ui::InputField::SubmitSettings::Both);\n\tInitMessageFieldHandlers({\n\t\t.session = &_item->history()->session(),\n\t\t.field = _replyArea.data(),\n\t});\n\n\t// Catch mouse press event to activate the window.\n\tQCoreApplication::instance()->installEventFilter(this);\n\t_replyArea->heightChanges(\n\t) | rpl::on_next([=] {\n\t\treplyResized();\n\t}, _replyArea->lifetime());\n\t_replyArea->submits(\n\t) | rpl::on_next([=] { sendReply(); }, _replyArea->lifetime());\n\t_replyArea->cancelled(\n\t) | rpl::on_next([=] {\n\t\treplyCancel();\n\t}, _replyArea->lifetime());\n\n\t_replySend.create(this, st::notifySendReply);\n\t_replySend->moveToRight(st::notifyBorderWidth, st::notifyMinHeight);\n\t_replySend->show();\n\t_replySend->setClickedCallback([this] { sendReply(); });\n\n\ttoggleActionButtons(false);\n\n\treplyResized();\n\tupdate();\n}\n\nvoid Notification::sendReply() {\n\tif (!_history) return;\n\n\tmanager()->notificationReplied(\n\t\tmyId(),\n\t\t_replyArea->getTextWithAppliedMarkdown());\n\n\tmanager()->startAllHiding();\n}\n\nNotifications::Manager::NotificationId Notification::myId() const {\n\tif (!_history) {\n\t\treturn {};\n\t}\n\treturn { .contextId = {\n\t\t.sessionId = _history->session().uniqueId(),\n\t\t.peerId = _history->peer->id,\n\t\t.topicRootId = _topicRootId,\n\t}, .msgId = _item ? _item->id : ShowAtUnreadMsgId };\n}\n\nvoid Notification::changeHeight(int newHeight) {\n\tmanager()->changeNotificationHeight(this, newHeight);\n}\n\nbool Notification::unlinkHistory(\n\t\tHistory *history,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId) {\n\tconst auto unlink = _history\n\t\t&& (history == _history || !history)\n\t\t&& (topicRootId == _topicRootId || !topicRootId)\n\t\t&& (monoforumPeerId == _monoforumPeerId || !monoforumPeerId);\n\tif (unlink) {\n\t\thideFast();\n\t\t_history = nullptr;\n\t\t_topic = nullptr;\n\t\t_item = nullptr;\n\t}\n\treturn unlink;\n}\n\nbool Notification::unlinkSession(not_null<Main::Session*> session) {\n\tconst auto unlink = _history && (&_history->session() == session);\n\tif (unlink) {\n\t\thideFast();\n\t\t_history = nullptr;\n\t\t_item = nullptr;\n\t}\n\treturn unlink;\n}\n\nvoid Notification::enterEventHook(QEnterEvent *e) {\n\tif (!_history) {\n\t\treturn;\n\t}\n\tmanager()->stopAllHiding();\n\tif (!_replyArea && canReply()) {\n\t\ttoggleActionButtons(true);\n\t}\n}\n\nvoid Notification::leaveEventHook(QEvent *e) {\n\tif (!_history) {\n\t\treturn;\n\t}\n\tmanager()->startAllHiding();\n\ttoggleActionButtons(false);\n}\n\nvoid Notification::startHiding() {\n\tif (!_history) return;\n\thideSlow();\n}\n\nvoid Notification::mousePressEvent(QMouseEvent *e) {\n\tif (!_history) return;\n\n\tif (e->button() == Qt::RightButton) {\n\t\tunlinkHistoryInManager();\n\t} else {\n\t\te->ignore();\n\t\tmanager()->notificationActivated(myId(), {\n\t\t\t.allowNewWindow = true,\n\t\t});\n\t}\n}\n\nbool Notification::eventFilter(QObject *o, QEvent *e) {\n\tif (e->type() == QEvent::MouseButtonPress) {\n\t\tif (auto receiver = qobject_cast<QWidget*>(o)) {\n\t\t\tif (isAncestorOf(receiver)) {\n\t\t\t\traise();\n\t\t\t\tactivateWindow();\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid Notification::stopHiding() {\n\tif (!_history) return;\n\t_hideTimer.stop();\n\tWidget::hideStop();\n}\n\nHideAllButton::HideAllButton(\n\tnot_null<Manager*> manager,\n\tQPoint startPosition,\n\tint shift,\n\tDirection shiftDirection)\n: Widget(manager, startPosition, shift, shiftDirection) {\n\tsetCursor(style::cur_pointer);\n\n\tauto position = computePosition(st::notifyHideAllHeight);\n\tupdateGeometry(position.x(), position.y(), st::notifyWidth, st::notifyHideAllHeight);\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\tshow();\n}\n\nvoid HideAllButton::startHiding() {\n\thideSlow();\n}\n\nvoid HideAllButton::startHidingFast() {\n\thideFast();\n}\n\nvoid HideAllButton::stopHiding() {\n\thideStop();\n}\n\nvoid HideAllButton::enterEventHook(QEnterEvent *e) {\n\t_mouseOver = true;\n\tupdate();\n}\n\nvoid HideAllButton::leaveEventHook(QEvent *e) {\n\t_mouseOver = false;\n\tupdate();\n}\n\nvoid HideAllButton::mousePressEvent(QMouseEvent *e) {\n\t_mouseDown = true;\n}\n\nvoid HideAllButton::mouseReleaseEvent(QMouseEvent *e) {\n\tauto mouseDown = base::take(_mouseDown);\n\tif (mouseDown && _mouseOver) {\n\t\tmanager()->clearAll();\n\t}\n}\n\nvoid HideAllButton::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\tp.setClipRect(e->rect());\n\n\tp.fillRect(rect(), _mouseOver ? st::lightButtonBgOver : st::lightButtonBg);\n\tp.fillRect(0, 0, width(), st::notifyBorderWidth, st::notifyBorder);\n\tp.fillRect(0, height() - st::notifyBorderWidth, width(), st::notifyBorderWidth, st::notifyBorder);\n\tp.fillRect(0, st::notifyBorderWidth, st::notifyBorderWidth, height() - 2 * st::notifyBorderWidth, st::notifyBorder);\n\tp.fillRect(width() - st::notifyBorderWidth, st::notifyBorderWidth, st::notifyBorderWidth, height() - 2 * st::notifyBorderWidth, st::notifyBorder);\n\n\tp.setFont(st::defaultLinkButton.font);\n\tp.setPen(_mouseOver ? st::lightButtonFgOver : st::lightButtonFg);\n\tp.drawText(rect(), tr::lng_notification_hide_all(tr::now), style::al_center);\n}\n\n} // namespace internal\n} // namespace Default\n} // namespace Notifications\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/notifications_manager_default.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"window/notifications_manager.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/text/text.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/userpic_view.h\"\n#include \"base/timer.h\"\n#include \"base/binary_guard.h\"\n#include \"base/object_ptr.h\"\n\n#include <QtCore/QTimer>\n\nnamespace Ui {\nclass IconButton;\nclass RoundButton;\nclass InputField;\n} // namespace Ui\n\nnamespace Window {\nnamespace Notifications {\nnamespace Default {\nnamespace internal {\nclass Widget;\nclass Notification;\nclass HideAllButton;\n} // namespace internal\n\nclass Manager;\nstd::unique_ptr<Manager> Create(System *system);\n\nclass Manager final : public Notifications::Manager {\npublic:\n\tManager(System *system);\n\t~Manager();\n\n\t[[nodiscard]] ManagerType type() const override {\n\t\treturn ManagerType::Default;\n\t}\n\n\ttemplate <typename Method>\n\tvoid enumerateNotifications(Method method) {\n\t\tfor (const auto &notification : _notifications) {\n\t\t\tmethod(notification);\n\t\t}\n\t}\n\nprivate:\n\tfriend class internal::Notification;\n\tfriend class internal::HideAllButton;\n\tfriend class internal::Widget;\n\tusing Notification = internal::Notification;\n\tusing HideAllButton = internal::HideAllButton;\n\tstruct SessionSubscription {\n\t\trpl::lifetime subscription;\n\t\trpl::lifetime lifetime;\n\t};\n\n\t[[nodiscard]] QPixmap hiddenUserpicPlaceholder() const;\n\n\tvoid doUpdateAll() override;\n\tvoid doShowNotification(NotificationFields &&fields) override;\n\tvoid doClearAll() override;\n\tvoid doClearAllFast() override;\n\tvoid doClearFromTopic(not_null<Data::ForumTopic*> topic) override;\n\tvoid doClearFromSublist(not_null<Data::SavedSublist*> sublist) override;\n\tvoid doClearFromHistory(not_null<History*> history) override;\n\tvoid doClearFromSession(not_null<Main::Session*> session) override;\n\tvoid doClearFromItem(not_null<HistoryItem*> item) override;\n\tbool doSkipToast() const override;\n\tvoid doMaybePlaySound(Fn<void()> playSound) override;\n\tvoid doMaybeFlashBounce(Fn<void()> flashBounce) override;\n\n\tvoid showNextFromQueue();\n\tvoid unlinkFromShown(Notification *remove);\n\tvoid startAllHiding();\n\tvoid stopAllHiding();\n\tvoid checkLastInput();\n\n\tvoid removeWidget(internal::Widget *remove);\n\n\tfloat64 demoMasterOpacity() const;\n\tvoid demoMasterOpacityCallback();\n\n\tvoid moveWidgets();\n\tvoid changeNotificationHeight(Notification *widget, int newHeight);\n\tvoid settingsChanged(ChangeType change);\n\n\tbool hasReplyingNotification() const;\n\n\tvoid subscribeToSession(not_null<Main::Session*> session);\n\n\tstd::vector<std::unique_ptr<Notification>> _notifications;\n\tbase::flat_map<\n\t\tnot_null<Main::Session*>,\n\t\tSessionSubscription> _subscriptions;\n\n\tstd::unique_ptr<HideAllButton> _hideAll;\n\n\tbool _positionsOutdated = false;\n\tbase::Timer _inputCheckTimer;\n\n\tstruct QueuedNotification {\n\t\tQueuedNotification(NotificationFields &&fields);\n\n\t\tnot_null<History*> history;\n\t\tMsgId topicRootId = 0;\n\t\tPeerId monoforumPeerId = 0;\n\t\tnot_null<PeerData*> peer;\n\t\tData::ReactionId reaction;\n\t\tQString author;\n\t\tHistoryItem *item = nullptr;\n\t\tint forwardedCount = 0;\n\t\tbool fromScheduled = false;\n\t};\n\tstd::deque<QueuedNotification> _queuedNotifications;\n\n\tUi::Animations::Simple _demoMasterOpacity;\n\tbool _demoIsShown = false;\n\n\tmutable QPixmap _hiddenUserpicPlaceholder;\n\n\trpl::lifetime _lifetime;\n\n};\n\nnamespace internal {\n\nclass Widget : public Ui::RpWidget {\npublic:\n\tenum class Direction {\n\t\tUp,\n\t\tDown,\n\t};\n\tWidget(\n\t\tnot_null<Manager*> manager,\n\t\tQPoint startPosition,\n\t\tint shift,\n\t\tDirection shiftDirection);\n\n\tbool isFadingIn() const {\n\t\treturn _a_opacity.animating() && !_hiding;\n\t}\n\n\tvoid updateOpacity();\n\tvoid changeShift(int top);\n\tint currentShift() const {\n\t\treturn _shift.current();\n\t}\n\tvoid updatePosition(QPoint startPosition, Direction shiftDirection);\n\tvoid addToHeight(int add);\n\tvoid addToShift(int add);\n\nprotected:\n\tvoid hideSlow();\n\tvoid hideFast();\n\tvoid hideStop();\n\tQPoint computePosition(int height) const;\n\n\tvirtual void updateGeometry(int x, int y, int width, int height);\n\nprotected:\n\t[[nodiscard]] not_null<Manager*> manager() const {\n\t\treturn _manager;\n\t}\n\nprivate:\n\tvoid opacityAnimationCallback();\n\tvoid moveByShift();\n\tvoid hideAnimated(float64 duration, const anim::transition &func);\n\tbool shiftAnimationCallback(crl::time now);\n\n\tconst not_null<Manager*> _manager;\n\n\tbool _hiding = false;\n\tbase::binary_guard _hidingDelayed;\n\tUi::Animations::Simple _a_opacity;\n\n\tQPoint _startPosition;\n\tDirection _direction;\n\tanim::value _shift;\n\tUi::Animations::Basic _shiftAnimation;\n\n};\n\nclass Background : public Ui::RpWidget {\npublic:\n\tBackground(QWidget *parent);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n};\n\nclass Notification final : public Widget {\npublic:\n\tNotification(\n\t\tnot_null<Manager*> manager,\n\t\tnot_null<History*> history,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tnot_null<PeerData*> peer,\n\t\tconst QString &author,\n\t\tHistoryItem *item,\n\t\tconst Data::ReactionId &reaction,\n\t\tint forwardedCount,\n\t\tbool fromScheduled,\n\t\tQPoint startPosition,\n\t\tint shift,\n\t\tDirection shiftDirection);\n\n\tvoid startHiding();\n\tvoid stopHiding();\n\n\tvoid updateNotifyDisplay();\n\tvoid updatePeerPhoto();\n\n\tbool isUnlinked() const {\n\t\treturn !_history;\n\t}\n\tbool isReplying() const {\n\t\treturn _replyArea && !isUnlinked();\n\t}\n\t[[nodiscard]] History *maybeHistory() const {\n\t\treturn _history;\n\t}\n\n\t// Called only by Manager.\n\tbool unlinkItem(HistoryItem *del);\n\tbool unlinkHistory(\n\t\tHistory *history = nullptr,\n\t\tMsgId topicRootId = 0,\n\t\tPeerId monoforumPeerId = 0);\n\tbool unlinkSession(not_null<Main::Session*> session);\n\tbool checkLastInput(\n\t\tbool hasReplyingNotifications,\n\t\tstd::optional<crl::time> lastInputTime);\n\nprotected:\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tbool eventFilter(QObject *o, QEvent *e) override;\n\nprivate:\n\tvoid refreshLang();\n\tvoid updateReplyGeometry();\n\tbool canReply() const;\n\tvoid replyResized();\n\tvoid replyCancel();\n\n\tvoid unlinkHistoryInManager();\n\tvoid toggleActionButtons(bool visible);\n\tvoid prepareActionsCache();\n\tvoid showReplyField();\n\tvoid sendReply();\n\tvoid changeHeight(int newHeight);\n\tvoid updateGeometry(int x, int y, int width, int height) override;\n\tvoid actionsOpacityCallback();\n\tvoid repaintText();\n\tvoid paintTitle(Painter &p);\n\tvoid paintText(Painter &p);\n\tvoid customEmojiCallback();\n\n\t[[nodiscard]] Notifications::Manager::NotificationId myId() const;\n\n\tconst not_null<PeerData*> _peer;\n\n\tQImage _cache;\n\tUi::Text::String _titleCache;\n\tUi::Text::String _textCache;\n\tQRect _titleRect;\n\tQRect _textRect;\n\n\tbool _hideReplyButton = false;\n\tbool _actionsVisible = false;\n\tbool _textsRepaintScheduled = false;\n\tUi::Animations::Simple a_actionsOpacity;\n\tQPixmap _buttonsCache;\n\n\tcrl::time _started;\n\n\tHistory *_history = nullptr;\n\tData::ForumTopic *_topic = nullptr;\n\tMsgId _topicRootId = 0;\n\tData::SavedSublist *_sublist = nullptr;\n\tPeerId _monoforumPeerId = 0;\n\tUi::PeerUserpicView _userpicView;\n\tQString _author;\n\tData::ReactionId _reaction;\n\tHistoryItem *_item = nullptr;\n\tint _forwardedCount = 0;\n\tbool _fromScheduled = false;\n\tobject_ptr<Ui::IconButton> _close;\n\tobject_ptr<Ui::RoundButton> _reply;\n\tobject_ptr<Background> _background = { nullptr };\n\tobject_ptr<Ui::InputField> _replyArea = { nullptr };\n\tobject_ptr<Ui::IconButton> _replySend = { nullptr };\n\tbool _waitingForInput = true;\n\n\tQTimer _hideTimer;\n\n\tint _replyPadding = 0;\n\n\tbool _userpicLoaded = false;\n\n};\n\nclass HideAllButton : public Widget {\npublic:\n\tHideAllButton(\n\t\tnot_null<Manager*> manager,\n\t\tQPoint startPosition,\n\t\tint shift,\n\t\tDirection shiftDirection);\n\n\tvoid startHiding();\n\tvoid startHidingFast();\n\tvoid stopHiding();\n\nprotected:\n\tvoid enterEventHook(QEnterEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tbool _mouseOver = false;\n\tbool _mouseDown = false;\n\n};\n\n} // namespace internal\n} // namespace Default\n} // namespace Notifications\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/notifications_utilities.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/notifications_utilities.h\"\n\n#include \"window/main_window.h\"\n#include \"base/platform/base_platform_file_utilities.h\"\n#include \"base/random.h\"\n#include \"core/application.h\"\n#include \"data/data_peer.h\"\n#include \"ui/empty_userpic.h\"\n#include \"styles/style_window.h\"\n\nnamespace Window::Notifications {\nnamespace {\n\n// Delete notify photo file after 1 minute of not using.\nconstexpr int kNotifyDeletePhotoAfterMs = 60000;\n\n} // namespace\n\nQImage GenerateUserpic(not_null<PeerData*> peer, Ui::PeerUserpicView &view) {\n\treturn peer->isSelf()\n\t\t? Ui::EmptyUserpic::GenerateSavedMessages(st::notifyMacPhotoSize)\n\t\t: peer->isRepliesChat()\n\t\t? Ui::EmptyUserpic::GenerateRepliesMessages(st::notifyMacPhotoSize)\n\t\t: PeerData::GenerateUserpicImage(peer, view, st::notifyMacPhotoSize);\n}\n\nCachedUserpics::CachedUserpics()\n: _clearTimer([=] { clear(); }) {\n\tQDir().mkpath(cWorkingDir() + u\"tdata/temp\"_q);\n}\n\nCachedUserpics::~CachedUserpics() {\n\tif (_someSavedFlag) {\n\t\tfor (const auto &item : std::as_const(_images)) {\n\t\t\tQFile(item.path).remove();\n\t\t}\n\n\t\t// This works about 1200ms on Windows for a folder with one image O_o\n\t\t//base::Platform::DeleteDirectory(cWorkingDir() + u\"tdata/temp\"_q);\n\t}\n}\n\nQString CachedUserpics::get(\n\t\tconst InMemoryKey &key,\n\t\tnot_null<PeerData*> peer,\n\t\tUi::PeerUserpicView &view) {\n\tauto ms = crl::now();\n\tauto i = _images.find(key);\n\tif (i != _images.cend()) {\n\t\tif (i->until) {\n\t\t\ti->until = ms + kNotifyDeletePhotoAfterMs;\n\t\t\tclearInMs(-kNotifyDeletePhotoAfterMs);\n\t\t}\n\t} else {\n\t\tImage v;\n\t\tif (key.first) {\n\t\t\tv.until = ms + kNotifyDeletePhotoAfterMs;\n\t\t\tclearInMs(-kNotifyDeletePhotoAfterMs);\n\t\t} else {\n\t\t\tv.until = 0;\n\t\t}\n\t\tv.path = u\"%1tdata/temp/%2.png\"_q.arg(\n\t\t\tcWorkingDir(),\n\t\t\tQString::number(base::RandomValue<uint64>(), 16));\n\t\tif (key.first || key.second) {\n\t\t\tGenerateUserpic(peer, view).save(v.path, \"PNG\");\n\t\t} else {\n\t\t\tLogoNoMargin().save(v.path, \"PNG\");\n\t\t}\n\t\ti = _images.insert(key, v);\n\t\t_someSavedFlag = true;\n\t}\n\treturn i->path;\n}\n\ncrl::time CachedUserpics::clear(crl::time ms) {\n\tcrl::time result = 0;\n\tfor (auto i = _images.begin(); i != _images.end();) {\n\t\tif (!i->until) {\n\t\t\t++i;\n\t\t\tcontinue;\n\t\t}\n\t\tif (i->until <= ms) {\n\t\t\tQFile(i->path).remove();\n\t\t\ti = _images.erase(i);\n\t\t} else {\n\t\t\tif (!result) {\n\t\t\t\tresult = i->until;\n\t\t\t} else {\n\t\t\t\taccumulate_min(result, i->until);\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid CachedUserpics::clearInMs(int ms) {\n\tif (ms < 0) {\n\t\tms = -ms;\n\t\tif (_clearTimer.isActive() && _clearTimer.remainingTime() <= ms) {\n\t\t\treturn;\n\t\t}\n\t}\n\t_clearTimer.callOnce(ms);\n}\n\nvoid CachedUserpics::clear() {\n\tauto ms = crl::now();\n\tauto minuntil = clear(ms);\n\tif (minuntil) {\n\t\tclearInMs(int(minuntil - ms));\n\t}\n}\n\n} // namespace Window::Notifications\n"
  },
  {
    "path": "Telegram/SourceFiles/window/notifications_utilities.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"window/notifications_manager.h\"\n#include \"base/timer.h\"\n\nnamespace Ui {\nstruct PeerUserpicView;\n} // namespace Ui\n\nnamespace Window::Notifications {\n\n[[nodiscard]] QImage GenerateUserpic(\n\tnot_null<PeerData*> peer,\n\tUi::PeerUserpicView &view);\n\nclass CachedUserpics : public QObject {\npublic:\n\tCachedUserpics();\n\t~CachedUserpics();\n\n\t[[nodiscard]] QString get(\n\t\tconst InMemoryKey &key,\n\t\tnot_null<PeerData*> peer,\n\t\tUi::PeerUserpicView &view);\n\nprivate:\n\tvoid clear();\n\tvoid clearInMs(int ms);\n\tcrl::time clear(crl::time ms);\n\n\tstruct Image {\n\t\tcrl::time until = 0;\n\t\tQString path;\n\t};\n\tusing Images = QMap<InMemoryKey, Image>;\n\tImages _images;\n\tbool _someSavedFlag = false;\n\tbase::Timer _clearTimer;\n\n};\n\n} // namespace Window::Notifications\n"
  },
  {
    "path": "Telegram/SourceFiles/window/section_memento.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass LayerWidget;\n} // namespace Ui\n\nnamespace Data {\nclass ForumTopic;\nclass SavedSublist;\n} // namespace Data\n\nnamespace Window {\n\nclass SessionController;\nclass SectionWidget;\nenum class Column;\n\nclass SectionMemento {\npublic:\n\t[[nodiscard]] virtual object_ptr<SectionWidget> createWidget(\n\t\tQWidget *parent,\n\t\tnot_null<SessionController*> controller,\n\t\tColumn column,\n\t\tconst QRect &geometry) = 0;\n\n\t[[nodiscard]] virtual object_ptr<Ui::LayerWidget> createLayer(\n\t\t\tnot_null<SessionController*> controller,\n\t\t\tconst QRect &geometry) {\n\t\treturn nullptr;\n\t}\n\t[[nodiscard]] virtual bool instant() const {\n\t\treturn false;\n\t}\n\n\t[[nodiscard]] virtual Data::ForumTopic *topicForRemoveRequests() const {\n\t\treturn nullptr;\n\t}\n\t[[nodiscard]] virtual auto sublistForRemoveRequests() const\n\t-> Data::SavedSublist* {\n\t\treturn nullptr;\n\t}\n\t[[nodiscard]] virtual rpl::producer<> removeRequests() const {\n\t\treturn rpl::never<>();\n\t}\n\n\tvirtual ~SectionMemento() = default;\n\n};\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/section_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/section_widget.h\"\n\n#include \"mainwidget.h\"\n#include \"mainwindow.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/painter.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_user.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_session.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"data/data_message_reactions.h\"\n#include \"data/data_peer_values.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"main/main_session.h\"\n#include \"window/section_memento.h\"\n#include \"window/window_slide_animation.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/themes/window_theme.h\"\n\n#include <rpl/range.h>\n\nnamespace Window {\nnamespace {\n\n[[nodiscard]] rpl::producer<QString> PeerThemeTokenValue(\n\t\tnot_null<PeerData*> peer) {\n\treturn peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::ChatThemeToken\n\t) | rpl::map([=] {\n\t\treturn peer->themeToken();\n\t});\n}\n\nstruct ResolvedPaper {\n\tData::WallPaper paper;\n\tstd::shared_ptr<Data::DocumentMedia> media;\n};\n\n[[nodiscard]] rpl::producer<const Data::WallPaper*> PeerWallPaperMapped(\n\t\tnot_null<PeerData*> peer) {\n\treturn peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::ChatWallPaper\n\t) | rpl::map([=]() -> rpl::producer<const Data::WallPaper*> {\n\t\treturn WallPaperResolved(&peer->owner(), peer->wallPaper());\n\t}) | rpl::flatten_latest();\n}\n\n[[nodiscard]] rpl::producer<std::optional<ResolvedPaper>> PeerWallPaperValue(\n\t\tnot_null<PeerData*> peer) {\n\treturn PeerWallPaperMapped(\n\t\tpeer\n\t) | rpl::map([=](const Data::WallPaper *paper)\n\t-> rpl::producer<std::optional<ResolvedPaper>> {\n\t\tconst auto single = [](std::optional<ResolvedPaper> value) {\n\t\t\treturn rpl::single(std::move(value));\n\t\t};\n\t\tif (!paper) {\n\t\t\treturn single({});\n\t\t}\n\t\tconst auto document = paper->document();\n\t\tauto value = ResolvedPaper{\n\t\t\t*paper,\n\t\t\tdocument ? document->createMediaView() : nullptr,\n\t\t};\n\t\tif (!value.media || value.media->loaded(true)) {\n\t\t\treturn single(std::move(value));\n\t\t}\n\t\tpaper->loadDocument();\n\t\treturn single(\n\t\t\tvalue\n\t\t) | rpl::then(document->session().downloaderTaskFinished(\n\t\t) | rpl::filter([=] {\n\t\t\treturn value.media->loaded(true);\n\t\t}) | rpl::take(1) | rpl::map_to(\n\t\t\tstd::optional<ResolvedPaper>(value)\n\t\t));\n\t}) | rpl::flatten_latest();\n}\n\n[[nodiscard]] auto MaybeChatThemeDataValueFromPeer(\n\tnot_null<PeerData*> peer)\n-> rpl::producer<std::optional<Data::CloudTheme>> {\n\treturn PeerThemeTokenValue(\n\t\tpeer\n\t) | rpl::map([=](const QString &token)\n\t-> rpl::producer<std::optional<Data::CloudTheme>> {\n\t\treturn peer->owner().cloudThemes().themeForTokenValue(token);\n\t}) | rpl::flatten_latest();\n}\n\n[[nodiscard]] rpl::producer<> DebouncedPaletteValue() {\n\treturn [=](auto consumer) {\n\t\tauto lifetime = rpl::lifetime();\n\n\t\tstruct State {\n\t\t\tbase::has_weak_ptr guard;\n\t\t\tbool scheduled = false;\n\t\t};\n\t\tconst auto state = lifetime.make_state<State>();\n\n\t\tconsumer.put_next_copy(rpl::empty);\n\t\tstyle::PaletteChanged(\n\t\t) | rpl::on_next([=] {\n\t\t\tif (state->scheduled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstate->scheduled = true;\n\t\t\tUi::PostponeCall(&state->guard, [=] {\n\t\t\t\tstate->scheduled = false;\n\t\t\t\tconsumer.put_next_copy(rpl::empty);\n\t\t\t});\n\t\t}, lifetime);\n\n\t\treturn lifetime;\n\t};\n}\n\nstruct ResolvedTheme {\n\tstd::optional<Data::CloudTheme> theme;\n\tstd::optional<ResolvedPaper> paper;\n\tbool dark = false;\n};\n\n[[nodiscard]] auto MaybeCloudThemeValueFromPeer(\n\tnot_null<PeerData*> peer)\n-> rpl::producer<ResolvedTheme> {\n\treturn rpl::combine(\n\t\tMaybeChatThemeDataValueFromPeer(peer),\n\t\tPeerWallPaperValue(peer),\n\t\tTheme::IsThemeDarkValue() | rpl::distinct_until_changed()\n\t) | rpl::map([](\n\t\t\tstd::optional<Data::CloudTheme> theme,\n\t\t\tstd::optional<ResolvedPaper> paper,\n\t\t\tbool night) -> rpl::producer<ResolvedTheme> {\n\t\tif (theme || !paper) {\n\t\t\treturn rpl::single<ResolvedTheme>({\n\t\t\t\tstd::move(theme),\n\t\t\t\tstd::move(paper),\n\t\t\t\tnight,\n\t\t\t});\n\t\t}\n\t\treturn DebouncedPaletteValue(\n\t\t) | rpl::map([=] {\n\t\t\treturn ResolvedTheme{\n\t\t\t\t.paper = paper,\n\t\t\t\t.dark = night,\n\t\t\t};\n\t\t});\n\t}) | rpl::flatten_latest();\n}\n\n} // namespace\n\nrpl::producer<const Data::WallPaper*> WallPaperResolved(\n\t\tnot_null<Data::Session*> owner,\n\t\tconst Data::WallPaper *paper) {\n\tconst auto id = paper ? paper->emojiId() : QString();\n\tif (id.isEmpty()) {\n\t\treturn rpl::single(paper);\n\t}\n\tconst auto themes = &owner->cloudThemes();\n\tauto fromThemes = [=](bool force)\n\t-> rpl::producer<const Data::WallPaper*> {\n\t\tif (themes->chatThemes().empty() && !force) {\n\t\t\treturn nullptr;\n\t\t}\n\t\treturn Window::Theme::IsNightModeValue(\n\t\t) | rpl::map([=](bool dark) -> const Data::WallPaper* {\n\t\t\tconst auto &list = themes->chatThemes();\n\t\t\tconst auto i = ranges::find(\n\t\t\t\tlist,\n\t\t\t\tid,\n\t\t\t\t&Data::CloudTheme::emoticon);\n\t\t\tif (i != end(list)) {\n\t\t\t\tusing Type = Data::CloudThemeType;\n\t\t\t\tconst auto type = dark ? Type::Dark : Type::Light;\n\t\t\t\tconst auto j = i->settings.find(type);\n\t\t\t\tif (j != end(i->settings) && j->second.paper) {\n\t\t\t\t\treturn &*j->second.paper;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nullptr;\n\t\t});\n\t};\n\tif (auto result = fromThemes(false)) {\n\t\treturn result;\n\t}\n\tthemes->refreshChatThemes();\n\treturn rpl::single<const Data::WallPaper*>(\n\t\tnullptr\n\t) | rpl::then(themes->chatThemesUpdated(\n\t) | rpl::take(1) | rpl::map([=] {\n\t\treturn fromThemes(true);\n\t}) | rpl::flatten_latest());\n}\n\nAbstractSectionWidget::AbstractSectionWidget(\n\tQWidget *parent,\n\tnot_null<SessionController*> controller,\n\trpl::producer<PeerData*> peerForBackground)\n: RpWidget(parent)\n, _controller(controller) {\n\tstd::move(\n\t\tpeerForBackground\n\t) | rpl::map([=](PeerData *peer) -> rpl::producer<> {\n\t\tif (!peer) {\n\t\t\treturn rpl::single(rpl::empty) | rpl::then(\n\t\t\t\tcontroller->defaultChatTheme()->repaintBackgroundRequests()\n\t\t\t);\n\t\t}\n\t\treturn ChatThemeValueFromPeer(\n\t\t\tcontroller,\n\t\t\tpeer\n\t\t) | rpl::map([](const std::shared_ptr<Ui::ChatTheme> &theme) {\n\t\t\treturn rpl::single(rpl::empty) | rpl::then(\n\t\t\t\ttheme->repaintBackgroundRequests()\n\t\t\t);\n\t\t}) | rpl::flatten_latest();\n\t}) | rpl::flatten_latest() | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n}\n\nMain::Session &AbstractSectionWidget::session() const {\n\treturn _controller->session();\n}\n\nSectionWidget::SectionWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\trpl::producer<PeerData*> peerForBackground)\n: AbstractSectionWidget(parent, controller, std::move(peerForBackground)) {\n}\n\nSectionWidget::SectionWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peerForBackground)\n: AbstractSectionWidget(\n\tparent,\n\tcontroller,\n\trpl::single(peerForBackground.get())) {\n}\n\nvoid SectionWidget::setGeometryWithTopMoved(\n\t\tconst QRect &newGeometry,\n\t\tint topDelta) {\n\t_topDelta = topDelta;\n\tbool willBeResized = (size() != newGeometry.size());\n\tif (geometry() != newGeometry) {\n\t\tauto weak = base::make_weak(this);\n\t\tsetGeometry(newGeometry);\n\t\tif (!weak) {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (!willBeResized) {\n\t\tresizeEvent(nullptr);\n\t}\n\t_topDelta = 0;\n}\n\nvoid SectionWidget::showAnimated(\n\t\tSlideDirection direction,\n\t\tconst SectionSlideParams &params) {\n\tvalidateSubsectionTabs();\n\tif (_showAnimation) {\n\t\treturn;\n\t}\n\n\tshowChildren();\n\tauto myContentCache = grabForShowAnimation(params);\n\thideChildren();\n\tshowAnimatedHook(params);\n\n\t_showAnimation = std::make_unique<SlideAnimation>();\n\t_showAnimation->setDirection(direction);\n\t_showAnimation->setRepaintCallback([this] { update(); });\n\t_showAnimation->setFinishedCallback([this] { showFinished(); });\n\t_showAnimation->setPixmaps(\n\t\tparams.oldContentCache,\n\t\tmyContentCache);\n\t_showAnimation->setTopBarShadow(params.withTopBarShadow);\n\t_showAnimation->setWithFade(params.withFade);\n\t_showAnimation->setTopSkip(params.topSkip);\n\t_showAnimation->setTopBarMask(params.topMask);\n\t_showAnimation->start();\n\n\tshow();\n}\n\nstd::shared_ptr<SectionMemento> SectionWidget::createMemento() {\n\treturn nullptr;\n}\n\nvoid SectionWidget::showFast() {\n\tvalidateSubsectionTabs();\n\tshow();\n\tshowFinished();\n}\n\nQPixmap SectionWidget::grabForShowAnimation(\n\t\tconst SectionSlideParams &params) {\n\treturn Ui::GrabWidget(this);\n}\n\nvoid SectionWidget::PaintBackground(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Ui::ChatTheme*> theme,\n\t\tnot_null<QWidget*> widget,\n\t\tQRect clip) {\n\tif (const auto id = theme->background().giftId) {\n\t\tconst auto fillHeight = controller->content()->height();\n\t\tconst auto fill = QSize(widget->width(), fillHeight);\n\t\tconst auto &state = theme->backgroundState(fill);\n\t\tconst auto make = [&] {\n\t\t\treturn std::make_unique<Ui::Text::LimitedLoopsEmoji>(\n\t\t\t\tcontroller->session().data().customEmojiManager().create(\n\t\t\t\t\tid,\n\t\t\t\t\tcrl::guard(widget, [=] { widget->update(); }),\n\t\t\t\t\tData::CustomEmojiSizeTag::Isolated),\n\t\t\t\t1);\n\t\t};\n\t\tif (!state.was.gift) {\n\t\t\tstate.was.gift = make();\n\t\t}\n\t\tif (!state.now.gift) {\n\t\t\tstate.now.gift = make();\n\t\t}\n\t}\n\n\tPaintBackground(\n\t\ttheme,\n\t\twidget,\n\t\tcontroller->content()->height(),\n\t\tcontroller->content()->backgroundFromY(),\n\t\tclip,\n\t\tcontroller->isGifPausedAtLeastFor(GifPauseReason::Any));\n}\n\nvoid SectionWidget::PaintBackground(\n\t\tnot_null<Ui::ChatTheme*> theme,\n\t\tnot_null<QWidget*> widget,\n\t\tint fillHeight,\n\t\tint fromy,\n\t\tQRect clip,\n\t\tbool paused) {\n\tauto p = QPainter(widget);\n\tif (fromy) {\n\t\tp.translate(0, fromy);\n\t\tclip = clip.translated(0, -fromy);\n\t}\n\tconst auto fill = QSize(widget->width(), fillHeight);\n\tPaintBackground(p, theme, fill, clip, paused);\n}\n\nvoid SectionWidget::PaintBackground(\n\t\tQPainter &p,\n\t\tnot_null<Ui::ChatTheme*> theme,\n\t\tQSize fill,\n\t\tQRect clip,\n\t\tbool paused) {\n\tconst auto &background = theme->background();\n\tif (background.colorForFill) {\n\t\tp.fillRect(clip, *background.colorForFill);\n\t\treturn;\n\t}\n\tconst auto &gradient = background.gradientForFill;\n\tconst auto &state = theme->backgroundState(fill);\n\tconst auto paintCache = [&](const Ui::CachedBackground &cache) {\n\t\tconst auto to = QRect(\n\t\t\tQPoint(cache.x, cache.y),\n\t\t\tcache.pixmap.size() / style::DevicePixelRatio());\n\t\tconst auto paintGift = [&](QRect area) {\n\t\t\tif (!cache.gift) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto hq = PainterHighQualityEnabler(p);\n\t\t\tconst auto center = area.center();\n\t\t\tconst auto size = Data::FrameSizeFromTag(\n\t\t\t\tData::CustomEmojiSizeTag::Isolated\n\t\t\t) / style::DevicePixelRatio();\n\t\t\tp.save();\n\t\t\tp.translate(center);\n\t\t\tp.rotate(cache.giftRotation);\n\t\t\tp.translate(-center);\n\t\t\tp.setOpacity(0.5);\n\t\t\tcache.gift->paint(p, {\n\t\t\t\t.textColor = st::windowFg->c,\n\t\t\t\t.size = QSize(size, size),\n\t\t\t\t.now = crl::now(),\n\t\t\t\t.scale = (area.width() / float64(size)),\n\t\t\t\t.position = area.topLeft(),\n\t\t\t\t.paused = paused,\n\t\t\t\t.scaled = true,\n\t\t\t});\n\t\t\tp.restore();\n\t\t};\n\t\tif (cache.waitingForNegativePattern) {\n\t\t\t// While we wait for pattern being loaded we paint just gradient.\n\t\t\t// But in case of negative patter opacity we just fill-black.\n\t\t\tp.fillRect(to, Qt::black);\n\t\t} else if (cache.area == fill) {\n\t\t\tp.drawPixmap(to, cache.pixmap);\n\t\t\tif (background.giftId && !cache.giftArea.isEmpty()) {\n\t\t\t\tpaintGift(cache.giftArea.translated(to.topLeft()));\n\t\t\t}\n\t\t} else {\n\t\t\tconst auto sx = fill.width() / float64(cache.area.width());\n\t\t\tconst auto sy = fill.height() / float64(cache.area.height());\n\t\t\tconst auto round = [](float64 value) -> int {\n\t\t\t\treturn (value >= 0.)\n\t\t\t\t\t? int(std::ceil(value))\n\t\t\t\t\t: int(std::floor(value));\n\t\t\t};\n\t\t\tconst auto sto = QPoint(round(to.x() * sx), round(to.y() * sy));\n\t\t\tp.drawPixmap(\n\t\t\t\tsto.x(),\n\t\t\t\tsto.y(),\n\t\t\t\tround((to.x() + to.width()) * sx) - sto.x(),\n\t\t\t\tround((to.y() + to.height()) * sy) - sto.y(),\n\t\t\t\tcache.pixmap);\n\t\t\tif (background.giftId && !cache.giftArea.isEmpty()) {\n\t\t\t\tpaintGift(QRect(\n\t\t\t\t\t(to.x() + cache.giftArea.x()) * sx,\n\t\t\t\t\t(to.y() + cache.giftArea.y()) * sy,\n\t\t\t\t\tcache.giftArea.width() * sx,\n\t\t\t\t\tcache.giftArea.height() * sy));\n\t\t\t}\n\t\t}\n\t};\n\tconst auto hasNow = !state.now.pixmap.isNull();\n\tconst auto goodNow = hasNow && (state.now.area == fill);\n\tconst auto useCache = goodNow || !gradient.isNull();\n\tif (useCache) {\n\t\tconst auto fade = (state.shown < 1. && !gradient.isNull());\n\t\tif (fade) {\n\t\t\tpaintCache(state.was);\n\t\t\tp.setOpacity(state.shown);\n\t\t}\n\t\tpaintCache(state.now);\n\t\tif (fade) {\n\t\t\tp.setOpacity(1.);\n\t\t}\n\t\treturn;\n\t}\n\tconst auto &prepared = background.prepared;\n\tif (prepared.isNull()) {\n\t\treturn;\n\t} else if (background.isPattern) {\n\t\tconst auto w = prepared.width() * fill.height() / prepared.height();\n\t\tconst auto cx = qCeil(fill.width() / float64(w));\n\t\tconst auto cols = (cx / 2) * 2 + 1;\n\t\tconst auto xshift = (fill.width() - w * cols) / 2;\n\t\tfor (auto i = 0; i != cols; ++i) {\n\t\t\tp.drawImage(\n\t\t\t\tQRect(xshift + i * w, 0, w, fill.height()),\n\t\t\t\tprepared,\n\t\t\t\tQRect(QPoint(), prepared.size()));\n\t\t}\n\t} else if (background.tile) {\n\t\tconst auto &tiled = background.preparedForTiled;\n\t\tconst auto left = clip.left();\n\t\tconst auto top = clip.top();\n\t\tconst auto right = clip.left() + clip.width();\n\t\tconst auto bottom = clip.top() + clip.height();\n\t\tconst auto w = tiled.width() / float64(style::DevicePixelRatio());\n\t\tconst auto h = tiled.height() / float64(style::DevicePixelRatio());\n\t\tconst auto sx = qFloor(left / w);\n\t\tconst auto sy = qFloor(top / h);\n\t\tconst auto cx = qCeil(right / w);\n\t\tconst auto cy = qCeil(bottom / h);\n\t\tfor (auto i = sx; i < cx; ++i) {\n\t\t\tfor (auto j = sy; j < cy; ++j) {\n\t\t\t\tp.drawImage(QPointF(i * w, j * h), tiled);\n\t\t\t}\n\t\t}\n\t} else {\n\t\tconst auto hq = PainterHighQualityEnabler(p);\n\t\tconst auto rects = Ui::ComputeChatBackgroundRects(\n\t\t\tfill,\n\t\t\tprepared.size());\n\t\tp.drawImage(rects.to, prepared, rects.from);\n\t}\n}\n\nvoid SectionWidget::paintEvent(QPaintEvent *e) {\n\tif (_showAnimation) {\n\t\tauto p = QPainter(this);\n\t\t_showAnimation->paintContents(p);\n\t}\n}\n\nbool SectionWidget::animatingShow() const {\n\treturn (_showAnimation != nullptr);\n}\n\nvoid SectionWidget::showFinished() {\n\t_showAnimation.reset();\n\tif (isHidden()) return;\n\n\tshowChildren();\n\tshowFinishedHook();\n\n\tif (isAncestorOf(window()->focusWidget())) {\n\t\tsetInnerFocus();\n\t} else {\n\t\tcontroller()->widget()->setInnerFocus();\n\t}\n}\n\nrpl::producer<int> SectionWidget::desiredHeight() const {\n\treturn rpl::single(height());\n}\n\nSectionWidget::~SectionWidget() = default;\n\nauto ChatThemeValueFromPeer(\n\tnot_null<SessionController*> controller,\n\tnot_null<PeerData*> peer)\n-> rpl::producer<std::shared_ptr<Ui::ChatTheme>> {\n\tauto cloud = MaybeCloudThemeValueFromPeer(\n\t\tpeer\n\t) | rpl::map([=](ResolvedTheme resolved)\n\t-> rpl::producer<std::shared_ptr<Ui::ChatTheme>> {\n\t\tif (!resolved.theme && !resolved.paper) {\n\t\t\treturn rpl::single(controller->defaultChatTheme());\n\t\t}\n\t\tconst auto theme = resolved.theme.value_or(Data::CloudTheme());\n\t\tconst auto paper = resolved.paper\n\t\t\t? resolved.paper->paper\n\t\t\t: Data::WallPaper(0);\n\t\tconst auto type = resolved.dark\n\t\t\t? Data::CloudThemeType::Dark\n\t\t\t: Data::CloudThemeType::Light;\n\t\tif (paper.document()\n\t\t\t&& resolved.paper->media\n\t\t\t&& !resolved.paper->media->loaded()\n\t\t\t&& !controller->chatThemeAlreadyCached(theme, paper, type)) {\n\t\t\treturn rpl::single(controller->defaultChatTheme());\n\t\t}\n\t\treturn controller->cachedChatThemeValue(theme, paper, type);\n\t}) | rpl::flatten_latest(\n\t) | rpl::distinct_until_changed();\n\n\treturn rpl::combine(\n\t\tstd::move(cloud),\n\t\tcontroller->peerThemeOverrideValue()\n\t) | rpl::map([=](\n\t\t\tstd::shared_ptr<Ui::ChatTheme> &&cloud,\n\t\t\tPeerThemeOverride &&overriden) {\n\t\treturn (overriden.peer == peer.get()\n\t\t\t&& peer->themeToken() != overriden.token)\n\t\t\t? std::move(overriden.theme)\n\t\t\t: std::move(cloud);\n\t});\n}\n\nbool ShowSendPremiumError(\n\t\tnot_null<SessionController*> controller,\n\t\tnot_null<DocumentData*> document) {\n\treturn ShowSendPremiumError(controller->uiShow(), document);\n}\n\nbool ShowSendPremiumError(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<DocumentData*> document) {\n\tif (!document->isPremiumSticker()\n\t\t|| document->session().premium()) {\n\t\treturn false;\n\t}\n\tShowStickerPreviewBox(std::move(show), document);\n\treturn true;\n}\n\nbool ShowReactPremiumError(\n\t\tnot_null<SessionController*> controller,\n\t\tnot_null<HistoryItem*> item,\n\t\tconst Data::ReactionId &id) {\n\tif (item->reactionsAreTags()) {\n\t\tif (controller->session().premium()) {\n\t\t\treturn false;\n\t\t}\n\t\tShowPremiumPreviewBox(controller, PremiumFeature::TagsForMessages);\n\t\treturn true;\n\t} else if (controller->session().premium()\n\t\t|| ranges::contains(item->chosenReactions(), id)\n\t\t|| item->history()->peer->isBroadcast()) {\n\t\treturn false;\n\t} else if (!id.custom()) {\n\t\treturn false;\n\t}\n\tShowPremiumPreviewBox(controller, PremiumFeature::InfiniteReactions);\n\treturn true;\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/section_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"chat_helpers/bot_command.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"media/player/media_player_float.h\" // FloatSectionDelegate\n#include \"base/object_ptr.h\"\n#include \"window/window_section_common.h\"\n\nclass PeerData;\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace Data {\nstruct ReactionId;\nclass ForumTopic;\nclass WallPaper;\nclass Session;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass LayerWidget;\nclass ChatTheme;\n} // namespace Ui\n\nnamespace Window {\n\nclass SessionController;\nclass SlideAnimation;\nstruct SectionShow;\nenum class SlideDirection;\n\nenum class Column {\n\tFirst,\n\tSecond,\n\tThird,\n};\n\nclass AbstractSectionWidget\n\t: public Ui::RpWidget\n\t, public Media::Player::FloatSectionDelegate {\npublic:\n\tAbstractSectionWidget(\n\t\tQWidget *parent,\n\t\tnot_null<SessionController*> controller,\n\t\trpl::producer<PeerData*> peerForBackground);\n\n\t[[nodiscard]] Main::Session &session() const;\n\t[[nodiscard]] not_null<SessionController*> controller() const {\n\t\treturn _controller;\n\t}\n\n\t// Tabbed selector management.\n\tvirtual bool pushTabbedSelectorToThirdSection(\n\t\t\tnot_null<Data::Thread*> thread,\n\t\t\tconst SectionShow &params) {\n\t\treturn false;\n\t}\n\tvirtual bool returnTabbedSelector() {\n\t\treturn false;\n\t}\n\nprivate:\n\tconst not_null<SessionController*> _controller;\n\n};\n\nclass SectionMemento;\n\nstruct SectionSlideParams {\n\tQPixmap oldContentCache;\n\tint topSkip = 0;\n\tQPixmap topMask;\n\tbool withTopBarShadow = false;\n\tbool withTabs = false;\n\tbool withFade = false;\n\n\texplicit operator bool() const {\n\t\treturn !oldContentCache.isNull();\n\t}\n};\n\nclass SectionWidget : public AbstractSectionWidget {\npublic:\n\tSectionWidget(\n\t\tQWidget *parent,\n\t\tnot_null<SessionController*> controller,\n\t\trpl::producer<PeerData*> peerForBackground = nullptr);\n\tSectionWidget(\n\t\tQWidget *parent,\n\t\tnot_null<SessionController*> controller,\n\t\tnot_null<PeerData*> peerForBackground);\n\n\tvirtual Dialogs::RowDescriptor activeChat() const {\n\t\treturn {};\n\t}\n\n\t// When resizing the widget with top edge moved up or down and we\n\t// want to add this top movement to the scroll position, so inner\n\t// content will not move.\n\tvoid setGeometryWithTopMoved(const QRect &newGeometry, int topDelta);\n\n\tvirtual bool hasTopBarShadow() const {\n\t\treturn false;\n\t}\n\tvirtual bool forceAnimateBack() const {\n\t\treturn false;\n\t}\n\tvoid showAnimated(\n\t\tSlideDirection direction,\n\t\tconst SectionSlideParams &params);\n\tvoid showFast();\n\t[[nodiscard]] bool animatingShow() const;\n\n\t// This can be used to grab with or without top bar shadow.\n\t// This will be protected when animation preparation will be done inside.\n\tvirtual QPixmap grabForShowAnimation(const SectionSlideParams &params);\n\n\t// Attempt to show the required section inside the existing one.\n\t// For example if this section already shows exactly the required\n\t// memento it can simply return true - it is shown already.\n\t//\n\t// If this method returns false it is not supposed to modify the memento.\n\t// If this method returns true it may modify the memento (\"take\" heavy items).\n\tvirtual bool showInternal(\n\t\tnot_null<SectionMemento*> memento,\n\t\tconst SectionShow &params) = 0;\n\tvirtual bool sameTypeAs(not_null<SectionMemento*> memento) {\n\t\treturn false;\n\t}\n\n\tvirtual bool showMessage(\n\t\t\tPeerId peerId,\n\t\t\tconst SectionShow &params,\n\t\t\tMsgId messageId) {\n\t\treturn false;\n\t}\n\tvirtual bool searchInChatEmbedded(\n\t\t\tQString query,\n\t\t\tDialogs::Key chat,\n\t\t\tPeerData *searchFrom = nullptr) {\n\t\treturn false;\n\t}\n\n\t[[nodiscard]] virtual bool preventsClose(\n\t\t\tFn<void()> &&continueCallback) const {\n\t\treturn false;\n\t}\n\n\t// Send bot command from peer info or media viewer.\n\tvirtual SectionActionResult sendBotCommand(\n\t\t\tBot::SendCommandRequest request) {\n\t\treturn SectionActionResult::Ignore;\n\t}\n\n\tvirtual bool confirmSendingFiles(const QStringList &files) {\n\t\treturn false;\n\t}\n\tvirtual bool confirmSendingFiles(not_null<const QMimeData*> data) {\n\t\treturn false;\n\t}\n\n\t// Create a memento of that section to store it in the history stack.\n\t// This method may modify the section (\"take\" heavy items).\n\tvirtual std::shared_ptr<SectionMemento> createMemento();\n\n\tvoid setInnerFocus() {\n\t\tdoSetInnerFocus();\n\t}\n\tvirtual void checkActivation() {\n\t}\n\n\t[[nodiscard]] virtual rpl::producer<int> desiredHeight() const;\n\t[[nodiscard]] virtual rpl::producer<> removeRequests() const {\n\t\treturn rpl::never<>();\n\t}\n\n\t// Some sections convert to layers on some geometry sizes.\n\t[[nodiscard]] virtual object_ptr<Ui::LayerWidget> moveContentToLayer(\n\t\t\tQRect bodyGeometry) {\n\t\treturn nullptr;\n\t}\n\n\tvirtual void validateSubsectionTabs() {\n\t}\n\n\tstatic void PaintBackground(\n\t\tnot_null<SessionController*> controller,\n\t\tnot_null<Ui::ChatTheme*> theme,\n\t\tnot_null<QWidget*> widget,\n\t\tQRect clip);\n\tstatic void PaintBackground(\n\t\tnot_null<Ui::ChatTheme*> theme,\n\t\tnot_null<QWidget*> widget,\n\t\tint fillHeight,\n\t\tint fromy,\n\t\tQRect clip,\n\t\tbool paused = false);\n\tstatic void PaintBackground(\n\t\tQPainter &p,\n\t\tnot_null<Ui::ChatTheme*> theme,\n\t\tQSize fill,\n\t\tQRect clip,\n\t\tbool paused = false);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\t// Temp variable used in resizeEvent() implementation, that is passed\n\t// to setGeometryWithTopMoved() to adjust the scroll position with the resize.\n\tint topDelta() const {\n\t\treturn _topDelta;\n\t}\n\n\t// Called after the hideChildren() call in showAnimated().\n\tvirtual void showAnimatedHook(\n\t\tconst SectionSlideParams &params) {\n\t}\n\n\t// Called after the showChildren() call in showFinished().\n\tvirtual void showFinishedHook() {\n\t}\n\n\tvirtual void doSetInnerFocus() {\n\t\tsetFocus();\n\t}\n\n\t~SectionWidget();\n\nprivate:\n\tvoid showFinished();\n\n\tstd::unique_ptr<SlideAnimation> _showAnimation;\n\n\t// Saving here topDelta in setGeometryWithTopMoved() to get it passed to resizeEvent().\n\tint _topDelta = 0;\n\n};\n\n[[nodiscard]] auto ChatThemeValueFromPeer(\n\tnot_null<SessionController*> controller,\n\tnot_null<PeerData*> peer)\n-> rpl::producer<std::shared_ptr<Ui::ChatTheme>>;\n\n[[nodiscard]] bool ShowSendPremiumError(\n\tnot_null<SessionController*> controller,\n\tnot_null<DocumentData*> document);\n[[nodiscard]] bool ShowSendPremiumError(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<DocumentData*> document);\n\n[[nodiscard]] bool ShowReactPremiumError(\n\tnot_null<SessionController*> controller,\n\tnot_null<HistoryItem*> item,\n\tconst Data::ReactionId &id);\n\n[[nodiscard]] rpl::producer<const Data::WallPaper*> WallPaperResolved(\n\tnot_null<Data::Session*> owner,\n\tconst Data::WallPaper *paper);\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/session/window_session_media.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_session_controller.h\"\n\n#include \"apiwrap.h\"\n#include \"boxes/send_files_box.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/history_view_draw_to_reply.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"mainwidget.h\"\n#include \"data/data_session.h\"\n#include \"data/data_thread.h\"\n#include \"storage/localimageloader.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"styles/style_boxes.h\"\n#include \"window/window_separate_id.h\"\n\nnamespace Window {\n\nvoid SessionController::handleDrawToReplyRequest(\n\t\tData::DrawToReplyRequest request) {\n\tif (content()->handleDrawToReplyRequest(request)) {\n\t\treturn;\n\t}\n\tauto image = HistoryView::ResolveDrawToReplyImage(\n\t\t&session().data(),\n\t\trequest);\n\tif (image.isNull()) {\n\t\treturn;\n\t}\n\tHistoryView::OpenDrawToReplyEditor(\n\t\tthis,\n\t\tstd::move(image),\n\t\tcrl::guard(this, [=](QImage &&result) {\n\t\t\tif (result.isNull()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto thread = resolveDrawToReplyThread(request);\n\t\t\tif (!thread) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto list = Storage::PrepareMediaFromImage(\n\t\t\t\tstd::move(result),\n\t\t\t\tQByteArray(),\n\t\t\t\tst::sendMediaPreviewSize);\n\t\t\tshowDrawToReplyFilesBox(\n\t\t\t\tthread,\n\t\t\t\trequest.messageId,\n\t\t\t\tstd::move(list));\n\t\t}));\n}\n\nData::Thread *SessionController::resolveDrawToReplyThread(\n\t\tconst Data::DrawToReplyRequest &request) const {\n\tif (const auto item = session().data().message(request.messageId)) {\n\t\tif (const auto topic = item->topic()) {\n\t\t\treturn topic;\n\t\t} else if (const auto sublist = item->savedSublist()) {\n\t\t\treturn sublist;\n\t\t}\n\t\treturn item->history();\n\t}\n\tif (const auto thread = activeChatCurrent().thread()) {\n\t\tif (thread->peer()->id == request.messageId.peer) {\n\t\t\treturn thread;\n\t\t}\n\t}\n\tif (const auto thread = windowId().thread) {\n\t\tif (thread->peer()->id == request.messageId.peer) {\n\t\t\treturn thread;\n\t\t}\n\t}\n\treturn session().data().historyLoaded(request.messageId.peer);\n}\n\nvoid SessionController::showDrawToReplyFilesBox(\n\t\tnot_null<Data::Thread*> thread,\n\t\tFullMsgId replyTo,\n\t\tUi::PreparedList &&list) {\n\tconst auto weak = base::make_weak(thread);\n\tconst auto peer = thread->peer();\n\tconst auto show = uiShow();\n\tshow->show(Box<SendFilesBox>(SendFilesBoxDescriptor{\n\t\t.show = show,\n\t\t.list = std::move(list),\n\t\t.caption = TextWithTags(),\n\t\t.toPeer = peer,\n\t\t.limits = DefaultLimitsForPeer(peer),\n\t\t.check = DefaultCheckForPeer(show, peer),\n\t\t.sendType = Api::SendType::Normal,\n\t\t.confirmed = crl::guard(this, [=](\n\t\t\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\t\t\tApi::SendOptions options) {\n\t\t\tif (const auto thread = weak.get()) {\n\t\t\t\tsendDrawToReplyFiles(\n\t\t\t\t\tthread,\n\t\t\t\t\treplyTo,\n\t\t\t\t\tstd::move(bundle),\n\t\t\t\t\toptions);\n\t\t\t}\n\t\t}),\n\t}));\n}\n\nvoid SessionController::sendDrawToReplyFiles(\n\t\tnot_null<Data::Thread*> thread,\n\t\tFullMsgId replyTo,\n\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\tApi::SendOptions options) {\n\tif (!bundle) {\n\t\treturn;\n\t}\n\tconst auto type = bundle->way.sendImagesAsPhotos()\n\t\t? SendMediaType::Photo\n\t\t: SendMediaType::File;\n\tauto action = Api::SendAction(thread, options);\n\taction.clearDraft = false;\n\taction.replyTo = {\n\t\t.messageId = replyTo,\n\t\t.topicRootId = thread->topicRootId(),\n\t\t.monoforumPeerId = thread->monoforumPeerId(),\n\t};\n\tauto &api = session().api();\n\tauto sent = false;\n\tfor (auto &group : bundle->groups) {\n\t\tconst auto album = (group.type != Ui::AlbumType::None)\n\t\t\t? std::make_shared<SendingAlbum>()\n\t\t\t: nullptr;\n\t\tapi.sendFiles(std::move(group.list), type, album, action);\n\t\tsent = true;\n\t}\n\tif (sent) {\n\t\tshowToast(tr::lng_stories_reply_sent(tr::now));\n\t}\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_theme.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/themes/window_theme.h\"\n\n#include \"window/themes/window_theme_preview.h\"\n#include \"window/themes/window_themes_embedded.h\"\n#include \"window/themes/window_theme_editor.h\"\n#include \"window/window_controller.h\"\n#include \"platform/platform_specific.h\"\n#include \"mainwidget.h\"\n#include \"main/main_session.h\"\n#include \"apiwrap.h\"\n#include \"storage/localstorage.h\"\n#include \"storage/localimageloader.h\"\n#include \"storage/file_upload.h\"\n#include \"base/random.h\"\n#include \"base/parse_helper.h\"\n#include \"base/zlib_help.h\"\n#include \"base/unixtime.h\"\n#include \"base/crc32hash.h\"\n#include \"base/never_freed_pointer.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document_resolver.h\"\n#include \"main/main_account.h\" // Account::local.\n#include \"main/main_domain.h\" // Domain::activeSessionValue.\n#include \"lang/lang_keys.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/image/image.h\"\n#include \"ui/style/style_palette_colorizer.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"boxes/background_box.h\"\n#include \"core/application.h\"\n#include \"webview/webview_common.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_chat.h\"\n\n#include <QtCore/QBuffer>\n#include <QtCore/QJsonDocument>\n#include <QtCore/QJsonObject>\n#include <QtCore/QFileSystemWatcher>\n#include <QtGui/QGuiApplication>\n#include <QtGui/QStyleHints>\n\nnamespace Window {\nnamespace Theme {\nnamespace {\n\nconstexpr auto kThemeFileSizeLimit = 5 * 1024 * 1024;\nconstexpr auto kBackgroundSizeLimit = 25 * 1024 * 1024;\nconstexpr auto kNightThemeFile = \":/gui/night.tdesktop-theme\"_cs;\nconstexpr auto kDarkValueThreshold = 0.5;\n\nstruct Applying {\n\tSaved data;\n\tQByteArray paletteForRevert;\n\tFn<void()> overrideKeep;\n};\n\nbase::NeverFreedPointer<ChatBackground> GlobalBackground;\nApplying GlobalApplying;\n\ninline bool AreTestingTheme() {\n\treturn !GlobalApplying.paletteForRevert.isEmpty();\n}\n\n[[nodiscard]] QImage ReadDefaultImage() {\n\treturn Ui::ReadBackgroundImage(\n\t\tu\":/gui/art/background.tgv\"_q,\n\t\tQByteArray(),\n\t\ttrue\n\t).image;\n}\n\n[[nodiscard]] bool GoodImageFormatAndSize(const QImage &image) {\n\treturn !image.size().isEmpty()\n\t\t&& (image.format() == QImage::Format_ARGB32_Premultiplied\n\t\t\t|| image.format() == QImage::Format_RGB32);\n}\n\nQByteArray readThemeContent(const QString &path) {\n\tQFile file(path);\n\tif (!file.exists()) {\n\t\tLOG((\"Theme Error: theme file not found: %1\").arg(path));\n\t\treturn QByteArray();\n\t}\n\n\tif (file.size() > kThemeFileSizeLimit) {\n\t\tLOG((\"Theme Error: theme file too large: %1 (should be less than 5 MB, got %2)\").arg(path).arg(file.size()));\n\t\treturn QByteArray();\n\t}\n\tif (!file.open(QIODevice::ReadOnly)) {\n\t\tLOG((\"Theme Error: could not open theme file: %1\").arg(path));\n\t\treturn QByteArray();\n\t}\n\n\treturn file.readAll();\n}\n\ninline uchar readHexUchar(char code, bool &error) {\n\tif (code >= '0' && code <= '9') {\n\t\treturn ((code - '0') & 0xFF);\n\t} else if (code >= 'a' && code <= 'f') {\n\t\treturn ((code + 10 - 'a') & 0xFF);\n\t} else if (code >= 'A' && code <= 'F') {\n\t\treturn ((code + 10 - 'A') & 0xFF);\n\t}\n\terror = true;\n\treturn 0xFF;\n}\n\ninline uchar readHexUchar(char char1, char char2, bool &error) {\n\treturn ((readHexUchar(char1, error) & 0x0F) << 4) | (readHexUchar(char2, error) & 0x0F);\n}\n\nbool readNameAndValue(const char *&from, const char *end, QLatin1String *outName, QLatin1String *outValue) {\n\tusing base::parse::skipWhitespaces;\n\tusing base::parse::readName;\n\n\tif (!skipWhitespaces(from, end)) return true;\n\n\t*outName = readName(from, end);\n\tif (outName->size() == 0) {\n\t\tLOG((\"Theme Error: Could not read name in the color scheme.\"));\n\t\treturn false;\n\t}\n\tif (!skipWhitespaces(from, end)) {\n\t\tLOG((\"Theme Error: Unexpected end of the color scheme.\"));\n\t\treturn false;\n\t}\n\tif (*from != ':') {\n\t\tLOG((\"Theme Error: Expected ':' between each name and value in the color scheme (while reading key '%1')\").arg(*outName));\n\t\treturn false;\n\t}\n\tif (!skipWhitespaces(++from, end)) {\n\t\tLOG((\"Theme Error: Unexpected end of the color scheme.\"));\n\t\treturn false;\n\t}\n\tauto valueStart = from;\n\tif (*from == '#') ++from;\n\n\tif (readName(from, end).size() == 0) {\n\t\tLOG((\"Theme Error: Expected a color value in #rrggbb or #rrggbbaa format in the color scheme (while reading key '%1')\").arg(*outName));\n\t\treturn false;\n\t}\n\t*outValue = QLatin1String(valueStart, from - valueStart);\n\n\tif (!skipWhitespaces(from, end)) {\n\t\tLOG((\"Theme Error: Unexpected end of the color scheme.\"));\n\t\treturn false;\n\t}\n\tif (*from != ';') {\n\t\tLOG((\"Theme Error: Expected ';' after each value in the color scheme (while reading key '%1')\").arg(*outName));\n\t\treturn false;\n\t}\n\t++from;\n\treturn true;\n}\n\nenum class SetResult {\n\tOk,\n\tNotFound,\n};\nSetResult setColorSchemeValue(\n\t\tQLatin1String name,\n\t\tQLatin1String value,\n\t\tconst style::colorizer &colorizer,\n\t\tInstance *out) {\n\tauto result = style::palette::SetResult::Ok;\n\tauto size = value.size();\n\tauto data = value.data();\n\tif (data[0] == '#' && (size == 7 || size == 9)) {\n\t\tauto error = false;\n\t\tauto r = readHexUchar(data[1], data[2], error);\n\t\tauto g = readHexUchar(data[3], data[4], error);\n\t\tauto b = readHexUchar(data[5], data[6], error);\n\t\tauto a = (size == 9) ? readHexUchar(data[7], data[8], error) : uchar(255);\n\t\tif (colorizer) {\n\t\t\tstyle::colorize(name, r, g, b, colorizer);\n\t\t}\n\t\tif (error) {\n\t\t\tLOG((\"Theme Warning: Skipping value '%1: %2' (expected a color value in #rrggbb or #rrggbbaa or a previously defined key in the color scheme)\").arg(name).arg(value));\n\t\t\treturn SetResult::Ok;\n\t\t} else if (out) {\n\t\t\tresult = out->palette.setColor(name, r, g, b, a);\n\t\t} else {\n\t\t\tresult = style::main_palette::setColor(name, r, g, b, a);\n\t\t}\n\t} else {\n\t\tif (out) {\n\t\t\tresult = out->palette.setColor(name, value);\n\t\t} else {\n\t\t\tresult = style::main_palette::setColor(name, value);\n\t\t}\n\t}\n\tif (result == style::palette::SetResult::Ok) {\n\t\treturn SetResult::Ok;\n\t} else if (result == style::palette::SetResult::KeyNotFound) {\n\t\treturn SetResult::NotFound;\n\t} else if (result == style::palette::SetResult::ValueNotFound) {\n\t\tLOG((\"Theme Warning: Skipping value '%1: %2' (expected a color value in #rrggbb or #rrggbbaa or a previously defined key in the color scheme)\").arg(name).arg(value));\n\t\treturn SetResult::Ok;\n\t} else if (result == style::palette::SetResult::Duplicate) {\n\t\tLOG((\"Theme Warning: Color value appears more than once in the color scheme (while applying '%1: %2')\").arg(name).arg(value));\n\t\treturn SetResult::Ok;\n\t} else {\n\t\tLOG((\"Theme Error: Unexpected internal error.\"));\n\t}\n\tUnexpected(\"Value after palette.setColor().\");\n}\n\nbool loadColorScheme(\n\t\tconst QByteArray &content,\n\t\tconst style::colorizer &colorizer,\n\t\tInstance *out) {\n\tauto unsupported = QMap<QLatin1String, QLatin1String>();\n\treturn ReadPaletteValues(content, [&](QLatin1String name, QLatin1String value) {\n\t\t// Find the named value in the already read unsupported list.\n\t\tvalue = unsupported.value(value, value);\n\n\t\tauto result = setColorSchemeValue(name, value, colorizer, out);\n\t\tif (result == SetResult::NotFound) {\n\t\t\tunsupported.insert(name, value);\n\t\t}\n\t\treturn true;\n\t});\n}\n\nvoid applyBackground(QImage &&background, bool tiled, Instance *out) {\n\tif (out) {\n\t\tout->background = std::move(background);\n\t\tout->tiled = tiled;\n\t} else {\n\t\tBackground()->setThemeData(std::move(background), tiled);\n\t}\n}\n\nenum class LoadResult {\n\tLoaded,\n\tFailed,\n\tNotFound,\n};\n\nLoadResult loadBackgroundFromFile(zlib::FileToRead &file, const char *filename, QByteArray *outBackground) {\n\t*outBackground = file.readFileContent(filename, zlib::kCaseInsensitive, kThemeBackgroundSizeLimit);\n\tif (file.error() == UNZ_OK) {\n\t\treturn LoadResult::Loaded;\n\t} else if (file.error() == UNZ_END_OF_LIST_OF_FILE) {\n\t\tfile.clearError();\n\t\treturn LoadResult::NotFound;\n\t}\n\tLOG((\"Theme Error: could not read '%1' in the theme file.\").arg(filename));\n\treturn LoadResult::Failed;\n}\n\nbool loadBackground(zlib::FileToRead &file, QByteArray *outBackground, bool *outTiled) {\n\tauto result = loadBackgroundFromFile(file, \"background.jpg\", outBackground);\n\tif (result != LoadResult::NotFound) return (result == LoadResult::Loaded);\n\n\tresult = loadBackgroundFromFile(file, \"background.png\", outBackground);\n\tif (result != LoadResult::NotFound) return (result == LoadResult::Loaded);\n\n\t*outTiled = true;\n\tresult = loadBackgroundFromFile(file, \"tiled.jpg\", outBackground);\n\tif (result != LoadResult::NotFound) return (result == LoadResult::Loaded);\n\n\tresult = loadBackgroundFromFile(file, \"tiled.png\", outBackground);\n\tif (result != LoadResult::NotFound) return (result == LoadResult::Loaded);\n\treturn true;\n}\n\nbool LoadTheme(\n\t\tconst QByteArray &content,\n\t\tconst style::colorizer &colorizer,\n\t\tconst std::optional<QByteArray> &editedPalette,\n\t\tCached *cache = nullptr,\n\t\tInstance *out = nullptr) {\n\tif (content.size() < 4) {\n\t\tLOG((\"Theme Error: Bad theme content size: %1\").arg(content.size()));\n\t\treturn false;\n\t}\n\n\tif (cache) {\n\t\t*cache = Cached();\n\t}\n\tzlib::FileToRead file(content);\n\n\tconst auto emptyColorizer = style::colorizer();\n\tconst auto &paletteColorizer = editedPalette ? emptyColorizer : colorizer;\n\n\tunz_global_info globalInfo = { 0 };\n\tfile.getGlobalInfo(&globalInfo);\n\tif (file.error() == UNZ_OK) {\n\t\tauto schemeContent = editedPalette.value_or(QByteArray());\n\t\tif (schemeContent.isEmpty()) {\n\t\t\tschemeContent = file.readFileContent(\"colors.tdesktop-theme\", zlib::kCaseInsensitive, kThemeSchemeSizeLimit);\n\t\t}\n\t\tif (schemeContent.isEmpty()) {\n\t\t\tfile.clearError();\n\t\t\tschemeContent = file.readFileContent(\"colors.tdesktop-palette\", zlib::kCaseInsensitive, kThemeSchemeSizeLimit);\n\t\t}\n\t\tif (file.error() != UNZ_OK) {\n\t\t\tLOG((\"Theme Error: could not read 'colors.tdesktop-theme' or 'colors.tdesktop-palette' in the theme file.\"));\n\t\t\treturn false;\n\t\t}\n\t\tif (!loadColorScheme(schemeContent, paletteColorizer, out)) {\n\t\t\tDEBUG_LOG((\"Theme: Could not loadColorScheme.\"));\n\t\t\treturn false;\n\t\t}\n\t\tif (!out) {\n\t\t\tBackground()->saveAdjustableColors();\n\t\t}\n\n\t\tauto backgroundTiled = false;\n\t\tauto backgroundContent = QByteArray();\n\t\tif (!loadBackground(file, &backgroundContent, &backgroundTiled)) {\n\t\t\tDEBUG_LOG((\"Theme: Could not loadBackground.\"));\n\t\t\treturn false;\n\t\t}\n\n\t\tif (!backgroundContent.isEmpty()) {\n\t\t\tauto check = QBuffer(&backgroundContent);\n\t\t\tauto reader = QImageReader(&check);\n\t\t\tconst auto size = reader.size();\n\t\t\tif (size.isEmpty()\n\t\t\t\t|| (size.width() * size.height() > kBackgroundSizeLimit)) {\n\t\t\t\tLOG((\"Theme Error: bad background image size in the theme file.\"));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tauto background = Images::Read({\n\t\t\t\t.content = backgroundContent,\n\t\t\t\t.forceOpaque = true,\n\t\t\t}).image;\n\t\t\tif (background.isNull()) {\n\t\t\t\tLOG((\"Theme Error: could not read background image in the theme file.\"));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (colorizer) {\n\t\t\t\tstyle::colorize(background, colorizer);\n\t\t\t}\n\t\t\tif (cache) {\n\t\t\t\tauto buffer = QBuffer(&cache->background);\n\t\t\t\tif (!background.save(&buffer, \"BMP\")) {\n\t\t\t\t\tLOG((\"Theme Error: could not write background image as a BMP to cache.\"));\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tcache->tiled = backgroundTiled;\n\t\t\t}\n\t\t\tapplyBackground(std::move(background), backgroundTiled, out);\n\t\t}\n\t} else {\n\t\t// Looks like it is not a .zip theme.\n\t\tif (!loadColorScheme(editedPalette.value_or(content), paletteColorizer, out)) {\n\t\t\tDEBUG_LOG((\"Theme: Could not loadColorScheme from non-zip.\"));\n\t\t\treturn false;\n\t\t}\n\t\tif (!out) {\n\t\t\tBackground()->saveAdjustableColors();\n\t\t}\n\t}\n\tif (out) {\n\t\tout->palette.finalize(paletteColorizer);\n\t}\n\tif (cache) {\n\t\tif (out) {\n\t\t\tcache->colors = out->palette.save();\n\t\t} else {\n\t\t\tcache->colors = style::main_palette::save();\n\t\t}\n\t\tcache->paletteChecksum = style::palette::Checksum();\n\t\tcache->contentChecksum = base::crc32(content.constData(), content.size());\n\t}\n\treturn true;\n}\n\nbool InitializeFromCache(\n\t\tconst QByteArray &content,\n\t\tconst Cached &cache) {\n\tif (cache.paletteChecksum != style::palette::Checksum()) {\n\t\treturn false;\n\t}\n\tif (cache.contentChecksum != base::crc32(content.constData(), content.size())) {\n\t\treturn false;\n\t}\n\n\tQImage background;\n\tif (!cache.background.isEmpty()) {\n\t\tQDataStream stream(cache.background);\n\t\tQImageReader reader(stream.device());\n\t\treader.setAutoTransform(true);\n\t\tif (!reader.read(&background) || background.isNull()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tif (!style::main_palette::load(cache.colors)) {\n\t\treturn false;\n\t}\n\tBackground()->saveAdjustableColors();\n\tif (!background.isNull()) {\n\t\tapplyBackground(std::move(background), cache.tiled, nullptr);\n\t}\n\n\treturn true;\n}\n\n[[nodiscard]] std::optional<QByteArray> ReadEditingPalette() {\n\tauto file = QFile(EditingPalettePath());\n\treturn file.open(QIODevice::ReadOnly)\n\t\t? std::make_optional(file.readAll())\n\t\t: std::nullopt;\n}\n\nbool InitializeFromSaved(Saved &&saved) {\n\tif (saved.object.content.size() < 4) {\n\t\tLOG((\"Theme Error: Could not load theme from '%1' (%2)\").arg(\n\t\t\tsaved.object.pathRelative,\n\t\t\tsaved.object.pathAbsolute));\n\t\treturn false;\n\t}\n\n\tconst auto editing = ReadEditingPalette();\n\tGlobalBackground.createIfNull();\n\tif (!editing && InitializeFromCache(saved.object.content, saved.cache)) {\n\t\treturn true;\n\t}\n\n\tconst auto colorizer = ColorizerForTheme(saved.object.pathAbsolute);\n\tif (!LoadTheme(saved.object.content, colorizer, editing, &saved.cache)) {\n\t\tDEBUG_LOG((\"Theme: Could not load from saved.\"));\n\t\treturn false;\n\t}\n\tif (editing) {\n\t\tBackground()->setEditingTheme(ReadCloudFromText(*editing));\n\t} else {\n\t\tLocal::writeTheme(saved);\n\t}\n\treturn true;\n}\n\n[[nodiscard]] QImage PostprocessBackgroundImage(\n\t\tQImage image,\n\t\tconst Data::WallPaper &paper) {\n\tif (image.format() != QImage::Format_ARGB32_Premultiplied) {\n\t\timage = std::move(image).convertToFormat(\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t}\n\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\tif (Data::IsLegacy3DefaultWallPaper(paper)) {\n\t\treturn Images::DitherImage(std::move(image));\n\t}\n\treturn image;\n}\n\nvoid ClearApplying() {\n\tGlobalApplying = Applying();\n}\n\nvoid ClearEditingPalette() {\n\tQFile(EditingPalettePath()).remove();\n}\n\n} // namespace\n\nChatBackground::AdjustableColor::AdjustableColor(style::color data)\n: item(data)\n, original(data->c) {\n}\n\n// They're duplicated in window_theme_editor_box.cpp:ReplaceAdjustableColors.\nChatBackground::ChatBackground() : _adjustableColors({\n\t\tst::msgServiceBg,\n\t\tst::msgServiceBgSelected,\n\t\tst::historyScrollBg,\n\t\tst::historyScrollBgOver,\n\t\tst::historyScrollBarBg,\n\t\tst::historyScrollBarBgOver }) {\n}\n\nChatBackground::~ChatBackground() = default;\n\nvoid ChatBackground::setThemeData(QImage &&themeImage, bool themeTile) {\n\t_themeImage = PostprocessBackgroundImage(\n\t\tstd::move(themeImage),\n\t\tData::ThemeWallPaper());\n\t_themeTile = themeTile;\n}\n\nvoid ChatBackground::initialRead() {\n\tif (started()) {\n\t\treturn;\n\t}\n\tif (_themeObject.pathAbsolute.isEmpty() && !nightMode()) {\n\t\tapplyDefaultThemeAccentColorizer();\n\t}\n\tif (!Local::readBackground()) {\n\t\tset(Data::ThemeWallPaper());\n\t}\n\tif (_localStoredTileDayValue) {\n\t\t_tileDayValue = *_localStoredTileDayValue;\n\t}\n\tif (_localStoredTileNightValue) {\n\t\t_tileNightValue = *_localStoredTileNightValue;\n\t}\n}\n\nvoid ChatBackground::start() {\n\tsaveAdjustableColors();\n\n\t_updates.events(\n\t) | rpl::on_next([=](const BackgroundUpdate &update) {\n\t\trefreshThemeWatcher();\n\t\tif (update.paletteChanged()) {\n\t\t\tstyle::NotifyPaletteChanged();\n\t\t}\n\t}, _lifetime);\n\n\tinitialRead();\n\n\tCore::App().domain().activeSessionValue(\n\t) | rpl::filter([=](Main::Session *session) {\n\t\treturn session != _session;\n\t}) | rpl::on_next([=](Main::Session *session) {\n\t\t_session = session;\n\t\tcheckUploadWallPaper();\n\t}, _lifetime);\n\n\trpl::combine(\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n\t\trpl::single(\n\t\t\tQGuiApplication::styleHints()->colorScheme()\n\t\t) | rpl::then(\n\t\t\tbase::qt_signal_producer(\n\t\t\t\tQGuiApplication::styleHints(),\n\t\t\t\t&QStyleHints::colorSchemeChanged\n\t\t\t)\n\t\t),\n#endif // Qt >= 6.5.0\n\t\trpl::single(\n\t\t\tQGuiApplication::palette()\n\t\t) | rpl::then(\n\t\t\tbase::qt_signal_producer(\n\t\t\t\tqApp,\n\t\t\t\t&QGuiApplication::paletteChanged\n\t\t\t)\n\t\t)\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n\t) | rpl::map([](Qt::ColorScheme colorScheme, const QPalette &palette) {\n\t\treturn colorScheme != Qt::ColorScheme::Unknown\n\t\t\t? colorScheme == Qt::ColorScheme::Dark\n#else // Qt >= 6.5.0\n\t) | rpl::map([](const QPalette &palette) {\n\t\tconst auto dark = Platform::IsDarkMode();\n\t\treturn dark\n\t\t\t? *dark\n#endif // Qt < 6.5.0\n\t\t\t: palette.windowText().color().lightness()\n\t\t\t\t> palette.window().color().lightness();\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::on_next([](bool dark) {\n\t\tCore::App().settings().setSystemDarkMode(dark);\n\t}, _lifetime);\n\n\trpl::single(\n\t\tQGuiApplication::palette()\n\t) | rpl::then(\n\t\tbase::qt_signal_producer(\n\t\t\tqApp,\n\t\t\t&QGuiApplication::paletteChanged\n\t\t)\n\t) | rpl::on_next([=] {\n\t\tconst auto &settings = Core::App().settings();\n\t\tif (!settings.systemAccentColorEnabled()\n\t\t\t|| _themeObject.cloud.id\n\t\t\t|| editingTheme()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto path = _themeObject.pathAbsolute;\n\t\tif (!IsEmbeddedTheme(path)) {\n\t\t\treturn;\n\t\t}\n\t\tApplyDefaultWithPath(path);\n\t\tKeepApplied();\n\t}, _lifetime);\n}\n\nvoid ChatBackground::refreshThemeWatcher() {\n\tconst auto path = _themeObject.pathAbsolute;\n\tif (path.isEmpty()\n\t\t|| !QFileInfo(path).isNativePath()\n\t\t|| editingTheme()) {\n\t\t_themeWatcher = nullptr;\n\t} else if (!_themeWatcher || !_themeWatcher->files().contains(path)) {\n\t\t_themeWatcher = std::make_unique<QFileSystemWatcher>(\n\t\t\tQStringList(path));\n\t\tQObject::connect(\n\t\t\t_themeWatcher.get(),\n\t\t\t&QFileSystemWatcher::fileChanged,\n\t\t\t[](const QString &path) {\n\t\t\tApply(path);\n\t\t\tKeepApplied();\n\t\t});\n\t}\n}\n\nvoid ChatBackground::checkUploadWallPaper() {\n\tif (!_session) {\n\t\t_wallPaperUploadLifetime = rpl::lifetime();\n\t\t_wallPaperUploadId = FullMsgId();\n\t\t_wallPaperRequestId = 0;\n\t\treturn;\n\t}\n\tif (const auto id = base::take(_wallPaperUploadId)) {\n\t\t_session->uploader().cancel(id);\n\t}\n\tif (const auto id = base::take(_wallPaperRequestId)) {\n\t\t_session->api().request(id).cancel();\n\t}\n\tif (!Data::IsCustomWallPaper(_paper)\n\t\t|| _original.isNull()\n\t\t|| _editingTheme.has_value()) {\n\t\treturn;\n\t}\n\n\tconst auto ready = PrepareWallPaper(_session->mainDcId(), _original);\n\tconst auto documentId = ready->id;\n\t_wallPaperUploadId = FullMsgId(\n\t\t_session->userPeerId(),\n\t\t_session->data().nextLocalMessageId());\n\t_session->uploader().upload(_wallPaperUploadId, ready);\n\tif (_wallPaperUploadLifetime) {\n\t\treturn;\n\t}\n\t_wallPaperUploadLifetime = _session->uploader().documentReady(\n\t) | rpl::on_next([=](const Storage::UploadedMedia &data) {\n\t\tif (data.fullId != _wallPaperUploadId) {\n\t\t\treturn;\n\t\t}\n\t\t_wallPaperUploadId = FullMsgId();\n\t\t_wallPaperRequestId = _session->api().request(\n\t\t\tMTPaccount_UploadWallPaper(\n\t\t\t\tMTP_flags(0),\n\t\t\t\tdata.info.file,\n\t\t\t\tMTP_string(\"image/jpeg\"),\n\t\t\t\t_paper.mtpSettings()\n\t\t\t)\n\t\t).done([=](const MTPWallPaper &result) {\n\t\t\tresult.match([&](const MTPDwallPaper &data) {\n\t\t\t\t_session->data().documentConvert(\n\t\t\t\t\t_session->data().document(documentId),\n\t\t\t\t\tdata.vdocument());\n\t\t\t}, [&](const MTPDwallPaperNoFile &data) {\n\t\t\t\tLOG((\"API Error: \"\n\t\t\t\t\t\"Got wallPaperNoFile after account.UploadWallPaper.\"));\n\t\t\t});\n\t\t\tif (const auto paper = Data::WallPaper::Create(_session, result)) {\n\t\t\t\tsetPaper(*paper);\n\t\t\t\twriteNewBackgroundSettings();\n\t\t\t\t_updates.fire({ BackgroundUpdate::Type::New, tile() });\n\t\t\t}\n\t\t}).send();\n\t});\n}\n\nQImage ChatBackground::postprocessBackgroundImage(QImage image) {\n\treturn PostprocessBackgroundImage(std::move(image), _paper);\n}\n\nvoid ChatBackground::set(const Data::WallPaper &paper, QImage image) {\n\timage = Ui::PreprocessBackgroundImage(std::move(image));\n\n\tconst auto needResetAdjustable = Data::IsDefaultWallPaper(paper)\n\t\t&& !Data::IsDefaultWallPaper(_paper)\n\t\t&& !nightMode()\n\t\t&& _themeObject.pathAbsolute.isEmpty();\n\tif (Data::IsThemeWallPaper(paper) && _themeImage.isNull()) {\n\t\tsetPaper(Data::DefaultWallPaper());\n\t} else {\n\t\tsetPaper(paper);\n\t\tif (needResetAdjustable) {\n\t\t\t// If we had a default color theme with non-default background,\n\t\t\t// and we switch to default background we must somehow switch from\n\t\t\t// adjusted service colors to default (non-adjusted) service colors.\n\t\t\t// The only way to do that right now is through full palette reset.\n\t\t\trestoreAdjustableColors();\n\t\t}\n\t}\n\tif (Data::IsThemeWallPaper(_paper)) {\n\t\t(nightMode() ? _tileNightValue : _tileDayValue) = _themeTile;\n\t\tsetPrepared(_themeImage, _themeImage, QImage());\n\t} else if (Data::details::IsTestingThemeWallPaper(_paper)\n\t\t|| Data::details::IsTestingDefaultWallPaper(_paper)\n\t\t|| Data::details::IsTestingEditorWallPaper(_paper)) {\n\t\tif (Data::details::IsTestingDefaultWallPaper(_paper)\n\t\t\t|| image.isNull()) {\n\t\t\timage = ReadDefaultImage();\n\t\t\tsetPaper(Data::details::TestingDefaultWallPaper());\n\t\t}\n\t\tsetPreparedAfterPaper(std::move(image));\n\t} else {\n\t\tif (Data::IsLegacy1DefaultWallPaper(_paper)) {\n\t\t\timage.load(u\":/gui/art/bg_initial.jpg\"_q);\n\t\t\tconst auto scale = cScale() * style::DevicePixelRatio();\n\t\t\tif (scale != 100) {\n\t\t\t\timage = image.scaledToWidth(\n\t\t\t\t\tstyle::ConvertScale(image.width(), scale),\n\t\t\t\t\tQt::SmoothTransformation);\n\t\t\t}\n\t\t} else if (Data::IsDefaultWallPaper(_paper)\n\t\t\t|| (_paper.backgroundColors().empty() && image.isNull())) {\n\t\t\tsetPaper(Data::DefaultWallPaper().withParamsFrom(_paper));\n\t\t\timage = ReadDefaultImage();\n\t\t}\n\t\tLocal::writeBackground(\n\t\t\t_paper,\n\t\t\t((Data::IsDefaultWallPaper(_paper)\n\t\t\t\t|| Data::IsLegacy1DefaultWallPaper(_paper))\n\t\t\t\t? QImage()\n\t\t\t\t: image));\n\t\tsetPreparedAfterPaper(std::move(image));\n\t}\n\tAssert(colorForFill()\n\t\t|| !_gradient.isNull()\n\t\t|| (!_original.isNull()\n\t\t\t&& !_prepared.isNull()\n\t\t\t&& !_preparedForTiled.isNull()));\n\n\t_updates.fire({ BackgroundUpdate::Type::New, tile() }); // delayed?\n\tif (needResetAdjustable) {\n\t\t_updates.fire({ BackgroundUpdate::Type::TestingTheme, tile() });\n\t\t_updates.fire({ BackgroundUpdate::Type::ApplyingTheme, tile() });\n\t}\n\tcheckUploadWallPaper();\n}\n\nvoid ChatBackground::setPreparedAfterPaper(QImage image) {\n\tconst auto &bgColors = _paper.backgroundColors();\n\tif (_paper.isPattern() && !image.isNull()) {\n\t\tif (bgColors.size() < 2) {\n\t\t\tauto prepared = postprocessBackgroundImage(\n\t\t\t\tUi::PreparePatternImage(\n\t\t\t\t\timage,\n\t\t\t\t\tbgColors,\n\t\t\t\t\t_paper.gradientRotation(),\n\t\t\t\t\t_paper.patternOpacity()));\n\t\t\tsetPrepared(\n\t\t\t\tstd::move(image),\n\t\t\t\tstd::move(prepared),\n\t\t\t\tQImage());\n\t\t} else {\n\t\t\timage = postprocessBackgroundImage(std::move(image));\n\t\t\tif (Ui::IsPatternInverted(bgColors, _paper.patternOpacity())) {\n\t\t\t\timage = Ui::InvertPatternImage(std::move(image));\n\t\t\t}\n\t\t\tsetPrepared(\n\t\t\t\timage,\n\t\t\t\timage,\n\t\t\t\tData::GenerateDitheredGradient(_paper));\n\t\t}\n\t} else if (bgColors.size() == 1) {\n\t\tsetPrepared(QImage(), QImage(), QImage());\n\t} else if (!bgColors.empty()) {\n\t\tsetPrepared(\n\t\t\tQImage(),\n\t\t\tQImage(),\n\t\t\tData::GenerateDitheredGradient(_paper));\n\t} else {\n\t\timage = postprocessBackgroundImage(std::move(image));\n\t\tsetPrepared(image, image, QImage());\n\t}\n}\n\nvoid ChatBackground::setPrepared(\n\t\tQImage original,\n\t\tQImage prepared,\n\t\tQImage gradient) {\n\tExpects(original.isNull() || GoodImageFormatAndSize(original));\n\tExpects(prepared.isNull() || GoodImageFormatAndSize(prepared));\n\tExpects(gradient.isNull() || GoodImageFormatAndSize(gradient));\n\n\tif (!prepared.isNull() && !_paper.isPattern() && _paper.isBlurred()) {\n\t\tprepared = Ui::PrepareBlurredBackground(std::move(prepared));\n\t}\n\tif (adjustPaletteRequired()) {\n\t\tif ((prepared.isNull() || _paper.isPattern())\n\t\t\t&& !_paper.backgroundColors().empty()) {\n\t\t\tadjustPaletteUsingColors(_paper.backgroundColors());\n\t\t} else if (!prepared.isNull()) {\n\t\t\tadjustPaletteUsingBackground(prepared);\n\t\t}\n\t}\n\n\t_original = std::move(original);\n\t_prepared = std::move(prepared);\n\t_gradient = std::move(gradient);\n\t_imageMonoColor = _gradient.isNull()\n\t\t? Ui::CalculateImageMonoColor(_prepared)\n\t\t: std::nullopt;\n\t_preparedForTiled = Ui::PrepareImageForTiled(_prepared);\n}\n\nvoid ChatBackground::setPaper(const Data::WallPaper &paper) {\n\t_paper = paper.withoutImageData();\n}\n\nbool ChatBackground::adjustPaletteRequired() {\n\tconst auto usingThemeBackground = [&] {\n\t\treturn Data::IsThemeWallPaper(_paper)\n\t\t\t|| Data::details::IsTestingThemeWallPaper(_paper);\n\t};\n\tconst auto usingDefaultBackground = [&] {\n\t\treturn Data::IsDefaultWallPaper(_paper)\n\t\t\t|| Data::details::IsTestingDefaultWallPaper(_paper);\n\t};\n\n\tif (_editingTheme.has_value()) {\n\t\treturn false;\n\t} else if (isNonDefaultThemeOrBackground() || nightMode()) {\n\t\treturn !usingThemeBackground();\n\t}\n\treturn !usingDefaultBackground();\n}\n\nstd::optional<Data::CloudTheme> ChatBackground::editingTheme() const {\n\treturn _editingTheme;\n}\n\nvoid ChatBackground::setEditingTheme(const Data::CloudTheme &editing) {\n\t_editingTheme = editing;\n\trefreshThemeWatcher();\n}\n\nvoid ChatBackground::clearEditingTheme(ClearEditing clear) {\n\tif (!_editingTheme) {\n\t\treturn;\n\t}\n\t_editingTheme = std::nullopt;\n\tif (clear == ClearEditing::Temporary) {\n\t\treturn;\n\t}\n\tClearEditingPalette();\n\tif (clear == ClearEditing::RevertChanges) {\n\t\treapplyWithNightMode(std::nullopt, _nightMode);\n\t\tKeepApplied();\n\t}\n\trefreshThemeWatcher();\n}\n\nvoid ChatBackground::adjustPaletteUsingBackground(const QImage &image) {\n\tadjustPaletteUsingColor(Ui::CountAverageColor(image));\n}\n\nvoid ChatBackground::adjustPaletteUsingColors(\n\t\tconst std::vector<QColor> &colors) {\n\tadjustPaletteUsingColor(Ui::CountAverageColor(colors));\n}\n\nvoid ChatBackground::adjustPaletteUsingColor(QColor color) {\n\tconst auto prepared = color.toHsl();\n\tfor (const auto &adjustable : _adjustableColors) {\n\t\tconst auto adjusted = Ui::ThemeAdjustedColor(adjustable.item->c, prepared);\n\t\tadjustable.item.set(\n\t\t\tadjusted.red(),\n\t\t\tadjusted.green(),\n\t\t\tadjusted.blue(),\n\t\t\tadjusted.alpha());\n\t}\n}\n\nstd::optional<QColor> ChatBackground::colorForFill() const {\n\treturn !_prepared.isNull()\n\t\t? imageMonoColor()\n\t\t: (!_gradient.isNull() || _paper.backgroundColors().empty())\n\t\t? std::nullopt\n\t\t: std::make_optional(_paper.backgroundColors().front());\n}\n\nQImage ChatBackground::gradientForFill() const {\n\treturn _gradient;\n}\n\nvoid ChatBackground::recacheGradientForFill(QImage gradient) {\n\tif (_gradient.size() == gradient.size()) {\n\t\t_gradient = std::move(gradient);\n\t}\n}\n\nQImage ChatBackground::createCurrentImage() const {\n\tif (const auto fill = colorForFill()) {\n\t\tauto result = QImage(512, 512, QImage::Format_ARGB32_Premultiplied);\n\t\tresult.fill(*fill);\n\t\treturn result;\n\t} else if (_gradient.isNull()) {\n\t\treturn _prepared;\n\t} else if (_prepared.isNull()) {\n\t\treturn _gradient;\n\t}\n\tauto result = _gradient.scaled(\n\t\t_prepared.size(),\n\t\tQt::IgnoreAspectRatio,\n\t\tQt::SmoothTransformation);\n\tresult.setDevicePixelRatio(1.);\n\t{\n\t\tauto p = QPainter(&result);\n\t\tconst auto patternOpacity = paper().patternOpacity();\n\t\tif (patternOpacity >= 0.) {\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_SoftLight);\n\t\t\tp.setOpacity(patternOpacity);\n\t\t} else {\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\t}\n\t\tp.drawImage(QRect(QPoint(), _prepared.size()), _prepared);\n\t\tif (patternOpacity < 0. && patternOpacity > -1.) {\n\t\t\tp.setCompositionMode(QPainter::CompositionMode_SourceOver);\n\t\t\tp.setOpacity(1. + patternOpacity);\n\t\t\tp.fillRect(QRect(QPoint(), _prepared.size()), Qt::black);\n\t\t}\n\t}\n\treturn result;\n}\n\nbool ChatBackground::tile() const {\n\tif (!started()) {\n\t\tconst auto &set = nightMode()\n\t\t\t? _localStoredTileNightValue\n\t\t\t: _localStoredTileDayValue;\n\t\tif (set.has_value()) {\n\t\t\treturn *set;\n\t\t}\n\t}\n\treturn nightMode() ? _tileNightValue : _tileDayValue;\n}\n\nbool ChatBackground::tileDay() const {\n\tif (!started() && _localStoredTileDayValue.has_value()) {\n\t\treturn *_localStoredTileDayValue;\n\t} else if (Data::details::IsTestingThemeWallPaper(_paper) ||\n\t\tData::details::IsTestingDefaultWallPaper(_paper)) {\n\t\tif (!nightMode()) {\n\t\t\treturn _tileForRevert;\n\t\t}\n\t}\n\treturn _tileDayValue;\n}\n\nbool ChatBackground::tileNight() const {\n\tif (!started() && _localStoredTileNightValue.has_value()) {\n\t\treturn *_localStoredTileNightValue;\n\t} else if (Data::details::IsTestingThemeWallPaper(_paper) ||\n\t\tData::details::IsTestingDefaultWallPaper(_paper)) {\n\t\tif (nightMode()) {\n\t\t\treturn _tileForRevert;\n\t\t}\n\t}\n\treturn _tileNightValue;\n}\n\nstd::optional<QColor> ChatBackground::imageMonoColor() const {\n\treturn _imageMonoColor;\n}\n\nvoid ChatBackground::setTile(bool tile) {\n\tExpects(started());\n\n\tconst auto old = this->tile();\n\tif (nightMode()) {\n\t\tsetTileNightValue(tile);\n\t} else {\n\t\tsetTileDayValue(tile);\n\t}\n\tif (this->tile() != old) {\n\t\tif (!Data::details::IsTestingThemeWallPaper(_paper)\n\t\t\t&& !Data::details::IsTestingDefaultWallPaper(_paper)) {\n\t\t\tLocal::writeSettings();\n\t\t}\n\t\t_updates.fire({ BackgroundUpdate::Type::Changed, tile }); // delayed?\n\t}\n}\n\nvoid ChatBackground::setTileDayValue(bool tile) {\n\tif (started()) {\n\t\t_tileDayValue = tile;\n\t} else {\n\t\t_localStoredTileDayValue = tile;\n\t}\n}\n\nvoid ChatBackground::setTileNightValue(bool tile) {\n\tif (started()) {\n\t\t_tileNightValue = tile;\n\t} else {\n\t\t_localStoredTileNightValue = tile;\n\t}\n}\n\nvoid ChatBackground::setThemeObject(const Object &object) {\n\t_themeObject = object;\n\t_themeObject.content = QByteArray();\n}\n\nconst Object &ChatBackground::themeObject() const {\n\treturn _themeObject;\n}\n\nvoid ChatBackground::reset() {\n\tif (Data::details::IsTestingThemeWallPaper(_paper)\n\t\t|| Data::details::IsTestingDefaultWallPaper(_paper)) {\n\t\tif (_themeImage.isNull()) {\n\t\t\t_paperForRevert = Data::DefaultWallPaper();\n\t\t\t_originalForRevert = QImage();\n\t\t\t_tileForRevert = false;\n\t\t} else {\n\t\t\t_paperForRevert = Data::ThemeWallPaper();\n\t\t\t_originalForRevert = _themeImage;\n\t\t\t_tileForRevert = _themeTile;\n\t\t}\n\t} else {\n\t\tset(Data::ThemeWallPaper());\n\t\trestoreAdjustableColors();\n\t\t_updates.fire({ BackgroundUpdate::Type::TestingTheme, tile() });\n\t\t_updates.fire({ BackgroundUpdate::Type::ApplyingTheme, tile() });\n\t}\n\twriteNewBackgroundSettings();\n}\n\nbool ChatBackground::started() const {\n\treturn !Data::details::IsUninitializedWallPaper(_paper);\n}\n\nvoid ChatBackground::saveForRevert() {\n\tExpects(started());\n\n\tif (!Data::details::IsTestingThemeWallPaper(_paper)\n\t\t&& !Data::details::IsTestingDefaultWallPaper(_paper)) {\n\t\t_paperForRevert = _paper;\n\t\t_originalForRevert = std::move(_original);\n\t\t_tileForRevert = tile();\n\t}\n}\n\nvoid ChatBackground::saveAdjustableColors() {\n\tfor (auto &color : _adjustableColors) {\n\t\tcolor.original = color.item->c;\n\t}\n}\n\nvoid ChatBackground::restoreAdjustableColors() {\n\tfor (const auto &color : _adjustableColors) {\n\t\tconst auto value = color.original;\n\t\tcolor.item.set(value.red(), value.green(), value.blue(), value.alpha());\n\t}\n}\n\nvoid ChatBackground::setTestingTheme(Instance &&theme) {\n\tstyle::main_palette::apply(theme.palette);\n\tsaveAdjustableColors();\n\n\tauto switchToThemeBackground = !theme.background.isNull()\n\t\t|| Data::IsThemeWallPaper(_paper)\n\t\t|| (Data::IsDefaultWallPaper(_paper)\n\t\t\t&& !nightMode()\n\t\t\t&& _themeObject.pathAbsolute.isEmpty());\n\tif (AreTestingTheme() && _editingTheme.has_value()) {\n\t\t// Grab current background image if it is not already custom\n\t\t// Use prepared pixmap, not original image, because we're\n\t\t// for sure switching to a non-pattern wall-paper (testing editor).\n\t\tif (!Data::IsCustomWallPaper(_paper)) {\n\t\t\tsaveForRevert();\n\t\t\tset(\n\t\t\t\tData::details::TestingEditorWallPaper(),\n\t\t\t\tbase::take(_prepared));\n\t\t}\n\t} else if (switchToThemeBackground) {\n\t\tsaveForRevert();\n\t\tset(\n\t\t\tData::details::TestingThemeWallPaper(),\n\t\t\tstd::move(theme.background));\n\t\tsetTile(theme.tiled);\n\t} else {\n\t\t// Apply current background image so that service bg colors are recounted.\n\t\tset(_paper, std::move(_original));\n\t}\n\t_updates.fire({ BackgroundUpdate::Type::TestingTheme, tile() });\n}\n\nvoid ChatBackground::setTestingDefaultTheme() {\n\tapplyDefaultThemeAccentColorizer();\n\n\tsaveForRevert();\n\tset(Data::details::TestingDefaultWallPaper());\n\tsetTile(false);\n\t_updates.fire({ BackgroundUpdate::Type::TestingTheme, tile() });\n}\n\nvoid ChatBackground::applyDefaultThemeAccentColorizer() {\n\tstyle::main_palette::reset(ColorizerForTheme(QString()));\n\tsaveAdjustableColors();\n}\n\nvoid ChatBackground::keepApplied(const Object &object, bool write) {\n\tsetThemeObject(object);\n\tif (Data::details::IsTestingEditorWallPaper(_paper)) {\n\t\tsetPaper(Data::CustomWallPaper());\n\t\t_themeImage = QImage();\n\t\t_themeTile = false;\n\t\tif (write) {\n\t\t\twriteNewBackgroundSettings();\n\t\t}\n\t} else if (Data::details::IsTestingThemeWallPaper(_paper)) {\n\t\tsetPaper(Data::ThemeWallPaper());\n\t\t_themeImage = postprocessBackgroundImage(base::duplicate(_original));\n\t\t_themeTile = tile();\n\t\tif (write) {\n\t\t\twriteNewBackgroundSettings();\n\t\t}\n\t} else if (Data::details::IsTestingDefaultWallPaper(_paper)) {\n\t\tsetPaper(Data::DefaultWallPaper());\n\t\t_themeImage = QImage();\n\t\t_themeTile = false;\n\t\tif (write) {\n\t\t\twriteNewBackgroundSettings();\n\t\t}\n\t}\n\t_updates.fire({ BackgroundUpdate::Type::ApplyingTheme, tile() });\n}\n\nbool ChatBackground::isNonDefaultThemeOrBackground() {\n\tinitialRead();\n\treturn nightMode()\n\t\t? (_themeObject.pathAbsolute != NightThemePath()\n\t\t\t|| !Data::IsThemeWallPaper(_paper))\n\t\t: (!_themeObject.pathAbsolute.isEmpty()\n\t\t\t|| !Data::IsDefaultWallPaper(_paper));\n}\n\nbool ChatBackground::isNonDefaultBackground() {\n\tinitialRead();\n\treturn _themeObject.pathAbsolute.isEmpty()\n\t\t? !Data::IsDefaultWallPaper(_paper)\n\t\t: !Data::IsThemeWallPaper(_paper);\n}\n\nvoid ChatBackground::writeNewBackgroundSettings() {\n\tif (tile() != _tileForRevert) {\n\t\tLocal::writeSettings();\n\t}\n\tLocal::writeBackground(\n\t\t_paper,\n\t\t((Data::IsThemeWallPaper(_paper)\n\t\t\t|| Data::IsDefaultWallPaper(_paper))\n\t\t\t? QImage()\n\t\t\t: _original));\n}\n\nvoid ChatBackground::revert() {\n\tif (Data::details::IsTestingThemeWallPaper(_paper)\n\t\t|| Data::details::IsTestingDefaultWallPaper(_paper)\n\t\t|| Data::details::IsTestingEditorWallPaper(_paper)) {\n\t\tsetTile(_tileForRevert);\n\t\tset(_paperForRevert, std::move(_originalForRevert));\n\t} else {\n\t\t// Apply current background image so that service bg colors are recounted.\n\t\tset(_paper, std::move(_original));\n\t}\n\t_updates.fire({ BackgroundUpdate::Type::RevertingTheme, tile() });\n}\n\nvoid ChatBackground::appliedEditedPalette() {\n\t_updates.fire({ BackgroundUpdate::Type::ApplyingEdit, tile() });\n}\n\nvoid ChatBackground::downloadingStarted(bool tile) {\n\t_updates.fire({ BackgroundUpdate::Type::Start, tile });\n}\n\nvoid ChatBackground::setNightModeValue(bool nightMode) {\n\t_nightMode = nightMode;\n}\n\nbool ChatBackground::nightMode() const {\n\treturn _nightMode;\n}\n\nvoid ChatBackground::reapplyWithNightMode(\n\t\tstd::optional<QString> themePath,\n\t\tbool newNightMode) {\n\tif (!started()) {\n\t\t// We can get here from legacy passcoded state.\n\t\t// In this case Background() is not started yet, because\n\t\t// some settings and the background itself were not read.\n\t\treturn;\n\t} else if (_nightMode != newNightMode && !nightModeChangeAllowed()) {\n\t\treturn;\n\t}\n\tconst auto settingExactTheme = themePath.has_value();\n\tconst auto nightModeChanged = (newNightMode != _nightMode);\n\tconst auto oldNightMode = _nightMode;\n\t_nightMode = newNightMode;\n\tauto read = settingExactTheme ? Saved() : Local::readThemeAfterSwitch();\n\tauto path = read.object.pathAbsolute;\n\n\t_nightMode = oldNightMode;\n\tauto oldTileValue = (_nightMode ? _tileNightValue : _tileDayValue);\n\tconst auto alreadyOnDisk = [&] {\n\t\tif (read.object.content.isEmpty()) {\n\t\t\treturn false;\n\t\t}\n\t\tauto preview = std::make_unique<Preview>();\n\t\tpreview->object = std::move(read.object);\n\t\tpreview->instance.cached = std::move(read.cache);\n\t\tconst auto loaded = LoadTheme(\n\t\t\tpreview->object.content,\n\t\t\tColorizerForTheme(path),\n\t\t\tstd::nullopt,\n\t\t\t&preview->instance.cached,\n\t\t\t&preview->instance);\n\t\tif (!loaded) {\n\t\t\treturn false;\n\t\t}\n\t\tApply(std::move(preview));\n\t\treturn true;\n\t}();\n\tif (!alreadyOnDisk) {\n\t\tpath = themePath\n\t\t\t? *themePath\n\t\t\t: (newNightMode ? NightThemePath() : QString());\n\t\tApplyDefaultWithPath(path);\n\t}\n\n\t// Theme editor could have already reverted the testing of this toggle.\n\tif (AreTestingTheme()) {\n\t\tGlobalApplying.overrideKeep = [=] {\n\t\t\tif (nightModeChanged) {\n\t\t\t\t_nightMode = newNightMode;\n\n\t\t\t\t// Restore the value, it was set inside theme testing.\n\t\t\t\t(oldNightMode ? _tileNightValue : _tileDayValue) = oldTileValue;\n\t\t\t}\n\n\t\t\tconst auto saved = std::move(GlobalApplying.data);\n\t\t\tif (!alreadyOnDisk) {\n\t\t\t\t// First-time switch to default night mode should write it.\n\t\t\t\tLocal::writeTheme(saved);\n\t\t\t}\n\t\t\tClearApplying();\n\t\t\tkeepApplied(saved.object, settingExactTheme);\n\t\t\tif (tile() != _tileForRevert || nightModeChanged) {\n\t\t\t\tLocal::writeSettings();\n\t\t\t}\n\t\t\tif (!settingExactTheme && !Local::readBackground()) {\n\t\t\t\tset(Data::ThemeWallPaper());\n\t\t\t}\n\t\t};\n\t}\n}\n\nbool ChatBackground::nightModeChangeAllowed() const {\n\tconst auto &settings = Core::App().settings();\n\tconst auto allowedToBeAfterChange = settings.systemDarkModeEnabled()\n\t\t? settings.systemDarkMode().value_or(!_nightMode)\n\t\t: !_nightMode;\n\treturn (_nightMode != allowedToBeAfterChange);\n}\n\nvoid ChatBackground::toggleNightMode(std::optional<QString> themePath) {\n\treapplyWithNightMode(themePath, !_nightMode);\n}\n\nChatBackground *Background() {\n\tGlobalBackground.createIfNull();\n\treturn GlobalBackground.data();\n}\n\nbool IsEmbeddedTheme(const QString &path) {\n\treturn path.isEmpty() || path.startsWith(u\":/gui/\"_q);\n}\n\nbool Initialize(Saved &&saved) {\n\tif (InitializeFromSaved(std::move(saved))) {\n\t\tBackground()->setThemeObject(saved.object);\n\t\treturn true;\n\t}\n\tDEBUG_LOG((\"Theme: Could not initialize from saved.\"));\n\treturn false;\n}\n\nvoid Uninitialize() {\n\tGlobalBackground.clear();\n\tGlobalApplying = Applying();\n}\n\nbool Apply(\n\t\tconst QString &filepath,\n\t\tconst Data::CloudTheme &cloud) {\n\tif (auto preview = PreviewFromFile(QByteArray(), filepath, cloud)) {\n\t\treturn Apply(std::move(preview));\n\t}\n\treturn false;\n}\n\nbool Apply(std::unique_ptr<Preview> preview) {\n\tGlobalApplying.data.object = std::move(preview->object);\n\tGlobalApplying.data.cache = std::move(preview->instance.cached);\n\tif (GlobalApplying.paletteForRevert.isEmpty()) {\n\t\tGlobalApplying.paletteForRevert = style::main_palette::save();\n\t}\n\tBackground()->setTestingTheme(std::move(preview->instance));\n\treturn true;\n}\n\nvoid ApplyDefaultWithPath(const QString &themePath) {\n\tif (!themePath.isEmpty()) {\n\t\tif (auto preview = PreviewFromFile(QByteArray(), themePath, {})) {\n\t\t\tApply(std::move(preview));\n\t\t}\n\t} else {\n\t\tGlobalApplying.data = Saved();\n\t\tif (GlobalApplying.paletteForRevert.isEmpty()) {\n\t\t\tGlobalApplying.paletteForRevert = style::main_palette::save();\n\t\t}\n\t\tBackground()->setTestingDefaultTheme();\n\t}\n}\n\nbool ApplyEditedPalette(const QByteArray &content) {\n\tauto out = Instance();\n\tif (!loadColorScheme(content, style::colorizer(), &out)) {\n\t\treturn false;\n\t}\n\tstyle::main_palette::apply(out.palette);\n\tBackground()->appliedEditedPalette();\n\treturn true;\n}\n\nvoid KeepApplied() {\n\tif (!AreTestingTheme()) {\n\t\treturn;\n\t} else if (GlobalApplying.overrideKeep) {\n\t\t// This callback will be destroyed while running.\n\t\t// And it won't be able to safely access captures after that.\n\t\t// So we save it on stack for the time while it is running.\n\t\tconst auto onstack = base::take(GlobalApplying.overrideKeep);\n\t\tonstack();\n\t\treturn;\n\t}\n\tconst auto saved = std::move(GlobalApplying.data);\n\tLocal::writeTheme(saved);\n\tClearApplying();\n\tBackground()->keepApplied(saved.object, true);\n}\n\nvoid KeepFromEditor(\n\t\tconst QByteArray &originalContent,\n\t\tconst ParsedTheme &originalParsed,\n\t\tconst Data::CloudTheme &cloud,\n\t\tconst QByteArray &themeContent,\n\t\tconst ParsedTheme &themeParsed,\n\t\tconst QImage &background) {\n\tClearApplying();\n\tconst auto content = themeContent.isEmpty()\n\t\t? originalContent\n\t\t: themeContent;\n\tauto saved = Saved();\n\tauto &cache = saved.cache;\n\tauto &object = saved.object;\n\tcache.colors = style::main_palette::save();\n\tcache.paletteChecksum = style::palette::Checksum();\n\tcache.contentChecksum = base::crc32(content.constData(), content.size());\n\tcache.background = themeParsed.background;\n\tcache.tiled = themeParsed.tiled;\n\tobject.cloud = cloud;\n\tobject.content = themeContent.isEmpty()\n\t\t? originalContent\n\t\t: themeContent;\n\tobject.pathAbsolute = object.pathRelative = CachedThemePath(\n\t\tcloud.documentId);\n\tLocal::writeTheme(saved);\n\tBackground()->keepApplied(saved.object, true);\n\tBackground()->setThemeData(\n\t\tbase::duplicate(background),\n\t\tthemeParsed.tiled);\n\tBackground()->set(Data::ThemeWallPaper());\n\tBackground()->writeNewBackgroundSettings();\n}\n\nvoid Revert() {\n\tif (!AreTestingTheme()) {\n\t\treturn;\n\t}\n\tstyle::main_palette::load(GlobalApplying.paletteForRevert);\n\tBackground()->saveAdjustableColors();\n\n\tClearApplying();\n\tBackground()->revert();\n}\n\nQString NightThemePath() {\n\treturn kNightThemeFile.utf16();\n}\n\nbool IsNonDefaultBackground() {\n\treturn Background()->isNonDefaultBackground();\n}\n\nbool IsNightMode() {\n\treturn GlobalBackground ? Background()->nightMode() : false;\n}\n\nrpl::producer<bool> IsNightModeValue() {\n\tauto changes = Background()->updates(\n\t) | rpl::filter([=](const BackgroundUpdate &update) {\n\t\treturn update.type == BackgroundUpdate::Type::ApplyingTheme;\n\t}) | rpl::to_empty;\n\n\treturn rpl::single(rpl::empty) | rpl::then(\n\t\tstd::move(changes)\n\t) | rpl::map([=] {\n\t\treturn IsNightMode();\n\t}) | rpl::distinct_until_changed();\n}\n\nvoid SetNightModeValue(bool nightMode) {\n\tif (GlobalBackground || nightMode) {\n\t\tBackground()->setNightModeValue(nightMode);\n\t}\n}\n\nvoid ToggleNightMode() {\n\tBackground()->toggleNightMode(std::nullopt);\n}\n\nvoid ToggleNightMode(const QString &path) {\n\tBackground()->toggleNightMode(path);\n}\n\nvoid ToggleNightModeWithConfirmation(\n\t\tnot_null<Controller*> window,\n\t\tFn<void()> toggle) {\n\tif (Background()->nightModeChangeAllowed()) {\n\t\ttoggle();\n\t} else {\n\t\tconst auto disableAndToggle = [=](Fn<void()> &&close) {\n\t\t\tCore::App().settings().setSystemDarkModeEnabled(false);\n\t\t\tCore::App().saveSettingsDelayed();\n\t\t\ttoggle();\n\t\t\tclose();\n\t\t};\n\t\twindow->show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_settings_auto_night_warning(),\n\t\t\t.confirmed = disableAndToggle,\n\t\t\t.confirmText = tr::lng_settings_auto_night_disable(),\n\t\t}));\n\t}\n}\n\nvoid ResetToSomeDefault() {\n\tBackground()->reapplyWithNightMode(\n\t\tIsNightMode() ? NightThemePath() : QString(),\n\t\tIsNightMode());\n}\n\nbool LoadFromFile(\n\t\tconst QString &path,\n\t\tnot_null<Instance*> out,\n\t\tCached *outCache,\n\t\tQByteArray *outContent) {\n\tconst auto colorizer = ColorizerForTheme(path);\n\treturn LoadFromFile(path, out, outCache, outContent, colorizer);\n}\n\nbool LoadFromFile(\n\t\tconst QString &path,\n\t\tnot_null<Instance*> out,\n\t\tCached *outCache,\n\t\tQByteArray *outContent,\n\t\tconst style::colorizer &colorizer) {\n\tconst auto content = readThemeContent(path);\n\tif (outContent) {\n\t\t*outContent = content;\n\t}\n\treturn LoadTheme(content, colorizer, std::nullopt, outCache, out);\n}\n\nbool LoadFromContent(\n\t\tconst QByteArray &content,\n\t\tnot_null<Instance*> out,\n\t\tCached *outCache) {\n\treturn LoadTheme(\n\t\tcontent,\n\t\tstyle::colorizer(),\n\t\tstd::nullopt,\n\t\toutCache,\n\t\tout);\n}\n\nrpl::producer<bool> IsThemeDarkValue() {\n\treturn rpl::single(rpl::empty) | rpl::then(\n\t\tstyle::PaletteChanged()\n\t) | rpl::map([] {\n\t\treturn (st::dialogsBg->c.valueF() < kDarkValueThreshold);\n\t});\n}\n\nQString EditingPalettePath() {\n\treturn cWorkingDir() + \"tdata/editing-theme.tdesktop-palette\";\n}\n\nbool ReadPaletteValues(const QByteArray &content, Fn<bool(QLatin1String name, QLatin1String value)> callback) {\n\tif (content.size() > kThemeSchemeSizeLimit) {\n\t\tLOG((\"Theme Error: color scheme file too large (should be less than 1 MB, got %2)\").arg(content.size()));\n\t\treturn false;\n\t}\n\n\tauto data = base::parse::stripComments(content);\n\tauto from = data.constData(), end = from + data.size();\n\twhile (from != end) {\n\t\tauto name = QLatin1String(\"\");\n\t\tauto value = QLatin1String(\"\");\n\t\tif (!readNameAndValue(from, end, &name, &value)) {\n\t\t\tDEBUG_LOG((\"Theme: Could not readNameAndValue.\"));\n\t\t\treturn false;\n\t\t}\n\t\tif (name.size() == 0) { // End of content reached.\n\t\t\treturn true;\n\t\t}\n\t\tif (!callback(name, value)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n[[nodiscard]] Webview::ThemeParams WebViewParams() {\n\tconst auto colors = std::vector<std::pair<QString, const style::color&>>{\n\t\t{ \"bg_color\", st::windowBg },\n\t\t{ \"secondary_bg_color\", st::boxDividerBg },\n\t\t{ \"text_color\", st::windowFg },\n\t\t{ \"hint_color\", st::windowSubTextFg },\n\t\t{ \"link_color\", st::windowActiveTextFg },\n\t\t{ \"button_color\", st::windowBgActive },\n\t\t{ \"button_text_color\", st::windowFgActive },\n\t\t{ \"header_bg_color\", st::windowBg },\n\t\t{ \"accent_text_color\", st::lightButtonFg },\n\t\t{ \"section_bg_color\", st::lightButtonBg },\n\t\t{ \"section_header_text_color\", st::windowActiveTextFg },\n\t\t{ \"subtitle_text_color\", st::windowSubTextFg },\n\t\t{ \"destructive_text_color\", st::attentionButtonFg },\n\t\t{ \"bottom_bar_bg_color\", st::windowBg },\n\t};\n\tauto object = QJsonObject();\n\tconst auto wrap = [](QColor color) {\n\t\tauto r = 0;\n\t\tauto g = 0;\n\t\tauto b = 0;\n\t\tcolor.getRgb(&r, &g, &b);\n\t\tconst auto hex = [](int component) {\n\t\t\tconst auto digit = [](int c) {\n\t\t\t\treturn QChar((c < 10) ? ('0' + c) : ('a' + c - 10));\n\t\t\t};\n\t\t\treturn QString() + digit(component / 16) + digit(component % 16);\n\t\t};\n\t\treturn '#' + hex(r) + hex(g) + hex(b);\n\t};\n\tfor (const auto &[name, color] : colors) {\n\t\tobject.insert(name, wrap(color->c));\n\t}\n\t{\n\t\tconst auto bg = st::windowBg->c;\n\t\tconst auto shadow = st::shadowFg->c;\n\t\tconst auto shadowAlpha = shadow.alphaF();\n\t\tconst auto mix = [&](int a, int b) {\n\t\t\treturn anim::interpolate(a, b, shadowAlpha);\n\t\t};\n\t\tobject.insert(\"section_separator_color\", wrap(QColor(\n\t\t\tmix(bg.red(), shadow.red()),\n\t\t\tmix(bg.green(), shadow.green()),\n\t\t\tmix(bg.blue(), shadow.blue()))));\n\t}\n\treturn {\n\t\t.bodyBg = st::windowBg->c,\n\t\t.titleBg = QColor(0, 0, 0, 0),\n\t\t.scrollBg = st::scrollBg->c,\n\t\t.scrollBgOver = st::scrollBgOver->c,\n\t\t.scrollBarBg = st::scrollBarBg->c,\n\t\t.scrollBarBgOver = st::scrollBarBgOver->c,\n\n\t\t.json = QJsonDocument(object).toJson(QJsonDocument::Compact),\n\t};\n}\n\nstd::shared_ptr<FilePrepareResult> PrepareWallPaper(\n\t\tMTP::DcId dcId,\n\t\tconst QImage &image) {\n\tPreparedPhotoThumbs thumbnails;\n\tQVector<MTPPhotoSize> sizes;\n\n\tQByteArray jpeg;\n\tQBuffer jpegBuffer(&jpeg);\n\timage.save(&jpegBuffer, \"JPG\", 87);\n\n\tconst auto scaled = [&](int size) {\n\t\treturn image.scaled(\n\t\t\tsize,\n\t\t\tsize,\n\t\t\tQt::KeepAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t};\n\tconst auto push = [&](const char *type, QImage &&image) {\n\t\tsizes.push_back(MTP_photoSize(\n\t\t\tMTP_string(type),\n\t\t\tMTP_int(image.width()),\n\t\t\tMTP_int(image.height()), MTP_int(0)));\n\t\tthumbnails.emplace(\n\t\t\ttype[0],\n\t\t\tPreparedPhotoThumb{ .image = std::move(image) });\n\t};\n\tpush(\"s\", scaled(320));\n\n\tconst auto id = base::RandomValue<DocumentId>();\n\tconst auto filename = u\"wallpaper.jpg\"_q;\n\tauto attributes = QVector<MTPDocumentAttribute>(\n\t\t1,\n\t\tMTP_documentAttributeFilename(MTP_string(filename)));\n\tattributes.push_back(MTP_documentAttributeImageSize(\n\t\tMTP_int(image.width()),\n\t\tMTP_int(image.height())));\n\n\tauto result = MakePreparedFile({\n\t\t.id = id,\n\t\t.type = SendMediaType::ThemeFile,\n\t});\n\tresult->filename = filename;\n\tresult->content = jpeg;\n\tresult->filesize = jpeg.size();\n\tresult->setFileData(jpeg);\n\tif (thumbnails.empty()) {\n\t\tresult->thumb = thumbnails.front().second.image;\n\t\tresult->thumbbytes = thumbnails.front().second.bytes;\n\t}\n\tresult->document = MTP_document(\n\t\tMTP_flags(0),\n\t\tMTP_long(id),\n\t\tMTP_long(0),\n\t\tMTP_bytes(),\n\t\tMTP_int(base::unixtime::now()),\n\t\tMTP_string(\"image/jpeg\"),\n\t\tMTP_long(jpeg.size()),\n\t\tMTP_vector<MTPPhotoSize>(sizes),\n\t\tMTPVector<MTPVideoSize>(),\n\t\tMTP_int(dcId),\n\t\tMTP_vector<MTPDocumentAttribute>(attributes));\n\treturn result;\n}\n\nstd::unique_ptr<Ui::ChatTheme> DefaultChatThemeOn(rpl::lifetime &lifetime) {\n\tauto result = std::make_unique<Ui::ChatTheme>();\n\n\tconst auto push = [=, raw = result.get()] {\n\t\tconst auto background = Background();\n\t\tconst auto &paper = background->paper();\n\t\traw->setBackground({\n\t\t\t.prepared = background->prepared(),\n\t\t\t.preparedForTiled = background->preparedForTiled(),\n\t\t\t.gradientForFill = background->gradientForFill(),\n\t\t\t.colorForFill = background->colorForFill(),\n\t\t\t.colors = paper.backgroundColors(),\n\t\t\t.patternOpacity = paper.patternOpacity(),\n\t\t\t.gradientRotation = paper.gradientRotation(),\n\t\t\t.isPattern = paper.isPattern(),\n\t\t\t.tile = background->tile(),\n\t\t\t});\n\t};\n\n\tpush();\n\tBackground()->updates(\n\t) | rpl::on_next([=](const BackgroundUpdate &update) {\n\t\tif (update.type == BackgroundUpdate::Type::New\n\t\t\t|| update.type == BackgroundUpdate::Type::Changed) {\n\t\t\tpush();\n\t\t}\n\t}, lifetime);\n\n\treturn result;\n}\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_theme.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_wall_paper.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"ui/style/style_core_palette.h\"\n\nclass QFileSystemWatcher;\nstruct FilePrepareResult;\n\nnamespace style {\nstruct colorizer;\n} // namespace style\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\nclass Controller;\n} // namespace Window\n\nnamespace Ui {\nstruct ChatThemeBackground;\nclass ChatTheme;\n} // namespace Ui\n\nnamespace Webview {\nstruct ThemeParams;\n} // namespace Webview\n\nnamespace Window {\nnamespace Theme {\n\ninline constexpr auto kThemeSchemeSizeLimit = 1024 * 1024;\ninline constexpr auto kThemeBackgroundSizeLimit = 4 * 1024 * 1024;\n\nstruct ParsedTheme;\n\n[[nodiscard]] bool IsEmbeddedTheme(const QString &path);\n\nstruct Object {\n\tQString pathRelative;\n\tQString pathAbsolute;\n\tQByteArray content;\n\tData::CloudTheme cloud;\n};\nstruct Cached {\n\tQByteArray colors;\n\tQByteArray background;\n\tbool tiled = false;\n\tint32 paletteChecksum = 0;\n\tint32 contentChecksum = 0;\n};\nstruct Saved {\n\tObject object;\n\tCached cache;\n};\nbool Initialize(Saved &&saved);\nvoid Uninitialize();\n\nstruct Instance {\n\tstyle::palette palette;\n\tQImage background;\n\tCached cached;\n\tbool tiled = false;\n};\n\nstruct Preview {\n\tObject object;\n\tInstance instance;\n\tQImage preview;\n};\n\nbool Apply(\n\tconst QString &filepath,\n\tconst Data::CloudTheme &cloud = Data::CloudTheme());\nbool Apply(std::unique_ptr<Preview> preview);\nvoid ApplyDefaultWithPath(const QString &themePath);\nbool ApplyEditedPalette(const QByteArray &content);\nvoid KeepApplied();\nvoid KeepFromEditor(\n\tconst QByteArray &originalContent,\n\tconst ParsedTheme &originalParsed,\n\tconst Data::CloudTheme &cloud,\n\tconst QByteArray &themeContent,\n\tconst ParsedTheme &themeParsed,\n\tconst QImage &background);\nQString NightThemePath();\n[[nodiscard]] bool IsNightMode();\nvoid SetNightModeValue(bool nightMode);\n[[nodiscard]] rpl::producer<bool> IsNightModeValue();\nvoid ToggleNightMode();\nvoid ToggleNightMode(const QString &themePath);\nvoid ToggleNightModeWithConfirmation(\n\tnot_null<Controller*> window,\n\tFn<void()> toggle);\nvoid ResetToSomeDefault();\n[[nodiscard]] bool IsNonDefaultBackground();\nvoid Revert();\n\n[[nodiscard]] rpl::producer<bool> IsThemeDarkValue();\n\n[[nodiscard]] QString EditingPalettePath();\n\n// NB! This method looks to Core::App().settings() to get colorizer by 'file'.\nbool LoadFromFile(\n\tconst QString &path,\n\tnot_null<Instance*> out,\n\tCached *outCache,\n\tQByteArray *outContent);\nbool LoadFromFile(\n\tconst QString &path,\n\tnot_null<Instance*> out,\n\tCached *outCache,\n\tQByteArray *outContent,\n\tconst style::colorizer &colorizer);\nbool LoadFromContent(\n\tconst QByteArray &content,\n\tnot_null<Instance*> out,\n\tCached *outCache);\n\nstruct BackgroundUpdate {\n\tenum class Type {\n\t\tNew,\n\t\tChanged,\n\t\tStart,\n\t\tTestingTheme,\n\t\tRevertingTheme,\n\t\tApplyingTheme,\n\t\tApplyingEdit,\n\t};\n\n\tBackgroundUpdate(Type type, bool tiled) : type(type), tiled(tiled) {\n\t}\n\t[[nodiscard]] bool paletteChanged() const {\n\t\treturn (type == Type::TestingTheme)\n\t\t\t|| (type == Type::RevertingTheme)\n\t\t\t|| (type == Type::ApplyingEdit)\n\t\t\t|| (type == Type::New);\n\t}\n\tType type;\n\tbool tiled;\n};\n\nenum class ClearEditing {\n\tTemporary,\n\tRevertChanges,\n\tKeepChanges,\n};\n\nclass ChatBackground final {\npublic:\n\tChatBackground();\n\t~ChatBackground();\n\n\t[[nodiscard]] rpl::producer<BackgroundUpdate> updates() const {\n\t\treturn _updates.events();\n\t}\n\n\tvoid start();\n\n\t// This method is allowed to (and should) be called before start().\n\tvoid setThemeData(QImage &&themeImage, bool themeTile);\n\n\t// This method is setting the default (themed) image if none was set yet.\n\tvoid set(const Data::WallPaper &paper, QImage image = QImage());\n\tvoid setTile(bool tile);\n\tvoid setTileDayValue(bool tile);\n\tvoid setTileNightValue(bool tile);\n\tvoid setThemeObject(const Object &object);\n\t[[nodiscard]] const Object &themeObject() const;\n\t[[nodiscard]] std::optional<Data::CloudTheme> editingTheme() const;\n\tvoid setEditingTheme(const Data::CloudTheme &editing);\n\tvoid clearEditingTheme(ClearEditing clear = ClearEditing::Temporary);\n\tvoid reset();\n\n\tvoid setTestingTheme(Instance &&theme);\n\tvoid saveAdjustableColors();\n\tvoid setTestingDefaultTheme();\n\tvoid revert();\n\n\tvoid appliedEditedPalette();\n\tvoid downloadingStarted(bool tile);\n\n\t[[nodiscard]] const Data::WallPaper &paper() const {\n\t\treturn _paper;\n\t}\n\t[[nodiscard]] WallPaperId id() const {\n\t\treturn _paper.id();\n\t}\n\t[[nodiscard]] const QImage &prepared() const {\n\t\treturn _prepared;\n\t}\n\t[[nodiscard]] const QImage &preparedForTiled() const {\n\t\treturn _preparedForTiled;\n\t}\n\t[[nodiscard]] std::optional<QColor> colorForFill() const;\n\t[[nodiscard]] QImage gradientForFill() const;\n\tvoid recacheGradientForFill(QImage gradient);\n\t[[nodiscard]] QImage createCurrentImage() const;\n\t[[nodiscard]] bool tile() const;\n\t[[nodiscard]] bool tileDay() const;\n\t[[nodiscard]] bool tileNight() const;\n\t[[nodiscard]] std::optional<QColor> imageMonoColor() const;\n\t[[nodiscard]] bool nightModeChangeAllowed() const;\n\nprivate:\n\tstruct AdjustableColor {\n\t\tAdjustableColor(style::color data);\n\n\t\tstyle::color item;\n\t\tQColor original;\n\t};\n\n\t[[nodiscard]] bool started() const;\n\tvoid initialRead();\n\tvoid saveForRevert();\n\tvoid setPreparedAfterPaper(QImage image);\n\tvoid setPrepared(QImage original, QImage prepared, QImage gradient);\n\tvoid prepareImageForTiled();\n\tvoid writeNewBackgroundSettings();\n\tvoid setPaper(const Data::WallPaper &paper);\n\n\t[[nodiscard]] bool adjustPaletteRequired();\n\tvoid applyDefaultThemeAccentColorizer();\n\tvoid adjustPaletteUsingBackground(const QImage &image);\n\tvoid adjustPaletteUsingColors(const std::vector<QColor> &colors);\n\tvoid adjustPaletteUsingColor(QColor color);\n\tvoid restoreAdjustableColors();\n\n\tvoid setNightModeValue(bool nightMode);\n\t[[nodiscard]] bool nightMode() const;\n\tvoid toggleNightMode(std::optional<QString> themePath);\n\tvoid reapplyWithNightMode(\n\t\tstd::optional<QString> themePath,\n\t\tbool newNightMode);\n\tvoid keepApplied(const Object &object, bool write);\n\t[[nodiscard]] bool isNonDefaultThemeOrBackground();\n\t[[nodiscard]] bool isNonDefaultBackground();\n\tvoid checkUploadWallPaper();\n\t[[nodiscard]] QImage postprocessBackgroundImage(QImage image);\n\tvoid refreshThemeWatcher();\n\n\tfriend bool IsNightMode();\n\tfriend void SetNightModeValue(bool nightMode);\n\tfriend void ToggleNightMode();\n\tfriend void ToggleNightMode(const QString &themePath);\n\tfriend void ResetToSomeDefault();\n\tfriend void KeepApplied();\n\tfriend void KeepFromEditor(\n\t\tconst QByteArray &originalContent,\n\t\tconst ParsedTheme &originalParsed,\n\t\tconst Data::CloudTheme &cloud,\n\t\tconst QByteArray &themeContent,\n\t\tconst ParsedTheme &themeParsed,\n\t\tconst QImage &background);\n\tfriend bool IsNonDefaultBackground();\n\n\tMain::Session *_session = nullptr;\n\trpl::event_stream<BackgroundUpdate> _updates;\n\tData::WallPaper _paper = Data::details::UninitializedWallPaper();\n\tstd::optional<QColor> _paperColor;\n\tQImage _gradient;\n\tQImage _original;\n\tQImage _prepared;\n\tQImage _preparedForTiled;\n\tbool _nightMode = false;\n\tbool _tileDayValue = false;\n\tbool _tileNightValue = true;\n\tstd::optional<bool> _localStoredTileDayValue;\n\tstd::optional<bool> _localStoredTileNightValue;\n\n\tstd::optional<QColor> _imageMonoColor;\n\n\tObject _themeObject;\n\tQImage _themeImage;\n\tbool _themeTile = false;\n\tstd::optional<Data::CloudTheme> _editingTheme;\n\tstd::unique_ptr<QFileSystemWatcher> _themeWatcher;\n\n\tData::WallPaper _paperForRevert\n\t\t= Data::details::UninitializedWallPaper();\n\tQImage _originalForRevert;\n\tbool _tileForRevert = false;\n\n\tstd::vector<AdjustableColor> _adjustableColors;\n\tFullMsgId _wallPaperUploadId;\n\tmtpRequestId _wallPaperRequestId = 0;\n\trpl::lifetime _wallPaperUploadLifetime;\n\n\trpl::lifetime _lifetime;\n\n};\n\n[[nodiscard]] std::shared_ptr<FilePrepareResult> PrepareWallPaper(\n\tMTP::DcId dcId,\n\tconst QImage &image);\n\n[[nodiscard]] ChatBackground *Background();\n\nbool ReadPaletteValues(\n\tconst QByteArray &content,\n\tFn<bool(QLatin1String name, QLatin1String value)> callback);\n\n[[nodiscard]] Webview::ThemeParams WebViewParams();\n\n[[nodiscard]] std::unique_ptr<Ui::ChatTheme> DefaultChatThemeOn(\n\trpl::lifetime &lifetime);\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_theme_editor.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/themes/window_theme_editor.h\"\n\n#include \"window/themes/window_theme.h\"\n#include \"window/themes/window_theme_editor_block.h\"\n#include \"window/themes/window_theme_editor_box.h\"\n#include \"window/themes/window_themes_embedded.h\"\n#include \"window/window_controller.h\"\n#include \"main/main_account.h\"\n#include \"mainwindow.h\"\n#include \"storage/localstorage.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/widgets/color_editor.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/multi_select.h\"\n#include \"ui/widgets/dropdown_menu.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/style/style_palette_colorizer.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"boxes/abstract_box.h\"\n#include \"base/parse_helper.h\"\n#include \"base/zlib_help.h\"\n#include \"base/call_delayed.h\"\n#include \"core/file_utilities.h\"\n#include \"core/application.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_menu_icons.h\"\n\nnamespace Window {\nnamespace Theme {\nnamespace {\n\ntemplate <size_t Size>\nQByteArray qba(const char(&string)[Size]) {\n\treturn QByteArray::fromRawData(string, Size - 1);\n}\n\nconst auto kCloudInTextStart = qba(\"// THEME EDITOR SERVICE INFO START\\n\");\nconst auto kCloudInTextEnd = qba(\"// THEME EDITOR SERVICE INFO END\\n\\n\");\n\nstruct ReadColorResult {\n\tReadColorResult(QColor color, bool error = false) : color(color), error(error) {\n\t}\n\tQColor color;\n\tbool error = false;\n};\n\nReadColorResult colorError(const QString &name) {\n\treturn { QColor(), true };\n}\n\nReadColorResult readColor(const QString &name, const char *data, int size) {\n\tif (size != 6 && size != 8) {\n\t\treturn colorError(name);\n\t}\n\tauto readHex = [](char ch) {\n\t\tif (ch >= '0' && ch <= '9') {\n\t\t\treturn (ch - '0');\n\t\t} else if (ch >= 'a' && ch <= 'f') {\n\t\t\treturn (ch - 'a' + 10);\n\t\t} else if (ch >= 'A' && ch <= 'F') {\n\t\t\treturn (ch - 'A' + 10);\n\t\t}\n\t\treturn -1;\n\t};\n\tauto readValue = [readHex](const char *data) {\n\t\tauto high = readHex(data[0]);\n\t\tauto low = readHex(data[1]);\n\t\treturn (high >= 0 && low >= 0) ? (high * 0x10 + low) : -1;\n\t};\n\tauto r = readValue(data);\n\tauto g = readValue(data + 2);\n\tauto b = readValue(data + 4);\n\tauto a = (size == 8) ? readValue(data + 6) : 255;\n\tif (r < 0 || g < 0 || b < 0 || a < 0) {\n\t\treturn colorError(name);\n\t}\n\treturn { QColor(r, g, b, a) };\n}\n\nbool skipComment(const char *&data, const char *end) {\n\tif (data == end) return false;\n\tif (*data == '/' && data + 1 != end) {\n\t\tif (*(data + 1) == '/') {\n\t\t\tdata += 2;\n\t\t\twhile (data != end && *data != '\\n') {\n\t\t\t\t++data;\n\t\t\t}\n\t\t\treturn true;\n\t\t} else if (*(data + 1) == '*') {\n\t\t\tdata += 2;\n\t\t\twhile (true) {\n\t\t\t\twhile (data != end && *data != '*') {\n\t\t\t\t\t++data;\n\t\t\t\t}\n\t\t\t\tif (data != end) {\n\t\t\t\t\t++data;\n\t\t\t\t\tif (data != end && *data == '/') {\n\t\t\t\t\t\t++data;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (data == end) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nvoid skipWhitespacesAndComments(const char *&data, const char *end) {\n\twhile (data != end) {\n\t\tif (!base::parse::skipWhitespaces(data, end)) return;\n\t\tif (!skipComment(data, end)) return;\n\t}\n}\n\nQLatin1String readValue(const char *&data, const char *end) {\n\tauto start = data;\n\tif (data != end && *data == '#') {\n\t\t++data;\n\t}\n\tbase::parse::readName(data, end);\n\treturn QLatin1String(start, data - start);\n}\n\nbool isValidColorValue(QLatin1String value) {\n\tauto isValidHexChar = [](char ch) {\n\t\treturn (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f');\n\t};\n\tauto data = value.data();\n\tauto size = value.size();\n\tif ((size != 7 && size != 9) || data[0] != '#') {\n\t\treturn false;\n\t}\n\tfor (auto i = 1; i != size; ++i) {\n\t\tif (!isValidHexChar(data[i])) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n[[nodiscard]] QByteArray ColorizeInContent(\n\t\tQByteArray content,\n\t\tconst style::colorizer &colorizer) {\n\tauto validNames = OrderedSet<QLatin1String>();\n\tcontent.detach();\n\tauto start = content.constBegin(), data = start, end = data + content.size();\n\twhile (data != end) {\n\t\tskipWhitespacesAndComments(data, end);\n\t\tif (data == end) break;\n\n\t\t[[maybe_unused]] auto foundName = base::parse::readName(data, end);\n\t\tskipWhitespacesAndComments(data, end);\n\t\tif (data == end || *data != ':') {\n\t\t\treturn \"error\";\n\t\t}\n\t\t++data;\n\t\tskipWhitespacesAndComments(data, end);\n\t\tauto value = readValue(data, end);\n\t\tif (value.size() == 0) {\n\t\t\treturn \"error\";\n\t\t}\n\t\tif (isValidColorValue(value)) {\n\t\t\tconst auto colorized = style::colorize(value, colorizer);\n\t\t\tAssert(colorized.size() == value.size());\n\t\t\tmemcpy(\n\t\t\t\tcontent.data() + (data - start) - value.size(),\n\t\t\t\tcolorized.data(),\n\t\t\t\tvalue.size());\n\t\t}\n\t\tskipWhitespacesAndComments(data, end);\n\t\tif (data == end || *data != ';') {\n\t\t\treturn \"error\";\n\t\t}\n\t\t++data;\n\t}\n\treturn content;\n}\n\nQString bytesToUtf8(QLatin1String bytes) {\n\treturn QString::fromUtf8(bytes.data(), bytes.size());\n}\n\n} // namespace\n\nclass Editor::Inner final : public Ui::RpWidget {\npublic:\n\tInner(QWidget *parent, const QString &path);\n\n\tvoid setErrorCallback(Fn<void()> callback) {\n\t\t_errorCallback = std::move(callback);\n\t}\n\tvoid setFocusCallback(Fn<void()> callback) {\n\t\t_focusCallback = std::move(callback);\n\t}\n\tvoid setScrollCallback(Fn<void(int top, int bottom)> callback) {\n\t\t_scrollCallback = std::move(callback);\n\t}\n\n\tvoid prepare();\n\t[[nodiscard]] QByteArray paletteContent() const {\n\t\treturn _paletteContent;\n\t}\n\n\tvoid filterRows(const QString &query);\n\tvoid chooseRow();\n\n\tvoid selectSkip(int direction);\n\tvoid selectSkipPage(int delta, int direction);\n\n\tvoid applyNewPalette(const QByteArray &newContent);\n\tvoid recreateRows();\n\n\t~Inner() {\n\t\tif (_context.colorEditor.box) {\n\t\t\t_context.colorEditor.box->closeBox();\n\t\t}\n\t}\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tint resizeGetHeight(int newWidth) override;\n\nprivate:\n\tbool readData();\n\tbool readExistingRows();\n\tbool feedExistingRow(const QString &name, QLatin1String value);\n\n\tvoid error() {\n\t\tif (_errorCallback) {\n\t\t\t_errorCallback();\n\t\t}\n\t}\n\tvoid applyEditing(const QString &name, const QString &copyOf, QColor value);\n\n\tvoid sortByAccentDistance();\n\n\tEditorBlock::Context _context;\n\n\tQString _path;\n\tQByteArray _paletteContent;\n\tFn<void()> _errorCallback;\n\tFn<void()> _focusCallback;\n\tFn<void(int top, int bottom)> _scrollCallback;\n\n\tobject_ptr<EditorBlock> _existingRows;\n\tobject_ptr<EditorBlock> _newRows;\n\n\tbool _applyingUpdate = false;\n\n};\n\nQByteArray ColorHexString(const QColor &color) {\n\tauto result = QByteArray();\n\tresult.reserve(9);\n\tresult.append('#');\n\tconst auto addHex = [&](int code) {\n\t\tif (code >= 0 && code < 10) {\n\t\t\tresult.append('0' + code);\n\t\t} else if (code >= 10 && code < 16) {\n\t\t\tresult.append('a' + (code - 10));\n\t\t}\n\t};\n\tconst auto addValue = [&](int code) {\n\t\taddHex(code / 16);\n\t\taddHex(code % 16);\n\t};\n\taddValue(color.red());\n\taddValue(color.green());\n\taddValue(color.blue());\n\tif (color.alpha() != 255) {\n\t\taddValue(color.alpha());\n\t}\n\treturn result;\n}\n\nQByteArray ReplaceValueInPaletteContent(\n\t\tconst QByteArray &content,\n\t\tconst QByteArray &name,\n\t\tconst QByteArray &value) {\n\tauto validNames = OrderedSet<QLatin1String>();\n\tauto start = content.constBegin(), data = start, end = data + content.size();\n\tauto lastValidValueStart = end, lastValidValueEnd = end;\n\twhile (data != end) {\n\t\tskipWhitespacesAndComments(data, end);\n\t\tif (data == end) break;\n\n\t\tauto foundName = base::parse::readName(data, end);\n\t\tskipWhitespacesAndComments(data, end);\n\t\tif (data == end || *data != ':') {\n\t\t\treturn \"error\";\n\t\t}\n\t\t++data;\n\t\tskipWhitespacesAndComments(data, end);\n\t\tauto valueStart = data;\n\t\tauto value = readValue(data, end);\n\t\tauto valueEnd = data;\n\t\tif (value.size() == 0) {\n\t\t\treturn \"error\";\n\t\t}\n\t\tauto validValue = validNames.contains(value) || isValidColorValue(value);\n\t\tif (validValue) {\n\t\t\tvalidNames.insert(foundName);\n\t\t\tif (foundName == name) {\n\t\t\t\tlastValidValueStart = valueStart;\n\t\t\t\tlastValidValueEnd = valueEnd;\n\t\t\t}\n\t\t}\n\t\tskipWhitespacesAndComments(data, end);\n\t\tif (data == end || *data != ';') {\n\t\t\treturn \"error\";\n\t\t}\n\t\t++data;\n\t}\n\tif (lastValidValueStart != end) {\n\t\tauto result = QByteArray();\n\t\tresult.reserve((lastValidValueStart - start) + value.size() + (end - lastValidValueEnd));\n\t\tresult.append(start, lastValidValueStart - start);\n\t\tresult.append(value);\n\t\tif (end - lastValidValueEnd > 0) result.append(lastValidValueEnd, end - lastValidValueEnd);\n\t\treturn result;\n\t}\n\tauto newline = (content.indexOf(\"\\r\\n\") >= 0 ? \"\\r\\n\" : \"\\n\");\n\tauto addedline = (content.endsWith('\\n') ? \"\" : newline);\n\treturn content + addedline + name + \": \" + value + \";\" + newline;\n}\n\n[[nodiscard]] QByteArray WriteCloudToText(const Data::CloudTheme &cloud) {\n\tauto result = QByteArray();\n\tconst auto add = [&](const QByteArray &key, const QString &value) {\n\t\tresult.append(\"// \" + key + \": \" + value.toLatin1() + \"\\n\");\n\t};\n\tresult.append(kCloudInTextStart);\n\tadd(\"ID\", QString::number(cloud.id));\n\tadd(\"ACCESS\", QString::number(cloud.accessHash));\n\tresult.append(kCloudInTextEnd);\n\treturn result;\n}\n\n[[nodiscard]] Data::CloudTheme ReadCloudFromText(const QByteArray &text) {\n\tconst auto index = text.indexOf(kCloudInTextEnd);\n\tif (index <= 1) {\n\t\treturn Data::CloudTheme();\n\t}\n\tauto result = Data::CloudTheme();\n\tconst auto list = text.mid(0, index - 1).split('\\n');\n\tconst auto take = [&](uint64 &value, int index) {\n\t\tif (list.size() <= index) {\n\t\t\treturn false;\n\t\t}\n\t\tconst auto &entry = list[index];\n\t\tconst auto position = entry.indexOf(\": \");\n\t\tif (position < 0) {\n\t\t\treturn false;\n\t\t}\n\t\tvalue = QString::fromLatin1(entry.mid(position + 2)).toULongLong();\n\t\treturn true;\n\t};\n\tif (!take(result.id, 1) || !take(result.accessHash, 2)) {\n\t\treturn Data::CloudTheme();\n\t}\n\treturn result;\n}\n\nQByteArray StripCloudTextFields(const QByteArray &text) {\n\tconst auto firstValue = text.indexOf(\": #\");\n\tauto start = 0;\n\twhile (true) {\n\t\tconst auto index = text.indexOf(kCloudInTextEnd, start);\n\t\tif (index < 0 || index > firstValue) {\n\t\t\tbreak;\n\t\t}\n\t\tstart = index + kCloudInTextEnd.size();\n\t}\n\treturn (start > 0) ? text.mid(start) : text;\n}\n\nEditor::Inner::Inner(QWidget *parent, const QString &path)\n: RpWidget(parent)\n, _path(path)\n, _existingRows(this, EditorBlock::Type::Existing, &_context)\n, _newRows(this, EditorBlock::Type::New, &_context) {\n\tresize(st::windowMinWidth, st::windowMinHeight);\n\n\t_context.resized.events(\n\t) | rpl::on_next([=] {\n\t\tresizeToWidth(width());\n\t}, lifetime());\n\n\tusing Context = EditorBlock::Context;\n\t_context.pending.events(\n\t) | rpl::on_next([=](const Context::EditionData &data) {\n\t\tapplyEditing(data.name, data.copyOf, data.value);\n\t}, lifetime());\n\n\t_context.updated.events(\n\t) | rpl::on_next([=] {\n\t\tif (_context.name.isEmpty() && _focusCallback) {\n\t\t\t_focusCallback();\n\t\t}\n\t}, lifetime());\n\n\t_context.scroll.events(\n\t) | rpl::on_next([=](const Context::ScrollData &data) {\n\t\tif (_scrollCallback) {\n\t\t\tauto top = (data.type == EditorBlock::Type::Existing\n\t\t\t\t? _existingRows\n\t\t\t\t: _newRows)->y();\n\t\t\ttop += data.position;\n\t\t\t_scrollCallback(top, top + data.height);\n\t\t}\n\t}, lifetime());\n\n\tBackground()->updates(\n\t) | rpl::on_next([=](const BackgroundUpdate &update) {\n\t\tif (_applyingUpdate || !Background()->editingTheme()) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (update.type == BackgroundUpdate::Type::TestingTheme) {\n\t\t\tRevert();\n\t\t\tbase::call_delayed(st::slideDuration, this, [] {\n\t\t\t\tUi::show(Ui::MakeInformBox(\n\t\t\t\t\ttr::lng_theme_editor_cant_change_theme()));\n\t\t\t});\n\t\t}\n\t}, lifetime());\n}\n\nvoid Editor::Inner::recreateRows() {\n\t_existingRows.create(this, EditorBlock::Type::Existing, &_context);\n\t_existingRows->show();\n\t_newRows.create(this, EditorBlock::Type::New, &_context);\n\t_newRows->show();\n\tif (!readData()) {\n\t\terror();\n\t}\n}\n\nvoid Editor::Inner::prepare() {\n\tQFile f(_path);\n\tif (!f.open(QIODevice::ReadOnly)) {\n\t\tLOG((\"Theme Error: could not open color palette file '%1'\").arg(_path));\n\t\terror();\n\t\treturn;\n\t}\n\n\t_paletteContent = f.readAll();\n\tif (f.error() != QFileDevice::NoError) {\n\t\tLOG((\"Theme Error: could not read content from palette file '%1'\").arg(_path));\n\t\terror();\n\t\treturn;\n\t}\n\tf.close();\n\n\tif (!readData()) {\n\t\terror();\n\t}\n}\n\nvoid Editor::Inner::filterRows(const QString &query) {\n\tif (query == \":sort-for-accent\") {\n\t\tsortByAccentDistance();\n\t\tfilterRows(QString());\n\t\treturn;\n\t}\n\t_existingRows->filterRows(query);\n\t_newRows->filterRows(query);\n}\n\nvoid Editor::Inner::chooseRow() {\n\tif (!_existingRows->hasSelected() && !_newRows->hasSelected()) {\n\t\tselectSkip(1);\n\t}\n\tif (_existingRows->hasSelected()) {\n\t\t_existingRows->chooseRow();\n\t} else if (_newRows->hasSelected()) {\n\t\t_newRows->chooseRow();\n\t}\n}\n\n// Block::selectSkip(-1) removes the selection if it can't select anything\n// Block::selectSkip(1) leaves the selection if it can't select anything\nvoid Editor::Inner::selectSkip(int direction) {\n\tif (direction > 0) {\n\t\tif (_newRows->hasSelected()) {\n\t\t\t_existingRows->clearSelected();\n\t\t\t_newRows->selectSkip(direction);\n\t\t} else if (_existingRows->hasSelected()) {\n\t\t\tif (!_existingRows->selectSkip(direction)) {\n\t\t\t\tif (_newRows->selectSkip(direction)) {\n\t\t\t\t\t_existingRows->clearSelected();\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif (!_existingRows->selectSkip(direction)) {\n\t\t\t\t_newRows->selectSkip(direction);\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (_existingRows->hasSelected()) {\n\t\t\t_newRows->clearSelected();\n\t\t\t_existingRows->selectSkip(direction);\n\t\t} else if (_newRows->hasSelected()) {\n\t\t\tif (!_newRows->selectSkip(direction)) {\n\t\t\t\t_existingRows->selectSkip(direction);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid Editor::Inner::selectSkipPage(int delta, int direction) {\n\tauto defaultRowHeight = st::themeEditorMargin.top()\n\t\t+ st::themeEditorSampleSize.height()\n\t\t+ st::themeEditorDescriptionSkip\n\t\t+ st::defaultTextStyle.font->height\n\t\t+ st::themeEditorMargin.bottom();\n\tfor (auto i = 0, count = ceilclamp(delta, defaultRowHeight, 1, delta); i != count; ++i) {\n\t\tselectSkip(direction);\n\t}\n}\n\nvoid Editor::Inner::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tp.setFont(st::boxTitleFont);\n\tp.setPen(st::windowFg);\n\tif (!_newRows->isHidden()) {\n\t\tp.drawTextLeft(st::themeEditorMargin.left(), _existingRows->y() + _existingRows->height() + st::boxTitlePosition.y(), width(), tr::lng_theme_editor_new_keys(tr::now));\n\t}\n}\n\nint Editor::Inner::resizeGetHeight(int newWidth) {\n\tauto rowsWidth = newWidth;\n\t_existingRows->resizeToWidth(rowsWidth);\n\t_newRows->resizeToWidth(rowsWidth);\n\n\t_existingRows->moveToLeft(0, 0);\n\t_newRows->moveToLeft(0, _existingRows->height() + st::boxTitleHeight);\n\n\tauto lowest = (_newRows->isHidden() ? _existingRows : _newRows).data();\n\n\treturn lowest->y() + lowest->height();\n}\n\nbool Editor::Inner::readData() {\n\tif (!readExistingRows()) {\n\t\treturn false;\n\t}\n\n\tconst auto rows = style::main_palette::data();\n\tfor (const auto &row : rows) {\n\t\tauto name = bytesToUtf8(row.name);\n\t\tauto description = bytesToUtf8(row.description);\n\t\tif (!_existingRows->feedDescription(name, description)) {\n\t\t\tif (row.value.data()[0] == '#') {\n\t\t\t\tauto result = readColor(name, row.value.data() + 1, row.value.size() - 1);\n\t\t\t\tAssert(!result.error);\n\t\t\t\t_newRows->feed(name, result.color);\n\t\t\t\t//if (!_newRows->feedFallbackName(name, row.fallback.utf16())) {\n\t\t\t\t//\tUnexpected(\"Row for fallback not found\");\n\t\t\t\t//}\n\t\t\t} else {\n\t\t\t\tauto copyOf = bytesToUtf8(row.value);\n\t\t\t\tif (auto result = _existingRows->find(copyOf)) {\n\t\t\t\t\t_newRows->feed(name, *result, copyOf);\n\t\t\t\t} else if (!_newRows->feedCopy(name, copyOf)) {\n\t\t\t\t\tUnexpected(\"Copy of unknown value in the default palette\");\n\t\t\t\t}\n\t\t\t\tAssert(row.fallback.size() == 0);\n\t\t\t}\n\t\t\tif (!_newRows->feedDescription(name, description)) {\n\t\t\t\tUnexpected(\"Row for description not found\");\n\t\t\t}\n\t\t}\n\t}\n\n\treturn true;\n}\n\nvoid Editor::Inner::sortByAccentDistance() {\n\tconst auto accent = *_existingRows->find(\"windowBgActive\");\n\t_existingRows->sortByDistance(accent);\n\t_newRows->sortByDistance(accent);\n}\n\nbool Editor::Inner::readExistingRows() {\n\treturn ReadPaletteValues(_paletteContent, [this](QLatin1String name, QLatin1String value) {\n\t\treturn feedExistingRow(name, value);\n\t});\n}\n\nbool Editor::Inner::feedExistingRow(const QString &name, QLatin1String value) {\n\tauto data = value.data();\n\tauto size = value.size();\n\tif (data[0] != '#') {\n\t\treturn _existingRows->feedCopy(name, QString(value));\n\t}\n\tauto result = readColor(name, data + 1, size - 1);\n\tif (result.error) {\n\t\tLOG((\"Theme Warning: Skipping value '%1: %2' (expected a color value in #rrggbb or #rrggbbaa or a previously defined key in the color scheme)\").arg(name).arg(value));\n\t} else {\n\t\t_existingRows->feed(name, result.color);\n\t}\n\treturn true;\n}\n\nvoid Editor::Inner::applyEditing(const QString &name, const QString &copyOf, QColor value) {\n\tauto plainName = name.toLatin1();\n\tauto plainValue = copyOf.isEmpty() ? ColorHexString(value) : copyOf.toLatin1();\n\tauto newContent = ReplaceValueInPaletteContent(_paletteContent, plainName, plainValue);\n\tif (newContent == \"error\") {\n\t\tLOG((\"Theme Error: could not replace '%1: %2' in content\").arg(name, copyOf.isEmpty() ? QString::fromLatin1(ColorHexString(value)) : copyOf));\n\t\terror();\n\t\treturn;\n\t}\n\tapplyNewPalette(newContent);\n}\n\nvoid Editor::Inner::applyNewPalette(const QByteArray &newContent) {\n\tQFile f(_path);\n\tif (!f.open(QIODevice::WriteOnly)) {\n\t\tLOG((\"Theme Error: could not open '%1' for writing a palette update.\").arg(_path));\n\t\terror();\n\t\treturn;\n\t}\n\tif (f.write(newContent) != newContent.size()) {\n\t\tLOG((\"Theme Error: could not write all content to '%1' while writing a palette update.\").arg(_path));\n\t\terror();\n\t\treturn;\n\t}\n\tf.close();\n\n\t_applyingUpdate = true;\n\tif (!ApplyEditedPalette(newContent)) {\n\t\tLOG((\"Theme Error: could not apply newly composed content :(\"));\n\t\terror();\n\t\treturn;\n\t}\n\t_applyingUpdate = false;\n\n\t_paletteContent = newContent;\n}\n\nEditor::Editor(\n\tQWidget*,\n\tnot_null<Window::Controller*> window,\n\tconst Data::CloudTheme &cloud)\n: _window(window)\n, _cloud(cloud)\n, _scroll(this)\n, _close(this, st::defaultMultiSelect.fieldCancel)\n, _menuToggle(this, st::themesMenuToggle)\n, _select(this, st::defaultMultiSelect, tr::lng_country_ph())\n, _leftShadow(this)\n, _topShadow(this)\n, _save(\n\t\tthis,\n\t\ttr::lng_theme_editor_save_button(tr::now),\n\t\tst::dialogsUpdateButton) {\n\tconst auto path = EditingPalettePath();\n\n\t_inner = _scroll->setOwnedWidget(object_ptr<Inner>(this, path));\n\n\t_save->setClickedCallback(base::fn_delayed(\n\t\tst::defaultRippleAnimation.hideDuration,\n\t\tthis,\n\t\t[=] { save(); }));\n\n\t_inner->setErrorCallback([=] {\n\t\twindow->show(Ui::MakeInformBox(tr::lng_theme_editor_error()));\n\n\t\t// This could be from inner->_context observable notification.\n\t\t// We should not destroy it while iterating in subscribers.\n\t\tcrl::on_main(this, [=] {\n\t\t\tcloseEditor();\n\t\t});\n\t});\n\t_inner->setFocusCallback([this] {\n\t\tbase::call_delayed(2 * st::boxDuration, this, [this] {\n\t\t\t_select->setInnerFocus();\n\t\t});\n\t});\n\t_inner->setScrollCallback([this](int top, int bottom) {\n\t\t_scroll->scrollToY(top, bottom);\n\t});\n\t_menuToggle->setClickedCallback([=] {\n\t\tshowMenu();\n\t});\n\t_close->setClickedCallback([=] {\n\t\tcloseWithConfirmation();\n\t});\n\t_close->show(anim::type::instant);\n\n\t_select->resizeToWidth(st::windowMinWidth);\n\t_select->setQueryChangedCallback([this](const QString &query) { _inner->filterRows(query); _scroll->scrollToY(0); });\n\t_select->setSubmittedCallback([this](Qt::KeyboardModifiers) { _inner->chooseRow(); });\n\n\t_inner->prepare();\n\tresizeToWidth(st::windowMinWidth);\n}\n\nvoid Editor::showMenu() {\n\tif (_menu) {\n\t\treturn;\n\t}\n\t_menu = base::make_unique_q<Ui::DropdownMenu>(\n\t\tthis,\n\t\tst::dropdownMenuWithIcons);\n\t_menu->setHiddenCallback([weak = base::make_weak(this), menu = _menu.get()]{\n\t\tmenu->deleteLater();\n\t\tif (weak && weak->_menu == menu) {\n\t\t\tweak->_menu = nullptr;\n\t\t\tweak->_menuToggle->setForceRippled(false);\n\t\t}\n\t});\n\t_menu->setShowStartCallback(crl::guard(this, [this, menu = _menu.get()]{\n\t\tif (_menu == menu) {\n\t\t\t_menuToggle->setForceRippled(true);\n\t\t}\n\t}));\n\t_menu->setHideStartCallback(crl::guard(this, [this, menu = _menu.get()]{\n\t\tif (_menu == menu) {\n\t\t\t_menuToggle->setForceRippled(false);\n\t\t}\n\t}));\n\n\t_menuToggle->installEventFilter(_menu);\n\t_menu->addAction(tr::lng_theme_editor_menu_export(tr::now), [=] {\n\t\tbase::call_delayed(st::defaultRippleAnimation.hideDuration, this, [=] {\n\t\t\texportTheme();\n\t\t});\n\t}, &st::menuIconExportTheme);\n\t_menu->addAction(tr::lng_theme_editor_menu_import(tr::now), [=] {\n\t\tbase::call_delayed(st::defaultRippleAnimation.hideDuration, this, [=] {\n\t\t\timportTheme();\n\t\t});\n\t}, &st::menuIconImportTheme);\n\t_menu->addAction(tr::lng_theme_editor_menu_show(tr::now), [=] {\n\t\tFile::ShowInFolder(EditingPalettePath());\n\t}, &st::menuIconPalette);\n\t_menu->moveToRight(st::themesMenuPosition.x(), st::themesMenuPosition.y());\n\t_menu->showAnimated(Ui::PanelAnimation::Origin::TopRight);\n}\n\nvoid Editor::exportTheme() {\n\tauto caption = tr::lng_theme_editor_choose_name(tr::now);\n\tauto filter = \"Themes (*.tdesktop-theme)\";\n\tauto name = \"awesome.tdesktop-theme\";\n\tFileDialog::GetWritePath(this, caption, filter, name, crl::guard(this, [=](const QString &path) {\n\t\tconst auto result = CollectForExport(_inner->paletteContent());\n\t\tQFile f(path);\n\t\tif (!f.open(QIODevice::WriteOnly)) {\n\t\t\tLOG((\"Theme Error: could not open zip-ed theme file '%1' for writing\").arg(path));\n\t\t\t_window->show(Ui::MakeInformBox(tr::lng_theme_editor_error()));\n\t\t\treturn;\n\t\t}\n\t\tif (f.write(result) != result.size()) {\n\t\t\tLOG((\"Theme Error: could not write zip-ed theme to file '%1'\").arg(path));\n\t\t\t_window->show(Ui::MakeInformBox(tr::lng_theme_editor_error()));\n\t\t\treturn;\n\t\t}\n\t\t_window->showToast(tr::lng_theme_editor_done(tr::now));\n\t}));\n}\n\nvoid Editor::importTheme() {\n\tauto filters = QStringList(\n\t\tu\"Theme files (*.tdesktop-theme *.tdesktop-palette)\"_q);\n\tfilters.push_back(FileDialog::AllFilesFilter());\n\tconst auto callback = crl::guard(this, [=](\n\t\tconst FileDialog::OpenResult &result) {\n\t\tconst auto path = result.paths.isEmpty()\n\t\t\t? QString()\n\t\t\t: result.paths.front();\n\t\tif (path.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tauto f = QFile(path);\n\t\tif (!f.open(QIODevice::ReadOnly)) {\n\t\t\treturn;\n\t\t}\n\t\tauto object = Object();\n\t\tobject.pathAbsolute = QFileInfo(path).absoluteFilePath();\n\t\tobject.pathRelative = QDir().relativeFilePath(path);\n\t\tobject.content = f.readAll();\n\t\tif (object.content.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\t_select->clearQuery();\n\t\tconst auto parsed = ParseTheme(object, false, false);\n\t\t_inner->applyNewPalette(parsed.palette);\n\t\t_inner->recreateRows();\n\t\tupdateControlsGeometry();\n\t\tauto image = Images::Read({\n\t\t\t.content = parsed.background,\n\t\t\t.forceOpaque = true,\n\t\t}).image;\n\t\tif (!image.isNull() && !image.size().isEmpty()) {\n\t\t\tBackground()->set(Data::CustomWallPaper(), std::move(image));\n\t\t\tBackground()->setTile(parsed.tiled);\n\t\t\tUi::ForceFullRepaint(_window->widget());\n\t\t}\n\t});\n\tFileDialog::GetOpenPath(\n\t\tthis,\n\t\ttr::lng_theme_editor_menu_import(tr::now),\n\t\tfilters.join(u\";;\"_q),\n\t\tcrl::guard(this, callback));\n}\n\nQByteArray Editor::ColorizeInContent(\n\t\tQByteArray content,\n\t\tconst style::colorizer &colorizer) {\n\treturn Window::Theme::ColorizeInContent(content, colorizer);\n}\n\nvoid Editor::save() {\n\tif (Core::App().passcodeLocked()) {\n\t\t_window->showToast(tr::lng_theme_editor_need_unlock(tr::now));\n\t\treturn;\n\t} else if (!_window->account().sessionExists()) {\n\t\t_window->showToast(tr::lng_theme_editor_need_auth(tr::now));\n\t\treturn;\n\t} else if (_saving) {\n\t\treturn;\n\t}\n\t_saving = true;\n\tconst auto unlock = crl::guard(this, [=] { _saving = false; });\n\tSaveTheme(_window, _cloud, _inner->paletteContent(), unlock);\n}\n\nvoid Editor::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid Editor::updateControlsGeometry() {\n\t_save->resizeToWidth(width());\n\t_close->moveToRight(0, 0);\n\t_menuToggle->moveToRight(_close->width(), 0);\n\n\t_select->resizeToWidth(width());\n\t_select->moveToLeft(0, _close->height());\n\n\tauto shadowTop = _select->y() + _select->height();\n\n\t_topShadow->resize(width() - st::lineWidth, st::lineWidth);\n\t_topShadow->moveToLeft(st::lineWidth, shadowTop);\n\t_leftShadow->resize(st::lineWidth, height());\n\t_leftShadow->moveToLeft(0, 0);\n\tauto scrollSize = QSize(width(), height() - shadowTop - _save->height());\n\tif (_scroll->size() != scrollSize) {\n\t\t_scroll->resize(scrollSize);\n\t}\n\t_inner->resizeToWidth(width());\n\t_scroll->moveToLeft(0, shadowTop);\n\tif (!_scroll->isHidden()) {\n\t\tauto scrollTop = _scroll->scrollTop();\n\t\t_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());\n\t}\n\t_save->moveToLeft(0, _scroll->y() + _scroll->height());\n}\n\nvoid Editor::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Escape) {\n\t\tif (!_select->getQuery().isEmpty()) {\n\t\t\t_select->clearQuery();\n\t\t} else {\n\t\t\t_window->widget()->setInnerFocus();\n\t\t}\n\t} else if (e->key() == Qt::Key_Down) {\n\t\t_inner->selectSkip(1);\n\t} else if (e->key() == Qt::Key_Up) {\n\t\t_inner->selectSkip(-1);\n\t} else if (e->key() == Qt::Key_PageDown) {\n\t\t_inner->selectSkipPage(_scroll->height(), 1);\n\t} else if (e->key() == Qt::Key_PageUp) {\n\t\t_inner->selectSkipPage(_scroll->height(), -1);\n\t}\n}\n\nvoid Editor::focusInEvent(QFocusEvent *e) {\n\t_select->setInnerFocus();\n}\n\nvoid Editor::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tp.fillRect(e->rect(), st::dialogsBg);\n\n\tp.setFont(st::boxTitleFont);\n\tp.setPen(st::windowFg);\n\tp.drawTextLeft(st::themeEditorMargin.left(), st::themeEditorMargin.top(), width(), tr::lng_theme_editor_title(tr::now));\n}\n\nvoid Editor::closeWithConfirmation() {\n\tif (!PaletteChanged(_inner->paletteContent(), _cloud)) {\n\t\tBackground()->clearEditingTheme(ClearEditing::KeepChanges);\n\t\tcloseEditor();\n\t\treturn;\n\t}\n\tconst auto close = crl::guard(this, [=](Fn<void()> &&close) {\n\t\tBackground()->clearEditingTheme(ClearEditing::RevertChanges);\n\t\tcloseEditor();\n\t\tclose();\n\t});\n\t_window->show(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_theme_editor_sure_close(),\n\t\t.confirmed = close,\n\t\t.confirmText = tr::lng_close(),\n\t}));\n}\n\nvoid Editor::closeEditor() {\n\t_window->widget()->showRightColumn(nullptr);\n\tBackground()->clearEditingTheme();\n}\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_theme_editor.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_cloud_themes.h\"\n#include \"ui/rp_widget.h\"\n#include \"base/object_ptr.h\"\n\nnamespace style {\nstruct colorizer;\n} // namespace style\n\nnamespace Ui {\nclass FlatButton;\nclass ScrollArea;\nclass CrossButton;\nclass MultiSelect;\nclass PlainShadow;\nclass DropdownMenu;\nclass IconButton;\n} // namespace Ui\n\nnamespace Window {\n\nclass Controller;\n\nnamespace Theme {\n\nstruct ParsedTheme {\n\tQByteArray palette;\n\tQByteArray background;\n\tbool isPng = false;\n\tbool tiled = false;\n};\n\n[[nodiscard]] QByteArray ColorHexString(const QColor &color);\n[[nodiscard]] QByteArray ReplaceValueInPaletteContent(\n\tconst QByteArray &content,\n\tconst QByteArray &name,\n\tconst QByteArray &value);\n[[nodiscard]] QByteArray WriteCloudToText(const Data::CloudTheme &cloud);\n[[nodiscard]] Data::CloudTheme ReadCloudFromText(const QByteArray &text);\n[[nodiscard]] QByteArray StripCloudTextFields(const QByteArray &text);\n\nclass Editor : public Ui::RpWidget {\npublic:\n\tEditor(\n\t\tQWidget*,\n\t\tnot_null<Window::Controller*> window,\n\t\tconst Data::CloudTheme &cloud);\n\n\t[[nodiscard]] static QByteArray ColorizeInContent(\n\t\tQByteArray content,\n\t\tconst style::colorizer &colorizer);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\n\tvoid focusInEvent(QFocusEvent *e) override;\n\nprivate:\n\tvoid save();\n\tvoid showMenu();\n\tvoid exportTheme();\n\tvoid importTheme();\n\tvoid closeEditor();\n\tvoid closeWithConfirmation();\n\tvoid updateControlsGeometry();\n\n\tconst not_null<Window::Controller*> _window;\n\tconst Data::CloudTheme _cloud;\n\n\tobject_ptr<Ui::ScrollArea> _scroll;\n\tclass Inner;\n\tQPointer<Inner> _inner;\n\tobject_ptr<Ui::CrossButton> _close;\n\tobject_ptr<Ui::IconButton> _menuToggle;\n\tbase::unique_qptr<Ui::DropdownMenu> _menu;\n\tobject_ptr<Ui::MultiSelect> _select;\n\tobject_ptr<Ui::PlainShadow> _leftShadow;\n\tobject_ptr<Ui::PlainShadow> _topShadow;\n\tobject_ptr<Ui::FlatButton> _save;\n\tbool _saving = false;\n\n};\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_theme_editor_block.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/themes/window_theme_editor_block.h\"\n\n#include \"base/call_delayed.h\"\n#include \"boxes/abstract_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/effects/ripple_animation.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/painter.h\"\n#include \"ui/widgets/color_editor.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_window.h\"\n\nnamespace Window {\nnamespace Theme {\nnamespace {\n\nauto SearchSplitter = QRegularExpression(u\"[\\\\@\\\\s\\\\-\\\\+\\\\(\\\\)\\\\[\\\\]\\\\{\\\\}\\\\<\\\\>\\\\,\\\\.\\\\:\\\\!\\\\_\\\\;\\\\\\\"\\\\'\\\\x0\\\\#]\"_q);\n\n} // namespace\n\nclass EditorBlock::Row {\npublic:\n\tRow(const QString &name, const QString &copyOf, QColor value);\n\n\tQString name() const {\n\t\treturn _name;\n\t}\n\n\tvoid setCopyOf(const QString &copyOf) {\n\t\t_copyOf = copyOf;\n\t\tfillSearchIndex();\n\t}\n\tQString copyOf() const {\n\t\treturn _copyOf;\n\t}\n\n\tvoid setValue(QColor value);\n\tconst QColor &value() const {\n\t\treturn _value;\n\t}\n\n\tQString description() const {\n\t\treturn _description.toString();\n\t}\n\tconst Ui::Text::String &descriptionText() const {\n\t\treturn _description;\n\t}\n\tvoid setDescription(const QString &description) {\n\t\t_description.setText(st::defaultTextStyle, description);\n\t\tfillSearchIndex();\n\t}\n\n\tconst base::flat_set<QString> &searchWords() const {\n\t\treturn _searchWords;\n\t}\n\tbool searchWordsContain(const QString &needle) const {\n\t\tfor (const auto &word : _searchWords) {\n\t\t\tif (word.startsWith(needle)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\tconst base::flat_set<QChar> &searchStartChars() const {\n\t\treturn _searchStartChars;\n\t}\n\n\tvoid setTop(int top) {\n\t\t_top = top;\n\t}\n\tint top() const {\n\t\treturn _top;\n\t}\n\n\tvoid setHeight(int height) {\n\t\t_height = height;\n\t}\n\tint height() const {\n\t\treturn _height;\n\t}\n\n\tUi::RippleAnimation *ripple() const {\n\t\treturn _ripple.get();\n\t}\n\tUi::RippleAnimation *setRipple(std::unique_ptr<Ui::RippleAnimation> ripple) const {\n\t\t_ripple = std::move(ripple);\n\t\treturn _ripple.get();\n\t}\n\tvoid resetRipple() const {\n\t\t_ripple = nullptr;\n\t}\n\nprivate:\n\tvoid fillValueString();\n\tvoid fillSearchIndex();\n\n\tQString _name;\n\tQString _copyOf;\n\tQColor _value;\n\tQString _valueString;\n\tUi::Text::String _description = { st::windowMinWidth / 2 };\n\n\tbase::flat_set<QString> _searchWords;\n\tbase::flat_set<QChar> _searchStartChars;\n\n\tint _top = 0;\n\tint _height = 0;\n\n\tmutable std::unique_ptr<Ui::RippleAnimation> _ripple;\n\n};\n\nEditorBlock::Row::Row(const QString &name, const QString &copyOf, QColor value)\n: _name(name)\n, _copyOf(copyOf) {\n\tsetValue(value);\n}\n\nvoid EditorBlock::Row::setValue(QColor value) {\n\t_value = value;\n\tfillValueString();\n\tfillSearchIndex();\n}\n\nvoid EditorBlock::Row::fillValueString() {\n\tauto addHex = [=](int code) {\n\t\tif (code >= 0 && code < 10) {\n\t\t\t_valueString.append(QChar('0' + code));\n\t\t} else if (code >= 10 && code < 16) {\n\t\t\t_valueString.append(QChar('a' + (code - 10)));\n\t\t}\n\t};\n\tauto addCode = [=](int code) {\n\t\taddHex(code / 16);\n\t\taddHex(code % 16);\n\t};\n\t_valueString.resize(0);\n\t_valueString.reserve(9);\n\t_valueString.append('#');\n\taddCode(_value.red());\n\taddCode(_value.green());\n\taddCode(_value.blue());\n\tif (_value.alpha() != 255) {\n\t\taddCode(_value.alpha());\n\t}\n}\n\nvoid EditorBlock::Row::fillSearchIndex() {\n\t_searchWords.clear();\n\t_searchStartChars.clear();\n\tconst auto toIndex = _name\n\t\t+ ' ' + _copyOf\n\t\t+ ' ' + TextUtilities::RemoveAccents(_description.toString())\n\t\t+ ' ' + _valueString;\n\tconst auto words = toIndex.toLower().split(\n\t\tSearchSplitter,\n\t\tQt::SkipEmptyParts);\n\tfor (const auto &word : words) {\n\t\t_searchWords.emplace(word);\n\t\t_searchStartChars.emplace(word[0]);\n\t}\n}\n\nEditorBlock::EditorBlock(QWidget *parent, Type type, Context *context)\n: RpWidget(parent)\n, _type(type)\n, _context(context)\n, _transparent(style::TransparentPlaceholder()) {\n\tsetMouseTracking(true);\n\n\t_context->updated.events(\n\t) | rpl::on_next([=] {\n\t\tif (_mouseSelection) {\n\t\t\t_lastGlobalPos = QCursor::pos();\n\t\t\tupdateSelected(mapFromGlobal(_lastGlobalPos));\n\t\t}\n\t\tupdate();\n\t}, lifetime());\n\n\tif (_type == Type::Existing) {\n\t\t_context->appended.events(\n\t\t) | rpl::on_next([=](const Context::AppendData &added) {\n\t\t\tauto name = added.name;\n\t\t\tauto value = added.value;\n\t\t\tfeed(name, value);\n\t\t\tfeedDescription(name, added.description);\n\n\t\t\tauto row = findRow(name);\n\t\t\tAssert(row != nullptr);\n\t\t\tauto possibleCopyOf = added.possibleCopyOf;\n\t\t\tauto copyOf = checkCopyOf(findRowIndex(row), possibleCopyOf) ? possibleCopyOf : QString();\n\t\t\tremoveFromSearch(*row);\n\t\t\trow->setCopyOf(copyOf);\n\t\t\taddToSearch(*row);\n\n\t\t\t_context->changed.fire({ QStringList(name), value });\n\t\t\t_context->resized.fire({});\n\t\t\t_context->pending.fire({ name, copyOf, value });\n\t\t}, lifetime());\n\t} else {\n\t\t_context->changed.events(\n\t\t) | rpl::on_next([=](const Context::ChangeData &data) {\n\t\t\tcheckCopiesChanged(0, data.names, data.value);\n\t\t}, lifetime());\n\t}\n}\n\nvoid EditorBlock::feed(const QString &name, QColor value, const QString &copyOfExisting) {\n\tif (findRow(name)) {\n\t\t// Remove the existing row and mark all its copies as unique keys.\n\t\tLOG((\"Theme Warning: Color value '%1' appears more than once in the color scheme.\").arg(name));\n\t\tremoveRow(name);\n\t}\n\taddRow(name, copyOfExisting, value);\n}\n\nbool EditorBlock::feedCopy(const QString &name, const QString &copyOf) {\n\tif (auto row = findRow(copyOf)) {\n\t\tif (copyOf == name) {\n\t\t\tLOG((\"Theme Warning: Skipping value '%1: %2' (the value refers to itself.)\").arg(name, copyOf));\n\t\t\treturn true;\n\t\t}\n\t\tif (findRow(name)) {\n\t\t\t// Remove the existing row and mark all its copies as unique keys.\n\t\t\tLOG((\"Theme Warning: Color value '%1' appears more than once in the color scheme.\").arg(name));\n\t\t\tremoveRow(name);\n\n\t\t\t// row was invalidated by removeRow() call.\n\t\t\trow = findRow(copyOf);\n\t\t\t// Should not happen, but still check.\n\t\t\tif (!row) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\taddRow(name, copyOf, row->value());\n\t} else {\n\t\tLOG((\"Theme Warning: Skipping value '%1: %2' (expected a color value in #rrggbb or #rrggbbaa or a previously defined key in the color scheme)\").arg(name, copyOf));\n\t}\n\treturn true;\n}\n\nvoid EditorBlock::removeRow(const QString &name, bool removeCopyReferences) {\n\tauto it = _indices.find(name);\n\tAssert(it != _indices.cend());\n\n\tauto index = it.value();\n\tfor (auto i = index + 1, count = static_cast<int>(_data.size()); i != count; ++i) {\n\t\tauto &row = _data[i];\n\t\tremoveFromSearch(row);\n\t\t_indices[row.name()] = i - 1;\n\t\tif (removeCopyReferences && row.copyOf() == name) {\n\t\t\trow.setCopyOf(QString());\n\t\t}\n\t}\n\tremoveFromSearch(_data[index]);\n\t_data.erase(_data.begin() + index);\n\t_indices.erase(it);\n\tfor (auto i = index, count = static_cast<int>(_data.size()); i != count; ++i) {\n\t\taddToSearch(_data[i]);\n\t}\n}\n\nvoid EditorBlock::addToSearch(const Row &row) {\n\tauto query = _searchQuery;\n\tif (!query.isEmpty()) resetSearch();\n\n\tauto index = findRowIndex(&row);\n\tfor (const auto &ch : row.searchStartChars()) {\n\t\t_searchIndex[ch].insert(index);\n\t}\n\n\tif (!query.isEmpty()) searchByQuery(query);\n}\n\nvoid EditorBlock::removeFromSearch(const Row &row) {\n\tauto query = _searchQuery;\n\tif (!query.isEmpty()) resetSearch();\n\n\tauto index = findRowIndex(&row);\n\tfor (const auto &ch : row.searchStartChars()) {\n\t\tconst auto i = _searchIndex.find(ch);\n\t\tif (i != end(_searchIndex)) {\n\t\t\ti->second.remove(index);\n\t\t\tif (i->second.empty()) {\n\t\t\t\t_searchIndex.erase(i);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!query.isEmpty()) searchByQuery(query);\n}\n\nvoid EditorBlock::filterRows(const QString &query) {\n\tsearchByQuery(query);\n}\n\nvoid EditorBlock::chooseRow() {\n\tif (_selected < 0) {\n\t\treturn;\n\t}\n\tactivateRow(rowAtIndex(_selected));\n}\n\nvoid EditorBlock::activateRow(const Row &row) {\n\tif (_context->colorEditor.editor) {\n\t\tif (_type == Type::Existing) {\n\t\t\t_context->possibleCopyOf = row.name();\n\t\t\t_context->colorEditor.editor->showColor(row.value());\n\t\t}\n\t} else {\n\t\t_editing = findRowIndex(&row);\n\t\tconst auto name = row.name();\n\t\tconst auto value = row.value();\n\t\tUi::show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\t\tconst auto editor = box->addRow(object_ptr<ColorEditor>(\n\t\t\t\tbox,\n\t\t\t\tColorEditor::Mode::RGBA,\n\t\t\t\tvalue));\n\t\t\tstruct State {\n\t\t\t\trpl::lifetime cancelLifetime;\n\t\t\t};\n\t\t\tconst auto state = editor->lifetime().make_state<State>();\n\n\t\t\tconst auto save = crl::guard(this, [=] {\n\t\t\t\tstate->cancelLifetime.destroy();\n\t\t\t\tsaveEditing(editor->color());\n\t\t\t});\n\t\t\tbox->boxClosing(\n\t\t\t) | rpl::on_next(crl::guard(this, [=] {\n\t\t\t\tcancelEditing();\n\t\t\t}), state->cancelLifetime);\n\t\t\teditor->submitRequests(\n\t\t\t) | rpl::on_next(save, editor->lifetime());\n\n\t\t\tbox->setFocusCallback([=] {\n\t\t\t\teditor->setInnerFocus();\n\t\t\t});\n\t\t\tbox->addButton(tr::lng_settings_save(), save);\n\t\t\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n\t\t\tbox->setTitle(name);\n\t\t\tbox->setWidth(editor->width());\n\n\t\t\t_context->colorEditor.box = box;\n\t\t\t_context->colorEditor.editor = editor;\n\t\t\t_context->name = name;\n\t\t\t_context->updated.fire({});\n\t\t}));\n\t}\n}\n\nbool EditorBlock::selectSkip(int direction) {\n\t_mouseSelection = false;\n\n\tauto maxSelected = size_type(isSearch()\n\t\t? _searchResults.size()\n\t\t: _data.size()) - 1;\n\tauto newSelected = _selected + direction;\n\tif (newSelected < -1 || newSelected > maxSelected) {\n\t\tnewSelected = maxSelected;\n\t}\n\tif (newSelected != _selected) {\n\t\tsetSelected(newSelected);\n\t\tscrollToSelected();\n\t\treturn (newSelected >= 0);\n\t}\n\treturn false;\n}\n\nvoid EditorBlock::scrollToSelected() {\n\tif (_selected >= 0) {\n\t\tconst auto &row = rowAtIndex(_selected);\n\t\t_context->scroll.fire({ _type, row.top(), row.height() });\n\t}\n}\n\nvoid EditorBlock::searchByQuery(QString query) {\n\tconst auto words = TextUtilities::PrepareSearchWords(\n\t\tquery,\n\t\t&SearchSplitter);\n\tquery = words.isEmpty() ? QString() : words.join(' ');\n\tif (_searchQuery != query) {\n\t\tsetSelected(-1);\n\t\tsetPressed(-1);\n\n\t\t_searchQuery = query;\n\t\t_searchResults.clear();\n\n\t\tauto toFilter = (base::flat_set<int>*)nullptr;\n\t\tfor (const auto &word : words) {\n\t\t\tif (word.isEmpty()) continue;\n\n\t\t\tconst auto i = _searchIndex.find(word[0]);\n\t\t\tif (i == end(_searchIndex) || i->second.empty()) {\n\t\t\t\ttoFilter = nullptr;\n\t\t\t\tbreak;\n\t\t\t} else if (!toFilter || i->second.size() < toFilter->size()) {\n\t\t\t\ttoFilter = &i->second;\n\t\t\t}\n\t\t}\n\t\tif (toFilter) {\n\t\t\tconst auto allWordsFound = [&](const Row &row) {\n\t\t\t\tfor (const auto &word : words) {\n\t\t\t\t\tif (!row.searchWordsContain(word)) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t};\n\t\t\tfor (const auto index : *toFilter) {\n\t\t\t\tif (allWordsFound(_data[index])) {\n\t\t\t\t\t_searchResults.push_back(index);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t_context->resized.fire({});\n\t}\n}\n\nconst QColor *EditorBlock::find(const QString &name) {\n\tif (auto row = findRow(name)) {\n\t\treturn &row->value();\n\t}\n\treturn nullptr;\n}\n\nbool EditorBlock::feedDescription(const QString &name, const QString &description) {\n\tif (auto row = findRow(name)) {\n\t\tremoveFromSearch(*row);\n\t\trow->setDescription(description);\n\t\taddToSearch(*row);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid EditorBlock::sortByDistance(const QColor &to) {\n\tauto toHue = int();\n\tauto toSaturation = int();\n\tauto toLightness = int();\n\tto.getHsl(&toHue, &toSaturation, &toLightness);\n\tranges::sort(_data, ranges::less(), [&](const Row &row) {\n\t\tauto fromHue = int();\n\t\tauto fromSaturation = int();\n\t\tauto fromLightness = int();\n\t\trow.value().getHsl(&fromHue, &fromSaturation, &fromLightness);\n\t\tif (!row.copyOf().isEmpty()) {\n\t\t\treturn 365;\n\t\t}\n\t\tconst auto a = std::abs(fromHue - toHue);\n\t\tconst auto b = 360 + fromHue - toHue;\n\t\tconst auto c = 360 + toHue - fromHue;\n\t\tif (std::min(a, std::min(b, c)) > 15) {\n\t\t\treturn 363;\n\t\t}\n\t\treturn 255 - fromSaturation;\n\t});\n}\n\ntemplate <typename Callback>\nvoid EditorBlock::enumerateRows(Callback callback) {\n\tif (isSearch()) {\n\t\tfor (const auto index : _searchResults) {\n\t\t\tif (!callback(_data[index])) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor (auto &row : _data) {\n\t\t\tif (!callback(row)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\ntemplate <typename Callback>\nvoid EditorBlock::enumerateRows(Callback callback) const {\n\tif (isSearch()) {\n\t\tfor (const auto index : _searchResults) {\n\t\t\tif (!callback(_data[index])) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor (const auto &row : _data) {\n\t\t\tif (!callback(row)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\ntemplate <typename Callback>\nvoid EditorBlock::enumerateRowsFrom(int top, Callback callback) {\n\tauto started = false;\n\tauto index = 0;\n\tenumerateRows([top, callback, &started, &index](Row &row) {\n\t\tif (!started) {\n\t\t\tif (row.top() + row.height() <= top) {\n\t\t\t\t++index;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tstarted = true;\n\t\t}\n\t\treturn callback(index++, row);\n\t});\n}\n\ntemplate <typename Callback>\nvoid EditorBlock::enumerateRowsFrom(int top, Callback callback) const {\n\tauto started = false;\n\tenumerateRows([top, callback, &started](const Row &row) {\n\t\tif (!started) {\n\t\t\tif (row.top() + row.height() <= top) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tstarted = true;\n\t\t}\n\t\treturn callback(row);\n\t});\n}\n\nint EditorBlock::resizeGetHeight(int newWidth) {\n\tauto result = 0;\n\tauto descriptionWidth = newWidth - st::themeEditorMargin.left() - st::themeEditorMargin.right();\n\tenumerateRows([&](Row &row) {\n\t\trow.setTop(result);\n\n\t\tauto height = row.height();\n\t\tif (!height) {\n\t\t\theight = st::themeEditorMargin.top() + st::themeEditorSampleSize.height();\n\t\t\tif (!row.descriptionText().isEmpty()) {\n\t\t\t\theight += st::themeEditorDescriptionSkip + row.descriptionText().countHeight(descriptionWidth);\n\t\t\t}\n\t\t\theight += st::themeEditorMargin.bottom();\n\t\t\trow.setHeight(height);\n\t\t}\n\t\tresult += row.height();\n\t\treturn true;\n\t});\n\n\tif (_type == Type::New) {\n\t\tsetHidden(!result);\n\t}\n\tif (_type == Type::Existing && !result && !isSearch()) {\n\t\treturn st::noContactsHeight;\n\t}\n\treturn result;\n}\n\nvoid EditorBlock::mousePressEvent(QMouseEvent *e) {\n\tupdateSelected(e->pos());\n\tsetPressed(_selected);\n}\n\nvoid EditorBlock::mouseReleaseEvent(QMouseEvent *e) {\n\tauto pressed = _pressed;\n\tsetPressed(-1);\n\tif (pressed == _selected) {\n\t\tif (_context->colorEditor.box) {\n\t\t\tchooseRow();\n\t\t} else if (_selected >= 0) {\n\t\t\tbase::call_delayed(st::defaultRippleAnimation.hideDuration, this, [this, index = findRowIndex(&rowAtIndex(_selected))] {\n\t\t\t\tif (index >= 0 && index < _data.size()) {\n\t\t\t\t\tactivateRow(_data[index]);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n}\n\nvoid EditorBlock::saveEditing(QColor value) {\n\tif (_editing < 0) {\n\t\treturn;\n\t}\n\tauto &row = _data[_editing];\n\tauto name = row.name();\n\tif (_type == Type::New) {\n\t\tsetSelected(-1);\n\t\tsetPressed(-1);\n\n\t\tauto possibleCopyOf = _context->possibleCopyOf.isEmpty() ? row.copyOf() : _context->possibleCopyOf;\n\t\tauto color = value;\n\t\tauto description = row.description();\n\n\t\tremoveRow(name, false);\n\n\t\t_context->appended.fire({ name, possibleCopyOf, color, description });\n\t} else if (_type == Type::Existing) {\n\t\tremoveFromSearch(row);\n\n\t\tauto valueChanged = (row.value() != value);\n\t\tif (valueChanged) {\n\t\t\trow.setValue(value);\n\t\t}\n\n\t\tauto possibleCopyOf = _context->possibleCopyOf.isEmpty() ? row.copyOf() : _context->possibleCopyOf;\n\t\tauto copyOf = checkCopyOf(_editing, possibleCopyOf) ? possibleCopyOf : QString();\n\t\tauto copyOfChanged = (row.copyOf() != copyOf);\n\t\tif (copyOfChanged) {\n\t\t\trow.setCopyOf(copyOf);\n\t\t}\n\n\t\taddToSearch(row);\n\n\t\tif (valueChanged || copyOfChanged) {\n\t\t\tcheckCopiesChanged(_editing + 1, QStringList(name), value);\n\t\t\t_context->pending.fire({ name, copyOf, value });\n\t\t}\n\t}\n\tcancelEditing();\n}\n\nvoid EditorBlock::checkCopiesChanged(int startIndex, QStringList names, QColor value) {\n\tfor (auto i = startIndex, count = static_cast<int>(_data.size()); i != count; ++i) {\n\t\tauto &checkIfIsCopy = _data[i];\n\t\tif (names.contains(checkIfIsCopy.copyOf())) {\n\t\t\tremoveFromSearch(checkIfIsCopy);\n\t\t\tcheckIfIsCopy.setValue(value);\n\t\t\tnames.push_back(checkIfIsCopy.name());\n\t\t\taddToSearch(checkIfIsCopy);\n\t\t}\n\t}\n\tif (_type == Type::Existing) {\n\t\t_context->changed.fire({ names, value });\n\t}\n}\n\nvoid EditorBlock::cancelEditing() {\n\t// The only place we need to check for _editing < _data.size(),\n\t// because we could remove the row that was edited in New block,\n\t// and it could be the last row, making _editing inconsistent.\n\tif (_editing >= 0 && _editing < _data.size()) {\n\t\tupdateRow(_data[_editing]);\n\t}\n\t_editing = -1;\n\tif (const auto box = base::take(_context->colorEditor.box)) {\n\t\tbox->closeBox();\n\t}\n\t_context->possibleCopyOf = QString();\n\tif (!_context->name.isEmpty()) {\n\t\t_context->name = QString();\n\t\t_context->updated.fire({});\n\t}\n}\n\nbool EditorBlock::checkCopyOf(int index, const QString &possibleCopyOf) {\n\tauto copyOfIndex = findRowIndex(possibleCopyOf);\n\treturn (copyOfIndex >= 0\n\t\t&& index > copyOfIndex\n\t\t&& _data[copyOfIndex].value().toRgb() == _data[index].value().toRgb());\n}\n\nvoid EditorBlock::mouseMoveEvent(QMouseEvent *e) {\n\tif (_lastGlobalPos != e->globalPos() || _mouseSelection) {\n\t\t_lastGlobalPos = e->globalPos();\n\t\tupdateSelected(e->pos());\n\t}\n}\n\nvoid EditorBlock::updateSelected(QPoint localPosition) {\n\t_mouseSelection = true;\n\tauto top = localPosition.y();\n\tauto underMouseIndex = -1;\n\tenumerateRowsFrom(top, [&underMouseIndex, top](int index, const Row &row) {\n\t\tif (row.top() <= top) {\n\t\t\tunderMouseIndex = index;\n\t\t}\n\t\treturn false;\n\t});\n\tsetSelected(underMouseIndex);\n}\n\nvoid EditorBlock::leaveEventHook(QEvent *e) {\n\t_mouseSelection = false;\n\tsetSelected(-1);\n}\n\nvoid EditorBlock::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tauto clip = e->rect();\n\tif (_data.empty()) {\n\t\tp.fillRect(clip, st::dialogsBg);\n\t\tp.setFont(st::noContactsFont);\n\t\tp.setPen(st::noContactsColor);\n\t\tp.drawText(QRect(0, 0, width(), st::noContactsHeight), tr::lng_theme_editor_no_keys(tr::now));\n\t}\n\n\tauto cliptop = clip.y();\n\tauto clipbottom = cliptop + clip.height();\n\tenumerateRowsFrom(cliptop, [&](int index, const Row &row) {\n\t\tif (row.top() >= clipbottom) {\n\t\t\treturn false;\n\t\t}\n\t\tpaintRow(p, index, row);\n\t\treturn true;\n\t});\n}\n\nvoid EditorBlock::paintRow(Painter &p, int index, const Row &row) {\n\tauto rowTop = row.top() + st::themeEditorMargin.top();\n\n\tauto rect = QRect(0, row.top(), width(), row.height());\n\tauto selected = (_pressed >= 0) ? (index == _pressed) : (index == _selected);\n\tauto active = (findRowIndex(&row) == _editing);\n\tp.fillRect(rect, active ? st::dialogsBgActive : selected ? st::dialogsBgOver : st::dialogsBg);\n\tif (auto ripple = row.ripple()) {\n\t\tripple->paint(p, 0, row.top(), width(), &(active ? st::activeButtonBgRipple : st::windowBgRipple)->c);\n\t\tif (ripple->empty()) {\n\t\t\trow.resetRipple();\n\t\t}\n\t}\n\n\tauto sample = QRect(width() - st::themeEditorMargin.right() - st::themeEditorSampleSize.width(), rowTop, st::themeEditorSampleSize.width(), st::themeEditorSampleSize.height());\n\tUi::Shadow::paint(p, sample, width(), st::defaultRoundShadow);\n\tif (row.value().alpha() != 255) {\n\t\tp.fillRect(myrtlrect(sample), _transparent);\n\t}\n\tp.fillRect(myrtlrect(sample), row.value());\n\n\tauto rowWidth = width() - st::themeEditorMargin.left() - st::themeEditorMargin.right();\n\tauto nameWidth = rowWidth - st::themeEditorSampleSize.width() - st::themeEditorDescriptionSkip;\n\n\tp.setFont(st::themeEditorNameFont);\n\tp.setPen(active ? st::dialogsNameFgActive : selected ? st::dialogsNameFgOver : st::dialogsNameFg);\n\tp.drawTextLeft(st::themeEditorMargin.left(), rowTop, width(), st::themeEditorNameFont->elided(row.name(), nameWidth));\n\n\tif (!row.copyOf().isEmpty()) {\n\t\tauto copyTop = rowTop + st::themeEditorNameFont->height;\n\t\tp.setFont(st::themeEditorCopyNameFont);\n\t\tp.drawTextLeft(st::themeEditorMargin.left(), copyTop, width(), st::themeEditorCopyNameFont->elided(\"= \" + row.copyOf(), nameWidth));\n\t}\n\n\tif (!row.descriptionText().isEmpty()) {\n\t\tauto descriptionTop = rowTop + st::themeEditorSampleSize.height() + st::themeEditorDescriptionSkip;\n\t\tp.setPen(active ? st::dialogsTextFgActive : selected ? st::dialogsTextFgOver : st::dialogsTextFg);\n\t\trow.descriptionText().drawLeft(p, st::themeEditorMargin.left(), descriptionTop, rowWidth, width());\n\t}\n\n\tif (isEditing() && !active && (_type == Type::New || (_editing >= 0 && findRowIndex(&row) >= _editing))) {\n\t\tp.fillRect(rect, st::layerBg);\n\t}\n}\n\nvoid EditorBlock::setSelected(int selected) {\n\tif (isEditing()) {\n\t\tif (_type == Type::New) {\n\t\t\tselected = -1;\n\t\t} else if (_editing >= 0 && selected >= 0 && findRowIndex(&rowAtIndex(selected)) >= _editing) {\n\t\t\tselected = -1;\n\t\t}\n\t}\n\tif (_selected != selected) {\n\t\tif (_selected >= 0) updateRow(rowAtIndex(_selected));\n\t\t_selected = selected;\n\t\tif (_selected >= 0) updateRow(rowAtIndex(_selected));\n\t\tsetCursor((_selected >= 0) ? style::cur_pointer : style::cur_default);\n\t}\n}\n\nvoid EditorBlock::setPressed(int pressed) {\n\tif (_pressed != pressed) {\n\t\tif (_pressed >= 0) {\n\t\t\tupdateRow(rowAtIndex(_pressed));\n\t\t\tstopLastRipple(_pressed);\n\t\t}\n\t\t_pressed = pressed;\n\t\tif (_pressed >= 0) {\n\t\t\taddRowRipple(_pressed);\n\t\t\tupdateRow(rowAtIndex(_pressed));\n\t\t}\n\t}\n}\n\nvoid EditorBlock::addRowRipple(int index) {\n\tauto &row = rowAtIndex(index);\n\tauto ripple = row.ripple();\n\tif (!ripple) {\n\t\tauto mask = Ui::RippleAnimation::RectMask(QSize(width(), row.height()));\n\t\tripple = row.setRipple(std::make_unique<Ui::RippleAnimation>(st::defaultRippleAnimation, std::move(mask), [this, index = findRowIndex(&row)] {\n\t\t\tupdateRow(_data[index]);\n\t\t}));\n\t}\n\tauto origin = mapFromGlobal(QCursor::pos()) - QPoint(0, row.top());\n\tripple->add(origin);\n}\n\nvoid EditorBlock::stopLastRipple(int index) {\n\tauto &row = rowAtIndex(index);\n\tif (row.ripple()) {\n\t\trow.ripple()->lastStop();\n\t}\n}\n\nvoid EditorBlock::updateRow(const Row &row) {\n\tupdate(0, row.top(), width(), row.height());\n}\n\nvoid EditorBlock::addRow(const QString &name, const QString &copyOf, QColor value) {\n\t_data.push_back({ name, copyOf, value });\n\t_indices.insert(name, _data.size() - 1);\n\taddToSearch(_data.back());\n}\n\nEditorBlock::Row &EditorBlock::rowAtIndex(int index) {\n\tif (isSearch()) {\n\t\treturn _data[_searchResults[index]];\n\t}\n\treturn _data[index];\n}\n\nint EditorBlock::findRowIndex(const QString &name) const {\n\treturn _indices.value(name, -1);\n}\n\nEditorBlock::Row *EditorBlock::findRow(const QString &name) {\n\tauto index = findRowIndex(name);\n\treturn (index >= 0) ? &_data[index] : nullptr;\n}\n\nint EditorBlock::findRowIndex(const Row *row) {\n\treturn row ? (row - &_data[0]) : -1;\n}\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_theme_editor_block.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\nclass BoxContent;\n} // namespace Ui;\n\nclass ColorEditor;\n\nnamespace Window {\nnamespace Theme {\n\nclass EditorBlock final : public Ui::RpWidget {\npublic:\n\tenum class Type {\n\t\tExisting,\n\t\tNew,\n\t};\n\tstruct Context {\n\t\tstruct {\n\t\t\tbase::weak_qptr<Ui::BoxContent> box;\n\t\t\tbase::weak_qptr<ColorEditor> editor;\n\t\t} colorEditor;\n\t\tQString name;\n\t\tQString possibleCopyOf;\n\n\t\trpl::event_stream<> updated;\n\t\trpl::event_stream<> resized;\n\n\t\tstruct AppendData {\n\t\t\tQString name;\n\t\t\tQString possibleCopyOf;\n\t\t\tQColor value;\n\t\t\tQString description;\n\t\t};\n\t\trpl::event_stream<AppendData> appended;\n\n\t\tstruct ChangeData {\n\t\t\tQStringList names;\n\t\t\tQColor value;\n\t\t};\n\t\trpl::event_stream<ChangeData> changed;\n\n\t\tstruct EditionData {\n\t\t\tQString name;\n\t\t\tQString copyOf;\n\t\t\tQColor value;\n\t\t};\n\t\trpl::event_stream<EditionData> pending;\n\n\t\tstruct ScrollData {\n\t\t\tType type = {};\n\t\t\tint position = 0;\n\t\t\tint height = 0;\n\t\t};\n\t\trpl::event_stream<ScrollData> scroll;\n\t};\n\tEditorBlock(QWidget *parent, Type type, Context *context);\n\n\tvoid filterRows(const QString &query);\n\tvoid chooseRow();\n\tbool hasSelected() const {\n\t\treturn (_selected >= 0);\n\t}\n\tvoid clearSelected() {\n\t\tsetSelected(-1);\n\t}\n\tbool selectSkip(int direction);\n\n\tvoid feed(const QString &name, QColor value, const QString &copyOfExisting = QString());\n\tbool feedCopy(const QString &name, const QString &copyOf);\n\tconst QColor *find(const QString &name);\n\n\tbool feedDescription(const QString &name, const QString &description);\n\n\tvoid sortByDistance(const QColor &to);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tint resizeGetHeight(int newWidth) override;\n\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid mouseReleaseEvent(QMouseEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\tvoid leaveEventHook(QEvent *e) override;\n\nprivate:\n\tclass Row;\n\n\tvoid addRow(const QString &name, const QString &copyOf, QColor value);\n\tvoid removeRow(const QString &name, bool removeCopyReferences = true);\n\n\tvoid addToSearch(const Row &row);\n\tvoid removeFromSearch(const Row &row);\n\n\ttemplate <typename Callback>\n\tvoid enumerateRows(Callback callback);\n\n\ttemplate <typename Callback>\n\tvoid enumerateRows(Callback callback) const;\n\n\ttemplate <typename Callback>\n\tvoid enumerateRowsFrom(int top, Callback callback);\n\n\ttemplate <typename Callback>\n\tvoid enumerateRowsFrom(int top, Callback callback) const;\n\n\tRow &rowAtIndex(int index);\n\tint findRowIndex(const QString &name) const;\n\tRow *findRow(const QString &name);\n\tint findRowIndex(const Row *row);\n\tvoid updateRow(const Row &row);\n\tvoid paintRow(Painter &p, int index, const Row &row);\n\n\tvoid updateSelected(QPoint localPosition);\n\tvoid setSelected(int selected);\n\tvoid setPressed(int pressed);\n\tvoid addRowRipple(int index);\n\tvoid stopLastRipple(int index);\n\tvoid scrollToSelected();\n\n\tbool isEditing() const {\n\t\treturn !_context->name.isEmpty();\n\t}\n\tvoid saveEditing(QColor value);\n\tvoid cancelEditing();\n\tbool checkCopyOf(int index, const QString &possibleCopyOf);\n\tvoid checkCopiesChanged(int startIndex, QStringList names, QColor value);\n\tvoid activateRow(const Row &row);\n\n\tbool isSearch() const {\n\t\treturn !_searchQuery.isEmpty();\n\t}\n\tvoid searchByQuery(QString query);\n\tvoid resetSearch() {\n\t\tsearchByQuery(QString());\n\t}\n\n\tType _type = Type::Existing;\n\tContext *_context = nullptr;\n\n\tstd::vector<Row> _data;\n\tQMap<QString, int> _indices;\n\n\tQString _searchQuery;\n\tstd::vector<int> _searchResults;\n\tbase::flat_map<QChar, base::flat_set<int>> _searchIndex;\n\n\tint _selected = -1;\n\tint _pressed = -1;\n\tint _editing = -1;\n\n\tQPoint _lastGlobalPos;\n\tbool _mouseSelection = false;\n\n\tQBrush _transparent;\n\n};\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/themes/window_theme_editor_box.h\"\n\n#include \"window/themes/window_theme.h\"\n#include \"window/themes/window_theme_editor.h\"\n#include \"window/themes/window_theme_preview.h\"\n#include \"window/themes/window_themes_generate_name.h\"\n#include \"window/window_controller.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/style/style_palette_colorizer.h\"\n#include \"ui/widgets/fields/special_fields.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n#include \"storage/localstorage.h\"\n#include \"core/file_utilities.h\"\n#include \"core/application.h\"\n#include \"lang/lang_keys.h\"\n#include \"base/event_filter.h\"\n#include \"base/base_file_utilities.h\"\n#include \"base/zlib_help.h\"\n#include \"base/unixtime.h\"\n#include \"base/random.h\"\n#include \"data/data_session.h\"\n#include \"data/data_document.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"storage/file_upload.h\"\n#include \"mainwindow.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\n#include <QtCore/QBuffer>\n\nnamespace Window {\nnamespace Theme {\nnamespace {\n\nconstexpr auto kRandomSlugSize = 16;\nconstexpr auto kMinSlugSize = 5;\nconstexpr auto kMaxSlugSize = 64;\n\nenum class SaveErrorType {\n\tOther,\n\tName,\n\tLink,\n};\n\nclass BackgroundSelector : public Ui::RpWidget {\npublic:\n\tBackgroundSelector(\n\t\tQWidget *parent,\n\t\tconst QImage &background,\n\t\tconst ParsedTheme &parsed);\n\n\t[[nodiscard]] ParsedTheme result() const;\n\t[[nodiscard]] QImage image() const;\n\n\tint resizeGetHeight(int newWidth) override;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid updateThumbnail();\n\tvoid chooseBackgroundFromFile();\n\n\tobject_ptr<Ui::LinkButton> _chooseFromFile;\n\tobject_ptr<Ui::Checkbox> _tileBackground;\n\n\tQImage _background;\n\tParsedTheme _parsed;\n\tQString _imageText;\n\tint _thumbnailSize = 0;\n\tQPixmap _thumbnail;\n\n};\n\ntemplate <size_t Size>\nQByteArray qba(const char(&string)[Size]) {\n\treturn QByteArray::fromRawData(string, Size - 1);\n}\n\nQByteArray qba(QLatin1String string) {\n\treturn QByteArray::fromRawData(string.data(), string.size());\n}\n\nBackgroundSelector::BackgroundSelector(\n\tQWidget *parent,\n\tconst QImage &background,\n\tconst ParsedTheme &parsed)\n: RpWidget(parent)\n, _chooseFromFile(\n\tthis,\n\ttr::lng_settings_bg_from_file(tr::now),\n\tst::boxLinkButton)\n, _tileBackground(\n\tthis,\n\ttr::lng_settings_bg_tile(tr::now),\n\tparsed.tiled,\n\tst::defaultBoxCheckbox)\n, _background(background)\n, _parsed(parsed) {\n\t_imageText = tr::lng_theme_editor_saved_to_jpg(\n\t\ttr::now,\n\t\tlt_size,\n\t\tUi::FormatSizeText(_parsed.background.size()));\n\t_chooseFromFile->setClickedCallback([=] { chooseBackgroundFromFile(); });\n\n\t_thumbnailSize = st::boxTextFont->height\n\t\t+ st::themesSmallSkip\n\t\t+ _chooseFromFile->heightNoMargins()\n\t\t+ st::themesSmallSkip\n\t\t+ _tileBackground->heightNoMargins();\n\tresize(width(), _thumbnailSize + st::themesSmallSkip);\n\n\tupdateThumbnail();\n}\n\nvoid BackgroundSelector::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tconst auto left = _thumbnailSize + st::themesSmallSkip;\n\n\tp.setPen(st::boxTextFg);\n\tp.setFont(st::boxTextFont);\n\tp.drawTextLeft(left, 0, width(), _imageText);\n\n\tp.drawPixmapLeft(0, 0, width(), _thumbnail);\n}\n\nint BackgroundSelector::resizeGetHeight(int newWidth) {\n\tconst auto left = _thumbnailSize + st::themesSmallSkip;\n\t_chooseFromFile->moveToLeft(left, st::boxTextFont->height + st::themesSmallSkip);\n\t_tileBackground->moveToLeft(left, st::boxTextFont->height + st::themesSmallSkip + _chooseFromFile->height() + st::themesSmallSkip);\n\treturn height();\n}\n\nvoid BackgroundSelector::updateThumbnail() {\n\tconst auto size = _thumbnailSize;\n\tauto back = QImage(\n\t\tQSize(size, size) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tback.setDevicePixelRatio(style::DevicePixelRatio());\n\t{\n\t\tPainter p(&back);\n\t\tPainterHighQualityEnabler hq(p);\n\n\t\tauto &pix = _background;\n\t\tint sx = (pix.width() > pix.height()) ? ((pix.width() - pix.height()) / 2) : 0;\n\t\tint sy = (pix.height() > pix.width()) ? ((pix.height() - pix.width()) / 2) : 0;\n\t\tint s = (pix.width() > pix.height()) ? pix.height() : pix.width();\n\t\tp.drawImage(QRect(0, 0, size, size), pix, QRect(sx, sy, s, s));\n\t}\n\t_thumbnail = Ui::PixmapFromImage(\n\t\tImages::Round(std::move(back), ImageRoundRadius::Small));\n\t_thumbnail.setDevicePixelRatio(style::DevicePixelRatio());\n\tupdate();\n}\n\nvoid BackgroundSelector::chooseBackgroundFromFile() {\n\tconst auto callback = [=](const FileDialog::OpenResult &result) {\n\t\tauto content = result.remoteContent;\n\t\tif (!result.paths.isEmpty()) {\n\t\t\tQFile f(result.paths.front());\n\t\t\tif (f.open(QIODevice::ReadOnly)) {\n\t\t\t\tcontent = f.readAll();\n\t\t\t\tf.close();\n\t\t\t}\n\t\t}\n\t\tif (!content.isEmpty()) {\n\t\t\tauto read = Images::Read({ .content = content });\n\t\t\tif (!read.image.isNull()\n\t\t\t\t&& (read.format == \"jpeg\"\n\t\t\t\t\t|| read.format == \"jpg\"\n\t\t\t\t\t|| read.format == \"png\")) {\n\t\t\t\t_background = std::move(read.image);\n\t\t\t\t_parsed.background = content;\n\t\t\t\t_parsed.isPng = (read.format == \"png\");\n\t\t\t\tconst auto phrase = _parsed.isPng\n\t\t\t\t\t? tr::lng_theme_editor_read_from_png\n\t\t\t\t\t: tr::lng_theme_editor_read_from_jpg;\n\t\t\t\t_imageText = phrase(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_size,\n\t\t\t\t\tUi::FormatSizeText(_parsed.background.size()));\n\t\t\t\t_tileBackground->setChecked(false);\n\t\t\t\tupdateThumbnail();\n\t\t\t}\n\t\t}\n\t};\n\tFileDialog::GetOpenPath(\n\t\tthis,\n\t\ttr::lng_theme_editor_choose_image(tr::now),\n\t\t\"Image files (*.jpeg *.jpg *.png)\",\n\t\tcrl::guard(this, callback));\n}\n\nParsedTheme BackgroundSelector::result() const {\n\tauto result = _parsed;\n\tresult.tiled = _tileBackground->checked();\n\treturn result;\n}\n\nQImage BackgroundSelector::image() const {\n\treturn _background;\n}\n\nbool PaletteChanged(\n\t\tconst QByteArray &editorPalette,\n\t\tconst QByteArray &originalPalette,\n\t\tconst Data::CloudTheme &cloud) {\n\treturn originalPalette.isEmpty()\n\t\t|| (editorPalette != WriteCloudToText(cloud) + originalPalette);\n}\n\nvoid ImportFromFile(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<QWidget*> parent) {\n\tauto filters = QStringList(\n\t\tu\"Theme files (*.tdesktop-theme *.tdesktop-palette)\"_q);\n\tfilters.push_back(FileDialog::AllFilesFilter());\n\tconst auto callback = crl::guard(session, [=](\n\t\tconst FileDialog::OpenResult &result) {\n\t\tconst auto path = result.paths.isEmpty()\n\t\t\t? QString()\n\t\t\t: result.paths.front();\n\t\tif (!path.isEmpty()) {\n\t\t\tWindow::Theme::Apply(path);\n\t\t}\n\t});\n\tFileDialog::GetOpenPath(\n\t\tparent.get(),\n\t\ttr::lng_theme_editor_menu_import(tr::now),\n\t\tfilters.join(u\";;\"_q),\n\t\tcrl::guard(parent, callback));\n}\n\n// They're duplicated in window_theme.cpp:ChatBackground::ChatBackground.\n[[nodiscard]] QByteArray ReplaceAdjustableColors(QByteArray data) {\n\tconst auto &themeObject = Background()->themeObject();\n\tconst auto &paper = Background()->paper();\n\tconst auto usingDefaultTheme = themeObject.pathAbsolute.isEmpty();\n\tconst auto usingThemeBackground = usingDefaultTheme\n\t\t? Data::IsDefaultWallPaper(paper)\n\t\t: Data::IsThemeWallPaper(paper);\n\n\tif (usingThemeBackground) {\n\t\treturn data;\n\t}\n\n\tconst auto adjustables = base::flat_map<QByteArray, style::color>{\n\t\t{ qba(qstr(\"msgServiceBg\")), st::msgServiceBg },\n\t\t{ qba(qstr(\"msgServiceBgSelected\")), st::msgServiceBgSelected },\n\t\t{ qba(qstr(\"historyScrollBg\")), st::historyScrollBg },\n\t\t{ qba(qstr(\"historyScrollBgOver\")), st::historyScrollBgOver },\n\t\t{ qba(qstr(\"historyScrollBarBg\")), st::historyScrollBarBg },\n\t\t{ qba(qstr(\"historyScrollBarBgOver\")), st::historyScrollBarBgOver }\n\t};\n\tfor (const auto &[name, color] : adjustables) {\n\t\tdata = ReplaceValueInPaletteContent(\n\t\t\tdata,\n\t\t\tname,\n\t\t\tColorHexString(color->c));\n\t\tif (data == \"error\") {\n\t\t\tLOG((\"Theme Error: could not adjust '%1: %2' in content\").arg(\n\t\t\t\tQString::fromLatin1(name),\n\t\t\t\tQString::fromLatin1(ColorHexString(color->c))));\n\t\t\treturn QByteArray();\n\t\t}\n\t}\n\treturn data;\n}\n\nQByteArray GenerateDefaultPalette() {\n\tauto result = QByteArray();\n\tconst auto rows = style::main_palette::data();\n\tfor (const auto &row : std::as_const(rows)) {\n\t\tresult.append(qba(row.name)\n\t\t).append(\": \"\n\t\t).append(qba(row.value)\n\t\t).append(\"; // \"\n\t\t).append(\n\t\t\tqba(\n\t\t\t\trow.description\n\t\t\t).replace(\n\t\t\t\t'\\n',\n\t\t\t\t' '\n\t\t\t).replace(\n\t\t\t\t'\\r',\n\t\t\t\t' ')\n\t\t).append('\\n');\n\t}\n\treturn result;\n}\n\nbool CopyColorsToPalette(\n\t\tconst QString &path,\n\t\tconst QByteArray &palette,\n\t\tconst Data::CloudTheme &cloud) {\n\tQFile f(path);\n\tif (!f.open(QIODevice::WriteOnly)) {\n\t\tLOG((\"Theme Error: could not open '%1' for writing.\").arg(path));\n\t\treturn false;\n\t}\n\n\tconst auto prefix = WriteCloudToText(cloud);\n\tif (f.write(prefix) != prefix.size()\n\t\t|| f.write(palette) != palette.size()) {\n\t\tLOG((\"Theme Error: could not write palette to '%1'\").arg(path));\n\t\treturn false;\n\t}\n\treturn true;\n}\n\n[[nodiscard]] QByteArray PackTheme(const ParsedTheme &parsed) {\n\tzlib::FileToWrite zip;\n\n\tzip_fileinfo zfi = { { 0, 0, 0, 0, 0, 0 }, 0, 0, 0 };\n\tconst auto back = std::string(parsed.tiled ? \"tiled\" : \"background\")\n\t\t+ (parsed.isPng ? \".png\" : \".jpg\");\n\tzip.openNewFile(\n\t\tback.c_str(),\n\t\t&zfi,\n\t\tnullptr,\n\t\t0,\n\t\tnullptr,\n\t\t0,\n\t\tnullptr,\n\t\tZ_DEFLATED,\n\t\tZ_DEFAULT_COMPRESSION);\n\tzip.writeInFile(\n\t\tparsed.background.constData(),\n\t\tparsed.background.size());\n\tzip.closeFile();\n\tconst auto scheme = \"colors.tdesktop-theme\";\n\tzip.openNewFile(\n\t\tscheme,\n\t\t&zfi,\n\t\tnullptr,\n\t\t0,\n\t\tnullptr,\n\t\t0,\n\t\tnullptr,\n\t\tZ_DEFLATED,\n\t\tZ_DEFAULT_COMPRESSION);\n\tzip.writeInFile(parsed.palette.constData(), parsed.palette.size());\n\tzip.closeFile();\n\tzip.close();\n\n\tif (zip.error() != ZIP_OK) {\n\t\tLOG((\"Theme Error: could not export zip-ed theme, status: %1\"\n\t\t\t).arg(zip.error()));\n\t\treturn QByteArray();\n\t}\n\treturn zip.result();\n}\n\n[[nodiscard]] bool IsGoodSlug(const QString &slug) {\n\tif (slug.size() < kMinSlugSize || slug.size() > kMaxSlugSize) {\n\t\treturn false;\n\t}\n\treturn ranges::none_of(slug, [](QChar ch) {\n\t\treturn (ch < 'A' || ch > 'Z')\n\t\t\t&& (ch < 'a' || ch > 'z')\n\t\t\t&& (ch < '0' || ch > '9')\n\t\t\t&& (ch != '_');\n\t});\n}\n\nstd::shared_ptr<FilePrepareResult> PrepareThemeMedia(\n\t\tMTP::DcId dcId,\n\t\tconst QString &name,\n\t\tconst QByteArray &content) {\n\tPreparedPhotoThumbs thumbnails;\n\tQVector<MTPPhotoSize> sizes;\n\n\tauto thumbnail = GeneratePreview(content, QString()).scaled(\n\t\t320,\n\t\t320,\n\t\tQt::KeepAspectRatio,\n\t\tQt::SmoothTransformation);\n\tauto thumbnailBytes = QByteArray();\n\t{\n\t\tQBuffer buffer(&thumbnailBytes);\n\t\tthumbnail.save(&buffer, \"JPG\", 87);\n\t}\n\n\tsizes.push_back(MTP_photoSize(\n\t\tMTP_string(\"s\"),\n\t\tMTP_int(thumbnail.width()),\n\t\tMTP_int(thumbnail.height()), MTP_int(0)));\n\n\tconst auto id = base::RandomValue<DocumentId>();\n\tconst auto filename = base::FileNameFromUserString(name)\n\t\t+ u\".tdesktop-theme\"_q;\n\tauto attributes = QVector<MTPDocumentAttribute>(\n\t\t1,\n\t\tMTP_documentAttributeFilename(MTP_string(filename)));\n\n\tauto result = MakePreparedFile({\n\t\t.id = id,\n\t\t.type = SendMediaType::ThemeFile,\n\t});\n\tresult->filename = filename;\n\tresult->content = content;\n\tresult->filesize = content.size();\n\tresult->thumb = thumbnail;\n\tresult->thumbname = \"thumb.jpg\";\n\tresult->setThumbData(thumbnailBytes);\n\tresult->document = MTP_document(\n\t\tMTP_flags(0),\n\t\tMTP_long(id),\n\t\tMTP_long(0),\n\t\tMTP_bytes(),\n\t\tMTP_int(base::unixtime::now()),\n\t\tMTP_string(\"application/x-tgtheme-tdesktop\"),\n\t\tMTP_long(content.size()),\n\t\tMTP_vector<MTPPhotoSize>(sizes),\n\t\tMTPVector<MTPVideoSize>(),\n\t\tMTP_int(dcId),\n\t\tMTP_vector<MTPDocumentAttribute>(attributes));\n\treturn result;\n}\n\nFn<void()> SavePreparedTheme(\n\t\tnot_null<Window::Controller*> window,\n\t\tconst ParsedTheme &parsed,\n\t\tconst QImage &background,\n\t\tconst QByteArray &originalContent,\n\t\tconst ParsedTheme &originalParsed,\n\t\tconst Data::CloudTheme &fields,\n\t\tFn<void()> done,\n\t\tFn<void(SaveErrorType,QString)> fail) {\n\tExpects(window->account().sessionExists());\n\n\tusing Storage::UploadedMedia;\n\tstruct State {\n\t\tFullMsgId id;\n\t\tbool generating = false;\n\t\tmtpRequestId requestId = 0;\n\t\tQByteArray themeContent;\n\t\tQString filename;\n\t\trpl::lifetime lifetime;\n\t};\n\n\tconst auto session = &window->account().session();\n\tconst auto api = &session->api();\n\tconst auto state = std::make_shared<State>();\n\tstate->id = FullMsgId(\n\t\tsession->userPeerId(),\n\t\tsession->data().nextLocalMessageId());\n\n\tconst auto creating = !fields.id\n\t\t|| (fields.createdBy != session->userId());\n\tconst auto changed = (parsed.background != originalParsed.background)\n\t\t|| (parsed.tiled != originalParsed.tiled)\n\t\t|| PaletteChanged(parsed.palette, originalParsed.palette, fields);\n\n\tconst auto finish = [=](const MTPTheme &result) {\n\t\tBackground()->clearEditingTheme(ClearEditing::KeepChanges);\n\t\tdone();\n\n\t\tconst auto cloud = result.match([&](const MTPDtheme &data) {\n\t\t\tconst auto result = Data::CloudTheme::Parse(session, data);\n\t\t\tsession->data().cloudThemes().savedFromEditor(result);\n\t\t\treturn result;\n\t\t});\n\t\tif (cloud.documentId && !state->themeContent.isEmpty()) {\n\t\t\tconst auto document = session->data().document(cloud.documentId);\n\t\t\tdocument->setDataAndCache(state->themeContent);\n\t\t}\n\t\tKeepFromEditor(\n\t\t\toriginalContent,\n\t\t\toriginalParsed,\n\t\t\tcloud,\n\t\t\tstate->themeContent,\n\t\t\tparsed,\n\t\t\tbackground);\n\t};\n\n\tconst auto createTheme = [=](const MTPDocument &data) {\n\t\tconst auto document = session->data().processDocument(data);\n\t\tstate->requestId = api->request(MTPaccount_CreateTheme(\n\t\t\tMTP_flags(MTPaccount_CreateTheme::Flag::f_document),\n\t\t\tMTP_string(fields.slug),\n\t\t\tMTP_string(fields.title),\n\t\t\tdocument->mtpInput(),\n\t\t\tMTPVector<MTPInputThemeSettings>()\n\t\t)).done([=](const MTPTheme &result) {\n\t\t\tfinish(result);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tfail(SaveErrorType::Other, error.type());\n\t\t}).send();\n\t};\n\n\tconst auto updateTheme = [=](const MTPDocument &data) {\n\t\tusing Flag = MTPaccount_UpdateTheme::Flag;\n\t\tconst auto document = session->data().processDocument(data);\n\t\tconst auto flags = Flag::f_title\n\t\t\t| Flag::f_slug\n\t\t\t| (data.type() == mtpc_documentEmpty\n\t\t\t\t? Flag(0)\n\t\t\t\t: Flag::f_document);\n\t\tstate->requestId = api->request(MTPaccount_UpdateTheme(\n\t\t\tMTP_flags(flags),\n\t\t\tMTP_string(Data::CloudThemes::Format()),\n\t\t\tMTP_inputTheme(MTP_long(fields.id), MTP_long(fields.accessHash)),\n\t\t\tMTP_string(fields.slug),\n\t\t\tMTP_string(fields.title),\n\t\t\tdocument->mtpInput(),\n\t\t\tMTPVector<MTPInputThemeSettings>()\n\t\t)).done([=](const MTPTheme &result) {\n\t\t\tfinish(result);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tfail(SaveErrorType::Other, error.type());\n\t\t}).send();\n\t};\n\n\tconst auto uploadTheme = [=](const UploadedMedia &data) {\n\t\tstate->requestId = api->request(MTPaccount_UploadTheme(\n\t\t\tMTP_flags(MTPaccount_UploadTheme::Flag::f_thumb),\n\t\t\tdata.info.file,\n\t\t\t*data.info.thumb,\n\t\t\tMTP_string(state->filename),\n\t\t\tMTP_string(\"application/x-tgtheme-tdesktop\")\n\t\t)).done([=](const MTPDocument &result) {\n\t\t\tif (creating) {\n\t\t\t\tcreateTheme(result);\n\t\t\t} else {\n\t\t\t\tupdateTheme(result);\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tfail(SaveErrorType::Other, error.type());\n\t\t}).send();\n\t};\n\n\tconst auto uploadFile = [=](const QByteArray &theme) {\n\t\tconst auto media = PrepareThemeMedia(\n\t\t\tsession->mainDcId(),\n\t\t\tfields.title,\n\t\t\ttheme);\n\t\tstate->filename = media->filename;\n\t\tstate->themeContent = theme;\n\n\t\tsession->uploader().documentReady(\n\t\t) | rpl::filter([=](const UploadedMedia &data) {\n\t\t\treturn (data.fullId == state->id) && data.info.thumb.has_value();\n\t\t}) | rpl::on_next([=](const UploadedMedia &data) {\n\t\t\tuploadTheme(data);\n\t\t}, state->lifetime);\n\n\t\tsession->uploader().upload(state->id, media);\n\t};\n\n\tconst auto save = [=] {\n\t\tif (!creating && !changed) {\n\t\t\tupdateTheme(MTP_documentEmpty(MTP_long(fields.documentId)));\n\t\t\treturn;\n\t\t}\n\t\tstate->generating = true;\n\t\tcrl::async([=] {\n\t\t\tcrl::on_main([=, ready = PackTheme(parsed)]{\n\t\t\t\tif (!state->generating) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tstate->generating = false;\n\t\t\t\tuploadFile(ready);\n\t\t\t});\n\t\t});\n\t};\n\n\tconst auto checkFields = [=] {\n\t\tstate->requestId = api->request(MTPaccount_CreateTheme(\n\t\t\tMTP_flags(MTPaccount_CreateTheme::Flag::f_document),\n\t\t\tMTP_string(fields.slug),\n\t\t\tMTP_string(fields.title),\n\t\t\tMTP_inputDocumentEmpty(),\n\t\t\tMTPVector<MTPInputThemeSettings>()\n\t\t)).done([=](const MTPTheme &result) {\n\t\t\tsave();\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tif (error.type() == u\"THEME_FILE_INVALID\"_q) {\n\t\t\t\tsave();\n\t\t\t} else {\n\t\t\t\tfail(SaveErrorType::Other, error.type());\n\t\t\t}\n\t\t}).send();\n\t};\n\n\tif (creating) {\n\t\tcheckFields();\n\t} else {\n\t\tsave();\n\t}\n\n\treturn [=] {\n\t\tstate->generating = false;\n\t\tapi->request(base::take(state->requestId)).cancel();\n\t\tsession->uploader().cancel(state->id);\n\t\tstate->lifetime.destroy();\n\t};\n}\n\n} // namespace\n\nbool PaletteChanged(\n\t\tconst QByteArray &editorPalette,\n\t\tconst Data::CloudTheme &cloud) {\n\tauto object = Local::ReadThemeContent();\n\tconst auto real = object.content.isEmpty()\n\t\t? GenerateDefaultPalette()\n\t\t: ParseTheme(object, true).palette;\n\treturn PaletteChanged(editorPalette, real, cloud);\n}\n\nvoid StartEditor(\n\t\tnot_null<Window::Controller*> window,\n\t\tconst Data::CloudTheme &cloud) {\n\tconst auto path = EditingPalettePath();\n\tauto object = Local::ReadThemeContent();\n\n\tconst auto palette = object.content.isEmpty()\n\t\t? GenerateDefaultPalette()\n\t\t: ParseTheme(object, true).palette;\n\tif (palette.isEmpty() || !CopyColorsToPalette(path, palette, cloud)) {\n\t\twindow->show(Ui::MakeInformBox(tr::lng_theme_editor_error()));\n\t\treturn;\n\t}\n\tif (Core::App().settings().systemDarkModeEnabled()) {\n\t\tCore::App().settings().setSystemDarkModeEnabled(false);\n\t\tCore::App().saveSettingsDelayed();\n\t}\n\tBackground()->setEditingTheme(cloud);\n\twindow->showRightColumn(Box<Editor>(window, cloud));\n}\n\nvoid CreateBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::Controller*> window) {\n\tCreateForExistingBox(box, window, Data::CloudTheme());\n}\n\nvoid CreateForExistingBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::Controller*> window,\n\t\tconst Data::CloudTheme &cloud) {\n\tconst auto amCreator = window->account().sessionExists()\n\t\t&& (window->account().session().userId() == cloud.createdBy);\n\tbox->setTitle(amCreator\n\t\t? (rpl::single(cloud.title) | rpl::map(tr::marked))\n\t\t: tr::lng_theme_editor_create_title(tr::marked));\n\n\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\tbox,\n\t\t(amCreator\n\t\t\t? tr::lng_theme_editor_attach_description\n\t\t\t: tr::lng_theme_editor_create_description)(),\n\t\tst::boxDividerLabel));\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::SettingsButton>(\n\t\t\tbox,\n\t\t\ttr::lng_theme_editor_import_existing(),\n\t\t\tst::createThemeImportButton),\n\t\tstyle::margins(\n\t\t\t0,\n\t\t\tst::boxRowPadding.left(),\n\t\t\t0,\n\t\t\t0)\n\t)->addClickHandler([=] {\n\t\tImportFromFile(&window->account().session(), box);\n\t});\n\n\tconst auto done = [=] {\n\t\tbox->closeBox();\n\t\tStartEditor(window, cloud);\n\t};\n\tbase::install_event_filter(box, box, [=](not_null<QEvent*> event) {\n\t\tif (event->type() == QEvent::KeyPress) {\n\t\t\tconst auto key = static_cast<QKeyEvent*>(event.get())->key();\n\t\t\tif (key == Qt::Key_Enter || key == Qt::Key_Return) {\n\t\t\t\tdone();\n\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t}\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\tbox->addButton(tr::lng_theme_editor_create(), done);\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\nvoid SaveTheme(\n\t\tnot_null<Window::Controller*> window,\n\t\tconst Data::CloudTheme &cloud,\n\t\tconst QByteArray &palette,\n\t\tFn<void()> unlock) {\n\tExpects(window->account().sessionExists());\n\n\tusing Data::CloudTheme;\n\n\tconst auto save = [=](const CloudTheme &fields) {\n\t\tunlock();\n\t\twindow->show(Box(SaveThemeBox, window, fields, palette));\n\t};\n\tif (cloud.id) {\n\t\twindow->account().session().api().request(MTPaccount_GetTheme(\n\t\t\tMTP_string(Data::CloudThemes::Format()),\n\t\t\tMTP_inputTheme(MTP_long(cloud.id), MTP_long(cloud.accessHash))\n\t\t)).done([=](const MTPTheme &result) {\n\t\t\tresult.match([&](const MTPDtheme &data) {\n\t\t\t\tsave(CloudTheme::Parse(&window->account().session(), data));\n\t\t\t});\n\t\t}).fail([=] {\n\t\t\tsave(CloudTheme());\n\t\t}).send();\n\t} else {\n\t\tsave(CloudTheme());\n\t}\n}\n\nstruct CollectedData {\n\tQByteArray originalContent;\n\tParsedTheme originalParsed;\n\tParsedTheme parsed;\n\tQImage background;\n\tQColor accent;\n};\n\n[[nodiscard]] CollectedData CollectData(const QByteArray &palette) {\n\tconst auto original = Local::ReadThemeContent();\n\tconst auto originalContent = original.content;\n\n\t// We don't need default palette here, because in case of it we are\n\t// not interested if the palette was changed, we'll save it anyway.\n\tconst auto originalParsed = originalContent.isEmpty()\n\t\t? ParsedTheme() // GenerateDefaultPalette()\n\t\t: ParseTheme(original);\n\n\tconst auto background = Background()->createCurrentImage();\n\tconst auto changed = !Data::IsThemeWallPaper(Background()->paper())\n\t\t|| originalParsed.background.isEmpty()\n\t\t|| ColorizerForTheme(original.pathAbsolute);\n\n\tauto parsed = ParsedTheme();\n\tparsed.palette = StripCloudTextFields(palette);\n\tparsed.isPng = false;\n\tif (changed) {\n\t\tQBuffer buffer(&parsed.background);\n\t\tbackground.save(&buffer, \"JPG\", 87);\n\t} else {\n\t\t// Use existing background serialization.\n\t\tparsed.background = originalParsed.background;\n\t\tparsed.isPng = originalParsed.isPng;\n\t}\n\tconst auto accent = st::windowActiveTextFg->c;\n\treturn { originalContent, originalParsed, parsed, background, accent };\n}\n\nQByteArray CollectForExport(const QByteArray &palette) {\n\treturn PackTheme(CollectData(palette).parsed);\n}\n\nvoid SaveThemeBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::Controller*> window,\n\t\tconst Data::CloudTheme &cloud,\n\t\tconst QByteArray &palette) {\n\tExpects(window->account().sessionExists());\n\n\tconst auto collected = CollectData(palette);\n\tconst auto title = cloud.title.isEmpty()\n\t\t? GenerateName(collected.accent)\n\t\t: cloud.title;\n\n\tbox->setTitle(tr::lng_theme_editor_save_title(tr::marked));\n\n\tconst auto name = box->addRow(object_ptr<Ui::InputField>(\n\t\tbox,\n\t\tst::defaultInputField,\n\t\ttr::lng_theme_editor_name(),\n\t\ttitle));\n\tconst auto linkWrap = box->addRow(\n\t\tobject_ptr<Ui::RpWidget>(box),\n\t\tstyle::margins(\n\t\t\tst::boxRowPadding.left(),\n\t\t\tst::themesSmallSkip,\n\t\t\tst::boxRowPadding.right(),\n\t\t\tst::boxRowPadding.bottom()));\n\tconst auto link = Ui::CreateChild<Ui::UsernameInput>(\n\t\tlinkWrap,\n\t\tst::createThemeLink,\n\t\trpl::single(u\"link\"_q),\n\t\tcloud.slug.isEmpty() ? GenerateSlug() : cloud.slug,\n\t\twindow->account().session().createInternalLink(QString()));\n\tlinkWrap->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tlink->resize(width, link->height());\n\t\tlink->moveToLeft(0, 0, width);\n\t}, link->lifetime());\n\tlink->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tlinkWrap->resize(linkWrap->width(), height);\n\t}, link->lifetime());\n\tlink->setLinkPlaceholder(\n\t\twindow->account().session().createInternalLink(u\"addtheme/\"_q));\n\tlink->setPlaceholderHidden(false);\n\tlink->setMaxLength(kMaxSlugSize);\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_theme_editor_link_about(),\n\t\t\tst::boxDividerLabel),\n\t\tstyle::margins(\n\t\t\tst::boxRowPadding.left(),\n\t\t\tst::themesSmallSkip,\n\t\t\tst::boxRowPadding.right(),\n\t\t\tst::boxRowPadding.bottom()));\n\n\tbox->addRow(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_theme_editor_background_image(),\n\t\t\tst::defaultSubsectionTitle),\n\t\tst::defaultSubsectionTitlePadding);\n\tconst auto back = box->addRow(\n\t\tobject_ptr<BackgroundSelector>(\n\t\t\tbox,\n\t\t\tcollected.background,\n\t\t\tcollected.parsed),\n\t\tstyle::margins(\n\t\t\tst::boxRowPadding.left(),\n\t\t\tst::themesSmallSkip,\n\t\t\tst::boxRowPadding.right(),\n\t\t\tst::boxRowPadding.bottom()));\n\n\tbox->setFocusCallback([=] { name->setFocusFast(); });\n\n\tbox->setWidth(st::boxWideWidth);\n\n\tconst auto saving = box->lifetime().make_state<bool>();\n\tconst auto cancel = std::make_shared<Fn<void()>>(nullptr);\n\tbox->lifetime().add([=] { if (*cancel) (*cancel)(); });\n\tconst auto save = [=] {\n\t\tif (*saving) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto done = crl::guard(box, [=] {\n\t\t\tbox->closeBox();\n\t\t\twindow->showRightColumn(nullptr);\n\t\t});\n\t\tconst auto fail = crl::guard(box, [=](\n\t\t\t\tSaveErrorType type,\n\t\t\t\tconst QString &error) {\n\t\t\t*saving = false;\n\t\t\tbox->showLoading(false);\n\t\t\tif (error == u\"THEME_TITLE_INVALID\"_q) {\n\t\t\t\ttype = SaveErrorType::Name;\n\t\t\t} else if (error == u\"THEME_SLUG_INVALID\"_q) {\n\t\t\t\ttype = SaveErrorType::Link;\n\t\t\t} else if (error == u\"THEME_SLUG_OCCUPIED\"_q) {\n\t\t\t\tbox->showToast(\n\t\t\t\t\ttr::lng_create_channel_link_occupied(tr::now));\n\t\t\t\ttype = SaveErrorType::Link;\n\t\t\t} else if (!error.isEmpty()) {\n\t\t\t\tbox->showToast(error);\n\t\t\t}\n\t\t\tif (type == SaveErrorType::Name) {\n\t\t\t\tname->showError();\n\t\t\t} else if (type == SaveErrorType::Link) {\n\t\t\t\tlink->showError();\n\t\t\t}\n\t\t});\n\t\tauto fields = cloud;\n\t\tfields.title = name->getLastText().trimmed();\n\t\tfields.slug = link->getLastText().trimmed();\n\t\tif (fields.title.isEmpty()) {\n\t\t\tfail(SaveErrorType::Name, QString());\n\t\t\treturn;\n\t\t} else if (!IsGoodSlug(fields.slug)) {\n\t\t\tfail(SaveErrorType::Link, QString());\n\t\t\treturn;\n\t\t}\n\n\t\t*saving = true;\n\t\tbox->showLoading(true);\n\t\t*cancel = SavePreparedTheme(\n\t\t\twindow,\n\t\t\tback->result(),\n\t\t\tback->image(),\n\t\t\tcollected.originalContent,\n\t\t\tcollected.originalParsed,\n\t\t\tfields,\n\t\t\tdone,\n\t\t\tfail);\n\t};\n\tbox->addButton(tr::lng_settings_save(), save);\n\tbox->addButton(tr::lng_cancel(), [=] { box->closeBox(); });\n}\n\nParsedTheme ParseTheme(\n\t\tconst Object &theme,\n\t\tbool onlyPalette,\n\t\tbool parseCurrent) {\n\tauto raw = ParsedTheme();\n\traw.palette = theme.content;\n\tconst auto result = [&] {\n\t\tif (const auto colorizer = ColorizerForTheme(theme.pathAbsolute)) {\n\t\t\traw.palette = Editor::ColorizeInContent(\n\t\t\t\tstd::move(raw.palette),\n\t\t\t\tcolorizer);\n\t\t}\n\t\tif (parseCurrent) {\n\t\t\traw.palette = ReplaceAdjustableColors(std::move(raw.palette));\n\t\t}\n\t\treturn raw;\n\t};\n\n\tzlib::FileToRead file(theme.content);\n\n\tunz_global_info globalInfo = { 0 };\n\tfile.getGlobalInfo(&globalInfo);\n\tif (file.error() != UNZ_OK) {\n\t\treturn result();\n\t}\n\traw.palette = file.readFileContent(\"colors.tdesktop-theme\", zlib::kCaseInsensitive, kThemeSchemeSizeLimit);\n\tif (file.error() == UNZ_END_OF_LIST_OF_FILE) {\n\t\tfile.clearError();\n\t\traw.palette = file.readFileContent(\"colors.tdesktop-palette\", zlib::kCaseInsensitive, kThemeSchemeSizeLimit);\n\t}\n\tif (file.error() != UNZ_OK) {\n\t\tLOG((\"Theme Error: could not read 'colors.tdesktop-theme' or 'colors.tdesktop-palette' in the theme file.\"));\n\t\treturn ParsedTheme();\n\t} else if (onlyPalette) {\n\t\treturn result();\n\t}\n\n\tconst auto fromFile = [&](const char *filename) {\n\t\traw.background = file.readFileContent(filename, zlib::kCaseInsensitive, kThemeBackgroundSizeLimit);\n\t\tif (file.error() == UNZ_OK) {\n\t\t\treturn true;\n\t\t} else if (file.error() == UNZ_END_OF_LIST_OF_FILE) {\n\t\t\tfile.clearError();\n\t\t\treturn true;\n\t\t}\n\t\tLOG((\"Theme Error: could not read '%1' in the theme file.\").arg(filename));\n\t\treturn false;\n\t};\n\n\tif (!fromFile(\"background.jpg\") || !raw.background.isEmpty()) {\n\t\treturn raw.background.isEmpty() ? ParsedTheme() : result();\n\t}\n\traw.isPng = true;\n\tif (!fromFile(\"background.png\") || !raw.background.isEmpty()) {\n\t\treturn raw.background.isEmpty() ? ParsedTheme() : result();\n\t}\n\traw.tiled = true;\n\tif (!fromFile(\"tiled.png\") || !raw.background.isEmpty()) {\n\t\treturn raw.background.isEmpty() ? ParsedTheme() : result();\n\t}\n\traw.isPng = false;\n\tif (!fromFile(\"background.jpg\") || !raw.background.isEmpty()) {\n\t\treturn raw.background.isEmpty() ? ParsedTheme() : result();\n\t}\n\treturn result();\n}\n\n[[nodiscard]] QString GenerateSlug() {\n\tconst auto letters = uint8('Z' + 1 - 'A');\n\tconst auto digits = uint8('9' + 1 - '0');\n\tconst auto firstValues = uint8(2 * letters);\n\tconst auto values = uint8(2 * letters + digits);\n\n\tauto result = QString();\n\tresult.reserve(kRandomSlugSize);\n\tfor (auto i = 0; i != kRandomSlugSize; ++i) {\n\t\tconst auto value = i\n\t\t\t? (base::RandomValue<uint8>() % values)\n\t\t\t: (base::RandomValue<uint8>() % firstValues);\n\t\tif (value < letters) {\n\t\t\tresult.append(char('A' + value));\n\t\t} else if (value < 2 * letters) {\n\t\t\tresult.append(char('a' + (value - letters)));\n\t\t} else {\n\t\t\tresult.append(char('0' + (value - 2 * letters)));\n\t\t}\n\t}\n\treturn result;\n}\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_theme_editor_box.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/layers/generic_box.h\"\n\nnamespace Data {\nstruct CloudTheme;\n} // namespace Data\n\nnamespace Window {\n\nclass Controller;\n\nnamespace Theme {\n\nstruct Object;\nstruct ParsedTheme;\n\nvoid StartEditor(\n\tnot_null<Window::Controller*> window,\n\tconst Data::CloudTheme &cloud);\nvoid CreateBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::Controller*> window);\nvoid CreateForExistingBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::Controller*> window,\n\tconst Data::CloudTheme &cloud);\nvoid SaveTheme(\n\tnot_null<Window::Controller*> window,\n\tconst Data::CloudTheme &cloud,\n\tconst QByteArray &palette,\n\tFn<void()> unlock);\nvoid SaveThemeBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::Controller*> window,\n\tconst Data::CloudTheme &cloud,\n\tconst QByteArray &palette);\n\n[[nodiscard]] bool PaletteChanged(\n\tconst QByteArray &editorPalette,\n\tconst Data::CloudTheme &cloud);\n\n[[nodiscard]] QByteArray CollectForExport(const QByteArray &palette);\n\n[[nodiscard]] ParsedTheme ParseTheme(\n\tconst Object &theme,\n\tbool onlyPalette = false,\n\tbool parseCurrent = true);\n\n[[nodiscard]] QString GenerateSlug();\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_theme_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/themes/window_theme_preview.h\"\n\n#include \"dialogs/dialogs_three_state_icon.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"platform/platform_window_title.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/empty_userpic.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/message_bubble.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_media_view.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_info.h\"\n\nnamespace Window {\nnamespace Theme {\nnamespace {\n\n[[nodiscard]] QString FillLetters(const QString &name) {\n\tQList<QString> letters;\n\tQList<int> levels;\n\tauto level = 0;\n\tauto letterFound = false;\n\tauto ch = name.constData(), end = ch + name.size();\n\twhile (ch != end) {\n\t\tauto emojiLength = 0;\n\t\tif (Ui::Emoji::Find(ch, end, &emojiLength)) {\n\t\t\tch += emojiLength;\n\t\t} else if (ch->isHighSurrogate()) {\n\t\t\t++ch;\n\t\t\tif (ch != end && ch->isLowSurrogate()) {\n\t\t\t\t++ch;\n\t\t\t}\n\t\t} else if (!letterFound && ch->isLetterOrNumber()) {\n\t\t\tletterFound = true;\n\t\t\tif (ch + 1 != end && Ui::Text::IsDiacritic(*(ch + 1))) {\n\t\t\t\tletters.push_back(QString(ch, 2));\n\t\t\t\tlevels.push_back(level);\n\t\t\t\t++ch;\n\t\t\t} else {\n\t\t\t\tletters.push_back(QString(ch, 1));\n\t\t\t\tlevels.push_back(level);\n\t\t\t}\n\t\t\t++ch;\n\t\t} else {\n\t\t\tif (*ch == ' ') {\n\t\t\t\tlevel = 0;\n\t\t\t\tletterFound = false;\n\t\t\t} else if (letterFound && *ch == '-') {\n\t\t\t\tlevel = 1;\n\t\t\t\tletterFound = true;\n\t\t\t}\n\t\t\t++ch;\n\t\t}\n\t}\n\n\t// We prefer the second letter to be after ' ', but it can also be after '-'.\n\tauto result = QString();\n\tif (!letters.isEmpty()) {\n\t\tresult += letters.front();\n\t\tauto bestIndex = 0;\n\t\tauto bestLevel = 2;\n\t\tfor (auto i = letters.size(); i != 1;) {\n\t\t\tif (levels[--i] < bestLevel) {\n\t\t\t\tbestIndex = i;\n\t\t\t\tbestLevel = levels[i];\n\t\t\t}\n\t\t}\n\t\tif (bestIndex > 0) {\n\t\t\tresult += letters[bestIndex];\n\t\t}\n\t}\n\treturn result.toUpper();\n}\n\nclass Generator {\npublic:\n\tGenerator(\n\t\tconst Instance &theme,\n\t\tCurrentData &&current,\n\t\tPreviewType type);\n\n\t[[nodiscard]] QImage generate();\n\nprivate:\n\tenum class Status {\n\t\tNone,\n\t\tSent,\n\t\tReceived\n\t};\n\tstruct Row {\n\t\tUi::Text::String name;\n\t\tQString letters;\n\t\tenum class Type {\n\t\t\tUser,\n\t\t\tGroup,\n\t\t\tChannel\n\t\t};\n\t\tType type = Type::User;\n\t\tint peerIndex = 0;\n\t\tint unreadCounter = 0;\n\t\tbool muted = false;\n\t\tbool pinned = false;\n\t\tQString date;\n\t\tUi::Text::String text;\n\t\tStatus status = Status::None;\n\t\tbool selected = false;\n\t\tbool active = false;\n\t};\n\tstruct Bubble {\n\t\tint width = 0;\n\t\tint height = 0;\n\t\tbool outbg = false;\n\t\tStatus status = Status::None;\n\t\tQString date;\n\t\tbool attachToTop = false;\n\t\tbool attachToBottom = false;\n\t\tbool tail = true;\n\t\tUi::Text::String text = { st::msgMinWidth };\n\t\tQVector<int> waveform;\n\t\tint waveactive = 0;\n\t\tQString wavestatus;\n\t\tQImage photo;\n\t\tint photoWidth = 0;\n\t\tint photoHeight = 0;\n\t\tUi::Text::String replyName = { st::msgMinWidth };\n\t\tUi::Text::String replyText = { st::msgMinWidth };\n\t};\n\n\t[[nodiscard]] bool extended() const;\n\tvoid prepare();\n\n\tvoid addRow(\n\t\tQString name,\n\t\tint peerIndex,\n\t\tQString date,\n\t\tconst TextWithEntities &text);\n\tvoid addBubble(Bubble bubble, int width, int height, QString date, Status status);\n\tvoid addAudioBubble(QVector<int> waveform, int waveactive, QString wavestatus, QString date, Status status);\n\tvoid addTextBubble(QString text, QString date, Status status);\n\tvoid addDateBubble(QString date);\n\tvoid addPhotoBubble(QString image, QString caption, QString date, Status status);\n\tQSize computeSkipBlock(Status status, QString date);\n\tint computeInfoWidth(Status status, QString date);\n\n\tvoid generateData();\n\n\tvoid paintHistoryList();\n\tvoid paintHistoryBackground();\n\tvoid paintTopBar();\n\tvoid paintComposeArea();\n\tvoid paintDialogs();\n\tvoid paintDialogsList();\n\tvoid paintHistoryShadows();\n\tvoid paintRow(const Row &row);\n\tvoid paintBubble(const Bubble &bubble);\n\tvoid paintService(QString text);\n\n\tvoid paintUserpic(int x, int y, Row::Type type, int index, QString letters);\n\n\tvoid setTextPalette(const style::TextPalette &st);\n\tvoid restoreTextPalette();\n\n\tconst Instance &_theme;\n\tconst style::palette &_palette;\n\tconst CurrentData _current;\n\tconst PreviewType _type;\n\tUi::ChatStyle _st;\n\tPainter *_p = nullptr;\n\n\tQRect _rect;\n\tQRect _inner;\n\tQRect _body;\n\tQRect _dialogs;\n\tQRect _dialogsList;\n\tQRect _topBar;\n\tQRect _composeArea;\n\tQRect _history;\n\n\tint _rowsTop = 0;\n\tstd::vector<Row> _rows;\n\n\tUi::Text::String _topBarName;\n\tQString _topBarStatus;\n\tbool _topBarStatusActive = false;\n\n\tint _historyBottom = 0;\n\tstd::vector<Bubble> _bubbles;\n\n\tstyle::TextPalette _textPalette;\n\n};\n\nbool Generator::extended() const {\n\treturn (_type == PreviewType::Extended);\n}\n\nvoid Generator::prepare() {\n\tconst auto size = extended()\n\t\t? QRect(\n\t\t\tQPoint(),\n\t\t\tst::themePreviewSize).marginsAdded(st::themePreviewMargin).size()\n\t\t: st::themePreviewSize;\n\t_rect = QRect(QPoint(), size);\n\t_inner = extended() ? _rect.marginsRemoved(st::themePreviewMargin) : _rect;\n\t_body = extended() ? _inner.marginsRemoved(QMargins(0, Platform::PreviewTitleHeight(), 0, 0)) : _inner;\n\t_dialogs = QRect(_body.x(), _body.y(), st::themePreviewDialogsWidth, _body.height());\n\t_dialogsList = _dialogs.marginsRemoved(QMargins(0, st::dialogsFilterPadding.y() + st::dialogsMenuToggle.height + st::dialogsFilterPadding.y(), 0, st::defaultDialogRow.padding.bottom()));\n\t_topBar = QRect(_dialogs.x() + _dialogs.width(), _dialogs.y(), _body.width() - _dialogs.width(), st::topBarHeight);\n\t_composeArea = QRect(_topBar.x(), _body.y() + _body.height() - st::historySendSize.height(), _topBar.width(), st::historySendSize.height());\n\t_history = QRect(_topBar.x(), _topBar.y() + _topBar.height(), _topBar.width(), _body.height() - _topBar.height() - _composeArea.height());\n\n\tgenerateData();\n}\n\nvoid Generator::addRow(\n\t\tQString name,\n\t\tint peerIndex,\n\t\tQString date,\n\t\tconst TextWithEntities &text) {\n\tRow row;\n\trow.name.setText(st::msgNameStyle, name, Ui::NameTextOptions());\n\n\trow.letters = FillLetters(name);\n\n\trow.peerIndex = peerIndex;\n\trow.date = date;\n\trow.text.setMarkedText(\n\t\tst::dialogsTextStyle,\n\t\ttext,\n\t\tUi::DialogTextOptions());\n\t_rows.push_back(std::move(row));\n}\n\nvoid Generator::addBubble(Bubble bubble, int width, int height, QString date, Status status) {\n\tbubble.width = width;\n\tbubble.height = height;\n\tbubble.date = date;\n\tbubble.status = status;\n\t_bubbles.push_back(std::move(bubble));\n}\n\nvoid Generator::addAudioBubble(QVector<int> waveform, int waveactive, QString wavestatus, QString date, Status status) {\n\tBubble bubble;\n\tbubble.waveform = waveform;\n\tbubble.waveactive = waveactive;\n\tbubble.wavestatus = wavestatus;\n\n\tauto skipBlock = computeSkipBlock(status, date);\n\n\tauto width = st::msgFileMinWidth;\n\tconst auto &st = st::msgFileLayout;\n\tauto tleft = st.padding.left() + st.thumbSize + st.thumbSkip;\n\taccumulate_max(width, tleft + st::normalFont->width(wavestatus) + skipBlock.width() + st::msgPadding.right());\n\taccumulate_min(width, st::msgMaxWidth);\n\n\tauto height = st.padding.top() + st.thumbSize + st.padding.bottom();\n\taddBubble(std::move(bubble), width, height, date, status);\n}\n\nQSize Generator::computeSkipBlock(Status status, QString date) {\n\tauto infoWidth = computeInfoWidth(status, date);\n\tauto width = st::msgDateSpace + infoWidth - st::msgDateDelta.x();\n\tauto height = st::msgDateFont->height - st::msgDateDelta.y();\n\treturn QSize(width, height);\n}\n\nint Generator::computeInfoWidth(Status status, QString date) {\n\tauto result = st::msgDateFont->width(date);\n\tif (status != Status::None) {\n\t\tresult += st::historySendStateSpace;\n\t}\n\treturn result;\n}\n\nvoid Generator::addTextBubble(QString text, QString date, Status status) {\n\tBubble bubble;\n\tauto skipBlock = computeSkipBlock(status, date);\n\tauto marked = TextWithEntities{ std::move(text) };\n\tbubble.text.setMarkedText(\n\t\tst::messageTextStyle,\n\t\tstd::move(marked),\n\t\tUi::ItemTextDefaultOptions());\n\tbubble.text.updateSkipBlock(skipBlock.width(), skipBlock.height());\n\n\tauto width = _history.width() - st::msgMargin.left() - st::msgMargin.right();\n\taccumulate_min(width, st::msgPadding.left() + bubble.text.maxWidth() + st::msgPadding.right());\n\taccumulate_min(width, st::msgMaxWidth);\n\n\tauto textWidth = qMax(width - st::msgPadding.left() - st::msgPadding.right(), 1);\n\tauto textHeight = bubble.text.countHeight(textWidth);\n\n\tauto height = st::msgPadding.top() + textHeight + st::msgPadding.bottom();\n\taddBubble(std::move(bubble), width, height, date, status);\n}\n\nvoid Generator::addDateBubble(QString date) {\n\tBubble bubble;\n\taddBubble(std::move(bubble), 0, 0, date, Status::None);\n}\n\nvoid Generator::addPhotoBubble(QString image, QString caption, QString date, Status status) {\n\tBubble bubble;\n\tbubble.photo.load(image);\n\tbubble.photoWidth = style::ConvertScale(bubble.photo.width() / 2);\n\tbubble.photoHeight = style::ConvertScale(bubble.photo.height() / 2);\n\tauto skipBlock = computeSkipBlock(status, date);\n\tauto marked = TextWithEntities{ std::move(caption) };\n\tbubble.text.setMarkedText(\n\t\tst::messageTextStyle,\n\t\tstd::move(marked),\n\t\tUi::ItemTextDefaultOptions());\n\tbubble.text.updateSkipBlock(skipBlock.width(), skipBlock.height());\n\n\tauto width = _history.width() - st::msgMargin.left() - st::msgMargin.right();\n\taccumulate_min(width, bubble.photoWidth);\n\taccumulate_min(width, st::msgMaxWidth);\n\n\tauto textWidth = qMax(width - st::msgPadding.left() - st::msgPadding.right(), 1);\n\tauto textHeight = bubble.text.countHeight(textWidth);\n\n\tauto height = st::mediaCaptionSkip + textHeight + st::msgPadding.bottom();\n\taddBubble(std::move(bubble), width, height, date, status);\n}\n\nvoid Generator::generateData() {\n\t_rows.reserve(9);\n\taddRow(\n\t\t\"Eva Summer\",\n\t\t0,\n\t\t\"11:00\",\n\t\t{ .text = \"We are too smart for this world. \"\n\t\t\t+ QString::fromUtf8(\"\\xf0\\x9f\\xa4\\xa3\\xf0\\x9f\\x98\\x82\") });\n\t_rows.back().active = true;\n\t_rows.back().pinned = true;\n\taddRow(\"Alexandra Smith\", 7, \"10:00\", { .text = \"This is amazing!\" });\n\t_rows.back().unreadCounter = 2;\n\taddRow(\n\t\t\"Mike Apple\",\n\t\t2,\n\t\t\"9:00\",\n\t\tUi::Text::Colorized(QChar(55357)\n\t\t\t+ QString()\n\t\t\t+ QChar(56836)\n\t\t\t+ \" Sticker\"));\n\t_rows.back().unreadCounter = 2;\n\t_rows.back().muted = true;\n\taddRow(\"Evening Club\", 1, \"8:00\", Ui::Text::Colorized(\"Eva: Photo\"));\n\t_rows.back().type = Row::Type::Group;\n\taddRow(\n\t\t\"Old Pirates\",\n\t\t6,\n\t\t\"7:00\",\n\t\tUi::Text::Colorized(\"Max:\").append(\" Yo-ho-ho!\"));\n\t_rows.back().type = Row::Type::Group;\n\taddRow(\"Max Bright\", 3, \"6:00\", { .text = \"How about some coffee?\" });\n\t_rows.back().status = Status::Received;\n\taddRow(\"Natalie Parker\", 4, \"5:00\", { .text = \"OK, great)\" });\n\t_rows.back().status = Status::Received;\n\taddRow(\"Davy Jones\", 5, \"4:00\", Ui::Text::Colorized(\"Keynote.pdf\"));\n\n\t_topBarName.setText(st::msgNameStyle, \"Eva Summer\", Ui::NameTextOptions());\n\t_topBarStatus = \"online\";\n\t_topBarStatusActive = true;\n\n\taddPhotoBubble(\":/gui/art/themeimage.jpg\", \"To reach a port, we must sail. \" + QString::fromUtf8(\"\\xf0\\x9f\\xa5\\xb8\"), \"7:00\", Status::None);\n\tint wavedata[] = { 0, 0, 0, 0, 27, 31, 4, 1, 0, 0, 23, 30, 18, 9, 7, 19, 4, 2, 2, 2, 0, 0, 15, 15, 15, 15, 3, 15, 19, 3, 2, 0, 0, 0, 0, 0, 3, 12, 16, 6, 4, 6, 14, 12, 2, 12, 12, 11, 3, 0, 7, 5, 7, 4, 7, 5, 2, 4, 0, 9, 5, 7, 6, 2, 2, 0, 0 };\n\tauto waveform = QVector<int>(base::array_size(wavedata));\n\tmemcpy(waveform.data(), wavedata, sizeof(wavedata));\n\taddAudioBubble(waveform, 33, \"0:07\", \"8:00\", Status::None);\n\t_bubbles.back().outbg = true;\n\t_bubbles.back().status = Status::Received;\n\taddDateBubble(\"December 26\");\n\taddTextBubble(\"Twenty years from now you will be more disappointed by the things that you didn't do than by the ones you did do. \" + QString::fromUtf8(\"\\xf0\\x9f\\xa7\\x90\"), \"10:00\", Status::Received);\n\t_bubbles.back().tail = false;\n\t_bubbles.back().outbg = true;\n\t_bubbles.back().attachToBottom = true;\n\taddTextBubble(\"Mark Twain said that \" + QString::fromUtf8(\"\\xe2\\x98\\x9d\\xef\\xb8\\x8f\"), \"10:00\", Status::Received);\n\t_bubbles.back().outbg = true;\n\t_bubbles.back().attachToTop = true;\n\t_bubbles.back().tail = true;\n\taddTextBubble(\"We are too smart for this world. \" + QString::fromUtf8(\"\\xf0\\x9f\\xa4\\xa3\\xf0\\x9f\\x98\\x82\"), \"11:00\", Status::None);\n\t_bubbles.back().replyName.setText(st::msgNameStyle, \"Alex Cassio\", Ui::NameTextOptions());\n\t_bubbles.back().replyText.setText(st::messageTextStyle, \"Mark Twain said that \" + QString::fromUtf8(\"\\xe2\\x98\\x9d\\xef\\xb8\\x8f\"), Ui::DialogTextOptions());\n}\n\nGenerator::Generator(\n\tconst Instance &theme,\n\tCurrentData &&current,\n\tPreviewType type)\n: _theme(theme)\n, _palette(_theme.palette)\n, _current(std::move(current))\n, _type(type)\n, _st(&_palette) {\n}\n\nQImage Generator::generate() {\n\tprepare();\n\n\tauto result = QImage(\n\t\t_rect.size() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.setDevicePixelRatio(style::DevicePixelRatio());\n\tresult.fill(st::themePreviewBg->c);\n\n\t{\n\t\tPainter p(&result);\n\t\tPainterHighQualityEnabler hq(p);\n\t\t_p = &p;\n\n\t\t_p->fillRect(_body, QColor(0, 0, 0));\n\t\t_p->fillRect(_body, st::windowBg[_palette]);\n\n\t\tpaintHistoryList();\n\t\tpaintTopBar();\n\t\tpaintComposeArea();\n\t\tpaintDialogs();\n\t\tpaintHistoryShadows();\n\t}\n\tif (extended()) {\n\t\tPlatform::PreviewWindowFramePaint(result, _palette, _body, _rect.width());\n\t}\n\n\treturn result;\n}\n\nvoid Generator::paintHistoryList() {\n\tpaintHistoryBackground();\n\n\t_historyBottom = _history.y() + _history.height();\n\t_historyBottom -= st::historyPaddingBottom;\n\t_p->setClipping(true);\n\tfor (auto i = _bubbles.size(); i != 0;) {\n\t\tauto &bubble = _bubbles[--i];\n\t\tif (bubble.width > 0) {\n\t\t\tpaintBubble(bubble);\n\t\t} else {\n\t\t\tpaintService(bubble.date);\n\t\t}\n\t}\n\n\t_p->setClipping(false);\n}\n\nvoid Generator::paintHistoryBackground() {\n\tauto fromy = (-st::topBarHeight);\n\tauto background = _theme.background;\n\tauto tiled = _theme.tiled;\n\tif (background.isNull()) {\n\t\tconst auto fakePaper = Data::WallPaper(_current.backgroundId);\n\t\tif (Data::IsThemeWallPaper(fakePaper)) {\n\t\t\tbackground = Ui::ReadBackgroundImage(\n\t\t\t\tu\":/gui/art/background.tgv\"_q,\n\t\t\t\tQByteArray(),\n\t\t\t\ttrue\n\t\t\t).image;\n\t\t\tconst auto paper = Data::DefaultWallPaper();\n\t\t\tbackground = Ui::PreparePatternImage(\n\t\t\t\tstd::move(background),\n\t\t\t\tpaper.backgroundColors(),\n\t\t\t\tpaper.gradientRotation(),\n\t\t\t\tpaper.patternOpacity());\n\t\t\ttiled = false;\n\t\t} else {\n\t\t\tbackground = std::move(_current.backgroundImage);\n\t\t\ttiled = _current.backgroundTiled;\n\t\t}\n\t}\n\tbackground = std::move(background).convertToFormat(\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tbackground.setDevicePixelRatio(style::DevicePixelRatio());\n\t_p->setClipRect(_history);\n\tif (tiled) {\n\t\tauto width = background.width();\n\t\tauto height = background.height();\n\t\tauto repeatTimesX = qCeil(_history.width()\n\t\t\t* style::DevicePixelRatio()\n\t\t\t/ float64(width));\n\t\tauto repeatTimesY = qCeil((_history.height() - fromy)\n\t\t\t* style::DevicePixelRatio()\n\t\t\t/ float64(height));\n\t\tauto imageForTiled = QImage(\n\t\t\twidth * repeatTimesX,\n\t\t\theight * repeatTimesY,\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\timageForTiled.setDevicePixelRatio(background.devicePixelRatio());\n\t\tauto imageForTiledBytes = imageForTiled.bits();\n\t\tauto bytesInLine = width * sizeof(uint32);\n\t\tfor (auto timesY = 0; timesY != repeatTimesY; ++timesY) {\n\t\t\tauto imageBytes = background.constBits();\n\t\t\tfor (auto y = 0; y != height; ++y) {\n\t\t\t\tfor (auto timesX = 0; timesX != repeatTimesX; ++timesX) {\n\t\t\t\t\tmemcpy(imageForTiledBytes, imageBytes, bytesInLine);\n\t\t\t\t\timageForTiledBytes += bytesInLine;\n\t\t\t\t}\n\t\t\t\timageBytes += background.bytesPerLine();\n\t\t\t\timageForTiledBytes += imageForTiled.bytesPerLine()\n\t\t\t\t\t- (repeatTimesX * bytesInLine);\n\t\t\t}\n\t\t}\n\t\t_p->drawImage(_history.x(), _history.y() + fromy, imageForTiled);\n\t} else {\n\t\tPainterHighQualityEnabler hq(*_p);\n\n\t\tauto fill = QSize(_topBar.width(), _body.height());\n\t\tconst auto rects = Ui::ComputeChatBackgroundRects(\n\t\t\tfill,\n\t\t\tbackground.size());\n\t\tauto to = rects.to;\n\t\tto.moveTop(to.top() + fromy);\n\t\tto.moveTopLeft(to.topLeft() + _history.topLeft());\n\t\t_p->drawImage(to, background, rects.from);\n\t}\n\t_p->setClipping(false);\n}\n\nvoid Generator::paintTopBar() {\n\t_p->fillRect(_topBar, st::topBarBg[_palette]);\n\n\tauto right = st::topBarMenuToggle.width;\n\tst::topBarMenuToggle.icon[_palette].paint(*_p, _topBar.x() + _topBar.width() - right + st::topBarMenuToggle.iconPosition.x(), _topBar.y() + st::topBarMenuToggle.iconPosition.y(), _rect.width());\n\tright += st::topBarSkip + st::topBarCall.width;\n\tst::topBarCall.icon[_palette].paint(*_p, _topBar.x() + _topBar.width() - right + st::topBarCall.iconPosition.x(), _topBar.y() + st::topBarCall.iconPosition.y(), _rect.width());\n\tright += st::topBarSearch.width;\n\tst::topBarSearch.icon[_palette].paint(*_p, _topBar.x() + _topBar.width() - right + st::topBarSearch.iconPosition.x(), _topBar.y() + st::topBarSearch.iconPosition.y(), _rect.width());\n\n\tauto decreaseWidth = st::topBarCall.width + st::topBarCallSkip + st::topBarSearch.width + st::topBarMenuToggle.width;\n\tauto nameleft = _topBar.x() + st::topBarArrowPadding.right();\n\tauto nametop = _topBar.y() + st::topBarArrowPadding.top();\n\tauto statustop = _topBar.y() + st::topBarHeight - st::topBarArrowPadding.bottom() - st::dialogsTextFont->height;\n\tauto namewidth = _topBar.x() + _topBar.width() - decreaseWidth - nameleft - st::topBarArrowPadding.right();\n\t_p->setFont(st::dialogsTextFont);\n\t_p->setPen(_topBarStatusActive ? st::historyStatusFgActive[_palette] : st::historyStatusFg[_palette]);\n\t_p->drawText(nameleft, statustop + st::dialogsTextFont->ascent, _topBarStatus);\n\n\t_p->setPen(st::dialogsNameFg[_palette]);\n\t_topBarName.drawElided(*_p, nameleft, nametop, namewidth);\n}\n\nvoid Generator::paintComposeArea() {\n\t_p->fillRect(_composeArea, st::historyReplyBg[_palette]);\n\n\tauto controlsTop = _composeArea.y() + _composeArea.height() - st::historySendSize.height();\n\tconst auto attachIconLeft = (st::historyAttach.iconPosition.x() < 0)\n\t\t? ((st::historyAttach.width - st::historyAttach.icon.width()) / 2)\n\t\t: st::historyAttach.iconPosition.x();\n\tconst auto attachIconTop = (st::historyAttach.iconPosition.y() < 0)\n\t\t? ((st::historyAttach.height - st::historyAttach.icon.height()) / 2)\n\t\t: st::historyAttach.iconPosition.y();\n\tst::historyAttach.icon[_palette].paint(*_p, _composeArea.x() + attachIconLeft, controlsTop + attachIconTop, _rect.width());\n\tauto right = st::historySendRight + st::historySendSize.width();\n\n\tconst auto recordIcon = Lottie::MakeIcon({\n\t\t.path = u\":/animations/chat/voice_to_video.tgs\"_q,\n\t\t.sizeOverride = st::historySend.recordSize,\n\t\t.colorizeUsingAlpha = true,\n\t});\n\trecordIcon->paintInCenter(*_p, QRect(_composeArea.x() + _composeArea.width() - right, controlsTop, st::historySendSize.width(), st::historySendSize.height()), st::historyRecordVoiceFg[_palette]->c);\n\n\tconst auto &emojiButton = st::historyAttachEmoji.inner;\n\tconst auto emojiIconLeft = (emojiButton.iconPosition.x() < 0)\n\t\t? ((emojiButton.width - emojiButton.icon.width()) / 2)\n\t\t: emojiButton.iconPosition.x();\n\tconst auto emojiIconTop = (emojiButton.iconPosition.y() < 0)\n\t\t? ((emojiButton.height - emojiButton.icon.height()) / 2)\n\t\t: emojiButton.iconPosition.y();\n\tconst auto &emojiIcon = emojiButton.icon[_palette];\n\tright += emojiButton.width;\n\tauto attachEmojiLeft = _composeArea.x() + _composeArea.width() - right;\n\t_p->fillRect(attachEmojiLeft, controlsTop, emojiButton.width, emojiButton.height, st::historyComposeAreaBg[_palette]);\n\temojiIcon.paint(*_p, attachEmojiLeft + emojiIconLeft, controlsTop + emojiIconTop, _rect.width());\n\n\tauto pen = st::historyEmojiCircleFg[_palette]->p;\n\tpen.setWidthF(style::ConvertScaleExact(st::historyEmojiCircleLine));\n\tpen.setCapStyle(Qt::RoundCap);\n\t_p->setPen(pen);\n\t_p->setBrush(Qt::NoBrush);\n\n\tPainterHighQualityEnabler hq(*_p);\n\tconst auto skipx = emojiIcon.width() / 4;\n\tconst auto skipy = emojiIcon.height() / 4;\n\tconst auto inner = QRect(\n\t\tattachEmojiLeft + emojiIconLeft + skipx,\n\t\tcontrolsTop + emojiIconTop + skipy,\n\t\temojiIcon.width() - 2 * skipx,\n\t\temojiIcon.height() - 2 * skipy);\n\t_p->drawEllipse(inner);\n\n\tauto fieldLeft = _composeArea.x() + st::historyAttach.width;\n\tauto fieldTop = _composeArea.y() + _composeArea.height() - st::historyAttach.height + st::historySendPadding;\n\tauto fieldWidth = _composeArea.width() - st::historyAttach.width - st::historySendSize.width() - st::historySendRight - emojiButton.width;\n\tauto fieldHeight = st::historySendSize.height() - 2 * st::historySendPadding;\n\tauto field = QRect(fieldLeft, fieldTop, fieldWidth, fieldHeight);\n\t_p->fillRect(field, st::historyComposeField.textBg[_palette]);\n\n\t_p->setClipRect(field);\n\t_p->save();\n\t_p->setFont(st::historyComposeField.style.font);\n\t_p->setPen(st::historyComposeField.placeholderFg[_palette]);\n\n\tauto placeholderRect = QRect(\n\t\tfield.x() + st::historyComposeField.textMargins.left() + st::historyComposeField.placeholderMargins.left(),\n\t\tfield.y() + st::historyComposeField.textMargins.top() + st::historyComposeField.placeholderMargins.top(),\n\t\tfield.width() - st::historyComposeField.textMargins.left() - st::historyComposeField.textMargins.right(),\n\t\tfield.height() - st::historyComposeField.textMargins.top() - st::historyComposeField.textMargins.bottom());\n\t_p->drawText(placeholderRect, tr::lng_message_ph(tr::now), QTextOption(st::historyComposeField.placeholderAlign));\n\n\t_p->restore();\n\t_p->setClipping(false);\n}\n\nvoid Generator::paintDialogs() {\n\t_p->fillRect(_dialogs, st::dialogsBg[_palette]);\n\n\tconst auto iconLeft = (st::dialogsMenuToggle.iconPosition.x() < 0)\n\t\t? (st::dialogsMenuToggle.width - st::dialogsMenuToggle.icon.width()) / 2\n\t\t: st::dialogsMenuToggle.iconPosition.x();\n\tconst auto iconTop = (st::dialogsMenuToggle.iconPosition.y() < 0)\n\t\t? (st::dialogsMenuToggle.height - st::dialogsMenuToggle.icon.height()) / 2\n\t\t: st::dialogsMenuToggle.iconPosition.y();\n\tst::dialogsMenuToggle.icon[_palette].paint(*_p, _dialogs.x() + st::dialogsFilterPadding.x() + iconLeft, _dialogs.y() + st::dialogsFilterPadding.y() + iconTop, _rect.width());\n\n\tauto filterLeft = _dialogs.x() + st::dialogsFilterPadding.x() + st::dialogsMenuToggle.width + st::dialogsFilterPadding.x();\n\tauto filterRight = st::dialogsFilterSkip + st::dialogsFilterPadding.x();\n\tauto filterWidth = _dialogs.x() + _dialogs.width() - filterLeft - filterRight;\n\tauto filterAreaHeight = st::topBarHeight;\n\tauto filterTop = _dialogs.y() + (filterAreaHeight - st::dialogsFilter.heightMin) / 2;\n\tauto filter = QRect(filterLeft, filterTop, filterWidth, st::dialogsFilter.heightMin);\n\n\tauto pen = st::dialogsFilter.borderFg[_palette]->p;\n\tpen.setWidth(st::dialogsFilter.border);\n\t_p->setPen(pen);\n\t_p->setBrush(st::dialogsFilter.textBg[_palette]);\n\t{\n\t\tPainterHighQualityEnabler hq(*_p);\n\t\tconst auto radius = st::dialogsFilter.borderRadius\n\t\t\t- (st::dialogsFilter.border / 2.);\n\t\t_p->drawRoundedRect(\n\t\t\tQRectF(filter).marginsRemoved(\n\t\t\t\tQMarginsF(\n\t\t\t\t\tst::dialogsFilter.border / 2.,\n\t\t\t\t\tst::dialogsFilter.border / 2.,\n\t\t\t\t\tst::dialogsFilter.border / 2.,\n\t\t\t\t\tst::dialogsFilter.border / 2.)),\n\t\t\tradius,\n\t\t\tradius);\n\t}\n\n\t_p->save();\n\t_p->setClipRect(filter);\n\tauto phRect = QRect(filter.x() + st::dialogsFilter.textMargins.left() + st::dialogsFilter.placeholderMargins.left(), filter.y() + st::dialogsFilter.textMargins.top() + st::dialogsFilter.placeholderMargins.top(), filter.width() - st::dialogsFilter.textMargins.left() - st::dialogsFilter.textMargins.right(), filter.height() - st::dialogsFilter.textMargins.top() - st::dialogsFilter.textMargins.bottom());\n\t_p->setFont(st::dialogsFilter.style.font);\n\t_p->setPen(st::dialogsFilter.placeholderFg[_palette]);\n\t_p->drawText(phRect, tr::lng_dlg_filter(tr::now), QTextOption(st::dialogsFilter.placeholderAlign));\n\t_p->restore();\n\t_p->setClipping(false);\n\n\tpaintDialogsList();\n}\n\nvoid Generator::paintDialogsList() {\n\t_p->setClipRect(_dialogsList);\n\t_rowsTop = _dialogsList.y();\n\tfor (auto &row : _rows) {\n\t\tpaintRow(row);\n\t\t_rowsTop += st::dialogsRowHeight;\n\t}\n\t_p->setClipping(false);\n}\n\nvoid Generator::paintRow(const Row &row) {\n\tauto x = _dialogsList.x();\n\tauto y = _rowsTop;\n\tauto fullWidth = _dialogsList.width();\n\tauto fullRect = QRect(x, y, fullWidth, st::dialogsRowHeight);\n\tif (row.active || row.selected) {\n\t\t_p->fillRect(fullRect, row.active ? st::dialogsBgActive[_palette] : st::dialogsBgOver[_palette]);\n\t}\n\tconst auto &st = st::defaultDialogRow;\n\tpaintUserpic(\n\t\tx + st.padding.left(),\n\t\ty + st.padding.top(),\n\t\trow.type,\n\t\trow.peerIndex,\n\t\trow.letters);\n\n\tauto nameleft = x + st.nameLeft;\n\tauto namewidth = x + fullWidth - nameleft - st.padding.right();\n\tauto rectForName = QRect(nameleft, y + st.nameTop, namewidth, st::msgNameFont->height);\n\n\tauto chatTypeIcon = ([&row]() -> const style::icon * {\n\t\tif (row.type == Row::Type::Group) {\n\t\t\treturn &Dialogs::ThreeStateIcon(\n\t\t\t\tst::dialogsChatIcon,\n\t\t\t\trow.active,\n\t\t\t\trow.selected);\n\t\t} else if (row.type == Row::Type::Channel) {\n\t\t\treturn &Dialogs::ThreeStateIcon(\n\t\t\t\tst::dialogsChannelIcon,\n\t\t\t\trow.active,\n\t\t\t\trow.selected);\n\t\t}\n\t\treturn nullptr;\n\t})();\n\tif (chatTypeIcon) {\n\t\t(*chatTypeIcon)[_palette].paint(*_p, rectForName.topLeft(), fullWidth);\n\t\trectForName.setLeft(rectForName.left()\n\t\t\t+ chatTypeIcon->width()\n\t\t\t+ st::dialogsChatTypeSkip);\n\t}\n\n\tauto texttop = y + st.textTop;\n\n\tauto dateWidth = st::dialogsDateFont->width(row.date);\n\trectForName.setWidth(rectForName.width() - dateWidth - st::dialogsDateSkip);\n\t_p->setFont(st::dialogsDateFont);\n\t_p->setPen(row.active ? st::dialogsDateFgActive[_palette] : (row.selected ? st::dialogsDateFgOver[_palette] : st::dialogsDateFg[_palette]));\n\t_p->drawText(rectForName.left() + rectForName.width() + st::dialogsDateSkip, rectForName.top() + st::msgNameFont->height - st::msgDateFont->descent, row.date);\n\n\tauto availableWidth = namewidth;\n\tif (row.unreadCounter) {\n\t\tauto counter = QString::number(row.unreadCounter);\n\t\tauto unreadRight = x + fullWidth - st.padding.right();\n\t\tauto unreadTop = texttop + st::dialogsTextFont->ascent - st::dialogsUnreadFont->ascent - (st::dialogsUnreadHeight - st::dialogsUnreadFont->height) / 2;\n\n\t\tauto unreadWidth = st::dialogsUnreadFont->width(counter);\n\t\tauto unreadRectWidth = unreadWidth + 2 * st::dialogsUnreadPadding;\n\t\tauto unreadRectHeight = st::dialogsUnreadHeight;\n\t\taccumulate_max(unreadRectWidth, unreadRectHeight);\n\n\t\tauto unreadRectLeft = unreadRight - unreadRectWidth;\n\t\tauto unreadRectTop = unreadTop;\n\t\tavailableWidth -= unreadRectWidth + st::dialogsUnreadPadding;\n\n\t\tstyle::color bg[] = {\n\t\t\tst::dialogsUnreadBg,\n\t\t\tst::dialogsUnreadBgOver,\n\t\t\tst::dialogsUnreadBgActive,\n\t\t\tst::dialogsUnreadBgMuted,\n\t\t\tst::dialogsUnreadBgMutedOver,\n\t\t\tst::dialogsUnreadBgMutedActive\n\t\t};\n\n\t\tauto index = (row.active ? 2 : row.selected ? 1 : 0) + (row.muted ? 3 : 0);\n\t\t_p->setPen(Qt::NoPen);\n\t\t_p->setBrush(bg[index][_palette]);\n\t\t_p->drawRoundedRect(QRectF(unreadRectLeft, unreadRectTop, unreadRectWidth, unreadRectHeight), unreadRectHeight / 2., unreadRectHeight / 2.);\n\n\t\tauto textTop = (unreadRectHeight - st::dialogsUnreadFont->height) / 2;\n\t\t_p->setFont(st::dialogsUnreadFont);\n\t\t_p->setPen(row.active ? st::dialogsUnreadFgActive[_palette] : (row.selected ? st::dialogsUnreadFgOver[_palette] : st::dialogsUnreadFg[_palette]));\n\t\t_p->drawText(unreadRectLeft + (unreadRectWidth - unreadWidth) / 2, unreadRectTop + textTop + st::dialogsUnreadFont->ascent, counter);\n\t} else if (row.pinned) {\n\t\tauto icon = Dialogs::ThreeStateIcon(\n\t\t\tst::dialogsPinnedIcon,\n\t\t\trow.active,\n\t\t\trow.selected)[_palette];\n\t\ticon.paint(*_p, x + fullWidth - st.padding.right() - icon.width(), texttop, fullWidth);\n\t\tavailableWidth -= icon.width() + st::dialogsUnreadPadding;\n\t}\n\tauto textRect = QRect(nameleft, texttop, availableWidth, st::dialogsTextFont->height);\n\tsetTextPalette(row.active ? st::dialogsTextPaletteActive : (row.selected ? st::dialogsTextPaletteOver : st::dialogsTextPalette));\n\t_p->setFont(st::dialogsTextFont);\n\t_p->setPen(row.active ? st::dialogsTextFgActive[_palette] : (row.selected ? st::dialogsTextFgOver[_palette] : st::dialogsTextFg[_palette]));\n\trow.text.drawElided(*_p, textRect.left(), textRect.top(), textRect.width(), textRect.height() / st::dialogsTextFont->height);\n\trestoreTextPalette();\n\n\tauto sendStateIcon = ([&row]() -> const style::icon* {\n\t\tif (row.status == Status::Sent) {\n\t\t\treturn &Dialogs::ThreeStateIcon(\n\t\t\t\tst::dialogsSentIcon,\n\t\t\t\trow.active,\n\t\t\t\trow.selected);\n\t\t} else if (row.status == Status::Received) {\n\t\t\treturn &Dialogs::ThreeStateIcon(\n\t\t\t\tst::dialogsReceivedIcon,\n\t\t\t\trow.active,\n\t\t\t\trow.selected);\n\t\t}\n\t\treturn nullptr;\n\t})();\n\tif (sendStateIcon) {\n\t\trectForName.setWidth(rectForName.width() - st::dialogsSendStateSkip);\n\t\t(*sendStateIcon)[_palette].paint(*_p, rectForName.topLeft() + QPoint(rectForName.width(), 0), fullWidth);\n\t}\n\t_p->setPen(row.active ? st::dialogsNameFgActive[_palette] : (row.selected ? st::dialogsNameFgOver[_palette] : st::dialogsNameFg[_palette]));\n\trow.name.drawElided(*_p, rectForName.left(), rectForName.top(), rectForName.width());\n}\n\nvoid Generator::paintBubble(const Bubble &bubble) {\n\tauto height = bubble.height;\n\tif (!bubble.replyName.isEmpty()) {\n\t\theight += st::historyReplyTop\n\t\t\t+ st::historyReplyPadding.top()\n\t\t\t+ st::msgServiceNameFont->height\n\t\t\t+ st::normalFont->height\n\t\t\t+ st::historyReplyPadding.bottom()\n\t\t\t+ st::historyReplyBottom;\n\t}\n\tauto isPhoto = !bubble.photo.isNull();\n\n\tauto x = _history.x();\n\tauto y = _historyBottom - st::msgMargin.bottom() - height;\n\tauto bubbleTop = y;\n\tauto bubbleHeight = height;\n\tif (isPhoto) {\n\t\tbubbleTop -= Ui::BubbleRadiusLarge() + 1;\n\t\tbubbleHeight += Ui::BubbleRadiusLarge() + 1;\n\t}\n\n\tauto left = bubble.outbg ? st::msgMargin.right() : st::msgMargin.left();\n\tif (bubble.outbg) {\n\t\tleft += _history.width() - st::msgMargin.left() - st::msgMargin.right() - bubble.width;\n\t}\n\tx += left;\n\n\tusing Corner = Ui::BubbleCornerRounding;\n\tauto rounding = Ui::BubbleRounding{\n\t\tCorner::Large,\n\t\tCorner::Large,\n\t\tCorner::Large,\n\t\tCorner::Large,\n\t};\n\tif (bubble.outbg) {\n\t\tif (bubble.attachToTop) {\n\t\t\trounding.topRight = Corner::Small;\n\t\t}\n\t\tif (bubble.attachToBottom) {\n\t\t\trounding.bottomRight = Corner::Small;\n\t\t} else if (bubble.tail) {\n\t\t\trounding.bottomRight = Corner::Tail;\n\t\t}\n\t} else {\n\t\tif (bubble.attachToTop) {\n\t\t\trounding.topLeft = Corner::Small;\n\t\t}\n\t\tif (bubble.attachToBottom) {\n\t\t\trounding.bottomLeft = Corner::Small;\n\t\t} else if (bubble.tail) {\n\t\t\trounding.bottomLeft = Corner::Tail;\n\t\t}\n\t}\n\tUi::PaintBubble(*_p, Ui::SimpleBubble{\n\t\t.st = &_st,\n\t\t.geometry = QRect(x, bubbleTop, bubble.width, bubbleHeight),\n\t\t.outerWidth = _rect.width(),\n\t\t.outbg = bubble.outbg,\n\t\t.rounding = rounding,\n\t});\n\n\tauto trect = QRect(x, y, bubble.width, bubble.height);\n\tif (isPhoto) {\n\t\ttrect = trect.marginsRemoved(QMargins(st::msgPadding.left(), st::mediaCaptionSkip, st::msgPadding.right(), st::msgPadding.bottom()));\n\t} else {\n\t\ttrect = trect.marginsRemoved(st::msgPadding);\n\t}\n\tif (!bubble.replyName.isEmpty()) {\n\t\ttrect.setY(trect.y() + st::historyReplyTop);\n\t\tauto bar = (bubble.outbg ? st::msgOutReplyBarColor[_palette] : st::msgInReplyBarColor[_palette]);\n\t\tauto rbar = style::rtlrect(\n\t\t\ttrect.x(),\n\t\t\ttrect.y(),\n\t\t\ttrect.width(),\n\t\t\t(st::historyReplyPadding.top()\n\t\t\t\t+ st::msgServiceNameFont->height\n\t\t\t\t+ st::normalFont->height\n\t\t\t\t+ st::historyReplyPadding.bottom()),\n\t\t\t_rect.width());\n\t\t{\n\t\t\tauto hq = PainterHighQualityEnabler(*_p);\n\t\t\t_p->setPen(Qt::NoPen);\n\t\t\t_p->setBrush(bar);\n\n\t\t\tconst auto outline = st::messageTextStyle.blockquote.outline;\n\t\t\tconst auto radius = st::messageTextStyle.blockquote.radius;\n\t\t\t_p->setOpacity(Ui::kDefaultOutline1Opacity);\n\t\t\t_p->setClipRect(rbar.x(), rbar.y(), outline, rbar.height());\n\t\t\t_p->drawRoundedRect(rbar, radius, radius);\n\t\t\t_p->setOpacity(Ui::kDefaultBgOpacity);\n\t\t\t_p->setClipRect(\n\t\t\t\trbar.x() + outline,\n\t\t\t\trbar.y(),\n\t\t\t\trbar.width() - outline,\n\t\t\t\trbar.height());\n\t\t\t_p->drawRoundedRect(rbar, radius, radius);\n\t\t}\n\t\t_p->setOpacity(1.);\n\t\t_p->setClipping(false);\n\n\t\t_p->setPen(bubble.outbg ? st::msgOutServiceFg[_palette] : st::msgInServiceFg[_palette]);\n\t\tbubble.replyName.drawLeftElided(*_p, trect.x() + st::historyReplyPadding.left(), trect.y() + st::historyReplyPadding.top(), bubble.width - st::historyReplyPadding.left() - st::historyReplyPadding.right(), _rect.width());\n\n\t\t_p->setPen(bubble.outbg ? st::historyTextOutFg[_palette] : st::historyTextInFg[_palette]);\n\t\tbubble.replyText.drawLeftElided(*_p, trect.x() + st::historyReplyPadding.left(), trect.y() + st::historyReplyPadding.top() + st::msgServiceNameFont->height, bubble.width - st::historyReplyPadding.left() - st::historyReplyPadding.right(), _rect.width());\n\n\t\ttrect.setY(trect.y() + rbar.height() + st::historyReplyBottom);\n\t}\n\n\tif (!bubble.text.isEmpty()) {\n\t\tsetTextPalette(bubble.outbg ? st::outTextPalette : st::inTextPalette);\n\t\t_p->setPen(bubble.outbg ? st::historyTextOutFg[_palette] : st::historyTextInFg[_palette]);\n\t\t_p->setFont(st::msgFont);\n\t\tbubble.text.draw(*_p, trect.x(), trect.y(), trect.width());\n\t} else if (!bubble.waveform.isEmpty()) {\n\t\tconst auto &st = st::msgFileLayout;\n\t\tauto nameleft = x + st.padding.left() + st.thumbSize + st.thumbSkip;\n\t\tauto nameright = st.padding.right();\n\t\tauto statustop = y + st.statusTop;\n\n\t\tauto inner = style::rtlrect(x + st.padding.left(), y + st.padding.top(), st.thumbSize, st.thumbSize, _rect.width());\n\t\t_p->setPen(Qt::NoPen);\n\t\t_p->setBrush(bubble.outbg ? st::msgFileOutBg[_palette] : st::msgFileInBg[_palette]);\n\n\t\t_p->drawEllipse(inner);\n\n\t\tauto icon = ([&bubble] {\n\t\t\treturn &(bubble.outbg ? st::historyFileOutPlay : st::historyFileInPlay);\n\t\t})();\n\t\t(*icon)[_palette].paintInCenter(*_p, inner);\n\n\t\tauto namewidth = x + bubble.width - nameleft - nameright;\n\n\t\t// rescale waveform by going in waveform.size * bar_count 1D grid\n\t\tauto active = bubble.outbg ? st::msgWaveformOutActive[_palette] : st::msgWaveformInActive[_palette];\n\t\tauto inactive = bubble.outbg ? st::msgWaveformOutInactive[_palette] : st::msgWaveformInInactive[_palette];\n\t\tauto wf_size = bubble.waveform.size();\n\t\tauto availw = namewidth + st::msgWaveformSkip;\n\t\tauto bar_count = qMin(availw / (st::msgWaveformBar + st::msgWaveformSkip), wf_size);\n\t\tauto max_value = 0;\n\t\tauto max_delta = st::msgWaveformMax - st::msgWaveformMin;\n\t\tauto wave_bottom = y + st::msgFileLayout.padding.top() + st::msgWaveformMax;\n\t\t_p->setPen(Qt::NoPen);\n\t\tauto norm_value = uchar(31);\n\t\tfor (auto i = 0, bar_x = 0, sum_i = 0; i < wf_size; ++i) {\n\t\t\tauto value = bubble.waveform[i];\n\t\t\tif (sum_i + bar_count >= wf_size) { // draw bar\n\t\t\t\tsum_i = sum_i + bar_count - wf_size;\n\t\t\t\tif (sum_i < (bar_count + 1) / 2) {\n\t\t\t\t\tif (max_value < value) max_value = value;\n\t\t\t\t}\n\t\t\t\tauto bar_value = ((max_value * max_delta) + ((norm_value + 1) / 2)) / (norm_value + 1);\n\n\t\t\t\tif (i >= bubble.waveactive) {\n\t\t\t\t\t_p->fillRect(nameleft + bar_x, wave_bottom - bar_value, st::msgWaveformBar, st::msgWaveformMin + bar_value, inactive);\n\t\t\t\t} else {\n\t\t\t\t\t_p->fillRect(nameleft + bar_x, wave_bottom - bar_value, st::msgWaveformBar, st::msgWaveformMin + bar_value, active);\n\t\t\t\t}\n\t\t\t\tbar_x += st::msgWaveformBar + st::msgWaveformSkip;\n\n\t\t\t\tif (sum_i < (bar_count + 1) / 2) {\n\t\t\t\t\tmax_value = 0;\n\t\t\t\t} else {\n\t\t\t\t\tmax_value = value;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (max_value < value) max_value = value;\n\n\t\t\t\tsum_i += bar_count;\n\t\t\t}\n\t\t}\n\n\t\tauto status = bubble.outbg ? st::mediaOutFg[_palette] : st::mediaInFg[_palette];\n\t\t_p->setFont(st::normalFont);\n\t\t_p->setPen(status);\n\t\t_p->drawTextLeft(nameleft, statustop, _rect.width(), bubble.wavestatus);\n\t}\n\n\t_p->setFont(st::msgDateFont);\n\tauto infoRight = x + bubble.width - st::msgPadding.right() + st::msgDateDelta.x();\n\tauto infoBottom = y + height - st::msgPadding.bottom() + st::msgDateDelta.y();\n\t_p->setPen(bubble.outbg ? st::msgOutDateFg[_palette] : st::msgInDateFg[_palette]);\n\tauto infoWidth = computeInfoWidth(bubble.status, bubble.date);\n\n\tauto dateX = infoRight - infoWidth;\n\tauto dateY = infoBottom - st::msgDateFont->height;\n\t_p->drawText(dateX, dateY + st::msgDateFont->ascent, bubble.date);\n\tauto icon = ([&bubble]() -> const style::icon * {\n\t\tif (bubble.status == Status::Sent) {\n\t\t\treturn &st::historySentIcon;\n\t\t} else if (bubble.status == Status::Received) {\n\t\t\treturn &st::historyReceivedIcon;\n\t\t}\n\t\treturn nullptr;\n\t})();\n\tif (icon) {\n\t\t(*icon)[_palette].paint(*_p, QPoint(infoRight, infoBottom) + st::historySendStatePosition, _rect.width());\n\t}\n\n\t_historyBottom = y - (bubble.attachToTop ? st::msgMarginTopAttached : st::msgMargin.top());\n\n\tif (isPhoto) {\n\t\tauto image = bubble.photo.scaled(\n\t\t\tQSize(bubble.photoWidth, bubble.photoHeight)\n\t\t\t\t* style::DevicePixelRatio(),\n\t\t\tQt::IgnoreAspectRatio,\n\t\t\tQt::SmoothTransformation);\n\t\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\t\t_p->drawImage(x, y - bubble.photoHeight, image);\n\t\t_historyBottom -= bubble.photoHeight;\n\t}\n}\n\nvoid Generator::paintService(QString text) {\n\tauto bubbleHeight = st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom();\n\tauto bubbleTop = _historyBottom - st::msgServiceMargin.bottom() - bubbleHeight;\n\tauto textWidth = st::msgServiceFont->width(text);\n\tauto bubbleWidth = st::msgServicePadding.left() + textWidth + st::msgServicePadding.right();\n\tauto radius = bubbleHeight / 2;\n\t_p->setPen(Qt::NoPen);\n\t_p->setBrush(st::msgServiceBg[_palette]);\n\tauto bubbleLeft = _history.x() + (_history.width() - bubbleWidth) / 2;\n\t_p->drawRoundedRect(bubbleLeft, bubbleTop, bubbleWidth, bubbleHeight, radius, radius);\n\t_p->setPen(st::msgServiceFg[_palette]);\n\t_p->setFont(st::msgServiceFont);\n\t_p->drawText(bubbleLeft + st::msgServicePadding.left(), bubbleTop + st::msgServicePadding.top() + st::msgServiceFont->ascent, text);\n\t_historyBottom = bubbleTop - st::msgServiceMargin.top();\n}\n\nvoid Generator::paintUserpic(int x, int y, Row::Type type, int index, QString letters) {\n\tconst auto colorIndex = Ui::DecideColorIndex(index);\n\tconst auto colors = Ui::EmptyUserpic::UserpicColor(colorIndex);\n\tauto userpic = Ui::EmptyUserpic(colors, letters);\n\n\tconst auto size = st::defaultDialogRow.photoSize;\n\tauto image = QImage(\n\t\tQSize(size, size) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\timage.fill(Qt::transparent);\n\t{\n\t\tPainter p(&image);\n\t\tuserpic.paintCircle(p, 0, 0, size, size);\n\t}\n\t_p->drawImage(rtl() ? (_rect.width() - x - size) : x, y, image);\n}\n\nvoid Generator::paintHistoryShadows() {\n\t_p->fillRect(_history.x() + st::lineWidth, _history.y(), _history.width() - st::lineWidth, st::lineWidth, st::shadowFg[_palette]);\n\t_p->fillRect(_history.x() + st::lineWidth, _history.y() + _history.height() - st::lineWidth, _history.width() - st::lineWidth, st::lineWidth, st::shadowFg[_palette]);\n\t_p->fillRect(_history.x(), _body.y(), st::lineWidth, _body.height(), st::shadowFg[_palette]);\n}\n\nvoid Generator::setTextPalette(const style::TextPalette &st) {\n\t_textPalette.linkFg = st.linkFg[_palette].clone();\n\t_textPalette.monoFg = st.monoFg[_palette].clone();\n\t_textPalette.spoilerFg = st.spoilerFg[_palette].clone();\n\t_textPalette.selectBg = st.selectBg[_palette].clone();\n\t_textPalette.selectFg = st.selectFg[_palette].clone();\n\t_textPalette.selectLinkFg = st.selectLinkFg[_palette].clone();\n\t_textPalette.selectMonoFg = st.selectMonoFg[_palette].clone();\n\t_textPalette.selectSpoilerFg = st.selectSpoilerFg[_palette].clone();\n\t_textPalette.selectOverlay = st.selectOverlay[_palette].clone();\n\t_p->setTextPalette(_textPalette);\n}\n\nvoid Generator::restoreTextPalette() {\n\t_p->restoreTextPalette();\n}\n\n} // namespace\n\nQString CachedThemePath(uint64 documentId) {\n\treturn QString::fromLatin1(\"special://cached-%1\").arg(documentId);\n}\n\nstd::unique_ptr<Preview> PreviewFromFile(\n\t\tconst QByteArray &bytes,\n\t\tconst QString &filepath,\n\t\tconst Data::CloudTheme &cloud) {\n\tauto result = std::make_unique<Preview>();\n\tauto &object = result->object;\n\tobject.cloud = cloud;\n\tobject.pathAbsolute = filepath.isEmpty()\n\t\t? CachedThemePath(cloud.documentId)\n\t\t: QFileInfo(filepath).absoluteFilePath();\n\tobject.pathRelative = filepath.isEmpty()\n\t\t? object.pathAbsolute\n\t\t: QDir().relativeFilePath(filepath);\n\tconst auto instance = &result->instance;\n\tconst auto cache = &result->instance.cached;\n\tif (bytes.isEmpty()) {\n\t\tif (!LoadFromFile(filepath, instance, cache, &object.content)) {\n\t\t\treturn nullptr;\n\t\t}\n\t} else {\n\t\tobject.content = bytes;\n\t\tif (!LoadFromContent(bytes, instance, cache)) {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\treturn result;\n}\n\nstd::unique_ptr<Preview> GeneratePreview(\n\t\tconst QByteArray &bytes,\n\t\tconst QString &filepath,\n\t\tconst Data::CloudTheme &cloud,\n\t\tCurrentData &&data,\n\t\tPreviewType type) {\n\tauto result = PreviewFromFile(bytes, filepath, cloud);\n\tif (!result) {\n\t\treturn nullptr;\n\t}\n\tresult->preview = Generator(\n\t\tresult->instance,\n\t\tstd::move(data),\n\t\ttype\n\t).generate();\n\treturn result;\n}\n\nQImage GeneratePreview(\n\t\tconst QByteArray &bytes,\n\t\tconst QString &filepath) {\n\tconst auto preview = GeneratePreview(\n\t\tbytes,\n\t\tfilepath,\n\t\tData::CloudTheme(),\n\t\tCurrentData{ Data::ThemeWallPaper().id() },\n\t\tPreviewType::Normal);\n\treturn preview ? preview->preview : QImage();\n}\n\nint DefaultPreviewTitleHeight() {\n\treturn st::defaultWindowTitle.height;\n}\n\nvoid DefaultPreviewWindowTitle(Painter &p, const style::palette &palette, QRect body, int outerWidth) {\n\tauto titleRect = QRect(body.x(), body.y() - st::defaultWindowTitle.height, body.width(), st::defaultWindowTitle.height);\n\tp.fillRect(titleRect, QColor(0, 0, 0));\n\tp.fillRect(titleRect, st::titleBgActive[palette]);\n\tauto right = st::defaultWindowTitle.close.width;\n\tst::defaultWindowTitle.close.icon[palette].paint(p, titleRect.x() + titleRect.width() - right + st::defaultWindowTitle.close.iconPosition.x(), titleRect.y() + st::windowTitleButtonClose.iconPosition.y(), outerWidth);\n\tright += st::defaultWindowTitle.maximize.width;\n\tst::defaultWindowTitle.maximize.icon[palette].paint(p, titleRect.x() + titleRect.width() - right + st::defaultWindowTitle.maximize.iconPosition.x(), titleRect.y() + st::defaultWindowTitle.maximize.iconPosition.y(), outerWidth);\n\tright += st::defaultWindowTitle.minimize.width;\n\tst::defaultWindowTitle.minimize.icon[palette].paint(p, titleRect.x() + titleRect.width() - right + st::defaultWindowTitle.minimize.iconPosition.x(), titleRect.y() + st::defaultWindowTitle.minimize.iconPosition.y(), outerWidth);\n\tp.fillRect(titleRect.x(), titleRect.y() + titleRect.height() - st::lineWidth, titleRect.width(), st::lineWidth, st::titleShadow[palette]);\n}\n\nvoid DefaultPreviewWindowFramePaint(QImage &preview, const style::palette &palette, QRect body, int outerWidth) {\n\tauto mask = QImage(\n\t\tst::windowShadow.size() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tmask.setDevicePixelRatio(style::DevicePixelRatio());\n\t{\n\t\tPainter p(&mask);\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tst::windowShadow.paint(p, 0, 0, st::windowShadow.width(), QColor(0, 0, 0));\n\t}\n\tauto maxSize = 0;\n\tauto currentInt = static_cast<uint32>(0);\n\tauto lastLineInts = reinterpret_cast<const uint32*>(mask.constBits() + (mask.height() - 1) * mask.bytesPerLine());\n\tfor (auto end = lastLineInts + mask.width(); lastLineInts != end; ++lastLineInts) {\n\t\tif (*lastLineInts < currentInt) {\n\t\t\tbreak;\n\t\t}\n\t\tcurrentInt = *lastLineInts;\n\t\t++maxSize;\n\t}\n\tif (maxSize % style::DevicePixelRatio()) {\n\t\tmaxSize -= (maxSize % style::DevicePixelRatio());\n\t}\n\tauto size = maxSize / style::DevicePixelRatio();\n\tauto bottom = size;\n\tauto left = size - st::windowShadowShift;\n\tauto right = left;\n\tauto top = size - 2 * st::windowShadowShift;\n\n\tauto sprite = st::windowShadow[palette];\n\tauto topLeft = QImage(\n\t\tsprite.size() * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\ttopLeft.setDevicePixelRatio(style::DevicePixelRatio());\n\t{\n\t\tPainter p(&topLeft);\n\t\tp.setCompositionMode(QPainter::CompositionMode_Source);\n\t\tsprite.paint(p, 0, 0, sprite.width());\n\t}\n\tauto width = sprite.width();\n\tauto height = sprite.height();\n\tauto topRight = topLeft.mirrored(true, false);\n\tauto bottomRight = topLeft.mirrored(true, true);\n\tauto bottomLeft = topLeft.mirrored(false, true);\n\n\tPainter p(&preview);\n\tDefaultPreviewWindowTitle(p, palette, body, outerWidth);\n\n\tauto inner = QRect(\n\t\tbody.x(),\n\t\tbody.y() - st::defaultWindowTitle.height,\n\t\tbody.width(),\n\t\tbody.height() + st::defaultWindowTitle.height);\n\tp.setClipRegion(QRegion(inner + Margins(size)) - inner);\n\tp.drawImage(inner.x() - left, inner.y() - top, topLeft);\n\tp.drawImage(\n\t\tinner.x() + inner.width() + right - width,\n\t\tinner.y() - top,\n\t\ttopRight);\n\tp.drawImage(\n\t\tinner.x() + inner.width() + right - width,\n\t\tinner.y() + inner.height() + bottom - height,\n\t\tbottomRight);\n\tp.drawImage(\n\t\tinner.x() - left,\n\t\tinner.y() + inner.height() + bottom - height,\n\t\tbottomLeft);\n\tp.drawImage(\n\t\tQRect(\n\t\t\tinner.x() - left,\n\t\t\tinner.y() - top + height,\n\t\t\tleft,\n\t\t\ttop + inner.height() + bottom - 2 * height),\n\t\ttopLeft,\n\t\tQRect(\n\t\t\t0,\n\t\t\ttopLeft.height() - style::DevicePixelRatio(),\n\t\t\tleft * style::DevicePixelRatio(),\n\t\t\tstyle::DevicePixelRatio()));\n\tp.drawImage(\n\t\tQRect(\n\t\t\tinner.x() - left + width,\n\t\t\tinner.y() - top,\n\t\t\tleft + inner.width() + right - 2 * width,\n\t\t\ttop),\n\t\ttopLeft,\n\t\tQRect(\n\t\t\ttopLeft.width() - style::DevicePixelRatio(),\n\t\t\t0,\n\t\t\tstyle::DevicePixelRatio(),\n\t\t\ttop * style::DevicePixelRatio()));\n\tp.drawImage(\n\t\tQRect(\n\t\t\tinner.x() + inner.width(),\n\t\t\tinner.y() - top + height,\n\t\t\tright,\n\t\t\ttop + inner.height() + bottom - 2 * height),\n\t\ttopRight,\n\t\tQRect(\n\t\t\ttopRight.width() - right * style::DevicePixelRatio(),\n\t\t\ttopRight.height() - style::DevicePixelRatio(),\n\t\t\tright * style::DevicePixelRatio(),\n\t\t\tstyle::DevicePixelRatio()));\n\tp.drawImage(\n\t\tQRect(\n\t\t\tinner.x() - left + width,\n\t\t\tinner.y() + inner.height(),\n\t\t\tleft + inner.width() + right - 2 * width,\n\t\t\tbottom),\n\t\tbottomRight,\n\t\tQRect(\n\t\t\t0,\n\t\t\tbottomRight.height() - bottom * style::DevicePixelRatio(),\n\t\t\tstyle::DevicePixelRatio(),\n\t\t\tbottom * style::DevicePixelRatio()));\n\n}\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_theme_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"window/themes/window_theme.h\"\n\nnamespace Data {\nstruct CloudTheme;\n} // namespace Data\n\nnamespace Window {\nnamespace Theme {\n\nstruct CurrentData {\n\tWallPaperId backgroundId = 0;\n\tQImage backgroundImage;\n\tbool backgroundTiled = false;\n};\n\nenum class PreviewType {\n\tNormal,\n\tExtended,\n};\n\n[[nodiscard]] QString CachedThemePath(uint64 documentId);\n\nstd::unique_ptr<Preview> PreviewFromFile(\n\tconst QByteArray &bytes,\n\tconst QString &filepath,\n\tconst Data::CloudTheme &cloud);\nstd::unique_ptr<Preview> GeneratePreview(\n\tconst QByteArray &bytes,\n\tconst QString &filepath,\n\tconst Data::CloudTheme &cloud,\n\tCurrentData &&data,\n\tPreviewType type);\nQImage GeneratePreview(\n\tconst QByteArray &bytes,\n\tconst QString &filepath);\n\nint DefaultPreviewTitleHeight();\nvoid DefaultPreviewWindowFramePaint(\n\tQImage &preview,\n\tconst style::palette &palette,\n\tQRect body,\n\tint outerWidth);\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_theme_warning.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/themes/window_theme_warning.h\"\n\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"window/themes/window_theme.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace Window {\nnamespace Theme {\nnamespace {\n\nconstexpr int kWaitBeforeRevertMs = 15999;\n\n} // namespace\n\nWarningWidget::WarningWidget(QWidget *parent)\n: RpWidget(parent)\n, _timer([=] { handleTimer(); })\n, _secondsLeft(kWaitBeforeRevertMs / 1000)\n, _keepChanges(this, tr::lng_theme_keep_changes(), st::defaultBoxButton)\n, _revert(this, tr::lng_theme_revert(), st::defaultBoxButton) {\n\t_keepChanges->setClickedCallback([] { KeepApplied(); });\n\t_revert->setClickedCallback([] { Revert(); });\n\tupdateText();\n}\n\nvoid WarningWidget::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Escape) {\n\t\tWindow::Theme::Revert();\n\t}\n}\n\nvoid WarningWidget::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tif (!_cache.isNull()) {\n\t\tif (!_animation.animating()) {\n\t\t\tif (isHidden()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tp.setOpacity(_animation.value(_hiding ? 0. : 1.));\n\t\tp.drawPixmap(_outer.topLeft(), _cache);\n\t\tif (!_animation.animating()) {\n\t\t\t_cache = QPixmap();\n\t\t\tshowChildren();\n\t\t\t_started = crl::now();\n\t\t\t_timer.callOnce(100);\n\t\t}\n\t\treturn;\n\t}\n\n\tUi::Shadow::paint(p, _inner, width(), st::boxRoundShadow);\n\tUi::FillRoundRect(p, _inner, st::boxBg, Ui::BoxCorners);\n\n\tp.setFont(st::boxTitleFont);\n\tp.setPen(st::boxTitleFg);\n\tp.drawTextLeft(_inner.x() + st::boxTitlePosition.x(), _inner.y() + st::boxTitlePosition.y(), width(), tr::lng_theme_sure_keep(tr::now));\n\n\tp.setFont(st::boxTextFont);\n\tp.setPen(st::boxTextFg);\n\tp.drawTextLeft(_inner.x() + st::boxTitlePosition.x(), _inner.y() + st::themeWarningTextTop, width(), _text);\n}\n\nvoid WarningWidget::resizeEvent(QResizeEvent *e) {\n\t_inner = QRect((width() - st::themeWarningWidth) / 2, (height() - st::themeWarningHeight) / 2, st::themeWarningWidth, st::themeWarningHeight);\n\t_outer = _inner.marginsAdded(st::boxRoundShadow.extend);\n\tupdateControlsGeometry();\n\tupdate();\n}\n\nvoid WarningWidget::updateControlsGeometry() {\n\tauto left = _inner.x() + _inner.width() - st::defaultBox.buttonPadding.right() - _keepChanges->width();\n\t_keepChanges->moveToLeft(left, _inner.y() + _inner.height() - st::defaultBox.buttonPadding.bottom() - _keepChanges->height());\n\t_revert->moveToLeft(left - st::defaultBox.buttonPadding.left() - _revert->width(), _keepChanges->y());\n}\n\nvoid WarningWidget::refreshLang() {\n\tInvokeQueued(this, [this] { updateControlsGeometry(); });\n}\n\nvoid WarningWidget::handleTimer() {\n\tauto msPassed = crl::now() - _started;\n\tsetSecondsLeft((kWaitBeforeRevertMs - msPassed) / 1000);\n}\n\nvoid WarningWidget::setSecondsLeft(int secondsLeft) {\n\tif (secondsLeft <= 0) {\n\t\tWindow::Theme::Revert();\n\t} else {\n\t\tif (_secondsLeft != secondsLeft) {\n\t\t\t_secondsLeft = secondsLeft;\n\t\t\tupdateText();\n\t\t\tupdate();\n\t\t}\n\t\t_timer.callOnce(100);\n\t}\n}\n\nvoid WarningWidget::updateText() {\n\t_text = tr::lng_theme_reverting(tr::now, lt_count, _secondsLeft);\n}\n\nvoid WarningWidget::showAnimated() {\n\tstartAnimation(false);\n\tshow();\n\tsetFocus();\n}\n\nvoid WarningWidget::hideAnimated() {\n\tstartAnimation(true);\n}\n\nvoid WarningWidget::startAnimation(bool hiding) {\n\t_timer.cancel();\n\t_hiding = hiding;\n\tif (_cache.isNull()) {\n\t\tshowChildren();\n\t\tUi::SendPendingMoveResizeEvents(this);\n\t\t_cache = Ui::GrabWidget(this, _outer);\n\t}\n\thideChildren();\n\t_animation.start([this] {\n\t\tupdate();\n\t\tif (_hiding) {\n\t\t\thide();\n\t\t\tif (_hiddenCallback) {\n\t\t\t\t_hiddenCallback();\n\t\t\t}\n\t\t}\n\t}, _hiding ? 1. : 0., _hiding ? 0. : 1., st::boxDuration);\n}\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_theme_warning.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"base/object_ptr.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Ui {\nclass RoundButton;\n} // namespace Ui\n\nnamespace Window {\nnamespace Theme {\n\nclass WarningWidget : public Ui::RpWidget {\npublic:\n\tWarningWidget(QWidget *parent);\n\n\tvoid setHiddenCallback(Fn<void()> callback) {\n\t\t_hiddenCallback = std::move(callback);\n\t}\n\n\tvoid showAnimated();\n\tvoid hideAnimated();\n\nprotected:\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid refreshLang();\n\tvoid updateControlsGeometry();\n\tvoid setSecondsLeft(int secondsLeft);\n\tvoid startAnimation(bool hiding);\n\tvoid updateText();\n\tvoid handleTimer();\n\n\tbool _hiding = false;\n\tUi::Animations::Simple _animation;\n\tQPixmap _cache;\n\tQRect _inner, _outer;\n\n\tbase::Timer _timer;\n\tcrl::time _started = 0;\n\tint _secondsLeft = 0;\n\tQString _text;\n\n\tobject_ptr<Ui::RoundButton> _keepChanges;\n\tobject_ptr<Ui::RoundButton> _revert;\n\n\tFn<void()> _hiddenCallback;\n\n};\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_themes_cloud_list.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/themes/window_themes_cloud_list.h\"\n\n#include \"window/themes/window_themes_embedded.h\"\n#include \"window/themes/window_theme_editor_box.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"data/data_cloud_themes.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_session.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/image/image_prepare.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/style/style_palette_colorizer.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/painter.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"core/application.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_menu_icons.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QClipboard>\n\nnamespace Window {\nnamespace Theme {\nnamespace {\n\nconstexpr auto kFakeCloudThemeId = 0xFFFFFFFFFFFFFFFAULL;\nconstexpr auto kShowPerRow = 4;\n\n[[nodiscard]] Data::CloudTheme FakeCloudTheme(const Object &object) {\n\tauto result = Data::CloudTheme();\n\tresult.id = result.documentId = kFakeCloudThemeId;\n\tresult.slug = object.pathAbsolute;\n\treturn result;\n}\n\n[[nodiscard]] QImage ColorsBackgroundFromImage(const QImage &source) {\n\tif (source.isNull()) {\n\t\treturn source;\n\t}\n\tconst auto from = source.size();\n\tconst auto to = st::settingsThemePreviewSize * style::DevicePixelRatio();\n\tif (to.width() * from.height() > to.height() * from.width()) {\n\t\tconst auto small = (from.width() > to.width())\n\t\t\t? source.scaledToWidth(to.width(), Qt::SmoothTransformation)\n\t\t\t: source;\n\t\tconst auto takew = small.width();\n\t\tconst auto takeh = std::max(\n\t\t\ttakew * to.height() / to.width(),\n\t\t\t1);\n\t\treturn (small.height() != takeh)\n\t\t\t? small.copy(0, (small.height() - takeh) / 2, takew, takeh)\n\t\t\t: small;\n\t} else {\n\t\tconst auto small = (from.height() > to.height())\n\t\t\t? source.scaledToHeight(to.height(), Qt::SmoothTransformation)\n\t\t\t: source;\n\t\tconst auto takeh = small.height();\n\t\tconst auto takew = std::max(\n\t\t\ttakeh * to.width() / to.height(),\n\t\t\t1);\n\t\treturn (small.width() != takew)\n\t\t\t? small.copy((small.width() - takew) / 2, 0, takew, takeh)\n\t\t\t: small;\n\t}\n}\n\n[[nodiscard]] std::optional<CloudListColors> ColorsFromTheme(\n\t\tconst QString &path,\n\t\tconst QByteArray &theme) {\n\tconst auto content = [&] {\n\t\tif (!theme.isEmpty()) {\n\t\t\treturn theme;\n\t\t}\n\t\tauto file = QFile(path);\n\t\treturn file.open(QIODevice::ReadOnly)\n\t\t\t? file.readAll()\n\t\t\t: QByteArray();\n\t}();\n\tif (content.isEmpty()) {\n\t\treturn std::nullopt;\n\t}\n\tauto instance = Instance();\n\tif (!LoadFromContent(content, &instance, nullptr)) {\n\t\treturn std::nullopt;\n\t}\n\tauto result = CloudListColors();\n\tresult.background = ColorsBackgroundFromImage(instance.background);\n\tresult.sent = st::msgOutBg[instance.palette]->c;\n\tresult.received = st::msgInBg[instance.palette]->c;\n\tresult.radiobuttonActive\n\t\t= result.radiobuttonInactive\n\t\t= st::msgServiceFg[instance.palette]->c;\n\treturn result;\n}\n\n[[nodiscard]] CloudListColors ColorsFromCurrentTheme() {\n\tauto result = CloudListColors();\n\tauto background = Background()->createCurrentImage();\n\tresult.background = ColorsBackgroundFromImage(background);\n\tresult.sent = st::msgOutBg->c;\n\tresult.received = st::msgInBg->c;\n\tresult.radiobuttonActive\n\t\t= result.radiobuttonInactive\n\t\t= st::msgServiceFg->c;\n\treturn result;\n}\n\n} // namespace\n\nCloudListColors ColorsFromScheme(const EmbeddedScheme &scheme) {\n\tauto result = CloudListColors();\n\tresult.sent = scheme.sent;\n\tresult.received = scheme.received;\n\tresult.radiobuttonActive = scheme.radiobuttonActive;\n\tresult.radiobuttonInactive = scheme.radiobuttonInactive;\n\tresult.background = QImage(\n\t\tQSize(1, 1) * style::DevicePixelRatio(),\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tresult.background.fill(scheme.background);\n\treturn result;\n}\n\nCloudListColors ColorsFromScheme(\n\t\tconst EmbeddedScheme &scheme,\n\t\tconst style::colorizer &colorizer) {\n\tif (!colorizer) {\n\t\treturn ColorsFromScheme(scheme);\n\t}\n\tauto copy = scheme;\n\tColorize(copy, colorizer);\n\treturn ColorsFromScheme(copy);\n}\n\nCloudListCheck::CloudListCheck(const Colors &colors, bool checked)\n: CloudListCheck(checked) {\n\tsetColors(colors);\n}\n\nCloudListCheck::CloudListCheck(bool checked)\n: AbstractCheckView(st::defaultRadio.duration, checked, nullptr)\n, _radio(st::defaultRadio, checked, [=] { update(); }) {\n}\n\nvoid CloudListCheck::setColors(const Colors &colors) {\n\t_colors = colors;\n\tif (!_colors->background.isNull()) {\n\t\tconst auto size = st::settingsThemePreviewSize\n\t\t\t* style::DevicePixelRatio();\n\t\t_backgroundFull = (_colors->background.size() == size)\n\t\t\t? _colors->background\n\t\t\t: _colors->background.scaled(\n\t\t\t\tsize,\n\t\t\t\tQt::IgnoreAspectRatio,\n\t\t\t\tQt::SmoothTransformation);\n\t\t_backgroundCacheWidth = -1;\n\n\t\tensureContrast();\n\t\t_radio.setToggledOverride(_colors->radiobuttonActive);\n\t\t_radio.setUntoggledOverride(_colors->radiobuttonInactive);\n\t}\n\tupdate();\n}\n\nvoid CloudListCheck::ensureContrast() {\n\tconst auto radio = _radio.getSize();\n\tconst auto x = (getSize().width() - radio.width()) / 2;\n\tconst auto y = getSize().height()\n\t\t- radio.height()\n\t\t- st::settingsThemeRadioBottom;\n\tconst auto under = QRect(\n\t\tQPoint(x, y) * style::DevicePixelRatio(),\n\t\tradio * style::DevicePixelRatio());\n\tconst auto image = _backgroundFull.copy(under).convertToFormat(\n\t\tQImage::Format_ARGB32_Premultiplied);\n\tconst auto active = style::internal::EnsureContrast(\n\t\t_colors->radiobuttonActive,\n\t\tUi::CountAverageColor(image));\n\t_colors->radiobuttonInactive = _colors->radiobuttonActive = QColor(\n\t\tactive.red(),\n\t\tactive.green(),\n\t\tactive.blue(),\n\t\t255);\n\t_colors->radiobuttonInactive.setAlpha(192);\n}\n\nQSize CloudListCheck::getSize() const {\n\treturn st::settingsThemePreviewSize;\n}\n\nvoid CloudListCheck::validateBackgroundCache(int width) {\n\tif (_backgroundCacheWidth == width || width <= 0) {\n\t\treturn;\n\t}\n\t_backgroundCacheWidth = width;\n\tconst auto imageWidth = width * style::DevicePixelRatio();\n\t_backgroundCache = (width == st::settingsThemePreviewSize.width())\n\t\t? _backgroundFull\n\t\t: _backgroundFull.copy(\n\t\t\t(_backgroundFull.width() - imageWidth) / 2,\n\t\t\t0,\n\t\t\timageWidth,\n\t\t\t_backgroundFull.height());\n\t_backgroundCache = Images::Round(\n\t\tstd::move(_backgroundCache),\n\t\tImageRoundRadius::Large);\n\t_backgroundCache.setDevicePixelRatio(style::DevicePixelRatio());\n}\n\nvoid CloudListCheck::paint(QPainter &p, int left, int top, int outerWidth) {\n\tif (!_colors) {\n\t\treturn;\n\t} else if (_colors->background.isNull()) {\n\t\tpaintNotSupported(p, left, top, outerWidth);\n\t} else {\n\t\tpaintWithColors(p, left, top, outerWidth);\n\t}\n}\n\nvoid CloudListCheck::paintNotSupported(\n\t\tQPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint outerWidth) {\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(st::settingsThemeNotSupportedBg);\n\n\tconst auto height = st::settingsThemePreviewSize.height();\n\tconst auto rect = QRect(0, 0, outerWidth, height);\n\tconst auto radius = st::roundRadiusLarge;\n\tp.drawRoundedRect(rect, radius, radius);\n\tst::settingsThemeNotSupportedIcon.paintInCenter(p, rect);\n}\n\nvoid CloudListCheck::paintWithColors(\n\t\tQPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint outerWidth) {\n\tExpects(_colors.has_value());\n\n\tvalidateBackgroundCache(outerWidth);\n\tp.drawImage(\n\t\tQRect(0, 0, outerWidth, st::settingsThemePreviewSize.height()),\n\t\t_backgroundCache);\n\n\tconst auto received = QRect(\n\t\tst::settingsThemeBubblePosition,\n\t\tst::settingsThemeBubbleSize);\n\tconst auto sent = QRect(\n\t\touterWidth - received.width() - st::settingsThemeBubblePosition.x(),\n\t\treceived.y() + received.height() + st::settingsThemeBubbleSkip,\n\t\treceived.width(),\n\t\treceived.height());\n\tconst auto radius = st::settingsThemeBubbleRadius;\n\n\tPainterHighQualityEnabler hq(p);\n\tp.setPen(Qt::NoPen);\n\n\tp.setBrush(_colors->received);\n\tp.drawRoundedRect(style::rtlrect(received, outerWidth), radius, radius);\n\tp.setBrush(_colors->sent);\n\tp.drawRoundedRect(style::rtlrect(sent, outerWidth), radius, radius);\n\n\tconst auto radio = _radio.getSize();\n\t_radio.paint(\n\t\tp,\n\t\t(outerWidth - radio.width()) / 2,\n\t\tgetSize().height() - radio.height() - st::settingsThemeRadioBottom,\n\t\touterWidth);\n}\n\nQImage CloudListCheck::prepareRippleMask() const {\n\treturn QImage();\n}\n\nbool CloudListCheck::checkRippleStartPosition(QPoint position) const {\n\treturn false;\n}\n\nvoid CloudListCheck::checkedChangedHook(anim::type animated) {\n\t_radio.setChecked(checked(), animated);\n}\n\nCloudList::CloudList(\n\tnot_null<QWidget*> parent,\n\tnot_null<Window::SessionController*> window)\n: _window(window)\n, _owned(parent)\n, _outer(_owned.data())\n, _group(std::make_shared<Ui::RadiobuttonGroup>()) {\n\tsetup();\n}\n\nvoid CloudList::showAll() {\n\t_showAll = true;\n}\n\nobject_ptr<Ui::RpWidget> CloudList::takeWidget() {\n\treturn std::move(_owned);\n}\n\nrpl::producer<bool> CloudList::empty() const {\n\tusing namespace rpl::mappers;\n\n\treturn _count.value() | rpl::map(_1 == 0);\n}\n\nrpl::producer<bool> CloudList::allShown() const {\n\tusing namespace rpl::mappers;\n\n\treturn rpl::combine(\n\t\t_showAll.value(),\n\t\t_count.value(),\n\t\t_1 || (_2 <= kShowPerRow));\n}\n\nvoid CloudList::setup() {\n\t_group->setChangedCallback([=](int selected) {\n\t\tconst auto &object = Background()->themeObject();\n\t\t_group->setValue(groupValueForId(\n\t\t\tobject.cloud.id ? object.cloud.id : kFakeCloudThemeId));\n\t});\n\n\tauto cloudListChanges = rpl::single(rpl::empty) | rpl::then(\n\t\t_window->session().data().cloudThemes().updated()\n\t);\n\n\tauto themeChanges = rpl::single(BackgroundUpdate(\n\t\tBackgroundUpdate::Type::ApplyingTheme,\n\t\tBackground()->tile()\n\t)) | rpl::then(\n\t\tBackground()->updates()\n\t) | rpl::filter([](const BackgroundUpdate &update) {\n\t\treturn (update.type == BackgroundUpdate::Type::ApplyingTheme);\n\t});\n\n\trpl::combine(\n\t\tstd::move(cloudListChanges),\n\t\tstd::move(themeChanges),\n\t\tallShown()\n\t) | rpl::map([=] {\n\t\treturn collectAll();\n\t}) | rpl::on_next([=](std::vector<Data::CloudTheme> &&list) {\n\t\trebuildUsing(std::move(list));\n\t}, _outer->lifetime());\n\n\t_outer->widthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tupdateGeometry();\n\t}, _outer->lifetime());\n}\n\nstd::vector<Data::CloudTheme> CloudList::collectAll() const {\n\tconst auto &object = Background()->themeObject();\n\tconst auto isDefault = IsEmbeddedTheme(object.pathAbsolute);\n\tauto result = _window->session().data().cloudThemes().list();\n\tif (!isDefault) {\n\t\tconst auto i = ranges::find(\n\t\t\tresult,\n\t\t\tobject.cloud.id,\n\t\t\t&Data::CloudTheme::id);\n\t\tif (i == end(result)) {\n\t\t\tif (object.cloud.id) {\n\t\t\t\tresult.push_back(object.cloud);\n\t\t\t} else {\n\t\t\t\tresult.push_back(FakeCloudTheme(object));\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid CloudList::rebuildUsing(std::vector<Data::CloudTheme> &&list) {\n\tconst auto fullCount = int(list.size());\n\tconst auto changed = applyChangesFrom(std::move(list));\n\t_count = fullCount;\n\tif (changed) {\n\t\tupdateGeometry();\n\t}\n}\n\nbool CloudList::applyChangesFrom(std::vector<Data::CloudTheme> &&list) {\n\tif (list.empty()) {\n\t\tif (_elements.empty()) {\n\t\t\treturn false;\n\t\t}\n\t\t_elements.clear();\n\t\treturn true;\n\t}\n\tauto changed = false;\n\tconst auto limit = _showAll.current() ? list.size() : kShowPerRow;\n\tconst auto &object = Background()->themeObject();\n\tconst auto id = object.cloud.id ? object.cloud.id : kFakeCloudThemeId;\n\tranges::stable_sort(list, std::less<>(), [&](const Data::CloudTheme &t) {\n\t\tif (t.id == id) {\n\t\t\treturn 0;\n\t\t} else if (t.documentId) {\n\t\t\treturn 1;\n\t\t} else {\n\t\t\treturn 2;\n\t\t}\n\t});\n\tif (list.front().id == id) {\n\t\tconst auto j = ranges::find(_elements, id, &Element::id);\n\t\tif (j == end(_elements)) {\n\t\t\tinsert(0, list.front());\n\t\t\tchanged = true;\n\t\t} else if (j - begin(_elements) >= limit) {\n\t\t\tstd::rotate(\n\t\t\t\tbegin(_elements) + limit - 1,\n\t\t\t\tj,\n\t\t\t\tj + 1);\n\t\t\tchanged = true;\n\t\t}\n\t}\n\tif (removeStaleUsing(list)) {\n\t\tchanged = true;\n\t}\n\tif (insertTillLimit(list, limit)) {\n\t\tchanged = true;\n\t}\n\t_group->setValue(groupValueForId(id));\n\treturn changed;\n}\n\nbool CloudList::removeStaleUsing(const std::vector<Data::CloudTheme> &list) {\n\tconst auto check = [&](Element &element) {\n\t\tconst auto j = ranges::find(\n\t\t\tlist,\n\t\t\telement.theme.id,\n\t\t\t&Data::CloudTheme::id);\n\t\tif (j == end(list)) {\n\t\t\treturn true;\n\t\t}\n\t\trefreshElementUsing(element, *j);\n\t\treturn false;\n\t};\n\tconst auto from = ranges::remove_if(_elements, check);\n\tif (from == end(_elements)) {\n\t\treturn false;\n\t}\n\t_elements.erase(from, end(_elements));\n\treturn true;\n}\n\nbool CloudList::insertTillLimit(\n\t\tconst std::vector<Data::CloudTheme> &list,\n\t\tint limit) {\n\tconst auto insertCount = (limit - int(_elements.size()));\n\tif (insertCount < 0) {\n\t\t_elements.erase(end(_elements) + insertCount, end(_elements));\n\t\treturn true;\n\t} else if (!insertCount) {\n\t\treturn false;\n\t}\n\tconst auto isGood = [](const Data::CloudTheme &theme) {\n\t\treturn (theme.documentId != 0);\n\t};\n\tauto positionForGood = ranges::find_if(_elements, [&](const Element &e) {\n\t\treturn !isGood(e.theme);\n\t}) - begin(_elements);\n\tauto positionForBad = end(_elements) - begin(_elements);\n\n\tauto insertElements = ranges::views::all(\n\t\tlist\n\t) | ranges::views::filter([&](const Data::CloudTheme &theme) {\n\t\tconst auto i = ranges::find(_elements, theme.id, &Element::id);\n\t\treturn (i == end(_elements));\n\t}) | ranges::views::take(insertCount);\n\n\tfor (const auto &theme : insertElements) {\n\t\tconst auto good = isGood(theme);\n\t\tinsert(good ? positionForGood : positionForBad, theme);\n\t\tif (good) {\n\t\t\t++positionForGood;\n\t\t}\n\t\t++positionForBad;\n\t}\n\treturn true;\n}\n\nvoid CloudList::insert(int index, const Data::CloudTheme &theme) {\n\tconst auto id = theme.id;\n\tconst auto value = groupValueForId(id);\n\tconst auto checked = _group->hasValue() && (_group->current() == value);\n\tauto check = std::make_unique<CloudListCheck>(checked);\n\tconst auto raw = check.get();\n\tauto button = std::make_unique<Ui::Radiobutton>(\n\t\t_outer,\n\t\t_group,\n\t\tvalue,\n\t\ttheme.title,\n\t\tst::settingsTheme,\n\t\tstd::move(check));\n\tbutton->setCheckAlignment(style::al_top);\n\tbutton->setAllowTextLines(2);\n\tbutton->setTextBreakEverywhere();\n\tbutton->show();\n\tbutton->setAcceptBoth(true);\n\tbutton->addClickHandler([=](Qt::MouseButton button) {\n\t\tconst auto i = ranges::find(_elements, id, &Element::id);\n\t\tif (i == end(_elements)\n\t\t\t|| id == kFakeCloudThemeId\n\t\t\t|| i->waiting) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto &cloud = i->theme;\n\t\tif (button == Qt::RightButton) {\n\t\t\tshowMenu(*i);\n\t\t} else if (cloud.documentId) {\n\t\t\t_window->session().data().cloudThemes().applyFromDocument(cloud);\n\t\t} else {\n\t\t\t_window->session().data().cloudThemes().showPreview(\n\t\t\t\t&_window->window(),\n\t\t\t\tcloud);\n\t\t}\n\t});\n\tauto &element = *_elements.insert(\n\t\tbegin(_elements) + index,\n\t\tElement{ theme, raw, std::move(button) });\n\trefreshColors(element);\n}\n\nvoid CloudList::refreshElementUsing(\n\t\tElement &element,\n\t\tconst Data::CloudTheme &data) {\n\tconst auto colorsChanged = (element.theme.documentId != data.documentId)\n\t\t|| ((element.id() == kFakeCloudThemeId)\n\t\t\t&& (element.theme.slug != data.slug));\n\tconst auto titleChanged = (element.theme.title != data.title);\n\telement.theme = data;\n\tif (colorsChanged) {\n\t\tsetWaiting(element, false);\n\t\trefreshColors(element);\n\t}\n\tif (titleChanged) {\n\t\telement.button->setText(data.title);\n\t}\n}\n\nvoid CloudList::refreshColors(Element &element) {\n\tconst auto currentId = Background()->themeObject().cloud.id;\n\tconst auto &theme = element.theme;\n\tconst auto document = theme.documentId\n\t\t? _window->session().data().document(theme.documentId).get()\n\t\t: nullptr;\n\tif (element.id() == kFakeCloudThemeId\n\t\t|| ((element.id() == currentId)\n\t\t\t&& (!document || !document->isTheme()))) {\n\t\telement.check->setColors(ColorsFromCurrentTheme());\n\t} else if (document) {\n\t\telement.media = document ? document->createMediaView() : nullptr;\n\t\tdocument->save(\n\t\t\tData::FileOriginTheme(theme.id, theme.accessHash),\n\t\t\tQString());\n\t\tif (element.media->loaded()) {\n\t\t\trefreshColorsFromDocument(element);\n\t\t} else {\n\t\t\tsetWaiting(element, true);\n\t\t\tsubscribeToDownloadFinished();\n\t\t}\n\t} else {\n\t\telement.check->setColors(CloudListColors());\n\t}\n}\n\nvoid CloudList::showMenu(Element &element) {\n\tif (_contextMenu) {\n\t\t_contextMenu = nullptr;\n\t\treturn;\n\t}\n\t_contextMenu = base::make_unique_q<Ui::PopupMenu>(\n\t\telement.button.get(),\n\t\tst::popupMenuWithIcons);\n\tconst auto cloud = element.theme;\n\tif (const auto slug = element.theme.slug; !slug.isEmpty()) {\n\t\t_contextMenu->addAction(tr::lng_theme_share(tr::now), [=] {\n\t\t\tQGuiApplication::clipboard()->setText(\n\t\t\t\t_window->session().createInternalLinkFull(\"addtheme/\" + slug));\n\t\t\t_window->window().showToast(\n\t\t\t\ttr::lng_background_link_copied(tr::now));\n\t\t}, &st::menuIconShare);\n\t}\n\tif (cloud.documentId\n\t\t&& cloud.createdBy == _window->session().userId()\n\t\t&& Background()->themeObject().cloud.id == cloud.id) {\n\t\t_contextMenu->addAction(tr::lng_theme_edit(tr::now), [=] {\n\t\t\tStartEditor(&_window->window(), cloud);\n\t\t}, &st::menuIconChangeColors);\n\t}\n\tconst auto id = cloud.id;\n\t_contextMenu->addAction(tr::lng_theme_delete(tr::now), [=] {\n\t\tconst auto remove = [=](Fn<void()> &&close) {\n\t\t\tclose();\n\t\t\tif (Background()->themeObject().cloud.id == id\n\t\t\t\t|| id == kFakeCloudThemeId) {\n\t\t\t\tif (Background()->editingTheme().has_value()) {\n\t\t\t\t\tBackground()->clearEditingTheme(\n\t\t\t\t\t\tClearEditing::KeepChanges);\n\t\t\t\t\t_window->window().showRightColumn(nullptr);\n\t\t\t\t}\n\t\t\t\tResetToSomeDefault();\n\t\t\t\tKeepApplied();\n\t\t\t}\n\t\t\tif (id != kFakeCloudThemeId) {\n\t\t\t\t_window->session().data().cloudThemes().remove(id);\n\t\t\t}\n\t\t};\n\t\t_window->window().show(Ui::MakeConfirmBox({\n\t\t\t.text = tr::lng_theme_delete_sure(),\n\t\t\t.confirmed = remove,\n\t\t\t.confirmText = tr::lng_theme_delete(),\n\t\t}));\n\t}, &st::menuIconDelete);\n\t_contextMenu->popup(QCursor::pos());\n}\n\nvoid CloudList::setWaiting(Element &element, bool waiting) {\n\telement.waiting = waiting;\n\telement.button->setPointerCursor(\n\t\t!waiting && (element.theme.documentId || amCreator(element.theme)));\n}\n\nbool CloudList::amCreator(const Data::CloudTheme &theme) const {\n\treturn (_window->session().userId() == theme.createdBy);\n}\n\nvoid CloudList::refreshColorsFromDocument(Element &element) {\n\tExpects(element.media != nullptr);\n\tExpects(element.media->loaded());\n\n\tconst auto id = element.id();\n\tconst auto path = element.media->owner()->filepath();\n\tconst auto data = base::take(element.media)->bytes();\n\tcrl::async([=, guard = element.generating.make_guard()]() mutable {\n\t\tcrl::on_main(std::move(guard), [\n\t\t\t=,\n\t\t\tresult = ColorsFromTheme(path, data)\n\t\t]() mutable {\n\t\t\tconst auto i = ranges::find(_elements, id, &Element::id);\n\t\t\tif (i == end(_elements) || !result) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto &element = *i;\n\t\t\tif (result->background.isNull()) {\n\t\t\t\tresult->background = ColorsFromCurrentTheme().background;\n\t\t\t}\n\t\t\telement.check->setColors(*result);\n\t\t\tsetWaiting(element, false);\n\t\t});\n\t});\n}\n\nvoid CloudList::subscribeToDownloadFinished() {\n\tif (_downloadFinishedLifetime) {\n\t\treturn;\n\t}\n\t_window->session().downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tauto &&waiting = _elements | ranges::views::filter(&Element::waiting);\n\t\tconst auto still = ranges::count_if(waiting, [&](Element &element) {\n\t\t\tif (!element.media) {\n\t\t\t\telement.waiting = false;\n\t\t\t\treturn false;\n\t\t\t} else if (!element.media->loaded()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\trefreshColorsFromDocument(element);\n\t\t\telement.waiting = false;\n\t\t\treturn false;\n\t\t});\n\t\tif (!still) {\n\t\t\t_downloadFinishedLifetime.destroy();\n\t\t}\n\t}, _downloadFinishedLifetime);\n}\n\nint CloudList::groupValueForId(uint64 id) {\n\tconst auto i = _groupValueById.find(id);\n\tif (i != end(_groupValueById)) {\n\t\treturn i->second;\n\t}\n\tconst auto result = int(_idByGroupValue.size());\n\t_groupValueById.emplace(id, result);\n\t_idByGroupValue.push_back(id);\n\treturn result;\n}\n\nvoid CloudList::updateGeometry() {\n\tconst auto width = _outer->width();\n\tif (!width) {\n\t\treturn;\n\t}\n\tconst auto height = resizeGetHeight(width);\n\tif (height != _outer->height()) {\n\t\t_outer->resize(width, height);\n\t}\n}\n\nint CloudList::resizeGetHeight(int newWidth) {\n\tconst auto minSkip = st::settingsThemeMinSkip;\n\tconst auto single = std::min(\n\t\tst::settingsThemePreviewSize.width(),\n\t\t(newWidth - minSkip * (kShowPerRow - 1)) / kShowPerRow);\n\tconst auto skip = (newWidth - kShowPerRow * single)\n\t\t/ float64(kShowPerRow - 1);\n\n\tauto x = 0.;\n\tauto y = 0;\n\n\tauto index = 0;\n\tauto rowHeight = 0;\n\tfor (const auto &element : _elements) {\n\t\tconst auto button = element.button.get();\n\t\tbutton->resizeToWidth(single);\n\t\tbutton->moveToLeft(int(base::SafeRound(x)), y);\n\t\taccumulate_max(rowHeight, button->height());\n\t\tx += single + skip;\n\t\tif (++index == kShowPerRow) {\n\t\t\tx = 0.;\n\t\t\tindex = 0;\n\t\t\ty += rowHeight + st::themesSmallSkip;\n\t\t\trowHeight = 0;\n\t\t}\n\t}\n\treturn rowHeight\n\t\t? (y + rowHeight)\n\t\t: (y > 0)\n\t\t? (y - st::themesSmallSkip)\n\t\t: 0;\n}\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_themes_cloud_list.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"data/data_cloud_themes.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"base/unique_qptr.h\"\n#include \"base/binary_guard.h\"\n\nnamespace style {\nstruct colorizer;\n} // namespace style\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Window {\n\nclass SessionController;\n\nnamespace Theme {\n\nstruct EmbeddedScheme;\n\nstruct CloudListColors {\n\tQImage background;\n\tQColor sent;\n\tQColor received;\n\tQColor radiobuttonInactive;\n\tQColor radiobuttonActive;\n};\n\n[[nodiscard]] CloudListColors ColorsFromScheme(const EmbeddedScheme &scheme);\n[[nodiscard]] CloudListColors ColorsFromScheme(\n\tconst EmbeddedScheme &scheme,\n\tconst style::colorizer &colorizer);\n\nclass CloudListCheck final : public Ui::AbstractCheckView {\npublic:\n\tusing Colors = CloudListColors;\n\n\texplicit CloudListCheck(bool checked);\n\tCloudListCheck(const Colors &colors, bool checked);\n\n\tQSize getSize() const override;\n\tvoid paint(\n\t\tQPainter &p,\n\t\tint left,\n\t\tint top,\n\t\tint outerWidth) override;\n\tQImage prepareRippleMask() const override;\n\tbool checkRippleStartPosition(QPoint position) const override;\n\n\tvoid setColors(const Colors &colors);\n\nprivate:\n\tvoid paintNotSupported(QPainter &p, int left, int top, int outerWidth);\n\tvoid paintWithColors(QPainter &p, int left, int top, int outerWidth);\n\tvoid checkedChangedHook(anim::type animated) override;\n\tvoid validateBackgroundCache(int width);\n\tvoid ensureContrast();\n\n\tstd::optional<Colors> _colors;\n\tUi::RadioView _radio;\n\tQImage _backgroundFull;\n\tQImage _backgroundCache;\n\tint _backgroundCacheWidth = -1;\n\n};\n\nclass CloudList final {\npublic:\n\tCloudList(\n\t\tnot_null<QWidget*> parent,\n\t\tnot_null<Window::SessionController*> window);\n\n\tvoid showAll();\n\t[[nodiscard]] rpl::producer<bool> empty() const;\n\t[[nodiscard]] rpl::producer<bool> allShown() const;\n\t[[nodiscard]] object_ptr<Ui::RpWidget> takeWidget();\n\nprivate:\n\tstruct Element {\n\t\tData::CloudTheme theme;\n\t\tnot_null<CloudListCheck*> check;\n\t\tstd::unique_ptr<Ui::Radiobutton> button;\n\t\tstd::shared_ptr<Data::DocumentMedia> media;\n\t\tbase::binary_guard generating;\n\t\tbool waiting = false;\n\n\t\tuint64 id() const {\n\t\t\treturn theme.id;\n\t\t}\n\t};\n\tvoid setup();\n\t[[nodiscard]] std::vector<Data::CloudTheme> collectAll() const;\n\tvoid rebuildUsing(std::vector<Data::CloudTheme> &&list);\n\tbool applyChangesFrom(std::vector<Data::CloudTheme> &&list);\n\tbool removeStaleUsing(const std::vector<Data::CloudTheme> &list);\n\tbool insertTillLimit(\n\t\tconst std::vector<Data::CloudTheme> &list,\n\t\tint limit);\n\tvoid refreshElementUsing(Element &element, const Data::CloudTheme &data);\n\tvoid insert(int index, const Data::CloudTheme &theme);\n\tvoid refreshColors(Element &element);\n\tvoid showMenu(Element &element);\n\tvoid refreshColorsFromDocument(Element &element);\n\tvoid setWaiting(Element &element, bool waiting);\n\tvoid subscribeToDownloadFinished();\n\tint resizeGetHeight(int newWidth);\n\tvoid updateGeometry();\n\n\t[[nodiscard]] bool amCreator(const Data::CloudTheme &theme) const;\n\t[[nodiscard]] int groupValueForId(uint64 id);\n\n\tconst not_null<Window::SessionController*> _window;\n\tobject_ptr<Ui::RpWidget> _owned;\n\tconst not_null<Ui::RpWidget*> _outer;\n\tconst std::shared_ptr<Ui::RadiobuttonGroup> _group;\n\trpl::variable<bool> _showAll = false;\n\trpl::variable<int> _count = 0;\n\tstd::vector<Element> _elements;\n\tstd::vector<uint64> _idByGroupValue;\n\tbase::flat_map<uint64, int> _groupValueById;\n\trpl::lifetime _downloadFinishedLifetime;\n\tbase::unique_qptr<Ui::PopupMenu> _contextMenu;\n\n};\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_themes_embedded.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/themes/window_themes_embedded.h\"\n\n#include \"window/themes/window_theme.h\"\n#include \"lang/lang_keys.h\"\n#include \"storage/serialize_common.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"ui/style/style_palette_colorizer.h\"\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QPalette>\n\nnamespace Window {\nnamespace Theme {\nnamespace {\n\nconstexpr auto kMaxAccentColors = 3;\nconstexpr auto kDayBaseFile = \":/gui/day-custom-base.tdesktop-theme\"_cs;\nconstexpr auto kNightBaseFile = \":/gui/night-custom-base.tdesktop-theme\"_cs;\n\nconst auto kColorizeIgnoredKeys = base::flat_set<QLatin1String>{ {\n\tqstr(\"boxTextFgGood\"),\n\tqstr(\"boxTextFgError\"),\n\tqstr(\"callIconFg\"),\n\tqstr(\"historyPeer1NameFg\"),\n\tqstr(\"historyPeer1NameFgSelected\"),\n\tqstr(\"historyPeer1UserpicBg\"),\n\tqstr(\"historyPeer2NameFg\"),\n\tqstr(\"historyPeer2NameFgSelected\"),\n\tqstr(\"historyPeer2UserpicBg\"),\n\tqstr(\"historyPeer3NameFg\"),\n\tqstr(\"historyPeer3NameFgSelected\"),\n\tqstr(\"historyPeer3UserpicBg\"),\n\tqstr(\"historyPeer4NameFg\"),\n\tqstr(\"historyPeer4NameFgSelected\"),\n\tqstr(\"historyPeer4UserpicBg\"),\n\tqstr(\"historyPeer5NameFg\"),\n\tqstr(\"historyPeer5NameFgSelected\"),\n\tqstr(\"historyPeer5UserpicBg\"),\n\tqstr(\"historyPeer6NameFg\"),\n\tqstr(\"historyPeer6NameFgSelected\"),\n\tqstr(\"historyPeer6UserpicBg\"),\n\tqstr(\"historyPeer7NameFg\"),\n\tqstr(\"historyPeer7NameFgSelected\"),\n\tqstr(\"historyPeer7UserpicBg\"),\n\tqstr(\"historyPeer8NameFg\"),\n\tqstr(\"historyPeer8NameFgSelected\"),\n\tqstr(\"historyPeer8UserpicBg\"),\n\tqstr(\"historyPeer1UserpicBg2\"),\n\tqstr(\"historyPeer2UserpicBg2\"),\n\tqstr(\"historyPeer3UserpicBg2\"),\n\tqstr(\"historyPeer4UserpicBg2\"),\n\tqstr(\"historyPeer5UserpicBg2\"),\n\tqstr(\"historyPeer6UserpicBg2\"),\n\tqstr(\"historyPeer7UserpicBg2\"),\n\tqstr(\"historyPeer8UserpicBg2\"),\n\tqstr(\"msgFile1Bg\"),\n\tqstr(\"msgFile1BgDark\"),\n\tqstr(\"msgFile1BgOver\"),\n\tqstr(\"msgFile1BgSelected\"),\n\tqstr(\"msgFile2Bg\"),\n\tqstr(\"msgFile2BgDark\"),\n\tqstr(\"msgFile2BgOver\"),\n\tqstr(\"msgFile2BgSelected\"),\n\tqstr(\"msgFile3Bg\"),\n\tqstr(\"msgFile3BgDark\"),\n\tqstr(\"msgFile3BgOver\"),\n\tqstr(\"msgFile3BgSelected\"),\n\tqstr(\"msgFile4Bg\"),\n\tqstr(\"msgFile4BgDark\"),\n\tqstr(\"msgFile4BgOver\"),\n\tqstr(\"msgFile4BgSelected\"),\n\tqstr(\"mediaviewFileRedCornerFg\"),\n\tqstr(\"mediaviewFileYellowCornerFg\"),\n\tqstr(\"mediaviewFileGreenCornerFg\"),\n\tqstr(\"mediaviewFileBlueCornerFg\"),\n\tqstr(\"settingsIconBg1\"),\n\tqstr(\"settingsIconBg2\"),\n\tqstr(\"settingsIconBg3\"),\n\tqstr(\"settingsIconBg4\"),\n\tqstr(\"settingsIconBg5\"),\n\tqstr(\"settingsIconBg6\"),\n\tqstr(\"settingsIconBg8\"),\n\tqstr(\"settingsIconBgArchive\"),\n\tqstr(\"premiumButtonBg1\"),\n\tqstr(\"premiumButtonBg2\"),\n\tqstr(\"premiumButtonBg3\"),\n\tqstr(\"premiumIconBg1\"),\n\tqstr(\"premiumIconBg2\"),\n} };\n\nstyle::colorizer::Color cColor(std::string_view hex) {\n\tconst auto q = style::ColorFromHex(hex);\n\tauto hue = int();\n\tauto saturation = int();\n\tauto value = int();\n\tq.getHsv(&hue, &saturation, &value);\n\treturn style::colorizer::Color{ hue, saturation, value };\n}\n\n} // namespace\n\nstyle::colorizer ColorizerFrom(\n\t\tconst EmbeddedScheme &scheme,\n\t\tconst QColor &color) {\n\tusing Color = style::colorizer::Color;\n\tusing Pair = std::pair<Color, Color>;\n\n\tauto result = style::colorizer();\n\tresult.ignoreKeys = kColorizeIgnoredKeys;\n\tresult.hueThreshold = 15;\n\tscheme.accentColor.getHsv(\n\t\t&result.was.hue,\n\t\t&result.was.saturation,\n\t\t&result.was.value);\n\tcolor.getHsv(\n\t\t&result.now.hue,\n\t\t&result.now.saturation,\n\t\t&result.now.value);\n\tswitch (scheme.type) {\n\tcase EmbeddedType::Default:\n\t\tresult.lightnessMax = 160;\n\t\tbreak;\n\tcase EmbeddedType::DayBlue:\n\t\tresult.lightnessMax = 160;\n\t\tbreak;\n\tcase EmbeddedType::Night:\n\t\tresult.keepContrast = base::flat_map<QLatin1String, Pair>{ {\n\t\t\t//{ qstr(\"windowFgActive\"), Pair{ cColor(\"5288c1\"), cColor(\"17212b\") } }, // windowBgActive\n\t\t\t{ qstr(\"activeButtonFg\"), Pair{ cColor(\"2f6ea5\"), cColor(\"17212b\") } }, // activeButtonBg\n\t\t\t{ qstr(\"profileVerifiedCheckFg\"), Pair{ cColor(\"5288c1\"), cColor(\"17212b\") } }, // profileVerifiedCheckBg\n\t\t\t{ qstr(\"overviewCheckFgActive\"), Pair{ cColor(\"5288c1\"), cColor(\"17212b\") } }, // overviewCheckBgActive\n\t\t\t{ qstr(\"historyFileInIconFg\"), Pair{ cColor(\"3f96d0\"), cColor(\"182533\") } }, // msgFileInBg, msgInBg\n\t\t\t{ qstr(\"historyFileInIconFgSelected\"), Pair{ cColor(\"6ab4f4\"), cColor(\"2e70a5\") } }, // msgFileInBgSelected, msgInBgSelected\n\t\t\t{ qstr(\"historyFileInRadialFg\"), Pair{ cColor(\"3f96d0\"), cColor(\"182533\") } }, // msgFileInBg, msgInBg\n\t\t\t{ qstr(\"historyFileInRadialFgSelected\"), Pair{ cColor(\"6ab4f4\"), cColor(\"2e70a5\") } }, // msgFileInBgSelected, msgInBgSelected\n\t\t\t{ qstr(\"historyFileOutIconFg\"), Pair{ cColor(\"4c9ce2\"), cColor(\"2b5278\") } }, // msgFileOutBg, msgOutBg\n\t\t\t{ qstr(\"historyFileOutIconFgSelected\"), Pair{ cColor(\"58abf3\"), cColor(\"2e70a5\") } }, // msgFileOutBgSelected, msgOutBgSelected\n\t\t\t{ qstr(\"historyFileOutRadialFg\"), Pair{ cColor(\"4c9ce2\"), cColor(\"2b5278\") } }, // msgFileOutBg, msgOutBg\n\t\t\t{ qstr(\"historyFileOutRadialFgSelected\"), Pair{ cColor(\"58abf3\"), cColor(\"2e70a5\") } }, // msgFileOutBgSelected, msgOutBgSelected\n\t\t} };\n\t\tresult.lightnessMin = 64;\n\t\tbreak;\n\tcase EmbeddedType::NightGreen:\n\t\tresult.keepContrast = base::flat_map<QLatin1String, Pair>{ {\n\t\t\t//{ qstr(\"windowFgActive\"), Pair{ cColor(\"3fc1b0\"), cColor(\"282e33\") } }, // windowBgActive, windowBg\n\t\t\t{ qstr(\"activeButtonFg\"), Pair{ cColor(\"2da192\"), cColor(\"282e33\") } }, // activeButtonBg, windowBg\n\t\t\t{ qstr(\"profileVerifiedCheckFg\"), Pair{ cColor(\"3fc1b0\"), cColor(\"282e33\") } }, // profileVerifiedCheckBg, windowBg\n\t\t\t{ qstr(\"overviewCheckFgActive\"), Pair{ cColor(\"3fc1b0\"), cColor(\"282e33\") } }, // overviewCheckBgActive\n\t\t\t// callIconFg is used not only over callAnswerBg,\n\t\t\t// so this contrast-forcing breaks other buttons.\n\t\t\t//{ qstr(\"callIconFg\"), Pair{ cColor(\"5ad1c1\"), cColor(\"1b1f23\") } }, // callAnswerBg, callBgOpaque\n\t\t} };\n\t\tresult.lightnessMin = 64;\n\t\tbreak;\n\t}\n\tconst auto nowLightness = color.lightness();\n\tconst auto limitedLightness = std::clamp(\n\t\tnowLightness,\n\t\tresult.lightnessMin,\n\t\tresult.lightnessMax);\n\tif (limitedLightness != nowLightness) {\n\t\tQColor::fromHsl(\n\t\t\tcolor.hslHue(),\n\t\t\tcolor.hslSaturation(),\n\t\t\tlimitedLightness).getHsv(\n\t\t\t\t&result.now.hue,\n\t\t\t\t&result.now.saturation,\n\t\t\t\t&result.now.value);\n\t}\n\treturn result;\n}\n\nstd::optional<QColor> SystemAccentColor() {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)\n\tconstexpr auto kAccentRole = QPalette::ColorRole::Accent;\n#else\n\tconstexpr auto kAccentRole = QPalette::ColorRole::Highlight;\n#endif\n\tconst auto accent = QGuiApplication::palette().color(kAccentRole);\n\treturn accent.isValid() ? std::make_optional(accent) : std::nullopt;\n}\n\nstyle::colorizer ColorizerForTheme(const QString &absolutePath) {\n\tif (!IsEmbeddedTheme(absolutePath)) {\n\t\treturn {};\n\t}\n\tconst auto schemes = EmbeddedThemes();\n\tconst auto i = ranges::find(\n\t\tschemes,\n\t\tabsolutePath,\n\t\t&EmbeddedScheme::path);\n\tif (i == end(schemes)) {\n\t\treturn {};\n\t}\n\tconst auto &settings = Core::App().settings();\n\tif (settings.systemAccentColorEnabled()) {\n\t\tif (const auto accent = SystemAccentColor()) {\n\t\t\treturn ColorizerFrom(*i, *accent);\n\t\t}\n\t}\n\tconst auto &colors = settings.themesAccentColors();\n\tif (const auto accent = colors.get(i->type)) {\n\t\treturn ColorizerFrom(*i, *accent);\n\t}\n\treturn {};\n}\n\nvoid Colorize(EmbeddedScheme &scheme, const style::colorizer &colorizer) {\n\tconst auto colors = {\n\t\t&EmbeddedScheme::background,\n\t\t&EmbeddedScheme::sent,\n\t\t&EmbeddedScheme::received,\n\t\t&EmbeddedScheme::radiobuttonActive,\n\t\t&EmbeddedScheme::radiobuttonInactive\n\t};\n\tfor (const auto color : colors) {\n\t\tif (const auto changed = style::colorize(scheme.*color, colorizer)) {\n\t\t\tscheme.*color = changed->toRgb();\n\t\t}\n\t}\n}\n\nstd::vector<EmbeddedScheme> EmbeddedThemes() {\n\tconst auto qColor = [](auto hex) {\n\t\treturn style::ColorFromHex(hex);\n\t};\n\tconst auto name = [](auto key) {\n\t\treturn rpl::deferred([=] { return key(); });\n\t};\n\treturn {\n\t\tEmbeddedScheme{\n\t\t\tEmbeddedType::Default,\n\t\t\tqColor(\"9bd494\"),\n\t\t\tqColor(\"eaffdc\"),\n\t\t\tqColor(\"ffffff\"),\n\t\t\tqColor(\"eaffdc\"),\n\t\t\tqColor(\"ffffff\"),\n\t\t\tname(tr::lng_settings_theme_classic),\n\t\t\tQString(),\n\t\t\tqColor(\"40a7e3\")\n\t\t},\n\t\tEmbeddedScheme{\n\t\t\tEmbeddedType::DayBlue,\n\t\t\tqColor(\"7ec4ea\"),\n\t\t\tqColor(\"d7f0ff\"),\n\t\t\tqColor(\"ffffff\"),\n\t\t\tqColor(\"d7f0ff\"),\n\t\t\tqColor(\"ffffff\"),\n\t\t\tname(tr::lng_settings_theme_day),\n\t\t\t\":/gui/day-blue.tdesktop-theme\",\n\t\t\tqColor(\"40a7e3\")\n\t\t},\n\t\tEmbeddedScheme{\n\t\t\tEmbeddedType::Night,\n\t\t\tqColor(\"485761\"),\n\t\t\tqColor(\"5ca7d4\"),\n\t\t\tqColor(\"6b808d\"),\n\t\t\tqColor(\"6b808d\"),\n\t\t\tqColor(\"5ca7d4\"),\n\t\t\tname(tr::lng_settings_theme_tinted),\n\t\t\t\":/gui/night.tdesktop-theme\",\n\t\t\tqColor(\"5288c1\")\n\t\t},\n\t\tEmbeddedScheme{\n\t\t\tEmbeddedType::NightGreen,\n\t\t\tqColor(\"485761\"),\n\t\t\tqColor(\"6b808d\"),\n\t\t\tqColor(\"6b808d\"),\n\t\t\tqColor(\"6b808d\"),\n\t\t\tqColor(\"75bfb5\"),\n\t\t\tname(tr::lng_settings_theme_night),\n\t\t\t\":/gui/night-green.tdesktop-theme\",\n\t\t\tqColor(\"3fc1b0\")\n\t\t},\n\t};\n}\n\nstd::vector<QColor> DefaultAccentColors(EmbeddedType type) {\n\tconst auto qColor = [](auto hex) {\n\t\treturn style::ColorFromHex(hex);\n\t};\n\tswitch (type) {\n\tcase EmbeddedType::DayBlue:\n\t\treturn {\n\t\t\tqColor(\"45bce7\"),\n\t\t\tqColor(\"52b440\"),\n\t\t\tqColor(\"d46c99\"),\n\t\t\tqColor(\"df8a49\"),\n\t\t\tqColor(\"9978c8\"),\n\t\t\tqColor(\"c55245\"),\n\t\t\tqColor(\"687b98\"),\n\t\t\tqColor(\"dea922\"),\n\t\t};\n\tcase EmbeddedType::Default:\n\t\treturn {\n\t\t\tqColor(\"45bce7\"),\n\t\t\tqColor(\"52b440\"),\n\t\t\tqColor(\"d46c99\"),\n\t\t\tqColor(\"df8a49\"),\n\t\t\tqColor(\"9978c8\"),\n\t\t\tqColor(\"c55245\"),\n\t\t\tqColor(\"687b98\"),\n\t\t\tqColor(\"dea922\"),\n\t\t};\n\tcase EmbeddedType::Night:\n\t\treturn {\n\t\t\tqColor(\"58bfe8\"),\n\t\t\tqColor(\"466f42\"),\n\t\t\tqColor(\"aa6084\"),\n\t\t\tqColor(\"a46d3c\"),\n\t\t\tqColor(\"917bbd\"),\n\t\t\tqColor(\"ab5149\"),\n\t\t\tqColor(\"697b97\"),\n\t\t\tqColor(\"9b834b\"),\n\t\t};\n\tcase EmbeddedType::NightGreen:\n\t\treturn {\n\t\t\tqColor(\"60a8e7\"),\n\t\t\tqColor(\"4e9c57\"),\n\t\t\tqColor(\"ca7896\"),\n\t\t\tqColor(\"cc925c\"),\n\t\t\tqColor(\"a58ed2\"),\n\t\t\tqColor(\"d27570\"),\n\t\t\tqColor(\"7b8799\"),\n\t\t\tqColor(\"cbac67\"),\n\t\t};\n\t}\n\tUnexpected(\"Type in Window::Theme::AccentColors.\");\n}\n\nFn<void(style::palette&)> PreparePaletteCallback(\n\t\tbool dark,\n\t\tstd::optional<QColor> accent) {\n\treturn [=](style::palette &palette) {\n\t\tusing namespace Theme;\n\t\tconst auto &embedded = EmbeddedThemes();\n\t\tconst auto i = ranges::find(\n\t\t\tembedded,\n\t\t\tdark ? EmbeddedType::Night : EmbeddedType::Default,\n\t\t\t&EmbeddedScheme::type);\n\t\tAssert(i != end(embedded));\n\t\tconst auto colorizer = accent\n\t\t\t? ColorizerFrom(*i, *accent)\n\t\t\t: style::colorizer();\n\n\t\tauto instance = Instance();\n\t\tconst auto loaded = LoadFromFile(\n\t\t\t(dark ? kNightBaseFile : kDayBaseFile).utf16(),\n\t\t\t&instance,\n\t\t\tnullptr,\n\t\t\tnullptr,\n\t\t\tcolorizer);\n\t\tAssert(loaded);\n\t\tpalette.finalize();\n\t\tpalette = instance.palette;\n\t};\n}\n\nFn<void(style::palette&)> PrepareCurrentPaletteCallback() {\n\treturn [=, data = style::main_palette::save()](style::palette &palette) {\n\t\tpalette.load(data);\n\t};\n}\n\nQByteArray AccentColors::serialize() const {\n\tauto result = QByteArray();\n\tif (_data.empty()) {\n\t\treturn result;\n\t}\n\n\tconst auto count = _data.size();\n\tauto size = sizeof(qint32) * (count + 1)\n\t\t+ Serialize::colorSize() * count;\n\tresult.reserve(size);\n\n\tauto stream = QDataStream(&result, QIODevice::WriteOnly);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\tstream << qint32(_data.size());\n\tfor (const auto &[type, color] : _data) {\n\t\tstream << static_cast<qint32>(type);\n\t\tSerialize::writeColor(stream, color);\n\t}\n\tstream.device()->close();\n\n\treturn result;\n}\n\nbool AccentColors::setFromSerialized(const QByteArray &serialized) {\n\tif (serialized.isEmpty()) {\n\t\t_data.clear();\n\t\treturn true;\n\t}\n\tauto copy = QByteArray(serialized);\n\tauto stream = QDataStream(&copy, QIODevice::ReadOnly);\n\tstream.setVersion(QDataStream::Qt_5_1);\n\n\tauto count = qint32();\n\tstream >> count;\n\tif (stream.status() != QDataStream::Ok) {\n\t\treturn false;\n\t} else if (count <= 0 || count > kMaxAccentColors) {\n\t\treturn false;\n\t}\n\tauto data = base::flat_map<EmbeddedType, QColor>();\n\tfor (auto i = 0; i != count; ++i) {\n\t\tauto type = qint32();\n\t\tstream >> type;\n\t\tconst auto color = Serialize::readColor(stream);\n\t\tconst auto uncheckedType = static_cast<EmbeddedType>(type);\n\t\tswitch (uncheckedType) {\n\t\tcase EmbeddedType::Default:\n\t\tcase EmbeddedType::DayBlue:\n\t\tcase EmbeddedType::Night:\n\t\tcase EmbeddedType::NightGreen:\n\t\t\tdata.emplace(uncheckedType, color);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\treturn false;\n\t\t}\n\t}\n\tif (stream.status() != QDataStream::Ok) {\n\t\treturn false;\n\t}\n\t_data = std::move(data);\n\treturn true;\n}\n\nvoid AccentColors::set(EmbeddedType type, const QColor &value) {\n\t_data.emplace_or_assign(type, value);\n}\n\nvoid AccentColors::clear(EmbeddedType type) {\n\t_data.remove(type);\n}\n\nstd::optional<QColor> AccentColors::get(EmbeddedType type) const {\n\tconst auto i = _data.find(type);\n\treturn (i != end(_data)) ? std::make_optional(i->second) : std::nullopt;\n}\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_themes_embedded.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace style {\nstruct colorizer;\n} // namespace style\n\nnamespace Window {\nnamespace Theme {\n\nenum class EmbeddedType {\n\tDayBlue,\n\tDefault,\n\tNight,\n\tNightGreen,\n};\n\nstruct EmbeddedScheme {\n\tEmbeddedType type = EmbeddedType();\n\tQColor background;\n\tQColor sent;\n\tQColor received;\n\tQColor radiobuttonInactive;\n\tQColor radiobuttonActive;\n\trpl::producer<QString> name;\n\tQString path;\n\tQColor accentColor;\n};\n\nclass AccentColors final {\npublic:\n\t[[nodiscard]] QByteArray serialize() const;\n\tbool setFromSerialized(const QByteArray &serialized);\n\n\tvoid set(EmbeddedType type, const QColor &value);\n\tvoid clear(EmbeddedType type);\n\t[[nodiscard]] std::optional<QColor> get(EmbeddedType type) const;\n\nprivate:\n\tbase::flat_map<EmbeddedType, QColor> _data;\n\n};\n\n[[nodiscard]] style::colorizer ColorizerFrom(\n\tconst EmbeddedScheme &scheme,\n\tconst QColor &color);\n[[nodiscard]] std::optional<QColor> SystemAccentColor();\n[[nodiscard]] style::colorizer ColorizerForTheme(const QString &absolutePath);\n\nvoid Colorize(\n\tEmbeddedScheme &scheme,\n\tconst style::colorizer &colorizer);\n\n[[nodiscard]] std::vector<EmbeddedScheme> EmbeddedThemes();\n[[nodiscard]] std::vector<QColor> DefaultAccentColors(EmbeddedType type);\n\n[[nodiscard]] Fn<void(style::palette&)> PreparePaletteCallback(\n\tbool dark,\n\tstd::optional<QColor> accent);\n[[nodiscard]] Fn<void(style::palette&)> PrepareCurrentPaletteCallback();\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_themes_generate_name.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/themes/window_themes_generate_name.h\"\n\n#include \"base/random.h\"\n\nnamespace Window {\nnamespace Theme {\nnamespace {\n\nconst auto kColors = base::flat_map<uint32, const char*>{\n\t{ 0x8e0000U, \"Berry\" },\n\t{ 0xdec196U, \"Brandy\" },\n\t{ 0x800b47U, \"Cherry\" },\n\t{ 0xff7f50U, \"Coral\" },\n\t{ 0xdb5079U, \"Cranberry\" },\n\t{ 0xdc143cU, \"Crimson\" },\n\t{ 0xe0b0ffU, \"Mauve\" },\n\t{ 0xffc0cbU, \"Pink\" },\n\t{ 0xff0000U, \"Red\" },\n\t{ 0xff007fU, \"Rose\" },\n\t{ 0x80461bU, \"Russet\" },\n\t{ 0xff2400U, \"Scarlet\" },\n\t{ 0xf1f1f1U, \"Seashell\" },\n\t{ 0xff3399U, \"Strawberry\" },\n\t{ 0xffbf00U, \"Amber\" },\n\t{ 0xeb9373U, \"Apricot\" },\n\t{ 0xfbe7b2U, \"Banana\" },\n\t{ 0xa1c50aU, \"Citrus\" },\n\t{ 0xb06500U, \"Ginger\" },\n\t{ 0xffd700U, \"Gold\" },\n\t{ 0xfde910U, \"Lemon\" },\n\t{ 0xffa500U, \"Orange\" },\n\t{ 0xffe5b4U, \"Peach\" },\n\t{ 0xff6b53U, \"Persimmon\" },\n\t{ 0xe4d422U, \"Sunflower\" },\n\t{ 0xf28500U, \"Tangerine\" },\n\t{ 0xffc87cU, \"Topaz\" },\n\t{ 0xffff00U, \"Yellow\" },\n\t{ 0x384910U, \"Clover\" },\n\t{ 0x83aa5dU, \"Cucumber\" },\n\t{ 0x50c878U, \"Emerald\" },\n\t{ 0xb5b35cU, \"Olive\" },\n\t{ 0x00ff00U, \"Green\" },\n\t{ 0x00a86bU, \"Jade\" },\n\t{ 0x29ab87U, \"Jungle\" },\n\t{ 0xbfff00U, \"Lime\" },\n\t{ 0x0bda51U, \"Malachite\" },\n\t{ 0x98ff98U, \"Mint\" },\n\t{ 0xaddfadU, \"Moss\" },\n\t{ 0x315ba1U, \"Azure\" },\n\t{ 0x0000ffU, \"Blue\" },\n\t{ 0x0047abU, \"Cobalt\" },\n\t{ 0x4f69c6U, \"Indigo\" },\n\t{ 0x017987U, \"Lagoon\" },\n\t{ 0x71d9e2U, \"Aquamarine\" },\n\t{ 0x120a8fU, \"Ultramarine\" },\n\t{ 0x000080U, \"Navy\" },\n\t{ 0x2f519eU, \"Sapphire\" },\n\t{ 0x76d7eaU, \"Sky\" },\n\t{ 0x008080U, \"Teal\" },\n\t{ 0x40e0d0U, \"Turquoise\" },\n\t{ 0x9966ccU, \"Amethyst\" },\n\t{ 0x4d0135U, \"Blackberry\" },\n\t{ 0x614051U, \"Eggplant\" },\n\t{ 0xc8a2c8U, \"Lilac\" },\n\t{ 0xb57edcU, \"Lavender\" },\n\t{ 0xccccffU, \"Periwinkle\" },\n\t{ 0x843179U, \"Plum\" },\n\t{ 0x660099U, \"Purple\" },\n\t{ 0xd8bfd8U, \"Thistle\" },\n\t{ 0xda70d6U, \"Orchid\" },\n\t{ 0x240a40U, \"Violet\" },\n\t{ 0x3f2109U, \"Bronze\" },\n\t{ 0x370202U, \"Chocolate\" },\n\t{ 0x7b3f00U, \"Cinnamon\" },\n\t{ 0x301f1eU, \"Cocoa\" },\n\t{ 0x706555U, \"Coffee\" },\n\t{ 0x796989U, \"Rum\" },\n\t{ 0x4e0606U, \"Mahogany\" },\n\t{ 0x782d19U, \"Mocha\" },\n\t{ 0xc2b280U, \"Sand\" },\n\t{ 0x882d17U, \"Sienna\" },\n\t{ 0x780109U, \"Maple\" },\n\t{ 0xf0e68cU, \"Khaki\" },\n\t{ 0xb87333U, \"Copper\" },\n\t{ 0xb94e48U, \"Chestnut\" },\n\t{ 0xeed9c4U, \"Almond\" },\n\t{ 0xfffdd0U, \"Cream\" },\n\t{ 0xb9f2ffU, \"Diamond\" },\n\t{ 0xa98307U, \"Honey\" },\n\t{ 0xfffff0U, \"Ivory\" },\n\t{ 0xeae0c8U, \"Pearl\" },\n\t{ 0xeff2f3U, \"Porcelain\" },\n\t{ 0xd1bea8U, \"Vanilla\" },\n\t{ 0xffffffU, \"White\" },\n\t{ 0x808080U, \"Gray\" },\n\t{ 0x000000U, \"Black\" },\n\t{ 0xe8f1d4U, \"Chrome\" },\n\t{ 0x36454fU, \"Charcoal\" },\n\t{ 0x0c0b1dU, \"Ebony\" },\n\t{ 0xc0c0c0U, \"Silver\" },\n\t{ 0xf5f5f5U, \"Smoke\" },\n\t{ 0x262335U, \"Steel\" },\n\t{ 0x4fa83dU, \"Apple\" },\n\t{ 0x80b3c4U, \"Glacier\" },\n\t{ 0xfebaadU, \"Melon\" },\n\t{ 0xc54b8cU, \"Mulberry\" },\n\t{ 0xa9c6c2U, \"Opal\" },\n\t{ 0x54a5f8U, \"Blue\" }\n};\n\nconst auto kAdjectives = std::vector<const char*>{\n\t\"Ancient\",\n\t\"Antique\",\n\t\"Autumn\",\n\t\"Baby\",\n\t\"Barely\",\n\t\"Baroque\",\n\t\"Blazing\",\n\t\"Blushing\",\n\t\"Bohemian\",\n\t\"Bubbly\",\n\t\"Burning\",\n\t\"Buttered\",\n\t\"Classic\",\n\t\"Clear\",\n\t\"Cool\",\n\t\"Cosmic\",\n\t\"Cotton\",\n\t\"Cozy\",\n\t\"Crystal\",\n\t\"Dark\",\n\t\"Daring\",\n\t\"Darling\",\n\t\"Dawn\",\n\t\"Dazzling\",\n\t\"Deep\",\n\t\"Deepest\",\n\t\"Delicate\",\n\t\"Delightful\",\n\t\"Divine\",\n\t\"Double\",\n\t\"Downtown\",\n\t\"Dreamy\",\n\t\"Dusky\",\n\t\"Dusty\",\n\t\"Electric\",\n\t\"Enchanted\",\n\t\"Endless\",\n\t\"Evening\",\n\t\"Fantastic\",\n\t\"Flirty\",\n\t\"Forever\",\n\t\"Frigid\",\n\t\"Frosty\",\n\t\"Frozen\",\n\t\"Gentle\",\n\t\"Heavenly\",\n\t\"Hyper\",\n\t\"Icy\",\n\t\"Infinite\",\n\t\"Innocent\",\n\t\"Instant\",\n\t\"Luscious\",\n\t\"Lunar\",\n\t\"Lustrous\",\n\t\"Magic\",\n\t\"Majestic\",\n\t\"Mambo\",\n\t\"Midnight\",\n\t\"Millennium\",\n\t\"Morning\",\n\t\"Mystic\",\n\t\"Natural\",\n\t\"Neon\",\n\t\"Night\",\n\t\"Opaque\",\n\t\"Paradise\",\n\t\"Perfect\",\n\t\"Perky\",\n\t\"Polished\",\n\t\"Powerful\",\n\t\"Rich\",\n\t\"Royal\",\n\t\"Sheer\",\n\t\"Simply\",\n\t\"Sizzling\",\n\t\"Solar\",\n\t\"Sparkling\",\n\t\"Splendid\",\n\t\"Spicy\",\n\t\"Spring\",\n\t\"Stellar\",\n\t\"Sugared\",\n\t\"Summer\",\n\t\"Sunny\",\n\t\"Super\",\n\t\"Sweet\",\n\t\"Tender\",\n\t\"Tenacious\",\n\t\"Tidal\",\n\t\"Toasted\",\n\t\"Totally\",\n\t\"Tranquil\",\n\t\"Tropical\",\n\t\"True\",\n\t\"Twilight\",\n\t\"Twinkling\",\n\t\"Ultimate\",\n\t\"Ultra\",\n\t\"Velvety\",\n\t\"Vibrant\",\n\t\"Vintage\",\n\t\"Virtual\",\n\t\"Warm\",\n\t\"Warmest\",\n\t\"Whipped\",\n\t\"Wild\",\n\t\"Winsome\"\n};\n\nconst auto kSubjectives = std::vector<const char*>{\n\t\"Ambrosia\",\n\t\"Attack\",\n\t\"Avalanche\",\n\t\"Blast\",\n\t\"Bliss\",\n\t\"Blossom\",\n\t\"Blush\",\n\t\"Burst\",\n\t\"Butter\",\n\t\"Candy\",\n\t\"Carnival\",\n\t\"Charm\",\n\t\"Chiffon\",\n\t\"Cloud\",\n\t\"Comet\",\n\t\"Delight\",\n\t\"Dream\",\n\t\"Dust\",\n\t\"Fantasy\",\n\t\"Flame\",\n\t\"Flash\",\n\t\"Fire\",\n\t\"Freeze\",\n\t\"Frost\",\n\t\"Glade\",\n\t\"Glaze\",\n\t\"Gleam\",\n\t\"Glimmer\",\n\t\"Glitter\",\n\t\"Glow\",\n\t\"Grande\",\n\t\"Haze\",\n\t\"Highlight\",\n\t\"Ice\",\n\t\"Illusion\",\n\t\"Intrigue\",\n\t\"Jewel\",\n\t\"Jubilee\",\n\t\"Kiss\",\n\t\"Lights\",\n\t\"Lollypop\",\n\t\"Love\",\n\t\"Luster\",\n\t\"Madness\",\n\t\"Matte\",\n\t\"Mirage\",\n\t\"Mist\",\n\t\"Moon\",\n\t\"Muse\",\n\t\"Myth\",\n\t\"Nectar\",\n\t\"Nova\",\n\t\"Parfait\",\n\t\"Passion\",\n\t\"Pop\",\n\t\"Rain\",\n\t\"Reflection\",\n\t\"Rhapsody\",\n\t\"Romance\",\n\t\"Satin\",\n\t\"Sensation\",\n\t\"Silk\",\n\t\"Shine\",\n\t\"Shadow\",\n\t\"Shimmer\",\n\t\"Sky\",\n\t\"Spice\",\n\t\"Star\",\n\t\"Sugar\",\n\t\"Sunrise\",\n\t\"Sunset\",\n\t\"Sun\",\n\t\"Twist\",\n\t\"Unbound\",\n\t\"Velvet\",\n\t\"Vibrant\",\n\t\"Waters\",\n\t\"Wine\",\n\t\"Wink\",\n\t\"Wonder\",\n\t\"Zone\"\n};\n\n} // namespace\n\nQString GenerateName(const QColor &accent) {\n\tconst auto r1 = accent.red();\n\tconst auto g1 = accent.green();\n\tconst auto b1 = accent.blue();\n\tconst auto distance = [&](const auto &pair) {\n\t\tconst auto &[color, name] = pair;\n\t\tconst auto b2 = int(color & 0xFFU);\n\t\tconst auto g2 = int((color >> 8) & 0xFFU);\n\t\tconst auto r2 = int((color >> 16) & 0xFFU);\n\t\tconst auto rMean = (r1 + r2) / 2;\n\t\tconst auto r = r1 - r2;\n\t\tconst auto g = g1 - g2;\n\t\tconst auto b = b1 - b2;\n\t\treturn (((512 + rMean) * r * r) >> 8)\n\t\t\t+ (4 * g * g)\n\t\t\t+ (((767 - rMean) * b * b) >> 8);\n\t};\n\tconst auto pred = [&](const auto &a, const auto &b) {\n\t\treturn distance(a) < distance(b);\n\t};\n\tconst auto capitalized = [](const char *value) {\n\t\tExpects(*value != 0);\n\n\t\tauto result = QString::fromLatin1(value);\n\t\tresult[0] = result[0].toUpper();\n\t\treturn result;\n\t};\n\tconst auto random = [&](const std::vector<const char*> &values) {\n\t\tconst auto index = base::RandomValue<size_t>() % values.size();\n\t\treturn capitalized(values[index]);\n\t};\n\tconst auto min = ranges::min_element(kColors, pred);\n\tAssert(min != end(kColors));\n\tconst auto color = capitalized(min->second);\n\treturn (base::RandomValue<uint8>() % 2 == 0)\n\t\t? random(kAdjectives) + ' ' + color\n\t\t: color + ' ' + random(kSubjectives);\n}\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/themes/window_themes_generate_name.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Window {\nnamespace Theme {\n\n[[nodiscard]] QString GenerateName(const QColor &accent);\n\n} // namespace Theme\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window.style",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\nusing \"ui/basic.style\";\nusing \"ui/widgets/widgets.style\";\nusing \"chat_helpers/chat_helpers.style\";\nusing \"boxes/boxes.style\"; // UserpicButton\n\nwindowMinWidth: 380px;\nwindowMinHeight: 480px;\nwindowDefaultWidth: 800px;\nwindowDefaultHeight: 600px;\nwindowBigDefaultWidth: 1024px;\nwindowBigDefaultHeight: 768px;\n\ncolumnMinimalWidthLeft: 260px;\ncolumnMaximalWidthLeft: 540px;\ncolumnMinimalWidthMain: 380px;\ncolumnMinimalWidthThird: 292px;\ncolumnMaximalWidthThird: 392px;\n\nadaptiveChatWideWidth: 880px;\n\nnotifyBorder: windowShadowFgFallback;\nnotifyBorderWidth: 1px;\nnotifySlowHide: 4000;\nnotifyPhotoSize: 62px;\nnotifyMacPhotoSize: 64px;\nnotifyPhotoPos: point(9px, 9px);\nnotifyClosePos: point(1px, 2px);\nnotifyClose: IconButton {\n\twidth: 30px;\n\theight: 30px;\n\n\ticon: smallCloseIcon;\n\ticonOver: smallCloseIconOver;\n\n\ticonPosition: point(10px, 10px);\n\n\trippleAreaPosition: point(5px, 5px);\n\trippleAreaSize: 20px;\n\tripple: defaultRippleAnimationBgOver;\n}\nnotifyItemTop: 12px;\nnotifyTextLeft: 12px;\nnotifyTextTop: 7px;\nnotifyWaitLongHide: 3000;\nnotifyFastAnim: 150;\nnotifyWidth: 320px;\nnotifyMinHeight: 80px;\nnotifyDeltaX: 6px;\nnotifyDeltaY: 7px;\nnotifyActionsDuration: 200;\n\nnotifyHideAllHeight: 36px;\n\nnotifyReplyArea: InputField(defaultInputField) {\n\tstyle: defaultTextStyle;\n\ttextMargins: margins(8px, 8px, 8px, 6px);\n\theightMin: 36px;\n\theightMax: 72px;\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(2px, 0px, 2px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderFont: normalFont;\n\tborder: 0px;\n\tborderActive: 0px;\n}\nnotifySendReply: IconButton {\n\twidth: 36px;\n\theight: 36px;\n\n\ticon: historySendIcon;\n\ticonOver: historySendIconOver;\n\ticonPosition: point(6px, 6px);\n}\nnotifyFadeRight: icon {{ \"fade_horizontal\", notificationBg }};\n\ntitleUnreadCounterTop: 6px;\ntitleUnreadCounterRight: 35px;\n\nmainMenuWidth: 274px;\nmainMenuCoverHeight: 134px;\nmainMenuUserpicLeft: 24px;\nmainMenuUserpicTop: 20px;\nmainMenuUserpic: UserpicButton(defaultUserpicButton) {\n\tsize: size(48px, 48px);\n\tphotoSize: 48px;\n}\nmainMenuCloudButton: IconButton {\n\twidth: 48px;\n\theight: 48px;\n\n\ticon: icon {\n//\t\t{ \"menu_cloud\", mainMenuCloudFg },\n\t};\n\ticonPosition: point(22px, 22px);\n}\nmainMenuCloudSize: 32px;\nmainMenuResetScaleFont: font(20px semibold);\nmainMenuResetScaleLeft: 36px;\nmainMenuResetScaleRight: 12px;\nmainMenuResetScaleTop: 2px;\nmainMenuResetScaleIconLeft: 5px;\nmainMenuCoverNameLeft: 26px;\nmainMenuCoverNameTop: 84px;\nmainMenuCoverStatusLeft: mainMenuCoverNameLeft;\nmainMenuCoverStatusTop: 103px;\nmainMenuSkip: 6px;\nmainMenuFooterHeightMin: 80px;\nmainMenu: Menu(defaultMenu) {\n\titemFg: windowBoldFg;\n\titemFgOver: windowBoldFgOver;\n\titemStyle: semiboldTextStyle;\n\titemIconPosition: point(26px, 8px);\n\titemPadding: margins(76px, 13px, 28px, 13px);\n\titemToggle: Toggle(defaultMenuToggle) {\n\t\ttoggledFg: mainMenuCoverBg;\n\t}\n\titemToggleOver: Toggle(defaultMenuToggleOver) {\n\t\ttoggledFg: mainMenuCoverBg;\n\t}\n\titemToggleShift: 11px;\n}\nmainMenuButton: SettingsButton(defaultSettingsButton) {\n\tstyle: semiboldTextStyle;\n\tpadding: margins(61px, 11px, 20px, 9px);\n\ttoggleSkip: 19px;\n\ticonLeft: 21px;\n}\nmainMenuAddAccountButton: SettingsButton(mainMenuButton) {\n\ticonLeft: 23px;\n}\n\nmainMenuAccountSize: 26px;\nmainMenuAccountLine: 2px;\nmainMenuBadgeFont: font(11px bold);\nmainMenuBadgeSize: 18px;\n\nmainMenuFooterLeft: 25px;\nmainMenuTelegramLabel: FlatLabel(defaultFlatLabel) {\n\talign: align(left);\n\ttextFg: windowSubTextFg;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: semiboldFont;\n\t}\n\tpalette: TextPalette(defaultTextPalette) {\n\t\tlinkFg: windowSubTextFg;\n\t}\n}\nmainMenuTelegramBottom: 38px;\nmainMenuVersionLabel: FlatLabel(mainMenuTelegramLabel) {\n\tstyle: defaultTextStyle;\n}\nmainMenuVersionBottom: 17px;\n\nmainMenuToggleSize: 6px;\nmainMenuToggleFourStrokes: 3px;\nmainMenuTogglePosition: point(30px, 30px);\n\nthemeEditorSampleSize: size(90px, 51px);\nthemeEditorMargin: margins(17px, 10px, 17px, 10px);\nthemeEditorDescriptionSkip: 10px;\nthemeEditorNameFont: font(15px semibold);\nthemeEditorCopyNameFont: font(fsize semibold);\n\nconnectingLeftShadow: icon {{ \"connecting_left_shadow\", windowShadowFg }};\nconnectingLeft: icon {{ \"connecting_left\", windowBg }};\nconnectingRightShadow: icon {{ \"connecting_right_shadow\", windowShadowFg }};\nconnectingRight: icon {{ \"connecting_right\", windowBg }};\nconnectingBodyShadow: icon {{ \"connecting_body_shadow\", windowShadowFg }};\nconnectingBody: icon {{ \"connecting_body\", windowBg }};\nconnectingMargin: margins(2px, 2px, 2px, 2px);\nconnectingTextPadding: margins(18px, 11px, 18px, 0px);\nconnectingRadial: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {\n\tcolor: menuIconFg;\n\tthickness: 2px;\n\tsize: size(20px, 20px);\n}\nconnectingRetryLink: LinkButton(defaultLinkButton) {\n\tpadding: margins(6px, 11px, 6px, 0px);\n}\nconnectingProxyOff: icon {{ \"proxy_off\", menuIconFg }};\nconnectingProxyOn: icon {{ \"proxy_on\", windowBgActive }};\nconnectingDuration: 150;\n\nwindowOutdatedDetails: FlatLabel(defaultFlatLabel) {\n\talign: align(top);\n\tminWidth: 100px;\n\ttextFg: outdatedFg;\n}\nwindowOutdatedTitle: FlatLabel(windowOutdatedDetails) {\n\tstyle: semiboldTextStyle;\n}\nwindowOutdatedPadding: margins(10px, 10px, 10px, 10px);\nwindowOutdatedSkip: 6px;\nwindowOutdatedClose: IconButton(defaultIconButton) {\n\twidth: 60px;\n\theight: 60px;\n\ticon: icon {{ \"info/info_close\", outdatedFg }};\n\ticonOver: icon {{ \"info/info_close\", outdatedFg }};\n\ticonPosition: point(-1px, -1px);\n}\n\nwindowScreenReaderLabel: FlatLabel(defaultFlatLabel) {\n\talign: align(left);\n\ttextFg: activeButtonFg;\n\tstyle: semiboldTextStyle;\n}\nwindowScreenReaderPadding: margins(12px, 4px, 4px, 4px);\nwindowScreenReaderDisableTextStyle: semiboldTextStyle;\nwindowScreenReaderButtonPadding: margins(12px, 0px, 12px, 0px);\nwindowScreenReaderButtonHeight: 28px;\nwindowScreenReaderButtonBorderWidth: 2px;\nwindowScreenReaderButtonRadius: 14px;\nwindowScreenReaderButtonRipple: RippleAnimation(defaultRippleAnimation) {\n\tcolor: activeButtonBgRipple;\n}\n\ncreateThemeImportButton: SettingsButton(defaultSettingsButton) {\n\ttextFg: lightButtonFg;\n\ttextFgOver: lightButtonFgOver;\n\ttextBg: windowBg;\n\ttextBgOver: windowBgOver;\n\n\tstyle: semiboldTextStyle;\n\n\theight: 20px;\n\tpadding: margins(22px, 10px, 22px, 8px);\n\n\tripple: defaultRippleAnimation;\n}\ncreateThemeLink: InputField(defaultInputField) {\n\ttextMargins: margins(0px, 7px, 0px, 0px);\n\ttextBg: boxBg;\n\n\tplaceholderFg: placeholderFg;\n\tplaceholderFgActive: placeholderFgActive;\n\tplaceholderFgError: placeholderFgActive;\n\tplaceholderMargins: margins(0px, 0px, 0px, 0px);\n\tplaceholderScale: 0.;\n\tplaceholderFont: boxTextFont;\n\n\theightMin: 34px;\n}\n\nwindowFiltersWidth: 72px;\nwindowFiltersButton: SideBarButton(defaultSideBarButton) {\n\ttextTop: 40px;\n\ttextSkip: 6px;\n\tminHeight: 62px;\n\tminTextWidth: 48px;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(11px semibold);\n\t}\n\tbadgeStyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(12px semibold);\n\t}\n\tbadgeSkip: 4px;\n\tbadgeHeight: 17px;\n\tbadgeStroke: 2px;\n\tbadgePosition: point(3px, 7px);\n\ticonPosition: point(-1px, 6px);\n}\nwindowFiltersMainMenu: SideBarButton(windowFiltersButton) {\n\ticon: icon {{ \"dialogs/dialogs_menu\", sideBarIconFg }};\n\ticonPosition: point(-1px, -1px);\n\tminHeight: 54px;\n}\nwindowFiltersMainMenuUnread: icon {\n\t{ \"dialogs/dialogs_menu_unread\", sideBarIconFg },\n\t{ \"dialogs/dialogs_menu_unread_dot\", sideBarBadgeBg },\n};\nwindowFiltersMainMenuUnreadMuted: icon {\n\t{ \"dialogs/dialogs_menu_unread\", sideBarIconFg },\n\t{ \"dialogs/dialogs_menu_unread_dot\", sideBarBadgeBgMuted },\n};\nwindowFilterSmallItem: PeerListItem(defaultPeerListItem) {\n\theight: 44px;\n\tphotoPosition: point(13px, 5px);\n\tnamePosition: point(59px, 14px);\n\tphotoSize: 34px;\n\tcheckbox: RoundImageCheckbox(defaultPeerListCheckbox) {\n\t\timageRadius: 17px;\n\t\timageSmallRadius: 14px;\n\t}\n}\nwindowFilterSmallList: PeerList(defaultPeerList) {\n\titem: windowFilterSmallItem;\n}\nwindowFilterSmallRemove: IconButton(notifyClose) {\n}\nwindowFilterSmallRemoveRight: 10px;\nwindowFilterNameInput: InputField(defaultInputField) {\n\ttextMargins: margins(0px, 26px, 87px, 4px);\n}\nwindowFilterNameEmojiPosition: point(-65px, 22px);\nwindowFilterNameCharsLimitRightPosition: point(75px, 27px);\nwindowFilterStaticTitlePosition: point(0px, 5px);\nwindowFilterIconToggleSize: size(36px, 36px);\nwindowFilterIconTogglePosition: point(-4px, 18px);\nwindwoFilterIconPanelPosition: point(-2px, -1px);\nwindowFilterIconSingle: size(44px, 42px);\nwindowFilterIconPadding: margins(10px, 36px, 10px, 8px);\nwindowFilterIconHeaderPosition: point(18px, 14px);\nwindowFilterTypeContacts: icon {{ \"folders/folders_type_contacts\", historyPeerUserpicFg }};\nwindowFilterTypeNonContacts: icon {{ \"folders/folders_type_noncontacts\", historyPeerUserpicFg }};\nwindowFilterTypeGroups: icon {{ \"folders/folders_type_groups\", historyPeerUserpicFg }};\nwindowFilterTypeChannels: icon {{ \"folders/folders_type_channels\", historyPeerUserpicFg }};\nwindowFilterTypeBots: icon {{ \"folders/folders_type_bots\", historyPeerUserpicFg }};\nwindowFilterTypeNoMuted: icon {{ \"folders/folders_type_muted\", historyPeerUserpicFg }};\nwindowFilterTypeNoArchived: icon {{ \"folders/folders_type_archived\", historyPeerUserpicFg }};\nwindowFilterTypeNoRead: icon {{ \"folders/folders_type_read\", historyPeerUserpicFg }};\nwindowFilterTypeNewChats: icon {{ \"folders/folder_new_chats\", historyPeerUserpicFg }};\nwindowFilterTypeExistingChats: icon {{ \"folders/folder_existing_chats\", historyPeerUserpicFg }};\nwindowFilterChatsSectionSubtitleHeight: 28px;\nwindowFilterChatsSectionSubtitle: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: semiboldFont;\n\t}\n\ttextFg: searchedBarFg;\n}\nwindowFilterChatsSectionSubtitlePadding: margins(22px, 5px, 22px, 5px);\n\nwindowArchiveToast: Toast(defaultToast) {\n\tminWidth: boxWideWidth;\n\tmaxWidth: boxWideWidth;\n}\n\nwindowFeeItem: Menu(defaultMenu) {\n\titemPadding: margins(17px, 3px, 17px, 4px);\n\titemRightSkip: 0px;\n\titemStyle: whenReadStyle;\n\titemFgOver: windowFg;\n\titemFgDisabled: windowFg;\n}\n\nivWidthMin: 380px;\nivHeightMin: 480px;\nivWidthDefault: 600px;\nivHeightDefault: 800px;\n\nmaxWidthSharedMediaWindow: 419px;\n\nchatSwitchMargins: margins(16px, 16px, 16px, 16px);\nchatSwitchPadding: margins(12px, 12px, 12px, 12px);\nchatSwitchSize: size(72px, 104px);\nchatSwitchUserpic: UserpicButton(defaultUserpicButton) {\n\tsize: size(56px, 56px);\n\tphotoSize: 56px;\n}\nchatSwitchUserpicSublist: UserpicButton(defaultUserpicButton) {\n\tsize: size(40px, 40px);\n\tphotoSize: 40px;\n}\nchatSwitchUserpicSmall: UserpicButton(defaultUserpicButton) {\n\tsize: size(24px, 24px);\n\tphotoSize: 24px;\n}\nchatSwitchUserpicTop: 8px;\nchatSwitchNameLabel: FlatLabel(defaultFlatLabel) {\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(11px);\n\t}\n\talign: align(top);\n\tminWidth: 56px;\n\tmaxHeight: 36px;\n}\nchatSwitchNameSkip: 6px;\nchatSwitchSelectLine: 3px;\n\nlockSetupEmailLabelMaxWidth: 256px + 24px + 24px;\nlockSetupEmailTitle: FlatLabel(defaultFlatLabel) {\n\tminWidth: lockSetupEmailLabelMaxWidth;\n\tstyle: TextStyle(defaultTextStyle) {\n\t\tfont: font(19px);\n\t}\n}\nlockSetupEmailLabel: FlatLabel(defaultFlatLabel) {\n\tminWidth: 256px;\n}\nlockSetupEmailLogOutPosition: point(10px, 10px);\nlockSetupEmailUserpicSmall: UserpicButton(defaultUserpicButton) {\n\tsize: size(22px, 22px);\n\tphotoSize: 22px;\n}\n\n// Windows specific\n\nwinQuitIcon: icon {{ \"win_quit\", windowFg }};\n\n// Mac specific\n\nmacAccessoryWidth: 450.;\nmacAccessoryHeight: 90.;\nmacEnableFilterAdd: 2;\nmacEnableFilterTop: 5;\nmacSelectorTop: 6;\nmacAlwaysThisAppTop: 4;\nmacAppHintTop: 8;\nmacCautionIconSize: 16;\n\nmacWindowRoundRadius: 10;\nmacWindowShadowTopLeft: icon {{ \"mac_window_shadow_top_left\", windowShadowFg }};\nmacTrayIcon: icon {{ \"mac_tray_icon\", windowFg }};\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_adaptive.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_adaptive.h\"\n\n#include \"history/history_item.h\"\n#include \"data/data_media_types.h\"\n#include \"data/data_session.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n\nnamespace Window {\n\nAdaptive::Adaptive() = default;\n\nvoid Adaptive::setWindowLayout(WindowLayout value) {\n\t_layout = value;\n}\n\nvoid Adaptive::setChatLayout(ChatLayout value) {\n\t_chatLayout = value;\n}\n\nrpl::producer<> Adaptive::value() const {\n\treturn rpl::merge(\n\t\tCore::App().settings().adaptiveForWideValue() | rpl::to_empty,\n\t\t_chatLayout.changes() | rpl::to_empty,\n\t\t_layout.changes() | rpl::to_empty);\n}\n\nrpl::producer<> Adaptive::changes() const {\n\treturn rpl::merge(\n\t\tCore::App().settings().adaptiveForWideChanges() | rpl::to_empty,\n\t\t_chatLayout.changes() | rpl::to_empty,\n\t\t_layout.changes() | rpl::to_empty);\n}\n\nrpl::producer<bool> Adaptive::oneColumnValue() const {\n\treturn _layout.value(\n\t) | rpl::map([=] {\n\t\treturn isOneColumn();\n\t});\n}\n\nrpl::producer<Adaptive::ChatLayout> Adaptive::chatLayoutValue() const {\n\treturn _chatLayout.value();\n}\n\nbool Adaptive::isOneColumn() const {\n\treturn _layout.current() == WindowLayout::OneColumn;\n}\n\nbool Adaptive::isNormal() const {\n\treturn _layout.current() == WindowLayout::Normal;\n}\n\nbool Adaptive::isThreeColumn() const {\n\treturn _layout.current() == WindowLayout::ThreeColumn;\n}\n\nrpl::producer<bool> Adaptive::chatWideValue() const {\n\treturn rpl::combine(\n\t\t_chatLayout.value(\n\t\t) | rpl::map(rpl::mappers::_1 == Adaptive::ChatLayout::Wide),\n\t\tCore::App().settings().adaptiveForWideValue()\n\t) | rpl::map(rpl::mappers::_1 && rpl::mappers::_2);\n}\n\nbool Adaptive::isChatWide() const {\n\treturn Core::App().settings().adaptiveForWide()\n\t\t&& (_chatLayout.current() == Adaptive::ChatLayout::Wide);\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_adaptive.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Window {\n\nclass Adaptive {\npublic:\n\tenum class WindowLayout {\n\t\tOneColumn,\n\t\tNormal,\n\t\tThreeColumn,\n\t};\n\n\tenum class ChatLayout {\n\t\tNormal,\n\t\tWide,\n\t};\n\n\tAdaptive();\n\n\tvoid setWindowLayout(WindowLayout value);\n\tvoid setChatLayout(ChatLayout value);\n\n\t[[nodiscard]] rpl::producer<> value() const;\n\t[[nodiscard]] rpl::producer<> changes() const;\n\t[[nodiscard]] rpl::producer<bool> oneColumnValue() const;\n\t[[nodiscard]] rpl::producer<ChatLayout> chatLayoutValue() const;\n\n\t[[nodiscard]] bool isOneColumn() const;\n\t[[nodiscard]] bool isNormal() const;\n\t[[nodiscard]] bool isThreeColumn() const;\n\n\t[[nodiscard]] rpl::producer<bool> chatWideValue() const;\n\t[[nodiscard]] bool isChatWide() const;\n\nprivate:\n\trpl::variable<ChatLayout> _chatLayout;\n\trpl::variable<WindowLayout> _layout;\n\n};\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_chat_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_chat_preview.h\"\n\n#include \"data/data_forum_topic.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_session.h\"\n#include \"history/history.h\"\n#include \"history/view/history_view_chat_preview.h\"\n#include \"mainwidget.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller.h\"\n\nnamespace Window {\nnamespace {\n\nconstexpr auto kChatPreviewDelay = crl::time(1000);\n\n} // namespace\n\nChatPreviewManager::ChatPreviewManager(\n\tnot_null<SessionController*> controller)\n: _controller(controller)\n, _timer([=] { showScheduled(); }) {\n}\n\nbool ChatPreviewManager::show(\n\t\tDialogs::RowDescriptor row,\n\t\tFn<void(bool shown)> callback,\n\t\tQPointer<QWidget> parentOverride,\n\t\tstd::optional<QPoint> positionOverride) {\n\tcancelScheduled();\n\t_topicLifetime.destroy();\n\tif (const auto topic = row.key.topic()) {\n\t\t_topicLifetime = topic->destroyed() | rpl::on_next([=] {\n\t\t\t_menu = nullptr;\n\t\t});\n\t} else if (!row.key) {\n\t\treturn false;\n\t}\n\n\tconst auto parent = parentOverride\n\t\t? parentOverride\n\t\t: _controller->content();\n\tauto preview = HistoryView::MakeChatPreview(parent, row.key.entry());\n\tif (!preview.menu) {\n\t\treturn false;\n\t}\n\t_menu = std::move(preview.menu);\n\tconst auto weakMenu = base::make_weak(_menu.get());\n\tconst auto weakThread = base::make_weak(row.key.entry()->asThread());\n\tconst auto weakController = base::make_weak(_controller);\n\tstd::move(\n\t\tpreview.actions\n\t) | rpl::on_next([=](HistoryView::ChatPreviewAction action) {\n\t\tif (const auto controller = weakController.get()) {\n\t\t\tif (const auto thread = weakThread.get()) {\n\t\t\t\tconst auto itemId = action.openItemId;\n\t\t\t\tconst auto owner = &thread->owner();\n\t\t\t\tif (action.cancel) {\n\t\t\t\t} else if (action.markRead) {\n\t\t\t\t\tMarkAsReadThread(thread);\n\t\t\t\t} else if (action.markUnread) {\n\t\t\t\t\tif (const auto history = thread->asHistory()) {\n\t\t\t\t\t\thistory->owner().histories().changeDialogUnreadMark(\n\t\t\t\t\t\t\thistory,\n\t\t\t\t\t\t\ttrue);\n\t\t\t\t\t}\n\t\t\t\t} else if (action.openInfo) {\n\t\t\t\t\tcontroller->showPeerInfo(thread);\n\t\t\t\t} else if (const auto item = owner->message(itemId)) {\n\t\t\t\t\tcontroller->showMessage(item);\n\t\t\t\t} else {\n\t\t\t\t\tcontroller->showThread(thread);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (const auto strong = weakMenu.get()) {\n\t\t\tstrong->hideMenu();\n\t\t}\n\t}, _menu->lifetime());\n\tQObject::connect(_menu.get(), &QObject::destroyed, [=] {\n\t\t_topicLifetime.destroy();\n\t\tif (callback) {\n\t\t\tcallback(false);\n\t\t}\n\t});\n\n\tif (callback) {\n\t\tcallback(true);\n\t}\n\t_menu->popup(positionOverride.value_or(QCursor::pos()));\n\n\treturn true;\n}\n\nbool ChatPreviewManager::schedule(\n\t\tDialogs::RowDescriptor row,\n\t\tFn<void(bool shown)> callback,\n\t\tQPointer<QWidget> parentOverride,\n\t\tstd::optional<QPoint> positionOverride) {\n\tcancelScheduled();\n\t_topicLifetime.destroy();\n\tif (const auto topic = row.key.topic()) {\n\t\t_topicLifetime = topic->destroyed() | rpl::on_next([=] {\n\t\t\tcancelScheduled();\n\t\t\t_menu = nullptr;\n\t\t});\n\t} else if (!row.key.history()) {\n\t\treturn false;\n\t}\n\t_scheduled = std::move(row);\n\t_scheduledCallback = std::move(callback);\n\t_scheduledParentOverride = std::move(parentOverride);\n\t_scheduledPositionOverride = positionOverride;\n\t_timer.callOnce(kChatPreviewDelay);\n\treturn true;\n}\n\nvoid ChatPreviewManager::showScheduled() {\n\tshow(\n\t\tbase::take(_scheduled),\n\t\tbase::take(_scheduledCallback),\n\t\tnullptr,\n\t\tbase::take(_scheduledPositionOverride));\n}\n\nvoid ChatPreviewManager::cancelScheduled() {\n\t_scheduled = {};\n\t_scheduledCallback = nullptr;\n\t_scheduledPositionOverride = {};\n\t_timer.cancel();\n}\n\n} // namespace Window"
  },
  {
    "path": "Telegram/SourceFiles/window/window_chat_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"base/unique_qptr.h\"\n#include \"dialogs/dialogs_key.h\"\n\nnamespace Ui {\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Window {\n\nclass SessionController;\n\nclass ChatPreviewManager final {\npublic:\n\tChatPreviewManager(not_null<SessionController*> controller);\n\n\tbool show(\n\t\tDialogs::RowDescriptor row,\n\t\tFn<void(bool shown)> callback = nullptr,\n\t\tQPointer<QWidget> parentOverride = nullptr,\n\t\tstd::optional<QPoint> positionOverride = {});\n\tbool schedule(\n\t\tDialogs::RowDescriptor row,\n\t\tFn<void(bool shown)> callback = nullptr,\n\t\tQPointer<QWidget> parentOverride = nullptr,\n\t\tstd::optional<QPoint> positionOverride = {});\n\tvoid cancelScheduled();\n\nprivate:\n\tvoid showScheduled();\n\n\tconst not_null<SessionController*> _controller;\n\tDialogs::RowDescriptor _scheduled;\n\tFn<void(bool)> _scheduledCallback;\n\tQPointer<QWidget> _scheduledParentOverride;\n\tstd::optional<QPoint> _scheduledPositionOverride;\n\tbase::Timer _timer;\n\n\trpl::lifetime _topicLifetime;\n\n\tbase::unique_qptr<Ui::PopupMenu> _menu;\n\n};\n\n} // namespace Window"
  },
  {
    "path": "Telegram/SourceFiles/window/window_chat_switch_process.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_chat_switch_process.h\"\n\n#include \"core/application.h\"\n#include \"core/shortcuts.h\"\n#include \"data/components/recent_peers.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_thread.h\"\n#include \"info/profile/info_profile_cover.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_session.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/painter.h\"\n#include \"ui/rp_widget.h\"\n#include \"window/window_separate_id.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_window.h\"\n\nnamespace Window {\nnamespace {\n\nclass Button final : public Ui::AbstractButton {\npublic:\n\tButton(\n\t\tQWidget *parent,\n\t\tnot_null<Data::Thread*> thread,\n\t\tbase::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> &userpics);\n\n\t[[nodiscard]] rpl::producer<> selectRequests() const;\n\n\tvoid setSelected(\n\t\tbool selected,\n\t\tanim::type animated = anim::type::normal);\n\nprivate:\n\tvoid setup(\n\t\tnot_null<Data::Thread*> thread,\n\t\tbase::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> &userpics);\n\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid mouseMoveEvent(QMouseEvent *e) override;\n\n\trpl::event_stream<> _selectRequests;\n\tUi::Animations::Simple _overAnimation;\n\tbool _selected = false;\n\n};\n\nvoid CloseInWindows(not_null<Data::Thread*> thread) {\n\tusing WindowPointer = base::weak_ptr<Window::SessionController>;\n\tauto closing = std::vector<WindowPointer>();\n\tauto clearing = std::vector<WindowPointer>();\n\tfor (const auto &window : thread->session().windows()) {\n\t\tif (window->windowId().chat() == thread) {\n\t\t\tclosing.push_back(base::make_weak(window));\n\t\t} else if (window->activeChatCurrent().thread() == thread) {\n\t\t\tclearing.push_back(base::make_weak(window));\n\t\t}\n\t}\n\tfor (const auto &window : closing) {\n\t\tif (const auto strong = window.get()) {\n\t\t\tCore::App().closeWindow(&strong->window());\n\t\t}\n\t}\n\tfor (const auto &window : clearing) {\n\t\tif (const auto strong = window.get()) {\n\t\t\tstrong->clearSectionStack();\n\t\t}\n\t}\n}\n\nButton::Button(\n\tQWidget *parent,\n\tnot_null<Data::Thread*> thread,\n\tbase::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> &userpics)\n: AbstractButton(parent) {\n\tsetup(thread, userpics);\n}\n\nvoid Button::setup(\n\t\tnot_null<Data::Thread*> thread,\n\t\tbase::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> &userpics) {\n\tresize(st::chatSwitchSize);\n\n\tauto userpicSt = &st::chatSwitchUserpic;\n\tconst auto userpicSize = userpicSt->size;\n\tif (const auto topic = thread->asTopic()) {\n\t\tusing namespace Info::Profile;\n\t\tconst auto userpic = Ui::CreateChild<TopicIconButton>(\n\t\t\tthis,\n\t\t\ttopic,\n\t\t\t[] { return true; }); // paused\n\t\tuserpic->show();\n\t\tuserpic->move(\n\t\t\t((width() - userpic->width()) / 2),\n\t\t\tst::chatSwitchUserpicTop);\n\t\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\n\t\tuserpicSt = &st::chatSwitchUserpicSmall;\n\t} else if (const auto sublist = thread->asSublist()) {\n\t\tconst auto sublistPeer = sublist->sublistPeer();\n\t\tconst auto userpic = Ui::CreateChild<Ui::UserpicButton>(\n\t\t\tthis,\n\t\t\tsublistPeer,\n\t\t\tst::chatSwitchUserpicSublist);\n\t\tuserpic->showMyNotesOnSelf(true);\n\t\tuserpic->show();\n\t\tuserpic->move(\n\t\t\t((width() - userpicSize.width()) / 2),\n\t\t\tst::chatSwitchUserpicTop);\n\t\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\tuserpics.emplace(sublistPeer, sublistPeer->createUserpicView());\n\n\t\tuserpicSt = &st::chatSwitchUserpicSmall;\n\t}\n\tconst auto peer = thread->peer();\n\tconst auto userpic = Ui::CreateChild<Ui::UserpicButton>(\n\t\tthis,\n\t\tpeer,\n\t\t*userpicSt);\n\tuserpic->showSavedMessagesOnSelf(true);\n\tuserpic->show();\n\tuserpic->move(\n\t\t(((width() - userpicSize.width()) / 2)\n\t\t\t+ (userpicSize.width() - userpicSt->size.width())),\n\t\t(st::chatSwitchUserpicTop\n\t\t\t+ (userpicSize.height() - userpicSt->size.height())));\n\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\tuserpics.emplace(peer, peer->createUserpicView());\n\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tthis,\n\t\t((thread->asHistory() && peer->isSelf())\n\t\t\t? tr::lng_saved_messages(tr::now)\n\t\t\t: thread->chatListName()),\n\t\tst::chatSwitchNameLabel);\n\tlabel->setBreakEverywhere(true);\n\tlabel->show();\n\tlabel->resizeToNaturalWidth(\n\t\twidth() - 2 * st::chatSwitchNameSkip);\n\tlabel->move(\n\t\t(width() - label->width()) / 2,\n\t\t(height() + userpic->y() + userpic->height() - label->height()) / 2);\n\n\tshow();\n}\n\nrpl::producer<> Button::selectRequests() const {\n\treturn _selectRequests.events();\n}\n\nvoid Button::setSelected(bool selected, anim::type animated) {\n\tif (_selected == selected) {\n\t\tif (animated == anim::type::instant) {\n\t\t\t_overAnimation.stop();\n\t\t}\n\t\treturn;\n\t}\n\t_selected = selected;\n\tif (animated == anim::type::instant) {\n\t\t_overAnimation.stop();\n\t\tupdate();\n\t} else {\n\t\t_overAnimation.start(\n\t\t\t[=] { update(); },\n\t\t\tselected ? 0. : 1.,\n\t\t\tselected ? 1. : 0.,\n\t\t\tst::slideWrapDuration);\n\t}\n}\n\nvoid Button::paintEvent(QPaintEvent *e) {\n\tconst auto selected = _selected ? 1. : 0.;\n\tconst auto selection = _overAnimation.value(selected);\n\tif (selection <= 0.) {\n\t\treturn;\n\t}\n\n\tauto p = QPainter(this);\n\tauto hq = PainterHighQualityEnabler(p);\n\tconst auto radius = st::boxRadius;\n\tconst auto line = st::chatSwitchSelectLine;\n\tauto pen = st::defaultRoundCheckbox.bgActive->p;\n\tpen.setWidthF(line * selection);\n\tp.setPen(pen);\n\tconst auto r = QRectF(rect()).marginsRemoved(\n\t\t{ line / 2., line / 2., line / 2., line / 2. });\n\tp.drawRoundedRect(r, radius, radius);\n}\n\nvoid Button::mouseMoveEvent(QMouseEvent *e) {\n\tif (!_selected) {\n\t\t_selectRequests.fire({});\n\t}\n}\n\n} // namespace\n\nstruct ChatSwitchProcess::Entry {\n\tnot_null<Data::Thread*> thread;\n\tstd::unique_ptr<Button> button;\n};\n\nChatSwitchProcess::ChatSwitchProcess(\n\tnot_null<Ui::RpWidget*> geometry,\n\tnot_null<Main::Session*> session,\n\tData::Thread *opened)\n: _session(session)\n, _widget(std::make_unique<Ui::RpWidget>(\n\tgeometry->parentWidget() ? geometry->parentWidget() : geometry))\n, _view(Ui::CreateChild<Ui::RpWidget>(_widget.get()))\n, _bg(st::boxRadius, st::boxBg) {\n\tsetupWidget(geometry);\n\tsetupContent(opened);\n\tsetupView();\n}\n\nChatSwitchProcess::~ChatSwitchProcess() {\n\t_session->recentPeers().chatOpenKeepUserpics(std::move(_userpics));\n}\n\nrpl::producer<not_null<Data::Thread*>> ChatSwitchProcess::chosen() const {\n\treturn _chosen.events();\n}\n\nrpl::producer<> ChatSwitchProcess::closeRequests() const {\n\treturn _closeRequests.events();\n}\n\nvoid ChatSwitchProcess::process(const Request &request) {\n\tExpects(_selected < _shownCount);\n\n\tif (request.action == Qt::Key_Escape) {\n\t\t_closeRequests.fire({});\n\t} else if (request.action == Qt::Key_Enter) {\n\t\tif (_selected >= 0) {\n\t\t\t_chosen.fire_copy(_list[_selected]);\n\t\t} else {\n\t\t\t_closeRequests.fire({});\n\t\t}\n\t} else if (request.action == Qt::Key_Tab\n\t\t|| request.action == Qt::Key_Right) {\n\t\tif (_selected < 0 || _selected + 1 >= _shownCount) {\n\t\t\tsetSelected(0);\n\t\t} else {\n\t\t\tsetSelected(_selected + 1);\n\t\t}\n\t} else if (request.action == Qt::Key_Backtab\n\t\t|| request.action == Qt::Key_Left) {\n\t\tif (_selected <= 0) {\n\t\t\tsetSelected(_shownCount - 1);\n\t\t} else {\n\t\t\tsetSelected(_selected - 1);\n\t\t}\n\t} else if (request.action == Qt::Key_Up) {\n\t\tconst auto now = std::max(_selected, 0) - _shownPerRow;\n\t\tconst auto bound = (now < 0) ? (_shownCount + now) : now;\n\t\tsetSelected(bound);\n\t} else if (request.action == Qt::Key_Down) {\n\t\tconst auto now = std::max(_selected, 0) + _shownPerRow;\n\t\tconst auto bound = (now >= _shownCount) ? (now - _shownCount) : now;\n\t\tsetSelected(bound);\n\t} else if (request.action == Qt::Key_Q) {\n\t\tif (_selected >= 0) {\n\t\t\tconst auto thread = _list[_selected];\n\t\t\tthread->session().recentPeers().chatOpenRemove(thread);\n\t\t\tremove(thread);\n\t\t\tCloseInWindows(thread);\n\t\t}\n\t}\n}\n\nvoid ChatSwitchProcess::setSelected(int index) {\n\tif (_selected == index || _list.size() < 2) {\n\t\treturn;\n\t}\n\tif (_selected >= 0) {\n\t\t_entries[_selected].button->setSelected(false);\n\t}\n\t_selected = index;\n\tif (_selected >= 0) {\n\t\t_entries[_selected].button->setSelected(true);\n\t}\n}\n\nvoid ChatSwitchProcess::setupWidget(not_null<Ui::RpWidget*> geometry) {\n\tgeometry->geometryValue(\n\t) | rpl::on_next([=](QRect value) {\n\t\tconst auto parent = geometry->parentWidget();\n\t\t_widget->setGeometry((parent == _widget->parentWidget())\n\t\t\t? value\n\t\t\t: QRect(QPoint(), value.size()));\n\t}, _widget->lifetime());\n\n\t_widget->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::MouseButtonPress) {\n\t\t\tcrl::on_main(_widget.get(), [=] {\n\t\t\t\t_closeRequests.fire({});\n\t\t\t});\n\t\t}\n\t}, _widget->lifetime());\n\n\t_widget->show();\n}\n\nvoid ChatSwitchProcess::setupContent(Data::Thread *opened) {\n\t_list = _session->recentPeers().collectChatOpenHistory();\n\tif (_list.size() < 2) {\n\t\treturn;\n\t}\n\n\tif (opened) {\n\t\tconst auto i = ranges::find(_list, not_null(opened));\n\t\tif (i == end(_list)) {\n\t\t\t_list.insert(begin(_list), opened);\n\t\t} else if (i != begin(_list)) {\n\t\t\tranges::rotate(begin(_list), i, i + 1);\n\t\t}\n\t\t_selected = 0;\n\t}\n\n\tconst auto find = [=](Button *button) {\n\t\treturn ranges::find(_entries, button, [](const Entry &entry) {\n\t\t\treturn entry.button.get();\n\t\t});\n\t};\n\tfor (const auto &thread : _list) {\n\t\tauto button = std::make_unique<Button>(\n\t\t\t_view.get(),\n\t\t\tthread,\n\t\t\t_userpics);\n\t\tconst auto raw = button.get();\n\n\t\traw->selectRequests() | rpl::on_next([=] {\n\t\t\tconst auto i = find(raw);\n\t\t\tsetSelected(int(i - begin(_entries)));\n\t\t}, raw->lifetime());\n\n\t\traw->setClickedCallback([=] {\n\t\t\t_chosen.fire_copy(thread);\n\t\t});\n\n\t\t_entries.push_back({ thread, std::move(button) });\n\n\t\tauto destroyed = thread->asTopic()\n\t\t\t? thread->asTopic()->destroyed()\n\t\t\t: thread->asSublist()\n\t\t\t? thread->asSublist()->destroyed()\n\t\t\t: nullptr;\n\t\tif (!destroyed) {\n\t\t\tcontinue;\n\t\t}\n\t\tstd::move(destroyed) | rpl::on_next([=] {\n\t\t\tremove(thread);\n\t\t}, raw->lifetime());\n\t}\n}\n\nvoid ChatSwitchProcess::remove(not_null<Data::Thread*> thread) {\n\t_list.erase(ranges::remove(_list, thread), end(_list));\n\n\tconst auto i = ranges::find(_entries, thread, &Entry::thread);\n\tif (i != end(_entries)) {\n\t\tconst auto selected = _selected;\n\t\tconst auto index = int(i - begin(_entries));\n\t\tif (_selected > index) {\n\t\t\t--_selected;\n\t\t} else if (_selected == index) {\n\t\t\t_selected = -1;\n\t\t}\n\n\t\t_entries.erase(i);\n\t\tconst auto weak = base::make_weak(_widget.get());\n\t\tlayout(_widget->size());\n\t\tif (weak && _selected < 0 && selected > 0) {\n\t\t\tif (_entries.empty()) {\n\t\t\t\t_closeRequests.fire({});\n\t\t\t} else {\n\t\t\t\tsetSelected(std::min(selected - 1, _shownCount - 1));\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid ChatSwitchProcess::setupView() {\n\t_widget->sizeValue() | rpl::on_next([=](QSize size) {\n\t\tlayout(size);\n\t}, _view->lifetime());\n\t_view->show();\n\n\t_view->paintRequest() | rpl::on_next([=](QRect clip) {\n\t\tif (_outer.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\t\tauto p = QPainter(_view);\n\t\tp.translate(-_shadowed.topLeft());\n\t\tUi::Shadow::paint(p, _outer, _view->width(), st::boxRoundShadow);\n\t\t_bg.paint(p, _outer);\n\t}, _view->lifetime());\n\n\t_view->events() | rpl::on_next([=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::MouseButtonPress) {\n\t\t\te->accept();\n\t\t}\n\t}, _view->lifetime());\n}\n\nvoid ChatSwitchProcess::layout(QSize size) {\n\tconst auto full = QRect(QPoint(), size);\n\tconst auto outer = full.marginsRemoved(st::chatSwitchMargins);\n\tauto inner = outer.marginsRemoved(st::chatSwitchPadding);\n\tconst auto available = inner.width();\n\tconst auto canPerRow = (available / st::chatSwitchSize.width());\n\tconst auto canRows = (canPerRow > 2 * 7)\n\t\t? 1\n\t\t: (canPerRow > 3 * 4)\n\t\t? 2\n\t\t: 3;\n\tif (canPerRow < 1) {\n\t\treturn;\n\t} else if (_list.size() < 2) {\n\t\t_closeRequests.fire({});\n\t\treturn;\n\t}\n\tconst auto count = int(_list.size());\n\t_shownRows = std::min(canRows, (count + canPerRow - 1) / canPerRow);\n\t_shownPerRow = std::min(count / _shownRows, canPerRow);\n\tif (_shownRows > 2) {\n\t\tif (_shownPerRow * 2 > _shownRows * 4) {\n\t\t\t_shownRows = 2;\n\t\t} else if (_shownPerRow > 4) {\n\t\t\t_shownPerRow = 4;\n\t\t}\n\t}\n\tif (_shownRows > 1) {\n\t\tif (_shownPerRow > _shownRows * 7) {\n\t\t\t_shownRows = 1;\n\t\t} else if (_shownPerRow > 7) {\n\t\t\t_shownPerRow = 7;\n\t\t}\n\t}\n\t_shownCount = _shownPerRow * _shownRows;\n\tif (_selected >= _shownCount) {\n\t\t_selected = -1;\n\t}\n\n\tconst auto width = _shownPerRow * st::chatSwitchSize.width();\n\tconst auto height = _shownRows * st::chatSwitchSize.height();\n\n\tsize = QSize(width, height);\n\t_inner = QRect(\n\t\t(full.width() - width) / 2,\n\t\t(full.height() - height) / 2,\n\t\twidth,\n\t\theight);\n\t_outer = _inner.marginsAdded(st::chatSwitchPadding);\n\n\tconst auto padding = st::boxRoundShadow.extend + st::chatSwitchPadding;\n\n\tauto index = 0;\n\tauto top = padding.top();\n\tfor (auto row = 0; row != _shownRows; ++row) {\n\t\tauto left = padding.left();\n\t\tfor (auto column = 0; column != _shownPerRow; ++column) {\n\t\t\tauto &entry = _entries[index++];\n\t\t\tentry.button->moveToLeft(left, top, _inner.width());\n\t\t\tentry.button->show();\n\t\t\tleft += st::chatSwitchSize.width();\n\t\t}\n\t\ttop += st::chatSwitchSize.height();\n\t}\n\tfor (auto i = _shownRows * _shownPerRow; i < count; ++i) {\n\t\t_entries[i].button->hide();\n\t}\n\n\t_shadowed = _outer.marginsAdded(st::boxRoundShadow.extend);\n\t_view->setGeometry(_shadowed);\n}\n\nrpl::lifetime &ChatSwitchProcess::lifetime() {\n\treturn _lifetime;\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_chat_switch_process.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/round_rect.h\"\n#include \"ui/userpic_view.h\"\n\nnamespace Data {\nclass Thread;\n} // namespace Data\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Shortcuts {\nstruct ChatSwitchRequest;\n} // namespace Shortcuts\n\nnamespace Ui {\nclass AbstractButton;\nclass RpWidget;\n} // namespace Ui\n\nnamespace Window {\n\nclass ChatSwitchProcess final {\npublic:\n\t// Create widget in geometry->parentWidget() and geometry->geometry().\n\tChatSwitchProcess(\n\t\tnot_null<Ui::RpWidget*> geometry,\n\t\tnot_null<Main::Session*> session,\n\t\tData::Thread *opened);\n\t~ChatSwitchProcess();\n\n\t[[nodiscard]] rpl::producer<not_null<Data::Thread*>> chosen() const;\n\t[[nodiscard]] rpl::producer<> closeRequests() const;\n\n\tusing Request = Shortcuts::ChatSwitchRequest;\n\tvoid process(const Request &request);\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tstruct Entry;\n\n\tvoid setupWidget(not_null<Ui::RpWidget*> geometry);\n\tvoid setupContent(Data::Thread *opened);\n\tvoid setupView();\n\n\tvoid layout(QSize size);\n\tvoid remove(not_null<Data::Thread*> thread);\n\n\tvoid setSelected(int index);\n\n\tconst not_null<Main::Session*> _session;\n\tconst std::unique_ptr<Ui::RpWidget> _widget;\n\tconst not_null<Ui::RpWidget*> _view;\n\n\tQRect _shadowed;\n\tQRect _outer;\n\tQRect _inner;\n\tUi::RoundRect _bg;\n\n\tbase::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> _userpics;\n\tstd::vector<not_null<Data::Thread*>> _list;\n\tstd::vector<Entry> _entries;\n\n\tint _selected = -1;\n\tint _shownRows = 0;\n\tint _shownCount = 0;\n\tint _shownPerRow = 0;\n\n\trpl::event_stream<not_null<Data::Thread*>> _chosen;\n\trpl::event_stream<> _closeRequests;\n\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_connecting_widget.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_connecting_widget.h\"\n\n#include \"ui/widgets/buttons.h\"\n#include \"ui/effects/radial_animation.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"mtproto/mtp_instance.h\"\n#include \"mtproto/facade.h\"\n#include \"main/main_account.h\"\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/update_checker.h\"\n#include \"boxes/connection_box.h\"\n#include \"boxes/abstract_box.h\"\n#include \"lang/lang_keys.h\"\n#include \"styles/style_window.h\"\n\n#include <QtGui/QWindow>\n\nnamespace Window {\nnamespace {\n\nconstexpr auto kIgnoreStartConnectingFor = crl::time(3000);\nconstexpr auto kConnectingStateDelay = crl::time(1000);\nconstexpr auto kRefreshTimeout = crl::time(200);\nconstexpr auto kMinimalWaitingStateDuration = crl::time(4000);\n\nclass Progress : public Ui::RpWidget {\npublic:\n\tProgress(QWidget *parent);\n\n\trpl::producer<> animationStepRequests() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid animationStep();\n\n\tUi::InfiniteRadialAnimation _animation;\n\trpl::event_stream<> _animationStepRequests;\n\n};\n\nProgress::Progress(QWidget *parent)\n: RpWidget(parent)\n, _animation([=] { animationStep(); }, st::connectingRadial) {\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\tsetAttribute(Qt::WA_TransparentForMouseEvents);\n\tresize(st::connectingRadial.size);\n\t_animation.start(st::connectingRadial.sineDuration);\n}\n\nvoid Progress::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tp.fillRect(e->rect(), st::windowBg);\n\tconst auto &st = st::connectingRadial;\n\tconst auto shift = st.thickness - (st.thickness / 2);\n\t_animation.draw(\n\t\tp,\n\t\t{ shift, shift },\n\t\tQSize(st.size.width() - 2 * shift, st.size.height() - 2 * shift),\n\t\twidth());\n}\n\nvoid Progress::animationStep() {\n\tif (!anim::Disabled()) {\n\t\t_animationStepRequests.fire({});\n\t\tupdate();\n\t}\n}\n\nrpl::producer<> Progress::animationStepRequests() const {\n\treturn _animationStepRequests.events();\n}\n\n} // namespace\n\nclass ConnectionState::Widget : public Ui::AbstractButton {\npublic:\n\tWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Main::Account*> account,\n\t\tconst Layout &layout);\n\n\tQAccessible::Role accessibilityRole() override {\n\t\treturn QAccessible::Role::StatusBar;\n\t}\n\n\tvoid refreshRetryLink(bool hasRetry);\n\tvoid setLayout(const Layout &layout);\n\tvoid setProgressVisibility(bool visible);\n\n\trpl::producer<> refreshStateRequests() const;\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tvoid onStateChanged(State was, StateChangeSource source) override;\n\nprivate:\n\tclass ProxyIcon;\n\tusing State = ConnectionState::State;\n\tusing Layout = ConnectionState::Layout;\n\n\tvoid updateRetryGeometry();\n\tQRect innerRect() const;\n\tQRect contentRect() const;\n\tQRect textRect() const;\n\n\tconst not_null<Main::Account*> _account;\n\tLayout _currentLayout;\n\tbase::unique_qptr<Ui::LinkButton> _retry;\n\tQPointer<Progress> _progress;\n\tQPointer<ProxyIcon> _proxyIcon;\n\trpl::event_stream<> _refreshStateRequests;\n\n};\n\nclass ConnectionState::Widget::ProxyIcon final : public Ui::RpWidget {\npublic:\n\tProxyIcon(QWidget *parent);\n\n\tvoid setToggled(bool toggled);\n\tvoid setOpacity(float64 opacity);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\nprivate:\n\tvoid refreshCacheImages();\n\n\tfloat64 _opacity = 1.;\n\tQPixmap _cacheOn;\n\tQPixmap _cacheOff;\n\tbool _toggled = true;\n\n};\n\nConnectionState::Widget::ProxyIcon::ProxyIcon(QWidget *parent) : RpWidget(parent) {\n\tresize(\n\t\tstd::max(\n\t\t\tst::connectingRadial.size.width(),\n\t\t\tst::connectingProxyOn.width()),\n\t\tstd::max(\n\t\t\tst::connectingRadial.size.height(),\n\t\t\tst::connectingProxyOn.height()));\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\trefreshCacheImages();\n\t}, lifetime());\n\n\trefreshCacheImages();\n}\n\nvoid ConnectionState::Widget::ProxyIcon::refreshCacheImages() {\n\tconst auto prepareCache = [&](const style::icon &icon) {\n\t\tauto image = QImage(\n\t\t\tsize() * style::DevicePixelRatio(),\n\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\timage.setDevicePixelRatio(style::DevicePixelRatio());\n\t\timage.fill(st::windowBg->c);\n\t\t{\n\t\t\tauto p = QPainter(&image);\n\t\t\ticon.paint(\n\t\t\t\tp,\n\t\t\t\t(width() - icon.width()) / 2,\n\t\t\t\t(height() - icon.height()) / 2,\n\t\t\t\twidth());\n\t\t}\n\t\treturn Ui::PixmapFromImage(std::move(image));\n\t};\n\t_cacheOn = prepareCache(st::connectingProxyOn);\n\t_cacheOff = prepareCache(st::connectingProxyOff);\n}\n\nvoid ConnectionState::Widget::ProxyIcon::setToggled(bool toggled) {\n\tif (_toggled != toggled) {\n\t\t_toggled = toggled;\n\t\tupdate();\n\t}\n}\n\nvoid ConnectionState::Widget::ProxyIcon::setOpacity(float64 opacity) {\n\t_opacity = opacity;\n\tif (_opacity == 0.) {\n\t\thide();\n\t} else if (isHidden()) {\n\t\tshow();\n\t}\n\tupdate();\n}\n\nvoid ConnectionState::Widget::ProxyIcon::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tp.setOpacity(_opacity);\n\tp.drawPixmap(0, 0, _toggled ? _cacheOn : _cacheOff);\n}\n\nbool ConnectionState::State::operator==(const State &other) const {\n\treturn (type == other.type)\n\t\t&& (useProxy == other.useProxy)\n\t\t&& (exposed == other.exposed)\n\t\t&& (underCursor == other.underCursor)\n\t\t&& (updateReady == other.updateReady)\n\t\t&& (waitTillRetry == other.waitTillRetry);\n}\n\nConnectionState::ConnectionState(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Main::Account*> account,\n\trpl::producer<bool> shown)\n: _account(account)\n, _parent(parent)\n, _refreshTimer([=] { refreshState(); })\n, _currentLayout(computeLayout(_state)) {\n\trpl::combine(\n\t\tstd::move(shown),\n\t\tvisibility()\n\t) | rpl::on_next([=](bool shown, float64 visible) {\n\t\tif (!shown || visible == 0.) {\n\t\t\t_widget = nullptr;\n\t\t} else if (!_widget) {\n\t\t\tcreateWidget();\n\t\t}\n\t}, _lifetime);\n\n\tif (!Core::UpdaterDisabled()) {\n\t\tCore::UpdateChecker checker;\n\t\trpl::merge(\n\t\t\trpl::single(rpl::empty),\n\t\t\tchecker.ready()\n\t\t) | rpl::on_next([=] {\n\t\t\trefreshState();\n\t\t}, _lifetime);\n\t}\n\n\trpl::combine(\n\t\tCore::App().settings().proxy().connectionTypeValue(),\n\t\trpl::single(QRect()) | rpl::then(_parent->paintRequest())\n\t) | rpl::on_next([=] {\n\t\trefreshState();\n\t}, _lifetime);\n}\n\nvoid ConnectionState::createWidget() {\n\t_widget = base::make_unique_q<Widget>(_parent, _account, _currentLayout);\n\t_widget->setVisible(!_forceHidden);\n\n\tupdateWidth();\n\n\trpl::combine(\n\t\tvisibility(),\n\t\t_parent->heightValue(),\n\t\t_bottomSkip.value()\n\t) | rpl::on_next([=](float64 visible, int height, int skip) {\n\t\t_widget->moveToLeft(0, anim::interpolate(\n\t\t\theight - st::connectingMargin.top(),\n\t\t\theight - _widget->height() - skip,\n\t\t\tvisible));\n\t}, _widget->lifetime());\n\n\t_widget->refreshStateRequests(\n\t) | rpl::on_next([=] {\n\t\trefreshState();\n\t}, _widget->lifetime());\n}\n\nvoid ConnectionState::raise() {\n\tif (_widget) {\n\t\t_widget->raise();\n\t}\n}\n\nvoid ConnectionState::finishAnimating() {\n\tif (_contentWidth.animating()) {\n\t\t_contentWidth.stop();\n\t\tupdateWidth();\n\t}\n\tif (_visibility.animating()) {\n\t\t_visibility.stop();\n\t\tupdateVisibility();\n\t}\n}\n\nvoid ConnectionState::setForceHidden(bool hidden) {\n\t_forceHidden = hidden;\n\tif (_widget) {\n\t\t_widget->setVisible(!hidden);\n\t}\n}\n\nvoid ConnectionState::setBottomSkip(int skip) {\n\t_bottomSkip = skip;\n}\n\nvoid ConnectionState::refreshState() {\n\tusing Checker = Core::UpdateChecker;\n\tconst auto state = [&]() -> State {\n\t\tconst auto exposed = _parent->window()->windowHandle()\n\t\t\t&& _parent->window()->windowHandle()->isExposed();\n\t\tconst auto under = _widget && _widget->isOver();\n\t\tconst auto ready = (Checker().state() == Checker::State::Ready);\n\t\tconst auto state = _account->mtp().dcstate();\n\t\tconst auto proxy = Core::App().settings().proxy().isEnabled();\n\t\tif (state == MTP::ConnectingState\n\t\t\t|| state == MTP::DisconnectedState\n\t\t\t|| (state < 0 && state > -600)) {\n\t\t\treturn { State::Type::Connecting, proxy, exposed, under, ready };\n\t\t} else if (state < 0\n\t\t\t&& state >= -kMinimalWaitingStateDuration\n\t\t\t&& _state.type != State::Type::Waiting) {\n\t\t\treturn { State::Type::Connecting, proxy, exposed, under, ready };\n\t\t} else if (state < 0) {\n\t\t\tconst auto wait = ((-state) / 1000) + 1;\n\t\t\treturn { State::Type::Waiting, proxy, exposed, under, ready, wait };\n\t\t}\n\t\treturn { State::Type::Connected, proxy, exposed, under, ready };\n\t}();\n\tif (state.exposed && state.waitTillRetry > 0) {\n\t\t_refreshTimer.callOnce(kRefreshTimeout);\n\t}\n\tif (state == _state) {\n\t\treturn;\n\t} else if (state.type == State::Type::Connecting\n\t\t&& _state.type == State::Type::Connected) {\n\t\tconst auto now = crl::now();\n\t\tif (!_connectingStartedAt) {\n\t\t\t_connectingStartedAt = now;\n\t\t\t_refreshTimer.callOnce(kConnectingStateDelay);\n\t\t\treturn;\n\t\t}\n\t\tconst auto applyConnectingAt = std::max(\n\t\t\t_connectingStartedAt + kConnectingStateDelay,\n\t\t\tkIgnoreStartConnectingFor);\n\t\tif (now < applyConnectingAt) {\n\t\t\t_refreshTimer.callOnce(applyConnectingAt - now);\n\t\t\treturn;\n\t\t}\n\t}\n\tapplyState(state);\n}\n\nvoid ConnectionState::applyState(const State &state) {\n\tconst auto newLayout = computeLayout(state);\n\tconst auto guard = gsl::finally([&] { updateWidth(); });\n\n\t_state = state;\n\tif (_currentLayout.visible != newLayout.visible) {\n\t\tchangeVisibilityWithLayout(newLayout);\n\t\treturn;\n\t}\n\tif (_currentLayout.contentWidth != newLayout.contentWidth) {\n\t\tif (!_currentLayout.contentWidth\n\t\t\t|| !newLayout.contentWidth\n\t\t\t|| _contentWidth.animating()) {\n\t\t\t_contentWidth.start(\n\t\t\t\t[=] { updateWidth(); },\n\t\t\t\t_currentLayout.contentWidth,\n\t\t\t\tnewLayout.contentWidth,\n\t\t\t\tst::connectingDuration);\n\t\t}\n\t}\n\tconst auto saved = _currentLayout;\n\tsetLayout(newLayout);\n\tif (_currentLayout.text.isEmpty()\n\t\t&& !saved.text.isEmpty()\n\t\t&& _contentWidth.animating()) {\n\t\t_currentLayout.text = saved.text;\n\t\t_currentLayout.textWidth = saved.textWidth;\n\t}\n}\n\nvoid ConnectionState::changeVisibilityWithLayout(const Layout &layout) {\n\tExpects(_currentLayout.visible != layout.visible);\n\n\tconst auto changeLayout = !_currentLayout.visible;\n\t_visibility.start(\n\t\t[=] { updateVisibility(); },\n\t\tlayout.visible ? 0. : 1.,\n\t\tlayout.visible ? 1. : 0.,\n\t\tst::connectingDuration);\n\tif (_contentWidth.animating()) {\n\t\t_contentWidth.start(\n\t\t\t[=] { updateWidth(); },\n\t\t\t_currentLayout.contentWidth,\n\t\t\t(changeLayout ? layout : _currentLayout).contentWidth,\n\t\t\tst::connectingDuration);\n\t}\n\tif (changeLayout) {\n\t\tsetLayout(layout);\n\t} else {\n\t\t_currentLayout.visible = layout.visible;\n\t}\n}\n\nvoid ConnectionState::setLayout(const Layout &layout) {\n\t_currentLayout = layout;\n\tif (_widget) {\n\t\t_widget->setLayout(layout);\n\t}\n\trefreshProgressVisibility();\n}\n\nvoid ConnectionState::refreshProgressVisibility() {\n\tif (_widget) {\n\t\t_widget->setProgressVisibility(_contentWidth.animating()\n\t\t\t|| _currentLayout.progressShown);\n\t}\n}\n\nvoid ConnectionState::updateVisibility() {\n\tconst auto value = currentVisibility();\n\tif (value == 0. && _contentWidth.animating()) {\n\t\t_contentWidth.stop();\n\t\tupdateWidth();\n\t}\n\t_visibilityValues.fire_copy(value);\n}\n\nfloat64 ConnectionState::currentVisibility() const {\n\treturn _visibility.value(_currentLayout.visible ? 1. : 0.);\n}\n\nrpl::producer<float64> ConnectionState::visibility() const {\n\treturn _visibilityValues.events_starting_with(currentVisibility());\n}\n\nauto ConnectionState::computeLayout(const State &state) const -> Layout {\n\tauto result = Layout();\n\tresult.proxyEnabled = state.useProxy;\n\tresult.progressShown = (state.type != State::Type::Connected);\n\tresult.visible = state.exposed\n\t\t&& !state.updateReady\n\t\t&& (state.useProxy\n\t\t\t|| state.type == State::Type::Connecting\n\t\t\t|| state.type == State::Type::Waiting);\n\tswitch (state.type) {\n\tcase State::Type::Connecting:\n\t\tresult.text = state.underCursor\n\t\t\t? tr::lng_connecting(tr::now)\n\t\t\t: QString();\n\t\tbreak;\n\n\tcase State::Type::Waiting:\n\t\tAssert(state.waitTillRetry > 0);\n\t\tresult.text = tr::lng_reconnecting(\n\t\t\ttr::now,\n\t\t\tlt_count,\n\t\t\tstate.waitTillRetry);\n\t\tbreak;\n\t}\n\tresult.textWidth = st::normalFont->width(result.text);\n\tresult.contentWidth = (result.textWidth > 0)\n\t\t? (st::connectingTextPadding.left()\n\t\t\t+ result.textWidth\n\t\t\t+ st::connectingTextPadding.right())\n\t\t: 0;\n\tif (state.type == State::Type::Waiting) {\n\t\tresult.contentWidth += st::connectingRetryLink.padding.left()\n\t\t\t+ st::connectingRetryLink.font->width(\n\t\t\t\ttr::lng_reconnecting_try_now(tr::now))\n\t\t\t+ st::connectingRetryLink.padding.right();\n\t}\n\tresult.hasRetry = (state.type == State::Type::Waiting);\n\treturn result;\n}\n\nvoid ConnectionState::updateWidth() {\n\tconst auto current = _contentWidth.value(_currentLayout.contentWidth);\n\tconst auto height = st::connectingLeft.height();\n\tconst auto desired = QRect(0, 0, current, height).marginsAdded(\n\t\tstyle::margins(\n\t\t\tst::connectingLeft.width(),\n\t\t\t0,\n\t\t\tst::connectingRight.width(),\n\t\t\t0)\n\t).marginsAdded(\n\t\tst::connectingMargin\n\t);\n\tif (_widget) {\n\t\t_widget->resize(desired.size());\n\t\t_widget->update();\n\t}\n\trefreshProgressVisibility();\n}\n\nConnectionState::Widget::Widget(\n\tQWidget *parent,\n\tnot_null<Main::Account*> account,\n\tconst Layout &layout)\n: AbstractButton(parent)\n, _account(account)\n, _currentLayout(layout) {\n\t_proxyIcon = Ui::CreateChild<ProxyIcon>(this);\n\t_progress = Ui::CreateChild<Progress>(this);\n\n\taddClickHandler([=] {\n\t\tUi::show(ProxiesBoxController::CreateOwningBox(account));\n\t});\n\n\t_progress->animationStepRequests(\n\t) | rpl::on_next([=] {\n\t\t_refreshStateRequests.fire({});\n\t}, _progress->lifetime());\n}\n\nvoid ConnectionState::Widget::onStateChanged(\n\t\tAbstractButton::State was,\n\t\tStateChangeSource source) {\n\tUi::PostponeCall(crl::guard(this, [=] {\n\t\t_refreshStateRequests.fire({});\n\t}));\n}\n\nrpl::producer<> ConnectionState::Widget::refreshStateRequests() const {\n\treturn _refreshStateRequests.events();\n}\n\nvoid ConnectionState::Widget::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\tPainterHighQualityEnabler hq(p);\n\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(st::windowBg);\n\tconst auto inner = innerRect();\n\tconst auto content = contentRect();\n\tconst auto text = textRect();\n\tconst auto left = inner.topLeft();\n\tconst auto right = content.topLeft() + QPoint(content.width(), 0);\n\tst::connectingLeftShadow.paint(p, left, width());\n\tst::connectingLeft.paint(p, left, width());\n\tst::connectingRightShadow.paint(p, right, width());\n\tst::connectingRight.paint(p, right, width());\n\tst::connectingBodyShadow.fill(p, content);\n\tst::connectingBody.fill(p, content);\n\n\tconst auto available = text.width();\n\tif (available > 0 && !_currentLayout.text.isEmpty()) {\n\t\tp.setFont(st::normalFont);\n\t\tp.setPen(st::windowSubTextFg);\n\t\tif (available >= _currentLayout.textWidth) {\n\t\t\tp.drawTextLeft(\n\t\t\t\ttext.x(),\n\t\t\t\ttext.y(),\n\t\t\t\twidth(),\n\t\t\t\t_currentLayout.text,\n\t\t\t\t_currentLayout.textWidth);\n\t\t} else {\n\t\t\tp.drawTextLeft(\n\t\t\t\ttext.x(),\n\t\t\t\ttext.y(),\n\t\t\t\twidth(),\n\t\t\t\tst::normalFont->elided(_currentLayout.text, available));\n\t\t}\n\t}\n}\n\nQRect ConnectionState::Widget::innerRect() const {\n\treturn rect().marginsRemoved(\n\t\tst::connectingMargin\n\t);\n}\n\nQRect ConnectionState::Widget::contentRect() const {\n\treturn innerRect().marginsRemoved(style::margins(\n\t\tst::connectingLeft.width(),\n\t\t0,\n\t\tst::connectingRight.width(),\n\t\t0));\n}\n\nQRect ConnectionState::Widget::textRect() const {\n\treturn contentRect().marginsRemoved(\n\t\tst::connectingTextPadding\n\t);\n}\n\nvoid ConnectionState::Widget::resizeEvent(QResizeEvent *e) {\n\t{\n\t\tconst auto xShift = (height() - _progress->width()) / 2;\n\t\tconst auto yShift = (height() - _progress->height()) / 2;\n\t\t_progress->moveToLeft(xShift, yShift);\n\t}\n\t{\n\t\tconst auto xShift = (height() - _proxyIcon->width()) / 2;\n\t\tconst auto yShift = (height() - _proxyIcon->height()) / 2;\n\t\t_proxyIcon->moveToRight(xShift, yShift);\n\t}\n\tupdateRetryGeometry();\n}\n\nvoid ConnectionState::Widget::updateRetryGeometry() {\n\tif (!_retry) {\n\t\treturn;\n\t}\n\tconst auto text = textRect();\n\tconst auto available = text.width() - _currentLayout.textWidth;\n\tif (available <= 0) {\n\t\t_retry->hide();\n\t} else {\n\t\t_retry->show();\n\t\t_retry->resize(\n\t\t\tstd::min(available, _retry->naturalWidth()),\n\t\t\tinnerRect().height());\n\t\t_retry->moveToLeft(\n\t\t\ttext.x() + text.width() - _retry->width(),\n\t\t\tst::connectingMargin.top());\n\t}\n}\n\nvoid ConnectionState::Widget::setLayout(const Layout &layout) {\n\t_currentLayout = layout;\n\t_proxyIcon->setToggled(_currentLayout.proxyEnabled);\n\trefreshRetryLink(_currentLayout.hasRetry);\n\tsetAccessibleName(_currentLayout.text);\n}\n\nvoid ConnectionState::Widget::setProgressVisibility(bool visible) {\n\tif (_progress->isHidden() == visible) {\n\t\t_progress->setVisible(visible);\n\t}\n}\n\nvoid ConnectionState::Widget::refreshRetryLink(bool hasRetry) {\n\tif (hasRetry && !_retry) {\n\t\t_retry = base::make_unique_q<Ui::LinkButton>(\n\t\t\tthis,\n\t\t\ttr::lng_reconnecting_try_now(tr::now),\n\t\t\tst::connectingRetryLink);\n\t\t_retry->addClickHandler([=] {\n\t\t\t_account->mtp().restart();\n\t\t});\n\t\tupdateRetryGeometry();\n\t} else if (!hasRetry) {\n\t\t_retry = nullptr;\n\t}\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_connecting_widget.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"base/unique_qptr.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace Ui {\nclass RpWidget;\n} // namespace Ui\n\nnamespace Main {\nclass Account;\n} // namespace Main\n\nnamespace Window {\n\nclass ConnectionState {\npublic:\n\tConnectionState(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Main::Account*> account,\n\t\trpl::producer<bool> shown);\n\n\tvoid raise();\n\tvoid setForceHidden(bool hidden);\n\tvoid setBottomSkip(int skip);\n\n\trpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tclass Widget;\n\tstruct State {\n\t\tenum class Type {\n\t\t\tConnected,\n\t\t\tConnecting,\n\t\t\tWaiting,\n\t\t};\n\t\tType type = Type::Connected;\n\t\tbool useProxy = false;\n\t\tbool exposed = false;\n\t\tbool underCursor = false;\n\t\tbool updateReady = false;\n\t\tint waitTillRetry = 0;\n\n\t\tbool operator==(const State &other) const;\n\n\t};\n\tstruct Layout {\n\t\tbool visible = false;\n\t\tbool hasRetry = false;\n\t\tbool proxyEnabled = false;\n\t\tbool progressShown = false;\n\t\tint contentWidth = 0;\n\t\tQString text;\n\t\tint textWidth = 0;\n\n\t};\n\n\tvoid createWidget();\n\tvoid finishAnimating();\n\tvoid refreshState();\n\tvoid applyState(const State &state);\n\tvoid changeVisibilityWithLayout(const Layout &layout);\n\tLayout computeLayout(const State &state) const;\n\tvoid setLayout(const Layout &layout);\n\tfloat64 currentVisibility() const;\n\trpl::producer<float64> visibility() const;\n\tvoid updateWidth();\n\tvoid updateVisibility();\n\tvoid refreshProgressVisibility();\n\n\tconst not_null<Main::Account*> _account;\n\tnot_null<Ui::RpWidget*> _parent;\n\trpl::variable<int> _bottomSkip;\n\tbase::unique_qptr<Widget> _widget;\n\tbool _forceHidden = false;\n\tbase::Timer _refreshTimer;\n\tState _state;\n\tLayout _currentLayout;\n\tcrl::time _connectingStartedAt = 0;\n\tUi::Animations::Simple _contentWidth;\n\tUi::Animations::Simple _visibility;\n\n\trpl::event_stream<float64> _visibilityValues;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_controller.h\"\n\n#include \"api/api_updates.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"export/export_manager.h\"\n#include \"ui/platform/ui_platform_window.h\"\n#include \"platform/platform_window_title.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"main/main_app_config.h\"\n#include \"media/view/media_view_open_common.h\"\n#include \"lang/lang_keys.h\"\n#include \"intro/intro_widget.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/emoji_config.h\"\n#include \"chat_helpers/emoji_sets_manager.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/themes/window_theme_editor.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"data/components/promo_suggestions.h\"\n#include \"data/data_thread.h\"\n#include \"settings/settings_common.h\"\n#include \"apiwrap.h\" // ApiWrap::acceptTerms.\n#include \"styles/style_layers.h\"\n\n#include <QtGui/QWindow>\n#include <QtGui/QScreen>\n\nnamespace Window {\nnamespace {\n\nclass Show final : public Ui::Show {\npublic:\n\texplicit Show(not_null<Controller*> window);\n\n\tvoid showOrHideBoxOrLayer(\n\t\tstd::variant<\n\t\t\tv::null_t,\n\t\t\tobject_ptr<Ui::BoxContent>,\n\t\t\tstd::unique_ptr<Ui::LayerWidget>> &&layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) const override;\n\t[[nodiscard]] not_null<QWidget*> toastParent() const override;\n\t[[nodiscard]] bool valid() const override;\n\toperator bool() const override;\n\nprivate:\n\tconst base::weak_ptr<Controller> _window;\n\n};\n\nShow::Show(not_null<Controller*> window)\n: _window(base::make_weak(window)) {\n}\n\nvoid Show::showOrHideBoxOrLayer(\n\t\tstd::variant<\n\t\t\tv::null_t,\n\t\t\tobject_ptr<Ui::BoxContent>,\n\t\t\tstd::unique_ptr<Ui::LayerWidget>> &&layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) const {\n\tif (const auto window = _window.get()) {\n\t\twindow->widget()->showOrHideBoxOrLayer(\n\t\t\tstd::move(layer),\n\t\t\toptions,\n\t\t\tanimated);\n\t}\n}\n\nnot_null<QWidget*> Show::toastParent() const {\n\tconst auto window = _window.get();\n\tAssert(window != nullptr);\n\treturn window->widget()->bodyWidget();\n}\n\nbool Show::valid() const {\n\treturn !_window.empty();\n}\n\nShow::operator bool() const {\n\treturn valid();\n}\n\n} // namespace\n\nController::Controller() : Controller(CreateArgs{ nullptr }) {\n}\n\nController::Controller(SeparateId id, MsgId showAtMsgId)\n: Controller(CreateArgs{ id }) {\n\tif (id) {\n\t\tshowAccount(id.account, showAtMsgId);\n\t}\n}\n\nController::Controller(CreateArgs &&args)\n: _id(args.id)\n, _isActiveTimer([=] { updateIsActive(); })\n, _widget(this)\n, _adaptive(std::make_unique<Adaptive>()) {\n\t_widget.init();\n}\n\nController::~Controller() {\n\t// We want to delete all widgets before the _sessionController.\n\t_widget.ui_hideSettingsAndLayer(anim::type::instant);\n\t_widget.clearWidgets();\n\t_accountLifetime.destroy();\n\t_sessionControllerValue = nullptr;\n\t_sessionController = nullptr;\n}\n\nSeparateId Controller::id() const {\n\treturn _id;\n}\n\nbool Controller::isPrimary() const {\n\treturn _id.primary();\n}\n\nMain::Account &Controller::account() const {\n\tExpects(_id.account != nullptr);\n\n\treturn *_id.account;\n}\n\nvoid Controller::showAccount(not_null<Main::Account*> account) {\n\tshowAccount(account, ShowAtUnreadMsgId);\n}\n\nvoid Controller::showAccount(\n\t\tnot_null<Main::Account*> account,\n\t\tMsgId singlePeerShowAtMsgId) {\n\tExpects(isPrimary() || _id.account == account);\n\n\tconst auto prevSession = maybeSession();\n\tconst auto prevSessionUniqueId = prevSession\n\t\t? prevSession->uniqueId()\n\t\t: 0;\n\t_accountLifetime.destroy();\n\t_id.account = account;\n\tCore::App().checkWindowId(this);\n\n\tconst auto updateOnlineOfPrevSesssion = crl::guard(account, [=] {\n\t\tif (!prevSessionUniqueId) {\n\t\t\treturn;\n\t\t}\n\t\tfor (auto &[index, account] : _id.account->domain().accounts()) {\n\t\t\tif (const auto anotherSession = account->maybeSession()) {\n\t\t\t\tif (anotherSession->uniqueId() == prevSessionUniqueId) {\n\t\t\t\t\tanotherSession->updates().updateOnline(crl::now());\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\n\tif (!isPrimary()) {\n\t\t_id.account->sessionChanges(\n\t\t) | rpl::on_next([=](Main::Session *session) {\n\t\t\tCore::App().closeWindow(this);\n\t\t}, _accountLifetime);\n\t}\n\n\t_id.account->sessionValue(\n\t) | rpl::on_next([=](Main::Session *session) {\n\t\tconst auto was = base::take(_sessionController);\n\t\t_sessionController = session\n\t\t\t? std::make_unique<SessionController>(session, this)\n\t\t\t: nullptr;\n\t\t_sessionControllerValue = _sessionController.get();\n\n\t\tauto oldContentCache = _widget.grabForSlideAnimation();\n\t\t_widget.updateWindowIcon();\n\t\tif (session) {\n\t\t\tsetupSideBar();\n\t\t\tsetupMain(singlePeerShowAtMsgId, std::move(oldContentCache));\n\n\t\t\tsession->updates().isIdleValue(\n\t\t\t) | rpl::filter([=](bool idle) {\n\t\t\t\treturn !idle;\n\t\t\t}) | rpl::on_next([=] {\n\t\t\t\twidget()->checkActivation();\n\t\t\t}, _sessionController->lifetime());\n\n\t\t\tsession->termsLockValue(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\tcheckLockByTerms();\n\t\t\t\t_widget.updateGlobalMenu();\n\t\t\t}, _sessionController->lifetime());\n\n\t\t\twidget()->setInnerFocus();\n\n\t\t\t_sessionController->activeChatChanges(\n\t\t\t) | rpl::on_next([=] {\n\t\t\t\t_widget.updateTitle();\n\t\t\t}, _sessionController->lifetime());\n\t\t\t_widget.updateTitle();\n\n\t\t\tif (session->promoSuggestions().setupEmailState()\n\t\t\t\t!= Data::SetupEmailState::None) {\n\t\t\t\t_widget.setupSetupEmailLock();\n\t\t\t}\n\n\t\t\tsession->updates().updateOnline(crl::now());\n\t\t} else {\n\t\t\tsideBarChanged();\n\t\t\tsetupIntro(std::move(oldContentCache));\n\t\t\t_widget.updateGlobalMenu();\n\t\t}\n\n\t\tcrl::on_main(updateOnlineOfPrevSesssion);\n\t}, _accountLifetime);\n}\n\nvoid Controller::setupSideBar() {\n\tExpects(_sessionController != nullptr);\n\n\tif (!isPrimary()) {\n\t\treturn;\n\t}\n\t_sessionController->filtersMenuChanged(\n\t) | rpl::on_next([=] {\n\t\tsideBarChanged();\n\t}, _sessionController->lifetime());\n\n\tif (_sessionController->session().settings().dialogsFiltersEnabled()\n\t\t&& _sessionController->enoughSpaceForFilters()\n\t\t&& !Core::App().settings().chatFiltersHorizontal()) {\n\t\t_sessionController->toggleFiltersMenu(true);\n\t} else {\n\t\tsideBarChanged();\n\t}\n}\n\nvoid Controller::checkLockByTerms() {\n\tconst auto data = account().sessionExists()\n\t\t? account().session().termsLocked()\n\t\t: std::nullopt;\n\tif (!data) {\n\t\tif (_termsBox) {\n\t\t\t_termsBox->closeBox();\n\t\t}\n\t\treturn;\n\t}\n\thideSettingsAndLayer(anim::type::instant);\n\tconst auto box = show(Box<TermsBox>(\n\t\t*data,\n\t\ttr::lng_terms_agree(),\n\t\ttr::lng_terms_decline()));\n\n\tbox->setCloseByEscape(false);\n\tbox->setCloseByOutsideClick(false);\n\n\tconst auto id = data->id;\n\tbox->agreeClicks(\n\t) | rpl::on_next([=] {\n\t\tconst auto mention = box ? box->lastClickedMention() : QString();\n\t\tbox->closeBox();\n\t\tif (const auto session = account().maybeSession()) {\n\t\t\tsession->api().acceptTerms(id);\n\t\t\tsession->unlockTerms();\n\t\t\tif (!mention.isEmpty()) {\n\t\t\t\tMentionClickHandler(mention).onClick({});\n\t\t\t}\n\t\t}\n\t}, box->lifetime());\n\n\tbox->cancelClicks(\n\t) | rpl::on_next([=] {\n\t\tshowTermsDecline();\n\t}, box->lifetime());\n\n\tQObject::connect(box.get(), &QObject::destroyed, [=] {\n\t\tcrl::on_main(widget(), [=] { checkLockByTerms(); });\n\t});\n\n\t_termsBox = box;\n}\n\nvoid Controller::showTermsDecline() {\n\tconst auto box = show(Box<Window::TermsBox>(\n\t\tTextWithEntities{ tr::lng_terms_update_sorry(tr::now) },\n\t\ttr::lng_terms_decline_and_delete(),\n\t\ttr::lng_terms_back(),\n\t\ttrue));\n\n\tbox->agreeClicks(\n\t) | rpl::on_next([=] {\n\t\tif (box) {\n\t\t\tbox->closeBox();\n\t\t}\n\t\tshowTermsDelete();\n\t}, box->lifetime());\n\n\tbox->cancelClicks(\n\t) | rpl::on_next([=] {\n\t\tif (box) {\n\t\t\tbox->closeBox();\n\t\t}\n\t}, box->lifetime());\n}\n\nvoid Controller::showTermsDelete() {\n\tconst auto deleteByTerms = [=] {\n\t\tif (const auto session = account().maybeSession()) {\n\t\t\tsession->termsDeleteNow();\n\t\t} else {\n\t\t\thideLayer();\n\t\t}\n\t};\n\tshow(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_terms_delete_warning(),\n\t\t.confirmed = deleteByTerms,\n\t\t.confirmText = tr::lng_terms_delete_now(),\n\t\t.confirmStyle = &st::attentionBoxButton,\n\t}));\n}\n\nvoid Controller::firstShow() {\n\t_widget.firstShow();\n}\n\nvoid Controller::finishFirstShow() {\n\t_widget.finishFirstShow();\n\tcheckThemeEditor();\n}\n\nMain::Session *Controller::maybeSession() const {\n\treturn _id.account ? _id.account->maybeSession() : nullptr;\n}\n\nauto Controller::sessionControllerValue() const\n-> rpl::producer<SessionController*> {\n\treturn _sessionControllerValue.value();\n}\n\nauto Controller::sessionControllerChanges() const\n-> rpl::producer<SessionController*> {\n\treturn _sessionControllerValue.changes();\n}\n\nbool Controller::locked() const {\n\tif (Core::App().passcodeLocked()/* || Core::App().setupEmailLocked()*/) {\n\t\treturn true;\n\t} else if (const auto controller = sessionController()) {\n\t\treturn controller->session().termsLocked().has_value();\n\t}\n\treturn false;\n}\n\nvoid Controller::checkThemeEditor() {\n\tusing namespace Window::Theme;\n\n\tif (const auto editing = Background()->editingTheme()) {\n\t\tshowRightColumn(Box<Editor>(this, *editing));\n\t}\n}\n\nvoid Controller::setupPasscodeLock() {\n\t_widget.setupPasscodeLock();\n}\n\nvoid Controller::clearPasscodeLock() {\n\tif (!_id) {\n\t\tshowAccount(&Core::App().activeAccount());\n\t} else {\n\t\t_widget.clearPasscodeLock();\n\t}\n}\n\nvoid Controller::setupSetupEmailLock() {\n\tif (const auto &session = _sessionController) {\n\t\tif (session->session().promoSuggestions().setupEmailState()\n\t\t\t!= Data::SetupEmailState::None) {\n\t\t\t_widget.setupSetupEmailLock();\n\t\t}\n\t}\n}\n\nvoid Controller::clearSetupEmailLock() {\n\t_widget.clearSetupEmailLock();\n}\n\nvoid Controller::setupIntro(QPixmap oldContentCache) {\n\tconst auto point = Core::App().domain().maybeLastOrSomeAuthedAccount()\n\t\t? Intro::EnterPoint::Qr\n\t\t: Intro::EnterPoint::Start;\n\t_widget.setupIntro(point, std::move(oldContentCache));\n}\n\nvoid Controller::setupMain(\n\t\tMsgId singlePeerShowAtMsgId,\n\t\tQPixmap oldContentCache) {\n\tExpects(_sessionController != nullptr);\n\n\t_widget.setupMain(singlePeerShowAtMsgId, std::move(oldContentCache));\n\n\tif (const auto id = Ui::Emoji::NeedToSwitchBackToId()) {\n\t\tUi::Emoji::LoadAndSwitchTo(&_sessionController->session(), id);\n\t}\n}\n\nvoid Controller::showSettings() {\n\t_widget.showSettings();\n}\n\nint Controller::verticalShadowTop() const {\n\treturn (Platform::NativeTitleRequiresShadow()\n\t\t&& Ui::Platform::NativeWindowFrameSupported()\n\t\t&& Core::App().settings().nativeWindowFrame())\n\t\t? st::lineWidth\n\t\t: 0;\n}\n\nvoid Controller::showToast(Ui::Toast::Config &&config) {\n\tShow(this).showToast(std::move(config));\n}\n\nvoid Controller::showToast(TextWithEntities &&text, crl::time duration) {\n\tShow(this).showToast(std::move(text), duration);\n}\n\nvoid Controller::showToast(const QString &text, crl::time duration) {\n\tShow(this).showToast(text, duration);\n}\n\nvoid Controller::showLayer(\n\t\tstd::unique_ptr<Ui::LayerWidget> &&layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) {\n\t_widget.showOrHideBoxOrLayer(std::move(layer), options, animated);\n}\n\nvoid Controller::showBox(\n\t\tobject_ptr<Ui::BoxContent> content,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) {\n\t_widget.showOrHideBoxOrLayer(std::move(content), options, animated);\n}\n\nvoid Controller::showRightColumn(object_ptr<Ui::RpWidget> widget) {\n\t_widget.showRightColumn(std::move(widget));\n}\n\nvoid Controller::hideLayer(anim::type animated) {\n\t_widget.showOrHideBoxOrLayer(v::null, Ui::LayerOption::CloseOther, animated);\n}\n\nvoid Controller::hideSettingsAndLayer(anim::type animated) {\n\t_widget.ui_hideSettingsAndLayer(animated);\n}\n\nbool Controller::isLayerShown() const {\n\treturn _widget.ui_isLayerShown();\n}\n\nvoid Controller::sideBarChanged() {\n\t_widget.recountGeometryConstraints();\n}\n\nvoid Controller::activate() {\n\t_widget.activate();\n}\n\nvoid Controller::updateIsActiveFocus() {\n\t_isActiveTimer.callOnce(sessionController()\n\t\t? sessionController()->session().serverConfig().onlineFocusTimeout\n\t\t: crl::time(1000));\n}\n\nvoid Controller::updateIsActiveBlur() {\n\t_isActiveTimer.callOnce(sessionController()\n\t\t? sessionController()->session().serverConfig().offlineBlurTimeout\n\t\t: crl::time(1000));\n}\n\nvoid Controller::updateIsActive() {\n\t_widget.updateIsActive();\n}\n\nvoid Controller::minimize() {\n\tif (Core::App().settings().workMode()\n\t\t\t== Core::Settings::WorkMode::TrayOnly) {\n\t\t_widget.minimizeToTray();\n\t} else {\n\t\t_widget.setWindowState(_widget.windowState() | Qt::WindowMinimized);\n\t}\n}\n\nvoid Controller::close() {\n\t_widget.close();\n}\n\nvoid Controller::preventOrInvoke(Fn<void()> &&callback) {\n\t_widget.preventOrInvoke(std::move(callback));\n}\n\nvoid Controller::invokeForSessionController(\n\t\tnot_null<Main::Account*> account,\n\t\tPeerData *singlePeer,\n\t\tFn<void(not_null<SessionController*>)> &&callback) {\n\tconst auto separateWindow = singlePeer\n\t\t? Core::App().separateWindowFor(not_null(singlePeer))\n\t\t: nullptr;\n\tconst auto separateSession = separateWindow\n\t\t? separateWindow->sessionController()\n\t\t: nullptr;\n\tif (separateSession) {\n\t\treturn callback(separateSession);\n\t}\n\tconst auto accountWindow = account\n\t\t? Core::App().separateWindowFor(not_null(account))\n\t\t: nullptr;\n\tconst auto accountSession = accountWindow\n\t\t? accountWindow->sessionController()\n\t\t: nullptr;\n\tif (accountSession) {\n\t\treturn callback(accountSession);\n\t}\n\t_id.account->domain().activate(std::move(account));\n\tif (_sessionController) {\n\t\tcallback(_sessionController.get());\n\t}\n}\n\nQPoint Controller::getPointForCallPanelCenter() const {\n\treturn _widget.isActive()\n\t\t? _widget.geometry().center()\n\t\t: _widget.screen()->geometry().center();\n}\n\nvoid Controller::showLogoutConfirmation() {\n\tconst auto account = Core::App().passcodeLocked()\n\t\t? nullptr\n\t\t: sessionController()\n\t\t? &sessionController()->session().account()\n\t\t: nullptr;\n\tconst auto weak = base::make_weak(account);\n\tconst auto callback = [=](Fn<void()> close) {\n\t\tif (!account || weak) {\n\t\t\tCore::App().logoutWithChecks(account);\n\t\t}\n\t\tif (close) {\n\t\t\tclose();\n\t\t}\n\t};\n\tshow(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_sure_logout(),\n\t\t.confirmed = callback,\n\t\t.confirmText = tr::lng_settings_logout(),\n\t\t.confirmStyle = &st::attentionBoxButton,\n\t}));\n}\n\nWindow::Adaptive &Controller::adaptive() const {\n\treturn *_adaptive;\n}\n\nvoid Controller::openInMediaView(Media::View::OpenRequest &&request) {\n\t_openInMediaViewRequests.fire(std::move(request));\n}\n\nauto Controller::openInMediaViewRequests() const\n-> rpl::producer<Media::View::OpenRequest> {\n\treturn _openInMediaViewRequests.events();\n}\n\nvoid Controller::setDefaultFloatPlayerDelegate(\n\t\tnot_null<Media::Player::FloatDelegate*> delegate) {\n\t_defaultFloatPlayerDelegate = delegate;\n\t_replacementFloatPlayerDelegate = nullptr;\n\t_floatPlayerDelegate = delegate;\n}\n\nvoid Controller::replaceFloatPlayerDelegate(\n\t\tnot_null<Media::Player::FloatDelegate*> replacement) {\n\tExpects(_defaultFloatPlayerDelegate != nullptr);\n\n\t_replacementFloatPlayerDelegate = replacement;\n\t_floatPlayerDelegate = replacement;\n}\n\nvoid Controller::restoreFloatPlayerDelegate(\n\t\tnot_null<Media::Player::FloatDelegate*> replacement) {\n\tExpects(_defaultFloatPlayerDelegate != nullptr);\n\n\tif (_replacementFloatPlayerDelegate == replacement) {\n\t\t_replacementFloatPlayerDelegate = nullptr;\n\t\t_floatPlayerDelegate = _defaultFloatPlayerDelegate;\n\t}\n}\n\nauto Controller::floatPlayerDelegate() const -> FloatDelegate* {\n\treturn _floatPlayerDelegate.current();\n}\n\nauto Controller::floatPlayerDelegateValue() const\n-> rpl::producer<FloatDelegate*> {\n\treturn _floatPlayerDelegate.value();\n}\n\nstd::shared_ptr<Ui::Show> Controller::uiShow() {\n\treturn std::make_shared<Show>(this);\n}\n\nvoid Controller::setHighlightControlId(const QString &id) {\n\t_highlightControlId = id;\n}\n\nQString Controller::highlightControlId() const {\n\treturn _highlightControlId;\n}\n\nbool Controller::takeHighlightControlId(const QString &id) {\n\tif (_highlightControlId == id) {\n\t\t_highlightControlId = QString();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid Controller::checkHighlightControl(\n\t\tconst QString &id,\n\t\tQWidget *widget,\n\t\tSettings::HighlightArgs &&args) {\n\tif (widget && takeHighlightControlId(id)) {\n\t\tSettings::HighlightWidget(widget, std::move(args));\n\t}\n}\n\nvoid Controller::checkHighlightControl(const QString &id, QWidget *widget) {\n\tcheckHighlightControl(id, widget, {});\n}\n\nrpl::lifetime &Controller::lifetime() {\n\treturn _lifetime;\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mainwindow.h\"\n#include \"window/window_adaptive.h\"\n#include \"window/window_separate_id.h\"\n\nnamespace Main {\nclass Account;\nclass Session;\n} // namespace Main\n\nnamespace Ui {\nclass Show;\n} // namespace Ui\n\nnamespace Ui::Toast {\nstruct Config;\n} // namespace Ui::Toast\n\nnamespace Media::View {\nstruct OpenRequest;\n} // namespace Media::View\n\nnamespace Media::Player {\nclass FloatDelegate;\n} // namespace Media::Player\n\nnamespace Settings {\nstruct HighlightArgs;\n} // namespace Settings\n\nnamespace Window {\n\nclass Controller final : public base::has_weak_ptr {\npublic:\n\tController();\n\tController(SeparateId id, MsgId showAtMsgId);\n\t~Controller();\n\n\tController(const Controller &other) = delete;\n\tController &operator=(const Controller &other) = delete;\n\n\tvoid showAccount(not_null<Main::Account*> account);\n\t[[nodiscard]] SeparateId id() const;\n\t[[nodiscard]] bool isPrimary() const;\n\n\t[[nodiscard]] not_null<::MainWindow*> widget() {\n\t\treturn &_widget;\n\t}\n\t[[nodiscard]] Main::Account &account() const;\n\t[[nodiscard]] Main::Session *maybeSession() const;\n\t[[nodiscard]] SessionController *sessionController() const {\n\t\treturn _sessionController.get();\n\t}\n\t[[nodiscard]] auto sessionControllerValue() const\n\t\t-> rpl::producer<SessionController*>;\n\t[[nodiscard]] auto sessionControllerChanges() const\n\t\t-> rpl::producer<SessionController*>;\n\t[[nodiscard]] bool locked() const;\n\n\t[[nodiscard]] Adaptive &adaptive() const;\n\n\tvoid firstShow();\n\tvoid finishFirstShow();\n\n\tvoid setupPasscodeLock();\n\tvoid clearPasscodeLock();\n\tvoid setupSetupEmailLock();\n\tvoid clearSetupEmailLock();\n\n\tvoid showLogoutConfirmation();\n\n\tvoid showSettings();\n\n\t[[nodiscard]] int verticalShadowTop() const;\n\n\tvoid showToast(Ui::Toast::Config &&config);\n\tvoid showToast(TextWithEntities &&text, crl::time duration = 0);\n\tvoid showToast(const QString &text, crl::time duration = 0);\n\n\tvoid showRightColumn(object_ptr<Ui::RpWidget> widget);\n\n\tvoid showBox(\n\t\tobject_ptr<Ui::BoxContent> content,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated);\n\tvoid showLayer(\n\t\tstd::unique_ptr<Ui::LayerWidget> &&layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated = anim::type::normal);\n\n\tvoid hideLayer(anim::type animated = anim::type::normal);\n\tvoid hideSettingsAndLayer(anim::type animated = anim::type::normal);\n\t[[nodiscard]] bool isLayerShown() const;\n\n\ttemplate <\n\t\ttypename BoxType,\n\t\ttypename = std::enable_if_t<\n\t\t\tstd::is_base_of_v<Ui::BoxContent, BoxType>>>\n\t\tbase::weak_qptr<BoxType> show(\n\t\t\tobject_ptr<BoxType> content,\n\t\t\tUi::LayerOptions options = Ui::LayerOption::KeepOther,\n\t\t\tanim::type animated = anim::type()) {\n\t\tauto result = base::weak_qptr<BoxType>(content.data());\n\t\tshowBox(std::move(content), options, animated);\n\t\treturn result;\n\t}\n\n\tvoid activate();\n\tvoid updateIsActiveFocus();\n\tvoid updateIsActiveBlur();\n\tvoid updateIsActive();\n\tvoid minimize();\n\tvoid close();\n\n\tvoid preventOrInvoke(Fn<void()> &&callback);\n\n\tvoid invokeForSessionController(\n\t\tnot_null<Main::Account*> account,\n\t\tPeerData *singlePeer,\n\t\tFn<void(not_null<SessionController*>)> &&callback);\n\n\tvoid openInMediaView(Media::View::OpenRequest &&request);\n\t[[nodiscard]] auto openInMediaViewRequests() const\n\t-> rpl::producer<Media::View::OpenRequest>;\n\n\t[[nodiscard]] QPoint getPointForCallPanelCenter() const;\n\n\tusing FloatDelegate = Media::Player::FloatDelegate;\n\tvoid setDefaultFloatPlayerDelegate(\n\t\tnot_null<Media::Player::FloatDelegate*> delegate);\n\tvoid replaceFloatPlayerDelegate(\n\t\tnot_null<Media::Player::FloatDelegate*> replacement);\n\tvoid restoreFloatPlayerDelegate(\n\t\tnot_null<Media::Player::FloatDelegate*> replacement);\n\t[[nodiscard]] FloatDelegate *floatPlayerDelegate() const;\n\t[[nodiscard]] auto floatPlayerDelegateValue() const\n\t\t-> rpl::producer<FloatDelegate*>;\n\n\t[[nodiscard]] std::shared_ptr<Ui::Show> uiShow();\n\n\tvoid setHighlightControlId(const QString &id);\n\t[[nodiscard]] QString highlightControlId() const;\n\t[[nodiscard]] bool takeHighlightControlId(const QString &id);\n\tvoid checkHighlightControl(\n\t\tconst QString &id,\n\t\tQWidget *widget,\n\t\tSettings::HighlightArgs &&args);\n\tvoid checkHighlightControl(const QString &id, QWidget *widget);\n\n\t[[nodiscard]] rpl::lifetime &lifetime();\n\nprivate:\n\tstruct CreateArgs {\n\t\tSeparateId id;\n\t};\n\texplicit Controller(CreateArgs &&args);\n\n\tvoid setupIntro(QPixmap oldContentCache);\n\tvoid setupMain(MsgId singlePeerShowAtMsgId, QPixmap oldContentCache);\n\n\tvoid showAccount(\n\t\tnot_null<Main::Account*> account,\n\t\tMsgId singlePeerShowAtMsgId);\n\tvoid setupSideBar();\n\tvoid sideBarChanged();\n\n\tvoid checkThemeEditor();\n\tvoid checkLockByTerms();\n\tvoid showTermsDecline();\n\tvoid showTermsDelete();\n\n\tSeparateId _id;\n\tbase::Timer _isActiveTimer;\n\t::MainWindow _widget;\n\tconst std::unique_ptr<Adaptive> _adaptive;\n\tstd::unique_ptr<SessionController> _sessionController;\n\trpl::variable<SessionController*> _sessionControllerValue;\n\tbase::weak_qptr<Ui::BoxContent> _termsBox;\n\n\trpl::event_stream<Media::View::OpenRequest> _openInMediaViewRequests;\n\n\tFloatDelegate *_defaultFloatPlayerDelegate = nullptr;\n\tFloatDelegate *_replacementFloatPlayerDelegate = nullptr;\n\trpl::variable<FloatDelegate*> _floatPlayerDelegate = nullptr;\n\n\tQString _highlightControlId;\n\n\trpl::lifetime _accountLifetime;\n\trpl::lifetime _lifetime;\n\n};\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_filters_menu.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_filters_menu.h\"\n\n#include \"core/application.h\"\n#include \"core/core_settings.h\"\n#include \"core/fork_settings.h\"\n#include \"mainwindow.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_main_menu.h\"\n#include \"window/window_peer_menu.h\"\n#include \"main/main_session.h\"\n#include \"core/ui_integration.h\"\n#include \"data/data_session.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_user.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_unread_value.h\"\n#include \"lang/lang_keys.h\"\n#include \"ui/filter_icons.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/wrap/vertical_layout_reorder.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/power_saving.h\"\n#include \"ui/ui_utility.h\"\n#include \"boxes/filters/edit_filter_box.h\"\n#include \"boxes/choose_filter_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"settings/sections/settings_folders.h\"\n#include \"storage/storage_media_prepare.h\"\n#include \"api/api_chat_filters.h\"\n#include \"apiwrap.h\"\n#include \"styles/style_widgets.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_layers.h\" // attentionBoxButton\n#include \"styles/style_menu_icons.h\"\n\nnamespace Window {\n\nFiltersMenu::FiltersMenu(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<SessionController*> session)\n: _session(session)\n, _parent(parent)\n, _outer(_parent)\n, _menu(&_outer, TextWithEntities(), st::windowFiltersMainMenu)\n, _scroll(&_outer)\n, _container(\n\t_scroll.setOwnedWidget(\n\t\tobject_ptr<Ui::VerticalLayout>(&_scroll))) {\n\n\t_drag.timer.setCallback([=] {\n\t\tif (_drag.filterId >= 0) {\n\t\t\t_session->setActiveChatsFilter(_drag.filterId);\n\t\t}\n\t});\n\tsetup();\n}\n\nFiltersMenu::~FiltersMenu() = default;\n\nvoid FiltersMenu::setup() {\n\tsetupDragAndDrop();\n\tsetupMainMenuIcon();\n\t_menu.setIsMenuButton(true);\n\t_menu.setAccessibleName(tr::lng_main_menu(tr::now));\n\n\t_outer.setAttribute(Qt::WA_OpaquePaintEvent);\n\t_outer.show();\n\t_outer.paintRequest(\n\t) | rpl::on_next([=](QRect clip) {\n\t\tauto p = QPainter(&_outer);\n\t\tp.setPen(Qt::NoPen);\n\t\tp.setBrush(st::windowFiltersButton.textBg);\n\t\tp.drawRect(clip);\n\t}, _outer.lifetime());\n\n\t_parent->heightValue(\n\t) | rpl::on_next([=](int height) {\n\t\tconst auto width = st::windowFiltersWidth;\n\t\t_outer.setGeometry({ 0, 0, width, height });\n\t\t_menu.resizeToWidth(width);\n\t\t_menu.move(0, 0);\n\t\t_scroll.setGeometry(\n\t\t\t{ 0, _menu.height(), width, height - _menu.height() });\n\t\t_container->resizeToWidth(width);\n\t\t_container->move(0, 0);\n\t}, _outer.lifetime());\n\n\tauto premium = Data::AmPremiumValue(&_session->session());\n\n\tconst auto filters = &_session->session().data().chatsFilters();\n\trpl::combine(\n\t\trpl::single(rpl::empty) | rpl::then(filters->changed()),\n\t\tstd::move(premium)\n\t) | rpl::on_next([=] {\n\t\trefresh();\n\t}, _outer.lifetime());\n\n\t_activeFilterId = _session->activeChatsFilterCurrent();\n\t_session->activeChatsFilter(\n\t) | rpl::filter([=](FilterId id) {\n\t\treturn (id != _activeFilterId);\n\t}) | rpl::on_next([=](FilterId id) {\n\t\tif (!_list) {\n\t\t\t_activeFilterId = id;\n\t\t\treturn;\n\t\t}\n\t\tconst auto i = _filters.find(_activeFilterId);\n\t\tif (i != end(_filters)) {\n\t\t\ti->second->setActive(false);\n\t\t}\n\t\t_activeFilterId = id;\n\t\tconst auto j = _filters.find(_activeFilterId);\n\t\tif (j != end(_filters)) {\n\t\t\tj->second->setActive(true);\n\t\t\tscrollToButton(j->second);\n\t\t}\n\t\t_reorder->finishReordering();\n\t}, _outer.lifetime());\n\n\t_menu.setClickedCallback([=] {\n\t\t_session->widget()->showMainMenu();\n\t});\n}\n\nvoid FiltersMenu::setupDragAndDrop() {\n\tSetupFilterDragAndDrop(\n\t\t&_outer,\n\t\t&_session->session(),\n\t\t[=](QPoint globalPos) -> std::optional<FilterId> {\n\t\t\tif (!_list) {\n\t\t\t\treturn std::nullopt;\n\t\t\t}\n\t\t\tconst auto localPos = _list->mapFromGlobal(globalPos);\n\t\t\tfor (const auto &[id, button] : _filters) {\n\t\t\t\tif (button->geometry().contains(localPos)) {\n\t\t\t\t\treturn id;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn std::nullopt;\n\t\t},\n\t\t[=] { return _activeFilterId; },\n\t\t[=](FilterId filterId) {\n\t\t\tfor (const auto &[id, button] : _filters) {\n\t\t\t\tbutton->setForceRippled(id == filterId);\n\t\t\t}\n\t\t});\n}\n\nvoid FiltersMenu::setupMainMenuIcon() {\n\tOtherAccountsUnreadState(\n\t\t&_session->session().account()\n\t) | rpl::on_next([=](const OthersUnreadState &state) {\n\t\tconst auto icon = !state.count\n\t\t\t? nullptr\n\t\t\t: !state.allMuted\n\t\t\t? &st::windowFiltersMainMenuUnread\n\t\t\t: &st::windowFiltersMainMenuUnreadMuted;\n\t\t_menu.setIconOverride(icon, icon);\n\t}, _outer.lifetime());\n}\n\nvoid FiltersMenu::scrollToButton(not_null<Ui::RpWidget*> widget) {\n\tconst auto globalPosition = widget->mapToGlobal(QPoint(0, 0));\n\tconst auto localTop = _scroll.mapFromGlobal(globalPosition).y();\n\tconst auto localBottom = localTop + widget->height() - _scroll.height();\n\tconst auto isTopEdge = (localTop < 0);\n\tconst auto isBottomEdge = (localBottom > 0);\n\tif (!isTopEdge && !isBottomEdge) {\n\t\treturn;\n\t}\n\n\t_scrollToAnimation.stop();\n\tconst auto scrollTop = _scroll.scrollTop();\n\tconst auto scrollTo = scrollTop + (isBottomEdge ? localBottom : localTop);\n\n\tauto scroll = [=] {\n\t\t_scroll.scrollToY(qRound(_scrollToAnimation.value(scrollTo)));\n\t};\n\n\t_scrollToAnimation.start(\n\t\tstd::move(scroll),\n\t\tscrollTop,\n\t\tscrollTo,\n\t\tst::slideDuration,\n\t\tanim::sineInOut);\n}\n\nvoid FiltersMenu::refresh() {\n\tconst auto filters = &_session->session().data().chatsFilters();\n\tif (!filters->has() || _ignoreRefresh) {\n\t\treturn;\n\t}\n\tconst auto oldTop = _scroll.scrollTop();\n\tconst auto reorderAll = premium();\n\tif (!_list) {\n\t\tsetupList();\n\t}\n\t_reorder->cancel();\n\n\t_reorder->clearPinnedIntervals();\n\tconst auto maxLimit = (reorderAll ? 1 : 0)\n\t\t+ Data::PremiumLimits(&_session->session()).dialogFiltersCurrent();\n\tconst auto premiumFrom = (reorderAll ? 0 : 1) + maxLimit;\n\tif (!reorderAll) {\n\t\t_reorder->addPinnedInterval(0, 1);\n\t}\n\t_reorder->addPinnedInterval(\n\t\tpremiumFrom,\n\t\tstd::max(1, int(filters->list().size()) - maxLimit));\n\n\tauto now = base::flat_map<int, base::unique_qptr<Ui::SideBarButton>>();\n\tconst auto &currentFilter = _session->activeChatsFilterCurrent();\n\tfor (const auto &filter : filters->list()) {\n\t\tconst auto nextIsLocked = (now.size() >= premiumFrom);\n\t\tif (nextIsLocked && (currentFilter == filter.id())) {\n\t\t\t_session->setActiveChatsFilter(FilterId(0));\n\t\t}\n\t\tauto button = prepareButton(\n\t\t\t_list,\n\t\t\tfilter.id(),\n\t\t\tfilter.title(),\n\t\t\tUi::ComputeFilterIcon(filter));\n\t\tbutton->setLocked(nextIsLocked);\n\t\tnow.emplace(filter.id(), std::move(button));\n\t}\n\t_filters = std::move(now);\n\t_reorder->start();\n\n\t_container->resizeToWidth(_outer.width());\n\n\t// After the filters are refreshed, the scroll is reset,\n\t// so we have to restore it.\n\t_scroll.scrollToY(oldTop);\n}\n\nvoid FiltersMenu::setupList() {\n\t_list = _container->add(object_ptr<Ui::VerticalLayout>(_container));\n\t_setup = prepareButton(\n\t\t_container,\n\t\t-1,\n\t\t{ TextWithEntities{ tr::lng_filters_setup(tr::now) } },\n\t\tUi::FilterIcon::Edit);\n\t_reorder = std::make_unique<Ui::VerticalLayoutReorder>(_list, &_scroll);\n\n\t_reorder->updates(\n\t) | rpl::on_next([=](Ui::VerticalLayoutReorder::Single data) {\n\t\tusing State = Ui::VerticalLayoutReorder::State;\n\t\tif (data.state == State::Started) {\n\t\t\t++_reordering;\n\t\t} else {\n\t\t\tUi::PostponeCall(&_outer, [=] {\n\t\t\t\t--_reordering;\n\t\t\t});\n\t\t\tif (data.state == State::Applied) {\n\t\t\t\tapplyReorder(data.widget, data.oldPosition, data.newPosition);\n\t\t\t}\n\t\t}\n\t}, _outer.lifetime());\n}\n\nbool FiltersMenu::premium() const {\n\treturn _session->session().user()->isPremium();\n}\n\nbase::unique_qptr<Ui::SideBarButton> FiltersMenu::prepareAll() {\n\treturn prepareButton(_container, 0, {}, Ui::FilterIcon::All, true);\n}\n\nbase::unique_qptr<Ui::SideBarButton> FiltersMenu::prepareButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tFilterId id,\n\t\tData::ChatFilterTitle title,\n\t\tUi::FilterIcon icon,\n\t\tbool toBeginning) {\n\tconst auto isStatic = title.isStatic;\n\tconst auto paused = [=] {\n\t\treturn On(PowerSaving::kEmojiChat)\n\t\t\t|| _session->isGifPausedAtLeastFor(Window::GifPauseReason::Any);\n\t};\n\tauto prepared = object_ptr<Ui::SideBarButton>(\n\t\tcontainer,\n\t\tid ? title.text : TextWithEntities{ tr::lng_filters_all(tr::now) },\n\t\tst::windowFiltersButton,\n\t\tCore::TextContext({\n\t\t\t.session = &_session->session(),\n\t\t\t.customEmojiLoopLimit = isStatic ? -1 : 0,\n\t\t}),\n\t\tpaused);\n\tif (!id && Core::App().settings().fork().hideAllChatsTab()) {\n\t\tconst auto raw = prepared.data();\n\t\traw->sizeValue(\n\t\t) | rpl::filter([](const QSize &s) {\n\t\t\treturn s.height() > 0;\n\t\t}) | rpl::on_next([=] {\n\t\t\traw->resize(raw->width(), 0);\n\t\t}, raw->lifetime());\n\t}\n\tauto added = toBeginning\n\t\t? container->insert(0, std::move(prepared))\n\t\t: container->add(std::move(prepared));\n\tauto button = base::unique_qptr<Ui::SideBarButton>(std::move(added));\n\tconst auto raw = button.get();\n\tconst auto nameText = id\n\t\t? title.text.text\n\t\t: tr::lng_filters_all(tr::now);\n\tconst auto &icons = Ui::LookupFilterIcon(id\n\t\t? icon\n\t\t: Ui::FilterIcon::All);\n\traw->setIconOverride(icons.normal, icons.active);\n\tif (id >= 0) {\n\t\trpl::combine(\n\t\t\tData::UnreadStateValue(&_session->session(), id),\n\t\t\tData::IncludeMutedCounterFoldersValue()\n\t\t) | rpl::on_next([=](\n\t\t\t\tconst Dialogs::UnreadState &state,\n\t\t\t\tbool includeMuted) {\n\t\t\tconst auto chats = state.chats;\n\t\t\tconst auto chatsMuted = state.chatsMuted;\n\t\t\tconst auto muted = (chatsMuted + state.marksMuted);\n\t\t\tconst auto count = (chats + state.marks)\n\t\t\t\t- (includeMuted ? 0 : muted);\n\t\t\tconst auto string = !count\n\t\t\t\t? QString()\n\t\t\t\t: (count > 999)\n\t\t\t\t? \"99+\"\n\t\t\t\t: QString::number(count);\n\t\t\traw->setBadge(string, includeMuted && (count == muted));\n\t\t\traw->setAccessibleName(count\n\t\t\t\t? tr::lng_filter_unread_chats(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_count,\n\t\t\t\t\tcount,\n\t\t\t\t\tlt_text,\n\t\t\t\t\tnameText)\n\t\t\t\t: nameText);\n\t\t}, raw->lifetime());\n\t}\n\traw->setActive(_session->activeChatsFilterCurrent() == id);\n\traw->setClickedCallback([=] {\n\t\tif (_reordering) {\n\t\t\treturn;\n\t\t} else if (raw->locked()) {\n\t\t\t_session->show(Box(\n\t\t\t\tFiltersLimitBox,\n\t\t\t\t&_session->session(),\n\t\t\t\tstd::nullopt));\n\t\t} else if (id >= 0) {\n\t\t\t_session->setActiveChatsFilter(id);\n\t\t} else {\n\t\t\topenFiltersSettings();\n\t\t}\n\t});\n\tif (id >= 0) {\n\t\traw->setAcceptDrops(true);\n\t\traw->events(\n\t\t) | rpl::filter([=](not_null<QEvent*> e) {\n\t\t\treturn ((e->type() == QEvent::ContextMenu) && (id >= 0))\n\t\t\t\t|| e->type() == QEvent::DragEnter\n\t\t\t\t|| e->type() == QEvent::DragMove\n\t\t\t\t|| e->type() == QEvent::DragLeave;\n\t\t}) | rpl::on_next([=](not_null<QEvent*> e) {\n\t\t\tif (raw->locked()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (e->type() == QEvent::ContextMenu) {\n\t\t\t\tshowMenu(QCursor::pos(), id);\n\t\t\t} else if (e->type() == QEvent::DragEnter) {\n\t\t\t\tusing namespace Storage;\n\t\t\t\tconst auto d = static_cast<QDragEnterEvent*>(e.get());\n\t\t\t\tconst auto data = d->mimeData();\n\t\t\t\tif (ComputeMimeDataState(data) != MimeDataState::None) {\n\t\t\t\t\t_drag.timer.callOnce(ChoosePeerByDragTimeout);\n\t\t\t\t\t_drag.filterId = id;\n\t\t\t\t\td->setDropAction(Qt::CopyAction);\n\t\t\t\t\td->accept();\n\t\t\t\t}\n\t\t\t} else if (e->type() == QEvent::DragMove) {\n\t\t\t\t_drag.timer.callOnce(ChoosePeerByDragTimeout);\n\t\t\t} else if (e->type() == QEvent::DragLeave) {\n\t\t\t\t_drag.filterId = FilterId(-1);\n\t\t\t\t_drag.timer.cancel();\n\t\t\t}\n\t\t}, raw->lifetime());\n\t}\n\treturn button;\n}\n\nvoid FiltersMenu::openFiltersSettings() {\n\tconst auto filters = &_session->session().data().chatsFilters();\n\tif (filters->suggestedLoaded()) {\n\t\t_session->showSettings(Settings::FoldersId());\n\t} else if (!_waitingSuggested) {\n\t\t_waitingSuggested = true;\n\t\tfilters->requestSuggested();\n\t\tfilters->suggestedUpdated(\n\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t_session->showSettings(Settings::FoldersId());\n\t\t}, _outer.lifetime());\n\t}\n}\n\nvoid FiltersMenu::showMenu(QPoint position, FilterId id) {\n\tif (_popupMenu) {\n\t\t_popupMenu = nullptr;\n\t\treturn;\n\t}\n\tconst auto i = _filters.find(id);\n\tif ((i == end(_filters)) && id) {\n\t\treturn;\n\t}\n\t_popupMenu = base::make_unique_q<Ui::PopupMenu>(\n\t\ti->second.get(),\n\t\tst::popupMenuWithIcons);\n\tconst auto addAction = Ui::Menu::CreateAddActionCallback(_popupMenu);\n\tif (id) {\n\t\taddAction(\n\t\t\ttr::lng_filters_context_edit(tr::now),\n\t\t\tcrl::guard(&_outer, [=] { EditExistingFilter(_session, id); }),\n\t\t\t&st::menuIconEdit);\n\n\t\tauto filteredChats = [=] {\n\t\t\treturn _session->session().data().chatsFilters().chatsList(id);\n\t\t};\n\t\tWindow::MenuAddMarkAsReadChatListAction(\n\t\t\t_session,\n\t\t\tstd::move(filteredChats),\n\t\t\taddAction);\n\n\t\taddAction({\n\t\t\t.text = tr::lng_filters_context_remove(tr::now),\n\t\t\t.handler = crl::guard(&_outer, [=, this] {\n\t\t\t\t_removeApi.request(base::make_weak(&_outer), _session, id);\n\t\t\t}),\n\t\t\t.icon = &st::menuIconDeleteAttention,\n\t\t\t.isAttention = true,\n\t\t});\n\t} else {\n\t\tauto customUnreadState = [=] {\n\t\t\tconst auto session = &_session->session();\n\t\t\treturn Data::MainListMapUnreadState(\n\t\t\t\tsession,\n\t\t\t\tsession->data().chatsList()->unreadState());\n\t\t};\n\t\tWindow::MenuAddMarkAsReadChatListAction(\n\t\t\t_session,\n\t\t\t[=] { return _session->session().data().chatsList(); },\n\t\t\taddAction,\n\t\t\tstd::move(customUnreadState));\n\n\t\taddAction(\n\t\t\ttr::lng_filters_setup_menu(tr::now),\n\t\t\tcrl::guard(&_outer, [=] { openFiltersSettings(); }),\n\t\t\t&st::menuIconEdit);\n\t}\n\tif (_popupMenu->empty()) {\n\t\t_popupMenu = nullptr;\n\t\treturn;\n\t}\n\t_popupMenu->popup(position);\n}\n\nvoid FiltersMenu::applyReorder(\n\t\tnot_null<Ui::RpWidget*> widget,\n\t\tint oldPosition,\n\t\tint newPosition) {\n\tif (newPosition == oldPosition) {\n\t\treturn;\n\t}\n\n\tconst auto filters = &_session->session().data().chatsFilters();\n\tconst auto &list = filters->list();\n\tif (!premium()) {\n\t\tif (list[0].id() != FilterId()) {\n\t\t\tfilters->moveAllToFront();\n\t\t}\n\t}\n\tAssert(oldPosition >= 0 && oldPosition < list.size());\n\tAssert(newPosition >= 0 && newPosition < list.size());\n\tconst auto id = list[oldPosition].id();\n\tconst auto i = _filters.find(id);\n\tAssert(i != end(_filters));\n\tAssert(i->second == widget);\n\n\tauto order = ranges::views::all(\n\t\tlist\n\t) | ranges::views::transform(\n\t\t&Data::ChatFilter::id\n\t) | ranges::to_vector;\n\tbase::reorder(order, oldPosition, newPosition);\n\n\t_ignoreRefresh = true;\n\tfilters->saveOrder(order);\n\t_ignoreRefresh = false;\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_filters_menu.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_chat_filters_remove_manager.h\"\n#include \"base/timer.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/widgets/side_bar_button.h\"\n#include \"ui/widgets/scroll_area.h\"\n\nnamespace Data {\nstruct ChatFilterTitle;\n} // namespace Data\n\nnamespace Ui {\nclass VerticalLayout;\nclass VerticalLayoutReorder;\nenum class FilterIcon : uchar;\nclass PopupMenu;\n} // namespace Ui\n\nnamespace Window {\n\nclass SessionController;\n\nclass FiltersMenu final {\npublic:\n\tFiltersMenu(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<SessionController*> session);\n\t~FiltersMenu();\n\nprivate:\n\tvoid setup();\n\tvoid refresh();\n\tvoid setupList();\n\tvoid applyReorder(\n\t\tnot_null<Ui::RpWidget*> widget,\n\t\tint oldPosition,\n\t\tint newPosition);\n\t[[nodiscard]] bool premium() const;\n\t[[nodiscard]] base::unique_qptr<Ui::SideBarButton> prepareAll();\n\t[[nodiscard]] base::unique_qptr<Ui::SideBarButton> prepareButton(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tFilterId id,\n\t\tData::ChatFilterTitle title,\n\t\tUi::FilterIcon icon,\n\t\tbool toBeginning = false);\n\tvoid setupMainMenuIcon();\n\tvoid showMenu(QPoint position, FilterId id);\n\tvoid scrollToButton(not_null<Ui::RpWidget*> widget);\n\tvoid openFiltersSettings();\n\tvoid setupDragAndDrop();\n\n\tconst not_null<SessionController*> _session;\n\tconst not_null<Ui::RpWidget*> _parent;\n\tUi::RpWidget _outer;\n\tUi::SideBarButton _menu;\n\tUi::ScrollArea _scroll;\n\tnot_null<Ui::VerticalLayout*> _container;\n\tUi::VerticalLayout *_list = nullptr;\n\tstd::unique_ptr<Ui::VerticalLayoutReorder> _reorder;\n\tbase::unique_qptr<Ui::SideBarButton> _setup;\n\tbase::flat_map<FilterId, base::unique_qptr<Ui::SideBarButton>> _filters;\n\trpl::variable<bool> _includeMuted;\n\tFilterId _activeFilterId = 0;\n\tint _reordering = 0;\n\tbool _ignoreRefresh = false;\n\tbool _waitingSuggested = false;\n\n\tApi::RemoveComplexChatFilter _removeApi;\n\n\tbase::unique_qptr<Ui::PopupMenu> _popupMenu;\n\tstruct {\n\t\tbase::Timer timer;\n\t\tFilterId filterId = FilterId(-1);\n\t} _drag;\n\n\tUi::Animations::Simple _scrollToAnimation;\n\n};\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_history_hider.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_history_hider.h\"\n\n#include \"lang/lang_keys.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/cached_round_corners.h\"\n#include \"mainwidget.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_chat.h\"\n\nnamespace Window {\n\nHistoryHider::HistoryHider(QWidget *parent, const QString &text)\n: RpWidget(parent)\n, _text(text) {\n\tLang::Updated(\n\t) | rpl::on_next([=] {\n\t\trefreshLang();\n\t}, lifetime());\n\n\t_chooseWidth = st::historyForwardChooseFont->width(_text);\n\n\tresizeEvent(0);\n\t_a_opacity.start([=] { update(); }, 0., 1., st::boxDuration);\n}\n\nHistoryHider::~HistoryHider() = default;\n\nvoid HistoryHider::refreshLang() {\n\tInvokeQueued(this, [this] { updateControlsGeometry(); });\n}\n\nvoid HistoryHider::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\tauto opacity = _a_opacity.value(_hiding ? 0. : 1.);\n\tif (opacity == 0.) {\n\t\tif (_hiding) {\n\t\t\t_hidden.fire({});\n\t\t}\n\t\treturn;\n\t}\n\n\tp.setOpacity(opacity);\n\tp.fillRect(rect(), st::layerBg);\n\tp.setFont(st::historyForwardChooseFont);\n\tauto w = st::historyForwardChooseMargins.left() + _chooseWidth + st::historyForwardChooseMargins.right();\n\tauto h = st::historyForwardChooseMargins.top() + st::historyForwardChooseFont->height + st::historyForwardChooseMargins.bottom();\n\tUi::FillRoundRect(p, (width() - w) / 2, (height() - h) / 2, w, h, st::historyForwardChooseBg, Ui::ForwardCorners);\n\n\tp.setPen(st::historyForwardChooseFg);\n\tp.drawText(_box, _text, QTextOption(style::al_center));\n}\n\nvoid HistoryHider::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Escape) {\n\t\tstartHide();\n\t}\n}\n\nvoid HistoryHider::mousePressEvent(QMouseEvent *e) {\n\tif (e->button() == Qt::LeftButton) {\n\t\tif (!_box.contains(e->pos())) {\n\t\t\tstartHide();\n\t\t}\n\t}\n}\n\nvoid HistoryHider::startHide() {\n\tif (_hiding) return;\n\n\t_hiding = true;\n\t_a_opacity.start([=] { animationCallback(); }, 1., 0., st::boxDuration);\n}\n\nvoid HistoryHider::animationCallback() {\n\tupdate();\n\tif (!_a_opacity.animating() && _hiding) {\n\t\tcrl::on_main(this, [=] { _hidden.fire({}); });\n\t}\n}\n\nrpl::producer<> HistoryHider::hidden() const {\n\treturn _hidden.events();\n}\n\nvoid HistoryHider::resizeEvent(QResizeEvent *e) {\n\tupdateControlsGeometry();\n}\n\nvoid HistoryHider::updateControlsGeometry() {\n\tauto w = st::boxWidth;\n\tauto h = st::boxPadding.top() + st::boxPadding.bottom();\n\th += st::historyForwardChooseFont->height;\n\t_box = QRect((width() - w) / 2, (height() - h) / 2, w, h);\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_history_hider.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n\nnamespace Data {\nclass Thread;\n} // namespace Data\n\nnamespace Ui {\nclass RoundButton;\n} // namespace Ui\n\nnamespace Window {\n\nclass HistoryHider final : public Ui::RpWidget {\npublic:\n\tHistoryHider(QWidget *parent, const QString &text);\n\t~HistoryHider();\n\n\tvoid startHide();\n\t[[nodiscard]] rpl::producer<> hidden() const;\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\tvoid mousePressEvent(QMouseEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid refreshLang();\n\tvoid updateControlsGeometry();\n\tvoid animationCallback();\n\n\tQString _text;\n\tUi::Animations::Simple _a_opacity;\n\n\tQRect _box;\n\tbool _hiding = false;\n\n\tint _chooseWidth = 0;\n\n\trpl::event_stream<> _hidden;\n\n};\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_lock_widgets.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_lock_widgets.h\"\n\n#include \"base/platform/base_platform_info.h\"\n#include \"base/call_delayed.h\"\n#include \"base/system_unlock.h\"\n#include \"lang/lang_keys.h\"\n#include \"storage/storage_domain.h\"\n#include \"mainwindow.h\"\n#include \"core/application.h\"\n#include \"api/api_text_entities.h\"\n#include \"ui/text/text.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/fields/password_input.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"ui/toast/toast.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_slide_animation.h\"\n#include \"window/window_session_controller.h\"\n#include \"main/main_domain.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace Window {\nnamespace {\n\nconstexpr auto kSystemUnlockDelay = crl::time(1000);\n\n} // namespace\n\nLockWidget::LockWidget(QWidget *parent, not_null<Controller*> window)\n: RpWidget(parent)\n, _window(window) {\n\tshow();\n}\n\nLockWidget::~LockWidget() = default;\n\nnot_null<Controller*> LockWidget::window() const {\n\treturn _window;\n}\n\nvoid LockWidget::setInnerFocus() {\n\tsetFocus();\n}\n\nvoid LockWidget::showAnimated(QPixmap oldContentCache) {\n\t_showAnimation = nullptr;\n\n\tshowChildren();\n\tsetInnerFocus();\n\tauto newContentCache = Ui::GrabWidget(this);\n\thideChildren();\n\n\t_showAnimation = std::make_unique<Window::SlideAnimation>();\n\t_showAnimation->setRepaintCallback([=] { update(); });\n\t_showAnimation->setFinishedCallback([=] { showFinished(); });\n\t_showAnimation->setPixmaps(oldContentCache, newContentCache);\n\t_showAnimation->start();\n\n\tshow();\n}\n\nvoid LockWidget::showFinished() {\n\tshowChildren();\n\t_window->widget()->setInnerFocus();\n\t_showAnimation = nullptr;\n\tif (const auto controller = _window->sessionController()) {\n\t\tcontroller->clearSectionStack();\n\t}\n}\n\nvoid LockWidget::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tif (_showAnimation) {\n\t\t_showAnimation->paintContents(p);\n\t\treturn;\n\t}\n\tpaintContent(p);\n}\n\nvoid LockWidget::paintContent(QPainter &p) {\n\tp.fillRect(rect(), st::windowBg);\n}\n\nPasscodeLockWidget::PasscodeLockWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> window)\n: LockWidget(parent, window)\n, _passcode(this, st::passcodeInput, tr::lng_passcode_ph())\n, _submit(this, tr::lng_passcode_submit(), st::passcodeSubmit)\n, _logout(this, tr::lng_passcode_logout(tr::now)) {\n\tconnect(_passcode, &Ui::MaskedInputField::changed, [=] { changed(); });\n\tconnect(_passcode, &Ui::MaskedInputField::submitted, [=] { submit(); });\n\t_submit->setClickedCallback([=] { submit(); });\n\t_logout->setClickedCallback([=] {\n\t\twindow->showLogoutConfirmation();\n\t});\n\n\tusing namespace rpl::mappers;\n\tif (Core::App().settings().systemUnlockEnabled()) {\n\t\t_systemUnlockAvailable = base::SystemUnlockStatus(\n\t\t\ttrue\n\t\t) | rpl::map([](base::SystemUnlockAvailability status) {\n\t\t\treturn status.withBiometrics\n\t\t\t\t? SystemUnlockType::Biometrics\n\t\t\t\t: status.withCompanion\n\t\t\t\t? SystemUnlockType::Companion\n\t\t\t\t: status.available\n\t\t\t\t? SystemUnlockType::Default\n\t\t\t\t: SystemUnlockType::None;\n\t\t});\n\t\tif (Core::App().domain().started()) {\n\t\t\t_systemUnlockAllowed = _systemUnlockAvailable.value();\n\t\t\tsetupSystemUnlock();\n\t\t} else {\n\t\t\tsetupSystemUnlockInfo();\n\t\t}\n\t}\n}\n\nvoid PasscodeLockWidget::setupSystemUnlockInfo() {\n\tconst auto macos = [&] {\n\t\treturn _systemUnlockAvailable.value(\n\t\t) | rpl::map([](SystemUnlockType type) {\n\t\t\treturn (type == SystemUnlockType::Biometrics)\n\t\t\t\t? tr::lng_passcode_touchid()\n\t\t\t\t: (type == SystemUnlockType::Companion)\n\t\t\t\t? tr::lng_passcode_applewatch()\n\t\t\t\t: tr::lng_passcode_systempwd();\n\t\t}) | rpl::flatten_latest();\n\t};\n\tauto text = Platform::IsWindows()\n\t\t? tr::lng_passcode_winhello()\n\t\t: macos();\n\tconst auto info = Ui::CreateChild<Ui::FlatLabel>(\n\t\tthis,\n\t\tstd::move(text),\n\t\tst::passcodeSystemUnlockLater);\n\t_logout->geometryValue(\n\t) | rpl::on_next([=](QRect logout) {\n\t\tinfo->resizeToWidth(width()\n\t\t\t- st::boxRowPadding.left()\n\t\t\t- st::boxRowPadding.right());\n\t\tinfo->moveToLeft(\n\t\t\tst::boxRowPadding.left(),\n\t\t\tlogout.y() + logout.height() + st::passcodeSystemUnlockSkip);\n\t}, info->lifetime());\n\tinfo->showOn(_systemUnlockAvailable.value(\n\t) | rpl::map(rpl::mappers::_1 != SystemUnlockType::None));\n}\n\nvoid PasscodeLockWidget::setupSystemUnlock() {\n\twindowActiveValue() | rpl::skip(1) | rpl::filter([=](bool active) {\n\t\treturn active\n\t\t\t&& !_systemUnlockSuggested\n\t\t\t&& !_systemUnlockCooldown.isActive();\n\t}) | rpl::on_next([=](bool) {\n\t\t[[maybe_unused]] auto refresh = base::SystemUnlockStatus();\n\t\tsuggestSystemUnlock();\n\t}, lifetime());\n\n\tconst auto button = Ui::CreateChild<Ui::IconButton>(\n\t\t_passcode.data(),\n\t\tst::passcodeSystemUnlock);\n\tif (!Platform::IsWindows()) {\n\t\tusing namespace base;\n\t\t_systemUnlockAllowed.value(\n\t\t) | rpl::on_next([=](SystemUnlockType type) {\n\t\t\tconst auto icon = (type == SystemUnlockType::Biometrics)\n\t\t\t\t? &st::passcodeSystemTouchID\n\t\t\t\t: (type == SystemUnlockType::Companion)\n\t\t\t\t? &st::passcodeSystemAppleWatch\n\t\t\t\t: &st::passcodeSystemSystemPwd;\n\t\t\tbutton->setIconOverride(icon, icon);\n\t\t}, button->lifetime());\n\t}\n\tbutton->showOn(_systemUnlockAllowed.value(\n\t) | rpl::map(rpl::mappers::_1 != SystemUnlockType::None));\n\t_passcode->sizeValue() | rpl::on_next([=](QSize size) {\n\t\tbutton->moveToRight(0, size.height() - button->height());\n\t}, button->lifetime());\n\tbutton->setClickedCallback([=] {\n\t\tconst auto delay = st::passcodeSystemUnlock.ripple.hideDuration;\n\t\tbase::call_delayed(delay, this, [=] {\n\t\t\tsuggestSystemUnlock();\n\t\t});\n\t});\n}\n\nvoid PasscodeLockWidget::suggestSystemUnlock() {\n\tInvokeQueued(this, [=] {\n\t\tif (_systemUnlockSuggested) {\n\t\t\treturn;\n\t\t}\n\t\t_systemUnlockCooldown.cancel();\n\n\t\tusing namespace base;\n\t\t_systemUnlockAllowed.value(\n\t\t) | rpl::filter(\n\t\t\trpl::mappers::_1 != SystemUnlockType::None\n\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\tconst auto weak = base::make_weak(this);\n\t\t\tconst auto done = [weak](SystemUnlockResult result) {\n\t\t\t\tcrl::on_main([=] {\n\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\tstrong->systemUnlockDone(result);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t};\n\t\t\tSuggestSystemUnlock(\n\t\t\t\tthis,\n\t\t\t\t(::Platform::IsWindows()\n\t\t\t\t\t? tr::lng_passcode_winhello_unlock(tr::now)\n\t\t\t\t\t: tr::lng_passcode_touchid_unlock(tr::now)),\n\t\t\t\tdone);\n\t\t}, _systemUnlockSuggested);\n\t});\n}\n\nvoid PasscodeLockWidget::systemUnlockDone(base::SystemUnlockResult result) {\n\tif (result == base::SystemUnlockResult::Success) {\n\t\tCore::App().unlockPasscode();\n\t\treturn;\n\t}\n\t_systemUnlockCooldown.callOnce(kSystemUnlockDelay);\n\t_systemUnlockSuggested.destroy();\n\tif (result == base::SystemUnlockResult::FloodError) {\n\t\t_error = tr::lng_flood_error(tr::now);\n\t\t_passcode->setFocusFast();\n\t\tupdate();\n\t}\n}\n\nvoid PasscodeLockWidget::paintContent(QPainter &p) {\n\tLockWidget::paintContent(p);\n\n\tp.setFont(st::passcodeHeaderFont);\n\tp.setPen(st::windowFg);\n\tp.drawText(QRect(0, _passcode->y() - st::passcodeHeaderHeight, width(), st::passcodeHeaderHeight), tr::lng_passcode_enter(tr::now), style::al_center);\n\n\tif (!_error.isEmpty()) {\n\t\tp.setFont(st::boxTextFont);\n\t\tp.setPen(st::boxTextFgError);\n\t\tp.drawText(QRect(0, _passcode->y() + _passcode->height(), width(), st::passcodeSubmitSkip), _error, style::al_center);\n\t}\n}\n\nvoid PasscodeLockWidget::submit() {\n\tif (_passcode->text().isEmpty()) {\n\t\t_passcode->showError();\n\t\treturn;\n\t}\n\tif (!passcodeCanTry()) {\n\t\t_error = tr::lng_flood_error(tr::now);\n\t\t_passcode->showError();\n\t\tupdate();\n\t\treturn;\n\t}\n\n\tconst auto passcode = _passcode->text().toUtf8();\n\tauto &domain = Core::App().domain();\n\tconst auto correct = domain.started()\n\t\t? domain.local().checkPasscode(passcode)\n\t\t: (domain.start(passcode) == Storage::StartResult::Success);\n\tif (!correct) {\n\t\tcSetPasscodeBadTries(cPasscodeBadTries() + 1);\n\t\tcSetPasscodeLastTry(crl::now());\n\t\terror();\n\t\treturn;\n\t}\n\n\tCore::App().unlockPasscode(); // Destroys this widget.\n}\n\nvoid PasscodeLockWidget::submitOnChange() {\n\tif (_passcode->text().isEmpty()) {\n\t\t_passcode->showError();\n\t\treturn;\n\t}\n\n\tconst auto passcode = _passcode->text().toUtf8();\n\tauto &domain = Core::App().domain();\n\tconst auto correct = domain.started()\n\t\t? domain.local().checkPasscode(passcode)\n\t\t: (domain.start(passcode)\n\t\t\t!= Storage::StartResult::IncorrectPasscode);\n\tif (!correct) {\n\t\treturn;\n\t}\n\n\tCore::App().unlockPasscode(); // Destroys this widget.\n}\n\nvoid PasscodeLockWidget::error() {\n\t_error = tr::lng_passcode_wrong(tr::now);\n\t_passcode->selectAll();\n\t_passcode->showError();\n\tupdate();\n}\n\nvoid PasscodeLockWidget::changed() {\n\tif (!_error.isEmpty()) {\n\t\t_error = QString();\n\t\tupdate();\n\t}\n\n\tif (Core::App().settings().fork().autoSubmitPasscode()) {\n\t\tQTimer::singleShot(100, this, [=] { submitOnChange(); });\n\t}\n}\n\nvoid PasscodeLockWidget::resizeEvent(QResizeEvent *e) {\n\t_passcode->move((width() - _passcode->width()) / 2, (height() / 3));\n\t_submit->move(_passcode->x(), _passcode->y() + _passcode->height() + st::passcodeSubmitSkip);\n\t_logout->move(_passcode->x() + (_passcode->width() - _logout->width()) / 2, _submit->y() + _submit->height() + st::linkFont->ascent);\n}\n\nvoid PasscodeLockWidget::setInnerFocus() {\n\tLockWidget::setInnerFocus();\n\t_passcode->setFocusFast();\n}\n\nTermsLock TermsLock::FromMTP(\n\t\tMain::Session *session,\n\t\tconst MTPDhelp_termsOfService &data) {\n\tconst auto minAge = data.vmin_age_confirm();\n\treturn {\n\t\tbytes::make_vector(data.vid().c_dataJSON().vdata().v),\n\t\tTextWithEntities {\n\t\t\tqs(data.vtext()),\n\t\t\tApi::EntitiesFromMTP(session, data.ventities().v) },\n\t\t(minAge ? std::make_optional(minAge->v) : std::nullopt),\n\t\tdata.is_popup()\n\t};\n}\n\nTermsBox::TermsBox(\n\tQWidget*,\n\tconst TermsLock &data,\n\trpl::producer<QString> agree,\n\trpl::producer<QString> cancel)\n: _data(data)\n, _agree(std::move(agree))\n, _cancel(std::move(cancel)) {\n}\n\nTermsBox::TermsBox(\n\tQWidget*,\n\tconst TextWithEntities &text,\n\trpl::producer<QString> agree,\n\trpl::producer<QString> cancel,\n\tbool attentionAgree)\n: _data{ {}, text, std::nullopt, false }\n, _agree(std::move(agree))\n, _cancel(std::move(cancel))\n, _attentionAgree(attentionAgree) {\n}\n\nrpl::producer<> TermsBox::agreeClicks() const {\n\treturn _agreeClicks.events();\n}\n\nrpl::producer<> TermsBox::cancelClicks() const {\n\treturn _cancelClicks.events();\n}\n\nvoid TermsBox::prepare() {\n\tsetTitle(tr::lng_terms_header());\n\n\tauto check = std::make_unique<Ui::CheckView>(st::defaultCheck, false);\n\tconst auto ageCheck = check.get();\n\tconst auto age = _data.minAge\n\t\t? Ui::CreateChild<Ui::PaddingWrap<Ui::Checkbox>>(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\tthis,\n\t\t\t\ttr::lng_terms_age(tr::now, lt_count, *_data.minAge),\n\t\t\t\tst::defaultCheckbox,\n\t\t\t\tstd::move(check)),\n\t\t\tst::termsAgePadding)\n\t\t: nullptr;\n\tif (age) {\n\t\tage->resizeToNaturalWidth(st::boxWideWidth);\n\t}\n\n\tconst auto content = setInnerWidget(\n\t\tobject_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(\n\t\t\tthis,\n\t\t\tobject_ptr<Ui::FlatLabel> (\n\t\t\t\tthis,\n\t\t\t\trpl::single(_data.text),\n\t\t\t\tst::termsContent),\n\t\t\tst::termsPadding),\n\t\t0,\n\t\tage ? age->height() : 0);\n\tconst auto show = uiShow();\n\tcontent->entity()->setClickHandlerFilter([=](\n\t\t\tconst ClickHandlerPtr &handler,\n\t\t\tQt::MouseButton button) {\n\t\tconst auto link = handler\n\t\t\t? handler->copyToClipboardText()\n\t\t\t: QString();\n\t\tif (TextUtilities::RegExpMention().match(link).hasMatch()) {\n\t\t\t_lastClickedMention = link;\n\t\t\tshow->showToast(\n\t\t\t\ttr::lng_terms_agree_to_proceed(tr::now, lt_bot, link));\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n\n\tconst auto errorAnimationCallback = [=] {\n\t\tconst auto check = ageCheck;\n\t\tconst auto error = _ageErrorAnimation.value(\n\t\t\t_ageErrorShown ? 1. : 0.);\n\t\tif (error == 0.) {\n\t\t\tcheck->setUntoggledOverride(std::nullopt);\n\t\t} else {\n\t\t\tconst auto color = anim::color(\n\t\t\t\tst::defaultCheck.untoggledFg,\n\t\t\t\tst::boxTextFgError,\n\t\t\t\terror);\n\t\t\tcheck->setUntoggledOverride(color);\n\t\t}\n\t};\n\tconst auto toggleAgeError = [=](bool shown) {\n\t\tif (_ageErrorShown != shown) {\n\t\t\t_ageErrorShown = shown;\n\t\t\t_ageErrorAnimation.start(\n\t\t\t\t[=] { errorAnimationCallback(); },\n\t\t\t\t_ageErrorShown ? 0. : 1.,\n\t\t\t\t_ageErrorShown ? 1. : 0.,\n\t\t\t\tst::defaultCheck.duration);\n\t\t}\n\t};\n\n\tconst auto &agreeStyle = _attentionAgree\n\t\t? st::attentionBoxButton\n\t\t: st::defaultBoxButton;\n\taddButton(std::move(_agree), [=] {}, agreeStyle)->clicks(\n\t) | rpl::filter([=] {\n\t\tif (age && !age->entity()->checked()) {\n\t\t\ttoggleAgeError(true);\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}) | rpl::to_empty | rpl::start_to_stream(_agreeClicks, lifetime());\n\n\tif (_cancel) {\n\t\taddButton(std::move(_cancel), [] {})->clicks(\n\t\t) | rpl::to_empty | rpl::start_to_stream(_cancelClicks, lifetime());\n\t}\n\n\tif (age) {\n\t\tage->entity()->checkedChanges(\n\t\t) | rpl::on_next([=] {\n\t\t\ttoggleAgeError(false);\n\t\t}, age->lifetime());\n\n\t\theightValue(\n\t\t) | rpl::on_next([=](int height) {\n\t\t\tage->moveToLeft(0, height - age->height());\n\t\t}, age->lifetime());\n\t}\n\n\tcontent->resizeToWidth(st::boxWideWidth);\n\n\tusing namespace rpl::mappers;\n\trpl::combine(\n\t\tcontent->heightValue(),\n\t\tage ? age->heightValue() : rpl::single(0),\n\t\t_1 + _2\n\t) | rpl::on_next([=](int height) {\n\t\tsetDimensions(st::boxWideWidth, height);\n\t}, content->lifetime());\n}\n\nvoid TermsBox::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {\n\t\t_agreeClicks.fire({});\n\t} else {\n\t\tBoxContent::keyPressEvent(e);\n\t}\n}\n\nQString TermsBox::lastClickedMention() const {\n\treturn _lastClickedMention;\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_lock_widgets.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/rp_widget.h\"\n#include \"ui/effects/animations.h\"\n#include \"ui/layers/box_content.h\"\n#include \"base/bytes.h\"\n#include \"base/timer.h\"\n\nnamespace base {\nenum class SystemUnlockResult;\n} // namespace base\n\nnamespace Ui {\nclass PasswordInput;\nclass LinkButton;\nclass RoundButton;\nclass CheckView;\n} // namespace Ui\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace Window {\n\nclass Controller;\nclass SlideAnimation;\n\nclass LockWidget : public Ui::RpWidget {\npublic:\n\tLockWidget(QWidget *parent, not_null<Controller*> window);\n\t~LockWidget();\n\n\t[[nodiscard]] not_null<Controller*> window() const;\n\n\tvirtual void setInnerFocus();\n\n\tvoid showAnimated(QPixmap oldContentCache);\n\tvoid showFinished();\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvirtual void paintContent(QPainter &p);\n\nprivate:\n\tconst not_null<Controller*> _window;\n\tstd::unique_ptr<SlideAnimation> _showAnimation;\n\n};\n\nclass PasscodeLockWidget : public LockWidget {\npublic:\n\tPasscodeLockWidget(QWidget *parent, not_null<Controller*> window);\n\n\tvoid setInnerFocus() override;\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tenum class SystemUnlockType : uchar {\n\t\tNone,\n\t\tDefault,\n\t\tBiometrics,\n\t\tCompanion,\n\t};\n\n\tvoid paintContent(QPainter &p) override;\n\n\tvoid setupSystemUnlockInfo();\n\tvoid setupSystemUnlock();\n\tvoid suggestSystemUnlock();\n\tvoid systemUnlockDone(base::SystemUnlockResult result);\n\tvoid changed();\n\tvoid submit();\n\tvoid submitOnChange();\n\tvoid error();\n\n\trpl::variable<SystemUnlockType> _systemUnlockAvailable;\n\trpl::variable<SystemUnlockType> _systemUnlockAllowed;\n\tobject_ptr<Ui::PasswordInput> _passcode;\n\tobject_ptr<Ui::RoundButton> _submit;\n\tobject_ptr<Ui::LinkButton> _logout;\n\tQString _error;\n\n\trpl::lifetime _systemUnlockSuggested;\n\tbase::Timer _systemUnlockCooldown;\n\n};\n\nstruct TermsLock {\n\tbytes::vector id;\n\tTextWithEntities text;\n\tstd::optional<int> minAge;\n\tbool popup = false;\n\n\tinline bool operator==(const TermsLock &other) const {\n\t\treturn (id == other.id);\n\t}\n\tinline bool operator!=(const TermsLock &other) const {\n\t\treturn !(*this == other);\n\t}\n\n\tstatic TermsLock FromMTP(\n\t\tMain::Session *session,\n\t\tconst MTPDhelp_termsOfService &data);\n\n};\n\nclass TermsBox : public Ui::BoxContent {\npublic:\n\tTermsBox(\n\t\tQWidget*,\n\t\tconst TermsLock &data,\n\t\trpl::producer<QString> agree,\n\t\trpl::producer<QString> cancel);\n\tTermsBox(\n\t\tQWidget*,\n\t\tconst TextWithEntities &text,\n\t\trpl::producer<QString> agree,\n\t\trpl::producer<QString> cancel,\n\t\tbool attentionAgree = false);\n\n\trpl::producer<> agreeClicks() const;\n\trpl::producer<> cancelClicks() const;\n\tQString lastClickedMention() const;\n\nprotected:\n\tvoid prepare() override;\n\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tTermsLock _data;\n\trpl::producer<QString> _agree;\n\trpl::producer<QString> _cancel;\n\trpl::event_stream<> _agreeClicks;\n\trpl::event_stream<> _cancelClicks;\n\tQString _lastClickedMention;\n\tbool _attentionAgree = false;\n\n\tbool _ageErrorShown = false;\n\tUi::Animations::Simple _ageErrorAnimation;\n\n};\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_main_menu.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_main_menu.h\"\n\n#include \"apiwrap.h\"\n#include \"base/event_filter.h\"\n#include \"base/qt_signal_producer.h\"\n#include \"boxes/about_box.h\"\n#include \"boxes/peer_list_controllers.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/calls_box_controller.h\"\n#include \"calls/calls_instance.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_session.h\"\n#include \"data/data_stories.h\"\n#include \"data/data_user.h\"\n#include \"info/info_memento.h\"\n#include \"info/profile/info_profile_badge.h\"\n#include \"settings/settings_common.h\"\n#include \"info/profile/info_profile_emoji_status_panel.h\"\n#include \"info/profile/info_profile_icon.h\"\n#include \"info/stories/info_stories_widget.h\"\n#include \"lang/lang_keys.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"settings/sections/settings_advanced.h\"\n#include \"settings/sections/settings_calls.h\"\n#include \"settings/sections/settings_information.h\"\n#include \"storage/localstorage.h\"\n#include \"storage/storage_account.h\"\n#include \"support/support_templates.h\"\n#include \"tde2e/tde2e_api.h\"\n#include \"tde2e/tde2e_integration.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/controls/swipe_handler.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/effects/snowflakes.h\"\n#include \"ui/effects/toggle_arrow.h\"\n#include \"ui/painter.h\"\n#include \"ui/text/text_options.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/unread_badge_paint.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/scroll_area.h\"\n#include \"ui/widgets/shadow.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_main_menu_helpers.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\" // popupMenuExpandedSeparator\n#include \"styles/style_info.h\" // infoTopBarMenu\n#include \"styles/style_layers.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_window.h\"\n\n#include <QtGui/QWindow>\n#include <QtGui/QScreen>\n\n#include <QtGui/QGuiApplication>\n#include <QtGui/QClipboard>\n\nnamespace Window {\nnamespace {\n\nconstexpr auto kPlayStatusLimit = 2;\n\n[[nodiscard]] bool CanCheckSpecialEvent() {\n\tstatic const auto result = [] {\n\t\tconst auto now = QDate::currentDate();\n\t\treturn (now.month() == 12) || (now.month() == 1 && now.day() == 1);\n\t}();\n\treturn result;\n}\n\n[[nodiscard]] bool CheckSpecialEvent() {\n\tconst auto now = QDate::currentDate();\n\treturn (now.month() == 12 && now.day() >= 24)\n\t\t|| (now.month() == 1 && now.day() == 1);\n}\n\n[[nodiscard]] rpl::producer<TextWithEntities> SetStatusLabel(\n\t\tnot_null<Main::Session*> session) {\n\tconst auto self = session->user();\n\treturn session->changes().peerFlagsValue(\n\t\tself,\n\t\tData::PeerUpdate::Flag::EmojiStatus\n\t) | rpl::map([=] {\n\t\treturn !!self->emojiStatusId();\n\t}) | rpl::distinct_until_changed() | rpl::map([](bool has) {\n\t\tconst auto makeLink = [](const QString &text) {\n\t\t\treturn tr::link(text);\n\t\t};\n\t\treturn (has\n\t\t\t? tr::lng_menu_change_status\n\t\t\t: tr::lng_menu_set_status)(makeLink);\n\t}) | rpl::flatten_latest();\n}\n\n} // namespace\n\nclass MainMenu::ToggleAccountsButton final : public Ui::AbstractButton {\npublic:\n\tToggleAccountsButton(QWidget *parent, not_null<Main::Account*> current);\n\n\t[[nodiscard]] int rightSkip() const {\n\t\treturn _rightSkip.current();\n\t}\n\t[[nodiscard]] rpl::producer<int> rightSkipValue() const {\n\t\treturn _rightSkip.value();\n\t}\n\nprivate:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid paintUnreadBadge(Painter &p);\n\n\tvoid validateUnreadBadge();\n\t[[nodiscard]] QString computeUnreadBadge() const;\n\n\tconst not_null<Main::Account*> _current;\n\trpl::variable<int> _rightSkip = 0;\n\tUi::Animations::Simple _toggledAnimation;\n\tbool _toggled = false;\n\n\tQString _unreadBadge;\n\tbool _unreadBadgeStale = false;\n\n};\n\nclass MainMenu::ResetScaleButton final : public Ui::AbstractButton {\npublic:\n\tResetScaleButton(QWidget *parent);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\n\tstatic constexpr auto kText = \"100%\";\n\n};\n\nMainMenu::ToggleAccountsButton::ToggleAccountsButton(\n\tQWidget *parent,\n\tnot_null<Main::Account*> current)\n: AbstractButton(parent)\n, _current(current) {\n\trpl::single(rpl::empty) | rpl::then(\n\t\tCore::App().unreadBadgeChanges()\n\t) | rpl::on_next([=] {\n\t\t_unreadBadgeStale = true;\n\t\tif (!_toggled) {\n\t\t\tvalidateUnreadBadge();\n\t\t\tupdate();\n\t\t}\n\t}, lifetime());\n\n\tauto &settings = Core::App().settings();\n\tif (Core::App().domain().accounts().size() < 2\n\t\t&& settings.mainMenuAccountsShown()) {\n\t\tsettings.setMainMenuAccountsShown(false);\n\t}\n\tsettings.mainMenuAccountsShownValue(\n\t) | rpl::filter([=](bool value) {\n\t\treturn (_toggled != value);\n\t}) | rpl::on_next([=](bool value) {\n\t\t_toggled = value;\n\t\t_toggledAnimation.start(\n\t\t\t[=] { update(); },\n\t\t\t_toggled ? 0. : 1.,\n\t\t\t_toggled ? 1. : 0.,\n\t\t\tst::slideWrapDuration);\n\t\tvalidateUnreadBadge();\n\t}, lifetime());\n\t_toggledAnimation.stop();\n}\n\nvoid MainMenu::ToggleAccountsButton::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\n\tconst auto path = Ui::ToggleUpDownArrowPath(\n\t\t0. + width() - st::mainMenuTogglePosition.x(),\n\t\t0. + height() - st::mainMenuTogglePosition.y(),\n\t\tst::mainMenuToggleSize,\n\t\tst::mainMenuToggleFourStrokes,\n\t\t_toggledAnimation.value(_toggled ? 1. : 0.));\n\n\tauto hq = PainterHighQualityEnabler(p);\n\tp.fillPath(path, st::windowSubTextFg);\n\n\tpaintUnreadBadge(p);\n}\n\nvoid MainMenu::ToggleAccountsButton::paintUnreadBadge(Painter &p) {\n\tconst auto progress = 1. - _toggledAnimation.value(_toggled ? 1. : 0.);\n\tif (!progress) {\n\t\treturn;\n\t}\n\tvalidateUnreadBadge();\n\tif (_unreadBadge.isEmpty()) {\n\t\treturn;\n\t}\n\n\tauto st = Settings::Badge::Style();\n\tconst auto right = width()\n\t\t- st::mainMenuTogglePosition.x()\n\t\t- st::mainMenuToggleSize * 3;\n\tconst auto top = height()\n\t\t- st::mainMenuTogglePosition.y()\n\t\t- st::mainMenuBadgeSize / 2;\n\tp.setOpacity(progress);\n\tUi::PaintUnreadBadge(p, _unreadBadge, right, top, st);\n}\n\nvoid MainMenu::ToggleAccountsButton::validateUnreadBadge() {\n\tconst auto base = st::mainMenuTogglePosition.x()\n\t\t+ 2 * st::mainMenuToggleSize;\n\tif (_toggled) {\n\t\t_rightSkip = base;\n\t\treturn;\n\t} else if (!_unreadBadgeStale) {\n\t\treturn;\n\t}\n\t_unreadBadge = computeUnreadBadge();\n\n\tauto skip = base;\n\tif (!_unreadBadge.isEmpty()) {\n\t\tconst auto st = Settings::Badge::Style();\n\t\tskip += 2 * st::mainMenuToggleSize\n\t\t\t+ Ui::CountUnreadBadgeSize(_unreadBadge, st).width();\n\t}\n\t_rightSkip = skip;\n}\n\nQString MainMenu::ToggleAccountsButton::computeUnreadBadge() const {\n\tconst auto state = OtherAccountsUnreadStateCurrent(_current);\n\treturn state.allMuted\n\t\t? QString()\n\t\t: (state.count > 0)\n\t\t? Lang::FormatCountToShort(state.count).string\n\t\t: QString();\n}\n\nMainMenu::ResetScaleButton::ResetScaleButton(QWidget *parent)\n: AbstractButton(parent) {\n\tconst auto margin = st::mainMenuCloudButton.height\n\t\t- st::mainMenuCloudSize;\n\tconst auto textWidth = st::mainMenuResetScaleFont->width(kText);\n\tconst auto innerWidth = st::mainMenuResetScaleLeft\n\t\t+ textWidth\n\t\t+ st::mainMenuResetScaleRight;\n\tconst auto width = margin + innerWidth;\n\tresize(width, st::mainMenuCloudButton.height);\n}\n\nvoid MainMenu::ResetScaleButton::paintEvent(QPaintEvent *e) {\n\tPainter p(this);\n\n\tconst auto innerHeight = st::mainMenuCloudSize;\n\tconst auto radius = innerHeight / 2;\n\tconst auto margin = st::mainMenuCloudButton.height\n\t\t- st::mainMenuCloudSize;\n\tconst auto textWidth = st::mainMenuResetScaleFont->width(kText);\n\tconst auto innerWidth = st::mainMenuResetScaleLeft\n\t\t+ textWidth\n\t\t+ st::mainMenuResetScaleRight;\n\tconst auto left = margin / 2;\n\tconst auto top = margin / 2;\n\tp.setPen(Qt::NoPen);\n\tp.setBrush(st::mainMenuCloudBg);\n\tp.drawRoundedRect(left, top, innerWidth, innerHeight, radius, radius);\n\n\tst::settingsIconInterfaceScale.paint(\n\t\tp,\n\t\tleft + st::mainMenuResetScaleIconLeft,\n\t\ttop + ((innerHeight - st::settingsIconInterfaceScale.height()) / 2),\n\t\twidth(),\n\t\tst::mainMenuCloudFg->c);\n\n\tp.setFont(st::mainMenuResetScaleFont);\n\tp.setPen(st::mainMenuCloudFg);\n\tp.drawText(\n\t\tleft + st::mainMenuResetScaleLeft,\n\t\ttop + st::mainMenuResetScaleTop + st::mainMenuResetScaleFont->ascent,\n\t\tkText);\n}\n\nMainMenu::MainMenu(\n\tQWidget *parent,\n\tnot_null<SessionController*> controller)\n: LayerWidget(parent)\n, _controller(controller)\n, _userpicButton(\n\tthis,\n\t_controller->session().user(),\n\tst::mainMenuUserpic)\n, _toggleAccounts(this, &controller->session().account())\n, _setEmojiStatus(this, SetStatusLabel(&controller->session()))\n, _emojiStatusPanel(std::make_unique<Info::Profile::EmojiStatusPanel>())\n, _badge(std::make_unique<Info::Profile::Badge>(\n\tthis,\n\tst::settingsInfoPeerBadge,\n\t&controller->session(),\n\tInfo::Profile::BadgeContentForPeer(controller->session().user()),\n\t_emojiStatusPanel.get(),\n\t[=] { return controller->isGifPausedAtLeastFor(GifPauseReason::Layer); },\n\tkPlayStatusLimit,\n\tInfo::Profile::BadgeType::Premium))\n, _scroll(this, st::defaultSolidScroll)\n, _inner(_scroll->setOwnedWidget(\n\tobject_ptr<Ui::VerticalLayout>(_scroll.data())))\n, _topShadowSkip(_inner->add(\n\tobject_ptr<Ui::FixedHeightWidget>(_inner.get(), st::lineWidth)))\n, _accounts(_inner->add(object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t_inner.get(),\n\tobject_ptr<Ui::VerticalLayout>(_inner.get()))))\n, _shadow(_inner->add(object_ptr<Ui::SlideWrap<Ui::PlainShadow>>(\n\t_inner.get(),\n\tobject_ptr<Ui::PlainShadow>(_inner.get()))))\n, _menu(_inner->add(\n\tobject_ptr<Ui::VerticalLayout>(_inner.get()),\n\t{ 0, st::mainMenuSkip, 0, 0 }))\n, _footer(_inner->add(object_ptr<Ui::RpWidget>(_inner.get())))\n, _telegram(\n\tUi::CreateChild<Ui::FlatLabel>(_footer.get(), st::mainMenuTelegramLabel))\n, _version(AddVersionLabel(_footer)) {\n\tsetAttribute(Qt::WA_OpaquePaintEvent);\n\n\tsetupUserpicButton();\n\tsetupAccountsToggle();\n\tsetupSetEmojiStatus();\n\tsetupAccounts();\n\tsetupArchive();\n\tsetupMenu();\n\n\tconst auto shadow = Ui::CreateChild<Ui::PlainShadow>(this);\n\twidthValue(\n\t) | rpl::on_next([=](int width) {\n\t\tconst auto line = st::lineWidth;\n\t\tshadow->setGeometry(0, st::mainMenuCoverHeight - line, width, line);\n\t}, shadow->lifetime());\n\n\t_nightThemeSwitch.setCallback([this] {\n\t\tExpects(_nightThemeToggle != nullptr);\n\n\t\tconst auto nightMode = Window::Theme::IsNightMode();\n\t\tif (_nightThemeToggle->toggled() != nightMode) {\n\t\t\tWindow::Theme::ToggleNightMode();\n\t\t\tWindow::Theme::KeepApplied();\n\t\t}\n\t});\n\n\t_footer->heightValue(\n\t) | rpl::on_next([=] {\n\t\t_telegram->moveToLeft(st::mainMenuFooterLeft, _footer->height() - st::mainMenuTelegramBottom - _telegram->height());\n\t\t_version->moveToLeft(st::mainMenuFooterLeft, _footer->height() - st::mainMenuVersionBottom - _version->height());\n\t}, _footer->lifetime());\n\n\trpl::combine(\n\t\theightValue(),\n\t\t_inner->heightValue()\n\t) | rpl::on_next([=] {\n\t\tupdateInnerControlsGeometry();\n\t}, _inner->lifetime());\n\n\tparentResized();\n\n\t_telegram->setMarkedText(tr::link(\n\t\tAppNameF.utf8(),\n\t\tu\"https://github.com/Forkgram/tdesktop\"_q));\n\t_telegram->setLinksTrusted();\n\t_version->setMarkedText(\n\t\ttr::link(\n\t\t\ttr::lng_settings_current_version(\n\t\t\t\ttr::now,\n\t\t\t\tlt_version,\n\t\t\t\tcurrentVersionText()),\n\t\t\t1) // Link 1.\n\t\t.append(QChar(' '))\n\t\t.append(QChar(8211))\n\t\t.append(QChar(' '))\n\t\t.append(tr::link(tr::lng_menu_about(tr::now), 2))); // Link 2.\n\t_version->setLink(\n\t\t1,\n\t\tstd::make_shared<UrlClickHandler>(Core::App().changelogLink()));\n\t_version->setLink(\n\t\t2,\n\t\tstd::make_shared<LambdaClickHandler>([=] {\n\t\t\tcontroller->show(Box(AboutBox));\n\t\t}));\n\n\trpl::combine(\n\t\t_toggleAccounts->rightSkipValue(),\n\t\trpl::single(rpl::empty) | rpl::then(_badge->updated())\n\t) | rpl::on_next([=] {\n\t\tmoveBadge();\n\t}, lifetime());\n\t_badge->setPremiumClickCallback([=] {\n\t\tchooseEmojiStatus();\n\t});\n\n\t_controller->session().downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\tinitResetScaleButton();\n\n\tif (CanCheckSpecialEvent() && CheckSpecialEvent()) {\n\t\tconst auto snowLifetime = lifetime().make_state<rpl::lifetime>();\n\t\tconst auto rebuild = [=] {\n\t\t\tconst auto snowRaw = Ui::CreateChild<Ui::RpWidget>(this);\n\t\t\tconst auto snow = snowLifetime->make_state<Ui::Snowflakes>(\n\t\t\t\t[=](const QRect &r) { snowRaw->update(r); });\n\t\t\tsnow->setBrush(QColor(230, 230, 230));\n\t\t\t_showFinished.value(\n\t\t\t) | rpl::on_next([=](bool shown) {\n\t\t\t\tsnow->setPaused(!shown);\n\t\t\t}, snowRaw->lifetime());\n\t\t\tsnowRaw->paintRequest(\n\t\t\t) | rpl::on_next([=](const QRect &r) {\n\t\t\t\tauto p = Painter(snowRaw);\n\t\t\t\tp.fillRect(r, st::mainMenuBg);\n\t\t\t\tdrawName(p);\n\t\t\t\tsnow->paint(p, snowRaw->rect());\n\t\t\t}, snowRaw->lifetime());\n\t\t\twidthValue(\n\t\t\t) | rpl::on_next([=](int width) {\n\t\t\t\tsnowRaw->setGeometry(0, 0, width, st::mainMenuCoverHeight);\n\t\t\t}, snowRaw->lifetime());\n\t\t\tsnowRaw->show();\n\t\t\tsnowRaw->lower();\n\t\t\tsnowRaw->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t\tsnowLifetime->add([=] { base::unique_qptr{ snowRaw }; });\n\t\t};\n\t\tWindow::Theme::IsNightModeValue(\n\t\t) | rpl::on_next([=](bool isNightMode) {\n\t\t\tsnowLifetime->destroy();\n\t\t\tif (isNightMode) {\n\t\t\t\trebuild();\n\t\t\t}\n\t\t}, lifetime());\n\t}\n\n\tsetupSwipe();\n}\n\nMainMenu::~MainMenu() = default;\n\nvoid MainMenu::moveBadge() {\n\tif (!_badge->widget()) {\n\t\treturn;\n\t}\n\tconst auto available = width()\n\t\t- st::mainMenuCoverNameLeft\n\t\t- _toggleAccounts->rightSkip()\n\t\t- _badge->widget()->width();\n\tconst auto left = st::mainMenuCoverNameLeft\n\t\t+ std::min(_name.maxWidth() + st::semiboldFont->spacew, available);\n\t_badge->move(\n\t\tleft,\n\t\tst::mainMenuCoverNameTop,\n\t\tst::mainMenuCoverNameTop + st::semiboldFont->height);\n}\n\nvoid MainMenu::setupArchive() {\n\tusing namespace Settings;\n\n\tconst auto controller = _controller;\n\tconst auto folder = [=] {\n\t\treturn controller->session().data().folderLoaded(Data::Folder::kId);\n\t};\n\tconst auto showArchive = [=](Qt::KeyboardModifiers modifiers) {\n\t\tif (const auto f = folder()) {\n\t\t\tif (modifiers & Qt::ControlModifier) {\n\t\t\t\tcontroller->showInNewWindow(Window::SeparateId(\n\t\t\t\t\tWindow::SeparateType::Archive,\n\t\t\t\t\t&controller->session()));\n\t\t\t} else {\n\t\t\t\tcontroller->openFolder(f);\n\t\t\t}\n\t\t\tcontroller->window().hideSettingsAndLayer();\n\t\t}\n\t};\n\tconst auto checkArchive = [=] {\n\t\tconst auto f = folder();\n\t\treturn f\n\t\t\t&& (!f->chatsList()->empty() || f->storiesCount() > 0)\n\t\t\t&& controller->session().settings().archiveInMainMenu();\n\t};\n\n\tconst auto wrap = _menu->add(\n\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t_menu,\n\t\t\tobject_ptr<Ui::VerticalLayout>(_menu)));\n\tconst auto inner = wrap->entity();\n\twrap->toggle(checkArchive(), anim::type::instant);\n\n\tconst auto button = AddButtonWithIcon(\n\t\tinner,\n\t\ttr::lng_archived_name(),\n\t\tst::mainMenuButton,\n\t\t{ &st::menuIconArchiveOpen });\n\tinner->add(\n\t\tobject_ptr<Ui::PlainShadow>(inner),\n\t\t{ 0, st::mainMenuSkip, 0, st::mainMenuSkip });\n\tbutton->setAcceptBoth(true);\n\tbutton->clicks(\n\t) | rpl::on_next([=](Qt::MouseButton which) {\n\t\tif (which == Qt::LeftButton) {\n\t\t\tshowArchive(button->clickModifiers());\n\t\t\treturn;\n\t\t} else if (which != Qt::RightButton) {\n\t\t\treturn;\n\t\t}\n\t\t_contextMenu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tthis,\n\t\t\tst::popupMenuExpandedSeparator);\n\t\tWindow::FillDialogsEntryMenu(\n\t\t\t_controller,\n\t\t\tDialogs::EntryState{\n\t\t\t\t.key = folder(),\n\t\t\t\t.section = Dialogs::EntryState::Section::ContextMenu,\n\t\t\t},\n\t\t\tUi::Menu::CreateAddActionCallback(_contextMenu));\n\t\t_contextMenu->popup(QCursor::pos());\n\t}, button->lifetime());\n\n\tconst auto now = folder();\n\tauto folderValue = now\n\t\t? (rpl::single(now) | rpl::type_erased)\n\t\t: controller->session().data().chatsListChanges(\n\t\t) | rpl::filter([](Data::Folder *folder) {\n\t\t\treturn folder && (folder->id() == Data::Folder::kId);\n\t\t}) | rpl::take(1);\n\n\tusing namespace Settings;\n\tBadge::AddUnread(button, rpl::single(rpl::empty) | rpl::then(std::move(\n\t\tfolderValue\n\t) | rpl::map([=](not_null<Data::Folder*> folder) {\n\t\treturn folder->owner().chatsList(folder)->unreadStateChanges();\n\t}) | rpl::flatten_latest() | rpl::to_empty) | rpl::map([=] {\n\t\tconst auto loaded = folder();\n\t\tconst auto state = loaded\n\t\t\t? loaded->chatListBadgesState()\n\t\t\t: Dialogs::BadgesState();\n\t\treturn Badge::UnreadBadge{ state.unreadCounter, true };\n\t}));\n\n\trpl::merge(\n\t\tcontroller->session().data().chatsListChanges(\n\t\t) | rpl::filter([](Data::Folder *folder) {\n\t\t\treturn folder && (folder->id() == Data::Folder::kId);\n\t\t}) | rpl::to_empty,\n\t\tcontroller->session().data().stories().sourcesChanged(\n\t\t\tData::StorySourcesList::Hidden\n\t\t)\n\t) | rpl::on_next([=] {\n\t\tconst auto isArchiveVisible = checkArchive();\n\t\twrap->toggle(isArchiveVisible, anim::type::normal);\n\t\tif (!isArchiveVisible) {\n\t\t\t_contextMenu = nullptr;\n\t\t}\n\t\tupdate();\n\t}, lifetime());\n}\n\nvoid MainMenu::setupUserpicButton() {\n\t_userpicButton->setClickedCallback([=] { toggleAccounts(); });\n\t_userpicButton->show();\n}\n\nvoid MainMenu::toggleAccounts() {\n\tauto &settings = Core::App().settings();\n\tconst auto shown = !settings.mainMenuAccountsShown();\n\tsettings.setMainMenuAccountsShown(shown);\n\tCore::App().saveSettingsDelayed();\n}\n\nvoid MainMenu::setupAccounts() {\n\tconst auto inner = _accounts->entity();\n\n\tinner->add(object_ptr<Ui::FixedHeightWidget>(inner, st::mainMenuSkip));\n\tauto events = Settings::SetupAccounts(inner, _controller);\n\tinner->add(object_ptr<Ui::FixedHeightWidget>(inner, st::mainMenuSkip));\n\n\tstd::move(\n\t\tevents.closeRequests\n\t) | rpl::on_next([=] {\n\t\tcloseLayer();\n\t}, inner->lifetime());\n\n\t_accounts->toggleOn(Core::App().settings().mainMenuAccountsShownValue());\n\t_accounts->finishAnimating();\n\n\t_shadow->setDuration(0)->toggleOn(_accounts->shownValue());\n}\n\nvoid MainMenu::setupAccountsToggle() {\n\t_toggleAccounts->show();\n\t_toggleAccounts->setAcceptBoth();\n\t_toggleAccounts->addClickHandler([=](Qt::MouseButton button) {\n\t\tif (button == Qt::LeftButton) {\n\t\t\ttoggleAccounts();\n\t\t}\n\t});\n}\n\nvoid MainMenu::setupSetEmojiStatus() {\n\t_setEmojiStatus->overrideLinkClickHandler([=] {\n\t\tchooseEmojiStatus();\n\t});\n}\n\nvoid MainMenu::parentResized() {\n\tresize(st::mainMenuWidth, parentWidget()->height());\n}\n\nvoid MainMenu::showFinished() {\n\t_showFinished = true;\n\n\t_controller->checkHighlightControl(\n\t\tu\"main-menu/emoji-status\"_q,\n\t\t_setEmojiStatus,\n\t\tSettings::SubsectionTitleHighlight());\n\t_controller->checkHighlightControl(\n\t\tu\"main-menu/night-mode\"_q,\n\t\t_nightThemeToggle);\n}\n\nvoid MainMenu::setupMenu() {\n\tusing namespace Settings;\n\n\tconst auto controller = _controller;\n\tconst auto addAction = [&](\n\t\t\trpl::producer<QString> text,\n\t\t\tIconDescriptor &&descriptor) {\n\t\treturn AddButtonWithIcon(\n\t\t\t_menu,\n\t\t\tstd::move(text),\n\t\t\tst::mainMenuButton,\n\t\t\tstd::move(descriptor));\n\t};\n\tif (!_controller->session().supportMode()) {\n\t\t_menu->add(\n\t\t\tCreateButtonWithIcon(\n\t\t\t\t_menu,\n\t\t\t\ttr::lng_menu_my_profile(),\n\t\t\t\tst::mainMenuButton,\n\t\t\t\t{ &st::menuIconProfile })\n\t\t)->setClickedCallback([=] {\n\t\t\tcontroller->showSection(\n\t\t\t\tInfo::Stories::Make(controller->session().user()));\n\t\t});\n\n\t\tSetupMenuBots(_menu, controller);\n\n\t\t_menu->add(\n\t\t\tobject_ptr<Ui::PlainShadow>(_menu),\n\t\t\t{ 0, st::mainMenuSkip, 0, st::mainMenuSkip });\n\n\t\tAddMyChannelsBox(addAction(\n\t\t\ttr::lng_create_group_title(),\n\t\t\t{ &st::menuIconGroups }\n\t\t), controller, true)->addClickHandler([=](Qt::MouseButton which) {\n\t\t\tif (which == Qt::LeftButton) {\n\t\t\t\tcontroller->showNewGroup();\n\t\t\t}\n\t\t});\n\n\t\tAddMyChannelsBox(addAction(\n\t\t\ttr::lng_create_channel_title(),\n\t\t\t{ &st::menuIconChannel }\n\t\t), controller, false)->addClickHandler([=](Qt::MouseButton which) {\n\t\t\tif (which == Qt::LeftButton) {\n\t\t\t\tcontroller->showNewChannel();\n\t\t\t}\n\t\t});\n\n\t\taddAction(\n\t\t\ttr::lng_menu_contacts(),\n\t\t\t{ &st::menuIconUserShow }\n\t\t)->setClickedCallback([=] {\n\t\t\tcontroller->show(PrepareContactsBox(controller));\n\t\t});\n\t\taddAction(\n\t\t\ttr::lng_menu_calls(),\n\t\t\t{ &st::menuIconPhone }\n\t\t)->setClickedCallback([=] {\n\t\t\t::Calls::ShowCallsBox(controller);\n\t\t});\n\t\taddAction(\n\t\t\ttr::lng_saved_messages(),\n\t\t\t{ &st::menuIconSavedMessages }\n\t\t)->setClickedCallback([=] {\n\t\t\tcontroller->showPeerHistory(controller->session().user());\n\t\t});\n\t} else {\n\t\taddAction(\n\t\t\ttr::lng_profile_add_contact(),\n\t\t\t{ &st::menuIconProfile }\n\t\t)->setClickedCallback([=] {\n\t\t\tcontroller->showAddContact();\n\t\t});\n\t\taddAction(\n\t\t\trpl::single(u\"Fix chats order\"_q),\n\t\t\t{ &st::menuIconPin }\n\t\t)->toggleOn(rpl::single(\n\t\t\t_controller->session().settings().supportFixChatsOrder()\n\t\t))->toggledChanges(\n\t\t) | rpl::on_next([=](bool fix) {\n\t\t\t_controller->session().settings().setSupportFixChatsOrder(fix);\n\t\t\t_controller->session().saveSettings();\n\t\t}, _menu->lifetime());\n\t\taddAction(\n\t\t\trpl::single(u\"Reload templates\"_q),\n\t\t\t{ &st::menuIconRestore }\n\t\t)->setClickedCallback([=] {\n\t\t\t_controller->session().supportTemplates().reload();\n\t\t});\n\t}\n\taddAction(\n\t\ttr::lng_menu_settings(),\n\t\t{ &st::menuIconSettings }\n\t)->setClickedCallback([=] {\n\t\tcontroller->showSettings();\n\t});\n\n\t_nightThemeToggle = addAction(\n\t\ttr::lng_menu_night_mode(),\n\t\t{ &st::menuIconNightMode }\n\t)->toggleOn(_nightThemeSwitches.events_starting_with(\n\t\tWindow::Theme::IsNightMode()\n\t));\n\t_nightThemeToggle->toggledChanges(\n\t) | rpl::filter([=](bool night) {\n\t\treturn (night != Window::Theme::IsNightMode());\n\t}) | rpl::on_next([=](bool night) {\n\t\tif (Window::Theme::Background()->editingTheme()) {\n\t\t\t_nightThemeSwitches.fire(!night);\n\t\t\tcontroller->show(Ui::MakeInformBox(\n\t\t\t\ttr::lng_theme_editor_cant_change_theme()));\n\t\t\treturn;\n\t\t}\n\t\tconst auto weak = base::make_weak(this);\n\t\tconst auto toggle = [=] {\n\t\t\tif (!weak) {\n\t\t\t\tWindow::Theme::ToggleNightMode();\n\t\t\t\tWindow::Theme::KeepApplied();\n\t\t\t} else {\n\t\t\t\t_nightThemeSwitch.callOnce(st::mainMenu.itemToggle.duration);\n\t\t\t}\n\t\t};\n\t\tWindow::Theme::ToggleNightModeWithConfirmation(\n\t\t\t&_controller->window(),\n\t\t\ttoggle);\n\t}, _nightThemeToggle->lifetime());\n\n\tCore::App().settings().systemDarkModeValue(\n\t) | rpl::on_next([=](std::optional<bool> darkMode) {\n\t\tconst auto darkModeEnabled\n\t\t\t= Core::App().settings().systemDarkModeEnabled();\n\t\tif (darkModeEnabled && darkMode.has_value()) {\n\t\t\t_nightThemeSwitches.fire_copy(*darkMode);\n\t\t}\n\t}, _nightThemeToggle->lifetime());\n}\n\nvoid MainMenu::resizeEvent(QResizeEvent *e) {\n\t_inner->resizeToWidth(width());\n\tupdateControlsGeometry();\n}\n\nvoid MainMenu::updateControlsGeometry() {\n\t_userpicButton->moveToLeft(\n\t\tst::mainMenuUserpicLeft,\n\t\tst::mainMenuUserpicTop);\n\tif (_resetScaleButton) {\n\t\t_resetScaleButton->moveToRight(0, 0);\n\t}\n\t_setEmojiStatus->moveToLeft(\n\t\tst::mainMenuCoverStatusLeft,\n\t\tst::mainMenuCoverStatusTop,\n\t\twidth());\n\t_toggleAccounts->setGeometry(\n\t\t0,\n\t\tst::mainMenuCoverNameTop,\n\t\twidth(),\n\t\tst::mainMenuCoverHeight - st::mainMenuCoverNameTop);\n\t// Allow cover shadow over the scrolled content.\n\tconst auto top = st::mainMenuCoverHeight - st::lineWidth;\n\t_scroll->setGeometry(0, top, width(), height() - top);\n\tupdateInnerControlsGeometry();\n}\n\nvoid MainMenu::updateInnerControlsGeometry() {\n\tconst auto contentHeight = _accounts->height()\n\t\t+ _shadow->height()\n\t\t+ st::mainMenuSkip\n\t\t+ _menu->height();\n\tconst auto available = height() - st::mainMenuCoverHeight - contentHeight;\n\tconst auto footerHeight = std::max(\n\t\tavailable,\n\t\tst::mainMenuFooterHeightMin);\n\tif (_footer->height() != footerHeight) {\n\t\t_footer->resize(_footer->width(), footerHeight);\n\t}\n}\n\nvoid MainMenu::chooseEmojiStatus() {\n\tif (_controller->showFrozenError()) {\n\t\treturn;\n\t} else if (const auto widget = _badge->widget()) {\n\t\t_emojiStatusPanel->show(_controller, widget, _badge->sizeTag());\n\t} else {\n\t\tShowPremiumPreviewBox(_controller, PremiumFeature::EmojiStatus);\n\t}\n}\n\nbool MainMenu::eventHook(QEvent *event) {\n\tconst auto type = event->type();\n\tif (type == QEvent::TouchBegin\n\t\t|| type == QEvent::TouchUpdate\n\t\t|| type == QEvent::TouchEnd\n\t\t|| type == QEvent::TouchCancel) {\n\t\tQGuiApplication::sendEvent(_inner, event);\n\t}\n\treturn RpWidget::eventHook(event);\n}\n\nvoid MainMenu::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\tconst auto clip = e->rect();\n\tconst auto cover = QRect(0, 0, width(), st::mainMenuCoverHeight);\n\n\tp.fillRect(clip, st::mainMenuBg);\n\tif (cover.intersects(clip)) {\n\t\tdrawName(p);\n\t}\n}\n\nvoid MainMenu::drawName(Painter &p) {\n\tconst auto widthText = width()\n\t\t- st::mainMenuCoverNameLeft\n\t\t- _toggleAccounts->rightSkip();\n\n\tconst auto user = _controller->session().user();\n\tif (_nameVersion < user->nameVersion()) {\n\t\t_nameVersion = user->nameVersion();\n\t\t_name.setText(\n\t\t\tst::semiboldTextStyle,\n\t\t\tuser->name(),\n\t\t\tUi::NameTextOptions());\n\t\tmoveBadge();\n\t}\n\tp.setFont(st::semiboldFont);\n\tp.setPen(st::windowBoldFg);\n\t_name.drawLeftElided(\n\t\tp,\n\t\tst::mainMenuCoverNameLeft,\n\t\tst::mainMenuCoverNameTop,\n\t\t(widthText\n\t\t\t- (_badge->widget()\n\t\t\t\t? (st::semiboldFont->spacew + _badge->widget()->width())\n\t\t\t\t: 0)),\n\t\twidth());\n}\n\nvoid MainMenu::initResetScaleButton() {\n\t_controller->widget()->screenValue(\n\t) | rpl::map([](not_null<QScreen*> screen) {\n\t\treturn rpl::single(\n\t\t\tscreen->availableGeometry()\n\t\t) | rpl::then(\n\t\t\tbase::qt_signal_producer(\n\t\t\t\tscreen.get(),\n\t\t\t\t&QScreen::availableGeometryChanged\n\t\t\t)\n\t\t);\n\t}) | rpl::flatten_latest(\n\t) | rpl::map([](QRect available) {\n\t\treturn (available.width() >= st::windowMinWidth)\n\t\t\t&& (available.height() >= st::windowMinHeight);\n\t}) | rpl::distinct_until_changed(\n\t) | rpl::on_next([=](bool good) {\n\t\tif (good) {\n\t\t\t_resetScaleButton.destroy();\n\t\t} else {\n\t\t\t_resetScaleButton.create(this);\n\t\t\t_resetScaleButton->addClickHandler([] {\n\t\t\t\tcSetConfigScale(style::kScaleDefault);\n\t\t\t\tLocal::writeSettings();\n\t\t\t\tCore::Restart();\n\t\t\t});\n\t\t\t_resetScaleButton->show();\n\t\t\tupdateControlsGeometry();\n\t\t}\n\t}, lifetime());\n}\n\nOthersUnreadState OtherAccountsUnreadStateCurrent(\n\t\tnot_null<Main::Account*> current) {\n\tauto &domain = Core::App().domain();\n\tauto counter = 0;\n\tauto allMuted = true;\n\tfor (const auto &[index, account] : domain.accounts()) {\n\t\tif (account.get() == current) {\n\t\t\tcontinue;\n\t\t} else if (const auto session = account->maybeSession()) {\n\t\t\tcounter += session->data().unreadWithMentionsBadge();\n\t\t\tif (!session->data().unreadWithMentionsBadgeMuted()) {\n\t\t\t\tallMuted = false;\n\t\t\t}\n\t\t}\n\t}\n\treturn {\n\t\t.count = counter,\n\t\t.allMuted = allMuted,\n\t};\n}\n\nrpl::producer<OthersUnreadState> OtherAccountsUnreadState(\n\t\tnot_null<Main::Account*> current) {\n\treturn rpl::single(rpl::empty) | rpl::then(\n\t\tCore::App().unreadBadgeChanges()\n\t) | rpl::map([=] {\n\t\treturn OtherAccountsUnreadStateCurrent(current);\n\t});\n}\n\nbase::EventFilterResult MainMenu::redirectToInnerChecked(not_null<QEvent*> e) {\n\tif (_insideEventRedirect) {\n\t\treturn base::EventFilterResult::Continue;\n\t}\n\tconst auto weak = base::make_weak(this);\n\t_insideEventRedirect = true;\n\tQGuiApplication::sendEvent(_inner, e);\n\tif (weak) {\n\t\t_insideEventRedirect = false;\n\t}\n\treturn base::EventFilterResult::Cancel;\n}\n\nvoid MainMenu::setupSwipe() {\n\tconst auto outer = _controller->widget()->body();\n\tbase::install_event_filter(this, outer, [=](not_null<QEvent*> e) {\n\t\tconst auto type = e->type();\n\t\tif (type == QEvent::TouchBegin\n\t\t\t|| type == QEvent::TouchUpdate\n\t\t\t|| type == QEvent::TouchEnd\n\t\t\t|| type == QEvent::TouchCancel) {\n\t\t\treturn redirectToInnerChecked(e);\n\t\t} else if (type == QEvent::Wheel) {\n\t\t\tconst auto w = static_cast<QWheelEvent*>(e.get());\n\t\t\tconst auto d = Ui::ScrollDeltaF(w);\n\t\t\tif (std::abs(d.x()) > std::abs(d.y())) {\n\t\t\t\treturn redirectToInnerChecked(e);\n\t\t\t}\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\tconst auto handles = outer->testAttribute(Qt::WA_AcceptTouchEvents);\n\tif (!handles) {\n\t\touter->setAttribute(Qt::WA_AcceptTouchEvents);\n\t\tlifetime().add([=] {\n\t\t\touter->setAttribute(Qt::WA_AcceptTouchEvents, false);\n\t\t});\n\t}\n\n\tauto update = [=](Ui::Controls::SwipeContextData data) {\n\t\tif (data.translation < 0) {\n\t\t\tif (!_swipeBackData.callback) {\n\t\t\t\t_swipeBackData = Ui::Controls::SetupSwipeBack(\n\t\t\t\t\tthis,\n\t\t\t\t\t[=]() -> std::pair<QColor, QColor> {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tst::historyForwardChooseBg->c,\n\t\t\t\t\t\t\tst::historyForwardChooseFg->c,\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t}\n\t\t\t_swipeBackData.callback(data);\n\t\t\treturn;\n\t\t} else if (_swipeBackData.lifetime) {\n\t\t\t_swipeBackData = {};\n\t\t}\n\t};\n\n\tauto init = [=](int, Qt::LayoutDirection direction) {\n\t\tif (direction != Qt::LeftToRight) {\n\t\t\treturn Ui::Controls::SwipeHandlerFinishData();\n\t\t}\n\t\tif (_emojiStatusPanel && _emojiStatusPanel->hasFocus()) {\n\t\t\treturn Ui::Controls::SwipeHandlerFinishData();\n\t\t}\n\t\treturn Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {\n\t\t\tcloseLayer();\n\t\t});\n\t};\n\n\tUi::Controls::SetupSwipeHandler({\n\t\t.widget = _inner,\n\t\t.scroll = _scroll.data(),\n\t\t.update = std::move(update),\n\t\t.init = std::move(init),\n\t});\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_main_menu.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"base/object_ptr.h\"\n#include \"base/binary_guard.h\"\n#include \"ui/rp_widget.h\"\n#include \"ui/unread_badge.h\"\n#include \"ui/controls/swipe_handler_data.h\"\n#include \"ui/layers/layer_widget.h\"\n\nnamespace base {\nenum class EventFilterResult;\n} // namespace base\n\nnamespace Ui {\nclass IconButton;\nclass FlatLabel;\nclass UserpicButton;\nclass PopupMenu;\nclass ScrollArea;\nclass VerticalLayout;\nclass RippleButton;\nclass PlainShadow;\nclass SettingsButton;\ntemplate <typename Widget>\nclass SlideWrap;\n} // namespace Ui\n\nnamespace Info::Profile {\nclass Badge;\nclass EmojiStatusPanel;\n} // namespace Info::Profile\n\nnamespace Main {\nclass Account;\n} // namespace Main\n\nnamespace Window {\n\nclass SessionController;\n\nclass MainMenu final : public Ui::LayerWidget {\npublic:\n\tMainMenu(QWidget *parent, not_null<SessionController*> controller);\n\t~MainMenu();\n\n\tvoid parentResized() override;\n\tvoid showFinished() override;\n\nprivate:\n\tclass ToggleAccountsButton;\n\tclass ResetScaleButton;\n\n\tbool eventHook(QEvent *event) override;\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\n\tvoid doSetInnerFocus() override {\n\t\tsetFocus();\n\t}\n\n\tvoid moveBadge();\n\tvoid setupUserpicButton();\n\tvoid setupAccounts();\n\tvoid setupAccountsToggle();\n\tvoid setupSetEmojiStatus();\n\tvoid setupArchive();\n\tvoid setupMenu();\n\tvoid updateControlsGeometry();\n\tvoid updateInnerControlsGeometry();\n\tvoid initResetScaleButton();\n\tvoid toggleAccounts();\n\tvoid chooseEmojiStatus();\n\tvoid setupSwipe();\n\n\t[[nodiscard]] base::EventFilterResult redirectToInnerChecked(\n\t\tnot_null<QEvent*> e);\n\n\tvoid drawName(Painter &p);\n\n\tconst not_null<SessionController*> _controller;\n\tobject_ptr<Ui::UserpicButton> _userpicButton;\n\tUi::Text::String _name;\n\tint _nameVersion = 0;\n\tobject_ptr<ToggleAccountsButton> _toggleAccounts;\n\tobject_ptr<Ui::FlatLabel> _setEmojiStatus;\n\tstd::unique_ptr<Info::Profile::EmojiStatusPanel> _emojiStatusPanel;\n\tstd::unique_ptr<Info::Profile::Badge> _badge;\n\tobject_ptr<ResetScaleButton> _resetScaleButton = { nullptr };\n\tobject_ptr<Ui::ScrollArea> _scroll;\n\tnot_null<Ui::VerticalLayout*> _inner;\n\tnot_null<Ui::RpWidget*> _topShadowSkip;\n\tnot_null<Ui::SlideWrap<Ui::VerticalLayout>*> _accounts;\n\tnot_null<Ui::SlideWrap<Ui::PlainShadow>*> _shadow;\n\tnot_null<Ui::VerticalLayout*> _menu;\n\tnot_null<Ui::RpWidget*> _footer;\n\tnot_null<Ui::FlatLabel*> _telegram;\n\tnot_null<Ui::FlatLabel*> _version;\n\tQPointer<Ui::SettingsButton> _nightThemeToggle;\n\trpl::event_stream<bool> _nightThemeSwitches;\n\tbase::Timer _nightThemeSwitch;\n\tbase::unique_qptr<Ui::PopupMenu> _contextMenu;\n\n\tUi::Controls::SwipeBackResult _swipeBackData;\n\n\trpl::variable<bool> _showFinished = false;\n\tbool _insideEventRedirect = false;\n\n};\n\nstruct OthersUnreadState {\n\tint count = 0;\n\tbool allMuted = false;\n};\n\n[[nodiscard]] OthersUnreadState OtherAccountsUnreadStateCurrent(\n\tnot_null<Main::Account*> current);\n[[nodiscard]] rpl::producer<OthersUnreadState> OtherAccountsUnreadState(\n\tnot_null<Main::Account*> current);\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_main_menu_helpers.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_main_menu_helpers.h\"\n\n#include \"apiwrap.h\"\n#include \"base/platform/base_platform_info.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_session.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/new_badges.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/tooltip.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_window.h\"\n\nnamespace Window {\n\n[[nodiscard]] not_null<Ui::FlatLabel*> AddVersionLabel(\n\t\tnot_null<Ui::RpWidget*> parent) {\n\tconst auto label = Ui::CreateChild<Ui::FlatLabel>(\n\t\tparent.get(),\n\t\tst::mainMenuVersionLabel);\n\tif constexpr (!Platform::IsMacStoreBuild()\n\t\t&& !Platform::IsWindowsStoreBuild()) {\n\t\tUi::InstallTooltip(label, [] {\n\t\t\treturn u\"Build date: %1.\"_q.arg(__DATE__);\n\t\t});\n\t}\n\treturn label;\n}\n\nnot_null<Ui::SettingsButton*> AddMyChannelsBox(\n\t\tnot_null<Ui::SettingsButton*> button,\n\t\tnot_null<SessionController*> controller,\n\t\tbool chats) {\n\tbutton->setAcceptBoth(true);\n\n\tconst auto requestIcon = [=, session = &controller->session()](\n\t\t\tnot_null<Ui::GenericBox*> box,\n\t\t\tFn<void(not_null<DocumentData*>)> done) {\n\t\tconst auto api = box->lifetime().make_state<MTP::Sender>(\n\t\t\t&session->mtp());\n\t\tapi->request(MTPmessages_GetStickerSet(\n\t\t\tData::InputStickerSet({\n\t\t\t\t.shortName = u\"tg_placeholders_android\"_q,\n\t\t\t}),\n\t\t\tMTP_int(0)\n\t\t)).done([=](const MTPmessages_StickerSet &result) {\n\t\t\tresult.match([&](const MTPDmessages_stickerSet &data) {\n\t\t\t\tconst auto &v = data.vdocuments().v;\n\t\t\t\tif (v.size() > 1) {\n\t\t\t\t\tdone(session->data().processDocument(v[1]));\n\t\t\t\t}\n\t\t\t}, [](const MTPDmessages_stickerSetNotModified &) {\n\t\t\t});\n\t\t}).send();\n\t};\n\tconst auto addIcon = [=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto widget = box->addRow(object_ptr<Ui::RpWidget>(box));\n\t\twidget->paintRequest(\n\t\t) | rpl::on_next([=] {\n\t\t\tauto p = QPainter(widget);\n\t\t\tp.setFont(st::boxTextFont);\n\t\t\tp.setPen(st::windowSubTextFg);\n\t\t\tp.drawText(\n\t\t\t\twidget->rect(),\n\t\t\t\ttr::lng_contacts_loading(tr::now),\n\t\t\t\tstyle::al_center);\n\t\t}, widget->lifetime());\n\t\twidget->resize(Size(st::maxStickerSize));\n\t\twidget->show();\n\t\tbox->verticalLayout()->resizeToWidth(box->width());\n\t\trequestIcon(box, [=](not_null<DocumentData*> document) {\n\t\t\tconst auto view = document->createMediaView();\n\t\t\tconst auto origin = document->stickerSetOrigin();\n\t\t\tcontroller->session().downloaderTaskFinished(\n\t\t\t) | rpl::take_while([=] {\n\t\t\t\tif (view->bytes().isEmpty()) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tauto owned = Lottie::MakeIcon({\n\t\t\t\t\t.json = Images::UnpackGzip(view->bytes()),\n\t\t\t\t\t.sizeOverride = Size(st::maxStickerSize),\n\t\t\t\t});\n\t\t\t\tconst auto icon = owned.get();\n\t\t\t\twidget->lifetime().add([kept = std::move(owned)]{});\n\t\t\t\twidget->paintRequest(\n\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\tauto p = QPainter(widget);\n\t\t\t\t\ticon->paint(p, (widget->width() - icon->width()) / 2, 0);\n\t\t\t\t}, widget->lifetime());\n\t\t\t\ticon->animate(\n\t\t\t\t\t[=] { widget->update(); },\n\t\t\t\t\t0,\n\t\t\t\t\ticon->framesCount());\n\t\t\t\treturn false;\n\t\t\t}) | rpl::start(widget->lifetime());\n\t\t\tview->automaticLoad(origin, nullptr);\n\t\t\tview->videoThumbnailWanted(origin);\n\t\t});\n\t};\n\n\tconst auto myChannelsBox = [=](not_null<Ui::GenericBox*> box) {\n\t\tbox->setTitle(chats\n\t\t\t? tr::lng_notification_groups()\n\t\t\t: tr::lng_notification_channels());\n\t\tbox->addButton(tr::lng_close(), [=] { box->closeBox(); });\n\n\t\tconst auto st = box->lifetime().make_state<style::UserpicButton>(\n\t\t\tst::defaultUserpicButton);\n\t\tst->photoSize = st::defaultPeerListItem.photoSize;\n\t\tst->size = QSize(st->photoSize, st->photoSize);\n\n\t\tconst auto megagroupMark = u\"[s] \"_q;\n\t\tconst auto add = [&](\n\t\t\t\tnot_null<PeerData*> peer,\n\t\t\t\tnot_null<Ui::VerticalLayout*> container) {\n\t\t\tconst auto row = container->add(\n\t\t\t\tobject_ptr<Ui::AbstractButton>::fromRaw(\n\t\t\t\t\tUi::CreateSimpleSettingsButton(\n\t\t\t\t\t\tcontainer,\n\t\t\t\t\t\tst::defaultRippleAnimation,\n\t\t\t\t\t\tst::defaultSettingsButton.textBgOver)));\n\t\t\trow->resize(row->width(), st::defaultPeerListItem.height);\n\t\t\tconst auto c = peer->asChannel();\n\t\t\tconst auto g = peer->asChat();\n\t\t\tconst auto count = c ? c->membersCount() : g->count;\n\t\t\tconst auto text = std::make_shared<Ui::Text::String>(\n\t\t\t\tst::defaultPeerListItem.nameStyle,\n\t\t\t\t((c && c->isMegagroup()) ? megagroupMark : QString())\n\t\t\t\t\t+ peer->name());\n\t\t\tconst auto status = std::make_shared<Ui::Text::String>(\n\t\t\t\tst::defaultTextStyle,\n\t\t\t\t(g && !g->amIn())\n\t\t\t\t\t? tr::lng_chat_status_unaccessible(tr::now)\n\t\t\t\t\t: !peer->username().isEmpty()\n\t\t\t\t\t? ('@' + peer->username())\n\t\t\t\t\t: (count > 0)\n\t\t\t\t\t? ((c && !c->isMegagroup())\n\t\t\t\t\t\t? tr::lng_chat_status_subscribers\n\t\t\t\t\t\t: tr::lng_chat_status_members)(\n\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\tcount)\n\t\t\t\t\t: QString());\n\t\t\trow->paintRequest() | rpl::on_next([=] {\n\t\t\t\tauto p = QPainter(row);\n\t\t\t\tconst auto &st = st::defaultPeerListItem;\n\t\t\t\tconst auto availableWidth = row->width()\n\t\t\t\t\t- st::boxRowPadding.right()\n\t\t\t\t\t- st.namePosition.x();\n\t\t\t\tp.setPen(st.nameFg);\n\t\t\t\tauto context = Ui::Text::PaintContext{\n\t\t\t\t\t.position = st.namePosition,\n\t\t\t\t\t.outerWidth = availableWidth,\n\t\t\t\t\t.availableWidth = availableWidth,\n\t\t\t\t\t.elisionLines = 1,\n\t\t\t\t};\n\t\t\t\ttext->draw(p, context);\n\t\t\t\tp.setPen(st.statusFg);\n\t\t\t\tcontext.position = st.statusPosition;\n\t\t\t\tstatus->draw(p, context);\n\t\t\t}, row->lifetime());\n\t\t\trow->setClickedCallback([=] {\n\t\t\t\tcontroller->showPeerHistory(peer);\n\t\t\t});\n\t\t\tusing Button = Ui::UserpicButton;\n\t\t\tconst auto userpic = Ui::CreateChild<Button>(row, peer, *st);\n\t\t\tuserpic->move(st::defaultPeerListItem.photoPosition);\n\t\t\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t};\n\n\t\tconst auto inaccessibleWrap = box->verticalLayout()->add(\n\t\t\tobject_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(\n\t\t\t\tbox->verticalLayout(),\n\t\t\t\tobject_ptr<Ui::VerticalLayout>(box->verticalLayout())));\n\t\tinaccessibleWrap->toggle(false, anim::type::instant);\n\n\t\tconst auto &data = controller->session().data();\n\t\tauto ids = std::vector<PeerId>();\n\t\tauto inaccessibleIds = std::vector<PeerId>();\n\n\t\tif (chats) {\n\t\t\tdata.enumerateGroups([&](not_null<PeerData*> peer) {\n\t\t\t\tpeer = peer->migrateToOrMe();\n\t\t\t\tif (ranges::contains(ids, peer->id)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst auto c = peer->asChannel();\n\t\t\t\tconst auto g = peer->asChat();\n\t\t\t\tif ((c && c->amCreator()) || (g && g->amCreator())) {\n\t\t\t\t\tif (g && !g->amIn()) {\n\t\t\t\t\t\tinaccessibleIds.push_back(peer->id);\n\t\t\t\t\t\tadd(peer, inaccessibleWrap->entity());\n\t\t\t\t\t} else {\n\t\t\t\t\t\tadd(peer, box->verticalLayout());\n\t\t\t\t\t}\n\t\t\t\t\tids.push_back(peer->id);\n\t\t\t\t}\n\t\t\t});\n\t\t} else {\n\t\t\tdata.enumerateBroadcasts([&](not_null<ChannelData*> channel) {\n\t\t\t\tif (channel->amCreator()\n\t\t\t\t\t&& !ranges::contains(ids, channel->id)) {\n\t\t\t\t\tids.push_back(channel->id);\n\t\t\t\t\tadd(channel, box->verticalLayout());\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\tif (ids.empty()) {\n\t\t\taddIcon(box);\n\t\t}\n\t\tif (!inaccessibleIds.empty()) {\n\t\t\tconst auto icon = [=] {\n\t\t\t\treturn !inaccessibleWrap->toggled()\n\t\t\t\t\t? &st::menuIconGroups\n\t\t\t\t\t: &st::menuIconGroupsHide;\n\t\t\t};\n\t\t\tauto button = object_ptr<Ui::IconButton>(box, st::backgroundSwitchToDark);\n\t\t\tbutton->setClickedCallback([=, raw = button.data()] {\n\t\t\t\tinaccessibleWrap->toggle(\n\t\t\t\t\t!inaccessibleWrap->toggled(),\n\t\t\t\t\tanim::type::normal);\n\t\t\t\traw->setIconOverride(icon(), icon());\n\t\t\t});\n\t\t\tbutton->setIconOverride(icon(), icon());\n\t\t\tbox->addTopButton(std::move(button));\n\t\t}\n\t};\n\n\tusing Menu = base::unique_qptr<Ui::PopupMenu>;\n\tconst auto menu = button->lifetime().make_state<Menu>();\n\tbutton->addClickHandler([=](Qt::MouseButton which) {\n\t\tif (which != Qt::RightButton) {\n\t\t\treturn;\n\t\t}\n\n\t\t(*menu) = base::make_unique_q<Ui::PopupMenu>(\n\t\t\tbutton,\n\t\t\tst::popupMenuWithIcons);\n\t\t(*menu)->addAction(\n\t\t\t(chats ? tr::lng_menu_my_groups : tr::lng_menu_my_channels)(\n\t\t\t\ttr::now),\n\t\t\t[=] { controller->uiShow()->showBox(Box(myChannelsBox)); },\n\t\t\tchats ? &st::menuIconGroups : &st::menuIconChannel);\n\t\t(*menu)->popup(QCursor::pos());\n\t});\n\n\treturn button;\n}\n\nvoid SetupMenuBots(\n\t\tnot_null<Ui::VerticalLayout*> container,\n\t\tnot_null<Window::SessionController*> controller) {\n\tconst auto wrap = container->add(\n\t\tobject_ptr<Ui::VerticalLayout>(container));\n\tconst auto bots = &controller->session().attachWebView();\n\tconst auto iconLoadLifetime = wrap->lifetime().make_state<\n\t\trpl::lifetime\n\t>();\n\n\trpl::single(\n\t\trpl::empty\n\t) | rpl::then(\n\t\tbots->attachBotsUpdates()\n\t) | rpl::on_next([=] {\n\t\tconst auto width = container->widthNoMargins();\n\t\twrap->clear();\n\t\tfor (const auto &bot : bots->attachBots()) {\n\t\t\tconst auto user = bot.user;\n\t\t\tif (!bot.inMainMenu || !bot.media) {\n\t\t\t\tcontinue;\n\t\t\t} else if (const auto media = bot.media; !media->loaded()) {\n\t\t\t\tif (!*iconLoadLifetime) {\n\t\t\t\t\tauto &session = user->session();\n\t\t\t\t\t*iconLoadLifetime = session.downloaderTaskFinished(\n\t\t\t\t\t) | rpl::on_next([=] {\n\t\t\t\t\t\tif (media->loaded()) {\n\t\t\t\t\t\t\ticonLoadLifetime->destroy();\n\t\t\t\t\t\t\tbots->notifyBotIconLoaded();\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst auto button = wrap->add(object_ptr<Ui::SettingsButton>(\n\t\t\t\twrap,\n\t\t\t\trpl::single(bot.name),\n\t\t\t\tst::mainMenuButton));\n\t\t\tconst auto menu = button->lifetime().make_state<\n\t\t\t\tbase::unique_qptr<Ui::PopupMenu>\n\t\t\t>();\n\t\t\tconst auto icon = Ui::CreateChild<InlineBots::MenuBotIcon>(\n\t\t\t\tbutton,\n\t\t\t\tbot.media);\n\t\t\tbutton->heightValue(\n\t\t\t) | rpl::on_next([=](int height) {\n\t\t\t\ticon->move(\n\t\t\t\t\tst::mainMenuButton.iconLeft,\n\t\t\t\t\t(height - icon->height()) / 2);\n\t\t\t}, button->lifetime());\n\t\t\tconst auto weak = base::make_weak(container);\n\t\t\tconst auto show = controller->uiShow();\n\t\t\tbutton->setAcceptBoth(true);\n\t\t\tbutton->clicks(\n\t\t\t) | rpl::on_next([=](Qt::MouseButton which) {\n\t\t\t\tif (which == Qt::LeftButton) {\n\t\t\t\t\tbots->open({\n\t\t\t\t\t\t.bot = user,\n\t\t\t\t\t\t.context = { .controller = controller },\n\t\t\t\t\t\t.source = InlineBots::WebViewSourceMainMenu(),\n\t\t\t\t\t});\n\t\t\t\t\tif (weak) {\n\t\t\t\t\t\tcontroller->window().hideSettingsAndLayer();\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t(*menu) = nullptr;\n\t\t\t\t\t(*menu) = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\t\t\tbutton,\n\t\t\t\t\t\tst::popupMenuWithIcons);\n\t\t\t\t\t(*menu)->addAction(\n\t\t\t\t\t\ttr::lng_bot_remove_from_menu(tr::now),\n\t\t\t\t\t\t[=] { bots->removeFromMenu(show, user); },\n\t\t\t\t\t\t&st::menuIconDelete);\n\t\t\t\t\t(*menu)->popup(QCursor::pos());\n\t\t\t\t}\n\t\t\t}, button->lifetime());\n\n\t\t\tif (bots->showMainMenuNewBadge(bot)) {\n\t\t\t\tUi::NewBadge::AddToRight(button);\n\t\t\t}\n\t\t}\n\t\twrap->resizeToWidth(width);\n\t}, wrap->lifetime());\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_main_menu_helpers.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Ui {\nclass FlatLabel;\nclass RpWidget;\nclass SettingsButton;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Window {\n\nclass SessionController;\n\n[[nodiscard]] not_null<Ui::FlatLabel*> AddVersionLabel(\n\tnot_null<Ui::RpWidget*>);\n\n[[nodiscard]] not_null<Ui::SettingsButton*> AddMyChannelsBox(\n\tnot_null<Ui::SettingsButton*> button,\n\tnot_null<SessionController*> controller,\n\tbool chats);\n\nvoid SetupMenuBots(\n\tnot_null<Ui::VerticalLayout*> container,\n\tnot_null<SessionController*> controller);\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_media_preview.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_media_preview.h\"\n\n#include \"chat_helpers/stickers_emoji_pack.h\"\n#include \"chat_helpers/stickers_lottie.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_document.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_photo.h\"\n#include \"data/data_session.h\"\n#include \"data/stickers/data_stickers.h\"\n#include \"history/view/media/history_view_sticker.h\"\n#include \"lottie/lottie_single_player.h\"\n#include \"main/main_session.h\"\n#include \"ui/emoji_config.h\"\n#include \"ui/image/image.h\"\n#include \"ui/painter.h\"\n#include \"ui/rect.h\"\n#include \"ui/ui_utility.h\"\n#include \"window/window_session_controller.h\"\n#include \"styles/style_chat_helpers.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_layers.h\"\n\nnamespace Window {\nnamespace {\n\nconstexpr auto kStickerPreviewEmojiLimit = 10;\nconstexpr auto kPremiumShift = 21. / 240;\nconstexpr auto kPremiumMultiplier = (1 + 0.245 * 2);\nconstexpr auto kPremiumDownscale = 1.25;\n\n} // namespace\n\nMediaPreviewWidget::MediaPreviewWidget(\n\tQWidget *parent,\n\tnot_null<Window::SessionController*> controller)\n: RpWidget(parent)\n, _controller(controller)\n, _emojiSize(Ui::Emoji::GetSizeLarge() / style::DevicePixelRatio()) {\n\tsetAttribute(Qt::WA_TransparentForMouseEvents);\n\t_controller->session().downloaderTaskFinished(\n\t) | rpl::on_next([=] {\n\t\tupdate();\n\t}, lifetime());\n\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tif (_document && _document->emojiUsesTextColor()) {\n\t\t\t_cache = QPixmap();\n\t\t}\n\t}, lifetime());\n}\n\nQRect MediaPreviewWidget::updateArea() const {\n\tconst auto size = currentDimensions();\n\tconst auto position = QPoint(\n\t\t(width() - size.width()) / 2,\n\t\t(height() - size.height()) / 2 + _contentShiftY);\n\tconst auto premium = _document && _document->isPremiumSticker();\n\tconst auto adjusted = position\n\t\t- (premium\n\t\t\t? QPoint(\n\t\t\t\tsize.width() - (size.width() / 2),\n\t\t\t\tsize.height() / 2)\n\t\t\t: QPoint())\n\t\t+ (!_customPadding.isNull()\n\t\t\t? QPoint(0, _customPadding.top())\n\t\t\t: QPoint());\n\treturn QRect(adjusted, size * (premium ? 2 : 1));\n}\n\nvoid MediaPreviewWidget::paintEvent(QPaintEvent *e) {\n\tauto p = QPainter(this);\n\n\tif (_customRadius > 0) {\n\t\tauto hq = PainterHighQualityEnabler(p);\n\t\tconst auto r = rect() - _backgroundMargins;\n\t\tauto path = QPainterPath();\n\t\tpath.addRoundedRect(r, _customRadius, _customRadius);\n\t\tp.setClipPath(path);\n\t}\n\n\tconst auto r = e->rect();\n\tconst auto factor = style::DevicePixelRatio();\n\tconst auto dimensions = currentDimensions();\n\tconst auto frame = (_lottie && _lottie->ready())\n\t\t? _lottie->frameInfo({\n\t\t\t.box = dimensions * factor,\n\t\t\t.colored = ((_document && _document->emojiUsesTextColor())\n\t\t\t\t? st::windowFg->c\n\t\t\t\t: QColor(0, 0, 0, 0)),\n\t\t})\n\t\t: Lottie::Animation::FrameInfo();\n\tconst auto effect = (_effect && _effect->ready())\n\t\t? _effect->frameInfo({ dimensions * kPremiumMultiplier * factor })\n\t\t: Lottie::Animation::FrameInfo();\n\tconst auto image = frame.image;\n\tconst auto effectImage = effect.image;\n\t//const auto framesCount = !image.isNull() ? _lottie->framesCount() : 1;\n\t//const auto effectsCount = !effectImage.isNull()\n\t//\t? _effect->framesCount()\n\t//\t: 1;\n\tconst auto pixmap = image.isNull() ? currentImage() : QPixmap();\n\tconst auto size = image.isNull() ? pixmap.size() : image.size();\n\tconst auto w = size.width() / factor;\n\tconst auto h = size.height() / factor;\n\tconst auto shown = _a_shown.value(_hiding ? 0. : 1.);\n\tif (!_a_shown.animating()) {\n\t\tif (_hiding) {\n\t\t\thide();\n\t\t\t_controller->disableGifPauseReason(\n\t\t\t\tWindow::GifPauseReason::MediaPreview);\n\t\t\treturn;\n\t\t}\n\t} else {\n\t\tp.setOpacity(shown);\n//\t\tw = qMax(qRound(w * (st::stickerPreviewMin\n//\t\t\t+ ((1. - st::stickerPreviewMin) * shown)) / 2.) * 2\n//\t\t\t+ int(w % 2), 1);\n//\t\th = qMax(qRound(h * (st::stickerPreviewMin\n//\t\t\t+ ((1. - st::stickerPreviewMin) * shown)) / 2.) * 2\n//\t\t\t+ int(h % 2), 1);\n\t}\n\tif (_backgroundMargins.isNull()) {\n\t\tp.fillRect(r, st::stickerPreviewBg);\n\t} else {\n\t\tp.fillRect(rect() - _backgroundMargins, st::stickerPreviewBg);\n\t}\n\tif (!_customPadding.isNull()) {\n\t\tp.translate(0, _customPadding.top());\n\t}\n\tconst auto position = innerPosition({ w, h });\n\tif (image.isNull()) {\n\t\tp.drawPixmap(position, pixmap);\n\t} else {\n\t\tp.drawImage(QRect(position, QSize(w, h)), image);\n\t}\n\tif (!effectImage.isNull()) {\n\t\tp.drawImage(\n\t\t\tQRect(outerPosition({ w, h }), effectImage.size() / factor),\n\t\t\teffectImage);\n\t}\n\tif (!_emojiList.empty()) {\n\t\tconst auto emojiCount = _emojiList.size();\n\t\tconst auto emojiWidth = (emojiCount * _emojiSize)\n\t\t\t+ (emojiCount - 1) * st::stickerEmojiSkip;\n\t\tauto emojiLeft = (width() - emojiWidth) / 2;\n\t\tconst auto esize = Ui::Emoji::GetSizeLarge();\n\t\tfor (const auto &emoji : _emojiList) {\n\t\t\tUi::Emoji::Draw(\n\t\t\t\tp,\n\t\t\t\temoji,\n\t\t\t\tesize,\n\t\t\t\temojiLeft,\n\t\t\t\t(height() - h) / 2 - (_emojiSize * 2));\n\t\t\temojiLeft += _emojiSize + st::stickerEmojiSkip;\n\t\t}\n\t}\n\tif (!frame.image.isNull()/*\n\t\t&& (!_effect || ((frame.index % effectsCount) <= effect.index))*/) {\n\t\t_lottie->markFrameShown();\n\t}\n\tif (!effect.image.isNull()/*\n\t\t&& ((effect.index % framesCount) <= frame.index)*/) {\n\t\t_effect->markFrameShown();\n\t}\n}\n\nvoid MediaPreviewWidget::resizeEvent(QResizeEvent *e) {\n\tupdate();\n}\n\nQPoint MediaPreviewWidget::innerPosition(QSize size) const {\n\tif (!_document || !_document->isPremiumSticker()) {\n\t\treturn QPoint(\n\t\t\t(width() - size.width()) / 2,\n\t\t\t(height() - size.height()) / 2 + _contentShiftY);\n\t}\n\tconst auto outer = size * kPremiumMultiplier;\n\tconst auto shift = size.width() * kPremiumShift;\n\treturn outerPosition(size)\n\t\t+ QPoint(\n\t\t\touter.width() - size.width() - shift,\n\t\t\t(outer.height() - size.height()) / 2);\n}\n\nQPoint MediaPreviewWidget::outerPosition(QSize size) const {\n\tconst auto outer = size * kPremiumMultiplier;\n\treturn QPoint(\n\t\t(width() - outer.width()) / 2,\n\t\t(height() - outer.height()) / 2 + _contentShiftY);\n}\n\nvoid MediaPreviewWidget::showPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<DocumentData*> document) {\n\tif (!document\n\t\t|| (!document->isAnimation() && !document->sticker())\n\t\t|| document->isVideoMessage()) {\n\t\thidePreview();\n\t\treturn;\n\t}\n\n\tstartShow();\n\t_origin = origin;\n\t_photo = nullptr;\n\t_photoMedia = nullptr;\n\t_document = document;\n\t_documentMedia = _document->createMediaView();\n\t_documentMedia->thumbnailWanted(_origin);\n\t_documentMedia->videoThumbnailWanted(_origin);\n\t_documentMedia->automaticLoad(_origin, nullptr);\n\tfillEmojiString();\n\tresetGifAndCache();\n}\n\nvoid MediaPreviewWidget::showPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<PhotoData*> photo) {\n\tstartShow();\n\t_origin = origin;\n\t_document = nullptr;\n\t_documentMedia = nullptr;\n\t_photo = photo;\n\t_photoMedia = _photo->createMediaView();\n\tfillEmojiString();\n\tresetGifAndCache();\n}\n\nvoid MediaPreviewWidget::startShow() {\n\t_cache = QPixmap();\n\tif (isHidden() || _a_shown.animating()) {\n\t\tif (isHidden()) {\n\t\t\tshow();\n\t\t\t_controller->enableGifPauseReason(\n\t\t\t\tWindow::GifPauseReason::MediaPreview);\n\t\t}\n\t\t_hiding = false;\n\t\tconst auto duration = _customDuration\n\t\t\t? _customDuration\n\t\t\t: st::stickerPreviewDuration;\n\t\t_a_shown.start([=] { update(); }, 0., 1., duration);\n\t} else {\n\t\tupdate();\n\t}\n}\n\nvoid MediaPreviewWidget::hidePreview() {\n\tif (isHidden()) {\n\t\treturn;\n\t}\n\tif (_gif || _gifThumbnail) {\n\t\t_cache = currentImage();\n\t}\n\t_hiding = true;\n\tconst auto duration = _customDuration\n\t\t? _customDuration\n\t\t: st::stickerPreviewDuration;\n\t_a_shown.start([=] { update(); }, 1., 0., duration);\n\t_photo = nullptr;\n\t_photoMedia = nullptr;\n\t_document = nullptr;\n\t_documentMedia = nullptr;\n\tresetGifAndCache();\n}\n\nvoid MediaPreviewWidget::fillEmojiString() {\n\t_emojiList.clear();\n\tif (_photo || _hideEmoji) {\n\t\treturn;\n\t}\n\tif (const auto sticker = _document->sticker()) {\n\t\tif (const auto list\n\t\t\t= _document->owner().stickers().getEmojiListFromSet(_document)) {\n\t\t\t_emojiList = std::move(*list);\n\t\t\twhile (_emojiList.size() > kStickerPreviewEmojiLimit) {\n\t\t\t\t_emojiList.pop_back();\n\t\t\t}\n\t\t} else if (const auto emoji = Ui::Emoji::Find(sticker->alt)) {\n\t\t\t_emojiList.emplace_back(emoji);\n\t\t}\n\t}\n}\n\nvoid MediaPreviewWidget::resetGifAndCache() {\n\t_lottie = nullptr;\n\t_effect = nullptr;\n\t_gif.reset();\n\t_gifThumbnail.reset();\n\t_gifLastPosition = 0;\n\t_cacheStatus = CacheNotLoaded;\n\t_cachedSize = QSize();\n}\n\nvoid MediaPreviewWidget::setCustomPadding(const QMargins &padding) {\n\t_customPadding = padding;\n\t_cachedSize = QSize();\n\tupdate();\n}\n\nvoid MediaPreviewWidget::setBackgroundMargins(const QMargins &margins) {\n\t_backgroundMargins = margins;\n\tupdate();\n}\n\nvoid MediaPreviewWidget::setCustomRadius(int radius) {\n\t_customRadius = radius;\n\tupdate();\n}\n\nvoid MediaPreviewWidget::setCustomDuration(crl::time duration) {\n\t_customDuration = duration;\n}\n\nvoid MediaPreviewWidget::setHideEmoji(bool hide) {\n\t_hideEmoji = hide;\n\tif (hide) {\n\t\t_emojiList.clear();\n\t}\n}\n\nvoid MediaPreviewWidget::setContentShift(int y) {\n\t_contentShiftY = y;\n\t_cachedSize = QSize();\n\tupdate();\n}\n\nint MediaPreviewWidget::contentBottom() const {\n\tconst auto s = currentDimensions();\n\treturn (height() + s.height()) / 2 + _contentShiftY;\n}\n\nQSize MediaPreviewWidget::currentDimensions() const {\n\tif (!_cachedSize.isEmpty()) {\n\t\treturn _cachedSize;\n\t}\n\tif (!_document && !_photo) {\n\t\t_cachedSize = _cache.size() * style::DevicePixelRatio();\n\t\treturn _cachedSize;\n\t}\n\n\tauto result = QSize();\n\tauto box = QSize();\n\tif (_photo) {\n\t\tresult = QSize(_photo->width(), _photo->height());\n\t\tconst auto skip = st::mediaPreviewPhotoSkip;\n\t\tconst auto shiftSkip = 2 * std::abs(_contentShiftY);\n\t\tbox = QSize(width() - 2 * skip, height() - 2 * skip - shiftSkip);\n\t} else {\n\t\tresult = _document->dimensions;\n\t\tif (result.isEmpty()) {\n\t\t\tconst auto &gif = (_gif && _gif->ready())\n\t\t\t\t? _gif\n\t\t\t\t: _gifThumbnail;\n\t\t\tif (gif && gif->ready()) {\n\t\t\t\tresult = QSize(gif->width(), gif->height());\n\t\t\t}\n\t\t}\n\t\tif (_document->sticker()) {\n\t\t\tbox = QSize(st::maxStickerSize, st::maxStickerSize);\n\t\t\tif (_document->isPremiumSticker()) {\n\t\t\t\tresult = (box /= kPremiumDownscale);\n\t\t\t}\n\t\t} else {\n\t\t\tbox = QSize(2 * st::maxStickerSize, 2 * st::maxStickerSize);\n\t\t}\n\t}\n\tresult = QSize(\n\t\tstd::max(style::ConvertScale(result.width()), 1),\n\t\tstd::max(style::ConvertScale(result.height()), 1));\n\n\tif (!_customPadding.isNull()) {\n\t\tconst auto emojiHeight = _emojiList.empty() ? 0 : (_emojiSize * 3);\n\t\tconst auto widgetBox = QSize(\n\t\t\twidth() - rect::m::sum::h(_customPadding),\n\t\t\theight() - rect::m::sum::v(_customPadding) - emojiHeight);\n\t\tresult = result.scaled(widgetBox, Qt::KeepAspectRatio);\n\t} else {\n\t\tresult = result.scaled(box, Qt::KeepAspectRatio);\n\t}\n\n\tresult = QSize(\n\t\tstd::max(result.width(), 1),\n\t\tstd::max(result.height(), 1));\n\n\tif (_photo) {\n\t\t_cachedSize = result;\n\t}\n\treturn result;\n}\n\nvoid MediaPreviewWidget::createLottieIfReady(\n\t\tnot_null<DocumentData*> document) {\n\tconst auto sticker = document->sticker();\n\tif (!sticker\n\t\t|| !sticker->isLottie()\n\t\t|| _lottie\n\t\t|| !_documentMedia->loaded()) {\n\t\treturn;\n\t} else if (document->isPremiumSticker()\n\t\t&& _documentMedia->videoThumbnailContent().isEmpty()) {\n\t\treturn;\n\t}\n\tconst_cast<MediaPreviewWidget*>(this)->setupLottie();\n}\n\nvoid MediaPreviewWidget::setupLottie() {\n\tExpects(_document != nullptr);\n\n\tconst auto factor = style::DevicePixelRatio();\n\tif (_document->isPremiumSticker()) {\n\t\tconst auto size = HistoryView::Sticker::Size(_document);\n\t\t_cachedSize = size;\n\t\t_lottie = ChatHelpers::LottiePlayerFromDocument(\n\t\t\t_documentMedia.get(),\n\t\t\tnullptr,\n\t\t\tChatHelpers::StickerLottieSize::MessageHistory,\n\t\t\tsize * factor,\n\t\t\tLottie::Quality::High);\n\t\t_effect = _document->session().emojiStickersPack().effectPlayer(\n\t\t\t_document,\n\t\t\t_documentMedia->videoThumbnailContent(),\n\t\t\tQString(),\n\t\t\tStickers::EffectType::PremiumSticker);\n\t} else {\n\t\tconst auto size = currentDimensions();\n\t\t_lottie = std::make_unique<Lottie::SinglePlayer>(\n\t\t\tLottie::ReadContent(\n\t\t\t\t_documentMedia->bytes(),\n\t\t\t\t_document->filepath()),\n\t\t\tLottie::FrameRequest{ size * factor },\n\t\t\tLottie::Quality::High);\n\t}\n\n\tconst auto handler = [=](Lottie::Update update) {\n\t\tv::match(update.data, [&](const Lottie::Information &) {\n\t\t\tthis->update();\n\t\t}, [&](const Lottie::DisplayFrameRequest &) {\n\t\t\tthis->update(updateArea());\n\t\t});\n\t};\n\n\t_lottie->updates() | rpl::on_next(handler, lifetime());\n\tif (_effect) {\n\t\t_effect->updates() | rpl::on_next(handler, lifetime());\n\t}\n}\n\nQPixmap MediaPreviewWidget::currentImage() const {\n\tconst auto blur = Images::PrepareArgs{ .options = Images::Option::Blur };\n\tif (_document) {\n\t\tconst auto sticker = _document->sticker();\n\t\tconst auto webm = sticker && sticker->isWebm();\n\t\tif (sticker && !webm) {\n\t\t\tif (_cacheStatus != CacheLoaded) {\n\t\t\t\tconst_cast<MediaPreviewWidget*>(this)->createLottieIfReady(\n\t\t\t\t\t_document);\n\t\t\t\tif (_lottie && _lottie->ready()) {\n\t\t\t\t\treturn QPixmap();\n\t\t\t\t} else if (const auto image\n\t\t\t\t\t\t= _documentMedia->getStickerLarge()) {\n\t\t\t\t\tconst auto s = currentDimensions();\n\t\t\t\t\t_cache = image->pix(s);\n\t\t\t\t\t_cacheStatus = CacheLoaded;\n\t\t\t\t} else if (_cacheStatus != CacheThumbLoaded\n\t\t\t\t\t&& _document->hasThumbnail()\n\t\t\t\t\t&& _documentMedia->thumbnail()) {\n\t\t\t\t\tconst auto s = currentDimensions();\n\t\t\t\t\t_cache = _documentMedia->thumbnail()->pix(s, blur);\n\t\t\t\t\tif (_document && _document->emojiUsesTextColor()) {\n\t\t\t\t\t\t_cache = Ui::PixmapFromImage(\n\t\t\t\t\t\t\tImages::Colored(\n\t\t\t\t\t\t\t\t_cache.toImage(),\n\t\t\t\t\t\t\t\tst::windowFg->c));\n\t\t\t\t\t}\n\t\t\t\t\t_cacheStatus = CacheThumbLoaded;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tconst_cast<MediaPreviewWidget*>(this)->validateGifAnimation();\n\t\t\tconst auto &gif = (_gif && _gif->started())\n\t\t\t\t? _gif\n\t\t\t\t: _gifThumbnail;\n\t\t\tif (gif && gif->started()) {\n\t\t\t\tconst auto paused = _controller->isGifPausedAtLeastFor(\n\t\t\t\t\tWindow::GifPauseReason::MediaPreview);\n\t\t\t\treturn QPixmap::fromImage(\n\t\t\t\t\tgif->current(\n\t\t\t\t\t\t{ .frame = currentDimensions(), .keepAlpha = webm },\n\t\t\t\t\t\tpaused ? 0 : crl::now()),\n\t\t\t\t\tQt::ColorOnly);\n\t\t\t}\n\t\t\tif (_cacheStatus != CacheThumbLoaded\n\t\t\t\t&& _document->hasThumbnail()) {\n\t\t\t\tconst auto s = currentDimensions();\n\t\t\t\tconst auto thumbnail = _documentMedia->thumbnail();\n\t\t\t\tif (thumbnail) {\n\t\t\t\t\t_cache = thumbnail->pix(s, blur);\n\t\t\t\t\t_cacheStatus = CacheThumbLoaded;\n\t\t\t\t} else if (const auto blurred\n\t\t\t\t\t\t= _documentMedia->thumbnailInline()) {\n\t\t\t\t\t_cache = blurred->pix(s, blur);\n\t\t\t\t\t_cacheStatus = CacheThumbLoaded;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if (_photo) {\n\t\tif (_cacheStatus != CacheLoaded) {\n\t\t\tif (_photoMedia->loaded()) {\n\t\t\t\tconst auto s = currentDimensions();\n\t\t\t\t_cache = _photoMedia->image(Data::PhotoSize::Large)->pix(s);\n\t\t\t\t_cacheStatus = CacheLoaded;\n\t\t\t} else {\n\t\t\t\t_photo->load(_origin);\n\t\t\t\tif (_cacheStatus != CacheThumbLoaded) {\n\t\t\t\t\tconst auto s = currentDimensions();\n\t\t\t\t\tif (const auto thumb\n\t\t\t\t\t\t\t= _photoMedia->image(Data::PhotoSize::Thumbnail)) {\n\t\t\t\t\t\t_cache = thumb->pix(s, blur);\n\t\t\t\t\t\t_cacheStatus = CacheThumbLoaded;\n\t\t\t\t\t} else if (const auto small\n\t\t\t\t\t\t\t= _photoMedia->image(Data::PhotoSize::Small)) {\n\t\t\t\t\t\t_cache = small->pix(s, blur);\n\t\t\t\t\t\t_cacheStatus = CacheThumbLoaded;\n\t\t\t\t\t} else if (const auto blurred\n\t\t\t\t\t\t\t= _photoMedia->thumbnailInline()) {\n\t\t\t\t\t\t_cache = blurred->pix(s, blur);\n\t\t\t\t\t\t_cacheStatus = CacheThumbLoaded;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t_photoMedia->wanted(Data::PhotoSize::Small, _origin);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn _cache;\n}\n\nvoid MediaPreviewWidget::startGifAnimation(\n\t\tconst Media::Clip::ReaderPointer &gif) {\n\tgif->start({ .frame = currentDimensions(), .keepAlpha = _gifWithAlpha });\n}\n\nvoid MediaPreviewWidget::validateGifAnimation() {\n\tExpects(_documentMedia != nullptr);\n\n\tif (_gifThumbnail && _gifThumbnail->started()) {\n\t\tconst auto position = _gifThumbnail->getPositionMs();\n\t\tif (_gif\n\t\t\t&& _gif->ready()\n\t\t\t&& !_gif->started()\n\t\t\t&& (_gifLastPosition > position)) {\n\t\t\tstartGifAnimation(_gif);\n\t\t\t_gifThumbnail.reset();\n\t\t\t_gifLastPosition = 0;\n\t\t\treturn;\n\t\t} else {\n\t\t\t_gifLastPosition = position;\n\t\t}\n\t} else if (_gif || _gif.isBad()) {\n\t\treturn;\n\t}\n\n\tconst auto contentLoaded = _documentMedia->loaded();\n\tconst auto thumbContent = _documentMedia->videoThumbnailContent();\n\tconst auto thumbLoaded = !thumbContent.isEmpty();\n\tif (!contentLoaded\n\t\t&& (_gifThumbnail || _gifThumbnail.isBad() | !thumbLoaded)) {\n\t\treturn;\n\t}\n\tconst auto callback = [=](Media::Clip::Notification notification) {\n\t\tclipCallback(notification);\n\t};\n\t_gifWithAlpha = (_documentMedia->owner()->sticker() != nullptr);\n\tif (contentLoaded) {\n\t\t_gif = Media::Clip::MakeReader(\n\t\t\t_documentMedia->owner()->location(),\n\t\t\t_documentMedia->bytes(),\n\t\t\tstd::move(callback));\n\t} else {\n\t\t_gifThumbnail = Media::Clip::MakeReader(\n\t\t\tthumbContent,\n\t\t\tstd::move(callback));\n\t}\n}\n\nvoid MediaPreviewWidget::clipCallback(\n\t\tMedia::Clip::Notification notification) {\n\tusing namespace Media::Clip;\n\tswitch (notification) {\n\tcase Notification::Reinit: {\n\t\tif (_gifThumbnail && _gifThumbnail->state() == State::Error) {\n\t\t\t_gifThumbnail.setBad();\n\t\t}\n\t\tif (_gif && _gif->state() == State::Error) {\n\t\t\t_gif.setBad();\n\t\t}\n\n\t\tif (_gif\n\t\t\t&& _gif->ready()\n\t\t\t&& !_gif->started()\n\t\t\t&& (!_gifThumbnail || !_gifThumbnail->started())) {\n\t\t\tstartGifAnimation(_gif);\n\t\t} else if (!_gif\n\t\t\t&& _gifThumbnail\n\t\t\t&& _gifThumbnail->ready()\n\t\t\t&& !_gifThumbnail->started()) {\n\t\t\tstartGifAnimation(_gifThumbnail);\n\t\t}\n\t\tupdate();\n\t} break;\n\n\tcase Notification::Repaint: {\n\t\tif ((_gif && _gif->started() && !_gif->currentDisplayed())\n\t\t\t|| (_gifThumbnail\n\t\t\t\t&& _gifThumbnail->started()\n\t\t\t\t&& !_gifThumbnail->currentDisplayed())) {\n\t\t\tupdate(updateArea());\n\t\t}\n\t} break;\n\t}\n}\n\nMediaPreviewWidget::~MediaPreviewWidget() {\n\tif (!isHidden()) {\n\t\t_controller->disableGifPauseReason(\n\t\t\tWindow::GifPauseReason::MediaPreview);\n\t}\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_media_preview.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"media/clip/media_clip_reader.h\"\n#include \"ui/effects/animations.h\"\n#include \"data/data_file_origin.h\"\n#include \"ui/rp_widget.h\"\n\nnamespace Data {\nclass PhotoMedia;\nclass DocumentMedia;\n} // namespace Data\n\nnamespace Lottie {\nclass SinglePlayer;\n} // namespace Lottie\n\nnamespace Window {\n\nclass SessionController;\n\nclass MediaPreviewWidget final : public Ui::RpWidget {\npublic:\n\tMediaPreviewWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Window::SessionController*> controller);\n\n\tvoid showPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<DocumentData*> document);\n\tvoid showPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<PhotoData*> photo);\n\tvoid hidePreview();\n\tvoid setCustomPadding(const QMargins &padding = QMargins());\n\tvoid setBackgroundMargins(const QMargins &margins = QMargins());\n\tvoid setCustomRadius(int radius);\n\tvoid setCustomDuration(crl::time duration);\n\tvoid setHideEmoji(bool hide);\n\tvoid setContentShift(int y);\n\t[[nodiscard]] int contentBottom() const;\n\n\t~MediaPreviewWidget();\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid validateGifAnimation();\n\tvoid startGifAnimation(const Media::Clip::ReaderPointer &gif);\n\tQSize currentDimensions() const;\n\tQPixmap currentImage() const;\n\tvoid createLottieIfReady(not_null<DocumentData*> document);\n\tvoid setupLottie();\n\tvoid startShow();\n\tvoid fillEmojiString();\n\tvoid resetGifAndCache();\n\t[[nodiscard]] QPoint innerPosition(QSize size) const;\n\t[[nodiscard]] QPoint outerPosition(QSize size) const;\n\t[[nodiscard]] QRect updateArea() const;\n\n\tnot_null<Window::SessionController*> _controller;\n\n\tUi::Animations::Simple _a_shown;\n\tbool _hiding = false;\n\tData::FileOrigin _origin;\n\tPhotoData *_photo = nullptr;\n\tDocumentData *_document = nullptr;\n\tstd::shared_ptr<Data::PhotoMedia> _photoMedia;\n\tstd::shared_ptr<Data::DocumentMedia> _documentMedia;\n\tMedia::Clip::ReaderPointer _gif, _gifThumbnail;\n\tbool _gifWithAlpha = false;\n\tcrl::time _gifLastPosition = 0;\n\tstd::unique_ptr<Lottie::SinglePlayer> _lottie;\n\tstd::unique_ptr<Lottie::SinglePlayer> _effect;\n\n\tint _emojiSize;\n\tstd::vector<not_null<EmojiPtr>> _emojiList;\n\n\tvoid clipCallback(Media::Clip::Notification notification);\n\n\tenum CacheStatus {\n\t\tCacheNotLoaded,\n\t\tCacheThumbLoaded,\n\t\tCacheLoaded,\n\t};\n\tmutable CacheStatus _cacheStatus = CacheNotLoaded;\n\tmutable QPixmap _cache;\n\tmutable QSize _cachedSize;\n\tQMargins _customPadding;\n\tQMargins _backgroundMargins;\n\tint _customRadius = 0;\n\tcrl::time _customDuration = 0;\n\tbool _hideEmoji = false;\n\tint _contentShiftY = 0;\n\n};\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_peer_menu.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_peer_menu.h\"\n\n#include \"base/call_delayed.h\"\n#include \"menu/menu_check_item.h\"\n#include \"boxes/about_box.h\"\n#include \"boxes/share_box.h\"\n#include \"boxes/star_gift_box.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"chat_helpers/message_field.h\"\n#include \"chat_helpers/share_message_phrase_factory.h\"\n#include \"ui/basic_click_handlers.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/wrap/slide_wrap.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"api/api_chat_participants.h\"\n#include \"api/api_global_privacy.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"base/random.h\"\n#include \"base/options.h\"\n#include \"base/unixtime.h\"\n#include \"base/unique_qptr.h\"\n#include \"base/qt/qt_key_modifiers.h\"\n#include \"boxes/delete_messages_box.h\"\n#include \"boxes/max_invite_box.h\"\n#include \"boxes/moderate_messages_box.h\"\n#include \"boxes/select_future_owner_box.h\"\n#include \"boxes/choose_filter_box.h\"\n#include \"boxes/create_poll_box.h\"\n#include \"boxes/edit_todo_list_box.h\"\n#include \"boxes/pin_messages_box.h\"\n#include \"boxes/premium_limits_box.h\"\n#include \"boxes/report_messages_box.h\"\n#include \"boxes/peers/add_bot_to_chat_box.h\"\n#include \"boxes/peers/add_participants_box.h\"\n#include \"boxes/peers/edit_forum_topic_box.h\"\n#include \"boxes/peers/edit_contact_box.h\"\n#include \"boxes/peers/prepare_short_info_box.h\"\n#include \"calls/calls_instance.h\"\n#include \"inline_bots/bot_attach_web_view.h\" // InlineBots::PeerType.\n#include \"ui/toast/toast.h\"\n#include \"ui/text/custom_emoji_helper.h\"\n#include \"ui/text/format_values.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/widgets/chat_filters_tabs_strip.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/checkbox.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/menu/menu_add_action_callback_factory.h\"\n#include \"ui/layers/generic_box.h\"\n#include \"ui/delayed_activation.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/controls/feature_list.h\"\n#include \"ui/ui_utility.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"menu/menu_mute.h\"\n#include \"menu/menu_ttl_validator.h\"\n#include \"apiwrap.h\"\n#include \"mainwidget.h\"\n#include \"api/api_blocked_peers.h\"\n#include \"api/api_chat_filters.h\"\n#include \"api/api_polls.h\"\n#include \"api/api_todo_lists.h\"\n#include \"api/api_updates.h\"\n#include \"mtproto/mtproto_config.h\"\n#include \"history/history.h\"\n#include \"history/history_item_helpers.h\" // GetErrorForSending.\n#include \"history/history_item_components.h\"\n#include \"history/view/history_view_context_menu.h\"\n#include \"history/view/history_view_scheduled_section.h\"\n#include \"window/window_separate_id.h\"\n#include \"window/window_session_controller.h\"\n#include \"window/window_controller.h\"\n#include \"settings/sections/settings_advanced.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"settings/settings_common.h\"\n#include \"support/support_helper.h\"\n#include \"info/info_controller.h\"\n#include \"info/info_memento.h\"\n#include \"info/channel_statistics/boosts/info_boosts_widget.h\"\n#include \"info/channel_statistics/earn/info_channel_earn_widget.h\"\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"info/profile/info_profile_cover.h\"\n#include \"info/profile/info_profile_values.h\"\n#include \"info/statistics/info_statistics_widget.h\"\n#include \"info/stories/info_stories_widget.h\"\n#include \"data/components/scheduled_messages.h\"\n#include \"data/notify/data_notify_settings.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_session.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_poll.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_user.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_histories.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_peer_values.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"core/application.h\"\n#include \"core/ui_integration.h\"\n#include \"export/export_manager.h\"\n#include \"boxes/peers/edit_participants_box.h\"\n#include \"boxes/peers/edit_peer_info_box.h\"\n#include \"boxes/premium_preview_box.h\"\n#include \"styles/style_chat.h\"\n#include \"styles/style_credits.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_window.h\" // st::windowMinWidth\n#include \"styles/style_menu_icons.h\"\n#include \"styles/style_premium.h\"\n#include \"styles/style_settings.h\"\n\n#include <QAction>\n#include <QtWidgets/QApplication>\n\nnamespace Window {\nnamespace {\n\nconstexpr auto kTopicsSearchMinCount = 1;\n\nvoid ShareBotGame(\n\t\tnot_null<UserData*> bot,\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst QString &shortName) {\n\tauto &histories = thread->owner().histories();\n\tconst auto history = thread->owningHistory();\n\tconst auto randomId = base::RandomValue<uint64>();\n\tconst auto replyTo = thread->topicRootId();\n\tconst auto topicRootId = replyTo;\n\tauto flags = MTPmessages_SendMedia::Flags(0);\n\tif (replyTo) {\n\t\tflags |= MTPmessages_SendMedia::Flag::f_reply_to;\n\t}\n\thistories.sendPreparedMessage(\n\t\thistory,\n\t\tFullReplyTo{\n\t\t\t.messageId = { replyTo ? history->peer->id : 0, replyTo },\n\t\t\t.topicRootId = topicRootId,\n\t\t},\n\t\trandomId,\n\t\tData::Histories::PrepareMessage<MTPmessages_SendMedia>(\n\t\t\tMTP_flags(flags),\n\t\t\thistory->peer->input(),\n\t\t\tData::Histories::ReplyToPlaceholder(),\n\t\t\tMTP_inputMediaGame(\n\t\t\t\tMTP_inputGameShortName(\n\t\t\t\t\tbot->inputUser(),\n\t\t\t\t\tMTP_string(shortName))),\n\t\t\tMTP_string(),\n\t\t\tMTP_long(randomId),\n\t\t\tMTPReplyMarkup(),\n\t\t\tMTPVector<MTPMessageEntity>(),\n\t\t\tMTPint(), // schedule_date\n\t\t\tMTPint(), // schedule_repeat_period\n\t\t\tMTPInputPeer(), // send_as\n\t\t\tMTPInputQuickReplyShortcut(),\n\t\t\tMTPlong(),\n\t\t\tMTPlong(),\n\t\t\tMTPSuggestedPost()\n\t\t), [=](const MTPUpdates &, const MTP::Response &) {\n\t}, [=](const MTP::Error &error, const MTP::Response &) {\n\t\thistory->session().api().sendMessageFail(error, history->peer);\n\t});\n}\n\n} // namespace\n\nconst char kOptionViewProfileInChatsListContextMenu[]\n\t= \"view-profile-in-chats-list-context-menu\";\n\nnamespace {\n\nconstexpr auto kArchivedToastDuration = crl::time(5000);\nconstexpr auto kMaxUnreadWithoutConfirmation = 1000;\n\n[[nodiscard]] QString LookupMemberRank(\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user) {\n\tif (const auto chat = peer->asChat()) {\n\t\tconst auto i = chat->memberRanks.find(peerToUser(user->id));\n\t\treturn (i != chat->memberRanks.end()) ? i->second : QString();\n\t} else if (const auto channel = peer->asChannel()) {\n\t\tif (channel->mgInfo) {\n\t\t\tconst auto i = channel->mgInfo->memberRanks.find(\n\t\t\t\tpeerToUser(user->id));\n\t\t\treturn (i != channel->mgInfo->memberRanks.end())\n\t\t\t\t? i->second\n\t\t\t\t: QString();\n\t\t}\n\t}\n\treturn QString();\n}\n\nbase::options::toggle ViewProfileInChatsListContextMenu({\n\t.id = kOptionViewProfileInChatsListContextMenu,\n\t.name = \"Add \\\"View Profile\\\"\",\n\t.description = \"Add \\\"View Profile\\\" to context menu in chat list\",\n});\n\nvoid SetActionText(not_null<QAction*> action, rpl::producer<QString> &&text) {\n\tconst auto lifetime = Ui::CreateChild<rpl::lifetime>(action.get());\n\tstd::move(\n\t\ttext\n\t) | rpl::on_next([=](const QString &actionText) {\n\t\taction->setText(actionText);\n\t}, *lifetime);\n}\n\nvoid MarkAsReadChatList(not_null<Dialogs::MainList*> list) {\n\tauto mark = std::vector<not_null<History*>>();\n\tfor (const auto &row : list->indexed()->all()) {\n\t\tif (const auto history = row->history()) {\n\t\t\tmark.push_back(history);\n\t\t}\n\t}\n\tranges::for_each(mark, MarkAsReadThread);\n}\n\nvoid PeerMenuAddMuteSubmenuAction(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst PeerMenuCallback &addAction) {\n\tconst auto notifySettings = &thread->owner().notifySettings();\n\tnotifySettings->request(thread);\n\tconst auto weak = base::make_weak(thread);\n\tconst auto with = [=](Fn<void(not_null<Data::Thread*>)> callback) {\n\t\treturn [=] {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tcallback(strong);\n\t\t\t}\n\t\t};\n\t};\n\tconst auto isMuted = notifySettings->isMuted(thread);\n\tif (isMuted) {\n\t\tconst auto text = tr::lng_context_unmute(tr::now)\n\t\t\t+ '\\t'\n\t\t\t+ Ui::FormatMuteForTiny(thread->notify().muteUntil().value_or(0)\n\t\t\t\t- base::unixtime::now());\n\t\taddAction(text, with([=](not_null<Data::Thread*> thread) {\n\t\t\tnotifySettings->update(thread, { .unmute = true });\n\t\t}), &st::menuIconUnmute);\n\t} else {\n\t\tconst auto show = controller->uiShow();\n\t\taddAction(PeerMenuCallback::Args{\n\t\t\t.text = tr::lng_context_mute(tr::now),\n\t\t\t.handler = nullptr,\n\t\t\t.icon = (notifySettings->sound(thread).none\n\t\t\t\t? &st::menuIconSilent\n\t\t\t\t: &st::menuIconMute),\n\t\t\t.fillSubmenu = [&](not_null<Ui::PopupMenu*> menu) {\n\t\t\t\tMuteMenu::FillMuteMenu(menu, thread, show);\n\t\t\t},\n\t\t});\n\t}\n}\n\nFn<void()> GoToFirstMessageHandler(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer) {\n\tconst auto weak = base::make_weak(controller.get());\n\tconst auto jump = [=](const QDate &date) {\n\t\tconst auto chat = peer->owner().history(peer->id);\n\t\tconst auto open = [=](not_null<PeerData*> peer, MsgId id) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->showPeerHistory(peer, SectionShow::Way::Forward, id);\n\t\t\t}\n\t\t};\n\t\tpeer->session().api().resolveJumpToDate(chat, date, open);\n\t};\n\treturn [=] { jump(QDate(2013, 8, 1)); };\n}\n\nclass Filler {\npublic:\n\tFiller(\n\t\tnot_null<SessionController*> controller,\n\t\tDialogs::EntryState request,\n\t\tconst PeerMenuCallback &addAction);\n\tvoid fill();\n\nprivate:\n\tusing Section = Dialogs::EntryState::Section;\n\n\tvoid fillChatsListActions();\n\tvoid fillHistoryActions();\n\tvoid fillProfileActions();\n\tvoid fillRepliesActions();\n\tvoid fillScheduledActions();\n\tvoid fillArchiveActions();\n\tvoid fillSavedSublistActions();\n\tvoid fillContextMenuActions();\n\tvoid fillMonoforumPeerActions();\n\n\tvoid addHidePromotion();\n\tvoid addTogglePin();\n\tvoid addToggleMuteSubmenu(bool addSeparator);\n\tvoid addSupportInfo();\n\tvoid addInfo();\n\tvoid addStoryArchive();\n\tvoid addNewWindow(bool addSeparator = true);\n\tvoid addToggleFolder();\n\tvoid addToggleUnreadMark();\n\tvoid addToggleArchive();\n\tvoid addClearHistory();\n\tvoid addDeleteChat();\n\tvoid addLeaveChat();\n\tvoid addJoinChat();\n\tvoid addTopicLink();\n\tvoid addManageTopic();\n\tvoid addManageChat();\n\tvoid addCreatePoll();\n\tvoid addCreateTodoList();\n\tvoid addThemeEdit();\n\tvoid addToggleNoForwards();\n\tvoid addBlockUser();\n\tvoid addViewDiscussion();\n\tvoid addDirectMessages();\n\tvoid addToggleTopicClosed();\n\tvoid addExportChat();\n\tvoid addTranslate();\n\tvoid addReport();\n\tvoid addNewContact();\n\tvoid addShareContact();\n\tvoid addEditContact();\n\tvoid addBotToGroup();\n\tvoid addNewMembers();\n\tvoid addDeleteContact();\n\tvoid addTTLSubmenu(bool addSeparator);\n\tvoid addSendGift();\n\tvoid addCreateTopic();\n\tvoid addViewAsMessages();\n\tvoid addViewAsTopics();\n\tvoid addSearchTopics();\n\tvoid addDeleteTopic();\n\tvoid addVideoChat();\n\tvoid addViewStatistics();\n\tvoid addBoostChat();\n\tvoid addToggleFee();\n\tvoid addSetPersonalChannel();\n\n\tvoid addGoToFirstMessage();\n\tvoid addGoToScheduled();\n\n\t[[nodiscard]] bool skipCreateActions() const;\n\n\tnot_null<SessionController*> _controller;\n\tDialogs::EntryState _request;\n\tData::Thread *_thread = nullptr;\n\tData::ForumTopic *_topic = nullptr;\n\tPeerData *_peer = nullptr;\n\tData::Folder *_folder = nullptr;\n\tData::SavedSublist *_sublist = nullptr;\n\tconst PeerMenuCallback &_addAction;\n\n};\n\nHistory *FindWastedPin(not_null<Data::Session*> data, Data::Folder *folder) {\n\tconst auto &order = data->pinnedChatsOrder(folder);\n\tfor (const auto &pinned : order) {\n\t\tif (const auto history = pinned.history()) {\n\t\t\tif (history->peer->isChat()\n\t\t\t\t&& history->peer->asChat()->isDeactivated()\n\t\t\t\t&& !history->inChatList()) {\n\t\t\t\treturn history;\n\t\t\t}\n\t\t}\n\t}\n\treturn nullptr;\n}\n\nvoid AddChatMembers(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChatData*> chat) {\n\tAddParticipantsBoxController::Start(navigation, chat);\n}\n\nbool PinnedLimitReached(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Dialogs::Entry*> entry) {\n\tconst auto owner = &entry->owner();\n\tif (owner->pinnedCanPin(entry)) {\n\t\treturn false;\n\t}\n\t// Some old chat, that was converted, maybe is still pinned.\n\tif (const auto sublist = entry->asSublist()) {\n\t\tcontroller->show(Box(SublistsPinsLimitBox, &sublist->session()));\n\t\treturn true;\n\t} else if (const auto topic = entry->asTopic()) {\n\t\tcontroller->show(Box(ForumPinsLimitBox, topic->forum()));\n\t\treturn true;\n\t}\n\tconst auto history = entry->asHistory();\n\tAssert(history != nullptr);\n\tconst auto folder = history->folder();\n\tconst auto wasted = FindWastedPin(owner, folder);\n\tif (wasted) {\n\t\towner->setChatPinned(wasted, FilterId(), false);\n\t\towner->setChatPinned(history, FilterId(), true);\n\t\thistory->session().api().savePinnedOrder(folder);\n\t} else if (folder) {\n\t\tcontroller->show(Box(FolderPinsLimitBox, &history->session()));\n\t} else {\n\t\tcontroller->show(Box(PinsLimitBox, &history->session()));\n\t}\n\treturn true;\n}\n\nbool PinnedLimitReached(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<History*> history,\n\t\tFilterId filterId) {\n\tconst auto owner = &history->owner();\n\tif (owner->pinnedCanPin(filterId, history)) {\n\t\treturn false;\n\t}\n\tcontroller->show(\n\t\tBox(FilterPinsLimitBox, &history->session(), filterId));\n\treturn true;\n}\n\nvoid TogglePinnedThread(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Dialogs::Entry*> entry,\n\t\tFn<void()> onToggled) {\n\tif (!entry->folderKnown()) {\n\t\treturn;\n\t}\n\tconst auto owner = &entry->owner();\n\tconst auto isPinned = !entry->isPinnedDialog(FilterId());\n\tif (isPinned && PinnedLimitReached(controller, entry)) {\n\t\treturn;\n\t}\n\n\towner->setChatPinned(entry, FilterId(), isPinned);\n\tif (const auto history = entry->asHistory()) {\n\t\tconst auto flags = isPinned\n\t\t\t? MTPmessages_ToggleDialogPin::Flag::f_pinned\n\t\t\t: MTPmessages_ToggleDialogPin::Flag(0);\n\t\towner->session().api().request(MTPmessages_ToggleDialogPin(\n\t\t\tMTP_flags(flags),\n\t\t\tMTP_inputDialogPeer(history->peer->input())\n\t\t)).done([=] {\n\t\t\towner->notifyPinnedDialogsOrderUpdated();\n\t\t\tif (onToggled) {\n\t\t\t\tonToggled();\n\t\t\t}\n\t\t}).send();\n\t\tif (isPinned) {\n\t\t\tcontroller->content()->dialogsToUp();\n\t\t}\n\t} else if (const auto topic = entry->asTopic()) {\n\t\tconst auto peer = topic->peer();\n\t\towner->session().api().request(MTPmessages_UpdatePinnedForumTopic(\n\t\t\tpeer->input(),\n\t\t\tMTP_int(topic->rootId()),\n\t\t\tMTP_bool(isPinned)\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\towner->session().api().applyUpdates(result);\n\t\t\tif (onToggled) {\n\t\t\t\tonToggled();\n\t\t\t}\n\t\t}).send();\n\t} else if (const auto sublist = entry->asSublist()) {\n\t\tconst auto flags = isPinned\n\t\t\t? MTPmessages_ToggleSavedDialogPin::Flag::f_pinned\n\t\t\t: MTPmessages_ToggleSavedDialogPin::Flag(0);\n\t\towner->session().api().request(MTPmessages_ToggleSavedDialogPin(\n\t\t\tMTP_flags(flags),\n\t\t\tMTP_inputDialogPeer(sublist->sublistPeer()->input())\n\t\t)).done([=] {\n\t\t\towner->notifyPinnedDialogsOrderUpdated();\n\t\t\tif (onToggled) {\n\t\t\t\tonToggled();\n\t\t\t}\n\t\t}).send();\n\t\t//if (isPinned) {\n\t\t//\tcontroller->content()->dialogsToUp();\n\t\t//}\n\t}\n}\n\nFiller::Filler(\n\tnot_null<SessionController*> controller,\n\tDialogs::EntryState request,\n\tconst PeerMenuCallback &addAction)\n: _controller(controller)\n, _request(request)\n, _thread(request.key.thread())\n, _topic(request.key.topic())\n, _peer(request.key.peer())\n, _folder(request.key.folder())\n, _sublist(request.key.sublist())\n, _addAction(addAction) {\n}\n\nvoid Filler::addHidePromotion() {\n\tconst auto history = _request.key.history();\n\tif (_topic\n\t\t|| !history\n\t\t|| !history->useTopPromotion()\n\t\t|| history->topPromotionType().isEmpty()) {\n\t\treturn;\n\t}\n\t_addAction(tr::lng_context_hide_psa(tr::now), [=] {\n\t\thistory->cacheTopPromotion(false, QString(), QString());\n\t\thistory->session().api().request(MTPhelp_HidePromoData(\n\t\t\thistory->peer->input()\n\t\t)).send();\n\t}, &st::menuIconRemove);\n}\n\nvoid Filler::addToggleTopicClosed() {\n\tif (!_topic || !_topic->canToggleClosed()) {\n\t\treturn;\n\t}\n\tconst auto closed = _topic->closed();\n\tconst auto weak = base::make_weak(_topic);\n\tconst auto text = closed\n\t\t? tr::lng_forum_topic_reopen(tr::now)\n\t\t: tr::lng_forum_topic_close(tr::now);\n\t_addAction(text, [=] {\n\t\tif (const auto topic = weak.get()) {\n\t\t\ttopic->setClosedAndSave(!closed);\n\t\t}\n\t}, closed ? &st::menuIconRestartBot : &st::menuIconBlock);\n}\n\nvoid Filler::addTogglePin() {\n\tif ((!_sublist && !_peer) || (_topic && !_topic->canTogglePinned())) {\n\t\treturn;\n\t} else if (_request.section == Section::SubsectionTabsMenu\n\t\t&& !_sublist\n\t\t&& !_topic) {\n\t\treturn;\n\t} else if (_sublist && !_peer->isSelf()) {\n\t\treturn;\n\t}\n\tconst auto controller = _controller;\n\tconst auto filterId = _request.filterId;\n\tconst auto entry = _thread ? (Dialogs::Entry*)_thread : _sublist;\n\tif (!entry || entry->fixedOnTopIndex()) {\n\t\treturn;\n\t}\n\tconst auto pinText = [=] {\n\t\treturn entry->isPinnedDialog(filterId)\n\t\t\t? tr::lng_context_unpin_from_top(tr::now)\n\t\t\t: tr::lng_context_pin_to_top(tr::now);\n\t};\n\tconst auto weak = base::make_weak(entry);\n\tconst auto pinToggle = [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tTogglePinnedThread(controller, strong, filterId, nullptr);\n\t\t}\n\t};\n\t_addAction(\n\t\tpinText(),\n\t\tpinToggle,\n\t\t(entry->isPinnedDialog(filterId)\n\t\t\t? &st::menuIconUnpin\n\t\t\t: &st::menuIconPin));\n}\n\nvoid Filler::addToggleMuteSubmenu(bool addSeparator) {\n\tif (!_thread\n\t\t|| _thread->peer()->isSelf()\n\t\t|| _thread->asSublist()\n\t\t|| (_thread->asHistory() && _thread->asHistory()->isForum())) {\n\t\treturn;\n\t}\n\tPeerMenuAddMuteSubmenuAction(_controller, _thread, _addAction);\n\tif (addSeparator) {\n\t\t_addAction(PeerMenuCallback::Args{ .isSeparator = true });\n\t}\n}\n\nvoid Filler::addSupportInfo() {\n\tif (!_peer->session().supportMode()) {\n\t\treturn;\n\t}\n\tconst auto user = _peer->asUser();\n\tif (!user) {\n\t\treturn;\n\t}\n\tconst auto controller = _controller;\n\t_addAction(\"Edit support info\", [=] {\n\t\tuser->session().supportHelper().editInfo(controller, user);\n\t}, &st::menuIconEdit);\n}\n\nvoid Filler::addInfo() {\n\tconst auto sublist = _thread ? _thread->asSublist() : nullptr;\n\tconst auto infoPeer = sublist ? sublist->sublistPeer().get() : _peer;\n\tif (infoPeer\n\t\t&& (infoPeer->isSelf()\n\t\t\t|| infoPeer->isRepliesChat()\n\t\t\t|| infoPeer->isVerifyCodes())) {\n\t\treturn;\n\t} else if (!_thread) {\n\t\treturn;\n\t} else if (_controller->adaptive().isThreeColumn()) {\n\t\tconst auto thread = _controller->activeChatCurrent().thread();\n\t\tif (thread && !thread->asSublist() && thread == _thread) {\n\t\t\tif (Core::App().settings().thirdSectionInfoEnabled()\n\t\t\t\t|| Core::App().settings().tabbedReplacedWithInfo()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\tconst auto controller = _controller;\n\tconst auto weak = base::make_weak(_thread);\n\tconst auto text = _thread->asTopic()\n\t\t? (_thread->peer()->isBot()\n\t\t\t? tr::lng_context_view_thread(tr::now)\n\t\t\t: tr::lng_context_view_topic(tr::now))\n\t\t: (infoPeer->isChat() || infoPeer->isMegagroup())\n\t\t? tr::lng_context_view_group(tr::now)\n\t\t: infoPeer->isUser()\n\t\t? tr::lng_context_view_profile(tr::now)\n\t\t: tr::lng_context_view_channel(tr::now);\n\t_addAction(text, [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tif (base::IsCtrlPressed()) {\n\t\t\t\tcontroller->uiShow()->showBox(\n\t\t\t\t\tPrepareShortInfoBox(infoPeer, controller));\n\t\t\t} else {\n\t\t\t\tcontroller->showPeerInfo(strong);\n\t\t\t}\n\t\t}\n\t}, infoPeer->isUser() ? &st::menuIconProfile : &st::menuIconInfo);\n}\n\nvoid Filler::addStoryArchive() {\n\tconst auto channel = _peer ? _peer->asChannel() : nullptr;\n\tif (!channel || !channel->canEditStories()) {\n\t\treturn;\n\t}\n\tconst auto controller = _controller;\n\tconst auto weak = base::make_weak(_thread);\n\t_addAction(tr::lng_stories_archive_button(tr::now), [=] {\n\t\tif ([[maybe_unused]] const auto strong = weak.get()) {\n\t\t\tcontroller->showSection(Info::Stories::Make(\n\t\t\t\tchannel,\n\t\t\t\tInfo::Stories::ArchiveId()));\n\t\t}\n\t}, &st::menuIconStoriesArchiveSection);\n}\n\nvoid Filler::addToggleFolder() {\n\tconst auto controller = _controller;\n\tconst auto history = _request.key.history();\n\tif (_topic\n\t\t|| !history\n\t\t|| !history->owner().chatsFilters().has()\n\t\t|| !history->inChatList()) {\n\t\treturn;\n\t} else if (_request.section == Section::SubsectionTabsMenu\n\t\t&& !_sublist\n\t\t&& !_topic) {\n\t\treturn;\n\t}\n\t_addAction(PeerMenuCallback::Args{\n\t\t.text = tr::lng_filters_menu_add(tr::now),\n\t\t.handler = nullptr,\n\t\t.icon = &st::menuIconAddToFolder,\n\t\t.fillSubmenu = [&](not_null<Ui::PopupMenu*> menu) {\n\t\t\tFillChooseFilterMenu(controller, menu, history);\n\t\t},\n\t\t.submenuSt = &st::foldersMenu,\n\t});\n}\n\nvoid Filler::addToggleUnreadMark() {\n\tconst auto peer = _peer;\n\tconst auto unread = IsUnreadThread(_thread);\n\tconst auto history = _request.key.history();\n\tif (!_thread || !_thread->canToggleUnread(unread)) {\n\t\treturn;\n\t}\n\tconst auto weak = base::make_weak(_thread);\n\tconst auto label = unread\n\t\t? tr::lng_context_mark_read(tr::now)\n\t\t: tr::lng_context_mark_unread(tr::now);\n\t_addAction(label, [=] {\n\t\tconst auto thread = weak.get();\n\t\tif (!thread) {\n\t\t\treturn;\n\t\t}\n\t\tif (unread) {\n\t\t\tMarkAsReadThread(thread);\n\t\t} else if (const auto sublist = thread->asSublist()) {\n\t\t\tpeer->owner().histories().changeSublistUnreadMark(sublist, true);\n\t\t} else if (history) {\n\t\t\tpeer->owner().histories().changeDialogUnreadMark(history, true);\n\t\t}\n\t}, (unread ? &st::menuIconMarkRead : &st::menuIconMarkUnread));\n}\n\nvoid Filler::addNewWindow(bool addSeparator) {\n\tconst auto controller = _controller;\n\tif (_folder) {\n\t\t_addAction(tr::lng_context_new_window(tr::now), [=] {\n\t\t\tUi::PreventDelayedActivation();\n\t\t\tcontroller->showInNewWindow(SeparateId(\n\t\t\t\tSeparateType::Archive,\n\t\t\t\t&controller->session()));\n\t\t}, &st::menuIconNewWindow);\n\t\tif (addSeparator) {\n\t\t\tAddSeparatorAndShiftUp(_addAction);\n\t\t}\n\t\treturn;\n\t} else if (const auto weak = base::make_weak(_sublist)) {\n\t\t_addAction(tr::lng_context_new_window(tr::now), [=] {\n\t\t\tUi::PreventDelayedActivation();\n\t\t\tif (const auto sublist = weak.get()) {\n\t\t\t\tcontroller->showInNewWindow(SeparateId(\n\t\t\t\t\tSeparateType::SavedSublist,\n\t\t\t\t\tsublist));\n\t\t\t}\n\t\t}, &st::menuIconNewWindow);\n\t\tif (addSeparator) {\n\t\t\tAddSeparatorAndShiftUp(_addAction);\n\t\t}\n\t\treturn;\n\t}\n\tconst auto history = _request.key.history();\n\tif (!_peer\n\t\t|| (history\n\t\t\t&& history->useTopPromotion()\n\t\t\t&& !history->topPromotionType().isEmpty())) {\n\t\treturn;\n\t}\n\tconst auto peer = _peer;\n\tconst auto thread = _topic\n\t\t? not_null<Data::Thread*>(_topic)\n\t\t: _peer->owner().history(_peer);\n\tconst auto weak = base::make_weak(thread);\n\t_addAction(tr::lng_context_new_window(tr::now), [=] {\n\t\tUi::PreventDelayedActivation();\n\t\tif (const auto strong = weak.get()) {\n\t\t\tconst auto forum = !strong->asTopic()\n\t\t\t\t&& peer->isForum()\n\t\t\t\t&& !peer->useSubsectionTabs();\n\t\t\tcontroller->showInNewWindow(SeparateId(\n\t\t\t\tforum ? SeparateType::Forum : SeparateType::Chat,\n\t\t\t\tstrong));\n\t\t}\n\t}, &st::menuIconNewWindow);\n\tif (addSeparator) {\n\t\tAddSeparatorAndShiftUp(_addAction);\n\t}\n}\n\nvoid Filler::addToggleArchive() {\n\tif (!_peer\n\t\t|| _topic\n\t\t|| _request.section == Section::SubsectionTabsMenu) {\n\t\treturn;\n\t}\n\tconst auto peer = _peer;\n\tconst auto history = _request.key.history();\n\tif (!CanArchive(history, peer)) {\n\t\treturn;\n\t}\n\tconst auto isArchived = [=] {\n\t\treturn IsArchived(history);\n\t};\n\tconst auto label = [=] {\n\t\treturn isArchived()\n\t\t\t? tr::lng_archived_remove(tr::now)\n\t\t\t: tr::lng_archived_add(tr::now);\n\t};\n\tconst auto toggle = [=, show = _controller->uiShow()] {\n\t\tToggleHistoryArchived(show, history, !isArchived());\n\t};\n\tconst auto archiveAction = _addAction(\n\t\tlabel(),\n\t\ttoggle,\n\t\tisArchived() ? &st::menuIconUnarchive : &st::menuIconArchive);\n\n\tauto actionText = history->session().changes().historyUpdates(\n\t\thistory,\n\t\tData::HistoryUpdate::Flag::Folder\n\t) | rpl::map(label);\n\tSetActionText(archiveAction, std::move(actionText));\n}\n\nvoid Filler::addClearHistory() {\n\tif (_topic || _peer->isMonoforum()) {\n\t\treturn;\n\t}\n\tconst auto channel = _peer->asChannel();\n\tconst auto isGroup = _peer->isChat() || _peer->isMegagroup();\n\tif (channel) {\n\t\tif (!channel->amIn()) {\n\t\t\treturn;\n\t\t} else if (!channel->canDeleteMessages()\n\t\t\t&& (!isGroup || channel->isPublic() || channel->isForum())) {\n\t\t\treturn;\n\t\t}\n\t}\n\t_addAction(\n\t\ttr::lng_profile_clear_history(tr::now),\n\t\tClearHistoryHandler(_controller, _peer),\n\t\t&st::menuIconClear);\n}\n\nvoid Filler::addDeleteChat() {\n\tif (_topic || (!_sublist && _peer->isChannel())) {\n\t\treturn;\n\t}\n\t_addAction({\n\t\t.text = ((_peer->isUser() || _sublist)\n\t\t\t? tr::lng_profile_delete_conversation(tr::now)\n\t\t\t: tr::lng_profile_clear_and_exit(tr::now)),\n\t\t.handler = (_sublist\n\t\t\t? DeleteSublistHandler(_controller, _sublist)\n\t\t\t: DeleteAndLeaveHandler(_controller, _peer)),\n\t\t.icon = &st::menuIconDeleteAttention,\n\t\t.isAttention = true,\n\t});\n}\n\nvoid Filler::addLeaveChat() {\n\tconst auto channel = _peer->asChannel();\n\tif (_topic || _sublist || !channel || !channel->amIn()) {\n\t\treturn;\n\t}\n\t_addAction({\n\t\t.text = (_peer->isMegagroup()\n\t\t\t? tr::lng_profile_leave_group(tr::now)\n\t\t\t: tr::lng_profile_leave_channel(tr::now)),\n\t\t.handler = DeleteAndLeaveHandler(_controller, _peer),\n\t\t.icon = &st::menuIconLeaveAttention,\n\t\t.isAttention = true,\n\t});\n}\n\nvoid Filler::addJoinChat() {\n\tconst auto channel = _peer->asChannel();\n\tif (_topic || !channel || channel->amIn()) {\n\t\treturn;\n\t}\n\tconst auto label = _peer->isMegagroup()\n\t\t? tr::lng_profile_join_group(tr::now)\n\t\t: tr::lng_profile_join_channel(tr::now);\n\t_addAction(label, [=] {\n\t\tchannel->session().api().joinChannel(channel);\n\t}, &st::menuIconAddToFolder);\n}\n\nvoid Filler::addBlockUser() {\n\tconst auto user = _peer->asUser();\n\tif (!user\n\t\t|| user->isInaccessible()\n\t\t|| user->isSelf()\n\t\t|| user->isRepliesChat()\n\t\t|| user->isVerifyCodes()) {\n\t\treturn;\n\t}\n\tconst auto window = _controller;\n\tconst auto blockText = [](not_null<UserData*> user) {\n\t\treturn user->isBlocked()\n\t\t\t? ((user->isBot() && !user->isSupport())\n\t\t\t\t? tr::lng_profile_restart_bot(tr::now)\n\t\t\t\t: tr::lng_profile_unblock_user(tr::now))\n\t\t\t: ((user->isBot() && !user->isSupport())\n\t\t\t\t? tr::lng_profile_block_bot(tr::now)\n\t\t\t\t: tr::lng_profile_block_user(tr::now));\n\t};\n\tconst auto blockAction = _addAction(blockText(user), [=] {\n\t\tconst auto show = window->uiShow();\n\t\tif (show->showFrozenError()) {\n\t\t\treturn;\n\t\t} else if (user->isBlocked()) {\n\t\t\tPeerMenuUnblockUserWithBotRestart(show, user);\n\t\t} else if (user->isBot()) {\n\t\t\tuser->session().api().blockedPeers().block(user);\n\t\t} else {\n\t\t\twindow->show(Box(\n\t\t\t\tPeerMenuBlockUserBox,\n\t\t\t\t&window->window(),\n\t\t\t\tuser,\n\t\t\t\tv::null,\n\t\t\t\tv::null));\n\t\t}\n\t}, (!user->isBlocked()\n\t\t? &st::menuIconBlock\n\t\t: user->isBot()\n\t\t? &st::menuIconRestartBot\n\t\t: &st::menuIconUnblock));\n\n\tauto actionText = _peer->session().changes().peerUpdates(\n\t\t_peer,\n\t\tData::PeerUpdate::Flag::IsBlocked\n\t) | rpl::map([=] { return blockText(user); });\n\tSetActionText(blockAction, std::move(actionText));\n\n\tif (user->blockStatus() == UserData::BlockStatus::Unknown) {\n\t\tuser->session().api().requestFullPeer(user);\n\t}\n}\n\nvoid Filler::addViewDiscussion() {\n\tconst auto channel = _peer->asBroadcast();\n\tif (!channel) {\n\t\treturn;\n\t}\n\tconst auto chat = channel->discussionLink();\n\tif (!chat) {\n\t\treturn;\n\t}\n\tconst auto navigation = _controller;\n\t_addAction(tr::lng_profile_view_discussion(tr::now), [=] {\n\t\tif (channel->invitePeekExpires()) {\n\t\t\tnavigation->showToast(tr::lng_channel_invite_private(tr::now));\n\t\t\treturn;\n\t\t}\n\t\tnavigation->showPeerHistory(\n\t\t\tchat,\n\t\t\tWindow::SectionShow::Way::Forward);\n\t}, &st::menuIconDiscussion);\n}\n\nvoid Filler::addDirectMessages() {\n\tconst auto channel = _peer->asBroadcast();\n\tif (!channel) {\n\t\treturn;\n\t}\n\tconst auto monoforum = channel->broadcastMonoforum();\n\tif (!monoforum || !monoforum->amMonoforumAdmin()) {\n\t\treturn;\n\t}\n\tconst auto navigation = _controller;\n\t_addAction(tr::lng_profile_direct_messages(tr::now), [=] {\n\t\tnavigation->showPeerHistory(\n\t\t\tmonoforum,\n\t\t\tWindow::SectionShow::Way::Forward);\n\t}, &st::menuIconChatDiscuss);\n}\n\nvoid Filler::addExportChat() {\n\tif (!_peer->canExportChatHistory()) {\n\t\treturn;\n\t}\n\tconst auto peer = _peer;\n\tconst auto navigation = _controller;\n\tif (const auto topic = _thread->asTopic()) {\n\t\tconst auto topicRootId = topic->rootId();\n\t\t_addAction(\n\t\t\ttr::lng_profile_export_topic(tr::now),\n\t\t\t[=] { PeerMenuExportTopic(navigation, peer, topicRootId); },\n\t\t\t&st::menuIconExport);\n\t\treturn;\n\t}\n\t_addAction(\n\t\ttr::lng_profile_export_chat(tr::now),\n\t\t[=] { PeerMenuExportChat(navigation, peer); },\n\t\t&st::menuIconExport);\n}\n\nvoid Filler::addTranslate() {\n\tif (_peer->translationFlag() != PeerData::TranslationFlag::Disabled\n\t\t|| !_peer->session().premium()\n\t\t|| !Core::App().settings().translateChatEnabled()) {\n\t\treturn;\n\t}\n\tconst auto history = _peer->owner().historyLoaded(_peer);\n\tif (!history\n\t\t|| !history->translateOfferedFrom()\n\t\t|| history->translatedTo()) {\n\t\treturn;\n\t}\n\t_addAction(tr::lng_context_translate(tr::now), [=] {\n\t\thistory->peer->saveTranslationDisabled(false);\n\t}, &st::menuIconTranslate);\n}\n\nvoid Filler::addReport() {\n\tconst auto chat = _peer->asChat();\n\tconst auto channel = _peer->asChannel();\n\tif (_topic\n\t\t|| ((!chat || chat->amCreator())\n\t\t\t&& (!channel || channel->amCreator()))) {\n\t\treturn;\n\t}\n\tconst auto peer = _peer;\n\tconst auto navigation = _controller;\n\t_addAction(tr::lng_profile_report(tr::now), [=] {\n\t\tShowReportMessageBox(navigation->uiShow(), peer, {}, {});\n\t}, &st::menuIconReport);\n}\n\nvoid Filler::addNewContact() {\n\tconst auto user = _peer->asUser();\n\tif (!user\n\t\t|| user->isContact()\n\t\t|| user->isSelf()\n\t\t|| user->isInaccessible()\n\t\t|| user->isBot()) {\n\t\treturn;\n\t}\n\tconst auto controller = _controller;\n\tconst auto edit = [=] {\n\t\tif (controller->showFrozenError()) {\n\t\t\treturn;\n\t\t}\n\t\tcontroller->show(Box(EditContactBox, controller, user));\n\t};\n\t_addAction(\n\t\ttr::lng_info_add_as_contact(tr::now),\n\t\tedit,\n\t\t&st::menuIconInvite);\n}\n\nvoid Filler::addShareContact() {\n\tconst auto user = _peer->asUser();\n\tif (!user || !user->canShareThisContact()) {\n\t\treturn;\n\t}\n\tconst auto controller = _controller;\n\t_addAction(\n\t\ttr::lng_info_share_contact(tr::now),\n\t\t[=] { PeerMenuShareContactBox(controller, user); },\n\t\t&st::menuIconShare);\n}\n\nvoid Filler::addEditContact() {\n\tconst auto user = _peer->asUser();\n\tif (!user || !user->isContact() || user->isSelf()) {\n\t\treturn;\n\t}\n\tconst auto controller = _controller;\n\tconst auto edit = [=] {\n\t\tif (controller->showFrozenError()) {\n\t\t\treturn;\n\t\t}\n\t\tcontroller->show(Box(EditContactBox, controller, user));\n\t};\n\t_addAction(\n\t\ttr::lng_info_edit_contact(tr::now),\n\t\tedit,\n\t\t&st::menuIconEdit);\n}\n\nvoid Filler::addBotToGroup() {\n\tconst auto user = _peer->asUser();\n\tif (!user) {\n\t\treturn;\n\t}\n\t[[maybe_unused]] const auto lifetime = Info::Profile::InviteToChatButton(\n\t\tuser\n\t) | rpl::take(1) | rpl::on_next([=](QString label) {\n\t\tif (!label.isEmpty()) {\n\t\t\tconst auto controller = _controller;\n\t\t\t_addAction(\n\t\t\t\tlabel,\n\t\t\t\t[=] { AddBotToGroupBoxController::Start(controller, user); },\n\t\t\t\t&st::menuIconInvite);\n\t\t}\n\t});\n}\n\nvoid Filler::addNewMembers() {\n\tconst auto chat = _peer->asChat();\n\tconst auto channel = _peer->asChannel();\n\tif ((!chat || !chat->canAddMembers())\n\t\t&& (!channel || !channel->canAddMembers())) {\n\t\treturn;\n\t}\n\tconst auto navigation = _controller;\n\tconst auto callback = chat\n\t\t? Fn<void()>([=] { AddChatMembers(navigation, chat); })\n\t\t: [=] { PeerMenuAddChannelMembers(navigation, channel); };\n\t_addAction(\n\t\t((chat || channel->isMegagroup())\n\t\t\t? tr::lng_channel_add_members(tr::now)\n\t\t\t: tr::lng_channel_add_users(tr::now)),\n\t\tcallback,\n\t\t&st::menuIconInvite);\n}\n\nvoid Filler::addDeleteContact() {\n\tconst auto user = _peer->asUser();\n\tif (!user || !user->isContact() || user->isSelf()) {\n\t\treturn;\n\t}\n\tconst auto controller = _controller;\n\t_addAction({\n\t\t.text = tr::lng_info_delete_contact(tr::now),\n\t\t.handler = [=] { PeerMenuDeleteContact(controller, user); },\n\t\t.icon = &st::menuIconDeleteAttention,\n\t\t.isAttention = true,\n\t});\n}\n\nvoid Filler::addDeleteTopic() {\n\tif (!_topic || !_topic->canDelete()) {\n\t\treturn;\n\t}\n\tconst auto controller = _controller;\n\tconst auto weak = base::make_weak(_topic);\n\tconst auto callback = [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tPeerMenuDeleteTopicWithConfirmation(controller, strong);\n\t\t}\n\t};\n\t_addAction({\n\t\t.text = tr::lng_forum_topic_delete(tr::now),\n\t\t.handler = callback,\n\t\t.icon = &st::menuIconDeleteAttention,\n\t\t.isAttention = true,\n\t});\n}\n\nvoid Filler::addTopicLink() {\n\tif (!_topic || _topic->creating()) {\n\t\treturn;\n\t}\n\tconst auto channel = _topic->channel();\n\tif (!channel) {\n\t\treturn;\n\t}\n\tconst auto controller = _controller;\n\tconst auto weak = base::make_weak(_topic);\n\t_addAction(tr::lng_context_copy_topic_link(tr::now), [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tconst auto link = Info::Profile::TopicLink(strong, true);\n\t\t\tQGuiApplication::clipboard()->setText(link);\n\t\t\tcontroller->showToast(channel->hasUsername()\n\t\t\t\t? tr::lng_channel_public_link_copied(tr::now)\n\t\t\t\t: tr::lng_context_about_private_link(tr::now));\n\t\t}\n\t}, &st::menuIconCopy);\n}\n\nvoid Filler::addManageTopic() {\n\tif (!_topic || !_topic->canEdit()) {\n\t\treturn;\n\t}\n\tconst auto history = _topic->history();\n\tconst auto rootId = _topic->rootId();\n\tconst auto navigation = _controller;\n\tconst auto text = _topic->forum()->peer()->isBot()\n\t\t? tr::lng_bot_thread_edit(tr::now)\n\t\t: tr::lng_forum_topic_edit(tr::now);\n\t_addAction(text, [=] {\n\t\tnavigation->show(\n\t\t\tBox(EditForumTopicBox, navigation, history, rootId));\n\t}, &st::menuIconEdit);\n}\n\nvoid Filler::addGoToFirstMessage() {\n\t_addAction(\n\t\tQString(\"Go to the first message\"),\n\t\tGoToFirstMessageHandler(_controller, _peer),\n\t\t&st::menuIconShowInChat);\n}\n\nvoid Filler::addGoToScheduled() {\n\tif (!Data::CanSendAnything(_peer)) {\n\t\treturn;\n\t}\n\tconst auto history = _peer->owner().historyLoaded(_peer);\n\t_addAction(tr::lng_scheduled_messages({}), [=, controller = _controller] {\n\t\tcontroller->showSection(\n\t\t\tstd::make_shared<HistoryView::ScheduledMemento>(history));\n\t}, &st::menuIconGroupLog);\n}\n\nvoid Filler::addManageChat() {\n\tif (!EditPeerInfoBox::Available(_peer)) {\n\t\treturn;\n\t}\n\tconst auto peer = _peer;\n\tconst auto navigation = _controller;\n\tconst auto text = peer->isUser()\n\t\t? tr::lng_manage_bot_title(tr::now)\n\t\t: (peer->isChat() || peer->isMegagroup())\n\t\t? tr::lng_manage_group_title(tr::now)\n\t\t: tr::lng_manage_channel_title(tr::now);\n\t_addAction(text, [=] {\n\t\tnavigation->showEditPeerBox(peer);\n\t}, &st::menuIconManage);\n}\n\nvoid Filler::addBoostChat() {\n\tif (const auto channel = _peer->asChannel()) {\n\t\tif (channel->isMonoforum()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto text = channel->isMegagroup()\n\t\t\t? tr::lng_boost_group_button(tr::now)\n\t\t\t: tr::lng_boost_channel_button(tr::now);\n\t\tconst auto weak = base::make_weak(_controller);\n\t\t_addAction(text, [=] {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->resolveBoostState(channel);\n\t\t\t}\n\t\t}, &st::menuIconBoosts);\n\t}\n}\n\nvoid Filler::addViewStatistics() {\n\tif (const auto channel = _peer->asChannel()) {\n\t\tif (channel->isMonoforum()) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto controller = _controller;\n\t\tconst auto weak = base::make_weak(_thread);\n\t\tconst auto peer = _peer;\n\t\tusing Flag = ChannelDataFlag;\n\t\tconst auto canGetStats = (channel->flags() & Flag::CanGetStatistics);\n\t\tconst auto canViewEarn = (channel->flags() & Flag::CanViewRevenue);\n\t\tconst auto canViewCreditsEarn\n\t\t\t= (channel->flags() & Flag::CanViewCreditsRevenue);\n\t\tif (canGetStats) {\n\t\t\t_addAction(tr::lng_stats_title(tr::now), [=] {\n\t\t\t\tif ([[maybe_unused]] const auto strong = weak.get()) {\n\t\t\t\t\tusing namespace Info;\n\t\t\t\t\tcontroller->showSection(Statistics::Make(peer, {}, {}));\n\t\t\t\t}\n\t\t\t}, &st::menuIconStats);\n\t\t}\n\t\tif (canGetStats\n\t\t\t|| channel->amCreator()\n\t\t\t|| channel->canPostStories()) {\n\t\t\t_addAction(tr::lng_boosts_title(tr::now), [=] {\n\t\t\t\tif ([[maybe_unused]] const auto strong = weak.get()) {\n\t\t\t\t\tcontroller->showSection(Info::Boosts::Make(peer));\n\t\t\t\t}\n\t\t\t}, &st::menuIconBoosts);\n\t\t}\n\t\tif (canViewEarn || canViewCreditsEarn) {\n\t\t\t_addAction(tr::lng_channel_earn_title(tr::now), [=] {\n\t\t\t\tif ([[maybe_unused]] const auto strong = weak.get()) {\n\t\t\t\t\tcontroller->showSection(Info::ChannelEarn::Make(peer));\n\t\t\t\t}\n\t\t\t}, &st::menuIconEarn);\n\t\t}\n\t}\n}\n\nbool Filler::skipCreateActions() const {\n\tif (_peer && _peer->isRepliesChat()) {\n\t\treturn true;\n\t}\n\tconst auto isJoinChannel = [&] {\n\t\tif (_request.section != Section::Replies) {\n\t\t\tif (const auto c = _peer->asChannel(); c && !c->amIn()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}();\n\tconst auto isBotStart = [&] {\n\t\tconst auto user = _peer ? _peer->asUser() : nullptr;\n\t\tif (!user || !user->isBot()) {\n\t\t\treturn false;\n\t\t} else if (!user->botInfo->startToken.isEmpty()) {\n\t\t\treturn true;\n\t\t}\n\t\tconst auto history = _peer->owner().history(_peer);\n\t\tif (history && history->isEmpty() && !history->lastMessage()) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}();\n\tconst auto isBlocked = [&] {\n\t\treturn _peer && _peer->isUser() && _peer->asUser()->isBlocked();\n\t}();\n\treturn isBlocked || isJoinChannel || isBotStart;\n}\n\nvoid Filler::addCreatePoll() {\n\tif (skipCreateActions()) {\n\t\treturn;\n\t}\n\tconst auto can = _topic\n\t\t? Data::CanSend(_topic, ChatRestriction::SendPolls)\n\t\t: _peer->canCreatePolls();\n\tif (!can) {\n\t\treturn;\n\t}\n\tconst auto peer = _peer;\n\tconst auto controller = _controller;\n\tconst auto source = (_request.section == Section::Scheduled)\n\t\t? Api::SendType::Scheduled\n\t\t: Api::SendType::Normal;\n\tconst auto sendMenuType = (_request.section == Section::Scheduled)\n\t\t? SendMenu::Type::Disabled\n\t\t: (_request.section == Section::Replies\n\t\t\t|| _peer->starsPerMessageChecked())\n\t\t? SendMenu::Type::SilentOnly\n\t\t: SendMenu::Type::Scheduled;\n\tconst auto replyTo = _request.currentReplyTo;\n\tconst auto suggest = _request.currentSuggest;\n\tconst auto chosen = kDefaultPollCreateFlags;\n\tauto callback = [=] {\n\t\tPeerMenuCreatePoll(\n\t\t\tcontroller,\n\t\t\tpeer,\n\t\t\treplyTo,\n\t\t\tsuggest,\n\t\t\tchosen,\n\t\t\tPollData::Flags(),\n\t\t\tsource,\n\t\t\t{ sendMenuType });\n\t};\n\t_addAction(\n\t\ttr::lng_polls_create(tr::now),\n\t\tstd::move(callback),\n\t\t&st::menuIconCreatePoll);\n}\n\nvoid Filler::addCreateTodoList() {\n\tif (skipCreateActions()) {\n\t\treturn;\n\t}\n\tconst auto can = _topic\n\t\t? (_peer->session().premium()\n\t\t\t&& Data::CanSend(_topic, ChatRestriction::SendPolls))\n\t\t: _peer->canCreateTodoLists();\n\tif (!can) {\n\t\treturn;\n\t}\n\tconst auto peer = _peer;\n\tconst auto controller = _controller;\n\tconst auto source = (_request.section == Section::Scheduled)\n\t\t? Api::SendType::Scheduled\n\t\t: Api::SendType::Normal;\n\tconst auto sendMenuType = (_request.section == Section::Scheduled)\n\t\t? SendMenu::Type::Disabled\n\t\t: (_request.section == Section::Replies\n\t\t\t|| _peer->starsPerMessageChecked())\n\t\t? SendMenu::Type::SilentOnly\n\t\t: SendMenu::Type::Scheduled;\n\tconst auto replyTo = _request.currentReplyTo;\n\tconst auto suggest = _request.currentSuggest;\n\tauto callback = [=] {\n\t\tPeerMenuCreateTodoList(\n\t\t\tcontroller,\n\t\t\tpeer,\n\t\t\treplyTo,\n\t\t\tsuggest,\n\t\t\tsource,\n\t\t\t{ sendMenuType });\n\t};\n\t_addAction(\n\t\ttr::lng_todo_create(tr::now),\n\t\tstd::move(callback),\n\t\t&st::menuIconCreateTodoList);\n}\n\nvoid Filler::addThemeEdit() {\n\tif (_peer->isVerifyCodes() || _peer->isRepliesChat()) {\n\t\treturn;\n\t}\n\tconst auto user = _peer->asUser();\n\tif (!user || user->isInaccessible()) {\n\t\treturn;\n\t}\n\tif (user->requiresPremiumToWrite() && !user->session().premium()) {\n\t\treturn;\n\t}\n\tconst auto controller = _controller;\n\t_addAction(\n\t\ttr::lng_chat_theme_wallpaper(tr::now),\n\t\t[=] { controller->toggleChooseChatTheme(user); },\n\t\t&st::menuIconChangeColors);\n}\n\nvoid ShowDisableSharingBox(\n\t\tnot_null<SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tFn<void(bool)> toggleNoForwards) {\n\tcontroller->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tbox->setStyle(st::showOrBox);\n\t\tbox->setWidth(st::boxWideWidth);\n\t\tbox->addTopButton(st::boxTitleClose, [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\n\t\tconst auto buttonPadding = QMargins(\n\t\t\tst::showOrBox.buttonPadding.left(),\n\t\t\t0,\n\t\t\tst::showOrBox.buttonPadding.right(),\n\t\t\t0);\n\n\t\tauto icon = Settings::CreateLottieIcon(\n\t\t\tbox,\n\t\t\t{\n\t\t\t\t.name = u\"stop\"_q,\n\t\t\t\t.sizeOverride = st::normalBoxLottieSize,\n\t\t\t},\n\t\t\t{});\n\t\tbox->verticalLayout()->add(\n\t\t\tstd::move(icon.widget),\n\t\t\tst::disableSharingIconPadding,\n\t\t\tstyle::al_top);\n\n\t\tUi::AddSkip(box->verticalLayout());\n\n\t\tbox->addRow(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_disable_sharing_title(),\n\t\t\t\tst::boostCenteredTitle),\n\t\t\tQMargins(0, 0, 0, st::showOrTitleIconMargin) + buttonPadding,\n\t\t\tstyle::al_top);\n\n\t\tconst auto features = std::vector<Ui::FeatureListEntry>{\n\t\t\t{\n\t\t\t\tst::menuIconShareOff,\n\t\t\t\ttr::lng_disable_sharing_no_forwarding(tr::now),\n\t\t\t\t{ tr::lng_disable_sharing_no_forwarding_about(tr::now) },\n\t\t\t},\n\t\t\t{\n\t\t\t\tst::menuIconDownloadOff,\n\t\t\t\ttr::lng_disable_sharing_no_saving(tr::now),\n\t\t\t\t{ tr::lng_disable_sharing_no_saving_about(tr::now) },\n\t\t\t},\n\t\t};\n\t\tfor (const auto &feature : features) {\n\t\t\tbox->addRow(\n\t\t\t\tUi::MakeFeatureListEntry(box, feature),\n\t\t\t\tst::boxRowPadding);\n\t\t}\n\n\t\tconst auto button = box->addButton(rpl::single(QString()), [=] {\n\t\t\tif (peer->session().premium()) {\n\t\t\t\ttoggleNoForwards(true);\n\t\t\t\tbox->closeBox();\n\t\t\t} else {\n\t\t\t\tShowPremiumPreviewBox(controller, PremiumFeature::NoForwards);\n\t\t\t}\n\t\t});\n\t\tbutton->setText(\n\t\t\tData::AmPremiumValue(&peer->session())\n\t\t\t| rpl::map([](bool premium) {\n\t\t\t\tif (premium) {\n\t\t\t\t\treturn TextWithEntities{\n\t\t\t\t\t\ttr::lng_disable_sharing_button(tr::now) };\n\t\t\t\t}\n\t\t\t\treturn Ui::Text::IconEmoji(\n\t\t\t\t\t&st::disableSharingButtonLock\n\t\t\t\t).append(\n\t\t\t\t\t' ' + tr::lng_disable_sharing_unlock(tr::now));\n\t\t\t}));\n\n\t\tbox->setShowFinishedCallback([animate = std::move(icon.animate)] {\n\t\t\tanimate(anim::repeat::once);\n\t\t});\n\t}));\n}\n\nvoid Filler::addToggleNoForwards() {\n\tconst auto user = _peer->asUser();\n\tif (!user\n\t\t|| user->isInaccessible()\n\t\t|| user->isBot()\n\t\t|| user->isServiceUser()\n\t\t|| user->isSelf()) {\n\t\treturn;\n\t}\n\tconst auto controller = _controller;\n\tconst auto peer = _peer;\n\tconst auto toggleNoForwards = [=](bool enabled) {\n\t\tusing Flag = MTPmessages_ToggleNoForwards::Flag;\n\t\tpeer->session().api().request(MTPmessages_ToggleNoForwards(\n\t\t\tMTP_flags(Flag()),\n\t\t\tpeer->input(),\n\t\t\tMTP_bool(enabled),\n\t\t\tMTPint()\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\tpeer->session().api().applyUpdates(result);\n\t\t\tif (enabled) {\n\t\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\t\tpeer->session().data().recordSharingDisabledTime(user);\n\t\t\t\t}\n\t\t\t}\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tif (error.type() != u\"CHAT_NOT_MODIFIED\"_q) {\n\t\t\t\tcontroller->showToast(error.type());\n\t\t\t}\n\t\t}).send();\n\t};\n\tconst auto disabledNow = !user->allowsForwarding();\n\t_addAction(disabledNow\n\t\t? tr::lng_enable_sharing(tr::now)\n\t\t: tr::lng_disable_sharing(tr::now), [=] {\n\t\tif (controller->showFrozenError()) {\n\t\t\treturn;\n\t\t} else if (disabledNow) {\n\t\t\tconst auto willBeRequest = true\n\t\t\t\t&& (user->flags() & UserDataFlag::NoForwardsPeerEnabled)\n\t\t\t\t&& !peer->session().data().sharingRecentlyDisabledByMe(user);\n\t\t\tif (willBeRequest) {\n\t\t\t\tcontroller->show(Ui::MakeConfirmBox({\n\t\t\t\t\t.text = tr::lng_enable_sharing_request_text(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_name,\n\t\t\t\t\t\ttr::marked(user->shortName()),\n\t\t\t\t\t\ttr::rich),\n\t\t\t\t\t.confirmed = [=](Fn<void()> close) {\n\t\t\t\t\t\ttoggleNoForwards(false);\n\t\t\t\t\t\tcontroller->showPeerHistory(peer->id);\n\t\t\t\t\t\tclose();\n\t\t\t\t\t},\n\t\t\t\t\t.confirmText = tr::lng_enable_sharing_request_button(),\n\t\t\t\t\t.title = tr::lng_enable_sharing_request_title(),\n\t\t\t\t}));\n\t\t\t} else {\n\t\t\t\ttoggleNoForwards(false);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tauto &settings = peer->session().settings();\n\t\tif (!settings.shouldShowDisableSharingBox()) {\n#ifdef _DEBUG\n\t\t\tsettings.resetDisableSharingBoxShown();\n\t\t\tpeer->session().saveSettingsDelayed();\n#endif\n\t\t\tif (peer->session().premium()) {\n\t\t\t\ttoggleNoForwards(true);\n\t\t\t} else {\n\t\t\t\tShowPremiumPreviewBox(controller, PremiumFeature::NoForwards);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tsettings.incrementDisableSharingBoxShown();\n\t\tpeer->session().saveSettingsDelayed();\n\t\tShowDisableSharingBox(controller, peer, toggleNoForwards);\n\t}, disabledNow ? &st::menuIconShareOn : &st::menuIconShareOff);\n}\n\nvoid Filler::addTTLSubmenu(bool addSeparator) {\n\tif (_thread->asTopic() || !_peer || _peer->isMonoforum()) {\n\t\treturn; // #TODO later forum\n\t}\n\tconst auto validator = TTLMenu::TTLValidator(\n\t\t_controller->uiShow(),\n\t\t_peer);\n\tif (!validator.can()) {\n\t\treturn;\n\t}\n\tconst auto text = tr::lng_manage_messages_ttl_menu(tr::now)\n\t\t+ (_peer->messagesTTL()\n\t\t\t? ('\\t' + Ui::FormatTTLTiny(_peer->messagesTTL()))\n\t\t\t: QString());\n\t_addAction(text, [=] { validator.showBox(); }, validator.icon());\n\tif (addSeparator) {\n\t\t_addAction(PeerMenuCallback::Args{ .isSeparator = true });\n\t}\n}\n\nvoid Filler::addSendGift() {\n\tconst auto user = _peer->asUser();\n\tconst auto channel = _peer->asBroadcast();\n\tif (!user && !channel) {\n\t\treturn;\n\t} else if (user\n\t\t&& (user->isInaccessible()\n\t\t\t|| user->isSelf()\n\t\t\t|| user->isBot()\n\t\t\t|| user->isServiceUser()\n\t\t\t|| user->isNotificationsUser()\n\t\t\t|| user->isRepliesChat()\n\t\t\t|| user->isVerifyCodes()\n\t\t\t|| !user->session().premiumCanBuy())) {\n\t\treturn;\n\t} else if (channel\n\t\t&& (channel->isForbidden() || !channel->stargiftsAvailable())) {\n\t\treturn;\n\t}\n\n\tconst auto peer = _peer;\n\tconst auto navigation = _controller;\n\t_addAction(tr::lng_profile_gift_premium(tr::now), [=] {\n\t\tUi::ShowStarGiftBox(navigation, peer);\n\t}, &st::menuIconGiftPremium);\n}\n\nvoid Filler::fill() {\n\tif (_folder) {\n\t\tfillArchiveActions();\n\t} else if (_sublist && _peer->isSelf()) {\n\t\tfillSavedSublistActions();\n\t} else switch (_request.section) {\n\tcase Section::ChatsList: fillChatsListActions(); break;\n\tcase Section::History: fillHistoryActions(); break;\n\tcase Section::Profile: fillProfileActions(); break;\n\tcase Section::Replies: fillRepliesActions(); break;\n\tcase Section::Scheduled: fillScheduledActions(); break;\n\tcase Section::ContextMenu:\n\tcase Section::SubsectionTabsMenu: fillContextMenuActions(); break;\n\tcase Section::SavedSublist: fillMonoforumPeerActions(); break;\n\tdefault: Unexpected(\"_request.section in Filler::fill.\");\n\t}\n}\n\nvoid Filler::addCreateTopic() {\n\tif (!_peer || !_peer->canCreateTopics() || _peer->isBot()) {\n\t\treturn;\n\t}\n\tconst auto peer = _peer;\n\tconst auto controller = _controller;\n\t_addAction(tr::lng_forum_create_topic(tr::now), [=] {\n\t\tif (const auto forum = peer->forum()) {\n\t\t\tcontroller->show(Box(\n\t\t\t\tNewForumTopicBox,\n\t\t\t\tcontroller,\n\t\t\t\tforum->history()));\n\t\t}\n\t}, &st::menuIconDiscussion);\n\t_addAction(PeerMenuCallback::Args{ .isSeparator = true });\n}\n\nvoid Filler::addViewAsMessages() {\n\tif (!_peer || !_peer->isForum()) {\n\t\treturn;\n\t}\n\tconst auto peer = _peer;\n\tconst auto controller = _controller;\n\tconst auto parentHideRequests = std::make_shared<rpl::event_stream<>>();\n\tconst auto filterOutChatPreview = [=] {\n\t\tif (base::IsAltPressed()) {\n\t\t\tconst auto callback = [=](bool shown) {\n\t\t\t\tif (!shown) {\n\t\t\t\t\tparentHideRequests->fire({});\n\t\t\t\t}\n\t\t\t};\n\t\t\tcontroller->showChatPreview({\n\t\t\t\tpeer->owner().history(peer),\n\t\t\t\tFullMsgId(),\n\t\t\t}, callback, QApplication::activePopupWidget());\n\t\t\treturn true;\n\t\t} else if (base::IsCtrlPressed()) {\n\t\t\tUi::PreventDelayedActivation();\n\t\t\tcontroller->showInNewWindow(SeparateId(\n\t\t\t\tSeparateType::Chat,\n\t\t\t\tpeer->owner().history(peer)));\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\tconst auto open = [=] {\n\t\tif (const auto forum = peer->forum()) {\n\t\t\tpeer->owner().saveViewAsMessages(forum, true);\n\t\t}\n\t\tcontroller->showPeerHistory(peer->id);\n\t};\n\tauto to_instant = rpl::map_to(anim::type::instant);\n\tauto make = [=](not_null<Ui::PopupMenu*> popupMenu) {\n\t\tauto owned = base::make_unique_q<Ui::Menu::Action>(\n\t\t\tpopupMenu->menu(),\n\t\t\tpopupMenu->menu()->st(),\n\t\t\tUi::Menu::CreateAction(\n\t\t\t\tpopupMenu->menu(),\n\t\t\t\ttr::lng_forum_view_as_messages(tr::now),\n\t\t\t\t[=, weak = base::make_weak(popupMenu)] {\n\t\t\t\t\tif (filterOutChatPreview()) {\n\t\t\t\t\t} else {\n\t\t\t\t\t\topen();\n\t\t\t\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\t\t\t\tstrong->hideMenu(false);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t&st::menuIconAsMessages,\n\t\t\t&st::menuIconAsMessages);\n\t\towned->setPreventClose(true);\n\t\treturn owned;\n\t};\n\t_addAction({\n\t\t.make = std::move(make),\n\t\t.hideRequests = parentHideRequests->events() | to_instant,\n\t});\n}\n\nvoid Filler::addViewAsTopics() {\n\tif (!_peer\n\t\t|| !_peer->isForum()\n\t\t|| !_peer->isChannel()\n\t\t|| (_peer->asChannel()->flags() & ChannelDataFlag::ForumTabs)\n\t\t|| !_controller->adaptive().isOneColumn()) {\n\t\treturn;\n\t}\n\tconst auto peer = _peer;\n\tconst auto controller = _controller;\n\t_addAction(tr::lng_forum_view_as_topics(tr::now), [=] {\n\t\tif (const auto forum = peer->forum()) {\n\t\t\tpeer->owner().saveViewAsMessages(forum, false);\n\t\t\tcontroller->showForum(forum);\n\t\t}\n\t}, &st::menuIconAsTopics);\n}\n\nvoid Filler::addSearchTopics() {\n\tconst auto forum = _peer ? _peer->forum() : nullptr;\n\tif (!forum) {\n\t\treturn;\n\t}\n\tconst auto history = forum->history();\n\tconst auto controller = _controller;\n\t_addAction(tr::lng_dlg_filter(tr::now), [=] {\n\t\tcontroller->searchInChat(history);\n\t}, &st::menuIconSearch);\n}\n\nvoid Filler::fillChatsListActions() {\n\tif (!_peer || !_peer->isForum()) {\n\t\treturn;\n\t}\n\taddCreateTopic();\n\taddInfo();\n\taddViewAsMessages();\n\tconst auto &all = _peer->forum()->topicsList()->indexed()->all();\n\tif (all.size() > kTopicsSearchMinCount) {\n\t\taddSearchTopics();\n\t}\n\taddManageChat();\n\taddNewMembers();\n\taddBoostChat();\n\taddVideoChat();\n\t_addAction(PeerMenuCallback::Args{ .isSeparator = true });\n\taddReport();\n\tif (_peer->asChannel()->amIn()) {\n\t\taddLeaveChat();\n\t} else {\n\t\taddJoinChat();\n\t}\n}\n\nvoid Filler::addVideoChat() {\n\tauto test = Ui::PopupMenu(nullptr);\n\tFillVideoChatMenu(\n\t\t_controller,\n\t\t_request,\n\t\tUi::Menu::CreateAddActionCallback(&test));\n\tif (test.actions().size() < 2) {\n\t\tFillVideoChatMenu(_controller, _request, _addAction);\n\t\treturn;\n\t}\n\t_addAction(PeerMenuCallback::Args{\n\t\t.text = tr::lng_menu_start_group_call_options(tr::now),\n\t\t.handler = nullptr,\n\t\t.icon = &st::menuIconVideoChat,\n\t\t.fillSubmenu = [&](not_null<Ui::PopupMenu*> menu) {\n\t\t\tFillVideoChatMenu(\n\t\t\t\t_controller,\n\t\t\t\t_request,\n\t\t\t\tUi::Menu::CreateAddActionCallback(menu));\n\t\t},\n\t});\n}\n\nvoid Filler::fillContextMenuActions() {\n\taddNewWindow();\n\taddHidePromotion();\n\taddToggleArchive();\n\taddTogglePin();\n\tif (ViewProfileInChatsListContextMenu.value()) {\n\t\taddInfo();\n\t}\n\taddToggleMuteSubmenu(false);\n\taddToggleUnreadMark();\n\taddToggleTopicClosed();\n\taddToggleFolder();\n\tif (const auto user = _peer->asUser()) {\n\t\tif (!user->isContact()) {\n\t\t\taddBlockUser();\n\t\t}\n\t}\n\taddClearHistory();\n\taddDeleteChat();\n\taddLeaveChat();\n\taddDeleteTopic();\n}\n\nvoid Filler::fillHistoryActions() {\n\taddToggleMuteSubmenu(true);\n\taddCreateTopic();\n\taddInfo();\n\taddViewAsTopics();\n\taddManageChat();\n\taddStoryArchive();\n\taddSupportInfo();\n\taddBoostChat();\n\taddCreatePoll();\n\taddCreateTodoList();\n\taddThemeEdit();\n\taddToggleNoForwards();\n\taddViewDiscussion();\n\taddDirectMessages();\n\taddExportChat();\n\taddTranslate();\n\taddReport();\n\taddClearHistory();\n\taddDeleteChat();\n\taddLeaveChat();\n\taddGoToFirstMessage();\n\taddGoToScheduled();\n}\n\nvoid Filler::fillProfileActions() {\n\taddTTLSubmenu(true);\n\taddSupportInfo();\n\taddNewContact();\n\taddShareContact();\n\taddEditContact();\n\taddBotToGroup();\n\taddNewMembers();\n\taddSendGift();\n\taddViewStatistics();\n\taddStoryArchive();\n\taddManageChat();\n\taddSetPersonalChannel();\n\taddTopicLink();\n\taddManageTopic();\n\taddToggleTopicClosed();\n\taddViewDiscussion();\n\taddDirectMessages();\n\taddExportChat();\n\taddToggleNoForwards();\n\taddToggleFolder();\n\taddBlockUser();\n\taddReport();\n\taddLeaveChat();\n\taddDeleteContact();\n\taddDeleteTopic();\n\taddGoToFirstMessage();\n\taddGoToScheduled();\n}\n\nvoid Filler::fillRepliesActions() {\n\tif (_topic) {\n\t\taddInfo();\n\t\taddManageTopic();\n\t}\n\taddBoostChat();\n\taddCreatePoll();\n\taddCreateTodoList();\n\taddToggleTopicClosed();\n\taddDeleteTopic();\n}\n\nvoid Filler::fillScheduledActions() {\n\taddCreatePoll();\n\taddCreateTodoList();\n}\n\nvoid Filler::fillArchiveActions() {\n\tExpects(_folder != nullptr);\n\n\tif (_folder->id() != Data::Folder::kId) {\n\t\treturn;\n\t}\n\taddNewWindow();\n\n\tconst auto controller = _controller;\n\tconst auto hidden = controller->session().settings().archiveCollapsed();\n\tconst auto inmenu = controller->session().settings().archiveInMainMenu();\n\tif (!inmenu) {\n\t\tconst auto text = hidden\n\t\t\t? tr::lng_context_archive_expand(tr::now)\n\t\t\t: tr::lng_context_archive_collapse(tr::now);\n\t\t_addAction(text, [=] {\n\t\t\tcontroller->session().settings().setArchiveCollapsed(!hidden);\n\t\t\tcontroller->session().saveSettingsDelayed();\n\t\t}, hidden ? &st::menuIconExpand : &st::menuIconCollapse);\n\t}\n\t{\n\t\tconst auto text = inmenu\n\t\t\t? tr::lng_context_archive_to_list(tr::now)\n\t\t\t: tr::lng_context_archive_to_menu(tr::now);\n\t\t_addAction(text, [=] {\n\t\t\tif (!inmenu) {\n\t\t\t\tcontroller->showToast({\n\t\t\t\t\t.text = {\n\t\t\t\t\t\ttr::lng_context_archive_to_menu_info(tr::now)\n\t\t\t\t\t},\n\t\t\t\t\t.st = &st::windowArchiveToast,\n\t\t\t\t\t.duration = kArchivedToastDuration,\n\t\t\t\t});\n\t\t\t}\n\t\t\tcontroller->session().settings().setArchiveInMainMenu(!inmenu);\n\t\t\tcontroller->session().saveSettingsDelayed();\n\t\t\tcontroller->window().hideSettingsAndLayer();\n\t\t}, inmenu ? &st::menuIconFromMainMenu : &st::menuIconToMainMenu);\n\t}\n\n\tMenuAddMarkAsReadChatListAction(\n\t\tcontroller,\n\t\t[folder = _folder] { return folder->chatsList(); },\n\t\t_addAction);\n\n\t_addAction({ .isSeparator = true });\n\n\tSettings::PreloadArchiveSettings(&controller->session());\n\tconst auto openSettings = [=] {\n\t\tcontroller->show(Box(Settings::ArchiveSettingsBox, controller));\n\t};\n\t_addAction(\n\t\ttr::lng_context_archive_settings(tr::now),\n\t\topenSettings,\n\t\t&st::menuIconManage);\n\t_addAction(tr::lng_context_archive_how_does_it_work(tr::now), [=] {\n\t\tconst auto unarchiveOnNewMessage = controller->session().api(\n\t\t\t).globalPrivacy().unarchiveOnNewMessageCurrent();\n\t\tcontroller->show(\n\t\t\tBox(\n\t\t\t\tArchiveHintBox,\n\t\t\t\tunarchiveOnNewMessage != Api::UnarchiveOnNewMessage::None,\n\t\t\t\topenSettings),\n\t\t\tUi::LayerOption::CloseOther);\n\t}, &st::menuIconFaq);\n}\n\nvoid Filler::fillSavedSublistActions() {\n\taddNewWindow(false);\n\taddTogglePin();\n}\n\nvoid Filler::fillMonoforumPeerActions() {\n\tExpects(_sublist != nullptr);\n\n\taddToggleFee();\n}\n\nvoid Filler::addToggleFee() {\n\tconst auto feeRemoved = _sublist->isFeeRemoved();\n\tconst auto text = feeRemoved\n\t\t? tr::lng_context_charge_fee(tr::now)\n\t\t: tr::lng_context_remove_fee(tr::now);\n\tconst auto navigation = _controller;\n\tconst auto parent = _sublist->parentChat();\n\tconst auto user = _sublist->sublistPeer()->asUser();\n\tif (!parent || !user) {\n\t\treturn;\n\t}\n\tconst auto paidAmount = std::make_shared<rpl::variable<int>>();\n\t_addAction(text, [=] {\n\t\tconst auto removeFee = !feeRemoved;\n\t\tPeerMenuConfirmToggleFee(\n\t\t\tnavigation,\n\t\t\tpaidAmount,\n\t\t\tparent,\n\t\t\tuser,\n\t\t\tremoveFee);\n\t}, feeRemoved ? &st::menuIconEarn : &st::menuIconCancelFee);\n\t_addAction({ .isSeparator = true });\n\t_addAction({ .make = [=](not_null<Ui::PopupMenu*> menuParent) {\n\t\tconst auto actionParent = menuParent->menu();\n\t\tauto helper = Ui::Text::CustomEmojiHelper();\n\t\tconst auto text = feeRemoved\n\t\t\t? tr::lng_context_fee_free(\n\t\t\t\ttr::now,\n\t\t\t\tlt_name,\n\t\t\t\tTextWithEntities{ user->shortName() },\n\t\t\t\ttr::marked)\n\t\t\t: tr::lng_context_fee_now(\n\t\t\t\ttr::now,\n\t\t\t\tlt_name,\n\t\t\t\tTextWithEntities{ user->shortName() },\n\t\t\t\tlt_amount,\n\t\t\t\thelper.paletteDependent(\n\t\t\t\t\tUi::Earn::IconCurrencyEmojiSmall()\n\t\t\t\t).append(Lang::FormatCountDecimal(\n\t\t\t\t\tuser->owner().commonStarsPerMessage(parent)\n\t\t\t\t)),\n\t\t\t\ttr::marked);\n\t\tconst auto action = Ui::CreateChild<QAction>(actionParent);\n\t\taction->setDisabled(true);\n\t\tauto result = base::make_unique_q<Ui::Menu::Action>(\n\t\t\tactionParent,\n\t\t\tst::windowFeeItem,\n\t\t\taction,\n\t\t\tnullptr,\n\t\t\tnullptr);\n\t\tresult->setMarkedText(\n\t\t\ttext,\n\t\t\tQString(),\n\t\t\tCore::TextContext({ .session = &user->session() }));\n\t\treturn result;\n\t} });\n}\n\nvoid Filler::addSetPersonalChannel() {\n\tconst auto channel = _peer->asChannel();\n\tif (!channel\n\t\t|| _peer->session().user()->personalChannelId()\n\t\t|| channel->isMegagroup()\n\t\t|| !channel->amCreator()\n\t\t|| !channel->isPublic()) {\n\t\treturn;\n\t}\n\t_addAction(tr::lng_edit_channel_personal_channel(tr::now), [=] {\n\t\tUrlClickHandler::Open(u\"internal:edit_personal_channel:\"_q\n\t\t\t+ QString::number(channel->id.value));\n\t}, &st::menuIconProfile);\n}\n\n} // namespace\n\nvoid PeerMenuExportChat(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer) {\n\tbase::call_delayed(st::defaultPopupMenu.showDuration, [=] {\n\t\tCore::App().exportManager().start(peer);\n\t});\n}\n\nvoid PeerMenuExportTopic(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId) {\n\tbase::call_delayed(st::defaultPopupMenu.showDuration, [=] {\n\t\tconst auto topic = peer->forumTopicFor(topicRootId);\n\t\tconst auto topicTitle = topic ? topic->title() : QString();\n\t\tCore::App().exportManager().startTopic(peer, topicRootId, topicTitle);\n\t});\n}\n\nvoid PeerMenuDeleteContact(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<UserData*> user) {\n\tif (controller->showFrozenError()) {\n\t\treturn;\n\t}\n\tconst auto text = tr::lng_sure_delete_contact(\n\t\ttr::now,\n\t\tlt_contact,\n\t\tuser->name());\n\tconst auto deleteSure = [=](Fn<void()> &&close) {\n\t\tclose();\n\t\tuser->session().api().request(MTPcontacts_DeleteContacts(\n\t\t\tMTP_vector<MTPInputUser>(1, user->inputUser())\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\tuser->session().api().applyUpdates(result);\n\t\t}).send();\n\t};\n\tauto box = Box([=](not_null<Ui::GenericBox*> box) {\n\t\tUi::AddSkip(box->verticalLayout());\n\t\tUi::IconWithTitle(\n\t\t\tbox->verticalLayout(),\n\t\t\tUi::CreateChild<Ui::UserpicButton>(\n\t\t\t\tbox,\n\t\t\t\tuser,\n\t\t\t\tst::mainMenuUserpic),\n\t\t\tUi::CreateChild<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttr::lng_info_delete_contact(tr::bold),\n\t\t\t\tbox->getDelegate()->style().title));\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = text,\n\t\t\t.confirmed = deleteSure,\n\t\t\t.confirmText = tr::lng_box_delete(),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t});\n\t});\n\tcontroller->show(std::move(box), Ui::LayerOption::CloseOther);\n}\n\nvoid PeerMenuDeleteTopicWithConfirmation(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<Data::ForumTopic*> topic) {\n\tconst auto weak = base::make_weak(topic);\n\tconst auto callback = [=](Fn<void()> &&close) {\n\t\tclose();\n\t\tif (const auto strong = weak.get()) {\n\t\t\tPeerMenuDeleteTopic(navigation, strong);\n\t\t}\n\t};\n\tconst auto controller = navigation->parentController();\n\tcontroller->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tUi::AddSkip(box->verticalLayout());\n\t\tUi::IconWithTitle(\n\t\t\tbox->verticalLayout(),\n\t\t\tUi::CreateChild<Info::Profile::TopicIconButton>(\n\t\t\t\tbox,\n\t\t\t\tcontroller,\n\t\t\t\ttopic),\n\t\t\tUi::CreateChild<Ui::FlatLabel>(\n\t\t\t\tbox,\n\t\t\t\ttopic->title(),\n\t\t\t\tbox->getDelegate()->style().title));\n\t\tUi::AddSkip(box->verticalLayout());\n\t\tUi::AddSkip(box->verticalLayout());\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = tr::lng_forum_topic_delete_sure(tr::now),\n\t\t\t.confirmed = callback,\n\t\t\t.confirmText = tr::lng_box_delete(),\n\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t.labelPadding = st::boxRowPadding,\n\t\t});\n\t}));\n}\n\nvoid PeerMenuDeleteTopic(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId rootId) {\n\tconst auto api = &peer->session().api();\n\tapi->request(MTPmessages_DeleteTopicHistory(\n\t\tpeer->input(),\n\t\tMTP_int(rootId)\n\t)).done([=](const MTPmessages_AffectedHistory &result) {\n\t\tconst auto offset = api->applyAffectedHistory(peer, result);\n\t\tif (offset > 0) {\n\t\t\tPeerMenuDeleteTopic(navigation, peer, rootId);\n\t\t} else if (const auto forum = peer->forum()) {\n\t\t\tforum->applyTopicDeleted(rootId);\n\t\t}\n\t}).send();\n}\n\nvoid PeerMenuDeleteTopic(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<Data::ForumTopic*> topic) {\n\tPeerMenuDeleteTopic(navigation, topic->peer(), topic->rootId());\n}\n\nvoid PeerMenuShareContactBox(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<UserData*> user) {\n\tif (navigation->showFrozenError()) {\n\t\treturn;\n\t}\n\t// There is no async to make weak from controller.\n\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tauto callback = [=](not_null<Data::Thread*> thread) {\n\t\tconst auto peer = thread->peer();\n\t\tif (!Data::CanSend(thread, ChatRestriction::SendOther)) {\n\t\t\tnavigation->parentController()->show(\n\t\t\t\tUi::MakeInformBox(tr::lng_forward_share_cant()));\n\t\t\treturn;\n\t\t} else if (peer->isSelf()) {\n\t\t\tauto action = Api::SendAction(thread);\n\t\t\taction.clearDraft = false;\n\t\t\tuser->session().api().shareContact(user, action);\n\t\t\tnavigation->showToast(tr::lng_share_done(tr::now));\n\t\t\tif (auto strong = *weak) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst auto title = thread->asTopic()\n\t\t\t? thread->asTopic()->title()\n\t\t\t: peer->name();\n\t\tauto recipient = peer->isUser()\n\t\t\t? title\n\t\t\t: ('\\xAB' + title + '\\xBB');\n\t\tstruct State {\n\t\t\tbase::weak_ptr<Data::Thread> weak;\n\t\t\tFn<void(Api::SendOptions)> share;\n\t\t\tSendPaymentHelper sendPayment;\n\t\t};\n\t\tconst auto state = std::make_shared<State>();\n\t\tstate->weak = thread;\n\t\tstate->share = [=](Api::SendOptions options) {\n\t\t\tconst auto strong = state->weak.get();\n\t\t\tif (!strong) {\n\t\t\t\tstate->share = nullptr;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tauto action = Api::SendAction(strong, options);\n\t\t\taction.clearDraft = false;\n\n\t\t\tconst auto withPaymentApproved = [=](int stars) {\n\t\t\t\tif (const auto onstack = state->share) {\n\t\t\t\t\tauto copy = options;\n\t\t\t\t\tcopy.starsApproved = stars;\n\t\t\t\t\tonstack(copy);\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto checked = state->sendPayment.check(\n\t\t\t\tnavigation,\n\t\t\t\tpeer,\n\t\t\t\taction.options,\n\t\t\t\t1,\n\t\t\t\twithPaymentApproved);\n\t\t\tif (!checked) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tnavigation->showThread(\n\t\t\t\tstrong,\n\t\t\t\tShowAtTheEndMsgId,\n\t\t\t\tWindow::SectionShow::Way::ClearStack);\n\t\t\tstrong->session().api().shareContact(user, action);\n\t\t\tstate->share = nullptr;\n\t\t};\n\n\t\tnavigation->parentController()->show(\n\t\t\tUi::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_forward_share_contact(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_recipient,\n\t\t\t\t\trecipient),\n\t\t\t\t.confirmed = [state](Fn<void()> &&close) {\n\t\t\t\t\tstate->share({});\n\t\t\t\t\tclose();\n\t\t\t\t},\n\t\t\t\t.confirmText = tr::lng_forward_send(),\n\t\t\t}));\n\t};\n\t*weak = navigation->parentController()->show(\n\t\tBox<PeerListBox>(\n\t\t\tstd::make_unique<ChooseRecipientBoxController>(\n\t\t\t\tChooseRecipientArgs{\n\t\t\t\t\t.session = &navigation->session(),\n\t\t\t\t\t.callback = std::move(callback),\n\t\t\t\t\t.moneyRestrictionError = WriteMoneyRestrictionError,\n\t\t\t\t}),\n\t\t\t[](not_null<PeerListBox*> box) {\n\t\t\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t});\n\t\t\t}));\n}\n\nvoid PeerMenuCreatePoll(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tFullReplyTo replyTo,\n\t\tSuggestOptions suggest,\n\t\tPollData::Flags chosen,\n\t\tPollData::Flags disabled,\n\t\tApi::SendType sendType,\n\t\tSendMenu::Details sendMenuDetails) {\n\tif (peer->isChannel() && !peer->isMegagroup()) {\n\t\tchosen &= ~PollData::Flag::PublicVotes;\n\t\tdisabled |= PollData::Flag::PublicVotes;\n\t\tchosen &= ~PollData::Flag::OpenAnswers;\n\t\tdisabled |= PollData::Flag::OpenAnswers;\n\t}\n\tauto starsRequired = peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::FullInfo\n\t\t| Data::PeerUpdate::Flag::StarsPerMessage\n\t) | rpl::map([=] {\n\t\treturn peer->starsPerMessageChecked();\n\t});\n\tauto box = Box<CreatePollBox>(\n\t\tcontroller,\n\t\tpeer,\n\t\tchosen,\n\t\tdisabled,\n\t\tstd::move(starsRequired),\n\t\tsendType,\n\t\tsendMenuDetails);\n\tstruct State {\n\t\tFn<void(const CreatePollBox::Result &)> create;\n\t\tSendPaymentHelper sendPayment;\n\t\tbool lock = false;\n\t};\n\tconst auto weak = base::make_weak(box);\n\tconst auto state = box->lifetime().make_state<State>();\n\tstate->create = [=](const CreatePollBox::Result &result) {\n\t\tauto action = Api::SendAction(\n\t\t\tpeer->owner().history(peer),\n\t\t\tresult.options);\n\t\taction.replyTo = replyTo;\n\t\taction.options.suggest = suggest;\n\n\t\tconst auto withPaymentApproved = crl::guard(weak, [=](int stars) {\n\t\t\tif (const auto onstack = state->create) {\n\t\t\t\tauto copy = result;\n\t\t\t\tcopy.options.starsApproved = stars;\n\t\t\t\tonstack(copy);\n\t\t\t}\n\t\t});\n\t\tconst auto checked = state->sendPayment.check(\n\t\t\tcontroller,\n\t\t\tpeer,\n\t\t\taction.options,\n\t\t\t1,\n\t\t\twithPaymentApproved);\n\t\tif (!checked || std::exchange(state->lock, true)) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto local = action.history->localDraft(\n\t\t\treplyTo.topicRootId,\n\t\t\treplyTo.monoforumPeerId);\n\t\tif (local) {\n\t\t\taction.clearDraft = local->textWithTags.text.isEmpty();\n\t\t} else {\n\t\t\taction.clearDraft = false;\n\t\t}\n\t\tconst auto api = &peer->session().api();\n\t\tapi->polls().create(\n\t\t\tresult.poll,\n\t\t\tresult.text,\n\t\t\taction,\n\t\t\tcrl::guard(weak, [=] {\n\t\t\t\tstate->create = nullptr;\n\t\t\t\tweak->closeBox();\n\t\t\t}),\n\t\t\tcrl::guard(weak, [=](bool fileReferenceExpired) {\n\t\t\t\tstate->lock = false;\n\t\t\t\tif (fileReferenceExpired) {\n\t\t\t\t\tweak->submitMediaExpired();\n\t\t\t\t} else {\n\t\t\t\t\tweak->submitFailed(tr::lng_attach_failed(tr::now));\n\t\t\t\t}\n\t\t\t}));\n\t};\n\tbox->submitRequests(\n\t) | rpl::on_next(state->create, box->lifetime());\n\tcontroller->show(std::move(box), Ui::LayerOption::CloseOther);\n}\n\nvoid PeerMenuTodoWantsPremium(TodoWantsPremium type) {\n\tconst auto window = Core::App().activeWindow();\n\tif (!window) {\n\t\treturn;\n\t}\n\tconst auto filter = [=](const auto &...) {\n\t\tif (const auto controller = window->sessionController()) {\n\t\t\tShowPremiumPreviewBox(controller, PremiumFeature::TodoLists);\n\t\t\twindow->activate();\n\t\t}\n\t\treturn false;\n\t};\n\tconst auto link = tr::link(\n\t\ttr::semibold(tr::lng_todo_premium_link(tr::now)));\n\tconst auto text = [&] {\n\t\tswitch (type) {\n\t\tcase TodoWantsPremium::Create: return tr::lng_todo_create_premium;\n\t\tcase TodoWantsPremium::Add: return tr::lng_todo_add_premium;\n\t\tcase TodoWantsPremium::Mark: return tr::lng_todo_mark_premium;\n\t\t}\n\t\tUnexpected(\"Type in PeerMenuTodoWantsPremium.\");\n\t}();\n\tconstexpr auto kToastDuration = crl::time(4000);\n\twindow->uiShow()->showToast(Ui::Toast::Config{\n\t\t.text = text(\n\t\t\ttr::now,\n\t\t\tlt_link,\n\t\t\tlink,\n\t\t\ttr::marked),\n\t\t.filter = filter,\n\t\t.duration = kToastDuration,\n\t});\n}\n\nvoid PeerMenuCreateTodoList(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tFullReplyTo replyTo,\n\t\tSuggestOptions suggest,\n\t\tApi::SendType sendType,\n\t\tSendMenu::Details sendMenuDetails) {\n\tif (!peer->session().premium()) {\n\t\tPeerMenuTodoWantsPremium(TodoWantsPremium::Create);\n\t\treturn;\n\t}\n\tauto starsRequired = peer->session().changes().peerFlagsValue(\n\t\tpeer,\n\t\tData::PeerUpdate::Flag::FullInfo\n\t\t| Data::PeerUpdate::Flag::StarsPerMessage\n\t) | rpl::map([=] {\n\t\treturn peer->starsPerMessageChecked();\n\t});\n\tauto box = Box<EditTodoListBox>(\n\t\tcontroller,\n\t\tstd::move(starsRequired),\n\t\tsendType,\n\t\tsendMenuDetails);\n\tstruct State {\n\t\tFn<void(const EditTodoListBox::Result &)> create;\n\t\tSendPaymentHelper sendPayment;\n\t\tbool lock = false;\n\t};\n\tconst auto weak = base::make_weak(box);\n\tconst auto state = box->lifetime().make_state<State>();\n\tstate->create = [=](const EditTodoListBox::Result &result) {\n\t\tconst auto withPaymentApproved = crl::guard(weak, [=](int stars) {\n\t\t\tif (const auto onstack = state->create) {\n\t\t\t\tauto copy = result;\n\t\t\t\tcopy.options.starsApproved = stars;\n\t\t\t\tonstack(copy);\n\t\t\t}\n\t\t});\n\t\tauto action = Api::SendAction(\n\t\t\tpeer->owner().history(peer),\n\t\t\tresult.options);\n\t\taction.replyTo = replyTo;\n\t\taction.options.suggest = suggest;\n\n\t\tconst auto checked = state->sendPayment.check(\n\t\t\tcontroller,\n\t\t\tpeer,\n\t\t\taction.options,\n\t\t\t1,\n\t\t\twithPaymentApproved);\n\t\tif (!checked || std::exchange(state->lock, true)) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst auto local = action.history->localDraft(\n\t\t\treplyTo.topicRootId,\n\t\t\treplyTo.monoforumPeerId);\n\t\tif (local) {\n\t\t\taction.clearDraft = local->textWithTags.text.isEmpty();\n\t\t} else {\n\t\t\taction.clearDraft = false;\n\t\t}\n\t\tconst auto api = &peer->session().api();\n\t\tapi->todoLists().create(result.todolist, action, crl::guard(weak, [=] {\n\t\t\tstate->create = nullptr;\n\t\t\tweak->closeBox();\n\t\t}), crl::guard(weak, [=](const QString &error) {\n\t\t\tstate->lock = false;\n\t\t\tweak->submitFailed(error);\n\t\t}));\n\t};\n\tbox->submitRequests(\n\t) | rpl::on_next(state->create, box->lifetime());\n\tcontroller->show(std::move(box), Ui::LayerOption::CloseOther);\n}\n\nvoid PeerMenuEditTodoList(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto media = item->media();\n\tconst auto todolist = media ? media->todolist() : nullptr;\n\tif (!todolist) {\n\t\treturn;\n\t} else if (!item->history()->session().premium()) {\n\t\tPeerMenuTodoWantsPremium(TodoWantsPremium::Add);\n\t\treturn;\n\t}\n\tauto box = Box<EditTodoListBox>(controller, item);\n\tconst auto weak = base::make_weak(box);\n\tbox->submitRequests(\n\t) | rpl::on_next([=](const EditTodoListBox::Result &result) {\n\t\tconst auto api = &item->history()->session().api();\n\t\tapi->todoLists().edit(\n\t\t\titem,\n\t\t\tresult.todolist,\n\t\t\tresult.options,\n\t\t\tcrl::guard(weak, [=] { weak->closeBox(); }),\n\t\t\tcrl::guard(weak, [=](const QString &error) {\n\t\t\t\tweak->submitFailed(error);\n\t\t\t}));\n\t}, box->lifetime());\n\tcontroller->show(std::move(box), Ui::LayerOption::CloseOther);\n}\n\nbool PeerMenuShowAddTodoListTasks(not_null<HistoryItem*> item) {\n\tconst auto media = item ? item->media() : nullptr;\n\tconst auto todolist = media ? media->todolist() : nullptr;\n\tconst auto appConfig = &item->history()->session().appConfig();\n\treturn item->isRegular()\n\t\t&& !item->Has<HistoryMessageForwarded>()\n\t\t&& todolist\n\t\t&& (todolist->items.size() < appConfig->todoListItemsLimit())\n\t\t&& (item->out()\n\t\t\t|| item->history()->peer->isSelf()\n\t\t\t|| todolist->othersCanAppend());\n}\n\nvoid PeerMenuAddTodoListTasks(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<HistoryItem*> item) {\n\tconst auto session = &item->history()->session();\n\tif (!session->premium()) {\n\t\tPeerMenuTodoWantsPremium(TodoWantsPremium::Add);\n\t\treturn;\n\t}\n\tconst auto media = item->media();\n\tconst auto todolist = media ? media->todolist() : nullptr;\n\tif (!todolist) {\n\t\treturn;\n\t}\n\tauto box = Box<AddTodoListTasksBox>(controller, item);\n\tconst auto raw = box.data();\n\tbox->submitRequests(\n\t) | rpl::on_next([=](const AddTodoListTasksBox::Result &result) {\n\t\tconst auto show = raw->uiShow();\n\t\traw->closeBox();\n\t\tsession->api().todoLists().add(\n\t\t\titem,\n\t\t\tresult.items,\n\t\t\t[] {},\n\t\t\t[=](const QString &error) { show->showToast(error); });\n\t}, box->lifetime());\n\tcontroller->show(std::move(box), Ui::LayerOption::CloseOther);\n}\n\nvoid PeerMenuBlockUserBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<Window::Controller*> window,\n\t\tnot_null<PeerData*> peer,\n\t\tstd::variant<v::null_t, bool> suggestReport,\n\t\tstd::variant<v::null_t, ClearChat, ClearReply> suggestClear) {\n\tconst auto settings = peer->barSettings().value_or(PeerBarSettings(0));\n\tconst auto reportNeeded = v::is_null(suggestReport)\n\t\t? ((settings & PeerBarSetting::ReportSpam) != 0)\n\t\t: v::get<bool>(suggestReport);\n\n\tconst auto user = peer->asUser();\n\tconst auto name = user ? user->shortName() : peer->name();\n\tif (user) {\n\t\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_blocked_list_confirm_text(\n\t\t\t\tlt_name,\n\t\t\t\trpl::single(tr::bold(name)),\n\t\t\t\ttr::marked),\n\t\t\tst::blockUserConfirmation));\n\n\t\tbox->addSkip(st::boxMediumSkip);\n\t}\n\tconst auto report = reportNeeded\n\t\t? box->addRow(object_ptr<Ui::Checkbox>(\n\t\t\tbox,\n\t\t\ttr::lng_report_spam(tr::now),\n\t\t\ttrue,\n\t\t\tst::defaultBoxCheckbox))\n\t\t: nullptr;\n\n\tif (report) {\n\t\tbox->addSkip(st::boxMediumSkip);\n\t}\n\n\tconst auto clear = v::is<ClearChat>(suggestClear)\n\t\t? box->addRow(object_ptr<Ui::Checkbox>(\n\t\t\tbox,\n\t\t\ttr::lng_blocked_list_confirm_clear(tr::now),\n\t\t\ttrue,\n\t\t\tst::defaultBoxCheckbox))\n\t\t: v::is<ClearReply>(suggestClear)\n\t\t? box->addRow(object_ptr<Ui::Checkbox>(\n\t\t\tbox,\n\t\t\ttr::lng_context_delete_msg(tr::now),\n\t\t\ttrue,\n\t\t\tst::defaultBoxCheckbox))\n\t\t: nullptr;\n\tif (clear) {\n\t\tbox->addSkip(st::boxMediumSkip);\n\t}\n\tconst auto allFromUser = v::is<ClearReply>(suggestClear)\n\t\t? box->addRow(object_ptr<Ui::Checkbox>(\n\t\t\tbox,\n\t\t\ttr::lng_delete_all_from_user(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\ttr::bold(peer->name()),\n\t\t\t\ttr::marked),\n\t\t\ttrue,\n\t\t\tst::defaultBoxCheckbox))\n\t\t: nullptr;\n\n\tif (allFromUser) {\n\t\tbox->addSkip(st::boxLittleSkip);\n\t}\n\n\tbox->setTitle(tr::lng_blocked_list_confirm_title(\n\t\tlt_name,\n\t\trpl::single(name)));\n\n\tbox->addButton(tr::lng_blocked_list_confirm_ok(), [=] {\n\t\tconst auto reportChecked = report && report->checked();\n\t\tconst auto clearChecked = clear && clear->checked();\n\t\tconst auto fromUserChecked = allFromUser && allFromUser->checked();\n\n\t\tbox->closeBox();\n\n\t\tif (const auto clearReply = std::get_if<ClearReply>(&suggestClear)) {\n\t\t\tusing Flag = MTPcontacts_BlockFromReplies::Flag;\n\t\t\tpeer->session().api().request(MTPcontacts_BlockFromReplies(\n\t\t\t\tMTP_flags((clearChecked ? Flag::f_delete_message : Flag(0))\n\t\t\t\t\t| (fromUserChecked ? Flag::f_delete_history : Flag(0))\n\t\t\t\t\t| (reportChecked ? Flag::f_report_spam : Flag(0))),\n\t\t\t\tMTP_int(clearReply->replyId.msg)\n\t\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t\tpeer->session().updates().applyUpdates(result);\n\t\t\t}).send();\n\t\t} else {\n\t\t\tpeer->session().api().blockedPeers().block(peer);\n\t\t\tif (reportChecked) {\n\t\t\t\tpeer->session().api().request(MTPmessages_ReportSpam(\n\t\t\t\t\tpeer->input()\n\t\t\t\t)).send();\n\t\t\t}\n\t\t\tif (clearChecked) {\n\t\t\t\tcrl::on_main(&peer->session(), [=] {\n\t\t\t\t\tpeer->session().api().deleteConversation(peer, false);\n\t\t\t\t});\n\t\t\t\twindow->sessionController()->showBackFromStack();\n\t\t\t}\n\t\t}\n\n\t\twindow->showToast(\n\t\t\ttr::lng_new_contact_block_done(tr::now, lt_user, name));\n\t}, st::attentionBoxButton);\n\n\tbox->addButton(tr::lng_cancel(), [=] {\n\t\tbox->closeBox();\n\t});\n}\n\nvoid PeerMenuUnblockUserWithBotRestart(\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tnot_null<UserData*> user) {\n\tuser->session().api().blockedPeers().unblock(user, [=](bool success) {\n\t\tif (success && user->isBot() && !user->isSupport()) {\n\t\t\tuser->session().api().sendBotStart(show, user);\n\t\t}\n\t});\n}\n\nvoid BlockSenderFromRepliesBox(\n\t\tnot_null<Ui::GenericBox*> box,\n\t\tnot_null<SessionController*> controller,\n\t\tFullMsgId id) {\n\tconst auto item = controller->session().data().message(id);\n\tAssert(item != nullptr);\n\n\tPeerMenuBlockUserBox(\n\t\tbox,\n\t\t&controller->window(),\n\t\titem->originalSender(),\n\t\ttrue,\n\t\tWindow::ClearReply{ id });\n}\n\nobject_ptr<Ui::BoxContent> PrepareChooseRecipientBox(\n\t\tnot_null<Main::Session*> session,\n\t\tFnMut<bool(not_null<Data::Thread*>)> &&chosen,\n\t\trpl::producer<QString> titleOverride,\n\t\tFnMut<void()> &&successCallback,\n\t\tInlineBots::PeerTypes typesRestriction,\n\t\tFn<void(\n\t\t\tstd::vector<not_null<Data::Thread*>>,\n\t\t\tApi::SendOptions)> sendMany) {\n\tconst auto weak = std::make_shared<base::weak_qptr<PeerListBox>>();\n\tconst auto selectable = (sendMany != nullptr);\n\tclass Controller final : public ChooseRecipientBoxController {\n\tpublic:\n\t\tusing Chosen = not_null<Data::Thread*>;\n\n\t\tController(\n\t\t\tnot_null<Main::Session*> session,\n\t\t\tFnMut<void(Chosen)> callback,\n\t\t\tFn<bool(Chosen)> filter,\n\t\t\tbool selectable)\n\t\t: ChooseRecipientBoxController({\n\t\t\t.session = session,\n\t\t\t.callback = std::move(callback),\n\t\t\t.filter = filter,\n\t\t\t.moneyRestrictionError = WriteMoneyRestrictionError,\n\t\t})\n\t\t, _selectable(selectable) {\n\t\t}\n\n\t\tusing PeerListController::setSearchNoResultsText;\n\n\t\tvoid rowClicked(not_null<PeerListRow*> row) override final {\n\t\t\tif (!_selectable) {\n\t\t\t\treturn ChooseRecipientBoxController::rowClicked(row);\n\t\t\t}\n\t\t\tconst auto count = delegate()->peerListSelectedRowsCount();\n\t\t\tconst auto forum = row->peer()->isForum();\n\t\t\tconst auto monoforum = row->peer()->isMonoforum();\n\t\t\tif (showLockedError(row) || (count && (forum || monoforum))) {\n\t\t\t\treturn;\n\t\t\t} else if (forum || monoforum) {\n\t\t\t\tChooseRecipientBoxController::rowClicked(row);\n\t\t\t} else {\n\t\t\t\tdelegate()->peerListSetRowChecked(row, !row->checked());\n\t\t\t\t_selectionChanges.fire({});\n\t\t\t}\n\t\t}\n\n\t\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\t\t\tQWidget *parent,\n\t\t\t\tnot_null<PeerListRow*> row) override final {\n\t\t\tif (!_selectable) {\n\t\t\t\treturn ChooseRecipientBoxController::rowContextMenu(\n\t\t\t\t\tparent,\n\t\t\t\t\trow);\n\t\t\t}\n\t\t\tif (!row->checked()\n\t\t\t\t&& !row->peer()->isForum()\n\t\t\t\t&& !row->peer()->isMonoforum()) {\n\t\t\t\tauto menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\t\tparent,\n\t\t\t\t\tst::popupMenuWithIcons);\n\t\t\t\tmenu->addAction(tr::lng_bot_choose_chat(tr::now), [=] {\n\t\t\t\t\tdelegate()->peerListSetRowChecked(row, true);\n\t\t\t\t\t_selectionChanges.fire({});\n\t\t\t\t}, &st::menuIconSelect);\n\t\t\t\treturn menu;\n\t\t\t}\n\t\t\treturn nullptr;\n\t\t}\n\n\t\t[[nodiscard]] rpl::producer<> selectionChanges() const {\n\t\t\treturn _selectionChanges.events_starting_with({});\n\t\t}\n\t\t[[nodiscard]] bool hasSelected() const {\n\t\t\treturn delegate()->peerListSelectedRowsCount() > 0;\n\t\t}\n\n\t\t[[nodiscard]] rpl::producer<Chosen> singleChosen() const {\n\t\t\treturn _singleChosen.events();\n\t\t}\n\n\tprivate:\n\t\trpl::event_stream<Chosen> _singleChosen;\n\t\trpl::event_stream<> _selectionChanges;\n\t\tbool _selectable = false;\n\n\t};\n\n\tauto callback = [\n\t\tchosen = std::move(chosen),\n\t\tsuccess = std::move(successCallback),\n\t\tweak\n\t](not_null<Data::Thread*> thread) mutable {\n\t\tif (!chosen(thread)) {\n\t\t\treturn;\n\t\t} else if (const auto strong = *weak) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t\tif (success) {\n\t\t\tsuccess();\n\t\t}\n\t};\n\tauto filter = typesRestriction\n\t\t? [=](not_null<Data::Thread*> thread) -> bool {\n\t\t\tusing namespace InlineBots;\n\t\t\tconst auto peer = thread->peer();\n\t\t\tif (const auto user = peer->asUser()) {\n\t\t\t\tif (user->isBot()) {\n\t\t\t\t\treturn (typesRestriction & PeerType::Bot);\n\t\t\t\t} else {\n\t\t\t\t\treturn (typesRestriction & PeerType::User);\n\t\t\t\t}\n\t\t\t} else if (peer->isBroadcast()) {\n\t\t\t\treturn (typesRestriction & PeerType::Broadcast);\n\t\t\t} else {\n\t\t\t\treturn (typesRestriction & PeerType::Group);\n\t\t\t}\n\t\t}\n\t\t: Fn<bool(not_null<Data::Thread*>)>();\n\tauto controller = std::make_unique<Controller>(\n\t\tsession,\n\t\tstd::move(callback),\n\t\tstd::move(filter),\n\t\tselectable);\n\tconst auto raw = controller.get();\n\n\tstruct State {\n\t\tFn<void(Api::SendOptions)> submit;\n\t\trpl::variable<int> starsToSend;\n\t\tFn<void()> refreshStarsToSend;\n\t\trpl::lifetime submitLifetime;\n\t};\n\tconst auto state = std::make_shared<State>();\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tstate->refreshStarsToSend = [=] {\n\t\t\tauto perMessage = 0;\n\t\t\tfor (const auto &peer : box->collectSelectedRows()) {\n\t\t\t\tperMessage += peer->starsPerMessageChecked();\n\t\t\t}\n\t\t\tstate->starsToSend = perMessage;\n\t\t};\n\t\traw->selectionChanges(\n\t\t) | rpl::on_next([=] {\n\t\t\tbox->clearButtons();\n\t\t\tstate->refreshStarsToSend();\n\t\t\tconst auto shown = raw->hasSelected();\n\t\t\tif (shown) {\n\t\t\t\tconst auto weak = base::make_weak(box);\n\t\t\t\tstate->submit = [=](Api::SendOptions options) {\n\t\t\t\t\tstate->submitLifetime.destroy();\n\t\t\t\t\tconst auto show = box->peerListUiShow();\n\t\t\t\t\tconst auto peers = box->collectSelectedRows();\n\t\t\t\t\tconst auto withPaymentApproved = crl::guard(weak, [=](\n\t\t\t\t\t\t\tint approved) {\n\t\t\t\t\t\tauto copy = options;\n\t\t\t\t\t\tcopy.starsApproved = approved;\n\t\t\t\t\t\tif (const auto onstack = state->submit) {\n\t\t\t\t\t\t\tonstack(copy);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\tconst auto alreadyApproved = options.starsApproved;\n\t\t\t\t\tauto paid = std::vector<not_null<PeerData*>>();\n\t\t\t\t\tauto waiting = base::flat_set<not_null<PeerData*>>();\n\t\t\t\t\tauto totalStars = 0;\n\t\t\t\t\tfor (const auto &peer : peers) {\n\t\t\t\t\t\tconst auto details = ComputePaymentDetails(peer, 1);\n\t\t\t\t\t\tif (!details) {\n\t\t\t\t\t\t\twaiting.emplace(peer);\n\t\t\t\t\t\t} else if (details->stars > 0) {\n\t\t\t\t\t\t\ttotalStars += details->stars;\n\t\t\t\t\t\t\tpaid.push_back(peer);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (!waiting.empty()) {\n\t\t\t\t\t\tsession->changes().peerUpdates(\n\t\t\t\t\t\t\tData::PeerUpdate::Flag::FullInfo\n\t\t\t\t\t\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\t\t\t\t\t\tif (waiting.contains(update.peer)) {\n\t\t\t\t\t\t\t\twithPaymentApproved(alreadyApproved);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}, state->submitLifetime);\n\n\t\t\t\t\t\tif (!session->credits().loaded()) {\n\t\t\t\t\t\t\tsession->credits().loadedValue(\n\t\t\t\t\t\t\t) | rpl::filter(\n\t\t\t\t\t\t\t\trpl::mappers::_1\n\t\t\t\t\t\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t\t\t\t\t\twithPaymentApproved(alreadyApproved);\n\t\t\t\t\t\t\t}, state->submitLifetime);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t} else if (totalStars > alreadyApproved) {\n\t\t\t\t\t\tShowSendPaidConfirm(show, paid, SendPaymentDetails{\n\t\t\t\t\t\t\t.messages = 1,\n\t\t\t\t\t\t\t.stars = totalStars,\n\t\t\t\t\t\t}, [=] { withPaymentApproved(totalStars); });\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tstate->submit = nullptr;\n\n\t\t\t\t\tsendMany(ranges::views::all(\n\t\t\t\t\t\tpeers\n\t\t\t\t\t) | ranges::views::transform([&](\n\t\t\t\t\t\tnot_null<PeerData*> peer) -> Controller::Chosen {\n\t\t\t\t\t\treturn peer->owner().history(peer);\n\t\t\t\t\t}) | ranges::to_vector, options);\n\t\t\t\t};\n\t\t\t\tconst auto send = box->addButton(\n\t\t\t\t\ttr::lng_send_button(),\n\t\t\t\t\t[=] {\n\t\t\t\t\t\tif (const auto onstack = state->submit) {\n\t\t\t\t\t\t\tonstack({});\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\tsend->setText(PaidSendButtonText(\n\t\t\t\t\tstate->starsToSend.value(),\n\t\t\t\t\ttr::lng_send_button()));\n\t\t\t}\n\t\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\t\tbox->closeBox();\n\t\t\t});\n\t\t}, box->lifetime());\n\t\tif (titleOverride) {\n\t\t\tbox->setTitle(std::move(titleOverride));\n\t\t}\n\t};\n\tauto result = Box<PeerListBox>(\n\t\tstd::move(controller),\n\t\tstd::move(initBox));\n\t*weak = result.data();\n\n\treturn result;\n}\n\nbase::weak_qptr<Ui::BoxContent> ShowChooseRecipientBox(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tFnMut<bool(not_null<Data::Thread*>)> &&chosen,\n\t\trpl::producer<QString> titleOverride,\n\t\tFnMut<void()> &&successCallback,\n\t\tInlineBots::PeerTypes typesRestriction) {\n\treturn navigation->parentController()->show(PrepareChooseRecipientBox(\n\t\t&navigation->session(),\n\t\tstd::move(chosen),\n\t\tstd::move(titleOverride),\n\t\tstd::move(successCallback),\n\t\ttypesRestriction));\n}\n\nbase::weak_qptr<Ui::BoxContent> ShowForwardMessagesBox(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tData::ForwardDraft &&draft,\n\t\tFn<void()> &&successCallback) {\n\tconst auto session = &show->session();\n\tconst auto owner = &session->data();\n\tconst auto itemsList = owner->idsToItems(draft.ids);\n\tconst auto msgIds = owner->itemsToIds(itemsList);\n\tconst auto sendersCount = ItemsForwardSendersCount(itemsList);\n\tconst auto captionsCount = ItemsForwardCaptionsCount(itemsList);\n\tif (msgIds.empty()) {\n\t\treturn nullptr;\n\t}\n\n\tclass ListBox final : public PeerListBox {\n\tpublic:\n\t\tListBox(\n\t\t\tQWidget *parent,\n\t\t\tstd::unique_ptr<PeerListController> controller,\n\t\t\tFn<void(not_null<ListBox*>)> init)\n\t\t: PeerListBox(\n\t\t\tparent,\n\t\t\tstd::move(controller),\n\t\t\t[=](not_null<PeerListBox*> box) {\n\t\t\t\tinit(static_cast<ListBox*>(box.get()));\n\t\t\t}) {\n\t\t}\n\n\t\tvoid setBottomSkip(int bottomSkip) {\n\t\t\tPeerListBox::setInnerBottomSkip(bottomSkip);\n\t\t}\n\n\t\t[[nodiscard]] rpl::producer<> focusRequests() const {\n\t\t\treturn _focusRequests.events();\n\t\t}\n\n\t\t[[nodiscard]] Data::ForwardOptions forwardOptionsData() const {\n\t\t\treturn (_forwardOptions.captionsCount\n\t\t\t\t\t&& _forwardOptions.dropCaptions)\n\t\t\t\t? Data::ForwardOptions::NoNamesAndCaptions\n\t\t\t\t: _forwardOptions.dropNames\n\t\t\t\t? Data::ForwardOptions::NoSenderNames\n\t\t\t\t: Data::ForwardOptions::PreserveInfo;\n\t\t}\n\t\t[[nodiscard]] Ui::ForwardOptions forwardOptions() const {\n\t\t\treturn _forwardOptions;\n\t\t}\n\t\tvoid setForwardOptions(Ui::ForwardOptions forwardOptions) {\n\t\t\t_forwardOptions = forwardOptions;\n\t\t}\n\n\t\tnot_null<PeerListContent*> peerListContent() const {\n\t\t\treturn PeerListBox::content();\n\t\t}\n\n\t\tvoid setFilterId(FilterId filterId) {\n\t\t\t_filterId = filterId;\n\t\t}\n\t\t[[nodiscard]] FilterId filterId() const {\n\t\t\treturn _filterId;\n\t\t}\n\n\tprivate:\n\t\trpl::event_stream<> _focusRequests;\n\t\tUi::ForwardOptions _forwardOptions;\n\t\tFilterId _filterId = 0;\n\n\t};\n\n\tclass Controller final : public ChooseRecipientBoxController {\n\tpublic:\n\t\tusing Chosen = not_null<Data::Thread*>;\n\n\t\tController(not_null<Main::Session*> session)\n\t\t: ChooseRecipientBoxController({\n\t\t\t.session = session,\n\t\t\t.callback = [=](Chosen thread) {\n\t\t\t\t_singleChosen.fire_copy(thread);\n\t\t\t},\n\t\t\t.moneyRestrictionError = WriteMoneyRestrictionError,\n\t\t}) {\n\t\t}\n\n\t\tstd::unique_ptr<PeerListRow> createRestoredRow(\n\t\t\t\tnot_null<PeerData*> peer) override final {\n\t\t\treturn ChooseRecipientBoxController::createRow(\n\t\t\t\tpeer->owner().history(peer));\n\t\t}\n\n\t\tusing PeerListController::setSearchNoResultsText;\n\n\t\tvoid rowClicked(not_null<PeerListRow*> row) override final {\n\t\t\tconst auto count = delegate()->peerListSelectedRowsCount();\n\t\t\tconst auto forum = row->peer()->isForum();\n\t\t\tconst auto monoforum = row->peer()->isMonoforum();\n\t\t\tif (showLockedError(row) || (count && (forum || monoforum))) {\n\t\t\t\treturn;\n\t\t\t} else if (!count || forum || monoforum) {\n\t\t\t\tChooseRecipientBoxController::rowClicked(row);\n\t\t\t} else if (count) {\n\t\t\t\tdelegate()->peerListSetRowChecked(row, !row->checked());\n\t\t\t\t_selectionChanges.fire({});\n\t\t\t}\n\t\t}\n\n\t\tbase::unique_qptr<Ui::PopupMenu> rowContextMenu(\n\t\t\t\tQWidget *parent,\n\t\t\t\tnot_null<PeerListRow*> row) override final {\n\t\t\tif (!row->checked()\n\t\t\t\t&& !row->peer()->isForum()\n\t\t\t\t&& !row->peer()->isMonoforum()) {\n\t\t\t\tauto menu = base::make_unique_q<Ui::PopupMenu>(\n\t\t\t\t\tparent,\n\t\t\t\t\tst::popupMenuWithIcons);\n\t\t\t\tmenu->addAction(tr::lng_bot_choose_chat(tr::now), [=] {\n\t\t\t\t\tdelegate()->peerListSetRowChecked(row, true);\n\t\t\t\t\t_selectionChanges.fire({});\n\t\t\t\t}, &st::menuIconSelect);\n\t\t\t\treturn menu;\n\t\t\t}\n\t\t\treturn nullptr;\n\t\t}\n\n\t\t[[nodiscard]] rpl::producer<> selectionChanges() const {\n\t\t\treturn _selectionChanges.events_starting_with({});\n\t\t}\n\t\t[[nodiscard]] bool hasSelected() const {\n\t\t\treturn delegate()->peerListSelectedRowsCount() > 0;\n\t\t}\n\n\t\t[[nodiscard]] rpl::producer<Chosen> singleChosen() const{\n\t\t\treturn _singleChosen.events();\n\t\t}\n\n\tprivate:\n\t\trpl::event_stream<Chosen> _singleChosen;\n\t\trpl::event_stream<> _selectionChanges;\n\n\t};\n\n\tstruct State {\n\t\tnot_null<ListBox*> box;\n\t\tnot_null<Controller*> controller;\n\t\tbase::unique_qptr<Ui::PopupMenu> menu;\n\t\tFn<void(Api::SendOptions options)> submit;\n\t\trpl::variable<int> starsToSend;\n\t\tFn<void()> refreshStarsToSend;\n\t\trpl::lifetime submitLifetime;\n\t};\n\n\tconst auto applyFilter = [=](not_null<ListBox*> box, FilterId id) {\n\t\tbox->scrollToY(0);\n\t\tauto &filters = session->data().chatsFilters();\n\t\tconst auto &list = filters.list();\n\t\tif (list.size() <= 1) {\n\t\t\treturn;\n\t\t}\n\t\tif (box->filterId() == id) {\n\t\t\treturn;\n\t\t}\n\t\tbox->setFilterId(id);\n\n\t\tusing SavedState = PeerListController::SavedStateBase;\n\t\tauto state = std::make_unique<PeerListState>();\n\t\tstate->controllerState = std::make_unique<SavedState>();\n\n\t\tconst auto addList = [&](auto chats) {\n\t\t\tfor (const auto &row : chats->all()) {\n\t\t\t\tif (const auto history = row->history()) {\n\t\t\t\t\tstate->list.push_back(history->peer);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tif (!id) {\n\t\t\tstate->list.push_back(session->user());\n\t\t\taddList(session->data().chatsList()->indexed());\n\t\t\tconst auto folderId = Data::Folder::kId;\n\t\t\tif (const auto folder = session->data().folderLoaded(folderId)) {\n\t\t\t\taddList(folder->chatsList()->indexed());\n\t\t\t}\n\t\t\taddList(session->data().contactsNoChatsList());\n\t\t} else {\n\t\t\taddList(session->data().chatsFilters().chatsList(id)->indexed());\n\t\t}\n\t\tbox->peerListContent()->restoreState(std::move(state));\n\t};\n\n\tconst auto state = [&] {\n\t\tauto controller = std::make_unique<Controller>(session);\n\t\tconst auto controllerRaw = controller.get();\n\t\tauto init = [=](not_null<ListBox*> box) {\n\t\t\tcontrollerRaw->setSearchNoResultsText(\n\t\t\t\ttr::lng_bot_chats_not_found(tr::now));\n\t\t\tconst auto lastFilterId = box->lifetime().make_state<FilterId>(0);\n\t\t\tconst auto chatsFilters = Ui::AddChatFiltersTabsStrip(\n\t\t\t\tbox,\n\t\t\t\tsession,\n\t\t\t\t[=](FilterId id) {\n\t\t\t\t\t*lastFilterId = id;\n\t\t\t\t\tapplyFilter(box, id);\n\t\t\t\t},\n\t\t\t\tWindow::GifPauseReason::Layer);\n\t\t\tchatsFilters->lower();\n\t\t\trpl::combine(\n\t\t\t\tchatsFilters->heightValue(),\n\t\t\t\trpl::producer<bool>([=](auto consumer) {\n\t\t\t\t\tauto lifetime = rpl::lifetime();\n\t\t\t\t\tconsumer.put_next(false);\n\t\t\t\t\tbox->appendQueryChangedCallback([=](const QString &q) {\n\t\t\t\t\t\tconst auto hasQuery = !q.isEmpty();\n\t\t\t\t\t\tapplyFilter(box, hasQuery ? 0 : (*lastFilterId));\n\t\t\t\t\t\tconsumer.put_next_copy(hasQuery);\n\t\t\t\t\t});\n\t\t\t\t\treturn lifetime;\n\t\t\t\t})\n\t\t\t) | rpl::on_next([box](int h, bool hasQuery) {\n\t\t\t\tbox->setAddedTopScrollSkip(hasQuery ? 0 : h);\n\t\t\t}, box->lifetime());\n\t\t\tbox->multiSelectHeightValue() | rpl::on_next([=](int h) {\n\t\t\t\tchatsFilters->moveToLeft(0, h);\n\t\t\t}, chatsFilters->lifetime());\n\t\t};\n\t\tauto box = Box<ListBox>(std::move(controller), std::move(init));\n\t\tconst auto boxRaw = box.data();\n\t\tboxRaw->setForwardOptions({\n\t\t\t.sendersCount = sendersCount,\n\t\t\t.captionsCount = captionsCount,\n\t\t});\n\t\tshow->showBox(std::move(box));\n\t\tauto state = State{ boxRaw, controllerRaw };\n\t\treturn boxRaw->lifetime().make_state<State>(std::move(state));\n\t}();\n\n\t{ // Chosen a single.\n\t\tauto chosen = [show, draft = std::move(draft)](\n\t\t\t\tnot_null<Data::Thread*> thread) mutable {\n\t\t\tconst auto peer = thread->peer();\n\t\t\tif (peer->isSelf()\n\t\t\t\t&& !draft.ids.empty()\n\t\t\t\t&& draft.ids.front().peer != peer->id) {\n\t\t\t\tForwardToSelf(show, draft);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst auto id = SeparateId(\n\t\t\t\t((peer->isForum() && !peer->useSubsectionTabs())\n\t\t\t\t\t? SeparateType::Forum\n\t\t\t\t\t: SeparateType::Chat),\n\t\t\t\tthread);\n\t\t\tauto controller = Core::App().windowFor(id);\n\t\t\tif (!controller) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (controller->maybeSession() != &peer->session()) {\n\t\t\t\tcontroller = Core::App().ensureSeparateWindowFor(id);\n\t\t\t\tif (controller->maybeSession() != &peer->session()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst auto content = controller->sessionController()->content();\n\t\t\treturn content->setForwardDraft(thread, std::move(draft));\n\t\t};\n\t\tauto callback = [=, chosen = std::move(chosen)](\n\t\t\t\tController::Chosen thread) mutable {\n\t\t\tconst auto weak = base::make_weak(state->box);\n\t\t\tif (!chosen(thread)) {\n\t\t\t\treturn;\n\t\t\t} else if (const auto strong = weak.get()) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t\tif (successCallback) {\n\t\t\t\tsuccessCallback();\n\t\t\t}\n\t\t};\n\t\tstate->controller->singleChosen(\n\t\t) | rpl::on_next(std::move(callback), state->box->lifetime());\n\t}\n\n\tconst auto comment = Ui::CreateChild<Ui::SlideWrap<Ui::InputField>>(\n\t\tstate->box.get(),\n\t\tobject_ptr<Ui::InputField>(\n\t\t\tstate->box,\n\t\t\tst::shareComment,\n\t\t\tUi::InputField::Mode::MultiLine,\n\t\t\ttr::lng_photos_comment()),\n\t\tst::shareCommentPadding);\n\n\tconst auto history = session->data().message(msgIds.front())->history();\n\tconst auto send = ShareBox::DefaultForwardCallback(\n\t\tshow,\n\t\thistory,\n\t\tmsgIds);\n\tconst auto countMessages = ShareBox::DefaultForwardCountMessages(\n\t\thistory,\n\t\tmsgIds);\n\n\tconst auto weak = base::make_weak(state->box);\n\tconst auto field = comment->entity();\n\tstate->submit = [=](Api::SendOptions options) {\n\t\tconst auto peers = state->box->collectSelectedRows();\n\t\tauto comment = field->getTextWithAppliedMarkdown();\n\t\tconst auto checkPaid = [=] {\n\t\t\tconst auto withPaymentApproved = crl::guard(weak, [=](\n\t\t\t\t\tint approved) {\n\t\t\t\tauto copy = options;\n\t\t\t\tcopy.starsApproved = approved;\n\t\t\t\tif (const auto onstack = state->submit) {\n\t\t\t\t\tonstack(copy);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tconst auto alreadyApproved = options.starsApproved;\n\t\t\tconst auto messagesCount = countMessages(comment);\n\t\t\tauto paid = std::vector<not_null<PeerData*>>();\n\t\t\tauto waiting = base::flat_set<not_null<PeerData*>>();\n\t\t\tauto totalStars = 0;\n\t\t\tfor (const auto &peer : peers) {\n\t\t\t\tconst auto details = ComputePaymentDetails(\n\t\t\t\t\tpeer,\n\t\t\t\t\tmessagesCount);\n\t\t\t\tif (!details) {\n\t\t\t\t\twaiting.emplace(peer);\n\t\t\t\t} else if (details->stars > 0) {\n\t\t\t\t\ttotalStars += details->stars;\n\t\t\t\t\tpaid.push_back(peer);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!waiting.empty()) {\n\t\t\t\tsession->changes().peerUpdates(\n\t\t\t\t\tData::PeerUpdate::Flag::FullInfo\n\t\t\t\t) | rpl::on_next([=](const Data::PeerUpdate &update) {\n\t\t\t\t\tif (waiting.contains(update.peer)) {\n\t\t\t\t\t\twithPaymentApproved(alreadyApproved);\n\t\t\t\t\t}\n\t\t\t\t}, state->submitLifetime);\n\n\t\t\t\tif (!session->credits().loaded()) {\n\t\t\t\t\tsession->credits().loadedValue(\n\t\t\t\t\t) | rpl::filter(\n\t\t\t\t\t\trpl::mappers::_1\n\t\t\t\t\t) | rpl::take(1) | rpl::on_next([=] {\n\t\t\t\t\t\twithPaymentApproved(alreadyApproved);\n\t\t\t\t\t}, state->submitLifetime);\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t} else if (totalStars > alreadyApproved) {\n\t\t\t\tShowSendPaidConfirm(show, paid, SendPaymentDetails{\n\t\t\t\t\t.messages = messagesCount,\n\t\t\t\t\t.stars = totalStars,\n\t\t\t\t}, [=] { withPaymentApproved(totalStars); });\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tstate->submit = nullptr;\n\t\t\treturn true;\n\t\t};\n\t\tsend(\n\t\t\tranges::views::all(\n\t\t\t\tpeers\n\t\t\t) | ranges::views::transform([&](\n\t\t\t\t\tnot_null<PeerData*> peer) -> Controller::Chosen {\n\t\t\t\treturn peer->owner().history(peer);\n\t\t\t}) | ranges::to_vector,\n\t\t\tcheckPaid,\n\t\t\tstd::move(comment),\n\t\t\toptions,\n\t\t\tstate->box->forwardOptionsData());\n\t\tif (!state->submit && successCallback) {\n\t\t\tsuccessCallback();\n\t\t}\n\t};\n\n\tconst auto sendMenuType = [=] {\n\t\tconst auto selected = state->box->collectSelectedRows();\n\t\tconst auto hasPaid = [&] {\n\t\t\tfor (const auto &peer : selected) {\n\t\t\t\tif (peer->starsPerMessageChecked()) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t}();\n\t\treturn hasPaid\n\t\t\t? SendMenu::Type::SilentOnly\n\t\t\t: ranges::all_of(selected, HistoryView::CanScheduleUntilOnline)\n\t\t\t? SendMenu::Type::ScheduledToUser\n\t\t\t: ((selected.size() == 1) && selected.front()->isSelf())\n\t\t\t? SendMenu::Type::Reminder\n\t\t\t: SendMenu::Type::Scheduled;\n\t};\n\n\tconst auto showForwardOptions = true;\n\tconst auto showMenu = [=](not_null<Ui::RpWidget*> parent) {\n\t\tif (state->menu) {\n\t\t\tstate->menu = nullptr;\n\t\t\treturn;\n\t\t}\n\t\tstate->menu.emplace(parent, st::popupMenuWithIcons);\n\n\t\tif (showForwardOptions) {\n\t\t\tauto createView = [&](\n\t\t\t\t\trpl::producer<QString> &&text,\n\t\t\t\t\tbool checked) {\n\t\t\t\tauto item = base::make_unique_q<Menu::ItemWithCheck>(\n\t\t\t\t\tstate->menu->menu(),\n\t\t\t\t\tst::popupMenuWithIcons.menu,\n\t\t\t\t\tUi::CreateChild<QAction>(state->menu->menu().get()),\n\t\t\t\t\tnullptr,\n\t\t\t\t\tnullptr);\n\t\t\t\tstd::move(\n\t\t\t\t\ttext\n\t\t\t\t) | rpl::on_next([action = item->action()](\n\t\t\t\t\t\tQString text) {\n\t\t\t\t\taction->setText(text);\n\t\t\t\t}, item->lifetime());\n\t\t\t\titem->init(checked);\n\t\t\t\tconst auto view = item->checkView();\n\t\t\t\tstate->menu->addAction(std::move(item));\n\t\t\t\treturn view;\n\t\t\t};\n\t\t\tUi::FillForwardOptions(\n\t\t\t\tstd::move(createView),\n\t\t\t\tstate->box->forwardOptions(),\n\t\t\t\t[=](Ui::ForwardOptions o) {\n\t\t\t\t\tstate->box->setForwardOptions(o);\n\t\t\t\t},\n\t\t\t\tstate->menu->lifetime());\n\n\t\t\tstate->menu->addSeparator();\n\t\t}\n\t\tstate->menu->setForcedVerticalOrigin(\n\t\t\tUi::PopupMenu::VerticalOrigin::Bottom);\n\t\tSendMenu::FillSendMenu(\n\t\t\tstate->menu.get(),\n\t\t\tshow,\n\t\t\tSendMenu::Details{ sendMenuType() },\n\t\t\tSendMenu::DefaultCallback(show, crl::guard(parent, [=](\n\t\t\t\t\tApi::SendOptions options) {\n\t\t\t\tif (const auto onstack = state->submit) {\n\t\t\t\t\tonstack(options);\n\t\t\t\t}\n\t\t\t})));\n\t\tif (showForwardOptions || !state->menu->empty()) {\n\t\t\tstate->menu->popup(QCursor::pos());\n\t\t}\n\t};\n\n\tstate->refreshStarsToSend = [=] {\n\t\tauto perMessage = 0;\n\t\tfor (const auto &peer : state->box->collectSelectedRows()) {\n\t\t\tperMessage += peer->starsPerMessageChecked();\n\t\t}\n\t\tstate->starsToSend = perMessage\n\t\t\t* countMessages(field->getTextWithTags());\n\t};\n\n\tcomment->hide(anim::type::instant);\n\tcomment->toggleOn(state->controller->selectionChanges(\n\t) | rpl::map([=] {\n\t\treturn state->controller->hasSelected();\n\t}));\n\n\trpl::combine(\n\t\tstate->box->sizeValue(),\n\t\tcomment->heightValue()\n\t) | rpl::on_next([=](const QSize &size, int commentHeight) {\n\t\tcomment->moveToLeft(0, size.height() - commentHeight);\n\t\tcomment->resizeToWidth(size.width());\n\n\t\tstate->box->setBottomSkip(comment->isHidden() ? 0 : commentHeight);\n\t}, comment->lifetime());\n\n\tfield->submits(\n\t) | rpl::on_next([=] {\n\t\tif (const auto onstack = state->submit) {\n\t\t\tonstack({});\n\t\t}\n\t}, field->lifetime());\n\tInitMessageFieldHandlers({\n\t\t.session = session,\n\t\t.show = show,\n\t\t.field = field,\n\t\t.customEmojiPaused = [=] {\n\t\t\treturn show->paused(GifPauseReason::Layer);\n\t\t},\n\t});\n\tfield->setSubmitSettings(Core::App().settings().sendSubmitWay());\n\tfield->changes() | rpl::on_next([=] {\n\t\tstate->refreshStarsToSend();\n\t}, field->lifetime());\n\n\tUi::SendPendingMoveResizeEvents(comment);\n\n\tstate->box->focusRequests(\n\t) | rpl::on_next([=] {\n\t\tif (!comment->isHidden()) {\n\t\t\tcomment->entity()->setFocusFast();\n\t\t}\n\t}, comment->lifetime());\n\n\tstate->controller->selectionChanges(\n\t) | rpl::on_next([=] {\n\t\tconst auto shown = state->controller->hasSelected();\n\n\t\tstate->box->clearButtons();\n\t\tstate->refreshStarsToSend();\n\t\tif (shown) {\n\t\t\tconst auto send = state->box->addButton(\n\t\t\t\ttr::lng_send_button(),\n\t\t\t\t[=] {\n\t\t\t\t\tif (const auto onstack = state->submit) {\n\t\t\t\t\t\tonstack({});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\tsend->setAcceptBoth();\n\t\t\tsend->clicks(\n\t\t\t) | rpl::on_next([=](Qt::MouseButton button) {\n\t\t\t\tif (button == Qt::RightButton) {\n\t\t\t\t\tshowMenu(send);\n\t\t\t\t}\n\t\t\t}, send->lifetime());\n\t\t\tsend->setText(PaidSendButtonText(\n\t\t\t\tstate->starsToSend.value(),\n\t\t\t\ttr::lng_send_button()));\n\t\t}\n\t\tstate->box->addButton(tr::lng_cancel(), [=] {\n\t\t\tstate->box->closeBox();\n\t\t});\n\t}, state->box->lifetime());\n\n\treturn base::make_weak(state->box);\n}\n\nbase::weak_qptr<Ui::BoxContent> ShowForwardMessagesBox(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tData::ForwardDraft &&draft,\n\t\tFn<void()> &&successCallback) {\n\treturn ShowForwardMessagesBox(\n\t\tnavigation->uiShow(),\n\t\tstd::move(draft),\n\t\tstd::move(successCallback));\n}\n\nbase::weak_qptr<Ui::BoxContent> ShowForwardMessagesBox(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tMessageIdsList &&items,\n\t\tFn<void()> &&successCallback) {\n\treturn ShowForwardMessagesBox(\n\t\tnavigation,\n\t\tData::ForwardDraft{ .ids = std::move(items) },\n\t\tstd::move(successCallback));\n}\n\nbase::weak_qptr<Ui::BoxContent> ShowShareGameBox(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<UserData*> bot,\n\t\tQString shortName) {\n\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tauto chosen = [=](not_null<Data::Thread*> thread) mutable {\n\t\tconst auto confirm = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\t\tauto send = crl::guard(thread, [=] {\n\t\t\tShareBotGame(bot, thread, shortName);\n\t\t\tif (const auto strong = *weak) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t\tif (const auto strong = *confirm) {\n\t\t\t\tstrong->closeBox();\n\t\t\t}\n\t\t\tnavigation->showThread(\n\t\t\t\tthread,\n\t\t\t\tShowAtUnreadMsgId,\n\t\t\t\tSectionShow::Way::ClearStack);\n\t\t});\n\t\tconst auto confirmText = thread->peer()->isUser()\n\t\t\t? tr::lng_bot_sure_share_game(\n\t\t\t\ttr::now,\n\t\t\t\tlt_user,\n\t\t\t\tthread->chatListName())\n\t\t\t: tr::lng_bot_sure_share_game_group(\n\t\t\t\ttr::now,\n\t\t\t\tlt_group,\n\t\t\t\tthread->chatListName());\n\t\t*confirm = navigation->parentController()->show(\n\t\t\tUi::MakeConfirmBox({\n\t\t\t\t.text = confirmText,\n\t\t\t\t.confirmed = std::move(send),\n\t\t\t}));\n\t};\n\tauto filter = [](not_null<Data::Thread*> thread) {\n\t\treturn !thread->peer()->isSelf()\n\t\t\t&& (Data::CanSend(thread, ChatRestriction::SendGames)\n\t\t\t\t|| thread->asForum());\n\t};\n\tauto initBox = [](not_null<PeerListBox*> box) {\n\t\tbox->addButton(tr::lng_cancel(), [box] {\n\t\t\tbox->closeBox();\n\t\t});\n\t};\n\t*weak = navigation->parentController()->show(Box<PeerListBox>(\n\t\tstd::make_unique<ChooseRecipientBoxController>(ChooseRecipientArgs{\n\t\t\t.session = &navigation->session(),\n\t\t\t.callback = std::move(chosen),\n\t\t\t.filter = std::move(filter),\n\t\t\t.moneyRestrictionError = WriteMoneyRestrictionError,\n\t\t}),\n\t\tstd::move(initBox)));\n\treturn weak->get();\n}\n\nbase::weak_qptr<Ui::BoxContent> ShowDropMediaBox(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tstd::shared_ptr<QMimeData> data,\n\t\tnot_null<Data::Forum*> forum,\n\t\tFnMut<void()> &&successCallback) {\n\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tauto chosen = [\n\t\tdata = std::move(data),\n\t\tcallback = std::move(successCallback),\n\t\tweak,\n\t\tnavigation\n\t](not_null<Data::Thread*> thread) mutable {\n\t\tconst auto content = navigation->parentController()->content();\n\t\tif (!content->filesOrForwardDrop(thread, data.get(), true)) {\n\t\t\treturn;\n\t\t} else if (const auto strong = *weak) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t\tif (callback) {\n\t\t\tcallback();\n\t\t}\n\t};\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\n\t\tforum->destroyed(\n\t\t) | rpl::on_next([=] {\n\t\t\tbox->closeBox();\n\t\t}, box->lifetime());\n\t};\n\t*weak = navigation->parentController()->show(Box<PeerListBox>(\n\t\tstd::make_unique<ChooseTopicBoxController>(\n\t\t\tforum,\n\t\t\tstd::move(chosen)),\n\t\tstd::move(initBox)));\n\treturn weak->get();\n}\n\nbase::weak_qptr<Ui::BoxContent> ShowDropMediaBox(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tstd::shared_ptr<QMimeData> data,\n\t\tnot_null<Data::SavedMessages*> monoforum,\n\t\tFnMut<void()> &&successCallback) {\n\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\tauto chosen = [\n\t\tdata = std::move(data),\n\t\tcallback = std::move(successCallback),\n\t\tweak,\n\t\tnavigation\n\t](not_null<Data::SavedSublist*> sublist) mutable {\n\t\tconst auto content = navigation->parentController()->content();\n\t\tif (!content->filesOrForwardDrop(sublist, data.get(), true)) {\n\t\t\treturn;\n\t\t} else if (const auto strong = *weak) {\n\t\t\tstrong->closeBox();\n\t\t}\n\t\tif (callback) {\n\t\t\tcallback();\n\t\t}\n\t};\n\tauto initBox = [=](not_null<PeerListBox*> box) {\n\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\n\t\tmonoforum->destroyed(\n\t\t) | rpl::on_next([=] {\n\t\t\tbox->closeBox();\n\t\t}, box->lifetime());\n\t};\n\t*weak = navigation->parentController()->show(Box<PeerListBox>(\n\t\tstd::make_unique<ChooseSublistBoxController>(\n\t\t\tmonoforum,\n\t\t\tstd::move(chosen)),\n\t\tstd::move(initBox)));\n\treturn weak->get();\n}\n\nbase::weak_qptr<Ui::BoxContent> ShowSendNowMessagesBox(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<History*> history,\n\t\tMessageIdsList &&items,\n\t\tFn<void()> &&successCallback) {\n\tconst auto session = &navigation->session();\n\tconst auto text = (items.size() > 1)\n\t\t? tr::lng_scheduled_send_now_many(tr::now, lt_count, items.size())\n\t\t: tr::lng_scheduled_send_now(tr::now);\n\n\tconst auto list = session->data().idsToItems(items);\n\tconst auto error = GetErrorForSending(\n\t\thistory->peer,\n\t\t{ .forward = &list });\n\tif (error) {\n\t\tData::ShowSendErrorToast(navigation, history->peer, error);\n\t\treturn { nullptr };\n\t}\n\tauto done = [\n\t\t=,\n\t\tlist = std::move(items),\n\t\tcallback = std::move(successCallback)\n\t](Fn<void()> &&close) {\n\t\tclose();\n\t\tauto ids = QVector<MTPint>();\n\t\tauto sorted = session->data().idsToItems(list);\n\t\tranges::sort(sorted, ranges::less(), &HistoryItem::date);\n\t\tfor (const auto &item : sorted) {\n\t\t\tif (item->allowsSendNow()) {\n\t\t\t\tids.push_back(\n\t\t\t\t\tMTP_int(session->scheduledMessages().lookupId(item)));\n\t\t\t}\n\t\t}\n\t\tsession->api().request(MTPmessages_SendScheduledMessages(\n\t\t\thistory->peer->input(),\n\t\t\tMTP_vector<MTPint>(ids)\n\t\t)).done([=](const MTPUpdates &result) {\n\t\t\tsession->api().applyUpdates(result);\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tsession->api().sendMessageFail(error, history->peer);\n\t\t}).send();\n\t\tif (callback) {\n\t\t\tcallback();\n\t\t}\n\t};\n\treturn navigation->parentController()->show(Ui::MakeConfirmBox({\n\t\t.text = text,\n\t\t.confirmed = std::move(done),\n\t\t.confirmText = tr::lng_send_button(),\n\t}));\n}\n\nvoid PeerMenuAddChannelMembers(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<ChannelData*> channel) {\n\tif (!channel->isMegagroup()\n\t\t&& (channel->membersCount()\n\t\t\t>= channel->session().serverConfig().chatSizeMax)) {\n\t\tnavigation->parentController()->show(Box<MaxInviteBox>(channel));\n\t\treturn;\n\t}\n\tconst auto api = &channel->session().api();\n\tapi->chatParticipants().requestForAdd(channel, crl::guard(navigation, [=](\n\t\t\tconst Api::ChatParticipants::TLMembers &data) {\n\t\tconst auto &[availableCount, list] = Api::ChatParticipants::Parse(\n\t\t\tchannel,\n\t\t\tdata);\n\t\tconst auto already = (\n\t\t\tlist\n\t\t) | ranges::views::transform([&](const Api::ChatParticipant &p) {\n\t\t\treturn p.isUser()\n\t\t\t\t? channel->owner().userLoaded(p.userId())\n\t\t\t\t: nullptr;\n\t\t}) | ranges::views::filter([](UserData *user) {\n\t\t\treturn (user != nullptr);\n\t\t}) | ranges::to_vector;\n\n\t\tAddParticipantsBoxController::Start(\n\t\t\tnavigation,\n\t\t\tchannel,\n\t\t\t{ already.begin(), already.end() });\n\t}));\n}\n\nvoid ToggleMessagePinned(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tFullMsgId itemId,\n\t\tbool pin) {\n\tconst auto item = navigation->session().data().message(itemId);\n\tif (!item || !item->canPin()) {\n\t\treturn;\n\t}\n\tif (pin) {\n\t\tnavigation->parentController()->show(\n\t\t\tBox(PinMessageBox, item),\n\t\t\tUi::LayerOption::CloseOther);\n\t} else {\n\t\tconst auto peer = item->history()->peer;\n\t\tconst auto session = &peer->session();\n\t\tconst auto callback = crl::guard(session, [=](Fn<void()> &&close) {\n\t\t\tclose();\n\t\t\tsession->api().request(MTPmessages_UpdatePinnedMessage(\n\t\t\t\tMTP_flags(MTPmessages_UpdatePinnedMessage::Flag::f_unpin),\n\t\t\t\tpeer->input(),\n\t\t\t\tMTP_int(itemId.msg)\n\t\t\t)).done([=](const MTPUpdates &result) {\n\t\t\t\tsession->api().applyUpdates(result);\n\t\t\t}).send();\n\t\t});\n\t\tnavigation->parentController()->show(\n\t\t\tUi::MakeConfirmBox({\n\t\t\t\t.text = tr::lng_pinned_unpin_sure(),\n\t\t\t\t.confirmed = callback,\n\t\t\t\t.confirmText = tr::lng_pinned_unpin(),\n\t\t\t}),\n\t\t\tUi::LayerOption::CloseOther);\n\t}\n}\n\nvoid HidePinnedBar(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<PeerData*> peer,\n\t\tMsgId topicRootId,\n\t\tPeerId monoforumPeerId,\n\t\tFn<void()> onHidden) {\n\tconst auto callback = crl::guard(navigation, [=](Fn<void()> &&close) {\n\t\tclose();\n\t\tauto &session = peer->session();\n\t\tconst auto migrated = (topicRootId || monoforumPeerId)\n\t\t\t? nullptr\n\t\t\t: peer->migrateFrom();\n\t\tconst auto top = Data::ResolveTopPinnedId(\n\t\t\tpeer,\n\t\t\ttopicRootId,\n\t\t\tmonoforumPeerId,\n\t\t\tmigrated);\n\t\tconst auto universal = !top\n\t\t\t? MsgId(0)\n\t\t\t: (migrated && !peerIsChannel(top.peer))\n\t\t\t? (top.msg - ServerMaxMsgId)\n\t\t\t: top.msg;\n\t\tif (universal) {\n\t\t\tsession.settings().setHiddenPinnedMessageId(\n\t\t\t\tpeer->id,\n\t\t\t\ttopicRootId,\n\t\t\t\tmonoforumPeerId,\n\t\t\t\tuniversal);\n\t\t\tsession.saveSettingsDelayed();\n\t\t\tif (onHidden) {\n\t\t\t\tonHidden();\n\t\t\t}\n\t\t} else {\n\t\t\tsession.api().requestFullPeer(peer);\n\t\t}\n\t});\n\tnavigation->parentController()->show(\n\t\tUi::MakeConfirmBox({\n\t\t\t.text = tr::lng_pinned_hide_all_sure(),\n\t\t\t.confirmed = callback,\n\t\t\t.confirmText = tr::lng_pinned_hide_all_hide(),\n\t\t}),\n\t\tUi::LayerOption::CloseOther);\n}\n\nvoid UnpinAllMessages(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tnot_null<Data::Thread*> thread) {\n\tconst auto weak = base::make_weak(thread);\n\tconst auto callback = crl::guard(navigation, [=](Fn<void()> &&close) {\n\t\tclose();\n\t\tconst auto strong = weak.get();\n\t\tif (!strong) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto api = &strong->session().api();\n\t\tconst auto sendRequest = [=](auto self) -> void {\n\t\t\tconst auto history = strong->owningHistory();\n\t\t\tconst auto topicRootId = strong->topicRootId();\n\t\t\tconst auto sublist = strong->asSublist();\n\t\t\tconst auto monoforumPeerId = strong->monoforumPeerId();\n\t\t\tusing Flag = MTPmessages_UnpinAllMessages::Flag;\n\t\t\tapi->request(MTPmessages_UnpinAllMessages(\n\t\t\t\tMTP_flags((topicRootId ? Flag::f_top_msg_id : Flag())\n\t\t\t\t\t| (sublist ? Flag::f_saved_peer_id : Flag())),\n\t\t\t\thistory->peer->input(),\n\t\t\t\tMTP_int(topicRootId.bare),\n\t\t\t\tsublist ? sublist->sublistPeer()->input() : MTPInputPeer()\n\t\t\t)).done([=](const MTPmessages_AffectedHistory &result) {\n\t\t\t\tconst auto peer = history->peer;\n\t\t\t\tconst auto offset = api->applyAffectedHistory(peer, result);\n\t\t\t\tif (offset > 0) {\n\t\t\t\t\tself(self);\n\t\t\t\t} else {\n\t\t\t\t\thistory->unpinMessagesFor(topicRootId, monoforumPeerId);\n\t\t\t\t}\n\t\t\t}).send();\n\t\t};\n\t\tsendRequest(sendRequest);\n\t});\n\tnavigation->parentController()->show(\n\t\tUi::MakeConfirmBox({\n\t\t\t.text = tr::lng_pinned_unpin_all_sure(),\n\t\t\t.confirmed = callback,\n\t\t\t.confirmText = tr::lng_pinned_unpin(),\n\t\t}),\n\t\tUi::LayerOption::CloseOther);\n}\n\nvoid MenuAddMarkAsReadAllChatsAction(\n\t\tnot_null<Main::Session*> session,\n\t\tstd::shared_ptr<Ui::Show> show,\n\t\tconst PeerMenuCallback &addAction) {\n\tauto callback = [=, owner = &session->data()] {\n\t\tauto boxCallback = [=](Fn<void()> &&close) {\n\t\t\tclose();\n\n\t\t\tMarkAsReadChatList(owner->chatsList());\n\t\t\tif (const auto folder = owner->folderLoaded(Data::Folder::kId)) {\n\t\t\t\tMarkAsReadChatList(folder->chatsList());\n\t\t\t}\n\t\t};\n\t\tshow->show(\n\t\t\tBox([=](not_null<Ui::GenericBox*> box) {\n\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\tUi::AddSkip(box->verticalLayout());\n\t\t\t\tconst auto userpic = Ui::CreateChild<Ui::UserpicButton>(\n\t\t\t\t\tbox->verticalLayout(),\n\t\t\t\t\tsession->user(),\n\t\t\t\t\tst::mainMenuUserpic);\n\t\t\t\tUi::IconWithTitle(\n\t\t\t\t\tbox->verticalLayout(),\n\t\t\t\t\tuserpic,\n\t\t\t\t\tUi::CreateChild<Ui::FlatLabel>(\n\t\t\t\t\t\tbox->verticalLayout(),\n\t\t\t\t\t\tInfo::Profile::NameValue(session->user()),\n\t\t\t\t\t\tbox->getDelegate()->style().title));\n\t\t\t\tauto text = rpl::combine(\n\t\t\t\t\ttr::lng_context_mark_read_all_sure(),\n\t\t\t\t\ttr::lng_context_mark_read_all_sure_2(\n\t\t\t\t\t\ttr::rich)\n\t\t\t\t) | rpl::map([](QString t1, TextWithEntities t2) {\n\t\t\t\t\treturn TextWithEntities()\n\t\t\t\t\t\t.append(std::move(t1))\n\t\t\t\t\t\t.append('\\n')\n\t\t\t\t\t\t.append('\\n')\n\t\t\t\t\t\t.append(std::move(t2));\n\t\t\t\t});\n\t\t\t\tUi::ConfirmBox(box, {\n\t\t\t\t\t.text = std::move(text),\n\t\t\t\t\t.confirmed = std::move(boxCallback),\n\t\t\t\t\t.confirmStyle = &st::attentionBoxButton,\n\t\t\t\t});\n\t\t\t}),\n\t\t\tUi::LayerOption::CloseOther);\n\t};\n\taddAction(\n\t\ttr::lng_context_mark_read_all(tr::now),\n\t\tstd::move(callback),\n\t\t&st::menuIconMarkRead);\n}\n\nvoid MenuAddMarkAsReadChatListAction(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tFn<not_null<Dialogs::MainList*>()> &&list,\n\t\tconst PeerMenuCallback &addAction,\n\t\tFn<Dialogs::UnreadState()> customUnreadState) {\n\t// There is no async to make weak from controller.\n\tconst auto unreadState = customUnreadState\n\t\t? customUnreadState()\n\t\t: list()->unreadState();\n\tif (!unreadState.messages && !unreadState.marks && !unreadState.chats) {\n\t\treturn;\n\t}\n\n\tauto callback = [=] {\n\t\tif (unreadState.messages > kMaxUnreadWithoutConfirmation) {\n\t\t\tauto boxCallback = [=](Fn<void()> &&close) {\n\t\t\t\tMarkAsReadChatList(list());\n\t\t\t\tclose();\n\t\t\t};\n\t\t\tcontroller->show(\n\t\t\t\tUi::MakeConfirmBox({\n\t\t\t\t\ttr::lng_context_mark_read_sure(),\n\t\t\t\t\tstd::move(boxCallback)\n\t\t\t\t}),\n\t\t\t\tUi::LayerOption::CloseOther);\n\t\t} else {\n\t\t\tMarkAsReadChatList(list());\n\t\t}\n\t};\n\taddAction(\n\t\ttr::lng_context_mark_read(tr::now),\n\t\tstd::move(callback),\n\t\t&st::menuIconMarkRead);\n}\n\nvoid ToggleHistoryArchived(\n\t\tstd::shared_ptr<ChatHelpers::Show> show,\n\t\tnot_null<History*> history,\n\t\tbool archived) {\n\tconst auto callback = [=] {\n\t\tshow->showToast(Ui::Toast::Config{\n\t\t\t.text = { (archived\n\t\t\t\t? tr::lng_archived_added(tr::now)\n\t\t\t\t: tr::lng_archived_removed(tr::now)) },\n\t\t\t.st = &st::windowArchiveToast,\n\t\t\t.duration = (archived\n\t\t\t\t? kArchivedToastDuration\n\t\t\t\t: Ui::Toast::kDefaultDuration),\n\t\t});\n\t};\n\thistory->session().api().toggleHistoryArchived(\n\t\thistory,\n\t\tarchived,\n\t\tcallback);\n}\n\nFn<void()> ClearHistoryHandler(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer) {\n\treturn [=] {\n\t\tif (!controller->showFrozenError()) {\n\t\t\tcontroller->show(Box<DeleteMessagesBox>(peer, true));\n\t\t}\n\t};\n}\n\nFn<void()> DeleteAndLeaveHandler(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<PeerData*> peer) {\n\tconst auto isCreator = [&] {\n\t\tif (const auto channel = peer->asChannel()) {\n\t\t\treturn channel->amCreator();\n\t\t} else if (const auto chat = peer->asChat()) {\n\t\t\treturn chat->amCreator();\n\t\t}\n\t\treturn false;\n\t}();\n\tif (isCreator) {\n\t\tconst auto requestId = std::make_shared<mtpRequestId>(0);\n\t\treturn [=] {\n\t\t\tif (controller->showFrozenError() || (*requestId > 0)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t*requestId = peer->session().api().request(\n\t\t\t\tMTPmessages_GetFutureChatCreatorAfterLeave(\n\t\t\t\t\tpeer->input()\n\t\t\t)).done([=](const MTPUser &result) {\n\t\t\t\t*requestId = 0;\n\t\t\t\tconst auto user = peer->owner().processUser(result);\n\t\t\t\tcontroller->show(Box(SelectFutureOwnerbox, peer, user));\n\t\t\t}).fail([=](const MTP::Error &error) {\n\t\t\t\t*requestId = 0;\n\t\t\t\tcontroller->show(Box(DeleteChatBox, peer));\n\t\t\t}).send();\n\t\t};\n\t}\n\treturn [=] {\n\t\tif (!controller->showFrozenError()) {\n\t\t\tcontroller->show(Box(DeleteChatBox, peer));\n\t\t}\n\t};\n}\n\nFn<void()> DeleteSublistHandler(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Data::SavedSublist*> sublist) {\n\tconst auto weak = base::make_weak(sublist.get());\n\treturn [=] {\n\t\tif (const auto strong = weak.get()) {\n\t\t\tif (!controller->showFrozenError()) {\n\t\t\t\tcontroller->show(Box(DeleteSublistBox, strong));\n\t\t\t}\n\t\t}\n\t};\n}\n\nvoid FillDialogsEntryMenu(\n\t\tnot_null<SessionController*> controller,\n\t\tDialogs::EntryState request,\n\t\tconst PeerMenuCallback &callback) {\n\tFiller(controller, request, callback).fill();\n}\n\nbool FillVideoChatMenu(\n\t\tnot_null<SessionController*> controller,\n\t\tDialogs::EntryState request,\n\t\tconst PeerMenuCallback &addAction) {\n\tconst auto peer = request.key.peer();\n\tif (!peer || peer->isUser()) {\n\t\treturn false;\n\t}\n\n\tconst auto callback = [=](Calls::StartGroupCallArgs &&args) {\n\t\tcontroller->startOrJoinGroupCall(peer, std::move(args));\n\t};\n\tconst auto rtmpCallback = [=] {\n\t\tCore::App().calls().showStartWithRtmp(controller->uiShow(), peer);\n\t};\n\tconst auto livestream = !peer->isMegagroup() && peer->isChannel();\n\tconst auto has = (peer->groupCall() != nullptr);\n\tconst auto manager = peer->canManageGroupCall();\n\tif (has) {\n\t\taddAction(\n\t\t\ttr::lng_menu_start_group_call_join(tr::now),\n\t\t\t[=] { callback({}); },\n\t\t\t&st::menuIconVideoChat);\n\t} else if (manager) {\n\t\taddAction(\n\t\t\t(livestream\n\t\t\t\t? tr::lng_menu_start_group_call_channel\n\t\t\t\t: tr::lng_menu_start_group_call)(tr::now),\n\t\t\t[=] { callback({}); },\n\t\t\t&st::menuIconStartStream);\n\t}\n\tif (!has && manager) {\n\t\taddAction(\n\t\t\t(livestream\n\t\t\t\t? tr::lng_menu_start_group_call_scheduled_channel\n\t\t\t\t: tr::lng_menu_start_group_call_scheduled)(tr::now),\n\t\t\t[=] { callback({ .scheduleNeeded = true }); },\n\t\t\t&st::menuIconReschedule);\n\t\taddAction(\n\t\t\t(livestream\n\t\t\t\t? tr::lng_menu_start_group_call_with_channel\n\t\t\t\t: tr::lng_menu_start_group_call_with)(tr::now),\n\t\t\trtmpCallback,\n\t\t\t&st::menuIconStartStreamWith);\n\t}\n\treturn has || manager;\n}\n\nvoid FillSenderUserpicMenu(\n\t\tnot_null<SessionController*> controller,\n\t\tnot_null<PeerData*> peer,\n\t\tPeerData *groupPeer,\n\t\tUi::InputField *fieldForMention,\n\t\tDialogs::Key searchInEntry,\n\t\tconst PeerMenuCallback &addAction) {\n\tconst auto group = (peer->isChat() || peer->isMegagroup());\n\tconst auto channel = peer->isChannel();\n\tconst auto viewProfileText = group\n\t\t? tr::lng_context_view_group(tr::now)\n\t\t: channel\n\t\t? tr::lng_context_view_channel(tr::now)\n\t\t: tr::lng_context_view_profile(tr::now);\n\taddAction(viewProfileText, [=] {\n\t\tcontroller->showPeerInfo(peer, Window::SectionShow::Way::Forward);\n\t}, channel ? &st::menuIconInfo : &st::menuIconProfile);\n\n\tconst auto showHistoryText = group\n\t\t? tr::lng_context_open_group(tr::now)\n\t\t: channel\n\t\t? tr::lng_context_open_channel(tr::now)\n\t\t: tr::lng_profile_send_message(tr::now);\n\taddAction(showHistoryText, [=] {\n\t\tcontroller->showPeerHistory(peer, Window::SectionShow::Way::Forward);\n\t}, channel ? &st::menuIconChannel : &st::menuIconChatBubble);\n\n\tconst auto username = peer->username();\n\tconst auto mention = !username.isEmpty() || peer->isUser();\n\tif (const auto guard = mention ? fieldForMention : nullptr) {\n\t\taddAction(tr::lng_context_mention(tr::now), crl::guard(guard, [=] {\n\t\t\tif (!username.isEmpty()) {\n\t\t\t\tfieldForMention->insertTag('@' + username);\n\t\t\t} else {\n\t\t\t\tfieldForMention->insertTag(\n\t\t\t\t\tpeer->shortName(),\n\t\t\t\t\tPrepareMentionTag(peer->asUser()));\n\t\t\t}\n\t\t}), &st::menuIconUsername);\n\t}\n\n\tif (searchInEntry) {\n\t\taddAction(tr::lng_context_search_from(tr::now), [=] {\n\t\t\tcontroller->searchInChat(searchInEntry, peer);\n\t\t}, &st::menuIconSearch);\n\t}\n\n\tif (const auto user = peer->asUser()) {\n\t\tif (groupPeer) {\n\t\t\t// Discussion group users may not be members,\n\t\t\t// so editing their tag is not available.\n\t\t\tconst auto groupChannel = groupPeer->asChannel();\n\t\t\tconst auto isDiscussionGroup = groupChannel\n\t\t\t\t&& groupChannel->isMegagroup()\n\t\t\t\t&& groupChannel->discussionLink();\n\t\t\tconst auto canEditTarget = [&] {\n\t\t\t\tif (const auto chat = groupPeer->asChat()) {\n\t\t\t\t\tif (peerToUser(user->id) == chat->creator) {\n\t\t\t\t\t\treturn chat->amCreator();\n\t\t\t\t\t}\n\t\t\t\t\tif (chat->admins.contains(user)) {\n\t\t\t\t\t\treturn chat->amCreator();\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t} else if (const auto channel = groupPeer->asChannel()) {\n\t\t\t\t\tif (channel->mgInfo\n\t\t\t\t\t&& (channel->mgInfo->lastAdmins.contains(user)\n\t\t\t\t\t\t|| channel->mgInfo->creator == user)) {\n\t\t\t\t\treturn channel->canEditAdmin(user);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}();\n\t\t\tif (!isDiscussionGroup\n\t\t\t\t&& canEditTarget\n\t\t\t\t&& groupPeer->canManageRanks()\n\t\t\t\t&& !user->isSelf()) {\n\t\t\t\tconst auto currentRank = LookupMemberRank(\n\t\t\t\t\tgroupPeer,\n\t\t\t\t\tuser);\n\t\t\t\taddAction(\n\t\t\t\t\t(currentRank.isEmpty()\n\t\t\t\t\t\t? tr::lng_context_add_member_tag(tr::now)\n\t\t\t\t\t\t: tr::lng_context_edit_member_tag(tr::now)),\n\t\t\t\t\t[=] {\n\t\t\t\t\t\tcontroller->show(Box(\n\t\t\t\t\t\t\tEditCustomRankBox,\n\t\t\t\t\t\t\tcontroller->uiShow(),\n\t\t\t\t\t\t\tgroupPeer,\n\t\t\t\t\t\t\tuser,\n\t\t\t\t\t\t\tcurrentRank,\n\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\tnullptr));\n\t\t\t\t\t},\n\t\t\t\t\t&st::menuIconEdit);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid AddSenderUserpicModerateAction(\n\t\tnot_null<SessionController*> controller,\n\t\tHistoryItem *moderateItem,\n\t\tconst PeerMenuCallback &addAction) {\n\tconst auto moderateChannel = moderateItem\n\t\t? moderateItem->history()->peer->asChannel()\n\t\t: nullptr;\n\tconst auto moderateFrom = moderateItem\n\t\t? moderateItem->from().get()\n\t\t: nullptr;\n\tconst auto moderateUser = moderateFrom\n\t\t? moderateFrom->asUser()\n\t\t: nullptr;\n\tconst auto canDeleteAndBan = moderateItem\n\t\t&& moderateChannel\n\t\t&& moderateChannel->isMegagroup()\n\t\t&& moderateFrom\n\t\t&& (!moderateUser || !moderateChannel->isGroupAdmin(moderateUser))\n\t\t&& moderateItem->suggestBanReport()\n\t\t&& moderateItem->suggestDeleteAllReport()\n\t\t&& CanCreateModerateMessagesBox(\n\t\t\tHistoryItemsList{ not_null<HistoryItem*>(moderateItem) });\n\tif (canDeleteAndBan) {\n\t\taddAction({ .isSeparator = true });\n\t\taddAction({\n\t\t\t.text = tr::lng_context_delete_and_ban(tr::now),\n\t\t\t.handler = [=] {\n\t\t\t\tcontroller->show(Box(\n\t\t\t\t\tCreateModerateMessagesBox,\n\t\t\t\t\tHistoryItemsList{ not_null<HistoryItem*>(moderateItem) },\n\t\t\t\t\tnullptr,\n\t\t\t\t\tModerateMessagesBoxOptions{\n\t\t\t\t\t\t.reportSpam = true,\n\t\t\t\t\t\t.deleteAll = true,\n\t\t\t\t\t\t.banUser = true,\n\t\t\t\t\t}));\n\t\t\t},\n\t\t\t.icon = &st::menuIconBlockAttention,\n\t\t\t.isAttention = true,\n\t\t});\n\t}\n}\n\nbool IsUnreadThread(not_null<Data::Thread*> thread) {\n\treturn thread->chatListBadgesState().unread;\n}\n\nvoid MarkAsReadThread(not_null<Data::Thread*> thread) {\n\tconst auto readHistory = [&](not_null<History*> history) {\n\t\thistory->owner().histories().readInbox(history);\n\t};\n\tif (!IsUnreadThread(thread)) {\n\t\treturn;\n\t} else if (const auto forum = thread->asForum()) {\n\t\tforum->enumerateTopics([](not_null<Data::ForumTopic*> topic) {\n\t\t\tMarkAsReadThread(topic);\n\t\t});\n\t} else if (const auto history = thread->asHistory()) {\n\t\treadHistory(history);\n\t\tif (const auto migrated = history->migrateSibling()) {\n\t\t\treadHistory(migrated);\n\t\t}\n\t} else if (const auto topic = thread->asTopic()) {\n\t\ttopic->readTillEnd();\n\t} else if (const auto sublist = thread->asSublist()) {\n\t\tsublist->readTillEnd();\n\t}\n}\n\nvoid AddSeparatorAndShiftUp(const PeerMenuCallback &addAction) {\n\taddAction({\n\t\t.separatorSt = &st::popupMenuExpandedSeparator.menu.separator,\n\t});\n\n\tconst auto &st = st::popupMenuExpandedSeparator.menu;\n\tconst auto shift = st::popupMenuExpandedSeparator.scrollPadding.top()\n\t\t+ st.itemPadding.top()\n\t\t+ st.itemStyle.font->height\n\t\t+ st.itemPadding.bottom()\n\t\t+ st.separator.padding.top()\n\t\t+ st.separator.width / 2;\n\taddAction({ .addTopShift = -shift });\n}\n\nvoid TogglePinnedThread(\n\t\tnot_null<Window::SessionController*> controller,\n\t\tnot_null<Dialogs::Entry*> entry,\n\t\tFilterId filterId,\n\t\tFn<void()> onToggled) {\n\tif (!filterId) {\n\t\treturn TogglePinnedThread(controller, entry, onToggled);\n\t}\n\tconst auto history = entry->asHistory();\n\tif (!history) {\n\t\treturn;\n\t}\n\tconst auto owner = &history->owner();\n\n\t// This can happen when you remove this filter from another client.\n\tif (!ranges::contains(\n\t\t\t(&owner->session())->data().chatsFilters().list(),\n\t\t\tfilterId,\n\t\t\t&Data::ChatFilter::id)) {\n\t\tcontroller->showToast(tr::lng_cant_do_this(tr::now));\n\t\treturn;\n\t}\n\n\tconst auto isPinned = !history->isPinnedDialog(filterId);\n\tif (isPinned && PinnedLimitReached(controller, history, filterId)) {\n\t\treturn;\n\t}\n\n\towner->setChatPinned(history, filterId, isPinned);\n\tApi::SaveNewFilterPinned(&owner->session(), filterId);\n\tif (isPinned) {\n\t\tcontroller->content()->dialogsToUp();\n\t\tif (onToggled) {\n\t\t\tonToggled();\n\t\t}\n\t}\n}\n\nbool IsArchived(not_null<History*> history) {\n\treturn (history->folder() != nullptr);\n}\n\nbool CanArchive(History *history, PeerData *peer) {\n\tif (history && history->useTopPromotion()) {\n\t\treturn false;\n\t} else if (peer && (peer->isNotificationsUser() || peer->isSelf())) {\n\t\tif (!history || !history->folder()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nvoid PeerMenuConfirmToggleFee(\n\t\tnot_null<Window::SessionNavigation*> navigation,\n\t\tstd::shared_ptr<rpl::variable<int>> paidAmount,\n\t\tnot_null<PeerData*> peer,\n\t\tnot_null<UserData*> user,\n\t\tbool removeFee) {\n\tconst auto parent = peer->isChannel() ? peer->asChannel() : nullptr;\n\tconst auto exception = [=](bool refund) {\n\t\tusing Flag = MTPaccount_ToggleNoPaidMessagesException::Flag;\n\t\tconst auto api = &user->session().api();\n\t\tapi->request(MTPaccount_ToggleNoPaidMessagesException(\n\t\t\tMTP_flags((refund ? Flag::f_refund_charged : Flag())\n\t\t\t\t| (removeFee ? Flag() : Flag::f_require_payment)\n\t\t\t\t| (parent ? Flag::f_parent_peer : Flag())),\n\t\t\t(parent ? parent->input() : MTPInputPeer()),\n\t\t\tuser->inputUser()\n\t\t)).done([=] {\n\t\t\tif (!parent) {\n\t\t\t\tuser->clearPaysPerMessage();\n\t\t\t} else if (const auto monoforum = peer->monoforum()) {\n\t\t\t\tif (const auto sublist = monoforum->sublistLoaded(user)) {\n\t\t\t\t\tsublist->toggleFeeRemoved(removeFee);\n\t\t\t\t}\n\t\t\t}\n\t\t}).send();\n\t};\n\tif (!removeFee) {\n\t\texception(false);\n\t\treturn;\n\t}\n\tnavigation->uiShow()->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tconst auto refund = std::make_shared<base::weak_qptr<Ui::Checkbox>>();\n\t\tUi::ConfirmBox(box, {\n\t\t\t.text = tr::lng_payment_refund_text(\n\t\t\t\ttr::now,\n\t\t\t\tlt_name,\n\t\t\t\ttr::bold(user->shortName()),\n\t\t\t\ttr::marked),\n\t\t\t.confirmed = [=](Fn<void()> close) {\n\t\t\t\texception(*refund && (*refund)->checked());\n\t\t\t\tclose();\n\t\t\t},\n\t\t\t.confirmText = tr::lng_payment_refund_confirm(tr::now),\n\t\t\t.title = tr::lng_payment_refund_title(tr::now),\n\t\t});\n\t\tconst auto paid = box->lifetime().make_state<\n\t\t\trpl::variable<int>\n\t\t>();\n\t\t*paid = paidAmount->value();\n\t\tpaid->value() | rpl::on_next([=](int already) {\n\t\t\tif (!already) {\n\t\t\t\tdelete base::take(*refund).get();\n\t\t\t} else if (!*refund) {\n\t\t\t\tconst auto skip = st::defaultCheckbox.margin.top();\n\t\t\t\t*refund = box->addRow(\n\t\t\t\t\tobject_ptr<Ui::Checkbox>(\n\t\t\t\t\t\tbox,\n\t\t\t\t\t\ttr::lng_payment_refund_also(\n\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\tpaid->value() | tr::to_count()),\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\tst::defaultCheckbox),\n\t\t\t\t\tst::boxRowPadding + QMargins(0, skip, 0, skip));\n\t\t\t}\n\t\t}, box->lifetime());\n\n\t\tusing Flag = MTPaccount_GetPaidMessagesRevenue::Flag;\n\t\tuser->session().api().request(MTPaccount_GetPaidMessagesRevenue(\n\t\t\tMTP_flags(parent ? Flag::f_parent_peer : Flag()),\n\t\t\tparent ? parent->input() : MTPInputPeer(),\n\t\t\tuser->inputUser()\n\t\t)).done([=](const MTPaccount_PaidMessagesRevenue &result) {\n\t\t\t*paidAmount = result.data().vstars_amount().v;\n\t\t}).send();\n\t}));\n}\n\nvoid ForwardToSelf(\n\t\tstd::shared_ptr<Main::SessionShow> show,\n\t\tconst Data::ForwardDraft &draft) {\n\tconst auto session = &show->session();\n\tconst auto history = session->data().history(session->user());\n\tauto resolved = history->resolveForwardDraft(draft);\n\tif (!resolved.items.empty()) {\n\t\tconst auto count = resolved.items.size();\n\t\tauto action = Api::SendAction(history);\n\t\taction.clearDraft = false;\n\t\taction.generateLocal = false;\n\t\tsession->api().forwardMessages(\n\t\t\tstd::move(resolved),\n\t\t\taction,\n\t\t\t[=] {\n\t\t\t\tauto phrase = rpl::variable<TextWithEntities>(\n\t\t\t\t\tChatHelpers::ForwardedMessagePhrase({\n\t\t\t\t\t.toCount = 1,\n\t\t\t\t\t.singleMessage = (count == 1),\n\t\t\t\t\t.to1 = session->user(),\n\t\t\t\t})).current();\n\t\t\t\tif (!phrase.empty()) {\n\t\t\t\t\tshow->showToast(std::move(phrase));\n\t\t\t\t}\n\t\t\t});\n\t}\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_peer_menu.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"api/api_common.h\"\n#include \"base/object_ptr.h\"\n#include \"menu/menu_send.h\"\n#include \"data/data_poll.h\"\n#include \"ui/widgets/menu/menu_add_action_callback.h\"\n\nclass History;\nclass HistoryItem;\n\nnamespace Api {\nstruct SendOptions;\n} // namespace Api\n\nnamespace Ui {\nclass RpWidget;\nclass BoxContent;\nclass GenericBox;\nclass Show;\n} // namespace Ui\n\nnamespace Data {\nclass Forum;\nclass Folder;\nclass Session;\nstruct ForwardDraft;\nclass ForumTopic;\nclass SavedMessages;\nclass SavedSublist;\nclass Thread;\n} // namespace Data\n\nnamespace Dialogs {\nclass MainList;\nstruct EntryState;\nstruct UnreadState;\nclass Key;\nclass Entry;\n} // namespace Dialogs\n\nnamespace ChatHelpers {\nclass Show;\n} // namespace ChatHelpers\n\nnamespace InlineBots {\nenum class PeerType : uint8;\nusing PeerTypes = base::flags<PeerType>;\n} // namespace InlineBots\n\nnamespace Main {\nclass SessionShow;\n} // namespace Main\n\nnamespace Window {\n\nclass Controller;\nclass SessionController;\nclass SessionNavigation;\n\nextern const char kOptionViewProfileInChatsListContextMenu[];\n\nusing PeerMenuCallback = Ui::Menu::MenuCallback;\n\nvoid FillDialogsEntryMenu(\n\tnot_null<SessionController*> controller,\n\tDialogs::EntryState request,\n\tconst PeerMenuCallback &addAction);\nbool FillVideoChatMenu(\n\tnot_null<SessionController*> controller,\n\tDialogs::EntryState request,\n\tconst PeerMenuCallback &addAction);\n\nvoid FillSenderUserpicMenu(\n\tnot_null<SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\tPeerData *groupPeer,\n\tUi::InputField *fieldForMention,\n\tDialogs::Key searchInEntry,\n\tconst PeerMenuCallback &addAction);\n\nvoid AddSenderUserpicModerateAction(\n\tnot_null<SessionController*> controller,\n\tHistoryItem *moderateItem,\n\tconst PeerMenuCallback &addAction);\n\nvoid MenuAddMarkAsReadAllChatsAction(\n\tnot_null<Main::Session*> session,\n\tstd::shared_ptr<Ui::Show> show,\n\tconst PeerMenuCallback &addAction);\n\nvoid MenuAddMarkAsReadChatListAction(\n\tnot_null<Window::SessionController*> controller,\n\tFn<not_null<Dialogs::MainList*>()> &&list,\n\tconst PeerMenuCallback &addAction,\n\tFn<Dialogs::UnreadState()> customUnreadState = nullptr);\n\nvoid PeerMenuExportChat(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer);\nvoid PeerMenuExportTopic(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tMsgId topicRootId);\nvoid PeerMenuDeleteContact(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<UserData*> user);\nvoid PeerMenuShareContactBox(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<UserData*> user);\nvoid PeerMenuAddChannelMembers(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<ChannelData*> channel);\nvoid PeerMenuCreatePoll(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\tFullReplyTo replyTo = FullReplyTo(),\n\tSuggestOptions suggest = SuggestOptions(),\n\tPollData::Flags chosen = kDefaultPollCreateFlags,\n\tPollData::Flags disabled = PollData::Flags(),\n\tApi::SendType sendType = Api::SendType::Normal,\n\tSendMenu::Details sendMenuDetails = SendMenu::Details());\nenum class TodoWantsPremium {\n\tCreate,\n\tAdd,\n\tMark,\n};\nvoid PeerMenuTodoWantsPremium(TodoWantsPremium type);\nvoid PeerMenuCreateTodoList(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer,\n\tFullReplyTo replyTo = FullReplyTo(),\n\tSuggestOptions suggest = SuggestOptions(),\n\tApi::SendType sendType = Api::SendType::Normal,\n\tSendMenu::Details sendMenuDetails = SendMenu::Details());\nvoid PeerMenuEditTodoList(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<HistoryItem*> item);\n[[nodiscard]] bool PeerMenuShowAddTodoListTasks(not_null<HistoryItem*> item);\nvoid PeerMenuAddTodoListTasks(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<HistoryItem*> item);\nvoid PeerMenuDeleteTopicWithConfirmation(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<Data::ForumTopic*> topic);\nvoid PeerMenuDeleteTopic(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<Data::ForumTopic*> topic);\n\nstruct ClearChat {\n};\nstruct ClearReply {\n\tFullMsgId replyId;\n};\nvoid PeerMenuBlockUserBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::Controller*> window,\n\tnot_null<PeerData*> peer,\n\tstd::variant<v::null_t, bool> suggestReport,\n\tstd::variant<v::null_t, ClearChat, ClearReply> suggestClear);\nvoid PeerMenuUnblockUserWithBotRestart(\n\tstd::shared_ptr<Ui::Show> show,\n\tnot_null<UserData*> user);\n\nvoid BlockSenderFromRepliesBox(\n\tnot_null<Ui::GenericBox*> box,\n\tnot_null<Window::SessionController*> controller,\n\tFullMsgId id);\n\nvoid ToggleHistoryArchived(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tnot_null<History*> history,\n\tbool archived);\nFn<void()> ClearHistoryHandler(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer);\nFn<void()> DeleteAndLeaveHandler(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<PeerData*> peer);\nFn<void()> DeleteSublistHandler(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Data::SavedSublist*> sublist);\n\nobject_ptr<Ui::BoxContent> PrepareChooseRecipientBox(\n\tnot_null<Main::Session*> session,\n\tFnMut<bool(not_null<Data::Thread*>)> &&chosen,\n\trpl::producer<QString> titleOverride = nullptr,\n\tFnMut<void()> &&successCallback = nullptr,\n\tInlineBots::PeerTypes typesRestriction = 0,\n\tFn<void(\n\t\tstd::vector<not_null<Data::Thread*>>,\n\t\tApi::SendOptions)> sendMany = nullptr);\nbase::weak_qptr<Ui::BoxContent> ShowChooseRecipientBox(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tFnMut<bool(not_null<Data::Thread*>)> &&chosen,\n\trpl::producer<QString> titleOverride = nullptr,\n\tFnMut<void()> &&successCallback = nullptr,\n\tInlineBots::PeerTypes typesRestriction = 0);\nbase::weak_qptr<Ui::BoxContent> ShowForwardMessagesBox(\n\tstd::shared_ptr<ChatHelpers::Show> show,\n\tData::ForwardDraft &&draft,\n\tFn<void()> &&successCallback = nullptr);\nbase::weak_qptr<Ui::BoxContent> ShowForwardMessagesBox(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tData::ForwardDraft &&draft,\n\tFn<void()> &&successCallback = nullptr);\nbase::weak_qptr<Ui::BoxContent> ShowForwardMessagesBox(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tMessageIdsList &&items,\n\tFn<void()> &&successCallback = nullptr);\nbase::weak_qptr<Ui::BoxContent> ShowShareUrlBox(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tconst QString &url,\n\tconst QString &text,\n\tFnMut<void()> &&successCallback = nullptr);\nbase::weak_qptr<Ui::BoxContent> ShowShareGameBox(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<UserData*> bot,\n\tQString shortName);\nbase::weak_qptr<Ui::BoxContent> ShowDropMediaBox(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tstd::shared_ptr<QMimeData> data,\n\tnot_null<Data::Forum*> forum,\n\tFnMut<void()> &&successCallback = nullptr);\nbase::weak_qptr<Ui::BoxContent> ShowDropMediaBox(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tstd::shared_ptr<QMimeData> data,\n\tnot_null<Data::SavedMessages*> monoforum,\n\tFnMut<void()> &&successCallback = nullptr);\n\nbase::weak_qptr<Ui::BoxContent> ShowSendNowMessagesBox(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<History*> history,\n\tMessageIdsList &&items,\n\tFn<void()> &&successCallback = nullptr);\n\nvoid ToggleMessagePinned(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tFullMsgId itemId,\n\tbool pin);\nvoid TogglePinnedThread(\n\tnot_null<Window::SessionController*> controller,\n\tnot_null<Dialogs::Entry*> entry,\n\tFilterId filterId,\n\tFn<void()> onToggled);\nvoid HidePinnedBar(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<PeerData*> peer,\n\tMsgId topicRootId,\n\tPeerId monoforumPeerId,\n\tFn<void()> onHidden);\nvoid UnpinAllMessages(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tnot_null<Data::Thread*> thread);\n\n[[nodiscard]] bool IsUnreadThread(not_null<Data::Thread*> thread);\nvoid MarkAsReadThread(not_null<Data::Thread*> thread);\n\nvoid AddSeparatorAndShiftUp(const PeerMenuCallback &addAction);\n\n[[nodiscard]] bool IsArchived(not_null<History*> history);\n[[nodiscard]] bool CanArchive(History *history, PeerData *peer);\n\nvoid PeerMenuConfirmToggleFee(\n\tnot_null<Window::SessionNavigation*> navigation,\n\tstd::shared_ptr<rpl::variable<int>> paidAmount,\n\tnot_null<PeerData*> peer,\n\tnot_null<UserData*> user,\n\tbool removeFee);\n\nvoid ForwardToSelf(\n\tstd::shared_ptr<Main::SessionShow> show,\n\tconst Data::ForwardDraft &draft);\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_section_common.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace Window {\n\nenum class SectionActionResult {\n\tHandle, // Handle an action and stay in the current section.\n\tFallback, // Ignore an action and fallback to the HistoryWidget.\n\tIgnore, // Ignore an action and stay in the current section.\n};\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_separate_id.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_separate_id.h\"\n\n#include \"data/data_channel.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_peer.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_session.h\"\n#include \"data/data_thread.h\"\n#include \"history/history.h\"\n#include \"main/main_account.h\"\n#include \"main/main_session.h\"\n\nnamespace Window {\n\nSeparateId::SeparateId(std::nullptr_t) {\n}\n\nSeparateId::SeparateId(not_null<Main::Account*> account)\n: account(account) {\n}\n\nSeparateId::SeparateId(SeparateType type, not_null<Main::Session*> session)\n: type(type)\n, account(&session->account()) {\n}\n\nSeparateId::SeparateId(SeparateType type, not_null<Data::Thread*> thread)\n: type(type)\n, account(&thread->session().account())\n, thread(thread) {\n}\n\nSeparateId::SeparateId(not_null<Data::Thread*> thread)\n: SeparateId(SeparateType::Chat, thread) {\n}\n\nSeparateId::SeparateId(not_null<PeerData*> peer)\n: SeparateId(SeparateType::Chat, peer->owner().history(peer)) {\n}\n\nSeparateId::SeparateId(\n\tnot_null<Data::Thread*> thread,\n\tStorage::SharedMediaType sharedMediaType)\n: type(SeparateType::SharedMedia)\n, sharedMediaType(sharedMediaType)\n, account(&thread->session().account())\n, thread(thread) {\n}\n\nbool SeparateId::primary() const {\n\treturn (type == SeparateType::Primary);\n}\n\nData::Thread *SeparateId::chat() const {\n\treturn (type == SeparateType::Chat) ? thread : nullptr;\n}\n\nData::Forum *SeparateId::forum() const {\n\treturn (type == SeparateType::Forum) ? thread->asForum() : nullptr;\n}\n\nData::Folder *SeparateId::folder() const {\n\treturn (type == SeparateType::Archive)\n\t\t? account->session().data().folder(Data::Folder::kId).get()\n\t\t: nullptr;\n}\n\nData::SavedSublist *SeparateId::sublist() const {\n\treturn (type != SeparateType::SavedSublist)\n\t\t? nullptr\n\t\t: thread->asSublist();\n}\n\nbool SeparateId::hasChatsList() const {\n\treturn (type == SeparateType::Primary)\n\t\t|| (type == SeparateType::Archive)\n\t\t|| (type == SeparateType::Forum);\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_separate_id.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nclass ChannelData;\nclass PeerData;\n\nnamespace Data {\nclass Thread;\nclass Folder;\nclass Forum;\nclass SavedSublist;\n} // namespace Data\n\nnamespace Main {\nclass Account;\nclass Session;\n} // namespace Main\n\nnamespace Storage {\nenum class SharedMediaType : signed char;\n} // namespace Storage\n\nnamespace Window {\n\nenum class SeparateType {\n\tPrimary,\n\tArchive,\n\tChat,\n\tForum,\n\tSavedSublist,\n\tSharedMedia,\n};\n\nstruct SeparateSharedMedia {\n\tnot_null<Data::Thread*> thread;\n\tStorage::SharedMediaType type = {};\n};\n\nstruct SeparateId {\n\tSeparateId(std::nullptr_t);\n\tSeparateId(not_null<Main::Account*> account);\n\tSeparateId(SeparateType type, not_null<Main::Session*> session);\n\tSeparateId(SeparateType type, not_null<Data::Thread*> thread);\n\tSeparateId(not_null<Data::Thread*> thread);\n\tSeparateId(not_null<PeerData*> peer);\n\tSeparateId(\n\t\tnot_null<Data::Thread*> thread,\n\t\tStorage::SharedMediaType sharedMediaType);\n\n\tSeparateType type = SeparateType::Primary;\n\tStorage::SharedMediaType sharedMediaType = {};\n\tMain::Account *account = nullptr;\n\tData::Thread *thread = nullptr; // For types except Main and Archive.\n\t[[nodiscard]] bool valid() const {\n\t\treturn account != nullptr;\n\t}\n\texplicit operator bool() const {\n\t\treturn valid();\n\t}\n\n\t[[nodiscard]] bool primary() const;\n\t[[nodiscard]] Data::Thread *chat() const;\n\t[[nodiscard]] Data::Forum *forum() const;\n\t[[nodiscard]] Data::Folder *folder() const;\n\t[[nodiscard]] Data::SavedSublist *sublist() const;\n\n\t[[nodiscard]] bool hasChatsList() const;\n\n\tfriend inline auto operator<=>(\n\t\tconst SeparateId &,\n\t\tconst SeparateId &) = default;\n\tfriend inline bool operator==(\n\t\tconst SeparateId &,\n\t\tconst SeparateId &) = default;\n};\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_session_controller.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_session_controller.h\"\n\n#include \"apiwrap.h\"\n#include \"api/api_cloud_password.h\"\n#include \"api/api_text_entities.h\"\n#include \"boxes/peers/add_bot_to_chat_box.h\"\n#include \"boxes/peers/edit_peer_info_box.h\"\n#include \"boxes/peers/replace_boost_box.h\"\n#include \"boxes/add_contact_box.h\"\n#include \"boxes/delete_messages_box.h\"\n#include \"boxes/star_gift_auction_box.h\"\n#include \"window/window_chat_preview.h\"\n#include \"window/window_chat_switch_process.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_filters_menu.h\"\n#include \"window/window_separate_id.h\"\n#include \"info/channel_statistics/earn/info_channel_earn_list.h\"\n#include \"info/peer_gifts/info_peer_gifts_widget.h\"\n#include \"info/stories/info_stories_widget.h\"\n#include \"info/info_memento.h\"\n#include \"info/info_controller.h\"\n#include \"inline_bots/bot_attach_web_view.h\"\n#include \"history/history.h\"\n#include \"history/history_item.h\"\n#include \"history/view/reactions/history_view_reactions.h\"\n//#include \"history/view/reactions/history_view_reactions_button.h\"\n#include \"history/view/history_view_chat_section.h\"\n#include \"history/view/history_view_scheduled_section.h\"\n#include \"history/view/history_view_subsection_tabs.h\"\n#include \"media/player/media_player_instance.h\"\n#include \"media/view/media_view_open_common.h\"\n#include \"data/components/gift_auctions.h\"\n#include \"data/components/recent_peers.h\"\n#include \"data/stickers/data_custom_emoji.h\"\n#include \"data/data_document_resolver.h\"\n#include \"data/data_download_manager.h\"\n#include \"data/data_saved_messages.h\"\n#include \"data/data_saved_sublist.h\"\n#include \"data/data_session.h\"\n#include \"data/data_thread.h\"\n#include \"data/data_file_origin.h\"\n#include \"data/data_flags.h\"\n#include \"data/data_folder.h\"\n#include \"data/data_channel.h\"\n#include \"data/data_chat.h\"\n#include \"data/data_user.h\"\n#include \"data/data_document.h\"\n#include \"data/data_document_media.h\"\n#include \"data/data_file_click_handler.h\"\n#include \"data/data_photo_media.h\"\n#include \"data/data_changes.h\"\n#include \"data/data_group_call.h\"\n#include \"data/data_forum.h\"\n#include \"data/data_forum_topic.h\"\n#include \"data/data_chat_filters.h\"\n#include \"data/data_replies_list.h\"\n#include \"data/data_peer_values.h\"\n#include \"data/data_premium_limits.h\"\n#include \"data/data_web_page.h\"\n#include \"data/data_search_calendar.h\"\n#include \"dialogs/ui/chat_search_in.h\"\n#include \"passport/passport_form_controller.h\"\n#include \"chat_helpers/tabbed_selector.h\"\n#include \"chat_helpers/emoji_interactions.h\"\n#include \"core/shortcuts.h\"\n#include \"core/application.h\"\n#include \"core/click_handler_types.h\"\n#include \"core/file_utilities.h\"\n#include \"core/ui_integration.h\"\n#include \"base/options.h\"\n#include \"base/unixtime.h\"\n#include \"info/channel_statistics/earn/earn_icons.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/text/text_utilities.h\"\n#include \"ui/text/format_values.h\" // Ui::FormatPhone.\n#include \"ui/delayed_activation.h\"\n#include \"ui/boxes/boost_box.h\"\n#include \"ui/chat/chat_style.h\"\n#include \"ui/chat/chat_theme.h\"\n#include \"ui/effects/message_sending_animation_controller.h\"\n#include \"ui/style/style_palette_colorizer.h\"\n#include \"ui/toast/toast.h\"\n#include \"calls/calls_instance.h\" // Core::App().calls().inCall().\n#include \"calls/group/calls_group_call.h\"\n#include \"calls/group/calls_group_common.h\"\n#include \"calls/group/calls_group_invite_controller.h\"\n#include \"ui/boxes/calendar_box.h\"\n#include \"ui/boxes/collectible_info_box.h\"\n#include \"ui/boxes/confirm_box.h\"\n#include \"ui/dynamic_thumbnails.h\"\n#include \"ui/ui_utility.h\"\n#include \"mainwidget.h\"\n#include \"main/main_app_config.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"main/main_session_settings.h\"\n#include \"lang/lang_keys.h\"\n#include \"apiwrap.h\"\n#include \"api/api_chat_invite.h\"\n#include \"api/api_global_privacy.h\"\n#include \"api/api_blocked_peers.h\"\n#include \"api/api_premium.h\"\n#include \"boxes/star_gift_craft_box.h\"\n#include \"support/support_helper.h\"\n#include \"storage/file_upload.h\"\n#include \"storage/download_manager_mtproto.h\"\n#include \"storage/storage_account.h\"\n#include \"window/themes/window_theme.h\"\n#include \"window/window_peer_menu.h\"\n#include \"window/window_session_controller_link_info.h\"\n#include \"settings/cloud_password/settings_cloud_password_input.h\"\n#include \"settings/cloud_password/settings_cloud_password_start.h\"\n#include \"settings/cloud_password/settings_cloud_password_email_confirm.h\"\n#include \"settings/sections/settings_main.h\"\n#include \"styles/style_chat.h\"\n#include \"settings/sections/settings_premium.h\"\n#include \"settings/sections/settings_privacy_security.h\"\n#include \"styles/style_window.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_dialogs.h\"\n#include \"styles/style_layers.h\" // st::boxLabel\n\nnamespace Window {\nnamespace {\n\nconstexpr auto kCustomThemesInMemory = 5;\nconstexpr auto kMaxChatEntryHistorySize = 50;\n\nbase::options::toggle OptionExternalMediaViewer({\n\t.id = kOptionExternalMediaViewer,\n\t.name = \"External media viewer\",\n\t.description = \"Use system media viewer instead of the internal one.\",\n});\n\nclass MainWindowShow final : public ChatHelpers::Show {\npublic:\n\texplicit MainWindowShow(not_null<SessionController*> controller);\n\n\tvoid activate() override;\n\n\tvoid showOrHideBoxOrLayer(\n\t\tstd::variant<\n\t\t\tv::null_t,\n\t\t\tobject_ptr<Ui::BoxContent>,\n\t\t\tstd::unique_ptr<Ui::LayerWidget>> &&layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) const override;\n\n\tnot_null<QWidget*> toastParent() const override;\n\tbool valid() const override;\n\toperator bool() const override;\n\n\tMain::Session &session() const override;\n\tbool paused(ChatHelpers::PauseReason reason) const override;\n\trpl::producer<> pauseChanged() const override;\n\n\trpl::producer<bool> adjustShadowLeft() const override;\n\tSendMenu::Details sendMenuDetails() const override;\n\n\tbool showMediaPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<DocumentData*> document) const override;\n\tbool showMediaPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<PhotoData*> photo) const override;\n\n\tvoid processChosenSticker(\n\t\tChatHelpers::FileChosen &&chosen) const override;\n\nprivate:\n\tconst base::weak_ptr<SessionController> _window;\n\n};\n\n[[nodiscard]] Ui::ChatThemeBubblesData PrepareBubblesData(\n\t\tconst Data::CloudTheme &theme,\n\t\tData::CloudThemeType type) {\n\tconst auto i = theme.settings.find(type);\n\treturn {\n\t\t.colors = (i != end(theme.settings)\n\t\t\t? i->second.outgoingMessagesColors\n\t\t\t: std::vector<QColor>()),\n\t\t.accent = (i != end(theme.settings)\n\t\t\t? i->second.outgoingAccentColor\n\t\t\t: std::optional<QColor>()),\n\t};\n}\n\n[[nodiscard]] bool DownloadingDocument(not_null<DocumentData*> document) {\n\tfor (const auto id : Core::App().downloadManager().loadingList()) {\n\t\tif (id->object.document == document.get()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n[[nodiscard]] Ui::CollectibleInfo Parse(\n\t\tconst QString &entity,\n\t\tnot_null<PeerData*> owner,\n\t\tconst MTPfragment_CollectibleInfo &info) {\n\tconst auto &data = info.data();\n\treturn {\n\t\t.entity = entity,\n\t\t.copyText = (entity.startsWith('+')\n\t\t\t? QString()\n\t\t\t: owner->session().createInternalLinkFull(entity)),\n\t\t.ownerUserpic = Ui::MakeUserpicThumbnail(owner, true),\n\t\t.ownerName = owner->name(),\n\t\t.cryptoAmount = data.vcrypto_amount().v,\n\t\t.amount = data.vamount().v,\n\t\t.cryptoCurrency = qs(data.vcrypto_currency()),\n\t\t.currency = qs(data.vcurrency()),\n\t\t.url = qs(data.vurl()),\n\t\t.date = data.vpurchase_date().v,\n\t};\n}\n\nMainWindowShow::MainWindowShow(not_null<SessionController*> controller)\n: _window(base::make_weak(controller)) {\n}\n\nvoid MainWindowShow::activate() {\n\tif (const auto window = _window.get()) {\n\t\tWindow::ActivateWindow(window);\n\t}\n}\n\nvoid MainWindowShow::showOrHideBoxOrLayer(\n\t\tstd::variant<\n\t\t\tv::null_t,\n\t\t\tobject_ptr<Ui::BoxContent>,\n\t\t\tstd::unique_ptr<Ui::LayerWidget>> &&layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) const {\n\tif (const auto window = _window.get()) {\n\t\twindow->window().widget()->showOrHideBoxOrLayer(\n\t\t\tstd::move(layer),\n\t\t\toptions,\n\t\t\tanimated);\n\t}\n}\n\nnot_null<QWidget*> MainWindowShow::toastParent() const {\n\tconst auto window = _window.get();\n\tAssert(window != nullptr);\n\treturn window->widget()->bodyWidget();\n}\n\nbool MainWindowShow::valid() const {\n\treturn !_window.empty();\n}\n\nMainWindowShow::operator bool() const {\n\treturn valid();\n}\n\nMain::Session &MainWindowShow::session() const {\n\tconst auto window = _window.get();\n\tAssert(window != nullptr);\n\treturn window->session();\n}\n\nbool MainWindowShow::paused(ChatHelpers::PauseReason reason) const {\n\tconst auto window = _window.get();\n\treturn window && window->isGifPausedAtLeastFor(reason);\n}\n\nrpl::producer<> MainWindowShow::pauseChanged() const {\n\tconst auto window = _window.get();\n\tif (!window) {\n\t\treturn rpl::never<>();\n\t}\n\treturn window->gifPauseLevelChanged();\n}\n\nrpl::producer<bool> MainWindowShow::adjustShadowLeft() const {\n\tconst auto window = _window.get();\n\tif (!window) {\n\t\treturn rpl::single(false);\n\t}\n\treturn window->adaptive().value(\n\t) | rpl::map([=] {\n\t\treturn !window->adaptive().isOneColumn();\n\t});\n}\n\nSendMenu::Details MainWindowShow::sendMenuDetails() const {\n\tconst auto window = _window.get();\n\tif (!window) {\n\t\treturn SendMenu::Details();\n\t}\n\treturn window->content()->sendMenuDetails();\n}\n\nbool MainWindowShow::showMediaPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<DocumentData*> document) const {\n\tconst auto window = _window.get();\n\treturn window && window->widget()->showMediaPreview(origin, document);\n}\n\nbool MainWindowShow::showMediaPreview(\n\t\tData::FileOrigin origin,\n\t\tnot_null<PhotoData*> photo) const {\n\tconst auto window = _window.get();\n\treturn window && window->widget()->showMediaPreview(origin, photo);\n}\n\nvoid MainWindowShow::processChosenSticker(\n\t\tChatHelpers::FileChosen &&chosen) const {\n\tif (const auto window = _window.get()) {\n\t\tUi::PostponeCall(window, [=, chosen = std::move(chosen)]() mutable {\n\t\t\twindow->stickerOrEmojiChosen(std::move(chosen));\n\t\t});\n\t}\n}\n\n} // namespace\n\nconst char kOptionExternalMediaViewer[] = \"external-media-viewer\";\n\nvoid ActivateWindow(not_null<SessionController*> controller) {\n\tUi::ActivateWindow(controller->widget());\n}\n\nbool IsPaused(\n\t\tnot_null<SessionController*> controller,\n\t\tGifPauseReason level) {\n\treturn controller->isGifPausedAtLeastFor(level);\n}\n\nFn<bool()> PausedIn(\n\t\tnot_null<SessionController*> controller,\n\t\tGifPauseReason level) {\n\treturn [=] { return IsPaused(controller, level); };\n}\n\nbool operator==(const PeerThemeOverride &a, const PeerThemeOverride &b) {\n\treturn (a.peer == b.peer) && (a.theme == b.theme);\n}\n\nbool operator!=(const PeerThemeOverride &a, const PeerThemeOverride &b) {\n\treturn !(a == b);\n}\n\nDateClickHandler::DateClickHandler(Dialogs::Key chat, QDate date)\n: _chat(chat)\n, _weak(chat.topic())\n, _date(date) {\n}\n\nvoid DateClickHandler::setDate(QDate date) {\n\t_date = date;\n}\n\nvoid DateClickHandler::onClick(ClickContext context) const {\n\tconst auto my = context.other.value<ClickHandlerContext>();\n\tif (const auto window = my.sessionWindow.get()) {\n\t\tif (!_chat.topic()) {\n\t\t\twindow->showCalendar({ _chat, _date, true, true });\n\t\t} else if (const auto strong = _weak.get()) {\n\t\t\twindow->showCalendar({ strong, _date, true, true });\n\t\t}\n\t}\n}\n\nForumThreadClickHandler::ForumThreadClickHandler(not_null<HistoryItem*> item)\n: _thread(resolveThread(item)) {\n}\n\nvoid ForumThreadClickHandler::update(not_null<HistoryItem*> item) {\n\t_thread = resolveThread(item);\n}\n\nvoid ForumThreadClickHandler::onClick(ClickContext context) const {\n\tconst auto my = context.other.value<ClickHandlerContext>();\n\tif (const auto window = my.sessionWindow.get()) {\n\t\tif (const auto strong = _thread.get()) {\n\t\t\twindow->showThread(strong, 0, SectionShow::Way::ClearStack);\n\t\t}\n\t}\n}\n\nbase::weak_ptr<Data::Thread> ForumThreadClickHandler::resolveThread(\n\t\tnot_null<HistoryItem*> item) const {\n\tif (const auto sublist = item->savedSublist()) {\n\t\treturn sublist;\n\t} else if (const auto topic = item->topic()) {\n\t\treturn topic;\n\t}\n\treturn nullptr;\n}\n\nMessageHighlightId SearchHighlightId(const QString &query) {\n\tauto result = MessageHighlightId{ .quote = { query } };\n\tif (!result.quote.empty()) {\n\t\tresult.quoteOffset = kSearchQueryOffsetHint;\n\t}\n\treturn result;\n}\n\nSessionNavigation::SessionNavigation(not_null<Main::Session*> session)\n: _session(session)\n, _api(&_session->mtp()) {\n}\n\nSessionNavigation::~SessionNavigation() = default;\n\nMain::Session &SessionNavigation::session() const {\n\treturn *_session;\n}\n\nbool SessionNavigation::showFrozenError() {\n\treturn uiShow()->showFrozenError();\n}\n\nvoid SessionNavigation::showPeerByLink(const PeerByLinkInfo &info) {\n\tCore::App().hideMediaView();\n\tif (!info.phone.isEmpty()) {\n\t\tresolvePhone(info.phone, [=](not_null<PeerData*> peer) {\n\t\t\tshowPeerByLinkResolved(peer, info);\n\t\t});\n\t} else if (!info.chatLinkSlug.isEmpty()) {\n\t\tresolveChatLink(info.chatLinkSlug, [=](\n\t\t\t\tnot_null<PeerData*> peer,\n\t\t\t\tTextWithEntities draft) {\n\t\t\tData::SetChatLinkDraft(peer, draft);\n\t\t\tshowPeerByLinkResolved(peer, info);\n\t\t});\n\t} else if (const auto name = std::get_if<QString>(&info.usernameOrId)) {\n\t\tresolveUsername(*name, [=](not_null<PeerData*> peer) {\n\t\t\tif (info.startAutoSubmit) {\n\t\t\t\tpeer->session().api().blockedPeers().unblock(\n\t\t\t\t\tpeer,\n\t\t\t\t\t[=](bool) { showPeerByLinkResolved(peer, info); },\n\t\t\t\t\ttrue);\n\t\t\t} else if (info.joinChannel && peer->isChannel()) {\n\t\t\t\tpeer->session().api().joinChannel(peer->asChannel());\n\t\t\t} else {\n\t\t\t\tshowPeerByLinkResolved(peer, info);\n\t\t\t}\n\t\t}, info.referral);\n\t} else if (const auto id = std::get_if<ChannelId>(&info.usernameOrId)) {\n\t\tresolveChannelById(*id, [=](not_null<ChannelData*> channel) {\n\t\t\tshowPeerByLinkResolved(channel, info);\n\t\t});\n\t}\n}\n\nvoid SessionNavigation::fullInfoLoadedHook(not_null<PeerData*> peer) {\n\tif (!_waitingDirectChannel || _waitingDirectChannel != peer) {\n\t\treturn;\n\t}\n\t_waitingDirectChannel = nullptr;\n\tconst auto monoforum = peer->broadcastMonoforum();\n\tconst auto open = monoforum ? monoforum : peer.get();\n\tshowPeerHistory(open, SectionShow::Way::Forward, ShowAtUnreadMsgId);\n}\n\nvoid SessionNavigation::resolvePhone(\n\t\tconst QString &phone,\n\t\tFn<void(not_null<PeerData*>)> done) {\n\tif (const auto peer = _session->data().userByPhone(phone)) {\n\t\tdone(peer);\n\t\treturn;\n\t}\n\t_api.request(base::take(_resolveRequestId)).cancel();\n\t_resolveRequestId = _api.request(MTPcontacts_ResolvePhone(\n\t\tMTP_string(phone)\n\t)).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\tresolveDone(result, done);\n\t}).fail([=](const MTP::Error &error) {\n\t\t_resolveRequestId = 0;\n\t\tif (error.code() == 400) {\n\t\t\tparentController()->show(\n\t\t\t\tUi::MakeInformBox(tr::lng_username_by_phone_not_found(\n\t\t\t\t\ttr::now,\n\t\t\t\t\tlt_phone,\n\t\t\t\t\tUi::FormatPhone(phone))),\n\t\t\t\tUi::LayerOption::CloseOther);\n\t\t}\n\t}).send();\n}\n\nvoid SessionNavigation::resolveChatLink(\n\t\tconst QString &slug,\n\t\tFn<void(not_null<PeerData*> peer, TextWithEntities draft)> done) {\n\t_api.request(base::take(_resolveRequestId)).cancel();\n\t_resolveRequestId = _api.request(MTPaccount_ResolveBusinessChatLink(\n\t\tMTP_string(slug)\n\t)).done([=](const MTPaccount_ResolvedBusinessChatLinks &result) {\n\t\t_resolveRequestId = 0;\n\t\tparentController()->hideLayer();\n\t\tconst auto &data = result.data();\n\t\t_session->data().processUsers(data.vusers());\n\t\t_session->data().processChats(data.vchats());\n\n\t\tusing namespace Api;\n\t\tconst auto peerId = peerFromMTP(data.vpeer());\n\t\tdone(_session->data().peer(peerId), {\n\t\t\tqs(data.vmessage()),\n\t\t\tEntitiesFromMTP(_session, data.ventities().value_or_empty())\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\t_resolveRequestId = 0;\n\t\tif (error.code() == 400) {\n\t\t\tshowToast(tr::lng_confirm_phone_link_invalid(tr::now));\n\t\t}\n\t}).send();\n}\n\nvoid SessionNavigation::resolveUsername(\n\t\tconst QString &username,\n\t\tFn<void(not_null<PeerData*>)> done,\n\t\tconst QString &referral) {\n\tif (referral.isEmpty()) {\n\t\tif (const auto peer = _session->data().peerByUsername(username)) {\n\t\t\tdone(peer);\n\t\t\treturn;\n\t\t}\n\t}\n\t_api.request(base::take(_resolveRequestId)).cancel();\n\tusing Flag = MTPcontacts_ResolveUsername::Flag;\n\t_resolveRequestId = _api.request(MTPcontacts_ResolveUsername(\n\t\tMTP_flags(referral.isEmpty() ? Flag() : Flag::f_referer),\n\t\tMTP_string(username),\n\t\tMTP_string(referral)\n\t)).done([=](const MTPcontacts_ResolvedPeer &result) {\n\t\tresolveDone(result, done);\n\t}).fail([=](const MTP::Error &error) {\n\t\t_resolveRequestId = 0;\n\t\tif (error.type() == u\"STARREF_EXPIRED\"_q) {\n\t\t\tparentController()->showToast(tr::lng_star_ref_stopped(tr::now));\n\t\t} else if (error.code() == 400) {\n\t\t\tparentController()->show(\n\t\t\t\tUi::MakeInformBox(\n\t\t\t\t\ttr::lng_username_not_found(tr::now, lt_user, username)),\n\t\t\t\tUi::LayerOption::CloseOther);\n\t\t}\n\t}).send();\n}\n\nvoid SessionNavigation::resolveDone(\n\t\tconst MTPcontacts_ResolvedPeer &result,\n\t\tFn<void(not_null<PeerData*>)> done) {\n\t_resolveRequestId = 0;\n\tparentController()->hideLayer();\n\tresult.match([&](const MTPDcontacts_resolvedPeer &data) {\n\t\t_session->data().processUsers(data.vusers());\n\t\t_session->data().processChats(data.vchats());\n\t\tif (const auto peerId = peerFromMTP(data.vpeer())) {\n\t\t\tdone(_session->data().peer(peerId));\n\t\t}\n\t});\n}\n\nvoid SessionNavigation::resolveChannelById(\n\t\tChannelId channelId,\n\t\tFn<void(not_null<ChannelData*>)> done) {\n\tif (const auto channel = _session->data().channelLoaded(channelId)) {\n\t\tdone(channel);\n\t\treturn;\n\t}\n\tconst auto fail = crl::guard(this, [=] {\n\t\tuiShow()->showToast(tr::lng_error_post_link_invalid(tr::now));\n\t});\n\t_api.request(base::take(_resolveRequestId)).cancel();\n\t_resolveRequestId = _api.request(MTPchannels_GetChannels(\n\t\tMTP_vector<MTPInputChannel>(\n\t\t\t1,\n\t\t\tMTP_inputChannel(MTP_long(channelId.bare), MTP_long(0)))\n\t)).done([=](const MTPmessages_Chats &result) {\n\t\tresult.match([&](const auto &data) {\n\t\t\tconst auto peer = _session->data().processChats(data.vchats());\n\t\t\tif (peer && peer->id == peerFromChannel(channelId)) {\n\t\t\t\tdone(peer->asChannel());\n\t\t\t} else {\n\t\t\t\tfail();\n\t\t\t}\n\t\t});\n\t}).fail(fail).send();\n}\n\nvoid SessionNavigation::showMessageByLinkResolved(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst PeerByLinkInfo &info) {\n\tauto params = SectionShow{\n\t\tSectionShow::Way::Forward\n\t};\n\tparams.origin = SectionShow::OriginMessage{\n\t\tinfo.clickFromMessageId\n\t};\n\tparams.highlight.pollOption = info.pollOption;\n\tconst auto peer = item->history()->peer;\n\tconst auto topicId = peer->isForum() ? item->topicRootId() : 0;\n\tif (topicId) {\n\t\tconst auto messageId = (item->id == topicId) ? MsgId() : item->id;\n\t\tshowRepliesForMessage(item->history(), topicId, messageId, params);\n\t} else {\n\t\tshowPeerHistory(peer, params, item->id);\n\t}\n}\n\nvoid SessionNavigation::showPeerByLinkResolved(\n\t\tnot_null<PeerData*> peer,\n\t\tconst PeerByLinkInfo &info) {\n\tauto params = SectionShow{\n\t\tSectionShow::Way::Forward\n\t};\n\tparams.origin = SectionShow::OriginMessage{\n\t\tinfo.clickFromMessageId\n\t};\n\tparams.highlight.pollOption = info.pollOption;\n\tif (info.voicechatHash && peer->isChannel()) {\n\t\t// First show the channel itself.\n\t\tcrl::on_main(this, [=] {\n\t\t\tshowPeerHistory(peer, params, ShowAtUnreadMsgId);\n\t\t});\n\n\t\t// Then try to join the voice chat.\n\t\tjoinVoiceChatFromLink(peer, info);\n\t\treturn;\n\t}\n\tusing Scope = AddBotToGroupBoxController::Scope;\n\tconst auto user = peer->asUser();\n\tconst auto bot = (user && user->isBot()) ? user : nullptr;\n\tconst auto applyBotStartToken = [&] {\n\t\tif (bot && bot->botInfo->startToken != info.startToken) {\n\t\t\tbot->botInfo->startToken = info.startToken;\n\t\t\tbot->session().changes().peerUpdated(\n\t\t\t\tbot,\n\t\t\t\tData::PeerUpdate::Flag::BotStartToken);\n\t\t}\n\t};\n\n\t// t.me/username/012345 - we thought it was a channel post link, but\n\t// after resolving the username we found out it is a bot.\n\tconst auto resolveType = (bot\n\t\t&& !info.botAppName.isEmpty()\n\t\t&& info.resolveType == ResolveType::Default)\n\t\t? ResolveType::BotApp\n\t\t: info.resolveType;\n\n\t// Show specific posts only in channels / supergroups.\n\tconst auto useRequestedMessageId = peer->isChannel();\n\tconst auto msgId = useRequestedMessageId\n\t\t? info.messageId\n\t\t: info.startAutoSubmit\n\t\t? ShowAndStartBotMsgId\n\t\t: (bot && !info.startToken.isEmpty())\n\t\t? ShowAndMaybeStartBotMsgId\n\t\t: ShowAtUnreadMsgId;\n\n\tconst auto &replies = info.repliesInfo;\n\tif (const auto threadId = std::get_if<ThreadId>(&replies)) {\n\t\tconst auto history = peer->owner().history(peer);\n\t\tconst auto controller = parentController();\n\t\tif (const auto forum = peer->forum()) {\n\t\t\tif (controller->windowId().hasChatsList()\n\t\t\t\t&& !controller->adaptive().isOneColumn()\n\t\t\t\t&& controller->shownForum().current() != forum\n\t\t\t\t&& !forum->peer()->useSubsectionTabs()) {\n\t\t\t\tcontroller->showForum(forum);\n\t\t\t}\n\t\t}\n\t\tshowRepliesForMessage(history, threadId->id, msgId, params);\n\t} else if (const auto commentId = std::get_if<CommentId>(&replies)) {\n\t\tconst auto history = peer->owner().history(peer);\n\t\tshowRepliesForMessage(history, msgId, commentId->id, params);\n\t} else if (resolveType == ResolveType::Profile) {\n\t\tshowPeerInfo(peer, params);\n\t} else if (resolveType == ResolveType::HashtagSearch) {\n\t\tsearchMessages(info.text, peer->owner().history(peer));\n\t} else if (info.storyParam == u\"live\"_q) {\n\t\tparentController()->openPeerStories(peer->id, std::nullopt, true);\n\t} else if (const auto storyId = info.storyParam.toInt()) {\n\t\tconst auto id = FullStoryId{ peer->id, storyId };\n\t\tconst auto context = (info.storyAlbumId > 0)\n\t\t\t? Data::StoriesContext{ Data::StoriesContextAlbum{\n\t\t\t\tinfo.storyAlbumId,\n\t\t\t} }\n\t\t\t: Data::StoriesContext{ Data::StoriesContextSingle() };\n\t\tpeer->owner().stories().resolve(id, crl::guard(this, [=] {\n\t\t\tif (peer->owner().stories().lookup(id)) {\n\t\t\t\tparentController()->openPeerStory(peer, id.story, context);\n\t\t\t} else {\n\t\t\t\tshowToast(tr::lng_stories_link_invalid(tr::now));\n\t\t\t}\n\t\t}));\n\t} else if (info.storyAlbumId > 0) {\n\t\tshowSection(Info::Stories::Make(peer, info.storyAlbumId));\n\t} else if (info.giftCollectionId > 0) {\n\t\tshowSection(Info::PeerGifts::Make(peer, info.giftCollectionId));\n\t} else if (bot && resolveType == ResolveType::BotApp) {\n\t\tconst auto itemId = info.clickFromMessageId;\n\t\tconst auto item = _session->data().message(itemId);\n\t\tconst auto contextPeer = item\n\t\t\t? item->history()->peer\n\t\t\t: bot;\n\t\tconst auto action = info.clickFromBotWebviewContext\n\t\t\t? info.clickFromBotWebviewContext->action\n\t\t\t: Api::SendAction(bot->owner().history(contextPeer));\n\t\tcrl::on_main(this, [=] {\n\t\t\tbot->session().attachWebView().open({\n\t\t\t\t.bot = bot,\n\t\t\t\t.context = {\n\t\t\t\t\t.controller = parentController(),\n\t\t\t\t\t.action = action,\n\t\t\t\t\t.fullscreen = info.botAppFullScreen,\n\t\t\t\t\t.maySkipConfirmation = !info.botAppForceConfirmation,\n\t\t\t\t},\n\t\t\t\t.button = { .startCommand = info.startToken },\n\t\t\t\t.source = InlineBots::WebViewSourceLinkApp{\n\t\t\t\t\t.appname = info.botAppName,\n\t\t\t\t\t.token = info.startToken,\n\t\t\t\t},\n\t\t\t});\n\t\t});\n\t} else if (bot && resolveType == ResolveType::ShareGame) {\n\t\tWindow::ShowShareGameBox(parentController(), bot, info.startToken);\n\t} else if (bot\n\t\t&& (resolveType == ResolveType::AddToGroup\n\t\t\t|| resolveType == ResolveType::AddToChannel)) {\n\t\tconst auto scope = (resolveType == ResolveType::AddToGroup)\n\t\t\t? (info.startAdminRights ? Scope::GroupAdmin : Scope::All)\n\t\t\t: (resolveType == ResolveType::AddToChannel)\n\t\t\t? Scope::ChannelAdmin\n\t\t\t: Scope::None;\n\t\tAssert(scope != Scope::None);\n\n\t\tAddBotToGroupBoxController::Start(\n\t\t\tparentController(),\n\t\t\tbot,\n\t\t\tscope,\n\t\t\tinfo.startToken,\n\t\t\tinfo.startAdminRights);\n\t} else if (resolveType == ResolveType::Boost && peer->isChannel()) {\n\t\tresolveBoostState(peer->asChannel());\n\t} else if (peer->isForum()) {\n\t\tif (!msgId || !useRequestedMessageId) {\n\t\t\tapplyBotStartToken();\n\t\t\tparentController()->showForum(peer->forum(), params, msgId);\n\t\t} else if (const auto item = peer->owner().message(peer, msgId)) {\n\t\t\tshowMessageByLinkResolved(item, info);\n\t\t} else {\n\t\t\tconst auto callback = crl::guard(this, [=] {\n\t\t\t\tif (const auto item = peer->owner().message(peer, msgId)) {\n\t\t\t\t\tshowMessageByLinkResolved(item, info);\n\t\t\t\t} else {\n\t\t\t\t\tshowPeerHistory(peer, params, msgId);\n\t\t\t\t}\n\t\t\t});\n\t\t\tpeer->session().api().requestMessageData(peer, msgId, callback);\n\t\t}\n\t} else if (resolveType == ResolveType::Mention) {\n\t\tif (bot || peer->isChannel()) {\n\t\t\tcrl::on_main(this, [=] {\n\t\t\t\tshowPeerHistory(peer, params);\n\t\t\t});\n\t\t} else {\n\t\t\tshowPeerInfo(peer, params);\n\t\t}\n\t} else if (resolveType == ResolveType::ChannelDirect\n\t\t&& !peer->isFullLoaded()) {\n\t\t_waitingDirectChannel = peer;\n\t\tpeer->updateFull();\n\t} else if (const auto monoforum = peer->broadcastMonoforum()\n\t\t; monoforum && resolveType == ResolveType::ChannelDirect) {\n\t\tshowPeerHistory(monoforum, params, ShowAtUnreadMsgId);\n\t} else {\n\t\tconst auto attachBotUsername = info.attachBotUsername;\n\t\tapplyBotStartToken();\n\t\tif (!attachBotUsername.isEmpty()) {\n\t\t\tcrl::on_main(this, [=] {\n\t\t\t\tconst auto history = peer->owner().history(peer);\n\t\t\t\tshowPeerHistory(history, params, msgId);\n\n\t\t\t\tpeer->session().attachWebView().openByUsername(\n\t\t\t\t\tparentController(),\n\t\t\t\t\tApi::SendAction(history),\n\t\t\t\t\tattachBotUsername,\n\t\t\t\t\tinfo.attachBotToggleCommand.value_or(QString()),\n\t\t\t\t\tinfo.botAppFullScreen);\n\t\t\t});\n\t\t} else if (bot && info.attachBotMainOpen) {\n\t\t\tconst auto startCommand = info.attachBotToggleCommand.value_or(\n\t\t\t\tQString());\n\t\t\tbot->session().attachWebView().open({\n\t\t\t\t.bot = bot,\n\t\t\t\t.context = {\n\t\t\t\t\t.controller = parentController(),\n\t\t\t\t\t.fullscreen = info.botAppFullScreen,\n\t\t\t\t},\n\t\t\t\t.button = { .startCommand = startCommand },\n\t\t\t\t.source = InlineBots::WebViewSourceLinkBotProfile{\n\t\t\t\t\t.token = startCommand,\n\t\t\t\t\t.compact = info.attachBotMainCompact,\n\t\t\t\t},\n\t\t\t});\n\t\t} else if (bot && info.attachBotToggleCommand) {\n\t\t\tconst auto itemId = info.clickFromMessageId;\n\t\t\tconst auto item = _session->data().message(itemId);\n\t\t\tconst auto contextPeer = item\n\t\t\t\t? item->history()->peer.get()\n\t\t\t\t: nullptr;\n\t\t\tconst auto contextUser = contextPeer\n\t\t\t\t? contextPeer->asUser()\n\t\t\t\t: nullptr;\n\t\t\tbot->session().attachWebView().open({\n\t\t\t\t.bot = bot,\n\t\t\t\t.context = {\n\t\t\t\t\t.controller = parentController(),\n\t\t\t\t\t.action = (contextUser\n\t\t\t\t\t\t? Api::SendAction(\n\t\t\t\t\t\t\tcontextUser->owner().history(contextUser))\n\t\t\t\t\t\t: std::optional<Api::SendAction>()),\n\t\t\t\t\t.fullscreen = info.botAppFullScreen,\n\t\t\t\t},\n\t\t\t\t.button = { .startCommand = *info.attachBotToggleCommand },\n\t\t\t\t.source = InlineBots::WebViewSourceLinkAttachMenu{\n\t\t\t\t\t.choose = info.attachBotChooseTypes,\n\t\t\t\t\t.token = *info.attachBotToggleCommand,\n\t\t\t\t},\n\t\t\t});\n\t\t} else {\n\t\t\tconst auto draft = info.text;\n\t\t\tconst auto historyInNewWindow = info.historyInNewWindow;\n\t\t\tparams.videoTimestamp = info.videoTimestamp;\n\t\t\tcrl::on_main(this, [=] {\n\t\t\t\tif (peer->isUser() && !draft.isEmpty()) {\n\t\t\t\t\tData::SetChatLinkDraft(peer, { draft });\n\t\t\t\t}\n\t\t\t\tif (historyInNewWindow) {\n\t\t\t\t\tconst auto window\n\t\t\t\t\t\t= Core::App().ensureSeparateWindowFor(peer);\n\t\t\t\t\tconst auto controller = window\n\t\t\t\t\t\t? window->sessionController()\n\t\t\t\t\t\t: nullptr;\n\t\t\t\t\tif (controller) {\n\t\t\t\t\t\tcontroller->showPeerHistory(peer, params, msgId);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tshowPeerHistory(peer, params, msgId);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tshowPeerHistory(peer, params, msgId);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n}\n\nvoid SessionNavigation::resolveBoostState(\n\t\tnot_null<ChannelData*> channel,\n\t\tint boostsToLift) {\n\t_boostsToLift = boostsToLift;\n\tif (_boostStateResolving == channel) {\n\t\treturn;\n\t}\n\t_boostStateResolving = channel;\n\t_api.request(MTPpremium_GetBoostsStatus(\n\t\tchannel->input()\n\t)).done([=](const MTPpremium_BoostsStatus &result) {\n\t\tif (base::take(_boostStateResolving) != channel) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto boosted = std::make_shared<bool>();\n\t\tchannel->updateLevelHint(result.data().vlevel().v);\n\t\tconst auto submit = [=](Fn<void(Ui::BoostCounters)> done) {\n\t\t\tapplyBoost(channel, [=](Ui::BoostCounters counters) {\n\t\t\t\t*boosted = true;\n\t\t\t\tdone(counters);\n\t\t\t});\n\t\t};\n\t\tconst auto lifting = base::take(_boostsToLift);\n\t\tconst auto box = uiShow()->show(Box(Ui::BoostBox, Ui::BoostBoxData{\n\t\t\t.name = channel->name(),\n\t\t\t.boost = ParseBoostCounters(result),\n\t\t\t.features = LookupBoostFeatures(channel),\n\t\t\t.lifting = lifting,\n\t\t\t.allowMulti = (BoostsForGift(_session) > 0),\n\t\t\t.group = channel->isMegagroup(),\n\t\t}, submit));\n\t\tif (lifting) {\n\t\t\tbox->boxClosing() | rpl::on_next([=] {\n\t\t\t\tif (*boosted) {\n\t\t\t\t\tchannel->updateFullForced();\n\t\t\t\t}\n\t\t\t}, box->lifetime());\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\t_boostStateResolving = nullptr;\n\t\tshowToast(u\"Error: \"_q + error.type());\n\t}).send();\n}\n\nvoid SessionNavigation::resolveCollectible(\n\t\tPeerId ownerId,\n\t\tconst QString &entity,\n\t\tFn<void(QString)> fail) {\n\tif (_collectibleEntity == entity) {\n\t\treturn;\n\t}\n\t_collectibleEntity = entity;\n\t_api.request(base::take(_collectibleRequestId)).cancel();\n\t_collectibleRequestId = _api.request(MTPfragment_GetCollectibleInfo(\n\t\t((Ui::DetectCollectibleType(entity) == Ui::CollectibleType::Phone)\n\t\t\t? MTP_inputCollectiblePhone(MTP_string(entity))\n\t\t\t: MTP_inputCollectibleUsername(MTP_string(entity)))\n\t)).done([=](const MTPfragment_CollectibleInfo &result) {\n\t\tconst auto entity = base::take(_collectibleEntity);\n\t\t_collectibleRequestId = 0;\n\t\tuiShow()->show(Box(\n\t\t\tUi::CollectibleInfoBox,\n\t\t\tParse(entity, _session->data().peer(ownerId), result)));\n\t}).fail([=](const MTP::Error &error) {\n\t\t_collectibleEntity = QString();\n\t\t_collectibleRequestId = 0;\n\t\tif (fail) {\n\t\t\tfail(error.type());\n\t\t}\n\t}).send();\n}\n\nvoid SessionNavigation::resolveConferenceCall(\n\t\tQString slug,\n\t\tFullMsgId contextId) {\n\tresolveConferenceCall(std::move(slug), 0, contextId);\n}\n\nvoid SessionNavigation::resolveConferenceCall(\n\t\tMsgId inviteMsgId,\n\t\tFullMsgId contextId) {\n\tresolveConferenceCall({}, inviteMsgId, contextId);\n}\n\n[[nodiscard]] std::vector<not_null<UserData*>> ExtractParticipantsForInvite(\n\t\tHistoryItem *item) {\n\tif (!item) {\n\t\treturn {};\n\t}\n\tauto result = std::vector<not_null<UserData*>>();\n\tconst auto add = [&](not_null<PeerData*> peer) {\n\t\tif (const auto user = peer->asUser()) {\n\t\t\tif (!user->isSelf()\n\t\t\t\t&& !ranges::contains(result, not_null(user))) {\n\t\t\t\tresult.push_back(user);\n\t\t\t}\n\t\t}\n\t};\n\tadd(item->from());\n\tconst auto media = item->media();\n\tif (const auto call = media ? media->call() : nullptr) {\n\t\tfor (const auto &peer : call->otherParticipants) {\n\t\t\tadd(peer);\n\t\t}\n\t}\n\treturn result;\n}\n\nvoid SessionNavigation::resolveConferenceCall(\n\t\tQString slug,\n\t\tMsgId inviteMsgId,\n\t\tFullMsgId contextId) {\n\t_conferenceCallResolveContextId = contextId;\n\tif (_conferenceCallSlug == slug\n\t\t&& _conferenceCallInviteMsgId == inviteMsgId) {\n\t\treturn;\n\t}\n\t_api.request(base::take(_conferenceCallRequestId)).cancel();\n\t_conferenceCallSlug = slug;\n\t_conferenceCallInviteMsgId = inviteMsgId;\n\n\tconst auto limit = 5;\n\t_conferenceCallRequestId = _api.request(MTPphone_GetGroupCall(\n\t\t(inviteMsgId\n\t\t\t? MTP_inputGroupCallInviteMessage(MTP_int(inviteMsgId.bare))\n\t\t\t: MTP_inputGroupCallSlug(MTP_string(slug))),\n\t\tMTP_int(limit)\n\t)).done([=](const MTPphone_GroupCall &result) {\n\t\t_conferenceCallRequestId = 0;\n\t\tconst auto slug = base::take(_conferenceCallSlug);\n\t\tconst auto inviteMsgId = base::take(_conferenceCallInviteMsgId);\n\t\tconst auto contextId = base::take(_conferenceCallResolveContextId);\n\t\tconst auto context = session().data().message(contextId);\n\t\tresult.data().vcall().match([&](const MTPDgroupCall &data) {\n\t\t\tconst auto call = session().data().sharedConferenceCall(\n\t\t\t\tdata.vid().v,\n\t\t\t\tdata.vaccess_hash().v);\n\t\t\tcall->processFullCall(result);\n\t\t\tconst auto join = [=](Fn<void()> close) {\n\t\t\t\tconst auto &appConfig = call->session().appConfig();\n\t\t\t\tconst auto conferenceLimit = appConfig.confcallSizeLimit();\n\t\t\t\tif (call->fullCount() >= conferenceLimit) {\n\t\t\t\t\tshowToast(tr::lng_confcall_participants_limit(tr::now));\n\t\t\t\t} else {\n\t\t\t\t\t//parentController()->window().openInMediaView(\n\t\t\t\t\t//\tMedia::View::OpenRequest(\n\t\t\t\t\t//\t\tparentController(),\n\t\t\t\t\t//\t\tcall,\n\t\t\t\t\t//\t\tslug,\n\t\t\t\t\t//\t\tinviteMsgId));\n\t\t\t\t\t//AssertIsDebug();\n\t\t\t\t\tCore::App().calls().startOrJoinConferenceCall({\n\t\t\t\t\t\t.call = call,\n\t\t\t\t\t\t.linkSlug = slug,\n\t\t\t\t\t\t.joinMessageId = inviteMsgId,\n\t\t\t\t\t});\n\t\t\t\t\tclose();\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst auto inviter = context\n\t\t\t\t? context->from()->asUser()\n\t\t\t\t: nullptr;\n\t\t\tif (inviteMsgId && call->participants().empty()) {\n\t\t\t\tuiShow()->show(Calls::Group::PrepareInviteToEmptyBox(\n\t\t\t\t\tcall,\n\t\t\t\t\tinviteMsgId,\n\t\t\t\t\tExtractParticipantsForInvite(context)));\n\t\t\t} else {\n\t\t\t\tuiShow()->show(Box(\n\t\t\t\t\tCalls::Group::ConferenceCallJoinConfirm,\n\t\t\t\t\tcall,\n\t\t\t\t\t(inviter && !inviter->isSelf()) ? inviter : nullptr,\n\t\t\t\t\tjoin));\n\t\t\t}\n\t\t}, [&](const MTPDgroupCallDiscarded &data) {\n\t\t\tif (inviteMsgId) {\n\t\t\t\tuiShow()->show(\n\t\t\t\t\tCalls::Group::PrepareCreateCallBox(\n\t\t\t\t\t\tparentController(),\n\t\t\t\t\t\tnullptr,\n\t\t\t\t\t\tinviteMsgId,\n\t\t\t\t\t\tExtractParticipantsForInvite(context)));\n\t\t\t} else {\n\t\t\t\tshowToast(tr::lng_confcall_link_inactive(tr::now));\n\t\t\t}\n\t\t});\n\t}).fail([=] {\n\t\t_conferenceCallRequestId = 0;\n\t\t_conferenceCallSlug = QString();\n\t\tconst auto contextId = base::take(_conferenceCallResolveContextId);\n\t\tconst auto context = session().data().message(contextId);\n\t\tconst auto inviteMsgId = base::take(_conferenceCallInviteMsgId);\n\t\tif (inviteMsgId) {\n\t\t\tuiShow()->show(\n\t\t\t\tCalls::Group::PrepareCreateCallBox(\n\t\t\t\t\tparentController(),\n\t\t\t\t\tnullptr,\n\t\t\t\t\tinviteMsgId,\n\t\t\t\t\tExtractParticipantsForInvite(context)));\n\t\t} else {\n\t\t\tshowToast(tr::lng_confcall_link_inactive(tr::now));\n\t\t}\n\t}).send();\n}\n\nvoid SessionNavigation::applyBoost(\n\t\tnot_null<ChannelData*> channel,\n\t\tFn<void(Ui::BoostCounters)> done) {\n\t_api.request(MTPpremium_GetMyBoosts(\n\t)).done([=](const MTPpremium_MyBoosts &result) {\n\t\tconst auto &data = result.data();\n\t\t_session->data().processUsers(data.vusers());\n\t\t_session->data().processChats(data.vchats());\n\t\tconst auto slots = ParseForChannelBoostSlots(\n\t\t\tchannel,\n\t\t\tdata.vmy_boosts().v);\n\t\tif (!slots.free.empty()) {\n\t\t\tapplyBoostsChecked(channel, { slots.free.front() }, done);\n\t\t} else if (slots.other.empty()) {\n\t\t\tif (!slots.already.empty()) {\n\t\t\t\tif (const auto receive = BoostsForGift(_session)) {\n\t\t\t\t\tconst auto again = true;\n\t\t\t\t\tconst auto name = channel->name();\n\t\t\t\t\tuiShow()->show(\n\t\t\t\t\t\tBox(Ui::GiftForBoostsBox, name, receive, again));\n\t\t\t\t} else {\n\t\t\t\t\tuiShow()->show(\n\t\t\t\t\t\tBox(Ui::BoostBoxAlready, channel->isMegagroup()));\n\t\t\t\t}\n\t\t\t} else if (!_session->premium()) {\n\t\t\t\tconst auto group = channel->isMegagroup();\n\t\t\t\tuiShow()->show(Box(Ui::PremiumForBoostsBox, group, [=] {\n\t\t\t\t\tconst auto id = peerToChannel(channel->id).bare;\n\t\t\t\t\tSettings::ShowPremium(\n\t\t\t\t\t\tparentController(),\n\t\t\t\t\t\t\"channel_boost__\" + QString::number(id));\n\t\t\t\t}));\n\t\t\t} else if (const auto receive = BoostsForGift(_session)) {\n\t\t\t\tconst auto again = false;\n\t\t\t\tconst auto name = channel->name();\n\t\t\t\tuiShow()->show(\n\t\t\t\t\tBox(Ui::GiftForBoostsBox, name, receive, again));\n\t\t\t} else {\n\t\t\t\tuiShow()->show(\n\t\t\t\t\tBox(Ui::GiftedNoBoostsBox, channel->isMegagroup()));\n\t\t\t}\n\t\t\tdone({});\n\t\t} else {\n\t\t\tconst auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();\n\t\t\tconst auto reassign = [=](\n\t\t\t\t\tstd::vector<int> slots,\n\t\t\t\t\tint groups,\n\t\t\t\t\tint channels) {\n\t\t\t\tconst auto count = int(slots.size());\n\t\t\t\tconst auto callback = [=](Ui::BoostCounters counters) {\n\t\t\t\t\tif (const auto strong = weak->get()) {\n\t\t\t\t\t\tstrong->closeBox();\n\t\t\t\t\t}\n\t\t\t\t\tdone(counters);\n\t\t\t\t\tuiShow()->showToast(tr::lng_boost_reassign_done(\n\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\tcount,\n\t\t\t\t\t\tlt_channels,\n\t\t\t\t\t\t(!groups\n\t\t\t\t\t\t\t? tr::lng_boost_reassign_channels\n\t\t\t\t\t\t\t: !channels\n\t\t\t\t\t\t\t? tr::lng_boost_reassign_groups\n\t\t\t\t\t\t\t: tr::lng_boost_reassign_mixed)(\n\t\t\t\t\t\t\t\t\ttr::now,\n\t\t\t\t\t\t\t\t\tlt_count,\n\t\t\t\t\t\t\t\t\tgroups + channels)));\n\t\t\t\t};\n\t\t\t\tapplyBoostsChecked(\n\t\t\t\t\tchannel,\n\t\t\t\t\tslots,\n\t\t\t\t\tcrl::guard(this, callback));\n\t\t\t};\n\t\t\t*weak = uiShow()->show(ReassignBoostsBox(\n\t\t\t\tchannel,\n\t\t\t\tslots.other,\n\t\t\t\treassign,\n\t\t\t\t[=] { done({}); }));\n\t\t}\n\t}).fail([=](const MTP::Error &error) {\n\t\tconst auto type = error.type();\n\t\tshowToast(u\"Error: \"_q + type);\n\t\tdone({});\n\t}).handleFloodErrors().send();\n}\n\nvoid SessionNavigation::applyBoostsChecked(\n\t\tnot_null<ChannelData*> channel,\n\t\tstd::vector<int> slots,\n\t\tFn<void(Ui::BoostCounters)> done) {\n\tauto mtp = MTP_vector_from_range(ranges::views::all(\n\t\tslots\n\t) | ranges::views::transform([](int slot) {\n\t\treturn MTP_int(slot);\n\t}));\n\t_api.request(MTPpremium_ApplyBoost(\n\t\tMTP_flags(MTPpremium_ApplyBoost::Flag::f_slots),\n\t\tstd::move(mtp),\n\t\tchannel->input()\n\t)).done([=](const MTPpremium_MyBoosts &result) {\n\t\t_api.request(MTPpremium_GetBoostsStatus(\n\t\t\tchannel->input()\n\t\t)).done([=](const MTPpremium_BoostsStatus &result) {\n\t\t\tchannel->updateLevelHint(result.data().vlevel().v);\n\t\t\tdone(ParseBoostCounters(result));\n\t\t}).fail([=](const MTP::Error &error) {\n\t\t\tshowToast(u\"Error: \"_q + error.type());\n\t\t\tdone({});\n\t\t}).send();\n\t}).fail([=](const MTP::Error &error) {\n\t\tshowToast(u\"Error: \"_q + error.type());\n\t\tdone({});\n\t}).send();\n}\n\nvoid SessionNavigation::joinVoiceChatFromLink(\n\t\tnot_null<PeerData*> peer,\n\t\tconst PeerByLinkInfo &info) {\n\tExpects(info.voicechatHash.has_value());\n\n\tconst auto bad = crl::guard(this, [=] {\n\t\tuiShow()->showToast(tr::lng_group_invite_bad_link(tr::now));\n\t});\n\tconst auto hash = *info.voicechatHash;\n\t_api.request(base::take(_resolveRequestId)).cancel();\n\t_resolveRequestId = _api.request(\n\t\tMTPchannels_GetFullChannel(peer->asChannel()->inputChannel())\n\t).done([=](const MTPmessages_ChatFull &result) {\n\t\t_session->api().processFullPeer(peer, result);\n\t\tconst auto call = peer->groupCall();\n\t\tif (!call) {\n\t\t\tbad();\n\t\t\treturn;\n\t\t}\n\t\tconst auto join = [=] {\n\t\t\tparentController()->startOrJoinGroupCall(\n\t\t\t\tpeer,\n\t\t\t\t{ hash, Calls::StartGroupCallArgs::JoinConfirm::Always });\n\t\t};\n\t\tif (call->loaded()) {\n\t\t\tjoin();\n\t\t\treturn;\n\t\t}\n\t\tconst auto id = call->id();\n\t\tconst auto limit = 5;\n\t\t_resolveRequestId = _api.request(\n\t\t\tMTPphone_GetGroupCall(call->input(), MTP_int(limit))\n\t\t).done([=](const MTPphone_GroupCall &result) {\n\t\t\tif (const auto now = peer->groupCall(); now && now->id() == id) {\n\t\t\t\tif (!now->loaded()) {\n\t\t\t\t\tnow->processFullCall(result);\n\t\t\t\t}\n\t\t\t\tjoin();\n\t\t\t} else {\n\t\t\t\tbad();\n\t\t\t}\n\t\t}).fail(bad).send();\n\t}).send();\n}\n\nvoid SessionNavigation::showRepliesForMessage(\n\t\tnot_null<History*> history,\n\t\tMsgId rootId,\n\t\tMsgId commentId,\n\t\tconst SectionShow &params) {\n\tif (const auto topic = history->peer->forumTopicFor(rootId)) {\n\t\tauto replies = topic->replies();\n\t\tif (replies->unreadCountKnown()) {\n\t\t\tusing namespace HistoryView;\n\t\t\tauto memento = std::make_shared<ChatMemento>(\n\t\t\t\tChatViewId{\n\t\t\t\t\t.history = history,\n\t\t\t\t\t.repliesRootId = rootId,\n\t\t\t\t},\n\t\t\t\tcommentId,\n\t\t\t\tparams.highlight);\n\t\t\tmemento->setFromTopic(topic);\n\t\t\tshowSection(std::move(memento), params);\n\t\t\treturn;\n\t\t}\n\t}\n\tif (_showingRepliesRequestId\n\t\t&& _showingRepliesHistory == history.get()\n\t\t&& _showingRepliesRootId == rootId) {\n\t\treturn;\n\t} else if (!history->peer->asChannel()) {\n\t\t// HistoryView::ChatWidget replies right now handles only channels.\n\t\treturn;\n\t}\n\t_api.request(base::take(_showingRepliesRequestId)).cancel();\n\n\tconst auto postPeer = history->peer;\n\t_showingRepliesHistory = history;\n\t_showingRepliesRootId = rootId;\n\t_showingRepliesRequestId = _api.request(\n\t\tMTPmessages_GetDiscussionMessage(\n\t\t\thistory->peer->input(),\n\t\t\tMTP_int(rootId))\n\t).done([=](const MTPmessages_DiscussionMessage &result) {\n\t\t_showingRepliesRequestId = 0;\n\t\tresult.match([&](const MTPDmessages_discussionMessage &data) {\n\t\t\t_session->data().processUsers(data.vusers());\n\t\t\t_session->data().processChats(data.vchats());\n\t\t\t_session->data().processMessages(\n\t\t\t\tdata.vmessages(),\n\t\t\t\tNewMessageType::Existing);\n\t\t\tconst auto list = data.vmessages().v;\n\t\t\tconst auto deleted = list.isEmpty();\n\t\t\tconst auto comments = history->peer->isBroadcast();\n\t\t\tif (comments && deleted) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst auto id = deleted ? rootId : IdFromMessage(list.front());\n\t\t\tconst auto peer = deleted\n\t\t\t\t? history->peer->id\n\t\t\t\t: PeerFromMessage(list.front());\n\t\t\tif (!peer || !id) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto item = deleted\n\t\t\t\t? nullptr\n\t\t\t\t: _session->data().message(peer, id);\n\t\t\tif (comments && !item) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauto &groups = _session->data().groups();\n\t\t\tif (const auto group = item ? groups.find(item) : nullptr) {\n\t\t\t\titem = group->items.front();\n\t\t\t}\n\t\t\tif (comments) {\n\t\t\t\tconst auto post = _session->data().message(postPeer, rootId);\n\t\t\t\tif (post) {\n\t\t\t\t\tpost->setCommentsItemId(item->fullId());\n\t\t\t\t\tif (const auto maxId = data.vmax_id()) {\n\t\t\t\t\t\tpost->setCommentsMaxId(maxId->v);\n\t\t\t\t\t}\n\t\t\t\t\tpost->setCommentsInboxReadTill(\n\t\t\t\t\t\tdata.vread_inbox_max_id().value_or_empty());\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (deleted || item) {\n\t\t\t\tusing namespace HistoryView;\n\t\t\t\tauto memento = item\n\t\t\t\t\t? std::make_shared<ChatMemento>(\n\t\t\t\t\t\tChatMemento::Comments(),\n\t\t\t\t\t\titem,\n\t\t\t\t\t\tcommentId)\n\t\t\t\t\t: std::make_shared<ChatMemento>(ChatViewId{\n\t\t\t\t\t\t.history = history,\n\t\t\t\t\t\t.repliesRootId = rootId,\n\t\t\t\t\t}, commentId);\n\t\t\t\tmemento->setReadInformation(\n\t\t\t\t\tdata.vread_inbox_max_id().value_or_empty(),\n\t\t\t\t\tdata.vunread_count().v,\n\t\t\t\t\tdata.vread_outbox_max_id().value_or_empty());\n\t\t\t\tshowSection(std::move(memento), params);\n\t\t\t}\n\t\t});\n\t}).fail([=](const MTP::Error &error) {\n\t\t_showingRepliesRequestId = 0;\n\t\tif (error.type() == u\"CHANNEL_PRIVATE\"_q\n\t\t\t|| error.type() == u\"USER_BANNED_IN_CHANNEL\"_q) {\n\t\t\tshowToast(tr::lng_group_not_accessible(tr::now));\n\t\t} else if (error.type() == u\"MSG_ID_INVALID\"_q) {\n\t\t\tshowToast(tr::lng_message_not_found(tr::now));\n\t\t}\n\t}).send();\n}\n\nvoid SessionNavigation::showPeerInfo(\n\t\tPeerId peerId,\n\t\tconst SectionShow &params) {\n\tshowPeerInfo(_session->data().peer(peerId), params);\n}\n\nvoid SessionNavigation::showTopic(\n\t\tnot_null<Data::ForumTopic*> topic,\n\t\tMsgId itemId,\n\t\tconst SectionShow &params) {\n\treturn showRepliesForMessage(\n\t\ttopic->history(),\n\t\ttopic->rootId(),\n\t\titemId,\n\t\tparams);\n}\n\nvoid SessionNavigation::showSublist(\n\t\tnot_null<Data::SavedSublist*> sublist,\n\t\tMsgId itemId,\n\t\tconst SectionShow &params) {\n\tusing namespace HistoryView;\n\tauto memento = std::make_shared<ChatMemento>(\n\t\tChatViewId{\n\t\t\t.history = sublist->owningHistory(),\n\t\t\t.sublist = sublist,\n\t\t},\n\t\titemId,\n\t\tparams.highlight);\n\tshowSection(std::move(memento), params);\n}\n\nvoid SessionNavigation::showThread(\n\t\tnot_null<Data::Thread*> thread,\n\t\tMsgId itemId,\n\t\tconst SectionShow &params) {\n\tif (const auto topic = thread->asTopic()) {\n\t\tshowTopic(topic, itemId, params);\n\t} else if (const auto sublist = thread->asSublist()) {\n\t\tshowSublist(sublist, itemId, params);\n\t} else {\n\t\tshowPeerHistory(thread->asHistory(), params, itemId);\n\t}\n\tif (parentController()->activeChatCurrent().thread() == thread) {\n\t\tparentController()->content()->hideDragForwardInfo();\n\t}\n}\n\nvoid SessionNavigation::showPeerInfo(\n\t\tnot_null<PeerData*> peer,\n\t\tconst SectionShow &params) {\n\t//if (Adaptive::ThreeColumn()\n\t//\t&& !Core::App().settings().thirdSectionInfoEnabled()) {\n\t//\tCore::App().settings().setThirdSectionInfoEnabled(true);\n\t//\tCore::App().saveSettingsDelayed();\n\t//}\n\tshowSection(std::make_shared<Info::Memento>(peer), params);\n}\n\nvoid SessionNavigation::showPeerInfo(\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst SectionShow &params) {\n\tif (const auto topic = thread->asTopic()) {\n\t\tshowSection(std::make_shared<Info::Memento>(topic), params);\n\t} else if (const auto sublist = thread->asSublist()\n\t\t; sublist && sublist->parentChat()) {\n\t\tshowPeerInfo(sublist->sublistPeer()->id, params);\n\t} else {\n\t\tshowPeerInfo(thread->peer()->id, params);\n\t}\n}\n\nvoid SessionNavigation::showPeerHistory(\n\t\tnot_null<PeerData*> peer,\n\t\tconst SectionShow &params,\n\t\tMsgId msgId) {\n\tshowPeerHistory(peer->id, params, msgId);\n}\n\nvoid SessionNavigation::showPeerHistory(\n\t\tnot_null<History*> history,\n\t\tconst SectionShow &params,\n\t\tMsgId msgId) {\n\tshowPeerHistory(history->peer->id, params, msgId);\n}\n\nvoid SessionNavigation::showByInitialId(\n\t\tconst SectionShow &params,\n\t\tMsgId msgId) {\n\tconst auto parent = parentController();\n\tconst auto id = parent->window().id();\n\tauto instant = params;\n\tinstant.animated = anim::type::instant;\n\tswitch (id.type) {\n\tcase SeparateType::Archive:\n\t\tclearSectionStack(instant);\n\t\tparent->openFolder(id.folder());\n\t\tbreak;\n\tcase SeparateType::Forum:\n\t\tclearSectionStack(instant);\n\t\tparent->showForum(id.forum(), instant);\n\t\tbreak;\n\tcase SeparateType::Primary:\n\t\tclearSectionStack(instant);\n\t\tbreak;\n\tcase SeparateType::Chat:\n\t\tshowThread(id.thread, msgId, instant);\n\t\tbreak;\n\tcase SeparateType::SharedMedia: {\n\t\tclearSectionStack(instant);\n\t\tconst auto type = id.sharedMediaType;\n\t\tconst auto topic = id.thread->asTopic();\n\t\tshowSection(\n\t\t\t(topic\n\t\t\t\t? std::make_shared<Info::Memento>(topic, type)\n\t\t\t\t: std::make_shared<Info::Memento>(id.thread->peer(), type)),\n\t\t\tinstant);\n\t\tparent->widget()->setMaximumWidth(st::maxWidthSharedMediaWindow);\n\t\tbreak;\n\t}\n\tcase SeparateType::SavedSublist:\n\t\tusing namespace HistoryView;\n\t\tshowSection(\n\t\t\tstd::make_shared<ChatMemento>(ChatViewId{\n\t\t\t\t.history = id.sublist()->owningHistory(),\n\t\t\t\t.sublist = id.sublist(),\n\t\t\t}),\n\t\t\tinstant);\n\t\tbreak;\n\t}\n}\n\nvoid SessionNavigation::showSettings(\n\t\tSettings::Type type,\n\t\tconst SectionShow &params) {\n\tshowSection(\n\t\tstd::make_shared<Info::Memento>(\n\t\t\tInfo::Settings::Tag{ _session->user() },\n\t\t\tInfo::Section(type)),\n\t\tparams);\n}\n\nvoid SessionNavigation::showSettings(const SectionShow &params) {\n\tshowSettings(Settings::MainId(), params);\n}\n\nvoid SessionNavigation::showPollResults(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId contextId,\n\t\tconst SectionShow &params) {\n\tshowSection(std::make_shared<Info::Memento>(poll, contextId), params);\n}\n\nvoid SessionNavigation::searchInChat(\n\t\tDialogs::Key inChat,\n\t\tPeerData *searchFrom) {\n\tsearchMessages(QString(), inChat, searchFrom);\n}\n\nvoid SessionNavigation::searchMessages(\n\t\tconst QString &query,\n\t\tDialogs::Key inChat,\n\t\tPeerData *searchFrom) {\n\tparentController()->content()->searchMessages(query, inChat, searchFrom);\n}\n\nauto SessionNavigation::showToast(Ui::Toast::Config &&config)\n-> base::weak_ptr<Ui::Toast::Instance> {\n\treturn uiShow()->showToast(std::move(config));\n}\n\nauto SessionNavigation::showToast(const QString &text, crl::time duration)\n-> base::weak_ptr<Ui::Toast::Instance> {\n\treturn uiShow()->showToast(text);\n}\n\nauto SessionNavigation::showToast(\n\tTextWithEntities &&text,\n\tcrl::time duration)\n-> base::weak_ptr<Ui::Toast::Instance> {\n\treturn uiShow()->showToast(std::move(text));\n}\n\nstd::shared_ptr<ChatHelpers::Show> SessionNavigation::uiShow() {\n\treturn parentController()->uiShow();\n}\n\nstruct SessionController::CachedThemeKey {\n\tUi::ChatThemeKey theme;\n\tQString paper;\n\n\tfriend inline auto operator<=>(\n\t\tconst CachedThemeKey&,\n\t\tconst CachedThemeKey&) = default;\n\t[[nodiscard]] explicit operator bool() const {\n\t\treturn theme || !paper.isEmpty();\n\t}\n};\n\nstruct SessionController::CachedTheme {\n\tstd::weak_ptr<Ui::ChatTheme> theme;\n\tstd::shared_ptr<Data::DocumentMedia> media;\n\tstd::unique_ptr<Ui::Text::CustomEmoji> giftSymbol;\n\tuint64 giftId = 0;\n\tData::WallPaper paper;\n\tbool basedOnDark = false;\n\tbool caching = false;\n\trpl::lifetime lifetime;\n};\n\nSessionController::SessionController(\n\tnot_null<Main::Session*> session,\n\tnot_null<Controller*> window)\n: SessionNavigation(session)\n, _window(window)\n, _emojiInteractions(\n\tstd::make_unique<ChatHelpers::EmojiInteractions>(session))\n, _chatPreviewManager(std::make_unique<ChatPreviewManager>(this))\n, _isPrimary(window->isPrimary())\n, _hasDialogs(window->id().hasChatsList())\n, _sendingAnimation(\n\tstd::make_unique<Ui::MessageSendingAnimationController>(this))\n, _tabbedSelector(\n\tstd::make_unique<ChatHelpers::TabbedSelector>(\n\t\t_window->widget(),\n\t\tuiShow(),\n\t\tGifPauseReason::TabbedPanel))\n, _invitePeekTimer([=] { checkInvitePeek(); })\n, _activeChatsFilter(session->data().chatsFilters().defaultId())\n, _openedFolder(window->id().folder())\n, _defaultChatTheme(std::make_shared<Ui::ChatTheme>())\n, _chatStyle(std::make_unique<Ui::ChatStyle>(session->colorIndicesValue())) {\n\tinit();\n\n\t_chatStyleTheme = _defaultChatTheme;\n\t_chatStyle->apply(_defaultChatTheme.get());\n\n\tpushDefaultChatBackground();\n\tTheme::Background()->updates(\n\t) | rpl::on_next([=](const Theme::BackgroundUpdate &update) {\n\t\tif (update.type == Theme::BackgroundUpdate::Type::New\n\t\t\t|| update.type == Theme::BackgroundUpdate::Type::Changed) {\n\t\t\tpushDefaultChatBackground();\n\t\t}\n\t}, _lifetime);\n\tstyle::PaletteChanged(\n\t) | rpl::on_next([=] {\n\t\tfor (auto &[key, value] : _customChatThemes) {\n\t\t\tif (!key.theme.id) {\n\t\t\t\tvalue.theme.reset();\n\t\t\t}\n\t\t}\n\t}, _lifetime);\n\n\t_authedName = session->user()->name();\n\tsession->changes().peerUpdates(\n\t\tData::PeerUpdate::Flag::FullInfo\n\t\t| Data::PeerUpdate::Flag::Name\n\t) | rpl::filter([=](const Data::PeerUpdate &update) {\n\t\tif (update.flags & Data::PeerUpdate::Flag::Name) {\n\t\t\tconst auto user = session->user();\n\t\t\tif (update.peer == user) {\n\t\t\t\t_authedName = user->name();\n\t\t\t\tconst auto &settings = Core::App().settings();\n\t\t\t\tif (!settings.windowTitleContent().hideAccountName) {\n\t\t\t\t\twidget()->updateTitle();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (update.flags & Data::PeerUpdate::Flag::FullInfo) {\n\t\t\tif (update.peer->isSelf()) {\n\t\t\t\tSupport::Helper::CheckIfLost(this);\n\t\t\t}\n\t\t\tfullInfoLoadedHook(update.peer);\n\t\t}\n\t\treturn (update.flags & Data::PeerUpdate::Flag::FullInfo)\n\t\t\t&& (update.peer == _showEditPeer);\n\t}) | rpl::on_next([=] {\n\t\tshow(Box<EditPeerInfoBox>(this, base::take(_showEditPeer)));\n\t}, lifetime());\n\n\tsession->data().chatsListChanges(\n\t) | rpl::filter([=](Data::Folder *folder) {\n\t\treturn (folder != nullptr)\n\t\t\t&& (folder == _openedFolder.current())\n\t\t\t&& folder->chatsList()->indexed()->empty()\n\t\t\t&& !folder->storiesCount();\n\t}) | rpl::on_next([=](Data::Folder *folder) {\n\t\tfolder->updateChatListSortPosition();\n\t\tcloseFolder();\n\t}, lifetime());\n\n\tconst auto processFiltersMenu = [this] {\n\t\tif (SessionNavigation::session().data().chatsFilters().has()) {\n\t\t\tconst auto isHorizontal\n\t\t\t\t= Core::App().settings().chatFiltersHorizontal()\n\t\t\t\t\t|| !enoughSpaceForFilters();\n\t\t\tcontent()->toggleFiltersMenu(isHorizontal);\n\t\t\ttoggleFiltersMenu(!isHorizontal);\n\t\t} else {\n\t\t\tcontent()->toggleFiltersMenu(false);\n\t\t\ttoggleFiltersMenu(false);\n\t\t}\n\t};\n\trpl::merge(\n\t\tenoughSpaceForFiltersValue() | rpl::skip(1) | rpl::to_empty,\n\t\tCore::App().settings().chatFiltersHorizontalChanges() | rpl::to_empty,\n\t\tsession->data().chatsFilters().changed()\n\t) | rpl::on_next([=] {\n\t\tif (!_filtersActivated) {\n\t\t\tprocessFiltersMenu();\n\t\t}\n\t\tcheckOpenedFilter();\n\t\tcrl::on_main(this, processFiltersMenu);\n\t}, lifetime());\n\n\tsession->data().itemIdChanged(\n\t) | rpl::on_next([=](Data::Session::IdChange change) {\n\t\tconst auto current = _activeChatEntry.current();\n\t\tif (const auto topic = current.key.topic()) {\n\t\t\tif (topic->rootId() == change.oldId) {\n\t\t\t\tsetActiveChatEntry({\n\t\t\t\t\tDialogs::Key(topic->forum()->topicFor(change.newId.msg)),\n\t\t\t\t\tcurrent.fullId,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tfor (auto &entry : _chatEntryHistory) {\n\t\t\tif (const auto topic = entry.key.topic()) {\n\t\t\t\tif (topic->rootId() == change.oldId) {\n\t\t\t\t\tentry.key = Dialogs::Key(\n\t\t\t\t\t\ttopic->forum()->topicFor(change.newId.msg));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, lifetime());\n\n\tsession->data().documentLoadProgress(\n\t) | rpl::filter([=](not_null<DocumentData*> document) {\n\t\treturn _pendingOpenDocumentId == document->id\n\t\t\t&& !document->filepath().isEmpty();\n\t}) | rpl::on_next([=](not_null<DocumentData*> document) {\n\t\t_pendingOpenDocumentId = {};\n\t\tFile::Launch(document->filepath());\n\t}, _lifetime);\n\n\tsession->downloaderTaskFinished(\n\t) | rpl::filter([=] {\n\t\treturn _pendingOpenPhoto.media && _pendingOpenPhoto.media->loaded();\n\t}) | rpl::on_next([=] {\n\t\tif (_pendingOpenPhoto.media->saveToFile(_pendingOpenPhoto.filepath)) {\n\t\t\t_pendingOpenPhoto.data->setLocation(\n\t\t\t\tCore::FileLocation(_pendingOpenPhoto.filepath));\n\t\t\tFile::Launch(_pendingOpenPhoto.filepath);\n\t\t}\n\t\t_pendingOpenPhoto = {};\n\t}, _lifetime);\n\n\tsession->api().globalPrivacy().suggestArchiveAndMute(\n\t) | rpl::take(1) | rpl::on_next([=] {\n\t\tsession->api().globalPrivacy().reload(crl::guard(this, [=] {\n\t\t\tif (!session->api().globalPrivacy().archiveAndMuteCurrent()) {\n\t\t\t\tsuggestArchiveAndMute();\n\t\t\t}\n\t\t}));\n\t}, _lifetime);\n\n\tsession->downloader().nonPremiumDelays(\n\t) | rpl::on_next([=](DocumentId id) {\n\t\tcheckNonPremiumLimitToastDownload(id);\n\t}, _lifetime);\n\n\tsession->uploader().nonPremiumDelays(\n\t) | rpl::on_next([=](FullMsgId id) {\n\t\tcheckNonPremiumLimitToastUpload(id);\n\t}, _lifetime);\n\n\tsession->addWindow(this);\n\n\tcrl::on_main(this, [=] {\n\t\tactivateFirstChatsFilter();\n\t\tsetupPremiumToast();\n\t});\n\n#if _DEBUG // TEST: Auto-open craft box on startup\n\tconstexpr auto kGiftsCount = 4;\n\tcrl::on_main(this, [=] {\n\t\tif (rand() % 2 >= 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst auto user = session->user();\n\t\tsession->api().request(MTPpayments_GetSavedStarGifts(\n\t\t\tMTP_flags(MTPpayments_GetSavedStarGifts::Flag::f_exclude_unlimited),\n\t\t\tuser->input(),\n\t\t\tMTP_int(0),\n\t\t\tMTP_string(QString()),\n\t\t\tMTP_int(50)\n\t\t)).done([=](const MTPpayments_SavedStarGifts &result) {\n\t\t\tconst auto &data = result.data();\n\t\t\tsession->data().processUsers(data.vusers());\n\t\t\tsession->data().processChats(data.vchats());\n\n\t\t\tauto craftableGifts = std::vector<Ui::GiftForCraftEntry>();\n\t\t\tcraftableGifts.reserve(kGiftsCount);\n\n\t\t\tfor (const auto &gift : data.vgifts().v) {\n\t\t\t\tif (auto parsed = Api::FromTL(user, gift)) {\n\t\t\t\t\tconst auto unique = parsed->info.unique;\n\t\t\t\t\tif (unique\n\t\t\t\t\t\t&& unique->craftChancePermille > 0\n\t\t\t\t\t\t&& unique->canCraftAt <= base::unixtime::now()) {\n\t\t\t\t\t\tcraftableGifts.push_back({\n\t\t\t\t\t\t\tunique,\n\t\t\t\t\t\t\tparsed->manageId,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif (craftableGifts.size() >= kGiftsCount) {\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!craftableGifts.empty()) {\n\t\t\t\tUi::ShowTestGiftCraftBox(this, std::move(craftableGifts));\n\t\t\t}\n\t\t}).send();\n\t});\n#endif\n}\n\nbool SessionController::skipNonPremiumLimitToast(bool download) const {\n\tif (session().premium()) {\n\t\treturn true;\n\t}\n\tconst auto now = base::unixtime::now();\n\tconst auto last = download\n\t\t? session().settings().lastNonPremiumLimitDownload()\n\t\t: session().settings().lastNonPremiumLimitUpload();\n\tconst auto delay = session().appConfig().get<int>(\n\t\tu\"upload_premium_speedup_notify_period\"_q,\n\t\t3600);\n\treturn (last && now < last + delay && now > last - delay);\n}\n\nvoid SessionController::checkNonPremiumLimitToastDownload(DocumentId id) {\n\tif (skipNonPremiumLimitToast(true)) {\n\t\treturn;\n\t}\n\tconst auto document = session().data().document(id);\n\tconst auto visible = session().data().queryDocumentVisibility(document)\n\t\t|| DownloadingDocument(document);\n\tif (!visible) {\n\t\treturn;\n\t}\n\tcontent()->showNonPremiumLimitToast(true);\n\tconst auto now = base::unixtime::now();\n\tsession().settings().setLastNonPremiumLimitDownload(now);\n\tsession().saveSettingsDelayed();\n}\n\nvoid SessionController::checkNonPremiumLimitToastUpload(FullMsgId id) {\n\tif (skipNonPremiumLimitToast(false)) {\n\t\treturn;\n\t} else if (const auto item = session().data().message(id)) {\n\t\tif (!session().data().queryItemVisibility(item)) {\n\t\t\treturn;\n\t\t}\n\t\tcontent()->showNonPremiumLimitToast(false);\n\t\tconst auto now = base::unixtime::now();\n\t\tsession().settings().setLastNonPremiumLimitUpload(now);\n\t\tsession().saveSettingsDelayed();\n\t}\n}\n\nvoid SessionController::suggestArchiveAndMute() {\n\tconst auto weak = base::make_weak(this);\n\t_window->show(Box([=](not_null<Ui::GenericBox*> box) {\n\t\tbox->setTitle(tr::lng_suggest_hide_new_title());\n\t\tbox->addRow(object_ptr<Ui::FlatLabel>(\n\t\t\tbox,\n\t\t\ttr::lng_suggest_hide_new_about(tr::rich),\n\t\t\tst::boxLabel));\n\t\tbox->addButton(tr::lng_suggest_hide_new_to_settings(), [=] {\n\t\t\tshowSettings(Settings::PrivacySecurityId());\n\t\t});\n\t\tbox->setCloseByOutsideClick(false);\n\t\tbox->boxClosing(\n\t\t) | rpl::on_next([=] {\n\t\t\tcrl::on_main(weak, [=] {\n\t\t\t\tauto &privacy = session().api().globalPrivacy();\n\t\t\t\tprivacy.dismissArchiveAndMuteSuggestion();\n\t\t\t});\n\t\t}, box->lifetime());\n\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\tbox->closeBox();\n\t\t});\n\t}));\n}\n\nSeparateId SessionController::windowId() const {\n\treturn _window->id();\n}\n\nbool SessionController::isPrimary() const {\n\treturn _isPrimary;\n}\n\nnot_null<::MainWindow*> SessionController::widget() const {\n\treturn _window->widget();\n}\n\nauto SessionController::sendingAnimation() const\n-> Ui::MessageSendingAnimationController & {\n\treturn *_sendingAnimation;\n}\n\nauto SessionController::tabbedSelector() const\n-> not_null<ChatHelpers::TabbedSelector*> {\n\treturn _tabbedSelector.get();\n}\n\nvoid SessionController::takeTabbedSelectorOwnershipFrom(\n\t\tnot_null<QWidget*> parent) {\n\tif (_tabbedSelector->parent() == parent) {\n\t\tif (const auto chats = widget()->sessionContent()) {\n\t\t\tchats->returnTabbedSelector();\n\t\t}\n\t\tif (_tabbedSelector->parent() == parent) {\n\t\t\t_tabbedSelector->hide();\n\t\t\t_tabbedSelector->setParent(widget());\n\t\t}\n\t}\n}\n\nbool SessionController::hasTabbedSelectorOwnership() const {\n\treturn (_tabbedSelector->parent() == widget());\n}\n\nvoid SessionController::showEditPeerBox(PeerData *peer) {\n\t_showEditPeer = peer;\n\tsession().api().requestFullPeer(peer);\n}\n\nvoid SessionController::init() {\n\tif (session().supportMode()) {\n\t\tsession().supportHelper().registerWindow(this);\n\t}\n\tsession().data().drawToReplyRequests(\n\t) | rpl::on_next([=](Data::DrawToReplyRequest request) {\n\t\thandleDrawToReplyRequest(std::move(request));\n\t}, lifetime());\n\tsetupShortcuts();\n}\n\nvoid SessionController::setupShortcuts() {\n\tusing namespace Shortcuts;\n\n\tChatSwitchRequests(\n\t) | rpl::filter([=](const ChatSwitchRequest &request) {\n\t\treturn !window().locked()\n\t\t\t&& (_chatSwitchProcess\n\t\t\t\t|| (request.started\n\t\t\t\t\t&& (Core::App().activeWindow() == &window())));\n\t}) | rpl::on_next([=](const ChatSwitchRequest &request) {\n\t\tif (!_chatSwitchProcess) {\n\t\t\t_chatSwitchProcess = std::make_unique<ChatSwitchProcess>(\n\t\t\t\twidget()->bodyWidget(),\n\t\t\t\t&session(),\n\t\t\t\tactiveChatCurrent().thread());\n\t\t\tconst auto close = [this, raw = _chatSwitchProcess.get()] {\n\t\t\t\tif (_chatSwitchProcess.get() == raw) {\n\t\t\t\t\tbase::take(_chatSwitchProcess);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t_chatSwitchProcess->chosen(\n\t\t\t) | rpl::on_next([=](not_null<Data::Thread*> thread) {\n\t\t\t\tclose();\n\n\t\t\t\tconst auto id = SeparateId(thread);\n\t\t\t\tif (const auto window = Core::App().separateWindowFor(id)) {\n\t\t\t\t\twindow->activate();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tjumpToChatListEntry({ Dialogs::Key(thread), FullMsgId() });\n\t\t\t}, _chatSwitchProcess->lifetime());\n\n\t\t\t_chatSwitchProcess->closeRequests(\n\t\t\t) | rpl::on_next(close, _chatSwitchProcess->lifetime());\n\t\t}\n\t\t_chatSwitchProcess->process(request);\n\t}, _lifetime);\n\n\tRequests(\n\t) | rpl::filter([=] {\n\t\treturn (Core::App().activeWindow() == &window())\n\t\t\t&& !isLayerShown()\n\t\t\t&& !window().locked();\n\t}) | rpl::on_next([=](not_null<Request*> request) {\n\t\tusing C = Command;\n\n\t\tconst auto app = &Core::App();\n\t\tconst auto accountsCount = int(app->domain().accounts().size());\n\t\tauto &&accounts = ranges::views::zip(\n\t\t\tkShowAccount,\n\t\t\tranges::views::ints(0, accountsCount));\n\t\tfor (const auto &[command, index] : accounts) {\n\t\t\trequest->check(command) && request->handle([=] {\n\t\t\t\tconst auto list = app->domain().orderedAccounts();\n\t\t\t\tif (index >= list.size()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst auto account = list[index];\n\t\t\t\tif (account == &session().account()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst auto window = app->separateWindowFor(account);\n\t\t\t\tif (window) {\n\t\t\t\t\twindow->activate();\n\t\t\t\t} else {\n\t\t\t\t\tapp->domain().maybeActivate(account);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\n\t\tif (!session().supportMode()) {\n\t\t\treturn;\n\t\t}\n\t\trequest->check(C::SupportHistoryBack) && request->handle([=] {\n\t\t\treturn chatEntryHistoryMove(-1);\n\t\t});\n\t\trequest->check(C::SupportHistoryForward) && request->handle([=] {\n\t\t\treturn chatEntryHistoryMove(1);\n\t\t});\n\t}, lifetime());\n}\n\nvoid SessionController::toggleFiltersMenu(bool enabled) {\n\tif (!_isPrimary || (!enabled == !_filters)) {\n\t\treturn;\n\t} else if (enabled) {\n\t\t_filters = std::make_unique<FiltersMenu>(\n\t\t\twidget()->bodyWidget(),\n\t\t\tthis);\n\t} else {\n\t\t_filters = nullptr;\n\t}\n\t_filtersMenuChanged.fire({});\n}\n\nrpl::producer<> SessionController::filtersMenuChanged() const {\n\treturn _filtersMenuChanged.events();\n}\n\nvoid SessionController::checkOpenedFilter() {\n\tactivateFirstChatsFilter();\n\tif (const auto filterId = activeChatsFilterCurrent()) {\n\t\tconst auto &list = session().data().chatsFilters().list();\n\t\tconst auto i = ranges::find(list, filterId, &Data::ChatFilter::id);\n\t\tif (i == end(list)) {\n\t\t\tsetActiveChatsFilter(\n\t\t\t\t0,\n\t\t\t\t{ anim::type::normal, anim::activation::background });\n\t\t}\n\t}\n}\n\nvoid SessionController::activateFirstChatsFilter() {\n\tif (_filtersActivated\n\t\t|| !isPrimary()\n\t\t|| !session().data().chatsFilters().loaded()) {\n\t\treturn;\n\t}\n\t_filtersActivated = true;\n\tsetActiveChatsFilter(session().data().chatsFilters().defaultId());\n}\n\nbool SessionController::uniqueChatsInSearchResults(\n\t\tconst Dialogs::SearchState &state) const {\n\tconst auto global = (state.tab == Dialogs::ChatSearchTab::MyMessages)\n\t\t|| (state.tab == Dialogs::ChatSearchTab::PublicPosts);\n\treturn session().supportMode()\n\t\t&& !session().settings().supportAllSearchResults()\n\t\t&& (global || !state.inChat);\n}\n\nbool SessionController::openFolderInDifferentWindow(\n\t\tnot_null<Data::Folder*> folder) {\n\tconst auto id = SeparateId(SeparateType::Archive, &session());\n\tif (const auto separate = Core::App().separateWindowFor(id)) {\n\t\tif (separate == _window) {\n\t\t\treturn false;\n\t\t}\n\t\tseparate->sessionController()->showByInitialId();\n\t\tseparate->activate();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid SessionController::openFolder(not_null<Data::Folder*> folder) {\n\tif (openFolderInDifferentWindow(folder)) {\n\t\treturn;\n\t} else if (_openedFolder.current() != folder) {\n\t\tresetFakeUnreadWhileOpened();\n\t}\n\tif (activeChatsFilterCurrent() != 0) {\n\t\tsetActiveChatsFilter(0);\n\t} else if (adaptive().isOneColumn()) {\n\t\tclearSectionStack(SectionShow::Way::ClearStack);\n\t}\n\tcloseForum();\n\t_openedFolder = folder.get();\n}\n\nvoid SessionController::closeFolder() {\n\tif (_openedFolder.current()\n\t\t&& windowId().type == SeparateType::Archive) {\n\t\tCore::App().closeWindow(_window);\n\t\treturn;\n\t}\n\t_openedFolder = nullptr;\n}\n\nbool SessionController::showForumInDifferentWindow(\n\t\tnot_null<Data::Forum*> forum,\n\t\tconst SectionShow &params,\n\t\tMsgId showAtMsgId) {\n\tconst auto window = Core::App().windowForShowingForum(forum);\n\tif (window == _window) {\n\t\treturn false;\n\t} else if (window) {\n\t\twindow->sessionController()->showForum(forum, params, showAtMsgId);\n\t\twindow->activate();\n\t\treturn true;\n\t} else if (windowId().hasChatsList()) {\n\t\treturn false;\n\t}\n\tconst auto account = not_null(&session().account());\n\tauto primary = Core::App().separateWindowFor(account);\n\tif (!primary) {\n\t\tCore::App().domain().activate(account);\n\t\tprimary = Core::App().separateWindowFor(account);\n\t}\n\tif (primary && &primary->account() == account) {\n\t\tprimary->sessionController()->showForum(forum, params, showAtMsgId);\n\t\tprimary->activate();\n\t}\n\treturn true;\n}\n\nvoid SessionController::showForum(\n\t\tnot_null<Data::Forum*> forum,\n\t\tconst SectionShow &params,\n\t\tMsgId showAtMsgId) {\n\tconst auto forced = params.forceTopicsList;\n\tif (showForumInDifferentWindow(forum, params, showAtMsgId)) {\n\t\treturn;\n\t} else if (!forced && forum->peer()->useSubsectionTabs()) {\n\t\tconst auto active = (showAtMsgId == ShowAtUnreadMsgId)\n\t\t\t? forum->activeSubsectionThread()\n\t\t\t: nullptr;\n\t\tif (active) {\n\t\t\tshowThread(active, showAtMsgId, params);\n\t\t} else {\n\t\t\tshowPeerHistory(forum->peer(), params, showAtMsgId);\n\t\t}\n\t\treturn;\n\t}\n\t_shownForumLifetime.destroy();\n\tif (_shownForum.current() != forum) {\n\t\tresetFakeUnreadWhileOpened();\n\t}\n\tif (forum\n\t\t&& _activeChatEntry.current().key.peer()\n\t\t&& adaptive().isOneColumn()) {\n\t\tclearSectionStack(params);\n\t}\n\t_shownForum = forum.get();\n\tif (_shownForum.current() != forum) {\n\t\treturn;\n\t}\n\tconst auto history = forum->history();\n\tconst auto closeAndShowHistory = [=](bool showOnlyIfEmpty) {\n\t\tconst auto now = activeChatCurrent().owningHistory();\n\t\tconst auto showHistory = !now\n\t\t\t|| (!showOnlyIfEmpty && (now == history));\n\t\tconst auto weak = base::make_weak(this);\n\t\tcloseForum();\n\t\tif (weak && showHistory) {\n\t\t\tshowPeerHistory(history, {\n\t\t\t\tSectionShow::Way::Backward,\n\t\t\t\tanim::type::normal,\n\t\t\t\tanim::activation::background,\n\t\t\t});\n\t\t}\n\t};\n\tcontent()->showForum(forum, params);\n\tif (_shownForum.current() != forum) {\n\t\treturn;\n\t}\n\n\tforum->destroyed(\n\t) | rpl::on_next([=] {\n\t\tcloseAndShowHistory(false);\n\t}, _shownForumLifetime);\n\tif (!forced) {\n\t\tusing FlagChange = Data::Flags<ChannelDataFlags>::Change;\n\t\tif (const auto channel = forum->channel()) {\n\t\t\tchannel->flagsValue(\n\t\t\t) | rpl::on_next([=](FlagChange change) {\n\t\t\t\tif (change.diff & ChannelDataFlag::ForumTabs) {\n\t\t\t\t\tif (HistoryView::SubsectionTabs::UsedFor(history)) {\n\t\t\t\t\t\tcloseAndShowHistory(true);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, _shownForumLifetime);\n\t\t}\n\t}\n}\n\nvoid SessionController::closeForum() {\n\tif (const auto forum = _shownForum.current()) {\n\t\tconst auto id = windowId();\n\t\tif (id.type == SeparateType::Forum) {\n\t\t\tconst auto initial = id.forum();\n\t\t\tif (!initial || initial == forum) {\n\t\t\t\tCore::App().closeWindow(_window);\n\t\t\t} else {\n\t\t\t\tshowForum(initial);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t}\n\t_shownForumLifetime.destroy();\n\t_shownForum = nullptr;\n}\n\nvoid SessionController::setupPremiumToast() {\n\trpl::combine(\n\t\tData::AmPremiumValue(&session()),\n\t\tsession().changes().peerUpdates(\n\t\t\tData::PeerUpdate::Flag::FullInfo\n\t\t)\n\t) | rpl::filter([=] {\n\t\treturn session().user()->isFullLoaded();\n\t}) | rpl::map([=](bool premium, const auto&) {\n\t\treturn premium;\n\t}) | rpl::distinct_until_changed() | rpl::skip(\n\t\t1\n\t) | rpl::filter([=](bool premium) {\n\t\tsession().mtp().requestConfig();\n\t\treturn premium;\n\t}) | rpl::on_next([=] {\n\t\tMainWindowShow(this).showToast({\n\t\t\t.text = { tr::lng_premium_success(tr::now) },\n\t\t\t.adaptive = true,\n\t\t});\n\t}, _lifetime);\n}\n\nconst rpl::variable<Data::Folder*> &SessionController::openedFolder() const {\n\treturn _openedFolder;\n}\n\nconst rpl::variable<Data::Forum*> &SessionController::shownForum() const {\n\treturn _shownForum;\n}\n\nvoid SessionController::setActiveChatEntry(Dialogs::RowDescriptor row) {\n\tif (windowId().type == SeparateType::SharedMedia) {\n\t\treturn;\n\t}\n\tconst auto was = _activeChatEntry.current();\n\tif (was.key && was.key != row.key) {\n\t\tsession().api().saveCurrentDraftToCloud();\n\t}\n\tconst auto wasHistory = was.key.history();\n\tconst auto nowHistory = row.key.history();\n\tif (wasHistory && wasHistory != nowHistory) {\n\t\t_activeHistoryLifetime.destroy();\n\t\twasHistory->setFakeUnreadWhileOpened(false);\n\t\t_invitePeekTimer.cancel();\n\t}\n\t_activeChatEntry = row;\n\tif (nowHistory) {\n\t\tnowHistory->setFakeUnreadWhileOpened(true);\n\t\tif (const auto channel = nowHistory->peer->asChannel()\n\t\t\t; channel && !channel->isForum()) {\n\t\t\tData::PeerFlagValue(\n\t\t\t\tchannel,\n\t\t\t\tChannelData::Flag::Forum\n\t\t\t) | rpl::filter([=](bool forum) {\n\t\t\t\treturn forum && !channel->useSubsectionTabs();\n\t\t\t}) | rpl::on_next([=] {\n\t\t\t\tclearSectionStack(\n\t\t\t\t\t{ anim::type::normal, anim::activation::background });\n\t\t\t\tshowForum(channel->forum(),\n\t\t\t\t\t{ anim::type::normal, anim::activation::background });\n\t\t\t}, _activeHistoryLifetime);\n\t\t}\n\t}\n\tif (const auto thread = row.key.thread()) {\n\t\tsession().recentPeers().chatOpenPush(thread);\n\t}\n\tif (session().supportMode()) {\n\t\tpushToChatEntryHistory(row);\n\t}\n\tcheckInvitePeek();\n}\n\nvoid SessionController::checkInvitePeek() {\n\tconst auto history = activeChatCurrent().history();\n\tif (!history) {\n\t\treturn;\n\t}\n\tconst auto channel = history->peer->asChannel();\n\tif (!channel) {\n\t\treturn;\n\t}\n\tconst auto expires = channel->invitePeekExpires();\n\tif (!expires) {\n\t\treturn;\n\t}\n\tconst auto now = base::unixtime::now();\n\tif (expires > now) {\n\t\t_invitePeekTimer.callOnce((expires - now) * crl::time(1000));\n\t\treturn;\n\t}\n\tconst auto hash = channel->invitePeekHash();\n\tchannel->clearInvitePeek();\n\tApi::CheckChatInvite(this, hash, channel);\n}\n\nvoid SessionController::resetFakeUnreadWhileOpened() {\n\tif (const auto history = _activeChatEntry.current().key.history()) {\n\t\thistory->setFakeUnreadWhileOpened(false);\n\t}\n}\n\nbool SessionController::chatEntryHistoryMove(int steps) {\n\tif (_chatEntryHistory.empty()) {\n\t\treturn false;\n\t}\n\tconst auto position = _chatEntryHistoryPosition + steps;\n\tif (!base::in_range(position, 0, int(_chatEntryHistory.size()))) {\n\t\treturn false;\n\t}\n\t_chatEntryHistoryPosition = position;\n\treturn jumpToChatListEntry(_chatEntryHistory[position]);\n}\n\nbool SessionController::jumpToChatListEntry(Dialogs::RowDescriptor row) {\n\tif (const auto thread = row.key.thread()) {\n\t\tshowThread(\n\t\t\tthread,\n\t\t\trow.fullId.msg,\n\t\t\tSectionShow::Way::ClearStack);\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nvoid SessionController::setDialogsEntryState(\n\t\tDialogs::EntryState state) {\n\t_dialogsEntryState = state;\n}\n\nDialogs::EntryState SessionController::dialogsEntryStateCurrent() const {\n\treturn _dialogsEntryState.current();\n}\n\nauto SessionController::dialogsEntryStateValue() const\n-> rpl::producer<Dialogs::EntryState> {\n\treturn _dialogsEntryState.value();\n}\n\nbool SessionController::switchInlineQuery(\n\t\tDialogs::EntryState to,\n\t\tnot_null<UserData*> bot,\n\t\tconst QString &query) {\n\tExpects(to.key.owningHistory() != nullptr);\n\n\tusing Section = Dialogs::EntryState::Section;\n\n\tconst auto thread = to.key.thread();\n\tif (!thread || !Data::CanSend(thread, ChatRestriction::SendInline)) {\n\t\tshow(Ui::MakeInformBox(tr::lng_inline_switch_cant()));\n\t\treturn false;\n\t}\n\n\tconst auto history = to.key.owningHistory();\n\tconst auto textWithTags = TextWithTags{\n\t\t'@' + bot->username() + ' ' + query,\n\t\tTextWithTags::Tags(),\n\t};\n\tMessageCursor cursor = {\n\t\tint(textWithTags.text.size()),\n\t\tint(textWithTags.text.size()),\n\t\tUi::kQFixedMax\n\t};\n\tif (to.currentReplyTo.messageId.msg == to.currentReplyTo.topicRootId\n\t\t&& to.currentReplyTo.quote.empty()) {\n\t\tto.currentReplyTo.messageId.msg = MsgId();\n\t}\n\tif (!history->suggestDraftAllowed()) {\n\t\tto.currentSuggest = SuggestOptions();\n\t}\n\tauto draft = std::make_unique<Data::Draft>(\n\t\ttextWithTags,\n\t\tto.currentReplyTo,\n\t\tto.currentSuggest,\n\t\tcursor,\n\t\tData::WebPageDraft());\n\n\tauto params = Window::SectionShow();\n\tparams.reapplyLocalDraft = true;\n\tif (to.section == Section::Scheduled) {\n\t\thistory->setDraft(Data::DraftKey::Scheduled(), std::move(draft));\n\t\tshowSection(\n\t\t\tstd::make_shared<HistoryView::ScheduledMemento>(history),\n\t\t\tparams);\n\t} else {\n\t\tconst auto topicRootId = to.currentReplyTo.topicRootId;\n\t\tconst auto monoforumPeerId = to.currentReplyTo.monoforumPeerId;\n\t\thistory->setLocalDraft(std::move(draft));\n\t\thistory->clearLocalEditDraft(topicRootId, monoforumPeerId);\n\t\tif (to.section == Section::Replies) {\n\t\t\tconst auto commentId = MsgId();\n\t\t\tshowRepliesForMessage(history, topicRootId, commentId, params);\n\t\t} else {\n\t\t\tshowPeerHistory(history->peer, params);\n\t\t}\n\t}\n\treturn true;\n}\n\nbool SessionController::switchInlineQuery(\n\t\tnot_null<Data::Thread*> thread,\n\t\tnot_null<UserData*> bot,\n\t\tconst QString &query) {\n\tconst auto entryState = Dialogs::EntryState{\n\t\t.key = thread,\n\t\t.section = (thread->asTopic()\n\t\t\t? Dialogs::EntryState::Section::Replies\n\t\t\t: Dialogs::EntryState::Section::History),\n\t\t.currentReplyTo = { .topicRootId = thread->topicRootId() },\n\t};\n\treturn switchInlineQuery(entryState, bot, query);\n}\n\nDialogs::RowDescriptor SessionController::resolveChatNext(\n\t\tDialogs::RowDescriptor from) const {\n\treturn content()->resolveChatNext(from);\n}\n\nDialogs::RowDescriptor SessionController::resolveChatPrevious(\n\t\tDialogs::RowDescriptor from) const {\n\treturn content()->resolveChatPrevious(from);\n}\n\nvoid SessionController::pushToChatEntryHistory(Dialogs::RowDescriptor row) {\n\tif (!_chatEntryHistory.empty()\n\t\t&& _chatEntryHistory[_chatEntryHistoryPosition] == row) {\n\t\treturn;\n\t}\n\t_chatEntryHistory.resize(++_chatEntryHistoryPosition);\n\t_chatEntryHistory.push_back(row);\n\tif (_chatEntryHistory.size() > kMaxChatEntryHistorySize) {\n\t\t_chatEntryHistory.pop_front();\n\t\t--_chatEntryHistoryPosition;\n\t}\n}\n\nvoid SessionController::setActiveChatEntry(Dialogs::Key key) {\n\tsetActiveChatEntry({ key, FullMsgId() });\n}\n\nDialogs::RowDescriptor SessionController::activeChatEntryCurrent() const {\n\treturn _activeChatEntry.current();\n}\n\nDialogs::Key SessionController::activeChatCurrent() const {\n\treturn activeChatEntryCurrent().key;\n}\n\nauto SessionController::activeChatEntryChanges() const\n-> rpl::producer<Dialogs::RowDescriptor> {\n\treturn _activeChatEntry.changes();\n}\n\nrpl::producer<Dialogs::Key> SessionController::activeChatChanges() const {\n\treturn activeChatEntryChanges(\n\t) | rpl::map([](const Dialogs::RowDescriptor &value) {\n\t\treturn value.key;\n\t}) | rpl::distinct_until_changed();\n}\n\nauto SessionController::activeChatEntryValue() const\n-> rpl::producer<Dialogs::RowDescriptor> {\n\treturn _activeChatEntry.value();\n}\n\nrpl::producer<Dialogs::Key> SessionController::activeChatValue() const {\n\treturn activeChatEntryValue(\n\t) | rpl::map([](const Dialogs::RowDescriptor &value) {\n\t\treturn value.key;\n\t}) | rpl::distinct_until_changed();\n}\n\nvoid SessionController::enableGifPauseReason(GifPauseReason reason) {\n\tif (!(_gifPauseReasons & reason)) {\n\t\tauto notify = (static_cast<int>(_gifPauseReasons) < static_cast<int>(reason));\n\t\t_gifPauseReasons |= reason;\n\t\tif (notify) {\n\t\t\t_gifPauseLevelChanged.fire({});\n\t\t}\n\t}\n}\n\nvoid SessionController::disableGifPauseReason(GifPauseReason reason) {\n\tif (_gifPauseReasons & reason) {\n\t\t_gifPauseReasons &= ~reason;\n\t\tif (_gifPauseReasons < reason) {\n\t\t\t_gifPauseLevelChanged.fire({});\n\t\t}\n\t}\n}\n\nbool SessionController::isGifPausedAtLeastFor(GifPauseReason reason) const {\n\tif (reason == GifPauseReason::Any) {\n\t\treturn (_gifPauseReasons != 0) || !widget()->isActive();\n\t}\n\treturn (static_cast<int>(_gifPauseReasons) >= 2 * static_cast<int>(reason)) || !widget()->isActive();\n}\n\nvoid SessionController::floatPlayerAreaUpdated() {\n\tif (const auto main = widget()->sessionContent()) {\n\t\tmain->floatPlayerAreaUpdated();\n\t}\n}\n\nint SessionController::dialogsSmallColumnWidth() const {\n\treturn st::defaultDialogRow.padding.left()\n\t\t+ st::defaultDialogRow.photoSize\n\t\t+ st::defaultDialogRow.padding.left();\n}\n\nint SessionController::minimalThreeColumnWidth() const {\n\treturn (_hasDialogs ? st::columnMinimalWidthLeft : 0)\n\t\t+ st::columnMinimalWidthMain\n\t\t+ st::columnMinimalWidthThird;\n}\n\nauto SessionController::computeColumnLayout() const -> ColumnLayout {\n\tauto layout = Adaptive::WindowLayout::OneColumn;\n\n\tauto bodyWidth = widget()->bodyWidget()->width() - filtersWidth();\n\tauto dialogsWidth = 0, chatWidth = 0, thirdWidth = 0;\n\n\tauto useOneColumnLayout = [&] {\n\t\tauto minimalNormal = st::columnMinimalWidthLeft\n\t\t\t+ st::columnMinimalWidthMain;\n\t\tif (_hasDialogs && bodyWidth < minimalNormal) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\n\tauto useNormalLayout = [&] {\n\t\t// Used if useSmallColumnLayout() == false.\n\t\tif (bodyWidth < minimalThreeColumnWidth()) {\n\t\t\treturn true;\n\t\t}\n\t\tif (!Core::App().settings().tabbedSelectorSectionEnabled()\n\t\t\t&& !Core::App().settings().thirdSectionInfoEnabled()) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\n\tif (useOneColumnLayout()) {\n\t\tdialogsWidth = chatWidth = bodyWidth;\n\t} else if (useNormalLayout()) {\n\t\tlayout = Adaptive::WindowLayout::Normal;\n\t\tdialogsWidth = countDialogsWidthFromRatio(bodyWidth);\n\t\taccumulate_min(dialogsWidth, bodyWidth - st::columnMinimalWidthMain);\n\t\tchatWidth = bodyWidth - dialogsWidth;\n\t} else {\n\t\tlayout = Adaptive::WindowLayout::ThreeColumn;\n\t\tdialogsWidth = countDialogsWidthFromRatio(bodyWidth);\n\t\tthirdWidth = countThirdColumnWidthFromRatio(bodyWidth);\n\t\tauto shrink = shrinkDialogsAndThirdColumns(\n\t\t\tdialogsWidth,\n\t\t\tthirdWidth,\n\t\t\tbodyWidth);\n\t\tdialogsWidth = shrink.dialogsWidth;\n\t\tthirdWidth = shrink.thirdWidth;\n\n\t\tchatWidth = bodyWidth - dialogsWidth - thirdWidth;\n\t}\n\treturn { bodyWidth, dialogsWidth, chatWidth, thirdWidth, layout };\n}\n\nint SessionController::countDialogsWidthFromRatio(int bodyWidth) const {\n\tif (!_hasDialogs) {\n\t\treturn 0;\n\t}\n\tconst auto nochat = !mainSectionShown();\n\tconst auto width = bodyWidth\n\t\t* Core::App().settings().dialogsWidthRatio(nochat);\n\tauto result = qRound(width);\n\taccumulate_max(result, st::columnMinimalWidthLeft);\n//\taccumulate_min(result, st::columnMaximalWidthLeft);\n\treturn result;\n}\n\nint SessionController::countThirdColumnWidthFromRatio(int bodyWidth) const {\n\tauto result = Core::App().settings().thirdColumnWidth();\n\taccumulate_max(result, st::columnMinimalWidthThird);\n\taccumulate_min(result, st::columnMaximalWidthThird);\n\treturn result;\n}\n\nSessionController::ShrinkResult SessionController::shrinkDialogsAndThirdColumns(\n\t\tint dialogsWidth,\n\t\tint thirdWidth,\n\t\tint bodyWidth) const {\n\tauto chatWidth = st::columnMinimalWidthMain;\n\tif (dialogsWidth + thirdWidth + chatWidth <= bodyWidth) {\n\t\treturn { dialogsWidth, thirdWidth };\n\t}\n\tauto thirdWidthNew = ((bodyWidth - chatWidth) * thirdWidth)\n\t\t/ (dialogsWidth + thirdWidth);\n\tauto dialogsWidthNew = ((bodyWidth - chatWidth) * dialogsWidth)\n\t\t/ (dialogsWidth + thirdWidth);\n\tif (thirdWidthNew < st::columnMinimalWidthThird) {\n\t\tthirdWidthNew = st::columnMinimalWidthThird;\n\t\tdialogsWidthNew = bodyWidth - thirdWidthNew - chatWidth;\n\t\tAssert(!_hasDialogs || dialogsWidthNew >= st::columnMinimalWidthLeft);\n\t} else if (_hasDialogs && dialogsWidthNew < st::columnMinimalWidthLeft) {\n\t\tdialogsWidthNew = st::columnMinimalWidthLeft;\n\t\tthirdWidthNew = bodyWidth - dialogsWidthNew - chatWidth;\n\t\tAssert(thirdWidthNew >= st::columnMinimalWidthThird);\n\t}\n\treturn { dialogsWidthNew, thirdWidthNew };\n}\n\nbool SessionController::canShowThirdSection() const {\n\tauto currentLayout = computeColumnLayout();\n\tauto minimalExtendBy = minimalThreeColumnWidth()\n\t\t- currentLayout.bodyWidth;\n\treturn (minimalExtendBy <= widget()->maximalExtendBy());\n}\n\nbool SessionController::canShowThirdSectionWithoutResize() const {\n\tauto currentWidth = computeColumnLayout().bodyWidth;\n\treturn currentWidth >= minimalThreeColumnWidth();\n}\n\nbool SessionController::takeThirdSectionFromLayer() {\n\treturn widget()->takeThirdSectionFromLayer();\n}\n\nvoid SessionController::resizeForThirdSection() {\n\tif (adaptive().isThreeColumn()) {\n\t\treturn;\n\t}\n\n\tauto &settings = Core::App().settings();\n\tauto layout = computeColumnLayout();\n\tauto tabbedSelectorSectionEnabled\n\t\t= settings.tabbedSelectorSectionEnabled();\n\tauto thirdSectionInfoEnabled\n\t\t= settings.thirdSectionInfoEnabled();\n\tsettings.setTabbedSelectorSectionEnabled(false);\n\tsettings.setThirdSectionInfoEnabled(false);\n\n\tauto wanted = countThirdColumnWidthFromRatio(layout.bodyWidth);\n\tauto minimal = st::columnMinimalWidthThird;\n\tauto extendBy = wanted;\n\tauto extendedBy = [&] {\n\t\t// Best - extend by third column without moving the window.\n\t\t// Next - extend by minimal third column without moving.\n\t\t// Next - show third column inside the window without moving.\n\t\t// Last - extend with moving.\n\t\tif (widget()->canExtendNoMove(wanted)) {\n\t\t\treturn widget()->tryToExtendWidthBy(wanted);\n\t\t} else if (widget()->canExtendNoMove(minimal)) {\n\t\t\textendBy = minimal;\n\t\t\treturn widget()->tryToExtendWidthBy(minimal);\n\t\t} else if (layout.bodyWidth >= minimalThreeColumnWidth()) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn widget()->tryToExtendWidthBy(minimal);\n\t}();\n\tif (extendedBy) {\n\t\tif (extendBy != settings.thirdColumnWidth()) {\n\t\t\tsettings.setThirdColumnWidth(extendBy);\n\t\t}\n\t\tconst auto nochat = !mainSectionShown();\n\t\tauto newBodyWidth = layout.bodyWidth + extendedBy;\n\t\tauto currentRatio = settings.dialogsWidthRatio(nochat);\n\t\tsettings.updateDialogsWidthRatio(\n\t\t\t(currentRatio * layout.bodyWidth) / newBodyWidth,\n\t\t\tnochat);\n\t}\n\tauto savedValue = (extendedBy == extendBy) ? -1 : extendedBy;\n\tsettings.setThirdSectionExtendedBy(savedValue);\n\n\tsettings.setTabbedSelectorSectionEnabled(\n\t\ttabbedSelectorSectionEnabled);\n\tsettings.setThirdSectionInfoEnabled(\n\t\tthirdSectionInfoEnabled);\n}\n\nvoid SessionController::closeThirdSection() {\n\tauto &settings = Core::App().settings();\n\tauto newWindowSize = widget()->size();\n\tauto layout = computeColumnLayout();\n\tif (layout.windowLayout == Adaptive::WindowLayout::ThreeColumn) {\n\t\tconst auto nochat = !mainSectionShown();\n\t\tauto noResize = widget()->isFullScreen()\n\t\t\t|| widget()->isMaximized();\n\t\tauto savedValue = settings.thirdSectionExtendedBy();\n\t\tauto extendedBy = (savedValue == -1)\n\t\t\t? layout.thirdWidth\n\t\t\t: savedValue;\n\t\tauto newBodyWidth = noResize\n\t\t\t? layout.bodyWidth\n\t\t\t: (layout.bodyWidth - extendedBy);\n\t\tauto currentRatio = settings.dialogsWidthRatio(nochat);\n\t\tsettings.updateDialogsWidthRatio(\n\t\t\t(currentRatio * layout.bodyWidth) / newBodyWidth,\n\t\t\tnochat);\n\t\tnewWindowSize = QSize(\n\t\t\twidget()->width() + (newBodyWidth - layout.bodyWidth),\n\t\t\twidget()->height());\n\t}\n\tsettings.setTabbedSelectorSectionEnabled(false);\n\tsettings.setThirdSectionInfoEnabled(false);\n\tCore::App().saveSettingsDelayed();\n\tif (widget()->size() != newWindowSize) {\n\t\twidget()->resize(newWindowSize);\n\t} else {\n\t\tupdateColumnLayout();\n\t}\n}\n\nbool SessionController::canShowSeparateWindow(SeparateId id) const {\n\tif (const auto thread = id.thread) {\n\t\treturn thread->peer()->computeUnavailableReason().isEmpty();\n\t}\n\treturn true;\n}\n\nvoid SessionController::showPeer(not_null<PeerData*> peer, MsgId msgId) {\n\tconst auto currentPeer = activeChatCurrent().peer();\n\tif (peer && peer->isChannel() && currentPeer != peer) {\n\t\tconst auto clickedChannel = peer->asChannel();\n\t\tif (!clickedChannel->isPublic()\n\t\t\t&& !clickedChannel->amIn()\n\t\t\t&& (!currentPeer->isChannel()\n\t\t\t\t|| currentPeer->asChannel()->discussionLink()\n\t\t\t\t\t!= clickedChannel)) {\n\t\t\tMainWindowShow(this).showToast(peer->isMegagroup()\n\t\t\t\t? tr::lng_group_not_accessible(tr::now)\n\t\t\t\t: tr::lng_channel_not_accessible(tr::now));\n\t\t} else {\n\t\t\tshowPeerHistory(peer->id, SectionShow(), msgId);\n\t\t}\n\t} else {\n\t\tshowPeerInfo(peer, SectionShow());\n\t}\n}\n\nvoid SessionController::startOrJoinGroupCall(not_null<PeerData*> peer) {\n\tstartOrJoinGroupCall(peer, {});\n}\n\nvoid SessionController::startOrJoinGroupCall(\n\t\tnot_null<PeerData*> peer,\n\t\tCalls::StartGroupCallArgs args) {\n\tCore::App().calls().startOrJoinGroupCall(uiShow(), peer, args);\n}\n\nvoid SessionController::showCalendar(ShowCalendarDescriptor &&descriptor) {\n\tconst auto chat = descriptor.chat;\n\tconst auto requestedDate = descriptor.date;\n\tconst auto topic = chat.topic();\n\tconst auto sublist = chat.sublist();\n\tconst auto history = chat.owningHistory();\n\tif (!history) {\n\t\treturn;\n\t}\n\tconst auto currentPeerDate = [&] {\n\t\tif (topic) {\n\t\t\tif (const auto item = topic->lastMessage()) {\n\t\t\t\treturn base::unixtime::parse(item->date()).date();\n\t\t\t}\n\t\t\treturn QDate();\n\t\t} else if (history->scrollTopItem) {\n\t\t\treturn history->scrollTopItem->dateTime().date();\n\t\t} else if (history->loadedAtTop()\n\t\t\t&& !history->isEmpty()\n\t\t\t&& history->peer->migrateFrom()) {\n\t\t\tif (const auto migrated = history->owner().historyLoaded(history->peer->migrateFrom())) {\n\t\t\t\tif (migrated->scrollTopItem) {\n\t\t\t\t\t// We're up in the migrated history.\n\t\t\t\t\t// So current date is the date of first message here.\n\t\t\t\t\treturn history->blocks.front()->messages.front()->dateTime().date();\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (const auto item = history->lastMessage()) {\n\t\t\treturn base::unixtime::parse(item->date()).date();\n\t\t}\n\t\treturn QDate();\n\t}();\n\tconst auto maxPeerDate = [&] {\n\t\tif (topic) {\n\t\t\tif (const auto item = topic->lastMessage()) {\n\t\t\t\treturn base::unixtime::parse(item->date()).date();\n\t\t\t}\n\t\t\treturn QDate();\n\t\t}\n\t\tconst auto check = history->peer->migrateTo()\n\t\t\t? history->owner().historyLoaded(history->peer->migrateTo())\n\t\t\t: history;\n\t\tif (const auto item = check ? check->lastMessage() : nullptr) {\n\t\t\treturn base::unixtime::parse(item->date()).date();\n\t\t}\n\t\treturn QDate();\n\t}();\n\tconst auto minPeerDate = [&] {\n\t\tconst auto startDate = [&] {\n\t\t\t// Telegram was launched in August 2013 :)\n\t\t\treturn QDate(2013, 8, 1);\n\t\t};\n\t\tif (topic) {\n\t\t\treturn base::unixtime::parse(topic->creationDate()).date();\n\t\t} else if (const auto chat = history->peer->migrateFrom()) {\n\t\t\tif (const auto history = chat->owner().historyLoaded(chat)) {\n\t\t\t\tif (history->loadedAtTop()) {\n\t\t\t\t\tif (!history->isEmpty()) {\n\t\t\t\t\t\treturn history->blocks.front()->messages.front()->dateTime().date();\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\treturn startDate();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (history->loadedAtTop()) {\n\t\t\tif (!history->isEmpty()) {\n\t\t\t\treturn history->blocks.front()->messages.front()->dateTime().date();\n\t\t\t}\n\t\t\treturn QDate::currentDate();\n\t\t}\n\t\treturn startDate();\n\t}();\n\tconst auto highlighted = !requestedDate.isNull()\n\t\t? requestedDate\n\t\t: !currentPeerDate.isNull()\n\t\t? currentPeerDate\n\t\t: QDate::currentDate();\n\tconst auto performJump = descriptor.customJump;\n\tstruct ButtonState {\n\t\tenum class Type {\n\t\t\tNone,\n\t\t\tDisabled,\n\t\t\tActive,\n\t\t};\n\t\tType type = Type::None;\n\t\tstyle::complex_color disabledFg = style::complex_color([] {\n\t\t\tauto result = st::attentionBoxButton.textFg->c;\n\t\t\tresult.setAlpha(result.alpha() / 2);\n\t\t\treturn result;\n\t\t});\n\t\tstyle::RoundButton disabled = st::attentionBoxButton;\n\t};\n\tconst auto buttonState = std::make_shared<ButtonState>();\n\tbuttonState->disabled.textFg\n\t\t= buttonState->disabled.textFgOver\n\t\t= buttonState->disabledFg.color();\n\tbuttonState->disabled.ripple.color\n\t\t= buttonState->disabled.textBgOver\n\t\t= buttonState->disabled.textBg;\n\tconst auto selectionChanged = [=](\n\t\t\tnot_null<Ui::CalendarBox*> box,\n\t\t\tstd::optional<int> selected) {\n\t\tif (!selected.has_value()) {\n\t\t\tbuttonState->type = ButtonState::Type::None;\n\t\t\treturn;\n\t\t}\n\t\tconst auto type = (*selected > 0)\n\t\t\t? ButtonState::Type::Active\n\t\t\t: ButtonState::Type::Disabled;\n\t\tif (buttonState->type == type) {\n\t\t\treturn;\n\t\t}\n\t\tbuttonState->type = type;\n\t\tbox->clearButtons();\n\t\tbox->addButton(tr::lng_cancel(), [=] {\n\t\t\tbox->toggleSelectionMode(false);\n\t\t});\n\t\tauto text = tr::lng_profile_clear_history();\n\t\tconst auto button = box->addLeftButton(std::move(text), [=] {\n\t\t\tconst auto firstDate = box->selectedFirstDate();\n\t\t\tconst auto lastDate = box->selectedLastDate();\n\t\t\tif (!firstDate.isNull()) {\n\t\t\t\tauto confirm = Box<DeleteMessagesBox>(\n\t\t\t\t\thistory->peer,\n\t\t\t\t\tfirstDate,\n\t\t\t\t\tlastDate);\n\t\t\t\tconfirm->setDeleteConfirmedCallback(crl::guard(box, [=] {\n\t\t\t\t\tbox->closeBox();\n\t\t\t\t}));\n\t\t\t\tbox->getDelegate()->show(std::move(confirm));\n\t\t\t}\n\t\t}, (*selected > 0) ? st::attentionBoxButton : buttonState->disabled);\n\t\tif (!*selected) {\n\t\t\tbutton->setPointerCursor(false);\n\t\t}\n\t};\n\tstruct SearchCalendarResult {\n\t\tFn<void(QDate, Ui::CalendarImageSetter)> factory;\n\t\tFn<void(const QDate &, Fn<void()>)> customJump;\n\t};\n\tconst auto searchCalendarResult = [&]() -> SearchCalendarResult {\n\t\tusing Factory = Fn<void(QDate, Ui::CalendarImageSetter)>;\n\t\tusing CustomJump = Fn<void(const QDate &, Fn<void()>)>;\n\t\tif (!descriptor.mediaPhoto && !descriptor.mediaVideo) {\n\t\t\treturn {};\n\t\t}\n\t\tconst auto search = std::make_shared<Api::SearchCalendarController>(\n\t\t\t&session(),\n\t\t\thistory->peer->id,\n\t\t\t(descriptor.mediaPhoto && descriptor.mediaVideo)\n\t\t\t\t? Storage::SharedMediaType::PhotoVideo\n\t\t\t\t: descriptor.mediaPhoto\n\t\t\t\t? Storage::SharedMediaType::Photo\n\t\t\t\t: Storage::SharedMediaType::Video);\n\t\tconst auto factory = [=](QDate date, Ui::CalendarImageSetter set) {\n\t\t\tsearch->monthThumbnails(\n\t\t\t\tbase::unixtime::serialize(QDateTime(date, QTime())),\n\t\t\t\t[=](const std::vector<Api::DayThumbnail> &thumbnails) {\n\t\t\t\t\tfor (const auto &thumb : thumbnails) {\n\t\t\t\t\t\tset(\n\t\t\t\t\t\t\tbase::unixtime::parse(thumb.date).date(),\n\t\t\t\t\t\t\tthumb.image);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t};\n\t\tauto customJump = CustomJump(nullptr);\n\t\tif (const auto performJump = descriptor.customJump) {\n\t\t\tcustomJump = [=](const QDate &d, Fn<void()> close) {\n\t\t\t\tconst auto date = base::unixtime::serialize(\n\t\t\t\t\tQDateTime(d, QTime()));\n\t\t\t\tif (const auto msgId = search->resolveMsgIdByDate(date)) {\n\t\t\t\t\tperformJump(FullMsgId(history->peer->id, *msgId), close);\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t\treturn { Factory(factory), std::move(customJump) };\n\t}();\n\tconst auto weak = base::make_weak(this);\n\tconst auto weakThread = base::make_weak(chat.thread());\n\tconst auto jump = [=](const QDate &date, Fn<void()> close) {\n\t\tconst auto open = [=](not_null<PeerData*> peer, MsgId id) {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tif (performJump) {\n\t\t\t\t\tperformJump(FullMsgId(peer->id, id), close);\n\t\t\t\t} else if (!topic && !sublist) {\n\t\t\t\t\tstrong->showPeerHistory(\n\t\t\t\t\t\tpeer,\n\t\t\t\t\t\tSectionShow::Way::Forward,\n\t\t\t\t\t\tid);\n\t\t\t\t} else if (const auto strongThread = weakThread.get()) {\n\t\t\t\t\tstrong->showThread(\n\t\t\t\t\t\tstrongThread,\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tSectionShow::Way::Forward);\n\t\t\t\t\tstrong->hideLayer(anim::type::normal);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tif ((!topic && !sublist) || weakThread) {\n\t\t\tsession().api().resolveJumpToDate(chat, date, open);\n\t\t}\n\t};\n\tconst auto requireImage = !!searchCalendarResult.customJump;\n\tshow(Box<Ui::CalendarBox>(Ui::CalendarBoxArgs{\n\t\t.month = highlighted,\n\t\t.highlighted = highlighted,\n\t\t.callback = searchCalendarResult.customJump\n\t\t\t? std::move(searchCalendarResult.customJump)\n\t\t\t: jump,\n\t\t.minDate = minPeerDate,\n\t\t.maxDate = maxPeerDate,\n\t\t.allowsSelection = history->peer->isUser(),\n\t\t.selectionChanged = selectionChanged,\n\t\t.dynamicImageForDate = std::move(searchCalendarResult.factory),\n\t\t.requireImage = requireImage,\n\t}));\n}\n\nvoid SessionController::showPassportForm(const Passport::FormRequest &request) {\n\t_passportForm = std::make_unique<Passport::FormController>(\n\t\tthis,\n\t\trequest);\n\t_passportForm->show();\n}\n\nvoid SessionController::clearPassportForm() {\n\t_passportForm = nullptr;\n}\n\nvoid SessionController::showChooseReportMessages(\n\t\tnot_null<PeerData*> peer,\n\t\tData::ReportInput reportInput,\n\t\tFn<void(std::vector<MsgId>)> done) const {\n\tcontent()->showChooseReportMessages(peer, reportInput, std::move(done));\n}\n\nvoid SessionController::clearChooseReportMessages() const {\n\tcontent()->clearChooseReportMessages();\n}\n\nvoid SessionController::showInNewWindow(\n\t\tSeparateId id,\n\t\tMsgId msgId) {\n\tif (!canShowSeparateWindow(id)) {\n\t\tAssert(id.thread != nullptr);\n\t\tshowThread(id.thread, msgId, SectionShow::Way::ClearStack);\n\t\treturn;\n\t}\n\tconst auto active = activeChatCurrent();\n\t// windows check active forum / active archive\n\tconst auto fromActive = active.thread()\n\t\t? (active.thread() == id.thread && id.type == SeparateType::Chat)\n\t\t: false;\n\tconst auto toSeparate = [=] {\n\t\tCore::App().ensureSeparateWindowFor(id, msgId);\n\t};\n\tif (fromActive) {\n\t\twindow().preventOrInvoke([=] {\n\t\t\tclearSectionStack();\n\t\t\ttoSeparate();\n\t\t});\n\t} else {\n\t\ttoSeparate();\n\t}\n}\n\nvoid SessionController::toggleChooseChatTheme(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::optional<bool> show) {\n\tif (showFrozenError()) {\n\t\treturn;\n\t}\n\tcontent()->toggleChooseChatTheme(peer, show);\n}\n\nvoid SessionController::finishChatThemeEdit(not_null<PeerData*> peer) {\n\ttoggleChooseChatTheme(peer, false);\n\tconst auto weak = base::make_weak(this);\n\tconst auto history = activeChatCurrent().history();\n\tif (!history || history->peer != peer) {\n\t\tshowPeerHistory(peer);\n\t}\n\tif (weak) {\n\t\thideLayer();\n\t}\n}\n\nvoid SessionController::updateColumnLayout() const {\n\tcontent()->updateColumnLayout();\n}\n\nvoid SessionController::showPeerHistory(\n\t\tPeerId peerId,\n\t\tconst SectionShow &params,\n\t\tMsgId msgId) {\n\tcontent()->showHistory(peerId, params, msgId);\n}\n\nvoid SessionController::showMessage(\n\t\tnot_null<const HistoryItem*> item,\n\t\tconst SectionShow &params) {\n\t_window->invokeForSessionController(\n\t\t&item->history()->session().account(),\n\t\titem->history()->peer,\n\t\t[&](not_null<SessionController*> controller) {\n\t\t\tif (item->isScheduled()) {\n\t\t\t\tcontroller->showSection(\n\t\t\t\t\tstd::make_shared<HistoryView::ScheduledMemento>(\n\t\t\t\t\t\titem->history()),\n\t\t\t\t\tparams);\n\t\t\t\tif (params.activation != anim::activation::background) {\n\t\t\t\t\tcontroller->window().activate();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcontroller->content()->showMessage(item, params);\n\t\t\t}\n\t\t});\n}\n\nvoid SessionController::cancelUploadLayer(not_null<HistoryItem*> item) {\n\tconst auto itemId = item->fullId();\n\tsession().uploader().pause(itemId);\n\tconst auto stopUpload = [=](Fn<void()> close) {\n\t\tauto &data = session().data();\n\t\tif (const auto item = data.message(itemId)) {\n\t\t\tif (!item->isEditingMedia()) {\n\t\t\t\tconst auto history = item->history();\n\t\t\t\titem->destroy();\n\t\t\t\thistory->requestChatListMessage();\n\t\t\t} else {\n\t\t\t\titem->returnSavedMedia();\n\t\t\t\tsession().uploader().cancel(item->fullId());\n\t\t\t}\n\t\t\tdata.sendHistoryChangeNotifications();\n\t\t}\n\t\tsession().uploader().unpause();\n\t\tclose();\n\t};\n\tconst auto continueUpload = [=](Fn<void()> close) {\n\t\tsession().uploader().unpause();\n\t\tclose();\n\t};\n\n\tshow(Ui::MakeConfirmBox({\n\t\t.text = tr::lng_selected_cancel_sure_this(),\n\t\t.confirmed = stopUpload,\n\t\t.cancelled = continueUpload,\n\t\t.confirmText = tr::lng_box_yes(),\n\t\t.cancelText = tr::lng_box_no(),\n\t}));\n}\n\nvoid SessionController::showSection(\n\t\tstd::shared_ptr<SectionMemento> memento,\n\t\tconst SectionShow &params) {\n\tif (!params.thirdColumn\n\t\t&& widget()->showSectionInExistingLayer(memento.get(), params)) {\n\t\treturn;\n\t}\n\tcontent()->showSection(std::move(memento), params);\n}\n\nvoid SessionController::showBackFromStack(const SectionShow &params) {\n\tconst auto bad = [&] {\n\t\t// If we show a currently-being-destroyed topic, then\n\t\t// skip it and show back one more.\n\t\tconst auto topic = _activeChatEntry.current().key.topic();\n\t\treturn topic && topic->forum()->topicDeleted(topic->rootId());\n\t};\n\tdo {\n\t\tconst auto empty = content()->stackIsEmpty();\n\t\tconst auto shown = content()->showBackFromStack(params);\n\t\tif (empty && !shown && content()->stackIsEmpty() && bad()) {\n\t\t\tclearSectionStack(anim::type::instant);\n\t\t\twindow().close();\n\t\t\tbreak;\n\t\t}\n\t} while (bad());\n}\n\nvoid SessionController::showSpecialLayer(\n\t\tobject_ptr<Ui::LayerWidget> &&layer,\n\t\tanim::type animated) {\n\twidget()->showSpecialLayer(std::move(layer), animated);\n}\n\nvoid SessionController::showLayer(\n\t\tstd::unique_ptr<Ui::LayerWidget> &&layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) {\n\t_window->showLayer(std::move(layer), options, animated);\n}\n\nvoid SessionController::removeLayerBlackout() {\n\twidget()->ui_removeLayerBlackout();\n}\n\nbool SessionController::isLayerShown() const {\n\treturn _window->isLayerShown();\n}\n\nnot_null<MainWidget*> SessionController::content() const {\n\treturn widget()->sessionContent();\n}\n\nint SessionController::filtersWidth() const {\n\treturn _filters ? st::windowFiltersWidth : 0;\n}\n\nbool SessionController::enoughSpaceForFilters() const {\n\treturn widget()->width() >= widget()->minimumWidth() + st::windowFiltersWidth;\n}\n\nrpl::producer<bool> SessionController::enoughSpaceForFiltersValue() const {\n\treturn widget()->widthValue() | rpl::map([=] {\n\t\treturn enoughSpaceForFilters();\n\t}) | rpl::distinct_until_changed();\n}\n\nrpl::producer<FilterId> SessionController::activeChatsFilter() const {\n\treturn _activeChatsFilter.value();\n}\n\nFilterId SessionController::activeChatsFilterCurrent() const {\n\treturn _activeChatsFilter.current();\n}\n\nvoid SessionController::setActiveChatsFilter(\n\t\tFilterId id,\n\t\tconst SectionShow &params) {\n\tif (!isPrimary()) {\n\t\treturn;\n\t}\n\tconst auto changed = (activeChatsFilterCurrent() != id);\n\tif (changed) {\n\t\tresetFakeUnreadWhileOpened();\n\t}\n\t_activeChatsFilter.force_assign(id);\n\tif (id || !changed) {\n\t\tcloseForum();\n\t\tcloseFolder();\n\t}\n\tif (adaptive().isOneColumn()) {\n\t\tclearSectionStack(params);\n\t}\n}\n\nvoid SessionController::showAddContact() {\n\t_window->show(Box<AddContactBox>(&session()));\n}\n\nvoid SessionController::showNewGroup() {\n\t_window->show(Box<GroupInfoBox>(this, GroupInfoBox::Type::Group));\n}\n\nvoid SessionController::showNewChannel() {\n\t_window->show(Box<GroupInfoBox>(this, GroupInfoBox::Type::Channel));\n}\n\nWindow::Adaptive &SessionController::adaptive() const {\n\treturn _window->adaptive();\n}\n\nvoid SessionController::setConnectingBottomSkip(int skip) {\n\t_connectingBottomSkip = skip;\n}\n\nrpl::producer<int> SessionController::connectingBottomSkipValue() const {\n\treturn _connectingBottomSkip.value();\n}\n\nvoid SessionController::stickerOrEmojiChosen(FileChosen chosen) {\n\t_stickerOrEmojiChosen.fire(std::move(chosen));\n}\n\nauto SessionController::stickerOrEmojiChosen() const\n-> rpl::producer<FileChosen> {\n\treturn _stickerOrEmojiChosen.events();\n}\n\nbase::weak_qptr<Ui::BoxContent> SessionController::show(\n\t\tobject_ptr<Ui::BoxContent> content,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated) {\n\treturn _window->show(std::move(content), options, animated);\n}\n\nvoid SessionController::hideLayer(anim::type animated) {\n\t_window->hideLayer(animated);\n}\n\nbool SessionController::openPhotoExternal(\n\t\tnot_null<PhotoData*> photo,\n\t\tData::FileOrigin origin) {\n\tif (!OptionExternalMediaViewer.value()) {\n\t\treturn false;\n\t}\n\tconst auto media = photo->createMediaView();\n\tconst auto existing = photo->location(true).name();\n\tif (!existing.isEmpty()) {\n\t\tFile::Launch(existing);\n\t\treturn true;\n\t}\n\tconst auto filepath = FileNameForSave(\n\t\t&session(),\n\t\ttr::lng_save_photo(tr::now),\n\t\tu\"JPEG Image (*.jpg);;\"_q + FileDialog::AllFilesFilter(),\n\t\tu\"photo\"_q,\n\t\tu\".jpg\"_q,\n\t\tfalse);\n\tif (media->loaded()) {\n\t\tif (media->saveToFile(filepath)) {\n\t\t\tphoto->setLocation(Core::FileLocation(filepath));\n\t\t\tFile::Launch(filepath);\n\t\t}\n\t\treturn true;\n\t}\n\t_pendingOpenPhoto = { photo, media, filepath };\n\tphoto->load(origin, LoadFromCloudOrLocal, true);\n\treturn true;\n}\n\nvoid SessionController::openPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tMessageContext message,\n\t\tconst Data::StoriesContext *stories) {\n\tconst auto item = session().data().message(message.id);\n\tif (openSharedStory(item) || openFakeItemStory(message.id, stories)) {\n\t\treturn;\n\t}\n\tconst auto origin = item\n\t\t? Data::FileOrigin(item->fullId())\n\t\t: Data::FileOrigin();\n\tif (openPhotoExternal(photo, origin)) {\n\t\treturn;\n\t}\n\t_window->openInMediaView(Media::View::OpenRequest(\n\t\tthis,\n\t\tphoto,\n\t\titem,\n\t\tmessage.topicRootId,\n\t\tmessage.monoforumPeerId,\n\t\tmessage.showDrawButton));\n}\n\nvoid SessionController::openPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tnot_null<PeerData*> peer) {\n\tconst auto origin = peer->isUser()\n\t\t? Data::FileOrigin(Data::FileOriginUserPhoto(\n\t\t\tpeerToUser(peer->id),\n\t\t\tphoto->id))\n\t\t: Data::FileOrigin(Data::FileOriginPeerPhoto(peer->id));\n\tif (openPhotoExternal(photo, origin)) {\n\t\treturn;\n\t}\n\t_window->openInMediaView(Media::View::OpenRequest(this, photo, peer));\n}\n\nvoid SessionController::openDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tbool showInMediaView,\n\t\tMessageContext message,\n\t\tconst Data::StoriesContext *stories,\n\t\tstd::optional<TimeId> videoTimestampOverride) {\n\tconst auto item = session().data().message(message.id);\n\tif (openSharedStory(item) || openFakeItemStory(message.id, stories)) {\n\t\treturn;\n\t} else if (showInMediaView) {\n\t\tif (OptionExternalMediaViewer.value() && !document->isTheme()) {\n\t\t\tconst auto filepath = document->filepath();\n\t\t\tif (filepath.isEmpty()) {\n\t\t\t\tif (document->loadedInMediaCache()) {\n\t\t\t\t\t_pendingOpenDocumentId = document->id;\n\t\t\t\t}\n\t\t\t\tDocumentSaveClickHandler::Save(\n\t\t\t\t\tmessage.id,\n\t\t\t\t\tdocument,\n\t\t\t\t\tDocumentSaveClickHandler::Mode::ToFile);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tFile::Launch(filepath);\n\t\t\treturn;\n\t\t}\n\t\tusing namespace Media::View;\n\t\tconst auto saved = session().local().mediaLastPlaybackPosition(\n\t\t\tdocument->id);\n\t\tconst auto timestamp = item ? ExtractVideoTimestamp(item) : 0;\n\t\tconst auto usedTimestamp = videoTimestampOverride\n\t\t\t? ((*videoTimestampOverride) * crl::time(1000))\n\t\t\t: saved\n\t\t\t? saved\n\t\t\t: timestamp\n\t\t\t? (timestamp * crl::time(1000))\n\t\t\t: crl::time();\n\t\t_window->openInMediaView(OpenRequest(\n\t\t\tthis,\n\t\t\tdocument,\n\t\t\titem,\n\t\t\tmessage.topicRootId,\n\t\t\tmessage.monoforumPeerId,\n\t\t\tfalse,\n\t\t\tusedTimestamp,\n\t\t\tmessage.showDrawButton));\n\t\treturn;\n\t}\n\tData::ResolveDocument(\n\t\tthis,\n\t\tdocument,\n\t\titem,\n\t\tmessage.topicRootId,\n\t\tmessage.monoforumPeerId,\n\t\tmessage.showDrawButton);\n}\n\nbool SessionController::openSharedStory(HistoryItem *item) {\n\tif (const auto media = item ? item->media() : nullptr) {\n\t\tif (const auto storyId = media->storyId()) {\n\t\t\tconst auto story = session().data().stories().lookup(storyId);\n\t\t\tif (story) {\n\t\t\t\t_window->openInMediaView(::Media::View::OpenRequest(\n\t\t\t\t\tthis,\n\t\t\t\t\t*story,\n\t\t\t\t\tData::StoriesContext{ Data::StoriesContextSingle() }));\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nbool SessionController::openFakeItemStory(\n\t\tFullMsgId fakeItemId,\n\t\tconst Data::StoriesContext *stories) {\n\tif (peerIsChat(fakeItemId.peer)\n\t\t|| !IsStoryMsgId(fakeItemId.msg)) {\n\t\treturn false;\n\t}\n\tconst auto maybeStory = session().data().stories().lookup({\n\t\tfakeItemId.peer,\n\t\tStoryIdFromMsgId(fakeItemId.msg),\n\t});\n\tif (maybeStory) {\n\t\tusing namespace Data;\n\t\tconst auto story = *maybeStory;\n\t\tconst auto context = stories\n\t\t\t? *stories\n\t\t\t: StoriesContext{ StoriesContextSingle() };\n\t\t_window->openInMediaView(\n\t\t\t::Media::View::OpenRequest(this, story, context));\n\t}\n\treturn true;\n}\n\nauto SessionController::cachedChatThemeValue(\n\tconst Data::CloudTheme &data,\n\tconst Data::WallPaper &paper,\n\tData::CloudThemeType type)\n-> rpl::producer<std::shared_ptr<Ui::ChatTheme>> {\n\tconst auto themeKey = Ui::ChatThemeKey{\n\t\tdata.id,\n\t\t(type == Data::CloudThemeType::Dark),\n\t};\n\tif (!themeKey && paper.isNull()) {\n\t\treturn rpl::single(_defaultChatTheme);\n\t}\n\tconst auto settings = data.settings.find(type);\n\tif (data.id && settings == end(data.settings)) {\n\t\treturn rpl::single(_defaultChatTheme);\n\t}\n\tif (paper.isNull()\n\t\t&& (!settings->second.paper\n\t\t\t|| settings->second.paper->backgroundColors().empty())) {\n\t\treturn rpl::single(_defaultChatTheme);\n\t}\n\tconst auto key = CachedThemeKey{\n\t\tthemeKey,\n\t\t!paper.isNull() ? paper.key() : settings->second.paper->key(),\n\t};\n\tconst auto i = _customChatThemes.find(key);\n\tif (i != end(_customChatThemes)) {\n\t\tif (auto strong = i->second.theme.lock()) {\n\t\t\tpushLastUsedChatTheme(strong);\n\t\t\treturn rpl::single(std::move(strong));\n\t\t}\n\t}\n\tif (i == end(_customChatThemes) || !i->second.caching) {\n\t\tcacheChatTheme(key, data, paper, type);\n\t}\n\tconst auto limit = Data::CloudThemes::TestingColors() ? (1 << 20) : 1;\n\tusing namespace rpl::mappers;\n\treturn rpl::single(\n\t\t_defaultChatTheme\n\t) | rpl::then(_cachedThemesStream.events(\n\t) | rpl::filter([=](const std::shared_ptr<Ui::ChatTheme> &theme) {\n\t\tif (theme->key() != key.theme\n\t\t\t|| theme->background().key != key.paper) {\n\t\t\treturn false;\n\t\t}\n\t\tpushLastUsedChatTheme(theme);\n\t\treturn true;\n\t}) | rpl::take(limit));\n}\n\nbool SessionController::chatThemeAlreadyCached(\n\t\tconst Data::CloudTheme &data,\n\t\tconst Data::WallPaper &paper,\n\t\tData::CloudThemeType type) {\n\tExpects(paper.document() != nullptr);\n\n\tconst auto key = CachedThemeKey{\n\t\tUi::ChatThemeKey{\n\t\t\tdata.id,\n\t\t\t(type == Data::CloudThemeType::Dark),\n\t\t},\n\t\tpaper.key(),\n\t};\n\tconst auto i = _customChatThemes.find(key);\n\treturn (i != end(_customChatThemes))\n\t\t&& (i->second.theme.lock() != nullptr);\n}\n\nvoid SessionController::pushLastUsedChatTheme(\n\t\tconst std::shared_ptr<Ui::ChatTheme> &theme) {\n\tconst auto i = ranges::find(_lastUsedCustomChatThemes, theme);\n\tif (i == end(_lastUsedCustomChatThemes)) {\n\t\tif (_lastUsedCustomChatThemes.size() >= kCustomThemesInMemory) {\n\t\t\t_lastUsedCustomChatThemes.pop_back();\n\t\t}\n\t\t_lastUsedCustomChatThemes.push_front(theme);\n\t} else if (i != begin(_lastUsedCustomChatThemes)) {\n\t\tstd::rotate(begin(_lastUsedCustomChatThemes), i, i + 1);\n\t}\n}\n\nnot_null<Ui::ChatTheme*> SessionController::currentChatTheme() const {\n\tif (const auto custom = content()->customChatTheme()) {\n\t\treturn custom;\n\t}\n\treturn defaultChatTheme().get();\n}\n\nvoid SessionController::setChatStyleTheme(\n\t\tconst std::shared_ptr<Ui::ChatTheme> &theme) {\n\tif (_chatStyleTheme.lock() == theme) {\n\t\treturn;\n\t}\n\t_chatStyleTheme = theme;\n\t_chatStyle->apply(theme.get());\n}\n\nvoid SessionController::clearCachedChatThemes() {\n\t_customChatThemes.clear();\n}\n\nvoid SessionController::overridePeerTheme(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<Ui::ChatTheme> theme,\n\t\tQString token) {\n\t_peerThemeOverride = PeerThemeOverride{\n\t\tpeer,\n\t\ttheme ? theme : _defaultChatTheme,\n\t\ttoken,\n\t};\n}\n\nvoid SessionController::clearPeerThemeOverride(not_null<PeerData*> peer) {\n\tif (_peerThemeOverride.current().peer == peer.get()) {\n\t\t_peerThemeOverride = PeerThemeOverride();\n\t}\n}\n\nvoid SessionController::pushDefaultChatBackground() {\n\tconst auto background = Theme::Background();\n\tconst auto &paper = background->paper();\n\t_defaultChatTheme->setBackground({\n\t\t.prepared = background->prepared(),\n\t\t.preparedForTiled = background->preparedForTiled(),\n\t\t.gradientForFill = background->gradientForFill(),\n\t\t.colorForFill = background->colorForFill(),\n\t\t.colors = paper.backgroundColors(),\n\t\t.patternOpacity = paper.patternOpacity(),\n\t\t.gradientRotation = paper.gradientRotation(),\n\t\t.isPattern = paper.isPattern(),\n\t\t.tile = background->tile(),\n\t});\n}\n\nvoid SessionController::cacheChatTheme(\n\t\tCachedThemeKey key,\n\t\tconst Data::CloudTheme &data,\n\t\tconst Data::WallPaper &paper,\n\t\tData::CloudThemeType type) {\n\tExpects(data.id != 0 || !paper.isNull());\n\n\tconst auto dark = (type == Data::CloudThemeType::Dark);\n\tconst auto i = data.settings.find(type);\n\tAssert((!data.id || (i != end(data.settings)))\n\t\t&& (!paper.isNull()\n\t\t\t|| (i->second.paper.has_value()\n\t\t\t\t&& !i->second.paper->backgroundColors().empty())));\n\tconst auto &use = !paper.isNull() ? paper : *i->second.paper;\n\tconst auto document = use.document();\n\tconst auto media = document ? document->createMediaView() : nullptr;\n\tconst auto findGiftSymbols = (data.unique != nullptr);\n\tconst auto reportSymbolLoaded = [weak = base::make_weak(this)] {\n\t\t// We must notify async here, because we destroy emoji in\n\t\t// the handler and we can't destroy emoji in repaint callback.\n\t\tcrl::on_main([weak] {\n\t\t\tif (const auto strong = weak.get()) {\n\t\t\t\tstrong->_giftSymbolLoaded.fire({});\n\t\t\t}\n\t\t});\n\t};\n\tauto giftSymbol = findGiftSymbols\n\t\t? session().data().customEmojiManager().create(\n\t\t\tdata.unique->pattern.document,\n\t\t\treportSymbolLoaded,\n\t\t\tData::CustomEmojiSizeTag::Large)\n\t\t: nullptr;\n\tconst auto giftId = findGiftSymbols\n\t\t? data.unique->model.document->id\n\t\t: uint64();\n\tconst auto giftSymbolReady = !giftSymbol || giftSymbol->ready();\n\tuse.loadDocument();\n\tauto &theme = [&]() -> CachedTheme& {\n\t\tconst auto i = _customChatThemes.find(key);\n\t\tif (i != end(_customChatThemes)) {\n\t\t\ti->second.media = media;\n\t\t\ti->second.giftSymbol = std::move(giftSymbol);\n\t\t\ti->second.giftId = giftId;\n\t\t\ti->second.paper = use;\n\t\t\ti->second.basedOnDark = dark;\n\t\t\ti->second.caching = true;\n\t\t\treturn i->second;\n\t\t}\n\t\treturn _customChatThemes.emplace(\n\t\t\tkey,\n\t\t\tCachedTheme{\n\t\t\t\t.media = media,\n\t\t\t\t.giftSymbol = std::move(giftSymbol),\n\t\t\t\t.giftId = giftId,\n\t\t\t\t.paper = use,\n\t\t\t\t.basedOnDark = dark,\n\t\t\t\t.caching = true,\n\t\t\t}).first->second;\n\t}();\n\tauto descriptor = Ui::ChatThemeDescriptor{\n\t\t.key = key.theme,\n\t\t.preparePalette = (data.id\n\t\t\t? Theme::PreparePaletteCallback(dark, i->second.accentColor)\n\t\t\t: Theme::PrepareCurrentPaletteCallback()),\n\t\t.backgroundData = backgroundData(theme),\n\t\t.bubblesData = PrepareBubblesData(data, type),\n\t\t.basedOnDark = dark,\n\t};\n\tcrl::async([\n\t\tthis,\n\t\tdescriptor = std::move(descriptor),\n\t\tweak = base::make_weak(this)\n\t]() mutable {\n\t\tcrl::on_main(weak,[\n\t\t\tthis,\n\t\t\tresult = std::make_shared<Ui::ChatTheme>(std::move(descriptor))\n\t\t]() mutable {\n\t\t\tresult->finishCreateOnMain();\n\t\t\tcacheChatThemeDone(std::move(result));\n\t\t});\n\t});\n\tif (media && media->loaded(true)) {\n\t\ttheme.media = nullptr;\n\t\tif (giftSymbolReady) {\n\t\t\ttheme.giftSymbol = nullptr;\n\t\t}\n\t}\n}\n\nvoid SessionController::cacheChatThemeDone(\n\t\tstd::shared_ptr<Ui::ChatTheme> result) {\n\tExpects(result != nullptr);\n\n\tconst auto key = CachedThemeKey{\n\t\tresult->key(),\n\t\tresult->background().key,\n\t};\n\tconst auto i = _customChatThemes.find(key);\n\tif (i == end(_customChatThemes)) {\n\t\treturn;\n\t}\n\ti->second.caching = false;\n\ti->second.theme = result;\n\tconst auto media = i->second.media.get();\n\tconst auto giftSymbol = i->second.giftSymbol.get();\n\tif (media || giftSymbol) {\n\t\tif ((!media || media->loaded(true))\n\t\t\t&& (!giftSymbol || giftSymbol->ready())) {\n\t\t\tupdateCustomThemeBackground(i->second);\n\t\t} else {\n\t\t\trpl::merge(\n\t\t\t\tsession().downloaderTaskFinished(),\n\t\t\t\t((giftSymbol && !giftSymbol->ready())\n\t\t\t\t\t? (_giftSymbolLoaded.events() | rpl::type_erased)\n\t\t\t\t\t: rpl::never<rpl::empty_value>())\n\t\t\t) | rpl::filter([=] {\n\t\t\t\tconst auto i = _customChatThemes.find(key);\n\t\t\t\tAssert(i != end(_customChatThemes));\n\t\t\t\tconst auto media = i->second.media.get();\n\t\t\t\tconst auto giftSymbol = i->second.giftSymbol.get();\n\t\t\t\treturn (!media || media->loaded(true))\n\t\t\t\t\t&& (!giftSymbol || giftSymbol->ready());\n\t\t\t}) | rpl::on_next([=] {\n\t\t\t\tconst auto i = _customChatThemes.find(key);\n\t\t\t\tAssert(i != end(_customChatThemes));\n\t\t\t\tupdateCustomThemeBackground(i->second);\n\t\t\t}, i->second.lifetime);\n\t\t}\n\t}\n\t_cachedThemesStream.fire(std::move(result));\n}\n\nvoid SessionController::updateCustomThemeBackground(CachedTheme &theme) {\n\tconst auto guard = gsl::finally([&] {\n\t\ttheme.lifetime.destroy();\n\t\ttheme.media = nullptr;\n\t\ttheme.giftSymbol = nullptr;\n\t});\n\tconst auto strong = theme.theme.lock();\n\tconst auto media = theme.media.get();\n\tconst auto giftSymbol = theme.giftSymbol.get();\n\tif (!strong\n\t\t|| (!media && !giftSymbol)\n\t\t|| (media && !media->loaded(true))\n\t\t|| (giftSymbol && !giftSymbol->ready())) {\n\t\treturn;\n\t}\n\tconst auto key = strong->key();\n\tconst auto weak = base::make_weak(this);\n\tcrl::async([=, data = backgroundData(theme, false)] {\n\t\tcrl::on_main(weak, [\n\t\t\t=,\n\t\t\tresult = Ui::PrepareBackgroundImage(data)\n\t\t]() mutable {\n\t\t\tconst auto cacheKey = CachedThemeKey{ key, result.key };\n\t\t\tconst auto i = _customChatThemes.find(cacheKey);\n\t\t\tif (i != end(_customChatThemes)) {\n\t\t\t\tif (const auto strong = i->second.theme.lock()) {\n\t\t\t\t\tstrong->updateBackgroundImageFrom(std::move(result));\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n}\n\nUi::ChatThemeBackgroundData SessionController::backgroundData(\n\t\tCachedTheme &theme,\n\t\tbool generateGradient) const {\n\tconst auto &paper = theme.paper;\n\tconst auto &media = theme.media;\n\tconst auto paperPath = media ? media->owner()->filepath() : QString();\n\tconst auto paperBytes = media ? media->bytes() : QByteArray();\n\tconst auto gzipSvg = media && media->owner()->isPatternWallPaperSVG();\n\tconst auto &colors = paper.backgroundColors();\n\tconst auto isPattern = paper.isPattern();\n\tconst auto patternOpacity = paper.patternOpacity();\n\tconst auto isBlurred = paper.isBlurred();\n\tconst auto gradientRotation = paper.gradientRotation();\n\tconst auto darkModeDimming = isPattern\n\t\t? 100\n\t\t: std::clamp(paper.patternIntensity(), 0, 100);\n\treturn {\n\t\t.key = paper.key(),\n\t\t.path = paperPath,\n\t\t.bytes = paperBytes,\n\t\t.giftSymbolFrame = Ui::PrepareGiftSymbol(theme.giftSymbol),\n\t\t.giftId = theme.giftId,\n\t\t.gzipSvg = gzipSvg,\n\t\t.colors = colors,\n\t\t.isPattern = isPattern,\n\t\t.patternOpacity = patternOpacity,\n\t\t.darkModeDimming = darkModeDimming,\n\t\t.isBlurred = isBlurred,\n\t\t.forDarkMode = theme.basedOnDark,\n\t\t.generateGradient = generateGradient,\n\t\t.gradientRotation = gradientRotation,\n\t};\n}\n\nvoid SessionController::openPeerStory(\n\t\tnot_null<PeerData*> peer,\n\t\tStoryId storyId,\n\t\tData::StoriesContext context) {\n\tusing namespace Media::View;\n\tusing namespace Data;\n\n\tinvalidate_weak_ptrs(&_storyOpenGuard);\n\tauto &stories = session().data().stories();\n\tconst auto from = stories.lookup({ peer->id, storyId });\n\tif (from) {\n\t\twindow().openInMediaView(OpenRequest(this, *from, context));\n\t} else if (from.error() == Data::NoStory::Unknown) {\n\t\tconst auto done = crl::guard(&_storyOpenGuard, [=] {\n\t\t\topenPeerStory(peer, storyId, context);\n\t\t});\n\t\tstories.resolve({ peer->id, storyId }, done);\n\t}\n}\n\nvoid SessionController::openPeerStories(\n\t\tPeerId peerId,\n\t\tstd::optional<Data::StorySourcesList> list,\n\t\tbool onlyLive,\n\t\tbool afterReload) {\n\tusing namespace Media::View;\n\tusing namespace Data;\n\n\tinvalidate_weak_ptrs(&_storyOpenGuard);\n\tauto &stories = session().data().stories();\n\tif (const auto source = stories.source(peerId)) {\n\t\tif (const auto idDates = source->toOpen()) {\n\t\t\tif (onlyLive && !idDates.videoStream) {\n\t\t\t\tshowToast(tr::lng_stories_live_finished(tr::now));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\topenPeerStory(\n\t\t\t\tsource->peer,\n\t\t\t\tidDates.id,\n\t\t\t\t(list\n\t\t\t\t\t? StoriesContext{ *list }\n\t\t\t\t\t: StoriesContext{ StoriesContextPeer() }));\n\t\t} else if (onlyLive) {\n\t\t\tshowToast(tr::lng_stories_live_finished(tr::now));\n\t\t}\n\t} else if (afterReload) {\n\t\tif (onlyLive) {\n\t\t\tshowToast(tr::lng_stories_live_finished(tr::now));\n\t\t}\n\t} else if (const auto peer = session().data().peerLoaded(peerId)) {\n\t\tconst auto done = crl::guard(&_storyOpenGuard, [=] {\n\t\t\topenPeerStories(peerId, list, onlyLive, true);\n\t\t});\n\t\tstories.requestPeerStories(peer, done);\n\t}\n}\n\nHistoryView::PaintContext SessionController::preparePaintContext(\n\t\tUi::ChatPaintContextArgs &&args) {\n\tconst auto visibleAreaTopLocal = content()->mapFromGlobal(\n\t\targs.visibleAreaPositionGlobal).y();\n\tconst auto area = QRect(\n\t\t0,\n\t\targs.visibleAreaTop,\n\t\targs.visibleAreaWidth,\n\t\targs.visibleAreaHeight);\n\tconst auto viewport = QRect(\n\t\t0,\n\t\targs.visibleAreaTop - visibleAreaTopLocal,\n\t\targs.visibleAreaWidth,\n\t\tcontent()->height());\n\treturn args.theme->preparePaintContext(\n\t\t_chatStyle.get(),\n\t\tviewport,\n\t\tarea,\n\t\targs.clip,\n\t\tisGifPausedAtLeastFor(GifPauseReason::Any));\n}\n\nvoid SessionController::setPremiumRef(const QString &ref) {\n\t_premiumRef = ref;\n}\n\nQString SessionController::premiumRef() const {\n\treturn _premiumRef;\n}\n\nbool SessionController::showChatPreview(\n\t\tDialogs::RowDescriptor row,\n\t\tFn<void(bool shown)> callback,\n\t\tQPointer<QWidget> parentOverride,\n\t\tstd::optional<QPoint> positionOverride) {\n\treturn _chatPreviewManager->show(\n\t\tstd::move(row),\n\t\tstd::move(callback),\n\t\tstd::move(parentOverride),\n\t\tpositionOverride);\n}\n\nbool SessionController::scheduleChatPreview(\n\t\tDialogs::RowDescriptor row,\n\t\tFn<void(bool shown)> callback,\n\tQPointer<QWidget> parentOverride,\n\tstd::optional<QPoint> positionOverride) {\n\treturn _chatPreviewManager->schedule(\n\t\tstd::move(row),\n\t\tstd::move(callback),\n\t\tstd::move(parentOverride),\n\t\tpositionOverride);\n}\n\nvoid SessionController::cancelScheduledPreview() {\n\t_chatPreviewManager->cancelScheduled();\n}\n\nbool SessionController::contentOverlapped(QWidget *w, QPaintEvent *e) const {\n\treturn widget()->contentOverlapped(w, e);\n}\n\nstd::shared_ptr<ChatHelpers::Show> SessionController::uiShow() {\n\tif (!_cachedShow) {\n\t\t_cachedShow = std::make_shared<MainWindowShow>(this);\n\t}\n\treturn _cachedShow;\n}\n\nvoid SessionController::saveSubsectionTabs(\n\t\tstd::unique_ptr<HistoryView::SubsectionTabs> tabs) {\n\t_savedSubsectionTabsLifetime.destroy();\n\t_savedSubsectionTabs = std::move(tabs);\n\t_savedSubsectionTabs->extractToParent(widget());\n\t_savedSubsectionTabs->removeRequests() | rpl::on_next([=] {\n\t\t_savedSubsectionTabs = nullptr;\n\t}, _savedSubsectionTabsLifetime);\n}\n\nauto SessionController::restoreSubsectionTabsFor(\n\tnot_null<Ui::RpWidget*> parent,\n\tnot_null<Data::Thread*> thread)\n-> std::unique_ptr<HistoryView::SubsectionTabs> {\n\tif (!_savedSubsectionTabs) {\n\t\treturn nullptr;\n\t} else if (_savedSubsectionTabs->switchTo(thread, parent)) {\n\t\t_savedSubsectionTabsLifetime.destroy();\n\t\treturn base::take(_savedSubsectionTabs);\n\t}\n\treturn nullptr;\n}\n\nvoid SessionController::dropSubsectionTabs() {\n\t_savedSubsectionTabsLifetime.destroy();\n\tbase::take(_savedSubsectionTabs);\n}\n\nvoid SessionController::showStarGiftAuction(const QString &slug) {\n\t_starGiftAuctionLifetime.destroy();\n\n\tconst auto requesting = _starGiftAuctionLifetime.make_state<\n\t\tbase::has_weak_ptr\n\t>();\n\tconst auto guard = base::make_weak(requesting);\n\tconst auto weak = base::make_weak(this);\n\tsession().giftAuctions().resolveSlug(slug, [=](uint64 giftId) {\n\t\tif (!guard || !weak) {\n\t\t\treturn;\n\t\t}\n\t\t_starGiftAuctionLifetime.destroy();\n\t\tif (giftId) {\n\t\t\tshowStarGiftAuction(giftId);\n\t\t}\n\t});\n}\n\nvoid SessionController::showStarGiftAuction(uint64 giftId) {\n\t_starGiftAuctionLifetime.destroy();\n\t_starGiftAuctionLifetime = Ui::ShowStarGiftAuction(\n\t\tthis,\n\t\tnullptr,\n\t\tgiftId,\n\t\t[] {},\n\t\t[=] { _starGiftAuctionLifetime.destroy(); });\n}\n\nvoid SessionController::showCloudPassword(const QString &highlight) {\n\tif (!highlight.isEmpty()) {\n\t\tsetHighlightControlId(highlight);\n\t}\n\tsession().api().cloudPassword().reload();\n\n\tenum class PasswordState {\n\t\tLoading,\n\t\tOn,\n\t\tOff,\n\t\tUnconfirmed,\n\t};\n\t_showCloudPasswordLifetime = rpl::single(\n\t\tPasswordState::Loading\n\t) | rpl::then(session().api().cloudPassword().state(\n\t) | rpl::map([](const Core::CloudPasswordState &state) {\n\t\treturn (!state.unconfirmedPattern.isEmpty())\n\t\t\t? PasswordState::Unconfirmed\n\t\t\t: state.hasPassword\n\t\t\t? PasswordState::On\n\t\t\t: PasswordState::Off;\n\t})) | rpl::distinct_until_changed() | rpl::filter(\n\t\trpl::mappers::_1 != PasswordState::Loading\n\t) | rpl::take(1) | rpl::on_next([=](PasswordState state) {\n\t\tif (state == PasswordState::On) {\n\t\t\tshowSettings(Settings::CloudPasswordInputId());\n\t\t} else if (state == PasswordState::Off) {\n\t\t\tshowSettings(Settings::CloudPasswordStartId());\n\t\t} else if (state == PasswordState::Unconfirmed) {\n\t\t\tshowSettings(Settings::CloudPasswordEmailConfirmId());\n\t\t}\n\t});\n}\n\nvoid SessionController::setHighlightControlId(const QString &id) {\n\t_window->setHighlightControlId(id);\n}\n\nQString SessionController::highlightControlId() const {\n\treturn _window->highlightControlId();\n}\n\nbool SessionController::takeHighlightControlId(const QString &id) {\n\treturn _window->takeHighlightControlId(id);\n}\n\nvoid SessionController::checkHighlightControl(\n\t\tconst QString &id,\n\t\tQWidget *widget,\n\t\tSettings::HighlightArgs &&args) {\n\t_window->checkHighlightControl(id, widget, std::move(args));\n}\n\nvoid SessionController::checkHighlightControl(\n\t\tconst QString &id,\n\t\tQWidget *widget) {\n\t_window->checkHighlightControl(id, widget);\n}\n\nSessionController::~SessionController() {\n\tresetFakeUnreadWhileOpened();\n\tdropSubsectionTabs();\n}\n\nbool CheckAndJumpToNearChatsFilter(\n\t\tnot_null<SessionController*> controller,\n\t\tbool isNext,\n\t\tbool jump) {\n\tconst auto id = controller->activeChatsFilterCurrent();\n\tconst auto session = &controller->session();\n\tconst auto list = &session->data().chatsFilters().list();\n\tconst auto index = int(ranges::find(\n\t\t*list,\n\t\tid,\n\t\t&Data::ChatFilter::id\n\t) - begin(*list));\n\tif (index == list->size() && id != 0) {\n\t\treturn false;\n\t}\n\tconst auto changed = index + (isNext ? 1 : -1);\n\tif (changed >= int(list->size()) || changed < 0) {\n\t\treturn false;\n\t}\n\tif (changed > Data::PremiumLimits(session).dialogFiltersCurrent()) {\n\t\treturn false;\n\t}\n\tif (jump) {\n\t\tcontroller->setActiveChatsFilter((changed >= 0)\n\t\t\t? (*list)[changed].id()\n\t\t\t: 0);\n\t}\n\treturn true;\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_session_controller.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"base/timer.h\"\n#include \"chat_helpers/compose/compose_show.h\"\n#include \"data/data_chat_participant_status.h\"\n#include \"data/data_report.h\"\n#include \"dialogs/dialogs_key.h\"\n#include \"mtproto/sender.h\"\n#include \"settings/settings_type.h\"\n#include \"window/window_adaptive.h\"\n\nclass PhotoData;\nclass MainWidget;\nclass MainWindow;\n\nnamespace Adaptive {\nenum class WindowLayout;\n} // namespace Adaptive\n\nnamespace Data {\nstruct StoriesContext;\nstruct DrawToReplyRequest;\nclass SavedMessages;\nenum class StorySourcesList : uchar;\n} // namespace Data\n\nnamespace Dialogs {\nstruct SearchState;\n} // namespace Dialogs\n\nnamespace ChatHelpers {\nclass TabbedSelector;\nclass EmojiInteractions;\nstruct FileChosen;\n} // namespace ChatHelpers\n\nnamespace Main {\nclass Session;\n} // namespace Main\n\nnamespace InlineBots {\nclass AttachWebView;\nenum class PeerType : uint8;\nusing PeerTypes = base::flags<PeerType>;\n} // namespace InlineBots\n\nnamespace Calls {\nstruct StartGroupCallArgs;\n} // namespace Calls\n\nnamespace Passport {\nstruct FormRequest;\nclass FormController;\n} // namespace Passport\n\nnamespace Ui {\nclass LayerWidget;\nclass ChatStyle;\nclass ChatTheme;\nstruct ChatThemeKey;\nstruct ChatPaintContext;\nstruct ChatThemeBackground;\nstruct ChatThemeBackgroundData;\nclass MessageSendingAnimationController;\nstruct BoostCounters;\nstruct ChatPaintContextArgs;\nstruct PreparedList;\nstruct PreparedBundle;\n} // namespace Ui\n\nnamespace Api {\nstruct SendOptions;\n} // namespace Api\n\nnamespace Data {\nstruct CloudTheme;\nenum class CloudThemeType;\nclass PhotoMedia;\nclass Thread;\nclass Forum;\nclass ForumTopic;\nclass SavedSublist;\nclass WallPaper;\n} // namespace Data\n\nnamespace HistoryView {\nclass SubsectionTabs;\n} // namespace HistoryView\n\nnamespace HistoryView::Reactions {\nclass CachedIconFactory;\n} // namespace HistoryView::Reactions\n\nnamespace Settings {\nstruct HighlightArgs;\n} // namespace Settings\n\nnamespace Window {\n\nusing GifPauseReason = ChatHelpers::PauseReason;\nusing GifPauseReasons = ChatHelpers::PauseReasons;\n\nclass SectionMemento;\nclass Controller;\nclass FiltersMenu;\nclass ChatPreviewManager;\nclass ChatSwitchProcess;\n\nstruct PeerByLinkInfo;\nstruct SeparateId;\n\nextern const char kOptionExternalMediaViewer[];\n\nstruct PeerThemeOverride {\n\tPeerData *peer = nullptr;\n\tstd::shared_ptr<Ui::ChatTheme> theme;\n\tQString token;\n};\nbool operator==(const PeerThemeOverride &a, const PeerThemeOverride &b);\nbool operator!=(const PeerThemeOverride &a, const PeerThemeOverride &b);\n\nclass DateClickHandler : public ClickHandler {\npublic:\n\tDateClickHandler(Dialogs::Key chat, QDate date);\n\n\tvoid setDate(QDate date);\n\tvoid onClick(ClickContext context) const override;\n\nprivate:\n\tDialogs::Key _chat;\n\tbase::weak_ptr<Data::ForumTopic> _weak;\n\tQDate _date;\n\n};\n\nclass ForumThreadClickHandler : public ClickHandler {\npublic:\n\texplicit ForumThreadClickHandler(not_null<HistoryItem*> item);\n\n\tvoid update(not_null<HistoryItem*> item);\n\tvoid onClick(ClickContext context) const override;\n\nprivate:\n\t[[nodiscard]] base::weak_ptr<Data::Thread> resolveThread(\n\t\tnot_null<HistoryItem*> item) const;\n\n\tbase::weak_ptr<Data::Thread> _thread;\n\n};\n\nstruct SectionShow {\n\tenum class Way {\n\t\tForward,\n\t\tBackward,\n\t\tClearStack,\n\t};\n\n\tstruct OriginMessage {\n\t\tFullMsgId id;\n\t};\n\tusing Origin = std::variant<v::null_t, OriginMessage>;\n\n\tSectionShow(\n\t\tWay way = Way::Forward,\n\t\tanim::type animated = anim::type::normal,\n\t\tanim::activation activation = anim::activation::normal)\n\t: way(way)\n\t, animated(animated)\n\t, activation(activation) {\n\t}\n\tSectionShow(\n\t\tanim::type animated,\n\t\tanim::activation activation = anim::activation::normal)\n\t: animated(animated)\n\t, activation(activation) {\n\t}\n\n\t[[nodiscard]] SectionShow withWay(Way newWay) const {\n\t\treturn SectionShow(newWay, animated, activation);\n\t}\n\t[[nodiscard]] SectionShow withThirdColumn() const {\n\t\tauto copy = *this;\n\t\tcopy.thirdColumn = true;\n\t\treturn copy;\n\t}\n\t[[nodiscard]] SectionShow withChildColumn() const {\n\t\tauto copy = *this;\n\t\tcopy.childColumn = true;\n\t\treturn copy;\n\t}\n\n\tMessageHighlightId highlight;\n\tint highlightPartOffsetHint = 0;\n\tint highlightTodoItemId = 0;\n\tstd::optional<TimeId> videoTimestamp;\n\tWay way = Way::Forward;\n\tanim::type animated = anim::type::normal;\n\tanim::activation activation = anim::activation::normal;\n\tbool thirdColumn = false;\n\tbool childColumn = false;\n\tbool forbidLayer = false;\n\tbool forceTopicsList = false;\n\tbool reapplyLocalDraft = false;\n\tbool dropSameFromStack = false;\n\tbool allowDuplicateInStack = false;\n\tOrigin origin;\n\n};\n\n[[nodiscard]] MessageHighlightId SearchHighlightId(const QString &query);\n\nclass SessionController;\n\nclass SessionNavigation : public base::has_weak_ptr {\npublic:\n\texplicit SessionNavigation(not_null<Main::Session*> session);\n\tvirtual ~SessionNavigation();\n\n\t[[nodiscard]] Main::Session &session() const;\n\n\tbool showFrozenError();\n\n\tvirtual void showSection(\n\t\tstd::shared_ptr<SectionMemento> memento,\n\t\tconst SectionShow &params = SectionShow()) = 0;\n\tvirtual void showBackFromStack(\n\t\tconst SectionShow &params = SectionShow()) = 0;\n\tvirtual not_null<SessionController*> parentController() = 0;\n\n\tvoid showPeerByLink(const PeerByLinkInfo &info);\n\n\tvoid showRepliesForMessage(\n\t\tnot_null<History*> history,\n\t\tMsgId rootId,\n\t\tMsgId commentId = 0,\n\t\tconst SectionShow &params = SectionShow());\n\tvoid showTopic(\n\t\tnot_null<Data::ForumTopic*> topic,\n\t\tMsgId itemId = 0,\n\t\tconst SectionShow &params = SectionShow());\n\tvoid showSublist(\n\t\tnot_null<Data::SavedSublist*> sublist,\n\t\tMsgId itemId = 0,\n\t\tconst SectionShow &params = SectionShow());\n\tvoid showThread(\n\t\tnot_null<Data::Thread*> thread,\n\t\tMsgId itemId = 0,\n\t\tconst SectionShow &params = SectionShow());\n\n\tvoid showPeerInfo(\n\t\tPeerId peerId,\n\t\tconst SectionShow &params = SectionShow());\n\tvoid showPeerInfo(\n\t\tnot_null<PeerData*> peer,\n\t\tconst SectionShow &params = SectionShow());\n\tvoid showPeerInfo(\n\t\tnot_null<Data::Thread*> thread,\n\t\tconst SectionShow &params = SectionShow());\n\n\tvirtual void showPeerHistory(\n\t\tPeerId peerId,\n\t\tconst SectionShow &params = SectionShow::Way::ClearStack,\n\t\tMsgId msgId = ShowAtUnreadMsgId) = 0;\n\tvoid showPeerHistory(\n\t\tnot_null<PeerData*> peer,\n\t\tconst SectionShow &params = SectionShow::Way::ClearStack,\n\t\tMsgId msgId = ShowAtUnreadMsgId);\n\tvoid showPeerHistory(\n\t\tnot_null<History*> history,\n\t\tconst SectionShow &params = SectionShow::Way::ClearStack,\n\t\tMsgId msgId = ShowAtUnreadMsgId);\n\n\tvoid clearSectionStack(\n\t\t\tconst SectionShow &params = SectionShow::Way::ClearStack) {\n\t\tshowPeerHistory(\n\t\t\tPeerId(0),\n\t\t\tparams,\n\t\t\tShowAtUnreadMsgId);\n\t}\n\n\tvoid showByInitialId(\n\t\tconst SectionShow &params = SectionShow::Way::ClearStack,\n\t\tMsgId msgId = ShowAtUnreadMsgId);\n\n\tvoid showSettings(\n\t\tSettings::Type type,\n\t\tconst SectionShow &params = SectionShow());\n\tvoid showSettings(const SectionShow &params = SectionShow());\n\n\tvoid showPollResults(\n\t\tnot_null<PollData*> poll,\n\t\tFullMsgId contextId,\n\t\tconst SectionShow &params = SectionShow());\n\n\tvoid searchInChat(Dialogs::Key inChat, PeerData *searchFrom = nullptr);\n\tvoid searchMessages(\n\t\tconst QString &query,\n\t\tDialogs::Key inChat,\n\t\tPeerData *searchFrom = nullptr);\n\n\tvoid resolveBoostState(\n\t\tnot_null<ChannelData*> channel,\n\t\tint boostsToLift = 0);\n\n\tvoid resolveCollectible(\n\t\tPeerId ownerId,\n\t\tconst QString &entity,\n\t\tFn<void(QString)> fail = nullptr);\n\tvoid resolveConferenceCall(\n\t\tQString slug,\n\t\tFullMsgId contextId);\n\tvoid resolveConferenceCall(\n\t\tMsgId inviteMsgId,\n\t\tFullMsgId contextId);\n\n\tbase::weak_ptr<Ui::Toast::Instance> showToast(\n\t\tUi::Toast::Config &&config);\n\tbase::weak_ptr<Ui::Toast::Instance> showToast(\n\t\tTextWithEntities &&text,\n\t\tcrl::time duration = 0);\n\tbase::weak_ptr<Ui::Toast::Instance> showToast(\n\t\tconst QString &text,\n\t\tcrl::time duration = 0);\n\n\t[[nodiscard]] virtual std::shared_ptr<ChatHelpers::Show> uiShow();\n\nprotected:\n\tvoid fullInfoLoadedHook(not_null<PeerData*> peer);\n\nprivate:\n\tvoid resolvePhone(\n\t\tconst QString &phone,\n\t\tFn<void(not_null<PeerData*>)> done);\n\tvoid resolveChatLink(\n\t\tconst QString &slug,\n\t\tFn<void(not_null<PeerData*> peer, TextWithEntities draft)> done);\n\tvoid resolveUsername(\n\t\tconst QString &username,\n\t\tFn<void(not_null<PeerData*>)> done,\n\t\tconst QString &starref = QString());\n\tvoid resolveChannelById(\n\t\tChannelId channelId,\n\t\tFn<void(not_null<ChannelData*>)> done);\n\tvoid resolveConferenceCall(\n\t\tQString slug,\n\t\tMsgId inviteMsgId,\n\t\tFullMsgId contextId);\n\n\tvoid resolveDone(\n\t\tconst MTPcontacts_ResolvedPeer &result,\n\t\tFn<void(not_null<PeerData*>)> done);\n\n\tvoid showMessageByLinkResolved(\n\t\tnot_null<HistoryItem*> item,\n\t\tconst PeerByLinkInfo &info);\n\tvoid showPeerByLinkResolved(\n\t\tnot_null<PeerData*> peer,\n\t\tconst PeerByLinkInfo &info);\n\tvoid joinVoiceChatFromLink(\n\t\tnot_null<PeerData*> peer,\n\t\tconst PeerByLinkInfo &info);\n\n\tvoid applyBoost(\n\t\tnot_null<ChannelData*> channel,\n\t\tFn<void(Ui::BoostCounters)> done);\n\tvoid applyBoostsChecked(\n\t\tnot_null<ChannelData*> channel,\n\t\tstd::vector<int> slots,\n\t\tFn<void(Ui::BoostCounters)> done);\n\n\tconst not_null<Main::Session*> _session;\n\n\tMTP::Sender _api;\n\n\tmtpRequestId _resolveRequestId = 0;\n\tPeerData *_waitingDirectChannel = nullptr;\n\n\tHistory *_showingRepliesHistory = nullptr;\n\tMsgId _showingRepliesRootId = 0;\n\tmtpRequestId _showingRepliesRequestId = 0;\n\n\tChannelData *_boostStateResolving = nullptr;\n\tint _boostsToLift = 0;\n\n\tQString _collectibleEntity;\n\tmtpRequestId _collectibleRequestId = 0;\n\n\tQString _conferenceCallSlug;\n\tMsgId _conferenceCallInviteMsgId;\n\tFullMsgId _conferenceCallResolveContextId;\n\tmtpRequestId _conferenceCallRequestId = 0;\n\n};\n\nclass SessionController : public SessionNavigation {\npublic:\n\tSessionController(\n\t\tnot_null<Main::Session*> session,\n\t\tnot_null<Controller*> window);\n\t~SessionController();\n\n\t[[nodiscard]] Controller &window() const {\n\t\treturn *_window;\n\t}\n\t[[nodiscard]] SeparateId windowId() const;\n\t[[nodiscard]] bool isPrimary() const;\n\t[[nodiscard]] not_null<::MainWindow*> widget() const;\n\t[[nodiscard]] not_null<MainWidget*> content() const;\n\t[[nodiscard]] Adaptive &adaptive() const;\n\t[[nodiscard]] ChatHelpers::EmojiInteractions &emojiInteractions() const {\n\t\treturn *_emojiInteractions;\n\t}\n\n\tvoid setConnectingBottomSkip(int skip);\n\trpl::producer<int> connectingBottomSkipValue() const;\n\n\tusing FileChosen = ChatHelpers::FileChosen;\n\tvoid stickerOrEmojiChosen(FileChosen chosen);\n\t[[nodiscard]] rpl::producer<FileChosen> stickerOrEmojiChosen() const;\n\n\tbase::weak_qptr<Ui::BoxContent> show(\n\t\tobject_ptr<Ui::BoxContent> content,\n\t\tUi::LayerOptions options = Ui::LayerOption::KeepOther,\n\t\tanim::type animated = anim::type::normal);\n\tvoid hideLayer(anim::type animated = anim::type::normal);\n\n\t[[nodiscard]] auto sendingAnimation() const\n\t-> Ui::MessageSendingAnimationController &;\n\t[[nodiscard]] auto tabbedSelector() const\n\t-> not_null<ChatHelpers::TabbedSelector*>;\n\tvoid takeTabbedSelectorOwnershipFrom(not_null<QWidget*> parent);\n\t[[nodiscard]] bool hasTabbedSelectorOwnership() const;\n\n\t// This is needed for History TopBar updating when searchInChat\n\t// is changed in the Dialogs::Widget of the current window.\n\trpl::producer<Dialogs::Key> searchInChatValue() const {\n\t\treturn _searchInChat.value();\n\t}\n\tvoid setSearchInChat(Dialogs::Key value) {\n\t\t_searchInChat = value;\n\t}\n\tbool uniqueChatsInSearchResults(const Dialogs::SearchState &state) const;\n\n\tvoid openFolder(not_null<Data::Folder*> folder);\n\tvoid closeFolder();\n\tconst rpl::variable<Data::Folder*> &openedFolder() const;\n\n\tvoid showForum(\n\t\tnot_null<Data::Forum*> forum,\n\t\tconst SectionShow &params = SectionShow::Way::ClearStack,\n\t\tMsgId showAtMsgId = ShowAtUnreadMsgId);\n\tvoid closeForum();\n\tconst rpl::variable<Data::Forum*> &shownForum() const;\n\n\tvoid setActiveChatEntry(Dialogs::RowDescriptor row);\n\tvoid setActiveChatEntry(Dialogs::Key key);\n\tDialogs::RowDescriptor activeChatEntryCurrent() const;\n\tDialogs::Key activeChatCurrent() const;\n\trpl::producer<Dialogs::RowDescriptor> activeChatEntryChanges() const;\n\trpl::producer<Dialogs::Key> activeChatChanges() const;\n\trpl::producer<Dialogs::RowDescriptor> activeChatEntryValue() const;\n\trpl::producer<Dialogs::Key> activeChatValue() const;\n\tbool jumpToChatListEntry(Dialogs::RowDescriptor row);\n\n\tvoid setDialogsEntryState(Dialogs::EntryState state);\n\t[[nodiscard]] Dialogs::EntryState dialogsEntryStateCurrent() const;\n\t[[nodiscard]] auto dialogsEntryStateValue() const\n\t\t-> rpl::producer<Dialogs::EntryState>;\n\tbool switchInlineQuery(\n\t\tDialogs::EntryState to,\n\t\tnot_null<UserData*> bot,\n\t\tconst QString &query);\n\tbool switchInlineQuery(\n\t\tnot_null<Data::Thread*> thread,\n\t\tnot_null<UserData*> bot,\n\t\tconst QString &query);\n\n\t[[nodiscard]] Dialogs::RowDescriptor resolveChatNext(\n\t\tDialogs::RowDescriptor from = {}) const;\n\t[[nodiscard]] Dialogs::RowDescriptor resolveChatPrevious(\n\t\tDialogs::RowDescriptor from = {}) const;\n\n\tvoid showEditPeerBox(PeerData *peer);\n\n\tvoid enableGifPauseReason(GifPauseReason reason);\n\tvoid disableGifPauseReason(GifPauseReason reason);\n\trpl::producer<> gifPauseLevelChanged() const {\n\t\treturn _gifPauseLevelChanged.events();\n\t}\n\tbool isGifPausedAtLeastFor(GifPauseReason reason) const;\n\tvoid floatPlayerAreaUpdated();\n\n\tstruct ColumnLayout {\n\t\tint bodyWidth = 0;\n\t\tint dialogsWidth = 0;\n\t\tint chatWidth = 0;\n\t\tint thirdWidth = 0;\n\t\tAdaptive::WindowLayout windowLayout = Adaptive::WindowLayout();\n\t};\n\t[[nodiscard]] ColumnLayout computeColumnLayout() const;\n\tint dialogsSmallColumnWidth() const;\n\tvoid updateColumnLayout() const;\n\tbool canShowThirdSection() const;\n\tbool canShowThirdSectionWithoutResize() const;\n\tbool takeThirdSectionFromLayer();\n\tvoid resizeForThirdSection();\n\tvoid closeThirdSection();\n\n\t[[nodiscard]] bool canShowSeparateWindow(SeparateId id) const;\n\tvoid showPeer(not_null<PeerData*> peer, MsgId msgId = ShowAtUnreadMsgId);\n\n\tvoid startOrJoinGroupCall(not_null<PeerData*> peer);\n\tvoid startOrJoinGroupCall(\n\t\tnot_null<PeerData*> peer,\n\t\tCalls::StartGroupCallArgs args);\n\n\tvoid showSection(\n\t\tstd::shared_ptr<SectionMemento> memento,\n\t\tconst SectionShow &params = SectionShow()) override;\n\tvoid showBackFromStack(\n\t\tconst SectionShow &params = SectionShow()) override;\n\n\tusing SessionNavigation::showPeerHistory;\n\tvoid showPeerHistory(\n\t\tPeerId peerId,\n\t\tconst SectionShow &params = SectionShow::Way::ClearStack,\n\t\tMsgId msgId = ShowAtUnreadMsgId) override;\n\n\tvoid showMessage(\n\t\tnot_null<const HistoryItem*> item,\n\t\tconst SectionShow &params = SectionShow::Way::ClearStack);\n\tvoid cancelUploadLayer(not_null<HistoryItem*> item);\n\n\tvoid showLayer(\n\t\tstd::unique_ptr<Ui::LayerWidget> &&layer,\n\t\tUi::LayerOptions options,\n\t\tanim::type animated = anim::type::normal);\n\n\tvoid showSpecialLayer(\n\t\tobject_ptr<Ui::LayerWidget> &&layer,\n\t\tanim::type animated = anim::type::normal);\n\tvoid hideSpecialLayer(\n\t\t\tanim::type animated = anim::type::normal) {\n\t\tshowSpecialLayer(nullptr, animated);\n\t}\n\tvoid removeLayerBlackout();\n\t[[nodiscard]] bool isLayerShown() const;\n\n\tstruct ShowCalendarDescriptor {\n\t\tDialogs::Key chat;\n\t\tQDate date;\n\t\tbool mediaPhoto = false;\n\t\tbool mediaVideo = false;\n\t\tFn<void(FullMsgId, Fn<void()>)> customJump;\n\t};\n\tvoid showCalendar(ShowCalendarDescriptor &&descriptor);\n\n\tvoid showAddContact();\n\tvoid showNewGroup();\n\tvoid showNewChannel();\n\n\tvoid showPassportForm(const Passport::FormRequest &request);\n\tvoid clearPassportForm();\n\n\tstruct MessageContext {\n\t\tFullMsgId id;\n\t\tMsgId topicRootId;\n\t\tPeerId monoforumPeerId;\n\t\tbool showDrawButton = false;\n\t};\n\tvoid openPhoto(\n\t\tnot_null<PhotoData*> photo,\n\t\tMessageContext message,\n\t\tconst Data::StoriesContext *stories = nullptr);\n\tvoid openPhoto(not_null<PhotoData*> photo, not_null<PeerData*> peer);\n\tvoid openDocument(\n\t\tnot_null<DocumentData*> document,\n\t\tbool showInMediaView,\n\t\tMessageContext message,\n\t\tconst Data::StoriesContext *stories = nullptr,\n\t\tstd::optional<TimeId> videoTimestampOverride = {});\n\tbool openSharedStory(HistoryItem *item);\n\tbool openFakeItemStory(\n\t\tFullMsgId fakeItemId,\n\t\tconst Data::StoriesContext *stories = nullptr);\n\n\tvoid showChooseReportMessages(\n\t\tnot_null<PeerData*> peer,\n\t\tData::ReportInput reportInput,\n\t\tFn<void(std::vector<MsgId>)> done) const;\n\tvoid clearChooseReportMessages() const;\n\n\tvoid showInNewWindow(\n\t\tSeparateId id,\n\t\tMsgId msgId = ShowAtUnreadMsgId);\n\n\tvoid toggleChooseChatTheme(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::optional<bool> show = std::nullopt);\n\tvoid finishChatThemeEdit(not_null<PeerData*> peer);\n\n\t[[nodiscard]] bool mainSectionShown() const {\n\t\treturn _mainSectionShown.current();\n\t}\n\t[[nodiscard]] rpl::producer<bool> mainSectionShownChanges() const {\n\t\treturn _mainSectionShown.changes();\n\t}\n\tvoid setMainSectionShown(bool value) {\n\t\t_mainSectionShown = value;\n\t}\n\n\t[[nodiscard]] bool chatsForceDisplayWide() const {\n\t\treturn _chatsForceDisplayWide.current();\n\t}\n\t[[nodiscard]] auto chatsForceDisplayWideChanges() const\n\t-> rpl::producer<bool> {\n\t\treturn _chatsForceDisplayWide.changes();\n\t}\n\tvoid setChatsForceDisplayWide(bool value) {\n\t\t_chatsForceDisplayWide = value;\n\t}\n\n\tnot_null<SessionController*> parentController() override {\n\t\treturn this;\n\t}\n\n\t[[nodiscard]] int filtersWidth() const;\n\t[[nodiscard]] bool enoughSpaceForFilters() const;\n\t[[nodiscard]] rpl::producer<bool> enoughSpaceForFiltersValue() const;\n\t[[nodiscard]] rpl::producer<FilterId> activeChatsFilter() const;\n\t[[nodiscard]] FilterId activeChatsFilterCurrent() const;\n\tvoid setActiveChatsFilter(\n\t\tFilterId id,\n\t\tconst SectionShow &params = SectionShow::Way::ClearStack);\n\n\tvoid toggleFiltersMenu(bool enabled);\n\t[[nodiscard]] rpl::producer<> filtersMenuChanged() const;\n\n\t[[nodiscard]] auto defaultChatTheme() const\n\t-> const std::shared_ptr<Ui::ChatTheme> & {\n\t\treturn _defaultChatTheme;\n\t}\n\t[[nodiscard]] auto cachedChatThemeValue(\n\t\tconst Data::CloudTheme &data,\n\t\tconst Data::WallPaper &paper,\n\t\tData::CloudThemeType type)\n\t-> rpl::producer<std::shared_ptr<Ui::ChatTheme>>;\n\t[[nodiscard]] bool chatThemeAlreadyCached(\n\t\tconst Data::CloudTheme &data,\n\t\tconst Data::WallPaper &paper,\n\t\tData::CloudThemeType type);\n\tvoid setChatStyleTheme(const std::shared_ptr<Ui::ChatTheme> &theme);\n\tvoid clearCachedChatThemes();\n\tvoid pushLastUsedChatTheme(const std::shared_ptr<Ui::ChatTheme> &theme);\n\t[[nodiscard]] not_null<Ui::ChatTheme*> currentChatTheme() const;\n\n\tvoid overridePeerTheme(\n\t\tnot_null<PeerData*> peer,\n\t\tstd::shared_ptr<Ui::ChatTheme> theme,\n\t\tQString token);\n\tvoid clearPeerThemeOverride(not_null<PeerData*> peer);\n\t[[nodiscard]] auto peerThemeOverrideValue() const\n\t\t-> rpl::producer<PeerThemeOverride> {\n\t\treturn _peerThemeOverride.value();\n\t}\n\n\tvoid openPeerStory(\n\t\tnot_null<PeerData*> peer,\n\t\tStoryId storyId,\n\t\tData::StoriesContext context);\n\tvoid openPeerStories(\n\t\tPeerId peerId,\n\t\tstd::optional<Data::StorySourcesList> list = std::nullopt,\n\t\tbool onlyLive = false,\n\t\tbool afterReload = false);\n\n\t[[nodiscard]] Ui::ChatPaintContext preparePaintContext(\n\t\tUi::ChatPaintContextArgs &&args);\n\t[[nodiscard]] not_null<const Ui::ChatStyle*> chatStyle() const {\n\t\treturn _chatStyle.get();\n\t}\n\n\t[[nodiscard]] QString authedName() const {\n\t\treturn _authedName;\n\t}\n\n\tvoid setPremiumRef(const QString &ref);\n\t[[nodiscard]] QString premiumRef() const;\n\n\tbool showChatPreview(\n\t\tDialogs::RowDescriptor row,\n\t\tFn<void(bool shown)> callback = nullptr,\n\t\tQPointer<QWidget> parentOverride = nullptr,\n\t\tstd::optional<QPoint> positionOverride = {});\n\tbool scheduleChatPreview(\n\t\tDialogs::RowDescriptor row,\n\t\tFn<void(bool shown)> callback = nullptr,\n\t\tQPointer<QWidget> parentOverride = nullptr,\n\t\tstd::optional<QPoint> positionOverride = {});\n\tvoid cancelScheduledPreview();\n\n\t[[nodiscard]] bool contentOverlapped(QWidget *w, QPaintEvent *e) const;\n\n\t[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() override;\n\n\tvoid saveSubsectionTabs(\n\t\tstd::unique_ptr<HistoryView::SubsectionTabs> tabs);\n\t[[nodiscard]] auto restoreSubsectionTabsFor(\n\t\tnot_null<Ui::RpWidget*> parent,\n\t\tnot_null<Data::Thread*> thread)\n\t\t-> std::unique_ptr<HistoryView::SubsectionTabs>;\n\tvoid dropSubsectionTabs();\n\n\tvoid showStarGiftAuction(const QString &slug);\n\tvoid showStarGiftAuction(uint64 giftId);\n\n\tvoid showCloudPassword(const QString &highlightId = QString());\n\n\tvoid setHighlightControlId(const QString &id);\n\t[[nodiscard]] QString highlightControlId() const;\n\t[[nodiscard]] bool takeHighlightControlId(const QString &id);\n\tvoid checkHighlightControl(\n\t\tconst QString &id,\n\t\tQWidget *widget,\n\t\tSettings::HighlightArgs &&args);\n\tvoid checkHighlightControl(const QString &id, QWidget *widget);\n\n\t[[nodiscard]] rpl::lifetime &lifetime() {\n\t\treturn _lifetime;\n\t}\n\nprivate:\n\tstruct CachedThemeKey;\n\tstruct CachedTheme;\n\n\tvoid init();\n\tvoid setupShortcuts();\n\tvoid checkOpenedFilter();\n\tvoid suggestArchiveAndMute();\n\tvoid activateFirstChatsFilter();\n\n\tint minimalThreeColumnWidth() const;\n\tint countDialogsWidthFromRatio(int bodyWidth) const;\n\tint countThirdColumnWidthFromRatio(int bodyWidth) const;\n\tstruct ShrinkResult {\n\t\tint dialogsWidth;\n\t\tint thirdWidth;\n\t};\n\tShrinkResult shrinkDialogsAndThirdColumns(\n\t\tint dialogsWidth,\n\t\tint thirdWidth,\n\t\tint bodyWidth) const;\n\n\tvoid pushToChatEntryHistory(Dialogs::RowDescriptor row);\n\tbool chatEntryHistoryMove(int steps);\n\tvoid resetFakeUnreadWhileOpened();\n\n\tvoid checkInvitePeek();\n\tvoid setupPremiumToast();\n\n\tvoid pushDefaultChatBackground();\n\tvoid cacheChatTheme(\n\t\tCachedThemeKey key,\n\t\tconst Data::CloudTheme &data,\n\t\tconst Data::WallPaper &paper,\n\t\tData::CloudThemeType type);\n\tvoid cacheChatThemeDone(std::shared_ptr<Ui::ChatTheme> result);\n\tvoid updateCustomThemeBackground(CachedTheme &theme);\n\t[[nodiscard]] Ui::ChatThemeBackgroundData backgroundData(\n\t\tCachedTheme &theme,\n\t\tbool generateGradient = true) const;\n\n\t[[nodiscard]] bool skipNonPremiumLimitToast(bool download) const;\n\tvoid checkNonPremiumLimitToastDownload(DocumentId id);\n\tvoid checkNonPremiumLimitToastUpload(FullMsgId id);\n\n\tbool openFolderInDifferentWindow(not_null<Data::Folder*> folder);\n\tbool showForumInDifferentWindow(\n\t\tnot_null<Data::Forum*> forum,\n\t\tconst SectionShow &params,\n\t\tMsgId showAtMsgId);\n\n\t[[nodiscard]] bool openPhotoExternal(\n\t\tnot_null<PhotoData*> photo,\n\t\tData::FileOrigin origin);\n\tvoid handleDrawToReplyRequest(Data::DrawToReplyRequest request);\n\t[[nodiscard]] Data::Thread *resolveDrawToReplyThread(\n\t\tconst Data::DrawToReplyRequest &request) const;\n\tvoid showDrawToReplyFilesBox(\n\t\tnot_null<Data::Thread*> thread,\n\t\tFullMsgId replyTo,\n\t\tUi::PreparedList &&list);\n\tvoid sendDrawToReplyFiles(\n\t\tnot_null<Data::Thread*> thread,\n\t\tFullMsgId replyTo,\n\t\tstd::shared_ptr<Ui::PreparedBundle> bundle,\n\t\tApi::SendOptions options);\n\n\tconst not_null<Controller*> _window;\n\tconst std::unique_ptr<ChatHelpers::EmojiInteractions> _emojiInteractions;\n\tconst std::unique_ptr<ChatPreviewManager> _chatPreviewManager;\n\tconst bool _isPrimary = false;\n\tconst bool _hasDialogs = false;\n\n\tmutable std::shared_ptr<ChatHelpers::Show> _cachedShow;\n\n\tQString _authedName;\n\n\tusing SendingAnimation = Ui::MessageSendingAnimationController;\n\tconst std::unique_ptr<SendingAnimation> _sendingAnimation;\n\n\tstd::unique_ptr<Passport::FormController> _passportForm;\n\tstd::unique_ptr<FiltersMenu> _filters;\n\n\tGifPauseReasons _gifPauseReasons = 0;\n\trpl::event_stream<> _gifPauseLevelChanged;\n\n\t// Depends on _gifPause*.\n\tconst std::unique_ptr<ChatHelpers::TabbedSelector> _tabbedSelector;\n\n\trpl::variable<Dialogs::Key> _searchInChat;\n\trpl::variable<Dialogs::RowDescriptor> _activeChatEntry;\n\trpl::lifetime _activeHistoryLifetime;\n\trpl::variable<bool> _mainSectionShown = false;\n\trpl::variable<bool> _chatsForceDisplayWide = false;\n\tstd::deque<Dialogs::RowDescriptor> _chatEntryHistory;\n\tint _chatEntryHistoryPosition = -1;\n\tbool _filtersActivated = false;\n\n\trpl::variable<Dialogs::EntryState> _dialogsEntryState;\n\n\tbase::Timer _invitePeekTimer;\n\n\trpl::variable<FilterId> _activeChatsFilter;\n\n\trpl::variable<int> _connectingBottomSkip;\n\n\trpl::event_stream<ChatHelpers::FileChosen> _stickerOrEmojiChosen;\n\n\tPeerData *_showEditPeer = nullptr;\n\trpl::variable<Data::Folder*> _openedFolder;\n\trpl::variable<Data::Forum*> _shownForum;\n\trpl::lifetime _shownForumLifetime;\n\n\trpl::event_stream<> _filtersMenuChanged;\n\n\tconst std::shared_ptr<Ui::ChatTheme> _defaultChatTheme;\n\tbase::flat_map<CachedThemeKey, CachedTheme> _customChatThemes;\n\trpl::event_stream<std::shared_ptr<Ui::ChatTheme>> _cachedThemesStream;\n\trpl::event_stream<> _giftSymbolLoaded;\n\tconst std::unique_ptr<Ui::ChatStyle> _chatStyle;\n\tstd::weak_ptr<Ui::ChatTheme> _chatStyleTheme;\n\tstd::deque<std::shared_ptr<Ui::ChatTheme>> _lastUsedCustomChatThemes;\n\trpl::variable<PeerThemeOverride> _peerThemeOverride;\n\n\tstd::unique_ptr<ChatSwitchProcess> _chatSwitchProcess;\n\n\tDocumentId _pendingOpenDocumentId = 0;\n\tstruct PendingOpenPhoto {\n\t\tPhotoData *data = nullptr;\n\t\tstd::shared_ptr<Data::PhotoMedia> media;\n\t\tQString filepath;\n\t} _pendingOpenPhoto;\n\n\tbase::has_weak_ptr _storyOpenGuard;\n\n\tQString _premiumRef;\n\tstd::unique_ptr<HistoryView::SubsectionTabs> _savedSubsectionTabs;\n\trpl::lifetime _savedSubsectionTabsLifetime;\n\n\trpl::lifetime _starGiftAuctionLifetime;\n\trpl::lifetime _showCloudPasswordLifetime;\n\n\trpl::lifetime _lifetime;\n\n};\n\nvoid ActivateWindow(not_null<SessionController*> controller);\n\n[[nodiscard]] bool IsPaused(\n\tnot_null<SessionController*> controller,\n\tGifPauseReason level);\n[[nodiscard]] Fn<bool()> PausedIn(\n\tnot_null<SessionController*> controller,\n\tGifPauseReason level);\n\nbool CheckAndJumpToNearChatsFilter(\n\tnot_null<SessionController*> controller,\n\tbool isNext,\n\tbool jump);\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_session_controller_link_info.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\nnamespace InlineBots {\nstruct WebViewContext;\n} // namespace InlineBots\n\nnamespace Window {\n\nenum class ResolveType {\n\tDefault,\n\tBotApp,\n\tBotStart,\n\tAddToGroup,\n\tAddToChannel,\n\tHashtagSearch,\n\tShareGame,\n\tMention,\n\tBoost,\n\tChannelDirect,\n\tProfile,\n};\n\nstruct CommentId {\n\tMsgId id = 0;\n};\nstruct ThreadId {\n\tMsgId id = 0;\n};\nusing RepliesByLinkInfo = std::variant<v::null_t, CommentId, ThreadId>;\n\nstruct PeerByLinkInfo {\n\tstd::variant<QString, ChannelId> usernameOrId;\n\tQString phone;\n\tQString chatLinkSlug;\n\tMsgId messageId = ShowAtUnreadMsgId;\n\tQByteArray pollOption;\n\tQString storyParam;\n\tint storyAlbumId = 0;\n\tint giftCollectionId = 0;\n\tstd::optional<TimeId> videoTimestamp;\n\tQString text;\n\tRepliesByLinkInfo repliesInfo;\n\tResolveType resolveType = ResolveType::Default;\n\tQString referral;\n\tQString startToken;\n\tChatAdminRights startAdminRights;\n\tbool startAutoSubmit = false;\n\tbool joinChannel = false;\n\tQString botAppName;\n\tbool botAppForceConfirmation = false;\n\tbool botAppFullScreen = false;\n\tQString attachBotUsername;\n\tstd::optional<QString> attachBotToggleCommand;\n\tbool attachBotMainOpen = false;\n\tbool attachBotMainCompact = false;\n\tInlineBots::PeerTypes attachBotChooseTypes;\n\tstd::optional<QString> voicechatHash;\n\tFullMsgId clickFromMessageId;\n\tstd::shared_ptr<InlineBots::WebViewContext> clickFromBotWebviewContext;\n\tbool historyInNewWindow = false;\n};\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_setup_email.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_setup_email.h\"\n\n#include \"api/api_cloud_password.h\"\n#include \"apiwrap.h\"\n#include \"base/call_delayed.h\"\n#include \"base/event_filter.h\"\n#include \"core/application.h\"\n#include \"data/components/promo_suggestions.h\"\n#include \"data/data_session.h\"\n#include \"data/data_user.h\"\n#include \"intro/intro_code_input.h\"\n#include \"lang/lang_keys.h\"\n#include \"lottie/lottie_icon.h\"\n#include \"main/main_account.h\"\n#include \"main/main_domain.h\"\n#include \"main/main_session.h\"\n#include \"settings/settings_common.h\"\n#include \"ui/painter.h\"\n#include \"ui/ui_utility.h\"\n#include \"ui/vertical_list.h\"\n#include \"ui/controls/userpic_button.h\"\n#include \"ui/widgets/buttons.h\"\n#include \"ui/widgets/fields/input_field.h\"\n#include \"ui/widgets/labels.h\"\n#include \"ui/widgets/popup_menu.h\"\n#include \"ui/widgets/menu/menu_action.h\"\n#include \"ui/widgets/sent_code_field.h\"\n#include \"ui/wrap/vertical_layout.h\"\n#include \"window/window_controller.h\"\n#include \"window/window_slide_animation.h\"\n#include \"styles/style_boxes.h\"\n#include \"styles/style_info.h\"\n#include \"styles/style_intro.h\"\n#include \"styles/style_layers.h\"\n#include \"styles/style_settings.h\"\n#include \"styles/style_window.h\"\n\nnamespace Window {\nnamespace {\n\n[[nodiscard]] Settings::LottieIcon CreateEmailIcon(\n\t\tnot_null<Ui::VerticalLayout*> container) {\n\treturn Settings::CreateLottieIcon(\n\t\tcontainer,\n\t\t{\n\t\t\t.name = u\"cloud_password/email\"_q,\n\t\t\t.sizeOverride = st::normalBoxLottieSize,\n\t\t},\n\t\t{});\n}\n\n} // namespace\n\nSetupEmailLockWidget::SetupEmailLockWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> window)\n: LockWidget(parent, window)\n, _layout(this)\n, _confirmWidget(nullptr) {\n\tconst auto session = window->maybeSession();\n\tconst auto state = session\n\t\t? session->promoSuggestions().setupEmailState()\n\t\t: Data::SetupEmailState::Setup;\n\n\tconst auto noSkip = state == Data::SetupEmailState::SetupNoSkip;\n\tconst auto accountsCount = session\n\t\t? session->domain().accountsAuthedCount()\n\t\t: 0;\n\n\tif (!window->isPrimary()) {\n\t// if (state == Data::SetupEmailState::SettingUp\n\t// \t|| state == Data::SetupEmailState::SettingUpNoSkip) {\n\t\tauto icon = CreateEmailIcon(_layout);\n\t\t_iconAnimate = std::move(icon.animate);\n\t\t_layout->add(std::move(icon.widget));\n\t\tbase::call_delayed(st::slideDuration, crl::guard(this, [=] {\n\t\t\tif (_iconAnimate) {\n\t\t\t\t_iconAnimate(anim::repeat::once);\n\t\t\t}\n\t\t}));\n\n\t\tUi::AddSkip(_layout);\n\t\tUi::AddSkip(_layout);\n\t\tUi::AddSkip(_layout);\n\n\t\t_layout->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t_layout,\n\t\t\t\ttr::lng_settings_cloud_login_email_title(),\n\t\t\t\tst::lockSetupEmailTitle),\n\t\t\tst::boxRowPadding,\n\t\t\tstyle::al_top);\n\n\t\tUi::AddSkip(_layout);\n\t\tUi::AddSkip(_layout);\n\n\t\t_layout->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t_layout,\n\t\t\t\ttr::lng_settings_cloud_login_email_busy(),\n\t\t\t\tst::lockSetupEmailLabel),\n\t\t\tst::boxRowPadding,\n\t\t\tstyle::al_top);\n\t} else {\n\t\tif (!noSkip) {\n\t\t\t_backButton = object_ptr<Ui::IconButton>(\n\t\t\t\tthis,\n\t\t\t\tst::introBackButton);\n\t\t\tif (session) {\n\t\t\t\tsession->promoSuggestions().setSetupEmailState(\n\t\t\t\t\tData::SetupEmailState::SettingUp);\n\t\t\t\t_backButton->setClickedCallback([=] {\n\t\t\t\t\tsession->promoSuggestions().dismissSetupEmail([=] {\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (accountsCount <= 1) {\n\t\t\t_logoutButton = object_ptr<Ui::RoundButton>(\n\t\t\t\tthis,\n\t\t\t\ttr::lng_settings_logout(),\n\t\t\t\tst::defaultBoxButton);\n\t\t\tif (session) {\n\t\t\t\tsession->promoSuggestions().setSetupEmailState(\n\t\t\t\t\tData::SetupEmailState::SettingUpNoSkip);\n\t\t\t\t_logoutButton->setClickedCallback([=] {\n\t\t\t\t\twindow->showLogoutConfirmation();\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\t_backButton = object_ptr<Ui::IconButton>(\n\t\t\t\tthis,\n\t\t\t\tst::introBackButton);\n\t\t\tif (session) {\n\t\t\t\t_backButton->setClickedCallback([=] {\n\t\t\t\t\tshowAccountsMenu();\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n#ifdef _DEBUG\n\t\tif (session->isTestMode()) {\n\t\t\t_debugButton = object_ptr<Ui::RoundButton>(\n\t\t\t\tthis,\n\t\t\t\trpl::single(u\"[DEBUG] Clear bio\"_q),\n\t\t\t\tst::defaultBoxButton);\n\t\t\t_debugButton->setClickedCallback([=] {\n\t\t\t\tsession->api().saveSelfBio({});\n\t\t\t});\n\t\t}\n#endif\n\n\t\tauto icon = CreateEmailIcon(_layout);\n\t\t_iconAnimate = std::move(icon.animate);\n\t\t_layout->add(std::move(icon.widget));\n\t\tbase::call_delayed(st::slideDuration, crl::guard(this, [=] {\n\t\t\tif (_iconAnimate) {\n\t\t\t\t_iconAnimate(anim::repeat::once);\n\t\t\t}\n\t\t}));\n\n\t\tUi::AddSkip(_layout);\n\t\tUi::AddSkip(_layout);\n\t\tUi::AddSkip(_layout);\n\n\t\t_layout->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t_layout,\n\t\t\t\ttr::lng_settings_cloud_login_email_title(),\n\t\t\t\tst::lockSetupEmailTitle),\n\t\t\tst::boxRowPadding,\n\t\t\tstyle::al_top);\n\n\t\tUi::AddSkip(_layout);\n\t\tUi::AddSkip(_layout);\n\n\t\t_layout->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t_layout,\n\t\t\t\ttr::lng_settings_cloud_login_email_about(),\n\t\t\t\tst::lockSetupEmailLabel),\n\t\t\tst::boxRowPadding,\n\t\t\tstyle::al_top);\n\n\t\tUi::AddSkip(_layout);\n\t\tUi::AddSkip(_layout);\n\t\tUi::AddSkip(_layout);\n\t\tUi::AddSkip(_layout);\n\n\t\tauto emailInput = _layout->add(\n\t\t\tobject_ptr<Ui::InputField>(\n\t\t\t\t_layout,\n\t\t\t\tst::settingLocalPasscodeInputField,\n\t\t\t\ttr::lng_settings_cloud_login_email_placeholder()),\n\t\t\tst::boxRowPadding,\n\t\t\tstyle::al_top);\n\n\t\tUi::AddSkip(_layout);\n\t\tUi::AddSkip(_layout);\n\n\t\tauto errorLabel = _layout->add(\n\t\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t\t_layout,\n\t\t\t\tQString(),\n\t\t\t\tst::lockSetupEmailLabel),\n\t\t\t{},\n\t\t\tstyle::al_top);\n\t\terrorLabel->setTextColorOverride(st::boxTextFgError->c);\n\t\terrorLabel->hide();\n\n\t\tUi::AddSkip(_layout);\n\t\tUi::AddSkip(_layout);\n\t\tUi::AddSkip(_layout);\n\t\tUi::AddSkip(_layout);\n\n\t\tauto submit = _layout->add(\n\t\t\tobject_ptr<Ui::RoundButton>(\n\t\t\t\t_layout,\n\t\t\t\ttr::lng_settings_cloud_login_email_confirm(),\n\t\t\t\tst::changePhoneButton),\n\t\t\tst::boxRowPadding,\n\t\t\tstyle::al_top);\n\n\t\t_emailInput = emailInput;\n\t\t_errorLabel = errorLabel;\n\t\t_submit = submit;\n\n\t\tsubmit->setClickedCallback([=] { this->submit(); });\n\n\t\temailInput->changes() | rpl::on_next([=] {\n\t\t\t_error = QString();\n\t\t\terrorLabel->hide();\n\t\t}, emailInput->lifetime());\n\n\t\temailInput->submits() | rpl::on_next([=] {\n\t\t\tthis->submit();\n\t\t}, emailInput->lifetime());\n\t}\n}\n\nvoid SetupEmailLockWidget::paintContent(QPainter &p) {\n\tif (_backAnimation) {\n\t\t_backAnimation->paintContents(p);\n\t\treturn;\n\t}\n\n\tLockWidget::paintContent(p);\n}\n\nvoid SetupEmailLockWidget::submit() {\n\tif (!_emailInput) {\n\t\treturn;\n\t}\n\tconst auto email = _emailInput->getLastText().trimmed();\n\tif (email.isEmpty()) {\n\t\t_emailInput->setFocus();\n\t\t_emailInput->showError();\n\t\treturn;\n\t}\n\n\tif (!email.contains('@') || !email.contains('.')) {\n\t\t_error = tr::lng_cloud_password_bad_email(tr::now);\n\t\t_errorLabel->setText(_error);\n\t\t_errorLabel->show();\n\t\t_emailInput->setFocus();\n\t\t_emailInput->showError();\n\t\t_emailInput->selectAll();\n\t\treturn;\n\t}\n\n\tconst auto session = window()->maybeSession();\n\tif (!session) {\n\t\tshowConfirmWidget(email, 6, QString());\n\t\treturn;\n\t}\n\n\t_api.emplace(&session->mtp());\n\tconst auto done = crl::guard(this, [=](\n\t\t\tint length,\n\t\t\tconst QString &pattern) {\n\t\t_api.reset();\n\t\tshowConfirmWidget(email, length, pattern);\n\t});\n\tconst auto fail = crl::guard(this, [=](const QString &type) {\n\t\t_api.reset();\n\t\tif (MTP::IsFloodError(type)) {\n\t\t\t_error = tr::lng_flood_error(tr::now);\n\t\t} else if (type == u\"EMAIL_INVALID\"_q) {\n\t\t\t_error = tr::lng_cloud_password_bad_email(tr::now);\n\t\t} else {\n\t\t\t_error = tr::lng_cloud_password_bad_email(tr::now);\n\t\t}\n\t\t_errorLabel->setText(_error);\n\t\t_errorLabel->show();\n\t\t_emailInput->setFocus();\n\t\t_emailInput->showError();\n\t\t_emailInput->selectAll();\n\t});\n\tApi::RequestLoginEmailCode(*_api, email, done, fail);\n}\n\nvoid SetupEmailLockWidget::showConfirmWidget(\n\t\tconst QString &email,\n\t\tint codeLength,\n\t\tconst QString &emailPattern) {\n\t_layout->hide();\n\tauto oldContentCache = grabContent();\n\n\t_confirmWidget = Ui::CreateChild<SetupEmailConfirmWidget>(\n\t\tthis,\n\t\twindow(),\n\t\temail,\n\t\tcodeLength,\n\t\temailPattern);\n\t_confirmWidget->resize(size());\n\t_confirmWidget->show();\n\t_confirmWidget->showAnimated(std::move(oldContentCache));\n\t_confirmWidget->setFocus();\n\n\t_confirmWidget->backRequests(\n\t) | rpl::on_next([=] {\n\t\tshowEmailInput();\n\t}, _confirmWidget->lifetime());\n\n\t_confirmWidget->confirmations(\n\t) | rpl::on_next([=, controller = window()] {\n\t\tif (const auto session = controller->maybeSession()) {\n\t\t\tsession->promoSuggestions().setSetupEmailState(\n\t\t\t\tData::SetupEmailState::None);\n\t\t}\n\t}, _confirmWidget->lifetime());\n}\n\nvoid SetupEmailLockWidget::showEmailInput() {\n\tif (!_confirmWidget) {\n\t\treturn;\n\t}\n\n\tauto oldContentCache = Ui::GrabWidget(_confirmWidget);\n\t_confirmWidget->hide();\n\t_confirmWidget->deleteLater();\n\t_confirmWidget = nullptr;\n\n\tif (_logoutButton) {\n\t\t_logoutButton->show();\n\t}\n\t_layout->show();\n\tauto newContentCache = grabContent();\n\tif (_logoutButton) {\n\t\t_logoutButton->hide();\n\t}\n\t_layout->hide();\n\n\t_backAnimation = std::make_unique<SlideAnimation>();\n\t_backAnimation->setDirection(SlideDirection::FromLeft);\n\t_backAnimation->setRepaintCallback([=] { update(); });\n\t_backAnimation->setFinishedCallback([=] {\n\t\t_layout->show();\n\t\tif (_logoutButton) {\n\t\t\t_logoutButton->show();\n\t\t}\n\t\t_backAnimation.reset();\n\t\tif (_iconAnimate) {\n\t\t\t_iconAnimate(anim::repeat::once);\n\t\t}\n\t});\n\t_backAnimation->setPixmaps(\n\t\tstd::move(oldContentCache),\n\t\tstd::move(newContentCache));\n\t_backAnimation->start();\n\n\tif (_emailInput) {\n\t\t_emailInput->setFocus();\n\t}\n}\n\nQPixmap SetupEmailLockWidget::grabContent() {\n\treturn Ui::GrabWidget(this);\n}\n\nvoid SetupEmailLockWidget::resizeEvent(QResizeEvent *e) {\n\tconst auto layoutWidth = std::min(\n\t\twidth(),\n\t\tst::lockSetupEmailLabelMaxWidth);\n\t_layout->resizeToWidth(layoutWidth);\n\t_layout->moveToLeft(\n\t\t(width() - layoutWidth) / 2,\n\t\tst::infoLayerTopBarHeight);\n\n\tif (_backButton) {\n\t\t_backButton->moveToLeft(0, 0);\n\t}\n\n\tif (_logoutButton) {\n\t\t_logoutButton->move(st::lockSetupEmailLogOutPosition);\n\t}\n\n\tif (_debugButton) {\n\t\t_debugButton->moveToLeft(\n\t\t\tst::lockSetupEmailLogOutPosition.x(),\n\t\t\theight() - _debugButton->height()\n\t\t\t\t- st::lockSetupEmailLogOutPosition.y());\n\t}\n\n\tif (_confirmWidget) {\n\t\t_confirmWidget->resize(size());\n\t}\n}\n\nvoid SetupEmailLockWidget::setInnerFocus() {\n\tLockWidget::setInnerFocus();\n\tif (_confirmWidget && _confirmWidget->isVisible()) {\n\t\t_confirmWidget->setFocus();\n\t} else if (_emailInput) {\n\t\t_emailInput->setFocus();\n\t}\n}\n\nSetupEmailConfirmWidget::SetupEmailConfirmWidget(\n\tQWidget *parent,\n\tnot_null<Controller*> window,\n\tconst QString &email,\n\tint codeLength,\n\tconst QString &emailPattern)\n: Ui::RpWidget(parent)\n, _window(window)\n, _email(email)\n, _emailPattern(emailPattern)\n, _backButton(this, st::introBackButton)\n, _layout(this) {\n\tauto icon = CreateEmailIcon(_layout);\n\t_iconAnimate = std::move(icon.animate);\n\t_layout->add(std::move(icon.widget));\n\n\tUi::AddSkip(_layout);\n\tUi::AddSkip(_layout);\n\tUi::AddSkip(_layout);\n\n\t_layout->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_layout,\n\t\t\ttr::lng_settings_cloud_login_email_code_title(),\n\t\t\tst::lockSetupEmailTitle),\n\t\tst::boxRowPadding,\n\t\tstyle::al_top);\n\n\tUi::AddSkip(_layout);\n\tUi::AddSkip(_layout);\n\n\t_layout->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_layout,\n\t\t\ttr::lng_settings_cloud_login_email_code_about(\n\t\t\t\ttr::now,\n\t\t\t\tlt_email,\n\t\t\t\t_emailPattern.isEmpty() ? _email : _emailPattern),\n\t\t\tst::lockSetupEmailLabel),\n\t\tst::boxRowPadding,\n\t\tstyle::al_top);\n\n\tUi::AddSkip(_layout);\n\tUi::AddSkip(_layout);\n\tUi::AddSkip(_layout);\n\tUi::AddSkip(_layout);\n\n\tauto codeInput = _layout->add(\n\t\tobject_ptr<Ui::CodeInput>(_layout),\n\t\tstyle::al_top);\n\tcodeInput->setDigitsCountMax(codeLength > 0 ? codeLength : 6);\n\n\tUi::AddSkip(_layout);\n\tUi::AddSkip(_layout);\n\n\tauto errorLabel = _layout->add(\n\t\tobject_ptr<Ui::FlatLabel>(\n\t\t\t_layout,\n\t\t\tQString(),\n\t\t\tst::lockSetupEmailLabel),\n\t\tst::boxRowPadding,\n\t\tstyle::al_top);\n\terrorLabel->setTextColorOverride(st::boxTextFgError->c);\n\terrorLabel->hide();\n\n\t_codeInput = codeInput;\n\t_errorLabel = errorLabel;\n\n\t_backButton->clicks(\n\t) | rpl::to_empty | rpl::start_to_stream(_backRequests, lifetime());\n\n\tbase::install_event_filter(codeInput, [=](not_null<QEvent*> e) {\n\t\tif (e->type() == QEvent::KeyPress) {\n\t\t\tconst auto k = static_cast<QKeyEvent*>(e.get());\n\t\t\tif (k->key() == Qt::Key_Escape) {\n\t\t\t\t_backRequests.fire({});\n\t\t\t\treturn base::EventFilterResult::Cancel;\n\t\t\t}\n\t\t}\n\t\treturn base::EventFilterResult::Continue;\n\t});\n\n\tcodeInput->codeCollected(\n\t) | rpl::on_next([=](const QString &code) {\n\t\tverifyCode(code);\n\t}, codeInput->lifetime());\n}\n\nrpl::producer<> SetupEmailConfirmWidget::backRequests() const {\n\treturn _backRequests.events();\n}\n\nrpl::producer<> SetupEmailConfirmWidget::confirmations() const {\n\treturn _confirmations.events();\n}\n\nvoid SetupEmailConfirmWidget::paintEvent(QPaintEvent *e) {\n\tauto p = Painter(this);\n\n\tif (_showAnimation) {\n\t\t_showAnimation->paintContents(p);\n\t\treturn;\n\t}\n\n\tp.fillRect(rect(), st::windowBg);\n}\n\nvoid SetupEmailConfirmWidget::showAnimated(QPixmap oldContentCache) {\n\t_showAnimation = nullptr;\n\n\tshowChildren();\n\tauto newContentCache = Ui::GrabWidget(this);\n\thideChildren();\n\n\t_showAnimation = std::make_unique<SlideAnimation>();\n\t_showAnimation->setDirection(SlideDirection::FromRight);\n\t_showAnimation->setRepaintCallback([=] { update(); });\n\t_showAnimation->setFinishedCallback([=] {\n\t\tshowChildren();\n\t\t_showAnimation.reset();\n\t\tif (_iconAnimate) {\n\t\t\t_iconAnimate(anim::repeat::once);\n\t\t}\n\t\tif (_codeInput) {\n\t\t\t_codeInput->setFocus();\n\t\t}\n\t});\n\t_showAnimation->setPixmaps(\n\t\tstd::move(oldContentCache),\n\t\tstd::move(newContentCache));\n\t_showAnimation->start();\n}\n\nvoid SetupEmailConfirmWidget::resizeEvent(QResizeEvent *e) {\n\tconst auto layoutWidth = std::min(\n\t\twidth(),\n\t\tst::lockSetupEmailLabelMaxWidth);\n\t_layout->resizeToWidth(layoutWidth);\n\t_layout->move((width() - layoutWidth) / 2, st::infoLayerTopBarHeight);\n\t_backButton->move(0, 0);\n}\n\nvoid SetupEmailConfirmWidget::keyPressEvent(QKeyEvent *e) {\n\tif (e->key() == Qt::Key_Escape) {\n\t\t_backRequests.fire({});\n\t\treturn;\n\t}\n\tUi::RpWidget::keyPressEvent(e);\n}\n\nvoid SetupEmailConfirmWidget::verifyCode(const QString &code) {\n\tconst auto session = _window->maybeSession();\n\tif (!session) {\n\t\t_confirmations.fire({});\n\t\treturn;\n\t}\n\n\t_api.emplace(&session->mtp());\n\tconst auto done = crl::guard(this, [=] {\n#ifdef _DEBUG\n\t\tif (session->isTestMode()) {\n\t\t\tsession->api().saveSelfBio({});\n\t\t}\n#endif\n\t\t_window->uiShow()->showToast(\n\t\t\ttr::lng_settings_cloud_login_email_set_success(tr::now));\n\t\t_api.reset();\n\t\t_confirmations.fire({});\n\t});\n\tconst auto fail = crl::guard(this, [=](const QString &type) {\n\t\t_api.reset();\n\t\tif (type.isEmpty()) {\n\t\t\t_confirmations.fire({});\n\t\t\treturn;\n\t\t}\n\t\tif (MTP::IsFloodError(type)) {\n\t\t\t_error = tr::lng_flood_error(tr::now);\n\t\t} else if (type == u\"EMAIL_NOT_ALLOWED\"_q) {\n\t\t\t_error = tr::lng_settings_error_email_not_alowed(tr::now);\n\t\t} else if (type == u\"CODE_INVALID\"_q) {\n\t\t\t_error = tr::lng_signin_wrong_code(tr::now);\n\t\t} else if (type == u\"CODE_EXPIRED\"_q\n\t\t\t|| type == u\"EMAIL_HASH_EXPIRED\"_q) {\n\t\t\t_error = Lang::Hard::EmailConfirmationExpired();\n\t\t} else {\n\t\t\t_error = Lang::Hard::ServerError();\n\t\t}\n\t\t_errorLabel->setText(_error);\n\t\t_errorLabel->show();\n\t\t_codeInput->setFocus();\n\t\t_codeInput->showError();\n\t});\n\tApi::VerifyLoginEmail(*_api, code, done, fail);\n}\n\nvoid SetupEmailLockWidget::showAccountsMenu() {\n\tconst auto session = window()->maybeSession();\n\tif (!session) {\n\t\treturn;\n\t}\n\n\tconst auto &st = st::popupMenuWithIcons;\n\n\t_accountsMenu = base::make_unique_q<Ui::PopupMenu>(this, st);\n\tconst auto accounts = session->domain().orderedAccounts();\n\n\tfor (const auto &account : accounts) {\n\t\tif (!account->sessionExists()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto user = account->session().user();\n\t\tif (user == session->user()) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst auto action = new QAction(user->name(), _accountsMenu);\n\t\tQObject::connect(action, &QAction::triggered, [=] {\n\t\t\tCore::App().domain().maybeActivate(account);\n\t\t});\n\t\tauto owned = base::make_unique_q<Ui::Menu::Action>(\n\t\t\t_accountsMenu->menu(),\n\t\t\t_accountsMenu->menu()->st(),\n\t\t\taction,\n\t\t\tnullptr,\n\t\t\tnullptr);\n\t\tconst auto userpic = Ui::CreateChild<Ui::UserpicButton>(\n\t\t\towned.get(),\n\t\t\tuser,\n\t\t\tst::lockSetupEmailUserpicSmall);\n\t\tuserpic->move(st.menu.itemIconPosition);\n\t\tuserpic->setAttribute(Qt::WA_TransparentForMouseEvents);\n\t\t_accountsMenu->addAction(std::move(owned));\n\t}\n\n\t_accountsMenu->setForcedOrigin(Ui::PanelAnimation::Origin::TopLeft);\n\t_accountsMenu->popup(_backButton\n\t\t? mapToGlobal(QPoint(_backButton->x(), _backButton->height()))\n\t\t: QCursor::pos());\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_setup_email.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"mtproto/sender.h\"\n#include \"window/window_lock_widgets.h\"\n\nnamespace Ui {\nclass InputField;\nclass CodeInput;\nclass RoundButton;\nclass IconButton;\nclass FlatLabel;\nclass VerticalLayout;\n} // namespace Ui\n\nnamespace Window {\n\nclass SlideAnimation;\n\nclass SetupEmailConfirmWidget : public Ui::RpWidget {\n\npublic:\n\tSetupEmailConfirmWidget(\n\t\tQWidget *parent,\n\t\tnot_null<Controller*> window,\n\t\tconst QString &email,\n\t\tint codeLength,\n\t\tconst QString &emailPattern);\n\n\trpl::producer<> backRequests() const;\n\trpl::producer<> confirmations() const;\n\n\tvoid showAnimated(QPixmap oldContentCache);\n\nprotected:\n\tvoid paintEvent(QPaintEvent *e) override;\n\tvoid resizeEvent(QResizeEvent *e) override;\n\tvoid keyPressEvent(QKeyEvent *e) override;\n\nprivate:\n\tvoid verifyCode(const QString &code);\n\n\tnot_null<Controller*> _window;\n\tQString _email;\n\tQString _emailPattern;\n\tQString _error;\n\trpl::lifetime _requestLifetime;\n\tstd::optional<::MTP::Sender> _api;\n\tobject_ptr<Ui::IconButton> _backButton;\n\tobject_ptr<Ui::VerticalLayout> _layout;\n\tUi::CodeInput *_codeInput = nullptr;\n\tUi::FlatLabel *_errorLabel = nullptr;\n\n\trpl::event_stream<> _backRequests;\n\trpl::event_stream<> _confirmations;\n\n\tstd::unique_ptr<SlideAnimation> _showAnimation;\n\tFn<void(anim::repeat)> _iconAnimate;\n\n};\n\nclass SetupEmailLockWidget : public LockWidget {\npublic:\n\tSetupEmailLockWidget(QWidget *parent, not_null<Controller*> window);\n\n\tvoid setInnerFocus() override;\n\nprotected:\n\tvoid resizeEvent(QResizeEvent *e) override;\n\nprivate:\n\tvoid paintContent(QPainter &p) override;\n\tvoid submit();\n\tvoid showConfirmWidget(\n\t\tconst QString &email,\n\t\tint codeLength,\n\t\tconst QString &emailPattern);\n\tvoid showEmailInput();\n\tvoid showAccountsMenu();\n\tQPixmap grabContent();\n\n\tobject_ptr<Ui::VerticalLayout> _layout;\n\tSetupEmailConfirmWidget *_confirmWidget;\n\trpl::lifetime _requestLifetime;\n\tstd::optional<::MTP::Sender> _api;\n\tobject_ptr<Ui::IconButton> _backButton = { nullptr };\n\tobject_ptr<Ui::RoundButton> _logoutButton = { nullptr };\n\tobject_ptr<Ui::RoundButton> _debugButton = { nullptr };\n\tbase::unique_qptr<Ui::PopupMenu> _accountsMenu;\n\tUi::InputField *_emailInput = nullptr;\n\tUi::RoundButton *_submit = nullptr;\n\tUi::FlatLabel *_errorLabel = nullptr;\n\tQString _error;\n\n\tstd::unique_ptr<SlideAnimation> _backAnimation;\n\tFn<void(anim::repeat)> _iconAnimate;\n\n};\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_slide_animation.cpp",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#include \"window/window_slide_animation.h\"\n\n#include \"styles/style_window.h\"\n#include \"styles/style_boxes.h\"\n\nnamespace Window {\n\nvoid SlideAnimation::paintContents(QPainter &p) const {\n\tconst auto retina = style::DevicePixelRatio();\n\n\tconst auto slideLeft = (_direction == SlideDirection::FromLeft);\n\tconst auto progress = _animation.value(slideLeft ? 0. : 1.);\n\tif (_withFade) {\n\t\tconst auto dt = slideLeft\n\t\t\t? (1. - progress)\n\t\t\t: progress;\n\t\tconst auto easeOut = anim::easeOutCirc(1., dt);\n\t\tconst auto easeIn = anim::easeInCirc(1., dt);\n\t\tconst auto arrivingAlpha = easeIn;\n\t\tconst auto departingAlpha = 1. - easeOut;\n\t\tconst auto leftWidthFull = _cacheUnder.width() / retina;\n\t\tconst auto rightWidthFull = _cacheOver.width() / retina;\n\t\tconst auto leftCoord = slideLeft\n\t\t\t? anim::interpolate(-leftWidthFull, 0, easeOut)\n\t\t\t: anim::interpolate(0, -leftWidthFull, easeIn);\n\t\tconst auto leftAlpha = (slideLeft ? arrivingAlpha : departingAlpha);\n\t\tconst auto rightCoord = slideLeft\n\t\t\t? anim::interpolate(0, rightWidthFull, easeIn)\n\t\t\t: anim::interpolate(rightWidthFull, 0, easeOut);\n\t\tconst auto rightAlpha = (slideLeft ? departingAlpha : arrivingAlpha);\n\n\t\tconst auto leftWidth = (leftWidthFull + leftCoord);\n\t\tconst auto rightWidth = rightWidthFull - rightCoord;\n\n\t\tif (!_mask.isNull()) {\n\t\t\tauto frame = QImage(\n\t\t\t\t_mask.size(),\n\t\t\t\tQImage::Format_ARGB32_Premultiplied);\n\t\t\tframe.setDevicePixelRatio(_mask.devicePixelRatio());\n\t\t\tframe.fill(Qt::transparent);\n\t\t\tQPainter q(&frame);\n\n\t\t\tif (leftWidth > 0) {\n\t\t\t\tq.setOpacity(leftAlpha);\n\t\t\t\tq.drawPixmap(\n\t\t\t\t\t0,\n\t\t\t\t\t0,\n\t\t\t\t\t_cacheUnder,\n\t\t\t\t\t_cacheUnder.width()\n\t\t\t\t\t\t- leftWidth * style::DevicePixelRatio(),\n\t\t\t\t\t0,\n\t\t\t\t\tleftWidth * style::DevicePixelRatio(),\n\t\t\t\t\t_topSkip * retina);\n\t\t\t}\n\n\t\t\tif (rightWidth > 0) {\n\t\t\t\tq.setOpacity(rightAlpha);\n\t\t\t\tq.drawPixmap(\n\t\t\t\t\trightCoord,\n\t\t\t\t\t0,\n\t\t\t\t\t_cacheOver,\n\t\t\t\t\t0,\n\t\t\t\t\t0,\n\t\t\t\t\trightWidth * style::DevicePixelRatio(),\n\t\t\t\t\t_topSkip * retina);\n\t\t\t}\n\n\t\t\tq.setOpacity(1.);\n\t\t\tq.setCompositionMode(QPainter::CompositionMode_DestinationIn);\n\t\t\tq.drawPixmap(0, 0, _mask);\n\n\t\t\tp.drawImage(0, 0, frame);\n\t\t}\n\n\t\tif (leftWidth > 0) {\n\t\t\tp.setOpacity(leftAlpha);\n\t\t\tp.drawPixmap(\n\t\t\t\t0,\n\t\t\t\t_topSkip,\n\t\t\t\t_cacheUnder,\n\t\t\t\t(_cacheUnder.width() - leftWidth * retina),\n\t\t\t\t_topSkip * retina,\n\t\t\t\tleftWidth * retina,\n\t\t\t\t_cacheUnder.height() - _topSkip * retina);\n\t\t}\n\t\tif (rightWidth > 0) {\n\t\t\tp.setOpacity(rightAlpha);\n\t\t\tp.drawPixmap(\n\t\t\t\trightCoord,\n\t\t\t\t_topSkip,\n\t\t\t\t_cacheOver,\n\t\t\t\t0,\n\t\t\t\t_topSkip * retina,\n\t\t\t\trightWidth * retina,\n\t\t\t\t_cacheOver.height() - _topSkip * retina);\n\t\t}\n\t} else {\n\t\tconst auto coordUnder = anim::interpolate(\n\t\t\t0,\n\t\t\t-st::slideShift,\n\t\t\tprogress);\n\t\tconst auto coordOver = anim::interpolate(\n\t\t\t_cacheOver.width() / retina,\n\t\t\t0,\n\t\t\tprogress);\n\t\tif (coordOver) {\n\t\t\tp.drawPixmap(\n\t\t\t\tQRect(0, 0, coordOver, _cacheUnder.height() / retina),\n\t\t\t\t_cacheUnder,\n\t\t\t\tQRect(\n\t\t\t\t\t-coordUnder * retina,\n\t\t\t\t\t0,\n\t\t\t\t\tcoordOver * retina,\n\t\t\t\t\t_cacheUnder.height()));\n\t\t\tp.setOpacity(progress);\n\t\t\tp.fillRect(\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\tcoordOver, _cacheUnder.height() / retina,\n\t\t\t\tst::slideFadeOutBg);\n\t\t\tp.setOpacity(1);\n\t\t}\n\t\tp.drawPixmap(\n\t\t\tQRect(QPoint(coordOver, 0), _cacheOver.size() / retina),\n\t\t\t_cacheOver,\n\t\t\tQRect(QPoint(), _cacheOver.size()));\n\t\tp.setOpacity(progress);\n\t\tst::slideShadow.fill(\n\t\t\tp,\n\t\t\tQRect(\n\t\t\t\tcoordOver - st::slideShadow.width(),\n\t\t\t\t0,\n\t\t\t\tst::slideShadow.width(),\n\t\t\t\t_cacheOver.height() / retina));\n\t}\n}\n\nfloat64 SlideAnimation::progress() const {\n\tconst auto slideLeft = (_direction == SlideDirection::FromLeft);\n\tconst auto progress = _animation.value(slideLeft ? 0. : 1.);\n\treturn slideLeft ? (1. - progress) : progress;\n}\n\nvoid SlideAnimation::setDirection(SlideDirection direction) {\n\t_direction = direction;\n}\n\nvoid SlideAnimation::setPixmaps(\n\t\tconst QPixmap &oldContentCache,\n\t\tconst QPixmap &newContentCache) {\n\t_cacheUnder = oldContentCache;\n\t_cacheOver = newContentCache;\n}\n\nvoid SlideAnimation::setTopBarShadow(bool enabled) {\n\t_topBarShadowEnabled = enabled;\n}\n\nvoid SlideAnimation::setTopSkip(int skip) {\n\t_topSkip = skip;\n}\n\nvoid SlideAnimation::setWithFade(bool withFade) {\n\t_withFade = withFade;\n}\n\nvoid SlideAnimation::setRepaintCallback(RepaintCallback &&callback) {\n\t_repaintCallback = std::move(callback);\n}\n\nvoid SlideAnimation::setFinishedCallback(FinishedCallback &&callback) {\n\t_finishedCallback = std::move(callback);\n}\n\nvoid SlideAnimation::setTopBarMask(const QPixmap &mask) {\n\t_mask = mask;\n}\n\nvoid SlideAnimation::start() {\n\tconst auto fromLeft = (_direction == SlideDirection::FromLeft);\n\tif (fromLeft) {\n\t\tstd::swap(_cacheUnder, _cacheOver);\n\t}\n\t_animation.start(\n\t\t[this] { animationCallback(); },\n\t\tfromLeft ? 1. : 0.,\n\t\tfromLeft ? 0. : 1.,\n\t\tst::slideDuration,\n\t\ttransition());\n\tif (const auto onstack = _repaintCallback) {\n\t\tonstack();\n\t}\n}\n\nvoid SlideAnimation::animationCallback() {\n\tif (const auto onstack = _repaintCallback) {\n\t\tonstack();\n\t}\n\tif (!_animation.animating()) {\n\t\tif (const auto onstack = _finishedCallback) {\n\t\t\tonstack();\n\t\t}\n\t}\n}\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_slide_animation.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/effects/animations.h\"\n\nnamespace Window {\n\nenum class SlideDirection {\n\tFromRight,\n\tFromLeft,\n};\n\nclass SlideAnimation {\npublic:\n\tvoid paintContents(QPainter &p) const;\n\n\t[[nodiscard]] float64 progress() const;\n\n\tvoid setDirection(SlideDirection direction);\n\tvoid setPixmaps(\n\t\tconst QPixmap &oldContentCache,\n\t\tconst QPixmap &newContentCache);\n\tvoid setTopBarShadow(bool enabled);\n\tvoid setTopSkip(int skip);\n\tvoid setTopBarMask(const QPixmap &mask);\n\tvoid setWithFade(bool withFade);\n\n\tusing RepaintCallback = Fn<void()>;\n\tvoid setRepaintCallback(RepaintCallback &&callback);\n\n\tusing FinishedCallback = Fn<void()>;\n\tvoid setFinishedCallback(FinishedCallback &&callback);\n\n\tvoid start();\n\n\tstatic const anim::transition &transition() {\n\t\treturn anim::easeOutCirc;\n\t}\n\nprivate:\n\tvoid animationCallback();\n\n\tSlideDirection _direction = SlideDirection::FromRight;\n\tint _topSkip = 0;\n\tbool _topBarShadowEnabled = false;\n\tbool _withFade = false;\n\n\tmutable Ui::Animations::Simple _animation;\n\tQPixmap _cacheUnder, _cacheOver;\n\tQPixmap _mask;\n\n\tRepaintCallback _repaintCallback;\n\tFinishedCallback _finishedCallback;\n\n};\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/SourceFiles/window/window_top_bar_wrap.h",
    "content": "/*\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n*/\n#pragma once\n\n#include \"ui/wrap/slide_wrap.h\"\n\nnamespace Window {\n\ntemplate <typename Inner>\nclass TopBarWrapWidget : public Ui::SlideWrap<Inner> {\n\tusing Parent = Ui::SlideWrap<Inner>;\n\npublic:\n\tTopBarWrapWidget(\n\t\tQWidget *parent,\n\t\tobject_ptr<Inner> inner,\n\t\trpl::producer<bool> oneColumnValue)\n\t: Parent(parent, std::move(inner)) {\n\t\tthis->sizeValue(\n\t\t) | rpl::on_next([=](const QSize &size) {\n\t\t\tupdateShadowGeometry(size);\n\t\t}, this->lifetime());\n\n\t\tstd::move(\n\t\t\toneColumnValue\n\t\t) | rpl::on_next([=](bool oneColumn) {\n\t\t\t_isOneColumn = oneColumn;\n\t\t}, this->lifetime());\n\t}\n\n\tvoid updateAdaptiveLayout() {\n\t\tupdateShadowGeometry(this->size());\n\t}\n\tvoid showShadow() {\n\t\tthis->entity()->showShadow();\n\t}\n\tvoid hideShadow() {\n\t\tthis->entity()->hideShadow();\n\t}\n\tint contentHeight() const {\n\t\treturn qMax(this->height() - st::lineWidth, 0);\n\t}\n\nprivate:\n\tvoid updateShadowGeometry(const QSize &size) {\n\t\tconst auto skip = _isOneColumn ? 0 : st::lineWidth;\n\t\tthis->entity()->setShadowGeometryToLeft(\n\t\t\tskip,\n\t\t\tsize.height() - st::lineWidth,\n\t\t\tsize.width() - skip,\n\t\t\tst::lineWidth);\n\t}\n\n\tbool _isOneColumn = false;\n\n};\n\n} // namespace Window\n"
  },
  {
    "path": "Telegram/Telegram/Breakpad.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n    <key>com.apple.security.inherit</key>\n    <true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "Telegram/Telegram/Images.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"icon16.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"icon16@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"32x32\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"icon32.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"32x32\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"icon32@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"128x128\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"icon128.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"128x128\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"icon128@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"256x256\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"icon256.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"256x256\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"icon256@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"512x512\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"icon512.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"512x512\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"icon512@2x.png\",\n      \"scale\" : \"2x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Telegram/Telegram/Telegram Lite.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n\t<key>com.apple.security.files.downloads.read-write</key>\n\t<true/>\n\t<key>com.apple.security.files.user-selected.read-write</key>\n\t<true/>\n\t<key>com.apple.security.files.bookmarks.app-scope</key>\n\t<true/>\n\t<key>com.apple.security.network.client</key>\n\t<true/>\n\t<key>com.apple.security.network.server</key>\n\t<true/>\n\t<key>com.apple.security.device.audio-input</key>\n\t<true/>\n\t<key>com.apple.security.device.camera</key>\n\t<true/>\n\t<key>com.apple.security.personal-information.location</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "Telegram/Telegram/Telegram.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.device.audio-input</key>\n\t<true/>\n\t<key>com.apple.security.device.camera</key>\n\t<true/>\n\t<key>com.apple.security.personal-information.location</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "Telegram/Telegram.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleExecutable</key>\n\t<string>@output_name@</string>\n\t<key>CFBundleGetInfoString</key>\n\t<string>Telegram Desktop messaging app</string>\n\t<key>CFBundleIconFile</key>\n\t<string>Icon.icns</string>\n\t<key>CFBundleIconName</key>\n\t<string>Icon.icns</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>@bundle_identifier_plist@</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>@output_name@</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>@desktop_app_version_string@</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleSupportedPlatforms</key>\n\t<array>\n\t\t<string>MacOSX</string>\n\t</array>\n\t<key>CFBundleURLTypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>CFBundleTypeRole</key>\n\t\t\t<string>Viewer</string>\n\t\t\t<key>CFBundleURLIconFile</key>\n\t\t\t<string>Icon.icns</string>\n\t\t\t<key>CFBundleURLName</key>\n\t\t\t<string>@bundle_identifier_plist@</string>\n\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t<array>\n\t\t\t\t<string>tg</string>\n\t\t\t\t<string>tonsite</string>\n\t\t\t</array>\n\t\t</dict>\n\t</array>\n\t<key>CFBundleDocumentTypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>CFBundleTypeName</key>\n\t\t\t<string>Any File</string>\n\t\t\t<key>CFBundleTypeRole</key>\n\t\t\t<string>Viewer</string>\n\t\t\t<key>LSHandlerRank</key>\n\t\t\t<string>Alternate</string>\n\t\t\t<key>LSItemContentTypes</key>\n\t\t\t<array>\n\t\t\t\t<string>public.data</string>\n\t\t\t</array>\n\t\t</dict>\n\t</array>\n\t<key>CFBundleVersion</key>\n\t<string>@desktop_app_version_string@</string>\n\t<key>ITSAppUsesNonExemptEncryption</key>\n\t<false/>\n\t<key>LSApplicationCategoryType</key>\n\t<string>public.app-category.social-networking</string>\n\t<key>LSFileQuarantineEnabled</key>\n\t<true/>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>@CMAKE_OSX_DEPLOYMENT_TARGET@</string>\n\t<key>NOTE</key>\n\t<string></string>\n\t<key>NSMicrophoneUsageDescription</key>\n\t<string>We need access to your microphone so that you can record voice messages and make calls.</string>\n\t<key>NSCameraUsageDescription</key>\n\t<string>We need access to your camera so that you can record video messages and make video calls.</string>\n\t<key>NSLocationUsageDescription</key>\n\t<string>We need access to your location so that you can send your current locations.</string>\n\t<key>NSPrincipalClass</key>\n\t<string>NSApplication</string>\n\t<key>NSSupportsAutomaticGraphicsSwitching</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "Telegram/ThirdParty/minizip/crypt.h",
    "content": "/* crypt.h -- base code for crypt/uncrypt ZIPfile\n\n\n   Version 1.01e, February 12th, 2005\n\n   Copyright (C) 1998-2005 Gilles Vollant\n\n   This code is a modified version of crypting code in Infozip distribution\n\n   The encryption/decryption parts of this source code (as opposed to the\n   non-echoing password parts) were originally written in Europe.  The\n   whole source package can be freely distributed, including from the USA.\n   (Prior to January 2000, re-export from the US was a violation of US law.)\n\n   This encryption code is a direct transcription of the algorithm from\n   Roger Schlafly, described by Phil Katz in the file appnote.txt.  This\n   file (appnote.txt) is distributed with the PKZIP program (even in the\n   version without encryption capabilities).\n\n   If you don't need crypting in your application, just define symbols\n   NOCRYPT and NOUNCRYPT.\n\n   This code support the \"Traditional PKWARE Encryption\".\n\n   The new AES encryption added on Zip format by Winzip (see the page\n   http://www.winzip.com/aes_info.htm ) and PKWare PKZip 5.x Strong\n   Encryption is not supported.\n*/\n\n#define CRC32(c, b) ((*(pcrc_32_tab+(((int)(c) ^ (b)) & 0xff))) ^ ((c) >> 8))\n\n/***********************************************************************\n * Return the next byte in the pseudo-random sequence\n */\nstatic int decrypt_byte(unsigned long* pkeys, const z_crc_t* pcrc_32_tab) {\n    unsigned temp;  /* POTENTIAL BUG:  temp*(temp^1) may overflow in an\n                     * unpredictable manner on 16-bit systems; not a problem\n                     * with any known compiler so far, though */\n\n    (void)pcrc_32_tab;\n    temp = ((unsigned)(*(pkeys+2)) & 0xffff) | 2;\n    return (int)(((temp * (temp ^ 1)) >> 8) & 0xff);\n}\n\n/***********************************************************************\n * Update the encryption keys with the next byte of plain text\n */\nstatic int update_keys(unsigned long* pkeys, const z_crc_t* pcrc_32_tab, int c) {\n    (*(pkeys+0)) = CRC32((*(pkeys+0)), c);\n    (*(pkeys+1)) += (*(pkeys+0)) & 0xff;\n    (*(pkeys+1)) = (*(pkeys+1)) * 134775813L + 1;\n    {\n      register int keyshift = (int)((*(pkeys+1)) >> 24);\n      (*(pkeys+2)) = CRC32((*(pkeys+2)), keyshift);\n    }\n    return c;\n}\n\n\n/***********************************************************************\n * Initialize the encryption keys and the random header according to\n * the given password.\n */\nstatic void init_keys(const char* passwd, unsigned long* pkeys, const z_crc_t* pcrc_32_tab) {\n    *(pkeys+0) = 305419896L;\n    *(pkeys+1) = 591751049L;\n    *(pkeys+2) = 878082192L;\n    while (*passwd != '\\0') {\n        update_keys(pkeys,pcrc_32_tab,(int)*passwd);\n        passwd++;\n    }\n}\n\n#define zdecode(pkeys,pcrc_32_tab,c) \\\n    (update_keys(pkeys,pcrc_32_tab,c ^= decrypt_byte(pkeys,pcrc_32_tab)))\n\n#define zencode(pkeys,pcrc_32_tab,c,t) \\\n    (t=decrypt_byte(pkeys,pcrc_32_tab), update_keys(pkeys,pcrc_32_tab,c), (Byte)t^(c))\n\n#ifdef INCLUDECRYPTINGCODE_IFCRYPTALLOWED\n\n#define RAND_HEAD_LEN  12\n   /* \"last resort\" source for second part of crypt seed pattern */\n#  ifndef ZCR_SEED2\n#    define ZCR_SEED2 3141592654UL      /* use PI as default pattern */\n#  endif\n\nstatic unsigned crypthead(const char* passwd,       /* password string */\n                          unsigned char* buf,       /* where to write header */\n                          int bufSize,\n                          unsigned long* pkeys,\n                          const z_crc_t* pcrc_32_tab,\n                          unsigned long crcForCrypting) {\n    unsigned n;                  /* index in random header */\n    int t;                       /* temporary */\n    int c;                       /* random byte */\n    unsigned char header[RAND_HEAD_LEN-2]; /* random header */\n    static unsigned calls = 0;   /* ensure different random header each time */\n\n    if (bufSize<RAND_HEAD_LEN)\n      return 0;\n\n    /* First generate RAND_HEAD_LEN-2 random bytes. We encrypt the\n     * output of rand() to get less predictability, since rand() is\n     * often poorly implemented.\n     */\n    if (++calls == 1)\n    {\n        srand((unsigned)(time(NULL) ^ ZCR_SEED2));\n    }\n    init_keys(passwd, pkeys, pcrc_32_tab);\n    for (n = 0; n < RAND_HEAD_LEN-2; n++)\n    {\n        c = (rand() >> 7) & 0xff;\n        header[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, c, t);\n    }\n    /* Encrypt random header (last two bytes is high word of crc) */\n    init_keys(passwd, pkeys, pcrc_32_tab);\n    for (n = 0; n < RAND_HEAD_LEN-2; n++)\n    {\n        buf[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, header[n], t);\n    }\n    buf[n++] = (unsigned char)zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 16) & 0xff, t);\n    buf[n++] = (unsigned char)zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 24) & 0xff, t);\n    return n;\n}\n\n#endif\n"
  },
  {
    "path": "Telegram/ThirdParty/minizip/ioapi.c",
    "content": "/* ioapi.h -- IO base function header for compress/uncompress .zip\n   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )\n\n         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )\n\n         Modifications for Zip64 support\n         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )\n\n         For more info read MiniZip_info.txt\n\n*/\n\n#if defined(_WIN32) && (!(defined(_CRT_SECURE_NO_WARNINGS)))\n        #define _CRT_SECURE_NO_WARNINGS\n#endif\n\n#if defined(__APPLE__) || defined(IOAPI_NO_64) || defined(__HAIKU__) || defined(MINIZIP_FOPEN_NO_64)\n// In darwin and perhaps other BSD variants off_t is a 64 bit value, hence no need for specific 64 bit functions\n#define FOPEN_FUNC(filename, mode) fopen(filename, mode)\n#define FTELLO_FUNC(stream) ftello(stream)\n#define FSEEKO_FUNC(stream, offset, origin) fseeko(stream, offset, origin)\n#else\n#define FOPEN_FUNC(filename, mode) fopen64(filename, mode)\n#define FTELLO_FUNC(stream) ftello64(stream)\n#define FSEEKO_FUNC(stream, offset, origin) fseeko64(stream, offset, origin)\n#endif\n\n\n#include \"ioapi.h\"\n\nvoidpf call_zopen64 (const zlib_filefunc64_32_def* pfilefunc, const void*filename, int mode) {\n    if (pfilefunc->zfile_func64.zopen64_file != NULL)\n        return (*(pfilefunc->zfile_func64.zopen64_file)) (pfilefunc->zfile_func64.opaque,filename,mode);\n    else\n    {\n        return (*(pfilefunc->zopen32_file))(pfilefunc->zfile_func64.opaque,(const char*)filename,mode);\n    }\n}\n\nlong call_zseek64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin) {\n    if (pfilefunc->zfile_func64.zseek64_file != NULL)\n        return (*(pfilefunc->zfile_func64.zseek64_file)) (pfilefunc->zfile_func64.opaque,filestream,offset,origin);\n    else\n    {\n        uLong offsetTruncated = (uLong)offset;\n        if (offsetTruncated != offset)\n            return -1;\n        else\n            return (*(pfilefunc->zseek32_file))(pfilefunc->zfile_func64.opaque,filestream,offsetTruncated,origin);\n    }\n}\n\nZPOS64_T call_ztell64 (const zlib_filefunc64_32_def* pfilefunc, voidpf filestream) {\n    if (pfilefunc->zfile_func64.zseek64_file != NULL)\n        return (*(pfilefunc->zfile_func64.ztell64_file)) (pfilefunc->zfile_func64.opaque,filestream);\n    else\n    {\n        uLong tell_uLong = (uLong)(*(pfilefunc->ztell32_file))(pfilefunc->zfile_func64.opaque,filestream);\n        if ((tell_uLong) == MAXU32)\n            return (ZPOS64_T)-1;\n        else\n            return tell_uLong;\n    }\n}\n\nvoid fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32, const zlib_filefunc_def* p_filefunc32) {\n    p_filefunc64_32->zfile_func64.zopen64_file = NULL;\n    p_filefunc64_32->zopen32_file = p_filefunc32->zopen_file;\n    p_filefunc64_32->zfile_func64.zread_file = p_filefunc32->zread_file;\n    p_filefunc64_32->zfile_func64.zwrite_file = p_filefunc32->zwrite_file;\n    p_filefunc64_32->zfile_func64.ztell64_file = NULL;\n    p_filefunc64_32->zfile_func64.zseek64_file = NULL;\n    p_filefunc64_32->zfile_func64.zclose_file = p_filefunc32->zclose_file;\n    p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file;\n    p_filefunc64_32->zfile_func64.opaque = p_filefunc32->opaque;\n    p_filefunc64_32->zseek32_file = p_filefunc32->zseek_file;\n    p_filefunc64_32->ztell32_file = p_filefunc32->ztell_file;\n}\n\n\n\nstatic voidpf ZCALLBACK fopen_file_func(voidpf opaque, const char* filename, int mode) {\n    FILE* file = NULL;\n    const char* mode_fopen = NULL;\n    (void)opaque;\n    if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ)\n        mode_fopen = \"rb\";\n    else\n    if (mode & ZLIB_FILEFUNC_MODE_EXISTING)\n        mode_fopen = \"r+b\";\n    else\n    if (mode & ZLIB_FILEFUNC_MODE_CREATE)\n        mode_fopen = \"wb\";\n\n    if ((filename!=NULL) && (mode_fopen != NULL))\n        file = fopen(filename, mode_fopen);\n    return file;\n}\n\nstatic voidpf ZCALLBACK fopen64_file_func(voidpf opaque, const void* filename, int mode) {\n    FILE* file = NULL;\n    const char* mode_fopen = NULL;\n    (void)opaque;\n    if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ)\n        mode_fopen = \"rb\";\n    else\n    if (mode & ZLIB_FILEFUNC_MODE_EXISTING)\n        mode_fopen = \"r+b\";\n    else\n    if (mode & ZLIB_FILEFUNC_MODE_CREATE)\n        mode_fopen = \"wb\";\n\n    if ((filename!=NULL) && (mode_fopen != NULL))\n        file = FOPEN_FUNC((const char*)filename, mode_fopen);\n    return file;\n}\n\n\nstatic uLong ZCALLBACK fread_file_func(voidpf opaque, voidpf stream, void* buf, uLong size) {\n    uLong ret;\n    (void)opaque;\n    ret = (uLong)fread(buf, 1, (size_t)size, (FILE *)stream);\n    return ret;\n}\n\nstatic uLong ZCALLBACK fwrite_file_func(voidpf opaque, voidpf stream, const void* buf, uLong size) {\n    uLong ret;\n    (void)opaque;\n    ret = (uLong)fwrite(buf, 1, (size_t)size, (FILE *)stream);\n    return ret;\n}\n\nstatic long ZCALLBACK ftell_file_func(voidpf opaque, voidpf stream) {\n    long ret;\n    (void)opaque;\n    ret = ftell((FILE *)stream);\n    return ret;\n}\n\n\nstatic ZPOS64_T ZCALLBACK ftell64_file_func(voidpf opaque, voidpf stream) {\n    ZPOS64_T ret;\n    (void)opaque;\n    ret = (ZPOS64_T)FTELLO_FUNC((FILE *)stream);\n    return ret;\n}\n\nstatic long ZCALLBACK fseek_file_func(voidpf opaque, voidpf stream, uLong offset, int origin) {\n    int fseek_origin=0;\n    long ret;\n    (void)opaque;\n    switch (origin)\n    {\n    case ZLIB_FILEFUNC_SEEK_CUR :\n        fseek_origin = SEEK_CUR;\n        break;\n    case ZLIB_FILEFUNC_SEEK_END :\n        fseek_origin = SEEK_END;\n        break;\n    case ZLIB_FILEFUNC_SEEK_SET :\n        fseek_origin = SEEK_SET;\n        break;\n    default: return -1;\n    }\n    ret = 0;\n    if (fseek((FILE *)stream, (long)offset, fseek_origin) != 0)\n        ret = -1;\n    return ret;\n}\n\nstatic long ZCALLBACK fseek64_file_func(voidpf opaque, voidpf stream, ZPOS64_T offset, int origin) {\n    int fseek_origin=0;\n    long ret;\n    (void)opaque;\n    switch (origin)\n    {\n    case ZLIB_FILEFUNC_SEEK_CUR :\n        fseek_origin = SEEK_CUR;\n        break;\n    case ZLIB_FILEFUNC_SEEK_END :\n        fseek_origin = SEEK_END;\n        break;\n    case ZLIB_FILEFUNC_SEEK_SET :\n        fseek_origin = SEEK_SET;\n        break;\n    default: return -1;\n    }\n    ret = 0;\n\n    if(FSEEKO_FUNC((FILE *)stream, (z_off64_t)offset, fseek_origin) != 0)\n                        ret = -1;\n\n    return ret;\n}\n\n\nstatic int ZCALLBACK fclose_file_func(voidpf opaque, voidpf stream) {\n    int ret;\n    (void)opaque;\n    ret = fclose((FILE *)stream);\n    return ret;\n}\n\nstatic int ZCALLBACK ferror_file_func(voidpf opaque, voidpf stream) {\n    int ret;\n    (void)opaque;\n    ret = ferror((FILE *)stream);\n    return ret;\n}\n\nvoid fill_fopen_filefunc(zlib_filefunc_def* pzlib_filefunc_def) {\n    pzlib_filefunc_def->zopen_file = fopen_file_func;\n    pzlib_filefunc_def->zread_file = fread_file_func;\n    pzlib_filefunc_def->zwrite_file = fwrite_file_func;\n    pzlib_filefunc_def->ztell_file = ftell_file_func;\n    pzlib_filefunc_def->zseek_file = fseek_file_func;\n    pzlib_filefunc_def->zclose_file = fclose_file_func;\n    pzlib_filefunc_def->zerror_file = ferror_file_func;\n    pzlib_filefunc_def->opaque = NULL;\n}\n\nvoid fill_fopen64_filefunc(zlib_filefunc64_def* pzlib_filefunc_def) {\n    pzlib_filefunc_def->zopen64_file = fopen64_file_func;\n    pzlib_filefunc_def->zread_file = fread_file_func;\n    pzlib_filefunc_def->zwrite_file = fwrite_file_func;\n    pzlib_filefunc_def->ztell64_file = ftell64_file_func;\n    pzlib_filefunc_def->zseek64_file = fseek64_file_func;\n    pzlib_filefunc_def->zclose_file = fclose_file_func;\n    pzlib_filefunc_def->zerror_file = ferror_file_func;\n    pzlib_filefunc_def->opaque = NULL;\n}\n"
  },
  {
    "path": "Telegram/ThirdParty/minizip/ioapi.h",
    "content": "/* ioapi.h -- IO base function header for compress/uncompress .zip\n   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )\n\n         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )\n\n         Modifications for Zip64 support\n         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )\n\n         For more info read MiniZip_info.txt\n\n         Changes\n\n    Oct-2009 - Defined ZPOS64_T to fpos_t on windows and u_int64_t on linux. (might need to find a better why for this)\n    Oct-2009 - Change to fseeko64, ftello64 and fopen64 so large files would work on linux.\n               More if/def section may be needed to support other platforms\n    Oct-2009 - Defined fxxxx64 calls to normal fopen/ftell/fseek so they would compile on windows.\n                          (but you should use iowin32.c for windows instead)\n\n*/\n\n#ifndef _ZLIBIOAPI64_H\n#define _ZLIBIOAPI64_H\n\n#if (!defined(_WIN32)) && (!defined(WIN32)) && (!defined(__APPLE__))\n\n  // Linux needs this to support file operation on files larger then 4+GB\n  // But might need better if/def to select just the platforms that needs them.\n\n        #ifndef __USE_FILE_OFFSET64\n                #define __USE_FILE_OFFSET64\n        #endif\n        #ifndef __USE_LARGEFILE64\n                #define __USE_LARGEFILE64\n        #endif\n        #ifndef _LARGEFILE64_SOURCE\n                #define _LARGEFILE64_SOURCE\n        #endif\n        #ifndef _FILE_OFFSET_BIT\n                #define _FILE_OFFSET_BIT 64\n        #endif\n\n#endif\n\n#include <stdio.h>\n#include <stdlib.h>\n#include \"zlib.h\"\n\n#if defined(USE_FILE32API)\n#define fopen64 fopen\n#define ftello64 ftell\n#define fseeko64 fseek\n#else\n#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__HAIKU__) || defined(MINIZIP_FOPEN_NO_64)\n#define fopen64 fopen\n#define ftello64 ftello\n#define fseeko64 fseeko\n#endif\n#ifdef _MSC_VER\n #define fopen64 fopen\n #if (_MSC_VER >= 1400) && (!(defined(NO_MSCVER_FILE64_FUNC)))\n  #define ftello64 _ftelli64\n  #define fseeko64 _fseeki64\n #else // old MSC\n  #define ftello64 ftell\n  #define fseeko64 fseek\n #endif\n#endif\n#endif\n\n/*\n#ifndef ZPOS64_T\n  #ifdef _WIN32\n                #define ZPOS64_T fpos_t\n  #else\n    #include <stdint.h>\n    #define ZPOS64_T uint64_t\n  #endif\n#endif\n*/\n\n#ifdef HAVE_MINIZIP64_CONF_H\n#include \"mz64conf.h\"\n#endif\n\n/* a type chosen by DEFINE */\n#ifdef HAVE_64BIT_INT_CUSTOM\ntypedef  64BIT_INT_CUSTOM_TYPE ZPOS64_T;\n#else\n#ifdef HAS_STDINT_H\n#include \"stdint.h\"\ntypedef uint64_t ZPOS64_T;\n#else\n\n\n\n#if defined(_MSC_VER) || defined(__BORLANDC__)\ntypedef unsigned __int64 ZPOS64_T;\n#else\ntypedef unsigned long long int ZPOS64_T;\n#endif\n#endif\n#endif\n\n/* Maximum unsigned 32-bit value used as placeholder for zip64 */\n#ifndef MAXU32\n#define MAXU32 (0xffffffff)\n#endif\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n\n#define ZLIB_FILEFUNC_SEEK_CUR (1)\n#define ZLIB_FILEFUNC_SEEK_END (2)\n#define ZLIB_FILEFUNC_SEEK_SET (0)\n\n#define ZLIB_FILEFUNC_MODE_READ      (1)\n#define ZLIB_FILEFUNC_MODE_WRITE     (2)\n#define ZLIB_FILEFUNC_MODE_READWRITEFILTER (3)\n\n#define ZLIB_FILEFUNC_MODE_EXISTING (4)\n#define ZLIB_FILEFUNC_MODE_CREATE   (8)\n\n\n#ifndef ZCALLBACK\n #if (defined(WIN32) || defined(_WIN32) || defined (WINDOWS) || defined (_WINDOWS)) && defined(CALLBACK) && defined (USEWINDOWS_CALLBACK)\n   #define ZCALLBACK CALLBACK\n #else\n   #define ZCALLBACK\n #endif\n#endif\n\n\n\n\ntypedef voidpf   (ZCALLBACK *open_file_func)      (voidpf opaque, const char* filename, int mode);\ntypedef uLong    (ZCALLBACK *read_file_func)      (voidpf opaque, voidpf stream, void* buf, uLong size);\ntypedef uLong    (ZCALLBACK *write_file_func)     (voidpf opaque, voidpf stream, const void* buf, uLong size);\ntypedef int      (ZCALLBACK *close_file_func)     (voidpf opaque, voidpf stream);\ntypedef int      (ZCALLBACK *testerror_file_func) (voidpf opaque, voidpf stream);\n\ntypedef long     (ZCALLBACK *tell_file_func)      (voidpf opaque, voidpf stream);\ntypedef long     (ZCALLBACK *seek_file_func)      (voidpf opaque, voidpf stream, uLong offset, int origin);\n\n\n/* here is the \"old\" 32 bits structure */\ntypedef struct zlib_filefunc_def_s\n{\n    open_file_func      zopen_file;\n    read_file_func      zread_file;\n    write_file_func     zwrite_file;\n    tell_file_func      ztell_file;\n    seek_file_func      zseek_file;\n    close_file_func     zclose_file;\n    testerror_file_func zerror_file;\n    voidpf              opaque;\n} zlib_filefunc_def;\n\ntypedef ZPOS64_T (ZCALLBACK *tell64_file_func)    (voidpf opaque, voidpf stream);\ntypedef long     (ZCALLBACK *seek64_file_func)    (voidpf opaque, voidpf stream, ZPOS64_T offset, int origin);\ntypedef voidpf   (ZCALLBACK *open64_file_func)    (voidpf opaque, const void* filename, int mode);\n\ntypedef struct zlib_filefunc64_def_s\n{\n    open64_file_func    zopen64_file;\n    read_file_func      zread_file;\n    write_file_func     zwrite_file;\n    tell64_file_func    ztell64_file;\n    seek64_file_func    zseek64_file;\n    close_file_func     zclose_file;\n    testerror_file_func zerror_file;\n    voidpf              opaque;\n} zlib_filefunc64_def;\n\nvoid fill_fopen64_filefunc(zlib_filefunc64_def* pzlib_filefunc_def);\nvoid fill_fopen_filefunc(zlib_filefunc_def* pzlib_filefunc_def);\n\n/* now internal definition, only for zip.c and unzip.h */\ntypedef struct zlib_filefunc64_32_def_s\n{\n    zlib_filefunc64_def zfile_func64;\n    open_file_func      zopen32_file;\n    tell_file_func      ztell32_file;\n    seek_file_func      zseek32_file;\n} zlib_filefunc64_32_def;\n\n\n#define ZREAD64(filefunc,filestream,buf,size)     ((*((filefunc).zfile_func64.zread_file))   ((filefunc).zfile_func64.opaque,filestream,buf,size))\n#define ZWRITE64(filefunc,filestream,buf,size)    ((*((filefunc).zfile_func64.zwrite_file))  ((filefunc).zfile_func64.opaque,filestream,buf,size))\n//#define ZTELL64(filefunc,filestream)            ((*((filefunc).ztell64_file)) ((filefunc).opaque,filestream))\n//#define ZSEEK64(filefunc,filestream,pos,mode)   ((*((filefunc).zseek64_file)) ((filefunc).opaque,filestream,pos,mode))\n#define ZCLOSE64(filefunc,filestream)             ((*((filefunc).zfile_func64.zclose_file))  ((filefunc).zfile_func64.opaque,filestream))\n#define ZERROR64(filefunc,filestream)             ((*((filefunc).zfile_func64.zerror_file))  ((filefunc).zfile_func64.opaque,filestream))\n\nvoidpf call_zopen64(const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode);\nlong call_zseek64(const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin);\nZPOS64_T call_ztell64(const zlib_filefunc64_32_def* pfilefunc,voidpf filestream);\n\nvoid fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32);\n\n#define ZOPEN64(filefunc,filename,mode)         (call_zopen64((&(filefunc)),(filename),(mode)))\n#define ZTELL64(filefunc,filestream)            (call_ztell64((&(filefunc)),(filestream)))\n#define ZSEEK64(filefunc,filestream,pos,mode)   (call_zseek64((&(filefunc)),(filestream),(pos),(mode)))\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif\n"
  },
  {
    "path": "Telegram/ThirdParty/minizip/unzip.c",
    "content": "/* unzip.c -- IO for uncompress .zip files using zlib\n   Version 1.1, February 14h, 2010\n   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )\n\n         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )\n\n         Modifications of Unzip for Zip64\n         Copyright (C) 2007-2008 Even Rouault\n\n         Modifications for Zip64 support on both zip and unzip\n         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )\n\n         For more info read MiniZip_info.txt\n\n\n  ------------------------------------------------------------------------------------\n  Decryption code comes from crypt.c by Info-ZIP but has been greatly reduced in terms of\n  compatibility with older software. The following is from the original crypt.c.\n  Code woven in by Terry Thorsen 1/2003.\n\n  Copyright (c) 1990-2000 Info-ZIP.  All rights reserved.\n\n  See the accompanying file LICENSE, version 2000-Apr-09 or later\n  (the contents of which are also included in zip.h) for terms of use.\n  If, for some reason, all these files are missing, the Info-ZIP license\n  also may be found at:  ftp://ftp.info-zip.org/pub/infozip/license.html\n\n        crypt.c (full version) by Info-ZIP.      Last revised:  [see crypt.h]\n\n  The encryption/decryption parts of this source code (as opposed to the\n  non-echoing password parts) were originally written in Europe.  The\n  whole source package can be freely distributed, including from the USA.\n  (Prior to January 2000, re-export from the US was a violation of US law.)\n\n        This encryption code is a direct transcription of the algorithm from\n  Roger Schlafly, described by Phil Katz in the file appnote.txt.  This\n  file (appnote.txt) is distributed with the PKZIP program (even in the\n  version without encryption capabilities).\n\n        ------------------------------------------------------------------------------------\n\n        Changes in unzip.c\n\n        2007-2008 - Even Rouault - Addition of cpl_unzGetCurrentFileZStreamPos\n  2007-2008 - Even Rouault - Decoration of symbol names unz* -> cpl_unz*\n  2007-2008 - Even Rouault - Remove old C style function prototypes\n  2007-2008 - Even Rouault - Add unzip support for ZIP64\n\n        Copyright (C) 2007-2008 Even Rouault\n\n\n  Oct-2009 - Mathias Svensson - Removed cpl_* from symbol names (Even Rouault added them but since this is now moved to a new project (minizip64) I renamed them again).\n  Oct-2009 - Mathias Svensson - Fixed problem if uncompressed size was > 4G and compressed size was <4G\n                                should only read the compressed/uncompressed size from the Zip64 format if\n                                the size from normal header was 0xFFFFFFFF\n  Oct-2009 - Mathias Svensson - Applied some bug fixes from patches received from Gilles Vollant\n  Oct-2009 - Mathias Svensson - Applied support to unzip files with compression method BZIP2 (bzip2 lib is required)\n                                Patch created by Daniel Borca\n\n  Jan-2010 - back to unzip and minizip 1.0 name scheme, with compatibility layer\n\n  Copyright (C) 1998 - 2010 Gilles Vollant, Even Rouault, Mathias Svensson\n\n*/\n\n\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\n#ifndef NOUNCRYPT\n        #define NOUNCRYPT\n#endif\n\n#include \"zlib.h\"\n#include \"unzip.h\"\n\n#ifdef STDC\n#  include <stddef.h>\n#endif\n#ifdef NO_ERRNO_H\n    extern int errno;\n#else\n#   include <errno.h>\n#endif\n\n\n#ifndef local\n#  define local static\n#endif\n/* compile with -Dlocal if your debugger can't find static symbols */\n\n\n#ifndef CASESENSITIVITYDEFAULT_NO\n#  if !defined(unix) && !defined(CASESENSITIVITYDEFAULT_YES)\n#    define CASESENSITIVITYDEFAULT_NO\n#  endif\n#endif\n\n\n#ifndef UNZ_BUFSIZE\n#define UNZ_BUFSIZE (16384)\n#endif\n\n#ifndef UNZ_MAXFILENAMEINZIP\n#define UNZ_MAXFILENAMEINZIP (256)\n#endif\n\n#ifndef ALLOC\n# define ALLOC(size) (malloc(size))\n#endif\n\n#define SIZECENTRALDIRITEM (0x2e)\n#define SIZEZIPLOCALHEADER (0x1e)\n\n\nconst char unz_copyright[] =\n   \" unzip 1.01 Copyright 1998-2004 Gilles Vollant - http://www.winimage.com/zLibDll\";\n\n/* unz_file_info_interntal contain internal info about a file in zipfile*/\ntypedef struct unz_file_info64_internal_s\n{\n    ZPOS64_T offset_curfile;/* relative offset of local header 8 bytes */\n} unz_file_info64_internal;\n\n\n/* file_in_zip_read_info_s contain internal information about a file in zipfile,\n    when reading and decompress it */\ntypedef struct\n{\n    char  *read_buffer;         /* internal buffer for compressed data */\n    z_stream stream;            /* zLib stream structure for inflate */\n\n#ifdef HAVE_BZIP2\n    bz_stream bstream;          /* bzLib stream structure for bziped */\n#endif\n\n    ZPOS64_T pos_in_zipfile;       /* position in byte on the zipfile, for fseek*/\n    uLong stream_initialised;   /* flag set if stream structure is initialised*/\n\n    ZPOS64_T offset_local_extrafield;/* offset of the local extra field */\n    uInt  size_local_extrafield;/* size of the local extra field */\n    ZPOS64_T pos_local_extrafield;   /* position in the local extra field in read*/\n    ZPOS64_T total_out_64;\n\n    uLong crc32;                /* crc32 of all data uncompressed */\n    uLong crc32_wait;           /* crc32 we must obtain after decompress all */\n    ZPOS64_T rest_read_compressed; /* number of byte to be decompressed */\n    ZPOS64_T rest_read_uncompressed;/*number of byte to be obtained after decomp*/\n    zlib_filefunc64_32_def z_filefunc;\n    voidpf filestream;        /* io structure of the zipfile */\n    uLong compression_method;   /* compression method (0==store) */\n    ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/\n    int   raw;\n} file_in_zip64_read_info_s;\n\n\n/* unz64_s contain internal information about the zipfile\n*/\ntypedef struct\n{\n    zlib_filefunc64_32_def z_filefunc;\n    int is64bitOpenFunction;\n    voidpf filestream;        /* io structure of the zipfile */\n    unz_global_info64 gi;       /* public global information */\n    ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/\n    ZPOS64_T num_file;             /* number of the current file in the zipfile*/\n    ZPOS64_T pos_in_central_dir;   /* pos of the current file in the central dir*/\n    ZPOS64_T current_file_ok;      /* flag about the usability of the current file*/\n    ZPOS64_T central_pos;          /* position of the beginning of the central dir*/\n\n    ZPOS64_T size_central_dir;     /* size of the central directory  */\n    ZPOS64_T offset_central_dir;   /* offset of start of central directory with\n                                   respect to the starting disk number */\n\n    unz_file_info64 cur_file_info; /* public info about the current file in zip*/\n    unz_file_info64_internal cur_file_info_internal; /* private info about it*/\n    file_in_zip64_read_info_s* pfile_in_zip_read; /* structure about the current\n                                        file if we are decompressing it */\n    int encrypted;\n\n    int isZip64;\n\n#    ifndef NOUNCRYPT\n    unsigned long keys[3];     /* keys defining the pseudo-random sequence */\n    const z_crc_t* pcrc_32_tab;\n#    endif\n} unz64_s;\n\n\n#ifndef NOUNCRYPT\n#include \"crypt.h\"\n#endif\n\n\n/* ===========================================================================\n   Reads a long in LSB order from the given gz_stream. Sets\n*/\n\nlocal int unz64local_getShort(const zlib_filefunc64_32_def* pzlib_filefunc_def,\n                              voidpf filestream,\n                              uLong *pX) {\n    unsigned char c[2];\n    int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,c,2);\n    if (err==2)\n    {\n        *pX = c[0] | ((uLong)c[1] << 8);\n        return UNZ_OK;\n    }\n    else\n    {\n        *pX = 0;\n        if (ZERROR64(*pzlib_filefunc_def,filestream))\n            return UNZ_ERRNO;\n        else\n            return UNZ_EOF;\n    }\n}\n\nlocal int unz64local_getLong(const zlib_filefunc64_32_def* pzlib_filefunc_def,\n                             voidpf filestream,\n                             uLong *pX) {\n    unsigned char c[4];\n    int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,c,4);\n    if (err==4)\n    {\n        *pX = c[0] | ((uLong)c[1] << 8) | ((uLong)c[2] << 16) | ((uLong)c[3] << 24);\n        return UNZ_OK;\n    }\n    else\n    {\n        *pX = 0;\n        if (ZERROR64(*pzlib_filefunc_def,filestream))\n            return UNZ_ERRNO;\n        else\n            return UNZ_EOF;\n    }\n}\n\n\nlocal int unz64local_getLong64(const zlib_filefunc64_32_def* pzlib_filefunc_def,\n                               voidpf filestream,\n                               ZPOS64_T *pX) {\n    unsigned char c[8];\n    int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,c,8);\n    if (err==8)\n    {\n        *pX = c[0] | ((ZPOS64_T)c[1] << 8) | ((ZPOS64_T)c[2] << 16) | ((ZPOS64_T)c[3] << 24)\n            | ((ZPOS64_T)c[4] << 32) | ((ZPOS64_T)c[5] << 40) | ((ZPOS64_T)c[6] << 48) | ((ZPOS64_T)c[7] << 56);\n        return UNZ_OK;\n    }\n    else\n    {\n        *pX = 0;\n        if (ZERROR64(*pzlib_filefunc_def,filestream))\n            return UNZ_ERRNO;\n        else\n            return UNZ_EOF;\n    }\n}\n\n/* My own strcmpi / strcasecmp */\nlocal int strcmpcasenosensitive_internal(const char* fileName1, const char* fileName2) {\n    for (;;)\n    {\n        char c1=*(fileName1++);\n        char c2=*(fileName2++);\n        if ((c1>='a') && (c1<='z'))\n            c1 -= 0x20;\n        if ((c2>='a') && (c2<='z'))\n            c2 -= 0x20;\n        if (c1=='\\0')\n            return ((c2=='\\0') ? 0 : -1);\n        if (c2=='\\0')\n            return 1;\n        if (c1<c2)\n            return -1;\n        if (c1>c2)\n            return 1;\n    }\n}\n\n\n#ifdef  CASESENSITIVITYDEFAULT_NO\n#define CASESENSITIVITYDEFAULTVALUE 2\n#else\n#define CASESENSITIVITYDEFAULTVALUE 1\n#endif\n\n#ifndef STRCMPCASENOSENTIVEFUNCTION\n#define STRCMPCASENOSENTIVEFUNCTION strcmpcasenosensitive_internal\n#endif\n\n/*\n   Compare two filenames (fileName1,fileName2).\n   If iCaseSensitivity = 1, comparison is case sensitive (like strcmp)\n   If iCaseSensitivity = 2, comparison is not case sensitive (like strcmpi\n                                                                or strcasecmp)\n   If iCaseSensitivity = 0, case sensitivity is default of your operating system\n        (like 1 on Unix, 2 on Windows)\n\n*/\nextern int ZEXPORT unzStringFileNameCompare (const char*  fileName1,\n                                             const char*  fileName2,\n                                             int iCaseSensitivity) {\n    if (iCaseSensitivity==0)\n        iCaseSensitivity=CASESENSITIVITYDEFAULTVALUE;\n\n    if (iCaseSensitivity==1)\n        return strcmp(fileName1,fileName2);\n\n    return STRCMPCASENOSENTIVEFUNCTION(fileName1,fileName2);\n}\n\n#ifndef BUFREADCOMMENT\n#define BUFREADCOMMENT (0x400)\n#endif\n\n#ifndef CENTRALDIRINVALID\n#define CENTRALDIRINVALID ((ZPOS64_T)(-1))\n#endif\n\n/*\n  Locate the Central directory of a zipfile (at the end, just before\n    the global comment)\n*/\nlocal ZPOS64_T unz64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream) {\n    unsigned char* buf;\n    ZPOS64_T uSizeFile;\n    ZPOS64_T uBackRead;\n    ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */\n    ZPOS64_T uPosFound=CENTRALDIRINVALID;\n\n    if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)\n        return CENTRALDIRINVALID;\n\n\n    uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);\n\n    if (uMaxBack>uSizeFile)\n        uMaxBack = uSizeFile;\n\n    buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);\n    if (buf==NULL)\n        return CENTRALDIRINVALID;\n\n    uBackRead = 4;\n    while (uBackRead<uMaxBack)\n    {\n        uLong uReadSize;\n        ZPOS64_T uReadPos ;\n        int i;\n        if (uBackRead+BUFREADCOMMENT>uMaxBack)\n            uBackRead = uMaxBack;\n        else\n            uBackRead+=BUFREADCOMMENT;\n        uReadPos = uSizeFile-uBackRead ;\n\n        uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?\n                     (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);\n        if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)\n            break;\n\n        if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)\n            break;\n\n        for (i=(int)uReadSize-3; (i--)>0;)\n            if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) &&\n                ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06))\n            {\n                uPosFound = uReadPos+(unsigned)i;\n                break;\n            }\n\n        if (uPosFound!=CENTRALDIRINVALID)\n            break;\n    }\n    free(buf);\n    return uPosFound;\n}\n\n\n/*\n  Locate the Central directory 64 of a zipfile (at the end, just before\n    the global comment)\n*/\nlocal ZPOS64_T unz64local_SearchCentralDir64(const zlib_filefunc64_32_def* pzlib_filefunc_def,\n                                             voidpf filestream) {\n    unsigned char* buf;\n    ZPOS64_T uSizeFile;\n    ZPOS64_T uBackRead;\n    ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */\n    ZPOS64_T uPosFound=CENTRALDIRINVALID;\n    uLong uL;\n                ZPOS64_T relativeOffset;\n\n    if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)\n        return CENTRALDIRINVALID;\n\n\n    uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);\n\n    if (uMaxBack>uSizeFile)\n        uMaxBack = uSizeFile;\n\n    buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);\n    if (buf==NULL)\n        return CENTRALDIRINVALID;\n\n    uBackRead = 4;\n    while (uBackRead<uMaxBack)\n    {\n        uLong uReadSize;\n        ZPOS64_T uReadPos;\n        int i;\n        if (uBackRead+BUFREADCOMMENT>uMaxBack)\n            uBackRead = uMaxBack;\n        else\n            uBackRead+=BUFREADCOMMENT;\n        uReadPos = uSizeFile-uBackRead ;\n\n        uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?\n                     (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);\n        if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)\n            break;\n\n        if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)\n            break;\n\n        for (i=(int)uReadSize-3; (i--)>0;)\n            if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) &&\n                ((*(buf+i+2))==0x06) && ((*(buf+i+3))==0x07))\n            {\n                uPosFound = uReadPos+(unsigned)i;\n                break;\n            }\n\n        if (uPosFound!=CENTRALDIRINVALID)\n            break;\n    }\n    free(buf);\n    if (uPosFound == CENTRALDIRINVALID)\n        return CENTRALDIRINVALID;\n\n    /* Zip64 end of central directory locator */\n    if (ZSEEK64(*pzlib_filefunc_def,filestream, uPosFound,ZLIB_FILEFUNC_SEEK_SET)!=0)\n        return CENTRALDIRINVALID;\n\n    /* the signature, already checked */\n    if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)\n        return CENTRALDIRINVALID;\n\n    /* number of the disk with the start of the zip64 end of  central directory */\n    if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)\n        return CENTRALDIRINVALID;\n    if (uL != 0)\n        return CENTRALDIRINVALID;\n\n    /* relative offset of the zip64 end of central directory record */\n    if (unz64local_getLong64(pzlib_filefunc_def,filestream,&relativeOffset)!=UNZ_OK)\n        return CENTRALDIRINVALID;\n\n    /* total number of disks */\n    if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)\n        return CENTRALDIRINVALID;\n    if (uL != 1)\n        return CENTRALDIRINVALID;\n\n    /* Goto end of central directory record */\n    if (ZSEEK64(*pzlib_filefunc_def,filestream, relativeOffset,ZLIB_FILEFUNC_SEEK_SET)!=0)\n        return CENTRALDIRINVALID;\n\n     /* the signature */\n    if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)\n        return CENTRALDIRINVALID;\n\n    if (uL != 0x06064b50)\n        return CENTRALDIRINVALID;\n\n    return relativeOffset;\n}\n\n/*\n  Open a Zip file. path contain the full pathname (by example,\n     on a Windows NT computer \"c:\\\\test\\\\zlib114.zip\" or on an Unix computer\n     \"zlib/zlib114.zip\".\n     If the zipfile cannot be opened (file doesn't exist or in not valid), the\n       return value is NULL.\n     Else, the return value is a unzFile Handle, usable with other function\n       of this unzip package.\n*/\nlocal unzFile unzOpenInternal(const void *path,\n                              zlib_filefunc64_32_def* pzlib_filefunc64_32_def,\n                              int is64bitOpenFunction) {\n    unz64_s us;\n    unz64_s *s;\n    ZPOS64_T central_pos;\n    uLong   uL;\n\n    uLong number_disk;          /* number of the current dist, used for\n                                   spanning ZIP, unsupported, always 0*/\n    uLong number_disk_with_CD;  /* number the the disk with central dir, used\n                                   for spanning ZIP, unsupported, always 0*/\n    ZPOS64_T number_entry_CD;      /* total number of entries in\n                                   the central dir\n                                   (same than number_entry on nospan) */\n\n    int err=UNZ_OK;\n\n    if (unz_copyright[0]!=' ')\n        return NULL;\n\n    us.z_filefunc.zseek32_file = NULL;\n    us.z_filefunc.ztell32_file = NULL;\n    if (pzlib_filefunc64_32_def==NULL)\n        fill_fopen64_filefunc(&us.z_filefunc.zfile_func64);\n    else\n        us.z_filefunc = *pzlib_filefunc64_32_def;\n    us.is64bitOpenFunction = is64bitOpenFunction;\n\n\n\n    us.filestream = ZOPEN64(us.z_filefunc,\n                                                 path,\n                                                 ZLIB_FILEFUNC_MODE_READ |\n                                                 ZLIB_FILEFUNC_MODE_EXISTING);\n    if (us.filestream==NULL)\n        return NULL;\n\n    central_pos = unz64local_SearchCentralDir64(&us.z_filefunc,us.filestream);\n    if (central_pos!=CENTRALDIRINVALID)\n    {\n        uLong uS;\n        ZPOS64_T uL64;\n\n        us.isZip64 = 1;\n\n        if (ZSEEK64(us.z_filefunc, us.filestream,\n                                      central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0)\n        err=UNZ_ERRNO;\n\n        /* the signature, already checked */\n        if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)\n            err=UNZ_ERRNO;\n\n        /* size of zip64 end of central directory record */\n        if (unz64local_getLong64(&us.z_filefunc, us.filestream,&uL64)!=UNZ_OK)\n            err=UNZ_ERRNO;\n\n        /* version made by */\n        if (unz64local_getShort(&us.z_filefunc, us.filestream,&uS)!=UNZ_OK)\n            err=UNZ_ERRNO;\n\n        /* version needed to extract */\n        if (unz64local_getShort(&us.z_filefunc, us.filestream,&uS)!=UNZ_OK)\n            err=UNZ_ERRNO;\n\n        /* number of this disk */\n        if (unz64local_getLong(&us.z_filefunc, us.filestream,&number_disk)!=UNZ_OK)\n            err=UNZ_ERRNO;\n\n        /* number of the disk with the start of the central directory */\n        if (unz64local_getLong(&us.z_filefunc, us.filestream,&number_disk_with_CD)!=UNZ_OK)\n            err=UNZ_ERRNO;\n\n        /* total number of entries in the central directory on this disk */\n        if (unz64local_getLong64(&us.z_filefunc, us.filestream,&us.gi.number_entry)!=UNZ_OK)\n            err=UNZ_ERRNO;\n\n        /* total number of entries in the central directory */\n        if (unz64local_getLong64(&us.z_filefunc, us.filestream,&number_entry_CD)!=UNZ_OK)\n            err=UNZ_ERRNO;\n\n        if ((number_entry_CD!=us.gi.number_entry) ||\n            (number_disk_with_CD!=0) ||\n            (number_disk!=0))\n            err=UNZ_BADZIPFILE;\n\n        /* size of the central directory */\n        if (unz64local_getLong64(&us.z_filefunc, us.filestream,&us.size_central_dir)!=UNZ_OK)\n            err=UNZ_ERRNO;\n\n        /* offset of start of central directory with respect to the\n          starting disk number */\n        if (unz64local_getLong64(&us.z_filefunc, us.filestream,&us.offset_central_dir)!=UNZ_OK)\n            err=UNZ_ERRNO;\n\n        us.gi.size_comment = 0;\n    }\n    else\n    {\n        central_pos = unz64local_SearchCentralDir(&us.z_filefunc,us.filestream);\n        if (central_pos==CENTRALDIRINVALID)\n            err=UNZ_ERRNO;\n\n        us.isZip64 = 0;\n\n        if (ZSEEK64(us.z_filefunc, us.filestream,\n                                        central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0)\n            err=UNZ_ERRNO;\n\n        /* the signature, already checked */\n        if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)\n            err=UNZ_ERRNO;\n\n        /* number of this disk */\n        if (unz64local_getShort(&us.z_filefunc, us.filestream,&number_disk)!=UNZ_OK)\n            err=UNZ_ERRNO;\n\n        /* number of the disk with the start of the central directory */\n        if (unz64local_getShort(&us.z_filefunc, us.filestream,&number_disk_with_CD)!=UNZ_OK)\n            err=UNZ_ERRNO;\n\n        /* total number of entries in the central dir on this disk */\n        if (unz64local_getShort(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)\n            err=UNZ_ERRNO;\n        us.gi.number_entry = uL;\n\n        /* total number of entries in the central dir */\n        if (unz64local_getShort(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)\n            err=UNZ_ERRNO;\n        number_entry_CD = uL;\n\n        if ((number_entry_CD!=us.gi.number_entry) ||\n            (number_disk_with_CD!=0) ||\n            (number_disk!=0))\n            err=UNZ_BADZIPFILE;\n\n        /* size of the central directory */\n        if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)\n            err=UNZ_ERRNO;\n        us.size_central_dir = uL;\n\n        /* offset of start of central directory with respect to the\n            starting disk number */\n        if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)\n            err=UNZ_ERRNO;\n        us.offset_central_dir = uL;\n\n        /* zipfile comment length */\n        if (unz64local_getShort(&us.z_filefunc, us.filestream,&us.gi.size_comment)!=UNZ_OK)\n            err=UNZ_ERRNO;\n    }\n\n    if ((central_pos<us.offset_central_dir+us.size_central_dir) &&\n        (err==UNZ_OK))\n        err=UNZ_BADZIPFILE;\n\n    if (err!=UNZ_OK)\n    {\n        ZCLOSE64(us.z_filefunc, us.filestream);\n        return NULL;\n    }\n\n    us.byte_before_the_zipfile = central_pos -\n                            (us.offset_central_dir+us.size_central_dir);\n    us.central_pos = central_pos;\n    us.pfile_in_zip_read = NULL;\n    us.encrypted = 0;\n\n\n    s=(unz64_s*)ALLOC(sizeof(unz64_s));\n    if( s != NULL)\n    {\n        *s=us;\n        unzGoToFirstFile((unzFile)s);\n    }\n    return (unzFile)s;\n}\n\n\nextern unzFile ZEXPORT unzOpen2(const char *path,\n                                zlib_filefunc_def* pzlib_filefunc32_def) {\n    if (pzlib_filefunc32_def != NULL)\n    {\n        zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;\n        fill_zlib_filefunc64_32_def_from_filefunc32(&zlib_filefunc64_32_def_fill,pzlib_filefunc32_def);\n        return unzOpenInternal(path, &zlib_filefunc64_32_def_fill, 0);\n    }\n    else\n        return unzOpenInternal(path, NULL, 0);\n}\n\nextern unzFile ZEXPORT unzOpen2_64(const void *path,\n                                   zlib_filefunc64_def* pzlib_filefunc_def) {\n    if (pzlib_filefunc_def != NULL)\n    {\n        zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;\n        zlib_filefunc64_32_def_fill.zfile_func64 = *pzlib_filefunc_def;\n        zlib_filefunc64_32_def_fill.ztell32_file = NULL;\n        zlib_filefunc64_32_def_fill.zseek32_file = NULL;\n        return unzOpenInternal(path, &zlib_filefunc64_32_def_fill, 1);\n    }\n    else\n        return unzOpenInternal(path, NULL, 1);\n}\n\nextern unzFile ZEXPORT unzOpen(const char *path) {\n    return unzOpenInternal(path, NULL, 0);\n}\n\nextern unzFile ZEXPORT unzOpen64(const void *path) {\n    return unzOpenInternal(path, NULL, 1);\n}\n\n/*\n  Close a ZipFile opened with unzOpen.\n  If there is files inside the .Zip opened with unzOpenCurrentFile (see later),\n    these files MUST be closed with unzCloseCurrentFile before call unzClose.\n  return UNZ_OK if there is no problem. */\nextern int ZEXPORT unzClose(unzFile file) {\n    unz64_s* s;\n    if (file==NULL)\n        return UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n\n    if (s->pfile_in_zip_read!=NULL)\n        unzCloseCurrentFile(file);\n\n    ZCLOSE64(s->z_filefunc, s->filestream);\n    free(s);\n    return UNZ_OK;\n}\n\n\n/*\n  Write info about the ZipFile in the *pglobal_info structure.\n  No preparation of the structure is needed\n  return UNZ_OK if there is no problem. */\nextern int ZEXPORT unzGetGlobalInfo64(unzFile file, unz_global_info64* pglobal_info) {\n    unz64_s* s;\n    if (file==NULL)\n        return UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n    *pglobal_info=s->gi;\n    return UNZ_OK;\n}\n\nextern int ZEXPORT unzGetGlobalInfo(unzFile file, unz_global_info* pglobal_info32) {\n    unz64_s* s;\n    if (file==NULL)\n        return UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n    /* to do : check if number_entry is not truncated */\n    pglobal_info32->number_entry = (uLong)s->gi.number_entry;\n    pglobal_info32->size_comment = s->gi.size_comment;\n    return UNZ_OK;\n}\n/*\n   Translate date/time from Dos format to tm_unz (readable more easily)\n*/\nlocal void unz64local_DosDateToTmuDate(ZPOS64_T ulDosDate, tm_unz* ptm) {\n    ZPOS64_T uDate;\n    uDate = (ZPOS64_T)(ulDosDate>>16);\n    ptm->tm_mday = (int)(uDate&0x1f) ;\n    ptm->tm_mon =  (int)((((uDate)&0x1E0)/0x20)-1) ;\n    ptm->tm_year = (int)(((uDate&0x0FE00)/0x0200)+1980) ;\n\n    ptm->tm_hour = (int) ((ulDosDate &0xF800)/0x800);\n    ptm->tm_min =  (int) ((ulDosDate&0x7E0)/0x20) ;\n    ptm->tm_sec =  (int) (2*(ulDosDate&0x1f)) ;\n}\n\n/*\n  Get Info about the current file in the zipfile, with internal only info\n*/\nlocal int unz64local_GetCurrentFileInfoInternal(unzFile file,\n                                                unz_file_info64 *pfile_info,\n                                                unz_file_info64_internal\n                                                *pfile_info_internal,\n                                                char *szFileName,\n                                                uLong fileNameBufferSize,\n                                                void *extraField,\n                                                uLong extraFieldBufferSize,\n                                                char *szComment,\n                                                uLong commentBufferSize) {\n    unz64_s* s;\n    unz_file_info64 file_info;\n    unz_file_info64_internal file_info_internal;\n    int err=UNZ_OK;\n    uLong uMagic;\n    long lSeek=0;\n    uLong uL;\n\n    if (file==NULL)\n        return UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n    if (ZSEEK64(s->z_filefunc, s->filestream,\n              s->pos_in_central_dir+s->byte_before_the_zipfile,\n              ZLIB_FILEFUNC_SEEK_SET)!=0)\n        err=UNZ_ERRNO;\n\n\n    /* we check the magic */\n    if (err==UNZ_OK)\n    {\n        if (unz64local_getLong(&s->z_filefunc, s->filestream,&uMagic) != UNZ_OK)\n            err=UNZ_ERRNO;\n        else if (uMagic!=0x02014b50)\n            err=UNZ_BADZIPFILE;\n    }\n\n    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.version) != UNZ_OK)\n        err=UNZ_ERRNO;\n\n    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.version_needed) != UNZ_OK)\n        err=UNZ_ERRNO;\n\n    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.flag) != UNZ_OK)\n        err=UNZ_ERRNO;\n\n    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.compression_method) != UNZ_OK)\n        err=UNZ_ERRNO;\n\n    if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.dosDate) != UNZ_OK)\n        err=UNZ_ERRNO;\n\n    unz64local_DosDateToTmuDate(file_info.dosDate,&file_info.tmu_date);\n\n    if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.crc) != UNZ_OK)\n        err=UNZ_ERRNO;\n\n    if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK)\n        err=UNZ_ERRNO;\n    file_info.compressed_size = uL;\n\n    if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK)\n        err=UNZ_ERRNO;\n    file_info.uncompressed_size = uL;\n\n    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.size_filename) != UNZ_OK)\n        err=UNZ_ERRNO;\n\n    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.size_file_extra) != UNZ_OK)\n        err=UNZ_ERRNO;\n\n    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.size_file_comment) != UNZ_OK)\n        err=UNZ_ERRNO;\n\n    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.disk_num_start) != UNZ_OK)\n        err=UNZ_ERRNO;\n\n    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.internal_fa) != UNZ_OK)\n        err=UNZ_ERRNO;\n\n    if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.external_fa) != UNZ_OK)\n        err=UNZ_ERRNO;\n\n                // relative offset of local header\n    if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK)\n        err=UNZ_ERRNO;\n    file_info_internal.offset_curfile = uL;\n\n    lSeek+=file_info.size_filename;\n    if ((err==UNZ_OK) && (szFileName!=NULL))\n    {\n        uLong uSizeRead ;\n        if (file_info.size_filename<fileNameBufferSize)\n        {\n            *(szFileName+file_info.size_filename)='\\0';\n            uSizeRead = file_info.size_filename;\n        }\n        else\n            uSizeRead = fileNameBufferSize;\n\n        if ((file_info.size_filename>0) && (fileNameBufferSize>0))\n            if (ZREAD64(s->z_filefunc, s->filestream,szFileName,uSizeRead)!=uSizeRead)\n                err=UNZ_ERRNO;\n        lSeek -= uSizeRead;\n    }\n\n    // Read extrafield\n    if ((err==UNZ_OK) && (extraField!=NULL))\n    {\n        ZPOS64_T uSizeRead ;\n        if (file_info.size_file_extra<extraFieldBufferSize)\n            uSizeRead = file_info.size_file_extra;\n        else\n            uSizeRead = extraFieldBufferSize;\n\n        if (lSeek!=0)\n        {\n            if (ZSEEK64(s->z_filefunc, s->filestream,(ZPOS64_T)lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0)\n                lSeek=0;\n            else\n                err=UNZ_ERRNO;\n        }\n\n        if ((file_info.size_file_extra>0) && (extraFieldBufferSize>0))\n            if (ZREAD64(s->z_filefunc, s->filestream,extraField,(uLong)uSizeRead)!=uSizeRead)\n                err=UNZ_ERRNO;\n\n        lSeek += file_info.size_file_extra - (uLong)uSizeRead;\n    }\n    else\n        lSeek += file_info.size_file_extra;\n\n\n    if ((err==UNZ_OK) && (file_info.size_file_extra != 0))\n    {\n                                uLong acc = 0;\n\n        // since lSeek now points to after the extra field we need to move back\n        lSeek -= file_info.size_file_extra;\n\n        if (lSeek!=0)\n        {\n            if (ZSEEK64(s->z_filefunc, s->filestream,(ZPOS64_T)lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0)\n                lSeek=0;\n            else\n                err=UNZ_ERRNO;\n        }\n\n        while(acc < file_info.size_file_extra)\n        {\n            uLong headerId;\n                                                uLong dataSize;\n\n            if (unz64local_getShort(&s->z_filefunc, s->filestream,&headerId) != UNZ_OK)\n                err=UNZ_ERRNO;\n\n            if (unz64local_getShort(&s->z_filefunc, s->filestream,&dataSize) != UNZ_OK)\n                err=UNZ_ERRNO;\n\n            /* ZIP64 extra fields */\n            if (headerId == 0x0001)\n            {\n                if(file_info.uncompressed_size == MAXU32)\n                {\n                    if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info.uncompressed_size) != UNZ_OK)\n                        err=UNZ_ERRNO;\n                }\n\n                if(file_info.compressed_size == MAXU32)\n                {\n                    if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info.compressed_size) != UNZ_OK)\n                        err=UNZ_ERRNO;\n                }\n\n                if(file_info_internal.offset_curfile == MAXU32)\n                {\n                    /* Relative Header offset */\n                    if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info_internal.offset_curfile) != UNZ_OK)\n                        err=UNZ_ERRNO;\n                }\n\n                if(file_info.disk_num_start == 0xffff)\n                {\n                    /* Disk Start Number */\n                    if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.disk_num_start) != UNZ_OK)\n                        err=UNZ_ERRNO;\n                }\n\n            }\n            else\n            {\n                if (ZSEEK64(s->z_filefunc, s->filestream,dataSize,ZLIB_FILEFUNC_SEEK_CUR)!=0)\n                    err=UNZ_ERRNO;\n            }\n\n            acc += 2 + 2 + dataSize;\n        }\n    }\n\n    if ((err==UNZ_OK) && (szComment!=NULL))\n    {\n        uLong uSizeRead ;\n        if (file_info.size_file_comment<commentBufferSize)\n        {\n            *(szComment+file_info.size_file_comment)='\\0';\n            uSizeRead = file_info.size_file_comment;\n        }\n        else\n            uSizeRead = commentBufferSize;\n\n        if (lSeek!=0)\n        {\n            if (ZSEEK64(s->z_filefunc, s->filestream,(ZPOS64_T)lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0)\n                lSeek=0;\n            else\n                err=UNZ_ERRNO;\n        }\n\n        if ((file_info.size_file_comment>0) && (commentBufferSize>0))\n            if (ZREAD64(s->z_filefunc, s->filestream,szComment,uSizeRead)!=uSizeRead)\n                err=UNZ_ERRNO;\n        lSeek+=file_info.size_file_comment - uSizeRead;\n    }\n    else\n        lSeek+=file_info.size_file_comment;\n\n\n    if ((err==UNZ_OK) && (pfile_info!=NULL))\n        *pfile_info=file_info;\n\n    if ((err==UNZ_OK) && (pfile_info_internal!=NULL))\n        *pfile_info_internal=file_info_internal;\n\n    return err;\n}\n\n\n\n/*\n  Write info about the ZipFile in the *pglobal_info structure.\n  No preparation of the structure is needed\n  return UNZ_OK if there is no problem.\n*/\nextern int ZEXPORT unzGetCurrentFileInfo64(unzFile file,\n                                           unz_file_info64 * pfile_info,\n                                           char * szFileName, uLong fileNameBufferSize,\n                                           void *extraField, uLong extraFieldBufferSize,\n                                           char* szComment,  uLong commentBufferSize) {\n    return unz64local_GetCurrentFileInfoInternal(file,pfile_info,NULL,\n                                                 szFileName,fileNameBufferSize,\n                                                 extraField,extraFieldBufferSize,\n                                                 szComment,commentBufferSize);\n}\n\nextern int ZEXPORT unzGetCurrentFileInfo(unzFile file,\n                                         unz_file_info * pfile_info,\n                                         char * szFileName, uLong fileNameBufferSize,\n                                         void *extraField, uLong extraFieldBufferSize,\n                                         char* szComment,  uLong commentBufferSize) {\n    int err;\n    unz_file_info64 file_info64;\n    err = unz64local_GetCurrentFileInfoInternal(file,&file_info64,NULL,\n                                                szFileName,fileNameBufferSize,\n                                                extraField,extraFieldBufferSize,\n                                                szComment,commentBufferSize);\n    if ((err==UNZ_OK) && (pfile_info != NULL))\n    {\n        pfile_info->version = file_info64.version;\n        pfile_info->version_needed = file_info64.version_needed;\n        pfile_info->flag = file_info64.flag;\n        pfile_info->compression_method = file_info64.compression_method;\n        pfile_info->dosDate = file_info64.dosDate;\n        pfile_info->crc = file_info64.crc;\n\n        pfile_info->size_filename = file_info64.size_filename;\n        pfile_info->size_file_extra = file_info64.size_file_extra;\n        pfile_info->size_file_comment = file_info64.size_file_comment;\n\n        pfile_info->disk_num_start = file_info64.disk_num_start;\n        pfile_info->internal_fa = file_info64.internal_fa;\n        pfile_info->external_fa = file_info64.external_fa;\n\n        pfile_info->tmu_date = file_info64.tmu_date;\n\n\n        pfile_info->compressed_size = (uLong)file_info64.compressed_size;\n        pfile_info->uncompressed_size = (uLong)file_info64.uncompressed_size;\n\n    }\n    return err;\n}\n/*\n  Set the current file of the zipfile to the first file.\n  return UNZ_OK if there is no problem\n*/\nextern int ZEXPORT unzGoToFirstFile(unzFile file) {\n    int err=UNZ_OK;\n    unz64_s* s;\n    if (file==NULL)\n        return UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n    s->pos_in_central_dir=s->offset_central_dir;\n    s->num_file=0;\n    err=unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info,\n                                             &s->cur_file_info_internal,\n                                             NULL,0,NULL,0,NULL,0);\n    s->current_file_ok = (err == UNZ_OK);\n    return err;\n}\n\n/*\n  Set the current file of the zipfile to the next file.\n  return UNZ_OK if there is no problem\n  return UNZ_END_OF_LIST_OF_FILE if the actual file was the latest.\n*/\nextern int ZEXPORT unzGoToNextFile(unzFile file) {\n    unz64_s* s;\n    int err;\n\n    if (file==NULL)\n        return UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n    if (!s->current_file_ok)\n        return UNZ_END_OF_LIST_OF_FILE;\n    if (s->gi.number_entry != 0xffff)    /* 2^16 files overflow hack */\n      if (s->num_file+1==s->gi.number_entry)\n        return UNZ_END_OF_LIST_OF_FILE;\n\n    s->pos_in_central_dir += SIZECENTRALDIRITEM + s->cur_file_info.size_filename +\n            s->cur_file_info.size_file_extra + s->cur_file_info.size_file_comment ;\n    s->num_file++;\n    err = unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info,\n                                               &s->cur_file_info_internal,\n                                               NULL,0,NULL,0,NULL,0);\n    s->current_file_ok = (err == UNZ_OK);\n    return err;\n}\n\n\n/*\n  Try locate the file szFileName in the zipfile.\n  For the iCaseSensitivity signification, see unzStringFileNameCompare\n\n  return value :\n  UNZ_OK if the file is found. It becomes the current file.\n  UNZ_END_OF_LIST_OF_FILE if the file is not found\n*/\nextern int ZEXPORT unzLocateFile(unzFile file, const char *szFileName, int iCaseSensitivity) {\n    unz64_s* s;\n    int err;\n\n    /* We remember the 'current' position in the file so that we can jump\n     * back there if we fail.\n     */\n    unz_file_info64 cur_file_infoSaved;\n    unz_file_info64_internal cur_file_info_internalSaved;\n    ZPOS64_T num_fileSaved;\n    ZPOS64_T pos_in_central_dirSaved;\n\n\n    if (file==NULL)\n        return UNZ_PARAMERROR;\n\n    if (strlen(szFileName)>=UNZ_MAXFILENAMEINZIP)\n        return UNZ_PARAMERROR;\n\n    s=(unz64_s*)file;\n    if (!s->current_file_ok)\n        return UNZ_END_OF_LIST_OF_FILE;\n\n    /* Save the current state */\n    num_fileSaved = s->num_file;\n    pos_in_central_dirSaved = s->pos_in_central_dir;\n    cur_file_infoSaved = s->cur_file_info;\n    cur_file_info_internalSaved = s->cur_file_info_internal;\n\n    err = unzGoToFirstFile(file);\n\n    while (err == UNZ_OK)\n    {\n        char szCurrentFileName[UNZ_MAXFILENAMEINZIP+1];\n        err = unzGetCurrentFileInfo64(file,NULL,\n                                    szCurrentFileName,sizeof(szCurrentFileName)-1,\n                                    NULL,0,NULL,0);\n        if (err == UNZ_OK)\n        {\n            if (unzStringFileNameCompare(szCurrentFileName,\n                                            szFileName,iCaseSensitivity)==0)\n                return UNZ_OK;\n            err = unzGoToNextFile(file);\n        }\n    }\n\n    /* We failed, so restore the state of the 'current file' to where we\n     * were.\n     */\n    s->num_file = num_fileSaved ;\n    s->pos_in_central_dir = pos_in_central_dirSaved ;\n    s->cur_file_info = cur_file_infoSaved;\n    s->cur_file_info_internal = cur_file_info_internalSaved;\n    return err;\n}\n\n\n/*\n///////////////////////////////////////////\n// Contributed by Ryan Haksi (mailto://cryogen@infoserve.net)\n// I need random access\n//\n// Further optimization could be realized by adding an ability\n// to cache the directory in memory. The goal being a single\n// comprehensive file read to put the file I need in a memory.\n*/\n\n/*\ntypedef struct unz_file_pos_s\n{\n    ZPOS64_T pos_in_zip_directory;   // offset in file\n    ZPOS64_T num_of_file;            // # of file\n} unz_file_pos;\n*/\n\nextern int ZEXPORT unzGetFilePos64(unzFile file, unz64_file_pos* file_pos) {\n    unz64_s* s;\n\n    if (file==NULL || file_pos==NULL)\n        return UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n    if (!s->current_file_ok)\n        return UNZ_END_OF_LIST_OF_FILE;\n\n    file_pos->pos_in_zip_directory  = s->pos_in_central_dir;\n    file_pos->num_of_file           = s->num_file;\n\n    return UNZ_OK;\n}\n\nextern int ZEXPORT unzGetFilePos(unzFile file, unz_file_pos* file_pos) {\n    unz64_file_pos file_pos64;\n    int err = unzGetFilePos64(file,&file_pos64);\n    if (err==UNZ_OK)\n    {\n        file_pos->pos_in_zip_directory = (uLong)file_pos64.pos_in_zip_directory;\n        file_pos->num_of_file = (uLong)file_pos64.num_of_file;\n    }\n    return err;\n}\n\nextern int ZEXPORT unzGoToFilePos64(unzFile file, const unz64_file_pos* file_pos) {\n    unz64_s* s;\n    int err;\n\n    if (file==NULL || file_pos==NULL)\n        return UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n\n    /* jump to the right spot */\n    s->pos_in_central_dir = file_pos->pos_in_zip_directory;\n    s->num_file           = file_pos->num_of_file;\n\n    /* set the current file */\n    err = unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info,\n                                               &s->cur_file_info_internal,\n                                               NULL,0,NULL,0,NULL,0);\n    /* return results */\n    s->current_file_ok = (err == UNZ_OK);\n    return err;\n}\n\nextern int ZEXPORT unzGoToFilePos(unzFile file, unz_file_pos* file_pos) {\n    unz64_file_pos file_pos64;\n    if (file_pos == NULL)\n        return UNZ_PARAMERROR;\n\n    file_pos64.pos_in_zip_directory = file_pos->pos_in_zip_directory;\n    file_pos64.num_of_file = file_pos->num_of_file;\n    return unzGoToFilePos64(file,&file_pos64);\n}\n\n/*\n// Unzip Helper Functions - should be here?\n///////////////////////////////////////////\n*/\n\n/*\n  Read the local header of the current zipfile\n  Check the coherency of the local header and info in the end of central\n        directory about this file\n  store in *piSizeVar the size of extra info in local header\n        (filename and size of extra field data)\n*/\nlocal int unz64local_CheckCurrentFileCoherencyHeader(unz64_s* s, uInt* piSizeVar,\n                                                     ZPOS64_T * poffset_local_extrafield,\n                                                     uInt  * psize_local_extrafield) {\n    uLong uMagic,uData,uFlags;\n    uLong size_filename;\n    uLong size_extra_field;\n    int err=UNZ_OK;\n\n    *piSizeVar = 0;\n    *poffset_local_extrafield = 0;\n    *psize_local_extrafield = 0;\n\n    if (ZSEEK64(s->z_filefunc, s->filestream,s->cur_file_info_internal.offset_curfile +\n                                s->byte_before_the_zipfile,ZLIB_FILEFUNC_SEEK_SET)!=0)\n        return UNZ_ERRNO;\n\n\n    if (err==UNZ_OK)\n    {\n        if (unz64local_getLong(&s->z_filefunc, s->filestream,&uMagic) != UNZ_OK)\n            err=UNZ_ERRNO;\n        else if (uMagic!=0x04034b50)\n            err=UNZ_BADZIPFILE;\n    }\n\n    if (unz64local_getShort(&s->z_filefunc, s->filestream,&uData) != UNZ_OK)\n        err=UNZ_ERRNO;\n/*\n    else if ((err==UNZ_OK) && (uData!=s->cur_file_info.wVersion))\n        err=UNZ_BADZIPFILE;\n*/\n    if (unz64local_getShort(&s->z_filefunc, s->filestream,&uFlags) != UNZ_OK)\n        err=UNZ_ERRNO;\n\n    if (unz64local_getShort(&s->z_filefunc, s->filestream,&uData) != UNZ_OK)\n        err=UNZ_ERRNO;\n    else if ((err==UNZ_OK) && (uData!=s->cur_file_info.compression_method))\n        err=UNZ_BADZIPFILE;\n\n    if ((err==UNZ_OK) && (s->cur_file_info.compression_method!=0) &&\n/* #ifdef HAVE_BZIP2 */\n                         (s->cur_file_info.compression_method!=Z_BZIP2ED) &&\n/* #endif */\n                         (s->cur_file_info.compression_method!=Z_DEFLATED))\n        err=UNZ_BADZIPFILE;\n\n    if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* date/time */\n        err=UNZ_ERRNO;\n\n    if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* crc */\n        err=UNZ_ERRNO;\n    else if ((err==UNZ_OK) && (uData!=s->cur_file_info.crc) && ((uFlags & 8)==0))\n        err=UNZ_BADZIPFILE;\n\n    if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* size compr */\n        err=UNZ_ERRNO;\n    else if (uData != 0xFFFFFFFF && (err==UNZ_OK) && (uData!=s->cur_file_info.compressed_size) && ((uFlags & 8)==0))\n        err=UNZ_BADZIPFILE;\n\n    if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* size uncompr */\n        err=UNZ_ERRNO;\n    else if (uData != 0xFFFFFFFF && (err==UNZ_OK) && (uData!=s->cur_file_info.uncompressed_size) && ((uFlags & 8)==0))\n        err=UNZ_BADZIPFILE;\n\n    if (unz64local_getShort(&s->z_filefunc, s->filestream,&size_filename) != UNZ_OK)\n        err=UNZ_ERRNO;\n    else if ((err==UNZ_OK) && (size_filename!=s->cur_file_info.size_filename))\n        err=UNZ_BADZIPFILE;\n\n    *piSizeVar += (uInt)size_filename;\n\n    if (unz64local_getShort(&s->z_filefunc, s->filestream,&size_extra_field) != UNZ_OK)\n        err=UNZ_ERRNO;\n    *poffset_local_extrafield= s->cur_file_info_internal.offset_curfile +\n                                    SIZEZIPLOCALHEADER + size_filename;\n    *psize_local_extrafield = (uInt)size_extra_field;\n\n    *piSizeVar += (uInt)size_extra_field;\n\n    return err;\n}\n\n/*\n  Open for reading data the current file in the zipfile.\n  If there is no error and the file is opened, the return value is UNZ_OK.\n*/\nextern int ZEXPORT unzOpenCurrentFile3(unzFile file, int* method,\n                                       int* level, int raw, const char* password) {\n    int err=UNZ_OK;\n    uInt iSizeVar;\n    unz64_s* s;\n    file_in_zip64_read_info_s* pfile_in_zip_read_info;\n    ZPOS64_T offset_local_extrafield;  /* offset of the local extra field */\n    uInt  size_local_extrafield;    /* size of the local extra field */\n#    ifndef NOUNCRYPT\n    char source[12];\n#    else\n    if (password != NULL)\n        return UNZ_PARAMERROR;\n#    endif\n\n    if (file==NULL)\n        return UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n    if (!s->current_file_ok)\n        return UNZ_PARAMERROR;\n\n    if (s->pfile_in_zip_read != NULL)\n        unzCloseCurrentFile(file);\n\n    if (unz64local_CheckCurrentFileCoherencyHeader(s,&iSizeVar, &offset_local_extrafield,&size_local_extrafield)!=UNZ_OK)\n        return UNZ_BADZIPFILE;\n\n    pfile_in_zip_read_info = (file_in_zip64_read_info_s*)ALLOC(sizeof(file_in_zip64_read_info_s));\n    if (pfile_in_zip_read_info==NULL)\n        return UNZ_INTERNALERROR;\n\n    pfile_in_zip_read_info->read_buffer=(char*)ALLOC(UNZ_BUFSIZE);\n    pfile_in_zip_read_info->offset_local_extrafield = offset_local_extrafield;\n    pfile_in_zip_read_info->size_local_extrafield = size_local_extrafield;\n    pfile_in_zip_read_info->pos_local_extrafield=0;\n    pfile_in_zip_read_info->raw=raw;\n\n    if (pfile_in_zip_read_info->read_buffer==NULL)\n    {\n        free(pfile_in_zip_read_info);\n        return UNZ_INTERNALERROR;\n    }\n\n    pfile_in_zip_read_info->stream_initialised=0;\n\n    if (method!=NULL)\n        *method = (int)s->cur_file_info.compression_method;\n\n    if (level!=NULL)\n    {\n        *level = 6;\n        switch (s->cur_file_info.flag & 0x06)\n        {\n          case 6 : *level = 1; break;\n          case 4 : *level = 2; break;\n          case 2 : *level = 9; break;\n        }\n    }\n\n    if ((s->cur_file_info.compression_method!=0) &&\n/* #ifdef HAVE_BZIP2 */\n        (s->cur_file_info.compression_method!=Z_BZIP2ED) &&\n/* #endif */\n        (s->cur_file_info.compression_method!=Z_DEFLATED))\n\n        err=UNZ_BADZIPFILE;\n\n    pfile_in_zip_read_info->crc32_wait=s->cur_file_info.crc;\n    pfile_in_zip_read_info->crc32=0;\n    pfile_in_zip_read_info->total_out_64=0;\n    pfile_in_zip_read_info->compression_method = s->cur_file_info.compression_method;\n    pfile_in_zip_read_info->filestream=s->filestream;\n    pfile_in_zip_read_info->z_filefunc=s->z_filefunc;\n    pfile_in_zip_read_info->byte_before_the_zipfile=s->byte_before_the_zipfile;\n\n    pfile_in_zip_read_info->stream.total_out = 0;\n\n    if ((s->cur_file_info.compression_method==Z_BZIP2ED) && (!raw))\n    {\n#ifdef HAVE_BZIP2\n      pfile_in_zip_read_info->bstream.bzalloc = (void *(*) (void *, int, int))0;\n      pfile_in_zip_read_info->bstream.bzfree = (free_func)0;\n      pfile_in_zip_read_info->bstream.opaque = (voidpf)0;\n      pfile_in_zip_read_info->bstream.state = (voidpf)0;\n\n      pfile_in_zip_read_info->stream.zalloc = (alloc_func)0;\n      pfile_in_zip_read_info->stream.zfree = (free_func)0;\n      pfile_in_zip_read_info->stream.opaque = (voidpf)0;\n      pfile_in_zip_read_info->stream.next_in = (voidpf)0;\n      pfile_in_zip_read_info->stream.avail_in = 0;\n\n      err=BZ2_bzDecompressInit(&pfile_in_zip_read_info->bstream, 0, 0);\n      if (err == Z_OK)\n        pfile_in_zip_read_info->stream_initialised=Z_BZIP2ED;\n      else\n      {\n        free(pfile_in_zip_read_info->read_buffer);\n        free(pfile_in_zip_read_info);\n        return err;\n      }\n#else\n      pfile_in_zip_read_info->raw=1;\n#endif\n    }\n    else if ((s->cur_file_info.compression_method==Z_DEFLATED) && (!raw))\n    {\n      pfile_in_zip_read_info->stream.zalloc = (alloc_func)0;\n      pfile_in_zip_read_info->stream.zfree = (free_func)0;\n      pfile_in_zip_read_info->stream.opaque = (voidpf)0;\n      pfile_in_zip_read_info->stream.next_in = 0;\n      pfile_in_zip_read_info->stream.avail_in = 0;\n\n      err=inflateInit2(&pfile_in_zip_read_info->stream, -MAX_WBITS);\n      if (err == Z_OK)\n        pfile_in_zip_read_info->stream_initialised=Z_DEFLATED;\n      else\n      {\n        free(pfile_in_zip_read_info->read_buffer);\n        free(pfile_in_zip_read_info);\n        return err;\n      }\n        /* windowBits is passed < 0 to tell that there is no zlib header.\n         * Note that in this case inflate *requires* an extra \"dummy\" byte\n         * after the compressed stream in order to complete decompression and\n         * return Z_STREAM_END.\n         * In unzip, i don't wait absolutely Z_STREAM_END because I known the\n         * size of both compressed and uncompressed data\n         */\n    }\n    pfile_in_zip_read_info->rest_read_compressed =\n            s->cur_file_info.compressed_size ;\n    pfile_in_zip_read_info->rest_read_uncompressed =\n            s->cur_file_info.uncompressed_size ;\n\n\n    pfile_in_zip_read_info->pos_in_zipfile =\n            s->cur_file_info_internal.offset_curfile + SIZEZIPLOCALHEADER +\n              iSizeVar;\n\n    pfile_in_zip_read_info->stream.avail_in = (uInt)0;\n\n    s->pfile_in_zip_read = pfile_in_zip_read_info;\n                s->encrypted = 0;\n\n#    ifndef NOUNCRYPT\n    if (password != NULL)\n    {\n        int i;\n        s->pcrc_32_tab = get_crc_table();\n        init_keys(password,s->keys,s->pcrc_32_tab);\n        if (ZSEEK64(s->z_filefunc, s->filestream,\n                  s->pfile_in_zip_read->pos_in_zipfile +\n                     s->pfile_in_zip_read->byte_before_the_zipfile,\n                  SEEK_SET)!=0)\n            return UNZ_INTERNALERROR;\n        if(ZREAD64(s->z_filefunc, s->filestream,source, 12)<12)\n            return UNZ_INTERNALERROR;\n\n        for (i = 0; i<12; i++)\n            zdecode(s->keys,s->pcrc_32_tab,source[i]);\n\n        s->pfile_in_zip_read->pos_in_zipfile+=12;\n        s->encrypted=1;\n    }\n#    endif\n\n\n    return UNZ_OK;\n}\n\nextern int ZEXPORT unzOpenCurrentFile(unzFile file) {\n    return unzOpenCurrentFile3(file, NULL, NULL, 0, NULL);\n}\n\nextern int ZEXPORT unzOpenCurrentFilePassword(unzFile file, const char* password) {\n    return unzOpenCurrentFile3(file, NULL, NULL, 0, password);\n}\n\nextern int ZEXPORT unzOpenCurrentFile2(unzFile file, int* method, int* level, int raw) {\n    return unzOpenCurrentFile3(file, method, level, raw, NULL);\n}\n\n/** Addition for GDAL : START */\n\nextern ZPOS64_T ZEXPORT unzGetCurrentFileZStreamPos64(unzFile file) {\n    unz64_s* s;\n    file_in_zip64_read_info_s* pfile_in_zip_read_info;\n    s=(unz64_s*)file;\n    if (file==NULL)\n        return 0; //UNZ_PARAMERROR;\n    pfile_in_zip_read_info=s->pfile_in_zip_read;\n    if (pfile_in_zip_read_info==NULL)\n        return 0; //UNZ_PARAMERROR;\n    return pfile_in_zip_read_info->pos_in_zipfile +\n                         pfile_in_zip_read_info->byte_before_the_zipfile;\n}\n\n/** Addition for GDAL : END */\n\n/*\n  Read bytes from the current file.\n  buf contain buffer where data must be copied\n  len the size of buf.\n\n  return the number of byte copied if some bytes are copied\n  return 0 if the end of file was reached\n  return <0 with error code if there is an error\n    (UNZ_ERRNO for IO error, or zLib error for uncompress error)\n*/\nextern int ZEXPORT unzReadCurrentFile(unzFile file, voidp buf, unsigned len) {\n    int err=UNZ_OK;\n    uInt iRead = 0;\n    unz64_s* s;\n    file_in_zip64_read_info_s* pfile_in_zip_read_info;\n    if (file==NULL)\n        return UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n    pfile_in_zip_read_info=s->pfile_in_zip_read;\n\n    if (pfile_in_zip_read_info==NULL)\n        return UNZ_PARAMERROR;\n\n\n    if (pfile_in_zip_read_info->read_buffer == NULL)\n        return UNZ_END_OF_LIST_OF_FILE;\n    if (len==0)\n        return 0;\n\n    pfile_in_zip_read_info->stream.next_out = (Bytef*)buf;\n\n    pfile_in_zip_read_info->stream.avail_out = (uInt)len;\n\n    if ((len>pfile_in_zip_read_info->rest_read_uncompressed) &&\n        (!(pfile_in_zip_read_info->raw)))\n        pfile_in_zip_read_info->stream.avail_out =\n            (uInt)pfile_in_zip_read_info->rest_read_uncompressed;\n\n    if ((len>pfile_in_zip_read_info->rest_read_compressed+\n           pfile_in_zip_read_info->stream.avail_in) &&\n         (pfile_in_zip_read_info->raw))\n        pfile_in_zip_read_info->stream.avail_out =\n            (uInt)pfile_in_zip_read_info->rest_read_compressed+\n            pfile_in_zip_read_info->stream.avail_in;\n\n    while (pfile_in_zip_read_info->stream.avail_out>0)\n    {\n        if ((pfile_in_zip_read_info->stream.avail_in==0) &&\n            (pfile_in_zip_read_info->rest_read_compressed>0))\n        {\n            uInt uReadThis = UNZ_BUFSIZE;\n            if (pfile_in_zip_read_info->rest_read_compressed<uReadThis)\n                uReadThis = (uInt)pfile_in_zip_read_info->rest_read_compressed;\n            if (uReadThis == 0)\n                return UNZ_EOF;\n            if (ZSEEK64(pfile_in_zip_read_info->z_filefunc,\n                      pfile_in_zip_read_info->filestream,\n                      pfile_in_zip_read_info->pos_in_zipfile +\n                         pfile_in_zip_read_info->byte_before_the_zipfile,\n                         ZLIB_FILEFUNC_SEEK_SET)!=0)\n                return UNZ_ERRNO;\n            if (ZREAD64(pfile_in_zip_read_info->z_filefunc,\n                      pfile_in_zip_read_info->filestream,\n                      pfile_in_zip_read_info->read_buffer,\n                      uReadThis)!=uReadThis)\n                return UNZ_ERRNO;\n\n\n#            ifndef NOUNCRYPT\n            if(s->encrypted)\n            {\n                uInt i;\n                for(i=0;i<uReadThis;i++)\n                  pfile_in_zip_read_info->read_buffer[i] =\n                      zdecode(s->keys,s->pcrc_32_tab,\n                              pfile_in_zip_read_info->read_buffer[i]);\n            }\n#            endif\n\n\n            pfile_in_zip_read_info->pos_in_zipfile += uReadThis;\n\n            pfile_in_zip_read_info->rest_read_compressed-=uReadThis;\n\n            pfile_in_zip_read_info->stream.next_in =\n                (Bytef*)pfile_in_zip_read_info->read_buffer;\n            pfile_in_zip_read_info->stream.avail_in = (uInt)uReadThis;\n        }\n\n        if ((pfile_in_zip_read_info->compression_method==0) || (pfile_in_zip_read_info->raw))\n        {\n            uInt uDoCopy,i ;\n\n            if ((pfile_in_zip_read_info->stream.avail_in == 0) &&\n                (pfile_in_zip_read_info->rest_read_compressed == 0))\n                return (iRead==0) ? UNZ_EOF : (int)iRead;\n\n            if (pfile_in_zip_read_info->stream.avail_out <\n                            pfile_in_zip_read_info->stream.avail_in)\n                uDoCopy = pfile_in_zip_read_info->stream.avail_out ;\n            else\n                uDoCopy = pfile_in_zip_read_info->stream.avail_in ;\n\n            for (i=0;i<uDoCopy;i++)\n                *(pfile_in_zip_read_info->stream.next_out+i) =\n                        *(pfile_in_zip_read_info->stream.next_in+i);\n\n            pfile_in_zip_read_info->total_out_64 = pfile_in_zip_read_info->total_out_64 + uDoCopy;\n\n            pfile_in_zip_read_info->crc32 = crc32(pfile_in_zip_read_info->crc32,\n                                pfile_in_zip_read_info->stream.next_out,\n                                uDoCopy);\n            pfile_in_zip_read_info->rest_read_uncompressed-=uDoCopy;\n            pfile_in_zip_read_info->stream.avail_in -= uDoCopy;\n            pfile_in_zip_read_info->stream.avail_out -= uDoCopy;\n            pfile_in_zip_read_info->stream.next_out += uDoCopy;\n            pfile_in_zip_read_info->stream.next_in += uDoCopy;\n            pfile_in_zip_read_info->stream.total_out += uDoCopy;\n            iRead += uDoCopy;\n        }\n        else if (pfile_in_zip_read_info->compression_method==Z_BZIP2ED)\n        {\n#ifdef HAVE_BZIP2\n            uLong uTotalOutBefore,uTotalOutAfter;\n            const Bytef *bufBefore;\n            uLong uOutThis;\n\n            pfile_in_zip_read_info->bstream.next_in        = (char*)pfile_in_zip_read_info->stream.next_in;\n            pfile_in_zip_read_info->bstream.avail_in       = pfile_in_zip_read_info->stream.avail_in;\n            pfile_in_zip_read_info->bstream.total_in_lo32  = pfile_in_zip_read_info->stream.total_in;\n            pfile_in_zip_read_info->bstream.total_in_hi32  = 0;\n            pfile_in_zip_read_info->bstream.next_out       = (char*)pfile_in_zip_read_info->stream.next_out;\n            pfile_in_zip_read_info->bstream.avail_out      = pfile_in_zip_read_info->stream.avail_out;\n            pfile_in_zip_read_info->bstream.total_out_lo32 = pfile_in_zip_read_info->stream.total_out;\n            pfile_in_zip_read_info->bstream.total_out_hi32 = 0;\n\n            uTotalOutBefore = pfile_in_zip_read_info->bstream.total_out_lo32;\n            bufBefore = (const Bytef *)pfile_in_zip_read_info->bstream.next_out;\n\n            err=BZ2_bzDecompress(&pfile_in_zip_read_info->bstream);\n\n            uTotalOutAfter = pfile_in_zip_read_info->bstream.total_out_lo32;\n            uOutThis = uTotalOutAfter-uTotalOutBefore;\n\n            pfile_in_zip_read_info->total_out_64 = pfile_in_zip_read_info->total_out_64 + uOutThis;\n\n            pfile_in_zip_read_info->crc32 = crc32(pfile_in_zip_read_info->crc32,bufBefore, (uInt)(uOutThis));\n            pfile_in_zip_read_info->rest_read_uncompressed -= uOutThis;\n            iRead += (uInt)(uTotalOutAfter - uTotalOutBefore);\n\n            pfile_in_zip_read_info->stream.next_in   = (Bytef*)pfile_in_zip_read_info->bstream.next_in;\n            pfile_in_zip_read_info->stream.avail_in  = pfile_in_zip_read_info->bstream.avail_in;\n            pfile_in_zip_read_info->stream.total_in  = pfile_in_zip_read_info->bstream.total_in_lo32;\n            pfile_in_zip_read_info->stream.next_out  = (Bytef*)pfile_in_zip_read_info->bstream.next_out;\n            pfile_in_zip_read_info->stream.avail_out = pfile_in_zip_read_info->bstream.avail_out;\n            pfile_in_zip_read_info->stream.total_out = pfile_in_zip_read_info->bstream.total_out_lo32;\n\n            if (err==BZ_STREAM_END)\n              return (iRead==0) ? UNZ_EOF : iRead;\n            if (err!=BZ_OK)\n              break;\n#endif\n        } // end Z_BZIP2ED\n        else\n        {\n            ZPOS64_T uTotalOutBefore,uTotalOutAfter;\n            const Bytef *bufBefore;\n            ZPOS64_T uOutThis;\n            int flush=Z_SYNC_FLUSH;\n\n            uTotalOutBefore = pfile_in_zip_read_info->stream.total_out;\n            bufBefore = pfile_in_zip_read_info->stream.next_out;\n\n            /*\n            if ((pfile_in_zip_read_info->rest_read_uncompressed ==\n                     pfile_in_zip_read_info->stream.avail_out) &&\n                (pfile_in_zip_read_info->rest_read_compressed == 0))\n                flush = Z_FINISH;\n            */\n            err=inflate(&pfile_in_zip_read_info->stream,flush);\n\n            if ((err>=0) && (pfile_in_zip_read_info->stream.msg!=NULL))\n              err = Z_DATA_ERROR;\n\n            uTotalOutAfter = pfile_in_zip_read_info->stream.total_out;\n            /* Detect overflow, because z_stream.total_out is uLong (32 bits) */\n            if (uTotalOutAfter<uTotalOutBefore)\n                uTotalOutAfter += 1LL << 32; /* Add maximum value of uLong + 1 */\n            uOutThis = uTotalOutAfter-uTotalOutBefore;\n\n            pfile_in_zip_read_info->total_out_64 = pfile_in_zip_read_info->total_out_64 + uOutThis;\n\n            pfile_in_zip_read_info->crc32 =\n                crc32(pfile_in_zip_read_info->crc32,bufBefore,\n                        (uInt)(uOutThis));\n\n            pfile_in_zip_read_info->rest_read_uncompressed -=\n                uOutThis;\n\n            iRead += (uInt)(uTotalOutAfter - uTotalOutBefore);\n\n            if (err==Z_STREAM_END)\n                return (iRead==0) ? UNZ_EOF : (int)iRead;\n            if (err!=Z_OK)\n                break;\n        }\n    }\n\n    if (err==Z_OK)\n        return (int)iRead;\n    return err;\n}\n\n\n/*\n  Give the current position in uncompressed data\n*/\nextern z_off_t ZEXPORT unztell(unzFile file) {\n    unz64_s* s;\n    file_in_zip64_read_info_s* pfile_in_zip_read_info;\n    if (file==NULL)\n        return UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n    pfile_in_zip_read_info=s->pfile_in_zip_read;\n\n    if (pfile_in_zip_read_info==NULL)\n        return UNZ_PARAMERROR;\n\n    return (z_off_t)pfile_in_zip_read_info->stream.total_out;\n}\n\nextern ZPOS64_T ZEXPORT unztell64(unzFile file) {\n\n    unz64_s* s;\n    file_in_zip64_read_info_s* pfile_in_zip_read_info;\n    if (file==NULL)\n        return (ZPOS64_T)-1;\n    s=(unz64_s*)file;\n    pfile_in_zip_read_info=s->pfile_in_zip_read;\n\n    if (pfile_in_zip_read_info==NULL)\n        return (ZPOS64_T)-1;\n\n    return pfile_in_zip_read_info->total_out_64;\n}\n\n\n/*\n  return 1 if the end of file was reached, 0 elsewhere\n*/\nextern int ZEXPORT unzeof(unzFile file) {\n    unz64_s* s;\n    file_in_zip64_read_info_s* pfile_in_zip_read_info;\n    if (file==NULL)\n        return UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n    pfile_in_zip_read_info=s->pfile_in_zip_read;\n\n    if (pfile_in_zip_read_info==NULL)\n        return UNZ_PARAMERROR;\n\n    if (pfile_in_zip_read_info->rest_read_uncompressed == 0)\n        return 1;\n    else\n        return 0;\n}\n\n\n\n/*\nRead extra field from the current file (opened by unzOpenCurrentFile)\nThis is the local-header version of the extra field (sometimes, there is\nmore info in the local-header version than in the central-header)\n\n  if buf==NULL, it return the size of the local extra field that can be read\n\n  if buf!=NULL, len is the size of the buffer, the extra header is copied in\n    buf.\n  the return value is the number of bytes copied in buf, or (if <0)\n    the error code\n*/\nextern int ZEXPORT unzGetLocalExtrafield(unzFile file, voidp buf, unsigned len) {\n    unz64_s* s;\n    file_in_zip64_read_info_s* pfile_in_zip_read_info;\n    uInt read_now;\n    ZPOS64_T size_to_read;\n\n    if (file==NULL)\n        return UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n    pfile_in_zip_read_info=s->pfile_in_zip_read;\n\n    if (pfile_in_zip_read_info==NULL)\n        return UNZ_PARAMERROR;\n\n    size_to_read = (pfile_in_zip_read_info->size_local_extrafield -\n                pfile_in_zip_read_info->pos_local_extrafield);\n\n    if (buf==NULL)\n        return (int)size_to_read;\n\n    if (len>size_to_read)\n        read_now = (uInt)size_to_read;\n    else\n        read_now = (uInt)len ;\n\n    if (read_now==0)\n        return 0;\n\n    if (ZSEEK64(pfile_in_zip_read_info->z_filefunc,\n              pfile_in_zip_read_info->filestream,\n              pfile_in_zip_read_info->offset_local_extrafield +\n              pfile_in_zip_read_info->pos_local_extrafield,\n              ZLIB_FILEFUNC_SEEK_SET)!=0)\n        return UNZ_ERRNO;\n\n    if (ZREAD64(pfile_in_zip_read_info->z_filefunc,\n              pfile_in_zip_read_info->filestream,\n              buf,read_now)!=read_now)\n        return UNZ_ERRNO;\n\n    return (int)read_now;\n}\n\n/*\n  Close the file in zip opened with unzOpenCurrentFile\n  Return UNZ_CRCERROR if all the file was read but the CRC is not good\n*/\nextern int ZEXPORT unzCloseCurrentFile(unzFile file) {\n    int err=UNZ_OK;\n\n    unz64_s* s;\n    file_in_zip64_read_info_s* pfile_in_zip_read_info;\n    if (file==NULL)\n        return UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n    pfile_in_zip_read_info=s->pfile_in_zip_read;\n\n    if (pfile_in_zip_read_info==NULL)\n        return UNZ_PARAMERROR;\n\n\n    if ((pfile_in_zip_read_info->rest_read_uncompressed == 0) &&\n        (!pfile_in_zip_read_info->raw))\n    {\n        if (pfile_in_zip_read_info->crc32 != pfile_in_zip_read_info->crc32_wait)\n            err=UNZ_CRCERROR;\n    }\n\n\n    free(pfile_in_zip_read_info->read_buffer);\n    pfile_in_zip_read_info->read_buffer = NULL;\n    if (pfile_in_zip_read_info->stream_initialised == Z_DEFLATED)\n        inflateEnd(&pfile_in_zip_read_info->stream);\n#ifdef HAVE_BZIP2\n    else if (pfile_in_zip_read_info->stream_initialised == Z_BZIP2ED)\n        BZ2_bzDecompressEnd(&pfile_in_zip_read_info->bstream);\n#endif\n\n\n    pfile_in_zip_read_info->stream_initialised = 0;\n    free(pfile_in_zip_read_info);\n\n    s->pfile_in_zip_read=NULL;\n\n    return err;\n}\n\n\n/*\n  Get the global comment string of the ZipFile, in the szComment buffer.\n  uSizeBuf is the size of the szComment buffer.\n  return the number of byte copied or an error code <0\n*/\nextern int ZEXPORT unzGetGlobalComment(unzFile file, char * szComment, uLong uSizeBuf) {\n    unz64_s* s;\n    uLong uReadThis ;\n    if (file==NULL)\n        return (int)UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n\n    uReadThis = uSizeBuf;\n    if (uReadThis>s->gi.size_comment)\n        uReadThis = s->gi.size_comment;\n\n    if (ZSEEK64(s->z_filefunc,s->filestream,s->central_pos+22,ZLIB_FILEFUNC_SEEK_SET)!=0)\n        return UNZ_ERRNO;\n\n    if (uReadThis>0)\n    {\n      *szComment='\\0';\n      if (ZREAD64(s->z_filefunc,s->filestream,szComment,uReadThis)!=uReadThis)\n        return UNZ_ERRNO;\n    }\n\n    if ((szComment != NULL) && (uSizeBuf > s->gi.size_comment))\n        *(szComment+s->gi.size_comment)='\\0';\n    return (int)uReadThis;\n}\n\n/* Additions by RX '2004 */\nextern ZPOS64_T ZEXPORT unzGetOffset64(unzFile file) {\n    unz64_s* s;\n\n    if (file==NULL)\n          return 0; //UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n    if (!s->current_file_ok)\n      return 0;\n    if (s->gi.number_entry != 0 && s->gi.number_entry != 0xffff)\n      if (s->num_file==s->gi.number_entry)\n         return 0;\n    return s->pos_in_central_dir;\n}\n\nextern uLong ZEXPORT unzGetOffset(unzFile file) {\n    ZPOS64_T offset64;\n\n    if (file==NULL)\n          return 0; //UNZ_PARAMERROR;\n    offset64 = unzGetOffset64(file);\n    return (uLong)offset64;\n}\n\nextern int ZEXPORT unzSetOffset64(unzFile file, ZPOS64_T pos) {\n    unz64_s* s;\n    int err;\n\n    if (file==NULL)\n        return UNZ_PARAMERROR;\n    s=(unz64_s*)file;\n\n    s->pos_in_central_dir = pos;\n    s->num_file = s->gi.number_entry;      /* hack */\n    err = unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info,\n                                              &s->cur_file_info_internal,\n                                              NULL,0,NULL,0,NULL,0);\n    s->current_file_ok = (err == UNZ_OK);\n    return err;\n}\n\nextern int ZEXPORT unzSetOffset (unzFile file, uLong pos) {\n    return unzSetOffset64(file,pos);\n}\n"
  },
  {
    "path": "Telegram/ThirdParty/minizip/unzip.h",
    "content": "/* unzip.h -- IO for uncompress .zip files using zlib\n   Version 1.1, February 14h, 2010\n   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )\n\n         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )\n\n         Modifications of Unzip for Zip64\n         Copyright (C) 2007-2008 Even Rouault\n\n         Modifications for Zip64 support on both zip and unzip\n         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )\n\n         For more info read MiniZip_info.txt\n\n         ---------------------------------------------------------------------------------\n\n        Condition of use and distribution are the same than zlib :\n\n  This software is provided 'as-is', without any express or implied\n  warranty.  In no event will the authors be held liable for any damages\n  arising from the use of this software.\n\n  Permission is granted to anyone to use this software for any purpose,\n  including commercial applications, and to alter it and redistribute it\n  freely, subject to the following restrictions:\n\n  1. The origin of this software must not be misrepresented; you must not\n     claim that you wrote the original software. If you use this software\n     in a product, an acknowledgment in the product documentation would be\n     appreciated but is not required.\n  2. Altered source versions must be plainly marked as such, and must not be\n     misrepresented as being the original software.\n  3. This notice may not be removed or altered from any source distribution.\n\n  ---------------------------------------------------------------------------------\n\n        Changes\n\n        See header of unzip64.c\n\n*/\n\n#ifndef _unz64_H\n#define _unz64_H\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n#ifndef _ZLIB_H\n#include \"zlib.h\"\n#endif\n\n#ifndef  _ZLIBIOAPI_H\n#include \"ioapi.h\"\n#endif\n\n#ifdef HAVE_BZIP2\n#include \"bzlib.h\"\n#endif\n\n#define Z_BZIP2ED 12\n\n#if defined(STRICTUNZIP) || defined(STRICTZIPUNZIP)\n/* like the STRICT of WIN32, we define a pointer that cannot be converted\n    from (void*) without cast */\ntypedef struct TagunzFile__ { int unused; } unzFile__;\ntypedef unzFile__ *unzFile;\n#else\ntypedef voidp unzFile;\n#endif\n\n\n#define UNZ_OK                          (0)\n#define UNZ_END_OF_LIST_OF_FILE         (-100)\n#define UNZ_ERRNO                       (Z_ERRNO)\n#define UNZ_EOF                         (0)\n#define UNZ_PARAMERROR                  (-102)\n#define UNZ_BADZIPFILE                  (-103)\n#define UNZ_INTERNALERROR               (-104)\n#define UNZ_CRCERROR                    (-105)\n\n/* tm_unz contain date/time info */\ntypedef struct tm_unz_s\n{\n    int tm_sec;             /* seconds after the minute - [0,59] */\n    int tm_min;             /* minutes after the hour - [0,59] */\n    int tm_hour;            /* hours since midnight - [0,23] */\n    int tm_mday;            /* day of the month - [1,31] */\n    int tm_mon;             /* months since January - [0,11] */\n    int tm_year;            /* years - [1980..2044] */\n} tm_unz;\n\n/* unz_global_info structure contain global data about the ZIPfile\n   These data comes from the end of central dir */\ntypedef struct unz_global_info64_s\n{\n    ZPOS64_T number_entry;         /* total number of entries in\n                                     the central dir on this disk */\n    uLong size_comment;         /* size of the global comment of the zipfile */\n} unz_global_info64;\n\ntypedef struct unz_global_info_s\n{\n    uLong number_entry;         /* total number of entries in\n                                     the central dir on this disk */\n    uLong size_comment;         /* size of the global comment of the zipfile */\n} unz_global_info;\n\n/* unz_file_info contain information about a file in the zipfile */\ntypedef struct unz_file_info64_s\n{\n    uLong version;              /* version made by                 2 bytes */\n    uLong version_needed;       /* version needed to extract       2 bytes */\n    uLong flag;                 /* general purpose bit flag        2 bytes */\n    uLong compression_method;   /* compression method              2 bytes */\n    uLong dosDate;              /* last mod file date in Dos fmt   4 bytes */\n    uLong crc;                  /* crc-32                          4 bytes */\n    ZPOS64_T compressed_size;   /* compressed size                 8 bytes */\n    ZPOS64_T uncompressed_size; /* uncompressed size               8 bytes */\n    uLong size_filename;        /* filename length                 2 bytes */\n    uLong size_file_extra;      /* extra field length              2 bytes */\n    uLong size_file_comment;    /* file comment length             2 bytes */\n\n    uLong disk_num_start;       /* disk number start               2 bytes */\n    uLong internal_fa;          /* internal file attributes        2 bytes */\n    uLong external_fa;          /* external file attributes        4 bytes */\n\n    tm_unz tmu_date;\n} unz_file_info64;\n\ntypedef struct unz_file_info_s\n{\n    uLong version;              /* version made by                 2 bytes */\n    uLong version_needed;       /* version needed to extract       2 bytes */\n    uLong flag;                 /* general purpose bit flag        2 bytes */\n    uLong compression_method;   /* compression method              2 bytes */\n    uLong dosDate;              /* last mod file date in Dos fmt   4 bytes */\n    uLong crc;                  /* crc-32                          4 bytes */\n    uLong compressed_size;      /* compressed size                 4 bytes */\n    uLong uncompressed_size;    /* uncompressed size               4 bytes */\n    uLong size_filename;        /* filename length                 2 bytes */\n    uLong size_file_extra;      /* extra field length              2 bytes */\n    uLong size_file_comment;    /* file comment length             2 bytes */\n\n    uLong disk_num_start;       /* disk number start               2 bytes */\n    uLong internal_fa;          /* internal file attributes        2 bytes */\n    uLong external_fa;          /* external file attributes        4 bytes */\n\n    tm_unz tmu_date;\n} unz_file_info;\n\nextern int ZEXPORT unzStringFileNameCompare(const char* fileName1,\n                                            const char* fileName2,\n                                            int iCaseSensitivity);\n/*\n   Compare two filenames (fileName1,fileName2).\n   If iCaseSensitivity = 1, comparison is case sensitive (like strcmp)\n   If iCaseSensitivity = 2, comparison is not case sensitive (like strcmpi\n                                or strcasecmp)\n   If iCaseSensitivity = 0, case sensitivity is default of your operating system\n    (like 1 on Unix, 2 on Windows)\n*/\n\n\nextern unzFile ZEXPORT unzOpen(const char *path);\nextern unzFile ZEXPORT unzOpen64(const void *path);\n/*\n  Open a Zip file. path contain the full pathname (by example,\n     on a Windows XP computer \"c:\\\\zlib\\\\zlib113.zip\" or on an Unix computer\n     \"zlib/zlib113.zip\".\n     If the zipfile cannot be opened (file don't exist or in not valid), the\n       return value is NULL.\n     Else, the return value is a unzFile Handle, usable with other function\n       of this unzip package.\n     the \"64\" function take a const void* pointer, because the path is just the\n       value passed to the open64_file_func callback.\n     Under Windows, if UNICODE is defined, using fill_fopen64_filefunc, the path\n       is a pointer to a wide unicode string (LPCTSTR is LPCWSTR), so const char*\n       does not describe the reality\n*/\n\n\nextern unzFile ZEXPORT unzOpen2(const char *path,\n                                zlib_filefunc_def* pzlib_filefunc_def);\n/*\n   Open a Zip file, like unzOpen, but provide a set of file low level API\n      for read/write the zip file (see ioapi.h)\n*/\n\nextern unzFile ZEXPORT unzOpen2_64(const void *path,\n                                   zlib_filefunc64_def* pzlib_filefunc_def);\n/*\n   Open a Zip file, like unz64Open, but provide a set of file low level API\n      for read/write the zip file (see ioapi.h)\n*/\n\nextern int ZEXPORT unzClose(unzFile file);\n/*\n  Close a ZipFile opened with unzOpen.\n  If there is files inside the .Zip opened with unzOpenCurrentFile (see later),\n    these files MUST be closed with unzCloseCurrentFile before call unzClose.\n  return UNZ_OK if there is no problem. */\n\nextern int ZEXPORT unzGetGlobalInfo(unzFile file,\n                                    unz_global_info *pglobal_info);\n\nextern int ZEXPORT unzGetGlobalInfo64(unzFile file,\n                                      unz_global_info64 *pglobal_info);\n/*\n  Write info about the ZipFile in the *pglobal_info structure.\n  No preparation of the structure is needed\n  return UNZ_OK if there is no problem. */\n\n\nextern int ZEXPORT unzGetGlobalComment(unzFile file,\n                                       char *szComment,\n                                       uLong uSizeBuf);\n/*\n  Get the global comment string of the ZipFile, in the szComment buffer.\n  uSizeBuf is the size of the szComment buffer.\n  return the number of byte copied or an error code <0\n*/\n\n\n/***************************************************************************/\n/* Unzip package allow you browse the directory of the zipfile */\n\nextern int ZEXPORT unzGoToFirstFile(unzFile file);\n/*\n  Set the current file of the zipfile to the first file.\n  return UNZ_OK if there is no problem\n*/\n\nextern int ZEXPORT unzGoToNextFile(unzFile file);\n/*\n  Set the current file of the zipfile to the next file.\n  return UNZ_OK if there is no problem\n  return UNZ_END_OF_LIST_OF_FILE if the actual file was the latest.\n*/\n\nextern int ZEXPORT unzLocateFile(unzFile file,\n                                 const char *szFileName,\n                                 int iCaseSensitivity);\n/*\n  Try locate the file szFileName in the zipfile.\n  For the iCaseSensitivity signification, see unzStringFileNameCompare\n\n  return value :\n  UNZ_OK if the file is found. It becomes the current file.\n  UNZ_END_OF_LIST_OF_FILE if the file is not found\n*/\n\n\n/* ****************************************** */\n/* Ryan supplied functions */\n/* unz_file_info contain information about a file in the zipfile */\ntypedef struct unz_file_pos_s\n{\n    uLong pos_in_zip_directory;   /* offset in zip file directory */\n    uLong num_of_file;            /* # of file */\n} unz_file_pos;\n\nextern int ZEXPORT unzGetFilePos(\n    unzFile file,\n    unz_file_pos* file_pos);\n\nextern int ZEXPORT unzGoToFilePos(\n    unzFile file,\n    unz_file_pos* file_pos);\n\ntypedef struct unz64_file_pos_s\n{\n    ZPOS64_T pos_in_zip_directory;   /* offset in zip file directory */\n    ZPOS64_T num_of_file;            /* # of file */\n} unz64_file_pos;\n\nextern int ZEXPORT unzGetFilePos64(\n    unzFile file,\n    unz64_file_pos* file_pos);\n\nextern int ZEXPORT unzGoToFilePos64(\n    unzFile file,\n    const unz64_file_pos* file_pos);\n\n/* ****************************************** */\n\nextern int ZEXPORT unzGetCurrentFileInfo64(unzFile file,\n                                           unz_file_info64 *pfile_info,\n                                           char *szFileName,\n                                           uLong fileNameBufferSize,\n                                           void *extraField,\n                                           uLong extraFieldBufferSize,\n                                           char *szComment,\n                                           uLong commentBufferSize);\n\nextern int ZEXPORT unzGetCurrentFileInfo(unzFile file,\n                                         unz_file_info *pfile_info,\n                                         char *szFileName,\n                                         uLong fileNameBufferSize,\n                                         void *extraField,\n                                         uLong extraFieldBufferSize,\n                                         char *szComment,\n                                         uLong commentBufferSize);\n/*\n  Get Info about the current file\n  if pfile_info!=NULL, the *pfile_info structure will contain some info about\n        the current file\n  if szFileName!=NULL, the filemane string will be copied in szFileName\n            (fileNameBufferSize is the size of the buffer)\n  if extraField!=NULL, the extra field information will be copied in extraField\n            (extraFieldBufferSize is the size of the buffer).\n            This is the Central-header version of the extra field\n  if szComment!=NULL, the comment string of the file will be copied in szComment\n            (commentBufferSize is the size of the buffer)\n*/\n\n\n/** Addition for GDAL : START */\n\nextern ZPOS64_T ZEXPORT unzGetCurrentFileZStreamPos64(unzFile file);\n\n/** Addition for GDAL : END */\n\n\n/***************************************************************************/\n/* for reading the content of the current zipfile, you can open it, read data\n   from it, and close it (you can close it before reading all the file)\n   */\n\nextern int ZEXPORT unzOpenCurrentFile(unzFile file);\n/*\n  Open for reading data the current file in the zipfile.\n  If there is no error, the return value is UNZ_OK.\n*/\n\nextern int ZEXPORT unzOpenCurrentFilePassword(unzFile file,\n                                              const char* password);\n/*\n  Open for reading data the current file in the zipfile.\n  password is a crypting password\n  If there is no error, the return value is UNZ_OK.\n*/\n\nextern int ZEXPORT unzOpenCurrentFile2(unzFile file,\n                                       int* method,\n                                       int* level,\n                                       int raw);\n/*\n  Same than unzOpenCurrentFile, but open for read raw the file (not uncompress)\n    if raw==1\n  *method will receive method of compression, *level will receive level of\n     compression\n  note : you can set level parameter as NULL (if you did not want known level,\n         but you CANNOT set method parameter as NULL\n*/\n\nextern int ZEXPORT unzOpenCurrentFile3(unzFile file,\n                                       int* method,\n                                       int* level,\n                                       int raw,\n                                       const char* password);\n/*\n  Same than unzOpenCurrentFile, but open for read raw the file (not uncompress)\n    if raw==1\n  *method will receive method of compression, *level will receive level of\n     compression\n  note : you can set level parameter as NULL (if you did not want known level,\n         but you CANNOT set method parameter as NULL\n*/\n\n\nextern int ZEXPORT unzCloseCurrentFile(unzFile file);\n/*\n  Close the file in zip opened with unzOpenCurrentFile\n  Return UNZ_CRCERROR if all the file was read but the CRC is not good\n*/\n\nextern int ZEXPORT unzReadCurrentFile(unzFile file,\n                                      voidp buf,\n                                      unsigned len);\n/*\n  Read bytes from the current file (opened by unzOpenCurrentFile)\n  buf contain buffer where data must be copied\n  len the size of buf.\n\n  return the number of byte copied if some bytes are copied\n  return 0 if the end of file was reached\n  return <0 with error code if there is an error\n    (UNZ_ERRNO for IO error, or zLib error for uncompress error)\n*/\n\nextern z_off_t ZEXPORT unztell(unzFile file);\n\nextern ZPOS64_T ZEXPORT unztell64(unzFile file);\n/*\n  Give the current position in uncompressed data\n*/\n\nextern int ZEXPORT unzeof(unzFile file);\n/*\n  return 1 if the end of file was reached, 0 elsewhere\n*/\n\nextern int ZEXPORT unzGetLocalExtrafield(unzFile file,\n                                         voidp buf,\n                                         unsigned len);\n/*\n  Read extra field from the current file (opened by unzOpenCurrentFile)\n  This is the local-header version of the extra field (sometimes, there is\n    more info in the local-header version than in the central-header)\n\n  if buf==NULL, it return the size of the local extra field\n\n  if buf!=NULL, len is the size of the buffer, the extra header is copied in\n    buf.\n  the return value is the number of bytes copied in buf, or (if <0)\n    the error code\n*/\n\n/***************************************************************************/\n\n/* Get the current file offset */\nextern ZPOS64_T ZEXPORT unzGetOffset64 (unzFile file);\nextern uLong ZEXPORT unzGetOffset (unzFile file);\n\n/* Set the current file offset */\nextern int ZEXPORT unzSetOffset64 (unzFile file, ZPOS64_T pos);\nextern int ZEXPORT unzSetOffset (unzFile file, uLong pos);\n\n\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif /* _unz64_H */\n"
  },
  {
    "path": "Telegram/ThirdParty/minizip/zip.c",
    "content": "/* zip.c -- IO on .zip files using zlib\n   Version 1.1, February 14h, 2010\n   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )\n\n         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )\n\n         Modifications for Zip64 support\n         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )\n\n         For more info read MiniZip_info.txt\n\n         Changes\n   Oct-2009 - Mathias Svensson - Remove old C style function prototypes\n   Oct-2009 - Mathias Svensson - Added Zip64 Support when creating new file archives\n   Oct-2009 - Mathias Svensson - Did some code cleanup and refactoring to get better overview of some functions.\n   Oct-2009 - Mathias Svensson - Added zipRemoveExtraInfoBlock to strip extra field data from its ZIP64 data\n                                 It is used when recreating zip archive with RAW when deleting items from a zip.\n                                 ZIP64 data is automatically added to items that needs it, and existing ZIP64 data need to be removed.\n   Oct-2009 - Mathias Svensson - Added support for BZIP2 as compression mode (bzip2 lib is required)\n   Jan-2010 - back to unzip and minizip 1.0 name scheme, with compatibility layer\n\n*/\n\n\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <stdint.h>\n#include <time.h>\n#include \"zlib.h\"\n#include \"zip.h\"\n\n#ifdef STDC\n#  include <stddef.h>\n#endif\n#ifdef NO_ERRNO_H\n    extern int errno;\n#else\n#   include <errno.h>\n#endif\n\n\n#ifndef local\n#  define local static\n#endif\n/* compile with -Dlocal if your debugger can't find static symbols */\n\n#ifndef VERSIONMADEBY\n# define VERSIONMADEBY   (0x0) /* platform dependent */\n#endif\n\n#ifndef Z_BUFSIZE\n#define Z_BUFSIZE (64*1024) //(16384)\n#endif\n\n#ifndef Z_MAXFILENAMEINZIP\n#define Z_MAXFILENAMEINZIP (256)\n#endif\n\n#ifndef ALLOC\n# define ALLOC(size) (malloc(size))\n#endif\n\n/*\n#define SIZECENTRALDIRITEM (0x2e)\n#define SIZEZIPLOCALHEADER (0x1e)\n*/\n\n/* I've found an old Unix (a SunOS 4.1.3_U1) without all SEEK_* defined.... */\n\n\n// NOT sure that this work on ALL platform\n#define MAKEULONG64(a, b) ((ZPOS64_T)(((unsigned long)(a)) | ((ZPOS64_T)((unsigned long)(b))) << 32))\n\n#ifndef SEEK_CUR\n#define SEEK_CUR    1\n#endif\n\n#ifndef SEEK_END\n#define SEEK_END    2\n#endif\n\n#ifndef SEEK_SET\n#define SEEK_SET    0\n#endif\n\n#ifndef DEF_MEM_LEVEL\n#if MAX_MEM_LEVEL >= 8\n#  define DEF_MEM_LEVEL 8\n#else\n#  define DEF_MEM_LEVEL  MAX_MEM_LEVEL\n#endif\n#endif\nconst char zip_copyright[] =\" zip 1.01 Copyright 1998-2004 Gilles Vollant - http://www.winimage.com/zLibDll\";\n\n\n#define SIZEDATA_INDATABLOCK (4096-(4*4))\n\n#define LOCALHEADERMAGIC    (0x04034b50)\n#define CENTRALHEADERMAGIC  (0x02014b50)\n#define ENDHEADERMAGIC      (0x06054b50)\n#define ZIP64ENDHEADERMAGIC      (0x6064b50)\n#define ZIP64ENDLOCHEADERMAGIC   (0x7064b50)\n\n#define FLAG_LOCALHEADER_OFFSET (0x06)\n#define CRC_LOCALHEADER_OFFSET  (0x0e)\n\n#define SIZECENTRALHEADER (0x2e) /* 46 */\n\ntypedef struct linkedlist_datablock_internal_s\n{\n  struct linkedlist_datablock_internal_s* next_datablock;\n  uLong  avail_in_this_block;\n  uLong  filled_in_this_block;\n  uLong  unused; /* for future use and alignment */\n  unsigned char data[SIZEDATA_INDATABLOCK];\n} linkedlist_datablock_internal;\n\ntypedef struct linkedlist_data_s\n{\n    linkedlist_datablock_internal* first_block;\n    linkedlist_datablock_internal* last_block;\n} linkedlist_data;\n\n\ntypedef struct\n{\n    z_stream stream;            /* zLib stream structure for inflate */\n#ifdef HAVE_BZIP2\n    bz_stream bstream;          /* bzLib stream structure for bziped */\n#endif\n\n    int  stream_initialised;    /* 1 is stream is initialised */\n    uInt pos_in_buffered_data;  /* last written byte in buffered_data */\n\n    ZPOS64_T pos_local_header;     /* offset of the local header of the file\n                                     currently writing */\n    char* central_header;       /* central header data for the current file */\n    uLong size_centralExtra;\n    uLong size_centralheader;   /* size of the central header for cur file */\n    uLong size_centralExtraFree; /* Extra bytes allocated to the centralheader but that are not used */\n    uLong flag;                 /* flag of the file currently writing */\n\n    int  method;                /* compression method of file currently wr.*/\n    int  raw;                   /* 1 for directly writing raw data */\n    Byte buffered_data[Z_BUFSIZE];/* buffer contain compressed data to be writ*/\n    uLong dosDate;\n    uLong crc32;\n    int  encrypt;\n    int  zip64;               /* Add ZIP64 extended information in the extra field */\n    ZPOS64_T pos_zip64extrainfo;\n    ZPOS64_T totalCompressedData;\n    ZPOS64_T totalUncompressedData;\n#ifndef NOCRYPT\n    unsigned long keys[3];     /* keys defining the pseudo-random sequence */\n    const z_crc_t* pcrc_32_tab;\n    unsigned crypt_header_size;\n#endif\n} curfile64_info;\n\ntypedef struct\n{\n    zlib_filefunc64_32_def z_filefunc;\n    voidpf filestream;        /* io structure of the zipfile */\n    linkedlist_data central_dir;/* datablock with central dir in construction*/\n    int  in_opened_file_inzip;  /* 1 if a file in the zip is currently writ.*/\n    curfile64_info ci;            /* info on the file currently writing */\n\n    ZPOS64_T begin_pos;            /* position of the beginning of the zipfile */\n    ZPOS64_T add_position_when_writing_offset;\n    ZPOS64_T number_entry;\n\n#ifndef NO_ADDFILEINEXISTINGZIP\n    char *globalcomment;\n#endif\n\n} zip64_internal;\n\n\n#ifndef NOCRYPT\n#define INCLUDECRYPTINGCODE_IFCRYPTALLOWED\n#include \"crypt.h\"\n#endif\n\nlocal linkedlist_datablock_internal* allocate_new_datablock(void) {\n    linkedlist_datablock_internal* ldi;\n    ldi = (linkedlist_datablock_internal*)\n                 ALLOC(sizeof(linkedlist_datablock_internal));\n    if (ldi!=NULL)\n    {\n        ldi->next_datablock = NULL ;\n        ldi->filled_in_this_block = 0 ;\n        ldi->avail_in_this_block = SIZEDATA_INDATABLOCK ;\n    }\n    return ldi;\n}\n\nlocal void free_datablock(linkedlist_datablock_internal* ldi) {\n    while (ldi!=NULL)\n    {\n        linkedlist_datablock_internal* ldinext = ldi->next_datablock;\n        free(ldi);\n        ldi = ldinext;\n    }\n}\n\nlocal void init_linkedlist(linkedlist_data* ll) {\n    ll->first_block = ll->last_block = NULL;\n}\n\nlocal void free_linkedlist(linkedlist_data* ll) {\n    free_datablock(ll->first_block);\n    ll->first_block = ll->last_block = NULL;\n}\n\n\nlocal int add_data_in_datablock(linkedlist_data* ll, const void* buf, uLong len) {\n    linkedlist_datablock_internal* ldi;\n    const unsigned char* from_copy;\n\n    if (ll==NULL)\n        return ZIP_INTERNALERROR;\n\n    if (ll->last_block == NULL)\n    {\n        ll->first_block = ll->last_block = allocate_new_datablock();\n        if (ll->first_block == NULL)\n            return ZIP_INTERNALERROR;\n    }\n\n    ldi = ll->last_block;\n    from_copy = (const unsigned char*)buf;\n\n    while (len>0)\n    {\n        uInt copy_this;\n        uInt i;\n        unsigned char* to_copy;\n\n        if (ldi->avail_in_this_block==0)\n        {\n            ldi->next_datablock = allocate_new_datablock();\n            if (ldi->next_datablock == NULL)\n                return ZIP_INTERNALERROR;\n            ldi = ldi->next_datablock ;\n            ll->last_block = ldi;\n        }\n\n        if (ldi->avail_in_this_block < len)\n            copy_this = (uInt)ldi->avail_in_this_block;\n        else\n            copy_this = (uInt)len;\n\n        to_copy = &(ldi->data[ldi->filled_in_this_block]);\n\n        for (i=0;i<copy_this;i++)\n            *(to_copy+i)=*(from_copy+i);\n\n        ldi->filled_in_this_block += copy_this;\n        ldi->avail_in_this_block -= copy_this;\n        from_copy += copy_this ;\n        len -= copy_this;\n    }\n    return ZIP_OK;\n}\n\n\n\n/****************************************************************************/\n\n#ifndef NO_ADDFILEINEXISTINGZIP\n/* ===========================================================================\n   Inputs a long in LSB order to the given file\n   nbByte == 1, 2 ,4 or 8 (byte, short or long, ZPOS64_T)\n*/\n\nlocal int zip64local_putValue(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T x, int nbByte) {\n    unsigned char buf[8];\n    int n;\n    for (n = 0; n < nbByte; n++)\n    {\n        buf[n] = (unsigned char)(x & 0xff);\n        x >>= 8;\n    }\n    if (x != 0)\n      {     /* data overflow - hack for ZIP64 (X Roche) */\n      for (n = 0; n < nbByte; n++)\n        {\n          buf[n] = 0xff;\n        }\n      }\n\n    if (ZWRITE64(*pzlib_filefunc_def,filestream,buf,(uLong)nbByte)!=(uLong)nbByte)\n        return ZIP_ERRNO;\n    else\n        return ZIP_OK;\n}\n\nlocal void zip64local_putValue_inmemory (void* dest, ZPOS64_T x, int nbByte) {\n    unsigned char* buf=(unsigned char*)dest;\n    int n;\n    for (n = 0; n < nbByte; n++) {\n        buf[n] = (unsigned char)(x & 0xff);\n        x >>= 8;\n    }\n\n    if (x != 0)\n    {     /* data overflow - hack for ZIP64 */\n       for (n = 0; n < nbByte; n++)\n       {\n          buf[n] = 0xff;\n       }\n    }\n}\n\n/****************************************************************************/\n\n\nlocal uLong zip64local_TmzDateToDosDate(const tm_zip* ptm) {\n    uLong year = (uLong)ptm->tm_year;\n    if (year>=1980)\n        year-=1980;\n    else if (year>=80)\n        year-=80;\n    return\n      (uLong) (((uLong)(ptm->tm_mday) + (32 * (uLong)(ptm->tm_mon+1)) + (512 * year)) << 16) |\n        (((uLong)ptm->tm_sec/2) + (32 * (uLong)ptm->tm_min) + (2048 * (uLong)ptm->tm_hour));\n}\n\n\n/****************************************************************************/\n\nlocal int zip64local_getByte(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, int* pi) {\n    unsigned char c;\n    int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,&c,1);\n    if (err==1)\n    {\n        *pi = (int)c;\n        return ZIP_OK;\n    }\n    else\n    {\n        if (ZERROR64(*pzlib_filefunc_def,filestream))\n            return ZIP_ERRNO;\n        else\n            return ZIP_EOF;\n    }\n}\n\n\n/* ===========================================================================\n   Reads a long in LSB order from the given gz_stream. Sets\n*/\nlocal int zip64local_getShort(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX) {\n    uLong x ;\n    int i = 0;\n    int err;\n\n    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);\n    x = (uLong)i;\n\n    if (err==ZIP_OK)\n        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);\n    x += ((uLong)i)<<8;\n\n    if (err==ZIP_OK)\n        *pX = x;\n    else\n        *pX = 0;\n    return err;\n}\n\nlocal int zip64local_getLong(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX) {\n    uLong x ;\n    int i = 0;\n    int err;\n\n    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);\n    x = (uLong)i;\n\n    if (err==ZIP_OK)\n        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);\n    x += ((uLong)i)<<8;\n\n    if (err==ZIP_OK)\n        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);\n    x += ((uLong)i)<<16;\n\n    if (err==ZIP_OK)\n        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);\n    x += ((uLong)i)<<24;\n\n    if (err==ZIP_OK)\n        *pX = x;\n    else\n        *pX = 0;\n    return err;\n}\n\n\nlocal int zip64local_getLong64(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T *pX) {\n  ZPOS64_T x;\n  int i = 0;\n  int err;\n\n  err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);\n  x = (ZPOS64_T)i;\n\n  if (err==ZIP_OK)\n    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);\n  x += ((ZPOS64_T)i)<<8;\n\n  if (err==ZIP_OK)\n    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);\n  x += ((ZPOS64_T)i)<<16;\n\n  if (err==ZIP_OK)\n    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);\n  x += ((ZPOS64_T)i)<<24;\n\n  if (err==ZIP_OK)\n    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);\n  x += ((ZPOS64_T)i)<<32;\n\n  if (err==ZIP_OK)\n    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);\n  x += ((ZPOS64_T)i)<<40;\n\n  if (err==ZIP_OK)\n    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);\n  x += ((ZPOS64_T)i)<<48;\n\n  if (err==ZIP_OK)\n    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);\n  x += ((ZPOS64_T)i)<<56;\n\n  if (err==ZIP_OK)\n    *pX = x;\n  else\n    *pX = 0;\n\n  return err;\n}\n\n#ifndef BUFREADCOMMENT\n#define BUFREADCOMMENT (0x400)\n#endif\n/*\n  Locate the Central directory of a zipfile (at the end, just before\n    the global comment)\n*/\nlocal ZPOS64_T zip64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream) {\n  unsigned char* buf;\n  ZPOS64_T uSizeFile;\n  ZPOS64_T uBackRead;\n  ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */\n  ZPOS64_T uPosFound=0;\n\n  if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)\n    return 0;\n\n\n  uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);\n\n  if (uMaxBack>uSizeFile)\n    uMaxBack = uSizeFile;\n\n  buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);\n  if (buf==NULL)\n    return 0;\n\n  uBackRead = 4;\n  while (uBackRead<uMaxBack)\n  {\n    uLong uReadSize;\n    ZPOS64_T uReadPos ;\n    int i;\n    if (uBackRead+BUFREADCOMMENT>uMaxBack)\n      uBackRead = uMaxBack;\n    else\n      uBackRead+=BUFREADCOMMENT;\n    uReadPos = uSizeFile-uBackRead ;\n\n    uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?\n      (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);\n    if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)\n      break;\n\n    if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)\n      break;\n\n    for (i=(int)uReadSize-3; (i--)>0;)\n      if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) &&\n        ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06))\n      {\n        uPosFound = uReadPos+(unsigned)i;\n        break;\n      }\n\n    if (uPosFound!=0)\n      break;\n  }\n  free(buf);\n  return uPosFound;\n}\n\n/*\nLocate the End of Zip64 Central directory locator and from there find the CD of a zipfile (at the end, just before\nthe global comment)\n*/\nlocal ZPOS64_T zip64local_SearchCentralDir64(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream) {\n  unsigned char* buf;\n  ZPOS64_T uSizeFile;\n  ZPOS64_T uBackRead;\n  ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */\n  ZPOS64_T uPosFound=0;\n  uLong uL;\n  ZPOS64_T relativeOffset;\n\n  if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)\n    return 0;\n\n  uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);\n\n  if (uMaxBack>uSizeFile)\n    uMaxBack = uSizeFile;\n\n  buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);\n  if (buf==NULL)\n    return 0;\n\n  uBackRead = 4;\n  while (uBackRead<uMaxBack)\n  {\n    uLong uReadSize;\n    ZPOS64_T uReadPos;\n    int i;\n    if (uBackRead+BUFREADCOMMENT>uMaxBack)\n      uBackRead = uMaxBack;\n    else\n      uBackRead+=BUFREADCOMMENT;\n    uReadPos = uSizeFile-uBackRead ;\n\n    uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?\n      (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);\n    if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)\n      break;\n\n    if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)\n      break;\n\n    for (i=(int)uReadSize-3; (i--)>0;)\n    {\n      // Signature \"0x07064b50\" Zip64 end of central directory locater\n      if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) && ((*(buf+i+2))==0x06) && ((*(buf+i+3))==0x07))\n      {\n        uPosFound = uReadPos+(unsigned)i;\n        break;\n      }\n    }\n\n      if (uPosFound!=0)\n        break;\n  }\n\n  free(buf);\n  if (uPosFound == 0)\n    return 0;\n\n  /* Zip64 end of central directory locator */\n  if (ZSEEK64(*pzlib_filefunc_def,filestream, uPosFound,ZLIB_FILEFUNC_SEEK_SET)!=0)\n    return 0;\n\n  /* the signature, already checked */\n  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)\n    return 0;\n\n  /* number of the disk with the start of the zip64 end of  central directory */\n  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)\n    return 0;\n  if (uL != 0)\n    return 0;\n\n  /* relative offset of the zip64 end of central directory record */\n  if (zip64local_getLong64(pzlib_filefunc_def,filestream,&relativeOffset)!=ZIP_OK)\n    return 0;\n\n  /* total number of disks */\n  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)\n    return 0;\n  if (uL != 1)\n    return 0;\n\n  /* Goto Zip64 end of central directory record */\n  if (ZSEEK64(*pzlib_filefunc_def,filestream, relativeOffset,ZLIB_FILEFUNC_SEEK_SET)!=0)\n    return 0;\n\n  /* the signature */\n  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)\n    return 0;\n\n  if (uL != 0x06064b50) // signature of 'Zip64 end of central directory'\n    return 0;\n\n  return relativeOffset;\n}\n\nlocal int LoadCentralDirectoryRecord(zip64_internal* pziinit) {\n  int err=ZIP_OK;\n  ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/\n\n  ZPOS64_T size_central_dir;     /* size of the central directory  */\n  ZPOS64_T offset_central_dir;   /* offset of start of central directory */\n  ZPOS64_T central_pos;\n  uLong uL;\n\n  uLong number_disk;          /* number of the current disk, used for\n                              spanning ZIP, unsupported, always 0*/\n  uLong number_disk_with_CD;  /* number of the disk with central dir, used\n                              for spanning ZIP, unsupported, always 0*/\n  ZPOS64_T number_entry;\n  ZPOS64_T number_entry_CD;      /* total number of entries in\n                                the central dir\n                                (same than number_entry on nospan) */\n  uLong VersionMadeBy;\n  uLong VersionNeeded;\n  uLong size_comment;\n\n  int hasZIP64Record = 0;\n\n  // check first if we find a ZIP64 record\n  central_pos = zip64local_SearchCentralDir64(&pziinit->z_filefunc,pziinit->filestream);\n  if(central_pos > 0)\n  {\n    hasZIP64Record = 1;\n  }\n  else if(central_pos == 0)\n  {\n    central_pos = zip64local_SearchCentralDir(&pziinit->z_filefunc,pziinit->filestream);\n  }\n\n/* disable to allow appending to empty ZIP archive\n        if (central_pos==0)\n            err=ZIP_ERRNO;\n*/\n\n  if(hasZIP64Record)\n  {\n    ZPOS64_T sizeEndOfCentralDirectory;\n    if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, central_pos, ZLIB_FILEFUNC_SEEK_SET) != 0)\n      err=ZIP_ERRNO;\n\n    /* the signature, already checked */\n    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&uL)!=ZIP_OK)\n      err=ZIP_ERRNO;\n\n    /* size of zip64 end of central directory record */\n    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream, &sizeEndOfCentralDirectory)!=ZIP_OK)\n      err=ZIP_ERRNO;\n\n    /* version made by */\n    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &VersionMadeBy)!=ZIP_OK)\n      err=ZIP_ERRNO;\n\n    /* version needed to extract */\n    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &VersionNeeded)!=ZIP_OK)\n      err=ZIP_ERRNO;\n\n    /* number of this disk */\n    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&number_disk)!=ZIP_OK)\n      err=ZIP_ERRNO;\n\n    /* number of the disk with the start of the central directory */\n    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&number_disk_with_CD)!=ZIP_OK)\n      err=ZIP_ERRNO;\n\n    /* total number of entries in the central directory on this disk */\n    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream, &number_entry)!=ZIP_OK)\n      err=ZIP_ERRNO;\n\n    /* total number of entries in the central directory */\n    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&number_entry_CD)!=ZIP_OK)\n      err=ZIP_ERRNO;\n\n    if ((number_entry_CD!=number_entry) || (number_disk_with_CD!=0) || (number_disk!=0))\n      err=ZIP_BADZIPFILE;\n\n    /* size of the central directory */\n    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&size_central_dir)!=ZIP_OK)\n      err=ZIP_ERRNO;\n\n    /* offset of start of central directory with respect to the\n    starting disk number */\n    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&offset_central_dir)!=ZIP_OK)\n      err=ZIP_ERRNO;\n\n    // TODO..\n    // read the comment from the standard central header.\n    size_comment = 0;\n  }\n  else\n  {\n    // Read End of central Directory info\n    if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0)\n      err=ZIP_ERRNO;\n\n    /* the signature, already checked */\n    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&uL)!=ZIP_OK)\n      err=ZIP_ERRNO;\n\n    /* number of this disk */\n    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream,&number_disk)!=ZIP_OK)\n      err=ZIP_ERRNO;\n\n    /* number of the disk with the start of the central directory */\n    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream,&number_disk_with_CD)!=ZIP_OK)\n      err=ZIP_ERRNO;\n\n    /* total number of entries in the central dir on this disk */\n    number_entry = 0;\n    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)\n      err=ZIP_ERRNO;\n    else\n      number_entry = uL;\n\n    /* total number of entries in the central dir */\n    number_entry_CD = 0;\n    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)\n      err=ZIP_ERRNO;\n    else\n      number_entry_CD = uL;\n\n    if ((number_entry_CD!=number_entry) || (number_disk_with_CD!=0) || (number_disk!=0))\n      err=ZIP_BADZIPFILE;\n\n    /* size of the central directory */\n    size_central_dir = 0;\n    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)\n      err=ZIP_ERRNO;\n    else\n      size_central_dir = uL;\n\n    /* offset of start of central directory with respect to the starting disk number */\n    offset_central_dir = 0;\n    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)\n      err=ZIP_ERRNO;\n    else\n      offset_central_dir = uL;\n\n\n    /* zipfile global comment length */\n    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &size_comment)!=ZIP_OK)\n      err=ZIP_ERRNO;\n  }\n\n  if ((central_pos<offset_central_dir+size_central_dir) &&\n    (err==ZIP_OK))\n    err=ZIP_BADZIPFILE;\n\n  if (err!=ZIP_OK)\n  {\n    ZCLOSE64(pziinit->z_filefunc, pziinit->filestream);\n    return ZIP_ERRNO;\n  }\n\n  if (size_comment>0)\n  {\n    pziinit->globalcomment = (char*)ALLOC(size_comment+1);\n    if (pziinit->globalcomment)\n    {\n      size_comment = ZREAD64(pziinit->z_filefunc, pziinit->filestream, pziinit->globalcomment,size_comment);\n      pziinit->globalcomment[size_comment]=0;\n    }\n  }\n\n  byte_before_the_zipfile = central_pos - (offset_central_dir+size_central_dir);\n  pziinit->add_position_when_writing_offset = byte_before_the_zipfile;\n\n  {\n    ZPOS64_T size_central_dir_to_read = size_central_dir;\n    size_t buf_size = SIZEDATA_INDATABLOCK;\n    void* buf_read = (void*)ALLOC(buf_size);\n    if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, offset_central_dir + byte_before_the_zipfile, ZLIB_FILEFUNC_SEEK_SET) != 0)\n      err=ZIP_ERRNO;\n\n    while ((size_central_dir_to_read>0) && (err==ZIP_OK))\n    {\n      ZPOS64_T read_this = SIZEDATA_INDATABLOCK;\n      if (read_this > size_central_dir_to_read)\n        read_this = size_central_dir_to_read;\n\n      if (ZREAD64(pziinit->z_filefunc, pziinit->filestream,buf_read,(uLong)read_this) != read_this)\n        err=ZIP_ERRNO;\n\n      if (err==ZIP_OK)\n        err = add_data_in_datablock(&pziinit->central_dir,buf_read, (uLong)read_this);\n\n      size_central_dir_to_read-=read_this;\n    }\n    free(buf_read);\n  }\n  pziinit->begin_pos = byte_before_the_zipfile;\n  pziinit->number_entry = number_entry_CD;\n\n  if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, offset_central_dir+byte_before_the_zipfile,ZLIB_FILEFUNC_SEEK_SET) != 0)\n    err=ZIP_ERRNO;\n\n  return err;\n}\n\n\n#endif /* !NO_ADDFILEINEXISTINGZIP*/\n\n\n/************************************************************/\nextern zipFile ZEXPORT zipOpen3(const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_32_def* pzlib_filefunc64_32_def) {\n    zip64_internal ziinit;\n    zip64_internal* zi;\n    int err=ZIP_OK;\n\n    ziinit.z_filefunc.zseek32_file = NULL;\n    ziinit.z_filefunc.ztell32_file = NULL;\n    if (pzlib_filefunc64_32_def==NULL)\n        fill_fopen64_filefunc(&ziinit.z_filefunc.zfile_func64);\n    else\n        ziinit.z_filefunc = *pzlib_filefunc64_32_def;\n\n    ziinit.filestream = ZOPEN64(ziinit.z_filefunc,\n                  pathname,\n                  (append == APPEND_STATUS_CREATE) ?\n                  (ZLIB_FILEFUNC_MODE_READ | ZLIB_FILEFUNC_MODE_WRITE | ZLIB_FILEFUNC_MODE_CREATE) :\n                    (ZLIB_FILEFUNC_MODE_READ | ZLIB_FILEFUNC_MODE_WRITE | ZLIB_FILEFUNC_MODE_EXISTING));\n\n    if (ziinit.filestream == NULL)\n        return NULL;\n\n    if (append == APPEND_STATUS_CREATEAFTER)\n        ZSEEK64(ziinit.z_filefunc,ziinit.filestream,0,SEEK_END);\n\n    ziinit.begin_pos = ZTELL64(ziinit.z_filefunc,ziinit.filestream);\n    ziinit.in_opened_file_inzip = 0;\n    ziinit.ci.stream_initialised = 0;\n    ziinit.number_entry = 0;\n    ziinit.add_position_when_writing_offset = 0;\n    init_linkedlist(&(ziinit.central_dir));\n\n\n\n    zi = (zip64_internal*)ALLOC(sizeof(zip64_internal));\n    if (zi==NULL)\n    {\n        ZCLOSE64(ziinit.z_filefunc,ziinit.filestream);\n        return NULL;\n    }\n\n    /* now we add file in a zipfile */\n#    ifndef NO_ADDFILEINEXISTINGZIP\n    ziinit.globalcomment = NULL;\n    if (append == APPEND_STATUS_ADDINZIP)\n    {\n      // Read and Cache Central Directory Records\n      err = LoadCentralDirectoryRecord(&ziinit);\n    }\n\n    if (globalcomment)\n    {\n      *globalcomment = ziinit.globalcomment;\n    }\n#    endif /* !NO_ADDFILEINEXISTINGZIP*/\n\n    if (err != ZIP_OK)\n    {\n#    ifndef NO_ADDFILEINEXISTINGZIP\n        free(ziinit.globalcomment);\n#    endif /* !NO_ADDFILEINEXISTINGZIP*/\n        free(zi);\n        return NULL;\n    }\n    else\n    {\n        *zi = ziinit;\n        return (zipFile)zi;\n    }\n}\n\nextern zipFile ZEXPORT zipOpen2(const char *pathname, int append, zipcharpc* globalcomment, zlib_filefunc_def* pzlib_filefunc32_def) {\n    if (pzlib_filefunc32_def != NULL)\n    {\n        zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;\n        fill_zlib_filefunc64_32_def_from_filefunc32(&zlib_filefunc64_32_def_fill,pzlib_filefunc32_def);\n        return zipOpen3(pathname, append, globalcomment, &zlib_filefunc64_32_def_fill);\n    }\n    else\n        return zipOpen3(pathname, append, globalcomment, NULL);\n}\n\nextern zipFile ZEXPORT zipOpen2_64(const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_def* pzlib_filefunc_def) {\n    if (pzlib_filefunc_def != NULL)\n    {\n        zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;\n        zlib_filefunc64_32_def_fill.zfile_func64 = *pzlib_filefunc_def;\n        zlib_filefunc64_32_def_fill.ztell32_file = NULL;\n        zlib_filefunc64_32_def_fill.zseek32_file = NULL;\n        return zipOpen3(pathname, append, globalcomment, &zlib_filefunc64_32_def_fill);\n    }\n    else\n        return zipOpen3(pathname, append, globalcomment, NULL);\n}\n\n\n\nextern zipFile ZEXPORT zipOpen(const char* pathname, int append) {\n    return zipOpen3((const void*)pathname,append,NULL,NULL);\n}\n\nextern zipFile ZEXPORT zipOpen64(const void* pathname, int append) {\n    return zipOpen3(pathname,append,NULL,NULL);\n}\n\nlocal int Write_LocalFileHeader(zip64_internal* zi, const char* filename, uInt size_extrafield_local, const void* extrafield_local) {\n  /* write the local header */\n  int err;\n  uInt size_filename = (uInt)strlen(filename);\n  uInt size_extrafield = size_extrafield_local;\n\n  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)LOCALHEADERMAGIC, 4);\n\n  if (err==ZIP_OK)\n  {\n    if(zi->ci.zip64)\n      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);/* version needed to extract */\n    else\n      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)20,2);/* version needed to extract */\n  }\n\n  if (err==ZIP_OK)\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.flag,2);\n\n  if (err==ZIP_OK)\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.method,2);\n\n  if (err==ZIP_OK)\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.dosDate,4);\n\n  // CRC / Compressed size / Uncompressed size will be filled in later and rewritten later\n  if (err==ZIP_OK)\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* crc 32, unknown */\n  if (err==ZIP_OK)\n  {\n    if(zi->ci.zip64)\n      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xFFFFFFFF,4); /* compressed size, unknown */\n    else\n      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* compressed size, unknown */\n  }\n  if (err==ZIP_OK)\n  {\n    if(zi->ci.zip64)\n      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xFFFFFFFF,4); /* uncompressed size, unknown */\n    else\n      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* uncompressed size, unknown */\n  }\n\n  if (err==ZIP_OK)\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_filename,2);\n\n  if(zi->ci.zip64)\n  {\n    size_extrafield += 20;\n  }\n\n  if (err==ZIP_OK)\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_extrafield,2);\n\n  if ((err==ZIP_OK) && (size_filename > 0))\n  {\n    if (ZWRITE64(zi->z_filefunc,zi->filestream,filename,size_filename)!=size_filename)\n      err = ZIP_ERRNO;\n  }\n\n  if ((err==ZIP_OK) && (size_extrafield_local > 0))\n  {\n    if (ZWRITE64(zi->z_filefunc, zi->filestream, extrafield_local, size_extrafield_local) != size_extrafield_local)\n      err = ZIP_ERRNO;\n  }\n\n\n  if ((err==ZIP_OK) && (zi->ci.zip64))\n  {\n      // write the Zip64 extended info\n      short HeaderID = 1;\n      short DataSize = 16;\n      ZPOS64_T CompressedSize = 0;\n      ZPOS64_T UncompressedSize = 0;\n\n      // Remember position of Zip64 extended info for the local file header. (needed when we update size after done with file)\n      zi->ci.pos_zip64extrainfo = ZTELL64(zi->z_filefunc,zi->filestream);\n\n      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)HeaderID,2);\n      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)DataSize,2);\n\n      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)UncompressedSize,8);\n      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)CompressedSize,8);\n  }\n\n  return err;\n}\n\n/*\n NOTE.\n When writing RAW the ZIP64 extended information in extrafield_local and extrafield_global needs to be stripped\n before calling this function it can be done with zipRemoveExtraInfoBlock\n\n It is not done here because then we need to realloc a new buffer since parameters are 'const' and I want to minimize\n unnecessary allocations.\n */\nextern int ZEXPORT zipOpenNewFileInZip4_64(zipFile file, const char* filename, const zip_fileinfo* zipfi,\n                                           const void* extrafield_local, uInt size_extrafield_local,\n                                           const void* extrafield_global, uInt size_extrafield_global,\n                                           const char* comment, int method, int level, int raw,\n                                           int windowBits,int memLevel, int strategy,\n                                           const char* password, uLong crcForCrypting,\n                                           uLong versionMadeBy, uLong flagBase, int zip64) {\n    zip64_internal* zi;\n    uInt size_filename;\n    uInt size_comment;\n    uInt i;\n    int err = ZIP_OK;\n\n#    ifdef NOCRYPT\n    (crcForCrypting);\n    if (password != NULL)\n        return ZIP_PARAMERROR;\n#    endif\n\n    if (file == NULL)\n        return ZIP_PARAMERROR;\n\n#ifdef HAVE_BZIP2\n    if ((method!=0) && (method!=Z_DEFLATED) && (method!=Z_BZIP2ED))\n      return ZIP_PARAMERROR;\n#else\n    if ((method!=0) && (method!=Z_DEFLATED))\n      return ZIP_PARAMERROR;\n#endif\n\n    // The filename and comment length must fit in 16 bits.\n    if ((filename!=NULL) && (strlen(filename)>0xffff))\n        return ZIP_PARAMERROR;\n    if ((comment!=NULL) && (strlen(comment)>0xffff))\n        return ZIP_PARAMERROR;\n    // The extra field length must fit in 16 bits. If the member also requires\n    // a Zip64 extra block, that will also need to fit within that 16-bit\n    // length, but that will be checked for later.\n    if ((size_extrafield_local>0xffff) || (size_extrafield_global>0xffff))\n        return ZIP_PARAMERROR;\n\n    zi = (zip64_internal*)file;\n\n    if (zi->in_opened_file_inzip == 1)\n    {\n        err = zipCloseFileInZip (file);\n        if (err != ZIP_OK)\n            return err;\n    }\n\n    if (filename==NULL)\n        filename=\"-\";\n\n    if (comment==NULL)\n        size_comment = 0;\n    else\n        size_comment = (uInt)strlen(comment);\n\n    size_filename = (uInt)strlen(filename);\n\n    if (zipfi == NULL)\n        zi->ci.dosDate = 0;\n    else\n    {\n        if (zipfi->dosDate != 0)\n            zi->ci.dosDate = zipfi->dosDate;\n        else\n          zi->ci.dosDate = zip64local_TmzDateToDosDate(&zipfi->tmz_date);\n    }\n\n    zi->ci.flag = flagBase;\n    if ((level==8) || (level==9))\n      zi->ci.flag |= 2;\n    if (level==2)\n      zi->ci.flag |= 4;\n    if (level==1)\n      zi->ci.flag |= 6;\n    if (password != NULL)\n      zi->ci.flag |= 1;\n\n    zi->ci.crc32 = 0;\n    zi->ci.method = method;\n    zi->ci.encrypt = 0;\n    zi->ci.stream_initialised = 0;\n    zi->ci.pos_in_buffered_data = 0;\n    zi->ci.raw = raw;\n    zi->ci.pos_local_header = ZTELL64(zi->z_filefunc,zi->filestream);\n\n    zi->ci.size_centralheader = SIZECENTRALHEADER + size_filename + size_extrafield_global + size_comment;\n    zi->ci.size_centralExtraFree = 32; // Extra space we have reserved in case we need to add ZIP64 extra info data\n\n    zi->ci.central_header = (char*)ALLOC((uInt)zi->ci.size_centralheader + zi->ci.size_centralExtraFree);\n\n    zi->ci.size_centralExtra = size_extrafield_global;\n    zip64local_putValue_inmemory(zi->ci.central_header,(uLong)CENTRALHEADERMAGIC,4);\n    /* version info */\n    zip64local_putValue_inmemory(zi->ci.central_header+4,(uLong)versionMadeBy,2);\n    zip64local_putValue_inmemory(zi->ci.central_header+6,(uLong)20,2);\n    zip64local_putValue_inmemory(zi->ci.central_header+8,(uLong)zi->ci.flag,2);\n    zip64local_putValue_inmemory(zi->ci.central_header+10,(uLong)zi->ci.method,2);\n    zip64local_putValue_inmemory(zi->ci.central_header+12,(uLong)zi->ci.dosDate,4);\n    zip64local_putValue_inmemory(zi->ci.central_header+16,(uLong)0,4); /*crc*/\n    zip64local_putValue_inmemory(zi->ci.central_header+20,(uLong)0,4); /*compr size*/\n    zip64local_putValue_inmemory(zi->ci.central_header+24,(uLong)0,4); /*uncompr size*/\n    zip64local_putValue_inmemory(zi->ci.central_header+28,(uLong)size_filename,2);\n    zip64local_putValue_inmemory(zi->ci.central_header+30,(uLong)size_extrafield_global,2);\n    zip64local_putValue_inmemory(zi->ci.central_header+32,(uLong)size_comment,2);\n    zip64local_putValue_inmemory(zi->ci.central_header+34,(uLong)0,2); /*disk nm start*/\n\n    if (zipfi==NULL)\n        zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)0,2);\n    else\n        zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)zipfi->internal_fa,2);\n\n    if (zipfi==NULL)\n        zip64local_putValue_inmemory(zi->ci.central_header+38,(uLong)0,4);\n    else\n        zip64local_putValue_inmemory(zi->ci.central_header+38,(uLong)zipfi->external_fa,4);\n\n    if(zi->ci.pos_local_header >= 0xffffffff)\n      zip64local_putValue_inmemory(zi->ci.central_header+42,(uLong)0xffffffff,4);\n    else\n      zip64local_putValue_inmemory(zi->ci.central_header+42,(uLong)zi->ci.pos_local_header - zi->add_position_when_writing_offset,4);\n\n    for (i=0;i<size_filename;i++)\n        *(zi->ci.central_header+SIZECENTRALHEADER+i) = *(filename+i);\n\n    for (i=0;i<size_extrafield_global;i++)\n        *(zi->ci.central_header+SIZECENTRALHEADER+size_filename+i) =\n              *(((const char*)extrafield_global)+i);\n\n    for (i=0;i<size_comment;i++)\n        *(zi->ci.central_header+SIZECENTRALHEADER+size_filename+\n              size_extrafield_global+i) = *(comment+i);\n    if (zi->ci.central_header == NULL)\n        return ZIP_INTERNALERROR;\n\n    zi->ci.zip64 = zip64;\n    zi->ci.totalCompressedData = 0;\n    zi->ci.totalUncompressedData = 0;\n    zi->ci.pos_zip64extrainfo = 0;\n\n    err = Write_LocalFileHeader(zi, filename, size_extrafield_local, extrafield_local);\n\n#ifdef HAVE_BZIP2\n    zi->ci.bstream.avail_in = (uInt)0;\n    zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE;\n    zi->ci.bstream.next_out = (char*)zi->ci.buffered_data;\n    zi->ci.bstream.total_in_hi32 = 0;\n    zi->ci.bstream.total_in_lo32 = 0;\n    zi->ci.bstream.total_out_hi32 = 0;\n    zi->ci.bstream.total_out_lo32 = 0;\n#endif\n\n    zi->ci.stream.avail_in = (uInt)0;\n    zi->ci.stream.avail_out = (uInt)Z_BUFSIZE;\n    zi->ci.stream.next_out = zi->ci.buffered_data;\n    zi->ci.stream.total_in = 0;\n    zi->ci.stream.total_out = 0;\n    zi->ci.stream.data_type = Z_BINARY;\n\n#ifdef HAVE_BZIP2\n    if ((err==ZIP_OK) && (zi->ci.method == Z_DEFLATED || zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))\n#else\n    if ((err==ZIP_OK) && (zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))\n#endif\n    {\n        if(zi->ci.method == Z_DEFLATED)\n        {\n          zi->ci.stream.zalloc = (alloc_func)0;\n          zi->ci.stream.zfree = (free_func)0;\n          zi->ci.stream.opaque = (voidpf)0;\n\n          if (windowBits>0)\n              windowBits = -windowBits;\n\n          err = deflateInit2(&zi->ci.stream, level, Z_DEFLATED, windowBits, memLevel, strategy);\n\n          if (err==Z_OK)\n              zi->ci.stream_initialised = Z_DEFLATED;\n        }\n        else if(zi->ci.method == Z_BZIP2ED)\n        {\n#ifdef HAVE_BZIP2\n            // Init BZip stuff here\n          zi->ci.bstream.bzalloc = 0;\n          zi->ci.bstream.bzfree = 0;\n          zi->ci.bstream.opaque = (voidpf)0;\n\n          err = BZ2_bzCompressInit(&zi->ci.bstream, level, 0,35);\n          if(err == BZ_OK)\n            zi->ci.stream_initialised = Z_BZIP2ED;\n#endif\n        }\n\n    }\n\n#    ifndef NOCRYPT\n    zi->ci.crypt_header_size = 0;\n    if ((err==Z_OK) && (password != NULL))\n    {\n        unsigned char bufHead[RAND_HEAD_LEN];\n        unsigned int sizeHead;\n        zi->ci.encrypt = 1;\n        zi->ci.pcrc_32_tab = get_crc_table();\n        /*init_keys(password,zi->ci.keys,zi->ci.pcrc_32_tab);*/\n\n        sizeHead=crypthead(password,bufHead,RAND_HEAD_LEN,zi->ci.keys,zi->ci.pcrc_32_tab,crcForCrypting);\n        zi->ci.crypt_header_size = sizeHead;\n\n        if (ZWRITE64(zi->z_filefunc,zi->filestream,bufHead,sizeHead) != sizeHead)\n                err = ZIP_ERRNO;\n    }\n#    endif\n\n    if (err==Z_OK)\n        zi->in_opened_file_inzip = 1;\n    return err;\n}\n\nextern int ZEXPORT zipOpenNewFileInZip4(zipFile file, const char* filename, const zip_fileinfo* zipfi,\n                                        const void* extrafield_local, uInt size_extrafield_local,\n                                        const void* extrafield_global, uInt size_extrafield_global,\n                                        const char* comment, int method, int level, int raw,\n                                        int windowBits,int memLevel, int strategy,\n                                        const char* password, uLong crcForCrypting,\n                                        uLong versionMadeBy, uLong flagBase) {\n    return zipOpenNewFileInZip4_64(file, filename, zipfi,\n                                   extrafield_local, size_extrafield_local,\n                                   extrafield_global, size_extrafield_global,\n                                   comment, method, level, raw,\n                                   windowBits, memLevel, strategy,\n                                   password, crcForCrypting, versionMadeBy, flagBase, 0);\n}\n\nextern int ZEXPORT zipOpenNewFileInZip3(zipFile file, const char* filename, const zip_fileinfo* zipfi,\n                                        const void* extrafield_local, uInt size_extrafield_local,\n                                        const void* extrafield_global, uInt size_extrafield_global,\n                                        const char* comment, int method, int level, int raw,\n                                        int windowBits,int memLevel, int strategy,\n                                        const char* password, uLong crcForCrypting) {\n    return zipOpenNewFileInZip4_64(file, filename, zipfi,\n                                   extrafield_local, size_extrafield_local,\n                                   extrafield_global, size_extrafield_global,\n                                   comment, method, level, raw,\n                                   windowBits, memLevel, strategy,\n                                   password, crcForCrypting, VERSIONMADEBY, 0, 0);\n}\n\nextern int ZEXPORT zipOpenNewFileInZip3_64(zipFile file, const char* filename, const zip_fileinfo* zipfi,\n                                         const void* extrafield_local, uInt size_extrafield_local,\n                                         const void* extrafield_global, uInt size_extrafield_global,\n                                         const char* comment, int method, int level, int raw,\n                                         int windowBits,int memLevel, int strategy,\n                                         const char* password, uLong crcForCrypting, int zip64) {\n    return zipOpenNewFileInZip4_64(file, filename, zipfi,\n                                   extrafield_local, size_extrafield_local,\n                                   extrafield_global, size_extrafield_global,\n                                   comment, method, level, raw,\n                                   windowBits, memLevel, strategy,\n                                   password, crcForCrypting, VERSIONMADEBY, 0, zip64);\n}\n\nextern int ZEXPORT zipOpenNewFileInZip2(zipFile file, const char* filename, const zip_fileinfo* zipfi,\n                                        const void* extrafield_local, uInt size_extrafield_local,\n                                        const void* extrafield_global, uInt size_extrafield_global,\n                                        const char* comment, int method, int level, int raw) {\n    return zipOpenNewFileInZip4_64(file, filename, zipfi,\n                                   extrafield_local, size_extrafield_local,\n                                   extrafield_global, size_extrafield_global,\n                                   comment, method, level, raw,\n                                   -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,\n                                   NULL, 0, VERSIONMADEBY, 0, 0);\n}\n\nextern int ZEXPORT zipOpenNewFileInZip2_64(zipFile file, const char* filename, const zip_fileinfo* zipfi,\n                                           const void* extrafield_local, uInt size_extrafield_local,\n                                           const void* extrafield_global, uInt size_extrafield_global,\n                                           const char* comment, int method, int level, int raw, int zip64) {\n    return zipOpenNewFileInZip4_64(file, filename, zipfi,\n                                   extrafield_local, size_extrafield_local,\n                                   extrafield_global, size_extrafield_global,\n                                   comment, method, level, raw,\n                                   -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,\n                                   NULL, 0, VERSIONMADEBY, 0, zip64);\n}\n\nextern int ZEXPORT zipOpenNewFileInZip64(zipFile file, const char* filename, const zip_fileinfo* zipfi,\n                                         const void* extrafield_local, uInt size_extrafield_local,\n                                         const void*extrafield_global, uInt size_extrafield_global,\n                                         const char* comment, int method, int level, int zip64) {\n    return zipOpenNewFileInZip4_64(file, filename, zipfi,\n                                   extrafield_local, size_extrafield_local,\n                                   extrafield_global, size_extrafield_global,\n                                   comment, method, level, 0,\n                                   -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,\n                                   NULL, 0, VERSIONMADEBY, 0, zip64);\n}\n\nextern int ZEXPORT zipOpenNewFileInZip(zipFile file, const char* filename, const zip_fileinfo* zipfi,\n                                       const void* extrafield_local, uInt size_extrafield_local,\n                                       const void*extrafield_global, uInt size_extrafield_global,\n                                       const char* comment, int method, int level) {\n    return zipOpenNewFileInZip4_64(file, filename, zipfi,\n                                   extrafield_local, size_extrafield_local,\n                                   extrafield_global, size_extrafield_global,\n                                   comment, method, level, 0,\n                                   -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,\n                                   NULL, 0, VERSIONMADEBY, 0, 0);\n}\n\nlocal int zip64FlushWriteBuffer(zip64_internal* zi) {\n    int err=ZIP_OK;\n\n    if (zi->ci.encrypt != 0)\n    {\n#ifndef NOCRYPT\n        uInt i;\n        int t;\n        for (i=0;i<zi->ci.pos_in_buffered_data;i++)\n            zi->ci.buffered_data[i] = zencode(zi->ci.keys, zi->ci.pcrc_32_tab, zi->ci.buffered_data[i],t);\n#endif\n    }\n\n    if (ZWRITE64(zi->z_filefunc,zi->filestream,zi->ci.buffered_data,zi->ci.pos_in_buffered_data) != zi->ci.pos_in_buffered_data)\n      err = ZIP_ERRNO;\n\n    zi->ci.totalCompressedData += zi->ci.pos_in_buffered_data;\n\n#ifdef HAVE_BZIP2\n    if(zi->ci.method == Z_BZIP2ED)\n    {\n      zi->ci.totalUncompressedData += zi->ci.bstream.total_in_lo32;\n      zi->ci.bstream.total_in_lo32 = 0;\n      zi->ci.bstream.total_in_hi32 = 0;\n    }\n    else\n#endif\n    {\n      zi->ci.totalUncompressedData += zi->ci.stream.total_in;\n      zi->ci.stream.total_in = 0;\n    }\n\n\n    zi->ci.pos_in_buffered_data = 0;\n\n    return err;\n}\n\nextern int ZEXPORT zipWriteInFileInZip(zipFile file, const void* buf, unsigned int len) {\n    zip64_internal* zi;\n    int err=ZIP_OK;\n\n    if (file == NULL)\n        return ZIP_PARAMERROR;\n    zi = (zip64_internal*)file;\n\n    if (zi->in_opened_file_inzip == 0)\n        return ZIP_PARAMERROR;\n\n    zi->ci.crc32 = crc32(zi->ci.crc32,buf,(uInt)len);\n\n#ifdef HAVE_BZIP2\n    if(zi->ci.method == Z_BZIP2ED && (!zi->ci.raw))\n    {\n      zi->ci.bstream.next_in = (void*)buf;\n      zi->ci.bstream.avail_in = len;\n      err = BZ_RUN_OK;\n\n      while ((err==BZ_RUN_OK) && (zi->ci.bstream.avail_in>0))\n      {\n        if (zi->ci.bstream.avail_out == 0)\n        {\n          if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)\n            err = ZIP_ERRNO;\n          zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE;\n          zi->ci.bstream.next_out = (char*)zi->ci.buffered_data;\n        }\n\n\n        if(err != BZ_RUN_OK)\n          break;\n\n        if ((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))\n        {\n          uLong uTotalOutBefore_lo = zi->ci.bstream.total_out_lo32;\n//          uLong uTotalOutBefore_hi = zi->ci.bstream.total_out_hi32;\n          err=BZ2_bzCompress(&zi->ci.bstream,  BZ_RUN);\n\n          zi->ci.pos_in_buffered_data += (uInt)(zi->ci.bstream.total_out_lo32 - uTotalOutBefore_lo) ;\n        }\n      }\n\n      if(err == BZ_RUN_OK)\n        err = ZIP_OK;\n    }\n    else\n#endif\n    {\n      zi->ci.stream.next_in = (Bytef*)(uintptr_t)buf;\n      zi->ci.stream.avail_in = len;\n\n      while ((err==ZIP_OK) && (zi->ci.stream.avail_in>0))\n      {\n          if (zi->ci.stream.avail_out == 0)\n          {\n              if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)\n                  err = ZIP_ERRNO;\n              zi->ci.stream.avail_out = (uInt)Z_BUFSIZE;\n              zi->ci.stream.next_out = zi->ci.buffered_data;\n          }\n\n\n          if(err != ZIP_OK)\n              break;\n\n          if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))\n          {\n              uLong uTotalOutBefore = zi->ci.stream.total_out;\n              err=deflate(&zi->ci.stream,  Z_NO_FLUSH);\n\n              zi->ci.pos_in_buffered_data += (uInt)(zi->ci.stream.total_out - uTotalOutBefore) ;\n          }\n          else\n          {\n              uInt copy_this,i;\n              if (zi->ci.stream.avail_in < zi->ci.stream.avail_out)\n                  copy_this = zi->ci.stream.avail_in;\n              else\n                  copy_this = zi->ci.stream.avail_out;\n\n              for (i = 0; i < copy_this; i++)\n                  *(((char*)zi->ci.stream.next_out)+i) =\n                      *(((const char*)zi->ci.stream.next_in)+i);\n              {\n                  zi->ci.stream.avail_in -= copy_this;\n                  zi->ci.stream.avail_out-= copy_this;\n                  zi->ci.stream.next_in+= copy_this;\n                  zi->ci.stream.next_out+= copy_this;\n                  zi->ci.stream.total_in+= copy_this;\n                  zi->ci.stream.total_out+= copy_this;\n                  zi->ci.pos_in_buffered_data += copy_this;\n              }\n          }\n      }// while(...)\n    }\n\n    return err;\n}\n\nextern int ZEXPORT zipCloseFileInZipRaw(zipFile file, uLong uncompressed_size, uLong crc32) {\n    return zipCloseFileInZipRaw64 (file, uncompressed_size, crc32);\n}\n\nextern int ZEXPORT zipCloseFileInZipRaw64(zipFile file, ZPOS64_T uncompressed_size, uLong crc32) {\n    zip64_internal* zi;\n    ZPOS64_T compressed_size;\n    uLong invalidValue = 0xffffffff;\n    unsigned datasize = 0;\n    int err=ZIP_OK;\n\n    if (file == NULL)\n        return ZIP_PARAMERROR;\n    zi = (zip64_internal*)file;\n\n    if (zi->in_opened_file_inzip == 0)\n        return ZIP_PARAMERROR;\n    zi->ci.stream.avail_in = 0;\n\n    if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))\n                {\n                        while (err==ZIP_OK)\n                        {\n                                uLong uTotalOutBefore;\n                                if (zi->ci.stream.avail_out == 0)\n                                {\n                                        if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)\n                                                err = ZIP_ERRNO;\n                                        zi->ci.stream.avail_out = (uInt)Z_BUFSIZE;\n                                        zi->ci.stream.next_out = zi->ci.buffered_data;\n                                }\n                                uTotalOutBefore = zi->ci.stream.total_out;\n                                err=deflate(&zi->ci.stream,  Z_FINISH);\n                                zi->ci.pos_in_buffered_data += (uInt)(zi->ci.stream.total_out - uTotalOutBefore) ;\n                        }\n                }\n    else if ((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))\n    {\n#ifdef HAVE_BZIP2\n      err = BZ_FINISH_OK;\n      while (err==BZ_FINISH_OK)\n      {\n        uLong uTotalOutBefore;\n        if (zi->ci.bstream.avail_out == 0)\n        {\n          if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)\n            err = ZIP_ERRNO;\n          zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE;\n          zi->ci.bstream.next_out = (char*)zi->ci.buffered_data;\n        }\n        uTotalOutBefore = zi->ci.bstream.total_out_lo32;\n        err=BZ2_bzCompress(&zi->ci.bstream,  BZ_FINISH);\n        if(err == BZ_STREAM_END)\n          err = Z_STREAM_END;\n\n        zi->ci.pos_in_buffered_data += (uInt)(zi->ci.bstream.total_out_lo32 - uTotalOutBefore);\n      }\n\n      if(err == BZ_FINISH_OK)\n        err = ZIP_OK;\n#endif\n    }\n\n    if (err==Z_STREAM_END)\n        err=ZIP_OK; /* this is normal */\n\n    if ((zi->ci.pos_in_buffered_data>0) && (err==ZIP_OK))\n                {\n        if (zip64FlushWriteBuffer(zi)==ZIP_ERRNO)\n            err = ZIP_ERRNO;\n                }\n\n    if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))\n    {\n        int tmp_err = deflateEnd(&zi->ci.stream);\n        if (err == ZIP_OK)\n            err = tmp_err;\n        zi->ci.stream_initialised = 0;\n    }\n#ifdef HAVE_BZIP2\n    else if((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))\n    {\n      int tmperr = BZ2_bzCompressEnd(&zi->ci.bstream);\n                        if (err==ZIP_OK)\n                                err = tmperr;\n                        zi->ci.stream_initialised = 0;\n    }\n#endif\n\n    if (!zi->ci.raw)\n    {\n        crc32 = (uLong)zi->ci.crc32;\n        uncompressed_size = zi->ci.totalUncompressedData;\n    }\n    compressed_size = zi->ci.totalCompressedData;\n\n#    ifndef NOCRYPT\n    compressed_size += zi->ci.crypt_header_size;\n#    endif\n\n    // update Current Item crc and sizes,\n    if(compressed_size >= 0xffffffff || uncompressed_size >= 0xffffffff || zi->ci.pos_local_header >= 0xffffffff)\n    {\n      /*version Made by*/\n      zip64local_putValue_inmemory(zi->ci.central_header+4,(uLong)45,2);\n      /*version needed*/\n      zip64local_putValue_inmemory(zi->ci.central_header+6,(uLong)45,2);\n\n    }\n\n    zip64local_putValue_inmemory(zi->ci.central_header+16,crc32,4); /*crc*/\n\n\n    if(compressed_size >= 0xffffffff)\n      zip64local_putValue_inmemory(zi->ci.central_header+20, invalidValue,4); /*compr size*/\n    else\n      zip64local_putValue_inmemory(zi->ci.central_header+20, compressed_size,4); /*compr size*/\n\n    /// set internal file attributes field\n    if (zi->ci.stream.data_type == Z_ASCII)\n        zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)Z_ASCII,2);\n\n    if(uncompressed_size >= 0xffffffff)\n      zip64local_putValue_inmemory(zi->ci.central_header+24, invalidValue,4); /*uncompr size*/\n    else\n      zip64local_putValue_inmemory(zi->ci.central_header+24, uncompressed_size,4); /*uncompr size*/\n\n    // Add ZIP64 extra info field for uncompressed size\n    if(uncompressed_size >= 0xffffffff)\n      datasize += 8;\n\n    // Add ZIP64 extra info field for compressed size\n    if(compressed_size >= 0xffffffff)\n      datasize += 8;\n\n    // Add ZIP64 extra info field for relative offset to local file header of current file\n    if(zi->ci.pos_local_header >= 0xffffffff)\n      datasize += 8;\n\n    if(datasize > 0)\n    {\n      char* p = NULL;\n\n      if((uLong)(datasize + 4) > zi->ci.size_centralExtraFree)\n      {\n        // we can not write more data to the buffer that we have room for.\n        return ZIP_BADZIPFILE;\n      }\n\n      p = zi->ci.central_header + zi->ci.size_centralheader;\n\n      // Add Extra Information Header for 'ZIP64 information'\n      zip64local_putValue_inmemory(p, 0x0001, 2); // HeaderID\n      p += 2;\n      zip64local_putValue_inmemory(p, datasize, 2); // DataSize\n      p += 2;\n\n      if(uncompressed_size >= 0xffffffff)\n      {\n        zip64local_putValue_inmemory(p, uncompressed_size, 8);\n        p += 8;\n      }\n\n      if(compressed_size >= 0xffffffff)\n      {\n        zip64local_putValue_inmemory(p, compressed_size, 8);\n        p += 8;\n      }\n\n      if(zi->ci.pos_local_header >= 0xffffffff)\n      {\n        zip64local_putValue_inmemory(p, zi->ci.pos_local_header, 8);\n        p += 8;\n      }\n\n      // Update how much extra free space we got in the memory buffer\n      // and increase the centralheader size so the new ZIP64 fields are included\n      // ( 4 below is the size of HeaderID and DataSize field )\n      zi->ci.size_centralExtraFree -= datasize + 4;\n      zi->ci.size_centralheader += datasize + 4;\n\n      // Update the extra info size field\n      zi->ci.size_centralExtra += datasize + 4;\n      zip64local_putValue_inmemory(zi->ci.central_header+30,(uLong)zi->ci.size_centralExtra,2);\n    }\n\n    if (err==ZIP_OK)\n        err = add_data_in_datablock(&zi->central_dir, zi->ci.central_header, (uLong)zi->ci.size_centralheader);\n\n    free(zi->ci.central_header);\n\n    if (err==ZIP_OK)\n    {\n        // Update the LocalFileHeader with the new values.\n\n        ZPOS64_T cur_pos_inzip = ZTELL64(zi->z_filefunc,zi->filestream);\n\n        if (ZSEEK64(zi->z_filefunc,zi->filestream, zi->ci.pos_local_header + 14,ZLIB_FILEFUNC_SEEK_SET)!=0)\n            err = ZIP_ERRNO;\n\n        if (err==ZIP_OK)\n            err = zip64local_putValue(&zi->z_filefunc,zi->filestream,crc32,4); /* crc 32, unknown */\n\n        if(uncompressed_size >= 0xffffffff || compressed_size >= 0xffffffff )\n        {\n          if(zi->ci.pos_zip64extrainfo > 0)\n          {\n            // Update the size in the ZIP64 extended field.\n            if (ZSEEK64(zi->z_filefunc,zi->filestream, zi->ci.pos_zip64extrainfo + 4,ZLIB_FILEFUNC_SEEK_SET)!=0)\n              err = ZIP_ERRNO;\n\n            if (err==ZIP_OK) /* compressed size, unknown */\n              err = zip64local_putValue(&zi->z_filefunc, zi->filestream, uncompressed_size, 8);\n\n            if (err==ZIP_OK) /* uncompressed size, unknown */\n              err = zip64local_putValue(&zi->z_filefunc, zi->filestream, compressed_size, 8);\n          }\n          else\n              err = ZIP_BADZIPFILE; // Caller passed zip64 = 0, so no room for zip64 info -> fatal\n        }\n        else\n        {\n          if (err==ZIP_OK) /* compressed size, unknown */\n              err = zip64local_putValue(&zi->z_filefunc,zi->filestream,compressed_size,4);\n\n          if (err==ZIP_OK) /* uncompressed size, unknown */\n              err = zip64local_putValue(&zi->z_filefunc,zi->filestream,uncompressed_size,4);\n        }\n\n        if (ZSEEK64(zi->z_filefunc,zi->filestream, cur_pos_inzip,ZLIB_FILEFUNC_SEEK_SET)!=0)\n            err = ZIP_ERRNO;\n    }\n\n    zi->number_entry ++;\n    zi->in_opened_file_inzip = 0;\n\n    return err;\n}\n\nextern int ZEXPORT zipCloseFileInZip(zipFile file) {\n    return zipCloseFileInZipRaw (file,0,0);\n}\n\nlocal int Write_Zip64EndOfCentralDirectoryLocator(zip64_internal* zi, ZPOS64_T zip64eocd_pos_inzip) {\n  int err = ZIP_OK;\n  ZPOS64_T pos = zip64eocd_pos_inzip - zi->add_position_when_writing_offset;\n\n  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ZIP64ENDLOCHEADERMAGIC,4);\n\n  /*num disks*/\n    if (err==ZIP_OK) /* number of the disk with the start of the central directory */\n      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4);\n\n  /*relative offset*/\n    if (err==ZIP_OK) /* Relative offset to the Zip64EndOfCentralDirectory */\n      err = zip64local_putValue(&zi->z_filefunc,zi->filestream, pos,8);\n\n  /*total disks*/ /* Do not support spawning of disk so always say 1 here*/\n    if (err==ZIP_OK) /* number of the disk with the start of the central directory */\n      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)1,4);\n\n    return err;\n}\n\nlocal int Write_Zip64EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip) {\n  int err = ZIP_OK;\n\n  uLong Zip64DataSize = 44;\n\n  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ZIP64ENDHEADERMAGIC,4);\n\n  if (err==ZIP_OK) /* size of this 'zip64 end of central directory' */\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(ZPOS64_T)Zip64DataSize,8); // why ZPOS64_T of this ?\n\n  if (err==ZIP_OK) /* version made by */\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);\n\n  if (err==ZIP_OK) /* version needed */\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);\n\n  if (err==ZIP_OK) /* number of this disk */\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4);\n\n  if (err==ZIP_OK) /* number of the disk with the start of the central directory */\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4);\n\n  if (err==ZIP_OK) /* total number of entries in the central dir on this disk */\n    err = zip64local_putValue(&zi->z_filefunc, zi->filestream, zi->number_entry, 8);\n\n  if (err==ZIP_OK) /* total number of entries in the central dir */\n    err = zip64local_putValue(&zi->z_filefunc, zi->filestream, zi->number_entry, 8);\n\n  if (err==ZIP_OK) /* size of the central directory */\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(ZPOS64_T)size_centraldir,8);\n\n  if (err==ZIP_OK) /* offset of start of central directory with respect to the starting disk number */\n  {\n    ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (ZPOS64_T)pos,8);\n  }\n  return err;\n}\n\nlocal int Write_EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip) {\n  int err = ZIP_OK;\n\n  /*signature*/\n  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ENDHEADERMAGIC,4);\n\n  if (err==ZIP_OK) /* number of this disk */\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,2);\n\n  if (err==ZIP_OK) /* number of the disk with the start of the central directory */\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,2);\n\n  if (err==ZIP_OK) /* total number of entries in the central dir on this disk */\n  {\n    {\n      if(zi->number_entry >= 0xFFFF)\n        err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xffff,2); // use value in ZIP64 record\n      else\n        err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->number_entry,2);\n    }\n  }\n\n  if (err==ZIP_OK) /* total number of entries in the central dir */\n  {\n    if(zi->number_entry >= 0xFFFF)\n      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xffff,2); // use value in ZIP64 record\n    else\n      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->number_entry,2);\n  }\n\n  if (err==ZIP_OK) /* size of the central directory */\n    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_centraldir,4);\n\n  if (err==ZIP_OK) /* offset of start of central directory with respect to the starting disk number */\n  {\n    ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;\n    if(pos >= 0xffffffff)\n    {\n      err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (uLong)0xffffffff,4);\n    }\n    else\n      err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (uLong)(centraldir_pos_inzip - zi->add_position_when_writing_offset),4);\n  }\n\n   return err;\n}\n\nlocal int Write_GlobalComment(zip64_internal* zi, const char* global_comment) {\n  int err = ZIP_OK;\n  uInt size_global_comment = 0;\n\n  if(global_comment != NULL)\n    size_global_comment = (uInt)strlen(global_comment);\n\n  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_global_comment,2);\n\n  if (err == ZIP_OK && size_global_comment > 0)\n  {\n    if (ZWRITE64(zi->z_filefunc,zi->filestream, global_comment, size_global_comment) != size_global_comment)\n      err = ZIP_ERRNO;\n  }\n  return err;\n}\n\nextern int ZEXPORT zipClose(zipFile file, const char* global_comment) {\n    zip64_internal* zi;\n    int err = 0;\n    uLong size_centraldir = 0;\n    ZPOS64_T centraldir_pos_inzip;\n    ZPOS64_T pos;\n\n    if (file == NULL)\n        return ZIP_PARAMERROR;\n\n    zi = (zip64_internal*)file;\n\n    if (zi->in_opened_file_inzip == 1)\n    {\n        err = zipCloseFileInZip (file);\n    }\n\n#ifndef NO_ADDFILEINEXISTINGZIP\n    if (global_comment==NULL)\n        global_comment = zi->globalcomment;\n#endif\n\n    centraldir_pos_inzip = ZTELL64(zi->z_filefunc,zi->filestream);\n\n    if (err==ZIP_OK)\n    {\n        linkedlist_datablock_internal* ldi = zi->central_dir.first_block;\n        while (ldi!=NULL)\n        {\n            if ((err==ZIP_OK) && (ldi->filled_in_this_block>0))\n            {\n                if (ZWRITE64(zi->z_filefunc,zi->filestream, ldi->data, ldi->filled_in_this_block) != ldi->filled_in_this_block)\n                    err = ZIP_ERRNO;\n            }\n\n            size_centraldir += ldi->filled_in_this_block;\n            ldi = ldi->next_datablock;\n        }\n    }\n    free_linkedlist(&(zi->central_dir));\n\n    pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;\n    if(pos >= 0xffffffff || zi->number_entry >= 0xFFFF)\n    {\n      ZPOS64_T Zip64EOCDpos = ZTELL64(zi->z_filefunc,zi->filestream);\n      Write_Zip64EndOfCentralDirectoryRecord(zi, size_centraldir, centraldir_pos_inzip);\n\n      Write_Zip64EndOfCentralDirectoryLocator(zi, Zip64EOCDpos);\n    }\n\n    if (err==ZIP_OK)\n      err = Write_EndOfCentralDirectoryRecord(zi, size_centraldir, centraldir_pos_inzip);\n\n    if(err == ZIP_OK)\n      err = Write_GlobalComment(zi, global_comment);\n\n    if (ZCLOSE64(zi->z_filefunc,zi->filestream) != 0)\n        if (err == ZIP_OK)\n            err = ZIP_ERRNO;\n\n#ifndef NO_ADDFILEINEXISTINGZIP\n    free(zi->globalcomment);\n#endif\n    free(zi);\n\n    return err;\n}\n\nextern int ZEXPORT zipRemoveExtraInfoBlock(char* pData, int* dataLen, short sHeader) {\n  char* p = pData;\n  int size = 0;\n  char* pNewHeader;\n  char* pTmp;\n  short header;\n  short dataSize;\n\n  int retVal = ZIP_OK;\n\n  if(pData == NULL || dataLen == NULL || *dataLen < 4)\n    return ZIP_PARAMERROR;\n\n  pNewHeader = (char*)ALLOC((unsigned)*dataLen);\n  pTmp = pNewHeader;\n\n  while(p < (pData + *dataLen))\n  {\n    header = *(short*)p;\n    dataSize = *(((short*)p)+1);\n\n    if( header == sHeader ) // Header found.\n    {\n      p += dataSize + 4; // skip it. do not copy to temp buffer\n    }\n    else\n    {\n      // Extra Info block should not be removed, So copy it to the temp buffer.\n      memcpy(pTmp, p, dataSize + 4);\n      p += dataSize + 4;\n      size += dataSize + 4;\n    }\n\n  }\n\n  if(size < *dataLen)\n  {\n    // clean old extra info block.\n    memset(pData,0, *dataLen);\n\n    // copy the new extra info block over the old\n    if(size > 0)\n      memcpy(pData, pNewHeader, size);\n\n    // set the new extra info size\n    *dataLen = size;\n\n    retVal = ZIP_OK;\n  }\n  else\n    retVal = ZIP_ERRNO;\n\n  free(pNewHeader);\n\n  return retVal;\n}\n"
  },
  {
    "path": "Telegram/ThirdParty/minizip/zip.h",
    "content": "/* zip.h -- IO on .zip files using zlib\n   Version 1.1, February 14h, 2010\n   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )\n\n         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )\n\n         Modifications for Zip64 support\n         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )\n\n         For more info read MiniZip_info.txt\n\n         ---------------------------------------------------------------------------\n\n   Condition of use and distribution are the same than zlib :\n\n  This software is provided 'as-is', without any express or implied\n  warranty.  In no event will the authors be held liable for any damages\n  arising from the use of this software.\n\n  Permission is granted to anyone to use this software for any purpose,\n  including commercial applications, and to alter it and redistribute it\n  freely, subject to the following restrictions:\n\n  1. The origin of this software must not be misrepresented; you must not\n     claim that you wrote the original software. If you use this software\n     in a product, an acknowledgment in the product documentation would be\n     appreciated but is not required.\n  2. Altered source versions must be plainly marked as such, and must not be\n     misrepresented as being the original software.\n  3. This notice may not be removed or altered from any source distribution.\n\n        ---------------------------------------------------------------------------\n\n        Changes\n\n        See header of zip.h\n\n*/\n\n#ifndef _zip12_H\n#define _zip12_H\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n//#define HAVE_BZIP2\n\n#ifndef _ZLIB_H\n#include \"zlib.h\"\n#endif\n\n#ifndef _ZLIBIOAPI_H\n#include \"ioapi.h\"\n#endif\n\n#ifdef HAVE_BZIP2\n#include \"bzlib.h\"\n#endif\n\n#define Z_BZIP2ED 12\n\n#if defined(STRICTZIP) || defined(STRICTZIPUNZIP)\n/* like the STRICT of WIN32, we define a pointer that cannot be converted\n    from (void*) without cast */\ntypedef struct TagzipFile__ { int unused; } zipFile__;\ntypedef zipFile__ *zipFile;\n#else\ntypedef voidp zipFile;\n#endif\n\n#define ZIP_OK                          (0)\n#define ZIP_EOF                         (0)\n#define ZIP_ERRNO                       (Z_ERRNO)\n#define ZIP_PARAMERROR                  (-102)\n#define ZIP_BADZIPFILE                  (-103)\n#define ZIP_INTERNALERROR               (-104)\n\n#ifndef DEF_MEM_LEVEL\n#  if MAX_MEM_LEVEL >= 8\n#    define DEF_MEM_LEVEL 8\n#  else\n#    define DEF_MEM_LEVEL  MAX_MEM_LEVEL\n#  endif\n#endif\n/* default memLevel */\n\n/* tm_zip contain date/time info */\ntypedef struct tm_zip_s\n{\n    int tm_sec;             /* seconds after the minute - [0,59] */\n    int tm_min;             /* minutes after the hour - [0,59] */\n    int tm_hour;            /* hours since midnight - [0,23] */\n    int tm_mday;            /* day of the month - [1,31] */\n    int tm_mon;             /* months since January - [0,11] */\n    int tm_year;            /* years - [1980..2044] */\n} tm_zip;\n\ntypedef struct\n{\n    tm_zip      tmz_date;       /* date in understandable format           */\n    uLong       dosDate;       /* if dos_date == 0, tmu_date is used      */\n/*    uLong       flag;        */   /* general purpose bit flag        2 bytes */\n\n    uLong       internal_fa;    /* internal file attributes        2 bytes */\n    uLong       external_fa;    /* external file attributes        4 bytes */\n} zip_fileinfo;\n\ntypedef const char* zipcharpc;\n\n\n#define APPEND_STATUS_CREATE        (0)\n#define APPEND_STATUS_CREATEAFTER   (1)\n#define APPEND_STATUS_ADDINZIP      (2)\n\nextern zipFile ZEXPORT zipOpen(const char *pathname, int append);\nextern zipFile ZEXPORT zipOpen64(const void *pathname, int append);\n/*\n  Create a zipfile.\n     pathname contain on Windows XP a filename like \"c:\\\\zlib\\\\zlib113.zip\" or on\n       an Unix computer \"zlib/zlib113.zip\".\n     if the file pathname exist and append==APPEND_STATUS_CREATEAFTER, the zip\n       will be created at the end of the file.\n         (useful if the file contain a self extractor code)\n     if the file pathname exist and append==APPEND_STATUS_ADDINZIP, we will\n       add files in existing zip (be sure you don't add file that doesn't exist)\n     If the zipfile cannot be opened, the return value is NULL.\n     Else, the return value is a zipFile Handle, usable with other function\n       of this zip package.\n*/\n\n/* Note : there is no delete function into a zipfile.\n   If you want delete file into a zipfile, you must open a zipfile, and create another\n   Of course, you can use RAW reading and writing to copy the file you did not want delete\n*/\n\nextern zipFile ZEXPORT zipOpen2(const char *pathname,\n                                int append,\n                                zipcharpc* globalcomment,\n                                zlib_filefunc_def* pzlib_filefunc_def);\n\nextern zipFile ZEXPORT zipOpen2_64(const void *pathname,\n                                   int append,\n                                   zipcharpc* globalcomment,\n                                   zlib_filefunc64_def* pzlib_filefunc_def);\n\nextern zipFile ZEXPORT zipOpen3(const void *pathname,\n                                int append,\n                                zipcharpc* globalcomment,\n                                zlib_filefunc64_32_def* pzlib_filefunc64_32_def);\n\nextern int ZEXPORT zipOpenNewFileInZip(zipFile file,\n                                       const char* filename,\n                                       const zip_fileinfo* zipfi,\n                                       const void* extrafield_local,\n                                       uInt size_extrafield_local,\n                                       const void* extrafield_global,\n                                       uInt size_extrafield_global,\n                                       const char* comment,\n                                       int method,\n                                       int level);\n\nextern int ZEXPORT zipOpenNewFileInZip64(zipFile file,\n                                         const char* filename,\n                                         const zip_fileinfo* zipfi,\n                                         const void* extrafield_local,\n                                         uInt size_extrafield_local,\n                                         const void* extrafield_global,\n                                         uInt size_extrafield_global,\n                                         const char* comment,\n                                         int method,\n                                         int level,\n                                         int zip64);\n\n/*\n  Open a file in the ZIP for writing.\n  filename : the filename in zip (if NULL, '-' without quote will be used\n  *zipfi contain supplemental information\n  if extrafield_local!=NULL and size_extrafield_local>0, extrafield_local\n    contains the extrafield data for the local header\n  if extrafield_global!=NULL and size_extrafield_global>0, extrafield_global\n    contains the extrafield data for the global header\n  if comment != NULL, comment contain the comment string\n  method contain the compression method (0 for store, Z_DEFLATED for deflate)\n  level contain the level of compression (can be Z_DEFAULT_COMPRESSION)\n  zip64 is set to 1 if a zip64 extended information block should be added to the local file header.\n                    this MUST be '1' if the uncompressed size is >= 0xffffffff.\n\n*/\n\n\nextern int ZEXPORT zipOpenNewFileInZip2(zipFile file,\n                                        const char* filename,\n                                        const zip_fileinfo* zipfi,\n                                        const void* extrafield_local,\n                                        uInt size_extrafield_local,\n                                        const void* extrafield_global,\n                                        uInt size_extrafield_global,\n                                        const char* comment,\n                                        int method,\n                                        int level,\n                                        int raw);\n\n\nextern int ZEXPORT zipOpenNewFileInZip2_64(zipFile file,\n                                           const char* filename,\n                                           const zip_fileinfo* zipfi,\n                                           const void* extrafield_local,\n                                           uInt size_extrafield_local,\n                                           const void* extrafield_global,\n                                           uInt size_extrafield_global,\n                                           const char* comment,\n                                           int method,\n                                           int level,\n                                           int raw,\n                                           int zip64);\n/*\n  Same than zipOpenNewFileInZip, except if raw=1, we write raw file\n */\n\nextern int ZEXPORT zipOpenNewFileInZip3(zipFile file,\n                                        const char* filename,\n                                        const zip_fileinfo* zipfi,\n                                        const void* extrafield_local,\n                                        uInt size_extrafield_local,\n                                        const void* extrafield_global,\n                                        uInt size_extrafield_global,\n                                        const char* comment,\n                                        int method,\n                                        int level,\n                                        int raw,\n                                        int windowBits,\n                                        int memLevel,\n                                        int strategy,\n                                        const char* password,\n                                        uLong crcForCrypting);\n\nextern int ZEXPORT zipOpenNewFileInZip3_64(zipFile file,\n                                           const char* filename,\n                                           const zip_fileinfo* zipfi,\n                                           const void* extrafield_local,\n                                           uInt size_extrafield_local,\n                                           const void* extrafield_global,\n                                           uInt size_extrafield_global,\n                                           const char* comment,\n                                           int method,\n                                           int level,\n                                           int raw,\n                                           int windowBits,\n                                           int memLevel,\n                                           int strategy,\n                                           const char* password,\n                                           uLong crcForCrypting,\n                                           int zip64);\n\n/*\n  Same than zipOpenNewFileInZip2, except\n    windowBits,memLevel,,strategy : see parameter strategy in deflateInit2\n    password : crypting password (NULL for no crypting)\n    crcForCrypting : crc of file to compress (needed for crypting)\n */\n\nextern int ZEXPORT zipOpenNewFileInZip4(zipFile file,\n                                        const char* filename,\n                                        const zip_fileinfo* zipfi,\n                                        const void* extrafield_local,\n                                        uInt size_extrafield_local,\n                                        const void* extrafield_global,\n                                        uInt size_extrafield_global,\n                                        const char* comment,\n                                        int method,\n                                        int level,\n                                        int raw,\n                                        int windowBits,\n                                        int memLevel,\n                                        int strategy,\n                                        const char* password,\n                                        uLong crcForCrypting,\n                                        uLong versionMadeBy,\n                                        uLong flagBase);\n\n\nextern int ZEXPORT zipOpenNewFileInZip4_64(zipFile file,\n                                           const char* filename,\n                                           const zip_fileinfo* zipfi,\n                                           const void* extrafield_local,\n                                           uInt size_extrafield_local,\n                                           const void* extrafield_global,\n                                           uInt size_extrafield_global,\n                                           const char* comment,\n                                           int method,\n                                           int level,\n                                           int raw,\n                                           int windowBits,\n                                           int memLevel,\n                                           int strategy,\n                                           const char* password,\n                                           uLong crcForCrypting,\n                                           uLong versionMadeBy,\n                                           uLong flagBase,\n                                           int zip64);\n/*\n  Same than zipOpenNewFileInZip4, except\n    versionMadeBy : value for Version made by field\n    flag : value for flag field (compression level info will be added)\n */\n\n\nextern int ZEXPORT zipWriteInFileInZip(zipFile file,\n                                       const void* buf,\n                                       unsigned len);\n/*\n  Write data in the zipfile\n*/\n\nextern int ZEXPORT zipCloseFileInZip(zipFile file);\n/*\n  Close the current file in the zipfile\n*/\n\nextern int ZEXPORT zipCloseFileInZipRaw(zipFile file,\n                                        uLong uncompressed_size,\n                                        uLong crc32);\n\nextern int ZEXPORT zipCloseFileInZipRaw64(zipFile file,\n                                          ZPOS64_T uncompressed_size,\n                                          uLong crc32);\n\n/*\n  Close the current file in the zipfile, for file opened with\n    parameter raw=1 in zipOpenNewFileInZip2\n  uncompressed_size and crc32 are value for the uncompressed size\n*/\n\nextern int ZEXPORT zipClose(zipFile file,\n                            const char* global_comment);\n/*\n  Close the zipfile\n*/\n\n\nextern int ZEXPORT zipRemoveExtraInfoBlock(char* pData, int* dataLen, short sHeader);\n/*\n  zipRemoveExtraInfoBlock -  Added by Mathias Svensson\n\n  Remove extra information block from a extra information data for the local file header or central directory header\n\n  It is needed to remove ZIP64 extra information blocks when before data is written if using RAW mode.\n\n  0x0001 is the signature header for the ZIP64 extra information blocks\n\n  usage.\n                        Remove ZIP64 Extra information from a central director extra field data\n              zipRemoveExtraInfoBlock(pCenDirExtraFieldData, &nCenDirExtraFieldDataLen, 0x0001);\n\n                        Remove ZIP64 Extra information from a Local File Header extra field data\n        zipRemoveExtraInfoBlock(pLocalHeaderExtraFieldData, &nLocalHeaderExtraFieldDataLen, 0x0001);\n*/\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif /* _zip64_H */\n"
  },
  {
    "path": "Telegram/build/build.bat",
    "content": "@echo OFF\r\nsetlocal enabledelayedexpansion\r\nset \"FullScriptPath=%~dp0\"\r\nset \"FullExecPath=%cd%\"\r\n\r\nif not exist \"%FullScriptPath%..\\..\\..\\DesktopPrivate\" (\r\n  echo.\r\n  echo This script is for building the production version of Telegram Desktop.\r\n  echo.\r\n  echo For building custom versions please visit the build instructions page at:\r\n  echo https://github.com/telegramdesktop/tdesktop/#build-instructions\r\n  exit /b\r\n)\r\n\r\nFOR /F \"tokens=1* delims= \" %%i in (%FullScriptPath%target) do set \"BuildTarget=%%i\"\r\n\r\nset \"Build64=0\"\r\nset \"BuildARM=0\"\r\nset \"BuildUWP=0\"\r\nif \"%BuildTarget%\" equ \"win64\" (\r\n  set \"Build64=1\"\r\n) else if \"%BuildTarget%\" equ \"winarm\" (\r\n  set \"BuildARM=1\"\r\n) else if \"%BuildTarget%\" equ \"uwp\" (\r\n  set \"BuildUWP=1\"\r\n) else if \"%BuildTarget%\" equ \"uwp64\" (\r\n  set \"Build64=1\"\r\n  set \"BuildUWP=1\"\r\n) else if \"%BuildTarget%\" equ \"uwparm\" (\r\n  set \"BuildARM=1\"\r\n  set \"BuildUWP=1\"\r\n)\r\n\r\nif %Build64% neq 0 (\r\n  if \"%Platform%\" neq \"x64\" (\r\n    echo Bad environment. Make sure to run from 'x64 Native Tools Command Prompt for VS 2022'.\r\n    exit /b\r\n  ) else if \"%VSCMD_ARG_HOST_ARCH%\" neq \"x64\" (\r\n    echo Bad environment. Make sure to run from 'x64 Native Tools Command Prompt for VS 2022'.\r\n    exit /b\r\n  ) else if \"%VSCMD_ARG_TGT_ARCH%\" neq \"x64\" (\r\n    echo Bad environment. Make sure to run from 'x64 Native Tools Command Prompt for VS 2022'.\r\n    exit /b\r\n  )\r\n) else if %BuildARM% neq 0 (\r\n  if \"%Platform%\" neq \"arm64\" (\r\n    echo Bad environment. Make sure to run from 'ARM64 Native Tools Command Prompt for VS 2022'.\r\n    exit /b\r\n  ) else if \"%VSCMD_ARG_HOST_ARCH%\" neq \"arm64\" (\r\n    echo Bad environment. Make sure to run from 'ARM64 Native Tools Command Prompt for VS 2022'.\r\n    exit /b\r\n  ) else if \"%VSCMD_ARG_TGT_ARCH%\" neq \"arm64\" (\r\n    echo Bad environment. Make sure to run from 'ARM64 Native Tools Command Prompt for VS 2022'.\r\n    exit /b\r\n  )\r\n) else (\r\n  if \"%Platform%\" neq \"x86\" (\r\n    echo Bad environment. Make sure to run from 'x86 Native Tools Command Prompt for VS 2022'.\r\n    exit /b\r\n  ) else if \"%VSCMD_ARG_HOST_ARCH%\" neq \"x86\" (\r\n    echo Bad environment. Make sure to run from 'x86 Native Tools Command Prompt for VS 2022'.\r\n    exit /b\r\n  ) else if \"%VSCMD_ARG_TGT_ARCH%\" neq \"x86\" (\r\n    echo Bad environment. Make sure to run from 'x86 Native Tools Command Prompt for VS 2022'.\r\n    exit /b\r\n  )\r\n)\r\n\r\nFOR /F \"tokens=1,2* delims= \" %%i in (%FullScriptPath%version) do set \"%%i=%%j\"\r\n\r\nset \"VersionForPacker=%AppVersion%\"\r\nif %AlphaVersion% neq 0 (\r\n  set \"AppVersion=%AlphaVersion%\"\r\n  set \"AppVersionStrFull=%AppVersionStr%_%AlphaVersion%\"\r\n  set \"AlphaBetaParam=-alpha %AlphaVersion%\"\r\n  set \"AlphaKeyFile=talpha_%AlphaVersion%_key\"\r\n) else (\r\n  if %BetaChannel% neq 0 (\r\n    set \"AlphaBetaParam=-beta\"\r\n    set \"AppVersionStrFull=%AppVersionStr%.beta\"\r\n  ) else (\r\n    set \"AlphaBetaParam=\"\r\n    set \"AppVersionStrFull=%AppVersionStr%\"\r\n  )\r\n)\r\n\r\necho.\r\nif %BuildUWP% neq 0 (\r\n  if %Build64% neq 0 (\r\n    echo Building version %AppVersionStrFull% for UWP 64 bit..\r\n  ) else if %BuildARM% neq 0 (\r\n    echo Building version %AppVersionStrFull% for UWP ARM..\r\n  ) else (\r\n    echo Building version %AppVersionStrFull% for UWP..\r\n  )\r\n) else (\r\n  if %Build64% neq 0 (\r\n    echo Building version %AppVersionStrFull% for Windows 64 bit..\r\n  ) else if %BuildARM% neq 0 (\r\n    echo Building version %AppVersionStrFull% for Windows on ARM..\r\n  ) else (\r\n    echo Building version %AppVersionStrFull% for Windows..\r\n  )\r\n)\r\necho.\r\n\r\nset \"HomePath=%FullScriptPath%..\"\r\nset \"ResourcesPath=%HomePath%\\Resources\"\r\nset \"SolutionPath=%HomePath%\\..\\out\"\r\nif %Build64% neq 0 (\r\n  set \"UpdateFile=tx64upd%AppVersion%\"\r\n  set \"SetupFile=tsetup-x64.%AppVersionStrFull%.exe\"\r\n  set \"PortableFile=tportable-x64.%AppVersionStrFull%.zip\"\r\n  set \"DumpSymsPath=%SolutionPath%\\..\\..\\Libraries\\win64\\breakpad\\src\\tools\\windows\\dump_syms\\Release\\dump_syms.exe\"\r\n) else if %BuildARM% neq 0 (\r\n  set \"UpdateFile=tarm64upd%AppVersion%\"\r\n  set \"SetupFile=tsetup-arm64.%AppVersionStrFull%.exe\"\r\n  set \"PortableFile=tportable-arm64.%AppVersionStrFull%.zip\"\r\n  set \"DumpSymsPath=%SolutionPath%\\..\\..\\Libraries\\breakpad\\src\\tools\\windows\\dump_syms\\Release\\dump_syms.exe\"\r\n) else (\r\n  set \"UpdateFile=tupdate%AppVersion%\"\r\n  set \"SetupFile=tsetup.%AppVersionStrFull%.exe\"\r\n  set \"PortableFile=tportable.%AppVersionStrFull%.zip\"\r\n  set \"DumpSymsPath=%SolutionPath%\\..\\..\\Libraries\\breakpad\\src\\tools\\windows\\dump_syms\\Release\\dump_syms.exe\"\r\n)\r\nset \"ReleasePath=%SolutionPath%\\Release\"\r\nset \"DeployPath=%ReleasePath%\\deploy\\%AppVersionStrMajor%\\%AppVersionStrFull%\"\r\nset \"SignPath=%HomePath%\\..\\..\\DesktopPrivate\\Sign.bat\"\r\nset \"BinaryName=Telegram\"\r\nset \"DropboxSymbolsPath=Y:\\Telegram\\symbols\"\r\nset \"DropboxSymbolsPathFallback=%HomePath%\\..\\..\\Dropbox\\Telegram\\symbols\"\r\nset \"FinalReleasePath=Z:\\Projects\\backup\\tdesktop\"\r\nset \"FinalReleasePathFallback=%HomePath%\\..\\..\\Projects\\backup\\tdesktop\"\r\n\r\nif not exist %DropboxSymbolsPath% (\r\n  if exist %DropboxSymbolsPathFallback% (\r\n    set \"DropboxSymbolsPath=%DropboxSymbolsPathFallback%\"\r\n  ) else (\r\n    echo Dropbox path %DropboxSymbolsPath% not found!\r\n    exit /b 1\r\n  )\r\n)\r\n\r\nif not exist %FinalReleasePath% (\r\n  if exist %FinalReleasePathFallback% (\r\n    set \"FinalReleasePath=%FinalReleasePathFallback%\"\r\n  ) else (\r\n    echo Release path %FinalReleasePath% not found!\r\n    exit /b 1\r\n  )\r\n)\r\n\r\nif %BuildUWP% neq 0 (\r\n  if exist %ReleasePath%\\AppX\\ (\r\n    echo Result folder out\\Release\\AppX already exists!\r\n    exit /b 1\r\n  )\r\n)\r\nif %AlphaVersion% neq 0 (\r\n  if exist %DeployPath%\\ (\r\n    echo Deploy folder for version %AppVersionStr% already exists!\r\n    exit /b 1\r\n  )\r\n  if exist %ReleasePath%\\%AlphaKeyFile% (\r\n    echo Alpha version key file for version %AppVersion% already exists!\r\n    exit /b 1\r\n  )\r\n) else (\r\n  if exist %ReleasePath%\\deploy\\%AppVersionStrMajor%\\%AppVersionStr%.alpha\\ (\r\n    echo Deploy folder for version %AppVersionStr%.alpha already exists!\r\n    exit /b 1\r\n  )\r\n  if exist %ReleasePath%\\deploy\\%AppVersionStrMajor%\\%AppVersionStr%.beta\\ (\r\n    echo Deploy folder for version %AppVersionStr%.beta already exists!\r\n    exit /b 1\r\n  )\r\n  if exist %ReleasePath%\\deploy\\%AppVersionStrMajor%\\%AppVersionStr%\\ (\r\n    echo Deploy folder for version %AppVersionStr% already exists!\r\n    exit /b 1\r\n  )\r\n  if exist %ReleasePath%\\tupdate%AppVersion% (\r\n    echo Update file for version %AppVersion% already exists!\r\n    exit /b 1\r\n  )\r\n)\r\n\r\ncd \"%HomePath%\"\r\n\r\ncall configure.bat -DDESKTOP_APP_ENABLE_LTO=ON\r\nif %errorlevel% neq 0 goto error\r\n\r\ncd \"%SolutionPath%\"\r\ncall cmake --build . --config Release --target Telegram\r\nif %errorlevel% neq 0 goto error\r\n\r\necho.\r\necho Version %AppVersionStrFull% build successfull. Preparing..\r\necho.\r\n\r\nif not exist \"%DumpSymsPath%\" (\r\n  echo Utility dump_syms not found!\r\n  exit /b 1\r\n)\r\n\r\necho Dumping debug symbols..\r\ncall \"%DumpSymsPath%\" \"%ReleasePath%\\%BinaryName%.pdb\" > \"%ReleasePath%\\%BinaryName%.sym\"\r\necho Done!\r\n\r\nset \"PATH=%PATH%;C:\\Program Files\\7-Zip;C:\\Program Files (x86)\\Inno Setup 5\"\r\n\r\ncd \"%ReleasePath%\"\r\n\r\n:sign1\r\ncall \"%SignPath%\" \"%BinaryName%.exe\"\r\nif %errorlevel% neq 0 (\r\n  timeout /t 3\r\n  goto sign1\r\n)\r\n\r\nif %BuildUWP% equ 0 (\r\n:sign2\r\n  call \"%SignPath%\" \"Updater.exe\"\r\n  if %errorlevel% neq 0 (\r\n    timeout /t 3\r\n    goto sign2\r\n  )\r\n\r\n  if %AlphaVersion% equ 0 (\r\n    iscc /dMyAppVersion=%AppVersionStrSmall% /dMyAppVersionZero=%AppVersionStr% /dMyAppVersionFull=%AppVersionStrFull% \"/dReleasePath=%ReleasePath%\" \"/dMyBuildTarget=%BuildTarget%\" \"%FullScriptPath%setup.iss\"\r\n    if %errorlevel% neq 0 goto error\r\n    if not exist \"%SetupFile%\" goto error\r\n  )\r\n\r\n  if %BuildARM% neq 0 (\r\n    call Packer.exe -version %VersionForPacker% -path %BinaryName%.exe -path Updater.exe -target %BuildTarget% %AlphaBetaParam%\r\n  ) else (\r\n    call Packer.exe -version %VersionForPacker% -path %BinaryName%.exe -path Updater.exe -path \"modules\\%Platform%\\d3d\\d3dcompiler_47.dll\" -target %BuildTarget% %AlphaBetaParam%\r\n  )\r\n  if %errorlevel% neq 0 goto error\r\n\r\n  if %AlphaVersion% neq 0 (\r\n    if not exist \"%ReleasePath%\\%AlphaKeyFile%\" (\r\n      echo Alpha version key file not found!\r\n      exit /b 1\r\n    )\r\n\r\n    FOR /F \"tokens=1* delims= \" %%i in (%ReleasePath%\\%AlphaKeyFile%) do set \"AlphaSignature=%%i\"\r\n  )\r\n  if %errorlevel% neq 0 goto error\r\n\r\n  if %AlphaVersion% neq 0 (\r\n    set \"UpdateFile=!UpdateFile!_!AlphaSignature!\"\r\n    set \"PortableFile=talpha!AlphaVersion!_!AlphaSignature!.zip\"\r\n  )\r\n) else (\r\n:sign2\r\n  call \"%SignPath%\" \"StartupTask.exe\"\r\n  if %errorlevel% neq 0 (\r\n    timeout /t 3\r\n    goto sign2\r\n  )\r\n)\r\n\r\nfor /f ^\"usebackq^ eol^=^\r\n\r\n^ delims^=^\" %%a in (%ReleasePath%\\%BinaryName%.sym) do (\r\n  set \"SymbolsHashLine=%%a\"\r\n  goto symbolslinedone\r\n)\r\n:symbolslinedone\r\nFOR /F \"tokens=1,2,3,4* delims= \" %%i in (\"%SymbolsHashLine%\") do set \"SymbolsHash=%%l\"\r\n\r\necho Copying %BinaryName%.sym to %DropboxSymbolsPath%\\%BinaryName%.pdb\\%SymbolsHash%\r\nif not exist %DropboxSymbolsPath%\\%BinaryName%.pdb mkdir %DropboxSymbolsPath%\\%BinaryName%.pdb\r\nif not exist %DropboxSymbolsPath%\\%BinaryName%.pdb\\%SymbolsHash% mkdir %DropboxSymbolsPath%\\%BinaryName%.pdb\\%SymbolsHash%\r\nmove \"%ReleasePath%\\%BinaryName%.sym\" %DropboxSymbolsPath%\\%BinaryName%.pdb\\%SymbolsHash%\\\r\necho Done!\r\n\r\nif %BuildUWP% neq 0 (\r\n  cd \"%HomePath%\"\r\n\r\n  if %BuildARM% equ 0 (\r\n    mkdir \"%ReleasePath%\\AppX\\modules\\%Platform%\\d3d\"\r\n  )\r\n  xcopy \"Resources\\uwp\\AppX\\*\" \"%ReleasePath%\\AppX\\\" /E\r\n  set \"ResourcePath=%ReleasePath%\\AppX\\AppxManifest.xml\"\r\n  call :repl \"Argument= (ProcessorArchitecture=)&quot;ARCHITECTURE&quot;/ $1&quot;%Platform%&quot;\" \"Filename=!ResourcePath!\" || goto error\r\n  makepri new /pr Resources\\uwp\\AppX\\ /cf Resources\\uwp\\priconfig.xml /mn %ReleasePath%\\AppX\\AppxManifest.xml /of %ReleasePath%\\AppX\\resources.pri\r\n  if %errorlevel% neq 0 goto error\r\n\r\n  xcopy \"%ReleasePath%\\%BinaryName%.exe\" \"%ReleasePath%\\AppX\\\"\r\n  xcopy \"%ReleasePath%\\StartupTask.exe\" \"%ReleasePath%\\AppX\\\"\r\n  if %BuildARM% equ 0 (\r\n    xcopy \"%ReleasePath%\\modules\\%Platform%\\d3d\\d3dcompiler_47.dll\" \"%ReleasePath%\\AppX\\modules\\%Platform%\\d3d\\\"\r\n  )\r\n\r\n  MakeAppx.exe pack /d \"%ReleasePath%\\AppX\" /l /p ..\\out\\Release\\%BinaryName%.%Platform%.appx\r\n  if %errorlevel% neq 0 goto error\r\n\r\n  if not exist \"%ReleasePath%\\deploy\" mkdir \"%ReleasePath%\\deploy\"\r\n  if not exist \"%ReleasePath%\\deploy\\%AppVersionStrMajor%\" mkdir \"%ReleasePath%\\deploy\\%AppVersionStrMajor%\"\r\n  mkdir \"%DeployPath%\"\r\n\r\n  move \"%ReleasePath%\\%BinaryName%.pdb\" \"%DeployPath%\\\"\r\n  move \"%ReleasePath%\\%BinaryName%.%Platform%.appx\" \"%DeployPath%\\\"\r\n  move \"%ReleasePath%\\%BinaryName%.exe\" \"%DeployPath%\\\"\r\n\r\n  if \"%AlphaBetaParam%\" equ \"\" (\r\n    move \"%ReleasePath%\\AppX\" \"%DeployPath%\\AppX\"\r\n  ) else (\r\n    echo Leaving result in out\\Release\\AppX_arch for now..\r\n  )\r\n) else (\r\n  if not exist \"%ReleasePath%\\deploy\" mkdir \"%ReleasePath%\\deploy\"\r\n  if not exist \"%ReleasePath%\\deploy\\%AppVersionStrMajor%\" mkdir \"%ReleasePath%\\deploy\\%AppVersionStrMajor%\"\r\n  mkdir \"%DeployPath%\\%BinaryName%\\modules\\%Platform%\\d3d\"\r\n  if %errorlevel% neq 0 goto error\r\n\r\n  move \"%ReleasePath%\\%BinaryName%.exe\" \"%DeployPath%\\%BinaryName%\\\"\r\n  xcopy \"%ReleasePath%\\modules\\%Platform%\\d3d\\d3dcompiler_47.dll\" \"%DeployPath%\\%BinaryName%\\modules\\%Platform%\\d3d\\\"\r\n  move \"%ReleasePath%\\Updater.exe\" \"%DeployPath%\\\"\r\n  move \"%ReleasePath%\\%BinaryName%.pdb\" \"%DeployPath%\\\"\r\n  move \"%ReleasePath%\\Updater.pdb\" \"%DeployPath%\\\"\r\n  if %AlphaVersion% equ 0 (\r\n    move \"%ReleasePath%\\%SetupFile%\" \"%DeployPath%\\\"\r\n  ) else (\r\n    move \"%ReleasePath%\\%AlphaKeyFile%\" \"%DeployPath%\\\"\r\n  )\r\n  move \"%ReleasePath%\\%UpdateFile%\" \"%DeployPath%\\\"\r\n  if %errorlevel% neq 0 goto error\r\n\r\n  cd \"%DeployPath%\"\r\n  7z a -mx9 %PortableFile% %BinaryName%\\\r\n  if %errorlevel% neq 0 goto error\r\n\r\n  move \"%DeployPath%\\%BinaryName%\\%BinaryName%.exe\" \"%DeployPath%\\\"\r\n  rmdir \"%DeployPath%\\%BinaryName%\"\r\n  if %errorlevel% neq 0 goto error\r\n)\r\n\r\nif %Build64% neq 0 (\r\n  set \"FinalDeployPath=%FinalReleasePath%\\%AppVersionStrMajor%\\%AppVersionStrFull%\\tx64\"\r\n) else if %BuildARM% neq 0 (\r\n  set \"FinalDeployPath=%FinalReleasePath%\\%AppVersionStrMajor%\\%AppVersionStrFull%\\tarm64\"\r\n) else (\r\n  set \"FinalDeployPath=%FinalReleasePath%\\%AppVersionStrMajor%\\%AppVersionStrFull%\\tsetup\"\r\n)\r\n\r\nif %BuildUWP% equ 0 (\r\n  echo.\r\n  echo Version %AppVersionStrFull% is ready for deploy!\r\n  echo.\r\n\r\n  if not exist \"%DeployPath%\\%UpdateFile%\" goto error\r\n  if not exist \"%DeployPath%\\%PortableFile%\" goto error\r\n  if %AlphaVersion% equ 0 (\r\n    if not exist \"%DeployPath%\\%SetupFile%\" goto error\r\n  )\r\n  if not exist \"%DeployPath%\\%BinaryName%.pdb\" goto error\r\n  if not exist \"%DeployPath%\\Updater.exe\" goto error\r\n  if not exist \"%DeployPath%\\Updater.pdb\" goto error\r\n  md \"%FinalDeployPath%\"\r\n\r\n  xcopy \"%DeployPath%\\%UpdateFile%\" \"%FinalDeployPath%\\\" /Y\r\n  xcopy \"%DeployPath%\\%PortableFile%\" \"%FinalDeployPath%\\\" /Y\r\n  if %AlphaVersion% equ 0 (\r\n    xcopy \"%DeployPath%\\%SetupFile%\" \"%FinalDeployPath%\\\" /Y\r\n  ) else (\r\n    xcopy \"%DeployPath%\\%AlphaKeyFile%\" \"%FinalDeployPath%\\\" /Y\r\n  )\r\n)\r\n\r\necho Version %AppVersionStrFull% is ready!\r\n\r\ncd \"%FullExecPath%\"\r\nexit /b\r\n\r\n:error\r\n(\r\n  set ErrorCode=%errorlevel%\r\n  if !ErrorCode! neq 0 (\r\n    echo Error !ErrorCode!\r\n  ) else (\r\n    echo Error 666\r\n    set ErrorCode=666\r\n  )\r\n  cd \"%FullExecPath%\"\r\n  exit /b !ErrorCode!\r\n)\r\n\r\n:repl\r\n(\r\n  set %1\r\n  set %2\r\n  set \"TempFilename=!Filename!__tmp__\"\r\n  cscript //Nologo \"%FullScriptPath%replace.vbs\" \"Replace\" \"!Argument!\" < \"!Filename!\" > \"!TempFilename!\" || goto :repl_finish\r\n  xcopy /Y !TempFilename! !Filename! >NUL || goto :repl_finish\r\n  goto :repl_finish\r\n)\r\n\r\n:repl_finish\r\n(\r\n  set ErrorCode=%errorlevel%\r\n  if !ErrorCode! neq 0 (\r\n    echo Replace error !ErrorCode!\r\n    echo While replacing \"%Replace%\"\r\n    echo In file \"%Filename%\"\r\n  )\r\n  del %TempFilename%\r\n  exit /b !ErrorCode!\r\n)\r\n"
  },
  {
    "path": "Telegram/build/build.sh",
    "content": "set -e\nFullExecPath=$PWD\npushd `dirname $0` > /dev/null\nFullScriptPath=`pwd`\npopd > /dev/null\n\narg1=\"$1\"\narg2=\"$2\"\narg3=\"$3\"\n\nif [ ! -d \"$FullScriptPath/../../../DesktopPrivate\" ]; then\n  echo \"\"\n  echo \"This script is for building the production version of Telegram Desktop.\"\n  echo \"\"\n  echo \"For building custom versions please visit the build instructions page at:\"\n  echo \"https://github.com/telegramdesktop/tdesktop/#build-instructions\"\n  exit\nfi\n\nError () {\n  cd $FullExecPath\n  echo \"$1\"\n  exit 1\n}\n\nif [ ! -f \"$FullScriptPath/target\" ]; then\n  Error \"Build target not found!\"\nfi\n\nwhile IFS='' read -r line || [[ -n \"$line\" ]]; do\n  BuildTarget=\"$line\"\ndone < \"$FullScriptPath/target\"\n\nwhile IFS='' read -r line || [[ -n \"$line\" ]]; do\n  set $line\n  eval $1=\"$2\"\ndone < \"$FullScriptPath/version\"\n\nVersionForPacker=\"$AppVersion\"\nif [ \"$AlphaVersion\" != \"0\" ]; then\n  AppVersion=\"$AlphaVersion\"\n  AppVersionStrFull=\"${AppVersionStr}_${AlphaVersion}\"\n  AlphaBetaParam=\"-alpha $AlphaVersion\"\n  AlphaKeyFile=\"talpha_${AppVersion}_key\"\nelif [ \"$BetaChannel\" == \"0\" ]; then\n  AppVersionStrFull=\"$AppVersionStr\"\n  AlphaBetaParam=''\nelse\n  AppVersionStrFull=\"$AppVersionStr.beta\"\n  AlphaBetaParam='-beta'\nfi\n\necho \"\"\nHomePath=\"$FullScriptPath/..\"\nif [ \"$BuildTarget\" == \"linux\" ]; then\n  echo \"Building version $AppVersionStrFull for Linux 64bit..\"\n  UpdateFile=\"tlinuxupd$AppVersion\"\n  SetupFile=\"tsetup.$AppVersionStrFull.tar.xz\"\n  ProjectPath=\"$HomePath/../out\"\n  ReleasePath=\"$ProjectPath/Release\"\n  BinaryName=\"Telegram\"\nelif [ \"$BuildTarget\" == \"mac\" ] ; then\n  if [ \"$arg1\" == \"x86_64\" ] || [ \"$arg1\" == \"arm64\" ]; then\n    echo \"Building version $AppVersionStrFull for macOS 10.13+ ($arg1)..\"\n    MacArch=\"$arg1\"\n    if [ \"$arg2\" == \"request_uuid\" ] && [ \"$arg3\" != \"\" ]; then\n      NotarizeRequestId=\"$arg3\"\n    fi\n  else\n    echo \"Building version $AppVersionStrFull for macOS 10.13+..\"\n    if [ \"$arg2\" != \"\" ]; then\n      if [ \"$arg1\" == \"request_uuid_x86_64\" ]; then\n        NotarizeRequestIdAMD64=\"$arg2\"\n      elif [ \"$arg1\" == \"request_uuid_arm64\" ]; then\n        NotarizeRequestIdARM64=\"$arg2\"\n      elif [ \"$arg1\" == \"request_uuid\" ]; then\n        NotarizeRequestId=\"$arg2\"\n      fi\n    fi\n  fi\n\n  if [ \"$AC_USERNAME\" == \"\" ]; then\n    Error \"AC_USERNAME not found!\"\n  fi\n  UpdateFileAMD64=\"tmacupd$AppVersion\"\n  UpdateFileARM64=\"tarmacupd$AppVersion\"\n  if [ \"$MacArch\" == \"arm64\" ]; then\n    UpdateFile=\"$UpdateFileARM64\"\n  elif [ \"$MacArch\" == \"x86_64\" ]; then\n    UpdateFile=\"$UpdateFileAMD64\"\n  fi\n  ProjectPath=\"$HomePath/../out\"\n  ReleasePath=\"$ProjectPath/Release\"\n  BinaryName=\"Telegram\"\n  if [ \"$MacArch\" != \"\" ]; then\n    BundleName=\"$BinaryName.$MacArch.app\"\n    SetupFile=\"tsetup.$MacArch.$AppVersionStrFull.dmg\"\n  else\n    BundleName=\"$BinaryName.app\"\n    SetupFile=\"tsetup.$AppVersionStrFull.dmg\"\n  fi\nelif [ \"$BuildTarget\" == \"macstore\" ]; then\n  if [ \"$AlphaVersion\" != \"0\" ]; then\n    Error \"Can't build macstore alpha version!\"\n  fi\n\n  echo \"Building version $AppVersionStrFull for Mac App Store..\"\n  ProjectPath=\"$HomePath/../out\"\n  ReleasePath=\"$ProjectPath/Release\"\n  BinaryName=\"Telegram Lite\"\n  BundleName=\"$BinaryName.app\"\nelse\n  Error \"Invalid target!\"\nfi\n\nif [ \"$AlphaVersion\" != \"0\" ]; then\n  if [ -d \"$ReleasePath/deploy/$AppVersionStrMajor/$AppVersionStrFull\" ]; then\n    Error \"Deploy folder for version $AppVersionStrFull already exists!\"\n  fi\nelse\n  if [ -d \"$ReleasePath/deploy/$AppVersionStrMajor/$AppVersionStr.alpha\" ]; then\n    Error \"Deploy folder for version $AppVersionStr.alpha already exists!\"\n  fi\n\n  if [ -d \"$ReleasePath/deploy/$AppVersionStrMajor/$AppVersionStr.beta\" ]; then\n    Error \"Deploy folder for version $AppVersionStr.beta already exists!\"\n  fi\n\n  if [ -d \"$ReleasePath/deploy/$AppVersionStrMajor/$AppVersionStr\" ]; then\n    Error \"Deploy folder for version $AppVersionStr already exists!\"\n  fi\n\n  if [ -f \"$ReleasePath/$UpdateFile\" ]; then\n    Error \"Update file for version $AppVersion already exists!\"\n  fi\nfi\n\nDeployPath=\"$ReleasePath/deploy/$AppVersionStrMajor/$AppVersionStrFull\"\n\nif [ \"$BuildTarget\" == \"linux\" ]; then\n\n  DropboxSymbolsPath=\"/media/psf/Dropbox/Telegram/symbols\"\n  if [ ! -d \"$DropboxSymbolsPath\" ]; then\n    DropboxSymbolsPath=\"/mnt/c/Telegram/Dropbox/Telegram/symbols\"\n    if [ ! -d \"$DropboxSymbolsPath\" ]; then\n      Error \"Dropbox path not found!\"\n    fi\n  fi\n\n  BackupPath=\"/media/psf/backup/tdesktop/$AppVersionStrMajor/$AppVersionStrFull/t$BuildTarget\"\n  if [ ! -d \"/media/psf/backup/tdesktop\" ]; then\n    BackupPath=\"/mnt/c/Telegram/Projects/backup/tdesktop/$AppVersionStrMajor/$AppVersionStrFull/t$BuildTarget\"\n    if [ ! -d \"/mnt/c/Telegram/Projects/backup/tdesktop\" ]; then\n      Error \"Backup folder not found!\"\n    fi\n  fi\n\n  ./build/docker/centos_env/run.sh /usr/src/tdesktop/Telegram/build/docker/build.sh\n\n  echo \"Copying from docker result folder.\"\n  cp \"$ReleasePath/root/$BinaryName\" \"$ReleasePath/$BinaryName\"\n  cp \"$ReleasePath/root/Updater\" \"$ReleasePath/Updater\"\n  cp \"$ReleasePath/root/Packer\" \"$ReleasePath/Packer\"\n\n  echo \"Dumping debug symbols..\"\n  \"$ReleasePath/dump_syms\" \"$ReleasePath/$BinaryName\" > \"$ReleasePath/$BinaryName.sym\"\n  echo \"Done!\"\n\n  echo \"Stripping the executable..\"\n  strip -s \"$ReleasePath/$BinaryName\"\n  echo \"Done!\"\n\n  echo \"Preparing version $AppVersionStrFull, executing Packer..\"\n  cd \"$ReleasePath\"\n  \"./Packer\" -path \"$BinaryName\" -path Updater -version $VersionForPacker $AlphaBetaParam\n  echo \"Packer done!\"\n\n  if [ \"$AlphaVersion\" != \"0\" ]; then\n    if [ ! -f \"$ReleasePath/$AlphaKeyFile\" ]; then\n      Error \"Alpha version key file not found!\"\n    fi\n\n    while IFS='' read -r line || [[ -n \"$line\" ]]; do\n      AlphaSignature=\"$line\"\n    done < \"$ReleasePath/$AlphaKeyFile\"\n\n    UpdateFile=\"${UpdateFile}_${AlphaSignature}\"\n    SetupFile=\"talpha${AlphaVersion}_${AlphaSignature}.tar.xz\"\n  fi\n\n  SymbolsHash=`head -n 1 \"$ReleasePath/$BinaryName.sym\" | awk -F \" \" 'END {print $4}'`\n  echo \"Copying $BinaryName.sym to $DropboxSymbolsPath/$BinaryName/$SymbolsHash\"\n  mkdir -p \"$DropboxSymbolsPath/$BinaryName/$SymbolsHash\"\n  cp \"$ReleasePath/$BinaryName.sym\" \"$DropboxSymbolsPath/$BinaryName/$SymbolsHash/\"\n  echo \"Done!\"\n\n  if [ ! -d \"$ReleasePath/deploy\" ]; then\n    mkdir \"$ReleasePath/deploy\"\n  fi\n\n  if [ ! -d \"$ReleasePath/deploy/$AppVersionStrMajor\" ]; then\n    mkdir \"$ReleasePath/deploy/$AppVersionStrMajor\"\n  fi\n\n  echo \"Copying $BinaryName, Updater and $UpdateFile to deploy/$AppVersionStrMajor/$AppVersionStrFull..\";\n  mkdir \"$DeployPath\"\n  mkdir \"$DeployPath/$BinaryName\"\n  mv \"$ReleasePath/$BinaryName\" \"$DeployPath/$BinaryName/\"\n  mv \"$ReleasePath/Updater\" \"$DeployPath/$BinaryName/\"\n  mv \"$ReleasePath/$UpdateFile\" \"$DeployPath/\"\n  if [ \"$AlphaVersion\" != \"0\" ]; then\n    mv \"$ReleasePath/$AlphaKeyFile\" \"$DeployPath/\"\n  fi\n  cd \"$DeployPath\"\n  tar -cJvf \"$SetupFile\" \"$BinaryName/\"\n\n  mkdir -p $BackupPath\n  cp \"$SetupFile\" \"$BackupPath/\"\n  cp \"$UpdateFile\" \"$BackupPath/\"\n  if [ \"$AlphaVersion\" != \"0\" ]; then\n    cp -v \"$AlphaKeyFile\" \"$BackupPath/\"\n  fi\nfi\n\nif [ \"$BuildTarget\" == \"mac\" ] || [ \"$BuildTarget\" == \"macstore\" ]; then\n\n  DropboxSymbolsPath=\"$HOME/Dropbox/Telegram/symbols\"\n  if [ ! -d \"$DropboxSymbolsPath\" ]; then\n    Error \"Dropbox path not found!\"\n  fi\n\n  BackupPath=\"$HOME/Projects/backup/tdesktop/$AppVersionStrMajor/$AppVersionStrFull\"\n  if [ ! -d \"$HOME/Projects/backup/tdesktop\" ]; then\n    Error \"Backup path not found!\"\n  fi\n\n  if [ \"$MacArch\" != \"\" ]; then\n    cd $ReleasePath\n\n    echo \"Preparing single $MacArch .app..\"\n    rm -rf $BundleName\n    cp -R $BinaryName.app $BundleName\n    lipo -thin $MacArch $BinaryName.app/Contents/MacOS/$BinaryName -output $BundleName/Contents/MacOS/$BinaryName\n    lipo -thin $MacArch $BinaryName.app/Contents/Frameworks/Updater -output $BundleName/Contents/Frameworks/Updater\n    lipo -thin $MacArch $BinaryName.app/Contents/Helpers/crashpad_handler -output $BundleName/Contents/Helpers/crashpad_handler\n    echo \"Done!\"\n  elif [ \"$NotarizeRequestId\" == \"\" ]; then\n    if [ \"$NotarizeRequestIdAMD64\" == \"\" ] && [ \"$NotarizeRequestIdARM64\" == \"\" ]; then\n      if [ -f \"$ReleasePath/$BinaryName.app/Contents/Info.plist\" ]; then\n        rm \"$ReleasePath/$BinaryName.app/Contents/Info.plist\"\n      fi\n      if [ -f \"$ProjectPath/Telegram/CMakeFiles/Telegram.dir/Info.plist\" ]; then\n        rm \"$ProjectPath/Telegram/CMakeFiles/Telegram.dir/Info.plist\"\n      fi\n      rm -rf \"$ReleasePath/$BinaryName.app/Contents/_CodeSignature\"\n      rm -rf \"$ReleasePath/Updater\"\n\n      ./configure.sh -D DESKTOP_APP_MAC_ARCH=\"arm64;x86_64\" -DDESKTOP_APP_ENABLE_LTO=ON\n\n      cd $ProjectPath\n      cmake --build . --config Release --target Telegram\n    fi\n\n    if [ ! -d \"$ReleasePath/$BinaryName.app\" ]; then\n      Error \"$BinaryName.app not found!\"\n    fi\n\n    cd $FullExecPath\n\n    if [ \"$BuildTarget\" == \"mac\" ]; then\n      if [ \"$NotarizeRequestIdAMD64\" == \"\" ]; then\n        echo \"Preparing single arm64 update..\"\n        ./$0 arm64 request_uuid $NotarizeRequestIdARM64\n      fi\n\n      echo \"Preparing single x86_64 update..\"\n      ./$0 x86_64 request_uuid $NotarizeRequestIdAMD64\n\n      echo \"Done.\"\n    fi\n    cd $ReleasePath\n  fi\n  if [ \"$NotarizeRequestId\" == \"\" ]; then\n    if [ \"$BuildTarget\" == \"mac\" ]; then\n      if [ ! -f \"$ReleasePath/$BundleName/Contents/Frameworks/Updater\" ]; then\n        Error \"Updater not found!\"\n      fi\n      if [ ! -f \"$ReleasePath/$BundleName/Contents/Helpers/crashpad_handler\" ]; then\n        Error \"crashpad_handler not found!\"\n      fi\n    fi\n    if [ \"$BuildTarget\" == \"macstore\" ]; then\n      if [ ! -d \"$ReleasePath/$BundleName/Contents/Frameworks/Breakpad.framework\" ]; then\n        Error \"Breakpad.framework not found!\"\n      fi\n    fi\n\n    if [ \"$MacArch\" == \"\" ]; then\n      echo \"Dumping debug symbols x86_64 from universal..\"\n      \"$HomePath/../../Libraries/breakpad/src/tools/mac/dump_syms/build/Release/dump_syms\" \"-a\" \"x86_64\" \"$ReleasePath/$BinaryName.app/Contents/MacOS/$BinaryName\" > \"$ReleasePath/$BinaryName.x86_64.sym\" 2>/dev/null\n      echo \"Done!\"\n\n      SymbolsHash=`head -n 1 \"$ReleasePath/$BinaryName.x86_64.sym\" | awk -F \" \" 'END {print $4}'`\n      echo \"Copying $BinaryName.x86_64.sym to $DropboxSymbolsPath/$BinaryName/$SymbolsHash\"\n      mkdir -p \"$DropboxSymbolsPath/$BinaryName/$SymbolsHash\"\n      cp \"$ReleasePath/$BinaryName.x86_64.sym\" \"$DropboxSymbolsPath/$BinaryName/$SymbolsHash/$BinaryName.sym\"\n      echo \"Done!\"\n\n      echo \"Dumping debug symbols arm64 from universal..\"\n      \"$HomePath/../../Libraries/breakpad/src/tools/mac/dump_syms/build/Release/dump_syms\" \"-a\" \"arm64\" \"$ReleasePath/$BinaryName.app/Contents/MacOS/$BinaryName\" > \"$ReleasePath/$BinaryName.arm64.sym\" 2>/dev/null\n      echo \"Done!\"\n\n      SymbolsHash=`head -n 1 \"$ReleasePath/$BinaryName.arm64.sym\" | awk -F \" \" 'END {print $4}'`\n      echo \"Copying $BinaryName.arm64.sym to $DropboxSymbolsPath/$BinaryName/$SymbolsHash\"\n      mkdir -p \"$DropboxSymbolsPath/$BinaryName/$SymbolsHash\"\n      cp \"$ReleasePath/$BinaryName.arm64.sym\" \"$DropboxSymbolsPath/$BinaryName/$SymbolsHash/$BinaryName.sym\"\n      echo \"Done!\"\n    fi\n\n    echo \"Stripping the executable..\"\n    strip \"$ReleasePath/$BundleName/Contents/MacOS/$BinaryName\"\n    if [ \"$BuildTarget\" == \"mac\" ]; then\n      strip \"$ReleasePath/$BundleName/Contents/Frameworks/Updater\"\n      strip \"$ReleasePath/$BundleName/Contents/Helpers/crashpad_handler\"\n    fi\n    echo \"Done!\"\n\n    echo \"Signing the application..\"\n    if [ \"$BuildTarget\" == \"mac\" ]; then\n      codesign --force --deep --timestamp --options runtime --sign \"Developer ID Application: Telegram FZ-LLC (C67CF9S4VU)\" \"$ReleasePath/$BundleName\" --entitlements \"$HomePath/Telegram/Telegram.entitlements\"\n    elif [ \"$BuildTarget\" == \"macstore\" ]; then\n      codesign --force --timestamp --options runtime --sign \"3rd Party Mac Developer Application: Telegram FZ-LLC (C67CF9S4VU)\" \"$ReleasePath/$BundleName/Contents/Frameworks/Breakpad.framework/Versions/A/Resources/breakpadUtilities.dylib\" --entitlements \"$HomePath/Telegram/Breakpad.entitlements\"\n      codesign --force --deep --timestamp --options runtime --sign \"3rd Party Mac Developer Application: Telegram FZ-LLC (C67CF9S4VU)\" \"$ReleasePath/$BundleName\" --entitlements \"$HomePath/Telegram/Telegram Lite.entitlements\"\n      echo \"Making an installer..\"\n      productbuild --sign \"3rd Party Mac Developer Installer: Telegram FZ-LLC (C67CF9S4VU)\" --component \"$ReleasePath/$BundleName\" /Applications \"$ReleasePath/$BinaryName.pkg\"\n    fi\n    echo \"Done!\"\n\n    if [ ! -f \"$ReleasePath/$BundleName/Contents/Resources/Icon.icns\" ]; then\n      Error \"Icon.icns not found in Resources!\"\n    fi\n\n    if [ ! -f \"$ReleasePath/$BundleName/Contents/MacOS/$BinaryName\" ]; then\n      Error \"$BinaryName not found in MacOS!\"\n    fi\n\n    if [ ! -d \"$ReleasePath/$BundleName/Contents/_CodeSignature\" ]; then\n      Error \"$BinaryName signature not found!\"\n    fi\n\n    if [ \"$BuildTarget\" == \"macstore\" ]; then\n      if [ ! -f \"$ReleasePath/$BinaryName.pkg\" ]; then\n        Error \"$BinaryName.pkg not found!\"\n      fi\n    fi\n  fi\n\n  if [ \"$BuildTarget\" == \"mac\" ]; then\n    cd \"$ReleasePath\"\n\n    if [ \"$NotarizeRequestId\" == \"\" ]; then\n      if [ \"$AlphaVersion\" == \"0\" ]; then\n        cp -f tsetup_template.dmg tsetup.temp.dmg\n        TempDiskPath=`hdiutil attach -nobrowse -noautoopenrw -readwrite tsetup.temp.dmg | awk -F \"\\t\" 'END {print $3}'`\n        cp -R \"./$BundleName\" \"$TempDiskPath/\"\n        bless --folder \"$TempDiskPath/\"\n        hdiutil detach \"$TempDiskPath\"\n        hdiutil convert tsetup.temp.dmg -format UDBZ -ov -o \"$SetupFile\"\n        rm tsetup.temp.dmg\n      fi\n    fi\n\n    if [ \"$AlphaVersion\" != \"0\" ]; then\n      cd $ReleasePath\n      \"./Packer\" -path \"$BundleName\" -target \"$BuildTarget\" -version $VersionForPacker $AlphaBetaParam -alphakey\n\n      if [ ! -f \"$AlphaKeyFile\" ]; then\n        Error \"Alpha version key file not found!\"\n      fi\n\n      while IFS='' read -r line || [[ -n \"$line\" ]]; do\n        AlphaSignature=\"$line\"\n      done < \"$ReleasePath/$AlphaKeyFile\"\n\n      UpdateFile=\"${UpdateFile}_${AlphaSignature}\"\n      UpdateFileAMD64=\"${UpdateFileAMD64}_${AlphaSignature}\"\n      UpdateFileARM64=\"${UpdateFileARM64}_${AlphaSignature}\"\n      if [ \"$MacArch\" != \"\" ]; then\n        SetupFile=\"talpha${AlphaVersion}_${MacArch}_${AlphaSignature}.zip\"\n      else\n        SetupFile=\"talpha${AlphaVersion}_${AlphaSignature}.zip\"\n      fi\n\n      if [ \"$NotarizeRequestId\" == \"\" ]; then\n        rm -rf \"$ReleasePath/AlphaTemp\"\n        mkdir \"$ReleasePath/AlphaTemp\"\n        mkdir \"$ReleasePath/AlphaTemp/$BinaryName\"\n        cp -r \"$ReleasePath/$BundleName\" \"$ReleasePath/AlphaTemp/$BinaryName/\"\n        cd \"$ReleasePath/AlphaTemp\"\n        zip -r \"$SetupFile\" \"$BinaryName\"\n        mv \"$SetupFile\" \"$ReleasePath/\"\n        cd \"$ReleasePath\"\n      fi\n    fi\n    echo \"Beginning notarization process.\"\n    xcrun notarytool submit \"$SetupFile\" --keychain-profile \"preston\" --wait\n    xcrun stapler staple \"$ReleasePath/$BundleName\"\n\n    if [ \"$MacArch\" != \"\" ]; then\n      rm \"$ReleasePath/$SetupFile\"\n      echo \"Setup file $SetupFile removed.\"\n    elif [ \"$AlphaVersion\" != \"0\" ]; then\n      rm -rf \"$ReleasePath/AlphaTemp\"\n      mkdir \"$ReleasePath/AlphaTemp\"\n      mkdir \"$ReleasePath/AlphaTemp/$BinaryName\"\n      cp -r \"$ReleasePath/$BinaryName.app\" \"$ReleasePath/AlphaTemp/$BinaryName/\"\n      cd \"$ReleasePath/AlphaTemp\"\n      zip -r \"$SetupFile\" \"$BinaryName\"\n      mv \"$SetupFile\" \"$ReleasePath/\"\n      cd \"$ReleasePath\"\n      echo \"Alpha archive re-created.\"\n    else\n      xcrun stapler staple \"$ReleasePath/$SetupFile\"\n    fi\n\n    if [ \"$MacArch\" != \"\" ]; then\n      UpdatePackPath=\"$ReleasePath/update_pack_${MacArch}\"\n      rm -rf \"$UpdatePackPath\"\n      mkdir \"$UpdatePackPath\"\n      mv \"$ReleasePath/$BundleName\" \"$UpdatePackPath/$BinaryName.app\"\n      cp \"$ReleasePath/Packer\" \"$UpdatePackPath/\"\n      cd \"$UpdatePackPath\"\n      \"./Packer\" -path \"$BinaryName.app\" -target \"$BuildTarget\" -version $VersionForPacker -arch $MacArch $AlphaBetaParam\n      echo \"Packer done!\"\n      mv \"$UpdateFile\" \"$ReleasePath/\"\n      cd \"$ReleasePath\"\n      rm -rf \"$UpdatePackPath\"\n      exit\n    fi\n  fi\n\n  if [ ! -d \"$ReleasePath/deploy\" ]; then\n    mkdir \"$ReleasePath/deploy\"\n  fi\n\n  if [ ! -d \"$ReleasePath/deploy/$AppVersionStrMajor\" ]; then\n    mkdir \"$ReleasePath/deploy/$AppVersionStrMajor\"\n  fi\n\n  if [ \"$BuildTarget\" == \"mac\" ]; then\n    echo \"Copying $BinaryName.app, $UpdateFileAMD64 and $UpdateFileARM64 to deploy/$AppVersionStrMajor/$AppVersionStr..\";\n    mkdir \"$DeployPath\"\n    mkdir \"$DeployPath/$BinaryName\"\n    cp -r \"$ReleasePath/$BinaryName.app\" \"$DeployPath/$BinaryName/\"\n    if [ \"$AlphaVersion\" != \"0\" ]; then\n      mv \"$ReleasePath/$AlphaKeyFile\" \"$DeployPath/\"\n    fi\n    rm \"$ReleasePath/$BinaryName.app/Contents/MacOS/$BinaryName\"\n    rm \"$ReleasePath/$BinaryName.app/Contents/Frameworks/Updater\"\n    mv \"$ReleasePath/$UpdateFileAMD64\" \"$DeployPath/\"\n    mv \"$ReleasePath/$UpdateFileARM64\" \"$DeployPath/\"\n    mv \"$ReleasePath/$SetupFile\" \"$DeployPath/\"\n\n    if [ \"$BuildTarget\" == \"mac\" ]; then\n      mkdir -p \"$BackupPath/tmac\"\n      cp \"$DeployPath/$UpdateFileAMD64\" \"$BackupPath/tmac/\"\n      cp \"$DeployPath/$UpdateFileARM64\" \"$BackupPath/tmac/\"\n      cp \"$DeployPath/$SetupFile\" \"$BackupPath/tmac/\"\n      if [ \"$AlphaVersion\" != \"0\" ]; then\n        cp -v \"$DeployPath/$AlphaKeyFile\" \"$BackupPath/tmac/\"\n      fi\n    fi\n  elif [ \"$BuildTarget\" == \"macstore\" ]; then\n    echo \"Copying $BinaryName.app to deploy/$AppVersionStrMajor/$AppVersionStr..\";\n    mkdir \"$DeployPath\"\n    cp -r \"$ReleasePath/$BinaryName.app\" \"$DeployPath/\"\n    mv \"$ReleasePath/$BinaryName.pkg\" \"$DeployPath/\"\n    rm \"$ReleasePath/$BinaryName.app/Contents/MacOS/$BinaryName\"\n  fi\nfi\n\necho \"Version $AppVersionStrFull is ready!\";\necho -en \"\\007\";\nsleep 1;\necho -en \"\\007\";\nsleep 1;\necho -en \"\\007\";\n"
  },
  {
    "path": "Telegram/build/changelog2appstream.py",
    "content": "#!/usr/bin/env python3\n\nimport re\nimport datetime\nfrom xml.etree import ElementTree as ET\nimport argparse\n\ndef parse_changelog(changelog_path):\n    version_re = re.compile(r'([\\d.-]+)\\s+(\\w+)?\\s*\\((\\d{2}.\\d{2}\\.\\d{2})\\)')\n    entry_re = re.compile(r'-\\s(.*)')\n\n    with open(changelog_path, \"r\", encoding=\"utf-8\") as f:\n        changelog_lines = f.read().splitlines()\n\n    releases = []\n    for l in changelog_lines:\n        version_match = version_re.match(l)\n        entry_match = entry_re.match(l)\n        if version_match is not None:\n            version, prerelease, date = version_match.groups()\n            release = (version,\n                       prerelease,\n                       datetime.datetime.strptime(date, '%d.%m.%y').date(),\n                       [])\n            releases.append(release)\n        elif entry_match is not None:\n            release[3].append(entry_match.group(1))\n\n    return releases\n\ndef get_release_xml(version, prerelease, date, changes):\n    release = ET.Element(\"release\")\n    if prerelease is None:\n        ver_str = version\n    else:\n        ver_str = f\"{version}~{prerelease}\"\n    release.set(\"version\", ver_str)\n    release.set(\"date\", date.isoformat())\n    description = ET.SubElement(release, \"description\")\n    changelist = ET.SubElement(description, \"ul\")\n    for c in changes:\n        change = ET.SubElement(changelist, \"li\")\n        change.text = c\n    return release\n\ndef get_changelog_xml(changelog, max_items=None):\n    releases = ET.Element(\"releases\")\n    if max_items is not None:\n        changelog = changelog[:max_items]\n    for rel in changelog:\n        release = get_release_xml(*rel)\n        releases.append(release)\n    return releases\n\ndef update_metadata(metadata_path, changelog, max_items=None):\n    metadata = ET.parse(metadata_path)\n    root = metadata.getroot()\n    releases = root.find(\"releases\")\n    if releases is not None:\n        root.remove(releases)\n    root.append(\n        get_changelog_xml(changelog, max_items)\n    )\n    metadata.write(metadata_path, encoding=\"utf-8\", xml_declaration=True)\n\ndef main():\n    ap = argparse.ArgumentParser(\"Parse Telegram changelog\")\n    ap.add_argument(\"-c\", \"--changelog-path\", default=\"changelog.txt\")\n    ap.add_argument(\"-m\", \"--metadata-path\", default=\"lib/xdg/org.telegram.desktop.metainfo.xml\")\n    ap.add_argument(\"-n\", \"--num-releases\", type=int, default=None)\n    args = ap.parse_args()\n    update_metadata(args.metadata_path,\n                   parse_changelog(args.changelog_path),\n                   max_items=args.num_releases)\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "Telegram/build/deploy.sh",
    "content": "set -e\nFullExecPath=$PWD\npushd `dirname $0` > /dev/null\nFullScriptPath=`pwd`\npopd > /dev/null\n\nif [ ! -d \"$FullScriptPath/../../../DesktopPrivate\" ]; then\n  echo \"\"\n  echo \"This script is for building the production version of Telegram Desktop.\"\n  echo \"\"\n  echo \"For building custom versions please visit the build instructions page at:\"\n  echo \"https://github.com/telegramdesktop/tdesktop/#build-instructions\"\n  exit\nfi\n\nError () {\n  cd $FullExecPath\n  echo \"$1\"\n  exit 1\n}\n\nDeployTarget=\"$1\"\n\nif [ ! -f \"$FullScriptPath/target\" ]; then\n  Error \"Build target not found!\"\nfi\n\nwhile IFS='' read -r line || [[ -n \"$line\" ]]; do\n  BuildTarget=\"$line\"\ndone < \"$FullScriptPath/target\"\n\nwhile IFS='' read -r line || [[ -n \"$line\" ]]; do\n  set $line\n  eval $1=\"$2\"\ndone < \"$FullScriptPath/version\"\n\nif [ \"$AlphaVersion\" != \"0\" ]; then\n  AppVersion=\"$AlphaVersion\"\n  AppVersionStrFull=\"${AppVersionStr}_${AlphaVersion}\"\n  AlphaKeyFile=\"talpha_${AppVersion}_key\"\nelif [ \"$BetaChannel\" == \"0\" ]; then\n  AppVersionStrFull=\"$AppVersionStr\"\nelse\n  AppVersionStrFull=\"$AppVersionStr.beta\"\nfi\n\necho \"\"\nHomePath=\"$FullScriptPath/..\"\nDeployMac=\"0\"\nDeployWin=\"0\"\nDeployWin64=\"0\"\nDeployWinArm=\"0\"\nDeployLinux=\"0\"\nif [ \"$DeployTarget\" == \"mac\" ]; then\n  DeployMac=\"1\"\n  echo \"Deploying version $AppVersionStrFull for macOS..\"\nelif [ \"$DeployTarget\" == \"win\" ]; then\n  DeployWin=\"1\"\n  echo \"Deploying version $AppVersionStrFull for Windows 32 bit..\"\nelif [ \"$DeployTarget\" == \"win64\" ]; then\n  DeployWin64=\"1\"\n  echo \"Deploying version $AppVersionStrFull for Windows 64 bit..\"\nelif [ \"$DeployTarget\" == \"winarm\" ]; then\n  DeployWinArm=\"1\"\n  echo \"Deploying version $AppVersionStrFull for Windows on ARM..\"\nelif [ \"$DeployTarget\" == \"linux\" ]; then\n  DeployLinux=\"1\"\n  echo \"Deploying version $AppVersionStrFull for Linux 64 bit..\"\nelse\n  DeployMac=\"1\"\n  DeployWin=\"1\"\n  DeployWin64=\"1\"\n  DeployWinArm=\"1\"\n  DeployLinux=\"1\"\n  echo \"Deploying five versions of $AppVersionStrFull: for Windows 32 bit / 64 bit / on ARM, macOS and Linux 64 bit..\"\nfi\nif [ \"$BuildTarget\" == \"mac\" ]; then\n  BackupPath=\"$HOME/Projects/backup/tdesktop\"\nelif [ \"$BuildTarget\" == \"linux\" ]; then\n  BackupPath=\"/media/psf/Home/Projects/backup/tdesktop\"\n  if [ ! -d \"$BackupPath\" ]; then\n    BackupPath=\"/mnt/c/Telegram/Projects/backup/tdesktop\"\n  fi\nelse\n  Error \"Can't deploy here\"\nfi\nMacDeployPath=\"$BackupPath/$AppVersionStrMajor/$AppVersionStrFull/tmac\"\nMacUpdateFile=\"tmacupd$AppVersion\"\nARMacUpdateFile=\"tarmacupd$AppVersion\"\nMacSetupFile=\"tsetup.$AppVersionStrFull.dmg\"\nMacRemoteFolder=\"tmac\"\nWinDeployPath=\"$BackupPath/$AppVersionStrMajor/$AppVersionStrFull/tsetup\"\nWinUpdateFile=\"tupdate$AppVersion\"\nWinSetupFile=\"tsetup.$AppVersionStrFull.exe\"\nWinPortableFile=\"tportable.$AppVersionStrFull.zip\"\nWinRemoteFolder=\"tsetup\"\nWin64DeployPath=\"$BackupPath/$AppVersionStrMajor/$AppVersionStrFull/tx64\"\nWin64UpdateFile=\"tx64upd$AppVersion\"\nWin64SetupFile=\"tsetup-x64.$AppVersionStrFull.exe\"\nWin64PortableFile=\"tportable-x64.$AppVersionStrFull.zip\"\nWin64RemoteFolder=\"tx64\"\nWinArmDeployPath=\"$BackupPath/$AppVersionStrMajor/$AppVersionStrFull/tarm64\"\nWinArmUpdateFile=\"tarm64upd$AppVersion\"\nWinArmSetupFile=\"tsetup-arm64.$AppVersionStrFull.exe\"\nWinArmPortableFile=\"tportable-arm64.$AppVersionStrFull.zip\"\nWinArmRemoteFolder=\"tarm64\"\nLinuxDeployPath=\"$BackupPath/$AppVersionStrMajor/$AppVersionStrFull/tlinux\"\nLinuxUpdateFile=\"tlinuxupd$AppVersion\"\nLinuxSetupFile=\"tsetup.$AppVersionStrFull.tar.xz\"\nLinuxRemoteFolder=\"tlinux\"\nDeployPath=\"$BackupPath/$AppVersionStrMajor/$AppVersionStrFull\"\n\nif [ \"$AlphaVersion\" != \"0\" ]; then\n  if [ \"$DeployTarget\" == \"win\" ]; then\n    AlphaFilePath=\"$WinDeployPath/$AlphaKeyFile\"\n  elif [ \"$DeployTarget\" == \"win64\" ]; then\n    AlphaFilePath=\"$Win64DeployPath/$AlphaKeyFile\"\n  elif [ \"$DeployTarget\" == \"winarm\" ]; then\n    AlphaFilePath=\"$WinArmDeployPath/$AlphaKeyFile\"\n  elif [ \"$DeployTarget\" == \"linux\" ]; then\n    AlphaFilePath=\"$LinuxDeployPath/$AlphaKeyFile\"\n  else\n    AlphaFilePath=\"$MacDeployPath/$AlphaKeyFile\"\n  fi\n  if [ ! -f \"$AlphaFilePath\" ]; then\n    Error \"Alpha key file for $AppVersionStrFull not found.\"\n  fi\n\n  while IFS='' read -r line || [[ -n \"$line\" ]]; do\n    AlphaSignature=\"$line\"\n  done < \"$AlphaFilePath\"\n\n  MacUpdateFile=\"${MacUpdateFile}_${AlphaSignature}\"\n  ARMacUpdateFile=\"${ARMacUpdateFile}_${AlphaSignature}\"\n  MacSetupFile=\"talpha${AlphaVersion}_${AlphaSignature}.zip\"\n  WinUpdateFile=\"${WinUpdateFile}_${AlphaSignature}\"\n  WinPortableFile=\"talpha${AlphaVersion}_${AlphaSignature}.zip\"\n  Win64UpdateFile=\"${Win64UpdateFile}_${AlphaSignature}\"\n  Win64PortableFile=\"talpha${AlphaVersion}_${AlphaSignature}.zip\"\n  WinArmUpdateFile=\"${WinArmUpdateFile}_${AlphaSignature}\"\n  WinArmPortableFile=\"talpha${AlphaVersion}_${AlphaSignature}.zip\"\n  LinuxUpdateFile=\"${LinuxUpdateFile}_${AlphaSignature}\"\n  LinuxSetupFile=\"talpha${AlphaVersion}_${AlphaSignature}.tar.xz\"\nfi\n\nif [ \"$DeployMac\" == \"1\" ]; then\n  if [ ! -f \"$MacDeployPath/$MacUpdateFile\" ]; then\n    Error \"$MacDeployPath/$MacUpdateFile not found!\";\n  fi\n  if [ ! -f \"$MacDeployPath/$ARMacUpdateFile\" ]; then\n    Error \"$MacDeployPath/$ARMacUpdateFile not found!\";\n  fi\n  if [ ! -f \"$MacDeployPath/$MacSetupFile\" ]; then\n    Error \"$MacDeployPath/$MacSetupFile not found!\"\n  fi\nfi\nif [ \"$DeployWin\" == \"1\" ]; then\n  if [ ! -f \"$WinDeployPath/$WinUpdateFile\" ]; then\n    Error \"$WinUpdateFile not found!\"\n  fi\n  if [ \"$AlphaVersion\" == \"0\" ]; then\n    if [ ! -f \"$WinDeployPath/$WinSetupFile\" ]; then\n      Error \"$WinSetupFile not found!\"\n    fi\n  fi\n  if [ ! -f \"$WinDeployPath/$WinPortableFile\" ]; then\n    Error \"$WinPortableFile not found!\"\n  fi\nfi\nif [ \"$DeployWin64\" == \"1\" ]; then\n  if [ ! -f \"$Win64DeployPath/$Win64UpdateFile\" ]; then\n    Error \"$Win64UpdateFile not found!\"\n  fi\n  if [ \"$AlphaVersion\" == \"0\" ]; then\n    if [ ! -f \"$Win64DeployPath/$Win64SetupFile\" ]; then\n      Error \"$Win64SetupFile not found!\"\n    fi\n  fi\n  if [ ! -f \"$Win64DeployPath/$Win64PortableFile\" ]; then\n    Error \"$Win64PortableFile not found!\"\n  fi\nfi\nif [ \"$DeployWinArm\" == \"1\" ]; then\n  if [ ! -f \"$WinArmDeployPath/$WinArmUpdateFile\" ]; then\n    Error \"$WinArmUpdateFile not found!\"\n  fi\n  if [ \"$AlphaVersion\" == \"0\" ]; then\n    if [ ! -f \"$WinArmDeployPath/$WinArmSetupFile\" ]; then\n      Error \"$WinArmSetupFile not found!\"\n    fi\n  fi\n  if [ ! -f \"$WinArmDeployPath/$WinArmPortableFile\" ]; then\n    Error \"$WinArmPortableFile not found!\"\n  fi\nfi\nif [ \"$DeployLinux\" == \"1\" ]; then\n  if [ ! -f \"$LinuxDeployPath/$LinuxUpdateFile\" ]; then\n    Error \"$LinuxDeployPath/$LinuxUpdateFile not found!\"\n  fi\n  if [ ! -f \"$LinuxDeployPath/$LinuxSetupFile\" ]; then\n    Error \"$LinuxDeployPath/$LinuxSetupFile not found!\"\n  fi\nfi\n\n$FullScriptPath/../../../DesktopPrivate/mount.sh\n\ndeclare -a Files\nif [ \"$DeployMac\" == \"1\" ]; then\n  Files+=(\"tmac/$MacUpdateFile\" \"tmac/$ARMacUpdateFile\" \"tmac/$MacSetupFile\")\nfi\nif [ \"$DeployWin\" == \"1\" ]; then\n  Files+=(\"tsetup/$WinUpdateFile\" \"tsetup/$WinPortableFile\")\n  if [ \"$AlphaVersion\" == \"0\" ]; then\n    Files+=(\"tsetup/$WinSetupFile\")\n  fi\nfi\nif [ \"$DeployWin64\" == \"1\" ]; then\n  Files+=(\"tx64/$Win64UpdateFile\" \"tx64/$Win64PortableFile\")\n  if [ \"$AlphaVersion\" == \"0\" ]; then\n    Files+=(\"tx64/$Win64SetupFile\")\n  fi\nfi\nif [ \"$DeployWinArm\" == \"1\" ]; then\n  Files+=(\"tarm64/$WinArmUpdateFile\" \"tarm64/$WinArmPortableFile\")\n  if [ \"$AlphaVersion\" == \"0\" ]; then\n    Files+=(\"tarm64/$WinArmSetupFile\")\n  fi\nfi\nif [ \"$DeployLinux\" == \"1\" ]; then\n  Files+=(\"tlinux/$LinuxUpdateFile\" \"tlinux/$LinuxSetupFile\")\nfi\ncd $DeployPath\nrsync -avR --no-g --progress ${Files[@]} \"$FullScriptPath/../../../DesktopPrivate/remote/files\"\n\necho \"Version $AppVersionStrFull was deployed!\"\ncd $FullExecPath\n\n"
  },
  {
    "path": "Telegram/build/docker/build.sh",
    "content": "#!/bin/bash\n\nset -e\nFullExecPath=$PWD\npushd `dirname $0` > /dev/null\nFullScriptPath=`pwd`\npopd > /dev/null\n\nif [ ! -d \"$FullScriptPath/../../../../DesktopPrivate\" ]; then\n  echo \"\"\n  echo \"This script is for building the production version of Telegram Desktop.\"\n  echo \"\"\n  echo \"For building custom versions please visit the build instructions page at:\"\n  echo \"https://github.com/telegramdesktop/tdesktop/#build-instructions\"\n  exit\nfi\n\nHomePath=\"$FullScriptPath/../..\"\ncd $HomePath\n\nProjectPath=\"$HomePath/../out\"\nReleasePath=\"$ProjectPath/Release\"\nBinaryName=\"Telegram\"\n\nif [ ! -f \"/usr/bin/cmake\" ]; then\n  ln -s cmake3 /usr/bin/cmake\nfi\n\n./configure.sh -DDESKTOP_APP_ENABLE_LTO=ON\n\ncd $ProjectPath\ncmake --build . --config Release --target Telegram\ncd $ReleasePath\n\necho \"$BinaryName build complete!\"\n\nError () {\n  cd $FullExecPath\n  echo \"$1\"\n  exit 1\n}\n\nif [ ! -f \"$ReleasePath/$BinaryName\" ]; then\n  Error \"$BinaryName not found!\"\nfi\n\nif [ ! -f \"$ReleasePath/Updater\" ]; then\n  Error \"Updater not found!\"\nfi\n\nrm -rf \"$ReleasePath/root\"\nmkdir \"$ReleasePath/root\"\nmv \"$ReleasePath/$BinaryName\" \"$ReleasePath/root/\"\nmv \"$ReleasePath/Updater\" \"$ReleasePath/root/\"\nmv \"$ReleasePath/Packer\" \"$ReleasePath/root/\"\n"
  },
  {
    "path": "Telegram/build/docker/centos_env/.gitignore",
    "content": "__pycache__\n"
  },
  {
    "path": "Telegram/build/docker/centos_env/Dockerfile",
    "content": "# syntax=docker/dockerfile:1\n\nFROM rockylinux:8 AS builder\nENV LANG=C.UTF-8\nENV TOOLSET=gcc-toolset-15\nENV PATH=/opt/rh/$TOOLSET/root/usr/bin:$PATH\nENV LIBRARY_PATH=/opt/rh/$TOOLSET/root/usr/lib64:/opt/rh/$TOOLSET/root/usr/lib:/usr/local/lib64:/usr/local/lib:/lib64:/lib:/usr/lib64:/usr/lib\nENV LD_LIBRARY_PATH=$LIBRARY_PATH\nENV PKG_CONFIG_PATH=/opt/rh/$TOOLSET/root/usr/lib64/pkgconfig:/opt/rh/$TOOLSET/root/usr/lib/pkgconfig:/usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig\n\nRUN dnf -y install epel-release \\\n\t&& dnf config-manager --set-enabled powertools \\\n\t&& dnf -y install cmake autoconf automake libtool pkgconfig make patch git \\\n\t\tpython3.11-pip python3.11-devel gperf flex bison clang clang-tools-extra \\\n\t\tlld nasm file which wget perl-open perl-XML-Parser perl-IPC-Cmd \\\n\t\txorg-x11-util-macros $TOOLSET-gcc $TOOLSET-gcc-c++ $TOOLSET-binutils \\\n\t\t$TOOLSET-gdb $TOOLSET-libasan-devel libffi-devel fontconfig-devel \\\n\t\tfreetype-devel libX11-devel wayland-devel alsa-lib-devel \\\n\t\tpulseaudio-libs-devel mesa-libGL-devel mesa-libEGL-devel mesa-libgbm-devel \\\n\t\tlibdrm-devel vulkan-devel libva-devel libvdpau-devel libselinux-devel \\\n\t\tlibmount-devel systemd-devel glib2-devel gobject-introspection-devel \\\n\t\tat-spi2-core-devel gtk3-devel \\\n\t&& dnf clean all\n\nRUN alternatives --set python3 /usr/bin/python3.11\nRUN python3 -m pip install meson ninja\nRUN cat <<EOF > /usr/local/bin/pkg-config && chmod +x /usr/local/bin/pkg-config\n#!/bin/sh\nfor i in \"\\$@\"; do\n\t[ \"\\$i\" = \"--version\" ] && exec /usr/bin/pkg-config \"\\$i\"\ndone\nexec /usr/bin/pkg-config --static \"\\$@\"\nEOF\nRUN sed -i '/CMAKE_${lang}_FLAGS_DEBUG_INIT/s/\")/ -O0 {% if LTO %}-fno-lto -fno-use-linker-plugin{% endif %} -fuse-ld=lld\")/' /usr/share/cmake/Modules/Compiler/GNU.cmake\nRUN sed -i 's/NO_DEFAULT_PATH//g; s/PKG_CONFIG_ALLOW_SYSTEM_LIBS/PKG_CONFIG_IS_DUMB/g' /usr/share/cmake/Modules/FindPkgConfig.cmake\nRUN sed -i '/Requires.private: valgrind/d' /usr/lib64/pkgconfig/libdrm.pc\nRUN sed -i '/Requires.private: gl/s/gl/opengl glx/' /usr/lib64/pkgconfig/epoxy.pc\nRUN sed -i 's/-lharfbuzz//' /usr/lib64/pkgconfig/harfbuzz.pc\nRUN sed -i 's/-lpng16//' /usr/lib64/pkgconfig/libpng16.pc\nRUN echo set debuginfod enabled on > /opt/rh/$TOOLSET/root/etc/gdbinit.d/00-debuginfod.gdb\nRUN adduser user\n\nWORKDIR /usr/src\nENV AR=gcc-ar\nENV RANLIB=gcc-ranlib\nENV NM=gcc-nm\nENV CFLAGS='{% if DEBUG %}-g{% endif %} {% if MINSIZE %}-Os{% else %}-O3{% endif %} {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fexceptions -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fhardened -Wno-hardened'\nENV CXXFLAGS=$CFLAGS\nENV LDFLAGS='-static-libstdc++ -static-libgcc -static-libasan -pthread -Wl,--push-state,--no-as-needed,-ldl,--pop-state -Wl,--as-needed -Wl,-z,muldefs'\n\nENV CMAKE_GENERATOR=Ninja\nENV CMAKE_BUILD_TYPE=None\nENV CMAKE_BUILD_PARALLEL_LEVEL='{{ JOBS }}'\n\nRUN git init Implib.so \\\n\t&& cd Implib.so \\\n\t&& git remote add origin https://github.com/yugr/Implib.so.git \\\n\t&& git fetch --depth=1 origin ecf7bb51a92a0fb16834c5b698570ab25f9f1d21 \\\n\t&& git reset --hard FETCH_HEAD \\\n\t&& mkdir build \\\n\t&& cd build \\\n\t&& implib() { \\\n\t\tLIBFILE=$(basename $1); \\\n\t\tLIBNAME=$(basename $1 .so); \\\n\t\t../implib-gen.py -q $1; \\\n\t\tgcc $CFLAGS -c -o $LIBFILE.tramp.o $LIBFILE.tramp.S; \\\n\t\tgcc $CFLAGS -c -o $LIBFILE.init.o $LIBFILE.init.c; \\\n\t\tar rcs /usr/local/lib64/$LIBNAME.a $LIBFILE.tramp.o $LIBFILE.init.o; \\\n\t} \\\n\t&& implib /usr/lib64/libgtk-3.so \\\n\t&& implib /usr/lib64/libgdk-3.so \\\n\t&& implib /usr/lib64/libgdk_pixbuf-2.0.so \\\n\t&& implib /usr/lib64/libpango-1.0.so \\\n\t&& implib /usr/lib64/libvdpau.so \\\n\t&& implib /usr/lib64/libva-x11.so \\\n\t&& implib /usr/lib64/libva-drm.so \\\n\t&& implib /usr/lib64/libva.so \\\n\t&& implib /usr/lib64/libEGL.so \\\n\t&& implib /usr/lib64/libGLX.so \\\n\t&& implib /usr/lib64/libOpenGL.so \\\n\t&& implib /usr/lib64/libGL.so \\\n\t&& implib /usr/lib64/libdrm.so \\\n\t&& implib /usr/lib64/libwayland-egl.so \\\n\t&& implib /usr/lib64/libwayland-cursor.so \\\n\t&& implib /usr/lib64/libwayland-client.so \\\n\t&& implib /usr/lib64/libwayland-server.so \\\n\t&& implib /usr/lib64/libX11-xcb.so \\\n\t&& implib /usr/lib64/libxcb.so \\\n\t&& cd ../.. \\\n\t&& rm -rf Implib.so\n\nFROM builder AS patches\nRUN git init patches \\\n\t&& cd patches \\\n\t&& git remote add origin https://github.com/desktop-app/patches.git \\\n\t&& git fetch --depth=1 origin 4519c85c924b9da81f29d4aac045886f896ee479 \\\n\t&& git reset --hard FETCH_HEAD \\\n\t&& rm -rf .git\n\nFROM builder AS zlib\nRUN git clone -b v1.3.1 --depth=1 https://github.com/madler/zlib.git \\\n\t&& cd zlib \\\n\t&& cmake -B build . -DZLIB_BUILD_EXAMPLES=OFF \\\n\t&& cmake --build build \\\n\t&& export DESTDIR=/usr/src/zlib-cache \\\n\t&& cmake --install build \\\n\t&& rm $DESTDIR/usr/local/lib/libz.so* \\\n\t&& cd .. \\\n\t&& rm -rf zlib\n\nFROM builder AS xz\nRUN git clone -b v5.8.1 --depth=1 https://github.com/tukaani-project/xz.git \\\n\t&& cd xz \\\n\t&& cmake -B build . \\\n\t&& cmake --build build \\\n\t&& DESTDIR=/usr/src/xz-cache cmake --install build \\\n\t&& cd .. \\\n\t&& rm -rf xz\n\nFROM builder AS protobuf\nRUN git clone -b v30.2 --depth=1 --recursive --shallow-submodules https://github.com/protocolbuffers/protobuf.git \\\n\t&& cd protobuf \\\n\t&& cmake -B build . \\\n\t\t-Dprotobuf_BUILD_TESTS=OFF \\\n\t\t-Dprotobuf_BUILD_PROTOBUF_BINARIES=ON \\\n\t\t-Dprotobuf_BUILD_LIBPROTOC=ON \\\n\t\t-Dprotobuf_WITH_ZLIB=OFF \\\n\t&& cmake --build build \\\n\t&& DESTDIR=/usr/src/protobuf-cache cmake --install build \\\n\t&& cd .. \\\n\t&& rm -rf protobuf\n\nFROM builder AS lcms2\nRUN git clone -b lcms2.15 --depth=1 https://github.com/mm2/Little-CMS.git \\\n\t&& cd Little-CMS \\\n\t&& meson build \\\n\t\t--buildtype=plain \\\n\t\t--default-library=static \\\n\t&& meson compile -C build \\\n\t&& DESTDIR=/usr/src/lcms2-cache meson install -C build \\\n\t&& cd .. \\\n\t&& rm -rf Little-CMS\n\nFROM builder AS brotli\nRUN git clone -b v1.1.0 --depth=1 https://github.com/google/brotli.git \\\n\t&& cd brotli \\\n\t&& cmake -B build . \\\n\t\t-DBUILD_SHARED_LIBS=OFF \\\n\t\t-DBROTLI_DISABLE_TESTS=ON \\\n\t&& cmake --build build \\\n\t&& DESTDIR=/usr/src/brotli-cache cmake --install build \\\n\t&& cd .. \\\n\t&& rm -rf brotli\n\nFROM builder AS highway\nRUN git clone -b 1.0.7 --depth=1 https://github.com/google/highway.git \\\n\t&& cd highway \\\n\t&& cmake -B build . \\\n\t\t-DBUILD_TESTING=OFF \\\n\t\t-DHWY_ENABLE_CONTRIB=OFF \\\n\t\t-DHWY_ENABLE_EXAMPLES=OFF \\\n\t&& cmake --build build \\\n\t&& DESTDIR=/usr/src/highway-cache cmake --install build \\\n\t&& cd .. \\\n\t&& rm -rf highway\n\nFROM builder AS mozjpeg\nRUN git clone -b v4.1.5 --depth=1 https://github.com/mozilla/mozjpeg.git \\\n\t&& cd mozjpeg \\\n\t&& cmake -B build . \\\n\t\t-DCMAKE_INSTALL_PREFIX=/usr/local \\\n\t\t-DENABLE_SHARED=OFF \\\n\t\t-DWITH_JPEG8=ON \\\n\t\t-DPNG_SUPPORTED=OFF \\\n\t&& cmake --build build --parallel \\\n\t&& DESTDIR=/usr/src/mozjpeg-cache cmake --install build \\\n\t&& cd .. \\\n\t&& rm -rf mozjpeg\n\nFROM builder AS opus\nRUN git clone -b v1.5.2 --depth=1 https://github.com/xiph/opus.git \\\n\t&& cd opus \\\n\t&& cmake -B build . \\\n\t&& cmake --build build \\\n\t&& DESTDIR=/usr/src/opus-cache cmake --install build \\\n\t&& cd .. \\\n\t&& rm -rf opus\n\nFROM builder AS dav1d\nRUN git clone -b 1.5.3 --depth=1 https://github.com/videolan/dav1d.git \\\n\t&& cd dav1d \\\n\t&& meson build \\\n\t\t--buildtype=plain \\\n\t\t--default-library=static \\\n\t\t-Denable_tools=false \\\n\t\t-Denable_tests=false \\\n\t&& meson compile -C build \\\n\t&& DESTDIR=/usr/src/dav1d-cache meson install -C build \\\n\t&& cd .. \\\n\t&& rm -rf dav1d\n\nFROM builder AS openh264\nRUN git clone -b v2.6.0 --depth=1 https://github.com/cisco/openh264.git \\\n\t&& cd openh264 \\\n\t&& meson build \\\n\t\t--buildtype=plain \\\n\t\t--default-library=static \\\n\t&& meson compile -C build \\\n\t&& DESTDIR=/usr/src/openh264-cache meson install -C build \\\n\t&& cd .. \\\n\t&& rm -rf openh264\n\nFROM builder AS de265\nRUN git clone -b v1.0.16 --depth=1 https://github.com/strukturag/libde265.git \\\n\t&& cd libde265 \\\n\t&& cmake -B build . \\\n\t\t-DCMAKE_BUILD_TYPE=None \\\n\t\t-DBUILD_SHARED_LIBS=OFF \\\n\t\t-DENABLE_DECODER=OFF \\\n\t\t-DENABLE_SDL=OFF \\\n\t&& cmake --build build \\\n\t&& DESTDIR=/usr/src/de265-cache cmake --install build \\\n\t&& cd .. \\\n\t&& rm -rf libde265\n\nFROM builder AS vpx\nRUN git init libvpx \\\n\t&& cd libvpx \\\n\t&& git remote add origin https://github.com/webmproject/libvpx.git \\\n\t&& git fetch --depth=1 origin 12f3a2ac603e8f10742105519e0cd03c3b8f71dd \\\n\t&& git reset --hard FETCH_HEAD \\\n\t&& CFLAGS=\"$CFLAGS -fno-lto\" CXXFLAGS=\"$CXXFLAGS -fno-lto\" ./configure \\\n\t\t--disable-examples \\\n\t\t--disable-unit-tests \\\n\t\t--disable-tools \\\n\t\t--disable-docs \\\n\t\t--enable-vp8 \\\n\t\t--enable-vp9 \\\n\t\t--enable-webm-io \\\n\t\t--size-limit=4096x4096 \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/vpx-cache install \\\n\t&& cd .. \\\n\t&& rm -rf libvpx\n\nFROM builder AS webp\nRUN git clone -b v1.6.0 --depth=1 https://github.com/webmproject/libwebp.git \\\n\t&& cd libwebp \\\n\t&& cmake -B build . \\\n\t\t-DWEBP_BUILD_ANIM_UTILS=OFF \\\n\t\t-DWEBP_BUILD_CWEBP=OFF \\\n\t\t-DWEBP_BUILD_DWEBP=OFF \\\n\t\t-DWEBP_BUILD_GIF2WEBP=OFF \\\n\t\t-DWEBP_BUILD_IMG2WEBP=OFF \\\n\t\t-DWEBP_BUILD_VWEBP=OFF \\\n\t\t-DWEBP_BUILD_WEBPMUX=OFF \\\n\t\t-DWEBP_BUILD_WEBPINFO=OFF \\\n\t\t-DWEBP_BUILD_EXTRAS=OFF \\\n\t&& cmake --build build \\\n\t&& DESTDIR=/usr/src/webp-cache cmake --install build \\\n\t&& cd .. \\\n\t&& rm -rf libwebp\n\nFROM builder AS avif\nCOPY --link --from=dav1d /usr/src/dav1d-cache /\n\nRUN git clone -b v1.3.0 --depth=1 https://github.com/AOMediaCodec/libavif.git \\\n\t&& cd libavif \\\n\t&& cmake -B build . \\\n\t\t-DBUILD_SHARED_LIBS=OFF \\\n\t\t-DAVIF_CODEC_DAV1D=SYSTEM \\\n\t\t-DAVIF_LIBYUV=OFF \\\n\t&& cmake --build build \\\n\t&& DESTDIR=/usr/src/avif-cache cmake --install build \\\n\t&& cd .. \\\n\t&& rm -rf libavif\n\nFROM builder AS heif\nCOPY --link --from=de265 /usr/src/de265-cache /\n\nRUN git clone -b v1.21.2 --depth=1 https://github.com/strukturag/libheif.git \\\n\t&& cd libheif \\\n\t&& cmake -B build . \\\n\t\t-DBUILD_SHARED_LIBS=OFF \\\n\t\t-DCMAKE_DISABLE_FIND_PACKAGE_Doxygen=ON \\\n\t\t-DBUILD_TESTING=OFF \\\n\t\t-DENABLE_PLUGIN_LOADING=OFF \\\n\t\t-DWITH_X265=OFF \\\n\t\t-DWITH_AOM_DECODER=OFF \\\n\t\t-DWITH_AOM_ENCODER=OFF \\\n\t\t-DWITH_X264=OFF \\\n\t\t-DWITH_OpenH264_DECODER=OFF \\\n\t\t-DWITH_RAV1E=OFF \\\n\t\t-DWITH_RAV1E_PLUGIN=OFF \\\n\t\t-DWITH_SvtEnc=OFF \\\n\t\t-DWITH_SvtEnc_PLUGIN=OFF \\\n\t\t-DWITH_DAV1D=OFF \\\n\t\t-DWITH_LIBSHARPYUV=OFF \\\n\t\t-DWITH_EXAMPLES=OFF \\\n\t&& cmake --build build \\\n\t&& DESTDIR=/usr/src/heif-cache cmake --install build \\\n\t&& cd .. \\\n\t&& rm -rf libheif\n\nFROM builder AS jxl\nCOPY --link --from=lcms2 /usr/src/lcms2-cache /\nCOPY --link --from=brotli /usr/src/brotli-cache /\nCOPY --link --from=highway /usr/src/highway-cache /\n\nRUN git clone -b v0.11.2 --depth=1 https://github.com/libjxl/libjxl.git \\\n\t&& cd libjxl \\\n\t&& cmake -B build . \\\n\t\t-DBUILD_SHARED_LIBS=OFF \\\n\t\t-DBUILD_TESTING=OFF \\\n\t\t-DJPEGXL_ENABLE_DEVTOOLS=OFF \\\n\t\t-DJPEGXL_ENABLE_TOOLS=OFF \\\n\t\t-DJPEGXL_ENABLE_DOXYGEN=OFF \\\n\t\t-DJPEGXL_ENABLE_MANPAGES=OFF \\\n\t\t-DJPEGXL_ENABLE_BENCHMARK=OFF \\\n\t\t-DJPEGXL_ENABLE_EXAMPLES=OFF \\\n\t\t-DJPEGXL_ENABLE_JNI=OFF \\\n\t\t-DJPEGXL_ENABLE_SJPEG=OFF \\\n\t\t-DJPEGXL_ENABLE_OPENEXR=OFF \\\n\t\t-DJPEGXL_ENABLE_SKCMS=OFF \\\n\t&& cmake --build build \\\n\t&& export DESTDIR=/usr/src/jxl-cache \\\n\t&& cmake --install build \\\n\t&& sed -i 's/-lstdc++//' $DESTDIR/usr/local/lib64/pkgconfig/libjxl*.pc \\\n\t&& cd .. \\\n\t&& rm -rf libjxl\n\nFROM builder AS rnnoise\nRUN git clone -b v0.2 --depth=1 https://github.com/xiph/rnnoise.git \\\n\t&& cd rnnoise \\\n\t&& ./autogen.sh \\\n\t&& ./configure --enable-static --disable-shared \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/rnnoise-cache install \\\n\t&& cd .. \\\n\t&& rm -rf rnnoise\n\nFROM builder AS xcb-proto\nRUN git clone -b xcb-proto-1.16.0 --depth=1 https://github.com/gitlab-freedesktop-mirrors/xcbproto.git \\\n\t&& cd xcbproto \\\n\t&& ./autogen.sh \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/xcb-proto-cache install \\\n\t&& cd .. \\\n\t&& rm -rf xcbproto\n\nFROM builder AS xcb\nCOPY --link --from=xcb-proto /usr/src/xcb-proto-cache /\n\nRUN git clone -b libxcb-1.16 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxcb.git \\\n\t&& cd libxcb \\\n\t&& ./autogen.sh --enable-static --disable-shared \\\n\t&& make -j$(nproc) \\\n\t&& export DESTDIR=/usr/src/xcb-cache \\\n\t&& make install \\\n\t&& rm $DESTDIR/usr/local/lib/{libxcb.{,l}a,pkgconfig/xcb.pc} \\\n\t&& cd .. \\\n\t&& rm -rf libxcb\n\nFROM builder AS xcb-wm\nRUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-wm.git \\\n\t&& cd libxcb-wm \\\n\t&& ./autogen.sh --enable-static --disable-shared \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/xcb-wm-cache install \\\n\t&& cd .. \\\n\t&& rm -rf libxcb-wm\n\nFROM builder AS xcb-util\nRUN git clone -b xcb-util-0.4.1-gitlab --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-util.git \\\n\t&& cd libxcb-util \\\n\t&& ./autogen.sh --enable-static --disable-shared \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/xcb-util-cache install \\\n\t&& cd .. \\\n\t&& rm -rf libxcb-util\n\nFROM builder AS xcb-image\nCOPY --link --from=xcb-util /usr/src/xcb-util-cache /\n\nRUN git clone -b xcb-util-image-0.4.1-gitlab --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-image.git \\\n\t&& cd libxcb-image \\\n\t&& ./autogen.sh --enable-static --disable-shared \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/xcb-image-cache install \\\n\t&& cd .. \\\n\t&& rm -rf libxcb-image\n\nFROM builder AS xcb-keysyms\nRUN git init libxcb-keysyms \\\n\t&& cd libxcb-keysyms \\\n\t&& git remote add origin https://github.com/gitlab-freedesktop-mirrors/libxcb-keysyms.git \\\n\t&& git fetch --depth=1 origin ef5cb393d27511ba511c68a54f8ff7b9aab4a384 \\\n\t&& git reset --hard FETCH_HEAD \\\n\t&& git submodule update --init --recursive --depth=1 \\\n\t&& ./autogen.sh --enable-static --disable-shared \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/xcb-keysyms-cache install \\\n\t&& cd .. \\\n\t&& rm -rf libxcb-keysyms\n\nFROM builder AS xcb-render-util\nRUN git init libxcb-render-util \\\n\t&& cd libxcb-render-util \\\n\t&& git remote add origin https://github.com/gitlab-freedesktop-mirrors/libxcb-render-util.git \\\n\t&& git fetch --depth=1 origin 5ad9853d6ddcac394d42dd2d4e34436b5db9da39 \\\n\t&& git reset --hard FETCH_HEAD \\\n\t&& git submodule update --init --recursive --depth=1 \\\n\t&& ./autogen.sh --enable-static --disable-shared \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/xcb-render-util-cache install \\\n\t&& cd .. \\\n\t&& rm -rf libxcb-render-util\n\nFROM builder AS xcb-cursor\nCOPY --link --from=xcb-util /usr/src/xcb-util-cache /\nCOPY --link --from=xcb-image /usr/src/xcb-image-cache /\nCOPY --link --from=xcb-render-util /usr/src/xcb-render-util-cache /\n\nRUN git init libxcb-cursor \\\n\t&& cd libxcb-cursor \\\n\t&& git remote add origin https://github.com/gitlab-freedesktop-mirrors/libxcb-cursor.git \\\n\t&& git fetch --depth=1 origin 4929f6051658ba5424b41703a1fb63f9db896065 \\\n\t&& git reset --hard FETCH_HEAD \\\n\t&& git submodule update --init --recursive --depth=1 \\\n\t&& ./autogen.sh --enable-static --disable-shared --with-cursorpath='~/.local/share/icons:~/.icons:/usr/share/icons:/usr/share/pixmaps' \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/xcb-cursor-cache install \\\n\t&& cd .. \\\n\t&& rm -rf libxcb-cursor\n\nFROM builder AS xext\nRUN git clone -b libXext-1.3.5 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxext.git \\\n\t&& cd libxext \\\n\t&& ./autogen.sh --enable-static --disable-shared \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/xext-cache install \\\n\t&& cd .. \\\n\t&& rm -rf libxext\n\nFROM builder AS xtst\nRUN git clone -b libXtst-1.2.4 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxtst.git \\\n\t&& cd libxtst \\\n\t&& ./autogen.sh --enable-static --disable-shared \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/xtst-cache install \\\n\t&& cd .. \\\n\t&& rm -rf libxtst\n\nFROM builder AS xfixes\nRUN git clone -b libXfixes-5.0.3 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxfixes.git \\\n\t&& cd libxfixes \\\n\t&& ./autogen.sh --enable-static --disable-shared \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/xfixes-cache install \\\n\t&& cd .. \\\n\t&& rm -rf libxfixes\n\nFROM builder AS xv\nCOPY --link --from=xext /usr/src/xext-cache /\n\nRUN git clone -b libXv-1.0.12 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxv.git \\\n\t&& cd libxv \\\n\t&& ./autogen.sh --enable-static --disable-shared \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/xv-cache install \\\n\t&& cd .. \\\n\t&& rm -rf libxv\n\nFROM builder AS xrandr\nRUN git clone -b libXrandr-1.5.3 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxrandr.git \\\n\t&& cd libxrandr \\\n\t&& ./autogen.sh --enable-static --disable-shared \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/xrandr-cache install \\\n\t&& cd .. \\\n\t&& rm -rf libxrandr\n\nFROM builder AS xrender\nRUN git clone -b libXrender-0.9.11 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxrender.git \\\n\t&& cd libxrender \\\n\t&& ./autogen.sh --enable-static --disable-shared \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/xrender-cache install \\\n\t&& cd .. \\\n\t&& rm -rf libxrender\n\nFROM builder AS xdamage\nRUN git clone -b libXdamage-1.1.6 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxdamage.git \\\n\t&& cd libxdamage \\\n\t&& ./autogen.sh --enable-static --disable-shared \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/xdamage-cache install \\\n\t&& cd .. \\\n\t&& rm -rf libxdamage\n\nFROM builder AS xcomposite\nRUN git clone -b libXcomposite-0.4.6 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxcomposite.git \\\n\t&& cd libxcomposite \\\n\t&& ./autogen.sh --enable-static --disable-shared \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/xcomposite-cache install \\\n\t&& cd .. \\\n\t&& rm -rf libxcomposite\n\nFROM builder AS nv-codec-headers\nRUN git clone -b n12.1.14.0 --depth=1 https://github.com/FFmpeg/nv-codec-headers.git \\\n\t&& DESTDIR=/usr/src/nv-codec-headers-cache make -C nv-codec-headers install \\\n\t&& rm -rf nv-codec-headers\n\nFROM builder AS ffmpeg\nCOPY --link --from=opus /usr/src/opus-cache /\nCOPY --link --from=openh264 /usr/src/openh264-cache /\nCOPY --link --from=dav1d /usr/src/dav1d-cache /\nCOPY --link --from=vpx /usr/src/vpx-cache /\nCOPY --link --from=xext /usr/src/xext-cache /\nCOPY --link --from=xv /usr/src/xv-cache /\nCOPY --link --from=nv-codec-headers /usr/src/nv-codec-headers-cache /\n\nRUN git clone -b n6.1.1 --depth=1 https://github.com/FFmpeg/FFmpeg.git \\\n\t&& cd FFmpeg \\\n\t&& ./configure \\\n\t\t--extra-cflags=\"-fno-lto -DCONFIG_SAFE_BITSTREAM_READER=1\" \\\n\t\t--extra-cxxflags=\"-fno-lto -DCONFIG_SAFE_BITSTREAM_READER=1\" \\\n\t\t--extra-ldflags=\"-Wl,--push-state,--no-as-needed,-lstdc++,--pop-state\" \\\n\t\t--disable-debug \\\n\t\t--disable-programs \\\n\t\t--disable-doc \\\n\t\t--disable-network \\\n\t\t--disable-autodetect \\\n\t\t--disable-everything \\\n{%- if MINSIZE %}\n\t\t--enable-small \\\n{%- endif %}\n\t\t--enable-libdav1d \\\n\t\t--enable-libopenh264 \\\n\t\t--enable-libopus \\\n\t\t--enable-libvpx \\\n\t\t--enable-vaapi \\\n\t\t--enable-vdpau \\\n\t\t--enable-xlib \\\n\t\t--enable-libdrm \\\n\t\t--enable-ffnvcodec \\\n\t\t--enable-nvdec \\\n\t\t--enable-cuvid \\\n\t\t--enable-protocol=file \\\n\t\t--enable-hwaccel=av1_vaapi \\\n\t\t--enable-hwaccel=av1_nvdec \\\n\t\t--enable-hwaccel=h264_vaapi \\\n\t\t--enable-hwaccel=h264_vdpau \\\n\t\t--enable-hwaccel=h264_nvdec \\\n\t\t--enable-hwaccel=hevc_vaapi \\\n\t\t--enable-hwaccel=hevc_vdpau \\\n\t\t--enable-hwaccel=hevc_nvdec \\\n\t\t--enable-hwaccel=mpeg2_vaapi \\\n\t\t--enable-hwaccel=mpeg2_vdpau \\\n\t\t--enable-hwaccel=mpeg2_nvdec \\\n\t\t--enable-hwaccel=mpeg4_vaapi \\\n\t\t--enable-hwaccel=mpeg4_vdpau \\\n\t\t--enable-hwaccel=mpeg4_nvdec \\\n\t\t--enable-hwaccel=vp8_vaapi \\\n\t\t--enable-hwaccel=vp8_nvdec \\\n\t\t--enable-decoder=aac \\\n\t\t--enable-decoder=aac_fixed \\\n\t\t--enable-decoder=aac_latm \\\n\t\t--enable-decoder=aasc \\\n\t\t--enable-decoder=ac3 \\\n\t\t--enable-decoder=alac \\\n\t\t--enable-decoder=av1 \\\n\t\t--enable-decoder=av1_cuvid \\\n\t\t--enable-decoder=eac3 \\\n\t\t--enable-decoder=flac \\\n\t\t--enable-decoder=gif \\\n\t\t--enable-decoder=h264 \\\n\t\t--enable-decoder=hevc \\\n\t\t--enable-decoder=libdav1d \\\n\t\t--enable-decoder=libvpx_vp8 \\\n\t\t--enable-decoder=libvpx_vp9 \\\n\t\t--enable-decoder=mp1 \\\n\t\t--enable-decoder=mp1float \\\n\t\t--enable-decoder=mp2 \\\n\t\t--enable-decoder=mp2float \\\n\t\t--enable-decoder=mp3 \\\n\t\t--enable-decoder=mp3adu \\\n\t\t--enable-decoder=mp3adufloat \\\n\t\t--enable-decoder=mp3float \\\n\t\t--enable-decoder=mp3on4 \\\n\t\t--enable-decoder=mp3on4float \\\n\t\t--enable-decoder=mpeg4 \\\n\t\t--enable-decoder=msmpeg4v2 \\\n\t\t--enable-decoder=msmpeg4v3 \\\n\t\t--enable-decoder=opus \\\n\t\t--enable-decoder=pcm_alaw \\\n\t\t--enable-decoder=pcm_f32be \\\n\t\t--enable-decoder=pcm_f32le \\\n\t\t--enable-decoder=pcm_f64be \\\n\t\t--enable-decoder=pcm_f64le \\\n\t\t--enable-decoder=pcm_lxf \\\n\t\t--enable-decoder=pcm_mulaw \\\n\t\t--enable-decoder=pcm_s16be \\\n\t\t--enable-decoder=pcm_s16be_planar \\\n\t\t--enable-decoder=pcm_s16le \\\n\t\t--enable-decoder=pcm_s16le_planar \\\n\t\t--enable-decoder=pcm_s24be \\\n\t\t--enable-decoder=pcm_s24daud \\\n\t\t--enable-decoder=pcm_s24le \\\n\t\t--enable-decoder=pcm_s24le_planar \\\n\t\t--enable-decoder=pcm_s32be \\\n\t\t--enable-decoder=pcm_s32le \\\n\t\t--enable-decoder=pcm_s32le_planar \\\n\t\t--enable-decoder=pcm_s64be \\\n\t\t--enable-decoder=pcm_s64le \\\n\t\t--enable-decoder=pcm_s8 \\\n\t\t--enable-decoder=pcm_s8_planar \\\n\t\t--enable-decoder=pcm_u16be \\\n\t\t--enable-decoder=pcm_u16le \\\n\t\t--enable-decoder=pcm_u24be \\\n\t\t--enable-decoder=pcm_u24le \\\n\t\t--enable-decoder=pcm_u32be \\\n\t\t--enable-decoder=pcm_u32le \\\n\t\t--enable-decoder=pcm_u8 \\\n\t\t--enable-decoder=pcm_zork \\\n\t\t--enable-decoder=vorbis \\\n\t\t--enable-decoder=vp8 \\\n\t\t--enable-decoder=wavpack \\\n\t\t--enable-decoder=wmalossless \\\n\t\t--enable-decoder=wmapro \\\n\t\t--enable-decoder=wmav1 \\\n\t\t--enable-decoder=wmav2 \\\n\t\t--enable-decoder=wmavoice \\\n\t\t--enable-encoder=aac \\\n\t\t--enable-encoder=libopenh264 \\\n\t\t--enable-encoder=libopus \\\n\t\t--enable-encoder=pcm_s16le \\\n\t\t--enable-filter=atempo \\\n\t\t--enable-parser=aac \\\n\t\t--enable-parser=aac_latm \\\n\t\t--enable-parser=flac \\\n\t\t--enable-parser=gif \\\n\t\t--enable-parser=h264 \\\n\t\t--enable-parser=hevc \\\n\t\t--enable-parser=mpeg4video \\\n\t\t--enable-parser=mpegaudio \\\n\t\t--enable-parser=opus \\\n\t\t--enable-parser=vorbis \\\n\t\t--enable-demuxer=aac \\\n\t\t--enable-demuxer=flac \\\n\t\t--enable-demuxer=gif \\\n\t\t--enable-demuxer=h264 \\\n\t\t--enable-demuxer=hevc \\\n\t\t--enable-demuxer=matroska \\\n\t\t--enable-demuxer=m4v \\\n\t\t--enable-demuxer=mov \\\n\t\t--enable-demuxer=mp3 \\\n\t\t--enable-demuxer=ogg \\\n\t\t--enable-demuxer=wav \\\n\t\t--enable-muxer=mp4 \\\n\t\t--enable-muxer=ogg \\\n\t\t--enable-muxer=opus \\\n\t\t--enable-muxer=wav \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/ffmpeg-cache install \\\n\t&& cd .. \\\n\t&& rm -rf ffmpeg\n\nFROM builder AS pipewire\nRUN git clone -b 0.3.62 --depth=1 https://github.com/PipeWire/pipewire.git \\\n\t&& cd pipewire \\\n\t&& meson build \\\n\t\t--buildtype=plain \\\n\t\t-Dtests=disabled \\\n\t\t-Dexamples=disabled \\\n\t\t-Dspa-plugins=disabled \\\n\t\t-Dsession-managers= \\\n\t&& meson compile -C build \\\n\t&& DESTDIR=/usr/src/pipewire-cache meson install -C build \\\n\t&& cd .. \\\n\t&& rm -rf pipewire\n\nFROM builder AS openal\nCOPY --link --from=pipewire /usr/src/pipewire-cache /\n\nRUN git clone -b 1.24.3 --depth=1 https://github.com/kcat/openal-soft.git \\\n\t&& cd openal-soft \\\n\t&& cmake -B build . \\\n\t\t-DLIBTYPE:STRING=STATIC \\\n\t\t-DALSOFT_EXAMPLES=OFF \\\n\t\t-DALSOFT_UTILS=OFF \\\n\t\t-DALSOFT_INSTALL_CONFIG=OFF \\\n\t&& cmake --build build \\\n\t&& DESTDIR=/usr/src/openal-cache cmake --install build \\\n\t&& cd .. \\\n\t&& rm -rf openal-soft\n\nFROM builder AS openssl\nRUN git clone -b openssl-3.2.1 --depth=1 https://github.com/openssl/openssl.git \\\n\t&& cd openssl \\\n\t&& ./config \\\n\t\t--openssldir=/etc/ssl \\\n\t\tno-shared \\\n\t\tno-tests \\\n\t\tno-dso \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/openssl-cache install_sw \\\n\t&& cd .. \\\n\t&& rm -rf openssl\n\nFROM builder AS xkbcommon\nCOPY --link --from=xcb /usr/src/xcb-cache /\n\nRUN git clone -b xkbcommon-1.6.0 --depth=1 https://github.com/xkbcommon/libxkbcommon.git \\\n\t&& cd libxkbcommon \\\n\t&& meson build \\\n\t\t--buildtype=plain \\\n\t\t--default-library=static \\\n\t\t-Denable-docs=false \\\n\t\t-Denable-wayland=false \\\n\t\t-Denable-xkbregistry=false \\\n\t\t-Dxkb-config-root=/usr/share/X11/xkb \\\n\t\t-Dxkb-config-extra-path=/etc/xkb \\\n\t\t-Dx-locale-root=/usr/share/X11/locale \\\n\t&& meson compile -C build \\\n\t&& DESTDIR=/usr/src/xkbcommon-cache meson install -C build \\\n\t&& cd .. \\\n\t&& rm -rf libxkbcommon\n\nFROM patches AS qt\nCOPY --link --from=zlib /usr/src/zlib-cache /\nCOPY --link --from=lcms2 /usr/src/lcms2-cache /\nCOPY --link --from=mozjpeg /usr/src/mozjpeg-cache /\nCOPY --link --from=webp /usr/src/webp-cache /\nCOPY --link --from=xcb /usr/src/xcb-cache /\nCOPY --link --from=xcb-wm /usr/src/xcb-wm-cache /\nCOPY --link --from=xcb-util /usr/src/xcb-util-cache /\nCOPY --link --from=xcb-image /usr/src/xcb-image-cache /\nCOPY --link --from=xcb-keysyms /usr/src/xcb-keysyms-cache /\nCOPY --link --from=xcb-render-util /usr/src/xcb-render-util-cache /\nCOPY --link --from=xcb-cursor /usr/src/xcb-cursor-cache /\nCOPY --link --from=openssl /usr/src/openssl-cache /\nCOPY --link --from=xkbcommon /usr/src/xkbcommon-cache /\n\nENV QT=6.11.0\nRUN git clone -b v$QT --depth=1 https://github.com/qt/qt5.git \\\n\t&& cd qt5 \\\n\t&& git submodule update --init --recursive --depth=1 qtbase qtdeclarative qtwayland qtimageformats qtsvg qtshadertools \\\n\t&& cd qtbase \\\n\t&& find ../../patches/qtbase_$QT -type f -print0 | sort -z | xargs -r0 git apply \\\n\t&& cd ../qtwayland \\\n\t&& find ../../patches/qtwayland_$QT -type f -print0 | sort -z | xargs -r0 git apply \\\n\t&& cd .. \\\n\t&& cmake -B build . \\\n\t\t-DCMAKE_INSTALL_PREFIX=/usr/local \\\n\t\t-DBUILD_SHARED_LIBS=OFF \\\n{%- if ASAN %}\n\t\t-DFEATURE_sanitize_address=ON \\\n{%- endif %}\n\t\t-DQT_QPA_PLATFORMS=\"wayland;xcb\" \\\n\t\t-DINPUT_libpng=qt \\\n\t\t-DINPUT_harfbuzz=qt \\\n\t\t-DINPUT_pcre=qt \\\n\t\t-DFEATURE_icu=OFF \\\n\t\t-DFEATURE_xcb_sm=OFF \\\n\t\t-DFEATURE_eglfs=OFF \\\n\t\t-DINPUT_dbus=runtime \\\n\t\t-DINPUT_openssl=linked \\\n\t&& cmake --build build \\\n\t&& DESTDIR=/usr/src/qt-cache cmake --install build \\\n\t&& cd .. \\\n\t&& rm -rf qt5\n\nFROM builder AS boost\nRUN curl -sSL https://archives.boost.io/release/1.90.0/source/boost_1_90_0.tar.gz | tar -xz \\\n\t&& cd boost_* \\\n\t&& ./bootstrap.sh --prefix=/usr/src/boost-cache/usr/local --with-libraries=regex \\\n\t&& ./b2 release link=static install \\\n\t&& cd .. \\\n\t&& rm -rf boost_*\n\nFROM builder AS breakpad\nRUN git clone -b v2024.02.16 --depth=1 https://chromium.googlesource.com/breakpad/breakpad.git \\\n\t&& cd breakpad \\\n\t&& git clone -b v2024.02.01 --depth=1 https://chromium.googlesource.com/linux-syscall-support.git src/third_party/lss \\\n\t&& CFLAGS=\"$CFLAGS -fno-lto\" CXXFLAGS=\"$CXXFLAGS -fno-lto\" ./configure \\\n\t&& make -j$(nproc) \\\n\t&& make DESTDIR=/usr/src/breakpad-cache install \\\n\t&& cd .. \\\n\t&& rm -rf breakpad\n\nFROM builder AS webrtc\nCOPY --link --from=zlib /usr/src/zlib-cache /\nCOPY --link --from=mozjpeg /usr/src/mozjpeg-cache /\nCOPY --link --from=opus /usr/src/opus-cache /\nCOPY --link --from=openh264 /usr/src/openh264-cache /\nCOPY --link --from=dav1d /usr/src/dav1d-cache /\nCOPY --link --from=vpx /usr/src/vpx-cache /\nCOPY --link --from=ffmpeg /usr/src/ffmpeg-cache /\nCOPY --link --from=openssl /usr/src/openssl-cache /\nCOPY --link --from=xext /usr/src/xext-cache /\nCOPY --link --from=xfixes /usr/src/xfixes-cache /\nCOPY --link --from=xtst /usr/src/xtst-cache /\nCOPY --link --from=xrandr /usr/src/xrandr-cache /\nCOPY --link --from=xrender /usr/src/xrender-cache /\nCOPY --link --from=xdamage /usr/src/xdamage-cache /\nCOPY --link --from=xcomposite /usr/src/xcomposite-cache /\nCOPY --link --from=pipewire /usr/src/pipewire-cache /\n\n# Shallow clone on a specific commit.\nRUN git init tg_owt \\\n\t&& cd tg_owt \\\n\t&& git remote add origin https://github.com/desktop-app/tg_owt.git \\\n\t&& git fetch --depth=1 origin 5c5c71258777d0196dbb3a09cc37d2f56ead28ab \\\n\t&& git reset --hard FETCH_HEAD \\\n\t&& git submodule update --init --recursive --depth=1 \\\n\t&& cmake -B build . -DTG_OWT_DLOPEN_PIPEWIRE=ON \\\n\t&& cmake --build build \\\n\t&& DESTDIR=/usr/src/webrtc-cache cmake --install build \\\n\t&& cd .. \\\n\t&& rm -rf tg_owt\n\nFROM builder AS ada\nRUN git clone -b v3.2.4 --depth=1 https://github.com/ada-url/ada.git \\\n\t&& cd ada \\\n\t&& cmake -B build . \\\n        -D ADA_TESTING=OFF \\\n        -D ADA_TOOLS=OFF \\\n        -D ADA_INCLUDE_URL_PATTERN=OFF \\\n\t&& cmake --build build \\\n\t&& DESTDIR=/usr/src/ada-cache cmake --install build \\\n\t&& cd .. \\\n\t&& rm -rf ada\n\nFROM builder AS tde2e\nCOPY --link --from=zlib /usr/src/zlib-cache /\nCOPY --link --from=openssl /usr/src/openssl-cache /\n\n# Shallow clone on a specific commit.\nRUN git init tde2e \\\n\t&& cd tde2e \\\n\t&& git remote add origin https://github.com/tdlib/td.git \\\n\t&& git fetch --depth=1 origin 51743dfd01dff6179e2d8f7095729caa4e2222e9 \\\n\t&& git reset --hard FETCH_HEAD \\\n\t&& cmake -B build . -DTD_E2E_ONLY=ON \\\n\t&& cmake --build build \\\n\t&& DESTDIR=/usr/src/tde2e-cache cmake --install build \\\n\t&& cd .. \\\n\t&& rm -rf tde2e\n\nFROM builder\nCOPY --link --from=zlib /usr/src/zlib-cache /\nCOPY --link --from=xz /usr/src/xz-cache /\nCOPY --link --from=protobuf /usr/src/protobuf-cache /\nCOPY --link --from=lcms2 /usr/src/lcms2-cache /\nCOPY --link --from=brotli /usr/src/brotli-cache /\nCOPY --link --from=highway /usr/src/highway-cache /\nCOPY --link --from=mozjpeg /usr/src/mozjpeg-cache /\nCOPY --link --from=opus /usr/src/opus-cache /\nCOPY --link --from=dav1d /usr/src/dav1d-cache /\nCOPY --link --from=openh264 /usr/src/openh264-cache /\nCOPY --link --from=de265 /usr/src/de265-cache /\nCOPY --link --from=vpx /usr/src/vpx-cache /\nCOPY --link --from=webp /usr/src/webp-cache /\nCOPY --link --from=avif /usr/src/avif-cache /\nCOPY --link --from=heif /usr/src/heif-cache /\nCOPY --link --from=jxl /usr/src/jxl-cache /\nCOPY --link --from=rnnoise /usr/src/rnnoise-cache /\nCOPY --link --from=xcb /usr/src/xcb-cache /\nCOPY --link --from=xcb-wm /usr/src/xcb-wm-cache /\nCOPY --link --from=xcb-util /usr/src/xcb-util-cache /\nCOPY --link --from=xcb-image /usr/src/xcb-image-cache /\nCOPY --link --from=xcb-keysyms /usr/src/xcb-keysyms-cache /\nCOPY --link --from=xcb-render-util /usr/src/xcb-render-util-cache /\nCOPY --link --from=xcb-cursor /usr/src/xcb-cursor-cache /\nCOPY --link --from=xext /usr/src/xext-cache /\nCOPY --link --from=xfixes /usr/src/xfixes-cache /\nCOPY --link --from=xv /usr/src/xv-cache /\nCOPY --link --from=xtst /usr/src/xtst-cache /\nCOPY --link --from=xrandr /usr/src/xrandr-cache /\nCOPY --link --from=xrender /usr/src/xrender-cache /\nCOPY --link --from=xdamage /usr/src/xdamage-cache /\nCOPY --link --from=xcomposite /usr/src/xcomposite-cache /\nCOPY --link --from=ffmpeg /usr/src/ffmpeg-cache /\nCOPY --link --from=openal /usr/src/openal-cache /\nCOPY --link --from=openssl /usr/src/openssl-cache /\nCOPY --link --from=xkbcommon /usr/src/xkbcommon-cache /\nCOPY --link --from=qt /usr/src/qt-cache /\nCOPY --link --from=boost /usr/src/boost-cache /\nCOPY --link --from=breakpad /usr/src/breakpad-cache /\nCOPY --link --from=webrtc /usr/src/webrtc-cache /\nCOPY --link --from=ada /usr/src/ada-cache /\nCOPY --link --from=tde2e /usr/src/tde2e-cache /\n\nCOPY --link --from=patches /usr/src/patches patches\nRUN patch -p1 -d /usr/lib64/gobject-introspection -i $PWD/patches/gobject-introspection.patch && rm -rf patches\n\nWORKDIR /usr/src/tdesktop\nUSER user\nVOLUME [ \"/usr/src/tdesktop\" ]\nCMD [ \"/usr/src/tdesktop/Telegram/build/docker/centos_env/build.sh\" ]\n"
  },
  {
    "path": "Telegram/build/docker/centos_env/build.sh",
    "content": "#!/bin/bash\nset -e\n\ncd Telegram\n./configure.sh \"$@\"\ncmake --build ../out --config \"${CONFIG:-MinSizeRel}\"\n"
  },
  {
    "path": "Telegram/build/docker/centos_env/centos_env/__init__.py",
    "content": "\n"
  },
  {
    "path": "Telegram/build/docker/centos_env/gen_dockerfile.py",
    "content": "#!/usr/bin/env python3\nfrom os import environ\nfrom os.path import dirname\nfrom jinja2 import Environment, FileSystemLoader\n\ndef checkEnv(envName, defaultValue):\n    if isinstance(defaultValue, bool):\n        return bool(len(environ[envName])) if envName in environ else defaultValue\n    return environ[envName] if envName in environ else defaultValue\n\ndef main():\n    print(Environment(loader=FileSystemLoader(dirname(__file__))).get_template(\"Dockerfile\").render(\n        DEBUG=checkEnv(\"DEBUG\", True),\n        MINSIZE=checkEnv(\"MINSIZE\", True),\n        LTO=checkEnv(\"LTO\", True),\n        ASAN=checkEnv(\"ASAN\", False),\n        JOBS=checkEnv(\"JOBS\", \"\"),\n    ))\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "Telegram/build/docker/centos_env/pyproject.toml",
    "content": "[tool.poetry]\nname = \"centos_env\"\nversion = \"0.1.0\"\ndescription = \"\"\nauthors = []\n\n[tool.poetry.dependencies]\npython = \"^3.7\"\nJinja2 = \"^3.1.6\"\n\n[tool.poetry.dev-dependencies]\n\n[tool.poetry.scripts]\ngen_dockerfile = \"gen_dockerfile:main\"\n\n[build-system]\nrequires = [\"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "Telegram/build/docker/centos_env/run.sh",
    "content": "set -e\nFullExecPath=$PWD\npushd `dirname $0` > /dev/null\nFullScriptPath=`pwd`\npopd > /dev/null\n\nif [ ! -d \"$FullScriptPath/../../../../../DesktopPrivate\" ]; then\n  echo \"\"\n  echo \"This script is for building the production version of Telegram Desktop.\"\n  echo \"\"\n  echo \"For building custom versions please visit the build instructions page at:\"\n  echo \"https://github.com/telegramdesktop/tdesktop/#build-instructions\"\n  exit\nfi\n\nCommand=\"$1\"\nif [ \"$Command\" == \"\" ]; then\n  Command=\"bash\"\nfi\n\ndocker run -it --rm --cpus=8 --memory=22g -u $(id -u) -v $HOME/Telegram/DesktopPrivate:/usr/src/DesktopPrivate -v $HOME/Telegram/tdesktop:/usr/src/tdesktop tdesktop:centos_env $Command\n"
  },
  {
    "path": "Telegram/build/mac_store_upload.sh",
    "content": "set -e\nFullExecPath=$PWD\npushd `dirname $0` > /dev/null\nFullScriptPath=`pwd`\npopd > /dev/null\n\nif [ ! -d \"$FullScriptPath/../../../DesktopPrivate\" ]; then\n  echo \"\"\n  echo \"This script is for building the production version of Telegram Desktop.\"\n  echo \"\"\n  echo \"For building custom versions please visit the build instructions page at:\"\n  echo \"https://github.com/telegramdesktop/tdesktop/#build-instructions\"\n  exit\nfi\n\nError () {\n  cd $FullExecPath\n  echo \"$1\"\n  exit 1\n}\n\nif [ ! -f \"$FullScriptPath/target\" ]; then\n  Error \"Build target not found!\"\nfi\n\nwhile IFS='' read -r line || [[ -n \"$line\" ]]; do\n  BuildTarget=\"$line\"\ndone < \"$FullScriptPath/target\"\n\nwhile IFS='' read -r line || [[ -n \"$line\" ]]; do\n  set $line\n  eval $1=\"$2\"\ndone < \"$FullScriptPath/version\"\n\nVersionForPacker=\"$AppVersion\"\nif [ \"$AlphaVersion\" != \"0\" ]; then\n  AppVersion=\"$AlphaVersion\"\n  AppVersionStrFull=\"${AppVersionStr}_${AlphaVersion}\"\n  AlphaBetaParam=\"-alpha $AlphaVersion\"\n  AlphaKeyFile=\"talpha_${AppVersion}_key\"\nelif [ \"$BetaChannel\" == \"0\" ]; then\n  AppVersionStrFull=\"$AppVersionStr\"\n  AlphaBetaParam=''\nelse\n  AppVersionStrFull=\"$AppVersionStr.beta\"\n  AlphaBetaParam='-beta'\nfi\n\necho \"\"\nHomePath=\"$FullScriptPath/..\"\nif [ \"$BuildTarget\" != \"macstore\" ]; then\n  Error \"Invalid target!\"\nfi\nif [ \"$AlphaVersion\" != \"0\" ]; then\n  Error \"Can't upload macstore alpha version!\"\nfi\n\necho \"Uploading version $AppVersionStrFull to Mac App Store..\"\nProjectPath=\"$HomePath/../out\"\nReleasePath=\"$ProjectPath/Release\"\nBinaryName=\"Telegram Lite\"\nDeployPath=\"$ReleasePath/deploy/$AppVersionStrMajor/$AppVersionStrFull\"\nPackageFile=\"$DeployPath/$BinaryName.pkg\"\n\nset +e\nxcrun altool --upload-app --username \"$AC_USERNAME\" --password \"@keychain:AC_PASSWORD\" -t macOS -f \"$PackageFile\"\nset -e\n\necho -en \"\\007\";\nsleep 1;\necho -en \"\\007\";\nsleep 1;\necho -en \"\\007\";\n"
  },
  {
    "path": "Telegram/build/prepare/linux.sh",
    "content": "#!/bin/bash\n\nset -e\nFullExecPath=$PWD\npushd `dirname $0` > /dev/null\nFullScriptPath=`pwd`\npopd > /dev/null\n\n\ncd $FullScriptPath/../docker/centos_env\npoetry install\npoetry run gen_dockerfile | DOCKER_BUILDKIT=1 docker build -t tdesktop:centos_env -\ncd $FullExecPath\n"
  },
  {
    "path": "Telegram/build/prepare/mac.sh",
    "content": "set -e\nFullExecPath=$PWD\npushd `dirname $0` > /dev/null\nFullScriptPath=`pwd`\npopd > /dev/null\n\npython3 $FullScriptPath/prepare.py \"$@\"\n"
  },
  {
    "path": "Telegram/build/prepare/prepare.py",
    "content": "import os, sys, pprint, re, json, pathlib, hashlib, subprocess, glob, tempfile\n\nexecutePath = os.getcwd()\nsys.dont_write_bytecode = True\nscriptPath = os.path.dirname(os.path.realpath(__file__))\nsys.path.append(scriptPath + '/..')\nimport qt_version\n\ndef finish(code):\n    global executePath\n    os.chdir(executePath)\n    sys.exit(code)\n\ndef error(text):\n    print('[ERROR] ' + text)\n    finish(1)\n\ndef nativeToolsError():\n    error('Make sure to run from Native Tools Command Prompt.')\n\nwin = (sys.platform == 'win32')\nmac = (sys.platform == 'darwin')\n\nif win and not 'Platform' in os.environ:\n    nativeToolsError()\n\nwin32 = win and (os.environ['Platform'] == 'x86')\nwin64 = win and (os.environ['Platform'] == 'x64')\nwinarm = win and (os.environ['Platform'] == 'arm64')\n\narch = ''\nif win32:\n    arch = 'x86'\nelif win64:\n    arch = 'x64'\nelif winarm:\n    arch = 'arm'\nif not qt_version.resolve(arch):\n    error('Usupported platform.')\n\nqt = os.environ.get('QT')\n\nif win and not 'COMSPEC' in os.environ:\n    error('COMSPEC environment variable is not set.')\n\nif win and not win32 and not win64 and not winarm:\n    nativeToolsError()\n\nos.chdir(scriptPath + '/../../../..')\n\npathSep = ';' if win else ':'\nlibsLoc = 'Libraries' if not win64 else (os.path.join('Libraries', 'win64'))\nkeysLoc = 'cache_keys'\n\nrootDir = os.getcwd()\nlibsDir = os.path.realpath(os.path.join(rootDir, libsLoc))\nthirdPartyDir = os.path.realpath(os.path.join(rootDir, 'ThirdParty'))\nusedPrefix = os.path.realpath(os.path.join(libsDir, 'local'))\n\noptionsList = [\n    'qt6',\n    'skip-release',\n    'build-stackwalk',\n]\noptions = []\nrunCommand = []\ncustomRunCommand = False\nfor arg in sys.argv[1:]:\n    if customRunCommand:\n        runCommand.append(arg)\n    if arg in optionsList:\n        options.append(arg)\n    elif arg == 'run':\n        customRunCommand = True\n    elif arg == 'shell':\n        customRunCommand = True\n        runCommand.append('shell')\n\nif not os.path.isdir(os.path.join(libsDir, keysLoc)):\n    pathlib.Path(os.path.join(libsDir, keysLoc)).mkdir(parents=True, exist_ok=True)\nif not os.path.isdir(os.path.join(thirdPartyDir, keysLoc)):\n    pathlib.Path(os.path.join(thirdPartyDir, keysLoc)).mkdir(parents=True, exist_ok=True)\n\npathPrefixes = [\n    'ThirdParty\\\\msys64\\\\mingw64\\\\bin',\n    'ThirdParty\\\\jom',\n    'ThirdParty\\\\gyp',\n] if win else [\n    'ThirdParty/gyp',\n]\npathPrefix = ''\nfor singlePrefix in pathPrefixes:\n    pathPrefix = pathPrefix + os.path.join(rootDir, singlePrefix) + pathSep\n\nenvironment = {\n    'USED_PREFIX': usedPrefix,\n    'ROOT_DIR': rootDir,\n    'LIBS_DIR': libsDir,\n    'THIRDPARTY_DIR': thirdPartyDir,\n    'PATH_PREFIX': pathPrefix,\n    'CMAKE_GENERATOR': 'Ninja Multi-Config',\n}\nif (win32):\n    environment.update({\n        'SPECIAL_TARGET': 'win',\n        'X8664': 'x86',\n    })\nelif (win64):\n    environment.update({\n        'SPECIAL_TARGET': 'win64',\n        'X8664': 'x64',\n    })\nelif (winarm):\n    environment.update({\n        'SPECIAL_TARGET': 'winarm',\n        'X8664': 'ARM64',\n    })\nelif (mac):\n    environment.update({\n        'SPECIAL_TARGET': 'mac',\n        'MAKE_THREADS_CNT': '-j' + str(os.cpu_count()),\n        'MACOSX_DEPLOYMENT_TARGET': '10.13',\n        'UNGUARDED': '-Werror=unguarded-availability-new',\n        'MIN_VER': '-mmacosx-version-min=10.13',\n        'CMAKE_GENERATOR': 'Ninja',\n    })\n\nignoreInCacheForThirdParty = [\n    'USED_PREFIX',\n    'LIBS_DIR',\n    'SPECIAL_TARGET',\n    'X8664',\n]\n\nenvironmentKeyString = ''\nenvForThirdPartyKeyString = ''\nfor key in environment:\n    part = key + '=' + environment[key] + ';'\n    environmentKeyString += part\n    if not key in ignoreInCacheForThirdParty:\n        envForThirdPartyKeyString += part\nenvironmentKey = hashlib.sha1(environmentKeyString.encode('utf-8')).hexdigest()\nenvForThirdPartyKey = hashlib.sha1(envForThirdPartyKeyString.encode('utf-8')).hexdigest()\n\nmodifiedEnv = os.environ.copy()\nfor key in environment:\n    modifiedEnv[key] = environment[key]\n\nmodifiedEnv['PATH'] = environment['PATH_PREFIX'] + modifiedEnv['PATH']\n\ndef computeFileHash(path):\n    sha1 = hashlib.sha1()\n    with open(path, 'rb') as f:\n        while True:\n            data = f.read(256 * 1024)\n            if not data:\n                break\n            sha1.update(data)\n    return sha1.hexdigest()\n\ndef computeCacheKey(stage):\n    if (stage['location'] == 'ThirdParty'):\n        envKey = envForThirdPartyKey\n    else:\n        envKey = environmentKey\n    objects = [\n        envKey,\n        stage['location'],\n        stage['name'],\n        stage['version'],\n        stage['commands']\n    ]\n    for pattern in stage['dependencies']:\n        pathlist = glob.glob(os.path.join(libsDir, pattern))\n        items = [pattern]\n        if len(pathlist) == 0:\n            pathlist = glob.glob(os.path.join(thirdPartyDir, pattern))\n        if len(pathlist) == 0:\n            error('Nothing found: ' + pattern)\n        for path in pathlist:\n            if not os.path.exists(path):\n                error('Not found: ' + path)\n            items.append(computeFileHash(path))\n        objects.append(':'.join(items))\n    return hashlib.sha1(';'.join(objects).encode('utf-8')).hexdigest()\n\ndef keyPath(stage):\n    return os.path.join(stage['directory'], keysLoc, stage['name'])\n\ndef checkCacheKey(stage):\n    if not 'key' in stage:\n        error('Key not set in stage: ' + stage['name'])\n    key = keyPath(stage)\n    if not os.path.exists(os.path.join(stage['directory'], stage['name'])):\n        return 'NotFound'\n    if not os.path.exists(key):\n        return 'Stale'\n    with open(key, 'r') as file:\n        return 'Good' if (file.read() == stage['key']) else 'Stale'\n\ndef clearCacheKey(stage):\n    key = keyPath(stage)\n    if os.path.exists(key):\n        os.remove(key)\n\ndef writeCacheKey(stage):\n    if not 'key' in stage:\n        error('Key not set in stage: ' + stage['name'])\n    key = keyPath(stage)\n    with open(key, 'w') as file:\n        file.write(stage['key'])\n\nstages = []\n\ndef removeDir(folder):\n    if win:\n        return 'if exist ' + folder + ' rmdir /Q /S ' + folder + '\\nif exist ' + folder + ' exit /b 1'\n    return 'rm -rf ' + folder\n\ndef setVar(key, multilineValue):\n    singlelineValue = ' '.join(multilineValue.replace('\\n', '').split());\n    if win:\n        return 'SET \"' + key + '=' + singlelineValue + '\"';\n    return key + '=\"' + singlelineValue + '\"';\n\ndef filterByPlatform(commands):\n    commands = commands.split('\\n')\n    result = ''\n    dependencies = []\n    version = '0'\n    skip = False\n    for command in commands:\n        m = re.match(r'(!?)([a-z0-9_]+):', command)\n        if m and m.group(2) != 'depends' and m.group(2) != 'version':\n            scopes = m.group(2).split('_')\n            inscope = 'common' in scopes\n            if win and 'win' in scopes:\n                inscope = True\n            if win32 and 'win32' in scopes:\n                inscope = True\n            if win64 and 'win64' in scopes:\n                inscope = True\n            if winarm and 'winarm' in scopes:\n                inscope = True\n            if mac and 'mac' in scopes:\n                inscope = True\n            # if linux and 'linux' in scopes:\n            #     inscope = True\n            if 'release' in scopes:\n                if 'skip-release' in options:\n                    inscope = False\n                elif len(scopes) == 1:\n                    continue\n            skip = inscope if m.group(1) == '!' else not inscope\n        elif not skip and not re.match(r'\\s*#', command):\n            if m and m.group(2) == 'version':\n                version = version + '.' + command[len(m.group(0)):].strip()\n            elif m and m.group(2) == 'depends':\n                pattern = command[len(m.group(0)):].strip()\n                dependencies.append(pattern)\n            else:\n                command = command.strip()\n                if len(command) > 0:\n                    result = result + command + '\\n'\n    return [result, dependencies, version]\n\ndef stage(name, commands, location = 'Libraries'):\n    if location == 'Libraries':\n        directory = libsDir\n    elif location == 'ThirdParty':\n        directory = thirdPartyDir\n    else:\n        error('Unknown location: ' + location)\n    [commands, dependencies, version] = filterByPlatform(commands)\n    if len(commands) > 0:\n        stages.append({\n            'name': name,\n            'location': location,\n            'directory': directory,\n            'commands': commands,\n            'version': version,\n            'dependencies': dependencies\n        })\n\ndef winFailOnEach(command):\n    commands = command.split('\\n')\n    result = ''\n    startingCommand = True\n    for command in commands:\n        command = re.sub(r'\\$([A-Za-z0-9_]+)', r'%\\1%', command)\n        if re.search(r'\\$[^<]', command):\n            error('Bad command: ' + command)\n        appendCall = startingCommand and not re.match(r'(if|for) ', command)\n        called = 'call ' + command if appendCall else command\n        result = result + called\n        if command.endswith('^'):\n            startingCommand = False\n        else:\n            startingCommand = True\n            result = result + '\\r\\nif %errorlevel% neq 0 exit /b %errorlevel%\\r\\n'\n    return result\n\ndef printCommands(commands):\n    print('---------------------------------COMMANDS-LIST----------------------------------')\n    print(commands, end='')\n    print('--------------------------------------------------------------------------------')\n\ndef run(commands):\n    printCommands(commands)\n    if win:\n        if os.path.exists(\"command.bat\"):\n            os.remove(\"command.bat\")\n        with open(\"command.bat\", 'w') as file:\n            file.write('@echo OFF\\r\\n' + winFailOnEach(commands))\n        result = subprocess.run(\"command.bat\", shell=True, env=modifiedEnv).returncode == 0\n        if result and os.path.exists(\"command.bat\"):\n            os.remove(\"command.bat\")\n        return result\n    elif re.search(r'\\%', commands):\n        error('Bad command: ' + commands)\n    else:\n        return subprocess.run(\"set -e\\n\" + commands, shell=True, env=modifiedEnv).returncode == 0\n\n# Thanks https://stackoverflow.com/a/510364\nclass _Getch:\n    \"\"\"Gets a single character from standard input.  Does not echo to the\nscreen.\"\"\"\n    def __init__(self):\n        try:\n            self.impl = _GetchWindows()\n        except ImportError:\n            self.impl = _GetchUnix()\n\n    def __call__(self): return self.impl()\n\nclass _GetchUnix:\n    def __init__(self):\n        import tty, sys\n\n    def __call__(self):\n        import sys, tty, termios\n        fd = sys.stdin.fileno()\n        old_settings = termios.tcgetattr(fd)\n        try:\n            tty.setraw(sys.stdin.fileno())\n            ch = sys.stdin.read(1)\n        finally:\n            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)\n        return ch\n\nclass _GetchWindows:\n    def __init__(self):\n        import msvcrt\n\n    def __call__(self):\n        import msvcrt\n        return msvcrt.getch().decode('ascii')\n\ngetch = _Getch()\n\ndef runStages():\n    onlyStages = []\n    rebuildStale = False\n    for arg in sys.argv[1:]:\n        if arg in options:\n            continue\n        elif arg == 'silent':\n            rebuildStale = True\n            continue\n        found = False\n        for stage in stages:\n            if stage['name'] == arg:\n                onlyStages.append(arg)\n                found = True\n                break\n        if not found:\n            error('Unknown argument: ' + arg)\n    count = len(stages)\n    index = 0\n    for stage in stages:\n        if len(onlyStages) > 0 and not stage['name'] in onlyStages:\n            continue\n        index = index + 1\n        version = ('#' + str(stage['version'])) if (stage['version'] != '0') else ''\n        prefix = '[' + str(index) + '/' + str(count) + '](' + stage['location'] + '/' + stage['name'] + version + ')'\n        print(prefix + ': ', end = '', flush=True)\n        stage['key'] = computeCacheKey(stage)\n        commands = removeDir(stage['name']) + '\\n' + stage['commands']\n        checkResult = 'Forced' if len(onlyStages) > 0 else checkCacheKey(stage)\n        if checkResult == 'Good':\n            print('SKIPPING')\n            continue\n        elif checkResult == 'NotFound':\n            print('NOT FOUND, ', end='')\n        elif checkResult == 'Stale' or checkResult == 'Forced':\n            if checkResult == 'Stale':\n                print('CHANGED, ', end='')\n            if rebuildStale:\n                checkResult == 'Rebuild'\n            else:\n                print('(r)ebuild, rebuild (a)ll, (s)kip, (p)rint, (q)uit?: ', end='', flush=True)\n                while True:\n                    ch = 'r' if rebuildStale else getch()\n                    if ch == 'q':\n                        finish(0)\n                    elif ch == 'p':\n                        printCommands(commands)\n                        checkResult = 'Printed'\n                        break\n                    elif ch == 's':\n                        checkResult = 'Skip'\n                        break\n                    elif ch == 'r':\n                        checkResult = 'Rebuild'\n                        break\n                    elif ch == 'a':\n                        checkResult = 'Rebuild'\n                        rebuildStale = True\n                        break\n        if checkResult == 'Printed':\n            continue\n        if checkResult == 'Skip':\n            print('SKIPPING')\n            continue\n        clearCacheKey(stage)\n        print('BUILDING:')\n        os.chdir(stage['directory'])\n        if not run(commands):\n            print(prefix + ': FAILED')\n            finish(1)\n        writeCacheKey(stage)\n\nif customRunCommand:\n    os.chdir(executePath)\n    if len(runCommand) == 1 and runCommand[0] == 'shell':\n        print('Preparing interactive mode..')\n        if win:\n            modifiedEnv['PROMPT'] = '(prepare) $P$G'\n            subprocess.run(\"cmd.exe\", shell=True, env=modifiedEnv)\n        else:\n            prompt = '(prepare) %~ %# '\n            with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp_zshrc:\n                tmp_zshrc.write(f'export PS1=\"{prompt}\"\\n')\n                tmp_zshrc_path = tmp_zshrc.name\n            subprocess.run(['zsh', '--rcs', tmp_zshrc_path], shell=True, env=modifiedEnv)\n            os.remove(tmp_zshrc_path)\n    elif not run(' '.join(runCommand) + '\\n'):\n        print('FAILED :(')\n        finish(1)\n    finish(0)\n\nstage('patches', \"\"\"\n    git clone https://github.com/desktop-app/patches.git\n    cd patches\n    git checkout 4519c85c924b9da81f29d4aac045886f896ee479\n\"\"\")\n\nstage('msys64', \"\"\"\nwin:\n    SET PATH=%THIRDPARTY_DIR%\\\\msys64\\\\usr\\\\bin;%PATH%\n    SET CHERE_INVOKING=enabled_from_arguments\n    SET MSYS2_PATH_TYPE=inherit\n\n    powershell -Command \"iwr -OutFile ./msys64.exe https://github.com/msys2/msys2-installer/releases/download/2025-08-30/msys2-base-x86_64-20250830.sfx.exe\"\n    msys64.exe\n    del msys64.exe\n\n    bash -c \"pacman-key --init; pacman-key --populate; pacman -Syu --noconfirm\"\n    pacman -Syu --noconfirm ^\n        make ^\n        mingw-w64-x86_64-diffutils ^\n        mingw-w64-x86_64-gperf ^\n        mingw-w64-x86_64-nasm ^\n        mingw-w64-x86_64-perl ^\n        mingw-w64-x86_64-pkgconf\n\"\"\", 'ThirdParty')\n\nstage('python', \"\"\"\nversion: \"\"\" + (subprocess.run(['python', '-V'], capture_output=True, text=True, env=modifiedEnv).stdout.strip().split()[-1] if win else '0') + \"\"\"\nwin:\n    python -m venv python\n    python\\\\Scripts\\\\activate.bat\n    pip install pywin32 six meson\n    deactivate\n\"\"\", 'ThirdParty')\n\nstage('NuGet', \"\"\"\nwin:\n    mkdir NuGet\n    powershell -Command \"iwr -OutFile ./NuGet/nuget.exe https://dist.nuget.org/win-x86-commandline/latest/nuget.exe\"\n\"\"\", 'ThirdParty')\n\nstage('jom', \"\"\"\nwin:\n    powershell -Command \"iwr -OutFile ./jom.zip https://master.qt.io/official_releases/jom/jom_1_1_3.zip\"\n    powershell -Command \"Expand-Archive ./jom.zip\"\n    del jom.zip\n\"\"\", 'ThirdParty')\n\nif not mac or 'build-stackwalk' in options:\n    stage('gyp', \"\"\"\nwin:\n    git clone https://github.com/desktop-app/gyp.git\n    cd gyp\n    git checkout 5e2425c47b\nmac:\n    python3 -m pip install \\\\\n        --ignore-installed \\\\\n        --target=$THIRDPARTY_DIR/gyp \\\\\n        git+https://chromium.googlesource.com/external/gyp@master six\n\"\"\", 'ThirdParty')\n\nstage('lzma', \"\"\"\nwin:\n    git clone https://github.com/desktop-app/lzma.git\n    cd lzma\\\\C\\\\Util\\\\LzmaLib\n    msbuild -m LzmaLib.sln /property:Configuration=Debug /property:Platform=\"$X8664\"\nrelease:\n    msbuild -m LzmaLib.sln /property:Configuration=Release /property:Platform=\"$X8664\"\n\"\"\")\n\nstage('xz', \"\"\"\n!win:\n    git clone -b v5.4.5 https://github.com/tukaani-project/xz.git\n    cd xz\n    sed -i '' '\\\\@check_symbol_exists(futimens \"sys/types.h;sys/stat.h\" HAVE_FUTIMENS)@d' CMakeLists.txt\n    CFLAGS=\"$UNGUARDED\" CPPFLAGS=\"$UNGUARDED\" cmake -B build . \\\\\n        -D CMAKE_OSX_ARCHITECTURES=\"x86_64;arm64\" \\\\\n        -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX\n    cmake --build build\n    cmake --install build\n\"\"\")\n\nstage('zlib', \"\"\"\n    git clone -b v1.3.1 https://github.com/madler/zlib.git\n    cd zlib\nwin:\n    cmake . ^\n        -DCMAKE_MSVC_RUNTIME_LIBRARY=\"MultiThreaded$<$<CONFIG:Debug>:Debug>\" ^\n        -DCMAKE_POLICY_DEFAULT_CMP0091=NEW ^\n        -DCMAKE_C_FLAGS=\"/DZLIB_WINAPI\" ^\n        -DZLIB_BUILD_EXAMPLES=OFF\n    cmake --build . --config Debug\nrelease:\n    cmake --build . --config Release\nmac:\n    CFLAGS=\"$MIN_VER $UNGUARDED\" LDFLAGS=\"$MIN_VER\" ./configure \\\\\n        --static \\\\\n        --prefix=$USED_PREFIX \\\\\n        --archs=\"-arch x86_64 -arch arm64\"\n    make $MAKE_THREADS_CNT\n    make install\n\"\"\")\n\nstage('mozjpeg', \"\"\"\n    git clone -b v4.1.5 https://github.com/mozilla/mozjpeg.git\n    cd mozjpeg\nwin:\n    cmake . ^\n        -DCMAKE_POLICY_VERSION_MINIMUM=3.5 ^\n        -DWITH_JPEG8=ON ^\n        -DPNG_SUPPORTED=OFF\n    cmake --build . --config Debug\nrelease:\n    cmake --build . --config Release\nmac:\n    CFLAGS=\"-arch arm64\" cmake -B build.arm64 . \\\\\n        -D CMAKE_POLICY_VERSION_MINIMUM=3.5 \\\\\n        -D CMAKE_SYSTEM_NAME=Darwin \\\\\n        -D CMAKE_SYSTEM_PROCESSOR=arm64 \\\\\n        -D CMAKE_BUILD_TYPE=Release \\\\\n        -D CMAKE_INSTALL_PREFIX=$USED_PREFIX \\\\\n        -D WITH_JPEG8=ON \\\\\n        -D ENABLE_SHARED=OFF \\\\\n        -D PNG_SUPPORTED=OFF\n    cmake --build build.arm64\n    CFLAGS=\"-arch x86_64\" cmake -B build . \\\\\n        -D CMAKE_POLICY_VERSION_MINIMUM=3.5 \\\\\n        -D CMAKE_SYSTEM_NAME=Darwin \\\\\n        -D CMAKE_SYSTEM_PROCESSOR=x86_64 \\\\\n        -D CMAKE_BUILD_TYPE=Release \\\\\n        -D CMAKE_INSTALL_PREFIX=$USED_PREFIX \\\\\n        -D WITH_JPEG8=ON \\\\\n        -D ENABLE_SHARED=OFF \\\\\n        -D PNG_SUPPORTED=OFF\n    cmake --build build\n    lipo -create build.arm64/libjpeg.a build/libjpeg.a -output build/libjpeg.a\n    lipo -create build.arm64/libturbojpeg.a build/libturbojpeg.a -output build/libturbojpeg.a\n    cmake --install build\n\"\"\")\n\nstage('openssl3', \"\"\"\n    git clone -b openssl-3.2.1 https://github.com/openssl/openssl openssl3\n    cd openssl3\nwin32:\n    perl Configure no-shared no-tests debug-VC-WIN32 /FS\nwin64:\n    perl Configure no-shared no-tests debug-VC-WIN64A /FS\nwinarm:\n    perl Configure no-shared no-tests debug-VC-WIN64-ARM /FS\nwin:\n    jom -j%NUMBER_OF_PROCESSORS% build_libs\n    mkdir out.dbg\n    move libcrypto.lib out.dbg\n    move libssl.lib out.dbg\n    move ossl_static.pdb out.dbg\nrelease:\n    move out.dbg\\\\ossl_static.pdb out.dbg\\\\ossl_static\n    jom clean\n    move out.dbg\\\\ossl_static out.dbg\\\\ossl_static.pdb\nwin32_release:\n    perl Configure no-shared no-tests VC-WIN32 /FS\nwin64_release:\n    perl Configure no-shared no-tests VC-WIN64A /FS\nwinarm_release:\n    perl Configure no-shared no-tests VC-WIN64-ARM /FS\nwin_release:\n    jom -j%NUMBER_OF_PROCESSORS% build_libs\n    mkdir out\n    move libcrypto.lib out\n    move libssl.lib out\n    move ossl_static.pdb out\nmac:\n    ./Configure --prefix=$USED_PREFIX no-shared no-tests darwin64-arm64-cc $MIN_VER\n    make build_libs $MAKE_THREADS_CNT\n    mkdir out.arm64\n    mv libssl.a out.arm64\n    mv libcrypto.a out.arm64\n    make clean\n    ./Configure --prefix=$USED_PREFIX no-shared no-tests darwin64-x86_64-cc $MIN_VER\n    make build_libs $MAKE_THREADS_CNT\n    mkdir out.x86_64\n    mv libssl.a out.x86_64\n    mv libcrypto.a out.x86_64\n    lipo -create out.arm64/libcrypto.a out.x86_64/libcrypto.a -output libcrypto.a\n    lipo -create out.arm64/libssl.a out.x86_64/libssl.a -output libssl.a\n\"\"\")\n\nstage('opus', \"\"\"\n    git clone -b v1.5.2 https://github.com/xiph/opus.git\n    cd opus\nwin:\n    cmake -B out . ^\n        -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^\n        -DOPUS_STATIC_RUNTIME=ON\n    cmake --build out --config Debug\n    cmake --build out --config Release\n    cmake --install out --config Release\nmac:\n    CFLAGS=\"$UNGUARDED\" CPPFLAGS=\"$UNGUARDED\" cmake -B build . \\\\\n        -D CMAKE_OSX_ARCHITECTURES=\"x86_64;arm64\" \\\\\n        -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX\n    cmake --build build\n    cmake --install build\n\"\"\")\n\nstage('rnnoise', \"\"\"\n    git clone https://github.com/desktop-app/rnnoise.git\n    cd rnnoise\n    git checkout d8ea2b0\n    mkdir out\n    cd out\nwin:\n    cmake .. -DCMAKE_MSVC_RUNTIME_LIBRARY=\"MultiThreaded$<$<CONFIG:Debug>:Debug>\"\n    cmake --build . --config Debug\nrelease:\n    cmake --build . --config Release\n!win:\n    mkdir Debug\n    cd Debug\n    cmake ../.. \\\\\n        -D CMAKE_BUILD_TYPE=Debug \\\\\n        -D CMAKE_OSX_ARCHITECTURES=\"x86_64;arm64\"\n    cmake --build .\nrelease:\n    cd ..\n    mkdir Release\n    cd Release\n    cmake ../.. \\\\\n        -D CMAKE_BUILD_TYPE=Release \\\\\n        -D CMAKE_OSX_ARCHITECTURES=\"x86_64;arm64\"\n    cmake --build .\n\"\"\")\n\nstage('libiconv', \"\"\"\nmac:\n    VERSION=1.18\n    rm -f libiconv.tar.gz\n    wget --timeout=30 --tries=2 -O libiconv.tar.gz ftp://ftp.gnu.org/gnu/libiconv/libiconv-$VERSION.tar.gz || wget -O libiconv.tar.gz https://ftp.gnu.org/pub/gnu/libiconv/libiconv-$VERSION.tar.gz\n    rm -rf libiconv-$VERSION\n    tar -xvzf libiconv.tar.gz\n    rm libiconv.tar.gz\n    mv libiconv-$VERSION libiconv\n    cd libiconv\n    CFLAGS=\"$MIN_VER $UNGUARDED -arch arm64\" CPPFLAGS=\"$MIN_VER $UNGUARDED -arch arm64\" LDFLAGS=\"$MIN_VER\" ./configure --enable-static --host=arm --prefix=$USED_PREFIX\n    make $MAKE_THREADS_CNT\n    mkdir out.arm64\n    mv lib/.libs/libiconv.a out.arm64\n    make clean\n    CFLAGS=\"$MIN_VER $UNGUARDED -arch x86_64\" CPPFLAGS=\"$MIN_VER $UNGUARDED -arch x86_64\" LDFLAGS=\"$MIN_VER\" ./configure --enable-static --host=x86_64 --prefix=$USED_PREFIX\n    make $MAKE_THREADS_CNT\n    mkdir out.x86_64\n    mv lib/.libs/libiconv.a out.x86_64\n    lipo -create out.arm64/libiconv.a out.x86_64/libiconv.a -output lib/.libs/libiconv.a\n    make install\n\"\"\")\n\nstage('gas-preprocessor', \"\"\"\nwin:\n    git clone https://github.com/FFmpeg/gas-preprocessor\n    cd gas-preprocessor\n    echo @echo off > cpp.bat\n    echo cl %%%%%%** >> cpp.bat\n\"\"\")\n\n# Somehow in x86 Debug build dav1d crashes on AV1 10bpc videos.\nstage('dav1d', \"\"\"\n    git clone -b 1.5.3 https://code.videolan.org/videolan/dav1d.git\n    cd dav1d\nwin32:\n    SET \"TARGET=x86\"\n    SET \"DAV1D_ASM_DISABLE=-Denable_asm=false\"\nwin64:\n    SET \"TARGET=x86_64\"\n    SET \"DAV1D_ASM_DISABLE=\"\nwinarm:\n    SET \"TARGET=aarch64\"\n    SET \"DAV1D_ASM_DISABLE=\"\n    SET \"PATH=%LIBS_DIR%\\\\gas-preprocessor;%PATH%\"\n    echo armasm64 fails with 'syntax error in expression: tbnz x14, #4, 8f' as if this instruction is unknown/unsupported.\n    git revert --no-edit d503bb0ccaf104b2f13da0f092e09cc9411b3297\nwin:\n    set FILE=cross-file.txt\n    echo [binaries] > %FILE%\n    echo c = 'cl' >> %FILE%\n    echo cpp = 'cl' >> %FILE%\n    echo ar = 'lib' >> %FILE%\n    echo windres = 'rc' >> %FILE%\n    echo [host_machine] >> %FILE%\n    echo system = 'windows' >> %FILE%\n    echo cpu_family = '%TARGET%' >> %FILE%\n    echo cpu = '%TARGET%' >> %FILE%\n    echo endian = 'little' >> %FILE%\n\ndepends:python/Scripts/activate.bat\n    %THIRDPARTY_DIR%\\\\python\\\\Scripts\\\\activate.bat\n    meson setup --cross-file %FILE% --prefix %LIBS_DIR%/local --default-library=static --buildtype=debug -Denable_tools=false -Denable_tests=false %DAV1D_ASM_DISABLE% -Db_vscrt=mtd builddir-debug\n    meson compile -C builddir-debug\n    meson install -C builddir-debug\nrelease:\n    meson setup --cross-file %FILE% --prefix %LIBS_DIR%/local --default-library=static --buildtype=release -Denable_tools=false -Denable_tests=false -Db_vscrt=mt builddir-release\n    meson compile -C builddir-release\n    meson install -C builddir-release\nwin:\n    copy %LIBS_DIR%\\\\local\\\\lib\\\\libdav1d.a %LIBS_DIR%\\\\local\\\\lib\\\\dav1d.lib\n    deactivate\nmac:\n    buildOneArch() {\n        arch=$1\n        folder=`pwd`/$2\n\n        meson setup \\\\\n            --cross-file ../patches/macos_meson_${arch}.txt \\\\\n            --prefix ${USED_PREFIX} \\\\\n            --default-library=static \\\\\n            --buildtype=minsize \\\\\n            -Denable_tools=false \\\\\n            -Denable_tests=false \\\\\n            ${folder}\n        meson compile -C ${folder}\n        meson install -C ${folder}\n\n        mv ${USED_PREFIX}/lib/libdav1d.a ${folder}/libdav1d.a\n    }\n\n    buildOneArch arm64 build.arm64\n    buildOneArch x86_64 build\n\n    lipo -create build.arm64/libdav1d.a build/libdav1d.a -output ${USED_PREFIX}/lib/libdav1d.a\n\"\"\")\n\nstage('openh264', \"\"\"\n    git clone -b v2.6.0 https://github.com/cisco/openh264.git\n    cd openh264\nwin32:\n    SET \"TARGET=x86\"\nwin64:\n    SET \"TARGET=x86_64\"\nwinarm:\n    SET \"TARGET=aarch64\"\n    SET \"PATH=%LIBS_DIR%\\\\gas-preprocessor;%PATH%\"\nwin:\n    set FILE=cross-file.txt\n    echo [binaries] > %FILE%\n    echo c = 'cl' >> %FILE%\n    echo cpp = 'cl' >> %FILE%\n    echo ar = 'lib' >> %FILE%\n    echo windres = 'rc' >> %FILE%\n    echo [host_machine] >> %FILE%\n    echo system = 'windows' >> %FILE%\n    echo cpu_family = '%TARGET%' >> %FILE%\n    echo cpu = '%TARGET%' >> %FILE%\n    echo endian = 'little' >> %FILE%\n\ndepends:python/Scripts/activate.bat\n    %THIRDPARTY_DIR%\\\\python\\\\Scripts\\\\activate.bat\n    meson setup --cross-file %FILE% --prefix %LIBS_DIR%/local --default-library=static --buildtype=debug -Db_vscrt=mtd builddir-debug\n    meson compile -C builddir-debug\n    meson install -C builddir-debug\nrelease:\n    meson setup --cross-file %FILE% --prefix %LIBS_DIR%/local --default-library=static --buildtype=release -Db_vscrt=mt builddir-release\n    meson compile -C builddir-release\n    meson install -C builddir-release\nwin:\n    copy %LIBS_DIR%\\\\local\\\\lib\\\\libopenh264.a %LIBS_DIR%\\\\local\\\\lib\\\\openh264.lib\n    deactivate\nmac:\n    buildOneArch() {\n        arch=$1\n        folder=`pwd`/$2\n\n        meson setup \\\n            --cross-file ../patches/macos_meson_${arch}.txt \\\n            --prefix ${USED_PREFIX} \\\n            --default-library=static \\\n            --buildtype=minsize \\\n            ${folder}\n        meson compile -C ${folder}\n        meson install -C ${folder}\n\n        mv ${USED_PREFIX}/lib/libopenh264.a ${folder}/libopenh264.a\n    }\n\n    buildOneArch aarch64 build.aarch64\n    buildOneArch x86_64 build.x86_64\n\n    lipo -create build.aarch64/libopenh264.a build.x86_64/libopenh264.a -output ${USED_PREFIX}/lib/libopenh264.a\n\"\"\")\n\nstage('libavif', \"\"\"\n    git clone -b v1.3.0 https://github.com/AOMediaCodec/libavif.git\n    cd libavif\nwin:\n    cmake . ^\n        -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^\n        -DCMAKE_MSVC_RUNTIME_LIBRARY=\"MultiThreaded$<$<CONFIG:Debug>:Debug>\" ^\n        -DCMAKE_POLICY_DEFAULT_CMP0091=NEW ^\n        -DBUILD_SHARED_LIBS=OFF ^\n        -DAVIF_ENABLE_WERROR=OFF ^\n        -DAVIF_CODEC_DAV1D=SYSTEM ^\n        -DAVIF_LIBYUV=OFF\n    cmake --build . --config Debug\n    cmake --install . --config Debug\nrelease:\n    cmake --build . --config Release\n    cmake --install . --config Release\nmac:\n    cmake . \\\\\n        -D CMAKE_OSX_ARCHITECTURES=\"x86_64;arm64\" \\\\\n        -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX \\\\\n        -D BUILD_SHARED_LIBS=OFF \\\\\n        -D AVIF_ENABLE_WERROR=OFF \\\\\n        -D AVIF_CODEC_DAV1D=SYSTEM \\\\\n        -D AVIF_LIBYUV=OFF\n    cmake --build . --config MinSizeRel\n    cmake --install . --config MinSizeRel\n\"\"\")\n\nstage('libde265', \"\"\"\n    git clone -b v1.0.16 https://github.com/strukturag/libde265.git\n    cd libde265\nwin:\n    cmake . ^\n        -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^\n        -DCMAKE_MSVC_RUNTIME_LIBRARY=\"MultiThreaded$<$<CONFIG:Debug>:Debug>\" ^\n        -DCMAKE_POLICY_DEFAULT_CMP0091=NEW ^\n        -DCMAKE_C_FLAGS=\"/DLIBDE265_STATIC_BUILD\" ^\n        -DCMAKE_CXX_FLAGS=\"/DLIBDE265_STATIC_BUILD\" ^\n        -DENABLE_SDL=OFF ^\n        -DBUILD_SHARED_LIBS=OFF ^\n        -DENABLE_DECODER=OFF ^\n        -DENABLE_ENCODER=OFF\n    cmake --build . --config Debug\n    cmake --install . --config Debug\nrelease:\n    cmake --build . --config Release\n    cmake --install . --config Release\nmac:\n    cmake . \\\\\n        -D CMAKE_OSX_ARCHITECTURES=\"x86_64;arm64\" \\\\\n        -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX \\\\\n        -D DISABLE_SSE=ON \\\\\n        -D ENABLE_SDL=OFF \\\\\n        -D BUILD_SHARED_LIBS=OFF \\\\\n        -D ENABLE_DECODER=ON \\\\\n        -D ENABLE_ENCODER=OFF\n    cmake --build . --config MinSizeRel\n    cmake --install . --config MinSizeRel\n\"\"\")\n\nstage('libwebp', \"\"\"\n    git clone -b v1.6.0 https://github.com/webmproject/libwebp.git\n    cd libwebp\nwin:\n    nmake /f Makefile.vc CFG=debug-static OBJDIR=out RTLIBCFG=static all\n    nmake /f Makefile.vc CFG=release-static OBJDIR=out RTLIBCFG=static all\n    copy out\\\\release-static\\\\$X8664\\\\lib\\\\libwebp.lib out\\\\release-static\\\\$X8664\\\\lib\\\\webp.lib\n    copy out\\\\release-static\\\\$X8664\\\\lib\\\\libwebpdemux.lib out\\\\release-static\\\\$X8664\\\\lib\\\\webpdemux.lib\n    copy out\\\\release-static\\\\$X8664\\\\lib\\\\libwebpmux.lib out\\\\release-static\\\\$X8664\\\\lib\\\\webpmux.lib\nmac:\n    buildOneArch() {\n        arch=$1\n        folder=$2\n\n        CFLAGS=$UNGUARDED cmake -B $folder . \\\\\n            -D CMAKE_BUILD_TYPE=Release \\\\\n            -D CMAKE_INSTALL_PREFIX=$USED_PREFIX \\\\\n            -D CMAKE_OSX_ARCHITECTURES=$arch \\\\\n            -D WEBP_BUILD_ANIM_UTILS=OFF \\\\\n            -D WEBP_BUILD_CWEBP=OFF \\\\\n            -D WEBP_BUILD_DWEBP=OFF \\\\\n            -D WEBP_BUILD_GIF2WEBP=OFF \\\\\n            -D WEBP_BUILD_IMG2WEBP=OFF \\\\\n            -D WEBP_BUILD_VWEBP=OFF \\\\\n            -D WEBP_BUILD_WEBPMUX=OFF \\\\\n            -D WEBP_BUILD_WEBPINFO=OFF \\\\\n            -D WEBP_BUILD_EXTRAS=OFF\n        cmake --build $folder\n    }\n    buildOneArch arm64 build.arm64\n    buildOneArch x86_64 build\n\n    lipo -create build.arm64/libsharpyuv.a build/libsharpyuv.a -output build/libsharpyuv.a\n    lipo -create build.arm64/libwebp.a build/libwebp.a -output build/libwebp.a\n    lipo -create build.arm64/libwebpdemux.a build/libwebpdemux.a -output build/libwebpdemux.a\n    lipo -create build.arm64/libwebpmux.a build/libwebpmux.a -output build/libwebpmux.a\n    cmake --install build\n\"\"\")\n\nstage('libheif', \"\"\"\n    git clone -b v1.21.2 https://github.com/strukturag/libheif.git\n    cd libheif\nwin:\n    %THIRDPARTY_DIR%\\\\msys64\\\\usr\\\\bin\\\\sed.exe -i 's/LIBHEIF_EXPORTS/LIBDE265_STATIC_BUILD/g' libheif/CMakeLists.txt\n    %THIRDPARTY_DIR%\\\\msys64\\\\usr\\\\bin\\\\sed.exe -i 's/HAVE_VISIBILITY/LIBHEIF_STATIC_BUILD/g' libheif/CMakeLists.txt\n    %THIRDPARTY_DIR%\\\\msys64\\\\usr\\\\bin\\\\sed.exe -i 's/LIBHEIF_EXPORTS/LIBDE265_STATIC_BUILD/g' heifio/CMakeLists.txt\n    %THIRDPARTY_DIR%\\\\msys64\\\\usr\\\\bin\\\\sed.exe -i 's/HAVE_VISIBILITY/LIBHEIF_STATIC_BUILD/g' heifio/CMakeLists.txt\n    cmake . ^\n        -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^\n        -DCMAKE_MSVC_RUNTIME_LIBRARY=\"MultiThreaded$<$<CONFIG:Debug>:Debug>\" ^\n        -DBUILD_SHARED_LIBS=OFF ^\n        -DCMAKE_DISABLE_FIND_PACKAGE_Doxygen=ON ^\n        -DBUILD_TESTING=OFF ^\n        -DENABLE_PLUGIN_LOADING=OFF ^\n        -DWITH_LIBDE265=ON ^\n        -DWITH_X264=OFF ^\n        -DWITH_OpenH264_DECODER=OFF ^\n        -DWITH_SvtEnc=OFF ^\n        -DWITH_SvtEnc_PLUGIN=OFF ^\n        -DWITH_RAV1E=OFF ^\n        -DWITH_RAV1E_PLUGIN=OFF ^\n        -DWITH_LIBSHARPYUV=OFF ^\n        -DCMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE ^\n        -DCMAKE_DISABLE_FIND_PACKAGE_JPEG=TRUE ^\n        -DCMAKE_DISABLE_FIND_PACKAGE_PNG=TRUE ^\n        -DWITH_EXAMPLES=OFF\n    cmake --build . --config Debug\n    cmake --install . --config Debug\nrelease:\n    cmake --build . --config Release\n    cmake --install . --config Release\nmac:\n    cmake . \\\\\n        -D CMAKE_OSX_ARCHITECTURES=\"x86_64;arm64\" \\\\\n        -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX \\\\\n        -D BUILD_SHARED_LIBS=OFF \\\\\n        -D CMAKE_DISABLE_FIND_PACKAGE_Doxygen=ON \\\\\n        -D BUILD_TESTING=OFF \\\\\n        -D ENABLE_PLUGIN_LOADING=OFF \\\\\n        -D WITH_AOM_ENCODER=OFF \\\\\n        -D WITH_AOM_DECODER=OFF \\\\\n        -D WITH_X265=OFF \\\\\n        -D WITH_X264=OFF \\\\\n        -D WITH_OpenH264_DECODER=OFF \\\\\n        -D WITH_SvtEnc=OFF \\\\\n        -D WITH_RAV1E=OFF \\\\\n        -D WITH_DAV1D=ON \\\\\n        -D WITH_LIBDE265=ON \\\\\n        -D LIBDE265_INCLUDE_DIR=$USED_PREFIX/include/ \\\\\n        -D LIBDE265_LIBRARY=$USED_PREFIX/lib/libde265.a \\\\\n        -D WITH_LIBSHARPYUV=OFF \\\\\n        -D CMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE \\\\\n        -D CMAKE_DISABLE_FIND_PACKAGE_JPEG=TRUE \\\\\n        -D CMAKE_DISABLE_FIND_PACKAGE_PNG=TRUE \\\\\n        -D WITH_EXAMPLES=OFF\n    cmake --build . --config MinSizeRel\n    cmake --install . --config MinSizeRel\n\"\"\")\n\nstage('libjxl', \"\"\"\n    git clone -b v0.11.2 --recursive --shallow-submodules https://github.com/libjxl/libjxl.git\n    cd libjxl\n\"\"\" + setVar(\"cmake_defines\", \"\"\"\n    -DBUILD_SHARED_LIBS=OFF\n    -DBUILD_TESTING=OFF\n    -DJPEGXL_ENABLE_FUZZERS=OFF\n    -DJPEGXL_ENABLE_DEVTOOLS=OFF\n    -DJPEGXL_ENABLE_TOOLS=OFF\n    -DJPEGXL_ENABLE_DOXYGEN=OFF\n    -DJPEGXL_ENABLE_MANPAGES=OFF\n    -DJPEGXL_ENABLE_EXAMPLES=OFF\n    -DJPEGXL_ENABLE_JNI=OFF\n    -DJPEGXL_ENABLE_JPEGLI_LIBJPEG=OFF\n    -DJPEGXL_ENABLE_SJPEG=OFF\n    -DJPEGXL_ENABLE_OPENEXR=OFF\n    -DJPEGXL_ENABLE_SKCMS=ON\n    -DJPEGXL_ENABLE_VIEWERS=OFF\n    -DJPEGXL_ENABLE_TCMALLOC=OFF\n    -DJPEGXL_ENABLE_PLUGINS=OFF\n    -DJPEGXL_ENABLE_COVERAGE=OFF\n    -DJPEGXL_WARNINGS_AS_ERRORS=OFF\n\"\"\") + \"\"\"\nwin:\n    cmake . ^\n        -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^\n        -DCMAKE_MSVC_RUNTIME_LIBRARY=\"MultiThreaded$<$<CONFIG:Debug>:Debug>\" ^\n        -DCMAKE_C_FLAGS=\"/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE /DJXL_CMS_STATIC_DEFINE\" ^\n        -DCMAKE_CXX_FLAGS=\"/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE /DJXL_CMS_STATIC_DEFINE\" ^\n        %cmake_defines%\n    cmake --build . --config Debug\n    cmake --install . --config Debug\nrelease:\n    cmake --build . --config Release\n    cmake --install . --config Release\nmac:\n    cmake . \\\\\n        -D CMAKE_OSX_ARCHITECTURES=\"x86_64;arm64\" \\\\\n        -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX \\\\\n        ${cmake_defines}\n    cmake --build . --config MinSizeRel\n    cmake --install . --config MinSizeRel\n\"\"\")\n\nstage('libvpx', \"\"\"\n    git clone https://github.com/webmproject/libvpx.git\ndepends:patches/libvpx/*.patch\n    cd libvpx\n    git checkout v1.14.1\nwin:\n    for /r %%i in (..\\\\patches\\\\libvpx\\\\*) do git apply %%i\n\n    SET PATH=%THIRDPARTY_DIR%\\\\msys64\\\\usr\\\\bin;%PATH%\n    SET CHERE_INVOKING=enabled_from_arguments\n    SET MSYS2_PATH_TYPE=inherit\n\nwin32:\n    SET \"TOOLCHAIN=x86-win32-vs17\"\nwin64:\n    SET \"TOOLCHAIN=x86_64-win64-vs17\"\nwinarm:\n    SET \"TOOLCHAIN=arm64-win64-vs17\"\nwin:\ndepends:patches/build_libvpx_win.sh\n    bash --login ../patches/build_libvpx_win.sh\nmac:\n    find ../patches/libvpx -type f -print0 | sort -z | xargs -0 git apply\n\n    ./configure --prefix=$USED_PREFIX \\\n    --target=arm64-darwin20-gcc \\\n    --disable-examples \\\n    --disable-unit-tests \\\n    --disable-tools \\\n    --disable-docs \\\n    --enable-vp8 \\\n    --enable-vp9 \\\n    --enable-webm-io \\\n    --size-limit=4096x4096\n\n    make $MAKE_THREADS_CNT\n\n    mkdir out.arm64\n    mv libvpx.a out.arm64\n\n    make clean\n\n    ./configure --prefix=$USED_PREFIX \\\n    --target=x86_64-darwin20-gcc \\\n    --disable-examples \\\n    --disable-unit-tests \\\n    --disable-tools \\\n    --disable-docs \\\n    --enable-vp8 \\\n    --enable-vp9 \\\n    --enable-webm-io\n\n    make $MAKE_THREADS_CNT\n\n    mkdir out.x86_64\n    mv libvpx.a out.x86_64\n\n    lipo -create out.arm64/libvpx.a out.x86_64/libvpx.a -output libvpx.a\n\n    make install\n\"\"\")\n\nstage('liblcms2', \"\"\"\n    git clone -b lcms2.16 https://github.com/mm2/Little-CMS.git liblcms2\n    cd liblcms2\nwin:\ndepends:python/Scripts/activate.bat\n    %THIRDPARTY_DIR%\\\\python\\\\Scripts\\\\activate.bat\n    meson setup --default-library=static --buildtype=debug -Db_vscrt=mtd out/Debug\n    meson compile -C out/Debug\nrelease:\n    meson setup --default-library=static --buildtype=release -Db_vscrt=mt out/Release\n    meson compile -C out/Release\nwin:\n    deactivate\nmac:\n    buildOneArch() {\n        arch=$1\n        folder=`pwd`/$2\n\n        meson setup \\\\\n            --cross-file ../patches/macos_meson_${arch}.txt \\\\\n            --prefix ${USED_PREFIX} \\\\\n            --default-library=static \\\\\n            --buildtype=minsize \\\\\n            ${folder}\n        meson compile -C ${folder}\n        meson install -C ${folder}\n\n        mv ${USED_PREFIX}/lib/liblcms2.a ${folder}/liblcms2.a\n    }\n\n    buildOneArch arm64 build.arm64\n    buildOneArch x86_64 build\n\n    lipo -create build.arm64/liblcms2.a build/liblcms2.a -output ${USED_PREFIX}/lib/liblcms2.a\n\"\"\")\n\nstage('nv-codec-headers', \"\"\"\nwin:\n    git clone -b n12.1.14.0 https://github.com/FFmpeg/nv-codec-headers.git\n\"\"\")\n\nstage('regex', \"\"\"\n    git clone -b boost-1.83.0 https://github.com/boostorg/regex.git\n\"\"\")\n\nstage('ffmpeg', \"\"\"\n    git clone -b n6.1.1 https://github.com/FFmpeg/FFmpeg.git ffmpeg\n    cd ffmpeg\nwin:\ndepends:patches/ffmpeg.patch\n    git apply ../patches/ffmpeg.patch\n\n    SET PATH=%THIRDPARTY_DIR%\\\\msys64\\\\usr\\\\bin;%PATH%\n    SET CHERE_INVOKING=enabled_from_arguments\n    SET MSYS2_PATH_TYPE=inherit\n\n    SET \"ARCH_PARAM=\"\nwinarm:\n    SET \"ARCH_PARAM=--arch=aarch64\"\nwin:\ndepends:patches/build_ffmpeg_win.sh\n    bash --login ../patches/build_ffmpeg_win.sh\nmac:\n    export PKG_CONFIG_PATH=$USED_PREFIX/lib/pkgconfig\n\n    configureFFmpeg() {\n        arch=$1\n\n        ./configure --prefix=$USED_PREFIX \\\n        --enable-cross-compile \\\n        --target-os=darwin \\\n        --arch=\"$arch\" \\\n        --extra-cflags=\"$MIN_VER -arch $arch $UNGUARDED -DCONFIG_SAFE_BITSTREAM_READER=1 -I$USED_PREFIX/include\" \\\n        --extra-cxxflags=\"$MIN_VER -arch $arch $UNGUARDED -DCONFIG_SAFE_BITSTREAM_READER=1 -I$USED_PREFIX/include\" \\\n        --extra-ldflags=\"$MIN_VER -arch $arch $USED_PREFIX/lib/libopus.a -lc++\" \\\n        --disable-programs \\\n        --disable-doc \\\n        --disable-network \\\n        --disable-everything \\\n        --enable-protocol=file \\\n        --enable-libdav1d \\\n        --enable-libopenh264 \\\n        --enable-libopus \\\n        --enable-libvpx \\\n        --enable-hwaccel=h264_videotoolbox \\\n        --enable-hwaccel=hevc_videotoolbox \\\n        --enable-hwaccel=mpeg1_videotoolbox \\\n        --enable-hwaccel=mpeg2_videotoolbox \\\n        --enable-hwaccel=mpeg4_videotoolbox \\\n        --enable-decoder=aac \\\n        --enable-decoder=aac_at \\\n        --enable-decoder=aac_fixed \\\n        --enable-decoder=aac_latm \\\n        --enable-decoder=aasc \\\n        --enable-decoder=ac3 \\\n        --enable-decoder=alac \\\n        --enable-decoder=alac_at \\\n        --enable-decoder=av1 \\\n        --enable-decoder=eac3 \\\n        --enable-decoder=flac \\\n        --enable-decoder=gif \\\n        --enable-decoder=h264 \\\n        --enable-decoder=hevc \\\n        --enable-decoder=libdav1d \\\n        --enable-decoder=libvpx_vp8 \\\n        --enable-decoder=libvpx_vp9 \\\n        --enable-decoder=mp1 \\\n        --enable-decoder=mp1float \\\n        --enable-decoder=mp2 \\\n        --enable-decoder=mp2float \\\n        --enable-decoder=mp3 \\\n        --enable-decoder=mp3adu \\\n        --enable-decoder=mp3adufloat \\\n        --enable-decoder=mp3float \\\n        --enable-decoder=mp3on4 \\\n        --enable-decoder=mp3on4float \\\n        --enable-decoder=mpeg4 \\\n        --enable-decoder=msmpeg4v2 \\\n        --enable-decoder=msmpeg4v3 \\\n        --enable-decoder=opus \\\n        --enable-decoder=pcm_alaw \\\n        --enable-decoder=pcm_alaw_at \\\n        --enable-decoder=pcm_f32be \\\n        --enable-decoder=pcm_f32le \\\n        --enable-decoder=pcm_f64be \\\n        --enable-decoder=pcm_f64le \\\n        --enable-decoder=pcm_lxf \\\n        --enable-decoder=pcm_mulaw \\\n        --enable-decoder=pcm_mulaw_at \\\n        --enable-decoder=pcm_s16be \\\n        --enable-decoder=pcm_s16be_planar \\\n        --enable-decoder=pcm_s16le \\\n        --enable-decoder=pcm_s16le_planar \\\n        --enable-decoder=pcm_s24be \\\n        --enable-decoder=pcm_s24daud \\\n        --enable-decoder=pcm_s24le \\\n        --enable-decoder=pcm_s24le_planar \\\n        --enable-decoder=pcm_s32be \\\n        --enable-decoder=pcm_s32le \\\n        --enable-decoder=pcm_s32le_planar \\\n        --enable-decoder=pcm_s64be \\\n        --enable-decoder=pcm_s64le \\\n        --enable-decoder=pcm_s8 \\\n        --enable-decoder=pcm_s8_planar \\\n        --enable-decoder=pcm_u16be \\\n        --enable-decoder=pcm_u16le \\\n        --enable-decoder=pcm_u24be \\\n        --enable-decoder=pcm_u24le \\\n        --enable-decoder=pcm_u32be \\\n        --enable-decoder=pcm_u32le \\\n        --enable-decoder=pcm_u8 \\\n        --enable-decoder=vorbis \\\n        --enable-decoder=vp8 \\\n        --enable-decoder=wavpack \\\n        --enable-decoder=wmalossless \\\n        --enable-decoder=wmapro \\\n        --enable-decoder=wmav1 \\\n        --enable-decoder=wmav2 \\\n        --enable-decoder=wmavoice \\\n        --enable-encoder=aac \\\n        --enable-encoder=libopus \\\n        --enable-encoder=libopenh264 \\\n        --enable-encoder=pcm_s16le \\\n        --enable-filter=atempo \\\n        --enable-parser=aac \\\n        --enable-parser=aac_latm \\\n        --enable-parser=flac \\\n        --enable-parser=gif \\\n        --enable-parser=h264 \\\n        --enable-parser=hevc \\\n        --enable-parser=mpeg4video \\\n        --enable-parser=mpegaudio \\\n        --enable-parser=opus \\\n        --enable-parser=vorbis \\\n        --enable-demuxer=aac \\\n        --enable-demuxer=flac \\\n        --enable-demuxer=gif \\\n        --enable-demuxer=h264 \\\n        --enable-demuxer=hevc \\\n        --enable-demuxer=matroska \\\n        --enable-demuxer=m4v \\\n        --enable-demuxer=mov \\\n        --enable-demuxer=mp3 \\\n        --enable-demuxer=ogg \\\n        --enable-demuxer=wav \\\n        --enable-muxer=mp4 \\\n        --enable-muxer=ogg \\\n        --enable-muxer=opus \\\n        --enable-muxer=wav\n    }\n\n    configureFFmpeg arm64\n    make $MAKE_THREADS_CNT\n\n    mkdir out.arm64\n    mv libavfilter/libavfilter.a out.arm64\n    mv libavformat/libavformat.a out.arm64\n    mv libavcodec/libavcodec.a out.arm64\n    mv libswresample/libswresample.a out.arm64\n    mv libswscale/libswscale.a out.arm64\n    mv libavutil/libavutil.a out.arm64\n\n    make clean\n\n    configureFFmpeg x86_64\n    make $MAKE_THREADS_CNT\n\n    mkdir out.x86_64\n    mv libavfilter/libavfilter.a out.x86_64\n    mv libavformat/libavformat.a out.x86_64\n    mv libavcodec/libavcodec.a out.x86_64\n    mv libswresample/libswresample.a out.x86_64\n    mv libswscale/libswscale.a out.x86_64\n    mv libavutil/libavutil.a out.x86_64\n\n    lipo -create out.arm64/libavfilter.a out.x86_64/libavfilter.a -output libavfilter/libavfilter.a\n    lipo -create out.arm64/libavformat.a out.x86_64/libavformat.a -output libavformat/libavformat.a\n    lipo -create out.arm64/libavcodec.a out.x86_64/libavcodec.a -output libavcodec/libavcodec.a\n    lipo -create out.arm64/libswresample.a out.x86_64/libswresample.a -output libswresample/libswresample.a\n    lipo -create out.arm64/libswscale.a out.x86_64/libswscale.a -output libswscale/libswscale.a\n    lipo -create out.arm64/libavutil.a out.x86_64/libavutil.a -output libavutil/libavutil.a\n\n    make install\n\"\"\")\n\nstage('openal-soft', \"\"\"\n    git clone https://github.com/telegramdesktop/openal-soft.git\n    cd openal-soft\nwin:\n    git checkout 291c0fdbbd\n    cmake -B build . ^\n        -D LIBTYPE:STRING=STATIC ^\n        -D FORCE_STATIC_VCRT=ON ^\n        -D ALSOFT_UTILS=OFF ^\n        -D ALSOFT_EXAMPLES=OFF ^\n        -D ALSOFT_TESTS=OFF\n    cmake --build build --config Debug\nrelease:\n    cmake --build build --config RelWithDebInfo\nmac:\n    git checkout coreaudio_device_uid\n    CFLAGS=$UNGUARDED CPPFLAGS=$UNGUARDED cmake -B build . \\\\\n        -D CMAKE_BUILD_TYPE=RelWithDebInfo \\\\\n        -D CMAKE_INSTALL_PREFIX:PATH=$USED_PREFIX \\\\\n        -D ALSOFT_EXAMPLES=OFF \\\\\n        -D ALSOFT_UTILS=OFF \\\\\n        -D ALSOFT_TESTS=OFF \\\\\n        -D LIBTYPE:STRING=STATIC \\\\\n        -D CMAKE_OSX_ARCHITECTURES=\"x86_64;arm64\"\n    cmake --build build\n    cmake --install build\n\"\"\")\n\nif 'build-stackwalk' in options:\n    stage('stackwalk', \"\"\"\nmac:\n    git clone https://chromium.googlesource.com/breakpad/breakpad stackwalk\n    cd stackwalk\n    git checkout dfcb7b6799\ndepends:patches/breakpad.diff\n    git apply ../patches/breakpad.diff\n    git clone -b release-1.11.0 https://github.com/google/googletest src/testing\n    git clone https://chromium.googlesource.com/linux-syscall-support src/third_party/lss\n    cd src/third_party/lss\n    git checkout e1e7b0ad8e\n    cd ../../build\n    PYTHONPATH=$THIRDPARTY_DIR/gyp python3 gyp_breakpad\n    cd ../processor\n    xcodebuild -project processor.xcodeproj -target minidump_stackwalk -configuration Release build\n\"\"\")\n\nstage('breakpad', \"\"\"\n    git clone https://chromium.googlesource.com/breakpad/breakpad\n    cd breakpad\n    git checkout dfcb7b6799\ndepends:patches/breakpad.diff\n    git apply ../patches/breakpad.diff\n    git clone -b release-1.11.0 https://github.com/google/googletest src/testing\nwin:\n    SET \"PYTHONUTF8=1\"\n    SET \"FolderPostfix=\"\nwin64:\n    SET \"FolderPostfix=_x64\"\nwinarm:\n    SET \"FolderPostfix=_ARM64\"\nwin:\ndepends:python/Scripts/activate.bat\n    %THIRDPARTY_DIR%\\\\python\\\\Scripts\\\\activate.bat\n    cd src\\\\client\\\\windows\n    gyp --no-circular-check breakpad_client.gyp --format=ninja\n    cd ..\\\\..\n    ninja -C out/Debug%FolderPostfix% common crash_generation_client exception_handler\nrelease:\n    ninja -C out/Release%FolderPostfix% common crash_generation_client exception_handler\n    cd tools\\\\windows\\\\dump_syms\n    gyp dump_syms.gyp --format=msvs\n    msbuild -m dump_syms.vcxproj /property:Configuration=Release /property:Platform=\"x64\"\nwin:\n    deactivate\nmac:\n    git clone https://chromium.googlesource.com/linux-syscall-support src/third_party/lss\n    cd src/third_party/lss\n    git checkout e1e7b0ad8e\n    cd ../../..\n    cd src/client/mac\n    xcodebuild -project Breakpad.xcodeproj -target Breakpad -configuration Debug build\nrelease:\n    xcodebuild -project Breakpad.xcodeproj -target Breakpad -configuration Release build\n    cd ../../tools/mac/dump_syms\n    xcodebuild -project dump_syms.xcodeproj -target dump_syms -configuration Release build\n\"\"\")\n\nstage('crashpad', \"\"\"\nmac:\n    git clone https://github.com/desktop-app/crashpad.git\n    cd crashpad\n    git checkout 3279fae3f0\n    git submodule init\n    git submodule update third_party/mini_chromium\n    ZLIB_PATH=$USED_PREFIX/include\n    ZLIB_LIB=$USED_PREFIX/lib/libz.a\n    mkdir out\n    cd out\n    mkdir Debug.x86_64\n    cd Debug.x86_64\n    cmake \\\n        -DCMAKE_BUILD_TYPE=Debug \\\n        -DCMAKE_OSX_ARCHITECTURES=x86_64 \\\n        -DCRASHPAD_SPECIAL_TARGET=$SPECIAL_TARGET \\\n        -DCRASHPAD_ZLIB_INCLUDE_PATH=$ZLIB_PATH \\\n        -DCRASHPAD_ZLIB_LIB_PATH=$ZLIB_LIB ../..\n    cmake --build .\n    cd ..\n    mkdir Debug.arm64\n    cd Debug.arm64\n    cmake \\\n        -DCMAKE_BUILD_TYPE=Debug \\\n        -DCMAKE_OSX_ARCHITECTURES=arm64 \\\n        -DCRASHPAD_SPECIAL_TARGET=$SPECIAL_TARGET \\\n        -DCRASHPAD_ZLIB_INCLUDE_PATH=$ZLIB_PATH \\\n        -DCRASHPAD_ZLIB_LIB_PATH=$ZLIB_LIB ../..\n    cmake --build .\n    cd ..\n    mkdir Debug\n    lipo -create Debug.arm64/crashpad_handler Debug.x86_64/crashpad_handler -output Debug/crashpad_handler\n    lipo -create Debug.arm64/libcrashpad_client.a Debug.x86_64/libcrashpad_client.a -output Debug/libcrashpad_client.a\nrelease:\n    mkdir Release.x86_64\n    cd Release.x86_64\n    cmake \\\n        -DCMAKE_BUILD_TYPE=Release \\\n        -DCMAKE_OSX_ARCHITECTURES=x86_64 \\\n        -DCRASHPAD_SPECIAL_TARGET=$SPECIAL_TARGET \\\n        -DCRASHPAD_ZLIB_INCLUDE_PATH=$ZLIB_PATH \\\n        -DCRASHPAD_ZLIB_LIB_PATH=$ZLIB_LIB ../..\n    cmake --build .\n    cd ..\n    mkdir Release.arm64\n    cd Release.arm64\n    cmake \\\n        -DCMAKE_BUILD_TYPE=Release \\\n        -DCMAKE_OSX_ARCHITECTURES=arm64 \\\n        -DCRASHPAD_SPECIAL_TARGET=$SPECIAL_TARGET \\\n        -DCRASHPAD_ZLIB_INCLUDE_PATH=$ZLIB_PATH \\\n        -DCRASHPAD_ZLIB_LIB_PATH=$ZLIB_LIB ../..\n    cmake --build .\n    cd ..\n    mkdir Release\n    lipo -create Release.arm64/crashpad_handler Release.x86_64/crashpad_handler -output Release/crashpad_handler\n    lipo -create Release.arm64/libcrashpad_client.a Release.x86_64/libcrashpad_client.a -output Release/libcrashpad_client.a\n\"\"\")\n\nif qt < '6':\n    if win:\n        stage('tg_angle', \"\"\"\nwin:\n    git clone https://github.com/desktop-app/tg_angle.git\n    cd tg_angle\n    git checkout e3f59e8d0c\n    cmake -B out ^\n        -DTG_ANGLE_SPECIAL_TARGET=%SPECIAL_TARGET% ^\n        -DTG_ANGLE_ZLIB_INCLUDE_PATH=%LIBS_DIR%/zlib\n    cmake --build out --config Debug\nrelease:\n    cmake --build out --config Release\n\"\"\")\n\n    stage('qt_' + qt, \"\"\"\n    git clone -b v$QT-lts-lgpl https://github.com/qt/qt5.git qt_$QT\n    cd qt_$QT\n    git submodule update --init --recursive --progress qtbase qtimageformats qtsvg\ndepends:patches/qtbase_\"\"\" + qt + \"\"\"/*.patch\n    cd qtbase\nwin:\n    setlocal enabledelayedexpansion\n    for /r %%i in (..\\\\..\\\\patches\\\\qtbase_%QT%\\\\*) do (\n        git apply %%i -v\n        if errorlevel 1 (\n            echo ERROR: Applying patch %%~nxi failed!\n            exit /b 1\n        )\n    )\n\n    cd ..\n\n    SET CONFIGURATIONS=-debug\nrelease:\n    SET CONFIGURATIONS=-debug-and-release\nwin:\n    \"\"\" + removeDir('\"%LIBS_DIR%\\\\Qt-' + qt + '\"') + \"\"\"\n    SET ANGLE_DIR=%LIBS_DIR%\\\\tg_angle\n    SET ANGLE_LIBS_DIR=%ANGLE_DIR%\\\\out\n    SET MOZJPEG_DIR=%LIBS_DIR%\\\\mozjpeg\n    SET OPENSSL_DIR=%LIBS_DIR%\\\\openssl3\n    SET OPENSSL_LIBS_DIR=%OPENSSL_DIR%\\\\out\n    SET ZLIB_LIBS_DIR=%LIBS_DIR%\\\\zlib\n    SET WEBP_DIR=%LIBS_DIR%\\\\libwebp\n    configure -prefix \"%LIBS_DIR%\\\\Qt-%QT%\" ^\n        %CONFIGURATIONS% ^\n        -force-debug-info ^\n        -opensource ^\n        -confirm-license ^\n        -static ^\n        -static-runtime ^\n        -opengl es2 -no-angle ^\n        -I \"%ANGLE_DIR%\\\\include\" ^\n        -D \"KHRONOS_STATIC=\" ^\n        -D \"DESKTOP_APP_QT_STATIC_ANGLE=\" ^\n        QMAKE_LIBS_OPENGL_ES2_DEBUG=\"%ANGLE_LIBS_DIR%\\\\Debug\\\\tg_angle.lib %ZLIB_LIBS_DIR%\\\\Debug\\\\zlibstaticd.lib d3d9.lib dxgi.lib dxguid.lib\" ^\n        QMAKE_LIBS_OPENGL_ES2_RELEASE=\"%ANGLE_LIBS_DIR%\\\\Release\\\\tg_angle.lib %ZLIB_LIBS_DIR%\\\\Release\\\\zlibstatic.lib d3d9.lib dxgi.lib dxguid.lib\" ^\n        -egl ^\n        QMAKE_LIBS_EGL_DEBUG=\"%ANGLE_LIBS_DIR%\\\\Debug\\\\tg_angle.lib %ZLIB_LIBS_DIR%\\\\Debug\\\\zlibstaticd.lib d3d9.lib dxgi.lib dxguid.lib Gdi32.lib User32.lib\" ^\n        QMAKE_LIBS_EGL_RELEASE=\"%ANGLE_LIBS_DIR%\\\\Release\\\\tg_angle.lib %ZLIB_LIBS_DIR%\\\\Release\\\\zlibstatic.lib d3d9.lib dxgi.lib dxguid.lib Gdi32.lib User32.lib\" ^\n        -openssl-linked ^\n        -I \"%OPENSSL_DIR%\\\\include\" ^\n        OPENSSL_LIBS_DEBUG=\"%OPENSSL_LIBS_DIR%.dbg\\\\libssl.lib %OPENSSL_LIBS_DIR%.dbg\\\\libcrypto.lib Ws2_32.lib Gdi32.lib Advapi32.lib Crypt32.lib User32.lib\" ^\n        OPENSSL_LIBS_RELEASE=\"%OPENSSL_LIBS_DIR%\\\\libssl.lib %OPENSSL_LIBS_DIR%\\\\libcrypto.lib Ws2_32.lib Gdi32.lib Advapi32.lib Crypt32.lib User32.lib\" ^\n        -I \"%MOZJPEG_DIR%\" ^\n        LIBJPEG_LIBS_DEBUG=\"%MOZJPEG_DIR%\\\\Debug\\\\jpeg-static.lib\" ^\n        LIBJPEG_LIBS_RELEASE=\"%MOZJPEG_DIR%\\\\Release\\\\jpeg-static.lib\" ^\n        -system-webp ^\n        -I \"%WEBP_DIR%\\\\src\" ^\n        -L \"%WEBP_DIR%\\\\out\\\\release-static\\\\$X8664\\\\lib\" ^\n        -mp ^\n        -no-feature-netlistmgr ^\n        -nomake examples ^\n        -nomake tests ^\n        -platform win32-msvc\n\n    jom -j%NUMBER_OF_PROCESSORS%\n    jom -j%NUMBER_OF_PROCESSORS% install\nmac:\n    QT_MAJOR_MINOR=$(echo $QT | grep -oE '^[0-9]+\\\\.[0-9]+')\n    if [ -d \"../../patches/qt6_highsierra/$QT_MAJOR_MINOR\" ]; then\n        find \"../../patches/qt6_highsierra/$QT_MAJOR_MINOR\" -type f -print0 | sort -z | xargs -0 git apply\n    fi\n    find ../../patches/qtbase_$QT -type f -print0 | sort -z | xargs -0 git apply\n    cd ..\n\n    CONFIGURATIONS=-debug\nrelease:\n    CONFIGURATIONS=-debug-and-release\nmac:\n    ./configure -prefix \"$USED_PREFIX/Qt-$QT\" \\\n        $CONFIGURATIONS \\\n        -force-debug-info \\\n        -opensource \\\n        -confirm-license \\\n        -static \\\n        -opengl desktop \\\n        -no-openssl \\\n        -securetransport \\\n        -I \"$USED_PREFIX/include\" \\\n        LIBJPEG_LIBS=\"$USED_PREFIX/lib/libjpeg.a\" \\\n        ZLIB_LIBS=\"$USED_PREFIX/lib/libz.a\" \\\n        -nomake examples \\\n        -nomake tests \\\n        -platform macx-clang\n\n    make $MAKE_THREADS_CNT\n    make install\n\"\"\")\nelse: # qt > '6'\n    branch = 'v$QT' + ('-lts-lgpl' if qt.startswith('6.2.') else '')\n    stage('qt_' + qt, \"\"\"\n    git clone -b \"\"\" + branch + \"\"\" https://github.com/qt/qt5.git qt_$QT\n    cd qt_$QT\n    git submodule update --init --recursive --progress qtbase qtimageformats qtsvg\ndepends:patches/qtbase_\"\"\" + qt + \"\"\"/*.patch\n    cd qtbase\nmac:\n    QT_MAJOR_MINOR=$(echo $QT | grep -oE '^[0-9]+\\\\.[0-9]+')\n    if [ -d \"../../patches/qt6_highsierra/$QT_MAJOR_MINOR\" ]; then\n        find \"../../patches/qt6_highsierra/$QT_MAJOR_MINOR\" -type f -print0 | sort -z | xargs -0 git apply -v\n    fi\n    find ../../patches/qtbase_$QT -type f -print0 | sort -z | xargs -0 git apply -v\n    cd ..\n    sed -i.bak 's/tqtc-//' {qtimageformats,qtsvg}/dependencies.yaml\n\n    CONFIGURATIONS=-debug\nrelease:\n    CONFIGURATIONS=-debug-and-release\nmac:\n    ./configure -prefix \"$USED_PREFIX/Qt-$QT\" \\\n        $CONFIGURATIONS \\\n        -force-debug-info \\\n        -opensource \\\n        -confirm-license \\\n        -static \\\n        -no-framework \\\n        -opengl desktop \\\n        -no-openssl \\\n        -securetransport \\\n        -system-webp \\\n        -I \"$USED_PREFIX/include\" \\\n        -no-feature-futimens \\\n        -no-feature-brotli \\\n        -no-feature-cxx17_filesystem \\\n        -platform macx-clang -- \\\n        -DCMAKE_OSX_ARCHITECTURES=\"x86_64;arm64\" \\\n        -DCMAKE_PREFIX_PATH=\"$USED_PREFIX\" \\\n        -DQT_NO_HANDLE_APPLE_SINGLE_ARCH_CROSS_COMPILING=ON \\\n        -DQT_SYNC_HEADERS_AT_CONFIGURE_TIME=ON\n\n    cmake --build .\n    cmake --install .\nwin:\n    for /r %%i in (..\\\\..\\\\patches\\\\qtbase_%QT%\\\\*) do git apply %%i -v\n    cd ..\n\n    SET CONFIGURATIONS=-debug\nrelease:\n    SET CONFIGURATIONS=-debug-and-release\nwin:\n    \"\"\" + removeDir('\"%LIBS_DIR%\\\\Qt' + qt + '\"') + \"\"\"\n    SET MOZJPEG_DIR=%LIBS_DIR%\\\\mozjpeg\n    SET OPENSSL_DIR=%LIBS_DIR%\\\\openssl3\n    SET OPENSSL_LIBS_DIR=%OPENSSL_DIR%\\\\out\n    SET ZLIB_LIBS_DIR=%LIBS_DIR%\\\\zlib\n    SET WEBP_DIR=%LIBS_DIR%\\\\libwebp\n    SET LCMS2_DIR=%LIBS_DIR%\\\\liblcms2\n    configure -prefix \"%LIBS_DIR%\\\\Qt-%QT%\" ^\n        %CONFIGURATIONS% ^\n        -force-debug-info ^\n        -opensource ^\n        -confirm-license ^\n        -static ^\n        -static-runtime ^\n        -feature-c++20 ^\n        -openssl linked ^\n        -system-webp ^\n        -system-zlib ^\n        -system-libjpeg ^\n        -platform win32-msvc ^\n        -D ZLIB_WINAPI ^\n        -- ^\n        -D OPENSSL_FOUND=1 ^\n        -D OPENSSL_INCLUDE_DIR=\"%OPENSSL_DIR%\\\\include\" ^\n        -D LIB_EAY_DEBUG=\"%OPENSSL_LIBS_DIR%.dbg\\\\libcrypto.lib\" ^\n        -D SSL_EAY_DEBUG=\"%OPENSSL_LIBS_DIR%.dbg\\\\libssl.lib\" ^\n        -D LIB_EAY_RELEASE=\"%OPENSSL_LIBS_DIR%\\\\libcrypto.lib\" ^\n        -D SSL_EAY_RELEASE=\"%OPENSSL_LIBS_DIR%\\\\libssl.lib\" ^\n        -D JPEG_FOUND=1 ^\n        -D JPEG_INCLUDE_DIR=\"%MOZJPEG_DIR%\" ^\n        -D JPEG_LIBRARY_DEBUG=\"%MOZJPEG_DIR%\\\\Debug\\\\jpeg-static.lib\" ^\n        -D JPEG_LIBRARY_RELEASE=\"%MOZJPEG_DIR%\\\\Release\\\\jpeg-static.lib\" ^\n        -D ZLIB_FOUND=1 ^\n        -D ZLIB_INCLUDE_DIR=\"%ZLIB_LIBS_DIR%\" ^\n        -D ZLIB_LIBRARY_DEBUG=\"%ZLIB_LIBS_DIR%\\\\Debug\\\\zlibstaticd.lib\" ^\n        -D ZLIB_LIBRARY_RELEASE=\"%ZLIB_LIBS_DIR%\\\\Release\\\\zlibstatic.lib\" ^\n        -D WebP_INCLUDE_DIR=\"%WEBP_DIR%\\\\src\" ^\n        -D WebP_demux_INCLUDE_DIR=\"%WEBP_DIR%\\\\src\" ^\n        -D WebP_mux_INCLUDE_DIR=\"%WEBP_DIR%\\\\src\" ^\n        -D WebP_LIBRARY=\"%WEBP_DIR%\\\\out\\\\release-static\\\\$X8664\\\\lib\\\\webp.lib\" ^\n        -D WebP_demux_LIBRARY=\"%WEBP_DIR%\\\\out\\\\release-static\\\\$X8664\\\\lib\\\\webpdemux.lib\" ^\n        -D WebP_mux_LIBRARY=\"%WEBP_DIR%\\\\out\\\\release-static\\\\$X8664\\\\lib\\\\webpmux.lib\" ^\n        -D LCMS2_FOUND=1 ^\n        -D LCMS2_INCLUDE_DIR=\"%LCMS2_DIR%\\\\include\" ^\n        -D LCMS2_LIBRARIES=\"%LCMS2_DIR%\\\\out\\\\Release\\\\src\\\\liblcms2.a\"\n\n    cmake --build . --config Debug\n    cmake --install . --config Debug\n    cmake --build .\n    cmake --install .\n\"\"\")\n\nstage('tg_owt', \"\"\"\n    git clone https://github.com/desktop-app/tg_owt.git\n    cd tg_owt\n    git checkout 89df288dd6ba5b2ec95b3c5eaf1e7e0c3a870fc4\n    git submodule update --init --recursive\nwin:\n    SET MOZJPEG_PATH=$LIBS_DIR/mozjpeg\n    SET OPUS_PATH=$USED_PREFIX/include/opus\n    SET OPENSSL_PATH=$LIBS_DIR/openssl3/include\n    SET LIBVPX_PATH=$USED_PREFIX/include\n    SET OPENH264_PATH=$USED_PREFIX/include\n    SET FFMPEG_PATH=$LIBS_DIR/ffmpeg\n    cmake -B out \\\n        -DCMAKE_MSVC_RUNTIME_LIBRARY=\"MultiThreaded$<$<CONFIG:Debug>:Debug>\" \\\n        -DTG_OWT_BUILD_AUDIO_BACKENDS=OFF \\\n        -DTG_OWT_SPECIAL_TARGET=$SPECIAL_TARGET \\\n        -DTG_OWT_LIBJPEG_INCLUDE_PATH=$MOZJPEG_PATH \\\n        -DTG_OWT_OPENSSL_INCLUDE_PATH=$OPENSSL_PATH \\\n        -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \\\n        -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \\\n        -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \\\n        -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH\n    cmake --build out --config Debug\nrelease:\n    cmake --build out --config Release\nmac:\n    MOZJPEG_PATH=$USED_PREFIX/include\n    OPUS_PATH=$USED_PREFIX/include/opus\n    LIBVPX_PATH=$USED_PREFIX/include\n    OPENH264_PATH=$USED_PREFIX/include\n    FFMPEG_PATH=$USED_PREFIX/include\n    mkdir out\n    cd out\n    mkdir Debug.x86_64\n    cd Debug.x86_64\n    cmake \\\n        -DCMAKE_BUILD_TYPE=Debug \\\n        -DCMAKE_OSX_ARCHITECTURES=x86_64 \\\n        -DTG_OWT_BUILD_AUDIO_BACKENDS=OFF \\\n        -DTG_OWT_SPECIAL_TARGET=$SPECIAL_TARGET \\\n        -DTG_OWT_LIBJPEG_INCLUDE_PATH=$MOZJPEG_PATH \\\n        -DTG_OWT_OPENSSL_INCLUDE_PATH=$LIBS_DIR/openssl3/include \\\n        -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \\\n        -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \\\n        -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \\\n        -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../..\n    cmake --build .\n    cd ..\n    mkdir Debug.arm64\n    cd Debug.arm64\n    cmake \\\n        -DCMAKE_BUILD_TYPE=Debug \\\n        -DCMAKE_OSX_ARCHITECTURES=arm64 \\\n        -DTG_OWT_BUILD_AUDIO_BACKENDS=OFF \\\n        -DTG_OWT_SPECIAL_TARGET=$SPECIAL_TARGET \\\n        -DTG_OWT_LIBJPEG_INCLUDE_PATH=$MOZJPEG_PATH \\\n        -DTG_OWT_OPENSSL_INCLUDE_PATH=$LIBS_DIR/openssl3/include \\\n        -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \\\n        -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \\\n        -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \\\n        -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../..\n    cmake --build .\n    cd ..\n    mkdir Debug\n    lipo -create Debug.arm64/libtg_owt.a Debug.x86_64/libtg_owt.a -output Debug/libtg_owt.a\nrelease:\n    mkdir Release.x86_64\n    cd Release.x86_64\n    cmake \\\n        -DCMAKE_BUILD_TYPE=Release \\\n        -DCMAKE_OSX_ARCHITECTURES=x86_64 \\\n        -DTG_OWT_SPECIAL_TARGET=$SPECIAL_TARGET \\\n        -DTG_OWT_LIBJPEG_INCLUDE_PATH=$MOZJPEG_PATH \\\n        -DTG_OWT_OPENSSL_INCLUDE_PATH=$LIBS_DIR/openssl3/include \\\n        -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \\\n        -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \\\n        -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \\\n        -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../..\n    cmake --build .\n    cd ..\n    mkdir Release.arm64\n    cd Release.arm64\n    cmake \\\n        -DCMAKE_BUILD_TYPE=Release \\\n        -DCMAKE_OSX_ARCHITECTURES=arm64 \\\n        -DTG_OWT_SPECIAL_TARGET=$SPECIAL_TARGET \\\n        -DTG_OWT_LIBJPEG_INCLUDE_PATH=$MOZJPEG_PATH \\\n        -DTG_OWT_OPENSSL_INCLUDE_PATH=$LIBS_DIR/openssl3/include \\\n        -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \\\n        -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \\\n        -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \\\n        -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../..\n    cmake --build .\n    cd ..\n    mkdir Release\n    lipo -create Release.arm64/libtg_owt.a Release.x86_64/libtg_owt.a -output Release/libtg_owt.a\n\"\"\")\n\nstage('ada', \"\"\"\n    git clone -b v3.2.4 https://github.com/ada-url/ada.git\n    cd ada\nwin:\n    cmake -B out . ^\n        -D ADA_TESTING=OFF ^\n        -D ADA_TOOLS=OFF ^\n        -D ADA_INCLUDE_URL_PATTERN=OFF ^\n        -D CMAKE_MSVC_RUNTIME_LIBRARY=\"MultiThreaded$<$<CONFIG:Debug>:Debug>\"\n    cmake --build out --config Debug\n    cmake --build out --config Release\nmac:\n    CFLAGS=\"$UNGUARDED\" CPPFLAGS=\"$UNGUARDED\" cmake -B build . \\\\\n        -D ADA_TESTING=OFF \\\\\n        -D ADA_TOOLS=OFF \\\\\n        -D CMAKE_OSX_ARCHITECTURES=\"x86_64;arm64\" \\\\\n        -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX\n    cmake --build build\n    cmake --install build\n\"\"\")\n\nstage('protobuf', \"\"\"\nwin:\n    git clone --recursive -b v21.9 https://github.com/protocolbuffers/protobuf\n    cd protobuf\n    git clone https://github.com/abseil/abseil-cpp third_party/abseil-cpp\n    cd third_party/abseil-cpp\n    git checkout 273292d1cf\n    cd ../..\n    mkdir build\n    cd build\n    cmake .. ^\n        -Dprotobuf_BUILD_TESTS=OFF ^\n        -Dprotobuf_BUILD_PROTOBUF_BINARIES=ON ^\n        -Dprotobuf_BUILD_LIBPROTOC=ON ^\n        -Dprotobuf_WITH_ZLIB_DEFAULT=OFF ^\n        -Dprotobuf_DEBUG_POSTFIX=\"\"\n    cmake --build . --config Release\n    cmake --build . --config Debug\n\"\"\")\n# mac:\n#     git clone --recursive -b v21.9 https://github.com/protocolbuffers/protobuf\n#     cd protobuf\n#     git clone https://github.com/abseil/abseil-cpp third_party/abseil-cpp\n#     cd third_party/abseil-cpp\n#     git checkout 273292d1cf\n#     cd ../..\n#     mkdir build\n#     cd build\n#     CFLAGS=\"$UNGUARDED\" CPPFLAGS=\"$UNGUARDED\" cmake .. \\\n#         -Dprotobuf_BUILD_TESTS=OFF \\\n#         -Dprotobuf_BUILD_PROTOBUF_BINARIES=ON \\\n#         -Dprotobuf_BUILD_LIBPROTOC=ON \\\n#         -Dprotobuf_WITH_ZLIB_DEFAULT=OFF\n#     cmake --build .\n\nstage('tde2e', \"\"\"\n    git clone https://github.com/tdlib/td.git tde2e\n    cd tde2e\n    git checkout 51743df\nwin:\n    SET OPENSSL_DIR=%LIBS_DIR%\\\\openssl3\n    SET OPENSSL_LIBS_DIR=%OPENSSL_DIR%\\\\out\n    SET ZLIB_LIBS_DIR=%LIBS_DIR%\\\\zlib\n    %THIRDPARTY_DIR%\\\\msys64\\\\usr\\\\bin\\\\sed -i \"s/STREQUAL/MATCHES/\" td/generate/CMakeLists.txt\n    mkdir out\n    cd out\n    mkdir Debug\n    cd Debug\n    cmake ^\n        -DOPENSSL_FOUND=1 ^\n        -DOPENSSL_INCLUDE_DIR=%OPENSSL_DIR%\\\\include ^\n        -DOPENSSL_CRYPTO_LIBRARY=\"%OPENSSL_LIBS_DIR%.dbg\\\\libcrypto.lib\" ^\n        -DZLIB_FOUND=1 ^\n        -DZLIB_INCLUDE_DIR=%ZLIB_LIBS_DIR% ^\n        -DZLIB_LIBRARIES=\"%ZLIB_LIBS_DIR%\\\\Debug\\\\zlibstaticd.lib\" ^\n        -DCMAKE_CONFIGURATION_TYPES=Debug ^\n        -DCMAKE_MSVC_RUNTIME_LIBRARY=\"MultiThreaded$<$<CONFIG:Debug>:Debug>\" ^\n        -DCMAKE_POLICY_DEFAULT_CMP0091=NEW ^\n        -DCMAKE_C_FLAGS=\"/DZLIB_WINAPI\" ^\n        -DCMAKE_CXX_FLAGS=\"/DZLIB_WINAPI\" ^\n        -DCMAKE_EXE_LINKER_FLAGS=\"/SAFESEH:NO Ws2_32.lib Gdi32.lib Advapi32.lib Crypt32.lib User32.lib %OPENSSL_LIBS_DIR%.dbg\\\\libssl.lib\" ^\n        -DCMAKE_SHARED_LINKER_FLAGS=\"/SAFESEH:NO Ws2_32.lib Gdi32.lib Advapi32.lib Crypt32.lib User32.lib %OPENSSL_LIBS_DIR%.dbg\\\\libssl.lib\" ^\n        -DTD_ENABLE_MULTI_PROCESSOR_COMPILATION=ON ^\n        -DTD_E2E_ONLY=ON ^\n        ../..\n    cmake --build . --config Debug\nrelease:\n    cd ..\n    mkdir Release\n    cd Release\n    cmake ^\n        -DOPENSSL_FOUND=1 ^\n        -DOPENSSL_INCLUDE_DIR=%OPENSSL_DIR%\\\\include ^\n        -DOPENSSL_CRYPTO_LIBRARY=\"%OPENSSL_LIBS_DIR%\\\\libcrypto.lib\" ^\n        -DZLIB_FOUND=1 ^\n        -DZLIB_INCLUDE_DIR=%ZLIB_LIBS_DIR% ^\n        -DZLIB_LIBRARIES=\"%ZLIB_LIBS_DIR%\\\\Release\\\\zlibstatic.lib\" ^\n        -DCMAKE_CONFIGURATION_TYPES=Release ^\n        -DCMAKE_MSVC_RUNTIME_LIBRARY=\"MultiThreaded$<$<CONFIG:Debug>:Debug>\" ^\n        -DCMAKE_POLICY_DEFAULT_CMP0091=NEW ^\n        -DCMAKE_C_FLAGS=\"/DZLIB_WINAPI\" ^\n        -DCMAKE_CXX_FLAGS=\"/DZLIB_WINAPI\" ^\n        -DCMAKE_EXE_LINKER_FLAGS=\"/SAFESEH:NO Ws2_32.lib Gdi32.lib Advapi32.lib Crypt32.lib User32.lib %OPENSSL_LIBS_DIR%\\\\libssl.lib\" ^\n        -DCMAKE_SHARED_LINKER_FLAGS=\"/SAFESEH:NO Ws2_32.lib Gdi32.lib Advapi32.lib Crypt32.lib User32.lib %OPENSSL_LIBS_DIR%\\\\libssl.lib\" ^\n        -DTD_ENABLE_MULTI_PROCESSOR_COMPILATION=ON ^\n        -DTD_E2E_ONLY=ON ^\n        ../..\n    cmake --build . --config Release\nmac:\n    buildTd() {\n        BUILD_CONFIG=$1\n        mkdir -p out/$BUILD_CONFIG\n        cd out/$BUILD_CONFIG\n        cmake \\\n            -DCMAKE_BUILD_TYPE=$BUILD_CONFIG \\\n            -DCMAKE_OSX_ARCHITECTURES=\"x86_64;arm64\" \\\n            -DOPENSSL_FOUND=1 \\\n            -DOPENSSL_INCLUDE_DIR=$LIBS_DIR/openssl3/include \\\n            -DOPENSSL_SSL_LIBRARY=$LIBS_DIR/openssl3/libssl.a \\\n            -DOPENSSL_CRYPTO_LIBRARY=$LIBS_DIR/openssl3/libcrypto.a \\\n            -DZLIB_FOUND=1 \\\n            -DZLIB_LIBRARIES=$USED_PREFIX/lib/libz.a \\\n            -DTD_E2E_ONLY=ON \\\n            ../..\n        cmake --build .\n        cd ../..\n    }\n\n    buildTd Debug\nrelease:\n    buildTd Release\n\"\"\")\n\nif win:\n    currentCodePage = subprocess.run('chcp', capture_output=True, shell=True, text=True, env=modifiedEnv).stdout.strip().split()[-1]\n    subprocess.run('chcp 65001 > nul', shell=True, env=modifiedEnv)\n    runStages()\n    subprocess.run('chcp ' + currentCodePage + ' > nul', shell=True, env=modifiedEnv)\nelse:\n    runStages()\n"
  },
  {
    "path": "Telegram/build/prepare/win.bat",
    "content": "@echo OFF\r\n\r\nset \"FullScriptPath=%~dp0\"\r\n\r\npython %FullScriptPath%prepare.py %*\r\nif %errorlevel% neq 0 goto error\r\n\r\nexit /b\r\n\r\n:error\r\necho FAILED\r\nexit /b 1\r\n"
  },
  {
    "path": "Telegram/build/qt_version.py",
    "content": "import sys, os\n\ndef resolve(arch):\n    if sys.platform == 'darwin':\n        os.environ['QT'] = '6.11.0'\n    elif sys.platform == 'win32':\n        if arch == 'arm' or 'qt6' in sys.argv:\n            print('Choosing Qt 6.')\n            os.environ['QT'] = '6.11.0'\n        else:\n            print('Choosing Qt 5.')\n            os.environ['QT'] = '5.15.18'\n    return True\n"
  },
  {
    "path": "Telegram/build/release.py",
    "content": "import os, sys, requests, pprint, re, json\nfrom uritemplate import URITemplate, expand\nfrom subprocess import call, Popen, PIPE\nfrom os.path import expanduser\n\nchangelog_file = '../../changelog.txt'\ntoken_file = '../../../DesktopPrivate/github-releases-token.txt'\n\nversion = ''\ncommit = ''\nfor arg in sys.argv:\n  if re.match(r'\\d+\\.\\d+', arg):\n    version = arg\n  elif re.match(r'^[a-f0-9]{40}$', arg):\n    commit = arg\n\n# thanks http://stackoverflow.com/questions/13909900/progress-of-python-requests-post\nclass upload_in_chunks(object):\n  def __init__(self, filename, chunksize=1 << 13):\n    self.filename = filename\n    self.chunksize = chunksize\n    self.totalsize = os.path.getsize(filename)\n    self.readsofar = 0\n\n  def __iter__(self):\n    with open(self.filename, 'rb') as file:\n      while True:\n        data = file.read(self.chunksize)\n        if not data:\n          sys.stderr.write(\"\\n\")\n          break\n        self.readsofar += len(data)\n        percent = self.readsofar * 1e2 / self.totalsize\n        sys.stderr.write(\"\\r{percent:3.0f}%\".format(percent=percent))\n        yield data\n\n  def __len__(self):\n    return self.totalsize\n\nclass IterableToFileAdapter(object):\n  def __init__(self, iterable):\n    self.iterator = iter(iterable)\n    self.length = len(iterable)\n\n  def read(self, size=-1): # TBD: add buffer for `len(data) > size` case\n    return next(self.iterator, b'')\n\n  def __len__(self):\n    return self.length\n\ndef checkResponseCode(result, right_code):\n  if (result.status_code != right_code):\n    print('Wrong result code: ' + str(result.status_code) + ', should be ' + str(right_code))\n    sys.exit(1)\n\ndef getOutput(command):\n  p = Popen(command.split(), stdout=PIPE)\n  output, err = p.communicate()\n  if err != None or p.returncode != 0:\n    print('ERROR!')\n    print(err)\n    print(p.returncode)\n    sys.exit(1)\n  return output.decode('utf-8')\n\ndef invoke(command):\n  return call(command.split()) == 0\n\ndef appendSubmodules(appendTo, root, rootRevision):\n  startpath = os.getcwd()\n  lines = getOutput('git submodule foreach').split('\\n')\n  for line in lines:\n    if len(line) == 0:\n      continue\n    match = re.match(r\"^Entering '([^']+)'$\", line)\n    if not match:\n      print('Bad line: ' + line)\n      return False\n    path = match.group(1)\n    subroot = root + '/' + path\n    revision = getOutput('git rev-parse ' + rootRevision + ':' + path).split('\\n')[0]\n    print('Adding submodule ' + path + '...')\n    os.chdir(path)\n    tmppath = appendTo + '_tmp'\n    if not invoke('git archive --prefix=' + subroot + '/ ' + revision + ' -o ' + tmppath + '.tar'):\n      os.remove(appendTo + '.tar')\n      os.remove(tmppath + '.tar')\n      return False\n    if not appendSubmodules(tmppath, subroot, revision):\n      return False\n    tar = 'tar' if sys.platform == 'linux' else 'gtar'\n    if not invoke(tar + ' --concatenate --file=' + appendTo + '.tar ' + tmppath + '.tar'):\n      os.remove(appendTo + '.tar')\n      os.remove(tmppath + '.tar')\n      return False\n    os.remove(tmppath + '.tar')\n    os.chdir(startpath)\n  return True\n\ndef prepareSources():\n  workpath = os.getcwd()\n  os.chdir('../..')\n  rootpath = os.getcwd()\n  finalpart = rootpath + '/out/Release/sources'\n  finalpath = finalpart + '.tar'\n  if os.path.exists(finalpath):\n    os.remove(finalpath)\n  if os.path.exists(finalpath + '.gz'):\n    os.remove(finalpath + '.gz')\n  tmppath = rootpath + '/out/Release/tmp.tar'\n  print('Preparing source tarball...')\n  revision = 'v' + version\n  targetRoot = 'tdesktop-' + version + '-full';\n  if not invoke('git archive --prefix=' + targetRoot + '/ -o ' + finalpath + ' ' + revision):\n    os.remove(finalpath)\n    sys.exit(1)\n  if not appendSubmodules(finalpart, targetRoot, revision):\n    sys.exit(1)\n  print('Compressing...')\n  if not invoke('gzip -9 ' + finalpath):\n    os.remove(finalpath)\n    sys.exit(1)\n  os.chdir(workpath)\n  return finalpath + '.gz'\n\npp = pprint.PrettyPrinter(indent=2)\nurl = 'https://api.github.com/'\n\nversion_parts = version.split('.')\n\nstable = 1\nbeta = 0\n\nif len(version_parts) < 2:\n  print('Error: expected at least major version ' + version)\n  sys.exit(1)\nif len(version_parts) > 4:\n  print('Error: bad version passed ' + version)\n  sys.exit(1)\nversion_major = version_parts[0] + '.' + version_parts[1]\nif len(version_parts) == 2:\n  version = version_major + '.0'\n  version_full = version\nelse:\n  version = version_major + '.' + version_parts[2]\n  version_full = version\n  if len(version_parts) == 4:\n    if version_parts[3] == 'beta':\n      beta = 1\n      stable = 0\n      version_full = version + '.beta'\n    else:\n      print('Error: unexpected version part ' + version_parts[3])\n      sys.exit(1)\n\naccess_token = ''\nif os.path.isfile(token_file):\n  with open(token_file) as f:\n    for line in f:\n      access_token = line.replace('\\n', '')\n\nif access_token == '':\n  print('Access token not found!')\n  sys.exit(1)\n\nprint('Version: ' + version_full)\nlocal_base = expanduser(\"~\") + '/Projects/backup/tdesktop'\nif not os.path.isdir(local_base):\n    local_base = '/mnt/c/Telegram/Projects/backup/tdesktop'\n    if not os.path.isdir(local_base):\n        print('Backup path not found: ' + local_base)\n        sys.exit(1)\nlocal_folder = local_base + '/' + version_major + '/' + version_full\n\nif stable == 1:\n  if os.path.isdir(local_folder + '.beta'):\n    beta = 1\n    stable = 0\n    version_full = version + '.beta'\n    local_folder = local_folder + '.beta'\n\nif not os.path.isdir(local_folder):\n  print('Storage path not found: ' + local_folder)\n  sys.exit(1)\n\nlocal_folder = local_folder + '/'\n\nfiles = []\nfiles.append({\n  'local': 'sources',\n  'remote': 'tdesktop-' + version + '-full.tar.gz',\n  'mime': 'application/x-gzip',\n  'label': 'Source code (tar.gz, full)',\n})\nfiles.append({\n  'local': 'tsetup.' + version_full + '.exe',\n  'remote': 'tsetup.' + version_full + '.exe',\n  'backup_folder': 'tsetup',\n  'mime': 'application/octet-stream',\n  'label': 'Windows 32 bit: Installer',\n})\nfiles.append({\n  'local': 'tportable.' + version_full + '.zip',\n  'remote': 'tportable.' + version_full + '.zip',\n  'backup_folder': 'tsetup',\n  'mime': 'application/zip',\n  'label': 'Windows 32 bit: Portable',\n})\nfiles.append({\n  'local': 'tsetup-x64.' + version_full + '.exe',\n  'remote': 'tsetup-x64.' + version_full + '.exe',\n  'backup_folder': 'tx64',\n  'mime': 'application/octet-stream',\n  'label': 'Windows 64 bit: Installer',\n})\nfiles.append({\n  'local': 'tportable-x64.' + version_full + '.zip',\n  'remote': 'tportable-x64.' + version_full + '.zip',\n  'backup_folder': 'tx64',\n  'mime': 'application/zip',\n  'label': 'Windows 64 bit: Portable',\n})\nfiles.append({\n  'local': 'tsetup-arm64.' + version_full + '.exe',\n  'remote': 'tsetup-arm64.' + version_full + '.exe',\n  'backup_folder': 'tarm64',\n  'mime': 'application/octet-stream',\n  'label': 'Windows on ARM: Installer',\n})\nfiles.append({\n  'local': 'tportable-arm64.' + version_full + '.zip',\n  'remote': 'tportable-arm64.' + version_full + '.zip',\n  'backup_folder': 'tarm64',\n  'mime': 'application/zip',\n  'label': 'Windows on ARM: Portable',\n})\nfiles.append({\n  'local': 'tsetup.' + version_full + '.dmg',\n  'remote': 'tsetup.' + version_full + '.dmg',\n  'backup_folder': 'tmac',\n  'mime': 'application/octet-stream',\n  'label': 'macOS 10.13+: Installer',\n})\nfiles.append({\n  'local': 'tsetup.' + version_full + '.tar.xz',\n  'remote': 'tsetup.' + version_full + '.tar.xz',\n  'backup_folder': 'tlinux',\n  'mime': 'application/octet-stream',\n  'label': 'Linux 64 bit: Binary',\n})\n\nr = requests.get(url + 'repos/telegramdesktop/tdesktop/releases/tags/v' + version)\nif r.status_code == 404:\n  print('Release not found, creating.')\n  if commit == '':\n    print('Error: specify the commit.')\n    sys.exit(1)\n  if not os.path.isfile(changelog_file):\n    print('Error: Changelog file not found.')\n    sys.exit(1)\n  changelog = ''\n  started = 0\n  with open(changelog_file) as f:\n    for line in f:\n      if started == 1:\n        if re.match(r'^\\d+\\.\\d+', line):\n          break\n        changelog += line\n      else:\n        if re.match(r'^\\d+\\.\\d+', line):\n          if line[0:len(version) + 1] == version + ' ':\n            started = 1\n          elif line[0:len(version_major) + 1] == version_major + ' ':\n            if version_major + '.0' == version:\n              started = 1\n  if started != 1:\n    print('Error: Changelog not found.')\n    sys.exit(1)\n\n  changelog = changelog.strip()\n  print('Changelog: ')\n  print(changelog)\n\n  r = requests.post(url + 'repos/telegramdesktop/tdesktop/releases', headers={'Authorization': 'token ' + access_token}, data=json.dumps({\n    'tag_name': 'v' + version,\n    'target_commitish': commit,\n    'name': 'v ' + version,\n    'body': changelog,\n    'prerelease': (beta == 1),\n  }))\n  checkResponseCode(r, 201)\n\ntagname = 'v' + version\ninvoke(\"git fetch origin\")\nif stable == 1:\n  invoke(\"git push launchpad {}:master\".format(tagname))\nelse:\n  invoke(\"git push launchpad {}:beta\".format(tagname))\ninvoke(\"git push --tags launchpad\")\n\nr = requests.get(url + 'repos/telegramdesktop/tdesktop/releases/tags/v' + version)\ncheckResponseCode(r, 200)\n\nrelease_data = r.json()\n#pp.pprint(release_data)\n\nrelease_id = release_data['id']\nprint('Release ID: ' + str(release_id))\n\nr = requests.get(url + 'repos/telegramdesktop/tdesktop/releases/' + str(release_id) + '/assets')\ncheckResponseCode(r, 200)\n\nassets = release_data['assets']\nfor asset in assets:\n  name = asset['name']\n  found = 0\n  for file in files:\n    if file['remote'] == name:\n      print('Already uploaded: ' + name)\n      file['already'] = 1\n      found = 1\n      break\n  if found == 0:\n    print('Warning: strange asset: ' + name)\n\nfor file in files:\n  if 'already' in file:\n    continue\n  if file['local'] == 'sources':\n    file_path = prepareSources()\n  else:\n    file_path = local_folder + file['backup_folder'] + '/' + file['local']\n  if not os.path.isfile(file_path):\n    print('Warning: file not found ' + file['local'])\n    continue\n\n  upload_url = expand(release_data['upload_url'], {'name': file['remote'], 'label': file['label']})\n\n  content = upload_in_chunks(file_path, 10)\n\n  print('Uploading: ' + file['remote'] + ' (' + str(round(len(content) / 10000) / 100.) + ' MB)')\n  r = requests.post(upload_url, headers={'Content-Type': file['mime'], 'Authorization': 'token ' + access_token}, data=IterableToFileAdapter(content))\n\n  checkResponseCode(r, 201)\n\n  print('Success! Removing.')\n  if not invoke('rm ' + file_path):\n    print('Bad rm return code :(')\n    sys.exit(1)\n\nsys.exit()\n"
  },
  {
    "path": "Telegram/build/release.sh",
    "content": "set -e\nFullExecPath=$PWD\npushd `dirname $0` > /dev/null\nFullScriptPath=`pwd`\npopd > /dev/null\n\nParam1=\"$1\"\nParam2=\"$2\"\nParam3=\"$3\"\nParam4=\"$4\"\n\nif [ ! -d \"$FullScriptPath/../../../DesktopPrivate\" ]; then\n  echo \"\"\n  echo \"This script is for building the production version of Telegram Desktop.\"\n  echo \"\"\n  echo \"For building custom versions please visit the build instructions page at:\"\n  echo \"https://github.com/telegramdesktop/tdesktop/#build-instructions\"\n  exit\nfi\n\nError () {\n  cd $FullExecPath\n  echo \"$1\"\n  exit 1\n}\n\nwhile IFS='' read -r line || [[ -n \"$line\" ]]; do\n  set $line\n  eval $1=\"$2\"\ndone < \"$FullScriptPath/version\"\n\nVersionForPacker=\"$AppVersion\"\nif [ \"$AlphaVersion\" != \"0\" ]; then\n  Error \"No releases for closed alpha versions\"\nelif [ \"$BetaChannel\" == \"0\" ]; then\n  AppVersionStrFull=\"$AppVersionStr\"\n  AlphaBetaParam=''\nelse\n  AppVersionStrFull=\"$AppVersionStr.beta\"\n  AlphaBetaParam='-beta'\nfi\n\ncd \"$FullScriptPath\"\npython3 release.py $AppVersionStr $Param1 $Param2 $Param3 $Param4\n"
  },
  {
    "path": "Telegram/build/replace.vbs",
    "content": "Dim action, pat, patparts, rxp, inp, matchCount\naction = WScript.Arguments(0)\npat = WScript.Arguments(1)\npat = Replace(pat, \"&quot;\", chr(34))\npat = Replace(pat, \"&hat;\", \"^\")\npat = Replace(pat, \"&amp;\", \"&\")\n\nSet rxp = new RegExp\nrxp.Global = True\nrxp.Multiline = False\nIf action = \"Replace\" Then\n  patparts = Split(pat, \"/\")\n  rxp.Pattern = patparts(0)\nElse\n  rxp.Pattern = pat\nEnd If\n\nmatchCount = 0\nDo While Not WScript.StdIn.AtEndOfStream\n  inp = WScript.StdIn.ReadLine()\n  If rxp.Test(inp) Then\n    matchCount = matchCount + 1\n  End If\n  If action = \"Replace\" Then\n    WScript.Echo rxp.Replace(inp, patparts(1))\n  End If\nLoop\n\nIf action = \"Replace\" Then\n  If matchCount = 0 Then\n    WScript.Quit(2)\n  End If\nElse\n  WScript.Echo matchCount\nEnd If\n"
  },
  {
    "path": "Telegram/build/set_version.bat",
    "content": "@echo OFF\r\n\r\nset \"FullScriptPath=%~dp0\"\r\n\r\npython %FullScriptPath%set_version.py %1\r\nif %errorlevel% neq 0 goto error\r\n\r\nexit /b\r\n\r\n:error\r\necho FAILED\r\nexit /b 1\r\n"
  },
  {
    "path": "Telegram/build/set_version.py",
    "content": "'''\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n'''\nimport sys, os, re, subprocess, io\n\ndef finish(code):\n    global executePath\n    os.chdir(executePath)\n    sys.exit(code)\n\nif sys.platform == 'win32' and not 'COMSPEC' in os.environ:\n    print('[ERROR] COMSPEC environment variable is not set.')\n    finish(1)\n\nexecutePath = os.getcwd()\nscriptPath = os.path.dirname(os.path.realpath(__file__))\n\ninputVersion = ''\nversionOriginal = ''\nversionMajor = ''\nversionMinor = ''\nversionPatch = ''\nversionAlpha = '0'\nversionBeta = False\nfor arg in sys.argv:\n  match = re.match(r'^\\s*(\\d+)\\.(\\d+)(\\.(\\d+)(\\.(\\d+|beta))?)?\\s*$', arg)\n  if match:\n    inputVersion = arg\n    versionOriginal = inputVersion\n    versionMajor = match.group(1)\n    versionMinor = match.group(2)\n    versionPatch = match.group(4) if match.group(4) else '0'\n    versionAlphaBeta = match.group(5) if match.group(5) else ''\n    if len(versionAlphaBeta) > 0:\n      if match.group(6) == 'beta':\n        versionBeta = True\n      else:\n        versionAlpha = match.group(6)\n\nif not len(versionMajor):\n  print(\"Wrong version parameter\")\n  finish(1)\n\ndef checkVersionPart(part):\n  cleared = int(part) % 1000 if len(part) > 0 else 0\n  if str(cleared) != part:\n    print(\"Bad version part: \" + part)\n    finish(1)\n\ncheckVersionPart(versionMajor)\ncheckVersionPart(versionMinor)\ncheckVersionPart(versionPatch)\ncheckVersionPart(versionAlpha)\n\nversionFull = str(int(versionMajor) * 1000000 + int(versionMinor) * 1000 + int(versionPatch))\nversionFullAlpha = '0'\nif versionAlpha != '0':\n  versionFullAlpha = str(int(versionFull) * 1000 + int(versionAlpha))\n\nversionStr = versionMajor + '.' + versionMinor + '.' + versionPatch\nversionStrSmall = versionStr if versionPatch != '0' else versionMajor + '.' + versionMinor\n\nif versionBeta:\n  print('Setting version: ' + versionStr + ' beta')\nelif versionAlpha != '0':\n  print('Setting version: ' + versionStr + '.' + versionAlpha + ' closed alpha')\nelse:\n  print('Setting version: ' + versionStr + ' stable')\n\n#def replaceInFile(path, replaces):\n\ndef checkChangelog():\n  global scriptPath, versionStr, versionStrSmall\n\n  count = 0\n  with io.open(scriptPath + '/../../changelog.txt', encoding='utf-8') as f:\n    for line in f:\n      if line.startswith(versionStr + ' ') or line.startswith(versionStrSmall + ' '):\n        count = count + 1\n  if count == 0:\n    print('Changelog entry not found!')\n    finish(1)\n  elif count != 1:\n    print('Wrong changelog entries count found: ' + count)\n    finish(1)\n\ncheckChangelog()\n\ndef replaceInFile(path, replacements):\n  content = ''\n  foundReplacements = {}\n  updated = False\n  with open(path, 'r') as f:\n    for line in f:\n      for replacement in replacements:\n        if re.search(replacement[0], line):\n          changed = re.sub(replacement[0], replacement[1], line)\n          if changed != line:\n            line = changed\n            updated = True\n          foundReplacements[replacement[0]] = True\n      content = content + line\n  for replacement in replacements:\n    if not replacement[0] in foundReplacements:\n      print('Could not find \"' + replacement[0] + '\" in \"' + path + '\".')\n      finish(1)\n  if updated:\n    with open(path, 'w') as f:\n      f.write(content)\n\nprint('Patching build/version...')\nreplaceInFile(scriptPath + '/version', [\n  [ r'(AppVersion\\s+)\\d+', r'\\g<1>' + versionFull ],\n  [ r'(AppVersionStrMajor\\s+)\\d[\\d\\.]*', r'\\g<1>' + versionMajor + '.' + versionMinor ],\n  [ r'(AppVersionStrSmall\\s+)\\d[\\d\\.]*', r'\\g<1>' + versionStrSmall ],\n  [ r'(AppVersionStr\\s+)\\d[\\d\\.]*', r'\\g<1>' + versionStr ],\n  [ r'(BetaChannel\\s+)\\d', r'\\g<1>' + ('1' if versionBeta else '0') ],\n  [ r'(AlphaVersion\\s+)\\d+', r'\\g<1>' + versionFullAlpha ],\n  [ r'(AppVersionOriginal\\s+)\\d[\\d\\.beta]*', r'\\g<1>' + versionOriginal ],\n])\n\nprint('Patching core/version.h...')\nreplaceInFile(scriptPath + '/../SourceFiles/core/version.h', [\n  [ r'(TDESKTOP_REQUESTED_ALPHA_VERSION\\s+)\\(\\d+ULL\\)', r'\\g<1>(' + versionFullAlpha + 'ULL)' ],\n  [ r'(AppVersion\\s+=\\s+)\\d+', r'\\g<1>' + versionFull ],\n  [ r'(AppVersionStr\\s+=\\s+)[^;]+', r'\\g<1>\"' + versionStrSmall + '\"' ],\n  [ r'(AppBetaVersion\\s+=\\s+)[a-z]+', r'\\g<1>' + ('true' if versionBeta else 'false') ],\n])\n\nparts = [versionMajor, versionMinor, versionPatch, versionAlpha]\nwithcomma = ','.join(parts)\nwithdot = '.'.join(parts)\nrcReplaces = [\n  [ r'(FILEVERSION\\s+)\\d+,\\d+,\\d+,\\d+', r'\\g<1>' + withcomma ],\n  [ r'(PRODUCTVERSION\\s+)\\d+,\\d+,\\d+,\\d+', r'\\g<1>' + withcomma ],\n  [ r'(\"FileVersion\",\\s+)\"\\d+\\.\\d+\\.\\d+\\.\\d+\"', r'\\g<1>\"' + withdot + '\"' ],\n  [ r'(\"ProductVersion\",\\s+)\"\\d+\\.\\d+\\.\\d+\\.\\d+\"', r'\\g<1>\"' + withdot + '\"' ],\n]\n\nprint('Patching Telegram.rc...')\nreplaceInFile(scriptPath + '/../Resources/winrc/Telegram.rc', rcReplaces)\n\nprint('Patching Updater.rc...')\nreplaceInFile(scriptPath + '/../Resources/winrc/Updater.rc', rcReplaces)\n\nprint('Patching appxmanifest.xml...')\nreplaceInFile(scriptPath + '/../Resources/uwp/AppX/AppxManifest.xml', [\n  [ r'( Version=)\"\\d+\\.\\d+\\.\\d+\\.\\d+\"', r'\\g<1>\"' + withdot + '\"' ],\n])\n"
  },
  {
    "path": "Telegram/build/set_version.sh",
    "content": "set -e\n\npushd `dirname $0` > /dev/null\nFullScriptPath=`pwd`\npopd > /dev/null\n\npython3 $FullScriptPath/set_version.py $1\n\nexit\n"
  },
  {
    "path": "Telegram/build/setup.iss",
    "content": "#define MyAppShortName \"Telegram\"\n#define MyAppName \"Telegram Desktop\"\n#define MyAppPublisher \"Forkgram | 23rd\"\n#define MyAppURL \"https://github.com/forkgram\"\n#define MyAppExeName \"Telegram.exe\"\n#define MyAppId \"40a6e443-0831-4590-8a14-93d5ef5cba7f\"\n#define CurrentYear GetDateTimeString('yyyy','','')\n\n[Setup]\n; NOTE: The value of AppId uniquely identifies this application.\n; Do not use the same AppId value in installers for other applications.\n; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)\nAppId={{{#MyAppId}}\nAppName={#MyAppName}\nAppVersion={#MyAppVersion}\nAppCopyright={#MyAppPublisher} 2017-{#CurrentYear}\nAppPublisher={#MyAppPublisher}\nAppPublisherURL={#MyAppURL}\nAppSupportURL={#MyAppURL}\nAppUpdatesURL={#MyAppURL}\nDefaultDirName={userappdata}\\{#MyAppName}\nDefaultGroupName={#MyAppName}\nAllowNoIcons=yes\nOutputDir={#ReleasePath}\nSetupIconFile={#SourcePath}..\\Resources\\art\\icon256.ico\nUninstallDisplayName={#MyAppName}\nUninstallDisplayIcon={app}\\Telegram.exe\nCompression=lzma\nSolidCompression=yes\nDisableStartupPrompt=yes\nPrivilegesRequired=lowest\nVersionInfoVersion={#MyAppVersion}.0\nCloseApplications=force\nDisableDirPage=no\nDisableProgramGroupPage=no\nWizardStyle=modern\n\n#if MyBuildTarget == \"winarm\"\n  ArchitecturesAllowed=\"arm64\"\n  OutputBaseFilename=tsetup-arm64.{#MyAppVersionFull}\n  #define ArchModulesFolder \"arm64\"\n  AppVerName={#MyAppName} {#MyAppVersion} arm64\n#elif MyBuildTarget == \"win64\"\n  ArchitecturesAllowed=\"x64compatible\"\n  ArchitecturesInstallIn64BitMode=\"x64compatible\"\n  OutputBaseFilename=tsetup-x64.{#MyAppVersionFull}\n  #define ArchModulesFolder \"x64\"\n  AppVerName={#MyAppName} {#MyAppVersion} 64bit\n#else\n  OutputBaseFilename=tsetup.{#MyAppVersionFull}\n  #define ArchModulesFolder \"x86\"\n  AppVerName={#MyAppName} {#MyAppVersion} 32bit\n#endif\n\n#define ModulesFolder \"modules\\\" + ArchModulesFolder\n\n[Languages]\nName: \"english\"; MessagesFile: \"compiler:Default.isl\"\nName: \"it\";      MessagesFile: \"compiler:Languages\\Italian.isl\"\nName: \"es\";      MessagesFile: \"compiler:Languages\\Spanish.isl\"\nName: \"de\";      MessagesFile: \"compiler:Languages\\German.isl\"\nName: \"nl\";      MessagesFile: \"compiler:Languages\\Dutch.isl\"\nName: \"pt_BR\";   MessagesFile: \"compiler:Languages\\BrazilianPortuguese.isl\"\nName: \"ru\";      MessagesFile: \"compiler:Languages\\Russian.isl\"\nName: \"fr\";      MessagesFile: \"compiler:Languages\\French.isl\"\nName: \"ua\";      MessagesFile: \"compiler:Languages\\Ukrainian.isl\"\n\n[Tasks]\nName: \"desktopicon\"; Description: \"{cm:CreateDesktopIcon}\"; GroupDescription: \"{cm:AdditionalIcons}\"\n\n[Files]\nSource: \"{#ReleasePath}\\Telegram.exe\"; DestDir: \"{app}\"; Flags: ignoreversion\n#if MyBuildTarget != \"winarm\"\n#endif\n; NOTE: Don't use \"Flags: ignoreversion\" on any shared system files\n\n[Icons]\nName: \"{group}\\{#MyAppShortName}\"; Filename: \"{app}\\{#MyAppExeName}\"\nName: \"{group}\\{cm:UninstallProgram,{#MyAppShortName}}\"; Filename: \"{uninstallexe}\"\nName: \"{userdesktop}\\{#MyAppShortName}\"; Filename: \"{app}\\{#MyAppExeName}\"; Tasks: desktopicon\n\n[Run]\nFilename: \"{app}\\{#MyAppExeName}\"; Description: \"{cm:LaunchProgram,{#StringChange(MyAppShortName, '&', '&&')}}\"; Flags: nowait postinstall skipifsilent\n\n[UninstallDelete]\nType: files; Name: \"{app}\\data\"\nType: files; Name: \"{app}\\data_config\"\nType: files; Name: \"{app}\\log.txt\"\nType: filesandordirs; Name: \"{app}\\DebugLogs\"\nType: filesandordirs; Name: \"{app}\\tupdates\"\nType: filesandordirs; Name: \"{app}\\tdata\"\nType: filesandordirs; Name: \"{app}\\tcache\"\nType: filesandordirs; Name: \"{app}\\tdumps\"\nType: filesandordirs; Name: \"{app}\\modules\"\nType: dirifempty; Name: \"{app}\"\nType: files; Name: \"{userappdata}\\{#MyAppName}\\data\"\nType: files; Name: \"{userappdata}\\{#MyAppName}\\data_config\"\nType: files; Name: \"{userappdata}\\{#MyAppName}\\log.txt\"\nType: filesandordirs; Name: \"{userappdata}\\{#MyAppName}\\DebugLogs\"\nType: filesandordirs; Name: \"{userappdata}\\{#MyAppName}\\tupdates\"\nType: filesandordirs; Name: \"{userappdata}\\{#MyAppName}\\tdata\"\nType: filesandordirs; Name: \"{userappdata}\\{#MyAppName}\\tcache\"\nType: filesandordirs; Name: \"{userappdata}\\{#MyAppName}\\tdumps\"\nType: filesandordirs; Name: \"{userappdata}\\{#MyAppName}\\modules\"\nType: dirifempty; Name: \"{userappdata}\\{#MyAppName}\"\n\n[Code]\nprocedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);\nvar ResultCode: Integer;\nbegin\n  if CurUninstallStep = usUninstall then\n  begin\n    ShellExec('', ExpandConstant('{app}\\{#MyAppExeName}'), '-cleanup', '', SW_SHOW, ewWaitUntilTerminated, ResultCode);\n  end;\nend;\n\nconst CSIDL_DESKTOPDIRECTORY = $0010;\n      CSIDL_COMMON_DESKTOPDIRECTORY = $0019;\n\nprocedure CurStepChanged(CurStep: TSetupStep);\nvar ResultCode: Integer;\n    HasOldKey: Boolean;\n    HasNewKey: Boolean;\n    HasOldLnk: Boolean;\n    HasNewLnk: Boolean;\n    UserDesktopLnk: String;\n    CommonDesktopLnk: String;\nbegin\n  if CurStep = ssPostInstall then\n  begin\n    HasNewKey := RegKeyExists(HKEY_CURRENT_USER, 'Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{{#MyAppId}}_is1') or RegKeyExists(HKEY_CURRENT_USER, 'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{{#MyAppId}}_is1');\n    HasOldKey := RegKeyExists(HKEY_LOCAL_MACHINE, 'SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{{#MyAppId}}_is1') or RegKeyExists(HKEY_LOCAL_MACHINE, 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{{#MyAppId}}_is1');\n    UserDesktopLnk := ExpandFileName(GetShellFolderByCSIDL(CSIDL_DESKTOPDIRECTORY, False) + '\\{#MyAppShortName}.lnk');\n    CommonDesktopLnk := ExpandFileName(GetShellFolderByCSIDL(CSIDL_COMMON_DESKTOPDIRECTORY, False) + '\\{#MyAppShortName}.lnk');\n    HasNewLnk := FileExists(UserDesktopLnk);\n    HasOldLnk := FileExists(CommonDesktopLnk) and (UserDesktopLnk <> CommonDesktopLnk);\n    if (HasOldKey and HasNewKey) or (HasOldLnk and HasNewLnk) then\n    begin\n      if (GetWindowsVersion >= $06000000) then // Vista or later\n        ShellExec('runas', ExpandConstant('{app}\\{#MyAppExeName}'), '-fixprevious', '', SW_SHOW, ewWaitUntilTerminated, ResultCode)\n      else\n        ShellExec('', ExpandConstant('{app}\\{#MyAppExeName}'), '-fixprevious', '', SW_SHOW, ewWaitUntilTerminated, ResultCode);\n    end;\n  end;\nend;\n"
  },
  {
    "path": "Telegram/build/test_package.bat",
    "content": "@echo OFF\r\nsetlocal enabledelayedexpansion\r\nset \"FullScriptPath=%~dp0\"\r\nset \"FullExecPath=%cd%\"\r\n\r\nif not exist \"%FullScriptPath%..\\..\\..\\DesktopPrivate\" (\r\n  echo.\r\n  echo This script is for building the production version of Telegram Desktop.\r\n  echo.\r\n  echo For building custom versions please visit the build instructions page at:\r\n  echo https://github.com/telegramdesktop/tdesktop/#build-instructions\r\n  exit /b\r\n)\r\n\r\nset \"HomePath=%FullScriptPath%..\"\r\nset \"SignAppxPath=%HomePath%\\..\\..\\DesktopPrivate\\AppxSign.bat\"\r\nset \"ResourcesPath=%HomePath%\\Resources\"\r\nset \"SolutionPath=%HomePath%\\..\"\r\nset \"ReleasePath=%HomePath%\\..\\out\\Debug\"\r\nset \"BinaryName=Telegram\"\r\n\r\nif exist %ReleasePath%\\AppX\\ (\r\n  echo Result folder out\\Debug\\AppX already exists!\r\n  exit /b 1\r\n)\r\n\r\ncd \"%HomePath%\"\r\n\r\ncall gyp\\refresh.bat\r\nif %errorlevel% neq 0 goto error\r\n\r\ncd \"%SolutionPath%\"\r\ncall ninja -C out/Debug Telegram\r\nif %errorlevel% neq 0 goto error\r\n\r\ncd \"%HomePath%\"\r\n\r\nmkdir \"%ReleasePath%\\AppX\"\r\nxcopy \"Resources\\uwp\\AppX\\*\" \"%ReleasePath%\\AppX\\\" /E\r\n\r\nset \"ResourcePath=%ReleasePath%\\AppX\\AppxManifest.xml\"\r\ncall :repl \"Argument= (Publisher=)&quot;CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A&quot;/ $1&quot;CN=Telegram FZ-LLC, O=Telegram FZ-LLC, L=Dubai, C=AE&quot;\" \"Filename=%ResourcePath%\" || goto :error\r\ncall :repl \"Argument= (ProcessorArchitecture=)&quot;ARCHITECTURE&quot;/ $1&quot;x64&quot;\" \"Filename=%ResourcePath%\" || goto :error\r\n\r\nmakepri new /pr Resources\\uwp\\AppX\\ /cf Resources\\uwp\\priconfig.xml /mn %ReleasePath%\\AppX\\AppxManifest.xml /of %ReleasePath%\\AppX\\resources.pri\r\nif %errorlevel% neq 0 goto error\r\n\r\nxcopy \"%ReleasePath%\\%BinaryName%.exe\" \"%ReleasePath%\\AppX\\\"\r\n\r\nMakeAppx.exe pack /d \"%ReleasePath%\\AppX\" /l /p ..\\out\\Debug\\%BinaryName%.appx\r\nif %errorlevel% neq 0 goto error\r\n\r\ncall \"%SignAppxPath%\" \"..\\out\\Debug\\%BinaryName%.appx\"\r\n\r\nmove \"%ReleasePath%\\%BinaryName%.appx\" \"%ReleasePath%\\AppX\\\"\r\n\r\necho Done.\r\n\r\nexit /b\r\n\r\n:repl\r\n(\r\n  set %1\r\n  set %2\r\n  set \"TempFilename=!Filename!__tmp__\"\r\n  cscript //Nologo \"%FullScriptPath%replace.vbs\" \"Replace\" \"!Argument!\" < \"!Filename!\" > \"!TempFilename!\" || goto :repl_finish\r\n  xcopy /Y !TempFilename! !Filename! >NUL || goto :repl_finish\r\n  goto :repl_finish\r\n)\r\n\r\n:repl_finish\r\n(\r\n  set ErrorCode=%errorlevel%\r\n  if !ErrorCode! neq 0 (\r\n    echo Replace error !ErrorCode!\r\n    echo While replacing \"%Replace%\"\r\n    echo In file \"%Filename%\"\r\n  )\r\n  del %TempFilename%\r\n  exit /b !ErrorCode!\r\n)\r\n"
  },
  {
    "path": "Telegram/build/updates.py",
    "content": "import os, sys, re, subprocess, datetime, time\n\nexecutePath = os.getcwd()\nscriptPath = os.path.dirname(os.path.realpath(__file__))\n\nlastCommit = ''\ntoday = ''\nuuid = ''\nnextLast = False\nnextDate = False\nnextUuid = False\nbuilding = True\ncomposing = False\nconf = 'Release'\nfor arg in sys.argv:\n    if nextLast:\n        lastCommit = arg\n        nextLast = False\n    elif nextDate:\n        today = arg\n        nextDate = False\n    elif nextUuid:\n        uuid = arg\n        nextUuid = False\n    elif arg == 'send':\n        building = False\n        composing = False\n    elif arg == 'from':\n        nextLast = True\n        building = False\n        composing = True\n    elif arg == 'date':\n        nextDate = True\n    elif arg == 'request_uuid':\n        nextUuid = True\n    elif arg == 'debug':\n        conf = 'Debug'\n\ndef finish(code, error = ''):\n    if error != '':\n        print('[ERROR] ' + error)\n    global executePath\n    os.chdir(executePath)\n    sys.exit(code)\n\nos.chdir(scriptPath + '/..')\n\nif 'AC_USERNAME' not in os.environ:\n    finish(1, 'AC_USERNAME not found!')\nusername = os.environ['AC_USERNAME']\n\nif today == '':\n    today = datetime.datetime.now().strftime(\"%d_%m_%y\")\noutputFolder = 'updates/' + today\n\narchive = 'tdesktop_macOS_' + today + '.zip'\n\nif building:\n    print('Building ' + conf + ' version for OS X 10.13+..')\n\n    if os.path.exists('../out/' + conf + '/' + outputFolder):\n        finish(1, 'Todays updates version exists.')\n\n    if uuid == '':\n        result = subprocess.call('./configure.sh', shell=True)\n        if result != 0:\n            finish(1, 'While calling GYP.')\n\n    os.chdir('../out')\n    if uuid == '':\n        result = subprocess.call('cmake --build . --config ' + conf + ' --target Telegram', shell=True)\n        if result != 0:\n            finish(1, 'While building Telegram.')\n\n    os.chdir(conf);\n    if uuid == '':\n        if not os.path.exists('Telegram.app'):\n            finish(1, 'Telegram.app not found.')\n\n        result = subprocess.call('strip Telegram.app/Contents/MacOS/Telegram', shell=True)\n        if result != 0:\n            finish(1, 'While stripping Telegram.')\n\n        result = subprocess.call('codesign --force --deep --timestamp --options runtime --sign \"Developer ID Application: Telegram FZ-LLC (C67CF9S4VU)\" Telegram.app --entitlements \"../../Telegram/Telegram/Telegram.entitlements\"', shell=True)\n        if result != 0:\n            finish(1, 'While signing Telegram.')\n\n        if not os.path.exists('Telegram.app/Contents/Frameworks/Updater'):\n            finish(1, 'Updater not found.')\n        elif not os.path.exists('Telegram.app/Contents/Helpers/crashpad_handler'):\n            finish(1, 'crashpad_handler not found.')\n        elif not os.path.exists('Telegram.app/Contents/_CodeSignature'):\n            finish(1, 'Signature not found.')\n\n        if os.path.exists(today):\n            subprocess.call('rm -rf ' + today, shell=True)\n        result = subprocess.call('mkdir -p ' + today + '/TelegramForcePortable', shell=True)\n        if result != 0:\n            finish(1, 'Creating folder ' + today + '/TelegramForcePortable')\n\n        result = subprocess.call('cp -r Telegram.app ' + today + '/', shell=True)\n        if result != 0:\n            finish(1, 'Cloning Telegram.app to ' + today + '.')\n\n        result = subprocess.call('zip -r ' + archive + ' ' + today, shell=True)\n        if result != 0:\n            finish(1, 'Adding tdesktop to archive.')\n\n        print('Beginning notarization process.')\n        result = subprocess.call('xcrun notarytool submit \"' + archive + '\" --keychain-profile \"preston\" --wait', shell=True)\n        if result != 0:\n            finish(1, 'Notarizing the archive.')\n    result = subprocess.call('xcrun stapler staple Telegram.app', shell=True)\n    if result != 0:\n        finish(1, 'Error calling stapler')\n\n    subprocess.call('rm -rf ' + today + '/Telegram.app', shell=True)\n    subprocess.call('rm ' + archive, shell=True)\n    result = subprocess.call('cp -r Telegram.app ' + today + '/', shell=True)\n    if result != 0:\n        finish(1, 'Re-Cloning Telegram.app to ' + today + '.')\n\n    result = subprocess.call('zip -r ' + archive + ' ' + today, shell=True)\n    if result != 0:\n        finish(1, 'Re-Adding tdesktop to archive.')\n    print('Re-Archived.')\n\n    subprocess.call('mkdir -p ' + outputFolder, shell=True)\n    subprocess.call('mv ' + archive + ' ' + outputFolder + '/', shell=True)\n    subprocess.call('rm -rf ' + today, shell=True)\n    print('Finished.')\n    finish(0)\n\ncommandPath = scriptPath + '/../../out/' + conf + '/' + outputFolder + '/command.txt'\n\nif composing:\n    templatePath = scriptPath + '/../../../DesktopPrivate/updates_template.txt'\n    if not os.path.exists(templatePath):\n        finish(1, 'Template file \"' + templatePath + '\" not found.')\n\n    if not re.match(r'^[a-f0-9]{9,40}$', lastCommit):\n        finish(1, 'Wrong last commit: ' + lastCommit)\n\n    log = subprocess.check_output(['git', 'log', lastCommit+'..HEAD']).decode('utf-8')\n    logLines = log.split('\\n')\n    firstCommit = ''\n    commits = []\n    for line in logLines:\n        if line.startswith('commit '):\n            commit = line.split(' ')[1]\n            if not len(firstCommit):\n                firstCommit = commit\n            commits.append('')\n        elif line.startswith('    '):\n            stripped = line[4:]\n            if not len(stripped):\n                continue\n            elif not len(commits):\n                print(log)\n                finish(1, 'Bad git log output.')\n            if len(commits[len(commits) - 1]):\n                commits[len(commits) - 1] += '\\n' + stripped\n            else:\n                commits[len(commits) - 1] = '- ' + stripped\n    commits.reverse()\n    if not len(commits):\n        finish(1, 'No commits since last build :(')\n\n    changelog = '\\n'.join(commits)\n    print('\\n\\nReady! File: ' + archive + '\\nChangelog:\\n' + changelog)\n    with open(templatePath, 'r') as template:\n        with open(commandPath, 'w') as f:\n            for line in template:\n                if line.startswith('//'):\n                    continue\n                line = line.replace('{path}', scriptPath + '/../../out/' + conf + '/' + outputFolder + '/' + archive)\n                line = line.replace('{caption}', 'TDesktop at ' + today.replace('_', '.') + ':\\n\\n' + changelog)\n                f.write(line)\n    print('\\n\\nEdit:\\n')\n    print('vi ' + commandPath)\n    finish(0)\n\nif not os.path.exists(commandPath):\n    finish(1, 'Command file not found.')\n\nreadingCaption = False\ncaption = ''\nwith open(commandPath, 'r') as f:\n    for line in f:\n        if readingCaption:\n            caption = caption + line\n        elif line.startswith('caption: '):\n            readingCaption = True\n            caption = line[len('caption: '):]\n            if not caption.startswith('TDesktop at ' + today.replace('_', '.') + ':'):\n                finish(1, 'Wrong caption start.')\nprint('\\n\\nSending! File: ' + archive + '\\nChangelog:\\n' + caption)\nif len(caption) > 1024:\n    print('Length: ' + str(len(caption)))\n    print('vi ' + commandPath)\n    finish(1, 'Too large.')\n\nif not os.path.exists('../out/' + conf + '/' + outputFolder + '/' + archive):\n    finish(1, 'Not built yet.')\n\nsubprocess.call(scriptPath + '/../../out/' + conf + '/Telegram.app/Contents/MacOS/Telegram -sendpath interpret://' + scriptPath + '/../../out/' + conf + '/' + outputFolder + '/command.txt', shell=True)\n\nfinish(0)\n"
  },
  {
    "path": "Telegram/build/updates.sh",
    "content": "set -e\nFullExecPath=$PWD\npushd `dirname $0` > /dev/null\nFullScriptPath=`pwd`\npopd > /dev/null\n\nif [ ! -d \"$FullScriptPath/../../../DesktopPrivate\" ]; then\n  echo \"\"\n  echo \"This script is for building the production version of Telegram Desktop.\"\n  echo \"\"\n  echo \"For building custom versions please visit the build instructions page at:\"\n  echo \"https://github.com/telegramdesktop/tdesktop/#build-instructions\"\n  exit\nfi\n\npushd `dirname $0` > /dev/null\nFullScriptPath=`pwd`\npopd > /dev/null\n\npython3 $FullScriptPath/updates.py $1 $2 $3 $4 $5 $6\n\nexit\n"
  },
  {
    "path": "Telegram/build/version",
    "content": "AppVersion         6007006\nAppVersionStrMajor 6.7\nAppVersionStrSmall 6.7.6\nAppVersionStr      6.7.6\nBetaChannel        0\nAlphaVersion       0\nAppVersionOriginal 6.7.6\n"
  },
  {
    "path": "Telegram/cmake/generate_appstream_changelog.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nfunction(generate_appstream_changelog target_name changelog_path metadata_path)\n    find_package(Python3 REQUIRED)\n\n    set(gen_dst ${CMAKE_CURRENT_BINARY_DIR}/gen)\n    file(MAKE_DIRECTORY ${gen_dst})\n\n    set(gen_timestamp ${gen_dst}/${target_name}_appstream_changelog.timestamp)\n    set(gen_files ${metadata_path})\n\n    add_custom_command(\n    OUTPUT\n        ${gen_timestamp}\n    COMMAND\n        ${Python3_EXECUTABLE}\n        ${submodules_loc}/build/changelog2appstream.py\n        -c \"${changelog_path}\"\n        -m \"${metadata_path}\"\n        -n 10\n    COMMAND\n        echo 1> ${gen_timestamp}\n    COMMENT \"Generating AppStream changelog (${target_name})\"\n    DEPENDS\n        ${submodules_loc}/build/changelog2appstream.py\n        ${changelog_path}\n        ${metadata_path}\n    )\n    generate_target(${target_name} appstream_changelog ${gen_timestamp} \"${gen_files}\" ${gen_dst})\nendfunction()\n"
  },
  {
    "path": "Telegram/cmake/generate_lang.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nfunction(generate_lang target_name lang_file)\n    set(gen_dst ${CMAKE_CURRENT_BINARY_DIR}/gen)\n    file(MAKE_DIRECTORY ${gen_dst})\n\n    set(gen_timestamp ${gen_dst}/lang_auto.timestamp)\n    set(gen_files\n        ${gen_dst}/lang_auto.cpp\n        ${gen_dst}/lang_auto.h\n    )\n\n    add_custom_command(\n    OUTPUT\n        ${gen_timestamp}\n    BYPRODUCTS\n        ${gen_files}\n    COMMAND\n        codegen_lang\n        -o${gen_dst}\n        ${lang_file}\n    COMMENT \"Generating lang (${target_name})\"\n    DEPENDS\n        codegen_lang\n        ${lang_file}\n    )\n    generate_target(${target_name} lang ${gen_timestamp} \"${gen_files}\" ${gen_dst})\nendfunction()\n"
  },
  {
    "path": "Telegram/cmake/generate_midl.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nfunction(generate_midl target_name src_loc)\n    set(gen_dst ${CMAKE_CURRENT_BINARY_DIR}/gen)\n    file(MAKE_DIRECTORY ${gen_dst})\n\n    if (build_winarm)\n        set(env arm64)\n    elseif (build_win64)\n        set(env x64)\n    else()\n        set(env win32)\n    endif()\n\n    set(gen_timestamp ${gen_dst}/${target_name}_midl.timestamp)\n    set(gen_files \"\")\n    set(full_generation_sources \"\")\n    set(full_dependencies_list \"\")\n    foreach (file ${ARGN})\n        list(APPEND full_generation_sources ${src_loc}/${file})\n        get_filename_component(file_name ${file} NAME_WLE)\n        list(APPEND gen_files\n            ${gen_dst}/${file_name}_i.c\n            ${gen_dst}/${file_name}_h.h\n        )\n        list(APPEND gen_commands\n        COMMAND\n            midl\n            /out ${gen_dst}\n            /h ${file_name}_h.h\n            /env ${env}\n            /notlb\n            ${src_loc}/${file}\n        )\n    endforeach()\n\n    add_custom_command(\n    OUTPUT\n        ${gen_timestamp}\n    BYPRODUCTS\n        ${gen_files}\n    ${gen_commands}\n    COMMAND\n        echo 1> ${gen_timestamp}\n    COMMENT \"Generating headers from IDLs (${target_name})\"\n    DEPENDS\n        ${full_generation_sources}\n    )\n    generate_target(${target_name} midl ${gen_timestamp} \"${gen_files}\" ${gen_dst})\nendfunction()\n"
  },
  {
    "path": "Telegram/cmake/generate_numbers.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nfunction(generate_numbers target_name numbers_file)\n    set(gen_dst ${CMAKE_CURRENT_BINARY_DIR}/gen)\n    file(MAKE_DIRECTORY ${gen_dst})\n\n    set(gen_timestamp ${gen_dst}/numbers.timestamp)\n    set(gen_files\n        ${gen_dst}/numbers.cpp\n        ${gen_dst}/numbers.h\n    )\n\n    add_custom_command(\n    OUTPUT\n        ${gen_timestamp}\n    BYPRODUCTS\n        ${gen_files}\n    COMMAND\n        codegen_numbers\n        -o${gen_dst}\n        ${numbers_file}\n    COMMENT \"Generating numbers (${target_name})\"\n    DEPENDS\n        codegen_numbers\n        ${numbers_file}\n    )\n    generate_target(${target_name} numbers ${gen_timestamp} \"${gen_files}\" ${gen_dst})\nendfunction()\n"
  },
  {
    "path": "Telegram/cmake/generate_scheme.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nfunction(generate_scheme target_name script scheme_files)\n    find_package(Python3 REQUIRED)\n\n    set(gen_dst ${CMAKE_CURRENT_BINARY_DIR}/gen)\n    file(MAKE_DIRECTORY ${gen_dst})\n\n    set(gen_timestamp ${gen_dst}/scheme.timestamp)\n    set(gen_files\n        ${gen_dst}/scheme.cpp\n        ${gen_dst}/scheme.h\n        ${gen_dst}/scheme-dump_to_text.cpp\n        ${gen_dst}/scheme-dump_to_text.h\n    )\n\n    add_custom_command(\n    OUTPUT\n        ${gen_timestamp}\n    BYPRODUCTS\n        ${gen_files}\n    COMMAND\n        ${Python3_EXECUTABLE}\n        ${script}\n        -o${gen_dst}/scheme\n        ${scheme_files}\n    COMMENT \"Generating scheme (${target_name})\"\n    DEPENDS\n        ${script}\n        ${submodules_loc}/lib_tl/tl/generate_tl.py\n        ${scheme_files}\n    )\n    generate_target(${target_name} scheme ${gen_timestamp} \"${gen_files}\" ${gen_dst})\nendfunction()\n"
  },
  {
    "path": "Telegram/cmake/lib_ffmpeg.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nadd_library(lib_ffmpeg OBJECT)\nadd_library(desktop-app::lib_ffmpeg ALIAS lib_ffmpeg)\ninit_target(lib_ffmpeg)\n\nnice_target_sources(lib_ffmpeg ${src_loc}\nPRIVATE\n    ffmpeg/ffmpeg_frame_generator.cpp\n    ffmpeg/ffmpeg_frame_generator.h\n    ffmpeg/ffmpeg_bytes_io_wrap.h\n    ffmpeg/ffmpeg_utility.cpp\n    ffmpeg/ffmpeg_utility.h\n)\n\ntarget_include_directories(lib_ffmpeg\nPUBLIC\n    ${src_loc}\n)\n\ntarget_link_libraries(lib_ffmpeg\nPUBLIC\n    desktop-app::lib_base\n    desktop-app::lib_ui\n    desktop-app::external_ffmpeg\n)\n\nif (DESKTOP_APP_SPECIAL_TARGET)\n    target_compile_definitions(lib_ffmpeg PRIVATE LIB_FFMPEG_USE_QT_PRIVATE_API)\nendif()\n"
  },
  {
    "path": "Telegram/cmake/lib_prisma.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nadd_library(lib_prisma STATIC)\ninit_target(lib_prisma)\n\nadd_library(desktop-app::lib_prisma ALIAS lib_prisma)\n\nset(prisma_loc ${third_party_loc}/libprisma/libprisma)\n\nnice_target_sources(lib_prisma ${prisma_loc}\nPRIVATE\n    Highlight.cpp\n    Highlight.h\n    LanguageTree.cpp\n    LanguageTree.h\n    SyntaxHighlighter.cpp\n    SyntaxHighlighter.h\n    TokenList.cpp\n    TokenList.h\n)\n\ntarget_include_directories(lib_prisma\nPUBLIC\n    ${prisma_loc}\n)\n\ntarget_link_libraries(lib_prisma\nPRIVATE\n    desktop-app::external_boost_regex\n)\n"
  },
  {
    "path": "Telegram/cmake/lib_stripe.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nadd_library(lib_stripe OBJECT)\nadd_library(desktop-app::lib_stripe ALIAS lib_stripe)\ninit_target(lib_stripe)\n\nset(stripe_src_loc ${src_loc}/payments)\n\ntarget_precompile_headers(lib_stripe PRIVATE ${stripe_src_loc}/stripe/stripe_pch.h)\nnice_target_sources(lib_stripe ${stripe_src_loc}\nPRIVATE\n    stripe/stripe_address.h\n    stripe/stripe_api_client.cpp\n    stripe/stripe_api_client.h\n    stripe/stripe_callbacks.h\n    stripe/stripe_card.cpp\n    stripe/stripe_card.h\n    stripe/stripe_card_params.cpp\n    stripe/stripe_card_params.h\n    stripe/stripe_card_validator.cpp\n    stripe/stripe_card_validator.h\n    stripe/stripe_decode.cpp\n    stripe/stripe_decode.h\n    stripe/stripe_error.cpp\n    stripe/stripe_error.h\n    stripe/stripe_form_encodable.h\n    stripe/stripe_form_encoder.cpp\n    stripe/stripe_form_encoder.h\n    stripe/stripe_payment_configuration.h\n    stripe/stripe_token.cpp\n    stripe/stripe_token.h\n\n    smartglocal/smartglocal_api_client.cpp\n    smartglocal/smartglocal_api_client.h\n    smartglocal/smartglocal_callbacks.h\n    smartglocal/smartglocal_card.cpp\n    smartglocal/smartglocal_card.h\n    smartglocal/smartglocal_error.cpp\n    smartglocal/smartglocal_error.h\n    smartglocal/smartglocal_token.cpp\n    smartglocal/smartglocal_token.h\n\n    stripe/stripe_pch.h\n)\n\ntarget_include_directories(lib_stripe\nPUBLIC\n    ${stripe_src_loc}\n)\n\ntarget_link_libraries(lib_stripe\nPUBLIC\n    desktop-app::lib_base\n    desktop-app::lib_crl\n    desktop-app::external_qt\n)\n"
  },
  {
    "path": "Telegram/cmake/lib_tgcalls.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nadd_library(lib_tgcalls STATIC)\ninit_target(lib_tgcalls) # Can't use std::optional::value on macOS.\n\nadd_library(tdesktop::lib_tgcalls ALIAS lib_tgcalls)\n\nset(tgcalls_dir ${third_party_loc}/tgcalls)\nset(tgcalls_loc ${tgcalls_dir}/tgcalls)\n\nnice_target_sources(lib_tgcalls ${tgcalls_loc}\nPRIVATE\n    Instance.cpp\n    Instance.h\n)\n\nnice_target_sources(lib_tgcalls ${tgcalls_loc}\nPRIVATE\n    AudioDeviceHelper.cpp\n    AudioDeviceHelper.h\n    ChannelManager.cpp\n    ChannelManager.h\n    CodecSelectHelper.cpp\n    CodecSelectHelper.h\n    CryptoHelper.cpp\n    CryptoHelper.h\n    DirectConnectionChannel.h\n    EncryptedConnection.cpp\n    EncryptedConnection.h\n    FakeAudioDeviceModule.cpp\n    FakeAudioDeviceModule.h\n    FieldTrialsConfig.cpp\n    FieldTrialsConfig.h\n    InstanceImpl.cpp\n    InstanceImpl.h\n    LogSinkImpl.cpp\n    LogSinkImpl.h\n    Manager.cpp\n    Manager.h\n    MediaManager.cpp\n    MediaManager.h\n    Message.cpp\n    Message.h\n    NetworkManager.cpp\n    NetworkManager.h\n    SctpDataChannelProviderInterfaceImpl.cpp\n    SctpDataChannelProviderInterfaceImpl.h\n    StaticThreads.cpp\n    StaticThreads.h\n    ThreadLocalObject.h\n    TurnCustomizerImpl.cpp\n    TurnCustomizerImpl.h\n    VideoCaptureInterface.cpp\n    VideoCaptureInterface.h\n    VideoCaptureInterfaceImpl.cpp\n    VideoCaptureInterfaceImpl.h\n    VideoCapturerInterface.h\n\n    utils/gzip.cpp\n    utils/gzip.h\n\n    v2/ContentNegotiation.cpp\n    v2/ContentNegotiation.h\n    v2/DirectNetworkingImpl.cpp\n    v2/DirectNetworkingImpl.h\n    v2/ExternalSignalingConnection.cpp\n    v2/ExternalSignalingConnection.h\n    v2/InstanceNetworking.h\n    v2/InstanceV2ReferenceImpl.cpp\n    v2/InstanceV2ReferenceImpl.h\n    v2/InstanceV2Impl.cpp\n    v2/InstanceV2Impl.h\n    v2/NativeNetworkingImpl.cpp\n    v2/NativeNetworkingImpl.h\n    v2/RawTcpSocket.cpp\n    v2/RawTcpSocket.h\n    v2/ReflectorPort.cpp\n    v2/ReflectorPort.h\n    v2/ReflectorRelayPortFactory.cpp\n    v2/ReflectorRelayPortFactory.h\n    v2/Signaling.cpp\n    v2/Signaling.h\n    v2/SignalingConnection.cpp\n    v2/SignalingConnection.h\n    v2/SignalingEncryption.cpp\n    v2/SignalingEncryption.h\n    v2/SignalingSctpConnection.cpp\n    v2/SignalingSctpConnection.h\n\n    # Desktop capturer\n    desktop_capturer/DesktopCaptureSource.h\n    desktop_capturer/DesktopCaptureSource.cpp\n    desktop_capturer/DesktopCaptureSourceHelper.h\n    desktop_capturer/DesktopCaptureSourceHelper.cpp\n    desktop_capturer/DesktopCaptureSourceManager.h\n    desktop_capturer/DesktopCaptureSourceManager.cpp\n\n    # Group calls\n    group/AVIOContextImpl.cpp\n    group/AVIOContextImpl.h\n    group/AudioStreamingPart.cpp\n    group/AudioStreamingPart.h\n    group/AudioStreamingPartInternal.cpp\n    group/AudioStreamingPartInternal.h\n    group/AudioStreamingPartPersistentDecoder.cpp\n    group/AudioStreamingPartPersistentDecoder.h\n    group/GroupInstanceCustomImpl.cpp\n    group/GroupInstanceCustomImpl.h\n    group/GroupInstanceImpl.h\n    group/GroupJoinPayloadInternal.cpp\n    group/GroupJoinPayloadInternal.h\n    group/GroupJoinPayload.h\n    group/GroupNetworkManager.cpp\n    group/GroupNetworkManager.h\n    group/StreamingMediaContext.cpp\n    group/StreamingMediaContext.h\n    group/VideoStreamingPart.cpp\n    group/VideoStreamingPart.h\n\n    platform/PlatformInterface.h\n\n    # Android\n    platform/android/AndroidContext.cpp\n    platform/android/AndroidContext.h\n    platform/android/AndroidInterface.cpp\n    platform/android/AndroidInterface.h\n    platform/android/VideoCameraCapturer.cpp\n    platform/android/VideoCameraCapturer.h\n    platform/android/VideoCapturerInterfaceImpl.cpp\n    platform/android/VideoCapturerInterfaceImpl.h\n\n    # iOS / macOS\n    platform/darwin/CustomSimulcastEncoderAdapter.cpp\n    platform/darwin/CustomSimulcastEncoderAdapter.h\n    platform/darwin/DarwinFFMpeg.h\n    platform/darwin/DarwinFFMpeg.mm\n    platform/darwin/DarwinInterface.h\n    platform/darwin/DarwinInterface.mm\n    platform/darwin/DarwinVideoSource.h\n    platform/darwin/DarwinVideoSource.mm\n    platform/darwin/DesktopSharingCapturer.h\n    platform/darwin/DesktopSharingCapturer.mm\n    platform/darwin/ExtractCVPixelBuffer.h\n    platform/darwin/ExtractCVPixelBuffer.mm\n    platform/darwin/h265_nalu_rewriter.cc\n    platform/darwin/h265_nalu_rewriter.h\n    platform/darwin/objc_video_encoder_factory.h\n    platform/darwin/objc_video_encoder_factory.mm\n    platform/darwin/objc_video_decoder_factory.h\n    platform/darwin/objc_video_decoder_factory.mm\n    platform/darwin/RTCCodecSpecificInfoH265+Private.h\n    platform/darwin/RTCCodecSpecificInfoH265.h\n    platform/darwin/RTCCodecSpecificInfoH265.mm\n    platform/darwin/RTCH265ProfileLevelId.h\n    platform/darwin/RTCH265ProfileLevelId.mm\n    platform/darwin/TGCMIOCapturer.h\n    platform/darwin/TGCMIOCapturer.m\n    platform/darwin/TGCMIODevice.h\n    platform/darwin/TGCMIODevice.mm\n    platform/darwin/TGRTCCVPixelBuffer.h\n    platform/darwin/TGRTCCVPixelBuffer.mm\n    platform/darwin/TGRTCDefaultVideoDecoderFactory.h\n    platform/darwin/TGRTCDefaultVideoDecoderFactory.mm\n    platform/darwin/TGRTCDefaultVideoEncoderFactory.h\n    platform/darwin/TGRTCDefaultVideoEncoderFactory.mm\n    platform/darwin/TGRTCVideoDecoderH264.h\n    platform/darwin/TGRTCVideoDecoderH264.mm\n    platform/darwin/TGRTCVideoDecoderH265.h\n    platform/darwin/TGRTCVideoDecoderH265.mm\n    platform/darwin/TGRTCVideoEncoderH264.h\n    platform/darwin/TGRTCVideoEncoderH264.mm\n    platform/darwin/TGRTCVideoEncoderH265.h\n    platform/darwin/TGRTCVideoEncoderH265.mm\n    platform/darwin/VideoCameraCapturer.h\n    platform/darwin/VideoCameraCapturer.mm\n    platform/darwin/VideoCameraCapturerMac.h\n    platform/darwin/VideoCameraCapturerMac.mm\n    platform/darwin/VideoCapturerInterfaceImpl.h\n    platform/darwin/VideoCapturerInterfaceImpl.mm\n    platform/darwin/VideoCMIOCapture.h\n    platform/darwin/VideoCMIOCapture.mm\n    platform/darwin/VideoMetalView.h\n    platform/darwin/VideoMetalView.mm\n    platform/darwin/VideoMetalViewMac.h\n    platform/darwin/VideoMetalViewMac.mm\n\n    # POSIX\n\n    # Teleram Desktop\n    platform/tdesktop/DesktopInterface.cpp\n    platform/tdesktop/DesktopInterface.h\n    platform/tdesktop/VideoCapturerInterfaceImpl.cpp\n    platform/tdesktop/VideoCapturerInterfaceImpl.h\n    platform/tdesktop/VideoCapturerTrackSource.cpp\n    platform/tdesktop/VideoCapturerTrackSource.h\n    platform/tdesktop/VideoCameraCapturer.cpp\n    platform/tdesktop/VideoCameraCapturer.h\n\n    # third-party\n    third-party/json11.cpp\n    third-party/json11.hpp\n)\n\ntarget_link_libraries(lib_tgcalls\nPRIVATE\n    desktop-app::external_webrtc\n    desktop-app::external_ffmpeg\n    desktop-app::external_openssl\n    desktop-app::external_rnnoise\n    desktop-app::external_zlib\n)\n\ntarget_compile_definitions(lib_tgcalls\nPUBLIC\n    TGCALLS_USE_STD_OPTIONAL\nPRIVATE\n    WEBRTC_APP_TDESKTOP\n    RTC_ENABLE_H265\n    RTC_ENABLE_VP9\n)\n\nif (APPLE)\n    target_compile_options(lib_tgcalls\n    PRIVATE\n        -fobjc-arc\n    )\n    remove_target_sources(lib_tgcalls ${tgcalls_loc}\n        platform/darwin/VideoCameraCapturer.h\n        platform/darwin/VideoCameraCapturer.mm\n        platform/darwin/VideoMetalView.h\n        platform/darwin/VideoMetalView.mm\n        platform/darwin/VideoMetalViewMac.h\n        platform/darwin/VideoMetalViewMac.mm\n        platform/tdesktop/DesktopInterface.cpp\n        platform/tdesktop/DesktopInterface.h\n        platform/tdesktop/VideoCapturerInterfaceImpl.cpp\n        platform/tdesktop/VideoCapturerInterfaceImpl.h\n        platform/tdesktop/VideoCapturerTrackSource.cpp\n        platform/tdesktop/VideoCapturerTrackSource.h\n        platform/tdesktop/VideoCameraCapturer.cpp\n        platform/tdesktop/VideoCameraCapturer.h\n    )\nendif()\n\nif (NOT MSVC)\n    target_compile_options_if_exists(lib_tgcalls\n    PRIVATE\n        -Wno-deprecated-volatile\n        -Wno-ambiguous-reversed-operator\n        -Wno-deprecated-declarations\n        -Wno-unqualified-std-cast-call\n        -Wno-unused-function\n    )\nendif()\n\nremove_target_sources(lib_tgcalls ${tgcalls_loc}\n    platform/android/AndroidContext.cpp\n    platform/android/AndroidContext.h\n    platform/android/AndroidInterface.cpp\n    platform/android/AndroidInterface.h\n    platform/android/VideoCameraCapturer.cpp\n    platform/android/VideoCameraCapturer.h\n    platform/android/VideoCapturerInterfaceImpl.cpp\n    platform/android/VideoCapturerInterfaceImpl.h\n    reference/InstanceImplReference.cpp\n    reference/InstanceImplReference.h\n)\n\ntarget_include_directories(lib_tgcalls\nPUBLIC\n    ${tgcalls_dir}\nPRIVATE\n    ${tgcalls_loc}\n)\n"
  },
  {
    "path": "Telegram/cmake/qrhi_shaders.cmake",
    "content": "# Compile QRhi shaders (.vert/.frag/.comp -> .qsb) at build time.\n#\n# Usage: include(cmake/qrhi_shaders.cmake)\n# Requires: target \"Telegram\" and function \"nice_target_sources\" to exist.\n\nif (NOT EXISTS \"${CMAKE_CURRENT_SOURCE_DIR}/shaders\")\n    return()\nendif()\n\nfind_program(QSB_EXECUTABLE qsb\n    HINTS \"${QT_DIR}/../../../libexec\" \"${QT_DIR}/../../../bin\"\n    PATHS ENV PATH)\n\nif (QSB_EXECUTABLE)\n    set(_shader_dir \"${CMAKE_CURRENT_SOURCE_DIR}/shaders\")\n    set(_qsb_out_dir \"${CMAKE_CURRENT_BINARY_DIR}/shaders\")\n    file(MAKE_DIRECTORY ${_qsb_out_dir})\n    file(GLOB _shader_sources\n        \"${_shader_dir}/*.vert\"\n        \"${_shader_dir}/*.frag\"\n        \"${_shader_dir}/*.comp\")\n    set(_qsb_outputs)\n    set(_qrc_entries)\n    foreach(_src ${_shader_sources})\n        get_filename_component(_name ${_src} NAME)\n        get_filename_component(_ext ${_src} LAST_EXT)\n        set(_qsb \"${_qsb_out_dir}/${_name}.qsb\")\n\n        if(\"${_ext}\" STREQUAL \".comp\")\n            set(_glsl_ver \"310es,430\")\n        else()\n            set(_glsl_ver \"100es,120,150\")\n        endif()\n\n        add_custom_command(\n            OUTPUT ${_qsb}\n            COMMAND ${QSB_EXECUTABLE}\n                --glsl \"${_glsl_ver}\"\n                --hlsl 50\n                --msl 12\n                -o ${_qsb}\n                ${_src}\n            DEPENDS ${_src}\n            COMMENT \"QSB: ${_name}\"\n            VERBATIM)\n        list(APPEND _qsb_outputs ${_qsb})\n        list(APPEND _qrc_entries \"        <file>${_name}.qsb</file>\")\n    endforeach()\n    list(SORT _qrc_entries)\n    list(JOIN _qrc_entries \"\\n\" _qrc_body)\n    file(WRITE \"${_qsb_out_dir}/shaders.qrc\"\n        \"<RCC>\\n    <qresource prefix=\\\"/shaders\\\">\\n${_qrc_body}\\n    </qresource>\\n</RCC>\\n\")\n    add_custom_target(compile_shaders DEPENDS ${_qsb_outputs})\n    nice_target_sources(Telegram ${_qsb_out_dir}\n    PRIVATE\n        shaders.qrc\n    )\n    add_dependencies(Telegram compile_shaders)\n    message(STATUS \"QSB: found ${QSB_EXECUTABLE}, will compile ${_shader_dir}/*.vert/*.frag/*.comp\")\nelse()\n    message(STATUS \"QSB: not found, shaders will not be compiled\")\nendif()\n"
  },
  {
    "path": "Telegram/cmake/td_export.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nadd_library(td_export OBJECT)\ninit_non_host_target(td_export)\nadd_library(tdesktop::td_export ALIAS td_export)\n\ntarget_precompile_headers(td_export PRIVATE ${src_loc}/export/export_pch.h)\nnice_target_sources(td_export ${src_loc}\nPRIVATE\n    export/export_api_wrap.cpp\n    export/export_api_wrap.h\n    export/export_controller.cpp\n    export/export_controller.h\n    export/export_pch.h\n    export/export_settings.cpp\n    export/export_settings.h\n    export/data/export_data_types.cpp\n    export/data/export_data_types.h\n    export/output/export_output_abstract.cpp\n    export/output/export_output_abstract.h\n    export/output/export_output_file.cpp\n    export/output/export_output_file.h\n    export/output/export_output_html.cpp\n    export/output/export_output_html.h\n    export/output/export_output_html_and_json.cpp\n    export/output/export_output_html_and_json.h\n    export/output/export_output_json.cpp\n    export/output/export_output_json.h\n    export/output/export_output_result.h\n    export/output/export_output_stats.cpp\n    export/output/export_output_stats.h\n)\n\ntarget_include_directories(td_export\nPUBLIC\n    ${src_loc}\n)\n\ntarget_link_libraries(td_export\nPUBLIC\n    desktop-app::lib_base\n    tdesktop::td_scheme\n)\n"
  },
  {
    "path": "Telegram/cmake/td_forkgram.cmake",
    "content": "# This file is part of Forkgram.\n\nadd_library(td_forkgram OBJECT)\ninit_target(td_forkgram)\nadd_library(tdesktop::td_forkgram ALIAS td_forkgram)\n\nnice_target_sources(td_forkgram ${src_loc}\nPRIVATE\n    forkgram/uri_open.cpp\n    forkgram/uri_open.h\n)\n\nif (WIN32)\n    nuget_add_winrt(td_forkgram)\nendif()\n\ntarget_include_directories(td_forkgram\nPUBLIC\n    ${src_loc}\n)\n\ntarget_link_libraries(td_forkgram\nPUBLIC\n    tdesktop::td_lang\n    desktop-app::lib_base\n)\n"
  },
  {
    "path": "Telegram/cmake/td_iv.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nadd_library(td_iv OBJECT)\ninit_non_host_target(td_iv)\nadd_library(tdesktop::td_iv ALIAS td_iv)\n\ntarget_precompile_headers(td_iv PRIVATE ${src_loc}/iv/iv_pch.h)\nnice_target_sources(td_iv ${src_loc}\nPRIVATE\n    iv/iv_controller.cpp\n    iv/iv_controller.h\n    iv/iv_data.cpp\n    iv/iv_data.h\n    iv/iv_delegate.h\n    iv/iv_pch.h\n    iv/iv_prepare.cpp\n    iv/iv_prepare.h\n)\n\nnice_target_sources(td_iv ${res_loc}\nPRIVATE\n    iv_html/page.css\n    iv_html/page.js\n)\n\ntarget_include_directories(td_iv\nPUBLIC\n    ${src_loc}\n)\n\ntarget_link_libraries(td_iv\nPUBLIC\n    desktop-app::lib_ui\n    tdesktop::td_scheme\nPRIVATE\n    desktop-app::lib_webview\n    desktop-app::external_ada\n    tdesktop::td_lang\n    tdesktop::td_ui\n)\n"
  },
  {
    "path": "Telegram/cmake/td_lang.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nadd_library(td_lang OBJECT)\ninit_non_host_target(td_lang)\nadd_library(tdesktop::td_lang ALIAS td_lang)\n\ninclude(cmake/generate_lang.cmake)\n\ngenerate_lang(td_lang ${res_loc}/langs/lang.strings)\n\ntarget_precompile_headers(td_lang PRIVATE ${src_loc}/lang/lang_pch.h)\nnice_target_sources(td_lang ${src_loc}\nPRIVATE\n    lang/lang_file_parser.cpp\n    lang/lang_file_parser.h\n    lang/lang_hardcoded.h\n    lang/lang_keys.cpp\n    lang/lang_keys.h\n    lang/lang_pch.h\n    lang/lang_tag.cpp\n    lang/lang_tag.h\n    lang/lang_text_entity.cpp\n    lang/lang_text_entity.h\n    lang/lang_values.h\n)\n\ntarget_include_directories(td_lang\nPUBLIC\n    ${src_loc}\n)\n\ntarget_link_libraries(td_lang\nPUBLIC\n    desktop-app::lib_ui\n)\n"
  },
  {
    "path": "Telegram/cmake/td_mtproto.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nadd_library(td_mtproto OBJECT)\ninit_non_host_target(td_mtproto)\nadd_library(tdesktop::td_mtproto ALIAS td_mtproto)\n\ntarget_precompile_headers(td_mtproto PRIVATE ${src_loc}/mtproto/mtproto_pch.h)\nnice_target_sources(td_mtproto ${src_loc}\nPRIVATE\n    mtproto/details/mtproto_abstract_socket.cpp\n    mtproto/details/mtproto_abstract_socket.h\n    mtproto/details/mtproto_bound_key_creator.cpp\n    mtproto/details/mtproto_bound_key_creator.h\n    mtproto/details/mtproto_dc_key_binder.cpp\n    mtproto/details/mtproto_dc_key_binder.h\n    mtproto/details/mtproto_dc_key_creator.cpp\n    mtproto/details/mtproto_dc_key_creator.h\n    mtproto/details/mtproto_dcenter.cpp\n    mtproto/details/mtproto_dcenter.h\n    mtproto/details/mtproto_domain_resolver.cpp\n    mtproto/details/mtproto_domain_resolver.h\n    mtproto/details/mtproto_dump_to_text.cpp\n    mtproto/details/mtproto_dump_to_text.h\n    mtproto/details/mtproto_received_ids_manager.cpp\n    mtproto/details/mtproto_received_ids_manager.h\n    mtproto/details/mtproto_rsa_public_key.cpp\n    mtproto/details/mtproto_rsa_public_key.h\n    mtproto/details/mtproto_serialized_request.cpp\n    mtproto/details/mtproto_serialized_request.h\n    mtproto/details/mtproto_tcp_socket.cpp\n    mtproto/details/mtproto_tcp_socket.h\n    mtproto/details/mtproto_tls_socket.cpp\n    mtproto/details/mtproto_tls_socket.h\n    mtproto/mtproto_auth_key.cpp\n    mtproto/mtproto_auth_key.h\n    mtproto/mtproto_concurrent_sender.cpp\n    mtproto/mtproto_concurrent_sender.h\n    mtproto/mtproto_config.cpp\n    mtproto/mtproto_config.h\n    mtproto/mtproto_dc_options.cpp\n    mtproto/mtproto_dc_options.h\n    mtproto/mtproto_dh_utils.cpp\n    mtproto/mtproto_dh_utils.h\n    mtproto/mtproto_pch.h\n    mtproto/mtproto_proxy_data.cpp\n    mtproto/mtproto_proxy_data.h\n    mtproto/mtproto_response.cpp\n    mtproto/mtproto_response.h\n)\n\ntarget_include_directories(td_mtproto\nPUBLIC\n    ${src_loc}\n)\n\ntarget_link_libraries(td_mtproto\nPUBLIC\n    tdesktop::td_scheme\nPRIVATE\n    desktop-app::external_zlib\n)\n"
  },
  {
    "path": "Telegram/cmake/td_scheme.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nadd_library(td_scheme OBJECT)\ninit_non_host_target(td_scheme)\nadd_library(tdesktop::td_scheme ALIAS td_scheme)\n\ninclude(cmake/generate_scheme.cmake)\n\nset(scheme_files\n    ${src_loc}/mtproto/scheme/api.tl\n    ${src_loc}/mtproto/scheme/mtproto.tl\n)\n\ngenerate_scheme(td_scheme ${src_loc}/codegen/scheme/codegen_scheme.py \"${scheme_files}\")\n\nnice_target_sources(td_scheme ${src_loc}/mtproto/scheme\nPRIVATE\n    api.tl\n    mtproto.tl\n)\n\ntarget_include_directories(td_scheme\nPUBLIC\n    ${src_loc}\n)\n\ntarget_link_libraries(td_scheme\nPUBLIC\n    desktop-app::lib_base\n    desktop-app::lib_tl\n)\n\nif (CMAKE_SYSTEM_PROCESSOR STREQUAL \"mips64\")\n    # Sometimes final linking may fail with error \"relocation truncated to fit\"\n    # due to large scheme size.\n    target_compile_options(td_scheme\n    PRIVATE\n        -mxgot\n    )\nendif()\n"
  },
  {
    "path": "Telegram/cmake/td_tde2e.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nadd_library(td_tde2e OBJECT)\ninit_non_host_target(td_tde2e)\nadd_library(tdesktop::td_tde2e ALIAS td_tde2e)\n\nnice_target_sources(td_tde2e ${src_loc}\nPRIVATE\n    tde2e/tde2e_api.cpp\n    tde2e/tde2e_api.h\n)\n\ntarget_include_directories(td_tde2e\nPUBLIC\n    ${src_loc}\n)\n\ntarget_link_libraries(td_tde2e\nPUBLIC\n    desktop-app::lib_base\nPRIVATE\n    desktop-app::external_tde2e\n)\n\n\n\n"
  },
  {
    "path": "Telegram/cmake/td_ui.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nadd_library(td_ui OBJECT)\ninit_non_host_target(td_ui)\nadd_library(tdesktop::td_ui ALIAS td_ui)\n\ninclude(lib_ui/cmake/generate_styles.cmake)\ninclude(cmake/generate_numbers.cmake)\n\nset(style_files\n    ui/td_common.style\n    ui/filter_icons.style\n    ui/menu_icons.style\n    ui/chat/chat.style\n    ui/effects/credits.style\n    ui/effects/premium.style\n    ui/color_indices.style\n    boxes/boxes.style\n    boxes/polls.style\n    dialogs/dialogs.style\n    chat_helpers/chat_helpers.style\n    calls/calls.style\n    export/view/export.style\n    info/info.style\n    info/channel_statistics/boosts/giveaway/giveaway.style\n    info/channel_statistics/earn/channel_earn.style\n    info/profile/info_levels.style\n    info/userpic/info_userpic_builder.style\n    intro/intro.style\n    iv/iv.style\n    media/player/media_player.style\n    media/stories/media_stories.style\n    passport/passport.style\n    payments/ui/payments.style\n    profile/profile.style\n    settings/settings.style\n    media/view/media_view.style\n    overview/overview.style\n    window/window.style\n    editor/editor.style\n    statistics/statistics.style\n)\n\nset(dependent_style_files\n    ${submodules_loc}/lib_ui/ui/colors.palette\n    ${submodules_loc}/lib_ui/ui/basic.style\n    ${submodules_loc}/lib_ui/ui/layers/layers.style\n    ${submodules_loc}/lib_ui/ui/widgets/widgets.style\n)\n\ngenerate_styles(td_ui ${src_loc} \"${style_files}\" \"${dependent_style_files}\")\n\ntarget_precompile_headers(td_ui PRIVATE ${src_loc}/ui/ui_pch.h)\nnice_target_sources(td_ui ${src_loc}\nPRIVATE\n    ${style_files}\n\n    boxes/peers/edit_peer_history_visibility_box.cpp\n    boxes/peers/edit_peer_history_visibility_box.h\n    boxes/peers/toggle_topics_box.cpp\n    boxes/peers/toggle_topics_box.h\n\n    boxes/url_auth_box_content.cpp\n    boxes/url_auth_box_content.h\n    boxes/translate_box_content.cpp\n    boxes/translate_box_content.h\n\n    calls/group/ui/calls_group_recording_box.cpp\n    calls/group/ui/calls_group_recording_box.h\n    calls/group/ui/calls_group_scheduled_labels.cpp\n    calls/group/ui/calls_group_scheduled_labels.h\n    calls/group/ui/calls_group_stars_coloring.cpp\n    calls/group/ui/calls_group_stars_coloring.h\n    calls/group/ui/desktop_capture_choose_source.cpp\n    calls/group/ui/desktop_capture_choose_source.h\n    calls/ui/calls_device_menu.cpp\n    calls/ui/calls_device_menu.h\n\n    chat_helpers/field_characters_count_manager.cpp\n    chat_helpers/field_characters_count_manager.h\n    chat_helpers/stickers_emoji_image_loader.cpp\n    chat_helpers/stickers_emoji_image_loader.h\n\n    core/current_geo_location.cpp\n    core/current_geo_location.h\n    core/file_location.cpp\n    core/file_location.h\n    core/mime_type.cpp\n    core/mime_type.h\n\n    countries/countries_instance.cpp\n    countries/countries_instance.h\n\n    data/raw/raw_countries_bounds.cpp\n    data/raw/raw_countries_bounds.h\n    data/data_authorization.h\n    data/data_birthday.cpp\n    data/data_birthday.h\n    data/data_channel_earn.h\n    data/data_credits.h\n    data/data_credits_earn.h\n    data/data_passkey_deserialize.cpp\n    data/data_passkey_deserialize.h\n    data/data_peer_colors.h\n    data/data_premium_subscription_option.h\n    data/data_statistics_chart.cpp\n    data/data_statistics_chart.h\n    data/data_subscriptions.h\n\n    dialogs/dialogs_common.h\n    dialogs/dialogs_three_state_icon.h\n    dialogs/ui/chat_search_empty.cpp\n    dialogs/ui/chat_search_empty.h\n    dialogs/ui/chat_search_in.cpp\n    dialogs/ui/chat_search_in.h\n    dialogs/ui/dialogs_quick_action_context.h\n    dialogs/ui/dialogs_quick_action.h\n    dialogs/ui/dialogs_stories_list.cpp\n    dialogs/ui/dialogs_stories_list.h\n    dialogs/ui/dialogs_top_bar_suggestion_content.cpp\n    dialogs/ui/dialogs_top_bar_suggestion_content.h\n    dialogs/ui/posts_search_intro.cpp\n    dialogs/ui/posts_search_intro.h\n    dialogs/ui/top_peers_strip.cpp\n    dialogs/ui/top_peers_strip.h\n\n    editor/controllers/undo_controller.cpp\n    editor/controllers/undo_controller.h\n    editor/editor_crop.cpp\n    editor/editor_crop.h\n    editor/editor_layer_widget.cpp\n    editor/editor_layer_widget.h\n    editor/photo_editor_common.cpp\n    editor/photo_editor_common.h\n    editor/photo_editor_inner_common.h\n    editor/scene/scene.cpp\n    editor/scene/scene.h\n    editor/scene/scene_item_base.cpp\n    editor/scene/scene_item_base.h\n    editor/scene/scene_item_canvas.cpp\n    editor/scene/scene_item_canvas.h\n    editor/scene/scene_item_image.cpp\n    editor/scene/scene_item_image.h\n    editor/scene/scene_item_line.cpp\n    editor/scene/scene_item_line.h\n\n    ui/boxes/about_cocoon_box.h\n    ui/boxes/about_cocoon_box.cpp\n\n    history/admin_log/history_admin_log_filter_value.h\n    history/history_view_top_toast.cpp\n    history/history_view_top_toast.h\n    history/view/controls/history_view_characters_limit.cpp\n    history/view/controls/history_view_characters_limit.h\n    history/view/controls/history_view_voice_record_button.cpp\n    history/view/controls/history_view_voice_record_button.h\n\n    info/info_flexible_scroll.cpp\n    info/info_flexible_scroll.h\n\n    info/profile/info_profile_icon.cpp\n    info/profile/info_profile_icon.h\n    info/profile/info_profile_music_button.cpp\n    info/profile/info_profile_music_button.h\n    info/profile/info_profile_text.cpp\n    info/profile/info_profile_text.h\n    info/profile/info_profile_top_bar_action_button.cpp\n    info/profile/info_profile_top_bar_action_button.h\n    info/userpic/info_userpic_bubble_wrap.cpp\n    info/userpic/info_userpic_bubble_wrap.h\n    info/userpic/info_userpic_color_circle_button.cpp\n    info/userpic/info_userpic_color_circle_button.h\n    info/userpic/info_userpic_emoji_builder_layer.cpp\n    info/userpic/info_userpic_emoji_builder_layer.h\n\n    info/channel_statistics/boosts/giveaway/boost_badge.cpp\n    info/channel_statistics/boosts/giveaway/boost_badge.h\n    info/channel_statistics/boosts/giveaway/giveaway_type_row.cpp\n    info/channel_statistics/boosts/giveaway/giveaway_type_row.h\n    info/channel_statistics/boosts/giveaway/select_countries_box.cpp\n    info/channel_statistics/boosts/giveaway/select_countries_box.h\n\n    info/channel_statistics/earn/earn_format.cpp\n    info/channel_statistics/earn/earn_format.h\n    info/channel_statistics/earn/earn_icons.cpp\n    info/channel_statistics/earn/earn_icons.h\n\n    intro/intro_code_input.cpp\n    intro/intro_code_input.h\n\n    layout/abstract_layout_item.cpp\n    layout/abstract_layout_item.h\n    layout/layout_mosaic.cpp\n    layout/layout_mosaic.h\n    layout/layout_position.cpp\n    layout/layout_position.h\n    layout/layout_selection.cpp\n    layout/layout_selection.h\n\n    media/clip/media_clip_check_streaming.cpp\n    media/clip/media_clip_check_streaming.h\n    media/clip/media_clip_ffmpeg.cpp\n    media/clip/media_clip_ffmpeg.h\n    media/clip/media_clip_implementation.cpp\n    media/clip/media_clip_implementation.h\n    media/clip/media_clip_reader.cpp\n    media/clip/media_clip_reader.h\n\n    media/player/media_player_button.cpp\n    media/player/media_player_button.h\n    media/player/media_player_dropdown.cpp\n    media/player/media_player_dropdown.h\n\n    media/media_common.h\n\n    menu/gift_resale_filter.cpp\n    menu/gift_resale_filter.h\n    menu/menu_checked_action.cpp\n    menu/menu_checked_action.h\n    menu/menu_check_item.cpp\n    menu/menu_check_item.h\n    menu/menu_item_rate_transcribe.cpp\n    menu/menu_item_rate_transcribe.h\n    menu/menu_timecode_action.cpp\n    menu/menu_timecode_action.h\n    menu/menu_ttl.cpp\n    menu/menu_ttl.h\n\n    passport/ui/passport_details_row.cpp\n    passport/ui/passport_details_row.h\n    passport/ui/passport_form_row.cpp\n    passport/ui/passport_form_row.h\n\n    payments/ui/payments_edit_card.cpp\n    payments/ui/payments_edit_card.h\n    payments/ui/payments_edit_information.cpp\n    payments/ui/payments_edit_information.h\n    payments/ui/payments_form_summary.cpp\n    payments/ui/payments_form_summary.h\n    payments/ui/payments_field.cpp\n    payments/ui/payments_field.h\n    payments/ui/payments_panel.cpp\n    payments/ui/payments_panel.h\n    payments/ui/payments_panel_data.h\n    payments/ui/payments_panel_delegate.h\n    payments/ui/payments_reaction_box.cpp\n    payments/ui/payments_reaction_box.h\n\n    platform/linux/current_geo_location_linux.cpp\n    platform/linux/current_geo_location_linux.h\n    platform/linux/text_recognition_linux.h\n    platform/mac/file_bookmark_mac.h\n    platform/mac/file_bookmark_mac.mm\n    platform/mac/current_geo_location_mac.h\n    platform/mac/current_geo_location_mac.mm\n    platform/mac/text_recognition_mac.h\n    platform/mac/text_recognition_mac.mm\n    platform/win/current_geo_location_win.cpp\n    platform/win/current_geo_location_win.h\n    platform/win/text_recognition_win.h\n    platform/platform_file_bookmark.h\n    platform/platform_current_geo_location.h\n    platform/platform_text_recognition.h\n\n    profile/profile_back_button.cpp\n    profile/profile_back_button.h\n\n    settings/settings_common.cpp\n    settings/settings_common.h\n    settings/cloud_password/settings_cloud_password_common.cpp\n    settings/cloud_password/settings_cloud_password_common.h\n\n    statistics/chart_lines_filter_controller.cpp\n    statistics/chart_lines_filter_controller.h\n    statistics/chart_rulers_data.cpp\n    statistics/chart_rulers_data.h\n    statistics/chart_widget.cpp\n    statistics/chart_widget.h\n    statistics/segment_tree.cpp\n    statistics/segment_tree.h\n    statistics/statistics_common.h\n    statistics/statistics_data_deserialize.cpp\n    statistics/statistics_data_deserialize.h\n    statistics/statistics_format_values.cpp\n    statistics/statistics_format_values.h\n    statistics/statistics_graphics.cpp\n    statistics/statistics_graphics.h\n    statistics/statistics_types.h\n    statistics/view/abstract_chart_view.cpp\n    statistics/view/abstract_chart_view.h\n    statistics/view/bar_chart_view.cpp\n    statistics/view/bar_chart_view.h\n    statistics/view/chart_rulers_view.cpp\n    statistics/view/chart_rulers_view.h\n    statistics/view/chart_view_factory.cpp\n    statistics/view/chart_view_factory.h\n    statistics/view/linear_chart_view.cpp\n    statistics/view/linear_chart_view.h\n    statistics/view/stack_chart_common.cpp\n    statistics/view/stack_chart_common.h\n    statistics/view/stack_linear_chart_common.cpp\n    statistics/view/stack_linear_chart_common.h\n    statistics/view/stack_linear_chart_view.cpp\n    statistics/view/stack_linear_chart_view.h\n    statistics/widgets/chart_header_widget.cpp\n    statistics/widgets/chart_header_widget.h\n    statistics/widgets/chart_lines_filter_widget.cpp\n    statistics/widgets/chart_lines_filter_widget.h\n    statistics/widgets/point_details_widget.cpp\n    statistics/widgets/point_details_widget.h\n\n    ui/boxes/auto_delete_settings.cpp\n    ui/boxes/auto_delete_settings.h\n    ui/boxes/boost_box.cpp\n    ui/boxes/boost_box.h\n    ui/boxes/calendar_box.cpp\n    ui/boxes/calendar_box.h\n    ui/boxes/choose_date_time.cpp\n    ui/boxes/choose_date_time.h\n    ui/boxes/choose_font_box.cpp\n    ui/boxes/choose_font_box.h\n    ui/boxes/choose_language_box.cpp\n    ui/boxes/choose_language_box.h\n    ui/boxes/choose_time.cpp\n    ui/boxes/choose_time.h\n    ui/boxes/collectible_info_box.cpp\n    ui/boxes/collectible_info_box.h\n    ui/boxes/confirm_box.cpp\n    ui/boxes/confirm_box.h\n    ui/boxes/confirm_phone_box.cpp\n    ui/boxes/confirm_phone_box.h\n    ui/boxes/country_select_box.cpp\n    ui/boxes/country_select_box.h\n    ui/boxes/edit_birthday_box.cpp\n    ui/boxes/edit_birthday_box.h\n    ui/boxes/edit_factcheck_box.cpp\n    ui/boxes/edit_factcheck_box.h\n    ui/boxes/edit_invite_link.cpp\n    ui/boxes/edit_invite_link.h\n    ui/boxes/rate_call_box.cpp\n    ui/boxes/rate_call_box.h\n    ui/boxes/report_box_graphics.cpp\n    ui/boxes/report_box_graphics.h\n    ui/boxes/show_or_premium_box.cpp\n    ui/boxes/show_or_premium_box.h\n    ui/boxes/single_choice_box.cpp\n    ui/boxes/single_choice_box.h\n    ui/boxes/time_picker_box.cpp\n    ui/boxes/time_picker_box.h\n\n    ui/chat/attach/attach_abstract_single_file_preview.cpp\n    ui/chat/attach/attach_abstract_single_file_preview.h\n    ui/chat/attach/attach_abstract_single_media_preview.cpp\n    ui/chat/attach/attach_abstract_single_media_preview.h\n    ui/chat/attach/attach_abstract_single_preview.h\n    ui/chat/attach/attach_album_preview.cpp\n    ui/chat/attach/attach_album_preview.h\n    ui/chat/attach/attach_album_thumbnail.cpp\n    ui/chat/attach/attach_album_thumbnail.h\n    ui/chat/attach/attach_bot_downloads.cpp\n    ui/chat/attach/attach_bot_downloads.h\n    ui/chat/attach/attach_bot_webview.cpp\n    ui/chat/attach/attach_bot_webview.h\n    ui/chat/attach/attach_controls.cpp\n    ui/chat/attach/attach_controls.h\n    ui/chat/attach/attach_extensions.cpp\n    ui/chat/attach/attach_extensions.h\n    ui/chat/attach/attach_prepare.cpp\n    ui/chat/attach/attach_prepare.h\n    ui/chat/attach/attach_send_files_way.cpp\n    ui/chat/attach/attach_send_files_way.h\n    ui/chat/attach/attach_single_file_preview.cpp\n    ui/chat/attach/attach_single_file_preview.h\n    ui/chat/attach/attach_single_media_preview.cpp\n    ui/chat/attach/attach_single_media_preview.h\n    ui/chat/chat_style.cpp\n    ui/chat/chat_style.h\n    ui/chat/chat_style_radius.cpp\n    ui/chat/chat_style_radius.h\n    ui/chat/chat_theme.cpp\n    ui/chat/chat_theme.h\n    ui/chat/chats_filter_tag.cpp\n    ui/chat/chats_filter_tag.h\n    ui/chat/continuous_scroll.cpp\n    ui/chat/continuous_scroll.h\n    ui/chat/forward_options_box.cpp\n    ui/chat/forward_options_box.h\n    ui/chat/group_call_bar.cpp\n    ui/chat/group_call_bar.h\n    ui/chat/group_call_userpics.cpp\n    ui/chat/group_call_userpics.h\n    ui/chat/message_bar.cpp\n    ui/chat/message_bar.h\n    ui/chat/message_bubble.cpp\n    ui/chat/message_bubble.h\n    ui/chat/more_chats_bar.cpp\n    ui/chat/more_chats_bar.h\n    ui/chat/pinned_bar.cpp\n    ui/chat/pinned_bar.h\n    ui/chat/requests_bar.cpp\n    ui/chat/requests_bar.h\n    ui/controls/button_labels.cpp\n    ui/controls/button_labels.h\n    ui/controls/call_mute_button.cpp\n    ui/controls/call_mute_button.h\n    ui/controls/chat_service_checkbox.cpp\n    ui/controls/chat_service_checkbox.h\n    ui/controls/delete_message_context_action.cpp\n    ui/controls/delete_message_context_action.h\n    ui/controls/download_bar.cpp\n    ui/controls/download_bar.h\n    ui/controls/dynamic_images_strip.cpp\n    ui/controls/dynamic_images_strip.h\n    ui/controls/emoji_button.cpp\n    ui/controls/emoji_button.h\n    ui/controls/feature_list.cpp\n    ui/controls/feature_list.h\n    ui/controls/filter_link_header.cpp\n    ui/controls/filter_link_header.h\n    ui/controls/jump_down_button.cpp\n    ui/controls/jump_down_button.h\n    ui/controls/invite_link_buttons.cpp\n    ui/controls/invite_link_buttons.h\n    ui/controls/invite_link_label.cpp\n    ui/controls/invite_link_label.h\n    ui/controls/labeled_emoji_tabs.cpp\n    ui/controls/labeled_emoji_tabs.h\n    ui/controls/peer_list_dummy.cpp\n    ui/controls/peer_list_dummy.h\n    ui/controls/popup_selector.cpp\n    ui/controls/popup_selector.h\n    ui/controls/round_video_recorder_data.h\n    ui/controls/round_video_recorder.cpp\n    ui/controls/round_video_recorder.h\n    ui/controls/send_as_button.cpp\n    ui/controls/send_as_button.h\n    ui/controls/send_button.cpp\n    ui/controls/send_button.h\n    ui/controls/stars_rating.cpp\n    ui/controls/stars_rating.h\n    ui/controls/subsection_tabs_slider.cpp\n    ui/controls/subsection_tabs_slider.h\n    ui/controls/subsection_tabs_slider_reorder.cpp\n    ui/controls/subsection_tabs_slider_reorder.h\n    ui/controls/sub_tabs.cpp\n    ui/controls/sub_tabs.h\n    ui/controls/swipe_handler.cpp\n    ui/controls/swipe_handler.h\n    ui/controls/swipe_handler_data.h\n    ui/controls/tabbed_search.cpp\n    ui/controls/tabbed_search.h\n    ui/controls/ton_common.cpp\n    ui/controls/ton_common.h\n    ui/controls/who_reacted_context_action.cpp\n    ui/controls/who_reacted_context_action.h\n    ui/controls/window_outdated_bar.cpp\n    ui/controls/window_outdated_bar_dummy.cpp\n    ui/controls/window_outdated_bar.h\n    ui/controls/window_screen_reader_bar.cpp\n    ui/controls/window_screen_reader_bar.h\n    ui/effects/fireworks_animation.cpp\n    ui/effects/fireworks_animation.h\n    ui/effects/glare.cpp\n    ui/effects/glare.h\n    ui/effects/loading_element.cpp\n    ui/effects/loading_element.h\n    ui/effects/ministar_particles.cpp\n    ui/effects/ministar_particles.h\n    ui/effects/outline_segments.cpp\n    ui/effects/outline_segments.h\n    ui/effects/premium_bubble.cpp\n    ui/effects/premium_bubble.h\n    ui/effects/premium_graphics.cpp\n    ui/effects/premium_graphics.h\n    ui/effects/premium_stars.cpp\n    ui/effects/premium_stars.h\n    ui/effects/premium_stars_colored.cpp\n    ui/effects/premium_stars_colored.h\n    ui/effects/premium_top_bar.cpp\n    ui/effects/premium_top_bar.h\n    ui/effects/round_checkbox.cpp\n    ui/effects/round_checkbox.h\n    ui/effects/scroll_content_shadow.cpp\n    ui/effects/scroll_content_shadow.h\n    ui/effects/skeleton_animation.cpp\n    ui/effects/skeleton_animation.h\n    ui/effects/shake_animation.cpp\n    ui/effects/shake_animation.h\n    ui/effects/snowflakes.cpp\n    ui/effects/snowflakes.h\n    ui/effects/toggle_arrow.cpp\n    ui/effects/toggle_arrow.h\n    ui/effects/upload_progress_overlay.cpp\n    ui/effects/upload_progress_overlay.h\n    ui/effects/ttl_icon.cpp\n    ui/effects/ttl_icon.h\n    ui/search_field_controller.cpp\n    ui/search_field_controller.h\n    ui/text/format_song_name.cpp\n    ui/text/format_song_name.h\n    ui/text/format_values.cpp\n    ui/text/format_values.h\n    ui/text/text_lottie_custom_emoji.cpp\n    ui/text/text_lottie_custom_emoji.h\n    ui/text/text_options.cpp\n    ui/text/text_options.h\n\n    ui/image/svg_preview.cpp\n    ui/image/svg_preview.h\n\n    ui/widgets/fields/special_fields.cpp\n    ui/widgets/fields/special_fields.h\n    ui/widgets/fields/time_part_input_with_placeholder.cpp\n    ui/widgets/fields/time_part_input_with_placeholder.h\n\n    ui/widgets/chat_filters_tabs_slider.cpp\n    ui/widgets/chat_filters_tabs_slider.h\n    ui/widgets/chat_filters_tabs_slider_reorder.cpp\n    ui/widgets/chat_filters_tabs_slider_reorder.h\n    ui/widgets/cross_fade_label.cpp\n    ui/widgets/cross_fade_label.h\n    ui/widgets/color_editor.cpp\n    ui/widgets/color_editor.h\n    ui/widgets/continuous_sliders.cpp\n    ui/widgets/continuous_sliders.h\n    ui/widgets/discrete_sliders.cpp\n    ui/widgets/discrete_sliders.h\n    ui/widgets/gradient_round_button.cpp\n    ui/widgets/gradient_round_button.h\n    ui/widgets/horizontal_fit_container.cpp\n    ui/widgets/horizontal_fit_container.h\n    ui/widgets/level_meter.cpp\n    ui/widgets/level_meter.h\n    ui/widgets/middle_click_autoscroll.cpp\n    ui/widgets/middle_click_autoscroll.h\n    ui/widgets/multi_select.cpp\n    ui/widgets/multi_select.h\n    ui/widgets/sent_code_field.cpp\n    ui/widgets/sent_code_field.h\n    ui/widgets/participants_check_view.cpp\n    ui/widgets/participants_check_view.h\n    ui/widgets/slider_natural_width.h\n    ui/widgets/vertical_drum_picker.cpp\n    ui/widgets/vertical_drum_picker.h\n\n    ui/cached_round_corners.cpp\n    ui/cached_round_corners.h\n    ui/color_contrast.cpp\n    ui/color_contrast.h\n    ui/color_int_conversion.cpp\n    ui/color_int_conversion.h\n    ui/empty_userpic.cpp\n    ui/empty_userpic.h\n    ui/grouped_layout.cpp\n    ui/grouped_layout.h\n    ui/new_badges.cpp\n    ui/new_badges.h\n    ui/peer/color_sample.cpp\n    ui/peer/color_sample.h\n    ui/power_saving.cpp\n    ui/power_saving.h\n    ui/vertical_list.cpp\n    ui/vertical_list.h\n    ui/unread_badge_paint.cpp\n    ui/unread_badge_paint.h\n    ui/unread_counter_format.cpp\n    ui/unread_counter_format.h\n    ui/userpic_view.cpp\n    ui/userpic_view.h\n    ui/webview_helpers.cpp\n    ui/webview_helpers.h\n\n    window/window_slide_animation.cpp\n    window/window_slide_animation.h\n\n    ui/ui_pch.h\n)\n\nnice_target_sources(td_ui ${res_loc}\nPRIVATE\n    picker_html/picker.css\n    picker_html/picker.js\n)\n\nif (DESKTOP_APP_SPECIAL_TARGET)\n    remove_target_sources(td_ui ${src_loc}\n        ui/controls/window_outdated_bar_dummy.cpp\n    )\nelse()\n    remove_target_sources(td_ui ${src_loc}\n        ui/controls/window_outdated_bar.cpp\n    )\nendif()\n\ntarget_include_directories(td_ui\nPUBLIC\n    ${src_loc}\n)\n\ntarget_link_libraries(td_ui\nPUBLIC\n    tdesktop::td_lang\n    desktop-app::lib_ui\n    desktop-app::lib_lottie\nPRIVATE\n    tdesktop::lib_tgcalls\n    desktop-app::lib_ffmpeg\n    desktop-app::lib_webview\n    desktop-app::lib_webrtc\n    desktop-app::lib_spellcheck\n    desktop-app::lib_stripe\n    desktop-app::external_kcoreaddons\n    desktop-app::external_webrtc\n)\n"
  },
  {
    "path": "Telegram/cmake/telegram_apple_swift_runtime.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nfunction(telegram_add_apple_swift_runtime target_name)\n    if (NOT APPLE)\n        return()\n    endif()\n\n    execute_process(\n        COMMAND xcode-select -p\n        OUTPUT_VARIABLE DEVELOPER_DIR\n        OUTPUT_STRIP_TRAILING_WHITESPACE\n    )\n\n    set(SWIFT_LIB_DIR\n        \"${DEVELOPER_DIR}/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx\")\n\n    target_link_options(${target_name}\n    PRIVATE\n        \"-L${SWIFT_LIB_DIR}\"\n        \"-Wl,-rpath,/usr/lib/swift\"\n        \"-Wl,-rpath,@executable_path/../Frameworks\"\n    )\n\n    add_custom_command(TARGET ${target_name} POST_BUILD\n        COMMAND mkdir -p $<TARGET_FILE_DIR:${target_name}>/../Frameworks\n        COMMAND xcrun swift-stdlib-tool\n            --copy\n            --platform macosx\n            --scan-executable $<TARGET_FILE:${target_name}>\n            --destination $<TARGET_FILE_DIR:${target_name}>/../Frameworks\n        VERBATIM\n    )\nendfunction()"
  },
  {
    "path": "Telegram/cmake/telegram_options.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\noption(TDESKTOP_API_TEST \"Use test API credentials.\" OFF)\nset(TDESKTOP_API_ID \"0\" CACHE STRING \"Provide 'api_id' for the Telegram API access.\")\nset(TDESKTOP_API_HASH \"\" CACHE STRING \"Provide 'api_hash' for the Telegram API access.\")\n\nif (TDESKTOP_API_TEST)\n    set(TDESKTOP_API_ID 17349)\n    set(TDESKTOP_API_HASH 344583e45741c457fe1862106095a5eb)\nendif()\n\nif (TDESKTOP_API_ID STREQUAL \"0\" OR TDESKTOP_API_HASH STREQUAL \"\")\n    message(FATAL_ERROR\n    \" \\n\"\n    \" PROVIDE: -D TDESKTOP_API_ID=[API_ID] -D TDESKTOP_API_HASH=[API_HASH]\\n\"\n    \" \\n\"\n    \" > To build your version of Telegram Desktop you're required to provide\\n\"\n    \" > your own 'api_id' and 'api_hash' for the Telegram API access.\\n\"\n    \" >\\n\"\n    \" > How to obtain your 'api_id' and 'api_hash' is described here:\\n\"\n    \" > https://core.telegram.org/api/obtaining_api_id\\n\"\n    \" >\\n\"\n    \" > If you're building the application not for deployment,\\n\"\n    \" > but only for test purposes you can use TEST ONLY credentials,\\n\"\n    \" > which are very limited by the Telegram API server:\\n\"\n    \" >\\n\"\n    \" > api_id: 17349\\n\"\n    \" > api_hash: 344583e45741c457fe1862106095a5eb\\n\"\n    \" >\\n\"\n    \" > Your users will start getting internal server errors on login\\n\"\n    \" > if you deploy an app using those 'api_id' and 'api_hash'.\\n\"\n    \" \")\nendif()\n\nif (DESKTOP_APP_DISABLE_AUTOUPDATE)\n    target_compile_definitions(Telegram PRIVATE TDESKTOP_DISABLE_AUTOUPDATE)\nendif()\n\nif (DESKTOP_APP_DISABLE_CRASH_REPORTS)\n    target_compile_definitions(Telegram PRIVATE TDESKTOP_DISABLE_CRASH_REPORTS)\nendif()\n\nif (DESKTOP_APP_USE_PACKAGED)\n    target_compile_definitions(Telegram PRIVATE TDESKTOP_USE_PACKAGED)\nendif()\n\nif (DESKTOP_APP_SPECIAL_TARGET)\n    target_compile_definitions(Telegram PRIVATE TDESKTOP_ALLOW_CLOSED_ALPHA)\nendif()\n\noption(DESKTOP_APP_DISABLE_SWIFT6 \"Disable local on-device translation (build without Swift 6 on macOS).\" OFF)\nif (DESKTOP_APP_DISABLE_SWIFT6)\n    target_compile_definitions(Telegram PRIVATE TDESKTOP_DISABLE_SWIFT6)\nendif()\n"
  },
  {
    "path": "Telegram/cmake/tests.cmake",
    "content": "# This file is part of Telegram Desktop,\n# the official desktop application for the Telegram messaging service.\n#\n# For license and copyright information please follow this link:\n# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n\nadd_executable(test_text WIN32)\ninit_target(test_text \"(tests)\")\n\ntarget_include_directories(test_text PRIVATE ${src_loc})\n\nnice_target_sources(test_text ${src_loc}\nPRIVATE\n    tests/test_main.cpp\n    tests/test_main.h\n    tests/test_text.cpp\n)\n\nnice_target_sources(test_text ${res_loc}\nPRIVATE\n    qrc/emoji_1.qrc\n    qrc/emoji_2.qrc\n    qrc/emoji_3.qrc\n    qrc/emoji_4.qrc\n    qrc/emoji_5.qrc\n    qrc/emoji_6.qrc\n    qrc/emoji_7.qrc\n    qrc/emoji_8.qrc\n)\n\ntarget_link_libraries(test_text\nPRIVATE\n    desktop-app::lib_base\n    desktop-app::lib_crl\n    desktop-app::lib_ui\n    desktop-app::external_qt\n    desktop-app::external_qt_static_plugins\n)\n\nset_target_properties(test_text PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})\n\nadd_dependencies(Telegram test_text)\n\ntarget_prepare_qrc(test_text)\n"
  },
  {
    "path": "Telegram/configure.bat",
    "content": "@echo OFF\r\n\r\nsetlocal enabledelayedexpansion\r\nset \"FullScriptPath=%~dp0\"\r\n\r\npython %FullScriptPath%configure.py %*\r\nif %errorlevel% neq 0 goto error\r\n\r\nexit /b\r\n\r\n:error\r\necho FAILED\r\nexit /b 1\r\n"
  },
  {
    "path": "Telegram/configure.py",
    "content": "'''\nThis file is part of Telegram Desktop,\nthe official desktop application for the Telegram messaging service.\n\nFor license and copyright information please follow this link:\nhttps://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\n'''\nimport sys, os, re\n\nsys.dont_write_bytecode = True\nscriptPath = os.path.dirname(os.path.realpath(__file__))\nsys.path.append(scriptPath + '/../cmake')\nimport run_cmake\nsys.path.append(scriptPath + '/build')\nimport qt_version\n\nexecutePath = os.getcwd()\ndef finish(code):\n    global executePath\n    os.chdir(executePath)\n    sys.exit(code)\n\ndef error(message):\n    print('[ERROR] ' + message)\n    finish(1)\n\nif sys.platform == 'win32' and 'COMSPEC' not in os.environ:\n    error('COMSPEC environment variable is not set.')\n\nscriptName = os.path.basename(scriptPath)\n\narguments = sys.argv[1:]\n\nofficialTarget = ''\nofficialTargetFile = scriptPath + '/build/target'\nif os.path.isfile(officialTargetFile):\n    with open(officialTargetFile, 'r') as f:\n        for line in f:\n            officialTarget = line.strip()\n\narch = ''\nif officialTarget in ['win', 'uwp']:\n    arch = 'x86'\nelif officialTarget in ['win64', 'uwp64']:\n    arch = 'x64'\nelif officialTarget in ['winarm', 'uwparm']:\n    arch = 'arm'\nif not qt_version.resolve(arch):\n    error('Unsupported platform.')\n\nif 'qt6' in arguments:\n    arguments.remove('qt6')\n\nif officialTarget != '':\n    officialApiIdFile = scriptPath + '/../../DesktopPrivate/custom_api_id.h'\n    if not os.path.isfile(officialApiIdFile):\n        error('DesktopPrivate/custom_api_id.h not found.')\n    with open(officialApiIdFile, 'r') as f:\n        for line in f:\n            apiIdMatch = re.search(r'ApiId\\s+=\\s+(\\d+)', line)\n            apiHashMatch = re.search(r'ApiHash\\s+=\\s+\"([a-fA-F\\d]+)\"', line)\n            if apiIdMatch:\n                arguments.append('-DTDESKTOP_API_ID=' + apiIdMatch.group(1))\n            elif apiHashMatch:\n                arguments.append('-DTDESKTOP_API_HASH=' + apiHashMatch.group(1))\n    if arch != '':\n        arguments.append(arch)\n\nfinish(run_cmake.run(scriptName, arguments))\n"
  },
  {
    "path": "Telegram/configure.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\npushd `dirname $0` > /dev/null\nFullScriptPath=`pwd`\npopd > /dev/null\n\npython3 $FullScriptPath/configure.py \"$@\"\n\nexit\n"
  },
  {
    "path": "Telegram/create.bat",
    "content": "@echo off\r\nsetlocal enabledelayedexpansion\r\nset \"FullScriptPath=%~dp0\"\r\nset \"FullExecPath=%cd%\"\r\n\r\nset \"Command=%1\"\r\nif \"%Command%\" == \"test\" (\r\n  call :write_test %2\r\n  exit /b %errorlevel%\r\n) else if \"%Command%\" == \"header\" (\r\n  call :write_header %2\r\n  exit /b %errorlevel%\r\n) else if \"%Command%\" == \"source\" (\r\n  call :write_source %2\r\n  exit /b %errorlevel%\r\n) else if \"%Command%\" == \"\" (\r\n  echo This is an utility for fast blank module creation.\r\n  echo Please provide module path.\r\n  exit /b\r\n)\r\n\r\ncall :write_module %Command%\r\nexit /b %errorlevel%\r\n\r\n:write_module\r\n(\r\n  set \"CommandPath=%1\"\r\n  set \"CommandPathUnix=!CommandPath:\\=/!\"\r\n  if \"!CommandPathUnix!\" == \"\" (\r\n    echo Provide module path.\r\n    exit /b 1\r\n  )\r\n  echo Generating module !CommandPathUnix!..\r\n  call create.bat header !CommandPathUnix!\r\n  call create.bat source !CommandPathUnix!\r\n  exit /b\r\n)\r\n\r\n:write_header\r\n(\r\n  set \"CommandPath=%1\"\r\n  set \"CommandPathUnix=!CommandPath:\\=/!\"\r\n  set \"CommandPathWin=!CommandPath:/=\\!\"\r\n\r\n  if \"!CommandPathUnix!\" == \"\" (\r\n    echo Provide header path.\r\n    exit /b 1\r\n  ) else if exist \"SourceFiles\\!CommandPathWin!.h\" (\r\n    echo This header already exists.\r\n    exit /b 1\r\n  )\r\n  echo Generating header !CommandPathUnix!.h..\r\n  mkdir \"SourceFiles\\!CommandPathWin!.h\"\r\n  rmdir \"SourceFiles\\!CommandPathWin!.h\"\r\n\r\n  call :write_comment !CommandPathWin!.h\r\n  set \"header1=#pragma once\"\r\n  (\r\n    echo !header1!\r\n    echo.\r\n  )>> \"SourceFiles\\!CommandPathWin!.h\"\r\n  exit /b\r\n)\r\n\r\n:write_source\r\n(\r\n  set \"CommandPath=%1\"\r\n  set \"CommandPathUnix=!CommandPath:\\=/!\"\r\n  set \"CommandPathWin=!CommandPath:/=\\!\"\r\n  if \"!CommandPathUnix:~-4!\" == \"_mac\" (\r\n    set \"CommandExt=mm\"\r\n  ) else (\r\n    set \"CommandExt=cpp\"\r\n  )\r\n  if \"!CommandPathUnix!\" == \"\" (\r\n    echo Provide source path.\r\n    exit /b 1\r\n  ) else if exist \"SourceFiles\\!CommandPathWin!.!CommandExt!\" (\r\n    echo This source already exists.\r\n    exit /b 1\r\n  )\r\n  echo Generating source !CommandPathUnix!.!CommandExt!..\r\n  mkdir \"SourceFiles\\!CommandPathWin!.!CommandExt!\"\r\n  rmdir \"SourceFiles\\!CommandPathWin!.!CommandExt!\"\r\n\r\n  call :write_comment !CommandPathWin!.!CommandExt!\r\n  set \"quote=\"\"\"\r\n  set \"quote=!quote:~0,1!\"\r\n  set \"source1=#include !quote!!CommandPathUnix!.h!quote!\"\r\n  (\r\n    echo !source1!\r\n    echo.\r\n  )>> \"SourceFiles\\!CommandPathWin!.!CommandExt!\"\r\n  exit /b\r\n)\r\n\r\n:write_test\r\n(\r\n  set \"CommandPath=%1\"\r\n  set \"CommandPathUnix=!CommandPath:\\=/!\"\r\n  set \"CommandPathWin=!CommandPath:/=\\!\"\r\n\r\n  if \"!CommandPathUnix!\" == \"\" (\r\n    echo Provide source path.\r\n    exit /b 1\r\n  ) else if exist \"SourceFiles\\!CommandPathWin!.cpp\" (\r\n    echo This source already exists.\r\n    exit /b 1\r\n  )\r\n  echo Generating test !CommandPathUnix!.cpp..\r\n  mkdir \"SourceFiles\\!CommandPathWin!.cpp\"\r\n  rmdir \"SourceFiles\\!CommandPathWin!.cpp\"\r\n\r\n  call :write_comment !CommandPathWin!.cpp\r\n  set \"quote=\"\"\"\r\n  set \"quote=!quote:~0,1!\"\r\n  set \"source1=#include !quote!catch.hpp!quote!\"\r\n  (\r\n    echo !source1!\r\n    echo.\r\n  )>> \"SourceFiles\\!CommandPathWin!.cpp\"\r\n  exit /b\r\n)\r\n\r\n:write_comment\r\n(\r\n  set \"Path=%1\"\r\n  (\r\n    echo /*\r\n    echo This file is part of Telegram Desktop,\r\n    echo the official desktop application for the Telegram messaging service.\r\n    echo.\r\n    echo For license and copyright information please follow this link:\r\n    echo https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL\r\n    echo */\r\n  )> \"SourceFiles\\!Path!\"\r\n  exit /b\r\n)\r\n"
  },
  {
    "path": "Telegram/gyp/telegram/sources.txt",
    "content": ""
  },
  {
    "path": "Telegram/shaders/argb32.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D s_texture;\n\nvoid main() {\n\tfragColor = texture(s_texture, v_texcoord);\n}\n"
  },
  {
    "path": "Telegram/shaders/argb32.vert",
    "content": "#version 450\n\nlayout(location = 0) in vec2 position;\nlayout(location = 1) in vec2 v_texcoordIn;\n\nlayout(location = 0) out vec2 v_texcoord;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n};\n\nvoid main() {\n\tv_texcoord = v_texcoordIn;\n\tgl_Position = vec4(\n\t\tvec2(-1.0, -1.0) + 2.0 * position / viewport,\n\t\t0.0,\n\t\t1.0);\n}\n"
  },
  {
    "path": "Telegram/shaders/blur_h.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D b_texture;\n\nlayout(std140, binding = 0) uniform BlurParams {\n\tfloat texelOffset;\n};\n\nconst vec3 satLuminanceWeighting = vec3(0.2126, 0.7152, 0.0722);\nconst int radius = 15;\nconst int diameter = 2 * radius + 1;\n\nvoid main() {\n\tvec4 sum = vec4(0.0);\n\tfor (int i = 0; i < diameter; i++) {\n\t\tfloat offset = float(i - radius) * texelOffset;\n\t\tvec4 sample_ = texture(b_texture, v_texcoord + vec2(offset, 0.0));\n\t\tfloat luminance = dot(sample_.rgb, satLuminanceWeighting);\n\t\tsample_.rgb = mix(vec3(luminance), sample_.rgb, 0.65);\n\t\tsum += sample_;\n\t}\n\tfragColor = sum / float(diameter);\n}\n"
  },
  {
    "path": "Telegram/shaders/blur_v.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D b_texture;\n\nlayout(std140, binding = 0) uniform BlurParams {\n\tfloat texelOffset;\n};\n\nconst int radius = 15;\nconst int diameter = 2 * radius + 1;\n\nvoid main() {\n\tvec4 sum = vec4(0.0);\n\tfor (int i = 0; i < diameter; i++) {\n\t\tfloat offset = float(i - radius) * texelOffset;\n\t\tsum += texture(b_texture, v_texcoord + vec2(0.0, offset));\n\t}\n\tfragColor = sum / float(diameter);\n}\n"
  },
  {
    "path": "Telegram/shaders/controls.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D s_texture;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tfloat g_opacity;\n};\n\nvoid main() {\n\tvec4 result = texture(s_texture, v_texcoord);\n\tfragColor = result * g_opacity;\n}\n"
  },
  {
    "path": "Telegram/shaders/demo.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tfloat time;\n\tfloat _pad;\n};\n\nvoid main() {\n\tvec2 uv = v_texcoord;\n\tvec2 p = uv * 4.0 - 2.0;\n\n\tfloat a = time * 0.8;\n\tfloat c = cos(a);\n\tfloat s = sin(a);\n\tp = mat2(c, -s, s, c) * p;\n\n\tfloat d = length(p);\n\tfloat wave1 = sin(p.x * 3.0 + time * 2.0) * 0.5 + 0.5;\n\tfloat wave2 = sin(p.y * 2.5 - time * 1.5) * 0.5 + 0.5;\n\tfloat wave3 = sin(d * 5.0 - time * 3.0) * 0.5 + 0.5;\n\tfloat ring = smoothstep(0.3, 0.0, abs(d - 1.0 + 0.3 * sin(time)));\n\n\tfloat r = wave1 * 0.4 + wave3 * 0.3 + ring * 0.6;\n\tfloat g = wave2 * 0.3 + wave3 * 0.4 + ring * 0.2;\n\tfloat b = wave1 * 0.2 + wave2 * 0.5 + ring * 0.8;\n\n\tvec3 col = vec3(r, g, b);\n\tcol = pow(col, vec3(0.85));\n\n\tfragColor = vec4(col, 1.0);\n}\n"
  },
  {
    "path": "Telegram/shaders/demo.vert",
    "content": "#version 450\n\nlayout(location = 0) in vec2 position;\nlayout(location = 1) in vec2 v_texcoordIn;\n\nlayout(location = 0) out vec2 v_texcoord;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tfloat time;\n\tfloat _pad;\n};\n\nvoid main() {\n\tv_texcoord = v_texcoordIn;\n\tgl_Position = vec4(\n\t\tvec2(-1.0, -1.0) + 2.0 * position / viewport,\n\t\t0.0,\n\t\t1.0);\n}\n"
  },
  {
    "path": "Telegram/shaders/fill.frag",
    "content": "#version 450\n\nlayout(location = 0) out vec4 fragColor;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tvec4 s_color;\n};\n\nvoid main() {\n\tfragColor = s_color;\n}\n"
  },
  {
    "path": "Telegram/shaders/fill.vert",
    "content": "#version 450\n\nlayout(location = 0) in vec2 position;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n};\n\nvoid main() {\n\tgl_Position = vec4(\n\t\tvec2(-1.0, -1.0) + 2.0 * position / viewport,\n\t\t0.0,\n\t\t1.0);\n}\n"
  },
  {
    "path": "Telegram/shaders/group_frame.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 1) in vec2 b_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D s_texture;\nlayout(binding = 2) uniform sampler2D b_texture;\nlayout(binding = 3) uniform sampler2D n_texture;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tvec4 frameBg;\n\tvec4 shadow;\n\tfloat paused;\n\tvec4 roundRect;\n\tvec2 radiusOutline;\n\tvec4 roundBg;\n\tvec4 outlineFg;\n};\n\nvec2 roundedCorner(vec2 fc) {\n\tvec2 rectHalf = roundRect.zw / 2.0;\n\tvec2 rectCenter = roundRect.xy + rectHalf;\n\tvec2 fromRectCenter = abs(fc - rectCenter);\n\tvec2 vectorRadius = radiusOutline.xx + vec2(0.5);\n\tvec2 fromCenterWithRadius = fromRectCenter + vectorRadius;\n\tvec2 fromRoundingCenter = max(fromCenterWithRadius, rectHalf) - rectHalf;\n\tfloat rounded = length(fromRoundingCenter) - radiusOutline.x;\n\tfloat outline = rounded + radiusOutline.y;\n\treturn vec2(\n\t\t1.0 - smoothstep(0.0, 1.0, rounded),\n\t\t1.0 - (smoothstep(0.0, 1.0, outline) * outlineFg.a));\n}\n\nfloat insideTexture() {\n\tvec2 textureHalf = vec2(0.5, 0.5);\n\tvec2 fromTextureCenter = abs(v_texcoord - textureHalf);\n\tvec2 fromTextureEdge = max(fromTextureCenter, textureHalf) - textureHalf;\n\tfloat outsideCheck = dot(fromTextureEdge, fromTextureEdge);\n\treturn step(outsideCheck, 0.0);\n}\n\nvec4 background() {\n\tvec4 blur = texture(b_texture, b_texcoord);\n\tfloat blurOpacity = shadow.w;\n\treturn mix(frameBg, blur, blurOpacity);\n}\n\nvoid main() {\n\tvec2 fc = vec2(gl_FragCoord.x, viewport.y - gl_FragCoord.y);\n\n\tfloat inside = insideTexture() * (1.0 - paused);\n\tvec4 result;\n\tfloat backgroundOpacity = shadow.w;\n\tvec4 mainColor = texture(s_texture, v_texcoord);\n\tresult = mainColor * inside\n\t\t+ (1.0 - inside) * (backgroundOpacity * background()\n\t\t\t+ (1.0 - backgroundOpacity) * frameBg);\n\n\tfloat shadowCoord = fc.y - roundRect.y;\n\tfloat shadowValue = max(1.0 - (shadowCoord / shadow.x), 0.0);\n\tfloat shadowShown = max(shadowValue * shadow.y, paused) * shadow.z;\n\tresult = vec4(result.rgb * (1.0 - shadowShown), result.a);\n\n\tfloat noiseValue = texture(n_texture, fc / vec2(256.0)).r;\n\tresult.rgb += (noiseValue - 0.5) * 0.002;\n\n\tvec2 roundOutline = roundedCorner(fc);\n\tresult = result * roundOutline.y\n\t\t+ vec4(outlineFg.rgb, 1.0) * (1.0 - roundOutline.y);\n\tresult = result * roundOutline.x + roundBg * (1.0 - roundOutline.x);\n\tfragColor = result;\n}\n"
  },
  {
    "path": "Telegram/shaders/group_frame.vert",
    "content": "#version 450\n\nlayout(location = 0) in vec2 position;\nlayout(location = 1) in vec2 v_texcoordIn;\nlayout(location = 2) in vec2 b_texcoordIn;\n\nlayout(location = 0) out vec2 v_texcoord;\nlayout(location = 1) out vec2 b_texcoord;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tvec4 frameBg;\n\tvec4 shadow;\n\tfloat paused;\n\tvec4 roundRect;\n\tvec2 radiusOutline;\n\tvec4 roundBg;\n\tvec4 outlineFg;\n};\n\nvoid main() {\n\tv_texcoord = v_texcoordIn;\n\tb_texcoord = b_texcoordIn;\n\tgl_Position = vec4(\n\t\tvec2(-1.0, -1.0) + 2.0 * position / viewport,\n\t\t0.0,\n\t\t1.0);\n}\n"
  },
  {
    "path": "Telegram/shaders/incoming_shadow.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D s_texture;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tvec3 shadow;\n};\n\nvoid main() {\n\tfloat fragY = viewport.y - gl_FragCoord.y;\n\tvec4 result = texture(s_texture, v_texcoord);\n\tfloat shadowCoord = shadow.y - fragY;\n\tfloat shadowValue = clamp(shadowCoord / shadow.x, 0.0, 1.0);\n\tfloat shadowShown = shadowValue * shadow.z;\n\tfragColor = vec4(min(result.rgb, vec3(1.0)) * (1.0 - shadowShown), result.a);\n}\n"
  },
  {
    "path": "Telegram/shaders/incoming_yuv420.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D y_texture;\nlayout(binding = 2) uniform sampler2D u_texture;\nlayout(binding = 3) uniform sampler2D v_texture;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tvec3 shadow;\n};\n\nvoid main() {\n\tfloat fragY = viewport.y - gl_FragCoord.y;\n\tfloat y = texture(y_texture, v_texcoord).r - 0.0625;\n\tfloat u = texture(u_texture, v_texcoord).r - 0.5;\n\tfloat v = texture(v_texture, v_texcoord).r - 0.5;\n\tvec4 result = vec4(\n\t\t1.164 * y + 1.596 * v,\n\t\t1.164 * y - 0.392 * u - 0.813 * v,\n\t\t1.164 * y + 2.017 * u,\n\t\t1.0);\n\tfloat shadowCoord = shadow.y - fragY;\n\tfloat shadowValue = clamp(shadowCoord / shadow.x, 0.0, 1.0);\n\tfloat shadowShown = shadowValue * shadow.z;\n\tfragColor = vec4(min(result.rgb, vec3(1.0)) * (1.0 - shadowShown), result.a);\n}\n"
  },
  {
    "path": "Telegram/shaders/noise.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nconst float permTexUnit = 1.0 / 256.0;\nconst float permTexUnitHalf = 0.5 / 256.0;\nconst float grainsize = 1.3;\nconst float noiseCoordRotation = 1.425;\nconst vec2 dimensions = vec2(256.0, 256.0);\n\nlayout(binding = 1) uniform sampler2D p_texture;\n\nvec4 rnm(vec2 tc) {\n\tfloat noise = sin(dot(tc, vec2(12.9898, 78.233))) * 43758.5453;\n\tfloat noiseR = fract(noise) * 2.0 - 1.0;\n\tfloat noiseG = fract(noise * 1.2154) * 2.0 - 1.0;\n\tfloat noiseB = fract(noise * 1.3453) * 2.0 - 1.0;\n\tfloat noiseA = fract(noise * 1.4651) * 2.0 - 1.0;\n\treturn vec4(noiseR, noiseG, noiseB, noiseA);\n}\n\nfloat fade(float t) {\n\treturn t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\n\nfloat pnoise3D(vec3 p) {\n\tvec3 pi = permTexUnit * floor(p) + permTexUnitHalf;\n\tvec3 pf = fract(p);\n\tfloat perm00 = rnm(pi.xy).a;\n\tvec3 grad000 = rnm(vec2(perm00, pi.z)).rgb * 4.0 - 1.0;\n\tfloat n000 = dot(grad000, pf);\n\tvec3 grad001 = rnm(vec2(perm00, pi.z + permTexUnit)).rgb * 4.0 - 1.0;\n\tfloat n001 = dot(grad001, pf - vec3(0.0, 0.0, 1.0));\n\tfloat perm01 = rnm(pi.xy + vec2(0.0, permTexUnit)).a;\n\tvec3 grad010 = rnm(vec2(perm01, pi.z)).rgb * 4.0 - 1.0;\n\tfloat n010 = dot(grad010, pf - vec3(0.0, 1.0, 0.0));\n\tvec3 grad011 = rnm(vec2(perm01, pi.z + permTexUnit)).rgb * 4.0 - 1.0;\n\tfloat n011 = dot(grad011, pf - vec3(0.0, 1.0, 1.0));\n\tfloat perm10 = rnm(pi.xy + vec2(permTexUnit, 0.0)).a;\n\tvec3 grad100 = rnm(vec2(perm10, pi.z)).rgb * 4.0 - 1.0;\n\tfloat n100 = dot(grad100, pf - vec3(1.0, 0.0, 0.0));\n\tvec3 grad101 = rnm(vec2(perm10, pi.z + permTexUnit)).rgb * 4.0 - 1.0;\n\tfloat n101 = dot(grad101, pf - vec3(1.0, 0.0, 1.0));\n\tfloat perm11 = rnm(pi.xy + vec2(permTexUnit, permTexUnit)).a;\n\tvec3 grad110 = rnm(vec2(perm11, pi.z)).rgb * 4.0 - 1.0;\n\tfloat n110 = dot(grad110, pf - vec3(1.0, 1.0, 0.0));\n\tvec3 grad111 = rnm(vec2(perm11, pi.z + permTexUnit)).rgb * 4.0 - 1.0;\n\tfloat n111 = dot(grad111, pf - vec3(1.0, 1.0, 1.0));\n\tvec4 n_x = mix(\n\t\tvec4(n000, n001, n010, n011),\n\t\tvec4(n100, n101, n110, n111),\n\t\tfade(pf.x));\n\tvec2 n_xy = mix(n_x.xy, n_x.zw, fade(pf.y));\n\tfloat n_xyz = mix(n_xy.x, n_xy.y, fade(pf.z));\n\treturn n_xyz;\n}\n\nvec2 rotateTexCoords(vec2 tc, float angle) {\n\ttc -= 0.5;\n\tfloat s = sin(angle);\n\tfloat c = cos(angle);\n\ttc = mat2(c, -s, s, c) * tc;\n\ttc += 0.5;\n\treturn tc;\n}\n\nvoid main() {\n\tvec2 texCoord = v_texcoord * dimensions / grainsize;\n\tvec2 rotated = rotateTexCoords(texCoord, noiseCoordRotation);\n\tfloat noise = pnoise3D(vec3(rotated, 0.0));\n\tfragColor = vec4(noise * 0.5 + 0.5);\n}\n"
  },
  {
    "path": "Telegram/shaders/nv12.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D y_texture;\nlayout(binding = 2) uniform sampler2D uv_texture;\n\nvoid main() {\n\tfloat y = texture(y_texture, v_texcoord).r - 0.0625;\n\tvec2 uv = texture(uv_texture, v_texcoord).rg - vec2(0.5, 0.5);\n\tfloat u = uv.x;\n\tfloat v = uv.y;\n\tfragColor = vec4(\n\t\t1.164 * y + 1.596 * v,\n\t\t1.164 * y - 0.392 * u - 0.813 * v,\n\t\t1.164 * y + 2.017 * u,\n\t\t1.0);\n}\n"
  },
  {
    "path": "Telegram/shaders/nv12_content.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D y_texture;\nlayout(binding = 2) uniform sampler2D uv_texture;\nlayout(binding = 3) uniform sampler2D f_texture;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tvec4 shadowTopRect;\n\tvec4 shadowBottomSkipOpacityFullFade;\n\tvec4 roundRect;\n\tfloat roundRadius;\n};\n\nfloat roundedCorner(vec2 fragCoord) {\n\tvec2 rectHalf = roundRect.zw / 2.0;\n\tvec2 rectCenter = roundRect.xy + rectHalf;\n\tvec2 fromRectCenter = abs(fragCoord - rectCenter);\n\tvec2 vectorRadius = vec2(roundRadius + 0.5);\n\tvec2 fromCenterWithRadius = fromRectCenter + vectorRadius;\n\tvec2 fromRoundingCenter = max(fromCenterWithRadius, rectHalf) - rectHalf;\n\tfloat rounded = length(fromRoundingCenter) - roundRadius;\n\treturn 1.0 - smoothstep(0.0, 1.0, rounded);\n}\n\nvoid main() {\n\tvec2 fragCoord = vec2(gl_FragCoord.x, viewport.y - gl_FragCoord.y);\n\tfloat y = texture(y_texture, v_texcoord).r - 0.0625;\n\tvec2 uv = texture(uv_texture, v_texcoord).rg - vec2(0.5, 0.5);\n\tfloat u = uv.x;\n\tfloat v = uv.y;\n\tvec4 result = vec4(\n\t\t1.164 * y + 1.596 * v,\n\t\t1.164 * y - 0.392 * u - 0.813 * v,\n\t\t1.164 * y + 2.017 * u,\n\t\t1.0);\n\n\tfloat topHeight = shadowTopRect.w;\n\tfloat bottomHeight = shadowBottomSkipOpacityFullFade.x;\n\tfloat bottomSkip = shadowBottomSkipOpacityFullFade.y;\n\tfloat opacity = shadowBottomSkipOpacityFullFade.z;\n\tfloat fullFade = shadowBottomSkipOpacityFullFade.w;\n\tfloat viewportHeight = shadowTopRect.y + topHeight;\n\tfloat fullHeight = topHeight + bottomHeight;\n\tfloat topY = min(\n\t\t(viewportHeight - fragCoord.y) / fullHeight,\n\t\ttopHeight / fullHeight);\n\tfloat topX = (fragCoord.x - shadowTopRect.x) / shadowTopRect.z;\n\tvec4 fadeTop = texture(f_texture, vec2(topX, topY)) * opacity;\n\tfloat bottomY = max(bottomSkip + fullHeight - fragCoord.y, topHeight)\n\t\t/ fullHeight;\n\tvec4 fadeBottom = texture(f_texture, vec2(0.5, bottomY)) * opacity;\n\tfloat fade = min((1.0 - fadeTop.a) * (1.0 - fadeBottom.a), fullFade);\n\tresult.rgb = result.rgb * fade;\n\n\tresult *= roundedCorner(fragCoord);\n\tfragColor = result;\n}\n"
  },
  {
    "path": "Telegram/shaders/passthrough.vert",
    "content": "#version 450\n\nlayout(location = 0) in vec2 position;\nlayout(location = 1) in vec2 v_texcoordIn;\n\nlayout(location = 0) out vec2 v_texcoord;\n\nvoid main() {\n\tv_texcoord = v_texcoordIn;\n\tgl_Position = vec4(position, 0.0, 1.0);\n}\n"
  },
  {
    "path": "Telegram/shaders/pip_argb32.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D s_texture;\nlayout(binding = 2) uniform sampler2D h_texture;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tvec4 roundRect;\n\tfloat roundRadius;\n\tvec4 fadeColor;\n\tvec2 h_size;\n\tvec4 h_extend;\n\tvec4 h_components;\n};\n\nfloat roundedCorner(vec2 fc) {\n\tvec2 rectHalf = roundRect.zw / 2.0;\n\tvec2 rectCenter = roundRect.xy + rectHalf;\n\tvec2 fromRectCenter = abs(fc - rectCenter);\n\tvec2 vectorRadius = vec2(roundRadius + 0.5);\n\tvec2 fromCenterWithRadius = fromRectCenter + vectorRadius;\n\tvec2 fromRoundingCenter = max(fromCenterWithRadius, rectHalf) - rectHalf;\n\tfloat rounded = length(fromRoundingCenter) - roundRadius;\n\treturn 1.0 - smoothstep(0.0, 1.0, rounded);\n}\n\nfloat shadow(vec2 fc) {\n\tvec2 texcoord = fc - roundRect.xy + h_extend.xy;\n\tvec2 total = roundRect.zw + h_extend.xy + h_extend.zw;\n\tvec2 dividedTexcoord = texcoord / total;\n\tfloat left = h_components.x / h_size.x;\n\tfloat right = 1.0 - h_components.y / h_size.x;\n\tfloat top = h_components.z / h_size.y;\n\tfloat bottom = 1.0 - h_components.w / h_size.y;\n\tfloat sampleX = dividedTexcoord.x < left\n\t\t? dividedTexcoord.x * total.x / h_size.x\n\t\t: dividedTexcoord.x > right\n\t\t\t? 1.0 - (1.0 - dividedTexcoord.x) * total.x / h_size.x\n\t\t\t: mix(left, right, (dividedTexcoord.x - left) / (right - left) * (h_size.x - h_components.x - h_components.y) / h_size.x + h_components.x / h_size.x);\n\tfloat sampleY = dividedTexcoord.y < top\n\t\t? dividedTexcoord.y * total.y / h_size.y\n\t\t: dividedTexcoord.y > bottom\n\t\t\t? 1.0 - (1.0 - dividedTexcoord.y) * total.y / h_size.y\n\t\t\t: mix(top, bottom, (dividedTexcoord.y - top) / (bottom - top) * (h_size.y - h_components.z - h_components.w) / h_size.y + h_components.z / h_size.y);\n\treturn texture(h_texture, vec2(\n\t\tclamp(sampleX, 0.0, 1.0),\n\t\tclamp(sampleY, 0.0, 1.0))).a;\n}\n\nvoid main() {\n\tvec2 fc = vec2(gl_FragCoord.x, viewport.y - gl_FragCoord.y);\n\tvec4 result = texture(s_texture, v_texcoord);\n\tresult = result * (1.0 - fadeColor.a) + fadeColor;\n\tfloat corner = roundedCorner(fc);\n\tfloat shadowValue = shadow(fc);\n\tfragColor = result * corner + vec4(0.0, 0.0, 0.0, shadowValue) * (1.0 - corner);\n}\n"
  },
  {
    "path": "Telegram/shaders/pip_controls.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 1) in vec2 o_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D s_texture;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tfloat g_opacity;\n\tfloat o_opacity;\n};\n\nvoid main() {\n\tvec4 result = texture(s_texture, v_texcoord);\n\tvec4 over = texture(s_texture, o_texcoord);\n\tresult = result * (1.0 - o_opacity) + over * o_opacity;\n\tfragColor = result * g_opacity;\n}\n"
  },
  {
    "path": "Telegram/shaders/pip_controls.vert",
    "content": "#version 450\n\nlayout(location = 0) in vec2 position;\nlayout(location = 1) in vec2 v_texcoordIn;\nlayout(location = 2) in vec2 o_texcoordIn;\n\nlayout(location = 0) out vec2 v_texcoord;\nlayout(location = 1) out vec2 o_texcoord;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tfloat g_opacity;\n\tfloat o_opacity;\n};\n\nvoid main() {\n\tv_texcoord = v_texcoordIn;\n\to_texcoord = o_texcoordIn;\n\tgl_Position = vec4(\n\t\tvec2(-1.0, -1.0) + 2.0 * position / viewport,\n\t\t0.0,\n\t\t1.0);\n}\n"
  },
  {
    "path": "Telegram/shaders/pip_nv12.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D y_texture;\nlayout(binding = 2) uniform sampler2D uv_texture;\nlayout(binding = 3) uniform sampler2D h_texture;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tvec4 roundRect;\n\tfloat roundRadius;\n\tvec4 fadeColor;\n\tvec2 h_size;\n\tvec4 h_extend;\n\tvec4 h_components;\n};\n\nfloat roundedCorner(vec2 fc) {\n\tvec2 rectHalf = roundRect.zw / 2.0;\n\tvec2 rectCenter = roundRect.xy + rectHalf;\n\tvec2 fromRectCenter = abs(fc - rectCenter);\n\tvec2 vectorRadius = vec2(roundRadius + 0.5);\n\tvec2 fromCenterWithRadius = fromRectCenter + vectorRadius;\n\tvec2 fromRoundingCenter = max(fromCenterWithRadius, rectHalf) - rectHalf;\n\tfloat rounded = length(fromRoundingCenter) - roundRadius;\n\treturn 1.0 - smoothstep(0.0, 1.0, rounded);\n}\n\nfloat shadow(vec2 fc) {\n\tvec2 texcoord = fc - roundRect.xy + h_extend.xy;\n\tvec2 total = roundRect.zw + h_extend.xy + h_extend.zw;\n\tvec2 dividedTexcoord = texcoord / total;\n\tfloat left = h_components.x / h_size.x;\n\tfloat right = 1.0 - h_components.y / h_size.x;\n\tfloat top = h_components.z / h_size.y;\n\tfloat bottom = 1.0 - h_components.w / h_size.y;\n\tfloat sampleX = dividedTexcoord.x < left\n\t\t? dividedTexcoord.x * total.x / h_size.x\n\t\t: dividedTexcoord.x > right\n\t\t\t? 1.0 - (1.0 - dividedTexcoord.x) * total.x / h_size.x\n\t\t\t: mix(left, right, (dividedTexcoord.x - left) / (right - left) * (h_size.x - h_components.x - h_components.y) / h_size.x + h_components.x / h_size.x);\n\tfloat sampleY = dividedTexcoord.y < top\n\t\t? dividedTexcoord.y * total.y / h_size.y\n\t\t: dividedTexcoord.y > bottom\n\t\t\t? 1.0 - (1.0 - dividedTexcoord.y) * total.y / h_size.y\n\t\t\t: mix(top, bottom, (dividedTexcoord.y - top) / (bottom - top) * (h_size.y - h_components.z - h_components.w) / h_size.y + h_components.z / h_size.y);\n\treturn texture(h_texture, vec2(\n\t\tclamp(sampleX, 0.0, 1.0),\n\t\tclamp(sampleY, 0.0, 1.0))).a;\n}\n\nvoid main() {\n\tvec2 fc = vec2(gl_FragCoord.x, viewport.y - gl_FragCoord.y);\n\tfloat y = texture(y_texture, v_texcoord).r - 0.0625;\n\tvec2 uv = texture(uv_texture, v_texcoord).rg - vec2(0.5, 0.5);\n\tfloat u = uv.x;\n\tfloat v = uv.y;\n\tvec4 result = vec4(\n\t\t1.164 * y + 1.596 * v,\n\t\t1.164 * y - 0.392 * u - 0.813 * v,\n\t\t1.164 * y + 2.017 * u,\n\t\t1.0);\n\tresult = result * (1.0 - fadeColor.a) + fadeColor;\n\tfloat corner = roundedCorner(fc);\n\tfloat shadowValue = shadow(fc);\n\tfragColor = result * corner + vec4(0.0, 0.0, 0.0, shadowValue) * (1.0 - corner);\n}\n"
  },
  {
    "path": "Telegram/shaders/pip_yuv420.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D y_texture;\nlayout(binding = 2) uniform sampler2D u_texture;\nlayout(binding = 3) uniform sampler2D v_texture;\nlayout(binding = 4) uniform sampler2D h_texture;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tvec4 roundRect;\n\tfloat roundRadius;\n\tvec4 fadeColor;\n\tvec2 h_size;\n\tvec4 h_extend;\n\tvec4 h_components;\n};\n\nfloat roundedCorner(vec2 fc) {\n\tvec2 rectHalf = roundRect.zw / 2.0;\n\tvec2 rectCenter = roundRect.xy + rectHalf;\n\tvec2 fromRectCenter = abs(fc - rectCenter);\n\tvec2 vectorRadius = vec2(roundRadius + 0.5);\n\tvec2 fromCenterWithRadius = fromRectCenter + vectorRadius;\n\tvec2 fromRoundingCenter = max(fromCenterWithRadius, rectHalf) - rectHalf;\n\tfloat rounded = length(fromRoundingCenter) - roundRadius;\n\treturn 1.0 - smoothstep(0.0, 1.0, rounded);\n}\n\nfloat shadow(vec2 fc) {\n\tvec2 texcoord = fc - roundRect.xy + h_extend.xy;\n\tvec2 total = roundRect.zw + h_extend.xy + h_extend.zw;\n\tvec2 dividedTexcoord = texcoord / total;\n\tfloat left = h_components.x / h_size.x;\n\tfloat right = 1.0 - h_components.y / h_size.x;\n\tfloat top = h_components.z / h_size.y;\n\tfloat bottom = 1.0 - h_components.w / h_size.y;\n\tfloat sampleX = dividedTexcoord.x < left\n\t\t? dividedTexcoord.x * total.x / h_size.x\n\t\t: dividedTexcoord.x > right\n\t\t\t? 1.0 - (1.0 - dividedTexcoord.x) * total.x / h_size.x\n\t\t\t: mix(left, right, (dividedTexcoord.x - left) / (right - left) * (h_size.x - h_components.x - h_components.y) / h_size.x + h_components.x / h_size.x);\n\tfloat sampleY = dividedTexcoord.y < top\n\t\t? dividedTexcoord.y * total.y / h_size.y\n\t\t: dividedTexcoord.y > bottom\n\t\t\t? 1.0 - (1.0 - dividedTexcoord.y) * total.y / h_size.y\n\t\t\t: mix(top, bottom, (dividedTexcoord.y - top) / (bottom - top) * (h_size.y - h_components.z - h_components.w) / h_size.y + h_components.z / h_size.y);\n\treturn texture(h_texture, vec2(\n\t\tclamp(sampleX, 0.0, 1.0),\n\t\tclamp(sampleY, 0.0, 1.0))).a;\n}\n\nvoid main() {\n\tvec2 fc = vec2(gl_FragCoord.x, viewport.y - gl_FragCoord.y);\n\tfloat y = texture(y_texture, v_texcoord).r - 0.0625;\n\tfloat u = texture(u_texture, v_texcoord).r - 0.5;\n\tfloat v = texture(v_texture, v_texcoord).r - 0.5;\n\tvec4 result = vec4(\n\t\t1.164 * y + 1.596 * v,\n\t\t1.164 * y - 0.392 * u - 0.813 * v,\n\t\t1.164 * y + 2.017 * u,\n\t\t1.0);\n\tresult = result * (1.0 - fadeColor.a) + fadeColor;\n\tfloat corner = roundedCorner(fc);\n\tfloat shadowValue = shadow(fc);\n\tfragColor = result * corner + vec4(0.0, 0.0, 0.0, shadowValue) * (1.0 - corner);\n}\n"
  },
  {
    "path": "Telegram/shaders/rounded_corners.frag",
    "content": "#version 450\n\nlayout(location = 0) out vec4 fragColor;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tvec4 roundRect;\n\tfloat roundRadius;\n};\n\nfloat roundedCorner() {\n\tvec2 fragCoord = vec2(gl_FragCoord.x, viewport.y - gl_FragCoord.y);\n\tvec2 rectHalf = roundRect.zw / 2.0;\n\tvec2 rectCenter = roundRect.xy + rectHalf;\n\tvec2 fromRectCenter = abs(fragCoord - rectCenter);\n\tvec2 vectorRadius = vec2(roundRadius + 0.5);\n\tvec2 fromCenterWithRadius = fromRectCenter + vectorRadius;\n\tvec2 fromRoundingCenter = max(fromCenterWithRadius, rectHalf) - rectHalf;\n\tfloat rounded = length(fromRoundingCenter) - roundRadius;\n\treturn 1.0 - smoothstep(0.0, 1.0, rounded);\n}\n\nvoid main() {\n\tfragColor = vec4(1.0) * roundedCorner();\n}\n"
  },
  {
    "path": "Telegram/shaders/static_content.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D s_texture;\nlayout(binding = 2) uniform sampler2D f_texture;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tvec4 shadowTopRect;\n\tvec4 shadowBottomSkipOpacityFullFade;\n\tvec4 roundRect;\n\tfloat roundRadius;\n};\n\nfloat roundedCorner(vec2 fragCoord) {\n\tvec2 rectHalf = roundRect.zw / 2.0;\n\tvec2 rectCenter = roundRect.xy + rectHalf;\n\tvec2 fromRectCenter = abs(fragCoord - rectCenter);\n\tvec2 vectorRadius = vec2(roundRadius + 0.5);\n\tvec2 fromCenterWithRadius = fromRectCenter + vectorRadius;\n\tvec2 fromRoundingCenter = max(fromCenterWithRadius, rectHalf) - rectHalf;\n\tfloat rounded = length(fromRoundingCenter) - roundRadius;\n\treturn 1.0 - smoothstep(0.0, 1.0, rounded);\n}\n\nvoid main() {\n\tvec2 fragCoord = vec2(gl_FragCoord.x, viewport.y - gl_FragCoord.y);\n\tvec4 result = texture(s_texture, v_texcoord);\n\n\tfloat topHeight = shadowTopRect.w;\n\tfloat bottomHeight = shadowBottomSkipOpacityFullFade.x;\n\tfloat bottomSkip = shadowBottomSkipOpacityFullFade.y;\n\tfloat opacity = shadowBottomSkipOpacityFullFade.z;\n\tfloat fullFade = shadowBottomSkipOpacityFullFade.w;\n\tfloat viewportHeight = shadowTopRect.y + topHeight;\n\tfloat fullHeight = topHeight + bottomHeight;\n\tfloat topY = min(\n\t\t(viewportHeight - fragCoord.y) / fullHeight,\n\t\ttopHeight / fullHeight);\n\tfloat topX = (fragCoord.x - shadowTopRect.x) / shadowTopRect.z;\n\tvec4 fadeTop = texture(f_texture, vec2(topX, topY)) * opacity;\n\tfloat bottomY = max(bottomSkip + fullHeight - fragCoord.y, topHeight)\n\t\t/ fullHeight;\n\tvec4 fadeBottom = texture(f_texture, vec2(0.5, bottomY)) * opacity;\n\tfloat fade = min((1.0 - fadeTop.a) * (1.0 - fadeBottom.a), fullFade);\n\tresult.rgb = result.rgb * fade;\n\n\tresult *= roundedCorner(fragCoord);\n\tfragColor = result;\n}\n"
  },
  {
    "path": "Telegram/shaders/thanos.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 1) in float v_alpha;\n\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D tex;\n\nvoid main() {\n\tif (v_alpha <= 0.0) {\n\t\tdiscard;\n\t}\n\tvec4 color = texture(tex, vec2(v_texcoord.x, 1.0 - v_texcoord.y));\n\tfragColor = color * v_alpha;\n}\n"
  },
  {
    "path": "Telegram/shaders/thanos.vert",
    "content": "#version 450\n\nlayout(location = 0) in vec2 inQuadPos;\n\nlayout(location = 1) in vec2 inOffset;\nlayout(location = 2) in float inLifetime;\n\nlayout(location = 0) out vec2 v_texcoord;\nlayout(location = 1) out float v_alpha;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec4 rect;\n\tvec2 size;\n\tuvec2 particleResolution;\n\tvec4 scale;\n};\n\nvoid main() {\n\tuint particleId = uint(gl_InstanceIndex);\n\tuint pX = particleId % particleResolution.x;\n\tuint pY = particleId / particleResolution.x;\n\n\tvec2 particleSize = size / vec2(particleResolution);\n\n\tvec2 topLeft = vec2(float(pX) * particleSize.x, float(pY) * particleSize.y);\n\tv_texcoord = (topLeft + inQuadPos * particleSize) / size;\n\n\ttopLeft += inOffset;\n\tfloat scaleFactor = scale.x;\n\tvec2 center = topLeft + (particleSize * 0.5);\n\tvec2 position\n\t\t= center + ((inQuadPos - vec2(0.5)) * particleSize * scaleFactor);\n\n\tvec2 ndc;\n\tndc.x = rect.x + position.x / size.x * rect.z;\n\tndc.y = rect.y + position.y / size.y * rect.w;\n\tndc.x = -1.0 + ndc.x * 2.0;\n\tndc.y = -1.0 + ndc.y * 2.0;\n\n\tgl_Position = vec4(ndc, 0.0, 1.0);\n\n\tv_alpha = clamp(inLifetime / 0.6, 0.0, 1.0) * scale.z;\n}\n"
  },
  {
    "path": "Telegram/shaders/thanos_init.comp",
    "content": "#version 450\n\nlayout(local_size_x = 64) in;\n\nstruct Particle {\n\tvec2 offset;\n\tvec2 velocity;\n\tfloat lifetime;\n\tfloat _pad;\n};\n\nlayout(std430, binding = 0) buffer Particles {\n\tParticle particles[];\n};\n\nlayout(std140, binding = 1) uniform Params {\n\tuint particleCountX;\n\tuint particleCountY;\n\tuint seed;\n\tuint _pad;\n};\n\nuint hash(uint x) {\n\tx ^= x >> 16u;\n\tx *= 0x45d9f3bu;\n\tx ^= x >> 16u;\n\tx *= 0x45d9f3bu;\n\tx ^= x >> 16u;\n\treturn x;\n}\n\nfloat hashFloat(uint x) {\n\treturn float(hash(x)) / float(0xFFFFFFFFu);\n}\n\nvoid main() {\n\tuint gid = gl_GlobalInvocationID.x;\n\tuint count = particleCountX * particleCountY;\n\tif (gid >= count) {\n\t\treturn;\n\t}\n\n\tuint s = gid * 3u + seed;\n\n\tfloat direction = hashFloat(s) * (3.14159265 * 2.0);\n\tfloat speed = (0.1 + hashFloat(s + 1u) * 0.1) * 320.0;\n\n\tParticle p;\n\tp.offset = vec2(0.0, 0.0);\n\tp.velocity = vec2(cos(direction) * speed, sin(direction) * speed);\n\tp.lifetime = 1.5 + hashFloat(s + 2u) * 1.5;\n\tp._pad = 0.0;\n\n\tparticles[gid] = p;\n}\n"
  },
  {
    "path": "Telegram/shaders/thanos_update.comp",
    "content": "#version 450\n\nlayout(local_size_x = 64) in;\n\nstruct Particle {\n\tvec2 offset;\n\tvec2 velocity;\n\tfloat lifetime;\n\tfloat _pad;\n};\n\nlayout(std430, binding = 0) buffer Particles {\n\tParticle particles[];\n};\n\nlayout(std140, binding = 1) uniform Params {\n\tuint particleCountX;\n\tuint particleCountY;\n\tfloat phase;\n\tfloat timeStep;\n};\n\nfloat easeInWindow(float fraction, float t) {\n\tfloat windowSize = 0.8;\n\tfloat windowStart = -windowSize;\n\tfloat windowEnd = 1.0;\n\tfloat windowPos = (1.0 - fraction) * windowStart + fraction * windowEnd;\n\tfloat windowT = clamp(t - windowPos, 0.0, windowSize) / windowSize;\n\treturn 1.0 - windowT;\n}\n\nvoid main() {\n\tuint gid = gl_GlobalInvocationID.x;\n\tuint count = particleCountX * particleCountY;\n\tif (gid >= count) {\n\t\treturn;\n\t}\n\n\tfloat easeInDuration = 0.8;\n\tfloat effectFraction = clamp(phase, 0.0, easeInDuration) / easeInDuration;\n\n\tuint particleX = gid % particleCountX;\n\tfloat particleXFraction = float(particleX) / float(particleCountX);\n\tfloat particleFraction = easeInWindow(effectFraction, particleXFraction);\n\n\tParticle p = particles[gid];\n\n\tp.offset += p.velocity * timeStep * particleFraction;\n\tp.velocity.y += 80.0 * timeStep * particleFraction;\n\tp.lifetime = max(0.0, p.lifetime - 0.6 * timeStep * particleFraction);\n\n\tparticles[gid] = p;\n}\n"
  },
  {
    "path": "Telegram/shaders/transparent_content.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D s_texture;\nlayout(binding = 2) uniform sampler2D f_texture;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tvec4 shadowTopRect;\n\tvec4 shadowBottomSkipOpacityFullFade;\n\tvec4 transparentBg;\n\tvec4 transparentFg;\n\tfloat transparentSize;\n};\n\nvoid main() {\n\tvec2 fragCoord = vec2(gl_FragCoord.x, viewport.y - gl_FragCoord.y);\n\tvec4 result = texture(s_texture, v_texcoord);\n\n\tvec2 checkboardLadder = floor(fragCoord / transparentSize);\n\tfloat checkboard = mod(checkboardLadder.x + checkboardLadder.y, 2.0);\n\tvec4 bg = mix(transparentBg, transparentFg, checkboard);\n\tresult = vec4(result.rgb * result.a + bg.rgb * (1.0 - result.a), 1.0);\n\n\tfloat topHeight = shadowTopRect.w;\n\tfloat bottomHeight = shadowBottomSkipOpacityFullFade.x;\n\tfloat bottomSkip = shadowBottomSkipOpacityFullFade.y;\n\tfloat opacity = shadowBottomSkipOpacityFullFade.z;\n\tfloat fullFade = shadowBottomSkipOpacityFullFade.w;\n\tfloat viewportHeight = shadowTopRect.y + topHeight;\n\tfloat fullHeight = topHeight + bottomHeight;\n\tfloat topY = min(\n\t\t(viewportHeight - fragCoord.y) / fullHeight,\n\t\ttopHeight / fullHeight);\n\tfloat topX = (fragCoord.x - shadowTopRect.x) / shadowTopRect.z;\n\tvec4 fadeTop = texture(f_texture, vec2(topX, topY)) * opacity;\n\tfloat bottomY = max(bottomSkip + fullHeight - fragCoord.y, topHeight)\n\t\t/ fullHeight;\n\tvec4 fadeBottom = texture(f_texture, vec2(0.5, bottomY)) * opacity;\n\tfloat fade = min((1.0 - fadeTop.a) * (1.0 - fadeBottom.a), fullFade);\n\tresult.rgb = result.rgb * fade;\n\n\tfragColor = result;\n}\n"
  },
  {
    "path": "Telegram/shaders/yuv420.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D y_texture;\nlayout(binding = 2) uniform sampler2D u_texture;\nlayout(binding = 3) uniform sampler2D v_texture;\n\nvoid main() {\n\tfloat y = texture(y_texture, v_texcoord).r - 0.0625;\n\tfloat u = texture(u_texture, v_texcoord).r - 0.5;\n\tfloat v = texture(v_texture, v_texcoord).r - 0.5;\n\tfragColor = vec4(\n\t\t1.164 * y + 1.596 * v,\n\t\t1.164 * y - 0.392 * u - 0.813 * v,\n\t\t1.164 * y + 2.017 * u,\n\t\t1.0);\n}\n"
  },
  {
    "path": "Telegram/shaders/yuv420_content.frag",
    "content": "#version 450\n\nlayout(location = 0) in vec2 v_texcoord;\nlayout(location = 0) out vec4 fragColor;\n\nlayout(binding = 1) uniform sampler2D y_texture;\nlayout(binding = 2) uniform sampler2D u_texture;\nlayout(binding = 3) uniform sampler2D v_texture;\nlayout(binding = 4) uniform sampler2D f_texture;\n\nlayout(std140, binding = 0) uniform Params {\n\tvec2 viewport;\n\tvec4 shadowTopRect;\n\tvec4 shadowBottomSkipOpacityFullFade;\n\tvec4 roundRect;\n\tfloat roundRadius;\n};\n\nfloat roundedCorner(vec2 fragCoord) {\n\tvec2 rectHalf = roundRect.zw / 2.0;\n\tvec2 rectCenter = roundRect.xy + rectHalf;\n\tvec2 fromRectCenter = abs(fragCoord - rectCenter);\n\tvec2 vectorRadius = vec2(roundRadius + 0.5);\n\tvec2 fromCenterWithRadius = fromRectCenter + vectorRadius;\n\tvec2 fromRoundingCenter = max(fromCenterWithRadius, rectHalf) - rectHalf;\n\tfloat rounded = length(fromRoundingCenter) - roundRadius;\n\treturn 1.0 - smoothstep(0.0, 1.0, rounded);\n}\n\nvoid main() {\n\tvec2 fragCoord = vec2(gl_FragCoord.x, viewport.y - gl_FragCoord.y);\n\tfloat y = texture(y_texture, v_texcoord).r - 0.0625;\n\tfloat u = texture(u_texture, v_texcoord).r - 0.5;\n\tfloat v = texture(v_texture, v_texcoord).r - 0.5;\n\tvec4 result = vec4(\n\t\t1.164 * y + 1.596 * v,\n\t\t1.164 * y - 0.392 * u - 0.813 * v,\n\t\t1.164 * y + 2.017 * u,\n\t\t1.0);\n\n\tfloat topHeight = shadowTopRect.w;\n\tfloat bottomHeight = shadowBottomSkipOpacityFullFade.x;\n\tfloat bottomSkip = shadowBottomSkipOpacityFullFade.y;\n\tfloat opacity = shadowBottomSkipOpacityFullFade.z;\n\tfloat fullFade = shadowBottomSkipOpacityFullFade.w;\n\tfloat viewportHeight = shadowTopRect.y + topHeight;\n\tfloat fullHeight = topHeight + bottomHeight;\n\tfloat topY = min(\n\t\t(viewportHeight - fragCoord.y) / fullHeight,\n\t\ttopHeight / fullHeight);\n\tfloat topX = (fragCoord.x - shadowTopRect.x) / shadowTopRect.z;\n\tvec4 fadeTop = texture(f_texture, vec2(topX, topY)) * opacity;\n\tfloat bottomY = max(bottomSkip + fullHeight - fragCoord.y, topHeight)\n\t\t/ fullHeight;\n\tvec4 fadeBottom = texture(f_texture, vec2(0.5, bottomY)) * opacity;\n\tfloat fade = min((1.0 - fadeTop.a) * (1.0 - fadeBottom.a), fullFade);\n\tresult.rgb = result.rgb * fade;\n\n\tresult *= roundedCorner(fragCoord);\n\tfragColor = result;\n}\n"
  },
  {
    "path": "_config_github_pages.yml",
    "content": "title: Forkgram Desktop\ndescription: Forkgram is the cutting-edge Telegram fork with small Quality-of-Life enhancements.\nbaseurl: \"\"\nurl: \"https://forkgram.github.io/tdesktop\"\n\ninclude:\n  - \"*.html\"\n  - \"*.js\"\n  - \"*.css\""
  },
  {
    "path": "changelog.txt",
    "content": "6.7.6 (14.04.26)\n\n- Gradient-reveal animation for text appearing in messages.\n- Support draw-to-reply from shared media.\n- Frame-by-frame stepping and hold-to-speedup hotkeys in media view.\n- Fix loading stickers and emoji search results.\n- Custom emoji picker for premium quick reaction.\n- Search in section of pinned messages.\n- Recent search entries in settings search.\n- Hide phone number in main settings section.\n- Suggest sending large texts as files.\n- Allow creating polls with single answer option.\n- Bring menus and fullscreen media viewer to active space on macOS.\n- Fix several poll display and voting menu issues.\n- Fix possible crashes in chats list, url auth and others.\n\n6.7.5 (07.04.26)\n\n- Support for gradual appearing of bot reply messages.\n\n6.7.4 (07.04.26)\n\n- Add experimental option for \"AI Tools\" button.\n- Jump between markers with Alt+Arrows in video player.\n- Cocoon info button in AI Tools box.\n- Show warning for unofficial app builds.\n- Allow text emojify without selecting a style.\n- Accept also base64 (non-url) proxy secrets.\n- Use tg:// deeplinks for proxy share links.\n- Fix newbot deeplink parsing with trailing slash and unicode names.\n- Fix file reference refresh for user, group and channel photos.\n- Fix stale macOS tray icon colors after appearance switches.\n- Fix tooltip crash in top peers selector with small width.\n- Fix preserving markers after video quality switch in media view.\n\n6.7.3 (04.04.26)\n\n- Fix launch on older macOS versions.\n\n6.7.2 (03.04.26)\n\n- System audio capture for desktop sharing on macOS.\n- Split voice and audio playback speeds.\n- Unread media dot for voice messages in dialog list.\n- Add 'x' button to AI Tools promo toast.\n- Fix long forward messages preview in box.\n- Fix custom emoji reaction search by category.\n- Fix double context menu in photo sending confirmation.\n- Fix possible crash in middle-elision.\n- Fix possible crash in GIFs search.\n\n6.7.1 (01.04.26)\n\n- Fix crash in emoji panel animation.\n\n6.7 (01.04.26)\n\n- AI Text Editor for messages.\n- Enhanced Polls with media, locations and suggested options.\n- Bots can create and managed other Bots on your behalf.\n- Drawing tools in image editor (brush, marker, eraser, arrow).\n- Trim recorded voice messages.\n- Per-file captions when sending multiple files.\n- Audio sharing in calls on Linux.\n\n6.6.4 beta (16.03.26)\n\n- Fix launch on Linux.\n\n6.6.3 beta (12.03.26)\n\n- Drawing tools in image editor (brush, marker, eraser, arrow).\n- Draw-to-reply button in media viewer.\n- Trim recorded voice messages.\n- Per-file captions when sending multiple files.\n- Audio sharing in calls on Linux.\n- Ripple effects for reactions, send and reply buttons.\n- Fix saved music shuffled playing.\n\n6.6.2 (03.03.26)\n\n- Fix editing of media files.\n- Fix admin rights in legacy groups for tag editing.\n- Fix opening emoji packs from custom emoji click.\n\n6.6.1 (02.03.26)\n\n- Fix crash in legacy groups members list.\n- Fix display of member tags in legacy groups.\n- Make fast reply button appear instantly.\n- Allow hiding fast reply button.\n\n6.6 (01.03.26)\n\n- Member Tags in groups.\n- Disable Sharing in Private Chats.\n- Timestamps for Polls.\n- Log In With Telegram.\n- Time and Date Formatting.\n\n6.5.1 (09.02.26)\n\n- Drag-n-drop chats to folders.\n- Fix crash in adding Recommended folders.\n- Fix crash in crafting gifts selection layer.\n- Fix disappeared Login Email button in Settings.\n- Fix Tab focusing chats search when autocompleting hashtags or mentions.\n\n6.5 (06.02.26)\n\n- Leaving Groups to a New Admin.\n- Log In With Telegram.\n- Gift Crafting.\n- Colored Buttons for Bots.\n- Search in Settings.\n\n6.4.4 beta (29.01.26)\n\n- Fix crash in topic group opening.\n- Show correct profile colors preview when edit.\n- Strip quote entities in media viewer.\n\n6.4.3 beta (28.01.26)\n\n- Search in Settings.\n\n6.4.2 (08.01.26)\n\n- Fix non-closing popup menu items.\n- Fix crash in my profile section resize.\n- Fix some text glitches because of render optimizations.\n\n6.4.1 (05.01.26)\n\n- Fix context menu double activations.\n- Fix text rendering in animating bubbles.\n\n6.4 (03.01.26)\n\n- AI Summaries for posts in channels.\n\n6.3.10 beta (22.12.25)\n\n- Fix user info showing from large group messages.\n- Fix sent state of outgoing messages in forums.\n- Show who sent your unique collectible gifts.\n- Update Qt on all platforms.\n\n6.3.9 (15.12.25)\n\n- Fix possible crash in audio playback speed menu.\n- Fix possible crash in forum or bot topic deletion.\n\n6.3.8 (15.12.25)\n\n- Fix crash on older Linux.\n\n6.3.7 (12.12.25)\n\n- Fix login with passkey visibility on Windows.\n- Improve gift upgrading experience.\n\n6.3.6 (06.12.25)\n\n- Fix crash in media viewer.\n- Fix crash in experimental settings.\n\n6.3.5 (05.12.25)\n\n- Offer stars or TON for unique gifts.\n- Preview gift auctions before they start.\n- Support passkey login on Windows.\n\n6.3.4 (26.11.25)\n\n- Show active auctions above chats list.\n- Add star sending effects in live stories.\n- Fix back button in group member profile view.\n- Export separate topics message history.\n- Export audio files saved to profile.\n- Many different crash fixes.\n\n6.3.3 (21.11.25)\n\n- Some more improvements for gift auctions.\n\n6.3.2 (20.11.25)\n\n- Improved support for gift auctions.\n- Reorder stories in stories albums in My Profile.\n- Bug fixes in new profile cover design.\n- Add your Saved Music to data export.\n\n6.3.1 (17.11.25)\n\n- Reorder saved music in My Profile.\n- Fix new cover overlapping information in My Profile.\n- Fix opening stories from My Profile.\n- Fix media viewer breaking after viewing Live Stories.\n- Fix several possible crashes.\n\n6.3 (15.11.25)\n\n- Support for Gift auctions.\n- Support for Live stories.\n- New profile pages design.\n\n6.2.6 beta (07.11.25)\n\n- Bug fixes in the new profile page design.\n\n6.2.5 beta (28.10.25)\n\n- New profile pages design.\n- Qt 6.10 on Linux.\n\n6.2.4 (22.10.25)\n\n- Highlight links in contact notes.\n- Show menu with screen share controls in wide group call mode.\n- Fix possible crash in saved music removing.\n- Fix crash in theme editor.\n\n6.2.3 (12.10.25)\n\n- Fix crash when viewing messages with hidden senders.\n- Fix possible crash when editing contacts.\n- Fix layout of character count warning in contact notes.\n- Fix icons when editing contacts.\n- Fix color of sending messages animation in complex themes.\n- Fix crash in chat open on Flatpak build.\n\n6.2.2 (10.10.25)\n\n- Fix layer.\n\n6.2.1 (10.10.25)\n\n- Fix sending gifts.\n- Fix possible crash on Linux.\n\n6.2 (10.10.25)\n\n- Comments and reactions in group calls.\n- Notes for contacts.\n- Suggested birthdays.\n- Threads for AI bots.\n- Reorder gifts collections and stories albums.\n- Reorder gifts inside collections.\n- Apply tags instantly when forwarding to Saved Messages.\n- Choose which screen to show notifications on.\n- Nice text message sending animation.\n\n6.1.4 (06.10.25)\n\n- Allow add to folder from recent / top chats.\n- Fix clear history of group chats.\n- Fix several crashes.\n\n6.1.3 (06.09.25)\n\n- Fix dead keys / alt combinations / input methods issues.\n\n6.1.2 (02.09.25)\n\n- Close selected chat in Ctrl+Tab chat switch by 'Q'.\n- Auto-split pasted multiple lines to checklist task creation.\n- Fix color picker in userpic builder.\n\n6.1.1 (01.09.25)\n\n- Ctrl+Tab / Ctrl+Shift+Tab collect chats on every launch.\n- Fix Ctrl+Tab / Ctrl+Shift+Tab shortcut override.\n- Fix possible crashes in saved music.\n- Fix webview error label alignment.\n\n6.1 (31.08.25)\n\n- Music on Profiles.\n- Gift Themes.\n- Gift Value Information.\n- Upgrading Gifts for Other Users.\n\n6.0.3 beta (22.08.25)\n\n- Ctrl+Tab / Ctrl+Shift+Tab for last opened chats switching.\n- New topic dividers for groups with topics and tabs.\n- Adjust volume of notification sounds.\n- Show stars required for the next rating level.\n- IV support on Linux (in case WebView works).\n\n6.0.2 (01.08.25)\n\n- Improve design of profile stars rating.\n\n6.0.1 (01.08.25)\n\n- Fix crashing on Windows 7.\n\n6.0 (31.07.25)\n\n- Public Post Search.\n- Story Albums.\n- Gift Collections.\n- Profile Rating.\n\n5.16.6 (25.07.25)\n\n- Fix bad layout in box buttons.\n- Fix several problems in gift collections.\n\n5.16.5 (23.07.25)\n\n- Age verification support.\n\n5.16.4 (11.07.25)\n\n- Fix problem with negative unread counters.\n- Fix stars values display in statistics.\n- Fix crash in messages fee disabling.\n\n5.16.3 (08.07.25)\n\n- Allow removing / charging fee in channel direct messages.\n- Don't offer creating checklists in channels.\n- Support author channel in gifts.\n\n5.16.2 (04.07.25)\n\n- Fix crash in some checklists.\n- Fix problems with some private chats message sending.\n- Fix chats list preview for rejected/balance-low suggestions.\n\n5.16.1 (02.07.25)\n\n- Fix inline keyboard updating in bot messages.\n- Fix possible crash in fast chat switching.\n- Different minor fixes.\n\n5.16 (01.07.25)\n\n- Create private and group checklists.\n- Suggest Posts in Channels.\n- Monetizing via Suggested Posts.\n\n5.15.4 (12.06.25)\n\n- Fix updating messages in Saved Messages subchats.\n- Fix possible issues with mouse cursor on Linux.\n\n5.15.3 (09.06.25)\n\n- Fix new contact top bar appearance.\n- Remove change photo button for channel direct messages.\n\n5.15.2 (05.06.25)\n\n- Fix sending messages in new forum layout.\n- Add statistics for user stars.\n\n5.15.1 (05.06.25)\n\n- Fix launch on Windows 7.\n- Fix launch on older Linux distributions.\n- Fix crash in group chat message right click.\n- Fix unread counters in channel direct messages.\n- Don't generate \"User joined\" message in channel direct messages.\n- Fix some other glitches in new forums and channel direct messages.\n\n5.15 (04.06.25)\n\n- Send Direct Messages to Channels.\n- Enable New Tab Layout for Topics.\n- Create Polls with Up To 12 Options.\n\n5.14.3 (18.05.25)\n\n- Fix stale birthday suggestions removing.\n- Fix notification click opening a new window.\n\n5.14.2 (08.05.25)\n\n- Buy Rare Gifts in the Marketplace.\n- Sell Your Gifts for Stars.\n- Set Your Channel to Auto-Translate.\n\n5.14.1 (01.05.25)\n\n- Fix launching on Windows 7.\n\n5.14 (01.05.25)\n\n- Secure Group Calls Made Easy.\n- Automated Business Accounts.\n- New Gifts Settings.\n\n5.13.1 (26.03.25)\n\n- Fix crash in group/channel stories.\n\n5.13 (26.03.25)\n\n- Allow transferring my unique gifts from Send Gift section.\n- Edit type of gifts you accept in Settings > Privacy and Security.\n- Swipe navigation using swipe gesture on touchpads / touchscreens.\n- Choose chat list quick action for swipe or middle click.\n\n5.12.6 beta (20.03.25)\n\n- Allow customizing chats list swipe left action.\n- Fix custom emoji in poll results view.\n- Fix crash in main menu swipe closing.\n\n5.12.5 beta (14.03.25)\n\n- Support swipe navigation on touchscreens.\n- Close main menu by swipe navigation.\n\n5.12.4 beta (12.03.25)\n\n- Touchpad swipe back to go back in navigation history.\n- Touchpad swipe between folders if they're horizontal.\n- Out-of-the-box monochrome tray icon on Linux.\n- Icons telegram{,{,-attention,-mute}-panel} are renamed\nto org.telegram.desktop{,{,-attention,-mute}-symbolic} for\nunified icon lookup in multiple (sometimes sandboxed) builds\nand GTK icon recolorization support.\n- Snap package got icon theming support,\nthe icon names are snap.telegram-desktop.{,{,-attention,-mute}-symbolic}\n(notice the name ends with a dot in case of no suffix!).\n\n5.12.3 (10.03.25)\n\n- Fix a couple more crashes.\n- Fix gift disappearing on unpin.\n- Fix country emoji for Fragment numbers.\n\n5.12.2 (09.03.25)\n\n- Fix some crashes.\n\n5.12.1 (08.03.25)\n\n- Fix a crash in some chat switchings.\n- Fix crashes in empty repaint callbacks.\n\n5.12 (07.03.25)\n\n- Set a fee for incoming messages from unknown users.\n- Set a fee for messages in groups or channel comments.\n- Show some information about who's messaging you.\n- Pin gifts on your profile.\n\n5.11.1 (13.02.25)\n\n- Fix arbitrary cropping support in the image editor.\n- Fix crash on round video message playback.\n- Fix complex custom keyboard shortcuts.\n\n5.11 (12.02.25)\n\n- Copy video links at current time.\n- Set custom video covers when posting to channels.\n- Send paid reactions from the name of your channels.\n\n5.10.8 beta (12.02.25)\n\n- Edit keyboard shortcuts in Settings > Chat Settings.\n- Hide controls in calls with incoming video.\n- Fix several crashes.\n\n5.10.7 (27.01.25)\n\n- Fix some phrases for collectible gifts.\n- Fix freeze in sound reencoding on macOS and Linux.\n\n5.10.6 (26.01.25)\n\n- Fix crash in opening users gifts section.\n- Fix unintended sound in reaction notifications.\n- Fix disabling notifications sound on macOS and Linux.\n\n5.10.5 (24.01.25)\n\n- Count all unread topics as a single chat in archive unread counter.\n- Fix common groups section in bots profiles.\n- Fix crash on scheduling messages from Share box.\n- Fix silent notifications on macOS.\n- Add support for 'sound-file' notification hint on Linux.\n\n5.10.4 (22.01.25)\n\n- Add messages search in channel comments.\n- Allow sending your unique gifts to blockchain.\n- Add option for running in backround on Linux.\n- Fix possible freeze on quit on macOS.\n\n5.10.3 (09.01.25)\n\n- Fix a crash in legacy group opening.\n\n5.10.2 (07.01.25)\n\n- Fix double verification badge in profiles.\n\n5.10.1 (06.01.25)\n\n- Show \"Boost group the send messages\" information.\n- Fix several gifts bugs and glitches.\n- Fix several crashes.\n\n5.10 (02.01.25)\n\n- Collectible Gifts.\n- Reactions for Service Messages.\n- Verification from Third Parties.\n- Custom Emoji in Folder Names.\n\n5.9.2 beta (24.12.24)\n\n- Fix some jump-to-message highlightings.\n- Fix some long round video messages sending.\n- Fix possible crash in global photos / videos.\n- Fix streaming starting for some large video files.\n- Fix possible drop of top peers / recent chats cache.\n- Fix possible crashes in audio on macOS (rollback OpenAL).\n\n5.9.1 beta (18.12.24)\n\n- Add global media overview tabs in chats search.\n- Add main menu \"My Profile\" with my stories and gifts access.\n- Highlight some of search query on result message open.\n- Fix highlighting quotes from bottom parts of long messages.\n- Allow forward and reply bars together.\n- Make gift price categories scrollable.\n- Auto-send deep-link /start in existing bot chats.\n\n5.9 (04.12.24)\n\n- Affiliate programs for bots.\n- Add option to show folder tags in chats list.\n\n5.8.5 beta (29.11.24)\n\n- Fix pinned chats in folders.\n- Fix emoji in folder tags.\n- Fix several crashes.\n\n5.8.4 beta (28.11.24)\n\n- Add option to show folder tags in chats list.\n- Count group with topics as one chat in folder unread counter.\n\n5.8.3 (23.11.24)\n\n- Ctrl+Click on Reply in groups to reply in another chat.\n- Fix freeze on forward messages box opening.\n- Improve phrases in bot subscriptions.\n- Several bugfixes.\n\n5.8.2 (19.11.24)\n\n- Improve bottom label color in mini apps.\n- Fix some fullscreen issues in mini apps.\n- Fix miss-order of archived chats in forward box.\n- Fix file dialog for Windows on ARM.\n- Fix emoji status pack view from profile regression.\n\n5.8.1 (17.11.24)\n\n- Fix several possible crashes.\n\n5.8 (17.11.24)\n\n- Updates in Mini Apps platform.\n- Chat folders in forward and share boxes.\n- Horizontal strip mode for chat folders.\n\n5.7.4 beta (15.11.24)\n\n- Enable auto night mode by default.\n- Use muted chats folder badge in horizontal strip.\n- Fix custom userpic context menu in comments and topics.\n- Fix scrolling of chats folders together with the list.\n- Fix chats folders list disappearing in some cases.\n- Fix topic groups in share box with chats folders.\n- Fix bot commands list in some groups.\n- Fix crash in channel short info box display.\n- Fix crash in bot monetization section opening.\n- Fix crash in bot sponsored messages destruction.\n\n5.7.3 beta (13.11.24)\n\n- Option to show folders above the chats list.\n- Folders on top of recipients list box.\n\n5.7.2 (05.11.24)\n\n- Fix recompressed video playback cutoff.\n- Fix video message recroding on macOS.\n\n5.7.1 (01.11.24)\n\n- Fix occasional redundant newlines before message timestamp.\n- Remove playback speed change on settings button click.\n- Fix video playback settings button on hidpi screens.\n- Fix return from Picture-in-Picture to media viewer.\n- Fix video quality auto-switching in Picture-in-Picture.\n- Add message link to channel stars transaction information.\n- Fix GIF playback in media viewer.\n- Fix several possible crashes.\n\n5.7 (31.10.24)\n\n- Sending video messages.\n- New video quality selection in channels.\n- Adding media to sent text messages via Edit.\n- Ads in bots with revenue sharing with bot developers.\n- Chat-specific hashtags.\n\n5.6.4 beta (24.10.24)\n\n- Add recording of video messages in case a camera is available.\n- Add \"Respect the Focus settings\" for custom notifications on Windows.\n\n5.6.3 (15.10.24)\n\n- Add ability to change page scale in Instant View pages.\n- Fix unnecessary timestamp jump to a new line.\n- Fix secondary button positioning in miniapps.\n- Fix a crash in QR code copy for a chat without a photo.\n- Fix a crash in share options menu showing.\n- Fix a crash on monitor disconnect.\n\n5.6.2 (14.10.24)\n\n- Message selection marks.\n- Better copy username / public link / ID menu in profiles.\n- Keyboard navigation (Up/Down/PageUp/PageDown) in chat preview.\n- Improved QR options for profile links.\n- Fix calls color problems on Windows.\n- Fix scroll problems on Linux.\n\n5.6.1 (07.10.24)\n\n- Fix media viewer sometimes blocking the app after launch.\n\n5.6 (05.10.24)\n\n- Gifts for Telegram Stars.\n- Mention / hashtag autocomplete in media caption field.\n- Resizing the window used for web apps and games.\n- \"Include muted chats in folder counters\" notifications setting.\n- Emoji from Unicode 15.1.\n\n5.5.8 beta (03.10.24)\n\n- Allow drag-to-scroll in reply quote edit box.\n- Add bot verified badge to web app title.\n- Fix crash when opening some channels.\n- Fix some visual glitches.\n\n5.5.7 beta (01.10.24)\n\n- Add ability to share QR code of bots.\n- Add mention/hashtag autocomplete to Saved GIF caption.\n- Fixed display of working hours in profiles.\n- Respect the maximum reply quote length.\n\n5.5.6 beta (19.09.24)\n\n- Allow resizing the window used for web apps and games.\n- Add mention / hashtag autocomplete in media caption field.\n- Add \"Include muted chats in folder counters\" notifications setting.\n- Auto-select URL suggested from clipboard when creating a custom link.\n- Update emoji to Unicode 15.1.\n- Fix replying with a quote from media album caption.\n- Fix viewing custom emoji packs from media album caption.\n\n5.5.5 (13.09.24)\n\n- Suggest URL from clipboard when creating a custom link.\n- Fix animated topic icons in topic info page.\n- Fix excessive getChannelDifference requests.\n\n5.5.4 (12.09.24)\n\n- Fix channel updates stopping after difference request failing.\n- Add QR code generation for your username in Settings.\n- Fix swipe-to-reply gestures stopping. (macOS)\n\n5.5.3 (10.09.24)\n\n- Fix custom emoji sending.\n- Add mention link QR code sharing.\n- Fix sending files from network drives. (Windows)\n\n5.5.2 (09.09.24)\n\n- Support two bottom buttons in web apps.\n- Fix text layout outside of the message bubble glitch.\n- Don't stop video when dragging media viewer in window mode.\n\n5.5.1 (06.09.24)\n\n- Fix crash in peer short info box.\n\n5.5 (06.09.24)\n\n- Star Giveaways.\n- Swipe-to-Reply.\n- Audio device selection in one-on-one calls.\n- Text selection in collapsed / expanded quotes.\n\n5.4.6 beta (04.09.24)\n\n- Add Swipe-To-Reply for modern touchpads and touchscreens.\n- Add text selection in collapsed and expanded blockquotes.\n- Fix text rendering in 100% interface scale on Windows.\n\n5.4.5 beta (31.08.24)\n\n- Fix possible crash in text rendering.\n\n5.4.4 beta (29.08.24)\n\n- Fix wrong layout and crashes in text shaping.\n- Fix crashes in voice / video messages playback.\n\n5.4.3 beta (29.08.24)\n\n- Fix working on Windows 7.\n\n5.4.2 beta (28.08.24)\n\n- Select audio devices from one-on-one call window.\n- Bug fixes and other minor improvements.\n- New text layout testing.\n\n5.4.1 (17.08.24)\n\n- Fix crash when sending Star Reaction in comments.\n- Fix loading \"Send As\" channels in a channel.\n- Place Star Reaction always first in list.\n- Removed paid Invite Links in groups.\n\n5.4 (14.08.24)\n\n- Super Channels with post authors' profiles.\n- Support favorite channels with Star Reactions.\n- Channel Subscriptions with Stars monthly payments.\n\n5.3.2 (02.08.24)\n\n- Fix crash on launch in some Linux systems.\n\n5.3.1 (01.08.24)\n\n- Open normal links from tonsite-s in system browser.\n- Fix unicode tonsite:// links.\n- Fix crash on Linux X11.\n\n5.3 (31.07.24)\n\n- View recent and popular web apps in chats search.\n- Open several web apps in different windows.\n- Gift Telegram Stars to your friends.\n- Send location marks and venues.\n- Open tonsite:// links in webview.\n- Edit order of stickers in your packs.\n\n5.2.6 beta (29.07.24)\n\n- Fix launching on X11. (Linux)\n\n5.2.5 beta (27.07.24)\n\n- Fix media viewer context menu.\n- Fix blockquotes layout in messages.\n\n5.2.4 beta (24.07.24)\n\n- Allow opening several web apps.\n- Send location marks and venues.\n\n5.2.3 (07.07.24)\n\n- Fix crash in bot star stats page.\n- Bug fixes and other minor improvements.\n\n5.2.2 (02.07.24)\n\n- Fix topics search in topic groups.\n- Fix Instant View pages content updating.\n\n5.2.1 (01.07.24)\n\n- Fix crash when opening topic in a new window.\n- Fix crash in topic search scope dropdown.\n- Fix crash in video player.\n- Fix feeze and crash in Instant View (Windows).\n- Allow unlock by Apple Watch or System Password (macOS).\n\n5.2 (30.06.24)\n\n- Pay for content with Telegram Stars.\n- Enable local passcode unlock by Windows Hello and Touch ID.\n- Allow opening forums, topics and archive in a separate window.\n\n5.1.8 beta (15.06.24)\n\n- Support nice blockquotes and code blocks edition when composing messages.\n- Support collapsing blockquotes and specifying syntax highlight language.\n- Support nice spoiler animation in the message composing input field.\n\n5.1.7 (14.06.24)\n\n- Fix recently searched hashtags in chats search.\n- Fix formatting shortcuts on macOS.\n- Fix non-Telegram-Stars-invoice bot buttons with star emoji.\n\n5.1.6 (13.06.24)\n\n- Fix search in archived chats in single-column layout.\n- Improve chat previews for topics, Saved Messages and groups.\n- Fix formatting shortcuts on Linux.\n- Fix options for Telegram Stars buying in case of large amounts.\n\n5.1.5 (07.06.24)\n\n- Return WebView on Windows.\n\n5.1.4 (06.06.24)\n\n- Improve design of search in chat.\n- Show vCard information for shared contacts.\n- Allow scheduling media in topic groups.\n- Several minor bugfixes.\n\n5.1.3 (04.06.24)\n\n- Rebuild version for macOS to fix the phrases.\n\n5.1.2 (03.06.24)\n\n- Several bugs fixed including a couple of crashes.\n\n5.1.1 (01.06.24)\n\n- Fix caption display on some media.\n- Fix collapsed blockquotes rendering.\n- Fully close search in chat by \"Cancel\" click.\n- Allow editing caption placement and spoiler in topics.\n- Disable effects on forwarded messages and inline results.\n\n5.1 (31.05.24)\n\n- Send messages with effects.\n- Move photo or video captions above the media.\n- Chat preview on chat photo long press or Alt+Click.\n\n5.0.6 beta (30.05.24)\n\n- Fix chat preview with non-default themes.\n- Fix chat preview crash when scrolling up.\n- Jump to chat from preview only by Double-Click.\n- Show chat preview with Force Touch on macOS.\n\n5.0.5 beta (28.05.24)\n\n- Long press on chat userpic to show quick preview.\n- Alt+Click on chat to show quick preview.\n- Show author userpics in forwarded messages.\n\n5.0.4 (28.05.24)\n\n- Fix reply to last message by Ctrl+Up in topics.\n- Some other bug fixes.\n\n5.0.3 (28.05.24)\n\n- Ctrl+Click on Reply in menu to Reply in another chat.\n- Allow Zero-Width-Space character in text rendering.\n- Fix creating custom links in the message field.\n- Fix jump to the topic with last unread message.\n- Fix newline entering via Shift+Enter on Linux.\n- Fix forum search open by Ctrl+F.\n\n5.0.2 (24.05.24)\n\n- Toggle chats search focus by Tab key.\n- Unfocus empty search by Backspace key.\n- Add a spoiler to already sent media when editing.\n- Forward by drag-n-drop to frequent contacts / recent searches.\n- Allow wide chats list in empty window with narrow list when a chat is opened.\n- Improve custom font support in the input fields.\n- Fix input method typing from unfocused search.\n- Fix possible media reordering on batch send.\n- Fix crash in group emoji set saving.\n- Fix possible hang on quit on macOS.\n\n5.0.1 (04.05.24)\n\n- Fix several crashes in new interfaces.\n- Fix monospace font size.\n- Fix possible problem with underline font size.\n\n5.0 (02.05.24)\n\n- Choose custom font family in Settings > Chat settings > Font family.\n- Show \"Frequent contacts\" when you focus the search field.\n- Show \"Recent chats\" when you focus the search field.\n- Show \"Channels\" list and similar channels.\n- Premium users can use animated emoji in polls.\n- Group admins can mass-moderate many messages.\n- Fix frequent crashes on some Linux systems.\n\n4.16.10 beta (26.04.24)\n\n- Group admins can mass-moderate many messages.\n- Premium users can use animated emoji in polls.\n- Revert the default \"Open Sans\" font to 1.10.\n- Several crash fixes and small improvements.\n\n4.16.9 beta (23.04.24)\n\n- Show \"Frequent contacts\" when you focus the search field.\n- Show \"Recent chats\" when you focus the search field.\n- Show \"Channels\" and channel recommendations.\n- Allow changing font in Settings > Chat settings > Font family.\n\n4.16.8 (16.04.24)\n\n- Fix in-app playing of some video and audio files.\n- Fix crash on Linux opening chats with custom backgrounds.\n- Fix crash on quit after using scheduled messages.\n\n4.16.7 (15.04.24)\n\n- Reimplement file open confirmations.\n\n4.16.6 (09.04.24)\n\n- Show custom emoji preview on long press.\n- Fix resume chat bot button disappearance.\n- Fix GIF files playback. (regression in 4.16.3.beta)\n\n4.16.5 (08.04.24)\n\n- Fix editing privacy for groups and channels invitations.\n- Possible fix for the network unresponsiveness after sleep.\n- Possible fix for wide range of memory allocator crashes on Linux.\n\n4.16.4 (06.04.24)\n\n- Bug fixes and other minor improvements.\n\n4.16.3 beta (05.04.24)\n\n- Improve media upload speed.\n- Update FFmpeg to 6.1.1.\n\n4.16.2 (04.04.24)\n\n- Use IV by default for Telegraph and some Telegram links.\n- Support IV links in bot web-apps.\n- Some bug fixes.\n\n4.16.1 (02.04.24)\n\n- Show information about Fragment phone numbers and usernames.\n- Fix jump to original story from a story repost.\n- Fix old image display in viewer in some cases.\n- Fix several crashes.\n\n4.16 (01.04.24)\n\n- Instant View on Windows 10+ and macOS.\n- Allow scheduling messages in topics.\n- Telegram Business: Links to Chats.\n- Telegram Business: Custom Intro.\n- Telegram Business: Chatbots.\n- Sharing Revenue with Channel Owners.\n\n4.15.7 beta (01.04.24)\n\n- Test crashfix on Linux.\n\n4.15.6 beta (25.03.24)\n\n- Save the Instant View window geometry.\n- Fix jump to anchors with accents in the Instant View.\n- Fix possible crash in the ElasticScroll destructor.\n\n4.15.5 beta (16.03.24)\n\n- Fix a crash in Instant View article parsing.\n- Support AirPods Mute/Unmute toggle in calls on macOS.\n- Disable system proxy on Linux to check if it fixes crashes.\n\n4.15.4 beta (14.03.24)\n\n- Fix initial text color in dark mode Instant View on macOS.\n- Fix non-English symbols encoding in Instant View on macOS.\n- Fix sharing from Instant View on macOS.\n- Fix crash with long messages on Linux.\n\n4.15.3 beta (13.03.24)\n\n- Instant View on Windows (with WebView2) and macOS.\n- Allow scheduling messages in topics.\n- Fix system proxy support on Linux.\n\n4.15.2 (12.03.24)\n\n- Telegram Business: Greeting Message.\n- Telegram Business: Away Message.\n- Telegram Business: Quick Replies.\n- Telegram Business: Working Hours.\n- Close the ongoing call window without hanging up the call.\n- Fast scroll through chats list with Ctrl or Shift pressed.\n- Several bugfixes.\n\n4.15.1 (08.03.24)\n\n- Telegram Business features.\n\n4.15 (18.02.24)\n\n- Stories from groups.\n- Group appearance settings.\n- Group emoji pack.\n- Boost groups to unlock features.\n\n4.14.16 (16.02.24)\n\n- Boost groups.\n\n4.14.15 (10.02.24)\n\n- Fix webview regression on Linux X11. (2nd attempt)\n\n4.14.14 (09.02.24)\n\n- Fix webview regression on Linux X11.\n\n4.14.13 (02.02.24)\n\n- Fix display of statistics for single posts.\n- Allow editing tag name from search tags.\n- Fix a crash in tags removal.\n\n4.14.12 (01.02.24)\n\n- Tags in Saved Messages.\n- Audio output device selection for music and videos.\n- Audio input device selection for voice messages recording.\n- Default device changes should be applied instantly. (Windows / macOS)\n\n4.14.11 beta (25.01.24)\n\n- Fix crash when accepting calls.\n- Fix possible crash in loopback audio on Windows.\n\n4.14.10 beta (25.01.24)\n\n- Allow choosing audio device for music and video files.\n- Allow choosing microphone device for voice recording.\n- Allow pause and resume voice messages recording.\n- Track audio device changes on Windows and macOS.\n\n4.14.9 (19.01.24)\n\n- Fix two possible crashes in messages history and group search.\n- Fix local online status turning off. (once again)\n\n4.14.8 (18.01.24)\n\n- Fix initial position and size of secondary windows.\n- Optimize messages loading. (load replies on demand)\n- Fix local online status turning off.\n\n4.14.7 (18.01.24)\n\n- See when your message was read in private chat.\n- Premium users can see other last seen times if they weren't hidden explicitly.\n- Premium users can allow new chats only from their contacts and other Premium users.\n\n4.14.6 (15.01.24)\n\n- Fix one-time audio tooltip showing in wrong places.\n- Fix media viewer showing above taskbar on Windows.\n- Fix crash in one-time video message playback.\n\n4.14.5 (15.01.24)\n\n- Allow sending one-time voice messages.\n- Improve playing one-time voice and video messages.\n- Remove Ctrl+Shift+[1-6] shortcuts by default, some are used in input methods.\n- Some bugs and glitches fixed.\n\n4.14.4 (08.01.24)\n\n- Switch between logged in accounts using Ctrl+Shift+[1-6] shortcuts.\n- Add poll creation in groups to the attach menu, if exists.\n- Another fix for payment card validation.\n\n4.14.3 (04.01.24)\n\n- Allow sending single-time voice messages.\n- Fix payments card validation.\n- Fix crash when trying to join channels above the limit.\n- Add \"Quit Telegram\" to the Taskbar context menu. (Windows)\n- Fix opened windows list in the Dock icon context menu. (macOS)\n\n4.14.2 (02.01.24)\n\n- Show original senders name in reply to forward information.\n- Use original senders color / emoji pattern in forwards.\n- Highlight active saved messages chat in list.\n- Fix chats list scrolling on X11 (Linux).\n\n4.14.1 (01.01.24)\n\n- Fix crash in \"Author Hidden\" chat in \"Saved Messages\".\n- Improve jump-to-original button layout in \"Saved Messages\".\n- Show my own chat as \"My Notes\" in \"Saved Messages\".\n- In screen sharing source window select first screen by default.\n\n4.14 (31.12.23)\n\n- Improved saved messages.\n- One-time voice messages.\n\n4.13.1 (23.12.23)\n\n- Fix crash in chat history right click.\n- Fix user emoji status display in main menu, profile and settings.\n\n4.13 (22.12.23)\n\n- Support setting channel wallpaper.\n- Support setting channel emoji status.\n- Allow gifting premium to several recipients at once.\n\n4.12.2 (01.12.23)\n\n- Fix choosing custom reactions in channels.\n- Fix crash on launch using old hardware. (Windows)\n\n4.12.1 (01.12.23)\n\n- Fix assertion violation with imported messages.\n\n4.12 (30.11.23)\n\n- Similar channels.\n- Wallpapers for both sides.\n- Voice-to-Text for rveryone.\n- Story statistics for channels.\n- Custom Reactions for channels.\n- Automatic code highlighting in messages.\n\n4.11.8 (14.11.23)\n\n- Fix phrases on macOS by doing a clean rebuild.\n\n4.11.7 (13.11.23)\n\n- Fix sending media files with quote replies.\n- Fix quoted text highlighting in some cases.\n- Fix loading statistics for some channels.\n- Fix Ctrl+Shift+. shortcut on X11.\n- Fix a crash in statistics.\n\n4.11.6 (09.11.23)\n\n- Support multiple boosts and reassignment.\n- Improve giveaway creation flow.\n- Fix crash in topics creation.\n\n4.11.5 (06.11.23)\n\n- Giveaway phrases and sticker fixes.\n- Show quoted part in channel comments.\n- Show replies with icons and multiline preview.\n- Send correct replies in topics and channel comments.\n- In monochrome Windows tray icon use dot instead of counter.\n\n4.11.4 beta (06.11.23)\n\n- Show quoted part in channel comments.\n- Show replies with icons and multiline preview.\n- Send correct replies in topics and channel comments.\n- In monochrome Windows tray icon use dot instead of counter.\n\n4.11.3 (02.11.23)\n\n- Fix adding a link to media captions in scheduled / comments.\n- Fix crash in link preview options saving.\n- Fix possible crash in statistics.\n\n4.11.2 (01.11.23)\n\n- Highlight quoted parts in jump-to-message from replies.\n- Ctrl+Click on message field reply bar to jump to message.\n- Fix empty link preview displaying when generation failed.\n- Fix external replies in topic groups.\n- Allow enabling legacy tray icon on Windows.\n\n4.11.1 (29.10.23)\n\n- Fix crash in emoji status select.\n- Fix crash in language change.\n- Suggest shrinking only photos in webpage previews.\n- Fix opening video files in webpage previews in-app.\n- Fix sending links and markup with customized webpage previews.\n- Show \"Saved Messages\" as a first row when replying in another chat.\n- Fix selecting words by double-click with webpage previews.\n- Fix delayed webpage preview generation in preview options.\n- Add \"show-peer-id-below-about\" experimental option.\n\n4.11 (28.10.23)\n\n- View full statistics in your channels and group chats.\n- Choose which link preview in added to the message.\n- Choose if link preview is above or below the text.\n- Choose if link preview has large or small image.\n- Quote parts of text in replies.\n- Add quote formatting.\n- Reply in another chat.\n- Add nice looking code blocks with syntax highlighting.\n- Copy full code block by click on its header.\n- Send a highlighted code block using ```language syntax.\n- Change your name color in Chat Settings.\n- Customize quotes, link previews and replies to your messages.\n\n4.10.5 beta (23.10.23)\n\n- Fix crash in replies to messages with spoilers.\n- Enter boosts stats from three-dot menu.\n\n4.10.4 beta (21.10.23)\n\n- Statistics in channels and group chats.\n- Nice looking code blocks with syntax highlight.\n- Copy full code block by click on its header.\n- Send a highlighted code block using ```language syntax.\n\n4.10.3 (02.10.23)\n\n- Fix crash on external link opening. (Linux only)\n\n4.10.2 (28.09.23)\n\n- Bug fixes and other minor improvements.\n\n4.10.1 (23.09.23)\n\n- Rebuild macOS version with Xcode 14.0.1.\n\n4.10 (22.09.23)\n\n- Stories for Channels.\n- Reaction Stickers in Stories.\n\n4.9.10 beta (22.09.23)\n\n- Update Qt to 6.2.5 on macOS.\n- Update minimum target to macOS 10.13 and toolchain to Xcode 15.\n- Update Linux build host from CentOS 7 to Rocky Linux 8.\n- Update Linux toolchain to GCC 12.\n\n4.9.9 (18.09.23)\n\n- Add support for Emoji 15.\n- Several crash fixes.\n\n4.9.8 (16.09.23)\n\n- Fix t.me/botname?startapp=token deeplinks.\n- Fix a possible crash in media viewer on Wayland.\n\n4.9.7 (13.09.23)\n\n- Fix direct bot web app links handling.\n- Close main menu when opening a web app.\n- Bump libwebp revision.\n\n4.9.6 (12.09.23)\n\n- Some bot web-app improvements.\n- Bug fixes and other minor improvements.\n\n4.9.5 (04.09.23)\n\n- Several new bot web-app features.\n- Bug fixes and other minor improvements.\n\n4.9.4 (30.08.23)\n\n- Default private chats / groups / channels notification settings.\n- Forwarded / reply-to-a-story icon in chats list message preview.\n- Bug fixes and other minor improvements.\n\n4.9.3 (22.08.23)\n\n- Fix audio output on macOS.\n\n4.9.2 (21.08.23)\n\n- Remove single recently used emoji or reset the list from context menu.\n- Change all emoji skin colors from Emoji & People section.\n- Connected Websites section in Settings > Advanced.\n- Bug fixes and other minor improvements.\n\n4.9.1 (15.08.23)\n\n- Fix a crash in reply area ripple animation.\n- Fix a crash on start in some Linux distributions.\n\n4.9 (14.08.23)\n\n- Gradual stories rollout.\n\n4.8.12 beta (11.08.23)\n\n- Fix crash by a full rebuild on macOS.\n\n4.8.11 beta (10.08.23)\n\n- Fix initial video playback speed.\n- Use native window resize on Windows 11.\n- Fix memory leak in Direct3D 11 media viewer on Windows.\n\n4.8.10 (28.07.23)\n\n- Send story sharing comments as separate messages.\n- Fix stories explanation tooltip ordering.\n\n4.8.9 (26.07.23)\n\n- Bug fixes and other minor improvements.\n\n4.8.8 (25.07.23)\n\n- Several crash fixes and story viewer improvements.\n\n4.8.7 (21.07.23)\n\n- Several crash fixes and small stories improvements.\n\n4.8.6 (21.07.23)\n\n- Fix langpack keys by a full rebuild on macOS.\n\n4.8.5 (20.07.23)\n\n- Bug fixes and other minor improvements. And stories preview.\n\n4.8.4 (13.06.23)\n\n- Fix opening links on Linux.\n\n4.8.3 (31.05.23)\n\n- Fix main window focus from notifications with disabled animations.\n- Some minor fixes and improvements.\n\n4.8.2 (16.05.23)\n\n- Apply hardening runtime for Mac App Store version.\n\n4.8.1 (24.04.23)\n\n- Fix sending an album of ten scheduled messages.\n- Convert folder back to non-shareable on last link deletion.\n- Several fixes of focus control in discussions / channel comments.\n- Drop all formatting on paste in non-message input fields.\n- Clear search field on archive opening.\n- Show video upload / download progress over the spoiler.\n- Better support for text-colored emoji in reactions.\n- Close archive / topics group when clicking on currently active folder.\n- Allow replace media by paste in discussions / comments / scheduled messages.\n- Fix stuck Replace Media button after editing media in some chats.\n- Quick reply by double click only with the left mouse button.\n- Fix applying the same custom chat background with different dimming value.\n- Fix \"who reacted\" list display for media album parts.\n- Show full t.me/+ invite link prefix in Recent Actions.\n- Don't try to suggest userpic photos to bots.\n- Add \"Send when online\" to the send button context menu.\n- Fix crash in empty topics message sending.\n\n4.8 (21.04.23)\n\n- Share folders that include dozens of chats with friends or colleagues.\n- Anyone you invite can add the folder and join all its chats in one tap.\n- You can create multiple invite links to give different people access to different chats in the folder.\n- Set a custom wallpaper for any 1-on-1 chat.\n- Your chat partner can apply the same wallpaper - or choose their own.\n\n4.7.1 (21.03.23)\n\n- Fix media viewer with some system scale settings on Windows.\n- Fix calls on Linux.\n\n4.7 (19.03.23)\n\n- You can disable all resource-intensive animations and animated stickers and emoji.\n- Power saving mode turns on automatically based on system battery saving settings.\n- Fully flexible playback speed settings for videos, voice and video messages.\n- Click the \"1X\" button to quickly switch between normal and adjusted speed – or hold it to set any speed between 0.5-2.5x.\n- Improved sound quality for adjusted speed playback.\n- Read receipts in groups under 100 members now show reaction timestamps and when your messages were read.\n- When inviting people to groups, you can quickly send invite links to anyone who doesn't allow adding them directly.\n- Invite links now show previews in chats.\n- Fully translatable bots. Bot descriptions and ”What can this bot do?\" sections can now be translated.\n- Starting a call now shows a confirmation window.\n- Fixed pasting images from Firefox on Windows.\n- Global Fn+F shortcut to switch active window to full screen mode on macOS.\n- Silent notification sound in Focus Mode on macOS.\n\n4.6.12 beta (17.03.23)\n\n- Fix several possible crashes.\n- Deprecate macOS 10.12, Ubuntu 18.04 and CentOS 7 in July.\n\n4.6.11 beta (15.03.23)\n\n- Allow larger interface scale values on high-dpi screens.\n- Implement new voice and video speed change interface (up to 2.5x).\n- Support global Fn+F shortcut to toggle fullscreen on macOS.\n- Silent notification sound in Focus Mode on macOS.\n- Fix media viewer on macOS with several screens.\n- Fix a crash in connection type box.\n- Fix possible crash on quit.\n\n4.6.10 beta (12.03.23)\n\n- Suggest sending an invite link if user forbids inviting him to groups.\n- Show when a reaction was left on your message in small groups.\n- Fix a crash in video chats on Windows.\n- Fix a crash in audio speed change.\n\n4.6.9 beta (10.03.23)\n\n- Fix audio speed change filter in Windows x64 build.\n- Fix build scripts.\n\n4.6.8 beta (09.03.23)\n\n- Improve quality of voice messages with changed playback speed.\n- Show when your message was read in small groups.\n- Fix pasting images from Firefox on Windows.\n- Improve memory usage for custom emoji.\n\n4.6.7 beta (02.03.23)\n\n- Fix crash when accepting incoming calls.\n- Remove sound when cancelling an unconfirmed call.\n\n4.6.6 beta (01.03.23)\n\n- Confirmation window before starting a call.\n- New \"Battery and Animations\" settings section.\n- \"Save Power on Low Battery\" option for laptops.\n- Improved windowed mode support for media viewer.\n- Hardware accelerated video playback fix on macOS.\n- New application icon on macOS following the system guidelines.\n\n4.6.5 (25.02.23)\n\n- Fix payment card input field validation.\n- Fix video playback with hardware acceleration on macOS.\n\n4.6.4 beta (18.02.23)\n\n- Allow media viewer to exit fullscreen and become a normal window.\n\n4.6.3 (15.02.23)\n\n- Optimize chats list initial loading.\n- Various crash fixes.\n\n4.6.2 (07.02.23)\n\n- One more attempt to fix fonts on Windows.\n- Fix polls forwarding to private chats.\n- Improve translations bar appearance.\n- Improve userpic editor presets.\n\n4.6.1 (06.02.23)\n\n- Fix fonts fallback on Windows.\n- Fix crash in userpic editor.\n- Fix some crashes on 32 bit Window build.\n- Bug fixes and other minor improvements.\n\n4.6 (03.02.23)\n\n- Emoji Profile Pictures. Quickly create group and profile pictures from animated emoji and stickers with the new \"Use an Emoji\" option.\n- Emoji Categories. Filter stickers and emoji by categories like \"love\", \"cheers\" or \"sleeping\" in the sticker and emoji tabs.\n- Chat Translation. As a Premium user, translate entire chats in real time as you scroll them or receive new messages.\n- Media Permissions in Groups. Control whether members of your groups can send 9 distinct media types – like Photos, Voice or Video Messages.\n- Select Chats for Bots. Bot developers can now let users quickly select groups, channels or contacts that meet predefined criteria (more in @BotNews).\n- Open in New Window. Open chats or additional accounts in separate windows.\n\n4.5.9 beta (02.02.23)\n\n- Hide taskbar window preview when Telegram is locked by a passcode (Windows only).\n- More improvements for working with multiple windows.\n- Bug fixes and other minor improvements.\n\n4.5.8 beta (21.01.23)\n\n- Allow opening another account in a new window (see Settings > Advanced > Experimental Settings).\n- A lot of bugfixes for working with more than one window.\n\n4.5.7 beta (13.01.23)\n\n- Fix glitches after moving window to another screen (Windows only).\n\n4.5.6 beta (12.01.23)\n\n- Try enabling non-fractional scale High DPI support on Windows and Linux.\n- Experimental setting for fractional scale High DPI support on Windows and Linux.\n- Fix navigation to bottom problems in groups you didn't join.\n- Fix a crash in chat export settings changes.\n- Fix a crash in sending some of JPEG images.\n- Fix CJK fonts on Windows.\n\n4.5.5 beta (10.01.23)\n\n- Fix crash in Settings.\n\n4.5.4 beta (10.01.23)\n\n- Allow wide range of interface scale options.\n- Show opened chat name in the window title.\n- Bug fixes and other minor improvements.\n- Fix updating on macOS older than 10.14.\n\n4.5.3 (06.01.23)\n\n- Attempt to fix incoming video in calls from mobile apps.\n\n4.5.2 (03.01.23)\n\n- Fix unread reactions button in private chats.\n- Fix tile background saving after an app update.\n- Allow Ctrl+6,7,8 to activate extra pinned chats.\n\n4.5.1 (02.01.23)\n\n- Fix crash in profile photo privacy edition.\n- Allow sending photos larger than 1280px (in Experimental Settings).\n\n4.5 (30.12.22)\n\n- Media with spoiler effects. You can wrap photos and videos you send in a fuzzy cover by selecting media in the attachment menu and tapping (...) > Hide With Spoiler.\n- Setting pictures for your contacts. You can choose your own picture for a contact – only you will see it on their profile.\n- Suggested profile pictures. When editing your contacts, you can suggest a photo for their profile. It will take them just two clicks to add the picture you suggested.\n- Public profile pictures. If you only allow certain users to see your profile photos, you can set a public picture for everyone else.\n- Ultimate profile picture privacy. You can set 'Who can see my profile photos' to 'Nobody' and add some users or groups as exceptions.\n- Member list privacy. Owners of large groups can hide the list of members.\n\n4.4.3 beta (29.12.22)\n\n- Support for anonymous numbers from the Fragment.com platform.\n- Fix a crash in own profile photo updating.\n- Bug fixes and other minor improvements.\n\n4.4.2 beta (28.12.22)\n\n- Send photos and video files hidden by a spoiler effect.\n- Set a public photo for those who are restricted to see your profile photo in the Privacy Settings.\n- Bug fixes and other minor improvements.\n\n4.4.1 (07.12.22)\n\n- Bug fixes and other minor improvements.\n\n4.4 (06.12.22)\n\n- You can set a Global Auto-Delete Timer for all new chats and groups you create.\n- New messages will be deleted after 1 day, week, month - or a custom duration you choose.\n- The new menu in Settings > Privacy & Security > Auto-Delete Messages also lets you set up Auto-Delete for any of your existing chats faster.\n- Admins of groups with 100+ members can organize discussions into separate topics.\n- Topics now have a two-column layout that keeps recent chats easily accessible as you browse topics.\n- Previews of topic messages in the chat list work like a button - click to jump to the last updated topic.\n- Up to 5 topics can be pinned to the top of the list.\n- Each topic now supports multiple pinned messages.\n- The View as Messages mode now shows which topic each message belongs to.\n- Badge counters for topics you never opened will have a lighter, less distracting color.\n- A new General topic is now included by default, containing service messages and the earlier message history from the group.\n- Admins can rename the General topic.\n- Large groups can enable Aggressive Filtering in Manage Group > Administrators to remove more spam with automated algorithms.\n- Admins help improve filtering by reporting any false positives in Recent Actions.\n\n4.3.4 (25.11.22)\n\n- Fix OpenGL regression in Qt and language detection.\n\n4.3.3 (23.11.22)\n\n- Fix an issue with media auto-download on Windows.\n- Fix switching accounts in maximized window.\n- Fix collapsed archive row layout.\n\n4.3.2 (21.11.22)\n\n- Enable message translations in Settings > Language.\n- Fast jump to the last updated topic.\n- Bug fixes and other minor improvements.\n\n4.3.1 (07.11.22)\n\n- Critical bug fixes.\n\n4.3 (05.11.22)\n\n- Groups with more than 200 members can now have their discussions organized into topics.\n- Premium users can now convert both voice messages and video messages into text.\n\n4.2.4 (30.09.22)\n\n- Fix crash in emoji statuses clearing.\n\n4.2.3 (28.09.22)\n\n- Improve scaling / cropping for photos / video files.\n- Improve touch support in channel comments.\n- Nice animation for spoilers.\n\n4.2.2 beta (23.09.22)\n\n- Fix crash in spoiler revealing in media captions.\n- Fix spoiler revealing in media viewer captions.\n- Fix crash in folder editing on Linux.\n\n4.2.1 beta (22.09.22)\n\n- Improve scaling / cropping for photos / video files.\n- Improve touch support in channel comments.\n- Nice animation for spoilers.\n\n4.2 (17.09.22)\n\n- A new expandable reaction menu was added in private chats and groups.\n- All users get access to dozens of new reactions, including reactions previously reserved for Premium users.\n- The reactions you use most frequently will always be displayed at the top.\n- Premium users can react to messages with thousands of custom emoji and add up to 3 reactions to each message.\n- Group admins can control whether custom reactions are allowed in their groups.\n- Premium users can add an animated emoji status next to their name to display their current activity.\n- To set a status, tap your Premium badge in the main menu or Settings.\n- Popular suggestions for working, sleeping, traveling and more will be shown at the top.\n- To set a status for a specific duration like 1 hour or 2 days, right click the emoji.\n- A new format was supported for username links, in addition to \"t.me/username.\" You can now open Telegram accounts, groups or channels using links like \"username.t.me\" or \"https://username.t.me.\"\n- Improved support for long media captions.\n\n4.1.2 beta (14.09.22)\n\n- New reaction selector above the right click menu.\n- Premium: Set any custom emoji reactions in private chats.\n- Premium: Set any custom emoji as your profile status.\n- Insert or copy custom emoji from pack preview.\n\n4.1.1 (16.08.22)\n\n- Fix editing media captions with animated emoji.\n- Fix viewing animated emoji sets used in a message.\n- Fix premium sticker tooltip auto-show in channel comments.\n- Fix main window position and size restoring after relaunch.\n- Other bug fixes and minor improvements.\n\n4.1 (12.08.22)\n\n- Include animated emoji in messages and captions.\n- Premium users unlock over 500 new emoji, including custom packs with unique characters.\n- All users can view any emoji – and try them out in the Saved Messages chat.\n- Control who can send you voice and video messages with Telegram Premium.\n- Send a Premium subscription to any user from their profile.\n- Choose 3, 6, or 12 months – at a discount.\n\n4.0.4 beta (04.08.22)\n\n- Allow sending animated emoji to Saved Messages even without Telegram Premium.\n- Premium: Suggest animated emoji by regular emoji (can be disabled in Settings).\n- Premium: Show all suggested premium stickers in a special section of the stickers panel.\n- Premium: Allow hiding premium stickers special section of the stickers panel.\n- Fix a memory leak in RTMP livestreams.\n- Fix some bot webview bugs on macOS.\n- Fix forwarding of voice messages.\n\n4.0.3 beta (28.07.22)\n\n- Animated emoji for messages.\n- Premium: Privacy settings for voice messages.\n- Premium: Gifting Telegram Premium to any user from their profile page.\n\n4.0.2 (24.06.22)\n\n- Fix window title on Windows 7. (again)\n- Fix file chooser and global menu on macOS.\n- Crash fix and OpenAL check for PipeWire fix on Linux.\n\n4.0.1 (23.06.22)\n\n- Fix window title on Windows 7.\n- Bug fixes and other minor improvements.\n\n4.0 (22.06.22)\n\n- Premium: Send media and files each up to 4 GB in size.\n- Premium: Download media and files at the fastest possible speed, with no limits.\n- Premium: Double most of the limits in the app.\n- Premium: Get a new button next to any voice message to generate a transcript of its audio.\n- Premium: React with even more emoji, including :clown: and :heart_eyes:.\n- Premium: Send unique stickers with additional effects, updated monthly.\n- Premium: Set a default chat folder or enable tools to auto-archive and hide new chats.\n- Premium: Subscribers have a badge next to their name, showing they help support Telegram.\n- Premium: Show off your profile video that will be animated for everyone in chats and the chat list.\n- Premium: Sponsored Messages that are sometimes shown in public channels will no longer appear.\n- Enable join requests for your public groups – no invite links required.\n- Users who open the group will see an Apply to Join Group button.\n- Once approved by an admin, users will be able to participate in the chat.\n- Bots that are integrated into the attachment menu can be programmed to work in groups and channels.\n\n3.7.6 beta (16.06.22)\n\n- Settings > Advanced > Experimental adds an option to open chats in separate windows.\n- Fix possible crash in video chat reconnection.\n- Fix possible crash after account switch.\n\n3.7.5 beta (12.05.22)\n\n- Improve cloud password management design.\n- Fix a crash in shared media search.\n- Fix audio recording on macOS.\n\n3.7.4 beta (03.05.22)\n\n- More icons for chat folders.\n- Improve some more sections design.\n- Update the OpenAL library to 1.22.0.\n\n3.7.3 (26.04.22)\n\n- Fix a crash in the pinned bar bot button refresh.\n\n3.7.2 (25.04.22)\n\n- Fix mute period selector values.\n- Fix a crash in repeated context menu item selection.\n- Fix context menu item selection of systems without a compositor.\n\n3.7.1 (19.04.22)\n\n- Hardware accelerated video decoding off by default.\n- Fix several crashes.\n\n3.7 (16.04.22)\n\n- Use any short music file or voice message as a notification sound.\n- Right click audio files in chats to add them to your list of sounds - or use 'Upload Sound' in the Notifications menu.\n- Click on Mute notifications > Select sound in a chat's (...) menu to change its notification sound.\n- Set custom tones for notifications in Settings > Notifications > Play sound.\n- Click on Mute notifications in a chat's (...) menu to manage its notifications.\n- Choose 'Mute for...' to turn off notifications for a preset period, like 1 hour or 1 day.\n- Quickly configure Auto-Delete settings from any chat info page.\n- Click (...) to turn on Auto-Delete, then select a specific duration.\n- Replies are now preserved when forwarding messages, making forwarded conversations easier to read.\n- Bots can now open detailed pages directly in the chat.\n- Use these streamlined interfaces to buy real-world goods and services without leaving the app.\n- Open a bot's profile to add it to your group or channel.\n- Instantly configure a bot's rights and permissions when adding it.\n- Bots can send a new type of button that lets you add them to your group or channel.\n\n3.6.3 beta (13.04.22)\n\n- Allow sending the default reaction by a double click.\n- Select a custom sound for message notifications.\n- Add chats to folders from a chat context menu.\n- Fix group and channel photo upload.\n- Test hardware video decoding.\n\n3.6.2 (27.03.22)\n\n- Fix photo changing in settings, groups and channels.\n- Fix non-square animated video-stickers.\n\n3.6.1 (16.03.22)\n\n- Change volume for RTMP stream from the context menu.\n- Set a profile photo from your camera.\n- Improve RTMP stream full screen mode.\n- Improve edit account section design.\n- Fix switching personal account or channel in video chats.\n- Fix pinning chats in folders with 100 chats.\n- Fix YouTube timestamp links after 10 hours.\n\n3.6 (11.03.22)\n\n- Active and recently finished downloads pop up in bar in the bottom left corner, like they do in browsers.\n- View recently downloaded files in Settings > Advanced > Downloads.\n- Get an alert before closing the app if you have unfinished downloads.\n- Share a direct t.me link to your phone number that instantly opens a chat with you.\n- Use the full number in international format, like t.me/+123456789\n- Manage Live Streams in your channels using external software like OBS Studio or XSplit Broadcaster.\n- Choose \"Stream With...\" when staring a video chat or live stream - then copy your Stream Key and paste it into your streaming software.\n\n3.5.6 beta (08.03.22)\n\n- Show viewers count in RTMP streams.\n- Send GIFs search results without \"via @bot\".\n- Display the file thumbnail in downloads bar.\n- Always try to save original photo bytes to disk.\n- Fix crash when deleting a user from your contacts list.\n\n3.5.5 beta (05.03.22)\n\n- Support stereo audio output in RTMP streams.\n- Improve RTMP stream full screen mode.\n- Fix a couple of crashes.\n\n3.5.4 beta (01.03.22)\n\n- Bug fixes and other minor improvements.\n\n3.5.3 beta (28.02.22)\n\n- Check the status of media and file downloads by clicking on the new panel in the bottom of the chats list.\n- View recently downloaded files from the new Settings > Advanced > Downloads section.\n- Manage Live Streams in your groups and channels using external software like OBS Studio or XSplit Broadcaster.\n\n3.5.2 (06.02.22)\n\n- Fix a freeze in audio playback on Linux.\n- Fix a crash in screen sharing initialization on Linux.\n\n3.5.1 (04.02.22)\n\n- Keep the screen on while watching a video or participating in a video chat.\n- Save experimental settings between relaunches.\n- Bug fixes and other minor improvements.\n\n3.5 (31.01.22)\n\n- Use a new type of detailed stickers with smooth animations.\n- Create new sets by sending .webm videos to @stickers.\n- Bring your custom animated stickers from other apps.\n- See smaller, compact animations when reacting to messages.\n- See real-time animations in chat when a user reacts to your message.\n- React with additional emoji expressing love, appreciation, anger or surprise.\n- Tap the new button in chats to jump to your messages that have unseen reactions.\n- Watch the animations for unseen reactions play when you hit the button.\n- The app will warn you before closing if you are uploading photos or files to a chat.\n- Enjoy better screencast quality in video chats.\n\n3.4.8 (19.01.22)\n\n- Nice animations in reactions.\n- Add snap layouts support on Windows 11.\n- Bug fixes and other minor improvements.\n\n3.4.7 beta (19.01.22)\n\n- Fix a crash on launch on Windows.\n\n3.4.6 beta (18.01.22)\n\n- Add snap layouts support on Windows 11.\n- Fix crash in drafts after accounts switching.\n\n3.4.5 beta (16.01.22)\n\n- Fix crash in monospace blocks processing.\n- Fix reaction animations stopping after an hour uptime.\n\n3.4.4 beta (14.01.22)\n\n- Nice animations in reactions.\n\n3.4.3 (03.01.22)\n\n- Bug fixes and other minor improvements.\n\n3.4.2 (31.12.21)\n\n- Bug fixes and other minor improvements.\n\n3.4.1 (31.12.21)\n\n- Bug fixes and other minor improvements.\n\n3.4 (30.12.21)\n\n- Send reactions to messages.\n- Group and Channel admins can enable reactions in their chat via '...' menu > Manage > Reactions.\n- Select text when typing and choose 'Formatting > Spoiler' in the context menu to hide some or all of the contents of a message.\n- Click on the spoiler in chat to reveal its hidden text.\n- Spoiler formatting hides text in chat, as well as in the chat list and notifications.\n\n3.3.2 beta (29.12.21)\n\n- Select text when typing and choose 'Formatting > Spoiler' in the context menu to hide some or all of the contents of a message.\n- Click on the spoiler in chat to reveal its hidden text.\n- Spoiler formatting hides text in chat, as well as in the chat list and notifications.\n\n3.3.1 beta (22.12.21)\n\n- Switch between contacts list sorting modes.\n- Sort contacts list by last seen time by default.\n- Fix disappearing Send As Channel button after message editing.\n- Fix file upload cancelling.\n- Fix crash in video capture on macOS.\n- Fix labels in the About box.\n- Use Qt 6.2.2 for macOS and Linux builds.\n- Allow installing x64 Windows version on Windows ARM.\n\n3.3 (07.12.21)\n\n- Content creators can restrict the ability to save media and forward messages from their groups and channels.\n- Clear messages in one-on-one chats from a specific day or date range.\n- Comment as one of your channels in public groups and channel comments.\n- When you request to join a community and its admin or bot-admin contacts you with a message, you will see which chat they are from at the top of the chat.\n- Bot-admins can now ask users to complete tasks before they are allowed to join - like accepting community rules, passing a test, or making a donation to the content creators.\n\n3.2.8 beta (01.12.21)\n\n- Fix crash in opening shared media with another user.\n\n3.2.7 beta (30.11.21)\n\n- Active sessions list redesign.\n- Fix disappearing emoji selector button.\n- Fix a crash in archived stickers loading.\n- Fix a crash in calls to old Telegram versions.\n\n3.2.6 beta (26.11.21)\n\n- Try out the new audio player with playlist shuffle and repeat.\n- Give a custom name to your desktop session to distinguish it in the sessions list.\n\n3.2.5 (16.11.21) [Windows, macOS]\n\n- Fix connecting in case bad characters appear in device name on Windows.\n- Fix custom input methods on macOS.\n\n3.2.4 (13.11.21)\n\n- Fix editing of scheduled messages on macOS.\n- Fix launching on Chrome OS in Crostini container.\n\n3.2.3 (08.11.21)\n\n- Fix autoupdater launch on macOS.\n\n3.2.2 (05.11.21)\n\n- Fix webpage preview links when View Button is present.\n- Fixed possible crash on sponsored messages.\n\n3.2.1 (03.11.21)\n\n- Remove AppImage icon hack and -pie.\n\n3.2 (03.11.21)\n\n- Create special invite links that require admins to approve users before they become members.\n- Admins can view the applicants' profiles and bios by tapping the Join Requests bar at the top of the chat.\n- Add internal labels to your chat's Invite Links to keep them organized.\n- More Interactive Emoji - :ghost:, :dislike:, :face_vomiting:, :joy:, :money_with_wings: or :jack_o_lantern:\n\n3.1.13 beta (02.11.21)\n\n- Fix requests to groups / channels processing.\n- Fix internal link previews with View Content button layout.\n- Fix crash in messages search with imported messages results.\n- Don't use fractional system UI scaling on Linux.\n- Fix invite link icons on macOS.\n- Several crash fixes.\n\n3.1.12 beta (30.10.21)\n\n- Create special invite links that require admins to approve users before they become members.\n- Admins can view the applicants' profiles and bios by tapping the Join Requests bar at the top of the chat.\n- Add internal labels to your chat's Invite Links to keep them organized.\n- Run natively on Apple Silicon (macOS only).\n\n3.1.11 (29.10.21)\n\n- Create special invite links that require admins to approve users before they become members.\n- Admins can view the applicants' profiles and bios by tapping the Join Requests bar at the top of the chat.\n- Add internal labels to your chat's Invite Links to keep them organized.\n- Run natively on Apple Silicon (macOS only).\n\n3.1.10 beta (29.10.21)\n\n- Native support for M1 on macOS.\n- Build macOS and Linux versions on Qt 6.2.\n\n3.1.9 (08.10.21)\n\n- Fix crash in chat closing while scrolling (macOS only).\n\n3.1.8 (08.10.21)\n\n- Show small media previews in chats list.\n- Show media album previews and caption text in chats list.\n- Add \"Quick Reply\" and \"Mark as Read\" to native Windows notifications.\n\n3.1.7 beta (07.10.21)\n\n- Fix channel message views and comments counter updates.\n- Sponsored messages support.\n- Crash fix.\n\n3.1.6 beta (06.10.21)\n\n- Show small media previews in chats list.\n- Show media album previews and caption text in chats list.\n- Add \"Quick Reply\" and \"Mark as Read\" to native Windows notifications.\n\n3.1.5 beta (28.09.21)\n\n- Choose one of 8 new preset themes for any individual private chat.\n- Click on '...' menu > 'Change Colors' to pick a theme.\n- Both chat participants will see the same theme in that chat – on all their devices.\n- Each new theme features colorful gradient message bubbles, beautifully animated backgrounds and unique background patterns.\n- All chat themes have day and night versions and will follow your overall dark mode settings.\n- Implement main window rounded corners on Windows 11.\n- Fix audio capture from AirPods on macOS.\n\n3.1.4 beta (27.09.21)\n\n- Fix crash in network availability init.\n- Fix assertion violation after a NaN-resulting std::round call.\n\n3.1.3 beta (27.09.21)\n\n- Fix illegal instruction crash in opus encoder.\n\n3.1.2 beta (26.09.21)\n\n- Control video in fullscreen mode using arrows and numbers.\n- Open locations in browser if default Bing Maps is not installed.\n- Reconnect without timeout when network availability changes.\n- Crash fixes.\n\n3.1.1 (24.09.21)\n\n- Crash fixes.\n\n3.1 (19.09.21)\n\n- Some animated emoji now have extra effects.\n- Send :fireworks: :tada:, :balloon:, :like:, :poop: or :heart: to any private chat, then click on the animated emoji to launch the effect.\n- If your chat partner also has the chat open, you will both see the effects.\n- See the \"Watching\" status when your chat partner is enjoying emoji effects with you.\n- More interactive emoji coming soon.\n- Right click one of your outgoing messages in small groups to see who recently viewed it.\n- To protect privacy, read receipts are only stored for 7 days after the message was sent.\n- Record video and audio from live broadcasts in your group or channel.\n- Admins can start recording from the '...' menu.\n- Choose between recording in portrait or landscape orientation.\n- Finished recordings are sent to the admin's Saved Messages and can be easily shared.\n\n3.0.5 beta (17.09.21)\n\n- Add support for Emoji 13.1.\n\n3.0.4 beta (13.09.21)\n\n- Fix a crash when joining video chat or live broadcast.\n- Add a \"Close to Taskbar\" option when tray icon is disabled (Windows and Linux).\n\n3.0.3 beta (12.09.21)\n\n- Try fixing crashes in allocator on Linux.\n\n3.0.2 beta (10.09.21)\n\n- Check who've seen your message in small groups from the context menu.\n- Enable recording with video in live streams and video chats.\n\n3.0.1 (01.09.21)\n\n- Crash fixes.\n\n3.0 (31.08.21)\n\n- Broadcast video and share your screen to an unlimited number of viewers.\n- To begin, tap the Live Stream button in the title bar of a community where you are an admin.\n- Tap the \"Forward Message\" label above the input field to change how messages will be sent.\n- Hide or show the original sender's name.\n- Remove or keep captions from media messages.\n- See how many unread comments there are when opening a channel's comments.\n\n2.9.14 beta (31.08.21)\n\n- Fix crash in authorization after logout.\n\n2.9.13 beta (31.08.21)\n\n- See unread comments count when scrolling discussions in channels.\n\n2.9.12 beta (24.08.21)\n\n- Disable floating point exceptions in 32 bit Windows version.\n\n2.9.11 beta (24.08.21)\n\n- Resolve (again) a video playback crash in 32 bit Windows version.\n\n2.9.10 beta (24.08.21)\n\n- Resolve (hopefully) a video playback crash in 32 bit Windows version.\n\n2.9.9 beta (23.08.21)\n\n- Still(3) debugging a video playback crash in 32 bit Windows version.\n\n2.9.8 beta (23.08.21)\n\n- And still debugging a video playback crash in 32 bit Windows version.\n\n2.9.7 beta (23.08.21)\n\n- Still debugging a video playback crash in 32 bit Windows version.\n\n2.9.6 beta (21.08.21)\n\n- Debugging a video playback crash in 32 bit Windows version.\n\n2.9.5 beta (20.08.21)\n\n- Tile chat background patterns horizontally.\n- Fix a rare crash in spellchecker on Windows.\n- Fix animated chat backgrounds in Saved Messages.\n- Fix \"Sorry, group is inaccessible\" message in scheduled voice chats.\n\n2.9.4 beta (17.08.21)\n\n- Choose one from dozens of new gorgeous animated backgrounds in Chat Settings > Chat background.\n\n2.9.3 (11.08.21)\n\n- Fix requesting screencast rights on macOS (again).\n\n2.9.2 (10.08.21)\n\n- Fix crashes and bugs in scheduled messages.\n- Fix file sending after a call or voice chat on Windows.\n- Fix main window title glitches on Windows 7.\n\n2.9.1 (30.07.21)\n\n- Fix requesting screencast rights on macOS.\n\n2.9 (30.07.21)\n\n- Enable auto-delete in your chats to remove messages after 1 month (also 1 day or 1 week).\n- Quickly find all GIFs from a chat using the new GIF section in Shared Media.\n- Reset your Two-Step Verification password even if you forgot your old password and don't have a recovery email.\n- To do this, you must be logged into your account. The reset takes 7 days and can be cancelled from any of your devices.\n- Enjoy a new app icon and default background.\n\n2.8.13 beta (23.07.21)\n\n- Fix crash in messages animation.\n- Save GIFs from context menu in channel comments.\n\n2.8.12 beta (20.07.21)\n\n- Change the default chat background.\n- Add GIFs overview section in chats.\n- Add a simple new messages animation.\n- Use modern Telegram application icon.\n- Use Direct3D 11 by default on Windows.\n- Fix Direct3D acceleration on basic Windows 7 setup.\n\n2.8.11 (17.07.21)\n\n- Check shader compilation / linking before using ANGLE.\n\n2.8.10 (16.07.21)\n\n- Fix media loading.\n\n2.8.9 (16.07.21)\n\n- Fix GIF caption edit.\n- Fix version on Windows without SetDefaultDllDirectories.\n- Fix fonts on Linux.\n\n2.8.8 (15.07.21)\n\n- Added an image editor. Crop photos or highlight parts of screenshots before sending.\n\n2.8.7 beta (08.07.21)\n\n- (wasn't deployed)\n\n2.8.6 beta (06.07.21)\n\n- Added a simple image editor. Crop photos or highlight parts of screenshots before sending.\n- Use Direct3D 9 backend in ANGLE by default (Windows).\n- Fix \"Show in Finder\" not focusing the Finder window (macOS).\n- Use GTK from a child process (Linux).\n\n2.8.5 beta (02.07.21)\n\n- Use ANGLE for OpenGL over DirectX 9 / DirectX 11 (Windows).\n- Use GTK from a child process (Linux).\n\n2.8.4 (01.07.21)\n\n- Crash fixes in WebView on Windows.\n\n2.8.3 (28.06.21)\n\n- Fix crashes in OpenSSL on macOS.\n\n2.8.2 (27.06.21)\n\n- Attempt to fix random crashes on macOS.\n\n2.8.1 (26.06.21)\n\n- Fix crash in audio player volume slider.\n\n2.8 (24.06.21)\n\n- Start video conferences from Voice Chats in any group.\n- Share your screen or video from your camera with up to 30 participants (limit to be increased soon).\n- Talk without video with an unlimited number of participants.\n- Create voice chats from the info page of any group where you are an admin.\n- Group video calls are supported natively on all devices, including iPads and laptops.\n\n2.7.10 beta (22.06.21)\n\n- Added ability to mix together bold, italic and other formatting.\n- Fix voice chats and video calls OpenGL with some drivers on Windows.\n- Several bug fixes.\n\n2.7.9 beta (17.06.21)\n\n- Added \"Enable noise suppression\" option to group calls Settings.\n- Fix media viewer with Retina + Non-Retina dual monitor setup on macOS.\n- Several bug and crash fixes.\n\n2.7.8 beta (11.06.21)\n\n- Crash fixes.\n\n2.7.7 beta (11.06.21)\n\n- Optimized video playback in media viewer and Picture-in-Picture mode.\n- Added integration with System Media Transport Controls on Windows 10.\n- Added \"Now Playing\" integration for music playback on macOS.\n- Added \"Archive Sticker\" into the \"...\" menu of the Sticker Set Box.\n- Fixed memory not being freed on Linux.\n- Several crash fixes.\n\n2.7.6 beta (31.05.21)\n\n- Optimized video playback in media viewer and Picture-in-Picture on Windows and Linux.\n- Added integration with System Media Transport Controls on Windows 10.\n- Added \"Archive Sticker\" into the \"...\" menu of the Sticker Set Box.\n- Fixed memory not being freed on Linux.\n- Several crash fixes.\n\n2.7.5 beta (04.05.21)\n\n- Add \"Voice chats\" filter in \"Recent actions\" for channels.\n- Write local drafts to disk on a background thread.\n- Support autoupdate for Telegram in write-protected folders on Linux.\n- Fix crash in native notifications on Linux.\n- Fix crash in file dialog on Linux.\n\n2.7.4 (28.04.21)\n\n- Fix crash in viewing an invoice after a payment is made.\n- Respect Focus Assist only for native notifications.\n- Mark messages as read only in active window.\n\n2.7.3 (27.04.21)\n\n- Fix crash on some versions of Linux.\n- Fix video not stopping when PiP window is closed.\n- Fix messages marking as read if the Windows session is locked.\n\n2.7.2 (26.04.21)\n\n- Offer real goods and services for sale in any group, channel or bot – Telegram doesn't charge a commission.\n- Pay for goods securely using one of the 8 integrated payment providers – Telegram doesn't collect your payment info.\n- See how this works in our @TestStore.\n- Schedule voice chats to let participants know about them in advance.\n- View a countdown to the voice chat and get notified when it starts.\n- Try two new, fully-featured versions of Telegram Web – both supporting animated stickers, dark mode, chat folders and more: https://webk.telegram.org/ and https://webz.telegram.org/.\n\n2.7.1 (20.03.21)\n\n- Fix editing 'Manage Voice Chats' rights for channel admins.\n- Fix verification check display in voice chat participants list.\n- Allow removing and blocking channels from voice chats.\n\n2.7 (19.03.21)\n\n- Start limitless Voice Chats in Groups and Channels.\n- Host discussions that can be listened to by millions of people simultaneously.\n- Record voice chats to share or publish in Channels later.\n- See that a chat is being recorded from the red dot next to its title.\n- See user bio texts right from the list of participants.\n- Raise your hand to show admins you want to speak.\n- Create separate Voice Chat Invite Links for listeners or speakers.\n- Change the title of your Voice Chat to give people an idea of the current topic.\n- Join Voice Chats as one of your Channels to hide your personal account.\n\n2.6.8 beta (18.03.21)\n\n- Fix connecting and getting allowed to speak on voice chats.\n- MPRIS support on Linux.\n\n2.6.7 beta (18.03.21)\n\n- Improve voice chat participants list updating.\n\n2.6.6 beta (18.03.21)\n\n- Fix joining popular voice chats.\n\n2.6.5 beta (17.03.21)\n\n- Improvements and fixes in new voice chat features.\n\n2.6.4 beta (16.03.21)\n\n- Fix freeze in voice chats.\n- Make default interface scale 110% on macOS Retina screens.\n\n2.6.3 beta (16.03.21)\n\n- Fix audio device selection in voice chats.\n- Fix blinking self profile photo in case the profile photo privacy is used.\n- Fix voice chat admin menu on macOS.\n\n2.6.2 beta (13.03.21)\n\n- Fix text disappearing because of cloud drafts sync.\n\n2.6.1 (24.02.21)\n\n- Fix time formatting in links.\n- Fix copy QR code in night mode.\n- Fix invite link sharing without a comment.\n- Fix crash in link editing on Linux.\n\n2.6 (23.02.21)\n\n- Set messages to auto-delete for everyone 24 hours or 7 days after sending.\n- Control auto-delete settings in any of your chats, as well as in groups and channels where you are an admin.\n- To enable auto-delete, right click on the chat in the chat list > Clear History > Enable Auto-Delete.\n- Create invite links that work for a limited time or a limited number of uses.\n- See which users joined using your, or your admins', invite links.\n- Turn any invite link into a QR code users can scan with their phone cameras.\n- To manage invite links, click ... > Manage Group/Channel > Invite Links.\n- Convert groups that have reached 200,000 members into unlimited Broadcast Groups.\n\n2.5.9 (17.02.21)\n\n- Add 'Invite via Link' button to Add Members box.\n- Fix window size in Windows 10 Tablet Mode.\n- Fix layout of round video messages in channels.\n\n2.5.8 (29.01.21)\n\n- Fix OpenAL device closing in calls and voice chats.\n- Fix video chat rotation when calling from iOS.\n- Fix scheduling messages without sound.\n- Remove redundant Cancel button in ScheduleBox.\n\n2.5.7 (28.01.21)\n\n- Delete not only messages, but also groups you created and call history for all sides, without a trace.\n- Adjust volume for individual participants of a voice chat.\n- Report fake groups or channels impersonating famous people or organizations by opening their Profile > ... > Report.\n\n2.5.6 beta (22.01.21)\n\n- Press Up arrow to edit your last sent comment.\n- Add more information to date tooltips in Recent Actions and channel comments.\n- Bug and crash fixes.\n\n2.5.5 beta (10.01.21)\n\n- Fix recording of audio in voice chats.\n- Fix media viewer zoom and crashing.\n\n2.5.4 beta (07.01.21)\n\n- Implement new audio module code for calls and voice chats.\n- Allow retracting votes from polls in comments to channel posts.\n- Show small voice chat button for empty voice chats.\n- Fix media viewer updating when screen resolution is changed.\n\n2.5.3 beta (30.12.20)\n\n- Allow using mouse buttons in Push-to-Talk shortcut.\n- Fix blurred thumbnails in Shared Links section.\n\n2.5.2 beta (25.12.20)\n\n- Fix possible crash in video calls.\n- Fix possible crash in connecting to voice chats.\n- Use different audio module code on Windows in calls.\n\n2.5.1 (23.12.20)\n\n- Fix crash in voice calls.\n\n2.5 (23.12.20)\n\n- Turn any of your group chats into a hop-on, hop-off conference call.\n- Get up to several thousand participants in each voice chat.\n- Control the number of speakers with flexible admin tools.\n\n2.4.15 beta (19.12.20)\n\n- Improve design of voice chats.\n- Fix sending of voice messages as replies.\n- Fix 'Open With' menu position in macOS.\n- Fix freeze on secondary screen disconnect.\n\n2.4.14 beta (14.12.20)\n\n- Create voice chats in legacy groups.\n- Fix sticker pack opening.\n- Fix group status display.\n- Fix group members display.\n\n2.4.13 beta (09.12.20)\n\n- Fix voice messages sending.\n- Fix crash in voice calls.\n\n2.4.12 beta (09.12.20)\n\n- Voice chats in groups. (alpha version)\n\n2.4.11 beta (19.11.20)\n\n- Improve locked voice message recording.\n- Fix main window closing to tray on Windows.\n- Fix crash in bot command sending.\n- Fix adding additional photos when sending an album to a group with enabled slow mode.\n\n2.4.10 beta (17.11.20)\n\n- Use inline bots and sticker by emoji suggestions in channel comments.\n- Lock voice message recording, listen to your voice message before sending.\n\n2.4.9 beta (06.11.20)\n\n- Fix crash in tray icon removing. (macOS only)\n\n2.4.8 beta (06.11.20)\n\n- Upgrade Qt to version 5.15.1.\n- Upgrade FFmpeg to version 4.2.\n- Upgrade OpenAL to version 1.20.1.\n\n2.4.7 (05.11.20)\n\n- Fix playback display in albums of music files.\n- Several crash fixes.\n\n2.4.6 (02.11.20)\n\n- Fix image compression option when sending files with drag-n-drop.\n- Fix caption text selection in media albums.\n- Fix drafts display in personal chats in the chats list.\n- Bug fixes and other minor improvements.\n\n2.4.5 (30.10.20)\n\n- Pin several messages in any chat, including one-on-one chats.\n- Jump between pinned messages or open them all on a separate page via the top bar.\n- Send several music tracks as a playlist.\n- Send several files as an album in one chat bubble.\n- Send a :slot_machine: emoji to any chat to see if you hit the jackpot.\n- Hide Telegram taskbar icon on Linux in Settings > Advanced.\n\n2.4.4 (23.10.20)\n\n- Fix application quit on call end with main window hidden in tray.\n- Update OpenAL library on Windows.\n- Several crash fixes.\n\n2.4.3 (07.10.20)\n\n- Fix sending voice messages in scheduled messages section.\n- Fix deleting profile / group / channel photos.\n- Several crash fixes.\n\n2.4.2 (02.10.20)\n\n- Allow block, report and delete all messages from a user from \"user joined\" service message context menu.\n- Fix admin badge display in groups.\n- Fix loading and opening of comments in channels.\n\n2.4.1 (01.10.20)\n\n- Move by PageUp and PageDown in channel comments.\n- Several layout bugfixes.\n- Several crashfixes.\n\n2.4 (30.09.20)\n\n- Turn on \"Remain Anonymous\" in an admin's Permissions to let them post on behalf of the group and become invisible in the list of members.\n- Comment on posts in channels that have a discussion group.\n- Get notified about replies to your comments via the new Replies chat (if you are not a member of the discussion group).\n\n2.3.2 (23.08.20)\n\n- Revert custom window shadow on Linux.\n\n2.3.1 (21.08.20)\n\n- Fix Call Settings for Video Calls.\n\n2.3 (14.08.20)\n\n- Video Calls (alpha version).\n\n2.2 (26.07.20)\n\n- Quickly switch between different Telegram accounts if you use multiple phone numbers.\n- Share and store unlimited files of any type, now up to 2000 MB each.\n- Edit your scheduled messages.\n- Use Auto-Night Mode to make Telegram night mode match the system Dark Mode settings.\n- Also added an option to switch to system window frame in Windows and Linux.\n\n2.1.22 beta (24.07.20)\n\n- Fix crash in web page preview display.\n\n2.1.21 beta (24.07.20)\n\n- Edit your scheduled messages.\n- See the unread messages indicator for the secondary accounts on the main menu button.\n- Use Auto-Night Mode to make Telegram night mode match the system Dark Mode settings.\n- Enjoy dark native window frame for Telegram night mode on Windows.\n\n2.1.20 beta (17.07.20)\n\n- Fix animated emoji.\n- Fix crash in bot callback sending.\n\n2.1.19 beta (16.07.20)\n\n- File uploading in an inactive account correctly finishes.\n- Stickers panel works correctly after switching between accounts.\n- Large .webp files are not shown as stickers.\n- MacBook TouchBar support was fully rewritten with fixes for multiple accounts.\n- Custom window title bar works in all Linux versions.\n- Passcode doesn't auto-lock while you're active in other apps on Linux X11.\n\n2.1.18 beta (08.07.20)\n\n- Fix a possible crash in Picture-in-Picture video player.\n- Fix copying links from message texts.\n- Raise file size limit to 2000 MB.\n- Allow using system window frame in Windows and Linux.\n\n2.1.17 beta (02.07.20)\n\n- Fix messages editing in a non-active account.\n- Fix large animated emoji messages editing.\n- Fix high definition GIF animations opening in media viewer.\n- Multiple crash fixes.\n\n2.1.16 beta (01.07.20)\n\n- Crash fix.\n\n2.1.15 beta (30.06.20)\n\n- Receive notifications only from the active account in Settings > Notifications.\n- Fix saving chats list width between application relaunches.\n- Multiple crash fixes.\n\n2.1.14 beta (29.06.20)\n\n- Support for multiple accounts.\n\n2.1.13 (23.06.20)\n\n- Fix photos loading.\n- Fix Picture-in-Picture window movement on Wayland in Linux.\n\n2.1.12 (17.06.20)\n\n- Fix sticker and video results in inline bots.\n- Fix clipboard issues in Linux.\n- Fix several crashes.\n\n2.1.11 (08.06.20)\n\n- Fix launching on old Linux systems.\n\n2.1.10 (05.06.20)\n\n- Improve memory usage.\n- Add support for full group message history export.\n- Allow export of a single chat message history in JSON format.\n\n2.1.9 beta (04.06.20)\n\n- Several crash fixes.\n\n2.1.8 beta (03.06.20)\n\n- Add support for full group message history export.\n- Allow export of a single chat message history in JSON format.\n\n2.1.7 (24.05.20)\n\n- Fix the Fcitx input method plugin.\n\n2.1.6 (14.05.20)\n\n- Bug fixes and other minor improvements.\n\n2.1.5 (13.05.20)\n\n- Disable the taskbar icon flash or the dock icon bounce in Settings > Notifications.\n- View messages containing long monospace texts in wide bubbles.\n- Bug fixes and other minor improvements.\n\n2.1.4 (08.05.20)\n\n- Improve bold font selection.\n\n2.1.3 (08.05.20)\n\n- Added support for new emoji.\n- Channels to which you can't post will no longer be suggested when forwarding.\n- Improved font selection and bold font support for CJK and Farsi.\n\n2.1.2 (05.05.20)\n\n- Fix polls and quizes results viewing.\n- Fix memory leak in web page previews with autoplayed videos.\n- Fix running on OS X 10.10.\n- Other minor bug fixes and improvements.\n\n2.1.1 (01.05.20)\n\n- Improve quiz explanation tooltip layout.\n- Fix possible crash in theme editor.\n- Other minor bug fixes and improvements.\n\n2.1 (24.04.20)\n\n- Access a catalog of over 20,000 stickers made by professional artists from the updated Sticker Panel by clicking the '+' icon.\n- Use sticker search to find the stickers you're looking for - or scroll from the latest packs all the way to the classics.\n- Add explanations that appear after users respond to a quiz question.\n- See how much time you have left to answer a question from @QuizBot with the new countdown animation.\n- Send a single :dart: emoji to see if you hit the bullseye.\n\n2.0.1 (31.03.20)\n\n- Switch between folders using Ctrl+1, ..., Ctrl+8.\n- Fix crash when a pinned in folder chat was added to archive.\n- Fix font issues in Linux version.\n\n2.0 (30.03.20)\n\n- Organize chats into Chat Folders if you have too many chats.\n- Create custom folders with flexible settings, or use default recommendations.\n- Pin an unlimited number of chats in each folder.\n- Switch between folders in the new side bar to easily access all of your chats.\n- Send :dice: to any chat to try your luck and get a random number from the animated dice.\n- Send :virus:, :face_with_thermometer:, :mask:, :face_with_head_bandage:, :sneeze:, :sick:, :soap: or :ambulance: to try out the new animated emoji.\n\n1.9.22 beta (27.03.20)\n\n- Organize chats into Chat Folders if you have too many chats.\n\n1.9.21 (16.03.20)\n\n- Mark new messages as read while scrolling down through them.\n- Improved spellchecking on Windows 7 and Linux.\n\n1.9.20 beta (14.03.20)\n\n- Fix crash in shared links search.\n- Fix blurred thumbnails in albums with video files.\n- Fix a possible crash in animated stickers rendering.\n\n1.9.19 beta (25.02.20)\n\n- Bug fixes and other minor improvements.\n\n1.9.18 beta (25.02.20)\n\n- Bug fixes and other minor improvements.\n\n1.9.17 beta (24.02.20)\n\n- Spell checker on Windows 7.\n- Bug fixes and other minor improvements.\n\n1.9.16 beta (23.02.20)\n\n- Bug fixes and other minor improvements.\n\n1.9.15 beta (21.02.20)\n\n- Mark new messages as read while scrolling down through them.\n- Bug fixes and other minor improvements.\n\n1.9.14 (17.02.20)\n\n- Bug fixes and other minor improvements.\n\n1.9.13 (12.02.20)\n\n- Bug fixes and other minor improvements.\n\n1.9.12 (11.02.20)\n\n- Switch to Picture-in-Picture mode to watch your video in a small window while doing something else.\n- Change playback speed in the '...' menu when watching videos.\n- Rotate photos and videos in the media viewer using the rotate button in the bottom right corner.\n\n1.9.11 beta (10.02.20)\n\n- Bug fixes and other minor improvements.\n\n1.9.10 beta (05.02.20)\n\n- Switch to the Picture-in-Picture mode to watch your video in a small window.\n- Change video playback speed in the playback controls '...' menu.\n- Rotate photos and videos in the media viewer using the rotate button in the bottom right corner.\n\n1.9.9 (28.01.20)\n\n- Bug fixes and other minor improvements.\n\n1.9.8 (24.01.20)\n\n- Bug fixes and other minor improvements.\n\n1.9.7 (23.01.20)\n\n- Create three new kinds of polls.\n- See who voted for what in polls with visible votes.\n- Vote for several options in polls that allow multiple answers.\n- Guess the correct answer in quiz-style polls.\n- Explore various ways of combining the different poll options.\n- Add polls from the '...' menu in any group or channel.\n- Use bots like @QuizBot to create quizzes with several questions and media attachments.\n- Schedule messages to be sent when your recipient comes online (only works if you know their online status).\n\n1.9.6 (22.01.20)\n\n- Bug fixes and other minor improvements.\n\n1.9.5 (21.01.20)\n\n- Bug fixes and other minor improvements.\n\n1.9.4 (17.01.20)\n\n- Bug fixes and other minor improvements.\n\n1.9.3 (31.12.19)\n\n- Videos in chats start playing automatically.\n- Resume playback from where you left off when watching long videos and listening to long audio tracks.\n- Control automatic playback for videos, GIFs and round video messages in Settings > Advanced > Automatic media download.\n- Enjoy system spell checker support on all modern systems.\n\n1.9.2 beta (29.12.19)\n\n- Videos in chats start playing automatically.\n- Resume playback from where you left off when watching long videos.\n- Control videos, GIFs and round video messages automatic playback in Settings > Advanced > Automatic media download.\n- Spell checker on Linux using Enchant.\n\n1.9.1 beta (06.11.19)\n\n- Bug fixes and other minor improvements.\n\n1.9 beta (05.11.19)\n\n- System spellchecker on Windows 8+ and macOS 10.12+.\n\n1.8.15 (07.10.19)\n\n- Bug fixes and other minor improvements.\n\n1.8.14 (03.10.19)\n\n- Bug fixes and other minor improvements.\n\n1.8.13 (03.10.19)\n\n- Bug fixes and other minor improvements.\n\n1.8.12 (02.10.19)\n\n- Bug fixes and other minor improvements.\n\n1.8.11 (01.10.19)\n\n- Bug fixes and other minor improvements.\n\n1.8.10 (30.09.19)\n\n- Bug fixes and other minor improvements.\n\n1.8.9 (27.09.19)\n\n- Bug fixes and other minor improvements.\n\n1.8.8 (10.09.19)\n\n- Create new themes based on your color and wallpaper choices.\n- Share your themes with other users via links.\n- Update your theme for all its users when you change something.\n\n1.8.7 beta (10.09.19)\n\n- Bug fixes and other minor improvements.\n\n1.8.6 beta (09.09.19)\n\n- Bug fixes and other minor improvements.\n\n1.8.5 beta (08.09.19)\n\n- Create new themes based on your color and wallpaper choices.\n- Share your themes with other users via links.\n- Update your theme for all its users when you change something.\n\n1.8.4 (06.09.19)\n\n- Bug fixes and other minor improvements.\n\n1.8.3 (05.09.19)\n\n- Right click the 'Send' button and select 'Schedule Message' to automatically send something at a specified time.\n- Schedule reminders for yourself in the 'Saved Messages' chat.\n- Get a notification when any of your scheduled messages are sent.\n- Customize your app's appearance by choosing accent colors for the 'Day', 'Night' and 'Tinted' themes.\n- Choose who can find you on Telegram when they add your number to their phone contacts.\n- Send a single :grin:, :anguished:, :rage:, :poo:, :cry: or :open_mouth: to check out what's new in the animated emoji department.\n\n1.8.2 (20.08.19)\n\n- Bug fixes and other minor improvements.\n\n1.8.1 (09.08.19)\n\n- Bug fixes and other minor improvements.\n\n1.8 (09.08.19)\n\n- Right click the Send button to send any message without sound - in case the recipient is sleeping.\n- Enable Slow Mode in Group Permissions to control how frequently members can post.\n- Set custom titles for group admins - like 'Founder', 'CFO' or 'Spam Fighter'.\n- Toggle looped playback for animated stickers in Chat Settings.\n- Send a single :heart:, :like:, :unamused:, :flushed: or :party: to add a mighty animated emoji to the chat.\n\n1.7.15 beta (18.07.19)\n\n- Bug fixes and other minor improvements.\n\n1.7.14 (07.07.19)\n\n- Bug fixes and other minor improvements.\n\n1.7.13 (06.07.19)\n\n- Send ultra-lightweight high-quality animated stickers to express emotion with motion.\n- Receive animated stickers instantly on any connection at just 20-30 KB per sticker.\n- Enjoy smooth animations at 60 frames per second.\n- Create new animated sets and upload them to @stickers for everybody to use.\n- Try out these sample stickers: t.me/addstickers/hotcherry\n- Use strikethrough and underline formatting.\n\n1.7.12 beta (05.07.19)\n\n- Bug fixes and other minor improvements.\n\n1.7.11 beta (04.07.19)\n\n- Use strikethrough and underline formatting.\n- Bug fixes and other minor improvements.\n\n1.7.10 (24.06.19)\n\n- Bug fixes and other minor improvements.\n\n1.7.9 (23.06.19)\n\n- You can now add any users to your contacts, even if their phone numbers are not visible.\n- Transfer ownership of group chats and channels by granting full rights to another admin. Useful when switching jobs or if you just want to retire as creator.\n- Hide archived chats to the main menu.\n- See who is online straight from the chat list.\n- Use the MacBook Pro TouchBar to apply formatting to selected text, insert emoji, and send stickers.\n\n1.7.8 beta (17.06.19)\n\n- Hide archived chats in the main menu.\n- See who is online straight from the chat list.\n- Apply formatting to selected text parts from the MacBook Pro TouchBar.\n\n1.7.7 (10.06.19)\n\n- Bug fixes and other minor improvements.\n\n1.7.6 beta (06.06.19)\n\n- Bug fixes and other minor improvements.\n\n1.7.5 beta (05.06.19)\n\n- Crash fix.\n\n1.7.4 beta (04.06.19)\n\n- Download video files while watching them using streaming.\n- Set EOL for Windows XP / Vista and OS X 10.6 to 10.9 at September 1.\n\n1.7.3 (01.06.19)\n\n- Bug fixes and other minor improvements.\n\n1.7.2 (31.05.19)\n\n- Choose who can see your phone number with granular precision in Privacy & Security settings.\n- Add group chats to \"Always/Never Share\" exceptions for any privacy setting. Permissions will update as users leave and join the groups.\n- Connect a discussion group to your channel, subscribers will see a \"Discuss\" button.\n- Integrate bots seamlessly with web services. For example, see t.me/DiscussThis/1\n- Use TouchBar on MacBooks Pro to control music playback and switch between pinned chats.\n\n1.7.1 beta (28.05.19)\n\n- Disable pinned messages notifications in Settings.\n\n1.7 (08.05.19)\n\nIntroducing Archived Chats:\n\n- Archive any chat from the right-click menu.\n- Chats with enabled notifications will pop out of the archive when a notification arrives.\n- Muted chats will stay in the archive.\n- Pin an unlimited number of chats in your archive.\n\n1.6.7 (13.04.19)\n\n- Replace media when editing messages with media content.\n- Jump quickly to the top of your chats list.\n- Get emoji suggestions for the first word you type in a message.\n- Help Telegram improve emoji suggestions in your language using this interface https://translations.telegram.org/en/emoji\n\n1.6.6 beta (11.04.19)\n\n- Bug fixes and other minor improvements.\n\n1.6.5 beta (06.04.19)\n\n- Bug fixes and other minor improvements.\n\n1.6.4 beta (05.04.19)\n\n- Replace media when editing messages with media content.\n- Jump quickly to the top of your chats list.\n- Get emoji suggestions for the first word you type in a message.\n- Help Telegram improve emoji suggestions in your language using this interface https://translations.telegram.org/en/emoji\n\n1.6.3 (25.03.19)\n\n- Bug fixes and other minor improvements.\n\n1.6.2 (24.03.19)\n\n- Delete any message on both ends in any private chat, anytime.\n- Control whether your forwarded messages link back to your account.\n- Control who may see your profile picture.\n- Enjoy the new streamlined group management screen.\n\n1.6.1 (20.03.19)\n\n- Bug fixes and other minor improvements.\n\n1.6 (18.03.19)\n\n- Play video files and listen to music without waiting for them to fully download.\n- Press CTRL+0 (CMD+0 on macOS) to jump to your Saved Messages.\n\n1.5.18 beta (15.03.19)\n\n- Bug fixes and other minor improvements.\n\n1.5.17 beta (13.03.19)\n\n- Bug fixes and other minor improvements.\n\n1.5.16 beta (12.03.19)\n\n- Play video files and listen to received music without waiting for them to download.\n- Press CTRL+0 (CMD+0 on macOS) to jump to your Saved Messages.\n\n1.5.15 (12.02.19)\n\n- Crash fix.\n\n1.5.14 (12.02.19)\n\n- Crash fix.\n\n1.5.13 (12.02.19)\n\n- Bug fixes and other minor improvements.\n\n1.5.12 (09.02.19)\n\n- Apply blur effects to backgrounds.\n- Use the backgrounds you set in Telegram Desktop in all other Telegram apps.\n\n1.5.11 (01.02.19)\n\n- Bug fixes and other minor improvements.\n\n1.5.10 (01.02.19)\n\n- Bug fixes and other minor improvements.\n\n1.5.9 (31.01.19)\n\n- Bug fixes and other minor improvements.\n\n1.5.8 (21.01.19)\n\n- Global permissions for groups. Restrict all members in any group from posting certain types of content.\n- Unified group settings. Make groups public, set admins with granular permissions and toggle persistent history in just a few clicks in any group.\n- Choose the emoji set you would like to use in Chat Settings.\n- Choose input and output devices for Telegram Calls in Settings > Advanced > Call Settings.\n- Support for automatically downloading files and music.\n\n1.5.7 beta (11.01.19)\n\n- Choose the emoji set you would like to use in Settings > Chat Settings.\n- Choose input and output devices for Telegram Calls in Settings > Adavanced > Call Settings.\n\n1.5.6 beta (27.12.18)\n\n- Fix crash on macOS.\n\n1.5.5 beta (27.12.18)\n\n- Support for auto-download of files and music.\n- Improved auto-download settings.\n- Bug fixes and other minor improvements.\n\n1.5.4 (24.12.18)\n\n- Bug fixes and other minor improvements.\n\n1.5.3 (23.12.18)\n\n- Create polls in groups and channels - right from the chat menu.\n\n1.5.2 (13.12.18)\n\n- Bug fixes and other minor improvements.\n\n1.5.1 (11.12.18)\n\n- Bug fixes and other minor improvements.\n\n1.5 (10.12.18)\n\n- Support for custom languages. Crowdsource a cloud-based language pack for Telegram in any language using our Translations platform - then apply it in real time.\n- Interface scaling for large screens, up to 300% (up to 150% for macOS retina screens).\n- 'Count unread messages' setting for the Badge counter in Settings > Notifications. Disable to show number of unread chats.\n- Video messages displayed in shared media (under voice messages).\n- Updated emoji. Farewell to question marks!\n\nAlso in this update:\n- Listen to voice and video messages in 2X mode if you're in a hurry.\n- Add a comment when sharing posts from channels.\n- View all photos and videos in Twitter and Instagram link previews.\n- Add emoji to media captions.\n\n1.4.8 beta (04.12.18)\n\n- Add emoji to media captions.\n- Switch off the 'Count unread messages' option in Settings > Notifications if you want to see the unread chats count in the badge instead.\n\n1.4.7 beta (10.11.18)\n\n- Crash fix.\n\n1.4.6 beta (09.11.18)\n\n- Bug fixes and other minor improvements.\n\n1.4.5 beta (08.11.18)\n\n- Listen to voice and video messages in 2X mode if you're in a hurry.\n- Find video messages in the shared voice messages section.\n- Add a comment when you share posts from channels.\n- View all photos and videos in Twitter and Instagram link previews.\n- Bug fixes and other minor improvements.\n\n1.4.4 beta (16.10.18)\n\n- Interface scaling for large screens, up to 300% (up to 150% for macOS retina screens).\n- Updated emoji.\n\n1.4.3 (13.10.18)\n\n- Bug fixes and other minor improvements.\n\n1.4.2 (10.10.18)\n\n- Crash fix.\n\n1.4.1 (09.10.18)\n\n- Reduce crashes due to the out of memory exceptions.\n- Load map previews through mtproto.\n- Bug fixes and other minor improvements.\n\n1.4 (28.09.18)\n\n- Fully redesigned Settings section.\n- New theme selector in Chat Settings.\n- New local storage settings: Control how much disk space is used by the cache and for how long the cached files are stored.\n- Improved local caching for images and GIF animations.\n- New settings: Peer-to-Peer settings for calls, disable animations for low performance computers.\n- Various other improvements.\n\n1.3.17 beta (26.09.18)\n\n- Fully redisigned Settings section.\n- New theme selector in Chat Settings.\n- New settings: Peer-to-Peer settings for calls, disable animations for low performance computers.\n- Various other improvements.\n\n1.3.16 alpha (04.09.18)\n\n- Update libtgvoip, fix crash in calls.\n- Fix crash in local cache database.\n- Clear old local cache asynchronously.\n\n1.3.15 alpha (01.09.18)\n\n- Improved local caching for images and GIF animations.\n- Control how much disk space is used by the cache and for how long the cached files are stored.\n\n1.3.14 (27.08.18)\n\n- Fix a crash in calls.\n\n1.3.13 (25.08.18)\n\n- Export data from individual chats using the '...' menu.\n- Added a new night theme.\n- You can now assign custom themes as night and day themes to quickly switch between them.\n- Support for Telegram Passport 1.1 and improved password hashing algorithm to better protect Telegram Passport data.\n\n1.3.12 alpha (04.08.18)\n\n- Bug fixes and other minor improvements.\n\n1.3.11 alpha (01.08.18)\n\n- Added a new night theme.\n- You can now assign custom themes as night and day themes to quickly switch between them.\n\n1.3.10 (13.07.18)\n\n- Bug fixes and other minor improvements.\n\n1.3.9 (28.06.18)\n\n- Mark chats in the chat list as Read or Unread.\n- Improved censorship circumvention.\n\n1.3.8 (24.06.18)\n\n- Bug fixes and other minor improvements.\n\n1.3.7 (11.06.18)\n\n- Push fixes to stable version.\n\n1.3.6 alpha (11.06.18)\n\n- Bug fixes and other minor improvements.\n\n1.3.5 alpha (08.06.18)\n\n- Bug fixes and other minor improvements.\n\n1.3.4 alpha (07.06.18)\n\n- Bug fixes and other minor improvements.\n\n1.3.3 alpha (07.06.18)\n\n- Bug fixes and other minor improvements.\n\n1.3.2 alpha (07.06.18)\n\n- Bug fixes and other minor improvements.\n\n1.3.1 alpha (04.06.18)\n\n- Bug fixes and other minor improvements.\n\n1.3.0 (01.06.18)\n\n- Improved censorship circumvention.\n- Improved stability when working through proxy servers.\n- Save several proxy servers to quickly switch between them in Settings.\n- Use proxy for calls.\n- Emoji and text replacement now happens immediately after typing (instead of after sending) and can be rolled back using Backspace or CTRL/CMD + Z. Replacement no longer happens when pasting text.\n\nAdded formatting shortcuts. Select text and use:\n- CTRL/CMD + B/I for bold and italic\n- CTRL/CMD + K to create or edit a custom link\n- CTRL/CMD + SHIFT + M for monospace font\n- CTRL/CMD + SHIFT + N to clear formatting\n\n1.2.25 alpha (31.05.18)\n\n- Apply markdown formatting (```, `, **, __) only when sending the message.\n- Display connection quality bars in calls.\n- Telegram Desktop can update itself through MTProto.\n- Bug fixes and other minor improvements.\n\n1.2.24 alpha (26.05.18)\n\n- Add links with custom text from context menu or by Ctrl/Cmd + K keyboard shortcut.\n\n1.2.23 alpha (25.05.18)\n\n- Apply formatting from input field context menu.\n- Apply formatting by hotkeys.\n- Bug fixes and other minor improvements.\n\n1.2.22 alpha (24.05.18)\n\n- Use markdown in media captions (**bold**, __italic__, `tag` and ```code```).\n- Use emoji replacement in media captions, group and channel titles and descriptions (:like: etc.)\n- Markdown replacement now happens immediately after typing (instead of after sending) and can be rolled back using Backspace or Ctrl/Cmd + Z. Replacement no longer happens when pasting text.\n\n1.2.21 alpha (18.05.18)\n\n- Support domain names in mtproto proxy.\n- Bug fixes and other minor improvements.\n\n1.2.20 alpha (13.05.18)\n\n- Emoji and text replacements are done while you type the message.\n- Revert emoji and text replacements by pressing backspace.\n- Disable emoji replacements or suggestions in Settings.\n- Some critical bug fixes.\n\n1.2.19 alpha (08.05.18)\n\n- Enable proxy for calls in Settings.\n- Bug fixes and other minor improvements.\n\n1.2.18 alpha (05.05.18)\n\n- Improve working through proxy servers.\n- Bug fixes and other minor improvements.\n\n1.2.17 (08.04.18)\n\n- Bug fixes and other minor improvements.\n\n1.2.16 (07.04.18)\n\n- Bug fixes and other minor improvements.\n\n1.2.15 (26.03.18)\n\n- Bug fixes and other minor improvements.\n\n1.2.14 (21.03.18)\n\n- Discover new stickers. Type one emoji to see suggestions from popular sticker sets. Suggestions from your installed sticker sets will come first.\n- Search for Stickers. Click on the new search icon to access your sticker sets or find new ones.\n- Quick Reply. Double click near a message for a quick reply.\n\n1.2.13 alpha (21.03.18)\n\n- Bug fixes and other minor improvements.\n\n1.2.12 alpha (12.03.18)\n\n- Bug fixes and other minor improvements.\n\n1.2.11 alpha (10.03.18)\n\n- Bug fixes and other minor improvements.\n\n1.2.10 alpha (09.03.18)\n\n- Bug fixes and other minor improvements.\n\n1.2.9 alpha (07.03.18)\n\n- Quick Reply. Double click near a message for a quick reply.\n- Search for Stickers. Click on the new search icon to access your sticker sets or find new ones.\n\n1.2.8 alpha (03.01.18)\n\n- Bug fixes and other minor improvements.\n\n1.2.7 alpha (31.12.17)\n\n- Use fast reply button in group chats.\n- Select a message you want to reply to by pressing Ctrl+Up and Ctrl+Down.\n\n1.2.6 (30.12.17)\n\n- Grouped Photos. Group media into an album when sharing multiple photos and videos. Choose the exact order of media you send.\n\n1.2.5 alpha (29.12.17)\n\n- When viewing a photo from an album, you'll see other pictures from the same group as thumbnails in the lower part of the screen.\n- When composing an album paste additional media from the clipboard.\n- Bug fixes and other minor improvements.\n\n1.2.4 alpha (26.12.17)\n\n- Group media into an album when sharing multiple photos and videos.\n- Bug fixes and other minor improvements.\n\n1.2.3 alpha (17.12.17)\n\n- Several crash fixes.\n\n1.2.2 alpha (16.12.17)\n\n- Grouped photos and videos are displayed as albums.\n\n1.2.1 (12.12.17)\n\n- Bug fixes and other minor improvements.\n\n1.2.0 (10.12.17)\n\n- Radically improved navigation. New side panel on the right with quick access to shared media and group members.\n- Saved Messages. Bookmark messages by forwarding them to \"Saved Messages\". Access them from the Chats list or from the side menu.\n- Pinned Messages. If you are a channel admin, pin messages to focus your subscribers' attention on important announcements.\n- Easily recognize messages from group admins by the new 'admin' badge.\n- Also supported clearing history in supergroups and added a host of minor improvements.\n\n1.1.29 alpha (09.12.17)\n\n- Bug fixes and other minor improvements.\n\n1.1.28 alpha (09.12.17)\n\n- Bug fixes and other minor improvements.\n\n1.1.27 alpha (06.12.17)\n\n- Bookmark messages by forwarding them to \"Saved Messages\". Access them from the Chats list or from the side menu.\n\n1.1.26 alpha (02.12.17)\n\n- Admin badges in supergroup messages.\n- Fix crashing on launch in OS X 10.6.\n- Bug fixes and other minor improvements.\n\n1.1.25 alpha (30.11.17)\n\n- Bug fixes and other minor improvements.\n\n1.1.24 alpha (29.11.17)\n\n- Radically improved navigation. New side panel on the right with quick access to shared media and group members.\n- Pinned Messages. If you are a channel admin, pin messages to focus your subscribers' attention on important announcements.\n- Also supported clearing history in supergroups and added a host of minor improvements.\n\n1.1.23 (05.09.17)\n\n- See the message author photo and name while searching specific chat messages.\n- Fix \"Send To\" menu action on Windows.\n\n1.1.22 (04.09.17)\n\n- Bug fixes and other minor improvements.\n\n1.1.21 (03.09.17)\n\n- Bug fixes and other minor improvements.\n\n1.1.20 (03.09.17)\n\n- Groups with unread mentions and replies are now marked with an '@' badge in the chats list.\n- Navigate new mentions and replies in a group using the new '@' button.\n- Mark your stickers as “favorite” to quickly access them from the redesigned sticker panel.\n- Add an official sticker set for your group which all members will be able to use while chatting in your group (100+ member groups only).\n\n1.1.19 (01.08.17)\n\n- Search by messages of specific group members.\n- Bug fixes and other minor improvements.\n\n1.1.18 (27.07.17)\n\n- Bug fixes and other minor improvements.\n\n1.1.17 (26.07.17)\n\n- Bug fixes and other minor improvements.\n\n1.1.16 (26.07.17)\n\n- Autocompletion for emoji. Start typing :e to get suggestions.\n- Fixed a bug with forwarding messages.\n\n1.1.15 (23.07.17)\n\n- Send **bold** and __italic__ text in your messages.\n- Get a share link for posts in public supergroups.\n- Quickly share posts from channels and media messages from bots.\n- Search large supergroup members by name.\n- Search channel members by name for admins.\n- Use search in the service actions log.\n- Ban supergroup members via the right click menu in the service actions log.\n\n1.1.14 alpha (19.07.17)\n\n- Bug fixes and other minor improvements.\n\n1.1.13 alpha (14.07.17)\n\n- Various bug fixes.\n\n1.1.12 alpha (11.07.17)\n\n- Click on forwarded messages bar to change the recipient chat in case you chose a wrong one first.\n- Quickly share posts from channels and media messages from bots.\n- Search in large supergroup members by name.\n- Search in channel members by name if you're a channel admin.\n- Copy links to messages in public supergroups.\n\n1.1.11 alpha (06.07.17)\n\n- Send **bold** and __italic__ text in your messages (in addition to already supported `monospace` and ```multiline monospace```).\n- Search in channel and supergroup admin event log.\n- Ban members from right click menu in supergroup admin event log.\n\n1.1.10 (04.07.17)\n\n- Add event log filter for channel or supergroup event log.\n- Fix search by username in privacy exceptions editor.\n- Fix adding admins in channels.\n\n1.1.9 (30.06.17)\n\n- Supergroups can now have up to 10.000 members.\n- Appoint supergroup admins with granular rights. Choose who can add users, manage messages, block members, edit group info & username, add new admins, etc.\n- Restrict and ban supergroup members with granular precision. Read-only bans, GIF & sticker bans, media bans, temporary bans and restrictions.\n- Check the new event log to see all service actions taken by members and admins of a channel or supergroup in the last 48 hours.\n- Toggle night mode in the main menu.\n\n1.1.8 alpha (30.06.17)\n\n- Toggle night mode in the main menu.\n\n1.1.7 (30.05.17)\n\n- Improved video messages: radial playback progress, Picture-in-Picture support, duration countdown.\n- Voice and video messages now automatically play one after another.\n\n1.1.6 alpha (26.05.17)\n\n- Bug fixes and other minor improvements.\n\n1.1.5 alpha (26.05.17)\n\n- Bug fixes and other minor improvements.\n\n1.1.4 (24.05.17)\n\n- Fix a crash in animated history scrolling.\n- Fix a bug with pinned chat to supergroup upgrade.\n\n1.1.3 (24.05.17)\n\n- Improved video messages playback.\n- Video and audio messages play one after another.\n\n1.1.2 (17.05.17)\n\n- Emoji icon tooltip about hiding the sidebar.\n- Fix possible calls deadlock in Linux.\n- Preserve Emoji/Stickers/GIFs panel state between activations.\n\n1.1.1 alpha (17.05.17)\n\n- Emoji icon tooltip about hiding the sidebar.\n- Fix possible calls deadlock in Linux.\n\n1.1.0 (14.05.17)\n\n- Telegram Calls are now available on desktops: secure, crystal-clear, constantly improved by artificial intelligence.\n- The new Emoji, Stickers, and Saved GIFs panel becomes a separate space on the right when Telegram is running in a wide enough window.\n- Manage blocked users list in your supergroups.\n- Chat admins can delete messages by other members.\n\n1.0.38 alpha (13.05.17)\n\n- Fix crashes in Windows XP.\n- Fix calls in Linux without SSE4.1 support.\n- PulseAudio support in calls.\n- Bug fixes and other minor improvements.\n\n1.0.37 alpha (10.05.17)\n\n- Multiple crash fixes.\n\n1.0.36 alpha (09.05.17)\n\n- Telegram Calls are now available on desktops: secure, crystal-clear, constantly improved by artificial intelligence.\n- Bug fixes and other minor improvements.\n\n1.0.35 alpha (30.04.17)\n\n- Chat admins can delete other participants' messages.\n- Bug fixes and other minor improvements.\n\n1.0.34 alpha (21.04.17)\n\n- Bug fixes and other minor improvements.\n\n1.0.33 alpha (16.04.17)\n\n- Bug fixes and other minor improvements.\n\n1.0.32 alpha (12.04.17)\n\n- Testing a build with the latest API layer.\n- Bug fixes and other minor improvements.\n\n1.0.31 alpha (11.04.17)\n\n- Bug fixes and other minor improvements.\n\n1.0.30 alpha (11.04.17)\n\n- The new Emoji, Stickers, and Saved GIFs panel becomes a separate space on the right when Telegram is running in a wide enough window.\n- Manage blocked users list in your supergroups.\n\n1.0.29 (05.04.17)\n\n- Improved Emoji, Stickers, and Saved GIFs panel.\n- Bug fixes and other minor improvements.\n\n1.0.28 alpha (03.04.17)\n\n- Better Emoji & Stickers & Saved GIFs panel.\n- Linux: Tray icon should work better in GNOME based Desktop Environments.\n- Bug fixes and other minor improvements.\n\n1.0.27 (31.03.17)\n\n- Fix launch in Ubuntu 17.04.\n- Update API scheme.\n\n1.0.26 (30.03.17)\n\n— Send MP4/MOV files as videos that will play right inside Telegram.\n— Click on the date in any chat to quickly jump to messages from a specific day.\n— Change your phone number in Settings.\n— Edit who can see your last seen time and who can add you to groups in Settings.\n— Edit your list of blocked users in Settings.\n— App now respects the \"Do Not Disturb\" setting for macOS notifications.\n— Bug fixes and other minor improvements.\n\n1.0.25 alpha (21.03.17)\n\n- Edit your account phone number in Settings.\n\n1.0.24 alpha (19.03.17)\n\n- Added Last Seen and Group Invite privacy settings.\n\n1.0.23 alpha (15.03.17)\n\n- Edit list of blocked users in Settings.\n\n1.0.22 alpha (13.03.17)\n\n- Bug fixes and other minor improvements.\n\n1.0.21 alpha (11.03.17)\n\n- Send MP4/MOV files as videos that will play right inside Telegram.\n- Bug fixes and other minor improvements.\n\n1.0.20 alpha (08.03.17)\n\n- Bug fixes and other minor improvements.\n\n1.0.19 alpha (08.03.17)\n\n- Go to date. Click on the date in a chat to jump to a specific day.\n- Respect macOS \"Do not disturb\" settings for sound notifications.\n- Bug fixes and other minor improvements.\n\n1.0.18 alpha (01.03.17)\n\n- Bug fixes and other minor improvements.\n\n1.0.17 alpha (28.02.17)\n\n- Bug fixes and other minor improvements.\n\n1.0.16 alpha (27.02.17)\n\n- Bug fixes and other minor improvements.\n\n1.0.15 alpha (27.02.17)\n\n- Wrong supergroup members display fix.\n- RTL text layout fix.\n- Linux GTK file choose dialog should show image previews.\n- Bug fixes and other minor improvements.\n\n1.0.14 (20.02.17)\n\n- Bug fixes and other minor improvements.\n\n1.0.13 (20.02.17)\n\n- Bug fixes and other minor improvements.\n\n1.0.12 (19.02.17)\n\n- Support for more emoji.\n- Click and drag on waveform to play audio from a chosen moment.\n- Added Theme editor to Settings.\n- Bug fixes and other minor improvements.\n\n1.0.11 alpha (17.02.17)\n\n- Bug fixes and other minor improvements.\n\n1.0.10 alpha (17.02.17)\n\n- Support for more emoji.\n- Bug fixes and other minor improvements.\n\n1.0.9 alpha (11.02.17)\n\n- Bug fixes and other minor improvements.\n\n1.0.8 alpha (11.02.17)\n\n- Click and drag on waveform to play audio from a chosen moment.\n\n1.0.7 alpha (07.02.17)\n\n- Added Theme editor to Settings.\n\n1.0.6 (01.02.17)\n\n- Bug fixes and other minor improvements.\n\n1.0.5 (31.01.17)\n\n- Click and drag to reorder pinned chats.\n- Bug fixes and other minor improvements.\n\n1.0.4 alpha (30.01.17)\n\n- Click and drag to reorder pinned chats.\n\n1.0.3 alpha (27.01.17)\n\n- Audio device is opened only when some sound is played.\n- On Windows Vista and later audio device should switch after the system default changes.\n\n1.0.2 (19.01.17)\n\n- New option to minimize the chat list into a column of profile pictures.  Resize the list by clicking and dragging.\n- Fixed drag-n-drop images from Firefox on Windows.\n- Bug fixes and other minor improvements.\n\n1.0.1 alpha (16.01.17)\n\n- Resize chats list with mouse press-and-drag.\n- Drag-n-drop images from Firefox fixed in Windows.\n- Bug fixes and other minor improvements.\n\n1.0.0 (11.01.17)\n\n- Fabulous new material-style design and animations.\n- Support for custom themes! Check out some themes here: @TelegramThemes.\n- Convenient tools for building your own themes coming soon.\n- Delete messages for everyone. When you delete your messages in groups and one-on-one chats, you can now choose to delete them for everyone in the chat, not just yourself. This works only for recently sent messages (same as editing).\n- Pin important chats to the top of the list so that you never miss a new message (right click on a chat, then choose 'Pin to top').\n- Groups in common. A new option in your contacts' profiles that shows a list of all groups you share with that person.\n\n0.10.27 alpha (11.01.17)\n\n- Appoint admins in your supergroups from members list context menu.\n- Bug fixes and other minor improvements.\n\n0.10.26 alpha (07.01.17)\n\n- You can use t.me instead of telegram.me.\n- OpenAL updated to the latest version.\n- Bug fixes and other minor improvements.\n\n0.10.25 alpha (05.01.17)\n\n- Bug fixes and other minor improvements.\n\n0.10.24 alpha (05.01.17)\n\n- Bug fixes and other minor improvements.\n\n0.10.23 alpha (01.01.17)\n\n- Bug fixes and other minor improvements.\n\n0.10.22 alpha (31.12.16)\n\n- Bug fixes and other minor improvements.\n\n0.10.21 alpha (30.12.16)\n\n- Fabulous new material-style design and animations.\n- This version already supports custom themes that are coming soon.\n- Pin important chats to the top of the list so that you never miss a new message (right click on a chat, then choose 'Pin to top').\n- Groups in common. A new option in your contacts' profiles that shows a list of all groups you share with that person.\n\n0.10.20 (18.12.16)\n\n- Bug fixes and other minor improvements.\n\n0.10.19 (25.10.16)\n\n- Bug fixes and other minor improvements.\n\n0.10.18 (24.10.16)\n\n- New cute design for adding members to your groups.\n\n0.10.17 alpha (23.10.16)\n\n- New cute control for adding members to your groups.\n\n0.10.16 (19.10.16)\n\n- New audio player design.\n- Quick reply from notifications.\n- Hide all notifications button added.\n- Change notifications location and maximum count.\n- Respecting quiet hours for the notifications.\n- You can enable native notifications in Settings.\n\n0.10.15 alpha (18.10.16)\n\n- Bug fixes and other minor improvements.\n\n0.10.14 alpha (18.10.16)\n\n- New audio player design.\n- Moved to Qt library version 5.6.2.\n- Windows 10: Respecting quite hours for the notifications.\n\n0.10.13 alpha (07.10.16)\n\n- Bug fixes and other minor improvements.\n\n0.10.12 alpha (07.10.16)\n\n- Quick reply from notifications.\n- Hide all notifications button added.\n- Change notifications location and maximum count.\n- Linux: You can enable native notifications in Settings.\n\n0.10.11 (03.10.16)\n\n- Bug fixes and other minor improvements.\n\n0.10.10 (03.10.16)\n\n- Bug fixes and other minor improvements.\n\n0.10.9 (03.10.16)\n\n- Bug fixes and other minor improvements.\n\n0.10.8 (23.09.16)\n\n- Bug fixes and other minor improvements.\n\n0.10.7 (20.09.16)\n\n- Bug fixes and other minor improvements.\n\n0.10.6 (13.09.16)\n\n- Bug fixes and other minor improvements.\n\n0.10.5 (12.09.16)\n\n- New cute design for the Settings page.\n- Bug fixes and other minor improvements.\n\n0.10.4 alpha (03.09.16)\n\n- Bug fixes and other minor improvements.\n\n0.10.3 alpha (02.09.16)\n\n- New cute design for the Settings page.\n\n0.10.2 alpha (14.08.16)\n\n- Bug fixes and other minor improvements.\n\n0.10.1 (05.08.16)\n\n- Bug fixes and other minor improvements.\n\n0.10.0 (03.08.16)\n\n- Trending stickers. Check out and install noteworthy sticker packs from the new tab in Settings.\n- Archived stickers. Unused stickers are now archived automatically when you go over the 200 limit.\n- Group previews. Preview groups before joining them via invite link – see who else is in the group before joining.\n- New internal video player.\n- Improved design for chats.\n\n0.9.60 alpha (24.07.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.59 alpha (23.07.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.58 alpha (22.07.16)\n\n- Alpha version of an embedded video player.\n\n0.9.57 alpha (07.07.16)\n\n- Design improvements.\n- Linux: trying to use GTK file chooser when it is available.\n\n0.9.56 stable (26.06.16)\n\n- Fixed photo viewer to handle screen resolution change correctly.\n- Fixed forwarding photos via drag-n-drop.\n- Various design improvements and other bug fixes.\n\n0.9.55 alpha (24.06.16)\n\n- Main window position and size are saved between the launches in Windows.\n- Dock and top bar hiding fixed in OS X.\n- Various design improvements and other bug fixes.\n\n0.9.54 alpha (22.06.16)\n\n- Photo viewer handles screen resolution change.\n- Forward photo by drag-n-drop fixed.\n- Some design improvements and bug fixes.\n\n0.9.53 alpha (21.06.16)\n\n- Put your cursor over the members count in a group chat to see the members list.\n- Bug fixes and other minor improvements.\n\n0.9.52 alpha (17.06.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.51 stable (15.06.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.50 stable (14.06.16)\n\n- Introducing Drafts: Seamless syncing for unsent messages on all your devices. Drafts are now visible in your chats list.\n- Completely redesigned group and user profiles.\n- Unread messages counter on the 'Scroll to bottom' button.\n\n0.9.49 stable (16.05.16)\n\n- Edit your messages everywhere within 2 days after posting (press the up arrow button to edit your last message).\n- Mention people in groups by typing @ and selecting them from the list — even if they don't have a username.\n\n0.9.48 stable (10.05.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.47 alpha (02.05.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.46 alpha (27.04.16)\n\n- Alpha versions instead of dev.\n- Bug fixes and other minor improvements.\n\n0.9.45 dev (22.04.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.44 stable (20.04.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.43 dev (14.04.16)\n\n- Select and copy text in photo / video captions and web page previews.\n- Media player shortcuts are enabled only when player is opened.\n\n0.9.42 stable (12.04.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.41 stable (11.04.16)\n\n- Introducing Bot API 2.0, the biggest update to our bot platform since June 2015.\n- Bots can now update existing messages on the fly as you interact with them.\n- New Inline keyboards with callback, 'open URL' or 'switch to inline mode' buttons help create seamless interfaces.\n- Inline bots can now send all attachments supported by Telegram (videos, music, stickers, locations, etc.).\n\n0.9.40 stable (05.04.16)\n\n- Visual improvements.\n\n0.9.39 dev (01.04.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.38 dev (01.04.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.37 dev (31.03.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.36 dev (26.03.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.35 dev (25.03.16)\n\n- Design improvements.\n\n0.9.34 dev (17.03.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.33 stable (16.03.16)\n\n- Linux version critical bug fixed.\n\n0.9.32 stable (14.03.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.31 stable (14.03.16)\n\n- Supergroups can now have 5,000 members (up from 1,000).\n- Groups of any size may be converted to supergroups.\n- Make your supergroup public by setting up a public link – anyone will be able to view the chat and join it.\n- Pin messages to keep important updates visible and notify all members.\n    'Select messages to delete, report as spam, block users, or remove all messages from a user'\n\n0.9.30 dev (03.03.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.29 dev (01.03.16)\n\n- Ctrl+W or Ctrl+F4 for close window (Cmd in OS X).\n- Ctrl+L to lock Telegram if you use a local passcode (Cmd in OS X).\n\n0.9.28 stable (26.02.16)\n\n- Edit your messages in channels and supergroups.\n- Share links to specific posts in channels via the post context menu.\n- Add admin signatures to messages in channels.\n- Send silent messages in channels that will not notify members, useful for non-urgent or late night posting.\n\n0.9.27 dev (25.02.16)\n\n- Edit your messages in channels and supergroups.\n- Share links to specific posts in channels via the post context menu.\n- Add admin signatures to messages in channels.\n- Send silent messages in channels that will not notify members, useful for non-urgent or late night posting.\n\n0.9.26 stable (22.02.16)\n\n- Adaptive layout for wide screens switch added to Settings.\n- Linux version crash fix.\n\n0.9.25 dev (21.02.16)\n\n- Adaptive layout for wide screens switch added to Settings.\n- Linux version crash fix.\n\n0.9.24 stable (16.02.16)\n\n- New waveform visualizations for voice messages.\n- Sticker suggestions when you type an emoji.\n\n0.9.23 dev (15.02.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.22 dev (14.02.16)\n\n- Voice messages waveform visualizations.\n\n0.9.21 dev (09.02.16)\n\n- Bug fixes and other minor improvements.\n\n0.9.20 dev (08.02.16)\n\n- Testing new crash reporting system.\n- Conversation history is centered in wide windows.\n- New cute link and timestamp tooltips design.\n- Ctrl+W or Ctrl+F4 closes Telegram window.\n\n0.9.19 dev (09.01.16)\n\n- Choose an emoticon and see the suggested stickers.\n\n0.9.18 stable (05.01.16)\n\n- Bug fixes and minor improvements.\n\n0.9.17 stable (04.01.16)\n\n- Crash fixed.\n\n0.9.16 stable (04.01.16)\n\n- GIFs autoplay and automatic download.\n- Inline bots support.\n- Automatic download settings added.\n- All media redesigned.\n\n0.9.15 stable (09.12.15)\n\n- Sticker management: manually rearrange your sticker packs, pack order is now synced across all your devices.\n- Click and hold on a sticker to preview it before sending.\n- New context menu for chats in chats list.\n- Support for all existing emoji.\n\n0.9.14 dev (09.12.15)\n\n- Sticker management: manually rearrange your sticker packs, pack order is now synced across all your devices.\n- Click and hold on a sticker to preview it before sending.\n- New context menu for chats in chats list.\n- Support for all existing emoji.\n\n0.9.13 stable (25.11.15)\n\n- Groups can now have multiple administrators with the ability to edit the name and logo, and add and remove members.\n- Groups that have reached their capacity of 200 users can be upgraded to supergroups of up to 1,000 members.\n\n0.9.12 dev (22.11.15)\n\n- Bug fixes and other minor improvements.\n\n0.9.11 dev (22.11.15)\n\n- Groups can now have multiple administrators with the ability to edit the name and logo, and add and remove members.\n- Groups that have reached their capacity of 200 users can be upgraded to supergroups of up to 1,000 members.\n\n0.9.10 stable (04.11.15)\n\n- New shared audio files section.\n- New design for popup menus.\n- Add captions to any photos you send.\n- Surround text with `single backticks` for inline monowidth code and ```triple backticks``` for blocks of code.\n- The Tilde symbol is now displayed correctly.\n\n0.9.9 dev (02.11.15)\n\n- New popup menus in text input fields.\n- Error is displayed when trying to paste or drop a folder instead of a file.\n- Some bugfixes and improvements.\n\n0.9.8 dev (28.10.15)\n\n- Tilde symbol fixed in message input field.\n- Add a caption to any photo you send.\n- Bad sound quality bug fixed.\n- Some bugfixes and improvements.\n\n0.9.7 dev (23.10.15)\n\n- Tilde symbol display fixed.\n- Bold and Italic text display from bots supported.\n- Send code without text and emoji replaces using `such syntax` for inline parts and ```such syntax``` for blocks of code.\n- Some bugfixes and improvements.\n\n0.9.6 stable (14.10.15)\n\n- Bug fixes and other minor improvements.\n  ),\n ),\n0.9.5 stable (14.10.15)\n\n- Improved speed.\n- First steps towards a new design.\n  ),\n ),\n0.9.4 dev (12.10.15)\n\n- New design for all modal windows.\n- Toggle notifications from tray menu.\n\n0.9.3 dev (01.10.15)\n\n- Dialogs and emoji render made much faster.\n\n0.9.2 stable (25.09.15)\n\n- Bug fixes and other minor improvements.\n\n0.9.1 stable (24.09.15)\n\n- Bug fixes and other minor improvements.\n\n0.9 stable (24.09.15)\n\n- Introducing Channels — a great new way to broadcast your messages to unlimited audiences.\n- Other fixes and improvements.\n\n0.8.59 dev (23.09.15)\n\n- Channels members and admins management added.\n\n0.8.58 dev (22.09.15)\n\n- Limited Channels support added.\n\n0.8.57 stable (13.09.15)\n\n- Bug fixes and other minor improvements.\n\n0.8.56 stable (10.09.15)\n\n- Spam report buttons in new chats.\n- Other fixes and improvements.\n- OS X 10.8 notifications fixed.\n\n0.8.55 stable (02.09.15)\n\n- Include muted chats in unread count in Settings.\n- Shared links overview and search in shared media.\n- Preview when sending links to GIF animations and PDF files.\n\n0.8.54 dev (31.08.15)\n\n- Preview when sending links to GIF animations and PDF files.\n- Full date and time shown when mouse over message timestamp.\n\n0.8.53 dev (28.08.15)\n\n- Include muted chats in unread count in Settings.\n- Shared links overview and search in shared media.\n\n0.8.52 stable (19.08.15)\n\n- Bug fixes and other minor improvements.\n\n0.8.51 stable (17.08.15)\n\n- Block users from their profile page.\n- Added support for Windows toast notifications.\n- Fixed input methods on Linux (Fcitx and IBus).\n\n0.8.50 dev (14.08.15)\n\n- Bug fixes in Windows notifications.\n- Fixed input methods on Linux (Fcitx and IBus).\n\n0.8.49 dev (12.08.15)\n\n- Block users from user profile.\n- Ask support team a question from Settings.\n- Windows toast notifications support added.\n\n0.8.48 stable (08.08.15)\n\n- Search for messages in conversation.\n- Clear messages history in groups.\n- Contacts without messages are hidden from the conversations list.\n\n0.8.47 dev (07.08.15)\n\n- Search for messages in conversation.\n- Clear messages history in groups.\n- Contacts without messages are hidden from the conversations list.\n\n0.8.46 stable (05.08.15)\n\n- Crash fix on sending .webp files.\n\n0.8.45 stable (03.08.15)\n\n- Bug fixes and other minor improvements.\n\n0.8.44 dev (01.08.15)\n\n- Sending media and recording audio status display.\n\n0.8.43 stable (31.07.15)\n\n- Bug fixes and other minor improvements.\n\n0.8.42 dev (31.07.15)\n\n- Dev version will now get updated to stable as well.\n\n0.8.41 dev (21.07.15)\n\n- Pretty phone number formatting.\n- Fixed shared contacts display.\n- Fix KDE crash, use Qt tray icon in all Linux systems.\n\n0.8.40 dev (17.07.15)\n\n- Fixed critical Qt bug in image scale.\n- Huge amount of unread messages are loaded much faster.\n- Some bugfixes and optimizations.\n\n0.8.39 dev (14.07.15)\n\n- Moved to Qt 5.5 and Xcode 6.4.\n- Some bugfixes and optimizations.\n- In OS X 10.10.3 location marks sent from mobile should be displayed now.\n\n0.8.38 stable (03.07.15)\n\n- Improved in-app media playback.\n\n0.8.37 dev (03.07.15)\n\n- Improved in-app media playback.\n\n0.8.36 stable (01.07.15)\n\n- Forward photos, media and stickers with drag-n-drop.\n- Drag-n-drop text messages by timestamp to forward them.\n- Larger stickers panel.\n- IPv6 checkbox added to Connection Type in Settings.\n\n0.8.35 dev (30.06.15)\n\n- Bug fixes and minor improvements.\n\n0.8.34 dev (28.06.15)\n\n- Forward photos, media and stickers with drag-n-drop.\n- Drag-n-drop text messages by timestamp to forward them.\n- Larger stickers panel.\n\n0.8.33 dev (25.06.15)\n\n- IPv6 connection checkbox added to Connection Type box in Settings.\n\n0.8.32 stable (25.06.15)\n\n- Critical bug fixed.\n- IPv6 connection temporarily disabled.\n\n0.8.31 dev (24.06.15)\n\n- Bots support using the new bot API.\n- Drag-n-drop selected text and links from messages.\n- Forward selected messages by drag-n-drop.\n\n0.8.30 stable (24.06.15)\n\n- Bots support using the new bot API.\n\n0.8.29 dev (22.06.15)\n\n- Bug fixes and minor improvements.\n\n0.8.28 dev (18.06.15)\n\n- Bug fixes and minor improvements.\n\n0.8.27 dev (17.06.15)\n\n- Bug fixes and minor improvements.\n\n0.8.26 dev (17.06.15)\n\n- Bug fixes and minor improvements.\n\n0.8.25 dev (15.06.15)\n\n- IPv6 connections support.\n\n0.8.24 stable (08.06.15)\n\n- Improved sticker panel.\n\n0.8.23 dev (05.06.15)\n\n- Improved sticker panel.\n- New autoupdate check.\n\n0.8.22 dev (04.06.15)\n\n- OpenAL updated to a new version.\n- Autoupdating improved.\n- Bugfixes.\n\n0.8.21 stable (02.06.15)\n\n- Added sending voice messages.\n- Fixed disappearing stickers.\n\n0.8.20 dev (02.06.15)\n\n- Minor bug fixes.\n- Translations improved.\n\n0.8.19 dev (02.06.15)\n\n- Fixed disappearing stickers.\n\n0.8.18 dev (01.06.15)\n\n- Voice messages recording and sending.\n\n0.8.17 stable (21.05.15)\n\n- Crash fix.\n\n0.8.16 stable (20.05.15)\n\n- Added support for sticker packs.\n- New emoji and sticker panel.\n\n0.8.15 dev (20.05.15)\n\n- Video captions are displayed.\n- Photo captions are displayed in photo viewer.\n- Round corners for messages.\n\n0.8.14 dev (19.05.15)\n\n- Added support for sticker packs.\n- New emoji and sticker panel.\n\n0.8.13 stable (12.05.15)\n\n- Added support for new emoji.\n- Improved emoji and stickers panel.\n\n0.8.12 dev (08.05.15)\n\n- New emojis support added.\n- Emojis and stickers panel improved.\n\n0.8.11 stable (01.05.15)\n\n- Invite links for group chats.\n- Gray unread badge for muted conversations.\n\n0.8.10 dev (01.05.15)\n\n- Critical bug fixes for messages history loading.\n\n0.8.9 dev (30.04.15)\n\n- Fix for invite links.\n\n0.8.8 dev (30.04.15)\n\n- Invite links for group chats.\n- Gray unread badge for muted conversations.\n\n0.8.7 stable (24.04.15)\n\n- New photo viewer design.\n- Switch between files in the photo viewer.\n- Grouped notifications when several messages are forwarded.\n- New default chat background image (you can change it in Settings).\n\n0.8.6 dev (24.04.15)\n\n- Old default chat background image placed first in background Gallery.\n- Forwarded files, videos, audios and contacts original sender name is displayed.\n- Grouped notifications when several messages are forwarded.\n\n0.8.5 dev (22.04.15)\n\n- New photoviewer design.\n- Switch through files overview in photoviewer.\n- New default chat background image.\n\n0.8.4 stable (13.04.15)\n\n- Link preview layout bug fix.\n\n0.8.3 stable (08.04.15)\n\n- Link previews for Twitter, YouTube, Instagram and certain other links.\n- Two-step verification.\n- View all your Telegram sessions, terminate specific sessions.\n\n0.8.2 dev (07.04.15)\n\n- Link previews bugfixes.\n- Links in preview descriptions are now clickable.\n- Twitter and Instagram mentions and hashtags in previews are clickable.\n- Fixed file uploading.\n- Fixed photo, document and sticker forwarding.\n\n0.8.1 dev (05.04.15)\n\n- Link previews for Twitter, YouTube, Instagram and certain other links.\n- Two-step verification.\n- View all your Telegram sessions, terminate specific sessions.\n- Text is pasted from clipboard when clipboard has both text and image and image sending was cancelled.\n\n0.8 stable (26.03.15)\n\n- Add a comment before forwarded messages.\n- Hashtags suggestions in new message and search fields (based on recent searches).\n- Button to jump back to a reply after viewing the original message.\n- Add people to groups by username.\n\n0.7.26 dev (26.03.15)\n\n- Some bugfixes.\n\n0.7.25 dev (25.03.15)\n\n- Add people to groups by username.\n- Copy @username from profile context menu.\n\n0.7.24 dev (24.03.15)\n\n- Forward messages with comment or sticker before them.\n- Recent hashtags autocomplete in message and search fields.\n- Move from reply to original message and back.\n\n0.7.23 stable (20.03.15)\n\n- Reply to specific messages in groups.\n- Mention @usernames in groups to notify multiple users.\n\n0.7.22 dev (20.03.15)\n\n- Critical bug in messages delivery fixed.\n\n0.7.21 dev (19.03.15)\n\n- Reply to specific messages in groups.\n- Mention @usernames in groups to notify multiple users.\n\n0.7.20 stable (13.03.15)\n\n- Lock your app with a passcode.\n\n0.7.19 dev (03.03.15)\n\n- Passcode lock option added to Settings.\n\n0.7.18 dev (20.02.15)\n\n- Windows: crash on start fixed for some Intel cards.\n- Linux: tray icon returned in Pantheon and Gnome.\n\n0.7.17 stable (19.02.15)\n\n- Some minor improvements.\n- Windows: crash on app launch fixed for some Intel graphic cards.\n\n0.7.16 stable (17.02.15)\n\n- Some translations improvements.\n- Linux: tray icon should work better in non-Unity environments.\n\n0.7.15 dev (16.02.15)\n\n- Some translation keys added.\n- Linux: fixed semibold font.\n- Linux: tray icon should work better in non-Unity environments.\n\n0.7.14 dev (12.02.15)\n\n- Moved to Qt 5.4.0 version, please check everything.\n- Time values should be displayed like in your system.\n- Linux: fixed keyboard shortcuts when not english keyboard layout.\n\n0.7.13 stable (10.02.15)\n\n- You can now change the chat background.\n\n0.7.12 dev (06.02.15)\n\n- Chat background settings translated.\n- Mac OS X and Linux autoupdate for dev-version fixed.\n\n0.7.11 dev (05.02.15)\n\n- Chat background can be changed.\n- First dev-channel version.\n\n0.7.10 (29.01.15)\n\n- Online updated only while current window is active.\n\n0.7.9 (23.01.15)\n\n- Added Korean language.\n- Quick «open with» menu on Windows and OSX.\n\n0.7.8 (19.01.15)\n\n- Some graphics changed.\n- Windows: when activating window by click selection is not cleared.\n- Linux: tray icon and unity launcher counter support improved.\n\n0.7.7 (16.01.15)\n\n- Windows: system sleep / hibernation fixed when app is running.\n- Mac OS X: crashes on OS X 10.7 and 10.8 fixed.\n- Linux: tray icon support and unity launcher counter for unread messages added in Ubuntu.\n\n0.7.6 (06.01.15)\n\n- Some improvements for stickers.\n- Some translation improvements.\n\n0.7.5 (03.01.15)\n\n- Stickers support.\n- Local caching for voice messages.\n- Added Portuguese language.\n\n0.7.4 (23.12.14)\n\n- German and Dutch languages added.\n\n0.7.3 (21.12.14)\n\n- Italian and Spanish languages added.\n\n0.7.2 (16.12.14)\n\n- Title buttons bug fixed in single column layout.\n\n0.7.1 (16.12.14)\n\n- Linux: font for Korean characters added.\n- Various bugs fixed.\n\n0.7 (16.12.14)\n\n- Improved single column navigation.\n- Improved image viewer for large documents.\n\n0.6.21 (13.12.14)\n\n- Crash fixed for contacts without names.\n\n0.6.20 (13.12.14)\n\n- Crash fixed in single column layout.\n\n0.6.19 (13.12.14)\n\n- Some bugs fixed.\n\n0.6.18 (12.12.14)\n\n- Single-column layout support added for small chat window.\n- Photos are sent up to 1280x1280 size.\n- New device login notification added.\n- New version notification added.\n\n0.6.17 (08.12.14)\n\n- Filter in contacts lists (contacts, new group, add member) now is case-insensitive.\n- Win: telegram.me/username links handling fixed.\n\n0.6.16 (05.12.14)\n\n- telegram.me/username links supported.\n- Downloaded documents local cache added.\n- Some network improvements.\n\n0.6.15 (26.11.14)\n\n- Intro and login were slightly redesigned.\n- Some various bug fixes.\n\n0.6.14 (24.11.14)\n\n- Work over HTTP crashing fixed.\n\n0.6.13 (24.11.14)\n\n- Application icon changed.\n- Vimeo video links shown with preview.\n- Some iOS emojis display fixed.\n- Windows version crash when notification clicked fixed.\n\n0.6.12 (22.11.14)\n\n- Message drafts are saved.\n- Local photos cache added.\n- Shared contacts display and youtube links preview fixed.\n\n0.6.11 (18.11.14)\n\n- Some minor fixes.\n\n0.6.10 (18.11.14)\n\n- Portrait oriented photos display fixed.\n- Photo thumbs size decreased in conversations.\n- OS X: tray icon fixed in dark status bar theme.\n- OS X: main menu added.\n\n0.6.9 (15.11.14)\n\n- Some network problems fixed.\n\n0.6.8 (13.11.14)\n\n- YouTube and Instagram links are displayed with preview.\n- Location messages are displayed as a map with a link.\n- OS X: .dmg setup image now has a design.\n- OS X: updating after notebook wakeup fixed.\n\n0.6.7 (05.11.14)\n\n- Long texts are now split to several messages.\n- Some network improvements.\n\n0.6.6 (29.10.14)\n\n- Some network improvements.\n- Upload and download speed increased.\n\n0.6.5 (22.10.14)\n\n- Username support added.\n\n0.6.4 (17.10.14)\n\n- Photoviewer redesigned.\n- Default recent emojis filled.\n- Linux photo and document save from photoviewer fixed.\n\n0.6.3 (10.10.14)\n\n- Inline animated GIF documents view.\n- Crash in forward from search results fixed.\n\n0.6.2 (07.10.14)\n\n- Some documents (PNG and JPG) are opened in app photo viewer.\n- Photo and document viewer supports Ctrl + / Ctrl - / Ctrl 0 zoom hotkeys and image dragging.\n\n0.6.1 (30.09.14)\n\n- Windows only: fixed context menus.\n- OS X only: fixed crash with disabled tray icon.\n\n0.6 (30.09.14)\n\n- Photo loading indicator added.\n- Custom context menus added, including tray menu in Windows.\n- Default download folder to \"Downloads/Telegram Desktop\".\n- Improved OS X tray icon view and behaviour.\n\n0.5.21 (21.09.14)\n\n- Loading photos blurred in photos overview.\n- Full voice message duration displayed when playing.\n- Multimonitor photo view fixed.\n\n0.5.20 (20.09.14)\n\n- Loading photos blurred.\n- Default emoji images used.\n- OS X tray icon support added.\n- Windows version is now digitally signed.\n\n0.5.19 (05.09.14)\n\n- Long voice message playing fix.\n\n0.5.18 (05.09.14)\n\n- Voice messages auto download and in-app playing support.\n- Audio now is working through OpenAL library.\n- Linux version now is not dependent on libpulse.\n\n0.5.17 (21.08.14)\n\n- Multi selection added to media overview.\n- Photos in overview are displayed in right order now.\n- Some crashes fixed.\n\n0.5.16 (15.08.14)\n\n- Navigate through photos fixed.\n- Media overview in profiles (photos, video files, documents, voice messages).\n- Going to current message in history from media overview context menu.\n- Some protocol+network bugs fixed.\n- OS X application signed by Developer ID certificate.\n\n0.5.15 (12.08.14)\n\n- Fixes in new photo viewer.\n\n0.5.14 (11.08.14)\n\n- New photo viewer, fullscreen, with controls and moving through conversation photos.\n- Checkbox for sending images from clipboard without compression as documents.\n\n0.5.13 (02.08.14)\n\n- Some network and protocol issues fixed.\n\n0.5.12 (30.07.14)\n\n- Fixed diacritics input in Linux.\n- Fixed saving files with incorrect server-side filenames in Windows.\n\n0.5.11 (26.07.14)\n\n- High definition graphics added (2x interface scale and OS X retina displays).\n- Some invalid links highlighting removed.\n\n0.5.10 (24.07.14)\n\n- Escape key closes photo view.\n- On OS X now Ctrl+Tab and Ctrl+Shift+Tab are working for dialogs switching.\n- On Linux fixed notifies (they don't grab focus and aren't seen in windows switcher), fixed keyboard layout change.\n- On Linux all application data is now stored in ~/.TelegramDesktop directory.\n\n0.5.9 (18.07.14)\n\n- Choose between notification with preview, with only sender's name (message preview hidden) or without any information.\n- Sending documents from \"Send to\" explorer context menu added, see a checkbox in Settings (only Windows version).\n- Little features: Ctrl+Tab / Ctrl+Shift+Tab switch to near dialogs, links without top-level domain highlight, version in About dialog now leads to version history.\n- Some bugs fixed: maximize button in Ubuntu, running different copies of app under different users at the same time in Windows, Surface touchscreen support fixed.\n- Added 32 bit Ubuntu version.\n\n0.5.8 (16.07.14)\n\n- Fixed crash at contact message history clear.\n- Ubuntu 12.04 is supported now.\n- Added hashtag highlight and messages search by hashtags.\n\n0.5.7 (15.07.14)\n\n- All platforms are updated up to one version and code (Windows, OS X, Ubuntu).\n- Fixed crash at document open on OS X.\n- Message search results shown below the filtered dialogs when searching.\n\n0.5.6 (04.07.14)\n\n- Windows test version (no autoupdate) with server-side messages search.\n- First version of Ubuntu tdesktop build released.\n\n0.5.5 (25.06.14)\n\n- First version of OS X tdesktop build released.\n\n0.5.4 (17.06.14)\n\n- Font changed to Open Sans.\n\n0.5.3 (13.06.14)\n\n- Taskbar icon hide fixed (once again!), sorry :(.\n\n0.5.2 (12.06.14)\n\n- Two crashes fixed.\n- Ellipsis text overflow display fixed in dialogs list.\n\n0.5.1 (01.06.14)\n\n- Fixed audio notifications.\n- Fixed taskbar icon hiding.\n- source code: ./_qt_5_3_0_patch/ updated, Qt 5.3 rebuild required.\n\n0.5.0 (30.05.14)\n\n- Application source code is published on GitHub under GPL v3\n\n0.4.18 (28.05.14)\n\n- Crash in other users profile fixed.\n\n0.4.17 (28.05.14)\n\n- Desktop notifications bug fixed.\n\n0.4.16 (28.05.14)\n\n- Desktop and sound notifications one second delay added.\n- Chat and contact notifications settings cloud-stored and synced, loaded from server.\n- Mobile sign up skip added, you can join from desktop now.\n\n0.4.15 (29.04.14)\n\n- Default tcp and http connection types merged to one.\n- Using tcp connection by default, using http-transport if plain tcp is unavailable.\n- Files less than 10mb are md5-signed now.\n\n0.4.14 (19.04.14)\n\n- Documents and files up to 1500 Mb can be sent now.\n- Some lang phrases are fixed.\n- Memory leak fixed in file upload.\n\n0.4.13 (16.04.14)\n\n- Critical auth-storing bug fixed (auto-logout on app relaunch in some cases).\n\n0.4.12 (16.04.14)\n\n- Window shadow and resize area now scale with the rest of the interface.\n- Multiple delete and forward buttons and scale select design improved.\n- Portable version (the same Telegram.exe, actually) .zip archive added here.\n\n0.4.11 (09.04.14)\n\n- Emoji smiles support in message field added.\n- Hover-button for emoji tab showing added to the left from Send button.\n\n0.4.10 (31.03.14)\n\n- Some texts fixes and autoupdate check.\n\n0.4.9 (31.03.14)\n\n- High dpi support added, in settings: 1x, 1.25x, 1.5x, 2x interface scale.\n\n0.4.8 (25.03.14)\n\n- No changes, just autoupdate testing version.\n\n0.4.7 (25.03.14)\n\n- One more critical autoupdate bug fixed, users with not latin symbols in their Windows login names will have to install this version manually by setup executable.\n- Autoupdate behind authentication-requiring proxy network settings fixed.\n\n0.4.6 (22.03.14)\n\n- Critical autoupdate bug fixed, users with not latin (utf) symbols in their Windows login names will have to install this version manually by setup executable.\n- Voice messages extension changed from .mp3 to .ogg, because they are in OGG container, with Opus audio codec.\n\n0.4.5 (22.03.14)\n\n- Connection bug fixed.\n\n0.4.4 (22.03.14)\n\n- Fixed voice mail attach download, it works like video and documents now.\n- After video, audio or document download Download button becomes Open With instead of Save As.\n\n0.4.3 (16.03.14)\n\n- Default download path introduced, no folder select for each video, audio or other file.\n- You can choose old approach (choose download path for each file) in Settings.\n- In downloaded video, audio and file context menu (right-click) added \"Show in Folder\" option.\n\n0.4.2 (14.03.14)\n\n- Network bug fixed, file upload on weak connections should be better now.\n- Selected items clear after message forward fixed.\n- Suggested filename fixed in save file dialog box.\n\n0.4.1 (11.03.14)\n\n- Auto-update critical WinXP bug fixed.\n- You should install this version manually, if app was broken on WinXP.\n\n0.4 (10.03.14)\n\n- Better touch-screen support (tested on Surface Pro 2).\n- Moved to new versions of ide+compiler and of some libs.\n\n0.3.13 (07.03.14)\n\n- Crash on right mouse button press fixed.\n\n0.3.12 (07.03.14)\n\n- Text and messages selection.\n- Copy selected to clipboard (Ctrl+C and from context menu).\n- Multiple message forward and delete support.\n\n0.3.11 (04.03.14)\n\n- Long phone numbers enabled (12 digits after country code).\n\n0.3.10 (02.03.14)\n\n- Bad reconnect delay bug fixed.\n\n0.3.9 (27.02.14)\n\n- Some scroll bugs fixed.\n- Message forward confirm by Enter added.\n\n0.3.8 (26.02.14)\n\n- Dropdown with two attach types added, File (document) or Photo.\n- Different drag-n-drop zones done for photo and document sending.\n- File dialog open speed increased in some cases.\n\n0.3.7 (24.02.14)\n\n- New text layout used in messages, rtl/bidi support done.\n- Emoji auto replace in message text added (replace list in settings).\n- Clickable user links added to service messages (smbd invited smbd to group, etc).\n\n0.3.6 (23.02.14)\n\n- New text layout testing.\n- Email highlight in messages and click response fixed.\n\n0.3.5 (20.02.14)\n\n- Reconnect timeout added, must work better after sleep mode.\n- Minor design improvements and layout fixes.\n\n0.3.4 (13.02.14)\n\n- New design fully implemented.\n\n0.3.3 (12.02.14)\n\n- Test version of new design implementation.\n\n0.3.2 (11.02.14)\n\n- First (private) version of new design implementation.\n\n0.3.1 (26.01.14)\n\n- Rare protocol bug (with incorrect system time handle) fixed.\n\n0.3.0 (26.01.14)\n\n- Long links tooltips added.\n- Intro and settings animation improved.\n- Bug fixes (protocol, group creation, etc).\n\n0.2.9 (12.01.14)\n\n- Minor bug fixes.\n\n0.2.8 (12.01.14)\n\n- Differenct connection type supported (http-transport support added).\n- Connecting with system and custom proxy settings supported (http and socks5 proxy support added).\n\n0.2.7 (07.01.14)\n\n- Window resize areas are now positioned outside the window area, so they are wider and do not conflict with message history scrollbar.\n- Some bugs fixed, one connection problem solved.\n\n0.2.6 (06.01.14)\n\n- Document sharing supported, downloading and uploading up to 100 mb files.\n- Download videos, that were sent from mobile apps, as documents.\n\n0.2.5 (30.12.13)\n\n- Group photo upload fixed for some photo sizes.\n\n0.2.4 (30.12.13)\n\n- Delete history and Delete contact buttons in contacts, Delete and exit button in groups.\n- Adding and removing group participants supported.\n- New group creation supported.\n\n0.2.3 (20.12.13)\n\n- Couple of crashing bugs fixed.\n\n0.2.2 (20.12.13)\n\n- User and group profile pages now available, click photo or name in window title area.\n- Group name and photo change support from group profile page.\n- Group participants list with last activity time and online status displayed on group profile page.\n- Adding users to your contact list and editing their contact names from user profile page.\n- Contact sharing supported from user profile page.\n\n0.2.1 (16.12.13)\n\n- User name and profile photo update in settings.\n\n0.2 (11.12.13)\n\n- Auto-update support.\n- Setup no longer requires admin rights (except first installation over a previous version, that was installed using admin rights).\n- Application window retains screen position upon relaunch.\n\n0.1.16 (29.11.13)\n\n- Added «Delete Message» and «Forward Message» items to context menus.\n- No PUSH-notifications are sent to your mobile phone from the currently active Telegram Win conversation.\n- Fixed issue with broken long links in messages copied to clipboard.\n"
  },
  {
    "path": "docs/api_credentials.md",
    "content": "## Obtaining your API credentials\n\nTo build your version of Telegram Desktop you're required to provide your own **api_id** and **api_hash** for the Telegram API access.\n\nHow to obtain your **api_id** and **api_hash** is described here: [https://core.telegram.org/api/obtaining_api_id](https://core.telegram.org/api/obtaining_api_id)\n\nIf you're building the application not for deployment, but only for test purposes you can use TEST ONLY credentials, which are very limited by the Telegram API server:\n\n**api_id**: 17349\n**api_hash**: 344583e45741c457fe1862106095a5eb\n\nYour users will start getting internal server errors on login if you deploy an app using those **api_id** and **api_hash**.\n\n"
  },
  {
    "path": "docs/building-linux.md",
    "content": "## Build instructions for Linux using Docker\n\n### Prepare folder\n\nChoose a folder for the future build, for example **/home/user/TBuild**. It will be named ***BuildPath*** in the rest of this document. All commands will be launched from Terminal.\n\n### Obtain your API credentials\n\nYou will require **api_id** and **api_hash** to access the Telegram API servers. To learn how to obtain them [click here][api_credentials].\n\n### Clone source code and prepare libraries\n\nInstall [poetry](https://python-poetry.org), go to ***BuildPath*** and run\n\n    git clone --recursive https://github.com/telegramdesktop/tdesktop.git\n    ./tdesktop/Telegram/build/prepare/linux.sh\n\n### Building the project\n\nGo to ***BuildPath*/tdesktop** and run (using [your **api_id** and **api_hash**](#obtain-your-api-credentials))\n\n    docker run --rm -it \\\n        -u $(id -u) \\\n        -v \"$PWD:/usr/src/tdesktop\" \\\n        tdesktop:centos_env \\\n        /usr/src/tdesktop/Telegram/build/docker/centos_env/build.sh \\\n        -D TDESKTOP_API_ID=YOUR_API_ID \\\n        -D TDESKTOP_API_HASH=YOUR_API_HASH\n\nOr, to create a debug build, run (also using [your **api_id** and **api_hash**](#obtain-your-api-credentials))\n\n    docker run --rm -it \\\n        -u $(id -u) \\\n        -v \"$PWD:/usr/src/tdesktop\" \\\n        -e CONFIG=Debug \\\n        tdesktop:centos_env \\\n        /usr/src/tdesktop/Telegram/build/docker/centos_env/build.sh \\\n        -D TDESKTOP_API_ID=YOUR_API_ID \\\n        -D TDESKTOP_API_HASH=YOUR_API_HASH\n\nThe built files will be in the `out` directory.\n\n### Visual Studio Code integration\n\nEnsure you've followed the instruction up to the [**Clone source code and prepare libraries**](#clone-source-code-and-prepare-libraries) step at least.\n\nOpen the repository in Visual Studio Code, install the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension and add the following to `.vscode/settings.json` (using [your **api_id** and **api_hash**](#obtain-your-api-credentials)):\n\n    {\n        \"cmake.configureSettings\": {\n            \"TDESKTOP_API_ID\": \"YOUR_API_ID\",\n            \"TDESKTOP_API_HASH\": \"YOUR_API_HASH\"\n        }\n    }\n\nAfter that, choose **Reopen in Container** via the menu triggered by the green button in bottom left corner and you're done.\n\n![Quick actions Status bar item](https://code.visualstudio.com/assets/docs/devcontainers/containers/remote-dev-status-bar.png)\n\n[api_credentials]: api_credentials.md\n"
  },
  {
    "path": "docs/building-mac.md",
    "content": "## Build instructions for macOS\n\n### Prepare folder\n\nChoose a folder for the future build, for example **/Users/user/TBuild**. It will be named ***BuildPath*** in the rest of this document. All commands will be launched from Terminal.\n\n**Note about disk space:** The full build process will require approximately **55 GB** of free space. This includes:\n- **~35 GB** for libraries (when building for both x64 and arm64 architectures)\n- **~20 GB** for the compiled Telegram app (in the `out` folder)\n\n### Obtain your API credentials\n\nYou will require **api_id** and **api_hash** to access the Telegram API servers. To learn how to obtain them [click here][api_credentials].\n\n### Clone source code and prepare libraries\n\nGo to ***BuildPath*** and run\n\n    /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n    brew install git automake cmake wget pkg-config gnu-tar ninja nasm meson\n\n    sudo xcode-select -s /Applications/Xcode.app/Contents/Developer\n\n    git clone --recursive https://github.com/telegramdesktop/tdesktop.git\n    ./tdesktop/Telegram/build/prepare/mac.sh\n\n### Building the project\n\nGo to ***BuildPath*/tdesktop/Telegram** and run (using [your **api_id** and **api_hash**](#obtain-your-api-credentials))\n\n    ./configure.sh -D TDESKTOP_API_ID=YOUR_API_ID -D TDESKTOP_API_HASH=YOUR_API_HASH\n\nThen launch Xcode, open ***BuildPath*/tdesktop/out/Telegram.xcodeproj** and build for Debug / Release.\n\n[api_credentials]: api_credentials.md\n"
  },
  {
    "path": "docs/building-mas.md",
    "content": "## Build instructions for Mac App Store\n\n**NB** These are used for Mac App Store build, after the [Build for macOS][mac] instructions.\n\n### Prepare breakpad\n\nGo to ***BuildPath*** and run\n\n    MACOSX_DEPLOYMENT_TARGET=10.12\n\n    cd Libraries\n\n    git clone https://chromium.googlesource.com/breakpad/breakpad\n    cd breakpad\n    git checkout bc8fb886\n    git clone https://chromium.googlesource.com/linux-syscall-support src/third_party/lss\n    cd src/third_party/lss\n    git checkout a91633d1\n    cd ../../..\n    git apply ../patches/breakpad.diff\n    cd src/client/mac\n    xcodebuild -project Breakpad.xcodeproj -target Breakpad -configuration Debug build\n    xcodebuild -project Breakpad.xcodeproj -target Breakpad -configuration Release build\n    cd ../../tools/mac/dump_syms\n    xcodebuild -project dump_syms.xcodeproj -target dump_syms -configuration Release build\n    cd ../../../../..\n\n[xcode]: building-xcode.md\n"
  },
  {
    "path": "docs/building-win.md",
    "content": "# Build instructions for Windows\n\n- [Prepare folder](#prepare-folder)\n- [Install third party software](#install-third-party-software)\n- [Choose architecture and initialize terminal](#choose-architecture-and-initialize-terminal)\n- [Clone source code and prepare libraries](#clone-source-code-and-prepare-libraries)\n- [Build the project](#build-the-project)\n- [Qt Visual Studio Tools](#qt-visual-studio-tools)\n\n## Prepare folder\n\nThe build is done in **Visual Studio 2026** with **10.0.26100.0** SDK version.\n\nChoose an empty folder for the future build, for example **D:\\\\TBuild**. It will be named ***BuildPath*** in the rest of this document. Create two folders there, ***BuildPath*\\\\ThirdParty** and ***BuildPath*\\\\Libraries**.\n\nThe default modern toolset from Visual Studio 2026 (`v145`) does not support Windows 7, so for Telegram Desktop you must use `-vcvars_ver=14.44` (`v144.4`, based on `v143` with Windows 7 support).\n\n### Obtain your API credentials\n\nYou will require **api_id** and **api_hash** to access the Telegram API servers. To learn how to obtain them [click here][api_credentials].\n\n## Install third party software\n\n* Download **Python 3.10** installer from [https://www.python.org/downloads/](https://www.python.org/downloads/) and install it with adding to PATH.\n* Download **Git** installer from [https://git-scm.com/download/win](https://git-scm.com/download/win) and install it.\n\n## Choose architecture and initialize terminal\n\nBefore preparing libraries and running build commands, initialize the Visual Studio environment for your target architecture.\nThe default modern toolset from Visual Studio 2026 (`v145`) does not support Windows 7, so for Telegram Desktop you must use `-vcvars_ver=14.44` (`v144.4`, based on `v143` with Windows 7 support).\n\nFor `win` (32-bit):\n\n    %comspec% /k \"C:\\Program Files\\Microsoft Visual Studio\\18\\Community\\VC\\Auxiliary\\Build\\vcvars32.bat\" -vcvars_ver=14.44\n\nFor `win64` (64-bit):\n\n    %comspec% /k \"C:\\Program Files\\Microsoft Visual Studio\\18\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat\" -vcvars_ver=14.44\n\nRun both `Clone source code and prepare libraries` and `Build the project` sections in the terminal initialized with one of the commands above.\n\n## Clone source code and prepare libraries\n\nIn the initialized terminal, go to ***BuildPath*** and run\n\n    git clone --recursive https://github.com/telegramdesktop/tdesktop.git\n    tdesktop\\Telegram\\build\\prepare\\win.bat\n\n## Build the project\n\nGo to ***BuildPath*\\\\tdesktop\\\\Telegram** and run (using [your **api_id** and **api_hash**](#obtain-your-api-credentials)):\n\nFor `win` (32-bit):\n\n    configure.bat -D TDESKTOP_API_ID=YOUR_API_ID -D TDESKTOP_API_HASH=YOUR_API_HASH\n\nFor `win64` (64-bit):\n\n    configure.bat x64 -D TDESKTOP_API_ID=YOUR_API_ID -D TDESKTOP_API_HASH=YOUR_API_HASH\n\n* Open ***BuildPath*\\\\tdesktop\\\\out\\\\Telegram.slnx** in Visual Studio 2026\n* Select Telegram project and press Build > Build Telegram (Debug and Release configurations)\n* The result Telegram.exe will be located in **D:\\TBuild\\tdesktop\\out\\Debug** (and **Release**)\n\n### Qt Visual Studio Tools\n\nFor better debugging you may want to install Qt Visual Studio Tools:\n\n* Open **Extensions** -> **Manage Extensions**\n* Go to **Online** tab\n* Search for **Qt**\n* Install **Qt Visual Studio Tools** extension\n\n[api_credentials]: api_credentials.md\n"
  },
  {
    "path": "docs/misc.txt",
    "content": "macOS:\n\nbreakpad - clone breakpad like for msvc, including the patch, open breakpad/src/client/mac/Breakpad.xcodeproj and build Release\ndump_syms - open breakpad/src/tools/mac/dump_syms/dump_syms.xcodeproj and build Release, copy dump_syms from ~/Library/Developer/Xcode/DerivedData/dump_syms-[a-z0-9]+/Build/Products/Release/ to BuildPath/Libraries/breakpad/src/tools/mac/dump_syms/build/Release/\nminidump_stackwalk - go to Libraries/breakpad, run \"configure\", run \"make\"\n\nWindows:\n\ninstall InnoSetup\nopen brekpad/src/tools/windows/dump_syms/dump_syms.vcproj, upgrade, change SDK and build Debug and Release\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Project Releases | GitHub Pages</title>\n    <style>\n        :root {\n            --primary-color: #2ea44f;\n            --secondary-color: #f6f8fa;\n            --border-color: #e1e4e8;\n            --text-color: #24292e;\n            --bg-color: #ffffff;\n            --accent-color: #0366d6;\n        }\n\n        body {\n            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;\n            line-height: 1.6;\n            color: var(--text-color);\n            background-color: var(--bg-color);\n            margin: 0;\n            padding: 0;\n            max-width: 1012px;\n            margin: 0 auto;\n            padding: 32px;\n        }\n\n        header {\n            border-bottom: 1px solid var(--border-color);\n            padding-bottom: 32px;\n            margin-bottom: 32px;\n        }\n\n        h1 {\n            font-size: 32px;\n            font-weight: 600;\n            margin-bottom: 8px;\n        }\n\n        h2 {\n            font-size: 24px;\n            font-weight: 600;\n            margin-top: 32px;\n            margin-bottom: 16px;\n            padding-bottom: 8px;\n            border-bottom: 1px solid var(--border-color);\n        }\n\n        .description {\n            font-size: 18px;\n            color: #586069;\n        }\n\n        .card {\n            border: 1px solid var(--border-color);\n            border-radius: 6px;\n            padding: 16px;\n            margin-bottom: 16px;\n            background-color: var(--bg-color);\n        }\n\n        .release-table {\n            width: 100%;\n            border-collapse: collapse;\n            margin: 20px 0;\n        }\n\n        .release-table th, .release-table td {\n            border: 1px solid var(--border-color);\n            padding: 12px;\n            text-align: left;\n        }\n\n        .release-table th {\n            background-color: var(--secondary-color);\n            font-weight: 600;\n        }\n\n        .download-btn {\n            background-color: var(--primary-color);\n            color: white;\n            padding: 8px 16px;\n            text-decoration: none;\n            border-radius: 6px;\n            display: inline-block;\n            font-weight: 500;\n            transition: background-color 0.2s;\n        }\n\n        .download-btn:hover {\n            background-color: #2c974b;\n        }\n\n        .tag {\n            display: inline-block;\n            padding: 4px 8px;\n            font-size: 12px;\n            font-weight: 600;\n            border-radius: 2em;\n            background-color: #f1f8ff;\n            color: var(--accent-color);\n            margin-left: 8px;\n        }\n\n        .loading {\n            text-align: center;\n            padding: 40px;\n            color: #586069;\n        }\n\n        .error {\n            color: #cb2431;\n            padding: 16px;\n            border: 1px solid #f97583;\n            border-radius: 6px;\n            background-color: #ffebeb;\n        }\n\n        footer {\n            margin-top: 40px;\n            padding-top: 20px;\n            border-top: 1px solid var(--border-color);\n            text-align: center;\n            color: #586069;\n            font-size: 14px;\n        }\n\n        #readme-content h1, #readme-content h2, #readme-content h3 {\n            margin-top: 24px;\n            margin-bottom: 16px;\n            font-weight: 600;\n        }\n\n        #readme-content h1 {\n            font-size: 2em;\n            border-bottom: 1px solid var(--border-color);\n            padding-bottom: 0.3em;\n        }\n\n        #readme-content h2 {\n            font-size: 1.5em;\n            border-bottom: 1px solid var(--border-color);\n            padding-bottom: 0.3em;\n        }\n\n        #readme-content h3 {\n            font-size: 1.25em;\n        }\n\n        #readme-content p {\n            margin-bottom: 16px;\n        }\n\n        #readme-content a {\n            color: var(--accent-color);\n            text-decoration: none;\n        }\n\n        #readme-content a:hover {\n            text-decoration: underline;\n        }\n\n        #readme-content ul, #readme-content ol {\n            padding-left: 2em;\n            margin-bottom: 16px;\n        }\n\n        #readme-content code {\n            padding: 0.2em 0.4em;\n            margin: 0;\n            font-size: 85%;\n            background-color: var(--secondary-color);\n            border-radius: 6px;\n            font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n        }\n\n        #readme-content pre {\n            padding: 16px;\n            overflow: auto;\n            font-size: 85%;\n            line-height: 1.45;\n            background-color: var(--secondary-color);\n            border-radius: 6px;\n            margin-bottom: 16px;\n        }\n\n        #readme-content pre code {\n            padding: 0;\n            background-color: transparent;\n        }\n\n        #readme-content blockquote {\n            padding: 0 1em;\n            color: #6a737d;\n            border-left: 0.25em solid #dfe2e5;\n            margin: 0 0 16px 0;\n        }\n\n        @media (max-width: 768px) {\n            body {\n                padding: 16px;\n            }\n\n            .release-table {\n                display: block;\n                overflow-x: auto;\n            }\n        }\n    </style>\n</head>\n<body>\n    <main>\n        <section id=\"releases\">\n            <h2>Latest Releases</h2>\n            <div class=\"card\">\n                <div id=\"release-table\">\n                    <div class=\"loading\">Loading release information...</div>\n                </div>\n            </div>\n        </section>\n\n        <section id=\"readme\">\n            <h2>Overview</h2>\n            <div class=\"card\">\n                <div id=\"readme-content\">\n                    <div class=\"loading\">Loading README content...</div>\n                </div>\n            </div>\n        </section>\n    </main>\n\n    <footer>\n        <p>Generated with ❤️ for GitHub Pages</p>\n    </footer>\n\n    <script>\n        // Replace these with your GitHub username and repository name\n        const username = 'forkgram';\n        const repo = 'tdesktop';\n\n        // Function to fetch the latest release\n        async function fetchLatestRelease() {\n            try {\n                const response = await fetch(`https://api.github.com/repos/${username}/${repo}/releases/latest`);\n                if (!response.ok) {\n                    throw new Error(`HTTP error! status: ${response.status}`);\n                }\n                const release = await response.json();\n\n                if (release.assets && release.assets.length > 0) {\n                    const table = `\n                        <table class=\"release-table\">\n                            <thead>\n                                <tr>\n                                    <th>File</th>\n                                    <th>Size</th>\n                                    <th>Downloads</th>\n                                    <th>Action</th>\n                                </tr>\n                            </thead>\n                            <tbody>\n                                ${release.assets.map(asset => `\n                                    <tr>\n                                        <td>${asset.name}</td>\n                                        <td>${formatFileSize(asset.size)}</td>\n                                        <td>${asset.download_count}</td>\n                                        <td>\n                                            <a href=\"${asset.browser_download_url}\" class=\"download-btn\">\n                                                Download\n                                            </a>\n                                        </td>\n                                    </tr>\n                                `).join('')}\n                            </tbody>\n                        </table>\n                        <p><strong>Version:</strong> ${release.tag_name} <span class=\"tag\">Latest</span></p>\n                        <p><strong>Release Date:</strong> ${new Date(release.published_at).toLocaleDateString()}</p>\n                        ${release.body ? `<p><strong>Release Notes:</strong><br>${release.body}</p>` : ''}\n                    `;\n                    document.getElementById('release-table').innerHTML = table;\n                } else {\n                    document.getElementById('release-table').innerHTML = '<p>No release assets available.</p>';\n                }\n            } catch (error) {\n                console.error('Error fetching release:', error);\n                document.getElementById('release-table').innerHTML = `\n                    <div class=\"error\">\n                        <p>Error loading releases: ${error.message}</p>\n                        <p>Make sure you've set the correct username and repository name in the script.</p>\n                    </div>\n                `;\n            }\n        }\n\n        // Function to fetch README and extract content before \"## Features:\"\n        async function fetchReadme() {\n            try {\n                const response = await fetch(`https://api.github.com/repos/${username}/${repo}/readme`);\n                if (!response.ok) {\n                    throw new Error(`HTTP error! status: ${response.status}`);\n                }\n                const readme = await response.json();\n\n                const binaryString = atob(readme.content);\n                const bytes = new Uint8Array(binaryString.length);\n                for (let i = 0; i < binaryString.length; i++) {\n                    bytes[i] = binaryString.charCodeAt(i);\n                }\n                const content = new TextDecoder('utf-8').decode(bytes);\n\n                // Extract content before \"## Features:\" (case insensitive)\n                const featuresIndex = content.toLowerCase().indexOf('## features:');\n                let extractedContent = content;\n\n                if (featuresIndex !== -1) {\n                    extractedContent = content.substring(0, featuresIndex);\n                }\n\n                extractedContent = extractedContent\n                    .replace(/â/g, '—')\n                    .replace(/â/g, '’')\n                    .replace(/â/g, '“')\n                    .replace(/â/g, '”');\n\n                // Convert Markdown to HTML\n                const htmlContent = convertMarkdownToHtml(extractedContent);\n                document.getElementById('readme-content').innerHTML = htmlContent;\n            } catch (error) {\n                console.error('Error fetching README:', error);\n                document.getElementById('readme-content').innerHTML = `\n                    <div class=\"error\">\n                        <p>Error loading README: ${error.message}</p>\n                        <p>Make sure you've set the correct username and repository name in the script.</p>\n                    </div>\n                `;\n            }\n        }\n\n        // Enhanced Markdown to HTML conversion\n        function convertMarkdownToHtml(markdown) {\n            return markdown\n                // Headers\n                .replace(/^###### (.*$)/gim, '<h6>$1</h6>')\n                .replace(/^##### (.*$)/gim, '<h5>$1</h5>')\n                .replace(/^#### (.*$)/gim, '<h4>$1</h4>')\n                .replace(/^### (.*$)/gim, '<h3>$1</h3>')\n                .replace(/^## (.*$)/gim, '<h2>$1</h2>')\n                .replace(/^# (.*$)/gim, '<h1>$1</h1>')\n\n                // Bold and Italic\n                .replace(/\\*\\*\\*(.*?)\\*\\*\\*/gim, '<strong><em>$1</em></strong>')\n                .replace(/\\*\\*(.*?)\\*\\*/gim, '<strong>$1</strong>')\n                .replace(/\\*(.*?)\\*/gim, '<em>$1</em>')\n                .replace(/_(.*?)_/gim, '<em>$1</em>')\n\n                // Links\n                .replace(/!\\[(.*?)\\]\\((.*?)\\)/gim, \"<img alt='$1' src='$2' />\")\n                .replace(/\\[(.*?)\\]\\((.*?)\\)/gim, \"<a href='$2'>$1</a>\")\n\n                // Code blocks\n                .replace(/```(\\w+)?\\n([\\s\\S]*?)```/gim, '<pre><code>$2</code></pre>')\n                .replace(/`(.*?)`/gim, '<code>$1</code>')\n\n                // Blockquotes\n                .replace(/^>\\s(.*$)/gim, '<blockquote>$1</blockquote>')\n\n                // Horizontal rule\n                .replace(/^\\*\\*\\*\\*$/gim, '<hr>')\n\n                // Lists\n                .replace(/^\\s*[\\*\\-]\\s(.*$)/gim, '<ul><li>$1</li></ul>')\n                .replace(/^\\s*\\d\\.\\s(.*$)/gim, '<ol><li>$1</li></ol>')\n                .replace(/<\\/ul>\\s*<ul>/gim, '')\n                .replace(/<\\/ol>\\s*<ol>/gim, '')\n\n                // Paragraphs and line breaks\n                .replace(/\\n\\n/gim, '</p><p>')\n                .replace(/\\n/gim, '<br>')\n                .replace(/<p>(.*?)<\\/p>/gim, '<p>$1</p>')\n\n                // Final cleanup\n                .replace(/<p><\\/p>/gim, '')\n                .replace(/<p><\\/(ul|ol)>/gim, '</$1>')\n                .replace(/<(ul|ol)><\\/$1>/gim, '')\n                .replace(/<p><(ul|ol)>/gim, '<$1>')\n                .replace(/<\\/(ul|ol)><p>/gim, '</$1>');\n        }\n\n        // Format file size\n        function formatFileSize(bytes) {\n            if (bytes === 0) return '0 Bytes';\n            const k = 1024;\n            const sizes = ['Bytes', 'KB', 'MB', 'GB'];\n            const i = Math.floor(Math.log(bytes) / Math.log(k));\n            return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n        }\n\n        // Load data when page is ready\n        document.addEventListener('DOMContentLoaded', function() {\n            // Update page title and header with repo info\n            if (username !== 'your-username' && repo !== 'your-repository') {\n                document.title = `Forkgram | Releases`;\n                // document.querySelector('h1').textContent = repo;\n            }\n\n            fetchLatestRelease();\n            fetchReadme();\n        });\n    </script>\n</body>\n</html>"
  },
  {
    "path": "lib/xdg/org.telegram.desktop.desktop",
    "content": "[Desktop Entry]\nName=Forkgram\nComment=Enhanced twist on Telegram\nTryExec=Forkgram\nExec=Forkgram -- %U\nIcon=io.github.forkgram.tdesktop\nTerminal=false\nStartupWMClass=ForkgramDesktop\nType=Application\nCategories=Chat;Network;InstantMessaging;Qt;\nMimeType=x-scheme-handler/tg;x-scheme-handler/tonsite;\nKeywords=tg;chat;im;messaging;messenger;sms;tdesktop;\nActions=quit;\nDBusActivatable=true\nSingleMainWindow=true\nX-GNOME-UsesNotifications=true\nX-GNOME-SingleWindow=true\n\n[Desktop Action quit]\nExec=Forkgram -quit\nName=Quit Forkgram\nIcon=application-exit\n"
  },
  {
    "path": "lib/xdg/org.telegram.desktop.metainfo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<component type=\"desktop\">\n    <id>io.github.forkgram.tdesktop</id>\n    <metadata_license>CC0-1.0</metadata_license>\n    <project_license>GPL-3.0</project_license>\n    <name>Forkgram</name>\n    <summary>Enhanced twist on Telegram</summary>\n    <description>\n        <p>Unofficial Telegram client based on Telegram Desktop with a focus on practical improvements and a faster release cycle.</p>\n        <p>Refined Interface: Enjoy square contact avatars, a square tray icon, and the option to see a user's last seen time directly in the dialog list.</p>\n        <p>Enhanced Navigation: Jump instantly to the first unread message with a dedicated button. Keep important windows on top with an \"Always on top\" toggle.</p>\n        <p>Master Your Media: Swap images in albums you've already sent or group multiple photos into one album without re-uploading.</p>\n    </description>\n    <categories>\n        <category>Network</category>\n        <category>InstantMessaging</category>\n    </categories>\n    <developer id=\"io.github.forkgram\"><name>23rd</name></developer>\n    <url type=\"homepage\">https://forkgram.github.io/tdesktop/</url>\n    <url type=\"faq\">https://telegram.org/faq</url>\n    <url type=\"bugtracker\">https://github.com/forkgram/tdesktop/issues</url>\n    <url type=\"translate\">https://translations.telegram.org/</url>\n    <url type=\"vcs-browser\">https://github.com/forkgram/tdesktop</url>\n    <url type=\"contribute\">https://github.com/forkgram/tdesktop/blob/dev/.github/CONTRIBUTING.md</url>\n    <branding>\n        <color type=\"primary\" scheme_preference=\"light\">#c1b8e5</color>\n        <color type=\"primary\" scheme_preference=\"dark\">#5b586e</color>\n    </branding>\n    <screenshots>\n      <screenshot type=\"default\">\n        <image>https://raw.githubusercontent.com/forkgram/tdesktop/dev/docs/assets/frk/intro.png</image>\n        <caption>Introduction screen</caption>\n      </screenshot>\n      <screenshot>\n        <image>https://raw.githubusercontent.com/forkgram/tdesktop/dev/docs/assets/frk/dialogs.png</image>\n        <caption>User's last seen time directly in the dialog list</caption>\n      </screenshot>\n      <screenshot>\n        <image>https://raw.githubusercontent.com/forkgram/tdesktop/dev/docs/assets/frk/history.png</image>\n        <caption>Original date/time of forwarded messages directly in header of messages</caption>\n      </screenshot>\n      <screenshot>\n        <image>https://raw.githubusercontent.com/forkgram/tdesktop/dev/docs/assets/frk/menu.png</image>\n        <caption>Additional button to jump directly to the first message of a dialog via context menu</caption>\n      </screenshot>\n    </screenshots>\n    <keywords>\n        <keyword>tg</keyword>\n        <keyword>telegram</keyword>\n        <keyword>forkgram</keyword>\n        <keyword>tdesktop</keyword>\n        <keyword>messaging</keyword>\n        <keyword>messenger</keyword>\n        <keyword>chat</keyword>\n        <keyword>sms</keyword>\n        <keyword>im</keyword>\n    </keywords>\n    <requires>\n        <!-- windowMinHeight from Telegram/SourceFiles/window/window.style -->\n        <display_length compare=\"ge\">480</display_length>\n        <internet>always</internet>\n        <!-- on-screen keyboards are too big for current minimal display height -->\n        <control>keyboard</control>\n    </requires>\n    <supports>\n        <control>pointing</control>\n        <control>touch</control>\n    </supports>\n    <content_rating type=\"oars-1.1\">\n        <content_attribute id=\"violence-cartoon\">none</content_attribute>\n        <content_attribute id=\"violence-fantasy\">none</content_attribute>\n        <content_attribute id=\"violence-realistic\">none</content_attribute>\n        <content_attribute id=\"violence-bloodshed\">none</content_attribute>\n        <content_attribute id=\"violence-sexual\">none</content_attribute>\n        <content_attribute id=\"violence-desecration\">none</content_attribute>\n        <content_attribute id=\"violence-slavery\">none</content_attribute>\n        <content_attribute id=\"violence-worship\">none</content_attribute>\n        <content_attribute id=\"drugs-alcohol\">none</content_attribute>\n        <content_attribute id=\"drugs-narcotics\">none</content_attribute>\n        <content_attribute id=\"drugs-tobacco\">none</content_attribute>\n        <content_attribute id=\"sex-nudity\">none</content_attribute>\n        <content_attribute id=\"sex-themes\">none</content_attribute>\n        <content_attribute id=\"sex-homosexuality\">none</content_attribute>\n        <content_attribute id=\"sex-prostitution\">none</content_attribute>\n        <content_attribute id=\"sex-adultery\">none</content_attribute>\n        <content_attribute id=\"sex-appearance\">none</content_attribute>\n        <content_attribute id=\"language-profanity\">none</content_attribute>\n        <content_attribute id=\"language-humor\">none</content_attribute>\n        <content_attribute id=\"language-discrimination\">none</content_attribute>\n        <content_attribute id=\"social-chat\">intense</content_attribute>\n        <content_attribute id=\"social-info\">none</content_attribute>\n        <content_attribute id=\"social-audio\">intense</content_attribute>\n        <content_attribute id=\"social-location\">none</content_attribute>\n        <content_attribute id=\"social-contacts\">intense</content_attribute>\n        <content_attribute id=\"money-purchasing\">intense</content_attribute>\n        <content_attribute id=\"money-gambling\">none</content_attribute>\n        <content_attribute id=\"money-advertising\">moderate</content_attribute>\n    </content_rating>\n    <launchable type=\"desktop-id\">io.github.forkgram.tdesktop.desktop</launchable>\n    <provides>\n        <binary>Forkgram</binary>\n        <dbus type=\"session\">io.github.forkgram.tdesktop</dbus>\n        <mediatype>x-scheme-handler/tg</mediatype>\n        <mediatype>x-scheme-handler/tonsite</mediatype>\n    </provides>\n</component>\n"
  },
  {
    "path": "lib/xdg/org.telegram.desktop.service",
    "content": "[D-BUS Service]\nName=io.github.forkgram.tdesktop\nExec=@CMAKE_INSTALL_FULL_BINDIR@/Forkgram\n"
  },
  {
    "path": "patches/cmake_qt611.patch",
    "content": "From 982e3ecbcf9a874bd5f82433f514f3175a84eeb1 Mon Sep 17 00:00:00 2001\nFrom: 23rd <23rd@vivaldi.net>\nDate: Wed, 1 Apr 2026 09:06:36 +0300\nSubject: [PATCH] 1\n\n---\n external/qt/CMakeLists.txt | 22 ++++++++++++++++++++--\n options_mac.cmake          | 11 +++++++++++\n 2 files changed, 31 insertions(+), 2 deletions(-)\n\ndiff --git a/external/qt/CMakeLists.txt b/external/qt/CMakeLists.txt\nindex 577b67b..9845f55 100644\n--- a/external/qt/CMakeLists.txt\n+++ b/external/qt/CMakeLists.txt\n@@ -234,12 +234,30 @@ elseif (APPLE)\n         lib/${qt_lib_prefix}Qt${QT_VERSION_MAJOR}BundledFreetype\n         plugins/platforms/${qt_lib_prefix}qcocoa\n         plugins/tls/${qt_lib_prefix}qsecuretransportbackend\n-        plugins/networkinformation/${qt_lib_prefix}qscnetworkreachability\n     )\n+    if (QT_VERSION VERSION_GREATER_EQUAL \"6.11\")\n+        list(APPEND qt_libs\n+            plugins/networkinformation/${qt_lib_prefix}qapplenetworkinformation\n+        )\n+    else()\n+        list(APPEND qt_libs\n+            plugins/networkinformation/${qt_lib_prefix}qscnetworkreachability\n+        )\n+    endif()\n     list(APPEND qt_libs_list\n         $<TARGET_OBJECTS:Qt${QT_VERSION_MAJOR}::QCocoaIntegrationPlugin_init>\n         $<TARGET_OBJECTS:Qt${QT_VERSION_MAJOR}::QSecureTransportBackendPlugin_init>\n-        $<TARGET_OBJECTS:Qt${QT_VERSION_MAJOR}::QSCNetworkReachabilityNetworkInformationPlugin_init>\n+    )\n+    if (QT_VERSION VERSION_GREATER_EQUAL \"6.11\")\n+        list(APPEND qt_libs_list\n+            $<TARGET_OBJECTS:Qt${QT_VERSION_MAJOR}::QAppleNetworkInformationPlugin_init>\n+        )\n+    else()\n+        list(APPEND qt_libs_list\n+            $<TARGET_OBJECTS:Qt${QT_VERSION_MAJOR}::QSCNetworkReachabilityNetworkInformationPlugin_init>\n+        )\n+    endif()\n+    list(APPEND qt_libs_list\n         $<TARGET_OBJECTS:Qt${QT_VERSION_MAJOR}::QCocoaIntegrationPlugin_resources_1>\n     )\n     foreach (lib ${qt_libs})\ndiff --git a/options_mac.cmake b/options_mac.cmake\nindex 59494d8..73960ad 100644\n--- a/options_mac.cmake\n+++ b/options_mac.cmake\n@@ -80,3 +80,14 @@ INTERFACE\n     LocalAuthentication\n     Vision\n )\n+\n+target_link_frameworks_weak(common_options\n+INTERFACE\n+    Network\n+    UniformTypeIdentifiers\n+)\n+\n+target_link_libraries(common_options\n+INTERFACE\n+    resolv\n+)\n-- \n2.50.1 (Apple Git-155)\n\n"
  },
  {
    "path": "patches/cmake_zbar.patch",
    "content": "diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt\nindex d370526..f03e6d6 100644\n--- a/external/CMakeLists.txt\n+++ b/external/CMakeLists.txt\n@@ -43,6 +43,7 @@ add_checked_subdirectory(openssl)\n add_checked_subdirectory(opus)\n add_checked_subdirectory(qt)\n add_checked_subdirectory(qr_code_generator)\n+add_checked_subdirectory(zbar)\n add_checked_subdirectory(ranges)\n add_checked_subdirectory(rlottie)\n add_checked_subdirectory(rnnoise)\ndiff --git a/external/zbar/CMakeLists.txt b/external/zbar/CMakeLists.txt\nnew file mode 100644\nindex 0000000..2f3c55c\n--- /dev/null\n+++ b/external/zbar/CMakeLists.txt\n@@ -0,0 +1,111 @@\n+add_library(external_zbar INTERFACE IMPORTED GLOBAL)\n+add_library(desktop-app::external_zbar ALIAS external_zbar)\n+\n+set(zbar_loc ${third_party_loc}/zbar)\n+set(zbar_src ${zbar_loc}/zbar)\n+\n+add_library(external_zbar_bundled STATIC)\n+init_target(external_zbar_bundled \"(external)\")\n+\n+nice_target_sources(external_zbar_bundled ${zbar_src}\n+PRIVATE\n+    config.c\n+    convert.c\n+    decoder.c\n+    decoder.h\n+    error.c\n+    error.h\n+    image.c\n+    image.h\n+    img_scanner.c\n+    img_scanner.h\n+    misc.c\n+    misc.h\n+    refcnt.c\n+    refcnt.h\n+    scanner.c\n+    symbol.c\n+    symbol.h\n+    decoder/qr_finder.c\n+    decoder/qr_finder.h\n+    qrcode/bch15_5.c\n+    qrcode/bch15_5.h\n+    qrcode/binarize.c\n+    qrcode/binarize.h\n+    qrcode/isaac.c\n+    qrcode/isaac.h\n+    qrcode/qrdec.c\n+    qrcode/qrdec.h\n+    qrcode/qrdectxt.c\n+    qrcode/rs.c\n+    qrcode/rs.h\n+    qrcode/util.c\n+    qrcode/util.h\n+)\n+\n+target_include_directories(external_zbar_bundled SYSTEM\n+PUBLIC\n+    ${zbar_loc}/include\n+    ${zbar_src}\n+)\n+\n+target_include_directories(external_zbar_bundled\n+PRIVATE\n+    ${CMAKE_CURRENT_SOURCE_DIR}\n+)\n+\n+if (WIN32)\n+    target_sources(external_zbar_bundled PRIVATE\n+        ${CMAKE_CURRENT_SOURCE_DIR}/win_iconv/win_iconv.c\n+    )\n+    target_include_directories(external_zbar_bundled PRIVATE\n+        ${CMAKE_CURRENT_SOURCE_DIR}/win_iconv\n+    )\n+    target_link_libraries(external_zbar_bundled PRIVATE winmm)\n+elseif (APPLE)\n+    target_link_libraries(external_zbar_bundled PRIVATE\n+        desktop-app::external_iconv\n+    )\n+endif()\n+\n+if (MSVC)\n+    target_compile_options(external_zbar_bundled\n+    PRIVATE\n+        /wd4013\n+        /wd4146\n+        /wd4311\n+        /wd4554\n+        /wd4996\n+    )\n+else()\n+    target_compile_options(external_zbar_bundled PRIVATE -w)\n+endif()\n+\n+target_compile_definitions(external_zbar_bundled\n+PRIVATE\n+    HAVE_CONFIG_H=1\n+    HAVE_INTTYPES_H=1\n+    HAVE_ERRNO_H=1\n+    $<$<NOT:$<PLATFORM_ID:Windows>>:HAVE_SYS_TIME_H=1>\n+    NO_STATS=1\n+    ENABLE_QRCODE=1\n+    ENABLE_EAN=0\n+    ENABLE_DATABAR=0\n+    ENABLE_CODE128=0\n+    ENABLE_CODE93=0\n+    ENABLE_CODE39=0\n+    ENABLE_CODABAR=0\n+    ENABLE_I25=0\n+    ENABLE_PDF417=0\n+    ENABLE_SQCODE=0\n+    ZBAR_VERSION_MAJOR=0\n+    ZBAR_VERSION_MINOR=23\n+    ZBAR_VERSION_PATCH=0\n+    _CRT_SECURE_NO_WARNINGS=1\n+    _CRT_NONSTDC_NO_DEPRECATE=1\n+)\n+\n+target_link_libraries(external_zbar\n+INTERFACE\n+    external_zbar_bundled\n+)\ndiff --git a/external/zbar/config.h b/external/zbar/config.h\nnew file mode 100644\nindex 0000000..0d622d0\n--- /dev/null\n+++ b/external/zbar/config.h\n@@ -0,0 +1,14 @@\n+/* intentionally empty - all config is passed via CMake defines */\n+\n+#if defined(_WIN32) && !defined(timeGetTime)\n+/* WIN32_LEAN_AND_MEAN excludes mmsystem.h from windows.h,\n+   but zbar/timer.h needs timeGetTime() with correct calling\n+   convention (__stdcall) to link on x86. */\n+#ifdef __cplusplus\n+extern \"C\" {\n+#endif\n+__declspec(dllimport) unsigned long __stdcall timeGetTime(void);\n+#ifdef __cplusplus\n+}\n+#endif\n+#endif\ndiff --git a/external/zbar/win_iconv/iconv.h b/external/zbar/win_iconv/iconv.h\nnew file mode 100644\nindex 0000000..b5d08d9\n--- /dev/null\n+++ b/external/zbar/win_iconv/iconv.h\n@@ -0,0 +1,25 @@\n+/*\n+ * Minimal iconv shim for Windows using MultiByteToWideChar/WideCharToMultiByte.\n+ * Covers only the subset used by zbar: one-directional conversion to UTF-8.\n+ */\n+#ifndef WIN_ICONV_H\n+#define WIN_ICONV_H\n+\n+#include <stddef.h>\n+\n+#ifdef __cplusplus\n+extern \"C\" {\n+#endif\n+\n+typedef void *iconv_t;\n+\n+iconv_t iconv_open(const char *tocode, const char *fromcode);\n+size_t  iconv(iconv_t cd, char **inbuf, size_t *inbytesleft,\n+              char **outbuf, size_t *outbytesleft);\n+int     iconv_close(iconv_t cd);\n+\n+#ifdef __cplusplus\n+}\n+#endif\n+\n+#endif /* WIN_ICONV_H */\ndiff --git a/external/zbar/win_iconv/win_iconv.c b/external/zbar/win_iconv/win_iconv.c\nnew file mode 100644\nindex 0000000..b30a8a3\n--- /dev/null\n+++ b/external/zbar/win_iconv/win_iconv.c\n@@ -0,0 +1,112 @@\n+/*\n+ * Minimal iconv implementation for Windows.\n+ * Supports only conversions TO UTF-8 as required by zbar's qrdectxt.c.\n+ * Uses Windows MultiByteToWideChar / WideCharToMultiByte internally.\n+ */\n+#include \"iconv.h\"\n+\n+#include <windows.h>\n+#include <stdlib.h>\n+#include <string.h>\n+#include <errno.h>\n+\n+typedef struct {\n+    unsigned int from_cp;\n+    unsigned int to_cp;\n+} win_iconv_t;\n+\n+static unsigned int name_to_cp(const char *name)\n+{\n+    if (!name)\n+        return (unsigned int)-1;\n+    if (!_stricmp(name, \"UTF-8\") || !_stricmp(name, \"UTF8\"))\n+        return CP_UTF8;\n+    if (!_stricmp(name, \"SJIS\") || !_stricmp(name, \"SHIFT_JIS\")\n+        || !_stricmp(name, \"SHIFT-JIS\"))\n+        return 932;\n+    if (!_stricmp(name, \"BIG-5\") || !_stricmp(name, \"BIG5\"))\n+        return 950;\n+    if (!_stricmp(name, \"CP437\"))\n+        return 437;\n+    /* ISO8859-1 .. ISO8859-16 */\n+    if (!_strnicmp(name, \"ISO8859-\", 8) || !_strnicmp(name, \"ISO-8859-\", 9)) {\n+        const char *p = strrchr(name, '-');\n+        if (p) {\n+            int n = atoi(p + 1);\n+            if (n >= 1 && n <= 16)\n+                return 28590 + n; /* 28591..28606 */\n+        }\n+    }\n+    return (unsigned int)-1;\n+}\n+\n+iconv_t iconv_open(const char *tocode, const char *fromcode)\n+{\n+    unsigned int from_cp = name_to_cp(fromcode);\n+    unsigned int to_cp   = name_to_cp(tocode);\n+    win_iconv_t *cd;\n+    if (from_cp == (unsigned int)-1 || to_cp == (unsigned int)-1) {\n+        errno = EINVAL;\n+        return (iconv_t)-1;\n+    }\n+    cd = (win_iconv_t *)malloc(sizeof(win_iconv_t));\n+    if (!cd) {\n+        errno = ENOMEM;\n+        return (iconv_t)-1;\n+    }\n+    cd->from_cp = from_cp;\n+    cd->to_cp   = to_cp;\n+    return (iconv_t)cd;\n+}\n+\n+size_t iconv(iconv_t cd_opaque, char **inbuf, size_t *inbytesleft,\n+             char **outbuf, size_t *outbytesleft)\n+{\n+    win_iconv_t *cd = (win_iconv_t *)cd_opaque;\n+    int wlen, olen;\n+    wchar_t *wbuf;\n+\n+    if (!inbuf || !*inbuf)\n+        return 0; /* reset request, nothing to do */\n+\n+    /* trivial case: same codepage, just copy */\n+    if (cd->from_cp == cd->to_cp) {\n+        size_t n = *inbytesleft < *outbytesleft ? *inbytesleft : *outbytesleft;\n+        memcpy(*outbuf, *inbuf, n);\n+        *inbuf += n; *inbytesleft -= n;\n+        *outbuf += n; *outbytesleft -= n;\n+        if (*inbytesleft > 0) { errno = E2BIG; return (size_t)-1; }\n+        return 0;\n+    }\n+\n+    /* Step 1: source encoding -> UTF-16 */\n+    wlen = MultiByteToWideChar(cd->from_cp, 0, *inbuf, (int)*inbytesleft,\n+                               NULL, 0);\n+    if (wlen <= 0) { errno = EILSEQ; return (size_t)-1; }\n+    wbuf = (wchar_t *)malloc(wlen * sizeof(wchar_t));\n+    if (!wbuf) { errno = ENOMEM; return (size_t)-1; }\n+    MultiByteToWideChar(cd->from_cp, 0, *inbuf, (int)*inbytesleft, wbuf, wlen);\n+\n+    /* Step 2: UTF-16 -> target encoding */\n+    olen = WideCharToMultiByte(cd->to_cp, 0, wbuf, wlen,\n+                               NULL, 0, NULL, NULL);\n+    if (olen <= 0) { free(wbuf); errno = EILSEQ; return (size_t)-1; }\n+    if ((size_t)olen > *outbytesleft) {\n+        free(wbuf);\n+        errno = E2BIG;\n+        return (size_t)-1;\n+    }\n+    WideCharToMultiByte(cd->to_cp, 0, wbuf, wlen,\n+                        *outbuf, olen, NULL, NULL);\n+    free(wbuf);\n+\n+    *inbuf += *inbytesleft; *inbytesleft = 0;\n+    *outbuf += olen; *outbytesleft -= olen;\n+    return 0;\n+}\n+\n+int iconv_close(iconv_t cd)\n+{\n+    free(cd);\n+    return 0;\n+}\n"
  },
  {
    "path": "snap/snapcraft.yaml",
    "content": "name: forkgram\nadopt-info: forkgram\nicon: Telegram/Resources/art/icon512@2x.png\n\nbase: core24\ngrade: stable\nconfinement: strict\ncompression: lzo\n\napps:\n  forkgram:\n    command: usr/bin/forkgram\n    common-id: io.github.forkgram.tdesktop\n    desktop: usr/share/applications/io.github.forkgram.tdesktop.desktop\n    autostart: forkgram_forkgram.desktop\n    extensions: [gnome]\n    plugs:\n      - audio-playback\n      - audio-record\n      - camera\n      - hardware-observe\n      - home\n      - network\n      - network-bind\n      - network-status\n      - removable-media\n    slots:\n      - mpris\n\nlayout:\n  /var/tmp:\n    type: tmpfs\n\nparts:\n  forkgram:\n    plugin: cmake\n    source: .\n    source-type: git\n    parse-info: [usr/share/metainfo/io.github.forkgram.tdesktop.metainfo.xml]\n    build-environment:\n      - LDFLAGS: ${LDFLAGS:+$LDFLAGS} -Wl,--push-state,--no-as-needed,-ldav1d,--pop-state -s\n    build-packages:\n      - clang\n      - libboost-regex-dev\n      - libheif-dev\n      - libjxl-dev\n      - libssl-dev\n      - libxcb-keysyms1-dev\n      - libxcb-record0-dev\n      - libxcb-screensaver0-dev\n    cmake-generator: Ninja\n    cmake-parameters:\n      - -DCMAKE_BUILD_TYPE=Release\n      - -DCMAKE_INSTALL_PREFIX=/usr\n      - -DTDESKTOP_API_ID=14577864\n      - -DTDESKTOP_API_HASH=54d3ae230fd8f985ce9adccf08fbd9d6\n    override-pull: |\n      craftctl default\n\n      version_file=Telegram/build/version\n      version=$(sed -n \"s/AppVersionStr[ ]\\+\\(.*\\)\\+/\\1/p\" $version_file)\n      beta=$(sed -n \"s/BetaChannel[ ]\\+\\(.*\\)\\+/\\1/p\" $version_file)\n\n      if [ \"$beta\" != \"0\" ]; then\n        version=\"$version-beta\"\n      fi\n\n      version=\"${version}$(git describe --tags | sed 's,^v[^-]\\+,,')\"\n\n      craftctl set version=\"$version\"\n    override-build: |\n      craftctl default\n      mv \"$CRAFT_PART_INSTALL\"/usr/bin/{Forkgram,forkgram}\n\n      APP_ID=io.github.forkgram.tdesktop\n      sed -i \"s/^Icon=$APP_ID$/Icon=snap.$CRAFT_PROJECT_NAME./g\" \"$CRAFT_PART_INSTALL/usr/share/applications/$APP_ID.desktop\"\n      for i in $(find \"$CRAFT_PART_INSTALL/usr/share/icons\" -name \"$APP_ID*.svg\" -o -name \"$APP_ID*.png\"); do\n        PREFIXED=\"$(dirname \"$i\")/snap.$CRAFT_PROJECT_NAME.$(basename \"$i\" | sed \"s/$APP_ID//\")\"\n        mv \"$i\" \"$PREFIXED\"\n        DEST=\"$(echo \"$PREFIXED\" | sed 's@usr/share@meta/gui@')\"\n        DESTDIR=\"$(dirname \"$DEST\")\"\n        mkdir -p \"$DESTDIR\"\n        RELATIVE=\"$(realpath -s --relative-to=\"$DESTDIR\" \"$PREFIXED\")\"\n        ln -sfn \"$RELATIVE\" \"$DEST\"\n      done\n    after:\n      - ada\n      - avif\n      - ffmpeg\n      - mozjpeg\n      - openal\n      - protobuf\n      - qt\n      - rnnoise\n      - tde2e\n      - webrtc\n\n  pkgs:\n    plugin: nil\n    stage-packages:\n      - gstreamer1.0-fdkaac\n      - gstreamer1.0-plugins-bad\n      - libgeoclue-2-0\n      - libopenh264-7\n      - libxcb-cursor0\n      - libxcb-record0\n      - libxcb-screensaver0\n    stage:\n      - ./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/gstreamer-1.0/libgstde265.so\n      - ./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/gstreamer-1.0/libgstfdkaac.so\n      - ./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/gstreamer-1.0/libgstopenh264.so\n      - ./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/gstreamer-1.0/libgstsubenc.so\n      - ./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/gstreamer-1.0/libgstva.so\n      - ./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/gstreamer-1.0/libgstvideoparsersbad.so\n      - ./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libfdk-aac.so*\n      - ./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libgeoclue-2.so*\n      - ./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libopenh264.so*\n      - ./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libxcb-cursor.so*\n      - ./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libxcb-record.so*\n      - ./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libxcb-screensaver.so*\n\n  patches:\n    source: https://github.com/desktop-app/patches.git\n    source-depth: 1\n    source-commit: 4519c85c924b9da81f29d4aac045886f896ee479\n    plugin: nil\n    override-pull: |\n      craftctl default\n      cp -r . \"$CRAFT_STAGE/patches\"\n    override-prime: |\n      rm -rf patches\n\n  ada:\n    source: https://github.com/ada-url/ada.git\n    source-depth: 1\n    source-tag: v3.2.4\n    plugin: cmake\n    cmake-generator: Ninja\n    cmake-parameters:\n      - -DCMAKE_BUILD_TYPE=Release\n      - -DCMAKE_INSTALL_PREFIX=/usr\n      - -DADA_TESTING=OFF\n      - -DADA_TOOLS=OFF\n      - -DADA_INCLUDE_URL_PATTERN=OFF\n    prime: [-./*]\n\n  avif:\n    source: https://github.com/AOMediaCodec/libavif.git\n    source-depth: 1\n    source-tag: v1.3.0\n    plugin: cmake\n    build-packages:\n      - libdav1d-dev\n    cmake-generator: Ninja\n    cmake-parameters:\n      - -DCMAKE_BUILD_TYPE=Release\n      - -DCMAKE_INSTALL_PREFIX=/usr\n      - -DBUILD_SHARED_LIBS=OFF\n      - -DAVIF_CODEC_DAV1D=SYSTEM\n      - -DAVIF_LIBYUV=OFF\n    prime: [-./*]\n\n  ffmpeg:\n    source: https://github.com/FFmpeg/FFmpeg.git\n    source-depth: 1\n    source-branch: n6.1.1\n    plugin: autotools\n    build-packages:\n      - nasm\n      - libdav1d-dev\n      - libopenh264-dev\n      - libopus-dev\n      - libvdpau-dev\n      - libvpx-dev\n      - libxv-dev\n    autotools-configure-parameters:\n      - --prefix=/usr\n      - --libdir=/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR\n      - --extra-cflags=\"-DCONFIG_SAFE_BITSTREAM_READER=1\"\n      - --extra-cxxflags=\"-DCONFIG_SAFE_BITSTREAM_READER=1\"\n      - --disable-debug\n      - --disable-programs\n      - --disable-doc\n      - --disable-network\n      - --disable-autodetect\n      - --disable-everything\n      - --enable-libdav1d\n      - --enable-libopenh264\n      - --enable-libopus\n      - --enable-libvpx\n      - --enable-vaapi\n      - --enable-vdpau\n      - --enable-xlib\n      - --enable-libdrm\n      - --enable-ffnvcodec\n      - --enable-nvdec\n      - --enable-cuvid\n      - --enable-protocol=file\n      - --enable-hwaccel=av1_vaapi\n      - --enable-hwaccel=av1_nvdec\n      - --enable-hwaccel=h264_vaapi\n      - --enable-hwaccel=h264_vdpau\n      - --enable-hwaccel=h264_nvdec\n      - --enable-hwaccel=hevc_vaapi\n      - --enable-hwaccel=hevc_vdpau\n      - --enable-hwaccel=hevc_nvdec\n      - --enable-hwaccel=mpeg2_vaapi\n      - --enable-hwaccel=mpeg2_vdpau\n      - --enable-hwaccel=mpeg2_nvdec\n      - --enable-hwaccel=mpeg4_vaapi\n      - --enable-hwaccel=mpeg4_vdpau\n      - --enable-hwaccel=mpeg4_nvdec\n      - --enable-hwaccel=vp8_vaapi\n      - --enable-hwaccel=vp8_nvdec\n      - --enable-decoder=aac\n      - --enable-decoder=aac_fixed\n      - --enable-decoder=aac_latm\n      - --enable-decoder=aasc\n      - --enable-decoder=ac3\n      - --enable-decoder=alac\n      - --enable-decoder=av1\n      - --enable-decoder=av1_cuvid\n      - --enable-decoder=eac3\n      - --enable-decoder=flac\n      - --enable-decoder=gif\n      - --enable-decoder=h264\n      - --enable-decoder=hevc\n      - --enable-decoder=libdav1d\n      - --enable-decoder=libvpx_vp8\n      - --enable-decoder=libvpx_vp9\n      - --enable-decoder=mp1\n      - --enable-decoder=mp1float\n      - --enable-decoder=mp2\n      - --enable-decoder=mp2float\n      - --enable-decoder=mp3\n      - --enable-decoder=mp3adu\n      - --enable-decoder=mp3adufloat\n      - --enable-decoder=mp3float\n      - --enable-decoder=mp3on4\n      - --enable-decoder=mp3on4float\n      - --enable-decoder=mpeg4\n      - --enable-decoder=msmpeg4v2\n      - --enable-decoder=msmpeg4v3\n      - --enable-decoder=opus\n      - --enable-decoder=pcm_alaw\n      - --enable-decoder=pcm_f32be\n      - --enable-decoder=pcm_f32le\n      - --enable-decoder=pcm_f64be\n      - --enable-decoder=pcm_f64le\n      - --enable-decoder=pcm_lxf\n      - --enable-decoder=pcm_mulaw\n      - --enable-decoder=pcm_s16be\n      - --enable-decoder=pcm_s16be_planar\n      - --enable-decoder=pcm_s16le\n      - --enable-decoder=pcm_s16le_planar\n      - --enable-decoder=pcm_s24be\n      - --enable-decoder=pcm_s24daud\n      - --enable-decoder=pcm_s24le\n      - --enable-decoder=pcm_s24le_planar\n      - --enable-decoder=pcm_s32be\n      - --enable-decoder=pcm_s32le\n      - --enable-decoder=pcm_s32le_planar\n      - --enable-decoder=pcm_s64be\n      - --enable-decoder=pcm_s64le\n      - --enable-decoder=pcm_s8\n      - --enable-decoder=pcm_s8_planar\n      - --enable-decoder=pcm_u16be\n      - --enable-decoder=pcm_u16le\n      - --enable-decoder=pcm_u24be\n      - --enable-decoder=pcm_u24le\n      - --enable-decoder=pcm_u32be\n      - --enable-decoder=pcm_u32le\n      - --enable-decoder=pcm_u8\n      - --enable-decoder=pcm_zork\n      - --enable-decoder=vorbis\n      - --enable-decoder=vp8\n      - --enable-decoder=wavpack\n      - --enable-decoder=wmalossless\n      - --enable-decoder=wmapro\n      - --enable-decoder=wmav1\n      - --enable-decoder=wmav2\n      - --enable-decoder=wmavoice\n      - --enable-encoder=aac\n      - --enable-encoder=libopenh264\n      - --enable-encoder=libopus\n      - --enable-encoder=pcm_s16le\n      - --enable-filter=atempo\n      - --enable-parser=aac\n      - --enable-parser=aac_latm\n      - --enable-parser=flac\n      - --enable-parser=gif\n      - --enable-parser=h264\n      - --enable-parser=hevc\n      - --enable-parser=mpeg4video\n      - --enable-parser=mpegaudio\n      - --enable-parser=opus\n      - --enable-parser=vorbis\n      - --enable-demuxer=aac\n      - --enable-demuxer=flac\n      - --enable-demuxer=gif\n      - --enable-demuxer=h264\n      - --enable-demuxer=hevc\n      - --enable-demuxer=matroska\n      - --enable-demuxer=m4v\n      - --enable-demuxer=mov\n      - --enable-demuxer=mp3\n      - --enable-demuxer=ogg\n      - --enable-demuxer=wav\n      - --enable-muxer=mp4\n      - --enable-muxer=ogg\n      - --enable-muxer=opus\n      - --enable-muxer=wav\n    override-pull: |\n      craftctl default\n      sed -i 's/disable ffnvcodec cuvid nvdec nvenc/:/g' configure\n    prime: [-./*]\n    after:\n      - nv-codec-headers\n\n  mozjpeg:\n    source: https://github.com/mozilla/mozjpeg.git\n    source-depth: 1\n    source-tag: v4.1.5\n    plugin: cmake\n    cmake-generator: Ninja\n    cmake-parameters:\n      - -DCMAKE_BUILD_TYPE=Release\n      - -DCMAKE_INSTALL_PREFIX=/usr\n      - -DENABLE_SHARED=OFF\n      - -DWITH_JPEG8=ON\n      - -DPNG_SUPPORTED=OFF\n    prime: [-./*]\n\n  openal:\n    source: https://github.com/kcat/openal-soft.git\n    source-depth: 1\n    source-tag: 1.24.3\n    plugin: cmake\n    build-packages:\n      - libpipewire-0.3-dev\n    cmake-generator: Ninja\n    cmake-parameters:\n      - -DCMAKE_BUILD_TYPE=Release\n      - -DCMAKE_INSTALL_PREFIX=/usr\n      - -DLIBTYPE=STATIC\n      - -DALSOFT_DLOPEN=OFF\n      - -DALSOFT_EXAMPLES=OFF\n      - -DALSOFT_UTILS=OFF\n      - -DALSOFT_INSTALL_CONFIG=OFF\n      - -DALSOFT_BACKEND_PIPEWIRE=OFF\n    prime: [-./*]\n\n  nv-codec-headers:\n    source: https://github.com/FFmpeg/nv-codec-headers.git\n    source-depth: 1\n    source-branch: n12.1.14.0\n    plugin: make\n    make-parameters:\n      - PREFIX=/usr\n      - LIBDIR=lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR\n    prime: [-./*]\n\n  protobuf:\n    source: https://github.com/protocolbuffers/protobuf.git\n    source-depth: 1\n    source-tag: v30.2\n    plugin: cmake\n    cmake-generator: Ninja\n    cmake-parameters:\n      - -DCMAKE_BUILD_TYPE=Release\n      - -DCMAKE_INSTALL_PREFIX=/usr\n      - -Dprotobuf_BUILD_TESTS=OFF\n    prime: [-./*]\n\n  qt:\n    source: https://github.com/qt/qt5.git\n    source-depth: 1\n    source-tag: v6.11.0\n    source-submodules:\n      - qtbase\n      - qtdeclarative\n      - qtimageformats\n      - qtshadertools\n      - qtsvg\n      - qtwayland\n    plugin: cmake\n    build-packages:\n      - libssl-dev\n      - libxcb-cursor-dev\n      - libxcb-glx0-dev\n      - libxcb-icccm4-dev\n      - libxcb-image0-dev\n      - libxcb-keysyms1-dev\n      - libxcb-randr0-dev\n      - libxcb-render-util0-dev\n      - libxcb-shape0-dev\n      - libxcb-sync-dev\n      - libxcb-util-dev\n      - libxcb-xfixes0-dev\n      - libxcb-xkb-dev\n      - libxkbcommon-x11-dev\n    cmake-generator: Ninja\n    cmake-parameters:\n      - -DCMAKE_BUILD_TYPE=Release\n      - -DCMAKE_INSTALL_PREFIX=/usr\n      - -DINSTALL_LIBDIR=/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR\n      - -DBUILD_SHARED_LIBS=OFF\n      - -DQT_QPA_PLATFORMS=\"wayland;xcb\"\n      - -DFEATURE_eglfs=OFF\n      - -DINPUT_openssl=linked\n    override-pull: |\n      craftctl default\n      QT=\"$(grep 'set(QT_REPO_MODULE_VERSION' qtbase/.cmake.conf | sed -r 's/.*\"(.*)\".*/\\1/')\"\n      cd qtbase\n      find $CRAFT_STAGE/patches/qtbase_${QT} -type f -print0 | sort -z | xargs -r0 git apply\n      cd ../qtwayland\n      find $CRAFT_STAGE/patches/qtwayland_${QT} -type f -print0 | sort -z | xargs -r0 git apply\n      cd ..\n    prime: [-./*]\n    after:\n      - mozjpeg\n      - patches\n\n  rnnoise:\n    source: https://github.com/xiph/rnnoise.git\n    source-depth: 1\n    source-tag: v0.2\n    plugin: autotools\n    build-packages:\n      - wget\n    autotools-configure-parameters:\n      - --prefix=/usr\n      - --libdir=\\${exec_prefix}/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR\n      - --disable-shared\n      - --disable-examples\n      - --disable-doc\n    override-pull: |\n      craftctl default\n      sed -i 's/os_support.h/common.h/g;s/OPUS_CLEAR/RNN_CLEAR/g' src/vec{,_neon}.h\n    prime: [-./*]\n\n  tde2e:\n    source: https://github.com/tdlib/td.git\n    source-depth: 1\n    source-commit: 51743dfd01dff6179e2d8f7095729caa4e2222e9\n    plugin: cmake\n    build-packages:\n      - gperf\n      - libssl-dev\n    cmake-generator: Ninja\n    cmake-parameters:\n      - -DCMAKE_BUILD_TYPE=Release\n      - -DCMAKE_INSTALL_PREFIX=/usr\n      - -DCMAKE_INSTALL_LIBDIR=lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR\n      - -DTD_E2E_ONLY=ON\n    prime: [-./*]\n\n  webrtc:\n    source: https://github.com/desktop-app/tg_owt.git\n    source-depth: 1\n    source-commit: 26068e29bfa8d74a9dc9c8f7f94172fafbc262b8\n    plugin: cmake\n    build-packages:\n      - libopenh264-dev\n      - libopus-dev\n      - libpipewire-0.3-dev\n      - libssl-dev\n      - libvpx-dev\n    cmake-generator: Ninja\n    cmake-parameters:\n      - -DCMAKE_BUILD_TYPE=Release\n      - -DCMAKE_INSTALL_PREFIX=/usr\n    prime: [-./*]\n    after:\n      - ffmpeg\n      - mozjpeg\n"
  }
]